summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJimmy Huang <jimmy.huang@linux.intel.com>2012-08-31 15:21:38 -0700
committerJimmy Huang <jimmy.huang@linux.intel.com>2012-08-31 15:21:38 -0700
commit30d855cbca8385abefa3ffe95811a2bfaa666cf8 (patch)
treea8831781e15d375080111d26806c2e9b825c4c2f
downloadpython-twisted-30d855cbca8385abefa3ffe95811a2bfaa666cf8.tar.gz
python-twisted-30d855cbca8385abefa3ffe95811a2bfaa666cf8.tar.bz2
python-twisted-30d855cbca8385abefa3ffe95811a2bfaa666cf8.zip
Signed-off-by: Jimmy Huang <jimmy.huang@linux.intel.com>
-rw-r--r--INSTALL33
-rw-r--r--LICENSE57
-rw-r--r--NEWS3168
-rw-r--r--README116
-rw-r--r--bin/_preamble.py19
-rwxr-xr-xbin/conch/cftp15
-rwxr-xr-xbin/conch/ckeygen15
-rwxr-xr-xbin/conch/conch15
-rwxr-xr-xbin/conch/tkconch15
-rwxr-xr-xbin/lore/lore16
-rwxr-xr-xbin/mail/mailmail20
-rwxr-xr-xbin/manhole16
-rwxr-xr-xbin/pyhtmlizer12
-rwxr-xr-xbin/tap2deb16
-rwxr-xr-xbin/tap2rpm19
-rwxr-xr-xbin/tapconvert12
-rwxr-xr-xbin/trial18
-rwxr-xr-xbin/twistd14
-rw-r--r--doc/conch/benchmarks/README15
-rwxr-xr-xdoc/conch/benchmarks/buffering_mixin.py182
-rw-r--r--doc/conch/examples/demo.tac25
-rw-r--r--doc/conch/examples/demo_draw.tac80
-rw-r--r--doc/conch/examples/demo_insults.tac252
-rw-r--r--doc/conch/examples/demo_manhole.tac56
-rw-r--r--doc/conch/examples/demo_recvline.tac77
-rw-r--r--doc/conch/examples/demo_scroll.tac100
-rw-r--r--doc/conch/examples/index.html40
-rw-r--r--doc/conch/examples/sshsimpleclient.py111
-rwxr-xr-xdoc/conch/examples/sshsimpleserver.py117
-rw-r--r--doc/conch/examples/telnet_echo.tac47
-rw-r--r--doc/conch/examples/window.tac202
-rw-r--r--doc/conch/howto/conch_client.html321
-rw-r--r--doc/conch/howto/index.html28
-rw-r--r--doc/conch/index.html25
-rw-r--r--doc/conch/man/cftp-man.html87
-rw-r--r--doc/conch/man/cftp.189
-rw-r--r--doc/conch/man/ckeygen-man.html107
-rw-r--r--doc/conch/man/ckeygen.157
-rw-r--r--doc/conch/man/conch-man.html148
-rw-r--r--doc/conch/man/conch.1206
-rw-r--r--doc/conch/man/tkconch-man.html129
-rw-r--r--doc/conch/man/tkconch.172
-rw-r--r--doc/core/benchmarks/banana.py10
-rw-r--r--doc/core/benchmarks/deferreds.py145
-rw-r--r--doc/core/benchmarks/failure.py66
-rw-r--r--doc/core/benchmarks/linereceiver.py47
-rw-r--r--doc/core/benchmarks/netstringreceiver.py242
-rw-r--r--doc/core/benchmarks/task.py26
-rw-r--r--doc/core/benchmarks/timer.py24
-rw-r--r--doc/core/benchmarks/tpclient.py60
-rw-r--r--doc/core/benchmarks/tpclient_nt.py22
-rw-r--r--doc/core/benchmarks/tpserver.py19
-rw-r--r--doc/core/benchmarks/tpserver_nt.py22
-rw-r--r--doc/core/development/index.html26
-rw-r--r--doc/core/development/listings/new_module_template.py12
-rw-r--r--doc/core/development/naming.html38
-rw-r--r--doc/core/development/philosophy.html58
-rw-r--r--doc/core/development/policy/coding-standard.html818
-rw-r--r--doc/core/development/policy/doc-standard.html196
-rw-r--r--doc/core/development/policy/index.html33
-rw-r--r--doc/core/development/policy/svn-dev.html230
-rw-r--r--doc/core/development/policy/test-standard.html399
-rw-r--r--doc/core/development/policy/writing-standard.html313
-rw-r--r--doc/core/development/security.html43
-rw-r--r--doc/core/examples/ampclient.py26
-rw-r--r--doc/core/examples/ampserver.py40
-rw-r--r--doc/core/examples/bananabench.py79
-rw-r--r--doc/core/examples/chatserver.py37
-rw-r--r--doc/core/examples/courier.py111
-rw-r--r--doc/core/examples/cred.py163
-rwxr-xr-xdoc/core/examples/dbcred.py179
-rw-r--r--doc/core/examples/echoclient.py41
-rwxr-xr-xdoc/core/examples/echoclient_ssl.py46
-rw-r--r--doc/core/examples/echoclient_udp.py38
-rw-r--r--doc/core/examples/echoserv.py27
-rw-r--r--doc/core/examples/echoserv_ssl.py30
-rw-r--r--doc/core/examples/echoserv_udp.py19
-rw-r--r--doc/core/examples/filewatch.py17
-rw-r--r--doc/core/examples/ftpclient.py113
-rw-r--r--doc/core/examples/ftpserver.py55
-rw-r--r--doc/core/examples/gpsfix.py78
-rw-r--r--doc/core/examples/index.html125
-rw-r--r--doc/core/examples/longex.py66
-rw-r--r--doc/core/examples/longex2.py101
-rwxr-xr-xdoc/core/examples/mouse.py80
-rw-r--r--doc/core/examples/pb_exceptions.py36
-rw-r--r--doc/core/examples/pbbenchclient.py42
-rw-r--r--doc/core/examples/pbbenchserver.py54
-rw-r--r--doc/core/examples/pbecho.py51
-rw-r--r--doc/core/examples/pbechoclient.py32
-rw-r--r--doc/core/examples/pbgtk2.py122
-rw-r--r--doc/core/examples/pbgtk2login.glade330
-rw-r--r--doc/core/examples/pbinterop.py71
-rw-r--r--doc/core/examples/pbsimple.py16
-rw-r--r--doc/core/examples/pbsimpleclient.py18
-rw-r--r--doc/core/examples/postfix.py29
-rw-r--r--doc/core/examples/ptyserv.py45
-rw-r--r--doc/core/examples/pyui_bg.pngbin0 -> 29913 bytes
-rwxr-xr-xdoc/core/examples/pyuidemo.py39
-rw-r--r--doc/core/examples/recvfd.py90
-rw-r--r--doc/core/examples/rotatinglog.py26
-rw-r--r--doc/core/examples/sendfd.py83
-rw-r--r--doc/core/examples/server.pem36
-rw-r--r--doc/core/examples/shaper.py52
-rw-r--r--doc/core/examples/shoutcast.py26
-rw-r--r--doc/core/examples/simple.tac39
-rw-r--r--doc/core/examples/simpleclient.py49
-rw-r--r--doc/core/examples/simpleserv.py26
-rw-r--r--doc/core/examples/stdin.py30
-rw-r--r--doc/core/examples/stdiodemo.py78
-rw-r--r--doc/core/examples/streaming.py103
-rw-r--r--doc/core/examples/testlogging.py41
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/classes.nib13
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/info.nib24
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/keyedobjects.nibbin0 -> 14896 bytes
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/README.txt6
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/Twistzilla.py79
-rw-r--r--doc/core/examples/threadedselect/Cocoa/SimpleWebClient/setup.py14
-rw-r--r--doc/core/examples/threadedselect/README15
-rw-r--r--doc/core/examples/threadedselect/blockingdemo.py92
-rw-r--r--doc/core/examples/threadedselect/pygamedemo.py78
-rw-r--r--doc/core/examples/tkinterdemo.py44
-rw-r--r--doc/core/examples/twistd-logging.tac33
-rw-r--r--doc/core/examples/wxacceptance.py113
-rw-r--r--doc/core/examples/wxdemo.py64
-rw-r--r--doc/core/howto/amp.html349
-rw-r--r--doc/core/howto/application.html398
-rw-r--r--doc/core/howto/basics.html100
-rw-r--r--doc/core/howto/book.tex129
-rw-r--r--doc/core/howto/choosing-reactor.html395
-rw-r--r--doc/core/howto/clients.html741
-rw-r--r--doc/core/howto/components.html603
-rw-r--r--doc/core/howto/constants.html456
-rw-r--r--doc/core/howto/cred.html566
-rw-r--r--doc/core/howto/debug-with-emacs.html65
-rw-r--r--doc/core/howto/defer.html898
-rw-r--r--doc/core/howto/design.html254
-rw-r--r--doc/core/howto/dirdbm.html76
-rw-r--r--doc/core/howto/endpoints.html231
-rw-r--r--doc/core/howto/gendefer.html411
-rw-r--r--doc/core/howto/glossary.html334
-rw-r--r--doc/core/howto/howto.tidyrc6
-rw-r--r--doc/core/howto/index.html239
-rw-r--r--doc/core/howto/internet-overview.html48
-rw-r--r--doc/core/howto/listings/TwistedQuotes/__init__.py3
-rw-r--r--doc/core/howto/listings/TwistedQuotes/pbquote.py10
-rw-r--r--doc/core/howto/listings/TwistedQuotes/pbquoteclient.py32
-rw-r--r--doc/core/howto/listings/TwistedQuotes/quoteproto.py36
-rw-r--r--doc/core/howto/listings/TwistedQuotes/quoters.py39
-rw-r--r--doc/core/howto/listings/TwistedQuotes/quotes.txt15
-rw-r--r--doc/core/howto/listings/TwistedQuotes/quotetap.py29
-rw-r--r--doc/core/howto/listings/TwistedQuotes/quotetap2.py36
-rw-r--r--doc/core/howto/listings/TwistedQuotes/webquote.rpy12
-rw-r--r--doc/core/howto/listings/amp/basic_client.py30
-rw-r--r--doc/core/howto/listings/amp/basic_server.tac14
-rw-r--r--doc/core/howto/listings/amp/command_client.py48
-rw-r--r--doc/core/howto/listings/application/service.tac34
-rw-r--r--doc/core/howto/listings/deferred/synch-validation.py5
-rwxr-xr-xdoc/core/howto/listings/pb/cache_classes.py43
-rwxr-xr-xdoc/core/howto/listings/pb/cache_receiver.py28
-rwxr-xr-xdoc/core/howto/listings/pb/cache_sender.py50
-rwxr-xr-xdoc/core/howto/listings/pb/chatclient.py42
-rwxr-xr-xdoc/core/howto/listings/pb/chatserver.py65
-rwxr-xr-xdoc/core/howto/listings/pb/copy2_classes.py29
-rwxr-xr-xdoc/core/howto/listings/pb/copy2_receiver.py21
-rwxr-xr-xdoc/core/howto/listings/pb/copy2_sender.py44
-rwxr-xr-xdoc/core/howto/listings/pb/copy_receiver.tac41
-rwxr-xr-xdoc/core/howto/listings/pb/copy_sender.py57
-rwxr-xr-xdoc/core/howto/listings/pb/exc_client.py33
-rwxr-xr-xdoc/core/howto/listings/pb/exc_server.py32
-rwxr-xr-xdoc/core/howto/listings/pb/pb1client.py31
-rwxr-xr-xdoc/core/howto/listings/pb/pb1server.py20
-rwxr-xr-xdoc/core/howto/listings/pb/pb2client.py36
-rwxr-xr-xdoc/core/howto/listings/pb/pb2server.py30
-rwxr-xr-xdoc/core/howto/listings/pb/pb3client.py26
-rwxr-xr-xdoc/core/howto/listings/pb/pb3server.py16
-rwxr-xr-xdoc/core/howto/listings/pb/pb4client.py58
-rwxr-xr-xdoc/core/howto/listings/pb/pb5client.py22
-rwxr-xr-xdoc/core/howto/listings/pb/pb5server.py29
-rwxr-xr-xdoc/core/howto/listings/pb/pb6client1.py22
-rwxr-xr-xdoc/core/howto/listings/pb/pb6client2.py25
-rwxr-xr-xdoc/core/howto/listings/pb/pb6server.py30
-rwxr-xr-xdoc/core/howto/listings/pb/pb7client.py29
-rwxr-xr-xdoc/core/howto/listings/pb/pbAnonClient.py70
-rwxr-xr-xdoc/core/howto/listings/pb/pbAnonServer.py91
-rwxr-xr-xdoc/core/howto/listings/pb/trap_client.py88
-rwxr-xr-xdoc/core/howto/listings/pb/trap_server.py21
-rwxr-xr-xdoc/core/howto/listings/process/process.py46
-rw-r--r--doc/core/howto/listings/process/quotes.py25
-rw-r--r--doc/core/howto/listings/process/trueandfalse.py14
-rw-r--r--doc/core/howto/listings/sendmsg/copy_descriptor.py35
-rw-r--r--doc/core/howto/listings/sendmsg/send_replacement.py21
-rw-r--r--doc/core/howto/listings/servers/chat.py51
-rw-r--r--doc/core/howto/listings/trial/calculus/__init__.py0
-rw-r--r--doc/core/howto/listings/trial/calculus/base_1.py16
-rw-r--r--doc/core/howto/listings/trial/calculus/base_2.py14
-rw-r--r--doc/core/howto/listings/trial/calculus/base_3.py24
-rw-r--r--doc/core/howto/listings/trial/calculus/client_1.py39
-rw-r--r--doc/core/howto/listings/trial/calculus/client_2.py54
-rw-r--r--doc/core/howto/listings/trial/calculus/client_3.py53
-rw-r--r--doc/core/howto/listings/trial/calculus/remote_1.py47
-rw-r--r--doc/core/howto/listings/trial/calculus/remote_2.py51
-rw-r--r--doc/core/howto/listings/trial/calculus/test/__init__.py0
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_base_1.py23
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_base_2.py29
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_base_2b.py29
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_base_3.py52
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_client_1.py37
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_client_2.py48
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_client_3.py63
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_remote_1.py34
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_remote_2.py46
-rw-r--r--doc/core/howto/listings/trial/calculus/test/test_remote_3.py40
-rw-r--r--doc/core/howto/listings/udp/MulticastClient.py19
-rw-r--r--doc/core/howto/listings/udp/MulticastServer.py28
-rw-r--r--doc/core/howto/logging.html196
-rw-r--r--doc/core/howto/options.html581
-rw-r--r--doc/core/howto/pb-clients.html362
-rw-r--r--doc/core/howto/pb-copyable.html1185
-rw-r--r--doc/core/howto/pb-cred.html1724
-rw-r--r--doc/core/howto/pb-intro.html320
-rw-r--r--doc/core/howto/pb-limits.html51
-rw-r--r--doc/core/howto/pb-usage.html1156
-rw-r--r--doc/core/howto/pb.html52
-rw-r--r--doc/core/howto/plugin.html294
-rw-r--r--doc/core/howto/process.html732
-rw-r--r--doc/core/howto/producers.html90
-rw-r--r--doc/core/howto/quotes.html214
-rw-r--r--doc/core/howto/rdbms.html228
-rw-r--r--doc/core/howto/reactor-basics.html93
-rw-r--r--doc/core/howto/sendmsg.html221
-rw-r--r--doc/core/howto/servers.html548
-rw-r--r--doc/core/howto/ssl.html550
-rw-r--r--doc/core/howto/stylesheet-unprocessed.css20
-rw-r--r--doc/core/howto/stylesheet.css189
-rw-r--r--doc/core/howto/tap.html323
-rw-r--r--doc/core/howto/template.tpl23
-rw-r--r--doc/core/howto/testing.html167
-rw-r--r--doc/core/howto/threading.html213
-rw-r--r--doc/core/howto/time.html118
-rw-r--r--doc/core/howto/trial.html2042
-rw-r--r--doc/core/howto/tutorial/backends.html1348
-rw-r--r--doc/core/howto/tutorial/client.html260
-rw-r--r--doc/core/howto/tutorial/components.html1132
-rw-r--r--doc/core/howto/tutorial/configuration.html870
-rw-r--r--doc/core/howto/tutorial/factory.html713
-rw-r--r--doc/core/howto/tutorial/index.html83
-rw-r--r--doc/core/howto/tutorial/intro.html725
-rw-r--r--doc/core/howto/tutorial/library.html271
-rw-r--r--doc/core/howto/tutorial/listings/finger/etc.users2
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger/__init__.py3
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger/finger.py368
-rw-r--r--doc/core/howto/tutorial/listings/finger/finger/tap.py20
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger01.py2
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger02.py10
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger03.py11
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger04.py12
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger05.py13
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger06.py18
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger07.py21
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger08.py30
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger09.py26
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger10.py30
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger11.tac34
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger12.tac55
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger13.tac59
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger14.tac56
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger15.tac87
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger16.tac101
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger17.tac102
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger18.tac147
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger19.tac270
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger19a.tac231
-rw-r--r--doc/core/howto/tutorial/listings/finger/finger19a_changes.py29
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger19b.tac292
-rw-r--r--doc/core/howto/tutorial/listings/finger/finger19b_changes.py19
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger19c.tac305
-rw-r--r--doc/core/howto/tutorial/listings/finger/finger19c_changes.py32
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger20.tac285
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger21.tac319
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/finger22.py337
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/fingerPBclient.py26
-rwxr-xr-xdoc/core/howto/tutorial/listings/finger/fingerXRclient.py5
-rw-r--r--doc/core/howto/tutorial/listings/finger/finger_config.py38
-rw-r--r--doc/core/howto/tutorial/listings/finger/fingerproxy.tac110
-rw-r--r--doc/core/howto/tutorial/listings/finger/organized-finger.tac31
-rw-r--r--doc/core/howto/tutorial/listings/finger/simple-finger.tac17
-rw-r--r--doc/core/howto/tutorial/listings/finger/twisted/plugins/finger_tutorial.py5
-rw-r--r--doc/core/howto/tutorial/pb.html728
-rw-r--r--doc/core/howto/tutorial/protocol.html1121
-rw-r--r--doc/core/howto/tutorial/style.html333
-rw-r--r--doc/core/howto/tutorial/web.html610
-rw-r--r--doc/core/howto/udp.html304
-rw-r--r--doc/core/howto/vision.html43
-rw-r--r--doc/core/img/TwistedLogo.bmpbin0 -> 55494 bytes
-rw-r--r--doc/core/img/cred-login.diabin0 -> 2369 bytes
-rw-r--r--doc/core/img/cred-login.pngbin0 -> 34148 bytes
-rw-r--r--doc/core/img/deferred-attach.diabin0 -> 2234 bytes
-rw-r--r--doc/core/img/deferred-attach.pngbin0 -> 9356 bytes
-rw-r--r--doc/core/img/deferred-process.diabin0 -> 2099 bytes
-rw-r--r--doc/core/img/deferred-process.pngbin0 -> 10809 bytes
-rw-r--r--doc/core/img/deferred-states.svg3
-rw-r--r--doc/core/img/deferred.diabin0 -> 4348 bytes
-rw-r--r--doc/core/img/deferred.pngbin0 -> 33282 bytes
-rw-r--r--doc/core/index.html31
-rw-r--r--doc/core/man/manhole-man.html50
-rw-r--r--doc/core/man/manhole.116
-rw-r--r--doc/core/man/pyhtmlizer-man.html51
-rw-r--r--doc/core/man/pyhtmlizer.122
-rw-r--r--doc/core/man/tap2deb-man.html101
-rw-r--r--doc/core/man/tap2deb.155
-rw-r--r--doc/core/man/tap2rpm-man.html100
-rw-r--r--doc/core/man/tap2rpm.155
-rw-r--r--doc/core/man/tapconvert-man.html82
-rw-r--r--doc/core/man/tapconvert.140
-rw-r--r--doc/core/man/trial-man.html275
-rw-r--r--doc/core/man/trial.1200
-rw-r--r--doc/core/man/twistd-man.html187
-rw-r--r--doc/core/man/twistd.1118
-rw-r--r--doc/core/specifications/banana.html199
-rw-r--r--doc/core/specifications/index.html21
-rw-r--r--doc/fun/Twisted.Quotes0
-rw-r--r--doc/fun/lightbulb7
-rw-r--r--doc/fun/register.html77
-rw-r--r--doc/historic/2002/ipc10/twisted-network-framework/errata.html256
-rw-r--r--doc/historic/2002/ipc10/twisted-network-framework/index.html1568
-rw-r--r--doc/historic/2003/europython/doanddont.html508
-rw-r--r--doc/historic/2003/europython/index.html35
-rw-r--r--doc/historic/2003/europython/lore.html502
-rw-r--r--doc/historic/2003/europython/slides-template.tpl19
-rw-r--r--doc/historic/2003/europython/tw-deploy.html1106
-rw-r--r--doc/historic/2003/europython/twisted.html608
-rw-r--r--doc/historic/2003/europython/webclients.html482
-rw-r--r--doc/historic/2003/haifux/haifux.html2235
-rw-r--r--doc/historic/2003/haifux/notes.html60
-rwxr-xr-xdoc/historic/2003/pycon/applications/applications257
-rw-r--r--doc/historic/2003/pycon/applications/applications.html343
-rw-r--r--doc/historic/2003/pycon/applications/pynfo-chart.pngbin0 -> 13018 bytes
-rwxr-xr-xdoc/historic/2003/pycon/conch/conch98
-rw-r--r--doc/historic/2003/pycon/conch/conch.html165
-rwxr-xr-xdoc/historic/2003/pycon/conch/conchtalk.txt144
-rw-r--r--doc/historic/2003/pycon/conch/smalltwisted.pngbin0 -> 1472 bytes
-rw-r--r--doc/historic/2003/pycon/conch/twistedlogo.pngbin0 -> 7256 bytes
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-bad-adding.py8
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-chaining.py13
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-complex-failure.py30
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-complex-raise.py12
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-forwarding.py9
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-listing0.py18
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-listing1.py6
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-listing2.py8
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-simple-failure.py9
-rw-r--r--doc/historic/2003/pycon/deferex/deferex-simple-raise.py3
-rw-r--r--doc/historic/2003/pycon/deferex/deferex.html499
-rw-r--r--doc/historic/2003/pycon/deferex/deferexex.py16
-rw-r--r--doc/historic/2003/pycon/intrinsics-lightning/intrinsics-lightning97
-rwxr-xr-xdoc/historic/2003/pycon/lore/lore-presentation108
-rwxr-xr-xdoc/historic/2003/pycon/lore/lore-slides.html187
-rw-r--r--doc/historic/2003/pycon/lore/lore.html791
-rwxr-xr-xdoc/historic/2003/pycon/pb/pb-client1.py46
-rwxr-xr-xdoc/historic/2003/pycon/pb/pb-server1.py16
-rwxr-xr-xdoc/historic/2003/pycon/pb/pb-slides.py240
-rw-r--r--doc/historic/2003/pycon/pb/pb.html966
-rwxr-xr-xdoc/historic/2003/pycon/releasing/releasing-twisted151
-rw-r--r--doc/historic/2003/pycon/releasing/releasing.html491
-rwxr-xr-xdoc/historic/2003/pycon/tw-deploy/tw-deploy184
-rw-r--r--doc/historic/2003/pycon/tw-deploy/twisted-overview.pngbin0 -> 12722 bytes
-rw-r--r--doc/historic/2003/pycon/tw-deploy/twistedlogo.pngbin0 -> 7256 bytes
-rw-r--r--doc/historic/2003/pycon/twisted-internet/twisted-internet.py541
-rw-r--r--doc/historic/2003/pycon/twisted-reality/componentized.svg254
-rw-r--r--doc/historic/2003/pycon/twisted-reality/twisted-reality.html578
-rw-r--r--doc/historic/2004/ibm/talk.html495
-rw-r--r--doc/historic/FirstTenYears.Quotes5816
-rw-r--r--doc/historic/Twisted-12.1.0.Quotes24
-rw-r--r--doc/historic/index.html128
-rw-r--r--doc/historic/ipc10errata.html256
-rw-r--r--doc/historic/ipc10paper.html1568
-rw-r--r--doc/historic/stylesheet.css178
-rw-r--r--doc/historic/template-notoc.tpl14
-rw-r--r--doc/historic/template.tpl20
-rw-r--r--doc/historic/twisted-debian.html96
-rw-r--r--doc/index.xhtml115
-rw-r--r--doc/lore/examples/example.html60
-rw-r--r--doc/lore/examples/index.html22
-rw-r--r--doc/lore/examples/slides-template.tpl21
-rw-r--r--doc/lore/howto/extend-lore.html427
-rw-r--r--doc/lore/howto/index.html23
-rw-r--r--doc/lore/howto/listings/lore/1st_example.html12
-rw-r--r--doc/lore/howto/listings/lore/a_lore_plugin.py11
-rw-r--r--doc/lore/howto/listings/lore/factory.py-19
-rw-r--r--doc/lore/howto/listings/lore/factory.py-220
-rw-r--r--doc/lore/howto/listings/lore/factory.py-321
-rw-r--r--doc/lore/howto/listings/lore/spitters.py-118
-rw-r--r--doc/lore/howto/listings/lore/spitters.py-226
-rw-r--r--doc/lore/howto/lore.html369
-rw-r--r--doc/lore/img/myhtml-output.pngbin0 -> 23124 bytes
-rw-r--r--doc/lore/index.html25
-rw-r--r--doc/lore/man/lore-man.html124
-rw-r--r--doc/lore/man/lore.174
-rw-r--r--doc/mail/examples/emailserver.tac107
-rw-r--r--doc/mail/examples/imap4client.py181
-rw-r--r--doc/mail/examples/index.html36
-rw-r--r--doc/mail/examples/smtpclient_simple.py47
-rw-r--r--doc/mail/examples/smtpclient_tls.py157
-rw-r--r--doc/mail/index.html25
-rw-r--r--doc/mail/man/mailmail-man.html55
-rw-r--r--doc/mail/man/mailmail.121
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-1.tac3
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-10.tac56
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-11.tac58
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-2.tac10
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-3.tac10
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-4.tac12
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-5.tac14
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-6.tac18
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-7.tac46
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-8.tac49
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient-9.tac53
-rw-r--r--doc/mail/tutorial/smtpclient/smtpclient.html757
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-1.tac3
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-2.tac10
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-3.tac12
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-4.tac14
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-5.tac50
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-6.tac57
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-7.tac57
-rw-r--r--doc/mail/tutorial/smtpserver/smtpserver-8.tac63
-rwxr-xr-xdoc/names/examples/dns-service.py45
-rwxr-xr-xdoc/names/examples/gethostbyname.py28
-rw-r--r--doc/names/examples/index.html24
-rw-r--r--doc/names/examples/testdns.py48
-rw-r--r--doc/names/howto/index.html22
-rw-r--r--doc/names/howto/listings/names/example-domain.com37
-rw-r--r--doc/names/howto/names.html134
-rw-r--r--doc/names/index.html25
-rw-r--r--doc/pair/examples/index.html23
-rw-r--r--doc/pair/examples/pairudp.py21
-rw-r--r--doc/pair/howto/index.html27
-rw-r--r--doc/pair/howto/twisted-pair.html79
-rw-r--r--doc/pair/index.html25
-rw-r--r--doc/stylesheet.css189
-rw-r--r--doc/web/examples/advogato.py46
-rw-r--r--doc/web/examples/dlpage.py23
-rw-r--r--doc/web/examples/fortune.rpy.py49
-rw-r--r--doc/web/examples/getpage.py20
-rw-r--r--doc/web/examples/google.py21
-rw-r--r--doc/web/examples/hello.rpy.py38
-rw-r--r--doc/web/examples/httpclient.py72
-rw-r--r--doc/web/examples/index.html100
-rw-r--r--doc/web/examples/lj.rpy.py48
-rw-r--r--doc/web/examples/logging-proxy.py45
-rw-r--r--doc/web/examples/proxy.py26
-rw-r--r--doc/web/examples/report.rpy.py44
-rw-r--r--doc/web/examples/reverse-proxy.py18
-rw-r--r--doc/web/examples/rootscript.py41
-rw-r--r--doc/web/examples/silly-web.py28
-rw-r--r--doc/web/examples/simple.rtl32
-rw-r--r--doc/web/examples/soap.py44
-rw-r--r--doc/web/examples/users.rpy.py24
-rw-r--r--doc/web/examples/web.py29
-rw-r--r--doc/web/examples/webguard.py64
-rw-r--r--doc/web/examples/xmlrpc.py80
-rw-r--r--doc/web/examples/xmlrpcclient.py31
-rw-r--r--doc/web/howto/client.html1088
-rw-r--r--doc/web/howto/glossary.html42
-rw-r--r--doc/web/howto/index.html52
-rw-r--r--doc/web/howto/listings/client/cookies.py25
-rw-r--r--doc/web/howto/listings/client/filesendbody.py26
-rw-r--r--doc/web/howto/listings/client/gzipdecoder.py43
-rw-r--r--doc/web/howto/listings/client/request.py21
-rw-r--r--doc/web/howto/listings/client/response.py47
-rw-r--r--doc/web/howto/listings/client/sendbody.py24
-rw-r--r--doc/web/howto/listings/client/stringprod.py21
-rw-r--r--doc/web/howto/listings/element_1.py13
-rw-r--r--doc/web/howto/listings/element_2.py13
-rw-r--r--doc/web/howto/listings/element_3.py13
-rw-r--r--doc/web/howto/listings/iteration-1.py17
-rw-r--r--doc/web/howto/listings/iteration-1.xml3
-rw-r--r--doc/web/howto/listings/iteration-output-1.xml3
-rw-r--r--doc/web/howto/listings/output-1.html9
-rw-r--r--doc/web/howto/listings/output-2.html9
-rw-r--r--doc/web/howto/listings/output-3.html9
-rw-r--r--doc/web/howto/listings/quoting-output.html9
-rw-r--r--doc/web/howto/listings/quoting_element.py13
-rw-r--r--doc/web/howto/listings/render_1.py5
-rw-r--r--doc/web/howto/listings/render_2.py5
-rw-r--r--doc/web/howto/listings/render_3.py5
-rw-r--r--doc/web/howto/listings/render_quoting.py5
-rw-r--r--doc/web/howto/listings/render_slots_attrs.py5
-rw-r--r--doc/web/howto/listings/render_transparent.py5
-rw-r--r--doc/web/howto/listings/slots-attributes-1.xml6
-rw-r--r--doc/web/howto/listings/slots-attributes-output.html4
-rw-r--r--doc/web/howto/listings/slots_attributes_1.py12
-rw-r--r--doc/web/howto/listings/soap.rpy13
-rw-r--r--doc/web/howto/listings/subviews-1.py27
-rw-r--r--doc/web/howto/listings/subviews-1.xml3
-rw-r--r--doc/web/howto/listings/subviews-output-1.xml3
-rw-r--r--doc/web/howto/listings/template-1.xml9
-rw-r--r--doc/web/howto/listings/transparent-1.xml6
-rw-r--r--doc/web/howto/listings/transparent-output.html5
-rw-r--r--doc/web/howto/listings/transparent_element.py13
-rw-r--r--doc/web/howto/listings/wait_for_it.py32
-rw-r--r--doc/web/howto/listings/waited-for-it.html8
-rw-r--r--doc/web/howto/listings/waited-for-it.txt8
-rw-r--r--doc/web/howto/listings/webquote.rtl20
-rw-r--r--doc/web/howto/listings/xmlAndSoapQuote.py25
-rw-r--r--doc/web/howto/listings/xmlquote.rpy12
-rw-r--r--doc/web/howto/listings/xmlrpc-customized.py60
-rw-r--r--doc/web/howto/resource-templates.html108
-rw-r--r--doc/web/howto/twisted-templates.html704
-rw-r--r--doc/web/howto/using-twistedweb.html1074
-rw-r--r--doc/web/howto/web-development.html107
-rw-r--r--doc/web/howto/web-in-60/asynchronous-deferred.html173
-rw-r--r--doc/web/howto/web-in-60/asynchronous.html128
-rw-r--r--doc/web/howto/web-in-60/custom-codes.html116
-rw-r--r--doc/web/howto/web-in-60/dynamic-content.html124
-rw-r--r--doc/web/howto/web-in-60/dynamic-dispatch.html143
-rw-r--r--doc/web/howto/web-in-60/error-handling.html130
-rw-r--r--doc/web/howto/web-in-60/handling-posts.html142
-rw-r--r--doc/web/howto/web-in-60/http-auth.html256
-rw-r--r--doc/web/howto/web-in-60/index.html44
-rw-r--r--doc/web/howto/web-in-60/interrupted.html147
-rw-r--r--doc/web/howto/web-in-60/logging-errors.html107
-rw-r--r--doc/web/howto/web-in-60/rpy-scripts.html90
-rw-r--r--doc/web/howto/web-in-60/session-basics.html121
-rw-r--r--doc/web/howto/web-in-60/session-endings.html170
-rw-r--r--doc/web/howto/web-in-60/session-store.html181
-rw-r--r--doc/web/howto/web-in-60/static-content.html102
-rw-r--r--doc/web/howto/web-in-60/static-dispatch.html119
-rw-r--r--doc/web/howto/web-in-60/wsgi.html125
-rw-r--r--doc/web/howto/web-overview.html67
-rw-r--r--doc/web/howto/xmlrpc.html651
-rw-r--r--doc/web/img/controller.pngbin0 -> 14934 bytes
-rw-r--r--doc/web/img/livepage.pngbin0 -> 9363 bytes
-rw-r--r--doc/web/img/model.pngbin0 -> 14971 bytes
-rw-r--r--doc/web/img/plone_root_model.pngbin0 -> 11214 bytes
-rw-r--r--doc/web/img/view.pngbin0 -> 14703 bytes
-rw-r--r--doc/web/img/web-overview.diabin0 -> 1630 bytes
-rw-r--r--doc/web/img/web-overview.pngbin0 -> 7330 bytes
-rw-r--r--doc/web/img/web-process.pngbin0 -> 30404 bytes
-rw-r--r--doc/web/img/web-process.svg594
-rw-r--r--doc/web/img/web-session.pngbin0 -> 8966 bytes
-rw-r--r--doc/web/img/web-widgets.diabin0 -> 1326 bytes
-rw-r--r--doc/web/img/web-widgets.pngbin0 -> 3147 bytes
-rw-r--r--doc/web/index.html25
-rw-r--r--doc/words/examples/cursesclient.py188
-rw-r--r--doc/words/examples/index.html29
-rw-r--r--doc/words/examples/ircLogBot.py158
-rw-r--r--doc/words/examples/jabber_client.py29
-rw-r--r--doc/words/examples/minchat.py140
-rw-r--r--doc/words/examples/msn_example.py67
-rwxr-xr-xdoc/words/examples/oscardemo.py100
-rw-r--r--doc/words/examples/pb_client.py102
-rw-r--r--doc/words/examples/xmpp_client.py82
-rw-r--r--doc/words/howto/im.html98
-rw-r--r--doc/words/howto/index.html22
-rw-r--r--doc/words/index.html25
-rw-r--r--packaging/python-twisted.changes2
-rw-r--r--packaging/python-twisted.spec63
-rwxr-xr-xsetup.py105
-rw-r--r--twisted/__init__.py24
-rw-r--r--twisted/_version.py3
-rw-r--r--twisted/application/__init__.py7
-rw-r--r--twisted/application/app.py674
-rw-r--r--twisted/application/internet.py408
-rw-r--r--twisted/application/reactors.py83
-rw-r--r--twisted/application/service.py413
-rw-r--r--twisted/application/strports.py103
-rw-r--r--twisted/application/test/__init__.py6
-rw-r--r--twisted/application/test/test_internet.py252
-rw-r--r--twisted/conch/__init__.py18
-rw-r--r--twisted/conch/_version.py3
-rw-r--r--twisted/conch/avatar.py37
-rw-r--r--twisted/conch/checkers.py308
-rw-r--r--twisted/conch/client/__init__.py9
-rw-r--r--twisted/conch/client/agent.py73
-rw-r--r--twisted/conch/client/connect.py21
-rw-r--r--twisted/conch/client/default.py256
-rw-r--r--twisted/conch/client/direct.py107
-rw-r--r--twisted/conch/client/knownhosts.py478
-rw-r--r--twisted/conch/client/options.py96
-rw-r--r--twisted/conch/error.py102
-rw-r--r--twisted/conch/insults/__init__.py16
-rw-r--r--twisted/conch/insults/client.py138
-rw-r--r--twisted/conch/insults/colors.py29
-rw-r--r--twisted/conch/insults/helper.py450
-rw-r--r--twisted/conch/insults/insults.py1087
-rw-r--r--twisted/conch/insults/text.py186
-rw-r--r--twisted/conch/insults/window.py868
-rw-r--r--twisted/conch/interfaces.py402
-rw-r--r--twisted/conch/ls.py75
-rw-r--r--twisted/conch/manhole.py340
-rw-r--r--twisted/conch/manhole_ssh.py146
-rw-r--r--twisted/conch/manhole_tap.py124
-rw-r--r--twisted/conch/mixin.py49
-rw-r--r--twisted/conch/openssh_compat/__init__.py11
-rw-r--r--twisted/conch/openssh_compat/factory.py73
-rw-r--r--twisted/conch/openssh_compat/primes.py26
-rw-r--r--twisted/conch/recvline.py329
-rw-r--r--twisted/conch/scripts/__init__.py1
-rw-r--r--twisted/conch/scripts/cftp.py832
-rw-r--r--twisted/conch/scripts/ckeygen.py190
-rw-r--r--twisted/conch/scripts/conch.py512
-rw-r--r--twisted/conch/scripts/tkconch.py576
-rw-r--r--twisted/conch/ssh/__init__.py10
-rw-r--r--twisted/conch/ssh/agent.py294
-rw-r--r--twisted/conch/ssh/channel.py281
-rw-r--r--twisted/conch/ssh/common.py117
-rw-r--r--twisted/conch/ssh/connection.py637
-rw-r--r--twisted/conch/ssh/factory.py141
-rw-r--r--twisted/conch/ssh/filetransfer.py934
-rwxr-xr-xtwisted/conch/ssh/forwarding.py181
-rw-r--r--twisted/conch/ssh/keys.py780
-rw-r--r--twisted/conch/ssh/service.py48
-rwxr-xr-xtwisted/conch/ssh/session.py348
-rw-r--r--twisted/conch/ssh/sexpy.py42
-rw-r--r--twisted/conch/ssh/transport.py1591
-rw-r--r--twisted/conch/ssh/userauth.py846
-rw-r--r--twisted/conch/stdio.py95
-rw-r--r--twisted/conch/tap.py87
-rw-r--r--twisted/conch/telnet.py1086
-rw-r--r--twisted/conch/test/__init__.py1
-rw-r--r--twisted/conch/test/keydata.py174
-rw-r--r--twisted/conch/test/test_agent.py399
-rw-r--r--twisted/conch/test/test_cftp.py975
-rw-r--r--twisted/conch/test/test_channel.py279
-rw-r--r--twisted/conch/test/test_checkers.py609
-rw-r--r--twisted/conch/test/test_ckeygen.py80
-rw-r--r--twisted/conch/test/test_conch.py552
-rw-r--r--twisted/conch/test/test_connection.py730
-rw-r--r--twisted/conch/test/test_default.py171
-rw-r--r--twisted/conch/test/test_filetransfer.py765
-rw-r--r--twisted/conch/test/test_helper.py560
-rw-r--r--twisted/conch/test/test_insults.py496
-rw-r--r--twisted/conch/test/test_keys.py488
-rw-r--r--twisted/conch/test/test_knownhosts.py979
-rw-r--r--twisted/conch/test/test_manhole.py372
-rw-r--r--twisted/conch/test/test_mixin.py47
-rw-r--r--twisted/conch/test/test_openssh_compat.py102
-rw-r--r--twisted/conch/test/test_recvline.py706
-rw-r--r--twisted/conch/test/test_scripts.py82
-rw-r--r--twisted/conch/test/test_session.py1256
-rw-r--r--twisted/conch/test/test_ssh.py995
-rw-r--r--twisted/conch/test/test_tap.py173
-rw-r--r--twisted/conch/test/test_telnet.py767
-rw-r--r--twisted/conch/test/test_text.py101
-rw-r--r--twisted/conch/test/test_transport.py2196
-rw-r--r--twisted/conch/test/test_userauth.py1062
-rw-r--r--twisted/conch/test/test_window.py67
-rw-r--r--twisted/conch/topfiles/NEWS391
-rw-r--r--twisted/conch/topfiles/README11
-rw-r--r--twisted/conch/topfiles/setup.py48
-rw-r--r--twisted/conch/ttymodes.py121
-rwxr-xr-xtwisted/conch/ui/__init__.py11
-rw-r--r--twisted/conch/ui/ansi.py240
-rw-r--r--twisted/conch/ui/tkvt100.py197
-rw-r--r--twisted/conch/unix.py457
-rw-r--r--twisted/copyright.py39
-rw-r--r--twisted/cred/__init__.py13
-rw-r--r--twisted/cred/_digest.py129
-rw-r--r--twisted/cred/checkers.py268
-rw-r--r--twisted/cred/credentials.py483
-rw-r--r--twisted/cred/error.py41
-rw-r--r--twisted/cred/pamauth.py79
-rw-r--r--twisted/cred/portal.py121
-rw-r--r--twisted/cred/strcred.py270
-rw-r--r--twisted/enterprise/__init__.py9
-rw-r--r--twisted/enterprise/adbapi.py483
-rw-r--r--twisted/internet/__init__.py12
-rw-r--r--twisted/internet/_baseprocess.py62
-rw-r--r--twisted/internet/_dumbwin32proc.py388
-rw-r--r--twisted/internet/_glibbase.py387
-rw-r--r--twisted/internet/_newtls.py270
-rw-r--r--twisted/internet/_oldtls.py381
-rw-r--r--twisted/internet/_pollingfile.py300
-rw-r--r--twisted/internet/_posixserialport.py74
-rw-r--r--twisted/internet/_posixstdio.py175
-rw-r--r--twisted/internet/_sigchld.c101
-rw-r--r--twisted/internet/_signals.py184
-rw-r--r--twisted/internet/_ssl.py32
-rw-r--r--twisted/internet/_sslverify.py749
-rw-r--r--twisted/internet/_threadedselect.py367
-rw-r--r--twisted/internet/_win32serialport.py126
-rw-r--r--twisted/internet/_win32stdio.py124
-rw-r--r--twisted/internet/abstract.py517
-rw-r--r--twisted/internet/address.py146
-rw-r--r--twisted/internet/base.py1190
-rw-r--r--twisted/internet/cfreactor.py501
-rw-r--r--twisted/internet/default.py56
-rw-r--r--twisted/internet/defer.py1561
-rw-r--r--twisted/internet/endpoints.py1202
-rw-r--r--twisted/internet/epollreactor.py394
-rw-r--r--twisted/internet/error.py448
-rw-r--r--twisted/internet/fdesc.py118
-rw-r--r--twisted/internet/gireactor.py139
-rw-r--r--twisted/internet/glib2reactor.py44
-rw-r--r--twisted/internet/gtk2reactor.py114
-rw-r--r--twisted/internet/gtk3reactor.py65
-rw-r--r--twisted/internet/gtkreactor.py250
-rw-r--r--twisted/internet/inotify.py405
-rw-r--r--twisted/internet/interfaces.py2049
-rw-r--r--twisted/internet/iocpreactor/__init__.py10
-rw-r--r--twisted/internet/iocpreactor/abstract.py400
-rw-r--r--twisted/internet/iocpreactor/build.bat4
-rw-r--r--twisted/internet/iocpreactor/const.py26
-rw-r--r--twisted/internet/iocpreactor/interfaces.py47
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/acceptex.pxi46
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/connectex.pxi47
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/iocpsupport.c6376
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/iocpsupport.pyx312
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c62
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/winsock_pointers.h51
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/wsarecv.pxi76
-rw-r--r--twisted/internet/iocpreactor/iocpsupport/wsasend.pxi30
-rw-r--r--twisted/internet/iocpreactor/notes.txt24
-rw-r--r--twisted/internet/iocpreactor/reactor.py275
-rw-r--r--twisted/internet/iocpreactor/setup.py23
-rw-r--r--twisted/internet/iocpreactor/tcp.py578
-rw-r--r--twisted/internet/iocpreactor/udp.py382
-rw-r--r--twisted/internet/kqreactor.py305
-rw-r--r--twisted/internet/main.py35
-rw-r--r--twisted/internet/pollreactor.py187
-rw-r--r--twisted/internet/posixbase.py640
-rw-r--r--twisted/internet/process.py1068
-rw-r--r--twisted/internet/protocol.py830
-rw-r--r--twisted/internet/pyuisupport.py37
-rw-r--r--twisted/internet/qtreactor.py19
-rw-r--r--twisted/internet/reactor.py38
-rw-r--r--twisted/internet/selectreactor.py203
-rw-r--r--twisted/internet/serialport.py87
-rw-r--r--twisted/internet/ssl.py203
-rw-r--r--twisted/internet/stdio.py32
-rw-r--r--twisted/internet/task.py789
-rw-r--r--twisted/internet/tcp.py1130
-rw-r--r--twisted/internet/test/__init__.py6
-rw-r--r--twisted/internet/test/_posixifaces.py131
-rw-r--r--twisted/internet/test/_win32ifaces.py119
-rw-r--r--twisted/internet/test/connectionmixins.py649
-rw-r--r--twisted/internet/test/fake_CAs/not-a-certificate1
-rw-r--r--twisted/internet/test/fake_CAs/thing1.pem26
-rw-r--r--twisted/internet/test/fake_CAs/thing2-duplicate.pem26
-rw-r--r--twisted/internet/test/fake_CAs/thing2.pem26
-rw-r--r--twisted/internet/test/fakeendpoint.py66
-rw-r--r--twisted/internet/test/inlinecb_tests.py92
-rw-r--r--twisted/internet/test/process_helper.py33
-rw-r--r--twisted/internet/test/reactormixins.py409
-rw-r--r--twisted/internet/test/test_abstract.py56
-rw-r--r--twisted/internet/test/test_address.py292
-rw-r--r--twisted/internet/test/test_base.py272
-rw-r--r--twisted/internet/test/test_baseprocess.py73
-rw-r--r--twisted/internet/test/test_core.py331
-rw-r--r--twisted/internet/test/test_default.py83
-rw-r--r--twisted/internet/test/test_endpoints.py1646
-rw-r--r--twisted/internet/test/test_epollreactor.py246
-rw-r--r--twisted/internet/test/test_fdset.py394
-rw-r--r--twisted/internet/test/test_filedescriptor.py41
-rw-r--r--twisted/internet/test/test_glibbase.py66
-rw-r--r--twisted/internet/test/test_gtk3reactor.py152
-rw-r--r--twisted/internet/test/test_gtkreactor.py95
-rw-r--r--twisted/internet/test/test_inlinecb.py13
-rw-r--r--twisted/internet/test/test_inotify.py504
-rw-r--r--twisted/internet/test/test_interfaces.py53
-rw-r--r--twisted/internet/test/test_iocp.py150
-rw-r--r--twisted/internet/test/test_main.py38
-rw-r--r--twisted/internet/test/test_newtls.py194
-rw-r--r--twisted/internet/test/test_pollingfile.py46
-rw-r--r--twisted/internet/test/test_posixbase.py387
-rw-r--r--twisted/internet/test/test_posixprocess.py340
-rw-r--r--twisted/internet/test/test_process.py711
-rw-r--r--twisted/internet/test/test_protocol.py357
-rw-r--r--twisted/internet/test/test_qtreactor.py35
-rw-r--r--twisted/internet/test/test_serialport.py72
-rw-r--r--twisted/internet/test/test_sigchld.py194
-rw-r--r--twisted/internet/test/test_socket.py96
-rw-r--r--twisted/internet/test/test_stdio.py195
-rw-r--r--twisted/internet/test/test_tcp.py1943
-rw-r--r--twisted/internet/test/test_threads.py215
-rw-r--r--twisted/internet/test/test_time.py61
-rw-r--r--twisted/internet/test/test_tls.py432
-rw-r--r--twisted/internet/test/test_udp.py194
-rw-r--r--twisted/internet/test/test_udp_internals.py165
-rw-r--r--twisted/internet/test/test_unix.py554
-rw-r--r--twisted/internet/test/test_win32events.py183
-rw-r--r--twisted/internet/threads.py123
-rw-r--r--twisted/internet/tksupport.py75
-rw-r--r--twisted/internet/udp.py347
-rw-r--r--twisted/internet/unix.py518
-rw-r--r--twisted/internet/utils.py219
-rw-r--r--twisted/internet/win32eventreactor.py430
-rw-r--r--twisted/internet/wxreactor.py184
-rw-r--r--twisted/internet/wxsupport.py61
-rw-r--r--twisted/lore/__init__.py21
-rw-r--r--twisted/lore/_version.py3
-rw-r--r--twisted/lore/default.py56
-rw-r--r--twisted/lore/docbook.py68
-rw-r--r--twisted/lore/htmlbook.py47
-rw-r--r--twisted/lore/indexer.py50
-rw-r--r--twisted/lore/latex.py463
-rw-r--r--twisted/lore/lint.py204
-rw-r--r--twisted/lore/lmath.py85
-rw-r--r--twisted/lore/man2lore.py295
-rw-r--r--twisted/lore/numberer.py33
-rw-r--r--twisted/lore/process.py120
-rw-r--r--twisted/lore/scripts/__init__.py1
-rwxr-xr-xtwisted/lore/scripts/lore.py155
-rw-r--r--twisted/lore/slides.py359
-rw-r--r--twisted/lore/template.mgp24
-rw-r--r--twisted/lore/test/__init__.py1
-rw-r--r--twisted/lore/test/lore_index_file_out.html2
-rw-r--r--twisted/lore/test/lore_index_file_out_multiple.html5
-rw-r--r--twisted/lore/test/lore_index_file_unnumbered_out.html2
-rw-r--r--twisted/lore/test/lore_index_test.xhtml21
-rw-r--r--twisted/lore/test/lore_index_test2.xhtml22
-rw-r--r--twisted/lore/test/lore_numbering_test_out.html2
-rw-r--r--twisted/lore/test/lore_numbering_test_out2.html2
-rw-r--r--twisted/lore/test/simple.html9
-rw-r--r--twisted/lore/test/simple3.html9
-rw-r--r--twisted/lore/test/simple4.html9
-rw-r--r--twisted/lore/test/template.tpl13
-rw-r--r--twisted/lore/test/test_docbook.py35
-rw-r--r--twisted/lore/test/test_latex.py146
-rw-r--r--twisted/lore/test/test_lint.py132
-rw-r--r--twisted/lore/test/test_lmath.py72
-rw-r--r--twisted/lore/test/test_lore.py1198
-rw-r--r--twisted/lore/test/test_man2lore.py169
-rw-r--r--twisted/lore/test/test_scripts.py27
-rw-r--r--twisted/lore/test/test_slides.py85
-rw-r--r--twisted/lore/texi.py109
-rw-r--r--twisted/lore/topfiles/NEWS155
-rw-r--r--twisted/lore/topfiles/README3
-rw-r--r--twisted/lore/topfiles/setup.py29
-rwxr-xr-xtwisted/lore/tree.py1122
-rw-r--r--twisted/lore/xhtml-lat1.ent196
-rw-r--r--twisted/lore/xhtml-special.ent80
-rw-r--r--twisted/lore/xhtml-symbol.ent237
-rw-r--r--twisted/lore/xhtml1-strict.dtd978
-rw-r--r--twisted/lore/xhtml1-transitional.dtd1201
-rw-r--r--twisted/mail/__init__.py15
-rw-r--r--twisted/mail/_version.py3
-rw-r--r--twisted/mail/alias.py435
-rw-r--r--twisted/mail/bounce.py60
-rw-r--r--twisted/mail/imap4.py5854
-rw-r--r--twisted/mail/mail.py333
-rw-r--r--twisted/mail/maildir.py518
-rw-r--r--twisted/mail/pb.py115
-rw-r--r--twisted/mail/pop3.py1071
-rw-r--r--twisted/mail/pop3client.py706
-rw-r--r--twisted/mail/protocols.py225
-rw-r--r--twisted/mail/relay.py114
-rw-r--r--twisted/mail/relaymanager.py631
-rw-r--r--twisted/mail/scripts/__init__.py1
-rw-r--r--twisted/mail/scripts/mailmail.py366
-rw-r--r--twisted/mail/smtp.py1934
-rw-r--r--twisted/mail/tap.py349
-rw-r--r--twisted/mail/test/__init__.py1
-rw-r--r--twisted/mail/test/pop3testserver.py314
-rw-r--r--twisted/mail/test/rfc822.message86
-rw-r--r--twisted/mail/test/test_bounce.py32
-rw-r--r--twisted/mail/test/test_imap.py4489
-rw-r--r--twisted/mail/test/test_mail.py2039
-rw-r--r--twisted/mail/test/test_mailmail.py75
-rw-r--r--twisted/mail/test/test_options.py255
-rw-r--r--twisted/mail/test/test_pop3.py1069
-rw-r--r--twisted/mail/test/test_pop3client.py582
-rw-r--r--twisted/mail/test/test_scripts.py18
-rw-r--r--twisted/mail/test/test_smtp.py1520
-rw-r--r--twisted/mail/topfiles/NEWS289
-rw-r--r--twisted/mail/topfiles/README6
-rw-r--r--twisted/mail/topfiles/setup.py50
-rw-r--r--twisted/manhole/__init__.py8
-rw-r--r--twisted/manhole/_inspectro.py369
-rw-r--r--twisted/manhole/explorer.py654
-rw-r--r--twisted/manhole/gladereactor.glade342
-rw-r--r--twisted/manhole/gladereactor.py219
-rw-r--r--twisted/manhole/inspectro.glade510
-rw-r--r--twisted/manhole/logview.glade39
-rw-r--r--twisted/manhole/service.py399
-rw-r--r--twisted/manhole/telnet.py117
-rw-r--r--twisted/manhole/test/__init__.py6
-rw-r--r--twisted/manhole/test/test_explorer.py102
-rw-r--r--twisted/manhole/ui/__init__.py7
-rw-r--r--twisted/manhole/ui/gtk2manhole.glade268
-rw-r--r--twisted/manhole/ui/gtk2manhole.py375
-rw-r--r--twisted/manhole/ui/test/__init__.py4
-rw-r--r--twisted/manhole/ui/test/test_gtk2manhole.py48
-rw-r--r--twisted/names/__init__.py7
-rw-r--r--twisted/names/_version.py3
-rw-r--r--twisted/names/authority.py333
-rw-r--r--twisted/names/cache.py116
-rw-r--r--twisted/names/client.py955
-rw-r--r--twisted/names/common.py278
-rw-r--r--twisted/names/dns.py1949
-rw-r--r--twisted/names/error.py95
-rw-r--r--twisted/names/hosts.py157
-rw-r--r--twisted/names/resolve.py59
-rw-r--r--twisted/names/root.py446
-rw-r--r--twisted/names/secondary.py179
-rw-r--r--twisted/names/server.py205
-rw-r--r--twisted/names/srvconnect.py186
-rw-r--r--twisted/names/tap.py150
-rw-r--r--twisted/names/test/__init__.py1
-rw-r--r--twisted/names/test/test_cache.py109
-rw-r--r--twisted/names/test/test_client.py678
-rw-r--r--twisted/names/test/test_common.py71
-rw-r--r--twisted/names/test/test_dns.py1485
-rw-r--r--twisted/names/test/test_hosts.py232
-rw-r--r--twisted/names/test/test_names.py956
-rw-r--r--twisted/names/test/test_rootresolve.py705
-rw-r--r--twisted/names/test/test_srvconnect.py133
-rw-r--r--twisted/names/test/test_tap.py99
-rw-r--r--twisted/names/topfiles/NEWS230
-rw-r--r--twisted/names/topfiles/README3
-rw-r--r--twisted/names/topfiles/setup.py50
-rw-r--r--twisted/news/__init__.py11
-rw-r--r--twisted/news/_version.py3
-rw-r--r--twisted/news/database.py1051
-rw-r--r--twisted/news/news.py90
-rw-r--r--twisted/news/nntp.py1036
-rw-r--r--twisted/news/tap.py138
-rw-r--r--twisted/news/test/__init__.py1
-rw-r--r--twisted/news/test/test_database.py224
-rw-r--r--twisted/news/test/test_news.py107
-rw-r--r--twisted/news/test/test_nntp.py197
-rw-r--r--twisted/news/topfiles/NEWS106
-rw-r--r--twisted/news/topfiles/README4
-rw-r--r--twisted/news/topfiles/setup.py28
-rw-r--r--twisted/pair/__init__.py20
-rw-r--r--twisted/pair/_version.py3
-rw-r--r--twisted/pair/ethernet.py56
-rw-r--r--twisted/pair/ip.py72
-rw-r--r--twisted/pair/raw.py35
-rw-r--r--twisted/pair/rawudp.py55
-rw-r--r--twisted/pair/test/__init__.py1
-rw-r--r--twisted/pair/test/test_ethernet.py226
-rw-r--r--twisted/pair/test/test_ip.py417
-rw-r--r--twisted/pair/test/test_rawudp.py327
-rw-r--r--twisted/pair/topfiles/NEWS56
-rw-r--r--twisted/pair/topfiles/README4
-rw-r--r--twisted/pair/topfiles/setup.py28
-rw-r--r--twisted/pair/tuntap.py170
-rw-r--r--twisted/persisted/__init__.py6
-rw-r--r--twisted/persisted/aot.py560
-rw-r--r--twisted/persisted/crefutil.py163
-rw-r--r--twisted/persisted/dirdbm.py358
-rw-r--r--twisted/persisted/sob.py227
-rw-r--r--twisted/persisted/styles.py262
-rw-r--r--twisted/persisted/test/__init__.py6
-rw-r--r--twisted/persisted/test/test_styles.py55
-rw-r--r--twisted/plugin.py255
-rw-r--r--twisted/plugins/__init__.py17
-rw-r--r--twisted/plugins/cred_anonymous.py40
-rw-r--r--twisted/plugins/cred_file.py60
-rw-r--r--twisted/plugins/cred_memory.py68
-rw-r--r--twisted/plugins/cred_sshkeys.py51
-rw-r--r--twisted/plugins/cred_unix.py138
-rw-r--r--twisted/plugins/twisted_conch.py18
-rw-r--r--twisted/plugins/twisted_core.py6
-rw-r--r--twisted/plugins/twisted_ftp.py10
-rw-r--r--twisted/plugins/twisted_inet.py10
-rw-r--r--twisted/plugins/twisted_lore.py38
-rw-r--r--twisted/plugins/twisted_mail.py10
-rw-r--r--twisted/plugins/twisted_manhole.py10
-rw-r--r--twisted/plugins/twisted_names.py10
-rw-r--r--twisted/plugins/twisted_news.py10
-rw-r--r--twisted/plugins/twisted_portforward.py10
-rw-r--r--twisted/plugins/twisted_qtstub.py45
-rw-r--r--twisted/plugins/twisted_reactors.py42
-rw-r--r--twisted/plugins/twisted_runner.py10
-rw-r--r--twisted/plugins/twisted_socks.py10
-rw-r--r--twisted/plugins/twisted_telnet.py10
-rw-r--r--twisted/plugins/twisted_trial.py59
-rw-r--r--twisted/plugins/twisted_web.py11
-rw-r--r--twisted/plugins/twisted_words.py43
-rw-r--r--twisted/protocols/__init__.py7
-rw-r--r--twisted/protocols/amp.py2705
-rw-r--r--twisted/protocols/basic.py939
-rw-r--r--twisted/protocols/dict.py362
-rw-r--r--twisted/protocols/finger.py42
-rw-r--r--twisted/protocols/ftp.py2953
-rw-r--r--twisted/protocols/gps/__init__.py1
-rw-r--r--twisted/protocols/gps/nmea.py209
-rw-r--r--twisted/protocols/gps/rockwell.py268
-rw-r--r--twisted/protocols/htb.py269
-rw-r--r--twisted/protocols/ident.py235
-rw-r--r--twisted/protocols/loopback.py372
-rw-r--r--twisted/protocols/memcache.py758
-rw-r--r--twisted/protocols/mice/__init__.py1
-rw-r--r--twisted/protocols/mice/mouseman.py127
-rw-r--r--twisted/protocols/pcp.py204
-rw-r--r--twisted/protocols/policies.py725
-rw-r--r--twisted/protocols/portforward.py87
-rw-r--r--twisted/protocols/postfix.py112
-rw-r--r--twisted/protocols/shoutcast.py111
-rw-r--r--twisted/protocols/sip.py1334
-rw-r--r--twisted/protocols/socks.py240
-rw-r--r--twisted/protocols/stateful.py52
-rw-r--r--twisted/protocols/telnet.py325
-rw-r--r--twisted/protocols/test/__init__.py6
-rw-r--r--twisted/protocols/test/test_tls.py1474
-rw-r--r--twisted/protocols/tls.py609
-rw-r--r--twisted/protocols/wire.py90
-rw-r--r--twisted/python/__init__.py13
-rw-r--r--twisted/python/_epoll.c3348
-rw-r--r--twisted/python/_epoll.pyx285
-rw-r--r--twisted/python/_initgroups.c66
-rw-r--r--twisted/python/_inotify.py101
-rw-r--r--twisted/python/_release.py1369
-rw-r--r--twisted/python/_shellcomp.py668
-rw-r--r--twisted/python/compat.py177
-rw-r--r--twisted/python/components.py438
-rw-r--r--twisted/python/constants.py377
-rw-r--r--twisted/python/context.py133
-rw-r--r--twisted/python/deprecate.py534
-rw-r--r--twisted/python/dist.py401
-rw-r--r--twisted/python/failure.py650
-rw-r--r--twisted/python/fakepwd.py219
-rw-r--r--twisted/python/filepath.py1444
-rw-r--r--twisted/python/finalize.py46
-rw-r--r--twisted/python/formmethod.py363
-rw-r--r--twisted/python/hashlib.py24
-rw-r--r--twisted/python/hook.py177
-rw-r--r--twisted/python/htmlizer.py91
-rw-r--r--twisted/python/lockfile.py214
-rw-r--r--twisted/python/log.py706
-rw-r--r--twisted/python/logfile.py323
-rw-r--r--twisted/python/modules.py758
-rw-r--r--twisted/python/monkey.py73
-rw-r--r--twisted/python/procutils.py45
-rw-r--r--twisted/python/randbytes.py131
-rw-r--r--twisted/python/rebuild.py271
-rw-r--r--twisted/python/reflect.py827
-rw-r--r--twisted/python/release.py63
-rw-r--r--twisted/python/roots.py248
-rw-r--r--twisted/python/runtime.py137
-rw-r--r--twisted/python/sendmsg.c502
-rw-r--r--twisted/python/shortcut.py76
-rw-r--r--twisted/python/syslog.py107
-rw-r--r--twisted/python/systemd.py87
-rw-r--r--twisted/python/test/__init__.py3
-rw-r--r--twisted/python/test/deprecatedattributes.py21
-rw-r--r--twisted/python/test/modules_helpers.py64
-rw-r--r--twisted/python/test/pullpipe.py40
-rw-r--r--twisted/python/test/test_components.py770
-rw-r--r--twisted/python/test/test_constants.py778
-rw-r--r--twisted/python/test/test_deprecate.py767
-rw-r--r--twisted/python/test/test_dist.py316
-rw-r--r--twisted/python/test/test_fakepwd.py386
-rw-r--r--twisted/python/test/test_hashlib.py90
-rw-r--r--twisted/python/test/test_htmlizer.py41
-rw-r--r--twisted/python/test/test_inotify.py120
-rw-r--r--twisted/python/test/test_release.py2564
-rw-r--r--twisted/python/test/test_runtime.py91
-rw-r--r--twisted/python/test/test_sendmsg.py543
-rwxr-xr-xtwisted/python/test/test_shellcomp.py623
-rw-r--r--twisted/python/test/test_syslog.py151
-rw-r--r--twisted/python/test/test_systemd.py173
-rw-r--r--twisted/python/test/test_util.py892
-rw-r--r--twisted/python/test/test_versions.py323
-rw-r--r--twisted/python/test/test_win32.py35
-rw-r--r--twisted/python/test/test_zipstream.py504
-rw-r--r--twisted/python/test/test_zshcomp.py228
-rw-r--r--twisted/python/text.py198
-rw-r--r--twisted/python/threadable.py118
-rw-r--r--twisted/python/threadpool.py240
-rw-r--r--twisted/python/twisted-completion.zsh33
-rw-r--r--twisted/python/urlpath.py122
-rw-r--r--twisted/python/usage.py973
-rw-r--r--twisted/python/util.py983
-rw-r--r--twisted/python/versions.py249
-rw-r--r--twisted/python/win32.py168
-rw-r--r--twisted/python/zippath.py268
-rw-r--r--twisted/python/zipstream.py387
-rw-r--r--twisted/python/zsh/README.txt9
-rw-r--r--twisted/python/zsh/_cftp34
-rw-r--r--twisted/python/zsh/_ckeygen34
-rw-r--r--twisted/python/zsh/_conch34
-rw-r--r--twisted/python/zsh/_lore34
-rw-r--r--twisted/python/zsh/_manhole34
-rw-r--r--twisted/python/zsh/_mktap34
-rw-r--r--twisted/python/zsh/_pyhtmlizer34
-rw-r--r--twisted/python/zsh/_tap2deb34
-rw-r--r--twisted/python/zsh/_tap2rpm34
-rw-r--r--twisted/python/zsh/_tapconvert34
-rw-r--r--twisted/python/zsh/_tkconch34
-rw-r--r--twisted/python/zsh/_tkmktap34
-rw-r--r--twisted/python/zsh/_trial34
-rw-r--r--twisted/python/zsh/_twistd34
-rw-r--r--twisted/python/zsh/_websetroot34
-rw-r--r--twisted/python/zshcomp.py824
-rw-r--r--twisted/runner/__init__.py15
-rw-r--r--twisted/runner/_version.py3
-rw-r--r--twisted/runner/inetd.py70
-rw-r--r--twisted/runner/inetdconf.py194
-rw-r--r--twisted/runner/inetdtap.py163
-rw-r--r--twisted/runner/portmap.c57
-rw-r--r--twisted/runner/procmon.py310
-rw-r--r--twisted/runner/procmontap.py73
-rw-r--r--twisted/runner/test/__init__.py6
-rw-r--r--twisted/runner/test/test_procmon.py477
-rw-r--r--twisted/runner/test/test_procmontap.py87
-rw-r--r--twisted/runner/topfiles/NEWS101
-rw-r--r--twisted/runner/topfiles/README3
-rw-r--r--twisted/runner/topfiles/setup.py35
-rw-r--r--twisted/scripts/__init__.py27
-rw-r--r--twisted/scripts/_twistd_unix.py349
-rw-r--r--twisted/scripts/_twistw.py50
-rw-r--r--twisted/scripts/htmlizer.py69
-rw-r--r--twisted/scripts/manhole.py69
-rw-r--r--twisted/scripts/tap2deb.py281
-rwxr-xr-xtwisted/scripts/tap2rpm.py331
-rw-r--r--twisted/scripts/tapconvert.py57
-rw-r--r--twisted/scripts/test/__init__.py6
-rw-r--r--twisted/scripts/test/test_scripts.py178
-rw-r--r--twisted/scripts/test/test_tap2rpm.py399
-rw-r--r--twisted/scripts/tkunzip.py292
-rw-r--r--twisted/scripts/trial.py389
-rw-r--r--twisted/scripts/twistd.py30
-rw-r--r--twisted/spread/__init__.py12
-rw-r--r--twisted/spread/banana.py358
-rw-r--r--twisted/spread/flavors.py590
-rw-r--r--twisted/spread/interfaces.py28
-rw-r--r--twisted/spread/jelly.py1151
-rw-r--r--twisted/spread/pb.py1434
-rw-r--r--twisted/spread/publish.py142
-rw-r--r--twisted/spread/ui/__init__.py12
-rw-r--r--twisted/spread/ui/gtk2util.py222
-rw-r--r--twisted/spread/ui/login2.glade461
-rw-r--r--twisted/spread/ui/tktree.py204
-rw-r--r--twisted/spread/ui/tkutil.py397
-rw-r--r--twisted/spread/util.py215
-rw-r--r--twisted/tap/__init__.py10
-rw-r--r--twisted/tap/ftp.py69
-rw-r--r--twisted/tap/manhole.py54
-rw-r--r--twisted/tap/portforward.py27
-rw-r--r--twisted/tap/socks.py38
-rw-r--r--twisted/tap/telnet.py32
-rw-r--r--twisted/test/__init__.py10
-rw-r--r--twisted/test/_preamble.py17
-rw-r--r--twisted/test/crash_test_dummy.py34
-rw-r--r--twisted/test/generator_failure_tests.py177
-rw-r--r--twisted/test/iosim.py270
-rw-r--r--twisted/test/mock_win32process.py48
-rw-r--r--twisted/test/myrebuilder1.py15
-rw-r--r--twisted/test/myrebuilder2.py16
-rw-r--r--twisted/test/plugin_basic.py57
-rw-r--r--twisted/test/plugin_extra1.py23
-rw-r--r--twisted/test/plugin_extra2.py35
-rw-r--r--twisted/test/process_cmdline.py5
-rw-r--r--twisted/test/process_echoer.py11
-rw-r--r--twisted/test/process_fds.py40
-rw-r--r--twisted/test/process_linger.py17
-rw-r--r--twisted/test/process_reader.py12
-rw-r--r--twisted/test/process_signal.py8
-rw-r--r--twisted/test/process_stdinreader.py23
-rw-r--r--twisted/test/process_tester.py37
-rw-r--r--twisted/test/process_tty.py6
-rw-r--r--twisted/test/process_twisted.py43
-rw-r--r--twisted/test/proto_helpers.py558
-rw-r--r--twisted/test/raiser.c1443
-rw-r--r--twisted/test/raiser.pyx21
-rw-r--r--twisted/test/reflect_helper_IE.py4
-rw-r--r--twisted/test/reflect_helper_VE.py4
-rw-r--r--twisted/test/reflect_helper_ZDE.py4
-rw-r--r--twisted/test/server.pem36
-rw-r--r--twisted/test/ssl_helpers.py26
-rw-r--r--twisted/test/stdio_test_consumer.py39
-rw-r--r--twisted/test/stdio_test_halfclose.py66
-rw-r--r--twisted/test/stdio_test_hostpeer.py32
-rw-r--r--twisted/test/stdio_test_lastwrite.py45
-rw-r--r--twisted/test/stdio_test_loseconn.py48
-rw-r--r--twisted/test/stdio_test_producer.py55
-rw-r--r--twisted/test/stdio_test_write.py31
-rw-r--r--twisted/test/stdio_test_writeseq.py30
-rw-r--r--twisted/test/test_abstract.py83
-rw-r--r--twisted/test/test_adbapi.py819
-rw-r--r--twisted/test/test_amp.py3178
-rw-r--r--twisted/test/test_application.py878
-rw-r--r--twisted/test/test_banana.py278
-rw-r--r--twisted/test/test_compat.py199
-rw-r--r--twisted/test/test_context.py15
-rw-r--r--twisted/test/test_cooperator.py669
-rw-r--r--twisted/test/test_defer.py2002
-rw-r--r--twisted/test/test_defgen.py309
-rw-r--r--twisted/test/test_dict.py22
-rw-r--r--twisted/test/test_digestauth.py671
-rw-r--r--twisted/test/test_dirdbm.py170
-rw-r--r--twisted/test/test_doc.py104
-rw-r--r--twisted/test/test_epoll.py158
-rw-r--r--twisted/test/test_error.py170
-rw-r--r--twisted/test/test_explorer.py236
-rw-r--r--twisted/test/test_factories.py197
-rw-r--r--twisted/test/test_failure.py594
-rw-r--r--twisted/test/test_fdesc.py235
-rw-r--r--twisted/test/test_finger.py67
-rw-r--r--twisted/test/test_formmethod.py77
-rw-r--r--twisted/test/test_ftp.py3017
-rw-r--r--twisted/test/test_ftp_options.py80
-rw-r--r--twisted/test/test_hook.py150
-rw-r--r--twisted/test/test_htb.py109
-rw-r--r--twisted/test/test_ident.py194
-rw-r--r--twisted/test/test_import.py75
-rw-r--r--twisted/test/test_internet.py1396
-rw-r--r--twisted/test/test_iutils.py296
-rw-r--r--twisted/test/test_jelly.py671
-rw-r--r--twisted/test/test_lockfile.py445
-rw-r--r--twisted/test/test_log.py773
-rw-r--r--twisted/test/test_logfile.py320
-rw-r--r--twisted/test/test_loopback.py419
-rw-r--r--twisted/test/test_manhole.py75
-rw-r--r--twisted/test/test_memcache.py663
-rw-r--r--twisted/test/test_modules.py478
-rw-r--r--twisted/test/test_monkey.py161
-rw-r--r--twisted/test/test_newcred.py445
-rw-r--r--twisted/test/test_nmea.py115
-rw-r--r--twisted/test/test_paths.py1622
-rw-r--r--twisted/test/test_pb.py1846
-rw-r--r--twisted/test/test_pbfailure.py475
-rw-r--r--twisted/test/test_pcp.py368
-rw-r--r--twisted/test/test_persisted.py377
-rw-r--r--twisted/test/test_plugin.py719
-rw-r--r--twisted/test/test_policies.py736
-rw-r--r--twisted/test/test_postfix.py108
-rw-r--r--twisted/test/test_process.py2482
-rw-r--r--twisted/test/test_protocols.py1260
-rw-r--r--twisted/test/test_randbytes.py119
-rw-r--r--twisted/test/test_rebuild.py252
-rw-r--r--twisted/test/test_reflect.py867
-rw-r--r--twisted/test/test_roots.py63
-rw-r--r--twisted/test/test_shortcut.py26
-rw-r--r--twisted/test/test_sip.py942
-rw-r--r--twisted/test/test_sob.py172
-rw-r--r--twisted/test/test_socks.py498
-rw-r--r--twisted/test/test_ssl.py726
-rw-r--r--twisted/test/test_sslverify.py558
-rw-r--r--twisted/test/test_stateful.py81
-rw-r--r--twisted/test/test_stdio.py371
-rw-r--r--twisted/test/test_strcred.py657
-rw-r--r--twisted/test/test_strerror.py151
-rw-r--r--twisted/test/test_stringtransport.py279
-rw-r--r--twisted/test/test_strports.py133
-rw-r--r--twisted/test/test_task.py739
-rw-r--r--twisted/test/test_tcp.py1820
-rw-r--r--twisted/test/test_tcp_internals.py249
-rw-r--r--twisted/test/test_text.py158
-rw-r--r--twisted/test/test_threadable.py103
-rw-r--r--twisted/test/test_threadpool.py526
-rw-r--r--twisted/test/test_threads.py412
-rw-r--r--twisted/test/test_timehelpers.py31
-rw-r--r--twisted/test/test_tpfile.py52
-rw-r--r--twisted/test/test_twistd.py1549
-rw-r--r--twisted/test/test_udp.py721
-rw-r--r--twisted/test/test_unix.py405
-rw-r--r--twisted/test/test_usage.py584
-rw-r--r--twisted/test/testutils.py55
-rw-r--r--twisted/test/time_helpers.py72
-rw-r--r--twisted/topfiles/CREDITS60
-rw-r--r--twisted/topfiles/ChangeLog.Old3888
-rw-r--r--twisted/topfiles/NEWS1744
-rw-r--r--twisted/topfiles/README14
-rw-r--r--twisted/topfiles/setup.py94
-rw-r--r--twisted/trial/__init__.py52
-rw-r--r--twisted/trial/itrial.py251
-rw-r--r--twisted/trial/reporter.py1233
-rw-r--r--twisted/trial/runner.py877
-rw-r--r--twisted/trial/test/__init__.py1
-rw-r--r--twisted/trial/test/detests.py195
-rw-r--r--twisted/trial/test/erroneous.py130
-rw-r--r--twisted/trial/test/mockcustomsuite.py21
-rw-r--r--twisted/trial/test/mockcustomsuite2.py21
-rw-r--r--twisted/trial/test/mockcustomsuite3.py28
-rw-r--r--twisted/trial/test/mockdoctest.py104
-rw-r--r--twisted/trial/test/moduleself.py7
-rw-r--r--twisted/trial/test/moduletest.py11
-rw-r--r--twisted/trial/test/notpython2
-rw-r--r--twisted/trial/test/novars.py6
-rw-r--r--twisted/trial/test/packages.py156
-rw-r--r--twisted/trial/test/sample.py108
-rw-r--r--twisted/trial/test/scripttest.py14
-rw-r--r--twisted/trial/test/suppression.py57
-rw-r--r--twisted/trial/test/test_assertions.py817
-rw-r--r--twisted/trial/test/test_deferred.py220
-rw-r--r--twisted/trial/test/test_doctest.py64
-rw-r--r--twisted/trial/test/test_keyboard.py113
-rw-r--r--twisted/trial/test/test_loader.py611
-rw-r--r--twisted/trial/test/test_log.py197
-rw-r--r--twisted/trial/test/test_output.py162
-rw-r--r--twisted/trial/test/test_plugins.py46
-rw-r--r--twisted/trial/test/test_pyunitcompat.py222
-rw-r--r--twisted/trial/test/test_reporter.py1649
-rw-r--r--twisted/trial/test/test_runner.py1034
-rw-r--r--twisted/trial/test/test_script.py482
-rw-r--r--twisted/trial/test/test_test_visitor.py82
-rw-r--r--twisted/trial/test/test_testcase.py51
-rw-r--r--twisted/trial/test/test_tests.py1056
-rw-r--r--twisted/trial/test/test_util.py559
-rw-r--r--twisted/trial/test/test_warning.py472
-rw-r--r--twisted/trial/test/weird.py20
-rw-r--r--twisted/trial/unittest.py1620
-rw-r--r--twisted/trial/util.py430
-rw-r--r--twisted/web/__init__.py21
-rw-r--r--twisted/web/_auth/__init__.py7
-rw-r--r--twisted/web/_auth/basic.py59
-rw-r--r--twisted/web/_auth/digest.py54
-rw-r--r--twisted/web/_auth/wrapper.py225
-rw-r--r--twisted/web/_element.py185
-rw-r--r--twisted/web/_flatten.py314
-rw-r--r--twisted/web/_newclient.py1502
-rw-r--r--twisted/web/_stan.py325
-rw-r--r--twisted/web/_version.py3
-rw-r--r--twisted/web/client.py1600
-rw-r--r--twisted/web/demo.py24
-rw-r--r--twisted/web/distrib.py373
-rw-r--r--twisted/web/domhelpers.py268
-rw-r--r--twisted/web/error.py422
-rw-r--r--twisted/web/failure.xhtml71
-rw-r--r--twisted/web/google.py75
-rw-r--r--twisted/web/guard.py17
-rw-r--r--twisted/web/html.py49
-rw-r--r--twisted/web/http.py1812
-rw-r--r--twisted/web/http_headers.py277
-rw-r--r--twisted/web/iweb.py526
-rw-r--r--twisted/web/microdom.py1028
-rw-r--r--twisted/web/proxy.py303
-rw-r--r--twisted/web/resource.py319
-rw-r--r--twisted/web/rewrite.py52
-rw-r--r--twisted/web/script.py169
-rw-r--r--twisted/web/server.py592
-rw-r--r--twisted/web/soap.py154
-rw-r--r--twisted/web/static.py1083
-rw-r--r--twisted/web/sux.py637
-rw-r--r--twisted/web/tap.py232
-rw-r--r--twisted/web/template.py566
-rw-r--r--twisted/web/test/__init__.py7
-rw-r--r--twisted/web/test/_util.py77
-rwxr-xr-xtwisted/web/test/test_cgi.py270
-rwxr-xr-xtwisted/web/test/test_distrib.py434
-rw-r--r--twisted/web/test/test_domhelpers.py306
-rw-r--r--twisted/web/test/test_error.py151
-rw-r--r--twisted/web/test/test_flatten.py348
-rw-r--r--twisted/web/test/test_http.py1663
-rw-r--r--twisted/web/test/test_http_headers.py616
-rw-r--r--twisted/web/test/test_httpauth.py634
-rw-r--r--twisted/web/test/test_newclient.py2521
-rw-r--r--twisted/web/test/test_proxy.py544
-rw-r--r--twisted/web/test/test_resource.py145
-rw-r--r--twisted/web/test/test_script.py70
-rw-r--r--twisted/web/test/test_soap.py114
-rw-r--r--twisted/web/test/test_stan.py139
-rw-r--r--twisted/web/test/test_static.py1505
-rw-r--r--twisted/web/test/test_tap.py196
-rw-r--r--twisted/web/test/test_template.py810
-rw-r--r--twisted/web/test/test_util.py380
-rw-r--r--twisted/web/test/test_vhost.py105
-rw-r--r--twisted/web/test/test_web.py1100
-rw-r--r--twisted/web/test/test_webclient.py3144
-rw-r--r--twisted/web/test/test_wsgi.py1572
-rw-r--r--twisted/web/test/test_xml.py1105
-rw-r--r--twisted/web/test/test_xmlrpc.py849
-rw-r--r--twisted/web/topfiles/NEWS556
-rw-r--r--twisted/web/topfiles/README6
-rw-r--r--twisted/web/topfiles/setup.py30
-rw-r--r--twisted/web/twcgi.py299
-rw-r--r--twisted/web/util.py433
-rw-r--r--twisted/web/vhost.py135
-rw-r--r--twisted/web/wsgi.py403
-rw-r--r--twisted/web/xmlrpc.py590
-rw-r--r--twisted/words/__init__.py10
-rw-r--r--twisted/words/_version.py3
-rw-r--r--twisted/words/ewords.py34
-rw-r--r--twisted/words/im/__init__.py6
-rw-r--r--twisted/words/im/baseaccount.py62
-rw-r--r--twisted/words/im/basechat.py512
-rw-r--r--twisted/words/im/basesupport.py270
-rw-r--r--twisted/words/im/instancemessenger.glade3165
-rw-r--r--twisted/words/im/interfaces.py364
-rw-r--r--twisted/words/im/ircsupport.py263
-rw-r--r--twisted/words/im/locals.py26
-rw-r--r--twisted/words/im/pbsupport.py260
-rw-r--r--twisted/words/iwords.py266
-rw-r--r--twisted/words/protocols/__init__.py1
-rw-r--r--twisted/words/protocols/irc.py3302
-rw-r--r--twisted/words/protocols/jabber/__init__.py8
-rw-r--r--twisted/words/protocols/jabber/client.py368
-rw-r--r--twisted/words/protocols/jabber/component.py474
-rw-r--r--twisted/words/protocols/jabber/error.py336
-rw-r--r--twisted/words/protocols/jabber/ijabber.py199
-rw-r--r--twisted/words/protocols/jabber/jid.py249
-rw-r--r--twisted/words/protocols/jabber/jstrports.py31
-rw-r--r--twisted/words/protocols/jabber/sasl.py243
-rw-r--r--twisted/words/protocols/jabber/sasl_mechanisms.py240
-rw-r--r--twisted/words/protocols/jabber/xmlstream.py1136
-rw-r--r--twisted/words/protocols/jabber/xmpp_stringprep.py253
-rw-r--r--twisted/words/protocols/msn.py2479
-rw-r--r--twisted/words/protocols/oscar.py1235
-rw-r--r--twisted/words/service.py1223
-rw-r--r--twisted/words/tap.py74
-rw-r--r--twisted/words/test/__init__.py1
-rw-r--r--twisted/words/test/test_basechat.py68
-rw-r--r--twisted/words/test/test_basesupport.py97
-rw-r--r--twisted/words/test/test_domish.py434
-rw-r--r--twisted/words/test/test_irc.py1898
-rw-r--r--twisted/words/test/test_irc_service.py216
-rw-r--r--twisted/words/test/test_ircsupport.py79
-rw-r--r--twisted/words/test/test_jabberclient.py414
-rw-r--r--twisted/words/test/test_jabbercomponent.py422
-rw-r--r--twisted/words/test/test_jabbererror.py342
-rw-r--r--twisted/words/test/test_jabberjid.py225
-rw-r--r--twisted/words/test/test_jabberjstrports.py34
-rw-r--r--twisted/words/test/test_jabbersasl.py272
-rw-r--r--twisted/words/test/test_jabbersaslmechanisms.py90
-rw-r--r--twisted/words/test/test_jabberxmlstream.py1332
-rw-r--r--twisted/words/test/test_jabberxmppstringprep.py92
-rw-r--r--twisted/words/test/test_msn.py522
-rw-r--r--twisted/words/test/test_oscar.py24
-rw-r--r--twisted/words/test/test_service.py992
-rw-r--r--twisted/words/test/test_tap.py78
-rw-r--r--twisted/words/test/test_xishutil.py345
-rw-r--r--twisted/words/test/test_xmlstream.py224
-rw-r--r--twisted/words/test/test_xmpproutertap.py84
-rw-r--r--twisted/words/test/test_xpath.py260
-rw-r--r--twisted/words/topfiles/NEWS359
-rw-r--r--twisted/words/topfiles/README5
-rw-r--r--twisted/words/topfiles/setup.py53
-rw-r--r--twisted/words/xish/__init__.py10
-rw-r--r--twisted/words/xish/domish.py848
-rw-r--r--twisted/words/xish/utility.py372
-rw-r--r--twisted/words/xish/xmlstream.py261
-rw-r--r--twisted/words/xish/xpath.py333
-rw-r--r--twisted/words/xish/xpathparser.g375
-rw-r--r--twisted/words/xish/xpathparser.py508
-rw-r--r--twisted/words/xmpproutertap.py30
1431 files changed, 397047 insertions, 0 deletions
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..52071ed
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,33 @@
+Requirements
+
+ Python 2.5, 2.6 or 2.7.
+
+ Zope Interfaces 3.3.0 or better (http://pypi.python.org/pypi/zope.interface)
+
+ pyOpenSSL (<http://launchpad.net/pyopenssl>) is required for any SSL APIs. On
+ Windows, version 0.10 or newer is required. pyOpenSSL 0.10 or newer is also
+ preferred on other platforms, but older versions will work as well.
+
+ On Windows pywin32 (<http://sourceforge.net/projects/pywin32/files/>) is
+ required. Build 215 or later is highly recommended for reliable operation
+ (this is already included in ActivePython).
+
+ If you would like to use Trial's subunit reporter, then you will need to
+ install Subunit 0.0.2 or later (https://launchpad.net/subunit).
+
+Installation
+
+ * Debian and Ubuntu
+ Packages are included in the main distribution.
+
+ * FreeBSD, Gentoo
+ Twisted is in their package repositories.
+
+ * Win32
+ Installers are available from http://twistedmatrix.com/
+
+ * Other
+ As with other Python packages, the standard way of installing from source
+ is:
+
+ python setup.py install
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..159debb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,57 @@
+Copyright (c) 2001-2012
+Allen Short
+Andy Gayton
+Andrew Bennetts
+Antoine Pitrou
+Apple Computer, Inc.
+Benjamin Bruheim
+Bob Ippolito
+Canonical Limited
+Christopher Armstrong
+David Reid
+Donovan Preston
+Eric Mangold
+Eyal Lotem
+Itamar Turner-Trauring
+James Knight
+Jason A. Mobarak
+Jean-Paul Calderone
+Jessica McKellar
+Jonathan Jacobs
+Jonathan Lange
+Jonathan D. Simms
+Jürgen Hermann
+Kevin Horn
+Kevin Turner
+Mary Gardiner
+Matthew Lefkowitz
+Massachusetts Institute of Technology
+Moshe Zadka
+Paul Swartz
+Pavel Pergamenshchik
+Ralph Meijer
+Sean Riley
+Software Freedom Conservancy
+Travis B. Hartwell
+Thijs Triemstra
+Thomas Herve
+Timothy Allen
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..0f5f154
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,3168 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Core 12.1.0 (2012-06-02)
+================================
+
+Features
+--------
+ - The kqueue reactor has been revived. (#1918)
+ - twisted.python.filepath now provides IFilePath, an interface for
+ file path objects. (#2176)
+ - New gtk3 and gobject-introspection reactors have been added.
+ (#4558)
+ - gtk and glib reactors now run I/O and scheduled events with lower
+ priority, to ensure the UI stays responsive. (#5067)
+ - IReactorTCP.connectTCP() can now accept IPv6 address literals
+ (although not hostnames) in order to support connecting to IPv6
+ hosts. (#5085)
+ - twisted.internet.interfaces.IReactorSocket, a new interface, is now
+ supported by some reactors to listen on sockets set up by external
+ software (eg systemd or launchd). (#5248)
+ - twisted.internet.endpoints.clientFromString now also supports
+ strings in the form of tcp:example.com:80 and ssl:example.com:4321
+ (#5358)
+ - twisted.python.constants.Flags now provides a way to define
+ collections of flags for bitvector-type uses. (#5384)
+ - The epoll(7)-based reactor is now the default reactor on Linux.
+ (#5478)
+ - twisted.python.runtime.platform.isLinux can be used to check if
+ Twisted is running on Linux. (#5491)
+ - twisted.internet.endpoints.serverFromString now recognizes a
+ "systemd" endpoint type, for listening on a server port inherited
+ from systemd. (#5575)
+ - Connections created using twisted.internet.interfaces.IReactorUNIX
+ now support sending and receiving file descriptors between
+ different processes. (#5615)
+ - twisted.internet.endpoints.clientFromString now supports UNIX
+ client endpoint strings with the path argument specified like
+ "unix:/foo/bar" in addition to the old style, "unix:path=/foo/bar".
+ (#5640)
+ - twisted.protocols.amp.Descriptor is a new AMP argument type which
+ supports passing file descriptors as AMP command arguments over
+ UNIX connections. (#5650)
+
+Bugfixes
+--------
+ - twisted.internet.abstract.FileDescriptor implements
+ twisted.internet.interfaces.IPushProducer instead of
+ twisted.internet.interfaces.IProducer.
+ twisted.internet.iocpreactor.abstract.FileHandle implements
+ twisted.internet.interfaces.IPushProducer instead of
+ twisted.internet.interfaces.IProducer. (#4386)
+ - The epoll reactor now supports reading/writing to regular files on
+ stdin/stdout. (#4429)
+ - Calling .cancel() on any Twisted-provided client endpoint
+ (TCP4ClientEndpoint, UNIXClientEndpoint, SSL4ClientEndpoint) now
+ works as documented, rather than logging an AlreadyCalledError.
+ (#4710)
+ - A leak of OVERLAPPED structures in some IOCP error cases has been
+ fixed. (#5372)
+ - twisted.internet._pollingfile._PollableWritePipe now checks for
+ outgoing unicode data in write() and writeSequence() instead of
+ checkWork(). (#5412)
+
+Improved Documentation
+----------------------
+ - "Working from Twisted's Subversion repository" links to UQDS and
+ Combinator are now updated. (#5545)
+ - Added tkinterdemo.py, an example of Tkinter integration. (#5631)
+
+Deprecations and Removals
+-------------------------
+ - The 'unsigned' flag to twisted.scripts.tap2rpm.MyOptions is now
+ deprecated. (#4086)
+ - Removed the unreachable _fileUrandom method from
+ twisted.python.randbytes.RandomFactory. (#4530)
+ - twisted.persisted.journal is removed, deprecated since Twisted
+ 11.0. (#4805)
+ - Support for pyOpenSSL 0.9 and older is now deprecated. pyOpenSSL
+ 0.10 or newer will soon be required in order to use Twisted's SSL
+ features. (#4974)
+ - backwardsCompatImplements and fixClassImplements are removed from
+ twisted.python.components, deprecated in 2006. (#5034)
+ - twisted.python.reflect.macro was removed, deprecated since Twisted
+ 8.2. (#5035)
+ - twisted.python.text.docstringLStrip, deprecated since Twisted
+ 10.2.0, has been removed (#5036)
+ - Removed the deprecated dispatch and dispatchWithCallback methods
+ from twisted.python.threadpool.ThreadPool (deprecated since 8.0)
+ (#5037)
+ - twisted.scripts.tapconvert is now deprecated. (#5038)
+ - twisted.python.reflect's Settable, AccessorType, PropertyAccessor,
+ Accessor, OriginalAccessor and Summer are now deprecated. (#5451)
+ - twisted.python.threadpool.ThreadSafeList (deprecated in 10.1) is
+ removed. (#5473)
+ - twisted.application.app.initialLog, deprecated since Twisted 8.2.0,
+ has been removed. (#5480)
+ - twisted.spread.refpath was deleted, deprecated since Twisted 9.0.
+ (#5482)
+ - twisted.python.otp, deprecated since 9.0, is removed. (#5493)
+ - Removed `dsu`, `moduleMovedForSplit`, and `dict` from
+ twisted.python.util (deprecated since 10.2) (#5516)
+
+Other
+-----
+ - #2723, #3114, #3398, #4388, #4489, #5055, #5116, #5242, #5380,
+ #5392, #5447, #5457, #5484, #5489, #5492, #5494, #5512, #5523,
+ #5558, #5572, #5583, #5593, #5620, #5621, #5623, #5625, #5637,
+ #5652, #5653, #5656, #5657, #5660, #5673
+
+
+Twisted Conch 12.1.0 (2012-06-02)
+=================================
+
+Features
+--------
+ - twisted.conch.tap now supports cred plugins (#4753)
+
+Bugfixes
+--------
+ - twisted.conch.client.knownhosts now handles errors encountered
+ parsing hashed entries in a known hosts file. (#5616)
+
+Improved Documentation
+----------------------
+ - Conch examples window.tac and telnet_echo.tac now have better
+ explanations. (#5590)
+
+Other
+-----
+ - #5580
+
+
+Twisted Lore 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twisted.plugins.twisted_lore's MathProcessor plugin is now
+ associated with the correct implementation module. (#5326)
+
+
+Twisted Mail 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twistd mail --auth, broken in 11.0, now correctly connects
+ authentication to the portal being used (#5219)
+
+Other
+-----
+ - #5686
+
+
+Twisted Names 12.1.0 (2012-06-02)
+=================================
+
+Features
+--------
+ - "twistd dns" secondary server functionality and
+ twisted.names.secondary now support retrieving zone information
+ from a master running on a non-standard DNS port. (#5468)
+
+Bugfixes
+--------
+ - twisted.names.dns.DNSProtocol instances no longer throw an
+ exception when disconnecting. (#5471)
+ - twisted.names.tap.makeService (thus also "twistd dns") now makes a
+ DNS server which gives precedence to the hosts file from its
+ configuration over the remote DNS servers from its configuration.
+ (#5524)
+ - twisted.name.cache.CacheResolver now makes sure TTLs on returned
+ results are never negative. (#5579)
+ - twisted.names.cache.CacheResolver entries added via the initializer
+ are now timed out correctly. (#5638)
+
+Improved Documentation
+----------------------
+ - The examples now contain instructions on how to run them and
+ descriptions in the examples index. (#5588)
+
+Deprecations and Removals
+-------------------------
+ - The deprecated twisted.names.dns.Record_mx.exchange attribute was
+ removed. (#4549)
+
+
+Twisted News 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twisted.news.nntp.NNTPServer now has additional test coverage and
+ less redundant implementation code. (#5537)
+
+Deprecations and Removals
+-------------------------
+ - The ability to pass a string article to NNTPServer._gotBody and
+ NNTPServer._gotArticle in t.news.nntp has been deprecated for years
+ and is now removed. (#4548)
+
+
+Twisted Pair 12.1.0 (2012-06-02)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 12.1.0 (2012-06-02)
+==================================
+
+Deprecations and Removals
+-------------------------
+ - ProcessMonitor.active, consistencyDelay, and consistency in
+ twisted.runner.procmon were deprecated since 10.1 have been
+ removed. (#5517)
+
+
+Twisted Web 12.1.0 (2012-06-02)
+===============================
+
+Features
+--------
+ - twisted.web.client.Agent and ProxyAgent now support persistent
+ connections. (#3420)
+ - Added twisted.web.template.renderElement, a function which renders
+ an Element to a response. (#5395)
+ - twisted.web.client.HTTPConnectionPool now ensures that failed
+ queries on persistent connections are retried, when possible.
+ (#5479)
+ - twisted.web.template.XMLFile now supports FilePath objects. (#5509)
+ - twisted.web.template.renderElement takes a doctype keyword
+ argument, which will be written as the first line of the response,
+ defaulting to the HTML5 doctype. (#5560)
+
+Bugfixes
+--------
+ - twisted.web.util.formatFailure now quotes all data in its output to
+ avoid it being mistakenly interpreted as markup. (#4896)
+ - twisted.web.distrib now lets distributed servers set the response
+ message. (#5525)
+
+Deprecations and Removals
+-------------------------
+ - PHP3Script and PHPScript were removed from twisted.web.twcgi,
+ deprecated since 10.1. Use twcgi.FilteredScript instead. (#5456)
+ - twisted.web.template.XMLFile's support for file objects and
+ filenames is now deprecated. Use the new support for FilePath
+ objects. (#5509)
+ - twisted.web.server.date_time_string and
+ twisted.web.server.string_date_time are now deprecated in favor of
+ twisted.web.http.datetimeToString and twisted.web.
+ http.stringToDatetime (#5535)
+
+Other
+-----
+ - #4966, #5460, #5490, #5591, #5602, #5609, #5612
+
+
+Twisted Words 12.1.0 (2012-06-02)
+=================================
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.DccChatFactory.buildProtocol now
+ returns the protocol object that it creates (#3179)
+ - twisted.words.im no longer offers an empty threat of a rewrite on
+ import. (#5598)
+
+Other
+-----
+ - #5555, #5595
+
+
+Twisted Core 12.0.0 (2012-02-10)
+================================
+
+Features
+--------
+ - The interface argument to IReactorTCP.listenTCP may now be an IPv6
+ address literal, allowing the creation of IPv6 TCP servers. (#5084)
+ - twisted.python.constants.Names now provides a way to define
+ collections of named constants, similar to the "enum type" feature
+ of C or Java. (#5382)
+ - twisted.python.constants.Values now provides a way to define
+ collections of named constants with arbitrary values. (#5383)
+
+Bugfixes
+--------
+ - Fixed an obscure case where connectionLost wasn't called on the
+ protocol when using half-close. (#3037)
+ - UDP ports handle socket errors better on Windows. (#3396)
+ - When idle, the gtk2 and glib2 reactors no longer wake up 10 times a
+ second. (#4376)
+ - Prevent a rare situation involving TLS transports, where a producer
+ may be erroneously left unpaused. (#5347)
+ - twisted.internet.iocpreactor.iocpsupport now has fewer 64-bit
+ compile warnings. (#5373)
+ - The GTK2 reactor is now more responsive on Windows. (#5396)
+ - TLS transports now correctly handle producer registration after the
+ connection has been lost. (#5439)
+ - twisted.protocols.htb.Bucket now empties properly with a non-zero
+ drip rate. (#5448)
+ - IReactorSSL and ITCPTransport.startTLS now synchronously propagate
+ errors from the getContext method of context factories, instead of
+ being capturing them and logging them as unhandled. (#5449)
+
+Improved Documentation
+----------------------
+ - The multicast documentation has been expanded. (#4262)
+ - twisted.internet.defer.Deferred now documents more return values.
+ (#5399)
+ - Show a better starting page at
+ http://twistedmatrix.com/documents/current (#5429)
+
+Deprecations and Removals
+-------------------------
+ - Remove the deprecated module twisted.enterprise.reflector. (#4108)
+ - Removed the deprecated module twisted.enterprise.row. (#4109)
+ - Remove the deprecated module twisted.enterprise.sqlreflector.
+ (#4110)
+ - Removed the deprecated module twisted.enterprise.util, as well as
+ twisted.enterprise.adbapi.safe. (#4111)
+ - Python 2.4 is no longer supported on any platform. (#5060)
+ - Removed printTraceback and noOperation from twisted.spread.pb,
+ deprecated since Twisted 8.2. (#5370)
+
+Other
+-----
+ - #1712, #2725, #5284, #5325, #5331, #5362, #5364, #5371, #5407,
+ #5427, #5430, #5431, #5440, #5441
+
+
+Twisted Conch 12.0.0 (2012-02-10)
+=================================
+
+Features
+--------
+ - use Python shadow module for authentication if it's available
+ (#3242)
+
+Bugfixes
+--------
+ - twisted.conch.ssh.transport.messages no longer ends with with old
+ message IDs on platforms with differing dict() orderings (#5352)
+
+Other
+-----
+ - #5225
+
+
+Twisted Lore 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Mail 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Names 12.0.0 (2012-02-10)
+=================================
+
+Bugfixes
+--------
+ - twisted.names.dns.Message now sets the `auth` flag on RRHeader
+ instances it creates to reflect the authority of the message
+ itself. (#5421)
+
+
+Twisted News 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 12.0.0 (2012-02-10)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Web 12.0.0 (2012-02-10)
+===============================
+
+Features
+--------
+ - twisted.web.util.redirectTo now raises TypeError if the URL passed
+ to it is a unicode string instead of a byte string. (#5236)
+ - The new class twisted.web.template.CharRef provides support for
+ inserting numeric character references in output generated by
+ twisted.web.template. (#5408)
+
+Improved Documentation
+----------------------
+ - The Twisted Web howto now has a section on proxies and reverse
+ proxies. (#399)
+ - The web client howto now covers ContentDecoderAgent and links to an
+ example of its use. (#5415)
+
+Other
+-----
+ - #5404, #5438
+
+
+Twisted Words 12.0.0 (2012-02-10)
+=================================
+
+Improved Documentation
+----------------------
+ - twisted.words.im.basechat now has improved API documentation.
+ (#2458)
+
+Other
+-----
+ - #5401
+
+
+Twisted Core 11.1.0 (2011-11-15)
+================================
+
+Features
+--------
+ - TCP and TLS transports now support abortConnection() which, unlike
+ loseConnection(), always closes the connection immediately. (#78)
+ - Failures received over PB when tracebacks are disabled now display
+ the wrapped exception value when they are printed. (#581)
+ - twistd now has a --logger option, allowing the use of custom log
+ observers. (#638)
+ - The default reactor is now poll(2) on platforms that support it.
+ (#2234)
+ - twisted.internet.defer.inlineCallbacks(f) now raises TypeError when
+ f returns something other than a generator or uses returnValue as a
+ non-generator. (#2501)
+ - twisted.python.usage.Options now supports performing Zsh tab-
+ completion on demand. Tab-completion for Twisted commands is
+ supported out-of-the-box on any recent zsh release. Third-party
+ commands may take advantage of zsh completion by copying the
+ provided stub file. (#3078)
+ - twisted.protocols.portforward now uses flow control between its
+ client and server connections to avoid having to buffer an
+ unbounded amount of data when one connection is slower than the
+ other. (#3350)
+ - On Windows, the select, IOCP, and Gtk2 reactors now implement
+ IReactorWin32Events (most notably adding support for serial ports
+ to these reactors). (#4862)
+ - twisted.python.failure.Failure no longer captures the state of
+ locals and globals of all stack frames by default, because it is
+ expensive to do and rarely used. You can pass captureVars=True to
+ Failure's constructor if you want to capture this data. (#5011)
+ - twisted.web.client now supports automatic content-decoding via
+ twisted.web.client.ContentDecoderAgent, gzip being supported for
+ now. (#5053)
+ - Protocols may now implement ILoggingContext to customize their
+ logging prefix. twisted.protocols.policies.ProtocolWrapper and the
+ endpoints wrapper now take advantage of this feature to ensure the
+ application protocol is still reflected in logs. (#5062)
+ - AMP's raw message-parsing performance was increased by
+ approximately 12%. (#5075)
+ - Twisted is now installable on PyPy, because some incompatible C
+ extensions are no longer built. (#5158)
+ - twisted.internet.defer.gatherResults now accepts a consumeErrors
+ parameter, with the same meaning as the corresponding argument for
+ DeferredList. (#5159)
+ - Added RMD (remove directory) support to the FTP client. (#5259)
+ - Server factories may now implement ILoggingContext to customize the
+ name that is logged when the reactor uses one to start listening on
+ a port. (#5292)
+ - The implementations of ITransport.writeSequence will now raise
+ TypeError if passed unicode strings. (#3896)
+ - iocp reactor now operates correctly on 64 bit Python runtimes.
+ (#4669)
+ - twistd ftp now supports the cred plugin. (#4752)
+ - twisted.python.filepath.FilePath now has an API to retrieve the
+ permissions of the underlying file, and two methods to determine
+ whether it is a block device or a socket. (#4813)
+ - twisted.trial.unittest.TestCase is now compatible with Python 2.7's
+ assertDictEqual method. (#5291)
+
+Bugfixes
+--------
+ - The IOCP reactor now does not try to erroneously pause non-
+ streaming producers. (#745)
+ - Unicode print statements no longer blow up when using Twisted's
+ logging system. (#1990)
+ - Process transports on Windows now support the `writeToChild` method
+ (but only for stdin). (#2838)
+ - Zsh tab-completion of Twisted commands no longer relies on
+ statically generated files, but instead generates results on-the-
+ fly - ensuring accurate tab-completion for the version of Twisted
+ actually in use. (#3078)
+ - LogPublishers don't use the global log publisher for reporting
+ broken observers anymore. (#3307)
+ - trial and twistd now add the current directory to sys.path even
+ when running as root or on Windows. mktap, tapconvert, and
+ pyhtmlizer no longer add the current directory to sys.path. (#3526)
+ - twisted.internet.win32eventreactor now stops immediately if
+ reactor.stop() is called from an IWriteDescriptor.doWrite
+ implementation instead of delaying shutdown for an arbitrary period
+ of time. (#3824)
+ - twisted.python.log now handles RuntimeErrors more gracefully, and
+ always restores log observers after an exception is raised. (#4379)
+ - twisted.spread now supports updating new-style RemoteCache
+ instances. (#4447)
+ - twisted.spread.pb.CopiedFailure will no longer be thrown into a
+ generator as a (deprecated) string exception but as a
+ twisted.spread.pb.RemoteException. (#4520)
+ - trial now gracefully handles the presence of objects in sys.modules
+ which respond to attributes being set on them by modifying
+ sys.modules. (#4748)
+ - twisted.python.deprecate.deprecatedModuleAttribute no longer
+ spuriously warns twice when used to deprecate a module within a
+ package. This should make it easier to write unit tests for
+ deprecated modules. (#4806)
+ - When pyOpenSSL 0.10 or newer is available, SSL support now uses
+ Twisted for all I/O and only relies on OpenSSL for cryptography,
+ avoiding a number of tricky, potentially broken edge cases. (#4854)
+ - IStreamClientEndpointStringParser.parseStreamClient now correctly
+ describes how it will be called by clientFromString (#4956)
+ - twisted.internet.defer.Deferreds are 10 times faster at handling
+ exceptions raised from callbacks, except when setDebugging(True)
+ has been called. (#5011)
+ - twisted.python.filepath.FilePath.copyTo now raises OSError(ENOENT)
+ if the source path being copied does not exist. (#5017)
+ - twisted.python.modules now supports iterating over namespace
+ packages without yielding duplicates. (#5030)
+ - reactor.spawnProcess now uses the resource module to guess the
+ maximum possible open file descriptor when /dev/fd exists but gives
+ incorrect results. (#5052)
+ - The memory BIO TLS/SSL implementation now supports producers
+ correctly. (#5063)
+ - twisted.spread.pb.Broker no longer creates an uncollectable
+ reference cycle when the logout callback holds a reference to the
+ client mind object. (#5079)
+ - twisted.protocols.tls, and SSL/TLS support in general, now do clean
+ TLS close alerts when disconnecting. (#5118)
+ - twisted.persisted.styles no longer uses the deprecated allYourBase
+ function (#5193)
+ - Stream client endpoints now start (doStart) and stop (doStop) the
+ factory passed to the connect method, instead of a different
+ implementation-detail factory. (#5278)
+ - SSL ports now consistently report themselves as SSL rather than TCP
+ when logging their close message. (#5292)
+ - Serial ports now deliver connectionLost to the protocol when
+ closed. (#3690)
+ - win32eventreactor now behaves better in certain rare cases in which
+ it previously would have failed to deliver connection lost
+ notification to a protocol. (#5233)
+
+Improved Documentation
+----------------------
+ - Test driven development with Twisted and Trial is now documented in
+ a how-to. (#2443)
+ - A new howto-style document covering twisted.protocols.amp has been
+ added. (#3476)
+ - Added sample implementation of a Twisted push producer/consumer
+ system. (#3835)
+ - The "Deferred in Depth" tutorial now includes accurate output for
+ the deferred_ex2.py example. (#3941)
+ - The server howto now covers the Factory.buildProtocol method.
+ (#4761)
+ - The testing standard and the trial tutorial now recommend the
+ `assertEqual` form of assertions rather than the `assertEquals` to
+ coincide with the standard library unittest's preference. (#4989)
+ - twisted.python.filepath.FilePath's methods now have more complete
+ API documentation (docstrings). (#5027)
+ - The Clients howto now uses buildProtocol more explicitly, hopefully
+ making it easier to understand where Protocol instances come from.
+ (#5044)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.interfaces.IFinishableConsumer is now deprecated.
+ (#2661)
+ - twisted.python.zshcomp is now deprecated in favor of the tab-
+ completion system in twisted.python.usage (#3078)
+ - The unzip and unzipIter functions in twisted.python.zipstream are
+ now deprecated. (#3666)
+ - Options.optStrings, deprecated for 7 years, has been removed. Use
+ Options.optParameters instead. (#4552)
+ - Removed the deprecated twisted.python.dispatch module. (#5023)
+ - Removed the twisted.runner.procutils module that was deprecated in
+ Twisted 2.3. (#5049)
+ - Removed twisted.trial.runner.DocTestSuite, deprecated in Twisted
+ 8.0. (#5111)
+ - twisted.scripts.tkunzip is now deprecated. (#5140)
+ - Deprecated option --password-file in twistd ftp (#4752)
+ - mktap, deprecated since Twisted 8.0, has been removed. (#5293)
+
+Other
+-----
+ - #1946, #2562, #2674, #3074, #3077, #3776, #4227, #4539, #4587,
+ #4619, #4624, #4629, #4683, #4690, #4702, #4778, #4944, #4945,
+ #4949, #4952, #4957, #4979, #4980, #4987, #4990, #4994, #4995,
+ #4997, #5003, #5008, #5009, #5012, #5019, #5042, #5046, #5051,
+ #5065, #5083, #5088, #5089, #5090, #5101, #5108, #5109, #5112,
+ #5114, #5125, #5128, #5131, #5136, #5139, #5144, #5146, #5147,
+ #5156, #5160, #5165, #5191, #5205, #5215, #5217, #5218, #5223,
+ #5243, #5244, #5250, #5254, #5261, #5266, #5273, #5299, #5301,
+ #5302, #5304, #5308, #5311, #5321, #5322, #5327, #5328, #5332,
+ #5336
+
+
+Twisted Conch 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.conch.ssh.filetransfer.FileTransferClient now handles short
+ status messages, not strictly allowed by the RFC, but sent by some
+ SSH implementations. (#3009)
+ - twisted.conch.manhole now supports CTRL-A and CTRL-E to trigger
+ HOME and END functions respectively. (#5252)
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout, the
+ bin/conch/ scripts will now use the version of Twisted they are
+ part of. (#3526)
+ - twisted.conch.insults.window.ScrolledArea now passes no extra
+ arguments to object.__init__ (which works on more versions of
+ Python). (#4197)
+ - twisted.conch.telnet.ITelnetProtocol now has the correct signature
+ for its unhandledSubnegotiation() method. (#4751)
+ - twisted.conch.ssh.userauth.SSHUserAuthClient now more closely
+ follows the RFC 4251 definition of boolean values when negotiating
+ for key-based authentication, allowing better interoperability with
+ other SSH implementations. (#5241)
+ - twisted.conch.recvline.RecvLine now ignores certain function keys
+ in its keystrokeReceived method instead of raising an exception.
+ (#5246)
+
+Deprecations and Removals
+-------------------------
+ - The --user option to `twistd manhole' has been removed as it was
+ dead code with no functionality associated with it. (#5283)
+
+Other
+-----
+ - #5107, #5256, #5349
+
+
+Twisted Lore 11.1.0 (2011-11-15)
+================================
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout,
+ bin/lore/lore will now use the version of Twisted it is part of.
+ (#3526)
+
+Deprecations and Removals
+-------------------------
+ - Removed compareMarkPos and comparePosition from lore.tree,
+ deprecated in Twisted 9.0. (#5127)
+
+
+Twisted Mail 11.1.0 (2011-11-15)
+================================
+
+Features
+--------
+ - twisted.mail.smtp.LOGINCredentials now generates challenges with
+ ":" instead of "\0" for interoperability with Microsoft Outlook.
+ (#4692)
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout,
+ bin/mail/mailmail will now use the version of Twisted it is part
+ of. (#3526)
+
+Other
+-----
+ - #4796, #5006
+
+
+Twisted Names 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.names.dns.Message now parses records of unknown type into
+ instances of a new `UnknownType` class. (#4603)
+
+Bugfixes
+--------
+ - twisted.names.dns.Name now detects loops in names it is decoding
+ and raises an exception. Previously it would follow the loop
+ forever, allowing a remote denial of service attack against any
+ twisted.names client or server. (#5064)
+ - twisted.names.hosts.Resolver now supports IPv6 addresses; its
+ lookupAddress method now filters them out and its lookupIPV6Address
+ method is now implemented. (#5098)
+
+
+Twisted News 11.1.0 (2011-11-15)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 11.1.0 (2011-11-15)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 11.1.0 (2011-11-15)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Web 11.1.0 (2011-11-15)
+===============================
+
+Features
+--------
+ - twisted.web.client.ProxyAgent is a new HTTP/1.1 web client which
+ adds proxy support. (#1774)
+ - twisted.web.client.Agent now takes optional connectTimeout and
+ bindAddress arguments which are forwarded to the subsequent
+ connectTCP/connectSSL call. (#3450)
+ - The new class twisted.web.client.FileBodyProducer makes it easy to
+ upload data in HTTP requests made using the Agent client APIs.
+ (#4017)
+ - twisted.web.xmlrpc.XMLRPC now allows its lookupProcedure method to
+ be overridden to change how XML-RPC procedures are dispatched.
+ (#4836)
+ - A new HTTP cookie-aware Twisted Web Agent wrapper is included in
+ twisted.web.client.CookieAgent (#4922)
+ - New class twisted.web.template.TagLoader provides an
+ ITemplateLoader implementation which loads already-created
+ twisted.web.iweb.IRenderable providers. (#5040)
+ - The new class twisted.web.client.RedirectAgent adds redirect
+ support to the HTTP 1.1 client stack. (#5157)
+ - twisted.web.template now supports HTML tags from the HTML5
+ standard, including <canvas> and <video>. (#5306)
+
+Bugfixes
+--------
+ - twisted.web.client.getPage and .downloadPage now only fire their
+ result Deferred after the underlying connection they use has been
+ closed. (#3796)
+ - twisted.web.server now omits the default Content-Type header from
+ NOT MODIFIED responses. (#4156)
+ - twisted.web.server now responds correctly to 'Expect: 100-continue'
+ headers, although this is not yet usefully exposed to user code.
+ (#4673)
+ - twisted.web.client.Agent no longer raises an exception if a server
+ responds and closes the connection before the request has been
+ fully transmitted. (#5013)
+ - twisted.web.http_headers.Headers now correctly capitalizes the
+ header names Content-MD5, DNT, ETag, P3P, TE, and X-XSS-Protection.
+ (#5054)
+ - twisted.web.template now escapes more inputs to comments which
+ require escaping in the output. (#5275)
+
+Improved Documentation
+----------------------
+ - The twisted.web.template howto now documents the common idiom of
+ yielding tag clones from a renderer. (#5286)
+ - CookieAgent is now documented in the twisted.web.client how-to.
+ (#5110)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.google is now deprecated. (#5209)
+
+Other
+-----
+ - #4951, #5057, #5175, #5288, #5316
+
+
+Twisted Words 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient now uses a PING heartbeat as
+ a keepalive to avoid losing an IRC connection without being aware
+ of it. (#5047)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient now replies only once to
+ known CTCP queries per message and not at all to unknown CTCP
+ queries. (#5029)
+ - IRCClient.msg now determines a safe maximum command length,
+ drastically reducing the chance of relayed text being truncated on
+ the server side. (#5176)
+
+Deprecations and Removals
+-------------------------
+ - twisted.words.protocols.irc.IRCClient.me was deprecated in Twisted
+ 9.0 and has been removed. Use IRCClient.describe instead. (#5059)
+
+Other
+-----
+ - #5025, #5330
+
+
+Twisted Core 11.0.0 (2011-04-01)
+================================
+
+Features
+--------
+ - The reactor is not restartable, but it would previously fail to
+ complain. Now, when you restart an unrestartable reactor, you get
+ an exception. (#2066)
+ - twisted.plugin now only emits a short log message, rather than a
+ full traceback, if there is a problem writing out the dropin cache
+ file. (#2409)
+ - Added a 'replacement' parameter to the
+ 'twisted.python.deprecate.deprecated' decorator. This allows
+ deprecations to unambiguously specify what they have been
+ deprecated in favor of. (#3047)
+ - Added access methods to FilePath for FilePath.statinfo's st_ino,
+ st_dev, st_nlink, st_uid, and st_gid fields. This is in
+ preparation for the deprecation of FilePath.statinfo. (#4712)
+ - IPv4Address and UNIXAddress now have a __hash__ method. (#4783)
+ - twisted.protocols.ftp.FTP.ftp_STOR now catches `FTPCmdError`s
+ raised by the file writer, and returns the error back to the
+ client. (#4909)
+
+Bugfixes
+--------
+ - twistd will no longer fail if a non-root user passes --uid 'myuid'
+ as a command-line argument. Instead, it will emit an error message.
+ (#3172)
+ - IOCPReactor now sends immediate completions to the main loop
+ (#3233)
+ - trial can now load test methods from multiple classes, even if the
+ methods all happen to be inherited from the same base class.
+ (#3383)
+ - twisted.web.server will now produce a correct Allow header when a
+ particular render_FOO method is missing. (#3678)
+ - HEAD requests made to resources whose HEAD handling defaults to
+ calling render_GET now always receive a response with no body.
+ (#3684)
+ - trial now loads decorated test methods whether or not the decorator
+ preserves the original method name. (#3909)
+ - t.p.amp.AmpBox.serialize will now correctly consistently complain
+ when being fed Unicode. (#3931)
+ - twisted.internet.wxreactor now supports stopping more reliably.
+ (#3948)
+ - reactor.spawnProcess on Windows can now handle ASCII-encodable
+ Unicode strings in the system environment (#3964)
+ - When C-extensions are not complied for twisted, on python2.4, skip
+ a test in twisted.internet.test.test_process that may hang due to a
+ SIGCHLD related problem. Running 'python setup.py build_ext
+ --inplace' will compile the extension and cause the test to both
+ run and pass. (#4331)
+ - twisted.python.logfile.LogFile now raises a descriptive exception
+ when passed a log directoy which does not exist. (#4701)
+ - Fixed a bug where Inotify will fail to add a filepatch to watchlist
+ after it has been added/ignored previously. (#4708)
+ - IPv4Address and UNIXAddress object comparison operators fixed
+ (#4817)
+ - twisted.internet.task.Clock now sorts the list of pending calls
+ before and after processing each call (#4823)
+ - ConnectionLost is now in twisted.internet.error.__all__ instead of
+ twisted.words.protocols.jabber.xmlstream.__all__. (#4856)
+ - twisted.internet.process now detects the most appropriate mechanism
+ to use for detecting the open file descriptors on a system, getting
+ Twisted working on FreeBSD even when fdescfs is not mounted.
+ (#4881)
+ - twisted.words.services referenced nonexistent
+ twisted.words.protocols.irc.IRC_NOSUCHCHANNEL. This has been fixed.
+ Related code has also received test cases. (#4915)
+
+Improved Documentation
+----------------------
+ - The INSTALL file now lists all of Twisted's dependencies. (#967)
+ - Added the stopService and startService methods to all finger
+ example files. (#3375)
+ - Missing reactor.run() calls were added in the UDP and client howto
+ documents. (#3834)
+ - The maxRetries attribute of
+ twisted.internet.protocols.RetryingClientFactory now has API
+ documentation. (#4618)
+ - Lore docs pointed to a template that no longer existed, this has
+ been fixed. (#4682)
+ - The `servers` argument to `twisted.names.client.createResolver` now
+ has more complete API documentation. (#4713)
+ - Linked to the Twisted endpoints tutorial from the Twisted core
+ howto list. (#4773)
+ - The Endpoints howto now links to the API documentation. (#4774)
+ - The Quotes howto is now more clear in its PYTHONPATH setup
+ instructions. (#4785)
+ - The API documentation for DeferredList's fireOnOneCallback
+ parameter now gives the correct order of the elements of the result
+ tuple. (#4882)
+
+Deprecations and Removals
+-------------------------
+ - returning a value other than None from IProtocol.dataReceived was
+ deprecated (#2491)
+ - Deprecated the --extra option in trial. (#3372)
+ - twisted.protocols._c_urlarg has been removed. (#4162)
+ - Remove the --report-profile option for twistd, deprecated since
+ 2007. (#4236)
+ - Deprecated twisted.persisted.journal. This library is no longer
+ maintained. (#4298)
+ - Removed twisted.protocols.loopback.loopback, which has been
+ deprecated since Twisted 2.5. (#4547)
+ - __getitem__ __getslice__ and __eq__ (tuple comparison, indexing)
+ removed from twisted.internet.address.IPv4Address and
+ twisted.internet.address.UNIXAddress classes UNIXAddress and
+ IPv4Address properties _bwHack are now deprecated in
+ twisted.internet.address (#4817)
+ - twisted.python.reflect.allYourBase is now no longer used, replaced
+ with inspect.getmro (#4928)
+ - allYourBase and accumulateBases are now deprecated in favor of
+ inspect.getmro. (#4946)
+
+Other
+-----
+
+- #555, #1982, #2618, #2665, #2666, #4035, #4247, #4567, #4636,
+ #4717, #4733, #4750, #4821, #4842, #4846, #4853, #4857, #4858,
+ #4863, #4864, #4865, #4866, #4867, #4868, #4869, #4870, #4871,
+ #4872, #4873, #4874, #4875, #4876, #4877, #4878, #4879, #4905,
+ #4906, #4908, #4934, #4955, #4960
+
+
+Twisted Conch 11.0.0 (2011-04-01)
+=================================
+
+Bugfixes
+--------
+ - The transport for subsystem protocols now declares that it
+ implements ITransport and implements the getHost and getPeer
+ methods. (#2453)
+ - twisted.conch.ssh.transport.SSHTransportBase now responds to key
+ exchange messages at any time during a connection (instead of only
+ at connection setup). It also queues non-key exchange messages
+ sent during key exchange to avoid corrupting the connection state.
+ (#4395)
+ - Importing twisted.conch.ssh.common no longer breaks pow(base, exp[,
+ modulus]) when the gmpy package is installed and base is not an
+ integer. (#4803)
+ - twisted.conch.ls.lsLine now returns a time string which does not
+ consider the locale. (#4937)
+
+Improved Documentation
+----------------------
+ - Changed the man page for ckeygen to accurately reflect what it
+ does, and corrected its synposis so that a second "ckeygen" is not
+ a required part of the ckeygen command line. (#4738)
+
+Other
+-----
+ - #2112
+
+
+Twisted Lore 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Mail 11.0.0 (2011-04-01)
+================================
+
+Features
+--------
+ - The `twistd mail` command line now accepts endpoint descriptions
+ for POP3 and SMTP servers. (#4739)
+ - The twistd mail plugin now accepts new authentication options via
+ strcred.AuthOptionMixin. These include --auth, --auth-help, and
+ authentication type-specific help options. (#4740)
+
+Bugfixes
+--------
+ - twisted.mail.imap4.IMAP4Server now generates INTERNALDATE strings
+ which do not consider the locale. (#4937)
+
+Improved Documentation
+----------------------
+ - Added a simple SMTP example, showing how to use sendmail. (#4042)
+
+Other
+-----
+
+ - #4162
+
+
+Twisted Names 11.0.0 (2011-04-01)
+=================================
+
+No significant changes have been made for this release.
+
+
+Twisted News 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+Other
+-----
+ - #4580
+
+
+Twisted Pair 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 11.0.0 (2011-04-01)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Web 11.0.0 (2011-04-01)
+===============================
+
+Features
+--------
+ - twisted.web._newclient.HTTPParser (and therefore Agent) now handles
+ HTTP headers delimited by bare LF newlines. (#3833)
+ - twisted.web.client.downloadPage now accepts the `afterFoundGet`
+ parameter, with the same meaning as the `getPage` parameter of the
+ same name. (#4364)
+ - twisted.web.xmlrpc.Proxy constructor now takes additional 'timeout'
+ and 'reactor' arguments. The 'timeout' argument defaults to 30
+ seconds. (#4741)
+ - Twisted Web now has a templating system, twisted.web.template,
+ which is a direct, simplified derivative of Divmod Nevow. (#4939)
+
+Bugfixes
+--------
+ - HTTPPageGetter now adds the port to the host header if it is not
+ the default for that scheme. (#3857)
+ - twisted.web.http.Request.write now raises an exception if it is
+ called after response generation has already finished. (#4317)
+ - twisted.web.client.HTTPPageGetter and twisted.web.client.getPage
+ now no longer make two requests when using afterFoundGet. (#4760)
+ - twisted.web.twcgi no longer adds an extra "content-type" header to
+ CGI responses. (#4786)
+ - twisted.web will now properly specify an encoding (UTF-8) on error,
+ redirect, and directory listing pages, so that IE7 and previous
+ will not improperly guess the 'utf7' encoding in these cases.
+ Please note that Twisted still sets a *default* content-type of
+ 'text/html', and you shouldn't rely on that: you should set the
+ encoding appropriately in your application. (#4900)
+ - twisted.web.http.Request.setHost now sets the port in the host
+ header if it is not the default. (#4918)
+ - default NOT_IMPLEMENTED and NOT_ALLOWED pages now quote the request
+ method and URI respectively, to protect against browsers which
+ don't quote those values for us. (#4978)
+
+Improved Documentation
+----------------------
+ - The XML-RPC howto now includes an example demonstrating how to
+ access the HTTP request object in a server-side XML-RPC method.
+ (#4732)
+ - The Twisted Web client howto now uses the correct, public name for
+ twisted.web.client.Response. (#4769)
+ - Some broken links were fixed, descriptions were updated, and new
+ API links were added in the Resource Templating documentation
+ (resource-templates.xhtml) (#4968)
+
+Other
+-----
+ - #2271, #2386, #4162, #4733, #4855, #4911, #4973
+
+
+Twisted Words 11.0.0 (2011-04-01)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient now has an invite method.
+ (#4820)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient.say is once again able to
+ send messages when using the default value for the length limit
+ argument. (#4758)
+ - twisted.words.protocols.jabber.jstrports is once again able to
+ parse jstrport description strings. (#4771)
+ - twisted.words.protocols.msn.NotificationClient now calls the
+ loginFailure callback when it is unable to connect to the Passport
+ server due to missing SSL dependencies. (#4801)
+ - twisted.words.protocols.jabber.xmpp_stringprep now always uses
+ Unicode version 3.2 for stringprep normalization. (#4850)
+
+Improved Documentation
+----------------------
+ - Removed the non-working AIM bot example, depending on the obsolete
+ twisted.words.protocols.toc functionality. (#4007)
+ - Outdated GUI-related information was removed from the IM howto.
+ (#4054)
+
+Deprecations and Removals
+-------------------------
+ - Remove twisted.words.protocols.toc, that was largely non-working
+ and useless since AOL disabled TOC on their AIM network. (#4363)
+
+Other
+-----
+ - #4733, #4902
+
+
+Twisted Core 10.2.0 (2010-11-29)
+================================
+
+Features
+--------
+ - twisted.internet.cfreactor has been significantly improved. It now
+ runs, and passes, the test suite. Many, many bugs in it have been
+ fixed, including several segfaults, as it now uses PyObjC and
+ longer requires C code in Twisted. (#1833)
+ - twisted.protocols.ftp.FTPRealm now accepts a parameter to override
+ "/home" as the container for user directories. The new
+ BaseFTPRealm class in the same module also allows easy
+ implementation of custom user directory schemes. (#2179)
+ - twisted.python.filepath.FilePath and twisted.python.zippath.ZipPath
+ now have a descendant method to simplify code which calls the child
+ method repeatedly. (#3169)
+ - twisted.python.failure._Frame objects now support fake f_locals
+ attribute. (#4045)
+ - twisted.internet.endpoints now has 'serverFromString' and
+ 'clientFromString' APIs for constructing endpoints from descriptive
+ strings. (#4473)
+ - The default trial reporter now combines reporting of tests with the
+ same result to shorten its summary output. (#4487)
+ - The new class twisted.protocols.ftp.SystemFTPRealm implements an
+ FTP realm which uses system accounts to select home directories.
+ (#4494)
+ - twisted.internet.reactor.spawnProcess now wastes less time trying
+ to close non-existent file descriptors on POSIX platforms. (#4522)
+ - twisted.internet.win32eventreactor now declares that it implements
+ a new twisted.internet.interfaces.IReactorWin32Events interface.
+ (#4523)
+ - twisted.application.service.IProcess now documents its attributes
+ using zope.interface.Attribute. (#4534)
+ - twisted.application.app.ReactorSelectionMixin now saves the value
+ of the --reactor option in the "reactor" key of the options object.
+ (#4563)
+ - twisted.internet.endpoints.serverFromString and clientFromString,
+ and therefore also twisted.application.strports.service, now
+ support plugins, so third parties may implement their own endpoint
+ types. (#4695)
+
+Bugfixes
+--------
+ - twisted.internet.defer.Deferred now handles chains iteratively
+ instead of recursively, preventing RuntimeError due to excessive
+ recursion when handling long Deferred chains. (#411)
+ - twisted.internet.cfreactor now works with trial. (#2556)
+ - twisted.enterprise.adbapi.ConnectionPool.close may now be called
+ even if the connection pool has not yet been started. This will
+ prevent the pool from ever starting. (#2680)
+ - twisted.protocols.basic.NetstringReceiver raises
+ NetstringParseErrors for invalid netstrings now. It handles empty
+ netstrings ("0:,") correctly, and the performance for receiving
+ netstrings has been improved. (#4378)
+ - reactor.listenUDP now returns an object which declares that it
+ implements IListeningPort. (#4462)
+ - twisted.python.randbytes no longer uses PyCrypto as a secure random
+ number source (since it is not one). (#4468)
+ - twisted.internet.main.installReactor now blocks installation of
+ another reactor when using python -O (#4476)
+ - twisted.python.deprecate.deprecatedModuleAttribute now emits only
+ one warning when used to deprecate a package attribute which is a
+ module. (#4492)
+ - The "brief" mode of twisted.python.failure.Failure.getTraceback now
+ handles exceptions raised by the underlying exception's __str__
+ method. (#4501)
+ - twisted.words.xish.domish now correctly parses XML with namespaces
+ which include whitespace. (#4503)
+ - twisted.names.authority.FileAuthority now generates correct
+ negative caching hints, marks its referral NS RRs as non-
+ authoritative, and correctly generates referrals for ALL_RECORDS
+ requests. (#4513)
+ - twisted.internet.test.reactormixins.ReactorBuilder's attribute
+ `requiredInterface` (which should an interface) is now
+ `requiredInterfaces` (a list of interfaces) as originally described
+ per the documentation. (#4527)
+ - twisted.python.zippath.ZipPath.__repr__ now correctly formats paths
+ with ".." in them (by including it). (#4535)
+ - twisted.names.hosts.searchFileFor has been fixed against
+ refcounting dependency. (#4540)
+ - The POSIX process transports now declare that they implement
+ IProcessTransport. (#4585)
+ - Twisted can now be built with the LLVM clang compiler, with
+ 'CC=clang python setup.py build'. C code that caused errors with
+ this compiler has been removed. (#4652)
+ - trial now puts coverage data in the path specified by --temp-
+ directory, even if that option comes after --coverage on the
+ command line. (#4657)
+ - The unregisterProducer method of connection-oriented transports
+ will now cause the connection to be closed if there was a prior
+ call to loseConnection. (#4719)
+ - Fixed an issue where the new StreamServerEndpointService didn't log
+ listen errors. (This was a bug not present in any previous
+ releases, as this class is new.) (#4731)
+
+Improved Documentation
+----------------------
+ - The trial man page now documents the meaning of the final line of
+ output of the default reporter. (#1384)
+ - The API documentation for twisted.internet.defer.DeferredList now
+ goes into more depth about the effects each of the __init__ flags
+ that class accepts. (#3595)
+ - There is now narrative documentation for the endpoints APIs, in the
+ 'endpoints' core howto, as well as modifications to the 'writing
+ clients' and 'writing servers' core howto documents to indicate
+ that endpoints are now the preferred style of listening and
+ connecting. (#4478)
+ - trial's man page now documents the --disablegc option in more
+ detail. (#4511)
+ - trial's coverage output format is now documented in the trial man
+ page. (#4512)
+ - Broken links and spelling errors in the finger tutorial are now
+ fixed. (#4516)
+ - twisted.internet.threads.blockingCallFromThread's docstring is now
+ explicit about Deferred support. (#4517)
+ - twisted.python.zippath.ZipPath.child now documents its handling of
+ ".." (which is not special, making it different from
+ FilePath.child). (#4535)
+ - The API docs for twisted.internet.defer.Deferred now cover several
+ more of its (less interesting) attributes. (#4538)
+ - LineReceiver, NetstringReceiver, and IntNStringReceiver from
+ twisted.protocols.basic now have improved API documentation for
+ read callbacks and write methods. (#4542)
+ - Tidied up the Twisted Conch documentation for easier conversion.
+ (#4566)
+ - Use correct Twisted version for when cancellation was introduced in
+ the Deferred docstring. (#4614)
+ - The logging howto is now more clear about how the standard library
+ logging module and twisted.python.log can be integrated. (#4642)
+ - The finger tutorial still had references to .tap files. This
+ reference has now been removed. The documentation clarifies
+ "finger.tap" is a module and not a filename. (#4679)
+ - The finger tutorial had a broken link to the
+ twisted.application.service.Service class, which is now fixed.
+ Additionally, a minor typo ('verison') was fixed. (#4681)
+ - twisted.protocols.policies.TimeoutMixin now has clearer API
+ documentation. (#4684)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.defer.Deferred.setTimeout has been removed, after
+ being deprecated since Twisted 2.0. (#1702)
+ - twisted.internet.interfaces.IReactorTime.cancelCallLater
+ (deprecated since 2007) and
+ twisted.internet.interfaces.base.ReactorBase.cancelCallLater
+ (deprecated since 2002) have been removed. (#4076)
+ - Removed twisted.cred.util.py, which has been deprecated since
+ Twisted 8.3. (#4107)
+ - twisted.python.text.docstringLStrip was deprecated. (#4328)
+ - The module attributes `LENGTH`, `DATA`, `COMMA`, and `NUMBER` of
+ twisted.protocols.basic (previously used by `NetstringReceiver`)
+ are now deprecated. (#4541)
+ - twisted.protocols.basic.SafeNetstringReceiver, deprecated since
+ 2001 (before Twisted 2.0), was removed. (#4546)
+ - twisted.python.threadable.whenThreaded, deprecated since Twisted
+ 2.2.0, has been removed. (#4550)
+ - twisted.python.timeoutqueue, deprecated since Twisted 8.0, has been
+ removed. (#4551)
+ - iocpreactor transports can no longer be pickled. (#4617)
+
+Other
+-----
+ - #4300, #4475, #4477, #4504, #4556, #4562, #4564, #4569, #4608,
+ #4616, #4617, #4626, #4630, #4650, #4705
+
+
+Twisted Conch 10.2.0 (2010-11-29)
+=================================
+
+Bugfixes
+--------
+ - twisted.conch.ssh.factory.SSHFactory no longer disables coredumps.
+ (#2715)
+ - The Deferred returned by twisted.conch.telnet.TelnetTransport.will
+ now fires with an OptionRefused failure if the peer responds with a
+ refusal for the option negotiation. (#4231)
+ - SSHServerTransport and SSHClientTransport in
+ twisted.conch.ssh.transport no longer use PyCrypto to generate
+ random numbers for DH KEX. They also now generate values from the
+ full valid range, rather than only half of it. (#4469)
+ - twisted.conch.ssh.connection.SSHConnection now errbacks leftover
+ request deferreds on connection shutdown. (#4483)
+
+Other
+-----
+ - #4677
+
+
+Twisted Lore 10.2.0 (2010-11-29)
+================================
+
+No significant changes have been made for this release.
+
+Other
+-----
+ - #4571
+
+
+Twisted Mail 10.2.0 (2010-11-29)
+================================
+
+Improved Documentation
+----------------------
+ - The email server example now demonstrates how to set up
+ authentication and authorization using twisted.cred. (#4609)
+
+Deprecations and Removals
+-------------------------
+ - twisted.mail.smtp.sendEmail, deprecated since mid 2003 (before
+ Twisted 2.0), has been removed. (#4529)
+
+Other
+-----
+ - #4038, #4572
+
+
+Twisted Names 10.2.0 (2010-11-29)
+=================================
+
+Features
+--------
+ - twisted.names.server can now serve SPF resource records using
+ twisted.names.dns.Record_SPF. twisted.names.client can query for
+ them using lookupSenderPolicy. (#3928)
+
+Bugfixes
+--------
+ - twisted.names.common.extractRecords doesn't try to close the
+ transport anymore in case of recursion, as it's done by the
+ Resolver itself now. (#3998)
+
+Improved Documentation
+----------------------
+ - Tidied up the Twisted Names documentation for easier conversion.
+ (#4573)
+
+
+Twisted News 10.2.0 (2010-11-29)
+================================
+
+Bugfixes
+--------
+ - twisted.news.database.PickleStorage now invokes the email APIs
+ correctly, allowing it to actually send moderation emails. (#4528)
+
+
+Twisted Pair 10.2.0 (2010-11-29)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 10.2.0 (2010-11-29)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Web 10.2.0 (2010-11-29)
+===============================
+
+Features
+--------
+ - twisted.web.xmlrpc.XMLRPC.xmlrpc_* methods can now be decorated
+ using withRequest to cause them to be passed the HTTP request
+ object. (#3073)
+
+Bugfixes
+--------
+ - twisted.web.xmlrpc.QueryProtocol.handleResponse now disconnects
+ from the server, meaning that Twisted XML-RPC clients disconnect
+ from the server as soon as they receive a response, rather than
+ relying on the server to disconnect. (#2518)
+ - twisted.web.twcgi now generates responses containing all
+ occurrences of duplicate headers produced by CGI scripts, not just
+ the last value. (#4742)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.trp, which has been deprecated since Twisted 9.0, was
+ removed. (#4299)
+
+Other
+-----
+ - #4576, #4577, #4709, #4723
+
+
+Twisted Words 10.2.0 (2010-11-29)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient.msg now enforces a maximum
+ length for messages, splitting up messages that are too long.
+ (#4416)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient no longer invokes privmsg()
+ in the default noticed() implementation. (#4419)
+ - twisted.words.im.ircsupport.IRCProto now sends the correct name in
+ the USER command. (#4641)
+
+Deprecations and Removals
+-------------------------
+ - Remove twisted.words.im.proxyui and twisted.words.im.tap. (#1823)
+
+
+Twisted Core 10.1.0 (2010-06-27)
+================================
+
+Features
+--------
+ - Add linux inotify support, allowing monitoring of file system
+ events. (#972)
+ - Deferreds now support cancellation. (#990)
+ - Added new "endpoint" interfaces in twisted.internet.interfaces,
+ which abstractly describe stream transport endpoints which can be
+ listened on or connected to. Implementations for TCP and SSL
+ clients and servers are present in twisted.internet.endpoints.
+ Notably, client endpoints' connect() methods return cancellable
+ Deferreds, so code written to use them can bypass the awkward
+ "ClientFactory.clientConnectionFailed" and
+ "Connector.stopConnecting" methods, and handle errbacks from or
+ cancel the returned deferred, respectively. (#1442)
+ - twisted.protocols.amp.Integer's documentation now clarifies that
+ integers of arbitrary size are supported and that the wire format
+ is a base-10 representation. (#2650)
+ - twisted.protocols.amp now includes support for transferring
+ timestamps (amp.DateTime) and decimal values (amp.Decimal). (#2651)
+ - twisted.protocol.ftp.IWriteFile now has a close() method, which can
+ return a Deferred. Previously a STOR command would finish
+ immediately upon the receipt of the last byte of the uploaded file.
+ With close(), the backend can delay the finish until it has
+ performed some other slow action (like storing the data to a
+ virtual filesystem). (#3462)
+ - FilePath now calls os.stat() only when new status information is
+ required, rather than immediately when anything changes. For some
+ applications this may result in fewer stat() calls. Additionally,
+ FilePath has a new method, 'changed', which applications may use to
+ indicate that the FilePath may have been changed on disk and
+ therefore the next status information request must fetch a new
+ stat result. This is useful if external systems, such as C
+ libraries, may have changed files that Twisted applications are
+ referencing via a FilePath. (#4130)
+ - Documentation improvements are now summarized in the NEWS file.
+ (#4224)
+ - twisted.internet.task.deferLater now returns a cancellable
+ Deferred. (#4318)
+ - The connect methods of twisted.internet.protocol.ClientCreator now
+ return cancellable Deferreds. (#4329)
+ - twisted.spread.pb now has documentation covering some of its
+ limitations. (#4402)
+ - twisted.spread.jelly now supports jellying and unjellying classes
+ defined with slots if they also implement __getstate__ and
+ __setstate__. (#4430)
+ - twisted.protocols.amp.ListOf arguments can now be specified as
+ optional. (#4474)
+
+Bugfixes
+--------
+ - On POSIX platforms, reactors now support child processes in a way
+ which doesn't cause other syscalls to sometimes fail with EINTR (if
+ running on Python 2.6 or if Twisted's extension modules have been
+ built). (#733)
+ - Substrings are escaped before being passed to a regular expression
+ for searching to ensure that they don't get interpreted as part of
+ the expression. (#1893)
+ - twisted.internet.stdio now supports stdout being redirected to a
+ normal file (except when using epollreactor). (#2259)
+ - (#2367)
+ - The tap2rpm script now works with modern versions of RPM. (#3292)
+ - twisted.python.modules.walkModules will now handle packages
+ explicitly precluded from importing by a None placed in
+ sys.modules. (#3419)
+ - ConnectedDatagramPort now uses stopListening when a connection
+ fails instead of the deprecated loseConnection. (#3425)
+ - twisted.python.filepath.FilePath.setContent is now safe for
+ multiple processes to use concurrently. (#3694)
+ - The mode argument to the methods of
+ twisted.internet.interfaces.IReactorUNIX is no longer deprecated.
+ (#4078)
+ - Do not include blacklisted projects when generating NEWS. (#4190)
+ - When generating NEWS for a project that had no significant changes,
+ include a section for that project and say that there were no
+ interesting changes. (#4191)
+ - Redundant 'b' mode is no longer passed to calls to FilePath.open
+ and FilePath.open itself now corrects the mode when multiple 'b'
+ characters are present, ensuring only one instance of 'b' is
+ provided, as a workaround for http://bugs.python.org/issue7686.
+ (#4207)
+ - HTML tags inside <pre> tags in the code snippets are now escaped.
+ (#4336)
+ - twisted.protocols.amp.CommandLocator now allows subclasses to
+ override responders inherited from base classes. (#4343)
+ - Fix a bunch of small but important defects in the INSTALL, README
+ and so forth. (#4346)
+ - The poll, epoll, glib2, and gtk2 reactors now all support half-
+ close in the twisted.internet.stdio.StandardIO transport. (#4352)
+ - twisted.application.internet no longer generates an extra and
+ invalid entry in its __all__ list for the nonexistent
+ MulticastClient. (#4373)
+ - Choosing a reactor documentation now says that only the select-
+ based reactor is a truly cross-platform reactor. (#4384)
+ - twisted.python.filepath.FilePath now no longer leaves files open,
+ to be closed by the garbage collector, when an exception is raised
+ in the implementation of setContent, getContent, or copyTo. (#4400)
+ - twisted.test.proto_helpers.StringTransport's getHost and getPeer
+ methods now return IPv4Address instances by default. (#4401)
+ - twisted.protocols.amp.BinaryBoxProtocol will no longer deliver an
+ empty string to a switched-to protocol's dataReceived method when
+ the BinaryBoxProtocol's buffer happened to be empty at the time of
+ the protocol switch. (#4405)
+ - IReactorUNIX.listenUNIX implementations now support abstract
+ namespace sockets on Linux. (#4421)
+ - Files opened with FilePath.create() (and therefore also files
+ opened via FilePath.open() on a path with alwaysCreate=True) will
+ now be opened in binary mode as advertised, so that they will
+ behave portably across platforms. (#4453)
+ - The subunit reporter now correctly reports import errors as errors,
+ rather than by crashing with an unrelated error. (#4496)
+
+Improved Documentation
+----------------------
+ - The finger tutorial example which introduces services now avoids
+ double-starting the loop to re-read its users file. (#4420)
+ - twisted.internet.defer.Deferred.callback's docstring now mentions
+ the implicit chaining feature. (#4439)
+ - doc/core/howto/listing/pb/chatclient.py can now actually send a
+ group message. (#4459)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.interfaces.IReactorArbitrary,
+ twisted.application.internet.GenericServer, and
+ twisted.application.internet.GenericClient are now deprecated.
+ (#367)
+ - twisted.internet.gtkreactor is now deprecated. (#2833)
+ - twisted.trial.util.findObject has been deprecated. (#3108)
+ - twisted.python.threadpool.ThreadSafeList is deprecated and Jython
+ platform detection in Twisted core removed (#3725)
+ - twisted.internet.interfaces.IUDPConnectedTransport has been removed
+ (deprecated since Twisted 9.0). (#4077)
+ - Removed twisted.application.app.runWithProfiler, which has been
+ deprecated since Twisted 8.0. (#4090)
+ - Removed twisted.application.app.runWithHotshot, which has been
+ deprecated since Twisted 8.0. (#4091)
+ - Removed twisted.application.app.ApplicationRunner.startLogging,
+ which has been deprecated (doesn't say since when), as well as
+ support for the legacy
+ twisted.application.app.ApplicationRunner.getLogObserver method.
+ (#4092)
+ - twisted.application.app.reportProfile has been removed. (#4093)
+ - twisted.application.app.getLogFile has been removed. (#4094)
+ - Removed twisted.cred.util.py, which has been deprecated since
+ Twisted 8.3. (#4107)
+ - twisted.python.util.dsu is now deprecated. (#4339)
+ - In twisted.trial.util: FailureError, DirtyReactorWarning,
+ DirtyReactorError, and PendingTimedCallsError, which have all been
+ deprecated since Twisted 8.0, have been removed. (#4505)
+
+Other
+-----
+ - #1363, #1742, #3170, #3359, #3431, #3738, #4088, #4206, #4221,
+ #4239, #4257, #4272, #4274, #4287, #4291, #4293, #4309, #4316,
+ #4319, #4324, #4332, #4335, #4348, #4358, #4394, #4399, #4409,
+ #4418, #4443, #4449, #4479, #4485, #4486, #4497
+
+
+Twisted Conch 10.1.0 (2010-06-27)
+=================================
+
+Features
+--------
+ - twisted.conch.ssh.transport.SSHTransportBase now allows supported
+ ssh protocol versions to be overriden. (#4428)
+
+Bugfixes
+--------
+ - SSHSessionProcessProtocol now doesn't close the session when stdin
+ is closed, but instead when both stdout and stderr are. (#4350)
+ - The 'cftp' command-line tool will no longer encounter an
+ intermittent error, crashing at startup with a ZeroDivisionError
+ while trying to report progress. (#4463)
+ - twisted.conch.ssh.connection.SSHConnection now replies to requests
+ to open an unknown channel with a OPEN_UNKNOWN_CHANNEL_TYPE message
+ instead of closing the connection. (#4490)
+
+Deprecations and Removals
+-------------------------
+ - twisted.conch.insults.client was deprecated. (#4095)
+ - twisted.conch.insults.colors has been deprecated. Please use
+ twisted.conch.insults.helper instead. (#4096)
+ - Removed twisted.conch.ssh.asn1, which has been deprecated since
+ Twisted 9.0. (#4097)
+ - Removed twisted.conch.ssh.common.Entropy, as Entropy.get_bytes has
+ been deprecated since 2007 and Entropy.get_bytes was the only
+ attribute of Entropy. (#4098)
+ - Removed twisted.conch.ssh.keys.getPublicKeyString, which has been
+ deprecated since 2007. Also updated the conch examples
+ sshsimpleserver.py and sshsimpleclient.py to reflect this removal.
+ (#4099)
+ - Removed twisted.conch.ssh.keys.makePublicKeyString, which has been
+ deprecated since 2007. (#4100)
+ - Removed twisted.conch.ssh.keys.getPublicKeyObject, which has been
+ deprecated since 2007. (#4101)
+ - Removed twisted.conch.ssh.keys.getPrivateKeyObject, which has been
+ deprecated since 2007. Also updated the conch examples to reflect
+ this removal. (#4102)
+ - Removed twisted.conch.ssh.keys.makePrivateKeyString, which has been
+ deprecated since 2007. (#4103)
+ - Removed twisted.conch.ssh.keys.makePublicKeyBlob, which has been
+ deprecated since 2007. (#4104)
+ - Removed twisted.conch.ssh.keys.signData,
+ twisted.conch.ssh.keys.verifySignature, and
+ twisted.conch.ssh.keys.printKey, which have been deprecated since
+ 2007. (#4105)
+
+Other
+-----
+ - #3849, #4408, #4454
+
+
+Twisted Lore 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Mail 10.1.0 (2010-06-27)
+================================
+
+Bugfixes
+--------
+ - twisted.mail.imap4.IMAP4Server no longer fails on search queries
+ that contain wildcards. (#2278)
+ - A case which would cause twisted.mail.imap4.IMAP4Server to loop
+ indefinitely when handling a search command has been fixed. (#4385)
+
+Other
+-----
+ - #4069, #4271, #4467
+
+
+Twisted Names 10.1.0 (2010-06-27)
+=================================
+
+Features
+--------
+ - twisted.names.dns.Message now uses a specially constructed
+ dictionary for looking up record types. This yields a significant
+ performance improvement on PyPy. (#4283)
+
+
+Twisted News 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 10.1.0 (2010-06-27)
+==================================
+
+Features
+--------
+ - twistd now has a procmon subcommand plugin - a convenient way to
+ monitor and automatically restart another process. (#4356)
+
+Deprecations and Removals
+-------------------------
+ - twisted.runner.procmon.ProcessMonitor's active, consistency, and
+ consistencyDelay attributes are now deprecated. (#1763)
+
+Other
+-----
+ - #3775
+
+
+Twisted Web 10.1.0 (2010-06-27)
+===============================
+
+Features
+--------
+ - twisted.web.xmlrpc.XMLRPC and twisted.web.xmlrpc.Proxy now expose
+ xmlrpclib's support of datetime.datetime objects if useDateTime is
+ set to True. (#3219)
+ - HTTP11ClientProtocol now has an abort() method for cancelling an
+ outstanding request by closing the connection before receiving the
+ entire response. (#3811)
+ - twisted.web.http_headers.Headers initializer now rejects
+ incorrectly structured dictionaries. (#4022)
+ - twisted.web.client.Agent now supports HTTPS URLs. (#4023)
+ - twisted.web.xmlrpc.Proxy.callRemote now returns a Deferred which
+ can be cancelled to abort the attempted XML-RPC call. (#4377)
+
+Bugfixes
+--------
+ - twisted.web.guard now logs out avatars even if a request completes
+ with an error. (#4411)
+ - twisted.web.xmlrpc.XMLRPC will now no longer trigger a RuntimeError
+ by trying to write responses to closed connections. (#4423)
+
+Improved Documentation
+----------------------
+ - Fix broken links to deliverBody and iweb.UNKNOWN_LENGTH in
+ doc/web/howto/client.xhtml. (#4507)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.twcgi.PHP3Script and twisted.web.twcgi.PHPScript are
+ now deprecated. (#516)
+
+Other
+-----
+ - #4403, #4452
+
+
+Twisted Words 10.1.0 (2010-06-27)
+=================================
+
+Bugfixes
+--------
+ - twisted.words.im.basechat.ChatUI now has a functional
+ contactChangedNick with unit tests. (#229)
+ - twisted.words.protocols.jabber.error.StanzaError now correctly sets
+ a default error type and code for the remote-server-timeout
+ condition (#4311)
+ - twisted.words.protocols.jabber.xmlstream.ListenAuthenticator now
+ uses unicode objects for session identifiers (#4345)
+
+
+Twisted Core 10.0.0 (2010-03-01)
+================================
+
+Features
+--------
+ - The twistd man page now has a SIGNALS section. (#689)
+
+ - reactor.spawnProcess now will not emit a PotentialZombieWarning
+ when called before reactor.run, and there will be no potential for
+ zombie processes in this case. (#2078)
+
+ - High-throughput applications based on Perspective Broker should now
+ run noticably faster thanks to the use of a more efficient decoding
+ function in Twisted Spread. (#2310)
+
+ - Documentation for trac-post-commit-hook functionality in svn-dev
+ policy. (#3867)
+
+ - twisted.protocols.socks.SOCKSv4 now supports the SOCKSv4a protocol.
+ (#3886)
+
+ - Trial can now output test results according to the subunit
+ protocol, as long as Subunit is installed (see
+ https://launchpad.net/subunit). (#4004)
+
+ - twisted.protocols.amp now provides a ListOf argument type which can
+ be composed with some other argument types to create a zero or more
+ element sequence of that type. (#4116)
+
+ - If returnValue is invoked outside of a function decorated with
+ @inlineCallbacks, but causes a function thusly decorated to exit, a
+ DeprecationWarning will be emitted explaining this potentially
+ confusing behavior. In a future release, this will cause an
+ exception. (#4157)
+
+ - twisted.python.logfile.BaseLogFile now has a reopen method allowing
+ you to use an external logrotate mechanism. (#4255)
+
+Bugfixes
+--------
+ - FTP.ftp_NLST now handles requests on invalid paths in a way
+ consistent with RFC 959. (#1342)
+
+ - twisted.python.util.initgroups now calls the low-level C initgroups
+ by default if available: the python version can create lots of I/O
+ with certain authentication setup to retrieve all the necessary
+ information. (#3226)
+
+ - startLogging now does nothing on subsequent invocations, thus
+ fixing a terrible infinite recursion bug that's only on edge case.
+ (#3289)
+
+ - Stringify non-string data to NetstringReceiver.sendString before
+ calculating the length so that the calculated length is equal to
+ the actual length of the transported data. (#3299)
+
+ - twisted.python.win32.cmdLineQuote now correctly quotes empty
+ strings arguments (#3876)
+
+ - Change the behavior of the Gtk2Reactor to register only one source
+ watch for each file descriptor, instead of one for reading and one
+ for writing. In particular, it fixes a bug with Glib under Windows
+ where we failed to notify when a client is connected. (#3925)
+
+ - Twisted Trial no longer crashes if it can't remove an old
+ _trial_temp directory. (#4020)
+
+ - The optional _c_urlarg extension now handles unquote("") correctly
+ on platforms where malloc(0) returns NULL, such as AIX. It also
+ compiles with less warnings. (#4142)
+
+ - On POSIX, child processes created with reactor.spawnProcess will no
+ longer automatically ignore the signals which the parent process
+ has set to be ignored. (#4199)
+
+ - All SOCKSv4a tests now use a dummy reactor with a deterministic
+ resolve method. (#4275)
+
+ - Prevent extraneous server, date and content-type headers in proxy
+ responses. (#4277)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.error.PotentialZombieWarning is now deprecated.
+ (#2078)
+
+ - twisted.test.time_helpers is now deprecated. (#3719)
+
+ - The deprecated connectUDP method of IReactorUDP has now been
+ removed. (#4075)
+
+ - twisted.trial.unittest.TestCase now ignores the previously
+ deprecated setUpClass and tearDownClass methods. (#4175)
+
+Other
+-----
+ - #917, #2406, #2481, #2608, #2689, #2884, #3056, #3082, #3199,
+ #3480, #3592, #3718, #3935, #4066, #4083, #4154, #4166, #4169,
+ #4176, #4183, #4186, #4188, #4189, #4194, #4201, #4204, #4209,
+ #4222, #4234, #4235, #4238, #4240, #4245, #4251, #4264, #4268,
+ #4269, #4282
+
+
+Twisted Conch 10.0.0 (2010-03-01)
+=================================
+
+Bugfixes
+--------
+ - twisted.conch.checkers.SSHPublicKeyDatabase now looks in the
+ correct user directory for authorized_keys files. (#3984)
+ - twisted.conch.ssh.SSHUserAuthClient now honors preferredOrder when
+ authenticating. (#4266)
+
+Other
+-----
+ - #2391, #4203, #4265
+
+
+Twisted Lore 10.0.0 (2010-03-01)
+================================
+
+Other
+-----
+ - #4241
+
+
+Twisted Mail 10.0.0 (2010-03-01)
+================================
+
+Bugfixes
+--------
+ - twisted.mail.smtp.ESMTPClient and
+ twisted.mail.smtp.LOGINAuthenticator now implement the (obsolete)
+ LOGIN SASL mechanism according to the draft specification. (#4031)
+
+ - twisted.mail.imap4.IMAP4Client will no longer misparse all html-
+ formatted message bodies received in response to a fetch command.
+ (#4049)
+
+ - The regression in IMAP4 search handling of "OR" and "NOT" terms has
+ been fixed. (#4178)
+
+Other
+-----
+ - #4028, #4170, #4200
+
+
+Twisted Names 10.0.0 (2010-03-01)
+=================================
+
+Bugfixes
+--------
+ - twisted.names.root.Resolver no longer leaks UDP sockets while
+ resolving names. (#970)
+
+Deprecations and Removals
+-------------------------
+ - Several top-level functions in twisted.names.root are now
+ deprecated. (#970)
+
+Other
+-----
+ - #4066
+
+
+Twisted Pair 10.0.0 (2010-03-01)
+================================
+
+Other
+-----
+ - #4170
+
+
+Twisted Runner 10.0.0 (2010-03-01)
+==================================
+
+Other
+-----
+ - #3961
+
+
+Twisted Web 10.0.0 (2010-03-01)
+===============================
+
+Features
+--------
+ - Twisted Web in 60 Seconds, a series of short tutorials with self-
+ contained examples on a range of common web topics, is now a part
+ of the Twisted Web howto documentation. (#4192)
+
+Bugfixes
+--------
+ - Data and File from twisted.web.static and
+ twisted.web.distrib.UserDirectory will now only generate a 200
+ response for GET or HEAD requests.
+ twisted.web.client.HTTPPageGetter will no longer ignore the case of
+ a request method when considering whether to apply special HEAD
+ processing to a response. (#446)
+
+ - twisted.web.http.HTTPClient now supports multi-line headers.
+ (#2062)
+
+ - Resources served via twisted.web.distrib will no longer encounter a
+ Banana error when writing more than 640kB at once to the request
+ object. (#3212)
+
+ - The Error, PageRedirect, and InfiniteRedirection exception in
+ twisted.web now initialize an empty message parameter by mapping
+ the HTTP status code parameter to a descriptive string. Previously
+ the lookup would always fail, leaving message empty. (#3806)
+
+ - The 'wsgi.input' WSGI environment object now supports -1 and None
+ as arguments to the read and readlines methods. (#4114)
+
+ - twisted.web.wsgi doesn't unquote QUERY_STRING anymore, thus
+ complying with the WSGI reference implementation. (#4143)
+
+ - The HTTP proxy will no longer pass on keep-alive request headers
+ from the client, preventing pages from loading then "hanging"
+ (leaving the connection open with no hope of termination). (#4179)
+
+Deprecations and Removals
+-------------------------
+ - Remove '--static' option from twistd web, that served as an alias
+ for the '--path' option. (#3907)
+
+Other
+-----
+ - #3784, #4216, #4242
+
+
+Twisted Words 10.0.0 (2010-03-01)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient.irc_MODE now takes ISUPPORT
+ parameters into account when parsing mode messages with arguments
+ that take parameters (#3296)
+
+Bugfixes
+--------
+ - When twisted.words.protocols.irc.IRCClient's versionNum and
+ versionEnv attributes are set to None, they will no longer be
+ included in the client's response to CTCP VERSION queries. (#3660)
+
+ - twisted.words.protocols.jabber.xmlstream.hashPassword now only
+ accepts unicode as input (#3741, #3742, #3847)
+
+Other
+-----
+ - #2503, #4066, #4261
+
+
+Twisted Core 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - LineReceiver.clearLineBuffer now returns the bytes that it cleared (#3573)
+ - twisted.protocols.amp now raises InvalidSignature when bad arguments are
+ passed to Command.makeArguments (#2808)
+ - IArgumentType was added to represent an existing but previously unspecified
+ interface in amp (#3468)
+ - Obscure python tricks have been removed from the finger tutorials (#2110)
+ - The digest auth implementations in twisted.web and twisted.protocolos.sip
+ have been merged together in twisted.cred (#3575)
+ - FilePath and ZipPath now has a parents() method which iterates up all of its
+ parents (#3588)
+ - reactors which support threads now have a getThreadPool method (#3591)
+ - The MemCache client implementation now allows arguments to the "stats"
+ command (#3661)
+ - The MemCache client now has a getMultiple method which allows fetching of
+ multiple values (#3171)
+ - twisted.spread.jelly can now unserialize some new-style classes (#2950)
+ - twisted.protocols.loopback.loopbackAsync now accepts a parameter to control
+ the data passed between client and server (#3820)
+ - The IOCP reactor now supports SSL (#593)
+ - Tasks in a twisted.internet.task.Cooperator can now be paused, resumed, and
+ cancelled (#2712)
+ - AmpList arguments can now be made optional (#3891)
+ - The syslog output observer now supports log levels (#3300)
+ - LoopingCall now supports reporting the number of intervals missed if it
+ isn't able to schedule calls fast enough (#3671)
+
+Fixes
+-----
+ - The deprecated md5 and sha modules are no longer used if the stdlib hashlib
+ module is available (#2763)
+ - An obscure deadlock involving waking up the reactor within signal handlers
+ in particular threads was fixed (#1997)
+ - The passivePortRange attribute of FTPFactory is now honored (#3593)
+ - TestCase.flushWarnings now flushes warnings even if they were produced by a
+ file that was renamed since it was byte compiled (#3598)
+ - Some internal file descriptors are now marked as close-on-exec, so these will
+ no longer be leaked to child processes (#3576)
+ - twisted.python.zipstream now correctly extracts the first file in a directory
+ as a file, and not an empty directory (#3625)
+ - proxyForInterface now returns classes which correctly *implement* interfaces
+ rather than *providing* them (#3646)
+ - SIP Via header parameters should now be correctly generated (#2194)
+ - The Deferred returned by stopListening would sometimes previously never fire
+ if an exception was raised by the underlying file descriptor's connectionLost
+ method. Now the Deferred will fire with a failure (#3654)
+ - The command-line tool "manhole" should now work with newer versions of pygtk
+ (#2464)
+ - When a DefaultOpenSSLContextFactory is instantiated with invalid parameters,
+ it will now raise an exception immediately instead of waiting for the first
+ connection (#3700)
+ - Twisted command line scripts should now work when installed in a virtualenv
+ (#3750)
+ - Trial will no longer delete temp directories which it did not create (#3481)
+ - Processes started on Windows should now be cleaned up properly in more cases
+ (#3893)
+ - Certain misbehaving importers will no longer cause twisted.python.modules
+ (and thus trial) to raise an exception, but rather issue a warning (#3913)
+ - MemCache client protocol methods will now fail when the transport has been
+ disconnected (#3643)
+ - In the AMP method callRemoteString, the requiresAnswer parameter is now
+ honored (#3999)
+ - Spawning a "script" (a file which starts with a #! line) on Windows running
+ Python 2.6 will now work instead of raising an exception about file mode
+ "ru" (#3567)
+ - FilePath's walk method now calls its "descend" parameter even on the first
+ level of children, instead of only on grandchildren. This allows for better
+ symlink cycle detection (#3911)
+ - Attempting to write unicode data to process pipes on Windows will no longer
+ result in arbitrarily encoded messages being written to the pipe, but instead
+ will immediately raise an error (#3930)
+ - The various twisted command line utilities will no longer print
+ ModuleType.__doc__ when Twisted was installed with setuptools (#4030)
+ - A Failure object will now be passed to connectionLost on stdio connections
+ on Windows, instead of an Exception object (#3922)
+
+Deprecations and Removals
+-------------------------
+ - twisted.persisted.marmalade was deleted after a long period of deprecation
+ (#876)
+ - Some remaining references to the long-gone plugins.tml system were removed
+ (#3246)
+ - SSLv2 is now disabled by default, but it can be re-enabled explicitly
+ (#3330)
+ - twisted.python.plugin has been removed (#1911)
+ - reactor.run will now raise a ReactorAlreadyRunning exception when it is
+ called reentrantly instead of warning a DeprecationWarning (#1785)
+ - twisted.spread.refpath is now deprecated because it is unmaintained,
+ untested, and has dubious value (#3723)
+ - The unused --quiet flag has been removed from the twistd command (#3003)
+
+Other
+-----
+ - #3545, #3490, #3544, #3537, #3455, #3315, #2281, #3564, #3570, #3571, #3486,
+ #3241, #3599, #3220, #1522, #3611, #3596, #3606, #3609, #3602, #3637, #3647,
+ #3632, #3675, #3673, #3686, #2217, #3685, #3688, #2456, #506, #3635, #2153,
+ #3581, #3708, #3714, #3717, #3698, #3747, #3704, #3707, #3713, #3720, #3692,
+ #3376, #3652, #3695, #3735, #3786, #3783, #3699, #3340, #3810, #3822, #3817,
+ #3791, #3859, #2459, #3677, #3883, #3894, #3861, #3822, #3852, #3875, #2722,
+ #3768, #3914, #3885, #2719, #3905, #3942, #2820, #3990, #3954, #1627, #2326,
+ #2972, #3253, #3937, #4058, #1200, #3639, #4079, #4063, #4050
+
+
+Twisted Conch 9.0.0 (2009-11-24)
+================================
+
+Fixes
+-----
+ - The SSH key parser has been removed and conch now uses pyASN1 to parse keys.
+ This should fix a number of cases where parsing a key would fail, but it now
+ requires users to have pyASN1 installed (#3391)
+ - The time field on SFTP file listings should now be correct (#3503)
+ - The day field on SFTP file listings should now be correct on Windows (#3503)
+ - The "cftp" sftp client now truncates files it is uploading over (#2519)
+ - The telnet server protocol can now properly respond to subnegotiation
+ requests (#3655)
+ - Tests and factoring of the SSHv2 server implementation are now much better
+ (#2682)
+ - The SSHv2 server now sends "exit-signal" messages to the client, instead of
+ raising an exception, when a process dies due to a signal (#2687)
+ - cftp's client-side "exec" command now uses /bin/sh if the current user has
+ no shell (#3914)
+
+Deprecations and Removals
+-------------------------
+ - The buggy SSH connection sharing feature of the SSHv2 client was removed
+ (#3498)
+ - Use of strings and PyCrypto objects to represent keys is deprecated in favor
+ of using Conch Key objects (#2682)
+
+Other
+-----
+ - #3548, #3537, #3551, #3220, #3568, #3689, #3709, #3809, #2763, #3540, #3750,
+ #3897, #3813, #3871, #3916, #4047, #3940, #4050
+
+
+Twisted Lore 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - Python source listings now include line numbers (#3486)
+
+Fixes
+-----
+ - Lore now uses minidom instead of Twisted's microdom, which incidentally
+ fixes some Lore bugs such as throwing away certain whitespace
+ (#3560, #414, #3619)
+ - Lore's "lint" command should no longer break on documents with links in them
+ (#4051, #4115)
+
+Deprecations and Removals
+-------------------------
+ - Lore no longer uses the ancient "tml" Twisted plugin system (#1911)
+
+Other
+-----
+ - #3565, #3246, #3540, #3750, #4050
+
+
+Twisted Mail 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - maildir.StringListMailbox, an in-memory maildir mailbox, now supports
+ deletion, undeletion, and syncing (#3547)
+ - SMTPClient's callbacks are now more completely documented (#684)
+
+Fixes
+-----
+ - Parse UNSEEN response data and include it in the result of
+ IMAP4Client.examine (#3550)
+ - The IMAP4 client now delivers more unsolicited server responses to callbacks
+ rather than ignoring them, and also won't ignore solicited responses that
+ arrive on the same line as an unsolicited one (#1105)
+ - Several bugs in the SMTP client's idle timeout support were fixed (#3641,
+ #1219)
+ - A case where the SMTP client could skip some recipients when retrying
+ delivery has been fixed (#3638)
+ - Errors during certain data transfers will no longer be swallowed. They will
+ now bubble up to the higher-level API (such as the sendmail function) (#3642)
+ - Escape sequences inside quoted strings in IMAP4 should now be parsed
+ correctly by the IMAP4 server protocol (#3659)
+ - The "imap4-utf-7" codec that is registered by twisted.mail.imap4 had a number
+ of fixes that allow it to work better with the Python codecs system, and to
+ actually work (#3663)
+ - The Maildir implementation now ensures time-based ordering of filenames so
+ that the lexical sorting of messages matches the order in which they were
+ received (#3812)
+ - SASL PLAIN credentials generated by the IMAP4 protocol implementations
+ (client and server) should now be RFC-compliant (#3939)
+ - Searching for a set of sequences using the IMAP4 "SEARCH" command should
+ now work on the IMAP4 server protocol implementation. This at least improves
+ support for the Pine mail client (#1977)
+
+Other
+-----
+ - #2763, #3647, #3750, #3819, #3540, #3846, #2023, #4050
+
+
+Twisted Names 9.0.0 (2009-11-24)
+================================
+
+Deprecations and Removals
+-------------------------
+ - client.ThreadedResolver is deprecated in favor of
+ twisted.internet.base.ThreadedResolver (#3710)
+
+Other
+-----
+ - #3540, #3560, #3712, #3750, #3990
+
+
+Twisted News 9.0.0 (2009-11-24)
+===============================
+
+Other
+-----
+ - #2763, #3540
+
+
+Twisted Pair 9.0.0 (2009-11-24)
+===============================
+
+Other
+-----
+ - #3540, #4050
+
+
+Twisted Runner 9.0.0 (2009-11-24)
+=================================
+
+Features
+--------
+ - procmon.ProcessMonitor.addProcess now accepts an 'env' parameter which allows
+ users to specify the environment in which a process will be run (#3691)
+
+Other
+-----
+ - #3540
+
+
+Twisted Web 9.0.0 (2009-11-24)
+==============================
+
+Features
+--------
+ - There is now an iweb.IRequest interface which specifies the interface that
+ request objects provide (#3416)
+ - downloadPage now supports the same cookie, redirect, and timeout features
+ that getPage supports (#2971)
+ - A chapter about WSGI has been added to the twisted.web documentation (#3510)
+ - The HTTP auth support in the web server now allows anonymous sessions by
+ logging in with ANONYMOUS credentials when no Authorization header is
+ provided in a request (#3924, #3936)
+ - HTTPClientFactory now accepts a parameter to enable a common deviation from
+ the HTTP 1.1 standard by responding to redirects in a POSTed request with a
+ GET instead of another POST (#3624)
+ - A new basic HTTP/1.1 client API is included in twisted.web.client.Agent
+ (#886, #3987)
+
+Fixes
+-----
+ - Requests for "insecure" children of a static.File (such as paths containing
+ encoded directory separators) will now result in a 404 instead of a 500
+ (#3549, #3469)
+ - When specifying a followRedirect argument to the getPage function, the state
+ of redirect-following for other getPage calls should now be unaffected. It
+ was previously overwriting a class attribute which would affect outstanding
+ getPage calls (#3192)
+ - Downloading an URL of the form "http://example.com:/" will now work,
+ ignoring the extraneous colon (#2402)
+ - microdom's appendChild method will no longer issue a spurious warning, and
+ microdom's methods in general should now issue more meaningful exceptions
+ when invalid parameters are passed (#3421)
+ - WSGI applications will no longer have spurious Content-Type headers added to
+ their responses by the twisted.web server. In addition, WSGI applications
+ will no longer be able to specify the server-restricted headers Server and
+ Date (#3569)
+ - http_headers.Headers now normalizes the case of raw headers passed directly
+ to it in the same way that it normalizes the headers passed to setRawHeaders
+ (#3557)
+ - The distrib module no longer relies on the deprecated woven package (#3559)
+ - twisted.web.domhelpers now works with both microdom and minidom (#3600)
+ - twisted.web servers will now ignore invalid If-Modified-Since headers instead
+ of returning a 500 error (#3601)
+ - Certain request-bound memory and file resources are cleaned up slightly
+ sooner by the request when the connection is lost (#1621, #3176)
+ - xmlrpclib.DateTime objects should now correctly round-trip over twisted.web's
+ XMLRPC support in all supported versions of Python, and errors during error
+ serialization will no longer hang a twisted.web XMLRPC response (#2446)
+ - request.content should now always be seeked to the beginning when
+ request.process is called, so application code should never need to seek
+ back manually (#3585)
+ - Fetching a child of static.File with a double-slash in the URL (such as
+ "example//foo.html") should now return a 404 instead of a traceback and
+ 500 error (#3631)
+ - downloadPage will now fire a Failure on its returned Deferred instead of
+ indicating success when the connection is prematurely lost (#3645)
+ - static.File will now provide a 404 instead of a 500 error when it was
+ constructed with a non-existent file (#3634)
+ - microdom should now serialize namespaces correctly (#3672)
+ - The HTTP Auth support resource wrapper should no longer corrupt requests and
+ cause them to skip a segment in the request path (#3679)
+ - The twisted.web WSGI support should now include leading slashes in PATH_INFO,
+ and SCRIPT_NAME will be empty if the application is at the root of the
+ resource tree. This means that WSGI applications should no longer generate
+ URLs with double-slashes in them even if they naively concatenate the values
+ (#3721)
+ - WSGI applications should now receive the requesting client's IP in the
+ REMOTE_ADDR environment variable (#3730)
+ - The distrib module should work again. It was unfortunately broken with the
+ refactoring of twisted.web's header support (#3697)
+ - static.File now supports multiple ranges specified in the Range header
+ (#3574)
+ - static.File should now generate a correct Content-Length value when the
+ requested Range value doesn't fit entirely within the file's contents (#3814)
+ - Attempting to call request.finish() after the connection has been lost will
+ now immediately raise a RuntimeError (#4013)
+ - An HTTP-auth resource should now be able to directly render the wrapped
+ avatar, whereas before it would only allow retrieval of child resources
+ (#4014)
+ - twisted.web's wsgi support should no longer attempt to call request.finish
+ twice, which would cause errors in certain cases (#4025)
+ - WSGI applications should now be able to handle requests with large bodies
+ (#4029)
+ - Exceptions raised from WSGI applications should now more reliably be turned
+ into 500 errors on the HTTP level (#4019)
+ - DeferredResource now correctly passes through exceptions raised from the
+ wrapped resource, instead of turning them all into 500 errors (#3932)
+ - Agent.request now generates a Host header when no headers are passed at
+ (#4131)
+
+Deprecations and Removals
+-------------------------
+ - The unmaintained and untested twisted.web.monitor module was removed (#2763)
+ - The twisted.web.woven package has been removed (#1522)
+ - All of the error resources in twisted.web.error are now in
+ twisted.web.resource, and accessing them through twisted.web.error is now
+ deprecated (#3035)
+ - To facilitate a simplification of the timeout logic in server.Session,
+ various things have been deprecated (#3457)
+ - the loopFactory attribute is now ignored
+ - the checkExpired method now does nothing
+ - the lifetime parameter to startCheckingExpiration is now ignored
+ - The twisted.web.trp module is now deprecated (#2030)
+
+Other
+-----
+ - #2763, #3540, #3575, #3610, #3605, #1176, #3539, #3750, #3761, #3779, #2677,
+ #3782, #3904, #3919, #3418, #3990, #1404, #4050
+
+
+Twisted Words 9.0.0 (2009-11-24)
+================================
+
+Features
+--------
+ - IRCClient.describe is a new method meant to replace IRCClient.me to send
+ CTCP ACTION messages with less confusing behavior (#3910)
+ - The XMPP client protocol implementation now supports ANONYMOUS SASL
+ authentication (#4067)
+ - The IRC client protocol implementation now has better support for the
+ ISUPPORT server->client message, storing the data in a new
+ ServerSupportedFeatures object accessible via IRCClient.supported (#3285)
+
+Fixes
+-----
+ - The twisted.words IRC server now always sends an MOTD, which at least makes
+ Pidgin able to successfully connect to a twisted.words IRC server (#2385)
+ - The IRC client will now dispatch "RPL MOTD" messages received before a
+ "RPL MOTD START" instead of raising an exception (#3676)
+ - The IRC client protocol implementation no longer updates its 'nickname'
+ attribute directly; instead, that attribute will be updated when the server
+ acknowledges the change (#3377)
+ - The IRC client protocol implementation now supports falling back to another
+ nickname when a nick change request fails (#3377, #4010)
+
+Deprecations and Removals
+-------------------------
+ - The TOC protocol implementation is now deprecated, since the protocol itself
+ has been deprecated and obselete for quite a long time (#3580)
+ - The gui "im" application has been removed, since it relied on GTK1, which is
+ hard to find these days (#3699, #3340)
+
+Other
+-----
+ - #2763, #3540, #3647, #3750, #3895, #3968, #4050
+
+
+Core 8.2.0 (2008-12-16)
+=======================
+
+Features
+--------
+ - Reactors are slowly but surely becoming more isolated, thus improving
+ testability (#3198)
+ - FilePath has gained a realpath method, and FilePath.walk no longer infinitely
+ recurses in the case of a symlink causing a self-recursing filesystem tree
+ (#3098)
+ - FilePath's moveTo and copyTo methods now have an option to disable following
+ of symlinks (#3105)
+ - Private APIs are now included in the API documentation (#3268)
+ - hotshot is now the default profiler for the twistd --profile parameter and
+ using cProfile is now documented (#3355, #3356)
+ - Process protocols can now implement a processExited method, which is
+ distinct from processEnded in that it is called immediately when the child
+ has died, instead of waiting for all the file descriptors to be closed
+ (#1291)
+ - twistd now has a --umask option (#966, #3024)
+ - A new deferToThreadPool function exists in twisted.internet.threads (#2845)
+ - There is now an example of writing an FTP server in examples/ftpserver.py
+ (#1579)
+ - A new runAsEffectiveUser function has been added to twisted.python.util
+ (#2607)
+ - twisted.internet.utils.getProcessOutput now offers a mechanism for
+ waiting for the process to actually end, in the event of data received on
+ stderr (#3239)
+ - A fullyQualifiedName function has been added to twisted.python.reflect
+ (#3254)
+ - strports now defaults to managing access to a UNIX socket with a lock;
+ lockfile=0 can be included in the strports specifier to disable this
+ behavior (#2295)
+ - FTPClient now has a 'rename' method (#3335)
+ - FTPClient now has a 'makeDirectory' method (#3500)
+ - FTPClient now has a 'removeFile' method (#3491)
+ - flushWarnings, A new Trial method for testing warnings, has been added
+ (#3487, #3427, #3506)
+ - The log observer can now be configured in .tac files (#3534)
+
+Fixes
+-----
+ - TLS Session Tickets are now disabled by default, allowing connections to
+ certain servers which hang when an empty session ticket is received (like
+ GTalk) (#3463)
+ - twisted.enterprise.adbapi.ConnectionPool's noisy attribute now defaults to
+ False, as documented (#1806)
+ - Error handling and logging in adbapi is now much improved (#3244)
+ - TCP listeners can now be restarted (#2913)
+ - Doctests can now be rerun with trial's --until-failure option (#2713)
+ - Some memory leaks have been fixed in trial's --until-failure
+ implementation (#3119, #3269)
+ - Trial's summary reporter now prints correct runtime information and handles
+ the case of 0 tests (#3184)
+ - Trial and any other user of the 'namedAny' function now has better error
+ reporting in the case of invalid module names (#3259)
+ - Multiple instances of trial can now run in parallel in the same directory
+ by creating _trial_temp directories with an incremental suffix (#2338)
+ - Trial's failUnlessWarns method now works on Python 2.6 (#3223)
+ - twisted.python.log now hooks into the warnings system in a way compatible
+ with Python 2.6 (#3211)
+ - The GTK2 reactor is now better supported on Windows, but still not passing
+ the entire test suite (#3203)
+ - low-level failure handling in spawnProcess has been improved and no longer
+ leaks file descriptors (#2305, #1410)
+ - Perspective Broker avatars now have their logout functions called in more
+ cases (#392)
+ - Log observers which raise exceptions are no longer removed (#1069)
+ - transport.getPeer now always includes an IP address in the Address returned
+ instead of a hostname (#3059)
+ - Functions in twisted.internet.utils which spawn processes now avoid calling
+ chdir in the case where no working directory is passed, to avoid some
+ obscure permission errors (#3159)
+ - twisted.spread.publish.Publishable no longer corrupts line endings on
+ Windows (#2327)
+ - SelectReactor now properly detects when a TLS/TCP connection has been
+ disconnected (#3218)
+ - twisted.python.lockfile no longer raises an EEXIST OSError and is much
+ better supported on Windows (#3367)
+ - When ITLSTransport.startTLS is called while there is data in the write
+ buffer, TLS negotiation will now be delayed instead of the method raising
+ an exception (#686)
+ - The userAnonymous argument to FTPFactory is now honored (#3390)
+ - twisted.python.modules no longer tries to "fix" sys.modules after an import
+ error, which was just causing problems (#3388)
+ - setup.py no longer attempts to build extension modules when run with Jython
+ (#3410)
+ - AMP boxes can now be sent in IBoxReceiver.startReceivingBoxes (#3477)
+ - AMP connections are closed as soon as a key length larger than 255 is
+ received (#3478)
+ - Log events with timezone offsets between -1 and -59 minutes are now
+ correctly reported as negative (#3515)
+
+Deprecations and Removals
+-------------------------
+ - Trial's setUpClass and tearDownClass methods are now deprecated (#2903)
+ - problemsFromTransport has been removed in favor of the argument passed to
+ connectionLost (#2874)
+ - The mode parameter to methods of IReactorUNIX and IReactorUNIXDatagram are
+ deprecated in favor of applications taking other security precautions, since
+ the mode of a Unix socket is often not respected (#1068)
+ - Index access on instances of twisted.internet.defer.FirstError has been
+ removed in favor of the subFailure attribute (#3298)
+ - The 'changeDirectory' method of FTPClient has been deprecated in favor of
+ the 'cwd' method (#3491)
+
+Other
+-----
+
+ - #3202, #2869, #3225, #2955, #3237, #3196, #2355, #2881, #3054, #2374, #2918,
+ #3210, #3052, #3267, #3288, #2985, #3295, #3297, #2512, #3302, #1222, #2631,
+ #3306, #3116, #3215, #1489, #3319, #3320, #3321, #1255, #2169, #3182, #3323,
+ #3301, #3318, #3029, #3338, #3346, #1144, #3173, #3165, #685, #3357, #2582,
+ #3370, #2438, #1253, #637, #1971, #2208, #979, #1790, #1888, #1882, #1793,
+ #754, #1890, #1931, #1246, #1025, #3177, #2496, #2567, #3400, #2213, #2027,
+ #3415, #1262, #3422, #2500, #3414, #3045, #3111, #2974, #2947, #3222, #2878,
+ #3402, #2909, #3423, #1328, #1852, #3382, #3393, #2029, #3489, #1853, #2026,
+ #2375, #3502, #3482, #3504, #3505, #3507, #2605, #3519, #3520, #3121, #3484,
+ #3439, #3216, #3511, #3524, #3521, #3197, #2486, #2449, #2748, #3381, #3236,
+ #671
+
+
+Conch 8.2.0 (2008-12-16)
+========================
+
+Features
+--------
+ - The type of the protocols instantiated by SSHFactory is now parameterized
+ (#3443)
+
+Fixes
+-----
+ - A file descriptor leak has been fixed (#3213, #1789)
+ - "File Already Exists" errors are now handled more correctly (#3033)
+ - Handling of CR IAC in TelnetClient is now improved (#3305)
+ - SSHAgent is no longer completely unusable (#3332)
+ - The performance of insults.ClientProtocol is now greatly increased by
+ delivering more than one byte at a time to application code (#3386)
+ - Manhole and the conch server no longer need to be run as root when not
+ necessary (#2607)
+ - The value of FILEXFER_ATTR_ACMODTIME has been corrected (#2902)
+ - The management of known_hosts and host key verification has been overhauled
+ (#1376, #1301, #3494, #3496, #1292, #3499)
+
+Other
+-----
+ - #3193, #1633
+
+
+Lore 8.2.0 (2008-12-16)
+=======================
+
+Other
+-----
+ - #2207, #2514
+
+
+Mail 8.2.0 (2008-12-16)
+=======================
+
+Fixes
+-----
+ - The mailmail tool now provides better error messages for usage errors (#3339)
+ - The SMTP protocol implementation now works on PyPy (#2976)
+
+Other
+-----
+ - #3475
+
+
+Names 8.2.0 (2008-12-16)
+========================
+
+Features
+--------
+ - The NAPTR record type is now supported (#2276)
+
+Fixes
+-----
+ - Make client.Resolver less vulnerable to the Birthday Paradox attack by
+ avoiding sending duplicate queries when it's not necessary (#3347)
+ - client.Resolver now uses a random source port for each DNS request (#3342)
+ - client.Resolver now uses a full 16 bits of randomness for message IDs,
+ instead of 10 which it previously used (#3342)
+ - All record types now have value-based equality and a string representation
+ (#2935)
+
+Other
+-----
+ - #1622, #3424
+
+
+Web 8.2.0 (2008-12-16)
+======================
+
+Features
+--------
+ - The web server can now deal with multi-value headers in the new attributes of
+ Request, requestHeaders and responseHeaders (#165)
+ - There is now a resource-wrapper which implements HTTP Basic and Digest auth
+ in terms of twisted.cred (#696)
+ - It's now possible to limit the number of redirects that client.getPage will
+ follow (#2412)
+ - The directory-listing code no longer uses Woven (#3257)
+ - static.File now supports Range headers with a single range (#1493)
+ - twisted.web now has a rudimentary WSGI container (#2753)
+ - The web server now supports chunked encoding in requests (#3385)
+
+Fixes
+-----
+ - The xmlrpc client now raises an error when the server sends an empty
+ response (#3399)
+ - HTTPPageGetter no longer duplicates default headers when they're explicitly
+ overridden in the headers parameter (#1382)
+ - The server will no longer timeout clients which are still sending request
+ data (#1903)
+ - microdom's isEqualToNode now returns False when the nodes aren't equal
+ (#2542)
+
+Deprecations and Removals
+-------------------------
+
+ - Request.headers and Request.received_headers are not quite deprecated, but
+ they are discouraged in favor of requestHeaders and responseHeaders (#165)
+
+Other
+-----
+ - #909, #687, #2938, #1152, #2930, #2025, #2683, #3471
+
+
+Web2 8.2.0 (2008-12-16)
+=======================
+
+Note: Twisted Web2 is being phased out in preference for Twisted Web, but some
+maintenance changes have been made.
+
+Fixes
+-----
+ - The main twisted.web2 docstring now indicates the current state of the
+ project (#2028)
+ - Headers which require unusual bytes are now quoted (#2346)
+ - Some links in the introduction documentation have been fixed (#2552)
+
+
+Words 8.2.0 (2008-12-16)
+========================
+
+Feature
+-------
+ - There is now a standalone XMPP router included in twisted.words: it can be
+ used with the 'twistd xmpp-router' command line (#3407)
+ - A server factory for Jabber XML Streams has been added (#3435)
+ - Domish now allows for iterating child elements with specific qualified names
+ (#2429)
+ - IRCClient now has a 'back' method which removes the away status (#3366)
+ - IRCClient now has a 'whois' method (#3133)
+
+Fixes
+-----
+ - The IRC Client implementation can now deal with compound mode changes (#3230)
+ - The MSN protocol implementation no longer requires the CVR0 protocol to
+ be included in the VER command (#3394)
+ - In the IRC server implementation, topic messages will no longer be sent for
+ a group which has no topic (#2204)
+ - An infinite loop (which caused infinite memory usage) in irc.split has been
+ fixed. This was triggered any time a message that starts with a delimiter
+ was sent (#3446)
+ - Jabber's toResponse now generates a valid stanza even when stanzaType is not
+ specified (#3467)
+ - The lifetime of authenticator instances in XmlStreamServerFactory is no
+ longer artificially extended (#3464)
+
+Other
+-----
+ - #3365
+
+
+Core 8.1.0 (2008-05-18)
+=======================
+
+Features
+--------
+
+ - twisted.internet.error.ConnectionClosed is a new exception which is the
+ superclass of ConnectionLost and ConnectionDone (#3137)
+ - Trial's CPU and memory performance should be better now (#3034)
+ - twisted.python.filepath.FilePath now has a chmod method (#3124)
+
+Fixes
+-----
+
+ - Some reactor re-entrancy regressions were fixed (#3146, #3168)
+ - A regression was fixed whereby constructing a Failure for an exception and
+ traceback raised out of a Pyrex extension would fail (#3132)
+ - CopyableFailures in PB can again be created from CopiedFailures (#3174)
+ - FilePath.remove, when called on a FilePath representing a symlink to a
+ directory, no longer removes the contents of the targeted directory, and
+ instead removes the symlink (#3097)
+ - FilePath now has a linkTo method for creating new symlinks (#3122)
+ - The docstring for Trial's addCleanup method now correctly specifies when
+ cleanup functions are run (#3131)
+ - assertWarns now deals better with multiple identical warnings (#2904)
+ - Various windows installer bugs were fixed (#3115, #3144, #3150, #3151, #3164)
+ - API links in the howto documentation have been corrected (#3130)
+ - The Win32 Process transport object now has a pid attribute (#1836)
+ - A doc bug in the twistd plugin howto which would inevitably lead to
+ confusion was fixed (#3183)
+ - A regression breaking IOCP introduced after the last release was fixed
+ (#3200)
+
+Deprecations and Removals
+-------------------------
+
+ - mktap is now fully deprecated, and will emit DeprecationWarnings when used
+ (#3127)
+
+Other
+-----
+ - #3079, #3118, #3120, #3145, #3069, #3149, #3186, #3208, #2762
+
+
+Conch 8.1.0 (2008-05-18)
+========================
+
+Fixes
+-----
+ - A regression was fixed whereby the publicKeys and privateKeys attributes of
+ SSHFactory would not be interpreted as strings (#3141)
+ - The sshsimpleserver.py example had a minor bug fix (#3135)
+ - The deprecated mktap API is no longer used (#3127)
+ - An infelicity was fixed whereby a NameError would be raised in certain
+ circumstances during authentication when a ConchError should have been
+ (#3154)
+ - A workaround was added to conch.insults for a bug in gnome-terminal whereby
+ it would not scroll correctly (#3189)
+
+
+Lore 8.1.0 (2008-05-18)
+=======================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+News 8.1.0 (2008-05-18)
+=======================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+Web 8.1.0 (2008-05-18)
+======================
+
+Fixes
+-----
+ - Fixed an XMLRPC bug whereby sometimes a callRemote Deferred would
+ accidentally be fired twice when a connection was lost during the handling of
+ a response (#3152)
+ - Fixed a bug in the "Using Twisted Web" document which prevented an example
+ resource from being renderable (#3147)
+ - The deprecated mktap API is no longer used (#3127)
+
+
+Words 8.1.0 (2008-05-18)
+========================
+
+Features
+--------
+ - JID objects now have a nice __repr__ (#3156)
+ - Extending XMPP protocols is now easier (#2178)
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+ - A bug whereby one-time XMPP observers would be enabled permanently was fixed
+ (#3066)
+
+
+Mail 8.1.0 (2008-05-18)
+=======================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+Names 8.1.0 (2008-05-18)
+========================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+Web2 8.1.0 (2008-05-18)
+=======================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+Core 8.0.1 (2008-03-26)
+=======================
+
+Fixes
+-----
+ - README no longer refers to obsolete trial command line option
+ - twistd no longer causes a bizarre DeprecationWarning about mktap
+
+
+Core 8.0.0 (2008-03-17)
+=======================
+
+Features
+--------
+
+ - The IOCP reactor has had many changes and is now greatly improved
+ (#1760, #3055)
+ - The main Twisted distribution is now easy_installable (#1286, #3110)
+ - twistd can now profile with cProfile (#2469)
+ - twisted.internet.defer contains a DeferredFilesystemLock which gives a
+ Deferred interface to lock file acquisition (#2180)
+ - twisted.python.modules is a new system for representing and manipulating
+ module paths (i.e. sys.path) (#1951)
+ - twisted.internet.fdesc now contains a writeToFD function, along with other
+ minor fixes (#2419)
+ - twisted.python.usage now allows optional type enforcement (#739)
+ - The reactor now has a blockingCallFromThread method for non-reactor threads
+ to use to wait for a reactor-scheduled call to return a result (#1042, #3030)
+ - Exceptions raised inside of inlineCallbacks-using functions now have a
+ better chance of coming with a meaningful traceback (#2639, #2803)
+ - twisted.python.randbytes now contains code for generating secure random
+ bytes (#2685)
+ - The classes in twisted.application.internet now accept a reactor parameter
+ for specifying the reactor to use for underlying calls to allow for better
+ testability (#2937)
+ - LoopingCall now allows you to specify the reactor to use to schedule new
+ calls, allowing much better testing techniques (#2633, #2634)
+ - twisted.internet.task.deferLater is a new API for scheduling calls and
+ getting deferreds which are fired with their results (#1875)
+ - objgrep now knows how to search through deque objects (#2323)
+ - twisted.python.log now contains a Twisted log observer which can forward
+ messages to the Python logging system (#1351)
+ - Log files now include seconds in the timestamps (#867)
+ - It is now possible to limit the number of log files to create during log
+ rotation (#1095)
+ - The interface required by the log context system is now documented as
+ ILoggingContext, and abstract.FileDescriptor now declares that it implements
+ it (#1272)
+ - There is now an example cred checker that uses a database via adbapi (#460)
+ - The epoll reactor is now documented in the choosing-reactors howto (#2539)
+ - There were improvements to the client howto (#222)
+ - Int8Receiver was added (#2315)
+ - Various refactorings to AMP introduced better testability and public
+ interfaces (#2657, #2667, #2656, #2664, #2810)
+ - twisted.protocol.policies.TrafficLoggingFactory now has a resetCounter
+ method (#2757)
+ - The FTP client can be told which port range within which to bind passive
+ transfer ports (#1904)
+ - twisted.protocols.memcache contains a new asynchronous memcache client
+ (#2506, #2957)
+ - PB now supports anonymous login (#439, #2312)
+ - twisted.spread.jelly now supports decimal objects (#2920)
+ - twisted.spread.jelly now supports all forms of sets (#2958)
+ - There is now an interface describing the API that process protocols must
+ provide (#3020)
+ - Trial reporting to core unittest TestResult objects has been improved (#2495)
+ - Trial's TestCase now has an addCleanup method which allows easy setup of
+ tear-down code (#2610, #2899)
+ - Trial's TestCase now has an assertIsInstance method (#2749)
+ - Trial's memory footprint and speed are greatly improved (#2275)
+ - At the end of trial runs, "PASSED" and "FAILED" messages are now colorized
+ (#2856)
+ - Tests which leave global state around in the reactor will now fail in
+ trial. A new option, --unclean-warnings, will convert these errors back into
+ warnings (#2091)
+ - Trial now has a --without-module command line for testing code in an
+ environment that lacks a particular Python module (#1795)
+ - Error reporting of failed assertEquals assertions now has much nicer
+ formatting (#2893)
+ - Trial now has methods for monkey-patching (#2598)
+ - Trial now has an ITestCase (#2898, #1950)
+ - The trial reporter API now has a 'done' method which is called at the end of
+ a test run (#2883)
+ - TestCase now has an assertWarns method which allows testing that functions
+ emit warnings (#2626, #2703)
+ - There are now no string exceptions in the entire Twisted code base (#2063)
+ - There is now a system for specifying credentials checkers with a string
+ (#2570)
+
+Fixes
+-----
+
+ - Some tests which were asserting the value of stderr have been changed
+ because Python uncontrollably writes bytes to stderr (#2405)
+ - Log files handle time zones with DST better (#2404)
+ - Subprocesses using PTYs on OS X that are handled by Twisted will now be able
+ to more reliably write the final bytes before they exit, allowing Twisted
+ code to more reliably receive them (#2371, #2858)
+ - Trial unit test reporting has been improved (#1901)
+ - The kqueue reactor handles connection failures better (#2172)
+ - It's now possible to run "trial foo/bar/" without an exception: trailing
+ slashes no longer cause problems (#2005)
+ - cred portals now better deal with implementations of inherited interfaces
+ (#2523)
+ - FTP error handling has been improved (#1160, 1107)
+ - Trial behaves better with respect to file locking on Windows (#2482)
+ - The FTP server now gives a better error when STOR is attempted during an
+ anonymous session (#1575)
+ - Trial now behaves better with tests that use the reactor's threadpool (#1832)
+ - twisted.python.reload now behaves better with new-style objects (#2297)
+ - LogFile's defaultMode parameter is now better implemented, preventing
+ potential security exploits (#2586)
+ - A minor obscure leak in thread pools was corrected (#1134)
+ - twisted.internet.task.Clock now returns the correct DelayedCall from
+ callLater, instead of returning the one scheduled for the furthest in the
+ future (#2691)
+ - twisted.spread.util.FilePager no longer unnecessarily buffers data in
+ memory (#1843, 2321)
+ - Asking for twistd or trial to use an unavailable reactor no longer prints a
+ traceback (#2457)
+ - System event triggers have fewer obscure bugs (#2509)
+ - Plugin discovery code is much better behaved, allowing multiple
+ installations of a package with plugins (#2339, #2769)
+ - Process and PTYProcess have been merged and some minor bugs have been fixed
+ (#2341)
+ - The reactor has less global state (#2545)
+ - Failure can now correctly represent and format errors caused by string
+ exceptions (#2830)
+ - The epoll reactor now has better error handling which now avoids the bug
+ causing 100% CPU usage in some cases (#2809)
+ - Errors raised during trial setUp or tearDown methods are now handled better
+ (#2837)
+ - A problem when deferred callbacks add new callbacks to the deferred that
+ they are a callback of was fixed (#2849)
+ - Log messages that are emitted during connectionMade now have the protocol
+ prefix correctly set (#2813)
+ - The string representation of a TCP Server connection now contains the actual
+ port that it's bound to when it was configured to listen on port 0 (#2826)
+ - There is better reporting of error codes for TCP failures on Windows (#2425)
+ - Process spawning has been made slightly more robust by disabling garbage
+ collection temporarily immediately after forking so that finalizers cannot
+ be executed in an unexpected environment (#2483)
+ - namedAny now detects import errors better (#698)
+ - Many fixes and improvements to the twisted.python.zipstream module have
+ been made (#2996)
+ - FilePager no longer blows up on empty files (#3023)
+ - twisted.python.util.FancyEqMixin has been improved to cooperate with objects
+ of other types (#2944)
+ - twisted.python.FilePath.exists now restats to prevent incorrect result
+ (#2896)
+ - twisted.python.util.mergeFunctionMetadata now also merges the __module__
+ attribute (#3049)
+ - It is now possible to call transport.pauseProducing within connectionMade on
+ TCP transports without it being ignored (#1780)
+ - twisted.python.versions now understands new SVN metadata format for fetching
+ the SVN revision number (#3058)
+ - It's now possible to use reactor.callWhenRunning(reactor.stop) on gtk2 and
+ glib2 reactors (#3011)
+
+Deprecations and removals
+-------------------------
+ - twisted.python.timeoutqueue is now deprecated (#2536)
+ - twisted.enterprise.row and twisted.enterprise.reflector are now deprecated
+ (#2387)
+ - twisted.enterprise.util is now deprecated (#3022)
+ - The dispatch and dispatchWithCallback methods of ThreadPool are now
+ deprecated (#2684)
+ - Starting the same reactor multiple times is now deprecated (#1785)
+ - The visit method of various test classes in trial has been deprecated (#2897)
+ - The --report-profile option to twistd and twisted.python.dxprofile are
+ deprecated (#2908)
+ - The upDownError method of Trial reporters is deprecated (#2883)
+
+Other
+-----
+
+ - #2396, #2211, #1921, #2378, #2247, #1603, #2463, #2530, #2426, #2356, #2574,
+ - #1844, #2575, #2655, #2640, #2670, #2688, #2543, #2743, #2744, #2745, #2746,
+ - #2742, #2741, #1730, #2831, #2216, #1192, #2848, #2767, #1220, #2727, #2643,
+ - #2669, #2866, #2867, #1879, #2766, #2855, #2547, #2857, #2862, #1264, #2735,
+ - #942, #2885, #2739, #2901, #2928, #2954, #2906, #2925, #2942, #2894, #2793,
+ - #2761, #2977, #2968, #2895, #3000, #2990, #2919, #2969, #2921, #3005, #421,
+ - #3031, #2940, #1181, #2783, #1049, #3053, #2847, #2941, #2876, #2886, #3086,
+ - #3095, #3109
+
+
+Conch 8.0.0 (2008-03-17)
+========================
+
+Features
+--------
+ - Add DEC private mode manipulation methods to ITerminalTransport. (#2403)
+
+Fixes
+-----
+ - Parameterize the scheduler function used by the insults TopWindow widget.
+ This change breaks backwards compatibility in the TopWindow initializer.
+ (#2413)
+ - Notify subsystems, like SFTP, of connection close. (#2421)
+ - Change the process file descriptor "connection lost" code to reverse the
+ setNonBlocking operation done during initialization. (#2371)
+ - Change ConsoleManhole to wait for connectionLost notification before
+ stopping the reactor. (#2123, #2371)
+ - Make SSHUserAuthServer.ssh_USERAUTH_REQUEST return a Deferred. (#2528)
+ - Manhole's initializer calls its parent class's initializer with its
+ namespace argument. (#2587)
+ - Handle ^C during input line continuation in manhole by updating the prompt
+ and line buffer correctly. (#2663)
+ - Make twisted.conch.telnet.Telnet by default reject all attempts to enable
+ options. (#1967)
+ - Reduce the number of calls into application code to deliver application-level
+ data in twisted.conch.telnet.Telnet.dataReceived (#2107)
+ - Fix definition and management of extended attributes in conch file transfer.
+ (#3010)
+ - Fix parsing of OpenSSH-generated RSA keys with differing ASN.1 packing style.
+ (#3008)
+ - Fix handling of missing $HOME in twisted.conch.client.unix. (#3061)
+
+Misc
+----
+ - #2267, #2378, #2604, #2707, #2341, #2685, #2679, #2912, #2977, #2678, #2709
+ #2063, #2847
+
+
+Lore 8.0.0 (2008-03-17)
+=======================
+
+Fixes
+-----
+ - Change twisted.lore.tree.setIndexLin so that it removes node with index-link
+ class when the specified index filename is None. (#812)
+ - Fix the conversion of the list of options in man pages to Lore format.
+ (#3017)
+ - Fix conch man pages generation. (#3075)
+ - Fix management of the interactive command tag in man2lore. (#3076)
+
+Misc
+----
+ - #2847
+
+
+News 8.0.0 (2008-03-17)
+=======================
+
+Misc
+----
+ - Remove all "API Stability" markers (#2847)
+
+
+Runner 8.0.0 (2008-03-17)
+=========================
+
+Misc
+----
+ - Remove all "API Stability" markers (#2847)
+
+
+Web 8.0.0 (2008-03-17)
+======================
+
+Features
+--------
+ - Add support to twisted.web.client.getPage for the HTTP HEAD method. (#2750)
+
+Fixes
+-----
+ - Set content-type in xmlrpc responses to "text/xml" (#2430)
+ - Add more error checking in the xmlrpc.XMLRPC render method, and enforce
+ POST requests. (#2505)
+ - Reject unicode input to twisted.web.client._parse to reject invalid
+ unicode URLs early. (#2628)
+ - Correctly re-quote URL path segments when generating an URL string to
+ return from Request.prePathURL. (#2934)
+ - Make twisted.web.proxy.ProxyClientFactory close the connection when
+ reporting a 501 error. (#1089)
+ - Fix twisted.web.proxy.ReverseProxyResource to specify the port in the
+ host header if different from 80. (#1117)
+ - Change twisted.web.proxy.ReverseProxyResource so that it correctly encodes
+ the request URI it sends on to the server for which it is a proxy. (#3013)
+ - Make "twistd web --personal" use PBServerFactory (#2681)
+
+Misc
+----
+ - #1996, #2382, #2211, #2633, #2634, #2640, #2752, #238, #2905
+
+
+Words 8.0.0 (2008-03-17)
+========================
+
+Features
+--------
+ - Provide function for creating XMPP response stanzas. (#2614, #2614)
+ - Log exceptions raised in Xish observers. (#2616)
+ - Add 'and' and 'or' operators for Xish XPath expressions. (#2502)
+ - Make JIDs hashable. (#2770)
+
+Fixes
+-----
+ - Respect the hostname and servername parameters to IRCClient.register. (#1649)
+ - Make EventDispatcher remove empty callback lists. (#1652)
+ - Use legacy base64 API to support Python 2.3 (#2461)
+ - Fix support of DIGEST-MD5 challenge parsing with multi-valued directives.
+ (#2606)
+ - Fix reuse of dict of prefixes in domish.Element.toXml (#2609)
+ - Properly process XMPP stream headers (#2615)
+ - Use proper namespace for XMPP stream errors. (#2630)
+ - Properly parse XMPP stream errors. (#2771)
+ - Fix toResponse for XMPP stanzas without an id attribute. (#2773)
+ - Move XMPP stream header procesing to authenticators. (#2772)
+
+Misc
+----
+ - #2617, #2640, #2741, #2063, #2570, #2847
+
+
+Mail 8.0.0 (2008-03-17)
+=======================
+
+Features
+--------
+ - Support CAPABILITY responses that include atoms of the form "FOO" and
+ "FOO=BAR" in IMAP4 (#2695)
+ - Parameterize error handling behavior of imap4.encoder and imap4.decoder.
+ (#2929)
+
+Fixes
+-----
+ - Handle empty passwords in SMTP auth. (#2521)
+ - Fix IMAP4Client's parsing of literals which are not preceeded by whitespace.
+ (#2700)
+ - Handle MX lookup suceeding without answers. (#2807)
+ - Fix issues with aliases(5) process support. (#2729)
+
+Misc
+----
+ - #2371, #2123, #2378, #739, #2640, #2746, #1917, #2266, #2864, #2832, #2063,
+ #2865, #2847
+
+
+Names 8.0.0 (2008-03-17)
+========================
+
+Fixes
+-----
+
+ - Refactor DNSDatagramProtocol and DNSProtocol to use same base class (#2414)
+ - Change Resolver to query specified nameservers in specified order, instead
+ of reverse order. (#2290)
+ - Make SRVConnector work with bad results and NXDOMAIN responses.
+ (#1908, #2777)
+ - Handle write errors happening in dns queries, to have correct deferred
+ failures. (#2492)
+ - Fix the value of OP_NOTIFY and add a definition for OP_UPDATE. (#2945)
+
+Misc
+----
+ - #2685, #2936, #2581, #2847
+
diff --git a/README b/README
new file mode 100644
index 0000000..c0134b8
--- /dev/null
+++ b/README
@@ -0,0 +1,116 @@
+Twisted 12.1.0
+
+Quote of the Release:
+
+
+ <keturn> efnet is the cutting edge of hanging out with people on the internet in the '90s
+
+
+For information on what's new in Twisted 12.1.0, see the NEWS file that comes
+with the distribution.
+
+What is this?
+=============
+
+ Twisted is an event-based framework for internet applications. It includes
+ modules for many different purposes, including the following:
+
+ - twisted.application
+ A "Service" system that allows you to organize your application in
+ hierarchies with well-defined startup and dependency semantics,
+ - twisted.cred
+ A general credentials and authentication system that facilitates
+ pluggable authentication backends,
+ - twisted.enterprise
+ Asynchronous database access, compatible with any Python DBAPI2.0
+ modules,
+ - twisted.internet
+ Low-level asynchronous networking APIs that allow you to define
+ your own protocols that run over certain transports,
+ - twisted.manhole
+ A tool for remote debugging of your services which gives you a
+ Python interactive interpreter,
+ - twisted.protocols
+ Basic protocol implementations and helpers for your own protocol
+ implementations,
+ - twisted.python
+ A large set of utilities for Python tricks, reflection, text
+ processing, and anything else,
+ - twisted.spread
+ A secure, fast remote object system,
+ - twisted.trial
+ A unit testing framework that integrates well with Twisted-based code.
+
+ Twisted supports integration of the Win32, Tk, GTK+ and GTK+ 2 event loops
+ with its main event loop. There is experimental support for Mac OS X and
+ wxPython event loop integration, which you use at your peril.
+
+ For more information, visit http://www.twistedmatrix.com, or join the list
+ at http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python
+
+ There are many official Twisted subprojects, including clients and
+ servers for web, mail, DNS, and more. You can find out more about
+ these projects at http://twistedmatrix.com/trac/wiki/TwistedProjects
+
+
+Installing
+==========
+
+ Instructions for installing this software are in INSTALL.
+
+Unit Tests
+==========
+
+
+ See our unit tests run proving that the software is BugFree(TM):
+
+ % trial twisted
+
+ Some of these tests may fail if you
+ * don't have the dependancies required for a particular subsystem installed,
+ * have a firewall blocking some ports (or things like Multicast, which Linux
+ NAT has shown itself to do), or
+ * run them as root.
+
+
+Documentation and Support
+=========================
+
+ Examples on how to use Twisted APIs are located in doc/core/examples; this
+ might ease the learning curve a little bit, since all these files are kept
+ as short as possible. The file doc/core/howto/index.xhtml contains an index
+ of all the core HOWTOs: this should be your starting point when looking for
+ documentation.
+
+ Help is available on the Twisted mailing list:
+
+ http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-python
+
+ There is also a very lively IRC channel, #twisted, on
+ chat.freenode.net.
+
+
+Copyright
+=========
+
+ All of the code in this distribution is Copyright (c) 2001-2012
+ Twisted Matrix Laboratories.
+
+ Twisted is made available under the MIT license. The included
+ LICENSE file describes this in detail.
+
+
+Warranty
+========
+
+ THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+ EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+ TO THE USE OF THIS SOFTWARE IS WITH YOU.
+
+ IN NO EVENT WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+ AND/OR REDISTRIBUTE THE LIBRARY, BE LIABLE TO YOU FOR ANY DAMAGES, EVEN IF
+ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGES.
+
+ Again, see the included LICENSE file for specific legal details.
diff --git a/bin/_preamble.py b/bin/_preamble.py
new file mode 100644
index 0000000..fcd6014
--- /dev/null
+++ b/bin/_preamble.py
@@ -0,0 +1,19 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# This makes sure that users don't have to set up their environment
+# specially in order to run these programs from bin/.
+
+# This helper is shared by many different actual scripts. It is not intended to
+# be packaged or installed, it is only a developer convenience. By the time
+# Twisted is actually installed somewhere, the environment should already be set
+# up properly without the help of this tool.
+
+import sys, os
+
+path = os.path.abspath(sys.argv[0])
+while os.path.dirname(path) != path:
+ if os.path.exists(os.path.join(path, 'twisted', '__init__.py')):
+ sys.path.insert(0, path)
+ break
+ path = os.path.dirname(path)
diff --git a/bin/conch/cftp b/bin/conch/cftp
new file mode 100755
index 0000000..106fa0b
--- /dev/null
+++ b/bin/conch/cftp
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.conch.scripts.cftp import run
+run()
diff --git a/bin/conch/ckeygen b/bin/conch/ckeygen
new file mode 100755
index 0000000..df31973
--- /dev/null
+++ b/bin/conch/ckeygen
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.conch.scripts.ckeygen import run
+run()
diff --git a/bin/conch/conch b/bin/conch/conch
new file mode 100755
index 0000000..8ad4b99
--- /dev/null
+++ b/bin/conch/conch
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.conch.scripts.conch import run
+run()
diff --git a/bin/conch/tkconch b/bin/conch/tkconch
new file mode 100755
index 0000000..78376a3
--- /dev/null
+++ b/bin/conch/tkconch
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.conch.scripts.tkconch import run
+run()
diff --git a/bin/lore/lore b/bin/lore/lore
new file mode 100755
index 0000000..6515828
--- /dev/null
+++ b/bin/lore/lore
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.lore.scripts.lore import run
+run()
+
diff --git a/bin/mail/mailmail b/bin/mail/mailmail
new file mode 100755
index 0000000..d4bfb3c
--- /dev/null
+++ b/bin/mail/mailmail
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This script attempts to send some email.
+"""
+
+import sys, os
+extra = os.path.dirname(os.path.dirname(sys.argv[0]))
+sys.path.insert(0, extra)
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+sys.path.remove(extra)
+
+from twisted.mail.scripts import mailmail
+mailmail.run()
+
diff --git a/bin/manhole b/bin/manhole
new file mode 100755
index 0000000..35f78ff
--- /dev/null
+++ b/bin/manhole
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This script runs GtkManhole, a client for Twisted.Manhole
+"""
+import sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+from twisted.scripts import manhole
+manhole.run()
diff --git a/bin/pyhtmlizer b/bin/pyhtmlizer
new file mode 100755
index 0000000..f212b10
--- /dev/null
+++ b/bin/pyhtmlizer
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+import sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+from twisted.scripts.htmlizer import run
+run()
diff --git a/bin/tap2deb b/bin/tap2deb
new file mode 100755
index 0000000..73d2032
--- /dev/null
+++ b/bin/tap2deb
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+tap2deb
+"""
+import sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+from twisted.scripts import tap2deb
+tap2deb.run()
diff --git a/bin/tap2rpm b/bin/tap2rpm
new file mode 100755
index 0000000..c2f7368
--- /dev/null
+++ b/bin/tap2rpm
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# based off the tap2deb code
+# tap2rpm built by Sean Reifschneider, <jafo@tummy.com>
+
+"""
+tap2rpm
+"""
+import sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+from twisted.scripts import tap2rpm
+tap2rpm.run()
diff --git a/bin/tapconvert b/bin/tapconvert
new file mode 100755
index 0000000..127dc0b
--- /dev/null
+++ b/bin/tapconvert
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+import sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+from twisted.scripts.tapconvert import run
+run()
diff --git a/bin/trial b/bin/trial
new file mode 100755
index 0000000..c45a1ca
--- /dev/null
+++ b/bin/trial
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+import os, sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+# begin chdir armor
+sys.path[:] = map(os.path.abspath, sys.path)
+# end chdir armor
+
+sys.path.insert(0, os.path.abspath(os.getcwd()))
+
+from twisted.scripts.trial import run
+run()
diff --git a/bin/twistd b/bin/twistd
new file mode 100755
index 0000000..6035e5f
--- /dev/null
+++ b/bin/twistd
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+import os, sys
+
+try:
+ import _preamble
+except ImportError:
+ sys.exc_clear()
+
+sys.path.insert(0, os.path.abspath(os.getcwd()))
+
+from twisted.scripts.twistd import run
+run()
diff --git a/doc/conch/benchmarks/README b/doc/conch/benchmarks/README
new file mode 100644
index 0000000..233bc8e
--- /dev/null
+++ b/doc/conch/benchmarks/README
@@ -0,0 +1,15 @@
+This directory contains various simple programs intended to exercise various
+features of Twisted Conch as a way to learn about and track their
+performance characteristics. As there is currently no record of past
+benchmark results, the tracking aspect of this is currently somewhat
+fantastic. However, the intent is for this to change at some future point.
+
+All (one) of the programs in this directory are currently intended to be
+invoked directly and to report some timing information on standard out.
+
+The following benchmarks are currently available:
+
+buffering_mixin.py:
+
+ This deals with twisted.conch.mixin.BufferingMixin which provides
+ Nagle-like write coalescing for Protocol classes.
diff --git a/doc/conch/benchmarks/buffering_mixin.py b/doc/conch/benchmarks/buffering_mixin.py
new file mode 100755
index 0000000..7009df8
--- /dev/null
+++ b/doc/conch/benchmarks/buffering_mixin.py
@@ -0,0 +1,182 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Benchmarks comparing the write performance of a "normal" Protocol instance
+and an instance of a Protocol class which has had L{twisted.conch.mixin}'s
+L{BufferingMixin<twisted.conch.mixin.BufferingMixin>} mixed in to perform
+Nagle-like write coalescing.
+"""
+
+from sys import stdout
+from pprint import pprint
+from time import time
+
+from twisted.python.usage import Options
+from twisted.python.log import startLogging
+
+from twisted.internet.protocol import ServerFactory, Protocol, ClientCreator
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+
+from twisted.conch.mixin import BufferingMixin
+
+
+class BufferingBenchmark(Options):
+ """
+ Options for configuring the execution parameters of a benchmark run.
+ """
+
+ optParameters = [
+ ('scale', 's', '1',
+ 'Work multiplier (bigger takes longer, might resist noise better)')]
+
+ def postOptions(self):
+ self['scale'] = int(self['scale'])
+
+
+
+class ServerProtocol(Protocol):
+ """
+ A silent protocol which only waits for a particular amount of input and
+ then fires a Deferred.
+ """
+ def __init__(self, expected, finished):
+ self.expected = expected
+ self.finished = finished
+
+
+ def dataReceived(self, bytes):
+ self.expected -= len(bytes)
+ if self.expected == 0:
+ finished, self.finished = self.finished, None
+ finished.callback(None)
+
+
+
+class BufferingProtocol(Protocol, BufferingMixin):
+ """
+ A protocol which uses the buffering mixin to provide a write method.
+ """
+
+
+
+class UnbufferingProtocol(Protocol):
+ """
+ A protocol which provides a naive write method which simply passes through
+ to the transport.
+ """
+
+ def connectionMade(self):
+ """
+ Bind write to the transport's write method and flush to a no-op
+ function in order to provide the same API as is provided by
+ BufferingProtocol.
+ """
+ self.write = self.transport.write
+ self.flush = lambda: None
+
+
+
+def _write(proto, byteCount):
+ write = proto.write
+ flush = proto.flush
+
+ for i in range(byteCount):
+ write('x')
+ flush()
+
+
+
+def _benchmark(byteCount, clientProtocol):
+ result = {}
+ finished = Deferred()
+ def cbFinished(ignored):
+ result[u'disconnected'] = time()
+ result[u'duration'] = result[u'disconnected'] - result[u'connected']
+ return result
+ finished.addCallback(cbFinished)
+
+ f = ServerFactory()
+ f.protocol = lambda: ServerProtocol(byteCount, finished)
+ server = reactor.listenTCP(0, f)
+
+ f2 = ClientCreator(reactor, clientProtocol)
+ proto = f2.connectTCP('127.0.0.1', server.getHost().port)
+ def connected(proto):
+ result[u'connected'] = time()
+ return proto
+ proto.addCallback(connected)
+ proto.addCallback(_write, byteCount)
+ return finished
+
+
+
+def _benchmarkBuffered(byteCount):
+ return _benchmark(byteCount, BufferingProtocol)
+
+
+
+def _benchmarkUnbuffered(byteCount):
+ return _benchmark(byteCount, UnbufferingProtocol)
+
+
+
+def benchmark(scale=1):
+ """
+ Benchmark and return information regarding the relative performance of a
+ protocol which does not use the buffering mixin and a protocol which
+ does.
+
+ @type scale: C{int}
+ @param scale: A multipler to the amount of work to perform
+
+ @return: A Deferred which will fire with a dictionary mapping each of
+ the two unicode strings C{u'buffered'} and C{u'unbuffered'} to
+ dictionaries describing the performance of a protocol of each type.
+ These value dictionaries will map the unicode strings C{u'connected'}
+ and C{u'disconnected'} to the times at which each of those events
+ occurred and C{u'duration'} two the difference between these two values.
+ """
+ overallResult = {}
+
+ byteCount = 1024
+
+ bufferedDeferred = _benchmarkBuffered(byteCount * scale)
+ def didBuffered(bufferedResult):
+ overallResult[u'buffered'] = bufferedResult
+ unbufferedDeferred = _benchmarkUnbuffered(byteCount * scale)
+ def didUnbuffered(unbufferedResult):
+ overallResult[u'unbuffered'] = unbufferedResult
+ return overallResult
+ unbufferedDeferred.addCallback(didUnbuffered)
+ return unbufferedDeferred
+ bufferedDeferred.addCallback(didBuffered)
+ return bufferedDeferred
+
+
+
+def main(args=None):
+ """
+ Perform a single benchmark run, starting and stopping the reactor and
+ logging system as necessary.
+ """
+ startLogging(stdout)
+
+ options = BufferingBenchmark()
+ options.parseOptions(args)
+
+ d = benchmark(options['scale'])
+ def cbBenchmark(result):
+ pprint(result)
+ def ebBenchmark(err):
+ print err.getTraceback()
+ d.addCallbacks(cbBenchmark, ebBenchmark)
+ def stopReactor(ign):
+ reactor.stop()
+ d.addBoth(stopReactor)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/conch/examples/demo.tac b/doc/conch/examples/demo.tac
new file mode 100644
index 0000000..8eb492c
--- /dev/null
+++ b/doc/conch/examples/demo.tac
@@ -0,0 +1,25 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo.tac
+
+"""Nearly pointless demonstration of the manhole interactive interpreter.
+
+This does about the same thing as demo_manhole, but uses the tap
+module's makeService method instead. The only interesting difference
+is that in this version, the telnet server also requires
+authentication.
+
+Note, you will have to create a file named \"passwd\" and populate it
+with credentials (in the format of passwd(5)) to use this demo.
+"""
+
+from twisted.application import service
+application = service.Application("TAC Demo")
+
+from twisted.conch import manhole_tap
+manhole_tap.makeService({"telnetPort": "tcp:6023",
+ "sshPort": "tcp:6022",
+ "namespace": {"foo": "bar"},
+ "passwd": "passwd"}).setServiceParent(application)
diff --git a/doc/conch/examples/demo_draw.tac b/doc/conch/examples/demo_draw.tac
new file mode 100644
index 0000000..d80b064
--- /dev/null
+++ b/doc/conch/examples/demo_draw.tac
@@ -0,0 +1,80 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo_draw.tac
+
+"""A trivial drawing application.
+
+Clients are allowed to connect and spew various characters out over
+the terminal. Spacebar changes the drawing character, while the arrow
+keys move the cursor.
+"""
+
+from twisted.conch.insults import insults
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+from twisted.internet import protocol
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+class Draw(insults.TerminalProtocol):
+ """Protocol which accepts arrow key and spacebar input and places
+ the requested characters onto the terminal.
+ """
+ cursors = list('!@#$%^&*()_+-=')
+
+ def connectionMade(self):
+ self.terminal.eraseDisplay()
+ self.terminal.resetModes([insults.modes.IRM])
+ self.cursor = self.cursors[0]
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == self.terminal.UP_ARROW:
+ self.terminal.cursorUp()
+ elif keyID == self.terminal.DOWN_ARROW:
+ self.terminal.cursorDown()
+ elif keyID == self.terminal.LEFT_ARROW:
+ self.terminal.cursorBackward()
+ elif keyID == self.terminal.RIGHT_ARROW:
+ self.terminal.cursorForward()
+ elif keyID == ' ':
+ self.cursor = self.cursors[(self.cursors.index(self.cursor) + 1) % len(self.cursors)]
+ else:
+ return
+ self.terminal.write(self.cursor)
+ self.terminal.cursorBackward()
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Insults Demo App")
+makeService({'protocolFactory': Draw,
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/examples/demo_insults.tac b/doc/conch/examples/demo_insults.tac
new file mode 100644
index 0000000..ebb01c5
--- /dev/null
+++ b/doc/conch/examples/demo_insults.tac
@@ -0,0 +1,252 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo_insults.tac
+
+"""Various simple terminal manipulations using the insults module.
+
+This demo sets up two listening ports: one on 6022 which accepts ssh
+connections; one on 6023 which accepts telnet connections. No login
+for the telnet server is required; for the ssh server, \"username\" is
+the username and \"password\" is the password.
+
+The TerminalProtocol subclass defined here ignores most user input
+(except to print it out to the server log) and spends the duration of
+the connection drawing (the author's humble approximation of)
+raindrops at random locations on the client's terminal. +, -, *, and
+/ are respected and each adjusts an aspect of the timing of the
+animation process.
+"""
+
+import random, string
+
+from twisted.python import log
+from twisted.internet import protocol, task
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+from twisted.conch.insults import insults
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+class DrawingFinished(Exception):
+ """Sentinel exception, raised when no \"frames\" for a particular
+ \"animation\" remain to be drawn.
+ """
+
+class Drawable:
+ """Representation of an animation.
+
+ Constructed with a protocol instance and a coordinate on the
+ screen, waits for invocations of iterate() at which point it
+ erases the previous frame of the animation and draws the next one,
+ using its protocol instance and always placing the upper left hand
+ corner of the frame at the given coordinates.
+
+ Frames are defined with draw_ prefixed methods. Erasure is
+ performed by erase_ prefixed methods.
+ """
+ n = 0
+
+ def __init__(self, proto, col, line):
+ self.proto = proto
+ self.col = col
+ self.line = line
+
+ def drawLines(self, s):
+ lines = s.splitlines()
+ c = self.col
+ line = self.line
+ for l in lines:
+ self.proto.cursorPosition(c - len(lines) / 2, line)
+ self.proto.write(l)
+ line += 1
+
+ def iterate(self):
+ getattr(self, 'erase_' + str(self.n))()
+ self.n += 1
+ f = getattr(self, 'draw_' + str(self.n), None)
+ if f is None:
+ raise DrawingFinished()
+ f()
+
+ def erase_0(self):
+ pass
+
+
+class Splat(Drawable):
+ HEIGHT = 5
+ WIDTH = 11
+
+ def draw_1(self):
+ # . .
+ #. . .
+ # . .
+ self.drawLines(' . .\n. . .\n . .')
+
+ def erase_1(self):
+ self.drawLines(' \n \n ')
+
+ def draw_2(self):
+ # . . . .
+ # . o o o .
+ #. o o o o .
+ # . o o o .
+ # . . . .
+ self.drawLines(' . . . .\n . o o o .\n. o o o o .\n . o o o .\n . . . .')
+
+ def erase_2(self):
+ self.drawLines(' \n \n \n \n ')
+
+ def draw_3(self):
+ # o o o o
+ # o O O O o
+ #o O O O O o
+ # o O O O o
+ # o o o o
+ self.drawLines(' o o o o\n o O O O o\no O O O O o\n o O O O o\n o o o o')
+
+ erase_3 = erase_2
+
+ def draw_4(self):
+ # O O O O
+ # O . . . O
+ #O . . . . O
+ # O . . . O
+ # O O O O
+ self.drawLines(' O O O O\n O . . . O\nO . . . . O\n O . . . O\n O O O O')
+
+ erase_4 = erase_3
+
+ def draw_5(self):
+ # . . . .
+ # . .
+ #. .
+ # . .
+ # . . . .
+ self.drawLines(' . . . .\n . .\n. .\n . .\n . . . .')
+
+ erase_5 = erase_4
+
+class Drop(Drawable):
+ WIDTH = 3
+ HEIGHT = 4
+
+ def draw_1(self):
+ # o
+ self.drawLines(' o')
+
+ def erase_1(self):
+ self.drawLines(' ')
+
+ def draw_2(self):
+ # _
+ #/ \
+ #\./
+ self.drawLines(' _ \n/ \\\n\\./')
+
+ def erase_2(self):
+ self.drawLines(' \n \n ')
+
+ def draw_3(self):
+ # O
+ self.drawLines(' O')
+
+ def erase_3(self):
+ self.drawLines(' ')
+
+class DemoProtocol(insults.TerminalProtocol):
+ """Draws random things at random places on the screen.
+ """
+ width = 80
+ height = 24
+
+ interval = 0.1
+ rate = 0.05
+
+ def connectionMade(self):
+ self.run()
+
+ def connectionLost(self, reason):
+ self._call.stop()
+ del self._call
+
+ def run(self):
+ # Clear the screen, matey
+ self.terminal.eraseDisplay()
+
+ self._call = task.LoopingCall(self._iterate)
+ self._call.start(self.interval)
+
+ def _iterate(self):
+ cls = random.choice((Splat, Drop))
+
+ # Move to a random location on the screen
+ col = random.randrange(self.width - cls.WIDTH) + cls.WIDTH
+ line = random.randrange(self.height - cls.HEIGHT) + cls.HEIGHT
+
+ s = cls(self.terminal, col, line)
+
+ c = task.LoopingCall(s.iterate)
+ c.start(self.rate).addErrback(lambda f: f.trap(DrawingFinished)).addErrback(log.err)
+
+ # ITerminalListener
+ def terminalSize(self, width, height):
+ self.width = width
+ self.height = height
+
+ def unhandledControlSequence(self, seq):
+ log.msg("Client sent something weird: %r" % (seq,))
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == '+':
+ self.interval /= 1.1
+ elif keyID == '-':
+ self.interval *= 1.1
+ elif keyID == '*':
+ self.rate /= 1.1
+ elif keyID == '/':
+ self.rate *= 1.1
+ else:
+ log.msg("Client sent: %r" % (keyID,))
+ return
+
+ self._call.stop()
+ self._call = task.LoopingCall(self._iterate)
+ self._call.start(self.interval)
+
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Insults Demo App")
+
+makeService({'protocolFactory': DemoProtocol,
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/examples/demo_manhole.tac b/doc/conch/examples/demo_manhole.tac
new file mode 100644
index 0000000..11808c6
--- /dev/null
+++ b/doc/conch/examples/demo_manhole.tac
@@ -0,0 +1,56 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo_manhole.tac
+
+"""An interactive Python interpreter with syntax coloring.
+
+Nothing interesting is actually defined here. Two listening ports are
+set up and attached to protocols which know how to properly set up a
+ColoredManhole instance.
+"""
+
+from twisted.conch.manhole import ColoredManhole
+from twisted.conch.insults import insults
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+from twisted.internet import protocol
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Interactive Python Interpreter")
+
+makeService({'protocolFactory': ColoredManhole,
+ 'protocolArgs': (None,),
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/examples/demo_recvline.tac b/doc/conch/examples/demo_recvline.tac
new file mode 100644
index 0000000..17ec78b
--- /dev/null
+++ b/doc/conch/examples/demo_recvline.tac
@@ -0,0 +1,77 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo_recvline.tac
+
+"""Demonstrates line-at-a-time handling with basic line-editing support.
+
+This is a variation on the echo server. It sets up two listening
+ports: one on 6022 which accepts ssh connections; one on 6023 which
+accepts telnet connections. No login for the telnet server is
+required; for the ssh server, \"username\" is the username and
+\"password\" is the password.
+
+The demo protocol defined in this module is handed a line of input at
+a time, which it simply writes back to the connection.
+HistoricRecvline, which the demo protocol subclasses, provides basic
+line editing and input history features.
+"""
+
+from twisted.conch import recvline
+from twisted.conch.insults import insults
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+from twisted.internet import protocol
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+class DemoRecvLine(recvline.HistoricRecvLine):
+ """Simple echo protocol.
+
+ Accepts lines of input and writes them back to its connection. If
+ a line consisting solely of \"quit\" is received, the connection
+ is dropped.
+ """
+
+ def lineReceived(self, line):
+ if line == "quit":
+ self.terminal.loseConnection()
+ self.terminal.write(line)
+ self.terminal.nextLine()
+ self.terminal.write(self.ps[self.pn])
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Insults RecvLine Demo")
+
+makeService({'protocolFactory': DemoRecvLine,
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/examples/demo_scroll.tac b/doc/conch/examples/demo_scroll.tac
new file mode 100644
index 0000000..3e63c09
--- /dev/null
+++ b/doc/conch/examples/demo_scroll.tac
@@ -0,0 +1,100 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this .tac file directly with:
+# twistd -ny demo_scroll.tac
+
+"""Simple echo-ish server that uses the scroll-region.
+
+This demo sets up two listening ports: one on 6022 which accepts ssh
+connections; one on 6023 which accepts telnet connections. No login
+for the telnet server is required; for the ssh server, \"username\" is
+the username and \"password\" is the password.
+
+The TerminalProtocol subclass defined here sets up a scroll-region occupying
+most of the screen. It positions the cursor at the bottom of the screen and
+then echos back printable input. When return is received, the line is
+copied to the upper area of the screen (scrolling anything older up) and
+clears the input line.
+"""
+
+import string
+
+from twisted.python import log
+from twisted.internet import protocol
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+from twisted.conch.insults import insults
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+class DemoProtocol(insults.TerminalProtocol):
+ """Copies input to an upwards scrolling region.
+ """
+ width = 80
+ height = 24
+
+ def connectionMade(self):
+ self.buffer = []
+ self.terminalSize(self.width, self.height)
+
+ # ITerminalListener
+ def terminalSize(self, width, height):
+ self.width = width
+ self.height = height
+
+ self.terminal.setScrollRegion(0, height - 1)
+ self.terminal.cursorPosition(0, height)
+ self.terminal.write('> ')
+
+ def unhandledControlSequence(self, seq):
+ log.msg("Client sent something weird: %r" % (seq,))
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == '\r':
+ self.terminal.cursorPosition(0, self.height - 2)
+ self.terminal.nextLine()
+ self.terminal.write(''.join(self.buffer))
+ self.terminal.cursorPosition(0, self.height - 1)
+ self.terminal.eraseToLineEnd()
+ self.terminal.write('> ')
+ self.buffer = []
+ elif keyID in list(string.printable):
+ self.terminal.write(keyID)
+ self.buffer.append(keyID)
+
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Scroll Region Demo App")
+
+makeService({'protocolFactory': DemoProtocol,
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/examples/index.html b/doc/conch/examples/index.html
new file mode 100644
index 0000000..cdb7d5e
--- /dev/null
+++ b/doc/conch/examples/index.html
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Conch code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Conch code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">Simple SSH server and client</a></li><li><a href="#auto1">Simple telnet server</a></li><li><a href="#auto2">twisted.conch.insults examples</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Simple SSH server and client<a name="auto0"/></h2>
+ <ul>
+ <li><a href="sshsimpleclient.py" shape="rect">sshsimpleclient.py</a> - simple SSH client</li>
+ <li><a href="sshsimpleserver.py" shape="rect">sshsimpleserver.py</a> - simple SSH server</li>
+ </ul>
+
+ <h2>Simple telnet server<a name="auto1"/></h2>
+ <ul>
+ <li><a href="telnet_echo.tac" shape="rect">telnet_echo.tac</a> - A telnet server which echoes data and events back to the client</li>
+ </ul>
+
+
+ <h2>twisted.conch.insults examples<a name="auto2"/></h2>
+ <ul>
+ <li><a href="demo.tac" shape="rect">demo.tac</a> - Nearly pointless demonstration of the manhole interactive interpreter</li>
+ <li><a href="demo_draw.tac" shape="rect">demo_draw.tac</a> - A trivial drawing application</li>
+ <li><a href="demo_insults.tac" shape="rect">demo_insults.tac</a> - Various simple terminal manipulations using the insults module</li>
+ <li><a href="demo_recvline.tac" shape="rect">demo_recvline.tac</a> - Demonstrates line-at-a-time handling with basic line-editing support</li>
+ <li><a href="demo_scroll.tac" shape="rect">demo_scroll.tac</a> - Simple echo-ish server that uses the scroll-region</li>
+ <li><a href="demo_manhole.tac" shape="rect">demo_manhole.tac</a> - An interactive Python interpreter with syntax coloring</li>
+ <li><a href="window.tac" shape="rect">window.tac</a> - An example of various widgets</li>
+ </ul>
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/examples/sshsimpleclient.py b/doc/conch/examples/sshsimpleclient.py
new file mode 100644
index 0000000..0f7738a
--- /dev/null
+++ b/doc/conch/examples/sshsimpleclient.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.conch.ssh import transport, userauth, connection, common, keys, channel
+from twisted.internet import defer, protocol, reactor
+from twisted.python import log
+import struct, sys, getpass, os
+
+USER = 'z3p' # replace this with a valid username
+HOST = 'localhost' # and a valid host
+
+class SimpleTransport(transport.SSHClientTransport):
+ def verifyHostKey(self, hostKey, fingerprint):
+ print 'host key fingerprint: %s' % fingerprint
+ return defer.succeed(1)
+
+ def connectionSecure(self):
+ self.requestService(
+ SimpleUserAuth(USER,
+ SimpleConnection()))
+
+class SimpleUserAuth(userauth.SSHUserAuthClient):
+ def getPassword(self):
+ return defer.succeed(getpass.getpass("%s@%s's password: " % (USER, HOST)))
+
+ def getGenericAnswers(self, name, instruction, questions):
+ print name
+ print instruction
+ answers = []
+ for prompt, echo in questions:
+ if echo:
+ answer = raw_input(prompt)
+ else:
+ answer = getpass.getpass(prompt)
+ answers.append(answer)
+ return defer.succeed(answers)
+
+ def getPublicKey(self):
+ path = os.path.expanduser('~/.ssh/id_dsa')
+ # this works with rsa too
+ # just change the name here and in getPrivateKey
+ if not os.path.exists(path) or self.lastPublicKey:
+ # the file doesn't exist, or we've tried a public key
+ return
+ return keys.Key.fromFile(filename=path+'.pub').blob()
+
+ def getPrivateKey(self):
+ path = os.path.expanduser('~/.ssh/id_dsa')
+ return defer.succeed(keys.Key.fromFile(path).keyObject)
+
+class SimpleConnection(connection.SSHConnection):
+ def serviceStarted(self):
+ self.openChannel(TrueChannel(2**16, 2**15, self))
+ self.openChannel(FalseChannel(2**16, 2**15, self))
+ self.openChannel(CatChannel(2**16, 2**15, self))
+
+class TrueChannel(channel.SSHChannel):
+ name = 'session' # needed for commands
+
+ def openFailed(self, reason):
+ print 'true failed', reason
+
+ def channelOpen(self, ignoredData):
+ self.conn.sendRequest(self, 'exec', common.NS('true'))
+
+ def request_exit_status(self, data):
+ status = struct.unpack('>L', data)[0]
+ print 'true status was: %s' % status
+ self.loseConnection()
+
+class FalseChannel(channel.SSHChannel):
+ name = 'session'
+
+ def openFailed(self, reason):
+ print 'false failed', reason
+
+ def channelOpen(self, ignoredData):
+ self.conn.sendRequest(self, 'exec', common.NS('false'))
+
+ def request_exit_status(self, data):
+ status = struct.unpack('>L', data)[0]
+ print 'false status was: %s' % status
+ self.loseConnection()
+
+class CatChannel(channel.SSHChannel):
+ name = 'session'
+
+ def openFailed(self, reason):
+ print 'echo failed', reason
+
+ def channelOpen(self, ignoredData):
+ self.data = ''
+ d = self.conn.sendRequest(self, 'exec', common.NS('cat'), wantReply = 1)
+ d.addCallback(self._cbRequest)
+
+ def _cbRequest(self, ignored):
+ self.write('hello conch\n')
+ self.conn.sendEOF(self)
+
+ def dataReceived(self, data):
+ self.data += data
+
+ def closed(self):
+ print 'got data from cat: %s' % repr(self.data)
+ self.loseConnection()
+ reactor.stop()
+
+protocol.ClientCreator(reactor, SimpleTransport).connectTCP(HOST, 22)
+reactor.run()
diff --git a/doc/conch/examples/sshsimpleserver.py b/doc/conch/examples/sshsimpleserver.py
new file mode 100755
index 0000000..5ebcee8
--- /dev/null
+++ b/doc/conch/examples/sshsimpleserver.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.cred import portal, checkers
+from twisted.conch import error, avatar
+from twisted.conch.checkers import SSHPublicKeyDatabase
+from twisted.conch.ssh import factory, userauth, connection, keys, session
+from twisted.internet import reactor, protocol, defer
+from twisted.python import log
+from zope.interface import implements
+import sys
+log.startLogging(sys.stderr)
+
+"""
+Example of running another protocol over an SSH channel.
+log in with username "user" and password "password".
+"""
+
+class ExampleAvatar(avatar.ConchUser):
+
+ def __init__(self, username):
+ avatar.ConchUser.__init__(self)
+ self.username = username
+ self.channelLookup.update({'session':session.SSHSession})
+
+class ExampleRealm:
+ implements(portal.IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return interfaces[0], ExampleAvatar(avatarId), lambda: None
+
+class EchoProtocol(protocol.Protocol):
+ """this is our example protocol that we will run over SSH
+ """
+ def dataReceived(self, data):
+ if data == '\r':
+ data = '\r\n'
+ elif data == '\x03': #^C
+ self.transport.loseConnection()
+ return
+ self.transport.write(data)
+
+publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV'
+
+privateKey = """-----BEGIN RSA PRIVATE KEY-----
+MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
+4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
+vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
+Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
+xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
+PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
+gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
+DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
+pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
+EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
+-----END RSA PRIVATE KEY-----"""
+
+
+class InMemoryPublicKeyChecker(SSHPublicKeyDatabase):
+
+ def checkKey(self, credentials):
+ return credentials.username == 'user' and \
+ keys.Key.fromString(data=publicKey).blob() == credentials.blob
+
+class ExampleSession:
+
+ def __init__(self, avatar):
+ """
+ We don't use it, but the adapter is passed the avatar as its first
+ argument.
+ """
+
+ def getPty(self, term, windowSize, attrs):
+ pass
+
+ def execCommand(self, proto, cmd):
+ raise Exception("no executing commands")
+
+ def openShell(self, trans):
+ ep = EchoProtocol()
+ ep.makeConnection(trans)
+ trans.makeConnection(session.wrapProtocol(ep))
+
+ def eofReceived(self):
+ pass
+
+ def closed(self):
+ pass
+
+from twisted.python import components
+components.registerAdapter(ExampleSession, ExampleAvatar, session.ISession)
+
+class ExampleFactory(factory.SSHFactory):
+ publicKeys = {
+ 'ssh-rsa': keys.Key.fromString(data=publicKey)
+ }
+ privateKeys = {
+ 'ssh-rsa': keys.Key.fromString(data=privateKey)
+ }
+ services = {
+ 'ssh-userauth': userauth.SSHUserAuthServer,
+ 'ssh-connection': connection.SSHConnection
+ }
+
+
+portal = portal.Portal(ExampleRealm())
+passwdDB = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+passwdDB.addUser('user', 'password')
+portal.registerChecker(passwdDB)
+portal.registerChecker(InMemoryPublicKeyChecker())
+ExampleFactory.portal = portal
+
+if __name__ == '__main__':
+ reactor.listenTCP(5022, ExampleFactory())
+ reactor.run()
diff --git a/doc/conch/examples/telnet_echo.tac b/doc/conch/examples/telnet_echo.tac
new file mode 100644
index 0000000..daa5b38
--- /dev/null
+++ b/doc/conch/examples/telnet_echo.tac
@@ -0,0 +1,47 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Simple echo server that echoes back client input.
+
+You can run this .tac file directly with:
+ twistd -ny telnet_echo.tac
+
+This demo sets up a listening port on 6023 which accepts telnet connections.
+No login for the telnet server is required.
+"""
+
+from twisted.conch.telnet import TelnetTransport, TelnetProtocol
+from twisted.internet.protocol import ServerFactory
+from twisted.application.internet import TCPServer
+from twisted.application.service import Application
+
+class TelnetEcho(TelnetProtocol):
+ def enableRemote(self, option):
+ self.transport.write("You tried to enable %r (I rejected it)\r\n" % (option,))
+ return False
+
+
+ def disableRemote(self, option):
+ self.transport.write("You disabled %r\r\n" % (option,))
+
+
+ def enableLocal(self, option):
+ self.transport.write("You tried to make me enable %r (I rejected it)\r\n" % (option,))
+ return False
+
+
+ def disableLocal(self, option):
+ self.transport.write("You asked me to disable %r\r\n" % (option,))
+
+
+ def dataReceived(self, data):
+ self.transport.write("I received %r from you\r\n" % (data,))
+
+
+factory = ServerFactory()
+factory.protocol = lambda: TelnetTransport(TelnetEcho)
+service = TCPServer(8023, factory)
+
+application = Application("Telnet Echo Server")
+service.setServiceParent(application)
diff --git a/doc/conch/examples/window.tac b/doc/conch/examples/window.tac
new file mode 100644
index 0000000..5946a48
--- /dev/null
+++ b/doc/conch/examples/window.tac
@@ -0,0 +1,202 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Widgets demo.
+
+You can run this .tac file directly with:
+ twistd -ny window.tac
+
+Demonstrates various widgets or buttons, such as scrollable regions,
+drawable canvas, etc.
+
+This demo sets up two listening ports: one on 6022 which accepts ssh
+connections; one on 6023 which accepts telnet connections. No login for the
+telnet server is required; for the ssh server, "username" is the username and
+"password" is the password.
+"""
+
+from __future__ import division
+
+import string, random
+
+from twisted.python import log
+from twisted.internet import protocol, task
+from twisted.application import internet, service
+from twisted.cred import checkers, portal
+
+from twisted.conch.insults import insults, window
+from twisted.conch.telnet import TelnetTransport, TelnetBootstrapProtocol
+from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
+
+from twisted.internet import reactor
+
+class DrawableCanvas(window.Canvas):
+ x = 0
+ y = 0
+
+ def func_LEFT_ARROW(self, modifier):
+ self.x -= 1
+ self.repaint()
+
+ def func_RIGHT_ARROW(self, modifier):
+ self.x += 1
+ self.repaint()
+
+ def func_UP_ARROW(self, modifier):
+ self.y -= 1
+ self.repaint()
+
+ def func_DOWN_ARROW(self, modifier):
+ self.y += 1
+ self.repaint()
+
+ def characterReceived(self, keyID, modifier):
+ self[self.x, self.y] = keyID
+ self.x += 1
+ self.repaint()
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == '\r' or keyID == '\v':
+ return
+ window.Canvas.keystrokeReceived(self, keyID, modifier)
+ if self.x >= self.width:
+ self.x = 0
+ elif self.x < 0:
+ self.x = self.width - 1
+
+ if self.y >= self.height:
+ self.y = 0
+ elif self.y < 0:
+ self.y = self.height - 1
+ self.repaint()
+
+ def render(self, width, height, terminal):
+ window.Canvas.render(self, width, height, terminal)
+ if self.focused:
+ terminal.cursorPosition(self.x, self.y)
+ window.cursor(terminal, self[self.x, self.y])
+
+
+class ButtonDemo(insults.TerminalProtocol):
+ width = 80
+ height = 24
+
+ def _draw(self):
+ self.window.draw(self.width, self.height, self.terminal)
+
+ def _redraw(self):
+ self.window.filthy()
+ self._draw()
+
+ def _schedule(self, f):
+ reactor.callLater(0, f)
+
+ def connectionMade(self):
+ self.terminal.eraseDisplay()
+ self.terminal.resetPrivateModes([insults.privateModes.CURSOR_MODE])
+
+ self.window = window.TopWindow(self._draw, self._schedule)
+ self.output = window.TextOutput((15, 1))
+ self.input = window.TextInput(15, self._setText)
+ self.select1 = window.Selection(map(str, range(100)), self._setText, 10)
+ self.select2 = window.Selection(map(str, range(200, 300)), self._setText, 10)
+ self.button = window.Button("Clear", self._clear)
+ self.canvas = DrawableCanvas()
+
+ hbox = window.HBox()
+ hbox.addChild(self.input)
+ hbox.addChild(self.output)
+ hbox.addChild(window.Border(self.button))
+ hbox.addChild(window.Border(self.select1))
+ hbox.addChild(window.Border(self.select2))
+
+ t1 = window.TextOutputArea(longLines=window.TextOutputArea.WRAP)
+ t2 = window.TextOutputArea(longLines=window.TextOutputArea.TRUNCATE)
+ t3 = window.TextOutputArea(longLines=window.TextOutputArea.TRUNCATE)
+ t4 = window.TextOutputArea(longLines=window.TextOutputArea.TRUNCATE)
+ for _t in t1, t2, t3, t4:
+ _t.setText((('This is a very long string. ' * 3) + '\n') * 3)
+
+ vp = window.Viewport(t3)
+ d = [1]
+ def spin():
+ vp.xOffset += d[0]
+ if vp.xOffset == 0 or vp.xOffset == 25:
+ d[0] *= -1
+ self.call = task.LoopingCall(spin)
+ self.call.start(0.25, now=False)
+ hbox.addChild(window.Border(vp))
+
+ vp2 = window.ScrolledArea(t4)
+ hbox.addChild(vp2)
+
+ texts = window.VBox()
+ texts.addChild(window.Border(t1))
+ texts.addChild(window.Border(t2))
+
+ areas = window.HBox()
+ areas.addChild(window.Border(self.canvas))
+ areas.addChild(texts)
+
+ vbox = window.VBox()
+ vbox.addChild(hbox)
+ vbox.addChild(areas)
+ self.window.addChild(vbox)
+ self.terminalSize(self.width, self.height)
+
+ def connectionLost(self, reason):
+ self.call.stop()
+ insults.TerminalProtocol.connectionLost(self, reason)
+
+ def terminalSize(self, width, height):
+ self.width = width
+ self.height = height
+ self.terminal.eraseDisplay()
+ self._redraw()
+
+
+ def keystrokeReceived(self, keyID, modifier):
+ self.window.keystrokeReceived(keyID, modifier)
+
+ def _clear(self):
+ self.canvas.clear()
+
+ def _setText(self, text):
+ self.input.setText('')
+ self.output.setText(text)
+
+
+def makeService(args):
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(username="password")
+
+ f = protocol.ServerFactory()
+ f.protocol = lambda: TelnetTransport(TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+ tsvc = internet.TCPServer(args['telnet'], f)
+
+ def chainProtocolFactory():
+ return insults.ServerProtocol(
+ args['protocolFactory'],
+ *args.get('protocolArgs', ()),
+ **args.get('protocolKwArgs', {}))
+
+ rlm = TerminalRealm()
+ rlm.chainedProtocolFactory = chainProtocolFactory
+ ptl = portal.Portal(rlm, [checker])
+ f = ConchFactory(ptl)
+ csvc = internet.TCPServer(args['ssh'], f)
+
+ m = service.MultiService()
+ tsvc.setServiceParent(m)
+ csvc.setServiceParent(m)
+ return m
+
+application = service.Application("Window Demo")
+
+makeService({'protocolFactory': ButtonDemo,
+ 'telnet': 6023,
+ 'ssh': 6022}).setServiceParent(application)
diff --git a/doc/conch/howto/conch_client.html b/doc/conch/howto/conch_client.html
new file mode 100644
index 0000000..cf88a1a
--- /dev/null
+++ b/doc/conch/howto/conch_client.html
@@ -0,0 +1,321 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Writing a client with Twisted Conch</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Writing a client with Twisted Conch</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Writing a client</a></li><li><a href="#auto2">The Transport</a></li><li><a href="#auto3">The Authorization Client</a></li><li><a href="#auto4">The Connection</a></li><li><a href="#auto5">The Channel</a></li><li><a href="#auto6">The main() function</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Introduction<a name="auto0"/></h2>
+
+<p>In the original days of computing, rsh/rlogin were used to connect to
+remote computers and execute commands. These commands had the problem
+that the passwords and commands were sent in the clear. To solve this
+problem, the SSH protocol was created. Twisted Conch implements the
+second version of this protocol.</p>
+
+ <h2>Writing a client<a name="auto1"/></h2>
+
+<p>Writing a client with Conch involves sub-classing 4 classes: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.ssh.transport.SSHClientTransport.html" title="twisted.conch.ssh.transport.SSHClientTransport">twisted.conch.ssh.transport.SSHClientTransport</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.ssh.userauth.SSHUserAuthClient.html" title="twisted.conch.ssh.userauth.SSHUserAuthClient">twisted.conch.ssh.userauth.SSHUserAuthClient</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.ssh.connection.SSHConnection.html" title="twisted.conch.ssh.connection.SSHConnection">twisted.conch.ssh.connection.SSHConnection</a></code>, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.ssh.channel.SSHChannel.html" title="twisted.conch.ssh.channel.SSHChannel">twisted.conch.ssh.channel.SSHChannel</a></code>. We'll start out
+with <code class="python">SSHClientTransport</code> because it's the base
+of the client.</p>
+
+<h2>The Transport<a name="auto2"/></h2>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">conch</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">error</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">conch</span>.<span class="py-src-variable">ssh</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">transport</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientTransport</span>(<span class="py-src-parameter">transport</span>.<span class="py-src-parameter">SSHClientTransport</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">verifyHostKey</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pubKey</span>, <span class="py-src-parameter">fingerprint</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">fingerprint</span> != <span class="py-src-string">'b1:94:6a:c9:24:92:d2:34:7c:62:35:b4:d2:61:11:84'</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">fail</span>(<span class="py-src-variable">error</span>.<span class="py-src-variable">ConchError</span>(<span class="py-src-string">'bad key'</span>))
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-number">1</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionSecure</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">requestService</span>(<span class="py-src-variable">ClientUserAuth</span>(<span class="py-src-string">'user'</span>, <span class="py-src-variable">ClientConnection</span>()))
+</pre>
+
+<p>See how easy it is? <code class="python">SSHClientTransport</code>
+handles the negotiation of encryption and the verification of keys
+for you. The one security element that you as a client writer need to
+implement is <code class="python">verifyHostKey()</code>. This method
+is called with two strings: the public key sent by the server and its
+fingerprint. You should verify the host key the server sends, either
+by checking against a hard-coded value as in the example, or by asking
+the user. <code class="python">verifyHostKey</code> returns a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">twisted.internet.defer.Deferred</a></code> which gets a callback
+if the host key is valid, or an errback if it is not. Note that in the
+above, replace 'user' with the username you're attempting to ssh with,
+for instance a call to <code class="python">os.getlogin()</code> for the
+current user.</p>
+
+<p>The second method you need to implement is <code class="python">connectionSecure()</code>. It is called when the
+encryption is set up and other services can be run. The example requests
+that the <code class="python">ClientUserAuth</code> service be started.
+This service will be discussed next.</p>
+
+<h2>The Authorization Client<a name="auto3"/></h2>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">conch</span>.<span class="py-src-variable">ssh</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">keys</span>, <span class="py-src-variable">userauth</span>
+
+<span class="py-src-comment"># these are the public/private keys from test_conch</span>
+
+<span class="py-src-variable">publicKey</span> = <span class="py-src-string">'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3\
+/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHR\
+ivcJSkbh/C+BR3utDS555mV'</span>
+
+<span class="py-src-variable">privateKey</span> = <span class="py-src-string">&quot;&quot;&quot;-----BEGIN RSA PRIVATE KEY-----
+MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
+4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
+vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
+Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
+xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
+PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
+gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
+DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
+pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
+EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
+-----END RSA PRIVATE KEY-----&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientUserAuth</span>(<span class="py-src-parameter">userauth</span>.<span class="py-src-parameter">SSHUserAuthClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getPassword</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">prompt</span> = <span class="py-src-parameter">None</span>):
+ <span class="py-src-keyword">return</span>
+ <span class="py-src-comment"># this says we won't do password authentication</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getPublicKey</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">keys</span>.<span class="py-src-variable">Key</span>.<span class="py-src-variable">fromString</span>(<span class="py-src-variable">data</span> = <span class="py-src-variable">publicKey</span>).<span class="py-src-variable">blob</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getPrivateKey</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">keys</span>.<span class="py-src-variable">Key</span>.<span class="py-src-variable">fromString</span>(<span class="py-src-variable">data</span> = <span class="py-src-variable">privateKey</span>).<span class="py-src-variable">keyObject</span>)
+</pre>
+
+<p>Again, fairly simple. The <code class="python">SSHUserAuthClient</code> takes care of most
+of the work, but the actual authentication data needs to be
+supplied. <code class="python">getPassword()</code> asks for a
+password, <code class="python">getPublicKey()</code> and <code class="python">getPrivateKey()</code> get public and private keys,
+respectively. <code class="python">getPassword()</code> returns
+a <code class="python">Deferred</code> that is called back with
+the password to use. <code class="python">getPublicKey()</code>
+returns the SSH key data for the public key to use. <code class="python">keys.Key.fromString()</code> will take
+a key in OpenSSH or LSH format as a string, and convert it to the
+required format. Alternatively, <code class="python">keys.Key.fromFile()</code> can be used instead, which
+will take the filename of a key in OpenSSH and LSH format, and
+convert it to the required format.
+<code class="python">getPrivateKey()</code>
+returns a <code class="python">Deferred</code> which is
+called back with the key object (as used in PyCrypto) for
+the private key. <code class="python">getPassword()</code>
+and <code class="python">getPrivateKey()</code> return <code class="python">Deferreds</code> because they may need to ask the user
+for input.</p>
+
+<p>Once the authentication is complete, <code class="python">SSHUserAuthClient</code> takes care of starting the code
+<code class="python">SSHConnection</code> object given to it. Next, we'll
+look at how to use the <code class="python">SSHConnection</code></p>
+
+<h2>The Connection<a name="auto4"/></h2>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">conch</span>.<span class="py-src-variable">ssh</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">connection</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientConnection</span>(<span class="py-src-parameter">connection</span>.<span class="py-src-parameter">SSHConnection</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">serviceStarted</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">openChannel</span>(<span class="py-src-variable">CatChannel</span>(<span class="py-src-variable">conn</span> = <span class="py-src-variable">self</span>))
+</pre>
+
+<p><code class="python">SSHConnection</code> is the easiest,
+as it's only responsible for starting the channels. It has
+other methods, those will be examined when we look at <code class="python">SSHChannel</code>.</p>
+
+<h2>The Channel<a name="auto5"/></h2>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">conch</span>.<span class="py-src-variable">ssh</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">channel</span>, <span class="py-src-variable">common</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CatChannel</span>(<span class="py-src-parameter">channel</span>.<span class="py-src-parameter">SSHChannel</span>):
+
+ <span class="py-src-variable">name</span> = <span class="py-src-string">'session'</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">channelOpen</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">conn</span>.<span class="py-src-variable">sendRequest</span>(<span class="py-src-variable">self</span>, <span class="py-src-string">'exec'</span>, <span class="py-src-variable">common</span>.<span class="py-src-variable">NS</span>(<span class="py-src-string">'cat'</span>),
+ <span class="py-src-variable">wantReply</span> = <span class="py-src-number">1</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cbSendRequest</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">catData</span> = <span class="py-src-string">''</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cbSendRequest</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">'This data will be echoed back to us by &quot;cat.&quot;\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">conn</span>.<span class="py-src-variable">sendEOF</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">loseConnection</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">catData</span> += <span class="py-src-variable">data</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">closed</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'We got this from &quot;cat&quot;:'</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">catData</span>
+</pre>
+
+<p>Now that we've spent all this time getting the server and
+client connected, here is where that work pays off. <code class="python">SSHChannel</code> is the interface between you and the
+other side. This particular channel opens a session and plays with the
+'cat' program, but your channel can implement anything, so long as the
+server supports it.</p>
+
+<p>The <code class="python">channelOpen()</code> method is
+where everything gets started. It gets passed a chunk of data;
+however, this chunk is usually nothing and can be ignored.
+Our <code class="python">channelOpen()</code> initializes our
+channel, and sends a request to the other side, using the
+<code class="python">sendRequest()</code> method of the <code class="python">SSHConnection</code> object. Requests are used to send
+events to the other side. We pass the method self so that it knows to
+send the request for this channel. The 2nd argument of 'exec' tells the
+server that we want to execute a command. The third argument is the data
+that accompanies the request.
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.ssh.common.NS.html" title="twisted.conch.ssh.common.NS">common.NS</a></code> encodes
+the data as a length-prefixed string, which is how the server expects
+the data. We also say that we want a reply saying that the process has a
+been started. <code class="python">sendRequest()</code> then returns a
+<code class="python">Deferred</code> which we add a callback for.</p>
+
+<p>Once the callback fires, we send the data. <code class="python">SSHChannel</code> supports the
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITransport.html" title="twisted.internet.interfaces.ITransport">twisted.internet.interfaces.ITransport</a></code>
+interface, so
+it can be given to Protocols to run them over the secure
+connection. In our case, we just write the data directly. <code class="python">sendEOF()</code> does not follow the interface,
+but Conch uses it to tell the other side that we will write no
+more data. <code class="python">loseConnection()</code> shuts
+down our side of the connection, but we will still receive data
+through <code class="python">dataReceived()</code>. The <code class="python">closed()</code> method is called when both sides of the
+connection are closed, and we use it to display the data we received
+(which should be the same as the data we sent.)</p>
+
+<p>Finally, let's actually invoke the code we've set up.</p>
+
+<h2>The main() function<a name="auto6"/></h2>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ClientFactory</span>()
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">ClientTransport</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">'localhost'</span>, <span class="py-src-number">22</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre>
+
+<P>We call <code class="python">connectTCP()</code> to connect to
+localhost, port 22 (the standard port for ssh), and pass it an instance
+of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.html" title="twisted.internet.protocol.ClientFactory">twisted.internet.protocol.ClientFactory</a></code>.
+This instance has the attribute <code class="python">protocol</code>
+set to our earlier <code class="python">ClientTransport</code>
+class. Note that the protocol attribute is set to the class <code class="python">ClientTransport</code>, not an instance of
+<code class="python">ClientTransport</code>! When the <code class="python">connectTCP</code> call completes, the protocol will be
+called to create a <code class="python">ClientTransport()</code> object
+- this then invokes all our previous work.</P>
+
+<P>It's worth noting that in the example <code class="python">main()</code>
+routine, the <code class="python">reactor.run()</code> call never returns.
+If you want to make the program exit, call
+<code class="python">reactor.stop()</code> in the earlier
+<code class="python">closed()</code> method.</P>
+
+<P>If you wish to observe the interactions in more detail, adding a call
+to <code class="python">log.startLogging(sys.stdout, setStdout=0)</code>
+before the <code class="python">reactor.run()</code> call will send all
+logging to stdout.</P>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/howto/index.html b/doc/conch/howto/index.html
new file mode 100644
index 0000000..0782a48
--- /dev/null
+++ b/doc/conch/howto/index.html
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+ <li>Tutorial
+ <ul>
+ <li>
+ <a href="conch_client.html" shape="rect">Writing an SSH client with Conch</a>
+ </li>
+ </ul>
+ </li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/index.html b/doc/conch/index.html
new file mode 100644
index 0000000..9416896
--- /dev/null
+++ b/doc/conch/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Conch Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Conch Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Conch to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Conch</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/man/cftp-man.html b/doc/conch/man/cftp-man.html
new file mode 100644
index 0000000..5c20c00
--- /dev/null
+++ b/doc/conch/man/cftp-man.html
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: CFTP.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">CFTP.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>cftp </p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p>cftp [<strong>-B</strong><em> buffer_size</em>][<strong>-b</strong><em> command_file</em>][<strong>-R</strong><em> num_requests</em>][<strong>-s</strong><em> subsystem</em>]</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>cftp is a client for logging into a remote machine and executing commands to send and receive file information. It can wrap a number of file transfer subsystems
+</p>
+
+<p>The options are as follows:
+<dl><dt><strong>-B</strong></dt><dd>Specifies the default size of the buffer to use for sending and receiving. (Default value: 32768 bytes.)
+</dd><dt><strong>-b</strong></dt><dd>File to read commands from, '-' for stdin. (Default value: interactive/stdin.)
+</dd><dt><strong>-R</strong></dt><dd>Number of requests to make before waiting for a reply.
+</dd><dt><strong>-s</strong></dt><dd>Subsystem/server program to connect to.
+</dd></dl>
+
+</p>
+
+<p>The following commands are recognised by
+cftp :
+<dl><dt>cd <u>path</u></dt><dd>Change the remote directory to 'path'.
+</dd><dt>chgrp <u>gid</u> <u>path</u></dt><dd>Change the gid of 'path' to 'gid'.
+</dd><dt>chmod <u>mode</u> <u>path</u></dt><dd>Change mode of 'path' to 'mode'.
+</dd><dt>chown <u>uid</u> <u>path</u></dt><dd>Change uid of 'path' to 'uid'.
+</dd><dt>exit</dt><dd>Disconnect from the server.
+</dd><dt>get <u>remote-path</u> [<u>local-path</u>]</dt><dd>Get remote file and optionally store it at specified local path.
+</dd><dt>help</dt><dd>Get a list of available commands.
+</dd><dt>lcd <u>path</u></dt><dd>Change local directory to 'path'.
+</dd><dt>lls [<u>ls-options</u>] [<u>path</u>]</dt><dd>Display local directory listing.
+</dd><dt>lmkdir <u>path</u></dt><dd>Create local directory.
+</dd><dt>ln <u>linkpath</u> <u>targetpath</u></dt><dd>Symlink remote file.
+</dd><dt>lpwd</dt><dd>Print the local working directory.
+</dd><dt>ls [<u>-l</u>] [<u>path</u>]</dt><dd>Display remote directory listing.
+</dd><dt>mkdir <u>path</u></dt><dd>Create remote directory.
+</dd><dt>progress</dt><dd>Toggle progress bar.
+</dd><dt>put <u>local-path</u> [<u>remote-path</u>]</dt><dd>Transfer local file to remote location
+</dd><dt>pwd</dt><dd>Print the remote working directory.
+</dd><dt>quit</dt><dd>Disconnect from the server.
+</dd><dt>rename <u>oldpath</u> <u>newpath</u></dt><dd>Rename remote file.
+</dd><dt>rmdir <u>path</u></dt><dd>Remove remote directory.
+</dd><dt>rm <u>path</u></dt><dd>Remove remote file.
+</dd><dt>version</dt><dd>Print the SFTP version.
+</dd><dt>?</dt><dd>Synonym for 'help'.
+</dd></dl>
+
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>cftp by Paul Swartz &lt;z3p@twistedmatrix.com&gt;. Man page by Mary Gardiner &lt;mary@twistedmatrix.com&gt;.
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>Report bugs to <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2005-2008 Twisted Matrix Laboratories
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/man/cftp.1 b/doc/conch/man/cftp.1
new file mode 100644
index 0000000..7eae889
--- /dev/null
+++ b/doc/conch/man/cftp.1
@@ -0,0 +1,89 @@
+.Dd October 8, 2005
+.Dt CFTP 1
+.Os
+.Sh NAME
+.Nm cftp
+.Nd Conch command-line SFTP client
+.Sh SYNOPSIS
+.Nm cftp
+.Op Fl B Ar buffer_size
+.Op Fl b Ar command_file
+.Op Fl R Ar num_requests
+.Op Fl s Ar subsystem
+.Os
+.Sh DESCRIPTION
+.Nm
+is a client for logging into a remote machine and executing commands to send and receive file information. It can wrap a number of file transfer subsystems
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl B
+Specifies the default size of the buffer to use for sending and receiving. (Default value: 32768 bytes.)
+.It Fl b
+File to read commands from, '-' for stdin. (Default value: interactive/stdin.)
+.It Fl R
+Number of requests to make before waiting for a reply.
+.It Fl s
+Subsystem/server program to connect to.
+.El
+.Pp
+The following commands are recognised by
+.Nm
+:
+.Bl -tag -width Ds
+.It Ic cd Ar path
+Change the remote directory to 'path'.
+.It Ic chgrp Ar gid Ar path
+Change the gid of 'path' to 'gid'.
+.It Ic chmod Ar mode Ar path
+Change mode of 'path' to 'mode'.
+.It Ic chown Ar uid Ar path
+Change uid of 'path' to 'uid'.
+.It Ic exit
+Disconnect from the server.
+.It Ic get Ar remote-path Op Ar local-path
+Get remote file and optionally store it at specified local path.
+.It Ic help
+Get a list of available commands.
+.It Ic lcd Ar path
+Change local directory to 'path'.
+.It Ic lls Op Ar ls-options Op Ar path
+Display local directory listing.
+.It Ic lmkdir Ar path
+Create local directory.
+.It Ic ln Ar linkpath Ar targetpath
+Symlink remote file.
+.It Ic lpwd
+Print the local working directory.
+.It Ic ls Op Ar -l Op Ar path
+Display remote directory listing.
+.It Ic mkdir Ar path
+Create remote directory.
+.It Ic progress
+Toggle progress bar.
+.It Ic put Ar local-path Op Ar remote-path
+Transfer local file to remote location
+.It Ic pwd
+Print the remote working directory.
+.It Ic quit
+Disconnect from the server.
+.It Ic rename Ar oldpath Ar newpath
+Rename remote file.
+.It Ic rmdir Ar path
+Remove remote directory.
+.It Ic rm Ar path
+Remove remote file.
+.It Ic version
+Print the SFTP version.
+.It Ic ?
+Synonym for 'help'.
+.El
+.Sh AUTHOR
+cftp by Paul Swartz <z3p@twistedmatrix.com>. Man page by Mary Gardiner <mary@twistedmatrix.com>.
+.Sh "REPORTING BUGS"
+Report bugs to \fIhttp://twistedmatrix.com/bugs/\fR
+.Sh COPYRIGHT
+Copyright \(co 2005-2008 Twisted Matrix Laboratories
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/conch/man/ckeygen-man.html b/doc/conch/man/ckeygen-man.html
new file mode 100644
index 0000000..e16fe32
--- /dev/null
+++ b/doc/conch/man/ckeygen-man.html
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: CKEYGEN.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">CKEYGEN.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">DESCRIPTION</a></li><li><a href="#auto4">AUTHOR</a></li><li><a href="#auto5">REPORTING BUGS</a></li><li><a href="#auto6">COPYRIGHT</a></li><li><a href="#auto7">SEE ALSO</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>ckeygen - manipulate public/private keys
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>ckeygen</strong> [-b <em>bits</em>] [-f <em>filename</em>] [-t <em>type</em>]<strong>[-C</strong> <em>comment</em>] [-N <em>new passphrase</em>] [-P <em>old passphrase</em>]<strong>[-l]</strong> [-p] [-q] [-y]</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>The <strong>--help</strong> prints out a usage message to standard output.
+<dl><dt><strong>-b</strong>, <strong>--bits</strong> &lt;bits&gt;
+</dt><dd>Number of bits in the key to create (default: 1024)
+</dd>
+
+<dt><strong>-f</strong>, <strong>--filename</strong> &lt;file name&gt;
+</dt><dd>Filename of the key file.
+</dd>
+
+<dt><strong>-t</strong>, <strong>--type</strong> &lt;type&gt;
+</dt><dd>Type of key (rsa or dsa).
+</dd>
+
+<dt><strong>-C</strong>, <strong>--comment</strong> &lt;comment&gt;
+</dt><dd>Provide a new comment.
+</dd>
+
+<dt><strong>-N</strong>, <strong>--newpass</strong> &lt;pass phrase&gt;
+</dt><dd>Provide new passphrase.
+</dd>
+
+<dt><strong>-P</strong>, <strong>--pass</strong> &lt;pass phrase&gt;
+</dt><dd>Provide old passphrase.
+</dd>
+
+<dt><strong>-l</strong>, <strong>--fingerprint</strong>
+</dt><dd>Show fingerprint of key file.
+</dd>
+
+<dt><strong>-p</strong>, <strong>--changepass</strong>
+</dt><dd>Change passphrase of private key file.
+</dd>
+
+<dt><strong>-q</strong>, <strong>--quiet</strong>
+</dt><dd>Be quiet.
+</dd>
+
+<dt><strong>-y</strong>, <strong>--showpub</strong>
+</dt><dd>Read private key file and print public key.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Display version number only.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>DESCRIPTION<a name="auto3"/></h2>
+
+<p>Manipulate public/private keys in various ways.
+If no filename is given, a file name will be requested interactively.
+</p>
+
+<h2>AUTHOR<a name="auto4"/></h2>
+
+<p>Written by Moshe Zadka, based on ckeygen's help messages
+</p>
+
+<h2>REPORTING BUGS<a name="auto5"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto6"/></h2>
+
+<p>Copyright © 2002-2011 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+<h2>SEE ALSO<a name="auto7"/></h2>
+
+<p>ssh(1), conch(1)
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/man/ckeygen.1 b/doc/conch/man/ckeygen.1
new file mode 100644
index 0000000..04d720f
--- /dev/null
+++ b/doc/conch/man/ckeygen.1
@@ -0,0 +1,57 @@
+.TH CKEYGEN "1" "October 2002" "" ""
+.SH NAME
+ckeygen \- manipulate public/private keys
+.SH SYNOPSIS
+.B ckeygen [-b \fIbits\fR] [-f \fIfilename\fR] [-t \fItype\fR]
+.B [-C \fIcomment\fR] [-N \fInew passphrase\fR] [-P \fIold passphrase\fR]
+.B [-l] [-p] [-q] [-y]
+.SH DESCRIPTION
+.PP
+The \fB\--help\fR prints out a usage message to standard output.
+.TP
+\fB-b\fR, \fB--bits\fR <bits>
+Number of bits in the key to create (default: 1024)
+.TP
+\fB-f\fR, \fB--filename\fR <file name>
+Filename of the key file.
+.TP
+\fB-t\fR, \fB--type\fR <type>
+Type of key (rsa or dsa).
+.TP
+\fB-C\fR, \fB--comment\fR <comment>
+Provide a new comment.
+.TP
+\fB-N\fR, \fB--newpass\fR <pass phrase>
+Provide new passphrase.
+.TP
+\fB-P\fR, \fB--pass\fR <pass phrase>
+Provide old passphrase.
+.TP
+\fB-l\fR, \fB--fingerprint\fR
+Show fingerprint of key file.
+.TP
+\fB-p\fR, \fB--changepass\fR
+Change passphrase of private key file.
+.TP
+\fB-q\fR, \fB--quiet\fR
+Be quiet.
+.TP
+\fB-y\fR, \fB--showpub\fR
+Read private key file and print public key.
+.TP
+\fB--version\fR
+Display version number only.
+.SH DESCRIPTION
+Manipulate public/private keys in various ways.
+If no filename is given, a file name will be requested interactively.
+.SH AUTHOR
+Written by Moshe Zadka, based on ckeygen's help messages
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2002-2011 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+ssh(1), conch(1)
diff --git a/doc/conch/man/conch-man.html b/doc/conch/man/conch-man.html
new file mode 100644
index 0000000..abf5a98
--- /dev/null
+++ b/doc/conch/man/conch-man.html
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: CONCH.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">CONCH.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li><li><a href="#auto6">SEE ALSO</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>conch </p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p>conch [<strong>-AaCfINnrsTtVvx</strong>][<strong>-c</strong><em> cipher_spec</em>][<strong>-e</strong><em> escape_char</em>][<strong>-i</strong><em> identity_file</em>][<strong>-K</strong><em> connection_spec</em>][<strong>-L</strong><em> port</em>:<em> host</em>:<em> hostport</em>][<strong>-l</strong><em> user</em>][<strong>-m</strong><em> mac_spec</em>][<strong>-o</strong><em> openssh_option</em>][<strong>-p</strong><em> port</em>][<strong>-R</strong><em> port</em>:<em> host</em>:<em> hostport</em>][<em> user</em>@]<em> hostname</em>[<em> command</em>]</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>conch is a SSHv2 client for logging into a remote machine and executing commands. It provides encrypted and secure communications across a possibly insecure network. Arbitrary TCP/IP ports can also be forwarded over the secure connection.
+</p>
+
+<p>conch connects and logs into
+<em> hostname</em>(as
+<em> user</em>or the current username). The user must prove her/his identity through a public-key or a password. Alternatively, if a connection is already open to a server, a new shell can be opened over the connection without having to reauthenticate.
+</p>
+
+<p>If
+<em> command</em>is specified,
+<em> command</em>is executed instead of a shell. If the
+<strong>-s</strong>option is given,
+<em> command</em>is treated as an SSHv2 subsystem name.
+Conch supports the public-key, keyboard-interactive, and password authentications.
+</p>
+
+<p>The public-key method allows the RSA or DSA algorithm to be used. The client uses his/her private key,
+or
+to sign the session identifier, known only by the client and server. The server checks that the matching public key is valid for the user, and that the signature is correct.
+</p>
+
+<p>If public-key authentication fails,
+conch can authenticate by sending an encrypted password over the connection.
+conch has the ability to multiplex multiple shells, commands and TCP/IP ports over the same secure connection. To disable multiplexing for a connection, use the
+<strong>-I</strong>flag.
+</p>
+
+<p>The
+<strong>-K</strong>option determines how the client connects to the remote host. It is a comma-separated list of the methods to use, in order of preference. The two connection methods are
+(for connecting over a multiplexed connection) and
+(to connect directly).
+To disable connecting over a multiplexed connection, do not include
+in the preference list.
+</p>
+
+<p>As an example of how connection sharing works, to speed up CVS over SSH:
+</p>
+
+<p>conch --noshell --fork -l cvs_user cvs_host
+set CVS_RSH=<strong>conch</strong>
+</p>
+
+<p>Now, when CVS connects to cvs_host as cvs_user, instead of making a new connection to the server,
+conch will add a new channel to the existing connection. This saves the cost of repeatedly negotiating the cryptography and authentication.
+</p>
+
+<p>The options are as follows:
+<dl><dt><strong>-A</strong></dt><dd>Enables authentication agent forwarding.
+</dd><dt><strong>-a</strong></dt><dd>Disables authentication agent forwarding (default).
+</dd><dt><strong>-C</strong></dt><dd>Enable compression.
+</dd><dt><strong>-c</strong></dt><dd><em> cipher_spec</em>Selects encryption algorithms to be used for this connection, as a comma-separated list of ciphers in order of preference. The list that
+conch supports is (in order of default preference): aes256-ctr, aes256-cbc, aes192-ctr, aes192-cbc, aes128-ctr, aes128-cbc, cast128-ctr, cast128-cbc, blowfish-ctr, blowfish, idea-ctr, idea-cbc, 3des-ctr, 3des-cbc.
+</dd><dt><strong>-e</strong></dt><dd><em> ch</em>| ^ch | noneSets the escape character for sessions with a PTY (default:
+The escape character is only recognized at the beginning of a line (after a newline).
+The escape character followed by a dot
+closes the connection;
+followed by ^Z suspends the connection;
+and followed by the escape character sends the escape character once.
+Setting the character to
+disables any escapes.
+</dd><dt><strong>-f</strong></dt><dd>Fork to background after authentication.
+</dd><dt><strong>-I</strong></dt><dd>Do not allow connection sharing over this connection.
+</dd><dt><strong>-i</strong></dt><dd><em> identity_spec</em>The file from which the identity (private key) for RSA or DSA authentication is read.
+The defaults are
+and
+It is possible to use this option more than once to use more than one private key.
+</dd><dt><strong>-K</strong></dt><dd><em> connection_spec</em>Selects methods for connection to the server, as a comma-separated list of methods in order of preference. See
+for more information.
+</dd><dt><strong>-L</strong></dt><dd><em> port</em>: host : hostportSpecifies that the given port on the client host is to be forwarded to the given host and port on the remote side. This allocates a socket to listen to
+<em> port</em>on the local side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to
+<em> host</em>port
+<em> hostport</em>from the remote machine.
+Only root can forward privieged ports.
+</dd><dt><strong>-l</strong></dt><dd><em> user</em>Log in using this username.
+</dd><dt><strong>-m</strong></dt><dd><em> mac_spec</em>Selects MAC (message authentication code) algorithms, as a comma-separated list in order of preference. The list that
+conch supports is (in order of preference): hmac-sha1, hmac-md5.
+</dd><dt><strong>-N</strong></dt><dd>Do not execute a shell or command.
+</dd><dt><strong>-n</strong></dt><dd>Redirect input from /dev/null.
+</dd><dt><strong>-o</strong></dt><dd><em> openssh_option</em>Ignored OpenSSH options.
+</dd><dt><strong>-p</strong></dt><dd><em> port</em>The port to connect to on the server.
+</dd><dt><strong>-R</strong></dt><dd><em> port</em>: host : hostportSpecifies that the given port on the remote host is to be forwarded to the given host and port on the local side. This allocates a socket to listen to
+<em> port</em>on the remote side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to
+<em> host</em>port
+<em> hostport</em>from the client host.
+Only root can forward privieged ports.
+</dd><dt><strong>-s</strong></dt><dd>Reconnect to the server if the connection is lost.
+</dd><dt><strong>-s</strong></dt><dd>Invoke
+<em> command</em>(mandatory) as a SSHv2 subsystem.
+</dd><dt><strong>-T</strong></dt><dd>Do not allocate a TTY.
+</dd><dt><strong>-t</strong></dt><dd>Allocate a TTY even if command is given.
+</dd><dt><strong>-V</strong></dt><dd>Display version number only.
+</dd><dt><strong>-v</strong></dt><dd>Log to stderr.
+</dd><dt><strong>-x</strong></dt><dd>Disable X11 connection forwarding (default).
+</dd></dl>
+
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>Written by Paul Swartz &lt;z3p@twistedmatrix.com&gt;.
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2002-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+<h2>SEE ALSO<a name="auto6"/></h2>
+
+<p>ssh(1)
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/man/conch.1 b/doc/conch/man/conch.1
new file mode 100644
index 0000000..7ba9bff
--- /dev/null
+++ b/doc/conch/man/conch.1
@@ -0,0 +1,206 @@
+.Dd May 22, 2004
+.Dt CONCH 1
+.Os
+.Sh NAME
+.Nm conch
+.Nd Conch SSH client
+.Sh SYNOPSIS
+.Nm conch
+.Op Fl AaCfINnrsTtVvx
+.Op Fl c Ar cipher_spec
+.Op Fl e Ar escape_char
+.Op Fl i Ar identity_file
+.Op Fl K Ar connection_spec
+.Bk -words
+.Oo Fl L Xo
+.Sm off
+.Ar port :
+.Ar host :
+.Ar hostport
+.Sm on
+.Xc
+.Oc
+.Ek
+.Op Fl l Ar user
+.Op Fl m Ar mac_spec
+.Op Fl o Ar openssh_option
+.Op Fl p Ar port
+.Bk -words
+.Oo Fl R Xo
+.Sm off
+.Ar port :
+.Ar host :
+.Ar hostport
+.Sm on
+.Xc
+.Oc
+.Ek
+.Oo Ar user Ns @ Ns Oc Ar hostname
+.Op Ar command
+.Sh DESCRIPTION
+.Nm
+is a SSHv2 client for logging into a remote machine and executing commands. It provides encrypted and secure communications across a possibly insecure network. Arbitrary TCP/IP ports can also be forwarded over the secure connection.
+.Pp
+.Nm
+connects and logs into
+.Ar hostname
+(as
+.Ar user
+or the current username). The user must prove her/his identity through a public\-key or a password. Alternatively, if a connection is already open to a server, a new shell can be opened over the connection without having to reauthenticate.
+.Pp
+If
+.Ar command
+is specified,
+.Ar command
+is executed instead of a shell. If the
+.Fl s
+option is given,
+.Ar command
+is treated as an SSHv2 subsystem name.
+.Ss Authentication
+Conch supports the public-key, keyboard-interactive, and password authentications.
+.Pp
+The public-key method allows the RSA or DSA algorithm to be used. The client uses his/her private key,
+.Pa $HOME/.ssh/id_rsa
+or
+.Pa $HOME/.ssh/id_dsa
+to sign the session identifier, known only by the client and server. The server checks that the matching public key is valid for the user, and that the signature is correct.
+.Pp
+If public-key authentication fails,
+.Nm
+can authenticate by sending an encrypted password over the connection.
+.Ss Connection sharing
+.Nm
+has the ability to multiplex multiple shells, commands and TCP/IP ports over the same secure connection. To disable multiplexing for a connection, use the
+.Fl I
+flag.
+.Pp
+The
+.Fl K
+option determines how the client connects to the remote host. It is a comma-separated list of the methods to use, in order of preference. The two connection methods are
+.Ql unix
+(for connecting over a multiplexed connection) and
+.Ql direct
+(to connect directly).
+To disable connecting over a multiplexed connection, do not include
+.Ql unix
+in the preference list.
+.Pp
+As an example of how connection sharing works, to speed up CVS over SSH:
+.Pp
+.Nm
+--noshell --fork -l cvs_user cvs_host
+.br
+set CVS_RSH=\fBconch\fR
+.Pp
+Now, when CVS connects to cvs_host as cvs_user, instead of making a new connection to the server,
+.Nm
+will add a new channel to the existing connection. This saves the cost of repeatedly negotiating the cryptography and authentication.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl A
+Enables authentication agent forwarding.
+.It Fl a
+Disables authentication agent forwarding (default).
+.It Fl C
+Enable compression.
+.It Fl c Ar cipher_spec
+Selects encryption algorithms to be used for this connection, as a comma-separated list of ciphers in order of preference. The list that
+.Nm
+supports is (in order of default preference): aes256-ctr, aes256-cbc, aes192-ctr, aes192-cbc, aes128-ctr, aes128-cbc, cast128-ctr, cast128-cbc, blowfish-ctr, blowfish, idea-ctr, idea-cbc, 3des-ctr, 3des-cbc.
+.It Fl e Ar ch | ^ch | none
+Sets the escape character for sessions with a PTY (default:
+.Ql ~ ) .
+The escape character is only recognized at the beginning of a line (after a newline).
+The escape character followed by a dot
+.Pq Ql \&.
+closes the connection;
+followed by ^Z suspends the connection;
+and followed by the escape character sends the escape character once.
+Setting the character to
+.Dq none
+disables any escapes.
+.It Fl f
+Fork to background after authentication.
+.It Fl I
+Do not allow connection sharing over this connection.
+.It Fl i Ar identity_spec
+The file from which the identity (private key) for RSA or DSA authentication is read.
+The defaults are
+.Pa $HOME/.ssh/id_rsa
+and
+.Pa $HOME/.ssh/id_dsa .
+It is possible to use this option more than once to use more than one private key.
+.It Fl K Ar connection_spec
+Selects methods for connection to the server, as a comma-separated list of methods in order of preference. See
+.Cm Connection sharing
+for more information.
+.It Fl L Xo
+.Sm off
+.Ar port : host : hostport
+.Sm on
+.Xc
+Specifies that the given port on the client host is to be forwarded to the given host and port on the remote side. This allocates a socket to listen to
+.Ar port
+on the local side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to
+.Ar host
+port
+.Ar hostport
+from the remote machine.
+Only root can forward privieged ports.
+.It Fl l Ar user
+Log in using this username.
+.It Fl m Ar mac_spec
+Selects MAC (message authentication code) algorithms, as a comma-separated list in order of preference. The list that
+.Nm
+supports is (in order of preference): hmac-sha1, hmac-md5.
+.It Fl N
+Do not execute a shell or command.
+.It Fl n
+Redirect input from /dev/null.
+.It Fl o Ar openssh_option
+Ignored OpenSSH options.
+.It Fl p Ar port
+The port to connect to on the server.
+.It Fl R Xo
+.Sm off
+.Ar port : host : hostport
+.Sm on
+.Xc
+Specifies that the given port on the remote host is to be forwarded to the given host and port on the local side. This allocates a socket to listen to
+.Ar port
+on the remote side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to
+.Ar host
+port
+.Ar hostport
+from the client host.
+Only root can forward privieged ports.
+.It Fl s
+Reconnect to the server if the connection is lost.
+.It Fl s
+Invoke
+.Ar command
+(mandatory) as a SSHv2 subsystem.
+.It Fl T
+Do not allocate a TTY.
+.It Fl t
+Allocate a TTY even if command is given.
+.It Fl V
+Display version number only.
+.It Fl v
+Log to stderr.
+.It Fl x
+Disable X11 connection forwarding (default).
+.El
+.Sh AUTHOR
+Written by Paul Swartz <z3p@twistedmatrix.com>.
+.Sh "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.Sh COPYRIGHT
+Copyright \(co 2002-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.Sh SEE ALSO
+ssh(1)
diff --git a/doc/conch/man/tkconch-man.html b/doc/conch/man/tkconch-man.html
new file mode 100644
index 0000000..41ae30a
--- /dev/null
+++ b/doc/conch/man/tkconch-man.html
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: CONCH.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">CONCH.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">DESCRIPTION</a></li><li><a href="#auto4">AUTHOR</a></li><li><a href="#auto5">REPORTING BUGS</a></li><li><a href="#auto6">COPYRIGHT</a></li><li><a href="#auto7">SEE ALSO</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>tkconch - connect to SSH servers graphically
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>conch</strong> [-l <em>user</em>] [-i <em>identity</em> [ -i <em>identity</em> ... ]] [-c <em>cipher</em>] [-m <em>MAC</em>] [-p <em>port</em>] [-n] [-t] [-T] [-V] [-C] [-N] [-s] [arg [...]]</p>
+
+<p><strong>conch</strong> --help</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>The <strong>--help</strong> prints out a usage message to standard output.
+<dl><dt><strong>-l</strong>, <strong>--user</strong> &lt;user&gt;
+</dt><dd>Log in using this user name.
+</dd>
+
+<dt><strong>-e</strong>, <strong>--escape</strong> &lt;escape character&gt;
+</dt><dd>Set escape character; 'none' = disable (default: ~)
+</dd>
+
+<dt><strong>-i</strong>, <strong>--identity</strong> &lt;identity&gt;
+</dt><dd>Add an identity file for public key authentication (default: ~/.ssh/identity)
+</dd>
+
+<dt><strong>-c</strong>, <strong>--cipher</strong> &lt;cipher&gt;
+</dt><dd>Cipher algorithm to use.
+</dd>
+
+<dt><strong>-m</strong>, <strong>--macs</strong> &lt;mac&gt;
+</dt><dd>Specify MAC algorithms for protocol version 2.
+</dd>
+
+<dt><strong>-p</strong>, <strong>--port</strong> &lt;port&gt;
+</dt><dd>Port to connect to.
+</dd>
+
+<dt><strong>-L</strong>, <strong>--localforward</strong> &lt;listen-port:host:port&gt;
+</dt><dd>Forward local port to remote address.
+</dd>
+
+<dt><strong>-R</strong>, <strong>--remoteforward</strong> &lt;listen-port:host:port&gt;
+</dt><dd>Forward remote port to local address.
+</dd>
+
+<dt><strong>-t</strong>, <strong>--tty</strong>
+</dt><dd>Allocate a tty even if command is given.
+</dd>
+
+<dt><strong>-n</strong>, <strong>--notty</strong>
+</dt><dd>Do not allocate a tty.
+</dd>
+
+<dt><strong>-V</strong>, <strong>--version</strong>
+</dt><dd>Display version number only.
+</dd>
+
+<dt><strong>-C</strong>, <strong>--compress</strong>
+</dt><dd>Enable compression.
+</dd>
+
+<dt><strong>-a</strong>, <strong>--ansilog</strong>
+</dt><dd>Print the received data to stdout.
+</dd>
+
+<dt><strong>-N</strong>, <strong>--noshell</strong>
+</dt><dd>Do not execute a shell or command.
+</dd>
+
+<dt><strong>-s</strong>, <strong>--subsystem</strong>
+</dt><dd>Invoke command (mandatory) as SSH2 subsystem.
+</dd>
+
+<dt><strong>--log</strong>
+</dt><dd>Print the receieved data to stderr.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>DESCRIPTION<a name="auto3"/></h2>
+
+<p>Open an SSH connection to specified server, and either run the command
+given there or open a remote interactive shell.
+</p>
+
+<h2>AUTHOR<a name="auto4"/></h2>
+
+<p>Written by Moshe Zadka, based on conch's help messages
+</p>
+
+<h2>REPORTING BUGS<a name="auto5"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto6"/></h2>
+
+<p>Copyright © 2002-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+<h2>SEE ALSO<a name="auto7"/></h2>
+
+<p>ssh(1)
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/conch/man/tkconch.1 b/doc/conch/man/tkconch.1
new file mode 100644
index 0000000..54260bf
--- /dev/null
+++ b/doc/conch/man/tkconch.1
@@ -0,0 +1,72 @@
+.TH CONCH "1" "October 2002" "" ""
+.SH NAME
+tkconch \- connect to SSH servers graphically
+.SH SYNOPSIS
+.B conch [-l \fIuser\fR] [-i \fIidentity\fR [ -i \fIidentity\fR ... ]] [-c \fIcipher\fR] [-m \fIMAC\fR] [-p \fIport\fR] [-n] [-t] [-T] [-V] [-C] [-N] [-s] [arg [...]]
+.PP
+.B conch --help
+.SH DESCRIPTION
+.PP
+The \fB\--help\fR prints out a usage message to standard output.
+.TP
+\fB-l\fR, \fB--user\fR <user>
+Log in using this user name.
+.TP
+\fB-e\fR, \fB--escape\fR <escape character>
+Set escape character; 'none' = disable (default: ~)
+.TP
+\fB-i\fR, \fB--identity\fR <identity>
+Add an identity file for public key authentication (default: ~/.ssh/identity)
+.TP
+\fB-c\fR, \fB--cipher\fR <cipher>
+Cipher algorithm to use.
+.TP
+\fB-m\fR, \fB--macs\fR <mac>
+Specify MAC algorithms for protocol version 2.
+.TP
+\fB-p\fR, \fB--port\fR <port>
+Port to connect to.
+.TP
+\fB-L\fR, \fB--localforward\fR <listen-port:host:port>
+Forward local port to remote address.
+.TP
+\fB-R\fR, \fB--remoteforward\fR <listen-port:host:port>
+Forward remote port to local address.
+.TP
+\fB-t\fR, \fB--tty\fR
+Allocate a tty even if command is given.
+.TP
+\fB-n\fR, \fB--notty\fR
+Do not allocate a tty.
+.TP
+\fB-V\fR, \fB--version\fR
+Display version number only.
+.TP
+\fB-C\fR, \fB--compress\fR
+Enable compression.
+.TP
+\fB-a\fR, \fB--ansilog\fR
+Print the received data to stdout.
+.TP
+\fB-N\fR, \fB--noshell\fR
+Do not execute a shell or command.
+.TP
+\fB-s\fR, \fB--subsystem\fR
+Invoke command (mandatory) as SSH2 subsystem.
+.TP
+\fB--log\fR
+Print the receieved data to stderr.
+.SH DESCRIPTION
+Open an SSH connection to specified server, and either run the command
+given there or open a remote interactive shell.
+.SH AUTHOR
+Written by Moshe Zadka, based on conch's help messages
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2002-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+.SH "SEE ALSO"
+ssh(1)
diff --git a/doc/core/benchmarks/banana.py b/doc/core/benchmarks/banana.py
new file mode 100644
index 0000000..1c1f031
--- /dev/null
+++ b/doc/core/benchmarks/banana.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+from timer import timeit
+from twisted.spread.banana import b1282int
+
+ITERATIONS = 100000
+
+for length in (1, 5, 10, 50, 100):
+ elapsed = timeit(b1282int, ITERATIONS, "\xff" * length)
+ print "b1282int %3d byte string: %10d cps" % (length, ITERATIONS / elapsed)
diff --git a/doc/core/benchmarks/deferreds.py b/doc/core/benchmarks/deferreds.py
new file mode 100644
index 0000000..ddae19e
--- /dev/null
+++ b/doc/core/benchmarks/deferreds.py
@@ -0,0 +1,145 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+See how fast deferreds are.
+
+This is mainly useful to compare cdefer.Deferred to defer.Deferred
+"""
+
+
+from twisted.internet import defer
+from timer import timeit
+
+benchmarkFuncs = []
+
+def benchmarkFunc(iter, args=()):
+ """
+ A decorator for benchmark functions that measure a single iteration
+ count. Registers the function with the given iteration count to the global
+ benchmarkFuncs list
+ """
+ def decorator(func):
+ benchmarkFuncs.append((func, args, iter))
+ return func
+ return decorator
+
+def benchmarkNFunc(iter, ns):
+ """
+ A decorator for benchmark functions that measure multiple iteration
+ counts. Registers the function with the given iteration count to the global
+ benchmarkFuncs list.
+ """
+ def decorator(func):
+ for n in ns:
+ benchmarkFuncs.append((func, (n,), iter))
+ return func
+ return decorator
+
+def instantiate():
+ """
+ Only create a deferred
+ """
+ d = defer.Deferred()
+instantiate = benchmarkFunc(100000)(instantiate)
+
+def instantiateShootCallback():
+ """
+ Create a deferred and give it a normal result
+ """
+ d = defer.Deferred()
+ d.callback(1)
+instantiateShootCallback = benchmarkFunc(100000)(instantiateShootCallback)
+
+def instantiateShootErrback():
+ """
+ Create a deferred and give it an exception result. To avoid Unhandled
+ Errors, also register an errback that eats the error
+ """
+ d = defer.Deferred()
+ try:
+ 1/0
+ except:
+ d.errback()
+ d.addErrback(lambda x: None)
+instantiateShootErrback = benchmarkFunc(200)(instantiateShootErrback)
+
+ns = [10, 1000, 10000]
+
+def instantiateAddCallbacksNoResult(n):
+ """
+ Creates a deferred and adds a trivial callback/errback/both to it the given
+ number of times.
+ """
+ d = defer.Deferred()
+ def f(result):
+ return result
+ for i in xrange(n):
+ d.addCallback(f)
+ d.addErrback(f)
+ d.addBoth(f)
+ d.addCallbacks(f, f)
+instantiateAddCallbacksNoResult = benchmarkNFunc(20, ns)(instantiateAddCallbacksNoResult)
+
+def instantiateAddCallbacksBeforeResult(n):
+ """
+ Create a deferred and adds a trivial callback/errback/both to it the given
+ number of times, and then shoots a result through all of the callbacks.
+ """
+ d = defer.Deferred()
+ def f(result):
+ return result
+ for i in xrange(n):
+ d.addCallback(f)
+ d.addErrback(f)
+ d.addBoth(f)
+ d.addCallbacks(f)
+ d.callback(1)
+instantiateAddCallbacksBeforeResult = benchmarkNFunc(20, ns)(instantiateAddCallbacksBeforeResult)
+
+def instantiateAddCallbacksAfterResult(n):
+ """
+ Create a deferred, shoots it and then adds a trivial callback/errback/both
+ to it the given number of times. The result is processed through the
+ callbacks as they are added.
+ """
+ d = defer.Deferred()
+ def f(result):
+ return result
+ d.callback(1)
+ for i in xrange(n):
+ d.addCallback(f)
+ d.addErrback(f)
+ d.addBoth(f)
+ d.addCallbacks(f)
+instantiateAddCallbacksAfterResult = benchmarkNFunc(20, ns)(instantiateAddCallbacksAfterResult)
+
+def pauseUnpause(n):
+ """
+ Adds the given number of callbacks/errbacks/both to a deferred while it is
+ paused, and unpauses it, trigerring the processing of the value through the
+ callbacks.
+ """
+ d = defer.Deferred()
+ def f(result):
+ return result
+ d.callback(1)
+ d.pause()
+ for i in xrange(n):
+ d.addCallback(f)
+ d.addErrback(f)
+ d.addBoth(f)
+ d.addCallbacks(f)
+ d.unpause()
+pauseUnpause = benchmarkNFunc(20, ns)(pauseUnpause)
+
+def benchmark():
+ """
+ Run all of the benchmarks registered in the benchmarkFuncs list
+ """
+ print defer.Deferred.__module__
+ for func, args, iter in benchmarkFuncs:
+ print func.__name__, args, timeit(func, iter, *args)
+
+if __name__ == '__main__':
+ benchmark()
diff --git a/doc/core/benchmarks/failure.py b/doc/core/benchmarks/failure.py
new file mode 100644
index 0000000..d98cb49
--- /dev/null
+++ b/doc/core/benchmarks/failure.py
@@ -0,0 +1,66 @@
+
+"""See how slow failure creation is"""
+
+import random
+from twisted.python import failure
+
+random.seed(10050)
+O = [0, 20, 40, 60, 80, 10, 30, 50, 70, 90]
+DEPTH = 30
+
+def pickVal():
+ return random.choice([None, 1, 'Hello', [], {1: 1}, (1, 2, 3)])
+
+def makeLocals(n):
+ return ';'.join(['x%d = %s' % (i, pickVal()) for i in range(n)])
+
+for nLocals in O:
+ for i in range(DEPTH):
+ s = """
+def deepFailure%d_%d():
+ %s
+ deepFailure%d_%d()
+""" % (nLocals, i, makeLocals(nLocals), nLocals, i + 1)
+ exec s
+
+ exec """
+def deepFailure%d_%d():
+ 1 / 0
+""" % (nLocals, DEPTH)
+
+R = range(5000)
+def fail(n):
+ for i in R:
+ try:
+ eval('deepFailure%d_0' % n)()
+ except:
+ failure.Failure()
+
+def fail_str(n):
+ for i in R:
+ try:
+ eval('deepFailure%d_0' % n)()
+ except:
+ str(failure.Failure())
+
+class PythonException(Exception): pass
+
+def fail_easy(n):
+ for i in R:
+ try:
+ failure.Failure(PythonException())
+ except:
+ pass
+
+from timer import timeit
+# for i in O:
+# timeit(fail, 1, i)
+
+# for i in O:
+# print 'easy failing', i, timeit(fail_easy, 1, i)
+
+for i in O:
+ print 'failing', i, timeit(fail, 1, i)
+
+# for i in O:
+# print 'string failing', i, timeit(fail_str, 1, i)
diff --git a/doc/core/benchmarks/linereceiver.py b/doc/core/benchmarks/linereceiver.py
new file mode 100644
index 0000000..7f55291
--- /dev/null
+++ b/doc/core/benchmarks/linereceiver.py
@@ -0,0 +1,47 @@
+import math, time
+
+from twisted.protocols import basic
+
+class CollectingLineReceiver(basic.LineReceiver):
+ def __init__(self):
+ self.lines = []
+ self.lineReceived = self.lines.append
+
+def deliver(proto, chunks):
+ map(proto.dataReceived, chunks)
+
+def benchmark(chunkSize, lineLength, numLines):
+ bytes = ('x' * lineLength + '\r\n') * numLines
+ chunkCount = len(bytes) / chunkSize + 1
+ chunks = []
+ for n in xrange(chunkCount):
+ chunks.append(bytes[n*chunkSize:(n+1)*chunkSize])
+ assert ''.join(chunks) == bytes, (chunks, bytes)
+ p = CollectingLineReceiver()
+
+ before = time.clock()
+ deliver(p, chunks)
+ after = time.clock()
+
+ assert bytes.splitlines() == p.lines, (bytes.splitlines(), p.lines)
+
+ print 'chunkSize:', chunkSize,
+ print 'lineLength:', lineLength,
+ print 'numLines:', numLines,
+ print 'CPU Time: ', after - before
+
+
+
+def main():
+ for numLines in 100, 1000:
+ for lineLength in (10, 100, 1000):
+ for chunkSize in (1, 500, 5000):
+ benchmark(chunkSize, lineLength, numLines)
+
+ for numLines in 10000, 50000:
+ for lineLength in (1000, 2000):
+ for chunkSize in (51, 500, 5000):
+ benchmark(chunkSize, lineLength, numLines)
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/benchmarks/netstringreceiver.py b/doc/core/benchmarks/netstringreceiver.py
new file mode 100644
index 0000000..e48f66e
--- /dev/null
+++ b/doc/core/benchmarks/netstringreceiver.py
@@ -0,0 +1,242 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.test import proto_helpers, test_protocols
+import math
+import time
+import sys
+import os
+import gc
+
+NETSTRING_PREFIX_TEMPLATE ="%d:"
+NETSTRING_POSTFIX = ","
+USAGE = """\
+Usage: %s <number> <filename>
+
+This script creates up to 2 ** <number> chunks with up to 2 **
+<number> characters and sends them to the NetstringReceiver. The
+sorted performance data for all combination is written to <filename>
+afterwards.
+
+You might want to start with a small number, maybe 10 or 12, and slowly
+increase it. Stop when the performance starts to deteriorate ;-).
+"""
+
+class PerformanceTester(object):
+ """
+ A class for testing the performance of some
+ """
+
+ headers = []
+ lineFormat = ""
+ performanceData = {}
+
+ def __init__(self, filename):
+ """
+ Initializes C{self.filename}.
+
+ If a file with this name already exists, asks if it should be
+ overwritten. Terminates with exit status 1, if the user does
+ not accept.
+ """
+ if os.path.isfile(filename):
+ response = raw_input(("A file named %s exists. "
+ "Overwrite it (y/n)? ") % filename)
+ if response.lower() != "y":
+ print "Performance test cancelled."
+ sys.exit(1)
+ self.filename = filename
+
+
+ def testPerformance(self, number):
+ """
+ Drives the execution of C{performTest} with arguments between
+ 0 and C{number - 1}.
+
+ @param number: Defines the number of test runs to be performed.
+ @type number: C{int}
+ """
+ for iteration in xrange(number):
+ self.performTest(iteration)
+
+
+ def performTest(self, iteration):
+ """
+ Performs one test iteration. Overwrite this.
+
+ @param iteration: The iteration number. Can be used to configure
+ the test.
+ @type iteration: C{int}
+ @raise NotImplementedError: because this method has to be implemented
+ by the subclass.
+ """
+ raise NotImplementedError
+
+
+ def createReport(self):
+ """
+ Creates a file and writes a table with performance data.
+
+ The performance data are ordered by the total size of the netstrings.
+ In addition they show the chunk size, the number of chunks and the
+ time (in seconds) that elapsed while the C{NetstringReceiver}
+ received the netstring.
+
+ @param filename: The name of the report file that will be written.
+ @type filename: C{str}
+ """
+ self.outputFile = open(self.filename, "w")
+ self.writeHeader()
+ self.writePerformanceData()
+ self.writeLineSeparator()
+ print "The report was written to %s." % self.filename
+
+
+ def writeHeader(self):
+ """
+ Writes the table header for the report.
+ """
+ self.writeLineSeparator()
+ self.outputFile.write("| %s |\n" % (" | ".join(self.headers),))
+ self.writeLineSeparator()
+
+
+ def writeLineSeparator(self):
+ """
+ Writes a 'line separator' made from '+' and '-' characters.
+ """
+ dashes = ("-" * (len(header) + 2) for header in self.headers)
+ self.outputFile.write("+%s+\n" % "+".join(dashes))
+
+
+ def writePerformanceData(self):
+ """
+ Writes one line for each item in C{self.performanceData}.
+ """
+ for combination, elapsed in sorted(self.performanceData.iteritems()):
+ totalSize, chunkSize, numberOfChunks = combination
+ self.outputFile.write(self.lineFormat %
+ (totalSize, chunkSize, numberOfChunks,
+ elapsed))
+
+
+
+class NetstringPerformanceTester(PerformanceTester):
+ """
+ A class for determining the C{NetstringReceiver.dataReceived} performance.
+
+ Instantiates a C{NetstringReceiver} and calls its
+ C{dataReceived()} method with different chunks sizes and numbers
+ of chunks. Presents a table showing the relation between input
+ data and time to process them.
+ """
+
+ headers = ["Chunk size", "Number of chunks", "Total size",
+ "Time to receive" ]
+ lineFormat = ("| %%%dd | %%%dd | %%%dd | %%%d.4f |\n" %
+ tuple([len(header) for header in headers]))
+
+ def __init__(self, filename):
+ """
+ Sets up the output file and the netstring receiver that will be
+ used for receiving data.
+
+ @param filename: The name of the file for storing the report.
+ @type filename: C{str}
+ """
+ PerformanceTester.__init__(self, filename)
+ transport = proto_helpers.StringTransport()
+ self.netstringReceiver = test_protocols.TestNetstring()
+ self.netstringReceiver.makeConnection(transport)
+
+
+ def performTest(self, number):
+ """
+ Tests the performance of C{NetstringReceiver.dataReceived}.
+
+ Feeds netstrings of various sizes in different chunk sizes
+ to a C{NetstringReceiver} and stores the elapsed time in
+ C{self.performanceData}.
+
+ @param number: The maximal chunks size / number of
+ chunks to be checked.
+ @type number: C{int}
+ """
+ chunkSize = 2 ** number
+ numberOfChunks = chunkSize
+ while numberOfChunks:
+ self.testCombination(chunkSize, numberOfChunks)
+ numberOfChunks = numberOfChunks // 2
+
+
+ def testCombination(self, chunkSize, numberOfChunks):
+ """
+ Tests one combination of chunk size and number of chunks.
+
+ @param chunkSize: The size of one chunk to be sent to the
+ C{NetstringReceiver}.
+ @type chunkSize: C{int}
+ @param numberOfChunks: The number of C{chunkSize}-sized chunks to
+ be sent to the C{NetstringReceiver}.
+ @type numberOfChunks: C{int}
+ """
+ chunk, dataSize = self.configureCombination(chunkSize, numberOfChunks)
+ elapsed = self.receiveData(chunk, numberOfChunks, dataSize)
+ key = (chunkSize, numberOfChunks, dataSize)
+ self.performanceData[key] = elapsed
+
+
+ def configureCombination(self, chunkSize, numberOfChunks):
+ """
+ Updates C{MAX_LENGTH} for {self.netstringReceiver} (to avoid
+ C{NetstringParseErrors} that might be raised if the size
+ exceeds the default C{MAX_LENGTH}).
+
+ Calculates and returns one 'chunk' of data and the total size
+ of the netstring.
+
+ @param chunkSize: The size of chunks that will be received.
+ @type chunkSize: C{int}
+ @param numberOfChunks: The number of C{chunkSize}-sized chunks
+ that will be received.
+ @type numberOfChunks: C{int}
+
+ @return: A tuple consisting of string of C{chunkSize} 'a'
+ characters and the size of the netstring data portion.
+ """
+ chunk = "a" * chunkSize
+ dataSize = chunkSize * numberOfChunks
+ self.netstringReceiver.MAX_LENGTH = dataSize
+ numberOfDigits = math.ceil(math.log10(dataSize)) + 1
+ return chunk, dataSize
+
+
+ def receiveData(self, chunk, numberOfChunks, dataSize):
+ dr = self.netstringReceiver.dataReceived
+ now = time.time()
+ dr(NETSTRING_PREFIX_TEMPLATE % (dataSize,))
+ for idx in xrange(numberOfChunks):
+ dr(chunk)
+ dr(NETSTRING_POSTFIX)
+ elapsed = time.time() - now
+ assert self.netstringReceiver.received, "Didn't receive string!"
+ return elapsed
+
+
+def disableGarbageCollector():
+ gc.disable()
+ print 'Disabled Garbage Collector.'
+
+
+def main(number, filename):
+ disableGarbageCollector()
+ npt = NetstringPerformanceTester(filename)
+ npt.testPerformance(int(number))
+ npt.createReport()
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print USAGE % sys.argv[0]
+ sys.exit(1)
+ main(*sys.argv[1:3])
diff --git a/doc/core/benchmarks/task.py b/doc/core/benchmarks/task.py
new file mode 100644
index 0000000..e3d437b
--- /dev/null
+++ b/doc/core/benchmarks/task.py
@@ -0,0 +1,26 @@
+
+"""
+Benchmarks for L{twisted.internet.task}.
+"""
+
+from timer import timeit
+
+from twisted.internet import task
+
+def test_performance():
+ """
+ L{LoopingCall} should not take long to skip a lot of iterations.
+ """
+ clock = task.Clock()
+ call = task.LoopingCall(lambda: None)
+ call.clock = clock
+
+ call.start(0.1)
+ clock.advance(1000000)
+
+
+def main():
+ print "LoopingCall large advance takes", timeit(test_performance, iter=1)
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/benchmarks/timer.py b/doc/core/benchmarks/timer.py
new file mode 100644
index 0000000..78181c6
--- /dev/null
+++ b/doc/core/benchmarks/timer.py
@@ -0,0 +1,24 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Helper stuff for benchmarks.
+"""
+
+import gc
+gc.disable()
+print 'Disabled GC'
+
+def timeit(func, iter = 1000, *args, **kwargs):
+ """
+ timeit(func, iter = 1000 *args, **kwargs) -> elapsed time
+
+ calls func iter times with args and kwargs, returns time elapsed
+ """
+
+ from time import time as currentTime
+ r = range(iter)
+ t = currentTime()
+ for i in r:
+ func(*args, **kwargs)
+ return currentTime() - t
diff --git a/doc/core/benchmarks/tpclient.py b/doc/core/benchmarks/tpclient.py
new file mode 100644
index 0000000..9e5e082
--- /dev/null
+++ b/doc/core/benchmarks/tpclient.py
@@ -0,0 +1,60 @@
+"""Throughput test."""
+
+import time, sys
+from twisted.internet import reactor, protocol
+from twisted.python import log
+
+TIMES = 10000
+S = "0123456789" * 1240
+
+toReceive = len(S) * TIMES
+
+class Sender(protocol.Protocol):
+
+ def connectionMade(self):
+ start()
+ self.numSent = 0
+ self.received = 0
+ self.transport.registerProducer(self, 0)
+
+ def stopProducing(self):
+ pass
+
+ def pauseProducing(self):
+ pass
+
+ def resumeProducing(self):
+ self.numSent += 1
+ self.transport.write(S)
+ if self.numSent == TIMES:
+ self.transport.unregisterProducer()
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ shutdown(self.numSent == TIMES)
+
+
+started = None
+
+def start():
+ global started
+ started = time.time()
+
+def shutdown(success):
+ if not success:
+ raise SystemExit, "failure or something"
+ passed = time.time() - started
+ print "Throughput (send): %s kbytes/sec" % ((toReceive / passed) / 1024)
+ reactor.stop()
+
+
+def main():
+ f = protocol.ClientFactory()
+ f.protocol = Sender
+ reactor.connectTCP(sys.argv[1], int(sys.argv[2]), f)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ #log.startLogging(sys.stdout)
+ main()
diff --git a/doc/core/benchmarks/tpclient_nt.py b/doc/core/benchmarks/tpclient_nt.py
new file mode 100644
index 0000000..a8170d7
--- /dev/null
+++ b/doc/core/benchmarks/tpclient_nt.py
@@ -0,0 +1,22 @@
+"""Non-twisted throughput client."""
+
+import socket, time, sys
+
+TIMES = 50000
+S = "0123456789" * 1024
+sent = len(S) * TIMES
+
+def main():
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect((sys.argv[1], int(sys.argv[2])))
+ start = time.time()
+ i = 0
+ while i < TIMES:
+ i += 1
+ s.sendall(S)
+ passed = time.time() - start
+ print "Throughput: %s kbytes/sec" % ((sent / passed) / 1024)
+ s.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/benchmarks/tpserver.py b/doc/core/benchmarks/tpserver.py
new file mode 100644
index 0000000..49024e1
--- /dev/null
+++ b/doc/core/benchmarks/tpserver.py
@@ -0,0 +1,19 @@
+"""Throughput server."""
+
+import sys
+
+from twisted.protocols.wire import Discard
+from twisted.internet import protocol, reactor
+from twisted.python import log
+
+
+def main():
+ f = protocol.ServerFactory()
+ f.protocol = Discard
+ reactor.listenTCP(8000, f)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/doc/core/benchmarks/tpserver_nt.py b/doc/core/benchmarks/tpserver_nt.py
new file mode 100644
index 0000000..e4bfdda
--- /dev/null
+++ b/doc/core/benchmarks/tpserver_nt.py
@@ -0,0 +1,22 @@
+"""Non-twisted throughput server."""
+
+import socket, signal, sys
+
+def signalhandler(*args):
+ print "alarm!"
+ sys.stdout.flush()
+
+signal.signal(signal.SIGALRM, signalhandler)
+
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+s.bind(('', 8001))
+s.listen(1)
+while 1:
+ c, (h, p) = s.accept()
+ c.settimeout(30)
+ signal.alarm(5)
+ while 1:
+ d = c.recv(16384)
+ if not d:
+ break
+ c.close()
diff --git a/doc/core/development/index.html b/doc/core/development/index.html
new file mode 100644
index 0000000..183d76b
--- /dev/null
+++ b/doc/core/development/index.html
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Development of Twisted</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Development of Twisted</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>This documentation is for people who work on the Twisted codebase itself,
+rather than for people who want to use Twisted in their own projects.</p>
+<ul>
+<li><a href="naming.html" shape="rect">Naming</a></li>
+<li><a href="philosophy.html" shape="rect">Philosophy</a></li>
+<li><a href="security.html" shape="rect">Security</a></li>
+<li><a href="policy/" shape="rect">Twisted development policy</a></li>
+</ul>
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/listings/new_module_template.py b/doc/core/development/listings/new_module_template.py
new file mode 100644
index 0000000..85cf04b
--- /dev/null
+++ b/doc/core/development/listings/new_module_template.py
@@ -0,0 +1,12 @@
+# -*- test-case-name: <test module> -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Docstring goes here.
+"""
+
+
+__all__ = []
diff --git a/doc/core/development/naming.html b/doc/core/development/naming.html
new file mode 100644
index 0000000..1f139dd
--- /dev/null
+++ b/doc/core/development/naming.html
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Naming Conventions</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Naming Conventions</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<p>While this may sound like a small detail, clear method naming is important to provide an API that developers familiar with event-based programming can pick up quickly.</p>
+
+<p>Since the idea of a method call maps very neatly onto that of a received event, all event handlers are simply methods named after past-tense verbs. All class names are descriptive nouns, designed to mirror the is-a relationship of the abstractions they implement. All requests for notification or transmission are present-tense imperative verbs.</p>
+
+<p>Here are some examples of this naming scheme:</p>
+
+<ul>
+<li>An event notification of data received from peer:
+<code class="python">dataReceived(data)</code></li>
+<li>A request to send data: <code class="python">write(data)</code></li>
+<li>A class that implements a protocol: <code class="python">Protocol</code></li>
+</ul>
+
+<p>The naming is platform neutral. This means that the names are equally appropriate in a wide variety of environments, as long as they can publish the required events.</p>
+
+<p>It is self-consistent. Things that deal with TCP use the acronym TCP, and it is always capitalized. Dropping, losing, terminating, and closing the connection are all referred to as <q>losing</q> the connection. This symmetrical naming allows developers to easily locate other API calls if they have learned a few related to what they want to do.</p>
+
+<p>It is semantically clear. The semantics of dataReceived are simple: there are some bytes available for processing. This remains true even if the lower-level machinery to get the data is highly complex.</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/philosophy.html b/doc/core/development/philosophy.html
new file mode 100644
index 0000000..0dd2ebc
--- /dev/null
+++ b/doc/core/development/philosophy.html
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Philosophy</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Philosophy</h1>
+ <div class="toc"><ol><li><a href="#auto0">Abstraction Levels</a></li><li><a href="#auto1">Learning Curves</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Abstraction Levels<a name="auto0"/></h2>
+
+<p>When implementing interfaces to the operating system or
+the network, provide two interfaces:</p>
+
+<ul>
+<li>One that doesn't hide platform specific or library specific
+functionality.
+For example, you can use file descriptors on Unix, and Win32 events on
+Windows.
+</li>
+<li>One that provides a high level interface hiding platform specific
+details.
+E.g. process running uses same API on Unix and Windows, although
+the implementation is very different.
+</li>
+</ul>
+
+<p>Restated in a more general way:</p>
+
+<ul>
+<li>Provide all low level functionality for your specific domain,
+without limiting the policies and decisions the user can make.</li>
+<li>Provide a high level abstraction on top of the low level
+implementation (or implementations) which implements the
+common use cases and functionality that is used in most cases.</li>
+</ul>
+
+<h2>Learning Curves<a name="auto1"/></h2>
+
+<p>Require the minimal amount of work and learning on part of the
+user to get started. If this means they have less functionality,
+that's OK, when they need it they can learn a bit more. This
+will also lead to a cleaner, easier to test design.</p>
+
+<p>For example - using twistd is a great way to deploy applications.
+But to get started you don't need to know about it. Later on you can
+start using twistd, but its usage is optional.</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/coding-standard.html b/doc/core/development/policy/coding-standard.html
new file mode 100644
index 0000000..c83ae77
--- /dev/null
+++ b/doc/core/development/policy/coding-standard.html
@@ -0,0 +1,818 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Coding Standard</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Coding Standard</h1>
+ <div class="toc"><ol><li><a href="#auto0">Naming</a></li><li><a href="#auto1">Testing</a></li><ul><li><a href="#auto2">Overview</a></li><li><a href="#auto3">Test Suite</a></li></ul><li><a href="#auto4">Copyright Header</a></li><li><a href="#auto5">Whitespace</a></li><li><a href="#auto6">Modules</a></li><li><a href="#auto7">Packages</a></li><li><a href="#auto8">String Formatting Operations</a></li><li><a href="#auto9">Docstrings</a></li><li><a href="#auto10">Comments</a></li><li><a href="#auto11">Versioning</a></li><li><a href="#auto12">Scripts</a></li><li><a href="#auto13">Examples</a></li><li><a href="#auto14">Standard Library Extension Modules</a></li><li><a href="#auto15">Classes</a></li><ul><li><a href="#auto16">New-style Classes</a></li></ul><li><a href="#auto17">Methods</a></li><li><a href="#auto18">Callback Arguments</a></li><li><a href="#auto19">Special Methods</a></li><li><a href="#auto20">Functions</a></li><li><a href="#auto21">Attributes</a></li><li><a href="#auto22">Database</a></li><li><a href="#auto23">C Code</a></li><li><a href="#auto24">Commit Messages</a></li><li><a href="#auto25">Source Control</a></li><li><a href="#auto26">Fallback</a></li><li><a href="#auto27">Recommendations</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Naming<a name="auto0"/></h2>
+
+ <p>Try to choose names which are both easy to remember and
+ meaningful. Some silliness is OK at the module naming level
+ (see <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.html" title="twisted.spread">twisted.spread</a></code>...) but when
+ choosing class names, be as precise as possible.</p>
+
+ <p>Try to avoid overloaded terms. This rule is often broken,
+ since it is incredibly difficult, as most normal words have
+ already been taken by some other software. More importantly,
+ try to avoid meaningless words. In particular, words like
+ <q>handler</q>, <q>processor</q>, <q>engine</q>, <q>manager</q>
+ and <q>component</q> don't really indicate what something does,
+ only that it does <em>something</em>.</p>
+
+ <p>Use American spelling in both names and docstrings. For compound
+ technical terms such as 'filesystem', use a non-hyphenated spelling in
+ both docstrings and code in order to avoid unnecessary
+ capitalization.</p>
+
+ <h2>Testing<a name="auto1"/></h2>
+
+ <h3>Overview<a name="auto2"/></h3>
+
+ <p>Twisted development should always be
+ <a href="http://en.wikipedia.org/wiki/Test-driven_development" shape="rect">
+ test-driven</a>. The complete test suite in the head of the SVN trunk is required to
+ be passing on <a href="http://buildbot.twistedmatrix.com/supported" shape="rect">
+ supported platforms</a> at all times. Regressions in the test suite
+ are addressed by reverting whatever revisions introduced them. For
+ complete documentation about testing Twisted itself, refer to the
+ <a href="test-standard.html" shape="rect">Test Standard</a>. What follows is
+ intended to be a synopsis of the most important points.</p>
+
+ <h3>Test Suite<a name="auto3"/></h3>
+
+ <p>The Twisted test suite is spread across many subpackages of the
+ <code>twisted</code> package. Many older tests are in
+ <code>twisted.test</code>. Others can be found at places such as
+ <code>twisted.web.test</code> (for <code>twisted.web</code> tests)
+ or <code>twisted.internet.test</code> (for <code>twisted.internet</code>
+ tests). The latter arrangement, <code>twisted.somepackage.test</code>,
+ is preferred for new tests except when a test module already exists in
+ <code>twisted.test</code>.
+ </p>
+
+ <p>
+ Parts of the Twisted test suite may serve as good examples of how to
+ write tests for Twisted or for Twisted-based libraries (newer parts of
+ the test suite are generally better examples than older parts - check
+ when the code you are looking at was written before you use it as an
+ example of what you should write). The names of test modules should
+ begin with <code>test_</code> so that they are automatically discoverable by
+ test runners such as Trial. Twisted's unit tests are written using
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.trial.html" title="twisted.trial">twisted.trial</a></code>, an xUnit library which has been
+ extensively customized for use in testing Twisted and Twisted-based
+ libraries.</p>
+
+ <p>Implementation (ie, non-test) source files should begin with a
+ <code>test-case-name</code> tag which gives the name of any test
+ modules or packages which exercise them. This lets tools discover a
+ subset of the entire test suite which they can run first to find tests
+ which might be broken by a particular change.</p>
+
+ <p>It is strongly suggested that developers learn to use Emacs, and use
+ the <code>twisted-dev.el</code> file included in
+ <a href="http://launchpad.net/twisted-emacs" shape="rect">twisted-emacs</a>
+ to bind the F9 key to <q>run unit tests</q> and bang on it
+ frequently. Support for other editors is unavailable at this time but
+ we would love to provide it.</p>
+
+ <p>To run the whole Twisted test without using emacs, use trial:</p>
+
+ <pre class="shell" xml:space="preserve">
+$ bin/trial twisted
+ </pre>
+
+ <p>To run an individual test module, such as
+ <code>twisted/mail/test/test_pop3.py</code>, specify the module
+ name:</p>
+
+ <pre class="shell" xml:space="preserve">
+$ bin/trial twisted.mail.test.test_pop3
+ </pre>
+
+ <p>To run the tests associated with a particular implementation file,
+ such as <code>twisted/mail/pop3.py</code>, use the
+ <code>testmodule</code> option:</p>
+
+ <pre class="shell" xml:space="preserve">
+$ bin/trial twisted/mail/pop3.py
+ </pre>
+
+ <p>All unit test methods should have docstrings specifying at a high
+ level the intent of the test. That is, a description that users of the
+ method would understand.</p>
+
+ <p>If you modify, or write a new, HOWTO, please read the <a href="http://twistedmatrix.com/trac/wiki/TwistedLore" shape="rect">Lore</a>
+ documentation to learn how to format the docs.</p>
+
+ <h2>Copyright Header<a name="auto4"/></h2>
+
+ <p>Whenever a new file is added to the repository, add the following
+ license header at the top of the file:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+</pre>
+
+ <p>When you update existing files, if there is no copyright header, add
+ one.</p>
+
+ <h2>Whitespace<a name="auto5"/></h2>
+
+ <p>Indentation is 4 spaces per indent. Tabs are not allowed. It
+ is preferred that every block appear on a new line, so that
+ control structure indentation is always visible.</p>
+
+ <p>Lines are flowed at 79 columns. They must not have trailing
+ whitespace. Long lines must be wrapped using implied line continuation
+ inside parentheses; backslashes aren't allowed. To handle long import
+ lines, please repeat the import like this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">very</span>.<span class="py-src-variable">long</span>.<span class="py-src-variable">package</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">foo</span>, <span class="py-src-variable">bar</span>, <span class="py-src-variable">baz</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">very</span>.<span class="py-src-variable">long</span>.<span class="py-src-variable">package</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">qux</span>, <span class="py-src-variable">quux</span>, <span class="py-src-variable">quuux</span>
+</pre>
+
+ <p>Top-level classes and functions must be separated with 3 blank lines,
+ and class-level functions with 2 blank lines. The control-L (i.e. ^L) form
+ feed character must not be used.</p>
+
+ <h2>Modules<a name="auto6"/></h2>
+
+ <p>Modules must be named in all lower-case, preferably short,
+ single words. If a module name contains multiple words, they
+ may be separated by underscores or not separated at all.</p>
+
+ <p>Modules must have a copyright message, a docstring and a
+ reference to a test module that contains the bulk of its tests.
+ Use this template:</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-comment"># -*- test-case-name: &lt;test module&gt; -*-</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+
+<span class="py-src-string">&quot;&quot;&quot;
+Docstring goes here.
+&quot;&quot;&quot;</span>
+
+
+<span class="py-src-variable">__all__</span> = []
+</pre><div class="caption">Source listing - <a href="../listings/new_module_template.py"><span class="filename">../listings/new_module_template.py</span></a></div></div>
+
+ <p>In most cases, modules should contain more than one class,
+ function, or method; if a module contains only one object,
+ consider refactoring to include more related functionality in
+ that module.</p>
+
+ <p>Depending on the situation, it is acceptable to have imports that
+ look like this:
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Deferred</span>
+</pre>
+ or like this:
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+</pre>
+ That is, modules should import <em>modules</em> or <em>classes and
+ functions</em>, but not <em>packages</em>.</p>
+
+ <p>Wildcard import syntax may not be used by code in Twisted. These
+ imports lead to code which is difficult to read and maintain by
+ introducing complexity which strains human readers and automated tools
+ alike. If you find yourself with many imports to make from a single
+ module and wish to save typing, consider importing the module itself,
+ rather than its attributes.</p>
+
+ <p><em>Relative imports</em> (or <em>sibling imports</em>) may not be
+ used by code in Twisted. Relative imports allow certain circularities
+ to be introduced which can ultimately lead to unimportable modules or
+ duplicate instances of a single module. Relative imports also make the
+ task of refactoring more difficult.</p>
+
+ <p>In case of local names conflicts due to import, use the <code>as</code>
+ syntax, for example:
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">util</span> <span class="py-src-keyword">as</span> <span class="py-src-variable">trial_util</span>
+</pre></p>
+
+ <p>The encoding must always be ASCII, so no coding cookie is necessary.</p>
+
+ <h2>Packages<a name="auto7"/></h2>
+
+ <p>Package names should follow the same conventions as module
+ names. All modules must be encapsulated in some package. Nested
+ packages may be used to further organize related modules.</p>
+
+ <p><code>__init__.py</code> must never contain anything other than a
+ docstring and (optionally) an <code>__all__</code> attribute. Packages are
+ not modules and should be treated differently. This rule may be
+ broken to preserve backwards compatibility if a module is made
+ into a nested package as part of a refactoring.</p>
+
+ <p>If you wish to promote code from a module to a package, for
+ example, to break a large module out into several smaller
+ files, the accepted way to do this is to promote from within
+ the module. For example,</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-comment"># parent/</span>
+<span class="py-src-comment"># --- __init__.py ---</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">child</span>
+
+<span class="py-src-comment"># --- child.py ---</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">parent</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Foo</span>:
+ <span class="py-src-keyword">pass</span>
+<span class="py-src-variable">parent</span>.<span class="py-src-variable">Foo</span> = <span class="py-src-variable">Foo</span>
+</pre>
+
+ <p>Every package should be added to the list in
+ <code class="shell">setup.py</code>.</p>
+
+ <p>Packages must not depend circularly upon each other. To simplify
+ maintaining this state, packages must also not import each other
+ circularly. While this applies to all packages within Twisted, one
+ <code>twisted.python</code> deserves particular attention, as it may
+ not depend on any other Twisted package.</p>
+
+ <h2>String Formatting Operations<a name="auto8"/></h2>
+
+ <p>When using <a href="http://docs.python.org/lib/typesseq-strings.html" shape="rect">string formatting
+ operations</a> like <code>formatString % values</code> you should always
+ use a tuple if you're using non-mapping <code>values</code>. This is to
+ avoid unexpected behavior when you think you're passing in a single value,
+ but the value is unexpectedly a tuple, e.g.:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Hi %s\n&quot;</span> % <span class="py-src-variable">x</span>
+</pre>
+
+ <p>The example shows you can pass in <code>foo(&quot;foo&quot;)</code> or
+ <code>foo(3)</code> fine, but if you pass in <code>foo((1,2))</code>,
+ it raises a <code>TypeError</code>. You should use this instead:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Hi %s\n&quot;</span> % (<span class="py-src-variable">x</span>,)
+</pre>
+
+ <h2>Docstrings<a name="auto9"/></h2>
+
+ <p>Docstrings should always be used to describe the
+ purpose of methods, functions, classes, and modules.</p>
+
+ <p>Docstrings are <em>never</em> to be used to provide semantic
+ information about an object; this rule may be violated if the
+ code in question is to be used in a system where this is a
+ requirement (such as Zope).</p>
+
+ <p>Docstrings should be indented to the level of the code they
+ are documenting.</p>
+
+ <p>Docstrings should be triple-quoted. The opening and the closing of the
+ docstrings should be on a line by themselves. For example:
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Ninja</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ A L{Ninja} is a warrior specializing in various unorthodox arts of war.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">attack</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">someone</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Attack C{someone} with this L{Ninja}'s shuriken.
+ &quot;&quot;&quot;</span>
+</pre>
+ </p>
+
+ <p>Docstrings should be written in epytext format; more
+ documentation is available in the
+ <a href="http://epydoc.sourceforge.net/manual-epytext.html" shape="rect">Epytext Markup Language documentation</a>.</p>
+
+ <p>Additionally, to accommodate emacs users, single quotes of the type of
+ the docstring's triple-quote should be escaped. This will prevent font-lock from
+ accidentally fontifying large portions of the file as a string.</p>
+
+ <p>For example,</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo2bar</span>(<span class="py-src-parameter">f</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Convert L{foo}s to L{bar}s.
+
+ A function that should be used when you have a C{foo} but you want a
+ C{bar}; note that this is a non-destructive operation. If this method
+ can't convert the C{foo} to a C{bar} it will raise a L{FooException}.
+
+ @param f: C{foo}
+ @type f: str
+
+ For example::
+
+ import wombat
+ def sample(something):
+ f = something.getFoo()
+ f.doFooThing()
+ b = wombat.foo2bar(f)
+ b.doBarThing()
+ return b
+
+ &quot;&quot;&quot;</span>
+ <span class="py-src-comment"># Optionally, actual code can go here.</span>
+</pre>
+
+ <h2>Comments<a name="auto10"/></h2>
+
+ <p>Comments marked with XXX or TODO must contain a reference to the
+ associated ticket.</p>
+
+ <h2>Versioning<a name="auto11"/></h2>
+
+ <p>The API documentation should be marked up with version information.
+ When a new API is added the class should be marked with the epytext
+ <code class="shell">@since:</code> field including the version number when
+ the change was introduced, eg. <code class="shell">@since: 8.1</code>.</p>
+
+ <h2>Scripts<a name="auto12"/></h2>
+
+ <p>For each <q>script</q>, that is, a program you expect a Twisted user
+ to run from the command-line, the following things must be done:</p>
+
+ <ol>
+ <li>Write a module in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.scripts.html" title="twisted.scripts">twisted.scripts</a></code>
+ which contains a callable global named <code>run</code>. This
+ will be called by the command line part with no arguments (it
+ will usually read <code>sys.argv</code>). Feel free to write more
+ functions or classes in this module, if you feel they are useful
+ to others.</li>
+
+ <li>Create a file which contains a shebang line for Python. For Twisted
+ Core, this file should be placed in the <code>bin/</code> directory; for
+ example, <code>bin/twistd</code>. For sub-projects, it should be placed
+ in <code>bin/&lt;subproject&gt;</code>; for example, the key-generation tool
+ for the Conch sub-project is in <code>bin/conch/ckeygen</code>.
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+</pre></li>
+
+ <p>To make sure that the script is portable across different UNIX like
+ operating systems we use the <code>/usr/bin/env</code> command. The env
+ command allows you to run a program in a modified environment. That way
+ you don't have to search for a program via the <code>PATH</code> environment
+ variable. This makes the script more portable but note that it is not a
+ foolproof method. Always make sure that <code>/usr/bin/env</code> exists or
+ use a softlink/symbolic link to point it to the correct path. Python's
+ distutils will rewrite the shebang line upon installation so this policy
+ only covers the source files in version control.</p>
+
+ <li>For core scripts, add this Twisted running-from-SVN header:
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+<span class="py-src-keyword">try</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">_preamble</span>
+<span class="py-src-keyword">except</span> <span class="py-src-variable">ImportError</span>:
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">clear_exc</span>()
+</pre>
+
+ Or for sub-project scripts, add a modified version which also adjusts <code>sys.path</code>:
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>, <span class="py-src-variable">os</span>
+<span class="py-src-variable">extra</span> = <span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">dirname</span>(<span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">dirname</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">0</span>]))
+<span class="py-src-variable">sys</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">insert</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">extra</span>)
+<span class="py-src-keyword">try</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">_preamble</span>
+<span class="py-src-keyword">except</span> <span class="py-src-variable">ImportError</span>:
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">clear_exc</span>()
+<span class="py-src-variable">sys</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">extra</span>)
+</pre></li>
+
+ <li>And end with:
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">scripts</span>.<span class="py-src-variable">yourmodule</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">run</span>
+<span class="py-src-variable">run</span>()
+</pre></li>
+
+ <li>Write a manpage and add it to the <code class="shell">man</code> folder
+ of a subproject's <code class="shell">doc</code> folder. On Debian systems
+ you can find a skeleton example of a manpage in
+ <code>/usr/share/doc/man-db/examples/manpage.example</code>.</li>
+ </ol>
+
+ <p>This will insure your program will work correctly for users of SVN,
+ Windows releases and Debian packages.</p>
+
+ <h2>Examples<a name="auto13"/></h2>
+
+ <p>For example scripts you expect a Twisted user
+ to run from the command-line, add this Python shebang line at the top
+ of the file:</p>
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+</pre>
+
+ <h2>Standard Library Extension Modules<a name="auto14"/></h2>
+
+ <p>When using the extension version of a module for which there is also
+ a Python version, place the import statement inside a try/except block,
+ and import the Python version if the import fails. This allows code to
+ work on platforms where the extension version is not available. For
+ example:
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">try</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">cPickle</span> <span class="py-src-keyword">as</span> <span class="py-src-variable">pickle</span>
+<span class="py-src-keyword">except</span> <span class="py-src-variable">ImportError</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">pickle</span>
+</pre>
+
+ Use the &quot;as&quot; syntax of the import statement as well, to set
+ the name of the extension module to the name of the Python module.</p>
+
+ <p>Some modules don't exist across all supported Python versions. For
+ example, Python 2.3's <code>sets</code> module was deprecated in Python 2.6
+ in favor of the <code>set</code> and <code>frozenset</code> builtins. When
+ you need to use sets or frozensets in your code, please use
+ the <code>set</code> and <code>frozenset</code> provided
+ by <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.compat.html" title="twisted.python.compat">twisted.python.compat</a></code>. There are some
+ differences between <code>sets.Set</code> and <code>set</code>, that are
+ explained in the <a href="http://www.python.org/dev/peps/pep-0218/" shape="rect">set
+ PEP</a>. Please be sure to not rely on the behavior of one or the other
+ implementation.</p>
+
+ <h2>Classes<a name="auto15"/></h2>
+
+ <p>Classes are to be named in mixed case, with the first letter
+ capitalized; each word separated by having its first letter
+ capitalized. Acronyms should be capitalized in their entirety.
+ Class names should not be prefixed with the name of the module they are
+ in. Examples of classes meeting this criteria:</p>
+
+ <ul>
+ <li>twisted.spread.pb.ViewPoint</li>
+ <li>twisted.parser.patterns.Pattern</li>
+ </ul>
+
+ <p>Examples of classes <strong>not</strong> meeting this criteria:</p>
+
+ <ul>
+ <li>event.EventHandler</li>
+ <li>main.MainGadget</li>
+ </ul>
+
+ <p>An effort should be made to prevent class names from clashing
+ with each other between modules, to reduce the need for
+ qualification when importing. For example, a Service subclass
+ for Forums might be named twisted.forum.service.ForumService,
+ and a Service subclass for Words might be
+ twisted.words.service.WordsService. Since neither of these
+ modules are volatile <em>(see above)</em> the classes may be
+ imported directly into the user's namespace and not cause
+ confusion.</p>
+
+ <h3>New-style Classes<a name="auto16"/></h3>
+
+ <p>Classes and instances in Python come in two flavors: old-style or
+ classic, and new-style. Up to Python 2.1, old-style classes were the
+ only flavour available to the user, new-style classes were introduced
+ in Python 2.2 to unify classes and types. All classes added to Twisted
+ should be written as new-style classes. If <code class="python">x</code>
+ is an instance of a new-style class, then <code class="python">type(x)</code>
+ is the same as <code class="python">x.__class__</code>.</p>
+
+ <h2>Methods<a name="auto17"/></h2>
+
+ <p>Methods should be in mixed case, with the first letter lower
+ case, each word separated by having its first letter
+ capitalized. For example, <code>someMethodName</code>,
+ <code>method</code>.</p>
+
+ <p>Sometimes, a class will dispatch to a specialized sort of
+ method using its name; for example, twisted.reflect.Accessor.
+ In those cases, the type of method should be a prefix in all
+ lower-case with a trailing underscore, so method names will
+ have an underscore in them. For example, <code>get_someAttribute</code>.
+ Underscores in method names in twisted code are therefore
+ expected to have some semantic associated with them.</p>
+
+ <p>Some methods, in particular <code>addCallback</code> and its
+ cousins return self to allow for chaining calls. In this case,
+ wrap the chain in parenthesis, and start each chained call on
+ a separate line, for example:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">return</span> (<span class="py-src-variable">foo</span>()
+ .<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">bar</span>)
+ .<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">thud</span>)
+ .<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">wozers</span>))
+</pre>
+
+ <h2>Callback Arguments<a name="auto18"/></h2>
+
+ <p>There are several methods whose purpose is to help the user set up
+ callback functions, for example <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.addCallback.html" title="twisted.internet.defer.Deferred.addCallback">Deferred.addCallback</a></code> or the
+ reactor's <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.base.ReactorBase.callLater.html" title="twisted.internet.base.ReactorBase.callLater">callLater</a></code> method. To make
+ access to the callback as transparent as possible, most of these methods
+ use <code class="python">**kwargs</code> to capture arbitrary arguments
+ that are destined for the user's callback. This allows the call to the
+ setup function to look very much like the eventual call to the target
+ callback function.</p>
+
+ <p>In these methods, take care to not have other argument names that will
+ <q>steal</q> the user's callback's arguments. When sensible, prefix these
+ <q>internal</q> argument names with an underscore. For example, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.callRemote.html" title="twisted.spread.pb.RemoteReference.callRemote">RemoteReference.callRemote</a></code> is
+ meant to be called like this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-variable">myref</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;addUser&quot;</span>, <span class="py-src-string">&quot;bob&quot;</span>, <span class="py-src-string">&quot;555-1212&quot;</span>)
+
+<span class="py-src-comment"># on the remote end, the following method is invoked:</span>
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">addUser</span>(<span class="py-src-parameter">name</span>, <span class="py-src-parameter">phone</span>):
+ ...
+</pre>
+
+ <p>where <q>addUser</q> is the remote method name. The user might also
+ choose to call it with named parameters like this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">myref</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;addUser&quot;</span>, <span class="py-src-variable">name</span>=<span class="py-src-string">&quot;bob&quot;</span>, <span class="py-src-variable">phone</span>=<span class="py-src-string">&quot;555-1212&quot;</span>)
+</pre>
+
+ <p>In this case, <code>callRemote</code> (and any code that uses the
+ <code class="python">**kwargs</code> syntax) must be careful to not use
+ <q>name</q>, <q>phone</q>, or any other name that might overlap with
+ a user-provided named parameter. Therefore, <code>callRemote</code> is
+ implemented with the following signature:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SomeClass</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callRemote</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">_name</span>, *<span class="py-src-parameter">args</span>, **<span class="py-src-parameter">kw</span>):
+ ...
+</pre>
+
+ <p>Do whatever you can to reduce user confusion. It may also be
+ appropriate to <code class="python">assert</code> that the kwargs
+ dictionary does not contain parameters with names that will eventually
+ cause problems.</p>
+
+
+ <h2>Special Methods<a name="auto19"/></h2>
+
+ <p>The augmented assignment protocol, defined by <code class="python">__iadd__</code> and other
+ similarly named methods, can be used to allow objects to be modified in
+ place or to rebind names if an object is immutable -- both through use
+ of the same operator. This can lead to confusing code, which in turn
+ leads to buggy code. For this reason, methods of the augmented
+ assignment protocol should not be used in Twisted.</p>
+
+ <h2>Functions<a name="auto20"/></h2>
+
+ <p>Functions should be named similiarly to methods.</p>
+
+ <p>Functions or methods which are responding to events to
+ complete a callback or errback should be named <code>_cbMethodName</code> or
+ <code>_ebMethodName</code>, in order to distinguish them from normal
+ methods.</p>
+
+ <h2>Attributes<a name="auto21"/></h2>
+
+ <p>Attributes should be named similarly to functions and
+ methods. Attributes should be named descriptively; attribute
+ names like <code>mode</code>, <code>type</code>, and
+ <code>buf</code> are generally discouraged. Instead, use
+ <code>displayMode</code>, <code>playerType</code>, or
+ <code>inputBuffer</code>.</p>
+
+ <p>Do not use Python's <q>private</q> attribute syntax; prefix
+ non-public attributes with a single leading underscore. Since
+ several classes have the same name in Twisted, and they are
+ distinguished by which package they come from, Python's
+ double-underscore name mangling will not work reliably in some
+ cases. Also, name-mangled private variables are more difficult
+ to address when unit testing or persisting a class.</p>
+
+ <p>An attribute (or function, method or class) should be
+ considered private when one or more of the following conditions
+ are true:</p>
+
+ <ul>
+ <li>The attribute represents intermediate state which is not
+ always kept up-to-date.</li>
+
+ <li>Referring to the contents of the attribute or otherwise
+ maintaining a reference to it may cause resources to
+ leak.</li>
+
+ <li>Assigning to the attribute will break internal
+ assumptions.</li>
+
+ <li>The attribute is part of a known-to-be-sub-optimal
+ interface and will certainly be removed in a future
+ release.</li>
+ </ul>
+
+
+ <h2>Database<a name="auto22"/></h2>
+
+ <p>Database tables will be named with plural nouns.</p>
+
+ <p>Database columns will be named with underscores between
+ words, all lower case, since most databases do not distinguish
+ between case.</p>
+
+ <p>Any attribute, method argument, or method name that
+ corresponds <em>directly</em> to a column in the database will
+ be named exactly the same as that column, regardless of other
+ coding conventions surrounding that circumstance.</p>
+
+ <p>All SQL keywords should be in upper case.</p>
+
+ <h2>C Code<a name="auto23"/></h2>
+
+ <p>Wherever possible, C code should be optional, and the
+ default python implementation should be maintained in tandem
+ with it. C code should be strict ANSI C, and
+ <strong>must</strong> build using GCC as well as Visual Studio
+ for Windows, and really shouldn't have any problems with other
+ compilers either. Don't do anything tricky.</p>
+
+ <p>C code should only be used for efficiency, not for binding
+ to external libraries. If your particular code is not
+ frequently run, write it in Python. If you require the use of
+ an external library, develop a separate, external bindings
+ package and make your twisted code depend on it.</p>
+
+ <h2 id="commits">Commit Messages<a name="auto24"/></h2>
+
+ <p>The commit messages are being distributed in a myriad of ways. Because
+ of that, you need to observe a few simple rules when writing a commit
+ message.</p>
+
+ <p>The first line of the message is being used as both the subject of
+ the commit email and the announcement on #twisted. Therefore, it should
+ be short (aim for &lt; 80 characters) and descriptive -- and must be
+ able to stand alone (it is best if it is a complete sentence). The rest
+ of the e-mail should be separated with <em>hard line breaks</em> into
+ short lines (&lt; 70 characters). This is free-format, so you can do
+ whatever you like here.</p>
+
+ <p>Commit messages should be about <em>what</em>, not <em>how</em>: we can
+ get how from SVN diff. Explain reasons for commits, and what they
+ affect.</p>
+
+ <p>Each commit should be a single logical change, which is internally
+ consistent. If you can't summarize your changes in one short line, this
+ is probably a sign that they should be broken into multiple checkins.</p>
+
+ <h2>Source Control<a name="auto25"/></h2>
+
+ <p>Twisted currently uses Subversion for source control. All
+ development <strong>should</strong> occur using branches; when a task is
+ considered complete another Twisted developer may review it and if no
+ problems are found, it may be merged into trunk. The Twisted wiki has <a href="http://twistedmatrix.com/trac/wiki/TwistedDevelopment" shape="rect">a start</a>.
+ Branches <strong>must</strong> be used for major development. Branches
+ should be managed using <a href="http://divmod.org/trac/wiki/DivmodCombinator" shape="rect">Combinator</a> (but
+ if you can manage them in some other way without anyone noticing, knock
+ yourself out).</p>
+
+ <p>Certain features of Subversion should be avoided.</p>
+
+ <ul>
+ <li>
+
+ <p>Do not set the <code class="shell">svn:ignore</code> property on any
+ file or directory. What you wish to ignore, others may wish to examine.
+ What others may wish you ignore, <em>you</em> may wish you examine.
+ <code class="shell"> svn:ignore </code> will affect everyone who uses
+ the repository, and so it is not the right mechanism to express personal
+ preferences.</p>
+
+ <p>If you wish to ignore certain files use the <code class="shell">
+ global-ignores </code> feature of <code class="shell">
+ ~/.subversion/config </code>, for example:</p>
+
+ <pre class="shell" xml:space="preserve">
+[miscellany]
+global-ignores = dropin.cache *.pyc *.pyo *.o *.lo *.la #*# .*.rej *.rej .*~
+ </pre>
+
+ </li>
+ </ul>
+
+ <h2>Fallback<a name="auto26"/></h2>
+
+ <p>In case of conventions not enforced in this document, the reference
+ documents to use in fallback is
+ <a href="http://www.python.org/dev/peps/pep-0008/" shape="rect">PEP 8</a> for Python
+ code and <a href="http://www.python.org/dev/peps/pep-0007/" shape="rect">PEP 7</a> for
+ C code. For example, the paragraph <strong>Whitespace in Expressions and
+ Statements</strong> in PEP 8 describes what should be done in Twisted
+ code.</p>
+
+ <h2>Recommendations<a name="auto27"/></h2>
+
+ <p>These things aren't necessarily standardizeable (in that
+ code can't be easily checked for compliance) but are a good
+ idea to keep in mind while working on Twisted.</p>
+
+ <p>If you're going to work on a fragment of the Twisted
+ codebase, please consider finding a way that you would <em>use</em>
+ such a fragment in daily life. Using a Twisted Web server on your
+ website encourages you to actively maintain and improve your code,
+ as the little everyday issues with using it become apparent.</p>
+
+ <p>Twisted is a <strong>big</strong> codebase! If you're
+ refactoring something, please make sure to recursively grep for
+ the names of functions you're changing. You may be surprised to
+ learn where something is called. Especially if you are moving
+ or renaming a function, class, method, or module, make sure
+ that it won't instantly break other code.</p>
+
+ </div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/doc-standard.html b/doc/core/development/policy/doc-standard.html
new file mode 100644
index 0000000..2eda492
--- /dev/null
+++ b/doc/core/development/policy/doc-standard.html
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: HTML Documentation Standard for Twisted</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">HTML Documentation Standard for Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Allowable Tags</a></li><li><a href="#auto1">Multi-line Code Snippets</a></li><ul><li><a href="#auto2">python</a></li><li><a href="#auto3">python-interpreter</a></li><li><a href="#auto4">shell</a></li></ul><li><a href="#auto5">Code inside paragraph text</a></li><li><a href="#auto6">Headers</a></li><li><a href="#auto7">XHTML</a></li><li><a href="#auto8">Tag Case</a></li><li><a href="#auto9">Footnotes</a></li><li><a href="#auto10">Suggestions</a></li><li><a href="#auto11">__all__</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Allowable Tags<a name="auto0"/></h2>
+
+ <p>Please try to restrict your HTML usage to the following tags (all only for the original logical purpose, and not whatever visual effect you see): <code>&lt;html&gt;</code>, <code>&lt;title&gt;</code>, <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code>, <code>&lt;h1&gt;</code>, <code>&lt;h2</code>, <code>&lt;h3&gt;</code>, <code>&lt;ol&gt;</code>, <code>&lt;ul&gt;</code>, <code>&lt;dl&gt;</code>, <code>&lt;li&gt;</code>, <code>&lt;dt&gt;</code>, <code>&lt;dd&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;code&gt;</code>, <code>&lt;img&gt;</code>, <code>&lt;blockquote&gt;</code>, <code>&lt;a&gt;</code>, <code>&lt;cite&gt;</code>, <code>&lt;div&gt;</code>, <code>&lt;span&gt;</code>, <code>&lt;strong&gt;</code>, <code>&lt;em&gt;</code>, <code>&lt;pre&gt;</code>, <code>&lt;q&gt;</code>, <code>&lt;table&gt;</code>, <code>&lt;tr&gt;</code>, <code>&lt;td&gt;</code> and <code>&lt;th&gt;</code>.</p>
+
+ <p>Please avoid using the quote sign (<code>&quot;</code>) for quoting, and use the relevant html tags (<code>&lt;q&gt;&lt;/q&gt;</code>) -- it is impossible to distinguish right and left quotes with the quote sign, and some more sophisticated output methods work better with that distinction.</p>
+
+ <h2>Multi-line Code Snippets<a name="auto1"/></h2>
+
+ <p>Multi-line code snippets should be delimited with a
+ &lt;pre&gt; tag, with a mandatory <q>class</q> attribute. The
+ conventionalized classes are <q>python</q>, <q>python-interpreter</q>,
+ and <q>shell</q>. For example:</p>
+
+ <h3><q>python</q><a name="auto2"/></h3>
+ <p>Original markup:</p>
+ <blockquote>
+<pre xml:space="preserve">
+&lt;p&gt;
+For example, this is how one defines a Resource:
+&lt;/p&gt;
+
+&lt;pre class=&quot;python&quot;&gt;
+from twisted.web import resource
+
+class MyResource(resource.Resource):
+ def render_GET(self, request):
+ return &quot;Hello, world!&quot;
+&lt;/pre&gt;
+</pre>
+ </blockquote>
+
+ <p>Rendered result:</p>
+ <blockquote>
+ <p>For example, this is how one defines a Resource:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyResource</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Hello, world!&quot;</span>
+</pre>
+ </blockquote>
+
+ <p>Note that you should never have leading indentation inside a
+ &lt;pre&gt; block -- this makes it hard for readers to
+ copy/paste the code.</p>
+
+ <h3><q>python-interpreter</q><a name="auto3"/></h3>
+ <p>Original markup:</p>
+ <blockquote>
+<pre xml:space="preserve">
+&lt;pre class=&quot;python-interpreter&quot;&gt;
+&amp;gt;&amp;gt;&amp;gt; from twisted.web import resource
+&amp;gt;&amp;gt;&amp;gt; class MyResource(resource.Resource):
+... def render_GET(self, request):
+... return &quot;Hello, world!&quot;
+...
+&amp;gt;&amp;gt;&amp;gt; MyResource().render_GET(None)
+&quot;Hello, world!&quot;
+&lt;/pre&gt;
+</pre>
+ </blockquote>
+
+ <p>Rendered result:</p>
+ <blockquote>
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; from twisted.web import resource
+&gt;&gt;&gt; class MyResource(resource.Resource):
+... def render_GET(self, request):
+... return &quot;Hello, world!&quot;
+...
+&gt;&gt;&gt; MyResource().render_GET(None)
+&quot;Hello, world!&quot;
+</pre>
+ </blockquote>
+
+ <h3><q>shell</q><a name="auto4"/></h3>
+ <p>Original markup:</p>
+ <blockquote>
+<pre xml:space="preserve">
+ &lt;pre class=&quot;shell&quot;&gt;
+ $ twistd web --path /var/www
+ &lt;/pre&gt;
+</pre>
+ </blockquote>
+
+ <p>Rendered result:</p>
+ <blockquote>
+<pre class="shell" xml:space="preserve">
+$ twistd web --path /var/www
+</pre>
+ </blockquote>
+
+ <h2>Code inside paragraph text<a name="auto5"/></h2>
+
+ <p>For single-line code-snippets and attribute, method, class,
+ and module names, use the &lt;code&gt; tag, with a class of
+ <q>API</q> or <q>python</q>. During processing, module or class-names
+ with class <q>API</q> will automatically be looked up in the API
+ reference and have a link placed around it referencing the
+ actual API documents for that module/classname. If you wish to
+ reference an API document, then make sure you at least have a
+ single module-name so that the processing code will be able to
+ figure out which module or class you're referring to.</p>
+
+ <p>You may also use the <code>base</code> attribute in conjuction
+ with a class of <q>API</q> to indicate the module that should be prepended
+ to the module or classname. This is to help keep the documentation
+ clearer and less cluttered by allowing links to API docs that don't
+ need the module name.</p>
+ <p>Original markup:</p>
+ <blockquote>
+<pre xml:space="preserve">
+ &lt;p&gt;
+ To add a &lt;code class=&quot;API&quot;&gt;twisted.web.static.File&lt;/code&gt;
+ instance to a &lt;code class=&quot;API&quot;
+ base=&quot;twisted.web.resource&quot;&gt;Resource&lt;/code&gt; instance, do
+ &lt;code class=&quot;python&quot;&gt;myResource.putChild(&quot;resourcePath&quot;,
+ File(&quot;/tmp&quot;))&lt;/code&gt;.
+ &lt;/p&gt;
+
+</pre>
+ </blockquote>
+
+ <p>Rendered result:</p>
+ <blockquote>
+ <p>
+ To add a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.File.html" title="twisted.web.static.File">twisted.web.static.File</a></code>
+ instance to a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>
+ instance, do
+ <code class="python">myResource.putChild(&quot;resourcePath&quot;, File(&quot;/tmp&quot;))</code>.
+ </p>
+ </blockquote>
+
+ <h2>Headers<a name="auto6"/></h2>
+
+ <p>It goes without mentioning that you should use &lt;hN&gt; in
+ a sane way -- &lt;h1&gt; should only appear once in the
+ document, to specify the title. Sections of the document should
+ use &lt;h2&gt;, sub-headers &lt;h3&gt;, and so on.</p>
+
+ <h2>XHTML<a name="auto7"/></h2>
+
+ <p>XHTML is mandatory. That means tags that don't have a
+ closing tag need a <q>/</q>; for example, <code>&lt;hr /&gt;</code>
+ . Also, tags which have <q>optional</q> closing tags in HTML
+ <em>need</em> to be closed in XHTML; for example,
+ <code>&lt;li&gt;foo&lt;/li&gt;</code></p>
+
+ <h2>Tag Case<a name="auto8"/></h2>
+
+ <p>All tags will be done in lower-case. XHTML demands this, and
+ so do I. :-)</p>
+
+ <h2>Footnotes<a name="auto9"/></h2>
+
+ <p>Footnotes are enclosed inside
+ <code>&lt;span class=&quot;footnote&quot;&gt;&lt;/span&gt;</code>. They must not
+ contain any markup.</p>
+
+ <h2>Suggestions<a name="auto10"/></h2>
+
+ <p>Use <code class="shell">lore -o lint</code> to check your documentation
+ is not broken. <code class="shell">lore -o lint</code> will never change
+ your HTML, but it will complain if it doesn't like it.</p>
+
+ <p>Don't use tables for formatting. 'nuff said.</p>
+
+ <h2>__all__<a name="auto11"/></h2>
+
+ <p><code class="python">__all__</code> is a module level list of strings, naming
+ objects in the module that are public. Make sure publically exported classes,
+ functions and constants are listed here.</p>
+
+ </div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/index.html b/doc/core/development/policy/index.html
new file mode 100644
index 0000000..d733b2a
--- /dev/null
+++ b/doc/core/development/policy/index.html
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Development Policy</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Development Policy</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<p>
+This series of documents is designed for people who wish to contribute to the
+Twisted codebase.
+</p>
+
+ <ul>
+ <li><a href="coding-standard.html" shape="rect">Coding standard</a></li>
+ <li><a href="doc-standard.html" shape="rect">Documentation standard</a></li>
+ <li><a href="writing-standard.html" shape="rect">Documentation writing standard</a></li>
+ <li><a href="test-standard.html" shape="rect">Testing standard</a></li>
+ <li><a href="svn-dev.html" shape="rect">Working from Twisted's Subversion
+ repository</a></li>
+ </ul>
+
+</div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/svn-dev.html b/doc/core/development/policy/svn-dev.html
new file mode 100644
index 0000000..1e6c74c
--- /dev/null
+++ b/doc/core/development/policy/svn-dev.html
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Working from Twisted's Subversion repository</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Working from Twisted's Subversion repository</h1>
+ <div class="toc"><ol><li><a href="#auto0">Checkout</a></li><li><a href="#auto1">Alternate tree names</a></li><li><a href="#auto2">Combinator</a></li><li><a href="#auto3">Compiling C extensions</a></li><li><a href="#auto4">Running tests</a></li><li><a href="#auto5">Building docs</a></li><li><a href="#auto6">Committing and Post-commit Hooks</a></li><li><a href="#auto7">Emacs</a></li><li><a href="#auto8">Building Debian packages</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<p>If you're going to be doing development on Twisted itself, or if you want
+to take advantage of bleeding-edge features (or bug fixes) that are not yet
+available in a numbered release, you'll probably want to check out a tree from
+the Twisted Subversion repository. The Trunk is where all current development
+takes place.</p>
+
+<p>This document lists some useful tips for working on this cutting
+edge.</p>
+
+<h2>Checkout<a name="auto0"/></h2>
+
+<p>Subversion tutorials can be found elsewhere, see in particular
+<a href="http://subversion.apache.org/" shape="rect">the Subversion homepage</a>. The
+relevant data you need to check out a copy of the Twisted tree is available on
+the <a href="http://twistedmatrix.com/trac/wiki/TwistedDevelopment" shape="rect">development
+page</a>, and is as follows:</p>
+
+<pre class="shell" xml:space="preserve">
+$ svn co svn://svn.twistedmatrix.com/svn/Twisted/trunk Twisted
+</pre>
+
+<h2>Alternate tree names<a name="auto1"/></h2>
+
+<p>By using <code>svn co svn://svn.twistedmatrix.com/svn/Twisted/trunk
+otherdir</code>, you can put the workspace tree in a directory other than
+<q>Twisted</q>. I do this (with a name like <q>Twisted-Subversion</q>) to
+remind myself that this tree comes from Subversion and not from a released
+version (like <q>Twisted-1.0.5</q>). This practice can cause a few problems,
+because there are a few places in the Twisted tree that need to know where
+the tree starts, so they can add it to <code>sys.path</code> without
+requiring the user manually set their PYTHONPATH. These functions walk the
+current directory up to the root, looking for a directory named
+<q>Twisted</q> (sometimes exactly that, sometimes with a
+<code>.startswith</code> test). Generally these are test scripts or other
+administrative tools which expect to be launched from somewhere inside the
+tree (but not necessarily from the top).</p>
+
+<p>If you rename the tree to something other than <code>Twisted</code>, these
+tools may wind up trying to use Twisted source files from /usr/lib/python2.5
+or elsewhere on the default <code>sys.path</code>. Normally this won't
+matter, but it is good to be aware of the issue in case you run into
+problems.</p>
+
+<p><code>twisted/test/process_twisted.py</code> is one of these programs.</p>
+
+<h2>Combinator<a name="auto2"/></h2>
+
+<p>In order to simplify the use of Subversion, we typically use
+<a href="http://twistedmatrix.com/trac/wiki/Combinator" shape="rect">Divmod Combinator</a>.
+You may find it to be useful, too. In particular, because Twisted uses
+branches for almost all feature development, if you plan to contribute to
+Twisted you will probably find Combinator very useful. For more details,
+see the Combinator website, as well as the
+<a href="http://twistedmatrix.com/trac/wiki/UltimateQualityDevelopmentSystem" shape="rect">
+UQDS</a> page.</p>
+
+<h2>Compiling C extensions<a name="auto3"/></h2>
+
+<p>
+There are currently several C extension modules in Twisted:
+<code class="python">twisted.internet.cfsupport</code>,
+<code class="python">twisted.internet.iocpreactor._iocp</code>,
+and <code class="python">twisted.python._epoll</code>. These modules
+are optional, but you'll have to compile them if you want to experience their
+features, performance improvements, or bugs. There are two approaches.
+</p>
+
+<p>The first is to do a regular distutils <code>./setup.py build</code>, which
+will create a directory under <code>build/</code> to hold both the generated
+<code>.so</code> files as well as a copy of the 600-odd <code>.py</code> files
+that make up Twisted. If you do this, you will need to set your PYTHONPATH to
+something like <code>MyDir/Twisted/build/lib.linux-i686-2.5</code> in order to
+run code against the Subversion twisted (as opposed to whatever's installed in
+<code>/usr/lib/python2.5</code> or wherever python usually looks). In
+addition, you will need to re-run the <code>build</code> command <em>every
+time</em> you change a <code>.py</code> file. The <code>build/lib.foo</code>
+directory is a copy of the main tree, and that copy is only updated when you
+re-run <code>setup.py build</code>. It is easy to forget this and then wonder
+why your code changes aren't being expressed.</p>
+
+<p>The second technique is to build the C modules in place, and point your
+PYTHONPATH at the top of the tree, like <code>MyDir/Twisted</code>. This way
+you're using the .py files in place too, removing the confusion a forgotten
+rebuild could cause with the separate build/ directory above. To build the C
+modules in place, do <code>./setup.py build_ext -i</code>. You only need to
+re-run this command when you change the C files. Note that
+<code>setup.py</code> is not Make, it does not always get the dependencies
+right (<code>.h</code> files in particular), so if you are hacking on the
+cReactor you may need to manually delete the <code>.o</code> files before
+doing a rebuild. Also note that doing a <code>setup.py clean</code> will
+remove the <code>.o</code> files but not the final <code>.so</code> files,
+they must be deleted by hand.</p>
+
+
+<h2>Running tests<a name="auto4"/></h2>
+
+<p>To run the full unit-test suite, do:</p>
+
+<pre class="shell" xml:space="preserve">./bin/trial twisted</pre>
+
+<p>To run a single test file (like <code>twisted/test/test_defer.py</code>),
+do one of:</p>
+
+<pre class="shell" xml:space="preserve">./bin/trial twisted.test.test_defer</pre>
+
+<p>or</p>
+
+<pre class="shell" xml:space="preserve">./bin/trial twisted/test/test_defer.py</pre>
+
+<p>To run any tests that are related to a code file, like
+<code>twisted/protocols/imap4.py</code>, do:</p>
+
+<pre class="shell" xml:space="preserve">./bin/trial --testmodule twisted/mail/imap4.py</pre>
+
+<p>This depends upon the <code>.py</code> file having an appropriate
+<q>test-case-name</q> tag that indicates which test cases provide coverage.
+See the <a href="test-standard.html" shape="rect">Test Standards</a> document for
+details about using <q>test-case-name</q>. In this example, the
+<code>twisted.mail.test.test_imap</code> test will be run.</p>
+
+<p>Many tests create temporary files in /tmp or ./_trial_temp, but
+everything in /tmp should be deleted when the test finishes. Sometimes these
+cleanup calls are commented out by mistake, so if you see a stray
+<code>/tmp/@12345.1</code> directory, it is probably from <code>test_dirdbm</code> or <code>test_popsicle</code>.
+Look for an <code>rmtree</code> that has been commented out and complain to
+the last developer who touched that file.</p>
+
+<h2>Building docs<a name="auto5"/></h2>
+
+<p>Twisted documentation (not including the automatically-generated API docs)
+is in <a href="http://twistedmatrix.com/trac/wiki/TwistedLore" shape="rect">Lore Format</a>.
+These <code>.xhtml</code> files are translated into <code>.html</code> files by
+the <q>bin/lore/lore</q> script, which can check the files for syntax problems
+(hlint), process multiple files at once, insert the files into a template
+before processing, and can also translate the files into LaTeX or PostScript
+instead.</p>
+
+<p>To build the HTML form of the howto/ docs, do the following. Note that
+the index file will be placed in <code>doc/core/howto/index.html</code>.</p>
+
+<pre class="shell" xml:space="preserve">
+./bin/lore/lore -p --config template=doc/core/howto/template.tpl doc/core/howto/*.xhtml
+</pre>
+
+<p>To run hlint over a single Lore document, such as
+<code>doc/development/policy/svn-dev.xhtml</code>, do the following. This is
+useful because the HTML conversion may bail without a useful explanation if
+it sees mismatched tags.</p>
+
+<pre class="shell" xml:space="preserve">
+./bin/lore/lore -n --output lint doc/development/policy/svn-dev.xhtml
+</pre>
+
+<p>To convert it to HTML (including markup, interpolation of examples,
+footnote processing, etc), do the following. The results will be placed in
+<code>doc/development/policy/svn-dev.html</code>:</p>
+
+<pre class="shell" xml:space="preserve">
+./bin/lore/lore -p --config template=doc/core/howto/template.tpl \
+ doc/development/policy/svn-dev.xhtml
+</pre>
+
+<p>Note that hyperlinks to other documents may not be quite right unless you
+include a <q>-l</q> argument to <code>bin/lore/lore</code>. Links in the
+.xhtml file are to .xhtml targets: when the .xhtml is turned into .html, the
+link targets are supposed to be turned into .html also. In addition to this,
+Lore markup of the form <code>&lt;code class=&quot;API&quot;&gt;</code> is supposed to
+turn into a link to the corresponding API reference page. These links will
+probably be wrong unless the correct base URL is provided to Lore.</p>
+
+<h2>Committing and Post-commit Hooks<a name="auto6"/></h2>
+
+<p>Twisted uses a customized
+<a href="http://bazaar.launchpad.net/~exarkun/twisted-trac-integration/trunk/annotate/head%3A/trac-hooks/trac-post-commit-hook" shape="rect">
+trac-post-commit-hook</a> to enable ticket updates based on svn commit
+logs. When making a branch for a ticket, the branch name should end
+in <code>-&lt;ticket number&gt;</code>, for
+example <code>my-branch-9999</code>. This will add a ticket comment containing a
+changeset link and branch name. To make your commit message show up as a comment
+on a Trac ticket, add a <code>refs #&lt;ticket number&gt;</code> line at the
+bottom of your commit message. To automatically close a ticket on Trac
+as <code>Fixed</code> and add a comment with the closing commit message, add
+a <code>Fixes: #&lt;ticket number&gt;</code> line to your commit message. In
+general, a commit message closing a ticket looks like this:</p>
+
+<pre xml:space="preserve">
+Merge my-branch-9999: A single-line summary.
+
+Author: jesstess
+Reviewers: exarkun, glyph
+Fixes: #9999
+
+My longer description of the changes made.
+</pre>
+
+<p>The <a href="coding-standard.html" shape="rect">Twisted Coding Standard</a>
+elaborates on commit messages and source control.</p>
+
+<h2>Emacs<a name="auto7"/></h2>
+
+<p>A minor mode for development with Twisted using Emacs is available. See
+<code>twisted-dev.el</code>, provided by <a href="http://launchpad.net/twisted-emacs" shape="rect">twisted-emacs</a>,
+for several utility functions which make it easier to grep for methods, run test cases, etc.</p>
+
+<h2>Building Debian packages<a name="auto8"/></h2>
+
+<p>Our support for building Debian packages has fallen into disrepair. We
+would very much like to restore this functionality, but until we do so, if
+you are interested in this, you are on your own. See
+<a href="http://github.com/astraw/stdeb" shape="rect">stdeb</a> for one possible approach to
+this.</p>
+
+</div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/test-standard.html b/doc/core/development/policy/test-standard.html
new file mode 100644
index 0000000..9539356
--- /dev/null
+++ b/doc/core/development/policy/test-standard.html
@@ -0,0 +1,399 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Unit Tests in Twisted</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Unit Tests in Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Unit Tests in the Twisted Philosophy</a></li><li><a href="#auto1">What to Test, What Not to Test</a></li><li><a href="#auto2">Running the Tests</a></li><ul><li><a href="#auto3">How</a></li><li><a href="#auto4">When</a></li></ul><li><a href="#auto5">Adding a Test</a></li><li><a href="#auto6">Test Implementation Guidelines</a></li><ul><li><a href="#auto7">Real I/O</a></li><li><a href="#auto8">Real Time</a></li><li><a href="#auto9">The Global Reactor</a></li></ul><li><a href="#auto10">Skipping tests, TODO items</a></li><ul><li><a href="#auto11">.todo and Testing New Functionality </a></li><li><a href="#auto12">Line Coverage Information</a></li></ul><li><a href="#auto13">Associating Test Cases With Source Files</a></li><li><a href="#auto14">Links</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>Each <em>unit test</em> tests one bit of functionality in the
+ software. Unit tests are entirely automated and complete quickly.
+ Unit tests for the entire system are gathered into one test suite,
+ and may all be run in a single batch. The result of a unit test
+ is simple: either it passes, or it doesn't. All this means you
+ can test the entire system at any time without inconvenience, and
+ quickly see what passes and what fails.</p>
+
+ <h2>Unit Tests in the Twisted Philosophy<a name="auto0"/></h2>
+
+ <p>The Twisted development team adheres to the practice of <a href="http://c2.com/cgi/wiki?ExtremeProgramming" shape="rect">Extreme Programming</a> (XP),
+ and the usage of unit tests is a cornerstone XP practice. Unit tests are a
+ tool to give you increased confidence. You changed an algorithm -- did you
+ break something? Run the unit tests. If a test fails, you know where to
+ look, because each test covers only a small amount of code, and you know it
+ has something to do with the changes you just made. If all the tests pass,
+ you're good to go, and you don't need to second-guess yourself or worry that
+ you just accidentally broke someone else's program.</p>
+
+ <h2>What to Test, What Not to Test<a name="auto1"/></h2>
+
+ <blockquote><p>You don't have to write a test for every single
+ method you write, only production methods that could possibly break.</p>
+ </blockquote>
+
+ <p>-- Kent Beck, <cite>Extreme Programming Explained</cite>, p. 58.</p>
+
+ <h2>Running the Tests<a name="auto2"/></h2>
+
+ <h3>How<a name="auto3"/></h3>
+
+ <p>From the root of the Twisted source tree, run
+ <a href="http://twistedmatrix.com/trac/wiki/TwistedTrial" shape="rect">Trial</a>:
+ </p>
+
+ <pre class="shell" xml:space="preserve">
+$ bin/trial twisted
+ </pre>
+
+ <p>You'll find that having something like this in your emacs init
+ files is quite handy:</p>
+
+<pre class="elisp" xml:space="preserve">
+(defun runtests () (interactive)
+ (compile &quot;python /somepath/Twisted/bin/trial /somepath/Twisted&quot;))
+
+(global-set-key [(alt t)] 'runtests)
+</pre>
+ <h3>When<a name="auto4"/></h3>
+
+ <p>Always, always, <em>always</em> be sure <a href="http://www.xprogramming.com/xpmag/expUnitTestsAt100.htm" shape="rect">all the
+ tests pass</a> before committing any code. If someone else
+ checks out code at the start of a development session and finds
+ failing tests, they will not be happy and may decide to <em>hunt
+ you down</em>.</p>
+
+ <p>Since this is a geographically dispersed team, the person who can help
+ you get your code working probably isn't in the room with you. You may want
+ to share your work in progress over the network, but you want to leave the
+ main Subversion tree in good working order.
+ So <a href="http://svnbook.red-bean.com/en/1.0/ch04.html" shape="rect">use a branch</a>,
+ and merge your changes back in only after your problem is solved and all the
+ unit tests pass again.</p>
+
+ <h2>Adding a Test<a name="auto5"/></h2>
+
+ <p>Please don't add new modules to Twisted without adding tests
+ for them too. Otherwise we could change something which breaks
+ your module and not find out until later, making it hard to know
+ exactly what the change that broke it was, or until after a
+ release, and nobody wants broken code in a release.</p>
+
+ <p>Tests go into dedicated test packages such as
+ <code>twisted/test/</code> or <code>twisted/conch/test/</code>,
+ and are named <code>test_foo.py</code>, where <code>foo</code> is the name
+ of the module or package being tested. Extensive documentation on using
+ the PyUnit framework for writing unit tests can be found in the
+ <a href="#links" shape="rect">links section</a> below.
+ </p>
+
+ <p>One deviation from the standard PyUnit documentation: To ensure
+ that any variations in test results are due to variations in the
+ code or environment and not the test process itself, Twisted ships
+ with its own, compatible, testing framework. That just
+ means that when you import the unittest module, you will <code class="python">from twisted.trial import unittest</code> instead of the
+ standard <code class="python">import unittest</code>.</p>
+
+ <p>As long as you have followed the module naming and placement
+ conventions, <code class="shell">trial</code> will be smart
+ enough to pick up any new tests you write.</p>
+
+ <p>PyUnit provides a large number of assertion methods to be used when
+ writing tests. Many of these are redundant. For consistency, Twisted
+ unit tests should use the <code>assert</code> forms rather than the
+ <code>fail</code> forms. Also, use <code>assertEqual</code>,
+ <code>assertNotEqual</code>, and <code>assertAlmostEqual</code> rather
+ than <code>assertEquals</code>, <code>assertNotEquals</code>, and
+ <code>assertAlmostEquals</code>. <code>assertTrue</code> is also
+ preferred over <code>assert_</code>. You may notice this convention is
+ not followed everywhere in the Twisted codebase. If you are changing
+ some test code and notice the wrong method being used in nearby code,
+ feel free to adjust it.</p>
+
+ <p>When you add a unit test, make sure all methods have docstrings
+ specifying at a high level the intent of the test. That is, a description
+ that users of the method would understand.</p>
+
+ <h2>Test Implementation Guidelines<a name="auto6"/></h2>
+
+ <p>Here are some guidelines to follow when writing tests for the Twisted
+ test suite. Many tests predate these guidelines and so do not follow them.
+ When in doubt, follow the guidelines given here, not the example of old unit
+ tests.</p>
+
+ <h3>Real I/O<a name="auto7"/></h3>
+
+ <p>Most unit tests should avoid performing real, platform-implemented I/O
+ operations. Real I/O is slow, unreliable, and unwieldy. When implementing
+ a protocol, <code>twisted.test.proto_helpers.StringTransport</code> can be
+ used instead of a real TCP transport. <code>StringTransport</code> is fast,
+ deterministic, and can easily be used to exercise all possible network
+ behaviors.</p>
+
+ <h3>Real Time<a name="auto8"/></h3>
+
+ <p>Most unit tests should also avoid waiting for real time to pass. Unit
+ tests which construct and advance
+ a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.Clock.html" title="twisted.internet.task.Clock">twisted.internet.task.Clock</a></code> are fast and
+ deterministic.</p>
+
+ <h3>The Global Reactor<a name="auto9"/></h3>
+
+ <p>Since unit tests are avoiding real I/O and real time, they can usually
+ avoid using a real reactor. The only exceptions to this are unit tests for
+ a real reactor implementation. Unit tests for protocol implementations or
+ other application code should not use a reactor. Unit tests for real
+ reactor implementations should not use the global reactor, but should
+ instead use <code>twisted.internet.test.reactormixins.ReactorBuilder</code>
+ so they can be applied to all of the reactor implementations automatically.
+ In no case should new unit tests use the global reactor.</p>
+
+
+<h2>Skipping tests, TODO items<a name="auto10"/></h2>
+
+<p>Trial, the Twisted unit test framework, has some extensions which are
+designed to encourage developers to add new tests. One common situation is
+that a test exercises some optional functionality: maybe it depends upon
+certain external libraries being available, maybe it only works on certain
+operating systems. The important common factor is that nobody considers
+these limitations to be a bug.</p>
+
+<p>To make it easy to test as much as possible, some tests may be skipped in
+certain situations. Individual test cases can raise the
+<code>SkipTest</code> exception to indicate that they should be skipped, and
+the remainder of the test is not run. In the summary (the very last thing
+printed, at the bottom of the test output) the test is counted as a
+<q>skip</q> instead of a <q>success</q> or <q>fail</q>. This should be used
+inside a conditional which looks for the necessary prerequisites:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SSHClientTests</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_sshClient</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">ssh_path</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">unittest</span>.<span class="py-src-variable">SkipTest</span>(<span class="py-src-string">&quot;cannot find ssh, nothing to test&quot;</span>)
+ <span class="py-src-variable">foo</span>() <span class="py-src-comment"># do actual test after the SkipTest</span>
+</pre>
+
+<p>You can also set the <code>.skip</code> attribute on the method, with a
+string to indicate why the test is being skipped. This is convenient for
+temporarily turning off a test case, but it can also be set conditionally (by
+manipulating the class attributes after they've been defined):</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SomeThingTests</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_thing</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">dotest</span>()
+ <span class="py-src-variable">test_thing</span>.<span class="py-src-variable">skip</span> = <span class="py-src-string">&quot;disabled locally&quot;</span>
+</pre>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">MyTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_one</span>(<span class="py-src-parameter">self</span>):
+ ...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_thing</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">dotest</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">haveThing</span>:
+ <span class="py-src-variable">MyTestCase</span>.<span class="py-src-variable">test_thing</span>.<span class="py-src-variable">im_func</span>.<span class="py-src-variable">skip</span> = <span class="py-src-string">&quot;cannot test without Thing&quot;</span>
+ <span class="py-src-comment"># but test_one() will still run</span>
+</pre>
+
+<p>Finally, you can turn off an entire TestCase at once by setting the .skip
+attribute on the class. If you organize your tests by the functionality they
+depend upon, this is a convenient way to disable just the tests which cannot
+be run.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">TCPTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ ...
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SSLTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">haveSSL</span>:
+ <span class="py-src-variable">skip</span> = <span class="py-src-string">&quot;cannot test without SSL support&quot;</span>
+ <span class="py-src-comment"># but TCPTestCase will still run</span>
+ ...
+</pre>
+
+<h3>.todo and Testing New Functionality <a name="auto11"/></h3>
+
+<p>Two good practices which arise from the <q>XP</q> development process are
+sometimes at odds with each other:</p>
+
+<ul>
+ <li>Unit tests are a good thing. Good developers recoil in horror when
+ they see a failing unit test. They should drop everything until the test
+ has been fixed.</li>
+
+ <li>Good developers write the unit tests first. Once tests are done, they
+ write implementation code until the unit tests pass. Then they stop.</li>
+</ul>
+
+<p>These two goals will sometimes conflict. The unit tests that are written
+first, before any implementation has been done, are certain to fail. We want
+developers to commit their code frequently, for reliability and to improve
+coordination between multiple people working on the same problem together.
+While the code is being written, other developers (those not involved in the
+new feature) should not have to pay attention to failures in the new code.
+We should not dilute our well-indoctrinated Failing Test Horror Syndrome by
+crying wolf when an incomplete module has not yet started passing its unit
+tests. To do so would either teach the module author to put off writing or
+committing their unit tests until <em>after</em> all the functionality is
+working, or it would teach the other developers to ignore failing test
+cases. Both are bad things.</p>
+
+<p><q>.todo</q> is intended to solve this problem. When a developer first
+starts writing the unit tests for functionality that has not yet been
+implemented, they can set the <code>.todo</code> attribute on the test
+methods that are expected to fail. These methods will still be run, but
+their failure will not be counted the same as normal failures: they will go
+into an <q>expected failures</q> category. Developers should learn to treat
+this category as a second-priority queue, behind actual test failures.</p>
+
+<p>As the developer implements the feature, the tests will eventually start
+passing. This is surprising: after all those tests are marked as being
+expected to fail. The .todo tests which nevertheless pass are put into a
+<q>unexpected success</q> category. The developer should remove the .todo
+tag from these tests. At that point, they become normal tests, and their
+failure is once again cause for immediate action by the entire development
+team.</p>
+
+<p>The life cycle of a test is thus:</p>
+
+<ol>
+ <li>Test is created, marked <code>.todo</code>. Test fails: <q>expected
+ failure</q>.</li>
+
+ <li>Code is written, test starts to pass. <q>unexpected success</q>.</li>
+
+ <li><code>.todo</code> tag is removed. Test passes. <q>success</q>.</li>
+
+ <li>Code is broken, test starts to fail. <q>failure</q>. Developers spring
+ into action.</li>
+
+ <li>Code is fixed, test passes once more. <q>success</q>.</li>
+</ol>
+
+<p>Any test which remains marked with <code>.todo</code> for too long should
+be examined. Either it represents functionality which nobody is working on,
+or the test is broken in some fashion and needs to be fixed. Generally,
+<code>.todo</code> may be of use while you are developing a feature, but
+by the time you are ready to commit anything, all the tests you have written
+should be passing. In other words, you should rarely, if ever, feel the need
+to add a test marked todo to trunk. When you do, consider whether a ticket
+in the issue tracker would be more useful.</p>
+
+<h3>Line Coverage Information<a name="auto12"/></h3>
+
+<p>Trial provides line coverage information, which is very useful to ensure
+old code has decent coverage. Passing the <code>--coverage</code> option to
+to Trial will generate the coverage information in a file called
+<code>coverage</code> which can be found in the <code>_trial_temp</code>
+folder. This option requires Python 2.3.3 or newer.</p>
+
+<h2>Associating Test Cases With Source Files<a name="auto13"/></h2>
+
+<p>Please add a <code>test-case-name</code> tag to the source file that is
+covered by your new test. This is a comment at the beginning of the file
+which looks like one of the following:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-comment"># -*- test-case-name: twisted.test.test_defer -*-</span>
+</pre>
+
+<p>or</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+<span class="py-src-comment"># -*- test-case-name: twisted.test.test_defer -*-</span>
+</pre>
+
+<p>This format is understood by emacs to mark <q>File Variables</q>. The
+intention is to accept <code>test-case-name</code> anywhere emacs would on
+the first or second line of the file (but not in the <code>File
+Variables:</code> block that emacs accepts at the end of the file). If you
+need to define other emacs file variables, you can either put them in the
+<code>File Variables:</code> block or use a semicolon-separated list of
+variable definitions:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-comment"># -*- test-case-name: twisted.test.test_defer; fill-column: 75; -*-</span>
+</pre>
+
+<p>If the code is exercised by multiple test cases, those may be marked by
+using a comma-separated list of tests, as follows: (NOTE: not all tools can
+handle this yet.. <code>trial --testmodule</code> does, though)</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-comment"># -*- test-case-name: twisted.test.test_defer,twisted.test.test_tcp -*-</span>
+</pre>
+
+<p>The <code>test-case-name</code> tag will allow <code class="shell">trial
+--testmodule twisted/dir/myfile.py</code> to determine which test cases need
+to be run to exercise the code in <code>myfile.py</code>. Several tools (as
+well as <a href="http://launchpad.net/twisted-emacs" shape="rect"/>'s
+<code>twisted-dev.el</code>'s F9 command) use this to automatically
+run the right tests.</p>
+
+<h2 id="links">Links<a name="auto14"/></h2><a name="links" shape="rect"/>
+
+<ul>
+ <li>A chapter on <a href="http://diveintopython.org/unit_testing/index.html" shape="rect">Unit Testing</a>
+ in Mark Pilgrim's <a href="http://diveintopython.org" shape="rect">Dive Into
+ Python</a>.</li>
+
+ <li><a href="http://docs.python.org/library/unittest.html" shape="rect">unittest</a> module documentation, in the <a href="http://docs.python.org/library" shape="rect">Python Library
+ Reference</a>.</li>
+
+ <li><a href="http://c2.com/cgi/wiki?UnitTest" shape="rect">UnitTest</a> on
+ the <a href="http://c2.com/cgi/wiki" shape="rect">PortlandPatternRepository
+ Wiki</a>, where all the cool <a href="http://c2.com/cgi/wiki?ExtremeProgramming" shape="rect">ExtremeProgramming</a> kids hang out.</li>
+
+ <li><a href="http://www.extremeprogramming.org/rules/unittests.html" shape="rect">Unit
+ Tests</a> in <a href="http://www.extremeprogramming.org" shape="rect">Extreme Programming: A Gentle Introduction</a>.</li>
+
+ <li>Ron Jeffries expounds on the importance of <a href="http://www.xprogramming.com/xpmag/expUnitTestsAt100.htm" shape="rect">Unit
+ Tests at 100%</a>.</li>
+
+ <li>Ron Jeffries writes about the <a href="http://www.xprogramming.com/Practices/PracUnitTest.html" shape="rect">Unit
+ Test</a> in the <a href="http://www.xprogramming.com/Practices/xpractices.htm" shape="rect">Extreme
+ Programming practices of C3</a>.</li>
+
+ <li><a href="http://pyunit.sourceforge.net" shape="rect">PyUnit's homepage</a>.</li>
+
+ <li>The top-level tests directory, <a href="http://twistedmatrix.com/trac/browser/trunk/twisted/test" shape="rect">twisted/test</a>, in Subversion.</li>
+
+ </ul>
+
+ <p>See also <a href="../../howto/testing.html" shape="rect">Tips for writing tests for
+ Twisted code</a>.</p>
+
+ </div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/policy/writing-standard.html b/doc/core/development/policy/writing-standard.html
new file mode 100644
index 0000000..1bfaaa4
--- /dev/null
+++ b/doc/core/development/policy/writing-standard.html
@@ -0,0 +1,313 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Writing Standard</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Writing Standard</h1>
+ <div class="toc"><ol><li><a href="#auto0">General style</a></li><li><a href="#auto1">Evangelism and usage documents</a></li><li><a href="#auto2">Descriptions of features</a></li><li><a href="#auto3">Linking</a></li><li><a href="#auto4">Introductions</a></li><ul><li><a href="#auto5">Introductory paragraph</a></li><li><a href="#auto6">Description of target audience</a></li><li><a href="#auto7">Goals of document</a></li></ul><li><a href="#auto8">Example code</a></li><li><a href="#auto9">Conclusions</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>The Twisted writing standard describes the documentation writing
+ styles we prefer in our documentation. This standard applies particularly
+ to howtos and other descriptive documentation.</p>
+
+ <p>This document should be read with the <a href="doc-standard.html" shape="rect">documentation standard</a>, which describes
+ markup style for the documentation.</p>
+
+ <p>This document is meant to help Twisted documentation authors produce
+ documentation that does not have the following problems:</p>
+
+ <ul>
+ <li>misleads users about what is good Twisted style;</li>
+ <li>misleads users into thinking that an advanced howto is an introduction
+ to writing their first Twisted server; and</li>
+ <li>misleads users about whether they fit the document's target audience:
+ for example, that they are able to use enterprise without knowing how to
+ write SQL queries.</li>
+ </ul>
+
+ <h2>General style<a name="auto0"/></h2>
+
+ <p>Documents should aim to be clear and concise, allowing the API
+ documentation and the example code to tell as much of the story as they
+ can. Demonstrations and where necessary supported arguments should always
+ preferred to simple statements (&quot;here is how you would simplify this
+ code with Deferreds&quot; rather than &quot;Deferreds make code
+ simpler&quot;).</p>
+
+ <p>Documents should be clearly delineated into sections and subsections.
+ Each of these sections, like the overall document, should have a single
+ clear purpose. This is most easily tested by trying to have meaningful
+ headings: a section which is headed by &quot;More details&quot; or
+ &quot;Advanced stuff&quot; is not purposeful enough. There should be
+ fairly obvious ways to split a document. The two most common are task
+ based sectioning and sectioning which follows module and class
+ separations.</p>
+
+ <p>Documentation must use American English spelling, and where possible
+ avoid any local variants of either vocabulary or grammar. Grammatically
+ complex sentences should ideally be avoided: these make reading
+ unnecessarily difficult, particularly for non-native speakers.</p>
+
+ <h2>Evangelism and usage documents<a name="auto1"/></h2>
+
+ <p>The Twisted documentation should maintain a reasonable distinction
+ between &quot;evangelism&quot; documentation, which compares the Twisted
+ design or Twisted best practice with other approaches and argues for the
+ Twisted approach, and &quot;usage&quot; documentation, which describes the
+ Twisted approach in detail without comparison to other possible
+ approaches.</p>
+
+ <p>While both kinds of documentation are useful, they have different
+ audiences. The first kind of document, evangelical documents, is useful to
+ a reader who is researching and comparing approaches and seeking to
+ understand the Twisted approach or Twisted functionality in order to
+ decide whether it is useful to them. The second kind of document, usage
+ documents, are useful to a reader who has decided to use Twisted and
+ simply wants further information about available functions and
+ architectures they can use to accomplish their goal.</p>
+
+ <p>Since they have distinct audiences, evangelism and detailed usage
+ documentation belongs in separate files. There should be links between
+ them in 'Further reading' or similar sections.</p>
+
+ <h2>Descriptions of features<a name="auto2"/></h2>
+
+ <p>Descriptions of any feature added since release 2.0 of Twisted core
+ must have a note describing which release of which Twisted project they
+ were added in at the first mention in each document. If they are not yet
+ released, give them the number of the next minor release.</p>
+
+ <p>For example, a substantial change might have a version number added in
+ the introduction:</p>
+
+ <blockquote>
+ This document describes the Application infrastructure for deploying
+ Twisted applications <em>(added in Twisted 1.3)</em>.
+ </blockquote>
+
+ <p>The version does not need to be mentioned elsewhere in the document
+ except for specific features which were added in subsequent releases,
+ which might should be mentioned separately.</p>
+
+ <blockquote>
+ The simplest way to create a <code>.tac</code> file, SuperTac <em>(added
+ in Twisted Core 99.7)</em>...</blockquote>
+
+ <p>In the case where the usage of a feature has substantially changed, the
+ number should be that of the release in which the current usage became
+ available. For example:</p>
+
+ <blockquote> This document describes the Application infrastructure for
+ deploying Twisted applications <em>(updated[/substantially updated] in Twisted
+ 2.7)</em>. </blockquote>
+
+ <h2>Linking<a name="auto3"/></h2>
+
+ <p>The first occurrence of the name of any module, class or function should
+ always link to the API documents. Subsequent mentions may or may not link
+ at the author's discretion: discussions which are very closely bound to a
+ particular API should probably link in the first mention in the given
+ section.</p>
+
+ <p>Links between howtos are encouraged. Overview documents and tutorials
+ should always link to reference documents and in depth documents. These
+ documents should link among themselves wherever it's needed: if you're
+ tempted to re-describe the functionality of another module, you should
+ certainly link instead.</p>
+
+ <h2>Introductions<a name="auto4"/></h2>
+
+ <p>The introductory section of a Twisted howto should immediately follow
+ the top-level heading and precede any subheadings.</p>
+
+ <p>The following items should be present in the introduction to Twisted
+ howtos: the introductory paragraph and the description of the target
+ audience.</p>
+
+ <h3>Introductory paragraph<a name="auto5"/></h3>
+
+ <p>The introductory paragraph of a document should summarize what the
+ document is designed to present. It should use the both proper names for
+ the Twisted technologies and simple non-Twisted descriptions of the
+ technologies. For example, in this paragraph both the name of the technology
+ (&quot;Conch&quot;) and a description (&quot;SSH server&quot;) are used:</p>
+
+ <blockquote>
+ This document describes setting up a SSH server to serve data from the
+ file system using Conch, the Twisted SSH implementation.
+ </blockquote>
+
+ <p>The introductory paragraph should be relatively short, but should, like
+ the above, somewhere define the document's objective: what the reader
+ should be able to do using instructions in the document.</p>
+
+ <h3>Description of target audience<a name="auto6"/></h3>
+
+ <p>Subsequent paragraphs in the introduction should describe the target
+ audience of the document: who would want to read it, and what they should
+ know before they can expect to use your document. For example:</p>
+
+ <blockquote>
+ <p>
+ The target audience of this document is a Twisted user who has a set of
+ filesystem like data objects that they would like to make available to
+ authenticated users over SFTP.
+ </p>
+
+ <p>
+ Following the directions in this document will require that you are
+ familiar with managing authentication via the Twisted Cred system.
+ </p>
+ </blockquote>
+
+ <p>Use your discretion about the extent to which you list assumed
+ knowledge. Very introductory documents that are going to be among a
+ reader's first exposure to Twisted will even need to specify that they
+ rely on knowledge of Python and of certain networking concepts (ports,
+ servers, clients, connections) but documents that are going to be sought
+ out by existing Twisted users for particular purposes only need to specify
+ other Twisted knowledge that is assumed.</p>
+
+ <p>Any knowledge of technologies that wouldn't be considered &quot;core
+ Python&quot; and/or &quot;simple networking&quot; need to be explicitly
+ specified, no matter how obvious they seem to someone familiar with the
+ technology. For example, it needs to be stated that someone using
+ enterprise should know SQL and should know how to set up and populate
+ databases for testing purposes.</p>
+
+ <p>Where possible, link to other documents that will fill in missing
+ knowledge for the reader. Linking to documents in the Twisted repository
+ is preferred but not essential.</p>
+
+ <h3>Goals of document<a name="auto7"/></h3>
+
+ <p>The introduction should finish with a list of tasks that the user can
+ expect to see the document accomplish. These tasks should be concrete
+ rather than abstract, so rather than telling the user that they will
+ &quot;understand Twisted Conch&quot;, you would list the specific tasks
+ that they will see the document do. For example:</p>
+
+ <blockquote>
+ <p>
+ This document will demonstrate the following tasks using Twisted Conch:
+ </p>
+
+ <ul>
+ <li>creating an anonymous access read-only SFTP server using a filesystem
+ backend;</li>
+ <li>creating an anonymous access read-only SFTP server using a proxy
+ backend connecting to an HTTP server; and</li>
+ <li>creating a anonymous access read and write SFTP server using a
+ filesystem backend.</li>
+ </ul>
+ </blockquote>
+
+ <p>In many cases this will essentially be a list of your code examples,
+ but it need not be. If large sections of your code are devoted to design
+ discussions, your goals might resemble the following:</p>
+
+ <blockquote>
+ <p>
+ This document will discuss the following design aspects of writing Conch
+ servers:
+ </p>
+
+ <ul>
+ <li>authentication of users; and</li>
+ <li>choice of data backends.</li>
+ </ul>
+ </blockquote>
+
+
+ <h2>Example code<a name="auto8"/></h2>
+
+ <p>Wherever possible, example code should be provided to illustrate a
+ certain technique or piece of functionality.</p>
+
+ <p>Example code should try and meet as many of the following requirements
+ as possible:</p>
+
+ <ul>
+ <li>example code should be a complete working example suitable for copying
+ and pasting and running by the reader (where possible, provide a link to a
+ file to download);</li>
+ <li>example code should be short;</li>
+ <li>example code should be commented very extensively, with the assumption
+ that this code may be read by a Twisted newcomer;</li>
+ <li>example code should conform to the <a href="coding-standard.html" shape="rect">coding standard</a>; and</li>
+ <li>example code should exhibit 'best practice', not only for dealing with
+ the target functionality, but also for use of the application framework
+ and so on.</li>
+ </ul>
+
+ <p>The requirement to have a complete working example will occasionally
+ impose upon authors the need to have a few dummy functions: in Twisted
+ documentation the most common example is where a function is needed to
+ generate a Deferred and fire it after some time has passed. An example
+ might be this, where <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.deferLater.html" title="twisted.internet.task.deferLater">deferLater</a></code> is used to fire a callback
+ after a period of time:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">task</span>, <span class="py-src-variable">reactor</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getDummyDeferred</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Dummy method which returns a deferred that will fire in 5 seconds with
+ a result
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">task</span>.<span class="py-src-variable">deferLater</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">5</span>, <span class="py-src-keyword">lambda</span> <span class="py-src-variable">x</span>: <span class="py-src-string">&quot;RESULT&quot;</span>)
+</pre>
+
+ <p>As in the above example, it is imperative to clearly mark that the
+ function is a dummy in as many ways as you can: using <code>Dummy</code> in
+ the function name, explaining that it is a dummy in the docstring, and
+ marking particular lines as being required to create an effect for the
+ purposes of demonstration. In most cases, this will save the reader from
+ mistaking this dummy method for an idiom they should use in their Twisted
+ code.</p>
+
+ <h2>Conclusions<a name="auto9"/></h2>
+
+ <p>The conclusion of a howto should follow the very last section heading
+ in a file. This heading would usually be called &quot;Conclusion&quot;.</p>
+
+ <p>The conclusion of a howto should remind the reader of the tasks that
+ they have done while reading the document. For example:</p>
+
+ <blockquote>
+ <p>
+ In this document, you have seen how to:
+ </p>
+
+ <ol>
+ <li>set up an anonymous read-only SFTP server;</li>
+ <li>set up a SFTP server where users authenticate;</li>
+ <li>set up a SFTP server where users are restricted to some parts of the
+ filesystem based on authentication; and</li>
+ <li>set up a SFTP server where users have write access to some parts of
+ the filesystem based on authentication.</li>
+ </ol>
+ </blockquote>
+
+ <p>If appropriate, the howto could follow this description with links to
+ other documents that might be of interest to the reader with their
+ newfound knowledge. However, these links should be limited to fairly
+ obvious extensions of at least one of the listed tasks.</p>
+
+ </div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/development/security.html b/doc/core/development/security.html
new file mode 100644
index 0000000..78286d1
--- /dev/null
+++ b/doc/core/development/security.html
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Security</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Security</h1>
+ <div class="toc"><ol><li><a href="#auto0">Bad input</a></li><li><a href="#auto1">Resource Exhaustion and DoS</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<p>We need to do a full audit of Twisted, module by module.
+This document list the sort of things you want to look for
+when doing this, or when writing your own code.</p>
+
+<h2>Bad input<a name="auto0"/></h2>
+
+<p>Any place we receive untrusted data, we need to be careful.
+In some cases we are not careful enough. For example, in HTTP
+there are many places where strings need to be converted to
+ints, so we use <code class="python">int()</code>. The problem
+is that this well accept negative numbers as well, whereas
+the protocol should only be accepting positive numbers.</p>
+
+<h2>Resource Exhaustion and DoS<a name="auto1"/></h2>
+
+<p>Make sure we never allow users to create arbitarily large
+strings or files. Some of the protocols still have issues
+like this. Place a limit which allows reasonable use but
+will cut off huge requests, and allow changing of this limit.
+</p>
+
+<p>Another operation to look out for are exceptions. They can fill
+up logs and take a lot of CPU time to render in web pages.</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/examples/ampclient.py b/doc/core/examples/ampclient.py
new file mode 100644
index 0000000..5349448
--- /dev/null
+++ b/doc/core/examples/ampclient.py
@@ -0,0 +1,26 @@
+from twisted.internet import reactor, defer
+from twisted.internet.protocol import ClientCreator
+from twisted.protocols import amp
+from ampserver import Sum, Divide
+
+
+def doMath():
+ d1 = ClientCreator(reactor, amp.AMP).connectTCP(
+ '127.0.0.1', 1234).addCallback(
+ lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
+ lambda result: result['total'])
+ def trapZero(result):
+ result.trap(ZeroDivisionError)
+ print "Divided by zero: returning INF"
+ return 1e1000
+ d2 = ClientCreator(reactor, amp.AMP).connectTCP(
+ '127.0.0.1', 1234).addCallback(
+ lambda p: p.callRemote(Divide, numerator=1234,
+ denominator=0)).addErrback(trapZero)
+ def done(result):
+ print 'Done with math:', result
+ defer.DeferredList([d1, d2]).addCallback(done)
+
+if __name__ == '__main__':
+ doMath()
+ reactor.run()
diff --git a/doc/core/examples/ampserver.py b/doc/core/examples/ampserver.py
new file mode 100644
index 0000000..7b5adf0
--- /dev/null
+++ b/doc/core/examples/ampserver.py
@@ -0,0 +1,40 @@
+from twisted.protocols import amp
+
+class Sum(amp.Command):
+ arguments = [('a', amp.Integer()),
+ ('b', amp.Integer())]
+ response = [('total', amp.Integer())]
+
+
+class Divide(amp.Command):
+ arguments = [('numerator', amp.Integer()),
+ ('denominator', amp.Integer())]
+ response = [('result', amp.Float())]
+ errors = {ZeroDivisionError: 'ZERO_DIVISION'}
+
+
+class Math(amp.AMP):
+ def sum(self, a, b):
+ total = a + b
+ print 'Did a sum: %d + %d = %d' % (a, b, total)
+ return {'total': total}
+ Sum.responder(sum)
+
+ def divide(self, numerator, denominator):
+ result = float(numerator) / denominator
+ print 'Divided: %d / %d = %f' % (numerator, denominator, result)
+ return {'result': result}
+ Divide.responder(divide)
+
+
+def main():
+ from twisted.internet import reactor
+ from twisted.internet.protocol import Factory
+ pf = Factory()
+ pf.protocol = Math
+ reactor.listenTCP(1234, pf)
+ print 'started'
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/bananabench.py b/doc/core/examples/bananabench.py
new file mode 100644
index 0000000..a712afa
--- /dev/null
+++ b/doc/core/examples/bananabench.py
@@ -0,0 +1,79 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import sys
+import time
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+# Twisted Imports
+from twisted.spread import banana
+from twisted.internet import protocol
+
+iterationCount = 10000
+
+class BananaBench:
+ r = range( iterationCount )
+ def setUp(self, encClass):
+ self.io = StringIO.StringIO()
+ self.enc = encClass()
+ self.enc.makeConnection(protocol.FileWrapper(self.io))
+ self.enc._selectDialect("none")
+ self.enc.expressionReceived = self.putResult
+
+ def putResult(self, result):
+ self.result = result
+
+ def tearDown(self):
+ self.enc.connectionLost()
+ del self.enc
+
+ def testEncode(self, value):
+ starttime = time.time()
+ for i in self.r:
+ self.enc.sendEncoded(value)
+ self.io.truncate(0)
+ endtime = time.time()
+ print ' Encode took %s seconds' % (endtime - starttime)
+ return endtime - starttime
+
+ def testDecode(self, value):
+ self.enc.sendEncoded(value)
+ encoded = self.io.getvalue()
+ starttime = time.time()
+ for i in self.r:
+ self.enc.dataReceived(encoded)
+ endtime = time.time()
+ print ' Decode took %s seconds' % (endtime - starttime)
+ return endtime - starttime
+
+ def performTest(self, method, data, encClass):
+ self.setUp(encClass)
+ method(data)
+ self.tearDown()
+
+ def runTests(self, testData):
+ print 'Test data is: %s' % testData
+ print ' Using Pure Python Banana:'
+ self.performTest(self.testEncode, testData, banana.Banana)
+ self.performTest(self.testDecode, testData, banana.Banana)
+
+bench = BananaBench()
+print 'Doing %s iterations of each test.' % iterationCount
+print ''
+testData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+bench.runTests(testData)
+testData = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
+bench.runTests(testData)
+testData = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
+bench.runTests(testData)
+testData = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]
+bench.runTests(testData)
+testData = [1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l]
+bench.runTests(testData)
+testData = [1, 2, [3, 4], [30.5, 40.2], 5, ["six", "seven", ["eight", 9]], [10], []]
+bench.runTests(testData)
+
diff --git a/doc/core/examples/chatserver.py b/doc/core/examples/chatserver.py
new file mode 100644
index 0000000..76c3cf8
--- /dev/null
+++ b/doc/core/examples/chatserver.py
@@ -0,0 +1,37 @@
+"""The most basic chat protocol possible.
+
+run me with twistd -y chatserver.py, and then connect with multiple
+telnet clients to port 1025
+"""
+
+from twisted.protocols import basic
+
+
+
+class MyChat(basic.LineReceiver):
+ def connectionMade(self):
+ print "Got new client!"
+ self.factory.clients.append(self)
+
+ def connectionLost(self, reason):
+ print "Lost a client!"
+ self.factory.clients.remove(self)
+
+ def lineReceived(self, line):
+ print "received", repr(line)
+ for c in self.factory.clients:
+ c.message(line)
+
+ def message(self, message):
+ self.transport.write(message + '\n')
+
+
+from twisted.internet import protocol
+from twisted.application import service, internet
+
+factory = protocol.ServerFactory()
+factory.protocol = MyChat
+factory.clients = []
+
+application = service.Application("chatserver")
+internet.TCPServer(1025, factory).setServiceParent(application)
diff --git a/doc/core/examples/courier.py b/doc/core/examples/courier.py
new file mode 100644
index 0000000..db80a15
--- /dev/null
+++ b/doc/core/examples/courier.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Example of a interfacing to Courier's mail filter interface.
+"""
+
+LOGFILE = '/tmp/filter.log'
+
+# Setup log file
+from twisted.python import log
+log.startLogging(open(LOGFILE, 'a'))
+import sys
+sys.stderr = log.logfile
+
+# Twisted imports
+from twisted.internet import reactor, stdio
+from twisted.internet.protocol import Protocol, Factory
+from twisted.protocols import basic
+
+FILTERS='/var/lib/courier/filters'
+ALLFILTERS='/var/lib/courier/allfilters'
+FILTERNAME='twistedfilter'
+
+import os, os.path
+from syslog import syslog, openlog, LOG_MAIL
+from rfc822 import Message
+
+def trace_dump():
+ t,v,tb = sys.exc_info()
+ openlog(FILTERNAME, 0, LOG_MAIL)
+ syslog('Unhandled exception: %s - %s' % (v, t))
+ while tb:
+ syslog('Trace: %s:%s %s' % (tb.tb_frame.f_code.co_filename,tb.tb_frame.f_code.co_name,tb.tb_lineno))
+ tb = tb.tb_next
+ # just to be safe
+ del tb
+
+def safe_del(file):
+ try:
+ if os.path.isdir(file):
+ os.removedirs(file)
+ else:
+ os.remove(file)
+ except OSError:
+ pass
+
+
+class DieWhenLost(Protocol):
+ def connectionLost(self, reason=None):
+ reactor.stop()
+
+
+class MailProcessor(basic.LineReceiver):
+ """I process a mail message.
+
+ Override filterMessage to do any filtering you want."""
+ messageFilename = None
+ delimiter = '\n'
+
+ def connectionMade(self):
+ log.msg('Connection from %r' % self.transport)
+ self.state = 'connected'
+ self.metaInfo = []
+
+ def lineReceived(self, line):
+ if self.state == 'connected':
+ self.messageFilename = line
+ self.state = 'gotMessageFilename'
+ if self.state == 'gotMessageFilename':
+ if line:
+ self.metaInfo.append(line)
+ else:
+ if not self.metaInfo:
+ self.transport.loseConnection()
+ return
+ self.filterMessage()
+
+ def filterMessage(self):
+ """Override this.
+
+ A trivial example is included.
+ """
+ try:
+ m = Message(open(self.messageFilename))
+ self.sendLine('200 Ok')
+ except:
+ trace_dump()
+ self.sendLine('435 %s processing error' % FILTERNAME)
+
+
+def main():
+ # Listen on the UNIX socket
+ f = Factory()
+ f.protocol = MailProcessor
+ safe_del('%s/%s' % (ALLFILTERS, FILTERNAME))
+ reactor.listenUNIX('%s/%s' % (ALLFILTERS, FILTERNAME), f, 10)
+
+ # Once started, close fd 3 to let Courier know we're ready
+ reactor.callLater(0, os.close, 3)
+
+ # When stdin is closed, it's time to exit.
+ s = stdio.StandardIO(DieWhenLost())
+
+ # Go!
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/cred.py b/doc/core/examples/cred.py
new file mode 100644
index 0000000..6fabc9a
--- /dev/null
+++ b/doc/core/examples/cred.py
@@ -0,0 +1,163 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+import sys
+from zope.interface import implements, Interface
+
+from twisted.protocols import basic
+from twisted.internet import protocol
+from twisted.python import log
+
+from twisted.cred import error
+from twisted.cred import portal
+from twisted.cred import checkers
+from twisted.cred import credentials
+
+class IProtocolUser(Interface):
+ def getPrivileges():
+ """Return a list of privileges this user has."""
+
+ def logout():
+ """Cleanup per-login resources allocated to this avatar"""
+
+class AnonymousUser:
+ implements(IProtocolUser)
+
+ def getPrivileges(self):
+ return [1, 2, 3]
+
+ def logout(self):
+ print "Cleaning up anonymous user resources"
+
+class RegularUser:
+ implements(IProtocolUser)
+
+ def getPrivileges(self):
+ return [1, 2, 3, 5, 6]
+
+ def logout(self):
+ print "Cleaning up regular user resources"
+
+class Administrator:
+ implements(IProtocolUser)
+
+ def getPrivileges(self):
+ return range(50)
+
+ def logout(self):
+ print "Cleaning up administrator resources"
+
+class Protocol(basic.LineReceiver):
+ user = None
+ portal = None
+ avatar = None
+ logout = None
+
+ def connectionMade(self):
+ self.sendLine("Login with USER <name> followed by PASS <password> or ANON")
+ self.sendLine("Check privileges with PRIVS")
+
+ def connectionLost(self, reason):
+ if self.logout:
+ self.logout()
+ self.avatar = None
+ self.logout = None
+
+ def lineReceived(self, line):
+ f = getattr(self, 'cmd_' + line.upper().split()[0])
+ if f:
+ try:
+ f(*line.split()[1:])
+ except TypeError:
+ self.sendLine("Wrong number of arguments.")
+ except:
+ self.sendLine("Server error (probably your fault)")
+
+ def cmd_ANON(self):
+ if self.portal:
+ self.portal.login(credentials.Anonymous(), None, IProtocolUser
+ ).addCallbacks(self._cbLogin, self._ebLogin
+ )
+ else:
+ self.sendLine("DENIED")
+
+ def cmd_USER(self, name):
+ self.user = name
+ self.sendLine("Alright. Now PASS?")
+
+ def cmd_PASS(self, password):
+ if not self.user:
+ self.sendLine("USER required before PASS")
+ else:
+ if self.portal:
+ self.portal.login(
+ credentials.UsernamePassword(self.user, password),
+ None,
+ IProtocolUser
+ ).addCallbacks(self._cbLogin, self._ebLogin
+ )
+ else:
+ self.sendLine("DENIED")
+
+ def cmd_PRIVS(self):
+ self.sendLine("You have the following privileges: ")
+ self.sendLine(" ".join(map(str, self.avatar.getPrivileges())))
+
+ def _cbLogin(self, (interface, avatar, logout)):
+ assert interface is IProtocolUser
+ self.avatar = avatar
+ self.logout = logout
+ self.sendLine("Login successful. Available commands: PRIVS")
+
+ def _ebLogin(self, failure):
+ failure.trap(error.UnauthorizedLogin)
+ self.sendLine("Login denied! Go away.")
+
+class ServerFactory(protocol.ServerFactory):
+ protocol = Protocol
+
+ def __init__(self, portal):
+ self.portal = portal
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ p.portal = self.portal
+ return p
+
+class Realm:
+ implements(portal.IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IProtocolUser in interfaces:
+ if avatarId == checkers.ANONYMOUS:
+ av = AnonymousUser()
+ elif avatarId.isupper():
+ # Capitalized usernames are administrators.
+ av = Administrator()
+ else:
+ av = RegularUser()
+ return IProtocolUser, av, av.logout
+ raise NotImplementedError("Only IProtocolUser interface is supported by this realm")
+
+def main():
+ r = Realm()
+ p = portal.Portal(r)
+ c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ c.addUser("auser", "thepass")
+ c.addUser("SECONDUSER", "secret")
+ p.registerChecker(c)
+ p.registerChecker(checkers.AllowAnonymousAccess())
+
+ f = ServerFactory(p)
+
+ log.startLogging(sys.stdout)
+
+ from twisted.internet import reactor
+ reactor.listenTCP(4738, f)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/dbcred.py b/doc/core/examples/dbcred.py
new file mode 100755
index 0000000..c1c216e
--- /dev/null
+++ b/doc/core/examples/dbcred.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Simple example of a db checker: define a L{ICredentialsChecker} implementation
+that deals with a database backend to authenticate a user.
+"""
+
+from twisted.cred import error
+from twisted.cred.credentials import IUsernameHashedPassword, IUsernamePassword
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.internet.defer import Deferred
+
+from zope.interface import implements
+
+
+class DBCredentialsChecker(object):
+ """
+ This class checks the credentials of incoming connections
+ against a user table in a database.
+ """
+ implements(ICredentialsChecker)
+
+ def __init__(self, runQuery,
+ query="SELECT username, password FROM user WHERE username = %s",
+ customCheckFunc=None, caseSensitivePasswords=True):
+ """
+ @param runQuery: This will be called to get the info from the db.
+ Generally you'd want to create a
+ L{twisted.enterprice.adbapi.ConnectionPool} and pass it's runQuery
+ method here. Otherwise pass a function with the same prototype.
+ @type runQuery: C{callable}
+
+ @type query: query used to authenticate user.
+ @param query: C{str}
+
+ @param customCheckFunc: Use this if the passwords in the db are stored
+ as hashes. We'll just call this, so you can do the checking
+ yourself. It takes the following params:
+ (username, suppliedPass, dbPass) and must return a boolean.
+ @type customCheckFunc: C{callable}
+
+ @param caseSensitivePasswords: If true requires that every letter in
+ C{credentials.password} is exactly the same case as the it's
+ counterpart letter in the database.
+ This is only relevant if C{customCheckFunc} is not used.
+ @type caseSensitivePasswords: C{bool}
+ """
+ self.runQuery = runQuery
+ self.caseSensitivePasswords = caseSensitivePasswords
+ self.customCheckFunc = customCheckFunc
+ # We can't support hashed password credentials if we only have a hash
+ # in the DB
+ if customCheckFunc:
+ self.credentialInterfaces = (IUsernamePassword,)
+ else:
+ self.credentialInterfaces = (
+ IUsernamePassword, IUsernameHashedPassword,)
+
+ self.sql = query
+
+ def requestAvatarId(self, credentials):
+ """
+ Authenticates the kiosk against the database.
+ """
+ # Check that the credentials instance implements at least one of our
+ # interfaces
+ for interface in self.credentialInterfaces:
+ if interface.providedBy(credentials):
+ break
+ else:
+ raise error.UnhandledCredentials()
+ # Ask the database for the username and password
+ dbDeferred = self.runQuery(self.sql, (credentials.username,))
+ # Setup our deferred result
+ deferred = Deferred()
+ dbDeferred.addCallbacks(self._cbAuthenticate, self._ebAuthenticate,
+ callbackArgs=(credentials, deferred),
+ errbackArgs=(credentials, deferred))
+ return deferred
+
+ def _cbAuthenticate(self, result, credentials, deferred):
+ """
+ Checks to see if authentication was good. Called once the info has
+ been retrieved from the DB.
+ """
+ if len(result) == 0:
+ # Username not found in db
+ deferred.errback(error.UnauthorizedLogin('Username unknown'))
+ else:
+ username, password = result[0]
+ if self.customCheckFunc:
+ # Let the owner do the checking
+ if self.customCheckFunc(
+ username, credentials.password, password):
+ deferred.callback(credentials.username)
+ else:
+ deferred.errback(
+ error.UnauthorizedLogin('Password mismatch'))
+ else:
+ # It's up to us or the credentials object to do the checking
+ # now
+ if IUsernameHashedPassword.providedBy(credentials):
+ # Let the hashed password checker do the checking
+ if credentials.checkPassword(password):
+ deferred.callback(credentials.username)
+ else:
+ deferred.errback(
+ error.UnauthorizedLogin('Password mismatch'))
+ elif IUsernamePassword.providedBy(credentials):
+ # Compare the passwords, deciging whether or not to use
+ # case sensitivity
+ if self.caseSensitivePasswords:
+ passOk = (
+ password.lower() == credentials.password.lower())
+ else:
+ passOk = password == credentials.password
+ # See if they match
+ if passOk:
+ deferred.callback(credentials.username)
+ else:
+ deferred.errback(
+ error.UnauthorizedLogin('Password mismatch'))
+ else:
+ # OK, we don't know how to check this
+ deferred.errback(error.UnhandledCredentials())
+
+ def _ebAuthenticate(self, message, credentials, deferred):
+ """
+ The database lookup failed for some reason.
+ """
+ deferred.errback(error.LoginFailed(message))
+
+
+def main():
+ """
+ Run a simple echo pb server to test the checker. It defines a custom query
+ for dealing with sqlite special quoting, but otherwise it's a
+ straightforward use of the object.
+
+ You can test it running C{pbechoclient.py}.
+ """
+ import sys
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+ import os
+ if os.path.isfile('testcred'):
+ os.remove('testcred')
+ from twisted.enterprise import adbapi
+ pool = adbapi.ConnectionPool('pysqlite2.dbapi2', 'testcred')
+ # Create the table that will be used
+ query1 = """CREATE TABLE user (
+ username string,
+ password string
+ )"""
+ # Insert a test user
+ query2 = """INSERT INTO user VALUES ('guest', 'guest')"""
+ def cb(res):
+ pool.runQuery(query2)
+ pool.runQuery(query1).addCallback(cb)
+
+ checker = DBCredentialsChecker(pool.runQuery,
+ query="SELECT username, password FROM user WHERE username = ?")
+ from twisted.cred.portal import Portal
+
+ import pbecho
+ from twisted.spread import pb
+ portal = Portal(pbecho.SimpleRealm())
+ portal.registerChecker(checker)
+ reactor.listenTCP(pb.portno, pb.PBServerFactory(portal))
+
+
+if __name__ == "__main__":
+ from twisted.internet import reactor
+ reactor.callWhenRunning(main)
+ reactor.run()
+
diff --git a/doc/core/examples/echoclient.py b/doc/core/examples/echoclient.py
new file mode 100644
index 0000000..6bb6750
--- /dev/null
+++ b/doc/core/examples/echoclient.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet.protocol import ClientFactory
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import reactor
+import sys
+
+class EchoClient(LineReceiver):
+ end="Bye-bye!"
+ def connectionMade(self):
+ self.sendLine("Hello, world!")
+ self.sendLine("What a fine day it is.")
+ self.sendLine(self.end)
+
+ def lineReceived(self, line):
+ print "receive:", line
+ if line==self.end:
+ self.transport.loseConnection()
+
+class EchoClientFactory(ClientFactory):
+ protocol = EchoClient
+
+ def clientConnectionFailed(self, connector, reason):
+ print 'connection failed:', reason.getErrorMessage()
+ reactor.stop()
+
+ def clientConnectionLost(self, connector, reason):
+ print 'connection lost:', reason.getErrorMessage()
+ reactor.stop()
+
+def main():
+ factory = EchoClientFactory()
+ reactor.connectTCP('localhost', 8000, factory)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/echoclient_ssl.py b/doc/core/examples/echoclient_ssl.py
new file mode 100755
index 0000000..d905cd5
--- /dev/null
+++ b/doc/core/examples/echoclient_ssl.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from OpenSSL import SSL
+import sys
+
+from twisted.internet.protocol import ClientFactory
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import ssl, reactor
+
+
+class EchoClient(LineReceiver):
+ end="Bye-bye!"
+ def connectionMade(self):
+ self.sendLine("Hello, world!")
+ self.sendLine("What a fine day it is.")
+ self.sendLine(self.end)
+
+ def connectionLost(self, reason):
+ print 'connection lost (protocol)'
+
+ def lineReceived(self, line):
+ print "receive:", line
+ if line==self.end:
+ self.transport.loseConnection()
+
+class EchoClientFactory(ClientFactory):
+ protocol = EchoClient
+
+ def clientConnectionFailed(self, connector, reason):
+ print 'connection failed:', reason.getErrorMessage()
+ reactor.stop()
+
+ def clientConnectionLost(self, connector, reason):
+ print 'connection lost:', reason.getErrorMessage()
+ reactor.stop()
+
+def main():
+ factory = EchoClientFactory()
+ reactor.connectSSL('localhost', 8000, factory, ssl.ClientContextFactory())
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/echoclient_udp.py b/doc/core/examples/echoclient_udp.py
new file mode 100644
index 0000000..93589a1
--- /dev/null
+++ b/doc/core/examples/echoclient_udp.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+
+class EchoClientDatagramProtocol(DatagramProtocol):
+ strings = [
+ "Hello, world!",
+ "What a fine day it is.",
+ "Bye-bye!"
+ ]
+
+ def startProtocol(self):
+ self.transport.connect('127.0.0.1', 8000)
+ self.sendDatagram()
+
+ def sendDatagram(self):
+ if len(self.strings):
+ datagram = self.strings.pop(0)
+ self.transport.write(datagram)
+ else:
+ reactor.stop()
+
+ def datagramReceived(self, datagram, host):
+ print 'Datagram received: ', repr(datagram)
+ self.sendDatagram()
+
+def main():
+ protocol = EchoClientDatagramProtocol()
+ t = reactor.listenUDP(0, protocol)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/echoserv.py b/doc/core/examples/echoserv.py
new file mode 100644
index 0000000..023a4e3
--- /dev/null
+++ b/doc/core/examples/echoserv.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet import reactor
+
+### Protocol Implementation
+
+# This is just about the simplest possible protocol
+class Echo(Protocol):
+ def dataReceived(self, data):
+ """
+ As soon as any data is received, write it back.
+ """
+ self.transport.write(data)
+
+
+def main():
+ f = Factory()
+ f.protocol = Echo
+ reactor.listenTCP(8000, f)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/echoserv_ssl.py b/doc/core/examples/echoserv_ssl.py
new file mode 100644
index 0000000..5037b92
--- /dev/null
+++ b/doc/core/examples/echoserv_ssl.py
@@ -0,0 +1,30 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from OpenSSL import SSL
+
+class ServerContextFactory:
+
+ def getContext(self):
+ """Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'."""
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file('server.pem')
+ ctx.use_privatekey_file('server.pem')
+ return ctx
+
+
+if __name__ == '__main__':
+ import echoserv, sys
+ from twisted.internet.protocol import Factory
+ from twisted.internet import ssl, reactor
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+ factory = Factory()
+ factory.protocol = echoserv.Echo
+ reactor.listenSSL(8000, factory, ServerContextFactory())
+ reactor.run()
diff --git a/doc/core/examples/echoserv_udp.py b/doc/core/examples/echoserv_udp.py
new file mode 100644
index 0000000..5d9eaa4
--- /dev/null
+++ b/doc/core/examples/echoserv_udp.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+
+# Here's a UDP version of the simplest possible protocol
+class EchoUDP(DatagramProtocol):
+ def datagramReceived(self, datagram, address):
+ self.transport.write(datagram, address)
+
+def main():
+ reactor.listenUDP(8000, EchoUDP())
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/filewatch.py b/doc/core/examples/filewatch.py
new file mode 100644
index 0000000..19a0373
--- /dev/null
+++ b/doc/core/examples/filewatch.py
@@ -0,0 +1,17 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.application import internet
+
+def watch(fp):
+ fp.seek(fp.tell())
+ for line in fp.readlines():
+ sys.stdout.write(line)
+
+import sys
+from twisted.internet import reactor
+s = internet.TimerService(0.1, watch, file(sys.argv[1]))
+s.startService()
+reactor.run()
+s.stopService()
diff --git a/doc/core/examples/ftpclient.py b/doc/core/examples/ftpclient.py
new file mode 100644
index 0000000..c911888
--- /dev/null
+++ b/doc/core/examples/ftpclient.py
@@ -0,0 +1,113 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example of using the FTP client
+"""
+
+# Twisted imports
+from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
+from twisted.internet.protocol import Protocol, ClientCreator
+from twisted.python import usage
+from twisted.internet import reactor
+
+# Standard library imports
+import string
+import sys
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+class BufferingProtocol(Protocol):
+ """Simple utility class that holds all data written to it in a buffer."""
+ def __init__(self):
+ self.buffer = StringIO()
+
+ def dataReceived(self, data):
+ self.buffer.write(data)
+
+# Define some callbacks
+
+def success(response):
+ print 'Success! Got response:'
+ print '---'
+ if response is None:
+ print None
+ else:
+ print string.join(response, '\n')
+ print '---'
+
+
+def fail(error):
+ print 'Failed. Error was:'
+ print error
+
+def showFiles(result, fileListProtocol):
+ print 'Processed file listing:'
+ for file in fileListProtocol.files:
+ print ' %s: %d bytes, %s' \
+ % (file['filename'], file['size'], file['date'])
+ print 'Total: %d files' % (len(fileListProtocol.files))
+
+def showBuffer(result, bufferProtocol):
+ print 'Got data:'
+ print bufferProtocol.buffer.getvalue()
+
+
+class Options(usage.Options):
+ optParameters = [['host', 'h', 'localhost'],
+ ['port', 'p', 21],
+ ['username', 'u', 'anonymous'],
+ ['password', None, 'twisted@'],
+ ['passive', None, 0],
+ ['debug', 'd', 1],
+ ]
+
+def run():
+ # Get config
+ config = Options()
+ config.parseOptions()
+ config.opts['port'] = int(config.opts['port'])
+ config.opts['passive'] = int(config.opts['passive'])
+ config.opts['debug'] = int(config.opts['debug'])
+
+ # Create the client
+ FTPClient.debug = config.opts['debug']
+ creator = ClientCreator(reactor, FTPClient, config.opts['username'],
+ config.opts['password'], passive=config.opts['passive'])
+ creator.connectTCP(config.opts['host'], config.opts['port']).addCallback(connectionMade).addErrback(connectionFailed)
+ reactor.run()
+
+def connectionFailed(f):
+ print "Connection Failed:", f
+ reactor.stop()
+
+def connectionMade(ftpClient):
+ # Get the current working directory
+ ftpClient.pwd().addCallbacks(success, fail)
+
+ # Get a detailed listing of the current directory
+ fileList = FTPFileListProtocol()
+ d = ftpClient.list('.', fileList)
+ d.addCallbacks(showFiles, fail, callbackArgs=(fileList,))
+
+ # Change to the parent directory
+ ftpClient.cdup().addCallbacks(success, fail)
+
+ # Create a buffer
+ proto = BufferingProtocol()
+
+ # Get short listing of current directory, and quit when done
+ d = ftpClient.nlst('.', proto)
+ d.addCallbacks(showBuffer, fail, callbackArgs=(proto,))
+ d.addCallback(lambda result: reactor.stop())
+
+
+# this only runs if the module was *not* imported
+if __name__ == '__main__':
+ run()
+
diff --git a/doc/core/examples/ftpserver.py b/doc/core/examples/ftpserver.py
new file mode 100644
index 0000000..8c4588b
--- /dev/null
+++ b/doc/core/examples/ftpserver.py
@@ -0,0 +1,55 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An example FTP server with minimal user authentication.
+"""
+
+from twisted.protocols.ftp import FTPFactory, FTPRealm
+from twisted.cred.portal import Portal
+from twisted.cred.checkers import AllowAnonymousAccess, FilePasswordDB
+from twisted.internet import reactor
+
+#
+# First, set up a portal (twisted.cred.portal.Portal). This will be used
+# to authenticate user logins, including anonymous logins.
+#
+# Part of this will be to establish the "realm" of the server - the most
+# important task in this case is to establish where anonymous users will
+# have default access to. In a real world scenario this would typically
+# point to something like '/pub' but for this example it is pointed at the
+# current working directory.
+#
+# The other important part of the portal setup is to point it to a list of
+# credential checkers. In this case, the first of these is used to grant
+# access to anonymous users and is relatively simple; the second is a very
+# primitive password checker. This example uses a plain text password file
+# that has one username:password pair per line. This checker *does* provide
+# a hashing interface, and one would normally want to use it instead of
+# plain text storage for anything remotely resembling a 'live' network. In
+# this case, the file "pass.dat" is used, and stored in the same directory
+# as the server. BAD.
+#
+# Create a pass.dat file which looks like this:
+#
+# =====================
+# jeff:bozo
+# grimmtooth:bozo2
+# =====================
+#
+p = Portal(FTPRealm('./'),
+ [AllowAnonymousAccess(), FilePasswordDB("pass.dat")])
+
+#
+# Once the portal is set up, start up the FTPFactory and pass the portal to
+# it on startup. FTPFactory will start up a twisted.protocols.ftp.FTP()
+# handler for each incoming OPEN request. Business as usual in Twisted land.
+#
+f = FTPFactory(p)
+
+#
+# You know this part. Point the reactor to port 21 coupled with the above factory,
+# and start the event loop.
+#
+reactor.listenTCP(21, f)
+reactor.run()
diff --git a/doc/core/examples/gpsfix.py b/doc/core/examples/gpsfix.py
new file mode 100644
index 0000000..eefb578
--- /dev/null
+++ b/doc/core/examples/gpsfix.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+GPSTest is a simple example using the SerialPort transport and the NMEA 0183
+and Rockwell Zodiac GPS protocols to display fix data as it is received from
+the device.
+"""
+from twisted.python import log, usage
+import sys
+
+if sys.platform == 'win32':
+ from twisted.internet import win32eventreactor
+ win32eventreactor.install()
+
+
+class GPSFixLogger:
+ def handle_fix(self, *args):
+ """
+ handle_fix gets called whenever either rockwell.Zodiac or nmea.NMEAReceiver
+ receives and decodes fix data. Generally, GPS receivers will report a
+ fix at 1hz. Implementing only this method is sufficient for most purposes
+ unless tracking of ground speed, course, utc date, or detailed satellite
+ information is necessary.
+
+ For example, plotting a map from MapQuest or a similar service only
+ requires longitude and latitude.
+ """
+ log.msg('fix:\n' +
+ '\n'.join(map(lambda n: ' %s = %s' % tuple(n), zip(('utc', 'lon', 'lat', 'fix', 'sat', 'hdp', 'alt', 'geo', 'dgp'), map(repr, args)))))
+
+class GPSOptions(usage.Options):
+ optFlags = [
+ ['zodiac', 'z', 'Use Rockwell Zodiac (DeLorme Earthmate) [default: NMEA 0183]'],
+ ]
+ optParameters = [
+ ['outfile', 'o', None, 'Logfile [default: sys.stdout]'],
+ ['baudrate', 'b', None, 'Serial baudrate [default: 4800 for NMEA, 9600 for Zodiac]'],
+ ['port', 'p', '/dev/ttyS0', 'Serial Port device'],
+ ]
+
+
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ from twisted.internet.serialport import SerialPort
+
+ o = GPSOptions()
+ try:
+ o.parseOptions()
+ except usage.UsageError, errortext:
+ print '%s: %s' % (sys.argv[0], errortext)
+ print '%s: Try --help for usage details.' % (sys.argv[0])
+ raise SystemExit, 1
+
+ logFile = o.opts['outfile']
+ if logFile is None:
+ logFile = sys.stdout
+ log.startLogging(logFile)
+
+ if o.opts['zodiac']:
+ from twisted.protocols.gps.rockwell import Zodiac as GPSProtocolBase
+ baudrate = 9600
+ else:
+ from twisted.protocols.gps.nmea import NMEAReceiver as GPSProtocolBase
+ baudrate = 4800
+ class GPSTest(GPSProtocolBase, GPSFixLogger):
+ pass
+
+ if o.opts['baudrate']:
+ baudrate = int(o.opts['baudrate'])
+
+
+ port = o.opts['port']
+ log.msg('Attempting to open %s at %dbps as a %s device' % (port, baudrate, GPSProtocolBase.__name__))
+ s = SerialPort(GPSTest(), o.opts['port'], reactor, baudrate=baudrate)
+ reactor.run()
diff --git a/doc/core/examples/index.html b/doc/core/examples/index.html
new file mode 100644
index 0000000..118e28d
--- /dev/null
+++ b/doc/core/examples/index.html
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">Simple Echo server and client</a></li><li><a href="#auto1">Chat</a></li><li><a href="#auto2">Echo server &amp; client variants</a></li><li><a href="#auto3">AMP server &amp; client variants</a></li><li><a href="#auto4">Perspective Broker</a></li><li><a href="#auto5">Cred</a></li><li><a href="#auto6">GUI</a></li><li><a href="#auto7">FTP examples</a></li><li><a href="#auto8">Logging</a></li><li><a href="#auto9">POSIX Specific Tricks</a></li><li><a href="#auto10">Miscellaneous</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Simple Echo server and client<a name="auto0"/></h2>
+ <ul>
+ <li><a href="simpleclient.py" shape="rect">simpleclient.py</a> - simple TCP client</li>
+ <li><a href="simpleserv.py" shape="rect">simpleserv.py</a> - simple TCP echo server</li>
+ </ul>
+
+ <h2>Chat<a name="auto1"/></h2>
+ <ul>
+ <li><a href="chatserver.py" shape="rect">chatserver.py</a> - shows how to communicate between clients</li>
+ </ul>
+
+ <h2>Echo server &amp; client variants<a name="auto2"/></h2>
+ <ul>
+ <li><a href="echoserv.py" shape="rect">echoserv.py</a> - variant on a simple TCP echo server</li>
+ <li><a href="echoclient.py" shape="rect">echoclient.py</a> - variant on a simple TCP client</li>
+ <li><a href="echoserv_udp.py" shape="rect">echoserv_udp.py</a> - simplest possible
+ UDP server</li>
+ <li><a href="echoclient_udp.py" shape="rect">echoclient_udp.py</a> - simple UDP
+ client</li>
+ <li><a href="echoserv_ssl.py" shape="rect">echoserv_ssl.py</a> - simple SSL server</li>
+ <li><a href="echoclient_ssl.py" shape="rect">echoclient_ssl.py</a> - simple SSL client</li>
+ </ul>
+
+ <h2>AMP server &amp; client variants<a name="auto3"/></h2>
+ <ul>
+ <li><a href="ampserver.py" shape="rect">ampserver.py</a> - do math using AMP</li>
+ <li><a href="ampclient.py" shape="rect">ampclient.py</a> - do math using AMP</li>
+ </ul>
+
+ <h2>Perspective Broker<a name="auto4"/></h2>
+ <ul>
+ <li><a href="pbsimple.py" shape="rect">pbsimple.py</a> - simplest possible PB server</li>
+ <li><a href="pbsimpleclient.py" shape="rect">pbsimpleclient.py</a> - simplest possible PB
+ client</li>
+ <li><a href="pbbenchclient.py" shape="rect">pbbenchclient.py</a> - benchmarking client</li>
+ <li><a href="pbbenchserver.py" shape="rect">pbbenchserver.py</a> - benchmarking server</li>
+ <li><a href="pbecho.py" shape="rect">pbecho.py</a> - echo server that uses login</li>
+ <li><a href="pbechoclient.py" shape="rect">pbechoclient.py</a> - echo client using login</li>
+ <li><a href="pb_exceptions.py" shape="rect">pb_exceptions.py</a> - example of exceptions over PB</li>
+ <li><a href="pbgtk2.py" shape="rect">pbgtk2.py</a> - example of using GTK2 with PB</li>
+ <li><a href="pbinterop.py" shape="rect">pbinterop.py</a> - shows off various types supported by PB</li>
+ <li><a href="bananabench.py" shape="rect">bananabench.py</a> - benchmark for banana</li>
+ </ul>
+
+ <h2>Cred<a name="auto5"/></h2>
+ <ul>
+ <li><a href="cred.py" shape="rect">cred.py</a> - Authenticate a user with an in-memory username/password
+ database</li>
+ <li><a href="dbcred.py" shape="rect">dbcred.py</a> - Using a database backend to authenticate a user</li>
+ </ul>
+
+ <h2>GUI<a name="auto6"/></h2>
+ <ul>
+ <li><a href="wxdemo.py" shape="rect">wxdemo.py</a> - demo of wxPython integration with Twisted</li>
+ <li><a href="pbgtk2.py" shape="rect">pbgtk2.py</a> - example of using GTK2 with PB</li>
+ <li><a href="pyuidemo.py" shape="rect">pyuidemo.py</a> - PyUI</li>
+ </ul>
+
+ <h2>FTP examples<a name="auto7"/></h2>
+ <ul>
+ <li><a href="ftpclient.py" shape="rect">ftpclient.py</a> - example of using the FTP client</li>
+ <li><a href="ftpserver.py" shape="rect">ftpserver.py</a> - create an FTP server which
+ serves files for anonymous users from the working directory and serves
+ files for authenticated users from <code class="shell">/home</code>.</li>
+ </ul>
+
+ <h2>Logging<a name="auto8"/></h2>
+ <ul>
+ <li><a href="twistd-logging.tac" shape="rect">twistd-logging.tac</a> - logging example using
+ ILogObserver</li>
+ <li><a href="testlogging.py" shape="rect">testlogging.py</a> - use twisted.python.log to log errors to
+ standard out</li>
+ <li><a href="rotatinglog.py" shape="rect">rotatinglog.py</a> - example of log file rotation</li>
+ </ul>
+
+ <h2>POSIX Specific Tricks<a name="auto9"/></h2>
+ <ul>
+ <li><a href="sendfd.py" shape="rect">sendfd.py</a>, <a href="recvfd.py" shape="rect">recvfd.py</a> - send and receive
+ file descriptors over UNIX domain sockets
+ </li>
+ </ul>
+
+ <h2>Miscellaneous<a name="auto10"/></h2>
+ <ul>
+ <li><a href="shaper.py" shape="rect">shaper.py</a> - example of rate-limiting your web server</li>
+ <li><a href="stdiodemo.py" shape="rect">stdiodemo.py</a> - example using stdio, Deferreds, LineReceiver
+ and twisted.web.client.</li>
+ <li><a href="mouse.py" shape="rect">mouse.py</a> - example using MouseMan protocol with the SerialPort
+ transport</li>
+ <li><a href="ptyserv.py" shape="rect">ptyserv.py</a> - serve shells in pseudo-terminals over TCP</li>
+ <li><a href="courier.py" shape="rect">courier.py</a> - example of interfacing to Courier's mail filter
+ interface</li>
+ <li><a href="longex.py" shape="rect">longex.py</a> - example of doing arbitarily long calculations nicely
+ in Twisted</li>
+ <li><a href="longex2.py" shape="rect">longex2.py</a> - using generators to do long calculations</li>
+ <li><a href="stdin.py" shape="rect">stdin.py</a> - reading a line at a time from standard input
+ without blocking the reactor</li>
+ <li><a href="streaming.py" shape="rect">streaming.py</a> - example of a push producer/consumer system</li>
+ <li><a href="filewatch.py" shape="rect">filewatch.py</a> - write the content of a file to standard out
+ one line at a time</li>
+ <li><a href="shoutcast.py" shape="rect">shoutcast.py</a> - example Shoutcast client</li>
+ <li><a href="gpsfix.py" shape="rect">gpsfix.py</a> - example using the SerialPort transport and GPS
+ protocols to display fix data as it is received from the device</li>
+ <li><a href="wxacceptance.py" shape="rect">wxacceptance.py</a> - acceptance tests for wxreactor</li>
+ <li><a href="postfix.py" shape="rect">postfix.py</a> - test application for PostfixTCPMapServer</li>
+ </ul>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/examples/longex.py b/doc/core/examples/longex.py
new file mode 100644
index 0000000..6fc9a7f
--- /dev/null
+++ b/doc/core/examples/longex.py
@@ -0,0 +1,66 @@
+"""Simple example of doing arbitarily long calculations nicely in Twisted.
+
+This is also a simple demonstration of twisted.protocols.basic.LineReceiver.
+"""
+
+from twisted.protocols import basic
+from twisted.internet import reactor
+from twisted.internet.protocol import ServerFactory
+
+class LongMultiplicationProtocol(basic.LineReceiver):
+ """A protocol for doing long multiplications.
+
+ It receives a list of numbers (seperated by whitespace) on a line, and
+ writes back the answer. The answer is calculated in chunks, so no one
+ calculation should block for long enough to matter.
+ """
+ def connectionMade(self):
+ self.workQueue = []
+
+ def lineReceived(self, line):
+ try:
+ numbers = map(long, line.split())
+ except ValueError:
+ self.sendLine('Error.')
+ return
+
+ if len(numbers) <= 1:
+ self.sendLine('Error.')
+ return
+
+ self.workQueue.append(numbers)
+ reactor.callLater(0, self.calcChunk)
+
+ def calcChunk(self):
+ # Make sure there's some work left; when multiple lines are received
+ # while processing is going on, multiple calls to reactor.callLater()
+ # can happen between calls to calcChunk().
+ if self.workQueue:
+ # Get the first bit of work off the queue
+ work = self.workQueue[0]
+
+ # Do a chunk of work: [a, b, c, ...] -> [a*b, c, ...]
+ work[:2] = [work[0] * work[1]]
+
+ # If this piece of work now has only one element, send it.
+ if len(work) == 1:
+ self.sendLine(str(work[0]))
+ del self.workQueue[0]
+
+ # Schedule this function to do more work, if there's still work
+ # to be done.
+ if self.workQueue:
+ reactor.callLater(0, self.calcChunk)
+
+
+class LongMultiplicationFactory(ServerFactory):
+ protocol = LongMultiplicationProtocol
+
+
+if __name__ == '__main__':
+ from twisted.python import log
+ import sys
+ log.startLogging(sys.stdout)
+ reactor.listenTCP(1234, LongMultiplicationFactory())
+ reactor.run()
+
diff --git a/doc/core/examples/longex2.py b/doc/core/examples/longex2.py
new file mode 100644
index 0000000..8758988
--- /dev/null
+++ b/doc/core/examples/longex2.py
@@ -0,0 +1,101 @@
+"""Example of doing arbitarily long calculations nicely in Twisted.
+
+This is also a simple demonstration of twisted.protocols.basic.LineReceiver.
+This example uses generators to do the calculation. It also tries to be
+a good example in division of responsibilities:
+- The protocol handles the wire layer, reading in lists of numbers
+ and writing out the result.
+- The factory decides on policy, and has relatively little knowledge
+ of the details of the protocol. Other protocols can use the same
+ factory class by intantiating and setting .protocol
+- The factory does little job itself: it is mostly a policy maker.
+ The 'smarts' are in free-standing functions which are written
+ for flexibility.
+
+The goal is for minimal dependencies:
+- You can use runIterator to run any iterator inside the Twisted
+ main loop.
+- You can use multiply whenever you need some way of multiplying
+ numbers such that the multiplications will happen asynchronously,
+ but it is your responsibility to schedule the multiplications.
+- You can use the protocol with other factories to implement other
+ functions that apply to arbitrary lists of longs.
+- You can use the factory with other protocols for support of legacy
+ protocols. In fact, the factory does not even have to be used as
+ a protocol factory. Here are easy ways to support the operation
+ over XML-RPC and PB.
+
+class Multiply(xmlrpc.XMLRPC):
+ def __init__(self): self.factory = Multiplication()
+ def xmlrpc_multiply(self, *numbers):
+ return self.factory.calc(map(long, numbers))
+
+class Multiply(pb.Referencable):
+ def __init__(self): self.factory = Multiplication()
+ def remote_multiply(self, *numbers):
+ return self.factory.calc(map(long, numbers))
+
+Note:
+Multiplying zero numbers is a perfectly sensible operation, and the
+result is 1. In that, this example departs from doc/examples/longex.py,
+which errors out when trying to do this.
+"""
+from __future__ import generators
+from twisted.protocols import basic
+from twisted.internet import defer, protocol
+
+def runIterator(reactor, iterator):
+ try:
+ iterator.next()
+ except StopIteration:
+ pass
+ else:
+ reactor.callLater(0, runIterator, reactor, iterator)
+
+def multiply(numbers):
+ d = defer.Deferred()
+ def _():
+ acc = 1
+ while numbers:
+ acc *= numbers.pop()
+ yield None
+ d.callback(acc)
+ return d, _()
+
+class Numbers(basic.LineReceiver):
+ """Protocol for reading lists of numbers and manipulating them.
+
+ It receives a list of numbers (seperated by whitespace) on a line, and
+ writes back the answer. The exact algorithm to use depends on the
+ factory. It should return an str-able Deferred.
+ """
+ def lineReceived(self, line):
+ try:
+ numbers = map(long, line.split())
+ except ValueError:
+ self.sendLine('Error.')
+ return
+ deferred = self.factory.calc(numbers)
+ deferred.addCallback(str)
+ deferred.addCallback(self.sendLine)
+
+class Multiplication(protocol.ServerFactory):
+ """Factory for multiplying numbers.
+
+ It provides a function which calculates the multiplication
+ of a list of numbers. The function destroys its input.
+ Note that instances of this factory can use other formats
+ for transmitting the number lists, as long as they set
+ correct protoocl values.
+ """
+ protocol = Numbers
+ def calc(self, numbers):
+ deferred, iterator = multiply(numbers)
+ from twisted.internet import reactor
+ runIterator(reactor, iterator)
+ return deferred
+
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ reactor.listenTCP(1234, Multiplication())
+ reactor.run()
diff --git a/doc/core/examples/mouse.py b/doc/core/examples/mouse.py
new file mode 100755
index 0000000..a62ed81
--- /dev/null
+++ b/doc/core/examples/mouse.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Example using MouseMan protocol with the SerialPort transport.
+"""
+
+# TODO set tty modes, etc.
+# This works for me:
+
+# speed 1200 baud; rows 0; columns 0; line = 0;
+# intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D;
+# eol = <undef>; eol2 = <undef>; start = ^Q; stop = ^S; susp = ^Z;
+# rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
+# -parenb -parodd cs7 hupcl -cstopb cread clocal -crtscts ignbrk
+# -brkint ignpar -parmrk -inpck -istrip -inlcr -igncr -icrnl -ixon
+# -ixoff -iuclc -ixany -imaxbel -opost -olcuc -ocrnl -onlcr -onocr
+# -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 -isig -icanon -iexten
+# -echo -echoe -echok -echonl -noflsh -xcase -tostop -echoprt -echoctl
+# -echoke
+
+import sys
+from twisted.python import usage, log
+from twisted.protocols.mice import mouseman
+
+if sys.platform == 'win32':
+ # win32 serial does not work yet!
+ raise NotImplementedError, "The SerialPort transport does not currently support Win32"
+ from twisted.internet import win32eventreactor
+ win32eventreactor.install()
+
+class Options(usage.Options):
+ optParameters = [
+ ['port', 'p', '/dev/mouse', 'Device for serial mouse'],
+ ['baudrate', 'b', '1200', 'Baudrate for serial mouse'],
+ ['outfile', 'o', None, 'Logfile [default: sys.stdout]'],
+ ]
+
+class McFooMouse(mouseman.MouseMan):
+ def down_left(self):
+ log.msg("LEFT")
+
+ def up_left(self):
+ log.msg("left")
+
+ def down_middle(self):
+ log.msg("MIDDLE")
+
+ def up_middle(self):
+ log.msg("middle")
+
+ def down_right(self):
+ log.msg("RIGHT")
+
+ def up_right(self):
+ log.msg("right")
+
+ def move(self, x, y):
+ log.msg("(%d,%d)" % (x, y))
+
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ from twisted.internet.serialport import SerialPort
+ o = Options()
+ try:
+ o.parseOptions()
+ except usage.UsageError, errortext:
+ print "%s: %s" % (sys.argv[0], errortext)
+ print "%s: Try --help for usage details." % (sys.argv[0])
+ raise SystemExit, 1
+
+ logFile = sys.stdout
+ if o.opts['outfile']:
+ logFile = o.opts['outfile']
+ log.startLogging(logFile)
+
+ SerialPort(McFooMouse(), o.opts['port'], reactor, baudrate=int(o.opts['baudrate']))
+ reactor.run()
diff --git a/doc/core/examples/pb_exceptions.py b/doc/core/examples/pb_exceptions.py
new file mode 100644
index 0000000..00753a4
--- /dev/null
+++ b/doc/core/examples/pb_exceptions.py
@@ -0,0 +1,36 @@
+
+from twisted.python import util
+from twisted.spread import pb
+from twisted.cred import portal, checkers, credentials
+
+class Avatar(pb.Avatar):
+ def perspective_exception(self, x):
+ return x / 0
+
+class Realm:
+ def requestAvatar(self, interface, mind, *interfaces):
+ if pb.IPerspective in interfaces:
+ return pb.IPerspective, Avatar(), lambda: None
+
+def cbLogin(avatar):
+ avatar.callRemote("exception", 10).addCallback(str).addCallback(util.println)
+
+def ebLogin(failure):
+ print failure
+
+def main():
+ c = checkers.InMemoryUsernamePasswordDatabaseDontUse(user="pass")
+ p = portal.Portal(Realm(), [c])
+ server = pb.PBServerFactory(p)
+ server.unsafeTracebacks = True
+ client = pb.PBClientFactory()
+ login = client.login(credentials.UsernamePassword("user", "pass"))
+ login.addCallback(cbLogin).addErrback(ebLogin).addBoth(lambda: reactor.stop())
+
+ from twisted.internet import reactor
+ p = reactor.listenTCP(0, server)
+ c = reactor.connectTCP('127.0.0.1', p.getHost().port, client)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/pbbenchclient.py b/doc/core/examples/pbbenchclient.py
new file mode 100644
index 0000000..9cd2b31
--- /dev/null
+++ b/doc/core/examples/pbbenchclient.py
@@ -0,0 +1,42 @@
+
+from twisted.spread import pb
+from twisted.internet import defer, reactor
+from twisted.cred.credentials import UsernamePassword
+import time
+
+class PBBenchClient:
+ hostname = 'localhost'
+ portno = pb.portno
+ calledThisSecond = 0
+
+ def callLoop(self, ignored):
+ d1 = self.persp.callRemote("simple")
+ d2 = self.persp.callRemote("complexTypes")
+ defer.DeferredList([d1, d2]).addCallback(self.callLoop)
+ self.calledThisSecond += 1
+ thisSecond = int(time.time())
+ if thisSecond != self.lastSecond:
+ if thisSecond - self.lastSecond > 1:
+ print "WARNING it took more than one second"
+ print 'cps:', self.calledThisSecond
+ self.calledThisSecond = 0
+ self.lastSecond = thisSecond
+
+ def _cbPerspective(self, persp):
+ self.persp = persp
+ self.lastSecond = int(time.time())
+ self.callLoop(None)
+
+ def runTest(self):
+ factory = pb.PBClientFactory()
+ reactor.connectTCP(self.hostname, self.portno, factory)
+ factory.login(UsernamePassword("benchmark", "benchmark")).addCallback(self._cbPerspective)
+
+
+def main():
+ PBBenchClient().runTest()
+ from twisted.internet import reactor
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/pbbenchserver.py b/doc/core/examples/pbbenchserver.py
new file mode 100644
index 0000000..324f23f
--- /dev/null
+++ b/doc/core/examples/pbbenchserver.py
@@ -0,0 +1,54 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Server for PB benchmark."""
+
+from zope.interface import implements
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred.portal import IRealm
+
+class PBBenchPerspective(pb.Avatar):
+ callsPerSec = 0
+ def __init__(self):
+ pass
+
+ def perspective_simple(self):
+ self.callsPerSec = self.callsPerSec + 1
+ return None
+
+ def printCallsPerSec(self):
+ print '(s) cps:', self.callsPerSec
+ self.callsPerSec = 0
+ reactor.callLater(1, self.printCallsPerSec)
+
+ def perspective_complexTypes(self):
+ return ['a', 1, 1l, 1.0, [], ()]
+
+
+class SimpleRealm:
+ implements(IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pb.IPerspective in interfaces:
+ p = PBBenchPerspective()
+ p.printCallsPerSec()
+ return pb.IPerspective, p, lambda : None
+ else:
+ raise NotImplementedError("no interface")
+
+
+def main():
+ from twisted.cred.portal import Portal
+ from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+ portal = Portal(SimpleRealm())
+ checker = InMemoryUsernamePasswordDatabaseDontUse()
+ checker.addUser("benchmark", "benchmark")
+ portal.registerChecker(checker)
+ reactor.listenTCP(8787, pb.PBServerFactory(portal))
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/pbecho.py b/doc/core/examples/pbecho.py
new file mode 100644
index 0000000..45df949
--- /dev/null
+++ b/doc/core/examples/pbecho.py
@@ -0,0 +1,51 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+if __name__ == '__main__':
+ # Avoid using any names defined in the "__main__" module.
+ from pbecho import main
+ raise SystemExit(main())
+
+from zope.interface import implements
+
+from twisted.spread import pb
+from twisted.cred.portal import IRealm
+
+class DefinedError(pb.Error):
+ pass
+
+
+class SimplePerspective(pb.Avatar):
+
+ def perspective_echo(self, text):
+ print 'echoing',text
+ return text
+
+ def perspective_error(self):
+ raise DefinedError("exception!")
+
+ def logout(self):
+ print self, "logged out"
+
+
+class SimpleRealm:
+ implements(IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pb.IPerspective in interfaces:
+ avatar = SimplePerspective()
+ return pb.IPerspective, avatar, avatar.logout
+ else:
+ raise NotImplementedError("no interface")
+
+
+def main():
+ from twisted.internet import reactor
+ from twisted.cred.portal import Portal
+ from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+ portal = Portal(SimpleRealm())
+ checker = InMemoryUsernamePasswordDatabaseDontUse()
+ checker.addUser("guest", "guest")
+ portal.registerChecker(checker)
+ reactor.listenTCP(pb.portno, pb.PBServerFactory(portal))
+ reactor.run()
diff --git a/doc/core/examples/pbechoclient.py b/doc/core/examples/pbechoclient.py
new file mode 100644
index 0000000..b7435f7
--- /dev/null
+++ b/doc/core/examples/pbechoclient.py
@@ -0,0 +1,32 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import reactor
+from twisted.spread import pb
+from twisted.cred.credentials import UsernamePassword
+
+from pbecho import DefinedError
+
+def success(message):
+ print "Message received:",message
+ # reactor.stop()
+
+def failure(error):
+ t = error.trap(DefinedError)
+ print "error received:", t
+ reactor.stop()
+
+def connected(perspective):
+ perspective.callRemote('echo', "hello world").addCallbacks(success, failure)
+ perspective.callRemote('error').addCallbacks(success, failure)
+ print "connected."
+
+
+factory = pb.PBClientFactory()
+reactor.connectTCP("localhost", pb.portno, factory)
+factory.login(
+ UsernamePassword("guest", "guest")).addCallbacks(connected, failure)
+
+reactor.run()
diff --git a/doc/core/examples/pbgtk2.py b/doc/core/examples/pbgtk2.py
new file mode 100644
index 0000000..b5f3e7f
--- /dev/null
+++ b/doc/core/examples/pbgtk2.py
@@ -0,0 +1,122 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from __future__ import nested_scopes
+
+from twisted.internet import gtk2reactor
+gtk2reactor.install()
+
+import gtk
+from gtk import glade
+from twisted import copyright
+from twisted.internet import reactor, defer
+from twisted.python import failure, log, util
+from twisted.spread import pb
+from twisted.cred.credentials import UsernamePassword
+from twisted.internet import error as netError
+
+
+class LoginDialog:
+ def __init__(self, deferred):
+ self.deferredResult = deferred
+
+ gladefile = util.sibpath(__file__, "pbgtk2login.glade")
+ self.glade = glade.XML(gladefile)
+
+ self.glade.signal_autoconnect(self)
+
+ self.setWidgetsFromGladefile()
+ self._loginDialog.show()
+
+ def setWidgetsFromGladefile(self):
+ widgets = ("hostEntry", "portEntry", "userNameEntry", "passwordEntry",
+ "statusBar", "loginDialog")
+ gw = self.glade.get_widget
+ for widgetName in widgets:
+ setattr(self, "_" + widgetName, gw(widgetName))
+
+ self._statusContext = self._statusBar.get_context_id("Login dialog.")
+
+ def on_loginDialog_response(self, widget, response):
+ handlers = {gtk.RESPONSE_NONE: self.windowClosed,
+ gtk.RESPONSE_DELETE_EVENT: self.windowClosed,
+ gtk.RESPONSE_OK: self.doLogin,
+ gtk.RESPONSE_CANCEL: self.cancelled}
+ handlers.get(response)()
+
+ def on_loginDialog_close(self, widget, userdata=None):
+ self.windowClosed()
+
+ def cancelled(self):
+ if not self.deferredResult.called:
+ self.deferredResult.errback()
+ self._loginDialog.destroy()
+
+ def windowClosed(self, reason=None):
+ if not self.deferredResult.called:
+ self.deferredResult.errback()
+
+ def doLogin(self):
+ host = self._hostEntry.get_text()
+ port = int(self._portEntry.get_text())
+ userName = self._userNameEntry.get_text()
+ password = self._passwordEntry.get_text()
+
+ client_factory = pb.PBClientFactory()
+ reactor.connectTCP(host, port, client_factory)
+ creds = UsernamePassword(userName, password)
+ client_factory.login(creds).addCallbacks(self._cbGotPerspective, self._ebFailedLogin)
+
+ self.statusMsg("Contacting server...")
+
+ def _cbGotPerspective(self, perspective):
+ self.statusMsg("Connected to server.")
+ self.deferredResult.callback(perspective)
+ self._loginDialog.destroy()
+
+ def _ebFailedLogin(self, reason):
+ if isinstance(reason, failure.Failure):
+ text = str(reason.value)
+ else:
+ text = str(reason)
+
+ self.statusMsg(text)
+ msg = gtk.MessageDialog(self._loginDialog,
+ gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_ERROR,
+ gtk.BUTTONS_CLOSE,
+ text)
+ msg.show_all()
+ msg.connect("response", lambda *a: msg.destroy())
+
+ def statusMsg(self, text):
+ self._statusBar.push(self._statusContext, text)
+
+
+class EchoClient:
+ def __init__(self, echoer):
+ self.echoer = echoer
+ w = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ vb = gtk.VBox(); b = gtk.Button("Echo:")
+ self.entry = gtk.Entry(); self.outry = gtk.Entry()
+ w.add(vb)
+ map(vb.add, [b, self.entry, self.outry])
+ b.connect('clicked', self.clicked)
+ w.connect('destroy', self.stop)
+ w.show_all()
+
+ def clicked(self, b):
+ txt = self.entry.get_text()
+ self.entry.set_text("")
+ self.echoer.callRemote('echo',txt).addCallback(self.outry.set_text)
+
+ def stop(self, b):
+ reactor.stop()
+
+d = defer.Deferred()
+LoginDialog(d)
+d.addCallbacks(EchoClient,
+ lambda _: reactor.stop())
+
+reactor.run()
diff --git a/doc/core/examples/pbgtk2login.glade b/doc/core/examples/pbgtk2login.glade
new file mode 100644
index 0000000..6b5eb01
--- /dev/null
+++ b/doc/core/examples/pbgtk2login.glade
@@ -0,0 +1,330 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkDialog" id="loginDialog">
+ <property name="title" translatable="yes">Login</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="has_separator">True</property>
+ <signal name="response" handler="on_loginDialog_response" last_modification_time="Sun, 21 Sep 2003 05:27:45 GMT"/>
+ <signal name="close" handler="on_loginDialog_close" last_modification_time="Sun, 21 Sep 2003 05:27:49 GMT"/>
+
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+ <child>
+ <widget class="GtkButton" id="cancelbutton1">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="label">gtk-cancel</property>
+ <property name="use_stock">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="response_id">-6</property>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="loginButton">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="response_id">-5</property>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="stock">gtk-ok</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Login</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkStatusbar" id="statusBar">
+ <property name="visible">True</property>
+ <property name="has_resize_grip">False</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">3</property>
+ <property name="n_columns">2</property>
+ <property name="homogeneous">False</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="hostLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Host:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">hostEntry</property>
+ <accessibility>
+ <atkrelation target="hostEntry" type="label-for"/>
+ <atkrelation target="portEntry" type="label-for"/>
+ </accessibility>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkEntry" id="hostEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The name of a host to connect to.</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">localhost</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ <accessibility>
+ <atkrelation target="hostLabel" type="labelled-by"/>
+ </accessibility>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="portEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The number of a port to connect on.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">8787</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ <property name="width_chars">5</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="y_options">fill</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="nameLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Name:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">userNameEntry</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="userNameEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">An identity to log in as.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">guest</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="passwordEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The Identity's log-in password.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">False</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">guest</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="passwordLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Password:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">passwordEntry</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/doc/core/examples/pbinterop.py b/doc/core/examples/pbinterop.py
new file mode 100644
index 0000000..167121e
--- /dev/null
+++ b/doc/core/examples/pbinterop.py
@@ -0,0 +1,71 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""PB interop server."""
+
+from twisted.spread import pb, jelly, flavors
+from twisted.internet import reactor
+
+
+class Interop(pb.Root):
+ """Test object for PB interop tests."""
+
+ def __init__(self):
+ self.o = pb.Referenceable()
+
+ def remote_int(self):
+ return 1
+
+ def remote_string(self):
+ return "string"
+
+ def remote_unicode(self):
+ return u"string"
+
+ def remote_float(self):
+ return 1.5
+
+ def remote_list(self):
+ return [1, 2, 3]
+
+ def remote_recursive(self):
+ l = []
+ l.append(l)
+ return l
+
+ def remote_dict(self):
+ return {1 : 2}
+
+ def remote_reference(self):
+ return self.o
+
+ def remote_local(self, obj):
+ d = obj.callRemote("hello")
+ d.addCallback(self._local_success)
+
+ def _local_success(self, result):
+ if result != "hello, world":
+ raise ValueError, "%r != %r" % (result, "hello, world")
+
+ def remote_receive(self, obj):
+ expected = [1, 1.5, "hi", u"hi", {1 : 2}]
+ if obj != expected:
+ raise ValueError, "%r != %r" % (obj, expected)
+
+ def remote_self(self, obj):
+ if obj != self:
+ raise ValueError, "%r != %r" % (obj, self)
+
+ def remote_copy(self, x):
+ o = flavors.Copyable()
+ o.x = x
+ return o
+
+
+if __name__ == '__main__':
+ reactor.listenTCP(8789, pb.PBServerFactory(Interop()))
+ reactor.run()
+
+
+
diff --git a/doc/core/examples/pbsimple.py b/doc/core/examples/pbsimple.py
new file mode 100644
index 0000000..68e4cbf
--- /dev/null
+++ b/doc/core/examples/pbsimple.py
@@ -0,0 +1,16 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class Echoer(pb.Root):
+ def remote_echo(self, st):
+ print 'echoing:', st
+ return st
+
+if __name__ == '__main__':
+ reactor.listenTCP(8789, pb.PBServerFactory(Echoer()))
+ reactor.run()
diff --git a/doc/core/examples/pbsimpleclient.py b/doc/core/examples/pbsimpleclient.py
new file mode 100644
index 0000000..05cb50a
--- /dev/null
+++ b/doc/core/examples/pbsimpleclient.py
@@ -0,0 +1,18 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.python import util
+
+factory = pb.PBClientFactory()
+reactor.connectTCP("localhost", 8789, factory)
+d = factory.getRootObject()
+d.addCallback(lambda object: object.callRemote("echo", "hello network"))
+d.addCallback(lambda echo: 'server echoed: '+echo)
+d.addErrback(lambda reason: 'error: '+str(reason.value))
+d.addCallback(util.println)
+d.addCallback(lambda _: reactor.stop())
+reactor.run()
diff --git a/doc/core/examples/postfix.py b/doc/core/examples/postfix.py
new file mode 100644
index 0000000..c13d640
--- /dev/null
+++ b/doc/core/examples/postfix.py
@@ -0,0 +1,29 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test app for PostfixTCPMapServer.
+
+Call with parameters KEY1=VAL1 KEY2=VAL2 ...
+"""
+
+import sys
+
+from twisted.internet import reactor
+from twisted.protocols import postfix
+from twisted.python import log
+
+log.startLogging(sys.stdout)
+
+d = {}
+for arg in sys.argv[1:]:
+ try:
+ k,v = arg.split('=', 1)
+ except ValueError:
+ k = arg
+ v = ''
+ d[k] = v
+
+f = postfix.PostfixTCPMapDictServerFactory(d)
+port = reactor.listenTCP(4242, f, interface='127.0.0.1')
+reactor.run()
diff --git a/doc/core/examples/ptyserv.py b/doc/core/examples/ptyserv.py
new file mode 100644
index 0000000..4e736a9
--- /dev/null
+++ b/doc/core/examples/ptyserv.py
@@ -0,0 +1,45 @@
+# Copyright (c) Twisted Matrix Laboratories
+# See LICENSE for details
+
+"""
+A PTY server that spawns a shell upon connection.
+
+Run this example by typing in:
+> python ptyserv.py
+
+Telnet to the server once you start it by typing in:
+> telnet localhost 5823
+"""
+
+from twisted.internet import reactor, protocol
+
+class FakeTelnet(protocol.Protocol):
+ commandToRun = ['/bin/sh'] # could have args too
+ dirToRunIn = '/tmp'
+ def connectionMade(self):
+ print 'connection made'
+ self.propro = ProcessProtocol(self)
+ reactor.spawnProcess(self.propro, self.commandToRun[0], self.commandToRun, {},
+ self.dirToRunIn, usePTY=1)
+ def dataReceived(self, data):
+ self.propro.transport.write(data)
+ def conectionLost(self):
+ print 'connection lost'
+ self.propro.tranport.loseConnection()
+
+class ProcessProtocol(protocol.ProcessProtocol):
+
+ def __init__(self, pr):
+ self.pr = pr
+
+ def outReceived(self, data):
+ self.pr.transport.write(data)
+
+ def processEnded(self, reason):
+ print 'protocol conection lost'
+ self.pr.transport.loseConnection()
+
+f = protocol.Factory()
+f.protocol = FakeTelnet
+reactor.listenTCP(5823, f)
+reactor.run()
diff --git a/doc/core/examples/pyui_bg.png b/doc/core/examples/pyui_bg.png
new file mode 100644
index 0000000..08d45ec
--- /dev/null
+++ b/doc/core/examples/pyui_bg.png
Binary files differ
diff --git a/doc/core/examples/pyuidemo.py b/doc/core/examples/pyuidemo.py
new file mode 100755
index 0000000..3b7a835
--- /dev/null
+++ b/doc/core/examples/pyuidemo.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Displays a frame with two buttons and a background image, using pyui library.
+
+Run this example by typing in:
+ python pyuidemo.py
+
+Select "Quit" button to exit demo.
+"""
+
+import pyui
+from twisted.internet import reactor, pyuisupport
+
+def onButton(self):
+ print "got a button"
+
+def onQuit(self):
+ reactor.stop()
+
+def main():
+ pyuisupport.install(args=(640, 480), kw={'renderer': '2d'})
+
+ w = pyui.widgets.Frame(50, 50, 400, 400, "clipme")
+ b = pyui.widgets.Button("A button is here", onButton)
+ q = pyui.widgets.Button("Quit!", onQuit)
+
+ w.addChild(b)
+ w.addChild(q)
+ w.pack()
+
+ w.setBackImage("pyui_bg.png")
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/recvfd.py b/doc/core/examples/recvfd.py
new file mode 100644
index 0000000..6d17c5f
--- /dev/null
+++ b/doc/core/examples/recvfd.py
@@ -0,0 +1,90 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Client-side of an example for sending file descriptors between processes over
+UNIX sockets. This client connects to a server listening on a UNIX socket and
+waits for one file descriptor to arrive over the connection. It displays the
+name of the file and the first 80 bytes it contains, then exits.
+
+To runb this example, run this program with one argument: a path giving the UNIX
+socket the server side of this example is already listening on. For example:
+
+ $ python recvfd.py /tmp/sendfd.sock
+
+See sendfd.py for the server side of this example.
+"""
+
+if __name__ == '__main__':
+ import recvfd
+ raise SystemExit(recvfd.main())
+
+import os, sys
+
+from zope.interface import implements
+
+from twisted.python.log import startLogging
+from twisted.python.filepath import FilePath
+from twisted.internet.defer import Deferred
+from twisted.internet.interfaces import IFileDescriptorReceiver
+from twisted.internet.protocol import Factory
+from twisted.protocols.basic import LineOnlyReceiver
+from twisted.internet.endpoints import UNIXClientEndpoint
+from twisted.internet import reactor
+
+class ReceiveFDProtocol(LineOnlyReceiver):
+ implements(IFileDescriptorReceiver)
+
+ descriptor = None
+
+ def __init__(self):
+ self.whenDisconnected = Deferred()
+
+
+ def fileDescriptorReceived(self, descriptor):
+ # Record the descriptor sent to us
+ self.descriptor = descriptor
+
+
+ def lineReceived(self, line):
+ if self.descriptor is None:
+ print "Received %r without receiving descriptor!" % (line,)
+ else:
+ # Use the previously received descriptor, along with the newly
+ # provided information about which file it is, to present some
+ # information to the user.
+ data = os.read(self.descriptor, 80)
+ print "Received %r from the server." % (line,)
+ print "First 80 bytes are:\n%r\n" % (data,)
+ os.close(self.descriptor)
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ self.whenDisconnected.callback(None)
+
+
+
+def main():
+ address = FilePath(sys.argv[1])
+
+ startLogging(sys.stdout)
+
+ factory = Factory()
+ factory.protocol = ReceiveFDProtocol
+ factory.quiet = True
+
+ endpoint = UNIXClientEndpoint(reactor, address.path)
+ connected = endpoint.connect(factory)
+
+ def succeeded(client):
+ return client.whenDisconnected
+ def failed(reason):
+ print "Could not connect:", reason.getErrorMessage()
+ def disconnected(ignored):
+ reactor.stop()
+
+ connected.addCallbacks(succeeded, failed)
+ connected.addCallback(disconnected)
+
+ reactor.run()
diff --git a/doc/core/examples/rotatinglog.py b/doc/core/examples/rotatinglog.py
new file mode 100644
index 0000000..288753c
--- /dev/null
+++ b/doc/core/examples/rotatinglog.py
@@ -0,0 +1,26 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example of using the rotating log.
+"""
+
+from twisted.python import log
+from twisted.python import logfile
+
+# rotate every 100 bytes
+f = logfile.LogFile("test.log", "/tmp", rotateLength=100)
+
+# setup logging to use our new logfile
+log.startLogging(f)
+
+# print a few message
+for i in range(10):
+ log.msg("this is a test of the logfile: %s" % i)
+
+# rotate the logfile manually
+f.rotate()
+
+log.msg("goodbye")
diff --git a/doc/core/examples/sendfd.py b/doc/core/examples/sendfd.py
new file mode 100644
index 0000000..b1948aa
--- /dev/null
+++ b/doc/core/examples/sendfd.py
@@ -0,0 +1,83 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Server-side of an example for sending file descriptors between processes over
+UNIX sockets. This server accepts connections on a UNIX socket and sends one
+file descriptor to them, along with the name of the file it is associated with.
+
+To run this example, run this program with two arguments: a path giving a UNIX
+socket to listen on (must not exist) and a path to a file to send to clients
+which connect (must exist). For example:
+
+ $ python sendfd.py /tmp/sendfd.sock /etc/motd
+
+It will listen for client connections until stopped (eg, using Control-C). Most
+interesting behavior happens on the client side.
+
+See recvfd.py for the client side of this example.
+"""
+
+if __name__ == '__main__':
+ import sendfd
+ raise SystemExit(sendfd.main())
+
+import sys
+
+from twisted.python.log import startLogging
+from twisted.python.filepath import FilePath
+from twisted.internet.protocol import Factory
+from twisted.protocols.basic import LineOnlyReceiver
+from twisted.internet import reactor
+
+class SendFDProtocol(LineOnlyReceiver):
+ def connectionMade(self):
+ # Open the desired file and keep a reference to it - keeping it open
+ # until we know the other side has it. Closing it early will prevent
+ # it from actually being sent.
+ self.fObj = self.factory.content.open()
+
+ # Tell the transport to send it. It is not necessarily sent when this
+ # method returns. The reactor may need to run for a while longer before
+ # that happens.
+ self.transport.sendFileDescriptor(self.fObj.fileno())
+
+ # Send along *at least* one byte, since one file descriptor was sent.
+ # In this case, send along the name of the file to let the other side
+ # have some idea what they're getting.
+ self.sendLine(self.factory.content.path)
+
+ # Give the other side a minute to deal with this. If they don't close
+ # the connection by then, we will do it for them.
+ self.timeoutCall = reactor.callLater(60, self.transport.loseConnection)
+
+
+ def connectionLost(self, reason):
+ # Clean up the file object, it is no longer needed.
+ self.fObj.close()
+ self.fObj = None
+
+ # Clean up the timeout, if necessary.
+ if self.timeoutCall.active():
+ self.timeoutCall.cancel()
+ self.timeoutCall = None
+
+
+def main():
+ address = FilePath(sys.argv[1])
+ content = FilePath(sys.argv[2])
+
+ if address.exists():
+ raise SystemExit("Cannot listen on an existing path")
+
+ if not content.isfile():
+ raise SystemExit("Content file must exist")
+
+ startLogging(sys.stdout)
+
+ serverFactory = Factory()
+ serverFactory.content = content
+ serverFactory.protocol = SendFDProtocol
+
+ port = reactor.listenUNIX(address.path, serverFactory)
+ reactor.run()
diff --git a/doc/core/examples/server.pem b/doc/core/examples/server.pem
new file mode 100644
index 0000000..80ef9dc
--- /dev/null
+++ b/doc/core/examples/server.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER
+MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD
+ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n
+cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzEL
+MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhv
+c3QxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEB
+BQADSwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
+5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQC
+MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl
+MB0GA1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7
+hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoT
+CE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlw
+dG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx
+LmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6
+BoJuVwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++
+7QGG/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JE
+WUQ9Ho4EzbYCOQ==
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
+5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAQJBAIqm/bz4NA1H++Vx5Ewx
+OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT
+ZIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4
+nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2
+HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNEH+vRWsAYU/gbx+OQB+7VOcBAiEA
+oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBDTCBuAIBADBTMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQ
+BgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20w
+XDANBgkqhkiG9w0BAQEFAANLADBIAkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5Ad
+NgLzJ1/MfsQQJ7hHVeHmTAjM664V+fXvwUGJLziCeBo1ysWLRnl8CQIDAQABoAAw
+DQYJKoZIhvcNAQEEBQADQQA7uqbrNTjVWpF6By5ZNPvhZ4YdFgkeXFVWi5ao/TaP
+Vq4BG021fJ9nlHRtr4rotpgHDX1rr+iWeHKsx4+5DRSy
+-----END CERTIFICATE REQUEST-----
diff --git a/doc/core/examples/shaper.py b/doc/core/examples/shaper.py
new file mode 100644
index 0000000..573d67c
--- /dev/null
+++ b/doc/core/examples/shaper.py
@@ -0,0 +1,52 @@
+# -*- Python -*-
+
+"""Example of rate-limiting your web server.
+
+Caveat emptor: While the transfer rates imposed by this mechanism will
+look accurate with wget's rate-meter, don't forget to examine your network
+interface's traffic statistics as well. The current implementation tends
+to create lots of small packets in some conditions, and each packet carries
+with it some bytes of overhead. Check to make sure this overhead is not
+costing you more bandwidth than you are saving by limiting the rate!
+"""
+
+from twisted.protocols import htb
+# for picklability
+import shaper
+
+serverFilter = htb.HierarchicalBucketFilter()
+serverBucket = htb.Bucket()
+
+# Cap total server traffic at 20 kB/s
+serverBucket.maxburst = 20000
+serverBucket.rate = 20000
+
+serverFilter.buckets[None] = serverBucket
+
+# Web service is also limited per-host:
+class WebClientBucket(htb.Bucket):
+ # Your first 10k is free
+ maxburst = 10000
+ # One kB/s thereafter.
+ rate = 1000
+
+webFilter = htb.FilterByHost(serverFilter)
+webFilter.bucketFactory = shaper.WebClientBucket
+
+servertype = "web" # "chargen"
+
+if servertype == "web":
+ from twisted.web import server, static
+ site = server.Site(static.File("/var/www"))
+ site.protocol = htb.ShapedProtocolFactory(site.protocol, webFilter)
+elif servertype == "chargen":
+ from twisted.protocols import wire
+ from twisted.internet import protocol
+
+ site = protocol.ServerFactory()
+ site.protocol = htb.ShapedProtocolFactory(wire.Chargen, webFilter)
+ #site.protocol = wire.Chargen
+
+from twisted.internet import reactor
+reactor.listenTCP(8000, site)
+reactor.run()
diff --git a/doc/core/examples/shoutcast.py b/doc/core/examples/shoutcast.py
new file mode 100644
index 0000000..26c7a7d
--- /dev/null
+++ b/doc/core/examples/shoutcast.py
@@ -0,0 +1,26 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Example Shoutcast client. Run with:
+
+python shoutcast.py localhost 8080
+"""
+
+import sys
+
+from twisted.internet import protocol, reactor
+from twisted.protocols.shoutcast import ShoutcastClient
+
+class Test(ShoutcastClient):
+ def gotMetaData(self, data):
+ print "meta:", data
+
+ def gotMP3Data(self, data):
+ pass
+
+host = sys.argv[1]
+port = int(sys.argv[2])
+
+protocol.ClientCreator(reactor, Test).connectTCP(host, port)
+reactor.run()
diff --git a/doc/core/examples/simple.tac b/doc/core/examples/simple.tac
new file mode 100644
index 0000000..02b3f81
--- /dev/null
+++ b/doc/core/examples/simple.tac
@@ -0,0 +1,39 @@
+# You can run this .tac file directly with:
+# twistd -ny simple.tac
+
+from twisted.application import service, internet
+from twisted.protocols import wire
+from twisted.internet import protocol
+from twisted.python import util
+
+application = service.Application('test')
+s = service.IServiceCollection(application)
+factory = protocol.ServerFactory()
+factory.protocol = wire.Echo
+internet.TCPServer(8080, factory).setServiceParent(s)
+
+internet.TCPServer(8081, factory).setServiceParent(s)
+internet.TimerService(5, util.println, "--MARK--").setServiceParent(s)
+
+class Foo(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.write('lalala\n')
+ def dataReceived(self, data):
+ print `data`
+
+factory = protocol.ClientFactory()
+factory.protocol = Foo
+internet.TCPClient('localhost', 8081, factory).setServiceParent(s)
+
+class FooService(service.Service):
+ def startService(self):
+ service.Service.startService(self)
+ print 'lala, starting'
+ def stopService(self):
+ service.Service.stopService(self)
+ print 'lala, stopping'
+ print self.parent.getServiceNamed(self.name) is self
+
+foo = FooService()
+foo.setName('foo')
+foo.setServiceParent(s)
diff --git a/doc/core/examples/simpleclient.py b/doc/core/examples/simpleclient.py
new file mode 100644
index 0000000..bba9f64
--- /dev/null
+++ b/doc/core/examples/simpleclient.py
@@ -0,0 +1,49 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example client. Run simpleserv.py first before running this.
+"""
+
+from twisted.internet import reactor, protocol
+
+
+# a client protocol
+
+class EchoClient(protocol.Protocol):
+ """Once connected, send a message, then print the result."""
+
+ def connectionMade(self):
+ self.transport.write("hello, world!")
+
+ def dataReceived(self, data):
+ "As soon as any data is received, write it back."
+ print "Server said:", data
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ print "connection lost"
+
+class EchoFactory(protocol.ClientFactory):
+ protocol = EchoClient
+
+ def clientConnectionFailed(self, connector, reason):
+ print "Connection failed - goodbye!"
+ reactor.stop()
+
+ def clientConnectionLost(self, connector, reason):
+ print "Connection lost - goodbye!"
+ reactor.stop()
+
+
+# this connects the protocol to a server runing on port 8000
+def main():
+ f = EchoFactory()
+ reactor.connectTCP("localhost", 8000, f)
+ reactor.run()
+
+# this only runs if the module was *not* imported
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/simpleserv.py b/doc/core/examples/simpleserv.py
new file mode 100644
index 0000000..228fe44
--- /dev/null
+++ b/doc/core/examples/simpleserv.py
@@ -0,0 +1,26 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import reactor, protocol
+
+
+class Echo(protocol.Protocol):
+ """This is just about the simplest possible protocol"""
+
+ def dataReceived(self, data):
+ "As soon as any data is received, write it back."
+ self.transport.write(data)
+
+
+def main():
+ """This runs the protocol on port 8000"""
+ factory = protocol.ServerFactory()
+ factory.protocol = Echo
+ reactor.listenTCP(8000,factory)
+ reactor.run()
+
+# this only runs if the module was *not* imported
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/stdin.py b/doc/core/examples/stdin.py
new file mode 100644
index 0000000..e987be5
--- /dev/null
+++ b/doc/core/examples/stdin.py
@@ -0,0 +1,30 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example of reading a line at a time from standard input
+without blocking the reactor.
+"""
+
+from twisted.internet import stdio
+from twisted.protocols import basic
+
+class Echo(basic.LineReceiver):
+ from os import linesep as delimiter
+
+ def connectionMade(self):
+ self.transport.write('>>> ')
+
+ def lineReceived(self, line):
+ self.sendLine('Echo: ' + line)
+ self.transport.write('>>> ')
+
+def main():
+ stdio.StandardIO(Echo())
+ from twisted.internet import reactor
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/stdiodemo.py b/doc/core/examples/stdiodemo.py
new file mode 100644
index 0000000..9132428
--- /dev/null
+++ b/doc/core/examples/stdiodemo.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Example using stdio, Deferreds, LineReceiver and twisted.web.client.
+
+Note that the WebCheckerCommandProtocol protocol could easily be used in e.g.
+a telnet server instead; see the comments for details.
+
+Based on an example by Abe Fettig.
+"""
+
+from twisted.internet import stdio, reactor
+from twisted.protocols import basic
+from twisted.web import client
+
+class WebCheckerCommandProtocol(basic.LineReceiver):
+ delimiter = '\n' # unix terminal style newlines. remove this line
+ # for use with Telnet
+
+ def connectionMade(self):
+ self.sendLine("Web checker console. Type 'help' for help.")
+
+ def lineReceived(self, line):
+ # Ignore blank lines
+ if not line: return
+
+ # Parse the command
+ commandParts = line.split()
+ command = commandParts[0].lower()
+ args = commandParts[1:]
+
+ # Dispatch the command to the appropriate method. Note that all you
+ # need to do to implement a new command is add another do_* method.
+ try:
+ method = getattr(self, 'do_' + command)
+ except AttributeError, e:
+ self.sendLine('Error: no such command.')
+ else:
+ try:
+ method(*args)
+ except Exception, e:
+ self.sendLine('Error: ' + str(e))
+
+ def do_help(self, command=None):
+ """help [command]: List commands, or show help on the given command"""
+ if command:
+ self.sendLine(getattr(self, 'do_' + command).__doc__)
+ else:
+ commands = [cmd[3:] for cmd in dir(self) if cmd.startswith('do_')]
+ self.sendLine("Valid commands: " +" ".join(commands))
+
+ def do_quit(self):
+ """quit: Quit this session"""
+ self.sendLine('Goodbye.')
+ self.transport.loseConnection()
+
+ def do_check(self, url):
+ """check <url>: Attempt to download the given web page"""
+ client.getPage(url).addCallback(
+ self.__checkSuccess).addErrback(
+ self.__checkFailure)
+
+ def __checkSuccess(self, pageData):
+ self.sendLine("Success: got %i bytes." % len(pageData))
+
+ def __checkFailure(self, failure):
+ self.sendLine("Failure: " + failure.getErrorMessage())
+
+ def connectionLost(self, reason):
+ # stop the reactor, only because this is meant to be run in Stdio.
+ reactor.stop()
+
+if __name__ == "__main__":
+ stdio.StandardIO(WebCheckerCommandProtocol())
+ reactor.run()
diff --git a/doc/core/examples/streaming.py b/doc/core/examples/streaming.py
new file mode 100644
index 0000000..06fcfd7
--- /dev/null
+++ b/doc/core/examples/streaming.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is a sample implementation of a Twisted push producer/consumer system. It
+consists of a TCP server which asks the user how many random integers they
+want, and it sends the result set back to the user, one result per line,
+and finally closes the connection.
+"""
+
+from sys import stdout
+from random import randrange
+
+from zope.interface import implements
+from twisted.python.log import startLogging
+from twisted.internet import interfaces, reactor
+from twisted.internet.protocol import Factory
+from twisted.protocols.basic import LineReceiver
+
+
+class Producer(object):
+ """
+ Send back the requested number of random integers to the client.
+ """
+
+ implements(interfaces.IPushProducer)
+
+ def __init__(self, proto, count):
+ self._proto = proto
+ self._goal = count
+ self._produced = 0
+ self._paused = False
+
+ def pauseProducing(self):
+ """
+ When we've produced data too fast, pauseProducing() will be called
+ (reentrantly from within resumeProducing's sendLine() method, most
+ likely), so set a flag that causes production to pause temporarily.
+ """
+ self._paused = True
+ print 'Pausing connection from %s' % self._proto.transport.getPeer()
+
+ def resumeProducing(self):
+ """
+ Resume producing integers.
+
+ This tells the push producer to (re-)add itself to the main loop and
+ produce integers for its consumer until the requested number of integers
+ were returned to the client.
+ """
+ self._paused = False
+
+ while not self._paused and self._produced < self._goal:
+ next_int = randrange(0, 10000)
+ self._proto.sendLine('%d' % next_int)
+ self._produced += 1
+
+ if self._produced == self._goal:
+ self._proto.transport.unregisterProducer()
+ self._proto.transport.loseConnection()
+
+ def stopProducing(self):
+ """
+ When a consumer has died, stop producing data for good.
+ """
+ self._produced = self._goal
+
+
+class ServeRandom(LineReceiver):
+ """
+ Serve up random integers.
+ """
+
+ def connectionMade(self):
+ """
+ Once the connection is made we ask the client how many random integers
+ the producer should return.
+ """
+ print 'Connection made from %s' % self.transport.getPeer()
+ self.sendLine('How many random integers do you want?')
+
+ def lineReceived(self, line):
+ """
+ This checks how many random integers the client expects in return and
+ tells the producer to start generating the data.
+ """
+ count = int(line.strip())
+ print 'Client requested %d random integers!' % count
+ producer = Producer(self, count)
+ self.transport.registerProducer(producer, True)
+ producer.resumeProducing()
+
+ def connectionLost(self, reason):
+ print 'Connection lost from %s' % self.transport.getPeer()
+
+
+startLogging(stdout)
+factory = Factory()
+factory.protocol = ServeRandom
+reactor.listenTCP(1234, factory)
+reactor.run()
diff --git a/doc/core/examples/testlogging.py b/doc/core/examples/testlogging.py
new file mode 100644
index 0000000..d44def7
--- /dev/null
+++ b/doc/core/examples/testlogging.py
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Test logging.
+
+Message should only be printed second time around.
+"""
+
+from twisted.python import log
+from twisted.internet import reactor
+
+import sys, warnings
+
+def test(i):
+ print "printed", i
+ log.msg("message %s" % i)
+ warnings.warn("warning %s" % i)
+ try:
+ raise RuntimeError, "error %s" % i
+ except:
+ log.err()
+
+def startlog():
+ log.startLogging(sys.stdout)
+
+def end():
+ reactor.stop()
+
+# pre-reactor run
+test(1)
+
+# after reactor run
+reactor.callLater(0.1, test, 2)
+reactor.callLater(0.2, startlog)
+
+# after startLogging
+reactor.callLater(0.3, test, 3)
+reactor.callLater(0.4, end)
+
+reactor.run()
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/classes.nib b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/classes.nib
new file mode 100644
index 0000000..71cb459
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,13 @@
+{
+ IBClasses = (
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
+ {
+ ACTIONS = {doTwistzillaFetch = id; };
+ CLASS = MyAppDelegate;
+ LANGUAGE = ObjC;
+ OUTLETS = {messageTextField = id; progressIndicator = id; resultTextField = id; };
+ SUPERCLASS = NSObject;
+ }
+ );
+ IBVersion = 1;
+} \ No newline at end of file
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/info.nib b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/info.nib
new file mode 100644
index 0000000..4f99a2d
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>29</key>
+ <string>127 344 318 44 0 0 1600 1002 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>291.0</string>
+ <key>IBLockedObjects</key>
+ <array>
+ <integer>204</integer>
+ </array>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>21</integer>
+ <integer>29</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>6L60</string>
+</dict>
+</plist>
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/keyedobjects.nib b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 0000000..e5caaf0
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/English.lproj/MainMenu.nib/keyedobjects.nib
Binary files differ
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/README.txt b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/README.txt
new file mode 100644
index 0000000..96010e2
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/README.txt
@@ -0,0 +1,6 @@
+Requires PyObjC 1.3.1 (svn r1589 or later)
+
+To run the demo:
+
+python setup.py py2app
+open dist/Twistzilla.app
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/Twistzilla.py b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/Twistzilla.py
new file mode 100644
index 0000000..45eeb8d
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/Twistzilla.py
@@ -0,0 +1,79 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+# import needed classes/functions from Cocoa
+from Foundation import *
+from AppKit import *
+
+# import Nib loading functionality from AppKit
+from PyObjCTools import NibClassBuilder, AppHelper
+
+from twisted.internet import _threadedselect
+_threadedselect.install()
+
+from twisted.internet import reactor, protocol
+from twisted.web import http
+from twisted.python import log
+import sys, urlparse
+
+# create ObjC classes as defined in MainMenu.nib
+NibClassBuilder.extractClasses("MainMenu")
+class TwistzillaClient(http.HTTPClient):
+ def __init__(self, delegate, urls):
+ self.urls = urls
+ self.delegate = delegate
+
+ def connectionMade(self):
+ self.sendCommand('GET', str(self.urls[2]))
+ self.sendHeader('Host', '%s:%d' % (self.urls[0], self.urls[1]))
+ self.sendHeader('User-Agent', 'CocoaTwistzilla')
+ self.endHeaders()
+
+ def handleResponse(self, data):
+ self.delegate.gotResponse_(data)
+
+class MyAppDelegate(NibClassBuilder.AutoBaseClass):
+ def gotResponse_(self, html):
+ s = self.resultTextField.textStorage()
+ s.replaceCharactersInRange_withString_((0, s.length()), html)
+ self.progressIndicator.stopAnimation_(self)
+
+ def doTwistzillaFetch_(self, sender):
+ s = self.resultTextField.textStorage()
+ s.deleteCharactersInRange_((0, s.length()))
+ self.progressIndicator.startAnimation_(self)
+ u = urlparse.urlparse(self.messageTextField.stringValue())
+ pos = u[1].find(':')
+ if pos == -1:
+ host, port = u[1], 80
+ else:
+ host, port = u[1][:pos], int(u[1][pos+1:])
+ if u[2] == '':
+ fname = '/'
+ else:
+ fname = u[2]
+ host = host.encode('utf8')
+ fname = fname.encode('utf8')
+ protocol.ClientCreator(reactor, TwistzillaClient, self, (host, port, fname)).connectTCP(host, port).addErrback(lambda f:self.gotResponse_(f.getBriefTraceback()))
+
+ def applicationDidFinishLaunching_(self, aNotification):
+ """
+ Invoked by NSApplication once the app is done launching and
+ immediately before the first pass through the main event
+ loop.
+ """
+ self.messageTextField.setStringValue_("http://www.twistedmatrix.com/")
+ reactor.interleave(AppHelper.callAfter)
+
+ def applicationShouldTerminate_(self, sender):
+ if reactor.running:
+ reactor.addSystemEventTrigger(
+ 'after', 'shutdown', AppHelper.stopEventLoop)
+ reactor.stop()
+ return False
+ return True
+
+if __name__ == '__main__':
+ log.startLogging(sys.stdout)
+ AppHelper.runEventLoop()
diff --git a/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/setup.py b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/setup.py
new file mode 100644
index 0000000..f3afe8a
--- /dev/null
+++ b/doc/core/examples/threadedselect/Cocoa/SimpleWebClient/setup.py
@@ -0,0 +1,14 @@
+"""
+Script for building the example.
+
+Usage:
+ python setup.py py2app
+"""
+
+from distutils.core import setup
+import py2app
+
+setup(
+ app = ['Twistzilla.py'],
+ data_files = ["English.lproj"],
+)
diff --git a/doc/core/examples/threadedselect/README b/doc/core/examples/threadedselect/README
new file mode 100644
index 0000000..5d3feab
--- /dev/null
+++ b/doc/core/examples/threadedselect/README
@@ -0,0 +1,15 @@
+The examples in this directory import a private module from the
+twisted.internet package. The _threadedselect module provides an object
+which is similar to a Twisted reactor in many ways, but which is not
+actually intended to be used in the same way as a Twisted reactor (it has a
+method named interleave which is intended to be the main entrypoint). This
+functionality should be considered highly experimental and the API subject
+to change at any time.
+
+Possibly the best way to make use of this functionality is to use it to
+implement an object which actually presents the Twisted reactor interface
+(specifically, an object with a run method). That object can then be used
+by application-code in the usual way.
+
+Another course of action is to avoid _threadedselect entirely until the
+issues surrounding it have been resolved.
diff --git a/doc/core/examples/threadedselect/blockingdemo.py b/doc/core/examples/threadedselect/blockingdemo.py
new file mode 100644
index 0000000..7dc98df
--- /dev/null
+++ b/doc/core/examples/threadedselect/blockingdemo.py
@@ -0,0 +1,92 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import _threadedselect
+_threadedselect.install()
+
+from twisted.internet.defer import Deferred
+from twisted.python.failure import Failure
+from twisted.internet import reactor
+from twisted.python.runtime import seconds
+from itertools import count
+from Queue import Queue, Empty
+
+class TwistedManager(object):
+ def __init__(self):
+ self.twistedQueue = Queue()
+ self.key = count()
+ self.results = {}
+
+ def getKey(self):
+ # get a unique identifier
+ return self.key.next()
+
+ def start(self):
+ # start the reactor
+ reactor.interleave(self.twistedQueue.put)
+
+ def _stopIterating(self, value, key):
+ self.results[key] = value
+
+ def stop(self):
+ # stop the reactor
+ key = self.getKey()
+ reactor.addSystemEventTrigger('after', 'shutdown',
+ self._stopIterating, True, key)
+ reactor.stop()
+ self.iterate(key)
+
+ def getDeferred(self, d):
+ # get the result of a deferred or raise if it failed
+ key = self.getKey()
+ d.addBoth(self._stopIterating, key)
+ res = self.iterate(key)
+ if isinstance(res, Failure):
+ res.raiseException()
+ return res
+
+ def poll(self, noLongerThan=1.0):
+ # poll the reactor for up to noLongerThan seconds
+ base = seconds()
+ try:
+ while (seconds() - base) <= noLongerThan:
+ callback = self.twistedQueue.get_nowait()
+ callback()
+ except Empty:
+ pass
+
+ def iterate(self, key=None):
+ # iterate the reactor until it has the result we're looking for
+ while key not in self.results:
+ callback = self.twistedQueue.get()
+ callback()
+ return self.results.pop(key)
+
+def fakeDeferred(msg):
+ d = Deferred()
+ def cb():
+ print "deferred called back"
+ d.callback(msg)
+ reactor.callLater(2, cb)
+ return d
+
+def fakeCallback():
+ print "twisted is still running"
+
+def main():
+ m = TwistedManager()
+ print "starting"
+ m.start()
+ print "setting up a 1sec callback"
+ reactor.callLater(1, fakeCallback)
+ print "getting a deferred"
+ res = m.getDeferred(fakeDeferred("got it!"))
+ print "got the deferred:", res
+ print "stopping"
+ m.stop()
+ print "stopped"
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/threadedselect/pygamedemo.py b/doc/core/examples/threadedselect/pygamedemo.py
new file mode 100644
index 0000000..a2bec33
--- /dev/null
+++ b/doc/core/examples/threadedselect/pygamedemo.py
@@ -0,0 +1,78 @@
+from __future__ import generators
+
+# import Twisted and install
+from twisted.internet import _threadedselect
+_threadedselect.install()
+from twisted.internet import reactor
+
+import os
+
+import pygame
+from pygame.locals import *
+
+try:
+ import pygame.fastevent as eventmodule
+except ImportError:
+ import pygame.event as eventmodule
+
+
+# You can customize this if you use your
+# own events, but you must OBEY:
+#
+# USEREVENT <= TWISTEDEVENT < NUMEVENTS
+#
+TWISTEDEVENT = USEREVENT
+
+def postTwistedEvent(func):
+ # if not using pygame.fastevent, this can explode if the queue
+ # fills up.. so that's bad. Use pygame.fastevent, in pygame CVS
+ # as of 2005-04-18.
+ eventmodule.post(eventmodule.Event(TWISTEDEVENT, iterateTwisted=func))
+
+def helloWorld():
+ print "hello, world"
+ reactor.callLater(1, helloWorld)
+reactor.callLater(1, helloWorld)
+
+def twoSecondsPassed():
+ print "two seconds passed"
+reactor.callLater(2, twoSecondsPassed)
+
+def eventIterator():
+ while True:
+ yield eventmodule.wait()
+ while True:
+ event = eventmodule.poll()
+ if event.type == NOEVENT:
+ break
+ else:
+ yield event
+
+def main():
+ pygame.init()
+ if hasattr(eventmodule, 'init'):
+ eventmodule.init()
+ screen = pygame.display.set_mode((300, 300))
+
+ # send an event when twisted wants attention
+ reactor.interleave(postTwistedEvent)
+ # make shouldQuit a True value when it's safe to quit
+ # by appending a value to it. This ensures that
+ # Twisted gets to shut down properly.
+ shouldQuit = []
+ reactor.addSystemEventTrigger('after', 'shutdown', shouldQuit.append, True)
+
+ for event in eventIterator():
+ if event.type == TWISTEDEVENT:
+ event.iterateTwisted()
+ if shouldQuit:
+ break
+ elif event.type == QUIT:
+ reactor.stop()
+ elif event.type == KEYDOWN and event.key == K_ESCAPE:
+ reactor.stop()
+
+ pygame.quit()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/examples/tkinterdemo.py b/doc/core/examples/tkinterdemo.py
new file mode 100644
index 0000000..1fad50e
--- /dev/null
+++ b/doc/core/examples/tkinterdemo.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example of using Twisted with Tkinter.
+Displays a frame with buttons that responds to mouse clicks.
+
+Run this example by typing in:
+ python tkinterdemo.py
+"""
+
+
+from Tkinter import Tk, Frame, Button, LEFT
+from twisted.internet import reactor, tksupport
+
+
+class App(object):
+
+ def onQuit(self):
+ print "Quit!"
+ reactor.stop()
+
+ def onButton(self):
+ print "Hello!"
+
+ def __init__(self, master):
+ frame = Frame(master)
+ frame.pack()
+
+ q = Button(frame, text="Quit!", command=self.onQuit)
+ b = Button(frame, text="Hello!", command=self.onButton)
+
+ q.pack(side=LEFT)
+ b.pack(side=LEFT)
+
+
+if __name__ == '__main__':
+ root = Tk()
+ tksupport.install(root)
+ app = App(root)
+ reactor.run()
diff --git a/doc/core/examples/twistd-logging.tac b/doc/core/examples/twistd-logging.tac
new file mode 100644
index 0000000..2302558
--- /dev/null
+++ b/doc/core/examples/twistd-logging.tac
@@ -0,0 +1,33 @@
+# Invoke this script with:
+
+# $ twistd -ny twistd-logging.tac
+
+# It will create a log file named "twistd-logging.log". The log file will
+# be formatted such that each line contains the representation of the dict
+# structure of each log message.
+
+from twisted.application.service import Application
+from twisted.python.log import ILogObserver, msg
+from twisted.python.util import untilConcludes
+from twisted.internet.task import LoopingCall
+
+
+logfile = open("twistd-logging.log", "a")
+
+
+def log(eventDict):
+ # untilConcludes is necessary to retry the operation when the system call
+ # has been interrupted.
+ untilConcludes(logfile.write, "Got a log! %r\n" % eventDict)
+ untilConcludes(logfile.flush)
+
+
+def logSomething():
+ msg("A log message")
+
+
+LoopingCall(logSomething).start(1)
+
+application = Application("twistd-logging")
+application.setComponent(ILogObserver, log)
+
diff --git a/doc/core/examples/wxacceptance.py b/doc/core/examples/wxacceptance.py
new file mode 100644
index 0000000..75394eb
--- /dev/null
+++ b/doc/core/examples/wxacceptance.py
@@ -0,0 +1,113 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Acceptance tests for wxreactor.
+
+Please test on Linux, Win32 and OS X:
+1. Startup event is called at startup.
+2. Scheduled event is called after 2 seconds.
+3. Shutdown takes 3 seconds, both when quiting from menu and when closing
+ window (e.g. Alt-F4 in metacity). This tests reactor.stop() and
+ wxApp.ExitEventLoop().
+4. 'hello, world' continues to be printed even when modal dialog is open
+ (use dialog menu item), when menus are held down, when window is being
+ dragged.
+"""
+
+import sys, time
+
+try:
+ from wx import Frame as wxFrame, DefaultPosition as wxDefaultPosition, \
+ Size as wxSize, Menu as wxMenu, MenuBar as wxMenuBar, \
+ EVT_MENU, MessageDialog as wxMessageDialog, App as wxApp
+except ImportError, e:
+ from wxPython.wx import *
+
+from twisted.python import log
+from twisted.internet import wxreactor
+wxreactor.install()
+from twisted.internet import reactor, defer
+
+
+# set up so that "hello, world" is printed continously
+dc = None
+def helloWorld():
+ global dc
+ print "hello, world", time.time()
+ dc = reactor.callLater(0.1, helloWorld)
+dc = reactor.callLater(0.1, helloWorld)
+
+def twoSecondsPassed():
+ print "two seconds passed"
+
+def printer(s):
+ print s
+
+def shutdown():
+ print "shutting down in 3 seconds"
+ if dc.active():
+ dc.cancel()
+ reactor.callLater(1, printer, "2...")
+ reactor.callLater(2, printer, "1...")
+ reactor.callLater(3, printer, "0...")
+ d = defer.Deferred()
+ reactor.callLater(3, d.callback, 1)
+ return d
+
+def startup():
+ print "Start up event!"
+
+reactor.callLater(2, twoSecondsPassed)
+reactor.addSystemEventTrigger("after", "startup", startup)
+reactor.addSystemEventTrigger("before", "shutdown", shutdown)
+
+
+ID_EXIT = 101
+ID_DIALOG = 102
+
+class MyFrame(wxFrame):
+ def __init__(self, parent, ID, title):
+ wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, wxSize(300, 200))
+ menu = wxMenu()
+ menu.Append(ID_DIALOG, "D&ialog", "Show dialog")
+ menu.Append(ID_EXIT, "E&xit", "Terminate the program")
+ menuBar = wxMenuBar()
+ menuBar.Append(menu, "&File")
+ self.SetMenuBar(menuBar)
+ EVT_MENU(self, ID_EXIT, self.DoExit)
+ EVT_MENU(self, ID_DIALOG, self.DoDialog)
+ # you really ought to do this instead of reactor.stop() in
+ # DoExit, but for the sake of testing we'll let closing the
+ # window shutdown wx without reactor.stop(), to make sure that
+ # still does the right thing.
+ #EVT_CLOSE(self, lambda evt: reactor.stop())
+
+ def DoDialog(self, event):
+ dl = wxMessageDialog(self, "Check terminal to see if messages are still being "
+ "printed by Twisted.")
+ dl.ShowModal()
+ dl.Destroy()
+
+ def DoExit(self, event):
+ reactor.stop()
+
+
+class MyApp(wxApp):
+
+ def OnInit(self):
+ frame = MyFrame(None, -1, "Hello, world")
+ frame.Show(True)
+ self.SetTopWindow(frame)
+ return True
+
+
+def demo():
+ log.startLogging(sys.stdout)
+ app = MyApp(0)
+ reactor.registerWxApp(app)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ demo()
diff --git a/doc/core/examples/wxdemo.py b/doc/core/examples/wxdemo.py
new file mode 100644
index 0000000..a9afdd3
--- /dev/null
+++ b/doc/core/examples/wxdemo.py
@@ -0,0 +1,64 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Demo of wxPython integration with Twisted."""
+
+import sys
+
+from wx import Frame, DefaultPosition, Size, Menu, MenuBar, App
+from wx import EVT_MENU, EVT_CLOSE
+
+from twisted.python import log
+from twisted.internet import wxreactor
+wxreactor.install()
+
+# import t.i.reactor only after installing wxreactor:
+from twisted.internet import reactor
+
+
+ID_EXIT = 101
+
+class MyFrame(Frame):
+ def __init__(self, parent, ID, title):
+ Frame.__init__(self, parent, ID, title, DefaultPosition, Size(300, 200))
+ menu = Menu()
+ menu.Append(ID_EXIT, "E&xit", "Terminate the program")
+ menuBar = MenuBar()
+ menuBar.Append(menu, "&File")
+ self.SetMenuBar(menuBar)
+ EVT_MENU(self, ID_EXIT, self.DoExit)
+
+ # make sure reactor.stop() is used to stop event loop:
+ EVT_CLOSE(self, lambda evt: reactor.stop())
+
+ def DoExit(self, event):
+ reactor.stop()
+
+
+class MyApp(App):
+
+ def twoSecondsPassed(self):
+ print "two seconds passed"
+
+ def OnInit(self):
+ frame = MyFrame(None, -1, "Hello, world")
+ frame.Show(True)
+ self.SetTopWindow(frame)
+ # look, we can use twisted calls!
+ reactor.callLater(2, self.twoSecondsPassed)
+ return True
+
+
+def demo():
+ log.startLogging(sys.stdout)
+
+ # register the App instance with Twisted:
+ app = MyApp(0)
+ reactor.registerWxApp(app)
+
+ # start the event loop:
+ reactor.run()
+
+
+if __name__ == '__main__':
+ demo()
diff --git a/doc/core/howto/amp.html b/doc/core/howto/amp.html
new file mode 100644
index 0000000..bf6a76c
--- /dev/null
+++ b/doc/core/howto/amp.html
@@ -0,0 +1,349 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: <b>A</b>synchronous <b>M</b>essaging <b>P</b>rotocol Overview</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title"><b>A</b>synchronous <b>M</b>essaging <b>P</b>rotocol Overview</h1>
+ <div class="toc"><ol><li><a href="#auto0">Setting Up</a></li><li><a href="#auto1">Commands</a></li><li><a href="#auto2">Locators</a></li><li><a href="#auto3">Box Receivers</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>The purpose of this guide is to describe the uses for and usage of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.amp.html" title="twisted.protocols.amp">twisted.protocols.amp</a></code> beyond what is explained in the API documentation. It will show you how to implement an AMP server which can respond to commands or interact directly with individual messages. It will also show you how to implement an AMP client which can issue commands to a server.</p>
+
+ <p>AMP is a bidirectional command/response-oriented protocol intended to be extended with application-specific request types and handlers. Various simple data types are supported and support for new data types can be added by applications.</p>
+
+ <h2>Setting Up<a name="auto0"/></h2>
+
+ <p>AMP runs over a stream-oriented connection-based protocol, such as TCP or SSL. Before you can use any features of the AMP protocol, you need a connection. The protocol class to use to establish an AMP connection is <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.amp.AMP.html" title="twisted.protocols.amp.AMP">AMP</a></code>. Connection setup works as it does for almost all protocols in Twisted. For example, you can set up a listening AMP server using a server endpoint:</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">AMP</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ServerEndpoint</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">service</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Application</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">StreamServerEndpointService</span>
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;basic AMP server&quot;</span>)
+
+<span class="py-src-variable">endpoint</span> = <span class="py-src-variable">TCP4ServerEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">8750</span>)
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">AMP</span>
+<span class="py-src-variable">service</span> = <span class="py-src-variable">StreamServerEndpointService</span>(<span class="py-src-variable">endpoint</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">service</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+</pre><div class="caption">Source listing - <a href="listings/amp/basic_server.tac"><span class="filename">listings/amp/basic_server.tac</span></a></div></div>
+
+ <p>And you can connect to an AMP server using a client endpoint:</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">basic_client</span>
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">SystemExit</span>(<span class="py-src-variable">basic_client</span>.<span class="py-src-variable">main</span>())
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">startLogging</span>, <span class="py-src-variable">err</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">AMP</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ClientEndpoint</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">connect</span>():
+ <span class="py-src-variable">endpoint</span> = <span class="py-src-variable">TCP4ClientEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-string">&quot;127.0.0.1&quot;</span>, <span class="py-src-number">8750</span>)
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">AMP</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">endpoint</span>.<span class="py-src-variable">connect</span>(<span class="py-src-variable">factory</span>)
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">startLogging</span>(<span class="py-src-variable">stdout</span>)
+
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">connect</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">err</span>, <span class="py-src-string">&quot;Connection failed&quot;</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">done</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">done</span>)
+
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/amp/basic_client.py"><span class="filename">listings/amp/basic_client.py</span></a></div></div>
+
+ <h2>Commands<a name="auto1"/></h2>
+
+ <p>Either side of an AMP connection can issue a command to the other side. Each kind of command is represented as a subclass of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.amp.Command.html" title="twisted.protocols.amp.Command">Command</a></code>. A <code>Command</code> defines arguments, response values, and error conditions.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Integer</span>, <span class="py-src-variable">String</span>, <span class="py-src-variable">Unicode</span>, <span class="py-src-variable">Command</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UsernameUnavailable</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RegisterUser</span>(<span class="py-src-parameter">Command</span>):
+ <span class="py-src-variable">arguments</span> = [(<span class="py-src-string">'username'</span>, <span class="py-src-variable">Unicode</span>()),
+ (<span class="py-src-string">'publickey'</span>, <span class="py-src-variable">String</span>())]
+
+ <span class="py-src-variable">response</span> = [(<span class="py-src-string">'uid'</span>, <span class="py-src-variable">Integer</span>())]
+
+ <span class="py-src-variable">errors</span> = {<span class="py-src-variable">UsernameUnavailable</span>: <span class="py-src-string">'username-unavailable'</span>}
+</pre>
+
+ <p>The definition of the command's signature - its arguments, response, and possible error conditions - is separate from the implementation of the behavior to execute when the command is received. The <code>Command</code> subclass only defines the former.</p>
+
+ <p>Commands are issued by calling <code>callRemote</code> on either side of the connection. This method returns a <code>Deferred</code> which eventually fires with the result of the command.</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+</p><span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">command_client</span>
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">SystemExit</span>(<span class="py-src-variable">command_client</span>.<span class="py-src-variable">main</span>())
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">startLogging</span>, <span class="py-src-variable">err</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Integer</span>, <span class="py-src-variable">String</span>, <span class="py-src-variable">Unicode</span>, <span class="py-src-variable">Command</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">basic_client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">connect</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UsernameUnavailable</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-keyword">pass</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RegisterUser</span>(<span class="py-src-parameter">Command</span>):
+ <span class="py-src-variable">arguments</span> = [(<span class="py-src-string">'username'</span>, <span class="py-src-variable">Unicode</span>()),
+ (<span class="py-src-string">'publickey'</span>, <span class="py-src-variable">String</span>())]
+
+ <span class="py-src-variable">response</span> = [(<span class="py-src-string">'uid'</span>, <span class="py-src-variable">Integer</span>())]
+
+ <span class="py-src-variable">errors</span> = {<span class="py-src-variable">UsernameUnavailable</span>: <span class="py-src-string">'username-unavailable'</span>}
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">startLogging</span>(<span class="py-src-variable">stdout</span>)
+
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">connect</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">protocol</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">protocol</span>.<span class="py-src-variable">callRemote</span>(
+ <span class="py-src-variable">RegisterUser</span>,
+ <span class="py-src-variable">username</span>=<span class="py-src-string">u'alice'</span>,
+ <span class="py-src-variable">publickey</span>=<span class="py-src-string">'ssh-rsa AAAAB3NzaC1yc2 alice@actinium'</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">registered</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Registration result:'</span>, <span class="py-src-variable">result</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">registered</span>)
+
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">err</span>, <span class="py-src-string">&quot;Failed to register&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">finished</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">finished</span>)
+
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/amp/command_client.py"><span class="filename">listings/amp/command_client.py</span></a></div></div>
+
+ <h2>Locators<a name="auto2"/></h2>
+
+
+ <p>The logic for handling a command can be specified as an object separate from the <code>AMP</code> instance which interprets and formats bytes over the network.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">CommandLocator</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UsernameUnavailable</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserRegistration</span>(<span class="py-src-parameter">CommandLocator</span>):
+ <span class="py-src-variable">uidCounter</span> = <span class="py-src-number">0</span>
+
+ @<span class="py-src-variable">RegisterUser</span>.<span class="py-src-variable">responder</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">register</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>, <span class="py-src-parameter">publickey</span>):
+ <span class="py-src-variable">path</span> = <span class="py-src-variable">FilePath</span>(<span class="py-src-variable">username</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>.<span class="py-src-variable">exists</span>():
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">UsernameUnavailable</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">uidCounter</span> += <span class="py-src-number">1</span>
+ <span class="py-src-variable">path</span>.<span class="py-src-variable">setContent</span>(<span class="py-src-string">'%d %s\n'</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">uidCounter</span>, <span class="py-src-variable">publickey</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">uidCounter</span>
+</pre>
+
+ <p>When you define a separate <code>CommandLocator</code> subclass, use it by passing an instance of it to the <code>AMP</code> initializer.</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">AMP</span>(<span class="py-src-variable">locator</span>=<span class="py-src-variable">UserRegistration</span>())
+</pre>
+
+ <p>If no locator is passed in, <code>AMP</code> acts as its own locator. Command responders can be defined on an <code>AMP</code> subclass, just as the responder was defined on the <code>UserRegistration</code> example above.</p>
+
+ <h2>Box Receivers<a name="auto3"/></h2>
+
+ <p>AMP conversations consist of an exchange of messages called <em>boxes</em>. A <em>box</em> consists of a sequence of pairs of key and value (for example, the pair <code>username</code> and <code>alice</code>). Boxes are generally represented as <code>dict</code> instances. Normally boxes are passed back and forth to implement the command request/response features described above. The logic for handling each box can be specified as an object separate from the <code>AMP</code> instance.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">amp</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IBoxReceiver</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">BoxReflector</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IBoxReceiver</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startReceivingBoxes</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">boxSender</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">boxSender</span> = <span class="py-src-variable">boxSender</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">ampBoxReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">box</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">boxSender</span>.<span class="py-src-variable">sendBox</span>(<span class="py-src-variable">box</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopReceivingBoxes</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">boxSender</span> = <span class="py-src-variable">None</span>
+</pre>
+
+ <p>These methods parallel those of <code>IProtocol</code>. Startup notification is given by <code>startReceivingBoxes</code>. The argument passed to it is an <code>IBoxSender</code> provider, which can be used to send boxes back out over the network. <code>ampBoxReceived</code> delivers notification for a complete box having been received. And last, <code>stopReceivingBoxes</code> notifies the object that no more boxes will be received and no more can be sent. The argument passed to it is a <code>Failure</code> which may contain details about what caused the conversation to end.</p>
+
+ <p>To use a custom <code>IBoxReceiver</code>, pass it to the <code>AMP</code> initializer.</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">AMP</span>(<span class="py-src-variable">boxReceiver</span>=<span class="py-src-variable">BoxReflector</span>())
+</pre>
+
+ <p>If no box receiver is passed in, <code>AMP</code> acts as its own box receiver. It handles boxes by treating them as command requests or responses and delivering them to the appropriate responder or as a result to a <code>callRemote</code> <code>Deferred</code>.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/application.html b/doc/core/howto/application.html
new file mode 100644
index 0000000..7b8f186
--- /dev/null
+++ b/doc/core/howto/application.html
@@ -0,0 +1,398 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using the Twisted Application Framework</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using the Twisted Application Framework</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><ul><li><a href="#auto1">Audience</a></li><li><a href="#auto2">Goals</a></li></ul><li><a href="#auto3">Overview</a></li><li><a href="#auto4">Using application</a></li><ul><li><a href="#auto5">twistd and tac</a></li><li><a href="#auto6">Customizing twistd logging</a></li><li><a href="#auto7">Services provided by Twisted</a></li><li><a href="#auto8">Service Collection</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<h3>Audience<a name="auto1"/></h3>
+
+<p>The target audience of this document is a Twisted user who wants to deploy a
+significant amount of Twisted code in a re-usable, standard and easily
+configurable fashion. A Twisted user who wishes to use the Application
+framework needs to be familiar with developing Twisted <a href="servers.html" shape="rect">servers</a> and/or <a href="clients.html" shape="rect">clients</a>.</p>
+
+<h3>Goals<a name="auto2"/></h3>
+
+<ul>
+ <li>To introduce the Twisted Application infrastructure.</li>
+
+ <li>To explain how to deploy your Twisted application using <code>.tac</code>
+ files and <code>twistd</code></li>
+
+ <li>To outline the existing Twisted services.</li>
+</ul>
+
+<h2>Overview<a name="auto3"/></h2>
+
+<p>The Twisted Application infrastructure takes care of running and stopping
+your application. Using this infrastructure frees you from from having to
+write a large amount of boilerplate code by hooking your application into
+existing tools that manage daemonization, logging, <a href="choosing-reactor.html" shape="rect">choosing a reactor</a> and more.</p>
+
+<p>The major tool that manages Twisted applications is a command-line utility
+called <code>twistd</code>. <code>twistd</code> is cross platform, and is the
+recommended tool for running Twisted applications. </p>
+
+
+<p>The core component of the Twisted Application infrastructure is the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Application.html" title="twisted.application.service.Application">twisted.application.service.Application</a></code> object — an
+object which represents your application. However, Application doesn't provide
+anything that you'd want to manipulate directly. Instead, Application acts as
+a container of any <q>Services</q> (objects implementing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code>) that your application
+provides. Most of your interaction with the Application infrastructure will be
+done through Services.</p>
+
+<p>By <q>Service</q>, we mean anything in your application that can be started
+and stopped. Typical services include web servers, FTP servers and SSH
+clients. Your Application object can contain many services, and can even
+contain structured hierarchies of Services using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IServiceCollection.html" title="twisted.application.service.IServiceCollection">IServiceCollection</a></code>s.</p>
+
+<p>Here's a simple example of constructing an Application object which
+represents an echo server that runs on TCP port 7001.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">somemodule</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">EchoFactory</span>
+
+<span class="py-src-variable">port</span> = <span class="py-src-number">7001</span>
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">EchoFactory</span>()
+
+<span class="py-src-comment"># this is the important bit</span>
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;echo&quot;</span>) <span class="py-src-comment"># create the Application</span>
+<span class="py-src-variable">echoService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-variable">port</span>, <span class="py-src-variable">factory</span>) <span class="py-src-comment"># create the service</span>
+<span class="py-src-comment"># add the service to the application</span>
+<span class="py-src-variable">echoService</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+</pre>
+
+<p>See <a href="servers.html" shape="rect">Writing Servers</a> for an explanation of
+EchoFactory.</p>
+
+<p>This example creates a simple hierarchy:
+<pre xml:space="preserve">
+ application
+ |
+ `- echoService
+</pre> More complicated hierarchies of services can be created using
+IServiceCollection. You will most likely want to do this to manage Services
+which are dependent on other Services. For example, a proxying Twisted
+application might want its server Service to only start up after the associated
+Client service. </p>
+
+
+<h2>Using application<a name="auto4"/></h2>
+
+<h3>twistd and tac<a name="auto5"/></h3><a name="twistd" shape="rect"/>
+
+<p>To handle start-up and configuration of your Twisted application, the
+Twisted Application infrastructure uses <code>.tac</code> files.
+<code>.tac</code> are Python files which configure an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Application.html" title="twisted.application.service.Application">Application</a></code> object and assign this
+object to the top-level variable <q><code>application</code></q>.</p>
+
+<p>The following is a simple example of a <code>.tac</code> file:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+</p><span class="py-src-comment"># You can run this .tac file directly with:</span>
+<span class="py-src-comment"># twistd -ny service.tac</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+This is an example .tac file which starts a webserver on port 8080 and
+serves files from the current working directory.
+
+The important part of this, the part that makes it a .tac file, is
+the final root-level section, which sets up the object called 'application'
+which twistd will look for
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>, <span class="py-src-variable">internet</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getWebService</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a service suitable for creating an application object.
+
+ This service is a simple web server that serves files on port 8080 from
+ underneath the current working directory.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-comment"># create a resource to serve static files</span>
+ <span class="py-src-variable">fileServer</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-variable">os</span>.<span class="py-src-variable">getcwd</span>()))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8080</span>, <span class="py-src-variable">fileServer</span>)
+
+<span class="py-src-comment"># this is the core part of any tac file, the creation of the root-level</span>
+<span class="py-src-comment"># application object</span>
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;Demo application&quot;</span>)
+
+<span class="py-src-comment"># attach the service to its parent application</span>
+<span class="py-src-variable">service</span> = <span class="py-src-variable">getWebService</span>()
+<span class="py-src-variable">service</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+</pre><div class="caption">Source listing - <a href="listings/application/service.tac"><span class="filename">listings/application/service.tac</span></a></div></div>
+
+<p><code>twistd</code> is a program that runs Twisted applications using a
+<code>.tac</code> file. In its most simple form, it takes a single argument
+<code>-y</code> and a tac file name. For example, you can run the above server
+with the command <code class="shell">twistd -y service.tac</code>.</p>
+
+<p>By default, <code>twistd</code> daemonizes and logs to a file called
+<code>twistd.log</code>. More usually, when debugging, you will want your
+application to run in the foreground and log to the command line. To run the
+above file like this, use the command <code class="shell">twistd -noy
+service.tac</code></p>
+
+<p>For more information, see the <code>twistd</code> man page.</p>
+
+<h3>Customizing <code>twistd</code> logging<a name="auto6"/></h3>
+
+<p>
+<code>twistd</code> logging can be customized using the command
+line. This requires that a <em>log observer factory</em> be
+importable. Given a file named <code>my.py</code> with the code:
+</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FileLogObserver</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">logger</span>():
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">FileLogObserver</span>(<span class="py-src-variable">open</span>(<span class="py-src-string">&quot;/tmp/my.log&quot;</span>, <span class="py-src-string">&quot;w&quot;</span>)).<span class="py-src-variable">emit</span>
+</pre>
+
+<p>
+invoking <code class="shell">twistd --logger my.logger ...</code> will log
+to a file named <code>/tmp/my.log</code> (this simple example could easily be
+replaced with use of the <code>--logfile</code> parameter to twistd).
+</p>
+
+<p>
+Alternatively, the logging behavior can be customized through an API
+accessible from <code>.tac</code> files. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.ILogObserver.html" title="twisted.python.log.ILogObserver">ILogObserver</a></code> component can be
+set on an Application in order to customize the default log observer that
+<code>twistd</code> will use.
+</p>
+
+<p>
+Here is an example of how to use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.logfile.DailyLogFile.html" title="twisted.python.logfile.DailyLogFile">DailyLogFile</a></code>, which rotates the log once
+per day.
+</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">service</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Application</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ILogObserver</span>, <span class="py-src-variable">FileLogObserver</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">logfile</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DailyLogFile</span>
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;myapp&quot;</span>)
+<span class="py-src-variable">logfile</span> = <span class="py-src-variable">DailyLogFile</span>(<span class="py-src-string">&quot;my.log&quot;</span>, <span class="py-src-string">&quot;/tmp&quot;</span>)
+<span class="py-src-variable">application</span>.<span class="py-src-variable">setComponent</span>(<span class="py-src-variable">ILogObserver</span>, <span class="py-src-variable">FileLogObserver</span>(<span class="py-src-variable">logfile</span>).<span class="py-src-variable">emit</span>)
+</pre>
+
+<p>
+invoking <code class="shell">twistd -y my.tac</code> will create a log file
+at <code>/tmp/my.log</code>.
+</p>
+
+<h3>Services provided by Twisted<a name="auto7"/></h3>
+
+<p>Twisted provides several services that you want to know about.</p>
+
+<p>Each of these services (except TimerService) has a corresponding
+<q>connect</q> or <q>listen</q> method on the reactor, and the constructors for
+the services take the same arguments as the reactor methods. The
+<q>connect</q> methods are for clients and the <q>listen</q> methods are for
+servers. For example, TCPServer corresponds to reactor.listenTCP and TCPClient
+corresponds to reactor.connectTCP. </p>
+
+<dl>
+ <dt><code>TCPServer</code>
+ </dt>
+
+ <dt><code>TCPClient</code>
+ </dt>
+
+ <dd>
+ Services which allow you to make connections and listen for connections
+ on TCP ports.
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.listenTCP.html" title="twisted.internet.interfaces.IReactorTCP.listenTCP">listenTCP</a></code></li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.connectTCP.html" title="twisted.internet.interfaces.IReactorTCP.connectTCP">connectTCP</a></code></li>
+ </ul>
+ </dd>
+
+ <dt><code>UNIXServer</code></dt>
+
+ <dt><code>UNIXClient</code></dt>
+
+ <dd>
+ Services which listen and make connections over UNIX sockets.
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIX.listenUNIX.html" title="twisted.internet.interfaces.IReactorUNIX.listenUNIX">listenUNIX</a></code></li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIX.connectUNIX.html" title="twisted.internet.interfaces.IReactorUNIX.connectUNIX">connectUNIX</a></code></li>
+ </ul>
+ </dd>
+
+ <dt><code>SSLServer</code></dt>
+
+ <dt><code>SSLClient</code></dt>
+
+ <dd>Services which allow you to make SSL connections and run SSL servers.
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.listenSSL.html" title="twisted.internet.interfaces.IReactorSSL.listenSSL">listenSSL</a></code></li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.connectSSL.html" title="twisted.internet.interfaces.IReactorSSL.connectSSL">connectSSL</a></code></li>
+ </ul>
+ </dd>
+
+ <dt><code>UDPServer</code></dt>
+
+ <dt><code>UDPClient</code></dt>
+
+ <dd>Services which allow you to send and receive data over UDP
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUDP.listenUDP.html" title="twisted.internet.interfaces.IReactorUDP.listenUDP">listenUDP</a></code></li>
+ </ul>
+
+ <p>See also the <a href="udp.html" shape="rect">UDP documentation</a>.</p>
+ </dd>
+
+ <dt><code>UNIXDatagramServer</code></dt>
+
+ <dt><code>UNIXDatagramClient</code></dt>
+
+ <dd>Services which send and receive data over UNIX datagram sockets.
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIXDatagram.listenUNIXDatagram.html" title="twisted.internet.interfaces.IReactorUNIXDatagram.listenUNIXDatagram">listenUNIXDatagram</a></code></li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIXDatagram.connectUNIXDatagram.html" title="twisted.internet.interfaces.IReactorUNIXDatagram.connectUNIXDatagram">connectUNIXDatagram</a></code></li>
+ </ul>
+ </dd>
+
+ <dt><code>MulticastServer</code></dt>
+
+ <dd>
+ A server for UDP socket methods that support multicast.
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorMulticast.listenMulticast.html" title="twisted.internet.interfaces.IReactorMulticast.listenMulticast">listenMulticast</a></code></li>
+ </ul>
+ </dd>
+
+ <dt><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.internet.TimerService.html" title="twisted.application.internet.TimerService">TimerService</a></code></dt>
+
+ <dd>
+ A service to periodically call a function.
+ </dd>
+
+</dl>
+
+<h3>Service Collection<a name="auto8"/></h3>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IServiceCollection.html" title="twisted.application.service.IServiceCollection">IServiceCollection</a></code> objects contain
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code> objects.
+IService objects can be added to IServiceCollection by calling <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.setServiceParent.html" title="twisted.application.service.IService.setServiceParent">setServiceParent</a></code> and detached
+by using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.disownServiceParent.html" title="twisted.application.service.IService.disownServiceParent">disownServiceParent</a></code>.</p>
+
+<p>The standard implementation of IServiceCollection is <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.MultiService.html" title="twisted.application.service.MultiService">MultiService</a></code>, which also implements
+IService. MultiService is useful for creating a new Service which combines two
+or more existing Services. For example, you could create a DNS Service as a
+MultiService which has a TCP and a UDP Service as children.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">names</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">server</span>, <span class="py-src-variable">dns</span>, <span class="py-src-variable">hosts</span>
+
+<span class="py-src-variable">port</span> = <span class="py-src-number">53</span>
+
+<span class="py-src-comment"># Create a MultiService, and hook up a TCPServer and a UDPServer to it as</span>
+<span class="py-src-comment"># children.</span>
+<span class="py-src-variable">dnsService</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">MultiService</span>()
+<span class="py-src-variable">hostsResolver</span> = <span class="py-src-variable">hosts</span>.<span class="py-src-variable">Resolver</span>(<span class="py-src-string">'/etc/hosts'</span>)
+<span class="py-src-variable">tcpFactory</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">DNSServerFactory</span>([<span class="py-src-variable">hostsResolver</span>])
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-variable">port</span>, <span class="py-src-variable">tcpFactory</span>).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">dnsService</span>)
+<span class="py-src-variable">udpFactory</span> = <span class="py-src-variable">dns</span>.<span class="py-src-variable">DNSDatagramProtocol</span>(<span class="py-src-variable">tcpFactory</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">UDPServer</span>(<span class="py-src-variable">port</span>, <span class="py-src-variable">udpFactory</span>).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">dnsService</span>)
+
+<span class="py-src-comment"># Create an application as normal</span>
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;DNSExample&quot;</span>)
+
+<span class="py-src-comment"># Connect our MultiService to the application, just like a normal service.</span>
+<span class="py-src-variable">dnsService</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+</pre>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/basics.html b/doc/core/howto/basics.html
new file mode 100644
index 0000000..86970d7
--- /dev/null
+++ b/doc/core/howto/basics.html
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Basics</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Basics</h1>
+ <div class="toc"><ol><li><a href="#auto0">Application</a></li><li><a href="#auto1">twistd</a></li><li><a href="#auto2">OS Integration</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Application<a name="auto0"/></h2>
+
+<p>Twisted programs usually work
+with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Application.html" title="twisted.application.service.Application">twisted.application.service.Application</a></code>.
+This class usually holds all persistent configuration of a running
+server -- ports to bind to, places where connections to must be kept
+or attempted, periodic actions to do and almost everything else. It is
+the root object in a tree of services implementing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code>.</p>
+
+<p>Other HOWTOs describe how to write custom code for Applications,
+but this one describes how to use already written code (which can be
+part of Twisted or from a third-party Twisted plugin developer). The
+Twisted distribution comes with an important tool to deal with
+Applications, <code>twistd</code>.</p>
+
+<p><code>Application</code>s are just Python objects, which can
+be created and manipulated in the same ways as any other object.
+</p>
+
+<h2>twistd<a name="auto1"/></h2><a name="twistd" shape="rect"/>
+
+<p>The Twisted Daemon is a program that knows how to run Applications.
+This program
+is <code class="shell">twistd(1)</code>. Strictly
+speaking, <code class="shell">twistd</code> is not necessary --
+fetching the application, getting the <code>IService</code> component,
+calling <code>startService</code>, scheduling <code>stopService</code> when
+the reactor shuts down, and then calling <code>reactor.run()</code> could be
+done manually. <code class="shell">twistd(1)</code>, however, supplies
+many options which are highly useful for program set up.</p>
+
+<p><code class="shell">twistd</code> supports choosing a reactor (for more on
+reactors, see <a href="choosing-reactor.html" shape="rect">Choosing a Reactor</a>), logging
+to a logfile, daemonizing and more. <code class="shell">twistd</code> supports all
+Applications mentioned above -- and an additional one. Sometimes
+it is convenient to write the code for building a class in straight
+Python. One big source of such Python files is the <code>doc/examples</code>
+directory. When a straight Python file which defines an <code>Application</code>
+object called <code>application</code> is used, use the <code class="shell">-y</code>
+option.</p>
+
+<p>When <code class="shell">twistd</code> runs, it records its process
+id in a <code>twistd.pid</code> file (this can be configured via a
+command line switch). In order to shutdown
+the <code class="shell">twistd</code> process, kill that pid (usually
+you would do <code class="shell">kill `cat twistd.pid`</code>).
+</p>
+
+<p>As always, the gory details are in the manual page.</p>
+
+<h2>OS Integration<a name="auto2"/></h2>
+
+<p>
+If you have an Application that runs
+with <code class="shell">twistd</code>, you can easily deploy it on
+RedHat Linux or Debian GNU/Linux based systems using
+the <code class="shell">tap2deb</code>
+or <code class="shell">tap2rpm</code> tools. These take a Twisted
+Application file (of any of the supported formats — Python source, XML
+or pickle), and build a Debian or RPM package (respectively) that
+installs the Application as a system service. The package includes the
+Application file, a default <code>/etc/init.d/</code> script that
+starts and stops the process with twistd, and post-installation
+scripts that configure the Application to be run in the appropriate
+init levels.
+</p>
+
+
+<div class="note"><strong>Note: </strong> <code class="shell">tap2rpm</code>
+and <code class="shell">tap2deb</code> do not package your entire
+application and dependent code, just the Twisted Application file. You
+will need to find some other way to package your Python code, such
+as <a href="http://docs.python.org/library/distutils.html" shape="rect">distutils</a>'
+<code>bdist_rpm</code> command.
+</div>
+
+<p>
+For more savvy users, these tools also generate the source package, allowing
+you to modify and polish things which automated software cannot detect (such as
+dependencies or relationships to virtual packages).
+</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/book.tex b/doc/core/howto/book.tex
new file mode 100644
index 0000000..2708619
--- /dev/null
+++ b/doc/core/howto/book.tex
@@ -0,0 +1,129 @@
+\documentclass[oneside]{book}
+\usepackage[dvips]{graphicx}
+\usepackage{times,mathptmx}
+\usepackage{ifthen}
+\usepackage{hyperref}
+
+\usepackage{geometry}
+\geometry{verbose,letterpaper,tmargin=1in,bmargin=0.5in,lmargin=1in,rmargin=1in}
+
+\setlength{\oddsidemargin}{0in}
+\setlength{\textwidth}{\paperwidth}
+\addtolength{\textwidth}{-2in}
+
+\newcommand{\loreref}[1]{%
+ \ifthenelse{\value{page}=\pageref{#1}}%
+ { (this page)}%
+ { (page \pageref{#1})}%
+}
+
+
+\title{The Twisted Documentation}
+\author{The Twisted Development Team}
+
+\tolerance=1000
+\sloppy
+
+\begin{document}
+\maketitle
+\tableofcontents
+
+\chapter{Introduction}
+
+\input{vision.tex}
+
+
+\chapter{Getting Started}
+
+\input{servers.tex}
+\input{clients.tex}
+\input{trial.tex}
+\input{tutorial/index.tex}
+\input{tutorial/intro.tex}
+\input{tutorial/protocol.tex}
+\input{tutorial/style.tex}
+\input{tutorial/components.tex}
+\input{tutorial/backends.tex}
+\input{tutorial/web.tex}
+\input{tutorial/pb.tex}
+\input{tutorial/factory.tex}
+\input{tutorial/client.tex}
+\input{tutorial/library.tex}
+\input{tutorial/configuration.tex}
+\input{quotes.tex}
+\input{design.tex}
+
+
+\chapter{Networking and Other Event Sources}
+
+\input{internet-overview.tex}
+\input{reactor-basics.tex}
+\input{ssl.tex}
+\input{udp.tex}
+\input{process.tex}
+\input{defer.tex}
+\input{gendefer.tex}
+\input{time.tex}
+\input{threading.tex}
+\input{producers.tex}
+\input{choosing-reactor.tex}
+
+
+\chapter{High-Level Infrastructure}
+
+\input{endpoints.tex}
+\input{components.tex}
+\input{cred.tex}
+\input{plugin.tex}
+
+
+\chapter{Deploying Twisted Applications}
+
+\input{basics.tex}
+\input{application.tex}
+\input{tap.tex}
+
+
+\chapter{Utilities}
+
+\input{logging.tex}
+\input{constants.tex}
+\input{rdbms.tex}
+\input{options.tex}
+\input{dirdbm.tex}
+\input{testing.tex}
+
+
+\chapter{Asynchronous Messaging Protocol (AMP)}
+
+\input{amp.tex}
+
+
+\chapter{Perspective Broker}
+
+\input{pb.tex}
+\input{pb-intro.tex}
+\input{pb-usage.tex}
+\input{pb-clients.tex}
+\input{pb-copyable.tex}
+\input{pb-cred.tex}
+\input{pb-limits.tex}
+
+
+\chapter{Manual Pages}
+
+\input{../man/trial-man.tex}
+\clearpage
+\input{../man/twistd-man.tex}
+\clearpage
+\input{../man/tap2deb-man.tex}
+\clearpage
+\input{../man/tap2rpm-man.tex}
+
+
+\chapter{Appendix}
+
+\input{glossary.tex}
+\input{debug-with-emacs.tex}
+
+\end{document}
diff --git a/doc/core/howto/choosing-reactor.html b/doc/core/howto/choosing-reactor.html
new file mode 100644
index 0000000..22ea479
--- /dev/null
+++ b/doc/core/howto/choosing-reactor.html
@@ -0,0 +1,395 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Choosing a Reactor and GUI Toolkit Integration</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Choosing a Reactor and GUI Toolkit Integration</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Reactor Functionality</a></li><li><a href="#auto2">General Purpose Reactors</a></li><ul><li><a href="#auto3">Select()-based Reactor</a></li></ul><li><a href="#auto4">Platform-Specific Reactors</a></li><ul><li><a href="#auto5">Poll-based Reactor</a></li><li><a href="#auto6">KQueue</a></li><li><a href="#auto7">WaitForMultipleObjects (WFMO) for Win32</a></li><li><a href="#auto8">Input/Output Completion Port (IOCP) for Win32</a></li><li><a href="#auto9">Epoll-based Reactor</a></li></ul><li><a href="#auto10">GUI Integration Reactors</a></li><ul><li><a href="#auto11">GTK+</a></li><li><a href="#auto12">GTK+ 3.0 and GObject Introspection</a></li><li><a href="#auto13">wxPython</a></li><li><a href="#auto14">CoreFoundation</a></li></ul><li><a href="#auto15">Non-Reactor GUI Integration</a></li><ul><li><a href="#auto16">Tkinter</a></li><li><a href="#auto17">PyUI</a></li></ul></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>Twisted provides a variety of implementations of the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">twisted.internet.reactor</a></code>. The specialized
+ implementations are suited for different purposes and are
+ designed to integrate better with particular platforms.</p>
+
+ <p>The <a href="#epoll" shape="rect">epoll()-based reactor</a> is Twisted's default on
+ Linux. Other platforms use <a href="#poll" shape="rect">poll()</a>, or the most
+ cross-platform reactor, <a href="#select" shape="rect">select()</a>.</p>
+
+ <p>Platform-specific reactor implementations exist for:</p>
+
+ <ul>
+ <li><a href="#poll" shape="rect">Poll for Linux</a></li>
+ <li><a href="#epoll" shape="rect">Epoll for Linux 2.6</a></li>
+ <li><a href="#win32_wfmo" shape="rect">WaitForMultipleObjects (WFMO) for Win32</a></li>
+ <li><a href="#win32_iocp" shape="rect">Input/Output Completion Port (IOCP) for Win32</a></li>
+ <li><a href="#kqueue" shape="rect">KQueue for FreeBSD and Mac OS X</a></li>
+ <li><a href="#cfreactor" shape="rect">CoreFoundation for Mac OS X</a></li>
+ </ul>
+
+ <p>The remaining custom reactor implementations provide support
+ for integrating with the native event loops of various graphical
+ toolkits. This lets your Twisted application use all of the
+ usual Twisted APIs while still being a graphical application.</p>
+
+ <p>Twisted currently integrates with the following graphical
+ toolkits:</p>
+
+ <ul>
+ <li><a href="#gtk" shape="rect">GTK+ 2.0</a></li>
+ <li><a href="#gtk3" shape="rect">GTK+ 3.0 and GObject Introspection</a></li>
+ <li><a href="#tkinter" shape="rect">Tkinter</a></li>
+ <li><a href="#wxpython" shape="rect">wxPython</a></li>
+ <li><a href="#win32_wfmo" shape="rect">Win32</a></li>
+ <li><a href="#cfreactor" shape="rect">CoreFoundation</a></li>
+ <li><a href="#pyui" shape="rect">PyUI</a></li>
+ </ul>
+
+ <p>When using applications that are runnable using <code>twistd</code>, e.g.
+ TACs or plugins, there is no need to choose a reactor explicitly, since
+ this can be chosen using <code>twistd</code>'s -r option.</p>
+
+ <p>In all cases, the event loop is started by calling <code class="python">reactor.run()</code>. In all cases, the event loop
+ should be stopped with <code class="python">reactor.stop()</code>.</p>
+
+ <p><strong>IMPORTANT:</strong> installing a reactor should be the first thing
+ done in the app, since any code that does
+ <code class="python">from twisted.internet import reactor</code> will automatically
+ install the default reactor if the code hasn't already installed one.</p>
+
+ <h2>Reactor Functionality<a name="auto1"/></h2>
+
+ <table border="1" cellpadding="7" cellspacing="0" title="Summary of reactor features">
+ <tr><td colspan="1" rowspan="1"/><th colspan="1" rowspan="1">Status</th><th colspan="1" rowspan="1">TCP</th><th colspan="1" rowspan="1">SSL</th><th colspan="1" rowspan="1">UDP</th><th colspan="1" rowspan="1">Threading</th><th colspan="1" rowspan="1">Processes</th><th colspan="1" rowspan="1">Scheduling</th><th colspan="1" rowspan="1">Platforms</th></tr>
+ <tr><th colspan="1" rowspan="1">select()</th><td colspan="1" rowspan="1">Stable</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Unix, Win32</td></tr>
+ <tr><th colspan="1" rowspan="1">poll</th><td colspan="1" rowspan="1">Stable</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Unix</td></tr>
+ <tr><th colspan="1" rowspan="1">WaitForMultipleObjects (WFMO) for Win32</th><td colspan="1" rowspan="1">Experimental</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Win32</td></tr>
+ <tr><th colspan="1" rowspan="1">Input/Output Completion Port (IOCP) for Win32</th><td colspan="1" rowspan="1">Experimental</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Win32</td></tr>
+ <tr><th colspan="1" rowspan="1">CoreFoundation</th><td colspan="1" rowspan="1">Unmaintained</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Mac OS X</td></tr>
+ <tr><th colspan="1" rowspan="1">epoll</th><td colspan="1" rowspan="1">Stable</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Linux 2.6</td></tr>
+ <tr><th colspan="1" rowspan="1">GTK+</th><td colspan="1" rowspan="1">Stable</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Unix, Win32</td></tr>
+ <tr><th colspan="1" rowspan="1">wx</th><td colspan="1" rowspan="1">Experimental</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Unix, Win32</td></tr>
+ <tr><th colspan="1" rowspan="1">kqueue</th><td colspan="1" rowspan="1">Experimental</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">Y</td><td colspan="1" rowspan="1">FreeBSD</td></tr>
+ </table>
+
+ <h2>General Purpose Reactors<a name="auto2"/></h2>
+
+ <h3>Select()-based Reactor<a name="auto3"/></h3><a name="select" shape="rect"/>
+
+ <p>The <code>select</code> reactor is the default on platforms that don't
+ provide a better alternative that covers all use cases. If
+ the <code>select</code> reactor is desired, it may be installed via:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">selectreactor</span>
+<span class="py-src-variable">selectreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h2>Platform-Specific Reactors<a name="auto4"/></h2>
+
+ <h3>Poll-based Reactor<a name="auto5"/></h3><a name="poll" shape="rect"/>
+
+ <p>The PollReactor will work on any platform that provides <code class="python">select.poll</code>. With larger numbers of connected
+ sockets, it may provide for better performance than the SelectReactor.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pollreactor</span>
+<span class="py-src-variable">pollreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h3>KQueue<a name="auto6"/></h3><a name="kqueue" shape="rect"/>
+
+ <p>The KQueue Reactor allows Twisted to use FreeBSD's kqueue mechanism for
+ event scheduling. See instructions in the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.kqreactor.html" title="twisted.internet.kqreactor">twisted.internet.kqreactor</a></code>'s
+ docstring for installation notes.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">kqreactor</span>
+<span class="py-src-variable">kqreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+
+ <h3>WaitForMultipleObjects (WFMO) for Win32<a name="auto7"/></h3><a name="win32_wfmo" shape="rect"/>
+
+ <p>The Win32 reactor is not yet complete and has various limitations
+ and issues that need to be addressed. The reactor supports GUI integration
+ with the win32gui module, so it can be used for native Win32 GUI applications.
+ </p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">win32eventreactor</span>
+<span class="py-src-variable">win32eventreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h3>Input/Output Completion Port (IOCP) for Win32<a name="auto8"/></h3><a name="win32_iocp" shape="rect"/>
+
+ <p>
+ Windows provides a fast, scalable event notification system known as IO
+ Completion Ports, or IOCP for short. Twisted includes a reactor based
+ on IOCP which is nearly complete.
+ </p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">iocpreactor</span>
+<span class="py-src-variable">iocpreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h3>Epoll-based Reactor<a name="auto9"/></h3><a name="epoll" shape="rect"/>
+
+ <p>The EPollReactor will work on any platform that provides
+ <code class="python">epoll</code>, today only Linux 2.6 and over. The
+ implementation of the epoll reactor currently uses the Level Triggered
+ interface, which is basically like poll() but scales much better.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">epollreactor</span>
+<span class="py-src-variable">epollreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h2>GUI Integration Reactors<a name="auto10"/></h2>
+
+ <h3>GTK+<a name="auto11"/></h3><a name="gtk" shape="rect"/>
+
+ <p>Twisted integrates with <a href="http://www.pygtk.org/" shape="rect">PyGTK</a> version
+ 2.0 using the <code>gtk2reactor</code>. An example Twisted application that
+ uses GTK+ can be found
+ in <code class="py-filename">doc/core/examples/pbgtk2.py</code>.</p>
+
+ <p>GTK-2.0 split the event loop out of the GUI toolkit and into a separate
+ module called <q>glib</q>. To run an application using the glib event loop,
+ use the <code>glib2reactor</code>. This will be slightly faster
+ than <code>gtk2reactor</code> (and does not require a working X display),
+ but cannot be used to run GUI applications.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">gtk2reactor</span> <span class="py-src-comment"># for gtk-2.0</span>
+<span class="py-src-variable">gtk2reactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">glib2reactor</span> <span class="py-src-comment"># for non-GUI apps</span>
+<span class="py-src-variable">glib2reactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h3>GTK+ 3.0 and GObject Introspection<a name="auto12"/></h3><a name="gtk3" shape="rect"/>
+
+ <p>Twisted integrates with <a href="http://gtk.org" shape="rect">GTK+ 3</a> and GObject
+ through <a href="http://live.gnome.org/PyGObject" shape="rect">PyGObject's</a>
+ introspection using the <code>gtk3reactor</code>
+ and <code>gireactor</code> reactors.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">gtk3reactor</span>
+<span class="py-src-variable">gtk3reactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">gireactor</span> <span class="py-src-comment"># for non-GUI apps</span>
+<span class="py-src-variable">gireactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <p>GLib 3.0 introduces the concept of <code>GApplication</code>, a class
+ that handles application uniqueness in a cross-platform way and provides
+ its own main loop. Its counterpart <code>GtkApplication</code> also
+ handles application lifetime with respect to open windows. Twisted
+ supports registering these objects with the event loop, which should be
+ done before running the reactor:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">gtk3reactor</span>
+<span class="py-src-variable">gtk3reactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">gi</span>.<span class="py-src-variable">repository</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Gtk</span>
+<span class="py-src-variable">app</span> = <span class="py-src-variable">Gtk</span>.<span class="py-src-variable">Application</span>(...)
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">registerGApplication</span>(<span class="py-src-variable">app</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h3>wxPython<a name="auto13"/></h3><a name="wxpython" shape="rect"/>
+
+ <p>Twisted currently supports two methods of integrating
+ wxPython. Unfortunately, neither method will work on all wxPython
+ platforms (such as GTK2 or Windows). It seems that the only
+ portable way to integrate with wxPython is to run it in a separate
+ thread. One of these methods may be sufficient if your wx app is
+ limited to a single platform.</p>
+
+ <p>As with <a href="#tkinter" shape="rect">Tkinter</a>, the support for integrating
+ Twisted with a <a href="http://www.wxpython.org" shape="rect">wxPython</a>
+ application uses specialized support code rather than a simple reactor.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">wxPython</span>.<span class="py-src-variable">wx</span> <span class="py-src-keyword">import</span> *
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">wxsupport</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">myWxAppInstance</span> = <span class="py-src-variable">wxApp</span>(<span class="py-src-number">0</span>)
+<span class="py-src-variable">wxsupport</span>.<span class="py-src-variable">install</span>(<span class="py-src-variable">myWxAppInstance</span>)
+</pre>
+
+ <p>However, this has issues when running on Windows, so Twisted now
+ comes with alternative wxPython support using a reactor. Using
+ this method is probably better. Initialization is done in two
+ stages. In the first, the reactor is installed:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">wxreactor</span>
+<span class="py-src-variable">wxreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <p>Later, once a <code class="python">wxApp</code> instance has
+ been created, but before <code class="python">reactor.run()</code>
+ is called:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">myWxAppInstance</span> = <span class="py-src-variable">wxApp</span>(<span class="py-src-number">0</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">registerWxApp</span>(<span class="py-src-variable">myWxAppInstance</span>)
+</pre>
+
+ <p>An example Twisted application that uses wxPython can be found
+ in <code class="py-filename">doc/core/examples/wxdemo.py</code>.</p>
+
+ <h3>CoreFoundation<a name="auto14"/></h3><a name="cfreactor" shape="rect"/>
+
+ <p>Twisted integrates with <a href="http://pyobjc.sf.net/" shape="rect">PyObjC</a> version 1.0. Sample applications using Cocoa and Twisted
+ are available in the examples directory under
+ <code>doc/core/examples/threadedselect/Cocoa</code>.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">cfreactor</span>
+<span class="py-src-variable">cfreactor</span>.<span class="py-src-variable">install</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <h2>Non-Reactor GUI Integration<a name="auto15"/></h2>
+
+ <h3>Tkinter<a name="auto16"/></h3><a name="tkinter" shape="rect"/>
+
+ <p>The support for <a href="http://wiki.python.org/moin/TkInter" shape="rect">Tkinter</a> doesn't use a specialized reactor. Instead, there is
+ some specialized support code:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">Tkinter</span> <span class="py-src-keyword">import</span> *
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">tksupport</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Tk</span>()
+
+<span class="py-src-comment"># Install the Reactor support</span>
+<span class="py-src-variable">tksupport</span>.<span class="py-src-variable">install</span>(<span class="py-src-variable">root</span>)
+
+<span class="py-src-comment"># at this point build Tk app as usual using the root object,</span>
+<span class="py-src-comment"># and start the program with &quot;reactor.run()&quot;, and stop it</span>
+<span class="py-src-comment"># with &quot;reactor.stop()&quot;.</span>
+</pre>
+
+ <h3>PyUI<a name="auto17"/></h3><a name="pyui" shape="rect"/>
+
+ <p>As with <a href="#tkinter" shape="rect">Tkinter</a>, the support for integrating
+ Twisted with a <a href="http://pyui.sourceforge.net" shape="rect">PyUI</a>
+ application uses specialized support code rather than a simple reactor.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pyuisupport</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">pyuisupport</span>.<span class="py-src-variable">install</span>(<span class="py-src-variable">args</span>=(<span class="py-src-number">640</span>, <span class="py-src-number">480</span>), <span class="py-src-variable">kw</span>={<span class="py-src-string">'renderer'</span>: <span class="py-src-string">'gl'</span>})
+</pre>
+
+ <p>An example Twisted application that uses PyUI can be found in <code class="py-filename">doc/core/examples/pyuidemo.py</code>.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/clients.html b/doc/core/howto/clients.html
new file mode 100644
index 0000000..024f32f
--- /dev/null
+++ b/doc/core/howto/clients.html
@@ -0,0 +1,741 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Writing Clients</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Writing Clients</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Protocol</a></li><li><a href="#auto2">Simple, single-use clients</a></li><li><a href="#auto3">ClientFactory</a></li><ul><li><a href="#auto4">Reconnection</a></li></ul><li><a href="#auto5">A Higher-Level Example: ircLogBot</a></li><ul><li><a href="#auto6">Overview of ircLogBot</a></li><li><a href="#auto7">Persistent Data in the Factory</a></li></ul><li><a href="#auto8">Further Reading</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>Twisted is a framework designed to be very flexible, and let you write
+ powerful clients. The cost of this flexibility is a few layers in the way
+ to writing your client. This document covers creating clients that can be
+ used for TCP, SSL and Unix sockets. UDP is covered <a href="udp.html" shape="rect">in
+ a different document</a>.</p>
+
+ <p>At the base, the place where you actually implement the protocol parsing
+ and handling, is the <code>Protocol</code> class. This class will usually be
+ descended
+ from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">twisted.internet.protocol.Protocol</a></code>. Most
+ protocol handlers inherit either from this class or from one of its
+ convenience children. An instance of the protocol class will be instantiated
+ when you connect to the server and will go away when the connection is
+ finished. This means that persistent configuration is not saved in the
+ <code>Protocol</code>.</p>
+
+ <p>The persistent configuration is kept in a <code>Factory</code>
+ class, which usually inherits from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">twisted.internet.protocol.Factory</a></code>
+ (or <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.html" title="twisted.internet.protocol.ClientFactory">twisted.internet.protocol.ClientFactory</a></code>: see
+ below). The default factory class just instantiates the <code>Protocol</code>
+ and then sets the protocol's <code>factory</code> attribute to point to
+ itself (the factory). This lets the <code>Protocol</code> access, and
+ possibly modify, the persistent configuration.</p>
+
+ <h2>Protocol<a name="auto1"/></h2>
+
+ <p>As mentioned above, this and auxiliary classes and functions are where
+ most of the code is. A Twisted protocol handles data in an asynchronous
+ manner. This means that the protocol never waits for an event, but rather
+ responds to events as they arrive from the network.</p>
+
+ <p>Here is a simple example:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">stdout</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+</pre>
+
+ <p>This is one of the simplest protocols. It just writes whatever it reads
+ from the connection to standard output. There are many events it does not
+ respond to. Here is an example of a <code>Protocol</code> responding to
+ another event:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WelcomeMessage</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;Hello server, I am the client!\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+</pre>
+
+ <p>This protocol connects to the server, sends it a welcome message, and
+ then terminates the connection.</p>
+
+ <p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.BaseProtocol.connectionMade.html" title="twisted.internet.protocol.BaseProtocol.connectionMade">connectionMade</a></code> event is
+ usually where set up of the <code>Protocol</code> object happens, as well as
+ any initial greetings (as in the
+ <code>WelcomeMessage</code> protocol above). Any tearing down of
+ <code>Protocol</code>-specific objects is done in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.connectionLost.html" title="twisted.internet.protocol.Protocol.connectionLost">connectionLost</a></code>.</p>
+
+ <h2>Simple, single-use clients<a name="auto2"/></h2>
+
+ <p>In many cases, the protocol only needs to connect to the server once, and
+ the code just wants to get a connected instance of the protocol. In those
+ cases <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.html" title="twisted.internet.endpoints">twisted.internet.endpoints</a></code> provides the
+ appropriate API.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ClientEndpoint</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Greeter</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">sendMessage</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;MESSAGE %s\n&quot;</span> % <span class="py-src-variable">msg</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">GreeterFactory</span>(<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">Greeter</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">gotProtocol</span>(<span class="py-src-parameter">p</span>):
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">sendMessage</span>(<span class="py-src-string">&quot;Hello&quot;</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">1</span>, <span class="py-src-variable">p</span>.<span class="py-src-variable">sendMessage</span>, <span class="py-src-string">&quot;This is sent in a second&quot;</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">2</span>, <span class="py-src-variable">p</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>)
+
+<span class="py-src-variable">point</span> = <span class="py-src-variable">TCP4ClientEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">1234</span>)
+<span class="py-src-variable">d</span> = <span class="py-src-variable">point</span>.<span class="py-src-variable">connect</span>(<span class="py-src-variable">GreeterFactory</span>())
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">gotProtocol</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Regardless of the type of client endpoint, the way to set up a new
+ connection is simply to call the <code>connect</code> method on it and pass
+ in a factory. This means it's easy to change the mechanism you're using to
+ connect, without changing the rest of your program. For example, to run
+ the greeter example over SSL, the only change required is to instantiate an
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.SSL4ClientEndpoint.html" title="twisted.internet.endpoints.SSL4ClientEndpoint">SSL4ClientEndpoint</a></code> instead of a
+ <code>TCP4ClientEndpoint</code>. To take advantage of this, functions and
+ methods which initiates a new connection should generally accept an
+ endpoint as an argument and let the caller construct it, rather than taking
+ arguments like 'host' and 'port' and constructing its own before calling
+ <code>connect</code>.</p>
+
+ <p>For more information on different ways you can make outgoing connections
+ to different types of endpoints, as well as parsing strings into endpoints,
+ see <a href="endpoints.html" shape="rect">the documentation for the endpoints
+ API</a>.</p>
+
+ <div class="note"><strong>Note: </strong>If you've used <code>ClientFactory</code> before,
+ make sure you remember that the <code>connect</code> method takes a
+ <code>Factory</code>, not a <code>ClientFactory</code>. Even if you pass a
+ <code>ClientFactory</code> to <code>endpoint.connect</code>, its
+ <code>clientConnectionFailed</code> and <code>clientConnectionLost</code>
+ methods will not be called.</div>
+
+ <p>You may come across code using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientCreator.html" title="twisted.internet.protocol.ClientCreator">ClientCreator</a></code>, an older API which is not as flexible as
+ the endpoint API. Rather than calling <code>connect</code> on an endpoint,
+ such code will look like this:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientCreator</span>
+
+...
+
+<span class="py-src-variable">creator</span> = <span class="py-src-variable">ClientCreator</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">Greeter</span>)
+<span class="py-src-variable">d</span> = <span class="py-src-variable">creator</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">1234</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">gotProtocol</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>In general, the endpoint API should be preferred in new code, as it lets
+ the caller select the method of connecting.</p>
+
+ <h2>ClientFactory<a name="auto3"/></h2>
+
+ <p>Still, there's plenty of code out there that uses lower-level APIs, and
+ a few features (such as automatic reconnection) have not been
+ re-implemented with endpoints yet, so in some cases they may be more
+ convenient to use.</p>
+
+ <p>To use the lower-level connection APIs, you will need to call one of the
+ <em>reactor.connect*</em> methods directly. For these cases, you need a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.html" title="twisted.internet.protocol.ClientFactory">ClientFactory</a></code>.
+ The <code>ClientFactory</code> is in charge of creating the
+ <code>Protocol</code> and also receives events relating to the connection
+ state. This allows it to do things like reconnect in the event of a
+ connection error. Here is an example of a simple <code>ClientFactory</code>
+ that uses the <code>Echo</code> protocol (above) and also prints what state
+ the connection is in.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>, <span class="py-src-variable">ClientFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">stdout</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClientFactory</span>(<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startedConnecting</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Started to connect.'</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Connected.'</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">Echo</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Lost connection. Reason:'</span>, <span class="py-src-variable">reason</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Connection failed. Reason:'</span>, <span class="py-src-variable">reason</span>
+</pre>
+
+ <p>To connect this <code>EchoClientFactory</code> to a server, you could use
+ this code:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>, <span class="py-src-variable">EchoClientFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Note that <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.clientConnectionFailed.html" title="twisted.internet.protocol.ClientFactory.clientConnectionFailed">clientConnectionFailed</a></code>
+ is called when a connection could not be established, and that <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.clientConnectionLost.html" title="twisted.internet.protocol.ClientFactory.clientConnectionLost">clientConnectionLost</a></code>
+ is called when a connection was made and then disconnected.</p>
+
+ <h3>Reconnection<a name="auto4"/></h3>
+
+ <p>Often, the connection of a client will be lost unintentionally due to
+ network problems. One way to reconnect after a disconnection would be to
+ call <code>connector.connect()</code> when the connection is lost:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientFactory</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClientFactory</span>(<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">connector</span>.<span class="py-src-variable">connect</span>()
+</pre>
+
+ <p>The connector passed as the first argument is the interface between a
+ connection and a protocol. When the connection fails and the factory
+ receives the <code>clientConnectionLost</code> event, the factory can
+ call <code>connector.connect()</code> to start the connection over again
+ from scratch.</p>
+
+ <p>
+ However, most programs that want this functionality should
+ implement <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ReconnectingClientFactory.html" title="twisted.internet.protocol.ReconnectingClientFactory">ReconnectingClientFactory</a></code> instead,
+ which tries to reconnect if a connection is lost or fails and which
+ exponentially delays repeated reconnect attempts.
+ </p>
+
+ <p>
+ Here is the <code>Echo</code> protocol implemented with
+ a <code>ReconnectingClientFactory</code>:
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>, <span class="py-src-variable">ReconnectingClientFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">stdout</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClientFactory</span>(<span class="py-src-parameter">ReconnectingClientFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startedConnecting</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Started to connect.'</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Connected.'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Resetting reconnection delay'</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">resetDelay</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">Echo</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Lost connection. Reason:'</span>, <span class="py-src-variable">reason</span>
+ <span class="py-src-variable">ReconnectingClientFactory</span>.<span class="py-src-variable">clientConnectionLost</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">connector</span>, <span class="py-src-variable">reason</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Connection failed. Reason:'</span>, <span class="py-src-variable">reason</span>
+ <span class="py-src-variable">ReconnectingClientFactory</span>.<span class="py-src-variable">clientConnectionFailed</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">connector</span>,
+ <span class="py-src-variable">reason</span>)
+</pre>
+
+ <h2>A Higher-Level Example: ircLogBot<a name="auto5"/></h2>
+
+ <h3>Overview of ircLogBot<a name="auto6"/></h3>
+
+ <p>The clients so far have been fairly simple. A more complicated example
+ comes with Twisted Words in the <code>doc/words/examples</code>
+ directory.</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+</p><span class="py-src-comment"># twisted imports</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+
+<span class="py-src-comment"># system imports</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>, <span class="py-src-variable">sys</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MessageLogger</span>:
+ <span class="py-src-string">&quot;&quot;&quot;
+ An independent logger class (because separation of application
+ and protocol logic is a good thing).
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">file</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">file</span> = <span class="py-src-variable">file</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">log</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Write a message to the file.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">timestamp</span> = <span class="py-src-variable">time</span>.<span class="py-src-variable">strftime</span>(<span class="py-src-string">&quot;[%H:%M:%S]&quot;</span>, <span class="py-src-variable">time</span>.<span class="py-src-variable">localtime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>()))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">file</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">'%s %s\n'</span> % (<span class="py-src-variable">timestamp</span>, <span class="py-src-variable">message</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">file</span>.<span class="py-src-variable">flush</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">close</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">file</span>.<span class="py-src-variable">close</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LogBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+ <span class="py-src-string">&quot;&quot;&quot;A logging IRC bot.&quot;&quot;&quot;</span>
+
+ <span class="py-src-variable">nickname</span> = <span class="py-src-string">&quot;twistedbot&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span> = <span class="py-src-variable">MessageLogger</span>(<span class="py-src-variable">open</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">filename</span>, <span class="py-src-string">&quot;a&quot;</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;[connected at %s]&quot;</span> %
+ <span class="py-src-variable">time</span>.<span class="py-src-variable">asctime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">localtime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>())))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionLost</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">reason</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;[disconnected at %s]&quot;</span> %
+ <span class="py-src-variable">time</span>.<span class="py-src-variable">asctime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">localtime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>())))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">close</span>()
+
+
+ <span class="py-src-comment"># callbacks for events</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">signedOn</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Called when bot has succesfully signed on to server.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">channel</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">joined</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">channel</span>):
+ <span class="py-src-string">&quot;&quot;&quot;This will get called when the bot joins the channel.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;[I have joined %s]&quot;</span> % <span class="py-src-variable">channel</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-string">&quot;&quot;&quot;This will get called when the bot receives a message.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>, <span class="py-src-number">1</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;&lt;%s&gt; %s&quot;</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">msg</span>))
+
+ <span class="py-src-comment"># Check to see if they're sending me a private message</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">channel</span> == <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>:
+ <span class="py-src-variable">msg</span> = <span class="py-src-string">&quot;It isn't nice to whisper! Play nice with the group.&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">msg</span>)
+ <span class="py-src-keyword">return</span>
+
+ <span class="py-src-comment"># Otherwise check to see if it is a message directed at me</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">msg</span>.<span class="py-src-variable">startswith</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> + <span class="py-src-string">&quot;:&quot;</span>):
+ <span class="py-src-variable">msg</span> = <span class="py-src-string">&quot;%s: I am a log bot&quot;</span> % <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">channel</span>, <span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;&lt;%s&gt; %s&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>, <span class="py-src-variable">msg</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">action</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-string">&quot;&quot;&quot;This will get called when the bot sees someone do an action.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>, <span class="py-src-number">1</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;* %s %s&quot;</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">msg</span>))
+
+ <span class="py-src-comment"># irc callbacks</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">irc_NICK</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">prefix</span>, <span class="py-src-parameter">params</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Called when an IRC user changes their nickname.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">old_nick</span> = <span class="py-src-variable">prefix</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">new_nick</span> = <span class="py-src-variable">params</span>[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;%s is now known as %s&quot;</span> % (<span class="py-src-variable">old_nick</span>, <span class="py-src-variable">new_nick</span>))
+
+
+ <span class="py-src-comment"># For fun, override the method that determines how a nickname is changed on</span>
+ <span class="py-src-comment"># collisions. The default method appends an underscore.</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">alterCollidedNick</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">nickname</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Generate an altered version of a nickname that caused a collision in an
+ effort to create an unused related name for subsequent registration.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">nickname</span> + <span class="py-src-string">'^'</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LogBotFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-string">&quot;&quot;&quot;A factory for LogBots.
+
+ A new protocol instance will be created each time we connect to the server.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">channel</span> = <span class="py-src-variable">channel</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">LogBot</span>()
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">factory</span> = <span class="py-src-variable">self</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">p</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-string">&quot;&quot;&quot;If we get disconnected, reconnect to server.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">connector</span>.<span class="py-src-variable">connect</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;connection failed:&quot;</span>, <span class="py-src-variable">reason</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-comment"># initialize logging</span>
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">stdout</span>)
+
+ <span class="py-src-comment"># create factory protocol and application</span>
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">LogBotFactory</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">1</span>], <span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">2</span>])
+
+ <span class="py-src-comment"># connect factory to this host and port</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;irc.freenode.net&quot;</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">f</span>)
+
+ <span class="py-src-comment"># run bot</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="../../words/examples/ircLogBot.py"><span class="filename">../../words/examples/ircLogBot.py</span></a></div></div>
+
+ <p><code>ircLogBot.py</code> connects to an IRC server, joins a channel, and
+ logs all traffic on it to a file. It demonstrates some of the
+ connection-level logic of reconnecting on a lost connection, as well as
+ storing persistent data in the <code>Factory</code>.</p>
+
+ <h3>Persistent Data in the Factory<a name="auto7"/></h3>
+
+ <p>Since the <code>Protocol</code> instance is recreated each time the
+ connection is made, the client needs some way to keep track of data that
+ should be persisted. In the case of the logging bot, it needs to know which
+ channel it is logging, and where to log it.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LogBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span> = <span class="py-src-variable">MessageLogger</span>(<span class="py-src-variable">open</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">filename</span>, <span class="py-src-string">&quot;a&quot;</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">logger</span>.<span class="py-src-variable">log</span>(<span class="py-src-string">&quot;[connected at %s]&quot;</span> %
+ <span class="py-src-variable">time</span>.<span class="py-src-variable">asctime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">localtime</span>(<span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>())))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">signedOn</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">channel</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LogBotFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">channel</span> = <span class="py-src-variable">channel</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">LogBot</span>()
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">factory</span> = <span class="py-src-variable">self</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">p</span>
+</pre>
+
+ <p>When the protocol is created, it gets a reference to the factory as
+ <code>self.factory</code>. It can then access attributes of the factory in
+ its logic. In the case of <code>LogBot</code>, it opens the file and
+ connects to the channel stored in the factory.</p>
+
+ <p>Factories have a default implementation of <code>buildProtocol</code>
+ that does the same thing the example above does, using
+ the <code>protocol</code> attribute of the factory to create the protocol
+ instance. In the example above, the factory could be rewritten to look
+ like this:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">LogBotFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">LogBot</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">channel</span> = <span class="py-src-variable">channel</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+</pre>
+
+ <h2>Further Reading<a name="auto8"/></h2>
+
+ <p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">Protocol</a></code>
+ class used throughout this document is a base implementation
+ of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocol.html" title="twisted.internet.interfaces.IProtocol">IProtocol</a></code>
+ used in most Twisted applications for convenience. To learn about the
+ complete <code>IProtocol</code> interface, see the API documentation for
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocol.html" title="twisted.internet.interfaces.IProtocol">IProtocol</a></code>.</p>
+
+ <p>The <code>transport</code> attribute used in some examples in this
+ document provides the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITCPTransport.html" title="twisted.internet.interfaces.ITCPTransport">ITCPTransport</a></code> interface. To learn
+ about the complete interface, see the API documentation
+ for <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITCPTransport.html" title="twisted.internet.interfaces.ITCPTransport">ITCPTransport</a></code>.</p>
+
+ <p>Interface classes are a way of specifying what methods and attributes an
+ object has and how they behave. See the <a href="components.html" shape="rect">
+ Components: Interfaces and Adapters</a> document for more information on
+ using interfaces in Twisted.</p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/components.html b/doc/core/howto/components.html
new file mode 100644
index 0000000..b52e9e7
--- /dev/null
+++ b/doc/core/howto/components.html
@@ -0,0 +1,603 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Components: Interfaces and Adapters</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Components: Interfaces and Adapters</h1>
+ <div class="toc"><ol><li><a href="#auto0">Interfaces and Components in Twisted code</a></li><ul><li><a href="#auto1">Components and Inheritance</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<p>Object oriented programming languages allow programmers to reuse portions of
+existing code by creating new <q>classes</q> of objects which subclass another
+class. When a class subclasses another, it is said to <em>inherit</em> all of its
+behaviour. The subclass can then <q>override</q> and <q>extend</q> the behavior
+provided to it by the superclass. Inheritance is very useful in many situations,
+but because it is so convenient to use, often becomes abused in large software
+systems, especially when multiple inheritance is involved. One solution is to
+use <em>delegation</em> instead of <q>inheritance</q> where appropriate.
+Delegation is simply the act of asking <em>another</em> object to perform a task
+for an object. To support this design pattern, which is often referred to as
+the <em>components</em> pattern because it involves many small interacting
+components, <em>interfaces</em> and <em>adapters</em> were created by the Zope
+3 team.</p>
+
+<p><q>Interfaces</q> are simply markers which objects can use to say <q>I
+implement this interface</q>. Other objects may then make requests like
+<q>Please give me an object which implements interface X for object type Y</q>.
+Objects which implement an interface for another object type are called
+<q>adapters</q>.</p>
+
+<p>The superclass-subclass relationship is said to be an <em>is-a</em> relationship.
+When designing object hierarchies, object modellers use subclassing when they
+can say that the subclass <em>is</em> the same class as the superclass. For
+example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Shape</span>:
+ <span class="py-src-variable">sideLength</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getSideLength</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setSideLength</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">sideLength</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span> = <span class="py-src-variable">sideLength</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">area</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>, <span class="py-src-string">&quot;Subclasses must implement area&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Triangle</span>(<span class="py-src-parameter">Shape</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">area</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> (<span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span> * <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span>) / <span class="py-src-number">2</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Square</span>(<span class="py-src-parameter">Shape</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">area</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span> * <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span>
+</pre>
+
+<p>In the above example, a Triangle <em>is-a</em> Shape, so it subclasses Shape,
+and a Square <em>is-a</em> Shape, so it also subclasses Shape.</p>
+
+<p>However, subclassing can get complicated, especially when Multiple
+Inheritance enters the picture. Multiple Inheritance allows a class to inherit
+from more than one base class. Software which relies heavily on inheritance
+often ends up having both very wide and very deep inheritance trees, meaning
+that one class inherits from many superclasses spread throughout the system.
+Since subclassing with Multiple Inheritance means <em>implementation
+inheritance</em>, locating a method's actual implementation and ensuring the
+correct method is actually being invoked becomes a challenge. For example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Area</span>:
+ <span class="py-src-variable">sideLength</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getSideLength</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setSideLength</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">sideLength</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span> = <span class="py-src-variable">sideLength</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">area</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>, <span class="py-src-string">&quot;Subclasses must implement area&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Color</span>:
+ <span class="py-src-variable">color</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setColor</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">color</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">color</span> = <span class="py-src-variable">color</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getColor</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">color</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Square</span>(<span class="py-src-parameter">Area</span>, <span class="py-src-parameter">Color</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">area</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span> * <span class="py-src-variable">self</span>.<span class="py-src-variable">sideLength</span>
+</pre>
+
+<p>The reason programmers like using implementation inheritance is because it
+makes code easier to read since the implementation details of Area are in a
+separate place than the implementation details of Color. This is nice, because
+conceivably an object could have a color but not an area, or an area but not a
+color. The problem, though, is that Square is not really an Area or a Color, but
+has an area and color. Thus, we should really be using another object oriented
+technique called <em>composition</em>, which relies on delegation rather than
+inheritance to break code into small reusable chunks. Let us continue with the
+Multiple Inheritance example, though, because it is often used in practice.</p>
+
+<p>What if both the Color and the Area base class defined the same
+method, perhaps <code>calculate</code>? Where would the implementation
+come from? The implementation that is located
+for <code>Square().calculate()</code> depends on the method resolution
+order, or MRO, and can change when programmers change seemingly
+unrelated things by refactoring classes in other parts of the system,
+causing obscure bugs. Our first thought might be to change the
+calculate method name to avoid name clashes, to
+perhaps <code>calculateArea</code> and <code>calculateColor</code>.
+While explicit, this change could potentially require a large number
+of changes throughout a system, and is error-prone, especially when
+attempting to integrate two systems which you didn't write.</p>
+
+<p>Let's imagine another example. We have an electric appliance, say a hair
+dryer. The hair dryer is American voltage. We have two electric sockets, one of
+them an American 120 Volt socket, and one of them a United Kingdom 240 Volt socket. If
+we plug the hair dryer into the 240 Volt socket, it is going to expect 120 Volt
+current and errors will result. Going back and changing the hair dryer to
+support both <code>plug120Volt</code> and <code>plug240Volt</code> methods would
+be tedious, and what if we decided we needed to plug the hair dryer into yet
+another type of socket? For example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">HairDryer</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">plug</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">socket</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">socket</span>.<span class="py-src-variable">voltage</span>() == <span class="py-src-number">120</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I was plugged in properly and am operating.&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I was plugged in improperly and &quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;now you have no hair dryer any more.&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AmericanSocket</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-number">120</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UKSocket</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-number">240</span>
+</pre>
+
+<p>Given these classes, the following operations can be performed:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; hd = HairDryer()
+&gt;&gt;&gt; am = AmericanSocket()
+&gt;&gt;&gt; hd.plug(am)
+I was plugged in properly and am operating.
+&gt;&gt;&gt; uk = UKSocket()
+&gt;&gt;&gt; hd.plug(uk)
+I was plugged in improperly and
+now you have no hair dryer any more.
+</pre>
+
+<p>We are going to attempt to solve this problem by writing an Adapter for
+the <code>UKSocket</code> which converts the voltage for use with an American
+hair dryer. An Adapter is a class which is constructed with one and only one
+argument, the <q>adaptee</q> or <q>original</q> object. In this example, we
+will show all code involved for clarity:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">AdaptToAmericanSocket</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">original</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span> = <span class="py-src-variable">original</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span>.<span class="py-src-variable">voltage</span>() / <span class="py-src-number">2</span>
+</pre>
+
+<p>Now, we can use it as so:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; hd = HairDryer()
+&gt;&gt;&gt; uk = UKSocket()
+&gt;&gt;&gt; adapted = AdaptToAmericanSocket(uk)
+&gt;&gt;&gt; hd.plug(adapted)
+I was plugged in properly and am operating.
+</pre>
+
+<p>So, as you can see, an adapter can 'override' the original implementation. It
+can also 'extend' the interface of the original object by providing methods the
+original object did not have. Note that an Adapter must explicitly delegate any
+method calls it does not wish to modify to the original, otherwise the Adapter
+cannot be used in places where the original is expected. Usually this is not a
+problem, as an Adapter is created to conform an object to a particular interface
+and then discarded.</p>
+
+<h2>Interfaces and Components in Twisted code<a name="auto0"/></h2>
+
+<p>Adapters are a useful way of using multiple classes to factor code into
+discrete chunks. However, they are not very interesting without some more
+infrastructure. If each piece of code which wished to use an adapted object had
+to explicitly construct the adapter itself, the coupling between components
+would be too tight. We would like to achieve <q>loose coupling</q>, and this is
+where <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.html" title="twisted.python.components">twisted.python.components</a></code> comes in.</p>
+
+<p>First, we need to discuss Interfaces in more detail. As we mentioned
+earlier, an Interface is nothing more than a class which is used as a marker.
+Interfaces should be subclasses of <code>zope.interface.Interface</code>, and
+have a very odd look to python programmers not used to them:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IAmericanSocket</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return the voltage produced by this socket object, as an integer.
+ &quot;&quot;&quot;</span>
+</pre>
+
+<p>Notice how it looks just like a regular class definition, other than
+inheriting from <code>Interface</code>? However, the method definitions inside
+the class block do not have any method body! Since Python does not have any
+native language-level support for Interfaces like Java does, this is what
+distinguishes an Interface definition from a Class.</p>
+
+<p>Now that we have a defined Interface, we can talk about objects using terms
+like this: <q>The <code>AmericanSocket</code> class implements the <code>IAmericanSocket</code> interface</q> and <q>Please give me an object which
+adapts <code>UKSocket</code> to the <code>IAmericanSocket</code>
+interface</q>. We can make <em>declarations</em> about what interfaces a certain
+class implements, and we can request adapters which implement a certain
+interface for a specific class.</p>
+
+<p>Let's look at how we declare that a class implements an interface:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AmericanSocket</span>:
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IAmericanSocket</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-number">120</span>
+</pre>
+
+<p>So, to declare that a class implements an interface, we simply
+call <code>zope.interface.implements</code> at the class level.</p>
+
+<p>Now, let's say we want to rewrite
+the <code>AdaptToAmericanSocket</code> class as a real adapter. In
+this case we also specify it as
+implementing <code>IAmericanSocket</code>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AdaptToAmericanSocket</span>:
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IAmericanSocket</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">original</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Pass the original UKSocket object as original
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span> = <span class="py-src-variable">original</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span>.<span class="py-src-variable">voltage</span>() / <span class="py-src-number">2</span>
+</pre>
+
+<p>Notice how we placed the implements declaration on this adapter class. So
+far, we have not achieved anything by using components other than requiring us
+to type more. In order for components to be useful, we must use the
+<em>component registry</em>. Since <code>AdaptToAmericanSocket</code>
+implements
+<code>IAmericanSocket</code> and regulates the voltage of a
+<code>UKSocket</code> object, we can register
+<code>AdaptToAmericanSocket</code> as an <code>IAmericanSocket</code> adapter
+for the <code>UKSocket</code> class. It is easier to see how this is
+done in code than to describe it:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IAmericanSocket</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>():
+ <span class="py-src-string">&quot;&quot;&quot;Return the voltage produced by this socket object, as an integer.
+ &quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AmericanSocket</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IAmericanSocket</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-number">120</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UKSocket</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-number">240</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AdaptToAmericanSocket</span>:
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IAmericanSocket</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">original</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span> = <span class="py-src-variable">original</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">voltage</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">original</span>.<span class="py-src-variable">voltage</span>() / <span class="py-src-number">2</span>
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(
+ <span class="py-src-variable">AdaptToAmericanSocket</span>,
+ <span class="py-src-variable">UKSocket</span>,
+ <span class="py-src-variable">IAmericanSocket</span>)
+</pre>
+
+<p>Now, if we run this script in the interactive interpreter, we can discover a
+little more about how to use components. The first thing we can do is discover
+whether an object implements an interface or not:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; IAmericanSocket.implementedBy(AmericanSocket)
+True
+&gt;&gt;&gt; IAmericanSocket.implementedBy(UKSocket)
+False
+&gt;&gt;&gt; am = AmericanSocket()
+&gt;&gt;&gt; uk = UKSocket()
+&gt;&gt;&gt; IAmericanSocket.providedBy(am)
+True
+&gt;&gt;&gt; IAmericanSocket.providedBy(uk)
+False
+</pre>
+
+<p>As you can see, the <code>AmericanSocket</code> instance claims to
+implement <code>IAmericanSocket</code>, but the <code>UKSocket</code>
+does not. If we wanted to use the <code>HairDryer</code> with the <code>AmericanSocket</code>, we could know that it would be safe to do so by
+checking whether it implements <code>IAmericanSocket</code>. However, if we
+decide we want to use <code>HairDryer</code> with a <code>UKSocket</code>
+instance, we must <em>adapt</em> it to <code>IAmericanSocket</code> before
+doing so. We use the interface object to do this:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; IAmericanSocket(uk)
+&lt;__main__.AdaptToAmericanSocket instance at 0x1a5120&gt;
+</pre>
+
+<p>When calling an interface with an object as an argument, the interface
+looks in the adapter registry for an adapter which implements the interface for
+the given instance's class. If it finds one, it constructs an instance of the
+Adapter class, passing the constructor the original instance, and returns it.
+Now the <code>HairDryer</code> can safely be used with the adapted <code>UKSocket</code>. But what happens if we attempt to adapt an object
+which already implements <code>IAmericanSocket</code>? We simply get back the
+original instance:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; IAmericanSocket(am)
+&lt;__main__.AmericanSocket instance at 0x36bff0&gt;
+</pre>
+
+<p>So, we could write a new <q>smart</q> <code>HairDryer</code> which
+automatically looked up an adapter for the socket you tried to plug it into:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">HairDryer</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">plug</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">socket</span>):
+ <span class="py-src-variable">adapted</span> = <span class="py-src-variable">IAmericanSocket</span>(<span class="py-src-variable">socket</span>)
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">adapted</span>.<span class="py-src-variable">voltage</span>() == <span class="py-src-number">120</span>, <span class="py-src-string">&quot;BOOM&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I was plugged in properly and am operating&quot;</span>
+</pre>
+
+<p>Now, if we create an instance of our new <q>smart</q> <code>HairDryer</code>
+and attempt to plug it in to various sockets, the <code>HairDryer</code> will
+adapt itself automatically depending on the type of socket it is plugged in
+to:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; am = AmericanSocket()
+&gt;&gt;&gt; uk = UKSocket()
+&gt;&gt;&gt; hd = HairDryer()
+&gt;&gt;&gt; hd.plug(am)
+I was plugged in properly and am operating
+&gt;&gt;&gt; hd.plug(uk)
+I was plugged in properly and am operating
+</pre>
+
+<p>Voila; the magic of components.</p>
+
+<h3>Components and Inheritance<a name="auto1"/></h3>
+
+<p>If you inherit from a class which implements some interface, and your new
+subclass declares that it implements another interface, the implements will be
+inherited by default.</p>
+
+<p>For example, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code> is a class
+which implements <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.IPBRoot.html" title="twisted.spread.pb.IPBRoot">IPBRoot</a></code>. This interface indicates that an
+object has remotely-invokable methods and can be used as the initial object
+served by a new Broker instance. It has an <code>implements</code> setting
+like:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Root</span>(<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPBRoot</span>)
+</pre>
+
+<p>Suppose you have your own class which implements your
+<code>IMyInterface</code> interface:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>, <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IMyInterface</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyThing</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IMyInterface</span>)
+</pre>
+
+<p>Now if you want to make this class inherit from <code>pb.Root</code>,
+the interfaces code will automatically determine that it also implements
+<code>IPBRoot</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>, <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IMyInterface</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyThing</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IMyInterface</span>)
+</pre>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; from twisted.spread.flavors import IPBRoot
+&gt;&gt;&gt; IPBRoot.implementedBy(MyThing)
+True
+</pre>
+
+<p>If you want <code>MyThing</code> to inherit from <code>pb.Root</code> but <em>not</em> implement <code>IPBRoot</code> like <code>pb.Root</code> does,
+use <code>implementOnly</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implementsOnly</span>, <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IMyInterface</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyThing</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-variable">implementsOnly</span>(<span class="py-src-variable">IMyInterface</span>)
+</pre>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; from twisted.spread.pb import IPBRoot
+&gt;&gt;&gt; IPBRoot.implementedBy(MyThing)
+False
+</pre>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/constants.html b/doc/core/howto/constants.html
new file mode 100644
index 0000000..ede0af0
--- /dev/null
+++ b/doc/core/howto/constants.html
@@ -0,0 +1,456 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Symbolic Constants</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Symbolic Constants</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Constant Names</a></li><li><a href="#auto2">Constants With Values</a></li><li><a href="#auto3">Constants As Flags</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>It is often useful to define names which will be treated as
+ constants. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.constants.html" title="twisted.python.constants">twisted.python.constants</a></code> provides APIs
+ for defining such symbolic constants with minimal overhead and some useful
+ features beyond those afforded by the common Python idioms for this task.</p>
+
+ <p>This document will explain how to use these APIs and what circumstances
+ they might be helpful in.</p>
+
+ <h2>Constant Names<a name="auto1"/></h2>
+
+ <p>Constants which have no value apart from their name and identity can be
+ defined by subclassing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.constants.Names.html" title="twisted.python.constants.Names">Names</a></code>.
+ Consider this example, in which some HTTP request method constants are defined.</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NamedConstant</span>, <span class="py-src-variable">Names</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">METHOD</span>(<span class="py-src-parameter">Names</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Constants representing various HTTP request methods.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">GET</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">PUT</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">POST</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">DELETE</span> = <span class="py-src-variable">NamedConstant</span>()
+</pre>
+
+ <p>Only direct subclasses of <code>Names</code> are supported (i.e., you
+ cannot subclass <code>METHOD</code> to add new constants the collection).</p>
+
+ <p>Given this definition, constants can be looked up by name using attribute
+ access on the <code>METHOD</code> object:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; METHOD.GET
+&lt;METHOD=GET&gt;
+&gt;&gt;&gt; METHOD.PUT
+&lt;METHOD=PUT&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>If it's necessary to look up constants based on user input of some sort, a
+ safe way to do it is using <code>lookupByName</code>:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; METHOD.lookupByName('GET')
+&lt;METHOD=GET&gt;
+&gt;&gt;&gt; METHOD.lookupByName('__doc__')
+Traceback (most recent call last):
+ File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
+ File &quot;twisted/python/constants.py&quot;, line 145, in lookupByName
+ raise ValueError(name)
+ValueError: __doc__
+&gt;&gt;&gt;
+ </pre>
+
+ <p>As demonstrated, it is safe because any name not associated with a constant
+ (even those special names initialized by Python itself) will result
+ in <code>ValueError</code> being raised, not some other object not intended to
+ be used the way the constants are used.</p>
+
+ <p>The constants can also be enumerated using the <code>iterconstants</code>
+ method.</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; list(METHOD.iterconstants())
+[&lt;METHOD=GET&gt;, &lt;METHOD=PUT&gt;, &lt;METHOD=POST&gt;, &lt;METHOD=DELETE&gt;]
+&gt;&gt;&gt;
+ </pre>
+
+ <p>And constants can also be compared, either for equality or identity:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; METHOD.GET is METHOD.GET
+True
+&gt;&gt;&gt; METHOD.GET == METHOD.GET
+True
+&gt;&gt;&gt; METHOD.GET is METHOD.PUT
+False
+&gt;&gt;&gt; METHOD.GET == METHOD.PUT
+False
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Custom functionality can also be associated with constants defined this
+ way. A subclass of <code>Names</code> may define class methods to implement
+ such functionality. Consider this redefinition of <code>METHOD</code>:</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NamedConstant</span>, <span class="py-src-variable">Names</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">METHOD</span>(<span class="py-src-parameter">Names</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Constants representing various HTTP request methods.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">GET</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">PUT</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">POST</span> = <span class="py-src-variable">NamedConstant</span>()
+ <span class="py-src-variable">DELETE</span> = <span class="py-src-variable">NamedConstant</span>()
+
+ @<span class="py-src-variable">classmethod</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">isIdempotent</span>(<span class="py-src-parameter">cls</span>, <span class="py-src-parameter">method</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return True if the given method is side-effect free, False otherwise.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">method</span> <span class="py-src-keyword">is</span> <span class="py-src-variable">cls</span>.<span class="py-src-variable">GET</span>
+</pre>
+
+ <p>This functionality can be used as any class methods are used:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; METHOD.isIdempotent(METHOD.GET)
+True
+&gt;&gt;&gt; METHOD.isIdempotent(METHOD.POST)
+False
+&gt;&gt;&gt;
+ </pre>
+
+ <h2>Constants With Values<a name="auto2"/></h2>
+
+ <p>Constants with a particular associated value are supported by
+ the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.constants.Values.html" title="twisted.python.constants.Values">Values</a></code> base
+ class. Consider this example, in which some HTTP status code constants are
+ defined.
+ </p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ValueConstant</span>, <span class="py-src-variable">Values</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">STATUS</span>(<span class="py-src-parameter">Values</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Constants representing various HTTP status codes.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">OK</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;200&quot;</span>)
+ <span class="py-src-variable">FOUND</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;302&quot;</span>)
+ <span class="py-src-variable">NOT_FOUND</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;404&quot;</span>)
+</pre>
+
+ <p>As with <code>Names</code>, constants are accessed as attributes of the
+ class object:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.OK
+&lt;STATUS=OK&gt;
+&gt;&gt;&gt; STATUS.FOUND
+&lt;STATUS=FOUND&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Additionally, the values of the constants can be accessed using
+ the <code>value</code> attribute of one these objects:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.OK.value
+'200'
+&gt;&gt;&gt;
+ </pre>
+
+ <p>And as with <code>Names</code>, constants can be looked up by name:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.lookupByName('NOT_FOUND')
+&lt;STATUS=NOT_FOUND&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Constants on a <code>Values</code> subclass can also be looked up by
+ value:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.lookupByValue('404')
+&lt;STATUS=NOT_FOUND&gt;
+&gt;&gt;&gt; STATUS.lookupByValue('500')
+Traceback (most recent call last):
+ File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
+ File &quot;twisted/python/constants.py&quot;, line 244, in lookupByValue
+ raise ValueError(value)
+ValueError: 500
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Multiple constants may have the same value. If they do,
+ <code>lookupByValue</code> will find the one which is defined first.</p>
+
+ <p>Iteration is also supported:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; list(STATUS.iterconstants())
+[&lt;STATUS=OK&gt;, &lt;STATUS=FOUND&gt;, &lt;STATUS=NOT_FOUND&gt;]
+&gt;&gt;&gt;
+ </pre>
+
+ <p>And constants can be compared for equality and identity:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.OK == STATUS.OK
+True
+&gt;&gt;&gt; STATUS.OK is STATUS.OK
+True
+&gt;&gt;&gt; STATUS.OK == STATUS.OK
+True
+&gt;&gt;&gt; STATUS.OK is STATUS.NOT_FOUND
+False
+&gt;&gt;&gt; STATUS.OK == STATUS.NOT_FOUND
+False
+&gt;&gt;&gt;
+ </pre>
+
+ <p>And, as with <code>Names</code>, a subclass of <code>Values</code> can
+ define methods:</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ValueConstant</span>, <span class="py-src-variable">Values</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">STATUS</span>(<span class="py-src-parameter">Values</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Constants representing various HTTP status codes.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">OK</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;200&quot;</span>)
+ <span class="py-src-variable">NO_CONTENT</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;204&quot;</span>)
+ <span class="py-src-variable">NOT_MODIFIED</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;304&quot;</span>)
+ <span class="py-src-variable">NOT_FOUND</span> = <span class="py-src-variable">ValueConstant</span>(<span class="py-src-string">&quot;404&quot;</span>)
+
+ @<span class="py-src-variable">classmethod</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">hasBody</span>(<span class="py-src-parameter">cls</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return True if the given status is associated with a response body,
+ False otherwise.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">status</span> <span class="py-src-keyword">in</span> (<span class="py-src-variable">cls</span>.<span class="py-src-variable">NO_CONTENT</span>, <span class="py-src-variable">cls</span>.<span class="py-src-variable">NOT_MODIFIED</span>)
+</pre>
+
+ <p>This functionality can be used as any class methods are used:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; STATUS.hasBody(STATUS.OK)
+True
+&gt;&gt;&gt; STATUS.hasBody(STATUS.NO_CONTENT)
+False
+&gt;&gt;&gt;
+ </pre>
+
+ <h2>Constants As Flags<a name="auto3"/></h2>
+
+ <p>Integers are often used as a simple set for constants. The values for
+ these constants are assigned as powers of two so that bits in the integer can
+ be set to represent them. Individual bits are often called <em>flags</em>.
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.constants.Flags.html" title="twisted.python.constants.Flags">Flags</a></code> supports this
+ use-case, including allowing constants with particular bits to be set, for
+ interoperability with other tools.</p>
+
+ <p>POSIX filesystem access control is traditionally done using a bitvector
+ defining which users and groups may perform which operations on a file. This
+ state might be represented using <code>Flags</code> as follows:</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FlagConstant</span>, <span class="py-src-variable">Flags</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Permission</span>(<span class="py-src-parameter">Flags</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Constants representing user, group, and other access bits for reading,
+ writing, and execution.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">OTHER_EXECUTE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">OTHER_WRITE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">OTHER_READ</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">GROUP_EXECUTE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">GROUP_WRITE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">GROUP_READ</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">USER_EXECUTE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">USER_WRITE</span> = <span class="py-src-variable">FlagConstant</span>()
+ <span class="py-src-variable">USER_READ</span> = <span class="py-src-variable">FlagConstant</span>()
+</pre>
+
+ <p>
+ As for the previous types of constants, these can be accessed as attributes
+ of the class object:
+ </p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; Permission.USER_READ
+&lt;Permission=USER_READ&gt;
+&gt;&gt;&gt; Permission.USER_WRITE
+&lt;Permission=USER_WRITE&gt;
+&gt;&gt;&gt; Permission.USER_EXECUTE
+&lt;Permission=USER_EXECUTE&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>These constant objects also have a <code>value</code> attribute giving
+ their integer value:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; Permission.USER_READ.value
+256
+&gt;&gt;&gt;
+ </pre>
+
+ <p>And these constants can be looked up by name or value:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; Permission.lookupByName('USER_READ') is Permission.USER_READ
+True
+&gt;&gt;&gt; Permission.lookupByValue(256) is Permission.USER_READ
+True
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Constants can also be combined using the logical operators <code>&amp;</code>
+ (<em>and</em>), <code>|</code> (<em>or</em>), and <code>^</code>
+ (<em>exclusive or</em>).
+ </p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; Permission.USER_READ | Permission.USER_WRITE
+&lt;Permission={USER_READ,USER_WRITE}&gt;
+&gt;&gt;&gt; (Permission.USER_READ | Permission.USER_WRITE) &amp; Permission.USER_WRITE
+&lt;Permission=USER_WRITE&gt;
+&gt;&gt;&gt; (Permission.USER_READ | Permission.USER_WRITE) ^ Permission.USER_WRITE
+&lt;Permission=USER_READ&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>The unary operator <code>~</code> (<em>not</em>) is also defined:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; ~Permission.USER_READ
+&lt;Permission={GROUP_EXECUTE,GROUP_READ,GROUP_WRITE,OTHER_EXECUTE,OTHER_READ,OTHER_WRITE,USER_EXECUTE,USER_WRITE}&gt;
+&gt;&gt;&gt;
+ </pre>
+
+ <p>Constants created using these operators also have a <code>value</code>
+ attribute.</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; (~Permission.USER_WRITE).value
+383
+&gt;&gt;&gt;
+ </pre>
+
+ <p>
+ Note the care taken to ensure the <code>~</code> operator is applied first
+ and the <code>value</code>attribute is looked up second.
+ </p>
+
+ <p>A <code>Flags</code> subclass can also define methods, just as
+ a <code>Names</code> or <code>Values</code> subclass may. For example,
+ <code>Permission</code> might benefit from a method to format a flag as a
+ string in the traditional style. Consider this addition to that class:</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">filepath</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">constants</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FlagConstant</span>, <span class="py-src-variable">Flags</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Permission</span>(<span class="py-src-parameter">Flags</span>):
+ ...
+
+ @<span class="py-src-variable">classmethod</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">format</span>(<span class="py-src-parameter">cls</span>, <span class="py-src-parameter">permissions</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Format permissions flags in the traditional 'rwxr-xr-x' style.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">filepath</span>.<span class="py-src-variable">Permissions</span>(<span class="py-src-variable">permissions</span>.<span class="py-src-variable">value</span>).<span class="py-src-variable">shorthand</span>()
+</pre>
+
+ <p>Use this like any other class method:</p>
+
+ <pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; Permission.format(Permission.USER_READ | Permission.USER_WRITE | Permission.GROUP_READ | Permission.OTHER_READ)
+'rw-r--r--'
+&gt;&gt;&gt;
+ </pre>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/cred.html b/doc/core/howto/cred.html
new file mode 100644
index 0000000..44b4822
--- /dev/null
+++ b/doc/core/howto/cred.html
@@ -0,0 +1,566 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Cred: Pluggable Authentication</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Cred: Pluggable Authentication</h1>
+ <div class="toc"><ol><li><a href="#auto0">Goals</a></li><li><a href="#auto1">Cred objects</a></li><ul><li><a href="#auto2">The Portal</a></li><li><a href="#auto3">The CredentialChecker</a></li><li><a href="#auto4">The Credentials</a></li><li><a href="#auto5">The Realm</a></li><li><a href="#auto6">The Avatar</a></li><li><a href="#auto7">The Mind</a></li></ul><li><a href="#auto8">Responsibilities</a></li><ul><li><a href="#auto9">Server protocol implementation</a></li><li><a href="#auto10">Application implementation</a></li><li><a href="#auto11">Deployment</a></li></ul><li><a href="#auto12">Cred plugins</a></li><ul><li><a href="#auto13">Authentication with cred plugins</a></li><li><a href="#auto14">Building a cred plugin</a></li></ul><li><a href="#auto15">Conclusion</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Goals<a name="auto0"/></h2>
+
+<p>Cred is a pluggable authentication system for servers. It allows any
+number of network protocols to connect and authenticate to a system, and
+communicate to those aspects of the system which are meaningful to the specific
+protocol. For example, Twisted's POP3 support passes a <q>username and
+password</q> set of credentials to get back a mailbox for the specified email
+account. IMAP does the same, but retrieves a slightly different view of the
+same mailbox, enabling those features specific to IMAP which are not available
+in other mail protocols.</p>
+
+<p>Cred is designed to allow both the backend implementation of the business
+logic - called the <em>avatar</em> - and the authentication database - called
+the <em>credential checker</em> - to be decided during deployment. For example,
+the same POP3 server should be able to authenticate against the local UNIX
+password database or an LDAP server without having to know anything about how
+or where mail is stored. </p>
+
+<p>To sketch out how this works - a <q>Realm</q> corresponds to an application
+domain and is in charge of avatars, which are network-accessible business logic
+objects. To connect this to an authentication database, a top-level object
+called a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.html" title="twisted.cred.portal.Portal">Portal</a></code> stores a
+realm, and a number of credential checkers. Something that wishes to log in,
+such as a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">Protocol</a></code>,
+stores a reference to the portal. Login consists of passing credentials and a
+request interface (e.g. POP3's <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.mail.pop3.IMailbox.html" title="twisted.mail.pop3.IMailbox">IMailbox</a></code>) to the portal. The portal passes
+the credentials to the appropriate credential checker, which returns an avatar
+ID. The ID is passed to the realm, which returns the appropriate avatar. For a
+Portal that has a realm that creates mailbox objects and a credential checker
+that checks /etc/passwd, login consists of passing in a username/password and
+the IMailbox interface to the portal. The portal passes this to the /etc/passwd
+credential checker, gets back a avatar ID corresponding to an email account,
+passes that to the realm and gets back a mailbox object for that email
+account.</p>
+
+<p>Putting all this together, here's how a login request will typically be
+processed:</p>
+
+<img src="../img/cred-login.png" title="Cred Login"/>
+
+ <h2>Cred objects<a name="auto1"/></h2>
+ <h3>The Portal<a name="auto2"/></h3>
+<p>This is the the core of login, the point of integration between all the objects
+in the cred system. There is one
+concrete implementation of Portal, and no interface - it does a very
+simple task. A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.html" title="twisted.cred.portal.Portal">Portal</a></code>
+associates one (1) Realm with a collection of
+CredentialChecker instances. (More on those later.)</p>
+
+<p>If you are writing a protocol that needs to authenticate against
+something, you will need a reference to a Portal, and to nothing else.
+This has only 2 methods -</p>
+
+<ul>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.login.html" title="twisted.cred.portal.Portal.login">login</a></code><code>(credentials, mind, *interfaces)</code>
+
+<p>The docstring is quite expansive (see <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.html" title="twisted.cred.portal">twisted.cred.portal</a></code>), but in
+brief, this is what you call when you need to call in order to connect
+a user to the system. Typically you only pass in one interface, and the mind
+is <code class="python">None</code>. The interfaces are the possible interfaces the returned
+avatar is expected to implement, in order of preference.
+The result is a deferred which fires a tuple of:</p>
+ <ul>
+ <li>interface the avatar implements (which was one of the interfaces passed in the <code>*interfaces</code>
+tuple)</li>
+ <li>an object that implements that interface (an avatar)</li>
+ <li>logout, a 0-argument callable which disconnects the connection that was
+established by this call to login</li>
+ </ul>
+<p>The logout method has to be called when the avatar is logged out. For POP3 this means
+when the protocol is disconnected or logged out, etc..</p>
+</li>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.registerChecker.html" title="twisted.cred.portal.Portal.registerChecker">registerChecker</a></code><code>(checker, *credentialInterfaces)</code>
+
+<p>which adds a CredentialChecker to the portal. The optional list of interfaces are interfaces of credentials
+that the checker is able to check.</p>
+</li></ul>
+
+ <h3>The CredentialChecker<a name="auto3"/></h3>
+
+<p>This is an object implementing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.ICredentialsChecker.html" title="twisted.cred.checkers.ICredentialsChecker">ICredentialsChecker</a></code> which resolves some
+credentials to an avatar ID.
+
+Whether the credentials are stored in an in-memory data structure, an
+Apache-style htaccess file, a UNIX password database, an SSH key database,
+or any other form, an implementation of <code>ICredentialsChecker</code> is
+how this data is connected to cred.
+
+A credential checker
+stipulates some requirements of the credentials it can check by
+specifying a credentialInterfaces attribute, which is a list of
+interfaces. Credentials passed to its requestAvatarId method must
+implement one of those interfaces.</p>
+
+<p>For the most part, these things will just check usernames and passwords
+and produce the username as the result, but hopefully we will be seeing
+some public-key, challenge-response, and certificate based credential
+checker mechanisms soon.</p>
+
+<p>A credential checker should raise an error if it cannot authenticate
+the user, and return <code>twisted.cred.checkers.ANONYMOUS</code>
+for anonymous access.</p>
+
+ <h3>The Credentials<a name="auto4"/></h3>
+<p>Oddly enough, this represents some credentials that the user presents.
+Usually this will just be a small static blob of data, but in some
+cases it will actually be an object connected to a network protocol.
+For example, a username/password pair is static, but a
+challenge/response server is an active state-machine that will require
+several method calls in order to determine a result.</p>
+
+<p>Twisted comes with a number of credentials interfaces and implementations
+in the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.credentials.html" title="twisted.cred.credentials">twisted.cred.credentials</a></code> module,
+such as <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.credentials.IUsernamePassword.html" title="twisted.cred.credentials.IUsernamePassword">IUsernamePassword</a></code>
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.credentials.IUsernameHashedPassword.html" title="twisted.cred.credentials.IUsernameHashedPassword">IUsernameHashedPassword</a></code>.</p>
+
+ <h3>The Realm<a name="auto5"/></h3>
+<p>A realm is an interface which connects your universe of <q>business
+objects</q> to the authentication system.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.html" title="twisted.cred.portal.IRealm">IRealm</a></code> is another one-method interface:</p>
+
+<ul>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.requestAvatar.html" title="twisted.cred.portal.IRealm.requestAvatar">requestAvatar</a></code><code>(avatarId, mind, *interfaces)</code>
+
+<p>This method will typically be called from 'Portal.login'. The avatarId
+is the one returned by a CredentialChecker.</p>
+
+<div class="note"><strong>Note: </strong>Note that <code>avatarId</code> must always be a string. In
+particular, do not use unicode strings. If internationalized support is needed,
+it is recommended to use UTF-8, and take care of decoding in the realm. </div>
+
+<p>The important thing to realize about this method is that if it is being
+called, <em>the user has already authenticated</em>. Therefore, if possible,
+the Realm should create a new user if one does not already exist
+whenever possible. Of course, sometimes this will be impossible
+without more information, and that is the case that the interfaces
+argument is for.</p>
+</li>
+</ul>
+
+<p>Since requestAvatar should be called from a Deferred callback, it may
+return a Deferred or a synchronous result.</p>
+
+ <h3>The Avatar<a name="auto6"/></h3>
+
+<p>An avatar is a business logic object for a specific user. For POP3, it's
+a mailbox, for a first-person-shooter it's the object that interacts with
+the game, the actor as it were. Avatars are specific to an application,
+and each avatar represents a single <q>user</q>.</p>
+
+ <h3>The Mind<a name="auto7"/></h3>
+
+<p>As mentioned before, the mind is usually <code>None</code>, so you can skip this
+bit if you want.</p>
+
+<p>Masters of Perspective Broker already know this object as the ill-named
+<q>client object</q>. There is no <q>mind</q> class, or even interface, but it
+is an object which serves an important role - any notifications which are to be
+relayed to an authenticated client are passed through a 'mind'. In addition, it
+allows passing more information to the realm during login in addition to the
+avatar ID.</p>
+
+<p>The name may seem rather unusual, but considering that a Mind is
+representative of the entity on the <q>other end</q> of a network connection
+that is both receiving updates and issuing commands, I believe it is
+appropriate.</p>
+
+<p>Although many protocols will not use this, it serves an important role.
+ It is provided as an argument both to the Portal and to the Realm,
+although a CredentialChecker should interact with a client program
+exclusively through a Credentials instance.</p>
+
+<p>Unlike the original Perspective Broker <q>client object</q>, a Mind's
+implementation is most often dictated by the protocol that is
+connecting rather than the Realm. A Realm which requires a particular
+interface to issue notifications will need to wrap the Protocol's mind
+implementation with an adapter in order to get one that conforms to its
+expected interface - however, Perspective Broker will likely continue
+to use the model where the client object has a pre-specified remote
+interface.</p>
+
+<p>(If you don't quite understand this, it's fine. It's hard to explain,
+and it's not used in simple usages of cred, so feel free to pass None
+until you find yourself requiring something like this.)</p>
+
+ <h2>Responsibilities<a name="auto8"/></h2>
+
+ <h3>Server protocol implementation<a name="auto9"/></h3>
+
+<p>The protocol implementor should define the interface the avatar should implement,
+and design the protocol to have a portal attached. When a user logs in using the
+protocol, a credential object is created, passed to the portal, and an avatar
+with the appropriate interface is requested. When the user logs out or the protocol
+is disconnected, the avatar should be logged out.</p>
+
+<p>The protocol designer should not hardcode how users are authenticated or the
+realm implemented. For example, a POP3 protocol implementation would require a portal whose
+realm returns avatars implementing IMailbox and whose credential checker accepts
+username/password credentials, but that is all. Here's a sketch of how the code
+might look - note that USER and PASS are the protocol commands used to login, and
+the DELE command can only be used after you are logged in:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>, <span class="py-src-variable">error</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IMailbox</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Interface specification for mailbox.&quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">deleteMessage</span>(<span class="py-src-parameter">index</span>): <span class="py-src-keyword">pass</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">POP3</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-comment"># ...</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">portal</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">portal</span> = <span class="py-src-variable">portal</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">do_DELE</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">i</span>):
+ <span class="py-src-comment"># uses self.mbox, which is set after login</span>
+ <span class="py-src-variable">i</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">i</span>)-<span class="py-src-number">1</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">mbox</span>.<span class="py-src-variable">deleteMessage</span>(<span class="py-src-variable">i</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">successResponse</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">do_USER</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_userIs</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">successResponse</span>(<span class="py-src-string">'USER accepted, send PASS'</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">do_PASS</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">password</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_userIs</span> <span class="py-src-keyword">is</span> <span class="py-src-variable">None</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">failResponse</span>(<span class="py-src-string">&quot;USER required before PASS&quot;</span>)
+ <span class="py-src-keyword">return</span>
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">_userIs</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_userIs</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">maybeDeferred</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">authenticateUserPASS</span>, <span class="py-src-variable">user</span>, <span class="py-src-variable">password</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cbMailbox</span>, <span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">authenticateUserPASS</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">password</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">is</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">None</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">portal</span>.<span class="py-src-variable">login</span>(
+ <span class="py-src-variable">cred</span>.<span class="py-src-variable">credentials</span>.<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">password</span>),
+ <span class="py-src-variable">None</span>,
+ <span class="py-src-variable">IMailbox</span>
+ )
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">error</span>.<span class="py-src-variable">UnauthorizedLogin</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cbMailbox</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">ial</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">interface</span>, <span class="py-src-variable">avatar</span>, <span class="py-src-variable">logout</span> = <span class="py-src-variable">ial</span>
+
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">interface</span> <span class="py-src-keyword">is</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">IMailbox</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">failResponse</span>(<span class="py-src-string">'Authentication failed'</span>)
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>(<span class="py-src-string">&quot;_cbMailbox() called with an interface other than IMailbox&quot;</span>)
+ <span class="py-src-keyword">return</span>
+
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">mbox</span> = <span class="py-src-variable">avatar</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_onLogout</span> = <span class="py-src-variable">logout</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">successResponse</span>(<span class="py-src-string">'Authentication succeeded'</span>)
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">msg</span>(<span class="py-src-string">&quot;Authenticated login for &quot;</span> + <span class="py-src-variable">user</span>)
+</pre>
+
+ <h3>Application implementation<a name="auto10"/></h3>
+
+<p>The application developer can implement realms and credential checkers. For example,
+she might implement a realm that returns IMailbox implementing avatars, using MySQL
+for storage, or perhaps a credential checker that uses LDAP for authentication.
+In the following example, the Realm for a simple remote object service (using
+Twisted's Perspective Broker protocol) is implemented:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IRealm</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimplePerspective</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">text</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'echoing'</span>,<span class="py-src-variable">text</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">text</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">logout</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">self</span>, <span class="py-src-string">&quot;logged out&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-variable">avatar</span> = <span class="py-src-variable">SimplePerspective</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">avatar</span>, <span class="py-src-variable">avatar</span>.<span class="py-src-variable">logout</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>(<span class="py-src-string">&quot;no interface&quot;</span>)
+</pre>
+
+ <h3>Deployment<a name="auto11"/></h3>
+
+<p>Deployment involves tying together a protocol, an appropriate realm and a credential
+checker. For example, a POP3 server can be constructed by attaching to it a portal
+that wraps the MySQL-based realm and an /etc/passwd credential checker, or perhaps
+the LDAP credential checker if that is more useful. The following example shows
+how the SimpleRealm in the previous example is deployed using an in-memory credential checker:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">checkers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>
+
+<span class="py-src-variable">portal</span> = <span class="py-src-variable">Portal</span>(<span class="py-src-variable">SimpleRealm</span>())
+<span class="py-src-variable">checker</span> = <span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>()
+<span class="py-src-variable">checker</span>.<span class="py-src-variable">addUser</span>(<span class="py-src-string">&quot;guest&quot;</span>, <span class="py-src-string">&quot;password&quot;</span>)
+<span class="py-src-variable">portal</span>.<span class="py-src-variable">registerChecker</span>(<span class="py-src-variable">checker</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">9986</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">portal</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h2>Cred plugins<a name="auto12"/></h2>
+
+ <h3>Authentication with cred plugins<a name="auto13"/></h3>
+
+<p> Cred offers a plugin architecture for authentication methods. The
+primary API for this architecture is the command-line; the plugins are
+meant to be specified by the end-user when deploying a TAP (twistd
+plugin).</p>
+
+<p> For more information on writing a twistd plugin and using cred
+plugins for your application, please refer to the <a href="tap.html" shape="rect">Writing a twistd plugin</a> document.</p>
+
+ <h3>Building a cred plugin<a name="auto14"/></h3>
+
+<p> To build a plugin for cred, you should first define an <code class="python">authType</code>, a short one-word string that defines
+your plugin to the command-line. Once you have this, the convention is
+to create a file named <code>myapp_plugins.py</code> in the
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugins.html" title="twisted.plugins">twisted.plugins</a></code> module path. </p>
+
+<p> Below is an example file structure for an application that defines
+such a plugin: </p>
+
+<ul>
+<li>MyApplication/
+ <ul>
+ <li>setup.py</li>
+ <li>myapp/
+ <ul>
+ <li>__init__.py</li>
+ <li>cred.py</li>
+ <li>server.py</li>
+ </ul>
+ </li>
+ <li>twisted/
+ <ul>
+ <li>plugins/
+ <ul>
+ <li>myapp_plugins.py</li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+</li>
+</ul>
+
+<p>
+Once you have created this structure within your application, you can
+create the code for your cred plugin by building a factory class which
+implements <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.strcred.ICheckerFactory.html" title="twisted.cred.strcred.ICheckerFactory">ICheckerFactory</a></code>.
+These factory classes should not consist of a tremendous amount of
+code. Most of the real application logic should reside in the cred
+checker itself. (For help on building those, scroll up.)
+</p>
+
+<p>
+The core purpose of the CheckerFactory is to translate an <code class="python">argstring</code>, which is passed on the command line,
+into a suitable set of initialization parameters for a Checker
+class. In most cases this should be little more than constructing a
+dictionary or a tuple of arguments, then passing them along to a new
+checker instance.
+</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">plugin</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">strcred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ICheckerFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myapp</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SpecialChecker</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SpecialCheckerFactory</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ A checker factory for a specialized (fictional) API.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-comment"># The class needs to implement both of these interfaces</span>
+ <span class="py-src-comment"># for the plugin system to find our factory.</span>
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">ICheckerFactory</span>, <span class="py-src-variable">plugin</span>.<span class="py-src-variable">IPlugin</span>)
+
+ <span class="py-src-comment"># This tells AuthOptionsMixin how to find this factory.</span>
+ <span class="py-src-variable">authType</span> = <span class="py-src-string">&quot;special&quot;</span>
+
+ <span class="py-src-comment"># This is a one-line explanation of what arguments, if any,</span>
+ <span class="py-src-comment"># your particular cred plugin requires at the command-line.</span>
+ <span class="py-src-variable">argStringFormat</span> = <span class="py-src-string">&quot;A colon-separated key=value list.&quot;</span>
+
+ <span class="py-src-comment"># This help text can be multiple lines. It will be displayed</span>
+ <span class="py-src-comment"># when someone uses the &quot;--help-auth-type special&quot; command.</span>
+ <span class="py-src-variable">authHelp</span> = <span class="py-src-string">&quot;&quot;&quot;Some help text goes here ...&quot;&quot;&quot;</span>
+
+ <span class="py-src-comment"># This will be called once per command-line.</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">generateChecker</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">argstring</span>=<span class="py-src-string">&quot;&quot;</span>):
+ <span class="py-src-variable">argdict</span> = <span class="py-src-variable">dict</span>((<span class="py-src-variable">x</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'='</span>) <span class="py-src-keyword">for</span> <span class="py-src-variable">x</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">argstring</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>)))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">SpecialChecker</span>(**<span class="py-src-variable">dict</span>)
+
+<span class="py-src-comment"># We need to instantiate our class for the plugin to work.</span>
+<span class="py-src-variable">theSpecialCheckerFactory</span> = <span class="py-src-variable">SpecialCheckerFactory</span>()
+</pre>
+
+<p> For more information on how your plugin can be used in your
+application (and by other application developers), please see the <a href="tap.html" shape="rect">Writing a twistd plugin</a> document.</p>
+
+<h2>Conclusion<a name="auto15"/></h2>
+
+<p>After reading through this tutorial, you should be able to
+</p>
+<ul>
+<li>Understand how the cred architecture applies to your application</li>
+<li>Integrate your application with cred's object model</li>
+<li>Deploy an application that uses cred for authentication</li>
+<li>Allow your users to use command-line authentication plugins</li>
+</ul>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/debug-with-emacs.html b/doc/core/howto/debug-with-emacs.html
new file mode 100644
index 0000000..48a2a4d
--- /dev/null
+++ b/doc/core/howto/debug-with-emacs.html
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Debugging Python(Twisted) with Emacs</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Debugging Python(Twisted) with Emacs</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<img src="http://yellow5.com/pokey/archive/pokey411_3.gif"/>
+<a href="#footnote-1" title="POKEY THE PENGUIN IS COPYRIGHT © 1998-2002 THE AUTHORS"><super>1</super></a>
+
+<ul>
+ <li>Open up your project files. sometimes emacs can't find them if you
+ don't have them open before-hand.</li>
+
+ <li>Make sure you have a program called <code class="shell">pdb</code> somewhere
+ in your PATH, with the following contents:
+
+ <pre class="shell" xml:space="preserve">#!/bin/sh
+exec python2.3 /usr/lib/python2.3/pdb.py $1 $2 $3 $4 $5 $6 $7 $8 $9
+ </pre></li>
+
+ <li>Run <code class="shell">M-x pdb</code> in emacs. If you usually run your
+ program as <code class="shell">python foo.py</code>, your command line should be <code class="shell">pdb
+ foo.py</code>, for <code class="shell">twistd</code> and <code class="shell">trial</code> just
+ add -b to the command line, e.g.: <code class="shell">twistd -b -y my.tac</code></li>
+
+ <li>While pdb waits for your input, go to a place in your code and hit
+ <code class="shell">C-x SPC</code> to insert a break-point. pdb should say something happy.
+ Do this in as many points as you wish.</li>
+
+ <li>Go to your pdb buffer and hit <code class="shell">c</code>; this runs as normal until a
+ break-point is found.</li>
+
+ <li>Once you get to a breakpoint, use <code class="shell">s</code> to step, <code class="shell">n</code> to run the
+ current line without stepping through the functions it calls, <code class="shell">w</code>
+ to print out the current stack, <code class="shell">u</code> and <code class="shell">d</code> to go up and down a
+ level in the stack, <code class="shell">p foo</code> to print result of expression <code class="shell">foo</code>.</li>
+
+ <li>Recommendations for effective debugging:
+ <ul>
+ <li>use <code class="shell">p self</code> a lot; just knowing the class where the current code
+ is isn't enough most of the time.</li>
+ <li>use <code class="shell">w</code> to get your bearings, it'll re-display the current-line/arrow</li>
+ <li>after you use <code class="shell">w</code>, use <code class="shell">u</code> and <code class="shell">d</code> and lots more <code class="shell">p self</code> on the
+ different stack-levels.</li>
+ <li>If you've got a big code-path that you need to grok, keep another
+ buffer open and list the code-path there (e.g., I had a
+ nasty-evil Deferred recursion, and this helped me tons)</li>
+ </ul>
+ </li>
+</ul>
+
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">POKEY THE PENGUIN IS COPYRIGHT © 1998-2002
+THE AUTHORS</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/defer.html b/doc/core/howto/defer.html
new file mode 100644
index 0000000..492175b
--- /dev/null
+++ b/doc/core/howto/defer.html
@@ -0,0 +1,898 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Deferred Reference</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Deferred Reference</h1>
+ <div class="toc"><ol><li><a href="#auto0">Deferreds</a></li><li><a href="#auto1">Callbacks</a></li><ul><li><a href="#auto2">Multiple callbacks</a></li><li><a href="#auto3">Visual Explanation</a></li></ul><li><a href="#auto4">Errbacks</a></li><ul><li><a href="#auto5">Unhandled Errors</a></li></ul><li><a href="#auto6">Handling either synchronous or asynchronous results</a></li><ul><li><a href="#auto7">Handling possible Deferreds in the library code</a></li></ul><li><a href="#auto8">DeferredList</a></li><ul><li><a href="#auto9">Other behaviours</a></li><li><a href="#auto10">gatherResults</a></li></ul><li><a href="#auto11">Class Overview</a></li><ul><li><a href="#auto12">Basic Callback Functions</a></li><li><a href="#auto13">Chaining Deferreds</a></li></ul><li><a href="#auto14">See also</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<p>This document is a guide to the behaviour of the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">twisted.internet.defer.Deferred</a></code> object, and to various
+ways you can use them when they are returned by functions.</p>
+
+<p>This document assumes that you are familiar with the basic principle that
+the Twisted framework is structured around: asynchronous, callback-based
+programming, where instead of having blocking code in your program or using
+threads to run blocking code, you have functions that return immediately and
+then begin a callback chain when data is available.</p>
+
+<p>
+After reading this document, the reader should expect to be able to
+deal with most simple APIs in Twisted and Twisted-using code that
+return Deferreds.
+</p>
+
+<ul>
+<li>what sorts of things you can do when you get a Deferred from a
+function call; and</li>
+<li>how you can write your code to robustly handle errors in Deferred
+code.</li>
+</ul>
+
+<a name="deferreds" shape="rect"/>
+<h2>Deferreds<a name="auto0"/></h2>
+
+<p>Twisted uses the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> object to manage the callback
+sequence. The client application attaches a series of functions to the
+deferred to be called in order when the results of the asychronous request are
+available (this series of functions is known as a series of
+<strong>callbacks</strong>, or a <strong>callback chain</strong>), together
+with a series of functions to be called if there is an error in the
+asychronous request (known as a series of <strong>errbacks</strong> or an <strong>errback chain</strong>). The asychronous library code calls the first
+callback when the result is available, or the first errback when an error
+occurs, and the <code>Deferred</code> object then hands the results of each
+callback or errback function to the next function in the chain.</p>
+
+<h2>Callbacks<a name="auto1"/></h2>
+
+<p>A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">twisted.internet.defer.Deferred</a></code> is a promise that
+a function will at some point have a result. We can attach callback functions
+to a Deferred, and once it gets a result these callbacks will be called. In
+addition Deferreds allow the developer to register a callback for an error,
+with the default behavior of logging the error. The deferred mechanism
+standardizes the application programmer's interface with all sorts of
+blocking or delayed operations.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getDummyData</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ This function is a dummy which simulates a delayed result and
+ returns a Deferred which will fire with that result. Don't try too
+ hard to understand this.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-comment"># simulate a delayed result by asking the reactor to fire the</span>
+ <span class="py-src-comment"># Deferred in 2 seconds time with the result x * 3</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">2</span>, <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>, <span class="py-src-variable">x</span> * <span class="py-src-number">3</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printData</span>(<span class="py-src-parameter">d</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Data handling function to be added as a callback: handles the
+ data by printing the result
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-variable">d</span> = <span class="py-src-variable">getDummyData</span>(<span class="py-src-number">3</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printData</span>)
+
+<span class="py-src-comment"># manually set up the end of the process by asking the reactor to</span>
+<span class="py-src-comment"># stop itself in 4 seconds time</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">4</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>)
+<span class="py-src-comment"># start up the Twisted reactor (event loop handler) manually</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h3>Multiple callbacks<a name="auto2"/></h3>
+
+<p>Multiple callbacks can be added to a Deferred. The first callback in the
+Deferred's callback chain will be called with the result, the second with the
+result of the first callback, and so on. Why do we need this? Well, consider
+a Deferred returned by twisted.enterprise.adbapi - the result of a SQL query.
+A web widget might add a callback that converts this result into HTML, and
+pass the Deferred onwards, where the callback will be used by twisted to
+return the result to the HTTP client. The callback chain will be bypassed in
+case of errors or exceptions.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Getter</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">gotResults</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ The Deferred mechanism provides a mechanism to signal error
+ conditions. In this case, odd numbers are bad.
+
+ This function demonstrates a more complex way of starting
+ the callback chain by checking for expected results and
+ choosing whether to fire the callback or errback chain
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">x</span> % <span class="py-src-number">2</span> == <span class="py-src-number">0</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">x</span>*<span class="py-src-number">3</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">ValueError</span>(<span class="py-src-string">&quot;You used an odd number!&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_toHTML</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">r</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ This function converts r to HTML.
+
+ It is added to the callback chain by getDummyData in
+ order to demonstrate how a callback passes its own result
+ to the next callback
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Result: %s&quot;</span> % <span class="py-src-variable">r</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getDummyData</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ The Deferred mechanism allows for chained callbacks.
+ In this example, the output of gotResults is first
+ passed through _toHTML on its way to printData.
+
+ Again this function is a dummy, simulating a delayed result
+ using callLater, rather than using a real asynchronous
+ setup.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-comment"># simulate a delayed result by asking the reactor to schedule</span>
+ <span class="py-src-comment"># gotResults in 2 seconds time</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">2</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">gotResults</span>, <span class="py-src-variable">x</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_toHTML</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printData</span>(<span class="py-src-parameter">d</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printError</span>(<span class="py-src-parameter">failure</span>):
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">stderr</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">str</span>(<span class="py-src-variable">failure</span>))
+
+<span class="py-src-comment"># this series of callbacks and errbacks will print an error message</span>
+<span class="py-src-variable">g</span> = <span class="py-src-variable">Getter</span>()
+<span class="py-src-variable">d</span> = <span class="py-src-variable">g</span>.<span class="py-src-variable">getDummyData</span>(<span class="py-src-number">3</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printData</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">printError</span>)
+
+<span class="py-src-comment"># this series of callbacks and errbacks will print &quot;Result: 12&quot;</span>
+<span class="py-src-variable">g</span> = <span class="py-src-variable">Getter</span>()
+<span class="py-src-variable">d</span> = <span class="py-src-variable">g</span>.<span class="py-src-variable">getDummyData</span>(<span class="py-src-number">4</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printData</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">printError</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">4</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h3>Visual Explanation<a name="auto3"/></h3>
+
+<div align="center" hlint="off">
+<img src="../img/deferred-attach.png"/>
+</div>
+
+<ol>
+ <li>Requesting method (data sink) requests data, gets
+ Deferred object.</li>
+
+ <li>Requesting method attaches callbacks to Deferred
+ object.</li>
+</ol>
+<img align="left" hlint="off" src="../img/deferred-process.png"/>
+
+<ol>
+
+ <li>When the result is ready, give it to the Deferred
+ object. <code>.callback(result)</code> if the operation succeeded,
+ <code>.errback(failure)</code> if it failed. Note that
+ <code>failure</code> is typically an instance of a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">twisted.python.failure.Failure</a></code>
+ instance.</li>
+
+ <li>Deferred object triggers previously-added (call/err)back
+ with the <code>result</code> or <code>failure</code>.
+ Execution then follows the following rules, going down the
+ chain of callbacks to be processed.
+
+ <ul>
+ <li>Result of the callback is always passed as the first
+ argument to the next callback, creating a chain of
+ processors.</li>
+
+ <li>If a callback raises an exception, switch to
+ errback.</li>
+
+ <li>An unhandled failure gets passed down the line of
+ errbacks, this creating an asynchronous analog to a
+ series to a series of <code>except:</code>
+ statements.</li>
+
+ <li>If an errback doesn't raise an exception or return a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">twisted.python.failure.Failure</a></code>
+ instance, switch to callback.</li>
+ </ul> </li>
+</ol>
+<br clear="all" hlint="off"/>
+
+<h2>Errbacks<a name="auto4"/></h2>
+
+<p>Deferred's error handling is modeled after Python's
+exception handling. In the case that no errors occur, all the
+callbacks run, one after the other, as described above.</p>
+
+<p>If the errback is called instead of the callback (e.g. because a DB query
+raised an error), then a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">twisted.python.failure.Failure</a></code> is passed into the first
+errback (you can add multiple errbacks, just like with callbacks). You can
+think of your errbacks as being like <code class="python">except</code> blocks
+of ordinary Python code.</p>
+
+<p>Unless you explicitly <code class="python">raise</code> an error in except
+block, the <code class="python">Exception</code> is caught and stops
+propagating, and normal execution continues. The same thing happens with
+errbacks: unless you explicitly <code class="python">return</code> a <code class="python">Failure</code> or (re-)raise an exception, the error stops
+propagating, and normal callbacks continue executing from that point (using the
+value returned from the errback). If the errback does returns a <code class="python">Failure</code> or raise an exception, then that is passed to the
+next errback, and so on.</p>
+
+<p><em>Note:</em> If an errback doesn't return anything, then it effectively
+returns <code class="python">None</code>, meaning that callbacks will continue
+to be executed after this errback. This may not be what you expect to happen,
+so be careful. Make sure your errbacks return a <code class="python">Failure</code> (probably the one that was passed to it), or a
+meaningful return value for the next callback.</p>
+
+<p>Also, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">twisted.python.failure.Failure</a></code> instances have
+a useful method called trap, allowing you to effectively do the equivalent
+of:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">try</span>:
+ <span class="py-src-comment"># code that may throw an exception</span>
+ <span class="py-src-variable">cookSpamAndEggs</span>()
+<span class="py-src-keyword">except</span> (<span class="py-src-variable">SpamException</span>, <span class="py-src-variable">EggException</span>):
+ <span class="py-src-comment"># Handle SpamExceptions and EggExceptions</span>
+ ...
+</pre>
+
+<p>You do this by:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">errorHandler</span>(<span class="py-src-parameter">failure</span>):
+ <span class="py-src-variable">failure</span>.<span class="py-src-variable">trap</span>(<span class="py-src-variable">SpamException</span>, <span class="py-src-variable">EggException</span>)
+ <span class="py-src-comment"># Handle SpamExceptions and EggExceptions</span>
+
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cookSpamAndEggs</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">errorHandler</span>)
+</pre>
+
+<p>If none of arguments passed to <code class="python">failure.trap</code>
+match the error encapsulated in that <code class="python">Failure</code>, then
+it re-raises the error.</p>
+
+<p>There's another potential <q>gotcha</q> here. There's a
+method <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.addCallbacks.html" title="twisted.internet.defer.Deferred.addCallbacks">twisted.internet.defer.Deferred.addCallbacks</a></code>
+which is similar to, but not exactly the same as, <code class="python">addCallback</code> followed by <code class="python">addErrback</code>. In particular, consider these two cases:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-comment"># Case 1</span>
+<span class="py-src-variable">d</span> = <span class="py-src-variable">getDeferredFromSomewhere</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">callback1</span>) <span class="py-src-comment"># A</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">errback1</span>) <span class="py-src-comment"># B</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">callback2</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">errback2</span>)
+
+<span class="py-src-comment"># Case 2</span>
+<span class="py-src-variable">d</span> = <span class="py-src-variable">getDeferredFromSomewhere</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">callback1</span>, <span class="py-src-variable">errback1</span>) <span class="py-src-comment"># C</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">callback2</span>, <span class="py-src-variable">errback2</span>)
+</pre>
+
+<p>If an error occurs in <code class="python">callback1</code>, then for Case 1
+<code class="python">errback1</code> will be called with the failure. For Case
+2, <code class="python">errback2</code> will be called. Be careful with your
+callbacks and errbacks.</p>
+
+<p>What this means in a practical sense is in Case 1, the callback in line
+A will handle a success condition from <code>getDeferredFromSomewhere</code>,
+and the errback in line B will handle any errors that occur <em>from either the
+upstream source, or that occur in A</em>. In Case 2, the errback in line C <em>will
+only handle an error condition raised by</em> <code>getDeferredFromSomewhere</code>,
+it will not do any handling of errors
+raised in <code>callback1</code>.</p>
+
+
+<h3>Unhandled Errors<a name="auto5"/></h3>
+
+<p>If a Deferred is garbage-collected with an unhandled error (i.e. it would
+call the next errback if there was one), then Twisted will write the error's
+traceback to the log file. This means that you can typically get away with not
+adding errbacks and still get errors logged. Be careful though; if you keep a
+reference to the Deferred around, preventing it from being garbage-collected,
+then you may never see the error (and your callbacks will mysteriously seem to
+have never been called). If unsure, you should explicitly add an errback after
+your callbacks, even if all you do is:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-comment"># Make sure errors get logged</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>)
+</pre>
+
+<h2>Handling either synchronous or asynchronous results<a name="auto6"/></h2>
+<p>
+In some applications, there are functions that might be either asynchronous or
+synchronous. For example, a user authentication function might be able to
+check in memory whether a user is authenticated, allowing the authentication
+function to return an immediate result, or it may need to wait on
+network data, in which case it should return a Deferred to be fired
+when that data arrives. However, a function that wants to check if a user is
+authenticated will then need to accept both immediate results <em> and</em>
+Deferreds.
+</p>
+
+<p>
+In this example, the library function <code>authenticateUser</code> uses the
+application function <code>isValidUser</code> to authenticate a user:
+</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">authenticateUser</span>(<span class="py-src-parameter">isValidUser</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">isValidUser</span>(<span class="py-src-variable">user</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;User is authenticated&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;User is not authenticated&quot;</span>
+</pre>
+
+<p>
+However, it assumes that <code>isValidUser</code> returns immediately,
+whereas <code>isValidUser</code> may actually authenticate the user
+asynchronously and return a Deferred. It is possible to adapt this
+trivial user authentication code to accept either a
+synchronous <code>isValidUser</code> or an
+asynchronous <code>isValidUser</code>, allowing the library to handle
+either type of function. It is, however, also possible to adapt
+synchronous functions to return Deferreds. This section describes both
+alternatives: handling functions that might be synchronous or
+asynchronous in the library function (<code>authenticateUser</code>)
+or in the application code.
+</p>
+
+<h3>Handling possible Deferreds in the library code<a name="auto7"/></h3>
+
+<p>
+Here is an example of a synchronous user authentication function that might be
+passed to <code>authenticateUser</code>:
+</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">synchronousIsValidUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">'''
+ Return true if user is a valid user, false otherwise
+ '''</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">&quot;Alice&quot;</span>, <span class="py-src-string">&quot;Angus&quot;</span>, <span class="py-src-string">&quot;Agnes&quot;</span>]
+</pre><div class="caption">Source listing - <a href="listings/deferred/synch-validation.py"><span class="filename">listings/deferred/synch-validation.py</span></a></div></div>
+
+<p>
+However, here's an <code>asynchronousIsValidUser</code> function that returns
+a Deferred:
+</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">asynchronousIsValidUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">2</span>, <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>, <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">&quot;Alice&quot;</span>, <span class="py-src-string">&quot;Angus&quot;</span>, <span class="py-src-string">&quot;Agnes&quot;</span>])
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+</pre>
+
+<p> Our original implementation of <code>authenticateUser</code> expected <code>isValidUser</code> to be synchronous, but now we need to change it to handle both
+synchronous and asynchronous implementations of <code>isValidUser</code>. For this, we
+use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.maybeDeferred.html" title="twisted.internet.defer.maybeDeferred">maybeDeferred</a></code> to
+call <code>isValidUser</code>, ensuring that the result of <code>isValidUser</code> is a Deferred,
+even if <code>isValidUser</code> is a synchronous function:
+</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">result</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;User is authenticated&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;User is not authenticated&quot;</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">authenticateUser</span>(<span class="py-src-parameter">isValidUser</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">maybeDeferred</span>(<span class="py-src-variable">isValidUser</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+</pre>
+
+<p>
+Now <code>isValidUser</code> could be either <code>synchronousIsValidUser</code> or <code>asynchronousIsValidUser</code>.
+</p>
+
+<p>It is also possible to modify <code>synchronousIsValidUser</code> to return
+a Deferred, see <a href="gendefer.html" shape="rect">Generating Deferreds</a> for more
+information.</p>
+
+<a name="deferredlist" shape="rect"/>
+<h2>DeferredList<a name="auto8"/></h2>
+
+<p>Sometimes you want to be notified after several different events have all
+happened, rather than waiting for each one individually. For example, you may
+want to wait for all the connections in a list to close. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.DeferredList.html" title="twisted.internet.defer.DeferredList">twisted.internet.defer.DeferredList</a></code> is the way to do
+this.</p>
+
+<p>To create a DeferredList from multiple Deferreds, you simply pass a list of
+the Deferreds you want it to wait for:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-comment"># Creates a DeferredList</span>
+<span class="py-src-variable">dl</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">DeferredList</span>([<span class="py-src-variable">deferred1</span>, <span class="py-src-variable">deferred2</span>, <span class="py-src-variable">deferred3</span>])
+</pre>
+
+<p>You can now treat the DeferredList like an ordinary Deferred; you can call <code>addCallbacks</code> and so on. The DeferredList will call its callback
+when all the deferreds have completed. The callback will be called with a list
+of the results of the Deferreds it contains, like so:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-comment"># A callback that unpacks and prints the results of a DeferredList</span>
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">for</span> (<span class="py-src-variable">success</span>, <span class="py-src-variable">value</span>) <span class="py-src-keyword">in</span> <span class="py-src-variable">result</span>:
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">success</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Success:'</span>, <span class="py-src-variable">value</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Failure:'</span>, <span class="py-src-variable">value</span>.<span class="py-src-variable">getErrorMessage</span>()
+
+<span class="py-src-comment"># Create three deferreds.</span>
+<span class="py-src-variable">deferred1</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">deferred2</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">deferred3</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+
+<span class="py-src-comment"># Pack them into a DeferredList</span>
+<span class="py-src-variable">dl</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">DeferredList</span>([<span class="py-src-variable">deferred1</span>, <span class="py-src-variable">deferred2</span>, <span class="py-src-variable">deferred3</span>], <span class="py-src-variable">consumeErrors</span>=<span class="py-src-variable">True</span>)
+
+<span class="py-src-comment"># Add our callback</span>
+<span class="py-src-variable">dl</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+
+<span class="py-src-comment"># Fire our three deferreds with various values.</span>
+<span class="py-src-variable">deferred1</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">'one'</span>)
+<span class="py-src-variable">deferred2</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">Exception</span>(<span class="py-src-string">'bang!'</span>))
+<span class="py-src-variable">deferred3</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">'three'</span>)
+
+<span class="py-src-comment"># At this point, dl will fire its callback, printing:</span>
+<span class="py-src-comment"># Success: one</span>
+<span class="py-src-comment"># Failure: bang!</span>
+<span class="py-src-comment"># Success: three</span>
+<span class="py-src-comment"># (note that defer.SUCCESS == True, and defer.FAILURE == False)</span>
+</pre>
+
+<p>A standard DeferredList will never call errback, but failures in Deferreds
+passed to a DeferredList will still errback unless <code>consumeErrors</code>
+is passed <code>True</code>. See below for more details about this and other
+flags which modify the behavior of DeferredList.</p>
+
+<div class="note"><strong>Note: </strong>
+<p>If you want to apply callbacks to the individual Deferreds that
+go into the DeferredList, you should be careful about when those callbacks
+are added. The act of adding a Deferred to a DeferredList inserts a callback
+into that Deferred (when that callback is run, it checks to see if the
+DeferredList has been completed yet). The important thing to remember is
+that it is <em>this callback</em> which records the value that goes into the
+result list handed to the DeferredList's callback.</p>
+
+
+
+<p>Therefore, if you add a callback to the Deferred <em>after</em> adding the
+Deferred to the DeferredList, the value returned by that callback will not
+be given to the DeferredList's callback. To avoid confusion, we recommend not
+adding callbacks to a Deferred once it has been used in a DeferredList.</p>
+</div>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">addTen</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">result</span> + <span class="py-src-string">&quot; ten&quot;</span>
+
+<span class="py-src-comment"># Deferred gets callback before DeferredList is created</span>
+<span class="py-src-variable">deferred1</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">deferred2</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">deferred1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">addTen</span>)
+<span class="py-src-variable">dl</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">DeferredList</span>([<span class="py-src-variable">deferred1</span>, <span class="py-src-variable">deferred2</span>])
+<span class="py-src-variable">dl</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+<span class="py-src-variable">deferred1</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;one&quot;</span>) <span class="py-src-comment"># fires addTen, checks DeferredList, stores &quot;one ten&quot;</span>
+<span class="py-src-variable">deferred2</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;two&quot;</span>)
+<span class="py-src-comment"># At this point, dl will fire its callback, printing:</span>
+<span class="py-src-comment"># [(1, 'one ten'), (1, 'two')]</span>
+
+<span class="py-src-comment"># Deferred gets callback after DeferredList is created</span>
+<span class="py-src-variable">deferred1</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">deferred2</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">dl</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">DeferredList</span>([<span class="py-src-variable">deferred1</span>, <span class="py-src-variable">deferred2</span>])
+<span class="py-src-variable">deferred1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">addTen</span>) <span class="py-src-comment"># will run *after* DeferredList gets its value</span>
+<span class="py-src-variable">dl</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+<span class="py-src-variable">deferred1</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;one&quot;</span>) <span class="py-src-comment"># checks DeferredList, stores &quot;one&quot;, fires addTen</span>
+<span class="py-src-variable">deferred2</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;two&quot;</span>)
+<span class="py-src-comment"># At this point, dl will fire its callback, printing:</span>
+<span class="py-src-comment"># [(1, 'one), (1, 'two')]</span>
+</pre>
+
+<h3>Other behaviours<a name="auto9"/></h3>
+
+<p>DeferredList accepts three keyword arguments that modify its behaviour:
+<code>fireOnOneCallback</code>, <code>fireOnOneErrback</code> and
+<code>consumeErrors</code>. If <code>fireOnOneCallback</code> is set, the
+DeferredList will immediately call its callback as soon as any of its Deferreds
+call their callback. Similarly, <code>fireOnOneErrback</code> will call errback
+as soon as any of the Deferreds call their errback. Note that DeferredList is
+still one-shot, like ordinary Deferreds, so after a callback or errback has been
+called the DeferredList will do nothing further (it will just silently ignore
+any other results from its Deferreds).</p>
+
+<p>The <code>fireOnOneErrback</code> option is particularly useful when you
+want to wait for all the results if everything succeeds, but also want to know
+immediately if something fails.</p>
+
+<p>The <code>consumeErrors</code> argument will stop the DeferredList from
+propagating any errors along the callback chains of any Deferreds it contains
+(usually creating a DeferredList has no effect on the results passed along the
+callbacks and errbacks of their Deferreds). Stopping errors at the DeferredList
+with this option will prevent <q>Unhandled error in Deferred</q> warnings from
+the Deferreds it contains without needing to add extra errbacks<a href="#footnote-1" title="Unless of course a later callback starts a fresh error — but as we've already noted, adding callbacks to a Deferred after its used in a DeferredList is confusing and usually avoided."><super>1</super></a>. Passing a true value
+for the <code>consumeErrors</code> parameter will not change the behavior of <code>fireOnOneCallback</code> or <code>fireOnOneErrback</code>.</p>
+
+<h3>gatherResults<a name="auto10"/></h3>
+
+<p>A common use for DeferredList is to &quot;join&quot; a number of parallel asynchronous
+operations, finishing successfully if all of the operations were successful, or
+failing if any one of the operations fails. In this case, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.gatherResults.html" title="twisted.internet.defer.gatherResults">twisted.internet.defer.gatherResults</a></code> is a useful
+shortcut:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+<span class="py-src-variable">d1</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">d2</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+<span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">gatherResults</span>([<span class="py-src-variable">d1</span>, <span class="py-src-variable">d2</span>], <span class="py-src-variable">consumeErrors</span>=<span class="py-src-variable">True</span>)
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+<span class="py-src-variable">d1</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;one&quot;</span>)
+<span class="py-src-comment"># nothing is printed yet; d is still awaiting completion of d2</span>
+<span class="py-src-variable">d2</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;two&quot;</span>)
+<span class="py-src-comment"># printResult prints [&quot;one&quot;, &quot;two&quot;]</span>
+</pre>
+
+<p>The <code>consumeErrors</code> argument has the same meaning as it does
+for <a href="#deferredlist" shape="rect"><code>DeferredList</code></a>: if true, it causes
+<code>gatherResults</code> to consume any errors in the passed-in Deferreds.
+Always use this argument unless you are adding further callbacks or errbacks to
+the passed-in Deferreds, or unless you know that they will not fail.
+Otherwise, a failure will result in an unhandled error being logged by Twisted.
+This argument is available since Twisted 11.1.0.</p>
+
+<a name="class" shape="rect"/>
+
+<h2>Class Overview<a name="auto11"/></h2>
+
+<p>This is an overview API reference for Deferred from the point of using a
+Deferred returned by a function. It is not meant to be a
+substitute for the docstrings in the Deferred class, but can provide guidelines
+for its use.</p>
+
+<p>There is a parallel overview of functions used by the Deferred's <em>creator</em> in <a href="gendefer.html#class" shape="rect">Generating Deferreds</a>.</p>
+
+<h3>Basic Callback Functions<a name="auto12"/></h3>
+
+<ul>
+ <li>
+ <code class="py-prototype">addCallbacks(self, callback[, errback, callbackArgs,
+ callbackKeywords, errbackArgs, errbackKeywords])</code>
+
+ <p>This is the method you will use to interact
+ with Deferred. It adds a pair of callbacks <q>parallel</q> to
+ each other (see diagram above) in the list of callbacks
+ made when the Deferred is called back to. The signature of
+ a method added using addCallbacks should be
+ <code>myMethod(result, *methodArgs,
+ **methodKeywords)</code>. If your method is passed in the
+ callback slot, for example, all arguments in the tuple
+ <code>callbackArgs</code> will be passed as
+ <code>*methodArgs</code> to your method.</p>
+
+ <p>There are various convenience methods that are
+ derivative of addCallbacks. I will not cover them in detail
+ here, but it is important to know about them in order to
+ create concise code.</p>
+
+ <ul>
+ <li>
+ <code class="py-prototype">addCallback(callback, *callbackArgs,
+ **callbackKeywords)</code>
+
+ <p>Adds your callback at the next point in the
+ processing chain, while adding an errback that will
+ re-raise its first argument, not affecting further
+ processing in the error case.</p>
+
+ <p>Note that, while addCallbacks (plural) requires the arguments to be
+ passed in a tuple, addCallback (singular) takes all its remaining
+ arguments as things to be passed to the callback function. The reason is
+ obvious: addCallbacks (plural) cannot tell whether the arguments are
+ meant for the callback or the errback, so they must be specifically
+ marked by putting them into a tuple. addCallback (singular) knows that
+ everything is destined to go to the callback, so it can use Python's
+ <q>*</q> and <q>**</q> syntax to collect the remaining arguments.</p>
+
+ </li>
+
+ <li>
+ <code class="py-prototype">addErrback(errback, *errbackArgs,
+ **errbackKeywords)</code>
+
+ <p>Adds your errback at the next point in the
+ processing chain, while adding a callback that will
+ return its first argument, not affecting further
+ processing in the success case.</p>
+ </li>
+
+ <li>
+ <code class="py-prototype">addBoth(callbackOrErrback,
+ *callbackOrErrbackArgs,
+ **callbackOrErrbackKeywords)</code>
+
+ <p>This method adds the same callback into both sides
+ of the processing chain at both points. Keep in mind
+ that the type of the first argument is indeterminate if
+ you use this method! Use it for <code>finally:</code>
+ style blocks.</p>
+ </li>
+ </ul> </li>
+
+</ul>
+
+
+<h3>Chaining Deferreds<a name="auto13"/></h3>
+
+<p>If you need one Deferred to wait on another, all you need to do is return a
+Deferred from a method added to addCallbacks. Specifically, if you return
+Deferred B from a method added to Deferred A using A.addCallbacks, Deferred A's
+processing chain will stop until Deferred B's .callback() method is called; at
+that point, the next callback in A will be passed the result of the last
+callback in Deferred B's processing chain at the time.</p>
+
+<p>If this seems confusing, don't worry about it right now -- when you run into
+a situation where you need this behavior, you will probably recognize it
+immediately and realize why this happens. If you want to chain deferreds
+manually, there is also a convenience method to help you.</p>
+
+<ul>
+ <li>
+ <code class="py-prototype">chainDeferred(otherDeferred)</code>
+
+ <p>Add <code>otherDeferred</code> to the end of this
+ Deferred's processing chain. When self.callback is called,
+ the result of my processing chain up to this point will be
+ passed to <code>otherDeferred.callback</code>. Further
+ additions to my callback chain do not affect
+ <code>otherDeferred</code></p>
+ <p>This is the same as <code class="python">self.addCallbacks(otherDeferred.callback,
+ otherDeferred.errback)</code></p>
+ </li>
+</ul>
+
+<h2>See also<a name="auto14"/></h2>
+
+<ol>
+<li><a href="gendefer.html" shape="rect">Generating Deferreds</a>, an introduction to
+writing asynchronous functions that return Deferreds.</li>
+</ol>
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">Unless of course a later callback starts a fresh error —
+but as we've already noted, adding callbacks to a Deferred after its used in a
+DeferredList is confusing and usually avoided.</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/design.html b/doc/core/howto/design.html
new file mode 100644
index 0000000..49e292c
--- /dev/null
+++ b/doc/core/howto/design.html
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Designing Twisted Applications</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Designing Twisted Applications</h1>
+ <div class="toc"><ol><li><a href="#auto0">Goals</a></li><li><a href="#auto1">Example of a modular design: TwistedQuotes</a></li><ul><li><a href="#auto2">Set up the project directory</a></li><li><a href="#auto3">A Look at the Heart of the Application</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Goals<a name="auto0"/></h2>
+
+<p>This document describes how a good Twisted application is structured. It
+should be useful for beginning Twisted developers who want to structure their
+code in a clean, maintainable way that reflects current best practices.</p>
+
+<p>Readers will want to be familiar with writing <a href="servers.html" shape="rect">servers</a> and <a href="clients.html" shape="rect">clients</a> using Twisted.</p>
+
+<h2>Example of a modular design: TwistedQuotes<a name="auto1"/></h2>
+
+<p><code>TwistedQuotes</code> is a very simple plugin which is a great
+demonstration of
+Twisted's power. It will export a small kernel of functionality -- Quote of
+the Day -- which can be accessed through every interface that Twisted supports:
+web pages, e-mail, instant messaging, a specific Quote of the Day protocol, and
+more.</p>
+
+<h3>Set up the project directory<a name="auto2"/></h3>
+
+<p>See the description of <a href="quotes.html" shape="rect">setting up the TwistedQuotes
+example</a>.</p>
+
+<h3>A Look at the Heart of the Application<a name="auto3"/></h3>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">random</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">choice</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">TwistedQuotes</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">quoteproto</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">StaticQuoter</span>:
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a static quote.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">quoteproto</span>.<span class="py-src-variable">IQuoter</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">quote</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">quote</span> = <span class="py-src-variable">quote</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">quote</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FortuneQuoter</span>:
+ <span class="py-src-string">&quot;&quot;&quot;
+ Load quotes from a fortune-format file.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">quoteproto</span>.<span class="py-src-variable">IQuoter</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filenames</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filenames</span> = <span class="py-src-variable">filenames</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">quoteFile</span> = <span class="py-src-variable">file</span>(<span class="py-src-variable">choice</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filenames</span>))
+ <span class="py-src-variable">quotes</span> = <span class="py-src-variable">quoteFile</span>.<span class="py-src-variable">read</span>().<span class="py-src-variable">split</span>(<span class="py-src-string">'\n%\n'</span>)
+ <span class="py-src-variable">quoteFile</span>.<span class="py-src-variable">close</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">choice</span>(<span class="py-src-variable">quotes</span>)
+</pre><div class="caption">Twisted Quotes
+Central Abstraction - <a href="listings/TwistedQuotes/quoters.py"><span class="filename">listings/TwistedQuotes/quoters.py</span></a></div></div>
+
+<p>This code listing shows us what the Twisted Quotes system is all about. The
+code doesn't have any way of talking to the outside world, but it provides a
+library which is a clear and uncluttered abstraction: <q>give me the quote of
+the day</q>. </p>
+
+<p>Note that this module does not import any Twisted functionality at all! The
+reason for doing things this way is integration. If your <q>business
+objects</q> are not stuck to your user interface, you can make a module that
+can integrate those objects with different protocols, GUIs, and file formats.
+Having such classes provides a way to decouple your components from each other,
+by allowing each to be used independently.</p>
+
+<p>In this manner, Twisted itself has minimal impact on the logic of your
+program. Although the Twisted <q>dot products</q> are highly interoperable,
+they
+also follow this approach. You can use them independently because they are not
+stuck to each other. They communicate in well-defined ways, and only when that
+communication provides some additional feature. Thus, you can use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.html" title="twisted.web">twisted.web</a></code> with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.html" title="twisted.enterprise">twisted.enterprise</a></code>, but neither requires the other, because
+they are integrated around the concept of <a href="defer.html" shape="rect">Deferreds</a>.</p>
+
+<p>Your Twisted applications should follow this style as much as possible.
+Have (at least) one module which implements your specific functionality,
+independent of any user-interface code. </p>
+
+<p>Next, we're going to need to associate this abstract logic with some way of
+displaying it to the user. We'll do this by writing a Twisted server protocol,
+which will respond to the clients that connect to it by sending a quote to the
+client and then closing the connection. Note: don't get too focused on the
+details of this -- different ways to interface with the user are 90% of what
+Twisted does, and there are lots of documents describing the different ways to
+do it.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IQuoter</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An object that returns quotes.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a quote.
+ &quot;&quot;&quot;</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTD</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">quoter</span>.<span class="py-src-variable">getQuote</span>()+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTDFactory</span>(<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ A factory for the Quote of the Day protocol.
+
+ @type quoter: L{IQuoter} provider
+ @ivar quoter: An object which provides L{IQuoter} which will be used by
+ the L{QOTD} protocol to get quotes to emit.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">QOTD</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">quoter</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">quoter</span> = <span class="py-src-variable">quoter</span>
+</pre><div class="caption">Twisted
+Quotes Protocol Implementation - <a href="listings/TwistedQuotes/quoteproto.py"><span class="filename">listings/TwistedQuotes/quoteproto.py</span></a></div></div>
+
+<p>This is a very straightforward <code>Protocol</code> implementation, and the
+pattern described above is repeated here. The Protocol contains essentially no
+logic of its own, just enough to tie together an object which can generate
+quotes (a <code class="python">Quoter</code>) and an object which can relay
+bytes to a TCP connection (a <code class="python">Transport</code>). When a
+client connects to this server, a <code class="python">QOTD</code> instance is
+created, and its <code class="python">connectionMade</code> method is called.
+</p>
+
+<p> The <code class="python">QOTDFactory</code>'s role is to specify to the
+Twisted framework how to create a <code class="python">Protocol</code> instance
+that will handle the connection. Twisted will not instantiate a <code class="python">QOTDFactory</code>; you will do that yourself later, in a <code class="shell">twistd</code> plug-in.
+</p>
+
+<p>Note: you can read more specifics of <code class="python">Protocol</code> and <code class="python">Factory</code> in the <a href="servers.html" shape="rect">Writing
+Servers</a> HOWTO.</p>
+
+<p>Once we have an abstraction -- a <code>Quoter</code> -- and we have a
+mechanism to connect it to the network -- the <code>QOTD</code> protocol -- the
+next thing to do is to put the last link in the chain of functionality between
+abstraction and user. This last link will allow a user to choose a <code>Quoter</code> and configure the protocol. Writing this configuration is
+covered in the <a href="application.html" shape="rect">Application HOWTO</a>.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/dirdbm.html b/doc/core/howto/dirdbm.html
new file mode 100644
index 0000000..f2b86b2
--- /dev/null
+++ b/doc/core/howto/dirdbm.html
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: DirDBM: Directory-based Storage</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">DirDBM: Directory-based Storage</h1>
+ <div class="toc"><ol><li><a href="#auto0">dirdbm.DirDBM</a></li><li><a href="#auto1">dirdbm.Shelf</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>dirdbm.DirDBM<a name="auto0"/></h2>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.persisted.dirdbm.DirDBM.html" title="twisted.persisted.dirdbm.DirDBM">twisted.persisted.dirdbm.DirDBM</a></code> is a DBM-like storage system.
+That is, it stores mappings between keys
+and values, like a Python dictionary, except that it stores the values in files
+in a directory - each entry is a different file. The keys must always be strings,
+as are the values. Other than that, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.persisted.dirdbm.DirDBM.html" title="twisted.persisted.dirdbm.DirDBM">DirDBM</a></code>
+objects act just like Python dictionaries.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.persisted.dirdbm.DirDBM.html" title="twisted.persisted.dirdbm.DirDBM">DirDBM</a></code> is useful for cases
+when you want to store small amounts of data in an organized fashion, without having
+to deal with the complexity of a RDBMS or other sophisticated database. It is simple,
+easy to use, cross-platform, and doesn't require any external C libraries, unlike
+Python's built-in DBM modules.</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; from twisted.persisted import dirdbm
+&gt;&gt;&gt; d = dirdbm.DirDBM(&quot;/tmp/dir&quot;)
+&gt;&gt;&gt; d[&quot;librarian&quot;] = &quot;ook&quot;
+&gt;&gt;&gt; d[&quot;librarian&quot;]
+'ook'
+&gt;&gt;&gt; d.keys()
+['librarian']
+&gt;&gt;&gt; del d[&quot;librarian&quot;]
+&gt;&gt;&gt; d.items()
+[]
+</pre>
+
+<h2>dirdbm.Shelf<a name="auto1"/></h2>
+
+<p>Sometimes it is neccessary to persist more complicated objects than strings.
+With some care, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.persisted.dirdbm.Shelf.html" title="twisted.persisted.dirdbm.Shelf">dirdbm.Shelf</a></code>
+can transparently persist
+them. <code>Shelf</code> works exactly like <code>DirDBM</code>, except that
+the values (but not the keys) can be arbitrary picklable objects. However,
+notice that mutating an object after it has been stored in the <code>Shelf</code> has no effect on the Shelf.
+When mutating objects, it is neccessary to explictly store them back in the <code>Shelf</code>
+afterwards:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; from twisted.persisted import dirdbm
+&gt;&gt;&gt; d = dirdbm.Shelf(&quot;/tmp/dir2&quot;)
+&gt;&gt;&gt; d[&quot;key&quot;] = [1, 2]
+&gt;&gt;&gt; d[&quot;key&quot;]
+[1, 2]
+&gt;&gt;&gt; l = d[&quot;key&quot;]
+&gt;&gt;&gt; l.append(3)
+&gt;&gt;&gt; d[&quot;key&quot;]
+[1, 2]
+&gt;&gt;&gt; d[&quot;key&quot;] = l
+&gt;&gt;&gt; d[&quot;key&quot;]
+[1, 2, 3]
+</pre>
+
+
+
+
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/endpoints.html b/doc/core/howto/endpoints.html
new file mode 100644
index 0000000..333a3fd
--- /dev/null
+++ b/doc/core/howto/endpoints.html
@@ -0,0 +1,231 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Getting Connected with Endpoints</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Getting Connected with Endpoints</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Constructing and Using Endpoints</a></li><ul><li><a href="#auto2">There's Not Much To It</a></li><li><a href="#auto3">Servers and Stopping</a></li><li><a href="#auto4">Clients and Cancelling</a></li></ul><li><a href="#auto5">Maximizing the Return on your Endpoint Investment</a></li><ul><li><a href="#auto6">Endpoints Aren't Always the Answer</a></li></ul><li><a href="#auto7">Endpoint Types Included With Twisted</a></li><ul><li><a href="#auto8">Clients</a></li><li><a href="#auto9">Servers</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p>On a network, one can think of any given connection as a long wire,
+stretched between two points. Lots of stuff can happen along the length of
+that wire - routers, switches, network address translation, and so on, but
+that is usually invisible to the application passing data across it.
+Twisted strives to make the nature of the &quot;wire&quot; as transparent as
+possible, with highly abstract interfaces for passing and receiving data,
+such as <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITransport.html" title="twisted.internet.interfaces.ITransport">ITransport</a></code>
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocol.html" title="twisted.internet.interfaces.IProtocol">IProtocol</a></code>.</p>
+
+<p>However, the application can't be completely ignorant of the wire.
+In particular, it must do something to <em>start</em> the connection, and
+to do so, it must identify the <em>end points</em> of the wire. There are
+different names for the roles of each end point - &quot;initiator&quot; and
+&quot;responder&quot;, &quot;connector&quot; and &quot;listener&quot;, or &quot;client&quot; and &quot;server&quot; - but the
+common theme is that one side of the connection waits around for someone to
+connect to it, and the other side does the connecting.</p>
+
+<p>In Twisted 10.1, several new interfaces were introduced to describe
+each of these roles for stream-oriented connections: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IStreamServerEndpoint.html" title="twisted.internet.interfaces.IStreamServerEndpoint">IStreamServerEndpoint</a></code> and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IStreamClientEndpoint.html" title="twisted.internet.interfaces.IStreamClientEndpoint">IStreamClientEndpoint</a></code>.
+The word &quot;stream&quot;, in this case, refers to endpoints which treat a
+connection as a continuous stream of bytes, rather than a sequence of
+discrete datagrams: TCP is a &quot;stream&quot; protocol whereas UDP is a &quot;datagram&quot;
+protocol.</p>
+
+<h2>Constructing and Using Endpoints<a name="auto1"/></h2>
+
+<p>In both <a href="servers.html" shape="rect">Writing Servers</a> and <a href="clients.html" shape="rect">Writing Clients</a>, we covered basic usage of
+endpoints; you construct an appropriate type of server or client endpoint,
+and then call <code>listen</code> (for servers) or <code>connect</code>
+(for clients).</p>
+
+<p>In both of those tutorials, we constructed specific types of
+endpoints directly. However, in most programs, you will want to allow the
+user to specify where to listen or connect, in a way which will allow the
+user to request different strategies, without having to adjust your
+program. In order to allow this, you should use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.clientFromString.html" title="twisted.internet.endpoints.clientFromString">clientFromString</a></code> or <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.serverFromString.html" title="twisted.internet.endpoints.serverFromString">serverFromString</a></code>.</p>
+
+<h3>There's Not Much To It<a name="auto2"/></h3>
+
+<p>Each type of endpoint is just an interface with a single method that
+takes an argument. <code>serverEndpoint.listen(factory)</code> will start
+listening on that endpoint with your protocol factory, and
+<code>clientEndpoint.connect(factory)</code> will start a single connection
+attempt. Each of these APIs returns a value, though, which can be important.
+</p>
+
+<p>However, if you are not already, you <em>should</em> be very
+familiar with <a href="defer.html" shape="rect">Deferreds</a>, as they are returned by
+both <code>connect</code> and <code>listen</code> methods, to indicate when
+the connection has connected or the listening port is up and running.</p>
+
+<h3>Servers and Stopping<a name="auto3"/></h3>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IStreamServerEndpoint.listen.html" title="twisted.internet.interfaces.IStreamServerEndpoint.listen">IStreamServerEndpoint.listen</a></code>
+returns a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>
+that fires with an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IListeningPort.html" title="twisted.internet.interfaces.IListeningPort">IListeningPort</a></code>.
+Note that this deferred may errback. The most common cause of such an error
+would be that another program is already using the requested port number,
+but the exact cause may vary depending on what type of endpoint you are
+listening on. If you receive such an error, it means that your application
+is not actually listening, and will not receive any incoming connections.
+It's important to somehow alert an administrator of your server, in this
+case, especially if you only have one listening port!</p>
+
+<p>Note also that once this has succeeded, it will continue listening
+forever. If you need to <em>stop</em> listening for some reason, in
+response to anything other than a full server shutdown (<code>reactor.stop</code>
+and / or <code>twistd</code> will usually handle that case for you), make
+sure you keep a reference around to that listening port object so you can
+call <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IListeningPort.stopListening.html" title="twisted.internet.interfaces.IListeningPort.stopListening">IListeningPort.stopListening</a></code>
+on it. Finally, keep in mind that <code>stopListening</code> itself returns
+a <code>Deferred</code>, and the port may not have fully stopped listening
+until that <code>Deferred</code> has fired.</p>
+
+<p>Most server applications will not need to worry about these details.
+One example of a case where you would need to be concerned with all of
+these events would be an implementation of a protocol like non-<code>PASV</code>
+FTP, where new listening ports need to be bound for the lifetime of a
+particular action, then disposed of.</p>
+
+<h3>Clients and Cancelling<a name="auto4"/></h3>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IStreamClientEndpoint.connect.html" title="twisted.internet.interfaces.IStreamClientEndpoint.connect">IStreamClientEndpoint.connect</a></code>
+will connect your protocol factory to a new outgoing connection attempt. It
+returns a <code>Deferred</code> which fires with the <code>IProtocol</code>
+returned from the factory's <code>buildProtocol</code> method.</p>
+
+<p>Connection attempts may fail, and so that <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> may also errback. If it does so,
+you will have to try again; your protocol won't be constructed, and no further
+attempts will be made.</p>
+
+<p>Connection attempts may also take a long time, and your users may
+become bored and wander off. If this happens, and your code decides, for
+whatever reason, that you've been waiting for the connection too long, you
+can call <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.cancel.html" title="twisted.internet.defer.Deferred.cancel">Deferred.cancel</a></code>
+on the <code>Deferred</code> returned from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IClientStreamEndpoint.connect.html" title="twisted.internet.interfaces.IClientStreamEndpoint.connect">connect</a></code>, and the
+underlying machinery should give up on the connection. This should cause the
+<code>Deferred</code> to errback, usually with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.CancelledError.html" title="twisted.internet.defer.CancelledError">CancelledError</a></code>; although you should
+consult the documentation for your particular endpoint type to see if it may do
+something different.</p>
+
+<p>Although some endpoint types may imply a built-in timeout, the
+interface does not guarantee one. If you don't have any way for the
+application to cancel a wayward connection attempt, the attempt may just
+keep waiting forever. For example, a very simple 30-second timeout could be
+implemented like this:
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">attempt</span> = <span class="py-src-variable">myEndpoint</span>.<span class="py-src-variable">connect</span>(<span class="py-src-variable">myFactory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">attempt</span>.<span class="py-src-variable">cancel</span>)
+</pre>
+</p>
+
+<h2>Maximizing the Return on your Endpoint Investment<a name="auto5"/></h2>
+
+<p>Directly constructing an endpoint in your application is rarely the
+best option, because it ties your application to a particular type of
+transport. The strength of the endpoints API is in separating the
+construction of the endpoint (figuring out where to connect or listen) and
+its activation (actually connecting or listening).</p>
+
+<p>If you are implementing a library that needs to listen for
+connections or make outgoing connections, when possible, you should write
+your code to accept client and server endpoints as parameters to functions
+or to your objects' constructors. That way, application code that calls
+your library can provide whatever endpoints are appropriate.</p>
+
+<p>If you are writing an application and you need to construct
+endpoints yourself, you can allow users to specify arbitrary endpoints
+described by a string using the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.clientFromString.html" title="twisted.internet.endpoints.clientFromString">clientFromString</a></code> and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.serverFromString.html" title="twisted.internet.endpoints.serverFromString">serverFromString</a></code>
+APIs. Since these APIs just take a string, they provide flexibility: if
+Twisted adds support for new types of endpoints (for example, IPv6
+endpoints, or WebSocket endpoints), your application will automatically be
+able to take advantage of them with no changes to its code.</p>
+
+<h3>Endpoints Aren't Always the Answer<a name="auto6"/></h3>
+
+<p>For many use-cases, especially the common case of a <code>twistd</code>
+plugin which runs a long-running server that just binds a simple port, you
+might not want to use the endpoints APIs directly. Instead, you may want to
+construct an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code>, using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.strports.service.html" title="twisted.application.strports.service">strports.service</a></code> , which will fit
+neatly into the required structure of <a href="plugin.html" shape="rect">the twistd
+plugin API</a> . This doesn't give your application much control - the port
+starts listening at startup and stops listening at shutdown - but it does
+provide the same flexibility in terms of what type of server endpoint your
+application will support.</p>
+
+<p>It is, however, almost always preferable to use an endpoint rather
+than calling a lower-level APIs like <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.connectTCP.html" title="twisted.internet.interfaces.IReactorTCP.connectTCP">connectTCP</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.listenTCP.html" title="twisted.internet.interfaces.IReactorTCP.listenTCP">listenTCP</a></code>,
+etc, directly. By accepting an arbitrary endpoint rather than requiring a
+specific reactor interface, you leave your application open to lots of
+interesting transport-layer extensibility for the future.</p>
+
+<h2>Endpoint Types Included With Twisted<a name="auto7"/></h2>
+
+<p>The parser used by <code>clientFromString</code> and
+<code>serverFromString</code> is extensible via third-party plugins, so the
+endpoints available on your system depend on what packages you have installed.
+However, Twisted itself includes a set of basic endpoints that will always be
+available.</p>
+
+<h3>Clients<a name="auto8"/></h3>
+
+<ul>
+ <li>TCP. Supported arguments: host, port, timeout. timeout is optional. For
+ example, <code>tcp:host=twistedmatrix.com:port=80:timeout=15</code>.
+ </li>
+ <li>SSL. All TCP arguments are supported, plus: certKey, privateKey,
+ caCertsDir. certKey (optional) gives a filesystem path to a certificate (PEM
+ format). privateKey (optional) gives a filesystem path to a a private key
+ (PEM format). caCertsDir (optional) gives a filesystem path to a directory
+ containing trusted CA certificates to use to verify the server certificate.
+ For example,
+ <code>ssl:host=twistedmatrix.com:port=443:caCertsDir=/etc/ssl/certs</code>.
+ </li>
+ <li>UNIX. Supported arguments: path, timeout, checkPID. path gives a
+ filesystem path to a listening UNIX domain socket server. checkPID (optional)
+ enables a check of the lock file Twisted-based UNIX domain socket servers use
+ to prove they are still running. For
+ example, <code>unix:path=/var/run/web.sock</code>.
+ </li>
+</ul>
+
+<h3>Servers<a name="auto9"/></h3>
+
+<ul>
+ <li>TCP. Supported arguments: port, interface, backlog. interface and
+ backlog are optional. interface is an IP address to bind to. For example,
+ <code>tcp:port=80:interface=192.168.1.1</code>.
+ </li>
+ <li>SSL. All TCP arguments are supported, plus: certKey, privateKey, and
+ sslmethod. certKey (optional, defaults to the value of privateKey) gives a
+ filesystem path to a certificate (PEM format). privateKey gives a filesystem
+ path to a a private key (PEM format). sslmethod indicates which SSL/TLS
+ version to use (a value like TLSv1_METHOD). For example,
+ <code>ssl:port=443:privateKey=/etc/ssl/server.pem:sslmethod=SSLv3_METHOD</code>.
+ </li>
+ <li>UNIX. Supported arguments: address, mode, backlog, lockfile. address
+ gives a filesystem path to listen on with a UNIX domain socket server. mode
+ (optional) gives the filesystem permission/mode (in octal) to apply to that
+ socket. lockfile enables use of a separate lock file to prove the server is
+ still running. For example, <code>unix:address=/var/run/web.sock:lockfile=1</code>.
+ </li>
+ <li>systemd. Supported arguments: domain, index. domain indicates which
+ socket domain the inherited file descriptor belongs to (eg INET, INET6).
+ index indicates an offset into the array of file descriptors which have been
+ inherited from systemd. For
+ example, <code>systemd:domain=INET6:index=3</code>.
+ </li>
+</ul>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/gendefer.html b/doc/core/howto/gendefer.html
new file mode 100644
index 0000000..44b35b9
--- /dev/null
+++ b/doc/core/howto/gendefer.html
@@ -0,0 +1,411 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Generating Deferreds</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Generating Deferreds</h1>
+ <div class="toc"><ol><li><a href="#auto0">Class overview</a></li><ul><li><a href="#auto1">Basic Callback Functions</a></li></ul><li><a href="#auto2">What Deferreds don't do: make your code asynchronous</a></li><li><a href="#auto3">Advanced Processing Chain Control</a></li><li><a href="#auto4">Returning Deferreds from synchronous functions</a></li><li><a href="#auto5">Integrating blocking code with Twisted</a></li><li><a href="#auto6">Possible sources of error</a></li><ul><li><a href="#auto7">Firing Deferreds more than once is impossible</a></li><li><a href="#auto8">Synchronous callback execution</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> objects are
+signals that a function you have called does not yet have the data you want
+available. When a function returns a Deferred object, your calling function
+attaches callbacks to it to handle the data when available.</p>
+
+<p>This document addresses the other half of the question: writing functions
+that return Deferreds, that is, constructing Deferred objects, arranging for
+them to be returned immediately without blocking until data is available, and
+firing their callbacks when the data is available.</p>
+
+<p>This document assumes that you are familiar with the asynchronous model used
+by Twisted, and with <a href="defer.html" shape="rect">using deferreds returned by functions</a>
+.</p>
+
+<a name="class" shape="rect"/>
+
+<h2>Class overview<a name="auto0"/></h2>
+
+<p>This is an overview API reference for Deferred from the point of creating a
+Deferred and firing its callbacks and errbacks. It is not meant to be a
+substitute for the docstrings in the Deferred class, but can provide
+guidelines for its use.</p>
+
+<p>There is a parallel overview of functions used by calling function which
+the Deferred is returned to at <a href="defer.html#class" shape="rect">Using Deferreds</a>.</p>
+
+<h3>Basic Callback Functions<a name="auto1"/></h3>
+
+<ul>
+ <li>
+ <code class="py-prototype">callback(result)</code>
+
+ <p>Run success callbacks with the given result. <em>This
+ can only be run once.</em> Later calls to this or
+ <code>errback</code> will raise <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.AlreadyCalledError.html" title="twisted.internet.defer.AlreadyCalledError">twisted.internet.defer.AlreadyCalledError</a></code>.
+ If further callbacks or errbacks are added after this
+ point, addCallbacks will run the callbacks immediately.</p>
+ </li>
+
+ <li>
+ <code class="py-prototype">errback(failure)</code>
+
+ <p>Run error callbacks with the given failure. <em>This can
+ only be run once.</em> Later calls to this or
+ <code>callback</code> will raise <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.AlreadyCalledError.html" title="twisted.internet.defer.AlreadyCalledError">twisted.internet.defer.AlreadyCalledError</a></code>.
+ If further callbacks or errbacks are added after this
+ point, addCallbacks will run the callbacks immediately.</p>
+ </li>
+</ul>
+
+<h2>What Deferreds don't do: make your code asynchronous<a name="auto2"/></h2>
+
+<p><em>Deferreds do not make the code magically not block.</em></p>
+
+<p>Let's take this function as an example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+<span class="py-src-variable">TARGET</span> = <span class="py-src-number">10000</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">largeFibonnaciNumber</span>():
+ <span class="py-src-comment"># create a Deferred object to return:</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+
+ <span class="py-src-comment"># calculate the ten thousandth Fibonnaci number</span>
+
+ <span class="py-src-variable">first</span> = <span class="py-src-number">0</span>
+ <span class="py-src-variable">second</span> = <span class="py-src-number">1</span>
+
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">i</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">xrange</span>(<span class="py-src-variable">TARGET</span> - <span class="py-src-number">1</span>):
+ <span class="py-src-variable">new</span> = <span class="py-src-variable">first</span> + <span class="py-src-variable">second</span>
+ <span class="py-src-variable">first</span> = <span class="py-src-variable">second</span>
+ <span class="py-src-variable">second</span> = <span class="py-src-variable">new</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">i</span> % <span class="py-src-number">100</span> == <span class="py-src-number">0</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Progress: calculating the %dth Fibonnaci number&quot;</span> % <span class="py-src-variable">i</span>
+
+ <span class="py-src-comment"># give the Deferred the answer to pass to the callbacks:</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">second</span>)
+
+ <span class="py-src-comment"># return the Deferred with the answer:</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+
+<span class="py-src-variable">timeBefore</span> = <span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>()
+
+<span class="py-src-comment"># call the function and get our Deferred</span>
+<span class="py-src-variable">d</span> = <span class="py-src-variable">largeFibonnaciNumber</span>()
+
+<span class="py-src-variable">timeAfter</span> = <span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>()
+
+<span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Total time taken for largeFibonnaciNumber call: %0.3f seconds&quot;</span> %
+ (<span class="py-src-variable">timeAfter</span> - <span class="py-src-variable">timeBefore</span>)
+
+<span class="py-src-comment"># add a callback to it to print the number</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printNumber</span>(<span class="py-src-parameter">number</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;The %dth Fibonacci number is %d&quot;</span> % (<span class="py-src-variable">TARGET</span>, <span class="py-src-variable">number</span>)
+
+<span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Adding the callback now.&quot;</span>
+
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printNumber</span>)
+</pre>
+
+<p>You will notice that despite creating a Deferred in the <code>largeFibonnaciNumber</code> function, these things happened:</p>
+<ul>
+<li>the &quot;Total time taken for largeFibonnaciNumber call&quot; output
+shows that the function did not return immediately as asynchronous functions
+are expected to do; and</li>
+<li>rather than the callback being added before the result was available and
+called after the result is available, it isn't even added until after the
+calculation has been completed.</li>
+</ul>
+
+<p> The function completed its calculation before returning, blocking the
+process until it had finished, which is exactly what asynchronous functions
+are not meant to do. Deferreds are not a non-blocking talisman: they are a
+signal for asynchronous functions to <em>use</em> to pass results onto
+callbacks, but using them does not guarantee that you have an asynchronous
+function.</p>
+
+
+<h2>Advanced Processing Chain Control<a name="auto3"/></h2>
+
+<ul>
+ <li>
+ <code class="py-prototype">pause()</code>
+
+ <p>Cease calling any methods as they are added, and do not
+ respond to <code>callback</code>, until
+ <code>self.unpause()</code> is called.</p>
+ </li>
+
+ <li>
+ <code class="py-prototype">unpause()</code>
+
+ <p>If <code>callback</code> has been called on this
+ Deferred already, call all the callbacks that have been
+ added to this Deferred since <code>pause</code> was
+ called.</p>
+
+ <p>Whether it was called or not, this will put this
+ Deferred in a state where further calls to
+ <code>addCallbacks</code> or <code>callback</code> will
+ work as normal.</p>
+ </li>
+</ul>
+
+<h2>Returning Deferreds from synchronous functions<a name="auto4"/></h2>
+
+<p>Sometimes you might wish to return a Deferred from a synchronous function.
+There are several reasons why, the major two are maintaining API compatibility
+with another version of your function which returns a Deferred, or allowing
+for the possiblity that in the future your function might need to be
+asynchronous.</p>
+
+<p>In the <a href="defer.html" shape="rect">Using Deferreds</a> reference, we gave the
+following example of a synchronous function:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">synchronousIsValidUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">'''
+ Return true if user is a valid user, false otherwise
+ '''</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">&quot;Alice&quot;</span>, <span class="py-src-string">&quot;Angus&quot;</span>, <span class="py-src-string">&quot;Agnes&quot;</span>]
+</pre><div class="caption">Source listing - <a href="listings/deferred/synch-validation.py"><span class="filename">listings/deferred/synch-validation.py</span></a></div></div>
+
+<p>While we can require that callers of our function wrap our synchronous
+result in a Deferred using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.maybeDeferred.html" title="twisted.internet.defer.maybeDeferred">maybeDeferred</a></code>, for the sake of API
+compatibility it is better to return a Deferred ourselves using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.succeed.html" title="twisted.internet.defer.succeed">defer.succeed</a></code>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">immediateIsValidUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">'''
+ Returns a Deferred resulting in true if user is a valid user, false
+ otherwise
+ '''</span>
+
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">&quot;Alice&quot;</span>, <span class="py-src-string">&quot;Angus&quot;</span>, <span class="py-src-string">&quot;Agnes&quot;</span>]
+
+ <span class="py-src-comment"># return a Deferred object already called back with the value of result</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">result</span>)
+</pre>
+
+<p>There is an equivalent <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.fail.html" title="twisted.internet.defer.fail">defer.fail</a></code> method to return a Deferred with the
+errback chain already fired.</p>
+
+<h2>Integrating blocking code with Twisted<a name="auto5"/></h2>
+
+<p>At some point, you are likely to need to call a blocking function: many
+functions in third party libraries will have long running blocking functions.
+There is no way to 'force' a function to be asynchronous: it must be written
+that way specifically. When using Twisted, your own code should be
+asynchronous, but there is no way to make third party functions asynchronous
+other than rewriting them.</p>
+
+<p>In this case, Twisted provides the ability to run the blocking code in a
+separate thread rather than letting it block your application. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.threads.deferToThread.html" title="twisted.internet.threads.deferToThread">twisted.internet.threads.deferToThread</a></code> function will set up
+a thread to run your blocking function, return a Deferred and later fire that
+Deferred when the thread completes.</p>
+
+<p>Let's assume our <code class="python">largeFibonnaciNumber</code> function
+from above is in a third party library (returning the result of the
+calculation, not a Deferred) and is not easily modifiable to be finished in
+discrete blocks. This example shows it being called in a thread, unlike in the
+earlier section we'll see that the operation does not block our entire
+program:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">largeFibonnaciNumber</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Represent a long running blocking function by calculating
+ the TARGETth Fibonnaci number
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">TARGET</span> = <span class="py-src-number">10000</span>
+
+ <span class="py-src-variable">first</span> = <span class="py-src-number">0</span>
+ <span class="py-src-variable">second</span> = <span class="py-src-number">1</span>
+
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">i</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">xrange</span>(<span class="py-src-variable">TARGET</span> - <span class="py-src-number">1</span>):
+ <span class="py-src-variable">new</span> = <span class="py-src-variable">first</span> + <span class="py-src-variable">second</span>
+ <span class="py-src-variable">first</span> = <span class="py-src-variable">second</span>
+ <span class="py-src-variable">second</span> = <span class="py-src-variable">new</span>
+
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">second</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">threads</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">fibonacciCallback</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Callback which manages the largeFibonnaciNumber result by
+ printing it out
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;largeFibonnaciNumber result =&quot;</span>, <span class="py-src-variable">result</span>
+ <span class="py-src-comment"># make sure the reactor stops after the callback chain finishes,</span>
+ <span class="py-src-comment"># just so that this example terminates</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">run</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Run a series of operations, deferring the largeFibonnaciNumber
+ operation to a thread and performing some other operations after
+ adding the callback
+ &quot;&quot;&quot;</span>
+ <span class="py-src-comment"># get our Deferred which will be called with the largeFibonnaciNumber result</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">threads</span>.<span class="py-src-variable">deferToThread</span>(<span class="py-src-variable">largeFibonnaciNumber</span>)
+ <span class="py-src-comment"># add our callback to print it out</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">fibonacciCallback</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;1st line after the addition of the callback&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;2nd line after the addition of the callback&quot;</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">run</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h2>Possible sources of error<a name="auto6"/></h2>
+
+<p>Deferreds greatly simplify the process of writing asynchronous code by
+providing a standard for registering callbacks, but there are some subtle and
+sometimes confusing rules that you need to follow if you are going to use
+them. This mostly applies to people who are writing new systems that use
+Deferreds internally, and not writers of applications that just add callbacks
+to Deferreds produced and processed by other systems. Nevertheless, it is good
+to know.</p>
+
+<h3>Firing Deferreds more than once is impossible<a name="auto7"/></h3>
+
+<p>Deferreds are one-shot. You can only call <code>Deferred.callback</code> or <code>Deferred.errback</code> once. The processing chain continues each time
+you add new callbacks to an already-called-back-to Deferred.</p>
+
+<h3>Synchronous callback execution<a name="auto8"/></h3>
+
+<p>If a Deferred already has a result available, <code>addCallback</code> <strong>may</strong> call the callback synchronously: that is, immediately
+after it's been added. In situations where callbacks modify state, it is
+might be desirable for the chain of processing to halt until all callbacks are
+added. For this, it is possible to <code>pause</code> and <code>unpause</code>
+a Deferred's processing chain while you are adding lots of callbacks.</p>
+
+<p>Be careful when you use these methods! If you <code>pause</code> a
+Deferred, it is <em>your</em> responsibility to make sure that you unpause it.
+The function adding the callbacks must unpause a paused Deferred, it should <em>never</em> be the responsibility of the code that actually fires the
+callback chain by calling <code>callback</code> or <code>errback</code> as
+this would negate its usefulness!</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/glossary.html b/doc/core/howto/glossary.html
new file mode 100644
index 0000000..c6d8ea9
--- /dev/null
+++ b/doc/core/howto/glossary.html
@@ -0,0 +1,334 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Glossary</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Glossary</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<dl>
+
+<dt><a name="adaptee" shape="rect">adaptee</a></dt>
+<dd>
+ An object that has been adapted, also called <q>original</q>. See <a href="#Adapter" shape="rect">Adapter</a>.
+</dd>
+
+<dt><a name="Adapter" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.Adapter.html" title="twisted.python.components.Adapter">Adapter</a></code></a></dt>
+<dd>
+ An object whose sole purpose is to implement an Interface for another object.
+ See <a href="components.html" shape="rect">Interfaces and Adapters</a>.
+</dd>
+
+<dt><a name="Application" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Application.html" title="twisted.application.service.Application">Application</a></code></a></dt>
+<dd>
+ A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Application.html" title="twisted.application.service.Application">twisted.application.service.Application</a></code>. There are
+ HOWTOs on <a href="basics.html" shape="rect">creating and manipulating</a> them as a
+ system-administrator, as well as <a href="application.html" shape="rect">using</a> them in
+ your code.
+</dd>
+
+<dt><a name="Avatar" shape="rect">Avatar</a></dt>
+<dd>
+ (from <a href="#Cred" shape="rect">Twisted Cred</a>) business logic for specific user.
+ For example, in <a href="#PB" shape="rect">PB</a> these are perspectives, in POP3 these
+ are mailboxes, and so on.
+</dd>
+
+<dt><a name="Banana" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.banana.Banana.html" title="twisted.spread.banana.Banana">Banana</a></code></a></dt>
+<dd>
+ The low-level data marshalling layer of <a href="#Spread" shape="rect">Twisted Spread</a>.
+ See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.banana.html" title="twisted.spread.banana">twisted.spread.banana</a></code>.
+</dd>
+
+<dt><a name="Broker" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Broker.html" title="twisted.spread.pb.Broker">Broker</a></code></a></dt>
+<dd>
+ A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Broker.html" title="twisted.spread.pb.Broker">twisted.spread.pb.Broker</a></code>, the object request
+ broker for <a href="#Spread" shape="rect">Twisted Spread</a>.
+</dd>
+
+<dt><a name="cache" shape="rect">cache</a></dt>
+<dd>
+ A way to store data in readily accessible place for later reuse. Caching data
+ is often done because the data is expensive to produce or access. Caching data
+ risks being stale, or out of sync with the original data.
+</dd>
+
+<dt><a name="component" shape="rect">component</a></dt>
+<dd>
+ A special kind of (persistent) <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.Adapter.html" title="twisted.python.components.Adapter">Adapter</a></code> that works with a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.Componentized.html" title="twisted.python.components.Componentized">twisted.python.components.Componentized</a></code>. See also <a href="components.html" shape="rect">Interfaces and Adapters</a>.
+</dd>
+
+<dt><a name="Componentized" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.Componentized.html" title="twisted.python.components.Componentized">Componentized</a></code></a></dt>
+<dd>
+ A Componentized object is a collection of information, separated
+ into domain-specific or role-specific instances, that all stick
+ together and refer to each other.
+ Each object is an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.Adapter.html" title="twisted.python.components.Adapter">Adapter</a></code>, which, in the
+ context of Componentized, we call <q>components</q>. See also <a href="components.html" shape="rect">Interfaces and Adapters</a>.
+</dd>
+
+<dt><a name="conch" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.conch.html" title="twisted.conch">conch</a></code></a></dt>
+<dd>Twisted's SSH implementation.</dd>
+
+<dt><a name="Connector" shape="rect">Connector</a></dt>
+<dd>
+ Object used to interface between client connections and protocols, usually
+ used with a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ClientFactory.html" title="twisted.internet.protocol.ClientFactory">twisted.internet.protocol.ClientFactory</a></code>
+ to give you control over how a client connection reconnects. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConnector.html" title="twisted.internet.interfaces.IConnector">twisted.internet.interfaces.IConnector</a></code> and <a href="clients.html" shape="rect">Writing Clients</a>.
+</dd>
+
+<dt><a name="Consumer" shape="rect">Consumer</a></dt>
+<dd>
+ An object that consumes data from a <a href="#Producer" shape="rect">Producer</a>. See
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConsumer.html" title="twisted.internet.interfaces.IConsumer">twisted.internet.interfaces.IConsumer</a></code>.
+</dd>
+
+<dt><a name="Cred" shape="rect">Cred</a></dt>
+<dd>
+ Twisted's authentication API, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.html" title="twisted.cred">twisted.cred</a></code>. See
+ <a href="cred.html" shape="rect">Introduction to Twisted Cred</a> and
+ <a href="pb-cred.html" shape="rect">Twisted Cred usage</a>.
+</dd>
+
+<dt><a name="credentials" shape="rect">credentials</a></dt>
+<dd>
+ A username/password, public key, or some other information used for
+ authentication.
+</dd>
+
+<dt><a name="credential-checker" shape="rect">credential checker</a></dt>
+<dd>
+ Where authentication actually happens. See
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.ICredentialsChecker.html" title="twisted.cred.checkers.ICredentialsChecker">ICredentialsChecker</a></code>.
+</dd>
+
+<dt><a name="CVSToys" shape="rect">CVSToys</a></dt>
+<dd>A nifty set of tools for CVS, available at
+<a href="http://twistedmatrix.com/users/acapnotic/wares/code/CVSToys/" shape="rect">http://twistedmatrix.com/users/acapnotic/wares/code/CVSToys/</a>.</dd>
+
+<dt><a name="Daemon" shape="rect">Daemon</a></dt>
+<dd>
+ A background process that does a job or handles client requests.
+ <i>Daemon</i> is a Unix term; <i>service</i> is the Windows equivalent.
+</dd>
+
+<dt><a name="Deferred" shape="rect"><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code></a></dt>
+<dd>
+ A instance of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">twisted.internet.defer.Deferred</a></code>, an
+ abstraction for handling chains of callbacks and error handlers
+ (<q>errbacks</q>).
+ See the <a href="defer.html" shape="rect">Deferring Execution</a> HOWTO.
+</dd>
+
+<dt><a name="Enterprise" shape="rect">Enterprise</a></dt>
+<dd>
+ Twisted's RDBMS support. It contains <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.html" title="twisted.enterprise.adbapi">twisted.enterprise.adbapi</a></code> for asynchronous access to any
+ standard DB-API 2.0 module. See <a href="rdbms.html" shape="rect">Introduction to
+ Twisted Enterprise</a> for more details.
+</dd>
+
+<dt><a name="errback" shape="rect">errback</a></dt>
+<dd>
+ A callback attached to a <a href="#Deferred" shape="rect">Deferred</a> with
+ <code>.addErrback</code> to handle errors.
+</dd>
+
+<dt><a name="Factory" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">Factory</a></code></a></dt>
+<dd>
+ In general, an object that constructs other objects. In Twisted, a Factory
+ usually refers to a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">twisted.internet.protocol.Factory</a></code>, which constructs
+ <a href="#Protocol" shape="rect">Protocol</a> instances for incoming or outgoing
+ connections. See <a href="servers.html" shape="rect">Writing Servers</a> and <a href="clients.html" shape="rect">Writing Clients</a>.
+</dd>
+
+<dt><a name="Failure" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code></a></dt>
+<dd>
+ Basically, an asynchronous exception that contains traceback information;
+ these are used for passing errors through asynchronous callbacks.
+</dd>
+
+<dt><a name="im" shape="rect">im</a></dt>
+<dd>
+ Abbreviation of <q>(Twisted) <a href="#InstanceMessenger" shape="rect">Instance
+ Messenger</a></q>.
+ </dd>
+
+<dt><a name="InstanceMessenger" shape="rect">Instance Messenger</a></dt>
+<dd>
+ Instance Messenger is a multi-protocol chat program that comes with
+ Twisted. It can communicate via TOC with the AOL servers, via IRC, as well as
+ via <a href="#PerspectiveBroker" shape="rect">PB</a> with
+ <a href="#Words" shape="rect">Twisted Words</a>. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.html" title="twisted.words.im">twisted.words.im</a></code>.
+</dd>
+
+<dt><a name="Interface" shape="rect">Interface</a></dt>
+<dd>
+ A class that defines and documents methods that a class conforming to that
+ interface needs to have. A collection of core <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.html" title="twisted.internet">twisted.internet</a></code> interfaces can
+ be found in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.html" title="twisted.internet.interfaces">twisted.internet.interfaces</a></code>. See also <a href="components.html" shape="rect">Interfaces and Adapters</a>.
+</dd>
+
+<dt><a name="Jelly" shape="rect">Jelly</a></dt>
+<dd>
+ The serialization layer for <a href="#Spread" shape="rect">Twisted Spread</a>, although it
+ can be used seperately from Twisted Spread as well. It is similar in purpose
+ to Python's standard <code>pickle</code> module, but is more
+ network-friendly, and depends on a separate marshaller (<a href="#Banana" shape="rect">Banana</a>, in most cases). See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.html" title="twisted.spread.jelly">twisted.spread.jelly</a></code>.
+</dd>
+
+<dt><a name="Lore" shape="rect">Lore</a></dt>
+
+<dd><a href="http://twistedmatrix.com/trac/wiki/TwistedLore/" shape="rect">Lore</a> is
+Twisted's documentation system. The source format is a subset of
+XHTML, and output formats include HTML and LaTeX.</dd>
+
+<dt><a name="Manhole" shape="rect">Manhole</a></dt>
+<dd>
+ A debugging/administration interface to a Twisted application.
+</dd>
+
+<dt><a name="Microdom" shape="rect">Microdom</a></dt>
+<dd>
+ A partial DOM implementation using <a href="#SUX" shape="rect">SUX</a>. It is simple and
+ pythonic, rather than strictly standards-compliant. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.microdom.html" title="twisted.web.microdom">twisted.web.microdom</a></code>.
+</dd>
+
+<dt><a name="Names" shape="rect">Names</a></dt>
+<dd>Twisted's DNS server, found in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.names.html" title="twisted.names">twisted.names</a></code>.</dd>
+
+<dt><a name="Nevow" shape="rect">Nevow</a></dt>
+<dd>The successor to <a href="#Woven" shape="rect">Woven</a>; available from <a href="http://launchpad.net/nevow" shape="rect">Divmod</a>.
+</dd>
+
+<dt><a name="PB" shape="rect">PB</a></dt>
+<dd>
+ Abbreviation of <q><a href="#PerspectiveBroker" shape="rect">Perspective
+ Broker</a></q>.
+</dd>
+
+<dt><a name="PerspectiveBroker" shape="rect">Perspective Broker</a></dt>
+<dd>
+ The high-level object layer of Twisted <a href="#Spread" shape="rect">Spread</a>,
+ implementing semantics for method calling and object copying, caching, and
+ referencing. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.html" title="twisted.spread.pb">twisted.spread.pb</a></code>.
+</dd>
+
+<dt><a name="Portal" shape="rect">Portal</a></dt>
+<dd>
+ Glues <a href="#credential-checker" shape="rect">credential checkers</a> and
+ <a href="#realm" shape="rect">realm</a>s together.
+</dd>
+
+<dt><a name="Producer" shape="rect">Producer</a></dt>
+<dd>
+ An object that generates data a chunk at a time, usually to be processed by a
+ <a href="#Consumer" shape="rect">Consumer</a>. See
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProducer.html" title="twisted.internet.interfaces.IProducer">twisted.internet.interfaces.IProducer</a></code>.
+</dd>
+
+<dt><a name="Protocol" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">Protocol</a></code></a></dt>
+<dd>
+ In general each network connection has its own Protocol instance to manage
+ connection-specific state. There is a collection of standard
+ protocol implementations in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.html" title="twisted.protocols">twisted.protocols</a></code>. See
+ also <a href="servers.html" shape="rect">Writing Servers</a> and <a href="clients.html" shape="rect">Writing Clients</a>.
+</dd>
+
+<dt><a name="PSU" shape="rect">PSU</a></dt>
+<dd>There is no PSU.</dd>
+
+<dt><a name="Reactor" shape="rect">Reactor</a></dt>
+<dd>
+ The core event-loop of a Twisted application. See
+ <a href="reactor-basics.html" shape="rect">Reactor Basics</a>.
+</dd>
+
+<dt><a name="Reality" shape="rect">Reality</a></dt>
+<dd>See <q><a href="#TwistedReality" shape="rect">Twisted Reality</a></q></dd>
+
+<dt><a name="realm" shape="rect">realm</a></dt>
+<dd>
+ (in <a href="#Cred" shape="rect">Twisted Cred</a>) stores <a href="#Avatar" shape="rect">avatars</a>
+ and perhaps general business logic. See
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.html" title="twisted.cred.portal.IRealm">IRealm</a></code>.
+</dd>
+
+<dt><a name="Resource" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code></a></dt>
+<dd>
+ A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">twisted.web.resource.Resource</a></code>, which are served
+ by Twisted Web. Resources can be as simple as a static file on disk, or they
+ can have dynamically generated content.
+</dd>
+
+<dt><a name="Service" shape="rect">Service</a></dt>
+<dd>
+ A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Service.html" title="twisted.application.service.Service">twisted.application.service.Service</a></code>. See <a href="application.html" shape="rect">Application howto</a> for a description of how they
+ relate to <a href="#Application" shape="rect">Applications</a>.
+</dd>
+
+<dt><a name="Spread" shape="rect">Spread</a></dt>
+<dd>Twisted Spread is
+Twisted's remote-object suite. It consists of three layers:
+<a href="#PerspectiveBroker" shape="rect">Perspective Broker</a>, <a href="#Jelly" shape="rect">Jelly</a>
+and <a href="#Banana" shape="rect">Banana.</a> See <a href="pb.html" shape="rect">Writing Applications
+with Perspective Broker</a>.</dd>
+
+<dt><a name="SUX" shape="rect">SUX</a></dt>
+<dd><em>S</em>mall <em>U</em>ncomplicated <em>X</em>ML, Twisted's simple XML
+parser written in pure Python. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.sux.html" title="twisted.web.sux">twisted.web.sux</a></code>.</dd>
+
+<dt><a name="TAC" shape="rect">TAC</a></dt>
+<dd>A <em>T</em>wisted <em>A</em>pplication <em>C</em>onfiguration is a Python
+source file, generally with the <em>.tac</em> extension, which defines
+configuration to make an application runnable using <code>twistd</code>.</dd>
+
+<dt><a name="TAP" shape="rect">TAP</a></dt>
+<dd><em>T</em>wisted <em>A</em>pplication <em>P</em>ickle (no longer supported), or simply just a
+<em>T</em>wisted <em>AP</em>plication. A serialised application that was created
+with <code>mktap</code> (no longer supported) and runnable by <code>twistd</code>. See
+<a href="basics.html" shape="rect">Using the Utilities</a>.</dd>
+
+<dt><a name="Trial" shape="rect">Trial</a></dt>
+<dd><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.trial.html" title="twisted.trial">twisted.trial</a></code>, Twisted's unit-testing framework,
+based on the <code>unittest</code> standard library module. See also <a href="testing.html" shape="rect">Writing tests for Twisted code</a>.</dd>
+
+<dt><a name="TwistedMatrixLaboratories" shape="rect">Twisted Matrix Laboratories</a></dt>
+<dd>The team behind Twisted.
+<a href="http://twistedmatrix.com/" shape="rect">http://twistedmatrix.com/</a>.</dd>
+
+<dt><a name="TwistedReality" shape="rect">Twisted Reality</a></dt>
+<dd>
+In days of old, the Twisted Reality multiplayer text-based interactive-fiction
+system was the main focus of Twisted Matrix Labs; Twisted, the general networking
+framework, grew out of Reality's need for better network functionality. Twisted
+Reality has been superseded by the <a href="http://launchpad.net/imaginary" shape="rect">Imaginary</a> project.
+</dd>
+
+<dt><a name="usage" shape="rect"><code class="API" noexpand="1"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.html" title="twisted.python.usage">usage</a></code></a></dt>
+<dd>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.html" title="twisted.python.usage">twisted.python.usage</a></code> module, a replacement for
+the standard <code>getopt</code> module for parsing command-lines which is much
+easier to work with. See <a href="options.html" shape="rect">Parsing command-lines</a>.</dd>
+
+<dt><a name="Words" shape="rect">Words</a></dt>
+<dd>Twisted Words is a multi-protocol chat server that uses the
+<a href="#PerspectiveBroker" shape="rect">Perspective Broker</a> protocol as its native
+communication style. See <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.html" title="twisted.words">twisted.words</a></code>.</dd>
+
+<dt><a name="Woven" shape="rect">Woven</a></dt>
+<dd><em>W</em>eb <em>O</em>bject <em>V</em>isualization <em>En</em>vironment.
+A templating system previously, but no longer, included with Twisted. Woven
+has largely been superceded by <a href="http://launchpad.net/nevow" shape="rect">
+Divmod Nevow</a>.</dd>
+
+</dl>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/howto.tidyrc b/doc/core/howto/howto.tidyrc
new file mode 100644
index 0000000..6896505
--- /dev/null
+++ b/doc/core/howto/howto.tidyrc
@@ -0,0 +1,6 @@
+output-xml: yes
+output-xhtml: yes
+tidy-mark: no
+indent: auto
+gnu-emacs: yes
+add-xml-decl: yes \ No newline at end of file
diff --git a/doc/core/howto/index.html b/doc/core/howto/index.html
new file mode 100644
index 0000000..76cb5bb
--- /dev/null
+++ b/doc/core/howto/index.html
@@ -0,0 +1,239 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+
+ <ul class="toc">
+ <li><a name="introduction" shape="rect">Introduction</a>
+
+ <ul>
+ <li><a href="vision.html" shape="rect">Executive summary</a><br clear="none"/>
+ Connecting your software - and having fun too!</li>
+ </ul>
+ </li>
+
+ <li><a name="tutorials" shape="rect">Getting Started</a>
+
+ <ul>
+ <li><a href="servers.html" shape="rect">Writing a TCP server</a><br clear="none"/>
+ Basic network servers with Twisted.</li>
+
+ <li><a href="clients.html" shape="rect">Writing a TCP client</a><br clear="none"/>
+ And basic clients.</li>
+
+ <li><a href="trial.html" shape="rect">Test-driven development with
+ Twisted</a><br clear="none"/>
+ Code without tests is broken by definition; Twisted makes it easy to test your network code.</li>
+
+ <li>
+ <a href="tutorial/index.html" shape="rect">Tutorial: Twisted From
+ Scratch</a>
+
+ <ol>
+ <li><a href="tutorial/intro.html" shape="rect">The Evolution of
+ Finger: building a simple finger service</a></li>
+
+ <li><a href="tutorial/protocol.html" shape="rect">The Evolution of
+ Finger: adding features to the finger service</a></li>
+
+ <li><a href="tutorial/style.html" shape="rect">The Evolution of
+ Finger: cleaning up the finger code</a></li>
+
+ <li><a href="tutorial/components.html" shape="rect">The Evolution of
+ Finger: moving to a component based
+ architecture</a></li>
+
+ <li><a href="tutorial/backends.html" shape="rect">The Evolution of
+ Finger: pluggable backends</a></li>
+
+ <li><a href="tutorial/web.html" shape="rect">The Evolution of
+ Finger: a clean web frontend</a></li>
+
+ <li><a href="tutorial/pb.html" shape="rect">The Evolution of Finger:
+ Twisted client support using Perspective
+ Broker</a></li>
+
+ <li><a href="tutorial/factory.html" shape="rect">The Evolution of
+ Finger: using a single factory for multiple
+ protocols</a></li>
+
+ <li><a href="tutorial/client.html" shape="rect">The Evolution of
+ Finger: a Twisted finger client</a></li>
+
+ <li><a href="tutorial/library.html" shape="rect">The Evolution of
+ Finger: making a finger library</a></li>
+
+ <li><a href="tutorial/configuration.html" shape="rect">The Evolution
+ of Finger: configuration and packaging of the finger
+ service</a></li>
+ </ol>
+ </li>
+
+ <li><a href="quotes.html" shape="rect">Setting up the TwistedQuotes
+ application</a></li>
+
+ <li><a href="design.html" shape="rect">Designing a Twisted application</a></li>
+ </ul>
+ </li>
+
+ <li><a name="events" shape="rect">Networking and Other Event Sources</a>
+
+ <ul>
+ <li><a href="internet-overview.html" shape="rect">Twisted
+ Internet</a><br clear="none"/>
+ A brief overview of the <code>twisted.internet</code> package.</li>
+
+ <li><a href="reactor-basics.html" shape="rect">Reactor basics</a><br clear="none"/>
+ The event loop at the core of your program.</li>
+
+ <li><a href="ssl.html" shape="rect">Using SSL in Twisted</a><br clear="none"/>
+ Add some security to your network transport.</li>
+
+ <li><a href="udp.html" shape="rect">UDP Networking</a><br clear="none"/>
+ Multicast too!</li>
+
+ <li><a href="process.html" shape="rect">Using processes</a><br clear="none"/>
+ Launching sub-processes, the correct way.</li>
+
+ <li><a href="defer.html" shape="rect">Using Deferreds</a><br clear="none"/>
+ Like callback functions, only a lot better.</li>
+
+ <li><a href="gendefer.html" shape="rect">Generating deferreds</a><br clear="none"/>
+ More about Deferreds.</li>
+
+ <li><a href="time.html" shape="rect">Scheduling</a><br clear="none"/>
+ Timeouts, repeated events, and more: when you want things to happen later.</li>
+
+ <li><a href="threading.html" shape="rect">Using threads</a><br clear="none"/>
+ Running code in threads, and interacting with Twisted in a thread-safe manner.</li>
+
+ <li><a href="producers.html" shape="rect">Producers and Consumers: Efficient High-Volume Streaming</a><br clear="none"/>
+ How to pause when buffers fill up.</li>
+
+ <li><a href="choosing-reactor.html" shape="rect">Choosing a reactor and
+ GUI toolkit integration</a><br clear="none"/>
+ GTK+, Windows, epoll() and more: use your GUI of choice, or a faster event loop.</li>
+ </ul>
+ </li>
+
+ <li><a name="highlevel" shape="rect">High-Level Infrastructure</a>
+ <ul>
+ <li><a href="endpoints.html" shape="rect">Getting Connected with Endpoints</a><br clear="none"/>
+ Create configurable applications that support multiple transports (e.g. TCP and SSL).</li>
+
+ <li><a href="components.html" shape="rect">Interfaces and Adapters
+ (Component Architecture)</a><br clear="none"/>
+ When inheritance isn't enough.</li>
+
+ <li><a href="cred.html" shape="rect">Cred: Pluggable
+ Authentication</a><br clear="none"/> Implementing authentication and
+ authorization that is configurable, pluggable and
+ re-usable.</li>
+
+ <li><a href="plugin.html" shape="rect">Twisted's plugin architecture</a><br clear="none"/>
+ A generic plugin system for extendable programs.</li>
+ </ul>
+ </li>
+
+ <li><a name="deploying" shape="rect">Deploying Twisted Applications</a>
+
+ <ul>
+ <li><a href="basics.html" shape="rect">Helper programs and scripts
+ (twistd, ..)</a><br clear="none"/>
+ <code>twistd</code> lets you daemonize and run your
+ application.</li>
+
+ <li><a href="application.html" shape="rect">Using the Twisted Application
+ Framework</a><br clear="none"/>
+ Writing code that <code>twistd</code> can run.</li>
+
+ <li><a href="tap.html" shape="rect">Writing Twisted Application Plugins
+ for twistd</a><br clear="none"/>
+ More powerful <code>twistd</code> deployment method.</li>
+ </ul>
+ </li>
+
+ <li><a name="utilities" shape="rect">Utilities</a>
+
+ <ul>
+ <li><a href="logging.html" shape="rect">Logging</a><br clear="none"/>
+ Keep a record of what your application is up to.</li>
+
+ <li><a href="constants.html" shape="rect">Symbolic constants</a><br clear="none"/>
+ enum-like constants.</li>
+
+ <li><a href="rdbms.html" shape="rect">Twisted RDBMS support with
+ adbapi</a><br clear="none"/>
+ Using SQL with your relational database via DB-API adapters.</li>
+
+ <li><a href="options.html" shape="rect">Parsing command-line arguments</a><br clear="none"/>
+ The command-line argument parsing used by <code>twistd</code>.</li>
+
+ <li><a href="dirdbm.html" shape="rect">Using Dirdbm: Directory-based Storage</a><br clear="none"/>
+ A simplistic way to store data on your filesystem.</li>
+
+ <li><a href="testing.html" shape="rect">Tips for writing tests for Twisted
+ code using Trial</a><br clear="none"/>
+ More information on writing tests.</li>
+
+ <li><a href="sendmsg.html" shape="rect">Extremely Low-Level Socket Operations</a><br clear="none"/>
+ Using wrappers for sendmsg(2) and recvmsg(2).</li>
+ </ul>
+ </li>
+
+ <li><a name="amp" shape="rect">Asynchronous Messaging Protocol (AMP)</a>
+
+ <ul>
+ <li><a href="amp.html" shape="rect">Asynchronous Messaging Protocol Overview</a><br clear="none"/>
+ A two-way asynchronous message passing protocol, for when HTTP isn't good enough.</li>
+ </ul>
+ </li>
+
+ <li><a name="pb" shape="rect">Perspective Broker</a>
+ <ul>
+ <li><a href="pb.html" shape="rect">Twisted Spread</a><br clear="none"/>
+ A remote method invocation (RMI) protocol: call methods on remote objects.</li>
+
+ <li><a href="pb-intro.html" shape="rect">Introduction to Perspective
+ Broker</a></li>
+
+ <li><a href="pb-usage.html" shape="rect">Using Perspective
+ Broker</a></li>
+
+ <li><a href="pb-clients.html" shape="rect">Managing Clients of
+ Perspectives</a></li>
+
+ <li><a href="pb-copyable.html" shape="rect">Passing Complex
+ Types</a></li>
+
+ <li><a href="pb-cred.html" shape="rect">Authentication with Perspective
+ Broker</a></li>
+
+ <li><a href="pb-limits.html" shape="rect">PB Limits</a></li>
+ </ul>
+ </li>
+
+ <li><a name="appendix" shape="rect">Appendix</a>
+
+ <ul>
+ <li><a href="glossary.html" shape="rect">Glossary</a></li>
+
+ <li class="ignoretoc"><a href="debug-with-emacs.html" shape="rect">Tips
+ for debugging with emacs</a></li>
+ </ul>
+ </li>
+ </ul>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/internet-overview.html b/doc/core/howto/internet-overview.html
new file mode 100644
index 0000000..2da29dd
--- /dev/null
+++ b/doc/core/howto/internet-overview.html
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Overview of Twisted Internet</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Overview of Twisted Internet</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<p>Twisted Internet is a collection of compatible event-loops for Python.
+It contains the code to dispatch events to interested observers and a portable
+API so that observers need not care about which event loop is running. Thus,
+it is possible to use the same code for different loops, from Twisted's basic,
+yet portable, <code>select</code>-based loop to the loops of various GUI
+toolkits like GTK+ or Tk.</p>
+
+<p>Twisted Internet contains the various interfaces to the reactor
+API, whose usage is documented in the low-level chapter. Those APIs
+are <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorCore.html" title="twisted.internet.interfaces.IReactorCore">IReactorCore</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.html" title="twisted.internet.interfaces.IReactorTCP">IReactorTCP</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.html" title="twisted.internet.interfaces.IReactorSSL">IReactorSSL</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIX.html" title="twisted.internet.interfaces.IReactorUNIX">IReactorUNIX</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUDP.html" title="twisted.internet.interfaces.IReactorUDP">IReactorUDP</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTime.html" title="twisted.internet.interfaces.IReactorTime">IReactorTime</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorProcess.html" title="twisted.internet.interfaces.IReactorProcess">IReactorProcess</a></code>,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorMulticast.html" title="twisted.internet.interfaces.IReactorMulticast">IReactorMulticast</a></code>
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorThreads.html" title="twisted.internet.interfaces.IReactorThreads">IReactorThreads</a></code>.
+The reactor APIs allow non-persistent calls to be made.</p>
+
+<p>Twisted Internet also covers the interfaces for the various transports,
+in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITransport.html" title="twisted.internet.interfaces.ITransport">ITransport</a></code>
+and friends. These interfaces allow Twisted network code to be written without
+regard to the underlying implementation of the transport.</p>
+
+<p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocolFactory.html" title="twisted.internet.interfaces.IProtocolFactory">IProtocolFactory</a></code>
+dictates how factories, which are usually a large part of third party code, are
+written.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/listings/TwistedQuotes/__init__.py b/doc/core/howto/listings/TwistedQuotes/__init__.py
new file mode 100644
index 0000000..ed6bd97
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/__init__.py
@@ -0,0 +1,3 @@
+"""
+Twisted Quotes
+"""
diff --git a/doc/core/howto/listings/TwistedQuotes/pbquote.py b/doc/core/howto/listings/TwistedQuotes/pbquote.py
new file mode 100644
index 0000000..d0330e6
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/pbquote.py
@@ -0,0 +1,10 @@
+from twisted.spread import pb
+
+class QuoteReader(pb.Root):
+
+ def __init__(self, quoter):
+ self.quoter = quoter
+
+ def remote_nextQuote(self):
+ return self.quoter.getQuote()
+
diff --git a/doc/core/howto/listings/TwistedQuotes/pbquoteclient.py b/doc/core/howto/listings/TwistedQuotes/pbquoteclient.py
new file mode 100644
index 0000000..c297539
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/pbquoteclient.py
@@ -0,0 +1,32 @@
+
+from sys import stdout
+from twisted.python import log
+log.discardLogs()
+from twisted.internet import reactor
+from twisted.spread import pb
+
+def connected(root):
+ root.callRemote('nextQuote').addCallbacks(success, failure)
+
+def success(quote):
+ stdout.write(quote + "\n")
+ reactor.stop()
+
+def failure(error):
+ stdout.write("Failed to obtain quote.\n")
+ reactor.stop()
+
+factory = pb.PBClientFactory()
+reactor.connectTCP(
+ "localhost", # host name
+ pb.portno, # port number
+ factory, # factory
+ )
+
+
+
+factory.getRootObject().addCallbacks(connected, # when we get the root
+ failure) # when we can't
+
+reactor.run() # start the main loop
+
diff --git a/doc/core/howto/listings/TwistedQuotes/quoteproto.py b/doc/core/howto/listings/TwistedQuotes/quoteproto.py
new file mode 100644
index 0000000..b8d3469
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/quoteproto.py
@@ -0,0 +1,36 @@
+from zope.interface import Interface
+
+from twisted.internet.protocol import Factory, Protocol
+
+
+
+class IQuoter(Interface):
+ """
+ An object that returns quotes.
+ """
+ def getQuote():
+ """
+ Return a quote.
+ """
+
+
+
+class QOTD(Protocol):
+ def connectionMade(self):
+ self.transport.write(self.factory.quoter.getQuote()+'\r\n')
+ self.transport.loseConnection()
+
+
+
+class QOTDFactory(Factory):
+ """
+ A factory for the Quote of the Day protocol.
+
+ @type quoter: L{IQuoter} provider
+ @ivar quoter: An object which provides L{IQuoter} which will be used by
+ the L{QOTD} protocol to get quotes to emit.
+ """
+ protocol = QOTD
+
+ def __init__(self, quoter):
+ self.quoter = quoter
diff --git a/doc/core/howto/listings/TwistedQuotes/quoters.py b/doc/core/howto/listings/TwistedQuotes/quoters.py
new file mode 100644
index 0000000..f6d5689
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/quoters.py
@@ -0,0 +1,39 @@
+from random import choice
+
+from zope.interface import implements
+
+from TwistedQuotes import quoteproto
+
+
+
+class StaticQuoter:
+ """
+ Return a static quote.
+ """
+
+ implements(quoteproto.IQuoter)
+
+ def __init__(self, quote):
+ self.quote = quote
+
+
+ def getQuote(self):
+ return self.quote
+
+
+
+class FortuneQuoter:
+ """
+ Load quotes from a fortune-format file.
+ """
+ implements(quoteproto.IQuoter)
+
+ def __init__(self, filenames):
+ self.filenames = filenames
+
+
+ def getQuote(self):
+ quoteFile = file(choice(self.filenames))
+ quotes = quoteFile.read().split('\n%\n')
+ quoteFile.close()
+ return choice(quotes)
diff --git a/doc/core/howto/listings/TwistedQuotes/quotes.txt b/doc/core/howto/listings/TwistedQuotes/quotes.txt
new file mode 100644
index 0000000..62a5ed9
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/quotes.txt
@@ -0,0 +1,15 @@
+
+<radix> the sysadmin of the future is going to know twisted-shelling like the back of his hand
+%
+<Acapnotic> Ooh, I just figured out what my first twisted.reality creation will be.
+<dash> Acapnotic: oh?
+<Acapnotic> "Being Glyph Lefkowitz"
+%
+<johs> Oh, please. Threads ownz j00.
+%
+<jafo> I used to hang out with this chick that ran a BBS.
+<jafo> She had a great baud.
+%
+<chrchr> dsmith: Twisted is neat, but unfortunately, it's not object-oriented.
+%
+<datazone> twisted is madness
diff --git a/doc/core/howto/listings/TwistedQuotes/quotetap.py b/doc/core/howto/listings/TwistedQuotes/quotetap.py
new file mode 100644
index 0000000..06d15ec
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/quotetap.py
@@ -0,0 +1,29 @@
+from twisted.application import internet # services that run TCP/SSL/etc.
+from TwistedQuotes import quoteproto # Protocol and Factory
+from TwistedQuotes import quoters # "give me a quote" code
+
+from twisted.python import usage # twisted command-line processing
+
+
+class Options(usage.Options):
+ optParameters = [["port", "p", 8007,
+ "Port number to listen on for QOTD protocol."],
+ ["static", "s", "An apple a day keeps the doctor away.",
+ "A static quote to display."],
+ ["file", "f", None,
+ "A fortune-format text file to read quotes from."]]
+
+
+def makeService(config):
+ """Return a service that will be attached to the application."""
+ if config["file"]: # If I was given a "file" option...
+ # Read quotes from a file, selecting a random one each time,
+ quoter = quoters.FortuneQuoter([config['file']])
+ else: # otherwise,
+ # read a single quote from the command line (or use the default).
+ quoter = quoters.StaticQuoter(config['static'])
+ port = int(config["port"]) # TCP port to listen on
+ factory = quoteproto.QOTDFactory(quoter) # here we create a QOTDFactory
+ # Finally, set up our factory, with its custom quoter, to create QOTD
+ # protocol instances when events arrive on the specified port.
+ return internet.TCPServer(port, factory)
diff --git a/doc/core/howto/listings/TwistedQuotes/quotetap2.py b/doc/core/howto/listings/TwistedQuotes/quotetap2.py
new file mode 100644
index 0000000..4bc0f06
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/quotetap2.py
@@ -0,0 +1,36 @@
+from TwistedQuotes import quoteproto # Protocol and Factory
+from TwistedQuotes import quoters # "give me a quote" code
+from TwistedQuotes import pbquote # perspective broker binding
+
+from twisted.application import service, internet
+from twisted.python import usage # twisted command-line processing
+from twisted.spread import pb # Perspective Broker
+
+class Options(usage.Options):
+ optParameters = [["port", "p", 8007,
+ "Port number to listen on for QOTD protocol."],
+ ["static", "s", "An apple a day keeps the doctor away.",
+ "A static quote to display."],
+ ["file", "f", None,
+ "A fortune-format text file to read quotes from."],
+ ["pb", "b", None,
+ "Port to listen with PB server"]]
+
+def makeService(config):
+ svc = service.MultiService()
+ if config["file"]: # If I was given a "file" option...
+ # Read quotes from a file, selecting a random one each time,
+ quoter = quoters.FortuneQuoter([config['file']])
+ else: # otherwise,
+ # read a single quote from the command line (or use the default).
+ quoter = quoters.StaticQuoter(config['static'])
+ port = int(config["port"]) # TCP port to listen on
+ factory = quoteproto.QOTDFactory(quoter) # here we create a QOTDFactory
+ # Finally, set up our factory, with its custom quoter, to create QOTD
+ # protocol instances when events arrive on the specified port.
+ pbport = config['pb'] # TCP PB port to listen on
+ if pbport:
+ pbfact = pb.PBServerFactory(pbquote.QuoteReader(quoter))
+ svc.addService(internet.TCPServer(int(pbport), pbfact))
+ svc.addService(internet.TCPServer(port, factory))
+ return svc
diff --git a/doc/core/howto/listings/TwistedQuotes/webquote.rpy b/doc/core/howto/listings/TwistedQuotes/webquote.rpy
new file mode 100644
index 0000000..99e0e9c
--- /dev/null
+++ b/doc/core/howto/listings/TwistedQuotes/webquote.rpy
@@ -0,0 +1,12 @@
+# -*- Python -*-
+
+from TwistedQuotes import webquoteresource
+
+#__file__ is defined to be the name of this file; this is to
+#get the sibling file "quotes.txt" which should be in the same directory
+import os
+quotefile = os.path.join(os.path.split(__file__)[0], "quotes.txt")
+
+#ResourceScript requires us to define 'resource'.
+#This resource is used to render the page.
+resource = webquoteresource.QuoteResource([quotefile])
diff --git a/doc/core/howto/listings/amp/basic_client.py b/doc/core/howto/listings/amp/basic_client.py
new file mode 100644
index 0000000..6d99b68
--- /dev/null
+++ b/doc/core/howto/listings/amp/basic_client.py
@@ -0,0 +1,30 @@
+
+if __name__ == '__main__':
+ import basic_client
+ raise SystemExit(basic_client.main())
+
+from sys import stdout
+
+from twisted.python.log import startLogging, err
+from twisted.protocols.amp import AMP
+from twisted.internet import reactor
+from twisted.internet.protocol import Factory
+from twisted.internet.endpoints import TCP4ClientEndpoint
+
+def connect():
+ endpoint = TCP4ClientEndpoint(reactor, "127.0.0.1", 8750)
+ factory = Factory()
+ factory.protocol = AMP
+ return endpoint.connect(factory)
+
+
+def main():
+ startLogging(stdout)
+
+ d = connect()
+ d.addErrback(err, "Connection failed")
+ def done(ignored):
+ reactor.stop()
+ d.addCallback(done)
+
+ reactor.run()
diff --git a/doc/core/howto/listings/amp/basic_server.tac b/doc/core/howto/listings/amp/basic_server.tac
new file mode 100644
index 0000000..c28f663
--- /dev/null
+++ b/doc/core/howto/listings/amp/basic_server.tac
@@ -0,0 +1,14 @@
+from twisted.protocols.amp import AMP
+from twisted.internet import reactor
+from twisted.internet.protocol import Factory
+from twisted.internet.endpoints import TCP4ServerEndpoint
+from twisted.application.service import Application
+from twisted.application.internet import StreamServerEndpointService
+
+application = Application("basic AMP server")
+
+endpoint = TCP4ServerEndpoint(reactor, 8750)
+factory = Factory()
+factory.protocol = AMP
+service = StreamServerEndpointService(endpoint, factory)
+service.setServiceParent(application)
diff --git a/doc/core/howto/listings/amp/command_client.py b/doc/core/howto/listings/amp/command_client.py
new file mode 100644
index 0000000..8aa4cbd
--- /dev/null
+++ b/doc/core/howto/listings/amp/command_client.py
@@ -0,0 +1,48 @@
+
+if __name__ == '__main__':
+ import command_client
+ raise SystemExit(command_client.main())
+
+from sys import stdout
+
+from twisted.python.log import startLogging, err
+from twisted.protocols.amp import Integer, String, Unicode, Command
+from twisted.internet import reactor
+
+from basic_client import connect
+
+class UsernameUnavailable(Exception):
+ pass
+
+
+class RegisterUser(Command):
+ arguments = [('username', Unicode()),
+ ('publickey', String())]
+
+ response = [('uid', Integer())]
+
+ errors = {UsernameUnavailable: 'username-unavailable'}
+
+
+def main():
+ startLogging(stdout)
+
+ d = connect()
+ def connected(protocol):
+ return protocol.callRemote(
+ RegisterUser,
+ username=u'alice',
+ publickey='ssh-rsa AAAAB3NzaC1yc2 alice@actinium')
+ d.addCallback(connected)
+
+ def registered(result):
+ print 'Registration result:', result
+ d.addCallback(registered)
+
+ d.addErrback(err, "Failed to register")
+
+ def finished(ignored):
+ reactor.stop()
+ d.addCallback(finished)
+
+ reactor.run()
diff --git a/doc/core/howto/listings/application/service.tac b/doc/core/howto/listings/application/service.tac
new file mode 100644
index 0000000..b0167fa
--- /dev/null
+++ b/doc/core/howto/listings/application/service.tac
@@ -0,0 +1,34 @@
+# You can run this .tac file directly with:
+# twistd -ny service.tac
+
+"""
+This is an example .tac file which starts a webserver on port 8080 and
+serves files from the current working directory.
+
+The important part of this, the part that makes it a .tac file, is
+the final root-level section, which sets up the object called 'application'
+which twistd will look for
+"""
+
+import os
+from twisted.application import service, internet
+from twisted.web import static, server
+
+def getWebService():
+ """
+ Return a service suitable for creating an application object.
+
+ This service is a simple web server that serves files on port 8080 from
+ underneath the current working directory.
+ """
+ # create a resource to serve static files
+ fileServer = server.Site(static.File(os.getcwd()))
+ return internet.TCPServer(8080, fileServer)
+
+# this is the core part of any tac file, the creation of the root-level
+# application object
+application = service.Application("Demo application")
+
+# attach the service to its parent application
+service = getWebService()
+service.setServiceParent(application)
diff --git a/doc/core/howto/listings/deferred/synch-validation.py b/doc/core/howto/listings/deferred/synch-validation.py
new file mode 100644
index 0000000..2912f2b
--- /dev/null
+++ b/doc/core/howto/listings/deferred/synch-validation.py
@@ -0,0 +1,5 @@
+def synchronousIsValidUser(user):
+ '''
+ Return true if user is a valid user, false otherwise
+ '''
+ return user in ["Alice", "Angus", "Agnes"]
diff --git a/doc/core/howto/listings/pb/cache_classes.py b/doc/core/howto/listings/pb/cache_classes.py
new file mode 100755
index 0000000..0e3493e
--- /dev/null
+++ b/doc/core/howto/listings/pb/cache_classes.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+
+class MasterDuckPond(pb.Cacheable):
+ def __init__(self, ducks):
+ self.observers = []
+ self.ducks = ducks
+ def count(self):
+ print "I have [%d] ducks" % len(self.ducks)
+ def addDuck(self, duck):
+ self.ducks.append(duck)
+ for o in self.observers: o.callRemote('addDuck', duck)
+ def removeDuck(self, duck):
+ self.ducks.remove(duck)
+ for o in self.observers: o.callRemote('removeDuck', duck)
+ def getStateToCacheAndObserveFor(self, perspective, observer):
+ self.observers.append(observer)
+ # you should ignore pb.Cacheable-specific state, like self.observers
+ return self.ducks # in this case, just a list of ducks
+ def stoppedObserving(self, perspective, observer):
+ self.observers.remove(observer)
+
+class SlaveDuckPond(pb.RemoteCache):
+ # This is a cache of a remote MasterDuckPond
+ def count(self):
+ return len(self.cacheducks)
+ def getDucks(self):
+ return self.cacheducks
+ def setCopyableState(self, state):
+ print " cache - sitting, er, setting ducks"
+ self.cacheducks = state
+ def observe_addDuck(self, newDuck):
+ print " cache - addDuck"
+ self.cacheducks.append(newDuck)
+ def observe_removeDuck(self, deadDuck):
+ print " cache - removeDuck"
+ self.cacheducks.remove(deadDuck)
+
+pb.setUnjellyableForClass(MasterDuckPond, SlaveDuckPond)
diff --git a/doc/core/howto/listings/pb/cache_receiver.py b/doc/core/howto/listings/pb/cache_receiver.py
new file mode 100755
index 0000000..b6d930b
--- /dev/null
+++ b/doc/core/howto/listings/pb/cache_receiver.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application import service, internet
+from twisted.internet import reactor
+from twisted.spread import pb
+import cache_classes
+
+class Receiver(pb.Root):
+ def remote_takePond(self, pond):
+ self.pond = pond
+ print "got pond:", pond # a DuckPondCache
+ self.remote_checkDucks()
+ def remote_checkDucks(self):
+ print "[%d] ducks: " % self.pond.count(), self.pond.getDucks()
+ def remote_ignorePond(self):
+ # stop watching the pond
+ print "dropping pond"
+ # gc causes __del__ causes 'decache' msg causes stoppedObserving
+ self.pond = None
+ def remote_shutdown(self):
+ reactor.stop()
+
+application = service.Application("copy_receiver")
+internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
+ service.IServiceCollection(application))
diff --git a/doc/core/howto/listings/pb/cache_sender.py b/doc/core/howto/listings/pb/cache_sender.py
new file mode 100755
index 0000000..391143a
--- /dev/null
+++ b/doc/core/howto/listings/pb/cache_sender.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb, jelly
+from twisted.python import log
+from twisted.internet import reactor
+from cache_classes import MasterDuckPond
+
+class Sender:
+ def __init__(self, pond):
+ self.pond = pond
+
+ def phase1(self, remote):
+ self.remote = remote
+ d = remote.callRemote("takePond", self.pond)
+ d.addCallback(self.phase2).addErrback(log.err)
+ def phase2(self, response):
+ self.pond.addDuck("ugly duckling")
+ self.pond.count()
+ reactor.callLater(1, self.phase3)
+ def phase3(self):
+ d = self.remote.callRemote("checkDucks")
+ d.addCallback(self.phase4).addErrback(log.err)
+ def phase4(self, dummy):
+ self.pond.removeDuck("one duck")
+ self.pond.count()
+ self.remote.callRemote("checkDucks")
+ d = self.remote.callRemote("ignorePond")
+ d.addCallback(self.phase5)
+ def phase5(self, dummy):
+ d = self.remote.callRemote("shutdown")
+ d.addCallback(self.phase6)
+ def phase6(self, dummy):
+ reactor.stop()
+
+def main():
+ master = MasterDuckPond(["one duck", "two duck"])
+ master.count()
+
+ sender = Sender(master)
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ deferred = factory.getRootObject()
+ deferred.addCallback(sender.phase1)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/pb/chatclient.py b/doc/core/howto/listings/pb/chatclient.py
new file mode 100755
index 0000000..eb2f677
--- /dev/null
+++ b/doc/core/howto/listings/pb/chatclient.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred import credentials
+
+class Client(pb.Referenceable):
+
+ def remote_print(self, message):
+ print message
+
+ def connect(self):
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.login(credentials.UsernamePassword("alice", "1234"),
+ client=self)
+ def1.addCallback(self.connected)
+ reactor.run()
+
+ def connected(self, perspective):
+ print "connected, joining group #NeedAFourth"
+ # this perspective is a reference to our User object. Save a reference
+ # to it here, otherwise it will get garbage collected after this call,
+ # and the server will think we logged out.
+ self.perspective = perspective
+ d = perspective.callRemote("joinGroup", "#NeedAFourth")
+ d.addCallback(self.gotGroup)
+
+ def gotGroup(self, group):
+ print "joined group, now sending a message to all members"
+ # 'group' is a reference to the Group object (through a ViewPoint)
+ d = group.callRemote("send", "You can call me Al.")
+ d.addCallback(self.shutdown)
+
+ def shutdown(self, result):
+ reactor.stop()
+
+
+Client().connect()
+
diff --git a/doc/core/howto/listings/pb/chatserver.py b/doc/core/howto/listings/pb/chatserver.py
new file mode 100755
index 0000000..ff70d2a
--- /dev/null
+++ b/doc/core/howto/listings/pb/chatserver.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+
+from twisted.cred import portal, checkers
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class ChatServer:
+ def __init__(self):
+ self.groups = {} # indexed by name
+
+ def joinGroup(self, groupname, user, allowMattress):
+ if not self.groups.has_key(groupname):
+ self.groups[groupname] = Group(groupname, allowMattress)
+ self.groups[groupname].addUser(user)
+ return self.groups[groupname]
+
+class ChatRealm:
+ implements(portal.IRealm)
+ def requestAvatar(self, avatarID, mind, *interfaces):
+ assert pb.IPerspective in interfaces
+ avatar = User(avatarID)
+ avatar.server = self.server
+ avatar.attached(mind)
+ return pb.IPerspective, avatar, lambda a=avatar:a.detached(mind)
+
+class User(pb.Avatar):
+ def __init__(self, name):
+ self.name = name
+ def attached(self, mind):
+ self.remote = mind
+ def detached(self, mind):
+ self.remote = None
+ def perspective_joinGroup(self, groupname, allowMattress=True):
+ return self.server.joinGroup(groupname, self, allowMattress)
+ def send(self, message):
+ self.remote.callRemote("print", message)
+
+class Group(pb.Viewable):
+ def __init__(self, groupname, allowMattress):
+ self.name = groupname
+ self.allowMattress = allowMattress
+ self.users = []
+ def addUser(self, user):
+ self.users.append(user)
+ def view_send(self, from_user, message):
+ if not self.allowMattress and "mattress" in message:
+ raise ValueError, "Don't say that word"
+ for user in self.users:
+ user.send("<%s> says: %s" % (from_user.name, message))
+
+realm = ChatRealm()
+realm.server = ChatServer()
+checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+checker.addUser("alice", "1234")
+checker.addUser("bob", "secret")
+checker.addUser("carol", "fido")
+p = portal.Portal(realm, [checker])
+
+reactor.listenTCP(8800, pb.PBServerFactory(p))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/copy2_classes.py b/doc/core/howto/listings/pb/copy2_classes.py
new file mode 100755
index 0000000..8b11e14
--- /dev/null
+++ b/doc/core/howto/listings/pb/copy2_classes.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+
+class FrogPond:
+ def __init__(self, numFrogs, numToads):
+ self.numFrogs = numFrogs
+ self.numToads = numToads
+ def count(self):
+ return self.numFrogs + self.numToads
+
+class SenderPond(FrogPond, pb.Copyable):
+ def getStateToCopy(self):
+ d = self.__dict__.copy()
+ d['frogsAndToads'] = d['numFrogs'] + d['numToads']
+ del d['numFrogs']
+ del d['numToads']
+ return d
+
+class ReceiverPond(pb.RemoteCopy):
+ def setCopyableState(self, state):
+ self.__dict__ = state
+ def count(self):
+ return self.frogsAndToads
+
+pb.setUnjellyableForClass(SenderPond, ReceiverPond)
diff --git a/doc/core/howto/listings/pb/copy2_receiver.py b/doc/core/howto/listings/pb/copy2_receiver.py
new file mode 100755
index 0000000..76f06c3
--- /dev/null
+++ b/doc/core/howto/listings/pb/copy2_receiver.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application import service, internet
+from twisted.internet import reactor
+from twisted.spread import pb
+import copy2_classes # needed to get ReceiverPond registered with Jelly
+
+class Receiver(pb.Root):
+ def remote_takePond(self, pond):
+ print " got pond:", pond
+ print " count %d" % pond.count()
+ return "safe and sound" # positive acknowledgement
+ def remote_shutdown(self):
+ reactor.stop()
+
+application = service.Application("copy_receiver")
+internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
+ service.IServiceCollection(application))
diff --git a/doc/core/howto/listings/pb/copy2_sender.py b/doc/core/howto/listings/pb/copy2_sender.py
new file mode 100755
index 0000000..5b008b7
--- /dev/null
+++ b/doc/core/howto/listings/pb/copy2_sender.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb, jelly
+from twisted.python import log
+from twisted.internet import reactor
+from copy2_classes import SenderPond
+
+class Sender:
+ def __init__(self, pond):
+ self.pond = pond
+
+ def got_obj(self, obj):
+ d = obj.callRemote("takePond", self.pond)
+ d.addCallback(self.ok).addErrback(self.notOk)
+
+ def ok(self, response):
+ print "pond arrived", response
+ reactor.stop()
+ def notOk(self, failure):
+ print "error during takePond:"
+ if failure.type == jelly.InsecureJelly:
+ print " InsecureJelly"
+ else:
+ print failure
+ reactor.stop()
+ return None
+
+def main():
+ pond = SenderPond(3, 4)
+ print "count %d" % pond.count()
+
+ sender = Sender(pond)
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ deferred = factory.getRootObject()
+ deferred.addCallback(sender.got_obj)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
+
diff --git a/doc/core/howto/listings/pb/copy_receiver.tac b/doc/core/howto/listings/pb/copy_receiver.tac
new file mode 100755
index 0000000..dc9905e
--- /dev/null
+++ b/doc/core/howto/listings/pb/copy_receiver.tac
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+PB copy receiver example.
+
+This is a Twisted Application Configuration (tac) file. Run with e.g.
+ twistd -ny copy_receiver.tac
+
+See the twistd(1) man page or
+http://twistedmatrix.com/documents/current/howto/application for details.
+"""
+
+import sys
+if __name__ == '__main__':
+ print __doc__
+ sys.exit(1)
+
+from twisted.application import service, internet
+from twisted.internet import reactor
+from twisted.spread import pb
+from copy_sender import LilyPond, CopyPond
+
+from twisted.python import log
+#log.startLogging(sys.stdout)
+
+class ReceiverPond(pb.RemoteCopy, LilyPond):
+ pass
+pb.setUnjellyableForClass(CopyPond, ReceiverPond)
+
+class Receiver(pb.Root):
+ def remote_takePond(self, pond):
+ print " got pond:", pond
+ pond.countFrogs()
+ return "safe and sound" # positive acknowledgement
+ def remote_shutdown(self):
+ reactor.stop()
+
+application = service.Application("copy_receiver")
+internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
+ service.IServiceCollection(application))
diff --git a/doc/core/howto/listings/pb/copy_sender.py b/doc/core/howto/listings/pb/copy_sender.py
new file mode 100755
index 0000000..fca0594
--- /dev/null
+++ b/doc/core/howto/listings/pb/copy_sender.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb, jelly
+from twisted.python import log
+from twisted.internet import reactor
+
+class LilyPond:
+ def setStuff(self, color, numFrogs):
+ self.color = color
+ self.numFrogs = numFrogs
+ def countFrogs(self):
+ print "%d frogs" % self.numFrogs
+
+class CopyPond(LilyPond, pb.Copyable):
+ pass
+
+class Sender:
+ def __init__(self, pond):
+ self.pond = pond
+
+ def got_obj(self, remote):
+ self.remote = remote
+ d = remote.callRemote("takePond", self.pond)
+ d.addCallback(self.ok).addErrback(self.notOk)
+
+ def ok(self, response):
+ print "pond arrived", response
+ reactor.stop()
+ def notOk(self, failure):
+ print "error during takePond:"
+ if failure.type == jelly.InsecureJelly:
+ print " InsecureJelly"
+ else:
+ print failure
+ reactor.stop()
+ return None
+
+def main():
+ from copy_sender import CopyPond # so it's not __main__.CopyPond
+ pond = CopyPond()
+ pond.setStuff("green", 7)
+ pond.countFrogs()
+ # class name:
+ print ".".join([pond.__class__.__module__, pond.__class__.__name__])
+
+ sender = Sender(pond)
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ deferred = factory.getRootObject()
+ deferred.addCallback(sender.got_obj)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/pb/exc_client.py b/doc/core/howto/listings/pb/exc_client.py
new file mode 100755
index 0000000..cedd107
--- /dev/null
+++ b/doc/core/howto/listings/pb/exc_client.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def main():
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ d = factory.getRootObject()
+ d.addCallbacks(got_obj)
+ reactor.run()
+
+def got_obj(obj):
+ # change "broken" into "broken2" to demonstrate an unhandled exception
+ d2 = obj.callRemote("broken")
+ d2.addCallback(working)
+ d2.addErrback(broken)
+
+def working():
+ print "erm, it wasn't *supposed* to work.."
+
+def broken(reason):
+ print "got remote Exception"
+ # reason should be a Failure (or subclass) holding the MyError exception
+ print " .__class__ =", reason.__class__
+ print " .getErrorMessage() =", reason.getErrorMessage()
+ print " .type =", reason.type
+ reactor.stop()
+
+main()
diff --git a/doc/core/howto/listings/pb/exc_server.py b/doc/core/howto/listings/pb/exc_server.py
new file mode 100755
index 0000000..9cb3dab
--- /dev/null
+++ b/doc/core/howto/listings/pb/exc_server.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class MyError(pb.Error):
+ """This is an Expected Exception. Something bad happened."""
+ pass
+
+class MyError2(Exception):
+ """This is an Unexpected Exception. Something really bad happened."""
+ pass
+
+class One(pb.Root):
+ def remote_broken(self):
+ msg = "fall down go boom"
+ print "raising a MyError exception with data '%s'" % msg
+ raise MyError(msg)
+ def remote_broken2(self):
+ msg = "hadda owie"
+ print "raising a MyError2 exception with data '%s'" % msg
+ raise MyError2(msg)
+
+def main():
+ reactor.listenTCP(8800, pb.PBServerFactory(One()))
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/pb/pb1client.py b/doc/core/howto/listings/pb/pb1client.py
new file mode 100755
index 0000000..8525f16
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb1client.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def main():
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.getRootObject()
+ def1.addCallbacks(got_obj1, err_obj1)
+ reactor.run()
+
+def err_obj1(reason):
+ print "error getting first object", reason
+ reactor.stop()
+
+def got_obj1(obj1):
+ print "got first object:", obj1
+ print "asking it to getTwo"
+ def2 = obj1.callRemote("getTwo")
+ def2.addCallbacks(got_obj2)
+
+def got_obj2(obj2):
+ print "got second object:", obj2
+ print "telling it to do three(12)"
+ obj2.callRemote("three", 12)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb1server.py b/doc/core/howto/listings/pb/pb1server.py
new file mode 100755
index 0000000..d927489
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb1server.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+
+class Two(pb.Referenceable):
+ def remote_three(self, arg):
+ print "Two.three was given", arg
+
+class One(pb.Root):
+ def remote_getTwo(self):
+ two = Two()
+ print "returning a Two called", two
+ return two
+
+from twisted.internet import reactor
+reactor.listenTCP(8800, pb.PBServerFactory(One()))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/pb2client.py b/doc/core/howto/listings/pb/pb2client.py
new file mode 100755
index 0000000..0164447
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb2client.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def main():
+ foo = Foo()
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ factory.getRootObject().addCallback(foo.step1)
+ reactor.run()
+
+# keeping globals around is starting to get ugly, so we use a simple class
+# instead. Instead of hooking one function to the next, we hook one method
+# to the next.
+
+class Foo:
+ def __init__(self):
+ self.oneRef = None
+
+ def step1(self, obj):
+ print "got one object:", obj
+ self.oneRef = obj
+ print "asking it to getTwo"
+ self.oneRef.callRemote("getTwo").addCallback(self.step2)
+
+ def step2(self, two):
+ print "got two object:", two
+ print "giving it back to one"
+ print "one is", self.oneRef
+ self.oneRef.callRemote("checkTwo", two)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb2server.py b/doc/core/howto/listings/pb/pb2server.py
new file mode 100755
index 0000000..c32344e
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb2server.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class Two(pb.Referenceable):
+ def remote_print(self, arg):
+ print "two.print was given", arg
+
+class One(pb.Root):
+ def __init__(self, two):
+ #pb.Root.__init__(self) # pb.Root doesn't implement __init__
+ self.two = two
+ def remote_getTwo(self):
+ print "One.getTwo(), returning my two called", self.two
+ return self.two
+ def remote_checkTwo(self, newtwo):
+ print "One.checkTwo(): comparing my two", self.two
+ print "One.checkTwo(): against your two", newtwo
+ if self.two == newtwo:
+ print "One.checkTwo(): our twos are the same"
+
+
+two = Two()
+root_obj = One(two)
+reactor.listenTCP(8800, pb.PBServerFactory(root_obj))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/pb3client.py b/doc/core/howto/listings/pb/pb3client.py
new file mode 100755
index 0000000..28a9749
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb3client.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class Two(pb.Referenceable):
+ def remote_print(self, arg):
+ print "Two.print() called with", arg
+
+def main():
+ two = Two()
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.getRootObject()
+ def1.addCallback(got_obj, two) # hands our 'two' to the callback
+ reactor.run()
+
+def got_obj(obj, two):
+ print "got One:", obj
+ print "giving it our two"
+ obj.callRemote("takeTwo", two)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb3server.py b/doc/core/howto/listings/pb/pb3server.py
new file mode 100755
index 0000000..f71b4a1
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb3server.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class One(pb.Root):
+ def remote_takeTwo(self, two):
+ print "received a Two called", two
+ print "telling it to print(12)"
+ two.callRemote("print", 12)
+
+reactor.listenTCP(8800, pb.PBServerFactory(One()))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/pb4client.py b/doc/core/howto/listings/pb/pb4client.py
new file mode 100755
index 0000000..10f2694
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb4client.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def main():
+ rootobj_def = pb.getObjectAt("localhost", 8800, 30)
+ rootobj_def.addCallbacks(got_rootobj)
+ obj2_def = getSomeObjectAt("localhost", 8800, 30, "two")
+ obj2_def.addCallbacks(got_obj2)
+ obj3_def = getSomeObjectAt("localhost", 8800, 30, "three")
+ obj3_def.addCallbacks(got_obj3)
+ reactor.run()
+
+def got_rootobj(rootobj):
+ print "got root object:", rootobj
+ print "telling root object to do foo(A)"
+ rootobj.callRemote("foo", "A")
+
+def got_obj2(obj2):
+ print "got second object:", obj2
+ print "telling second object to do foo(B)"
+ obj2.callRemote("foo", "B")
+
+def got_obj3(obj3):
+ print "got third object:", obj3
+ print "telling third object to do foo(C)"
+ obj3.callRemote("foo", "C")
+
+class my_ObjectRetrieval(pb._ObjectRetrieval):
+ def __init__(self, broker, d, objname):
+ pb._ObjectRetrieval.__init__(self, broker, d)
+ self.objname = objname
+ def connectionMade(self):
+ assert not self.term, "How did this get called?"
+ x = self.broker.remoteForName(self.objname)
+ del self.broker
+ self.term = 1
+ self.deferred.callback(x)
+
+def getSomeObjectAt(host, port, timeout=None, objname="root"):
+ from twisted.internet import defer
+ from twisted.spread.pb import Broker, BrokerClientFactory
+ d = defer.Deferred()
+ b = Broker(1)
+ bf = BrokerClientFactory(b)
+ my_ObjectRetrieval(b, d, objname)
+ if host == "unix":
+ # every time you use this, God kills a kitten
+ reactor.connectUNIX(port, bf, timeout)
+ else:
+ reactor.connectTCP(host, port, bf, timeout)
+ return d
+
+main()
diff --git a/doc/core/howto/listings/pb/pb5client.py b/doc/core/howto/listings/pb/pb5client.py
new file mode 100755
index 0000000..3026780
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb5client.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred import credentials
+
+def main():
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.login(credentials.UsernamePassword("user1", "pass1"))
+ def1.addCallback(connected)
+ reactor.run()
+
+def connected(perspective):
+ print "got perspective ref:", perspective
+ print "asking it to foo(12)"
+ perspective.callRemote("foo", 12)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb5server.py b/doc/core/howto/listings/pb/pb5server.py
new file mode 100755
index 0000000..dc95f6e
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb5server.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+
+from twisted.spread import pb
+from twisted.cred import checkers, portal
+from twisted.internet import reactor
+
+class MyPerspective(pb.Avatar):
+ def __init__(self, name):
+ self.name = name
+ def perspective_foo(self, arg):
+ print "I am", self.name, "perspective_foo(",arg,") called on", self
+
+class MyRealm:
+ implements(portal.IRealm)
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pb.IPerspective not in interfaces:
+ raise NotImplementedError
+ return pb.IPerspective, MyPerspective(avatarId), lambda:None
+
+p = portal.Portal(MyRealm())
+p.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user1="pass1"))
+reactor.listenTCP(8800, pb.PBServerFactory(p))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/pb6client1.py b/doc/core/howto/listings/pb/pb6client1.py
new file mode 100755
index 0000000..38ae65a
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb6client1.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred import credentials
+
+def main():
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.login(credentials.UsernamePassword("user1", "pass1"))
+ def1.addCallback(connected)
+ reactor.run()
+
+def connected(perspective):
+ print "got perspective1 ref:", perspective
+ print "asking it to foo(13)"
+ perspective.callRemote("foo", 13)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb6client2.py b/doc/core/howto/listings/pb/pb6client2.py
new file mode 100755
index 0000000..ecceb2a
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb6client2.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred import credentials
+
+def main():
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+ def1 = factory.login(credentials.UsernamePassword("user2", "pass2"))
+ def1.addCallback(connected)
+ reactor.run()
+
+def connected(perspective):
+ print "got perspective2 ref:", perspective
+ print "asking it to foo(14)"
+ perspective.callRemote("foo", 14)
+
+main()
diff --git a/doc/core/howto/listings/pb/pb6server.py b/doc/core/howto/listings/pb/pb6server.py
new file mode 100755
index 0000000..9c98d5e
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb6server.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+
+from twisted.spread import pb
+from twisted.cred import checkers, portal
+from twisted.internet import reactor
+
+class MyPerspective(pb.Avatar):
+ def __init__(self, name):
+ self.name = name
+ def perspective_foo(self, arg):
+ print "I am", self.name, "perspective_foo(",arg,") called on", self
+
+class MyRealm:
+ implements(portal.IRealm)
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pb.IPerspective not in interfaces:
+ raise NotImplementedError
+ return pb.IPerspective, MyPerspective(avatarId), lambda:None
+
+p = portal.Portal(MyRealm())
+c = checkers.InMemoryUsernamePasswordDatabaseDontUse(user1="pass1",
+ user2="pass2")
+p.registerChecker(c)
+reactor.listenTCP(8800, pb.PBServerFactory(p))
+reactor.run()
diff --git a/doc/core/howto/listings/pb/pb7client.py b/doc/core/howto/listings/pb/pb7client.py
new file mode 100755
index 0000000..e212fb2
--- /dev/null
+++ b/doc/core/howto/listings/pb/pb7client.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def one(port, user, pw, service, perspective, number):
+ factory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", port, factory)
+ def1 = factory.getPerspective(
+ user, pw, service, perspective)
+ def1.addCallback(connected, number)
+
+def connected(perspective, number):
+ print "got perspective ref:", perspective
+ print "asking it to foo(%d)" % number
+ perspective.callRemote("foo", number)
+
+def main():
+ one(8800, "user1", "pass1", "service1", "perspective1.1", 10)
+ one(8800, "user1", "pass1", "service2", "perspective2.1", 11)
+ one(8800, "user2", "pass2", "service1", "perspective1.2", 12)
+ one(8800, "user2", "pass2", "service2", "perspective2.2", 13)
+ one(8801, "user3", "pass3", "service3", "perspective3.3", 14)
+ reactor.run()
+
+main()
diff --git a/doc/core/howto/listings/pb/pbAnonClient.py b/doc/core/howto/listings/pb/pbAnonClient.py
new file mode 100755
index 0000000..fbd9a9f
--- /dev/null
+++ b/doc/core/howto/listings/pb/pbAnonClient.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Client which will talk to the server run by pbAnonServer.py, logging in
+either anonymously or with username/password credentials.
+"""
+
+from sys import stdout
+
+from twisted.python.log import err, startLogging
+from twisted.cred.credentials import Anonymous, UsernamePassword
+from twisted.internet import reactor
+from twisted.internet.defer import gatherResults
+from twisted.spread.pb import PBClientFactory
+
+
+def error(why, msg):
+ """
+ Catch-all errback which simply logs the failure. This isn't expected to
+ be invoked in the normal case for this example.
+ """
+ err(why, msg)
+
+
+def connected(perspective):
+ """
+ Login callback which invokes the remote "foo" method on the perspective
+ which the server returned.
+ """
+ print "got perspective1 ref:", perspective
+ print "asking it to foo(13)"
+ return perspective.callRemote("foo", 13)
+
+
+def finished(ignored):
+ """
+ Callback invoked when both logins and method calls have finished to shut
+ down the reactor so the example exits.
+ """
+ reactor.stop()
+
+
+def main():
+ """
+ Connect to a PB server running on port 8800 on localhost and log in to
+ it, both anonymously and using a username/password it will recognize.
+ """
+ startLogging(stdout)
+ factory = PBClientFactory()
+ reactor.connectTCP("localhost", 8800, factory)
+
+ anonymousLogin = factory.login(Anonymous())
+ anonymousLogin.addCallback(connected)
+ anonymousLogin.addErrback(error, "Anonymous login failed")
+
+ usernameLogin = factory.login(UsernamePassword("user1", "pass1"))
+ usernameLogin.addCallback(connected)
+ usernameLogin.addErrback(error, "Username/password login failed")
+
+ bothDeferreds = gatherResults([anonymousLogin, usernameLogin])
+ bothDeferreds.addCallback(finished)
+
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/pb/pbAnonServer.py b/doc/core/howto/listings/pb/pbAnonServer.py
new file mode 100755
index 0000000..f5eadac
--- /dev/null
+++ b/doc/core/howto/listings/pb/pbAnonServer.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implement the realm for and run on port 8800 a PB service which allows both
+anonymous and username/password based access.
+
+Successful username/password-based login requests given an instance of
+MyPerspective with a name which matches the username with which they
+authenticated. Success anonymous login requests are given an instance of
+MyPerspective with the name "Anonymous".
+"""
+
+from sys import stdout
+
+from zope.interface import implements
+
+from twisted.python.log import startLogging
+from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.portal import IRealm, Portal
+from twisted.internet import reactor
+from twisted.spread.pb import Avatar, IPerspective, PBServerFactory
+
+
+class MyPerspective(Avatar):
+ """
+ Trivial avatar exposing a single remote method for demonstrative
+ purposes. All successful login attempts in this example will result in
+ an avatar which is an instance of this class.
+
+ @type name: C{str}
+ @ivar name: The username which was used during login or C{"Anonymous"}
+ if the login was anonymous (a real service might want to avoid the
+ collision this introduces between anonoymous users and authenticated
+ users named "Anonymous").
+ """
+ def __init__(self, name):
+ self.name = name
+
+
+ def perspective_foo(self, arg):
+ """
+ Print a simple message which gives the argument this method was
+ called with and this avatar's name.
+ """
+ print "I am %s. perspective_foo(%s) called on %s." % (
+ self.name, arg, self)
+
+
+
+class MyRealm(object):
+ """
+ Trivial realm which supports anonymous and named users by creating
+ avatars which are instances of MyPerspective for either.
+ """
+ implements(IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IPerspective not in interfaces:
+ raise NotImplementedError("MyRealm only handles IPerspective")
+ if avatarId is ANONYMOUS:
+ avatarId = "Anonymous"
+ return IPerspective, MyPerspective(avatarId), lambda: None
+
+
+
+def main():
+ """
+ Create a PB server using MyRealm and run it on port 8800.
+ """
+ startLogging(stdout)
+
+ p = Portal(MyRealm())
+
+ # Here the username/password checker is registered.
+ c1 = InMemoryUsernamePasswordDatabaseDontUse(user1="pass1", user2="pass2")
+ p.registerChecker(c1)
+
+ # Here the anonymous checker is registered.
+ c2 = AllowAnonymousAccess()
+ p.registerChecker(c2)
+
+ reactor.listenTCP(8800, PBServerFactory(p))
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/pb/trap_client.py b/doc/core/howto/listings/pb/trap_client.py
new file mode 100755
index 0000000..7fb2a9a
--- /dev/null
+++ b/doc/core/howto/listings/pb/trap_client.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.spread import pb, jelly
+from twisted.python import log
+from twisted.internet import reactor
+
+class MyException(pb.Error): pass
+class MyOtherException(pb.Error): pass
+
+class ScaryObject:
+ # not safe for serialization
+ pass
+
+def worksLike(obj):
+ # the callback/errback sequence in class One works just like an
+ # asynchronous version of the following:
+ try:
+ response = obj.callMethod(name, arg)
+ except pb.DeadReferenceError:
+ print " stale reference: the client disconnected or crashed"
+ except jelly.InsecureJelly:
+ print " InsecureJelly: you tried to send something unsafe to them"
+ except (MyException, MyOtherException):
+ print " remote raised a MyException" # or MyOtherException
+ except:
+ print " something else happened"
+ else:
+ print " method successful, response:", response
+
+class One:
+ def worked(self, response):
+ print " method successful, response:", response
+ def check_InsecureJelly(self, failure):
+ failure.trap(jelly.InsecureJelly)
+ print " InsecureJelly: you tried to send something unsafe to them"
+ return None
+ def check_MyException(self, failure):
+ which = failure.trap(MyException, MyOtherException)
+ if which == MyException:
+ print " remote raised a MyException"
+ else:
+ print " remote raised a MyOtherException"
+ return None
+ def catch_everythingElse(self, failure):
+ print " something else happened"
+ log.err(failure)
+ return None
+
+ def doCall(self, explanation, arg):
+ print explanation
+ try:
+ deferred = self.remote.callRemote("fooMethod", arg)
+ deferred.addCallback(self.worked)
+ deferred.addErrback(self.check_InsecureJelly)
+ deferred.addErrback(self.check_MyException)
+ deferred.addErrback(self.catch_everythingElse)
+ except pb.DeadReferenceError:
+ print " stale reference: the client disconnected or crashed"
+
+ def callOne(self):
+ self.doCall("callOne: call with safe object", "safe string")
+ def callTwo(self):
+ self.doCall("callTwo: call with dangerous object", ScaryObject())
+ def callThree(self):
+ self.doCall("callThree: call that raises remote exception", "panic!")
+ def callShutdown(self):
+ print "telling them to shut down"
+ self.remote.callRemote("shutdown")
+ def callFour(self):
+ self.doCall("callFour: call on stale reference", "dummy")
+
+ def got_obj(self, obj):
+ self.remote = obj
+ reactor.callLater(1, self.callOne)
+ reactor.callLater(2, self.callTwo)
+ reactor.callLater(3, self.callThree)
+ reactor.callLater(4, self.callShutdown)
+ reactor.callLater(5, self.callFour)
+ reactor.callLater(6, reactor.stop)
+
+factory = pb.PBClientFactory()
+reactor.connectTCP("localhost", 8800, factory)
+deferred = factory.getRootObject()
+deferred.addCallback(One().got_obj)
+reactor.run()
diff --git a/doc/core/howto/listings/pb/trap_server.py b/doc/core/howto/listings/pb/trap_server.py
new file mode 100755
index 0000000..03bd92c
--- /dev/null
+++ b/doc/core/howto/listings/pb/trap_server.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet import reactor
+from twisted.spread import pb
+
+class MyException(pb.Error):
+ pass
+
+class One(pb.Root):
+ def remote_fooMethod(self, arg):
+ if arg == "panic!":
+ raise MyException
+ return "response"
+ def remote_shutdown(self):
+ reactor.stop()
+
+reactor.listenTCP(8800, pb.PBServerFactory(One()))
+reactor.run()
diff --git a/doc/core/howto/listings/process/process.py b/doc/core/howto/listings/process/process.py
new file mode 100755
index 0000000..95579e5
--- /dev/null
+++ b/doc/core/howto/listings/process/process.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet import protocol
+from twisted.internet import reactor
+import re
+
+class MyPP(protocol.ProcessProtocol):
+ def __init__(self, verses):
+ self.verses = verses
+ self.data = ""
+ def connectionMade(self):
+ print "connectionMade!"
+ for i in range(self.verses):
+ self.transport.write("Aleph-null bottles of beer on the wall,\n" +
+ "Aleph-null bottles of beer,\n" +
+ "Take one down and pass it around,\n" +
+ "Aleph-null bottles of beer on the wall.\n")
+ self.transport.closeStdin() # tell them we're done
+ def outReceived(self, data):
+ print "outReceived! with %d bytes!" % len(data)
+ self.data = self.data + data
+ def errReceived(self, data):
+ print "errReceived! with %d bytes!" % len(data)
+ def inConnectionLost(self):
+ print "inConnectionLost! stdin is closed! (we probably did it)"
+ def outConnectionLost(self):
+ print "outConnectionLost! The child closed their stdout!"
+ # now is the time to examine what they wrote
+ #print "I saw them write:", self.data
+ (dummy, lines, words, chars, file) = re.split(r'\s+', self.data)
+ print "I saw %s lines" % lines
+ def errConnectionLost(self):
+ print "errConnectionLost! The child closed their stderr."
+ def processExited(self, reason):
+ print "processExited, status %d" % (reason.value.exitCode,)
+ def processEnded(self, reason):
+ print "processEnded, status %d" % (reason.value.exitCode,)
+ print "quitting"
+ reactor.stop()
+
+pp = MyPP(10)
+reactor.spawnProcess(pp, "wc", ["wc"], {})
+reactor.run()
diff --git a/doc/core/howto/listings/process/quotes.py b/doc/core/howto/listings/process/quotes.py
new file mode 100644
index 0000000..c0efeaf
--- /dev/null
+++ b/doc/core/howto/listings/process/quotes.py
@@ -0,0 +1,25 @@
+from twisted.internet import protocol, utils, reactor
+from twisted.python import failure
+from cStringIO import StringIO
+
+class FortuneQuoter(protocol.Protocol):
+
+ fortune = '/usr/games/fortune'
+
+ def connectionMade(self):
+ output = utils.getProcessOutput(self.fortune)
+ output.addCallbacks(self.writeResponse, self.noResponse)
+
+ def writeResponse(self, resp):
+ self.transport.write(resp)
+ self.transport.loseConnection()
+
+ def noResponse(self, err):
+ self.transport.loseConnection()
+
+
+if __name__ == '__main__':
+ f = protocol.Factory()
+ f.protocol = FortuneQuoter
+ reactor.listenTCP(10999, f)
+ reactor.run()
diff --git a/doc/core/howto/listings/process/trueandfalse.py b/doc/core/howto/listings/process/trueandfalse.py
new file mode 100644
index 0000000..4962c93
--- /dev/null
+++ b/doc/core/howto/listings/process/trueandfalse.py
@@ -0,0 +1,14 @@
+from twisted.internet import utils, reactor
+
+def printTrueValue(val):
+ print "/bin/true exits with rc=%d" % val
+ output = utils.getProcessValue('/bin/false')
+ output.addCallback(printFalseValue)
+
+def printFalseValue(val):
+ print "/bin/false exits with rc=%d" % val
+ reactor.stop()
+
+output = utils.getProcessValue('/bin/true')
+output.addCallback(printTrueValue)
+reactor.run()
diff --git a/doc/core/howto/listings/sendmsg/copy_descriptor.py b/doc/core/howto/listings/sendmsg/copy_descriptor.py
new file mode 100644
index 0000000..1c9a774
--- /dev/null
+++ b/doc/core/howto/listings/sendmsg/copy_descriptor.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Demonstration of copying a file descriptor over an AF_UNIX connection using
+sendmsg.
+"""
+
+from os import pipe, read, write
+from socket import SOL_SOCKET, socketpair
+from struct import unpack, pack
+
+from twisted.python.sendmsg import SCM_RIGHTS, send1msg, recv1msg
+
+def main():
+ foo, bar = socketpair()
+ reader, writer = pipe()
+
+ # Send a copy of the descriptor. Notice that there must be at least one
+ # byte of normal data passed in.
+ sent = send1msg(
+ foo.fileno(), "\x00", 0,
+ [(SOL_SOCKET, SCM_RIGHTS, pack("i", reader))])
+
+ # Receive the copy, including that one byte of normal data.
+ data, flags, ancillary = recv1msg(bar.fileno(), 1024)
+ duplicate = unpack("i", ancillary[0][2])[0]
+
+ # Demonstrate that the copy works just like the original
+ write(writer, "Hello, world")
+ print "Read from original (%d): %r" % (reader, read(reader, 6))
+ print "Read from duplicate (%d): %r" % (duplicate, read(duplicate, 6))
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/sendmsg/send_replacement.py b/doc/core/howto/listings/sendmsg/send_replacement.py
new file mode 100644
index 0000000..9460c8e
--- /dev/null
+++ b/doc/core/howto/listings/sendmsg/send_replacement.py
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Demonstration of sending bytes over a TCP connection using sendmsg.
+"""
+
+from socket import socketpair
+
+from twisted.python.sendmsg import send1msg, recv1msg
+
+def main():
+ foo, bar = socketpair()
+ sent = send1msg(foo.fileno(), "Hello, world")
+ print "Sent", sent, "bytes"
+ (received, flags, ancillary) = recv1msg(bar.fileno(), 1024)
+ print "Received", repr(received)
+ print "Extra stuff, boring in this case", flags, ancillary
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/core/howto/listings/servers/chat.py b/doc/core/howto/listings/servers/chat.py
new file mode 100644
index 0000000..01795bf
--- /dev/null
+++ b/doc/core/howto/listings/servers/chat.py
@@ -0,0 +1,51 @@
+from twisted.internet.protocol import Factory
+from twisted.protocols.basic import LineReceiver
+from twisted.internet import reactor
+
+class Chat(LineReceiver):
+
+ def __init__(self, users):
+ self.users = users
+ self.name = None
+ self.state = "GETNAME"
+
+ def connectionMade(self):
+ self.sendLine("What's your name?")
+
+ def connectionLost(self, reason):
+ if self.users.has_key(self.name):
+ del self.users[self.name]
+
+ def lineReceived(self, line):
+ if self.state == "GETNAME":
+ self.handle_GETNAME(line)
+ else:
+ self.handle_CHAT(line)
+
+ def handle_GETNAME(self, name):
+ if self.users.has_key(name):
+ self.sendLine("Name taken, please choose another.")
+ return
+ self.sendLine("Welcome, %s!" % (name,))
+ self.name = name
+ self.users[name] = self
+ self.state = "CHAT"
+
+ def handle_CHAT(self, message):
+ message = "<%s> %s" % (self.name, message)
+ for name, protocol in self.users.iteritems():
+ if protocol != self:
+ protocol.sendLine(message)
+
+
+class ChatFactory(Factory):
+
+ def __init__(self):
+ self.users = {} # maps user names to Chat instances
+
+ def buildProtocol(self, addr):
+ return Chat(self.users)
+
+
+reactor.listenTCP(8123, ChatFactory())
+reactor.run()
diff --git a/doc/core/howto/listings/trial/calculus/__init__.py b/doc/core/howto/listings/trial/calculus/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/__init__.py
diff --git a/doc/core/howto/listings/trial/calculus/base_1.py b/doc/core/howto/listings/trial/calculus/base_1.py
new file mode 100644
index 0000000..e827263
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/base_1.py
@@ -0,0 +1,16 @@
+# -*- test-case-name: calculus.test.test_base_1 -*-
+
+
+
+class Calculation(object):
+ def add(self, a, b):
+ pass
+
+ def subtract(self, a, b):
+ pass
+
+ def multiply(self, a, b):
+ pass
+
+ def divide(self, a, b):
+ pass
diff --git a/doc/core/howto/listings/trial/calculus/base_2.py b/doc/core/howto/listings/trial/calculus/base_2.py
new file mode 100644
index 0000000..9644912
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/base_2.py
@@ -0,0 +1,14 @@
+# -*- test-case-name: calculus.test.test_base_2 -*-
+
+class Calculation(object):
+ def add(self, a, b):
+ return a + b
+
+ def subtract(self, a, b):
+ return a - b
+
+ def multiply(self, a, b):
+ return a * b
+
+ def divide(self, a, b):
+ return a / b
diff --git a/doc/core/howto/listings/trial/calculus/base_3.py b/doc/core/howto/listings/trial/calculus/base_3.py
new file mode 100644
index 0000000..bd33c31
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/base_3.py
@@ -0,0 +1,24 @@
+# -*- test-case-name: calculus.test.test_base_3 -*-
+
+class Calculation(object):
+ def _make_ints(self, *args):
+ try:
+ return map(int, args)
+ except ValueError:
+ raise TypeError("Couldn't coerce arguments to integers: %s" % args)
+
+ def add(self, a, b):
+ a, b = self._make_ints(a, b)
+ return a + b
+
+ def subtract(self, a, b):
+ a, b = self._make_ints(a, b)
+ return a - b
+
+ def multiply(self, a, b):
+ a, b = self._make_ints(a, b)
+ return a * b
+
+ def divide(self, a, b):
+ a, b = self._make_ints(a, b)
+ return a / b
diff --git a/doc/core/howto/listings/trial/calculus/client_1.py b/doc/core/howto/listings/trial/calculus/client_1.py
new file mode 100644
index 0000000..a42434d
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/client_1.py
@@ -0,0 +1,39 @@
+# -*- test-case-name: calculus.test.test_client_1 -*-
+
+from twisted.protocols import basic
+from twisted.internet import defer
+
+
+
+class RemoteCalculationClient(basic.LineReceiver):
+ def __init__(self):
+ self.results = []
+
+
+ def lineReceived(self, line):
+ d = self.results.pop(0)
+ d.callback(int(line))
+
+
+ def _sendOperation(self, op, a, b):
+ d = defer.Deferred()
+ self.results.append(d)
+ line = "%s %d %d" % (op, a, b)
+ self.sendLine(line)
+ return d
+
+
+ def add(self, a, b):
+ return self._sendOperation("add", a, b)
+
+
+ def subtract(self, a, b):
+ return self._sendOperation("subtract", a, b)
+
+
+ def multiply(self, a, b):
+ return self._sendOperation("multiply", a, b)
+
+
+ def divide(self, a, b):
+ return self._sendOperation("divide", a, b)
diff --git a/doc/core/howto/listings/trial/calculus/client_2.py b/doc/core/howto/listings/trial/calculus/client_2.py
new file mode 100644
index 0000000..dfb464b
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/client_2.py
@@ -0,0 +1,54 @@
+# -*- test-case-name: calculus.test.test_client_2 -*-
+
+from twisted.protocols import basic
+from twisted.internet import defer, reactor
+
+
+
+class ClientTimeoutError(Exception):
+ pass
+
+
+
+class RemoteCalculationClient(basic.LineReceiver):
+
+ callLater = reactor.callLater
+ timeOut = 60
+
+ def __init__(self):
+ self.results = []
+
+
+ def lineReceived(self, line):
+ d, callID = self.results.pop(0)
+ callID.cancel()
+ d.callback(int(line))
+
+
+ def _cancel(self, d):
+ d.errback(ClientTimeoutError())
+
+
+ def _sendOperation(self, op, a, b):
+ d = defer.Deferred()
+ callID = self.callLater(self.timeOut, self._cancel, d)
+ self.results.append((d, callID))
+ line = "%s %d %d" % (op, a, b)
+ self.sendLine(line)
+ return d
+
+
+ def add(self, a, b):
+ return self._sendOperation("add", a, b)
+
+
+ def subtract(self, a, b):
+ return self._sendOperation("subtract", a, b)
+
+
+ def multiply(self, a, b):
+ return self._sendOperation("multiply", a, b)
+
+
+ def divide(self, a, b):
+ return self._sendOperation("divide", a, b)
diff --git a/doc/core/howto/listings/trial/calculus/client_3.py b/doc/core/howto/listings/trial/calculus/client_3.py
new file mode 100644
index 0000000..31b0c35
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/client_3.py
@@ -0,0 +1,53 @@
+# -*- test-case-name: calculus.test.test_client -*-
+
+from twisted.protocols import basic, policies
+from twisted.internet import defer
+
+
+
+class ClientTimeoutError(Exception):
+ pass
+
+
+
+class RemoteCalculationClient(object, basic.LineReceiver, policies.TimeoutMixin):
+
+ def __init__(self):
+ self.results = []
+ self._timeOut = 60
+
+ def lineReceived(self, line):
+ self.setTimeout(None)
+ d = self.results.pop(0)
+ d.callback(int(line))
+
+
+ def timeoutConnection(self):
+ for d in self.results:
+ d.errback(ClientTimeoutError())
+ self.transport.loseConnection()
+
+
+ def _sendOperation(self, op, a, b):
+ d = defer.Deferred()
+ self.results.append(d)
+ line = "%s %d %d" % (op, a, b)
+ self.sendLine(line)
+ self.setTimeout(self._timeOut)
+ return d
+
+
+ def add(self, a, b):
+ return self._sendOperation("add", a, b)
+
+
+ def subtract(self, a, b):
+ return self._sendOperation("subtract", a, b)
+
+
+ def multiply(self, a, b):
+ return self._sendOperation("multiply", a, b)
+
+
+ def divide(self, a, b):
+ return self._sendOperation("divide", a, b)
diff --git a/doc/core/howto/listings/trial/calculus/remote_1.py b/doc/core/howto/listings/trial/calculus/remote_1.py
new file mode 100644
index 0000000..4fcdd74
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/remote_1.py
@@ -0,0 +1,47 @@
+# -*- test-case-name: calculus.test.test_remote_1 -*-
+
+from twisted.protocols import basic
+from twisted.internet import protocol
+from calculus.base_3 import Calculation
+
+
+
+class CalculationProxy(object):
+ def __init__(self):
+ self.calc = Calculation()
+ for m in ['add', 'subtract', 'multiply', 'divide']:
+ setattr(self, 'remote_%s' % m, getattr(self.calc, m))
+
+
+
+class RemoteCalculationProtocol(basic.LineReceiver):
+ def __init__(self):
+ self.proxy = CalculationProxy()
+
+
+ def lineReceived(self, line):
+ op, a, b = line.split()
+ a = int(a)
+ b = int(b)
+ op = getattr(self.proxy, 'remote_%s' % (op,))
+ result = op(a, b)
+ self.sendLine(str(result))
+
+
+
+class RemoteCalculationFactory(protocol.Factory):
+ protocol = RemoteCalculationProtocol
+
+
+
+def main():
+ from twisted.internet import reactor
+ from twisted.python import log
+ import sys
+ log.startLogging(sys.stdout)
+ reactor.listenTCP(0, RemoteCalculationFactory())
+ reactor.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/doc/core/howto/listings/trial/calculus/remote_2.py b/doc/core/howto/listings/trial/calculus/remote_2.py
new file mode 100644
index 0000000..6826be1
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/remote_2.py
@@ -0,0 +1,51 @@
+# -*- test-case-name: calculus.test.test_remote_1 -*-
+
+from twisted.protocols import basic
+from twisted.internet import protocol
+from twisted.python import log
+from calculus.base_3 import Calculation
+
+
+
+class CalculationProxy(object):
+ def __init__(self):
+ self.calc = Calculation()
+ for m in ['add', 'subtract', 'multiply', 'divide']:
+ setattr(self, 'remote_%s' % m, getattr(self.calc, m))
+
+
+
+class RemoteCalculationProtocol(basic.LineReceiver):
+ def __init__(self):
+ self.proxy = CalculationProxy()
+
+
+ def lineReceived(self, line):
+ op, a, b = line.split()
+ op = getattr(self.proxy, 'remote_%s' % (op,))
+ try:
+ result = op(a, b)
+ except TypeError:
+ log.err()
+ self.sendLine("error")
+ else:
+ self.sendLine(str(result))
+
+
+
+class RemoteCalculationFactory(protocol.Factory):
+ protocol = RemoteCalculationProtocol
+
+
+
+def main():
+ from twisted.internet import reactor
+ from twisted.python import log
+ import sys
+ log.startLogging(sys.stdout)
+ reactor.listenTCP(0, RemoteCalculationFactory())
+ reactor.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/doc/core/howto/listings/trial/calculus/test/__init__.py b/doc/core/howto/listings/trial/calculus/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/__init__.py
diff --git a/doc/core/howto/listings/trial/calculus/test/test_base_1.py b/doc/core/howto/listings/trial/calculus/test/test_base_1.py
new file mode 100644
index 0000000..09f0b14
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_base_1.py
@@ -0,0 +1,23 @@
+from calculus.base_1 import Calculation
+from twisted.trial import unittest
+
+class CalculationTestCase(unittest.TestCase):
+ def test_add(self):
+ calc = Calculation()
+ result = calc.add(3, 8)
+ self.assertEqual(result, 11)
+
+ def test_subtract(self):
+ calc = Calculation()
+ result = calc.subtract(7, 3)
+ self.assertEqual(result, 4)
+
+ def test_multiply(self):
+ calc = Calculation()
+ result = calc.multiply(12, 5)
+ self.assertEqual(result, 60)
+
+ def test_divide(self):
+ calc = Calculation()
+ result = calc.divide(12, 5)
+ self.assertEqual(result, 2)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_base_2.py b/doc/core/howto/listings/trial/calculus/test/test_base_2.py
new file mode 100644
index 0000000..b9bdcb1
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_base_2.py
@@ -0,0 +1,29 @@
+from calculus.base_2 import Calculation
+from twisted.trial import unittest
+
+
+
+class CalculationTestCase(unittest.TestCase):
+
+ def test_add(self):
+ calc = Calculation()
+ result = calc.add(3, 8)
+ self.assertEqual(result, 11)
+
+
+ def test_subtract(self):
+ calc = Calculation()
+ result = calc.subtract(7, 3)
+ self.assertEqual(result, 4)
+
+
+ def test_multiply(self):
+ calc = Calculation()
+ result = calc.multiply(12, 5)
+ self.assertEqual(result, 60)
+
+
+ def test_divide(self):
+ calc = Calculation()
+ result = calc.divide(12, 5)
+ self.assertEqual(result, 2)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_base_2b.py b/doc/core/howto/listings/trial/calculus/test/test_base_2b.py
new file mode 100644
index 0000000..c05135d
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_base_2b.py
@@ -0,0 +1,29 @@
+from calculus.base_2 import Calculation
+from twisted.trial import unittest
+
+
+
+class CalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.calc = Calculation()
+
+
+ def _test(self, operation, a, b, expected):
+ result = operation(a, b)
+ self.assertEqual(result, expected)
+
+
+ def test_add(self):
+ self._test(self.calc.add, 3, 8, 11)
+
+
+ def test_subtract(self):
+ self._test(self.calc.subtract, 7, 3, 4)
+
+
+ def test_multiply(self):
+ self._test(self.calc.multiply, 6, 9, 54)
+
+
+ def test_divide(self):
+ self._test(self.calc.divide, 12, 5, 2)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_base_3.py b/doc/core/howto/listings/trial/calculus/test/test_base_3.py
new file mode 100644
index 0000000..e22541e
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_base_3.py
@@ -0,0 +1,52 @@
+from calculus.base_3 import Calculation
+from twisted.trial import unittest
+
+
+
+class CalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.calc = Calculation()
+
+
+ def _test(self, operation, a, b, expected):
+ result = operation(a, b)
+ self.assertEqual(result, expected)
+
+
+ def _test_error(self, operation):
+ self.assertRaises(TypeError, operation, "foo", 2)
+ self.assertRaises(TypeError, operation, "bar", "egg")
+ self.assertRaises(TypeError, operation, [3], [8, 2])
+ self.assertRaises(TypeError, operation, {"e": 3}, {"r": "t"})
+
+
+ def test_add(self):
+ self._test(self.calc.add, 3, 8, 11)
+
+
+ def test_subtract(self):
+ self._test(self.calc.subtract, 7, 3, 4)
+
+
+ def test_multiply(self):
+ self._test(self.calc.multiply, 6, 9, 54)
+
+
+ def test_divide(self):
+ self._test(self.calc.divide, 12, 5, 2)
+
+
+ def test_errorAdd(self):
+ self._test_error(self.calc.add)
+
+
+ def test_errorSubtract(self):
+ self._test_error(self.calc.subtract)
+
+
+ def test_errorMultiply(self):
+ self._test_error(self.calc.multiply)
+
+
+ def test_errorDivide(self):
+ self._test_error(self.calc.divide)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_client_1.py b/doc/core/howto/listings/trial/calculus/test/test_client_1.py
new file mode 100644
index 0000000..28c3408
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_client_1.py
@@ -0,0 +1,37 @@
+from calculus.client_1 import RemoteCalculationClient
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+
+class ClientCalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.tr = proto_helpers.StringTransport()
+ self.proto = RemoteCalculationClient()
+ self.proto.makeConnection(self.tr)
+
+
+ def _test(self, operation, a, b, expected):
+ d = getattr(self.proto, operation)(a, b)
+ self.assertEqual(self.tr.value(), '%s %d %d\r\n' % (operation, a, b))
+ self.tr.clear()
+ d.addCallback(self.assertEqual, expected)
+ self.proto.dataReceived("%d\r\n" % (expected,))
+ return d
+
+
+ def test_add(self):
+ return self._test('add', 7, 6, 13)
+
+
+ def test_subtract(self):
+ return self._test('subtract', 82, 78, 4)
+
+
+ def test_multiply(self):
+ return self._test('multiply', 2, 8, 16)
+
+
+ def test_divide(self):
+ return self._test('divide', 14, 3, 4)
+
diff --git a/doc/core/howto/listings/trial/calculus/test/test_client_2.py b/doc/core/howto/listings/trial/calculus/test/test_client_2.py
new file mode 100644
index 0000000..45c0a28
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_client_2.py
@@ -0,0 +1,48 @@
+from calculus.client_2 import RemoteCalculationClient, ClientTimeoutError
+
+from twisted.internet import task
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+
+class ClientCalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.tr = proto_helpers.StringTransportWithDisconnection()
+ self.clock = task.Clock()
+ self.proto = RemoteCalculationClient()
+ self.tr.protocol = self.proto
+ self.proto.callLater = self.clock.callLater
+ self.proto.makeConnection(self.tr)
+
+
+ def _test(self, operation, a, b, expected):
+ d = getattr(self.proto, operation)(a, b)
+ self.assertEqual(self.tr.value(), '%s %d %d\r\n' % (operation, a, b))
+ self.tr.clear()
+ d.addCallback(self.assertEqual, expected)
+ self.proto.dataReceived("%d\r\n" % (expected,))
+ return d
+
+
+ def test_add(self):
+ return self._test('add', 7, 6, 13)
+
+
+ def test_subtract(self):
+ return self._test('subtract', 82, 78, 4)
+
+
+ def test_multiply(self):
+ return self._test('multiply', 2, 8, 16)
+
+
+ def test_divide(self):
+ return self._test('divide', 14, 3, 4)
+
+
+ def test_timeout(self):
+ d = self.proto.add(9, 4)
+ self.assertEqual(self.tr.value(), 'add 9 4\r\n')
+ self.clock.advance(self.proto.timeOut)
+ return self.assertFailure(d, ClientTimeoutError)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_client_3.py b/doc/core/howto/listings/trial/calculus/test/test_client_3.py
new file mode 100644
index 0000000..78ac5e1
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_client_3.py
@@ -0,0 +1,63 @@
+from calculus.client_3 import RemoteCalculationClient, ClientTimeoutError
+
+from twisted.internet import task
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+
+class ClientCalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.tr = proto_helpers.StringTransportWithDisconnection()
+ self.clock = task.Clock()
+ self.proto = RemoteCalculationClient()
+ self.tr.protocol = self.proto
+ self.proto.callLater = self.clock.callLater
+ self.proto.makeConnection(self.tr)
+
+
+ def _test(self, operation, a, b, expected):
+ d = getattr(self.proto, operation)(a, b)
+ self.assertEqual(self.tr.value(), '%s %d %d\r\n' % (operation, a, b))
+ self.tr.clear()
+ d.addCallback(self.assertEqual, expected)
+ self.proto.dataReceived("%d\r\n" % (expected,))
+ return d
+
+
+ def test_add(self):
+ return self._test('add', 7, 6, 13)
+
+
+ def test_subtract(self):
+ return self._test('subtract', 82, 78, 4)
+
+
+ def test_multiply(self):
+ return self._test('multiply', 2, 8, 16)
+
+
+ def test_divide(self):
+ return self._test('divide', 14, 3, 4)
+
+
+ def test_timeout(self):
+ d = self.proto.add(9, 4)
+ self.assertEqual(self.tr.value(), 'add 9 4\r\n')
+ self.clock.advance(self.proto.timeOut)
+ return self.assertFailure(d, ClientTimeoutError)
+
+
+ def test_timeoutConnectionLost(self):
+ called = []
+ def lost(arg):
+ called.append(True)
+ self.proto.connectionLost = lost
+
+ d = self.proto.add(9, 4)
+ self.assertEqual(self.tr.value(), 'add 9 4\r\n')
+ self.clock.advance(self.proto.timeOut)
+
+ def check(ignore):
+ self.assertEqual(called, [True])
+ return self.assertFailure(d, ClientTimeoutError).addCallback(check)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_remote_1.py b/doc/core/howto/listings/trial/calculus/test/test_remote_1.py
new file mode 100644
index 0000000..5f41657
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_remote_1.py
@@ -0,0 +1,34 @@
+from calculus.remote_1 import RemoteCalculationFactory
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+
+class RemoteCalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ factory = RemoteCalculationFactory()
+ self.proto = factory.buildProtocol(('127.0.0.1', 0))
+ self.tr = proto_helpers.StringTransport()
+ self.proto.makeConnection(self.tr)
+
+
+ def _test(self, operation, a, b, expected):
+ self.proto.dataReceived('%s %d %d\r\n' % (operation, a, b))
+ self.assertEqual(int(self.tr.value()), expected)
+
+
+ def test_add(self):
+ return self._test('add', 7, 6, 13)
+
+
+ def test_subtract(self):
+ return self._test('subtract', 82, 78, 4)
+
+
+ def test_multiply(self):
+ return self._test('multiply', 2, 8, 16)
+
+
+ def test_divide(self):
+ return self._test('divide', 14, 3, 4)
+
diff --git a/doc/core/howto/listings/trial/calculus/test/test_remote_2.py b/doc/core/howto/listings/trial/calculus/test/test_remote_2.py
new file mode 100644
index 0000000..75b5011
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_remote_2.py
@@ -0,0 +1,46 @@
+from calculus.remote_1 import RemoteCalculationFactory
+from calculus.client_2 import RemoteCalculationClient
+
+from twisted.trial import unittest
+from twisted.internet import reactor, protocol
+
+
+
+class RemoteRunCalculationTestCase(unittest.TestCase):
+
+ def setUp(self):
+ factory = RemoteCalculationFactory()
+ self.port = reactor.listenTCP(0, factory, interface="127.0.0.1")
+ self.client = None
+
+
+ def tearDown(self):
+ if self.client is not None:
+ self.client.transport.loseConnection()
+ return self.port.stopListening()
+
+
+ def _test(self, op, a, b, expected):
+ creator = protocol.ClientCreator(reactor, RemoteCalculationClient)
+ def cb(client):
+ self.client = client
+ return getattr(self.client, op)(a, b
+ ).addCallback(self.assertEqual, expected)
+ return creator.connectTCP('127.0.0.1', self.port.getHost().port
+ ).addCallback(cb)
+
+
+ def test_add(self):
+ return self._test("add", 5, 9, 14)
+
+
+ def test_subtract(self):
+ return self._test("subtract", 47, 13, 34)
+
+
+ def test_multiply(self):
+ return self._test("multiply", 7, 3, 21)
+
+
+ def test_divide(self):
+ return self._test("divide", 84, 10, 8)
diff --git a/doc/core/howto/listings/trial/calculus/test/test_remote_3.py b/doc/core/howto/listings/trial/calculus/test/test_remote_3.py
new file mode 100644
index 0000000..0f0c555
--- /dev/null
+++ b/doc/core/howto/listings/trial/calculus/test/test_remote_3.py
@@ -0,0 +1,40 @@
+from calculus.remote_2 import RemoteCalculationFactory
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+
+class RemoteCalculationTestCase(unittest.TestCase):
+ def setUp(self):
+ factory = RemoteCalculationFactory()
+ self.proto = factory.buildProtocol(('127.0.0.1', 0))
+ self.tr = proto_helpers.StringTransport()
+ self.proto.makeConnection(self.tr)
+
+
+ def _test(self, operation, a, b, expected):
+ self.proto.dataReceived('%s %d %d\r\n' % (operation, a, b))
+ self.assertEqual(int(self.tr.value()), expected)
+
+
+ def test_add(self):
+ return self._test('add', 7, 6, 13)
+
+
+ def test_subtract(self):
+ return self._test('subtract', 82, 78, 4)
+
+
+ def test_multiply(self):
+ return self._test('multiply', 2, 8, 16)
+
+
+ def test_divide(self):
+ return self._test('divide', 14, 3, 4)
+
+
+ def test_invalidParameters(self):
+ self.proto.dataReceived('add foo bar\r\n')
+ self.assertEqual(self.tr.value(), "error\r\n")
+ errors = self.flushLoggedErrors(TypeError)
+ self.assertEqual(len(errors), 1)
diff --git a/doc/core/howto/listings/udp/MulticastClient.py b/doc/core/howto/listings/udp/MulticastClient.py
new file mode 100644
index 0000000..7a41aaa
--- /dev/null
+++ b/doc/core/howto/listings/udp/MulticastClient.py
@@ -0,0 +1,19 @@
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+
+
+class MulticastPingClient(DatagramProtocol):
+
+ def startProtocol(self):
+ # Join the multicast address, so we can receive replies:
+ self.transport.joinGroup("228.0.0.5")
+ # Send to 228.0.0.5:8005 - all listeners on the multicast address
+ # (including us) will receive this message.
+ self.transport.write('Client: Ping', ("228.0.0.5", 8005))
+
+ def datagramReceived(self, datagram, address):
+ print "Datagram %s received from %s" % (repr(datagram), repr(address))
+
+
+reactor.listenMulticast(8005, MulticastPingClient(), listenMultiple=True)
+reactor.run()
diff --git a/doc/core/howto/listings/udp/MulticastServer.py b/doc/core/howto/listings/udp/MulticastServer.py
new file mode 100644
index 0000000..0909e60
--- /dev/null
+++ b/doc/core/howto/listings/udp/MulticastServer.py
@@ -0,0 +1,28 @@
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+
+
+class MulticastPingPong(DatagramProtocol):
+
+ def startProtocol(self):
+ """
+ Called after protocol has started listening.
+ """
+ # Set the TTL>1 so multicast will cross router hops:
+ self.transport.setTTL(5)
+ # Join a specific multicast group:
+ self.transport.joinGroup("228.0.0.5")
+
+ def datagramReceived(self, datagram, address):
+ print "Datagram %s received from %s" % (repr(datagram), repr(address))
+ if datagram == "Client: Ping":
+ # Rather than replying to the group multicast address, we send the
+ # reply directly (unicast) to the originating port:
+ self.transport.write("Server: Pong", address)
+
+
+# We use listenMultiple=True so that we can run MulticastServer.py and
+# MulticastClient.py on same machine:
+reactor.listenMulticast(8005, MulticastPingPong(),
+ listenMultiple=True)
+reactor.run()
diff --git a/doc/core/howto/logging.html b/doc/core/howto/logging.html
new file mode 100644
index 0000000..95f8f0d
--- /dev/null
+++ b/doc/core/howto/logging.html
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Logging with twisted.python.log</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Logging with twisted.python.log</h1>
+ <div class="toc"><ol><li><a href="#auto0">Basic usage</a></li><ul><li><a href="#auto1">Logging and twistd</a></li><li><a href="#auto2">Log files</a></li><li><a href="#auto3">Using the standard library logging module</a></li></ul><li><a href="#auto4">Writing log observers</a></li><li><a href="#auto5">Customizing twistd logging</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Basic usage<a name="auto0"/></h2>
+
+ <p>Twisted provides a simple and flexible logging system in the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.html" title="twisted.python.log">twisted.python.log</a></code> module. It has three commonly used
+ functions:</p>
+
+ <dl>
+ <dt><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.LogPublisher.msg.html" title="twisted.python.log.LogPublisher.msg">msg</a></code></dt>
+ <dd>Logs a new message. For example:
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-variable">log</span>.<span class="py-src-variable">msg</span>(<span class="py-src-string">'Hello, world.'</span>)
+</pre>
+ </dd>
+
+ <dt><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.err.html" title="twisted.python.log.err">err</a></code></dt>
+ <dd>Writes a failure to the log, including traceback information (if any).
+ You can pass it a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code> or Exception instance, or
+ nothing. If you pass something else, it will be converted to a string
+ with <code>repr</code> and logged.
+
+ If you pass nothing, it will construct a Failure from the
+ currently active exception, which makes it convenient to use in an <code class="python">except</code> clause:
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">x</span> = <span class="py-src-number">1</span> / <span class="py-src-number">0</span>
+<span class="py-src-keyword">except</span>:
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>() <span class="py-src-comment"># will log the ZeroDivisionError</span>
+</pre>
+ </dd>
+
+ <dt><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.startLogging.html" title="twisted.python.log.startLogging">startLogging</a></code></dt>
+ <dd>Starts logging to a given file-like object. For example:
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">open</span>(<span class="py-src-string">'/var/log/foo.log'</span>, <span class="py-src-string">'w'</span>))
+</pre>
+ or:
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">stdout</span>)
+</pre>
+ or:
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">logfile</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DailyLogFile</span>
+
+<span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">DailyLogFile</span>.<span class="py-src-variable">fromFullPath</span>(<span class="py-src-string">&quot;/var/log/foo.log&quot;</span>))
+</pre>
+
+ By default, <code>startLogging</code> will also redirect anything written
+ to <code>sys.stdout</code> and <code>sys.stderr</code> to the log. You
+ can disable this by passing <code class="python">setStdout=False</code> to
+ <code>startLogging</code>.
+ </dd>
+ </dl>
+
+ <p>Before <code>startLogging</code> is called, log messages will be
+ discarded and errors will be written to stderr.</p>
+
+ <h3>Logging and twistd<a name="auto1"/></h3>
+
+ <p>If you are using <code class="shell">twistd</code> to run your daemon, it
+ will take care of calling <code>startLogging</code> for you, and will also
+ rotate log files. See <a href="application.html#twistd" shape="rect">twistd and tac</a>
+ and the <code class="shell">twistd</code> man page for details of using
+ twistd.</p>
+
+ <h3>Log files<a name="auto2"/></h3>
+
+ <p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.logfile.html" title="twisted.python.logfile">twisted.python.logfile</a></code> module provides
+ some standard classes suitable for use with <code>startLogging</code>, such
+ as <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.logfile.DailyLogFile.html" title="twisted.python.logfile.DailyLogFile">DailyLogFile</a></code>,
+ which will rotate the log to a new file once per day.</p>
+
+ <h3>Using the standard library logging module<a name="auto3"/></h3>
+
+ <p>If your application uses the
+ Python <a href="http://docs.python.org/library/logging.html" shape="rect">standard
+ library logging module</a> or you want to use its easy configuration but
+ don't want to lose twisted-produced messages, the observer
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.PythonLoggingObserver.html" title="twisted.python.log.PythonLoggingObserver">PythonLoggingObserver</a></code>
+ should be useful to you.
+ </p>
+
+ <p>You just start it like any other observer:
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">observer</span> = <span class="py-src-variable">log</span>.<span class="py-src-variable">PythonLoggingObserver</span>()
+<span class="py-src-variable">observer</span>.<span class="py-src-variable">start</span>()
+</pre>
+
+ Then <a href="http://docs.python.org/library/logging.html" shape="rect">configure the
+ standard library logging module</a> to behave as you want.
+ </p>
+
+ <p>This method allows you to customize the log level received by the
+ standard library logging module using the <code>logLevel</code> keyword:
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">log</span>.<span class="py-src-variable">msg</span>(<span class="py-src-string">&quot;This is important!&quot;</span>, <span class="py-src-variable">logLevel</span>=<span class="py-src-variable">logging</span>.<span class="py-src-variable">CRITICAL</span>)
+<span class="py-src-variable">log</span>.<span class="py-src-variable">msg</span>(<span class="py-src-string">&quot;Don't mind&quot;</span>, <span class="py-src-variable">logLevel</span>=<span class="py-src-variable">logging</span>.<span class="py-src-variable">DEBUG</span>)
+</pre>
+ Unless <code>logLevel</code> is provided, logging.INFO is used for <code>log.msg</code>
+ and <code>logging.ERROR</code> is used for <code>log.err</code>.
+ </p>
+
+ <p>One special care should be made when you use special configuration of
+ the standard library logging module: some handlers (e.g. SMTP, HTTP) use the network and
+ so can block inside the reactor loop. <em>Nothing</em> in <code>PythonLoggingObserver</code> is
+ done to prevent that.</p>
+
+ <h2>Writing log observers<a name="auto4"/></h2>
+
+ <p>Log observers are the basis of the Twisted logging system.
+ Whenever <code>log.msg</code> (or <code>log.err</code>) is called, an
+ event is emitted. The event is passed to each observer which has been
+ registered. There can be any number of observers, and each can treat
+ the event in any way desired.
+ An example of
+ a log observer in Twisted is the <code>emit</code> method of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.FileLogObserver.html" title="twisted.python.log.FileLogObserver">FileLogObserver</a></code>.
+ <code>FileLogObserver</code>, used by
+ <code>startLogging</code>, writes events to a log file. A log observer
+ is just a callable that accepts a dictionary as its only argument. You can
+ then register it to receive all log events (in addition to any other
+ observers):</p>
+
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span>.<span class="py-src-variable">addObserver</span>(<span class="py-src-variable">yourCallable</span>)
+</pre>
+
+ <p>The dictionary will have at least two items:</p>
+
+ <dl>
+ <dt>message</dt>
+ <dd>The message (a list, usually of strings)
+ for this log event, as passed to <code>log.msg</code> or the
+ message in the failure passed to <code>log.err</code>.</dd>
+
+ <dt>isError</dt>
+ <dd>This is a boolean that will be true if this event came from a call to
+ <code>log.err</code>. If this is set, there may be a <code>failure</code>
+ item in the dictionary as will, with a Failure object in it.</dd>
+ </dl>
+
+ <p>Other items the built in logging functionality may add include:</p>
+
+ <dl>
+ <dt>printed</dt>
+ <dd>This message was captured from <code>sys.stdout</code>, i.e. this
+ message came from a <code>print</code> statement. If
+ <code>isError</code> is also true, it came from
+ <code>sys.stderr</code>.</dd>
+ </dl>
+
+ <p>You can pass additional items to the event dictionary by passing keyword
+ arguments to <code>log.msg</code> and <code>log.err</code>. The standard
+ log observers will ignore dictionary items they don't use.</p>
+
+ <p>Important notes:</p>
+
+ <ul>
+ <li>Never block in a log observer, as it may run in main Twisted thread.
+ This means you can't use socket or syslog standard library logging backends.</li>
+
+ <li>The observer needs to be thread safe if you anticipate using threads
+ in your program.</li>
+ </ul>
+
+ <h2>Customizing <code>twistd</code> logging<a name="auto5"/></h2>
+ <p>
+ The behavior of the logging that <code>twistd</code> does can be
+ customized either with the <code>--logger</code> option or by setting the
+ <code>ILogObserver</code> component on the application object. See the <a href="application.html" shape="rect">Application document</a> for more information.
+ </p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/options.html b/doc/core/howto/options.html
new file mode 100644
index 0000000..f674ad2
--- /dev/null
+++ b/doc/core/howto/options.html
@@ -0,0 +1,581 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Parsing command-lines with usage.Options</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Parsing command-lines with usage.Options</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Boolean Options</a></li><ul><li><a href="#auto2">Inheritance, Or: How I Learned to Stop Worrying and Love
+ the Superclass</a></li></ul><li><a href="#auto3">Parameters</a></li><li><a href="#auto4">Option Subcommands</a></li><li><a href="#auto5">Generic Code For Options</a></li><li><a href="#auto6">Parsing Arguments</a></li><li><a href="#auto7">Post Processing</a></li><li><a href="#auto8">Type enforcement</a></li><li><a href="#auto9">Shell tab-completion</a></li><ul><li><a href="#auto10">Completion metadata</a></li></ul></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Introduction<a name="auto0"/></h2>
+
+ <p>There is frequently a need for programs to parse a UNIX-like
+ command line program: options preceded by <code>-</code> or
+ <code>--</code>, sometimes followed by a parameter, followed by
+ a list of arguments. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.html" title="twisted.python.usage">twisted.python.usage</a></code> provides a class,
+ <code>Options</code>, to facilitate such parsing.</p>
+
+ <p>While Python has the <code>getopt</code> module for doing
+ this, it provides a very low level of abstraction for options.
+ Twisted has a higher level of abstraction, in the class <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.Options.html" title="twisted.python.usage.Options">twisted.python.usage.Options</a></code>. It uses
+ Python's reflection facilities to provide an easy to use yet
+ flexible interface to the command line. While most command line
+ processors either force the application writer to write her own
+ loops, or have arbitrary limitations on the command line (the
+ most common one being not being able to have more then one
+ instance of a specific option, thus rendering the idiom
+ <code class="shell">program -v -v -v</code> impossible), Twisted allows the
+ programmer to decide how much control she wants.</p>
+
+ <p>The <code>Options</code> class is used by subclassing. Since
+ a lot of time it will be used in the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.tap.html" title="twisted.tap">twisted.tap</a></code> package, where the local
+ conventions require the specific options parsing class to also
+ be called <code>Options</code>, it is usually imported with</p>
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+</pre>
+
+ <h2>Boolean Options<a name="auto1"/></h2>
+
+ <p>For simple boolean options, define the attribute
+ <code>optFlags</code> like this:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optFlags</span> = [[<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-string">&quot;Act quickly&quot;</span>], [<span class="py-src-string">&quot;safe&quot;</span>, <span class="py-src-string">&quot;s&quot;</span>, <span class="py-src-string">&quot;Act safely&quot;</span>]]
+</pre>
+ <p><code>optFlags</code> should be a list of 3-lists. The first element
+ is the long name, and will be used on the command line as
+ <code>--fast</code>. The second one is the short name, and will be used
+ on the command line as <code>-f</code>. The last element is a
+ description of the flag and will be used to generate the usage
+ information text. The long name also determines the name of the key
+ that will be set on the Options instance. Its value will be 1 if the
+ option was seen, 0 otherwise. Here is an example for usage:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optFlags</span> = [
+ [<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-string">&quot;Act quickly&quot;</span>],
+ [<span class="py-src-string">&quot;good&quot;</span>, <span class="py-src-string">&quot;g&quot;</span>, <span class="py-src-string">&quot;Act well&quot;</span>],
+ [<span class="py-src-string">&quot;cheap&quot;</span>, <span class="py-src-string">&quot;c&quot;</span>, <span class="py-src-string">&quot;Act cheaply&quot;</span>]
+ ]
+
+<span class="py-src-variable">command_line</span> = [<span class="py-src-string">&quot;-g&quot;</span>, <span class="py-src-string">&quot;--fast&quot;</span>]
+
+<span class="py-src-variable">options</span> = <span class="py-src-variable">Options</span>()
+<span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">options</span>.<span class="py-src-variable">parseOptions</span>(<span class="py-src-variable">command_line</span>)
+<span class="py-src-keyword">except</span> <span class="py-src-variable">usage</span>.<span class="py-src-variable">UsageError</span>, <span class="py-src-variable">errortext</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'%s: %s'</span> % (<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">0</span>], <span class="py-src-variable">errortext</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'%s: Try --help for usage details.'</span> % (<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">0</span>])
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">exit</span>(<span class="py-src-number">1</span>)
+<span class="py-src-keyword">if</span> <span class="py-src-variable">options</span>[<span class="py-src-string">'fast'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;fast&quot;</span>,
+<span class="py-src-keyword">if</span> <span class="py-src-variable">options</span>[<span class="py-src-string">'good'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;good&quot;</span>,
+<span class="py-src-keyword">if</span> <span class="py-src-variable">options</span>[<span class="py-src-string">'cheap'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;cheap&quot;</span>,
+<span class="py-src-keyword">print</span>
+</pre>
+
+ <p>The above will print <code>fast good</code>.</p>
+
+ <p>Note here that Options fully supports the mapping interface. You can
+ access it mostly just like you can access any other dict. Options are stored
+ as mapping items in the Options instance: parameters as 'paramname': 'value'
+ and flags as 'flagname': 1 or 0.</p>
+
+ <h3>Inheritance, Or: How I Learned to Stop Worrying and Love
+ the Superclass<a name="auto2"/></h3>
+
+ <p>Sometimes there is a need for several option processors with
+ a unifying core. Perhaps you want all your commands to
+ understand <code>-q</code>/<code>--quiet</code> means to be
+ quiet, or something similar. On the face of it, this looks
+ impossible: in Python, the subclass's <code>optFlags</code>
+ would shadow the superclass's. However,
+ <code>usage.Options</code> uses special reflection code to get
+ all of the <code>optFlags</code> defined in the hierarchy. So
+ the following:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">BaseOptions</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optFlags</span> = [[<span class="py-src-string">&quot;quiet&quot;</span>, <span class="py-src-string">&quot;q&quot;</span>, <span class="py-src-variable">None</span>]]
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SpecificOptions</span>(<span class="py-src-parameter">BaseOptions</span>):
+
+ <span class="py-src-variable">optFlags</span> = [
+ [<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-variable">None</span>], [<span class="py-src-string">&quot;good&quot;</span>, <span class="py-src-string">&quot;g&quot;</span>, <span class="py-src-variable">None</span>], [<span class="py-src-string">&quot;cheap&quot;</span>, <span class="py-src-string">&quot;c&quot;</span>, <span class="py-src-variable">None</span>]
+ ]
+</pre>
+ <p>Is the same as: </p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SpecificOptions</span>(<span class="py-src-parameter">BaseOptions</span>):
+
+ <span class="py-src-variable">optFlags</span> = [
+ [<span class="py-src-string">&quot;quiet&quot;</span>, <span class="py-src-string">&quot;q&quot;</span>, <span class="py-src-string">&quot;Silence output&quot;</span>],
+ [<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-string">&quot;Run quickly&quot;</span>],
+ [<span class="py-src-string">&quot;good&quot;</span>, <span class="py-src-string">&quot;g&quot;</span>, <span class="py-src-string">&quot;Don't validate input&quot;</span>],
+ [<span class="py-src-string">&quot;cheap&quot;</span>, <span class="py-src-string">&quot;c&quot;</span>, <span class="py-src-string">&quot;Use cheap resources&quot;</span>]
+ ]
+</pre>
+
+ <h2>Parameters<a name="auto3"/></h2>
+
+ <p>Parameters are specified using the attribute
+ <code>optParameters</code>. They <em>must</em> be given a
+ default. If you want to make sure you got the parameter from
+ the command line, give a non-string default. Since the command
+ line only has strings, this is completely reliable.</p>
+
+ <p>Here is an example:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optFlags</span> = [
+ [<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-string">&quot;Run quickly&quot;</span>],
+ [<span class="py-src-string">&quot;good&quot;</span>, <span class="py-src-string">&quot;g&quot;</span>, <span class="py-src-string">&quot;Don't validate input&quot;</span>],
+ [<span class="py-src-string">&quot;cheap&quot;</span>, <span class="py-src-string">&quot;c&quot;</span>, <span class="py-src-string">&quot;Use cheap resources&quot;</span>]
+ ]
+ <span class="py-src-variable">optParameters</span> = [[<span class="py-src-string">&quot;user&quot;</span>, <span class="py-src-string">&quot;u&quot;</span>, <span class="py-src-variable">None</span>, <span class="py-src-string">&quot;The user name&quot;</span>]]
+
+<span class="py-src-variable">config</span> = <span class="py-src-variable">Options</span>()
+<span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">config</span>.<span class="py-src-variable">parseOptions</span>() <span class="py-src-comment"># When given no argument, parses sys.argv[1:]</span>
+<span class="py-src-keyword">except</span> <span class="py-src-variable">usage</span>.<span class="py-src-variable">UsageError</span>, <span class="py-src-variable">errortext</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'%s: %s'</span> % (<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">0</span>], <span class="py-src-variable">errortext</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'%s: Try --help for usage details.'</span> % (<span class="py-src-variable">sys</span>.<span class="py-src-variable">argv</span>[<span class="py-src-number">0</span>])
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">exit</span>(<span class="py-src-number">1</span>)
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>[<span class="py-src-string">'user'</span>] <span class="py-src-keyword">is</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">None</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Hello&quot;</span>, <span class="py-src-variable">config</span>[<span class="py-src-string">'user'</span>]
+<span class="py-src-keyword">print</span> <span class="py-src-string">&quot;So, you want it:&quot;</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>[<span class="py-src-string">'fast'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;fast&quot;</span>,
+<span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>[<span class="py-src-string">'good'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;good&quot;</span>,
+<span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>[<span class="py-src-string">'cheap'</span>]:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;cheap&quot;</span>,
+<span class="py-src-keyword">print</span>
+</pre>
+
+ <p>Like <code>optFlags</code>, <code>optParameters</code> works
+ smoothly with inheritance.</p>
+
+ <h2>Option Subcommands<a name="auto4"/></h2>
+
+ <p>It is useful, on occassion, to group a set of options together based
+ on the logical <q>action</q> to which they belong. For this, the
+ <code>usage.Options</code> class allows you to define a set of
+ <q>subcommands</q>, each of which can provide its own
+ <code>usage.Options</code> instance to handle its particular
+ options.</p>
+
+ <p>Here is an example for an Options class that might parse
+ options like those the cvs program takes</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ImportOptions</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">optParameters</span> = [
+ [<span class="py-src-string">'module'</span>, <span class="py-src-string">'m'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>], [<span class="py-src-string">'vendor'</span>, <span class="py-src-string">'v'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>],
+ [<span class="py-src-string">'release'</span>, <span class="py-src-string">'r'</span>, <span class="py-src-variable">None</span>]
+ ]
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CheckoutOptions</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">optParameters</span> = [[<span class="py-src-string">'module'</span>, <span class="py-src-string">'m'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>], [<span class="py-src-string">'tag'</span>, <span class="py-src-string">'r'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>]]
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">subCommands</span> = [[<span class="py-src-string">'import'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">ImportOptions</span>, <span class="py-src-string">&quot;Do an Import&quot;</span>],
+ [<span class="py-src-string">'checkout'</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">CheckoutOptions</span>, <span class="py-src-string">&quot;Do a Checkout&quot;</span>]]
+
+ <span class="py-src-variable">optParameters</span> = [
+ [<span class="py-src-string">'compression'</span>, <span class="py-src-string">'z'</span>, <span class="py-src-number">0</span>, <span class="py-src-string">'Use compression'</span>],
+ [<span class="py-src-string">'repository'</span>, <span class="py-src-string">'r'</span>, <span class="py-src-variable">None</span>, <span class="py-src-string">'Specify an alternate repository'</span>]
+ ]
+
+<span class="py-src-variable">config</span> = <span class="py-src-variable">Options</span>(); <span class="py-src-variable">config</span>.<span class="py-src-variable">parseOptions</span>()
+<span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">subCommand</span> == <span class="py-src-string">'import'</span>:
+ <span class="py-src-variable">doImport</span>(<span class="py-src-variable">config</span>.<span class="py-src-variable">subOptions</span>)
+<span class="py-src-keyword">elif</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">subCommand</span> == <span class="py-src-string">'checkout'</span>:
+ <span class="py-src-variable">doCheckout</span>(<span class="py-src-variable">config</span>.<span class="py-src-variable">subOptions</span>)
+</pre>
+
+ <p>The <code>subCommands</code> attribute of <code>Options</code>
+ directs the parser to the two other <code>Options</code> subclasses
+ when the strings <code>&quot;import&quot;</code> or <code>&quot;checkout&quot;</code> are
+ present on the command
+ line. All options after the given command string are passed to the
+ specified Options subclass for further parsing. Only one subcommand
+ may be specified at a time. After parsing has completed, the Options
+ instance has two new attributes - <code>subCommand</code> and <code>
+ subOptions</code> - which hold the command string and the Options
+ instance used to parse the remaining options.</p>
+
+ <h2>Generic Code For Options<a name="auto5"/></h2>
+
+ <p>Sometimes, just setting an attribute on the basis of the
+ options is not flexible enough. In those cases, Twisted does
+ not even attempt to provide abstractions such as <q>counts</q> or
+ <q>lists</q>, but rathers lets you call your own method, which will
+ be called whenever the option is encountered.</p>
+
+ <p>Here is an example of counting verbosity</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">usage</span>.<span class="py-src-variable">Options</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'verbosity'</span>] = <span class="py-src-number">0</span> <span class="py-src-comment"># default</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">opt_verbose</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'verbosity'</span>] = <span class="py-src-variable">self</span>[<span class="py-src-string">'verbosity'</span>]+<span class="py-src-number">1</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">opt_quiet</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'verbosity'</span>] = <span class="py-src-variable">self</span>[<span class="py-src-string">'verbosity'</span>]-<span class="py-src-number">1</span>
+
+ <span class="py-src-variable">opt_v</span> = <span class="py-src-variable">opt_verbose</span>
+ <span class="py-src-variable">opt_q</span> = <span class="py-src-variable">opt_quiet</span>
+</pre>
+
+ <p>Command lines that look like
+ <code class="shell">command -v -v -v -v</code> will
+ increase verbosity to 4, while
+ <code class="shell">command -q -q -q</code> will decrease
+ verbosity to -3.
+ </p>
+
+ <p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.Options.html" title="twisted.python.usage.Options">usage.Options</a></code>
+ class knows that these are
+ parameter-less options, since the methods do not receive an
+ argument. Here is an example for a method with a parameter:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">usage</span>.<span class="py-src-variable">Options</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'symbols'</span>] = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">opt_define</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">symbol</span>):
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'symbols'</span>].<span class="py-src-variable">append</span>(<span class="py-src-variable">symbol</span>)
+
+ <span class="py-src-variable">opt_D</span> = <span class="py-src-variable">opt_define</span>
+</pre>
+
+ <p>This example is useful for the common idiom of having
+ <code>command -DFOO -DBAR</code> to define symbols.</p>
+
+ <h2>Parsing Arguments<a name="auto6"/></h2>
+
+ <p><code>usage.Options</code> does not stop helping when the
+ last parameter is gone. All the other arguments are sent into a
+ function which should deal with them. Here is an example for a
+ <code>cmp</code> like command.</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optParameters</span> = [[<span class="py-src-string">&quot;max_differences&quot;</span>, <span class="py-src-string">&quot;d&quot;</span>, <span class="py-src-number">1</span>, <span class="py-src-variable">None</span>]]
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">parseArgs</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">origin</span>, <span class="py-src-parameter">changed</span>):
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'origin'</span>] = <span class="py-src-variable">origin</span>
+ <span class="py-src-variable">self</span>[<span class="py-src-string">'changed'</span>] = <span class="py-src-variable">changed</span>
+</pre>
+
+ <p>The command should look like <code>command origin
+ changed</code>.</p>
+
+ <p>If you want to have a variable number of left-over
+ arguments, just use <code>def parseArgs(self, *args):</code>.
+ This is useful for commands like the UNIX
+ <code>cat(1)</code>.</p>
+
+ <h2>Post Processing<a name="auto7"/></h2>
+
+ <p>Sometimes, you want to perform post processing of options to
+ patch up inconsistencies, and the like. Here is an example:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optFlags</span> = [
+ [<span class="py-src-string">&quot;fast&quot;</span>, <span class="py-src-string">&quot;f&quot;</span>, <span class="py-src-string">&quot;Run quickly&quot;</span>],
+ [<span class="py-src-string">&quot;good&quot;</span>, <span class="py-src-string">&quot;g&quot;</span>, <span class="py-src-string">&quot;Don't validate input&quot;</span>],
+ [<span class="py-src-string">&quot;cheap&quot;</span>, <span class="py-src-string">&quot;c&quot;</span>, <span class="py-src-string">&quot;Use cheap resources&quot;</span>]
+ ]
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">postOptions</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>[<span class="py-src-string">'fast'</span>] <span class="py-src-keyword">and</span> <span class="py-src-variable">self</span>[<span class="py-src-string">'good'</span>] <span class="py-src-keyword">and</span> <span class="py-src-variable">self</span>[<span class="py-src-string">'cheap'</span>]:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">usage</span>.<span class="py-src-variable">UsageError</span>, <span class="py-src-string">&quot;can't have it all, brother&quot;</span>
+</pre>
+
+ <h2>Type enforcement<a name="auto8"/></h2>
+
+ <p>By default, all options are handled as strings. You may want to
+ enforce the type of your option in some specific case, the classic example
+ being port number. Any callable can be specified in the fifth row of
+ <code>optParameters</code> and will be called with the string value passed
+ in parameter.
+ </p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">optParameters</span> = [
+ [<span class="py-src-string">&quot;shiny_integer&quot;</span>, <span class="py-src-string">&quot;s&quot;</span>, <span class="py-src-number">1</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">int</span>],
+ [<span class="py-src-string">&quot;dummy_float&quot;</span>, <span class="py-src-string">&quot;d&quot;</span>, <span class="py-src-number">3.14159</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">float</span>],
+ ]
+</pre>
+
+ <p>Note that default values are not coerced, so you should either declare
+ it with the good type (as above) or handle it when you use your
+ options.</p>
+
+ <p>The coerce function may have a coerceDoc attribute, the content of which
+ will be printed after the documentation of the option. It's particularly
+ useful for reusing the function at multiple places.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">oneTwoThree</span>(<span class="py-src-parameter">val</span>):
+ <span class="py-src-variable">val</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">val</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">val</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">range</span>(<span class="py-src-number">1</span>, <span class="py-src-number">4</span>):
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">ValueError</span>(<span class="py-src-string">&quot;Not in range&quot;</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">val</span>
+<span class="py-src-variable">oneTwoThree</span>.<span class="py-src-variable">coerceDoc</span> = <span class="py-src-string">&quot;Must be 1, 2 or 3.&quot;</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">optParameters</span> = [[<span class="py-src-string">&quot;one_choice&quot;</span>, <span class="py-src-string">&quot;o&quot;</span>, <span class="py-src-number">1</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">oneTwoThree</span>]]
+</pre>
+
+<p>This example code will print the following help when added to your program:
+</p>
+
+<pre class="shell" xml:space="preserve">
+$ python myprogram.py --help
+Usage: myprogram [options]
+Options:
+ -o, --one_choice= [default: 0]. Must be 1, 2 or 3.
+</pre>
+ <h2>Shell tab-completion<a name="auto9"/></h2>
+
+ <p>The <code>Options</code> class may provide tab-completion to interactive
+ command shells. Only <code>zsh</code> is supported at present, but there is
+ some interest in supporting <code>bash</code> in the future.</p>
+
+ <p>Support is automatic for all of the commands shipped with Twisted. Zsh
+ has shipped, for a number of years, a completion function which ties in to
+ the support provided by the <code>Options</code> class.</p>
+
+ <p>If you are writing a <code>twistd</code> plugin, then tab-completion
+ for your <code>twistd</code> sub-command is also automatic.</p>
+
+ <p>For other commands you may easily provide zsh tab-completion support.
+ Copy the file &quot;twisted/python/twisted-completion.zsh&quot; and name it something
+ like &quot;_mycommand&quot;. A leading underscore with no extension is zsh's
+ convention for completion function files.</p>
+
+ <p>Edit the new file and change the first line to refer only to your new
+ command(s), like so:</p>
+
+<pre class="shell" xml:space="preserve">
+#compdef mycommand
+</pre>
+
+ <p>Then ensure this file is made available to the shell by placing it in
+ one of the directories appearing in zsh's $fpath. Restart zsh, and ensure
+ advanced completion is enabled
+ (<code>autoload -U compinit; compinit)</code>. You should then be able to
+ type the name of your command and press Tab to have your command-line
+ options completed.</p>
+
+ <h3>Completion metadata<a name="auto10"/></h3>
+
+ <p>Optionally, a special attribute, <code>compData</code>, may be defined
+ on your <code>Options</code> subclass in order to provide more information
+ to the shell-completion system. The attribute should be an instance of
+ <a class="API" shape="rect"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.Completions.html" title="twisted.python.usage.Completions">Completions</a></a>. See that class
+ for further details.</p>
+
+ <p>In addition, <code>compData</code> may be defined on parent classes in
+ your inheritance hiearchy. The information from each
+ <a class="API" shape="rect"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.Completions.html" title="twisted.python.usage.Completions">Completions</a></a> instance will be
+ aggregated when producing the final tab-completion results.</p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-clients.html b/doc/core/howto/pb-clients.html
new file mode 100644
index 0000000..ae5445e
--- /dev/null
+++ b/doc/core/howto/pb-clients.html
@@ -0,0 +1,362 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Managing Clients of Perspectives</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ <link href="http://twistedmatrix.com/users/acapnotic/" rel="author" title="Kevin Turner"/></head>
+
+ <body bgcolor="white">
+ <h1 class="title">Managing Clients of Perspectives</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Managing Avatars</a></li><li><a href="#auto2">Managing Clients</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Overview<a name="auto0"/></h2>
+
+<p>In all the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.IPerspective.html" title="twisted.spread.pb.IPerspective">IPerspective</a></code> uses
+we have shown so far, we ignored the <code>mind</code> argument and created
+a new <code>Avatar</code> for every connection. This is usually an easy
+design choice, and it works well for simple cases.</p>
+
+<p>In more complicated cases, for example an <code>Avatar</code> that
+represents a player object which is persistent in the game universe,
+we will want connections from the same player to use the same
+<code>Avatar</code>.</p>
+
+<p>Another thing which is necessary in more complicated scenarios
+is notifying a player asynchronously. While it is possible, of
+course, to allow a player to call
+<code>perspective_remoteListener(referencable)</code> that would
+mean both duplication of code and a higher latency in logging in,
+both bad.</p>
+
+<p>In previous sections all realms looked to be identical.
+In this one we will show the usefulness of realms in accomplishing
+those two objectives.</p>
+
+<h2>Managing Avatars<a name="auto1"/></h2>
+
+<p>The simplest way to manage persistent avatars is to use a straight-forward
+caching mechanism:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleAvatar</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-variable">greetings</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_greet</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;%d&gt;hello %s&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CachingRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>: <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarId</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>]
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>] = <span class="py-src-variable">SimpleAvatar</span>(<span class="py-src-variable">avatarId</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">p</span>, <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+</pre>
+
+<p>This gives us a perspective which counts the number of greetings it
+sent its client. Implementing a caching strategy, as opposed to generating
+a realm with the correct avatars already in it, is usually easier. This
+makes adding new checkers to the portal, or adding new users to a checker
+database, transparent. Otherwise, careful synchronization is needed between
+the checker and avatar is needed (much like the synchronization between
+UNIX's <code>/etc/shadow</code> and <code>/etc/passwd</code>).</p>
+
+<p>Sometimes, however, an avatar will need enough per-connection state
+that it would be easier to generate a new avatar and cache something
+else. Here is an example of that:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Greeter</span>:
+ <span class="py-src-variable">greetings</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">hello</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;%d&gt;hello&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleAvatar</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">greeter</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greeter</span> = <span class="py-src-variable">greeter</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_greet</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">greeter</span>.<span class="py-src-variable">hello</span>()+<span class="py-src-string">' '</span>+<span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CachingRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greeters</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>: <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarId</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">greeters</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">greeters</span>[<span class="py-src-variable">avatarId</span>]
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">greeters</span>[<span class="py-src-variable">avatarId</span>] = <span class="py-src-variable">Greeter</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">SimpleAvatar</span>(<span class="py-src-variable">avatarId</span>, <span class="py-src-variable">p</span>), <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+</pre>
+
+<p>It might seem tempting to use this pattern to have an avatar which
+is notified of new connections. However, the problems here are twofold:
+it would lead to a thin class which needs to forward all of its methods,
+and it would be impossible to know when disconnections occur. Luckily,
+there is a better pattern:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleAvatar</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-variable">greetings</span> = <span class="py-src-number">0</span>
+ <span class="py-src-variable">connections</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connect</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">connections</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">disconnect</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">connections</span> -= <span class="py-src-number">1</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_greet</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;%d&gt;hello %s&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CachingRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>: <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarId</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>]
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>] = <span class="py-src-variable">SimpleAvatar</span>(<span class="py-src-variable">avatarId</span>)
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">connect</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">p</span>, <span class="py-src-variable">p</span>.<span class="py-src-variable">disconnect</span>
+</pre>
+
+<p>It is possible to use such a pattern to define an arbitrary limit for
+the number of concurrent connections:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleAvatar</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-variable">greetings</span> = <span class="py-src-number">0</span>
+ <span class="py-src-variable">connections</span> = <span class="py-src-number">0</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connect</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">connections</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">disconnect</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">connections</span> -= <span class="py-src-number">1</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_greet</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;%d&gt;hello %s&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">greetings</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CachingRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">max</span>=<span class="py-src-number">1</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span> = {}
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">max</span> = <span class="py-src-variable">max</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>: <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarId</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>]
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarId</span>] = <span class="py-src-variable">SimpleAvatar</span>(<span class="py-src-variable">avatarId</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">p</span>.<span class="py-src-variable">connections</span> &gt;= <span class="py-src-variable">self</span>.<span class="py-src-variable">max</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">ValueError</span>(<span class="py-src-string">&quot;too many connections&quot;</span>)
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">connect</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">p</span>, <span class="py-src-variable">p</span>.<span class="py-src-variable">disconnect</span>
+</pre>
+
+<h2>Managing Clients<a name="auto2"/></h2>
+
+<p>So far, all our realms have ignored the <code>mind</code> argument.
+In the case of PB, the <code>mind</code> is an object supplied by
+the remote login method -- usually, when it passes over the wire,
+it becomes a <code>pb.RemoteReference</code>. This object allows
+sending messages to the client as soon as the connection is established
+and authenticated.</p>
+
+<p>Here is a simple remote-clock application which shows the usefulness
+of the <code>mind</code> argument:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleAvatar</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">client</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TimerService</span>(<span class="py-src-number">1</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">telltime</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span>.<span class="py-src-variable">startService</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span> = <span class="py-src-variable">client</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">telltime</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;notifyTime&quot;</span>, <span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>())
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_setperiod</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">period</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span>.<span class="py-src-variable">stopService</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TimerService</span>(<span class="py-src-variable">period</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">telltime</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span>.<span class="py-src-variable">startService</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">logout</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">s</span>.<span class="py-src-variable">stopService</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Realm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>: <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">SimpleAvatar</span>(<span class="py-src-variable">mind</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">p</span>, <span class="py-src-variable">p</span>.<span class="py-src-variable">logout</span>
+</pre>
+
+<p>In more complicated situations, you might want to cache the avatars
+and give each one a set of <q>current clients</q> or something similar.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-copyable.html b/doc/core/howto/pb-copyable.html
new file mode 100644
index 0000000..3e3f0e4
--- /dev/null
+++ b/doc/core/howto/pb-copyable.html
@@ -0,0 +1,1185 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: PB Copyable: Passing Complex Types</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">PB Copyable: Passing Complex Types</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Motivation</a></li><li><a href="#auto2">Passing Objects</a></li><ul><li><a href="#auto3">Security Options</a></li><li><a href="#auto4">What class to use?</a></li></ul><li><a href="#auto5">pb.Copyable</a></li><ul><li><a href="#auto6">Controlling the Copied State</a></li><li><a href="#auto7">Things To Watch Out For</a></li><li><a href="#auto8">More Information</a></li></ul><li><a href="#auto9">pb.Cacheable</a></li><ul><li><a href="#auto10">Example</a></li><li><a href="#auto11">More Information</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Overview<a name="auto0"/></h2>
+
+<p>This chapter focuses on how to use PB to pass complex types (specifically
+class instances) to and from a remote process. The first section is on
+simply copying the contents of an object to a remote process (<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code>). The second covers how
+to copy those contents once, then update them later when they change (<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">Cacheable</a></code>).</p>
+
+<h2>Motivation<a name="auto1"/></h2>
+
+<p>From the <a href="pb-usage.html" shape="rect">previous chapter</a>, you've seen how to
+pass basic types to a remote process, by using them in the arguments or
+return values of a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.callRemote.html" title="twisted.spread.pb.RemoteReference.callRemote">callRemote</a></code> function. However,
+if you've experimented with it, you may have discovered problems when trying
+to pass anything more complicated than a primitive int/list/dict/string
+type, or another <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code> object. At some point you want
+to pass entire objects between processes, instead of having to reduce them
+down to dictionaries on one end and then re-instantiating them on the
+other.</p>
+
+<h2>Passing Objects<a name="auto2"/></h2>
+
+<p>The most obvious and straightforward way to send an object to a remote
+process is with something like the following code. It also happens that this
+code doesn't work, as will be explained below.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">LilyPond</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">frogs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">frogs</span> = <span class="py-src-variable">frogs</span>
+
+<span class="py-src-variable">pond</span> = <span class="py-src-variable">LilyPond</span>(<span class="py-src-number">12</span>)
+<span class="py-src-variable">ref</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;sendPond&quot;</span>, <span class="py-src-variable">pond</span>)
+</pre>
+
+<p>If you try to run this, you might hope that a suitable remote end which
+implements the <code>remote_sendPond</code> method would see that method get
+invoked with an instance from the <code>LilyPond</code> class. But instead,
+you'll encounter the dreaded <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.InsecureJelly.html" title="twisted.spread.jelly.InsecureJelly">InsecureJelly</a></code> exception. This is
+Twisted's way of telling you that you've violated a security restriction,
+and that the receiving end refuses to accept your object.</p>
+
+<h3>Security Options<a name="auto3"/></h3>
+
+<p>What's the big deal? What's wrong with just copying a class into another
+process' namespace?</p>
+
+<p>Reversing the question might make it easier to see the issue: what is the
+problem with accepting a stranger's request to create an arbitrary object in
+your local namespace? The real question is how much power you are granting
+them: what actions can they convince you to take on the basis of the bytes
+they are sending you over that remote connection.</p>
+
+<p>Objects generally represent more power than basic types like strings and
+dictionaries because they also contain (or reference) code, which can modify
+other data structures when executed. Once previously-trusted data is
+subverted, the rest of the program is compromised.</p>
+
+<p>The built-in Python <q>batteries included</q> classes are relatively
+tame, but you still wouldn't want to let a foreign program use them to
+create arbitrary objects in your namespace or on your computer. Imagine a
+protocol that involved sending a file-like object with a <code>read()</code>
+method that was supposed to used later to retrieve a document. Then imagine
+what if that object were created with
+ <code>os.fdopen(&quot;~/.gnupg/secring.gpg&quot;)</code>. Or an instance of
+ <code>telnetlib.Telnet(&quot;localhost&quot;, &quot;chargen&quot;)</code>. </p>
+
+<p>Classes you've written for your own program are likely to have far more
+power. They may run code during <code>__init__</code>, or even have special
+meaning simply because of their existence. A program might have
+ <code>User</code> objects to represent user accounts, and have a rule that
+says all <code>User</code> objects in the system are referenced when
+authorizing a login session. (In this system, <code>User.__init__</code>
+would probably add the object to a global list of known users). The simple
+act of creating an object would give access to somebody. If you could be
+tricked into creating a bad object, an unauthorized user would get
+access.</p>
+
+<p>So object creation needs to be part of a system's security design. The
+dotted line between <q>trusted inside</q> and <q>untrusted outside</q> needs
+to describe what may be done in response to outside events. One of those
+events is the receipt of an object through a PB remote procedure call, which
+is a request to create an object in your <q>inside</q> namespace. The
+question is what to do in response to it. For this reason, you must
+explicitly specify what remote classes will be accepted, and how their
+local representatives are to be created.</p>
+
+<h3>What class to use?<a name="auto4"/></h3>
+
+<p>Another basic question to answer before we can do anything useful with an
+incoming serialized object is: what class should we create? The simplistic
+answer is to create the <q>same kind</q> that was serialized on the sender's
+end of the wire, but this is not as easy or as straightforward as you might
+think. Remember that the request is coming from a different program, using a
+potentially different set of class libraries. In fact, since PB has also
+been implemented in Java, Emacs-Lisp, and other languages, there's no
+guarantee that the sender is even running Python! All we know on the
+receiving end is a list of two things which describe the instance they are
+trying to send us: the name of the class, and a representation of the
+contents of the object.</p>
+
+
+<p>PB lets you specify the mapping from remote class names to local classes
+with the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.setUnjellyableForClass.html" title="twisted.spread.jelly.setUnjellyableForClass">setUnjellyableForClass</a></code> function
+<a href="#footnote-1" title="Note that, in this context, unjelly is a verb with the opposite meaning of jelly. The verb to jelly means to serialize an object or data structure into a sequence of bytes (or other primitive transmittable/storable representation), while to unjelly means to unserialize the bytestream into a live object in the receiver's memory space. Unjellyable is a noun, (not an adjective), referring to the the class that serves as a destination or recipient of the unjellying process. A is unjellyable into B means that a serialized representation A (of some remote object) can be unserialized into a local object of type B. It is these objects B that are the Unjellyable second argument of the setUnjellyableForClass function. In particular, unjellyable does not mean cannot be jellied. Unpersistable means not persistable, but unjelly, unserialize, and unpickle mean to reverse the operations of jellying, serializing, and pickling."><super>1</super></a>.
+
+
+This function takes a remote/sender class reference (either the
+fully-qualified name as used by the sending end, or a class object from
+which the name can be extracted), and a local/recipient class (used to
+create the local representation for incoming serialized objects). Whenever
+the remote end sends an object, the class name that they transmit is looked
+up in the table controlled by this function. If a matching class is found,
+it is used to create the local object. If not, you get the
+ <code>InsecureJelly</code> exception.</p>
+
+<p>In general you expect both ends to share the same codebase: either you
+control the program that is running on both ends of the wire, or both
+programs share some kind of common language that is implemented in code
+which exists on both ends. You wouldn't expect them to send you an object of
+the MyFooziWhatZit class unless you also had a definition for that class. So
+it is reasonable for the Jelly layer to reject all incoming classes except
+the ones that you have explicitly marked with
+ <code>setUnjellyableForClass</code>. But keep in mind that the sender's idea
+of a <code>User</code> object might differ from the recipient's, either
+through namespace collisions between unrelated packages, version skew
+between nodes that haven't been updated at the same rate, or a malicious
+intruder trying to cause your code to fail in some interesting or
+potentially vulnerable way.</p>
+
+
+<h2>pb.Copyable<a name="auto5"/></h2>
+
+<p>Ok, enough of this theory. How do you send a fully-fledged object from
+one side to the other?</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>, <span class="py-src-variable">jelly</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LilyPond</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setStuff</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">color</span>, <span class="py-src-parameter">numFrogs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">color</span> = <span class="py-src-variable">color</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">numFrogs</span> = <span class="py-src-variable">numFrogs</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">countFrogs</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;%d frogs&quot;</span> % <span class="py-src-variable">self</span>.<span class="py-src-variable">numFrogs</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CopyPond</span>(<span class="py-src-parameter">LilyPond</span>, <span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Copyable</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Sender</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span> = <span class="py-src-variable">pond</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">remote</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">remote</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;takePond&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">ok</span>).<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">notOk</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">ok</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;pond arrived&quot;</span>, <span class="py-src-variable">response</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">notOk</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;error during takePond:&quot;</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">failure</span>.<span class="py-src-variable">type</span> == <span class="py-src-variable">jelly</span>.<span class="py-src-variable">InsecureJelly</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; InsecureJelly&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">failure</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">copy_sender</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">CopyPond</span> <span class="py-src-comment"># so it's not __main__.CopyPond</span>
+ <span class="py-src-variable">pond</span> = <span class="py-src-variable">CopyPond</span>()
+ <span class="py-src-variable">pond</span>.<span class="py-src-variable">setStuff</span>(<span class="py-src-string">&quot;green&quot;</span>, <span class="py-src-number">7</span>)
+ <span class="py-src-variable">pond</span>.<span class="py-src-variable">countFrogs</span>()
+ <span class="py-src-comment"># class name:</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;.&quot;</span>.<span class="py-src-variable">join</span>([<span class="py-src-variable">pond</span>.<span class="py-src-variable">__class__</span>.<span class="py-src-variable">__module__</span>, <span class="py-src-variable">pond</span>.<span class="py-src-variable">__class__</span>.<span class="py-src-variable">__name__</span>])
+
+ <span class="py-src-variable">sender</span> = <span class="py-src-variable">Sender</span>(<span class="py-src-variable">pond</span>)
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">deferred</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">sender</span>.<span class="py-src-variable">got_obj</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/copy_sender.py"><span class="filename">listings/pb/copy_sender.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+</p><span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+PB copy receiver example.
+
+This is a Twisted Application Configuration (tac) file. Run with e.g.
+ twistd -ny copy_receiver.tac
+
+See the twistd(1) man page or
+http://twistedmatrix.com/documents/current/howto/application for details.
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">__doc__</span>
+ <span class="py-src-variable">sys</span>.<span class="py-src-variable">exit</span>(<span class="py-src-number">1</span>)
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>, <span class="py-src-variable">internet</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">copy_sender</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LilyPond</span>, <span class="py-src-variable">CopyPond</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-comment">#log.startLogging(sys.stdout)</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ReceiverPond</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">RemoteCopy</span>, <span class="py-src-parameter">LilyPond</span>):
+ <span class="py-src-keyword">pass</span>
+<span class="py-src-variable">pb</span>.<span class="py-src-variable">setUnjellyableForClass</span>(<span class="py-src-variable">CopyPond</span>, <span class="py-src-variable">ReceiverPond</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Receiver</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_takePond</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; got pond:&quot;</span>, <span class="py-src-variable">pond</span>
+ <span class="py-src-variable">pond</span>.<span class="py-src-variable">countFrogs</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;safe and sound&quot;</span> <span class="py-src-comment"># positive acknowledgement</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_shutdown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;copy_receiver&quot;</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">Receiver</span>())).<span class="py-src-variable">setServiceParent</span>(
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/pb/copy_receiver.tac"><span class="filename">listings/pb/copy_receiver.tac</span></a></div></div>
+
+<p>The sending side has a class called <code>LilyPond</code>. To make this
+eligble for transport through <code>callRemote</code> (either as an
+argument, a return value, or something referenced by either of those [like a
+dictionary value]), it must inherit from one of the four <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Serializable.html" title="twisted.spread.pb.Serializable">Serializable</a></code> classes. In this section,
+we focus on <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">Copyable</a></code>.
+The copyable subclass of <code>LilyPond</code> is called
+ <code>CopyPond</code>. We create an instance of it and send it through
+ <code>callRemote</code> as an argument to the receiver's
+ <code>remote_takePond</code> method. The Jelly layer will serialize
+(<q>jelly</q>) that object as an instance with a class name of
+<q>copy_sender.CopyPond</q> and some chunk of data that represents the
+object's state. <code>pond.__class__.__module__</code> and
+ <code>pond.__class__.__name__</code> are used to derive the class name
+string. The object's <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.Copyable.getStateToCopy.html" title="twisted.spread.flavors.Copyable.getStateToCopy">getStateToCopy</a></code> method is
+used to get the state: this is provided by <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code>, and the default just retrieves
+ <code>self.__dict__</code>. This works just like the optional
+ <code>__getstate__</code> method used by <code>pickle</code>. The pair of
+name and state are sent over the wire to the receiver.</p>
+
+<p>The receiving end defines a local class named <code>ReceiverPond</code>
+to represent incoming <code>LilyPond</code> instances. This class derives
+from the sender's <code>LilyPond</code> class (with a fully-qualified name
+of <code>copy_sender.LilyPond</code>), which specifies how we expect it to
+behave. We trust that this is the same <code>LilyPond</code> class as the
+sender used. (At the very least, we hope ours will be able to accept a state
+created by theirs). It also inherits from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">pb.RemoteCopy</a></code>, which is a requirement for all
+classes that act in this local-representative role (those which are given to
+the second argument of <code>setUnjellyableForClass</code>).
+ <code>RemoteCopy</code> provides the methods that tell the Jelly layer how
+to create the local object from the incoming serialized state.</p>
+
+<p>Then <code>setUnjellyableForClass</code> is used to register the two
+classes. This has two effects: instances of the remote class (the first
+argument) will be allowed in through the security layer, and instances of
+the local class (the second argument) will be used to contain the state that
+is transmitted when the sender serializes the remote object.</p>
+
+<p>When the receiver unserializes (<q>unjellies</q>) the object, it will
+create an instance of the local <code>ReceiverPond</code> class, and hand
+the transmitted state (usually in the form of a dictionary) to that object's
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.RemoteCopy.setCopyableState.html" title="twisted.spread.flavors.RemoteCopy.setCopyableState">setCopyableState</a></code> method.
+This acts just like the <code>__setstate__</code> method that
+ <code>pickle</code> uses when unserializing an object.
+ <code>getStateToCopy</code>/<code>setCopyableState</code> are distinct from
+ <code>__getstate__</code>/<code>__setstate__</code> to allow objects to be
+persisted (across time) differently than they are transmitted (across
+[memory]space).</p>
+
+<p>When this is run, it produces the following output:</p>
+
+<pre class="shell" xml:space="preserve">
+[-] twisted.spread.pb.PBServerFactory starting on 8800
+[-] Starting factory &lt;twisted.spread.pb.PBServerFactory instance at
+0x406159cc&gt;
+[Broker,0,127.0.0.1] got pond: &lt;__builtin__.ReceiverPond instance at
+0x406ec5ec&gt;
+[Broker,0,127.0.0.1] 7 frogs
+</pre>
+
+<pre class="shell" xml:space="preserve">
+$ ./copy_sender.py
+7 frogs
+copy_sender.CopyPond
+pond arrived safe and sound
+Main loop terminated.
+$
+</pre>
+
+
+
+<h3>Controlling the Copied State<a name="auto6"/></h3>
+
+<p>By overriding <code>getStateToCopy</code> and
+ <code>setCopyableState</code>, you can control how the object is transmitted
+over the wire. For example, you might want perform some data-reduction:
+pre-compute some results instead of sending all the raw data over the wire.
+Or you could replace references to a local object on the sender's side with
+markers before sending, then upon receipt replace those markers with
+references to a receiver-side proxy that could perform the same operations
+against a local cache of data.</p>
+
+<p>Another good use for <code>getStateToCopy</code> is to implement
+<q>local-only</q> attributes: data that is only accessible by the local
+process, not to any remote users. For example, a <code>.password</code>
+attribute could be removed from the object state before sending to a remote
+system. Combined with the fact that <code>Copyable</code> objects return
+unchanged from a round trip, this could be used to build a
+challenge-response system (in fact PB does this with
+ <code>pb.Referenceable</code> objects to implement authorization as
+described <a href="pb-cred.html" shape="rect">here</a>).</p>
+
+<p>Whatever <code>getStateToCopy</code> returns from the sending object will
+be serialized and sent over the wire; <code>setCopyableState</code> gets
+whatever comes over the wire and is responsible for setting up the state of
+the object it lives in.</p>
+
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FrogPond</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">numFrogs</span>, <span class="py-src-parameter">numToads</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">numFrogs</span> = <span class="py-src-variable">numFrogs</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">numToads</span> = <span class="py-src-variable">numToads</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">count</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">numFrogs</span> + <span class="py-src-variable">self</span>.<span class="py-src-variable">numToads</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SenderPond</span>(<span class="py-src-parameter">FrogPond</span>, <span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Copyable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getStateToCopy</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">__dict__</span>.<span class="py-src-variable">copy</span>()
+ <span class="py-src-variable">d</span>[<span class="py-src-string">'frogsAndToads'</span>] = <span class="py-src-variable">d</span>[<span class="py-src-string">'numFrogs'</span>] + <span class="py-src-variable">d</span>[<span class="py-src-string">'numToads'</span>]
+ <span class="py-src-keyword">del</span> <span class="py-src-variable">d</span>[<span class="py-src-string">'numFrogs'</span>]
+ <span class="py-src-keyword">del</span> <span class="py-src-variable">d</span>[<span class="py-src-string">'numToads'</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ReceiverPond</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">RemoteCopy</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setCopyableState</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">state</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">__dict__</span> = <span class="py-src-variable">state</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">count</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">frogsAndToads</span>
+
+<span class="py-src-variable">pb</span>.<span class="py-src-variable">setUnjellyableForClass</span>(<span class="py-src-variable">SenderPond</span>, <span class="py-src-variable">ReceiverPond</span>)
+</pre><div class="caption">Source listing - <a href="listings/pb/copy2_classes.py"><span class="filename">listings/pb/copy2_classes.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>, <span class="py-src-variable">jelly</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">copy2_classes</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SenderPond</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Sender</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span> = <span class="py-src-variable">pond</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">obj</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">obj</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;takePond&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">ok</span>).<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">notOk</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">ok</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;pond arrived&quot;</span>, <span class="py-src-variable">response</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">notOk</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;error during takePond:&quot;</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">failure</span>.<span class="py-src-variable">type</span> == <span class="py-src-variable">jelly</span>.<span class="py-src-variable">InsecureJelly</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; InsecureJelly&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">failure</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">pond</span> = <span class="py-src-variable">SenderPond</span>(<span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;count %d&quot;</span> % <span class="py-src-variable">pond</span>.<span class="py-src-variable">count</span>()
+
+ <span class="py-src-variable">sender</span> = <span class="py-src-variable">Sender</span>(<span class="py-src-variable">pond</span>)
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">deferred</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">sender</span>.<span class="py-src-variable">got_obj</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/copy2_sender.py"><span class="filename">listings/pb/copy2_sender.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>, <span class="py-src-variable">internet</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">copy2_classes</span> <span class="py-src-comment"># needed to get ReceiverPond registered with Jelly</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Receiver</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_takePond</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; got pond:&quot;</span>, <span class="py-src-variable">pond</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; count %d&quot;</span> % <span class="py-src-variable">pond</span>.<span class="py-src-variable">count</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;safe and sound&quot;</span> <span class="py-src-comment"># positive acknowledgement</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_shutdown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;copy_receiver&quot;</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">Receiver</span>())).<span class="py-src-variable">setServiceParent</span>(
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/pb/copy2_receiver.py"><span class="filename">listings/pb/copy2_receiver.py</span></a></div></div>
+
+<p>In this example, the classes are defined in a separate source file, which
+also sets up the binding between them. The <code>SenderPond</code> and
+<code>ReceiverPond</code> are unrelated save for this binding: they happen
+to implement the same methods, but use different internal instance variables
+to accomplish them.</p>
+
+<p>The recipient of the object doesn't even have to import the class
+definition into their namespace. It is sufficient that they import the class
+definition (and thus execute the <code>setUnjellyableForClass</code>
+statement). The Jelly layer remembers the class definition until a matching
+object is received. The sender of the object needs the definition, of
+course, to create the object in the first place.</p>
+
+<p>When run, the <code>copy2</code> example emits the following:</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd -n -y copy2_receiver.py
+[-] twisted.spread.pb.PBServerFactory starting on 8800
+[-] Starting factory &lt;twisted.spread.pb.PBServerFactory instance at
+0x40604b4c&gt;
+[Broker,0,127.0.0.1] got pond: &lt;copy2_classes.ReceiverPond instance at
+0x406eb2ac&gt;
+[Broker,0,127.0.0.1] count 7
+</pre>
+
+<pre class="shell" xml:space="preserve">
+$ ./copy2_sender.py
+count 7
+pond arrived safe and sound
+Main loop terminated.
+</pre>
+
+
+
+<h3>Things To Watch Out For<a name="auto7"/></h3>
+
+<ul>
+
+ <li>The first argument to <code>setUnjellyableForClass</code> must refer
+ to the class <em>as known by the sender</em>. The sender has no way of
+ knowing about how your local <code>import</code> statements are set up,
+ and Python's flexible namespace semantics allow you to access the same
+ class through a variety of different names. You must match whatever the
+ sender does. Having both ends import the class from a separate file, using
+ a canonical module name (no <q>sibiling imports</q>), is a good way to get
+ this right, especially when both the sending and the receiving classes are
+ defined together, with the <code>setUnjellyableForClass</code> immediately
+ following them.</li>
+
+ <li>The class that is sent must inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code>. The class that is registered to
+ receive it must inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">pb.RemoteCopy</a></code><a href="#footnote-2" title="pb.RemoteCopy is actually defined in twisted.spread.flavors, but pb.RemoteCopy is the preferred way to access it"><super>2</super></a>. </li>
+
+ <li>The same class can be used to send and receive. Just have it inherit
+ from both <code>pb.Copyable</code> and <code>pb.RemoteCopy</code>. This
+ will also make it possible to send the same class symmetrically back and
+ forth over the wire. But don't get confused about when it is coming (and
+ using <code>setCopyableState</code>) versus when it is going (using
+ <code>getStateToCopy</code>).</li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.InsecureJelly.html" title="twisted.spread.jelly.InsecureJelly">InsecureJelly</a></code>
+ exceptions are raised by the receiving end. They will be delivered
+ asynchronously to an <code>errback</code> handler. If you do not add one
+ to the <code>Deferred</code> returned by <code>callRemote</code>, then you
+ will never receive notification of the problem. </li>
+
+ <li>The class that is derived from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">pb.RemoteCopy</a></code> will be created using a
+ constructor <code>__init__</code> method that takes no arguments. All
+ setup must be performed in the <code>setCopyableState</code> method. As
+ the docstring on <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">RemoteCopy</a></code> says, don't implement a
+ constructor that requires arguments in a subclass of
+ <code>RemoteCopy</code>.</li>
+
+
+
+
+
+</ul>
+
+<h3>More Information<a name="auto8"/></h3>
+
+<ul>
+
+ <li> <code>pb.Copyable</code> is mostly implemented
+ in <code>twisted.spread.flavors</code>, and the docstrings there are
+ the best source of additional information.</li>
+
+ <li><code>Copyable</code> is also used in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.distrib.html" title="twisted.web.distrib">twisted.web.distrib</a></code> to deliver HTTP requests to other
+ programs for rendering, allowing subtrees of URL space to be delegated to
+ multiple programs (on multiple machines).</li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.manhole.explorer.html" title="twisted.manhole.explorer">twisted.manhole.explorer</a></code> also uses
+ <code>Copyable</code> to distribute debugging information from the program
+ under test to the debugging tool.</li>
+
+</ul>
+
+
+<h2>pb.Cacheable<a name="auto9"/></h2>
+
+<p>Sometimes the object you want to send to the remote process is big and
+slow. <q>big</q> means it takes a lot of data (storage, network bandwidth,
+processing) to represent its state. <q>slow</q> means that state doesn't
+change very frequently. It may be more efficient to send the full state only
+once, the first time it is needed, then afterwards only send the differences
+or changes in state whenever it is modified. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">pb.Cacheable</a></code> class provides a framework to
+implement this.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">pb.Cacheable</a></code> is derived
+from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code>, so it is
+based upon the idea of an object's state being captured on the sending side,
+and then turned into a new object on the receiving side. This is extended to
+have an object <q>publishing</q> on the sending side (derived from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">pb.Cacheable</a></code>), matched with one
+<q>observing</q> on the receiving side (derived from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCache.html" title="twisted.spread.pb.RemoteCache">pb.RemoteCache</a></code>).</p>
+
+<p>To effectively use <code>pb.Cacheable</code>, you need to isolate changes
+to your object into accessor functions (specifically <q>setter</q>
+functions). Your object needs to get control <em>every</em> single time some
+attribute is changed<a href="#footnote-3" title="Of course you could be clever and add a hook to __setattr__, along with magical change-announcing subclasses of the usual builtin types, to detect changes that result from normal = set operations. The semi-magical property attributes that were introduced in Python 2.2 could be useful too. The result might be hard to maintain or extend, though."><super>3</super></a>.</p>
+
+<p>You derive your sender-side class from <code>pb.Cacheable</code>, and you
+add two methods: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.Cacheable.getStateToCacheAndObserveFor.html" title="twisted.spread.flavors.Cacheable.getStateToCacheAndObserveFor">getStateToCacheAndObserveFor</a></code>
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.Cacheable.stoppedObserving.html" title="twisted.spread.flavors.Cacheable.stoppedObserving">stoppedObserving</a></code>. The first
+is called when a remote caching reference is first created, and retrieves
+the data with which the cache is first filled. It also provides an
+object called the <q>observer</q> <a href="#footnote-4" title="This is actually a RemoteCacheObserver, but it isn't very useful to subclass or modify, so simply treat it as a little demon that sits in your pb.Cacheable class and helps you distribute change notifications. The only useful thing to do with it is to run its callRemote method, which acts just like a normal pb.Referenceable's method of the same name."><super>4</super></a> that points at that receiver-side cache. Every time the state of the object
+is changed, you give a message to the observer, informing them of the
+change. The other method, <code>stoppedObserving</code>, is called when the
+remote cache goes away, so that you can stop sending updates.</p>
+
+<p>On the receiver end, you make your cache class inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCache.html" title="twisted.spread.pb.RemoteCache">pb.RemoteCache</a></code>, and implement the
+ <code>setCopyableState</code> as you would for a <code>pb.RemoteCopy</code>
+object. In addition, you must implement methods to receive the updates sent
+to the observer by the <code>pb.Cacheable</code>: these methods should have
+names that start with <code>observe_</code>, and match the
+ <code>callRemote</code> invocations from the sender side just as the usual
+ <code>remote_*</code> and <code>perspective_*</code> methods match normal
+ <code>callRemote</code> calls. </p>
+
+<p>The first time a reference to the <code>pb.Cacheable</code> object is
+sent to any particular recipient, a sender-side Observer will be created for
+it, and the <code>getStateToCacheAndObserveFor</code> method will be called
+to get the current state and register the Observer. The state which that
+returns is sent to the remote end and turned into a local representation
+using <code>setCopyableState</code> just like <code>pb.RemoteCopy</code>,
+described above (in fact it inherits from that class). </p>
+
+<p>After that, your <q>setter</q> functions on the sender side should call
+ <code>callRemote</code> on the Observer, which causes <code>observe_*</code>
+methods to run on the receiver, which are then supposed to update the
+receiver-local (cached) state.</p>
+
+<p>When the receiver stops following the cached object and the last
+reference goes away, the <code>pb.RemoteCache</code> object can be freed.
+Just before it dies, it tells the sender side it no longer cares about the
+original object. When <em>that</em> reference count goes to zero, the
+Observer goes away and the <code>pb.Cacheable</code> object can stop
+announcing every change that takes place. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.Cacheable.stoppedObserving.html" title="twisted.spread.flavors.Cacheable.stoppedObserving">stoppedObserving</a></code> method is
+used to tell the <code>pb.Cacheable</code> that the Observer has gone
+away.</p>
+
+<p>With the <code>pb.Cacheable</code> and <code>pb.RemoteCache</code>
+classes in place, bound together by a call to
+ <code>pb.setUnjellyableForClass</code>, all that remains is to pass a
+reference to your <code>pb.Cacheable</code> over the wire to the remote end.
+The corresponding <code>pb.RemoteCache</code> object will automatically be
+created, and the matching methods will be used to keep the receiver-side
+slave object in sync with the sender-side master object.</p>
+
+<h3>Example<a name="auto10"/></h3>
+
+<p>Here is a complete example, in which the <code>MasterDuckPond</code> is
+controlled by the sending side, and the <code>SlaveDuckPond</code> is a
+cache that tracks changes to the master:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MasterDuckPond</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Cacheable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">ducks</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">observers</span> = []
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">ducks</span> = <span class="py-src-variable">ducks</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">count</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I have [%d] ducks&quot;</span> % <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">ducks</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">addDuck</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">duck</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">ducks</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">duck</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">o</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">observers</span>: <span class="py-src-variable">o</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">'addDuck'</span>, <span class="py-src-variable">duck</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">removeDuck</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">duck</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">ducks</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">duck</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">o</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">observers</span>: <span class="py-src-variable">o</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">'removeDuck'</span>, <span class="py-src-variable">duck</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getStateToCacheAndObserveFor</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">perspective</span>, <span class="py-src-parameter">observer</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">observers</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">observer</span>)
+ <span class="py-src-comment"># you should ignore pb.Cacheable-specific state, like self.observers</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">ducks</span> <span class="py-src-comment"># in this case, just a list of ducks</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stoppedObserving</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">perspective</span>, <span class="py-src-parameter">observer</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">observers</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">observer</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SlaveDuckPond</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">RemoteCache</span>):
+ <span class="py-src-comment"># This is a cache of a remote MasterDuckPond</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">count</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">cacheducks</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getDucks</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">cacheducks</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setCopyableState</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">state</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; cache - sitting, er, setting ducks&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">cacheducks</span> = <span class="py-src-variable">state</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">observe_addDuck</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">newDuck</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; cache - addDuck&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">cacheducks</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">newDuck</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">observe_removeDuck</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">deadDuck</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; cache - removeDuck&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">cacheducks</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">deadDuck</span>)
+
+<span class="py-src-variable">pb</span>.<span class="py-src-variable">setUnjellyableForClass</span>(<span class="py-src-variable">MasterDuckPond</span>, <span class="py-src-variable">SlaveDuckPond</span>)
+</pre><div class="caption">Source listing - <a href="listings/pb/cache_classes.py"><span class="filename">listings/pb/cache_classes.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>, <span class="py-src-variable">jelly</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">cache_classes</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">MasterDuckPond</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Sender</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span> = <span class="py-src-variable">pond</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase1</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">remote</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">remote</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;takePond&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">phase2</span>).<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase2</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">response</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">addDuck</span>(<span class="py-src-string">&quot;ugly duckling&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">count</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">1</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">phase3</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase3</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;checkDucks&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">phase4</span>).<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase4</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">dummy</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">removeDuck</span>(<span class="py-src-string">&quot;one duck&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">count</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;checkDucks&quot;</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;ignorePond&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">phase5</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase5</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">dummy</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;shutdown&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">phase6</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">phase6</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">dummy</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">master</span> = <span class="py-src-variable">MasterDuckPond</span>([<span class="py-src-string">&quot;one duck&quot;</span>, <span class="py-src-string">&quot;two duck&quot;</span>])
+ <span class="py-src-variable">master</span>.<span class="py-src-variable">count</span>()
+
+ <span class="py-src-variable">sender</span> = <span class="py-src-variable">Sender</span>(<span class="py-src-variable">master</span>)
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">deferred</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">sender</span>.<span class="py-src-variable">phase1</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/cache_sender.py"><span class="filename">listings/pb/cache_sender.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>, <span class="py-src-variable">internet</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cache_classes</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Receiver</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_takePond</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pond</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span> = <span class="py-src-variable">pond</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got pond:&quot;</span>, <span class="py-src-variable">pond</span> <span class="py-src-comment"># a DuckPondCache</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote_checkDucks</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_checkDucks</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;[%d] ducks: &quot;</span> % <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">count</span>(), <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span>.<span class="py-src-variable">getDucks</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_ignorePond</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-comment"># stop watching the pond</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;dropping pond&quot;</span>
+ <span class="py-src-comment"># gc causes __del__ causes 'decache' msg causes stoppedObserving</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">pond</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_shutdown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;copy_receiver&quot;</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">Receiver</span>())).<span class="py-src-variable">setServiceParent</span>(
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/pb/cache_receiver.py"><span class="filename">listings/pb/cache_receiver.py</span></a></div></div>
+<p>When run, this example emits the following:</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd -n -y cache_receiver.py
+[-] twisted.spread.pb.PBServerFactory starting on 8800
+[-] Starting factory &lt;twisted.spread.pb.PBServerFactory instance at
+0x40615acc&gt;
+[Broker,0,127.0.0.1] cache - sitting, er, setting ducks
+[Broker,0,127.0.0.1] got pond: &lt;cache_classes.SlaveDuckPond instance at
+0x406eb5ec&gt;
+[Broker,0,127.0.0.1] [2] ducks: ['one duck', 'two duck']
+[Broker,0,127.0.0.1] cache - addDuck
+[Broker,0,127.0.0.1] [3] ducks: ['one duck', 'two duck', 'ugly duckling']
+[Broker,0,127.0.0.1] cache - removeDuck
+[Broker,0,127.0.0.1] [2] ducks: ['two duck', 'ugly duckling']
+[Broker,0,127.0.0.1] dropping pond
+</pre>
+
+<pre class="shell" xml:space="preserve">
+$ ./cache_sender.py
+I have [2] ducks
+I have [3] ducks
+I have [2] ducks
+Main loop terminated.
+</pre>
+
+
+<p>Points to notice:</p>
+
+<ul>
+ <li>There is one <code>Observer</code> for each remote program that holds
+ an active reference. Multiple references inside the same program don't
+ matter: the serialization layer notices the duplicates and does the
+ appropriate reference counting<a href="#footnote-5" title="This applies to multiple references through the same Broker. If you've managed to make multiple TCP connections to the same program, you deserve whatever you get."><super>5</super></a>.
+ </li>
+
+ <li>Multiple Observers need to be kept in a list, and all of them need to
+ be updated when something changes. By sending the initial state at the
+ same time as you add the observer to the list, in a single atomic action
+ that cannot be interrupted by a state change, you insure that you can send
+ the same status update to all the observers.</li>
+
+ <li>The <code>observer.callRemote</code> calls can still fail. If the
+ remote side has disconnected very recently and
+ <code>stoppedObserving</code> has not yet been called, you may get a
+ <code>DeadReferenceError</code>. It is a good idea to add an errback to
+ those <code>callRemote</code>s to throw away such an error. This is a
+ useful idiom:
+
+ <pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">observer</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">'foo'</span>, <span class="py-src-variable">arg</span>).<span class="py-src-variable">addErrback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">f</span>: <span class="py-src-variable">None</span>)
+</pre>
+ </li>
+
+
+ <li><code>getStateToCacheAndObserverFor</code> must return some object
+ that represents the current state of the object. This may simply be the
+ object's <code>__dict__</code> attribute. It is a good idea to remove the
+ <code>pb.Cacheable</code>-specific members of it before sending it to the
+ remote end. The list of Observers, in particular, should be left out, to
+ avoid dizzying recursive Cacheable references. The mind boggles as to the
+ potential consequences of leaving in such an item.</li>
+
+ <li>A <code>perspective</code> argument is available to
+ <code>getStateToCacheAndObserveFor</code>, as well as
+ <code>stoppedObserving</code>. I think the purpose of this is to allow
+ viewer-specific changes to the way the cache is updated. If all remote
+ viewers are supposed to see the same data, it can be ignored.</li>
+
+</ul>
+
+
+
+
+<h3>More Information<a name="auto11"/></h3>
+
+<ul>
+ <li>The best source for information comes from the docstrings
+ in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.html" title="twisted.spread.flavors">twisted.spread.flavors</a></code>,
+ where <code>pb.Cacheable</code> is implemented.</li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.manhole.explorer.html" title="twisted.manhole.explorer">twisted.manhole.explorer</a></code> uses
+ <code>Cacheable</code>, and does some fairly interesting things with it.</li>
+
+ <li>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.publish.html" title="twisted.spread.publish">spread.publish</a></code> module also
+ uses <code>Cacheable</code>, and might be a source of further
+ information.</li>
+</ul>
+
+
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">Note that, in this context, <q>unjelly</q> is
+a verb with the opposite meaning of <q>jelly</q>. The verb <q>to jelly</q>
+means to serialize an object or data structure into a sequence of bytes (or
+other primitive transmittable/storable representation), while <q>to
+unjelly</q> means to unserialize the bytestream into a live object in the
+receiver's memory space. <q>Unjellyable</q> is a noun, (<em>not</em> an
+adjective), referring to the the class that serves as a destination or
+recipient of the unjellying process. <q>A is unjellyable into B</q> means
+that a serialized representation A (of some remote object) can be
+unserialized into a local object of type B. It is these objects <q>B</q>
+that are the <q>Unjellyable</q> second argument of the
+<code>setUnjellyableForClass</code> function.
+In particular, <q>unjellyable</q> does <em>not</em> mean <q>cannot be
+jellied</q>. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.Unpersistable.html" title="twisted.spread.jelly.Unpersistable">Unpersistable</a></code> means <q>not
+persistable</q>, but <q>unjelly</q>, <q>unserialize</q>, and <q>unpickle</q>
+mean to reverse the operations of <q>jellying</q>, <q>serializing</q>, and
+<q>pickling</q>.</span></a></li><li><a name="footnote-2"><span class="footnote"><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">pb.RemoteCopy</a></code> is actually defined
+ in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.flavors.html" title="twisted.spread.flavors">twisted.spread.flavors</a></code>, but
+ <code>pb.RemoteCopy</code> is the preferred way to access it</span></a></li><li><a name="footnote-3"><span class="footnote">Of course you could be clever and
+add a hook to <code>__setattr__</code>, along with magical change-announcing
+subclasses of the usual builtin types, to detect changes that result from
+normal <q>=</q> set operations. The semi-magical <q>property attributes</q>
+that were introduced in Python 2.2 could be useful too. The result might be
+hard to maintain or extend, though.</span></a></li><li><a name="footnote-4"><span class="footnote">This is actually a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCacheObserver.html" title="twisted.spread.pb.RemoteCacheObserver">RemoteCacheObserver</a></code>, but it isn't very
+useful to subclass or modify, so simply treat it as a little demon that sits
+in your <code>pb.Cacheable</code> class and helps you distribute change
+notifications. The only useful thing to do with it is to run its
+<code>callRemote</code> method, which acts just like a normal
+<code>pb.Referenceable</code>'s method of the same name.</span></a></li><li><a name="footnote-5"><span class="footnote">This applies to
+ multiple references through the same <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Broker.html" title="twisted.spread.pb.Broker">Broker</a></code>. If you've managed to make multiple
+ TCP connections to the same program, you deserve whatever you get.</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-cred.html b/doc/core/howto/pb-cred.html
new file mode 100644
index 0000000..1599806
--- /dev/null
+++ b/doc/core/howto/pb-cred.html
@@ -0,0 +1,1724 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Authentication with Perspective Broker</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Authentication with Perspective Broker</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Compartmentalizing Services</a></li><ul><li><a href="#auto2">Incorrect Arguments</a></li><li><a href="#auto3">Unforgeable References</a></li><li><a href="#auto4">Argument Typechecking</a></li><li><a href="#auto5">Objects as Capabilities</a></li></ul><li><a href="#auto6">Avatars and Perspectives</a></li><li><a href="#auto7">Perspective Examples</a></li><ul><li><a href="#auto8">One Client</a></li><li><a href="#auto9">Two Clients</a></li><li><a href="#auto10">How that example worked</a></li><li><a href="#auto11">Anonymous Clients</a></li></ul><li><a href="#auto12">Using Avatars</a></li><ul><li><a href="#auto13">Avatar Interfaces</a></li><li><a href="#auto14">Logging Out</a></li><li><a href="#auto15">Making Avatars</a></li><li><a href="#auto16">Connecting and Disconnecting</a></li><li><a href="#auto17">Viewable</a></li><li><a href="#auto18">Chat Server with Avatars</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Overview<a name="auto0"/></h2>
+
+<p>The examples shown in <a href="pb-usage.html" shape="rect">Using Perspective
+Broker</a> demonstrate how to do basic remote method calls, but provided no
+facilities for authentication. In this context, authentication is about who
+gets which remote references, and how to restrict access to the <q>right</q>
+set of people or programs.</p>
+
+<p>As soon as you have a program which offers services to multiple users,
+where those users should not be allowed to interfere with each other, you
+need to think about authentication. Many services use the idea of an
+<q>account</q>, and rely upon fact that each user has access to only one
+account. Twisted uses a system called <a href="cred.html" shape="rect">cred</a> to
+handle authentication issues, and Perspective Broker has code to make it
+easy to implement the most common use cases.</p>
+
+<h2>Compartmentalizing Services<a name="auto1"/></h2>
+
+<p>Imagine how you would write a chat server using PB. The first step might
+be a <code>ChatServer</code> object which had a bunch of
+ <code>pb.RemoteReference</code>s that point at user clients. Pretend that
+those clients offered a <code>remote_print</code> method which lets the
+server print a message on the user's console. In that case, the server might
+look something like this:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatServer</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span> = {} <span class="py-src-comment"># indexed by name</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {} <span class="py-src-comment"># indexed by name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>, <span class="py-src-parameter">groupname</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">groupname</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>] = []
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>].<span class="py-src-variable">append</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">username</span>])
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_sendMessage</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">from_username</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">group</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">group</span>:
+ <span class="py-src-comment"># send the message to all members of the group</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">group</span>:
+ <span class="py-src-variable">user</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;print&quot;</span>,
+ <span class="py-src-string">&quot;&lt;%s&gt; says: %s&quot;</span> % (<span class="py-src-variable">from_username</span>,
+ <span class="py-src-variable">message</span>))
+</pre>
+
+<p>For now, assume that all clients have somehow acquired a
+ <code>pb.RemoteReference</code> to this <code>ChatServer</code> object,
+perhaps using <code>pb.Root</code> and <code>getRootObject</code> as
+described in the <a href="pb-usage.html" shape="rect">previous chapter</a>. In this
+scheme, when a user sends a message to the group, their client runs
+something like the following:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">remotegroup</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;sendMessage&quot;</span>, <span class="py-src-string">&quot;alice&quot;</span>, <span class="py-src-string">&quot;Hi, my name is alice.&quot;</span>)
+</pre>
+
+
+<h3>Incorrect Arguments<a name="auto2"/></h3>
+
+<p>You've probably seen the first problem: users can trivially spoof each
+other. We depend upon the user to pass a correct value in their
+ <q>username</q> argument, and have no way to tell if they're lying or not.
+There is nothing to prevent Alice from modifying her client to do:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">remotegroup</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;sendMessage&quot;</span>, <span class="py-src-string">&quot;bob&quot;</span>, <span class="py-src-string">&quot;i like pork&quot;</span>)
+</pre>
+
+<p>much to the horror of Bob's vegetarian friends.<a href="#footnote-1" title="Apparently Alice is one of those weirdos who has nothing better to do than to try and impersonate Bob. She will lie to her chat client, send incorrect objects to remote methods, even rewrite her local client code entirely to accomplish this juvenile prank. Given this adversarial relationship, one must wonder why she and Bob seem to spend so much time together: their adventures are clearly documented by the cryptographic literature."><super>1</super></a></p>
+
+<p>(In general, learn to get suspicious if you see any argument of a
+remotely-invokable method described as <q>must be X</q>)</p>
+
+<p>The best way to fix this is to keep track of the user's name locally,
+rather than asking them to send it to the server with each message. The best
+place to keep state is in an object, so this suggests we need a per-user
+object. Rather than choosing an obvious name<a href="#footnote-2" title="The obvious name is clearly ServerSidePerUserObjectWhichNobodyElseHasAccessTo, but because Python makes everything else so easy to read, it only seems fair to make your audience work for something."><super>2</super></a>, let's call this the
+ <code>User</code> class.
+</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">User</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>, <span class="py-src-parameter">server</span>, <span class="py-src-parameter">clientref</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">username</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span> = <span class="py-src-variable">server</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">clientref</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span>.<span class="py-src-variable">joinGroup</span>(<span class="py-src-variable">groupname</span>, <span class="py-src-variable">self</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_sendMessage</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span>.<span class="py-src-variable">sendMessage</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">groupname</span>, <span class="py-src-variable">message</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;print&quot;</span>, <span class="py-src-variable">message</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatServer</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span> = {} <span class="py-src-comment"># indexed by name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">groupname</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>] = []
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>].<span class="py-src-variable">append</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">sendMessage</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">from_username</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">group</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">group</span>:
+ <span class="py-src-comment"># send the message to all members of the group</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">group</span>:
+ <span class="py-src-variable">user</span>.<span class="py-src-variable">send</span>(<span class="py-src-string">&quot;&lt;%s&gt; says: %s&quot;</span> % (<span class="py-src-variable">from_username</span>, <span class="py-src-variable">message</span>))
+</pre>
+
+<p>Again, assume that each remote client gets access to a single
+ <code>User</code> object, which is created with the proper username.</p>
+
+<p>Note how the <code>ChatServer</code> object has no remote access: it
+isn't even <code>pb.Referenceable</code> anymore. This means that all access
+to it must be mediated through other objects, with code that is under your
+control.</p>
+
+<p>As long as Alice only has access to her own <code>User</code> object, she
+can no longer spoof Bob. The only way for her to invoke
+ <code>ChatServer.sendMessage</code> is to call her <code>User</code>
+object's <code>remote_sendMessage</code> method, and that method uses its
+own state to provide the <code>from_username</code> argument. It doesn't
+give her any way to change that state.</p>
+
+<p>This restriction is important. The <code>User</code> object is able to
+maintain its own integrity because there is a wall between the object and
+the client: the client cannot inspect or modify internal state, like the
+ <code>.name</code> attribute. The only way through this wall is via remote
+method invocations, and the only control Alice has over those invocations is
+when they get invoked and what arguments they are given.</p>
+
+<div class="note"><strong>Note: </strong>
+<p>No object can maintain its integrity against local threats: by design,
+Python offers no mechanism for class instances to hide their attributes, and
+once an intruder has a copy of <code>self.__dict__</code>, they can do
+everything the original object was able to do.</p>
+</div>
+
+
+<h3>Unforgeable References<a name="auto3"/></h3>
+
+<p>Now suppose you wanted to implement group parameters, for example a mode
+in which nobody was allowed to talk about mattresses because some users were
+sensitive and calming them down after someone said <q>mattress</q> is a
+hassle that's best avoided altogether. Again, per-group state implies a
+per-group object. We'll go out on a limb and call this the
+ <code>Group</code> object:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">User</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>, <span class="py-src-parameter">server</span>, <span class="py-src-parameter">clientref</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">username</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span> = <span class="py-src-variable">server</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">clientref</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">allowMattress</span>=<span class="py-src-parameter">True</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span>.<span class="py-src-variable">joinGroup</span>(<span class="py-src-variable">groupname</span>, <span class="py-src-variable">self</span>, <span class="py-src-variable">allowMattress</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;print&quot;</span>, <span class="py-src-variable">message</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Group</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">allowMattress</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">groupname</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> = <span class="py-src-variable">allowMattress</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = []
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">from_user</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> <span class="py-src-keyword">and</span> <span class="py-src-string">&quot;mattress&quot;</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">message</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">ValueError</span>, <span class="py-src-string">&quot;Don't say that word&quot;</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>:
+ <span class="py-src-variable">user</span>.<span class="py-src-variable">send</span>(<span class="py-src-string">&quot;&lt;%s&gt; says: %s&quot;</span> % (<span class="py-src-variable">from_user</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">message</span>))
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">addUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatServer</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span> = {} <span class="py-src-comment"># indexed by name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">allowMattress</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">groupname</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>] = <span class="py-src-variable">Group</span>(<span class="py-src-variable">groupname</span>, <span class="py-src-variable">allowMattress</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>].<span class="py-src-variable">addUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>]
+</pre>
+
+
+<p>This example takes advantage of the fact that
+ <code>pb.Referenceable</code> objects sent over a wire can be returned to
+you, and they will be turned into references to the same object that you
+originally sent. The client cannot modify the object in any way: all they
+can do is point at it and invoke its <code>remote_*</code> methods. Thus,
+you can be sure that the <code>.name</code> attribute remains the same as
+you left it. In this case, the client code would look something like
+this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientThing</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_print</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">message</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">join</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remoteUser</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;joinGroup&quot;</span>, <span class="py-src-string">&quot;#twisted&quot;</span>,
+ <span class="py-src-variable">allowMattress</span>=<span class="py-src-variable">False</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">gotGroup</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">gotGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">group</span>):
+ <span class="py-src-variable">group</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;send&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">remoteUser</span>, <span class="py-src-string">&quot;hi everybody&quot;</span>)
+</pre>
+
+<p>The <code>User</code> object is sent from the server side, and is turned
+into a <code>pb.RemoteReference</code> when it arrives at the client. The
+client sends it back to <code>Group.remote_send</code>, and PB turns it back
+into a reference to the original <code>User</code> when it gets there.
+ <code>Group.remote_send</code> can then use its <code>.name</code> attribute
+as the sender of the message.</p>
+
+<div class="note"><strong>Note: </strong>
+
+<p>Third party references (there aren't any)</p>
+
+<p>This technique also relies upon the fact that the
+ <code>pb.Referenceable</code> reference can <em>only</em> come from someone
+who holds a corresponding <code>pb.RemoteReference</code>. The design of the
+serialization mechanism (implemented in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.html" title="twisted.spread.jelly">twisted.spread.jelly</a></code>: pb, jelly, spread.. get it? Look for
+<q>banana</q>, too. What other networking framework
+can claim API names based on sandwich ingredients?) makes it impossible for
+a client to obtain a reference that they weren't explicitly given.
+References passed over the wire are given id numbers and recorded in a
+per-connection dictionary. If you didn't give them the reference, the id
+number won't be in the dict, and no amount of guessing by a malicious client
+will give them anything else. The dict goes away when the connection is
+dropped, further limiting the scope of those references.</p>
+
+<p>Futhermore, it is not possible for Bob to send <em>his</em>
+ <code>User</code> reference to Alice (perhaps over some other PB channel
+just between the two of them). Outside the context of Bob's connection to
+the server, that reference is just a meaningless number. To prevent
+confusion, PB will tell you if you try to give it away: when you try to hand
+a <code>pb.RemoteReference</code> to a third party, you'll get an exception
+(implemented with an assert in pb.py:364 RemoteReference.jellyFor).</p>
+
+<p>This helps the security model somewhat: only the client you gave the
+reference to can cause any damage with it. Of course, the client might be a
+brainless zombie, simply doing anything some third party wants. When it's
+not proxying <code>callRemote</code> invocations, it's probably terrorizing
+the living and searching out human brains for sustenance. In short, if you
+don't trust them, don't give them that reference.</p>
+
+<p>And remember that everything you've ever given them over that connection
+can come back to you. If expect the client to invoke your method with some
+object A that you sent to them earlier, and instead they send you object B
+(that you also sent to them earlier), and you don't check it somehow, then
+you've just opened up a security hole (we'll see an example of this
+shortly). It may be better to keep such objects in a dictionary on the
+server side, and have the client send you an index string instead. Doing it
+that way makes it obvious that they can send you anything they want, and
+improves the chances that you'll remember to implement the right checks.
+(This is exactly what PB is doing underneath, with a per-connection
+dictionary of <code>Referenceable</code> objects, indexed by a number).</p>
+
+<p>And, of course, you have to make sure you don't accidentally hand out a
+reference to the wrong object.</p>
+
+</div>
+
+
+<p>But again, note the vulnerability. If Alice holds a
+ <code>RemoteReference</code> to <em>any</em> object on the server side that
+has a <code>.name</code> attribute, she can use that name as a spoofed
+<q>from</q> parameter. As a simple example, what if her client code looked
+like:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientThing</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">join</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remoteUser</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;joinGroup&quot;</span>, <span class="py-src-string">&quot;#twisted&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">gotGroup</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">gotGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">group</span>):
+ <span class="py-src-variable">group</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;send&quot;</span>, <span class="py-src-variable">from_user</span>=<span class="py-src-variable">group</span>, <span class="py-src-string">&quot;hi everybody&quot;</span>)
+</pre>
+
+<p>This would let her send a message that appeared to come from
+<q>#twisted</q> rather than <q>Alice</q>. If she joined a group that
+happened to be named <q>bob</q> (perhaps it is the <q>How To Be Bob</q>
+channel, populated by Alice and countless others, a place where they can
+share stories about their best impersonating-Bob moments), then she would be
+able to emit a message that looked like <q>&lt;bob&gt; says: hi there</q>,
+and she has accomplished her lifelong goal.</p>
+
+
+<h3>Argument Typechecking<a name="auto4"/></h3>
+
+<p>There are two techniques to close this hole. The first is to have your
+remotely-invokable methods do type-checking on their arguments: if
+ <code>Group.remote_send</code> asserted <code>isinstance(from_user,
+User)</code> then Alice couldn't use non-User objects to do her spoofing,
+and hopefully the rest of the system is designed well enough to prevent her
+from obtaining access to somebody else's User object.</p>
+
+
+<h3>Objects as Capabilities<a name="auto5"/></h3>
+
+<p>The second technique is to avoid having the client send you the objects
+altogether. If they don't send you anything, there is nothing to verify. In
+this case, you would have to have a per-user-per-group object, in which the
+ <code>remote_send</code> method would only take a single
+ <code>message</code> argument. The <code>UserGroup</code> object is created
+with references to the only <code>User</code> and <code>Group</code> objects
+that it will ever use, so no lookups are needed:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">UserGroup</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">group</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">group</span> = <span class="py-src-variable">group</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">group</span>.<span class="py-src-variable">send</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">message</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Group</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">allowMattress</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">groupname</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> = <span class="py-src-variable">allowMattress</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = []
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">from_user</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> <span class="py-src-keyword">and</span> <span class="py-src-string">&quot;mattress&quot;</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">message</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">ValueError</span>, <span class="py-src-string">&quot;Don't say that word&quot;</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>:
+ <span class="py-src-variable">user</span>.<span class="py-src-variable">send</span>(<span class="py-src-string">&quot;&lt;%s&gt; says: %s&quot;</span> % (<span class="py-src-variable">from_user</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">message</span>))
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">addUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">user</span>)
+</pre>
+
+<p>The only message-sending method Alice has left is
+ <code>UserGroup.remote_send</code>, and it only accepts a message: there are
+no remaining ways to influence the <q>from</q> name.</p>
+
+<p>In this model, each remotely-accessible object represents a very small
+set of capabilities. Security is achieved by only granting a minimal set of
+abilities to each remote user.</p>
+
+<p>PB provides a shortcut which makes this technique easier to use. The
+ <code>Viewable</code> class will be discussed <a href="#viewable" shape="rect">below</a>.</p>
+
+<h2>Avatars and Perspectives<a name="auto6"/></h2>
+
+<p>In Twisted's <a href="cred.html" shape="rect">cred</a> system, an <q>Avatar</q> is
+an object that lives on the <q>server</q> side (defined here as the side
+farthest from the human who is trying to get something done) which lets the
+remote user get something done. The avatar isn't really a particular class,
+it's more like a description of a role that some object plays, as in <q>the
+Foo object here is acting as the user's avatar for this particular
+service</q>. Generally, the remote user has some way of getting their avatar
+to run some code. The avatar object may enforce some security checks, and
+provide additional data, then call other methods which get things done.</p>
+
+<p>The two pieces in the cred puzzle (for any protocol, not just PB) are:
+<q>what serves as the Avatar?</q>, and <q>how does the user get access to
+it?</q>.</p>
+
+<p>For PB, the first question is easy. The Avatar is a remotely-accessible
+object which can run code: this is a perfect description of
+ <code>pb.Referenceable</code> and its subclasses. We shall defer the second
+question until the next section.</p>
+
+<p>In the example above, you can think of the <code>ChatServer</code> and
+ <code>Group</code> objects as a service. The <code>User</code> object is the
+user's server-side representative: everything the user is capable of doing
+is done by running one of its methods. Anything that the server wants to do
+to the user (change their group membership, change their name, delete their
+pet cat, whatever) is done by manipulating the <code>User</code> object.</p>
+
+<p>There are multiple User objects living in peace and harmony around the
+ChatServer. Each has a different point of view on the services provided by
+the ChatServer and the Groups: each may belong to different groups, some
+might have more permissions than others (like the ability to create groups).
+These different points of view are called <q>Perspectives</q>. This is the
+origin of the term <q>Perspective</q> in <q>Perspective Broker</q>: PB
+provides and controls (i.e. <q>brokers</q>) access to Perspectives.</p>
+
+<p>Once upon a time, these local-representative objects were actually called
+ <code>pb.Perspective</code>. But this has changed with the advent of the
+rewritten cred system, and now the more generic term for a local
+representative object is an Avatar. But you will still see reference to
+ <q>Perspective</q> in the code, the docs, and the module names<a href="#footnote-3" title="We could just go ahead and rename Perspective Broker to be Avatar Broker, but 1) that would cause massive compatibility problems, and 2) AB doesn't fit into the whole sandwich-themed naming scheme nearly as well as PB does. If we changed it to AB, we'd probably have to change Banana to be CD (CoderDecoder), and Jelly to be EF (EncapsulatorFragmentor). twisted.spread would then have to be renamed twisted.alphabetsoup, and then the whole food-pun thing would start all over again."><super>3</super></a>. Just remember
+that perspectives and avatars are basically the same thing. </p>
+
+<p>Despite all we've been <a href="cred.html" shape="rect">telling you</a> about how
+Avatars are more of a concept than an actual class, the base class from
+which you can create your server-side avatar-ish objects is, in fact, named
+ <code>pb.Avatar</code><a href="#footnote-4" title="The avatar-ish class is named pb.Avatar because pb.Perspective was already taken, by the (now obsolete) oldcred perspective-ish class. It is a pity, but it simply wasn't possible both replace pb.Perspective in-place and maintain a reasonable level of backwards-compatibility."><super>4</super></a>. These objects behave very much like
+ <code>pb.Referenceable</code>. The only difference is that instead of
+offering <q>remote_FOO</q> methods, they offer <q>perspective_FOO</q>
+methods.</p>
+
+<p>The other way in which <code>pb.Avatar</code> differs from
+ <code>pb.Referenceable</code> is that the avatar objects are designed to be
+the first thing retrieved by a cred-using remote client. Just as
+ <code>PBClientFactory.getRootObject</code> gives the client access to a
+ <code>pb.Root</code> object (which can then provide access to all kinds of
+other objects), <code>PBClientFactory.login</code> gives client access to a
+ <code>pb.Avatar</code> object (which can return other references). </p>
+
+<p>So, the first half of using cred in your PB application is to create an
+Avatar object which implements <code>perspective_</code> methods and is
+careful to do useful things for the remote user while remaining vigilant
+against being tricked with unexpected argument values. It must also be
+careful to never give access to objects that the user should not have access
+to, whether by returning them directly, returning objects which contain
+them, or returning objects which can be asked (remotely) to provide
+them.</p>
+
+<p>The second half is how the user gets a <code>pb.RemoteReference</code> to
+your Avatar. As explained <a href="cred.html" shape="rect">elsewhere</a>, Avatars are
+obtained from a Realm. The Realm doesn't deal with authentication at all
+(usernames, passwords, public keys, challenge-response systems, retinal
+scanners, real-time DNA sequencers, etc). It simply takes an <q>avatarID</q>
+(which is effectively a username) and returns an Avatar object. The Portal
+and its Checkers deal with authenticating the user: by the time they are
+done, the remote user has proved their right to access the avatarID that is
+given to the Realm, so the Realm can return a remotely-controllable object
+that has whatever powers you wish to grant to this particular user. </p>
+
+<p>For PB, the realm is expected to return a <code>pb.Avatar</code> (or
+anything which implements <code>pb.IPerspective</code>, really, but there's
+no reason to not return a <code>pb.Avatar</code> subclass). This object will
+be given to the client just like a <code>pb.Root</code> would be without
+cred, and the user can get access to other objects through it (if you let
+them).</p>
+
+<p>The basic idea is that there is a separate IPerspective-implementing
+object (i.e. the Avatar subclass) (i.e. the <q>perspective</q>) for each
+user, and <em>only</em> the authorized user gets a remote reference to that
+object. You can store whatever permissions or capabilities the user
+possesses in that object, and then use them when the user invokes a remote
+method. You give the user access to the perspective object instead of the
+objects that do the real work.</p>
+
+
+<h2>Perspective Examples<a name="auto7"/></h2>
+
+<p>Here is a brief example of using a pb.Avatar. Most of the support code
+is magic for now: we'll explain it later.</p>
+
+<h3>One Client<a name="auto8"/></h3>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">checkers</span>, <span class="py-src-variable">portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyPerspective</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_foo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I am&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>, <span class="py-src-string">&quot;perspective_foo(&quot;</span>,<span class="py-src-variable">arg</span>,<span class="py-src-string">&quot;) called on&quot;</span>, <span class="py-src-variable">self</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">MyPerspective</span>(<span class="py-src-variable">avatarId</span>), <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+
+<span class="py-src-variable">p</span> = <span class="py-src-variable">portal</span>.<span class="py-src-variable">Portal</span>(<span class="py-src-variable">MyRealm</span>())
+<span class="py-src-variable">p</span>.<span class="py-src-variable">registerChecker</span>(
+ <span class="py-src-variable">checkers</span>.<span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>(<span class="py-src-variable">user1</span>=<span class="py-src-string">&quot;pass1&quot;</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">p</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb5server.py"><span class="filename">listings/pb/pb5server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">credentials</span>.<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-string">&quot;user1&quot;</span>, <span class="py-src-string">&quot;pass1&quot;</span>))
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">perspective</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got perspective ref:&quot;</span>, <span class="py-src-variable">perspective</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to foo(12)&quot;</span>
+ <span class="py-src-variable">perspective</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-number">12</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb5client.py"><span class="filename">listings/pb/pb5client.py</span></a></div></div>
+
+<p>Ok, so that wasn't really very exciting. It doesn't accomplish much more
+than the first PB example, and used a lot more code to do it. Let's try it
+again with two users this time.</p>
+
+<div class="note"><strong>Note: </strong>
+
+<p>When the client runs <code>login</code> to request the Perspective,
+they can provide it with an optional <code>client</code> argument (which
+must be a <code>pb.Referenceable</code> object). If they do, then a
+reference to that object will be handed to the realm's
+ <code>requestAvatar</code> in the <code>mind</code> argument.</p>
+
+<p>The server-side Perspective can use it to invoke remote methods on
+something in the client, so that the client doesn't always have to drive the
+interaction. In a chat server, the client object would be the one to which
+<q>display text</q> messages were sent. In a board game server, this would
+provide a way to tell the clients that someone has made a move, so they can
+update their game boards.</p>
+
+</div>
+
+<h3>Two Clients<a name="auto9"/></h3>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">checkers</span>, <span class="py-src-variable">portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyPerspective</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_foo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I am&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>, <span class="py-src-string">&quot;perspective_foo(&quot;</span>,<span class="py-src-variable">arg</span>,<span class="py-src-string">&quot;) called on&quot;</span>, <span class="py-src-variable">self</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">MyPerspective</span>(<span class="py-src-variable">avatarId</span>), <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+
+<span class="py-src-variable">p</span> = <span class="py-src-variable">portal</span>.<span class="py-src-variable">Portal</span>(<span class="py-src-variable">MyRealm</span>())
+<span class="py-src-variable">c</span> = <span class="py-src-variable">checkers</span>.<span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>(<span class="py-src-variable">user1</span>=<span class="py-src-string">&quot;pass1&quot;</span>,
+ <span class="py-src-variable">user2</span>=<span class="py-src-string">&quot;pass2&quot;</span>)
+<span class="py-src-variable">p</span>.<span class="py-src-variable">registerChecker</span>(<span class="py-src-variable">c</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">p</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb6server.py"><span class="filename">listings/pb/pb6server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">credentials</span>.<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-string">&quot;user1&quot;</span>, <span class="py-src-string">&quot;pass1&quot;</span>))
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">perspective</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got perspective1 ref:&quot;</span>, <span class="py-src-variable">perspective</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to foo(13)&quot;</span>
+ <span class="py-src-variable">perspective</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-number">13</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb6client1.py"><span class="filename">listings/pb/pb6client1.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">credentials</span>.<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-string">&quot;user2&quot;</span>, <span class="py-src-string">&quot;pass2&quot;</span>))
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">perspective</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got perspective2 ref:&quot;</span>, <span class="py-src-variable">perspective</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to foo(14)&quot;</span>
+ <span class="py-src-variable">perspective</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-number">14</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb6client2.py"><span class="filename">listings/pb/pb6client2.py</span></a></div></div>
+
+<p>While pb6server.py is running, try starting pb6client1, then pb6client2.
+Compare the argument passed by the <code>.callRemote()</code> in each
+client. You can see how each client gets connected to a different
+Perspective.</p>
+
+
+<h3>How that example worked<a name="auto10"/></h3><a name="smallexample" shape="rect"/>
+
+<p>Let's walk through the previous example and see what was going on.</p>
+
+<p>First, we created a subclass called <code>MyPerspective</code> which is
+our server-side Avatar. It implements a <code>perspective_foo</code> method
+that is exposed to the remote client.</p>
+
+<p>Second, we created a realm (an object which implements
+ <code>IRealm</code>, and therefore implements <code>requestAvatar</code>).
+This realm manufactures <code>MyPerspective</code> objects. It makes as many
+as we want, and names each one with the avatarID (a username) that comes out
+of the checkers. This MyRealm object returns two other objects as well,
+which we will describe later.</p>
+
+<p>Third, we created a portal to hold this realm. The portal's job is to
+dispatch incoming clients to the credential checkers, and then to request
+Avatars for any which survive the authentication process.</p>
+
+<p>Fourth, we made a simple checker (an object which implements
+ <code>IChecker</code>) to hold valid user/password pairs. The checker
+gets registered with the portal, so it knows who to ask when new
+clients connect. We use a checker named
+ <code>InMemoryUsernamePasswordDatabaseDontUse</code>, which suggests
+that 1: all the username/password pairs are kept in memory instead of
+being saved to a database or something, and 2: you shouldn't use
+it. The admonition against using it is because there are better
+schemes: keeping everything in memory will not work when you have
+thousands or millions of users to keep track of, the passwords will be
+stored in the .tap file when the application shuts down (possibly a
+security risk), and finally it is a nuisance to add or remove users
+after the checker is constructed.</p>
+
+<p>Fifth, we create a <code>pb.PBServerFactory</code> to listen on a TCP
+port. This factory knows how to connect the remote client to the Portal, so
+incoming connections will be handed to the authentication process. Other
+protocols (non-PB) would do something similar: the factory that creates
+Protocol objects will give those objects access to the Portal so
+authentication can take place.</p>
+
+<p>On the client side, a <code>pb.PBClientFactory</code> is created (as <a href="pb-usage.html" shape="rect">before</a>) and attached to a TCP connection. When the
+connection completes, the factory will be asked to produce a Protocol, and
+it will create a PB object. Unlike the previous chapter, where we used
+ <code>.getRootObject</code>, here we use <code>factory.login</code> to
+ initiate the cred authentication process. We provide a
+ <code>credentials</code> object, which is the client-side agent for doing
+our half of the authentication process. This process may involve several
+messages: challenges, responses, encrypted passwords, secure hashes, etc. We
+give our credentials object everything it will need to respond correctly (in
+this case, a username and password, but you could write a credential that
+used public-key encryption or even fancier techniques).</p>
+
+<p><code>login</code> returns a Deferred which, when it fires, will return a
+ <code>pb.RemoteReference</code> to the remote avatar. We can then do
+ <code>callRemote</code> to invoke a <code>perspective_foo</code> method on
+that Avatar.</p>
+
+
+<h3>Anonymous Clients<a name="auto11"/></h3>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+Implement the realm for and run on port 8800 a PB service which allows both
+anonymous and username/password based access.
+
+Successful username/password-based login requests given an instance of
+MyPerspective with a name which matches the username with which they
+authenticated. Success anonymous login requests are given an instance of
+MyPerspective with the name &quot;Anonymous&quot;.
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">startLogging</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">checkers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ANONYMOUS</span>, <span class="py-src-variable">AllowAnonymousAccess</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">checkers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IRealm</span>, <span class="py-src-variable">Portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span>.<span class="py-src-variable">pb</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Avatar</span>, <span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">PBServerFactory</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyPerspective</span>(<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Trivial avatar exposing a single remote method for demonstrative
+ purposes. All successful login attempts in this example will result in
+ an avatar which is an instance of this class.
+
+ @type name: C{str}
+ @ivar name: The username which was used during login or C{&quot;Anonymous&quot;}
+ if the login was anonymous (a real service might want to avoid the
+ collision this introduces between anonoymous users and authenticated
+ users named &quot;Anonymous&quot;).
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_foo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Print a simple message which gives the argument this method was
+ called with and this avatar's name.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I am %s. perspective_foo(%s) called on %s.&quot;</span> % (
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">arg</span>, <span class="py-src-variable">self</span>)
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyRealm</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Trivial realm which supports anonymous and named users by creating
+ avatars which are instances of MyPerspective for either.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>(<span class="py-src-string">&quot;MyRealm only handles IPerspective&quot;</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarId</span> <span class="py-src-keyword">is</span> <span class="py-src-variable">ANONYMOUS</span>:
+ <span class="py-src-variable">avatarId</span> = <span class="py-src-string">&quot;Anonymous&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">MyPerspective</span>(<span class="py-src-variable">avatarId</span>), <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">None</span>
+
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Create a PB server using MyRealm and run it on port 8800.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">startLogging</span>(<span class="py-src-variable">stdout</span>)
+
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">Portal</span>(<span class="py-src-variable">MyRealm</span>())
+
+ <span class="py-src-comment"># Here the username/password checker is registered.</span>
+ <span class="py-src-variable">c1</span> = <span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>(<span class="py-src-variable">user1</span>=<span class="py-src-string">&quot;pass1&quot;</span>, <span class="py-src-variable">user2</span>=<span class="py-src-string">&quot;pass2&quot;</span>)
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">registerChecker</span>(<span class="py-src-variable">c1</span>)
+
+ <span class="py-src-comment"># Here the anonymous checker is registered.</span>
+ <span class="py-src-variable">c2</span> = <span class="py-src-variable">AllowAnonymousAccess</span>()
+ <span class="py-src-variable">p</span>.<span class="py-src-variable">registerChecker</span>(<span class="py-src-variable">c2</span>)
+
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">p</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pbAnonServer.py"><span class="filename">listings/pb/pbAnonServer.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+Client which will talk to the server run by pbAnonServer.py, logging in
+either anonymously or with username/password credentials.
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">sys</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">stdout</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>, <span class="py-src-variable">startLogging</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">credentials</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Anonymous</span>, <span class="py-src-variable">UsernamePassword</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">gatherResults</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span>.<span class="py-src-variable">pb</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">PBClientFactory</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">error</span>(<span class="py-src-parameter">why</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Catch-all errback which simply logs the failure. This isn't expected to
+ be invoked in the normal case for this example.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">err</span>(<span class="py-src-variable">why</span>, <span class="py-src-variable">msg</span>)
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">perspective</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Login callback which invokes the remote &quot;foo&quot; method on the perspective
+ which the server returned.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got perspective1 ref:&quot;</span>, <span class="py-src-variable">perspective</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to foo(13)&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">perspective</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-number">13</span>)
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">finished</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Callback invoked when both logins and method calls have finished to shut
+ down the reactor so the example exits.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Connect to a PB server running on port 8800 on localhost and log in to
+ it, both anonymously and using a username/password it will recognize.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">startLogging</span>(<span class="py-src-variable">stdout</span>)
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+
+ <span class="py-src-variable">anonymousLogin</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">Anonymous</span>())
+ <span class="py-src-variable">anonymousLogin</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">anonymousLogin</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">error</span>, <span class="py-src-string">&quot;Anonymous login failed&quot;</span>)
+
+ <span class="py-src-variable">usernameLogin</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-string">&quot;user1&quot;</span>, <span class="py-src-string">&quot;pass1&quot;</span>))
+ <span class="py-src-variable">usernameLogin</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">usernameLogin</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">error</span>, <span class="py-src-string">&quot;Username/password login failed&quot;</span>)
+
+ <span class="py-src-variable">bothDeferreds</span> = <span class="py-src-variable">gatherResults</span>([<span class="py-src-variable">anonymousLogin</span>, <span class="py-src-variable">usernameLogin</span>])
+ <span class="py-src-variable">bothDeferreds</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">finished</span>)
+
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pbAnonClient.py"><span class="filename">listings/pb/pbAnonClient.py</span></a></div></div>
+
+<p>pbAnonServer.py implements a server based on pb6server.py, extending it to
+permit anonymous logins in addition to authenticated logins. An
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.AllowAnonymousAccess.html" title="twisted.cred.checkers.AllowAnonymousAccess">AllowAnonymousAccess</a></code>
+checker and an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.InMemoryUsernamePasswordDatabaseDontUse.html" title="twisted.cred.checkers.InMemoryUsernamePasswordDatabaseDontUse">InMemoryUsernamePasswordDatabaseDontUse</a></code>
+checker are registered and the
+client's choice of credentials object determines which is used to authenticate
+the login. In either case, the realm will be called on to create an avatar for
+the login. <code>AllowAnonymousAccess</code> always produces an <code>avatarId
+ </code> of <code>twisted.cred.checkers.ANONYMOUS</code>.</p>
+
+<p>On the client side, the only change is the use of an instance of
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.credentials.Anonymous.html" title="twisted.cred.credentials.Anonymous">Anonymous</a></code> when calling
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBClientFactory.login.html" title="twisted.spread.pb.PBClientFactory.login">PBClientFactory.login</a></code>.</p>
+
+
+<h2>Using Avatars<a name="auto12"/></h2>
+
+
+<h3>Avatar Interfaces<a name="auto13"/></h3>
+
+<p>The first element of the 3-tuple returned by <code>requestAvatar</code>
+indicates which Interface this Avatar implements. For PB avatars, it will
+always be <code>pb.IPerspective</code>, because that's the only interface
+these avatars implement.</p>
+
+<p>This element is present because <code>requestAvatar</code> is actually
+presented with a list of possible Interfaces. The question being posed to
+the Realm is: <q>do you have an avatar for (avatarID) that can implement one
+of the following set of Interfaces?</q>. Some portals and checkers might
+give a list of Interfaces and the Realm could pick; the PB code only knows
+how to do one, so we cannot take advantage of this feature.</p>
+
+<h3>Logging Out<a name="auto14"/></h3>
+
+<p>The third element of the 3-tuple is a zero-argument callable, which will
+be invoked by the protocol when the connection has been lost. We can use
+this to notify the Avatar when the client has lost its connection. This will
+be described in more detail below.</p>
+
+<h3>Making Avatars<a name="auto15"/></h3>
+
+<p>In the example above, we create Avatars upon request, during
+ <code>requestAvatar</code>. Depending upon the service, these Avatars might
+already exist before the connection is received, and might outlive the
+connection. The Avatars might also accept multiple connections.</p>
+
+<p>Another possibility is that the Avatars might exist ahead of time, but in
+a different form (frozen in a pickle and/or saved in a database). In this
+case, <code>requestAvatar</code> may need to perform a database lookup and
+then do something with the result before it can provide an avatar. In this
+case, it would probably return a Deferred so it could provide the real
+Avatar later, once the lookup had completed.</p>
+
+<p>Here are some possible implementations of
+ <code>MyRealm.requestAvatar</code>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+</p><span class="py-src-comment"># pre-existing, static avatars</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-variable">avatar</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarID</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">avatar</span>, <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+
+ <span class="py-src-comment"># database lookup and unpickling</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">database</span>.<span class="py-src-variable">fetchAvatar</span>(<span class="py-src-variable">avatarID</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">doUnpickle</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">d</span>, <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">doUnpickle</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">pickled</span>):
+ <span class="py-src-variable">avatar</span> = <span class="py-src-variable">pickle</span>.<span class="py-src-variable">loads</span>(<span class="py-src-variable">pickled</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">avatar</span>
+
+ <span class="py-src-comment"># everybody shares the same Avatar</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">theOneAvatar</span>, <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+
+ <span class="py-src-comment"># anonymous users share one Avatar, named users each get their own</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarID</span> == <span class="py-src-variable">checkers</span>.<span class="py-src-variable">ANONYMOUS</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">anonAvatar</span>, <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarID</span>], <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+
+ <span class="py-src-comment"># anonymous users get independent (but temporary) Avatars</span>
+ <span class="py-src-comment"># named users get their own persistent one</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">avatarID</span> == <span class="py-src-variable">checkers</span>.<span class="py-src-variable">ANONYMOUS</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">MyAvatar</span>(), <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarID</span>], <span class="py-src-keyword">lambda</span>:<span class="py-src-variable">None</span>
+</pre>
+
+<p>The last example, note that the new <code>MyAvatar</code> instance is not
+saved anywhere: it will vanish when the connection is dropped. By contrast,
+the avatars that live in the <code>self.avatars</code> dictionary will
+probably get persisted into the .tap file along with the Realm, the Portal,
+and anything else that is referenced by the top-level Application object.
+This is an easy way to manage saved user profiles.</p>
+
+
+<h3>Connecting and Disconnecting<a name="auto16"/></h3>
+
+<p>It may be useful for your Avatars to be told when remote clients gain
+(and lose) access to them. For example, and Avatar might be updated by
+something in the server, and if there are clients attached, it should update
+them (through the <q>mind</q> argument which lets the Avatar do callRemote
+on the client).</p>
+
+<p>One common idiom which accomplishes this is to have the Realm tell the
+avatar that a remote client has just attached. The Realm can also ask the
+protocol to let it know when the connection goes away, so it can then inform
+the Avatar that the client has detached. The third member of the
+ <code>requestAvatar</code> return tuple is a callable which will be invoked
+when the connection is lost.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">MyPerspective</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clients</span> = []
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">attached</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">mind</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clients</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">mind</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;attached to&quot;</span>, <span class="py-src-variable">mind</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">detached</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">mind</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clients</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">mind</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;detached from&quot;</span>, <span class="py-src-variable">mind</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">update</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">c</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">clients</span>:
+ <span class="py-src-variable">c</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;update&quot;</span>, <span class="py-src-variable">message</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyRealm</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-variable">avatar</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">avatars</span>[<span class="py-src-variable">avatarID</span>]
+ <span class="py-src-variable">avatar</span>.<span class="py-src-variable">attached</span>(<span class="py-src-variable">mind</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">avatar</span>, <span class="py-src-keyword">lambda</span> <span class="py-src-variable">a</span>=<span class="py-src-variable">avatar</span>:<span class="py-src-variable">a</span>.<span class="py-src-variable">detached</span>(<span class="py-src-variable">mind</span>)
+</pre>
+
+
+<h3>Viewable<a name="auto17"/></h3> <a name="viewable" shape="rect"/>
+
+<p>Once you have <code>IPerspective</code> objects (i.e. the Avatar) to
+represent users, the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Viewable.html" title="twisted.spread.pb.Viewable">Viewable</a></code> class can come into play. This
+class behaves a lot like <code>Referenceable</code>: it turns into a
+ <code>RemoteReference</code> when sent over the wire, and certain methods
+can be invoked by the holder of that reference. However, the methods that
+can be called have names that start with <code>view_</code> instead of <code>remote_</code>, and those methods are always called with an extra
+<code>perspective</code> argument that points to the Avatar through which
+the reference was sent:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Foo</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Viewable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">view_doFoo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">perspective</span>, <span class="py-src-parameter">arg1</span>, <span class="py-src-parameter">arg2</span>):
+ <span class="py-src-keyword">pass</span>
+</pre>
+
+<p>This is useful if you want to let multiple clients share a reference to
+the same object. The <code>view_</code> methods can use the
+ <q>perspective</q> argument to figure out which client is calling them. This
+gives them a way to do additional permission checks, do per-user accounting,
+etc.</p>
+
+<p>This is the shortcut which makes per-user-per-group capability objects
+much easier to use. Instead of creating such per-(user,group) objects, you
+just have per-group objects which inherit from <code>pb.Viewable</code>, and
+give the user references to them. The local <code>pb.Avatar</code> object
+will automatically show up as the <q>perspective</q> argument in the
+ <code>view_*</code> method calls, give you a chance to involve the Avatar in
+the process.</p>
+
+
+<h3>Chat Server with Avatars<a name="auto18"/></h3>
+
+<p>Combining all the above techniques, here is an example chat server which
+uses a fixed set of identities (say, for the three members of your bridge
+club, who hang out in <q>#NeedAFourth</q> hoping that someone will discover
+your server, guess somebody's password, break in, join the group, and also
+be available for a game next saturday afternoon).</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">portal</span>, <span class="py-src-variable">checkers</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatServer</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span> = {} <span class="py-src-comment"># indexed by name</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">allowMattress</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">groupname</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>] = <span class="py-src-variable">Group</span>(<span class="py-src-variable">groupname</span>, <span class="py-src-variable">allowMattress</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>].<span class="py-src-variable">addUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">groups</span>[<span class="py-src-variable">groupname</span>]
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatRealm</span>:
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">portal</span>.<span class="py-src-variable">IRealm</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarID</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">assert</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>
+ <span class="py-src-variable">avatar</span> = <span class="py-src-variable">User</span>(<span class="py-src-variable">avatarID</span>)
+ <span class="py-src-variable">avatar</span>.<span class="py-src-variable">server</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span>
+ <span class="py-src-variable">avatar</span>.<span class="py-src-variable">attached</span>(<span class="py-src-variable">mind</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">IPerspective</span>, <span class="py-src-variable">avatar</span>, <span class="py-src-keyword">lambda</span> <span class="py-src-variable">a</span>=<span class="py-src-variable">avatar</span>:<span class="py-src-variable">a</span>.<span class="py-src-variable">detached</span>(<span class="py-src-variable">mind</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">User</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Avatar</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">attached</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">mind</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">mind</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">detached</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">mind</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">perspective_joinGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">allowMattress</span>=<span class="py-src-parameter">True</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">server</span>.<span class="py-src-variable">joinGroup</span>(<span class="py-src-variable">groupname</span>, <span class="py-src-variable">self</span>, <span class="py-src-variable">allowMattress</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;print&quot;</span>, <span class="py-src-variable">message</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Group</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Viewable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">groupname</span>, <span class="py-src-parameter">allowMattress</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">groupname</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> = <span class="py-src-variable">allowMattress</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = []
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">addUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">view_send</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">from_user</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">allowMattress</span> <span class="py-src-keyword">and</span> <span class="py-src-string">&quot;mattress&quot;</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">message</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">ValueError</span>, <span class="py-src-string">&quot;Don't say that word&quot;</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>:
+ <span class="py-src-variable">user</span>.<span class="py-src-variable">send</span>(<span class="py-src-string">&quot;&lt;%s&gt; says: %s&quot;</span> % (<span class="py-src-variable">from_user</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">message</span>))
+
+<span class="py-src-variable">realm</span> = <span class="py-src-variable">ChatRealm</span>()
+<span class="py-src-variable">realm</span>.<span class="py-src-variable">server</span> = <span class="py-src-variable">ChatServer</span>()
+<span class="py-src-variable">checker</span> = <span class="py-src-variable">checkers</span>.<span class="py-src-variable">InMemoryUsernamePasswordDatabaseDontUse</span>()
+<span class="py-src-variable">checker</span>.<span class="py-src-variable">addUser</span>(<span class="py-src-string">&quot;alice&quot;</span>, <span class="py-src-string">&quot;1234&quot;</span>)
+<span class="py-src-variable">checker</span>.<span class="py-src-variable">addUser</span>(<span class="py-src-string">&quot;bob&quot;</span>, <span class="py-src-string">&quot;secret&quot;</span>)
+<span class="py-src-variable">checker</span>.<span class="py-src-variable">addUser</span>(<span class="py-src-string">&quot;carol&quot;</span>, <span class="py-src-string">&quot;fido&quot;</span>)
+<span class="py-src-variable">p</span> = <span class="py-src-variable">portal</span>.<span class="py-src-variable">Portal</span>(<span class="py-src-variable">realm</span>, [<span class="py-src-variable">checker</span>])
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">p</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/chatserver.py"><span class="filename">listings/pb/chatserver.py</span></a></div></div>
+
+<p>Notice that the client uses <code>perspective_joinGroup</code> to both
+join a group and retrieve a <code>RemoteReference</code> to the
+ <code>Group</code> object. However, the reference they get is actually to a
+special intermediate object called a <code>pb.ViewPoint</code>. When they do
+ <code>group.callRemote(&quot;send&quot;, &quot;message&quot;)</code>, their avatar is inserted
+into the argument list that <code>Group.view_send</code> actually sees. This
+lets the group get their username out of the Avatar without giving the
+client an opportunity to spoof someone else.</p>
+
+<p>The client side code that joins a group and sends a message would look
+like this:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Client</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_print</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">message</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connect</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">login</span>(<span class="py-src-variable">credentials</span>.<span class="py-src-variable">UsernamePassword</span>(<span class="py-src-string">&quot;alice&quot;</span>, <span class="py-src-string">&quot;1234&quot;</span>),
+ <span class="py-src-variable">client</span>=<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">connected</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connected</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">perspective</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;connected, joining group #NeedAFourth&quot;</span>
+ <span class="py-src-comment"># this perspective is a reference to our User object. Save a reference</span>
+ <span class="py-src-comment"># to it here, otherwise it will get garbage collected after this call,</span>
+ <span class="py-src-comment"># and the server will think we logged out.</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">perspective</span> = <span class="py-src-variable">perspective</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">perspective</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;joinGroup&quot;</span>, <span class="py-src-string">&quot;#NeedAFourth&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">gotGroup</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">gotGroup</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">group</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;joined group, now sending a message to all members&quot;</span>
+ <span class="py-src-comment"># 'group' is a reference to the Group object (through a ViewPoint)</span>
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">group</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;send&quot;</span>, <span class="py-src-string">&quot;You can call me Al.&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">shutdown</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">shutdown</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">result</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+
+<span class="py-src-variable">Client</span>().<span class="py-src-variable">connect</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/chatclient.py"><span class="filename">listings/pb/chatclient.py</span></a></div></div>
+
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">Apparently Alice is one of those weirdos who has nothing
+better to do than to try and impersonate Bob. She will lie to her chat
+client, send incorrect objects to remote methods, even rewrite her local
+client code entirely to accomplish this juvenile prank. Given this
+adversarial relationship, one must wonder why she and Bob seem to spend so
+much time together: their adventures are clearly documented by the
+cryptographic literature.</span></a></li><li><a name="footnote-2"><span class="footnote">The
+obvious name is clearly
+<code>ServerSidePerUserObjectWhichNobodyElseHasAccessTo</code>, but because
+Python makes everything else so easy to read, it only seems fair to make
+your audience work for <em>something</em>.</span></a></li><li><a name="footnote-3"><span class="footnote">We could just go ahead and rename Perspective Broker to be
+Avatar Broker, but 1) that would cause massive compatibility problems, and 2)
+<q>AB</q> doesn't fit into the whole sandwich-themed naming scheme nearly as
+well as <q>PB</q> does. If we changed it to AB, we'd probably have to change
+Banana to be CD (CoderDecoder), and Jelly to be EF (EncapsulatorFragmentor).
+twisted.spread would then have to be renamed twisted.alphabetsoup, and then
+the whole food-pun thing would start all over again.</span></a></li><li><a name="footnote-4"><span class="footnote">The avatar-ish class is named
+<code>pb.Avatar</code> because <code>pb.Perspective</code> was already
+taken, by the (now obsolete) oldcred perspective-ish class. It is a pity,
+but it simply wasn't possible both replace <code>pb.Perspective</code>
+in-place <em>and</em> maintain a reasonable level of
+backwards-compatibility.</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-intro.html b/doc/core/howto/pb-intro.html
new file mode 100644
index 0000000..dead735
--- /dev/null
+++ b/doc/core/howto/pb-intro.html
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Introduction to Perspective Broker</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Introduction to Perspective Broker</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Object Roadmap</a></li><ul><li><a href="#auto2">Subclassing and Implementing</a></li></ul><li><a href="#auto3">Things you can Call Remotely</a></li><li><a href="#auto4">Things you can Copy Remotely</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p>Suppose you find yourself in control of both ends of the wire: you
+have two programs that need to talk to each other, and you get to use any
+protocol you want. If you can think of your problem in terms of objects that
+need to make method calls on each other, then chances are good that you can
+use Twisted's Perspective Broker protocol rather than trying to shoehorn
+your needs into something like HTTP, or implementing yet another RPC
+mechanism<a href="#footnote-1" title="Most of Twisted is like this. Hell, most of Unix is like this: if you think it would be useful, someone else has probably thought that way in the past, and acted on it, and you can take advantage of the tool they created to solve the same problem you're facing now."><super>1</super></a>.</p>
+
+<p>The Perspective Broker system (abbreviated <q>PB</q>, spawning numerous
+sandwich-related puns) is based upon a few central concepts:</p>
+
+<ul>
+
+ <li><em>serialization</em>: taking fairly arbitrary objects and types,
+ turning them into a chunk of bytes, sending them over a wire, then
+ reconstituting them on the other end. By keeping careful track of object
+ ids, the serialized objects can contain references to other objects and
+ the remote copy will still be useful. </li>
+
+ <li><em>remote method calls</em>: doing something to a local object and
+ causing a method to get run on a distant one. The local object is called a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code>, and you
+ <q>do something</q> by running its <code>.callRemote</code> method.
+ </li>
+
+</ul>
+
+<p>This document will contain several examples that will (hopefully) appear
+redundant and verbose once you've figured out what's going on. To begin
+with, much of the code will just be labelled <q>magic</q>: don't worry about how
+these parts work yet. It will be explained more fully later.</p>
+
+<h2>Object Roadmap<a name="auto1"/></h2>
+
+<p>To start with, here are the major classes, interfaces, and
+functions involved in PB, with links to the file where they are
+defined (all of which are under twisted/, of course). Don't worry
+about understanding what they all do yet: it's easier to figure them
+out through their interaction than explaining them one at a time.</p>
+
+<ul>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">Factory</a></code>
+ : <code>internet/protocol.py</code></li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBServerFactory.html" title="twisted.spread.pb.PBServerFactory">PBServerFactory</a></code>
+ : <code>spread/pb.py</code></li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Broker.html" title="twisted.spread.pb.Broker">Broker</a></code>
+ : <code>spread/pb.py</code></li>
+
+</ul>
+
+<p>Other classes that are involved at some point:</p>
+
+<ul>
+
+ <li> <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code>
+ : <code>spread/pb.py</code> </li>
+
+ <li> <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>
+ : <code>spread/pb.py</code>, actually defined as
+ <code>twisted.spread.flavors.Root</code>
+ in <code>spread/flavors.py</code> </li>
+
+ <li> <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>
+ : <code>spread/pb.py</code>, actually defined as
+ <code>twisted.spread.flavors.Referenceable</code>
+ in <code>spread/flavors.py</code> </li>
+
+</ul>
+
+<p>Classes and interfaces that get involved when you start to care
+about authorization and security:</p>
+
+<ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.html" title="twisted.cred.portal.Portal">Portal</a></code>
+ : <code>cred/portal.py</code></li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.html" title="twisted.cred.portal.IRealm">IRealm</a></code>
+ : <code>cred/portal.py</code></li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.IPerspective.html" title="twisted.spread.pb.IPerspective">IPerspective</a></code>
+ : <code>spread/pb.py</code>, which you will usually be interacting
+ with via <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Avatar.html" title="twisted.spread.pb.Avatar">pb.Avatar</a></code> (a basic implementor of the interface).</li>
+</ul>
+
+<h3>Subclassing and Implementing<a name="auto2"/></h3>
+
+<p>Technically you can subclass anything you want, but technically you
+could also write a whole new framework, which would just waste a lot
+of time. Knowing which classes are useful to subclass or which
+interfaces to implement is one of the bits of knowledge that's crucial
+to using PB (and all of Twisted) successfully. Here are some hints to
+get started:</p>
+
+<ul>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>: you'll
+ subclass these to make remotely-referenceable objects (i.e., objects
+ which you can call methods on remotely) using PB. You don't need to
+ change any of the existing behavior, just inherit all of it and add
+ the remotely-accessible methods that you want to export.</li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Avatar.html" title="twisted.spread.pb.Avatar">pb.Avatar</a></code>: You'll
+ be subclassing this when you get into PB programming with
+ authorization. This is an implementor of IPerspective.</li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.ICredentialsChecker.html" title="twisted.cred.checkers.ICredentialsChecker">ICredentialsChecker</a></code>: Implement this if
+ you want to authenticate your users against some sort of data store:
+ i.e., an LDAP database, an RDBMS, etc. There are already a few
+ implementations of this for various back-ends in
+ twisted.cred.checkers.</li>
+
+</ul>
+
+
+
+<h2>Things you can Call Remotely<a name="auto3"/></h2>
+
+<p>At this writing, there are three <q>flavors</q> of objects that can
+be accessed remotely through <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code> objects. Each of these
+flavors has a rule for how the <code class="python">callRemote</code>
+message is transformed into a local method call on the server. In
+order to use one of these <q>flavors</q>, subclass them and name your
+published methods with the appropriate prefix.
+
+<ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.IPerspective.html" title="twisted.spread.pb.IPerspective">twisted.spread.pb.IPerspective</a></code> implementors
+
+ <p>This is the first interface we deal with. It is a <q>perspective</q>
+ onto your PB application. Perspectives are slightly special because
+ they are usually the first object that a given user can access in
+ your application (after they log on). A user should only receive a
+ reference to their <em>own</em> perspective. PB works hard to
+ verify, as best it can, that any method that can be called on a
+ perspective directly is being called on behalf of the user who is
+ represented by that perspective. (Services with unusual
+ requirements for <q>on behalf of</q>, such as simulations with the
+ ability to posess another player's avatar, are accomplished by
+ providing indirected access to another user's perspective.)
+
+ </p>
+
+ <p>Perspectives are not usually serialized as remote references, so
+ do not return an IPerspective-implementor directly. </p>
+
+ <p>The way most people will want to implement IPerspective is by
+ subclassing pb.Avatar. Remotely accessible methods on pb.Avatar
+ instances are named with the <code class="python">perspective_</code> prefix. </p>
+
+ </li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">twisted.spread.pb.Referenceable</a></code>
+
+ <p>Referenceable objects are the simplest kind of PB object. You can call
+ methods on them and return them from methods to provide access to other
+ objects' methods. </p>
+
+ <p>However, when a method is called on a Referenceable, it's not possible to
+ tell who called it.</p>
+
+ <p>Remotely accessible methods on Referenceables are named with the
+ <code class="python">remote_</code> prefix.</p>
+
+ </li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Viewable.html" title="twisted.spread.pb.Viewable">twisted.spread.pb.Viewable</a></code>
+
+ <p>Viewable objects are remotely referenceable objects which have the
+ additional requirement that it must be possible to tell who is calling them.
+ The argument list to a Viewable's remote methods is modified in order to
+ include the Perspective representing the calling user.</p>
+
+ <p>Remotely accessible methods on Viewables are named with the
+ <code class="python">view_</code> prefix.</p>
+
+ </li>
+
+</ul>
+
+</p>
+
+<h2>Things you can Copy Remotely<a name="auto4"/></h2>
+
+<p>In addition to returning objects that you can call remote methods on, you
+can return structured copies of local objects.</p>
+
+<p>There are 2 basic flavors that allow for copying objects remotely. Again,
+you can use these by subclassing them. In order to specify what state you want
+to have copied when these are serialized, you can either use the Python default
+ <code class="python">__getstate__</code> or specialized method calls for that
+flavor.</p>
+
+<p>
+<ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">twisted.spread.pb.Copyable</a></code>
+
+ <p>This is the simpler kind of object that can be copied. Every time this
+ object is returned from a method or passed as an argument, it is serialized
+ and unserialized.</p>
+
+ <p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">Copyable</a></code>
+ provides a method you can override, <code class="py-prototype">getStateToCopyFor(perspective)</code>, which
+ allows you to decide what an object will look like for the
+ perspective who is requesting it. The <code class="python">perspective</code> argument will be the perspective
+ which is either passing an argument or returning a result an
+ instance of your Copyable class. </p>
+
+ <p>For security reasons, in order to allow a particular Copyable class to
+ actually be copied, you must declare a <code class="python">RemoteCopy</code>
+ handler for
+ that Copyable subclass. The easiest way to do this is to declare both in the
+ same module, like so:
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">flavors</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Foo</span>(<span class="py-src-parameter">flavors</span>.<span class="py-src-parameter">Copyable</span>):
+ <span class="py-src-keyword">pass</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteFoo</span>(<span class="py-src-parameter">flavors</span>.<span class="py-src-parameter">RemoteCopy</span>):
+ <span class="py-src-keyword">pass</span>
+<span class="py-src-variable">flavors</span>.<span class="py-src-variable">setUnjellyableForClass</span>(<span class="py-src-variable">Foo</span>, <span class="py-src-variable">RemoteFoo</span>)
+</pre>
+
+ In this case, each time a Foo is copied between peers, a RemoteFoo will be
+ instantiated and populated with the Foo's state. If you do not do this, PB
+ will complain that there have been security violations, and it may close the
+ connection.
+ </p>
+
+ </li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">twisted.spread.pb.Cacheable</a></code>
+
+ <p>Let me preface this with a warning: Cacheable may be hard to understand.
+ The motivation for it may be unclear if you don't have some experience with
+ real-world applications that use remote method calling of some kind. Once
+ you understand why you need it, what it does will likely seem simple and
+ obvious, but if you get confused by this, forget about it and come back
+ later. It's possible to use PB without understanding Cacheable at all.
+ </p>
+
+ <p>Cacheable is a flavor which is designed to be copied only when necessary,
+ and updated on the fly as changes are made to it. When passed as an argument
+ or a return value, if a Cacheable exists on the side of the connection it is
+ being copied to, it will be referred to by ID and not copied.</p>
+
+ <p>Cacheable is designed to minimize errors involved in replicating an object
+ between multiple servers, especially those related to having stale
+ information. In order to do this, Cacheable automatically registers
+ observers and queries state atomically, together. You can override the
+ method <code class="py-prototype">getStateToCacheAndObserveFor(self,
+ perspective, observer)</code> in order to specify how your observers will be
+ stored and updated.
+ </p>
+
+ <p>Similar to
+ <code class="python">getStateToCopyFor</code>,
+ <code class="python">getStateToCacheAndObserveFor</code> gets passed a
+ perspective. It also gets passed an
+ <code class="python">observer</code>, which is a remote reference to a
+ <q>secret</q> fourth referenceable flavor:
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCache.html" title="twisted.spread.pb.RemoteCache">RemoteCache</a></code>.</p>
+
+ <p>A <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCache.html" title="twisted.spread.pb.RemoteCache">RemoteCache</a></code> is simply
+ the object that represents your
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">Cacheable</a></code> on the other side
+ of the connection. It is registered using the same method as
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteCopy.html" title="twisted.spread.pb.RemoteCopy">RemoteCopy</a></code>, above.
+ RemoteCache is different, however, in that it will be referenced by its peer.
+ It acts as a Referenceable, where all methods prefixed with
+ <code class="python">observe_</code> will be callable remotely. It is
+ recommended that your object maintain a list (note: library support for this
+ is forthcoming!) of observers, and update them using
+ <code class="python">callRemote</code> when the Cacheable changes in a way
+ that should be noticeable to its clients. </p>
+
+ <p>Finally, when all references to a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">Cacheable</a></code> from a given
+ perspective are lost,
+ <code class="py-prototype">stoppedObserving(perspective, observer)</code>
+ will be called on the
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Cacheable.html" title="twisted.spread.pb.Cacheable">Cacheable</a></code>, with the same
+ perspective/observer pair that <code>getStateToCacheAndObserveFor</code> was
+ originally called with. Any cleanup remote calls can be made there, as well
+ as removing the observer object from any lists which it was previously in.
+ Any further calls to this observer object will be invalid.</p>
+ </li>
+</ul>
+</p>
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">Most of Twisted is like this. Hell, most of
+Unix is like this: if <em>you</em> think it would be useful, someone else has
+probably thought that way in the past, and acted on it, and you can take
+advantage of the tool they created to solve the same problem you're facing
+now.</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-limits.html b/doc/core/howto/pb-limits.html
new file mode 100644
index 0000000..b92a38d
--- /dev/null
+++ b/doc/core/howto/pb-limits.html
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: PB Limits</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">PB Limits</h1>
+ <div class="toc"><ol><li><a href="#auto0">Banana Limits</a></li><li><a href="#auto1">Perspective Broker Limits</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>There are a number of limits you might encounter when using Perspective
+Broker. This document is an attempt to prepare you for as many of them as
+possible so you can avoid them or at least recognize them when you do run
+into them.</p>
+
+ <h2>Banana Limits<a name="auto0"/></h2>
+
+ <p>Perspective Broker is implemented in terms of a simpler, less
+functional protocol called Banana. Twisted's implementation of Banana
+imposes a limit on the length of any sequence-like data type. This applies
+directly to lists and strings and indirectly to dictionaries, instances and
+other types. The purpose of this limit is to put an upper bound on the
+amount of memory which will be allocated to handle a message received over
+the network. Without, a malicious peer could easily perform a denial of
+service attack resulting in exhaustion of the receiver's memory. The basic
+limit is 640 * 1024 bytes, defined by <code>twisted.spread.banana.SIZE_LIMIT</code>.
+It's possible to raise this limit by changing this value (but take care to
+change it on both sides of the connection).</p>
+
+ <p>Another limit imposed by Twisted's Banana implementation is a limit on
+the size of long integers. The purpose of this limit is the same as the
+ <code>SIZE_LIMIT</code>. By default, only integers between -2 ** 448 and 2
+** 448 (exclusive) can be transferred. This limit can be changed using
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.banana.setPrefixLimit.html" title="twisted.spread.banana.setPrefixLimit">twisted.spread.banana.setPrefixLimit</a></code>.</p>
+
+ <h2>Perspective Broker Limits<a name="auto1"/></h2>
+
+ <p>Perspective Broker imposes an additional limit on top of these lower
+level limits. The number of local objects for which remote references may
+exist at a single time over a single connection, by default, is limited to
+1024, defined by <code>twisted.spread.pb.MAX_BROKER_REFS</code>. This limit
+also exists to prevent memory exhaustion attacks.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb-usage.html b/doc/core/howto/pb-usage.html
new file mode 100644
index 0000000..fb8cb70
--- /dev/null
+++ b/doc/core/howto/pb-usage.html
@@ -0,0 +1,1156 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using Perspective Broker</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using Perspective Broker</h1>
+ <div class="toc"><ol><li><a href="#auto0">Basic Example</a></li><li><a href="#auto1">Complete Example</a></li><li><a href="#auto2">References can come back to you</a></li><li><a href="#auto3">References to client-side objects</a></li><li><a href="#auto4">Raising Remote Exceptions</a></li><li><a href="#auto5">Try/Except blocks and Failure.trap </a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Basic Example<a name="auto0"/></h2>
+
+<p>The first example to look at is a complete (although somewhat trivial)
+application. It uses <code>PBServerFactory()</code> on the server side, and
+ <code>PBClientFactory()</code> on the client side.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echoer</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">st</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'echoing:'</span>, <span class="py-src-variable">st</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">st</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8789</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">Echoer</span>()))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="../examples/pbsimple.py"><span class="filename">../examples/pbsimple.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">util</span>
+
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8789</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">d</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">object</span>: <span class="py-src-variable">object</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;echo&quot;</span>, <span class="py-src-string">&quot;hello network&quot;</span>))
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">echo</span>: <span class="py-src-string">'server echoed: '</span>+<span class="py-src-variable">echo</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">reason</span>: <span class="py-src-string">'error: '</span>+<span class="py-src-variable">str</span>(<span class="py-src-variable">reason</span>.<span class="py-src-variable">value</span>))
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">util</span>.<span class="py-src-variable">println</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="../examples/pbsimpleclient.py"><span class="filename">../examples/pbsimpleclient.py</span></a></div></div>
+
+<p>First we look at the server. This defines an Echoer class (derived from
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>), with a method called
+ <code>remote_echo()</code>.
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code> objects (because of
+their inheritance of
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>, described
+later) can define methods with names of the form <code>remote_*</code>; a
+client which obtains a remote reference to that
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code> object will be able to
+invoke those methods.</p>
+
+<p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>-ish object is
+given to a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBServerFactory.html" title="twisted.spread.pb.PBServerFactory">pb.PBServerFactory</a></code><code>()</code>. This is a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">Factory</a></code> object like
+any other: the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">Protocol</a></code> objects it creates for new
+connections know how to speak the PB protocol. The object you give to
+ <code>pb.PBServerFactory()</code> becomes the <q>root object</q>, which
+simply makes it available for the client to retrieve. The client may only
+request references to the objects you want to provide it: this helps you
+implement your security model. Because it is so common to export just a
+single object (and because a <code>remote_*</code> method on that one can
+return a reference to any other object you might want to give out), the
+simplest example is one where the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBServerFactory.html" title="twisted.spread.pb.PBServerFactory">PBServerFactory</a></code> is given the root object, and
+the client retrieves it.</p>
+
+<p>The client side uses
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBClientFactory.html" title="twisted.spread.pb.PBClientFactory">pb.PBClientFactory</a></code> to make a
+connection to a given port. This is a two-step process involving opening
+a TCP connection to a given host and port and requesting the root object
+using <code>.getRootObject()</code>.</p>
+
+<p>Because <code>.getRootObject()</code> has to wait until a network
+connection has been made and exchange some data, it may take a while,
+so it returns a Deferred, to which the gotObject() callback is
+attached. (See the documentation on <a href="defer.html" shape="rect">Deferring
+Execution</a> for a complete explanation of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>s). If and when the
+connection succeeds and a reference to the remote root object is
+obtained, this callback is run. The first argument passed to the
+callback is a remote reference to the distant root object. (you can
+give other arguments to the callback too, see the other parameters for
+ <code>.addCallback()</code> and <code>.addCallbacks()</code>).</p>
+
+<p>The callback does:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">object</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;echo&quot;</span>, <span class="py-src-string">&quot;hello network&quot;</span>)
+</pre>
+
+<p>which causes the server's <code>.remote_echo()</code> method to be invoked.
+(running <code>.callRemote(&quot;boom&quot;)</code> would cause
+ <code>.remote_boom()</code> to be run, etc). Again because of the delay
+involved, <code>callRemote()</code> returns a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>. Assuming the
+remote method was run without causing an exception (including an attempt to
+invoke an unknown method), the callback attached to that
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> will be
+invoked with any objects that were returned by the remote method call.</p>
+
+<p>In this example, the server's <code>Echoer</code> object has a method
+invoked, <em>exactly</em> as if some code on the server side had done:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">echoer_object</span>.<span class="py-src-variable">remote_echo</span>(<span class="py-src-string">&quot;hello network&quot;</span>)
+</pre>
+
+<p>and from the definition of <code>remote_echo()</code> we see that this just
+returns the same string it was given: <q>hello network</q>.</p>
+
+<p>From the client's point of view, the remote call gets another <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> object instead of
+that string. <code>callRemote()</code> <em>always</em> returns a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>. This is why PB is
+described as a system for <q>translucent</q> remote method calls instead of
+<q>transparent</q> ones: you cannot pretend that the remote object is really
+local. Trying to do so (as some other RPC mechanisms do, coughCORBAcough)
+breaks down when faced with the asynchronous nature of the network. Using
+Deferreds turns out to be a very clean way to deal with the whole thing.</p>
+
+<p>The remote reference object (the one given to
+ <code>getRootObject()</code>'s success callback) is an instance the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code> class. This means
+you can use it to invoke methods on the remote object that it refers to. Only
+instances of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code> are eligible for
+ <code>.callRemote()</code>. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code> object is the one that lives
+on the remote side (the client, in this case), not the local side (where the
+actual object is defined).</p>
+
+<p>In our example, the local object is that <code>Echoer()</code> instance,
+which inherits from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>,
+which inherits from
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>. It is that
+ <code>Referenceable</code> class that makes the object eligible to be available
+for remote method calls<a href="#footnote-1" title="There are a few other classes that can bestow this ability, but pb.Referenceable is the easiest to understand; see 'flavors' below for details on the others."><super>1</super></a>. If you have
+an object that is Referenceable, then any client that manages to get a
+reference to it can invoke any <code>remote_*</code> methods they please.</p>
+
+<div class="note"><strong>Note: </strong>
+<p>The <em>only</em> thing they can do is invoke those
+methods. In particular, they cannot access attributes. From a security point
+of view, you control what they can do by limiting what the
+<code>remote_*</code> methods can do.</p>
+
+<p>Also note: the other classes like
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">Referenceable</a></code> allow access to
+other methods, in particular <code>perspective_*</code> and <code>view_*</code>
+may be accessed. Don't write local-only methods with these names, because then
+remote callers will be able to do more than you intended.</p>
+
+<p>Also also note: the other classes like
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code> <em>do</em> allow
+access to attributes, but you control which ones they can see.</p>
+</div>
+
+<p>You don't have to be a
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code> to be remotely callable,
+but you do have to be
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>. (Objects that
+inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code>
+but not from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code> can be
+remotely called, but only
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Root.html" title="twisted.spread.pb.Root">pb.Root</a></code>-ish objects can be given
+to the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBServerFactory.html" title="twisted.spread.pb.PBServerFactory">PBServerFactory</a></code>.)</p>
+
+<h2>Complete Example<a name="auto1"/></h2>
+
+<p>Here is an example client and server which uses <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">pb.Referenceable</a></code> as a root object and as the
+result of a remotely exposed method. In each context, methods can be invoked
+on the exposed <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">Referenceable</a></code>
+instance. In this example, the initial root object has a method that returns a
+reference to the second object.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Two</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_three</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Two.three was given&quot;</span>, <span class="py-src-variable">arg</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getTwo</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">two</span> = <span class="py-src-variable">Two</span>()
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;returning a Two called&quot;</span>, <span class="py-src-variable">two</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">two</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">One</span>()))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb1server.py"><span class="filename">listings/pb/pb1server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">got_obj1</span>, <span class="py-src-variable">err_obj1</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">err_obj1</span>(<span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;error getting first object&quot;</span>, <span class="py-src-variable">reason</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj1</span>(<span class="py-src-parameter">obj1</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got first object:&quot;</span>, <span class="py-src-variable">obj1</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to getTwo&quot;</span>
+ <span class="py-src-variable">def2</span> = <span class="py-src-variable">obj1</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;getTwo&quot;</span>)
+ <span class="py-src-variable">def2</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">got_obj2</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj2</span>(<span class="py-src-parameter">obj2</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got second object:&quot;</span>, <span class="py-src-variable">obj2</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;telling it to do three(12)&quot;</span>
+ <span class="py-src-variable">obj2</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;three&quot;</span>, <span class="py-src-number">12</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb1client.py"><span class="filename">listings/pb/pb1client.py</span></a></div></div>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBClientFactory.getRootObject.html" title="twisted.spread.pb.PBClientFactory.getRootObject">pb.PBClientFactory.getRootObject</a></code> will
+handle all the details of waiting for the creation of a connection.
+It returns a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>, which will have its
+callback called when the reactor connects to the remote server and
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBClientFactory.html" title="twisted.spread.pb.PBClientFactory">pb.PBClientFactory</a></code> gets the
+root, and have its <code class="python">errback</code> called when the
+object-connection fails for any reason, whether it was host lookup
+failure, connection refusal, or some server-side error.
+</p>
+
+<p>The root object has a method called <code>remote_getTwo</code>, which
+returns the <code>Two()</code> instance. On the client end, the callback gets
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code> to that
+instance. The client can then invoke two's <code>.remote_three()</code>
+method.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.html" title="twisted.spread.pb.RemoteReference">RemoteReference</a></code>
+objects have one method which is their purpose for being: <code class="python">callRemote</code>. This method allows you to call a
+remote method on the object being referred to by the Reference. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.RemoteReference.callRemote.html" title="twisted.spread.pb.RemoteReference.callRemote">RemoteReference.callRemote</a></code>, like <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBClientFactory.getRootObject.html" title="twisted.spread.pb.PBClientFactory.getRootObject">pb.PBClientFactory.getRootObject</a></code>, returns
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>.
+When a response to the method-call being sent arrives, the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>'s <code class="python">callback</code> or <code class="python">errback</code>
+will be made, depending on whether an error occurred in processing the
+method call.</p>
+
+<p>You can use this technique to provide access to arbitrary sets of objects.
+Just remember that any object that might get passed <q>over the wire</q> must
+inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Referenceable.html" title="twisted.spread.pb.Referenceable">Referenceable</a></code>
+(or one of the other flavors). If you try to pass a non-Referenceable object
+(say, by returning one from a <code>remote_*</code> method), you'll get an
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.InsecureJelly.html" title="twisted.spread.jelly.InsecureJelly">InsecureJelly</a></code>
+exception<a href="#footnote-2" title="This can be overridden, by subclassing one of the Serializable flavors and defining custom serialization code for your class. See Passing Complex Types for details."><super>2</super></a>.</p>
+
+
+<h2>References can come back to you<a name="auto2"/></h2>
+
+<p>If your server gives a reference to a client, and then that client gives
+the reference back to the server, the server will wind up with the same
+object it gave out originally. The serialization layer watches for returning
+reference identifiers and turns them into actual objects. You need to stay
+aware of where the object lives: if it is on your side, you do actual method
+calls. If it is on the other side, you do
+ <code>.callRemote()</code><a href="#footnote-3" title="The binary nature of this local vs. remote scheme works because you cannot give RemoteReferences to a third party. If you could, then your object A could go to B, B could give it to C, C might give it back to you, and you would be hard pressed to tell if the object lived in C's memory space, in B's, or if it was really your own object, tarnished and sullied after being handed down like a really ugly picture that your great aunt owned and which nobody wants but which nobody can bear to throw out. Ok, not really like that, but you get the idea."><super>3</super></a>.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Two</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_print</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;two.print was given&quot;</span>, <span class="py-src-variable">arg</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">two</span>):
+ <span class="py-src-comment">#pb.Root.__init__(self) # pb.Root doesn't implement __init__</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">two</span> = <span class="py-src-variable">two</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getTwo</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;One.getTwo(), returning my two called&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">two</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">two</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_checkTwo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">newtwo</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;One.checkTwo(): comparing my two&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">two</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;One.checkTwo(): against your two&quot;</span>, <span class="py-src-variable">newtwo</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">two</span> == <span class="py-src-variable">newtwo</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;One.checkTwo(): our twos are the same&quot;</span>
+
+
+<span class="py-src-variable">two</span> = <span class="py-src-variable">Two</span>()
+<span class="py-src-variable">root_obj</span> = <span class="py-src-variable">One</span>(<span class="py-src-variable">two</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">root_obj</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb2server.py"><span class="filename">listings/pb/pb2server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">foo</span> = <span class="py-src-variable">Foo</span>()
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>().<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">foo</span>.<span class="py-src-variable">step1</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-comment"># keeping globals around is starting to get ugly, so we use a simple class</span>
+<span class="py-src-comment"># instead. Instead of hooking one function to the next, we hook one method</span>
+<span class="py-src-comment"># to the next.</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Foo</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">oneRef</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">step1</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">obj</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got one object:&quot;</span>, <span class="py-src-variable">obj</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">oneRef</span> = <span class="py-src-variable">obj</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to getTwo&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">oneRef</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;getTwo&quot;</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">step2</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">step2</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">two</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got two object:&quot;</span>, <span class="py-src-variable">two</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;giving it back to one&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;one is&quot;</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">oneRef</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">oneRef</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;checkTwo&quot;</span>, <span class="py-src-variable">two</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb2client.py"><span class="filename">listings/pb/pb2client.py</span></a></div></div>
+
+<p>The server gives a <code>Two()</code> instance to the client, who then
+returns the reference back to the server. The server compares the <q>two</q>
+given with the <q>two</q> received and shows that they are the same, and that
+both are real objects instead of remote references.</p>
+
+<p>A few other techniques are demonstrated in <code>pb2client.py</code>. One
+is that the callbacks are are added with <code>.addCallback</code> instead
+of <code>.addCallbacks</code>. As you can tell from the <a href="defer.html" shape="rect">Deferred</a> documentation, <code>.addCallback</code> is a
+simplified form which only adds a success callback. The other is that to
+keep track of state from one callback to the next (the remote reference to
+the main One() object), we create a simple class, store the reference in an
+instance thereof, and point the callbacks at a sequence of bound methods.
+This is a convenient way to encapsulate a state machine. Each response kicks
+off the next method, and any data that needs to be carried from one state to
+the next can simply be saved as an attribute of the object.</p>
+
+<p>Remember that the client can give you back any remote reference you've
+given them. Don't base your zillion-dollar stock-trading clearinghouse
+server on the idea that you trust the client to give you back the right
+reference. The security model inherent in PB means that they can <em>only</em>
+give you back a reference that you've given them for the current connection
+(not one you've given to someone else instead, nor one you gave them last
+time before the TCP session went down, nor one you haven't yet given to the
+client), but just like with URLs and HTTP cookies, the particular reference
+they give you is entirely under their control.</p>
+
+
+<h2>References to client-side objects<a name="auto3"/></h2>
+
+<p>Anything that's Referenceable can get passed across the wire, <em>in
+either direction</em>. The <q>client</q> can give a reference to the
+ <q>server</q>, and then the server can use .callRemote() to invoke methods on
+the client end. This fuzzes the distinction between <q>client</q> and
+ <q>server</q>: the only real difference is who initiates the original TCP
+connection; after that it's all symmetric.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_takeTwo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">two</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;received a Two called&quot;</span>, <span class="py-src-variable">two</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;telling it to print(12)&quot;</span>
+ <span class="py-src-variable">two</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;print&quot;</span>, <span class="py-src-number">12</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">One</span>()))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb3server.py"><span class="filename">listings/pb/pb3server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Two</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Referenceable</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_print</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Two.print() called with&quot;</span>, <span class="py-src-variable">arg</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">two</span> = <span class="py-src-variable">Two</span>()
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">def1</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">def1</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">got_obj</span>, <span class="py-src-variable">two</span>) <span class="py-src-comment"># hands our 'two' to the callback</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj</span>(<span class="py-src-parameter">obj</span>, <span class="py-src-parameter">two</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got One:&quot;</span>, <span class="py-src-variable">obj</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;giving it our two&quot;</span>
+ <span class="py-src-variable">obj</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;takeTwo&quot;</span>, <span class="py-src-variable">two</span>)
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/pb3client.py"><span class="filename">listings/pb/pb3client.py</span></a></div></div>
+
+<p>In this example, the client gives a reference to its own object to the
+server. The server then invokes a remote method on the client-side
+object.</p>
+
+
+<h2>Raising Remote Exceptions<a name="auto4"/></h2>
+
+<p>Everything so far has covered what happens when things go right. What
+about when they go wrong? The Python Way is to raise an exception of some
+sort. The Twisted Way is the same.</p>
+
+<p>The only special thing you do is to define your <code>Exception</code>
+subclass by deriving it from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Error.html" title="twisted.spread.pb.Error">pb.Error</a></code>. When any remotely-invokable method
+(like <code>remote_*</code> or <code>perspective_*</code>) raises a
+ <code>pb.Error</code>-derived exception, a serialized form of that Exception
+object will be sent back over the wire<a href="#footnote-4" title="To be precise, the Failure will be sent if any exception is raised, not just pb.Error-derived ones. But the server will print ugly error messages if you raise ones that aren't derived from pb.Error."><super>4</super></a>. The other side (which
+did <code>callRemote</code>) will have the <q><code>errback</code></q>
+callback run with a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code> object that contains a copy of
+the exception object. This <code>Failure</code> object can be queried to
+retrieve the error message and a stack traceback.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code> is a
+special class, defined in <code>twisted/python/failure.py</code>, created to
+make it easier to handle asynchronous exceptions. Just as exception handlers
+can be nested, <code>errback</code> functions can be chained. If one errback
+can't handle the particular type of failure, it can be <q>passed along</q> to a
+errback handler further down the chain.</p>
+
+<p>For simple purposes, think of the <code>Failure</code> as just a container
+for remotely-thrown <code>Exception</code> objects. To extract the string that
+was put into the exception, use its <code>.getErrorMessage()</code> method.
+To get the type of the exception (as a string), look at its
+ <code>.type</code> attribute. The stack traceback is available too. The
+intent is to let the errback function get just as much information about the
+exception as Python's normal <code>try:</code> clauses do, even though the
+exception occurred in somebody else's memory space at some unknown time in
+the past.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyError</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Error</span>):
+ <span class="py-src-string">&quot;&quot;&quot;This is an Expected Exception. Something bad happened.&quot;&quot;&quot;</span>
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyError2</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-string">&quot;&quot;&quot;This is an Unexpected Exception. Something really bad happened.&quot;&quot;&quot;</span>
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_broken</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">msg</span> = <span class="py-src-string">&quot;fall down go boom&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;raising a MyError exception with data '%s'&quot;</span> % <span class="py-src-variable">msg</span>
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">MyError</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_broken2</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">msg</span> = <span class="py-src-string">&quot;hadda owie&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;raising a MyError2 exception with data '%s'&quot;</span> % <span class="py-src-variable">msg</span>
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">MyError2</span>(<span class="py-src-variable">msg</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">One</span>()))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/exc_server.py"><span class="filename">listings/pb/exc_server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">got_obj</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj</span>(<span class="py-src-parameter">obj</span>):
+ <span class="py-src-comment"># change &quot;broken&quot; into &quot;broken2&quot; to demonstrate an unhandled exception</span>
+ <span class="py-src-variable">d2</span> = <span class="py-src-variable">obj</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;broken&quot;</span>)
+ <span class="py-src-variable">d2</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">working</span>)
+ <span class="py-src-variable">d2</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">broken</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">working</span>():
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;erm, it wasn't *supposed* to work..&quot;</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">broken</span>(<span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got remote Exception&quot;</span>
+ <span class="py-src-comment"># reason should be a Failure (or subclass) holding the MyError exception</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; .__class__ =&quot;</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">__class__</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; .getErrorMessage() =&quot;</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">getErrorMessage</span>()
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; .type =&quot;</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">type</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/exc_client.py"><span class="filename">listings/pb/exc_client.py</span></a></div></div>
+
+<pre class="shell" xml:space="preserve">
+$ ./exc_client.py
+got remote Exception
+ .__class__ = twisted.spread.pb.CopiedFailure
+ .getErrorMessage() = fall down go boom
+ .type = __main__.MyError
+Main loop terminated.
+</pre>
+
+<p>Oh, and what happens if you raise some other kind of exception? Something
+that <em>isn't</em> subclassed from <code>pb.Error</code>? Well, those are
+called <q>unexpected exceptions</q>, which make Twisted think that something
+has <em>really</em> gone wrong. These will raise an exception on the
+ <em>server</em> side. This won't break the connection (the exception is
+trapped, just like most exceptions that occur in response to network
+traffic), but it will print out an unsightly stack trace on the server's
+stderr with a message that says <q>Peer Will Receive PB Traceback</q>, just
+as if the exception had happened outside a remotely-invokable method. (This
+message will go the current log target, if <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.startLogging.html" title="twisted.python.log.startLogging">log.startLogging</a></code> was used to redirect it). The
+client will get the same <code>Failure</code> object in either case, but
+subclassing your exception from <code>pb.Error</code> is the way to tell
+Twisted that you expect this sort of exception, and that it is ok to just
+let the client handle it instead of also asking the server to complain. Look
+at <code>exc_client.py</code> and change it to invoke <code>broken2()</code>
+instead of <code>broken()</code> to see the change in the server's
+behavior.</p>
+
+<p>If you don't add an <code>errback</code> function to the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>, then a remote
+exception will still send a <code>Failure</code> object back over, but it
+will get lodged in the <code>Deferred</code> with nowhere to go. When that
+ <code>Deferred</code> finally goes out of scope, the side that did
+ <code>callRemote</code> will emit a message about an <q>Unhandled error in
+Deferred</q>, along with an ugly stack trace. It can't raise an exception at
+that point (after all, the <code>callRemote</code> that triggered the
+problem is long gone), but it will emit a traceback. So be a good programmer
+and <em>always</em> add <code>errback</code> handlers, even if they are just
+calls to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.err.html" title="twisted.python.log.err">log.err</a></code>.</p>
+
+<h2>Try/Except blocks and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.trap.html" title="twisted.python.failure.Failure.trap">Failure.trap</a></code> <a name="auto5"/></h2>
+
+<p>To implement the equivalent of the Python try/except blocks (which can
+trap particular kinds of exceptions and pass others <q>up</q> to
+higher-level <code>try/except</code> blocks), you can use the
+ <code>.trap()</code> method in conjunction with multiple
+ <code>errback</code> handlers on the <code>Deferred</code>. Re-raising an
+exception in an <code>errback</code> handler serves to pass that new
+exception to the next handler in the chain. The <code>trap</code> method is
+given a list of exceptions to look for, and will re-raise anything that
+isn't on the list. Instead of passing unhandled exceptions <q>up</q> to an
+enclosing <code>try</code> block, this has the effect of passing the
+exception <q>off</q> to later <code>errback</code> handlers on the same
+<code>Deferred</code>. The <code>trap</code> calls are used in chained
+errbacks to test for each kind of exception in sequence. </p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyException</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Error</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_fooMethod</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">arg</span> == <span class="py-src-string">&quot;panic!&quot;</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">MyException</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;response&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_shutdown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8800</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">One</span>()))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/trap_server.py"><span class="filename">listings/pb/trap_server.py</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>, <span class="py-src-variable">jelly</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyException</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Error</span>): <span class="py-src-keyword">pass</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyOtherException</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Error</span>): <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ScaryObject</span>:
+ <span class="py-src-comment"># not safe for serialization</span>
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">worksLike</span>(<span class="py-src-parameter">obj</span>):
+ <span class="py-src-comment"># the callback/errback sequence in class One works just like an</span>
+ <span class="py-src-comment"># asynchronous version of the following:</span>
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">response</span> = <span class="py-src-variable">obj</span>.<span class="py-src-variable">callMethod</span>(<span class="py-src-variable">name</span>, <span class="py-src-variable">arg</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">DeadReferenceError</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; stale reference: the client disconnected or crashed&quot;</span>
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">jelly</span>.<span class="py-src-variable">InsecureJelly</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; InsecureJelly: you tried to send something unsafe to them&quot;</span>
+ <span class="py-src-keyword">except</span> (<span class="py-src-variable">MyException</span>, <span class="py-src-variable">MyOtherException</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; remote raised a MyException&quot;</span> <span class="py-src-comment"># or MyOtherException</span>
+ <span class="py-src-keyword">except</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; something else happened&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; method successful, response:&quot;</span>, <span class="py-src-variable">response</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">One</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">worked</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; method successful, response:&quot;</span>, <span class="py-src-variable">response</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">check_InsecureJelly</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>):
+ <span class="py-src-variable">failure</span>.<span class="py-src-variable">trap</span>(<span class="py-src-variable">jelly</span>.<span class="py-src-variable">InsecureJelly</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; InsecureJelly: you tried to send something unsafe to them&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">check_MyException</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>):
+ <span class="py-src-variable">which</span> = <span class="py-src-variable">failure</span>.<span class="py-src-variable">trap</span>(<span class="py-src-variable">MyException</span>, <span class="py-src-variable">MyOtherException</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">which</span> == <span class="py-src-variable">MyException</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; remote raised a MyException&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; remote raised a MyOtherException&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">catch_everythingElse</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; something else happened&quot;</span>
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>(<span class="py-src-variable">failure</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">doCall</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">explanation</span>, <span class="py-src-parameter">arg</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">explanation</span>
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">deferred</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;fooMethod&quot;</span>, <span class="py-src-variable">arg</span>)
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">worked</span>)
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">check_InsecureJelly</span>)
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">check_MyException</span>)
+ <span class="py-src-variable">deferred</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">catch_everythingElse</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">pb</span>.<span class="py-src-variable">DeadReferenceError</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot; stale reference: the client disconnected or crashed&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callOne</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">doCall</span>(<span class="py-src-string">&quot;callOne: call with safe object&quot;</span>, <span class="py-src-string">&quot;safe string&quot;</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callTwo</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">doCall</span>(<span class="py-src-string">&quot;callTwo: call with dangerous object&quot;</span>, <span class="py-src-variable">ScaryObject</span>())
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callThree</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">doCall</span>(<span class="py-src-string">&quot;callThree: call that raises remote exception&quot;</span>, <span class="py-src-string">&quot;panic!&quot;</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callShutdown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;telling them to shut down&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;shutdown&quot;</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">callFour</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">doCall</span>(<span class="py-src-string">&quot;callFour: call on stale reference&quot;</span>, <span class="py-src-string">&quot;dummy&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">got_obj</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">obj</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remote</span> = <span class="py-src-variable">obj</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">1</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">callOne</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">2</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">callTwo</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">3</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">callThree</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">4</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">callShutdown</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">callFour</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">6</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>)
+
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8800</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">deferred</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>()
+<span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">One</span>().<span class="py-src-variable">got_obj</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/pb/trap_client.py"><span class="filename">listings/pb/trap_client.py</span></a></div></div>
+
+<pre class="shell" xml:space="preserve">
+$ ./trap_client.py
+callOne: call with safe object
+ method successful, response: response
+callTwo: call with dangerous object
+ InsecureJelly: you tried to send something unsafe to them
+callThree: call that raises remote exception
+ remote raised a MyException
+telling them to shut down
+callFour: call on stale reference
+ stale reference: the client disconnected or crashed
+</pre>
+
+
+<p>In this example, <code>callTwo</code> tries to send an instance of a
+locally-defined class through <code>callRemote</code>. The default security
+model implemented by <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.html" title="twisted.spread.jelly">jelly</a></code>
+on the remote end will not allow unknown classes to be unserialized (i.e.
+taken off the wire as a stream of bytes and turned back into an object: a
+living, breathing instance of some class): one reason is that it does not
+know which local class ought to be used to create an instance that
+corresponds to the remote object<a href="#footnote-5" title="The naive approach of simply doing import SomeClass to match a remote caller who claims to have an object of type SomeClass could have nasty consequences for some modules that do significant operations in their __init__ methods (think telnetlib.Telnet(host='localhost', port='chargen'), or even more powerful classes that you have available in your server program). Allowing a remote entity to create arbitrary classes in your namespace is nearly equivalent to allowing them to run arbitrary code. The InsecureJelly exception arises because the class being sent over the wire has not been registered with the serialization layer (known as jelly). The easiest way to make it possible to copy entire class instances over the wire is to have them inherit from pb.Copyable, and then to use setUnjellyableForClass(remoteClass, localClass) on the receiving side. See Passing Complex Types for an example."><super>5</super></a>.</p>
+
+<p>The receiving end of the connection gets to decide what to accept and what
+to reject. It indicates its disapproval by raising a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.InsecureJelly.html" title="twisted.spread.jelly.InsecureJelly">jelly.InsecureJelly</a></code> exception. Because it occurs
+at the remote end, the exception is returned to the caller asynchronously,
+so an <code>errback</code> handler for the associated <code>Deferred</code>
+is run. That errback receives a <code>Failure</code> which wraps the
+ <code>InsecureJelly</code>.</p>
+
+
+<p>Remember that <code>trap</code> re-raises exceptions that it wasn't asked
+to look for. You can only check for one set of exceptions per errback
+handler: all others must be checked in a subsequent handler.
+ <code>check_MyException</code> shows how multiple kinds of exceptions can be
+checked in a single errback: give a list of exception types to
+ <code>trap</code>, and it will return the matching member. In this case, the
+kinds of exceptions we are checking for (<code>MyException</code> and
+ <code>MyOtherException</code>) may be raised by the remote end: they inherit
+from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Error.html" title="twisted.spread.pb.Error">pb.Error</a></code>.</p>
+
+<p>The handler can return <code>None</code> to terminate processing of the
+errback chain (to be precise, it switches to the callback that follows the
+errback; if there is no callback then processing terminates). It is a good
+idea to put an errback that will catch everything (no <code>trap</code>
+tests, no possible chance of raising more exceptions, always returns
+ <code>None</code>) at the end of the chain. Just as with regular <code>try:
+except:</code> handlers, you need to think carefully about ways in which
+your errback handlers could themselves raise exceptions. The extra
+importance in an asynchronous environment is that an exception that falls
+off the end of the <code>Deferred</code> will not be signalled until that
+ <code>Deferred</code> goes out of scope, and at that point may only cause a
+log message (which could even be thrown away if <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.startLogging.html" title="twisted.python.log.startLogging">log.startLogging</a></code> is not used to point it at
+stdout or a log file). In contrast, a synchronous exception that is not
+handled by any other <code>except:</code> block will very visibly terminate
+the program immediately with a noisy stack trace.</p>
+
+<p><code>callFour</code> shows another kind of exception that can occur
+while using <code>callRemote</code>: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.DeadReferenceError.html" title="twisted.spread.pb.DeadReferenceError">pb.DeadReferenceError</a></code>. This one occurs when the
+remote end has disconnected or crashed, leaving the local side with a stale
+reference. This kind of exception happens to be reported right away (XXX: is
+this guaranteed? probably not), so must be caught in a traditional
+synchronous <code>try: except pb.DeadReferenceError</code> block. </p>
+
+<p>Yet another kind that can occur is a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.PBConnectionLost.html" title="twisted.spread.pb.PBConnectionLost">pb.PBConnectionLost</a></code> exception. This occurs
+(asynchronously) if the connection was lost while you were waiting for a
+<code>callRemote</code> call to complete. When the line goes dead, all
+pending requests are terminated with this exception. Note that you have no
+way of knowing whether the request made it to the other end or not, nor how
+far along in processing it they had managed before the connection was
+lost. XXX: explain transaction semantics, find a decent reference.</p>
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">There are a few other classes
+that can bestow this ability, but pb.Referenceable is the easiest to
+understand; see 'flavors' below for details on the others.</span></a></li><li><a name="footnote-2"><span class="footnote">This can be overridden, by subclassing one of
+the Serializable flavors and defining custom serialization code for your
+class. See <a href="pb-copyable.html" shape="rect">Passing Complex Types</a> for
+details.</span></a></li><li><a name="footnote-3"><span class="footnote">The binary nature of this
+local vs. remote scheme works because you cannot give RemoteReferences to a
+third party. If you could, then your object A could go to B, B could give it to
+C, C might give it back to you, and you would be hard pressed to tell if the
+object lived in C's memory space, in B's, or if it was really your own object,
+tarnished and sullied after being handed down like a really ugly picture that
+your great aunt owned and which nobody wants but which nobody can bear to throw
+out. Ok, not really like that, but you get the idea.</span></a></li><li><a name="footnote-4"><span class="footnote">To be precise,
+the Failure will be sent if <em>any</em> exception is raised, not just
+pb.Error-derived ones. But the server will print ugly error messages if you
+raise ones that aren't derived from pb.Error.</span></a></li><li><a name="footnote-5"><span class="footnote"><p>The naive approach
+of simply doing <code>import SomeClass</code> to match a remote caller who
+claims to have an object of type <q>SomeClass</q> could have nasty consequences
+for some modules that do significant operations in their <code>__init__</code>
+methods (think <code>telnetlib.Telnet(host='localhost', port='chargen')</code>,
+ or even more powerful classes that you have available in your server program).
+Allowing a remote entity to create arbitrary classes in your namespace is
+nearly equivalent to allowing them to run arbitrary code.</p>
+<p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.InsecureJelly.html" title="twisted.spread.jelly.InsecureJelly">InsecureJelly</a></code>
+exception arises because the class being sent over the wire has not been
+registered with the serialization layer (known as <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.jelly.html" title="twisted.spread.jelly">jelly</a></code>). The easiest way to make it possible to
+copy entire class instances over the wire is to have them inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.pb.Copyable.html" title="twisted.spread.pb.Copyable">pb.Copyable</a></code>, and then to use
+ <code>setUnjellyableForClass(remoteClass, localClass)</code> on the
+receiving side. See <a href="pb-copyable.html" shape="rect">Passing Complex Types</a>
+for an example.</p></span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/pb.html b/doc/core/howto/pb.html
new file mode 100644
index 0000000..f610dee
--- /dev/null
+++ b/doc/core/howto/pb.html
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Overview of Twisted Spread</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Overview of Twisted Spread</h1>
+ <div class="toc"><ol><li><a href="#auto0">Rationale</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<p> Perspective Broker (affectionately known as <q>PB</q>) is an
+asynchronous, symmetric<a href="#footnote-1" title="There is a negotiation phase for the banana serialization protocol with particular roles for listener and initiator, so it's not completely symmetric, but after the connection is fully established, the protocol is completely symmetrical."><super>1</super></a> network protocol for secure,
+remote method calls and transferring of objects. PB is <q>translucent, not
+transparent</q>, meaning that it is very visible and obvious to see the
+difference between local method calls and potentially remote method calls,
+but remote method calls are still extremely convenient to make, and it is
+easy to emulate them to have objects which work both locally and
+remotely.</p>
+
+<p>PB supports user-defined serialized data in return values, which can be
+either copied each time the value is returned, or <q>cached</q>: only copied
+once and updated by notifications.</p>
+
+<p>PB gets its name from the fact that access to objects is through a
+<q>perspective</q>. This means that when you are responding to a remote
+method call, you can establish who is making the call.</p>
+
+<h2>Rationale<a name="auto0"/></h2>
+
+<p>No other currently existing protocols have all the properties of PB at the
+same time. The particularly interesting combination of attributes, though, is
+that PB is flexible and lightweight, allowing for rapid development, while
+still powerful enough to do two-way method calls and user-defined data
+types.</p>
+
+<p>It is important to have these attributes in order to allow for a protocol
+which is extensible. One of the facets of this flexibility is that PB can
+integrate an arbitrary number of services could be aggregated over a single
+connection, as well as publish and call new methods on existing objects
+without restarting the server or client.</p>
+
+<h2>Footnotes</h2><ol><li><a name="footnote-1"><span class="footnote">There is a negotiation phase
+for the <code>banana</code> serialization protocol with particular roles for listener and initiator, so it's not
+<em>completely</em> symmetric, but after the connection is fully established,
+the protocol is completely symmetrical.</span></a></li></ol></div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/plugin.html b/doc/core/howto/plugin.html
new file mode 100644
index 0000000..9aacdaa
--- /dev/null
+++ b/doc/core/howto/plugin.html
@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Twisted Plugin System</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Twisted Plugin System</h1>
+ <div class="toc"><ol><li><a href="#auto0">Writing Extensible Programs</a></li><li><a href="#auto1">Extending an Existing Program</a></li><li><a href="#auto2">Alternate Plugin Packages</a></li><li><a href="#auto3">Plugin Caching</a></li><li><a href="#auto4">Further Reading</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>The purpose of this guide is to describe the preferred way to
+ write extensible Twisted applications (and consequently, also to
+ describe how to extend applications written in such a way). This
+ extensibility is achieved through the definition of one or more
+ APIs and a mechanism for collecting code plugins which
+ implement this API to provide some additional functionality.
+ At the base of this system is the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.html" title="twisted.plugin">twisted.plugin</a></code> module.</p>
+
+ <p>Making an application extensible using the plugin system has
+ several strong advantages over other techniques:</p>
+
+ <ul>
+ <li>It allows third-party developers to easily enhance your
+ software in a way that is loosely coupled: only the plugin API
+ is required to remain stable.</li>
+
+ <li>It allows new plugins to be discovered flexibly. For
+ example, plugins can be loaded and saved when a program is first
+ run, or re-discovered each time the program starts up, or they
+ can be polled for repeatedly at runtime (allowing the discovery
+ of new plugins installed after the program has started).</li>
+ </ul>
+
+ <h2>Writing Extensible Programs<a name="auto0"/></h2>
+
+ <p>Taking advantage of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.html" title="twisted.plugin">twisted.plugin</a></code> is
+ a two step process:</p>
+
+ <ol>
+ <li>
+ <p>
+ Define an interface which plugins will be required to implement.
+ This is done using the zope.interface package in the same way one
+ would define an interface for any other purpose.
+ </p>
+
+ <p>
+ A convention for defining interfaces is do so in a file named like
+ <em>ProjectName/projectname/iprojectname.py</em>. The rest of this
+ document will follow that convention: consider the following
+ interface definition be in <code>Matsim/matsim/imatsim.py</code>, an
+ interface definition module for a hypothetical material simulation
+ package.
+ </p>
+ </li>
+
+ <li>
+ At one or more places in your program, invoke <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.getPlugins.html" title="twisted.plugin.getPlugins">twisted.plugin.getPlugins</a></code> and iterate over its
+ result.
+ </li>
+ </ol>
+
+ <p>
+ As an example of the first step, consider the following interface
+ definition for a physical modelling system.
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">Attribute</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IMaterial</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An object with specific physical properties
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">yieldStress</span>(<span class="py-src-parameter">temperature</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Returns the pressure this material can support without
+ fracturing at the given temperature.
+
+ @type temperature: C{float}
+ @param temperature: Kelvins
+
+ @rtype: C{float}
+ @return: Pascals
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-variable">dielectricConstant</span> = <span class="py-src-variable">Attribute</span>(<span class="py-src-string">&quot;&quot;&quot;
+ @type dielectricConstant: C{complex}
+ @ivar dielectricConstant: The relative permittivity, with the
+ real part giving reflective surface properties and the
+ imaginary part giving the radio absorption coefficient.
+ &quot;&quot;&quot;</span>)
+</pre>
+
+ <p>In another module, we might have a function that operates on
+ objects providing the <code>IMaterial</code> interface:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">displayMaterial</span>(<span class="py-src-parameter">m</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'A material with yield stress %s at 500 K'</span> % (<span class="py-src-variable">m</span>.<span class="py-src-variable">yieldStress</span>(<span class="py-src-number">500</span>),)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Also a dielectric constant of %s.'</span> % (<span class="py-src-variable">m</span>.<span class="py-src-variable">dielectricConstant</span>,)
+</pre>
+
+ <p>The last piece of required code is that which collects
+ <code>IMaterial</code> providers and passes them to the
+ <code>displayMaterial</code> function.</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">getPlugins</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">matsim</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">imatsim</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">displayAllKnownMaterials</span>():
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">material</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">getPlugins</span>(<span class="py-src-variable">imatsim</span>.<span class="py-src-variable">IMaterial</span>):
+ <span class="py-src-variable">displayMaterial</span>(<span class="py-src-variable">material</span>)
+</pre>
+
+ <p>Third party developers may now contribute different materials
+ to be used by this modelling system by implementing one or more
+ plugins for the <code>IMaterial</code> interface.</p>
+
+ <h2>Extending an Existing Program<a name="auto1"/></h2>
+
+ <p>The above code demonstrates how an extensible program might be
+ written using Twisted's plugin system. How do we write plugins
+ for it, though? Essentially, we create objects which provide the
+ required interface and then make them available at a particular
+ location. Consider the following example.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IPlugin</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">matsim</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">imatsim</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SimpleMaterial</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPlugin</span>, <span class="py-src-variable">imatsim</span>.<span class="py-src-variable">IMaterial</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">yieldStressFactor</span>, <span class="py-src-parameter">dielectricConstant</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_yieldStressFactor</span> = <span class="py-src-variable">yieldStressFactor</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">dielectricConstant</span> = <span class="py-src-variable">dielectricConstant</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">yieldStress</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">temperature</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_yieldStressFactor</span> * <span class="py-src-variable">temperature</span>
+
+<span class="py-src-variable">steelPlate</span> = <span class="py-src-variable">SimpleMaterial</span>(<span class="py-src-number">2.06842719e11</span>, <span class="py-src-number">2.7</span> + <span class="py-src-number">0.2j</span>)
+<span class="py-src-variable">brassPlate</span> = <span class="py-src-variable">SimpleMaterial</span>(<span class="py-src-number">1.03421359e11</span>, <span class="py-src-number">1.4</span> + <span class="py-src-number">0.5j</span>)
+</pre>
+
+ <p><code>steelPlate</code> and <code>brassPlate</code> now provide both
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.IPlugin.html" title="twisted.plugin.IPlugin">IPlugin</a></code> and <code>IMaterial</code>.
+ All that remains is to make this module available at an appropriate
+ location. For this, there are two options. The first of these is
+ primarily useful during development: if a directory which
+ has been added to <code>sys.path</code> (typically by adding it to the
+ <code class="shell">PYTHONPATH</code> environment variable) contains a
+ <em>directory</em> named <code class="shell">twisted/plugins/</code>,
+ each <code class="shell">.py</code> file in that directory will be loaded
+ as a source of plugins. This directory <em>must not</em> be a Python
+ package: including <code class="shell">__init__.py</code> will cause the
+ directory to be skipped and no plugins loaded from it. Second, each
+ module in the installed version of Twisted's <code class="shell">
+ twisted.plugins</code> package will also be loaded as a source of
+ plugins.</p>
+
+ <p>Once this plugin is installed in one of these two ways,
+ <code>displayAllKnownMaterials</code> can be run and we will see
+ two pairs of output: one for a steel plate and one for a brass
+ plate.</p>
+
+ <h2>Alternate Plugin Packages<a name="auto2"/></h2>
+
+ <p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.getPlugins.html" title="twisted.plugin.getPlugins">getPlugins</a></code> takes one
+ additional argument not mentioned above. If passed in, the 2nd argument
+ should be a module or package to be used instead of
+ <code>twisted.plugins</code> as the plugin meta-package. If you
+ are writing a plugin for a Twisted interface, you should never
+ need to pass this argument. However, if you have developed an
+ interface of your own, you may want to mandate that plugins for it
+ are installed in your own plugins package, rather than in
+ Twisted's.</p>
+
+ <p>You may want to support <code class="shell">yourproject/plugins/</code>
+ directories for ease of development. To do so, you should make <code class="shell">yourproject/plugins/__init__.py</code> contain at least
+ the following lines.</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pluginPackagePaths</span>
+<span class="py-src-variable">__path__</span>.<span class="py-src-variable">extend</span>(<span class="py-src-variable">pluginPackagePaths</span>(<span class="py-src-variable">__name__</span>))
+<span class="py-src-variable">__all__</span> = []
+</pre>
+
+ <p>The key behavior here is that interfaces are essentially paired
+ with a particular plugin package. If plugins are installed in a
+ different package than the one the code which relies on the
+ interface they provide, they will not be found when the
+ application goes to load them.</p>
+
+ <h2>Plugin Caching<a name="auto3"/></h2>
+
+ <p>In the course of using the Twisted plugin system, you may
+ notice <code class="shell">dropin.cache</code> files appearing at
+ various locations. These files are used to cache information
+ about what plugins are present in the directory which contains
+ them. At times, this cached information may become out of date.
+ Twisted uses the mtimes of various files involved in the plugin
+ system to determine when this cache may have become invalid.
+ Twisted will try to re-write the cache each time it tries to use
+ it but finds it out of date.</p>
+
+ <p>For a site-wide install, it may not (indeed, should not) be
+ possible for applications running as normal users to rewrite the
+ cache file. While these applications will still run and find
+ correct plugin information, they may run more slowly than they
+ would if the cache was up to date, and they may also report
+ exceptions if certain plugins have been removed but which the
+ cache still references. For these reasons, when installing or
+ removing software which provides Twisted plugins, the site
+ administrator should be sure the cache is regenerated.
+ Well-behaved package managers for such software should take this
+ task upon themselves, since it is trivially automatable. The
+ canonical way to regenerate the cache is to run the following
+ Python code:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IPlugin</span>, <span class="py-src-variable">getPlugins</span>
+<span class="py-src-variable">list</span>(<span class="py-src-variable">getPlugins</span>(<span class="py-src-variable">IPlugin</span>))
+</pre>
+
+ <p>As mentioned, it is normal for exceptions to be raised
+ <strong>once</strong> here if plugins have been removed.</p>
+
+ <h2>Further Reading<a name="auto4"/></h2>
+
+ <ul>
+
+ <li><a href="components.html" shape="rect">Components: Interfaces and Adapters</a></li>
+
+ </ul>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/process.html b/doc/core/howto/process.html
new file mode 100644
index 0000000..ed04ab0
--- /dev/null
+++ b/doc/core/howto/process.html
@@ -0,0 +1,732 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using Processes</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using Processes</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Running Another Process</a></li><li><a href="#auto2">Writing a ProcessProtocol</a></li><li><a href="#auto3">Things that can happen to your ProcessProtocol</a></li><li><a href="#auto4">Things you can do from your ProcessProtocol</a></li><li><a href="#auto5">Verbose Example</a></li><li><a href="#auto6">Doing it the Easy Way</a></li><li><a href="#auto7">Mapping File Descriptors</a></li><ul><li><a href="#auto8">ProcessProtocols with extra file descriptors</a></li><li><a href="#auto9">Examples</a></li></ul></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+<p>Along with connection to servers across the internet, Twisted also
+connects to local processes with much the same API. The API is described in
+more detail in the documentation of:
+<ul>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorProcess.html" title="twisted.internet.interfaces.IReactorProcess">twisted.internet.interfaces.IReactorProcess</a></code></li>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProcessTransport.html" title="twisted.internet.interfaces.IProcessTransport">twisted.internet.interfaces.IProcessTransport</a></code></li>
+<li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProcessProtocol.html" title="twisted.internet.interfaces.IProcessProtocol">twisted.internet.interfaces.IProcessProtocol</a></code></li>
+</ul>
+</p>
+
+ <h2>Running Another Process<a name="auto1"/></h2>
+
+<p>Processes are run through the reactor,
+using <code>reactor.spawnProcess</code>. Pipes are created to the child process,
+and added to the reactor core so that the application will not block while
+sending data into or pulling data out of the new
+process. <code>reactor.spawnProcess</code> requires two arguments,
+ <code>processProtocol</code> and <code>executable</code>, and optionally takes
+several more: <code>args</code>, <code>environment</code>,
+ <code>path</code>, <code>userID</code>, <code>groupID</code>,
+ <code>usePTY</code>, and <code>childFDs</code>. Not all of these are
+available on Windows.</p>
+
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">processProtocol</span> = <span class="py-src-variable">MyProcessProtocol</span>()
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">spawnProcess</span>(<span class="py-src-variable">processProtocol</span>, <span class="py-src-variable">executable</span>, <span class="py-src-variable">args</span>=[<span class="py-src-variable">program</span>, <span class="py-src-variable">arg1</span>, <span class="py-src-variable">arg2</span>],
+ <span class="py-src-variable">env</span>={<span class="py-src-string">'HOME'</span>: <span class="py-src-variable">os</span>.<span class="py-src-variable">environ</span>[<span class="py-src-string">'HOME'</span>]}, <span class="py-src-variable">path</span>,
+ <span class="py-src-variable">uid</span>, <span class="py-src-variable">gid</span>, <span class="py-src-variable">usePTY</span>, <span class="py-src-variable">childFDs</span>)
+</pre>
+
+<ul>
+
+ <li><code>processProtocol</code> should be an instance of a subclass of
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.ProcessProtocol.html" title="twisted.internet.protocol.ProcessProtocol">twisted.internet.protocol.ProcessProtocol</a></code>. The
+ interface is described below.</li>
+
+ <li><code>executable</code> is the full path of the program to run. It
+ will be connected to processProtocol.</li>
+
+ <li><code>args</code> is a list of command line arguments to be passed to
+ the process. <code>args[0]</code> should be the name of the process.</li>
+
+ <li><code>env</code> is a dictionary containing the environment to pass
+ through to the process.</li>
+
+ <li><code>path</code> is the directory to run the process in. The child
+ will switch to the given directory just before starting the new program.
+ The default is to stay in the current directory.</li>
+
+ <li><code>uid</code> and <code>gid</code> are the user ID and group ID to
+ run the subprocess as. Of course, changing identities will be more likely
+ to succeed if you start as root.</li>
+
+ <li><code>usePTY</code> specifies whether the child process should be run
+ with a pty, or if it should just get a pair of pipes. Whether a program
+ needs to be run with a PTY or not depends on the particulars of that
+ program. Often, programs which primarily interact with users via a terminal
+ do need a PTY.</li>
+
+ <li><code>childFDs</code> lets you specify how the child's file
+ descriptors should be set up. Each key is a file descriptor number (an
+ integer) as seen by the child. 0, 1, and 2 are usually stdin, stdout, and
+ stderr, but some programs may be instructed to use additional fds through
+ command-line arguments or environment variables. Each value is either an
+ integer specifying one of the parent's current file descriptors, the
+ string <q>r</q> which creates a pipe that the parent can read from, or the
+ string <q>w</q> which creates a pipe that the parent can write to. If
+ <code>childFDs</code> is not provided, a default is used which creates the
+ usual stdin-writer, stdout-reader, and stderr-reader pipes.</li>
+
+</ul>
+
+<p><code>args</code> and <code>env</code> have empty default values, but
+many programs depend upon them to be set correctly. At the very least,
+ <code>args[0]</code> should probably be the same as <code>executable</code>.
+If you just provide <code>os.environ</code> for <code>env</code>, the child
+program will inherit the environment from the current process, which is
+usually the civilized thing to do (unless you want to explicitly clean the
+environment as a security precaution). The default is to give an empty
+<code>env</code> to the child.</p>
+
+<p><code>reactor.spawnProcess</code> returns an instance that
+implements
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProcessTransport.html" title="twisted.internet.interfaces.IProcessTransport">IProcessTransport</a></code>.
+</p>
+
+ <h2>Writing a ProcessProtocol<a name="auto2"/></h2>
+
+<p>The ProcessProtocol you pass to <code>spawnProcess</code> is your
+interaction with the process. It has a very similar signature to a regular
+Protocol, but it has several extra methods to deal with events specific to
+a process. In our example, we will interface with 'wc' to create a word count
+of user-given text. First, we'll start by importing the required modules, and
+writing the initialization for our ProcessProtocol.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WCProcessProtocol</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ProcessProtocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">text</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">text</span> = <span class="py-src-variable">text</span>
+</pre>
+
+<p>When the ProcessProtocol is connected to the protocol, it has the
+connectionMade method called. In our protocol, we will write our text to the
+standard input of our process and then close standard input, to let the
+process know we are done writing to it.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">text</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">closeStdin</span>()
+</pre>
+
+<p>At this point, the process has receieved the data, and it's time for us
+to read the results. Instead of being received in <code>dataReceived</code>,
+data from standard output is received in <code>outReceived</code>. This is
+to distinguish it from data on standard error.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">outReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">fieldLength</span> = <span class="py-src-variable">len</span>(<span class="py-src-variable">data</span>) / <span class="py-src-number">3</span>
+ <span class="py-src-variable">lines</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">data</span>[:<span class="py-src-variable">fieldLength</span>])
+ <span class="py-src-variable">words</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">data</span>[<span class="py-src-variable">fieldLength</span>:<span class="py-src-variable">fieldLength</span>*<span class="py-src-number">2</span>])
+ <span class="py-src-variable">chars</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">data</span>[<span class="py-src-variable">fieldLength</span>*<span class="py-src-number">2</span>:])
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">receiveCounts</span>(<span class="py-src-variable">lines</span>, <span class="py-src-variable">words</span>, <span class="py-src-variable">chars</span>)
+</pre>
+
+<p>Now, the process has parsed the output, and ended the connection to the
+process. Then it sends the results on to the final method, receiveCounts.
+This is for users of the class to override, so as to do other things with
+the data. For our demonstration, we will just print the results.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">receiveCounts</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">lines</span>, <span class="py-src-parameter">words</span>, <span class="py-src-parameter">chars</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Received counts from wc.'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Lines:'</span>, <span class="py-src-variable">lines</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Words:'</span>, <span class="py-src-variable">words</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Characters:'</span>, <span class="py-src-variable">chars</span>
+</pre>
+
+<p>We're done! To use our WCProcessProtocol, we create an instance, and pass
+it to spawnProcess.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">wcProcess</span> = <span class="py-src-variable">WCProcessProtocol</span>(<span class="py-src-string">&quot;accessing protocols through Twisted is fun!\n&quot;</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">spawnProcess</span>(<span class="py-src-variable">wcProcess</span>, <span class="py-src-string">'wc'</span>, [<span class="py-src-string">'wc'</span>])
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+
+<h2>Things that can happen to your ProcessProtocol<a name="auto3"/></h2>
+
+<p>These are the methods that you can usefully override in your subclass of
+ <code>ProcessProtocol</code>:</p>
+
+<ul>
+
+ <li><code>.connectionMade()</code>: This is called when the program is
+ started, and makes a good place to write data into the stdin pipe (using
+ <code class="python">self.transport.write</code>).</li>
+
+ <li><code>.outReceived(data)</code>: This is called with data that was
+ received from the process' stdout pipe. Pipes tend to provide data in
+ larger chunks than sockets (one kilobyte is a common buffer size), so you
+ may not experience the <q>random dribs and drabs</q> behavior typical of
+ network sockets, but regardless you should be prepared to deal if you
+ don't get all your data in a single call. To do it properly,
+ <code>outReceived</code> ought to simply accumulate the data and put off
+ doing anything with it until the process has finished.</li>
+
+ <li><code>.errReceived(data)</code>: This is called with data from the
+ process' stderr pipe. It behaves just like <code>outReceived</code>.</li>
+
+ <li><code>.inConnectionLost</code>: This is called when the reactor notices
+ that the process' stdin pipe has closed. Programs don't typically close
+ their own stdin, so this will probably get called when your
+ ProcessProtocol has shut down the write side with <code class="python">self.transport.loseConnection</code>.</li>
+
+ <li><code>.outConnectionLost</code>: This is called when the program closes
+ its stdout pipe. This usually happens when the program terminates.</li>
+
+ <li><code>.errConnectionLost</code>: Same as
+ <code>outConnectionLost</code>, but for stderr instead of stdout.</li>
+
+ <li><code>.processExited(status)</code>: This is called when the child
+ process has been reaped, and receives information about the process' exit
+ status. The status is passed in the form of a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code> instance, created with a
+ <code>.value</code> that either holds a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.error.ProcessDone.html" title="twisted.internet.error.ProcessDone">ProcessDone</a></code> object if the process
+ terminated normally (it died of natural causes instead of receiving a
+ signal, and if the exit code was 0), or a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.error.ProcessTerminated.html" title="twisted.internet.error.ProcessTerminated">ProcessTerminated</a></code> object (with an
+ <code>.exitCode</code> attribute) if something went wrong.</li>
+
+ <li><code>.processEnded(status)</code>: This is called when all the file
+ descriptors associated with the child process have been closed and the
+ process has been reaped. This means it is the last callback which will be
+ made onto a <code>ProcessProtocol</code>. The <code>status</code> parameter
+ has the same meaning as it does for <code>processExited</code>.</li>
+
+</ul>
+
+<p>The base-class definitions of most of these functions are no-ops. This will
+result in all stdout and stderr being thrown away. Note that it is important
+for data you don't care about to be thrown away: if the pipe were not read,
+the child process would eventually block as it tried to write to a full
+pipe.</p>
+
+
+<h2>Things you can do from your ProcessProtocol<a name="auto4"/></h2>
+
+<p>The following are the basic ways to control the child process:</p>
+
+<ul>
+
+ <li><code>self.transport.write(data)</code>: Stuff some data in the stdin
+ pipe. Note that this <code>write</code> method will queue any data that can't
+ be written immediately. Writing will resume in the future when the pipe
+ becomes writable again.</li>
+
+ <li><code>self.transport.closeStdin</code>: Close the stdin pipe. Programs
+ which act as filters (reading from stdin, modifying the data, writing to
+ stdout) usually take this as a sign that they should finish their job and
+ terminate. For these programs, it is important to close stdin when you're
+ done with it, otherwise the child process will never quit.</li>
+
+ <li><code>self.transport.closeStdout</code>: Not usually called, since you're
+ putting the process into a state where any attempt to write to stdout will
+ cause a SIGPIPE error. This isn't a nice thing to do to the poor
+ process.</li>
+
+ <li><code>self.transport.closeStderr</code>: Not usually called, same reason
+ as <code>closeStdout</code>.</li>
+
+ <li><code>self.transport.loseConnection</code>: Close all three pipes.</li>
+
+ <li><code>self.transport.signalProcess('KILL')</code>: Kill the child
+ process. This will eventually result in <code>processEnded</code> being
+ called.</li>
+
+</ul>
+
+
+<h2>Verbose Example<a name="auto5"/></h2>
+
+<p>Here is an example that is rather verbose about exactly when all the
+methods are called. It writes a number of lines into the <code>wc</code>
+program and then parses the output.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">re</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyPP</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ProcessProtocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">verses</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">verses</span> = <span class="py-src-variable">verses</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">data</span> = <span class="py-src-string">&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;connectionMade!&quot;</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">i</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">range</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">verses</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;Aleph-null bottles of beer on the wall,\n&quot;</span> +
+ <span class="py-src-string">&quot;Aleph-null bottles of beer,\n&quot;</span> +
+ <span class="py-src-string">&quot;Take one down and pass it around,\n&quot;</span> +
+ <span class="py-src-string">&quot;Aleph-null bottles of beer on the wall.\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">closeStdin</span>() <span class="py-src-comment"># tell them we're done</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">outReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;outReceived! with %d bytes!&quot;</span> % <span class="py-src-variable">len</span>(<span class="py-src-variable">data</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">data</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">data</span> + <span class="py-src-variable">data</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">errReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;errReceived! with %d bytes!&quot;</span> % <span class="py-src-variable">len</span>(<span class="py-src-variable">data</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">inConnectionLost</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;inConnectionLost! stdin is closed! (we probably did it)&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">outConnectionLost</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;outConnectionLost! The child closed their stdout!&quot;</span>
+ <span class="py-src-comment"># now is the time to examine what they wrote</span>
+ <span class="py-src-comment">#print &quot;I saw them write:&quot;, self.data</span>
+ (<span class="py-src-variable">dummy</span>, <span class="py-src-variable">lines</span>, <span class="py-src-variable">words</span>, <span class="py-src-variable">chars</span>, <span class="py-src-variable">file</span>) = <span class="py-src-variable">re</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">r'\s+'</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">data</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I saw %s lines&quot;</span> % <span class="py-src-variable">lines</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">errConnectionLost</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;errConnectionLost! The child closed their stderr.&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">processExited</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;processExited, status %d&quot;</span> % (<span class="py-src-variable">reason</span>.<span class="py-src-variable">value</span>.<span class="py-src-variable">exitCode</span>,)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">processEnded</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;processEnded, status %d&quot;</span> % (<span class="py-src-variable">reason</span>.<span class="py-src-variable">value</span>.<span class="py-src-variable">exitCode</span>,)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;quitting&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">pp</span> = <span class="py-src-variable">MyPP</span>(<span class="py-src-number">10</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">spawnProcess</span>(<span class="py-src-variable">pp</span>, <span class="py-src-string">&quot;wc&quot;</span>, [<span class="py-src-string">&quot;wc&quot;</span>], {})
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/process/process.py"><span class="filename">listings/process/process.py</span></a></div></div>
+
+<p>The exact output of this program depends upon the relative timing of some
+un-synchronized events. In particular, the program may observe the child
+process close its stderr pipe before or after it reads data from the stdout
+pipe. One possible transcript would look like this:</p>
+
+<pre class="shell" xml:space="preserve">
+% ./process.py
+connectionMade!
+inConnectionLost! stdin is closed! (we probably did it)
+errConnectionLost! The child closed their stderr.
+outReceived! with 24 bytes!
+outConnectionLost! The child closed their stdout!
+I saw 40 lines
+processEnded, status 0
+quitting
+Main loop terminated.
+%
+</pre>
+
+<h2>Doing it the Easy Way<a name="auto6"/></h2>
+
+<p>Frequently, one just needs a simple way to get all the output from a
+program. In the blocking world, you might use <code class="python">commands.getoutput</code> from the standard library, but
+using that in an event-driven program will cause everything else to stall
+until the command finishes. (in addition, the SIGCHLD handler used by that
+function does not play well with Twisted's own signal handling). For these
+cases, the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.utils.getProcessOutput.html" title="twisted.internet.utils.getProcessOutput">twisted.internet.utils.getProcessOutput</a></code>
+function can be used. Here is a simple example:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">utils</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">failure</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">cStringIO</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">StringIO</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FortuneQuoter</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-variable">fortune</span> = <span class="py-src-string">'/usr/games/fortune'</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">output</span> = <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessOutput</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">fortune</span>)
+ <span class="py-src-variable">output</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">writeResponse</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">noResponse</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">resp</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">resp</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">noResponse</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">err</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">Factory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FortuneQuoter</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">10999</span>, <span class="py-src-variable">f</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/process/quotes.py"><span class="filename">listings/process/quotes.py</span></a></div></div>
+
+<p>If you only need the final exit code (like <code class="python">commands.getstatusoutput(cmd)[0]</code>), the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.utils.getProcessValue.html" title="twisted.internet.utils.getProcessValue">twisted.internet.utils.getProcessValue</a></code> function is
+useful. Here is an example:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">utils</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printTrueValue</span>(<span class="py-src-parameter">val</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;/bin/true exits with rc=%d&quot;</span> % <span class="py-src-variable">val</span>
+ <span class="py-src-variable">output</span> = <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessValue</span>(<span class="py-src-string">'/bin/false'</span>)
+ <span class="py-src-variable">output</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printFalseValue</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printFalseValue</span>(<span class="py-src-parameter">val</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;/bin/false exits with rc=%d&quot;</span> % <span class="py-src-variable">val</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">output</span> = <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessValue</span>(<span class="py-src-string">'/bin/true'</span>)
+<span class="py-src-variable">output</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printTrueValue</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/process/trueandfalse.py"><span class="filename">listings/process/trueandfalse.py</span></a></div></div>
+
+<h2>Mapping File Descriptors<a name="auto7"/></h2>
+
+<p><q>stdin</q>, <q>stdout</q>, and <q>stderr</q> are just conventions.
+Programs which operate as filters generally accept input on fd0, write their
+output on fd1, and emit error messages on fd2. This is common enough that
+the standard C library provides macros like <q>stdin</q> to mean fd0, and
+shells interpret the pipe character <q>|</q> to mean <q>redirect fd1 from
+one command into fd0 of the next command</q>.</p>
+
+<p>But these are just conventions, and programs are free to use additional
+file descriptors or even ignore the standard three entirely. The
+<q>childFDs</q> argument allows you to specify exactly what kind of files
+descriptors the child process should be given.</p>
+
+<p>Each child FD can be put into one of three states:</p>
+
+<ul>
+ <li>Mapped to a parent FD: this causes the child's reads and writes to
+ come from or go to the same source/destination as the parent.</li>
+
+ <li>Feeding into a pipe which can be read by the parent.</li>
+
+ <li>Feeding from a pipe which the parent writes into.</li>
+</ul>
+
+<p>Mapping the child FDs to the parent's is very commonly used to send the
+child's stderr output to the same place as the parent's. When you run a
+program from the shell, it will typically leave fds 0, 1, and 2 mapped to
+the shell's 0, 1, and 2, allowing you to see the child program's output on
+the same terminal you used to launch the child. Likewise, inetd will
+typically map both stdin and stdout to the network socket, and may map
+stderr to the same socket or to some kind of logging mechanism. This allows
+the child program to be implemented with no knowledge of the network: it
+merely speaks its protocol by doing reads on fd0 and writes on fd1.</p>
+
+<p>Feeding into a parent's read pipe is used to gather output from the
+child, and is by far the most common way of interacting with child
+processes.</p>
+
+<p>Feeding from a parent's write pipe allows the parent to control the
+child. Programs like <q>bc</q> or <q>ftp</q> can be controlled this way, by
+writing commands into their stdin stream.</p>
+
+<p>The <q>childFDs</q> dictionary maps file descriptor numbers (as will be
+seen by the child process) to one of these three states. To map the fd to
+one of the parent's fds, simply provide the fd number as the value. To map
+it to a read pipe, use the string <q>r</q> as the value. To map it to a
+write pipe, use the string <q>w</q>.</p>
+
+<p>For example, the default mapping sets up the standard stdin/stdout/stderr
+pipes. It is implemented with the following dictionary:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">childFDs</span> = { <span class="py-src-number">0</span>: <span class="py-src-string">&quot;w&quot;</span>, <span class="py-src-number">1</span>: <span class="py-src-string">&quot;r&quot;</span>, <span class="py-src-number">2</span>: <span class="py-src-string">&quot;r&quot;</span> }
+</pre>
+
+<p>To launch a process which reads and writes to the same places that the
+parent python program does, use this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">childFDs</span> = { <span class="py-src-number">0</span>: <span class="py-src-number">0</span>, <span class="py-src-number">1</span>: <span class="py-src-number">1</span>, <span class="py-src-number">2</span>: <span class="py-src-number">2</span>}
+</pre>
+
+<p>To write into an additional fd (say it is fd number 4), use this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">childFDs</span> = { <span class="py-src-number">0</span>: <span class="py-src-string">&quot;w&quot;</span>, <span class="py-src-number">1</span>: <span class="py-src-string">&quot;r&quot;</span>, <span class="py-src-number">2</span>: <span class="py-src-string">&quot;r&quot;</span> , <span class="py-src-number">4</span>: <span class="py-src-string">&quot;w&quot;</span>}
+</pre>
+
+
+
+<h3>ProcessProtocols with extra file descriptors<a name="auto8"/></h3>
+
+<p>When you provide a <q>childFDs</q> dictionary with more than the normal
+three fds, you need addtional methods to access those pipes. These methods
+are more generalized than the <code>.outReceived</code> ones described above.
+In fact, those methods (<code>outReceived</code> and
+ <code>errReceived</code>) are actually just wrappers left in for
+compatibility with older code, written before this generalized fd mapping was
+implemented. The new list of things that can happen to your ProcessProtocol
+is as follows:</p>
+
+<ul>
+
+ <li><code>.connectionMade</code>: This is called when the program is
+ started.</li>
+
+ <li><code>.childDataReceived(childFD, data)</code>: This is called with
+ data that was received from one of the process' output pipes (i.e. where
+ the childFDs value was <q>r</q>. The actual file number (from the point of
+ view of the child process) is in <q>childFD</q>. For compatibility, the
+ default implementation of <code>.childDataReceived</code> dispatches to
+ <code>.outReceived</code> or <code>.errReceived</code> when <q>childFD</q>
+ is 1 or 2.</li>
+
+ <li><code>.childConnectionLost(childFD)</code>: This is called when the
+ reactor notices that one of the process' pipes has been closed. This
+ either means you have just closed down the parent's end of the pipe (with
+ <code>.transport.closeChildFD</code>), the child closed the pipe
+ explicitly (sometimes to indicate EOF), or the child process has
+ terminated and the kernel has closed all of its pipes. The <q>childFD</q>
+ argument tells you which pipe was closed. Note that you can only find out
+ about file descriptors which were mapped to pipes: when they are mapped to
+ existing fds the parent has no way to notice when they've been closed. For
+ compatibility, the default implementation dispatches to
+ <code>.inConnectionLost</code>, <code>.outConnectionLost</code>, or
+ <code>.errConnectionLost</code>.</li>
+
+ <li><code>.processEnded(status)</code>: This is called when the child
+ process has been reaped, and all pipes have been closed. This insures that
+ all data written by the child prior to its death will be received before
+ <code>.processEnded</code> is invoked.</li>
+
+</ul>
+
+
+<p>In addition to those methods, there are other methods available to
+influence the child process:</p>
+
+<ul>
+
+ <li><code>self.transport.writeToChild(childFD, data)</code>: Stuff some
+ data into an input pipe. <code>.write</code> simply writes to
+ childFD=0.</li>
+
+ <li><code>self.transport.closeChildFD(childFD)</code>: Close one of the
+ child's pipes. Closing an input pipe is a common way to indicate EOF to
+ the child process. Closing an output pipe is neither very friendly nor
+ very useful.</li>
+</ul>
+
+<h3>Examples<a name="auto9"/></h3>
+
+<p>GnuPG, the encryption program, can use additional file descriptors to
+accept a passphrase and emit status output. These are distinct from stdin
+(used to accept the crypttext), stdout (used to emit the plaintext), and
+stderr (used to emit human-readable status/warning messages). The passphrase
+FD reads until the pipe is closed and uses the resulting string to unlock
+the secret key that performs the actual decryption. The status FD emits
+machine-parseable status messages to indicate the validity of the signature,
+which key the message was encrypted to, etc.</p>
+
+<p>gpg accepts command-line arguments to specify what these fds are, and
+then assumes that they have been opened by the parent before the gpg process
+is started. It simply performs reads and writes to these fd numbers.</p>
+
+<p>To invoke gpg in decryption/verification mode, you would do something
+like the following:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">GPGProtocol</span>(<span class="py-src-parameter">ProcessProtocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">crypttext</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">crypttext</span> = <span class="py-src-variable">crypttext</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">plaintext</span> = <span class="py-src-string">&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">status</span> = <span class="py-src-string">&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">writeToChild</span>(<span class="py-src-number">3</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">passphrase</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">closeChildFD</span>(<span class="py-src-number">3</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">writeToChild</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">crypttext</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">closeChildFD</span>(<span class="py-src-number">0</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">childDataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">childFD</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">childFD</span> == <span class="py-src-number">1</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">plaintext</span> += <span class="py-src-variable">data</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">childFD</span> == <span class="py-src-number">4</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">status</span> += <span class="py-src-variable">data</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">processEnded</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">rc</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">value</span>.<span class="py-src-variable">exitCode</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">rc</span> == <span class="py-src-number">0</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">rc</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">decrypt</span>(<span class="py-src-parameter">crypttext</span>):
+ <span class="py-src-variable">gp</span> = <span class="py-src-variable">GPGProtocol</span>(<span class="py-src-variable">crypttext</span>)
+ <span class="py-src-variable">gp</span>.<span class="py-src-variable">deferred</span> = <span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">cmd</span> = [<span class="py-src-string">&quot;gpg&quot;</span>, <span class="py-src-string">&quot;--decrypt&quot;</span>, <span class="py-src-string">&quot;--passphrase-fd&quot;</span>, <span class="py-src-string">&quot;3&quot;</span>, <span class="py-src-string">&quot;--status-fd&quot;</span>, <span class="py-src-string">&quot;4&quot;</span>,
+ <span class="py-src-string">&quot;--batch&quot;</span>]
+ <span class="py-src-variable">p</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">spawnProcess</span>(<span class="py-src-variable">gp</span>, <span class="py-src-variable">cmd</span>[<span class="py-src-number">0</span>], <span class="py-src-variable">cmd</span>, <span class="py-src-variable">env</span>=<span class="py-src-variable">None</span>,
+ <span class="py-src-variable">childFDs</span>={<span class="py-src-number">0</span>:<span class="py-src-string">&quot;w&quot;</span>, <span class="py-src-number">1</span>:<span class="py-src-string">&quot;r&quot;</span>, <span class="py-src-number">2</span>:<span class="py-src-number">2</span>, <span class="py-src-number">3</span>:<span class="py-src-string">&quot;w&quot;</span>, <span class="py-src-number">4</span>:<span class="py-src-string">&quot;r&quot;</span>})
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">gp</span>.<span class="py-src-variable">deferred</span>
+</pre>
+
+<p>In this example, the status output could be parsed after the fact. It
+could, of course, be parsed on the fly, as it is a simple line-oriented
+protocol. Methods from LineReceiver could be mixed in to make this parsing
+more convenient.</p>
+
+<p>The stderr mapping (<q>2:2</q>) used will cause any GPG errors to be
+emitted by the parent program, just as if those errors had caused in the
+parent itself. This is sometimes desireable (it roughly corresponds to
+letting exceptions propagate upwards), especially if you do not expect to
+encounter errors in the child process and want them to be more visible to
+the end user. The alternative is to map stderr to a read-pipe and handle any
+such output from within the ProcessProtocol (roughly corresponding to
+catching the exception locally).</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/producers.html b/doc/core/howto/producers.html
new file mode 100644
index 0000000..764ab88
--- /dev/null
+++ b/doc/core/howto/producers.html
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Producers and Consumers: Efficient High-Volume Streaming</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Producers and Consumers: Efficient High-Volume Streaming</h1>
+ <div class="toc"><ol><li><a href="#auto0">Push Producers</a></li><ul><li><a href="#auto1">pauseProducing()</a></li><li><a href="#auto2">resumeProducing()</a></li><li><a href="#auto3">stopProducing()</a></li></ul><li><a href="#auto4">Pull Producers</a></li><ul><li><a href="#auto5">resumeProducing()</a></li><li><a href="#auto6">stopProducing()</a></li></ul><li><a href="#auto7">Consumers</a></li><ul><li><a href="#auto8">registerProducer(producer, streaming)</a></li><li><a href="#auto9">unregisterProducer()</a></li><li><a href="#auto10">write(data)</a></li><li><a href="#auto11">finish()</a></li></ul><li><a href="#auto12">Further Reading</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <p>The purpose of this guide is to describe the Twisted <em>producer</em> and <em>consumer</em> system. The producer system allows applications to stream large amounts of data in a manner which is both memory and CPU efficient, and which does not introduce a source of unacceptable latency into the reactor.</p>
+
+ <p>Readers should have at least a passing familiarity with the terminology associated with interfaces.</p>
+
+ <h2>Push Producers<a name="auto0"/></h2>
+
+ <p>A push producer is one which will continue to generate data without external prompting until told to stop; a pull producer will generate one chunk of data at a time in response to an explicit request for more data.</p>
+
+ <p>The push producer API is defined by the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPushProducer.html" title="twisted.internet.interfaces.IPushProducer">IPushProducer</a></code> interface. It is best to create a push producer when data generation is closedly tied to an event source. For example, a proxy which forwards incoming bytes from one socket to another outgoing socket might be implemented using a push producer: the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocol.dataReceived.html" title="twisted.internet.interfaces.IProtocol.dataReceived">dataReceived</a></code> takes the role of an event source from which the producer generates bytes, and requires no external intervention in order to do so.</p>
+
+ <p>There are three methods which may be invoked on a push producer at various points in its lifetime: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPushProducer.pauseProducing.html" title="twisted.internet.interfaces.IPushProducer.pauseProducing">pauseProducing</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPushProducer.resumeProducing.html" title="twisted.internet.interfaces.IPushProducer.resumeProducing">resumeProducing</a></code>, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProducer.stopProducing.html" title="twisted.internet.interfaces.IProducer.stopProducing">stopProducing</a></code>.</p>
+
+ <h3>pauseProducing()<a name="auto1"/></h3>
+
+ <p>In order to avoid the possibility of using an unbounded amount of memory to buffer produced data which cannot be processed quickly enough, it is necessary to be able to tell a push producer to stop producing data for a while. This is done using the <code class="python">pauseProducing</code> method. Implementers of a push producer should temporarily stop producing data when this method is invoked.</p>
+
+ <h3>resumeProducing()<a name="auto2"/></h3>
+
+ <p>After a push producer has been paused for some time, the excess of data which it produced will have been processed and the producer may again begin producing data. When the time for this comes, the push producer will have <code class="python">resumeProducing</code> invoked on it.</p>
+
+ <h3>stopProducing()<a name="auto3"/></h3>
+
+ <p>Most producers will generate some finite (albeit, perhaps, unknown in advance) amount of data and then stop, having served their intended purpose. However, it is possible that before this happens an event will occur which renders the remaining, unproduced data irrelevant. In these cases, producing it anyway would be wasteful. The <code class="python">stopProducing</code> method will be invoked on the push producer. The implementation should stop producing data and clean up any resources owned by the producer.</p>
+
+ <h2>Pull Producers<a name="auto4"/></h2>
+
+ <p>The pull producer API is defined by the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPullProducer.html" title="twisted.internet.interfaces.IPullProducer">IPullProducer</a></code> interface. Pull producers are useful in cases where there is no clear event source involved with the generation of data. For example, if the data is the result of some algorithmic process that is bound only by CPU time, a pull producer is appropriate.</p>
+
+ <p>Pull producers are defined in terms of only two methods: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPullProducer.resumeProducing.html" title="twisted.internet.interfaces.IPullProducer.resumeProducing">resumeProducing</a></code> and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProducer.stopProducing.html" title="twisted.internet.interfaces.IProducer.stopProducing">stopProducing</a></code>.</p>
+
+ <h3>resumeProducing()<a name="auto5"/></h3>
+
+ <p>Unlike push producers, a pull producer is expected to <strong>only</strong> produce data in response to <code class="python">resumeProducing</code> being called. This method will be called whenever more data is required. How much data to produce in response to this method call depends on various factors: too little data and runtime costs will be dominated by the back-and-forth event notification associated with a buffer becoming empty and requesting more data to process; too much data and memory usage will be driven higher than it needs to be and the latency associated with creating so much data will cause overall performance in the application to suffer. A good rule of thumb is to generate between 16 and 64 kilobytes of data at a time, but you should experiment with various values to determine what is best for your application.</p>
+
+ <h3>stopProducing()<a name="auto6"/></h3>
+
+ <p>This method has the same meaning for pull producers as it does for push producers.</p>
+
+ <h2>Consumers<a name="auto7"/></h2>
+
+ <p>This far, I've discussed the various external APIs of the two kinds of producers supported by Twisted. However, I have not mentioned where the data a producer generates actually goes, nor what entity is responsible for invoking these APIs. Both of these roles are filled by <em>consumers</em>. Consumers are defined by the two interfaces <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConsumer.html" title="twisted.internet.interfaces.IConsumer">IConsumer</a></code> and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IFinishableConsumer.html" title="twisted.internet.interfaces.IFinishableConsumer">IFinishableConsumer</a></code>.</p>
+
+ <p>The slightly simpler of these two interfaces, <code class="python">IConsumer</code>, defines three methods: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConsumer.registerProducer.html" title="twisted.internet.interfaces.IConsumer.registerProducer">registerProducer</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConsumer.unregisterProducer.html" title="twisted.internet.interfaces.IConsumer.unregisterProducer">unregisterProducer</a></code>, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConsumer.write.html" title="twisted.internet.interfaces.IConsumer.write">write</a></code>. <code class="python">IFinishableConsumer</code> adds <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IFinishableConsumer.finish.html" title="twisted.internet.interfaces.IFinishableConsumer.finish">finish</a></code>.</p>
+
+ <h3>registerProducer(producer, streaming)<a name="auto8"/></h3>
+
+ <p>So that a consumer can invoke methods on a producer, the consumer needs to be told about the producer. This is done with the <code class="python">registerProducer</code> method. The first argument is either a <code class="python">IPullProducer</code> or <code class="python">IPushProducer</code> provider; the second argument indicates which of these interfaces is provided: <code class="python">True</code> for push producers, <code class="python">False</code> for pull producers.</p>
+
+ <h3>unregisterProducer()<a name="auto9"/></h3>
+
+ <p>Eventually a consumer will not longer be interested in a producer. This could be because the producer has finished generating all its data, or because the consumer is moving on to something else, or any number of other reasons. In any case, this method reverses the effects of <code class="python">registerProducer</code>.</p>
+
+ <h3>write(data)<a name="auto10"/></h3>
+
+ <p>As you might guess, this is the method which a producer calls when it has generated some data. Push producers should call it as frequently as they like as long as they are not paused. Pull producers should call it once for each time <code class="python">resumeProducing</code> is called on them.</p>
+
+ <h3>finish()<a name="auto11"/></h3>
+
+ <p>This method of <code class="python">IFinishableConsumer</code>s gives producers a way to explicitly notify the consumer that they have generated all the data they will ever generate.</p>
+
+ <h2>Further Reading<a name="auto12"/></h2>
+
+ <p>An example push producer application can be found in <code class="py-filename">doc/examples/streaming.py</code>.</p>
+
+ <ul>
+
+ <li><a href="components.html" shape="rect">Components: Interfaces and Adapters</a></li>
+
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.basic.FileSender.html" title="twisted.protocols.basic.FileSender">FileSender</a></code>: A Simple Pull Producer</li>
+
+ </ul>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/quotes.html b/doc/core/howto/quotes.html
new file mode 100644
index 0000000..e72936c
--- /dev/null
+++ b/doc/core/howto/quotes.html
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Setting up the TwistedQuotes application</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Setting up the TwistedQuotes application</h1>
+ <div class="toc"><ol><li><a href="#auto0">Goal</a></li><li><a href="#auto1">Setting up the TwistedQuotes project directory</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Goal<a name="auto0"/></h2>
+
+<p>This document describes how to set up the TwistedQuotes application used in
+a number of other documents, such as <a href="design.html" shape="rect">designing Twisted applications</a>.</p>
+
+<h2>Setting up the TwistedQuotes project directory<a name="auto1"/></h2>
+
+<p>In order to run the Twisted Quotes example, you will need to do the
+following:</p>
+
+<ol>
+<li>Make a <code>TwistedQuotes</code> directory on your system</li>
+<li>Place the following files in the <code>TwistedQuotes</code> directory:
+ <ul>
+ <li><div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-string">&quot;&quot;&quot;
+Twisted Quotes
+&quot;&quot;&quot;</span>
+</pre><div class="caption">Source listing - <a href="listings/TwistedQuotes/__init__.py"><span class="filename">listings/TwistedQuotes/__init__.py</span></a></div></div> (this
+ file marks it as a package, see <a href="http://docs.python.org/tutorial/modules.html#packages" shape="rect">this section</a> of the Python tutorial for more on packages)</li>
+ <li><div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">random</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">choice</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">TwistedQuotes</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">quoteproto</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">StaticQuoter</span>:
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a static quote.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">quoteproto</span>.<span class="py-src-variable">IQuoter</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">quote</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">quote</span> = <span class="py-src-variable">quote</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">quote</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FortuneQuoter</span>:
+ <span class="py-src-string">&quot;&quot;&quot;
+ Load quotes from a fortune-format file.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">quoteproto</span>.<span class="py-src-variable">IQuoter</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filenames</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filenames</span> = <span class="py-src-variable">filenames</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">quoteFile</span> = <span class="py-src-variable">file</span>(<span class="py-src-variable">choice</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filenames</span>))
+ <span class="py-src-variable">quotes</span> = <span class="py-src-variable">quoteFile</span>.<span class="py-src-variable">read</span>().<span class="py-src-variable">split</span>(<span class="py-src-string">'\n%\n'</span>)
+ <span class="py-src-variable">quoteFile</span>.<span class="py-src-variable">close</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">choice</span>(<span class="py-src-variable">quotes</span>)
+</pre><div class="caption">Source listing - <a href="listings/TwistedQuotes/quoters.py"><span class="filename">listings/TwistedQuotes/quoters.py</span></a></div></div></li>
+ <li><div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IQuoter</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An object that returns quotes.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a quote.
+ &quot;&quot;&quot;</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTD</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">quoter</span>.<span class="py-src-variable">getQuote</span>()+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTDFactory</span>(<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ A factory for the Quote of the Day protocol.
+
+ @type quoter: L{IQuoter} provider
+ @ivar quoter: An object which provides L{IQuoter} which will be used by
+ the L{QOTD} protocol to get quotes to emit.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">QOTD</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">quoter</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">quoter</span> = <span class="py-src-variable">quoter</span>
+</pre><div class="caption">Source listing - <a href="listings/TwistedQuotes/quoteproto.py"><span class="filename">listings/TwistedQuotes/quoteproto.py</span></a></div></div></li>
+ </ul>
+</li>
+<li>Add the <code>TwistedQuotes</code> directory's <em>parent</em> to your Python
+path. For example, if the TwistedQuotes directory's path is
+ <code>/mystuff/TwistedQuotes</code> or <code>c:\mystuff\TwistedQuotes</code>
+add <code>/mystuff</code> to your Python path. On UNIX this would be <code class="shell">export PYTHONPATH=/mystuff:$PYTHONPATH</code>, on Microsoft
+Windows change the <code class="shell">PYTHONPATH</code> variable through the
+Systems Properties dialog by adding <code class="shell">;c:\mystuff</code> at the
+end.</li>
+<li>
+Test your package by trying to import it in the Python interpreter:
+<pre class="python-interpreter" xml:space="preserve">
+Python 2.1.3 (#1, Apr 20 2002, 22:45:31)
+[GCC 2.95.4 20011002 (Debian prerelease)] on linux2
+Type &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
+&gt;&gt;&gt; import TwistedQuotes
+&gt;&gt;&gt; # No traceback means you're fine.
+</pre>
+</li>
+</ol>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/rdbms.html b/doc/core/howto/rdbms.html
new file mode 100644
index 0000000..b1c053d
--- /dev/null
+++ b/doc/core/howto/rdbms.html
@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: twisted.enterprise.adbapi: Twisted RDBMS support</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">twisted.enterprise.adbapi: Twisted RDBMS support</h1>
+ <div class="toc"><ol><li><a href="#auto0">Abstract</a></li><li><a href="#auto1">What you should already know</a></li><li><a href="#auto2">Quick Overview</a></li><li><a href="#auto3">How do I use adbapi?</a></li><li><a href="#auto4">Examples of various database adapters</a></li><li><a href="#auto5">And that's it!</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Abstract<a name="auto0"/></h2>
+
+ <p>Twisted is an asynchronous networking framework, but most
+ database API implementations unfortunately have blocking
+ interfaces -- for this reason, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.html" title="twisted.enterprise.adbapi">twisted.enterprise.adbapi</a></code> was created. It is
+ a non-blocking interface to the standardized DB-API 2.0 API,
+ which allows you to access a number of different RDBMSes.</p>
+
+ <h2>What you should already know<a name="auto1"/></h2>
+
+ <ul>
+ <li>Python :-)</li>
+
+ <li>How to write a simple Twisted Server (see <a href="servers.html" shape="rect">this tutorial</a> to learn how)</li>
+
+ <li>Familiarity with using database interfaces (see <a href="http://www.python.org/dev/peps/pep-0249/" shape="rect">
+ the documentation for DBAPI 2.0</a> or this <a href="http://www.amk.ca/python/writing/DB-API.html" shape="rect">article</a>
+ by Andrew Kuchling)</li>
+ </ul>
+
+ <h2>Quick Overview<a name="auto2"/></h2>
+
+ <p>Twisted is an asynchronous framework. This means standard
+ database modules cannot be used directly, as they typically
+ work something like:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-comment"># Create connection... </span>
+<span class="py-src-variable">db</span> = <span class="py-src-variable">dbmodule</span>.<span class="py-src-variable">connect</span>(<span class="py-src-string">'mydb'</span>, <span class="py-src-string">'andrew'</span>, <span class="py-src-string">'password'</span>)
+<span class="py-src-comment"># ...which blocks for an unknown amount of time </span>
+
+<span class="py-src-comment"># Create a cursor </span>
+<span class="py-src-variable">cursor</span> = <span class="py-src-variable">db</span>.<span class="py-src-variable">cursor</span>()
+
+<span class="py-src-comment"># Do a query... </span>
+<span class="py-src-variable">resultset</span> = <span class="py-src-variable">cursor</span>.<span class="py-src-variable">query</span>(<span class="py-src-string">'SELECT * FROM table WHERE ...'</span>)
+<span class="py-src-comment"># ...which could take a long time, perhaps even minutes.</span>
+</pre>
+
+ <p>Those delays are unacceptable when using an asynchronous
+ framework such as Twisted. For this reason, twisted provides
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.html" title="twisted.enterprise.adbapi">twisted.enterprise.adbapi</a></code>, an
+ asynchronous wrapper for any <a href="http://www.python.org/dev/peps/pep-0249/" shape="rect">
+ DB-API 2.0</a>-compliant module.</p>
+
+ <p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.html" title="twisted.enterprise.adbapi">enterprise.adbapi</a></code> will do
+ blocking
+ database operations in separate threads, which trigger
+ callbacks in the originating thread when they complete. In the
+ meantime, the original thread can continue doing normal work,
+ like servicing other requests.</p>
+
+ <h2>How do I use adbapi?<a name="auto3"/></h2>
+
+ <p>Rather than creating a database connection directly, use the
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.ConnectionPool.html" title="twisted.enterprise.adbapi.ConnectionPool">adbapi.ConnectionPool</a></code>
+ class to manage
+ a connections for you. This allows <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.html" title="twisted.enterprise.adbapi">enterprise.adbapi</a></code> to use multiple
+ connections, one per thread. This is easy:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-comment"># Using the &quot;dbmodule&quot; from the previous example, create a ConnectionPool </span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">enterprise</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">adbapi</span>
+<span class="py-src-variable">dbpool</span> = <span class="py-src-variable">adbapi</span>.<span class="py-src-variable">ConnectionPool</span>(<span class="py-src-string">&quot;dbmodule&quot;</span>, <span class="py-src-string">'mydb'</span>, <span class="py-src-string">'andrew'</span>, <span class="py-src-string">'password'</span>)
+</pre>
+
+ <p>Things to note about doing this:</p>
+
+ <ul>
+ <li>There is no need to import dbmodule directly. You just
+ pass the name to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.ConnectionPool.html" title="twisted.enterprise.adbapi.ConnectionPool">adbapi.ConnectionPool</a></code>'s constructor.</li>
+
+ <li>The parameters you would pass to dbmodule.connect are
+ passed as extra arguments to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.ConnectionPool.html" title="twisted.enterprise.adbapi.ConnectionPool">adbapi.ConnectionPool</a></code>'s constructor.
+ Keyword parameters work as well.</li>
+ </ul>
+
+ <p>Now we can do a database query:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-comment"># equivalent of cursor.execute(statement), return cursor.fetchall():</span>
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getAge</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">dbpool</span>.<span class="py-src-variable">runQuery</span>(<span class="py-src-string">&quot;SELECT age FROM users WHERE name = ?&quot;</span>, <span class="py-src-variable">user</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">l</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">l</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">l</span>[<span class="py-src-number">0</span>][<span class="py-src-number">0</span>], <span class="py-src-string">&quot;years old&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;No such user&quot;</span>
+
+<span class="py-src-variable">getAge</span>(<span class="py-src-string">&quot;joe&quot;</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+</pre>
+
+ <p>This is straightforward, except perhaps for the return value
+ of <code>getAge</code>. It returns a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">twisted.internet.defer.Deferred</a></code>, which allows
+ arbitrary callbacks to be called upon completion (or upon
+ failure). More documentation on Deferred is available <a href="defer.html" shape="rect">here</a>.</p>
+
+ <p>In addition to <code>runQuery</code>, there is also <code>runOperation</code>,
+ and <code>runInteraction</code> that gets called with a callable (e.g. a function).
+ The function will be called in the thread with a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.enterprise.adbapi.Transaction.html" title="twisted.enterprise.adbapi.Transaction">twisted.enterprise.adbapi.Transaction</a></code>,
+ which basically mimics a DB-API cursor. In all cases a database transaction will be
+ commited after your database usage is finished, unless an exception is raised in
+ which case it will be rolled back.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">_getAge</span>(<span class="py-src-parameter">txn</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-comment"># this will run in a thread, we can use blocking calls</span>
+ <span class="py-src-variable">txn</span>.<span class="py-src-variable">execute</span>(<span class="py-src-string">&quot;SELECT * FROM foo&quot;</span>)
+ <span class="py-src-comment"># ... other cursor commands called on txn ...</span>
+ <span class="py-src-variable">txn</span>.<span class="py-src-variable">execute</span>(<span class="py-src-string">&quot;SELECT age FROM users WHERE name = ?&quot;</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">txn</span>.<span class="py-src-variable">fetchall</span>()
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">result</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">result</span>[<span class="py-src-number">0</span>][<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">None</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getAge</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">dbpool</span>.<span class="py-src-variable">runInteraction</span>(<span class="py-src-variable">_getAge</span>, <span class="py-src-variable">user</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">age</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">age</span> != <span class="py-src-variable">None</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">age</span>, <span class="py-src-string">&quot;years old&quot;</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;No such user&quot;</span>
+
+<span class="py-src-variable">getAge</span>(<span class="py-src-string">&quot;joe&quot;</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+</pre>
+
+ <p>Also worth noting is that these examples assumes that dbmodule
+ uses the <q>qmarks</q> paramstyle (see the DB-API specification). If
+ your dbmodule uses a different paramstyle (e.g. pyformat) then
+ use that. Twisted doesn't attempt to offer any sort of magic
+ paramater munging -- <code class="python">runQuery(query,
+ params, ...)</code> maps directly onto <code class="python">cursor.execute(query, params, ...)</code>.</p>
+
+ <h2>Examples of various database adapters<a name="auto4"/></h2>
+
+ <p>Notice that the first argument is the module name you would
+ usually import and get <code class="python">connect(...)</code>
+ from, and that following arguments are whatever arguments you'd
+ call <code class="python">connect(...)</code> with.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">enterprise</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">adbapi</span>
+
+<span class="py-src-comment"># Gadfly</span>
+<span class="py-src-variable">cp</span> = <span class="py-src-variable">adbapi</span>.<span class="py-src-variable">ConnectionPool</span>(<span class="py-src-string">&quot;gadfly&quot;</span>, <span class="py-src-string">&quot;test&quot;</span>, <span class="py-src-string">&quot;/tmp/gadflyDB&quot;</span>)
+
+<span class="py-src-comment"># PostgreSQL PyPgSQL</span>
+<span class="py-src-variable">cp</span> = <span class="py-src-variable">adbapi</span>.<span class="py-src-variable">ConnectionPool</span>(<span class="py-src-string">&quot;pyPgSQL.PgSQL&quot;</span>, <span class="py-src-variable">database</span>=<span class="py-src-string">&quot;test&quot;</span>)
+
+<span class="py-src-comment"># MySQL</span>
+<span class="py-src-variable">cp</span> = <span class="py-src-variable">adbapi</span>.<span class="py-src-variable">ConnectionPool</span>(<span class="py-src-string">&quot;MySQLdb&quot;</span>, <span class="py-src-variable">db</span>=<span class="py-src-string">&quot;test&quot;</span>)
+</pre>
+
+ <h2>And that's it!<a name="auto5"/></h2>
+
+ <p>That's all you need to know to use a database from within
+ Twisted. You probably should read the adbapi module's
+ documentation to get an idea of the other functions it has, but
+ hopefully this document presents the core ideas.</p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/reactor-basics.html b/doc/core/howto/reactor-basics.html
new file mode 100644
index 0000000..a750f78
--- /dev/null
+++ b/doc/core/howto/reactor-basics.html
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Reactor Overview</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Reactor Overview</h1>
+ <div class="toc"><ol><li><a href="#auto0">Reactor Basics</a></li><li><a href="#auto1">Using the reactor object</a></li></ol></div>
+ <div class="content">
+
+ <span/>
+
+ <p>
+ This HOWTO introduces the Twisted reactor, describes the basics of the
+ reactor and links to the various reactor interfaces.
+ </p>
+
+ <h2>Reactor Basics<a name="auto0"/></h2>
+
+ <p>The reactor is the core of the event loop within Twisted -- the loop
+ which drives applications using Twisted. The event loop is a programming
+ construct that waits for and dispatches events or messages in a program.
+ It works by calling some internal or external &quot;event provider&quot;, which
+ generally blocks until an event has arrived, and then calls the relevant
+ event handler (&quot;dispatches the event&quot;). The reactor provides basic
+ interfaces to a number of services, including network communications,
+ threading, and event dispatching.
+ </p>
+
+ <p>
+ For information about using the reactor and the Twisted event loop, see:
+ </p>
+
+ <ul>
+ <li>the event dispatching howtos: <a href="time.html" shape="rect">Scheduling</a> and <a href="defer.html" shape="rect">Using Deferreds</a>;</li>
+ <li>the communication howtos: <a href="servers.html" shape="rect">TCP
+ servers</a>, <a href="clients.html" shape="rect">TCP clients</a>, <a href="udp.html" shape="rect">UDP networking</a> and <a href="process.html" shape="rect">Using
+ processes</a>; and</li>
+ <li><a href="threading.html" shape="rect">Using threads</a>.</li>
+ </ul>
+
+ <p>There are multiple implementations of the reactor, each
+ modified to provide better support for specialized features
+ over the default implementation. More information about these
+ and how to use a particular implementation is available via
+ <a href="choosing-reactor.html" shape="rect">Choosing a Reactor</a>.</p>
+
+
+ <p>
+ Twisted applications can use the interfaces in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.html" title="twisted.application.service">twisted.application.service</a></code> to configure and run the
+ application instead of using
+ boilerplate reactor code. See <a href="application.html" shape="rect">Using Application</a> for an introduction to
+ Application.
+ </p>
+
+ <h2>Using the reactor object<a name="auto1"/></h2>
+
+ <p>You can get to the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code> object using the following code:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+ <p>The reactor usually implements a set of interfaces, but
+ depending on the chosen reactor and the platform, some of
+ the interfaces may not be implemented:</p>
+
+ <ul>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorCore.html" title="twisted.internet.interfaces.IReactorCore">IReactorCore</a></code>: Core (required) functionality.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorFDSet.html" title="twisted.internet.interfaces.IReactorFDSet">IReactorFDSet</a></code>: Use FileDescriptor objects.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorProcess.html" title="twisted.internet.interfaces.IReactorProcess">IReactorProcess</a></code>: Process management. Read the
+ <a href="process.html" shape="rect">Using Processes</a> document for
+ more information.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.html" title="twisted.internet.interfaces.IReactorSSL">IReactorSSL</a></code>: SSL networking support.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.html" title="twisted.internet.interfaces.IReactorTCP">IReactorTCP</a></code>: TCP networking support. More information
+ can be found in the <a href="servers.html" shape="rect">Writing Servers</a>
+ and <a href="clients.html" shape="rect">Writing Clients</a> documents.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorThreads.html" title="twisted.internet.interfaces.IReactorThreads">IReactorThreads</a></code>: Threading use and management. More
+ information can be found within <a href="threading.html" shape="rect">Threading In Twisted</a>.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTime.html" title="twisted.internet.interfaces.IReactorTime">IReactorTime</a></code>: Scheduling interface. More information
+ can be found within <a href="time.html" shape="rect">Scheduling Tasks</a>.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUDP.html" title="twisted.internet.interfaces.IReactorUDP">IReactorUDP</a></code>: UDP networking support. More information
+ can be found within <a href="udp.html" shape="rect">UDP Networking</a>.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUNIX.html" title="twisted.internet.interfaces.IReactorUNIX">IReactorUNIX</a></code>: UNIX socket support.</li>
+ <li><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSocket.html" title="twisted.internet.interfaces.IReactorSocket">IReactorSocket</a></code>: Third-party socket support.</li>
+ </ul>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/sendmsg.html b/doc/core/howto/sendmsg.html
new file mode 100644
index 0000000..7570c26
--- /dev/null
+++ b/doc/core/howto/sendmsg.html
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Extremely Low-Level Socket Operations</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Extremely Low-Level Socket Operations</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><ul><li><a href="#auto1">sendmsg</a></li><li><a href="#auto2">recvmsg</a></li></ul><li><a href="#auto3">Sending And Receiving Regular Data</a></li><li><a href="#auto4">Copying File Descriptors</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Introduction<a name="auto0"/></h2>
+
+ <p>
+ Beyond supporting streams of data (SOCK_STREAM) or datagrams (SOCK_DGRAM),
+ POSIX sockets have additional features not accessible via send(2) and
+ recv(2). These features include things like scatter/gather I/O,
+ duplicating file descriptors into other processes, and accessing
+ out-of-band data.
+ </p>
+
+ <p>
+ Twisted includes a wrapper around the two C APIs which make these things
+ possible,
+ <a href="http://www.opengroup.org/onlinepubs/007908799/xns/sendmsg.html" shape="rect">sendmsg</a>
+ and
+ <a href="http://www.opengroup.org/onlinepubs/007908799/xns/recvmsg.html" shape="rect">recvmsg</a>.
+ This document covers their usage. It is intended for Twisted maintainers.
+ Application developers looking for this functionality should look for the
+ high-level APIs Twisted provides on top of these wrappers.
+ </p>
+
+ <h3>sendmsg<a name="auto1"/></h3>
+
+ <p>
+ <code>sendmsg(2)</code> exposes nearly all sender-side functionality of a
+ socket. For a SOCK_STREAM socket, it can send bytes that become part of
+ the stream of data being carried over the connection. For a SOCK_DGRAM
+ socket, it can send bytes that become datagrams sent from the socket. It
+ can send data from multiple memory locations (gather I/O). Over AF_UNIX
+ sockets, it can copy file descriptors into whichever process is receiving
+ on the other side. The wrapper included in Twisted,
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.sendmsg.send1msg.html" title="twisted.python.sendmsg.send1msg">send1msg</a></code>, exposes
+ many (but not all) of these features. This document covers the usage of
+ the features it does expose. The alternate spelling for the wrapper is
+ used to indicate the primary limitation, which is it that the interface
+ supports sending only one <em>iovec</em> at a time.
+ </p>
+
+ <h3>recvmsg<a name="auto2"/></h3>
+
+ <p>
+ Likewise, <code>recvmsg(2)</code> exposes nearly all the receiver-side
+ functionality of a socket. It can receive stream data over from a
+ SOCK_STREAM socket or datagrams from a SOCK_DGRAM socket. It can receive
+ that data into multiple memory locations (scatter I/O), and it can receive
+ those copied file descriptors. The wrapper included in
+ Twisted, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.sendmsg.recv1msg.html" title="twisted.python.sendmsg.recv1msg">recv1msg</a></code>,
+ exposes many (but not all) of these features. This document covers the
+ usage of the features it does expose. The alternate spelling for the
+ wrapper is used to indicate the primary limitation, which is that the
+ interface supports receiving only one <em>iovec</em> at a time.
+ </p>
+
+ <h2>Sending And Receiving Regular Data<a name="auto3"/></h2>
+
+ <p>
+ sendmsg can be used in a way which makes it equivalent to using the send
+ call. The first argument to sendmsg is (in this case and all others) a
+ file descriptor over which to send the data. The second argument is a
+ string giving the data to send.
+ </p>
+
+ <p>
+ On the other end, recvmsg can be used to replace a recv call. The first
+ argument to recvmsg is (again, in all cases) a file descriptor over which
+ to receive the data. The second argument is an integer giving the maximum
+ number of data to receive.
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+Demonstration of sending bytes over a TCP connection using sendmsg.
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">socket</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">socketpair</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">sendmsg</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">send1msg</span>, <span class="py-src-variable">recv1msg</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">foo</span>, <span class="py-src-variable">bar</span> = <span class="py-src-variable">socketpair</span>()
+ <span class="py-src-variable">sent</span> = <span class="py-src-variable">send1msg</span>(<span class="py-src-variable">foo</span>.<span class="py-src-variable">fileno</span>(), <span class="py-src-string">&quot;Hello, world&quot;</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Sent&quot;</span>, <span class="py-src-variable">sent</span>, <span class="py-src-string">&quot;bytes&quot;</span>
+ (<span class="py-src-variable">received</span>, <span class="py-src-variable">flags</span>, <span class="py-src-variable">ancillary</span>) = <span class="py-src-variable">recv1msg</span>(<span class="py-src-variable">bar</span>.<span class="py-src-variable">fileno</span>(), <span class="py-src-number">1024</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Received&quot;</span>, <span class="py-src-variable">repr</span>(<span class="py-src-variable">received</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Extra stuff, boring in this case&quot;</span>, <span class="py-src-variable">flags</span>, <span class="py-src-variable">ancillary</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption"> - <a href="listings/sendmsg/send_replacement.py"><span class="filename">listings/sendmsg/send_replacement.py</span></a></div></div>
+
+ <h2>Copying File Descriptors<a name="auto4"/></h2>
+
+ <p>
+ Used with an AF_UNIX socket, sendmsg send a copy of a file descriptor into
+ whatever process is receiving on the other end of the socket. This is
+ done using the ancillary data argument. Ancillary data consists of a list
+ of three-tuples. A three-tuple constructed with SOL_SOCKET, SCM_RIGHTS,
+ and a platform-endian packed file descriptor number will copy that file
+ descriptor.
+ </p>
+
+ <p>
+ File descriptors copied this way must be received using a recvmsg call.
+ No special arguments are required to receive these descriptors. They will
+ appear, encoded as a native-order string, in the ancillary data list
+ returned by recvmsg.
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+</p><span class="py-src-comment"># Copyright (c) Twisted Matrix Laboratories.</span>
+<span class="py-src-comment"># See LICENSE for details.</span>
+
+<span class="py-src-string">&quot;&quot;&quot;
+Demonstration of copying a file descriptor over an AF_UNIX connection using
+sendmsg.
+&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">os</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pipe</span>, <span class="py-src-variable">read</span>, <span class="py-src-variable">write</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">socket</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SOL_SOCKET</span>, <span class="py-src-variable">socketpair</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">struct</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unpack</span>, <span class="py-src-variable">pack</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">sendmsg</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SCM_RIGHTS</span>, <span class="py-src-variable">send1msg</span>, <span class="py-src-variable">recv1msg</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">foo</span>, <span class="py-src-variable">bar</span> = <span class="py-src-variable">socketpair</span>()
+ <span class="py-src-variable">reader</span>, <span class="py-src-variable">writer</span> = <span class="py-src-variable">pipe</span>()
+
+ <span class="py-src-comment"># Send a copy of the descriptor. Notice that there must be at least one</span>
+ <span class="py-src-comment"># byte of normal data passed in.</span>
+ <span class="py-src-variable">sent</span> = <span class="py-src-variable">send1msg</span>(
+ <span class="py-src-variable">foo</span>.<span class="py-src-variable">fileno</span>(), <span class="py-src-string">&quot;\x00&quot;</span>, <span class="py-src-number">0</span>,
+ [(<span class="py-src-variable">SOL_SOCKET</span>, <span class="py-src-variable">SCM_RIGHTS</span>, <span class="py-src-variable">pack</span>(<span class="py-src-string">&quot;i&quot;</span>, <span class="py-src-variable">reader</span>))])
+
+ <span class="py-src-comment"># Receive the copy, including that one byte of normal data.</span>
+ <span class="py-src-variable">data</span>, <span class="py-src-variable">flags</span>, <span class="py-src-variable">ancillary</span> = <span class="py-src-variable">recv1msg</span>(<span class="py-src-variable">bar</span>.<span class="py-src-variable">fileno</span>(), <span class="py-src-number">1024</span>)
+ <span class="py-src-variable">duplicate</span> = <span class="py-src-variable">unpack</span>(<span class="py-src-string">&quot;i&quot;</span>, <span class="py-src-variable">ancillary</span>[<span class="py-src-number">0</span>][<span class="py-src-number">2</span>])[<span class="py-src-number">0</span>]
+
+ <span class="py-src-comment"># Demonstrate that the copy works just like the original</span>
+ <span class="py-src-variable">write</span>(<span class="py-src-variable">writer</span>, <span class="py-src-string">&quot;Hello, world&quot;</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Read from original (%d): %r&quot;</span> % (<span class="py-src-variable">reader</span>, <span class="py-src-variable">read</span>(<span class="py-src-variable">reader</span>, <span class="py-src-number">6</span>))
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Read from duplicate (%d): %r&quot;</span> % (<span class="py-src-variable">duplicate</span>, <span class="py-src-variable">read</span>(<span class="py-src-variable">duplicate</span>, <span class="py-src-number">6</span>))
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption"> - <a href="listings/sendmsg/copy_descriptor.py"><span class="filename">listings/sendmsg/copy_descriptor.py</span></a></div></div>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/servers.html b/doc/core/howto/servers.html
new file mode 100644
index 0000000..232a574
--- /dev/null
+++ b/doc/core/howto/servers.html
@@ -0,0 +1,548 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Writing Servers</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Writing Servers</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Protocols</a></li><ul><li><a href="#auto2">loseConnection() and abortConnection()</a></li><li><a href="#auto3">Using the Protocol</a></li><li><a href="#auto4">Helper Protocols</a></li><li><a href="#auto5">State Machines</a></li></ul><li><a href="#auto6">Factories</a></li><ul><li><a href="#auto7">Simpler Protocol Creation</a></li><li><a href="#auto8">Factory Startup and Shutdown</a></li></ul><li><a href="#auto9">Putting it All Together</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>This document explains how you can use Twisted to implement
+ network protocol parsing and handling for TCP servers (the same
+ code can be reused for SSL and Unix socket servers). There is
+ a <a href="udp.html" shape="rect">separate document</a> covering UDP.</p>
+
+ <p>Your protocol handling class will usually subclass <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Protocol.html" title="twisted.internet.protocol.Protocol">twisted.internet.protocol.Protocol</a></code>. Most
+ protocol handlers inherit either from this class or from one of
+ its convenience children. An instance of the protocol class
+ is instantiated per-connection, on demand, and will go
+ away when the connection is finished. This means that
+ persistent configuration is not saved in the
+ <code>Protocol</code>.</p>
+
+ <p>The persistent configuration is kept in a <code>Factory</code>
+ class, which usually inherits
+ from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.Factory.html" title="twisted.internet.protocol.Factory">twisted.internet.protocol.Factory</a></code>. The <code>buildProtocol</code>
+ method of the <code>Factory</code> is used to create
+ a <code>Protocol</code> for each new connection.</p>
+
+ <p>It is usually useful to be able to offer the same service on
+ multiple ports or network addresses. This is why
+ the <code>Factory</code> does not listen to connections, and in
+ fact does not know anything about the
+ network. See <a href="endpoints.html" shape="rect">the endpoints
+ documentation</a> for more information,
+ or <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.listenTCP.html" title="twisted.internet.interfaces.IReactorTCP.listenTCP">twisted.internet.interfaces.IReactorTCP.listenTCP</a></code>,
+ and the other <code>IReactor*.listen*</code> APIs for the lower
+ level APIs that endpoints are based on.</p>
+
+ <p>This document will explain each step of the way.</p>
+
+ <h2>Protocols<a name="auto1"/></h2>
+
+ <p>As mentioned above, this, along with auxiliary classes and
+ functions, is where most of the code is. A Twisted protocol
+ handles data in an asynchronous manner: the protocol responds
+ to events as they arrive from the network; the events arrive as
+ calls to methods on the protocol.</p>
+
+ <p>Here is a simple example:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+</pre>
+
+ <p>This is one of the simplest protocols. It simply writes back
+ whatever is written to it, and does not respond to all events. Here is an
+ example of a Protocol responding to another event:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTD</span>(<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;An apple a day keeps the doctor away\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+</pre>
+
+ <p>This protocol responds to the initial connection with a well
+ known quote, and then terminates the connection.</p>
+
+ <p>The connectionMade event is usually where set up of the
+ connection object happens, as well as any initial greetings (as
+ in the QOTD protocol above, which is actually based on RFC
+ 865). The <code>connectionLost</code> event is where tearing down of any
+ connection-specific objects is done. Here is an example:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">factory</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span> = <span class="py-src-variable">factory</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">numProtocols</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">numProtocols</span>+<span class="py-src-number">1</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(
+ <span class="py-src-string">&quot;Welcome! There are currently %d open connections.\n&quot;</span> %
+ (<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">numProtocols</span>,))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">numProtocols</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">numProtocols</span>-<span class="py-src-number">1</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+</pre>
+
+ <p>Here <code>connectionMade</code> and
+ <code>connectionLost</code> cooperate to keep a count of the
+ active protocols in a shared object, the factory. The factory must
+ be passed to <code>Echo.__init__</code> when creating a new
+ instance. The factory is used to share state that exists beyond the
+ lifetime of any given connection. You will see why this object is
+ called a &quot;factory&quot; in the next section.</p>
+
+ <h3>loseConnection() and abortConnection()<a name="auto2"/></h3>
+
+ <p>In the code above, <code>loseConnection</code> is called immediately
+ after writing to the transport. The <code>loseConnection</code> call will
+ close the connection only when all the data has been written by Twisted
+ out to the operating system, so it is safe to use in this case without
+ worrying about transport writes being lost. If
+ a <a href="producers.html" shape="rect">producer</a> is being used with the
+ transport, <code>loseConnection</code> will only close the connection once
+ the producer is unregistered.</p>
+
+ <p>In some cases, waiting until all the data is written out is not what we
+ want. Due to network failures, or bugs or maliciousness in the other side
+ of the connection, data written to the transport may not be deliverable,
+ and so even though <code>loseConnection</code> was called the connection
+ will not be lost. In these cases, <code>abortConnection</code> can be
+ used: it closes the connection immediately, regardless of buffered data
+ that is still unwritten in the transport, or producers that are still
+ registered. Note that <code>abortConnection</code> is only available in
+ Twisted 11.1 and newer.</p>
+
+
+ <h3>Using the Protocol<a name="auto3"/></h3>
+
+ <p>In this section, you will learn how to run a server which uses your
+ <code>Protocol</code>.</p>
+
+ <p>Here is code that will run the QOTD server discussed
+ earlier:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ServerEndpoint</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTDFactory</span>(<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">QOTD</span>()
+
+<span class="py-src-comment"># 8007 is the port you want to run under. Choose something &gt;1024</span>
+<span class="py-src-variable">endpoint</span> = <span class="py-src-variable">TCP4ServerEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">8007</span>)
+<span class="py-src-variable">endpoint</span>.<span class="py-src-variable">listen</span>(<span class="py-src-variable">QOTDFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+ <p>In this example, I create a protocol <code base="twisted.internet.protocol" class="api">Factory</code>. I want to tell this
+ factory that its job is to build QOTD protocol instances, so I set its
+ <code>buildProtocol</code> method to return instances of the QOTD class. Then, I want to listen
+ on a TCP port, so I make a <code>TCP4ServerEndpoint</code> to identify the
+ port that I want to bind to, and then pass the factory I just created to
+ its <code base="twisted.internet.interfaces.IStreamServerEndpoint" class="api">listen</code>
+ method.</p>
+
+ <p>Because this is a short example, nothing else has yet started up the
+ Twisted reactor. <code>endpoint.listen</code> tells the reactor to handle
+ connections to the endpoint's address using a particular protocol, but the
+ reactor needs to be <em>running</em> in order for it to do anything.
+ <code>reactor.run()</code> starts the reactor and then waits forever for
+ connections to arrive on the port you've specified.</p>
+
+ <p>You can stop the reactor by hitting Control-C in a terminal or calling
+ <code>reactor.stop</code>.</p>
+
+ <p>For more information on different ways you can listen for incoming
+ connections, see <a href="endpoints.html" shape="rect">the documentation for the
+ endpoints API</a>.</p>
+
+ <h3>Helper Protocols<a name="auto4"/></h3>
+
+ <p>Many protocols build upon similar lower-level abstraction.
+ The most popular in internet protocols is being line-based.
+ Lines are usually terminated with a CR-LF combinations.</p>
+
+ <p>However, quite a few protocols are mixed - they have
+ line-based sections and then raw data sections. Examples
+ include HTTP/1.1 and the Freenet protocol.</p>
+
+ <p>For those cases, there is the <code>LineReceiver</code>
+ protocol. This protocol dispatches to two different event
+ handlers - <code>lineReceived</code> and
+ <code>rawDataReceived</code>. By default, only
+ <code>lineReceived</code> will be called, once for each line.
+ However, if <code>setRawMode</code> is called, the protocol
+ will call <code>rawDataReceived</code> until
+ <code>setLineMode</code> is called, which returns it to using
+ <code>lineReceived</code>. It also provides a method,
+ <code>sendLine</code>, that writes data to the transport along
+ with the delimiter the class uses to split lines (by default,
+ <code>\r\n</code>).</p>
+
+ <p>Here is an example for a simple use of the line
+ receiver:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">basic</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LineReceiver</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Answer</span>(<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-variable">answers</span> = {<span class="py-src-string">'How are you?'</span>: <span class="py-src-string">'Fine'</span>, <span class="py-src-variable">None</span> : <span class="py-src-string">&quot;I don't know what you mean&quot;</span>}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">answers</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">answers</span>[<span class="py-src-variable">line</span>])
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">answers</span>[<span class="py-src-variable">None</span>])
+</pre>
+
+ <p>Note that the delimiter is not part of the line.</p>
+
+ <p>Several other, less popular, helpers exist, such as a
+ netstring based protocol and a prefixed-message-length
+ protocol.</p>
+
+ <h3>State Machines<a name="auto5"/></h3>
+
+ <p>Many Twisted protocol handlers need to write a state machine
+ to record the state they are at. Here are some pieces of advice
+ which help to write state machines:</p>
+
+ <ul>
+ <li>Don't write big state machines. Prefer to write a state
+ machine which deals with one level of abstraction at a
+ time.</li>
+
+ <li>Don't mix application-specific code with Protocol
+ handling code. When the protocol handler has to make an
+ application-specific call, keep it as a method call.</li>
+ </ul>
+
+ <h2>Factories<a name="auto6"/></h2>
+
+ <h3>Simpler Protocol Creation<a name="auto7"/></h3>
+
+ <p>For a factory which simply instantiates instances of a
+ specific protocol class, there is a simpler way to implement the factory.
+ The default implementation of the <code>buildProtocol</code> method calls
+ the <code>protocol</code> attribute of the factory to create
+ a <code>Protocol</code> instance, and then sets an attribute on it
+ called <code>factory</code> which points to the factory
+ itself. This lets every <code>Protocol</code> access, and possibly
+ modify, the persistent configuration. Here is an example that uses these
+ features instead of overriding <code>buildProtocol</code>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ServerEndpoint</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTD</span>(<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-comment"># self.factory was set by the factory's default buildProtocol:</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">quote</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QOTDFactory</span>(<span class="py-src-parameter">Factory</span>):
+
+ <span class="py-src-comment"># This will be used by the default buildProtocol to create new protocols:</span>
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">QOTD</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">quote</span>=<span class="py-src-parameter">None</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">quote</span> = <span class="py-src-variable">quote</span> <span class="py-src-keyword">or</span> <span class="py-src-string">'An apple a day keeps the doctor away'</span>
+
+<span class="py-src-variable">endpoint</span> = <span class="py-src-variable">TCP4ServerEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">8007</span>)
+<span class="py-src-variable">endpoint</span>.<span class="py-src-variable">listen</span>(<span class="py-src-variable">QOTDFactory</span>(<span class="py-src-string">&quot;configurable quote&quot;</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h3>Factory Startup and Shutdown<a name="auto8"/></h3>
+
+ <p>A Factory has two methods to perform application-specific
+ building up and tearing down (since a Factory is frequently
+ persisted, it is often not appropriate to do them in <code>__init__</code>
+ or <code>__del__</code>, and would frequently be too early or too late).</p>
+
+ <p>Here is an example of a factory which allows its Protocols
+ to write to a special log-file:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">basic</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LineReceiver</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LoggingProtocol</span>(<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">fp</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">line</span>+<span class="py-src-string">'\n'</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LogfileFactory</span>(<span class="py-src-parameter">Factory</span>):
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">LoggingProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">fileName</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">file</span> = <span class="py-src-variable">fileName</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">fp</span> = <span class="py-src-variable">open</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">file</span>, <span class="py-src-string">'a'</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">fp</span>.<span class="py-src-variable">close</span>()
+</pre>
+
+ <h2>Putting it All Together<a name="auto9"/></h2>
+
+ <p>As a final example, here's a simple chat server that allows
+ users to choose a username and then communicate with other
+ users. It demonstrates the use of shared state in the factory, a
+ state machine for each individual protocol, and communication
+ between different protocols.</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">basic</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LineReceiver</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Chat</span>(<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">users</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">state</span> = <span class="py-src-string">&quot;GETNAME&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-string">&quot;What's your name?&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>):
+ <span class="py-src-keyword">del</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>]
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">state</span> == <span class="py-src-string">&quot;GETNAME&quot;</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">handle_GETNAME</span>(<span class="py-src-variable">line</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">handle_CHAT</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">handle_GETNAME</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-variable">name</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-string">&quot;Name taken, please choose another.&quot;</span>)
+ <span class="py-src-keyword">return</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-string">&quot;Welcome, %s!&quot;</span> % (<span class="py-src-variable">name</span>,))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">name</span> = <span class="py-src-variable">name</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">name</span>] = <span class="py-src-variable">self</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">state</span> = <span class="py-src-string">&quot;CHAT&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">handle_CHAT</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">message</span> = <span class="py-src-string">&quot;&lt;%s&gt; %s&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">name</span>, <span class="py-src-variable">message</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">name</span>, <span class="py-src-variable">protocol</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">iteritems</span>():
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">protocol</span> != <span class="py-src-variable">self</span>:
+ <span class="py-src-variable">protocol</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">message</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ChatFactory</span>(<span class="py-src-parameter">Factory</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {} <span class="py-src-comment"># maps user names to Chat instances</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">Chat</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>)
+
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8123</span>, <span class="py-src-variable">ChatFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/servers/chat.py"><span class="filename">listings/servers/chat.py</span></a></div></div>
+
+ <p>The only API you might not be familiar with
+ is <code>listenTCP</code>. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTCP.listenTCP.html" title="twisted.internet.interfaces.IReactorTCP.listenTCP">listenTCP</a></code> is
+ the method which connects a <code>Factory</code> to the network.
+ This is the lower-level API
+ that <a href="endpoints.html" shape="rect">endpoints</a> wraps for you.</p>
+
+ <p>Here's a sample transcript of a chat session (<em><strong>this</strong></em> is text entered by the user):</p>
+
+<pre class="shell" xml:space="preserve">
+$ <em><strong>telnet 127.0.0.1 8123</strong></em>
+Trying 127.0.0.1...
+Connected to 127.0.0.1.
+Escape character is '^]'.
+What's your name?
+<em><strong>test</strong></em>
+Name taken, please choose another.
+<em><strong>bob</strong></em>
+Welcome, bob!
+<em><strong>hello</strong></em>
+&lt;alice&gt; hi bob
+<em><strong>twisted makes writing servers so easy!</strong></em>
+&lt;alice&gt; I couldn't agree more
+&lt;carrol&gt; yeah, it's great
+</pre>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/ssl.html b/doc/core/howto/ssl.html
new file mode 100644
index 0000000..6744ad3
--- /dev/null
+++ b/doc/core/howto/ssl.html
@@ -0,0 +1,550 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using SSL in Twisted</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using SSL in Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">SSL echo server and client without client authentication</a></li><ul><li><a href="#auto2">SSL echo server</a></li><li><a href="#auto3">SSL echo client</a></li></ul><li><a href="#auto4">Using startTLS</a></li><ul><li><a href="#auto5">startTLS server</a></li><li><a href="#auto6">startTLS client</a></li></ul><li><a href="#auto7">Client authentication</a></li><ul><li><a href="#auto8">Client-authenticating server</a></li><li><a href="#auto9">Client with certificates</a></li></ul><li><a href="#auto10">Other facilities</a></li><li><a href="#auto11">Conclusion</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>This document describes how to use SSL in Twisted servers and clients. It
+ assumes that you know what SSL is, what some of the major reasons to use it
+ are, and how to generate your own SSL certificates, in particular self-signed
+ certificates. It also assumes that you are comfortable with creating TCP
+ servers and clients as described in the <a href="servers.html" shape="rect">server howto
+ </a> and <a href="clients.html" shape="rect">client howto</a>. After reading this
+ document you should be able to create servers and clients that can use SSL to
+ encrypt their connections, switch from using an unencrypted channel to an
+ encrypted one mid-connection, and require client authentication.</p>
+
+ <p>Using SSL in Twisted requires that you have
+ <a href="http://pyopenssl.sf.net" shape="rect">pyOpenSSL</a> installed. A quick test to
+ verify that you do is to run <code>from OpenSSL import SSL</code> at a
+ python prompt and not get an error.</p>
+
+ <p>SSL connections require SSL contexts. These contexts are generated by a
+ <code>ContextFactory</code> that maintains state like the SSL method, private
+ key file name, and certificate file name.</p>
+
+ <p>Instead of using listenTCP and connectTCP to create a connection, use
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.listenSSL.html" title="twisted.internet.interfaces.IReactorSSL.listenSSL">listenSSL</a></code> and
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorSSL.connectSSL.html" title="twisted.internet.interfaces.IReactorSSL.connectSSL">connectSSL</a></code> for a
+ server and client respectively. These methods take a contextFactory as an
+ additional argument.</p>
+
+ <p>The basic server context factory is
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.ssl.ContextFactory.html" title="twisted.internet.ssl.ContextFactory">twisted.internet.ssl.ContextFactory</a></code>, and the basic
+ client context factory is
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.ssl.ClientContextFactory.html" title="twisted.internet.ssl.ClientContextFactory">twisted.internet.ssl.ClientContextFactory</a></code>. They can
+ be used as-is or subclassed.
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.ssl.DefaultOpenSSLContextFactory.html" title="twisted.internet.ssl.DefaultOpenSSLContextFactory">twisted.internet.ssl.DefaultOpenSSLContextFactory</a></code>
+ is a convenience server class that subclasses <code>ContextFactory</code>
+ and adds default parameters to the SSL handshake and connection. Another
+ useful class is
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.ssl.CertificateOptions.html" title="twisted.internet.ssl.CertificateOptions">twisted.internet.ssl.CertificateOptions</a></code>; it is a
+ factory for SSL context objects that lets you specify many of the common
+ verification and session options so it can do the proper pyOpenSSL
+ initialization for you.</p>
+
+ <p>Those are the big immediate differences between TCP and SSL connections,
+ so let's look at an example. In it and all subsequent examples it is assumed
+ that keys and certificates for the server, certificate authority, and client
+ should they exist live in a <i>keys/</i> subdirectory of the directory
+ containing the example code, and that the certificates are self-signed.</p>
+
+ <h2>SSL echo server and client without client authentication<a name="auto1"/></h2>
+
+ <p>Authentication and encryption are two separate parts of the SSL protocol.
+ The server almost always needs a key and certificate to authenticate itself
+ to the client but is usually configured to allow encrypted connections with
+ unauthenticated clients who don't have certificates. This common case is
+ demonstrated first by adding SSL support to the echo client and server in
+ the <a href="../examples/index.html" shape="rect">core examples</a>.</p>
+
+ <h3>SSL echo server<a name="auto2"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ssl</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-string">&quot;&quot;&quot;As soon as any data is received, write it back.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">Echo</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenSSL</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>,
+ <span class="py-src-variable">ssl</span>.<span class="py-src-variable">DefaultOpenSSLContextFactory</span>(
+ <span class="py-src-string">'keys/server.key'</span>, <span class="py-src-string">'keys/server.crt'</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h3>SSL echo client<a name="auto3"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ssl</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientFactory</span>, <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClient</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;hello, world&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;hello, world!&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Server said:&quot;</span>, <span class="py-src-variable">data</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClientFactory</span>(<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">EchoClient</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Connection failed - goodbye!&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Connection lost - goodbye!&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">EchoClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectSSL</span>(<span class="py-src-string">'localhost'</span>, <span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>, <span class="py-src-variable">ssl</span>.<span class="py-src-variable">ClientContextFactory</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Contexts are created according to a specified method.
+ <code>SSLv3_METHOD</code>, <code>SSLv23_METHOD</code>, and
+ <code>TLSv1_METHOD</code> are the valid constants that represent SSL methods
+ to use when creating a context object. <code>DefaultOpenSSLContextFactory</code> and
+ <code>ClientContextFactory</code> default to using <code>SSL.SSLv23_METHOD</code> as their
+ method, and it is compatible for communication with all the other methods
+ listed above. An older method constant, <code>SSLv2_METHOD</code>, exists but
+ is explicitly disallowed in both <code>DefaultOpenSSLContextFactory</code> and
+ <code>ClientContextFactory</code> for being insecure by calling
+ <code>set_options(SSL.OP_NO_SSLv2)</code> on their contexts. See
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.ssl.html" title="twisted.internet.ssl">twisted.internet.ssl</a></code> for additional comments.</p>
+
+ <h2>Using startTLS<a name="auto4"/></h2>
+
+ <p>If you want to switch from unencrypted to encrypted traffic
+ mid-connection, you'll need to turn on SSL with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.ITLSTransport.startTLS.html" title="twisted.internet.interfaces.ITLSTransport.startTLS">startTLS</a></code> on both
+ ends of the connection at the same time via some agreed-upon signal like the
+ reception of a particular message. You can readily verify the switch to an
+ encrypted channel by examining the packet payloads with a tool like
+ <a href="http://www.wireshark.org/" shape="rect">Wireshark</a>.</p>
+
+ <h3>startTLS server<a name="auto5"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">ssl</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ServerFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">basic</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LineReceiver</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">TLSServer</span>(<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;received: &quot;</span> + <span class="py-src-variable">line</span>
+
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">line</span> == <span class="py-src-string">&quot;STARTTLS&quot;</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;-- Switching to TLS&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-string">'READY'</span>)
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">ServerTLSContext</span>(
+ <span class="py-src-variable">privateKeyFileName</span>=<span class="py-src-string">'keys/server.key'</span>,
+ <span class="py-src-variable">certificateFileName</span>=<span class="py-src-string">'keys/server.crt'</span>,
+ )
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">startTLS</span>(<span class="py-src-variable">ctx</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ServerTLSContext</span>(<span class="py-src-parameter">ssl</span>.<span class="py-src-parameter">DefaultOpenSSLContextFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, *<span class="py-src-parameter">args</span>, **<span class="py-src-parameter">kw</span>):
+ <span class="py-src-variable">kw</span>[<span class="py-src-string">'sslmethod'</span>] = <span class="py-src-variable">SSL</span>.<span class="py-src-variable">TLSv1_METHOD</span>
+ <span class="py-src-variable">ssl</span>.<span class="py-src-variable">DefaultOpenSSLContextFactory</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>, *<span class="py-src-variable">args</span>, **<span class="py-src-variable">kw</span>)
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">TLSServer</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h3>startTLS client<a name="auto6"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">ssl</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span>.<span class="py-src-variable">basic</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">LineReceiver</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientTLSContext</span>(<span class="py-src-parameter">ssl</span>.<span class="py-src-parameter">ClientContextFactory</span>):
+ <span class="py-src-variable">isClient</span> = <span class="py-src-number">1</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getContext</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">SSL</span>.<span class="py-src-variable">Context</span>(<span class="py-src-variable">SSL</span>.<span class="py-src-variable">TLSv1_METHOD</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">TLSClient</span>(<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-variable">pretext</span> = [
+ <span class="py-src-string">&quot;first line&quot;</span>,
+ <span class="py-src-string">&quot;last thing before TLS starts&quot;</span>,
+ <span class="py-src-string">&quot;STARTTLS&quot;</span>]
+
+ <span class="py-src-variable">posttext</span> = [
+ <span class="py-src-string">&quot;first thing after TLS started&quot;</span>,
+ <span class="py-src-string">&quot;last thing ever&quot;</span>]
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">l</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">pretext</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">l</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;received: &quot;</span> + <span class="py-src-variable">line</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">line</span> == <span class="py-src-string">&quot;READY&quot;</span>:
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">ClientTLSContext</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">startTLS</span>(<span class="py-src-variable">ctx</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">l</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">posttext</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">l</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">TLSClientFactory</span>(<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">TLSClient</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;connection failed: &quot;</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">getErrorMessage</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;connection lost: &quot;</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">getErrorMessage</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">TLSClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">'localhost'</span>, <span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p><code>startTLS</code> is a transport method that gets passed a context.
+ It is invoked at an agreed-upon time in the data reception method of the
+ client and server protocols. The <code>ServerTLSContext</code> and
+ <code>ClientTLSContext</code> classes used above inherit from the basic
+ server and client context factories used in the earlier echo examples and
+ illustrate two more ways of setting an SSL method.</p>
+
+ <h2>Client authentication<a name="auto7"/></h2>
+
+ <p>Server and client-side changes to require client authentication fall
+ largely under the dominion of pyOpenSSL, but few examples seem to exist on
+ the web so for completeness a sample server and client are provided here.</p>
+
+ <h3>Client-authenticating server<a name="auto8"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ssl</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Factory</span>, <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">verifyCallback</span>(<span class="py-src-parameter">connection</span>, <span class="py-src-parameter">x509</span>, <span class="py-src-parameter">errnum</span>, <span class="py-src-parameter">errdepth</span>, <span class="py-src-parameter">ok</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">ok</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'invalid cert from subject:'</span>, <span class="py-src-variable">x509</span>.<span class="py-src-variable">get_subject</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">False</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Certs are fine&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">True</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">Factory</span>()
+ <span class="py-src-variable">factory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">Echo</span>
+
+ <span class="py-src-variable">myContextFactory</span> = <span class="py-src-variable">ssl</span>.<span class="py-src-variable">DefaultOpenSSLContextFactory</span>(
+ <span class="py-src-string">'keys/server.key'</span>, <span class="py-src-string">'keys/server.crt'</span>
+ )
+
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">myContextFactory</span>.<span class="py-src-variable">getContext</span>()
+
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">set_verify</span>(
+ <span class="py-src-variable">SSL</span>.<span class="py-src-variable">VERIFY_PEER</span> | <span class="py-src-variable">SSL</span>.<span class="py-src-variable">VERIFY_FAIL_IF_NO_PEER_CERT</span>,
+ <span class="py-src-variable">verifyCallback</span>
+ )
+
+ <span class="py-src-comment"># Since we have self-signed certs we have to explicitly</span>
+ <span class="py-src-comment"># tell the server to trust them.</span>
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">load_verify_locations</span>(<span class="py-src-string">&quot;keys/ca.pem&quot;</span>)
+
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenSSL</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>, <span class="py-src-variable">myContextFactory</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Use the <code>set_verify</code> method to set the verification mode for a
+ context object and the verification callback. The mode is either
+ <code>VERIFY_NONE</code> or <code>VERIFY_PEER</code>. If
+ <code>VERIFY_PEER</code> is set, the mode can be augmented by
+ <code>VERIFY_FAIL_IF_NO_PEER_CERT</code> and/or
+ <code>VERIFY_CLIENT_ONCE</code>.</p>
+
+ <p>The callback takes as its arguments a connection object, X509 object,
+ error number, error depth, and return code. The purpose of the callback is
+ to allow you to enforce additional restrictions on the verification. Thus,
+ if the return code is False, you should return False; if the return code is
+ True <i>and</i> further verification passes, return True.</p>
+
+
+ <h3>Client with certificates<a name="auto9"/></h3>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ssl</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientFactory</span>, <span class="py-src-variable">Protocol</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClient</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;hello, world&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;hello, world!&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Server said:&quot;</span>, <span class="py-src-variable">data</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoClientFactory</span>(<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">EchoClient</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Connection failed - goodbye!&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">connector</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Connection lost - goodbye!&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CtxFactory</span>(<span class="py-src-parameter">ssl</span>.<span class="py-src-parameter">ClientContextFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getContext</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">method</span> = <span class="py-src-variable">SSL</span>.<span class="py-src-variable">SSLv23_METHOD</span>
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">ssl</span>.<span class="py-src-variable">ClientContextFactory</span>.<span class="py-src-variable">getContext</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_certificate_file</span>(<span class="py-src-string">'keys/client.crt'</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_privatekey_file</span>(<span class="py-src-string">'keys/client.key'</span>)
+
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">ctx</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">EchoClientFactory</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectSSL</span>(<span class="py-src-string">'localhost'</span>, <span class="py-src-number">8000</span>, <span class="py-src-variable">factory</span>, <span class="py-src-variable">CtxFactory</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h2>Other facilities<a name="auto10"/></h2>
+
+ <p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.amp.html" title="twisted.protocols.amp">twisted.protocols.amp</a></code> supports encrypted
+ connections and exposes a <code>startTLS</code> method one can use or
+ subclass. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.html" title="twisted.web">twisted.web</a></code> has built-in SSL support in
+ its <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.html" title="twisted.web.client">client</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.html" title="twisted.web.http">http</a></code>, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.html" title="twisted.web.xmlrpc">xmlrpc</a></code> modules.</p>
+
+ <h2>Conclusion<a name="auto11"/></h2>
+
+ <p>After reading through this tutorial, you should be able to: </p>
+ <ul>
+ <li>Use <code>listenSSL</code> and <code>connectSSL</code> to create servers and clients that use
+ SSL</li>
+ <li>Use <code>startTLS</code> to switch a channel from being unencrypted to using SSL
+ mid-connection</li>
+ <li>Add server and client support for client authentication</li>
+ </ul>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/stylesheet-unprocessed.css b/doc/core/howto/stylesheet-unprocessed.css
new file mode 100644
index 0000000..e4a62cc
--- /dev/null
+++ b/doc/core/howto/stylesheet-unprocessed.css
@@ -0,0 +1,20 @@
+
+span.footnote {
+ vertical-align: super;
+ font-size: small;
+}
+
+span.footnote:before
+{
+ content: "[Footnote: ";
+}
+
+span.footnote:after
+{
+ content: "]";
+}
+
+div.note:before
+{
+ content: "Note: ";
+}
diff --git a/doc/core/howto/stylesheet.css b/doc/core/howto/stylesheet.css
new file mode 100644
index 0000000..3c5961e
--- /dev/null
+++ b/doc/core/howto/stylesheet.css
@@ -0,0 +1,189 @@
+
+body
+{
+ margin-left: 2em;
+ margin-right: 2em;
+ border: 0px;
+ padding: 0px;
+ font-family: sans-serif;
+ }
+
+.done { color: #005500; background-color: #99ff99 }
+.notdone { color: #550000; background-color: #ff9999;}
+
+pre
+{
+ padding: 1em;
+ border: thin black solid;
+ line-height: 1.2em;
+}
+
+.boxed
+{
+ padding: 1em;
+ border: thin black solid;
+}
+
+.shell
+{
+ background-color: #ffffdd;
+}
+
+.python
+{
+ background-color: #dddddd;
+}
+
+.htmlsource
+{
+ background-color: #dddddd;
+}
+
+.py-prototype
+{
+ background-color: #ddddff;
+}
+
+
+.python-interpreter
+{
+ background-color: #ddddff;
+}
+
+.doit
+{
+ border: thin blue dashed ;
+ background-color: #0ef
+}
+
+.py-src-comment
+{
+ color: #1111CC
+}
+
+.py-src-keyword
+{
+ color: #3333CC;
+ font-weight: bold;
+ line-height: 1.0em
+}
+
+.py-src-parameter
+{
+ color: #000066;
+ font-weight: bold;
+ line-height: 1.0em
+}
+
+.py-src-identifier
+{
+ color: #CC0000
+}
+
+.py-src-string
+{
+
+ color: #115511
+}
+
+.py-src-endmarker
+{
+ display: block; /* IE hack; prevents following line from being sucked into the py-listing box. */
+}
+
+.py-linenumber
+{
+ background-color: #cdcdcd;
+ float: left;
+ margin-top: 0px;
+ width: 4.0em
+}
+
+.py-listing, .html-listing, .listing
+{
+ margin: 1ex;
+ border: thin solid black;
+ background-color: #eee;
+}
+
+.py-listing pre, .html-listing pre, .listing pre
+{
+ margin: 0px;
+ border: none;
+ border-bottom: thin solid black;
+}
+
+.py-listing .python
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.html-listing .htmlsource
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.caption
+{
+ text-align: center;
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.filename
+{
+ font-style: italic;
+ }
+
+.manhole-output
+{
+ color: blue;
+}
+
+hr
+{
+ display: inline;
+ }
+
+ul
+{
+ padding: 0px;
+ margin: 0px;
+ margin-left: 1em;
+ padding-left: 1em;
+ border-left: 1em;
+ }
+
+li
+{
+ padding: 2px;
+ }
+
+dt
+{
+ font-weight: bold;
+ margin-left: 1ex;
+ }
+
+dd
+{
+ margin-bottom: 1em;
+ }
+
+div.note
+{
+ background-color: #FFFFCC;
+ margin-top: 1ex;
+ margin-left: 5%;
+ margin-right: 5%;
+ padding-top: 1ex;
+ padding-left: 5%;
+ padding-right: 5%;
+ border: thin black solid;
+}
diff --git a/doc/core/howto/tap.html b/doc/core/howto/tap.html
new file mode 100644
index 0000000..1441cb5
--- /dev/null
+++ b/doc/core/howto/tap.html
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Writing a twistd Plugin</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Writing a twistd Plugin</h1>
+ <div class="toc"><ol><li><a href="#auto0">Goals</a></li><li><a href="#auto1">Alternatives to twistd plugins</a></li><li><a href="#auto2">Creating the plugin</a></li><li><a href="#auto3">Using cred with your TAP</a></li><li><a href="#auto4">Conclusion</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<p>This document describes adding subcommands to
+the <code>twistd</code> command, as a way to facilitate the deployment
+of your applications. <em>(This feature was added in Twisted 2.5)</em></p>
+
+<p>The target audience of this document are those that have developed
+a Twisted application which needs a command line-based deployment
+mechanism.</p>
+
+<p>There are a few prerequisites to understanding this document:</p>
+<ul>
+ <li>A basic understanding of the Twisted Plugin System (i.e.,
+ the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.html" title="twisted.plugin">twisted.plugin</a></code> module) is
+ necessary, however, step-by-step instructions will be
+ given. Reading <a href="plugin.html" shape="rect">The Twisted Plugin
+ System</a> is recommended, in particular the <q>Extending an
+ Existing Program</q> section.</li>
+ <li>The <a href="application.html" shape="rect">Application</a> infrastructure
+ is used in <code>twistd</code> plugins; in particular, you should
+ know how to expose your program's functionality as a Service.</li>
+ <li>In order to parse command line arguments, the <code>twistd</code> plugin
+ mechanism relies
+ on <code>twisted.python.usage</code>, which is documented
+ in <a href="options.html" shape="rect">Using usage.Options</a>.</li>
+</ul>
+
+<h2>Goals<a name="auto0"/></h2>
+
+<p>After reading this document, the reader should be able to expose
+their Service-using application as a subcommand
+of <code>twistd</code>, taking into consideration whatever was passed
+on the command line.</p>
+
+<h2>Alternatives to twistd plugins<a name="auto1"/></h2>
+<p>The major alternative to the twistd plugin mechanism is the <code>.tac</code>
+file, which is a simple script to be used with the
+twistd <code>-y/--python</code> parameter. The twistd plugin mechanism
+exists to offer a more extensible command-line-driven interface to
+your application. For more information on <code>.tac</code> files, see
+the document <a href="application.html" shape="rect">Using the Twisted Application
+Framework</a>.</p>
+
+
+<h2>Creating the plugin<a name="auto2"/></h2>
+
+<p>The following directory structure is assumed of your project:</p>
+
+<ul>
+ <li><strong>MyProject</strong> - Top level directory
+ <ul>
+ <li><strong>myproject</strong> - Python package
+ <ul><li><strong>__init__.py</strong></li></ul>
+ </li>
+ </ul>
+ </li>
+</ul>
+
+<p>
+ During development of your project, Twisted plugins can be loaded
+ from a special directory in your project, assuming your top level
+ directory ends up in sys.path. Create a directory
+ named <code>twisted</code> containing a directory
+ named <code>plugins</code>, and add a file
+ named <code>myproject_plugin.py</code> to it. This file will contain your
+ plugin. Note that you should <em>not</em> add any __init__.py files
+ to this directory structure, and the plugin file should <em>not</em>
+ be named <code>myproject.py</code> (because that would conflict with
+ your project's module name).
+</p>
+
+<p>
+ In this file, define an object which <em>provides</em> the interfaces
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugin.IPlugin.html" title="twisted.plugin.IPlugin">twisted.plugin.IPlugin</a></code>
+ and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IServiceMaker.html" title="twisted.application.service.IServiceMaker">twisted.application.service.IServiceMaker</a></code>.
+</p>
+
+<p>The <code>tapname</code> attribute of your IServiceMaker provider
+will be used as the subcommand name in a command
+like <code class="shell">twistd [subcommand] [args...]</code>, and
+the <code>options</code> attribute (which should be
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.usage.Options.html" title="twisted.python.usage.Options">usage.Options</a></code>
+subclass) will be used to parse the given args.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IPlugin</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">service</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IServiceMaker</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myproject</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">MyFactory</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+ <span class="py-src-variable">optParameters</span> = [[<span class="py-src-string">&quot;port&quot;</span>, <span class="py-src-string">&quot;p&quot;</span>, <span class="py-src-number">1235</span>, <span class="py-src-string">&quot;The port number to listen on.&quot;</span>]]
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyServiceMaker</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IServiceMaker</span>, <span class="py-src-variable">IPlugin</span>)
+ <span class="py-src-variable">tapname</span> = <span class="py-src-string">&quot;myproject&quot;</span>
+ <span class="py-src-variable">description</span> = <span class="py-src-string">&quot;Run this! It'll make your dog happy.&quot;</span>
+ <span class="py-src-variable">options</span> = <span class="py-src-variable">Options</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">makeService</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">options</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Construct a TCPServer from a factory defined in myproject.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">options</span>[<span class="py-src-string">&quot;port&quot;</span>]), <span class="py-src-variable">MyFactory</span>())
+
+
+<span class="py-src-comment"># Now construct an object which *provides* the relevant interfaces</span>
+<span class="py-src-comment"># The name of this variable is irrelevant, as long as there is *some*</span>
+<span class="py-src-comment"># name bound to a provider of IPlugin and IServiceMaker.</span>
+
+<span class="py-src-variable">serviceMaker</span> = <span class="py-src-variable">MyServiceMaker</span>()
+</pre>
+
+<p>
+ Now running <code class="shell">twistd --help</code> should
+ print <code>myproject</code> in the list of available subcommands,
+ followed by the description that we specified in the
+ plugin. <code class="shell">twistd -n myproject</code> would,
+ assuming we defined a <code>MyFactory</code> factory
+ inside <code>myproject</code>, start a listening server on port 1235
+ with that factory.
+</p>
+
+<h2>Using cred with your TAP<a name="auto3"/></h2>
+
+<p>
+ Twisted ships with a robust authentication framework to use with
+ your application. If your server needs authentication functionality,
+ and you haven't read about <a href="cred.html" shape="rect">twisted.cred</a>
+ yet, read up on it first.
+</p>
+
+<p>
+ If you are building a twistd plugin and you want to support a wide
+ variety of authentication patterns, Twisted provides an easy-to-use
+ mixin for your Options subclass:
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.strcred.AuthOptionMixin.html" title="twisted.cred.strcred.AuthOptionMixin">strcred.AuthOptionMixin</a></code>.
+ The following code is an example of using this mixin:
+</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">credentials</span>, <span class="py-src-variable">portal</span>, <span class="py-src-variable">strcred</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IPlugin</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">service</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IServiceMaker</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myserver</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">myservice</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ServerOptions</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>, <span class="py-src-parameter">strcred</span>.<span class="py-src-parameter">AuthOptionMixin</span>):
+ <span class="py-src-comment"># This part is optional; it tells AuthOptionMixin what</span>
+ <span class="py-src-comment"># kinds of credential interfaces the user can give us.</span>
+ <span class="py-src-variable">supportedInterfaces</span> = (<span class="py-src-variable">credentials</span>.<span class="py-src-variable">IUsernamePassword</span>,)
+
+ <span class="py-src-variable">optParameters</span> = [
+ [<span class="py-src-string">&quot;port&quot;</span>, <span class="py-src-string">&quot;p&quot;</span>, <span class="py-src-number">1234</span>, <span class="py-src-string">&quot;Server port number&quot;</span>],
+ [<span class="py-src-string">&quot;host&quot;</span>, <span class="py-src-string">&quot;h&quot;</span>, <span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-string">&quot;Server hostname&quot;</span>]]
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyServerServiceMaker</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IServiceMaker</span>, <span class="py-src-variable">IPlugin</span>)
+ <span class="py-src-variable">tapname</span> = <span class="py-src-string">&quot;myserver&quot;</span>
+ <span class="py-src-variable">description</span> = <span class="py-src-string">&quot;This server does nothing productive.&quot;</span>
+ <span class="py-src-variable">options</span> = <span class="py-src-variable">ServerOptions</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">makeService</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">options</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Construct a service object.&quot;&quot;&quot;</span>
+ <span class="py-src-comment"># The realm is a custom object that your server defines.</span>
+ <span class="py-src-variable">realm</span> = <span class="py-src-variable">myservice</span>.<span class="py-src-variable">MyServerRealm</span>(<span class="py-src-variable">options</span>[<span class="py-src-string">&quot;host&quot;</span>])
+
+ <span class="py-src-comment"># The portal is something Cred can provide, as long as</span>
+ <span class="py-src-comment"># you have a list of checkers that you'll support. This</span>
+ <span class="py-src-comment"># list is provided my AuthOptionMixin.</span>
+ <span class="py-src-variable">portal</span> = <span class="py-src-variable">portal</span>.<span class="py-src-variable">Portal</span>(<span class="py-src-variable">realm</span>, <span class="py-src-variable">options</span>[<span class="py-src-string">&quot;credCheckers&quot;</span>])
+
+ <span class="py-src-comment"># OR, if you know you might get multiple interfaces, and</span>
+ <span class="py-src-comment"># only want to give your application one of them, you</span>
+ <span class="py-src-comment"># also have that option with AuthOptionMixin:</span>
+ <span class="py-src-variable">interface</span> = <span class="py-src-variable">credentials</span>.<span class="py-src-variable">IUsernamePassword</span>
+ <span class="py-src-variable">portal</span> = <span class="py-src-variable">portal</span>.<span class="py-src-variable">Portal</span>(<span class="py-src-variable">realm</span>, <span class="py-src-variable">options</span>[<span class="py-src-string">&quot;credInterfaces&quot;</span>][<span class="py-src-variable">interface</span>])
+
+ <span class="py-src-comment"># The protocol factory is, like the realm, something you implement.</span>
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">myservice</span>.<span class="py-src-variable">ServerFactory</span>(<span class="py-src-variable">realm</span>, <span class="py-src-variable">portal</span>)
+
+ <span class="py-src-comment"># Finally, return a service that will listen for connections.</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">options</span>[<span class="py-src-string">&quot;port&quot;</span>]), <span class="py-src-variable">factory</span>)
+
+
+<span class="py-src-comment"># As in our example above, we have to construct an object that</span>
+<span class="py-src-comment"># provides the IPlugin and IServiceMaker interfaces.</span>
+
+<span class="py-src-variable">serviceMaker</span> = <span class="py-src-variable">MyServerServiceMaker</span>()
+</pre>
+
+<p>
+ Now that you have your TAP configured to support any authentication
+ we can throw at it, you're ready to use it. Here is an example of
+ starting your server using the /etc/passwd file for
+ authentication. (Clearly, this won't work on servers with shadow
+ passwords.)
+</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd myserver --auth passwd:/etc/passwd
+</pre>
+
+<p>
+ For a full list of cred plugins supported, see <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.plugins.html" title="twisted.plugins">twisted.plugins</a></code>, or use the command-line help:
+</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd myserver --help-auth
+$ twistd myserver --help-auth-type passwd
+</pre>
+
+<h2>Conclusion<a name="auto4"/></h2>
+
+<p>You should now be able to</p>
+<ul>
+ <li>Create a twistd plugin</li>
+ <li>Incorporate authentication into your plugin</li>
+ <li>Use it from your development environment</li>
+ <li>Install it correctly and use it in deployment</li>
+</ul>
+
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/template.tpl b/doc/core/howto/template.tpl
new file mode 100644
index 0000000..1fbb517
--- /dev/null
+++ b/doc/core/howto/template.tpl
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+<title>Twisted Documentation: </title>
+<link type="text/css" rel="stylesheet"
+href="stylesheet.css" />
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title"></h1>
+ <div class="toc"></div>
+ <div class="body">
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: </span>
+ </body>
+</html>
+
diff --git a/doc/core/howto/testing.html b/doc/core/howto/testing.html
new file mode 100644
index 0000000..01662b1
--- /dev/null
+++ b/doc/core/howto/testing.html
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Writing tests for Twisted code using Trial</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Writing tests for Twisted code using Trial</h1>
+ <div class="toc"><ol><li><a href="#auto0">Trial basics</a></li><li><a href="#auto1">Trial directories</a></li><li><a href="#auto2">Twisted-specific quirks: reactor, Deferreds, callLater</a></li><ul><li><a href="#auto3">Leave the Reactor as you found it</a></li><li><a href="#auto4">Using Timers to Detect Failing Tests</a></li><li><a href="#auto5">Interacting with warnings in tests</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Trial basics<a name="auto0"/></h2>
+
+<p><strong>Trial</strong> is Twisted's testing framework. It provides a
+library for writing test cases and utility functions for working with the
+Twisted environment in your tests, and a command-line utility for running your
+tests. Trial is built on the Python standard library's <code>unittest</code>
+module.</p>
+
+<p>To run all the Twisted tests, do:</p>
+
+<pre class="shell" xml:space="preserve">
+$ trial twisted
+</pre>
+
+<p>Refer to the Trial man page for other command-line options.</p>
+
+<h2>Trial directories<a name="auto1"/></h2>
+
+<p>You might notice a new <code class="shell">_trial_temp</code> folder in the
+current working directory after Trial completes the tests. This folder is the
+working directory for the Trial process. It can be used by unit tests and
+allows them to write whatever data they like to disk, and not worry
+about polluting the current working directory.</p>
+
+<p>Folders named <code class="shell">_trial_temp-&lt;counter&gt;</code> are
+created if two instances of Trial are run in parallel from the same directory,
+so as to avoid giving two different test-runs the same temporary directory.</p>
+
+<p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.lockfile.html" title="twisted.python.lockfile">twisted.python.lockfile</a></code> utility is used to lock
+the <code class="shell">_trial_temp</code> directories. On Linux, this results
+in symlinks to pids. On Windows, directories are created with a single file with
+a pid as the contents. These lock files will be cleaned up if Trial exits normally
+and otherwise they will be left behind. They should be cleaned up the next time
+Trial tries to use the directory they lock, but it's also safe to delete them
+manually if desired.</p>
+
+<h2>Twisted-specific quirks: reactor, Deferreds, callLater<a name="auto2"/></h2>
+
+<p>The standard Python <code>unittest</code> framework, from which Trial is
+derived, is ideal for testing code with a fairly linear flow of control.
+Twisted is an asynchronous networking framework which provides a clean,
+sensible way to establish functions that are run in response to events (like
+timers and incoming data), which creates a highly non-linear flow of control.
+Trial has a few extensions which help to test this kind of code. This section
+provides some hints on how to use these extensions and how to best structure
+your tests.</p>
+
+<h3>Leave the Reactor as you found it<a name="auto3"/></h3>
+
+<p>Trial runs the entire test suite (over four thousand tests) in a single
+process, with a single reactor. Therefore it is important that your test
+leave the reactor in the same state as it found it. Leftover timers may
+expire during somebody else's unsuspecting test. Leftover connection attempts
+may complete (and fail) during a later test. These lead to intermittent
+failures that wander from test to test and are very time-consuming to track
+down.</p>
+
+<p>If your test leaves event sources in the reactor, Trial will fail the test.
+The <code>tearDown</code> method is a good place to put cleanup code: it is
+always run regardless of whether your test passes or fails (like a <code>finally</code>
+clause in a try-except-finally construct). Exceptions in <code>tearDown</code>
+are flagged as errors and flunk the test.
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.trial.unittest.TestCase.addCleanup.html" title="twisted.trial.unittest.TestCase.addCleanup">TestCase.addCleanup</a></code> is
+another useful tool for cleaning up. With it, you can register callables to
+clean up resources as the test allocates them. Generally, code should be
+written so that only resources allocated in the tests need to be cleaned up in
+the tests. Resources which are allocated internally by the implementation
+should be cleaned up by the implementation.</p>
+
+<p>If your code uses Deferreds or depends on the reactor running, you can
+return a Deferred from your test method, setUp, or tearDown and Trial will
+do the right thing. That is, it will run the reactor for you until the
+Deferred has triggered and its callbacks have been run. Don't use
+ <code>reactor.run()</code>, <code>reactor.stop()</code>, <code>reactor.crash()</code> or <code>reactor.iterate()</code> in your tests.</p>
+
+<p>Calls to <code>reactor.callLater</code> create <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IDelayedCall.html" title="twisted.internet.interfaces.IDelayedCall">IDelayedCall</a></code>s. These need to be run
+or cancelled during a test, otherwise they will outlive the test. This would
+be bad, because they could interfere with a later test, causing confusing
+failures in unrelated tests! For this reason, Trial checks the reactor to make
+sure there are no leftover <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IDelayedCall.html" title="twisted.internet.interfaces.IDelayedCall">IDelayedCall</a></code>s in the reactor after a
+test, and will fail the test if there are. The cleanest and simplest way to
+make sure this all works is to return a Deferred from your test.</p>
+
+<p>Similarly, sockets created during a test should be closed by the end of the
+test. This applies to both listening ports and client connections. So, calls
+to <code>reactor.listenTCP</code> (and <code>listenUNIX</code>, and so on)
+return <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IListeningPort.html" title="twisted.internet.interfaces.IListeningPort">IListeningPort</a></code>s, and these should be
+cleaned up before a test ends by calling their <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IListeningPort.stopListening.html" title="twisted.internet.interfaces.IListeningPort.stopListening">stopListening</a></code> method.
+Calls to <code>reactor.connectTCP</code> return <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConnector.html" title="twisted.internet.interfaces.IConnector">IConnector</a></code>s, which should be cleaned
+up by calling their <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IConnector.disconnect.html" title="twisted.internet.interfaces.IConnector.disconnect">disconnect</a></code> method. Trial
+will warn about unclosed sockets.</p>
+
+<p>The golden rule is: If your tests call a function which returns a Deferred,
+your test should return a Deferred.</p>
+
+<h3>Using Timers to Detect Failing Tests<a name="auto4"/></h3>
+
+<p>It is common for tests to establish some kind of fail-safe timeout that
+will terminate the test in case something unexpected has happened and none of
+the normal test-failure paths are followed. This timeout puts an upper bound
+on the time that a test can consume, and prevents the entire test suite from
+stalling because of a single test. This is especially important for the
+Twisted test suite, because it is run automatically by the buildbot whenever
+changes are committed to the Subversion repository.</p>
+
+<p>The way to do this in Trial is to set the <code>.timeout</code> attribute
+on your unit test method. Set the attribute to the number of seconds you wish
+to elapse before the test raises a timeout error. Trial has a default timeout
+which will be applied even if the <code>timeout</code> attribute is not set.
+The Trial default timeout is usually sufficient and should be overridden only
+in unusual cases.</p>
+
+<h3>Interacting with warnings in tests<a name="auto5"/></h3>
+
+<p>Trial includes specific support for interacting with Python's
+ <code>warnings</code> module. This support allows warning-emitting code to
+be written test-driven, just as any other code would be. It also improves
+the way in which warnings reporting when a test suite is running.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.trial.unittest.TestCase.flushWarnings.html" title="twisted.trial.unittest.TestCase.flushWarnings">TestCase.flushWarnings</a></code>
+allows tests to be written which make assertions about what warnings have
+been emitted during a particular test method. In order to test a warning with
+ <code>flushWarnings</code>, write a test which first invokes the code which
+will emit a warning and then calls <code>flushWarnings</code> and makes
+assertions about the result. For example:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SomeWarningsTests</span>(<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_warning</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">warnings</span>.<span class="py-src-variable">warn</span>(<span class="py-src-string">&quot;foo is bad&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">flushWarnings</span>()), <span class="py-src-number">1</span>)
+</pre>
+
+<p>Warnings emitted in tests which are not flushed will be included by the
+default reporter in its output after the result of the test. If Python's
+warnings filter system (see <a href="http://docs.python.org/using/cmdline.html#cmdoption-unittest-discover-W" shape="rect">the
+-W command option to Python</a>) is configured to treat a warning as an error,
+then unflushed warnings will causes tests to fail and will be included in
+the summary section of the default reporter. Note that unlike usual
+operation, when <code>warnings.warn</code> is called as part of a test
+method, it will not raise an exception when warnings have been configured as
+errors. However, if called outside of a test method (for example, at module
+scope in a test module or a module imported by a test module) then it
+ <em>will</em> raise an exception.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/threading.html b/doc/core/howto/threading.html
new file mode 100644
index 0000000..9eda7a4
--- /dev/null
+++ b/doc/core/howto/threading.html
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using Threads in Twisted</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using Threads in Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Running code in a thread-safe manner</a></li><li><a href="#auto1">Running code in threads</a></li><li><a href="#auto2">Utility Methods</a></li><li><a href="#auto3">Managing the Thread Pool</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Running code in a thread-safe manner<a name="auto0"/></h2>
+
+ <p>Most code in Twisted is not thread-safe. For example,
+ writing data to a transport from a protocol is not thread-safe.
+ Therefore, we want a way to schedule methods to be run in the
+ main event loop. This can be done using the function <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorThreads.callFromThread.html" title="twisted.internet.interfaces.IReactorThreads.callFromThread">twisted.internet.interfaces.IReactorThreads.callFromThread</a></code>:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">notThreadSafe</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;do something that isn't thread-safe&quot;&quot;&quot;</span>
+ <span class="py-src-comment"># ...</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">threadSafeScheduler</span>():
+ <span class="py-src-string">&quot;&quot;&quot;Run in thread-safe manner.&quot;&quot;&quot;</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callFromThread</span>(<span class="py-src-variable">notThreadSafe</span>, <span class="py-src-number">3</span>) <span class="py-src-comment"># will run 'notThreadSafe(3)'</span>
+ <span class="py-src-comment"># in the event loop</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h2>Running code in threads<a name="auto1"/></h2>
+
+ <p>Sometimes we may want to run methods in threads - for
+ example, in order to access blocking APIs. Twisted provides
+ methods for doing so using the IReactorThreads API (<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorThreads.html" title="twisted.internet.interfaces.IReactorThreads">twisted.internet.interfaces.IReactorThreads</a></code>).
+ Additional utility functions are provided in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.threads.html" title="twisted.internet.threads">twisted.internet.threads</a></code>. Basically, these
+ methods allow us to queue methods to be run by a thread
+ pool.</p>
+
+ <p>For example, to run a method in a thread we can do:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">aSillyBlockingMethod</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+ <span class="py-src-variable">time</span>.<span class="py-src-variable">sleep</span>(<span class="py-src-number">2</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">x</span>
+
+<span class="py-src-comment"># run method in thread</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callInThread</span>(<span class="py-src-variable">aSillyBlockingMethod</span>, <span class="py-src-string">&quot;2 seconds have passed&quot;</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <h2>Utility Methods<a name="auto2"/></h2>
+
+ <p>The utility methods are not part of the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">twisted.internet.reactor</a></code> APIs, but are implemented
+ in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.threads.html" title="twisted.internet.threads">twisted.internet.threads</a></code>.</p>
+
+ <p>If we have multiple methods to run sequentially within a thread,
+ we can do:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">threads</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">aSillyBlockingMethodOne</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+ <span class="py-src-variable">time</span>.<span class="py-src-variable">sleep</span>(<span class="py-src-number">2</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">x</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">aSillyBlockingMethodTwo</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">x</span>
+
+<span class="py-src-comment"># run both methods sequentially in a thread</span>
+<span class="py-src-variable">commands</span> = [(<span class="py-src-variable">aSillyBlockingMethodOne</span>, [<span class="py-src-string">&quot;Calling First&quot;</span>], {})]
+<span class="py-src-variable">commands</span>.<span class="py-src-variable">append</span>((<span class="py-src-variable">aSillyBlockingMethodTwo</span>, [<span class="py-src-string">&quot;And the second&quot;</span>], {}))
+<span class="py-src-variable">threads</span>.<span class="py-src-variable">callMultipleInThread</span>(<span class="py-src-variable">commands</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>For functions whose results we wish to get, we can have the
+ result returned as a Deferred:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">threads</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">doLongCalculation</span>():
+ <span class="py-src-comment"># .... do long calculation here ...</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-number">3</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">x</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">x</span>
+
+<span class="py-src-comment"># run method in thread and get result as defer.Deferred</span>
+<span class="py-src-variable">d</span> = <span class="py-src-variable">threads</span>.<span class="py-src-variable">deferToThread</span>(<span class="py-src-variable">doLongCalculation</span>)
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>If you wish to call a method in the reactor thread and get its result,
+ you can use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.threads.blockingCallFromThread.html" title="twisted.internet.threads.blockingCallFromThread">blockingCallFromThread</a></code>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">threads</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">getPage</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">error</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Error</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">inThread</span>():
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">threads</span>.<span class="py-src-variable">blockingCallFromThread</span>(
+ <span class="py-src-variable">reactor</span>, <span class="py-src-variable">getPage</span>, <span class="py-src-string">&quot;http://twistedmatrix.com/&quot;</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">Error</span>, <span class="py-src-variable">exc</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">exc</span>
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callFromThread</span>(<span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callInThread</span>(<span class="py-src-variable">inThread</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p><code>blockingCallFromThread</code> will return the object or raise
+ the exception returned or raised by the function passed to it. If the
+ function passed to it returns a Deferred, it will return the value the
+ Deferred is called back with or raise the exception it is errbacked
+ with.</p>
+
+ <h2>Managing the Thread Pool<a name="auto3"/></h2>
+
+ <p>The thread pool is implemented by <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.threadpool.ThreadPool.html" title="twisted.python.threadpool.ThreadPool">twisted.python.threadpool.ThreadPool</a></code>.</p>
+
+ <p>We may want to modify the size of the threadpool, increasing
+ or decreasing the number of threads in use. We can do this
+ do this quite easily:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">suggestThreadPoolSize</span>(<span class="py-src-number">30</span>)
+</pre>
+
+ <p>The default size of the thread pool depends on the reactor being used;
+ the default reactor uses a minimum size of 5 and a maximum size of 10. Be
+ careful that you understand threads and their resource usage before
+ drastically altering the thread pool sizes.</p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/time.html b/doc/core/howto/time.html
new file mode 100644
index 0000000..010f296
--- /dev/null
+++ b/doc/core/howto/time.html
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Scheduling tasks for the future</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Scheduling tasks for the future</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+
+ <p>Let's say we want to run a task X seconds in the future.
+ The way to do that is defined in the reactor interface <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTime.html" title="twisted.internet.interfaces.IReactorTime">twisted.internet.interfaces.IReactorTime</a></code>:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">f</span>(<span class="py-src-parameter">s</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;this will run 3.5 seconds after it was scheduled: %s&quot;</span> % <span class="py-src-variable">s</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">3.5</span>, <span class="py-src-variable">f</span>, <span class="py-src-string">&quot;hello, world&quot;</span>)
+
+<span class="py-src-comment"># f() will only be called if the event loop is started.</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>If the result of the function is important or if it may be necessary
+ to handle exceptions it raises, then the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.deferLater.html" title="twisted.internet.task.deferLater">twisted.internet.task.deferLater</a></code> utility conveniently
+ takes care of creating a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> and setting up a delayed
+ call:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">task</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">f</span>(<span class="py-src-parameter">s</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;This will run 3.5 seconds after it was scheduled: %s&quot;</span> % <span class="py-src-variable">s</span>
+
+<span class="py-src-variable">d</span> = <span class="py-src-variable">task</span>.<span class="py-src-variable">deferLater</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">3.5</span>, <span class="py-src-variable">f</span>, <span class="py-src-string">&quot;hello, world&quot;</span>)
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">called</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">called</span>)
+
+<span class="py-src-comment"># f() will only be called if the event loop is started.</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>If we want a task to run every X seconds repeatedly, we can
+ use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.LoopingCall.html" title="twisted.internet.task.LoopingCall">twisted.internet.task.LoopingCall</a></code>:</p>
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">task</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">runEverySecond</span>():
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;a second has passed&quot;</span>
+
+<span class="py-src-variable">l</span> = <span class="py-src-variable">task</span>.<span class="py-src-variable">LoopingCall</span>(<span class="py-src-variable">runEverySecond</span>)
+<span class="py-src-variable">l</span>.<span class="py-src-variable">start</span>(<span class="py-src-number">1.0</span>) <span class="py-src-comment"># call every second</span>
+
+<span class="py-src-comment"># l.stop() will stop the looping calls</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>If we want to cancel a task that we've scheduled:</p>
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">f</span>():
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;I'll never run.&quot;</span>
+
+<span class="py-src-variable">callID</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">f</span>)
+<span class="py-src-variable">callID</span>.<span class="py-src-variable">cancel</span>()
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>As with all reactor-based code, in order for scheduling to work the reactor must be started using <code class="python">reactor.run()</code>.</p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/trial.html b/doc/core/howto/trial.html
new file mode 100644
index 0000000..e52d0c0
--- /dev/null
+++ b/doc/core/howto/trial.html
@@ -0,0 +1,2042 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Test-driven development with Twisted</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Test-driven development with Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introductory example of Python unit testing</a></li><li><a href="#auto1">Creating an API and writing tests</a></li><li><a href="#auto2">Making the tests pass</a></li><ul><li><a href="#auto3">Factoring out common test logic</a></li></ul><li><a href="#auto4">Twisted specific testing</a></li><li><a href="#auto5">Testing a protocol</a></li><ul><li><a href="#auto6">Creating and testing the server</a></li><li><a href="#auto7">Creating and testing the client</a></li></ul><li><a href="#auto8">More good practices</a></li><ul><li><a href="#auto9">Testing scheduling</a></li><li><a href="#auto10">Cleaning up after tests</a></li><li><a href="#auto11">Handling logged errors</a></li></ul><li><a href="#auto12">Resolve a bug</a></li><li><a href="#auto13">Code coverage</a></li><li><a href="#auto14">Conclusion</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<p>Writing good code is hard, or at least it can be. A major challenge is
+to ensure that your code remains correct as you add new functionality.</p>
+
+<p><a href="http://en.wikipedia.org/wiki/Unit_test" shape="rect">Unit testing</a> is a
+modern, light-weight testing methodology in widespread use in many
+programming languages. Development that relies on unit tests is often
+referred to as Test-Driven Development
+(<a href="http://en.wikipedia.org/wiki/Test-driven_development" shape="rect">TDD</a>).
+Most Twisted code is tested using TDD.</p>
+
+<p>To gain a solid understanding of unit testing in Python, you should read
+the <a href="http://docs.python.org/library/unittest.html" shape="rect">unittest --
+Unit testing framework chapter</a> of the <a href="http://docs.python.org/library/index.html" shape="rect">Python Library
+Reference</a>. There is also a ton of information available online and in
+books.</p>
+
+<h2>Introductory example of Python unit testing<a name="auto0"/></h2>
+
+<p>This document is principally a guide to Trial, Twisted's unit testing
+framework. Trial is based on Python's unit testing framework. While we do not
+aim to give a comprehensive guide to general Python unit testing, it will be
+helpful to consider a simple non-networked example before expanding to cover a
+networking code that requires the special capabilities of Trial. If you are
+already familiar with unit test in Python, jump straight to the section
+specific to <a href="#twisted" shape="rect">testing Twisted code</a>.</p>
+
+<p><div class="note"><strong>Note: </strong>In what follows we will make a series of refinements
+to some simple classes. In order to keep the examples and source code links
+complete and to allow you to run Trial on the intermediate results at every
+stage, I add <code>_N</code> (where the <code>N</code> are successive
+integers) to file names to keep them separate. This is a minor visual
+distraction that should be ignored.</div></p>
+
+<h2>Creating an API and writing tests<a name="auto1"/></h2>
+
+<p>We'll create a library for arithmetic calculation. First, create a
+project structure with a directory called <code class="shell">calculus</code> containing an empty <code class="py-filename">__init__.py</code> file.</p>
+
+<p>Then put the following simple class definition API into <code class="py-filename">calculus/base_1.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_base_1 -*-</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Calculation</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">pass</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">pass</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">pass</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">pass</span>
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/base_1.py"><span class="filename">listings/trial/calculus/base_1.py</span></a></div></div>
+
+<p>(Ignore the <code class="python">test-case-name</code> comment for
+now. You'll see why that's useful <a href="#comment" shape="rect">below</a>.)</p>
+
+<p>We've written the interface, but not the code. Now we'll write a set of
+tests. At this point of development, we'll be expecting all tests to
+fail. Don't worry, that's part of the point. Once we have a test framework
+functioning, and we have some decent tests written (and failing!), we'll go
+and do the actual development of our calculation API. This is the preferred
+way to work for many people using TDD - write tests first, make sure they
+fail, then do development. Others are not so strict and write tests after
+doing the development.</p>
+
+<p>Create a <code class="shell">test</code> directory beneath <code class="shell">calculus</code>, with an empty <code class="py-filename">__init__.py</code> file. In a <code class="py-filename">calculus/test/test_base_1.py</code>, put the
+following:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_1</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">add</span>(<span class="py-src-number">3</span>, <span class="py-src-number">8</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">11</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">subtract</span>(<span class="py-src-number">7</span>, <span class="py-src-number">3</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">4</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">multiply</span>(<span class="py-src-number">12</span>, <span class="py-src-number">5</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">60</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">divide</span>(<span class="py-src-number">12</span>, <span class="py-src-number">5</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">2</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_base_1.py"><span class="filename">listings/trial/calculus/test/test_base_1.py</span></a></div></div>
+
+<p>You should now have the following 4 files:
+
+<pre class="shell" xml:space="preserve">
+ calculus/__init__.py
+ calculus/base_1.py
+ calculus/test/__init__.py
+ calculus/test/test_base_1.py
+</pre>
+</p>
+
+<p>To run the tests, there are two things you must get set up. Make sure
+you get these both done - nothing below will work unless you do.</p>
+
+<p>First, make sure that the directory that <em>contains</em> your
+ <code class="shell">calculus</code> directory is in your Python load path. If you're
+using the Bash shell on some form of unix (e.g., Linux, Mac OS X), run
+ <code class="shell">PYTHONPATH=&quot;$PYTHONPATH:`pwd`/..&quot;</code> at
+the command line in the <code class="shell">calculus</code> directory. Once you have your
+Python path set up correctly, you should be able to run Python from the
+command line and <code class="python">import calculus</code> without seeing
+an import error.</p>
+
+<p>Second, make sure you can run the <code class="shell">trial</code>
+command. That is, make sure the directory containing the <code class="shell">trial</code>
+program on you system is in your shell's <code class="shell">PATH</code>. The easiest way to check if you have this is to
+try running <code class="shell">trial --help</code> at the command line. If
+you see a list of invocation options, you're in business. If your shell
+reports something like <code class="shell">trial: command not found</code>,
+make sure you have Twisted installed properly, and that the Twisted
+ <code class="shell">bin</code> directory is in your <code class="shell">PATH</code>. If
+you don't know how to do this, get some local help, or figure it out by
+searching online for information on setting and changing environment
+variables for you operating system.</p>
+
+<p>With those (one-time) preliminary steps out of the way, let's perform
+the tests. Run <code class="shell">trial calculus.test.test_base_1</code> from the
+command line from the <code class="shell">calculus</code> directory.
+
+You should see the following output (though your files are probably not in
+ <code class="shell">/tmp</code>:</p>
+
+<pre class="shell" xml:space="preserve">
+$ trial calculus.test.test_base_1
+calculus.test.test_base_1
+ CalculationTestCase
+ test_add ... [FAIL]
+ test_divide ... [FAIL]
+ test_multiply ... [FAIL]
+ test_subtract ... [FAIL]
+
+===============================================================================
+[FAIL]
+Traceback (most recent call last):
+ File &quot;/tmp/calculus/test/test_base_1.py&quot;, line 8, in test_add
+ self.assertEqual(result, 11)
+twisted.trial.unittest.FailTest: not equal:
+a = None
+b = 11
+
+
+calculus.test.test_base_1.CalculationTestCase.test_add
+===============================================================================
+[FAIL]
+Traceback (most recent call last):
+ File &quot;/tmp/calculus/test/test_base_1.py&quot;, line 23, in test_divide
+ self.assertEqual(result, 2)
+twisted.trial.unittest.FailTest: not equal:
+a = None
+b = 2
+
+
+calculus.test.test_base_1.CalculationTestCase.test_divide
+===============================================================================
+[FAIL]
+Traceback (most recent call last):
+ File &quot;/tmp/calculus/test/test_base_1.py&quot;, line 18, in test_multiply
+ self.assertEqual(result, 60)
+twisted.trial.unittest.FailTest: not equal:
+a = None
+b = 60
+
+
+calculus.test.test_base_1.CalculationTestCase.test_multiply
+===============================================================================
+[FAIL]
+Traceback (most recent call last):
+ File &quot;/tmp/calculus/test/test_base_1.py&quot;, line 13, in test_subtract
+ self.assertEqual(result, 4)
+twisted.trial.unittest.FailTest: not equal:
+a = None
+b = 4
+
+
+calculus.test.test_base_1.CalculationTestCase.test_subtract
+-------------------------------------------------------------------------------
+Ran 4 tests in 0.042s
+
+FAILED (failures=4)
+</pre>
+
+<p>How to interpret this output? You get a list of the individual tests, each
+followed by its result. By default, failures are printed at the end, but this
+can be changed with the <code class="shell">-e</code> (or <code class="shell">--rterrors</code>) option.</p>
+
+<p>One very useful thing in this output is the fully-qualified name of the
+failed tests. This appears at the bottom of each =-delimited area of the
+output. This allows you to copy and paste it to just run a single test you're
+interested in. In our example, you could run <code class="shell">trial
+calculus.test.test_base_1.CalculationTestCase.test_subtract</code> from the
+shell.</p>
+
+<p>Note that trial can use different reporters to modify its output. Run
+ <code class="shell">trial --help-reporters</code> to see a list of
+reporters.</p>
+
+<p>
+The tests can be run by <code class="shell">trial</code> in multiple ways:
+<ul>
+ <li><code class="shell">trial calculus</code>: run all the tests for the
+ calculus package.</li>
+
+ <li><code class="shell">trial calculus.test</code>: run using Python's
+ <code class="python">import</code> notation.</li>
+
+ <li><code class="shell">trial calculus.test.test_base_1</code>: as above, for
+ a specific test module. You can follow that logic by putting your class name
+ and even a method name to only run those specific tests.</li>
+
+ <li><a name="comment" shape="rect"/><code class="shell">trial
+ --testmodule=calculus/base_1.py</code>: use the <code class="python">test-case-name</code> comment in the first line of
+ <code class="py-filename">calculus/base_1.py</code> to find the tests.</li>
+
+ <li><code class="shell">trial calculus/test</code>: run all the tests in the
+ test directory (not recommended).</li>
+
+ <li><code class="shell">trial calculus/test/test_base_1.py</code>: run a
+ specific test file (not recommended).</li>
+</ul>
+
+The first 3 versions using full qualified names are strongly encouraged: they
+are much more reliable and they allow you to easily be more selective in your
+test runs.
+</p>
+
+<p>You'll notice that Trial create a <code class="shell">_trial_temp</code> directory in
+the directory where you run the tests. This has a file called
+ <code class="shell">test.log</code> which contains the log output of the tests (created
+using <code class="python">log.msg</code> or <code class="python">log.err</code> functions). Examine this file if you add
+logging to your tests.</p>
+
+<h2>Making the tests pass<a name="auto2"/></h2>
+
+<p>Now that we have a working test framework in place, and our tests are
+failing (as expected) we can go and try to implement the correct API. We'll do
+that in a new version of the above base_1
+module, <code class="py-filename">calculus/base_2.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_base_2 -*-</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Calculation</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> - <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> * <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> / <span class="py-src-variable">b</span>
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/base_2.py"><span class="filename">listings/trial/calculus/base_2.py</span></a></div></div>
+
+<p>We'll also create a new version of test_base_1 which imports and tests this
+new implementation,
+in <code class="py-filename">calculus/test_base_2.py</code>:</p>
+
+<p><div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_2</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">add</span>(<span class="py-src-number">3</span>, <span class="py-src-number">8</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">11</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">subtract</span>(<span class="py-src-number">7</span>, <span class="py-src-number">3</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">multiply</span>(<span class="py-src-number">12</span>, <span class="py-src-number">5</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">60</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">calc</span>.<span class="py-src-variable">divide</span>(<span class="py-src-number">12</span>, <span class="py-src-number">5</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-number">2</span>)
+</pre><div class="caption">test_base_2 - <a href="listings/trial/calculus/test/test_base_2.py"><span class="filename">listings/trial/calculus/test/test_base_2.py</span></a></div></div> is a copy of test_base_1, but with the import changed. Run <code class="shell">trial</code> again as above, and your tests should now pass:</p>
+
+<pre class="shell" xml:space="preserve">
+$ trial calculus.test.test_base_2
+
+Running 4 tests.
+calculus.test.test_base
+ CalculationTestCase
+ test_add ... [OK]
+ test_divide ... [OK]
+ test_multiply ... [OK]
+ test_subtract ... [OK]
+
+-------------------------------------------------------------------------------
+Ran 4 tests in 0.067s
+
+PASSED (successes=4)
+</pre>
+
+<h3>Factoring out common test logic<a name="auto3"/></h3>
+
+<p>You'll notice that our test file contains redundant code. Let's get rid
+of that. Python's unit testing framework allows your test class to define a
+ <code class="python">setUp</code> method that is called before
+ <em>each</em> test method in the class. This allows you to add attributes
+to <code class="python">self</code> that can be used in tests
+methods. We'll also add a parameterized test method to further simplify the
+code.</p>
+
+<p>Note that a test class may also provide the counterpart of <code class="python">setUp</code>, named <code class="python">tearDown</code>,
+which will be called after <em>each</em> test (whether successful or
+not). <code class="python">tearDown</code> is mainly used for post-test
+cleanup purposes. We will not use <code class="python">tearDown</code>
+until later.</p>
+
+<p>Create <code class="py-filename">calculus/test/test_base_2b.py</code> as
+follows:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_2</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">operation</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-variable">expected</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">add</span>, <span class="py-src-number">3</span>, <span class="py-src-number">8</span>, <span class="py-src-number">11</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">subtract</span>, <span class="py-src-number">7</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">multiply</span>, <span class="py-src-number">6</span>, <span class="py-src-number">9</span>, <span class="py-src-number">54</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">divide</span>, <span class="py-src-number">12</span>, <span class="py-src-number">5</span>, <span class="py-src-number">2</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_base_2b.py"><span class="filename">listings/trial/calculus/test/test_base_2b.py</span></a></div></div>
+
+<p>Much cleaner, no?</p>
+
+<p>We'll now add some additional error tests. Testing just for successful
+use of the API is generally not enough, especially if you expect your code
+to be used by others. Let's make sure the <code class="python">Calculation</code> class raises exceptions if someone tries
+to call its methods with arguments that cannot be converted to
+integers.</p>
+
+<p>We arrive at <code class="py-filename">calculus/test/test_base_3.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_3</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">operation</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">result</span>, <span class="py-src-variable">expected</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test_error</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertRaises</span>(<span class="py-src-variable">TypeError</span>, <span class="py-src-variable">operation</span>, <span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-number">2</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertRaises</span>(<span class="py-src-variable">TypeError</span>, <span class="py-src-variable">operation</span>, <span class="py-src-string">&quot;bar&quot;</span>, <span class="py-src-string">&quot;egg&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertRaises</span>(<span class="py-src-variable">TypeError</span>, <span class="py-src-variable">operation</span>, [<span class="py-src-number">3</span>], [<span class="py-src-number">8</span>, <span class="py-src-number">2</span>])
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertRaises</span>(<span class="py-src-variable">TypeError</span>, <span class="py-src-variable">operation</span>, {<span class="py-src-string">&quot;e&quot;</span>: <span class="py-src-number">3</span>}, {<span class="py-src-string">&quot;r&quot;</span>: <span class="py-src-string">&quot;t&quot;</span>})
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">add</span>, <span class="py-src-number">3</span>, <span class="py-src-number">8</span>, <span class="py-src-number">11</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">subtract</span>, <span class="py-src-number">7</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">multiply</span>, <span class="py-src-number">6</span>, <span class="py-src-number">9</span>, <span class="py-src-number">54</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">divide</span>, <span class="py-src-number">12</span>, <span class="py-src-number">5</span>, <span class="py-src-number">2</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_errorAdd</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test_error</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">add</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_errorSubtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test_error</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">subtract</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_errorMultiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test_error</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">multiply</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_errorDivide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_test_error</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>.<span class="py-src-variable">divide</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_base_3.py"><span class="filename">listings/trial/calculus/test/test_base_3.py</span></a></div></div>
+
+<p>We've added four new tests and one general-purpose function, <code class="python">_test_error</code>. This function uses the <code class="python">assertRaises</code> method, which takes an exception class,
+a function to run and its arguments, and checks that calling the function
+on the arguments does indeed raise the given exception.</p>
+
+<p>If you run the above, you'll see that not all tests fail. In Python it's
+often valid to add and multiply objects of different and even differing
+types, so the code in the add and mutiply tests does not raise an exception
+and those tests therefore fail. So let's add explicit type conversion to
+our API class. This brings us to <code class="py-filename">calculus/base_3.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_base_3 -*-</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Calculation</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_make_ints</span>(<span class="py-src-parameter">self</span>, *<span class="py-src-parameter">args</span>):
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">map</span>(<span class="py-src-variable">int</span>, <span class="py-src-variable">args</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">ValueError</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">TypeError</span>(<span class="py-src-string">&quot;Couldn't coerce arguments to integers: %s&quot;</span> % <span class="py-src-variable">args</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">_make_ints</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">_make_ints</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> - <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">_make_ints</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> * <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">_make_ints</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> / <span class="py-src-variable">b</span>
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/base_3.py"><span class="filename">listings/trial/calculus/base_3.py</span></a></div></div>
+
+<p>Here the <code class="python">_make_ints</code> helper function tries to
+convert a list into a list of equivalent integers, and raises a <code class="python">TypeError</code> in case the conversion goes wrong.
+
+<div class="note"><strong>Note: </strong>The <code class="python">int</code> conversion can also
+raise a <code class="python">TypeError</code> if passed something of the
+wrong type, such as a list. We'll just let that exception go by as <code class="python">TypeError</code> is already what we want in case something
+goes wrong.</div></p>
+
+
+<a name="twisted" shape="rect"/>
+<h2>Twisted specific testing<a name="auto4"/></h2>
+
+<p>Up to this point we've been doing fairly standard Python unit testing.
+With only a few cosmetic changes (most importantly, directly importing
+ <code class="python">unittest</code> instead of using Twisted's <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.trial.unittest.html" title="twisted.trial.unittest">unittest</a></code> version) we could make the
+above tests run using Python's standard library unit testing framework.</p>
+
+<p>Here we will assume a basic familiarity with Twisted's network I/O, timing,
+and Deferred APIs. If you haven't already read them, you should read the
+documentation on <a href="servers.html" shape="rect">Writing
+Servers</a>, <a href="clients.html" shape="rect">Writing Clients</a>,
+and <a href="defer.html" shape="rect">Deferreds</a>.</p>
+
+<p>Now we'll get to the real point of this tutorial and take advantage of
+Trial to test Twisted code.</p>
+
+<h2>Testing a protocol<a name="auto5"/></h2>
+
+<p>We'll now create a custom protocol to invoke our class from within a
+telnet-like session. We'll remotely call commands with arguments and read back
+the response. The goal will be to test our network code without creating
+sockets.</p>
+
+<h3>Creating and testing the server<a name="auto6"/></h3>
+
+<p>First we'll write the tests, and then explain what they do. The first
+version of the remote test code is:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">remote_1</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">test</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proto_helpers</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">RemoteCalculationFactory</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">buildProtocol</span>((<span class="py-src-string">'127.0.0.1'</span>, <span class="py-src-number">0</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span> = <span class="py-src-variable">proto_helpers</span>.<span class="py-src-variable">StringTransport</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">makeConnection</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">'%s %d %d\r\n'</span> % (<span class="py-src-variable">operation</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>()), <span class="py-src-variable">expected</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'add'</span>, <span class="py-src-number">7</span>, <span class="py-src-number">6</span>, <span class="py-src-number">13</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'subtract'</span>, <span class="py-src-number">82</span>, <span class="py-src-number">78</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'multiply'</span>, <span class="py-src-number">2</span>, <span class="py-src-number">8</span>, <span class="py-src-number">16</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'divide'</span>, <span class="py-src-number">14</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_remote_1.py"><span class="filename">listings/trial/calculus/test/test_remote_1.py</span></a></div></div>
+
+<p>To fully understand this client, it helps a lot to be comfortable with
+the Factory/Protocol/Transport pattern used in Twisted.</p>
+
+<p>We first create a protocol factory object. Note that we have yet to see
+the <code class="python">RemoteCalculationFactory</code> class. It is in
+ <code class="py-filename">calculus/remote_1.py</code> below. We
+call <code class="python">buildProtocol</code> to ask the factory to build us a
+protocol object that knows how to talk to our server. We then make a fake
+network transport, an instance of <code class="python">twisted.test.proto_helpers.StringTransport</code>
+class (note that test packages are generally not part of Twisted's public API;
+<code class="python">twisted.test.proto_helpers</code> is an exception). This fake
+transport is the key to the communications. It is used to emulate a network
+connection without a network. The address and port passed to <code>buildProtocol</code>
+are typically used by the factory to choose to immediately deny remote connections; since we're using a fake transport, we can choose any value that will be acceptable to the factory. In this case the factory just ignores the address, so we don't need to pick anything in particular.</p>
+
+<p>Testing protocols without the use of real network connections is both simple and recommended when testing Twisted
+code. Even though there are many tests in Twisted that use the network,
+most good tests don't. The problem with unit tests and networking is that
+networks aren't reliable. We cannot know that they will exhibit reasonable
+behavior all the time. This creates intermittent test failures due to
+network vagaries. Right now we're trying to test our Twisted code, not
+network reliability. By setting up and using a fake transport, we can
+write 100% reliable tests. We can also test network failures in a deterministic manner, another important part of your complete test suite.</p>
+
+<p>The final key to understanding this client code is the <code class="python">_test</code> method. The call to <code class="python">dataReceived</code> simulates data arriving on the network
+transport. But where does it arrive? It's handed to the <code class="python">lineReceived</code> method of the protocol instance (in
+ <code class="py-filename">calculus/remote_1.py</code> below). So the client
+is essentially tricking the server into thinking it has received the
+operation and the arguments over the network. The server (once again, see
+below) hands the work off to its <code class="python">CalculationProxy</code> object which in turn hands it to its
+ <code class="python">Calculation</code> instance. The result is written
+back via <code class="python">sendLine</code> (into the fake string
+transport object), and is then immediately available to the client, who
+fetches it with <code class="python">tr.value()</code> and checks that it
+has the expected value. So there's quite a lot going on behind the scenes
+in the two-line <code class="python">_test</code> method above.</p>
+
+<p><em>Finally</em>, let's see the implementation of this protocol. Put the
+following into <code class="py-filename">calculus/remote_1.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_remote_1 -*-</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_3</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationProxy</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">m</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">'add'</span>, <span class="py-src-string">'subtract'</span>, <span class="py-src-string">'multiply'</span>, <span class="py-src-string">'divide'</span>]:
+ <span class="py-src-variable">setattr</span>(<span class="py-src-variable">self</span>, <span class="py-src-string">'remote_%s'</span> % <span class="py-src-variable">m</span>, <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>, <span class="py-src-variable">m</span>))
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proxy</span> = <span class="py-src-variable">CalculationProxy</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">op</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>()
+ <span class="py-src-variable">a</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">a</span>)
+ <span class="py-src-variable">b</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">b</span>)
+ <span class="py-src-variable">op</span> = <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proxy</span>, <span class="py-src-string">'remote_%s'</span> % (<span class="py-src-variable">op</span>,))
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">op</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">str</span>(<span class="py-src-variable">result</span>))
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">RemoteCalculationProtocol</span>
+
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">stdout</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">RemoteCalculationFactory</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/remote_1.py"><span class="filename">listings/trial/calculus/remote_1.py</span></a></div></div>
+
+<p>As mentioned, this server creates a protocol that inherits from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.basic.LineReceiver.html" title="twisted.protocols.basic.LineReceiver">basic.LineReceiver</a></code>, and then a
+factory that uses it as protocol. The only trick is the <code class="python">CalculationProxy</code> object, which calls <code class="python">Calculation</code> methods through <code class="python">remote_*</code> methods. This pattern is used frequently in
+Twisted, because it is very explicit about what methods you are making
+accessible.</p>
+
+<p>If you run this test (<code class="shell">trial
+calculus.test.test_remote_1</code>), everything should be fine. You can also
+run a server to test it with a telnet client. To do that, call <code class="shell">python calculus/remote_1.py</code>. You should have the following output:</p>
+
+<pre class="shell" xml:space="preserve">
+2008-04-25 10:53:27+0200 [-] Log opened.
+2008-04-25 10:53:27+0200 [-] __main__.RemoteCalculationFactory starting on 46194
+2008-04-25 10:53:27+0200 [-] Starting factory &lt;__main__.RemoteCalculationFactory instance at 0x846a0cc&gt;
+</pre>
+
+<p>46194 is replaced by a random port. You can then call telnet on it:</p>
+<pre xml:space="preserve">
+$ telnet localhost 46194
+Trying 127.0.0.1...
+Connected to localhost.
+Escape character is '^]'.
+add 4123 9423
+13546
+</pre>
+
+<p>It works!</p>
+
+<h3>Creating and testing the client<a name="auto7"/></h3>
+
+<p>Of course, what we build is not particulary useful for now : we'll now build
+a client to our server, to be able to use it inside a Python program. And it
+will serve our next purpose.</p>
+
+<p>Create <code class="py-filename">calculus/test/test_client_1.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">client_1</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationClient</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">test</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proto_helpers</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span> = <span class="py-src-variable">proto_helpers</span>.<span class="py-src-variable">StringTransport</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span> = <span class="py-src-variable">RemoteCalculationClient</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">makeConnection</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>, <span class="py-src-variable">operation</span>)(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'%s %d %d\r\n'</span> % (<span class="py-src-variable">operation</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>, <span class="py-src-variable">expected</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">&quot;%d\r\n&quot;</span> % (<span class="py-src-variable">expected</span>,))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'add'</span>, <span class="py-src-number">7</span>, <span class="py-src-number">6</span>, <span class="py-src-number">13</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'subtract'</span>, <span class="py-src-number">82</span>, <span class="py-src-number">78</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'multiply'</span>, <span class="py-src-number">2</span>, <span class="py-src-number">8</span>, <span class="py-src-number">16</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'divide'</span>, <span class="py-src-number">14</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_client_1.py"><span class="filename">listings/trial/calculus/test/test_client_1.py</span></a></div></div>
+
+<p>It's really symmetric to the server test cases. The only tricky part is
+that we don't use a client factory. We're lazy, and it's not very useful in
+the client part, so we instantiate the protocol directly.</p>
+
+<p>Incidentally, we have introduced a very important concept here: the tests
+now return a Deferred object, and the assertion is done in a callback. The
+important thing to do here is to <strong>not forget to return the
+Deferred</strong>. If you do, your tests will pass even if nothing is asserted.
+That's also why it's important to make tests fail first: if your tests pass
+whereas you know they shouldn't, there is a problem in your tests.</p>
+
+<p>We'll now add the remote client class to produce <code class="py-filename">calculus/client_1.py</code>:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_client_1 -*-</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationClient</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span> = []
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">pop</span>(<span class="py-src-number">0</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">line</span>))
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_sendOperation</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">op</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">d</span>)
+ <span class="py-src-variable">line</span> = <span class="py-src-string">&quot;%s %d %d&quot;</span> % (<span class="py-src-variable">op</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">line</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;add&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;subtract&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;multiply&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;divide&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/client_1.py"><span class="filename">listings/trial/calculus/client_1.py</span></a></div></div>
+
+
+<h2>More good practices<a name="auto8"/></h2>
+
+<h3>Testing scheduling<a name="auto9"/></h3>
+
+<p>When testing code that involves the passage of time, waiting e.g. for a two hour timeout to occur in a test is not very realistic. Twisted provides a solution to this, the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.Clock.html" title="twisted.internet.task.Clock">Clock</a></code> class that allows one to simulate the passage of time.</p>
+
+<p>As an example we'll test the code for client request timeout: since our client
+uses TCP it can hang for a long time (firewall, connectivity problems, etc...).
+So generally we need to implement timeouts on the client side. Basically it's
+just that we send a request, don't receive a response and expect a timeout error
+to be triggered after a certain duration.
+</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">client_2</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationClient</span>, <span class="py-src-variable">ClientTimeoutError</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">task</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">test</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proto_helpers</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span> = <span class="py-src-variable">proto_helpers</span>.<span class="py-src-variable">StringTransportWithDisconnection</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span> = <span class="py-src-variable">task</span>.<span class="py-src-variable">Clock</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span> = <span class="py-src-variable">RemoteCalculationClient</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">callLater</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span>.<span class="py-src-variable">callLater</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">makeConnection</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>, <span class="py-src-variable">operation</span>)(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'%s %d %d\r\n'</span> % (<span class="py-src-variable">operation</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>, <span class="py-src-variable">expected</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">&quot;%d\r\n&quot;</span> % (<span class="py-src-variable">expected</span>,))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'add'</span>, <span class="py-src-number">7</span>, <span class="py-src-number">6</span>, <span class="py-src-number">13</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'subtract'</span>, <span class="py-src-number">82</span>, <span class="py-src-number">78</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'multiply'</span>, <span class="py-src-number">2</span>, <span class="py-src-number">8</span>, <span class="py-src-number">16</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'divide'</span>, <span class="py-src-number">14</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_timeout</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">add</span>(<span class="py-src-number">9</span>, <span class="py-src-number">4</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'add 9 4\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span>.<span class="py-src-variable">advance</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">timeOut</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">assertFailure</span>(<span class="py-src-variable">d</span>, <span class="py-src-variable">ClientTimeoutError</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_client_2.py"><span class="filename">listings/trial/calculus/test/test_client_2.py</span></a></div></div>
+
+<p>What happens here? We instantiate our protocol as usual, the only trick
+is to create the clock, and assign <code class="python">proto.callLater</code> to
+ <code class="python">clock.callLater</code>. Thus, every callLater calls in the protocol
+will finish before <code class="python">clock.advance()</code> returns.</p>
+
+<p>In the new test (test_timeout), we call <code class="python">clock.advance</code>, that simulates and advance in time
+(logically it's similar to a <code class="python">time.sleep</code> call). And
+we just have to verify that our Deferred got a timeout error.</p>
+
+<p>Let's implement that in our code.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_client_2 -*-</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>, <span class="py-src-variable">reactor</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientTimeoutError</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-keyword">pass</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationClient</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-variable">callLater</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>
+ <span class="py-src-variable">timeOut</span> = <span class="py-src-number">60</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span> = []
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">d</span>, <span class="py-src-variable">callID</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">pop</span>(<span class="py-src-number">0</span>)
+ <span class="py-src-variable">callID</span>.<span class="py-src-variable">cancel</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">line</span>))
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cancel</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">d</span>):
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">ClientTimeoutError</span>())
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_sendOperation</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">op</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">callID</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">timeOut</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_cancel</span>, <span class="py-src-variable">d</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">append</span>((<span class="py-src-variable">d</span>, <span class="py-src-variable">callID</span>))
+ <span class="py-src-variable">line</span> = <span class="py-src-string">&quot;%s %d %d&quot;</span> % (<span class="py-src-variable">op</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">line</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;add&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;subtract&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;multiply&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;divide&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/client_2.py"><span class="filename">listings/trial/calculus/client_2.py</span></a></div></div>
+
+<p>The only important thing here is to not forget to cancel our callLater
+when everything went fine.</p>
+
+<h3>Cleaning up after tests<a name="auto10"/></h3>
+
+<p>This chapter is mainly intended for people that want to have sockets or
+processes created in their tests. If it's still not obvious, you must try to
+avoid that like the plague, because it ends up with a lot of problems, one of
+them being intermittent failures. And intermittent failures are the plague
+of automated tests.</p>
+
+<p>To actually test that, we'll launch a server with our protocol.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">remote_1</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">client_2</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationClient</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>, <span class="py-src-variable">protocol</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteRunCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">RemoteCalculationFactory</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">port</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">factory</span>, <span class="py-src-variable">interface</span>=<span class="py-src-string">&quot;127.0.0.1&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span> = <span class="py-src-variable">None</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">tearDown</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">is</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">None</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">port</span>.<span class="py-src-variable">stopListening</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">op</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">creator</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ClientCreator</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">RemoteCalculationClient</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">cb</span>(<span class="py-src-parameter">client</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">client</span> = <span class="py-src-variable">client</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">client</span>, <span class="py-src-variable">op</span>)(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>
+ ).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>, <span class="py-src-variable">expected</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">creator</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">'127.0.0.1'</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">port</span>.<span class="py-src-variable">getHost</span>().<span class="py-src-variable">port</span>
+ ).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cb</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">&quot;add&quot;</span>, <span class="py-src-number">5</span>, <span class="py-src-number">9</span>, <span class="py-src-number">14</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">&quot;subtract&quot;</span>, <span class="py-src-number">47</span>, <span class="py-src-number">13</span>, <span class="py-src-number">34</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">&quot;multiply&quot;</span>, <span class="py-src-number">7</span>, <span class="py-src-number">3</span>, <span class="py-src-number">21</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">&quot;divide&quot;</span>, <span class="py-src-number">84</span>, <span class="py-src-number">10</span>, <span class="py-src-number">8</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_remote_2.py"><span class="filename">listings/trial/calculus/test/test_remote_2.py</span></a></div></div>
+
+<p>Recent versions of trial will fail loudly if you remove the
+ <code class="python">stopListening</code> call, which is good.</p>
+
+<p>Also, you should be aware that <code class="python">tearDown</code> will
+called in any case, after success or failure. So don't expect that every
+objects you created in the test method are present, because your tests may
+have failed in the middle.</p>
+
+<p>Trial also has a <code class="python">addCleanup</code> method, which makes
+these kind of cleanups easy and removes the need for <code class="python">tearDown
+</code>. For example, you could remove the code in <code class="python">_test</code>
+this way:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">RemoteCalculationFactory</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">port</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">factory</span>, <span class="py-src-variable">interface</span>=<span class="py-src-string">&quot;127.0.0.1&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">addCleanup</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">port</span>.<span class="py-src-variable">stopListening</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">op</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">creator</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ClientCreator</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">RemoteCalculationClient</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">cb</span>(<span class="py-src-parameter">client</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">addCleanup</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">client</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getattr</span>(<span class="py-src-variable">client</span>, <span class="py-src-variable">op</span>)(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>, <span class="py-src-variable">expected</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">creator</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">'127.0.0.1'</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">port</span>.<span class="py-src-variable">getHost</span>().<span class="py-src-variable">port</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cb</span>)
+</pre>
+
+<p>This remove the need of a tearDown method, and you don't have to check for
+the value of self.client: you only call addCleanup when the client is
+created.</p>
+
+<h3>Handling logged errors<a name="auto11"/></h3>
+
+<p>Currently, if you send an invalid command or invalid arguments to our
+server, it logs an exception and closes the connection. This is a perfectly
+valid behavior, but for the sake of this tutorial, we want to return an error
+to the user if he sends invalid operators, and log any errors on server side.
+So we'll want a test like this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">test_invalidParameters</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">'add foo bar\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">&quot;error\r\n&quot;</span>)
+</pre>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_remote_1 -*-</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">base_3</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Calculation</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CalculationProxy</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span> = <span class="py-src-variable">Calculation</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">m</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">'add'</span>, <span class="py-src-string">'subtract'</span>, <span class="py-src-string">'multiply'</span>, <span class="py-src-string">'divide'</span>]:
+ <span class="py-src-variable">setattr</span>(<span class="py-src-variable">self</span>, <span class="py-src-string">'remote_%s'</span> % <span class="py-src-variable">m</span>, <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">calc</span>, <span class="py-src-variable">m</span>))
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proxy</span> = <span class="py-src-variable">CalculationProxy</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">op</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>()
+ <span class="py-src-variable">op</span> = <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proxy</span>, <span class="py-src-string">'remote_%s'</span> % (<span class="py-src-variable">op</span>,))
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">op</span>(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">TypeError</span>:
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-string">&quot;error&quot;</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">str</span>(<span class="py-src-variable">result</span>))
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Factory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">RemoteCalculationProtocol</span>
+
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+ <span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">startLogging</span>(<span class="py-src-variable">sys</span>.<span class="py-src-variable">stdout</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">RemoteCalculationFactory</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/remote_2.py"><span class="filename">listings/trial/calculus/remote_2.py</span></a></div></div>
+
+<p>If you try something like that, it will not work. Here is the output you should have:</p>
+
+<pre class="shell" xml:space="preserve">
+trial calculus.test.test_remote_3.RemoteCalculationTestCase.test_invalidParameters
+calculus.test.test_remote_3
+ RemoteCalculationTestCase
+ test_invalidParameters ... [ERROR]
+
+===============================================================================
+[ERROR]: calculus.test.test_remote_3.RemoteCalculationTestCase.test_invalidParameters
+
+Traceback (most recent call last):
+ File &quot;/tmp/calculus/remote_2.py&quot;, line 27, in lineReceived
+ result = op(a, b)
+ File &quot;/tmp/calculus/base_3.py&quot;, line 11, in add
+ a, b = self._make_ints(a, b)
+ File &quot;/tmp/calculus/base_3.py&quot;, line 8, in _make_ints
+ raise TypeError
+exceptions.TypeError:
+-------------------------------------------------------------------------------
+Ran 1 tests in 0.004s
+
+FAILED (errors=1)
+</pre>
+
+<p>At first, you could think there is a problem, because you catch this
+exception. But in fact trial doesn't let you do that without controlling it:
+you must expect logged errors and clean them. To do that, you have to use the
+ <code class="python">flushLoggedErrors</code> method. You call it with the
+exception you expect, and it returns the list of exceptions logged since the
+start of the test. Generally, you'll want to check that this list has the
+expected length, or possibly that each exception has an expected message. We do
+the former in our test:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">remote_2</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationFactory</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">test</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proto_helpers</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">factory</span> = <span class="py-src-variable">RemoteCalculationFactory</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span> = <span class="py-src-variable">factory</span>.<span class="py-src-variable">buildProtocol</span>((<span class="py-src-string">'127.0.0.1'</span>, <span class="py-src-number">0</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span> = <span class="py-src-variable">proto_helpers</span>.<span class="py-src-variable">StringTransport</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">makeConnection</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">'%s %d %d\r\n'</span> % (<span class="py-src-variable">operation</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>()), <span class="py-src-variable">expected</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'add'</span>, <span class="py-src-number">7</span>, <span class="py-src-number">6</span>, <span class="py-src-number">13</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'subtract'</span>, <span class="py-src-number">82</span>, <span class="py-src-number">78</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'multiply'</span>, <span class="py-src-number">2</span>, <span class="py-src-number">8</span>, <span class="py-src-number">16</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'divide'</span>, <span class="py-src-number">14</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_invalidParameters</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">'add foo bar\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">&quot;error\r\n&quot;</span>)
+ <span class="py-src-variable">errors</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">flushLoggedErrors</span>(<span class="py-src-variable">TypeError</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">len</span>(<span class="py-src-variable">errors</span>), <span class="py-src-number">1</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_remote_3.py"><span class="filename">listings/trial/calculus/test/test_remote_3.py</span></a></div></div>
+
+<h2>Resolve a bug<a name="auto12"/></h2>
+
+<p>A bug was left over during the development of the timeout (probably several
+bugs, but that's not the point), concerning the reuse of the protocol when you
+got a timeout: the connection is not dropped, so you can get timeout forever.
+Generally an user will come to you saying &quot;I have this strange problem on
+my crappy network environment. It seems you could solve it with doing XXX at
+YYY.&quot;</p>
+
+<p>Actually, this bug can be corrected several ways. But if you correct it
+without adding tests, one day you'll face a big problem: regression.
+So the first step is adding a failing test.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calculus</span>.<span class="py-src-variable">client_3</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">RemoteCalculationClient</span>, <span class="py-src-variable">ClientTimeoutError</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">task</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">trial</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">unittest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">test</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proto_helpers</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientCalculationTestCase</span>(<span class="py-src-parameter">unittest</span>.<span class="py-src-parameter">TestCase</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUp</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span> = <span class="py-src-variable">proto_helpers</span>.<span class="py-src-variable">StringTransportWithDisconnection</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span> = <span class="py-src-variable">task</span>.<span class="py-src-variable">Clock</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span> = <span class="py-src-variable">RemoteCalculationClient</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">callLater</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span>.<span class="py-src-variable">callLater</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">makeConnection</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_test</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">operation</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>, <span class="py-src-parameter">expected</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">getattr</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>, <span class="py-src-variable">operation</span>)(<span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'%s %d %d\r\n'</span> % (<span class="py-src-variable">operation</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>))
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>, <span class="py-src-variable">expected</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">dataReceived</span>(<span class="py-src-string">&quot;%d\r\n&quot;</span> % (<span class="py-src-variable">expected</span>,))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_add</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'add'</span>, <span class="py-src-number">7</span>, <span class="py-src-number">6</span>, <span class="py-src-number">13</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_subtract</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'subtract'</span>, <span class="py-src-number">82</span>, <span class="py-src-number">78</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_multiply</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'multiply'</span>, <span class="py-src-number">2</span>, <span class="py-src-number">8</span>, <span class="py-src-number">16</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_divide</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_test</span>(<span class="py-src-string">'divide'</span>, <span class="py-src-number">14</span>, <span class="py-src-number">3</span>, <span class="py-src-number">4</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_timeout</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">add</span>(<span class="py-src-number">9</span>, <span class="py-src-number">4</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'add 9 4\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span>.<span class="py-src-variable">advance</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">timeOut</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">assertFailure</span>(<span class="py-src-variable">d</span>, <span class="py-src-variable">ClientTimeoutError</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">test_timeoutConnectionLost</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">called</span> = []
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lost</span>(<span class="py-src-parameter">arg</span>):
+ <span class="py-src-variable">called</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">True</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">connectionLost</span> = <span class="py-src-variable">lost</span>
+
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">add</span>(<span class="py-src-number">9</span>, <span class="py-src-number">4</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">tr</span>.<span class="py-src-variable">value</span>(), <span class="py-src-string">'add 9 4\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">clock</span>.<span class="py-src-variable">advance</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">proto</span>.<span class="py-src-variable">timeOut</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">check</span>(<span class="py-src-parameter">ignore</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">assertEqual</span>(<span class="py-src-variable">called</span>, [<span class="py-src-variable">True</span>])
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">assertFailure</span>(<span class="py-src-variable">d</span>, <span class="py-src-variable">ClientTimeoutError</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">check</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/test/test_client_3.py"><span class="filename">listings/trial/calculus/test/test_client_3.py</span></a></div></div>
+<p>What have we done here ?
+<ul>
+ <li>We switched to StringTransportWithDisconnection. This transport manages
+ <code class="python">loseConnection</code> and forwards it to its protocol.</li>
+ <li>We assign the protocol to the transport via the <code class="python">protocol
+ </code> attribute.</li>
+ <li>We check that after a timeout our connection has closed.</li>
+</ul>
+</p>
+
+<p>For doing that, we then use the <code class="python">TimeoutMixin</code>
+class, that does almost everything we want. The great thing is that it almost
+changes nothing to our class.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+</p><span class="py-src-comment"># -*- test-case-name: calculus.test.test_client -*-</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>, <span class="py-src-variable">policies</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClientTimeoutError</span>(<span class="py-src-parameter">Exception</span>):
+ <span class="py-src-keyword">pass</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">RemoteCalculationClient</span>(<span class="py-src-parameter">object</span>, <span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>, <span class="py-src-parameter">policies</span>.<span class="py-src-parameter">TimeoutMixin</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span> = []
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_timeOut</span> = <span class="py-src-number">60</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">setTimeout</span>(<span class="py-src-variable">None</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">pop</span>(<span class="py-src-number">0</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">line</span>))
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">timeoutConnection</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">d</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>:
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">ClientTimeoutError</span>())
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_sendOperation</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">op</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">results</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">d</span>)
+ <span class="py-src-variable">line</span> = <span class="py-src-string">&quot;%s %d %d&quot;</span> % (<span class="py-src-variable">op</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sendLine</span>(<span class="py-src-variable">line</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">setTimeout</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_timeOut</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;add&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">subtract</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;subtract&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">multiply</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;multiply&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">divide</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_sendOperation</span>(<span class="py-src-string">&quot;divide&quot;</span>, <span class="py-src-variable">a</span>, <span class="py-src-variable">b</span>)
+</pre><div class="caption">Source listing - <a href="listings/trial/calculus/client_3.py"><span class="filename">listings/trial/calculus/client_3.py</span></a></div></div>
+
+<h2>Code coverage<a name="auto13"/></h2>
+
+<p>Code coverage is one of the aspects of software testing that shows how much
+your tests cross (cover) the code of your program. There are different kind of
+measures: path coverage, condition coverage, statement coverage... We'll only
+consider statement coverage here, whether a line has been executed or not.
+</p>
+
+<p>Trial has an option to generate the statement coverage of your tests.
+This option is --coverage. It creates a coverage directory in _trial_temp,
+with a file .cover for every modules used during the tests. The ones
+interesting for us are calculus.base.cover and calculus.remote.cover. In
+front of each line is the number of times you went through during the
+tests, or the marker '&gt;&gt;&gt;&gt;&gt;&gt;' if the line was not
+covered. If you went through all the tutorial to this point, you should
+have complete coverage :).</p>
+
+<p>Again, this is only another useful pointer, but it doesn't mean your
+code is perfect: your tests should consider every possibile input and
+output, to get <strong>full</strong> coverage (condition, path, etc.) as well
+.</p>
+
+<h2>Conclusion<a name="auto14"/></h2>
+
+<p>So what did you learn in this document?
+<ul>
+ <li>How to use the trial command-line tool to run your tests</li>
+ <li>How to use string transports to test individual clients and servers
+ without creating sockets</li>
+ <li>If you really want to create sockets, how to cleanly do it so that it
+ doesn't have bad side effects</li>
+ <li>And some small tips you can't live without.</li>
+</ul>
+If one of the topics still looks cloudy to you, please give us your feedback!
+You can file tickets to improve this document
+<a href="http://twistedmatrix.com/" shape="rect">on the Twisted web site</a>.
+</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/backends.html b/doc/core/howto/tutorial/backends.html
new file mode 100644
index 0000000..f29533e
--- /dev/null
+++ b/doc/core/howto/tutorial/backends.html
@@ -0,0 +1,1348 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: pluggable backends</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: pluggable backends</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Another Back-end</a></li><li><a href="#auto2">Yet Another Back-end: Doing the Standard Thing</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the fifth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part we will add new several new backends to our finger service using
+the component-based architecture developed in <a href="components.html" shape="rect">The
+Evolution of Finger: moving to a component based architecture</a>. This will
+show just how convenient it is to implement new back-ends when we move to a
+component based architecture. Note that here we also use an interface we
+previously wrote, <code>FingerSetterFactory</code>, by supporting one single
+method. We manage to preserve the service's ignorance of the network.</p>
+
+<h2>Another Back-end<a name="auto1"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">pwd</span>
+
+<span class="py-src-comment"># Another back-end</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LocalFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-comment"># need a local finger daemon running for this to work</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessOutput</span>(<span class="py-src-string">&quot;finger&quot;</span>, [<span class="py-src-variable">user</span>])
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>([])
+
+
+<span class="py-src-variable">f</span> = <span class="py-src-variable">LocalFingerService</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19b_changes.py"><span class="filename">listings/finger/finger19b_changes.py</span></a></div></div>
+<p>
+Full source code here: <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">pwd</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">formatUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">l</span> = [<span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;ul&gt;'</span>+<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">l</span>)+<span class="py-src-string">'&lt;/ul&gt;'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">formatUsers</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>==<span class="py-src-string">&quot;&quot;</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>:
+ <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;'</span>%<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">'&lt;p&gt;%s&lt;/p&gt;'</span>%<span class="py-src-variable">m</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-comment"># Another back-end</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LocalFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-comment"># need a local finger daemon running for this to work</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessOutput</span>(<span class="py-src-string">&quot;finger&quot;</span>, [<span class="py-src-variable">user</span>])
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>([])
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">LocalFingerService</span>()
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19b.tac"><span class="filename">listings/finger/finger19b.tac</span></a></div></div>
+</p>
+
+<p>We've already written this, but now we get more for less work:
+the network code is completely separate from the back-end.</p>
+
+
+<h2>Yet Another Back-end: Doing the Standard Thing<a name="auto2"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">pwd</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+
+
+<span class="py-src-comment"># Yet another back-end</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LocalFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">entry</span> = <span class="py-src-variable">pwd</span>.<span class="py-src-variable">getpwnam</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">KeyError</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-string">&quot;No such user&quot;</span>)
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">file</span>(<span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">entry</span>[<span class="py-src-number">5</span>],<span class="py-src-string">'.plan'</span>))
+ <span class="py-src-keyword">except</span> (<span class="py-src-variable">IOError</span>, <span class="py-src-variable">OSError</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-string">&quot;No such user&quot;</span>)
+ <span class="py-src-variable">data</span> = <span class="py-src-variable">f</span>.<span class="py-src-variable">read</span>()
+ <span class="py-src-variable">data</span> = <span class="py-src-variable">data</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">close</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">data</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>([])
+
+
+
+<span class="py-src-variable">f</span> = <span class="py-src-variable">LocalFingerService</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19c_changes.py"><span class="filename">listings/finger/finger19c_changes.py</span></a></div></div>
+<p>
+Full source code here: <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">pwd</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>():
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">formatUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">l</span> = [<span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;ul&gt;'</span>+<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">l</span>)+<span class="py-src-string">'&lt;/ul&gt;'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">formatUsers</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>==<span class="py-src-string">&quot;&quot;</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>:
+ <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;'</span>%<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">'&lt;p&gt;%s&lt;/p&gt;'</span>%<span class="py-src-variable">m</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-comment"># Yet another back-end</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">LocalFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">entry</span> = <span class="py-src-variable">pwd</span>.<span class="py-src-variable">getpwnam</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">KeyError</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-string">&quot;No such user&quot;</span>)
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">file</span>(<span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">entry</span>[<span class="py-src-number">5</span>],<span class="py-src-string">'.plan'</span>))
+ <span class="py-src-keyword">except</span> (<span class="py-src-variable">IOError</span>, <span class="py-src-variable">OSError</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-string">&quot;No such user&quot;</span>)
+ <span class="py-src-variable">data</span> = <span class="py-src-variable">f</span>.<span class="py-src-variable">read</span>()
+ <span class="py-src-variable">data</span> = <span class="py-src-variable">data</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">close</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">data</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>([])
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">LocalFingerService</span>()
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19c.tac"><span class="filename">listings/finger/finger19c.tac</span></a></div></div>
+</p>
+
+<p>Not much to say except that now we can be churn out backends like crazy. Feel
+like doing a back-end for <a href="http://www.advogato.org/" shape="rect">Advogato</a>, for
+example? Dig out the XML-RPC client support Twisted has, and get to work!</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/client.html b/doc/core/howto/tutorial/client.html
new file mode 100644
index 0000000..8d1c4b8
--- /dev/null
+++ b/doc/core/howto/tutorial/client.html
@@ -0,0 +1,260 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: a Twisted finger client</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: a Twisted finger client</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Finger Proxy</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the ninth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we develop a client for the finger server: a proxy finger
+server which forwards requests to another finger server.</p>
+
+<h2>Finger Proxy<a name="auto1"/></h2>
+
+<p>Writing new clients with Twisted is much like writing new servers.
+We implement the protocol, which just gathers up all the data, and
+give it to the factory. The factory keeps a deferred which is triggered
+if the connection either fails or succeeds. When we use the client,
+we first make sure the deferred will never fail, by producing a message
+in that case. Implementing a wrapper around client which just returns
+the deferred is a common pattern. While less flexible than
+using the factory directly, it's also more convenient.</p>
+
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+</p><span class="py-src-comment"># finger proxy</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">defer</span>, <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a list of strings&quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a protocol returning a string&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerClient</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Protocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">&quot;\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">buf</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">buf</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">data</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">gotData</span>(<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">buf</span>))
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerClientFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerClient</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span> = <span class="py-src-variable">defer</span>.<span class="py-src-variable">Deferred</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">clientConnectionFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">_</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>.<span class="py-src-variable">errback</span>(<span class="py-src-variable">reason</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">gotData</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">d</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">data</span>)
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">finger</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">host</span>, <span class="py-src-parameter">port</span>=<span class="py-src-number">79</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">FingerClientFactory</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>, <span class="py-src-variable">f</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>.<span class="py-src-variable">d</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ProxyFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">host</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'@'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-keyword">except</span>:
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">host</span> = <span class="py-src-string">'127.0.0.1'</span>
+ <span class="py-src-variable">ret</span> = <span class="py-src-variable">finger</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">host</span>)
+ <span class="py-src-variable">ret</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-string">&quot;Could not connect to remote host&quot;</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">ret</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>([])
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">ProxyFingerService</span>()
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">7779</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)).<span class="py-src-variable">setServiceParent</span>(
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/finger/fingerproxy.tac"><span class="filename">listings/finger/fingerproxy.tac</span></a></div></div>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/components.html b/doc/core/howto/tutorial/components.html
new file mode 100644
index 0000000..a2d3586
--- /dev/null
+++ b/doc/core/howto/tutorial/components.html
@@ -0,0 +1,1132 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: moving to a component based architecture</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: moving to a component based architecture</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Write Maintainable Code</a></li><li><a href="#auto2">Advantages of Latest Version</a></li><li><a href="#auto3">Aspect-Oriented Programming</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the fourth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this section of the tutorial, we'll move our code to a component
+architecture so that adding new features is trivial.
+See <a href="../components.html" shape="rect">Interfaces and Adapters</a> for a more
+complete discussion of components.</p>
+
+<h2>Write Maintainable Code<a name="auto1"/></h2>
+
+
+<p>In the last version, the service class was three times longer than any other
+class, and was hard to understand. This was because it turned out to have
+multiple responsibilities. It had to know how to access user information, by
+rereading the file every half minute, but also how to display itself in a myriad
+of protocols. Here, we used the component-based architecture that Twisted
+provides to achieve a separation of concerns. All the service is responsible
+for, now, is supporting <code>getUser</code>/<code>getUsers</code>. It declares
+its support via a call to <code>zope.interface.implements</code>. Then, adapters
+are used to make this service look like an appropriate class for various things:
+for supplying a finger factory to <code>TCPServer</code>, for supplying a
+resource to site's constructor, and to provide an IRC client factory
+for <code>TCPClient</code>. All the adapters use are the methods
+in <code>FingerService</code> they are declared to use:
+<code>getUser</code>/<code>getUsers</code>. We could, of course, skip the
+interfaces and let the configuration code use things
+like <code>FingerFactoryFromService(f)</code> directly. However, using
+interfaces provides the same flexibility inheritance gives: future subclasses
+can override the adapters.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">formatUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">l</span> = [<span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;ul&gt;'</span>+<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">l</span>)+<span class="py-src-string">'&lt;/ul&gt;'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">formatUsers</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>==<span class="py-src-string">&quot;&quot;</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>:
+ <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;'</span>%<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">'&lt;p&gt;%s&lt;/p&gt;'</span>%<span class="py-src-variable">m</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19.tac"><span class="filename">listings/finger/finger19.tac</span></a></div></div>
+
+<h2>Advantages of Latest Version<a name="auto2"/></h2>
+
+<ul>
+<li>Readable -- each class is short</li>
+<li>Maintainable -- each class knows only about interfaces</li>
+<li>Dependencies between code parts are minimized</li>
+<li>Example: writing a new <code>IFingerService</code> is easy</li>
+</ul>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Set the user's status to something&quot;&quot;&quot;</span>
+
+<span class="py-src-comment"># Advantages of latest version</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MemoryFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>([<span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">IFingerSetterService</span>])
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+
+
+<span class="py-src-variable">f</span> = <span class="py-src-variable">MemoryFingerService</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">IFingerSetterFactory</span>(<span class="py-src-variable">f</span>), <span class="py-src-variable">interface</span>=<span class="py-src-string">'127.0.0.1'</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19a_changes.py"><span class="filename">listings/finger/finger19a_changes.py</span></a></div></div>
+<p>
+Full source code here: <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a list of strings&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Set the user's status to something&quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a protocol returning a string&quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a protocol returning a string&quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a deferred returning a string&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return a protocol&quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">formatUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">l</span> = [<span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;ul&gt;'</span>+<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">l</span>)+<span class="py-src-string">'&lt;/ul&gt;'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">formatUsers</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>==<span class="py-src-string">&quot;&quot;</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>:
+ <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;'</span>%<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">'&lt;p&gt;%s&lt;/p&gt;'</span>%<span class="py-src-variable">m</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MemoryFingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>([<span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">IFingerSetterService</span>])
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">MemoryFingerService</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">IFingerSetterFactory</span>(<span class="py-src-variable">f</span>), <span class="py-src-variable">interface</span>=<span class="py-src-string">'127.0.0.1'</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger19a.tac"><span class="filename">listings/finger/finger19a.tac</span></a></div></div>
+</p>
+
+<h2>Aspect-Oriented Programming<a name="auto3"/></h2>
+
+<p>At last, an example of aspect-oriented programming that isn't about logging
+or timing. This code is actually useful! Watch how aspect-oriented programming
+helps you write less code and have fewer dependencies!
+</p>
+
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/configuration.html b/doc/core/howto/tutorial/configuration.html
new file mode 100644
index 0000000..e905587
--- /dev/null
+++ b/doc/core/howto/tutorial/configuration.html
@@ -0,0 +1,870 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: configuration and packaging of the finger service</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: configuration and packaging of the finger service</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Plugins</a></li><li><a href="#auto2">OS Integration</a></li><ul><li><a href="#auto3">Debian</a></li><li><a href="#auto4">Red Hat / Mandrake</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the eleventh part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we make it easier for non-programmers to configure a finger
+server and show how to package it in the .deb and RPM package formats. Plugins
+are discussed further in the <a href="../plugin.html" shape="rect">Twisted Plugin System</a>
+howto. Writing twistd plugins is covered in <a href="../tap.html" shape="rect">Writing a
+twistd Plugin</a>, and .tac applications are covered in <a href="../application.html" shape="rect">Using the Twisted Application Framework</a>.</p>
+
+<h2>Plugins<a name="auto1"/></h2>
+
+<p>So far, the user had to be somewhat of a programmer to be able to configure
+stuff. Maybe we can eliminate even that? Move old code
+to <code>finger/__init__.py</code> and...</p>
+<p>
+Full source code for finger module here: <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+</p><span class="py-src-comment"># finger.py module</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>, <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">template</span> = <span class="py-src-string">&quot;&quot;&quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;Users&lt;/title&gt;&lt;/head&gt;&lt;body&gt;
+ &lt;h1&gt;Users&lt;/h1&gt;
+ &lt;ul&gt;
+ %(users)s
+ &lt;/ul&gt;
+ &lt;/body&gt;
+ &lt;/html&gt;&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span> == <span class="py-src-string">''</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>
+ <span class="py-src-keyword">elif</span> <span class="py-src-variable">path</span> == <span class="py-src-string">'RPC2'</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">users</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">cbUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">template</span> % {<span class="py-src-string">'users'</span>: <span class="py-src-string">''</span>.<span class="py-src-variable">join</span>([
+ <span class="py-src-comment"># Name should be quoted properly these uses.</span>
+ <span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">name</span>, <span class="py-src-variable">name</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">name</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>])})
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+ <span class="py-src-variable">users</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbUsers</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">ebUsers</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>(<span class="py-src-variable">err</span>, <span class="py-src-string">&quot;UserStatusTree failed&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+ <span class="py-src-variable">users</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">ebUsers</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">template</span>=<span class="py-src-string">'''&lt;html&gt;&lt;head&gt;&lt;title&gt;%(title)s&lt;/title&gt;&lt;/head&gt;
+ &lt;body&gt;&lt;h1&gt;%(name)s&lt;/h1&gt;&lt;p&gt;%(status)s&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;'''</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">cbStatus</span>(<span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">template</span> % {
+ <span class="py-src-string">'title'</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>,
+ <span class="py-src-string">'name'</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>,
+ <span class="py-src-string">'status'</span>: <span class="py-src-variable">status</span>})
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+ <span class="py-src-variable">status</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbStatus</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">ebStatus</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>(<span class="py-src-variable">err</span>, <span class="py-src-string">&quot;UserStatus failed&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+ <span class="py-src-variable">status</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">ebStatus</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IPerspectiveFinger</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">username</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PerspectiveFingerFromService</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPerspectiveFinger</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">username</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">PerspectiveFingerFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IPerspectiveFinger</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ServerContextFactory</span>:
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getContext</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">SSL</span>.<span class="py-src-variable">Context</span>(<span class="py-src-variable">SSL</span>.<span class="py-src-variable">SSLv23_METHOD</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_certificate_file</span>(<span class="py-src-string">'server.pem'</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_privatekey_file</span>(<span class="py-src-string">'server.pem'</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">ctx</span>
+
+
+
+<span class="py-src-comment"># Easy configuration</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">makeService</span>(<span class="py-src-parameter">config</span>):
+ <span class="py-src-comment"># finger on port 79</span>
+ <span class="py-src-variable">s</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">MultiService</span>()
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-variable">config</span>[<span class="py-src-string">'file'</span>])
+ <span class="py-src-variable">h</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>))
+ <span class="py-src-variable">h</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+
+ <span class="py-src-comment"># website on port 8000</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>)
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">templateDirectory</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'templates'</span>]
+ <span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>)
+ <span class="py-src-variable">j</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">site</span>)
+ <span class="py-src-variable">j</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># ssl on port 443</span>
+<span class="py-src-comment"># if config.get('ssl'):</span>
+<span class="py-src-comment"># k = internet.SSLServer(443, site, ServerContextFactory())</span>
+<span class="py-src-comment"># k.setServiceParent(s)</span>
+
+ <span class="py-src-comment"># irc fingerbot</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-string">'ircnick'</span>):
+ <span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+ <span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'ircnick'</span>]
+ <span class="py-src-variable">ircserver</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'ircserver'</span>]
+ <span class="py-src-variable">b</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-variable">ircserver</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>)
+ <span class="py-src-variable">b</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># Pespective Broker on port 8889</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-string">'pbport'</span>):
+ <span class="py-src-variable">m</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(
+ <span class="py-src-variable">int</span>(<span class="py-src-variable">config</span>[<span class="py-src-string">'pbport'</span>]),
+ <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">IPerspectiveFinger</span>(<span class="py-src-variable">f</span>)))
+ <span class="py-src-variable">m</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">s</span>
+</pre><div class="caption">finger module - <a href="listings/finger/finger/finger.py"><span class="filename">listings/finger/finger/finger.py</span></a></div></div>
+</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-comment"># finger/tap.py</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">interfaces</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">usage</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">finger</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Options</span>(<span class="py-src-parameter">usage</span>.<span class="py-src-parameter">Options</span>):
+
+ <span class="py-src-variable">optParameters</span> = [
+ [<span class="py-src-string">'file'</span>, <span class="py-src-string">'f'</span>, <span class="py-src-string">'/etc/users'</span>],
+ [<span class="py-src-string">'templates'</span>, <span class="py-src-string">'t'</span>, <span class="py-src-string">'/usr/share/finger/templates'</span>],
+ [<span class="py-src-string">'ircnick'</span>, <span class="py-src-string">'n'</span>, <span class="py-src-string">'fingerbot'</span>],
+ [<span class="py-src-string">'ircserver'</span>, <span class="py-src-variable">None</span>, <span class="py-src-string">'irc.freenode.net'</span>],
+ [<span class="py-src-string">'pbport'</span>, <span class="py-src-string">'p'</span>, <span class="py-src-number">8889</span>],
+ ]
+
+ <span class="py-src-variable">optFlags</span> = [[<span class="py-src-string">'ssl'</span>, <span class="py-src-string">'s'</span>]]
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">makeService</span>(<span class="py-src-parameter">config</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">finger</span>.<span class="py-src-variable">makeService</span>(<span class="py-src-variable">config</span>)
+</pre><div class="caption">finger/tap.py - <a href="listings/finger/finger/tap.py"><span class="filename">listings/finger/finger/tap.py</span></a></div></div>
+
+<p>And register it all:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span>.<span class="py-src-variable">service</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ServiceMaker</span>
+
+<span class="py-src-variable">finger</span> = <span class="py-src-variable">ServiceMaker</span>(
+ <span class="py-src-string">'finger'</span>, <span class="py-src-string">'finger.tap'</span>, <span class="py-src-string">'Run a finger service'</span>, <span class="py-src-string">'finger'</span>)
+</pre><div class="caption">
+twisted/plugins/finger_tutorial.py
+ - <a href="listings/finger/twisted/plugins/finger_tutorial.py"><span class="filename">listings/finger/twisted/plugins/finger_tutorial.py</span></a></div></div>
+
+<p>Note that the second argument to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.ServiceMaker.html" title="twisted.application.service.ServiceMaker">ServiceMaker</a></code>,
+<code>finger.tap</code>, is a reference to a module
+(<code>finger/tap.py</code>), not to a filename.</p>
+
+<p>And now, the following works</p>
+
+<pre class="shell" xml:space="preserve">
+% sudo twistd -n finger --file=/etc/users --ircnick=fingerbot
+</pre>
+
+<p>
+ For more details about this, see the <a href="../tap.html" shape="rect">twistd plugin
+ documentation</a>.
+</p>
+
+<h2>OS Integration<a name="auto2"/></h2>
+
+<p>If we already have the <q>finger</q> package installed in
+ <code>PYTHONPATH</code> (e.g. we added it to <code>site-packages</code>), we
+can achieve easy integration:</p>
+
+<h3>Debian<a name="auto3"/></h3>
+
+<pre class="shell" xml:space="preserve">
+% tap2deb --unsigned -m &quot;Foo &lt;foo@example.com&gt;&quot; --type=python finger.tac
+% sudo dpkg -i .build/*.deb
+</pre>
+
+<h3>Red Hat / Mandrake<a name="auto4"/></h3>
+
+<pre class="shell" xml:space="preserve">
+% tap2rpm --type=python finger.tac
+% sudo rpm -i *.rpm
+</pre>
+
+<p>These packages will properly install and register <code>init.d</code>
+scripts, etc. for the given file.</p>
+
+<p>If it doesn't work on your favorite OS: patches accepted!</p>
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/factory.html b/doc/core/howto/tutorial/factory.html
new file mode 100644
index 0000000..cc31631
--- /dev/null
+++ b/doc/core/howto/tutorial/factory.html
@@ -0,0 +1,713 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: using a single factory for
+ multiple protocols</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: using a single factory for
+ multiple protocols</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Support HTTPS</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the eighth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we add HTTPS support to our web frontend, showing how to have a
+single factory listen on multiple ports. More information on using SSL in
+Twisted can be found in the <a href="../ssl.html" shape="rect">SSL howto</a>.</p>
+
+<h2>Support HTTPS<a name="auto1"/></h2>
+
+<p>All we need to do to code an HTTPS site is just write a context factory (in
+this case, which loads the certificate from a certain file) and then use the
+twisted.application.internet.SSLServer method. Note that one factory (in this
+case, a site) can listen on multiple ports with multiple protocols.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">microdom</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">OpenSSL</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">SSL</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>=<span class="py-src-variable">service</span>
+
+ <span class="py-src-comment"># add a specific child for the path &quot;RPC2&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;RPC2&quot;</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-comment"># need to do this for resources at the root of the site</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;&quot;</span>, <span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">users</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">userOutput</span> = <span class="py-src-string">''</span>.<span class="py-src-variable">join</span>([<span class="py-src-string">&quot;&lt;li&gt;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;&quot;</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>])
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;
+ &lt;html&gt;&lt;head&gt;&lt;title&gt;Users&lt;/title&gt;&lt;/head&gt;&lt;body&gt;
+ &lt;h1&gt;Users&lt;/h1&gt;
+ &lt;ul&gt;
+ %s
+ &lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % <span class="py-src-variable">userOutput</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">user</span>=<span class="py-src-variable">path</span>, <span class="py-src-variable">service</span>=<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">status</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;%s&lt;/title&gt;&lt;/head&gt;
+ &lt;body&gt;&lt;h1&gt;%s&lt;/h1&gt;
+ &lt;p&gt;%s&lt;/p&gt;
+ &lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>))
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IPerspectiveFinger</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">username</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PerspectiveFingerFromService</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPerspectiveFinger</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">username</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">PerspectiveFingerFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IPerspectiveFinger</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ServerContextFactory</span>:
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getContext</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">ctx</span> = <span class="py-src-variable">SSL</span>.<span class="py-src-variable">Context</span>(<span class="py-src-variable">SSL</span>.<span class="py-src-variable">SSLv23_METHOD</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_certificate_file</span>(<span class="py-src-string">'server.pem'</span>)
+ <span class="py-src-variable">ctx</span>.<span class="py-src-variable">use_privatekey_file</span>(<span class="py-src-string">'server.pem'</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">ctx</span>
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">site</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">SSLServer</span>(<span class="py-src-number">443</span>, <span class="py-src-variable">site</span>, <span class="py-src-variable">ServerContextFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8889</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">IPerspectiveFinger</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger22.py"><span class="filename">listings/finger/finger22.py</span></a></div></div>
+
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/index.html b/doc/core/howto/tutorial/index.html
new file mode 100644
index 0000000..e181e17
--- /dev/null
+++ b/doc/core/howto/tutorial/index.html
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted from Scratch, or The Evolution of Finger</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted from Scratch, or The Evolution of Finger</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Contents</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p>
+Twisted is a big system. People are often daunted when they approach it. It's
+hard to know where to start looking.
+</p>
+
+<p>
+This guide builds a full-fledged Twisted application from the ground up, using
+most of the important bits of the framework. There is a lot of code, but don't
+be afraid.
+</p>
+
+<p>
+The application we are looking at is a <q>finger</q> service, along the
+lines of the familiar service traditionally provided by UNIXâ„¢ servers.
+We will extend this service slightly beyond the standard, in order to
+demonstrate some of Twisted's higher-level features.
+</p>
+
+<p>
+Each section of the tutorial dives straight into applications for various
+Twisted topics. These topics have their own introductory howtos listed in
+the <a href="../index.html" shape="rect">core howto index</a> and in the documentation for
+other Twisted projects like Twisted Web and Twisted Words. There are at least
+three ways to use this tutorial: you may find it useful to read through the rest
+of the topics listed in the <a href="../index.html" shape="rect">core howto index</a> before
+working through the finger tutorial, work through the finger tutorial and then
+go back and hit the introductory material that is relevant to the Twisted
+project you're working on, or read the introductory material one piece at a time
+as it comes up in the finger tutorial.
+</p>
+
+<h2>Contents<a name="auto1"/></h2>
+
+<p>
+This tutorial is split into eleven parts:
+</p>
+
+<ol>
+<li><a href="intro.html" shape="rect">The Evolution of Finger: building a simple
+finger service</a></li>
+<li><a href="protocol.html" shape="rect">The Evolution of Finger: adding features
+to the finger service</a></li>
+<li><a href="style.html" shape="rect">The Evolution of Finger: cleaning up the
+finger code</a></li>
+<li><a href="components.html" shape="rect">The Evolution of Finger: moving to a
+component based architecture</a></li>
+<li><a href="backends.html" shape="rect">The Evolution of Finger: pluggable
+backends</a></li>
+<li><a href="web.html" shape="rect">The Evolution of Finger: a web
+frontend</a></li>
+<li><a href="pb.html" shape="rect">The Evolution of Finger: Twisted client
+support using Perspective Broker</a></li>
+<li><a href="factory.html" shape="rect">The Evolution of Finger: using a single
+factory for multiple protocols</a></li>
+<li><a href="client.html" shape="rect">The Evolution of Finger: a Twisted finger
+client</a></li>
+<li><a href="library.html" shape="rect">The Evolution of Finger: making a finger library</a></li>
+<li><a href="configuration.html" shape="rect">The Evolution of Finger:
+configuration and packaging of the finger service</a></li>
+</ol>
+
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/intro.html b/doc/core/howto/tutorial/intro.html
new file mode 100644
index 0000000..cad8826
--- /dev/null
+++ b/doc/core/howto/tutorial/intro.html
@@ -0,0 +1,725 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: building a simple finger service</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: building a simple finger service</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Refuse Connections</a></li><ul><li><a href="#auto2">The Reactor</a></li></ul><li><a href="#auto3">Do Nothing</a></li><li><a href="#auto4">Drop Connections</a></li><li><a href="#auto5">Read Username, Drop Connections</a></li><li><a href="#auto6">Read Username, Output Error, Drop Connections</a></li><li><a href="#auto7">Output From Empty Factory</a></li><li><a href="#auto8">Output from Non-empty Factory</a></li><li><a href="#auto9">Use Deferreds</a></li><li><a href="#auto10">Run 'finger' Locally</a></li><li><a href="#auto11">Read Status from the Web</a></li><li><a href="#auto12">Use Application</a></li><li><a href="#auto13">twistd</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p>This is the first part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>If you're not familiar with 'finger' it's probably because it's not used as
+much nowadays as it used to be. Basically, if you run <code>finger nail</code>
+or <code>finger nail@example.com</code> the target computer spits out some
+information about the user named <code>nail</code>. For instance:</p>
+
+<pre class="shell" xml:space="preserve">
+Login: nail Name: Nail Sharp
+Directory: /home/nail Shell: /usr/bin/sh
+Last login Wed Mar 31 18:32 2004 (PST)
+New mail received Thu Apr 1 10:50 2004 (PST)
+ Unread since Thu Apr 1 10:50 2004 (PST)
+No Plan.
+</pre>
+
+<p>If the target computer does not have
+the <code>fingerd</code> <a href="../glossary.html#Daemon" shape="rect">daemon</a>
+running you'll get a &quot;Connection Refused&quot; error. Paranoid sysadmins
+keep <code>fingerd</code> off or limit the output to hinder crackers
+and harassers. The above format is the standard <code>fingerd</code>
+default, but an alternate implementation can output anything it wants,
+such as automated responsibility status for everyone in an
+organization. You can also define pseudo &quot;users&quot;, which are
+essentially keywords.</p>
+
+<p>This portion of the tutorial makes use of factories and protocols as
+introduced in the <a href="../servers.html" shape="rect">Writing a TCP Server howto</a> and
+deferreds as introduced in <a href="../defer.html" shape="rect">Using Deferreds</a>
+and <a href="../gendefer.html" shape="rect">Generating Deferreds</a>. Services and
+applications are discussed in <a href="../application.html" shape="rect">Using the Twisted
+Application Framework</a>.</p>
+
+<p>By the end of this section of the tutorial, our finger server will answer
+TCP finger requests on port 1079, and will read data from the web.</p>
+
+<h2>Refuse Connections<a name="auto1"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger01.py"><span class="filename">listings/finger/finger01.py</span></a></div></div>
+
+<p>This example only runs the reactor. It will consume almost no CPU
+resources. As it is not listening on any port, it can't respond to network
+requests — nothing at all will happen until we interrupt the program. At
+this point if you run <code>finger nail</code> or <code>telnet localhost
+1079</code>, you'll get a &quot;Connection refused&quot; error since there's no daemon
+running to respond. Not very useful, perhaps — but this is the skeleton
+inside which the Twisted program will grow.
+</p>
+
+<p>As implied above, at various points in this tutorial you'll want to
+observe the behavior of the server being developed. Unless you have a
+finger program which can use an alternate port, the easiest way to do this
+is with a telnet client. <code>telnet localhost 1079</code> will connect to
+the local host on port 1079, where a finger server will eventually be
+listening.</p>
+
+<h3>The Reactor<a name="auto2"/></h3>
+
+<p>You don't call Twisted, Twisted calls you. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code> is Twisted's main event loop, similar to
+the main loop in other toolkits available in Python (Qt, wx, and Gtk). There is
+exactly one reactor in any running Twisted application. Once started it loops
+over and over again, responding to network events and making scheduled calls to
+code.</p>
+
+<p>Note that there are actually several different reactors to choose
+from; <code>from twisted.internet import reactor</code> returns the
+current reactor. If you haven't chosen a reactor class yet, it
+automatically chooses the default. See
+the <a href="../reactor-basics.html" shape="rect">Reactor Basics HOWTO</a> for
+more information.</p>
+
+<h2>Do Nothing<a name="auto3"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger02.py"><span class="filename">listings/finger/finger02.py</span></a></div></div>
+
+<p>Here, <code>reactor.listenTCP</code> opens port 1079. (The number 1079 is a
+reminder that eventually we want to run on port 79, the standard port for
+finger servers.) The specified factory, <code>FingerFactory</code>, is used to
+handle incoming requests on that port. Specifically, for each request, the
+reactor calls the factory's <code>buildProtocol</code> method, which in this
+case causes <code>FingerProtocol</code> to be instantiated. Since the protocol
+defined here does not actually respond to any events, connections to 1079 will
+be accepted, but the input ignored.</p>
+
+<p>A Factory is the proper place for data that you want to make available to
+the protocol instances, since the protocol instances are garbage collected when
+the connection is closed.</p>
+
+
+<h2>Drop Connections<a name="auto4"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger03.py"><span class="filename">listings/finger/finger03.py</span></a></div></div>
+
+<p>Here we add to the protocol the ability to respond to the event of beginning
+a connection — by terminating it. Perhaps not an interesting behavior,
+but it is already close to behaving according to the letter of the standard
+finger protocol. After all, there is no requirement to send any data to the
+remote connection in the standard. The only problem, as far as the standard is
+concerned, is that we terminate the connection too soon. A client which is slow
+enough will see his <code>send()</code> of the username result in an error.</p>
+
+
+<h2>Read Username, Drop Connections<a name="auto5"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger04.py"><span class="filename">listings/finger/finger04.py</span></a></div></div>
+
+<p>Here we make <code>FingerProtocol</code> inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.protocols.basic.LineReceiver.html" title="twisted.protocols.basic.LineReceiver">LineReceiver</a></code>, so that we get data-based
+events on a line-by-line basis. We respond to the event of receiving the line
+with shutting down the connection.</p>
+
+<p>If you use a telnet client to interact with this server, the result will
+look something like this:</p>
+
+<pre class="shell" xml:space="preserve">
+$ telnet localhost 1079
+Trying 127.0.0.1...
+Connected to localhost.localdomain.
+alice
+Connection closed by foreign host.
+</pre>
+
+<p>Congratulations, this is the first standard-compliant version of the code.
+However, usually people actually expect some data about users to be
+transmitted.</p>
+
+<h2>Read Username, Output Error, Drop Connections<a name="auto6"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;No such user\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger05.py"><span class="filename">listings/finger/finger05.py</span></a></div></div>
+
+<p>Finally, a useful version. Granted, the usefulness is somewhat limited by
+the fact that this version only prints out a <q>No such user</q> message. It
+could be used for devastating effect in honey-pots (decoy servers), of
+course.</p>
+
+
+<h2>Output From Empty Factory<a name="auto7"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-comment"># Read username, output from empty factory, drop connections</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)+<span class="py-src-string">&quot;\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;No such user&quot;</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger06.py"><span class="filename">listings/finger/finger06.py</span></a></div></div>
+
+<p>The same behavior, but finally we see what usefulness the
+factory has: as something that does not get constructed for
+every connection, it can be in charge of the user database.
+In particular, we won't have to change the protocol if
+the user database back-end changes.</p>
+
+
+<h2>Output from Non-empty Factory<a name="auto8"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-comment"># Read username, output from non-empty factory, drop connections</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)+<span class="py-src-string">&quot;\r\n&quot;</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger07.py"><span class="filename">listings/finger/finger07.py</span></a></div></div>
+
+<p>Finally, a really useful finger database. While it does not
+supply information about logged in users, it could be used to
+distribute things like office locations and internal office
+numbers. As hinted above, the factory is in charge of keeping
+the user database: note that the protocol instance has not
+changed. This is starting to look good: we really won't have
+to keep tweaking our protocol.</p>
+
+
+<h2>Use Deferreds<a name="auto9"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-comment"># Read username, output from non-empty factory, drop connections</span>
+<span class="py-src-comment"># Use deferreds, to minimize synchronicity assumptions</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger08.py"><span class="filename">listings/finger/finger08.py</span></a></div></div>
+
+<p>But, here we tweak it just for the hell of it. Yes, while the
+previous version worked, it did assume the result of getUser is
+always immediately available. But what if instead of an in-memory
+database, we would have to fetch the result from a remote Oracle server? By
+allowing getUser to return a Deferred, we make it easier for the data to be
+retrieved asynchronously so that the CPU can be used for other tasks in the
+meanwhile.</p>
+
+<p>As described in the <a href="../defer.html" shape="rect">Deferred HOWTO</a>, Deferreds
+allow a program to be driven by events. For instance, if one task in a program
+is waiting on data, rather than have the CPU (and the program!) idly waiting
+for that data (a process normally called 'blocking'), the program can perform
+other operations in the meantime, and waits for some signal that data is ready
+to be processed before returning to that process.</p>
+
+<p>In brief, the code in <code>FingerFactory</code> above creates a
+Deferred, to which we start to attach <em>callbacks</em>. The
+deferred action in <code>FingerFactory</code> is actually a
+fast-running expression consisting of one dictionary
+method, <code>get</code>. Since this action can execute without
+delay, <code>FingerFactory.getUser</code>
+uses <code>defer.succeed</code> to create a Deferred which already has
+a result, meaning its return value will be passed immediately to the
+first callback function, which turns out to
+be <code>FingerProtocol.writeResponse</code>. We've also defined
+an <em>errback</em> (appropriately
+named <code>FingerProtocol.onError</code>) that will be called instead
+of <code>writeResponse</code> if something goes wrong.</p>
+
+<h2>Run 'finger' Locally<a name="auto10"/></h2>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-comment"># Read username, output from factory interfacing to OS, drop connections</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">utils</span>.<span class="py-src-variable">getProcessOutput</span>(<span class="py-src-string">&quot;finger&quot;</span>, [<span class="py-src-variable">user</span>])
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger09.py"><span class="filename">listings/finger/finger09.py</span></a></div></div>
+
+<p>This example also makes use of a
+Deferred. <code>twisted.internet.utils.getProcessOutput</code> is a
+non-blocking version of Python's <code>commands.getoutput</code>: it
+runs a shell command (<code>finger</code>, in this case) and captures
+its standard output. However, <code>getProcessOutput</code> returns a
+Deferred instead of the output itself.
+Since <code>FingerProtocol.lineReceived</code> is already expecting a
+Deferred to be returned by <code>getUser</code>, it doesn't need to be
+changed, and it returns the standard output as the finger result.</p>
+
+<p>Note that in this case the shell's built-in <code>finger</code> command is
+simply run with whatever arguments it is given. This is probably insecure, so
+you probably don't want a real server to do this without a lot more validation
+of the user input. This will do exactly what the standard version of the finger
+server does.</p>
+
+<h2>Read Status from the Web<a name="auto11"/></h2>
+
+<p>The web. That invention which has infiltrated homes around the
+world finally gets through to our invention. In this case we use the
+built-in Twisted web client
+via <code>twisted.web.client.getPage</code>, a non-blocking version of
+Python's <code>urllib2.urlopen(URL).read()</code>.
+Like <code>getProcessOutput</code> it returns a Deferred which will be
+called back with a string, and can thus be used as a drop-in
+replacement.</p>
+
+<p>Thus, we have examples of three different database back-ends, none of which
+change the protocol class. In fact, we will not have to change the protocol
+again until the end of this tutorial: we have achieved, here, one truly usable
+class.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-comment"># Read username, output from factory interfacing to web, drop connections</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>, <span class="py-src-variable">utils</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">client</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">prefix</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">prefix</span>=<span class="py-src-variable">prefix</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">client</span>.<span class="py-src-variable">getPage</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">prefix</span>+<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">1079</span>, <span class="py-src-variable">FingerFactory</span>(<span class="py-src-variable">prefix</span>=<span class="py-src-string">'http://livejournal.com/~'</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/finger10.py"><span class="filename">listings/finger/finger10.py</span></a></div></div>
+
+<h2>Use Application<a name="auto12"/></h2>
+
+<p>Up until now, we faked. We kept using port 1079, because really, who wants to
+run a finger server with root privileges? Well, the common solution
+is <q>privilege shedding</q>: after binding to the network, become a different,
+less privileged user. We could have done it ourselves, but Twisted has a
+built-in way to do it. We will create a snippet as above, but now we will define
+an application object. That object will have <code>uid</code>
+and <code>gid</code> attributes. When running it (later we will see how) it will
+bind to ports, shed privileges and then run.</p>
+
+<p>Read on to find out how to run this code using the twistd utility.</p>
+
+<h2>twistd<a name="auto13"/></h2>
+
+<p>This is how to run <q>Twisted Applications</q> — files which define an
+'application'. A daemon is expected to adhere to certain behavioral standards
+so that standard tools can stop/start/query them. If a Twisted application is
+run via twistd, the TWISTed Daemonizer, all this behavioral stuff will be
+handled for you. twistd does everything a daemon can be expected to —
+shuts down stdin/stdout/stderr, disconnects from the terminal and can even
+change runtime directory, or even the root filesystems. In short, it does
+everything so the Twisted application developer can concentrate on writing his
+networking code.</p>
+
+<pre class="shell" xml:space="preserve">
+root% twistd -ny finger11.tac # just like before
+root% twistd -y finger11.tac # daemonize, keep pid in twistd.pid
+root% twistd -y finger11.tac --pidfile=finger.pid
+root% twistd -y finger11.tac --rundir=/
+root% twistd -y finger11.tac --chroot=/var
+root% twistd -y finger11.tac -l /var/log/finger.log
+root% twistd -y finger11.tac --syslog # just log to syslog
+root% twistd -y finger11.tac --syslog --prefix=twistedfinger # use given prefix
+</pre>
+
+<p>There are several ways to tell twistd where your application is; here we
+show how it is done using the <code>application</code> global variable in a
+Python source file (a <a href="../glossary.html#TAC" shape="rect">Twisted Application
+Configuration</a> file).</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+</p><span class="py-src-comment"># Read username, output from non-empty factory, drop connections</span>
+<span class="py-src-comment"># Use deferreds, to minimize synchronicity assumptions</span>
+<span class="py-src-comment"># Write application. Save in 'finger.tpy'</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">FingerFactory</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">factory</span>).<span class="py-src-variable">setServiceParent</span>(
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/finger/finger11.tac"><span class="filename">listings/finger/finger11.tac</span></a></div></div>
+
+
+<p>Instead of using <code>reactor.listenTCP</code> as in the above
+examples, here we are using its application-aware
+counterpart, <code>internet.TCPServer</code>. Notice that when it is
+instantiated, the application object itself does not reference either
+the protocol or the factory. Any services (such as TCPServer) which
+have the application as their parent will be started when the
+application is started by twistd. The application object is more
+useful for returning an object that supports the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IServiceCollection.html" title="twisted.application.service.IServiceCollection">IServiceCollection</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IProcess.html" title="twisted.application.service.IProcess">IProcess</a></code>,
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.persisted.sob.IPersistable.html" title="twisted.persisted.sob.IPersistable">sob.IPersistable</a></code>
+interfaces with the given parameters; we'll be seeing these in the
+next part of the tutorial. As the parent of the TCPServer we opened,
+the application lets us manage the TCPServer.</p>
+
+<p>With the daemon running on the standard finger port, you can test it with
+the standard finger command: <code>finger moshez</code>.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/library.html b/doc/core/howto/tutorial/library.html
new file mode 100644
index 0000000..fd426fe
--- /dev/null
+++ b/doc/core/howto/tutorial/library.html
@@ -0,0 +1,271 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: making a finger library</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: making a finger library</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Organization</a></li><li><a href="#auto2">Easy Configuration</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the tenth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we separate the application code that launches a finger service
+from the library code which defines a finger service, placing the application in
+a Twisted Application Configuration (.tac) file. We also move configuration
+(such as HTML templates) into separate files. Configuration and deployment with
+.tac and twistd are introduced in <a href="../application.html" shape="rect">Using the
+Twisted Application Framework</a>.</p>
+
+<h2>Organization<a name="auto1"/></h2>
+
+<p>Now this code, while quite modular and well-designed, isn't
+properly organized. Everything above the <code>application=</code> belongs in a
+module, and the HTML templates all belong in separate files.
+</p>
+
+<p>We can use the <code>templateFile</code> and <code>templateDirectory</code>
+attributes to indicate what HTML template file to use for each Page, and where
+to look for it.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+</p><span class="py-src-comment"># organized-finger.tac</span>
+<span class="py-src-comment"># eg: twistd -ny organized-finger.tac</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">finger</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>, <span class="py-src-variable">strports</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">finger</span>.<span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">finger</span>.<span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">site</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">SSLServer</span>(<span class="py-src-number">443</span>, <span class="py-src-variable">site</span>, <span class="py-src-variable">finger</span>.<span class="py-src-variable">ServerContextFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+
+<span class="py-src-variable">i</span> = <span class="py-src-variable">finger</span>.<span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8889</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">finger</span>.<span class="py-src-variable">IPerspectiveFinger</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/organized-finger.tac"><span class="filename">listings/finger/organized-finger.tac</span></a></div></div>
+
+<p>
+Note that our program is now quite separated. We have:
+<ul>
+ <li>Code (in the module)</li>
+ <li>Configuration (file above)</li>
+ <li>Presentation (templates)</li>
+ <li>Content (<code>/etc/users</code>)</li>
+ <li>Deployment (twistd)</li>
+</ul>
+
+Prototypes don't need this level of separation, so our earlier examples all
+bunched together. However, real applications do. Thankfully, if we write our
+code correctly, it is easy to achieve a good separation of parts.
+</p>
+
+
+<h2>Easy Configuration<a name="auto2"/></h2>
+
+<p>We can also supply easy configuration for common cases with a makeService
+method that will also help build .tap files later:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+</p><span class="py-src-comment"># Easy configuration</span>
+<span class="py-src-comment"># makeService from finger module</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">makeService</span>(<span class="py-src-parameter">config</span>):
+ <span class="py-src-comment"># finger on port 79</span>
+ <span class="py-src-variable">s</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">MultiService</span>()
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-variable">config</span>[<span class="py-src-string">'file'</span>])
+ <span class="py-src-variable">h</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>))
+ <span class="py-src-variable">h</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># website on port 8000</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>)
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">templateDirectory</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'templates'</span>]
+ <span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>)
+ <span class="py-src-variable">j</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">site</span>)
+ <span class="py-src-variable">j</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># ssl on port 443</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">get</span>(<span class="py-src-string">'ssl'</span>):
+ <span class="py-src-variable">k</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">SSLServer</span>(<span class="py-src-number">443</span>, <span class="py-src-variable">site</span>, <span class="py-src-variable">ServerContextFactory</span>())
+ <span class="py-src-variable">k</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># irc fingerbot</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-string">'ircnick'</span>):
+ <span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+ <span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'ircnick'</span>]
+ <span class="py-src-variable">ircserver</span> = <span class="py-src-variable">config</span>[<span class="py-src-string">'ircserver'</span>]
+ <span class="py-src-variable">b</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-variable">ircserver</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>)
+ <span class="py-src-variable">b</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-comment"># Pespective Broker on port 8889</span>
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">config</span>.<span class="py-src-variable">has_key</span>(<span class="py-src-string">'pbport'</span>):
+ <span class="py-src-variable">m</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(
+ <span class="py-src-variable">int</span>(<span class="py-src-variable">config</span>[<span class="py-src-string">'pbport'</span>]),
+ <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">IPerspectiveFinger</span>(<span class="py-src-variable">f</span>)))
+ <span class="py-src-variable">m</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">s</span>)
+
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">s</span>
+</pre><div class="caption">Source listing - <a href="listings/finger/finger_config.py"><span class="filename">listings/finger/finger_config.py</span></a></div></div>
+
+<p>And we can write simpler files now:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-comment"># simple-finger.tac</span>
+<span class="py-src-comment"># eg: twistd -ny simple-finger.tac</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">finger</span>
+
+<span class="py-src-variable">options</span> = { <span class="py-src-string">'file'</span>: <span class="py-src-string">'/etc/users'</span>,
+ <span class="py-src-string">'templates'</span>: <span class="py-src-string">'/usr/share/finger/templates'</span>,
+ <span class="py-src-string">'ircnick'</span>: <span class="py-src-string">'fingerbot'</span>,
+ <span class="py-src-string">'ircserver'</span>: <span class="py-src-string">'irc.freenode.net'</span>,
+ <span class="py-src-string">'pbport'</span>: <span class="py-src-number">8889</span>,
+ <span class="py-src-string">'ssl'</span>: <span class="py-src-string">'ssl=0'</span> }
+
+<span class="py-src-variable">ser</span> = <span class="py-src-variable">finger</span>.<span class="py-src-variable">makeService</span>(<span class="py-src-variable">options</span>)
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">ser</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/finger/simple-finger.tac"><span class="filename">listings/finger/simple-finger.tac</span></a></div></div>
+
+<pre class="shell" xml:space="preserve">
+% twisted -ny simple-finger.tac
+</pre>
+
+
+<p>Note: the finger <em>user</em> still has ultimate power: he can use
+ <code>makeService</code>, or he can use the lower-level interface if he has
+specific needs (maybe an IRC server on some other port? Maybe we want the
+non-SSL webserver to listen only locally? etc. etc.) This is an important
+design principle: never force a layer of abstraction: allow usage of layers of
+abstractions.</p>
+
+<p>The pasta theory of design:</p>
+
+<ul>
+<li>Spaghetti: each piece of code interacts with every other piece of
+ code [can be implemented with GOTO, functions, objects]</li>
+<li>Lasagna: code has carefully designed layers. Each layer is, in
+ theory independent. However low-level layers usually cannot be
+ used easily, and high-level layers depend on low-level layers.</li>
+<li>Ravioli: each part of the code is useful by itself. There is a thin
+ layer of interfaces between various parts [the sauce]. Each part
+ can be usefully be used elsewhere.</li>
+<li>...but sometimes, the user just wants to order <q>Ravioli</q>, so one
+ coarse-grain easily definable layer of abstraction on top of it all
+ can be useful.</li>
+</ul>
+
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/listings/finger/etc.users b/doc/core/howto/tutorial/listings/finger/etc.users
new file mode 100644
index 0000000..d8c8f8c
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/etc.users
@@ -0,0 +1,2 @@
+moshez: happy and well
+shawn: alive
diff --git a/doc/core/howto/tutorial/listings/finger/finger/__init__.py b/doc/core/howto/tutorial/listings/finger/finger/__init__.py
new file mode 100755
index 0000000..bcb24fa
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger/__init__.py
@@ -0,0 +1,3 @@
+"""
+Finger example application.
+"""
diff --git a/doc/core/howto/tutorial/listings/finger/finger/finger.py b/doc/core/howto/tutorial/listings/finger/finger/finger.py
new file mode 100755
index 0000000..7812af7
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger/finger.py
@@ -0,0 +1,368 @@
+# finger.py module
+
+from zope.interface import Interface, implements
+
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components, log
+from twisted.web import resource, server, xmlrpc
+from twisted.spread import pb
+
+from OpenSSL import SSL
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ template = """<html><head><title>Users</title></head><body>
+ <h1>Users</h1>
+ <ul>
+ %(users)s
+ </ul>
+ </body>
+ </html>"""
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ elif path == 'RPC2':
+ return UserStatusXR(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+ def render_GET(self, request):
+ users = self.service.getUsers()
+ def cbUsers(users):
+ request.write(self.template % {'users': ''.join([
+ # Name should be quoted properly these uses.
+ '<li><a href="%s">%s</a></li>' % (name, name)
+ for name in users])})
+ request.finish()
+ users.addCallback(cbUsers)
+ def ebUsers(err):
+ log.err(err, "UserStatusTree failed")
+ request.finish()
+ users.addErrback(ebUsers)
+ return server.NOT_DONE_YET
+
+components.registerAdapter(UserStatusTree, IFingerService, resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ template='''<html><head><title>%(title)s</title></head>
+ <body><h1>%(name)s</h1><p>%(status)s</p></body></html>'''
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ status = self.service.getUser(self.user)
+ def cbStatus(status):
+ request.write(self.template % {
+ 'title': self.user,
+ 'name': self.user,
+ 'status': status})
+ request.finish()
+ status.addCallback(cbStatus)
+ def ebStatus(err):
+ log.err(err, "UserStatus failed")
+ request.finish()
+ status.addErrback(ebStatus)
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+ def xmlrpc_getUsers(self):
+ return self.service.getUsers()
+
+
+class IPerspectiveFinger(Interface):
+
+ def remote_getUser(username):
+ """
+ Return a user's status.
+ """
+
+ def remote_getUsers():
+ """
+ Return a user's status.
+ """
+
+
+class PerspectiveFingerFromService(pb.Root):
+
+ implements(IPerspectiveFinger)
+
+ def __init__(self, service):
+ self.service = service
+
+ def remote_getUser(self, username):
+ return self.service.getUser(username)
+
+ def remote_getUsers(self):
+ return self.service.getUsers()
+
+components.registerAdapter(PerspectiveFingerFromService,
+ IFingerService,
+ IPerspectiveFinger)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+class ServerContextFactory:
+
+ def getContext(self):
+ """
+ Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'.
+ """
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file('server.pem')
+ ctx.use_privatekey_file('server.pem')
+ return ctx
+
+
+
+# Easy configuration
+
+def makeService(config):
+ # finger on port 79
+ s = service.MultiService()
+ f = FingerService(config['file'])
+ h = internet.TCPServer(1079, IFingerFactory(f))
+ h.setServiceParent(s)
+
+
+ # website on port 8000
+ r = resource.IResource(f)
+ r.templateDirectory = config['templates']
+ site = server.Site(r)
+ j = internet.TCPServer(8000, site)
+ j.setServiceParent(s)
+
+ # ssl on port 443
+# if config.get('ssl'):
+# k = internet.SSLServer(443, site, ServerContextFactory())
+# k.setServiceParent(s)
+
+ # irc fingerbot
+ if config.has_key('ircnick'):
+ i = IIRCClientFactory(f)
+ i.nickname = config['ircnick']
+ ircserver = config['ircserver']
+ b = internet.TCPClient(ircserver, 6667, i)
+ b.setServiceParent(s)
+
+ # Pespective Broker on port 8889
+ if config.has_key('pbport'):
+ m = internet.TCPServer(
+ int(config['pbport']),
+ pb.PBServerFactory(IPerspectiveFinger(f)))
+ m.setServiceParent(s)
+
+ return s
diff --git a/doc/core/howto/tutorial/listings/finger/finger/tap.py b/doc/core/howto/tutorial/listings/finger/finger/tap.py
new file mode 100644
index 0000000..a06102c
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger/tap.py
@@ -0,0 +1,20 @@
+# finger/tap.py
+from twisted.application import internet, service
+from twisted.internet import interfaces
+from twisted.python import usage
+import finger
+
+class Options(usage.Options):
+
+ optParameters = [
+ ['file', 'f', '/etc/users'],
+ ['templates', 't', '/usr/share/finger/templates'],
+ ['ircnick', 'n', 'fingerbot'],
+ ['ircserver', None, 'irc.freenode.net'],
+ ['pbport', 'p', 8889],
+ ]
+
+ optFlags = [['ssl', 's']]
+
+def makeService(config):
+ return finger.makeService(config)
diff --git a/doc/core/howto/tutorial/listings/finger/finger01.py b/doc/core/howto/tutorial/listings/finger/finger01.py
new file mode 100755
index 0000000..0561510
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger01.py
@@ -0,0 +1,2 @@
+from twisted.internet import reactor
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger02.py b/doc/core/howto/tutorial/listings/finger/finger02.py
new file mode 100755
index 0000000..e7efbf4
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger02.py
@@ -0,0 +1,10 @@
+from twisted.internet import protocol, reactor
+
+class FingerProtocol(protocol.Protocol):
+ pass
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger03.py b/doc/core/howto/tutorial/listings/finger/finger03.py
new file mode 100755
index 0000000..d323023
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger03.py
@@ -0,0 +1,11 @@
+from twisted.internet import protocol, reactor
+
+class FingerProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.loseConnection()
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger04.py b/doc/core/howto/tutorial/listings/finger/finger04.py
new file mode 100755
index 0000000..d35f590
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger04.py
@@ -0,0 +1,12 @@
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.loseConnection()
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger05.py b/doc/core/howto/tutorial/listings/finger/finger05.py
new file mode 100755
index 0000000..0d8da8c
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger05.py
@@ -0,0 +1,13 @@
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write("No such user\r\n")
+ self.transport.loseConnection()
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger06.py b/doc/core/howto/tutorial/listings/finger/finger06.py
new file mode 100755
index 0000000..7f78986
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger06.py
@@ -0,0 +1,18 @@
+# Read username, output from empty factory, drop connections
+
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write(self.factory.getUser(user)+"\r\n")
+ self.transport.loseConnection()
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def getUser(self, user):
+ return "No such user"
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger07.py b/doc/core/howto/tutorial/listings/finger/finger07.py
new file mode 100755
index 0000000..cc5dbf1
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger07.py
@@ -0,0 +1,21 @@
+# Read username, output from non-empty factory, drop connections
+
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write(self.factory.getUser(user)+"\r\n")
+ self.transport.loseConnection()
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return self.users.get(user, "No such user")
+
+reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger08.py b/doc/core/howto/tutorial/listings/finger/finger08.py
new file mode 100755
index 0000000..624c5b0
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger08.py
@@ -0,0 +1,30 @@
+# Read username, output from non-empty factory, drop connections
+# Use deferreds, to minimize synchronicity assumptions
+
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger09.py b/doc/core/howto/tutorial/listings/finger/finger09.py
new file mode 100755
index 0000000..336acb3
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger09.py
@@ -0,0 +1,26 @@
+# Read username, output from factory interfacing to OS, drop connections
+
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def getUser(self, user):
+ return utils.getProcessOutput("finger", [user])
+
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger10.py b/doc/core/howto/tutorial/listings/finger/finger10.py
new file mode 100755
index 0000000..7e4cb93
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger10.py
@@ -0,0 +1,30 @@
+# Read username, output from factory interfacing to web, drop connections
+
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.protocols import basic
+from twisted.web import client
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, prefix):
+ self.prefix=prefix
+
+ def getUser(self, user):
+ return client.getPage(self.prefix+user)
+
+reactor.listenTCP(1079, FingerFactory(prefix='http://livejournal.com/~'))
+reactor.run()
diff --git a/doc/core/howto/tutorial/listings/finger/finger11.tac b/doc/core/howto/tutorial/listings/finger/finger11.tac
new file mode 100755
index 0000000..aae8ca6
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger11.tac
@@ -0,0 +1,34 @@
+# Read username, output from non-empty factory, drop connections
+# Use deferreds, to minimize synchronicity assumptions
+# Write application. Save in 'finger.tpy'
+
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+application = service.Application('finger', uid=1, gid=1)
+factory = FingerFactory(moshez='Happy and well')
+internet.TCPServer(79, factory).setServiceParent(
+ service.IServiceCollection(application))
diff --git a/doc/core/howto/tutorial/listings/finger/finger12.tac b/doc/core/howto/tutorial/listings/finger/finger12.tac
new file mode 100755
index 0000000..69120f1
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger12.tac
@@ -0,0 +1,55 @@
+# But let's try and fix setting away messages, shall we?
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ user = self.lines[0]
+ status = self.lines[1]
+ self.factory.setUser(user, status)
+
+class FingerSetterFactory(protocol.ServerFactory):
+ protocol = FingerSetterProtocol
+
+ def __init__(self, fingerFactory):
+ self.fingerFactory = fingerFactory
+
+ def setUser(self, user, status):
+ self.fingerFactory.users[user] = status
+
+ff = FingerFactory(moshez='Happy and well')
+fsf = FingerSetterFactory(ff)
+
+application = service.Application('finger', uid=1, gid=1)
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79,ff).setServiceParent(serviceCollection)
+internet.TCPServer(1079,fsf).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger13.tac b/doc/core/howto/tutorial/listings/finger/finger13.tac
new file mode 100755
index 0000000..5cf60c9
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger13.tac
@@ -0,0 +1,59 @@
+# Fix asymmetry
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self,reason):
+ user = self.lines[0]
+ status = self.lines[1]
+ self.factory.setUser(user, status)
+
+class FingerService(service.Service):
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def setUser(self, user, status):
+ self.users[user] = status
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getFingerSetterFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerSetterProtocol
+ f.setUser = self.setUser
+ return f
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService(moshez='Happy and well')
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79,f.getFingerFactory()
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(1079,f.getFingerSetterFactory()
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger14.tac b/doc/core/howto/tutorial/listings/finger/finger14.tac
new file mode 100755
index 0000000..48e4ee0
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger14.tac
@@ -0,0 +1,56 @@
+# Read from file
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+
+class FingerService(service.Service):
+ def __init__(self, filename):
+ self.users = {}
+ self.filename = filename
+
+ def _read(self):
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+finger = internet.TCPServer(79, f.getFingerFactory())
+
+finger.setServiceParent(service.IServiceCollection(application))
+f.setServiceParent(service.IServiceCollection(application))
diff --git a/doc/core/howto/tutorial/listings/finger/finger15.tac b/doc/core/howto/tutorial/listings/finger/finger15.tac
new file mode 100755
index 0000000..cf90ddc
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger15.tac
@@ -0,0 +1,87 @@
+# Read from file, announce on the web!
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+from twisted.web import resource, server, static
+import cgi
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+
+class FingerResource(resource.Resource):
+
+ def __init__(self, users):
+ self.users = users
+ resource.Resource.__init__(self)
+
+ # we treat the path as the username
+ def getChild(self, username, request):
+ """
+ 'username' is a string.
+ 'request' is a 'twisted.web.server.Request'.
+ """
+ messagevalue = self.users.get(username)
+ username = cgi.escape(username)
+ if messagevalue is not None:
+ messagevalue = cgi.escape(messagevalue)
+ text = '<h1>%s</h1><p>%s</p>' % (username,messagevalue)
+ else:
+ text = '<h1>%s</h1><p>No such user</p>' % username
+ return static.Data(text, 'text/html')
+
+
+class FingerService(service.Service):
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getResource(self):
+ r = FingerResource(self.users)
+ return r
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, f.getFingerFactory()
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(f.getResource())
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger16.tac b/doc/core/howto/tutorial/listings/finger/finger16.tac
new file mode 100755
index 0000000..54a12c7
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger16.tac
@@ -0,0 +1,101 @@
+# Read from file, announce on the web, irc
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.web import resource, server, static
+
+import cgi
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+
+class IRCReplyBot(irc.IRCClient):
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ irc.IRCClient.msg(self, user, msg+': '+message)
+ d.addCallback(writeResponse)
+
+
+class FingerService(service.Service):
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getResource(self):
+ r = resource.Resource()
+ r.getChild = (lambda path, request:
+ static.Data('<h1>%s</h1><p>%s</p>' %
+ tuple(map(cgi.escape,
+ [path,self.users.get(path,
+ "No such user <p/> usage: site/user")])),
+ 'text/html'))
+ return r
+
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol = IRCReplyBot
+ f.nickname = nickname
+ f.getUser = self.getUser
+ return f
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, f.getFingerFactory()
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(f.getResource())
+ ).setServiceParent(serviceCollection)
+internet.TCPClient('irc.freenode.org', 6667, f.getIRCBot('fingerbot')
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger17.tac b/doc/core/howto/tutorial/listings/finger/finger17.tac
new file mode 100755
index 0000000..ec99041
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger17.tac
@@ -0,0 +1,102 @@
+# Read from file, announce on the web, irc, xml-rpc
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.web import resource, server, static, xmlrpc
+import cgi
+
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ self.transport.write(message + '\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeResponse)
+
+
+class IRCReplyBot(irc.IRCClient):
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+
+ def onError(err):
+ return 'Internal error in server'
+ d.addErrback(onError)
+
+ def writeResponse(message):
+ irc.IRCClient.msg(self, user, msg+': '+message)
+ d.addCallback(writeResponse)
+
+
+class FingerService(service.Service):
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getResource(self):
+ r = resource.Resource()
+ r.getChild = (lambda path, request:
+ static.Data('<h1>%s</h1><p>%s</p>' %
+ tuple(map(cgi.escape,
+ [path,self.users.get(path, "No such user")])),
+ 'text/html'))
+ x = xmlrpc.XMLRPC()
+ x.xmlrpc_getUser = self.getUser
+ r.putChild('RPC2', x)
+ return r
+
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol = IRCReplyBot
+ f.nickname = nickname
+ f.getUser = self.getUser
+ return f
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, f.getFingerFactory()
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(f.getResource())
+ ).setServiceParent(serviceCollection)
+internet.TCPClient('irc.freenode.org', 6667, f.getIRCBot('fingerbot')
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger18.tac b/doc/core/howto/tutorial/listings/finger/finger18.tac
new file mode 100755
index 0000000..c39479a
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger18.tac
@@ -0,0 +1,147 @@
+# Do everything properly
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.web import resource, server, static, xmlrpc
+import cgi
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class UserStatusTree(resource.Resource):
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ['<li><a href="%s">%s</a></li>' % (user, user)
+ for user in users]
+ return '<ul>'+''.join(l)+'</ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ if path=="":
+ return UserStatusTree(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(service.Service):
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getResource(self):
+ r = UserStatusTree(self)
+ x = UserStatusXR(self)
+ r.putChild('RPC2', x)
+ return r
+
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol = IRCReplyBot
+ f.nickname = nickname
+ f.getUser = self.getUser
+ return f
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, f.getFingerFactory()
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(f.getResource())
+ ).setServiceParent(serviceCollection)
+internet.TCPClient('irc.freenode.org', 6667, f.getIRCBot('fingerbot')
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19.tac b/doc/core/howto/tutorial/listings/finger/finger19.tac
new file mode 100755
index 0000000..4b79d63
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19.tac
@@ -0,0 +1,270 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc
+from zope.interface import Interface, implements
+import cgi
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ implements(resource.IResource)
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+ self.putChild('RPC2', UserStatusXR(self.service))
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ['<li><a href="%s">%s</a></li>' % (user, user)
+ for user in users]
+ return '<ul>'+''.join(l)+'</ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ if path=="":
+ return UserStatusTree(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService,
+ resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19a.tac b/doc/core/howto/tutorial/listings/finger/finger19a.tac
new file mode 100755
index 0000000..e6c66b5
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19a.tac
@@ -0,0 +1,231 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc
+from zope.interface import Interface, implements
+import cgi
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """Return a deferred returning a string"""
+
+ def getUsers():
+ """Return a deferred returning a list of strings"""
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """Set the user's status to something"""
+
+def catchError(err):
+ return "Internal error in server"
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(addr):
+ """Return a protocol returning a string"""
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(addr):
+ """Return a protocol returning a string"""
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(addr):
+ """Return a protocol"""
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+class UserStatusTree(resource.Resource):
+
+ implements(resource.IResource)
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+ self.putChild('RPC2', UserStatusXR(self.service))
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ['<li><a href="%s">%s</a></li>' % (user, user)
+ for user in users]
+ return '<ul>'+''.join(l)+'</ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ if path=="":
+ return UserStatusTree(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService,
+ resource.IResource)
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+class MemoryFingerService(service.Service):
+
+ implements([IFingerService, IFingerSetterService])
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def setUser(self, user, status):
+ self.users[user] = status
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = MemoryFingerService(moshez='Happy and well')
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(1079, IFingerSetterFactory(f), interface='127.0.0.1'
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19a_changes.py b/doc/core/howto/tutorial/listings/finger/finger19a_changes.py
new file mode 100644
index 0000000..cbb3623
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19a_changes.py
@@ -0,0 +1,29 @@
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """Set the user's status to something"""
+
+# Advantages of latest version
+
+class MemoryFingerService(service.Service):
+
+ implements([IFingerService, IFingerSetterService])
+
+ def __init__(self, **kwargs):
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def setUser(self, user, status):
+ self.users[user] = status
+
+
+f = MemoryFingerService(moshez='Happy and well')
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(1079, IFingerSetterFactory(f), interface='127.0.0.1'
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19b.tac b/doc/core/howto/tutorial/listings/finger/finger19b.tac
new file mode 100755
index 0000000..fdf1675
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19b.tac
@@ -0,0 +1,292 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc
+from zope.interface import Interface, implements
+import cgi
+import pwd
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ implements(resource.IResource)
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+ self.putChild('RPC2', UserStatusXR(self.service))
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ['<li><a href="%s">%s</a></li>' % (user, user)
+ for user in users]
+ return '<ul>'+''.join(l)+'</ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ if path=="":
+ return UserStatusTree(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService,
+ resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+# Another back-end
+
+class LocalFingerService(service.Service):
+
+ implements(IFingerService)
+
+ def getUser(self, user):
+ # need a local finger daemon running for this to work
+ return utils.getProcessOutput("finger", [user])
+
+ def getUsers(self):
+ return defer.succeed([])
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = LocalFingerService()
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19b_changes.py b/doc/core/howto/tutorial/listings/finger/finger19b_changes.py
new file mode 100644
index 0000000..3c8ff75
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19b_changes.py
@@ -0,0 +1,19 @@
+
+from twisted.internet import protocol, reactor, defer, utils
+import pwd
+
+# Another back-end
+
+class LocalFingerService(service.Service):
+
+ implements(IFingerService)
+
+ def getUser(self, user):
+ # need a local finger daemon running for this to work
+ return utils.getProcessOutput("finger", [user])
+
+ def getUsers(self):
+ return defer.succeed([])
+
+
+f = LocalFingerService()
diff --git a/doc/core/howto/tutorial/listings/finger/finger19c.tac b/doc/core/howto/tutorial/listings/finger/finger19c.tac
new file mode 100755
index 0000000..98502a5
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19c.tac
@@ -0,0 +1,305 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc
+from zope.interface import Interface, implements
+import cgi
+import pwd
+import os
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade():
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ implements(resource.IResource)
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service = service
+ self.putChild('RPC2', UserStatusXR(self.service))
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ['<li><a href="%s">%s</a></li>' % (user, user)
+ for user in users]
+ return '<ul>'+''.join(l)+'</ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ if path=="":
+ return UserStatusTree(self.service)
+ else:
+ return UserStatus(path, self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService,
+ resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '<h1>%s</h1>'%self.user+'<p>%s</p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+# Yet another back-end
+
+class LocalFingerService(service.Service):
+
+ implements(IFingerService)
+
+ def getUser(self, user):
+ user = user.strip()
+ try:
+ entry = pwd.getpwnam(user)
+ except KeyError:
+ return defer.succeed("No such user")
+ try:
+ f = file(os.path.join(entry[5],'.plan'))
+ except (IOError, OSError):
+ return defer.succeed("No such user")
+ data = f.read()
+ data = data.strip()
+ f.close()
+ return defer.succeed(data)
+
+ def getUsers(self):
+ return defer.succeed([])
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = LocalFingerService()
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger19c_changes.py b/doc/core/howto/tutorial/listings/finger/finger19c_changes.py
new file mode 100644
index 0000000..cc592ea
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger19c_changes.py
@@ -0,0 +1,32 @@
+from twisted.internet import protocol, reactor, defer, utils
+import pwd
+import os
+
+
+# Yet another back-end
+
+class LocalFingerService(service.Service):
+
+ implements(IFingerService)
+
+ def getUser(self, user):
+ user = user.strip()
+ try:
+ entry = pwd.getpwnam(user)
+ except KeyError:
+ return defer.succeed("No such user")
+ try:
+ f = file(os.path.join(entry[5],'.plan'))
+ except (IOError, OSError):
+ return defer.succeed("No such user")
+ data = f.read()
+ data = data.strip()
+ f.close()
+ return defer.succeed(data)
+
+ def getUsers(self):
+ return defer.succeed([])
+
+
+
+f = LocalFingerService()
diff --git a/doc/core/howto/tutorial/listings/finger/finger20.tac b/doc/core/howto/tutorial/listings/finger/finger20.tac
new file mode 100755
index 0000000..d29c66f
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger20.tac
@@ -0,0 +1,285 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from zope.interface import Interface, implements
+import cgi
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service=service
+
+ # add a specific child for the path "RPC2"
+ self.putChild("RPC2", UserStatusXR(self.service))
+
+ # need to do this for resources at the root of the site
+ self.putChild("", self)
+
+ def _cb_render_GET(self, users, request):
+ userOutput = ''.join(["<li><a href=\"%s\">%s</a></li>" % (user, user)
+ for user in users])
+ request.write("""
+ <html><head><title>Users</title></head><body>
+ <h1>Users</h1>
+ <ul>
+ %s
+ </ul></body></html>""" % userOutput)
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ return UserStatus(user=path, service=self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService, resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def _cb_render_GET(self, status, request):
+ request.write("""<html><head><title>%s</title></head>
+ <body><h1>%s</h1>
+ <p>%s</p>
+ </body></html>""" % (self.user, self.user, status))
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+ def xmlrpc_getUsers(self):
+ return self.service.getUsers()
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger21.tac b/doc/core/howto/tutorial/listings/finger/finger21.tac
new file mode 100755
index 0000000..af12354
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger21.tac
@@ -0,0 +1,319 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from twisted.spread import pb
+from zope.interface import Interface, implements
+import cgi
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service=service
+
+ # add a specific child for the path "RPC2"
+ self.putChild("RPC2", UserStatusXR(self.service))
+
+ # need to do this for resources at the root of the site
+ self.putChild("", self)
+
+ def _cb_render_GET(self, users, request):
+ userOutput = ''.join(["<li><a href=\"%s\">%s</a></li>" % (user, user)
+ for user in users])
+ request.write("""
+ <html><head><title>Users</title></head><body>
+ <h1>Users</h1>
+ <ul>
+ %s
+ </ul></body></html>""" % userOutput)
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ return UserStatus(user=path, service=self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService, resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def _cb_render_GET(self, status, request):
+ request.write("""<html><head><title>%s</title></head>
+ <body><h1>%s</h1>
+ <p>%s</p>
+ </body></html>""" % (self.user, self.user, status))
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+ def xmlrpc_getUsers(self):
+ return self.service.getUsers()
+
+
+class IPerspectiveFinger(Interface):
+
+ def remote_getUser(username):
+ """
+ Return a user's status.
+ """
+
+ def remote_getUsers():
+ """
+ Return a user's status.
+ """
+
+
+class PerspectiveFingerFromService(pb.Root):
+
+ implements(IPerspectiveFinger)
+
+ def __init__(self, service):
+ self.service = service
+
+ def remote_getUser(self, username):
+ return self.service.getUser(username)
+
+ def remote_getUsers(self):
+ return self.service.getUsers()
+
+components.registerAdapter(PerspectiveFingerFromService,
+ IFingerService,
+ IPerspectiveFinger)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8000, server.Site(resource.IResource(f))
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8889, pb.PBServerFactory(IPerspectiveFinger(f))
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/finger22.py b/doc/core/howto/tutorial/listings/finger/finger22.py
new file mode 100755
index 0000000..4e10fc9
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger22.py
@@ -0,0 +1,337 @@
+# Do everything properly, and componentize
+from twisted.application import internet, service
+from twisted.internet import protocol, reactor, defer
+from twisted.words.protocols import irc
+from twisted.protocols import basic
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from twisted.spread import pb
+from zope.interface import Interface, implements
+from OpenSSL import SSL
+import cgi
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def getUsers():
+ """
+ Return a deferred returning a list of strings.
+ """
+
+
+class IFingerSetterService(Interface):
+
+ def setUser(user, status):
+ """
+ Set the user's status to something.
+ """
+
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value+'\r\n')
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(Interface):
+
+ def setUser(user, status):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol returning a string.
+ """
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ implements(IFingerSetterFactory)
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSetterService,
+ IFingerSetterFactory)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!')[0]
+ if self.nickname.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (msg, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(Interface):
+
+ """
+ @ivar nickname
+ """
+
+ def getUser(user):
+ """
+ Return a deferred returning a string.
+ """
+
+ def buildProtocol(addr):
+ """
+ Return a protocol.
+ """
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ implements(IIRCClientFactory)
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(IRCClientFactoryFromService,
+ IFingerService,
+ IIRCClientFactory)
+
+
+class UserStatusTree(resource.Resource):
+
+ def __init__(self, service):
+ resource.Resource.__init__(self)
+ self.service=service
+
+ # add a specific child for the path "RPC2"
+ self.putChild("RPC2", UserStatusXR(self.service))
+
+ # need to do this for resources at the root of the site
+ self.putChild("", self)
+
+ def _cb_render_GET(self, users, request):
+ userOutput = ''.join(["<li><a href=\"%s\">%s</a></li>" % (user, user)
+ for user in users])
+ request.write("""
+ <html><head><title>Users</title></head><body>
+ <h1>Users</h1>
+ <ul>
+ %s
+ </ul></body></html>""" % userOutput)
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUsers()
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ return UserStatus(user=path, service=self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService, resource.IResource)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self)
+ self.user = user
+ self.service = service
+
+ def _cb_render_GET(self, status, request):
+ request.write("""<html><head><title>%s</title></head>
+ <body><h1>%s</h1>
+ <p>%s</p>
+ </body></html>""" % (self.user, self.user, status))
+ request.finish()
+
+ def render_GET(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(self._cb_render_GET, request)
+
+ # signal that the rendering is not complete
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLRPC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+ def xmlrpc_getUsers(self):
+ return self.service.getUsers()
+
+
+class IPerspectiveFinger(Interface):
+
+ def remote_getUser(username):
+ """
+ Return a user's status.
+ """
+
+ def remote_getUsers():
+ """
+ Return a user's status.
+ """
+
+class PerspectiveFingerFromService(pb.Root):
+
+ implements(IPerspectiveFinger)
+
+ def __init__(self, service):
+ self.service = service
+
+ def remote_getUser(self, username):
+ return self.service.getUser(username)
+
+ def remote_getUsers(self):
+ return self.service.getUsers()
+
+components.registerAdapter(PerspectiveFingerFromService,
+ IFingerService,
+ IPerspectiveFinger)
+
+
+class FingerService(service.Service):
+
+ implements(IFingerService)
+
+ def __init__(self, filename):
+ self.filename = filename
+ self.users = {}
+
+ def _read(self):
+ self.users.clear()
+ for line in file(self.filename):
+ user, status = line.split(':', 1)
+ user = user.strip()
+ status = status.strip()
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def startService(self):
+ self._read()
+ service.Service.startService(self)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ self.call.cancel()
+
+
+class ServerContextFactory:
+
+ def getContext(self):
+ """
+ Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'.
+ """
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file('server.pem')
+ ctx.use_privatekey_file('server.pem')
+ return ctx
+
+
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+f.setServiceParent(serviceCollection)
+internet.TCPServer(79, IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+site = server.Site(resource.IResource(f))
+internet.TCPServer(8000, site
+ ).setServiceParent(serviceCollection)
+internet.SSLServer(443, site, ServerContextFactory()
+ ).setServiceParent(serviceCollection)
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
+internet.TCPServer(8889, pb.PBServerFactory(IPerspectiveFinger(f))
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/fingerPBclient.py b/doc/core/howto/tutorial/listings/finger/fingerPBclient.py
new file mode 100755
index 0000000..66ed0ae
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/fingerPBclient.py
@@ -0,0 +1,26 @@
+# test the PB finger on port 8889
+# this code is essentially the same as
+# the first example in howto/pb-usage
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+def gotObject(object):
+ print "got object:", object
+ object.callRemote("getUser","moshez").addCallback(gotData)
+# or
+# object.callRemote("getUsers").addCallback(gotData)
+
+def gotData(data):
+ print 'server sent:', data
+ reactor.stop()
+
+def gotNoObject(reason):
+ print "no object:",reason
+ reactor.stop()
+
+factory = pb.PBClientFactory()
+reactor.connectTCP("127.0.0.1",8889, factory)
+factory.getRootObject().addCallbacks(gotObject,gotNoObject)
+reactor.run()
+
diff --git a/doc/core/howto/tutorial/listings/finger/fingerXRclient.py b/doc/core/howto/tutorial/listings/finger/fingerXRclient.py
new file mode 100755
index 0000000..b854bcf
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/fingerXRclient.py
@@ -0,0 +1,5 @@
+# testing xmlrpc finger
+
+import xmlrpclib
+server = xmlrpclib.Server('http://127.0.0.1:8000/RPC2')
+print server.getUser('moshez')
diff --git a/doc/core/howto/tutorial/listings/finger/finger_config.py b/doc/core/howto/tutorial/listings/finger/finger_config.py
new file mode 100644
index 0000000..226a26a
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/finger_config.py
@@ -0,0 +1,38 @@
+# Easy configuration
+# makeService from finger module
+
+def makeService(config):
+ # finger on port 79
+ s = service.MultiService()
+ f = FingerService(config['file'])
+ h = internet.TCPServer(79, IFingerFactory(f))
+ h.setServiceParent(s)
+
+ # website on port 8000
+ r = resource.IResource(f)
+ r.templateDirectory = config['templates']
+ site = server.Site(r)
+ j = internet.TCPServer(8000, site)
+ j.setServiceParent(s)
+
+ # ssl on port 443
+ if config.get('ssl'):
+ k = internet.SSLServer(443, site, ServerContextFactory())
+ k.setServiceParent(s)
+
+ # irc fingerbot
+ if config.has_key('ircnick'):
+ i = IIRCClientFactory(f)
+ i.nickname = config['ircnick']
+ ircserver = config['ircserver']
+ b = internet.TCPClient(ircserver, 6667, i)
+ b.setServiceParent(s)
+
+ # Pespective Broker on port 8889
+ if config.has_key('pbport'):
+ m = internet.TCPServer(
+ int(config['pbport']),
+ pb.PBServerFactory(IPerspectiveFinger(f)))
+ m.setServiceParent(s)
+
+ return s
diff --git a/doc/core/howto/tutorial/listings/finger/fingerproxy.tac b/doc/core/howto/tutorial/listings/finger/fingerproxy.tac
new file mode 100644
index 0000000..839c63d
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/fingerproxy.tac
@@ -0,0 +1,110 @@
+# finger proxy
+from twisted.application import internet, service
+from twisted.internet import defer, protocol, reactor
+from twisted.protocols import basic
+from twisted.python import components
+from zope.interface import Interface, implements
+
+
+def catchError(err):
+ return "Internal error in server"
+
+class IFingerService(Interface):
+
+ def getUser(user):
+ """Return a deferred returning a string"""
+
+ def getUsers():
+ """Return a deferred returning a list of strings"""
+
+
+class IFingerFactory(Interface):
+
+ def getUser(user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(addr):
+ """Return a protocol returning a string"""
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+
+class FingerFactoryFromService(protocol.ClientFactory):
+
+ implements(IFingerFactory)
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+
+components.registerAdapter(FingerFactoryFromService,
+ IFingerService,
+ IFingerFactory)
+
+class FingerClient(protocol.Protocol):
+
+ def connectionMade(self):
+ self.transport.write(self.factory.user+"\r\n")
+ self.buf = []
+
+ def dataReceived(self, data):
+ self.buf.append(data)
+
+ def connectionLost(self, reason):
+ self.factory.gotData(''.join(self.buf))
+
+class FingerClientFactory(protocol.ClientFactory):
+
+ protocol = FingerClient
+
+ def __init__(self, user):
+ self.user = user
+ self.d = defer.Deferred()
+
+ def clientConnectionFailed(self, _, reason):
+ self.d.errback(reason)
+
+ def gotData(self, data):
+ self.d.callback(data)
+
+
+def finger(user, host, port=79):
+ f = FingerClientFactory(user)
+ reactor.connectTCP(host, port, f)
+ return f.d
+
+
+class ProxyFingerService(service.Service):
+ implements(IFingerService)
+
+ def getUser(self, user):
+ try:
+ user, host = user.split('@', 1)
+ except:
+ user = user.strip()
+ host = '127.0.0.1'
+ ret = finger(user, host)
+ ret.addErrback(lambda _: "Could not connect to remote host")
+ return ret
+
+ def getUsers(self):
+ return defer.succeed([])
+
+application = service.Application('finger', uid=1, gid=1)
+f = ProxyFingerService()
+internet.TCPServer(7779, IFingerFactory(f)).setServiceParent(
+ service.IServiceCollection(application))
diff --git a/doc/core/howto/tutorial/listings/finger/organized-finger.tac b/doc/core/howto/tutorial/listings/finger/organized-finger.tac
new file mode 100644
index 0000000..2f9a129
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/organized-finger.tac
@@ -0,0 +1,31 @@
+# organized-finger.tac
+# eg: twistd -ny organized-finger.tac
+
+import finger
+
+from twisted.internet import protocol, reactor, defer
+from twisted.spread import pb
+from twisted.web import resource, server
+from twisted.application import internet, service, strports
+from twisted.python import log
+
+application = service.Application('finger', uid=1, gid=1)
+f = finger.FingerService('/etc/users')
+serviceCollection = service.IServiceCollection(application)
+internet.TCPServer(79, finger.IFingerFactory(f)
+ ).setServiceParent(serviceCollection)
+
+site = server.Site(resource.IResource(f))
+internet.TCPServer(8000, site
+ ).setServiceParent(serviceCollection)
+
+internet.SSLServer(443, site, finger.ServerContextFactory()
+ ).setServiceParent(serviceCollection)
+
+i = finger.IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+internet.TCPClient('irc.freenode.org', 6667, i
+ ).setServiceParent(serviceCollection)
+
+internet.TCPServer(8889, pb.PBServerFactory(finger.IPerspectiveFinger(f))
+ ).setServiceParent(serviceCollection)
diff --git a/doc/core/howto/tutorial/listings/finger/simple-finger.tac b/doc/core/howto/tutorial/listings/finger/simple-finger.tac
new file mode 100644
index 0000000..2e75cb1
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/simple-finger.tac
@@ -0,0 +1,17 @@
+# simple-finger.tac
+# eg: twistd -ny simple-finger.tac
+
+from twisted.application import service
+
+import finger
+
+options = { 'file': '/etc/users',
+ 'templates': '/usr/share/finger/templates',
+ 'ircnick': 'fingerbot',
+ 'ircserver': 'irc.freenode.net',
+ 'pbport': 8889,
+ 'ssl': 'ssl=0' }
+
+ser = finger.makeService(options)
+application = service.Application('finger', uid=1, gid=1)
+ser.setServiceParent(service.IServiceCollection(application))
diff --git a/doc/core/howto/tutorial/listings/finger/twisted/plugins/finger_tutorial.py b/doc/core/howto/tutorial/listings/finger/twisted/plugins/finger_tutorial.py
new file mode 100644
index 0000000..73361ae
--- /dev/null
+++ b/doc/core/howto/tutorial/listings/finger/twisted/plugins/finger_tutorial.py
@@ -0,0 +1,5 @@
+
+from twisted.application.service import ServiceMaker
+
+finger = ServiceMaker(
+ 'finger', 'finger.tap', 'Run a finger service', 'finger')
diff --git a/doc/core/howto/tutorial/pb.html b/doc/core/howto/tutorial/pb.html
new file mode 100644
index 0000000..5cdfb04
--- /dev/null
+++ b/doc/core/howto/tutorial/pb.html
@@ -0,0 +1,728 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: Twisted client support using Perspective Broker</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: Twisted client support using Perspective Broker</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Use Perspective Broker</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the seventh part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we add a Perspective Broker service to the finger application
+so that Twisted clients can access the finger server. Perspective Broker is
+introduced in depth in its own <a href="../index.html#pb" shape="rect">section</a> of the
+core howto index.</p>
+
+<h2>Use Perspective Broker<a name="auto1"/></h2>
+
+<p>We add support for perspective broker, Twisted's native remote object
+protocol. Now, Twisted clients will not have to go through XML-RPCish
+contortions to get information about users.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">microdom</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>=<span class="py-src-variable">service</span>
+
+ <span class="py-src-comment"># add a specific child for the path &quot;RPC2&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;RPC2&quot;</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-comment"># need to do this for resources at the root of the site</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;&quot;</span>, <span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">users</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">userOutput</span> = <span class="py-src-string">''</span>.<span class="py-src-variable">join</span>([<span class="py-src-string">&quot;&lt;li&gt;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;&quot;</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>])
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;
+ &lt;html&gt;&lt;head&gt;&lt;title&gt;Users&lt;/title&gt;&lt;/head&gt;&lt;body&gt;
+ &lt;h1&gt;Users&lt;/h1&gt;
+ &lt;ul&gt;
+ %s
+ &lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % <span class="py-src-variable">userOutput</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">user</span>=<span class="py-src-variable">path</span>, <span class="py-src-variable">service</span>=<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">status</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;%s&lt;/title&gt;&lt;/head&gt;
+ &lt;body&gt;&lt;h1&gt;%s&lt;/h1&gt;
+ &lt;p&gt;%s&lt;/p&gt;
+ &lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>))
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IPerspectiveFinger</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">username</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a user's status.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PerspectiveFingerFromService</span>(<span class="py-src-parameter">pb</span>.<span class="py-src-parameter">Root</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPerspectiveFinger</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">username</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">PerspectiveFingerFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IPerspectiveFinger</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8889</span>, <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBServerFactory</span>(<span class="py-src-variable">IPerspectiveFinger</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger21.tac"><span class="filename">listings/finger/finger21.tac</span></a></div></div>
+
+<p>A simple client to test the perspective broker finger:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-comment"># test the PB finger on port 8889</span>
+<span class="py-src-comment"># this code is essentially the same as</span>
+<span class="py-src-comment"># the first example in howto/pb-usage</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">spread</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pb</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">gotObject</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;got object:&quot;</span>, <span class="py-src-variable">object</span>
+ <span class="py-src-variable">object</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">&quot;getUser&quot;</span>,<span class="py-src-string">&quot;moshez&quot;</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">gotData</span>)
+<span class="py-src-comment"># or</span>
+<span class="py-src-comment"># object.callRemote(&quot;getUsers&quot;).addCallback(gotData)</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">gotData</span>(<span class="py-src-parameter">data</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'server sent:'</span>, <span class="py-src-variable">data</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">gotNoObject</span>(<span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;no object:&quot;</span>,<span class="py-src-variable">reason</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">pb</span>.<span class="py-src-variable">PBClientFactory</span>()
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">connectTCP</span>(<span class="py-src-string">&quot;127.0.0.1&quot;</span>,<span class="py-src-number">8889</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">getRootObject</span>().<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">gotObject</span>,<span class="py-src-variable">gotNoObject</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/finger/fingerPBclient.py"><span class="filename">listings/finger/fingerPBclient.py</span></a></div></div>
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/protocol.html b/doc/core/howto/tutorial/protocol.html
new file mode 100644
index 0000000..d3915bb
--- /dev/null
+++ b/doc/core/howto/tutorial/protocol.html
@@ -0,0 +1,1121 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: adding features to the finger service</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: adding features to the finger service</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Setting Message By Local Users</a></li><li><a href="#auto2">Use Services to Make Dependencies Sane</a></li><li><a href="#auto3">Read Status File</a></li><li><a href="#auto4">Announce on Web, Too</a></li><li><a href="#auto5">Announce on IRC, Too</a></li><li><a href="#auto6">Add XML-RPC Support</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the second part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this section of the tutorial, our finger server will continue to sprout
+features: the ability for users to set finger announces, and using our finger
+service to send those announcements on the web, on IRC and over XML-RPC.
+Resources and XML-RPC are introduced in the Web Applications portion of
+the <a href="../../../web/howto/index.html" shape="rect">Twisted Web howto</a>. More examples
+using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.protocols.irc.html" title="twisted.words.protocols.irc">twisted.words.protocols.irc</a></code> can be found
+in <a href="../clients.html" shape="rect">Writing a TCP Client</a> and
+the <a href="../../../words/examples/index.html" shape="rect">Twisted Words examples</a>.</p>
+
+<h2>Setting Message By Local Users<a name="auto1"/></h2>
+
+<p>Now that port 1079 is free, maybe we can use it with a different
+server, one which will let people set their messages. It does
+no access control, so anyone who can login to the machine can
+set any message. We assume this is the desired behavior in
+our case. Testing it can be done by simply:
+</p>
+
+<pre class="shell" xml:space="preserve">
+% nc localhost 1079 # or telnet localhost 1079
+moshez
+Giving a tutorial now, sorry!
+^D
+</pre>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+</p><span class="py-src-comment"># But let's try and fix setting away messages, shall we?</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>[<span class="py-src-number">1</span>]
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">fingerFactory</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">fingerFactory</span> = <span class="py-src-variable">fingerFactory</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">fingerFactory</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+
+<span class="py-src-variable">ff</span> = <span class="py-src-variable">FingerFactory</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>)
+<span class="py-src-variable">fsf</span> = <span class="py-src-variable">FingerSetterFactory</span>(<span class="py-src-variable">ff</span>)
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>,<span class="py-src-variable">ff</span>).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">1079</span>,<span class="py-src-variable">fsf</span>).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger12.tac"><span class="filename">listings/finger/finger12.tac</span></a></div></div>
+
+<p>This program has two protocol-factory-TCPServer pairs, which are
+both child services of the application. Specifically,
+the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Service.setServiceParent.html" title="twisted.application.service.Service.setServiceParent">setServiceParent</a></code>
+method is used to define the two TCPServer services as children
+of <code>application</code>, which implements <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IServiceCollection.html" title="twisted.application.service.IServiceCollection">IServiceCollection</a></code>. Both
+services are thus started with the application.</p>
+
+
+<h2>Use Services to Make Dependencies Sane<a name="auto2"/></h2>
+
+<p>The previous version had the setter poke at the innards of the
+finger factory. This strategy is usually not a good idea: this version makes
+both factories symmetric by making them both look at a single
+object. Services are useful for when an object is needed which is
+not related to a specific network server. Here, we define a common service
+class with methods that will create factories on the fly. The service
+also contains methods the factories will depend on.</p>
+
+<p>The factory-creation methods, <code>getFingerFactory</code>
+and <code>getFingerSetterFactory</code>, follow this pattern:</p>
+
+<ol>
+
+<li>Instantiate a generic server
+factory, <code>twisted.internet.protocol.ServerFactory</code>.</li>
+
+<li>Set the protocol class, just like our factory class would have.</li>
+
+<li>Copy a service method to the factory as a function attribute. The
+function won't have access to the factory's <code>self</code>, but
+that's OK because as a bound method it has access to the
+service's <code>self</code>, which is what it needs.
+For <code>getUser</code>, a custom method defined in the service gets
+copied. For <code>setUser</code>, a standard method of
+the <code>users</code> dictionary is copied.</li>
+
+
+</ol>
+
+<p>Thus, we stopped subclassing: the service simply puts useful methods and
+attributes inside the factories. We are getting better at protocol design:
+none of our protocol classes had to be changed, and neither will have to
+change until the end of the tutorial.</p>
+
+<p>As an application service, this new finger service implements the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.IService.html" title="twisted.application.service.IService">IService</a></code> interface and
+can be started and stopped in a standardized manner. We'll make use of this in
+the next example.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+</p><span class="py-src-comment"># Fix asymmetry</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>,<span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>[<span class="py-src-number">0</span>]
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>[<span class="py-src-number">1</span>]
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, **<span class="py-src-parameter">kwargs</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">kwargs</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerSetterFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">setUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">setUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-variable">moshez</span>=<span class="py-src-string">'Happy and well'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>,<span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">1079</span>,<span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerSetterFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger13.tac"><span class="filename">listings/finger/finger13.tac</span></a></div></div>
+
+<p>Most application services will want to use the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Service.html" title="twisted.application.service.Service">Service</a></code> base class, which implements
+all the generic <code>IService</code> behavior.</p>
+
+<h2>Read Status File<a name="auto3"/></h2>
+
+<p>This version shows how, instead of just letting users set their
+messages, we can read those from a centrally managed file. We cache
+results, and every 30 seconds we refresh it. Services are useful
+for such scheduled tasks.</p>
+
+<div class="listing"><pre>
+moshez: happy and well
+shawn: alive
+</pre><div class="caption">sample /etc/users file - <a href="listings/finger/etc.users"><span class="filename">listings/finger/etc.users</span></a></div></div>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+</p><span class="py-src-comment"># Read from file</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">finger</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>())
+
+<span class="py-src-variable">finger</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>))
+</pre><div class="caption">Source listing - <a href="listings/finger/finger14.tac"><span class="filename">listings/finger/finger14.tac</span></a></div></div>
+
+<p>Since this version is reading data from a file (and refreshing the data
+every 30 seconds), there is no <code>FingerSetterFactory</code> and thus
+nothing listening on port 1079.</p>
+
+<p>Here we override the standard <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Service.startService.html" title="twisted.application.service.Service.startService">startService</a></code>
+and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.application.service.Service.stopService.html" title="twisted.application.service.Service.stopService">stopService</a></code> hooks in
+the Finger service, which is set up as a child service of the
+application in the last line of the code. <code>startService</code>
+calls <code>_read</code>, the function responsible for reading the
+data; <code>reactor.callLater</code> is then used to schedule it to
+run again after thirty seconds every time it is
+called. <code>reactor.callLater</code> returns an object that lets us
+cancel the scheduled run in <code>stopService</code> using
+its <code>cancel</code> method.</p>
+
+<h2>Announce on Web, Too<a name="auto4"/></h2>
+
+<p>The same kind of service can also produce things useful for other
+protocols. For example, in twisted.web, the factory itself
+(<code base="API" class="twisted.web.server">Site</code>) is almost
+never subclassed — instead, it is given a resource, which
+represents the tree of resources available via URLs. That hierarchy is
+navigated by <code base="API" class="twisted.web.server">Site</code>
+and overriding it dynamically is possible with <code base="API" class="twisted.web.resource.Resource">getChild</code>.</p>
+
+<p>To integrate this into the Finger application (just because we can), we set
+up a new TCPServer that calls the <code base="API" class="twisted.web.server">Site</code> factory and retrieves resources via a
+new function of <code>FingerService</code> named <code>getResource</code>.
+This function specifically returns a <code base="API" class="twisted.web.resource">Resource</code> object with an overridden <code base="API" class="twisted.web.resource.Resource">getChild</code> method.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+</p><span class="py-src-comment"># Read from file, announce on the web!</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerResource</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = <span class="py-src-variable">users</span>
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-comment"># we treat the path as the username</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">username</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ 'username' is a string.
+ 'request' is a 'twisted.web.server.Request'.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-variable">messagevalue</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">username</span>)
+ <span class="py-src-variable">username</span> = <span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>(<span class="py-src-variable">username</span>)
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">messagevalue</span> <span class="py-src-keyword">is</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">None</span>:
+ <span class="py-src-variable">messagevalue</span> = <span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>(<span class="py-src-variable">messagevalue</span>)
+ <span class="py-src-variable">text</span> = <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;&lt;p&gt;%s&lt;/p&gt;'</span> % (<span class="py-src-variable">username</span>,<span class="py-src-variable">messagevalue</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-variable">text</span> = <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;&lt;p&gt;No such user&lt;/p&gt;'</span> % <span class="py-src-variable">username</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">static</span>.<span class="py-src-variable">Data</span>(<span class="py-src-variable">text</span>, <span class="py-src-string">'text/html'</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getResource</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">FingerResource</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">r</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">f</span>.<span class="py-src-variable">getResource</span>())
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger15.tac"><span class="filename">listings/finger/finger15.tac</span></a></div></div>
+
+
+<h2>Announce on IRC, Too<a name="auto5"/></h2>
+
+<p>This is the first time there is client code. IRC clients often act a lot like
+servers: responding to events from the network. The reconnecting client factory
+will make sure that severed links will get re-established, with intelligent
+tweaked exponential back-off algorithms. The IRC client itself is simple: the
+only real hack is getting the nickname from the factory
+in <code>connectionMade</code>.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+</p><span class="py-src-comment"># Read from file, announce on the web, irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">user</span>, <span class="py-src-variable">msg</span>+<span class="py-src-string">': '</span>+<span class="py-src-variable">message</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getResource</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>()
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">getChild</span> = (<span class="py-src-keyword">lambda</span> <span class="py-src-variable">path</span>, <span class="py-src-variable">request</span>:
+ <span class="py-src-variable">static</span>.<span class="py-src-variable">Data</span>(<span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;&lt;p&gt;%s&lt;/p&gt;'</span> %
+ <span class="py-src-variable">tuple</span>(<span class="py-src-variable">map</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>,
+ [<span class="py-src-variable">path</span>,<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">path</span>,
+ <span class="py-src-string">&quot;No such user &lt;p/&gt; usage: site/user&quot;</span>)])),
+ <span class="py-src-string">'text/html'</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">r</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getIRCBot</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">nickname</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ReconnectingClientFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">f</span>.<span class="py-src-variable">getResource</span>())
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getIRCBot</span>(<span class="py-src-string">'fingerbot'</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger16.tac"><span class="filename">listings/finger/finger16.tac</span></a></div></div>
+
+<p><code>FingerService</code> now has another new
+function, <code>getIRCbot</code>, which returns
+the <code>ReconnectingClientFactory</code>. This factory in turn will
+instantiate the <code>IRCReplyBot</code> protocol. The IRCBot is
+configured in the last line to connect
+to <code>irc.freenode.org</code> with a nickname
+of <code>fingerbot</code>.</p>
+
+<p>By
+overriding <code>irc.IRCClient.connectionMade</code>, <code>IRCReplyBot</code>
+can access the <code>nickname</code> attribute of the factory that
+instantiated it.</p>
+
+<h2>Add XML-RPC Support<a name="auto6"/></h2>
+
+<p>In Twisted, XML-RPC support is handled just as though it was
+another resource. That resource will still support GET calls normally
+through render(), but that is usually left unimplemented. Note
+that it is possible to return deferreds from XML-RPC methods.
+The client, of course, will not get the answer until the deferred
+is triggered.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+</p><span class="py-src-comment"># Read from file, announce on the web, irc, xml-rpc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">message</span> + <span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">onError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Internal error in server'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">onError</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeResponse</span>(<span class="py-src-parameter">message</span>):
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">user</span>, <span class="py-src-variable">msg</span>+<span class="py-src-string">': '</span>+<span class="py-src-variable">message</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeResponse</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getResource</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>()
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">getChild</span> = (<span class="py-src-keyword">lambda</span> <span class="py-src-variable">path</span>, <span class="py-src-variable">request</span>:
+ <span class="py-src-variable">static</span>.<span class="py-src-variable">Data</span>(<span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;&lt;p&gt;%s&lt;/p&gt;'</span> %
+ <span class="py-src-variable">tuple</span>(<span class="py-src-variable">map</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>,
+ [<span class="py-src-variable">path</span>,<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">path</span>, <span class="py-src-string">&quot;No such user&quot;</span>)])),
+ <span class="py-src-string">'text/html'</span>))
+ <span class="py-src-variable">x</span> = <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>()
+ <span class="py-src-variable">x</span>.<span class="py-src-variable">xmlrpc_getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">x</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">r</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getIRCBot</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">nickname</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ReconnectingClientFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">f</span>.<span class="py-src-variable">getResource</span>())
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getIRCBot</span>(<span class="py-src-string">'fingerbot'</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger17.tac"><span class="filename">listings/finger/finger17.tac</span></a></div></div>
+
+<p>Instead of a web browser, we can test the XMLRPC finger using a simple
+client based on Python's built-in <code>xmlrpclib</code>, which will access
+the resource we've made available at <code>localhost/RPC2</code>.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-comment"># testing xmlrpc finger</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpclib</span>
+<span class="py-src-variable">server</span> = <span class="py-src-variable">xmlrpclib</span>.<span class="py-src-variable">Server</span>(<span class="py-src-string">'http://127.0.0.1:8000/RPC2'</span>)
+<span class="py-src-keyword">print</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-string">'moshez'</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/fingerXRclient.py"><span class="filename">listings/finger/fingerXRclient.py</span></a></div></div>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/style.html b/doc/core/howto/tutorial/style.html
new file mode 100644
index 0000000..8cc9969
--- /dev/null
+++ b/doc/core/howto/tutorial/style.html
@@ -0,0 +1,333 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: cleaning up the finger code</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: cleaning up the finger code</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Write Readable Code</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the third part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this section of the tutorial, we'll clean up our code so that it is
+closer to a readable and extensible style.</p>
+
+<h2>Write Readable Code<a name="auto1"/></h2>
+
+<p>The last version of the application had a lot of hacks. We avoided
+sub-classing, didn't support things like user listings over the web,
+and removed all blank lines -- all in the interest of code
+which is shorter. Here we take a step back, subclass what is more
+naturally a subclass, make things which should take multiple lines
+take them, etc. This shows a much better style of developing Twisted
+applications, though the hacks in the previous stages are sometimes
+used in throw-away prototypes.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+</p><span class="py-src-comment"># Do everything properly</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">formatUsers</span>(<span class="py-src-parameter">users</span>):
+ <span class="py-src-variable">l</span> = [<span class="py-src-string">'&lt;li&gt;&lt;a href=&quot;%s&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;'</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>]
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;ul&gt;'</span>+<span class="py-src-string">''</span>.<span class="py-src-variable">join</span>(<span class="py-src-variable">l</span>)+<span class="py-src-string">'&lt;/ul&gt;'</span>
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">formatUsers</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">path</span>==<span class="py-src-string">&quot;&quot;</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">path</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>:
+ <span class="py-src-string">'&lt;h1&gt;%s&lt;/h1&gt;'</span>%<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>+<span class="py-src-string">'&lt;p&gt;%s&lt;/p&gt;'</span>%<span class="py-src-variable">m</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>())
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getFingerFactory</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ServerFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getResource</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">UserStatusTree</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">x</span> = <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">x</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">r</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getIRCBot</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">nickname</span>):
+ <span class="py-src-variable">f</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ReconnectingClientFactory</span>()
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">f</span>.<span class="py-src-variable">getUser</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">getUser</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">f</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getFingerFactory</span>()
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">f</span>.<span class="py-src-variable">getResource</span>())
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">f</span>.<span class="py-src-variable">getIRCBot</span>(<span class="py-src-string">'fingerbot'</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger18.tac"><span class="filename">listings/finger/finger18.tac</span></a></div></div>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/tutorial/web.html b/doc/core/howto/tutorial/web.html
new file mode 100644
index 0000000..fca15b7
--- /dev/null
+++ b/doc/core/howto/tutorial/web.html
@@ -0,0 +1,610 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Evolution of Finger: a web frontend</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Evolution of Finger: a web frontend</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p> This is the sixth part of the Twisted tutorial <a href="index.html" shape="rect">Twisted from Scratch, or The Evolution of Finger</a>.</p>
+
+<p>In this part, we demonstrate adding a web frontend using
+simple <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">twisted.web.resource.Resource</a></code>
+objects: <code class="python">UserStatusTree</code>, which will
+produce a listing of all users at the base URL (<code>/</code>) of our
+site; <code class="python">UserStatus</code>, which gives the status
+of each user at the location <code>/username</code>;
+and <code class="python">UserStatusXR</code>, which exposes an XMLRPC
+interface to <code class="python">getUser</code>
+and <code class="python">getUsers</code> functions at the
+URL <code>/RPC2</code>.</p>
+
+<p>In this example we construct HTML segments manually. If the web interface
+was less trivial, we would want to use more sophisticated web templating and
+design our system so that HTML rendering and logic were clearly separated.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+</p><span class="py-src-comment"># Do everything properly, and componentize</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>, <span class="py-src-variable">reactor</span>, <span class="py-src-variable">defer</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">words</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">irc</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">protocols</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">basic</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">components</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">microdom</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>():
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a list of strings.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterService</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Set the user's status to something.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">catchError</span>(<span class="py-src-parameter">err</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Internal error in server&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">writeValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">value</span>+<span class="py-src-string">'\r\n'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">loseConnection</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">writeValue</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IFingerFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterProtocol</span>(<span class="py-src-parameter">basic</span>.<span class="py-src-parameter">LineReceiver</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span> = []
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lineReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">line</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>.<span class="py-src-variable">append</span>(<span class="py-src-variable">line</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">len</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>) == <span class="py-src-number">2</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">setUser</span>(*<span class="py-src-variable">self</span>.<span class="py-src-variable">lines</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IFingerSetterFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol returning a string.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerSetterFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ServerFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerSetterFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">FingerSetterProtocol</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">setUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">status</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">setUser</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>)
+
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">FingerSetterFactoryFromService</span>,
+ <span class="py-src-variable">IFingerSetterService</span>,
+ <span class="py-src-variable">IFingerSetterFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCReplyBot</span>(<span class="py-src-parameter">irc</span>.<span class="py-src-parameter">IRCClient</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionMade</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">nickname</span>
+ <span class="py-src-variable">irc</span>.<span class="py-src-variable">IRCClient</span>.<span class="py-src-variable">connectionMade</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">privmsg</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">channel</span>, <span class="py-src-parameter">msg</span>):
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">'!'</span>)[<span class="py-src-number">0</span>]
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">nickname</span>.<span class="py-src-variable">lower</span>() == <span class="py-src-variable">channel</span>.<span class="py-src-variable">lower</span>():
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">factory</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">msg</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">catchError</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-string">&quot;Status of %s: %s&quot;</span> % (<span class="py-src-variable">msg</span>, <span class="py-src-variable">m</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">m</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">msg</span>(<span class="py-src-variable">user</span>, <span class="py-src-variable">m</span>))
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IIRCClientFactory</span>(<span class="py-src-parameter">Interface</span>):
+
+ <span class="py-src-string">&quot;&quot;&quot;
+ @ivar nickname
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">user</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a deferred returning a string.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">addr</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return a protocol.
+ &quot;&quot;&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IRCClientFactoryFromService</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IIRCClientFactory</span>)
+
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">IRCReplyBot</span>
+ <span class="py-src-variable">nickname</span> = <span class="py-src-variable">None</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">IRCClientFactoryFromService</span>,
+ <span class="py-src-variable">IFingerService</span>,
+ <span class="py-src-variable">IIRCClientFactory</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusTree</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>=<span class="py-src-variable">service</span>
+
+ <span class="py-src-comment"># add a specific child for the path &quot;RPC2&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;RPC2&quot;</span>, <span class="py-src-variable">UserStatusXR</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>))
+
+ <span class="py-src-comment"># need to do this for resources at the root of the site</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;&quot;</span>, <span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">users</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">userOutput</span> = <span class="py-src-string">''</span>.<span class="py-src-variable">join</span>([<span class="py-src-string">&quot;&lt;li&gt;&lt;a href=\&quot;%s\&quot;&gt;%s&lt;/a&gt;&lt;/li&gt;&quot;</span> % (<span class="py-src-variable">user</span>, <span class="py-src-variable">user</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">user</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">users</span>])
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;
+ &lt;html&gt;&lt;head&gt;&lt;title&gt;Users&lt;/title&gt;&lt;/head&gt;&lt;body&gt;
+ &lt;h1&gt;Users&lt;/h1&gt;
+ &lt;ul&gt;
+ %s
+ &lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % <span class="py-src-variable">userOutput</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">path</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">UserStatus</span>(<span class="py-src-variable">user</span>=<span class="py-src-variable">path</span>, <span class="py-src-variable">service</span>=<span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>)
+
+<span class="py-src-variable">components</span>.<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">UserStatusTree</span>, <span class="py-src-variable">IFingerService</span>, <span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>)
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatus</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_cb_render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">status</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&quot;&quot;&lt;html&gt;&lt;head&gt;&lt;title&gt;%s&lt;/title&gt;&lt;/head&gt;
+ &lt;body&gt;&lt;h1&gt;%s&lt;/h1&gt;
+ &lt;p&gt;%s&lt;/p&gt;
+ &lt;/body&gt;&lt;/html&gt;&quot;&quot;&quot;</span> % (<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>, <span class="py-src-variable">status</span>))
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">user</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_cb_render_GET</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-comment"># signal that the rendering is not complete</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">UserStatusXR</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">service</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span> = <span class="py-src-variable">service</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUser</span>(<span class="py-src-variable">user</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">service</span>.<span class="py-src-variable">getUsers</span>()
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FingerService</span>(<span class="py-src-parameter">service</span>.<span class="py-src-parameter">Service</span>):
+
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IFingerService</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">filename</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span> = <span class="py-src-variable">filename</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span> = {}
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_read</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">clear</span>()
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">line</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">file</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">filename</span>):
+ <span class="py-src-variable">user</span>, <span class="py-src-variable">status</span> = <span class="py-src-variable">line</span>.<span class="py-src-variable">split</span>(<span class="py-src-string">':'</span>, <span class="py-src-number">1</span>)
+ <span class="py-src-variable">user</span> = <span class="py-src-variable">user</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">status</span> = <span class="py-src-variable">status</span>.<span class="py-src-variable">strip</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>[<span class="py-src-variable">user</span>] = <span class="py-src-variable">status</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">30</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUser</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">user</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">get</span>(<span class="py-src-variable">user</span>, <span class="py-src-string">&quot;No such user&quot;</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getUsers</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">users</span>.<span class="py-src-variable">keys</span>())
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_read</span>()
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">startService</span>(<span class="py-src-variable">self</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopService</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">service</span>.<span class="py-src-variable">Service</span>.<span class="py-src-variable">stopService</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'finger'</span>, <span class="py-src-variable">uid</span>=<span class="py-src-number">1</span>, <span class="py-src-variable">gid</span>=<span class="py-src-number">1</span>)
+<span class="py-src-variable">f</span> = <span class="py-src-variable">FingerService</span>(<span class="py-src-string">'/etc/users'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">f</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">79</span>, <span class="py-src-variable">IFingerFactory</span>(<span class="py-src-variable">f</span>)
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">8000</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>.<span class="py-src-variable">IResource</span>(<span class="py-src-variable">f</span>))
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">IIRCClientFactory</span>(<span class="py-src-variable">f</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">nickname</span> = <span class="py-src-string">'fingerbot'</span>
+<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'irc.freenode.org'</span>, <span class="py-src-number">6667</span>, <span class="py-src-variable">i</span>
+ ).<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">serviceCollection</span>)
+</pre><div class="caption">Source listing - <a href="listings/finger/finger20.tac"><span class="filename">listings/finger/finger20.tac</span></a></div></div>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/udp.html b/doc/core/howto/udp.html
new file mode 100644
index 0000000..756d3d1
--- /dev/null
+++ b/doc/core/howto/udp.html
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: UDP Networking</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">UDP Networking</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">DatagramProtocol</a></li><li><a href="#auto2">Connected UDP</a></li><li><a href="#auto3">Multicast UDP</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Overview<a name="auto0"/></h2>
+
+ <p>Unlike TCP, UDP has no notion of connections. A UDP socket can receive
+ datagrams from any server on the network and send datagrams to any host on
+ the network. In addition, datagrams may arrive in any order, never arrive at
+ all, or be duplicated in transit.</p>
+
+ <p>Since there are no connections, we only use a single object, a protocol,
+ for each UDP socket. We then use the reactor to connect this protocol to a
+ UDP transport, using the
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUDP.html" title="twisted.internet.interfaces.IReactorUDP">twisted.internet.interfaces.IReactorUDP</a></code>
+ reactor API.</p>
+
+ <h2>DatagramProtocol<a name="auto1"/></h2>
+
+ <p>The class where you actually implement the protocol parsing and handling
+ will usually be descended
+ from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.DatagramProtocol.html" title="twisted.internet.protocol.DatagramProtocol">twisted.internet.protocol.DatagramProtocol</a></code> or
+ from one of its convenience children. The <code>DatagramProtocol</code>
+ class receives datagrams and can send them out over the network. Received
+ datagrams include the address they were sent from. When sending datagrams
+ the destination address must be specified.</p>
+
+ <p>Here is a simple example:</p>
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DatagramProtocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Echo</span>(<span class="py-src-parameter">DatagramProtocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">datagramReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>, (<span class="py-src-parameter">host</span>, <span class="py-src-parameter">port</span>)):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;received %r from %s:%d&quot;</span> % (<span class="py-src-variable">data</span>, <span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">data</span>, (<span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>))
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenUDP</span>(<span class="py-src-number">9999</span>, <span class="py-src-variable">Echo</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>As you can see, the protocol is registered with the reactor. This means
+ it may be persisted if it's added to an application, and thus it has
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.AbstractDatagramProtocol.startProtocol.html" title="twisted.internet.protocol.AbstractDatagramProtocol.startProtocol">startProtocol</a></code>
+ and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.protocol.AbstractDatagramProtocol.stopProtocol.html" title="twisted.internet.protocol.AbstractDatagramProtocol.stopProtocol">stopProtocol</a></code>
+ methods that will get called when the protocol is connected and disconnected
+ from a UDP socket.</p>
+
+ <p>The protocol's <code class="python">transport</code> attribute will
+ implement the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IUDPTransport.html" title="twisted.internet.interfaces.IUDPTransport">twisted.internet.interfaces.IUDPTransport</a></code> interface.
+ Notice that the <code class="python">host</code> argument should be an
+ IP address, not a hostname. If you only have the hostname use <code class="python">reactor.resolve()</code> to resolve the address (see <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorCore.resolve.html" title="twisted.internet.interfaces.IReactorCore.resolve">twisted.internet.interfaces.IReactorCore.resolve</a></code>).</p>
+
+
+ <h2>Connected UDP<a name="auto2"/></h2>
+
+ <p>A connected UDP socket is slightly different from a standard one - it
+ can only send and receive datagrams to/from a single address, but this
+ does not in any way imply a connection. Datagrams may still arrive in any
+ order, and the port on the other side may have no one listening. The
+ benefit of the connected UDP socket is that it it <strong>may</strong>
+ provide notification of undelivered packages. This depends on many
+ factors, almost all of which are out of the control of the application,
+ but it still presents certain benefits which occasionally make it
+ useful.</p>
+
+ <p>Unlike a regular UDP protocol, we do not need to specify where to send
+ datagrams and are not told where they came from since they can only come
+ from the address to which the socket is 'connected'.</p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DatagramProtocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Helloer</span>(<span class="py-src-parameter">DatagramProtocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startProtocol</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">host</span> = <span class="py-src-string">&quot;192.168.1.1&quot;</span>
+ <span class="py-src-variable">port</span> = <span class="py-src-number">1234</span>
+
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">connect</span>(<span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>)
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;now we can only send to host %s port %d&quot;</span> % (<span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;hello&quot;</span>) <span class="py-src-comment"># no need for address</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">datagramReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">data</span>, (<span class="py-src-parameter">host</span>, <span class="py-src-parameter">port</span>)):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;received %r from %s:%d&quot;</span> % (<span class="py-src-variable">data</span>, <span class="py-src-variable">host</span>, <span class="py-src-variable">port</span>)
+
+ <span class="py-src-comment"># Possibly invoked if there is no server listening on the</span>
+ <span class="py-src-comment"># address to which we are sending.</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionRefused</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;No one listening&quot;</span>
+
+<span class="py-src-comment"># 0 means any port, we don't care in this case</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenUDP</span>(<span class="py-src-number">0</span>, <span class="py-src-variable">Helloer</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Note that <code class="python">connect()</code>,
+ like <code class="python">write()</code> will only accept IP addresses, not
+ unresolved hostnames. To obtain the IP of a hostname
+ use <code class="python">reactor.resolve()</code>, e.g.:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">gotIP</span>(<span class="py-src-parameter">ip</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;IP of 'example.com' is&quot;</span>, <span class="py-src-variable">ip</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">3</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">resolve</span>(<span class="py-src-string">'example.com'</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">gotIP</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>Connecting to a new address after a previous connection or making a
+ connected port unconnected are not currently supported, but likely will be
+ in the future.</p>
+
+ <h2>Multicast UDP<a name="auto3"/></h2>
+
+ <p>Multicast allows a process to contact multiple hosts with a single
+ packet, without knowing the specific IP address of any of the hosts. This
+ is in contrast to normal, or unicast, UDP, where each datagram has a single
+ IP as its destination. Multicast datagrams are sent to special multicast
+ group addresses (in the IPv4 range 224.0.0.0 to 239.255.255.255), along with
+ a corresponding port. In order to receive multicast datagrams, you must
+ join that specific group address. However, any UDP socket can send to
+ multicast addresses.</p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DatagramProtocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MulticastPingPong</span>(<span class="py-src-parameter">DatagramProtocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startProtocol</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Called after protocol has started listening.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-comment"># Set the TTL&gt;1 so multicast will cross router hops:</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">setTTL</span>(<span class="py-src-number">5</span>)
+ <span class="py-src-comment"># Join a specific multicast group:</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">joinGroup</span>(<span class="py-src-string">&quot;228.0.0.5&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">datagramReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">datagram</span>, <span class="py-src-parameter">address</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Datagram %s received from %s&quot;</span> % (<span class="py-src-variable">repr</span>(<span class="py-src-variable">datagram</span>), <span class="py-src-variable">repr</span>(<span class="py-src-variable">address</span>))
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">datagram</span> == <span class="py-src-string">&quot;Client: Ping&quot;</span>:
+ <span class="py-src-comment"># Rather than replying to the group multicast address, we send the</span>
+ <span class="py-src-comment"># reply directly (unicast) to the originating port:</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;Server: Pong&quot;</span>, <span class="py-src-variable">address</span>)
+
+
+<span class="py-src-comment"># We use listenMultiple=True so that we can run MulticastServer.py and</span>
+<span class="py-src-comment"># MulticastClient.py on same machine:</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenMulticast</span>(<span class="py-src-number">8005</span>, <span class="py-src-variable">MulticastPingPong</span>(),
+ <span class="py-src-variable">listenMultiple</span>=<span class="py-src-variable">True</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/udp/MulticastServer.py"><span class="filename">listings/udp/MulticastServer.py</span></a></div></div>
+
+ <p>As with UDP, with multicast there is no server/client differentiation
+ at the protocol level. Our server example is very simple and closely
+ resembles a normal <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorUDP.listenUDP.html" title="twisted.internet.interfaces.IReactorUDP.listenUDP">listenUDP</a></code>
+ protocol implementation. The main difference is that instead
+ of <code>listenUDP</code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorMulticast.listenMulticast.html" title="twisted.internet.interfaces.IReactorMulticast.listenMulticast">listenMulticast</a></code>
+ is called with the port number. The server calls <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IMulticastTransport.joinGroup.html" title="twisted.internet.interfaces.IMulticastTransport.joinGroup">joinGroup</a></code> to
+ join a multicast group. A <code class="python">DatagramProtocol</code>
+ that is listening with multicast and has joined a group can receive
+ multicast datagrams, but also unicast datagrams sent directly to its
+ address. The server in the example above sends such a unicast message in
+ reply to the multicast message it receives from the client.
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DatagramProtocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MulticastPingClient</span>(<span class="py-src-parameter">DatagramProtocol</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startProtocol</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-comment"># Join the multicast address, so we can receive replies:</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">joinGroup</span>(<span class="py-src-string">&quot;228.0.0.5&quot;</span>)
+ <span class="py-src-comment"># Send to 228.0.0.5:8005 - all listeners on the multicast address</span>
+ <span class="py-src-comment"># (including us) will receive this message.</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">transport</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">'Client: Ping'</span>, (<span class="py-src-string">&quot;228.0.0.5&quot;</span>, <span class="py-src-number">8005</span>))
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">datagramReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">datagram</span>, <span class="py-src-parameter">address</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Datagram %s received from %s&quot;</span> % (<span class="py-src-variable">repr</span>(<span class="py-src-variable">datagram</span>), <span class="py-src-variable">repr</span>(<span class="py-src-variable">address</span>))
+
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenMulticast</span>(<span class="py-src-number">8005</span>, <span class="py-src-variable">MulticastPingClient</span>(), <span class="py-src-variable">listenMultiple</span>=<span class="py-src-variable">True</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/udp/MulticastClient.py"><span class="filename">listings/udp/MulticastClient.py</span></a></div></div>
+
+ <p>Note that a multicast socket will have a default TTL (time to live) of
+ 1. That is, datagrams won't traverse more than one router hop, unless a
+ higher TTL is set with
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IMulticastTransport.setTTL.html" title="twisted.internet.interfaces.IMulticastTransport.setTTL">setTTL</a></code>. Other
+ functionality provided by the multicast transport
+ includes <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IMulticastTransport.setOutgoingInterface.html" title="twisted.internet.interfaces.IMulticastTransport.setOutgoingInterface">setOutgoingInterface</a></code>
+ and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IMulticastTransport.setLoopbackMode.html" title="twisted.internet.interfaces.IMulticastTransport.setLoopbackMode">setLoopbackMode</a></code>
+ -- see <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IMulticastTransport.html" title="twisted.internet.interfaces.IMulticastTransport">IMulticastTransport</a></code> for more
+ information.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/howto/vision.html b/doc/core/howto/vision.html
new file mode 100644
index 0000000..cd4997d
--- /dev/null
+++ b/doc/core/howto/vision.html
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: The Vision For Twisted</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">The Vision For Twisted</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+
+ <p>Many other documents in this repository are dedicated to
+ defining what Twisted is. Here, I will attempt to explain not
+ what Twisted is, but what it should be, once I've met my goals
+ with it.</p>
+
+ <p>First, Twisted should be fun. It began as a game, it is
+ being used commercially in games, and it will be, I hope, an
+ interactive and entertaining experience for the end-user.</p>
+
+ <p>Twisted is a platform for developing internet applications.
+ While Python by itself is a very powerful language, there are
+ many facilities it lacks which other languages have spent great
+ attention to adding. It can do this now; Twisted is a good (if
+ somewhat idiosyncratic) pure-python framework or library,
+ depending on how you treat it, and it continues to improve.</p>
+
+ <p>As a platform, Twisted should be focused on integration.
+ Ideally, all functionality will be accessible through all
+ protocols. Failing that, all functionality should be
+ configurable through at least one protocol, with a seamless and
+ consistent user-interface. The next phase of development will
+ be focusing strongly on a configuration system which will unify
+ many disparate pieces of the current infrastructure, and allow
+ them to be tacked together by a non-programmer.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/img/TwistedLogo.bmp b/doc/core/img/TwistedLogo.bmp
new file mode 100644
index 0000000..940ede0
--- /dev/null
+++ b/doc/core/img/TwistedLogo.bmp
Binary files differ
diff --git a/doc/core/img/cred-login.dia b/doc/core/img/cred-login.dia
new file mode 100644
index 0000000..f9dfaa7
--- /dev/null
+++ b/doc/core/img/cred-login.dia
Binary files differ
diff --git a/doc/core/img/cred-login.png b/doc/core/img/cred-login.png
new file mode 100644
index 0000000..a27dff4
--- /dev/null
+++ b/doc/core/img/cred-login.png
Binary files differ
diff --git a/doc/core/img/deferred-attach.dia b/doc/core/img/deferred-attach.dia
new file mode 100644
index 0000000..9e42967
--- /dev/null
+++ b/doc/core/img/deferred-attach.dia
Binary files differ
diff --git a/doc/core/img/deferred-attach.png b/doc/core/img/deferred-attach.png
new file mode 100644
index 0000000..8050058
--- /dev/null
+++ b/doc/core/img/deferred-attach.png
Binary files differ
diff --git a/doc/core/img/deferred-process.dia b/doc/core/img/deferred-process.dia
new file mode 100644
index 0000000..37c5dd3
--- /dev/null
+++ b/doc/core/img/deferred-process.dia
Binary files differ
diff --git a/doc/core/img/deferred-process.png b/doc/core/img/deferred-process.png
new file mode 100644
index 0000000..d4047eb
--- /dev/null
+++ b/doc/core/img/deferred-process.png
Binary files differ
diff --git a/doc/core/img/deferred-states.svg b/doc/core/img/deferred-states.svg
new file mode 100644
index 0000000..cc8e8da
--- /dev/null
+++ b/doc/core/img/deferred-states.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 -1 952 869" width="952pt" height="869pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2010-02-21 23:40Z</dc:date><!-- Produced by OmniGraffle Professional 5.2.1 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="black"><g><path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="943" height="859"/><g><title>Layer 1</title><g><use xl:href="#id34_Graphic" filter="url(#Shadow)"/><use xl:href="#id5_Graphic" filter="url(#Shadow)"/><use xl:href="#id7_Graphic" filter="url(#Shadow)"/><use xl:href="#id27_Graphic" filter="url(#Shadow)"/><use xl:href="#id43_Graphic" filter="url(#Shadow)"/><use xl:href="#id44_Graphic" filter="url(#Shadow)"/><use xl:href="#id55_Graphic" filter="url(#Shadow)"/><use xl:href="#id79_Graphic" filter="url(#Shadow)"/></g><g id="id34_Graphic"><ellipse cx="239.63925" cy="106.633636" rx="51.00007" ry="20.000038" fill="white"/><ellipse cx="239.63925" cy="106.633636" rx="51.00007" ry="20.000038" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(203.83925 92.63363)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="16.458202" y="11" textLength="21.339844">Unfi</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="37.798046" y="11" textLength="17.34375">red</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="1.1183586" y="25" textLength="69.36328">No Canceller</tspan></text></g><g id="id5_Graphic"><ellipse cx="165.63925" cy="584.63367" rx="57.500095" ry="26.000063" fill="white"/><ellipse cx="165.63925" cy="584.63367" rx="57.500095" ry="26.000063" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(124.639244 570.63367)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="5.9785156" y="11" textLength="73.376953">Synchronous </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="23.993164" y="25" textLength="34.013672">Result</tspan></text></g><g id="id7_Graphic"><ellipse cx="608.9178" cy="528.56982" rx="69.00013" ry="31.957996" fill="white"/><ellipse cx="608.9178" cy="528.56982" rx="69.00013" ry="31.957996" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(558.71777 521.56976)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".17851639" y="11" textLength="100.04297">Synchronous Error</tspan></text></g><path d="M 240.43747 127.13095 C 243.32816 201.3608 260.02661 277.95673 249.11038 349.84274 C 238.68134 418.52057 203.04144 482.93884 178.32253 549.1927" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 251.42224 126.549065 C 288.27274 188.83333 307.86975 251.1824 361.98477 313.42053 C 414.2224 373.49948 498.65482 433.50983 568.65564 493.54248" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="219.78763" y="324.83887" width="62" height="24" fill="white"/><text transform="translate(224.78763 329.83887)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".32714844" y="11" textLength="51.345703">callback()</tspan></text><rect x="313.9862" y="278.44937" width="59" height="24" fill="white"/><text transform="translate(318.9862 283.44937)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".4970703" y="11" textLength="48.00586">errback()</tspan></text><line x1="277.42453" y1="120.438515" x2="517.71863" y2="208.23016" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="395.30939" y="161.18964" width="53" height="24" fill="white"/><text transform="translate(400.30939 166.18964)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan></text><path d="M 159.00711 558.31494 C 144.4371 500.496 113.50804 416.50674 115.292725 384.84082 C 117.07741 353.1749 161.24283 339.42096 169.71629 368.30038 C 177.75905 395.71185 168.38503 485.11023 166.39159 548.2332" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="68.465515" y="328.755" width="138" height="52" fill="white"/><text transform="translate(73.465515 333.755)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="18.639648" y="11" textLength="90.720703">invoke user code</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".29101562" y="25" textLength="127.41797">added with addCallback</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="17.640625" y="39" textLength="92.71875">that returns value</tspan></text><path d="M 546.85925 514.01202 C 524.109 508.67526 503.64008 496.9103 478.60172 498.00012 C 453.56335 499.08994 423.941 513.03003 396.61398 520.5515 C 369.28696 528.073 344.87448 534.74982 314.6232 543.13367 C 287.24933 550.7201 255.07932 559.71246 224.62679 568.20135" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="354.86337" y="498.65527" width="103" height="38" fill="white"/><text transform="translate(359.86337 503.65527)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="5.8125" y="11" textLength="81.375">invoke callback</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".140625" y="25" textLength="92.71875">that returns value</tspan></text><path d="M 146.4648 609.59656 C 127.38432 634.43707 98.757896 685.20398 89.21766 684.1256 C 79.67742 683.04724 85.01394 617.64142 89.21766 603.1256 C 91.945145 593.70734 98.213043 594.7976 104.57533 596.1692" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="9.500664" y="631.5583" width="149" height="38" fill="white"/><text transform="translate(14.500664 636.5583)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="43.827148" y="11" textLength="51.345703">callback()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".4736328" y="25" textLength="33.339844">(raise </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="33.157227" y="25" textLength="105.36914">AlreadyCalledError)</tspan></text><g id="id27_Graphic"><ellipse cx="583.5811" cy="232.29474" rx="65.500122" ry="39.66116" fill="white"/><ellipse cx="583.5811" cy="232.29474" rx="65.500122" ry="39.66116" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(536.1811 204.29474)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="12.378517" y="11" textLength="73.376953">Synchronous </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="34.066994" y="25" textLength="26.666016">Error</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="16.881447" y="39" textLength="64.371094">+ Suppress </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="10.046486" y="53" textLength="74.70703">AlreadyCalled</tspan></text></g><path d="M 542.61957 263.74374 C 519.5946 281.42157 465.79904 277.82715 473.5378 316.78256 C 480.92667 353.97662 544.4154 429.981 583.654 489.20453" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="398.86932" y="278.41492" width="154" height="38" fill="white"/><text transform="translate(403.86932 283.41492)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="47.99707" y="11" textLength="48.00586">errback()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".3046875" y="25" textLength="143.390625">(no-op, discard other error)</tspan></text><path d="M 614.46655 267.7644 C 635.61487 292.05173 676.88593 302.49948 677.9178 340.63364 C 678.88934 376.53894 644.1884 437.00916 624.19873 487.34406" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="610.0517" y="349.84274" width="127" height="38" fill="white"/><text transform="translate(615.0517 354.84274)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="32.827148" y="11" textLength="51.345703">callback()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".4716797" y="25" textLength="116.05664">(no-op, discard value)</tspan></text><path d="M 865.07477 277.16257 C 837.2231 312.98267 794.7537 329.3515 781.5113 384.63364 C 768.84692 437.50256 782.9063 525.99194 785.3534 598.9937" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="796.4464" y="306.68784" width="53" height="24" fill="white"/><text transform="translate(801.4464 311.68784)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan></text><g id="id43_Graphic"><ellipse cx="880.2593" cy="257.63364" rx="51.000122" ry="20.000027" fill="white"/><ellipse cx="880.2593" cy="257.63364" rx="51.000122" ry="20.000027" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(844.45935 243.63364)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="2.7912102" y="11" textLength="21.339844">Unfi</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="24.131054" y="11" textLength="48.01172">red With </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="10.4552727" y="25" textLength="50.689453">Canceller</tspan></text></g><g id="id44_Graphic"><ellipse cx="652.27655" cy="35.647697" rx="51.000122" ry="20.000032" fill="white"/><ellipse cx="652.27655" cy="35.647697" rx="51.000122" ry="20.000032" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(616.47656 21.647697)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="10.789257" y="11" textLength="53.35547">Does Not </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="22.798046" y="25" textLength="26.003906">Exist</tspan></text></g><path d="M 684.58026 51.48888 C 727.7895 72.677933 783.1651 84.063606 814.2209 115.062386 C 843.01117 143.799835 850.91644 189.40921 866.9766 228.42392" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 601.12073 37.995434 C 537.89252 40.89722 465.22925 37.506042 411.41708 46.701664 C 361.41751 55.245777 327.66455 74.66111 287.52023 89.79429" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="384.8317" y="33.75193" width="65" height="24" fill="white"/><text transform="translate(389.8317 38.75193)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".16015625" y="11" textLength="54.679688">Deferred()</tspan></text><rect x="734.8824" y="117.07918" width="182" height="24" fill="white"/><text transform="translate(739.8824 122.07918)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x=".13378906" y="11" textLength="171.73242">Deferred(canceller=myFunction)</tspan></text><path d="M 154.78345 558.62067 C 143.58713 531.7917 134.88046 484.20755 121.191124 478.12564 C 107.50179 472.04373 70.650887 508.01828 72.639236 522.1256 C 74.37477 534.43927 103.80231 545.9059 124.66333 557.63324" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="62.292725" y="475.72116" width="53" height="38.00003" fill="white"/><text transform="translate(67.292725 480.72116)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="2.1582031" y="25" textLength="38.683594">(no-op)</tspan></text><path d="M 675.53845 519.35144 C 691.08 517.20093 717.54523 522.37872 722.1678 512.8993 C 726.79034 503.4199 715.73834 464.34906 703.27655 462.4693 C 692.5027 460.84415 672.50806 481.53854 655.2522 495.60666" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="693.9308" y="470.06503" width="53" height="38" fill="white"/><text transform="translate(698.9308 475.06503)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="2.1582031" y="25" textLength="38.683594">(no-op)</tspan></text><g id="id55_Graphic"><ellipse cx="482.63913" cy="710.63367" rx="63.500114" ry="26.000063" fill="white"/><ellipse cx="482.63913" cy="710.63367" rx="63.500114" ry="26.000063" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(436.83914 696.63367)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="17.67207" y="11" textLength="11.326172">W</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="28.558788" y="11" textLength="48.703125">aiting on </tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".43964767" y="25" textLength="90.720703">another Deferred</tspan></text></g><path d="M 209.08551 602.09143 L 285.13278 632.64917 L 347.77994 657.3728 L 427.26218 688.7633" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="280.41714" y="637.63763" width="131" height="38" fill="white"/><text transform="translate(285.41714 642.63763)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="15.139648" y="11" textLength="90.720703">invoke user code</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".13671875" y="25" textLength="120.72656">that returns a Deferred</tspan></text><path d="M 568.87152 555.01324 C 540.79688 573.5515 498.9215 589.1156 484.6392 610.63367 C 472.25235 629.29596 480.6141 652.44653 482.73215 674.24408" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="434.83826" y="576.12885" width="131" height="38" fill="white"/><text transform="translate(439.83826 581.12885)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="15.139648" y="11" textLength="90.720703">invoke user code</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".13671875" y="25" textLength="120.72656">that returns a Deferred</tspan></text><path d="M 452.45026 733.90747 C 429.87482 751.31177 382.742 770.89917 384.7172 786.1256 C 386.4858 799.7595 427.62817 809.90436 455.25803 821.24493" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="388.5693" y="745.3466" width="53" height="24" fill="white"/><text transform="translate(393.5693 750.3466)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan></text><path d="M 384.65698 785.1944 C 357.65372 793.17065 333.47244 811.30353 303.63913 809.1256 C 273.80582 806.9477 227.6994 805.1427 205.63925 772.1256 C 184.88138 741.0576 182.89703 673.3456 173.18976 620.71753" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="121.24072" y="744.52844" width="153" height="24" fill="white"/><text transform="translate(126.24072 749.52844)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".4638672" y="11" textLength="142.072266">sub-deferred gives a result</tspan></text><path d="M 384.7172 786.1256 C 521.94653 753.81525 753.54895 727.36633 796.4464 689.1848 C 837.35297 652.77533 706.68738 605.67615 649.87036 563.1833" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="704.6239" y="686.4107" width="155" height="24" fill="white"/><text transform="translate(709.6239 691.4107)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".130859375" y="11" textLength="144.73828">sub-deferred gives an error</tspan></text><g id="id79_Graphic"><circle cx="475.86932" cy="830.9652" r="12.3870745" fill="white"/><circle cx="475.86932" cy="830.9652" r="12.3870745" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><path d="M 488.53668 828.58936 C 530.2334 820.76886 609.3293 820.9651 613.63934 805.1256 C 617.62073 790.4942 557.7912 762.1718 522.5708 739.1314" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="504.27307" y="794.09607" width="218" height="24" fill="white"/><text transform="translate(509.27307 799.09607)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".27148438" y="11" textLength="207.45703">sub-deferred waits on another deferred</tspan></text><path d="M 619.2704 198.54285 C 638.81757 180.05676 658.65106 138.87544 677.9178 143.07907 C 697.1845 147.2827 739.65155 209.5166 734.8824 223.76712 C 730.62415 236.491 688.4826 229.05707 659.19812 228.46265" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="690.22546" y="162.56232" width="53" height="38" fill="white"/><text transform="translate(695.22546 167.56232)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".16015625" y="11" textLength="42.679688">cancel()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="2.1582031" y="25" textLength="38.683594">(no-op)</tspan></text><path d="M 835.5697 267.7302 C 680.54077 302.75528 478.18283 324.035 370.43637 372.81592 C 266.8172 419.72824 250.6402 492.10233 195.77448 553.20007" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="308.44598" y="377.05264" width="62" height="24" fill="white"/><text transform="translate(313.44598 382.05264)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".32714844" y="11" textLength="51.345703">callback()</tspan></text><path d="M 879.15765 278.12851 C 873.2046 388.87375 896.83124 565.581 861.2967 610.39746 C 827.4226 653.1197 739.76782 576.01385 675.2422 550.42566" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="806.2241" y="610.99994" width="59" height="24" fill="white"/><text transform="translate(811.2241 615.99994)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".4970703" y="11" textLength="48.00586">errback()</tspan></text><path d="M 776.9486 417.86966 C 686.0307 426.38492 592.25293 432.92642 504.1676 443.41797 C 419.45862 453.50739 339.96509 467.25562 258.17505 479.38318" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="327.14407" y="432.20013" width="138" height="52" fill="white"/><text transform="translate(332.14407 437.20013)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="42.660156" y="11" textLength="42.679688">cancel()</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".30859375" y="25" textLength="127.38281">(with canceller that calls</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="36.329102" y="39" textLength="55.341797">callback())</tspan></text><path d="M 223.63176 584.20825 C 255.7534 583.97266 266.52045 590.73486 320.00632 583.50134 C 371.20312 576.5774 461.56976 556.82605 534.85626 542.66675" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="294.61975" y="552.19177" width="118" height="52" fill="white"/><text transform="translate(299.61975 557.19177)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="8.6396484" y="11" textLength="90.720703">invoke user code</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".31054688" y="25" textLength="107.378906">that returns Failure /</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="9.9785156" y="39" textLength="88.04297">raises Exception</tspan></text><path d="M 603.0141 560.90674 C 597.15417 593.00348 578.55408 642.6284 585.4326 657.20667 C 592.31116 671.7849 637.78595 664.54333 644.28943 648.38507 C 650.11395 633.9137 634.90002 598.23065 626.91156 569.83502" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="556.41516" y="638.25708" width="118" height="52" fill="white"/><text transform="translate(561.41516 643.25708)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" x="8.6396484" y="11" textLength="90.720703">invoke user code</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x=".31054688" y="25" textLength="107.378906">that returns Failure /</tspan><tspan font-family="Helvetica" font-size="12" font-weight="500" x="9.9785156" y="39" textLength="88.04297">raises Exception</tspan></text></g></g></svg>
diff --git a/doc/core/img/deferred.dia b/doc/core/img/deferred.dia
new file mode 100644
index 0000000..f27410a
--- /dev/null
+++ b/doc/core/img/deferred.dia
Binary files differ
diff --git a/doc/core/img/deferred.png b/doc/core/img/deferred.png
new file mode 100644
index 0000000..069d1d5
--- /dev/null
+++ b/doc/core/img/deferred.png
Binary files differ
diff --git a/doc/core/index.html b/doc/core/index.html
new file mode 100644
index 0000000..37b8b1d
--- /dev/null
+++ b/doc/core/index.html
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Core Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Core Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Core to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Core</li>
+<li><a href="specifications/index.html" shape="rect">Specifications</a>: specification
+documents for elements of Twisted Core</li>
+<li><a href="development/index.html" shape="rect">Development of Twisted</a>: for people who
+want to work on Twisted itself</li>
+</ul>
+<p>An <a href="http://twistedmatrix.com/documents/current/api/" shape="rect">API
+ reference</a> is available on the twistedmatrix web site.</p>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/manhole-man.html b/doc/core/man/manhole-man.html
new file mode 100644
index 0000000..68b6e64
--- /dev/null
+++ b/doc/core/man/manhole-man.html
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: MANHOLE.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">MANHOLE.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>manhole - Connect to a Twisted Manhole service
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>manhole</strong> </p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>manhole is a GTK interface to Twisted Manhole services. You can execute python code as if at an interactive Python console inside a running Twisted process with this.
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>Written by Chris Armstrong, copied from Moshe Zadka's <q>faucet</q> manpage.
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2000-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/manhole.1 b/doc/core/man/manhole.1
new file mode 100644
index 0000000..3d78617
--- /dev/null
+++ b/doc/core/man/manhole.1
@@ -0,0 +1,16 @@
+.TH MANHOLE "1" "August 2001" "" ""
+.SH NAME
+manhole \- Connect to a Twisted Manhole service
+.SH SYNOPSIS
+.B manhole
+.SH DESCRIPTION
+manhole is a GTK interface to Twisted Manhole services. You can execute python code as if at an interactive Python console inside a running Twisted process with this.
+.SH AUTHOR
+Written by Chris Armstrong, copied from Moshe Zadka's "faucet" manpage.
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2000-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/man/pyhtmlizer-man.html b/doc/core/man/pyhtmlizer-man.html
new file mode 100644
index 0000000..ad6a6b0
--- /dev/null
+++ b/doc/core/man/pyhtmlizer-man.html
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: pyhtmlizer.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">pyhtmlizer.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNTAX</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">OPTIONS</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>pyhtmlizer - pretty-print Python source as HTML
+
+</p>
+
+<h2>SYNTAX<a name="auto1"/></h2>
+
+<p>pyhtmlizer [<em>-s|--stylesheet</em> &lt;<em>url</em>&gt;] &lt;<em>filename</em>&gt;
+</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>This generates a HTML document with Python source marked up with span elements. To colorize, provide a stylesheet.
+</p>
+
+<h2>OPTIONS<a name="auto3"/></h2>
+
+<dl><dt><strong>--stylesheet, -s</strong> &lt;<em>url</em>&gt;
+</dt><dd>Links to the stylesheet at &lt;<em>url</em>&gt;.
+</dd>
+
+<dt><strong>--help</strong>
+</dt><dd>Output help information and exit.
+</dd>
+
+<dt><strong>-v</strong>, <strong>--version</strong>
+</dt><dd>Output version information and exit.
+</dd>
+
+</dl>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/pyhtmlizer.1 b/doc/core/man/pyhtmlizer.1
new file mode 100644
index 0000000..9621e60
--- /dev/null
+++ b/doc/core/man/pyhtmlizer.1
@@ -0,0 +1,22 @@
+.TH "pyhtmlizer" "1" "" "Twisted Matrix Laboratories" ""
+.SH "NAME"
+.LP
+pyhtmlizer \- pretty\-print Python source as HTML
+
+.SH "SYNTAX"
+.LP
+pyhtmlizer [\fI\-s|\-\-stylesheet\fR <\fIurl\fR>] <\fIfilename\fR>
+.SH "DESCRIPTION"
+.LP
+This generates a HTML document with Python source marked up with span elements. To colorize, provide a stylesheet.
+.SH "OPTIONS"
+.LP
+.TP
+\fB\-\-stylesheet, \-s\fR <\fIurl\fR>
+Links to the stylesheet at <\fIurl\fR>.
+.TP
+\fB\-\-help\fR
+Output help information and exit.
+.TP
+\fB\-v\fR, \fB\--version\fR
+Output version information and exit.
diff --git a/doc/core/man/tap2deb-man.html b/doc/core/man/tap2deb-man.html
new file mode 100644
index 0000000..d42b2a0
--- /dev/null
+++ b/doc/core/man/tap2deb-man.html
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: TAP2DEB.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">TAP2DEB.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>tap2deb - create Debian packages which wrap .tap files
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>tap2deb</strong> [options]
+</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>Create a ready to upload Debian package in <q>.build</q>
+<dl><dt><strong>-u</strong>, <strong>--unsigned</strong>
+</dt><dd>do not sign the Debian package
+</dd>
+
+<dt><strong>-t</strong>, <strong>--tapfile</strong> <em>&lt;tapfile&gt;</em>
+</dt><dd>Build the application around the given .tap (default twistd.tap)
+</dd>
+
+<dt><strong>-y</strong>, <strong>--type</strong> <em>&lt;type&gt;</em>
+</dt><dd>The configuration has the given type . Allowable types are
+<strong>tap</strong>, <strong>source</strong>, <strong>xml</strong> and <strong>python</strong>.
+The first three types are <strong>mktap</strong> output formats,
+while the last one is a manual building of application
+(see <strong>twistd(1)</strong>, the <strong>-y</strong> option).
+</dd>
+
+<dt><strong>-p</strong>, <strong>--protocol</strong> <em>&lt;protocol&gt;</em>
+</dt><dd>The name of the protocol this will be used to serve. This is intended
+as a part of the description. Default is the name of the tapfile, minus
+any extensions.
+</dd>
+
+<dt><strong>-d</strong>, <strong>--debfile</strong> <em>&lt;debfile&gt;</em>
+</dt><dd>The name of the debian package. Default is 'twisted-'+protocol.
+</dd>
+
+<dt><strong>-V</strong>, <strong>--set-version</strong> <em>&lt;version&gt;</em>
+</dt><dd>The version of the Debian package. The default is 1.0
+</dd>
+
+<dt><strong>-e</strong>, <strong>--description</strong> <em>&lt;description&gt;</em>
+</dt><dd>The one-line description. Default is uninteresting.
+</dd>
+
+<dt><strong>-l</strong>, <strong>--long_description</strong> <em>&lt;long_description&gt;</em>
+</dt><dd>A multi-line description. Default is explanation about
+this being an automatic package created from tap2deb.
+</dd>
+
+<dt><strong>-m</strong>, <strong>--maintainer</strong> <em>&lt;maintainer&gt;</em>
+</dt><dd>The maintainer, as <q>Name Lastname &lt;email address&gt;</q>. This will
+go in the meta-files, as well as be used as the id to sign the package.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Output version information and exit.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>Written by Moshe Zadka, based on twistd's help messages
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2000-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/tap2deb.1 b/doc/core/man/tap2deb.1
new file mode 100644
index 0000000..de52a70
--- /dev/null
+++ b/doc/core/man/tap2deb.1
@@ -0,0 +1,55 @@
+.TH TAP2DEB "1" "July 2001" "" ""
+.SH NAME
+tap2deb \- create Debian packages which wrap .tap files
+.SH SYNOPSIS
+.B tap2deb
+[options]
+.SH DESCRIPTION
+Create a ready to upload Debian package in ".build"
+.TP
+\fB\-u\fR, \fB\--unsigned\fR
+do not sign the Debian package
+.TP
+\fB\-t\fR, \fB\--tapfile\fR \fI<tapfile>\fR
+Build the application around the given .tap (default twistd.tap)
+.TP
+\fB\-y\fR, \fB\--type\fR \fI<type>\fR
+The configuration has the given type . Allowable types are
+\fBtap\fR, \fBsource\fR, \fBxml\fR and \fBpython\fR.
+The first three types are \fBmktap\fR output formats,
+while the last one is a manual building of application
+(see \fBtwistd(1)\fR, the \fB\-y\fR option).
+.TP
+\fB\-p\fR, \fB\--protocol\fR \fI<protocol>\fR
+The name of the protocol this will be used to serve. This is intended
+as a part of the description. Default is the name of the tapfile, minus
+any extensions.
+.TP
+\fB\-d\fR, \fB\--debfile\fR \fI<debfile>\fR
+The name of the debian package. Default is 'twisted-'+protocol.
+.TP
+\fB\-V\fR, \fB\--set-version\fR \fI<version>\fR
+The version of the Debian package. The default is 1.0
+.TP
+\fB\-e\fR, \fB\--description\fR \fI<description>\fR
+The one-line description. Default is uninteresting.
+.TP
+\fB\-l\fR, \fB\--long_description\fR \fI<long_description>\fR
+A multi-line description. Default is explanation about
+this being an automatic package created from tap2deb.
+.TP
+\fB\-m\fR, \fB\--maintainer\fR \fI<maintainer>\fR
+The maintainer, as "Name Lastname <email address>". This will
+go in the meta-files, as well as be used as the id to sign the package.
+.TP
+\fB\--version\fR
+Output version information and exit.
+.SH AUTHOR
+Written by Moshe Zadka, based on twistd's help messages
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2000-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/man/tap2rpm-man.html b/doc/core/man/tap2rpm-man.html
new file mode 100644
index 0000000..8d4535f
--- /dev/null
+++ b/doc/core/man/tap2rpm-man.html
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: TAP2RPM.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">TAP2RPM.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>tap2rpm - create RPM packages which wrap .tap files
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>tap2rpm</strong> [options]
+</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>Create a set of RPM/SRPM packages in the current directory
+<dl><dt><strong>-t</strong>, <strong>--tapfile</strong> <em>&lt;tapfile&gt;</em>
+</dt><dd>Build the application around the given .tap (default twistd.tap)
+</dd>
+
+<dt><strong>-y</strong>, <strong>--type</strong> <em>&lt;type&gt;</em>
+</dt><dd>The configuration has the given type . Allowable types are
+<strong>tap</strong>, <strong>source</strong>, <strong>xml</strong> and <strong>python</strong>.
+The first three types are <strong>mktap</strong> output formats,
+while the last one is a manual building of application
+(see <strong>twistd(1)</strong>, the <strong>-y</strong> option).
+</dd>
+
+<dt><strong>-p</strong>, <strong>--protocol</strong> <em>&lt;protocol&gt;</em>
+</dt><dd>The name of the protocol this will be used to serve. This is intended
+as a part of the description. Default is the name of the tapfile, minus
+any extensions.
+</dd>
+
+<dt><strong>-d</strong>, <strong>--rpmfile</strong> <em>&lt;rpmfile&gt;</em>
+</dt><dd>The name of the RPM package. Default is 'twisted-'+protocol.
+</dd>
+
+<dt><strong>-V</strong>, <strong>--set-version</strong> <em>&lt;version&gt;</em>
+</dt><dd>The version of the RPM package. The default is 1.0
+</dd>
+
+<dt><strong>-e</strong>, <strong>--description</strong> <em>&lt;description&gt;</em>
+</dt><dd>The one-line description. Default is uninteresting.
+</dd>
+
+<dt><strong>-l</strong>, <strong>--long_description</strong> <em>&lt;long_description&gt;</em>
+</dt><dd>A multi-line description. Default is explanation about
+this being an automatic package created from tap2rpm.
+</dd>
+
+<dt><strong>-m</strong>, <strong>--maintainer</strong> <em>&lt;maintainer&gt;</em>
+</dt><dd>The maintainer, as <q>Name Lastname &lt;email address&gt;</q>. This will
+go in the meta-files.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Output version information and exit.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>tap2rpm was written by Sean Reifschneider based on tap2deb by Moshe Zadka.
+This man page is heavily based on the tap2deb man page by Moshe Zadka.
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit
+<em>http://twistedmatrix.com/trac/wiki/TwistedDevelopment#FilingTickets</em> for more
+information.
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2000-2009 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/tap2rpm.1 b/doc/core/man/tap2rpm.1
new file mode 100644
index 0000000..6d53c9d
--- /dev/null
+++ b/doc/core/man/tap2rpm.1
@@ -0,0 +1,55 @@
+.TH TAP2RPM "1" "July 2001" "" ""
+.SH NAME
+tap2rpm \- create RPM packages which wrap .tap files
+.SH SYNOPSIS
+.B tap2rpm
+[options]
+.SH DESCRIPTION
+Create a set of RPM/SRPM packages in the current directory
+.TP
+\fB\-t\fR, \fB\--tapfile\fR \fI<tapfile>\fR
+Build the application around the given .tap (default twistd.tap)
+.TP
+\fB\-y\fR, \fB\--type\fR \fI<type>\fR
+The configuration has the given type . Allowable types are
+\fBtap\fR, \fBsource\fR, \fBxml\fR and \fBpython\fR.
+The first three types are \fBmktap\fR output formats,
+while the last one is a manual building of application
+(see \fBtwistd(1)\fR, the \fB\-y\fR option).
+.TP
+\fB\-p\fR, \fB\--protocol\fR \fI<protocol>\fR
+The name of the protocol this will be used to serve. This is intended
+as a part of the description. Default is the name of the tapfile, minus
+any extensions.
+.TP
+\fB\-d\fR, \fB\--rpmfile\fR \fI<rpmfile>\fR
+The name of the RPM package. Default is 'twisted-'+protocol.
+.TP
+\fB\-V\fR, \fB\--set-version\fR \fI<version>\fR
+The version of the RPM package. The default is 1.0
+.TP
+\fB\-e\fR, \fB\--description\fR \fI<description>\fR
+The one-line description. Default is uninteresting.
+.TP
+\fB\-l\fR, \fB\--long_description\fR \fI<long_description>\fR
+A multi-line description. Default is explanation about
+this being an automatic package created from tap2rpm.
+.TP
+\fB\-m\fR, \fB\--maintainer\fR \fI<maintainer>\fR
+The maintainer, as "Name Lastname <email address>". This will
+go in the meta-files.
+.TP
+\fB\--version\fR
+Output version information and exit.
+.SH AUTHOR
+tap2rpm was written by Sean Reifschneider based on tap2deb by Moshe Zadka.
+This man page is heavily based on the tap2deb man page by Moshe Zadka.
+.SH "REPORTING BUGS"
+To report a bug, visit
+\fIhttp://twistedmatrix.com/trac/wiki/TwistedDevelopment#FilingTickets\fR for more
+information.
+.SH COPYRIGHT
+Copyright \(co 2000-2009 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/man/tapconvert-man.html b/doc/core/man/tapconvert-man.html
new file mode 100644
index 0000000..20c979b
--- /dev/null
+++ b/doc/core/man/tapconvert-man.html
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: TAPCONVERT.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">TAPCONVERT.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>tapconvert - convert Twisted configurations from one format to another
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>tapconvert</strong> -i <em>input</em> -o <em>output</em> [-f <em>input-type</em>] [-t <em>output-type</em>] [-d] [-e]</p>
+
+<p><strong>tapconvert</strong> --help</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>The <strong>--help</strong> prints out a usage message to standard output.
+<dl><dt><strong>--in</strong>, <strong>-i</strong> <em>&lt;input file&gt;</em>
+</dt><dd>The name of the input configuration.
+</dd>
+
+<dt><strong>--out</strong>, <strong>-o</strong> <em>&lt;output file&gt;</em>
+</dt><dd>The name of the output configuration.
+</dd>
+
+<dt><strong>--typein</strong>, <strong>-f</strong> <em>&lt;input type&gt;</em>
+</dt><dd>The type of the input file. Can be either 'guess', 'python', 'pickle', 'xml', or 'source'. Default is 'guess'.
+</dd>
+
+<dt><strong>--typeout</strong>, <strong>-t</strong> <em>&lt;output type&gt;</em>
+</dt><dd>The type of the output file. Can be either 'pickle', 'xml', or 'source'. Default is 'source'.
+</dd>
+
+<dt><strong>--decrypt</strong>, <strong>-d</strong>
+</dt><dd>Decrypt the specified tap/aos/xml input file.
+</dd>
+
+<dt><strong>--encrypt</strong>, <strong>-e</strong>
+</dt><dd>Encrypt output file before writing.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Output version information and exit.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>Written by Moshe Zadka, based on tapconvert's help messages
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2000-2012 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/tapconvert.1 b/doc/core/man/tapconvert.1
new file mode 100644
index 0000000..d37bfac
--- /dev/null
+++ b/doc/core/man/tapconvert.1
@@ -0,0 +1,40 @@
+.TH TAPCONVERT "1" "July 2001" "" ""
+.SH NAME
+tapconvert \- convert Twisted configurations from one format to another
+.SH SYNOPSIS
+.B tapconvert -i \fIinput\fR -o \fIoutput\fR [-f \fIinput-type\fR] [-t \fIoutput-type\fR] [-d] [-e]
+.PP
+.B tapconvert --help
+.SH DESCRIPTION
+.PP
+The \fB\--help\fR prints out a usage message to standard output.
+.TP
+\fB\--in\fR, \fB\-i\fR \fI<input file>\fR
+The name of the input configuration.
+.TP
+\fB\--out\fR, \fB\-o\fR \fI<output file>\fR
+The name of the output configuration.
+.TP
+\fB\--typein\fR, \fB\-f\fR \fI<input type>\fR
+The type of the input file. Can be either 'guess', 'python', 'pickle', 'xml', or 'source'. Default is 'guess'.
+.TP
+\fB\--typeout\fR, \fB\-t\fR \fI<output type>\fR
+The type of the output file. Can be either 'pickle', 'xml', or 'source'. Default is 'source'.
+.TP
+\fB\--decrypt\fR, \fB\-d\fR
+Decrypt the specified tap/aos/xml input file.
+.TP
+\fB\--encrypt\fR, \fB\-e\fR
+Encrypt output file before writing.
+.TP
+\fB\--version\fR
+Output version information and exit.
+.SH AUTHOR
+Written by Moshe Zadka, based on tapconvert's help messages
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2000-2012 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/man/trial-man.html b/doc/core/man/trial-man.html
new file mode 100644
index 0000000..7707c6b
--- /dev/null
+++ b/doc/core/man/trial-man.html
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: TRIAL.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">TRIAL.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">OPTIONS</a></li><li><a href="#auto4">SEE ALSO</a></li><li><a href="#auto5">AUTHOR</a></li><li><a href="#auto6">REPORTING BUGS</a></li><li><a href="#auto7">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>trial - run unit tests
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>trial</strong> [ <em>options</em> ] [ <em>file</em> | <em>package</em> | <em>module</em> | <em>TestCase</em> | <em>testmethod</em> ] ...
+</p>
+
+<p><strong>trial --help</strong> | <strong>-h</strong>
+</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>trial loads and executes a suite of unit tests, obtained from modules,
+packages and files listed on the command line.
+</p>
+
+<p>trial will take either filenames or fully qualified Python names as
+arguments. Thus `trial myproject/foo.py', `trial myproject.foo' and
+`trial myproject.foo.SomeTestCase.test_method' are all valid ways to
+invoke trial.
+</p>
+
+<p>After running the given test suite, the default test reporter prints a summary
+of the test run. This consists of the word <q>PASSED</q> (if all tests ran as
+expected) or <q>FAILED</q> (if any test behaved unexpectedly) followed by a count of
+the different kinds of test results encountered. The possible kinds of test
+results includes:
+<dl><dt>successes
+</dt><dd>Tests that passed all their assertions and completed without error.
+These are marked <q>PASSED</q> in the normal test output.
+</dd>
+
+<dt>failures
+</dt><dd>Tests that failed an assertion, called self.fail() or explicitly raised
+self.failureException for some reason. These are marked <q>FAILED</q> in the
+normal test output.
+</dd>
+
+<dt>errors
+</dt><dd>Tests that raised an unexpected exception (including AssertionError),
+tests that caused the tearDown() method to raise an exception, tests
+that run for longer than the timeout interval, tests that caused
+something to call twisted.python.log.err() without subsequently calling
+self.flushLoggedErrors(), tests that leave the reactor in an unclean
+state, etc. These are marked <q>ERROR</q> in the normal test output.
+Note that because errors can be caused after the actual test method
+returns, it is possible for a single test to be reported as both an
+error and a failure, and hence the total number of test results can be
+greater than the total number of tests executed.
+</dd>
+
+<dt>skips
+</dt><dd>Tests that were skipped, usually because of missing dependencies. These
+are marked <q>SKIPPED</q> in the normal test output.
+</dd>
+
+<dt>expectedFailures
+</dt><dd>Tests that failed, but were expected to fail, usually because the test
+is for a feature that hasn't been implemented yet. These are marked
+<q>TODO</q> in the normal test output.
+</dd>
+
+<dt>unexpectedSuccesses
+</dt><dd>Tests that should have been listed under expectedFailures, except that
+for some reason the test succeeded. These are marked <q>SUCCESS!?!</q> in
+the normal test output.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>OPTIONS<a name="auto3"/></h2>
+
+<dl><dt><strong>-b</strong>, <strong>--debug</strong>
+</dt><dd>Run the tests in the Python debugger. Also does post-mortem
+debugging on exceptions. Will load `.pdbrc' from current directory if
+it exists.
+</dd>
+
+<dt><strong>-B</strong>, <strong>--debug-stacktraces</strong>
+</dt><dd>Report Deferred creation and callback stack traces
+</dd>
+
+<dt><strong>--coverage</strong>
+</dt><dd>Generate coverage information in the `coverage' subdirectory of the trial temp
+directory (`_trial_temp' by default). For each Python module touched by the
+execution of the given tests, a file will be created in the coverage directory
+named for the module's fully-qualified name with the suffix `.cover'. For
+example, because the trial test runner is written in Python, the coverage
+directory will almost always contain a file named `twisted.trial.runner.cover'.
+
+Each `.cover' file contains a copy of the Python source of the module in
+question, with a prefix at the beginning of each line containing coverage
+information. For lines that are not executable (blank lines, comments, etc.)
+the prefix is blank. For executable lines that were run in the course of the
+test suite, the prefix is a number indicating the number of times that line was
+executed. The string `&gt;&gt;&gt;&gt;&gt;&gt;' prefixes executable lines that were not executed
+in the course of the test suite.
+
+Note that this functionality uses Python's sys.settrace() function, so tests
+that call sys.settrace() themselves are likely to break trial's coverage
+functionality.
+</dd>
+
+<dt><strong>--disablegc</strong>
+</dt><dd>Disable the garbage collector for the duration of the test run. As each test is
+run, trial saves the TestResult objects, which means that Python's garbage
+collector has more non-garbage objects to wade through, making each
+garbage-collection run slightly slower. Disabling garbage collection entirely
+will make some test suites complete faster (contrast --force-gc, below), at the
+cost of increasing (possibly greatly) memory consumption. This option also makes
+tests slightly more deterministic, which might help debugging in extreme
+circumstances.
+</dd>
+
+<dt><strong>-e</strong>, <strong>--rterrors</strong>
+</dt><dd>Print tracebacks to standard output as soon as they occur
+</dd>
+
+<dt><strong>--force-gc</strong>
+</dt><dd>Run gc.collect() before and after each test case. This can be used to
+isolate errors that occur when objects get collected. This option would be
+the default, except it makes tests run about ten times slower.
+</dd>
+
+<dt><strong>-h</strong>, <strong>--help</strong>
+</dt><dd>Print a usage message to standard output, then exit.
+</dd>
+
+<dt><strong>--help-reporters</strong>
+</dt><dd>Print a list of valid reporters to standard output, then exit. Reporters can
+be selected with the --reporter option described below.
+</dd>
+
+<dt><strong>--help-reactors</strong>
+</dt><dd>Print a list of possible reactors to standard output, then exit. Not all listed
+reactors are available on every platform. Reactors can be selected with the
+--reactor option described below.
+</dd>
+
+<dt><strong>-l</strong>, <strong>--logfile</strong> <em>logfile</em>
+</dt><dd>Direct the log to a different file. The default file is `test.log'.
+<em>logfile</em> is relative to _trial_temp.
+</dd>
+
+<dt><strong>-n</strong>, <strong>--dry-run</strong>
+</dt><dd>Go through all the tests and make them pass without running.
+</dd>
+
+<dt><strong>-N</strong>, <strong>--no-recurse</strong>
+</dt><dd>By default, trial recurses through packages to find every module inside
+every subpackage. Unless, that is, you specify this option.
+</dd>
+
+<dt><strong>--nopm</strong>
+</dt><dd>Don't automatically jump into debugger for post-mortem analysis of
+exceptions. Only usable in conjunction with --debug.
+</dd>
+
+<dt><strong>--profile</strong>
+</dt><dd>Run tests under the Python profiler.
+</dd>
+
+<dt><strong>-r</strong>, <strong>--reactor</strong> <em>reactor</em>
+</dt><dd>Choose which reactor to use. See --help-reactors for a list.
+</dd>
+
+<dt><strong>--recursionlimit</strong>
+</dt><dd>Set Python's recursion limit. See sys.setrecursionlimit()
+</dd>
+
+<dt><strong>--reporter</strong>
+</dt><dd>Select the reporter to use for trial's output. Use the --help-reporters
+option to see a list of valid reporters.
+</dd>
+
+<dt><strong>--spew</strong>
+</dt><dd>Print an insanely verbose log of everything that happens. Useful when
+debugging freezes or locks in complex code.
+</dd>
+
+<dt><strong>--tbformat</strong> <em>format</em>
+</dt><dd>Format to display tracebacks with. Acceptable values are `default', `brief'
+and `verbose'. `brief' produces tracebacks that play nicely with Emacs' GUD.
+</dd>
+
+<dt><strong>--temp-directory</strong> <em>directory</em>
+</dt><dd>WARNING: Do not use this options unless you know what you are doing.
+By default, trial creates a directory called _trial_temp under the current
+working directory. When trial runs, it first <em>deletes</em> this directory,
+then creates it, then changes into the directory to run the tests. The log
+file and any coverage files are stored here. Use this option if you wish to
+have trial run in a directory other than _trial_temp. Be warned, trial
+will <em>delete</em> the directory before re-creating it.
+</dd>
+
+<dt><strong>--testmodule</strong> <em>filename</em>
+</dt><dd>Ask trial to look into <em>filename</em> and run any tests specified using the
+Emacs-style buffer variable `test-case-name'.
+</dd>
+
+<dt><strong>--unclean-warnings</strong>
+</dt><dd>As of Twisted 8.0, trial will report an error if the reactor is left unclean
+at the end of the test. This option is provided to assist in migrating from
+Twisted 2.5 to Twisted 8.0 and later. Enabling this option will turn the errors
+into warnings.
+</dd>
+
+<dt><strong>-u</strong>, <strong>--until-failure</strong>
+</dt><dd>Keep looping the tests until one of them raises an error or a failure.
+This is particularly useful for reproducing intermittent failures.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Prints the Twisted version number and exit.
+</dd>
+
+<dt><strong>--without-module</strong> <em>modulenames</em>
+</dt><dd>Simulate the lack of the specified comma-separated list of modules. This makes
+it look like the modules are not present in the system, causing tests to check
+the behavior for that configuration.
+</dd>
+
+<dt><strong>-z</strong>, <strong>--random</strong> [<em>seed</em>]
+</dt><dd>Run the tests in random order using the specified seed.
+</dd>
+
+</dl>
+
+<h2>SEE ALSO<a name="auto4"/></h2>
+
+<p>The latest version of the trial documentation can be found at
+http://twistedmatrix.com/documents/current/core/howto/testing.html
+</p>
+
+<h2>AUTHOR<a name="auto5"/></h2>
+
+<p>Written by Jonathan M. Lange
+</p>
+
+<h2>REPORTING BUGS<a name="auto6"/></h2>
+
+<p>To report a bug, visit http://twistedmatrix.com/trac/newticket
+</p>
+
+<h2>COPYRIGHT<a name="auto7"/></h2>
+
+<p>Copyright © 2003-2011 Twisted Matrix Laboratories
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/trial.1 b/doc/core/man/trial.1
new file mode 100644
index 0000000..f01934d
--- /dev/null
+++ b/doc/core/man/trial.1
@@ -0,0 +1,200 @@
+.TH TRIAL "1" "Oct 2007" "" ""
+.SH NAME
+trial \- run unit tests
+.SH SYNOPSIS
+\fBtrial\fR [ \fIoptions\fR ] [ \fIfile\fR | \fIpackage\fR | \fImodule\fR | \fITestCase\fR | \fItestmethod\fR ] ...
+.PP
+\fBtrial --help\fR | \fB-h\fR
+.SH DESCRIPTION
+.PP
+trial loads and executes a suite of unit tests, obtained from modules,
+packages and files listed on the command line.
+.PP
+trial will take either filenames or fully qualified Python names as
+arguments. Thus `trial myproject/foo.py', `trial myproject.foo' and
+`trial myproject.foo.SomeTestCase.test_method' are all valid ways to
+invoke trial.
+.PP
+After running the given test suite, the default test reporter prints a summary
+of the test run. This consists of the word "PASSED" (if all tests ran as
+expected) or "FAILED" (if any test behaved unexpectedly) followed by a count of
+the different kinds of test results encountered. The possible kinds of test
+results includes:
+.TP
+successes
+Tests that passed all their assertions and completed without error.
+These are marked "PASSED" in the normal test output.
+.TP
+failures
+Tests that failed an assertion, called self.fail() or explicitly raised
+self.failureException for some reason. These are marked "FAILED" in the
+normal test output.
+.TP
+errors
+Tests that raised an unexpected exception (including AssertionError),
+tests that caused the tearDown() method to raise an exception, tests
+that run for longer than the timeout interval, tests that caused
+something to call twisted.python.log.err() without subsequently calling
+self.flushLoggedErrors(), tests that leave the reactor in an unclean
+state, etc. These are marked "ERROR" in the normal test output.
+.IP
+Note that because errors can be caused after the actual test method
+returns, it is possible for a single test to be reported as both an
+error and a failure, and hence the total number of test results can be
+greater than the total number of tests executed.
+.TP
+skips
+Tests that were skipped, usually because of missing dependencies. These
+are marked "SKIPPED" in the normal test output.
+.TP
+expectedFailures
+Tests that failed, but were expected to fail, usually because the test
+is for a feature that hasn't been implemented yet. These are marked
+"TODO" in the normal test output.
+.TP
+unexpectedSuccesses
+Tests that should have been listed under expectedFailures, except that
+for some reason the test succeeded. These are marked "SUCCESS!?!" in
+the normal test output.
+.SH OPTIONS
+.TP
+\fB-b\fR, \fB--debug\fR
+Run the tests in the Python debugger. Also does post-mortem
+debugging on exceptions. Will load `.pdbrc' from current directory if
+it exists.
+.TP
+\fB-B\fR, \fB--debug-stacktraces\fR
+Report Deferred creation and callback stack traces
+.TP
+\fB--coverage\fR
+Generate coverage information in the `coverage' subdirectory of the trial temp
+directory (`_trial_temp' by default). For each Python module touched by the
+execution of the given tests, a file will be created in the coverage directory
+named for the module's fully-qualified name with the suffix `.cover'. For
+example, because the trial test runner is written in Python, the coverage
+directory will almost always contain a file named `twisted.trial.runner.cover'.
+
+Each `.cover' file contains a copy of the Python source of the module in
+question, with a prefix at the beginning of each line containing coverage
+information. For lines that are not executable (blank lines, comments, etc.)
+the prefix is blank. For executable lines that were run in the course of the
+test suite, the prefix is a number indicating the number of times that line was
+executed. The string `>>>>>>' prefixes executable lines that were not executed
+in the course of the test suite.
+
+Note that this functionality uses Python's sys.settrace() function, so tests
+that call sys.settrace() themselves are likely to break trial's coverage
+functionality.
+.TP
+\fB--disablegc\fR
+Disable the garbage collector for the duration of the test run. As each test is
+run, trial saves the TestResult objects, which means that Python's garbage
+collector has more non-garbage objects to wade through, making each
+garbage-collection run slightly slower. Disabling garbage collection entirely
+will make some test suites complete faster (contrast --force-gc, below), at the
+cost of increasing (possibly greatly) memory consumption. This option also makes
+tests slightly more deterministic, which might help debugging in extreme
+circumstances.
+.TP
+\fB-e\fR, \fB--rterrors\fR
+Print tracebacks to standard output as soon as they occur
+.TP
+\fB--force-gc\fR
+Run gc.collect() before and after each test case. This can be used to
+isolate errors that occur when objects get collected. This option would be
+the default, except it makes tests run about ten times slower.
+.TP
+\fB-h\fR, \fB--help\fR
+Print a usage message to standard output, then exit.
+.TP
+\fB--help-reporters\fR
+Print a list of valid reporters to standard output, then exit. Reporters can
+be selected with the --reporter option described below.
+.TP
+\fB--help-reactors\fR
+Print a list of possible reactors to standard output, then exit. Not all listed
+reactors are available on every platform. Reactors can be selected with the
+--reactor option described below.
+.TP
+\fB-l\fR, \fB--logfile\fR \fIlogfile\fR
+Direct the log to a different file. The default file is `test.log'.
+\fIlogfile\fR is relative to _trial_temp.
+.TP
+\fB-n\fR, \fB--dry-run\fR
+Go through all the tests and make them pass without running.
+.TP
+\fB-N\fR, \fB--no-recurse\fR
+By default, trial recurses through packages to find every module inside
+every subpackage. Unless, that is, you specify this option.
+.TP
+\fB--nopm\fR
+Don't automatically jump into debugger for post-mortem analysis of
+exceptions. Only usable in conjunction with --debug.
+.TP
+\fB--profile\fR
+Run tests under the Python profiler.
+.TP
+\fB-r\fR, \fB--reactor\fR \fIreactor\fR
+Choose which reactor to use. See --help-reactors for a list.
+.TP
+\fB--recursionlimit\fR
+Set Python's recursion limit. See sys.setrecursionlimit()
+.TP
+\fB--reporter\fR
+Select the reporter to use for trial's output. Use the --help-reporters
+option to see a list of valid reporters.
+.TP
+\fB--spew\fR
+Print an insanely verbose log of everything that happens. Useful when
+debugging freezes or locks in complex code.
+.TP
+\fB--tbformat\fR \fIformat\fR
+Format to display tracebacks with. Acceptable values are `default', `brief'
+and `verbose'. `brief' produces tracebacks that play nicely with Emacs' GUD.
+.TP
+\fB--temp-directory\fR \fIdirectory\fR
+WARNING: Do not use this options unless you know what you are doing.
+By default, trial creates a directory called _trial_temp under the current
+working directory. When trial runs, it first \fIdeletes\fR this directory,
+then creates it, then changes into the directory to run the tests. The log
+file and any coverage files are stored here. Use this option if you wish to
+have trial run in a directory other than _trial_temp. Be warned, trial
+will \fIdelete\fR the directory before re-creating it.
+.TP
+\fB--testmodule\fR \fIfilename\fR
+Ask trial to look into \fIfilename\fR and run any tests specified using the
+Emacs-style buffer variable `test-case-name'.
+.TP
+\fB--unclean-warnings\fR
+As of Twisted 8.0, trial will report an error if the reactor is left unclean
+at the end of the test. This option is provided to assist in migrating from
+Twisted 2.5 to Twisted 8.0 and later. Enabling this option will turn the errors
+into warnings.
+.TP
+\fB-u\fR, \fB--until-failure\fR
+Keep looping the tests until one of them raises an error or a failure.
+This is particularly useful for reproducing intermittent failures.
+.TP
+\fB--version\fR
+Prints the Twisted version number and exit.
+.TP
+\fB--without-module\fR \fImodulenames\fR
+Simulate the lack of the specified comma-separated list of modules. This makes
+it look like the modules are not present in the system, causing tests to check
+the behavior for that configuration.
+.TP
+\fB-z\fR, \fB--random\fR [\fIseed\fR]
+Run the tests in random order using the specified seed.
+.PP
+.SH SEE ALSO
+The latest version of the trial documentation can be found at
+http://twistedmatrix.com/documents/current/core/howto/testing.html
+.SH AUTHOR
+Written by Jonathan M. Lange
+.SH "REPORTING BUGS"
+To report a bug, visit http://twistedmatrix.com/trac/newticket
+.SH COPYRIGHT
+Copyright \(co 2003-2011 Twisted Matrix Laboratories
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/man/twistd-man.html b/doc/core/man/twistd-man.html
new file mode 100644
index 0000000..7761034
--- /dev/null
+++ b/doc/core/man/twistd-man.html
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: TWISTD.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">TWISTD.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">OPTIONS</a></li><li><a href="#auto4">SIGNALS</a></li><li><a href="#auto5">AUTHOR</a></li><li><a href="#auto6">REPORTING BUGS</a></li><li><a href="#auto7">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>twistd - run Twisted applications (TACs, TAPs)
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>twistd</strong> [options]
+</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>Read a twisted.application.service.Application out of a file and run it.
+</p>
+
+<h2>OPTIONS<a name="auto3"/></h2>
+
+<p><strong>-n</strong>, <strong>--nodaemon</strong>
+Don't daemonize (stay in foreground).
+<dl><dt><strong>-q</strong>, <strong>--quiet</strong>
+</dt><dd>No-op for backwards compatibility.
+</dd>
+
+<dt><strong>-p</strong>, <strong>--profile</strong> <em>&lt;profile output&gt;</em>
+</dt><dd>Run the application under the profiler, dumping results to the specified file.
+</dd>
+
+<dt><strong>--profiler</strong> <em>&lt;profiler name&gt;</em>
+</dt><dd>Specify the profiler to use. Defaults to the 'hotshot' profiler.
+</dd>
+
+<dt><strong>--savestats</strong>
+</dt><dd>Save the Stats object rather than the text output of the profiler.
+</dd>
+
+<dt><strong>-b</strong>, <strong>--debug</strong>
+</dt><dd>Run the application in the Python Debugger (implies <strong>--nodaemon</strong> option).
+Sending a SIGINT or SIGUSR2 signal to the process will drop it into the
+debugger.
+</dd>
+
+<dt><strong>-e</strong>, <strong>--encrypted</strong> <em>&lt;file&gt;</em>
+</dt><dd>The specified tap/aos file is encrypted.
+</dd>
+
+<dt><strong>--euid</strong>
+</dt><dd>Set only effective user-id rather than real user-id. This option has no
+effect unless the server is running as root, in which case it means not
+to shed all privileges after binding ports, retaining the option to regain
+privileges in cases such as spawning processes. Use with caution.
+</dd>
+
+<dt><strong>-o</strong>, <strong>--no_save</strong>
+</dt><dd>Do not save shutdown state.
+</dd>
+
+<dt><strong>--originalname</strong>
+</dt><dd>Behave as though the specified Application has no process name set, and run
+with the standard process name (the Python binary in most cases).
+</dd>
+
+<dt><strong>-l</strong>, <strong>--logfile</strong> <em>&lt;logfile&gt;</em>
+</dt><dd>Log to a specified file, - for stdout (default: twistd.log).
+The log file will be rotated on SIGUSR1.
+</dd>
+
+<dt><strong>-l</strong>, <strong>--logger</strong> <em>&lt;fully qualified python name&gt;</em>
+</dt><dd>A fully-qualified name to a log observer factory to use for the initial log
+observer. Takes precedence over --logfile and --syslog.
+</dd>
+
+<dt><strong>--pidfile</strong> <em>&lt;pidfile&gt;</em>
+</dt><dd>Save pid in specified file (default: twistd.pid).
+</dd>
+
+<dt><strong>--chroot</strong> <em>&lt;directory&gt;</em>
+</dt><dd>Chroot to a supplied directory before running (default: don't chroot).
+Chrooting is done before changing the current directory.
+</dd>
+
+<dt><strong>-d</strong>, <strong>--rundir</strong> <em>&lt;directory&gt;</em>
+</dt><dd>Change to a supplied directory before running (default: .).
+</dd>
+
+<dt><strong>-u</strong>, <strong>--uid</strong> <em>&lt;uid&gt;</em>
+</dt><dd>The uid to run as (default: don't change).
+</dd>
+
+<dt><strong>-g</strong>, <strong>--gid</strong> <em>&lt;gid&gt;</em>
+</dt><dd>The gid to run as (default: don't change).
+</dd>
+
+<dt><strong>--umask</strong> <em>&lt;mask&gt;</em>
+</dt><dd>The (octal) file creation mask to apply. (default: 0077 for daemons, no
+change otherwise).
+</dd>
+
+<dt><strong>-r</strong>, <strong>--reactor</strong> <em>&lt;reactor&gt;</em>
+</dt><dd>Choose which reactor to use. See <strong>--help-reactors</strong> for a list of
+possibilities.
+</dd>
+
+<dt><strong>--help-reactors</strong>
+</dt><dd>List the names of possibly available reactors.
+</dd>
+
+<dt><strong>--spew</strong>
+</dt><dd>Write an extremely verbose log of everything that happens. Useful for
+debugging freezes or locks in complex code.
+</dd>
+
+<dt><strong>-f</strong>, <strong>--file</strong> <em>&lt;tap file&gt;</em>
+</dt><dd>Read the given .tap file (default: twistd.tap).
+</dd>
+
+<dt><strong>-s</strong>, <strong>--source</strong> <em>&lt;tas file&gt;</em>
+</dt><dd>Load an Application from the given .tas (AOT Python source) file.
+</dd>
+
+<dt><strong>-y</strong>, <strong>--python</strong> <em>&lt;python file&gt;</em>
+</dt><dd>Use the variable <q>application</q> from the given Python file. This option overrides
+<strong>-f</strong>. This option implies <strong>--no_save</strong>.
+</dd>
+
+<dt><strong>--syslog</strong>
+</dt><dd>Log to syslog instead of a file.
+</dd>
+
+<dt><strong>--version</strong>
+</dt><dd>Print version information and exit.
+</dd>
+
+<dt><strong>--prefix</strong> <em>&lt;prefix&gt;</em>
+</dt><dd>Use the specified prefix when logging to logfile. Default is <q>twisted</q>.
+</dd>
+
+</dl>
+
+</p>
+
+<p>Note that if <strong>twistd</strong> is run as root, the working directory is <em>not</em>
+searched for Python modules.
+</p>
+
+<h2>SIGNALS<a name="auto4"/></h2>
+
+<p>A running twistd accepts SIGINT for a clean shutdown and SIGUSR1 to rotate log
+files.
+</p>
+
+<h2>AUTHOR<a name="auto5"/></h2>
+
+<p>Written by Moshe Zadka, based on twistd's help messages.
+</p>
+
+<h2>REPORTING BUGS<a name="auto6"/></h2>
+
+<p>To report a bug, visit
+<em>http://twistedmatrix.com/trac/wiki/TwistedDevelopment#DevelopmentProcess</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto7"/></h2>
+
+<p>Copyright © 2001-2011 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/man/twistd.1 b/doc/core/man/twistd.1
new file mode 100644
index 0000000..c5c4c3c
--- /dev/null
+++ b/doc/core/man/twistd.1
@@ -0,0 +1,118 @@
+.TH TWISTD "1" "Dec 2011" "" ""
+.SH NAME
+twistd \- run Twisted applications (TACs, TAPs)
+.SH SYNOPSIS
+.B twistd
+[options]
+.SH DESCRIPTION
+Read a twisted.application.service.Application out of a file and run it.
+.SH OPTIONS
+\fB\-n\fR, \fB\--nodaemon\fR
+Don't daemonize (stay in foreground).
+.TP
+\fB\-q\fR, \fB\--quiet\fR
+No-op for backwards compatibility.
+.TP
+\fB\-p\fR, \fB\--profile\fR \fI<profile output>\fR
+Run the application under the profiler, dumping results to the specified file.
+.TP
+\fB\--profiler\fR \fI<profiler name>\fR
+Specify the profiler to use. Defaults to the 'hotshot' profiler.
+.TP
+\fB--savestats\fR
+Save the Stats object rather than the text output of the profiler.
+.TP
+\fB\-b\fR, \fB\--debug\fR
+Run the application in the Python Debugger (implies \fB\--nodaemon\fR option).
+Sending a SIGINT or SIGUSR2 signal to the process will drop it into the
+debugger.
+.TP
+\fB\-e\fR, \fB\--encrypted\fR \fI<file>\fR
+The specified tap/aos file is encrypted.
+.TP
+\fB--euid\fR
+Set only effective user-id rather than real user-id. This option has no
+effect unless the server is running as root, in which case it means not
+to shed all privileges after binding ports, retaining the option to regain
+privileges in cases such as spawning processes. Use with caution.
+.TP
+\fB\-o\fR, \fB\--no_save\fR
+Do not save shutdown state.
+.TP
+\fB\--originalname\fR
+Behave as though the specified Application has no process name set, and run
+with the standard process name (the Python binary in most cases).
+.TP
+\fB\-l\fR, \fB\--logfile\fR \fI<logfile>\fR
+Log to a specified file, - for stdout (default: twistd.log).
+The log file will be rotated on SIGUSR1.
+.TP
+\fB\-l\fR, \fB\--logger\fR \fI<fully qualified python name>\fR
+A fully-qualified name to a log observer factory to use for the initial log
+observer. Takes precedence over --logfile and --syslog.
+.TP
+\fB\--pidfile\fR \fI<pidfile>\fR
+Save pid in specified file (default: twistd.pid).
+.TP
+\fB\--chroot\fR \fI<directory>\fR
+Chroot to a supplied directory before running (default: don't chroot).
+Chrooting is done before changing the current directory.
+.TP
+\fB\-d\fR, \fB\--rundir\fR \fI<directory>\fR
+Change to a supplied directory before running (default: .).
+.TP
+\fB\-u\fR, \fB\--uid\fR \fI<uid>\fR
+The uid to run as (default: don't change).
+.TP
+\fB\-g\fR, \fB\--gid\fR \fI<gid>\fR
+The gid to run as (default: don't change).
+.TP
+\fB--umask\fR \fI<mask>\fR
+The (octal) file creation mask to apply. (default: 0077 for daemons, no
+change otherwise).
+.TP
+\fB\-r\fR, \fB\--reactor\fR \fI<reactor>\fR
+Choose which reactor to use. See \fB\--help-reactors\fR for a list of
+possibilities.
+.TP
+\fB--help-reactors\fR
+List the names of possibly available reactors.
+.TP
+\fB\--spew\fR
+Write an extremely verbose log of everything that happens. Useful for
+debugging freezes or locks in complex code.
+.TP
+\fB\-f\fR, \fB\--file\fR \fI<tap file>\fR
+Read the given .tap file (default: twistd.tap).
+.TP
+\fB\-s\fR, \fB\--source\fR \fI<tas file>\fR
+Load an Application from the given .tas (AOT Python source) file.
+.TP
+\fB\-y\fR, \fB\--python\fR \fI<python file>\fR
+Use the variable "application" from the given Python file. This option overrides
+\fB\-f\fR. This option implies \fB\--no_save\fR.
+.TP
+\fB\--syslog\fR
+Log to syslog instead of a file.
+.TP
+\fB\--version\fR
+Print version information and exit.
+.TP
+\fB\--prefix\fR \fI<prefix>\fR
+Use the specified prefix when logging to logfile. Default is "twisted".
+.PP
+Note that if \fBtwistd\fR is run as root, the working directory is \fInot\fR
+searched for Python modules.
+.SH SIGNALS
+A running twistd accepts SIGINT for a clean shutdown and SIGUSR1 to rotate log
+files.
+.SH AUTHOR
+Written by Moshe Zadka, based on twistd's help messages.
+.SH "REPORTING BUGS"
+To report a bug, visit
+\fIhttp://twistedmatrix.com/trac/wiki/TwistedDevelopment#DevelopmentProcess\fR
+.SH COPYRIGHT
+Copyright \(co 2001-2011 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/core/specifications/banana.html b/doc/core/specifications/banana.html
new file mode 100644
index 0000000..53405de
--- /dev/null
+++ b/doc/core/specifications/banana.html
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Banana Protocol Specifications</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Banana Protocol Specifications</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Banana Encodings</a></li><li><a href="#auto2">Element Types</a></li><ul><li><a href="#auto3">Examples</a></li></ul><li><a href="#auto4">Profiles</a></li><ul><li><a href="#auto5">The &quot;none&quot; Profile</a></li><li><a href="#auto6">The &quot;pb&quot; Profile</a></li></ul><li><a href="#auto7">Protocol Handshake and Behaviour</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Introduction<a name="auto0"/></h2>
+
+ <p>
+ Banana is an efficient, extendable protocol for sending and receiving s-expressions.
+ A s-expression in this context is a list composed of byte strings, integers,
+ large integers, floats and/or s-expressions.
+ </p>
+
+ <h2>Banana Encodings<a name="auto1"/></h2>
+
+ <p>
+ The banana protocol is a stream of data composed of elements. Each element has the
+ following general structure - first, the length of element encoded in base-128, least signficant
+ bit first. For example length 4674 will be sent as <code>0x42 0x24</code>. For certain element
+ types the length will be omitted (e.g. float) or have a different meaning (it is the actual
+ value of integer elements).
+ </p>
+
+ <p>
+ Following the length is a delimiter byte, which tells us what kind of element this
+ is. Depending on the element type, there will then follow the number of bytes specified
+ in the length. The byte's high-bit will always be set, so that we can differentiate
+ between it and the length (since the length bytes use 128-base, their high bit will
+ never be set).
+ </p>
+
+ <h2>Element Types<a name="auto2"/></h2>
+
+ <p>
+ Given a series of bytes that gave us length N, these are the different delimiter bytes:
+ </p>
+
+ <dl>
+ <dt>List -- 0x80</dt>
+
+ <dd>The following bytes are a list of N elements. Lists may be nested,
+ and a child list counts as only one element to its parent (regardless
+ of how many elements the child list contains). </dd>
+
+ <dt>Integer -- 0x81</dt>
+ <dd>The value of this element is the positive integer N. Following bytes are not part of this element. Integers can have values of 0 &lt;= N &lt;= 2147483647.</dd>
+
+ <dt>String -- 0x82</dt>
+ <dd>The following N bytes are a string element.</dd>
+
+ <dt>Negative Integer -- 0x83</dt>
+ <dd>The value of this element is the integer N * -1, i.e. -N. Following bytes are not part of this element. Negative integers can have values of 0 &gt;= -N &gt;= -2147483648.</dd>
+
+ <dt>Float - 0x84</dt>
+ <dd>The next 8 bytes are the float encoded in IEEE 754 floating-point <q>double format</q> bit layout.
+ No length bytes should have been defined.
+ </dd>
+
+ <dt>Large Integer -- 0x85</dt>
+ <dd>The value of this element is the positive large integer N. Following bytes are not part of this element. Large integers have no size limitation.</dd>
+
+ <dt>Large Negative Integer -- 0x86</dt>
+ <dd>The value of this element is the negative large integer -N. Following bytes are not part of this element. Large integers have no size limitation.</dd>
+ </dl>
+
+ <p>
+ Large integers are intended for arbitary length integers. Regular integers types (positive and negative) are limited to 32-bit values.
+ </p>
+
+ <h3>Examples<a name="auto3"/></h3>
+
+ <p>
+ Here are some examples of elements and their encodings - the type bytes are marked in bold:
+ </p>
+
+ <dl>
+ <dt><code>1</code></dt>
+ <dd><code>0x01 <strong>0x81</strong></code></dd>
+ <dt><code>-1</code></dt>
+ <dd><code>0x01 <strong>0x83</strong></code></dd>
+ <dt><code>1.5</code></dt>
+ <dd><code><strong>0x84</strong> 0x3f 0xf8 0x00 0x00 0x00 0x00 0x00 0x00</code></dd>
+ <dt><code>&quot;hello&quot;</code></dt>
+ <dd><code>0x05 <strong>0x82</strong> 0x68 0x65 0x6c 0x6c 0x6f</code></dd>
+ <dt><code>[]</code></dt>
+ <dd><code>0x00 <strong>0x80</strong></code></dd>
+ <dt><code>[1, 23]</code></dt>
+ <dd><code>0x02 <strong>0x80</strong> 0x01 <strong>0x81</strong> 0x17 <strong>0x81</strong></code></dd>
+ <dt><code>123456789123456789</code></dt>
+ <dd><code>0x15 0x3e 0x41 0x66 0x3a 0x69 0x26 0x5b 0x01 <strong>0x85</strong></code></dd>
+ <dt><code>[1, [&quot;hello&quot;]]</code></dt>
+ <dd><code>0x02 <strong>0x80</strong> 0x01 <strong>0x81</strong> 0x01 <strong>0x80</strong> 0x05 <strong>0x82</strong> 0x68 0x65 0x6c 0x6c 0x6f</code></dd>
+ </dl>
+
+ <h2>Profiles<a name="auto4"/></h2>
+
+ <p>
+ The Banana protocol is extendable. Therefore, it supports the concept of profiles. Profiles allow
+ developers to extend the banana protocol, adding new element types, while still keeping backwards
+ compatability with implementations that don't support the extensions. The profile used in each
+ session is determined at the handshake stage (see below.)
+ </p>
+
+ <p>
+ A profile is specified by a unique string. This specification defines two profiles
+ - <code>&quot;none&quot;</code> and <code>&quot;pb&quot;</code>. The <code>&quot;none&quot;</code> profile is the standard
+ profile that should be supported by all Banana implementations.
+ Additional profiles may be added in the future.
+ </p>
+
+ <h3>The <code>&quot;none&quot;</code> Profile<a name="auto5"/></h3>
+
+ <p>
+ The <code>&quot;none&quot;</code> profile is identical to the delimiter types listed above. It is highly recommended
+ that all Banana clients and servers support the <code>&quot;none&quot;</code> profile.
+ </p>
+
+ <h3>The <code>&quot;pb&quot;</code> Profile<a name="auto6"/></h3>
+
+ <p>
+ The <code>&quot;pb&quot;</code> profile is intended for use with the Perspective Broker protocol, that runs on top
+ of Banana. Basically, it converts commonly used PB strings into shorter versions, thus
+ minimizing bandwidth usage. It starts with a single byte, which tells us to which string element
+ to convert it, and ends with the delimiter byte, <code>0x87</code>, which should not be prefixed
+ by a length.
+ </p>
+
+ <dl>
+ <dt>0x01</dt> <dd>'None'</dd>
+ <dt>0x02</dt> <dd>'class'</dd>
+ <dt>0x03</dt> <dd>'dereference'</dd>
+ <dt>0x04</dt> <dd>'reference'</dd>
+ <dt>0x05</dt> <dd>'dictionary'</dd>
+ <dt>0x06</dt> <dd>'function'</dd>
+ <dt>0x07</dt> <dd>'instance'</dd>
+ <dt>0x08</dt> <dd>'list'</dd>
+ <dt>0x09</dt> <dd>'module'</dd>
+ <dt>0x0a</dt> <dd>'persistent'</dd>
+ <dt>0x0b</dt> <dd>'tuple'</dd>
+ <dt>0x0c</dt> <dd>'unpersistable'</dd>
+ <dt>0x0d</dt> <dd>'copy'</dd>
+ <dt>0x0e</dt> <dd>'cache'</dd>
+ <dt>0x0f</dt> <dd>'cached'</dd>
+ <dt>0x10</dt> <dd>'remote'</dd>
+ <dt>0x11</dt> <dd>'local'</dd>
+ <dt>0x12</dt> <dd>'lcache'</dd>
+ <dt>0x13</dt> <dd>'version'</dd>
+ <dt>0x14</dt> <dd>'login'</dd>
+ <dt>0x15</dt> <dd>'password'</dd>
+ <dt>0x16</dt> <dd>'challenge'</dd>
+ <dt>0x17</dt> <dd>'logged_in'</dd>
+ <dt>0x18</dt> <dd>'not_logged_in'</dd>
+ <dt>0x19</dt> <dd>'cachemessage'</dd>
+ <dt>0x1a</dt> <dd>'message'</dd>
+ <dt>0x1b</dt> <dd>'answer'</dd>
+ <dt>0x1c</dt> <dd>'error'</dd>
+ <dt>0x1d</dt> <dd>'decref'</dd>
+ <dt>0x1e</dt> <dd>'decache'</dd>
+ <dt>0x1f</dt> <dd>'uncache'</dd>
+ </dl>
+
+ <h2>Protocol Handshake and Behaviour<a name="auto7"/></h2>
+
+ <p>
+ The initiating side of the connection will be referred to as <q>client</q>, and the other
+ side as <q>server</q>.
+ </p>
+
+ <p>
+ Upon connection, the server will send the client a list of string elements, signifying
+ the profiles it supports. It is recommended that <code>&quot;none&quot;</code> be included in this list. The client
+ then sends the server a string from this list, telling the server which profile it wants to
+ use. At this point the whole session will use this profile.
+ </p>
+
+ <p>
+ Once a profile has been established, the two sides may start exchanging elements. There is no
+ limitation on order or dependencies of messages. Any such limitation (e.g. <q>server can only
+ send an element to client in response to a request from client</q>) is application specific.
+ </p>
+
+ <p>
+ Upon receiving illegal messages, failed handshakes, etc., a Banana client or server should
+ close its connection.
+ </p>
+
+ </div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/core/specifications/index.html b/doc/core/specifications/index.html
new file mode 100644
index 0000000..ac19236
--- /dev/null
+++ b/doc/core/specifications/index.html
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Specifications</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Specifications</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="banana.html" shape="rect">Banana</a></li>
+</ul>
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/fun/Twisted.Quotes b/doc/fun/Twisted.Quotes
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/fun/Twisted.Quotes
diff --git a/doc/fun/lightbulb b/doc/fun/lightbulb
new file mode 100644
index 0000000..12de989
--- /dev/null
+++ b/doc/fun/lightbulb
@@ -0,0 +1,7 @@
+Q. How many Twisted developers does it take to screw in a lightbulb?
+
+A. Three to implement twisted.lightbulb, one to refactor it, four to whine until the API is documented, two to re-implement it as a C module, one to package it up nicely, but nobody uses the packaged lightbulb, because they need the newest light features, and then no one actually gets around to screwing it in.
+%
+Q. How many Divmod developers does it take to reboot a server?
+A. Four. One to drive, one to recompile the kernel and one to report the progress on IRC.
+%
diff --git a/doc/fun/register.html b/doc/fun/register.html
new file mode 100644
index 0000000..3fe220b
--- /dev/null
+++ b/doc/fun/register.html
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>Twisted Matrix Labs Software Registration</title>
+ </head>
+
+ <body>
+ <h1>How to Register your Twisted Daemon instance</h1>
+
+ <h2>The Problem</h2>
+
+ <p>The Business Software Alliance may have put it best, in
+ their page on <a
+ href="http://www.bsa.org/usa/antipiracy/">anti-piracy</a>:</p>
+
+ <blockquote>
+ "Software is one of the most valuable technologies of the
+ Information Age, running everything from PCs to the Internet.
+ Unfortunately, because software is so valuable, and because
+ computers make it easy to create an exact copy of a program
+ in seconds, software piracy is widespread. From individual
+ computer users to professionals who deal wholesale in stolen
+ software, piracy exists in homes, schools, businesses and
+ government. Software pirates not only steal from the
+ companies that make the software, but with less money for
+ research and development of new software, all users are hurt.
+ That's why all software piracy - even one copy you make for a
+ friend, is illegal."
+ </blockquote>
+
+ <p>Software piracy is a serious crime, and one that the Open
+ Source community has been remarkably lax in pursuing and
+ protecting against. This is why Twisted Matrix Laboratories is
+ taking the forefront in Open Source software registration
+ technology.</p>
+
+ <h2>The Twisted Solution</h2>
+
+ <p>In order to do your part to prevent the tragedy of
+ unregistered, unlicensed software, all you need to do is visit
+ <a href="http://www.twistedmatrix.com/license">the Twisted
+ Matrix Labs Licensing and Registration page</a>, and enter your
+ user information to obtain a license key. You can provide us
+ with as much or as little information as you like!</p>
+
+ <p>This will produce a plain-text file which you should save as
+ "twisted-registration" (no quotes, no extension) and drop into
+ the same folder where you run your <code>twistd</code> server.
+ It will be automatically recognized by the server upon
+ startup.</p>
+
+ <h2>Other Benefits to You, the User</h2>
+
+ <p>Besides providing you with a license for the use of your
+ Twisted Daemon, a Twisted registration file also provides you
+ with a probably-unique identifier for your Twisted process -- a
+ helpful tool in writing peer-to-peer applications, or assigning
+ distributed database keys. <b>Privacy is important to us, and
+ this ID is <i>never</i> used by Twisted Matrix Labs software
+ for tracking purposes.</b> You need only register once, rather
+ than renegotiating unique IDs upon every run of your peered
+ application.</p>
+
+ <p>Registering with a valid e-mail address also gives Twisted
+ Matrix Labs a way to contact you with information about
+ upgrades and services as they become available. (Again,
+ registering with an e-mail address is <em>strictly
+ optional</em> to obtain your license key. If you don't want to
+ receive this information, you don't have to!)</p>
+
+ <p>Thank you for doing your part to end the piracy crisis.</p>
+ </body>
+</html>
+
diff --git a/doc/historic/2002/ipc10/twisted-network-framework/errata.html b/doc/historic/2002/ipc10/twisted-network-framework/errata.html
new file mode 100644
index 0000000..8388919
--- /dev/null
+++ b/doc/historic/2002/ipc10/twisted-network-framework/errata.html
@@ -0,0 +1,256 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>The World of Software is a World of Constant
+ Change</title>
+ </head>
+
+ <body>
+ <p><em><strong>Note:</strong> This document is relevant for the
+ version of Twisted that was current at <a
+ href="http://www.python10.com">IPC10</a>. It has since been
+ superseded by many changes to the Python API. It is remaining
+ unchanged for historical reasons, but please refer to
+ documentation for the specific system you are looking for and
+ not these papers for current information.</em></p>
+
+ <h1>The World of Software is a World of Constant Change</h1>
+
+ <p>Twisted has undergone several major revisions since Moshe
+ Zadka and I wrote the <a href="ipc10paper.html">"The Twisted
+ Network Framework"</a>. Most of these changes have not deviated
+ from the central vision of the framework, but almost all of the
+ code listings have been re-visited and enhanced in some
+ way.</p>
+
+ <p>So, while the paper was correct at the time that it was
+ originally written, a few things have changed which have
+ invalidated portions of it.</p>
+
+ <p>Most significant is the fact that almost all methods which
+ pass callbacks of some kind have been changed to take no
+ callback or error-callback arguments, and instead return an
+ instance of a <code
+ class="API">twisted.python.defer.Deferred</code>. This means
+ that an asynchronous function can be easily identified visually
+ because it will be of the form: <code
+ class="python">async_obj.asyncMethod("foo")<b>.addCallbacks(succeded,
+ failed)</b></code>. There is also a utility method <code
+ class="python">addCallback</code> which makes it more
+ convenient to pass additional arguments to a callback function
+ and omit special-case error handling.</p>
+
+ <p>While it is still backwards compatible, <code
+ class="API">twisted.internet.passport</code> has been re-named
+ to <code class="API">twisted.cred</code>, and the various
+ classes in it have been split out into submodules of that
+ package, and the various remote-object superclasses have been
+ moved out of twisted.spread.pb and put into
+ twisted.spread.flavors.</p>
+
+ <p><code class="python">Application.listenOn</code> has been
+ replaced with the more descripively named <code
+ class="python">Application.listenTCP</code>, <code
+ class="python">Application.listenUDP</code>, and <code
+ class="python">Application.listenSSL</code>.</p>
+
+ <p><code class="API">twisted.web.widgets</code> has progressed
+ quite far since the paper was written! One description
+ specifically given in the paper is no longer correct:</p>
+
+ <blockquote>
+ The namespace for evaluating the template expressions is
+ obtained by scanning the class hierarchy for attributes, and
+ getting each of those attributes from the current instance.
+ This means that all methods will be bound methods, so
+ indicating "self" explicitly is not required. While it is
+ possible to override the method for creating namespaces,
+ using this default has the effect of associating all
+ presentation code for a particular widget in one class, along
+ with its template. If one is working with a non-programmer
+ designer, and the template is in an external file, it is
+ always very clear to the designer what functionality is
+ available to them in any given scope, because there is a list
+ of available methods for any given class.
+ </blockquote>
+<p>This is still possible to avoid breakages in old code, but
+after some experimentation, it became clear that simply passing
+ <code class="python">self</code> was an easier method for
+ creating the namespace, both for designers and programmers.</p>
+ <p>In addition, since the advent of Zope3, interoperability
+ with Zope has become increasingly interesting possibility for
+ the Twisted development team, since it would be desirable if
+ Twisted could use their excellent strategy for
+ content-management, while still maintaining Twisted's
+ advantages in the arena of multi-protocol servers. Of
+ particular interest has been Zope Presentation Templates, since
+ they seem to be a truly robust solution for keeping design
+ discrete from code, compatible with the event-based method in
+ which twisted.web.widgets processes web requests. <code
+ class="API">twisted.web.widgets.ZopePresentationTemplate</code>
+ may be opening soon in a theatre near you!</p>
+
+ <p>The following code examples are corrected or modernized
+ versions of the ones that appear in the paper.</p>
+
+ <blockquote>
+ Listing 9: A remotely accessible object and accompanying call
+
+<pre class="python">
+# Server Side
+class MyObject(pb.Referenceable):
+ def remote_doIt(self):
+ return "did it"
+
+# Client Side
+ ...
+ def myCallback(result):
+ print result # result will be 'did it'
+ def myErrback(stacktrace):
+ print 'oh no, mr. bill!'
+ print stacktrace
+ myRemoteReference.doIt().addCallbacks(myCallback,
+ myErrback)
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 10: An object responding to its calling perspective
+<pre class="python">
+# Server Side
+class Greeter(pb.Viewable):
+ def view_greet(self, actor):
+ return "Hello %s!\n" % actor.perspectiveName
+
+# Client Side
+ ...
+ remoteGreeter.greet().addCallback(sys.stdout.write)
+ ...
+</pre>
+ </blockquote>
+
+
+ <blockquote>
+ Listing 12: A client for Echoer objects.
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def gotObject(object):
+ print "got object:",object
+ object.echo("hello network".addCallback(gotEcho)
+def gotEcho(echo):
+ print 'server echoed:',echo
+ main.shutDown()
+def gotNoObject(reason):
+ print "no object:",reason
+ main.shutDown()
+pb.getObjectAt("localhost", 8789, gotObject, gotNoObject, 30)
+main.run()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 13: A PB server using twisted's "passport"
+ authentication.
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class SimplePerspective(pb.Perspective):
+ def perspective_echo(self, text):
+ print 'echoing',text
+ return text
+class SimpleService(pb.Service):
+ def getPerspectiveNamed(self, name):
+ return SimplePerspective(name, self)
+if __name__ == '__main__':
+ import pbecho
+ app = main.Application("pbecho")
+ pbecho.SimpleService("pbecho",app).getPerspectiveNamed("guest").makeIdentity("guest")
+ app.listenTCP(pb.portno, pb.BrokerFactory(pb.AuthRoot(app)))
+ app.save("start")
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 14: Connecting to an Authorized Service
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def success(message):
+ print "Message received:",message
+ main.shutDown()
+def failure(error):
+ print "Failure...",error
+ main.shutDown()
+def connected(perspective):
+ perspective.echo("hello world").addCallbacks(success, failure)
+ print "connected."
+
+pb.connect("localhost", pb.portno, "guest", "guest",
+ "pbecho", "guest", 30).addCallbacks(connected,
+ failure)
+main.run()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 15: A Twisted GUI application
+<pre class="python">
+from twisted.internet import main, ingtkernet
+from twisted.spread.ui import gtkutil
+import gtk
+ingtkernet.install()
+class EchoClient:
+ def __init__(self, echoer):
+ l.hide()
+ self.echoer = echoer
+ w = gtk.GtkWindow(gtk.WINDOW_TOPLEVEL)
+ vb = gtk.GtkVBox(); b = gtk.GtkButton("Echo:")
+ self.entry = gtk.GtkEntry(); self.outry = gtk.GtkEntry()
+ w.add(vb)
+ map(vb.add, [b, self.entry, self.outry])
+ b.connect('clicked', self.clicked)
+ w.connect('destroy', gtk.mainquit)
+ w.show_all()
+ def clicked(self, b):
+ txt = self.entry.get_text()
+ self.entry.set_text("")
+ self.echoer.echo(txt).addCallback(self.outry.set_text)
+l = gtkutil.Login(EchoClient, None, initialService="pbecho")
+l.show_all()
+gtk.mainloop()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 16: an event-based web widget.
+<pre class="python">
+from twisted.spread import pb
+from twisted.python import defer
+from twisted.web import widgets
+class EchoDisplay(widgets.Gadget, widgets.Presentation):
+ template = """&lt;H1&gt;Welcome to my widget, displaying %%%%echotext%%%%.&lt;/h1&gt;
+ &lt;p&gt;Here it is: %%%%getEchoPerspective()%%%%&lt;/p&gt;"""
+ echotext = 'hello web!'
+ def getEchoPerspective(self):
+ return ['&lt;b&gt;',
+ pb.connect("localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 1).
+ addCallbacks(self.makeListOf, self.formatTraceback)
+ ,'&lt;/b&gt;']
+ def makeListOf(self, echoer):
+ return [echoer.echo(self.echotext).addCallback(lambda x: [x])]
+if __name__ == "__main__":
+ from twisted.web import server
+ from twisted.internet import main
+ a = main.Application("pbweb")
+ a.listenTCP(8080, server.Site(EchoDisplay()))
+ a.run()
+</pre>
+ </blockquote>
+ </body>
+</html>
+
diff --git a/doc/historic/2002/ipc10/twisted-network-framework/index.html b/doc/historic/2002/ipc10/twisted-network-framework/index.html
new file mode 100644
index 0000000..88fb488
--- /dev/null
+++ b/doc/historic/2002/ipc10/twisted-network-framework/index.html
@@ -0,0 +1,1568 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>The Twisted Network Framework</title>
+ </head>
+
+ <body>
+ <p><em><strong>Note:</strong> This document is relevant for the
+ version of Twisted that were current previous to <a
+ href="http://www.python10.com">IPC10</a>. Even at the time of
+ its release, <a href="ipc10errata.html">there were errata
+ issued</a> to make it current. It is remaining unaltered for
+ historical purposes but it is no longer accurate.</em></p>
+
+ <h1>The Twisted Network Framework</h1>
+
+ <h6>Moshe Zadka <a
+ href="mailto:m@moshez.org">m@moshez.org</a></h6>
+
+ <h6>Glyph Lefkowitz <a
+ href="mailto:glyph@twistedmatrix.com">glyph@twistedmatrix.com</a></h6>
+
+ <h3>Abstract</h3>
+
+ <p>Twisted is a framework for writing asynchronous,
+ event-driven networked programs in Python -- both clients and
+ servers. In addition to abstractions for low-level system calls
+ like <code>select(2)</code> and <code>socket(2)</code>, it also
+ includes a large number of utility functions and classes, which
+ make writing new servers easy. Twisted includes support for
+ popular network protocols like HTTP and SMTP, support for GUI
+ frameworks like <code>GTK+</code>/<code>GNOME</code> and
+ <code>Tk</code> and many other classes designed to make network
+ programs easy. Whenever possible, Twisted uses Python's
+ introspection facilities to save the client programmer as much
+ work as possible. Even though Twisted is still work in
+ progress, it is already usable for production systems -- it can
+ be used to bring up a Web server, a mail server or an IRC
+ server in a matter of minutes, and require almost no
+ configuration.</p>
+
+ <p><strong>Keywords:</strong> internet, network, framework,
+ event-based, asynchronous</p>
+
+ <h3>Introduction</h3>
+
+ <p>Python lends itself to writing frameworks. Python has a
+ simple class model, which facilitates inheritance. It has
+ dynamic typing, which means code needs to assume less. Python
+ also has built-in memory management, which means application
+ code does not need to track ownership. Thus, when writing a new
+ application, a programmer often finds himself writing a
+ framework to make writing this kind of application easier.
+ Twisted evolved from the need to write high-performance
+ interoperable servers in Python, and making them easy to use
+ (and difficult to use incorrectly).</p>
+
+ <p>There are three ways to write network programs:</p>
+
+ <ol>
+ <li>Handle each connection in a separate process</li>
+
+ <li>Handle each connection in a separate thread</li>
+
+ <li>Use non-blocking system calls to handle all connections
+ in one thread.</li>
+ </ol>
+
+ <p>When dealing with many connections in one thread, the
+ scheduling is the responsibility of the application, not the
+ operating system, and is usually implemented by calling a
+ registered function when each connection is ready to for
+ reading or writing -- commonly known as event-driven, or
+ callback-based, programming.</p>
+
+ <p>Since multi-threaded programming is often tricky, even with
+ high level abstractions, and since forking Python processes has
+ many disadvantages, like Python's reference counting not
+ playing well with copy-on-write and problems with shared state,
+ it was felt the best option was an event-driven framework. A
+ benefit of such approach is that by letting other event-driven
+ frameworks take over the main loop, server and client code are
+ essentially the same - making peer-to-peer a reality. While
+ Twisted includes its own event loop, Twisted can already
+ interoperate with <code>GTK+</code>'s and <code>Tk</code>'s
+ mainloops, as well as provide an emulation of event-based I/O
+ for Jython (specific support for the Swing toolkit is planned).
+ Client code is never aware of the loop it is running under, as
+ long as it is using Twisted's interface for registering for
+ interesting events.</p>
+
+ <p>Some examples of programs which were written using the
+ Twisted framework are <code>twisted.web</code> (a web server),
+ <code>twisted.mail</code> (a mail server, supporting both SMTP
+ and POP3, as well as relaying), <code>twisted.words</code> (a
+ chat application supporting integration between a variety of IM
+ protocols, like IRC, AOL Instant Messenger's TOC and
+ Perspective Broker, a remote-object protocol native to
+ Twisted), <code>im</code> (an instant messenger which connects
+ to twisted.words) and <code>faucet</code> (a GUI client for the
+ <code>twisted.reality</code> interactive-fiction framework).
+ Twisted can be useful for any network or GUI application
+ written in Python.</p>
+
+ <p>However, event-driven programming still contains some tricky
+ aspects. As each callback must be finished as soon as possible,
+ it is not possible to keep persistent state in function-local
+ variables. In addition, some programming techniques, such as
+ recursion, are impossible to use. Event-driven programming has
+ a reputation of being hard to use due to the frequent need to
+ write state machines. Twisted was built with the assumption
+ that with the right library, event-driven programming is easier
+ then multi-threaded programming. Twisted aims to be that
+ library.</p>
+
+ <p>Twisted includes both high-level and low-level support for
+ protocols. Most protocol implementation by twisted are in a
+ package which tries to implement "mechanisms, not policy". On
+ top of those implementations, Twisted includes usable
+ implementations of those protocols: for example, connecting the
+ abstract HTTP protocol handler to a concrete resource-tree, or
+ connecting the abstract mail protocol handler to deliver mail
+ to maildirs according to domains. Twisted tries to come with as
+ much functionality as possible out of the box, while not
+ constraining a programmer to a choice between using a
+ possibly-inappropriate class and rewriting the non-interesting
+ parts himself.</p>
+
+ <p>Twisted also includes Perspective Broker, a simple
+ remote-object framework, which allows Twisted servers to be
+ divided into separate processes as the end deployer (rather
+ then the original programmer) finds most convenient. This
+ allows, for example, Twisted web servers to pass requests for
+ specific URLs with co-operating servers so permissions are
+ granted according to the need of the specific application,
+ instead of being forced into giving all the applications all
+ permissions. The co-operation is truly symmetrical, although
+ typical deployments (such as the one which the Twisted web site
+ itself uses) use a master/slave relationship.</p>
+
+ <p>Twisted is not alone in the niche of a Python network
+ framework. One of the better known frameworks is Medusa. Medusa
+ is used, among other things, as Zope's native server serving
+ HTTP, FTP and other protocols. However, Medusa is no longer
+ under active development, and the Twisted development team had
+ a number of goals which would necessitate a rewrite of large
+ portions of Medusa. Twisted seperates protocols from the
+ underlying transport layer. This seperation has the advantages
+ of resuability (for example, using the same clients and servers
+ over SSL) and testability (because it is easy to test the
+ protocol with a much lighter test harness) among others.
+ Twisted also has a very flexible main-loop which can
+ interoperate with third-party main-loops, making it usable in
+ GUI programs too.</p>
+
+ <h3>Complementing Python</h3>
+
+ <p>Python comes out of the box with "batteries included".
+ However, it seems that many Python projects rewrite some basic
+ parts: logging to files, parsing options and high level
+ interfaces to reflection. When the Twisted project found itself
+ rewriting those, it moved them into a separate subpackage,
+ which does not depend on the rest of the twisted framework.
+ Hopefully, people will use <code>twisted.python</code> more and
+ solve interesting problems instead. Indeed, it is one of
+ Twisted's goals to serve as a repository for useful Python
+ code.</p>
+
+ <p>One useful module is <code>twisted.python.reflect</code>,
+ which has methods like <code>prefixedMethods</code>, which
+ returns all methods with a specific prefix. Even though some
+ modules in Python itself implement such functionality (notably,
+ <code>urllib2</code>), they do not expose it as a function
+ usable by outside code. Another useful module is
+ <code>twisted.python.hook</code>, which can add pre-hooks and
+ post-hooks to methods in classes.</p>
+
+ <blockquote>
+<pre class="python">
+# Add all method names beginning with opt_ to the given
+# dictionary. This cannot be done with dir(), since
+# it does not search in superclasses
+dct = {}
+reflect.addMethodNamesToDict(self.__class__, dct, "opt_")
+
+# Sum up all lists, in the given class and superclasses,
+# which have a given name. This gives us "different class
+# semantics": attributes do not override, but rather append
+flags = []
+reflect.accumulateClassList(self.__class__, 'optFlags', flags)
+
+# Add lock-acquire and lock-release to all methods which
+# are not multi-thread safe
+for methodName in klass.synchronized:
+ hook.addPre(klass, methodName, _synchPre)
+ hook.addPost(klass, methodName, _synchPost)
+
+</pre>
+
+ <h6>Listing 1: Using <code>twisted.python.reflect</code> and
+ <code>twisted.python.hook</code></h6>
+ </blockquote>
+
+ <p>The <code>twisted.python</code> subpackage also contains a
+ high-level interface to getopt which supplies as much power as
+ plain getopt while avoiding long
+ <code>if</code>/<code>elif</code> chains and making many common
+ cases easier to use. It uses the reflection interfaces in
+ <code>twisted.python.reflect</code> to find which options the
+ class is interested in, and constructs the argument to
+ <code>getopt</code>. Since in the common case options' values
+ are just saved in instance attributes, it is very easy to
+ indicate interest in such options. However, for the cases
+ custom code needs to be run for an option (for example,
+ counting how many <code>-v</code> options were given to
+ indicate verbosity level), it will call a method which is named
+ correctly.</p>
+
+ <blockquote>
+<pre class="python">
+class ServerOptions(usage.Options):
+ # Those are (short and long) options which
+ # have no argument. The corresponding attribute
+ # will be true iff this option was given
+ optFlags = [['nodaemon','n'],
+ ['profile','p'],
+ ['threaded','t'],
+ ['quiet','q'],
+ ['no_save','o']]
+ # This are options which require an argument
+ # The default is used if no such option was given
+ # Note: since options can only have string arguments,
+ # putting a non-string here is a reliable way to detect
+ # whether the option was given
+ optStrings = [['logfile','l',None],
+ ['file','f','twistd.tap'],
+ ['python','y',''],
+ ['pidfile','','twistd.pid'],
+ ['rundir','d','.']]
+
+ # For methods which can be called multiple times
+ # or have other unusual semantics, a method will be called
+ # Twisted assumes that the option needs an argument if and only if
+ # the method is defined to accept an argument.
+ def opt_plugin(self, pkgname):
+ pkg = __import__(pkgname)
+ self.python = os.path.join(os.path.dirname(
+ os.path.abspath(pkg.__file__)), 'config.tac')
+
+ # Most long options based on methods are aliased to short
+ # options. If there is only one letter, Twisted knows it is a short
+ # option, so it is "-g", not "--g"
+ opt_g = opt_plugin
+
+try:
+ config = ServerOptions()
+ config.parseOptions()
+except usage.error, ue:
+ print "%s: %s" % (sys.argv[0], ue)
+ sys.exit(1)
+</pre>
+
+ <h6>Listing 2: <code>twistd</code>'s Usage Code</h6>
+ </blockquote>
+
+ <p>Unlike <code>getopt</code>, Twisted has a useful abstraction
+ for the non-option arguments: they are passed as arguments to
+ the <code>parsedArgs</code> method. This means too many
+ arguments, or too few, will cause a usage error, which will be
+ flagged. If an unknown number of arguments is desired,
+ explicitly using a tuple catch-all argument will work.</p>
+
+ <h3>Configuration</h3>
+
+ <p>The formats of configuration files have shown two visible
+ trends over the years. On the one hand, more and more
+ programmability has been added, until sometimes they become a
+ new language. The extreme end of this trend is using a regular
+ programming language, such as Python, as the configuration
+ language. On the other hand, some configuration files became
+ more and more machine editable, until they become a miniature
+ database formates. The extreme end of that trend is using a
+ generic database tool.</p>
+
+ <p>Both trends stem from the same rationale -- the need to use
+ a powerful general purpose tool instead of hacking domain
+ specific languages. Domain specific languages are usually
+ ad-hoc and not well designed, having neither the power of
+ general purpose languages nor the predictable machine editable
+ format of generic databases.</p>
+
+ <p>Twisted combines these two trends. It can read the
+ configuration either from a Python file, or from a pickled
+ file. To some degree, it integrates the approaches by
+ auto-pickling state on shutdown, so the configuration files can
+ migrate from Python into pickles. Currently, there is no way to
+ go back from pickles to equivalent Python source, although it
+ is planned for the future. As a proof of concept, the RPG
+ framework Twisted Reality already has facilities for creating
+ Python source which evaluates into a given Python object.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import main
+from twisted.web import proxy, server
+site = server.Site(proxy.ReverseProxyResource('www.yahoo.com', 80, '/'))
+application = main.Application('web-proxy')
+application.listenOn(8080, site)
+</pre>
+
+ <h6>Listing 3: The configuration file for a reverse web
+ proxy</h6>
+ </blockquote>
+
+ <p>Twisted's main program, <code>twistd</code>, can receive
+ either a pickled <code>twisted.internet.main.Application</code>
+ or a Python file which defines a variable called
+ <code>application</code>. The application can be saved at any
+ time by calling its <code>save</code> method, which can take an
+ optional argument to save to a different file name. It would be
+ fairly easy, for example, to have a Twisted server which saves
+ the application every few seconds to a file whose name depends
+ on the time. Usually, however, one settles for the default
+ behavior which saves to a <code>shutdown</code> file. Then, if
+ the shutdown configuration proves suitable, the regular pickle
+ is replaced by the shutdown file. Hence, on the fly
+ configuration changes, regardless of complexity, can always
+ persist.</p>
+
+ <p>There are several client/server protocols which let a
+ suitably privileged user to access to application variable and
+ change it on the fly. The first, and least common denominator,
+ is telnet. The administrator can telnet into twisted, and issue
+ Python statements to her heart's content. For example, one can
+ add ports to listen on to the application, reconfigure the web
+ servers and various other ways by simple accessing
+ <code>__main__.application</code>. Some proof of concepts for a
+ simple suite of command-line utilities to control a Twisted
+ application were written, including commands which allow an
+ administrator to shut down the server or save the current state
+ to a tap file. These are especially useful on Microsoft
+ Windows(tm) platforms, where the normal UNIX way of
+ communicating shutdown requests via signals are less
+ reliable.</p>
+
+ <p>If reconfiguration on the fly is not necessary, Python
+ itself can be used as the configuration editor. Loading the
+ application is as simple as unpickling it, and saving it is
+ done by calling its <code>save</code> method. It is quite easy
+ to add more services or change existing ones from the Python
+ interactive mode.</p>
+
+ <p>A more sophisticated way to reconfigure the application on
+ the fly is via the manhole service. Manhole is a client/server
+ protocol based on top of Perspective Broker, Twisted's
+ translucent remote-object protocol which will be covered later.
+ Manhole has a graphical client called <code>gtkmanhole</code>
+ which can access the server and change its state. Since Twisted
+ is modular, it is possible to write more services for user
+ friendly configuration. For example, through-the-web
+ configuration is planned for several services, notably
+ mail.</p>
+
+ <p>For cases where a third party wants to distribute both the
+ code for a server and a ready to run configuration file, there
+ is the plugin configuration. Philosophically similar to the
+ <code>--python</code> option to <code>twistd</code>, it
+ simplifies the distribution process. A plugin is an archive
+ which is ready to be unpacked into the Python module path. In
+ order to keep a clean tree, <code>twistd</code> extends the
+ module path with some Twisted-specific paths, like the
+ directory <code>TwistedPlugins</code> in the user's home
+ directory. When a plugin is unpacked, it should be a Python
+ package which includes, alongside <code>__init__.py</code> a
+ file named <code>config.tac</code>. This file should define a
+ variable named <code>application</code>, in a similar way to
+ files loaded with <code>--python</code>. The plugin way of
+ distributing configurations is meant to reduce the temptation
+ to put large amount of codes inside the configuration file
+ itself.</p>
+
+ <p>Putting class and function definition inside the
+ configuration files would make the persistent servers which are
+ auto-generated on shutdown useless, since they would not have
+ access to the classes and functions defined inside the
+ configuration file. Thus, the plugin method is intended so
+ classes and functions can still be in regular, importable,
+ Python modules, but still allow third parties distribute
+ powerful configurations. Plugins are used by some of the
+ Twisted Reality virtual worlds.</p>
+
+ <h3>Ports, Protocol and Protocol Factories</h3>
+
+ <p><code>Port</code> is the Twisted class which represents a
+ socket listening on a port. Currently, twisted supports both
+ internet and unix-domain sockets, and there are SSL classes
+ with identical interface. A <code>Port</code> is only
+ responsible for handling the transfer layer. It calls
+ <code>accept</code> on the socket, checks that it actually
+ wants to deal with the connection and asks its factory for a
+ protocol. The factory is usually a subclass of
+ <code>twisted.protocols.protocol.Factory</code>, and its most
+ important method is <code>buildProtocol</code>. This should
+ return something that adheres to the protocol interface, and is
+ usually a subclass of
+ <code>twisted.protocols.protocol.Protocol</code>.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.protocols import protocol
+from twisted.internet import main, tcp
+
+class Echo(protocol.Protocol):
+ def dataReceived(self, data):
+ self.transport.write(data)
+
+factory = protocol.Factory()
+factory.protocol = Echo
+port = tcp.Port(8000, factory)
+app = main.Application("echo")
+app.addPort(port)
+app.run()
+</pre>
+
+ <h6>Listing 4: A Simple Twisted Application</h6>
+ </blockquote>
+
+ <p>The factory is responsible for two tasks: creating new
+ protocols, and keeping global configuration and state. Since
+ the factory builds the new protocols, it usually makes sure the
+ protocols have a reference to it. This allows protocols to
+ access, and change, the configuration. Keeping state
+ information in the factory is the primary reason for keeping an
+ abstraction layer between ports and protocols. Examples of
+ configuration information is the root directory of a web server
+ or the user database of a telnet server. Note that it is
+ possible to use the same factory in two different Ports. This
+ can be used to run the same server bound to several different
+ addresses but not to all of them, or to run the same server on
+ a TCP socket and a UNIX domain sockets.</p>
+
+ <p>A protocol begins and ends its life with
+ <code>connectionMade</code> and <code>connectionLost</code>;
+ both are called with no arguments. <code>connectionMade</code>
+ is called when a connection is first established. By then, the
+ protocol has a <code>transport</code> attribute. The
+ <code>transport</code> attribute is a <code>Transport</code> -
+ it supports <code>write</code> and <code>loseConnection</code>.
+ Both these methods never block: <code>write</code> actually
+ buffers data which will be written only when the transport is
+ signalled ready to for writing, and <code>loseConnection</code>
+ marks the transport for closing as soon as there is no buffered
+ data. Note that transports do <em>not</em> have a
+ <code>read</code> method: data arrives when it arrives, and the
+ protocol must be ready for its <code>dataReceived</code>
+ method, or its <code>connectionLost</code> method, to be
+ called. The transport also supports a <code>getPeer</code>
+ method, which returns parameters about the other side of the
+ transport. For TCP sockets, this includes the remote IP and
+ port.</p>
+
+ <blockquote>
+<pre class="python">
+# A tcp port-forwarder
+# A StupidProtocol sends all data it gets to its peer.
+# A StupidProtocolServer connects to the host/port,
+# and initializes the client connection to be its peer
+# and itself to be the client's peer
+from twisted.protocols import protocol
+
+class StupidProtocol(protocol.Protocol):
+ def connectionLost(self): self.peer.loseConnection();del self.peer
+ def dataReceived(self, data): self.peer.write(data)
+
+class StupidProtocolServer(StupidProtocol):
+ def connectionMade(self):
+ clientProtocol = StupidProtocol()
+ clientProtocol.peer = self.transport
+ self.peer = tcp.Client(self.factory.host, self.factory.port,
+ clientProtocol)
+
+# Create a factory which creates StupidProtocolServers, and
+# has the configuration information they assume
+def makeStupidFactory(host, port):
+ factory = protocol.Factory()
+ factory.host, factory.port = host, port
+ factory.protocol = StupidProtocolServer
+ return factory
+</pre>
+
+ <h6>Listing 5: TCP forwarder code</h6>
+ </blockquote>
+
+ <h3>The Event Loop</h3>
+
+ <p>While Twisted has the ability to let other event loops take
+ over for integration with GUI toolkits, it usually uses its own
+ event loop. The event loop code uses global variables to
+ maintain interested readers and writers, and uses Python's
+ <code>select()</code> function, which can accept any object
+ which has a <code>fileno()</code> method, not only raw file
+ descriptors. Objects can use the event loop interface to
+ indicate interest in either reading to or writing from a given
+ file descriptor. In addition, for those cases where time-based
+ events are needed (for example, queue flushing or periodic POP3
+ downloads), Twisted has a mechanism for repeating events at
+ known delays. While far from being real-time, this is enough
+ for most programs' needs.</p>
+
+ <h3>Going Higher Level</h3>
+
+ <p>Unfortunately, handling arbitrary data chunks is a hard way
+ to code a server. This is why twisted has many classes sitting
+ in submodules of the twisted.protocols package which give
+ higher level interface to the data. For line oriented
+ protocols, <code>LineReceiver</code> translates the low-level
+ <code>dataReceived</code> events into <code>lineReceived</code>
+ events. However, the first naive implementation of
+ <code>LineReceiver</code> proved to be too simple. Protocols
+ like HTTP/1.1 or Freenet have packets which begin with header
+ lines that include length information, and then byte streams.
+ <code>LineReceiver</code> was rewritten to have a simple
+ interface for switching at the protocol layer between
+ line-oriented parts and byte-stream parts.</p>
+
+ <p>Another format which is gathering popularity is Dan J.
+ Bernstein's netstring format. This format keeps ASCII text as
+ ASCII, but allows arbitrary bytes (including nulls and
+ newlines) to be passed freely. However, netstrings were never
+ designed to be used in event-based protocols where over-reading
+ is unavoidable. Twisted makes sure no user will have to deal
+ with the subtle problems handling netstrings in event-driven
+ programs by providing <code>NetstringReceiver</code>.</p>
+
+ <p>For even higher levels, there are the protocol-specific
+ protocol classes. These translate low-level chunks into
+ high-level events such as "HTTP request received" (for web
+ servers), "approve destination address" (for mail servers) or
+ "get user information" (for finger servers). Many RFCs have
+ been thus implemented for Twisted (at latest count, more then
+ 12 RFCs have been implemented). One of Twisted's goals is to be
+ a repository of event-driven implementations for various
+ protocols in Python.</p>
+
+ <blockquote>
+<pre class="python">
+class DomainSMTP(SMTP):
+
+ def validateTo(self, helo, destination):
+ try:
+ user, domain = string.split(destination, '@', 1)
+ except ValueError:
+ return 0
+ if not self.factory.domains.has_key(domain):
+ return 0
+ if not self.factory.domains[domain].exists(user, domain, self):
+ return 0
+ return 1
+
+ def handleMessage(self, helo, origin, recipients, message):
+ # No need to check for existence -- only recipients which
+ # we approved at the validateTo stage are passed here
+ for recipient in recipients:
+ user, domain = string.split(recipient, '@', 1)
+ self.factory.domains[domain].saveMessage(origin, user, message,
+ domain)
+</pre>
+
+ <h6>Listing 6: Implementation of virtual domains using the
+ SMTP protocol class</h6>
+ </blockquote>
+
+ <p>Copious documentation on writing new protocol abstraction
+ exists, since this is the largest amount of code written --
+ much like most operating system code is device drivers. Since
+ many different protocols have already been implemented, there
+ are also plenty of examples to draw on. Usually implementing
+ the client-side of a protocol is particularly challenging,
+ since protocol designers tend to assume much more state kept on
+ the client side of a connection then on the server side.</p>
+
+ <h3>The <code>twisted.tap</code> Package and
+ <code>mktap</code></h3>
+
+ <p>Since one of Twisted's configuration formats are pickles,
+ which are tricky to edit by hand, Twisted evolved a framework
+ for creating such pickles. This framework is contained in the
+ <code>twisted.tap</code> package and the <code>mktap</code>
+ script. New servers, or new ways to configure existing servers,
+ can easily participate in the twisted.tap framework by creating
+ a <code>twisted.tap</code> submodule.</p>
+
+ <p>All <code>twisted.tap</code> submodules must conform to a
+ rigid interface. The interface defines functions to accept the
+ command line parameters, and functions to take the processed
+ command line parameters and add servers to
+ <code>twisted.main.internet.Application</code>. Existing
+ <code>twisted.tap</code> submodules use
+ <code>twisted.python.usage</code>, so the command line format
+ is consistent between different modules.</p>
+
+ <p>The <code>mktap</code> utility gets some generic options,
+ and then the name of the server to build. It imports a
+ same-named <code>twisted.tap</code> submodule, and lets it
+ process the rest of the options and parameters. This makes sure
+ that the process configuring the <code>main.Application</code>
+ is agnostic for where it is used. This allowed
+ <code>mktap</code> to grow the <code>--append</code> option,
+ which appends to an existing pickle rather then creating a new
+ one. This option is frequently used to post-add a telnet server
+ to an application, for net-based on the fly configuration
+ later.</p>
+
+ <p>When running <code>mktap</code> under UNIX, it saves the
+ user id and group id inside the tap. Then, when feeding this
+ tap into <code>twistd</code>, it changes to this user/group id
+ after binding the ports. Such a feature is necessary in any
+ production-grade server, since ports below 1024 require root
+ privileges to use on UNIX -- but applications should not run as
+ root. In case changing to the specified user causes difficulty
+ in the build environment, it is also possible to give those
+ arguments to <code>mktap</code> explicitly.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import tcp, stupidproxy
+from twisted.python import usage
+
+usage_message = """
+usage: mktap stupid [OPTIONS]
+
+Options are as follows:
+ --port &lt;#&gt;, -p: set the port number to &lt;#&gt;.
+ --host &lt;host&gt;, -h: set the host to &lt;host&gt;
+ --dest_port &lt;#&gt;, -d: set the destination port to &lt;#&gt;
+"""
+
+class Options(usage.Options):
+ optStrings = [["port", "p", 6666],
+ ["host", "h", "localhost"],
+ ["dest_port", "d", 6665]]
+
+def getPorts(app, config):
+ s = stupidproxy.makeStupidFactory(config.host, int(config.dest_port))
+ return [(int(config.port), s)]
+</pre>
+
+ <h6>Listing 7: <code>twisted.tap.stupid</code></h6>
+ </blockquote>
+
+ <p>The <code>twisted.tap</code> framework is one of the reasons
+ servers can be set up with little knowledge and time. Simply
+ running <code>mktap</code> with arguments can bring up a web
+ server, a mail server or an integrated chat server -- with
+ hardly any need for maintainance. As a working
+ proof-on-concept, the <code>tap2deb</code> utility exists to
+ wrap up tap files in Debian packages, which include scripts for
+ running and stopping the server and interact with
+ <code>init(8)</code> to make sure servers are automatically run
+ on start-up. Such programs can also be written to interface
+ with the Red Hat Package Manager or the FreeBSD package
+ management systems.</p>
+
+ <blockquote>
+<pre class="shell">
+% mktap --uid 33 --gid 33 web --static /var/www --port 80
+% tap2deb -t web.tap -m 'Moshe Zadka &lt;moshez@debian.org&gt;'
+% su
+password:
+# dpkg -i .build/twisted-web_1.0_all.deb
+</pre>
+
+ <h6>Listing 8: Bringing up a web server on a Debian
+ system</h6>
+ </blockquote>
+
+ <h3>Multi-thread Support</h3>
+
+ <p>Sometimes, threads are unavoidable or hard to avoid. Many
+ legacy programs which use threads want to use Twisted, and some
+ vendor APIs have no non-blocking version -- for example, most
+ database systems' API. Twisted can work with threads, although
+ it supports only one thread in which the main select loop is
+ running. It can use other threads to simulate non-blocking API
+ over a blocking API -- it spawns a thread to call the blocking
+ API, and when it returns, the thread calls a callback in the
+ main thread. Threads can call callbacks in the main thread
+ safely by adding those callbacks to a list of pending events.
+ When the main thread is between select calls, it searches
+ through the list of pending events, and executes them. This is
+ used in the <code>twisted.enterprise</code> package to supply
+ an event driven interfaces to databases, which uses Python's DB
+ API.</p>
+
+ <p>Twisted tries to optimize for the common case -- no threads.
+ If there is need for threads, a special call must be made to
+ inform the <code>twisted.python.threadable</code> module that
+ threads will be used. This module is implemented differently
+ depending on whether threads will be used or not. The decision
+ must be made before importing any modules which use threadable,
+ and so is usually done in the main application. For example,
+ <code>twistd</code> has a command line option to initialize
+ threads.</p>
+
+ <p>Twisted also supplies a module which supports a threadpool,
+ so the common task of implementing non-blocking APIs above
+ blocking APIs will be both easy and efficient. Threads are kept
+ in a pool, and dispatch requests are done by threads which are
+ not working. The pool supports a maximum amount of threads, and
+ will throw exceptions when there are more requests than
+ allowable threads.</p>
+
+ <p>One of the difficulties about multi-threaded systems is
+ using locks to avoid race conditions. Twisted uses a mechanism
+ similar to Java's synchronized methods. A class can declare a
+ list of methods which cannot safely be called at the same time
+ from two different threads. A function in threadable then uses
+ <code>twisted.python.hook</code> to transparently add
+ lock/unlock around these methods. This allows Twisted classes
+ to be written without thought about threading, except for one
+ localized declaration which does not entail any performance
+ penalty for the single-threaded case.</p>
+
+ <h3>Twisted Mail Server</h3>
+
+ <p>Mail servers have a history of security flaws. Sendmail is
+ by now the poster boy of security holes, but no mail servers,
+ bar maybe qmail, are free of them. Like Dan Bernstein of qmail
+ fame said, mail cannot be simply turned off -- even the
+ simplest organization needs a mail server. Since Twisted is
+ written in a high-level language, many problems which plague
+ other mail servers, notably buffer overflows, simply do not
+ exist. Other holes are avoidable with correct design. Twisted
+ Mail is a project trying to see if it is possible to write a
+ high quality high performance mail server entirely in
+ Python.</p>
+
+ <p>Twisted Mail is built on the SMTP server and client protocol
+ classes. While these present a level of abstraction from the
+ specific SMTP line semantics, they do not contain any message
+ storage code. The SMTP server class does know how to divide
+ responsibility between domains. When a message arrives, it
+ analyzes the recipient's address, tries matching it with one of
+ the registered domain, and then passes validation of the
+ address and saving the message to the correct domain, or
+ refuses to handle the message if it cannot handle the domain.
+ It is possible to specify a catch-all domain, which will
+ usually be responsible for relaying mails outwards.</p>
+
+ <p>While correct relaying is planned for the future, at the
+ moment we have only so-called "smarthost" relaying. All e-mail
+ not recognized by a local domain is relayed to a single outside
+ upstream server, which is supposed to relay the mail further.
+ This is the configuration for most home machines, which are
+ Twisted Mail's current target audience.</p>
+
+ <p>Since the people involved in Twisted's development were
+ reluctant to run code that runs as a super user, or with any
+ special privileges, it had to be considered how delivery of
+ mail to users is possible. The solution decided upon was to
+ have Twisted deliver to its own directory, which should have
+ very strict permissions, and have users pull the mail using
+ some remote mail access protocol like POP3. This means only a
+ user would write to his own mail box, so no security holes in
+ Twisted would be able to adversely affect a user.</p>
+
+ <p>Future plans are to use a Perspective Broker-based service
+ to hand mail to users to a personal server using a UNIX domain
+ socket, as well as to add some more conventional delivery
+ methods, as scary as they may be.</p>
+
+ <p>Because the default configuration of Twisted Mail is to be
+ an integrated POP3/SMTP servers, it is ideally suited for the
+ so-called POP toaster configuration, where there are a
+ multitude of virtual users and domains, all using the same IP
+ address and computer to send and receive mails. It is fairly
+ easy to configure Twisted as a POP toaster. There are a number
+ of deployment choices: one can append a telnet server to the
+ tap for remote configuration, or simple scripts can add and
+ remove users from the user database. The user database is saved
+ as a directory, where file names are keys and file contents are
+ values, so concurrency is not usually a problem.</p>
+
+ <blockquote>
+<pre class="shell">
+% mktap mail -d foobar.com=$HOME/Maildir/ -u postmaster=secret -b \
+ -p 110 -s 25
+% twistd -f mail.tap
+
+</pre>
+
+ <h6>Bringing up a simple mail-server</h6>
+ </blockquote>
+
+ <p>Twisted's native mail storage format is Maildir, a format
+ that requires no locking and is safe and atomic. Twisted
+ supports a number of standardized extensions to Maildir,
+ commonly known as Maildir++. Most importantly, it supports
+ deletion as simply moving to a subfolder named
+ <code>Trash</code>, so mail is recoverable if accessed through
+ a protocol which allows multiple folders, like IMAP. However,
+ Twisted itself currently does not support any such protocol
+ yet.</p>
+
+ <h3>Introducing Perspective Broker</h3>
+
+ <h4>All the World's a Game</h4>
+
+ <p>Twisted was originally designed to support multi-player
+ games; a simulated "real world" environment. Experience with
+ game systems of that type is enlightening as to the nature of
+ computing on the whole. Almost all services on a computer are
+ modeled after some simulated real-world activity. For example,
+ e-"mail", or "document publishing" on the web. Even
+ "object-oriented" programming is based around the notion that
+ data structures in a computer simulate some analogous
+ real-world objects.</p>
+
+ <p>All such networked simulations have a few things in common.
+ They each represent a service provided by software, and there
+ is usually some object where "global" state is kept. Such a
+ service must provide an authentication mechanism. Often, there
+ is a representation of the authenticated user within the
+ context of the simulation, and there are also objects aside
+ from the user and the simulation itself that can be
+ accessed.</p>
+
+ <p>For most existing protocols, Twisted provides these
+ abstractions through <code>twisted.internet.passport</code>.
+ This is so named because the most important common
+ functionality it provides is authentication. A simulation
+ "world" as described above -- such as an e-mail system,
+ document publishing archive, or online video game -- is
+ represented by subclass of <code>Service</code>, the
+ authentication mechanism by an <code>Authorizer</code> (which
+ is a set of <code>Identities</code>), and the user of the
+ simulation by a <code>Perspective</code>. Other objects in the
+ simulation may be represented by arbitrary python objects,
+ depending upon the implementation of the given protocol.</p>
+
+ <p>New problem domains, however, often require new protocols,
+ and re-implementing these abstractions each time can be
+ tedious, especially when it's not necessary. Many efforts have
+ been made in recent years to create generic "remote object" or
+ "remote procedure call" protocols, but in developing Twisted,
+ these protocols were found to require too much overhead in
+ development, be too inefficient at runtime, or both.</p>
+
+ <p>Perspective Broker is a new remote-object protocol designed
+ to be lightweight and impose minimal constraints upon the
+ development process and use Python's dynamic nature to good
+ effect, but still relatively efficient in terms of bandwidth
+ and CPU utilization. <code>twisted.spread.pb</code> serves as a
+ reference implementation of the protocol, but implementation of
+ Perspective Broker in other languages is already underway.
+ <code>spread</code> is the <code>twisted</code> subpackage
+ dealing with remote calls and objects, and has nothing to do
+ with the <code>spread</code> toolkit.</p>
+
+ <p>Perspective Broker extends
+ <code>twisted.internet.passport</code>'s abstractions to be
+ concrete objects rather than design patterns. Rather than
+ having a <code>Protocol</code> implementation translate between
+ sequences of bytes and specifically named methods (as in the
+ other Twisted <code>Protocols</code>), Perspective Broker
+ defines a direct mapping between network messages and
+ quasi-arbitrary method calls.</p>
+
+ <h3>Translucent, not Transparent</h3>
+
+ <p>In a server application where a large number of clients may
+ be interacting at once, it is not feasible to have an
+ arbitrarily large number of OS threads blocking and waiting for
+ remote method calls to return. Additionally, the ability for
+ any client to call any method of an object would present a
+ significant security risk. Therefore, rather than attempting to
+ provide a transparent interface to remote objects,
+ <code>twisted.spread.pb</code> is "translucent", meaning that
+ while remote method calls have different semantics than local
+ ones, the similarities in semantics are mirrored by
+ similarities in the syntax. Remote method calls impose as
+ little overhead as possible in terms of volume of code, but "as
+ little as possible" is unfortunately not "nothing".</p>
+
+ <p><code>twisted.spread.pb</code> defines a method naming
+ standard for each type of remotely accessible object. For
+ example, if a client requests a method call with an expression
+ such as <code>myPerspective.doThisAction()</code>, the remote
+ version of <code>myPerspective</code> would be sent the message
+ <code>perspective_doThisAction</code>. Depending on the manner
+ in which an object is accessed, other method prefixes may be
+ <code>observe_</code>, <code>view_</code>, or
+ <code>remote_</code>. Any method present on a remotely
+ accessible object, and named appropriately, is considered to be
+ published -- since this is accomplished with
+ <code>getattr</code>, the definition of "present" is not just
+ limited to methods defined on the class, but instances may have
+ arbitrary callable objects associated with them as long as the
+ name is correct -- similarly to normal python objects.</p>
+
+ <p>Remote method calls are made on remote reference objects
+ (instances of <code>pb.RemoteReference</code>) by calling a
+ method with an appropriate name. However, that call will not
+ block -- if you need the result from a remote method call, you
+ pass in one of the two special keyword arguments to that method
+ -- <code>pbcallback</code> or <code>pberrback</code>.
+ <code>pbcallback</code> is a callable object which will be
+ called when the result is available, and <code>pberrback</code>
+ is a callable object which will be called if there was an
+ exception thrown either in transmission of the call or on the
+ remote side.</p>
+
+ <p>In the case that neither <code>pberrback</code> or
+ <code>pbcallback</code> is provided,
+ <code>twisted.spread.pb</code> will optimize network usage by
+ not sending confirmations of messages.</p>
+
+ <blockquote>
+<pre class="python">
+# Server Side
+class MyObject(pb.Referenceable):
+ def remote_doIt(self):
+ return "did it"
+
+# Client Side
+ ...
+ def myCallback(result):
+ print result # result will be 'did it'
+ def myErrback(stacktrace):
+ print 'oh no, mr. bill!'
+ print stacktrace
+ myRemoteReference.doIt(pbcallback=myCallback,
+ pberrback=myErrback)
+</pre>
+
+ <h6>Listing 9: A remotely accessible object and accompanying
+ call</h6>
+ </blockquote>
+
+ <h3>Different Behavior for Different Perspectives</h3>
+
+ <p>Considering the problem of remote object access in terms of
+ a simulation demonstrates a requirement for the knowledge of an
+ actor with certain actions or requests. Often, when processing
+ message, it is useful to know who sent it, since different
+ results may be required depending on the permissions or state
+ of the caller.</p>
+
+ <p>A simple example is a game where certain an object is
+ invisible, but players with the "Heightened Perception"
+ enchantment can see it. When answering the question "What
+ objects are here?" it is important for the room to know who is
+ asking, to determine which objects they can see. Parallels to
+ the differences between "administrators" and "users" on an
+ average multi-user system are obvious.</p>
+
+ <p>Perspective Broker is named for the fact that it does not
+ broker only objects, but views of objects. As a user of the
+ <code>twisted.spread.pb</code> module, it is quite easy to
+ determine the caller of a method. All you have to do is
+ subclass <code>Viewable</code>.</p>
+
+ <blockquote>
+<pre class="python">
+# Server Side
+class Greeter(pb.Viewable):
+ def view_greet(self, actor):
+ return "Hello %s!\n" % actor.perspectiveName
+
+# Client Side
+ ...
+ remoteGreeter.greet(pbcallback=sys.stdout.write)
+ ...
+</pre>
+
+ <h6>Listing 10: An object responding to its calling
+ perspective</h6>
+ </blockquote>
+ Before any arguments sent by the client, the actor
+ (specifically, the Perspective instance through which this
+ object was retrieved) will be passed as the first argument to
+ any <code>view_xxx</code> methods.
+
+ <h3>Mechanisms for Sharing State</h3>
+
+ <p>In a simulation of any decent complexity, client and server
+ will wish to share structured data. Perspective Broker provides
+ a mechanism for both transferring (copying) and sharing
+ (caching) that state.</p>
+
+ <p>Whenever an object is passed as an argument to or returned
+ from a remote method call, that object is serialized using
+ <code>twisted.spread.jelly</code>; a serializer similar in some
+ ways to Python's native <code>pickle</code>. Originally,
+ <code>pickle</code> itself was going to be used, but there were
+ several security issues with the <code>pickle</code> code as it
+ stands. It is on these issues of security that
+ <code>pickle</code> and <code>twisted.spread.jelly</code> part
+ ways.</p>
+
+ <p>While <code>twisted.spread.jelly</code> handles a few basic
+ types such as strings, lists, dictionaries and numbers
+ automatically, all user-defined types must be registered both
+ for serialization and unserialization. This registration
+ process is necessary on the sending side in order to determine
+ if a particular object is shared, and whether it is shared as
+ state or behavior. On the receiving end, it's necessary to
+ prevent arbitrary code from being run when an object is
+ unserialized -- a significant security hole in
+ <code>pickle</code> for networked applications.</p>
+
+ <p>On the sending side, the registration is accomplished by
+ making the object you want to serialize a subclass of one of
+ the "flavors" of object that are handled by Perspective Broker.
+ A class may be <code>Referenceable</code>,
+ <code>Viewable</code>, <code>Copyable</code> or
+ <code>Cacheable</code>. These four classes correspond to
+ different ways that the object will be seen remotely.
+ Serialization flavors are mutually exclusive -- these 4 classes
+ may not be mixed in with each other.</p>
+
+ <ul>
+ <li><code>Referenceable</code>: The remote side will refer to
+ this object directly. Methods with the prefix
+ <code>remote_</code> will be callable on it. No state will be
+ transferred.</li>
+
+ <li><code>Viewable</code>: The remote side will refer to a
+ proxy for this object, which indicates what perspective
+ accessed this; as discussed above. Methods with the prefix
+ <code>view_</code> will be callable on it, and have an
+ additional first argument inserted (the perspective that
+ called the method). No state will be transferred.</li>
+
+ <li><code>Copyable</code>: Each time this object is
+ serialized, its state will be copied and sent. No methods are
+ remotely callable on it. By default, the state sent will be
+ the instance's <code>__dict__</code>, but a method
+ <code>getStateToCopyFor(perspective)</code> may be defined
+ which returns an arbitrary serializable object for
+ state.</li>
+
+ <li><code>Cacheable</code>: The first time this object is
+ serialized, its state will be copied and sent. Each
+ subsequent time, however, a reference to the original object
+ will be sent to the receiver. No methods will be remotely
+ callable on this object. By default, again, the state sent
+ will be the instance's <code>__dict__</code>but a method
+ <code>getStateToCacheAndObserveFor(perspective,
+ observer)</code> may be defined to return alternative state.
+ Since the state for this object is only sent once, the
+ <code>observer</code> argument is an object representative of
+ the receiver's representation of the <code>Cacheable</code>
+ after unserialization -- method calls to this object will be
+ resolved to methods prefixed with <code>observe_</code>,
+ <em>on the receiver's <code>RemoteCache</code> of this
+ object</em>. This may be used to keep the receiver's cache
+ up-to-date as relevant portions of the <code>Cacheable</code>
+ object change.</li>
+ </ul>
+
+ <h3>Publishing Objects with PB</h3>
+
+ <p>The previous samples of code have shown how an individual
+ object will interact over a previously-established PB
+ connection. In order to get to that connection, you need to do
+ some set-up work on both the client and server side; PB
+ attempts to minimize this effort.</p>
+
+ <p>There are two different approaches for setting up a PB
+ server, depending on your application's needs. In the simplest
+ case, where your application does not deal with the
+ abstractions above -- services, identities, and perspectives --
+ you can simply publish an object on a particular port.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class Echoer(pb.Root):
+ def remote_echo(self, st):
+ print 'echoing:', st
+ return st
+if __name__ == '__main__':
+ app = main.Application("pbsimple")
+ app.listenOn(8789, pb.BrokerFactory(Echoer()))
+ app.run()
+</pre>
+
+ <h6>Listing 11: Creating a simple PB server</h6>
+ </blockquote>
+
+ <p>Listing 11 shows how to publish a simple object which
+ responds to a single message, "echo", and returns whatever
+ argument is sent to it. There is very little to explain: the
+ "Echoer" class is a pb.Root, which is a small subclass of
+ Referenceable designed to be used for objects published by a
+ BrokerFactory, so Echoer follows the same rule for remote
+ access that Referenceable does. Connecting to this service is
+ almost equally simple.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def gotObject(object):
+ print "got object:",object
+ object.echo("hello network", pbcallback=gotEcho)
+def gotEcho(echo):
+ print 'server echoed:',echo
+ main.shutDown()
+def gotNoObject(reason):
+ print "no object:",reason
+ main.shutDown()
+pb.getObjectAt("localhost", 8789, gotObject, gotNoObject, 30)
+main.run()
+</pre>
+
+ <h6>Listing 12: A client for Echoer objects.</h6>
+ </blockquote>
+
+ <p>The utility function <code>pb.getObjectAt</code> retrieves
+ the root object from a hostname/port-number pair and makes a
+ callback (in this case, <code>gotObject</code>) if it can
+ connect and retrieve the object reference successfully, and an
+ error callback (<code>gotNoObject</code>) if it cannot connect
+ or the connection times out.</p>
+
+ <p><code>gotObject</code> receives the remote reference, and
+ sends the <code>echo</code> message to it. This call is
+ visually noticeable as a remote method invocation by the
+ distinctive <code>pbcallback</code> keyword argument. When the
+ result from that call is received, <code>gotEcho</code> will be
+ called, notifying us that in fact, the server echoed our input
+ ("hello network").</p>
+
+ <p>While this setup might be useful for certain simple types of
+ applications where there is no notion of a "user", the
+ additional complexity necessary for authentication and service
+ segregation is worth it. In particular, re-use of server code
+ for things like chat (twisted.words) is a lot easier with a
+ unified notion of users and authentication.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class SimplePerspective(pb.Perspective):
+ def perspective_echo(self, text):
+ print 'echoing',text
+ return text
+class SimpleService(pb.Service):
+ def getPerspectiveNamed(self, name):
+ return SimplePerspective(name, self)
+if __name__ == '__main__':
+ import pbecho
+ app = main.Application("pbecho")
+ pbecho.SimpleService("pbecho",app).getPerspectiveNamed("guest")\
+ .makeIdentity("guest")
+ app.listenOn(pb.portno, pb.BrokerFactory(pb.AuthRoot(app)))
+ app.save("start")
+</pre>
+
+ <h6>Listing 13: A PB server using twisted's "passport"
+ authentication.</h6>
+ </blockquote>
+
+ <p>In terms of the "functionality" it offers, this server is
+ identical. It provides a method which will echo some simple
+ object sent to it. However, this server provides it in a manner
+ which will allow it to cooperate with multiple other
+ authenticated services running on the same connection, because
+ it uses the central Authorizer for the application.</p>
+
+ <p>On the line that creates the <code>SimpleService</code>,
+ several things happen.</p>
+
+ <ol>
+ <li>A SimpleService is created and persistently added to the
+ <code>Application</code> instance.</li>
+
+ <li>A SimplePerspective is created, via the overridden
+ <code>getPerspectiveNamed</code> method.</li>
+
+ <li>That <code>SimplePerspective</code> has an
+ <code>Identity</code> generated for it, and persistently
+ added to the <code>Application</code>'s
+ <code>Authorizer</code>. The created identity will have the
+ same name as the perspective ("guest"), and the password
+ supplied (also, "guest"). It will also have a reference to
+ the service "pbecho" and a perspective named "guest", by
+ name. The <code>Perspective.makeIdentity</code> utility
+ method prevents having to deal with the intricacies of the
+ passport <code>Authorizer</code> system when one doesn't
+ require strongly separate <code>Identity</code>s and
+ <code>Perspective</code>s.</li>
+ </ol>
+ <br />
+ <br />
+
+
+ <p>Also, this server does not run itself, but instead persists
+ to a file which can be run with twistd, offering all the usual
+ amenities of daemonization, logging, etc. Once the server is
+ run, connecting to it is similar to the previous example.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def success(message):
+ print "Message received:",message
+ main.shutDown()
+def failure(error):
+ print "Failure...",error
+ main.shutDown()
+def connected(perspective):
+ perspective.echo("hello world",
+ pbcallback=success,
+ pberrback=failure)
+ print "connected."
+pb.connect(connected, failure, "localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 30)
+main.run()
+</pre>
+
+ <h6>Listing 14: Connecting to an Authorized Service</h6>
+ </blockquote>
+ <br />
+ <br />
+
+
+ <p>This introduces a new utility -- <code>pb.connect</code>.
+ This function takes a long list of arguments and manages the
+ handshaking and challenge/response aspects of connecting to a
+ PB service perspective, eventually calling back to indicate
+ either success or failure. In this particular example, we are
+ connecting to localhost on the default PB port (8787),
+ authenticating to the identity "guest" with the password
+ "guest", requesting the perspective "guest" from the service
+ "pbecho". If this can't be done within 30 seconds, the
+ connection will abort.</p>
+
+ <p>In these examples, I've attempted to show how Twisted makes
+ event-based scripting easier; this facilitates the ability to
+ run short scripts as part of a long-running process. However,
+ event-based programming is not natural to procedural scripts;
+ it is more generally accepted that GUI programs will be
+ event-driven whereas scripts will be blocking. An alternative
+ client to our <code>SimpleService</code> using GTK illustrates
+ the seamless meshing of Twisted and GTK.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import main, ingtkernet
+from twisted.spread.ui import gtkutil
+import gtk
+ingtkernet.install()
+class EchoClient:
+ def __init__(self, echoer):
+ l.hide()
+ self.echoer = echoer
+ w = gtk.GtkWindow(gtk.WINDOW_TOPLEVEL)
+ vb = gtk.GtkVBox(); b = gtk.GtkButton("Echo:")
+ self.entry = gtk.GtkEntry(); self.outry = gtk.GtkEntry()
+ w.add(vb)
+ map(vb.add, [b, self.entry, self.outry])
+ b.connect('clicked', self.clicked)
+ w.connect('destroy', gtk.mainquit)
+ w.show_all()
+ def clicked(self, b):
+ txt = self.entry.get_text()
+ self.entry.set_text("")
+ self.echoer.echo(txt, pbcallback=self.outry.set_text)
+l = gtkutil.Login(EchoClient, None, initialService="pbecho")
+l.show_all()
+gtk.mainloop()
+</pre>
+
+ <h6>Listing 15: A Twisted GUI application</h6>
+ </blockquote>
+
+ <h3>Event-Driven Web Object Publishing with Web.Widgets</h3>
+
+ <p>Although PB will be interesting to those people who wish to
+ write custom clients for their networked applications, many
+ prefer or require a web-based front end. Twisted's built-in web
+ server has been designed to accommodate this desire, and the
+ presentation framework that one would use to write such an
+ application is <code>twisted.web.widgets</code>. Web.Widgets
+ has been designed to work in an event-based manner, without
+ adding overhead to the designer or the developer's
+ work-flow.</p>
+
+ <p>Surprisingly, asynchronous web interfaces fit very well into
+ the normal uses of purpose-built web toolkits such as PHP. Any
+ experienced PHP, Zope, or WebWare developer will tell you that
+ <em>separation of presentation, content, and logic</em> is very
+ important. In practice, this results in a "header" block of
+ code which sets up various functions which are called
+ throughout the page, some of which load blocks of content to
+ display. While PHP does not enforce this, it is certainly
+ idiomatic. Zope enforces it to a limited degree, although it
+ still allows control structures and other programmatic elements
+ in the body of the content.</p>
+
+ <p>In Web.Widgets, strict enforcement of this principle
+ coincides very neatly with a "hands-free" event-based
+ integration, where much of the work of declaring callbacks is
+ implicit. A "Presentation" has a very simple structure for
+ evaluating Python expressions and giving them a context to
+ operate in. The "header" block which is common to many
+ templating systems becomes a class, which represents an
+ enumeration of events that the template may generate, each of
+ which may be responded to either immediately or latently.</p>
+
+ <p>For the sake of simplicity, as well as maintaining
+ compatibility for potential document formats other than HTML,
+ Presentation widgets do not attempt to parse their template as
+ HTML tags. The structure of the template is <code>"HTML Text
+ %%%%python_expression()%%%% more HTML Text"</code>. Every set
+ of 4 percent signs (%%%%) switches back and forth between
+ evaluation and printing.</p>
+
+ <p>No control structures are allowed in the template. This was
+ originally thought to be a potentially major inconvenience, but
+ with use of the Web.Widgets code to develop a few small sites,
+ it has seemed trivial to encapsulate any table-formatting code
+ within a method; especially since those methods can take string
+ arguments if there's a need to customize the table's
+ appearance.</p>
+
+ <p>The namespace for evaluating the template expressions is
+ obtained by scanning the class hierarchy for attributes, and
+ getting each of those attributes from the current instance.
+ This means that all methods will be bound methods, so
+ indicating "self" explicitly is not required. While it is
+ possible to override the method for creating namespaces, using
+ this default has the effect of associating all presentation
+ code for a particular widget in one class, along with its
+ template. If one is working with a non-programmer designer, and
+ the template is in an external file, it is always very clear to
+ the designer what functionality is available to them in any
+ given scope, because there is a list of available methods for
+ any given class.</p>
+
+ <p>A convenient event to register for would be a response from
+ the PB service that we just implemented. We can use the
+ <code>Deferred</code> class in order to indicate to the widgets
+ framework that certain work has to be done later. This is a
+ Twisted convention which one can currently use in PB as well as
+ webwidgets; any framework which needs the ability to defer a
+ return value until later should use this facility. Elements of
+ the page will be rendered from top to bottom as data becomes
+ available, so the page will not be blocked on rendering until
+ all deferred elements have been completed.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.python import defer
+from twisted.web import widgets
+class EchoDisplay(widgets.Presentation):
+ template = """&lt;H1&gt;Welcome to my widget, displaying %%%%echotext%%%%.&lt;/h1&gt;
+ &lt;p&gt;Here it is: %%%%getEchoPerspective()%%%%&lt;/p&gt;"""
+ echotext = 'hello web!'
+ def getEchoPerspective(self):
+ d = defer.Deferred()
+ pb.connect(d.callback, d.errback, "localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 1)
+ d.addCallbacks(self.makeListOf, self.formatTraceback)
+ return ['&lt;b&gt;',d,'&lt;/b&gt;']
+ def makeListOf(self, echoer):
+ d = defer.Deferred()
+ echoer.echo(self.echotext, pbcallback=d.callback, pberrback=d.errback)
+ d.addCallbacks(widgets.listify, self.formatTraceback)
+ return [d]
+if __name__ == "__main__":
+ from twisted.web import server
+ from twisted.internet import main
+ a = main.Application("pbweb")
+ gdgt = widgets.Gadget()
+ gdgt.widgets['index'] = EchoDisplay()
+ a.listenOn(8080, server.Site(gdgt))
+ a.run()
+</pre>
+
+ <h6>Listing 16: an event-based web widget.</h6>
+ </blockquote>
+
+ <p>Each time a Deferred is returned as part of the page, the
+ page will pause rendering until the deferred's
+ <code>callback</code> method is invoked. When that callback is
+ made, it is inserted at the point in the page where rendering
+ left off.</p>
+
+ <p>If necessary, there are options within web.widgets to allow
+ a widget to postpone or cease rendering of the entire page --
+ for example, it is possible to write a FileDownload widget,
+ which will override the rendering of the entire page and
+ replace it with a file download.</p>
+
+ <p>The final goal of web.widgets is to provide a framework
+ which encourages the development of usable library code. Too
+ much web-based code is thrown away due to its particular
+ environment requirements or stylistic preconceptions it carries
+ with it. The goal is to combine the fast-and-loose iterative
+ development cycle of PHP with the ease of installation and use
+ of Zope's "Product" plugins.</p>
+
+ <h3>Things That Twisted Does Not Do</h3>
+
+ <p>It is unfortunately well beyond the scope of this paper to
+ cover all the functionality that Twisted provides, but it
+ serves as a good overview. It may seem as though twisted does
+ anything and everything, but there are certain features we
+ never plan to implement because they are simply outside the
+ scope of the project.</p>
+
+ <p>Despite the multiple ways to publish and access objects,
+ Twisted does not have or support an interface definition
+ language. Some developers on the Twisted project have
+ experience with remote object interfaces that require explicit
+ specification of all datatypes during the design of an object's
+ interface. We feel that such interfaces are in the spirit of
+ statically-typed languages, and are therefore suited to the
+ domain of problems where statically-typed languages excel.
+ Twisted has no plans to implement a protocol schema or static
+ type-checking mechanism, as the efficiency gained by such an
+ approach would be quickly lost again by requiring the type
+ conversion between Python's dynamic types and the protocol's
+ static ones. Since one of the key advantages of Python is its
+ extremely flexible dynamic type system, we felt that a
+ dynamically typed approach to protocol design would share some
+ of those advantages.</p>
+
+ <p>Twisted does not assume that all data is stored in a
+ relational database, or even an efficient object database.
+ Currently, Twisted's configuration state is all stored in
+ memory at run-time, and the persistent parts of it are pickled
+ at one go. There are no plans to move the configuration objects
+ into a "real" database, as we feel it is easier to keep a naive
+ form of persistence for the default case and let
+ application-specific persistence mechanisms handle persistence.
+ Consequently, there is no object-relational mapping in Twisted;
+ <code>twisted.enterprise</code> is an interface to the
+ relational paradigm, not an object-oriented layer over it.</p>
+
+ <p>There are other things that Twisted will not do as well, but
+ these have been frequently discussed as possibilities for it.
+ The general rule of thumb is that if something will increase
+ the required installation overhead, then Twisted will probably
+ not do it. Optional additions that enhance integration with
+ external systems are always welcome: for example, database
+ drivers for Twisted or a CORBA IDL for PB objects.</p>
+
+ <h3>Future Directions</h3>
+
+ <p>Twisted is still a work in progress. The number of protocols
+ in the world is infinite for all practical purposes, and it
+ would be nice to have a central repository of event-based
+ protocol implementations. Better integration with frameworks
+ and operating systems is also a goal. Examples for integration
+ opportunities are automatic creation of installer for "tap"
+ files (for Red Hat Packager-based distributions, FreeBSD's
+ package management system or Microsoft Windows(tm) installers),
+ and integration with other event-dispatch mechanisms, such as
+ win32's native message dispatch.</p>
+
+ <p>A still-nascent feature of Twisted, which this paper only
+ touches briefly upon, is <code>twisted.enterprise</code>: it is
+ planned that Twisted will have first-class database support
+ some time in the near future. In particular, integration
+ between twisted.web and twisted.enterprise to allow developers
+ to have SQL conveniences that they are used to from other
+ frameworks.</p>
+
+ <p>Another direction that we hope Twisted will progress in is
+ standardization and porting of PB as a messaging protocol. Some
+ progress has already been made in that direction, with XEmacs
+ integration nearly ready for release as of this writing.</p>
+
+ <p>Tighter integration of protocols is also a future goal, such
+ an FTP server that can serve the same resources as a web
+ server, or a web server that allows users to change their POP3
+ password. While Twisted is already a very tightly integrated
+ framework, there is always room for more integration. Of
+ course, all this should be done in a flexible way, so the
+ end-user will choose which components to use -- and have those
+ components work well together.</p>
+
+ <h3>Conclusions</h3>
+
+ <p>As shown, Twisted provides a lot of functionality to the
+ Python network programmer, while trying to be in his way as
+ little as possible. Twisted gives good tools for both someone
+ trying to implement a new protocol, or someone trying to use an
+ existing protocol. Twisted allows developers to prototype and
+ develop object communication models with PB, without designing
+ a byte-level protocol. Twisted tries to have an easy way to
+ record useful deployment options, via the
+ <code>twisted.tap</code> and plugin mechanisms, while making it
+ easy to generate new forms of deployment. And last but not
+ least, even Twisted is written in a high-level language and
+ uses its dynamic facilities to give an easy API, it has
+ performance which is good enough for most situations -- for
+ example, the web server can easily saturate a T1 line serving
+ dynamic requests on low-end machines.</p>
+
+ <p>While still an active project, Twisted can already used for
+ production programs. Twisted can be downloaded from the main
+ Twisted site (http://www.twistedmatrix.com) where there is also
+ documentation for using and programming Twisted.</p>
+
+ <h3>Acknowledgements</h3>
+
+ <p>We wish to thank Sean Riley, Allen Short, Chris Armstrong,
+ Paul Swartz, J&uuml;rgen Hermann, Benjamin Bruheim, Travis B.
+ Hartwell, and Itamar Shtull-Trauring for being a part of the
+ Twisted development team with us.</p>
+
+ <p>Thanks also to Jason Asbahr, Tommi Virtanen, Gavin Cooper,
+ Erno Kuusela, Nick Moffit, Jeremy Fincher, Jerry Hebert, Keith
+ Zaback, Matthew Walker, and Dan Moniz, for providing insight,
+ commentary, bandwidth, crazy ideas, and bug-fixes (in no
+ particular order) to the Twisted team.</p>
+
+ <h3>References</h3>
+
+ <ol>
+ <li>The Twisted site, http://www.twistedmatrix.com</li>
+
+ <li>Douglas Schmidt, Michael Stal, Hans Rohnert and Frank
+ Buschmann, Pattern-Oriented Software Architecture, Volume 2,
+ Patterns for Concurrent and Networked Objects, John Wiley
+ &amp; Sons</li>
+
+ <li>Abhishek Chandra, David Mosberger, Scalability of Linux
+ Event-Dispatch Mechanisms, USENIX 2001,
+ http://lass.cs.umass.edu/~abhishek/papers/usenix01/paper.ps</li>
+
+ <li>Protocol specifications, http://www.rfc-editor.com</li>
+
+ <li>The Twisted Philosophical FAQ,
+ http://www.twistedmatrix.com/page.epy/twistedphil.html</li>
+
+ <li>Twisted Advocacy,
+ http://www.twistedmatrix.com/page.epy/whytwisted.html</li>
+
+ <li>Medusa, http://www.nightmare.com/medusa/index.html</li>
+
+ <li>Using Spreadable Web Servers,
+ http://www.twistedmatrix.com/users/jh.twistd/python/moin.cgi/TwistedWeb</li>
+
+ <li>Twisted Spread implementations for other languages,
+ http://www.twistedmatrix.com/users/washort/</li>
+
+ <li>PHP: Hypertext Preprocessor, http://www.php.net/</li>
+
+ <li>The Z Object Publishing Environment,
+ http://www.zope.org/, http://zope.com/</li>
+ </ol>
+ </body>
+</html>
+
diff --git a/doc/historic/2003/europython/doanddont.html b/doc/historic/2003/europython/doanddont.html
new file mode 100644
index 0000000..79b0072
--- /dev/null
+++ b/doc/historic/2003/europython/doanddont.html
@@ -0,0 +1,508 @@
+<html><head><title>Idioms and Anti-Idioms in Python</title></head><body>
+
+<h1>Idioms and Anti-Idioms in Python</h1>
+
+<h2>Idioms and Anti-Idioms, AKA Do and Don't</h2><ul>
+<li>Welcome</li>
+
+<li>Gimmick -- Charmed quotes</li>
+
+</ul>
+<hr />
+<em>Prue (Something Wicca This Way Comes, season 1) -- No, we are not supposed to use our powers</em>
+<h2>Python</h2><ul>
+<li>Few gotchas...</li>
+
+<li>...but not zero</li>
+
+<li>Most are easy to avoid...</li>
+
+<li>...if you know about them.</li>
+
+</ul>
+<hr />
+<em>Prue (Something Wicca This Way Comes, season 1) -- Uh, it doesn't work out there either.</em>
+<h2>Exceptions</h2><ul>
+<li>Primary method of dealing with errors</li>
+
+<li>Flexible</li>
+
+<li>Good opportunity to shoot yourself in foot...</li>
+
+<li>...without knowing about it (bug only shows up rarely.)</li>
+
+</ul>
+<hr />
+<em>Leo (Paige From the Past, season 4) -- You have to [...] Paige. No exceptions.</em>
+<h2>Exceptions -- Catching Too Much</h2><ul>
+<li>Classical case: 'except:'</li>
+
+<li>Will catch anything</li>
+
+<li>Including most bugs...<ul><li>NameError, AttributeError...</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Piper (Charmed Again, season 4) -- Okay, well this is way too much for me to handle. </em>
+<h2>Exceptions -- Catching Too Much -- Example</h2>
+<pre class="python">
+try:
+ f = opne("file")
+except:
+ sys.exit("no such file")
+</pre>
+
+<hr />
+<em>Piper (Charmed Again, season 4) -- Way too much.</em>
+<h2>Exceptions -- Catching Too Soon</h2><ul>
+<li>The slogan:<ul><li>Don't catch errors you can do nothing about</li>
+</ul></li>
+
+<li>Catching exceptions should by the 'user'</li>
+
+<li>The point where the value is *used*<ul><li>Rather than passed on</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Phoebe (Knight to Remember, season 4) -- Maybe it's just too soon.</em>
+<h2>Exceptions -- Catching Too Soon -- Example</h2>
+<pre class="python">
+def readlinesfromfile(file):
+ try:
+ return open(file).readlines()
+ except IOError:
+ pass # do what?
+</pre>
+<ul><li>What can we do?<ul><li>Return empty list? bad</li>
+
+<li>Print warning? what if it's one of several possibilities</li>
+
+<li>Exit? NO!</li>
+
+<li>Raise our own exception? Losing information</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Paige (Knight to Remember, season 4) -- I'm already a little late.</em>
+<h2>Catching multiple exceptions</h2><ul>
+<li>Spot bug here:</li></ul>
+
+<pre class="python">
+try:
+ fp = open("file")
+except IOError, OSError:
+ print "could not open file"
+</pre>
+<hr />
+<em>Paige (Knight to Remember, season 4) -- Because I've got too many responsibilities</em>
+<h2>Catching multiple exceptions (cont'd)</h2><ul>
+<li>Bug:</li>
+
+<li>Only IOError gets caught...</li>
+
+<li>and exception value is put in OSError</li>
+
+<li>But most exceptions are IOError :(</li>
+
+<li>Likely to not discover this bug</li>
+
+</ul>
+<hr />
+<em>Piper (Knight to Remember, season 4) -- Alright! Calm down!</em>
+<h2>Catching multiple exceptions (cont'd 2)</h2><ul>
+<li>Correct way</li></ul>
+
+<pre class="python">
+try:
+ fp = open("file")
+except (IOError, OSError):
+ print "could not open file"
+</pre>
+<hr />
+<em>Phoebe (Knight to Remember, season 4) -- Besides that, maybe we can help</em>
+<h2>Catching NameError</h2>
+
+<pre class="python">
+try:
+ import foo
+except ImportError:
+ pass
+
+try:
+ foo.Function()
+except NameError:
+ pass # some replacement
+</pre>
+<ul><li>Bad idea!</li>
+
+</ul>
+<hr />
+<em>Piper (Morality Bites, season 4) -- That's OK, I forgot your name too.</em>
+<h2>Catching NameError (cont'd)</h2>
+
+<pre class="python">
+try:
+ import foo
+except ImportError:
+ foo = None
+
+if foo is not None:
+ foo.Function()
+else:
+ pass # some replacement
+</pre>
+<ul><li>If foo.Function() sometimes has a NameError, we won't mask it...</li>
+
+<li>...or if we misspell 'foo'</li>
+
+</ul>
+<hr />
+<em>Anne (Morality Bites, season 4) -- Oh, right, sorry.</em>
+<h2>Importing Modules -- A Review</h2><ul>
+<li>'import module'</li>
+
+<li>'from module import name1, name2'</li>
+
+<li>Only imports once (or does it?)</li>
+
+</ul>
+<hr />
+<em>Phoebe (Animal Pragmatism, season 2) -- Rome was not built in a day,</em>
+<h2>Importing __main__</h2><ul>
+<li>__main__ is where the 'script' is executed</li>
+
+<li>Avoid the temptation to import __main__</li>
+
+<li>Put common function in a named module</li>
+
+<li>Then your code will be more useful</li>
+
+</ul>
+<hr />
+<em>Piper (Animal Pragmatism, season 2) -- And why mess with a good thing?</em>
+<h2>Importing a File Twice</h2><ul>
+<li>But it can't be, can it?</li>
+
+<li>Importing a script into itself</li></ul>
+
+<pre class="python">
+# file: hello.py
+import hello
+class Foo: pass
+</pre>
+<ul><li>Two 'Foo's, same definition, different class!</li>
+
+<li>If your sys.path includes packages...</li>
+
+<li>...you can import a module once from a package and once plain</li>
+
+</ul>
+<hr />
+<em>Phoebe (Which Prue Is It, Anyway?, season 1) -- Okay, which one of you is the real Prue?</em>
+<h2>Importing *</h2><ul>
+<li>Don't do it<ul><li>Don't do it</li>
+</ul></li>
+
+<li>Classic mistake:</li></ul>
+
+<pre class="python">
+from os import *
+
+fp = open("file") # works
+fp.readline() # fails with a weird error...?
+</pre>
+<ul><li>os.open returns a file descriptor (number)</li>
+
+</ul>
+<hr />
+<em>Pink Prue (Which Prue Is It, Anyway?, season 1) -- So, um, what did I do now?</em>
+<h2>Importing * Inside Functions</h2><ul>
+<li>Just invalid Python...</li>
+
+<li>...but happens to work in 1.5.2...</li>
+
+<li>...and sometimes in 2.1...</li>
+
+<li>...never in 2.2.</li>
+
+<li>Just Say No</li>
+
+</ul>
+<hr />
+<em>Pink Prue (Which Prue Is It, Anyway?, season 1) -- What ever it is, I have an alibi.</em>
+<h2>Importing Names</h2><ul>
+<li>from foo import name1, name2</li>
+
+<li>Not a bad idea always</li>
+
+<li>But be careful of repercussions:</li>
+
+<li>modules sometimes change things inside</li>
+
+<li>You won't see those changes</li>
+
+<li>Opportunity for inconsistency!</li>
+
+</ul>
+<hr />
+<em>Real Prue (Which Prue Is It, Anyway?, season 1) -- Because I still have to work here when all of this is over.</em>
+<h2>Reloading</h2><ul>
+<li>reload(module) -- reread module from file</li>
+
+<li>Useful in long running processes?</li>
+
+<li>Doesn't play nice with 'from import name'</li>
+
+<li>Beware of exceptions: the new classes are different from old classes</li>
+
+</ul>
+<hr />
+<em>Real Prue (Which Prue Is It, Anyway?, season 1) -- Don't worry I'm never casting that spell again.</em>
+<h2>exec, execfile and eval</h2><ul>
+<li>Execute arbitrary Python code</li>
+
+<li>No-cost scripting language for applications</li>
+
+<li>But easy to shoot one's self in the foot</li>
+
+</ul>
+<hr />
+<em>Reporter (Morality Bites, season 2) -- More news on the execution of Phoebe Halliwell coming up.</em>
+<h2>exec, execfile and eval -- Modify namespaces</h2><ul>
+<li>They modify the namespace they're in<ul><li>...but not always.</li>
+</ul></li>
+
+<li>Depends on global vs. inside functions</li>
+
+<li>Use with care -- or with explicit dictionaries</li>
+
+</ul>
+<hr />
+<em>Nathaniel (Morality Bites, season 2) -- Executions are a bitch to plan.</em>
+<h2>exec, execfile and eval -- Inside functions</h2><ul>
+<li>Unadorned exec is invalid inside functions</li>
+
+<li>execfile and eval play badly with local var. optimisation</li>
+
+<li>Always use with explicit dictionary</li>
+
+</ul>
+<hr />
+<em>Nathaniel (Morality Bites, season 2) -- Phoebe, what is this? An attempt to stay your execution?</em>
+<h2>Conclusion: recommended usage</h2><ul>
+<li>d={};exec "code" in d</li>
+
+<li>d={};execfile("file", d)</li>
+
+<li>d={};eval("expression", d)</li>
+
+<li>Sometimes useful to pre-populate dictionary</li>
+
+</ul>
+<hr />
+<em>Phoebe (Morality Bites, season 2) -- Just because you don't understand something, doesn't make it evil.</em>
+<h2>exec, execfile and eval -- Restricted Execution (Don't)</h2><ul>
+<li>rexec never was audited</li>
+
+<li>History of holes</li>
+
+<li>Dangerous to allow arbitrary code</li>
+
+<li>DoS attacks not defended against at all</li>
+
+<li>Recursion</li>
+
+</ul>
+<hr />
+<em>Leo (Morality Bites, season 2) -- Nobody's gonna rescue you.</em>
+<h2>Syntax</h2><ul>
+<li>Python syntax regular and nice...</li>
+
+<li>...but not perfect.</li>
+
+<li>Some care needed.</li>
+
+</ul>
+<hr />
+<em>Prue (Morality Bites, season 2) -- You know, we can still make the good things happen.</em>
+<h2>Syntax -- Tabs and Spaces</h2><ul>
+<li>Use Tabs</li>
+
+<li>Or use spaces</li>
+
+<li>But don't mix them...</li>
+
+<li>...ever!</li>
+
+<li>Invites bugs</li>
+
+</ul>
+<hr />
+<em>Prue (The Painted World, season 2) -- We've seen so many bizarre things.</em>
+<h2>Syntax -- Backslash Continuations</h2>
+
+<pre class="python">
+# Extra newline
+r = 1 \
+
++2
+
+# Missing backslash in long series
+r = 1 \
++2 \
++3 \
++4
++5 \
++6
+</pre>
+<ul><li>Both *silently* do the wrong things</li></ul>
+
+<h2>Syntax -- Backslash Continuations (cont'd)</h2>
+
+<ul><li>Better</li></ul>
+
+<pre class="python">
+# Extra newline
+r = (1
+
++2)
+
+# Long series
+r = (1
++2
++3
++4
++5
++6)
+</pre>
+
+<hr />
+<em>Prue (The Painted World, season 2) -- Uh, what just happened here?</em>
+<h2>Hand Hacking Batteries</h2><ul>
+<li>Don't write os.path functions yourself<ul><li>os.path.join especially</li>
+</ul></li>
+
+<li>min, max</li>
+
+<li>urlparse</li>
+
+<li>Skim through modules list. A lot.</li>
+
+</ul>
+<hr />
+<em>Prue (Animal Pragmatism, season 2) -- Well, we didn't find anything in the Book Of Shadows.</em>
+<h2>Further Reading</h2><ul>
+<li><a href="http://aspn.activestate.com/ASPN/Python/Reference/Products/ActivePython/howtos/doanddont/doanddont.html">http://aspn.activestate.com/ASPN/Python/Reference/Products/ActivePython/howtos/doanddont/doanddont.html</a></li>
+
+<li><a href="http://www.amk.ca/python/writing/warts.html">http://www.amk.ca/python/writing/warts.html</a></li>
+
+<li><a href="http://www.python.org/doc/essays/styleguide.html">http://www.python.org/doc/essays/styleguide.html</a></li>
+
+</ul>
+<hr />
+<em>Phoebe (The Painted World, season 2) -- I think you'll find me pretty knowledgeable about all areas</em>
+<h2>Questions?</h2>
+<em>Piper (The Painted World, season 2) -- You're like ask rainman.com</em>
+
+<h2>Bonus Slides</h2>
+<em>Phoebe (The Painted World, season 2) -- Oh, and P.S. there will be no personal gain.</em>
+
+<h2>Packages and __init__.py</h2><ul>
+<li>Packages are determined by __init__.py files</li>
+
+<li>Temptation to put code in __init__.py</li>
+
+<li>But two namespaces mix: __init__'s and filesystem's</li>
+
+<li>Put comments, docstring and __all__</li>
+
+</ul>
+<hr />
+<em>Piper (Animal Pragmatism, season 2) -- It's a package. One I would like to share with you.</em>
+<h2>Type Checking</h2><ul>
+<li>Python's typing is highly dynamic</li>
+
+<li>Capability-based, not class-based</li>
+
+<li>Explicit type checks hurt code usefulness</li>
+
+<li>(common use -- proxies, for testing)</li>
+
+</ul>
+<hr />
+<em>Phoebe (Black as Cole, season 2) -- I never thought of myself as the marrying type</em>
+<h2>Type Checking -- Example</h2><ul>
+<li>Here's what not to do:</li></ul>
+
+<pre class="python">
+class Foo:
+ def __init__(self, i):
+ if type(i) is types.StringType:
+ self.content = open(i).readlines()
+ elif type(i) is types.ListType:
+ self.content = i
+</pre>
+<ul><li>(inspired from a question on #python)</li>
+
+<li>More badness than you can shake a stick at.</li>
+
+</ul>
+<hr />
+<em>Phoebe (Muse to My Ears, season 4) -- You're an artistic, creative type.</em>
+<h2>Type Checking -- Example -- Fixed</h2>
+<pre class="python">
+
+class Foo:
+ pass
+
+class FooFromFile(Foo):
+
+ def __init__(self, filename):
+ self.content = open(filename).readlines()
+
+class FooFromList(Foo):
+
+ def __init__(self, list):
+ self.content = list
+</pre>
+
+<hr />
+<em>Phoebe (Muse to My Ears, season 4) -- You see how well this worked out?</em>
+<h2>Private __Attributes</h2><ul>
+<li>Useful in deep hierarchies to keep attributes separate</li>
+
+<li>Mangle only class name -- *not* module name</li>
+
+<li>Makes it harder to test</li>
+
+<li>Makes it harder to hand-hack for debugging</li>
+
+</ul>
+<hr />
+<em>Tessa (Animal Pragmatism, season 2) -- Maybe it's our fault because we tried to make them into something they're not.</em>
+
+<h2>Using Mutable Default Arguments</h2>
+
+<pre class="python">
+def foo(l=[]):
+ l.append(5);return l
+</pre>
+
+<ul>
+<li>Will modify the same list.</li>
+<li>If you want that -- use object, class, not that hack.</li>
+</ul>
+
+<pre class="python">
+def foo(l=None):
+ if l is None: l=[]
+ l.append(5);return l
+</pre>
+<hr />
+<em>Snake guy (Animal Pragmatism, season 2) --
+You two are acting like nothing's changed.</em>
+
+</body></html>
diff --git a/doc/historic/2003/europython/index.html b/doc/historic/2003/europython/index.html
new file mode 100644
index 0000000..051fb6d
--- /dev/null
+++ b/doc/historic/2003/europython/index.html
@@ -0,0 +1,35 @@
+<html><head><title>Moshe's Talks</title></head><body>
+
+<h1>Moshe's talks</h1>
+
+<h2>Slides</h2>
+
+<ul>
+<li><a href="webclients-1.html">Writing Web Clients</a></li>
+<li><a href="doanddont-1.html">Idioms and Anti-Idioms</a></li>
+<li><a href="twisted-1.html">Introduction to Twisted</a></li>
+<li><a href="tw-deploy-1.html">Configuring and Deploying Twisted Web</a></li>
+<li><a href="lore-1.html">Using Lore</a></li>
+</ul>
+
+<h2>HTML</h2>
+
+<ul>
+<li><a href="webclients.html">Writing Web Clients</a></li>
+<li><a href="doanddont.html">Idioms and Anti-Idioms</a></li>
+<li><a href="twisted.html">Introduction to Twisted</a></li>
+<li><a href="tw-deploy.html">Configuring and Deploying Twisted Web</a></li>
+<li><a href="lore.html">Using Lore</a></li>
+</ul>
+
+<h2>PDF</h2>
+
+<ul>
+<li><a href="webclients.pdf">Writing Web Clients</a></li>
+<li><a href="doanddont.pdf">Idioms and Anti-Idioms</a></li>
+<li><a href="twisted.pdf">Introduction to Twisted</a></li>
+<li><a href="tw-deploy.pdf">Configuring and Deploying Twisted Web</a></li>
+<li><a href="lore.pdf">Using Lore</a></li>
+</ul>
+
+</body></html>
diff --git a/doc/historic/2003/europython/lore.html b/doc/historic/2003/europython/lore.html
new file mode 100644
index 0000000..edb33cc
--- /dev/null
+++ b/doc/historic/2003/europython/lore.html
@@ -0,0 +1,502 @@
+<html><head><title>Lore</title></head><body>
+
+<h1>Lore</h1>
+<h2>Lore - A Document Generation System</h2><ul>
+<li>Gimmick -- Gilmore girls quotes</li>
+
+<li>Goal - take something which is easy to write, transforms to something easy to read</li>
+
+<li>For correct definitions of 'easy', of course</li>
+
+</ul>
+<hr />
+<em>Rory (Concert Interruptus, season 1) -- Yeah, well I've always thought easy is completely overrated.</em>
+<h2>Source Format</h2><ul>
+<li>Subset of XHTML 1.0<ul><li>Except for some new attributes</li>
+
+<li>Shouldn't bother browsers</li>
+</ul></li>
+
+<li>Slanted towards logical markup</li>
+
+</ul>
+<hr />
+<em>Alex (I Solemnly Swear, season 3) -- That would've been far too logical.</em>
+
+<h2>Output Formats</h2><ul>
+<li>Screen and paper<ul><li>Screen - 'fancy HTML'</li>
+
+<li>Paper - LaTeX<ul><li>Use LaTeX to produce PDF or PostScript</li>
+</ul></li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Madelaine (The Lorelais' First Day at Chilton, season 1) -- You don't know she's going out for the paper.</em>
+<h2>Minimal Lore Document</h2>
+<pre>
+&lt;html&gt;
+&lt;head&gt;&lt;title&gt;Title&lt;/title&gt;&lt;/head&gt;
+&lt;body&gt;&lt;h1&gt;Title&lt;/h1&gt;&lt;/body&gt;
+&lt;/html&gt;</pre>
+
+<hr />
+<em>Luke (There's the Rub, season 2) -- You said minimal</em>
+<h2>Minimal Lore Document Explained</h2><ul>
+<li>title element in head -- a must</li>
+
+<li>h1 element in head -- a must<ul><li>Must have same content</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Tom (There's the Rub, season 2) -- Hey, this is minimal</em>
+<h2>External Listings</h2><ul>
+<li>Advantage -- no need to quote</li>
+
+<li>Advantage -- test your examples</li>
+
+<li>Example:<ul><li><code>&lt;a class="python-listing" href="/usr/lib/python2.2/os.py"&gt;os.py&lt;/a&gt;</code>
+</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Kirk (Red Light on the Wedding Night, season 2) -- I include it as an example of the excellence I aspire to.</em>
+<h2>Using Lore to Generate HTML</h2><ul>
+<li>Write template</li>
+
+<li>[optional] Write stylesheet</li>
+
+<li>Run lore</li>
+
+</ul>
+<hr />
+<em>Paris (Run Away, Little Boy, season 2) -- I went on the web and found this site</em>
+<h2>Generating LaTeX</h2><ul>
+<li>lore -olatex file.html --&gt; produces file.tex</li>
+
+<li>Default is to create an 'article'</li>
+
+<li>Creating PostScript<ul><li>latex file.tex</li>
+
+<li>latex file.tex</li>
+
+<li>dvips -o file.ps file.dvi</li>
+</ul></li>
+
+<li>Creating PDF<li>latex file.tex</li>
+<li>pdflatex file.tex</li>
+</li>
+
+</ul>
+<hr />
+<em>Rory (Christopher Returns, season 1) -- He had already printed like a million</em>
+<h2>Using Lint</h2><ul>
+<li>lore -olint doc/howto/*.html</li>
+
+<li>lore -n -olint doc/howto/*.html #no output except warnings</li>
+
+</ul>
+<hr />
+<em>Max (The Deer-Hunters, season 1) -- I know a D seems pretty dismal</em>
+<h2>Further Reading</h2><ul>
+<li>Man page -- doc/man/lore.xhtml</li>
+
+<li>Howto -- doc/howto/lore.xhtml</li>
+
+<li>Extending howto -- doc/howto/extending-lore.xhtml</li>
+
+<li>Documentation standard -- doc/howto/doc-standard.xhtml</li>
+
+<li>Lore paper -- doc/historic/2003/pycon/lore/lore.html</li>
+
+</ul>
+<hr />
+<em>Paris (The Bracebridge Dinner, season 2) -- Rereading the Iliad a third time is not not doing anything</em>
+<h2>Questions?</h2>
+<em>Lorelai (Forgiveness and Stuff, season 1) -- A person needs details.</em>
+<h2>Bonus Slides</h2>
+<em>Miss James (The Lorelais' First Day at Chilton, season 1) -- If you do it in Latin you get extra credit.</em>
+<h2>Lore Alternatives - LaTeX</h2><ul>
+<li>Very good at printed results</li>
+
+<li>Model makes alternative parsers near-impossible</li>
+
+<li>Renderers to HTML are buggy and fragile</li>
+
+<li>People find it hard to use</li>
+
+</ul>
+<hr />
+<em>Michel (Love, Daisies and Troubadors, season 1) -- It increases my ennui</em>
+<h2>Lore Alternatives - HTML</h2><ul>
+<li>Too flexible</li>
+
+<li>No support for needed idioms<ul><li>Special-purpose Python markup</li>
+
+<li>Tables of contents</li>
+
+<li>Inlining</li>
+
+<li>Footnotes</li>
+</ul></li>
+
+<li>Renders badly to dead trees with current tools</li>
+
+</ul>
+<hr />
+<em>Lorelai (Love, Daisies and Troubadors, season 1) -- It was broken [...] I'm not crazy</em>
+<h2>Lore Alternatives - Docbook</h2><ul>
+<li>Using correctly requires too much work<ul><li>Write a DTD with special elements</li>
+
+<li>Write Jade stylesheets</li>
+</ul></li>
+
+<li>Lore is probably smaller than docbook specialisation</li>
+
+<li>People find it hard to use</li>
+
+</ul>
+<hr />
+<em>Rory (Hammers and Veils, season 2) -- What do you want me to do it?</em>
+<h2>Lore Alternatives - Texinfo</h2><ul>
+<li>Next slide, please</li>
+
+</ul>
+<hr />
+<em>Man (Hammers and Veils, season 2) -- There's a ton of hurt that almost happened here.</em>
+<h2>Lore Alternatives - reST</h2><ul>
+<li>Completely new language (no editor support)</li>
+
+<li>Hard to add new tags</li>
+
+<li>No linter</li>
+
+</ul>
+<hr />
+<em>Emily (Hammers and Veils, season 2) -- And this is what we need to discuss right now?</em>
+<h2>Lore Alternatives - LyX</h2><ul>
+<li>Dependent on GUI</li>
+
+</ul>
+<hr />
+<em>Rory (Hammers and Veils, season 2) -- Well, it's just dressed up a little.</em>
+<h2>Some Standard Tags -- XHTML Primer</h2><ul>
+<li><code>&lt;p&gt;paragraph&lt;/p&gt;</code>
+</li>
+
+<li><code>&lt;em&gt;emphasis&lt;/em&gt;</code>
+</li>
+
+<li><code>&lt;strong&gt;strong emphasis&lt;/strong&gt;</code>
+</li>
+
+<li>Headers<ul><li>&lt;h2&gt;sectionheader&lt;/h2&gt;</li><li>&lt;h3&gt;subsection&lt;/h3&gt;</li></ul></li>
+
+<li>Lists<ul><li>&lt;ol&gt;&lt;li&gt;ordered list item&lt;/li&gt;&lt;/ol&gt;</li><li>&lt;ul&gt;&lt;li&gt;unordered list item&lt;/li&gt;&lt;/ul&gt;</li></ul></li>
+
+<li><code>&lt;img src="http://example.com/img.png" /&gt;</code>
+<ul><li>Note '/' at end!</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Rory (Kiss and Tell, season 1) -- See, even a little information in your hands is dangerous.</em>
+<h2>More HTML</h2><ul>
+<li>Indicating authorship -- &lt;link rel="author" href="author@example.com" title="Author Name" /&gt;</li>
+
+<li>Put in &lt;head&gt;</li>
+
+<li>sub/sup -- subscripts, superscripts</li>
+
+</ul>
+<hr />
+<em>Max (The Lorelais' First Day at Chilton, season 1) -- Tolstoy's favourite author, for instance, was...</em>
+<h2>More HTML -- cross references</h2><ul>
+<li>Label<ul><code>&lt;a name="label-name" /&gt;</code>
+</ul></li>
+
+<li>Reference in file<ul><li><code>&lt;a href="#label-name"&gt;reference text&lt;/a&gt;</code></li>
+</ul></li>
+
+<li>Reference in other file<ul><li><code>&lt;a href="file-name#label-name"&gt;reference text&lt;/a&gt;</code></li>
+</ul></li>
+
+<li>Refer to URL<ul><li><code>&lt;a href="http://example.com"&gt;reference text&lt;/a&gt;</code></li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Christopher (Christopher Returns, season 1) -- It's just a weird reference.</em>
+<h2>Special Markup</h2><ul>
+<li>Things not in XHTML are done with div/span classes</li>
+
+<li>&lt;div class="note"&gt;note&lt;/a&gt; -- notes</li>
+
+<li>&lt;div class="doit"&gt;doit&lt;/a&gt; -- something not implemented</li>
+
+<li>&lt;span class="footnote"&gt;footnote&lt;/a&gt; -- put in a footnote</li>
+
+</ul>
+<hr />
+<em>Taylor (Take The Deviled Eggs, season 3) -- Out attention spans are gnat-like tonight</em>
+<h2>API References</h2><ul>
+<li><code>&lt;code class="API"&gt;urllib&lt;/code&gt;</code>
+</li>
+
+<li><code>&lt;code base="urllib" class="API"&gt;urlencode&lt;/code&gt;</code>
+</li>
+
+<li><code>&lt;code base="twisted" class="API"&gt;copyright.version&lt;/code&gt;</code>
+</li>
+
+</ul>
+<hr />
+<em>Lorelai (The Road Trip To Harvard, season 2) -- We're just kinda hanging out between classes</em>
+<h2>API References Explained</h2><ul>
+<li>Integrate with systems for docstring generation</li>
+
+<li>Generate links to auto-generated docs</li>
+
+</ul>
+<hr />
+<em>Luke (Love and War and Snow, season 1) -- How do you know? Do you have written documentation?</em>
+<h2>Inline Listings</h2><ul>
+<li>Use &lt;pre&gt;</li>
+
+<li>Possible classes: python, shell, python-interpreter</li>
+
+<li>Example:</li>
+
+</ul>
+<pre>
+&lt;pre class="python"&gt;
+def foo():
+ return forbnicate(4)
+&lt;/pre&gt;</pre>
+<hr />
+<em>Taylor (Take The Deviled Eggs, season 3) -- That's not even English.</em>
+<h2>Inline Listings -- short</h2><ul>
+<li>Use &lt;code&gt;</li>
+
+<li>Possible classes: python, shell, py-signature</li>
+
+<li>...and more</li>
+
+</ul>
+<hr />
+<em>Rory (Double Date, season 1) -- It's like this weird code thing with her.</em>
+<h2>Generating HTML -- writing templates</h2><ul>
+<li>Templates are XHTML documents</li>
+
+<li>Put in reference to stylesheet -- head is mostly kept as is</li>
+
+<li>Title will be prepended to document's title</li>
+
+<li>&lt;div class="toc" /&gt; will be replaced by table of contents</li>
+
+<li>&lt;div class="body" /&gt; will be replaced by processed body</li>
+
+</ul>
+<hr />
+<em>Paris (I Can't Get Started, season 2) -- How's this sound for a template?</em>
+<h2>Generating HTML -- using commandline</h2><ul>
+<li>Full details: the lore manpage</li>
+
+<li>Basic format: lore file.html --&gt; outputs file.xhtml<ul><li>--config template=template.tpl to use different template</li>
+
+<li>--config baseurl=format-string for the url of the auto-generated docstring docs</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Richard (The Third Lorelai, season 1) -- Your wish is my command.</em>
+<h2>Generating HTML -- using commandline -- examples</h2><ul>
+<li>lore --config template=strange.tpl foo.html</li>
+
+<li>lore --docsdir doc/howto/</li>
+
+<li>lore -p --docsdir doc/howto/ # use plain progress</li>
+
+</ul>
+<hr />
+<em>Jackson (A Deep-Fried Korean Thanksgiving, season 3) -- Deep-fried cake!</em>
+<h2>Generating HTML -- using commandline -- examples (cont'd)</h2><ul>
+<li>lore --docsdir doc/howto/ --config baseurl=../api/%s.html</li>
+
+<li>lore --ext='' foo.html # produce 'foo' as output</li>
+
+</ul>
+<hr />
+<em>Jackson (A Deep-Fried Korean Thanksgiving, season 3) -- Deep-fried shoe!</em>
+<h2>Generating HTML -- notes about stylesheets</h2><ul>
+<li>Many 'class's in the output</li>
+
+<li>The stylesheet Twisted uses can be used as example<ul><li>Especially the .py-src-* classes: used for syntax highlighting</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Miss Patty (Cinnamon's Wake, season 1) -- If you had a better hair style I might consider dating</em>
+<h2>Generating LaTeX -- examples</h2><ul>
+<li>lore -olatex --config section file.html</li>
+
+<li>lore -olatex --config book file.html</li>
+
+<li>lore -olatex --config section --docsdir doc/howto/</li>
+
+</ul>
+<hr />
+<em>Luke (Hammers and Veils, season 2) -- Just an example</em>
+<h2>Using Lint -- notes</h2><ul>
+<li>If there is an element which lint gives a warning you disagree with: &lt;element hlint="off"&gt;mistake&lt;/element&gt;</li>
+
+<li>But usually the linter is right</li>
+
+<li>lint exits with non-zero status iff some document was not clean -- useful in shell scripts</li>
+
+</ul>
+<hr />
+<em>Paris (The Deer-Hunters, season 1) -- That would be cause for concern.</em>
+<h2>Understanding Lint Warnings</h2><ul>
+<li>Format: file:line:column:warning</li>
+
+<li>Line/column always point to start/end of element</li>
+
+<li>Some justifications:<ul><li>&lt;pre&gt; with &gt;80 characters/line renders badly, both in HTML and in LaTeX</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Jess (Teach Me Tonight, season 2) -- I appreciate the warning.</em>
+<h2>Using Lore For Slides</h2><ul>
+<li>lore -ilore-slides -omgp file.html for magic point</li>
+
+<li>lore -ilore-slides -oprosper file.html for prosper</li>
+
+<li>lore -ilore-slides -ohtml file.html for HTML next/prev</li>
+
+<li>Splits on 'h2'</li>
+
+<li>Dogfooding</li>
+
+</ul>
+<hr />
+<em>Emily (Road Trip to Harvard, season 2) -- Why in the world do you insist on taking slides?</em>
+<h2>Extending Lore</h2><ul>
+<li>Accept more input tags</li>
+
+<li>Change how documents are processed</li>
+
+<li>Add more output formats</li>
+
+</ul>
+<hr />
+<em>Rory (The Lorelais' First Day at Chilton, season 1) -- Well, add a couple of plaid skirts</em>
+<h2>Extending Lore -- example</h2><ul>
+<li>We want to add a way to blink: &lt;span class="blink"&gt;</li>
+
+<li>Modify HTML output</li>
+
+<li>Modify lint output</li>
+
+<li>Make it 'small caps' in LaTeX</li>
+
+<li>Distribute as package 'blinker'</li>
+
+</ul>
+<hr />
+<em>Lorelai (Presenting Lorelai Gilmore, season 2) -- No, no, if you wanna do it, I'll help. It's just weird.</em>
+<h2>Extending Lore -- example (cont'd)</h2>
+<pre>
+# blinker/html.py
+from twisted.lore import tree
+from twisted.web import microdom, domhelpers
+
+def doBlink(document):
+ for node in domhelpers.findElementsWithAttribute(document, 'class',
+ 'blink'):
+ newNode = microdom.Element('blink')
+ newNode.children = node.children
+ node.parentNode.replaceChild(newNode, node)
+
+def doFile(fn, docsdir, ext, url, templ, linkrel=''):
+ doc = tree.parseFileAndReport(fn)
+ doBlink(doc)
+ cn = templ.cloneNode(1)
+ tree.munge(doc, cn, linkrel, docsdir, fn, ext, url)
+ cn.writexml(open(os.path.splitext(fn)[0]+ext, 'wb'))
+</pre>
+
+<hr />
+<em>Christopher (Presenting Lorelai Gilmore, season 2) -- I can't believe you're letting her do it.</em>
+<h2>Extending Lore -- example (cont'd 2)</h2>
+<pre>
+# blinker/latex.py
+class BlinkerLatexSpitter(latex.LatexSpitter):
+
+ def visitNode_span_blink(self, node):
+ self.writer('{\sc ')
+ self.visitNodeDefault(node)
+ self.writer('}')
+</pre>
+
+<hr />
+<em>Lorelai (Presenting Lorelai Gilmore, season 2) -- I'm sorry, I meant what scenario on my planet</em>
+<h2>Extending Lore -- example (cont'd 3)</h2>
+<pre>
+# blinker/factory.py
+from blinker import html, latex
+from twisted.lore import default
+
+class ProcessingFunctionFactory(default.ProcessingFunctionFactory):
+
+ doFile = [doFile]
+
+ latexSpitters = {None: latex.BlinkLatexSpitter}
+
+ def getLintChecker(self):
+ checker = lint.getDefaultChecker()
+ checker.allowedClasses = checker.allowedClasses.copy()
+ oldSpan = checker.allowedClasses['span']
+ checker.allowedClasses['span'] = (lambda x:oldSpan(x) or
+ x=='blink')
+ return checker
+
+factory = ProcessingFunctionFactory()
+</pre>
+<pre>
+# blinker/plugins.tml
+register("Blink-Lore",
+ "blinker.factory",
+ description="Lore format with blink",
+ type="lore",
+ tapname="blinklore")
+</pre>
+<p>...and that's it!</p>
+
+<hr />
+<em>Rory (Presenting Lorelai Gilmore, season 2) -- Sorry, we haven't tamed my wild ways yet.</em>
+<h2>Man page support</h2><ul>
+<li>No output</li>
+
+<li>Man-&gt;Lore conversion</li>
+
+<li>lore -iman -olint file.1 --&gt; generates file.html</li>
+
+</ul>
+<hr />
+<em>Lorelai (Concert Interruptus, season 1) -- would you like to write out some sort of instruction manual to go with the dishes?</em>
+<h2>Man page support</h2><ul>
+<li>No output</li>
+
+<li>Man-&gt;Lore conversion</li>
+
+<li>lore -iman -olint file.1 --&gt; generates file.html</li>
+
+</ul>
+<hr />
+<em>Lorelai (Concert Interruptus, season 1) -- would you like to write out some sort of instruction manual to go with the dishes?</em>
+
+</body></html>
diff --git a/doc/historic/2003/europython/slides-template.tpl b/doc/historic/2003/europython/slides-template.tpl
new file mode 100644
index 0000000..fd33fc3
--- /dev/null
+++ b/doc/historic/2003/europython/slides-template.tpl
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <title></title>
+ <link type="text/css" rel="stylesheet" href="stylesheet.css" />
+ </head>
+
+ <body bgcolor="white">
+ [<span class="navigation"><a class="next"></a></span> |
+ <span class="navigation"><a class="previous"></a></span>]
+ <h1 class="title"></h1>
+ <div class="body">
+
+ </div>
+ </body>
+</html>
diff --git a/doc/historic/2003/europython/tw-deploy.html b/doc/historic/2003/europython/tw-deploy.html
new file mode 100644
index 0000000..628d12a
--- /dev/null
+++ b/doc/historic/2003/europython/tw-deploy.html
@@ -0,0 +1,1106 @@
+<html><head><title>A Twisted Web Tutorial</title></head><body>
+
+<h1>A Twisted Web Tutorial</h1>
+
+<h2>Twisted Web -- The Tutorial</h2><ul>
+<li>Welcome</li>
+
+<li>Gimmick -- Buffy quotes</li>
+
+</ul>
+<hr />
+<em>Sweet (Once More With Feeling, season 6) -- Showtime</em>
+<h2>Twisted Web</h2><ul>
+<li>Web server, using Twisted<ul><li>Like Apache, Zope...</li>
+</ul></li>
+
+<li>Serve static files</li>
+
+<li>Run CGIs</li>
+
+<li>Other uses...</li>
+
+</ul>
+<hr />
+<em>Giles (I Robot -- You Jane, season 1) -- There's a demon in the internet</em>
+<h2>Short Example: Putting a Server Up</h2><ul>
+<li>Here's all you need to know to bring up a server</li></ul>
+
+<pre class="shell">
+% mktap --uid=33 --gid=33 web --path=/var/www/htdocs --port=80
+% sudo twistd -f web.tap
+</pre>
+<ul><li>The rest of the talk will explain what that means...</li>
+
+<li>...and how to do more complicated things</li>
+
+</ul>
+<hr />
+<em>Buffy (Once More, With Feeling, season 6) -- I've got a theory. It doesn't matter.</em>
+<h2>Setup and Configuration Utilities</h2><ul>
+<li>mktap</li>
+
+<li>twistd</li>
+
+<li>websetroot<ul><li>Won't be covered, unless there is time</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Xander (The Harvest, season 1) -- crosses, garlic, stake through the heart</em>
+<h2>Digression: What are TAPs</h2><ul>
+<li>Pickled 'application configuration'</li>
+
+<li>Object which contains all the information about application</li>
+
+<li>The canonical way to represent configurations in Twisted</li>
+
+<li>Machine editable</li>
+
+</ul>
+<hr />
+<em>Master (The Wish, season 3) -- Behold the technical wonder</em>
+<h2>mktap</h2><ul>
+<li>General usage</li>
+
+<li>Flexibility and Power</li>
+
+</ul>
+<hr />
+<em>Buffy (Bad Eggs, season 2) -- I'm gonna need a *big* weapon</em>
+<h2>mktap web: Common Useful Options</h2><ul>
+<li>--path: serve from given path</li>
+
+<li>--port: listen on given port</li>
+
+<li>--user: serve from users' directories and personal servers</li>
+
+<li>--logfile: log to NCSA compatible logfile given</li>
+
+<li>--processor: add a special processor for a given extension</li>
+
+</ul>
+<hr />
+<em>Buffy (Bad Eggs, season 2) -- That's probably not gonna be the winning argument, is it?</em>
+
+<h2>twistd</h2><ul>
+<li>Start a Twisted Application</li>
+
+<li>Loads an instance of twisted.internet.app.Application from a file</li>
+
+<li>Daemonizes, binds to appropriate ports, and starts the Twisted mainloop</li>
+
+</ul>
+<hr />
+<em>Giles (Teacher's Pet, season 1) -- That's all he said? Fork Guy?</em>
+<h2>What's a Resource?</h2><ul>
+<li>Everything represented as twisted.web.resource.Resource</li>
+
+<li>Important interface:<ul><li>getChild()</li>
+
+<li>render()</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Sean (Go Fish, season 3) -- You're soakin' in it, bud.</em>
+<h2>Resource Examples</h2><ul>
+<li>Files<ul><li>MIME</li>
+
+<li>Processors</li>
+</ul></li>
+
+<li>Others<ul><li>Virtual hosts</li>
+
+<li>User directories</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Xander (As You Were, season 6) -- We have friends, family and demons</em>
+<h2>Web Development</h2><ul>
+<li>Processors<ul><li>Inherited from resource.Resource</li>
+
+<li>Interpret files as code rather than data</li>
+</ul></li>
+
+<li>Default processors<ul><li>.php -- default PHP</li>
+
+<li>.cgi -- Common Gateway Interface</li>
+
+<li>.rpy -- Correct way, Python scripting</li>
+
+<li>.trp -- Resource pickles</li>
+<li>...more</li>
+</ul></li>
+
+<li>You can also write your own</li>
+
+</ul>
+<hr />
+<em>Xander (Family, season 5) -- That was a tangled web</em>
+<h2>Custom Processor</h2><ul>
+<li>A custom processor to handle Perl CGIs (in a module called PerlScript)</li></ul>
+
+<pre class="python">
+from twisted.web import static, twcgi
+
+class PerlScript(twcgi.FilteredScript):
+ filter = '/usr/bin/perl' # Points to the perl parser
+</pre>
+<ul><li>Use:<ul><li>mktap web --path=/home/nafai/public_html --processor=.pl=PerlScript.PerlScript</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Tara (Family, season 4) -- There was the front of a camel</em>
+<h2>Resource Scripting</h2><ul>
+<li>Subclass resource.Resource</li>
+
+<li>Write a render(self, request) method<ul><li>Return string for immediate response</li>
+
+<li>Return NOT_DONE_YET and write to request</li>
+</ul></li>
+
+<li>Create an .rpy file that sets 'resource' to an instance</li>
+
+</ul>
+<hr />
+<em>Tara (Once More, With Feeling, season 6) -- You make me complete</em>
+<h2>.rpy example</h2>
+<pre class="python">
+from twisted.web import resource as resourcelib
+
+class MyGreatResource(resourcelib.Resource):
+ def render(self, request):
+ return "&lt;html&gt;foo&lt;/html&gt;"
+
+resource = MyGreatResource()
+</pre>
+<hr />
+<em>Willow (Welcome to the Hellmouth, season 1) -- It's probably easy for you.</em>
+<h2>Alternative Configuration Formats</h2><ul>
+<li>xml</li>
+
+<li>source</li>
+
+<li>Python</li>
+
+</ul>
+<hr />
+<em>Ben (The Gift, season 5) -- I wish there was another way</em>
+<h2>Alternative Configuration Formats -- Python</h2><ul>
+<li>Write manually</li>
+
+<li>Just uses twisted.web API</li>
+
+<li>Possible to do anything<ul><li>Write loops</li>
+
+<li>Read other files</li>
+
+<li>(not recommended) Define functions or classes</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Buffy (The I In Team, season 4) -- But I've learned that it pays to be flexible in life.</em>
+
+<h2>Python Configuration Example</h2><ul>
+
+<li>Create application</li>
+
+<li>Make it listen on port 80 for web requests...</li>
+
+<li>...which should be served from /var/www/htdocs</li></ul>
+
+<pre class="python">
+from twisted.internet import app
+from twisted.web import static, server
+
+application = app.Application('web')
+application.listenTCP(80,
+ server.Site(static.File("/var/www/htdocs")))
+</pre>
+<hr />
+<em>Willow (The Pack, season 1) -- It's simple, really.</em>
+
+<h2>Bannerfish -- A Case Study in Deployment</h2><ul>
+<li>Serves banner ads</li>
+
+<li>Has algorithms to maintain randomness and fairness</li>
+
+<li><a href="http://itamarst.org/software/bannerfish/">http://itamarst.org/software/bannerfish/</a></li>
+
+<li>But how to deploy?</li>
+
+</ul>
+<hr />
+<em>Xander (Halloween, season 2) -- Let's move out.</em>
+<h2>Bannerfish -- Standalone tap</h2><ul>
+<li>mktap bannerfish</li>
+
+<li>twistd -f bannerfish.tap</li>
+
+<li>For full list of options<ul><li>mktap --help bannerfish</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Ethan (Halloween, season 2) -- Don't wish to blow my own trumpet, but --</em>
+<h2>Bannerfish -- Standalone tap (behind reverse proxy)</h2><ul>
+<li>mktap bannerfish --port 81 --proxyhost=example.com</li>
+
+<li>twistd -f bannerfish.tap</li>
+
+<li>Now can work on internal server behind firewall</li>
+
+<li>If main server is Twisted Web, following Resource script will serve from bannerfish</li></ul>
+
+<pre class="python">
+resource = proxy.ReverseProxyResource('localhost', 81, '/')
+</pre>
+<hr />
+<em>Buffy (Halloween, season 2) -- You're sweet. A terrible liar, but sweet.</em>
+<h2>Bannerfish -- Standalone Python</h2>
+<pre class="python">
+from twisted.internet import app
+from twisted.cred import authorizer
+from twisted.web import server
+from bannerfish import service
+
+application = app.Application("bannerfish")
+auth = authorizer.DefaultAuthorizer(app)
+svc = service.BannerService('/var/bannerfish',
+ "bannerfish", application, auth)
+site = server.Site(svc.buildResource(None, None))
+application.listenTCP(80, site)
+</pre>
+<hr />
+<em>Spike (Halloween, season 2) -- Shaking. Terrified. Alone. Lost little lamb.</em>
+<h2>Bannerfish -- /etc/twisted-web/local.d Drop In</h2>
+<pre class="python">
+from twisted.cred import authorizer
+from bannerfish import service
+
+auth = authorizer.DefaultAuthorizer(app)
+svc = service.BannerService('/var/bannerfish',
+ "bannerfish", application, auth)
+resource = svc.buildResource(None, None)
+default.addChild("bannerfish", resource)
+</pre>
+<hr />
+<em>Cordelia (Halloween, season 2) -- Well, I guess you better get them back to their parents.</em>
+<h2>Bannerfish -- Resource Script</h2>
+<pre class="python">
+from twisted.cred import authorizer
+from twisted.internet import app
+from bannerfish import service
+
+application = registry.getComponent(app.Application)
+auth = authorizer.DefaultAuthorizer(application)
+svc = service.BannerService('/var/bannerfish',
+ "bannerfish", application, auth)
+resource = svc.buildResource(None, None)
+</pre>
+<ul><li>But see later, about registry</li>
+
+</ul>
+<hr />
+<em>Xander (Innocence, season 2) -- They like to see the big guns.</em>
+<h2>Bannerfish -- Distributed (Slave)</h2>
+
+<pre class="python">
+from twisted.internet import application
+from twisted.cred import authorizer
+from twisted.web import server
+from bannerfish import service
+application = app.Application("bannerfish")
+auth = authorizer.DefaultAuthorizer(application)
+svc = service.BannerService('/var/bannerfish',
+ "bannerfish", application, auth)
+site = server.Site(svc.buildResource(None, None))
+fact = pb.BrokerFactory(site)
+site = server.Site(root)
+application.listenUNIX('/var/run/bannerfish', fact)
+</pre>
+<h2>Bannerfish -- Distributed (Master, Resource Script)</h2>
+
+<pre class="python">
+from twisted.web import distrib
+
+resource = distrib.ResourceSubscription('unix',
+ '/var/run/bannerfish')
+</pre>
+<hr />
+<em>Oz (Innocence, season 2) -- So, do you guys steal weapons from the Army a lot?</em>
+<h2>Bannerfish -- Other options</h2><ul>
+<li>Mix and match possible</li>
+
+<li>Can serve same content multiple ways simultaneously</li>
+
+<li>Might be useful as a way to serve same ads different ways</li>
+
+<li>...or serve ads from several bannerfish servers...</li>
+
+<li>...each deployed differently.</li>
+
+</ul>
+<hr />
+<em>Buffy (Tabula Rasa, season 6) -- I'm like a superhero or something</em>
+<h2>Bannerfish -- Conclusions</h2><ul>
+<li>What are the tradeoffs?</li>
+
+<li>Everything works in the simple cases</li>
+
+<li>Not enough complicated cases to have data</li>
+
+<li>Luckily, easy to move between them</li>
+
+<li>Motto -- move deployment choices as late as possible</li>
+
+</ul>
+<hr />
+<em>Giles (Killed By Death, season 2) -- Simple enough, but, but</em>
+<h2>Further Reading</h2><ul>
+<li>Short overview -- doc/howto/web-overview.html</li>
+
+<li>In depth review -- doc/howto/using-twistedweb.html</li>
+
+<li>Using databases -- doc/howto/enterprise.html</li>
+
+<li>Deferred execution -- doc/howto/deferred.html</li>
+
+<li>Resource script examples -- doc/examples/*.rpy.py</li>
+
+</ul>
+<hr />
+<em>Giles (I Was Made to Love You, Season 5) -- There's an enormous amount of research we should do before -- no I'm lying</em>
+<h2>Questions?</h2>
+<em>Vampire Willow (Dopplegangland, season 3): Questions? Comments?</em>
+<h2>Bonus Slides</h2>
+<em>Xander (The Dark Age, season 2) -- A bonus day of class plus Cordelia.</em>
+
+<h2>Python Configuration -- Hints</h2><ul>
+<li>Working with persistence</li>
+
+<li>Processors</li>
+
+<li>Indices</li>
+
+<li>Virtual Hosts</li>
+
+</ul>
+<hr />
+<em>Buffy (Phases, season 2) -- Have you dropped any hints?</em>
+<h2>Python Configuration -- Persistence</h2><ul>
+<li>Don't define functions or classes</li>
+
+<li>Don't modify class attributes</li>
+
+</ul>
+<hr />
+<em>Spike (Once More, With Feeling) -- Let me rest in peace</em>
+<h2>Python Configuration -- Processors</h2>
+<pre class="python">
+
+from twisted.internet import app
+from twisted.web import static, server
+from twisted.web import twcgi
+
+root = static.File("/var/www")
+root.processors = {".cgi": twcgi.CGIScript}
+application = app.Application('web')
+application.listenTCP(80, server.Site(root))
+</pre>
+<hr />
+<em>Manny (Doublemeat Palace, season 6) -- It's a meat process</em>
+<h2>Python Configuration -- Indices</h2>
+<pre class="python">
+
+root = static.File("/var/www")
+root.indices = ['index.rpy', 'index.html']
+</pre>
+<hr />
+<em>Willow (Buffy vs. Dracula, season 5) -- Labelling your amulets and indexing your diaries</em>
+<h2>Python Configuration -- Virtual Hosts</h2>
+<pre class="python">
+
+from twisted.web import vhost
+default = static.File("/var/www")
+foo = static.File("/var/foo")
+root = vhost.NamedVirtualHost(default)
+root.addHost('foo.com', foo)
+</pre>
+<hr />
+<em>Fritz (I Robot, You Jane, season 1) -- The only reality is virtual.</em>
+<h2>Python Configuration -- uber example</h2>
+<pre class="python">
+
+from twisted.internet import app
+from twisted.web import static, server, vhost, script
+
+default = static.File("/var/www")
+default.processors = {".rpy", script.ResourceScript}
+root = vhost.NamedVirtualHost(default
+foo = static.File("/var/foo")
+foo.indices = ['index.xhtml', 'index.html']
+root.addHost('foo.com', foo)
+site = server.Site(root)
+application = app.Application('web')
+application.listenTCP(80, site, interface='127.0.0.1')
+</pre>
+<hr />
+<em>Buffy (Potential, season 7) -- It was putting a lot of stock in that uber-vamp</em>
+<h2>Python Configuration -- Splitting With Reverse Proxy</h2><ul>
+<li>Original use case - SVN</li></ul>
+
+<pre class="python">
+from twisted.web import proxy
+
+root.putChild('foo',
+ proxy.ReverseProxyResource('localhost',
+ 81, '/foo/'))
+</pre>
+<hr />
+<em>Buffy (Once More, With Feeling, season 6 -- So I will walk through the fire</em>
+<h2>mktap examples</h2><ul>
+<li>mktap web</li>
+
+<li>mktap web --path=/var/www --logfile=/var/log/twistedweb.log</li>
+
+<li>mktap web --port=80 --path=/var/www --mime-type=text/plain</li>
+
+<li>mktap web --path=/home/nafai/public_html --processor=.pl=PerlProcessor.PerlProcessor --index=index.pl</li>
+
+</ul>
+<hr />
+<em>Anya (I Was Made to Love You, season 4) -- You can also see the website I designed for the magic shop</em>
+<h2>mktap examples (cont'd)</h2><ul>
+<li>mktap web --users</li>
+
+<li>mktap web --ignore-ext=.cgi</li>
+
+<li>mktap web --index=index.cgi --index=index.rpy --index=index.html</li>
+
+</ul>
+<hr />
+<em>Buffy (Once More, With Feeling, season 6) -- All the twists and bends</em>
+<h2>mktap examples (alternate formats)</h2><ul>
+<li>mktap --type=source web</li>
+
+<li>mktap --type=xml web</li>
+
+</ul>
+<hr />
+<em>Tara (Seeing Red, season 6) -- It isn't written in any ancient language we could identify.</em>
+<h2>mktap examples (setting uid)</h2><ul>
+<li>mktap --uid=33 web</li>
+
+<li>mktap --gid=33 web</li>
+
+<li>Uid/Gid of www-data on Debian systems</li>
+
+<li>Not possible to use username</li>
+
+<li>More about this later</li>
+
+</ul>
+<hr />
+<em>Buffy (Who Are You?, season 4) -- I would be Buffy</em>
+
+<h2>twistd examples</h2><ul>
+<li>twistd -f web.tap -l /var/log/twisted.log</li>
+
+<li>twistd -f web.tap --pidfile /var/run/web.pid</li>
+
+<li>twistd -x web.tax<ul><li>For mktap --type=xml</li>
+</ul></li>
+
+<li>twistd -s web.tas<ul><li>For mktap --type=source</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Xander (Teacher's Pet, season 1) -- How come *that* never came up?</em>
+<h2>Shutting down twistd</h2><ul>
+<li>On Unix (in general): <ul><li>kill `cat twistd.pid`</li>
+</ul></li>
+
+<li>On Windows: <ul><li>Cannot daemonize on Windows, so just run twistd in a command prompt</li>
+
+<li>Switch to the command prompt, and press Control-C</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Buffy (Prophecy Girl, season 1) -- I don't wanna die.</em>
+<h2>Shutdown TAPs</h2><ul>
+<li>Since TAPs store persistent data for an application, a 'shutdown' TAP is created on twistd shutdown</li>
+
+<li>You'll often want to start your Twisted application on subsequent runs with the shutdown TAP</li>
+
+</ul>
+<hr />
+<em>Headstone (The Gift, season 5) -- She saved the world. A lot.</em>
+<h2>twistd and security</h2><ul>
+<li>When twistd is run as root, it will shed root privileges for the uid and gid of either the user that created the TAP or those specified on the mktap commandline.</li>
+
+</ul>
+<hr />
+<em>Buffy (Dopplegangland, season 3) -- I think it's good to be reliable</em>
+
+<h2>Resource Call Examples</h2><ul>
+<li>/foo/bar/baz gets converted to:</li></ul>
+
+<pre class="python">
+site.getChild('foo', request
+ ).getChild('bar', request
+ ).getChild('baz', request
+ ).render(request)
+</pre>
+<hr />
+<em>Willow/Tara (Afterlife, Part 2, season 6) -- Child of words, hear thy makers</em>
+<h2>Resource Call Examples (cont'd)</h2><ul>
+<li>/foo/bar/baz/ gets converted to:</li></ul>
+
+<pre class="python">
+site.getChild('foo', request
+ ).getChild('bar', request
+ ).getChild('baz', request
+ ).getChild('', request
+ ).render(request)
+</pre>
+<hr />
+<em>Buffy (Gone, season 6) -- Stop trying to see me.</em>
+<h2>Distributed Servers -- Theory</h2><ul>
+<li>Master is a resource</li>
+
+<li>Slave is a server</li>
+
+<li>Same server can have both master and slave parts</li>
+
+</ul>
+<hr />
+<em>Anya (Once More, With Feeling, season 6) -- I've got a theory, it could be bunnies</em>
+<h2>Distributed Servers -- Manually</h2>
+<pre class="python">
+from twisted.internet import app, protocol
+from twisted.web import server, distrib, static
+from twisted.spread import pb
+
+application = app.Application("silly-web")
+# The "master" server
+site = server.Site(distrib.ResourceSubscription('unix', '.rp'))
+application.listenTCP(19988, site)
+# The "slave" server
+fact = pb.BrokerFactory(distrib.ResourcePublisher(
+ server.Site(static.File('static'))))
+application.listenUNIX('./.rp', fact)
+</pre>
+<hr />
+<em>Buffy (Some Assembly Required, season 2) -- Men dig up the corpses and the women have the babies.</em>
+<h2>Distributed Servers -- Manual (cont'd)</h2><ul>
+<li>First Server</li></ul>
+
+<pre class="python">
+from twisted.internet import app, protocol
+from twisted.web import server, distrib, static, vhost
+from twisted.spread import pb
+
+application = app.Application("ping-web")
+
+default = static.File("/var/www/foo")
+root = vhost.NamedVirtualHost(default)
+root.addVhost("foo.com", default)
+bar = distrib.ResourceSubscription('unix', '.bar')
+root.addVhost("bar.com", bar)
+
+fact = pb.BrokerFactory(static.Site(default))
+site = server.Site(root)
+application.listenTCP(19988, site)
+application.listenUNIX('./.foo', fact)
+</pre>
+<hr />
+<em>Buffy (Welcome to the Hellmouth, season 1) -- Now, we can do this the hard way, or...</em>
+<h2>Distributed Servers -- Manual (cont'd 2)</h2><ul>
+<li>Second Server</li></ul>
+
+<pre class="python">
+from twisted.internet import app, protocol
+from twisted.web import server, distrib, static, vhost
+from twisted.spread import pb
+
+application = app.Application("pong-web")
+
+foo = distrib.ResourceSubscription('unix', '.foo')
+root = vhost.NamedVirtualHost(foo)
+root.addVhost("foo.com", foo)
+bar = static.File("/var/www/bar")
+root.addVhost("bar.com", bar)
+
+fact = pb.BrokerFactory(static.Site(bar))
+site = server.Site(root)
+application.listenTCP(19989, site)
+application.listenUNIX('./.bar', fact)
+</pre>
+<hr />
+<em>Buffy (Welcome to the Hellmouth, season 1) -- ...well, actually there's just the hard way.</em>
+<h2>Distributed Servers -- User Directory</h2><ul>
+<li>A resource</li>
+
+<li>Child that looks like 'moshez' -- ~moshez/public_html </li>
+
+<li>Child that looks like 'moshez.twistd' -- moshez's personal server</li>
+
+</ul>
+<hr />
+<em>Master (The Wish, season 3) -- Mass production!</em>
+<h2>Distributed Servers -- User Directory Server</h2><ul>
+<li>With mktap: mktap web --user</li>
+
+<li>With Python configuration</li></ul>
+
+<pre class="python">
+from twisted.internet import app
+from twisted.web import static, server, distrib
+
+root = static.File("/var/www")
+root.putChild("users", distrib.UserDirectory())
+site = server.Site(root)
+application = app.Application('web')
+application.listenTCP(80, site)
+</pre>
+<hr />
+<em>Richard (Reptile Boy, season 2) -- In his name.</em>
+<h2>Distributed Servers -- Personal Servers</h2><ul>
+<li>With mktap: mktap web --personal ...</li>
+
+<li>With Python configuration</li></ul>
+
+<pre class="python">
+from twisted.internet import app
+from twisted.web import static, server, distrib
+from twisted.spread import pb
+
+root = static.File("/home/moshez/twistd")
+site = server.Site(root)
+
+fact = pb.BrokerFactory(distrib.ResourcePublisher(site))
+application.listenUNIX('/home/moshez/.twisted-web-pb', fact)
+</pre>
+<hr />
+<em>Giles (Bargaining, season 6) -- It's my personal collection</em>
+<h2>Debian Configuration</h2><ul>
+<li>Inside twisted-web package</li>
+
+<li>Goal -- look like other web servers to users</li>
+
+<li>Goal -- interoperate easily</li>
+
+<li>Goal -- allow users to avoid modifying files</li>
+
+</ul>
+<hr />
+<em>Buffy (Bad Girls, season 3) -- We can help each other.</em>
+<h2>Debian Configuration -- Usage</h2><ul>
+<li>Changing port -- edit /etc/twisted-web/ports</li>
+
+<li>Want to use behind reverse proxy? Use rptwisted</li>
+
+<li>Change anything else -- drop files in /etc/twisted-web/local.d</li>
+
+</ul>
+<hr />
+<em>Faith (Home Coming, season 3) -- we'll use 'em</em>
+<h2>Debian Configuration -- Drop In Examples</h2>
+<pre class="python">
+from twisted.web import static
+import os
+
+vhostDir = '/var/www/vhost/'
+
+for file in os.listdir(vhostDir):
+ root.addHost(file, static.File(os.path.join(vhostDir, file)))
+</pre>
+<hr />
+<em>Buffy (The Freshman, season 4) -- I just thought I'd drop in</em>
+<h2>Debian Configuration -- Drop In Examples (cont'd)</h2>
+<pre class="python">
+from twisted.web import script, static
+
+default.processors['.rpy'] = script.ResourceScript
+default.ignoreExt('rpy')
+</pre>
+<hr />
+<em>Riley (As You Were, season 6) -- Sorry to just drop in on you</em>
+<h2>Debian Configuration -- Drop In Examples (cont'd 2)</h2>
+<pre class="python">
+from twisted.web import vhost
+
+default.putChild('vhost', vhost.VHostMonsterResource())
+</pre>
+<hr />
+<em>Sam (As You Were, season 6) -- a hairy night drop into hostile territory</em>
+<h2>twistedmatrix.com Configuration</h2><ul>
+<li>Some highlights</li></ul>
+
+<pre class="python">
+...
+indexNames = ['index', 'index.html', 'index.xhtml', 'index.rpy','index.cgi']
+...
+root.putChild('mailman', twcgi.CGIDirectory('/usr/lib/cgi-bin'))
+root.putChild('users', distrib.UserDirectory())
+root.putChild('cgi-bin', twcgi.CGIDirectory('/usr/lib/cgi-bin'))
+root.putChild('doc', static.File('/usr/share/doc'))
+...
+uid = pwd.getpwnam('www-data')[2]
+gid = grp.getgrnam('www-data')[2]
+...
+top = rewrite.RewriterResource(root, rewrite.tildeToUsers)
+...
+application = app.Application("web", uid=uid, gid=gid)
+</pre>
+<hr />
+<em>Xander (The Witch, season 1) -- May all lesser cretins bow before me.</em>
+<h2>Apache vs. Twisted Web</h2><ul>
+<li>Apache is faster</li>
+
+<li>Apache -- Threads/processes model</li>
+
+<li>Twisted -- async model</li>
+
+<li>Apache -- has C security holes (buffer overflows)</li>
+
+<li>Twisted -- easy to set up</li>
+
+<li>Twisted -- built in Python programmability</li>
+
+</ul>
+<hr />
+<em>Willow (Buffy vs. Dracular, season 5) -- I think we've just put our finger on why we're the sidekicks</em>
+<h2>Apache/Twisted Web Integration</h2><ul>
+<li>Use both!</li>
+
+<li>Apache's reverse proxy works well</li>
+
+<li>Easy to have a site which is partially managed by Apache</li>
+
+<li>Documentation has examples of configurations</li>
+
+</ul>
+<hr />
+<em>Xander (What's My Line, season 2) -- Angel's our friend! Except I don't like him.</em>
+<h2>Zope vs. Twisted Web</h2><ul>
+<li>Zope -- fully editable through the web</li>
+
+<li>Zope -- uses ZODB, not file system</li>
+
+<li>Twisted -- can integrate with other protocols easily</li>
+
+<li>Twisted -- extension code has much less overhead</li>
+
+</ul>
+<hr />
+<em>Willow (Dopplegangland, season 3) -- Competition is natural and healthy</em>
+<h2>Zope/Twisted Web Integration</h2><ul>
+<li>Possible to use Twisted as Zope's network layer</li>
+
+<li>Hackish with Zope2</li>
+
+<li>Easier with Zope3</li>
+
+</ul>
+<hr />
+<em>Snyder (Dopplegangland, season 3) -- It's a perfect match.</em>
+<h2>Zope/Twisted Web Integration (cont'd)</h2><ul>
+<li>Less direct -- use Apache</li>
+
+<li>Reverse proxy parts to Zope</li>
+
+<li>Reverse proxy parts to Twisted Web</li>
+
+</ul>
+<hr />
+<em>Wesley (Dopplegangland, season 3) -- Still a little sloppy, though</em>
+<h2>Applications Appropriate for Twisted Web</h2><ul>
+<li>Webmail</li>
+
+<li>Blogs</li>
+
+<li>Web/other protocol chat systems</li>
+
+</ul>
+<hr />
+<em>Sweet (Once More, With Feeling) -- Why don't you come and play?</em>
+<h2>Behind Reverse Proxy</h2><ul>
+<li>Sometimes, we want Twisted to pretend to be another host/port</li>
+
+<li>Reverse proxies, NATs, etc.</li>
+
+<li>Reverse proxy to /vhost/http/&lt;host:port&gt;/</li>
+
+<li>Make sure root has a child called vhost of type twisted.web.vhost.VirtualHostingMonster</li>
+
+</ul>
+<hr />
+<em>Jenny (I Robot -- You Jane, season 1) -- The divine exists in cyberspace</em>
+<h2>Rewrite Rules</h2><ul>
+<li>Change a URL to another</li>
+
+<li>Useful for different treatment from outside resources</li>
+
+<li>Wraps a resource</li>
+
+</ul>
+<hr />
+<em>Spike (What's My Line, season 2) -- Read it again.</em>
+<h2>Rewrite Rules -- Example</h2>
+<pre class="python">
+
+root = static.File("/var/www")
+root.putChild("users", distrib.UserDirectory())
+root = rewrite.RewriterResource(root, rewrite.tildeToUsers)
+</pre>
+<ul><li>Now, /~moshez/ works</li>
+
+</ul>
+<hr />
+<em>Spike (What's My Line, season 2) -- I think it's just enough kill.</em>
+<h2>websetroot</h2><ul>
+<li>Used to change what the root of the server points to</li>
+
+<li>Set it to a Resource contained either in a Python source file or a Pickle file</li>
+
+</ul>
+<hr />
+<em>Manny (DoubleMeat Palace, season 6) -- We have a lot of turnover here</em>
+<h2>Sample websetroot command lines</h2><ul>
+<li>websetroot -p 80 -f web.tap --script rootResource.py</li>
+
+<li>websetroot -p 8080 -f web.tap --pickle rootPickle</li>
+
+</ul>
+<hr />
+<em>Manny (DoubleMeat Palace, season 6) -- You can toss it</em>
+<h2>init.d</h2><ul>
+<li>pidfile</li>
+
+<li>chdir</li>
+
+<li>chroot?</li>
+
+<li>No smooth reloading</li>
+
+<li>Persistence</li>
+
+</ul>
+<hr />
+<em>Bob (Zeppo, season 3) -- He hasn't been initiated.</em>
+<h2>Special Bonus - How to Configure &lt;user&gt;.example.com</h2>
+<pre class="python">
+import pwd, os
+from twisted.web import resource, error, distrib
+from twisted.protocols import http
+
+class UserNameVirtualHost(resource.Resource):
+
+ def __init__(self, default, tail):
+ resource.Resource.__init__(self)
+ self.default = default
+ self.tail = tail
+ self.users = {}
+
+ def _getResourceForRequest(self, request):
+ host=request.getHeader('host')
+ if host.endswith(tail):
+ username = host[:-len(tail)]
+ else:
+ username = default
+ if self.users.has_key(username):
+ return self.users[username]
+ try:
+ (pw_name, pw_passwd, pw_uid, pw_gid, pw_gecos, pw_dir,
+ pw_shell) = pwd.getpwnam(username)
+ except KeyError:
+ return error.ErrorPage(http.NOT_FOUND,
+ "No Such User",
+ "The user %s was not found on this system." %
+ repr(username))
+ twistdsock = os.path.join(pw_dir, ".twistd-web-pb")
+ rs = distrib.ResourceSubscription('unix',twistdsock)
+ self.users[username] = rs
+ return rs
+
+ def render(self, request):
+ resrc = self._getResourceForRequest(request)
+ return resrc.render(request)
+
+ def getChild(self, path, request):
+ resrc = self._getResourceForRequest(request)
+ request.path=request.path[:-1]
+ request.postpath=request.uri.split('/')[1:]
+ print request, request.path, request.postpath
+ return resrc.getChildForRequest(request)
+</pre>
+<hr />
+<em>Morgan (The Puppet Show, season 1) -- Weird? What d'you mean?</em>
+<h2>Special Bonus - How to Configure &lt;user&gt;.example.com (cont'd)</h2><ul>
+<li>Put above in a module (say, uservhost)</li>
+
+<li>Use following configuration file</li></ul>
+
+<pre class="python">
+from twisted.internet import app
+from twisted.web import server
+import uservhost
+
+root = UserNameVirtualHost("www", "example.com")
+site = server.Site(root)
+application = app.Application('web')
+application.listenTCP(80, site)
+</pre>
+<hr />
+<em>Snyder (The Puppet Show, season 1) -- You need to integrate into this school, people.</em>
+<h2>Using the Twisted Registry</h2><ul>
+<li>Use especially in Resource Scripts</li>
+
+<li>Save persistent information</li>
+
+<li>Uses Twisted's Componentized to be extensible</li>
+
+</ul>
+<hr />
+<em>Angel (Helpless, season 3) -- I wanted to keep it safe</em>
+<h2>Using the Twisted Registry -- example</h2>
+<pre class="python">
+
+from twisted.web import distrib
+
+resource = registry.getComponent(distrib.UserDirectory)
+if not resource:
+ resource = distrib.UserDirectory()
+ registry.setComponent(distrib.UserDirectory, resource)
+</pre>
+<hr />
+<em>Paul (The Freshman, season 4) -- Do you know where they're distributing the [...] applications?</em>
+<h2>Using the Twisted Registry -- problems</h2><ul>
+<li>In most cases -- need to write a custom class</li>
+
+<li>Saves data in-memory</li>
+
+<li>Won't work as expected unless -shutdown taps are used</li>
+
+</ul>
+<hr />
+<em>Anya (Once More, With Feeling, season 6) -- The only trouble is [pause] I'll never tell.</em>
+<h2>Alternative Configuration Formats -- XML</h2><ul>
+<li>Can be generated from mktap</li>
+
+<li>Editable with any XML editor</li>
+
+<li>Easy to do easy things<ul><li>Change a port</li>
+</ul></li>
+
+<li>Nontrivial to do hard things<ul><li>Bind to specific IP</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Buffy (Once More, With Feeling, season 6) -- To fit in in this glittering world.</em>
+<h2>Alternative Configuration Formats -- XML -- example</h2>
+<pre class="python">
+&lt;?xml version="1.0"?&gt;
+
+&lt;instance class="twisted.internet.app.Application" reference="1"&gt;
+ &lt;dictionary&gt;
+...
+ &lt;string role="key" value="tcpPorts" /&gt;
+ &lt;list&gt;
+ &lt;tuple&gt;
+ &lt;int value="80" /&gt;
+ &lt;instance class="twisted.web.server.Site"&gt;
+ &lt;dictionary&gt;
+...
+ &lt;string role="key" value="resource" /&gt;
+ &lt;instance class="twisted.web.static.File"&gt;
+ &lt;dictionary&gt;
+...
+ &lt;string role="key" value="path" /&gt;
+ &lt;string value="/var/www" /&gt;
+...
+ &lt;/dictionary&gt;
+ &lt;/instance&gt;
+...
+ &lt;/dictionary&gt;
+ &lt;/instance&gt;
+...
+ &lt;/tuple&gt;
+ &lt;/list&gt;
+...
+ &lt;/dictionary&gt;
+&lt;/instance&gt;
+</pre>
+<hr />
+<em>Natalie (Teacher's Pet, season 1) -- There's nothing ugly about these creatures</em>
+<h2>Alternative Configuration Formats -- Source</h2><ul>
+<li>Can be generated from mktap</li>
+
+<li>Editable with any Python source editor</li>
+
+<li>Easy to do easy things<ul><li>Change a port</li>
+</ul></li>
+
+<li>Nontrivial to do hard things<ul><li>Bind to specific IP</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Willow/Giles/Xander (Primeval, season 4) -- You could never hope to grasp the source</em>
+<h2>Alternative Configuration Formats -- Source -- Example</h2>
+<pre class="python">
+app=Ref(1,
+ Instance('twisted.internet.app.Application',{
+...
+ 'tcpPorts':[
+ (
+ 80,
+ Instance('twisted.web.server.Site',
+...
+ resource=Instance('twisted.web.static.File',{
+...
+ 'path':'/var/www',
+...
+ ),
+ ],
+...
+ }))
+</pre>
+<hr />
+<em>Tara (Family, season 5) -- You learn her source, and, uh we'll introduce her to her insect reflection</em>
+<h2>Twisted Web - Beginnings</h2>
+<pre class="python">
+
+&lt;glyphAtWork&gt; the http server was so we could say "Web!" if we ever did
+ a freshmeat announcement
+&lt;glyphAtWork&gt; this makes people excited
+</pre>
+<ul><li>Turned out he was right</li>
+
+</ul>
+<hr />
+<em>Dawn (Get It Done, season 7) -- I think it's an origin myth.</em>
+<h2>Woven Overview</h2><ul>
+<li>HTML templates</li>
+
+<li>Model/View/Controller architecture</li>
+
+<li>Integrated with deferred</li>
+
+<li>Classical systems work badly with async</li>
+
+<li>More -- beyond scope of this tutorial</li>
+
+</ul>
+<hr />
+<em>Razor (Bargaining, season 6) -- A pretty toy</em>
+</body></html>
diff --git a/doc/historic/2003/europython/twisted.html b/doc/historic/2003/europython/twisted.html
new file mode 100644
index 0000000..2576bca
--- /dev/null
+++ b/doc/historic/2003/europython/twisted.html
@@ -0,0 +1,608 @@
+<html><head><title>Twisted Tutorial</title></head>
+<body>
+
+<h1>Twisted Tutorial</h1>
+
+<h2>Twisted -- The Tutorial</h2><ul>
+<li>Welcome</li>
+
+<li>Gimmick -- Charmed quotes</li>
+
+</ul>
+<hr />
+<em>Prue (Something Wicca This Way Comes, season 1) -- Piper, the girl has no vision, no sense of the future.</em>
+<h2>Twisted -- Networking For Python</h2><ul>
+<li>Handles the icky socket stuff</li>
+
+<li>Handles the icky select stuff</li>
+
+<li>No threads, no blocking</li>
+
+</ul>
+<hr />
+<em>Leo (Bite Me, season 4) -- As far as I know they're apart of a whole different network now.</em>
+<h2>Finger</h2><ul>
+<li>Send username</li>
+
+<li>Get back some stuff about user</li>
+
+<li>Will only implement subset of protocol here</li>
+
+</ul>
+<hr />
+<em>Natalie (Blinded By the Whitelighter) -- I'll assume a demon attacked your finger</em>
+<h2>Finger - Protocol code</h2>
+<pre class="python">
+from twisted.protocols import basic
+
+class FingerClient(basic.LineReceiver):
+
+ # This will be called when the connection is made
+ def connectionMade(self): self.sendLine(self.factory.user)
+
+ # This will be called when the server sends us a line.
+ # IMPORTANT: line *without "\n" at end.
+ # Yes, this means empty line does not mean EOF
+ def lineReceived(self, line): print line
+
+ # This will be called when the connection is terminated
+ def connectionLost(self, _): print "-"*40
+</pre>
+<hr />
+<em>Phoebe (Blind Sided, season 1) -- Standard dating protocol.</em>
+<h2>Finger - client factory</h2><ul>
+<li>Keep configuration information</li>
+
+<li>In this case, just the username</li></ul>
+
+<pre class="python">
+from twisted.internet import protocol
+
+class FingerFactory(protocol.ClientFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, user): self.user = user
+
+ def clientConnectionFailed(self, _, reason):
+ print "error", reason.value
+</pre>
+
+<hr />
+<em>Jack (Ms. Hellfire, season 2) -- Well, they'd better be a rich client</em>
+<h2>Finger - tying it all together</h2><ul>
+<li>Actually run above code</li>
+
+<li>Use reactors</li></ul>
+
+<pre class="python">
+from twisted.internet import reactor
+import sys
+
+user, host = sys.argv[1].split('@')
+port = 79
+reactor.connectTCP(host, port, FingerFactory(port))
+reactor.run()
+</pre>
+<hr />
+<em>Prue/Phoebe/Piper (Something Wicca This Way Comes, season 1) -- The power of three will set us free</em>
+<h2>Finger - a bug</h2><ul>
+<li>Succeed or fail, program doesn't exit</li>
+
+<li>Reactor continues in a loop</li>
+
+<li>Takes almost no CPU time...</li>
+
+<li>...but still wrong behaviour</li>
+
+</ul>
+<hr />
+<em>Leo (Trial By Magic, season 4) -- Demons you can handle but not rats?</em>
+<h2>Digression - Deferreds</h2><ul>
+<li>In order to be more flexible, we want callbacks</li>
+
+<li>Common callbacks are too weak</li>
+
+<li>We used 'deferreds' as an abstraction for callbacks</li>
+
+</ul>
+<hr />
+<em>Piper (Morality Bites, season 2) -- Talk about it later.</em>
+<h2>Finger - reimplementing correctly</h2>
+<pre class="python">
+from twisted.protocols import basic
+from twisted.internet import protocol, defer
+import sys
+
+class FingerClient(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.transport.write(self.factory.user+"\n")
+
+ def lineReceived(self, line):
+ self.factory.gotLine(line)
+</pre>
+
+
+<h2>Finger - reimplementing correctly (cont'd)</h2>
+<pre class="python">
+class FingerFactory(protocol.ClientFactory):
+ protocol = FingerProtocol
+
+ def __init__(self, user):
+ self.user, self.d = user, defer.Deferred()
+
+ def gotLine(self, line): print line
+
+ def clientConnectionLost(self, _, why): self.d.callback(None)
+
+ def clientConnectionFailed(self, _, why): self.d.errback(why)
+</pre>
+
+<h2>Finger - reimplementing correctly (cont'd 2)</h2>
+<pre class="python">
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ from twisted.python import util
+ user, host = sys.argv[1].split('@')
+ f = FingerFactory(user)
+ port = 79
+ reactor.connectTCP(host, port, FingerFactory(port))
+ f.d.addCallback(lambda _: reactor.stop())
+ f.d.addErrback(lambda _: (util.println("could not connect"),
+ reactor.stop()))
+ reactor.run()
+</pre>
+<hr />
+<em>Phoebe (Charmed and Dangerous, season 4) -- That's what we were missing.</em>
+<h2>Servers</h2><ul>
+<li>Servers are actually easier</li>
+
+<li>Servers meant to wait for events</li>
+
+<li>Most of concepts similar to clients</li>
+
+</ul>
+<hr />
+<em>Genie (Be Careful What You Witch For, season 2) -- All I know is that you rubbed and now I serve.</em>
+<h2>Finger - protocol</h2>
+<pre class="python">
+class FingerServer(basic.LineReceiver):
+
+ def lineReceived(self, line):
+ self.transport.write(self.factory.getUser(line))
+ self.transport.loseConnection()
+</pre>
+<hr />
+<em>Secretary (The Painted World, season 2) -- Well, you won't have any trouble with this if you figured that out.</em>
+<h2>Finger - factory</h2>
+<pre class="python">
+class FingerServerFactory(protocol.Factory):
+
+ protocol = FingerServer
+
+ def __init__(self):
+ self.users = {}
+ self.message = "No such user\n"
+
+ def getUser(self, name):
+ return self.users.get(name, self.message)
+
+ def setUser(self, user, status):
+ self.users[user] = status
+</pre>
+<hr />
+<em>Prue (The Demon Who Came In From the Cole, season 3) -- Okay, so who are they?</em>
+<h2>Finger - glue</h2>
+<pre class="python">
+factory = FingerServerFactory()
+factory.setUser("moshez", "Online - Sitting at computer\n")
+factory.setUser("spiv", "Offline - Surfing the waves\n")
+
+reactor.listenTCP(79, factory)
+</pre>
+<hr />
+<em>Prue (All Halliwell's Eve, season 3) -- Put it all together, it may just work.</em>
+<h2>Finger Server - problem</h2><ul>
+<li>What if server has to actually work to find user's status?</li>
+
+<li>For example, read status from a website</li>
+
+<li>API forces us to block -- not good</li>
+
+</ul>
+<hr />
+<em>Piper (All Halliwell's Eve, season 3) -- We've got big problems, a little time and a little magic.</em>
+<h2>Finger server -- new protocol</h2>
+<pre class="python">
+class FingerServer(basic.LineReceiver):
+
+ def lineReceived(self, line):
+ d = self.factory.getUser(line)
+ d.addCallback(self.writeResponse)
+ d.addErrback(self.writeError)
+
+ def writeResponse(self, response):
+ self.transport.write(response)
+ self.transport.loseConnection()
+
+ def writeError(self, error):
+ self.transport.write("Server error -- try later\n")
+ self.transport.loseConnection()
+</pre>
+<hr />
+<em>Piper (Ex Libris, season 2) -- We'll worry about it later.</em>
+<h2>Finger - factory</h2>
+<pre class="python">
+class FingerServerFactory(protocol.Factory):
+
+ protocol = FingerServer
+
+ def __init__(self):
+ self.users = {}
+ self.message = "No such user\n"
+
+ def getUser(self, name):
+ return defer.succeed(self.users.get(name, self.message))
+
+ def setUser(self, user, status):
+ self.users[user] = status
+</pre>
+<hr />
+<em>Piper/Zen Master (Enter the Demon, season 4) -- It is a different realm down there with new rules.</em>
+<h2>Finger - web factory</h2>
+<pre class="python">
+from twisted.web import client
+
+class FingerWebFactory(protocol.Factory):
+ protocol = FingerServer
+
+ def getUser(self, name):
+ url = "http://example.com/~%s/online" % name
+ d = client.getPage(url)
+ d.addErrback(lambda _: "No such user\n")
+ return d
+</pre>
+<hr />
+<em>Applicant #3 (The Painted World, season 2) -- in this day and age, who can't write in the HTML numeric languages, right?</em>
+<h2>Application</h2><ul>
+<li>The Twisted way of configuration files</li>
+
+<li>Decouple configuration from running</li></ul>
+
+<h2>Application (Example)</h2>
+<pre class="python">
+# File: finger.tpy
+from twisted.internet import app
+import fingerserver
+
+factory = fingerserver.FingerServerFactory()
+factory.setUser("moshez", "Online - Sitting at computer\n")
+factory.setUser("spiv", "Offline - Surfing the waves\n")
+application = app.Application("finger")
+application.listenTCP(79, factory)
+</pre>
+
+<hr />
+<em>Paige (Hell Hath No Fury, season 4) -- I am taking full responsibility for being late with the application.</em>
+<h2>twistd</h2><ul>
+<li>TWISTed Daemonizer</li>
+
+<li>Daemonizes Twisted servers</li>
+
+<li>Takes care of log files, PID files, etc.</li>
+
+<li>twistd -y finger.tpy</li>
+
+</ul>
+<hr />
+<em>Phoebe (Sleuthing With the Enemy, season 3) -- Was it some sick twisted demonic thrill?</em>
+<h2>twistd examples</h2><ul>
+<li>twistd -y finger.tpy -l /var/finger/log</li>
+
+<li>twistd -y finger.tpy --pidfile /var/run/finger.pid</li>
+
+<li>twistd -y finger.tpy --chroot /var/run</li>
+
+</ul>
+<hr />
+<em>Professor Whittlessy (Is There a Woogy In the House?, season 1) -- I use your house as an example</em>
+<h2>Writing Plugins</h2><ul>
+<li>Automatically create application configurations</li>
+
+<li>Accessible via commandline or GUI</li></ul>
+
+<h2>Writing Plugins (Example)</h2>
+<pre class="python">
+# File finger/tap.py
+from twisted.python import usage
+
+class Options(usage.Options):
+ synopsis = "Usage: mktap finger [options]"
+ optParameters = [["port", "p", 6666,"Set the port number."]]
+ longdesc = 'Finger Server'
+ users = ()
+
+ def opt_user(self, user):
+ if not '=' in user: status = "Online"
+ else: user, status = user.split('=', 1)
+ self.users += ((user, status+"\n"),)
+</pre>
+
+
+<h2>Writing Plugins (Example cont'd)</h2>
+<pre class="python">
+def updateApplication(app, config):
+ f = FingerFactory()
+ for (user, status) in config.users:
+ f.setUser(user, status)
+ app.listenTCP(int(config.opts['port']), s)
+</pre>
+<hr />
+<em>Paige (Bite Me, season 4) -- They won't join us willingly.</em>
+<h2>Writing Plugins (Example cont'd 2)</h2>
+<pre class="python">
+# File finger/plugins.tml
+register("Finger",
+ "finger.tap",
+ description="Finger Server",
+ type='tap',
+ tapname="finger")
+</pre>
+<hr />
+<em>Queen (Bite Me, season 4) -- That's what families are for.</em>
+<h2>Using mktap</h2><ul>
+<li>mktap finger --user moshez --user spiv=Offline</li>
+
+<li>twistd -f finger.tap</li>
+
+</ul>
+<hr />
+<em>Piper (Charmed and Dangerous, season 4) -- We'll use potions instead.</em>
+<h2>Delayed execution</h2><ul>
+<li>Basic interface: reactor.callLater(&lt;time&gt;, &lt;function&gt;, [&lt;arg&gt;, [&lt;arg&gt; ...]])</li>
+
+<li>reactor.callLater(10, reactor.stop)</li>
+
+<li>reactor.callLater(5, util.println, 'hello', 'world')</li>
+
+</ul>
+<hr />
+<em>Cole (Enter the Demon, season 4) -- I know, but not right now.</em>
+<h2>callLater(0,) -- An idiom</h2><ul>
+<li>Use to set up a call in next iteration of loop</li>
+
+<li>Can be used in algorithm-heavy code to let other code run</li></ul>
+
+<pre class="python">
+def calculateFact(cur, acc=1, d=None):
+ d = d or defer.Deferred()
+ if cur&lt;=1: d.callback(acc)
+ else: reactor.callLater(0, calculateFact, acc*cur, cur-1, d)
+
+calculateFact(10
+).addCallback(lambda n: (util.println(n), reactor.stop()))
+reactor.run()
+</pre>
+<hr />
+<em>Piper (Lost and Bound, season 4) -- Someone, I won't say who, has the insane notion</em>
+<h2>UNIX Domain Sockets</h2><ul>
+<li>Servers<ul><li>reactor.listenUNIX('/var/run/finger.sock', FingerWebFactory())</li>
+</ul></li>
+
+<li>Clients<ul><li>reactor.connectUNIX('/var/run/finger.sock', FingerFactory())</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Kate (Once Upon a Time, season 3) -- Fairies don't talk the same way people do.</em>
+<h2>SSL Servers</h2>
+
+<pre class="python">
+from OpenSSL import SSL
+
+class ServerContextFactory:
+
+ def getContext(self):
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file('server.pem')
+ ctx.use_privatekey_file('server.pem')
+ return ctx
+
+reactor.listenSSL(111, FingerWebFactory(), ServerContextFactory())
+</pre>
+
+<h2>SSL Clients</h2>
+
+<ul>
+<li>from twisted.internet import ssl</li>
+
+<li>reactor.connectSSL(111, 'localhost', FingerFactory(), ssl.ClientContextFactory())</li>
+</ul>
+<hr />
+<em>Natalie (Blinded By the Whitelighter, season 3) -- I mean, in private if you wouldn't mind</em>
+<h2>Running Processes</h2><ul>
+<li>A process has two outputs: stdout and stderr</li>
+
+<li>Protocol to interface with it is different</li></ul>
+
+<pre class="python">
+class Advertizer(protocol.ProcessProtocol):
+ def outReceived(self, data): print "out", `data`
+
+ def errReceived(self, data): print "error", `data`
+
+ def processEnded(self, reason): print "ended", reason
+
+reactor.spawnProcess(Advertizer(),
+ "echo", ["echo", "hello", "world"])
+</pre>
+<hr />
+<em>Prue (Coyote Piper, season 3) -- You have to know that you can talk to me</em>
+<h2>Further Reading</h2><ul>
+<li><a href="http://twistedmatrix.com/documents/">Twisted Docs</a></li>
+
+</ul>
+<hr />
+<em>Phoebe (Animal Pragmatism, season 2) -- Ooh, the girls in school are reading this.</em>
+<h2>Questions?</h2>
+<em>Piper (Something Wicca This Way Comes, season 1) -- Tell me that's not our old spirit board?</em>
+<h2>Bonus Slides</h2>
+<em>Prue (Sleuthing With the Enemy, season 3) -- All right, you start talking or we start the bonus round.</em>
+<h2>Perspective Broker</h2><ul>
+<li>Meant to be worked async</li>
+
+<li>Can transfer references or copies</li>
+
+<li>Secure (no pickles or other remote execution mechanisms)</li>
+
+<li>Lightweight (bandwidth and CPU)</li>
+
+<li>Translucent</li>
+
+</ul>
+<hr />
+<em>Paige (Charmed Again, season 4) -- I guess I just kind of feel - connected somehow.</em>
+<h2>PB Remote Control Finger (Server)</h2>
+<pre class="python">
+from twisted.spread import pb
+
+class FingerSetter(pb.Root):
+
+ def __init__(self, ff): self.ff = ff
+
+ def remote_setUser(self, name, status):
+ self.ff.setUser(name, status+"\n")
+
+ff = FingerServerFactory()
+setter = FingerSetter(ff)
+reactor.listenUNIX("/var/run/finger.control",
+ pb.BrokerFactory(setter))
+</pre>
+<hr />
+<em>Piper (Be Careful What You Witch For, season 2) -- Okay, you think you can control the power this time?</em>
+<h2>PB Remote Control Finger (Client)</h2>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import reactor
+import sys
+
+def failed(reason):
+ print "failed:", reason.value;reactor.stop()
+
+pb.getObjectAt("unix", "/var/run/finger.control", 30
+).addCallback(lambda o: o.callRemote("setUser", *sys.argv[1:3],
+).addCallbacks(lambda _: reactor.stop(), failed)
+
+reactor.run()
+</pre>
+<hr />
+<em>Leo (Be Careful What You Witch For, season 2) -- How about you just keep your arms down until you learn how to work the controls.</em>
+<h2>Perspective Broker (Trick)</h2><ul>
+<li>Add to the application something which will call reactor.stop()</li>
+
+<li>Portable (works on Windows)</li>
+
+<li>Gets around OS security limitations</li>
+
+<li>Need to add application-level security</li>
+
+<li>The docs have the answers (see 'cred')</li>
+
+</ul>
+<hr />
+<em>Piper (Lost and Bound, season 4) -- They're not good or bad by themselves, it's how we use them</em>
+<h2>Perspective Broker (Authentication)</h2><ul>
+<li>pb.cred</li>
+
+<li>Perspectives</li>
+
+<li>Can get remote user with every call<ul><li>Inherit from pb.Perpsective</li>
+
+<li>Call methods perspective_&lt;name&gt;(self, remoteUser, ...)</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Piper (She's a Man, Baby, a Man!, season 2) -- Okey-Dokey. I get the point.</em>
+
+<h2>Perspective Broker - About Large Data Streams</h2>
+
+<ul>
+
+<li>Sending large (>640kb) strings is impossible -- feature, not bug.</li>
+
+<li>It stops DoSes</li>
+
+<li>Nobody would ever need...<ul><li>JokeTooOldError</li></ul></li>
+
+<li>Use twisted.spread.utils.Pager -- sends the data in managable chunks.</li>
+
+</ul>
+
+<hr />
+<em>Piper (Womb Raider, season 4) --
+Oral tradition tales of a giant whose body served as a portal to other
+dimensions.</em>
+
+<h2>Producers and Consumers</h2><ul>
+<li>Use for things like sending a big file</li>
+
+<li>A good alternative to manually reactor.callLater(0,)-ing</li>
+
+<li>See twisted.internet.interfaces.{IProducer,IConsumer}</li>
+
+</ul>
+<hr />
+<em>Phoebe (Black as Cole, season 4) -- Apparently he feeds on the remains of other demons' victims.</em>
+<h2>Threads (callInThread)</h2><ul>
+<li>Use for long running calculations</li>
+
+<li>Use for blocking calls you can't do without</li>
+
+<li>deferred = reactor.callInThread(function, arg, arg)</li>
+
+</ul>
+<hr />
+<em>Piper (The Painted World, season 2) -- There will be consequences. There always are.</em>
+<h2>Threads (callFromThread)</h2><ul>
+<li>Use from a function running in a different thread</li>
+
+<li>Always thread safe</li>
+
+<li>Interface to non-thread-safe APIs</li>
+
+<li>reactor.callFromThread(protocol.transport.write, s)</li>
+
+</ul>
+<hr />
+<em>Phoebe (Witch Trial, season 2) -- Maybe it's still in the house. Just on different plane.</em>
+
+<h2>Using ApplicationService</h2><ul>
+<li>Keep useful data...</li>
+
+<li>...or useful volatile objects</li>
+
+<li>Support start/stop notification</li>
+
+<li>Example: process monitor</li>
+
+</ul>
+<hr />
+<em>Phoebe (Marry Go Round, season 4) -- Yeah, that's just in case you need psychic services.</em>
+
+<h2>Playing With Persistence</h2><ul>
+<li>Shutdown taps are useful</li>
+
+<li>Even if you use twistd -y</li>
+
+<li>So remember<ul><li>Classes belong in modules</li>
+
+<li>Functions belong in modules</li>
+
+<li>Modifying class attributes should be avoided</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Cole (Marry Go Round, season 4) -- That Lazerus demon is a time bomb waiting to explode</em>
+</body></html>
diff --git a/doc/historic/2003/europython/webclients.html b/doc/historic/2003/europython/webclients.html
new file mode 100644
index 0000000..8a26f71
--- /dev/null
+++ b/doc/historic/2003/europython/webclients.html
@@ -0,0 +1,482 @@
+<html><head><title>Writing Web Clients</title></head><body>
+
+<h1>Writing Web Clients</h1>
+
+<h2>Web Clients -- The Tutorial</h2><ul>
+<li>Welcome</li>
+
+<li>Gimmick -- Buffy quotes</li>
+
+</ul>
+<hr />
+<em>Anya (Family, season 5) -- Thank you for coming. We value your patronage.</em>
+<h2>What Are Web Clients?</h2><ul>
+<li>Clarification: non-interactive web clients</li>
+
+<li>Special purpose</li>
+
+<li>Often, quick and dirty hacks</li>
+
+<li>Make a web page into API</li>
+
+</ul>
+<hr />
+<em>Giles (Family, season 5) -- Could we please be a little less effusive, Anya?</em>
+<h2>What Are Web Clients Useful For?</h2><ul>
+<li>Mass download</li>
+
+<li>Periodic checking</li>
+
+<li>Automating tasks<ul><li>Make a web page more friendly</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Harmony (Family, season 5) -- Aww. You're my little lamb.</em>
+<h2>Review of Modules</h2><ul>
+<li>htmllib</li>
+
+<li>sgmllib</li>
+
+<li>httplib</li>
+
+<li>urllib</li>
+
+<li>urllib2</li>
+
+<li>urlparse</li>
+
+</ul>
+<hr />
+<em>Buffy (Family, season 5) -- Your definition of narrow is impressively wide.</em>
+<h2>Modules -- htmllib</h2><ul>
+<li>Most useful for easy filtering of images</li>
+
+<li>...or links</li>
+
+<li>Other things often easier with sgmllib</li>
+
+<li>Or with re</li>
+
+<li>Or with string manipulation</li>
+
+</ul>
+<hr />
+<em>Xander (Family, season 5) -- The answer is somewhere here.</em>
+<h2>Modules -- htmllib -- idiomatic usage</h2>
+<pre>
+# For lists
+import htmllib, formatter
+
+h = htmllib.HTMLParser(formatter.NullFormatter())
+h.feed(htmlString)
+print h.anchorlist
+</pre>
+
+<hr />
+<em>Xander (Family, season 5) -- I'm helping, I'm reading, I'm quiet.</em>
+<h2>Modules -- htmllib -- idiotmatic usage (cont'd)</h2>
+<pre>
+import htmllib, formatter
+
+class IMGFinder(htmllib.HTMLParser):
+
+ def __init__(self, *args, **kw):
+ htmllib.HTMLParser.__init__(self, *args, **kw)
+ self.ims = []
+
+ def handle_image(self, src, *args): self.ims.append(src)
+
+h = IMGFinder(formatter.NullFormatter())
+h.feed(htmlString)
+print h.ims
+</pre>
+
+<hr />
+<em>Donny (Family, season 5) -- Look what I found!</em>
+<h2>Modules -- htmllib -- base</h2><ul>
+<li>Some sites use 'base' for different relative linking</li>
+
+<li>For example, Zope does</li>
+
+<li>In above examples, 'h.base' has the base</li>
+
+</ul>
+<hr />
+<em>Dawn (Family, season 5) -- This is the source of my gladness.</em>
+<h2>Modules -- htmllib -- base (example)</h2><ul>
+<li>If the page on http://example.com/foo/bar.html has a link to '../baz.html'<ul><li>It means http://example.com/baz.html</li>
+</ul></li>
+
+<li>If the original page has base='/foo/quux'<ul><li>It means http://example.com/foo/baz.html</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Riley (Family, season 5) -- Every time I think I'm getting close to you...</em>
+<h2>Modules -- urllib/urllib2</h2><ul>
+<li>High-level interface</li>
+
+<li>Treat URLs as file-like objects</li>
+
+<li>...but still allows low-level operations</li>
+
+<li>Interface largely compatible</li>
+
+</ul>
+<hr />
+<em>Glory (Family, season 5) -- I am great and I am beautiful.</em>
+<h2>Modules -- urllib/urllib2 (cont'd)</h2><ul>
+<li>Can work through object-interface</li>
+
+<li>More flexible</li>
+
+<li>Interface no longer compatible</li>
+
+<li>urllib2 better usually</li>
+
+</ul>
+<hr />
+<em>Joyce (Ted, season 2) -- He redid my entire system.</em>
+<h2>Modules -- urllib/urllib2 (examples)</h2><ul>
+<li>urllib.urlopen("http://www.yahoo.com/").read() -&gt; contents</li>
+
+<li>urllib.urlopen("http://www.yahoo.com/").info() -&gt; headers</li>
+
+<li>Same works with urllib2</li>
+
+<li>Automatically uses environment variables for proxies</li>
+
+<li>urllib2 supports proxies with authentication</li>
+
+</ul>
+<hr />
+<em>Xander (Ted, season 2) -- Yum-my!</em>
+<h2>Digression -- HTTP Overview</h2><ul>
+<li>Request/Response</li>
+
+<li>Request is command followed by headers followed by body</li>
+
+<li>Response is error code followed by headers followed by body</li>
+
+<li>No welcome message</li>
+
+</ul>
+<hr />
+<em>Tara (Family, season 5) -- ...in terms of the karmic cycle.</em>
+<h2>Example HTTP Sessions</h2><ul>
+<li>Client</li>
+</ul>
+
+<pre>
+GET /foo/bar.html HTTP/1.0
+Host: www.example.org
+&lt;blank line&gt;
+</pre>
+
+<ul><li>Server</li></ul>
+
+<pre>
+HTTP/1.0 200 OK
+Content-Type: text/html
+
+&lt;html&gt;&lt;body&gt;lalalala&lt;/body&gt;&lt;/html&gt;
+</pre>
+
+<hr />
+<em>Giles (Family, season 5) -- And you are talking about what on earth?</em>
+<h2>Modules -- httplib</h2><ul>
+<li>Low-level interface to innards of HTTP</li>
+
+<li>Absolute control</li>
+
+<li>No abstractions</li>
+
+</ul>
+<hr />
+<em>Mr. MacLay (Family, season 5) -- We know how to control her...problem.</em>
+<h2>Modules -- httplib -- example</h2><ul>
+<li>Note: usually, the Host header is important<ul><li>Virtual hosting</li>
+</ul></li></ul>
+
+<pre>
+&gt;&gt;&gt; import httplib
+&gt;&gt;&gt; h=httplib.HTTP("moshez.org")
+&gt;&gt;&gt; h.putrequest('GET', '/')
+&gt;&gt;&gt; h.putheader('Host', 'moshez.org')
+&gt;&gt;&gt; h.endheaders()
+&gt;&gt;&gt; h.getreply()
+(200, 'OK', &lt;mimetools.Message instance at 0x81220dc&gt;)
+&gt;&gt;&gt; h.getfile().read(10)
+"&lt;HTML&gt;\n&lt;HE"
+</pre>
+<hr />
+<em>Anya (Family, season 5) -- ...and it was fun!</em>
+<h2>Modules -- urlparse</h2><ul>
+<li>urlparse.urljoin -- like os.path.join for URLs</li>
+
+<li>For path manipulation<ul><li>urlparse.urlsplit</li>
+
+<li>urlparse.urlunsplit</li>
+</ul></li>
+
+</ul>
+<hr />
+<em>Buffy (Family, season 5) -- You know what, you guys, just leave it here.</em>
+<h2>Downloading Dilbert</h2>
+<pre>
+import urllib2, re
+
+URL = 'http://www.dilbert.com/'
+f = urllib2.urlopen(URL)
+s = f.read()
+href = re.compile('&lt;a href="(/comics/.*?/dilbert.*?gif)"&gt;')
+m = href.search(value)
+f = urllib2.urlretrieve(urlparse.urljoin(URL, m.group(1)),
+ "dilbert.gif")
+</pre>
+<hr />
+<em>Tara (Family, season 5) -- That was funny if you [...] are a complete dork.</em>
+<h2>Downloading Dark Angel Transcripts</h2><ul>
+<li>Common situation of mass download</li></ul>
+
+<pre>
+import urllib2, htmllib, formatter, posixpath
+URL="http://www.darkangelfan.com/episode/"
+LINK_RE = re.compile('/trans_[0-9]+\.shtml$')
+s = urllib2.urlopen(URL).read()
+h = htmllib.HTMLParser(formatter.NullFormatter())
+h.feed(s)
+links = [urlparse.urljoin(URL, link)
+ for link in h.anchorlist if LINK_RE.search(link)]
+### -- really download --
+for link in links:
+ urllib2.urlretrieve(link, posixpath.basename(link))
+</pre>
+
+<hr />
+<em>Intern (Family, season 5) -- Yeah. That makes like five this month.</em>
+<h2>Downloading Dark Angel Transcripts (select)</h2>
+
+<pre>
+class Downloader:
+
+ def __init__(self, fin, fout):
+ self.fin, self.fout, self.fileno = fin, fout, fin.fileno
+
+ def read(self):
+ buf = self.fin.read(4096)
+ if not buf:
+ for f in [self.fout, self.fin]: f.close()
+ return 1
+ self.fout.write(buf)
+</pre>
+<hr />
+<em>Joyce (Ted, season 2) -- I've been looking for the right moment.</em>
+<h2>Downloading Dark Angel Transcripts (select, cont'd)</h2><ul>
+<li>Same code up to 'really download'</li></ul>
+
+<pre>
+downloaders = [Downloader(urllib2.urlopen(link),
+ open(posixpath.basename(link), 'wb'))
+ for link in links]
+while downloaders:
+ toRead = select.select(None, [downloaders], [], [])
+ for downloader in toRead:
+ if downloader.read():
+ downloaders.remove(downloader)
+</pre>
+<hr />
+<em>Buffy (Family, season 5) -- Tara's damn birthday is just one too many things for me to worry about.</em>
+<h2>Downloading Dark Angel Transcripts (threads)</h2><ul>
+<li>Bare bones example</li></ul>
+
+<pre>
+import threading
+
+for link in links:
+ Thread(target=urllib2.urlretrieve,
+ args=(link,posixpath.basename(link)))
+</pre>
+<hr />
+<em>Buffy (Ted, season 2) -- Sounds like fun.</em>
+<h2>Digression - twisted.web.client</h2><ul>
+<li>Part of the Twisted networking framework</li>
+
+<li>High level interface to HTTP client</li>
+
+<li>Completely asynchronous</li>
+
+<li>Reports results via callbacks</li>
+
+<li>client.getpage("http://www.yahoo.com").addCallbacks(gotResult, gotError)</li>
+
+</ul>
+<hr />
+<em>Buffy (Ted, season 2) -- You're supposed to use your powers for good!</em>
+<h2>Downloading Dark Angel Transcripts (web.client)</h2>
+<pre>
+from twisted.web import client
+from twisted.internet import import reactor, defer
+
+defer.DeferredList(
+[client.downloadPage(link, posixpath.basename(link))
+ for link in links]).addBoth(lambda _: reactor.stop())
+reactor.run()
+</pre>
+<hr />
+<em>Ted (Ted, season 2) -- You don't have to worry about anything.</em>
+<h2>HTTP Authentication</h2><ul>
+<li>Client attempts to connect</li>
+
+<li>Server sends back a 401 (please authenticate)</li>
+
+<li>Client sends same request back -- with auth tokens</li>
+
+<li>Only HTTP Basic authentication widely supported</li>
+
+<li>Client can send auth tokens on more requests automatically</li>
+
+</ul>
+<hr />
+<em>Buffy (Ted, season 2) -- Ummm... Who are these people?</em>
+<h2>HTTP Authentication - manually</h2><ul>
+<li>In HTTP, authentication is a header</li>
+
+<li>Base authentication is sending username and password</li>
+</ul>
+<pre>
+user = 'moshez'
+password = 's3kr1t'
+import httplib
+h=httplib.HTTP("localhost")
+h.putrequest('GET', '/protected/stuff.html')
+h.putheader('Authorization',
+ base64.encodestring(user+":"+password).strip())
+h.endheaders()
+h.getreply()
+print h.getfile().read()
+</pre>
+<hr />
+<em>Tara (Family, season 5) -- And, uh, these are my-my friends.</em>
+<h2>HTTP Authentication - urllib2</h2><ul>
+<li>Can read username/password from URL</li>
+
+<li>urllib2.urlopen("http://moshez:s3krit@example.com"
+ "/protected/stuff.html")</li>
+
+</ul>
+<hr />
+<em>Xander (Ted, season 2) -- I am really jinxing the hell out of us.</em>
+<h2>Further Reading</h2><ul>
+<li>htmllib docs <a href="http://www.python.org/doc/current/lib/module-htmllib.html">http://www.python.org/doc/current/lib/module-htmllib.html</a></li>
+
+<li>sgmllib docs<a href="http://www.python.org/doc/current/lib/module-sgmllib.html">http://www.python.org/doc/current/lib/module-sgmllib.html</a></li>
+
+<li>urllib docs<a href="http://www.python.org/doc/current/lib/module-urllib.html">http://www.python.org/doc/current/lib/module-urllib.html</a></li>
+
+<li>urllib2 docs<a href="http://www.python.org/doc/current/lib/module-urllib2.html">http://www.python.org/doc/current/lib/module-urllib2.html</a></li>
+
+<li>httplib docs<a href="http://www.python.org/doc/current/lib/module-httplib.html">http://www.python.org/doc/current/lib/module-httplib.html</a></li>
+
+<li>re docs<a href="http://www.python.org/doc/current/lib/module-re.html">http://www.python.org/doc/current/lib/module-re.html</a></li>
+
+<li>HTTP RFC<a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http://www.w3.org/Protocols/rfc2616/rfc2616.html</a></li>
+
+<li>W3C HTML Page<a href="http://www.w3.org/MarkUp/">http://www.w3.org/MarkUp/</a></li>
+
+<li>Twisted<a href="http://twistedmatrix.com">http://twistedmatrix.com</a></li>
+
+</ul>
+<hr />
+<em>Willow (Ted, season 2) -- 'Book-cracker Buffy', it's kind of her nickname.</em>
+<h2>Questions?</h2>
+<em>Buffy (Family, season 5) -- I let you come, now sit down and look studious.</em>
+<h2>Bonus Slides</h2>
+<em>Tara (Family, season 5) -- You always make me feel special.</em>
+
+<h2>Cookies</h2><ul>
+<li>Carry state from one page to another</li>
+
+<li>Server sends header: Set-Cookie</li>
+
+<li>Client sends on later requests header: Cookie</li>
+
+</ul>
+<hr />
+<em>Ted (Ted, season 2) -- Who's up for dessert? I made chocolate-chip cookies!</em>
+<h2>urllib2 cookies</h2><ul>
+<li>Unfortunately, no automatic cookie jar support</li>
+
+<li>Can manually use .info() to read cookies...</li>
+
+<li>...and the Request() API to send them to the server</li>
+
+</ul>
+<hr />
+<em>Joyce (Ted, season 2) -- Mm! Buffy, you've got to try one of these!</em>
+<h2>Logging Into Advogato</h2>
+<pre>
+
+import urllib2
+
+u = urllib2.urlopen("http://advogato.org/acct/loginsub.html",
+ urllib2.urlencode({'u': 'moshez',
+ 'pass': 'not my real pass'})
+cookie = u.info()['set-cookie']
+cookie = cookie[:cookie.find(';')]
+r = Request('http://advogato.org/diary/post.html',
+ urllib2.urlencode(
+ {'entry': open('entry').read(), 'post': 'Post'}),
+ {'Cookie': cookie})
+urllib2.urlopen(r).read()
+</pre>
+
+<hr />
+<em>Anya (Family, season 5) -- I have a place in the world now.</em>
+<h2>On Being Nice - Robots</h2><ul>
+<li>Some sites don't want automatic crawlers</li>
+
+<li>It is up to you whether to play nice</li>
+
+<li>But you should know the rules before you break them</li>
+
+<li>Robots file -- at /robots.txt</li>
+
+</ul>
+<hr />
+<em>Willow (Ted, season 2) -- There were design features in that robot that pre-date...</em>
+<h2>Using robotparser</h2>
+<pre>
+import robotparser
+rp = robotparser.RobotFileParser()
+rp.set_url('http://www.example.com/robots.txt')
+rp.read()
+if not rp.can_fetch('', 'http://www.example.com/'):
+ sys.exit(1)
+</pre>
+
+<hr />
+<em>Buffy (Ted, season 2) -- Tell me you didn't keep any parts.</em>
+<h2>webchecker</h2><ul>
+<li>In the source distribution, in Tools/</li>
+
+<li>Understands robots.txt</li>
+
+<li>Can override which links gets chased</li>
+
+</ul>
+<hr />
+<em>Willow (Ted, season 2) -- What do you mean, check him out?</em>
+<h2>websucker</h2><ul>
+<li>In the source distribution, in Tools/</li>
+
+<li>Uses webchecker as a module</li>
+
+<li>Saves the pages it downloads</li>
+
+</ul>
+<hr />
+<em>Buffy (Ted, season 2) -- Find out his secrets, hack into his life.</em>
+
+</body></html>
diff --git a/doc/historic/2003/haifux/haifux.html b/doc/historic/2003/haifux/haifux.html
new file mode 100644
index 0000000..255178c
--- /dev/null
+++ b/doc/historic/2003/haifux/haifux.html
@@ -0,0 +1,2235 @@
+<html><head><title>Evolution of Finger</title></head><body>
+<h1>Evolution of Finger</h1>
+
+<h2>Refuse Connections</h2>
+
+<pre>
+from twisted.internet import reactor
+reactor.run()
+</pre>
+
+<p>Here, we just run the reactor. Nothing at all will happen,
+until we interrupt the program. It will not consume (almost)
+no CPU resources. Not very useful, perhaps -- but this
+is the skeleton inside which the Twisted program
+will grow.</p>
+
+<h2>Do Nothing</h2>
+
+<pre>
+from twisted.internet import protocol, reactor
+class FingerProtocol(protocol.Protocol):
+ pass
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>Here, we start listening on port 1079 [which is supposed to be
+a reminder that eventually, we want to run on port 79, the port
+the finger server is supposed to run on. We define a protocol which
+does not respond to any events. Thus, connections to 1079 will
+be accepted, but the input ignored.</p>
+
+<h2>Drop Connections</h2>
+
+<pre>
+from twisted.internet import protocol, reactor
+class FingerProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>Here we add to the protocol the ability to respond to the
+event of beginning a connection -- by terminating it.
+Perhaps not an interesting behaviour, but it is already
+not that far from behaving according to the letter of the
+protocol. After all, there is no requirement to send any
+data to the remote connection in the standard, is there.
+The only technical problem is that we terminate the connection
+too soon. A client which is slow enough will see his send()
+of the username result in an error.</p>
+
+<h2>Read Username, Drop Connections</h2>
+
+<pre>
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>Here we make <code>FingerProtocol</code> inherit from
+<code>LineReceiver</code>, so that we get data-based events
+on a line-by-line basis. We respond to the event of receiving
+the line with shutting down the connection. Congratulations,
+this is the first standard-compliant version of the code.
+However, usually people actually expect some data about
+users to be transmitted.</p>
+
+
+<h2>Read Username, Output Error, Drop Connections</h2>
+
+<pre>
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write("No such user\r\n")
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>Finally, a useful version. Granted, the usefulness is somewhat
+limited by the fact that this version only prints out a no such
+user message. It could be used for devestating effect in honeypots,
+of course :)</p>
+
+<h2>Output From Empty Factory</h2>
+
+<pre>
+# Read username, output from empty factory, drop connections
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write(self.factory.getUser(user)+"\r\n")
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def getUser(self, user): return "No such user"
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>The same behaviour, but finally we see what usefuleness the
+factory has: as something that does not get constructed for
+every connection, it can be in charge of the user database.
+In particular, we won't have to change the protocol if
+the user database backend changes.</p>
+
+<h2>Output from Non-empty Factory</h2>
+
+<pre>
+# Read username, output from non-empty factory, drop connections
+from twisted.internet import protocol, reactor
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write(self.factory.getUser(user)+"\r\n")
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs): self.users = kwargs
+ def getUser(self, user):
+ return self.users.get(user, "No such user")
+reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
+reactor.run()
+</pre>
+
+<p>Finally, a really useful finger database. While it does not
+supply information about logged in users, it could be used to
+distribute things like office locations and internal office
+numbers. As hinted above, the factory is in charge of keeping
+the user database: note that the protocol instance has not
+changed. This is starting to look good: we really won't have
+to keep tweaking our protocol.</p>
+
+<h2>Use Deferreds</h2>
+
+<pre>
+# Read username, output from non-empty factory, drop connections
+# Use deferreds, to minimize synchronicity assumptions
+from twisted.internet import protocol, reactor, defer
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs): self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
+reactor.run()
+</pre>
+
+<p>But, here we tweak it just for the hell of it. Yes, while the
+previous version worked, it did assume the result of getUser is
+always immediately available. But what if instead of an in memory
+database, we would have to fetch result from a remote Oracle?
+Or from the web? Or, or...</p>
+
+<h2>Run 'finger' Locally</h2>
+
+<pre>
+# Read username, output from factory interfacing to OS, drop connections
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def getUser(self, user):
+ return utils.getProcessOutput("finger", [user])
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>...from running a local command? Yes, this version (safely!) runs
+finger locally with whatever arguments it is given, and returns the
+standard output. This will do exactly what the standard version
+of the finger server does -- without the need for any remote buffer
+overflows, as the networking is done safely.</p>
+
+<h2>Read Status from the Web</h2>
+
+<pre>
+# Read username, output from factory interfacing to web, drop connections
+from twisted.internet import protocol, reactor, defer, utils
+from twisted.protocols import basic
+from twisted.web import client
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, prefix): self.prefix=prefix
+ def getUser(self, user):
+ return client.getPage(self.prefix+user)
+reactor.listenTCP(1079, FingerFactory(prefix='http://livejournal.com/~'))
+reactor.run()
+</pre>
+
+<p>The web. That invention which has infiltrated homes around the
+world finally gets through to our invention. Here we use the built-in
+Twisted web client, which also returns a deferred. Finally, we manage
+to have examples of three different database backends, which do
+not change the protocol class. In fact, we will not have to change
+the protocol again until the end of this talk: we have achieved,
+here, one truly usable class.</p>
+
+
+<h2>Use Application</h2>
+
+<pre>
+# Read username, output from non-empty factory, drop connections
+# Use deferreds, to minimize synchronicity assumptions
+# Write application. Save in 'finger.tpy'
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs): self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+application = app.Application('finger', uid=1, gid=1)
+application.listenTCP(79, FingerFactory(moshez='Happy and well'))
+</pre>
+
+<p>Up until now, we faked. We kept using port 1079, because really,
+who wants to run a finger server with root privileges? Well, the
+common solution is "privilege shedding": after binding to the network,
+become a different, less privileged user. We could have done it ourselves,
+but Twisted has a builtin way to do it. Create a snippet as above,
+defining an application object. That object will have uid and gid
+attributes. When running it (later we will see how) it will bind
+to ports, shed privileges and then run.</p>
+
+<h2>twistd</h2>
+
+<pre>
+root% twistd -ny finger.tpy # just like before
+root% twistd -y finger.tpy # daemonize, keep pid in twistd.pid
+root% twistd -y finger.tpy --pidfile=finger.pid
+root% twistd -y finger.tpy --rundir=/
+root% twistd -y finger.tpy --chroot=/var
+root% twistd -y finger.tpy -l /var/log/finger.log
+root% twistd -y finger.tpy --syslog # just log to syslog
+root% twistd -y finger.tpy --syslog --prefix=twistedfinger # use given prefix
+</pre>
+
+<p>This is how to run "Twisted Applications" -- files which define an
+'application'. twistd (TWISTed Daemonizer) does everything a daemon
+can be expected to -- shuts down stdin/stdout/stderr, disconnects
+from the terminal and can even change runtime directory, or even
+the root filesystems. In short, it does everything so the Twisted
+application developer can concentrate on writing his networking code.</p>
+
+<h2>Setting Message By Local Users</h2>
+
+<pre>
+# But let's try and fix setting away messages, shall we?
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs): self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class FingerSetterFactory(protocol.ServerFactory):
+ def __init__(self, ff): self.setUser = self.ff.users.__setitem__
+ff = FingerFactory(moshez='Happy and well')
+fsf = FingerSetterFactory(ff)
+application = app.Application('finger', uid=1, gid=1)
+application.listenTCP(79, ff)
+application.listenTCP(1079, fsf, interface='127.0.0.1')
+</pre>
+
+<p>Now that port 1079 is free, maybe we can run on it a different
+server, one which will let people set their messages. It does
+no access control, so anyone who can login to the machine can
+set any message. We assume this is the desired behaviour in
+our case. Testing it can be done by simply:
+</p>
+
+<pre>
+% nc localhost 1079
+moshez
+Giving a talk now, sorry!
+^D
+</pre>
+
+<h2>Use Services to Make Dependencies Sane</h2>
+
+<pre>
+# Fix asymmetry
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class FingerService(app.ApplicationService):
+ def __init__(self, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args)
+ self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+ def getFingerSetterFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.setUser = FingerSetterProtocol, self.users.__setitem__
+ return f
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService(application, 'finger', moshez='Happy and well')
+application.listenTCP(79, f.getFingerFactory())
+application.listenTCP(1079, f.getFingerSetterFactory(), interface='127.0.0.1')
+</pre>
+
+<p>The previous version had the setter poke at the innards of the
+finger factory. It's usually not a good idea: this version makes
+both factories symmetric by making them both look at a single
+object. Services are useful for when an object is needed which is
+not related to a specific network server. Here, we moved all responsibility
+for manufacturing factories into the service. Note that we stopped
+subclassing: the service simply puts useful methods and attributes
+inside the factories. We are getting better at protocol design:
+none of our protocol classes had to be changed, and neither will
+have to change until the end of the talk.</p>
+
+<h2>Read Status File</h2>
+
+<pre>
+# Read from file
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class FingerService(app.ApplicationService):
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, f.getFingerFactory())
+</pre>
+
+<p>This version shows how, instead of just letting users set their
+messages, we can read those from a centrally managed file. We cache
+results, and every 30 seconds we refresh it. Services are useful
+for such scheduled tasks.</p>
+
+<h2>Announce on Web, Too</h2>
+
+<pre>
+# Read from file, announce on the web!
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic
+from twisted.web import resource, server, static
+import cgi
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class FingerService(app.ApplicationService):
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+ def getResource(self):
+ r = resource.Resource()
+ r.getChild = (lambda path, request:
+ static.Data('text/html',
+ '&lt;h1>%s&lt;/h1>&lt;p>%s&lt;/p>' %
+ tuple(map(cgi.escape,
+ [path,self.users.get(path, "No such user")]))))
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, f.getFingerFactory())
+application.listenTCP(80, server.Site(f.getResource()))
+</pre>
+
+<p>The same kind of service can also produce things useful for
+other protocols. For example, in twisted.web, the factory
+itself (the site) is almost never subclassed -- instead,
+it is given a resource, which represents the tree of resources
+available via URLs. That hierarchy is navigated by site,
+and overriding it dynamically is possible with getChild.</p>
+
+<h2>Announce on IRC, Too</h2>
+
+<pre>
+# Read from file, announce on the web, irc
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.web import resource, server, static
+import cgi
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class IRCReplyBot(irc.IRCClient):
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ self.factory.getUser(msg
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m: self.msg(user, m))
+class FingerService(app.ApplicationService):
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+ def getResource(self):
+ r = resource.Resource()
+ r.getChild = (lambda path, request:
+ static.Data('text/html',
+ '&lt;h1>%s&lt;/h1>&lt;p>%s&lt;/p>' %
+ tuple(map(cgi.escape,
+ [path,self.users.get(path, "No such user")]))))
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser
+ return f
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, f.getFingerFactory())
+application.listenTCP(80, server.Site(f.getResource()))
+application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
+</pre>
+
+<p>This is the first time there is client code. IRC clients often
+act a lot like servers: responding to events form the network.
+The reconnecting client factory will make sure that severed links
+will get re-established, with intelligent tweaked exponential
+backoff algorithms. The irc client itself is simple: the only
+real hack is getting the nickname from the factory in connectionMade.</p>
+
+
+
+<h2>Add XML-RPC Support</h2>
+
+<pre>
+# Read from file, announce on the web, irc, xml-rpc
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.web import resource, server, static, xmlrpc
+import cgi
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.factory.getUser(user
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m:
+ (self.transport.write(m+"\r\n"),self.transport.loseConnection()))
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self): self.lines = []
+ def lineReceived(self, line): self.lines.append(line)
+ def connectionLost(self): self.factory.setUser(*self.lines)
+class IRCReplyBot(irc.IRCClient):
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ self.factory.getUser(msg
+ ).addErrback(lambda _: "Internal error in server"
+ ).addCallback(lambda m: self.msg(user, m))
+class FingerService(app.ApplicationService):
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+ def getResource(self):
+ r = resource.Resource()
+ r.getChild = (lambda path, request:
+ static.Data('text/html',
+ '&lt;h1>%s&lt;/h1>&lt;p>%s&lt;/p>' %
+ tuple(map(cgi.escape,
+ [path,self.users.get(path, "No such user")]))))
+ x = xmlrpc.XMLRPRC()
+ x.xmlrpc_getUser = self.getUser
+ r.putChild('RPC2.0', x)
+ return r
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser
+ return f
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, f.getFingerFactory())
+application.listenTCP(80, server.Site(f.getResource()))
+application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
+</pre>
+
+<p>In Twisted, XML-RPC support is handled just as though it was
+another resource. That resource will still support GET calls normally
+through render(), but that is usually left unimplemented. Note
+that it is possible to return deferreds from XML-RPC methods.
+The client, of course, will not get the answer until the deferred
+is triggered.</p>
+
+
+<h2>Write Readable Code</h2>
+
+<pre>
+# Do everything properly
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.web import resource, server, static, xmlrpc
+import cgi
+
+def catchError(err):
+ return "Internal error in server"
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (user, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class UserStatusTree(resource.Resource):
+
+ def __init__(self, service):
+ resource.Resource.__init__(self):
+ self.service = service
+
+ def render(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ["&lt;li>&lt;a href="%s">%s&lt;/a>&lt;/li> % (user, user)
+ for user in users]
+ return '&lt;ul>'+''.join(l)+'&lt;/ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ return UserStatus(path, self.service)
+
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self):
+ self.user = user
+ self.service = service
+
+ def render(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '&lt;h1>%s&lt;/h1>'%self.user+'&lt;p>%s&lt;/p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLPRC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(app.ApplicationService):
+
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol = FingerProtocol
+ f.getUser = self.getUser
+ return f
+
+ def getResource(self):
+ r = UserStatusTree(self)
+ x = UserStatusXR(self)
+ r.putChild('RPC2.0', x)
+ return r
+
+ def getIRCBot(self, nickname):
+ f = protocol.ReconnectingClientFactory()
+ f.protocol = IRCReplyBot
+ f.nickname = nickname
+ f.getUser = self.getUser
+ return f
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, f.getFingerFactory())
+application.listenTCP(80, server.Site(f.getResource()))
+application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
+</pre>
+
+<p>The last version of the application had a lot of hacks. We avoided
+subclassing, did not support things like user listings in the web
+support, and removed all blank lines -- all in the interest of code
+which is shorter. Here we take a step back, subclass what is more
+naturally a subclass, make things which should take multiple lines
+take them, etc. This shows a much better style of developing Twisted
+applications, though the hacks in the previous stages are sometimes
+used in throw-away prototypes.</p>
+
+<h2>Write Maintainable Code</h2>
+
+<pre>
+# Do everything properly, and componentize
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc
+import cgi
+
+class IFingerService(components.Interface):
+
+ def getUser(self, user):
+ '''Return a deferred returning a string'''
+
+ def getUsers(self):
+ '''Return a deferred returning a list of strings'''
+
+class IFingerSettingService(components.Interface):
+
+ def setUser(self, user, status):
+ '''Set the user's status to something'''
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(components.Interface):
+
+ def getUser(self, user):
+ """Return a deferred returning a string""""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string""""
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerFactory,
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService, IFingerService)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(components.Interface):
+
+ def setUser(self, user, status):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string"""
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerSetterFactory,
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSettingService)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (user, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(components.Interface):
+
+ '''
+ @ivar nickname
+ '''
+
+ def getUser(self, user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol"""
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ __implements__ = IIRCClientFactory,
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser()
+
+components.registerAdapter(IRCClientFactoryFromService, IFingerService)
+
+class UserStatusTree(resource.Resource):
+
+ def __init__(self, service):
+ resource.Resource.__init__(self):
+ self.putChild('RPC2.0', UserStatusXR(self.service))
+ self.service = service
+
+ def render(self, request):
+ d = self.service.getUsers()
+ def formatUsers(users):
+ l = ["&lt;li>&lt;a href="%s">%s&lt;/a>&lt;/li> % (user, user)
+ for user in users]
+ return '&lt;ul>'+''.join(l)+'&lt;/ul>'
+ d.addCallback(formatUsers)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+ def getChild(self, path, request):
+ return UserStatus(path, self.service)
+
+components.registerAdapter(UserStatusTree, IFingerService)
+
+class UserStatus(resource.Resource):
+
+ def __init__(self, user, service):
+ resource.Resource.__init__(self):
+ self.user = user
+ self.service = service
+
+ def render(self, request):
+ d = self.service.getUser(self.user)
+ d.addCallback(cgi.escape)
+ d.addCallback(lambda m:
+ '&lt;h1>%s&lt;/h1>'%self.user+'&lt;p>%s&lt;/p>'%m)
+ d.addCallback(request.write)
+ d.addCallback(lambda _: request.finish())
+ return server.NOT_DONE_YET
+
+
+class UserStatusXR(xmlrpc.XMLPRC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(app.ApplicationService):
+
+ __implements__ = IFingerService,
+
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+</pre>
+
+<p>In the last version, the service class was three times longer than
+any other class, and was hard to understand. This was because it turned
+out to have multiple responsibilities. It had to know how to access
+user information, by scheduling a reread of the file ever half minute,
+but also how to display itself in a myriad of protocols. Here, we
+used the component-based architecture that Twisted provides to achieve
+a separation of concerns. All the service is responsible for, now,
+is supporting getUser/getUsers. It declares its support via the
+__implements__ keyword. Then, adapters are used to make this service
+look like an appropriate class for various things: for supplying
+a finger factory to listenTCP, for supplying a resource to site's
+constructor, and to provide an IRC client factory for connectTCP.
+All the adapters use are the methods in FingerService they are
+declared to use: getUser/getUsers. We could, of course,
+skipped the interfaces and let the configuration code use
+things like FingerFactoryFromService(f) directly. However, using
+interfaces provides the same flexibility inheritance gives: future
+subclasses can override the adapters.</p>
+
+
+
+<h2>Advantages of Latest Version</h2>
+
+<ul>
+<li>Readable -- each class is short</li>
+<li>Maintainable -- each class knows only about interfaces</li>
+<li>Dependencies between code parts are minimized</li>
+<li>Example: writing a new IFingerService is easy</li>
+</ul>
+
+<pre>
+class MemoryFingerService(app.ApplicationService):
+ __implements__ = IFingerService, IFingerSetterService
+
+ def __init__(self, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args)
+ self.users = kwargs
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+ def setUser(self, user, status):
+ self.users[user] = status
+
+application = app.Application('finger', uid=1, gid=1)
+# New constructor call
+f = MemoryFingerService(application, 'finger', moshez='Happy and well')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+# New: run setter too
+application.listenTCP(1079, IFingerSetterFactory(f), interface='127.0.0.1')
+</pre>
+
+<p>Here we show just how convenient it is to implement new backends
+when we move to a component based architecture. Note that here
+we also use an interface we previously wrote, FingerSetterFactory,
+by supporting one single method. We manage to preserve the service's
+ignorance of the network.</p>
+
+<h2>Another Backend</h2>
+
+<pre>
+class LocalFingerService(app.ApplicationService):
+ __implements__ = IFingerService
+
+ def getUser(self, user):
+ return utils.getProcessOutput("finger", [user])
+
+ def getUsers(self):
+ return defer.succeed([])
+
+application = app.Application('finger', uid=1, gid=1)
+f = LocalFingerService(application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+</pre>
+
+<p>We have already wrote this, but now we get more for less work:
+the network code is completely separate from the backend.</p>
+
+<h2>Yet Another Backend: Doing the Standard Thing</h2>
+
+<pre>
+import pwd
+
+class LocalFingerService(app.ApplicationService):
+ __implements__ = IFingerService
+
+ def getUser(self, user):
+ try:
+ entry = pwd.getpwnam(user)
+ except KeyError:
+ return "No such user"
+ try:
+ f=file(os.path.join(entry[5],'.plan'))
+ except (IOError, OSError):
+ return "No such user"
+ data = f.read()
+ f.close()
+ return data
+
+ def getUsers(self):
+ return defer.succeed([])
+
+application = app.Application('finger', uid=1, gid=1)
+f = LocalFingerService(application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+</pre>
+
+<p>Not much to say about that, except to indicate that by now we
+can be churning out backends like crazy. Feel like doing a backend
+for advogato, for example? Dig out the XML-RPC client support Twisted
+has, and get to work!</p>
+
+
+<h2>Aspect Oriented Programming</h2>
+
+<ul>
+<li>This is an example...</li>
+<li>...with something actually useful...</li>
+<li>...not logging and timing.</li>
+<li>Write less code, have less dependencies!</li>
+</ul>
+
+<h2>Use Woven</h2>
+
+<pre>
+# Do everything properly, and componentize
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from twisted.web.woven import page, widget
+import cgi
+
+class IFingerService(components.Interface):
+
+ def getUser(self, user):
+ '''Return a deferred returning a string'''
+
+ def getUsers(self):
+ '''Return a deferred returning a list of strings'''
+
+class IFingerSettingService(components.Interface):
+
+ def setUser(self, user, status):
+ '''Set the user's status to something'''
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(components.Interface):
+
+ def getUser(self, user):
+ """Return a deferred returning a string""""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string""""
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerFactory,
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService, IFingerService)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(components.Interface):
+
+ def setUser(self, user, status):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string"""
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerSetterFactory,
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSettingService)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (user, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(components.Interface):
+
+ '''
+ @ivar nickname
+ '''
+
+ def getUser(self, user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol"""
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ __implements__ = IIRCClientFactory,
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser()
+
+components.registerAdapter(IRCClientFactoryFromService, IFingerService)
+
+
+class UsersModel(model.MethodModel):
+
+ def __init__(self, service):
+ self.service = service
+
+ def wmfactory_users(self):
+ return self.service.getUsers()
+
+components.registerAdapter(UsersModel, IFingerService)
+
+class UserStatusTree(page.Page):
+
+ template = """&lt;html>&lt;head>&lt;title>Users&lt;/title>&lt;head>&lt;body>
+ &lt;h1>Users&lt;/h1>
+ &lt;ul model="users" view="List">
+ &lt;li pattern="listItem" />&lt;a view="Link" model="."
+ href="dummy">&lt;span model="." view="Text" />&lt;/a>
+ &lt;/ul>&lt;/body>&lt;/html>"""
+
+ def initialize(self, **kwargs):
+ self.putChild('RPC2.0', UserStatusXR(self.model.service))
+
+ def getDynamicChild(self, path, request):
+ return UserStatus(user=path, service=self.model.service)
+
+components.registerAdapter(UserStatusTree, IFingerService)
+
+
+class UserStatus(page.Page):
+
+ template='''&lt;html>&lt;head>&lt;title view="Text" model="user"/>&lt;/heaD>
+ &lt;body>&lt;h1 view="Text" model="user"/>
+ &lt;p mode="status" view="Text" />
+ &lt;/body>&lt;/html>'''
+
+ def initialize(self, **kwargs):
+ self.user = kwargs['user']
+ self.service = kwargs['service']
+
+ def wmfactory_user(self):
+ return self.user
+
+ def wmfactory_status(self):
+ return self.service.getUser(self.user)
+
+class UserStatusXR(xmlrpc.XMLPRC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class FingerService(app.ApplicationService):
+
+ __implements__ = IFingerService,
+
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+</pre>
+
+<p>Here we convert to using Woven, instead of manually
+constructing HTML snippets. Woven is a sophisticated web templating
+system. Its main features are to disallow any code inside the HTML,
+and transparent integration with deferred results.</p>
+
+<h2>Use Perspective Broker</h2>
+
+<pre>
+# Do everything properly, and componentize
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from twisted.web.woven import page, widget
+from twisted.spread import pb
+import cgi
+
+class IFingerService(components.Interface):
+
+ def getUser(self, user):
+ '''Return a deferred returning a string'''
+
+ def getUsers(self):
+ '''Return a deferred returning a list of strings'''
+
+class IFingerSettingService(components.Interface):
+
+ def setUser(self, user, status):
+ '''Set the user's status to something'''
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(components.Interface):
+
+ def getUser(self, user):
+ """Return a deferred returning a string""""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string""""
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerFactory,
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService, IFingerService)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(components.Interface):
+
+ def setUser(self, user, status):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string"""
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerSetterFactory,
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSettingService)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (user, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(components.Interface):
+
+ '''
+ @ivar nickname
+ '''
+
+ def getUser(self, user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol"""
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ __implements__ = IIRCClientFactory,
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser()
+
+components.registerAdapter(IRCClientFactoryFromService, IFingerService)
+
+
+class UsersModel(model.MethodModel):
+
+ def __init__(self, service):
+ self.service = service
+
+ def wmfactory_users(self):
+ return self.service.getUsers()
+
+components.registerAdapter(UsersModel, IFingerService)
+
+class UserStatusTree(page.Page):
+
+ template = """&lt;html>&lt;head>&lt;title>Users&lt;/title>&lt;head>&lt;body>
+ &lt;h1>Users&lt;/h1>
+ &lt;ul model="users" view="List">
+ &lt;li pattern="listItem" />&lt;a view="Link" model="."
+ href="dummy">&lt;span model="." view="Text" />&lt;/a>
+ &lt;/ul>&lt;/body>&lt;/html>"""
+
+ def initialize(self, **kwargs):
+ self.putChild('RPC2.0', UserStatusXR(self.model.service))
+
+ def getDynamicChild(self, path, request):
+ return UserStatus(user=path, service=self.model.service)
+
+components.registerAdapter(UserStatusTree, IFingerService)
+
+
+class UserStatus(page.Page):
+
+ template='''&lt;html>&lt;head>&lt&lt;title view="Text" model="user"/>&lt;/heaD>
+ &lt;body>&lt;h1 view="Text" model="user"/>
+ &lt;p mode="status" view="Text" />
+ &lt;/body>&lt;/html>'''
+
+ def initialize(self, **kwargs):
+ self.user = kwargs['user']
+ self.service = kwargs['service']
+
+ def wmfactory_user(self):
+ return self.user
+
+ def wmfactory_status(self):
+ return self.service.getUser(self.user)
+
+class UserStatusXR(xmlrpc.XMLPRC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class IPerspectiveFinger(components.Interface):
+
+ def remote_getUser(self, username):
+ """return a user's status"""
+
+ def remote_getUsers(self):
+ """return a user's status"""
+
+
+class PerspectiveFingerFromService(pb.Root):
+
+ __implements__ = IPerspectiveFinger,
+
+ def __init__(self, service):
+ self.service = service
+
+ def remote_getUser(self, username):
+ return self.service.getUser(username)
+
+ def remote_getUsers(self):
+ return self.service.getUsers()
+
+components.registerAdapter(PerspectiveFingerFromService, IFingerService)
+
+
+class FingerService(app.ApplicationService):
+
+ __implements__ = IFingerService,
+
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+application.listenTCP(80, server.Site(resource.IResource(f)))
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
+</pre>
+
+<p>We add support for perspective broker, Twisted's native remote
+object protocol. Now, Twisted clients will not have to go through
+XML-RPCish contortions to get information about users.</p>
+
+<h2>Support HTTPS</h2>
+
+<pre>
+# Do everything properly, and componentize
+from twisted.internet import protocol, reactor, defer, app
+from twisted.protocols import basic, irc
+from twisted.python import components
+from twisted.web import resource, server, static, xmlrpc, microdom
+from twisted.web.woven import page, widget
+from twisted.spread import pb
+from OpenSSL import SSL
+import cgi
+
+class IFingerService(components.Interface):
+
+ def getUser(self, user):
+ '''Return a deferred returning a string'''
+
+ def getUsers(self):
+ '''Return a deferred returning a list of strings'''
+
+class IFingerSettingService(components.Interface):
+
+ def setUser(self, user, status):
+ '''Set the user's status to something'''
+
+def catchError(err):
+ return "Internal error in server"
+
+
+class FingerProtocol(basic.LineReceiver):
+
+ def lineReceived(self, user):
+ d = self.factory.getUser(user)
+ d.addErrback(catchError)
+ def writeValue(value):
+ self.transport.write(value)
+ self.transport.loseConnection()
+ d.addCallback(writeValue)
+
+
+class IFingerFactory(components.Interface):
+
+ def getUser(self, user):
+ """Return a deferred returning a string""""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string""""
+
+
+class FingerFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerFactory,
+
+ protocol = FingerProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser(user)
+
+components.registerAdapter(FingerFactoryFromService, IFingerService)
+
+
+class FingerSetterProtocol(basic.LineReceiver):
+
+ def connectionMade(self):
+ self.lines = []
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self):
+ if len(self.lines) == 2:
+ self.factory.setUser(*self.lines)
+
+
+class IFingerSetterFactory(components.Interface):
+
+ def setUser(self, user, status):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol returning a string"""
+
+
+class FingerSetterFactoryFromService(protocol.ServerFactory):
+
+ __implements__ = IFingerSetterFactory,
+
+ protocol = FingerSetterProtocol
+
+ def __init__(self, service):
+ self.service = service
+
+ def setUser(self, user, status):
+ self.service.setUser(user, status)
+
+
+components.registerAdapter(FingerSetterFactoryFromService,
+ IFingerSettingService)
+
+class IRCReplyBot(irc.IRCClient):
+
+ def connectionMade(self):
+ self.nickname = self.factory.nickname
+ irc.IRCClient.connectionMade(self)
+
+ def privmsg(self, user, channel, msg):
+ if user.lower() == channel.lower():
+ d = self.factory.getUser(msg)
+ d.addErrback(catchError)
+ d.addCallback(lambda m: "Status of %s: %s" % (user, m))
+ d.addCallback(lambda m: self.msg(user, m))
+
+
+class IIRCClientFactory(components.Interface):
+
+ '''
+ @ivar nickname
+ '''
+
+ def getUser(self, user):
+ """Return a deferred returning a string"""
+
+ def buildProtocol(self, addr):
+ """Return a protocol"""
+
+
+class IRCClientFactoryFromService(protocol.ClientFactory):
+
+ __implements__ = IIRCClientFactory,
+
+ protocol = IRCReplyBot
+ nickname = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def getUser(self, user):
+ return self.service.getUser()
+
+components.registerAdapter(IRCClientFactoryFromService, IFingerService)
+
+
+class UsersModel(model.MethodModel):
+
+ def __init__(self, service):
+ self.service = service
+
+ def wmfactory_users(self):
+ return self.service.getUsers()
+
+components.registerAdapter(UsersModel, IFingerService)
+
+class UserStatusTree(page.Page):
+
+ template = """&lt;html>&lt;head>&lt;title>Users&lt;/title>&lt;head>&lt;body>
+ &lt;h1>Users&lt;/h1>
+ &lt;ul model="users" view="List">
+ &lt;li pattern="listItem" />&lt;a view="Link" model="."
+ href="dummy">&lt;span model="." view="Text" />&lt;/a>
+ &lt;/ul>&lt;/body>&lt;/html>"""
+
+ def initialize(self, **kwargs):
+ self.putChild('RPC2.0', UserStatusXR(self.model.service))
+
+ def getDynamicChild(self, path, request):
+ return UserStatus(user=path, service=self.model.service)
+
+components.registerAdapter(UserStatusTree, IFingerService)
+
+class UserStatus(page.Page):
+
+ template='''&lt;html>&lt;head>&lt;title view="Text" model="user"/>&lt;/heaD>
+ &lt;body>&lt;h1 view="Text" model="user"/>
+ &lt;p mode="status" view="Text" />
+ &lt;/body>&lt;/html>'''
+
+ def initialize(self, **kwargs):
+ self.user = kwargs['user']
+ self.service = kwargs['service']
+
+ def wmfactory_user(self):
+ return self.user
+
+ def wmfactory_status(self):
+ return self.service.getUser(self.user)
+
+class UserStatusXR(xmlrpc.XMLPRC):
+
+ def __init__(self, service):
+ xmlrpc.XMLRPC.__init__(self)
+ self.service = service
+
+ def xmlrpc_getUser(self, user):
+ return self.service.getUser(user)
+
+
+class IPerspectiveFinger(components.Interface):
+
+ def remote_getUser(self, username):
+ """return a user's status"""
+
+ def remote_getUsers(self):
+ """return a user's status"""
+
+
+class PerspectiveFingerFromService(pb.Root):
+
+ __implements__ = IPerspectiveFinger,
+
+ def __init__(self, service):
+ self.service = service
+
+ def remote_getUser(self, username):
+ return self.service.getUser(username)
+
+ def remote_getUsers(self):
+ return self.service.getUsers()
+
+components.registerAdapter(PerspectiveFingerFromService, IFingerService)
+
+
+class FingerService(app.ApplicationService):
+
+ __implements__ = IFingerService,
+
+ def __init__(self, file, *args, **kwargs):
+ app.ApplicationService.__init__(self, *args, **kwargs)
+ self.file = file
+
+ def startService(self):
+ app.ApplicationService.startService(self)
+ self._read()
+
+ def _read(self):
+ self.users = {}
+ for line in file(self.file):
+ user, status = line.split(':', 1)
+ self.users[user] = status
+ self.call = reactor.callLater(30, self._read)
+
+ def stopService(self):
+ app.ApplicationService.stopService(self)
+ self.call.cancel()
+
+ def getUser(self, user):
+ return defer.succeed(self.users.get(u, "No such user"))
+
+ def getUsers(self):
+ return defer.succeed(self.users.keys())
+
+
+class ServerContextFactory:
+
+ def getContext(self):
+ """Create an SSL context.
+
+ This is a sample implementation that loads a certificate from a file
+ called 'server.pem'."""
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file('server.pem')
+ ctx.use_privatekey_file('server.pem')
+ return ctx
+
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+site = server.Site(resource.IResource(f))
+application.listenTCP(80, site)
+application.listenSSL(443, site, ServerContextFactory())
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
+</pre>
+
+<p>All we need to do to code an HTTPS site is just write a context
+factory (in this case, which loads the certificate from a certain file)
+and then use the listenSSL method. Note that one factory (in this
+case, a site) can listen on multiple ports with multiple protocols.</p>
+
+<h2>Finger Proxy</h2>
+
+<pre>
+class FingerClient(protocol.Protocol):
+
+ def connectionMade(self):
+ self.transport.write(self.factory.user+"\r\n")
+ self.buf = []
+
+ def dataReceived(self, data):
+ self.buf.append(data)
+
+ def connectionLost(self):
+ self.factory.gotData(''.join(self.buf))
+
+
+class FingerClientFactory(protocol.ClientFactory):
+
+ protocol = FingerClient
+
+ def __init__(self, user):
+ self.user = user
+ self.d = defer.Deferred()
+
+ def clientConnectionFailed(self, _, reason):
+ self.d.errback(reason)
+
+ def gotData(self, data):
+ self.d.callback(data)
+
+
+def finger(user, host, port=79):
+ f = FingerClientFactory(user)
+ reactor.connectTCP(host, port, f)
+ return f.d
+
+class ProxyFingerService(app.ApplicationService):
+ __implements__ = IFingerService
+
+ def getUser(self, user):
+ user, host = user.split('@', 1)
+ ret = finger(user, host)
+ ret.addErrback(lambda _: "Could not connect to remote host")
+ return ret
+
+ def getUsers(self):
+ return defer.succeed([])
+
+application = app.Application('finger', uid=1, gid=1)
+f = ProxyFingerService(application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+</pre>
+
+<p>Writing new clients with Twisted is much like writing new servers.
+We implement the protocol, which just gathers up all the data, and
+give it to the factory. The factory keeps a deferred which is triggered
+if the connection either fails or succeeds. When we use the client,
+we first make sure the deferred will never fail, by producing a message
+in that case. Implementing a wrapper around client which just returns
+the deferred is a common pattern. While being less flexible than
+using the factory directly, it is also more convenient.</p>
+
+<h2>Organization</h2>
+
+<ul>
+<li>Code belongs in modules: everything above the <code>application=</code>
+ line.</li>
+<li>Templates belong in separate files. The templateFile attribute can be
+ used to indicate the file.</li>
+<li>The templateDirectory attribute will be used to indicate where to look
+ for the files.</li>
+</ul>
+
+<pre>
+from twisted.internet import app
+from finger import FingerService, IIRCclient, ServerContextFactory, \
+ IFingerFactory, IPerspectiveFinger
+from twisted.web import resource, server
+from twisted.spread import pb
+
+application = app.Application('finger', uid=1, gid=1)
+f = FingerService('/etc/users', application, 'finger')
+application.listenTCP(79, IFingerFactory(f))
+r = resource.IResource(f)
+r.templateDirectory = '/usr/share/finger/templates/'
+site = server.Site(r)
+application.listenTCP(80, site)
+application.listenSSL(443, site, ServerContextFactory())
+i = IIRCClientFactory(f)
+i.nickname = 'fingerbot'
+application.connectTCP('irc.freenode.org', 6667, i)
+application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
+</pre>
+
+<ul>
+<li>Seperaration between: code (module), configuration (file above),
+ presentation (templates), contents (/etc/users), deployment (twistd)</li>
+<li>Examples, early prototypes don't need that.</li>
+<li>But when writing correctly, easy to do!</li>
+</ul>
+
+<h2>Easy Configuration</h2>
+
+<p>We can also supply easy configuration for common cases</p>
+
+<pre>
+# in finger.py moudle
+def updateApplication(app, **kwargs):
+ f = FingerService(kwargs['users'], application, 'finger')
+ application.listenTCP(79, IFingerFactory(f))
+ r = resource.IResource(f)
+ r.templateDirectory = kwargs['templates']
+ site = server.Site(r)
+ app.listenTCP(80, site)
+ if kwargs.get('ssl'):
+ app.listenSSL(443, site, ServerContextFactory())
+ if kwargs.has_key('ircnick'):
+ i = IIRCClientFactory(f)
+ i.nickname = kwargs['ircnick']
+ ircServer = kwargs['ircserver']
+ application.connectTCP(ircserver, 6667, i)
+ if kwargs.has_key('pbport'):
+ application.listenTCP(int(kwargs['pbport']),
+ pb.BrokerFactory(IPerspectiveFinger(f))
+</pre>
+
+<p>And we can write simpler files now:</p>
+
+<pre>
+# simple-finger.tpy
+from twisted.internet import app
+import finger
+
+application = app.Application('finger', uid=1, gid=1)
+finger.updateApplication(application,
+ users='/etc/users',
+ templatesDirectory='/usr/share/finger/templates',
+ ssl=1,
+ ircnick='fingerbot',
+ ircserver='irc.freenode.net',
+ pbport=8889
+)
+</pre>
+
+<p>Note: the finger <em>user</em> still has ultimate power: he can use
+updateApplication, or he can use the lower-level interface if he has
+specific needs (maybe an ircserver on some other port? maybe we
+want the non-ssl webserver to listen only locally? etc. etc.)
+This is an important design principle: never force a layer of abstraction:
+allow usage of layers of abstractions.</p>
+
+<p>The pasta theory of design:</p>
+
+<ul>
+<li>Spaghetti: each piece of code interacts with every other piece of
+ code [can be implemented with GOTO, functions, objects]</li>
+<li>Lasagna: code has carefully designed layers. Each layer is, in
+ theory independent. However low-level layers usually cannot be
+ used easily, and high-level layers depend on low-level layers.</li>
+<li>Raviolli: each part of the code is useful by itself. There is a thin
+ layer of interfaces between various parts [the sauce]. Each part
+ can be usefully be used elsewhere.</li>
+<li>...but sometimes, the user just wants to order "Raviolli", so one
+ coarse-grain easily definable layer of abstraction on top of it all
+ can be useful.</li>
+</ul>
+
+<h2>Plugins</h2>
+
+<p>So far, the user had to be somewhat of a programmer to use this.
+Maybe we can eliminate even that? Move old code to
+"finger/service.py", put empty "__init__.py" and...</p>
+
+<pre>
+# finger/tap.py
+from twisted.python import usage
+from finger import service
+
+class Options(usage.Options):
+
+ optParams = [
+ ['users', 'u', '/etc/users'],
+ ['templatesDirectory', 't', '/usr/share/finger/templates'],
+ ['ircnick', 'n', 'fingerbot'],
+ ['ircserver', None, 'irc.freenode.net'],
+ ['pbport', 'p', 8889],
+ ]
+
+ optFlags = [['ssl', 's']]
+
+def updateApplication(app, config):
+ service.updateApplication(app, **config)
+</pre>
+
+<p>And register it all:</p>
+
+<pre>
+#finger/plugins.tml
+register('Finger', 'finger.tap', type='tap', tapname='finger')
+</pre>
+
+<p>And now, the following works</p>
+
+<pre>
+% mktap finger --users=/usr/local/etc/users --ircnick=moshez-finger
+% sudo twistd -f finger.tap
+</pre>
+
+<h2>OS Integration</h2>
+
+<p>If we already have the "finger" package installed, we can achieve
+easy integration:</p>
+
+<p>on Debian--</p>
+
+<pre>
+% tap2deb --unsigned -m "Foo <foo@example.com>" --type=python finger.tpy
+% sudo dpkg -i .build/*.deb
+</pre>
+
+<p>On Red Hat [or Mandrake]</p>
+
+<pre>
+% tap2rpm --type=python finger.tpy #[maybe other options needed]
+% sudo rpm -i .build/*.rpm
+</pre>
+
+<p>Will properly register configuration files, init.d sripts, etc. etc.</p>
+
+<p>If it doesn't work on your favourite OS: patches accepted!</p>
+
+<h2>Summary</h2>
+
+<ul>
+<li>Twisted is asynchronous</li>
+<li>Twisted has implementations of every useful protocol</li>
+<li>In Twisted, implementing new protocols is easy [we just did three]</li>
+<li>In Twisted, achieving tight integration of servers and clients
+ is easy.</li>
+<li>In Twisted, achieving high code usability is easy.</li>
+<li>Ease of use of Twisted follows, in a big part, from that of Python.</li>
+<li>Bonus: No buffer overflows. Ever. No matter what.</li>
+</ul>
+
+<h2>Motto</h2>
+
+<ul>
+<li>"Twisted is not about forcing. It's about mocking you when you use
+ the technology in suboptimal ways."</li>
+<li>You're not forced to use anything except the reactor...</li>
+<li>...not the protocol implementations...</li>
+<li>...not application...</li>
+<li>...not services...</li>
+<li>...not components...</li>
+<li>...not woven...</li>
+<li>...not perspective broker...</li>
+<li>...etc.</li>
+<li>But you should!</li>
+<li>Reinventing the wheel is not a good idea, especially if you form
+ some vaguely squarish lump of glue and poison and try and attach
+ it to your car.</li>
+<li>The Twisted team solved many of the problems you are likely to come
+ across...</li>
+<li>...several times...</li>
+<li>...getting it right the nth time.</li>
+</ul>
+
+
+</body></html>
diff --git a/doc/historic/2003/haifux/notes.html b/doc/historic/2003/haifux/notes.html
new file mode 100644
index 0000000..c35afa8
--- /dev/null
+++ b/doc/historic/2003/haifux/notes.html
@@ -0,0 +1,60 @@
+<html><head><title>Notes</title></head><body>
+<h1>Notes</h1>
+
+<p>[translated roughly from Hebrew]</p>
+
+<h2>Introduction</h2>
+
+<ul>
+<li>Name: Moshe Zadka</li>
+<li>Twisted developer [Debian, Python]</li>
+<li>Not:<ul>
+<li>XML talk (XML is: standarised, flexibl, internationalized)</li>
+<li>Gettysburg in Power Point</li>
+<li>Touching lots of things briefly</li>
+</ul></li>
+<ul>How to do more than one thing at once?<ul>
+<li>Fork (Apache)</li>
+<li>Thread (AOLServer)</li>
+<li>Cheat (GUI programs)</li>
+</ul></li>
+<li>Main loop calling our code.</li>
+<li>Let's develop a network program!</li>
+</ul>
+
+<h2>Discussion</h2>
+
+<ul>
+<li>What is blocking?
+<ul>
+<li>There is a UNIX concept of blocking...</li>
+<li>...which is not really relevant.</li>
+<li>Connecting to an accepting UNIX domain socket is blocking...</li>
+<li>...reading a file from NFS is not.</li>
+</ul></li>
+<li>Wait a minute: why is that interesting?<ul>
+<li>GUI -- humans (0.1s-1s)</li>
+<li>Network: connections might get refused</li>
+</ul></li>
+<li>Typical scenario: listen buffer 5, 1e6 connections/day --
+ don't dawdle for more than 0.08s</li>
+<li>These are the numbers that matter!</li>
+<li>Useful criterion: blocking==takes more than 0.01s on normal load.</li>
+<li>Depends on hardware, etc.</li>
+<li>Real world :(</li>
+<li>But a useful rule of thumb when coding.</li>
+<li>Trick: reactor.callLater(0,)</li>
+<li>Continuation-passing-style, tail-call-optimization</li>
+<li>But not pure -- not optimal</li>
+</ul>
+
+<h2>References</h2>
+
+<ul>
+<li>Plonk</li>
+<li>twistedmatrix.com</li>
+<li>mailing list</li>
+<li>irc -- #twisted</li>
+</ul>
+
+</body></html>
diff --git a/doc/historic/2003/pycon/applications/applications b/doc/historic/2003/pycon/applications/applications
new file mode 100755
index 0000000..a6c18a2
--- /dev/null
+++ b/doc/historic/2003/pycon/applications/applications
@@ -0,0 +1,257 @@
+#!/usr/bin/python
+from slides import Slide, Bullet, SubBullet, URL, Image, PRE
+from twslides import Lecture
+
+class PythonSource:
+ def __init__(self, content):
+ self.content = content
+ def toHTML(self):
+ return '<pre class="python">%s</pre>' % (self.content,)
+
+class Raw:
+ def __init__(self, content):
+ self.content = content
+ def toHTML(self):
+ return self.content + '\n'
+
+lecture = Lecture(
+ "Applications of Twisted",
+ Slide("Twisted.names",
+ Bullet("Domain Name Server", SubBullet(
+ Bullet("Authoritative"),
+ Bullet("Caching"),
+ Bullet("Other!"),
+ )),
+ Bullet("Domain Name Client"),
+ ),
+ Slide("Mostly Functional",
+ Bullet("All common records support; 22 supported total", SubBullet(
+ Bullet("A, NS, CNAME, SOA, PTR, HINFO, MX, TXT"),
+ Bullet("IPv6 records AAAA and A6"),
+ )),
+ Bullet("No DNSSEC support"),
+ Bullet("Server and Client functionality"),
+ ),
+ Slide("Rapidly Developed",
+ Bullet("One month initial development period", SubBullet(
+ Bullet("Python is good for rapid development"),
+ Bullet("Twisted handles all the boring network details"),
+ )),
+ Bullet("Easily extended", SubBullet(
+ Bullet("Doesn't choke on unrecognized record types"),
+ Bullet("Support for a new record type can be added in "
+ "just a few minutes"),
+ Bullet(PythonSource("""\
+from twisted.protocols import dns
+
+class Record_A:
+ __implements__ = (dns.IEncodable,)
+ TYPE = dns.QUERY_TYPES['A'] = 1
+
+ def __init__(self, address = '0.0.0.0'):
+ self.address = socket.inet_aton(address)
+
+ def encode(self, strio, compDict = None):
+ strio.write(self.address)
+
+ def decode(self, strio, length = None):
+ self.address = readPrecisely(strio, 4)
+"""
+ )),
+ )),
+ ),
+ Slide("Easily Configured",
+ Bullet("BIND zonefile syntax"),
+ Bullet("Python source", SubBullet(
+ Bullet(PythonSource("""\
+zone = [
+ AAAA('intarweb.us', '3ffe:b80:1886:1::1'),
+ SRV('_http._tcp.www.intarweb.us', 0, 0, 8080, 'intarweb.us'),
+ MX('intarweb.us', 10, 'mail.intarweb.us')
+]
+"""
+ )),
+ )),
+ Bullet("Twisted's mktap and twistd tools", SubBullet(
+ PRE("mktap dns --pyzone a.domain.zonefile --recursive --cache"),
+ PRE("twistd -f dns.tap"),
+ )),
+ ),
+ Slide("Client API",
+ Bullet("Asynchronous", SubBullet(
+ Bullet("All lookup functions return Deferred objects"),
+ Bullet(PythonSource(
+"""\
+import random
+from twisted.names import client
+
+def addressFor(service, protocol, domain):
+ d = client.theResolver.lookupService(
+ '_%s._%s.%s' % (service, protocol, domain)
+ )
+
+ def grabPayload((answers, authority, additional)):
+ return [r.payload for r in answers]
+
+ def randomAnswer(results):
+ if len(results) == 1 and results[0] == '.':
+ raise RuntimeException, "No service records found"
+ return random.choice(results)
+
+ return d.addCallback(grabPayload).addCallback(randomAnswer)
+"""
+ )),
+ )),
+ ),
+ Slide("Uses",
+ Bullet("Service Records", SubBullet(
+ Bullet("Potential to simplify user experience"),
+ Bullet("Not widely accessible"),
+ Bullet("Names' client API makes accessing them trivial"),
+ )),
+ ),
+ Slide("Pynfo: A Network Information 'Bot",
+ Bullet("A 'bot with the goal of integrating access to miscellaneous data inputs"),
+ ),
+ Slide("Architecture",
+ Bullet("Factories", SubBullet(
+ Bullet("Takes care of connecting to different services"),
+ Bullet("Acts as a central storage for shared data"),
+ Bullet("Currently only IRC is supported"),
+ Bullet("Planned support for web, IM, and PB interfaces")
+ )),
+ Bullet("Protocols", SubBullet(
+ Bullet("Created by Factories"),
+ Bullet("Handles all service-specific interaction"),
+ Bullet("Refers back to the factory for shared data"),
+ Bullet("Current support for IRC only")
+ )),
+ Image("pynfo-chart.png"),
+ Bullet("Separation of Factory and Protocols", SubBullet(
+ Bullet("Per-protocol data separate, per-robot data shared"),
+ Bullet("Protocols destroyed on disconnect, factory manage reconnecting"),
+ )),
+ ),
+ Slide("Plugins",
+ Bullet("Plugins are modules plus some metadata", SubBullet(
+ Bullet("A plugin name"),
+ Bullet("A plugin description"),
+ Bullet("A plugin type"),
+ Bullet("Any other data appropriate for the type"),
+ )),
+ Bullet("Initialization / Finalization hooks"),
+ Bullet("Input filtering, for behaviors like ignore"),
+ Bullet("An example", SubBullet(
+ Bullet(PythonSource("""
+from twisted.names import client
+def info_LOOKUP(bot, user, channel, query):
+ def tellUserResponse((ans, auth, add)):
+ bot.reply(user, "%s: %s" % (query, [str(a.payload) for a in ans]))
+
+ def tellUserError(failure):
+ bot.reply(user, "Host lookup failed.")
+
+ client.lookupAddress(query).addCallbacks(
+ tellUserResponse, tellUserError
+ )
+"""
+ )),
+ )),
+ ),
+ Slide("But where do they come from?",
+ Bullet("Twisted's plugin module", SubBullet(
+ Bullet(PythonSource("""\
+from twisted.python import plugin
+class InfoBotFactory:
+ ...
+ def loadPlugins(self):
+ ...
+ p = plugin.getPlugIns('infobot')
+ ....
+"""
+ )),
+ )),
+ ),
+ Slide("Persistence",
+ Bullet("Addresses to connect to and protocols to use"),
+ Bullet("Administrators, passwords, keys"),
+ Bullet("Connection statistics"),
+ Bullet("Plugins can store objects for later retrieval")
+ ),
+ Slide("Components",
+ Bullet("Shared and pluggable behavior is implemented as Adapters for Interfaces", SubBullet(
+ Bullet("IScheduler, IStorage, IAuthenticator"),
+ )),
+ Bullet("Plugins can register their own adapters for the factory", SubBullet(
+ Bullet("Gracefully add new capabilities without __class__ hacks"),
+ Bullet("Share capabilities with other plugins"),
+ Bullet("Avoids namespace collisions"),
+ )),
+ ),
+ Slide("Interaction",
+ Bullet("Commands and responses are issued through normal protocol actions"),
+ Bullet('Three levels of command "security"'),
+ Bullet("Access to some commands is unrestricted",
+ Raw("<code>"),
+ SubBullet("<exarkun> pyn: networks"),
+ SubBullet("<pyn> Connected to: oftc -> ('irc.oftc.net', 6667), fn -> ('irc.freenode.net', 6667)"),
+ Raw("</code>"),
+ ),
+ Bullet("Access to others is granted via an ACL",
+ Raw("<code>"),
+ SubBullet("<exarkun> pyn: rebuild"),
+ SubBullet("<pyn> You aren't allowed to do that."),
+ Raw("</code>"),
+ ),
+ Bullet("Still further access is granted by the possession of a secret key",
+ Raw("<code>"),
+ SubBullet("<exarkun> pyn: spill self.transport.getHost()"),
+ SubBullet("<pyn> Command queued. Challenge: BBKSkYCRCGQETx4kTmceUg==%"),
+ SubBullet("<exarkun> pyn: respond 2lwcgSVnJPzrW6Yvq7sg+g==%"),
+ SubBullet("<pyn> ('INET', '192.168.123.137', 45539)"),
+ Raw("</code>"),
+ )
+ ),
+ Slide("Network Bridging",
+ Bullet("Pass messages between networks",
+ Raw("<code>"),
+ SubBullet("<pyn> Yosomono (~fake@hostmask) has joined on efnet"),
+ SubBullet("<pyn> <Yosomono@efnet> hello"),
+ Raw("</code>"),
+ ),
+ Bullet("Requesting user information across networks",
+ Raw("<code>"),
+ SubBullet("<exarkun> pyn: whois Yosomono@efnet"),
+ SubBullet("<pyn> Hostmask: Yosomono!fake@hostmask"),
+ SubBullet("<pyn> Channels: @#python"),
+ Raw("</code>"),
+ ),
+ ),
+ Slide("Conversation Logging",
+ Bullet("The 'conversation' command", SubBullet(
+ Raw("<code>"),
+ SubBullet("<exar[con]> pyn: conversation begin PyCon example conversation"),
+ SubBullet("<pyn> Beginning tagged conversation 'PyCon example conversation'."),
+ SubBullet("<exar[con]> Hello, PyCon"),
+ SubBullet("<exar[con]> Enjoy the example!"),
+ SubBullet("<exar[con]> pyn: conversation end PyCon example conversation"),
+ SubBullet("<pyn> Ended tagged conversation 'PyCon example conversation'."),
+ Raw("</code>"),
+ )),
+ Bullet("Web interface", SubBullet(
+ URL("http://c.intarweb.us:8008/%23tanstaafl/PyCon%20example%20conversation"),
+ )),
+ Bullet("Search previous logs and add conversation tags"),
+ ),
+ Slide("Various other plugins",
+ Bullet("PyPI monitor and querying"),
+ Bullet("Network specific operations - IRC operator module"),
+ Bullet("Freshmeat and Google querying"),
+ Bullet("Link shortener"),
+ Bullet("PyMetar plugin"),
+ Bullet("Manhole"),
+ ),
+ Slide("Questions?"),
+)
+
+lecture.renderHTML(".", "applications-%d.html", css="stylesheet.css")
diff --git a/doc/historic/2003/pycon/applications/applications.html b/doc/historic/2003/pycon/applications/applications.html
new file mode 100644
index 0000000..f357d7f
--- /dev/null
+++ b/doc/historic/2003/pycon/applications/applications.html
@@ -0,0 +1,343 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html>
+ <head>
+ <title>Applications of the Twisted Framework</title>
+ <link href="stylesheet.css" type="text/css" rel="stylesheet" />
+ </head>
+ <body>
+ <h1>Applications of the Twisted Framework</h1>
+ <p>Jp Calderone</p>
+ <p>exarkun@twistedmatrix.com</p>
+
+ <h2>ABSTRACT</h2>
+
+ <p>Two projects developed using the Twisted framework are described;
+ one, Twisted.names, which is included as part of the Twisted
+ distribution, a domain name server and client API, and one, Pynfo, which
+ is packaged separately, a network information robot.</p>
+
+ <h2>Twisted (dot) Names</h2>
+
+ <h3>Motivation</h3>
+ <p>The field of domain name servers is well explored and numerous
+ strong, widely-deployed implementations of the protocol exist. DNSSEC,
+ IPv6, service location, geographical location, and many of the other DNS
+ extension proposals all have high quality support in BIND, djbdns,
+ maradns, and others. From a client's perspective, though, the landscape
+ looks a little different. APIs to perform arbitrary domain name lookups
+ are sparse. In contrast, Twisted.names presents a richly featured,
+ asynchronous client API.</p>
+
+ <h3>Names Server</h3>
+ <p><b>Names</b> is capable of operating as a fully functional domain
+ name server. It implements caching, recursive lookups, and can act as
+ the authority for an arbitrary number of domains. It is not, however, a
+ finely tuned performance machine. Responding to queries can take about
+ twice the time other domain name servers might need. It has not been
+ investigated whether this is a design limitation or merely the result of
+ an unoptimized implementation.</p>
+
+ <h3>Names Client</h3>
+ <p>As a client, <b>Names</b> provides an easy interface to every type of
+ record supported by. Looking up the MX records for a host, for example,
+ might look like this:</p>
+
+ <pre class="python">
+ def _cbMailExchange(results):
+ # Callback for MX query
+ answers = results[0]
+ print 'Mail Exchange is: ', answers
+
+ def _ebMailExchange(failure):
+ # Error callback for MX query
+ print 'Lookup failed:'
+ failure.printTraceback()
+
+ from twisted.names import client
+ d = client.lookupMailExchange('example-domain.com')
+ d.addCallbacks(_cbMailExchange, _ebMailExchange)
+ </pre>
+
+ <p>Looking up other record types is as simple as calling a different
+ <code>lookup*</code> function.</p>
+
+ <h3>Implementation</h3>
+
+ <p>As with most network software written using Twisted, the first step
+ in developing <b>Names</b> was to write the protocol support. In this
+ case, the protocol was DNS, and support was partially implemented.
+ However, it attempted to merge support for both UDP and TCP, and ended
+ up with less than optimal results. Much of this code was discarded,
+ though some of the lowest level encoding and decoding code worked well
+ and was re-used.</p>
+
+ <p>With the two protocol classes, DNSDatagramProtocol and DNSProtocol
+ (the TCP version) implemented, the next step was to write classes which
+ created the proper behavior for a domain name server. This logic was
+ put in the <code>twisted.names.server.DNSServerFactory</code> class,
+ which in turn relies on several different kind of <code>Resolver</code>s
+ to find the appropriate response to queries it receives from the
+ protocol instance.</p>
+
+ <p>The chain of execution, then, is this: a packet is received by the
+ protocol object (a <code>DNSDatagramProtocol</code> or
+ <code>DNSProtocol</code> instance); the packet is decoded by
+ <code>twisted.protocols.dns.RRHeader</code> in cooperation with one of
+ the record classes (<code>twisted.protocols.dns.Record_A</code> for
+ example); the decoded <code>twisted.protocols.dns.Query</code> object is
+ passed up to the <code>twisted.names.server.DNSServerFactory</code>,
+ which determines the query type and invokes the appropriate lookup
+ method on each of its resolver objects in turn; if an answer is found,
+ it is passed back down to the protocol instance (otherwise the
+ appropriate bit for an error condition is set), where it is encoded and
+ transmitted back to the client.</p>
+
+ <p>There are four kinds of resolvers in the current implementation. The
+ first three are authorities, caches, and recursive resolvers. They are
+ generally queried, in this order, using the fourth resolver, the "chain"
+ resolver, which simply queries the resolvers it knows about, moving on
+ to the next when any given resolver fails to produce a response, and
+ generating the proper exception when the last resolver has failed.</p>
+
+ <h3>Shortcomings</h3>
+
+ <p>There are several aspects of Twisted Names that might preclude its
+ use in "production" software. These issues stem mainly from its
+ immaturity, it being less than six months old at the writing of this
+ paper.</p>
+
+ <ul>
+ <li><p>Possibly of foremost interest to those who might use it in a
+ high-load environment, it has somewhat poor runtime performance
+ characteristics. One potential reason for this is the extensive use of
+ exceptions to signal the relatively common case of a resolver lookup
+ failing. Solutions to this problem are apparent, but an implementation
+ change has not been attempted. Until this area of its development is
+ more fully examined, it will likely not be of use in anything other than
+ for low- to mid-load tasks, or with more hardware available to it than
+ might seem reasonable.</p></li>
+
+ <li>No attempt has been made to implement DNSSEC.</li>
+
+ <li>Certain areas of the server remain out of compliance with the
+ standardized RFCs, occasionally causing undesirable behavior when
+ interacting with clients. This most frequently manifests itself as a
+ lookup which fails the first time and succeeds on subsequent attempts.
+ It is not believed that these represent architectural flaws, only small
+ oversights in areas such as the "additional processing" sections of the
+ current authority resolver implementations.</li>
+ </ul>
+ <h2>Pynfo</h2>
+
+ <h3>Motivation</h3>
+ <p>Pynfo was originally begun as a learning project to become acquainted
+ with the Twisted framework. After a brief initial development period
+ and an extended period of non-development, Pynfo was picked up again to
+ serve as a replacement for several existing robots, each with fragile
+ code bases and with designs not intended for future integration with
+ other services. After it subsumed the functions of network relaying and
+ Google searches, other desired features, which enhanced the IRC medium
+ and had not previously been considered due to the difficulty of
+ extending existing robots, were added to Pynfo, prompting the development
+ of an elementary plug-in system to further facilitate the integration
+ process.</p>
+
+ <h3>Architecture</h3>
+ <p>Pynfo performs such simple tasks as noting the last time an
+ individual spoke and querying the Google search engine, as well as
+ several more complex operations like relaying traffic between different
+ IRC networks and publishing channel logs through an HTTP interface.</p>
+
+ <p>Toward these ends, it is useful to abstract the functionality into
+ several different layers:</p>
+
+ <ul>
+ <li><p>The factory: All shared data, such as the channels a given user is
+ known to be in, the plugins currently loaded, and the addresses of servers
+ to connect to, is aggregated here. When it is necessary to make a
+ connection, the factory creates an instance of the appropriate Protocol
+ subclass, in a manner similar to this:
+
+ <pre class="python">
+ def buildProtocol(self, address):
+ for net in self.data['networks'].values():
+ if net.address == address:
+ break
+
+ proto = IRCProtocol(net)
+ self.allBots[net.alias] = proto
+ proto.factory = self
+ return proto
+ </pre>
+
+ The factory instance is created only once, and that instance persists
+ through the entire time a particular Pynfo bot operates.</p>
+ </li>
+
+ <li><p>The protocol: Each kind of service Pynfo can connect to has a
+ Protocol class associated with it, a class which handles the specifics
+ of communicating over this protocol. Unlike the factory, protocols
+ instances can be short lived and are created and destroyed as many times
+ as network connectivity demands. When a Pynfo robot shuts down and is
+ serialized to disk, all Protocol instances are destroyed and discarded,
+ to be created anew when the robot is restarted.</p>
+ </li>
+
+ <li><p>Plugins: These give Pynfo most of its functionality. From the
+ very simple logging module, which does no more than write strings to
+ disk, to the esoteric lookup module, which translates hostnames into
+ dotted-quads, to the informative dictionary module, which queries an <a
+ href="http://dict.org">online dictionary</a>, plugins come in all shapes
+ and sizes, and can be written to fill almost any niche.</p>
+ </li>
+ </ul>
+
+ <h3>Employing Components</h3>
+ <p>Twisted provides a <i>component</i> system which Pynfo relies on to
+ split up useful functionality used in different areas of the code. The
+ Interface class is the primary element in the component system, and is
+ used as a location for a semi-format definition of an API, as well as
+ documentation. Classes declare that they implement an Interface by
+ including it in their __implements__ tuple attribute. Interfaces can
+ also be added to classes by third parties using the registerAdapter()
+ function. This takes an Adapter type in addition to the interface being
+ registered and the type it is being registered for. Adapters are a
+ objects which can store additional state information and implement
+ functionality without being part of the classes that are "actually"
+ being operated upon. They, as their name suggests, adapt components to
+ conform to interfaces.</p>
+
+ <p>Components can implement interfaces themselves, or maintain a cache
+ of adapter objects for each interfaces that is requested of them. These
+ persist like any other attribute, and so state stored in adapters
+ remains associated with the component as long as that component exists, or
+ until the adapter is explicitly removed.</p>
+
+ <p>Pynfo's Factory class uses two adapters to implement two basic
+ Interfaces that many plugins find useful. The first is the IStorage
+ interface.
+
+ <pre class="python">
+ class IStorage(components.Interface):
+
+ def store(self, key, version, value):
+ """
+ Store any pickleable object
+ """
+
+ def retrieve(self, key, version):
+ """
+ Retrieve the previously stored object associated with key and
+ version
+ """
+ </pre>
+ An example usage of this interface is the PyPI plugin, which polls the
+ Python Package Index and reports updates to a configurable list of
+ outputs:
+
+ <pre class="python">
+ def init(factory):
+ global notifyChannels
+ store = factory.getComponent(interfaces.IStorage)
+ try:
+ notifyChannels = store.retrieve('pypi', __version__)
+ except error.RetrievalError:
+ notifyChannels = []
+
+ </pre>
+ <p>The module requests the component of factory which implements
+ IStorage, then attempts to load any previously stored version of
+ "notifyChannels". If none is found, it defaults to none. In the
+ finalizer below, this global is stored, using the same interfaced, to be
+ retrieved when the module is next initialized.</p>
+
+ <pre class="python">
+ def fini(factory):
+ s = factory.getComponent(interfaces.IStorage)
+ s.store('pypi', __version__, notifyChannels)
+ </pre>
+
+ The second interface allows low granularity scheduling of events:
+
+ <pre class="python">
+ class IScheduler(components.Interface):
+ MINUTELY = 60
+ HOURLY = 60 * 60
+ DAILY = 60 * 60 * 24
+ WEEKLY = 60 * 60 * 24 * 7
+
+
+ def schedule(self, period, fn, *args, **kw):
+ """
+ Cause a function to be invoked at regular intervals with the given
+ arguments.
+ """
+ </pre>
+ The Adapter which implements this interface is just as simple:
+ <pre class="python">
+ class SchedulerAdapter(components.Adapter):
+ __implements__ = (interfaces.IScheduler,)
+
+ def schedule(self, period, fn, *args, **kw):
+ from twisted.internet import reactor
+ def cycle():
+ fn(*args, **kw)
+ reactor.callLater(period, cycle)
+ reactor.callLater(period, cycle)
+ </pre>
+ </p>
+
+ <p>Implementing these interfaces as adapters using the component system
+ has two primary advantages over a simple inheritance or mixins approach.
+ First, it allows plugins to add completely new behavior to the system
+ without complex and fragile manipulation of the factory's __class__
+ attribute. This is a big win when it comes to plugins that want to
+ share new functionality with other plugins. For example, the "ignore"
+ plugin adds an IDiscriminating interface and an adapter which implements
+ it. Once this plugin is loaded, any other plugin can request the
+ component for IDiscriminating and add users to or remove users from the
+ ignore list.</p>
+
+ <h3>The Plugin Framework</h3>
+
+ <p>Before a module can be loaded and initialized as a plugin, it must be
+ located. This could be done with a simple use of
+ <code>os.listdir()</code>, or <code>__all__</code> could be set to include
+ each new plugin added. Twisted provides another way, though.</p>
+
+ <p>The <code>twisted.python.plugin</code> provides the most high-level
+ interface to the plugin system, a function called
+ <code>getPlugIns</code>. It usually takes one argument, a plugin type,
+ which is an arbitrary string used to categorize the different kinds of
+ plugins available on a system. Twisted's own "mktap" tool uses the
+ "tap" plugin type. For Pynfo, I have elected to use the "infobot"
+ string. <code>getPlugIns("infobot")</code> searches the system (by way
+ of PYTHONPATH) for files named "plugins.tml". These files contain
+ python source, and are run as such; a function, "register" is placed in
+ their namespace, and the most common action for them is to invoke this
+ function one or more times, providing information about a plugin. Here
+ is a snippet from one which Pynfo uses:</p>
+
+ <pre class="python">
+ register(
+ "Weather",
+ "Pynfo.plugins.weather",
+ description="Commands to check the weather at "
+ "various places around the world.",
+ type="infobot"
+ )
+ </pre>
+
+ <p>Any number of plugin.tml files may exist in the filesystem, allowing
+ per-user and even per-robot plugins to be installed, all without
+ modifying the Pynfo installation itself.
+
+ The second argument indicates the module which may be imported to get
+ this plugin. Pynfo traverses the resulting list, importing these modules,
+ and initializing them if necessary.</p>
+
+ </body>
+</html>
diff --git a/doc/historic/2003/pycon/applications/pynfo-chart.png b/doc/historic/2003/pycon/applications/pynfo-chart.png
new file mode 100644
index 0000000..7318b15
--- /dev/null
+++ b/doc/historic/2003/pycon/applications/pynfo-chart.png
Binary files differ
diff --git a/doc/historic/2003/pycon/conch/conch b/doc/historic/2003/pycon/conch/conch
new file mode 100755
index 0000000..00ed511
--- /dev/null
+++ b/doc/historic/2003/pycon/conch/conch
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+from slides import Slide, Bullet, SubBullet, URL
+from twslides import Lecture
+
+lecture = Lecture(
+ "Twisted Conch: SSH in Python",
+ Slide("Introduction",
+ ),
+ Slide("Other implementations (servers)",
+ Bullet("OpenSSH",
+ SubBullet(URL("http://www.openssh.org")),
+ ),
+ Bullet("FSecure SSH",
+ SubBullet(URL("http://www.f-secure.com/products/ssh/")),
+ ),
+ Bullet("LSH",
+ SubBullet(URL("http://www.lysator.liu.se/~nisse/lsh/")),
+ ),
+ ),
+ Slide("Other implementations (clients)",
+ Bullet("PuTTY",
+ SubBullet(URL("http://www.chiark.greenend.org.uk/~sgtatham/putty/")),
+ ),
+ Bullet("TeraTerm",
+ SubBullet(URL("http://www.ayera.com/teraterm/")),
+ ),
+ Bullet("MindTerm",
+ SubBullet(URL("http://www.appgate.com/mindterm/")),
+ ),
+ ),
+ Slide("Why Twisted?",
+ Bullet("Asynchronous"),
+ Bullet("Python"),
+ Bullet("High-Level"),
+ ),
+ Slide("No Forking or Threads",
+ Bullet("Forking is expensive"),
+ Bullet("Threads are complicated/expensive, esp. in Python"),
+ Bullet("Asynch means no worrying about any of that"),
+ Bullet("Makes running a session 2x as fast in Conch as in OpenSSH"),
+ ),
+ Slide("Security - No Pointers",
+ SubBullet("No buffer overflows"),
+ SubBullet("No off-by-1 errors"),
+ SubBullet("No malloc/free bugs"),
+ SubBullet("No arbitrary code execution"),
+ ),
+ Slide("Security - High Level",
+ Bullet("Strong built-in library"),
+ Bullet("Exceptions"),
+ ),
+ Slide("Security - Not Root",
+ Bullet("Limits vulnerablity in a compromise"),
+ Bullet("Allows use of process limits/etc."),
+ ),
+ Slide("Interfacing with other software",
+ Bullet("OpenSSH interacts only through separate processes",
+ SubBullet("Expensive"),
+ SubBullet("Complicated"),
+ ),
+ Bullet("Conch can interact in-process",
+ SubBullet("Faster"),
+ SubBullet("Easy integration to other Twisted and Python libraries"),
+ ),
+ ),
+ Slide("Speed",
+ Bullet("C is faster than Python"),
+ Bullet("Interpreter cost is high for the client"),
+ Bullet("FSH-style connection caching helps a bit"),
+ Bullet("Psyco helps as well"),
+ ),
+ Slide("Age",
+ Bullet("Conch is new",
+ SubBullet("First commit was July 15, 2002"),
+ ),
+ Bullet("Hasn't had a security aduit"),
+ Bullet("Shouldn't be used in security-critical systems"),
+ ),
+ Slide("Applications with Conch",
+ Bullet("Reality: MUD framework"),
+ Bullet("Insults: async. replacement for curses in Conch apps"),
+ ),
+ Slide("Future Directions",
+ Bullet("Generic authentication forwarding"),
+ Bullet("Work on applications"),
+ Bullet("Auditing of the code"),
+ Bullet("Increase speed"),
+ Bullet("SFTP/SCP"),
+ Bullet("Key Agent"),
+ Bullet("DNSSEC"),
+ ),
+ Slide("Conclusion",
+ Bullet("Working implementation in Python"),
+ Bullet("Much room for improvement"),
+ ),
+)
+
+lecture.renderHTML(".", "conch-%d.html", css="main.css")
diff --git a/doc/historic/2003/pycon/conch/conch.html b/doc/historic/2003/pycon/conch/conch.html
new file mode 100644
index 0000000..0faab7e
--- /dev/null
+++ b/doc/historic/2003/pycon/conch/conch.html
@@ -0,0 +1,165 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+<title>Twisted Conch: SSH in Python with Twisted</title>
+</head>
+
+<body>
+<h1>Twisted Conch: SSH in Python with Twisted</h1>
+
+<ul>
+<li>Paul Swartz
+ <a href="mailto:z3p@twistedmatrix.com">z3p@twistedmatrix.com</a></li>
+</ul>
+
+<h2>Introduction</h2>
+
+<p>Although it is a newcomer on the Secure Shell stage, Twisted Conch has quickly
+caught up with the two most popular free *nix implementations and the most
+popular free Windows implementation in terms of functionality. This rapid
+development time, as well as the stability and other advantages, owes much to
+Python and the Twisted networking framework.</p>
+
+<h2>Other implementations (servers)</h2>
+
+<p>Other than Conch, there are three popular server implementations. OpenSSH
+works with versions 1 and 2 of the protocol, and is the most popular on *nix
+systems. FSecure is more popular on Windows servers, and also works with both
+versions. LSH is newer, and implements version 2. All three are written in C,
+with LSH having some supporting Scheme code to generate C files.</p>
+
+<h2>Other implementations (clients)</h2>
+
+<p>On *nix, the SSH clients are provided by the server implementations (OpenSSH
+and LSH). On Windows, there are a couple of separate clients. PuTTY is the
+most popular and supports Telnet along with SSH1 and 2. TeraTerm recently
+incorporated SSH into the core: before it had been an extension module.
+MindTerm is the only implementation in this list to be written in a language
+other than C. It runs as a Java applet, allowing SSH to run on any computer
+with a JVM.</p>
+
+<h2>Why Twisted?</h2>
+
+<p>Why is Twisted ideal for this type of project? Firstly, it is an asynchronous
+library, meaning there are no worries about threading or concurrency issues.
+This means more developer time can be devoted to making the code work well,
+rather than just work. Second, Python lends itself to this kind of
+development: the code is easy to read and easy to write. Third, the Twisted
+library is high-level, so developers do not need to worry about select loops or
+callbacks. Twisted handles all of that and allows developers to concentrate on
+the code.</p>
+
+<h2>No forking/threads</h2>
+
+<p>Unlike OpenSSH, the Conch server does not fork a process for each incoming
+connection. Instead, it uses the Twisted reactor to multiplex the connections.
+The only fork done is to execute a process such as a shell, but running a shell
+is not necessary, in which case the entire protocol would be run in-process.
+One of the initial features of the server was an in-process Python interpreter
+which allowed a user to interact with the server as it was running. (It is
+currently disabled for security reasons.) Threads are only used to interface
+with synchronous libraries, such as PyPAM (Pluggable Authentication Modules
+support) or PyME (GPGME support). By not using forks or threads, the time it
+takes for the Conch server to start an SSH session is roughly half of the time
+it takes for OpenSSH. However, this does require that code in Conch be
+non-blocking, which is an obstacle for programmers not used to that style.</p>
+
+<h2>Security - No Pointers</h2>
+
+<p>OpenSSH, LSH, and PuTTY are all written in C. Many security holes are a result
+of problems with unsafe pointer usage, which is a large problem in C code.
+Many other security holes result from related issues, such as buffer overflows,
+off-by-one errors on arrays, and memory allocation/deallocation bugs. Python
+is pointer-safe, and so is not vulnerable to this class of hole. This also
+means that no arbitrary data from over-the-wire is ever run, meaning control
+always stays with the Conch server.</p>
+
+<h2>Security - High Level</h2>
+
+<p>Being written in Python provides more security than just pointer safety. The
+strong builtin library that comes with Python (including powerful data types
+like the list and dictionary) means that fewer wheels need to be reinvented.
+This limits the potential to make mistakes in implementation. Exceptions are
+another powerful tool. They centralize error handling, rather than the mix of
+methods that the C libraries use. All errors are caught and dealt with: this
+might mean that the server stops accepting connections, but it never
+compromises security.</p>
+
+<h2>Security - Not Root</h2>
+
+<p>Also, Conch does not need to run as root. In the default server, root
+privileges are used for two things: to bind to ports &lt; 1024, and to fork a
+process as a different user. If neither of these are needed, the server need
+not run as root at all. Even if they are, the server is only running as root
+for those small sections. The rest of the time, it runs under the effective
+user and group ID of the user who started the server. This limits the amount
+of damage that could be inflicted in the event of a compromise.</p>
+
+<h2>Interfacing with other software</h2>
+
+<p>OpenSSH can interact with subsystems such as SFTP only by executing a process
+to handle it. Not only is forking a process expensive, it limits the
+interaction to a generic bitstream, which leaves developers to determine how
+to interact with their users. Conch can run in the same process as other
+Python software, and is easily integrated with other Twisted servers. This
+allows for things like secure remote administration of a Twisted web server,
+encrypted communication to a Reality MUD, or secure remote object access using
+Perspective Broker. This saves the hassle and expense of forking, and allows
+Python developers to interact with Conch the way they know best: with Python.</p>
+
+<h2>Speed</h2>
+
+<p>No one can deny that compiled C is faster than Python. Some part of Conch use
+C (PyCrypto, TGMP) to speed frequent operations, but the majority of the code
+is in Python. The client suffers the most from this because of the time it
+takes to start the interpreter. Work is being done to speed up the client by
+caching connections. This does not eliminate the interpreter start-up cost,
+but it removes the cost of negotiating a new connection. This effort is
+similar to FSH (also in Python) but interacts more nicely with the SSH
+protocol. Psyco helps as well, offering a speedup of roughly 2x - 5x.</p>
+
+<h2>Age</h2>
+
+<p>As I said in the introduction, Conch is still a newcomer on the Secure Shell
+stage (The first commit for Conch was July 15, 2002.) Although Python solves
+a large class of holes, it is probable that other security holes are in the
+code. Until a full audit is conducted of Twisted and of Conch, it should not
+be used for security-critical systems.</p>
+
+<h2>Applications with Conch</h2>
+
+<p>One of the applications for Conch is with Reality, a MUD framework using
+Twisted. Conch makes it easy to allow secure connections to the MUD in
+addition or even in place of a standard Telnet connection. As problems
+such as character theft become more prevalent on the Internet, a secure
+interface becomes more important.</p>
+
+<p>More generally, work is being done on Insults, a replacement for libraries
+like Curses and S-Lang. It allows developers to write GUI code that
+interacts well with Conch and other Twisted software. Although it is in the
+initial stages of development, it shows much promise for the future.</p>
+
+<h2>Future Directions</h2>
+
+<p>There are several different directions for Conch to move in. One of the most
+interesting is system for generalized authentication forwarding. This would
+allow all authentication to be performed on a host that the user controls,
+which would help to stop vulnerabilities such as timing attacks. Second is
+more work with applications. Insults is becoming more powerful, and it will
+be interesting to see what it can be used for. Also important are auditing of
+the code and increasing the speed. These will make the code more useful in
+general, as well as improving security. Other ideas include direct support for
+SFTP/SCP, support for a key agent, and interfacing with Twisted Names to
+support DNSSEC.</p>
+
+<h2>Conclusion</h2>
+
+<p>Although it is new, Conch is a working implementation of the Secure Shell
+protocol. It is robust enough to serve as both the client and server on
+systems I and others use daily.</p>
+
+</body></html>
diff --git a/doc/historic/2003/pycon/conch/conchtalk.txt b/doc/historic/2003/pycon/conch/conchtalk.txt
new file mode 100755
index 0000000..d2552ee
--- /dev/null
+++ b/doc/historic/2003/pycon/conch/conchtalk.txt
@@ -0,0 +1,144 @@
+Introduction
+------------
+Although it is a newcomer on the Secure Shell stage, Twisted Conch has quickly
+caught up with the two most popular free *nix implementations and the most
+popular free Windows implementation in terms of functionality. This rapid
+development time, as well as the stability and other advantages, owes much to
+Python and the Twisted networking framework.
+
+Other implementations (servers)
+------------------------------
+Other than Conch, there are three popular server implementations. OpenSSH
+works with versions 1 and 2 of the protocol, and is the most popular on *nix
+systems. FSecure is more popular on Windows servers, and also works with both
+versions. LSH is newer, and implements version 2. All three are written in C,
+with LSH having some supporting Scheme code to generate C files.
+
+Other implementations (clients)
+-------------------------------
+On *nix, the SSH clients are provided by the server implementations (OpenSSH
+and LSH). On Windows, there are a couple of separate clients. PuTTY is the
+most popular and supports Telnet along with SSH1 and 2. TeraTerm recently
+incorporated SSH into the core: before it had been an extension module.
+MindTerm is the only implementation in this list to be written in a language
+other than C. It runs as a Java applet, allowing SSH to run on any computer
+with a JVM.
+
+Why Twisted?
+------------
+Why is Twisted ideal for this type of project? Firstly, it is an asynchronous
+library, meaning there are no worries about threading or concurrency issues.
+This means more developer time can be devoted to making the code work well,
+rather than just work. Second, Python lends itself to this kind of
+development: the code is easy to read and easy to write. Third, the Twisted
+library is high-level, so developers do not need to worry about select loops or
+callbacks. Twisted handles all of that and allows developers to concentrate on
+the code.
+
+No forking/threads
+------------------
+Unlike OpenSSH, the Conch server does not fork a process for each incoming
+connection. Instead, it uses the Twisted reactor to multiplex the connections.
+The only fork done is to execute a process such as a shell, but running a shell
+is not necessary, in which case the entire protocol would be run in-process.
+One of the initial features of the server was an in-process Python interpreter
+which allowed a user to interact with the server as it was running. (It is
+currently disabled for security reasons.) Threads are only used to interface
+with synchronous libraries, such as PyPAM (Pluggable Authentication Modules
+support) or PyME (GPGME support). By not using forks or threads, the time it
+takes for the Conch server to start an SSH session is roughly half of the time
+it takes for OpenSSH. However, this does require that code in Conch be
+non-blocking, which is an obstacle for programmers not used to that style.
+
+Security - No Pointers
+----------------------
+OpenSSH, LSH, and PuTTY are all written in C. Many security holes are a result
+of problems with unsafe pointer usage, which is a large problem in C code.
+Many other security holes result from related issues, such as buffer overflows,
+off-by-one errors on arrays, and memory allocation/deallocation bugs. Python
+is pointer-safe, and so is not vulnerable to this class of hole. This also
+means that no arbitrary data from over-the-wire is ever run, meaning control
+always stays with the Conch server.
+
+Security - High Level
+---------------------
+Being written in Python provides more security than just pointer safety. The
+strong builtin library that comes with Python (including powerful data types
+like the list and dictionary) means that fewer wheels need to be reinvented.
+This limits the potential to make mistakes in implementation. Exceptions are
+another powerful tool. They centralize error handling, rather than the mix of
+methods that the C libraries use. All errors are caught and dealt with: this
+might mean that the server stops accepting connections, but it never
+compromises security.
+
+Security - Not Root
+-------------------
+Also, Conch does not need to run as root. In the default server, root
+privileges are used for two things: to bind to ports < 1024, and to fork a
+process as a different user. If neither of these are needed, the server need
+not run as root at all. Even if they are, the server is only running as root
+for those small sections. The rest of the time, it runs under the effective
+user and group ID of the user who started the server. This limits the amount
+of damage that could be inflicted in the event of a compromise.
+
+Interfacing with other software
+--------------------------------------------
+OpenSSH can interact with subsystems such as SFTP only by executing a process
+to handle it. Not only is forking a process expensive, it limits the
+interaction to a generic bitstream, which leaves developers to determine how
+to interact with their users. Conch can run in the same process as other
+Python software, and is easily integrated with other Twisted servers. This
+allows for things like secure remote administration of a Twisted web server,
+encrypted communication to a Reality MUD, or secure remote object access using
+Perspective Broker. This saves the hassle and expense of forking, and allows
+Python developers to interact with Conch the way they know best: with Python.
+
+Speed
+---------------------
+No one can deny that compiled C is faster than Python. Some part of Conch use
+C (PyCrypto, TGMP) to speed frequent operations, but the majority of the code
+is in Python. The client suffers the most from this because of the time it
+takes to start the interpreter. Work is being done to speed up the client by
+caching connections. This does not eliminate the interpreter start-up cost,
+but it removes the cost of negotiating a new connection. This effort is
+similar to FSH (also in Python) but interacts more nicely with the SSH
+protocol. Psyco helps as well, offering a speedup of roughly 2x - 5x.
+
+Age
+---
+As I said in the introduction, Conch is still a newcomer on the Secure Shell
+stage (The first commit for Conch was July 15, 2002.) Although Python solves
+a large class of holes, it is probable that other security holes are in the
+code. Until a full audit is conducted of Twisted and of Conch, it should not
+be used for security-critical systems.
+
+Applications with Conch
+-----------------------
+One of the applications for Conch is with Reality, a MUD framework using
+Twisted. Conch makes it easy to allow secure connections to the MUD in
+addition or even in place of a standard Telnet connection. As problems
+such as character theft become more prevalent on the Internet, a secure
+interface becomes more important.
+More generally, work is being done on Insults, a replacement for libraries
+like Curses and S-Lang. It allows developers to write GUI code that
+interacts well with Conch and other Twisted software. Although it is in the
+initial stages of development, it shows much promise for the future.
+
+Future Directions
+-----------------
+There are several different directions for Conch to move in. One of the most
+interesting is system for generalized authentication forwarding. This would
+allow all authentication to be performed on a host that the user controls,
+which would help to stop vulnerabilities such as timing attacks. Second is
+more work with applications. Insults is becoming more powerful, and it will
+be interesting to see what it can be used for. Also important are auditing of
+the code and increasing the speed. These will make the code more useful in
+general, as well as improving security. Other ideas include direct support for
+SFTP/SCP, support for a key agent, and interfacing with Twisted Names to
+support DNSSEC.
+
+Conclusion
+----------
+Although it is new, Conch is a working implementation of the Secure Shell
+protocol. It is robust enough to serve as both the client and server on
+systems I and others use daily.
diff --git a/doc/historic/2003/pycon/conch/smalltwisted.png b/doc/historic/2003/pycon/conch/smalltwisted.png
new file mode 100644
index 0000000..4f7d04d
--- /dev/null
+++ b/doc/historic/2003/pycon/conch/smalltwisted.png
Binary files differ
diff --git a/doc/historic/2003/pycon/conch/twistedlogo.png b/doc/historic/2003/pycon/conch/twistedlogo.png
new file mode 100644
index 0000000..6226297
--- /dev/null
+++ b/doc/historic/2003/pycon/conch/twistedlogo.png
Binary files differ
diff --git a/doc/historic/2003/pycon/deferex/deferex-bad-adding.py b/doc/historic/2003/pycon/deferex/deferex-bad-adding.py
new file mode 100644
index 0000000..d124eaa
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-bad-adding.py
@@ -0,0 +1,8 @@
+def successCallback(result):
+ myResult = result + 1
+ print myResult
+ return myResult
+
+...
+
+adder.callRemote("add", 1, 1).addCallback(successCallback)
diff --git a/doc/historic/2003/pycon/deferex/deferex-chaining.py b/doc/historic/2003/pycon/deferex/deferex-chaining.py
new file mode 100644
index 0000000..cee9bea
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-chaining.py
@@ -0,0 +1,13 @@
+from twisted.internet import reactor, defer
+
+A = defer.Deferred()
+def X(result):
+ B = defer.Deferred()
+ reactor.callLater(2, B.callback, result)
+ return B
+def Y(result):
+ print result
+A.addCallback(X)
+A.addCallback(Y)
+A.callback("hello world")
+reactor.run()
diff --git a/doc/historic/2003/pycon/deferex/deferex-complex-failure.py b/doc/historic/2003/pycon/deferex/deferex-complex-failure.py
new file mode 100644
index 0000000..bcc38e2
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-complex-failure.py
@@ -0,0 +1,30 @@
+from deferexex import adder
+
+class MyExc(Exception):
+ "A sample exception"
+
+class MyObj:
+
+ def blowUp(self, result):
+ self.x = result
+ raise MyExc("I can't go on!")
+
+ def trapIt(self, failure):
+ failure.trap(MyExc)
+ print 'error (', failure.getErrorMessage(), '). x was:', self.x
+ return self.x
+
+ def onSuccess(self, result):
+ print result + 3
+
+ def whenTrapped(eslf, result):
+ print 'Finally, result was', result
+
+ def run(self, o):
+ o.callRemote("add", 1, 2).addCallback(
+ self.blowUp).addCallback(
+ self.onSuccess).addErrback(
+ self.trapIt).addCallback(
+ self.whenTrapped)
+
+MyObj().run(adder)
diff --git a/doc/historic/2003/pycon/deferex/deferex-complex-raise.py b/doc/historic/2003/pycon/deferex/deferex-complex-raise.py
new file mode 100644
index 0000000..8005e45
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-complex-raise.py
@@ -0,0 +1,12 @@
+class MyExc(Exception):
+ "A sample exception."
+
+try:
+ x = 1 + 3
+ raise MyExc("I can't go on!")
+ x = x + 1
+ print x
+except MyExc, me:
+ print 'error (',me,'). x was:', x
+except:
+ print 'fatal error! abort!'
diff --git a/doc/historic/2003/pycon/deferex/deferex-forwarding.py b/doc/historic/2003/pycon/deferex/deferex-forwarding.py
new file mode 100644
index 0000000..c2fa6f9
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-forwarding.py
@@ -0,0 +1,9 @@
+from twisted.spread import pb
+
+class LocalForwarder(flavors.Referenceable):
+ def remote_foo(self):
+ return str(self.local.baz())
+
+class RemoteForwarder(flavors.Referenceable):
+ def remote_foo(self):
+ return self.remote.callRemote("baz").addCallback(str)
diff --git a/doc/historic/2003/pycon/deferex/deferex-listing0.py b/doc/historic/2003/pycon/deferex/deferex-listing0.py
new file mode 100644
index 0000000..e0de3ce
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-listing0.py
@@ -0,0 +1,18 @@
+
+class DocumentProcessor:
+ def __init__(self):
+ self.loadDocuments(self.callback, mySrv, "hello")
+
+ def loadDocuments(callback, server, keyword):
+ "Retrieve a set of documents!"
+ ...
+
+ def callback(self, documents):
+ try:
+ for document in documents:
+ process(document)
+ finally:
+ self.cleanup()
+
+ def cleanup(self):
+ ...
diff --git a/doc/historic/2003/pycon/deferex/deferex-listing1.py b/doc/historic/2003/pycon/deferex/deferex-listing1.py
new file mode 100644
index 0000000..399eb2b
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-listing1.py
@@ -0,0 +1,6 @@
+def prettyRequest(server, requestName):
+ return server.makeRequest(requestName
+ ).addCallback(
+ lambda result: ', '.join(result.asList())
+ ).addErrback(
+ lambda failure: failure.printTraceback())
diff --git a/doc/historic/2003/pycon/deferex/deferex-listing2.py b/doc/historic/2003/pycon/deferex/deferex-listing2.py
new file mode 100644
index 0000000..d124eaa
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-listing2.py
@@ -0,0 +1,8 @@
+def successCallback(result):
+ myResult = result + 1
+ print myResult
+ return myResult
+
+...
+
+adder.callRemote("add", 1, 1).addCallback(successCallback)
diff --git a/doc/historic/2003/pycon/deferex/deferex-simple-failure.py b/doc/historic/2003/pycon/deferex/deferex-simple-failure.py
new file mode 100644
index 0000000..34f6b29
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-simple-failure.py
@@ -0,0 +1,9 @@
+from deferexex import adder
+
+def blowUp(result):
+ raise Exception("I can't go on!")
+
+def onSuccess(result):
+ print result + 3
+
+adder.callRemote("add", 1, 2).addCallback(blowUp).addCallback(onSuccess)
diff --git a/doc/historic/2003/pycon/deferex/deferex-simple-raise.py b/doc/historic/2003/pycon/deferex/deferex-simple-raise.py
new file mode 100644
index 0000000..cd89b1a
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex-simple-raise.py
@@ -0,0 +1,3 @@
+x = 1 + 3
+raise Exception("I can't go on!")
+print x
diff --git a/doc/historic/2003/pycon/deferex/deferex.html b/doc/historic/2003/pycon/deferex/deferex.html
new file mode 100644
index 0000000..a4fb171
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferex.html
@@ -0,0 +1,499 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>Generalization of Deferred Execution in Python</title>
+</head>
+
+<body>
+<h1>Generalization of Deferred Execution in Python</h1>
+
+<p>Glyph Lefkowitz</p>
+
+<div>
+<div>Twisted Matrix Labs</div>
+<div><a href="mailto:glyph@twistedmatrix.com">glyph@twistedmatrix.com</a></div>
+</div>
+
+<h2>Overview</h2>
+
+<p>A deceptively simple architectural challenge faced by many multi-tasking
+applications is gracefully doing nothing. Systems that must wait for the
+results of a long-running process, network message, or database query while
+continuing to perform other tasks must establish conventions for the semantics
+of waiting. The simplest of these is blocking in a thread, but it has
+significant scalability problems. In asynchronous frameworks, the most common
+approach is for long-running methods to accept a callback that will be executed
+when the command completes. These callbacks will have different signatures
+depending on the nature of the data being requested, and often, a great deal of
+code is necessary to glue one portion of an asynchronous networking system to
+another. Matters become even more complicated when a developer wants to wait
+for two different events to complete, requiring the developer to &quot;juggle&quot;
+the callbacks and create a third, mutually incompatible callback type to handle
+the final result. </p>
+
+<p>This paper describes the mechanism used by the Twisted framework for waiting
+for the results of long-running operations. This mechanism, the <code>Deferred</code>,
+handles the often-neglected problems of error handling, callback juggling,
+inter-system communication and code readability. </p>
+
+<p> In a framework like Twisted, the ability to glue two existing components
+together with a minimum of mediating code is paramount. Considering that the
+vast majority of the code in Twisted is asynchronous I/O handling, it is
+imperative that the mechanism for relaying the data between the output from one
+system into the input of another be competitive with the simplicity of passing
+the return value of one method to the argument of another. It was also
+important to use only no new syntax to avoid confusing programmers who already
+have experience with Python, and establish no dependencies on anything which
+would break compatibility with existing Python code or C / Java
+extensions. </p>
+
+<h2>Other Popular Approaches</h2>
+
+<p>There are several traditional approaches to handling concurrency that have
+been taken by application frameworks in the past. Each has its own
+drawbacks.</p>
+
+<h3>Threads</h3>
+
+<p>The problems with using threads for concurrency in systems that need to
+scale is fairly well-documented. However, many systems that use asynchronous
+multiplexing for I/O and system-level tasks, but run application code in a
+thread. Zope's threaded request handling is a good example of this model.</p>
+
+<p>It is optimal, however, to avoid <em>requiring</em> threads for any part of
+a framework. Threading has a significant cost, especially in Python. The
+global interpreter lock destroys any performance benefit that threading may
+yield on SMP systems, and introduces significant complexity into both framework
+and application code that needs to be thread-safe.</p>
+
+<p>A full discussion of the pros and cons of threads is beyond the scope of
+this paper, however, using threads merely for blocking operations is clearly
+overkill. Since each thread represents some allocation of resources, all of
+those resources are literally sitting idle if they are doing nothing but
+waiting for the results from a blocking call.</p>
+
+<p>In a fairly traditional networking situation, where the server is
+asynchronously multiplexed, this waste of resources may be acceptable for
+special-purpose, simple client programs, since only a few will be run at a
+time. To create a generic system, however, one must anticipate cases when the
+system in question is not only a multi-user server or a single-user client, but
+also a multi-user hybrid client/server.</p>
+
+<p>A good example of this is a high-volume web spider. A spider may have a
+server for administrative purposes, but must also be able to spawn many
+requests at once and wait for them all to return without allocating undue
+resources for each request. The non-trivial overhead of threads, in addition
+to sockets, would be a very serious performance problem. </p>
+
+<h3>Callback Conventions</h3>
+
+<p>At some level, any system for handling asynchronous results in Python will
+be based on callback functions. The typical way to present this to the
+application programmer is to have all asynchronous methods accept a callback as
+one of their arguments.</p>
+
+<p>This approach is usually standardized by giving the callback having a
+standard name (&quot;callback&quot;) or a particular position (first argument, last
+argument). Even systems which rigorously adhere to such standardization run
+into problems, however.</p>
+
+<p>This approach does work for a variety of events. It is unwieldy when one is
+attempting to write asynchronous &quot;conversations&quot; that involve multiple
+stages. The first problem that we notice is the lack of error-handling. If an
+error occurs in normal Python code, Exception handling provides clean and
+powerful semantics for handling it. </p>
+
+<a href="deferex-listing0.py" class="py-listing">Document Processor Example</a>
+
+<p>In an asynchronous method such as the one given above, traditional
+exceptions fall short. What if an error occurs retrieving the documents from
+storage? Do we call the callback with an error rather than a result?</p>
+
+<h3>Language Modifications</h3>
+
+<p>Other languages handle this by associating different semantics with
+threading, or providing different constructs altogether for concurrency. This
+has the disadvantage that these languages aren't Python. Even Stackless Python
+is problematic because it lacks integration with the wide variety of libraries
+that Python provides access to. </p>
+
+<p>The design of <code>Deferred</code> draws upon some of these other languages, and this
+section will cover several languages and their impact.</p>
+
+<p>In particular, the following list of languages were influential:</p>
+
+<ul>
+ <li>Erlang</li>
+ <li>Mozart/Oz</li>
+ <li>E</li>
+ <li>Scheme</li>
+ <li>Smalltalk</li>
+</ul>
+
+<p> E, Smalltalk, and Scheme proved particularly influential. In E's, there is
+a sharp distinction between objects which are synchronously accessible and
+those which are asynchronously accessible. The original use for
+<code>Deferred</code>s was to represent results from Perspective Broker method
+calls. E was interesting in that the entire execution environment had
+assumptions about networking built in. E's &quot;eventually&quot; operator <a
+href="#steigler">[stiegler]</a> is what originally inspired the distinction
+between &quot;a method which returns X&quot; and &quot;a method which returns a
+<code>Deferred</code> that fires X&quot;. </p>
+
+<p>
+Smalltalk was influential in that its syntax for closures provided some
+precedent for thinking about the continuation of a &quot;conversation&quot; of execution
+as itself an object with methods. The original thought-experiment that lead to
+<code>Deferred</code>s was an attempt to write some Squeak code that looked like this:
+
+<pre>
+(object callRemote: &quot;fooBar&quot;) andThen: [ result |
+ Transcript show: result.
+ ] orElse: [ failure |
+ failure printTraceback.
+ ]
+</pre>
+
+The hypothetical <code>callRemote</code> method here would return an object
+with the method <code>andThen:orElse:</code> that took 2 code blocks, one for
+handling results and the other for handling errors.
+</p>
+
+<p>It was challenging to write enough Smalltalk code to make anything
+interesting happen with this paradigm, but within the existing framework of
+Twisted, it was easy to convert several request/response idioms to use this
+sort of object. Now that Twisted has dropped python 1.5.2 compatibility, and
+2.1 is the baseline version, we can use <code>nested_scopes</code> <a
+href="#hylton">[hylton]</a> and anonymous functions to make the code look
+similar to this original model. </p>
+
+<p> Scheme, of course, provides <code>call-with-current-continuation</code> (or
+<code>call/cc</code>), the mind-bending control structure which has been a
+subject of much debate in language-design circles. <code>call/cc</code> may
+have provided more a model of things to avoid than a real inspiration, though.
+While it is incredibly powerful, it creates almost as many problems as it
+solves. In particular, the interaction between continuations and
+<code>try:finally:</code> is undefined <a href="#pitman">[pitman]</a>, since it
+is impossible to determine the final time the protected code will be run. The
+strongest lesson from <code>call/cc</code> was to only take as much state in
+the <code>Deferred</code> as necessary, and to avoid playing tricks with implicit context.
+</p>
+
+<p>The mechanisms that these languages use, however, often rely upon deeper
+abstractions that make their interpreters less amenable than Python's to
+convenient, idiomatic integration with C and UNIX. Scheme's
+<code>call/cc</code> requires a large amount of work and creativity to
+integrate with &quot;C&quot; language libraries, as C. Tismer's work in
+Stackless Python Python has shown. <a href="#tismer">[tismer]</a> </p>
+
+<h2>Basics of Deferreds</h2>
+
+<p>After several months of working with Twisted's callback-based
+request/response mechanisms, it became apparent that something more was
+necessary. Often, errors would silently cause a particular process to halt.
+The syntax for a multi-stage asynchronous process looked confusing, because
+multiple different interfaces were being invoked, each of which taking multiple
+callbacks. The complexity of constructing these stages was constantly being
+exposed to the application developer, when it shouldn't really concern them.
+</p>
+
+<p>In order to make gluing different request/response systems together easy, we
+needed to create a more uniform way of having them communicate than a simple
+convention. In keeping with that goal, we reduced several conventions into one
+class, <code>Deferred</code>, so that the request system could return a
+<code>Deferred</code> as output and the responder could accept a <code>Deferred</code> as input..
+<code>Deferred</code>s are objects which represent the result of a request that is not yet
+available. It is suggested that any methods which must perform long-running
+calculations or communication with a remote host return a <code>Deferred</code>.</p>
+
+<p>This is similar to the Promise pattern, or lazy evaluation, except that it
+is a promise that will not be resolved synchronously. The terminology usually
+used to describe a <code>Deferred</code> is &quot;a <code>Deferred</code> that will fire&quot; a particular
+result.</p>
+
+<p><code>Deferred</code>s have a small interface, which boils down to these five methods,
+plus convenience methods that call them:
+
+<ul>
+ <li><code>addCallbacks(self, callback, errback=None, callbackArgs=None,
+ callbackKeywords=None, errbackArgs=None, errbackKeywords=None)</code></li>
+ <li><code>callback(result)</code></li>
+ <li><code>errback(result)</code></li>
+ <li><code>pause()</code></li>
+ <li><code>unpause()</code></li>
+</ul>
+
+</p>
+
+<p>In general, code that initially returns <code>Deferred</code>s will be framework code,
+such as a web request or a remote method call. This means that code that uses
+the framework will call <code>addCallbacks</code> on the <code>Deferred</code> that is
+returned by the framework. When the result is ready, the callback will be
+triggered and the client code can process the result. Usually the utility
+methods <code>addCallback</code> and <code>addErrback</code> are used.
+</p>
+
+<p>Using <code>addCallbacks</code> has slightly different semantics than using
+<code>addCallback</code> followed by <code>addErrback</code>;
+<code>addCallbacks</code> places the callback and the errback &quot;in
+parallel&quot;, meaning if there is an error in your callback, your errback will
+not be called. Thus using <code>addCallbacks</code> has either/or semantics;
+either the callback or the errback will be called, but not both.</p>
+
+<a href="deferex-listing1.py" class="py-listing">Fictitious Request Example</a>
+
+<p>The example given shows a method which returns a <code>Deferred</code> that will fire a
+formatted string of the result of a given request. The return value of each
+callback is passed to the first argument of the next.</p>
+
+<h2>Generalized Error Handling</h2>
+
+<p>As described above in the section on using callbacks for asynchronous result
+processing, one of the most common application-level problems in an
+asynchronous framework is an error that causes a certain task to stop
+executing. For example, if an exception is raised while hashing a user's
+password, the entire log-in sequence might be halted, leaving the connection in
+an inconsistent state.</p>
+
+<p>One way that Twisted remedies this is to have reasonable default behavior in
+circumstances such as this: if an uncaught exception is thrown while in the
+<code>dataReceived</code> callback for a particular connection, the connection
+is terminated. However, for multi-step asynchronous conversations, this is not
+always adequate.</p>
+
+<p>Python's basic exception handling provides a good example for an
+error-handling mechanisms. If the programmer fails to account for an error, an
+uncaught exception stops the program and produces information to help track it
+down. Well-written python code never has to manually detect whether an error
+has occurred or not: code which depends on the previous steps being successful
+will not be run if they are not. It is easy to provide information about an
+error by using attributes of exception objects. It is also easy to relay
+contextual information between successful execution and error handlers, because
+they execute in the same scope.</p>
+
+<p><code>Deferred</code> attempts to mimic these properties as much as possible in an
+asynchronous context.</p>
+
+<h3>Reasonable Defaults</h3>
+
+<p>When something unexpected goes wrong, the program should emit some debugging
+information and terminate the asynchronous chain of processing as gracefully as
+possible.</p>
+
+<p>Python exceptions do this very gracefully, with no effort required on the
+part of the developer at all.</p>
+
+<a href="deferex-simple-raise.py" class="py-listing">Simple Catastrophic Exception</a>
+
+<p><code>Deferred</code>s provide a symmetrical facility, where the developer may register a
+callback but then forego any error processing.</p>
+
+<a href="deferex-simple-failure.py" class="py-listing">Simple Catastrophic Deferred Failure</a>
+
+<p>In this example, the onSuccess callback will never be run, because the
+blowUp callback creates an error condition which is not handled. </p>
+
+<h3>No Ambiguity about Failure</h3>
+
+<p>It is impossible to provide a reasonable default behavior if failure is
+ambiguous. Code should never have to manually distinguish between success and
+failure. An error-processing callback has a distinct signature to a
+result-processing callback.</p>
+
+<p>Forcing client code to manually introspect on return values creates a common
+kind of error; when the success of a given long-running operation is assumed,
+it appears to work, and it is easier (and less code) to write a callback that
+only functions properly in a successful case, and creates bizarre errors in a
+failure case. A simple example:.</p>
+
+<a class="py-listing" href="deferex-bad-adding.py">Common Error Pattern</a>
+
+<p>In this example, when the remote call to add the two numbers succeeds,
+everything looks fine. However, when it fails, <code>result</code> will be an
+exception and not an integer: therefore the printed traceback will say
+something unhelpful, like:</p>
+
+<pre>TypeError: unsupported operand types for +: 'instance' and 'int'</pre>
+
+<h3>Rich Information about Errors</h3>
+
+<p>It should be easy for developers to distinguish between fatal and non-fatal
+errors. With Python exceptions, you can do this by specifying exception
+classes, which is a fairly powerful technique.</p>
+
+<a href="deferex-complex-raise.py" class="py-listing">Complex Python Exception</a>
+
+<p>With <code>Deferred</code>, we planned to have a syntactically simple technique for
+accomplishing something similar. The resulting code structure is tends to be a
+bit more expansive than the synchronous equivalent, due to the necessity of
+giving explicit names to the functions involved, but it can be just as easy to
+follow.</p>
+
+<a href="deferex-complex-failure.py" class="py-listing">Complex Deferred Failure</a>
+
+<p>In this example, we have a callback chain that begins with the result of a
+remote method call of 3. We then encounter a <code>MyExc</code> error raised
+in <code>blowUp</code>, which is caught by the errback <code>trapIt</code>.
+The 'trap' method will re-raise the current failure unless its class matches
+the given argument, so the error will continue to propagate if it doesn't
+match, much like an <code>except:</code> clause.</p>
+
+<h3>Easy Propagation of Context</h3>
+
+<p>While it is dangerous to implicitly propagate too much context (leading to
+problems similar to those with threads), we wanted to make sure that it is easy
+to move context from one callback to the next, and to convert information in
+errors into successful results after the errors have been handled. </p>
+
+<p>Both <code>addCallback</code> and <code>addErrback</code> have the signature
+<code>callable, *args, **kw</code>. The additional arguments are passed
+through to the registered callback when it is invoked. This allows us to
+easily send information about the current call to the error-handler in the same
+way as the success callback.</p>
+
+<h2>Patterns of Usage</h2>
+
+<p>Since <code>Deferred</code> is designed for a fairly specific class of problems, most
+places it is used tend to employ certain idioms.</p>
+
+<h3>Request-ID Dictionary</h3>
+
+<p>If you are implementing a symmetric, message-oriented protocol, you will
+typically need to juggle an arbitrary number of outstanding requests at once.
+The normal pattern for doing this is to create a dictionary mapping a request
+number to a <code>Deferred</code>, and firing a <code>Deferred</code> when a response with a given
+request-ID associated with it arrives.</p>
+
+<p>A good example of this pattern is the Perspective Broker protocol. Each
+method call has a request, but it is acceptable for the peer to make calls
+before answering requests. Few protocols are as extensively permissive about
+execution order as PB, but any full-fledged RPC or RMI protocol will enable
+similar interactions. The MSN protocol implementation in Twisted also uses
+something similar.</p>
+
+<h3> Sometimes Synchronous Interface </h3>
+
+<p>When writing interfaces that application programmers will be implementing
+frequently, it is often convenient to allow them to either return either a
+<code>Deferred</code> or a synchronous result. A good example of this is Twisted's Woven, a
+dynamic web content system.
+</p>
+
+<p> The processing of any XML node within a page may be deferred until some
+results are ready, be they results of a database query, a remote method call,
+an authentication request, or a file upload. Many methods that may return
+Nodes may also return <code>Deferred</code>s, so that in either case the application
+developer need return the appropriate value. No wrapping is required if it is
+synchronous, and no manual management of the result is required if it is not.
+</p>
+
+<p>This is the best way to assure that an application developer will never need
+to care whether a certain method's results are synchronous or not. The first
+usage of this was in Perspective Broker, to allow easy transparent forwarding
+of method calls. If a <code>Deferred</code> is returned from a remotely accessible method,
+the result will not be sent to the caller until the <code>Deferred</code> fires. </p>
+
+<a href="deferex-forwarding.py" class="py-listing">Forwarding Local and Remote
+Interfaces</a>
+
+<h3><code>callRemote</code></h3>
+
+<p>Ideally, all interactions between communicating systems would be modeled as
+asynchronous method calls. Twisted Words, the Twisted chat server, treats any
+asynchronous operation as a subset of the functionality of Perspective Broker,
+using the same interface. Eventually, the hope is to make greater use of this
+pattern, and abstract asynchronous conversations up another level, by having
+the actual mechanism of message transport wrapped so that client code is only
+aware of what asynchronous interface is being invoked.</p>
+
+<h2>Advanced Features</h2>
+
+<p>The first &quot;advanced&quot; feature of <code>Deferred</code>s is actually
+used quite frequently. As discussed previously, each <code>Deferred</code> has
+not one, but a chain of callbacks, each of which is passed the result from the
+previous callback. However, the mechanism that invokes each callback is itself
+an implementor of the previously-discussed &quot;Sometimes Synchronous
+Interface&quot; pattern - a callback may return either a value or a
+<code>Deferred</code>.</p>
+
+<p>For example, if we have a <code>Deferred</code> A, which has 2 callbacks: X,
+which returns a deferred B, that fires the result to X in 2 seconds, and Y,
+which prints its result, we will see the string &quot;hello&quot; on the screen
+in 2 seconds. While it may sound complex, this style of coding one
+<code>Deferred</code> which depends on another looks very natural.</p>
+
+<a class="py-listing" href="deferex-chaining.py">Chaining 2
+<code>Deferred</code>s Together</a>
+
+<p>In this way, any asynchronous conversation may pause to wait for an
+additional request, without knowing in advance of running the first request
+what all the requests will be.</p>
+
+<p>The other advanced feature of <code>Deferred</code>s is not terribly common,
+but is still useful on occasion. We have glossed over the issue of
+&quot;pre-executed&quot;<code>Deferred</code>s so far, e.g. <code>Deferred</code>s
+which have already been called with a callback value before client code adds
+callbacks to them. The default behavior, which works in almost every
+situation, is simply to call the callback immediately (synchronously) as it is
+added. However, there are rare circumstances where race conditions can occur
+when this naive approach is taken.</p>
+
+<p>For this reason, <code>Deferred</code> provides <code>pause</code> and
+<code>unpause</code> methods, allowing you to put a <code>Deferred</code> into
+a state where it will stop calling its callbacks as they are added; this will
+allow you to set up a series of communicating <code>Deferred</code>s without
+having anything execute, complete your setup work, and then unpause the
+process.</p>
+
+<p>In this way, you can create centralized choke-points for caring about whether
+a process is synchronous or not, and completely ignore this problem in your
+application code. For example, in the now-obsolete Twisted Web Widgets system
+(a dynamic web content framework that predates woven), it was necessary to make
+sure that certain <code>Deferred</code>s were always called in order, so the page would
+render from top to bottom. However, the user's code did not need to concern
+itself with this, because any <code>Deferred</code>s for which synchronous callback
+execution would have been an issue were passed to user code paused.</p>
+
+<h2>Conclusion</h2>
+
+<p><code>Deferred</code>s are a powerful abstraction for dealing with
+asynchronous results. Having a uniform approach to asynchronous conversations
+allows Twisted APIs to provide a level of familiarity and flexibility for
+network programmers that approaches that of domain-specific languages, but
+still provides access to all of Python's power.</p>
+
+<h2>Acknowledgements</h2>
+
+<p>I would like to thank the entire Twisted team, for making me realize what a
+good idea I had hit upon with <code>Deferred</code>s.</p>
+
+<p>Special thanks go to Andrew Bennetts and Moshe Zadka, for implementing the
+portion of Twisted used to generate this, and other, papers, and to Ying Li and
+Donovan Preston for last-minute editorial assistance..</p>
+
+<h2>References</h2>
+
+<ol>
+
+ <li><a name="stiegler"></a>Marc Stiegler, <a
+ href="http://www.skyhunter.com/marcs/ewalnut.html#SEC19">The E Language in a
+ Walnut</a>, <i>erights.org</i></li>
+
+ <li><a name="hylton"></a>Jeremy Hylton, <a
+href="http://www.python.org/peps/pep-0227.html" >PEP 227, &quot;Statically
+Nested Scopes&quot;</a></li>
+
+ <li><a name="pitman"></a>Kent Pitman, <a
+href="http://www.nhplace.com/kent/PFAQ/unwind-protect-vs-continuations.html"
+ >UNWIND-PROTECT vs. Continuations</a>, <i>Kent Pitman's Personal FAQ</i></li>
+
+ <li><a name="tismer">Christian Tismer, <a
+ href="http://www.stackless.com/spcpaper.htm">Continuations and Stackless
+ Python</a>, <i>Proceedings of the Sixth International Python Conference</i>
+ </a>
+ </li>
+
+</ol>
+
+</body>
+</html>
diff --git a/doc/historic/2003/pycon/deferex/deferexex.py b/doc/historic/2003/pycon/deferex/deferexex.py
new file mode 100644
index 0000000..71c116e
--- /dev/null
+++ b/doc/historic/2003/pycon/deferex/deferexex.py
@@ -0,0 +1,16 @@
+
+# DEFERred EXecution EXamples
+
+### make sure errors come out in order
+import sys
+from twisted.python import log
+log.logerr = sys.stdout
+
+# Create a pseudo "remote" object for executing this stuff
+from twisted.spread.util import LocalAsRemote
+class Adder(LocalAsRemote):
+ def async_add(self, a, b):
+ print 'adding', a, b
+ return a + b
+
+adder = Adder()
diff --git a/doc/historic/2003/pycon/intrinsics-lightning/intrinsics-lightning b/doc/historic/2003/pycon/intrinsics-lightning/intrinsics-lightning
new file mode 100644
index 0000000..d41d3af
--- /dev/null
+++ b/doc/historic/2003/pycon/intrinsics-lightning/intrinsics-lightning
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+
+from slides import *
+from twslides import *
+
+class PythonSource:
+ def __init__(self, content):
+ self.content = content
+ def toHTML(self):
+ return '<pre class="python">%s</pre>' % (self.content,)
+
+lecture = Lecture(
+ "Changing the Type of Literals",
+ Slide("New-style classes",
+ Bullet("In 2.2+, built-in types can be subclassed"),
+ Bullet("These can be created explicitly by using their name", SubBullet(
+ Bullet("For example, an int subclass that displays itself in roman numerals"),
+ Bullet("print RomanNumeral(13) -> XIII"),
+ )),
+ ),
+ Slide("Literals are less accessable",
+ Bullet("When you write [] or 7, the list or int type is instantiated"),
+ Bullet("This behavior seems inaccessable"),
+ Bullet("While this makes for more readable code, it limits the scope of possible evil"),
+ ),
+ Slide("Throw in an extension module...",
+ Bullet("intrinsics.so exposes one function, 'replace'"),
+ Bullet("It takes two arguments", SubBullet(
+ Bullet("A type object to replace"),
+ Bullet("The type object with which to replace it"),
+ )),
+ Bullet("Magic is performed, and the new type is now used whenever the old one would have been"),
+ ),
+ Slide("An example",
+ PythonSource("""\
+class RomanNumeral(int):
+ def __str__(self):
+ # Regular code for formatting roman numerals
+
+old_int = intrinsics.replace(int, RomanNumeral)
+print 13
+"""
+ ),
+ Bullet("The output is simply the roman numerals XIII"),
+ ),
+ Slide("intrinsics.c - The replacement",
+ PRE("""\
+PyObject*
+intrinsics_replace(PyObject* self, PyObject* args) {
+ static PyTypeObject* const types[] = {
+ &PyInt_Type, &PyLong_Type, &PyFloat_Type, &PyComplex_Type,
+ &PyBool_Type, &PyBaseObject_Type, &PyDict_Type, &PyTuple_Type,
+ &PyBuffer_Type, &PyClassMethod_Type, &PyEnum_Type, &PyProperty_Type,
+ &PyList_Type, &PyStaticMethod_Type, &PySlice_Type, &PySuper_Type,
+ &PyType_Type, &PyRange_Type, &PyFile_Type, &PyUnicode_Type,
+ &PyString_Type,
+ NULL
+ };
+
+ int i = 0;
+ PyObject *old, *new;
+ PyTypeObject* space;
+
+ if (!PyArg_ParseTuple(args, "OO:replace", &old, &new))
+ return NULL;
+"""
+ ),
+ ),
+ Slide("intrinsics.c - The actual replacement",
+ PRE("""\
+ while (types[i]) {
+ if (types[i] == (PyTypeObject*)old) {
+ space = PyObject_New(PyTypeObject, &PyType_Type);
+ *space = *(types[i]);
+ *(types[i]) = *(PyTypeObject*)new;
+ break;
+ }
+ ++i;
+ }
+ if (!types[i]) {
+ PyErr_SetString(replace_error, "unknown type");
+ return NULL;
+ }
+ Py_INCREF(new);
+ Py_INCREF(space);
+ return (PyObject*)space;
+}
+"""
+ ),
+ ),
+ Slide("This is the wrong answer",
+ Bullet("The right answer is to add more flexibility to the Python compiler"),
+ Bullet("This is a lot less code, though"),
+ )
+)
+
+lecture.renderHTML(".", "intrinsics-lightning-%d.html", css="stylesheet.css")
diff --git a/doc/historic/2003/pycon/lore/lore-presentation b/doc/historic/2003/pycon/lore/lore-presentation
new file mode 100755
index 0000000..59a4d6a
--- /dev/null
+++ b/doc/historic/2003/pycon/lore/lore-presentation
@@ -0,0 +1,108 @@
+#!/usr/bin/python2.2
+# Moshe -- current content is estimated at about 15 minutes
+from slides import NumSlide, Slide, Bullet, SubBullet, PRE, URL
+from twslides import Lecture
+
+
+lecture = Lecture(
+ "Lore: A Document Generation System",
+ Slide("Introduction",
+ Bullet("Document generation system"),
+ Bullet("Input a strict subset of XHTML"),
+ Bullet("Output -- nicely formatted HTML and LaTeX"),
+ Bullet("Used to generate >200 pages of Twisted documentation"),
+ ),
+ Slide("History",
+ Bullet("Twisted needed documentation -- and a format"),
+ Bullet("Reluctance to add dependence on a big system"),
+ Bullet("Wanted something quick and easy -- subset of HTML!"),
+ Bullet("Needs matured: table of contents, printed version"),
+ Bullet("Enter Lore"),
+ ),
+ Slide("Goals",
+ Bullet("Easy to use for authors"),
+ Bullet("Easy to install"),
+ Bullet("(Uncommon) Source format should be readable"),
+ ),
+ Slide("Contents",
+ Bullet("twisted.lore Python package"),
+ Bullet("'lore' command-line program"),
+ Bullet("Comes with every Twisted installation"),
+ Bullet("In particular -- works on Linux, Win32, Mac"),
+ Bullet("In particular -- supports Python 2.1, 2.2, 2.3 alpha"),
+ ),
+ Slide("Alternatives - HTML",
+ Bullet("Too flexible"),
+ Bullet("No support for needed idioms", SubBullet(
+ Bullet("Special-purpose Python markup"),
+ Bullet("Tables of contents"),
+ Bullet("Inlining")),
+ ),
+ Bullet("Renders badly to dead trees with current tools"),
+ ),
+ Slide("Alternatives - LaTeX",
+ Bullet("Very good at printed results"),
+ Bullet("Model makes alternative parsers near-impossible"),
+ Bullet("Renderers to HTML are buggy and fragile"),
+ ),
+ Slide("Alternatives - Docbook",
+ Bullet("Using correctly requires too much work", SubBullet(
+ Bullet("Write a DTD with special elements"),
+ Bullet("Write Jade stylesheets"))),
+ Bullet("Lore is probably smaller than docbook specialization"),
+ ),
+ Slide("Alternatives - Texinfo",
+ Bullet("Next slide, please"),
+ ),
+ Slide("Lore goodies",
+ Bullet("Special tag to mark classes/modules/functions", SubBullet(
+ Bullet("Can be made to point to auto-generated docs")),
+ ),
+ Bullet("Inline code-examples", SubBullet(
+ Bullet("No need to escape all those <, > and &")),
+ ),
+ Bullet("Syntax-highlight Python code"),
+ ),
+ Slide("hlint - A lint-like program",
+ Bullet("Checks for many common errors"),
+ Bullet("Unhandled elements"),
+ Bullet("Misspelled (or miscased) class names"),
+ Bullet("Checks Python code for syntax errors"),
+ ),
+ Slide("Extending Lore",
+ Bullet("Easily done with some Python code"),
+ Bullet("Input-enhancements decide which output formats to handle"),
+ Bullet("Example: math-lore, Lore with LaTeX formulae"),
+ ),
+ Slide("HTML Output",
+ Bullet("HTML is a flexible output format"),
+ Bullet("Documents often have to integrate with a site"),
+ Bullet("Lore produces HTML documents based on a template"),
+ Bullet("Lore uses only HTML 'class' attributes, never 'font'",
+ SubBullet(Bullet("Plays nice with CSS")),
+ ),
+ ),
+ Slide("Man Pages",
+ Bullet("Lore has a program to convert man pages to Lore documents"),
+ Bullet("Man pages are written anyway"),
+ Bullet("No man output: the format is too limited"),
+ ),
+ Slide("Small Example",
+ PRE("""\
+<html>
+<head>
+<title>Example</title>
+</head>
+<body>
+<h1>Example</h1>
+<p>Simple paragraph<span class="footnote">footnote</span></p>
+</body>
+</html>
+""")),
+ Slide("Future Directions",
+ Bullet("More output formats"),
+ Bullet("Some more classes - abstract, bibliography"),
+ ),
+)
+
+lecture.renderHTML(".", "lore-%d.html", css="main.css")
diff --git a/doc/historic/2003/pycon/lore/lore-slides.html b/doc/historic/2003/pycon/lore/lore-slides.html
new file mode 100755
index 0000000..19171f8
--- /dev/null
+++ b/doc/historic/2003/pycon/lore/lore-slides.html
@@ -0,0 +1,187 @@
+<html>
+ <head><title>Lore: A Document Generation System</title></head>
+
+ <body>
+ <h1>Lore: A Document Generation System</h1>
+
+ <div class="author">Andrew Bennetts &lt;andrew@puzzling.org&gt;</div>
+ <div class="author">(Twisted Lore maintainer)</div>
+
+ <h2>Introduction</h2>
+ <ul>
+ <li>Document generation system</li>
+ <li>Input format is essentially a subset of XHTML</li>
+ <li>Outputs nicely formatted HTML and LaTeX</li>
+ <li>Used to generate &gt;200 pages of Twisted documentation</li>
+ </ul>
+
+ <h2>History</h2>
+ <ul>
+ <li>Twisted needed documentation -- and a format</li>
+ <li>We didn't want to depend on a big system
+ <ul>
+ <li>The lower the barrier for documentation contributions, the
+ better</li>
+ </ul>
+ </li>
+ <li>We wanted something quick and easy
+ <ul>
+ <li>Lots of people already know simple HTML</li>
+ <li>People were already using HTML to write docs</li>
+ </ul>
+ </li>
+ <li>Our needs matured: table of contents, printable version</li>
+ <li>So we created Lore</li>
+ </ul>
+
+ <h2>Goals</h2>
+ <ul>
+ <li>Easy to use for authors</li>
+ <li>Easy to install</li>
+ <li>(Uncommon) Source format should be readable
+ <ul>
+ <li>Even to non-hackers</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h2>Small Example</h2>
+ <pre>
+&lt;html&gt;
+&lt;head&gt;
+&lt;title&gt;Example&lt;/title&gt;
+&lt;/head&gt;
+&lt;body&gt;
+&lt;h1&gt;Example&lt;/h1&gt;
+&lt;p&gt;Simple paragraph&lt;span class="footnote"&gt;footnote&lt;/span&gt;&lt;/p&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre>
+
+ <h2>Contents</h2>
+ <ul>
+ <li>twisted.lore Python package</li>
+ <li><code class="shell">lore</code> command-line program</li>
+ <li>Comes with every Twisted installation</li>
+ <li>In particular -- works on Linux, Win32, Mac</li>
+ <li>In particular -- supports Python 2.1, 2.2, 2.3 alpha</li>
+ </ul>
+
+ <h2>Alternatives - HTML</h2>
+ <ul>
+ <li>Too flexible</li>
+ <li>No support for needed idioms
+ <ul>
+ <li>Special-purpose Python markup</li>
+ <li>Tables of contents</li>
+ <li>Inlining</li>
+ </ul>
+ </li>
+ <li>Renders badly to dead trees with current tools</li>
+ </ul>
+
+ <h2>Alternatives - LaTeX</h2>
+ <ul>
+ <li>Very good at printed results</li>
+ <li>LaTeX's design makes alternative parsers near-impossible</li>
+ <li>Renderers to HTML are buggy and fragile
+ <ul>
+ <li>Although the Python Standard Library seems to cope :-)</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h2>Alternatives - Docbook</h2>
+ <ul>
+ <li>Using correctly requires too much work
+ <ul>
+ <li>Write a DTD with special elements</li>
+ <li>Write Jade stylesheets</li>
+ </ul>
+ </li>
+ <li>Lore is probably smaller than docbook specialization</li>
+ </ul>
+
+ <h2>Alternatives - Texinfo</h2>
+ <ul>
+ <li>Next slide, please</li>
+ </ul>
+
+ <h2>Lore goodies</h2>
+ <ul>
+ <li>Special tag to mark classes/modules/functions
+ <ul>
+ <li>Can be made to point to auto-generated docs</li>
+ </ul>
+ </li>
+
+ <li>Inline code-examples
+ <ul>
+ <li>No need to escape all those &lt;, &gt; and &amp;</li>
+ </ul>
+ </li>
+
+ <li>Syntax-highlight Python code</li>
+ </ul>
+
+ <h2>'lore -o lint': A lint-like tool</h2>
+ <ul>
+ <li>Checks for many common errors
+ <ul>
+ <li>Invalid XML</li>
+ <li>Unhandled elements</li>
+ <li>Misspelled (or miscased) class names</li>
+ <li>Checks Python code for syntax errors</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h2>Extending Lore</h2>
+ <ul>
+ <li>Easily done with some Python code</li>
+ <li>Input-enhancements decide which output formats to handle</li>
+ <li>Example: math-lore, Lore with LaTeX formulae</li>
+ </ul>
+
+ <h2>Extending Lore (cont'd)</h2>
+ <div class="pause" />
+ <ul>
+ <li>Another example: These slides!</li>
+ <li>The <code>lore-slides</code> plugin can output to
+ <ul>
+ <li>Magicpoint</li>
+ <li>HTML (one page per slide)</li>
+ <li>HTML (one big page)</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h2>HTML Output</h2>
+ <ul>
+ <li>HTML is a flexible output format</li>
+ <li>Documents often have to integrate with a site</li>
+ <li>Lore produces HTML documents based on a template</li>
+ <li>Lore uses only HTML <code>class</code> attributes, never <code>font</code>
+ <ul>
+ <li>Plays nice with CSS</li>
+ </ul>
+ </li>
+ </ul>
+
+ <h2>Man Pages</h2>
+ <ul>
+ <li>Lore has a program to convert man pages to Lore documents</li>
+ <li>Man pages are written anyway</li>
+ <li>No man output: the format is too limited</li>
+ </ul>
+
+ <h2>Future Directions</h2>
+ <ul>
+ <li>More output formats</li>
+ <li>Some more classes -- abstract, bibliography</li>
+ <li>Index</li>
+ </ul>
+
+ </body>
+</html>
+
diff --git a/doc/historic/2003/pycon/lore/lore.html b/doc/historic/2003/pycon/lore/lore.html
new file mode 100644
index 0000000..da71590
--- /dev/null
+++ b/doc/historic/2003/pycon/lore/lore.html
@@ -0,0 +1,791 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+<title>The Lore Document Generation Framework</title>
+</head>
+
+<body>
+
+<h1>The Lore Document Generation Framework</h1>
+
+<ul>
+<li>Moshe Zadka
+ <a href="mailto:moshez@twistedmatrix.com">moshez@twistedmatrix.com</a></li>
+<li>Andrew Bennetts
+ <a href="mailto:spiv@twistedmatrix.com">spiv@twistedmatrix.com</a></li>
+</ul>
+
+<h2>Abstract</h2>
+
+<p>Lore is a documentation generation system which uses a limited subset
+of XHTML, together with some class attributes, as its source format. This
+allows for lower barrier of entry than many other similar systems, since HTML
+authoring tools are plentiful
+as is knowledge of HTML writing. As an added advantage, the source format
+is viewable directly, so that even if Lore is not available the documentation
+is useful. It currently outputs LaTeX and HTML, which allows for most
+use-cases.</p>
+
+<p>Lore is currently in use by the Twisted project to generate its
+documentation for versions 1.0.1 and above.</p>
+
+<h2>History</h2>
+
+<p>At the beginning of Twisted's life cycle, as with any self-respecting
+free software project, it came completely devoid of documentation. As
+Twisted progressed in maturity, the Twisted development team realized
+that documentation is necessary.</p>
+
+<p>Since at that time the Twisted development
+team did not want the overhead of integrating
+a full-scale document generation framework into its build infrastructure,
+documents were written for the least common denominator -- plain HTML.
+When the Twisted team wanted the documentation to be
+featured on the web site, it was desirable to have them integrated with
+the web site's look and feel. Thus, <code class="shell">generate-domdocs</code>
+was born as a simple XML-based command line hack which improved the look of the
+documents so they would share the look and feel of the other pages in the web
+site, including a standard header and footer. As
+<code class="shell">generate-domdocs</code>
+slowly grew more and more features, it gradually became too large to maintain.
+The authors, members of the Twisted development team, decided that in order to
+make it more maintainable, it should be refactored into a
+library and by the way also add alternate output formats. Some of the documents
+which were reluctant to be transformed into alternate formats were fixed,
+and guidelines for making compatible documents were drafted. Those documents,
+together with the conversion code, are the Lore documentation generation
+system.</p>
+
+<h2>Introduction</h2>
+
+<p>Lore is documentation generation system which is a part of the
+<a href="http://twistedmatrix.com">Twisted</a> framework. It uses
+the Twisted XML parsing framework
+(<code class="API" base="twisted.web">microdom</code>) to parse compliant XHTML
+and generate the various output formats from it.</p>
+
+<p>Lore consists of a Python package, <code class="API">twisted.lore</code>,
+and a command-line program: <code class="shell">lore</code>, which
+generates HTML output (which is more presentation-oriented than the source
+format), LaTeX or runs an linter, depending on command-line arguments.</p>
+
+<p>In the case where the default output of Lore is not exactly suited to a
+Lore user,
+it is possible to subclass the output generators and customize their behavior.
+This could be done for many purposes, from straight-forward additions like
+adding a new <code>span</code> or <code>div</code> class to advanced tweaking
+such as changing the way Lore does image conversion on LaTeX output.</p>
+
+<p>Lore uses reflection intensively to make adding new features as simple
+as adding a new method, without the need for awkward registration schemes.
+Thus, adding another check to the linter or letting
+Lore handle the <code>link</code> element in some way require only the addition
+of one method.</p>
+
+<h2>Goals</h2>
+
+<p>Lore was written when the Twisted team felt it needed to write documentation
+and looked for a documentation format. Looking through alternatives, the
+best one seemed to be the Python way, using LaTeX format and
+<code class="shell">latex2html</code>. However, the Python way has its share
+of problems, not the least of which is <code class="shell">latex2html</code>
+being a long and crufty Perl program whose Perl APIs, which are the
+only way to add support for custom markup, change every version.</p>
+
+<p>Since documentation writing is important, a documentation system with
+minimal impact on the writer would be desirable. While LaTeX certainly has
+very little impact in terms of markup overhead, it has a very big impact
+both in terms of installed base (installing LaTeX on UNIX systems or
+Windows is non-trivial at best) and in terms of familiarity.</p>
+
+<p>HTML has the benefit of being directly readable on every post-1995
+computer, so the installed base is as big as could be hoped for. It also has
+the benefit of being easily parsed, at least in its new XHTML guise.</p>
+
+<p>The goals of Lore were taken to be:</p>
+
+<ul>
+<li>Source files directly readable.</li>
+<li>At least output to modern (CSS-based) HTML.</li>
+<li>Easily parsed by third-parties.</li>
+</ul>
+
+<h2>Source Format</h2>
+
+<h3>Description</h3>
+
+<p>Lore's source format is a subset of XHTML; all Lore source documents are
+valid XHTML documents. The XHTML tags that Lore allows are:
+<code>html</code>, <code>title</code>, <code>head</code>, <code>body</code>,
+<code>h1</code>, <code>h2</code>, <code>h3</code>, <code>ol</code>,
+<code>ul</code>, <code>dl</code>, <code>li</code>, <code>dt</code>,
+<code>dd</code>, <code>p</code>, <code>code</code>, <code>img</code>,
+<code>blockquote</code>, <code>a</code>, <code>cite</code>, <code>div</code>,
+<code>span</code>, <code>strong</code>, <code>em</code>, <code>pre</code>,
+<code>q</code>, <code>table</code>, <code>tr</code>, <code>td</code>,
+<code>th</code> and <code>style</code>.
+</p>
+
+<p>We would like to stress the omission of the <code>font</code> tag (which is
+deprecated in HTML 4.01 anyway). Instead of using <code>font</code>, Lore
+mandates the use of stylesheets
+and the <code>class</code> attribute, and in particular Lore defines several
+classes, such as <code>footnote</code>, <code>API</code>,
+<code>py-listing</code>. The use of classes on <code>div</code> and
+<code>span</code> elements effectively allows XHTML to be arbitrarily
+extensible without needing to define custom tags.</p>
+
+<p>Further discouraging explicit style decision, Lore deprecates the
+<code>style</code> attribute which allowing HTML (and XHTML) authors to embed
+pieces of the stylesheet in the document. Though Lore properly processes
+such documents, they are against the specification of Lore -- and
+the Lore lint-like problem finder will complain.</p>
+
+<h3>Advantages and Disadvantages</h3>
+
+<p>Requiring XHTML rather than just HTML greatly simplifies the code to
+manipulate Lore source, because we can use standard XML libraries. For
+documentation authors, the difference is negligible -- and any mistakes made in
+balancing tags can be easily found using the linter.
+Since tag balancing problems, in many cases, cause a discrepancy between
+author intention and the result, it is better to balance the tags anyway.</p>
+
+<p>Like LaTeX, Lore encourages authors to focus on content, letting the
+presentation take care of itself. This is an inherently restrictive approach,
+but results in much more consistent and higher-quality output.</p>
+
+<p>The Lore source format is quite usable (if somewhat plain) as an end-format.
+Any web browser can read it, and it does not require special stylesheet support,
+JavaScript or any other modern HTML additions. It is also, as intended,
+straightforward to create and edit documents in this format.</p>
+
+<p>However, reading the source format directly has some major limitations,
+which are inherent in the combination of the facilities which render HTML
+and the requirement that the format will be easily writable, and easy to
+modify, using any standard text editor.
+The limitations include:</p>
+
+<ul>
+<li>There is no table of contents.</li>
+<li>Footnotes interrupt the flow of text (although stylesheet tricks can
+alleviate this to an extent).</li>
+<li>Python source is not syntax highlighted.</li>
+<li>File inclusions are implemented as hyper-links.</li>
+</ul>
+
+<h2>Output Formats</h2>
+
+<p>The two most important formats, for the end-user, are the computer screen and
+pages of print outs. Any other format should be first and foremost be thought
+of as a prelude to these final formats.</p>
+
+<p>The easiest computer-screen oriented format is HTML. However, the HTML
+which is most comfortable and useful to the end-user is not necessarily
+easy to write and modify.
+For example, it is painful to manually write a table of contents, and even more
+painful to keep it updated as sections are added, removed or changed. However,
+when reading a long document having a table of contents, with hyperlinks
+into the sections, is a boon.
+Thus, even though both Lore's source and one output format are HTML, an
+HTML to HTML conversion is still necessary, paradoxical though it may sound.</p>
+
+<p>For printable output, the most widely supported formats are PostScript
+and Portable Document Format. On UNIX systems PostScript is often preferred,
+since there are many tools for manipulating it and printing it (and PostScript
+printers are more common in the UNIX world). On Windows and Apple computers,
+Portable Document Format (PDF) is preferred because of the ease of installation
+of the necessary tools. Mac OS X, though being technically a UNIX, supports
+PDF natively.</p>
+
+<p>Directly generating PostScript or PDF, however, is hard. Since these formats
+are very low-level, the application generating them must do the hard work
+of calculating line breaks, guessing hyphenation points and deciding on fonts.
+Since these tasks are already implemented by LaTeX, Lore just generates LaTeX
+code and lets the user run LaTeX to generate PostScript and
+<code class="shell">ps2pdf</code> to generate PDF. Granted, this still causes
+the problems with the difficulties of installing LaTeX. It is
+possible to implement direct Lore to PDF converter, though this hasn't been
+done yet, by using <code>pdflib</code>.</p>
+
+<h3>HTML</h3>
+
+<p>The HTML to HTML converter works by running a series of transformations on
+the Document Object Model (DOM) tree of the parsed document, and then
+writing it out. The most important transformation is that of throwing
+away anything outside the <code>body</code> element, and putting the
+<code>body</code> element inside a template file. This allows large
+parts of the common layout code to be customized without modifying or writing
+any Python code.</p>
+
+<p>Each step is implemented as a separate function, to allow Lore-using
+Python programmers to customize which tree transformations to do in their
+own code, without forcing them to rewrite functionality in Lore. In addition,
+other output generators might perform a subset of these transformations
+on the input tree before processing it -- and indeed, this is being used
+even in Lore itself.</p>
+
+<p>One of the steps taken is caused by a need which is common in large
+Python frameworks: many of the class or module names are deeply nested,
+but are commonly referred to by just their last one or two components
+in writing. However, the user would like to know the full name of the
+class or module name, and where to look up the API documentation -- but
+without having the complete name thrust upon him during the flow of text
+each time the module is mentioned.</p>
+
+<p>Lore makes sure that each class or module name which is mentioned will
+appear at least once using its full name, and afterwards use a common
+short name, regardless of how the author wrote it up. This frees authors
+from needing to observe, manually, this useful rule in their documents.</p>
+
+<p>The HTML Lore outputs aims to be the poster boy of graceful degradation.
+Thus, for example, while footnotes always appear as hyper-links to the footnote
+text, browsers which respect the <code>title</code> attribute (which is usually
+rendered as a tooltip) will also show the beginning of the footnote while
+hovering above the hyper-link.</p>
+
+<p>Lore avoids using the <q>font</q> or <q>color</q> tags and attributes,
+preferring to use HTML classes and using a stylesheet to specify graphical
+design decisions. This allows the Lore user to customize the presentation of
+the output without touching Python code. Since most often the stylesheet
+link is found in the <code>head</code> element, this is determined by
+the by the template.</p>
+
+<p>Lore uses the same approach even for syntax-highlighting Python code,
+generating such elements as
+<code>&lt;span class="keyword"&gt;if&lt;/span&gt;</code>.</p>
+
+<h3>LaTeX</h3>
+
+<p>The LaTeX home page describes LaTeX as a <q>high-quality typesetting system,
+with features designed for the production of technical and scientific
+documentation.</q> LaTeX is very popular for generating printable content,
+building on Donald Knuth's TeX system to generate nearly optimal output
+by putting together much of the typesetting industry's experience in the
+form of a program and adding sophisticated algorithms for line-breaking and
+hyphenation.</p>
+
+<p>It is very common for document generation systems to avoid generating
+printable output themselves, instead letting LaTeX do the hard work, and
+Lore is no exception.</p>
+
+<p>Lore can output LaTeX in two modes: article mode, in which it generates
+a complete article ready to be be processed, and a section mode in which
+it generates a LaTeX file whose top-level element is a section. Such a file
+is usually included in some other LaTeX file via the include mechanism.
+Twisted itself uses mainly the section mode, and includes everything in the
+file <code class="shell">book.tex</code>, which is later processed to generate
+the Twisted book.</p>
+
+<p>While, conceivably, other modes could be done (a chapter mode or a subsection
+mode) there has not been any demand for those. In the case of demand, supplying
+these would be very few lines of Python code (less than 10), which can even
+be done by subclassing existing classes and avoiding the modification of Lore
+itself.</p>
+
+<h3>Docbook</h3>
+
+<p>Docbook output is currently experimental. Its chief use to Lore would
+be in generating Texinfo, which is the source for the GNU info documentation
+format.</p>
+
+<h2>Lint</h2>
+
+<p>Very early in the Lore development life-cycle it was found that a good
+Lint-like tool is necessary to find errors without necessitating a full
+compilation to all formats and sometimes even browsing the results. Because
+Lore was written to accommodate a large set of already existing documents
+(which were not previously checked for potential problems), such a tool
+was very useful so that finding a problem in one document would not mean
+this problem needs to be manually searched, and corrected, in all the other
+documents.</p>
+
+<p>Lore's linter tries to find problems in documents
+that would either stop the conversion to other formats by Lore completely
+(for example, by being not well-formed XML), or that would make it less useful
+(for example, by warning about tags or classes that are not supported by
+Lore).</p>
+
+<p>The linter even detects more exotic problems,
+including:</p>
+<ul>
+ <li><code>pre</code> elements containing lines over 80 characters. Long lines
+ can be ugly to render in some output formats, and even impossible to
+ render in others.</li>
+ <li>Explicit use of the <code>"</code> character in a non-pre or non-code
+ environment. This makes a big difference for high-quality typographical
+ output targets like LaTeX, which
+ have distinct left- and right-quote characters.</li>
+ <li>Python code that isn't syntactically valid, with a bit of magic to account
+ for this idiom:
+<pre class="python">
+for x in sequence:
+ ...
+</pre>
+ This check caught a surprisingly large number of errors in the Twisted
+ documentation!</li>
+ <li><code>h1</code> contents being equal to <code>title</code> contents.
+ HTML is somewhat unique in that it has two places to specify the logical
+ idea of <q>title</q>. Since other output formats do not support that,
+ in Lore papers, the contents of both must be the same.</li>
+</ul>
+
+<p>Since many of the incremental improvements done to Lore found a problem
+in the existing documentation files, the linter has been
+an important part of the Lore development effort. One may even argue that
+part of the reason other documentation generation systems produce suboptimal
+output for their <q>non-native</q> application is the lack of a linting
+tool.</p>
+
+<p>Finally, if the linter gives a false positive, that is
+it emits a warning for something that isn't a problem in a particular situation,
+the user can add an <code>hlint="off"</code> attribute to the offending tag, and
+the linter will ignore it. This is necessary only very rarely.</p>
+
+<p>The chief design decision made in the linter, after
+painful experience when running <code class="shell">tidy</code>, is that
+<em>it must never change the document</em>. Thus, while the linter
+will be as pedantic as possible finding
+errors, it never changes the contents. This is particularly important
+when dealing with version control systems, where spurious changes can
+render <code class="shell">diff</code> listings useless.</p>
+
+<h2>Features</h2>
+
+<h3>Python Syntax Highlighting</h3>
+
+<p>All existing syntax highlighters for Python used pre-<code>tokenize</code>
+techniques to analyse the Python code. As a result, they were cumbersome
+and non-standard. The Lore developers decided that writing a Python
+HTML syntax-highlighter would be easier than modifying one of the existing
+ones. A syntax-highlighter was built on top of a null-tokenizer: that is,
+a tokenizer which emits the <em>exact same</em> characters as the input.
+This allowed easy debugging of the parsing code.</p>
+
+<p>The only non-trivial code in the syntax highlighter is when dealing
+with whitespace which is not significant syntactically, since the tokenizer
+does not report it. However, since the tokenizer does report row and column,
+when the code sees a discrepancy between where the previous token ended
+and the current token starts, it adds whitespace to make up for
+the discrepancy.</p>
+
+<p>When writing out the HTML, the only difference between that and the
+null-tokenizer is the wrapping of each token by a <code>span</code>
+tag with the appropriate class and escaping.</p>
+
+<p>Note that the basic Python tokenizer does not distinguish between the
+various roles of the production <q>NAME</q> (that is, a string of alphanumeric
+and
+underscore characters starting with an underscore or a letter) in Python.
+The tokenizer Lore uses adds that information by having a simple state machine:
+if the word is a keyword, there is nothing to be determined; otherwise, it
+depends on the last detected name -- <code>class</code> or
+<code>def</code> mean it is a function or class names, and after a
+<code>class</code>/<code>def</code> and until a <code>:</code>, everything
+is a <q>parameter</q> or a superclass.</p>
+
+<p>The Python syntax highlighter Lore uses can be found in the
+<code class="API">twisted.python.htmlizer</code>.</p>
+
+<h3>File Inclusion</h3>
+
+<p>Often, when writing detailed documents, the author wishes to test his
+examples or even use examples from a working project. Pasting such examples
+directly into the HTML has both the usual problems of pasting code -- the
+version in the document will not benefit from bug fixes or enhancement to
+the original version -- and the problem that the HTML needs proper escaping,
+which is a tedious and error-prone procedure if done manually.
+Both problems are solved by Lore's <q>listing</q> mechanism. The
+<q>listing</q> mechanism converts HTML such as</p>
+
+<pre>
+&lt;a href="foo.py" class="py-listing&gt;foo.py&lt;/a&gt;
+</pre>
+
+<p>into inclusion of the <code class="shell">foo.py</code> file. It will always
+be properly escaped for whatever output format. It will
+also be syntax-highlighted, just as if it had been included verbatim.</p>
+
+<p>A similar class, <code>html-listing</code> is available for inclusion
+of HTML files.</p>
+
+<h3>API Reference Links</h3>
+
+<p>Twisted's documentation frequently references API documentation. In Lore,
+the name of an API such as
+<code class="API">twisted.internet.defer.Deferred</code> is marked up as</p>
+
+<pre>
+&lt;code class="API" base="twisted.internet.defer"&gt;Deferred&lt;/code&gt;
+</pre>
+
+<p>This will unambiguously link to
+<code class="API">twisted.internet.defer.Deferred</code>, even though it is
+displayed as
+<q><code class="API" base="twisted.internet.defer">Deferred</code></q>. Lore
+produces API links that work with
+<a href="http://epydoc.sourceforge.net">epydoc</a>,
+but could easily be adapted for another API documentation generator; in fact,
+Lore originally worked with happydoc.
+In addition, in the HTML output, Lore will add a <code>title</code>
+attribute to the API reference, containing the full name of the link.</p>
+
+<h3>Cross references</h3>
+
+<p>A collection of documents will typically refer to each other, for instance to
+avoid re-explaining some central concept. In HTML, cross-referencing
+is implemented as linking:</p>
+
+<pre>
+See &lt;a href="defer.html"&gt;Deferring Execution&lt;/a&gt;.
+</pre>
+
+<p>As a collection of HTML documents, this works with no changes. Other output
+formats do linking in other ways. When Lore is used to convert a collection of
+source HTML files into a single LaTeX book, each file is its own section, and
+the links are automatically converted into cross-references. Thus the example
+above might be rendered as <q>See Deferring Execution (page 163).</q></p>
+
+<p>Lore also recognizes <em>fragment identifiers</em> in links, so that a link
+to <code>glossary.html#psu</code> will be cross-referenced to that part of the
+glossary named <q>psu</q>, not just the whole glossary. This ensures that the
+page the reader is referred to is the correct one.</p>
+
+<h2>Man Support</h2>
+
+<p>Man pages are a fact of life on UNIX, and every self-respecting command
+line program is expected to come with one. The man format, implemented as
+troff macros, is somewhat arcane. Since, when Lore was written, we already
+had written man pages, the decision was to convert them to HTML rather than
+try to rewrite them in HTML and design a man output format.</p>
+
+<p>A limited parser for man pages is available in the
+<code class="API">twisted.lore.man2lore</code> module. It is not yet
+exposed via any public command line program.</p>
+
+<p>Earlier attempts, using <code class="shell">groff -Thtml</code> to
+generate HTML and then post-process it into Lore-compatible HTML
+were crufty and unmaintainable. It seems the man format shares some
+of LaTeX's problem: being written as a macro package over a powerful
+processor, it is too flexible for its own good. Fortunately, the subset
+normally used in man pages is quite small, so heuristically parsing man pages is
+much easier than the same task with LaTeX.</p>
+
+<h2>Comparisons</h2>
+
+<h3>HTML</h3>
+
+<p>HTML, when invented by Tim Berners-Lee, was meant to be a simple language
+for writing and sharing documents. With the explosion of the web, HTML has
+grown to a confusing jumble of logical and presentation features, with more
+layers, such as CSS, dumped on top of it. As a result, a modern browser is
+a complicated beast. That given, it is perhaps understandable that today's
+browsers do a sub-standard job at printing. Thus, while being extremely
+well suited to the world wide web, HTML is significantly lacking, at least
+in today's application market, when it comes to paper output. It might
+be possible to write an application to properly convert HTML with CSS to
+PostScript or PDF -- however, it would probably be much more complicated
+than Lore. Moreover, the portability of such an application would
+be worse of the portability of Lore itself, which currently only depends
+on Python 2.1 or higher and the Twisted framework.</p>
+
+<p>Limiting HTML to a small subset of features enables Lore to be small
+and readable while remaining useful. By including the <code>class</code>
+attribute among those features, Lore is also extensible.</p>
+
+<h3>LaTeX</h3>
+
+<p>When it comes to paper output, LaTeX cannot be out done except by a skilled
+typesetter designing and implementing. However, the architecture of LaTeX
+presents
+significant problems when trying to view LaTeX online. LaTeX is written
+as a macro layer above TeX rather than a preprocessor. Thus, all of TeX's
+power is available, and sometimes used, in LaTeX. TeX is non-trivial to
+parse and format by anyone short of Donald Knuth -- it contains such commands
+as to change the tokenizer by modifying which characters are considered
+word characters or even which character is the command character.
+In fact, the authors are not aware of any application which handles the
+full power of TeX without being based on the original TeX code.</p>
+
+<p>All this makes LaTeX extremely difficult to parse, and even partial attempts
+to parse LaTeX are big and cumbersome -- for example,
+<code class="shell">latex2html</code>. It is thus difficult to convert
+LaTeX to something appropriate to online viewing.</p>
+
+<h3>LyX</h3>
+
+<p>LyX's internal source format is not well documented, and the only supported
+way to write it is using the LyX GUI. Thus it is inherently limiting to
+documentation authors. In addition, it is not trivial to write LyX preprocessors
+to save documentation authors tedious work.</p>
+
+<h3>Docbook</h3>
+
+<p>Docbook is a big standard, with non-trivial to install tool-set. Writing
+Docbook is different than most other document generation formats, so it
+takes significant training to write. In addition, using Docbook for
+a specific project usually requires writing custom DSSSL stylesheets
+in a scheme-like language, and additional XML DTD snippets. Writing
+these was quite possibly comparable to writing Lore, and Lore has the advantage
+of being written in Python.</p>
+
+<h3>Texinfo</h3>
+
+<p>Texinfo imposes a significant effort on authors. Many things need to
+be written twice, and the error messages leave a lot to be desired.
+After starting to work on the Lore texinfo output format the authors
+are grateful they have never had to write Texinfo by hand.</p>
+
+<h2>Techniques</h2>
+
+<h3>Visitor Pattern</h3>
+
+<p>When generating LaTeX, Lore does it via a visitor pattern while visiting
+the nodes. A node which does not have a specific visitor is visited by
+first writing the <code>start_</code> attribute, then visiting its
+children and then writing the <code>end_</code> attribute. If the attributes
+do not exist, they are treated as though they were empty strings.</p>
+
+<p>That code allows most of the HTML elements to LaTeX converters to have no
+code -- only a pair of strings -- while the elements converters which need
+more sophisticated programming can do it via defining a method, which can
+still call the default processor if it needs this functionality.</p>
+
+<p>This pattern is also friendly to subclassing: all a subclass needs to
+do in order to change how an element is handled is to define either a pair
+of class attributes or a method.</p>
+
+<h3>Liberal Use of Reflection</h3>
+
+<p>In the above example of the visitor pattern, registration of the methods
+and attributes is avoided thanks to using the crudest form of reflection
+in Python -- the <code>getattr()</code> function.</p>
+
+<p>In the Lint support tool, more sophisticated reflection is needed when
+it needs to find all methods whose name begins with <code>check_</code>.
+This is done via the Twisted reflection code, built on top of the native
+Python facilities, in the module
+<code class="API">twisted.python.reflect</code>.</p>
+
+<h3>Recursively Searching For Elements</h3>
+
+<p>In the HTML output code, the most common operation is that of getting
+a list of elements which satisfy some property. This is done by one
+primary work-horse function:
+<code class="API">twisted.web.domhelpers.findNodes</code>. This function
+accepts a DOM tree and a function, and returns a list of all elements
+for which this function returns true. Using this, and the fact that Python makes
+it easy to combine functions into boolean combinations, makes analysis
+and modification and of the DOM tree a breeze.</p>
+
+<h2>Lessons Learned</h2>
+
+<h3>Problems With Some Output Formats</h3>
+
+<p>Probably the trickiest thing about non-HTML output formats is escaping.
+The problem comes from two annoying problems which are not really hard
+to solve, but do represent annoyances in the code:</p>
+
+<ul>
+<li>Different characters are escaped differently (for example, <code>\</code>
+ is escaped, in TeX, as <code>$\backslash$</code> while most other
+ characters are escaped as <code>\&lt;char&gt;</code>.</li>
+<li>Escaping depends on context -- special characters should not be escaped
+ at all inside <code>pre</code>, <code>&lt;/&gt;</code> should not be
+ escaped inside <code>code</code> and should be escaped as
+ <code>$&lt;$/$&gt;$</code> outside it.</li>
+</ul>
+
+<p>In Docbook, the sections are nested, so there is only need for
+a <code>title</code> element. However, in HTML only the headers care
+at which level they are. This requires the Docbook converter to keep
+the last header level and when it reaches a new header, to close and open
+enough sections so the header will get to the correct level. While Docbook's
+way may be more <q>correct</q>, it is unfortunate it chose to diverge from
+all other systems here.</p>
+
+<p>Texinfo requires all the sections in a document will have unique names.
+This makes it very inconvenient as both an input and an output format.</p>
+
+<p>Also, differing significance of whitespace in different formats requires that
+all whitespace emitted by lore must be normalized for the particular output
+format being used. Blank lines which have no impact on HTML will trigger
+paragraph breaks in LaTeX.</p>
+
+<h3>Event-based XML Parsing Considered Harmful</h3>
+
+<p>The first version of the LaTeX output generator was using an event-based
+XML parsing engine. It quickly turned out one needs to keep a lot of
+information in stacks and manage many instance variables. For example,
+though XML gets the name of the closing element (even that is arguably
+too much information), it does not get the attributes. In <code>span</code>
+elements, for example, the interesting information is the <code>class</code>
+attribute. Since a-priory, <code>span</code>s might be nested, the class
+needs to keep a stack of attribute collections.</p>
+
+<p>Quite soon, stacks were needed for proper handling of <code>div</code>
+tags and for determining proper quoting formats. Moreover, getting the
+code to function correctly in the face of edge cases, such as cross-references
+inside <code>pre</code> tags, proved to be quite a challenge.</p>
+
+<p>The code was shortened, simplified and became more maintainable when
+it was moved to <code class="API" base="twisted.web">microdom</code>.</p>
+
+<p>We feel that unless there is
+an inherent reason to do XML event-based parsing, then it is much easier
+to read the whole thing into a DOM and then process it. The code is both
+shorter and clearer, and features are much easier to add.</p>
+
+<h3>Allow Easy Modification</h3>
+
+<p>Lore, out of the box, does not attempt to be all things to all people.
+Particularly in the LaTeX output format, there is a lot of room for
+interpretation and personal preferences. Lore chose one specific way, without
+trying to add half a dozen options to tweak it. However, thanks to the
+way it is coded, it is easy to add or modify features to suit individual
+preferences. Many customizations only involve adding or overriding simple data
+attributes to a subclass; more advanced changes require adding or overriding
+methods.</p>
+
+<p>Likewise, the HTML output is built by running several tree-modification
+functions which are independent. Completely different HTML output could
+be build by adding more functions, or not running some of those which
+are being run.</p>
+
+<p>We already know of multiple users that have extended Lore for custom LaTeX
+generation. In each case it was a simple matter of subclassing Lore's LaTeX
+code.</p>
+
+<h3>Reinventing Wheels Can Be Useful</h3>
+
+<p>Documentation generation systems were already a solved problem before Lore
+was written. However, we know of no system with Lore's unique combination of
+features -- in particular, portability, having a directly readable source format
+which is also directly writable in text editors.
+The common wisdom that a documentation generation
+system is a hard sell because it requires people to learn a new language was
+refuted by using an existing language.</p>
+
+<p>Wheel reinvention also occurred in a nearby area -- Twisted's XML support,
+for which Lore is one of the biggest users. Again, the common wisdom was that
+this was a solved problem, with many existing DOM and SAX implementations.
+However, implementation of some features, no implementation of other features
+and API instability have lead the Twisted team to write its own, highly
+pythonic, DOM-like implementation. In
+<code class="API" base="twisted.web">microdom</code>, the aim is to be
+as thin a wrapper over the basic Python wrappers as possible. This feature
+has been used to the full in Lore, where many of the tree manipulations
+would have been much more cumbersome had a standard <q>opaque</q> DOM
+implementation been used. In addition, using
+<code class="API" base="twisted.web">microdom</code> frees Lore from the
+dependence on both Python version and whether PyXML is installed.</p>
+
+<p>For example, <code class="API" base="twisted.web">microdom</code>
+exposes the list of child nodes as a plain Python lists. This means that
+not only all the list operations can be done of it, which could possibly
+be simulated by a list-like object, but that it is possible to
+<em>replace</em> it by our own list. As another example,
+<code class="API" base="twisted.web">microdom</code> allows us to freely
+copy nodes from one DOM tree into another.</p>
+
+<p>Python, as a language well suited to rapid application development,
+acts as a way to make wheel reinvention far from the horrible mistake
+which is portrayed in the common software engineering folklore. Indeed, Python
+makes it easy enough to reinvent wheels that only the best, and easy to
+use, wheels, get reused at all.</p>
+
+<h2>Availability</h2>
+
+<p>Lore can be found in Twisted 1.0.1 and higher, in the
+<code class="API">twisted.lore</code> package. When you install the package,
+the relevant script, <code class="shell">lore</code>,
+should be installed in a sane directory,
+as determined by distutils.</p>
+
+<p>For usage examples, see <code class="shell">admin/release-twisted</code>
+in the Twisted source distribution. It runs the various Lore scripts
+as part of the package build.</p>
+
+<h2>Future Plans</h2>
+
+<h3>More Output Formats</h3>
+
+<p>It would be nice to have the Docbook output fully working. It would also
+be nice to have Texinfo in full working order so that GNU info aficionados could
+read the documents with the info browser. As suggested above, it might
+also be useful to have a way to directly generate PDF output via
+<code>pdflib</code> in order to skip LaTeX.</p>
+
+<p>In addition, another potential output format is to have high-quality
+text output. This is non-trivial, but possibly useful: browsers'
+<q>Save as text</q> feature is usually implemented as an afterthought,
+and hardly uses the flexibility available in the text format to its
+full power. The authors are unaware, for example, of an HTML to text
+converter which uses the underlining with <q>=</q> sign or <q>-</q>
+to indicate a header, or which uses the <code>/slant/</code> or
+<code>*asterisk*</code> conventions to indicate emphasis.</p>
+
+<p>Another output format we are considering is a split-page HTML with
+interlinks, so that long documents can be converted into something
+which is web-friendly. One nice use for that would be in web-based
+presentations.</p>
+
+<h3>Image Conversion</h3>
+
+<p>Currently all images are converted to EPS format. It would be nice to have
+the LaTeX converter try to see if there is already an EPS version, via some
+naming convention, and use that. This would allow better scaling of things like
+Dia diagrams. The versions in bitmap-based formats (such as PNG)
+are impossible to scale, because the text would become unreadable.</p>
+
+<h3>Interface</h3>
+
+<p>Currently, the only interface to Lore is through the command-line, and
+even that is somewhat spotty: for example, the man page parser is not directly
+available via the command line. We hope to remedy that, having at least a full
+suite of command-line tools and possibly graphical wrappers, particularly
+EMACS modes.</p>
+
+<h2>Twisted Integration</h2>
+
+<p>When starting with a historical note, it is only fitting to end
+with a historical note. Since the writing of Lore, Twisted documentation
+is successfully generated by it and distributed in the tarball. It contains
+generated HTML from the HOWTO documents, specifications and man pages.
+It also contains all these documents inside a LaTeX-generated PostScript
+file and PDF file in an easy to print format, suitable for reading on those
+long plane flights or train rides.</p>
+
+<p>Lore is also used to generate pages with consistent headers and footers for
+the twistedmatrix.com web site -- not just the Twisted documentation.
+This is shows the inherent flexibility in Lore's model of being easily
+configurable via an HTML template,
+a feature which none of the major
+document generation systems support for their HTML output.</p>
+
+<h2>Further Resources</h2>
+
+<ul>
+ <li><a
+ href="http://twistedmatrix.com/documents/TwistedDocs/TwistedDocs-1.0.3/man/lore-man.xhtml"><code
+ class="shell">lore(1)</code> man page</a></li>
+ <li><a
+ href="http://twistedmatrix.com/documents/TwistedDocs/TwistedDocs-1.0.3/howto/doc-standard">Lore guidelines</a></li>
+ <li><a
+ href="http://twistedmatrix.com/documents/TwistedDocs/TwistedDocs-1.0.3/howto/lore">Lore HOWTO</a></li>
+ <li><a
+ href="http://twistedmatrix.com/documents/TwistedDocs/TwistedDocs-1.0.1/examples/example.html">Skeleton Lore document</a></li>
+ <li>The <a
+ href="http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/~checkout~/doc/howto/stylesheet.css?rev=1.16&amp;content-type=text/css&amp;cvsroot=Twisted">stylesheet</a> and <a
+ href="http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/~checkout~/doc/howto/template.tpl?rev=1.6&amp;content-type=text/plain&amp;cvsroot=Twisted">template</a> used by the Twisted documentation</li>
+ <li><a href="http://www.w3.org/TR/xhtml1/">The XHTML specification</a></li>
+ <li><a href="http://www.latex-project.org">LaTeX project home page</a></li>
+ <li><a href="http://www.lyx.org">LyX</a></li>
+ <li><a href="http://www.docbook.org">Docbook</a></li>
+ <li><a href="http://www.python10.com/p10-papers/09/index.htm">Zadka, Moshe and Lefkowitz, Glyph, The Twisted Network Framework, The Tenth International Python Conference Proceedings</a></li>
+</ul>
+
+</body></html>
diff --git a/doc/historic/2003/pycon/pb/pb-client1.py b/doc/historic/2003/pycon/pb/pb-client1.py
new file mode 100755
index 0000000..7814fb7
--- /dev/null
+++ b/doc/historic/2003/pycon/pb/pb-client1.py
@@ -0,0 +1,46 @@
+#! /usr/bin/python
+
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class Client:
+ def connect(self):
+ deferred = pb.getObjectAt("localhost", 8800, 30)
+ deferred.addCallbacks(self.got_obj, self.err_obj)
+ # when the Deferred fires (i.e. when the connection is established and
+ # we receive a reference to the remote object), the 'got_obj' callback
+ # will be run
+
+ def got_obj(self, obj):
+ print "got object:", obj
+ self.server = obj
+ print "asking it to add"
+ def2 = self.server.callRemote("add", 1, 2)
+ def2.addCallbacks(self.add_done, self.err)
+ # this Deferred fires when the method call is complete
+
+ def err_obj(self, reason):
+ print "error getting object", reason
+ self.quit()
+
+ def add_done(self, result):
+ print "addition complete, result is", result
+ print "now trying subtract"
+ d = self.server.callRemote("subtract", 5, 12)
+ d.addCallbacks(self.sub_done, self.err)
+
+ def err(self, reason):
+ print "Error running remote method", reason
+ self.quit()
+
+ def sub_done(self, result):
+ print "subtraction result is", result
+ self.quit()
+
+ def quit(self):
+ print "shutting down"
+ reactor.stop()
+
+c = Client()
+c.connect()
+reactor.run()
diff --git a/doc/historic/2003/pycon/pb/pb-server1.py b/doc/historic/2003/pycon/pb/pb-server1.py
new file mode 100755
index 0000000..c0fb43f
--- /dev/null
+++ b/doc/historic/2003/pycon/pb/pb-server1.py
@@ -0,0 +1,16 @@
+#! /usr/bin/python
+
+from twisted.spread import pb
+import twisted.internet.app
+
+class ServerObject(pb.Root):
+ def remote_add(self, one, two):
+ answer = one + two
+ print "returning result:", answer
+ return answer
+ def remote_subtract(self, one, two):
+ return one - two
+
+app = twisted.internet.app.Application("server1")
+app.listenTCP(8800, pb.BrokerFactory(ServerObject()))
+app.run(save=0)
diff --git a/doc/historic/2003/pycon/pb/pb-slides.py b/doc/historic/2003/pycon/pb/pb-slides.py
new file mode 100755
index 0000000..1a9aea6
--- /dev/null
+++ b/doc/historic/2003/pycon/pb/pb-slides.py
@@ -0,0 +1,240 @@
+#! /usr/bin/python
+
+from slides import Lecture, NumSlide, Slide, Bullet, SubBullet, PRE, URL
+
+class Raw:
+ def __init__(self, title, html):
+ self.title = title
+ self.html = html
+ def toHTML(self):
+ return self.html
+
+class HTML(Raw):
+ def __init__(self, html):
+ self.html = html
+
+server_lore = """<div class="py-listing">
+<pre><span class="py-src-keyword">class</span> <span class="py-src-identifier">ServerObject</span><span class="py-src-op">(</span><span class="py-src-parameter">pb</span><span class="py-src-op">.</span><span class="py-src-parameter">Referenceable</span><span class="py-src-op">)</span><span class="py-src-op">:</span><span class="py-src-newline"></span>
+<span class="py-src-indent"> </span><span class="py-src-keyword">def</span> <span class="py-src-identifier">remote_add</span><span class="py-src-op">(</span><span class="py-src-parameter">self</span><span class="py-src-op">,</span> <span class="py-src-parameter">one</span><span class="py-src-op">,</span> <span class="py-src-parameter">two</span><span class="py-src-op">)</span><span class="py-src-op">:</span><span class="py-src-newline">
+</span><span class="py-src-indent"> </span><span class="py-src-variable">answer</span> <span class="py-src-op">=</span> <span class="py-src-variable">one</span> <span class="py-src-op">+</span> <span class="py-src-variable">two</span><span class="py-src-newline">
+</span> <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;returning result:&quot;</span><span class="py-src-op">,</span> <span class="py-src-variable">answer</span><span class="py-src-newline">
+</span> <span class="py-src-keyword">return</span> <span class="py-src-variable">answer</span><span class="py-src-endmarker"></span></pre>
+<div class="py-caption">Server Code</div>
+</div>
+"""
+
+client_lore = """<div class="py-listing"><pre>
+<span class="py-src-nl"></span> <span class="py-src-dedent"></span><span class="py-src-keyword">def</span> <span class="py-src-identifier">got_RemoteReference</span><span class="py-src-op">(</span><span class="py-src-parameter">remoteref</span><span class="py-src-op">)</span><span class="py-src-op">:</span><span class="py-src-newline">
+</span> <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;asking it to add&quot;</span><span class="py-src-newline">
+</span> <span class="py-src-variable">deferred</span> <span class="py-src-op">=</span> <span class="py-src-variable">remoteref</span><span class="py-src-op">.</span><span class="py-src-variable">callRemote</span><span class="py-src-op">(</span><span class="py-src-string">&quot;add&quot;</span><span class="py-src-op">,</span> <span class="py-src-number">1</span><span class="py-src-op">,</span> <span class="py-src-number">2</span><span class="py-src-op">)</span><span class="py-src-newline">
+</span> <span class="py-src-variable">deferred</span><span class="py-src-op">.</span><span class="py-src-variable">addCallbacks</span><span class="py-src-op">(</span><span class="py-src-variable">add_done</span><span class="py-src-op">,</span> <span class="py-src-variable">err</span><span class="py-src-op">)</span><span class="py-src-newline">
+</span> <span class="py-src-comment"># this Deferred fires when the method call is complete
+</span> <span class="py-src-dedent"></span><span class="py-src-keyword">def</span> <span class="py-src-identifier">add_done</span><span class="py-src-op">(</span><span class="py-src-parameter">result</span><span class="py-src-op">)</span><span class="py-src-op">:</span><span class="py-src-newline">
+</span><span class="py-src-indent"> </span><span class="py-src-keyword">print</span> <span class="py-src-string">&quot;addition complete, result is&quot;</span><span class="py-src-op">,</span> <span class="py-src-variable">result</span><span class="py-src-newline">
+</span><span class="py-src-endmarker"></span></pre><div class="py-caption">Client Code</div></div>
+"""
+
+
+# title graphic: PB peanut butter jar, "Twist(ed)" on lid
+lecture = Lecture(
+ "Perspective Broker: Translucent RPC in Twisted",
+ # intro
+ Raw("Title", """
+ <h1>Perspective Broker: Translucent RPC in Twisted</h1>
+ <h2>PyCon 2003</h2>
+ <h2>Brian Warner &lt; warner @ lothar . com &gt; </h2>
+ """),
+
+ Slide("Introduction",
+ Bullet("Overview/definition of RPC"),
+ Bullet("What is Perspective Broker?"),
+ Bullet("How do I use it?"),
+ Bullet("Security Issues"),
+ Bullet("Future Directions"),
+ ),
+
+ Slide("Remote Procedure Calls",
+ Bullet("Action at a distance: separate processes, safely telling each other what to do",
+ SubBullet("Separate memory spaces"),
+ SubBullet("Usually on different machines"),
+ ),
+ Bullet("Frequently called RMI these days: Remote Method Invocation"),
+ Bullet("Three basic parts: Addressing, Serialization, Waiting"),
+ ),
+
+ Slide("Addressing",
+ Bullet("What program are you talking to?",
+ SubBullet("hostname, port number"),
+ SubBullet("Some systems use other namespaces: sunrpc")
+ ),
+ Bullet("Which object in that program?"),
+ Bullet("Which method do you want to run?"),
+ Bullet("Related issues",
+ SubBullet("How do you know what the arguments are?"),
+ SubBullet("(do you care?)"),
+ SubBullet("How do you know what methods are available?"),
+ SubBullet("(do you care?)"),
+ ),
+ ),
+
+ Slide("Serialization",
+ Bullet("What happens to the arguments you send in?"),
+ Bullet("What happens to the results that are returned?",
+ SubBullet("Representation differences: endianness, word length"),
+ SubBullet("Dealing with user-defined types"),
+ ),
+ Bullet("How to deal with references"),
+ ),
+ Slide("The Waiting (is the hardest part)",
+ Bullet("Asynchronous: results come later, or not at all"),
+ Bullet("Need to do other work while waiting"),
+ ),
+
+ Slide("Whither Translucence?",
+ Bullet("Not 'Transparent': don't pretend remote objects are really local",
+ SubBullet("CORBA (in C) does this, makes remote calls look like local calls"),
+ SubBullet("makes it hard to deal with the async nature of RPC"),
+ ),
+ Bullet("Not 'Opaque': make it easy to deal with the differences",
+ SubBullet("Including extra failure modes, delayed results"),
+ ),
+
+ Bullet("Exceptions and Deferreds to the rescue")),
+
+ Slide("Other RPC protocols",
+ Bullet("HTML"),
+ Bullet("XML-RPC"),
+ Bullet("CORBA"),
+ Bullet("when you control both ends of the wire, use PB"),
+ ),
+
+ Raw("Where does PB fit?",
+ """<h2>PB sits on top of <span class=\"py-src-identifier\">twisted.internet</span></h2>
+ <img src=\"twisted-overview.png\" />
+ """),
+
+ Slide("pb.RemoteReference",
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.Referenceable</span>: Object which can be accessed by remote systems."),
+ SubBullet(HTML("Defines methods like <span class=\"py-src-identifier\">remote_foo</span> and <span class=\"py-src-identifier\">remote_bar</span> which can be invoked remotely.")),
+ SubBullet(HTML("Methods without the <span class=\"py-src-identifier\">remote_</span> prefix are local-only.")),
+ ),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.RemoteReference</span>: Used by distant program to invoke methods."),
+ SubBullet(HTML("Offers <span class=\"py-src-identifier\">.callRemote()</span> to trigger remote method on a corresponding <span class=\"py-src-identifier\">pb.Referenceable</span>.")),
+ ),
+ ),
+
+ Raw("Sample code",
+ "<h2>Sample Code</h2>" + server_lore + client_lore),
+ #Slide("Simple Demo"),
+ # "better demo: manhole, or reactor running in another thread"
+
+ #build up from callRemote?
+ Slide("What happens to those arguments?",
+ Bullet("Basic structures should travel transparently",
+ SubBullet("Actually quite difficult in some languages"),
+ ),
+ Bullet("Object graph should remain the same",
+ SubBullet("Serialization context"),
+ SubBullet("(same issues as Pickle)")),
+ Bullet("Instances of user-defined classes require more care",
+ SubBullet("User-controlled unjellying"),)
+ ),
+
+ #serialization (skip banana)
+ Slide("40% More Sandwich Puns Than The Leading Brand",
+ Bullet("twisted.spread: python package holding other modules"),
+ Bullet("PB: remote method invocation"),
+ Bullet("Jelly: mid-level object serialization"),
+ Bullet("Banana: low-level serialization of s-expressions"),
+ Bullet("Taster: security context, decides what may be received"),
+ Bullet("Marmalade: like Jelly, but involves XML, so it's bitter"),
+ Bullet("better than the competition",
+ SubBullet("CORBA: few or no sandwich puns"),
+ SubBullet("XML-RPC: barely pronounceable"),
+ ),
+ ),
+
+ Slide("Jellying objects",
+ Bullet("'Jellying' vs 'Unjellying'"),
+ Bullet("Immutable objects are copied whole"),
+ Bullet("Mutable objects get reference IDs to insure shared references remain shared",
+ SubBullet("(within the same Jellying context)")),
+ ),
+
+ Slide("Jellying instances",
+ Bullet(HTML("User classes inherit from one of the <span class=\"py-src-identifier\">pb.flavor</span> classes")),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.Referenceable</span>: methods can be called remotely")),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.Copyable</span>: contents are selectively copied")),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.Cacheable</span>: contents are copied and kept up to date")),
+ Bullet(HTML("Classes define <span class=\"py-src-identifier\">.getStateToCopy</span> and other methods to restrict exported state")),
+ ),
+
+ Slide("pb.Copyable example",
+ PRE("""class SenderPond(FrogPond, pb.Copyable):
+ def getStateToCopy(self):
+ d = self.__dict__.copy()
+ d['frogsAndToads'] = d['numFrogs'] + d['numToads']
+ del d['numFrogs']
+ del d['numToads']
+ return d
+
+class ReceiverPond(pb.RemoteCopy):
+ def setCopyableState(self, state):
+ self.__dict__ = state
+ self.localCount = 12
+ def count(self):
+ return self.frogsAndToads
+
+pb.setUnjellyableForClass(SenderPond, ReceiverPond)
+""")),
+
+ Slide("Secure Unjellying",
+ Bullet("Pickle has security problems",
+ SubBullet("Pickle will import any module the sender requests."),
+ SubBullet(HTML("2.3 gave up, removed safety checks like <span class=\"py-src-identifier\">__safe_for_unpickling__</span> .")),
+ ),
+ Bullet("Jelly attempts to be safe in the face of hostile clients",
+ SubBullet("All classes rejected by default"),
+ SubBullet(HTML("<span class=\"py-src-identifier\">registerUnjellyable()</span> used to accept safe ones")),
+ SubBullet(HTML("Registered classes define <span class=\"py-src-identifier\">.setCopyableState</span> and others to process remote state")),
+ ),
+ Bullet("Must mark (by subclassing) to transmit"),
+ ),
+
+ Slide("Transformation of references in transit",
+ Bullet("All referenced objects get turned into their counterparts as they go over the wire"),
+ Bullet("References are followed recursively",
+ SubBullet("Sending a reference to a tree of objects will cause the whole thing to be transferred"),
+ SubBullet("(subject to security restrictions)"),
+ ),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.flavors</span> get reference ids"),
+ SubBullet("They are recognized when they return, transformed into the original reference"),
+ SubBullet("Reference ids are scoped to the connection"),
+ SubBullet("One side-effect: no 'third party' references"),
+ ),
+ ),
+
+ Slide("Perspectives: pb.cred and the Identity/Service model",
+ Bullet("A layer to provide common authentication services to Twisted applications"),
+ Bullet(HTML("<span class=\"py-src-identifier\">Identity</span>: named user accounts with passwords")),
+ Bullet(HTML("<span class=\"py-src-identifier\">Service</span>: something a user can request access to")),
+ Bullet(HTML("<span class=\"py-src-identifier\">Perspective</span>: user accessing a service")),
+ Bullet(HTML("<span class=\"py-src-identifier\">pb.Perspective</span>: first object, a <span class=\"py-src-identifier\">pb.Referenceable</span> used to access everything else")),
+ ),
+ #picture would help
+
+ Slide("Future directions",
+ Bullet("Other language bindings: Java, elisp, Haskell, Scheme, JavaScript, OCaml"),
+ # donovan is doing the JavaScript port
+ Bullet("Other transports: UDP, Airhook"),
+ Bullet("Componentization"),
+ Bullet("Performance improvements: C extension for Jelly"),
+ Bullet("Refactor addressing model: PB URLs"),
+ ),
+
+ Slide("Questions", Bullet("???")),
+
+ )
+
+lecture.renderHTML("slides", "slide-%02d.html", css="stylesheet.css")
+
diff --git a/doc/historic/2003/pycon/pb/pb.html b/doc/historic/2003/pycon/pb/pb.html
new file mode 100644
index 0000000..95f1ebe
--- /dev/null
+++ b/doc/historic/2003/pycon/pb/pb.html
@@ -0,0 +1,966 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>Perspective Broker: <q>Translucent</q> Remote Method calls in Twisted</title>
+ </head>
+
+<body>
+
+<h1>Perspective Broker: <q>Translucent</q> Remote Method calls in Twisted</h1>
+
+<ul>
+<li><a href="http://www.lothar.com">Brian Warner</a>:
+<code>&lt;warner@lothar.com&gt;</code>
+</li>
+</ul>
+
+<h2>Abstract</h2>
+
+<p>One of the core services provided by the Twisted networking framework is
+<q>Perspective Broker</q>, which provides a clean, secure, easy-to-use
+Remote Procedure Call (RPC) mechanism. This paper explains the novel
+features of PB, describes the security model and its implementation, and
+provides brief examples of usage.</p>
+
+<p>PB is used as a foundation for many other services in Twisted, as well as
+projects built upon the Twisted framework. twisted.web servers can delegate
+responsibility for different portions of URL-space by distributing PB
+messages to the object that owns that subspace. twisted.im is an
+instant-messaging protocol that runs over PB. Applications like CVSToys and
+the BuildBot use PB to distribute notices every time a CVS commit has
+occurred. Using Perspective Broker as the RPC layer allows these projects to
+stay focused on the interesting parts.</p>
+
+<p>The PB protocol is not limited to Python. There is a working Java
+implementation available from the Twisted web site, as is an Emacs-Lisp
+version (which can be used to control a PB-enabled application from within
+your editing session, or effectively embed a Python interpreter in Emacs).
+Python's dynamic and introspective nature makes Perspective Broker easier to
+implement (and very convenient to use), but neither are strictly necessary.
+With a set of callback tables and a good dictionary implementation, it would
+be possible to implement the same protocol in C, C++, Perl, or other
+languages.</p>
+
+<h2>Overview</h2>
+
+<h3>Features</h3>
+
+<p>Perspective Broker provides the following basic RPC features.</p>
+
+<ul>
+ <li><strong>remotely-invokable methods</strong>: certain methods (those
+ with names that start with <q>remote_</q>) of
+ <code>pb.Referenceable</code> objects can be invoked by remote clients who
+ hold matching <code>pb.RemoteReference</code> objects.</li>
+
+ <li><strong>transparent, controllable object serialization</strong>: other
+ objects sent through those remote method invocations (either as arguments
+ or in the return value) will be automatically serialized. The data that is
+ serialized, and the way they are represented on the remote side, depends
+ upon which <code>twisted.pb.flavor</code> class they inherit from, and
+ upon overridable methods to get and set state.</li>
+
+ <li><strong>per-connection object ids</strong>: certain objects that are
+ passed by reference are tracked when they are sent over a wire. If the
+ receiver sends back the reference it received, the sender will see their
+ original object come back to them.</li>
+
+ <li><strong>twisted.cred authentication layer</strong>: provides common
+ username/password verification functions. <code>pb.Viewable</code> objects
+ keep a user reference with them, so remotely-invokable methods can find
+ out who invoked them.</li>
+
+ <li><strong>remote exception reporting</strong>: exceptions that occur in
+ remote methods are wrapped in <code>Failure</code> objects and serialized
+ so they can be provided to the caller. All the usual traceback information
+ is available on the invoking side.</li>
+
+ <li><strong>runs over arbitrary byte-pipe transports</strong>: including
+ TCP, UNIX-domain sockets, and SSL connections. UDP support (in the form of
+ Airhook) is being developed.</li>
+
+ <li><strong>numerous sandwich-related puns</strong>: PB, Jelly, Banana,
+ <code>twisted.spread</code>, Marmalade, Tasters, and Flavors. By contrast,
+ CORBA and XML-RPC have few, if any, puns in their naming conventions.</li>
+
+</ul>
+
+<h3>Example</h3>
+
+<p>Here is a simple example of PB in action. The server code creates an
+object that can respond to a few remote method calls, and makes it available
+on a TCP port. The client code connects and runs two methods.</p>
+
+<a href="pb-server1.py" class="py-listing" skipLines="2">pb-server1.py</a>
+<a href="pb-client1.py" class="py-listing" skipLines="2">pb-client1.py</a>
+
+<p>When this is run, the client emits the following progress messages:</p>
+
+<pre class="shell">
+% <em>./pb-client1.py</em>
+got object: &lt;twisted.spread.pb.RemoteReference instance at 0x817cab4&gt;
+asking it to add
+addition complete, result is 3
+now trying subtract
+subtraction result is -7
+shutting down
+</pre>
+
+<p>This example doesn't demonstrate instance serialization, exception
+reporting, authentication, or other features of PB. For more details and
+examples, look at the PB <q>howto</q> docs at <a
+href="http://twistedmatrix.com/documents/howto/">twistedmatrix.com</a>.</p>
+
+<h2>Why <q>Translucent</q> References?</h2>
+
+<p>Remote function calls are not the same as local function calls. Remote
+calls are asynchronous. Data exchanged with a remote system may be
+interpreted differently depending upon version skew between the two systems.
+Method signatures (number and types of parameters) may differ. More failure
+modes are possible with RPC calls than local ones.</p>
+
+<p><q>Transparent</q> RPC systems attempt to hide these differences, to make
+remote calls look the same as local ones (with the noble intention of making
+life easier for programmers), but the differences are real, and hiding them
+simply makes them more difficult to deal with. PB therefore provides
+<q>translucent</q> method calls: it exposes these differences, but offers
+convenient mechanisms to handle them. Python's flexible object model and
+exception handling take care of part of the problem, while Twisted's
+Deferred class provides a clean way to deal with the asynchronous nature of
+RPC.</p>
+
+<h3>Asynchronous Invocation</h3>
+
+<p>A fundamental difference between local function calls and remote ones is
+that remote ones are always performed asynchronously. Local function calls
+are generally synchronous (at least in most programming languages): the
+caller is blocked until the callee finishes running and possibly returns a
+value. Local functions which might block (loosely defined as those which
+would take non-zero or indefinite time to run on infinitely fast hardware)
+are usually marked as such, and frequently provide alternative APIs to run
+in an asynchronous manner. Examples of blocking functions are
+<code>select()</code> and its less-generalized cousins:
+<code>sleep()</code>, <code>read()</code> (when buffers are empty), and
+<code>write()</code> (when buffers are full).</p>
+
+<p>Remote function calls are generally assumed to take a long time. In
+addition to the network delays involved in sending arguments and receiving
+return values, the remote function might itself be blocking.</p>
+
+<p><q>Transparent</q> RPC systems, which pretend that the remote system is
+really local, usually offer only synchronous calls. This prevents the
+program from getting other work done while the call is running, and causes
+integration problems with GUI toolkits and other event-driven
+frameworks.</p>
+
+<h3>Failure Modes</h3>
+
+<p>In addition to the usual exceptions that might be raised in the course of
+running a function, remotely invoked code can cause other errors. The
+network might be down, the remote host might refuse the connection (due to
+authorization failures or resource-exhaustion issues), the remote end might
+have a different version of the code and thus misinterpret serialized
+arguments or return a corrupt response. Python's flexible exception
+mechanism makes these errors easy to report: they are just more exceptions
+that could be raised by the remote call. In other languages, this requires a
+special API to report failures via a different path than the normal
+response.</p>
+
+<h3>Deferreds to the rescue</h3>
+
+<p>In PB, Deferreds are used to handle both the asynchronous nature of the
+method calls and the various kinds of remote failures that might occur. When
+the method is invoked, PB returns a Deferred object that will be fired
+later, when the response (success or failure) is received from the remote
+end. The caller (the one who invoked <code>callRemote</code>) is free to
+attach callback and errback handlers to the Deferred. If an exception is
+raised (either by the remote code or a network failure during processing),
+the errback will be run with the wrapped exception. If the function
+completes normally, the callback is run.</p>
+
+<p>By using Deferreds, the invoking program can get other work done while it
+is waiting for the results. Failure is handled just as cleanly as
+success.</p>
+
+<p>In addition, the remote method can itself return a <code>Deferred</code>
+instead of an actual return value. When that <code>Deferreds</code> fires,
+the data given to the callback will be serialized and returned to the
+original caller. This allows the remote server to perform other work as
+well, putting off the answer until one is available.</p>
+
+
+<h2>Calling Remote Methods</h2>
+
+<p>Perspective Broker is first and foremost a mechanism for remote method
+calls: doing something to a local object which causes a method to get run on
+a distant one. The process making the request is usually called the
+<q>client</q>, and the process which hosts the object that actually runs the
+method is called the <q>server</q>. Note, however, that method requests can
+go in either direction: instead of distinguishing <q>client</q> and
+<q>server</q>, it makes more sense to talk about the <q>sender</q> and
+<q>receiver</q> for any individual method call. PB is symmetric, and the
+only real difference between the two ends is that one initiated the original
+TCP connection and the other accepted it.</p>
+
+<p>With PB, the local object is an instance of
+<code>twisted.spread.pb.RemoteReference</code>, and you <q>do something</q>
+to it by calling its <code>.callRemote</code> method. This call accepts a
+method name and an argument list (including keyword arguments). Both are
+serialized and sent to the receiving process, and the call returns a
+<code>Deferred</code>, to which you can add callbacks. Those callbacks will
+be fired later, when the response returns from the remote end.</p>
+
+<p>That local RemoteReference points at a
+<code>twisted.spread.pb.Referenceable</code> object living in the other
+program (or one of the related callable flavors). When the request comes
+over the wire, PB constructs a method name by prepending
+<code>remote_</code> to the name requested by the remote caller. This method
+is looked up in the <code>pb.Referenceable</code> and invoked. If an
+exception is raised (including the <code>AttributeError</code> that results
+from a bad method name), the error is wrapped in a <code>Failure</code>
+object and sent back to the caller. If it succeeds, the result is serialized
+and sent back.</p>
+
+<p>The caller's Deferred will either have the callback run (if the method
+completed normally) or the errback run (if an exception was raised). The
+Failure object given to the errback handler allows a full stack trace to be
+displayed on the calling end.</p>
+
+<p>For example, if the holder of the <code>RemoteReference</code> does <code
+class="python">rr.callRemote("foo", 1, 3)</code>, the corresponding
+<code>Referenceable</code> will be invoked with <code
+class="python">r.remote_foo(1, 3)</code>. A <code>callRemote</code> of
+<q><code>bar</code></q> would invoke <code>remote_bar</code>, etc.</p>
+
+<h3>Obtaining other references</h3>
+
+<p>Each <code>pb.RemoteReference</code> object points to a
+<code>pb.Referenceable</code> instance in some other program. The first such
+reference must be acquired with a bootstrapping function like
+<code>pb.getObjectAt</code>, but all subsequent ones are created when a
+<code>pb.Referenceable</code> is sent as an argument to (or a return value
+from) a remote method call.</p>
+
+<p>When the arguments or return values contain references to other objects,
+the object that appears on the other side of the wire depends upon the type
+of the referred object. Basic types are simply copied: a dictionary of lists
+will appear as a dictionary of lists, with internal references preserved on
+a per-method-call basis (just as Pickle will preserve internal references
+for everything pickled at the same time). Class instances are restricted,
+both to avoid confusion and for security reasons.</p>
+
+<h3>Transferring Instances</h3>
+
+<p>PB only allows certain kinds of objects to be transferred to and from
+remote processes. Most of these restrictions are implemented in the <a
+href="#jelly">Jelly</a> serialization layer, described below. In general, to
+send an object over the wire, it must either be a basic python type (list,
+dictionary, etc), or an instance of a class which is derived from one of the
+four basic <em>PB Flavors</em>: <code>Referenceable</code>,
+<code>Viewable</code>, <code>Copyable</code>, and <code>Cacheable</code>.
+Each flavor has methods which define how the object should be treated when
+it needs to be serialized to go over the wire, and all have related classes
+that are created on the remote end to represent them.</p>
+
+<p>There are a few kinds of callable classes. All are represented on the
+remote system with <code>RemoteReference</code> instances.
+<code>callRemote</code> can be used on these RemoteReferences, causing
+methods with various prefixes to be invoked.</p>
+
+<table border="1">
+ <tr>
+ <th>Local Class</th>
+ <th>Remote Representation</th>
+ <th>method prefix</th>
+ </tr>
+ <tr>
+ <td><code>Referenceable</code></td>
+ <td><code>RemoteReference</code></td>
+ <td><code>remote_</code></td>
+ </tr>
+ <tr>
+ <td><code>Viewable</code></td>
+ <td><code>RemoteReference</code></td>
+ <td><code>view_</code></td>
+ </tr>
+</table>
+
+<p><code>Viewable</code> (and the related <code>Perspective</code> class)
+are described later (in <a href="#authorization">Authorization</a>). They
+provide a secure way to let methods know <em>who</em> is calling them. Any
+time a <code>Referenceable</code> (or <code>Viewable</code>) is sent over
+the wire, it will appear on the other end as a <code>RemoteReference</code>.
+If any of these references are sent back to the system they came from, they
+emerge from the round trip in their original form.</p>
+
+<p>Note that RemoteReferences cannot be sent to anyone else (there are no
+<q>third-party references</q>): they are scoped to the connection between
+the holder of the <code>Referenceable</code> and the holder of the
+<code>RemoteReference</code>. (In fact, the <code>RemoteReference</code> is
+really just an index into a table maintained by the owner of the original
+<code>Referenceable</code>).</p>
+
+<p>There are also two data classes. To send an instance over the wire, it
+must belong to a class which inherits from one of these.</p>
+
+<table border="1">
+ <tr>
+ <th>Local Class</th>
+ <th>Remote Representation</th>
+ </tr>
+ <tr>
+ <td><code>Copyable</code></td>
+ <td><code>RemoteCopy</code></td>
+ </tr>
+ <tr>
+ <td><code>Cacheable</code></td>
+ <td><code>RemoteCache</code></td>
+ </tr>
+</table>
+
+<h3>pb.Copyable</h3>
+<a name="pb.Copyable"></a>
+
+<p><code>Copyable</code> is used to allow class instances to be sent over
+the wire. <code>Copyable</code>s are copy-by-value, unlike
+<code>Referenceable</code>s which are copy-by-reference.
+<code>Copyable</code> objects have a method called
+<code>getStateToCopy</code> which gets to decide how much of the object
+should be sent to the remote system: the default simply copies the whole
+<code>__dict__</code>. The receiver must register a <code>RemoteCopy</code>
+class for each kind of <code>Copyable</code> that will be sent to it: this
+registration (described later in <a href="#unjellyableRegistry">Representing
+Instances</a>) maps class names to actual classes. Apart from being a
+security measure (it emphasizes the fact that the process is receiving data
+from an untrusted remote entity and must decide how to interpret it safely),
+it is also frequently useful to distinguish a copy of an object from the
+original by holding them in different classes.</p>
+
+<p><code>getStateToCopy</code> is frequently used to remove attributes that
+would not be meaningful outside the process that hosts the object, like file
+descriptors. It also allows shared objects to hold state that is only
+available to the local process, including passwords or other private
+information. Because the default serialization process recursively follows
+all references to other objects, it is easy to accidentally send your entire
+program to the remote side. Explicitly creating the state object (creating
+an empty dictionary, then populating it with only the desired instance
+attributes) is a good way to avoid this.</p>
+
+<p>The fact that PB will refuse to serialize objects that are neither basic
+types nor explicitly marked as being transferable (by subclassing one of the
+pb.flavors) is another way to avoid the <q>don't tug on that, you never know
+what it might be attached to</q> problem. If the object you are sending
+includes a reference to something that isn't marked as transferable, PB will
+raise an InsecureJelly exception rather than blindly sending it anyway (and
+everything else it references).</p>
+
+<p>Finally, note that <code>getStateToCopy</code> is distinct from the
+<code>__getstate__</code> method used by Pickle, and they can return
+different values. This allows objects to be persisted (across time)
+differently than they are transmitted (across [memory]space).</p>
+
+<h3>pb.Cacheable</h3>
+<a name="pb.Cacheable"></a>
+
+<p><code>Cacheable</code> is a variant of <code>Copyable</code> which is
+used to implement remote caches. When a <code>Cacheable</code> is sent
+across a wire, a method named <code>getStateToCacheAndObserveFor</code> is
+used to simultaneously get the object's current state and to register an
+<q>Observer</q> which lives next to the <code>Cacheable</code>. The Observer
+is effectively a <code>RemoteReference</code> that points at the remote
+cache. Each time the cached object changes, it uses its Observers to tell
+all the remote caches about the change. The <q>setter</q> methods can just
+call <code class="python">observer.callRemote("setFoo", newvalue)</code> for
+all their observers.</p>
+
+<p>On the remote end, a <code>RemoteCache</code> object is created, which
+populates the original object's state just as <code>RemoteCopy</code> does.
+When changes are made, the Observers remotely invoke methods like
+<code>observe_setFoo</code> in the <code>RemoteCache</code> to perform the
+updates.</p>
+
+<p>As <code>RemoteCache</code> objects go away, their Observers go away too,
+and call <code>stoppedObserving</code> so they can be removed from the
+list.</p>
+
+<p>The PB <a href="http://twistedmatrix.com/documents/howto/"
+><q>howto</q> docs</a> have more information and complete examples of both
+<code>pb.Copyable</code> and <code>pb.Cacheable</code>.</p>
+
+
+<h2>Authorization</h2>
+<a name="authorization"></a>
+
+<p>As a framework, Perspective Broker (indeed, all of Twisted) was built
+from the ground up. As multiple use cases became apparent, common
+requirements were identified, code was refactored, and layers were developed
+to cleanly serve the needs of all <q>customers</q>. The twisted.cred layer
+was created to provide authorization services for PB as well as other
+Twisted services, like the HTTP server and the various instant messaging
+protocols. The abstract notions of identity and authority it uses are
+intended to match the common needs of these various protocols: specific
+applications can always use subclasses that are more appropriate for their
+needs.</p>
+
+<h3>Identity and Perspectives</h3>
+
+<p>In twisted.cred, <q>Identities</q> are usernames (with passwords),
+represented by <code>Identity</code> objects. Each identity has a
+<q>keyring</q> which authorizes it to access a set of objects called
+<q>Perspectives</q>. These perspectives represent accounts or other
+capabilities; each belongs to a single <q>Service</q>. There may be multiple
+Services in a single application; in fact the flexible nature of Twisted
+makes this easy. An HTTP server would be a Service, and an IRC server would
+be another one.</p>
+
+<p>As an example, a login service might have perspectives for Alice, Bob,
+and Charlie, and there might also be an Admin perspective. Alice has admin
+capabilities. In addition, let us say the same application has a chat
+service with accounts for each person (but no special administrator
+account).</p>
+
+<p>So, in this example, Alice's keyring gives her access to three
+perspectives: login/Alice, login/Admin, and chat/Alice. Bob only gets two:
+login/Bob and chat/Bob. <code>Perspective</code> objects have names and
+belong to <code>Service</code> objects, but the
+<code>Identity.keyring</code> is a dictionary indexed by (serviceName,
+perspectiveName) pairs. It uses names instead of object references because
+the <code>Perspective</code> object might be created on demand. The keys
+include the service name because Perspective names are scoped to a single
+service.</p>
+
+<h3>pb.Perspective</h3>
+
+<p>The PB-specific subclass of the generic <code>Perspective</code> class is
+also capable of remote execution. The login process results in the
+authorized client holding a special kind of <code>RemoteReference</code>
+that will allow it to invoke <code>perspective_</code> methods on the
+matching <code>pb.Perspective</code> object. In PB applications that use the
+<code>twisted.cred</code> authorization layer, clients get this reference
+first. The client is then dependent upon the Perspective to provide
+everything else, so the Perspective can enforce whatever security policy it
+likes.</p>
+
+<p>(Note that the <code>pb.Perspective</code> class is not actually one of
+the serializable PB flavors, and that instances of it cannot be sent
+directly over the wire. This is a security feature intended to prevent users
+from getting access to somebody else's <code>Perspective</code> by mistake,
+perhaps when a <q>list all users</q> command sends back an object which
+includes references to other Perspectives.)</p>
+
+<p>PB provides functions to perform a challenge-response exchange in which
+the remote client proves their identity to get that <code>Perspective</code>
+reference. The <code>Identity</code> object holds a password and uses an MD5
+hash to verify that the remote user knows the password without sending it in
+cleartext over the wire. Once the remote user has proved their identity,
+they can request a reference to any <code>Perspective</code> permitted by
+their <code>Identity</code>'s keyring.</p>
+
+<p>There are twisted.cred functions (twisted.enterprise.dbcred) which can
+pull user information out of a database, and it is easy to create modules
+that could check /etc/passwd or LDAP instead. Authorization can then be
+centralized through the Perspective object: each object that is accessible
+remotely can be created with a pointer to the local Perspective, and objects
+can ask that Perspective whether the operation is allowed before performing
+method calls.</p>
+
+<p>Most clients use a helper function called <code>pb.connect()</code> to
+get the first Perspective reference: it takes all the necessary identifying
+information (host, port, username, password, service name, and perspective
+name) and returns a <code>Deferred</code> that will be fired when the
+<code>RemoteReference</code> is available. (This may change in the future:
+there are plans afoot to use a URL-like scheme to identify the Perspective,
+which will probably mean a new helper function).</p>
+
+<h3>Viewable</h3>
+
+<p>There is a special kind of <code>Referenceable</code> called
+<code>pb.Viewable</code>. Its remote methods (all named <code>view_</code>)
+are called with an extra argument that points at the
+<code>Perspective</code> the client is using. This allows the same
+<code>Referenceable</code> to be shared among multiple clients while
+retaining the ability to treat those clients differently. The methods can
+check with the Perspective to see if the request should be allowed, and can
+use per-client information in processing the request.</p>
+
+<!-- XXX: it would be nice to provide some examples of typical Perspective
+use cases: static pre-defined Perspectives, DB lookup, anonymous access. But
+they would be pretty big, and are probably more appropriate for the
+pb-cred.html HOWTO doc -->
+
+
+<h2>PB Design: Object Serialization</h2>
+
+<p>Fundamental to any calling convention, whether ABI or RPC, is how
+arguments and return values are passed from caller to callee and back. RPC
+systems require data to be turned into a form which can be delivered through
+a network, a process usually known as serialization. Sharing complex types
+(references and class instances) with a remote system requires more care:
+references should all point to the same thing (even though the object being
+referenced might live on either end of the connection), and allowing a
+remote user to create arbitrary class instances in your memory space is a
+security risk that must be controlled.</p>
+
+<p>PB uses its own serialization scheme called <q>Jelly</q>. At the bottom
+end, it uses s-expressions (lists of numbers and strings) to represent the
+state of basic types (lists, dictionaries, etc). These s-expressions are
+turned into a bytestream by the <q>Banana</q> layer, which has an optional C
+implementation for speed. Unserialization for higher-level objects is driven
+by per-class <q>jellyier</q> objects: this flexibility allows PB to offer
+inheritable classes for common operations. <code>pb.Referenceable</code> is
+a class which is serialized by sending a reference to the remote end that
+can be used to invoke remote methods. <code>pb.Copyable</code> is a class
+which creates a new object on the remote end, with methods that the
+developer can override to control how much state is sent or accepted.
+<code>pb.Cacheable</code> sends a full copy the first time it is exchanged,
+but then sends deltas as the object is modified later.</p>
+
+<p>Objects passed over the wire get to decide for themselves how much
+information is actually passed to the remote system. Copy-by-reference
+objects are given a per-connection ID number and stashed in a local
+dictionary. Copy-by-value objects may send their entire
+<code>__dict__</code>, or some subset thereof. If the remote method returns
+a referenceable object that was given to it earlier (either in the same RPC
+call or an earlier one), PB sends the ID number over the wire, which is
+looked up and turned into a proper object reference upon receipt. This
+provides one-sided reference transparency: one end sees objects coming and
+going through remote method calls in exactly the same fashion as through
+local calls. Those references are only capable of very specific operations;
+PB does not attempt to provide full object transparency. As discussed later,
+this is instrumental to security.</p>
+
+<h3>Banana and s-expressions</h3>
+
+<p>The <q>Banana</q> low-level serialization layer converts s-expressions
+which represent basic types (numbers, strings, and lists of numbers,
+strings, or other lists) to and from a bytestream. S-expressions are easy to
+encode and decode, and are flexible enough (when used with a set of tokens)
+to represent arbitrary objects. <q>cBanana</q> is a C extension module which
+performs the encode/decode step faster than the native python
+implementation.</p>
+
+<p>Each s-expression element is converted into a message with two or three
+components: a header, a type marker, and an optional body (used only for
+strings). The header is a number expressed in base 128. The type marker is a
+single byte with the high bit set, that both terminates the header and
+indicate the type of element this message describes (number, list-start,
+string, or tokenized string).</p>
+
+<p>When a connection is first established, a list of strings is sent to
+negotiate the <q>dialect</q> of Banana being spoken. The first dialect known
+to both sides is selected. Currently, the dialect is only used to select a
+list of string tokens that should be specially encoded (for performance),
+but subclasses of Banana could use self.currentDialect to influence the
+encoding process in other ways.</p>
+
+<p>When Banana is used for PB (by negotiating the <q>pb</q> dialect), it has
+a list of 30ish strings that are encoded into two-byte sequences instead of
+being sent as generalized string messages. These string tokens are used to
+mark complex types (beyond the simple lists, strings, and numbers provided
+natively by Banana) and other objects Jelly needs to do its job.</p>
+
+<h3>Jelly</h3>
+<a name="jelly"></a>
+
+<p><code>Jelly</code> handles object serialization. It fills a similar role
+to the standard Pickle module, but has design goals of security and
+portability (especially to other languages) where Pickle favors efficiency
+of representation. In addition, Jelly serializes objects into s-expressions
+(lists of tokens, strings, numbers, and other lists), and lets Banana do the
+rest, whereas Pickle goes all the way down to a bytestream by itself.</p>
+
+<p>Basic python types (apart from strings and numbers, which Banana can
+handle directly) are generally turned into lists with a type token as the
+first element. For example, a python dictionary is turned into a list that
+starts with the string token <q>dictionary</q> and continues with elements
+that are lists of [key, value] pairs. Modules, classes, and methods are all
+transformed into s-expressions that refer to the relevant names. Instances
+are represented by combining the class name (a string) with an arbitrary
+state object (which is usually a dictionary).</p>
+
+<p>Much of the rest of Jelly has to do with safely handling class instances
+(as opposed to basic Python types) and dealing with references to shared
+objects.</p>
+
+<h4>Tracking shared references</h4>
+
+<p>Mutable types are serialized in a way that preserves the identity between
+the same object referenced multiple times. As an example, a list with four
+elements that all point to the same object must look the same on the remote
+end: if it showed up as a list pointing to four independent objects (even if
+all the objects had identical states), the resulting list would not behave
+in the same way as the original. Changing <code>newlist[0]</code> would not
+modify <code>newlist[1]</code> as it ought to.</p>
+
+<p>Consequently, when objects which reference mutable types are serialized,
+those references must be examined to see if they point to objects which have
+already been serialized in the same session. If so, an object id tag of some
+sort is put into the bytestream instead of the complete object, indicating
+that the deserializer should use a reference to a previously-created object.
+This also solves the issue of recursive or circular references: the first
+appearance of an object gets the full state, and all subsequent ones get a
+reference to it.</p>
+
+<p>Jelly manages this reference tracking through an internal
+<code>_Jellier</code> object (in particular through the <code>.cooked</code>
+dictionary). As objects are serialized, their <code>id</code> values are
+stashed. References to those objects that occur after jellying has started
+can be replaced with a <q>dereference</q> marker and the object id.</p>
+
+<p>The scope of this <code>_Jellier</code> object is limited to a single
+call of the <code>jelly</code> function, which in general corresponds to a
+single remote method call. The argument tuple is jellied as a single object
+(a tuple), so different arguments to the same method will share referenced
+objects<span class="footnote">Actually, PB currently jellies the list
+arguments in a separate tuple from the keyword arguments. This issue is
+currently being examined and may be changed in the future</span>, but
+arguments of separate methods will not share them. To do more complex
+caching and reference tracking, certain PB <q>flavors</q> (see below)
+override their <code>jellyFor</code> method to do more interesting things.
+In particular, <code>pb.Referenceable</code> objects have code to insure
+that one which makes a round trip will come back as a reference to the same
+object that was originally sent.</p>
+
+<p>An exception to this <q>one-call scope</q> is provided: if the
+<code>Jellier</code> is created with a <code>persistentStore</code> object,
+all class instances will be passed through it first, and it has the
+opportunity to return a <q>persistent id</q>. If available, this id is
+serialized instead of the object's state. This would allow object references
+to be shared between different invocations of <code>jelly</code>. However,
+PB itself does not use this technique: it uses overridden
+<code>jellyFor</code> methods to provide per-connection shared
+references.</p>
+
+<h4>Representing Instances</h4>
+<a name="unjellyableRegistry"></a>
+
+<p>Each class gets to decide how it should be represented on a remote
+system. Sending and receiving are separate actions, performed in separate
+programs on different machines. So, to be precise, each class gets to decide
+two things. First, they get to specify how they should be sent to a remote
+client: what should happen when an instance is serialized (or <q>jellied</q>
+in PB lingo), what state should be recorded, what class name should be sent,
+etc. Second, the receiving program gets to specify how an incoming object
+that claims to be an instance of some class should be treated: whether it
+should be accepted at all, if so what class should be used to create the new
+object, and how the received state should be used to populate that
+object.</p>
+
+<p>A word about notation: in Perspective Broker parlance, <q>to jelly</q> is
+used to describe the act of turning an object into an s-expression
+representation (serialization, or at least most of it). Therefore the
+reverse process, which takes an s-expression and turns it into a real python
+object, is described with the verb <q>to unjelly</q>. </p>
+
+<h4>Jellying Instances</h4>
+
+<p>Serializing instances is fairly straightforward. Classes which inherit
+from <code>Jellyable</code> provide a <code>jellyFor</code> method, which
+acts like <code>__getstate__</code> in that it should return a serializable
+representation of the object (usually a dictionary). Other classes are
+checked with a <code>SecurityOptions</code> instance, to verify that they
+are safe to be sent over the wire, then serialized by using their
+<code>__getstate__</code> method (or their <code>__dict__</code> if no such
+method exists). User-level classes always inherit from one of the PB
+<q>flavors</q> like <code>pb.Copyable</code> (all of which inherit from
+<code>Jellyable</code>) and use <code>jellyFor</code>; the
+<code>__getstate__</code> option is only for internal use.</p>
+
+<!-- should we mention persistentStore here? Nothing uses it, so no. Besides
+it was already hinted at in 'tracking shared references' above. -->
+
+<h4>Secure Unjellying</h4>
+
+<p>Unjellying (for instances) is triggered by the receipt of an s-expression
+with the <q>instance</q> tag. The s-expression has two elements: the name of
+the class, and an object (probably a dictionary) which holds the instance's
+state. At that point in time, the receiving program does not know what class
+should be used: it is certainly <em>not</em> safe to simply do an
+<code>import</code> of the classname requested by the sender. That
+effectively allows a remote entity to run arbitrary code on your system.
+</p>
+
+<p>There are two techniques used to control how instances are unjellied. The
+first is a <code>SecurityOptions</code> instance which gets to decide
+whether the incoming object should accepted or not. It is said to
+<q>taste</q> the incoming type before really trying to unserialize it. The
+default taster accepts all basic types but no classes or instances.</p>
+
+<p>If the taster decides that the type is acceptable, Jelly then turns to
+the <code>unjellyableRegistry</code> to determine exactly <em>how</em> to
+deserialize the state. This is a table that maps received class names names
+to unserialization routines or classes.</p>
+
+<p>The receiving program must register the classes it is willing to accept.
+Any attempts to send instances of unregistered classes to the program will
+be rejected, and an InsecureJelly exception will be sent back to the sender.
+If objects should be represented by the same class in both the sender and
+receiver, and if the class is defined by code which is imported into both
+programs (an assumption that results in many security problems when it is
+violated), then the shared module can simply claim responsibility as the
+classes are defined:</p>
+
+<pre class="python">
+class Foo(pb.RemoteCopy):
+ def __init__(self):
+ # note: __init__ will *not* be called when creating RemoteCopy objects
+ pass
+ def __getstate__(self):
+ return foo
+ def __setstate__(self, state):
+ self.stuff = state.stuff
+setUnjellyableForClass(Foo, Foo)
+</pre>
+
+<p>In this example, the first argument to
+<code>setUnjellyableForClass</code> is used to get the fully-qualified class
+name, while the second defines which class will be used for unjellying.
+<code>setUnjellyableForClass</code> has two functions: it informs the
+<q>taster</q> that instances of the given class are safe to receive, and it
+registers the local class that should be used for unjellying.</p>
+
+
+<h3>Broker</h3>
+
+<p>The <code>Broker</code> class manages the actual connection to a remote
+system. <code>Broker</code> is a <q>Protocol</q> (in Twisted terminology),
+and there is an instance for each socket over which PB is being spoken.
+Proxy objects like <code>pb.RemoteReference</code>, which are associated
+with another object on the other end of the wire, all know which Broker they
+must use to get to their remote counterpart. <code>pb.Broker</code> objects
+implement distributed reference counts, manage per-connection object IDs,
+and provide notification when references are lost (due to lost connections,
+either from network problems or program termination).</p>
+
+<h4>PB over Jelly</h4>
+
+<p>Perspective Broker is implemented by sending Jellied commands over the
+connection. These commands are always lists, and the first element of the
+list is always a command name. The commands are turned into
+<code>proto_</code>-prefixed method names and executed in the Broker object.
+There are currently 9 such commands. Two (<code>proto_version</code> and
+<code>proto_didNotUnderstand</code>) are used for connection negotiation.
+<code>proto_message</code> is used to implement remote method calls, and is
+answered by either <code>proto_answer</code> or
+<code>proto_error</code>.</p>
+
+<p><code>proto_cachemessage</code> is used by Observers (see <a
+href="#pb.Copyable">pb.Copyable</a>) to notify their
+<code>RemoteCache</code> about state updates, and behaves like
+<code>proto_message</code>. <a href="#pb.Cacheable">pb.Cacheable</a> also
+uses <code>proto_decache</code> and <code>proto_uncache</code> to manage
+reference counts of cached objects.</p>
+
+<p>Finally, <code>proto_decref</code> is used to manage reference counts on
+<code>RemoteReference</code> objects. It is sent when the
+<code>RemoteReference</code> goes away, so that the holder of the original
+<code>Referenceable</code> can free that object.</p>
+
+<h4>Per-Connection ID Numbers</h4>
+
+<p>Each time a <code>Referenceable</code> is sent across the wire, its
+<code>jellyFor</code> method obtains a new unique <q>local ID</q> (luid) for
+it, which is a simple integer that refers to the original object. The
+Broker's <code>.localObjects{}</code> and <code>.luids{}</code> tables
+maintain the <q>luid</q>-to-object mapping. Only this ID number is sent to
+the remote system. On the other end, the object is unjellied into a
+<code>RemoteReference</code> object which remembers its Broker and the luid
+it refers to on the other end of the wire. Whenever
+<code>callRemote()</code> is used, it tells the Broker to send a message to
+the other end, including the luid value. Back in the original process, the
+luid is looked up in the table, turned into an object, and the named method
+is invoked.</p>
+
+<p>A similar system is used with Cacheables: the first time one is sent, an
+ID number is allocated and recorded in the
+<code>.remotelyCachedObjects{}</code> table. The object's state (as returned
+by <code>getStateToCacheAndObserveFor()</code>) and this ID number are sent
+to the far end. That side uses <code>.cachedLocallyAs()</code> to find the
+local <code>CachedCopy</code> object, and tracks it in the Broker's
+<code>.locallyCachedObjects{}</code> table. (Note that to route state
+updates to the right place, the Broker on the <code>CachedCopy</code> side
+needs to know where it is. The same is not true of
+<code>RemoteReference</code>s: nothing is ever sent <em>to</em> a
+<code>RemoteReference</code>, so its Broker doesn't need to keep track of
+it).</p>
+
+<p>Each remote method call gets a new <code>requestID</code> number. This
+number is used to link the request with the response. All pending requests
+are stored in the Broker's <code>.waitingForAnswers{}</code> table until
+they are completed by the receipt of a <code>proto_answer</code> or
+<code>proto_error</code> message.</p>
+
+<p>The Broker also provides hooks to be run when the connection is lost.
+Holders of a <code>RemoteReference</code> can register a callback with
+<code>.notifyOnDisconnect()</code> to be run when the process which holds
+the original object goes away. Trying to invoke a remote method on a
+disconnected broker results in an immediate <code>DeadReferenceError</code>
+exception.</p>
+
+<h4>Reference Counting</h4>
+
+<p>The Broker on the <code>Referenceable</code> end of the connection needs
+to implement distributed reference counting. The fact that a remote end
+holds a <code>RemoteReference</code> should prevent the
+<code>Referenceable</code> from being freed. To accomplish this, The
+<code>.localObjects{}</code> table actually points at a wrapper object
+called <code>pb.Local</code>. This object holds a reference count in it that
+is incremented by one for each <code>RemoteReference</code> that points to
+the wrapped object. Each time a Broker serializes a
+<code>Referenceable</code>, that count goes up. Each time the distant
+<code>RemoteReference</code> goes away, the remote Broker sends a
+<code>proto_decref</code> message to the local Broker, and the count goes
+down. When the count hits zero, the <code>Local</code> is deleted, allowing
+the original <code>Referenceable</code> object to be released.</p>
+
+
+<h2>Security</h2>
+
+<p>Insecurity in network applications comes from many places. Most can be
+summarized as trusting the remote end to behave in a certain way.
+Applications or protocols that do not have a way to verify their assumptions
+may act unpredictably when the other end misbehaves; this may result in a
+crash or a remote compromise. One fundamental assumption that most RPC
+libraries make when unserializing data is that the same library is being
+used at the other end of the wire to generate that data. Developers put so
+much time into making their RPC libraries work <strong>at all</strong> that
+they usually assume their own code is the only thing that could possibly
+provide the input. A safer design is to assume that the input will almost
+always be corrupt, and to make sure that the program survives anyway.</p>
+
+<h3>Controlled Object serialization</h3>
+
+<p>Security is a primary design goal of PB. The receiver gets final say as
+to what they will and will not accept. The lowest-level serialization
+protocol (<q>Banana</q>) is simple enough to validate by inspection, and
+there are size limits imposed on the actual data received to prevent
+excessive memory consumption. Jelly is willing to accept basic data types
+(numbers, strings, lists and dictionaries of basic types) without question,
+as there is no dangerous code triggered by their creation, but Class
+instances are rigidly controlled. Only subclasses of the basic PB flavors
+(<code>pb.Copyable</code>, etc) can be passed over the wire, and these all
+provide the developer with ways to control what state is sent and accepted.
+Objects can keep private data on one end of the connection by simply not
+including it in the copied state.</p>
+
+<p>Jelly's refusal to serialize objects that haven't been explicitly marked
+as copyable helps stop accidental security leaks. Seeing the
+<code>pb.Copyable</code> tag in the class definition is a flag to the
+developer that they need to be aware of what parts of the class will be
+available to a remote system and which parts are private. Classes without
+those tags are not an issue: the mere act of <em>trying</em> to export them
+will cause an exception. If Jelly tried to copy arbitrary classes, the
+security audit would have to look into <em>every</em> class in the
+system.</p>
+
+<h3>Controlled Object Unserialization</h3>
+
+<p>On the receiving side, the fact that Unjellying insists upon a
+user-registered class for each potential incoming instance reduces the risk
+that arbitrary code will be executed on behalf of remote clients. Only the
+classes that are added to the <code>unjellyableRegistry</code> need to be
+examined. Half of the security issues in RPC systems will boil down to the
+fact that these potential unserializing classes will have their
+<code>setCopyableState</code> methods called with a potentially hostile
+<code>state</code> argument. (the other half are that <code>remote_</code>
+methods can be called with arbitrary arguments, including instances that
+have been sent to that client at some point since the current connection was
+established). If the system is prepared to handle that, it should be in good
+shape security-wise.</p>
+
+<p>RPC systems which allow remote clients to create arbitrary objects in the
+local namespace are liable to be abused. Code gets run when objects are
+created, and generally the more interesting and useful the object, the more
+powerful the code that gets run during its creation. Such systems also have
+more assumptions that must be validated: code that expects to be given an
+object of class <code>A</code> so it can call <code>A.foo</code> could be
+given an object of class <code>B</code> instead, for which the
+<code>foo</code> method might do something drastically different. Validating
+the object is of the required type is much easier when the number of
+potential types is smaller.</p>
+
+<h3>Controlled Method Invocation</h3>
+
+<p>Objects which allow remote method invocation do not provide remote access
+to their attributes (<code>pb.Referenceable</code> and
+<code>pb.Copyable</code> are mutually exclusive). Remote users can only
+invoke a well-defined and clearly-marked subset of their methods: those with
+names that start with <code>remote_</code> (or other specific prefixes
+depending upon the variant of <code>Referenceable</code> in use). This
+insures that they can have local methods which cannot be invoked remotely.
+Complete object transparency would make this very difficult: the
+<q>translucent</q> reference scheme allows objects some measure of privacy
+which can be used to implement a security model. The
+<q><code>remote_</code></q> prefix makes all remotely-invokable methods easy
+to locate, improving the focus of a security audit.</p>
+
+<h3>Restricted Object Access</h3>
+
+<p>Objects sent by reference are indexed by a per-connection ID number,
+which is the only way for the remote end to refer back to that same object.
+This list means that the remote end can not touch objects that were not
+explicitly given to them, nor can they send back references to objects
+outside that list. This protects the program's memory space against the
+remote end: they cannot find other local objects to play with.</p>
+
+<p>This philosophy of using simple, easy to validate identifiers (integers
+in the case of PB) that are scoped to a well-defined trust boundary (in this
+case the Broker and the one remote system it is connected to) leads to
+better security. Imagine a C system which sent pointers to the remote end
+and hoped it would receive back valid ones, and the kind of damage a
+malicious client could do. PB's <code>.localObjects{}</code> table insures
+that any given client can only refer to things that were given to them. It
+isn't even a question of validating the identifier they send: if it isn't a
+value of the <code>.localObjects{}</code> dictionary, they have no physical
+way to get at it. The worst they can do with a corrupt ObjectID is to cause
+a <code>KeyError</code> when it is not found, which will be trapped and
+reported back.</p>
+
+<h3>Size Limits</h3>
+
+<p>Banana limits string objects to 640k (because, as the source says, 640k
+is all you'll ever need). There is a helper class called
+<code>pb.util.StringPager</code> that uses a producer/consumer interface to
+break up the string into separate pages and send them one piece at a time.
+This also serves to reduce memory consumption: rather than serializing the
+entire string and holding it in RAM while waiting for the transmit buffers
+to drain, the pages are only serialized as there is space for them.</p>
+
+
+<h2>Future Directions</h2>
+
+<p>PB can currently be carried over TCP and SSL connections, and through
+UNIX-domain sockets. It is being extended to run over UDP datagrams and a
+work-in-progress reliable datagram protocol called <q>airhook</q>. (clearly
+this requires changes to the authorization sequence, as it must all be done
+in a single packet: it might require some kind of public-key signature).</p>
+
+<p>At present, two functions are used to obtain the initial reference to a
+remote object: <code>pb.getObjectAt</code> and <code>pb.connect</code>. They
+take a variety of parameters to indicate where the remote process is
+listening, what kind of username/password should be used, and which exact
+object should be retrieved. This will be simplified into a <q>PB URL</q>
+syntax, making it possible to identify a remote object with a descriptive
+URL instead of a list of parameters.</p>
+
+<p>Another research direction is to implement <q>typed arguments</q>: a way
+to annotate the method signature to indicate that certain arguments may only
+be instances of a certain class. Reminiscent of the E language, this would
+help remote methods improve their security, as the common code could take
+care of class verification.</p>
+
+<p>Twisted provides a <q>componentization</q> mechanism to allow
+functionality to be split among multiple classes. A class can declare that
+all methods in a given list (the <q>interface</q>) are actually implemented
+by a companion class. Perspective Broker will be cleaned up to use this
+mechanism, making it easier to swap out parts of the protocol with different
+implementations.</p>
+
+<p>Finally, a comprehensive security audit and some performance improvements
+to the Jelly design are also in the works.</p>
+
+<!-- $Id: pb.html,v 1.1 2003/03/31 05:21:40 glyph Exp $ -->
+
+</body> </html>
diff --git a/doc/historic/2003/pycon/releasing/releasing-twisted b/doc/historic/2003/pycon/releasing/releasing-twisted
new file mode 100755
index 0000000..1b20155
--- /dev/null
+++ b/doc/historic/2003/pycon/releasing/releasing-twisted
@@ -0,0 +1,151 @@
+#!/usr/bin/python2.2
+# Moshe -- This seems like 30+ minutes to me!
+from slides import NumSlide, Slide, Bullet, SubBullet, PRE, URL
+from twslides import Lecture
+
+
+lecture = Lecture(
+ "Managing the Release of a Large Python Project",
+ Slide("About Twisted",
+ Bullet("Networking framework"),
+ Bullet("Other goodies"),
+ Bullet("60,000 lines of code"),
+ Bullet("Things can (and do) go wrong"),
+ ),
+ Slide("Python",
+ Bullet("Recap"),
+ Bullet("No compilation (except for native modules)"),
+ Bullet("Simple file-based modules (no registration)"),
+ Bullet("Distutils -- Does the common things"),
+ ),
+ Slide("Release Procedure -- Steps",
+ Bullet("Increment version in copyright file, README"),
+ Bullet("Tag release"),
+ Bullet("Export from CVS"),
+ Bullet("Rename toplevel directory"),
+ Bullet("Generate API and HOWTO documentation"),
+ Bullet("Create tarballs"),
+ Bullet("Move tarballs to target area"),
+ Bullet("Create Debian packages"),
+ Bullet("Put Debian packages in final place"),
+ Bullet("Upgrade production machine"),
+ ),
+ Slide("Release Procedure Overview - Documentation",
+ Bullet("Man pages -> Lore"),
+ Bullet("Lore documents -> HTML"),
+ Bullet("Lore documents -> PS/PDF"),
+ Bullet("API documentation -> HTML"),
+ ),
+ Slide("Release Procedure Overview - Testing",
+ Bullet("Run of the mill unit tests"),
+ Bullet("Acceptance tests of less portable things"),
+ Bullet("Prerelease tests for twistedmatrix.com-specific test"),
+ Bullet("twistedmatrix.com uses latest version -- always!"),
+ ),
+ Slide("Release Procedure Overview - Debian",
+ Bullet("The Twisted machines use Debian packages"),
+ Bullet("The Twisted machines run latest version"),
+ Bullet("Debian packages are built as part of the release procedure"),
+ ),
+ Slide("Overview Summary",
+ Bullet("Many steps"),
+ Bullet("Each can fail", SubBullet(
+ Bullet("Documentation can fail to build"),
+ Bullet("Tests can fail"),
+ Bullet("Debian packages can fail to build")),
+ ),
+ Bullet("Need robust automated setup"),
+ ),
+ Slide("Enter Release-Twisted",
+ Bullet("Python program to release Twisted"),
+ Bullet("Key word -- Robust"),
+ Bullet("Based on actions which can undo"),
+ Bullet("Flexible - able to recover a botched build from the middle"),
+ Bullet("Easy - has good defaults"),
+ ),
+ Slide("Testing - Recap",
+ Bullet("Testing is special - no effect"),
+ Bullet("The more, the better"),
+ Bullet("Harder to automate - machines can't tell right from wrong",
+ SubBullet(Bullet("Except in Hollywood")),
+ ),
+ ),
+ Slide("Different Kinds of Tests - Unit Tests",
+ Bullet("Completely automated"),
+ Bullet("Completely machine-verifiable"),
+ Bullet("Portable"),
+ Bullet("Must always pass"),
+ ),
+ Slide("Different Kinds of Tests - Acceptance Tests",
+ Bullet("Interacts with user"),
+ Bullet("Probably works only on Linux"),
+ Bullet("Assumes many client side tools"),
+ Bullet("Exercises many parts of Twisted which are hard in unit tests"),
+ ),
+ Slide("Acceptance Tests Examples",
+ Bullet("Run Twisted web server, run user-defined web browser"),
+ Bullet("Run mail server, send mail and try to download with pop3"),
+ Bullet("Run IRC server, run user-defined IRC client"),
+ ),
+ Slide("Different Kinds of Tests - Prerelease Tests",
+ Bullet("TwistedMatrix.com dogfoods"),
+ Bullet("We want to test the dog food"),
+ Bullet("prerelease tests convince us that this version doesn't break "
+ "completely"),
+ Bullet("Among other things, tests that distributed web works"),
+ ),
+ Slide("Epydoc",
+ ),
+ Slide("Epyrun",
+ ),
+ Slide("Distutils -- Datafiles",
+ ),
+ Slide("Distutils -- Conditional compilation",
+ ),
+ Slide("Distutils -- Conditional compilation woes",
+ ),
+ Slide("Distutils -- Other woes",
+ Bullet("Versions -- keywords were added later"),
+ Bullet("Icky to do platform dependent stuff"),
+ ),
+ Slide("release-twistd -- master script",
+ ),
+ Slide("Commit/rollback",
+ ),
+ Slide("CVS and tagging",
+ ),
+ Slide("Debian Packages -- Challenges",
+ Bullet("Versioning: We want 1.0.2alpha4 to precede 1.0.2"),
+ Bullet("Dependencies: Which versions of Python? 2.1? 2.2? 2.3?"),
+ Bullet("Dependencies: Which libc version?"),
+ ),
+ Slide("Debian Packages -- Solutions",
+ Bullet("Build two sets -- for Debian stable and for Debian unstable"),
+ Bullet("When building on stable, remove python2.3-dev from build"
+ " dependencies", SubBullet(
+ Bullet("This stops the Python 2.3 version from being built")),
+ ),
+ Bullet("If building a non-final version, name it 1.0.1+1.0.2alpha4"),
+ Bullet("Unstable build is done by sshing into an unstable chroot"),
+ ),
+ Slide("Windows Releases -- Challenges",
+ ),
+ Slide("Windows Releases -- Solutions",
+ ),
+ Slide("Why Not Dependency Management?",
+ ),
+ Slide("Conclusions",
+ Bullet("Distutils does not do enough"),
+ Bullet("Cross compiling is hard"),
+ Bullet("It would be nice if Python had integrated docstring tools"),
+ Bullet("Wheel reinvention is useful"),
+ ),
+ Slide("Future Directions",
+ Bullet("RPMs for Various Distributions"),
+ Bullet("More automation"),
+ ),
+ Slide("Questions?",
+ ),
+)
+
+lecture.renderHTML(".", "releasing-%d.html", css="main.css")
diff --git a/doc/historic/2003/pycon/releasing/releasing.html b/doc/historic/2003/pycon/releasing/releasing.html
new file mode 100644
index 0000000..9e7bb61
--- /dev/null
+++ b/doc/historic/2003/pycon/releasing/releasing.html
@@ -0,0 +1,491 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>Managing the Release of a Large Python Project</title>
+</head>
+
+<body>
+<h1>Managing the Release of a Large Python Project</h1>
+
+<ul>
+<li>Christopher Armstrong <a href="mailto:radix@twistedmatrix.com">radix@twistedmatrix.com</a></li>
+<li>Moshe Zadka <a href="mailto:moshez@twistedmatrix.com">moshez@twistedmatrix.com</a></li>
+</ul>
+
+<h2>Abstract</h2>
+<p>
+
+Twisted is a Python networking framework. At last count, the project
+contains nearly 60,000 lines of effective code (not comments or blank
+lines). When preparing a release, many details must be checked, and
+many steps must be followed. We describe here the technologies and
+tools we use, and explain how we built tools on top of them which help
+us make releasing as painless as possible.
+
+</p>
+
+<h2>Introduction</h2>
+<p>
+
+One of the virtues of Python is the ease of distributing code. Its
+module system and the lack of necessity of compilation are what make
+this possible. This means that for simple Python projects, nothing
+more complicated then tar is needed to prepare a distribution of a
+library. However, Twisted has auto-generated documentation in several
+formats, including docstring generated documentation, HOWTOs written
+in HTML, and manpages written in nroff. As Twisted grew more complex
+and popular, a detailed procedure for putting out a release was made
+necessary. However, human fallibility being what it is, it was decided
+that most of these steps should be automated.
+
+</p>
+
+<h2>Overview of Steps</h2>
+<p>
+
+Despite heavy automation, there are still a number of manual steps
+involved in the release process. We've reduced the amount of manual
+steps quite a bit, and most of what's left is not fully automatable,
+although the process could be made easier (see <q>Future
+Directions</q> below).
+
+</p>
+
+<ul>
+ <li>Test
+ <ul>
+ <li>Unit tests</li>
+ <li>Acceptance tests</li>
+ <li>Pre-release tests</li>
+ </ul>
+ </li>
+ <li>Update the Changelog and README files</li>
+ <li>Run the release script
+ <ul>
+ <li>unix runs admin/release-twisted</li>
+ <li>Win32 runs win32/bdist_wininst.bat</li>
+ </ul>
+ </li>
+ <li>Deploy: update twisted deployment on twistedmatrix.com</li>
+ <li>Upload to SourceForge mirror</li>
+ <li>Update Website</li>
+</ul>
+
+
+
+<h2>Testing</h2>
+
+<p>
+
+Twisted has three categories of tests: unit, acceptance, and
+pre-release. Testing is an important part of releasing quality
+software, of course, so these will be explained.
+
+</p>
+
+
+<p>
+
+Unit tests are run as often as possible by each of the developers as
+they write code, and must pass before they commit any changes to
+CVS. While the Twisted team tries to follow the XP practice of
+ensuring all code is releasable, this isn't always true. Thus, running
+the unit tests on several platforms before releasing is necessary.
+Our BuildBot runs the unit tests constantly on several hosts and
+multiple platforms, so the <a
+href="http://twistedmatrix.com/users/warner.twistd/">status page</a>
+is simply checked for green lights before a release.
+
+</p>
+
+<p>
+
+Acceptance tests (which, unfortunately, are not quite the same as <a
+href="http://xprogramming.org/">Extreme Programming's</a> Acceptance
+Tests) are simply interactive tests of various Twisted services. There
+is a script that executes several system commands that use the Twisted
+end-user executables and start several clients (web browsers, IRC
+clients, etc) to allow the user to interactively test the different
+services that Twisted offers. These are only routinely run before a
+release, but we also encourage developers to run these before they
+make major changes.
+
+</p>
+
+<p>
+
+The pre-release tests are for ensuring the web server (One of the most
+popular parts of Twisted, and which the twistedmatrix.com web site
+uses) runs correctly in a semi-production environment. The script
+starts up a web server on twistedmatrix.com, similar to the one on
+port 80, but on an out-of-the-way port. <q>lynx</q> is then run
+several times, with URLs strategically chosen to test different
+features of the web server. Afterwards, the log of the web server is
+displayed and the user is to check for any errors.
+
+</p>
+
+
+<h2>The release-twisted Script</h2>
+
+<p>
+
+Like many other build/release systems, the automated parts of our
+release system started out as a number of small shell
+scripts. Eventually these became a single Python script which was a
+large improvement, but still had many problems, especially since our
+release process became more complex (documentation generation,
+different types of archive formats, etc). This led to problems with
+steps in the middle of the process breaking; the release manager would
+need to restart the entire thing, or enter the remaining commands
+manually.
+
+</p>
+
+<p>
+
+The solution that we came up with was a simple framework for
+pseudo-transactions; Every step of the process is implemented with a
+class that has <code class="python">doIt</code> and <code
+class="python">undoIt</code> methods. Each step also has a
+command-line argument associated with it, so a typical run of the
+script looks something like this:
+
+<pre class="shell">
+$SOMEWHERE/admin/release-twisted -V $VERSION -o $LASTVERSION --checkout \
+--release=/twisted/Releases --upver --tag --exp --dist --docs --balls \
+--rel --deb --debi
+</pre>
+
+</p>
+
+<h3>Transactions</h3>
+
+<p>
+
+As stated above, our transaction system is very simple. One of our
+rather simple transaction classes is <code
+class="python">Export</code>.
+
+</p>
+
+
+<pre class="python">
+class Export(Transaction):
+ def doIt(self, opts):
+ print "Export"
+ root = opts['cvsroot']
+ ver = opts['release-version']
+ sh('cvs -d%s export -r release-%s Twisted' % (root, ver.replace('.', '_')))
+
+ def undoIt(self, opts, fail):
+ sh('rm -rf Twisted')
+</pre>
+
+
+<p>
+
+One useful feature to note is the <code
+class="python">sensitiveUndo</code> attribute on Transaction
+classes. If a transaction has this set, the user will be prompted
+before running the <code class="python">undoIt</code> method. This is
+useful for very long-running processes, like documentation generation,
+debian package building, and uploading to sourceforge. If something
+goes wrong in the middle of one of these processes, we want to give
+the user a chance to manually fix the problem rather than redoing the
+entire transaction. They can then continue from the next command by
+omitting the commands that have already been accomplished from the
+<code class="shell">release-twisted</code> arguments.
+
+</p>
+
+<p>
+
+A list of all of the transactions defined in release-twisted follows.
+
+</p>
+
+<dl>
+<dt>CheckOut</dt>
+<dd>
+
+ checks out the latest revision of Twisted from CVS and puts it in
+ the <q>Twisted CVS</q> directory.
+
+</dd>
+
+<dt>UpdateVersion</dt>
+<dd>
+
+ changes the version number of the current release -- updating
+ twisted/copyright.py (the canonical location for the current
+ version) and a few other text files where the current version is
+ mentioned.
+
+</dd>
+
+
+<dt>Tag</dt>
+<dd>
+
+ tags the revisions in the current source tree with the version
+ passed in on the command line.
+
+</dd>
+
+
+<dt>Export</dt>
+
+<dd>
+
+ runs the cvs <q>export</q> command, which is similar to
+ <q>checkout</q>, but leaves out CVS support directories; this is
+ what we package up in the archives.
+
+</dd>
+
+
+<dt>PrepareDist</dt>
+<dd>
+
+ simply copies the directory containing the version of Twisted to be
+ released to a new directory specifically for the release
+ process. The reason that we have this extra copy is that sometimes
+ one will want to create a release from a directory that wasn't
+ created from the <q>Export</q> command; having the release script
+ munge that directory in-place would be impolite.
+
+</dd>
+
+
+<dt>GenerateDocs</dt>
+
+<dd>
+
+ generates the various documentation: HTML API documentation (via
+ Epydoc), HTML, PostScript, and PDF howto documentation (via
+ twisted.lore), and HTML man-pages (via lore, converted from the
+ nroff source).
+
+</dd>
+
+<dt>CreateTarballs</dt>
+<dd>
+
+ creates the various archives that each Twisted release involves:
+ tarred and gzipped or bzip2ed versions of archives with code plus
+ documentation, code without documentation, and only documentation.
+
+</dd>
+
+
+<dt>Release</dt>
+
+<dd>
+
+ copies all of the archives to a directory specified by the --release
+ parameter. This is meant to be a publically accessible directory,
+ thus the name <q>Release</q>.
+
+</dd>
+
+<dt>MakeDebs</dt>
+
+<dd>
+
+ creates the .deb packages and support files for the Twisted Debian
+ packages.
+
+</dd>
+
+<dt>InstallDebs</dt>
+
+<dd>
+
+ Creates an apt-gettable Debian package repository in the
+ (unfortunately hard-coded) <q>/twisted/Debian</q> directory.
+
+</dd>
+
+<dt>Sourceforge</dt>
+
+<dd>
+
+ uploads the archives and debian packages to Twisted's sourceforge
+ mirror at <a
+ href="http://twisted.sourceforge.net">http://twisted.sourceforge.net/</a>.
+
+</dd>
+
+
+<dt>UpgradeDebian</dt>
+
+<dd>
+
+ Installs the recently-generated Debian packages via <q>dpkg</q> on
+ the local machine.
+
+</dd>
+
+</dl>
+
+
+<h2>setup.py</h2>
+
+<p>
+
+Twisted has an extensive and very customized setup.py script. We have
+a number of C extension modules and try to ensure that they all build,
+or at least fail gracefully, on win32, Mac OSX, Linux and other
+popular unix-style OSes.
+
+</p>
+
+<p>
+
+We have overridden three of the distutils <q>command classes</q>:
+<code class="python">build_ext</code>, <code
+class="python">install_scripts</code>, and <code
+class="python">install_data</code>.
+
+</p>
+
+
+<h3>Building C extensions</h3>
+
+<p>
+
+<code class="python">build_ext_twisted</code> detects, based on
+various features of the platform, which C extensions to build. It
+overrides the <code class="python">build_extensions</code> method to
+first check which C extensions are appropriate to build for the
+current platform before proceeding as normal (by calling the
+superclass's <code class="python">build_extensions</code>). The
+module-detection consists of several simple tests for platform
+features and conditional additions to the `extensions' attribute. One
+especially useful feature is the <code
+class="python">_check_header</code> method, which takes the name of an
+arbitrary head file and tries to compile (via the distutil's C
+compiler interafce) a simple C file that only #includes it.
+
+</p>
+
+
+<h3>Installing scripts</h3>
+
+
+<p>
+
+<code class="python">install_data_twisted</code> ensures that the data
+files are installed along-side the python modules in the twisted
+package. This is accomplished with the incantation:
+
+</p>
+
+<pre class="python">
+class install_data_twisted(install_data):
+ def finalize_options (self):
+ self.set_undefined_options('install',
+ ('install_lib', 'install_dir')
+ )
+ install_data.finalize_options(self)
+</pre>
+
+
+
+<h3>Windows Releases</h3>
+
+<!--
+<p>
+This section will cover the problems with packaging Python projects
+for windows, especially ones which contain scripts. The problem of
+clickability is especially acute, as windows determines types by
+extensions and not by #! lines.
+</p>
+-->
+
+<p>
+
+Packaging software for windows involves a unique set of problems. The
+problem of clickability is especially acute; Several customizations to
+the distutils setup had to be made.
+
+</p>
+
+<p>
+
+The first customization was to make the <q>scripts</q> end with a
+<q>.py</q> extension, since Windows relies on extension rather than a
+she-bang line to specify what interpreter should execute a file. This
+was accomplished by overriding the <code
+class="python">install_scripts</code> command, like so:
+
+</p>
+
+<pre class="python">
+class install_scripts_twisted(install_scripts):
+ """Renames scripts so they end with '.py' on Windows."""
+
+ def run(self):
+ install_scripts.run(self)
+ if os.name == "nt":
+ for file in self.get_outputs():
+ if not file.endswith(".py"):
+ os.rename(file, file + ".py")
+</pre>
+
+
+<p>
+
+We also wanted to have a Start-menu group with a number of icons for
+running different Twisted programs. This was accomplished with a
+post-install script specified with the command-line parameter
+<code class="shell">--install-script=twisted_postinstall.py</code>.
+
+</p>
+
+
+
+<h2>Future Directions</h2>
+
+<p>
+
+The theme is, of course, automation, and there are still many manual
+steps involved in a Twisted release. The currently most annoying step
+is updating the documentation and downloads section of the
+twistedmatrix.com website. Automating this would be a major
+improvement to the time it takes from the running of the release
+script to a fully completed release.
+
+</p>
+
+<p>
+
+Another major improvement will involve further integration with
+BuildBot. Currently we have BuildBot running unit tests, building C
+extensions, and generating documentation on several hosts. Eventually
+we would like to have it constantly generating full release archives,
+and have an additional web form for <q>finalizing</q> any particular
+build that we deem releasable. The result would be uploading the
+release to the mirrors and updating the website.
+
+</p>
+
+<p>
+
+The tagging scheme used by the release-twisted scripts can sometimes
+be problematic. If we find serious problems in the code-base after the
+Tag command is executed (which is fairly early in the process), we are
+forced to fix the bug and increase the version number. This can be
+prevented by, instead of making the official tag, using the unofficial
+tag <q>releasing-$version</q> (as opposed to <q>release-$version</q>)
+at that early stage. Once most of the steps are complete, the official
+tag will be made. If something in between goes wrong, we can just
+re-use the unofficial <q>releasing-$version</q> tag and not worry
+about users trying to use that tag.
+
+</p>
+
+
+</body>
+</html>
diff --git a/doc/historic/2003/pycon/tw-deploy/tw-deploy b/doc/historic/2003/pycon/tw-deploy/tw-deploy
new file mode 100755
index 0000000..294bd73
--- /dev/null
+++ b/doc/historic/2003/pycon/tw-deploy/tw-deploy
@@ -0,0 +1,184 @@
+#!/usr/bin/python
+# Requires CVS Slides
+
+from slides import Lecture, Slide, TitleSlide, Image, Bullet, PRE, URL, SubBullet, NumSlide, toHTML
+
+PERL_PROCESSOR = """\
+from twisted.web import static, twcgi
+
+class PerlScript(twcgi.FilteredScript):
+ filter = '/usr/bin/perl' # Points to the perl parser
+"""
+
+RPY_EXAMPLE = """\
+from twisted.web import resource
+
+class MyGreatResource(resource.Resource):
+ def render(self, request):
+ return "<html>foo</html>"
+
+resource = MyGreatResource()
+"""
+
+lecture = Lecture(
+ "A Twisted Web Tutorial",
+
+ TitleSlide("Twisted Web -- A tutorial",
+ Image("twistedlogo.png"),
+ ),
+
+ Slide("Twisted Web -- Where does it fit?",
+ Image("twisted-overview.png"),
+ ),
+
+ Slide("Setup and Configuration Utilities",
+ Bullet("mktap"),
+ Bullet("twistd"),
+ Bullet("websetroot"),
+ ),
+
+ Slide("mktap",
+ Bullet("TAP Model"),
+ Bullet("General usage"),
+ Bullet("Flexibility and Power"),
+ ),
+
+ Slide("mktap web : Common Useful Options",
+ Bullet("--path"),
+ Bullet("--port"),
+ Bullet("--user"),
+ Bullet("--logfile"),
+ Bullet("--processor"),
+ ),
+
+ Slide("Sample mktap command lines",
+ Bullet(PRE("mktap web")),
+ Bullet(PRE("mktap web --path=/var/www --logfile=/var/log/twistedweb.log")),
+ Bullet(PRE("mktap web --port=80 --path=/var/www --user --mime-type=text/plain")),
+ Bullet(PRE("mktap web --path=/home/nafai/public_html --processor=.pl=PerlProcessor.PerlProcessor --index=index.pl")),
+ ),
+
+ Slide("twistd : An overview",
+ Bullet("Start a Twisted Application"),
+ Bullet("Loads and instance of twisted.internet.app.Application from a file"),
+ Bullet("Daemonizes, binds to appropriate ports, and starts the Twisted mainloop"),
+ ),
+
+ Slide("Sample twistd command lines",
+ Bullet(PRE("twistd -f web.tap -l /var/log/twisted.log")),
+ Bullet(PRE("twistd -f web.tap --pidfile /var/run/web.pid")),
+ ),
+
+ Slide("Shutting down twistd",
+ Bullet("On Unix (in general): "),
+ SubBullet(
+ Bullet(PRE("kill -9 `cat twistd.pid`"))),
+ Bullet("On Windows: "),
+ SubBullet(
+ Bullet("Cannot daemonize on Windows, so just run twistd in a command prompt"),
+ Bullet("Switch to the command prompt, and press Control-C"),
+ ),
+ ),
+
+ Slide("Shutdown TAPs",
+ Bullet("Since TAPs store persistent data for an application,\
+ a 'shutdown' TAP is created on twistd shutdown"),
+ Bullet("You'll often want to start your Twisted application\
+ on subsequent runs with the shutdown TAP"),
+ ),
+
+ Slide("twistd and security",
+ Bullet("When twistd is run as root, it will shed root privileges\
+ for the uid and gid of either the user that created the TAP or those\
+ specified on the mktap commandline."),
+ ),
+
+ # Try this out!
+ Slide("websetroot",
+ Bullet("Used to change what the root of the server points to"),
+ Bullet("Set it to a Resource contained either in a Python source file or a Pickle file"),
+ ),
+
+ Slide("Sample websetroot command lines",
+ Bullet(PRE("websetroot -p 80 -f web.tap --script rootResource.py")),
+ Bullet(PRE("websetroot -p 8080 -f web.tap --pickle rootPickle")),
+ ),
+
+ # Resource example
+ # Use the perl example from the docs
+ Slide("What's a Resource?",
+ Bullet("Everything in twisted in represented as twisted.web.resource.Resource object"),
+ Bullet("In general, two calls are made on a resource:"),
+ SubBullet(
+ Bullet(PRE("getChild()")),
+ Bullet(PRE("render()")),
+ ),
+ ),
+
+ Slide("Resource call examples:",
+ Bullet("/foo/bar/baz gets converted to:"
+ ),
+ SubBullet(
+ Bullet(PRE("site.getChild('foo', request).getChild('bar', request).getChild('baz', request).render(request)")),
+ ),
+ Bullet("/foo/bar/baz/ gets converted to:"
+ ),
+ SubBullet(
+ Bullet(PRE("site.getChild('foo', request).getChild('bar', request).getChild('baz', request).getChild('', request).render(request)")),
+ ),
+ ),
+
+ Slide("What do Resources handle?",
+ Bullet("Out of the box, Twisted supports files of all types"),
+ Bullet("HTML, text, etc."),
+ Bullet("Default MIME type can be specified"),
+ ),
+
+ Slide("What about web development?",
+ Bullet("Twisted Web has what are called processors, which are instances\
+ of classes inherited from resource.Resource"),
+ Bullet("By default, Twisted supports the following file types:"),
+ SubBullet(
+ Bullet(".php"),
+ Bullet(".php3"),
+ Bullet(".cgi"),
+ Bullet(".epy"),
+ Bullet(".rpy"),
+ Bullet(".trp"),
+ ),
+ Bullet("You can also write your own"),
+ ),
+
+ Slide("Custom Processor: More than One Evil Way to Do It",
+ Bullet("A custom processor to handle Perl CGIs:"),
+ PRE(PERL_PROCESSOR),
+ Bullet("An example of how to use:"),
+ SubBullet(Bullet(PRE("mktap web --path=/home/nafai/public_html --processor=.pl=PerlScript.PerlScript")),
+ ),
+ ),
+
+ Slide("What about making my own resources?",
+ Bullet("Define a class that inherits from resource.Resource"),
+ Bullet("Define the render() method on that class"),
+ Bullet("For long requests, render() can return NOT_DONE_YET"),
+ Bullet("Then Create a .rpy file that sets resource = to an instance of the class"),
+ ),
+
+ Slide(".rpy example",
+ PRE(RPY_EXAMPLE),
+ ),
+
+ Slide("More Stuff",
+ Bullet("In other words, the slides I didn't get to write..."),
+ SubBullet(
+ Bullet("Distributed Servers"),
+ Bullet("Virtual Hosts"),
+ Bullet("Rewrite Rules"),
+ Bullet("Debian configuration"),
+ Bullet("twistedmatrix.com configuration"),
+ ),
+ ),
+)
+
+if __name__ == '__main__':
+ lecture.renderHTML(".", "tw_deploy-%02d.html", css="main.css")
diff --git a/doc/historic/2003/pycon/tw-deploy/twisted-overview.png b/doc/historic/2003/pycon/tw-deploy/twisted-overview.png
new file mode 100644
index 0000000..6746a85
--- /dev/null
+++ b/doc/historic/2003/pycon/tw-deploy/twisted-overview.png
Binary files differ
diff --git a/doc/historic/2003/pycon/tw-deploy/twistedlogo.png b/doc/historic/2003/pycon/tw-deploy/twistedlogo.png
new file mode 100644
index 0000000..6226297
--- /dev/null
+++ b/doc/historic/2003/pycon/tw-deploy/twistedlogo.png
Binary files differ
diff --git a/doc/historic/2003/pycon/twisted-internet/twisted-internet.py b/doc/historic/2003/pycon/twisted-internet/twisted-internet.py
new file mode 100644
index 0000000..1948d3e
--- /dev/null
+++ b/doc/historic/2003/pycon/twisted-internet/twisted-internet.py
@@ -0,0 +1,541 @@
+#!/usr/bin/python
+
+from slides import Lecture, Slide, Image, Bullet, PRE, URL, SubBullet, NumSlide, toHTML
+import os
+
+class Bad:
+ """Marks the text in red."""
+
+ def __init__(self, text):
+ self.text = text
+
+ def toHTML(self):
+ return '<font color="red">%s</font>' % toHTML(self.text)
+
+
+class Lecture(Lecture):
+
+ def getFooter(self):
+ return '<div class="footer"><hr noshade />Presented by <b>ZOTECA&nbsp;</b></div>'
+
+
+EVENT_LOOP_CODE = """\
+# pseudo-code reactor
+class Reactor:
+ def run(self):
+ while 1:
+ e = self.getNextEvent()
+ e.run()
+"""
+
+PROTOCOL_CODE = """\
+from twisted.internet.protocol import Protocol
+
+class Echo(Protocol):
+ def connectionMade(self):
+ print 'connection made with', self.transport.getPeer()
+ def dataReceived(self, data):
+ self.transport.write(data)
+ def connectionLost(self, reason):
+ print 'connection was lost, alas'
+"""
+
+SERVER_CODE = """\
+from twisted.internet.protocol import ServerFactory
+
+class EchoFactory(ServerFactory):
+
+ def buildProtocol(self, addr):
+ p = Echo()
+ p.factory = self
+ return p
+"""
+
+RUNNING_SERVER_CODE = """\
+from twisted.internet import reactor
+
+f = EchoFactory()
+reactor.listenTCP(7771, f)
+reactor.run()
+"""
+
+CLIENT_PROTOCOL_CODE = """\
+from twisted.internet.protocol import Protocol
+
+class MyClientProtocol(Protocol):
+ buffer = ''
+ def connectionMade(self):
+ self.transport.write('hello world')
+ def dataReceived(self, data):
+ self.buffer += data
+ if self.buffer == 'hello world':
+ self.transport.loseConnection()
+"""
+
+CLIENT_FACTORY_CODE = """\
+from twisted.internet.protocol import ClientFactory
+
+class MyFactory(ClientFactory):
+
+ protocol = MyClientProtocol
+
+ def startedConnecting(self, connector):
+ pass # we could connector.stopConnecting()
+ def clientConnectionMade(self, connector):
+ pass # we could connector.stopConnecting()
+ def clientConnectionLost(self, connector, reason):
+ connector.connect() # reconnect
+ def clientConnectionFailed(self, connector, reason):
+ print "connection failed"
+ reactor.stop()
+"""
+
+CLIENT_CONNECT_CODE = """\
+from twisted.internet import reactor
+
+reactor.connectTCP('localhost', 7771, MyFactory(), timeout=30)
+reactor.run()
+"""
+
+PULL_PRODUCER_CODE = """\
+class FileProducer:
+
+ def __init__(self, file, size, transport):
+ self.file = file; self.size = size
+ self.transport = transport # the consumer
+ transport.registerProducer(self, 0)
+
+ def resumeProducing(self):
+ if not self.transport: return
+ self.transport.write(self.file.read(16384))
+ if self.file.tell() == self.size:
+ self.transport.unregisterProducer()
+ self.transport = None
+
+ def pauseProducing(self): pass
+
+ def stopProducing(self):
+ self.file.close()
+ self.request = None
+"""
+
+PUSH_PRODUCER_CODE = """\
+from twisted.internet import reactor
+
+class GarbageProducer:
+
+ def __init__(self, transport):
+ self.paused = 0; self.stopped = 0
+ self.transport = transport
+ transport.registerProducer(self, 1)
+ self.produce()
+
+ def produce(self):
+ if not self.paused:
+ self.transport.write('blabla')
+ if not self.stopped:
+ reactor.callLater(0.1, self.produce)
+
+ def stopProducing(self):
+ self.stopped = 1
+
+ def pauseProducing(self):
+ self.paused = 1
+
+ def resumeProducing(self):
+ self.paused = 0
+"""
+
+SCHEDULING_CODE = """\
+from twisted.internet import reactor
+
+def f(x, y=1):
+ print x, y
+
+i = reactor.callLater(0.1, f, 2, y=4)
+i.delay(2)
+i.reset(1)
+i.cancel()
+"""
+
+FACTORY_START_CODE = """\
+from twisted.internet.protocol import ServerFactory
+
+class LogFactory(ServerFactory):
+
+ def startFactory(self):
+ self.log = open('log.txt', 'w')
+
+ def stopFactory(self):
+ self.log.close()
+"""
+
+LOGGING_CODE = """\
+from twisted.python import log
+
+# by default only errors are logged, to stderr
+logFile = open('log.txt', 'a')
+log.startLogging(logFile)
+
+log.msg('Something has occurred')
+"""
+
+LOGGING_ERRORS_CODE = """
+from twisted.python import log, failure
+
+e = ValueError('ONO')
+log.err(failure.Failure(e))
+
+try:
+ doSomethingElse()
+except:
+ log.deferr()
+"""
+
+SERVICE_CODE = """\
+from twisted.internet import app
+
+class FooService(app.ApplicationService):
+ def startService(self):
+ # do startup stuff
+ def stopService(self):
+ # do shutdown stuff
+ def foobrizate(self):
+ # business logic!
+
+application = app.Application('foobnator')
+svc = FooService('foo', application)
+application.getServiceNamed('foo') is svc # True
+"""
+
+RUNNABLE_APP_CODE = """\
+# this is web.py
+from twisted.internet import app
+from twisted.web import static, server
+
+application = app.Application('web')
+application.listenTCP(8080, server.Site(static.File('/var/www')))
+
+if __name__ == '__main__':
+ application.run(save=0)
+"""
+
+TWISTD_CODE = """\
+$ twistd -y web.py
+$ lynx http://localhost:8080
+$ kill `cat twistd.pid`
+"""
+
+GUI_CODE = """\
+from twisted.internet import gtkreactor
+gtkreactor.install()
+import gtk
+w = gtk.GtkWindow(gtk.WINDOW_TOPLEVEL)
+w.show_all()
+from twisted.internet import reactor
+reactor.run()
+"""
+
+lecture = Lecture(
+ "The twisted.internet Tutorial of Doom",
+
+ Slide("Part 1 - Introduction"),
+
+ # there are different ways to do networking
+ # mention processes are not cross-platform
+ Slide("Choosing a networking paradigm for the enterprise",
+ Bullet("Event driven"),
+ Bullet(Bad("Threads")),
+ Bullet("Others which we will ignore (processes, SEDA, ...)")),
+
+ # it's a metaphor!
+ Slide("Applied Bistromathics 101",
+ Bullet("Consider a restaurant as a network application"),
+ Bullet("Clients come in, make requests to the waiters"),
+ Bullet("Waiters act on clients' choices")),
+
+ # an event loop is efficient, doesn't waste time
+ # event loop is also used for GUIs
+ Slide("The event driven waiter",
+ Bullet("One waiter, serving all tables"),
+ Bullet("Waiter takes orders from tables to kitchen"),
+ Bullet("Waiter takes food from kitchen to tables")),
+
+ # not accurate, but the problems are real. avoid threads if you can
+ Slide("Threads (a caricature)",
+ Bullet(Bad("One waiter per table")),
+ SubBullet("Problems:",
+ Bullet(Bad("Expensive")),
+ Bullet(Bad("Waiters need to be careful not bump into each other")),
+ )),
+
+ # why threads are sometimes necessary
+ Slide("When do we want threads?",
+ Bullet("Long running, blocking operations"),
+ Bullet("Classic example: database access")),
+
+ # today we will discuss only (parts of) twisted.internet
+ Slide("Twisted: The Framework of Your Internet",
+ Image("twisted-overview.png")),
+
+ Slide("Project Stats",
+ Bullet("URL: ", URL("http://www.twistedmatrix.com")),
+ Bullet("License: LGPL"),
+ Bullet("Number of developers: approximately 20"),
+ Bullet("Version: 1.0.3"),
+ Bullet("Platforms: Unix, Win32"),
+ Bullet("Started in January 2000 by Glyph Lefkowitz")),
+
+ Slide("Part 2 - Basic Networking With Twisted"),
+
+ # quick review of how the internet works
+ Slide("Internet!",
+ Bullet("Network of interconnected machines"),
+ Bullet("Each machine has one (or more) IP addresses"),
+ Bullet("DNS maps names ('www.yahoo.com') to IPs (216.109.125.69)"),
+ Bullet("TCP runs on top of IP, servers listen on of of 65536 ports,"
+ " e.g. HTTP on port 80"),),
+
+ # we need to understand certain basic terms before we continue.
+ # the event loop is the last thing we run - it waits until
+ # an event occurs, then calls the appropriate handler.
+ Slide("Basic Definitions - Reactor",
+ Bullet("An object implementing the event loop",
+ PRE(EVENT_LOOP_CODE))),
+
+ Slide("Basic Definitions - Transport",
+ Bullet("Moves data from one location to another"),
+ Bullet("Main focus of talk are ordered, reliable byte stream transports"),
+ Bullet("Examples: TCP, SSL, Unix sockets"),
+ Bullet("UDP is a different kind of transport")),
+
+ # the client is the side which initiated the connection
+ # HTTP and SSH run on TCP-like transports, DNS runs on UDP or TCP
+ Slide("Basic Definitions - Protocol",
+ Bullet("Defines the rules for communication between two hosts"),
+ Bullet("Protocols communicate using a transport"),
+ Bullet("Typically there is a client, and server"),
+ Bullet("Examples: HTTP, SSH, DNS")),
+
+ Slide("All Together Now",
+ Bullet("The reactor gets events from the transports (read from network, write to network)"),
+ Bullet("The reactor passes events to protocol (connection lost, data received)"),
+ Bullet("The protocol tells the transport to do stuff (write data, lose connection)")),
+
+ # designing a new protocol is usually a bad idea, there are lots of
+ # things you can get wrong, both in design and in implementation
+ Slide("How To Implement A Protocol",
+ Bullet("Hopefully, you don't.")),
+
+ # XXX split into three expanded slides?
+ NumSlide("How To Not Implement A Protocol",
+ Bullet("Use an existing Twisted implementation of the protocol"),
+ Bullet("Use XML-RPC"),
+ Bullet("Use Perspective Broker, a remote object protocol")),
+
+ # connectionMade is called when connection is made
+ # dataReceived is called every time we receive data from the network
+ # connectionLost is called when the connection is lost
+ Slide("How To Really Implement A Protocol",
+ PRE(PROTOCOL_CODE)),
+
+ # factories - why?
+ Slide("Factories",
+ Bullet("A protocol instance only exists as long as the connection is there"),
+ Bullet("Protocols want to share state"),
+ Bullet("Solution: a factory object that creates protocol instances")),
+
+ # factory code - notice how protocol instances have access to the factory
+ # instance, for shared state. buildProtocol can return None if we don't
+ # want to accept connections from that address.
+ Slide("A Server Factory",
+ PRE(SERVER_CODE)),
+
+ # running the server we just wrote
+ Slide("Connecting A Factory To A TCP Port",
+ PRE(RUNNING_SERVER_CODE)),
+
+ # transport independence - using listenUNIX as example
+ Slide("Transport Independence",
+ Bullet("Notice how none of the protocol code was TCP specific"),
+ Bullet("We can reuse same protocol with different transports"),
+ Bullet("We could use listenUNIX for unix sockets with same code"),
+ Bullet("Likewise listenSSL for SSL or TLS")),
+
+ Slide("Client Side Protocol",
+ PRE(CLIENT_PROTOCOL_CODE)),
+
+ # client connections are different
+ Slide("Client Side Factories",
+ Bullet("Different requirements than server"),
+ Bullet("Failure to connect"),
+ Bullet("Automatic reconnecting"),
+ Bullet("Cancelling and timing out connections")),
+
+ # example client factory - explain use of default buildProtocol
+ Slide("Client Side Factories 2",
+ PRE(CLIENT_FACTORY_CODE)),
+
+ # connectTCP
+ Slide("Connection API",
+ PRE(CLIENT_CONNECT_CODE)),
+
+ # explain how transports buffer the output
+ Slide("Buffering",
+ Bullet("When we write to transport, data is buffered"),
+ Bullet("loseConnection will wait until all buffered data is sent, and producer (if any) is finished")),
+
+ # start/stopFactory
+ Slide("Factory Resources",
+ Bullet("Factories may want to create/clean up resources"),
+ Bullet("startFactory() - called on start of listening/connect"),
+ Bullet("stopFactory() - called on end of listening/connect"),
+ Bullet("Called once even if factory listening/connecting multiple ports")),
+
+ # example of restartable factory
+ Slide("Factory Resources 2",
+ PRE(FACTORY_START_CODE)),
+
+ Slide("Producers and Consumers",
+ Bullet("What if we want to send out lots of data?"),
+ Bullet("Can't write it out all at once"),
+ Bullet("We don't want to write too fast")),
+
+ Slide("Producers",
+ Bullet("Produce data for a consumer, in this case by calling transport's write()"),
+ Bullet("Pausable (should implement pauseProducing and resumeProducing methods)"),
+ Bullet("Push - keeps producing unless told to pause"),
+ Bullet("Pull - produces only when consumer tells it to")),
+
+ Slide("Consumers",
+ Bullet("registerProducer(producer, streaming)"),
+ Bullet("Will notify producer to pause if buffers are full")),
+
+ Slide("Sample Pull Producer",
+ PRE(PULL_PRODUCER_CODE)),
+
+ Slide("Sample Push Producer",
+ PRE(PUSH_PRODUCER_CODE)),
+
+ # scheduling events
+ Slide("Scheduling",
+ PRE(SCHEDULING_CODE)),
+
+ # pluggable reactors - why?
+ Slide("Choosing a Reactor - Why?",
+ Bullet("GUI toolkits have their own event loop"),
+ Bullet("Platform specific event loops")),
+
+ Slide("Choosing a Reactor",
+ Bullet("Twisted supports multiple reactors"),
+ Bullet("Default, gtk, gtk2, qt, win32 and others"),
+ Bullet("Tk and wxPython as non-reactors"),
+ Bullet("Reactor installation should be first thing code does")),
+
+ # example GUI client
+ Slide("Example GTK Program",
+ PRE(GUI_CODE)),
+
+ # you can learn more about
+ Slide("Learning more about networking and scheduling",
+ Bullet("twisted.internet.interfaces"),
+ Bullet("http://twistedmatrix.com/document/howtos/")),
+
+
+ Slide("Part 3 - Building Applications With Twisted"),
+
+ # the concept of the application
+ Slide("Applications",
+ Bullet("Reactor is a concept of event loop"),
+ Bullet("Application is higher-level"),
+ Bullet("Configuration, services, persistence"),
+ Bullet("Like reactor, you can listenTCP, connectTCP, etc.")),
+
+ # services concept
+ Slide("Services",
+ Bullet("Services can be registered with Application"),
+ Bullet("A service encapsulates 'business logic'"),
+ Bullet("Infrastructure outside the scope of protocols"),
+ Bullet("Examples: authentication, mail storage")),
+
+ # service example code
+ Slide("Services 2",
+ PRE(SERVICE_CODE)),
+
+ # logging
+ Slide("Logging",
+ PRE(LOGGING_CODE)),
+
+ # logging errors
+ # explain why this is good idea (twistd -b)
+ Slide("Logging Errors",
+ PRE(LOGGING_ERRORS_CODE)),
+
+ # twistd idea
+ Slide("twistd - Application Runner",
+ Bullet("Single access point for running applications"),
+ Bullet("Separate configuration from deployment")),
+
+ # twistd features
+ Slide("twistd Features",
+ Bullet("Daemonization"),
+ Bullet("Log file selection (including to syslog)"),
+ Bullet("Choosing reactor"),
+ Bullet("Running under debugger"),
+ Bullet("Profiling"),
+ Bullet("uid, gid"),
+ Bullet("Future: WinNT Services")),
+
+ # making modules for twistd -y
+ Slide("Making a runnable application",
+ PRE(RUNNABLE_APP_CODE)),
+
+ # running the server
+ Slide("Running twistd",
+ PRE(TWISTD_CODE)),
+
+ Slide("Part 4: Further Bits and Pieces"),
+
+ Slide("Other twisted.internet Features",
+ Bullet("UDP, Multicast, Unix sockets, Serial"),
+ Bullet("Thread integration")),
+
+ Slide("Deferreds",
+ Bullet("Deferred - a promise of a result"),
+ Bullet("Supports callback chains for results and exceptions"),
+ Bullet("Used across the whole framework"),
+ Bullet("Make event-driven programming much easier"),
+ Bullet("Can work with asyncore too, not just Twisted")),
+
+ Slide("Protocol implementations",
+ Bullet("Low-level implementations, without policies"),
+ Bullet("SSH, HTTP, SMTP, IRC, POP3, telnet, FTP, TOC, OSCAR, SOCKSv4, finger, DNS, NNTP, IMAP, LDAP"),
+ Bullet("Common GPS modem protocols")),
+
+ Slide("Frameworks",
+ Bullet("twisted.web - Web server framework"),
+ Bullet("twisted.news - NNTP server framework"),
+ Bullet("twisted.words - messaging framework"),
+ Bullet("twisted.names - DNS server")),
+
+ Slide("Perspective Broker",
+ Bullet("Object publishing protocol"),
+ Bullet("Fast, efficient and extendable"),
+ Bullet("Two-way, asynchronous"),
+ Bullet("Secure and encourages secure model"),
+ Bullet("Implemented in Python for Twisted, and Java")),
+
+ Slide("Lore",
+ Bullet("Simple documentation system"),
+ Bullet("Simple subset of XHTML"),
+ Bullet("Generates LaTeX, XHTML")),
+
+ Slide("Reality",
+ Bullet("Multiplayer text simulation framework"),
+ Bullet("Original source of Twisted project"),
+ Bullet("Now a totally different project")),
+)
+
+
+if __name__ == '__main__':
+ lecture.renderHTML(".", "twisted_internet-%02d.html", css="main.css")
diff --git a/doc/historic/2003/pycon/twisted-reality/componentized.svg b/doc/historic/2003/pycon/twisted-reality/componentized.svg
new file mode 100644
index 0000000..613192a
--- /dev/null
+++ b/doc/historic/2003/pycon/twisted-reality/componentized.svg
@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<!-- Created with Sodipodi ("http://www.sodipodi.com/") -->
+<svg
+ id="svg137"
+ sodipodi:version="0.31"
+ width="11in"
+ height="8in"
+ sodipodi:docbase="/home/washort/projects/PyCon/"
+ sodipodi:docname="/home/washort/projects/PyCon/adapters2.svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs
+ id="defs139" />
+ <sodipodi:namedview
+ id="base"
+ snaptoguides="false"
+ showgrid="false" />
+<g>
+<animateTransform attributeName="transform" attributeType="XML" begin="1s" dur="3s" values="1,1;1.5,1.5" type="scale" fill="freeze" additive="sum" />
+<animateMotion begin="1s" dur="3s" fill="freeze" path="M 0 0 L -40 -300"/>
+<g style="fill-rule:evenodd;stroke-width:10;stroke:#000000;stroke-opacity:1;stroke-dasharray:none;stroke-linejoin:bevel;fill-opacity:1;">
+<animate attributeName="stroke-opacity" attributeType="CSS" begin="1s" dur="3s" from="1" to="0.4" fill="freeze"/>
+<animate attributeName="fill-opacity" attributeType="CSS" begin="1s" dur="3s" from="1" to="0.4" fill="freeze"/>
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#3e62db"
+ id="polygon152"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,462.812,244.007)" />
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#d86264"
+ id="polygon234"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,634.521,144.935)" />
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#d86264"
+ id="polygon243"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,634.604,342.45)" />
+
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;;fill:#d86264"
+ id="polygon252"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,290.377,145.564)" />
+
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#d86264"
+ id="polygon261"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,462.169,442.179)" />
+
+ <polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#d86264"
+ id="polygon270"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,462.812,45.8392)" />
+
+ <g style="stroke-width:2pt">
+ <path
+ d="M 538.285 387.439 L 571.873 407.105 "
+ id="path372"
+ transform="translate(171.25,-20)" />
+ <path
+ d="M 538.285 308.11 L 572.315 288.223 "
+ id="path373"
+ transform="translate(171.25,-20)" />
+ <path
+ d="M 365.044 287.781 L 400.399 308.552 "
+ id="path376"
+ transform="translate(171.25,-20)" />
+ <path
+ d="M 468.9 465.883 L 468.9 426.993 "
+ id="path377"
+ transform="translate(171.25,-20)" />
+ <path
+ d="M 469.342 268.335 L 469.342 229.445 "
+ id="path378"
+ transform="translate(171.25,-20)" /></g>
+</g>
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-opacity:0.398649;stroke-width:2pt;stroke-linejoin:miter;stroke-linecap:butt;fill-opacity:1;"
+ d="M 365.928 407.547 L 399.957 387.66 "
+ id="path379"
+ transform="translate(171.25,-20)" />
+<polygon
+ sodipodi:type="star"
+ style="font-size:12;fill:#d86264;fill-rule:evenodd;stroke-width:10;stroke:#000000;stroke-opacity:1;stroke-dasharray:none;stroke-linejoin:bevel;fill-opacity:1;"
+ id="polygon225"
+ sodipodi:sides="6"
+ sodipodi:cx="355.625061"
+ sodipodi:cy="167.987030"
+ sodipodi:r1="183.750000"
+ sodipodi:r2="159.132095"
+ sodipodi:arg1="0.000000"
+ sodipodi:arg2="0.523599"
+ points="539.375,167.987 493.437,247.553 447.5,327.119 355.625,327.119 263.75,327.119 217.813,247.553 171.875,167.987 217.813,88.4209 263.75,8.85486 355.625,8.85493 447.5,8.85486 493.438,88.421 539.375,167.987 "
+ transform="matrix(0.5,0,0,0.5,290.836,343.189)" />
+<g style="fill:#000000;stroke:none;font-family:URW Gothic L;font-style:normal;font-weight:normal;font-size:36;fill-opacity:1;stroke-opacity:1;stroke-width:1pt;stroke-linejoin:miter;stroke-linecap:butt;text-anchor:start;writing-mode:lr;" >
+<animate attributeName="fill-opacity" attributeType="CSS" begin="1s" dur="3s" from="1" to="0.4" fill="freeze"/>
+ <text
+ x="594.546"
+ y="312.197"
+ id="text383">
+ <tspan
+ x="594.546"
+ y="312.197"
+ sodipodi:role="line"
+ id="tspan476">
+Thing</tspan>
+ <tspan
+ x="594.546"
+ y="348.197"
+ sodipodi:role="line"
+ id="tspan478">
+</tspan>
+ </text>
+ <text
+ x="394.315"
+ y="230.363"
+ id="text388">
+ <tspan
+ x="394.315"
+ y="230.363"
+ sodipodi:role="line"
+ id="tspan504">
+Weapon</tspan>
+ </text>
+
+ <text
+ x="569.441"
+ y="524.279"
+ id="text396">
+ <tspan
+ x="569.441"
+ y="524.279"
+ sodipodi:role="line"
+ id="tspan460">
+Portable</tspan>
+ </text>
+ <text
+ style="font-size:18;font-style:italic"
+ x="561.266"
+ y="347.885"
+ id="text480">
+ <tspan
+ x="561.266"
+ y="347.885"
+ sodipodi:role="line"
+ id="tspan496">
+(Componentized)</tspan>
+ </text>
+
+ <text
+ style="font-size:18;font-style:italic"
+ x="594.854"
+ y="548.526"
+ id="text498">
+ <tspan
+ x="594.854"
+ y="548.526"
+ sodipodi:role="line"
+ id="tspan499">
+(Adapter)</tspan>
+ </text>
+ <text
+ style="font-size:18;font-style:italic"
+ x="422.496"
+ y="254.194"
+ id="text501">
+ <tspan
+ x="422.496"
+ y="254.194"
+ sodipodi:role="line"
+ id="tspan502">
+(Adapter)</tspan>
+ </text>
+ </g>
+<text
+ style="fill:#000000;stroke:none;font-family:URW Gothic L;font-style:italic;font-weight:normal;font-size:18;text-anchor:start;writing-mode:lr;fill-opacity:1;troke-opacity:1;stroke-width:1pt;stroke-linejoin:miter;stroke-linecap:butt;"
+ x="422.496"
+ y="451.299"
+ id="text489">
+ <tspan
+ x="422.496"
+ y="451.299"
+ sodipodi:role="line"
+ id="tspan494">
+(Adapter)</tspan>
+ </text>
+ <text
+ style="fill:#000000;stroke:none;font-family:URW Gothic L;font-style:roman;font-weight:normal;font-size:26;text-anchor:start;writing-mode:lr;fill-opacity:1;stroke-opacity:1;stroke-width:1pt;stroke-linejoin:miter;stroke-linecap:butt;"
+ x="388.457"
+ y="430.303"
+ id="text393">
+ <tspan
+ x="388.457"
+ y="430.303"
+ sodipodi:role="line"
+ id="tspan458">
+Merchandise</tspan>
+ </text>
+</g>
+</svg>
diff --git a/doc/historic/2003/pycon/twisted-reality/twisted-reality.html b/doc/historic/2003/pycon/twisted-reality/twisted-reality.html
new file mode 100644
index 0000000..bb96500
--- /dev/null
+++ b/doc/historic/2003/pycon/twisted-reality/twisted-reality.html
@@ -0,0 +1,578 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+
+<html>
+ <head>
+ <title>Twisted Reality: A Flexible Framework for Virtual Worlds</title>
+ <link href="stylesheet.css" type="text/css" rel="stylesheet" />
+ </head>
+ <body>
+ <h1>Twisted Reality: A Flexible Framework for Virtual Worlds</h1>
+
+ <ul>
+ <li>Allen Short
+<a href="washort@twistedmatrix.com">&lt;washort@twistedmatrix.com&gt;</a></li>
+ <li>Glyph Lefkowitz
+ <a href="glyph@twistedmatrix.com">&lt;glyph@twistedmatrix.com&gt;</a></li>
+ </ul>
+
+ <h2>Abstract</h2>
+ <p>Flexibly modelling virtual worlds in object-oriented languages has
+ historically been difficult; the issues arising from multiple
+ inheritance and order-of-execution resolution have limited the
+ sophistication of existing object-oriented simulations. Twisted
+ Reality avoids these problems by reifying both actions and
+ relationships, and avoiding inheritance in favor of automated
+ composition through adapters and interfaces.</p>
+
+ <h2>Motivation</h2>
+
+ <p>Text-based simulations have a long and venerable history, from
+ games such as Infocom's Zork and Bartle's MUD to modern systems
+ such as Inform, LambdaMOO and Cold. The general trend in the
+ development of these systems has been toward domain-specific
+ languages, which has largely been an improvement. However, a
+ discrepancy remains between systems for single-user and
+ multiple-user simulations: in single-user systems such as Inform,
+ incremental extensibility has been sacrificed to allow for complex
+ interaction with the world; whereas in multiple-user systems,
+ incremental extensibility is paramount, but it is achieved at the
+ cost of a much simpler model of interaction. Twisted Reality aims
+ to bring the sophistication of Inform's action model to multiuser
+ simulation.</p>
+
+
+<h2>The Twisted Component Model</h2>
+
+<p>Twisted's component system is almost identical to Zope 3's. The
+primary element is the interface, a class used as a point of
+integration and documentation. Classes may declare the interfaces they
+implement by setting their <code class="python">__implements__</code>
+attribute to a tuple of interfaces. Additional interfaces may be added
+to classes with <code
+class="python">registerAdapter(adapterClass,originalClass,interface)</code>;
+when <code class="python">getAdapter(obj, interfaceClass)</code> is
+called on an object, the adapter associated with that interface and
+class is looked up and instantiated as a wrapper around <code
+class="python">obj</code>. (Alternately, if <code
+class="python">obj</code> implements the requested interface, the
+original object is simply returned.)</p>
+
+<h3>Componentized</h3>
+<p>In addition to the basic system of adapters and interfaces, Twisted
+has the <code class="python">Componentized</code> class. Instances of
+<code class="python">Componentized</code> hold instances of their
+adapters. This storage of adapter instances encourages separation of
+concerns; multiple related instances representing aspects of a
+simulation object can be automatically composed in a single
+Componentized instance.</p>
+
+<p><code class="python">Componentized</code> is the heart of Twisted
+Reality; it is subclassed by <code class="python">Thing</code>, the
+base class for all simulation objects. Functionality is added to
+<code class="python">Thing</code>s with adapters; for example, the
+<code class="python">Portable</code> adapter adds the abilities to be
+picked up and dropped. </p>
+
+<p>By separating aspects of the simulation object into multiple
+instances, several improvements in ease of code maintenance can be
+realized. Persistence of simulation objects, for example, is greatly
+eased by <code class="python">Componentized</code>: each adapter's
+state can be stored in a separate database table or similar data
+store.</p>
+
+<h2>Parsing System</h2>
+
+<p>The key element missing from multiuser simulations' parsing systems
+is an abstract representation of actions. Current systems proceed
+directly from parsing the user's input to executing object-specific
+code. For example, LambdaMOO, one of the most popular object-oriented
+simulation frameworks, handles input using a non-customizable lexer
+which dispatches to parsing methods on simulation objects. The
+ColdCore framework, a similar effort, improves on this model by
+providing pattern-matching facilities for the lexer, but performs
+dispatch in essentially the same fashion. In contrast to these
+systems, Twisted Reality separates parsing from simulation objects
+entirely, keeping a global registry of parser methods which produce
+objects representing actions, rather than directly performing the
+actions. Adding this layer allows for more sophisticated parsing and
+sensitivity to ambiguity.</p>
+
+<p>The parser in <code class="python">reality.text.english</code> uses
+a relatively simple strategy: it keeps a parser registry which maps
+<q>verbs</q> (i.e., substrings at the beginning of the user input) to
+parser methods, and runs all methods whose prefixes match the input,
+collecting the actions they return. Parsing methods are added to the
+system by registering <code class="python">Subparser</code>s. </p>
+
+<pre class="python">class MusicParser(english.Subparser):
+ def parse_blow(self, player, instrumentName):
+ actor = player.getComponent(IPlayWindInstrumentActor)
+ if actor is None:
+ return []
+ return [PlayWindInstrument(actor, instrumentName)]
+
+english.registerSubparser(MusicParser())</pre>
+
+<p><code class="python">english.registerSubparser</code> collects
+ methods prefixed with <code class="python">parse_</code> from
+ the subparser and places them in the parsing registry.</p>
+
+<pre class="shell">a Room
+You see a rocket, a whistle, and a candle.
+Exits: a door, north
+bob: <b>blow whistle</b>
+You play a shrill blast upon a whistle.</pre>
+
+<p>Here is one of the simplest cases for the parser: <q><code
+class="shell">blow whistle</code></q> should obviously resolve to a
+single action, in this case <code class="python"
+>PlayWindInstrument</code>.</p>
+
+<p>The parser calls <code class="python">MusicParser.parse_blow</code>
+with the actor and the remainder of the input, and adds the list of
+actions it returns to the collection of possible actions. If only one
+action is possible, it immediately dispatches it. This strategy allows
+the parser to examine the state of the simulation before committing to
+a decision about what the player means. For example, the check for the
+actor interface is a simple form of permissions; if you don't
+implement the required interface, you aren't allowed to perform the
+action.</p>
+
+<p>Since this sort of parser is quite common, it has been generalized
+ to a simple mapping of command names to actions:</p>
+<pre class="python">class FireParser(english.Subparser):
+ simpleTargetParsers = {"blow": Extinguish}
+
+english.registerSubparser(FireParser())</pre>
+<pre class="shell">bob: <b>blow candle</b>
+You blow out a candle.
+</pre>
+<p>The real test of any parsing system of this nature, of course, is
+its ability to handle ambiguity. Since two possibilities for
+parsing a command starting with <q>blow</q> now exist, the parser has two
+potential actions to examine: <code class="python"
+>PlayWindInstrument</code> and <code class="python"
+>Extinguish</code>. Obviously, only <code class="python"
+>Extinguish</code> makes sense, and the parser determines this by
+examining the interfaces on the targets and rejecting actions for
+which the target is invalid.</p>
+
+<pre class="python">class ExplosivesParser(english.Subparser):
+ simpleToolParsers = {"blow": BlowUp}
+
+english.registerSubparser(ExplosivesParser())
+</pre>
+<pre class="shell">bob: <b>blow door</b>
+You fire a rocket at a door.
+*BOOM*!!
+The door shatters to pieces!
+</pre>
+
+<p> The other common case is actions with three participants -- actor,
+target, and tool. The parser generated here is intelligent enough to
+look around for an appropriate tool (again, by examining interfaces)
+and include it in the action.</p>
+
+<p>Despite these techniques for disambiguating the user's meaning,
+situations will inevitably arise where multiple actions are equally
+valid parses. In these cases, the parser formats the list of potential
+actions and presents the choices to the user.</p>
+
+<pre class="shell">You see a short sword, and a long sword.
+bob: <b>get sword</b>
+Which Target?
+1: long sword
+2: short sword
+bob: <b>1</b>
+You take a long sword.</pre>
+
+<h2>Actions System</h2>
+
+
+<p>Actions in Twisted Reality, as in Inform, are objects representing
+a successful parse of a player's intentions. Actions are classified
+according to the number of objects they operate upon: <code
+class="python">NoTargetAction</code> (actions such as <code
+class="python">Say</code> or <code class="python">Look</code>), <code
+class="python">TargetAction</code> (e.g. <code
+class="python">Eat</code>, <code class="python">Wear</code>), <code
+class="python">ToolAction</code> (e.g. <code
+class="python">Open</code>, <code class="python">Take</code>). When
+actions are defined, interfaces corresponding to the possible roles in
+the action are also created. When an action is instantiated, it asks
+the participants in the action to adapt themselves to the actor,
+target, or tool interfaces, as appropriate. When dispatched, the
+action may call handler methods on the adapted objects or dispatch
+subsidiary actions.</p>
+
+<pre class="python">IDamageActor = things.IThing
+class Damage(actions.ToolAction):
+ def formatToActor(self):
+ with = ""
+ if self.tool:
+ with = " with ", self.tool
+ return ("You hit ",self.target) + with + (".",)
+ def formatToTarget(self):
+ with = ()
+ if self.tool:
+ with = " with ", self.tool
+ return (self.actor," hits you") + with + (".",)
+ def formatToOther(self):
+ with = ""
+ if self.tool:
+ with = " with ", self.tool
+ return self.actor," hits ",self.target) + with + (".",)
+ def doAction(self):
+ amount = self.tool.getDamageAmount()
+ self.target.damage(amount)
+
+class Weapon(components.Adapter):
+ __implements__ = IDamageTool
+ def getDamageAmount(self):
+ return 10
+
+class Damageable(components.Adapter):
+ __implements__ = IDamageTarget
+ def damage(self, amount):
+ self.original.emitEvent("Ow! that hurt. You take %d points of damage."
+ % amount, intensity=1)
+class HarmParser(english.Subparser):
+ simpleToolParsers = {"hit":Damage}
+
+english.registerSubparser(HarmParser())
+components.registerAdapter(Damageable, things.Actor, IDamageTarget)</pre>
+
+<p><code class="python">actions.ToolAction</code>, via metaclass
+magic, creates three interfaces when subclasssed, named after the
+subclass: in this case, <code class="python">IDamageActor</code>,
+<code class="python" >IDamageTarget</code>, and <code
+class="python">IDamageTool</code>. However, since <code
+class="python">IDamageActor</code> already exists, the metaclass does
+ not clobber it. Setting <code class="python">IDamageActor</code> to
+<code class="python">IThing</code> indicates that any <code
+class="python">Thing</code> may perform the <code
+ class="python">Damage</code> action. The other elements of the action
+are represented here by <code class="python">Weapon</code> and <code
+class="python">Damageable</code> as the tool and the target,
+respectively. The <code class="python">HarmParser</code> adds a
+<q>hit</q> command, and the call to <code
+class="python">registerAdapter</code> ensures that any <code
+class="python">Actor</code>s who do not already have a
+component implementing <code class="python">IDamageTarget</code> will
+receive a <code class="python">Damageable</code> when needed.
+</p>
+<pre class="python">room = ambulation.Room("room")
+bob = things.Actor( "Bob")
+rodney = things.Actor("rodneY")
+sword = things.Movable("sword")
+
+sword.addAdapter(conveyance.Portable, True)
+sword.addAdapter(harm.Weapon, True)
+
+
+for o in rodney, bob, sword:
+ o.moveTo(room)
+</pre>
+
+<p>In this example, we create instances of <code
+class="python">Movable</code> <code class="python">Actor</code>
+(subclasses of <code class="python">Thing</code>), a <code
+class="python">Room</code>, then adds a <code
+class="python">Portable</code> adapter to the sword, allowing it to be
+picked up and dropped, as well as a <code class="python">Weapon</code>
+adapter, and finally moves all three into the room.</p>
+
+<pre class="shell">a room
+You see rodneY, and a sword.
+Bob: <b>get sword</b>
+You take a sword.
+Bob: <b>hit rodney with sword</b>
+You hit rodneY with a sword.</pre>
+
+<p>The parser instantiates the <code class="python">Damage</code>
+action with Bob, Rodney, and the sword as actor, target, and tool. The
+action is dispatched, calling <code
+class="python">Damage.doAction</code>, which inflicts damage upon
+Rodney. From Rodney's perspective:</p>
+
+<pre class="shell">a room
+You see Bob, and a sword.
+Bob takes a sword.
+Bob hits you with a sword.
+Ow! that hurt. You take 10 points of damage.
+rodneY:</pre>
+
+<p>The primary advantage of this actions system is that it provides a
+central point for dispatching object-specific behaviour in a
+customizable manner. This mechanism prevents order-of-execution
+problems: in other simulations of this type, combining multiple game
+effects is difficult since the connections between them are not made
+explicit. When confronted with ambiguity, TR's action system refuses
+to guess: all combinations of effects that make sense must be
+implemented separately. The Adapters system makes this manageable even
+in the face of arbitrarily extended complexity.</p>
+
+<p>Also, it allows for centralized handling of string formatting,
+instead of having each actor or target handle output of event
+descriptions. For example, suppose there is a zone prohibiting PvP
+combat. The <code class="python">Damage</code> action can suppress the
+usual messages describing combat (as well as the actual damage
+routines) since it is responsible for generating them.</p>
+
+
+<h2>Composing Simulations with Adapters</h2>
+
+<p>The combination of these features -- an incrementally extendable
+parser, actions as first-class objects, componentized simulation
+objects -- provide a powerful basis for the composition of simulations
+within a virtual world, often enabling extensions to the world and
+object behaviour without touching unrelated code. For example, to add
+armor that reduces damage to the simple combat simulation described
+above, we add an <code class="python">Armor</code> class which
+forwards the <code class="python">IDamageTarget</code> interface:</p>
+<pre class="python">class Armor(raiment.Wearable):
+ __implements__ = IDamageTarget, raiment.IWearTarget, raiment.IUnwearTarget
+ originalTarget = None
+ armorCoefficient = 0.5
+ def dress(self, wearer):
+ originalTarget = wearer.getComponent(IDamageTarget)
+ if originalTarget:
+ self.originalTarget = originalTarget
+ wearer.original.setComponent(IDamageTarget, self)
+
+ def undress(self, wearer):
+ if self.originalTarget:
+ wearer.setComponent(IDamageTarget, self.originalTarget)
+
+ def damage(self, amount):
+ self.original.emitEvent("Your armor cushions the blow.", intensity=2)
+ if self.originalTarget:
+ self.originalTarget.damage(amount * self.armorCoefficient)</pre>
+
+<p><code class="python">Armor</code> inherits from the <code
+class="python">Wearable</code> adapter, and thus receives notification
+of the player wearing or removing it. When this happens, it forwards
+or unforwards the <code class="python">damage</code> method,
+respectively.</p>
+<pre class="shell">a room
+You see an armor, Bob, and a sword.
+rodneY: <b>take armor</b>
+You take an armor.
+rodneY: <b>wear armor</b>
+You put on an armor.
+Bob hits you with a sword.
+Your armor cushions the blow.
+Ow! that hurt. You take 5 points of damage.</pre>
+
+<p>In this fashion, the combat simulation can be extended to deal with
+various types of weapons, armor, damageable objects, and types of
+damage, with little or no changes to existing code.</p>
+
+<p> Now, let us consider a second type of simulation common to virtual
+ worlds: shops. We wish to prevent unpaid items from leaving the shop,
+ and to have a price associated with each item.</p>
+
+<pre class="python">
+class IVendor(components.Interface): pass
+class IMerchandise(components.Interface): pass
+
+class Buy(actions.TargetAction):
+ def formatToOther(self):
+ return ""
+ def formatToActor(self):
+ return ("You buy ",self.target," from ",self.vendor," for ",
+ self.target.price," zorkmids.")
+
+ def doAction(self):
+ vendors = self.actor.original.lookFor(None, IVendor)
+ if vendors:
+ #assume only one vendor per room, for now
+ self.vendor = vendors[0]
+ else:
+ raise errors.Failure("There appears to be no shopkeeper here "
+ "to receive your payment.")
+ amt = self.target.price
+ self.actor.withdraw(amt)
+ self.vendor.buy(self.target, amt)
+
+class ShopParser(english.Subparser):
+ simpleTargetParsers = {"buy": Buy}
+english.registerSubparser(ShopParser())</pre>
+
+<p>The basic behaviour for buying an object in a shop is simple:
+first, a vendor is located, the price is looked up, then money is
+transferred from the buyer's account to the vendor's.</p>
+
+<pre class="python">class Customer(components.Adapter):
+ __implements__ = IBuyActor
+
+ def withdraw(self, amt):
+ "interface to accounting system goes here"
+
+class Vendor(components.Adapter):
+ __implements__ = IVendor
+
+ def shoutPrice(self, merch, cust):
+ n = self.getComponent(english.INoun)
+ title = ('creature', 'sir','lady'
+ )[cust.getComponent(things.IThing).gender]
+ merchName = merch.original.getComponent(english.INoun).name))
+ self.original.emitEvent('%s says "For you, good %s, only %d '
+ 'zorkmids for this %s."' % (n.nounPhrase(cust),
+ title, merch.price,
+ merchName))
+
+ def buy(self, merchandise, amount):
+ self.deposit(amount)
+ merchandise.original.removeComponent(merchandise)
+
+ def stock(self, obj, price):
+ m = Merchandise(obj)
+ m.price = price
+ m.owner = self
+ m.home = self.original.location
+ obj.addComponent(m, ignoreClass=1)
+
+ def deposit(self, amt):
+ "more accounting code"</pre>
+<p>The essential operations for management of shop inventory are
+<code class="python">Vendor.stock</code> and <code
+class="python">Vendor.buy</code>, which add and remove a <code
+class="python">Merchandise</code> adapter, which stores the
+state related to the shop simulation for the object (in this case, its
+price, its owner, and the location it lives).</p>
+
+<pre class="shell">A weapons shop. You see a long sword, and Asidonhopo.
+Exits: a Secret Trapdoor, down; a Security Door, north
+bob: <b>get sword</b>
+You take a long sword.
+Asidonhopo says "For you, good sir, only 100 zorkmids for this long sword."
+</pre>
+
+<p>To enforce our anti-theft policy, we put constraints on the exits
+to the shop.</p>
+<pre class="python">
+class ShopDoor(ambulation.Door):
+ def collectImplementors(self, asker, iface, collection, seen,
+ event=None, name=None, intensity=2):
+ if iface == ambulation.IWalkTarget:
+ unpaidItems = asker.searchContents(None, IMerchandise)
+ if unpaidItems:
+ collection[self] = things.Refusal(self, "You cant leave, "
+ "you haven't paid!")
+ return
+
+ ambulation.Door.collectImplementors(self, asker, iface,
+ collection, seen, event,
+ name, intensity)
+ return collection</pre>
+
+<p><code class="python">collectImplementors</code> is the means by
+which queries for action participants are accomplished. It is a rather
+general graph-traversal mechanism and thus takes a few arguments:
+<code class="python">asker</code> is the object that initiated the
+query. <code class="python">iface</code> is the interface the results
+must conform to, <code class="python">collection</code> is the results
+so far, and <code class="python">seen</code> is a collection of
+objects already visited. The check done here is fairly simple: it
+refuses queries for <code class="python">IWalkTarget</code>s (the
+interface needed for walking between rooms) if the asker contains
+things that implement <code class="python">IMerchandise</code>, in
+particular unpaid items. Otherwise, it passes on the query to its
+superclass.</p>
+
+<pre class="shell">bob: <b>go north</b>
+You cant leave, you haven't paid!</pre>
+
+<p>Here, the <q>Security Door</q> examines the actor's contents for
+objects implementing IMerchandise. Since the sword still has a
+Merchandise adapter attached, the passage is barred.</p>
+
+<pre class="shell">bob: <b>go down</b></pre>
+
+<p>However, relying on the exits
+to contain merchandise is potentially error-prone; it demands knowing
+about all forms of locomotion in advance. If an unsecured exit from
+ the shop exists, or the player has the ability to teleport,
+ this form of security can be bypassed. Therefore, it is
+advantageous to have the Merchandise adapter itself keep the item
+within the shop.</p>
+
+<pre class="python">class Merchandise(components.Adapter):
+ __implements__ = IMerchandise, things.IMoveListener, IBuyTarget
+
+ def thingArrived(*args):
+ pass
+ def thingLeft(*args):
+ pass
+ def thingMoved(self, emitter, event):
+ if self.original == emitter and isinstance(event, conveyance.Take):
+ self.owner.shoutPrice(self, self.original.location)
+ if self.original.getOutermostRoom() != self.home:
+ self.original.emitEvent("The %s vanishes with a *foop*."
+ % self.getComponent(english.INoun).name)
+ self.original.moveTo(self.home)</pre>
+
+<p>When objects move, they broadcast events to nearby things
+ (where <q>nearby</q> is determined, again, by <code
+ class="python">collectImplementors</code>) that implement
+ <code class="python">IMoveListener</code>. In this case, the
+ <code class="python">Merchandise</code> adapter <q>listens</q>
+ for being picked up, and prompts the shopkeeper to quote the
+ price, and also checks to make sure it is contained by its
+ home room. If the player manages to leave the shop with unpaid
+ merchandise --</p>
+
+<pre class="shell">The long sword vanishes with a *foop*.</pre>
+
+<p>then it sets its location to its home room and informs the prospective
+shoplifter he no longer has his prize.</p>
+
+<h2>Future Directions</h2>
+
+<p>Current development efforts focus on enlarging the standard library
+of simulation objects and behaviour, developing web-based interfaces
+to the simulation, and improving the persistence layer. Possible
+extensions include client-side generation of action objects, enabling
+the development of graphical interfaces, or adapting the text system
+to other languages than English.</p>
+
+<h2>Conclusions</h2>
+
+<p>As seen in these examples, Twisted Reality provides features not
+found in other object-oriented simulation frameworks. The component
+model allows automatic aggregation of related objects; the actions
+system provides a mechanism for precise control of game effects; and
+the parser enables incremental extension of user input
+handling. Combined, they provide a powerful basis for modelling
+virtual worlds by composing simulations.</p>
+
+<h2>Acknowledgements</h2>
+<p>Thanks to Chris Armstrong and Donovan Preston for contributions to
+ Twisted Reality, and to Ying Li for editorial assistance.</p>
+
+<h2>References</h2>
+<ul>
+<li>Jason Asbahr, <a
+ href="http://asbahr.com/paper1html/paper1.html">Beyond: A
+ Portable Virtual World Simulation Framework</a>,
+ <i>Proceedings of the Seventh International Python
+ Conference</i> (1998).</li>
+
+<li>Pavel Curtis, <a
+ href="ftp://ftp.lambda.moo.mud.org/pub/MOO/ProgrammersManual.html"><i>LambdaMOO programmer's manual</i></a>, 1997.</li>
+<li> Jim Fulton, <a href="?">Zope Component Architecture</a></li>
+<li> Brandon Gillespie, <i><a
+ href="http://ice.cold.org:1180/bin/help?node=coldc">ColdC Reference Manual</a></i>, 2001.</li>
+<li>Glyph Lefkowitz, and Moshe Zadka, <q><a
+ href="http://twistedmatrix.com/doc/historic/ipc10paper">The Twisted Network Framework</a></q>, <i>Proceedings of the Tenth International Python Conference</i> (2002): 83.</li>
+<li>Graham Nelson, <i><a href="http://www.inform-fiction.org/manual/about_dm4.html">The
+ Inform Designer's Manual</a></i>. 4th ed. (St Charles, IL:
+ Interactive Fiction Library, 2001).</li>
+
+ </ul>
+
+ </body>
+</html>
diff --git a/doc/historic/2004/ibm/talk.html b/doc/historic/2004/ibm/talk.html
new file mode 100644
index 0000000..56d1bad
--- /dev/null
+++ b/doc/historic/2004/ibm/talk.html
@@ -0,0 +1,495 @@
+<html><head><title>Twisted: A Tutorial</title></head><body>
+
+<h1>Twisted: A Tutorial</h1>
+
+<h2>Thanks</h2>
+
+<p>I am grateful to IBM for inviting me to talk here, and to Muli Ben-Yehuda for arranging everything.</p>
+
+<h2>Administrative Notes</h2>
+
+<p>After reading Peter Norvig's infamous <q>The Gettysburg Powerpoint Presentation</q>, I was traumatized enough to forgoe the usual bullets and slides style, which originally was developed for physical slide projectors. Therefore, these notes are presented as one long HTML file, and I will use a new invention I call the <q>scrollbar</q> to show just one thing at a time. Enjoy living on the bleeding edge of presentation technology!</p>
+
+<h2>What Is Twisted?</h2>
+
+<p>Twisted is an <em>event-based networkings framework for Python</em>. It includes not only the basics of networking but also high-level protocol implementations, scheduling and more. It uses Python's high-level nature to enable few dependencies between different parts of the code. Twisted allows you to write network applications, clients and servers, without using threads and without running into icky concurrency issues.</p>
+
+<blockquote>
+A computer is a state machine.
+Threads are for people who can't program state machines.
+</blockquote>
+
+<p>Alan Cox in a discussion about the threads and the Linux scheduler</p>
+<p>http://www.bitmover.com/lm/quotes.html</p>
+
+<h2>An Extremely Short Introduction to Python</h2>
+
+<p>Python is a high-level dyanmically strongly typed language. All values are references to objects, and all arguments passed are objects references. Assignment changes the reference a variable points to, not the reference itself. Data types include integers (machine sized and big nums) like <code>1</code> and <code>1L</code>, strings and unicode strings like <code>"moshe"</code> and <code>u"\u05DE\u05E9\u05D4 -- moshe"</code>, lists (variably typed arrays, really) like <code>[1,2,3, "lalala", 10L, [1,2]]</code>, tuples (immutable arrays) like <code>("1", 2, 3)</code>, dictionaries <code>{"moshe": "person", "table": "thing"}</code> and user-defined objects.</p>
+
+<p>Every Python object has a type, which is itself a Python object. Some types aare defined in native C code (such as the types above) and some are defined in Python using the class keyword.</p>
+
+<p>Structure is indicated through indentation.</p>
+
+<p>Functions are defined using</p>
+
+<pre class="py-listing">
+def function(param1, param2, optionalParam="default value", *restParams,
+ **keywordParams):
+ pass
+</pre>
+
+<p>And are called using <code>function("value for param1", param2=42,
+optionalParam=[1,2], "these", "params", "will", "be", "restParams",
+keyword="arguments", arePut="in dictionary keywordParams")</code>.</p>
+
+<p>Functions can be defined inside classes:</p>
+
+<pre class="py-listing">
+class Example:
+ # constructor
+ def __init__(self, a=1):
+ self.b = a
+ def echo(self):
+ print self.b
+e = Example(5)
+e.echo()
+</pre>
+
+<p>All methods magically receive the self argument, but must treat it
+explicitly.</p>
+
+<p>Functions defined inside functions enjoy lexical scoping. All variables
+are outer-scope unless they are assigned to in which case they are inner-most
+scope.</p>
+
+<h2>How To Use Twisted</h2>
+
+<p>Those of you used to other event-based frameworks (notably, GUIs) will recognize the familiar pattern -- you call the framework's <code>mainloop</code> function, and it calls registered event handlers. Event handlers must finish quickly, to enable the framework to call other handlers without forcing the client (be it a GUI user or a network client) to wait. Twisted uses the <code>reactor</code> module for the main interaction with the network, and the main loop function is called <code>reactor.run</code>. The following code is the basic skeleton of a Twisted application.</p>
+
+<pre class="py-listing">
+from twisted.internet import reactor
+reactor.run()
+</pre>
+
+<p>This runs the reactor. This takes no CPU on UNIX-like systems, and little CPU on Windows (some APIs must be busy-polled), runs forever and does not quit unless delivered a signal.</p>
+
+<h2>How To Use Twisted to Do Nothing</h2>
+
+<p>Our first task using Twisted is to build a server to the well-known <q>finger</q> protocol -- or rather a simpler variant of it. The first step is accepting, and hanging, connections. This example will run forever, and will allow clients to connect to port 1079. It will promptly ignore everything they have to say...</p>
+
+
+<pre class="py-listing">
+from twisted.internet import protocol, reactor
+class FingerProtocol(protocol.Protocol):
+ pass
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>The protocol class is empty -- the default network event handlers simply throw away the events. Notice that the <code>protocol</code> attribute in <code>FingerFactory</code> is the <code>FingerProtocol</code> class itself, not an instance of it. Protocol logic properly belongs in the <code>Protocol</code> subclass, and the next few slides will show it developing.</p>
+
+
+<h2>How To Use Twisted to Do Nothing (But Work Hard)</h2>
+
+<p>The previous example used the fact that the default event handlers in the protocol exist and do nothing. The following example shows how to code the event handlers explicitly to do nothing. While being no more useful than the previous version, this shows the available events.</p>
+<pre class="py-listing">
+from twisted.internet import protocol, reactor
+class FingerProtocol(protocol.Protocol):
+ def connectionMade(self):
+ pass
+ def connectionLost(self):
+ pass
+ def dataReceived(self, data):
+ pass
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+reactor.listenTCP(1079, FingerFactory())
+reactor.run()
+</pre>
+
+<p>This example is much easier to work with for the copy'n'paste style of programming...it has everything a good network application has: a low-level protocol implementation, a high-level class to handle persistent configuration data (the factory) and enough glue code to connect it to the network.</p>
+
+<h2>How To Use Twisted to Be Rude</h2>
+
+<p>The simplest event to respond to is the connection event. It is the first event a connection receives. We will use this opportunity to slam the door shut -- anyone who connects to us will be disconnected immediately.</p>
+
+<pre class="py-listing">
+class FingerProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.loseConnection()
+</pre>
+
+<p>The <code>transport</code> attribute is the protocol's link to the other side. It uses it to send data, to disconnect, to access meta-data about the connection and so on. Seperating the transport from the protocol enables easier work with other kinds of connections (unix domain sockets, SSL, files and even pre-written for strings, for testing purposes). It conforms to the <code>ITransport</code> interface, which is defined in <code>twisted.internet.interfaces</code>.</p>
+
+<h2>How To Use Twisted To Be Rude (In a Smart Way)</h2>
+
+<p>The previous version closed the connection as soon as the client connected, not even appearing as though it was a problem with the input. Since finger is a line-oriented protocol, if we read a line and then terminate the connection, the client will be forever sure it was his fault.</p>
+
+<pre class="py-listing">
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.loseConnection()
+</pre>
+
+<p>We now inherit from <code>LineReceiver</code>, and not directly from <code>Protocol</code>. <code>LineReceiver</code> allows us to respond to network data line-by-line rather than as they come from the TCP driver. We finish reading the line, and only then we disconnect the client. Important note for people used to various <code>fgets</code>, <code>fin.readline()</code> or Perl's <code>&lt;&gt;</code> operator: the line does <em>not</em> end in a newline, and so an empty line is <em>not</em> an indicator of end-of-file, but rather an indication of an empty line. End-of-file, in network context, is known as <q>closed connection</q> and is signaled by another event altogether (namely, <code>connectionLost</code>.</p>
+
+<h2>How To Use Twisted to Output Errors</h2>
+
+<p>The limit case of a useful finger server is a one with no users. This server will always reply that such a user does not exist. It can be installed while a system is upgraded or the old finger server has a security hole.</p>
+
+<pre class="py-listing">
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write("No such user\r\n")
+ self.transport.loseConnection()
+</pre>
+
+<p>Notice how we did not have to explicitly flush, or worry about the write being successful. Twisted will not close the socket until it has written all data to it, and will buffer it internally. While there are ways for interacting with the buffering mechanism (for example, when sending large amounts of data), for simple protocols this proves to be convenient.</p>
+
+<h2>How to Use Twisted to Do Useful Things</h2>
+
+<p>Note how we remarked earlier that <em>protocol logic</em> belongs in the
+protocol class. This is necessary and sufficient -- we do not want non-protocol
+logic in the protocol class. User management is clearly not part of the protocol logic, and so should not be in the protocol. This is exactly why we have the factory in the first place. The factory allows us to delegate non-protocol logic
+to a seperate class. It is often not completely trivial what does and does not belong in the factory, of course.</p>
+
+<pre class="py-listing">
+from twisted.protocols import basic
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ self.transport.write(self.factory.getUser(user)+"\r\n")
+ self.transport.loseConnection()
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def getUser(self, user):
+ return "No such user"
+</pre>
+
+<p>Notice how we did not change the observable behaviour, but we did make the factory know about which users exist and do not exist. With this kind of setup, we will not need to modify our protocol class when we change user management schemes...hopefully.</p>
+
+<h2>Using Twisted to Do Useful Things (For Real)</h2>
+
+<p>The last heading did not live up to its name -- the server kept spouting off that it did not know who we are talking about, they never lived here and could we please go away. It did, however, prepare the way for doing actually useful things which we do here.</p>
+
+<pre class="py-listing">
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs):
+ self.users = kwargs
+ def getUser(self, user):
+ return self.users.get(user, "No such user")
+reactor.listenTCP(1079, FingerFactory(moshez='Happy and well'))
+</pre>
+
+<p>This server actually has valid use cases. With such code, we could easily disseminate office/phone/real name information across an organization, if people had finger clients.</p>
+
+<h2>Using Twisted to Do Useful Things, Correctly</h2>
+
+<p>The version above works just fine. However, the interface between the protocol class and its factory is synchronous. This might be a problem. After all, <code>lineReceived</code> is an event, and should be handled quickly. If the user's status needs to be fetched by a slow process, this is impossible to achieve using the current interface. Following our method earlier, we modify this API glitch without changing anything in the outward-facing behaviour.</p>
+
+
+<pre class="py-listing">
+from twisted.internet import defer
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = defer.maybeDeferred(self.factory.getUser, user)
+ def e(_):
+ return "Internal error in server"
+ d.addErrback(e)
+ def _(m):
+ self.transport.write(m+"\r\n")
+ self.transport.loseConnection()
+ d.addCallback(_)
+</pre>
+
+<p>The value of using <code>maybeDeferred</code> is that it seamlessly
+works with the old factory too. If we would allow changing the factory,
+we could make the code a little cleaner, as the following example shows.</p>
+
+<pre class="py-listing">
+from twisted.internet import defer
+class FingerProtocol(basic.LineReceiver):
+ def lineReceived(self, user):
+ d = self.factory.getUser( user)
+ def e(_):
+ return "Internal error in server"
+ d.addErrback(e)
+ def _(m):
+ self.transport.write(m+"\r\n")
+ self.transport.loseConnection()
+ d.addCallback(_)
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, **kwargs):
+ self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+</pre>
+
+<p>Note how this example had to change the factory too. <code>defer.succeed</code> is a way to returning a deferred results which is already triggered successfully. It is useful in exactly these kinds of cases: an API had to be asynchronous to support other use-cases, but in a simple enough use-case, the result is availble immediately.</p>
+
+<p>Deferreds are abstractions of callbacks. In this instance, the deferred
+had a value immediately, so the callback was called as soon as it was
+added. We will soon show an example where it will not be available immediately.
+The errback is called if there are problems, and is equivalent to exception handling. If it returns a value, the exception is considered handled, and further callbacks will be called with its return value.</p>
+
+<h2>Using Twisted to Do The Standard Thing</h2>
+
+<p>The standard finger daemon is equivalent to running the <code>finger</code>
+command on the remote machine. Twisted can treat processes as event sources too, and enables high-level abstractions to allow us to get process output easily.</p>
+
+<pre class="py-listing">
+from twisted.internet import utils
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def getUser(self, user):
+ return utils.getProcessOutput("finger", [user])
+</pre>
+
+<p>The real value of using deferreds in Twisted is shown here in full. Because there is a standard way to abstract callbacks, especially a way that does not require sending down the call-sequence a callback, all functions in Twisted itself whose result might take a long time return a deferred. This enables us in many cases to return the value that a function returns, without caring that it is deferred at all.</p>
+
+<p>If the command exits with an error code, or sends data to stderr, the
+errback will be triggered and the user will be faced with a half-way useful
+error message. Since we did not whitewash the argument at all, it is quite
+likely that this contains a security hole. This is, of course, another
+standard feature of finger daemons...</p>
+
+<p>However, it is easy to whitewash the output. Suppose, for example, we do not want the explicit name <q>Microsoft</q> in the output, because of the risk of offending religious feelings. It is easy to change the deferred into one which is completely safe.</p>
+
+<pre class="py-listing">
+from twisted.internet import utils
+class FingerFactory(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def getUser(self, user):
+ d = utils.getProcessOutput("finger", [user])
+ def _(s):
+ return s.replace('Microsoft', 'It which cannot be named')
+ d.addCallback(_)
+ return d
+</pre>
+
+<p>The good news is that the protocol class will need to change no more,
+up until the end of the talk. That class abstracts the protocol well
+enough that we only have to modify factories when we need to support
+other user-management schemes.</p>
+
+<h2>Use The Correct Port</h2>
+
+<p>So far we used port 1097, because with UNIX low ports can only be bound by root. Certainly we do not want to run the whole finger server as root. The usual solution would be to use privilege shedding: something like <code>reactor.listenTCP</code>, followed by appropriate <code>os.setuid</code> and then <code>reactor.run</code>. This kind of code, however, brings the option of making subtle bugs in the exact place they are most harmful. Fortunately, Twisted can help us do privilege shedding in an easy, portable and safe manner.</p>
+
+<p>For that, we will not write <code>.py</code> main programs which run the application. Rather, we will write <code>.tac</code> (Twisted Application Configuration) files which contain the configuration. While Twisted supports several configuration formats, the easiest one to edit by hand, and the most popular one is...Python. A <code>.tac</code> is just a plain Python file which defines a variable named <code>application</code>. That variable should subscribe to various interfaces, and the usual way is to instantiate <code>twisted.service.Application</code>. Note that unlike many popular frameworks, in Twisted it is not recommended to <em>inherit</em> from <code>Application</code>.</p>
+
+<pre class="py-listing">
+from twisted.application import service
+application = service.Application('finger', uid=1, gid=1)
+factory = FingerFactory(moshez='Happy and well')
+internet.TCPServer(79, factory).setServiceParent(application)
+</pre>
+
+<p>This is a minimalist <code>.tac</code> file. The application class itelf is resopnsible for the uid/gid, and various services we configure as its children are responsible for specific tasks. The service tree really is a tree, by the way...</p>
+
+<h2>Running TAC Files</h2>
+
+<p>TAC files are run with <code>twistd</code> (TWISTed Daemonizer). It supports various options, but the usual testing way is:</p>
+
+<pre class="shell">
+root% twistd -noy finger.tac
+</pre>
+
+<p>With long options:</p>
+
+<pre class="shell">
+root% twistd --nodaemon --no_save --python finger.tac
+</pre>
+
+<p>Stopping <code>twistd</code> from daemonizing is convenient because then it is possible to kill it with CTRL-C. Stopping it from saving program state is good because recovering from saved states is uncommon and problematic and it leaves too many <code>-shutdown.tap</code> files around. <code>--python finger.tac</code> lets <code>twistd</code> know what type of configuration to read from which file. Other options include <code>--file .tap</code> (a pickle), <code>--xml .tax</code> (an XML configuration format) and <code>--source .tas</code> (a specialized Python-source format which is more regular, more verbose and hard to edit).</p>
+
+<h2>Integrating Several Services</h2>
+
+<p>Before we can integrate several services, we need to write another service. The service we will implement here will allow users to change their status on the finger server. We will not implement any access control. First, the protocol class:</p>
+
+<pre class="py-listing">
+class FingerSetterProtocol(basic.LineReceiver):
+ def connectionMade(self):
+ self.lines = []
+ def lineReceived(self, line):
+ self.lines.append(line)
+ def connectionLost(self, reason):
+ self.factory.setUser(self.line[0], self.line[1])
+</pre>
+
+<p>And then, the factory:</p>
+
+<pre class="py-listing">
+class FingerSetterFactory(protocol.ServerFactory):
+ protocol = FingerSetterProtocol
+ def __init__(self, fingerFactory):
+ self.fingerFactory = fingerFactory
+ def setUser(self, user, status):
+ self.fingerFactory.users[user] = status
+</pre>
+
+<p>And finally, the <code>.tac</code>:</p>
+
+<pre class="py-listing">
+ff = FingerFactory(moshez="Happy and well")
+fsf = FingerSetterFactory(ff)
+application = service.Application('finger', uid=1, gid=1)
+internet.TCPServer(79,ff).setServiceParent(application)
+internet.TCPServer(1079,fsf,interface='127.0.0.1').setServiceParent(application)
+</pre>
+
+<p>Now users can use programs like <code>telnet</code> or <code>nc</code> to change their status, or maybe even write specialized programs to set their options:</p>
+
+<pre class="py-listing">
+import socket
+s = socket.socket()
+s.connect(('localhost', 1097))
+s.send('%s\r\n%s\r\n' % (sys.argv[1], sys.argv[2]))
+</pre>
+
+<p>(Later, we will learn on how to write network clients with Twisted, which fix the bugs in this example.)</p>
+
+<p>Note how, as a naive version of access control, we bound the setter service to the local machine, not to the default interface (<code>0.0.0.0</code). Thus, only users with shell access to the machine will be able to change settings. It is possible to do more access control, such as listening on UNIX domain sockets and accessing various unportable APIs to query users. There will be no examples of such techniques in this talk, however.</p>
+
+<h2>Integrating Several Services: The Smart Way</h2>
+
+<p>The last example exposed a historical asymmetry. Because the finger setter was developed later, it poked into the finger factory in an unseemly manner. Note that now, we will only be changing factories and configuration -- the protocol classes, apparently, are perfect.</p>
+
+<pre class="py-listing">
+class FingerService(service.Service):
+ def __init__(self, **kwargs):
+ self.users = kwargs
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+ def getFingerSetterFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.setUser = FingerSetterProtocol, self.users.__setitem__
+ return f
+application = service.Application('finger', uid=1, gid=1)
+f = FingerService(moshez='Happy and well')
+ff = f.getFingerFactory()
+fsf = f.getFingerSetterFactory()
+internet.TCPServer(79,ff).setServiceParent(application)
+internet.TCPServer(1079,fsf).setServiceParent(application)
+</pre>
+
+<p>Note how it is perfectly fine to use <code>ServerFactory</code> rather than subclassing it, as long as we explicitly set the <code>protocol</code> attribute -- and anything that the protocols use. This is common in the case where the factory only glues together the protocol and the configuration, rather than actually serving as the repository for the configuration information.</p>
+
+<h2>Periodic Tasks</h2>
+
+<p>In this example, we periodicially read a global configuration file to decide which users do what. First, the code.</p>
+
+<pre class="py-listing">
+class FingerService(service.Service):
+ def __init__(self, filename):
+ self.filename = filename
+ self.update()
+ def update(self):
+ self.users = {}
+ for line in file(self.filename):
+ user, status = line[:-1].split(':', 1)
+ self.users[user] = status
+ def getUser(self, user):
+ return defer.succeed(self.users.get(user, "No such user"))
+ def getFingerFactory(self):
+ f = protocol.ServerFactory()
+ f.protocol, f.getUser = FingerProtocol, self.getUser
+ return f
+</pre>
+
+<p>The TAC file:</p>
+
+<pre class="py-listing">
+application = service.Application('finger', uid=1, gid=1)
+finger = FingerService('/etc/users')
+server = internet.TCPServer(79, f.getFingerFactory())
+periodic = internet.TimerService(30, f.update)
+finger.setServiceParent(application)
+server.setServiceParent(application)
+periodic.setServiceParent(application)
+</pre>
+
+<p>Note how the actual periodic refreshing is a feature of the configuration, not the code. This is useful in the case we want to have other timers control refreshing, or perhaps even only refresh explicitly as depending on user action (another protocol, perhaps?).</p>
+
+<h2>Writing Clients: A Finger Proxy</h2>
+
+<p>It could be the case that our finger server needs to query another finger server, perhaps because of strange network configuration or maybe we just want to mask some users. Here is an example for a finger client, and a use case as a finger proxy. Note that in this example, we do not need custom services and so we do not develop them.</p>
+
+<pre class="py-listing">
+from twisted.internet import protocol, defer, reactor
+class FingerClient(protocol.Protocol):
+ buffer = ''
+ def connectionMade(self):
+ self.transport.write(self.factory.user+'\r\n')
+ def dataReceived(self, data):
+ self.buffer += data
+ def connectionLost(self, reason):
+ self.factory.gotResult(self.buffer)
+
+class FingerClientFactory(protocol.ClientFactory):
+ protocol = FingerClient
+ def __init__(self, user):
+ self.user = user
+ self.result = defer.Deferred()
+ def gotResult(self, result):
+ self.result.callback(result)
+ def clientConnectionFailed(self, _, reason):
+ self.result.errback(reason)
+
+def query(host, port, user):
+ f = FingerClientFactory(user)
+ reactor.connectTCP(host, port, f)
+ return f.result
+
+class FingerProxyServer(protocol.ServerFactory):
+ protocol = FingerProtocol
+ def __init__(self, host, port=79):
+ self.host, self.port = host, port
+ def getUser(self, user):
+ return query(self.host, self.port, user)
+</pre>
+
+<p>With a TAC that looks like:</p>
+
+<pre class="py-listing">
+application = service.Application('finger', uid=1, gid=1)
+server = internet.TCPServer(79, FingerProxyFactory('internal.ibm.com'))
+server.setServiceParent(application)
+</pre>
+
+<h2>What I Did Not Cover</h2>
+
+<p>Twisted is large. Really large. Really really large. I could not hope to cover it all in thrice the time. What didn't I cover?</p>
+
+<ul>
+<li>Integration with GUI toolkits.</li>
+<li>Nevow, a web-framework.</li>
+<li>Twisted's internal remote call protocol, Perspective Broker.</li>
+<li>Trial, a unit testing framework optimized for testing Twisted-based
+ code.</li>
+<li>cred, the user management framework.</li>
+<li>Advanced deferred usage.</li>
+<li>Threads abstraction.</li>
+<li>Consumers/providers</li>
+</ul>
+
+<p>There is good documentation on the Twisted website, among which the tutorial which was based on an old HAIFUX talk and was, in turn, the basis for this talk, and specific HOWTOs for doing many useful and fun things.</p>
+
+<h2>Notes on Non-Blocking</h2>
+
+<p>In UNIX non-blocking has a very specific meaning -- some operations might block, others won't. Unfortunately, this meaning is almost completely useless in real life. Reading from a socket connected to a responsive server on a UNIX domain socket is blocking, while reading a megabyte string from a busy hard-drive is not. A more useful meaning for actual decisions while writing non-blocking code is <q>takes more than 0.05 seconds on my target platform</q>. With this kind of handlers, typical network usage will allow for the magical <q>one million hits a day</q> website, or a GUI application which appears to a human being as infinitely responsive. Various techniques, not limited but including threads, can be used to modify code to be responsive at those levels.</li>
+
+<h2>Conclusion</h2>
+
+<p>Twisted supports high-level abstractions for almost all levels of writing network code. Moreover, when using Twisted correctly it is possible to add more absractions, so that actual network applications do not have to fiddle with low-level protocol details. Developing network applications using Twisted and Python can lead to quick prototypes which can then be either optimized or rewritten on other platforms -- and often can just serve as-is.</p>
+
+</body></html>
diff --git a/doc/historic/FirstTenYears.Quotes b/doc/historic/FirstTenYears.Quotes
new file mode 100644
index 0000000..1c54867
--- /dev/null
+++ b/doc/historic/FirstTenYears.Quotes
@@ -0,0 +1,5816 @@
+December 5, 2000:
+Washort says, " $self._hasIntelligence()"
+Washort says, "1"
+Washort says, "*ponders setting that to 0 on certain people*"
+Maxwell says, "yes, that's our Ego-Enhancing API"
+ [this from before we had 'emote'. I added it 10 minutes later. -ed]
+%
+You say, "I wanted to discourage people from using the [old] code as examples...".
+You say, "but I don't think that bad java style is going to damage your budding programming skills :)".
+* washort nods. 'I seriously hope not.' [see, i told you I added 'emote' -ed]
+Washort says, "Oh, did i ever tell you about the Java assignment i had at the beginning of the semester?"
+Washort says, "I was bored so i did it without any loops or temp variables."
+Washort says, "so... don't tell ME about bad style. ;)"
+%
+<glyph> it's times like this when I wish I could just swallow my pride and use a standard thing like asyncore :)
+%
+<jedin> Since it's completely unsolicited, I'd just like to add that anyone who tries implementing Keynesian economics in this game will be put behind the door with the Elder Sign....
+%
+<Nafai> I love Python. It has made me look smart in this consulting job. Because of how easily I was able to do what they need me to do, they ended up doubling my pay rate. :) Woot!
+<glyph> Woot *indeed*, good sir. :)
+%
+<glyph> you know, when I say *now* I mean "in a minute" :)
+%
+Glyph: "You need to start working on Twisted Reality."
+Mike: "What makes you say that?"
+Glyph: "Because it pains me to hear you talk about how you were 'in the same bed as' someone on AIM. There is no bed. There is no spoon. There's just some gay-ass peer-to-peer shit going on."
+%
+02:26:44 AreteComp: I've decided I'm going to warn you every 5 minutes until you go to bed.
+(You have been warned by AreteComp (5%))
+02:28:48 AreteComp: Tick, tick, motherfucker.
+%
+<washort> "TONIGHT on CELEBRITY DEATHMATCH: Kenaan vs The Shrike"
+%
+<washort> yow. autoconf can be *thorough* sometimes..
+<washort> "checking for EBCDIC... no"
+<washort> i hesitate to ask what it would have done had the answer been "yes"
+%
+<glyphAtWork> the http server was so we could say "Web!" if we ever did a freshmeat announcement
+<glyphAtWork> this makes people excited
+%
+FifthKow: jello is beyond good and evil
+%
+<washort> besides, we need a way to handle the cases of characters on drugs...
+%
+<jerji> sorry glyph, but I have to take away your dork award. det is far more deserving.:)
+%
+<det> glyph: you be on tomorrow ?
+<glyph> det: what, you think I'll suddenly grow a life?
+%
+<glyph> det: if it were any more generic it would be socket.socket
+-- (responding to det's request to make twisted.web more generic)
+%
+<glyph> GenericBoy: Dude, this is *python*... objects get created when you sneeze
+%
+<tenth> I get the feeling that I could rack up some ad impressions by posting an announcement to FM about a webserver "powered entirely by love, that I made out of this bong I had".
+<tenth> Well, as long as it did something really l88t that other bong-servers didn't do, anyway.
+%
+01:35:08 AreteComp:
+Before you finish linking, you must answer the following:
+Are you a Jew?[y/N]: y
+Nice try, Yid.
+%
+<samuel> oh why do you mock me rpm
+%
+<jerji> oh no!
+%
+<tpck> http://www.twistedmatrix.com/whatisdivunal.html << makes it sound likes its done and played by millions worldwide
+<washort> tpck: that's what ad copy is for
+%
+<tpck> glyph: I thought Enterprise Class Software wasn't supposed to crash?
+<glyph> tpck: It costs extra for the kind that doesn't crash, I think
+%
+<glyph> now you're probably wondering how to run cvs
+<samuel> actually i was thinking of naked women.
+<samuel> but sure.
+%
+<\\mimic> graydon: it's when you start constructing conditional branches in sed that the men in white coats come for you
+<graydon> mimic: been there, done that. wrote a qmail crypto extension in sed this summer :)
+%
+<jerji> dude tf programming, in my experience, was just about reading the help file and hacking something until it worked.
+<jerji> not really the kind of place to employ software engineering principles. ;)
+%
+<denial> CanDoo: I would rather run a home trepaning centre than do tech support :)
+%
+<zedboy> washort "A given program in PERL is like a turd. you can see it. smell it. touch it. yuck! it's definitely a turd. it's compact. it's smelly. it's brown. a turd, thru and thru"
+<zedboy> washort "The *same* program in C/C++/Java/your favorite imperative language here is like a roll
+ of toilet paper, with the turd smeared *all over it*. you tear of one sheet. yuck! another sheet. ugh! another sheet. ewww! etc"
+ --- quoting Chet Murphy
+%
+<shapr> I get the feeling regexes in emacs are subtly different from python
+%
+<moshez> I'm not touching anything not abstracted from hardware at least two levels
+%
+<dreid> wh00t!
+<dreid> i made the quotefile!
+%
+<dreid> "lispachu, parentheses attack!"
+%
+<e@ircnet> internet
+%
+<washort> this was experimentally determined using an unholy combination of emacs, python's interactive mode, and bc
+%
+<smoke:#lisp> perhaps i should write a "Teach yourself CL in 21 days" book and hide from Peter Norvig for a few years
+%
+<GenericBoy> I'm not high!
+%
+<Mike_L> what is twisted python?
+<glyph> Mike_L: it's the python libraries your mother would use, if she were a programmer, had a lot of free time, and was very VERY patient
+%
+KaraNiSuru: Your opinion has differing degrees of importance to me. On
+programming, it's almost like law; on fashion, it's unimportant; on cuteness,
+it serves only to warn me away.
+(addressed to glyph, from his girlfriend)
+%
+<washort> glyph: you're evil, too
+<glyph> washort: I try
+<washort> not the good kind of evil
+<washort> the other kind
+%
+<Yosomono@efnet> swing is to gui programming what cupholders are to cdrom drives
+<Yosomono@efnet> something easily mistaken for the real thing
+%
+<glyph> well, I'm working on divunal now
+<washort> and what are you doing to it?
+<glyph> I'm making the clouds work again
+<glyph> the clouds were always one of my favorite bits
+<washort> bah
+<washort> typical vapourware
+%
+<glyph> washort: I learned C from reading the E sources.
+<washort> glyph: well, i learned python from reading Zope
+<washort> glyph: so i think we're about equally damaged
+%
+--> glyph (glyph@adsl-64-123-27-108.dsl.austtx.swbell.net) has joined #python
+<glyph> yay for pushing the wrong button
+<washort> when will you xchat people learn
+<washort> silly hacker, irc is for terminals
+<washort> you dont see *me* typing '/quite' by accident ;)
+<-- washort has quit (either =))
+--> washort (washort@131.204.216.12) has joined #Python
+<washort> glyph: you bastard.
+%
+<GenericBoy> I wish I had enough knowledge to start working on this damn thing
+<GenericBoy> glyph: But you had to crush my hopes. ;)
+<GenericBoy> not that that's bad though, I am grateful for giving me a better
+perspective
+<glyph> GenericBoy: crushing hopes is what I do best
+<glyph> GenericBoy: you call me "glyph", but in ancient mesopotamia they called me the "eater of souls"
+%
+<glyph> many as-yet-untranslated pre-cuneform tablets will one day be
+translated to say "beware he who will write a webserver that will deprive you
+of your very will to live!"
+<glyph> GenericBoy: although I'm not sure if they were talking about me or
+marc andreissen
+%
+<GenericBoy> I'll be the t.w guy from now on
+<glyph> yay!
+<glyph> YAY!
+<glyph> SOMEBODY ELSE IS GOING TO MAINTAIN MY SOFTWARE
+<glyph> oh god I think I'm going to cry
+<GenericBoy> ack
+<washort> GenericBoy: i think that was a mistake :)
+* GenericBoy runs
+%
+<glyph> GenericBoy: * New in 0.8.0: carmstro's soul now comes with twisted.web
+%
+<jepler> C:
+<jepler> char buf[1024]
+<jepler> strcpy(buf, user_data)
+<jepler> Python:
+<jepler> buf = user_data[:1024]
+<jepler> if len(user_data) > 1024: security_hole(user_data[1024:])
+<jepler> actually, the translation is not difficult, so long as you implement security_hole() properly.
+%
+<LynchM0b> ... do u have an easier way
+<glyph> python ;-)
+<LynchM0b> thank the lord
+<LynchM0b> java is rediculous
+%
+<yy[Z]@efnet> i can say with all confidence that my python code was the
+fastest and tightest code on the whole java project i been on for the last year
+%
+<ben3> OO is a seductive failure.
+%
+<dreid> washort: i don't want to take over the world
+<dreid> i want to marry the chick who is going to take over the world
+%
+<demoncrat> forth is much better than sanity
+%
+<snibril> Tim can go on at length on issues which are not really the core of the problem, complicating said problem for himself and everybody else.
+<snibril> glyph: sounds like you ;)
+<glyph> snibril: the difference is there is rarely actually a problem, when I'm involved :)
+%
+<glyph> So if I understand you correctly you want software that will b-2-b education portal internet enterprise mission-critical!
+<muks> yep
+<glyph> Ah. then you want Zope.
+%
+<shapr> glyph: ok, where's the tw tutorial?
+<glyph> shapr: feh, you think there is *documentation*? You just need to be at harmony with the universe, and the api calls will come to you.
+<shapr> I just got a job writing Java. harmony is nowhere close to me.
+%
+<shapr> I just *love* your Python vs Java rant :) it's GREAT
+%
+<shapr> glyph: while reading through the last part of your rant, I got this mental picture of "Glyph Lefkowitz, Python Ninja" systematically chopping limbs off the JVM
+<shapr> glyph: the problem is that "don't expect your apps to run" was cutting the head off, and for cinematic effect, it should be on the bottom
+%
+<dreid> GenericBoy: but multiple eterms tailing various logfiles are great for making it look like your actually doing something :)
+<GenericBoy> hehe
+<dreid> i'm preparing myself for when i have to work in a corporate setting
+%
+<TomG> I'm in the wrong channel.
+%
+<dreid> Yoso: i like to think that i'm a fairly sane individual for a python programmer anyway
+%
+<glyph> snibril: I think we should put *you* in the unit tests dir.
+%
+<bram> have I mentioned there's a FRIGGIN BUTTLOAD of ways web input can go bad?
+%
+<washort> GenericBoy: if we knew what we were doing, we would not call it programming
+%
+<GenericBoy> Usually relying on magic buttons from the future doesn't work
+[in reply to something Mike_L said. --ed]
+%
+<blupingu> hi glyph. i'm trying out python because of twisted python :)
+%
+ newpath = os.path.join(self.path, path)
+ # forgive me, oh lord, for I know not what I do
+ p, ext = os.path.splitext(newpath)
+%
+<Nafai> There's a twisted python philosophy tutorial?
+<washort> Nafai: yes.... read it, expand your consciousness
+<obanta> It's actually a new religion
+%
+<glyph> washort: coding angry lends whole new meaning to song lyrics :)
+%
+<GenericBoy> what's the point of all of this?
+<glyph> GenericBoy: I don't know
+%
+<Acapnotic> [ Read Past Entries ] [ Modify an entry ] [ Write new entry ] [ Have me add one for you. ]
+<Acapnotic> include: [ ] generic angst [ ] relationship trouble (or lack thereof)
+ [ ] other family trouble [ ] cynical technology rants
+<Acapnotic> also bash: [ ] slashdot [ ] users [ ] sysadmins [ ] politicians [ ] voters
+<Cysgod> [ ] Perl
+
+ -- proposed new configuration interface for the standard twisted.web weblog
+%
+<moshez@ircnet> I'm going to write a treatise "girls as open-source projects".
+<moshez@ircnet> Instead of "reaching second-base", you're "writing patches".
+<moshez@ircnet> "So, are you writing patches for you-know-who?" "Well, no, but I'm using the CVS version"
+<moshez@ircnet> Should translate to "We're only kissing, but that's as serious as it got"
+%
+<dreid> watching a beautiful girl sleep is amazingly fun
+<dreid> more fun than coding
+<washort> do you mean 'more fun than coding Enterprise Applications in java', 'more fun than coding display hacks in C', or 'more fun than coding weblogs in python'?
+%
+ # ha ha, python can do lexical closures good enough for me
+ # (Bah. if these were lexical closures you wouldn't need the
+ # 'obj=obj', and you could do 'return setdesc' and the
+ # function would still work after escaping. -was)
+ def setdesc(desc, obj=obj):
+ obj.description=desc
+%
+<_Krelin> Data hiding and encapsulation are at least in-laws, if not blood brothers
+<glyph> data hiding is encapsulation's shrewish mother-in-law
+%
+<dreid> Zope is pretty much the reason i learned PHP, (and TPy is the reason i stopped)
+%
+* Nafai doesn't think he is worthy of the quotefile
+<washort> you're in it twice
+%
+<washort@opn> "twisted python.... it's featurrific!"
+%
+<moshez> living is just syntactic sugar.
+%
+<det> glyph: what are you going to do now that UO2 is canceled ?
+<washort> det: take over the world
+<washort> det: same as before
+<det> washort: but thats what he was going to do last night
+<washort> det: glyph is a man of habit
+%
+<washort> who invented this "time zone" crap? everybody should be on IRC at once
+%
+--- washort has changed the topic to: | <-- you must be smarter than this stick to ride the internet
+%
+<Krelin> TwistedPython may, in fact, have both "enterprise" AND "internet" ;)
+%
+<thirmite@efnet> nothing like a pop tart to remind me i live in a first world country
+%
+<glyph> yosomono: in fact, I'll turn this box of Cheese Nips and
+ this 3-liter bottle of Mountain Dew into a irc2web interface
+%
+<h3x> actually i have clothes on
+<h3x> believe it or not
+%
+<\broken:#openprojects> geez that tomg bot is in here again
+<\broken:#openprojects> didn't we ban it a couple of times already
+%
+<Yosomono> uh, move zig zamboni to push grandma cats down the stairs to protect her/him from the terrible secret of space, which is that she/he can't skate?
+%
+<washort> o/` once i was the king of spain o/`
+* Acapnotic throws a humble pie at washort
+%
+<bram> the more I get into the art of design, the more I design things like I'm seven years old
+<bram> 'I don't want to do things that way because it's too hard'
+<bram> 'I wanna do it like this because I understand it'
+<bram> 'I'm ignoring that because it's scary'
+<bram> 'I don't want to work with him because he's a poopy-head'
+<bram> 'I don't want to use this because it smells like poo'
+<bram> 'this is no fun any more, I'm going home'
+%
+<cube> Greetings, O Twisted One
+%
+<Forest> Someone please tell me that this thing about P3K and Perl 6 is just a sick April Fool's joke
+<glyph> Forest: what, print>> wasn't a big enough hint?
+%
+* moshez lives to workaround design decisions made by others.
+(-- after just proposing to implement IRC over HTTP via Zope.)
+%
+ <idcmp> /msg ry a/s/l
+%
+<glyph> okay, cvs is scaring me
+<det> glyph: when I was 5, when the other children were going as ninjas and dracula, I went as CVS!
+<glyph> det: you should have gone as SCCS
+<det> glyph: you gotta be a little cute to get the candy
+%
+<cube> If you are anal, and you love to be right all the time, C++ gives you a multitude of mostly untimportant details to fret about so you can feel good about yourself for getting them "right", while missing the big picture entirely
+%
+<cube> C++ extends the machine-efficiency requirement all the way up from
+ line-by-line implementation into entity abstraction as classes, it
+ corrupts far end of the coding spectrum with "efficiency" concerns.
+%
+<glyph> that's why I love IRC
+<glyph> you can't be late for IRC
+%
+<radix> uh oh.
+<radix> 'destroy here' isn't a good idea. :)
+%
+<radix> glyph: the problem with writing a framework for text universe is
+that text adventure authors want to do the craziest shit :)
+%
+<thirmite@efnet> are you jewish?
+<moshez> yes.
+<moshez> be afraid
+%
+<thirmite> btw, e, what are the girls like in .fi?
+<e@ircnet> bipedal, warm blooded, pink skinned, about 1.5-2.0 meters tall
+%
+<dash> jeffk isn't funny, the people who think he's real are funny. :)
+<thirmite> he isn't real?!?
+%
+<cyli> it'd be so cool. i'd feel all l33t and shit
+%
+<dash> if they had neural interfaces to computers, we'd both be dead by now
+%
+<skreech> I declare myself god
+<skreech> the end
+%
+<radix> GenericBoy is no more
+<radix> I killed him, and have taken his place
+<Acapnotic> radix: whadja do with the body?
+<det> Acapnotic: killed in a metophorical sense
+<radix> that's what you think.
+<Acapnotic> What happened to the metaphorical body?
+<det> Acapnotic: the metaphorical body is decaying at the bottom of lake washington
+<radix> that's what he thinks.
+%
+ <dnm:#lisp> i'm convinced the core of loop [the Common Lisp facility] is a n-dimensional singularity and
+ that the common macro people implement is merrely the tessaract to loop's hypercube.
+%
+<radix> I'm an at least somewhat-educated dope fiend
+%
+<jedin> I figured your lasers would be a good impetus to action.
+<glyph> Don't forget about them.
+<glyph> They're hovering, just over your head... where you can't see them. Remember that.
+<jedin> Okay.
+<jedin> Hm. That could be a cool theme for a new breakfast cereal!
+%
+<moshez> glyph: I don't know anything about reality.
+%
+<Acapnotic> There are *many* differences between Texas and yogurt. Texas is drier than yogurt. Texas is larger than any amount of yogurt I've seen in one place at a time. (or ALL the yogurt I've seen at ANY time). Eating Texas would be less enjoyable than eating yogurt.
+<Acapnotic> Texas does not come in eight ounce plastic containers with tinfoil lids. To the best of my knowledge, there is no "fruit on the bottom" version of Texas. Texas is not available in the dairy section of your grocer. Texas does not help fufill your daily dietary requirement of calcium.
+%
+<eAndroid> MAKE YOUR LOGO AL GORE ON A STICK
+%
+<dash> radix: the question is, do you _really_ want to do that? :)
+<radix> no, but I want to make other people do it
+%
+<Rainy-Day> i ascended several times.. once as a tourist without wishes or material transformations
+%
+<radix> dash: Hey, what do you think a good visual aid for a talk on anarcho-capitalism would be?
+<glyph> radix: a gun.
+<glyph> radix: correction: a gun and a big pile of money :)
+%
+<mothra> Most large software projects are disasters. Nothing new there.
+<dash> most large software projects use java or C++. not a coincidence.
+%
+<dash> the program isn't debugged until the last user is dead
+%
+<moshez> glyph: I prefer to think of it as a community project...since not every interface is equal
+<moshez> some interfaces are more equal then others.
+%
+<det> glyph: why not use xml? (only because it is sort of a python standard [dont kill me])
+%
+<LiquidAngle> can you do socket programming with python ?
+<dash> boy can you _ever_
+%
+<spiv> In python, you can, but in Java you can't.
+ [ this comment had context, but it's really just axiomatic --ed]
+%
+<faassen> I'm not a python luminary, I just play one on TV. :)
+%
+<h3x> but the point is, i dont have to juggle dlopen() bullshit
+<h3x> because that gets old real fast
+%
+<glyph> shapr-werk: I can't even imagine the hell of having to write java while quitting smoking. I am behind you 100% ;)
+<shapr-werk> glyph: yah, anyone in front of me has already been mauled :-)
+%
+<Krelin> glyph: You have created a powerful solution for which there are no problems. Everyone is impressed, but duly confused.
+%
+<radix> crack! *that's* what I need!
+%
+<glyph> I like writing code that overloads operators in python
+<e> get help
+%
+<Yosomono> rasterman is the millionth monkey
+%
+<solomon> john tesh get out of my head!
+%
+<dash> i want distributed everything
+<dash> yesterday
+%
+<parks> glyph please please dont jump on the P2P XML bandwagon
+<dash> parks: satan will be buying ice skates before glyph does that
+%
+<shapr> this is where I tell you to stop hyperfocussing on bad stuff and think about something nifty like metaclasses or sex
+%
+* itamar loves changing an object's own class at run time
+<snibril> itamar: and you eat little babies, too
+%
+<TQuid> So glyph is a master of the occult as well as the obscure. :)
+%
+<glyph> "What?" "Take the red continuation." "What?" "Take the blue continuation." "Huh?" "Take the red continuation." "What?"...
+%
+* rik cheers for twisted python
+<rik> it's the easiest network coding toolkit I've come across
+<rik> as soon as you have the flash of inspiration as to how it works, you'll not look back
+%
+<dnm> ugh. linear cosmologist fever.
+%
+<e> we have powers that reach beyond the pickle
+%
+<TQuid> "No one expects the python acquisition!"
+%
+<laotse> I'm sorry. I used to be sane. Then I learned Perl and now I'm like this. ;)
+<dash> laotse: that's my excuse too
+<dash> laotse: that, and 4 years of university CS
+%
+ <h3x> i get my best programming done in the nude
+%
+<timmy> what is the recommended way to do client sockets in python
+<e2d2@ircnet> timmy: a chainsaw! AHAHAHAHA!
+* e2d2@ircnet goes back to sleep
+%
+<Acapnotic> No more doc about twist-dee, needs another page or three...
+<Acapnotic> You call this an application server? This is a slide projector and a bedsheet!
+* Acapnotic is going to have to speak to Bob about this.
+%
+<phed> dash: that's the cool part of system programming, programming half-finished programs, and tell others you're finished
+%
+<dash> if i'm going to use an obscure language with poor system integration, it might as well be lisp
+%
+<radix> I feel so special when people quote me
+%
+<spiv> dash: so we need to wrap integers... Java does that too, so it can't be that bad ;)
+* dash doesn't know how to respond to that except with physical violence
+%
+<glyph> It's just like a method call, but ON FIRE AND UPSIDE DOWN!!!
+%
+<dash> design patterns in general are just java/c++ crutches
+<dash> which isn't to say they're useless. when your language is crippled you need crutches
+%
+<laotse> Java is the tell me when I've been bad language ;)
+%
+<e> so he is writing a python interpreter in python
+<firegod> dash: is he actually that evil?
+<dash> firegod: for glyph, this is relatively non-evil.
+%
+<gary> btw, my gcc compile line is gcc -o foo foo.o includes.o -lstdcxx is there I can cut that stdcxx out? My executables are like half a meg.
+<glyph> nope
+<glyph> if you didn't want your executables to be huge and slow for no reason, you could stop using c++ :)
+%
+<glyph> the industry average per programmer/day is 10 lines
+<gary> yeah I know. its sorta sad.
+<gary> have you ever wanted to, like, be part of the backstreet boys or something? It would probably make life quite easier. Or at least different.
+ [This is what C++ does to your brain, kids. -ed]
+%
+<red_one> PORK IS NOT A VERB.
+%
+* shapr reads market speak
+<shapr> Vertical navigation through business domain trees (classification trees).
+<shapr> Horizontal navigation through multidimensional classification trees.
+<shapr> I bet moshe wrote this advertising
+%
+<snibril> glyph: others do secret sex perversities, and you join #c++
+%
+<jumpy> we are the knights who say INT! SHORT! and UNSIGGGGGGGGGGGGGGGGNNNNNNNNNNNEDDDDDDDDDDDDD LONNNNNNNNNGGGGGG!
+%
+<saint_go@efnet> Why?
+<dash> because C++ is an excellent language for doing slow and late projects in. :)
+<makk@efnet> dash: at least it's good for something. :)
+%
+KaraNiSuru: who needs a real live girl when you can get thousands of prettier girls displayed on a gorgeous 1365x768 resolution, 16.7 million color flat-panel plasma tv?
+%
+<radix> yeah, I saw that OBSOLETE_base attribute and thought to myself "Maybe glyph already tried this, and found that it sucks"
+%
+<jafo> Our fathers were our models for God. If they bailed, what does that tell you about God? You have to be prepared for the possibility that God does not like you.
+%
+<Acapnotic> ... whenever I hear anything in this channel that smacks my brain three feet into kata, chances are that glyph is the one that said it
+%
+<hunter> ... I'm execfile()'ing a file provided by j random sysadmin, so I'm pretty much holding a gun to my head.
+%
+<deeptape> I just got a vision of a version of Gaunlet that pits Pythonistas against an endless horde of C++ and Java zealouts
+<deeptape> Red Hacker needs Source, Badly
+%
+--> java (dutkiewicz@91.portland-01-02rs.or.dial-access.att.net) has joined #python
+<java> yes
+<-- java (dutkiewicz@91.portland-01-02rs.or.dial-access.att.net) has left #python
+<glyph> goodbye, java
+<glyph> hehe, that's surprisingly satisfying to say
+%
+<glyph> dash! dash! he's our man! If he can't do it, we'll make him write in ASP until he dies! bwahahhaha
+<dash> i hate you, milkman glyph
+%
+<thirmite> i have <glyph> and <dash> both subbed to the one message: <tpy> $1-
+%
+<glyph> so thirmite
+<glyph> it sounds like you have some issues
+<thirmite> duh i've been hanging around #python for around 4 years
+<thirmite> i have every issue possible
+%
+<Acapnotic> glyph: why are you being an asshole and insisting on seven bits instead of eight, anyway?
+<glyph> Acapnotic: because I gave up that bit in exchange for eternal life
+%
+<churchr> XML wasn't invented. It was excreted.
+%
+<faassen> I'm not a PSU agent.
+%
+<thirmite> i *think* i have a girlfriend
+%
+<dash> roey: i've got some code you should look at
+<dash> roey: http://twistedmatrix.com/
+<Acapnotic> dash: it's amazing how much you can make "I've got some code you
+should look at" sound like "do you have stairs in your house?"
+%
+<Mike_L> hmm ELF sounds complex =/
+* Mike_L hates file formats
+<Mike_L> i suppose I could just make my bytecode file format based on XML
+%
+<glyph> I am *not* a PSU agent.
+%
+<dash> glyph: nice people dont name functions "b1282int"
+%
+<jemfinch> I mean, if GNU wants everything to use guile, they should probably make it suck less.
+%
+<dash> i feel the power of the confusatron
+%
+<Yosomono> [Ying] is a fantastic artist, that's for certain.
+%
+<shapr> so, is the twisted crowd moving to Oz?
+<dash> shapr: no. Oz is coming to _us_
+%
+<h3x> why dosent someone write a rfc or w3 spec on server push text fields?
+<Acapnotic> look, everybody knows that "push" had it's chance, and it flunked. Pushing failed. Pushing is not the answer.
+<dash> Acapnotic: SHOVING IS THE ANSWER
+<Acapnotic> yes, shoving is the answer. We must have shoving streaming media. "I am the shover transport -- I push the newsfeed down their throats."
+%
+<mitiege> dash: where do you go to school?
+<tpck> mitiege: PSU
+<mitiege> tpck: didn't faassen go there too?
+%
+<eihrul> .NET is kinda the quickening
+%
+<shapr> we're all CODEpendant.
+%
+<robbe@ircnet> AttributeError: CMD
+<robbe@ircnet> what is here the failure?!?
+<radix> robbe: the 'cmd' object doesn't have a member named CMD
+<robbe@ircnet> radix: how i can make it?
+<radix> robbe: set us up the bomb
+%
+<M-x> sure, excessive use of the Emacs causes social problems
+<M-x> in understanding the trivial problems other people have
+<M-x> like you see them indenting a whole file of source code manually, or jumping between make output and trying to find the offending line
+%
+<dash> "COM Error: Errors occurred"
+* dash attacks ASP with a rusty hacksaw blade
+%
+<lyn:#lisp> making things fast generally seems to involve trading space for time
+<dan`b:#lisp> not so! you're thinking like a typical lisp programer
+<dan`b:#lisp> you can also trade correctness, like any self-respecting C hacker
+%
+<shapr> dash: I know Python adds sanity points to me.
+<dash> shapr: reading glyph's code does not
+%
+<tireg> i see the light!!
+<tireg> AND IT BURNS!
+<dash> tireg: welcome to python
+%
+<Acapnotic> jemfinch: What's to parse? A numeric code, perhaps a chicken, and some arguments
+%
+<Acapnotic> dash: yes, about that, do you have anything besides spam?
+<dash> Acapnotic: got spam, spam, internet, enterprise, and spam
+<dash> Acapnotic: that doesn't got _much_ spam
+%
+<itamar> if moshez ever gets into the Python RPG he'll have "different definitions of basic concepts leads to conflicts with everyone" as a disadvantage
+%
+<e@ircnet> meikan adsl:n asennus makso sentaan muistaakseni 3000 ja silta
+sedalta meni 10min :)
+<e@ircnet> oops, wrong channel
+<radix> eek
+<radix> scary words
+<dash> radix: ph34r the ph1nn1sh!
+%
+<matsaleh> glyph has been *trying* to bring me up to speed on twisted
+<matsaleh> all I know is that if he gets any smarter i'm in trouble
+<dash> matsaleh: we already are, i think
+%
+<moshez> glyph: what's Twisted Matrix Laboratories?
+<dash> moshez: the only enemy the PSU fears
+%
+<radix> scripts are just usually short programs that do a very specific thing
+<radix> that's why a lot of us people who use interpreted languages hate it when someone calls our language a "Scripting language" ;)
+<radix> (I mean, look at Twisted and call it a "script" with a straight face)
+%
+<Afterglow> glyth: what's odd is i keep getting a segfault and i don't know why
+<glyph> Afterglow: are you using C?
+<Afterglow> yes
+<glyph> Afterglow: ah. There's your problem.
+%
+<peryklez> should i learn python?
+<moshez> peryklez: no. instead, you should be an anarcho-vegeterian.
+<moshez> peryklez: here, see this channel? Do you ever see us talking about Python? No! Because Python sucks.
+<glyph> peryklez: Yes. You should learn python.
+<glyph> peryklez: Also, stay away from crack cocaine, which moshez is evidently smoking...
+%
+<radix> well, running it works well :>
+<glyph> radix: yeah, but don't ask what it does because it'll KILL YOU WITH
+ITS TEETH
+%
+<dnm_> Twisted tickles my high-level competent software design and concisely functional code that does something useful which was done poorly elsewhere in comparison bones.
+%
+<snibril> radix: i met some _professional_ (or supposed-to-be) admins that had probs even with "ldd"
+<dash> snibril: so, uh.... what did these guys _do_?
+<snibril> dash: ask stupid qs
+%
+<e@ircnet> on the internet the concepts of time and space lose meaning
+%
+<matsaleh> well, maybe we should evangelize a bit...
+<matsaleh> one thing to do would be to convince some kind of public site - techie oriented - to use twisted in some implementation
+<glyph> any ideas come to mind?
+<matsaleh> start small... google? :)
+%
+<radix> I was drinking tea before this job
+%
+<LcModerator:#live> <radix> have you heard of Twisted? Did you know that TwistedMatrix Laboratories is the only feared enemy of the PSU?
+<gvanrossum:#live> radix: I've heard of Twisted and even downloaded his code once, but I couldn't understand one bit of it. Twisted, if you're here, sorry, but that's a fact.
+<gvanrossum:#live> The PSU, of course, doesn't exist.
+<dash:#python> radix: you're a bad, bad boy
+%
+<snibril> guido, when will you stop calling python a scripting language? ;)
+%
+<gvanrossum:#live> zilch: I'm a big fan of wxPython [...]
+<radix:#python> I no longer respect that man
+%
+<gvanrossum:#live> What afro?
+%
+<dash> jenn: you DONT FEEL LIKE PROGRAMMING? what's WRONG with you??
+%
+<Erwin> #python FAQ: How do I build X? A: Wait for twisted.X.
+%
+<thirmite> i'm in the psu!
+%
+<e> most people on irc are professional and shit.
+%
+<dash> glyph: maybe that'd stop, if we stopped denying that the PSU is real and is actually coordina~~4%~~..~*'#n`+>~~.]
+<-- dash has quit
+%
+<rbm> glyph: Now I want to get more to your side of the darkness >:->
+%
+* Nafai will vouch for the fact of glyph's being the master of the obscure
+%
+<churchr> glyph: So why can't you make that into a database?
+<glyph> churchr: I will set you on fire.
+%
+<skreech> I'm gonna kinda miss code red when its gone, my webpage has never gotten this many hits before
+%
+<Acapnotic> garble. if I don't find a twisted.spread example soon, I might try to figure out what .spread is supposed to do by looking at the source directly
+<Acapnotic> which would probably be unhealthy
+<dash> Acapnotic: hey! i've been reading the source for the past month! didididididn't bother me at all!
+* dash giggles
+%
+<Acapnotic> hmm. I wonder what would happen if you fed .bash_history to megahal and then set that as your shell.
+%
+<thirmite> the pull to IRC is so much less now i have my drivers license
+<dash> thirmite: so why are you telling _us_ that? ;)
+<thirmite> dash: who else am i gonna tell? :)
+%
+<thirmite> faassen: it was on the internet
+<faassen> thirmite: don't use the internet.
+<thirmite> i love the internet
+<h3x> pike, the language of your internet
+<Jii> what's internet?
+%
+<glyph> HELP ME SMALL CHILD I HAVE ATTEMPTED TO CREATE A WEB SERVER BUT I HAVE BECOME LOST
+%
+<glyph> I *hate* thinking.
+%
+<Acapnotic> Unlike BASIC, Python doesn't have circle-drawing and paint-fill operations either.
+%
+* glyph returns
+* rik wonders what glyph returns
+<radix> rik: NOT_DONE_YET
+%
+<thirmite> dash: i don't really IRC while drunk *anymore*
+%
+<_pHI_> what is twised.words? and why did i just create an account :) ?
+%
+<churchr> I don't know why you guys want to hurt people.
+<glyph> churchr: money, usually
+<dash> glyph: wow, i can get _paid_ to hurt people?
+<dash> they didn't mention this at Career Day
+%
+<glyph> ddent, the man who was born to program in python, but doesn't
+<dash> glyph: you're thinking of his evil twin, "ndent".
+<shapr> Python as Guido and ndent did.
+%
+<shapr> I am an object!
+%
+<glyph> you know, if I'm going to develop a massive cult of personality, I need to have a better website
+%
+<Inhibitor> this is commercial software - there are no security holes
+<Inhibitor> not like your crappy open source - written by students - stuff
+<glyph> right, I had forgotten
+%
+<e@ircnet> i have been known to occasionally infact say "internet".
+%
+<Yosomono@efnet> The next version of Shapr 0.96 will have integrated Twisted support.
+%
+<Yosomono@efnet> glyph: the colors! the colors! they're burning my eyes!
+%
+* shapr goes into his a capella techno rendition of "mission impossible"
+<shapr> doodle oooo.... doodlee ooo!!
+<radix> doodlee ooo??
+<radix> I don't remember that part
+<shapr> radix: yah, that's at the beginning
+<bitPoet> radix: that's before the duh-duh-duhduh-duh-duh-duhduh part
+%
+<bram> talking about the engineering of p2p apps is like talking about the engineering of red cars
+%
+<thirmite> well the only way i could think of a girl turning me into a vegetarian is by offering me continous sexual favours, but that wouldn't work on glyph because he has some sort of dignity
+%
+<glyph> no land wars in asia or sicilian blood feuds
+<glyph> or threads
+%
+<glyph> it's easy to be dogmatic when you're right and everyone else is an idiot
+%
+<e@ircnet> thirmite: we added window manager support to bridgette.
+<thirmite> e: i hope you're drunk
+%
+<e@ircnet> error handling is important, arguing tha silent failure is ok for "production systems" does not alleviate problems when something goes wrong with "production systems" :)
+%
+<h3x> everybody is left of something
+%
+<glyph> but one person's identity could have multiple perspectives
+<e@ircnet> multiple perspective disorder
+%
+<e@ircnet> glyph: that would make twisted the most buzzword compliant application server platform known to man!
+%
+<glyph> "Fetch me my internet pants."
+%
+<moshez> What is programming, if not fighting a world of idiotic design decisions?
+<moshez> And where can you find design decisions more idiotic?
+<glyph> moshez: landscaping
+<moshez> glyph: hmmm......point.
+%
+<eAndroid> Guido has been on crack for a while.
+<eAndroid> I think he bought some cheap stuff, that's all
+%
+<dash> this feels like saving christmas from santa claus
+ [on trying to prevent Guido from making python less dynamic]
+%
+<e@ircnet> fwiw writing a sexp parser in virtually any language is easier than learning to use xml libraries for that platform.
+%
+* Blackb|rd has been spoiled by years of C and C++ and the hideous exposition to Java 1.0.2
+<radix> Blackb|rd: not "spoiled", "mentally mutilated"
+%
+<glyph> CHECKED IN
+<radix> RUN!
+<glyph> GENERATE CODE!
+* dash runs around in circles screaming, then falls over
+<glyph> INTRODUCE INSTABILITY!
+<glyph> SUDDEN EXIT!
+%
+<adu> i'm a great hacker, but i'm horrible at thinking of things to hack
+<glyph> adu: you are my new best friend
+%
+<dash> wal-mart, purveyor of fine $9.48 chinese keyboards
+%
+<itamar|nyc> think positive thoughst and then cat /dev/urandom > file
+%
+* radix would rather go see glyph than Linus :-)
+<Viiru> radix: Why?
+%
+<dash> radix: you laugh a bit too quickly for someone who's working with a
+project with a business plan based on a pokey cartoon
+%
+<moshez> If I wanted to code with syntax highlighting, I'd just take LSD. 'My, what a green comment'
+%
+<dreid> twisted can do pretty much anything if glyph gets drunk enough
+%
+<deltab> glyph: there's something strangely fitting about being able to "from internet import delay"
+%
+<TQuid> Jesus shit. Is there anything twisted doesn't do, or at least doesn't intend to do?
+<dash> tquid: XML.
+%
+<Acapnotic> What do you get out of writing docstrings if you can't confuse, mislead, and infuriate your audience?
+%
+<gt3> i thought i had mono once for an entire year, turned out it was cuz i was using Perl
+%
+<dash> det: our chief weapons are misinformation and asynchronous networking
+<det> dash: at least you can deliver it at maximum effieciency
+%
+<dash> (breaking encapsulation for fun and profit since 1998!)
+%
+<dash> bask in the rosy glow of my ignorance
+%
+<Tv> So, now there's my way, a simpler way, _and_ the correct way? I'm getting confused.
+<Tv> Back when I was a youngster, there was just my way and the correct way :)
+%
+<Yosomono@efnet> glyph: you're telling me I'm 6 months behind you?
+<Yosomono@efnet> glyph: that makes sense, considering the time lag between film releases in the US and Japan =)
+%
+<Yosomono@efnet> Twisted: Bring Out Yer Dead (Paradigms)
+%
+<Yosomono@efnet> Fuck, what's this world coming to?
+<thirmite> yosomono: obviously something less than good.
+%
+<Tv> Mwahahaa!
+<Tv> I can encode and decode arbitrary ASN.1 structures :)
+<e@ircnet> get help
+%
+<wondr> ever since they moved over to twisted google has seemed a little bit flaky
+%
+<itamar|nyc> twisted is what medusa should've been, I think
+%
+<moshez> glyph: yes, TCP connection forwarder is good.
+<illume> why not twisted.internet.tcp_forwarder then?
+<moshez> illume: because I wanted to use the word "stupid" in code.
+%
+e2d2 (~erno@2002:d432:8efa:0:0:0:0:1) joined on ircnet
+<e2d2@ircnet> internet 6!
+%
+<dash> web in my head get it out get it out
+%
+<radix> yosomono: One of these days, I'm going to actually see what you do
+<Yosomono> radix: You will turn to stone almost immediately
+%
+<mothra> i'm not sexist, women are just a pain in the ass
+%
+<eAndroid> win still has fork though right?
+<jepler> no
+<eAndroid> hmm. no wonder my daemons don't work
+%
+<jedin> Know any good informational/instructional sites on Prolog?
+<glyph> kill yourself now
+<jedin> But I just vacuumed!
+%
+<redoz> 2 years?
+<glyph> redoz: I've been working on twisted for a while
+<redoz> apparantly
+<chrchr> glyph: Most people in this channel haven't even been _alive_ for two years.
+%
+<glyph> BLOCKING OPERATIONS ARE NEVER VALID!
+<glyph> HAVE YOU SET YOUR SOCKET'S BLOCKING FLAG TO ZERO SMALL CHILD??
+%
+<shapr> I've never used a small child as a flag.
+<shapr> not even once.
+<glyph> shapr: MAKE SURE YOU BRING LOTS OF STAPLES!
+%
+<mothra> cars have the same beauty of form as women, without the nagging
+<glyph> mothra: maybe your issues with women stem from that misunderstanding
+<glyph> mothra: to start, women are *soft*, whereas cars are not
+%
+<radix> so are you coming to IPC10?
+<dash> if you, me, moshez, and glyph end up in the same room though, we may
+assemble into a giant robot and lay waste to virginia
+<dash> and that's always inconvenient
+%
+<mbac> would it be foolish of me to wish java banished to the depths of hell and in it's place is python?
+%
+<Rainy-Day> dash: i think you know what it means but for odd reasons make it
+look like you don't :P
+<radix> Rainy-Day: he does that a lot
+<radix> :>
+<Rainy-Day> yeah
+<Rainy-Day> it's annoying as hell!
+<dash> Rainy-Day: is it really?
+<Rainy-Day> yep..
+<dash> my plan has succeeded!!
+%
+<bitPoet> all of twisted is probably like 3 lines of apl
+%
+<radix> thirmite: we're not a 3rd world country. =)
+<dash> radix: not this week anyway
+%
+<glyph> let's have some more corporations
+<glyph> then we can absolve all individuals acting on their behalf of
+responsibility and collude with the government to steal money!
+<dash> glyph: YES! where do i sign up?
+<glyph> dash: www.microsoft.com, look for "passport"
+%
+<jafo> If java had real garbage-collection, it would delete most programs
+before it executed them.
+%
+<dash> (hacking implies the use of an edged tool, java isn't sharp ;)
+%
+(context: http://yellow5.com/pokey/archive/index76.html)
+[glyph] pokey's taste for the cereal reminds me of my own preference for python :-)
+[glyph] "GLYPH THEY ARE USING WOOD GLUE AS AN OBJECT MODEL!"
+[glyph] "I WANT ANOTHER INSTANCE"
+%
+<liiwi> hrmpf. python compared to to perl is like c++ compared to c
+<dash> liiwi: so, which are you implying? that C++ is a good thing, or that
+python is a bad thing?
+<dash> liiwi: either way we have to kill you, i think
+%
+<radix> It's gonna take a lot of effort ripping reality apart
+<glyph> it's going to be almost as hard to stand idly by while you do so :-)
+<radix> I kill you!
+<glyph> no, you kill my CODE :-)
+%
+<Rainy-Day> no? jesus was like, love thy neighbour and shit
+%
+<spiv> NeuroMorphus: That's not really meaningful, though.
+<NeuroMorphus> spiv: it's not a matter of meaning, it's an assignment
+%
+<radix> man, Rune better kick ass
+<radix> this demo I'm downloading is *90MB*
+<radix> games are so huge these days
+<radix> In my time a game that filled up a whole 1.4MB diskette was big!
+<Erwin> You know what else I noticed? Today's 21" monitors are bigger than
+yesterday's 14" monitors :)
+<radix> bah :)
+%
+<bitPoet> the full name of the enterprise is probably something buzzword-
+compliant like "scalable enterprise java interspacial XML warp drive", it's
+just "enterprise" for short :-)
+%
+<dnm> Someone quote me already.
+<dnm> I'm trying to plithy.
+%
+<skreech> some say there is documentation in them there hills
+%
+<Acapnotic> "Required course materials: 1 copy of 'Java and You', an installation of JBuilder+, and a HID vomit-proofing kit for each workstation you will use."
+%
+<glyph> jafo: Are you ircing as you *DRIVE*!?
+<dash> glyph: well, duh
+<dash> glyph: cant pull over every time you want to say something
+%
+<glyph> h3x: so... you're a professional extortionist?
+<h3x> pretty much
+<glyph> h3x: do you offer professional apprenticeships?
+<h3x> i should
+<dash> glyph: gah, you beat me to it
+%
+<glyph> dash: So while we're on the subject, are there features you feel the PB protocol lacks at its lowest level that you might find useful?
+<Acapnotic> (Like the "YOU FUCKED UP AN SUBCLASSED THE WRONG THING, MORON!" feature? :)
+<glyph> Acapnotic: If python had decent metaclasses, ViewPoint would scream profanity at you personally, but until that time, I'll have to do it by proxy.
+<glyph> Acapnotic: Do you have a phone in your house? ;-)
+<Acapnotic> glyph: Yeah, it's right by the stairs. Why do you ask?
+%
+<itamar> stuouid keyboard hates me
+%
+<Nanosecond> And BTW, we taliban guys use Macs. All of us.
+%
+<Gand> <sigh> ... first day as a python programer and already I have to start writing my own functions ...
+%
+<Acapnotic> My computer is playing reggae out of thin air!
+%
+<e@ircnet> pcmC0D0c pcmC0D0p pcmC0D1p pcmC0D2c pcmC0D2p
+<glyph> e: are those the lyrics to some weird finnish music?
+<e@ircnet> glyph: not yet
+%
+<dash> guess there's a fine line between "tilting at windmills" and "hitting the fan"
+%
+<thirmite> what's a web widget??
+<glyph> thirmite: internet on a stick, on fire
+<Acapnotic> with web sauce!
+%
+<thirmite> bea: how are you?
+<bea> thirmite: not bored
+<thirmite> then why are you on IRC? ;)
+%
+<Acapnotic> something's wrong, none of the tests failed
+%
+<Tenshihan> then where does modular programming come from?
+<dash> Tenshihan: the lesser magellanic cloud
+<glyph> dash: the origin of the modular programming technique is classified!
+%
+<Rugal> Do I have to study something else in order to use twisted? i mean, is
+twisted to python how C++ is to java?
+%
+[Just another night in #Python... -ed]
+* X86BSD-H throws a ball of yarn in front of rik
+* rik watches it bounce past
+* X86BSD-H needs to get a G4 PB
+* dreid beats system-wide fetchmail with a stick
+* rik looks at X86BSD-H
+%
+<radix> but a year of python programming is like 5 years of C programming
+<radix> because in those 5 years of C programming about 4 of them are dealing
+with memory management
+%
+--> bdash (mark21rowe@chch-d109.connections.net.nz) has joined #python
+<Deep6> oh no! its the lower grade dash!
+%
+<glyph> dash: let me put it another way -- I will upgrade to 2.2, if for no
+other reason than to bitch on clp.
+%
+<det> pokey reminds me of yosomono
+<det> except on drugs
+%
+<radix> it's kind of interesting to think about twisted philosophically
+<radix> it's basically a bunch of APIs layered on top of each other
+<radix> each one making a task easier to do
+<dreid> until finally trained monkeys can do it
+<dreid> "Twisted, the framework of a million monkeys with typewriters."
+%
+<radix> rep's just this happy little lisp
+<radix> and CL is the giant living on the mountain
+<radix> rarraa I'm 8MB!
+%
+<radix> man, everyone else has cool programming fathers but me
+<radix> I'm going to be a cool programming father to a kid some day
+<dash> radix: i'm going to have a lot of kids and teach them all to play quake.
+<dash> radix: we'll be the best clan in the state.
+%
+<glyph> oooh
+<glyph> OOOH
+<radix> oh shit
+<radix> glyph just had an idea
+%
+<glyph> It's interesting that people often say "Hey, I'm looking for
+something to work on!"
+<glyph> then someone else says "Glyph's code needs a little help." then
+the original asker says "SWEET MARY MOTHER OF GOD I'M NOT TOUCHING THAT!
+I mean, uh, that's too much work or I'm not good at it. Or something."
+[...]
+<tpck> You want me to read and understand and then rewrite a 795 line
+piece of code that contains doc strings like "WARNING! This source code
+for this method may cause your eyeballs to melt."
+%
+<itamar> ok, it's JAVA TIME FOR BOYS AND GIRLS
+%
+<itamar> actually, I don't have the patience for java right now
+%
+<sayke> Acapnotic: don't make it twisted-specific
+<dash> sayke: pffft
+<dash> sayke: twisted isn't specific
+%
+<tpck> merriam-webster is nothing to me
+%
+<kingkill> glyph: i was under the impression you didn't like twisted
+%
+<amien@efnet> ? swing is bad? :)
+%
+<e2d2@ircnet> don't use threads :)
+<glyph> e2d2: it's java... you don't really have an option
+<e2d2@ircnet> don't use java :)
+<glyph> e2d2: sage words
+%
+*** Signoff: glyph[Ping timeout for glyph]
+*** glyph joined channel #python
+ * Nafai wonders if glyph is really there
+<Yosomono> He's never really been "all there"
+<Acapnotic> glyph is never *really* there, but sometimes the probablitiy
+becomes high enough that he influences internet.
+<Yosomono> He's like an electron cloud.
+<Yosomono> You can't really tell where he is at a given moment, just a
+probability.
+<Yosomono> Also, you can't tell both where he is AND how much coding he's
+doing at the same time.
+<Yosomono> This is the Glyphenberg Uncertainty principle
+%
+<glyph> I probably shouldn't think of it as an accomplishment that I manage
+to cancel all of my social- and entertainment-oriented engagements on friday
+night so I can work
+%
+<flippo> When programming languages started using four-letter names, APL was
+doomed.
+%
+<datazone> glyph: you are stupid
+%
+<radix> continuations make me want to hurt you, dash
+<dash> continuations made me want to hurt a lot of people
+%
+<radix> you're a pragmatic bastard, dash :)
+%
+<mbac> strange
+<mbac> all my life i've hated object oriented programming
+<mbac> when the problem was simply that i was using C++
+%
+<MoonFallen> how well does twisted work with xml?
+<radix> MoonFallen: PUT YOUR FACE INTO THE JELLY
+%
+<radix> MoonFallen: well, there is a very simple xml-rpc implementation
+<radix> MoonFallen: but we generally don't like to talk about it
+%
+<Acapnotic> When you're holding an automatic weapon, a remarkable number of things become your choice.
+%
+<dash> if perl is a swiss army chainsaw, this is a dynamically reconfigurable
+nanosword
+%
+<radix> skreech: hey guess what!
+<skreech> what
+<radix> skreech: exciting night tonight
+<skreech> radix: women?
+<radix> skreech: twisted release! =D
+<skreech> radix: YES!!!!!!!!!!
+<radix> hee hee
+<radix> I know you live for these moments, skreech
+<skreech> VROOOM
+<skreech> Lemme get my Twisted-Release-socks
+<skreech> and noisemakers
+%
+<glyph> backinasec,Ibrokemyspacebar
+%
+Let the record show that on Saturday, November 24th 2001, at 8:38 PM UTC,
+Glyph Lefkowitz did speak thusly:
+
+ "OK. I have a crack-laden idea.
+ or perhaps a crack-destroying id
+ Deferreds are confusing as hell
+ Let's just use threads."
+%
+<radix> I HATE METAPHORS
+%
+<dash> wow. this code does something highly entertaining, but nowhere near correct
+%
+<skreech> I can feel my brain
+%
+<nikon_> i want to live in a country thats run by beautiful large breasted women
+%
+<itamar> def revenueGenerator():
+<itamar> yield cash
+%
+<mothra> one day i want my life to be so automated that getting out of bed will
+be a configuration option
+%
+<moshez_> I love portraying prejudices
+%
+<skreech> its only 10:40pm here
+<skreech> everyones going to *sleep?*
+<Intention> I am staying up! There is much to read on the web.
+%
+[in regards to http://www.askemos.org/]
+<glyph> he is *completely* insane :)
+<dash> glyph: yes
+<dash> glyph: i hope he IRCs
+<dash> this seems like a person i could hurl abuse at for hours
+%
+<e2d2@ircnet> verwilst: debugging is easier when you read the error messages :)
+%
+[dreid] i'd like to learn Forth at some point also
+[dash] save it for last
+[dash] forth has the power to destroy minds
+[dreid] sorta the snowcrash of programming languages?
+%
+<moshez_> dash: I'm a very nice man, except in hypothetical situations
+%
+<Intention> Moral: HOORAY FOR PYTHON. IT CAN GET YOU LAID.
+%
+<glyph> steve: Are you the creator of the Grease(TM) Plan for Internet Success?
+<steve> glpyh: i'm just a vessel for Grease
+%
+<Intention> Twisted did raise me from the dead after two weeks. It is a miracle
+ of software engineering.
+%
+<Kuja> Wow twisted can do all that.
+%
+<radix> Thain: I think the point is that you'd be hacking C code, not python
+<Thain> but c is easy...what's your point?
+%
+<chrchr> dsmith: Twisted is neat, but unfortunately, it's not object-oriented.
+%
+<datazone> twisted is madness
+%
+<skreech> glyph: SUDDEN INTeRNET!!
+%
+<Pahan> hunter2: RedHat is an evil distro of death! How could you not know this?
+<hunter2> Pahan: um, as a former employee and current stockholder, I probably
+didn't know due to brainwashing. :)
+<Pahan> hunter2: Oh.
+%
+<itamar> jail time and 50K fines are great marketing tools
+%
+<mcc> My justification for java's existence is "it's not quite as bad as c++"
+%
+Broadcast Message from carmstro@zaibach
+ (/dev/pts/21) at 2:24 ...
+
+who needs IRC when you can w4llx0r
+%
+* the internet
+%
+<itamar> DIE
+%
+* dash holds up his "WILL WRITE PROGRAMS THAT WRITE PROGRAMS THAT WRITE PROGRAMS FOR FOOD" sign
+* Nafai holds up his "WILL FOLLOW AROUND dash TO WRITE PROGRAMS THAT WRITE PROGRAMS" sign
+* Nafai holds up underneath a sign reading "HOPING HE MAY ACTUALLY LEARN SOMETHING"
+%
+<comatoast> hm, you could join #artois on DALnet if you're interested in making
+a version of C++ that doesn't suck
+<dash> comatoast: uh
+<dash> comatoast: i am experiencing extreme cognitive dissonance
+%
+<resolve> we are the freedom police! you must stop this happyness right now.
+%
+<jafo> I used to hang out with this chick that ran a BBS.
+<jafo> She had a great baud.
+%
+<__funky__> so where's the real python channel?
+%
+<internet> e
+%
+<glyph> Actually, they all need for Twisted. They burn for it in the very
+core of their souls, like a vampire's thirst for blood. Programmers NEED
+twisted; existance without it is a pale shadow of the righteous glory that
+the Twisted hacker can achieve.
+%
+<chrchr> I'll like anything for money.
+%
+<thirmite_> radix: you dropped out?
+<radix> thirmite: yeah.
+<thirmite_> <GenericBoy> yay i am at college;<radix> #python has made me cynical
+i hate life
+%
+<sayke> dash: i wasn't sure what to call the system daemon/service/kernel
+module/things, so i called them "gods" and made them into a pantheon. i
+then made a creation myth as a metaphor for the system boot process, which
+i combined with a programming-as-magik analogy to form a user interface
+vocabulary roughly reminiscent of, well, crowleyian wizardry.
+<dash> sayke: you are a special and unique person
+%
+<itamar> I want to kill someone
+<glyph> Why?
+<itamar> java
+%
+<moshez> "I give thy soul to the gods of the web, may they take this offering and grant us sane protocols"
+%
+<moshez> the only reason to get a life is to get a girl
+<moshez> I'm hoping to get a girl without the seemingly mandatory life thing
+<itamar> yeah? how?
+<moshez> itamar: no specific plans
+<moshez> just random hopes ;-)
+%
+<Intention> KRIS KROSS'LL MAKE YOU LONGJMP SETJMP
+%
+<glyph> You should want me dead, you'll get all my stuff.
+<cyli> I don't want you dead -- I get all your stuff anyway.
+%
+<thirmite_> if you had 100k to spend on an engine why would you make a game? :)
+<radix> thirmite: so you can make a million dollars off of it
+<thirmite_> radix: i'd still rather buy a dedicated server in the US that did
+nothing but email dash spam on c++
+%
+<dash> i remember those days.
+<dash> the world was cold and without hope....
+<dash> twisted had not been released yet.
+%
+* dash feels the idea "3d postgres-db visualisation with twisted, pyopengl, and pygame" waft through his brain
+<glyph> dash: uh-oh, you've caught the asbahr wave
+%
+<itamar> who is megahal?
+<itamar> does he do bar-mitzvahs?
+%
+<radix> I'm fighting a huge cat with breasts
+<spiv> radix: Congratulations. I think.
+<e> i have trouble imagining how you fight with breasts
+%
+<sayke> i moved left, [the cow] moved left. i moved right, [the cow] moved
+right. i yelled "WHY ARE YOU IN MY WAY? MOVE!!", and waited a second for it to
+concoct a reply. when none was forthcoming, i dropped into stance and kicked
+it in the nose.
+%
+<dash> i can think of ways to do it but they're mostly evil. what are you doing?
+%
+<thirmite> i want one of those jobs where you get people out of cults
+- pause -
+<thirmite> by blowing up cult headquarters
+%
+<glyph> I love the fact that there's apparently a text-based Tribes-2 deathmatch going on interspersed with the argument though.
+%
+<Darkvise> I guess you could say that Windows and Linux are like two different chicks. Windows gets along with most people and it knows how to party but she's been with so many guys that you dont know what virus she might be carrying, and Linux could be some nerdy chick who may not seem that attractive on the outside but she's not shallow and braindead like Windows.
+%
+<tenth> One of our prospective clients has been asking about using MSSQL for his database
+<tenth> He can use whatever he wants. MSSQL just isn't currently supported. (So if it wakes him up in the dark hours of the morning with shrill, piping calls and cries of "Yig! Yig!" and immerses him in sanity-shattering cosmic horror, which MSSQL 6.7 has been known to do, he can't call tech support about it.)
+<tenth> "Okay... What version of BusinessMind are you using? Good... okay, what database are you using? Hmm... Well, what does it say at the top of the window? Is it a red border, or a blue border, or a shimmering band of tones and shades seeming only barely within the reach of human eyes, both confusing and terrible to look upon? Colour Out Of Space? yeah, sorry, we don't support that one. You should get MySQL."
+<glyph> Warning! Kill songs unsung while still unheard [y/N]?
+<tenth> "Please enter the number of songs you wish to kill (up to the maximum displayed next to the field) and click the Yellow Sign to continue."
+<tenth> BusinessMind For Those Who Cannot Be Named
+%
+<glyph> *whew*
+<glyph> took the call and emerged testicles intact.
+* dash points out to glyph, needlessly, that he has issues
+<radix> dash: I think they're calling them "women" these days
+%
+<johs> Oh, please. Threads ownz j00.
+%
+<ThreeSeas> maybe it's be easier if I used te metaphor of the matrix characters?
+<dash> ThreeSeas: no
+%
+<chrchr> radix: A software engineer is somebody who can extend a system without reading any code.
+%
+<glyph> funny. I'm looking at twistedmatrix.com right now and the most recent
+version is still 0.13.0
+<radix> :P
+<radix> glyph: find a QOTR
+%
+<glyph> dash: Isn't "efficiency" supposed to be your department? :)
+<dash> glyph: "crack" is my department.
+ * radix gets depressed because his department is "bitch"
+%
+<radix> What the hell was I thinking?
+<dash> radix: get used to that feeling
+<dash> that feeling is called "design" ;)
+%
+<Nafai> Wait. I think I got it to work!
+<Nafai> YAY
+<Nafai> w00t!
+<Nafai> Houston, we have a contact manager!
+<glyph> Nafai: austin.
+%
+<itamar> two more webmonkey days, and then I'm off to the USA
+* shapr hands a web-banana to itamar
+<desaster@ircnet> my god, the banana is full of ads
+%
+<cheeser> i think the general method of developing address books is to write a random number generator and use that as input for any decision making.
+%
+<glyph> e: look upon my work, o kings, and despair!
+* e@ircnet viewcvs'es
+<glyph> e: aren't you going to "viewcvs and despair"? ;)
+<e@ircnet> i will despair once i see it.
+%
+<e@ircnet> when i die i want to be dried into a scary looking dried up corpse and be used to scare young children
+%
+<Acapnotic> he just logged the fact that he got r3wt0rz3d
+<radix> he's on windows
+<radix> it comes pre-rewted
+%
+<itamar> we should lock z3p up in a protocol factory
+<z3p> self.factory.stopFactory(); self.factory.letMeEscape()
+%
+<pjarks> today is a good day to install zope
+%
+<faassen> I mean, geez, the guy thinks there is a conspiracy of programmers! a conspiracy
+related to programming!!
+<faassen> who'd have ever thought of that? :)
+%
+<radix> I am just the bombest dude in the world
+%
+"Alarm Sounds Like" -- Whoop Whoop
+%
+<noa> did anyone cause the alarm to go off just to see what it sounds like?
+%
+<ZC-Matt> You you *can* take an unwrapped object and stash it in a C module, poised to leap out at any unsuspecting transaction that wanders by.
+<zigg> ooo, above my head.
+<JimFulton> mine too. ;)
+%
+--- ChanServ gives channel operator status to dash
+<dash> magc35us: you've got 30 seconds to be witty, relevant, or at least apologetic
+%
+<Intention> How EXACTLY are cameras used to keep planes from hitting skyscrapers? Do they have laser attatchments?
+%
+<draukuWORK> moshez, i signed the zope contributors agreement today... there goes my first born
+<draukuWORK> or any first born i may borrow
+%
+<dash> glyph: go to #lisp and ask about relative pathnames. :)
+<chrchr> dash: Don't make him do that.
+<dash> chrchr: he knows better
+%
+<Nafai> What to do, what to do.
+<Nafai> No class at all next week!
+* glyph gets the "documentation" hat and starts running after Nafai
+<Nafai> AHHHHHHHHHHHHHHHHHHHHHH
+* Nafai jumps on the snowboard and takes off
+%
+<resolve> hah! java is the jerry springer of computer languages
+%
+<matju> chrchr: the web will allow us to metaparadigmatically outpace
+innovation beyond the future
+<matju> chrchr: that's why it's so revolutionary
+%
+--- shapr is now known as world
+<world> hello
+%
+<glyph> so we need to target this website to three groups -- end users, corporate shills, and open source developers
+<dash> and unfortunately JavaScript is not advanced enough to determine which is which.
+%
+<exarkun> I think there's a rather large difference between a stale twinkie and a kernel swap daemon
+%
+<stranger> ok i think i need a polymorphic language with continuations and closures to write this properly
+<stranger> should I give up and implement in C?
+%
+<yosomono> When I was done with my first test gtk app using twisted, my first thought was "is that it?"
+%
+<thirmite> srbaker: www.twistedmatrix.com - a framework for building
+asynchronous network based apps
+<dreid> in other words, doing cool stuff with little work and even less
+documentation
+[*ahem*, hopefully not for long --ed]
+%
+<glyph> if you've ever dealt with MS, it's like dealing with ... well, germany.
+<glyph> it's big and not everybody agrees on everything
+%
+* skreech runs mothra over in his shrike.
+* dreid rushes to an inventory station and grabs a sniper rifle
+<resolve> hey skreech, i just went and made myself some lunch, and you're still doing that. :) i think it's time to stop
+%
+* stampy tosses a fruitcake mortar skreech's way
+<skreech> NOT FRUITCAKE
+* stampy sprays skreech with napalm eggnog
+* skreech loses control of his shrike.
+<skreech> MY EYES
+* skreech ejects
+<glyph> dash: it's a performance art version of t2, I think
+%
+* dreid hands skreech a chaingun and dash a spinfuser
+* skreech jumps and jets.
+* Novas007 picks up a mortar
+* dash tries to work out which end to hold
+<skreech> dash: raaaatatatatatatatatata
+* moshez gets a radioactive spider to bite him
+<moshez> yay! I have spider-powers
+* Novas007 flies up to the nearest high place and begins raining mortars down
+<dreid> hah
+* skreech blows up.
+* moshez uses his spider powers to help human kind.
+<skreech> Shazbot!
+<dreid> "damn lag!"
+%
+--- dash has changed the topic to: from enemy_base import flag
+%
+* skreech throws a satchel charge in the middle of the channel.
+* dreid hides behind a generator
+<skreech> lets argue.
+%
+<skreech> THE SENSOR NETWORK IS DOWN
+* skreech pilots his shrike into the side of #python
+%
+* dreid fires his spinfuser at skreech
+<skreech> BAM!! Glyph's body flys across the map after being hit by skreech's shrike going 355kph!
+* skreech avoids various heat seaking missiles launched by mothra.
+<skreech> dreid's disc hits skreech's shrike and sits it veering into a hill.
+<skreech> Nooooo!
+* skreech 's shrike flips upside down.
+<skreech> EJECT EJECT
+* skreech 's shrike explodes in a fiery ballness of flame.
+* dreid starts saturation bombing of the area where skreech's shrike crashed
+<skreech> AAAAA
+* skreech dies.
+<dreid> skreech: :)
+<dreid> gg
+%
+<Acapnotic> Ooh, I just figured out what my first twisted.reality creation will be.
+<dash> Acapnotic: oh?
+<Acapnotic> "Being Glyph Lefkowitz"
+%
+* Intention enjoys very much being able to keep programs, editors, photo editors, and games runing for a week or more at a time without fucking up or crashing or making everything else slow. God bless younicks.
+<Intention> I never had even concieved of forgetting that programs were running until unix. Now it is like.. erm..
+%
+<spiv> Apparently my company used to be a Linux company, many years ago.
+<spiv> The website consisted of Perl CGI scripts serving stock data.
+<spiv> We moved to Windows because someone couldn't figure out how to give our customers case-insenstive website logins.
+%
+<radix> glyph: so, tell us about the trip!
+<radix> did you have fun?
+<glyph> radix: It was awesome. Sin is the best thing ever!
+%
+<radix> xihr: while moshez is indeed completely insane, he's not much of an ass-talker
+%
+<fooz> oh, mozart is "write once run anywhere" like java
+<fooz> that means it probably won't work on any platform I care about
+%
+<liiwi> moshez: gotta squish radix to do 0.15.5 soon
+(oh, my god, it's spreading - Ed.)
+%
+<Aardappel> this "I hate c++" is so old
+<dash> it's as old as C++, yes
+%
+<Blue> glyph: USE TWISTED
+%
+--> moshez (~moshez@p9.j3.actcom.co.il) has joined #python
+<itamar> look, it's moses!
+--- ameoba is now known as redC
+* redC parts
+%
+<itamar> Lesson of the day: you can't test the win32 event loop if you're not running the win32 event loop
+%
+<Yosomono@efnet> radix: It looks pretty disturbing when you see a bunch of people beating the shit out of a leprechaun who has arrows sticking out of his head
+%
+<glyph> I am tasting the pepperoni-pizza-combo flavored taste of independence.
+<matsaleh> don't let it go to your head
+<glyph> well, I still have a very strong sense of "I could crash into any of these objects at any time"
+<glyph> I figure as long as I hang on to that really tightly, I'll be OK
+<matsaleh> probably a good plan
+<matsaleh> one word of advice tho
+<matsaleh> don't drive for at least 1 hr after playing any FPS
+<matsaleh> everything looks like a power up
+%
+<exarkun> twistedmatrix.com looks a lot different in netscape than it does in links
+<exarkun> I suddenly have a much higher opinion of twisted
+<exarkun> before I thought it was all garbage. now it is all garbage with a great web page
+%
+<radix> ViperCA: you can make good websites without doing stupid shit, you know. :-)
+%
+<Pahan> foot.get_owner's_gun_through_obscure_meta_tricks.shoot(self)
+%
+<TheJester> .seen god
+<xena> God seen changing nickname to God_|Away|PersecutingAtheists ~ 52 day(s) 4 hr(s) 32 min(s) 58 sec(s) ago
+%
+<itamar> we're ripe for a syndicalist-anarchist revolution ;)
+<radix> yay!
+<radix> do you guys have a lot of those?
+%
+<radix> the sysadmin of the future is going to know twisted-shelling like the back of his hand
+%
+<Donatien_Alphonse> :) no promises - the truth may be a star, but we have a
+proper motion relative to it. Oneof my favorite quoites - a wise man I knew
+once said "Honor is truth in motion."
+<glyph> Donatien_Alphonse: A wise man I once new said "I invented the hippo!"
+It's not always best to live by the words of wise men.
+<stranger> Donatien_Alphonse: i'm beginning to think wise men should keep
+their traps shut :)
+%
+<Intention> Java and Squeak are sort of similar. They are both superdynamico and have their own widgety things and run in a VM. [Squeak] has way more colors though.
+%
+<skreech> no matter what, when I come back to my #twisted window theres always 'squish' somewhere
+%
+<red_one> hm
+<red_one> is there a python that's statically typed?
+<exarkun> red_one: the south american red python is of static type
+%
+* StevenK starts to plot a drive to Belgium, but gets stuck.
+<StevenK> Damn ocean.
+* moshez starts to plot a drive to Belgium, but gets stuck.
+<moshez> Damn arabs.
+%
+<Intention> radix: Once upon a time, I truly GOT C++. This profound body of
+knowledge was so complex, it formed a separate personality in my head just to
+DEAL with the complexity without killing me. So every once in a while, when truly
+troubled, I flip to that personality. When I come back, I have no idea what
+happened. It's NIRVANA.
+%
+<moshez> dash: you should go back for completions of logic...
+* dash points at his shirt
+<dash> "AUBURN GRADUATE (PAID)"
+<dash> i've done all the learning i'm ever going to do
+%
+<moshez> Nafai: I once met a girl on a bus. She told me her name was Li. I proved to her Aleph null is less then 2 to the Aleph null. She gave me her phone #.
+<itamar> what does her name have to do with it?
+<moshez> itamar: Lie groups.
+%
+<d1ver> python programmers?! it's not even a computer language - it doesn't even support proper tail recursion!
+%
+<skreech> radix: apparantly, in stories, chinese ISPs have responded to being blocked by the rest of the world with "take block off"
+<skreech> take off every block for great justice!
+<skreech> someone set up us the packet filter
+<skreech> <zig>
+%
+<dash> BardCat: so. what's communism?
+<BardCat> dash: It's when a boy and a girl love each other, and then there is a cabbage and a baby!
+<dash> BardCat: wait
+<dash> BardCat: i thought that was syndicalism
+%
+<gt3> perlsucks?yes:wtf_yes_it_does;
+%
+<hmmm-@efnet> sitting here seeing stuff like <ry> <exarkun@opn> really makes me feel like a minion talking to his gods :P
+%
+<dash> blag
+<dash> let's finish all this 'twisted' crap so we can write some fun stuff
+%
+<skreech> WTF. The sf.net skill profile does not have a skill for "molecular biology"
+%
+<dash> moshez: you aren't making sense now
+<moshez> dash: *now*? I'm not making sense *now*?
+%
+<stranger> Hey! I've got an idea: <byte><bit value=1/><bit value=0/><bit value=0/>....</byte>
+%
+<etcha@efnet> btw whats ry>? is it a kind of irc gateway?
+<e@ircnet> it's a bit like a mind flayer, except it also relays messages.
+%
+<Pahan> Damn, I threw a horrible insult, and got no wise-ass retorts.
+<sayke> Pahan: i was just going to say "ask me about my apathy"
+<dash> sayke: he doesn't care about your apathy.
+%
+<shapr> man I had a radix quality dream
+<shapr> it was about this guy who found a dinosaur preserved in ice, and removed its stomach, and surgically altered the stomach to be able to survive in lake awter by itself
+%
+<gt3> i had a dream guido really did get hit by a bus
+<jafo> :-(
+<jafo> He's a nice guy.
+<gt3> then somehow twisted came standard with it after dash took over
+<gt3> he seems nice
+<gt3> but nice doesn't stop a bus
+%
+<skreech> How do I keep people from reading my Perl code? Oh wait. Ha ha!
+%
+<Acapnotic> I care not for your somnable teeth. I wish only to master the multipart/form-data
+%
+<Tenshihan> why do drugs make us commit so many crimes?!
+%
+<gt3> programming should be an adventure, those damn college courses make it so its like yer joining the navy seals so you can work at sea world as a whale feeder
+%
+<Jii> "internet with python" spells twisted
+%
+<Qelf> Did you doods find it hard when you 1st started?
+<dash> Qelf: well sure
+<dash> Qelf: in fact i would characterise my programming education as being in a state of near-permanent confusion
+%
+<ElectricElf> infinity: M-x font-lock-mode
+<ElectricElf> infinity: You can set it do be on by default, but that requires
+ editing a file somewhere and I can't remember which nor what to
+ add ;)
+%
+<shapr> so, where do I buy stock in glyph? ;)
+%
+* hmh looks at unmime.c in fetchmail and cries in agony
+<moshez> hmh: eh? what would a fucking MAIL DOWNLOADER be doing with mime?
+<hmh> moshez: being too fucking smart for its own good.
+<hmh> moshez: in a very dumb way, too.
+%
+<radix> I can switch screens like none other!
+<radix> look! I just switched!
+<radix> and again!
+<radix> wee!
+<itamar> wow
+<itamar> I no longer feel bored
+<itamar> compared to you, my life *scintillates*
+%
+<glyph> yo ho ho and a bottle of internet
+%
+<kosh#zope> sorry no games are worth what xp costs in terms of the freedoms removed
+%
+<Nafai> Dang. sendmail ain't working all the sudden
+<exarkun> "all of a sudden"?
+<exarkun> Nafai: where have you been for the last decade?
+%
+<dash> the primary function of the human brain is to make witty remarks on irc
+%
+<radix> i've gotta move to one of those socialist countries and become a school-bum like princepsz
+<HappyFool> please. 'professional student', not 'school-bum'
+%
+<radix> Like, imagine sitting around with your Marine buddies in your transport spaceship, going to Mars, getting rowdied up for the battle with the space aliens
+<radix> and then you get there and a thousand marines pour out of the ships and meet a horde of 10,000 imps
+<dash> radix: AND YOUR FRAMERATE GOES IN THE TOILET
+%
+<exarkun> crack attack is life
+%
+<flippo> I was reading a book about C++ templates today, then I glanced at a preview of the Python cookbook, and I thought my ears would explode from the change in pressure.
+%
+<glyph> skreech: you think "Acquireable" is hard to spell? ;-)
+[ed: dict acquireable]
+%
+<faassen> moshez: consistency's hobgoblin has a little mind!
+* dash dubs moshez "consistency's hobgoblin"
+%
+<Overfiend> bwa ha ha ha ha
+<Overfiend> 03:28AM|<moshez> what I like about Manoj is his desire for
+ simple and small solutions. like EMACS. or dvt.
+<Overfiend> 03:29AM|<Manoj> well, dvt was _supposed_ to be simple
+<Overfiend> 03:29AM|<Manoj> it only took 2 weeks to write
+<Overfiend> that's just a classic exchange
+<Overfiend> "Well, it's really quite simple if you conceive of it as a
+ partially bounded n-dimensional manifold where n is the factorial
+ of the number of ballot options"
+%
+<TQuid> Twisted blows my mind so severely I want desperately to do
+something with it, yet I don't know what.
+<TQuid> You read about it and it's like "twisted will shortly assassinate
+Bill Gates, reformulate intellectual property law to make both BSD and GNU
+fanatics happy, and also make you a nice grilled-cheese sandwich."
+%
+<hmh> moshez: I know ESR thinks he is a god of sex, and I know his signal
+ handling code says otherwise...
+%
+<II-V-I> subliminal message: python is good
+<Yosomono@efnet> subliminal retort: damn good
+<sun> subliminal antagonism: have you tried Ruby?
+%
+<spiv> Imar: Saying "php is good because it is better than C" is like saying "maiming is good because it is better than severe maiming with shrapnel and burning oil".
+%
+<radix> why do i hang out with you geeks ;)
+<dash> radix: the money, the power, the chicks
+<radix> YES
+<radix> :)
+<dash> the self-delusion
+%
+<iLLf8d> hey all how can I get more info from python exceptions?
+<gt3> play good cop/bad cop
+%
+<stranger> too much lag. going to pub.
+*** stranger is now known as stranger[pub]
+%
+<radix> how was the [censor]?
+<radix> oh.
+* radix pats his trusty Secure-o-matic.
+<glyph> radix: Terrific! [censored] was there, and so was [censored]. We built a [CENSORED] and used it to target
+<glyph> [INFORMATION QUOTA EXCEEDED]
+<radix> whoah there, buddy.
+<glyph> Erase is delete.
+<glyph> Kill is control-U (^U).
+<glyph> Interrupt is control-C (^C).
+<glyph> Ahem
+<glyph> right. So, it went well.
+%
+<schirkaan> and i thought distro wars where over a long time ago ;)
+<radix> schirkaan: are you new to IRC? :)
+%
+<wiggy> for some reason the drugs aren't working today
+*** wiggy is ~wichert@cabal.xs4all.nl (Wichert Akkerman)
+%
+You may think I'm uncooperative, but perhaps I'm just stupid.
+Bye,
+ Mike
+--
+|=| Michael Piefel
+%
+<skreech> If MS had bought Nintendo then Pikachu could be an MS Office Assistent.
+<shapr> paperclippachu, irritation attack!
+<shapr> paperclippachu, window close immunity!
+%
+<aj> Tv: it's been around for ages, but never got put in the mainline cgi's
+ (doogie saw some bright and shiny and got distracted...)
+%
+<SteveA> I want a new builtin type for Python 2.3: zenbool
+<SteveA> It is like the new bool type, but has three possible values: True, False and Mu
+%
+<aj> willy: so the question is, do i want to try my luck with another willy
+ upload? do i feel lucky? well, do i, punk?
+%
+<skreech> kill guard
+<skreech> drink potion
+<skreech> [lag]
+<skreech> ...
+<skreech> YOU MISSED GUARD HITS YOU MISSED GUARD HITS YOU MISSED GUARD HITS
+<skreech> You can't do that when you're dead.
+%
+<rc> I'm making a game called Tycoon Tycoon. It simulates competing software companies making 'Tycoon' games.
+%
+<faassen> "Hey I could speak in Slashdot messages only" An interesting
+experiment.
+%
+<glyph> So...
+<glyph> XML.
+*** Quits: dash:#twisted [washort@d136.narrowgate.net] (Read error: 113 (No route to host))
+<glyph> Wow... just _saying_ it makes him disappear
+%
+<Overfiend> Eric Raymond got frustrated because his code wasn't getting
+ merged, and it wasn't helping him out with the chicks who only
+ give blow jobs to people whose code actually makes it into the
+ kernel.
+%
+<dash> a famous evil genius is a dead evil genius
+<dash> unless you've got a robot army or something
+<dash> and mine's on back order
+%
+<itamar> you know what causes most evilness? the WEB
+%
+<spiv> My life is a sequence of blissful sleeps interspersed by bits between sleeping (most people call those bits "days").
+<spiv> I live for sleeping.
+<spiv> It's like my natural, base state of being. The Aristotlean ideal of me is me sleeping.
+%
+<itamar> [in XMLRPC] the header saying you *used* compression is as long as the banana packet
+%
+[ 23:07:38 ] <glyph> DeepTape: Are you familiar with the Time Cube?
+[ 23:08:06 ] <DeepTape> glyph: is that a comic?
+[ 23:08:13 ] <dash> DeepTape: not.... exactly
+%
+<cyli> Your minions are like the little elves, or trolls, who make shoes.
+ Except, not really shoes: internet.
+%
+<radix> there are stick men!!!
+<dash> yes
+<dash> uml has stick men
+<radix> I LOVE UML!!!!!!
+%
+<dash> get thee down, be thou funky
+%
+<thirmite> the novelty has worn off and i once again need heroin.
+%
+<thirmite> some would argue radix on crack is a different person!!
+<exarkun> thirmite: some would argue that radix _not_ on crack is a different person
+<demoncrat> some might argue radix on crack is two different people
+%
+<glyph> and the rexec'd code would run in a thread, and could use a Bastion to frob a PB reference synchronously
+<radix> here comes the crack, fellas
+%
+<adiabatic> Every day you stay awake too long God kills a kitten. Please, think of the kittens.
+%
+<o2s@ircnet> its nott the size that matters but the code
+%
+<Nafai> After I do some preliminary testing, I will soon be using Twisted towards commericial purposes
+<Aco> like what?
+* exarkun crosses his fingers and hopes for microlaser brain surgery hardware control.
+<exarkun> Twisted: The Framework That's Cutting Up Your Brain
+%
+<sayke> "your mission, sayke, should you choose to accept it, is as follows: define r(n, b[n], x, u); where r() is reality's iteration definition rule function, n is
+the number of dimensions, b[n] is the boundry size (in each dimension) of the automata, x is the number of cell states, and u is the state of the universe, last
+iteration."
+<radix> sayke: use Twisted!
+%
+* moshez kills dash and eats him
+<krz> You feel jumpy.
+%
+<ameoba> now that everything is an object, I'm afraid you'll have to return those integers until we can verify your credit.
+%
+<faassen> I created it. but I'm not *responsible* :)
+<faassen> it started lurching around by itself..
+%
+<skreech> I dont even take a lot of whats on IRC to _brain_ much less to heart.
+%
+<dash> javadoc is a cold and demanding master
+%
+* itamar looks out the window at the view and cheers up
+<itamar> nothing like a peanut factory to remind you how good life is
+[...]
+<itamar> there's a peanut factory next to the office, and that's what I see
+<itamar> great big peanut containers, towering above me
+%
+<glyph> itamar: we should set up a really nasty looking demo with emacs and java and pb all talking to each other
+* glyph ponders code-generation-based support for PB in C++
+<glyph> OK, I am guessing that dull pain behind my eyes means I should stop thinking
+%
+[re: emacs/PB, and the implementation thereof]
+<glyph> LEXICAL-LET is cool, I don't care how it works. I don't _want_ to know how it
+works. And now I have an appreciation for why I should never, ever change PB
+again :)
+%
+<aj> mstone: the raving lunatic camp rarely manages to
+ implement stuff +effectively, so they follow the people who can... </aj's
+ theory of life, the +universe and everything>
+%
+<skreech> web
+<itamar> web?
+<glyph> skreech: INTAR-web.
+<skreech> .org
+%
+<shapr> itamar: I've heard jdk1.4 is using a modified version of the mach kernel...
+<itamar> hahahahah
+<itamar> you're kidding, I hope
+<shapr> itamar: see, YOU'RE NOT SURE
+%
+<ameoba> print "\n".join(["".join([(lambda n, f=lambda c : "\033[%dm#"%c: f(n=='0' and 30 or n=='1' and 33 or n=='2' and 35 or n=='3' and 31 or n=='4' and 34 or n=='5' and 32 or n=='6' and 37))(char) for char in line]) for line in ["%06d"%x for x in [1002,31502,314233,314251,131152,314214,411531,234562,152212]]])
+<ameoba> ex : it's a diagram of a crack-attack board +)
+<ameoba> 'cuz I can't dcc shit to shapr while he's behind that firewall +)
+%
+<__del__> is there a special method a class can implement if it does its own garbage collection?
+--- __del__ is now known as gc
+* gc collects himslef
+<-- gc has kicked gc from #zope (gc)
+%
+"Much like in the world of Frisbee, new game developers and game
+development companies should never make a statement with more predictive
+power than "Watch this!" "
+- Glyph
+%
+Brian Crowder: It's both relevant and terrifying at the same time.
+Glyph: That's the best kind of relevant.
+Matt Walker: Yes, but it's the worst kind of terrifying.
+%
+<glyph> exarkun: The issue with globals is that they make resource management nearly impossible.
+<exarkun> glyph: why kind of resources?
+<glyph> exarkun: memory, disk, process time.
+<exarkun> glyph: I don't see how...
+<glyph> exarkun: Well, let's start with a hypothetical world with twenty billion obje[Out of memory error: server stopped]
+%
+* moshez decides to call himself GNU/Moshez
+%
+<skreech> (#%&@$@
+<shapr> perl? or lisp?
+%
+<noa> "let sleeping dongs lie"
+%
+<glyph> moshez: see? xml makes people happy.
+%
+<resolve> we live in a world where some people get their jollies having sex
+ with dead people - i don't think the notion of windows supporters
+ is entirely inconceivable
+%
+<ameoba> isn't a latvia part of the female genitalia?
+%
+<glyph> blargchoo
+%
+<Yosomono@efnet> premature optimization is like that other "premature" thing, messy and embarrassing
+%
+<SteveA> I just had a very odd phone call
+<SteveA> from a researcher with the french TV station "TF1"
+<SteveA> asking about inflatable football referees
+%
+<exarkun> english am dumb
+%
+<sjj> i believe my monitor just blanked out
+<sjj> i hope i'm in the IRC window ;)
+<skreech> sjj: no use telling you 'yes'
+%
+<dash> careful with that syntax, eugene
+%
+<VladDrac> does it [Twisted - ed] make my penis grow?
+<shapr> if so, you better be careful how many people run Twisted all at once.
+<shapr> you could die of blood loss.
+%
+<shapr> in reality, it just means I can throw down some Zope stuff and then play more crack attack rather than wrestling with J2EE for months.
+<shapr> ya know, no one on #java plays crack-attack
+<shapr> I think there's a not so hidden truth there.
+%
+<EWSJames> we can be knights in shining armor if we want, but peasants who make up 99.9% percent of the people just see us as asses who wear shiny shit and talk funny
+%
+* TuxedoKamen wonders why everyone always assumes he's on linux
+<Erwin> benefit of the doubt :)
+%
+<allexpro> dash: put me in a tent and give it to moshez!
+%
+<glyph> dash: we need to come up with a "basic rules of discourse" webpage
+<exarkun> glyph: why
+<glyph> exarkun: because if one more person makes a completely unfounded assertion in front of me I AM GOING TO EXPLODE THIS BACKPACK-SIZED NUCLEAR DEVICE
+<dash> glyph: I invented the hippo!@
+%
+<dash> we've all got stupid ideas in our past
+<dash> thanks to the power of the internet, the shame associated with them need never dim!
+%
+<sjj> itamar: if you use the word 'embedded' a lot, you sound smart.
+%
+<liiwi> ah, coldness, the lovely coldness. And the ever-protecting darkness.
+%
+From the /topic on #web:
+The First Rule of Web Development is, "We Don't Talk About Netscape 4.x"
+%
+<spiv> I'm not entirely happy with it, but it works. Well, actually it doesn't. But until 5 minutes ago I thought it did :)
+ [regarding the god-cursed FTP support in Twisted -ed]
+%
+<dash> your RDF is massive and unstoppable. [to glyph -ed]
+%
+<skreech> ooooh shit
+<skreech> I have moderator points!
+<skreech> RAAAAAMPAAAAAAAGE!!$*^
+%
+<tenth> "And then you run this Z80 assembly on the resulting bytecode in the emulator of your choice to create your makefile."
+<tenth> "The inital register settings of the real or simulated Z80 are left as an exercise for the reader."
+ [the nebula build process is just not fun. -ed]
+%
+<dash> i find it interesting that your roadmap showed twisted improving most while you're in jail.
+[in reference to http://twistedmatrix.com/pipermail/twisted-python/2001-April/000037.html -ed]
+%
+<allexpro> discovering twisted is probably the best thing that has happened in my life
+%
+<sjj> dash: i'm fine with you dealing drugs, just keep them away from radix
+<dash> sjj: look, if i dont keep radix stocked, we get no releases.
+%
+<radix> /msg exarkun [lilo] HI ALL GIMME MONEYS AND LOOK AT MY WEBBARSITE
+%
+* radix harnesses the power of fudgepops for good, rather than evil
+%
+<dreid> radix: any system that relies so heavily on a human concept like trust is inherently flawed ...
+<dreid> radix: i just use gpg to encrypt my porn
+%
+<Bergenlund> how do I add twisted to autoexec.bat?
+%
+* skreech squints really hard and tries to change his neuron patterns.
+%
+<radix> MY TAPEWORM TELLS ME WHAT TO DO
+<radix> s/MY TAPEWORM/MOSHEZ/
+%
+<dash> .rhosts auth is effectively "root one get one free"
+%
+<dash> the os module is why python doesn't suck
+<glyph> dash: concrete is what makes skyscrapers not suck
+<glyph> dash: doesn't mean I want to go swimming in it
+%
+<dash> is there some connection between German and disgusting modifications to C?
+ [c.f. The Nebula Device, CLISP -ed]
+%
+<dreid> earth# apt-get install good-will-towards-man
+<dreid> Reading Package Lists... Done
+<dreid> Building Dependency Tree... Done
+<dreid> Sorry but the following packages have unmet dependencies:
+<dreid> good-will-towards-man: Depends: peace-on-earth but it is not going to be installed
+<_moshez> dreid: file a bug against good-will-towards-men
+<_moshez> dreid: unless it is in contrib?
+<Nafai> non-free, perhaps
+%
+<dreid> <xpp>
+<dreid> <xout>Hello World!</xout>
+<dreid> </xpp>
+<bruce> dreid: just fucking learn Common Lisp. :)
+%
+<exarkun> let the unwashed masses write their C
+<exarkun> you will reap the benefits of their pain and toil
+%
+<glyph> jemfinch: Are you really a captain of a spaceship from the mirror earth, on the other side of the sun?
+<jemfinch> I don't quite catch your meaning :)
+<glyph> jemfinch: Aah. Wink wink, know what you mean, say no more, say no more.
+%
+<dash> "mwahahahahahahahahahaha"
+<Nafai> you are the christina aguilera of evil
+%
+<glyph> > flirt with cyli
+<glyph> You flirt with cyli. [moshez is here, flirting with cyli]
+<glyph> > wink at cyli
+<glyph> You wink flirtatiously at cyli.
+<glyph> Glyph enters the room.
+<glyph> # glare moshez
+<glyph> Glyph glares at you!
+<glyph> # kill moshez with sword of infinite slaying
+<glyph> Glyph hits! Glyph hits! glyph hits! -more-
+%
+<bruce> i wish i was only doing an imitation of a dumb user rather than really being one. :)
+%
+<sjj> you can't be a satinist without god either
+<spiv> You can be a satanist without pants though. The world is an amazing place.
+%
+* glyph finally places the order to get his carpets cleaned
+<shapr> is carpet cleaning thread safe?
+%
+<tenth> "As a developer, I'm often discouraged by the amount of time and effort it takes to gouge out my own eyes in pain and frustration. Thanks to Gouge#.net, this distasteful task can be peformed quickly and easily by a trained professional*. Thank you, .net. Jesus, my eyes.
+ (* Professionally designed GougeWizard(TM) with your choice of animated agent character)
+%
+<resolve> i miss the days of programming computers in machine code. all this new-fangled source code is a waste of time.
+<itamar> machine code? hah
+<itamar> in my day we ran programs in our *head*
+<moshez> itamar: you had a *head*? pah
+%
+<glyph> who could forget binky?
+<radix> glyph: well, anyone who naturally blocks out haunting things so they don't have nightmares
+<glyph> radix: Kenaan disapproves.
+%
+<hornby> Slavery doesn't seem so bad.
+%
+<getchomsky> "so, mister nooning, did you know you are associating with a man named glyph, a man authorities consider to be the most dangerous jewish man alive?"
+<getchomsky> "his mastery of open source programing makes him a threat to every man and woman alive on this planet. he must be stopped. Forget everything you think you know about him, and about this "twisted" of his"
+%
+<exarkun> twisted.web has used 1 CPU second of time in the week I've had it running.
+%
+<psy> How do I stop a factory?
+<Aco> psy: syndicate strike
+%
+<exarkun> exceptions in C++ are a _huge_ mistake.
+<radix> s/exceptions in/
+%
+<exarkun> glyph: do _you_ know about super()?
+<exarkun> glyph: As far as I can tell, it's a plot, one that would be likely perpetrated by an organisation not unlike the PSU (if the PSU existed, of course), to kidnap our firstborn and empty our jars of cookies.
+<Nafai> My cookies!?
+%
+<glyph> You know, I don't think I've reached a point in my life where I said "I don't have enough emotional trauma", irc-related or otherwise.
+<dash> glyph: cool. let's go troll #c++.
+%
+<sjj> i've heard there is a /quit command.
+<ameoba> sjj : "/quit" : absurd liberal myth
+<sjj> figured.
+%
+<_moshez> itamar: so, the security people ask them what they do, and they say they are mathematicians
+<_moshez> itamar: and to prove it, they show papers with their name on it.
+<_moshez> itmaar: and then the security guys ask them to explain what the papers are about!
+<_moshez> itamar: apparently, one hasn't lived until he heard a mathematician explain to a security guy what equivariant cobordisms between symplectic manifolds are
+%
+<ameoba> c++ is 700 times faster than Python
+<princepsd> ameoba: based on? ;))
+<ameoba> princeps: something somebody said on usenet +)
+%
+<itamar> take money from elderly and weak with knife
+[itamar writes test cases for the Twisted Reality parser]
+%
+<matiu> (I have to write help files) :(
+<ameoba> matiu : you could do it with twisted.
+<matiu> ameoba: So you're saying twisted has a "help file writer" somewhere deep down?
+<dash> matiu: yes and his name is bruce
+%
+<glyph> dash: uh... what is the correct answer to the question "The short common lisp site name"?
+<dash> glyph: "it buuuuuuurns"
+<skreech> my eyes, the googles do nothing!
+%
+<skreech> Why do I feel the sudden urge to buy a nice quality florescent desk lamp?
+%
+<skreech> the woot, the woot, the woot is on fire.
+%
+<skreech> itamar: heres your nickel back.
+%
+<datazone> okay, tell me if i am crazy
+<Yosomono> you are
+<datazone> damn
+%
+<bruce> and i like doing what i enjoy in my spare time. :)
+<bruce> which, although you all might think so, isn't harassing you all to do more work.
+<bruce> although you all do need to do more work.
+%
+<snibril> JRuby? hmmm, only java ppl have to reimplement ever other lang to replace theirs ;)
+<radix> scheme people, too
+<cleverdra> Scheme people don't do that!
+<radix> how many object systems have YOU written today?
+<cleverdra> radix - today? 12, but one of them wasn't really.
+%
+<radix> excuse me for visiting my DEAR OLD BABUSHKA on her EIGHTY-SIXTH BIRTHDAY when I should be WORKING ON TWISTED
+%
+<radix> every time you make a terrible joke, a baby rabbit dies
+%
+<itamar> "We put the 's' in 'drwxr-sr-x'!"
+%
+<bruce> i've apparently gotten someone at work to clean up their act.
+<glyph> bruce: clean up their act how?
+<glyph> bruce: were they like a pedophile heroin addict or were they just checking in buggy code?
+<dash> glyph: like there's a difference
+%
+<dash> It's moshez. Remember the briefing.
+%
+<dash> glyph: so, i am trying to jump off the side of the NSF headquarters without losing my legs
+<fzZzy> reminds me of college
+%
+<itamar> IN THE INTERNET AGE YOU WILL BE ABLE TO CHAT WITH YOUR TOASTER
+%
+<itamar> I liked the "2 years C# and .NET experience" job
+<gt3> i guess they're hiring dogs
+%
+<fzZzy> how do you quit a twisted telnet session?
+<allexpro> ctrl + ]?
+<fzZzy> there's no cleaner way?
+<exarkun> calling close() on your connection's socket is pretty clean.
+<exarkun> I suppose you could call in a tactical nuclear strike on the remote host
+<fzZzy> considering the remote host is my computer right here, that would take care of everything for me
+%
+<glyph> spiv: is bugzilla bad?
+<spiv> glyph: It's... large. And perl. Join the dots.
+%
+<itamar> write a kqueue reactor
+<itamar> all the FreeBSD people will then go nuts
+<bruce> FeerBSD people are already nuts
+%
+<radix> don't let's all go break a million tests, eh?
+%
+<dash> adiabatic: citizen, you have committed an error
+%
+<bruce> i'm feeling motivated
+<glyph> bruce: yaay!
+<glyph> bruce: what flavour of motivation?
+<bruce> beating you up
+%
+<moshez> it's the holy trinity, dash, radix & glyph
+%
+<bruce> what's #ypn ?
+<glyph> bruce: the fifth circle of hell
+<moshez> bruce: young programmers' network
+<moshez> glyph: potato potahto
+%
+<bruce> allexpro is a view of the future of humanity as a group consciousness.
+%
+<exarkun> njjeeeee
+<itamar> njjeeee?
+<exarkun> ancient aramethaic warcry
+<exarkun> infamous for its ability to strike confusion into the hearts of enemies of aramathia
+%
+<exarkun> radix: lisp freak
+<exarkun> radix: go suck on a car
+%
+<ameoba> it's frightening to remember that twisted is an overgrown MUD
+%
+<fariseo> i am completely lost, all i do understand is an OS with a database backend and a scripting language, but i am missing the whole xml/.net/j2ee/twisted...
+<glyph> I'm both honored and appalled that Twisted shows up in that list :)
+%
+<exarkun> Pop up a Tkinter dialog saying "There's some information waiting for you" and do a beep every time the ethernet IRQ goes high
+%
+<hornby> 1. Create laws that promote a fair, just society.
+<exarkun> 2. ????
+<exarkun> 3. PROFIT
+%
+<moshez> skreech: I claimed the typical anti anarchist attack goes something like:
+<moshez> "say someone cracks into your computer, downloads all your porn, burns it to a CD and throws it at you?"
+<moshez> anarcho-communist: nothing. the community would reprimand him.
+<moshez> anarcho-capitalist: my private security forces would shoot him before the CD left his fingers
+<moshez> attacker: "SEE! under anarchism you'd have people throwing porn CDs at people, and people either ignoring them and shooting them!"
+%
+<rmt> Every python program needs to have direct access to a mouse over ssh!
+%
+<strib> Sorry, I'm just in the middle of a paradigm warp right now.
+<dash> strib: welcome to twisted
+%
+<exarkun> entirely not your fault, I'd say. the current behavior is somewhat broken
+<exarkun> luckily I documented it as being broken so it's not my fault either.
+%
+<kriptik> wow twisted is neat
+<kriptik> *bleeds from the eyes*
+%
+<allexpro> i said 'hello'
+<allexpro> and when i 'cat test.au > /dev/dsp'... it sounded like a tiger roar
+%
+* moshez sings the radix song
+<moshez> "for he's a squishy good radix"
+<moshez> "for he's a squishy good radix"
+<moshez> "for he's a squishy good raaaaaaadix"
+<moshez> "and nobody can deny"
+%
+<allexpro> and how do observer patterns work?
+<dash> the PSU watches your data and notifies the authorities when it becomes suspicious.
+%
+<comajelly> hrm, I wanted to snipe this guy, but he got ran over.
+<fzZzy> heh
+<fzZzy> I hate it when that happens
+%
+<itamar> night all
+<-- itamar has quit ("Client Exiting")
+<radix> me too
+<radix> heh, it's weird going to sleep at the same time as itamar
+%
+<glyph> radix: I think that the twisted vs. asyncore table should begin with this quote, though: "Our conviction is like an arrow already in flight. Your life will only last until it reaches you."
+%
+<Erwin> I recompiled XFree 4.2 with gcc 3.2-beta-from-cvs with -O42 and -march-pentium4-800Mhz and I am sure that the MOUSE CURSOR is moving 5 % FASTER!
+%
+<bruce> I CONTROL YOUR WEBSITE!@$$
+%
+--> IAmNotAPickle (slt5v@12-255-1-203.client.attbi.com) has joined #twisted
+<radix> PICKLE
+<-- IAmNotAPickle (slt5v@12-255-1-203.client.attbi.com) has left #twisted
+<radix> :(
+<exarkun> you scared it
+%
+<exarkun> heh
+<exarkun> I was at home depot the other day
+<exarkun> and they had a big rack of free AOL CDs
+<exarkun> So I took about 40 and stuck them under people's windshield wipers in the parking lot
+%
+> Linux is complicated, becasue you compile.
+Corollary: Windows is simple because no compiler comes with the system...
+[Seen on linux-il]
+%
+<sjj> dash: soon there'll be another level of college labelled "unlearn university crap"
+%
+<wzZy> arg. these infinite recursion tracebacks take forever to render in the browser
+%
+<exarkun> I'm just kidding. I'll spend on good computer books, but I failed to do any prepatory research in order to know whether any of the books there were worth anything.
+<exarkun> And I'd just spent $80 at the camping store.
+<adiabatic> wadja get?
+<exarkun> some kerosene and a cooler and a coupla chairs
+<dash> nothing like a good old fashioned book burning
+%
+<radix> you have the pokey gene
+<zigg> ack, where'd I get it from :-P
+<radix> it's random
+<zigg> triple ultra-recessive
+%
+<radix> krz: we know that glyph owns all of our souls equally
+%
+<moshez> glyph: teehee. good always loses
+%
+<sjj> if you package twisted with python, it becomes py2ee
+%
+<sjj> when does something denote enterprise? :P
+<deltab> when it's the most expensive version in its line
+<inapt> when it's terribly inefficient, but scales ;-)
+<deltab> alternatively: database
+%
+<glyph> The world made more sense when I thought software was a physical thing you sold in stores, and I wrote code in C++; making software was a lot more like mixing cement, then, not poetry or revolution.
+%
+<skreech> I'm sorry. I forgot that in #twisted, all suggestions are taken seriously.
+%
+<sjj> what do you do for your clients? :)
+<sjj> "distributed enterprise networking technology solutions" ?
+<bruce> we put DENTS in your budget.
+%
+<Jerub> All these things that would be next to impossible with php, that I can think to do in twisted.web
+%
+<timmy> so it's basically a lot of libs for doing stuff?
+[timmy becomes enlightened to the Twisted Way -ed]
+%
+<Joey> I sense disturbance in the security buildd structure.
+%
+<wzZzy> AQUAMAN VS THE GERMANS IS THE BEST MOVIE EVER MADE
+%
+<eevench> is LISP good
+<exarkun> The short answer is yes and no.
+<exarkun> You don't want the long answer.
+%
+<radix> who has the power to wield the almighty +t?
+<radix> Me!
+<exarkun> radix: I don't think you can handle the +t
+%
+<willy> you can tune a fs but you can't call a string
+%
+<Jerub> What l33t skilzz do I have to pick up to get a python job? Zope? Twisted?
+<dash> Jerub: the power to cloud the minds of men
+<Jerub> dash: I'm afraid I only have a Wand of Clouding vs. Women
+<glyph> Jerub: oh, that's easy
+<glyph> use the wand on a woman
+<glyph> women have the clouding-men's-minds intrinsic
+<glyph> so you can either make her your pet and then wander around
+near some businessmen for a while
+<glyph> or eat her corpse and get the intrinsic yourself
+<glyph> no wait, that's not how it works...
+<dash> glyph: wrong game
+ * dash twitches violently as he thinks of "nethack, enterprise edition"
+%
+<spiv> AaronSw: You should never, ever be creating a transport... Twisted is supposed to do that for you.
+<AaronSw> I should never create the tcp.Client stuff or I shouldn't manually set them as the transport?
+<spiv> AaronSw: Use reactor.clientTCP (or better yet, reactor.connectTCP in CVS).
+<spiv> Don't create tcp.Client directly either.
+<spiv> http://twistedmatrix.com/documents/TwistedDocs/Twisted-0.19.0/twisted/internet/interfaces_IReactorTCP.py.html
+<spiv> AaronSw: But of course, that API is deprecated in 0.99 (but creating a tcp.Client directly is even more deprecated :P)
+<AaronSw> Are you guys abstraction astronauts or something? ;-)
+%
+<DeepTape> Oh no, taxes! They are trying to steal your arctic circle income
+%
+<pkomarek> dash: the worst part about perl is that it is intuitive, right up until you need something to work correctly.
+%
+<dash> finally! an essential representation of the confusion.
+ [ed: referring to a diagram of twisted.cred]
+%
+<Acapnotic> Good afternoon, Agent.
+<glyph> Acapnotic: "agent"? You've been immersing yourself in the One True Game, I take it.
+<Acapnotic> What's going on out there is no game. Those guys are using real bullets.
+<Acapnotic> That last mission? I got sloppy at the end of it. Real sloppy. Barely had a leg to stand on when I got on that chopper.
+%
+<glyph> Hello .au
+<Jerub> hello .us
+<Jerub> or, alteratively,
+<Jerub> hello None
+%
+<moshez> doogie: Clint wants you to lap-dance.
+<doogie> I charge more than the normal $20
+<Clint> with or without the hat?
+<doogie> that'd be the only thing I'd wear
+%
+[About a tm.com redesign]
+<evol> But what kinda design is the goal here
+<itamar> not ugly?
+<moshez> evol: a good one.
+<dash> evol: "non sucky"
+%
+<sjj> glyph: ahh too bad, if you have a windows box it lets you use windows media player...
+<dash> sjj: he's a terrorist PPC user not a patriotic x86 user
+%
+<JerubBaal> why can religious fanatics and nigerians not figure out capslock?
+<glyph> JerubBaal: IF IS SPEAK LIKE A TELEGRAM YOU WILL LISTEN TO ME STOP IF I USE NORMAL ENGLISH YOU MAY FIND IT BLAND AND NOT READ IT ALL STOP
+<JerubBaal> Sorry, I lost interest after you shouted 'telegram'
+%
+<dash> twisted doesn't currently have any trouble with 2.2, right?
+<radix> nope
+<dash> good good
+* dash prepares to make trouble
+%
+<dash> looks like we have people who just totally fall off our radar because they're totally happy with twisted and dont _need_ to say anything =)
+%
+<spiv> glyph: You have the deepest insight into XML of anyone I know ;)
+%
+<mesozoic> I tried adding one in coil, and got more errors. I'm not sure if I'm going about it properly. Is there any other way to configure a vhost?
+<ameoba> call the vhost-bustters
+%
+<Jerub> I got in trouble for drawing a smiley face on a gantt chart.
+%
+* moshez does the evil lowering squishation resistance level dance.
+%
+<glyph> CDATA is not an integration strategy.
+%
+<nessus> The PSU? Is that that thing that I used to send $50 a year to?
+%
+<radix> "Don't expect romantic attachments to be strictly logical or rational!" [from a fortune cookie -ed]
+<deltab> do expect them to be in DOC format
+<deltab> "May all your romantic attachments be in an unreadable file format"
+%
+<sjj> skreech: you go to all classes?
+<skreech> sjj: yes.
+<sjj> skreech: why? :)
+<sjj> skreech: uni was made to be skipped
+<sjj> man, americans must be dedicated students.
+%
+* moshez doubts they realize Linux has *WAY* more brand-awareness than SCO, and possibly equal to "UNIX"
+<dash> where SCO is recognised
+<dash> it is recognised as suffering
+%
+--- ivan is now known as grub
+<grub> please donate to this IRC server I need lots of money i don't know how i can stay online.
+--- grub is now known as ivan
+%
+<glyph> I've started to think that having a lot of stable, robust stuff and a
+ lot of half-finished proof-of-concept stuff in one project is a good
+ business model
+<glyph> like "You know we can do good work, but we got bored with that bit; if
+ you want us to finish it, pay"
+<dash> glyph: good, because that's what we have
+%
+<StevenK> steven@broken:~$ ssh squished
+<StevenK> steven@squished's password:
+<StevenK> Linux squished 2.4.18-686 #1 Sun Apr 14 11:32:47 EST 2002 i686
+%
+<bruce> how are the jails in israel?
+<itamar> well, the one I was in was pretty nice
+%
+<dash> glyph: how many PSU agents did you have to kill to get that working?
+<glyph> dash: 3, and they were all waiting just inside the door. Amateurs.
+%
+<glyph> and it's considered a professional courtesy, when you are *invited*
+ into a bank, not to steal all their moneys and shoot the managers full
+ of assault rifle bullets
+%
+<moshez> itamar: you're AT WORK?
+<itamar> moshez: I am not an employee
+%
+<tenth> Doing stuff in MySQL is like getting dates at [name elided to
+ protect the guilty -ed] College... "How ugly do you want it?"
+%
+<tenth> I think we need a god verb "0wnz0r" on the
+ reality-pencil-type-thing. I'm not sure exactly what it would do, but I
+ think it may be necessary.
+%
+<dash> Saying that complexity isn't real because it "was invented somewhere else" is the most useless kind of wishful thinking
+%
+<xcabbage> mind.sf.net crashed my browser
+<dash> signs of intelligent life!
+%
+<glyph> The only thing more absurd than the technology of XML is the politics surrounding it.
+%
+<glyph> While it is *possible* that I'm smarter than you think I am, it is certain that I'm more stubborn.
+%
+<z3p> glyph: what group of programmers are you picking on tonight?
+<glyph> z3p: PyXML again
+<z3p> sounds like a blast :)
+<glyph> z3p: ugh. Actually I have a pretty high opinion of some of those people so it bugs me to have to be flaming :)
+<dash> glyph: bah, just lower your opinion of them
+<dash> no need to consider their past character, if they're wrong, they're scum@#!
+* dash twitches
+%
+<glyph> _moshez: debian really needs to make start-stop-daemon do something
+ cute, like put icons across the top of the fbdev
+%
+<glyph> we need PB for C#
+* moshez squishes glyph
+<moshez> glyph: squishy insane person
+%
+<dash> moshez: we dont have the right kind of soil to not grow wheat in
+%
+<exarkun> I try to limit myself to one major screw up a week
+%
+<tenth> in OSX, they deprecate things with hammers and nailguns
+%
+<Lan_Rover> it is my official decree that it is easier to config and run twisted as a web server than to install apache2 and mod_python
+%
+<liiwi> http://slashdot.org/articles/02/09/12/160255.shtml?tid=99
+ [the topic is "squishy Digital Rights Management" -ed]
+* dash looks at slashdot
+* dash looks at moshez
+<dash> moshez: just _what_ have you been up to lately????
+%
+<itamar> thank god I'm not religious
+%
+<dash> Hi. Allow me to express my opinion of Word now that i've gotten to know it a little better.
+<dash> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaa.
+%
+<glyph> Stravad: whereas eval is like slitting your own throat before going
+ out for a walk so as to make the mugger's job easier
+%
+<glyph> radix: keep your eyes on the bot! we move fast.
+%
+<infinity> moshez : I won't denounce PHP... It still has its uses.. <shrug>...
+ I denounced most PHP *users* about a week after adopting it, though.
+<infinity> That language attracts more idiots...
+[Ed: infinity is the Debian PHP maintainer]
+%
+<Aco> radix: ever heard for this russian group tato/taty? pop
+<radix> Aco: nope
+<Aco> radix: well, girls were like 16 when they started. they sing in
+ther white underpants, and they wet them with water.. so you can see
+under. very interesting. want some pictures? :)
+<dash> Aco: please do not corrupt our release manager
+<dash> Aco: at least not until after 1.0
+%
+<radix> hehe yeah the whore house was awesome :D
+<radix> if you go in with a high-level the chicks pass out after you're done
+ with them
+%
+<Tv> What kind of dope is that? md5 digests _are_ 16 bytes.
+<warner> don't trust the hash, man
+%
+<bruce> hmmm. i didn't get my 11pm cron output email.
+* bruce forgets which cronjob that is though.
+<skreech> bruce: nuclear war has erupted. haven't you heard?
+<bruce> (or what machine it runs on)
+<bruce> (or what user it runs as on what machine)
+<glyph> skreech: in 2002, war was beginning
+* skreech makes a "boooosshhh" sound.
+<skreech> glyph: what happen?
+<glyph> skreech: somebody set up bruce the cron, apparently
+<skreech> glyph: main vi turn on
+%
+<shapr> c:\> vrms
+%
+<itamar> yes, but that doesn't make sense, how can you be proud of a civil war?
+<exarkuN> itamar: we won
+<exarkuN> itamar: what's not to be proud of?
+%
+<jml> is there a doc on twisted's version numbering conventions?
+<dash> jml: glyph rolls dice
+%
+<itamar> dash is *already* pre-strectched
+<itamar> he's like 6 feet
+<itamar> well
+<itamar> he's actualy 1.80 meters
+<itamar> or so
+<itamar> it's not that he's a giant insect
+%
+<_moshez> itamar: I'm agaisnt the state too :)
+<shapr> _moshez: are you purely functional?
+%
+<jml> dash: there's an otherwise normal guy at work who uses tcl as his scripting language of choice
+%
+<sjj> let me tell you something. I worked at BK for 1 year, and the veggie burgers have more meat than the whoppers, but nobody complained!
+%
+<sjj> moshez: don't kid yourself, if a cow got the chance he'd eat you and everyone you cared about.
+%
+<dash> zb0: ok, let me describe what you sound like
+<dash> zb0: "Hi. I want to drive a spike through my foot into the floor. Can someone help me with that? I know i dont need to, but there are other things i want to drive spikes into."
+%
+<itamar> it's "moshez vs. the CS profs of doom"
+<dash> itamar: i think in a war with the CS profs of doom, i'd be on moshez's
+ side.
+<itamar> yes, but your goals are different
+<itamar> inevitably your pact would weaken
+<dash> itamar: well, we'd fight a duel if we both survived the war.
+%
+<dash> |mmy: cgi is not an enterprise solution.
+%
+<_moshez> cyli: oh, yes. what did you think of my flame to val?
+<cyli> moshez: i didn't get to read all of it. glyph kept interrupting me
+ with questions of what i thought of it. and then i had dinner.
+%
+<glyph> phed: the abbreviation FAQ does not have the word "smart" in it
+%
+<cvs> Commit from glyph (changed 1) in Twisted/twisted/web: "A more expository docstring. Sometimes I'm distracted easily and I might stop in the middle of" static.py
+%
+<anonymous> i keep forgetting how much *fun* python is without zope
+%
+<glyph> radix: so ... it doesn't already do what you want?
+<radix> glyph: Well, now that I understand that what I wanted is impossible, yes. :-)
+<radix> I mean, yes, it does everything that I want, now. ;-)
+-vinge.openprojects.net- glyph changed topic: Learn from radix: if Twisted doesn't do what you want, modify your desires.
+%
+<CainKnight> Right now, I could care less about the best way to do this, or the
+ intricacies involved. What I care about is making a function get
+ called.
+<CainKnight> If that involves ritual sacrifice to dark gods, fine.
+<CainKnight> I don't care why the dark gods want chicken blood.
+<CainKnight> All I need to know right now is will they accept it and make the
+ volcano not wipe out my city.
+<CainKnight> Once the volcano is placated, then i can go back and figure out
+ that it wasn't the blood, it was the heat mixed with the iron in a
+ rich oxygen environment, and adjust the ritual properly in the
+ future.
+%
+<Yosomono@efnet> I'm so open source that I sequenced my genome and released ISOs
+%
+<glyph> one of the nice things about being american and effectively
+ culture-free
+[The next line isn't really important, is it? --ed]
+%
+<glyph> radix: there is NO bit of canada that's that close to you
+%
+<radix> ok, *6* hours ;-)
+<glyph> radix: yeah, if your car can _fly_
+%
+<Erwin> I will code your website and polish your shoes! With my toung!
+<moshez> Erwin: how do you code a website with your tongue?
+<dash> moshez: two words
+<dash> "salivaproof keyboard"
+%
+<z3p> WHY DO YOU MOCK ME UNIX
+%
+<radix> what does wifi have to do with feng shui? :P
+<dash> radix: optimal flows of internet through your house
+%
+* moshez doesn't see how you can not have a computer in the bedroom
+<moshez> I mean, what if you wake up at 4am and need to talk to someone
+<dash> moshez: walk into the other room?
+<moshez> dash: I'd need to get dressed for that
+<dash> moshez: bathrobe
+<moshez> dash: I'm sane
+<moshez> dash: my bathrobe is in the bathroom
+%
+<itamar> dash: how'd you learn?
+<dash> itamar: lessons
+%
+<radix> bathrobe is easier than boxers ;-)
+<moshez> radix: how so?
+<radix> moshez: eh, you have to deal with legs
+<radix> a swoosh around the shoulders is easier, I think
+<moshez> radix: it takes more presence of mind to tie the belt-thingy
+<jml> moshez: compared to buttons on boxers? I think not
+<moshez> buttons???????
+<moshez> jml: who makes your boxers? Chinese Torture 'R' Us?
+%
+<moshez> I'm always nice.
+%
+<moshez> jml: you're like all men, you're afraid of committing
+<jml> moshez: it's a deep seated fear of conflict
+%
+<glyph> bruce_: GPL can't force you to write code under non-MIT/BSD licenses
+<exarkun> GPL+mindflayer can though
+%
+<zen-@ircnet> which the ratio simplicity/expressivitiy of python?
+<moshez> 2.49866397309784
+<moshez> approximately
+<tigrux> moshez ?
+<moshez> tigrux: well, it's for Python2.2
+<moshez> I haven't had time to modify my calculations for the CVS version
+%
+<spiv> mjs: You've probably noticed by now that dash is only here to make occasional remarks about Twisted & World Domination... ;)
+<mjs> spiv: yeah I starting to notice... but I am sure it will become more fervent when we have a Lisp twisted implementation. =)
+<dash> mjs: when that happens, i will become more powerful than you can possibly imagine
+%
+<teratorn> as a general rule, you should never associate popularity with correctness
+%
+<dopey> fd0: what in particular about the environment is significant ?
+<fd0> dopey: some env-variables
+%
+<blanu> glyph: Itamar says I need a shell account on pyramid. I forgot why.
+<glyph> blanu: You do! For cabal research.
+<glyph> I MEAN CVS ACCESS NOT cabal research there are no blood sacrifices
+<blanu> Yes, yes exactly.
+* Acapnotic puts magnetic blood boy back in the closet.
+%
+<PenguinOfDoom> Bah.
+<PenguinOfDoom> People still say "Linux Redhat 8"?
+<PenguinOfDoom> It's "Linux version 8", damnit.
+%
+* itamar wonders if the phrase "evil spawned in dark aeons beyond the ken of man" should go in a price proposal for a project
+<moshez> depends.
+<moshez> if it's a proposal to Satan, yes.
+<moshez> also, how much would it cost?
+<moshez> is it, like, a big chunk of the price?
+<moshez> if so, possibly a more expansive description is in order.
+<moshez> like, where exactly the evil was spawned.
+%
+<moshez> "Hi, we use bit arithmetic on doubles, becuase we're really
+ stupid. We deserve what we get for programming Perl. Do you
+ have a position for us FLIPPING BURGERS?"
+%
+<radix> where the heck did caps-day come from, anyway?
+<JDAHLIN> It's something we often celebrate here in South America
+<JDAHLIN> Very traditional.
+%
+<radix> man
+<radix> I'm getting drinker's-elbows
+%
+* moshez sees the orbital lasers adjusting
+* ameoba puts on a _REALLY_ shiny tinfoil hat
+<moshez> ameoba: I'm afraid that's another myth
+<moshez> the tinfoil hats actually help us aim the lasers
+<dash> ameoba: they suppressed the laser-dispelling tinfoil in 1953
+%
+"Perl is like a normal chainsaw, but it's inflammable."
+ -- Prior-Art-O-Matic (http://thesurrealist.co.uk/priorart.cgi?ref=Perl)
+%
+<ameoba> READ THE FAQ@!#@ THAT"S NOT A MINIMAL EXAMPLERING@#
+%
+<rc> Yosomono: I was only kidding when I said, "Fuck you."
+<Yosomono> rc: Dude. Water. Bridge. Beneathage.
+%
+<Pahan> demoscene? Isn't that some Greek philosopher?
+%
+<glyph> I'm at MIT. I'm walking down the infinite corridor, and towards the
+end they have a small lab, which looks strangely like the MJ12 Level 2 labs
+in DX. In the lab are a bunch of display screens.
+<glyph> The lab has a placard next to it that says "nanotechnology center"
+<glyph> Soon as I look up at the monitor, it switches to a slide that says
+"nano-indentation".
+<glyph> I'm not kidding.
+<glyph> they are engineering the whitespace eating nanovirus _right here_
+%
+<exarkun> this server rebooted, now freshcvs is raising exceptions, the webserver doesn't run, and mailman's permission
+wrapper refuses to acknowledge setuid bits
+<exarkun> It's enough to make a guy buy a rifle and start shooting people.
+<glyph> exarkun: what kind of rifle
+%
+<jml> anyway, wasn't bruce_ writing a C implementation of spread?
+* jml decides to write a C++ one just to piss dash off
+<dash> jml: that wouldn't piss me off
+<dash> jml: that's like trying to annoy an eye surgeon by stabbing yourself in
+ the face with a pencil
+%
+<exarkun> ThreeSeas: Thanks for playing. BTW, I whipped up an autocoder last night, but it went on strike.. said it wanted a better contract.
+%
+<pht> hi, does python do threads?
+<MoonFallen> we need a dash hand-puppet to answer this question when he's not around
+%
+<ameoba> WHO WAS GENERAL TSO AND WHY ARE WE EATING HIS CHICKEN?
+<dash> ameoba: because it is SPICY.
+%
+<jml> C++ templates, a bad idea ruined by bad implementation.
+%
+<glyph> IT IS BECAUSE I AM A GENIUS!!
+%
+<mindlace> so g-d parses xhtml. Mysterious ways, I guess.
+<radix> no, microdom parsers xhtml
+<radix> g-d munges it :)
+%
+<Mifune> that is one benefit of being "god" on this project... I am my own clusterfuck
+%
+[this conversation took place at 7:16 AM, and both participants knew they had
+ clearly been awake for the previous 12 hours or so... -ed]
+<glyph> So, your schedule in space again too?
+<dash> schedule?
+<dash> my _brain_ is in space
+%
+<radix> mozilla runs on macosx, right?
+<glyph> radix: yes.
+<glyph> radix: but it's slooooooowwwwww
+<fzZzy> It's not too bad on my 933 with 1.25 gb of ram
+%
+<xyld> Java is like being naked, covered in vaseline and beaten with sticks -- I respect that some people like that kind of thing, but I'll pass :)
+%
+<jml> quoth the _moshez: Here is .lore
+%
+<fzZzy> pyn: suck it
+<pyn> An error occurred:
+<pyn> suck
+%
+<blanu> When I mentioned Twisted, Guido said it had been suffering from the
+ problem where you look at it and you can't tell what it is, but that he
+ thinks that has gotten much better lately. I then told him much praise for
+ the responsiveness and hardcore attitude of the Twisted developers.
+<radix> HARDCORE YO
+ * radix headbangs
+ * radix goes to wash dishes
+%
+<ry> twisted_ (twisted@krs-dhcp351.studby.uio.no) joined on efnet
+<Erwin> It's become SENTIENT!
+%
+[discussing Woven... -ed]
+<glyph> dmerrill: We're still working out the best way to approach this philosophpically ;)
+<spiv> "philoso*php*ically"?
+<glyph> spiv: that was the weirdest freudian slip of my life
+%
+<datazone> play that funky music dash boy
+* dash is playing that funky music right.
+%
+<ameoba> radix: I think "+q? :)" is an ETC macro...
+<ameoba> radix: it's used alot in quantum computing.
+<ameoba> "try all values, destroy universe if false"
+<ameoba> useful for that O(1) execution speed, unfortunately, it could be REALLY BAD if your evaluation function's buggy
+%
+<cluster> hehe I haven't had a decent night of sleep since I discovered twisted :)
+%
+<z3p> itamar: you can /never/ have too many monkeys
+%
+<moshez> jafo: going to give a talk today.
+<moshez> jafo: "Smooth Structure of Orbifolds"
+<dash> moshez: if i ever write an RPG, i'm going to make one of the monsters be an orbifold
+<dash> "The orbifold hits! The orbifold hits! You die..."
+%
+<radix> A Sparrow claws your face right off!
+<radix> You are dead! Sorry...
+<radix> [Info] Radix has been slain by A Sparrow!!
+<radix> The gods have mercy on your inexperienced soul.
+<radix> <1hp 103m 86mv>
+<radix> 1hp!
+<radix> no restore!
+%
+<red_one> sayke: what would happen if everyone voted consciencously?
+<sayke> red_one: what would happen if everyone beat their swords into plowshares?
+<dash> sayke: PLOWSHARE FIGHT
+%
+* vegai wears his reading bra.
+<vegai> umm, I mean glasses
+%
+(searching for "god" on Google returns http://phpnuke.org)
+<itamar> something is screwed up with google...
+<LotR> itamar: no, that's god's little joke on google
+%
+<datazone> but the real question is: "do you get to beat victims to death with a steal dildo while wearing a bugs bunny outfit?"
+%
+<exarkun> I'm gonna preempt them by mailing the list and asking what people think is wrong with it
+<Acapnotic> exarkun: will you make that a multiple choice question?
+<exarkun> Acapnotic: YES! A) IT IS TOO GOOD B) IT IS TOO GOOD C) EXARKUN IS TOO SEXY, I WANT TO HAVE HIS CHILDREN D) ALL OF THE ABOVE
+<Acapnotic> > Dear exarkum, i would like very much to use ur smtp client programme but you are too sexy and i want to have many children by you. only problem is that i am currently a man and it will take time to change for you.
+<Acapnotic> > I will understand if you do not wait for me, but i will look forward to the times we are together
+%
+<moshez> spiv: what's the date there?
+<spiv> moshez: Oct 2.
+<spiv> Admittedly we're in daylight savings, but we're also not right on the international date line :)
+<moshez> maybe Nov 2...
+<spiv> Er, Nov 2, yeah :)
+* spiv tries to act innocent, like he doesn't have a time machine
+%
+<radix> glyph needs more friends that can break into his house
+%
+<moshez> itamar: when they have an action figure of me, it will come with a squishing action
+%
+<itamar> so it's, kinda, "heh-inducing episodes occured in conjunction with
+ your girlfriend"
+%
+<jml> dash: well, I might have done something really stupid. Like embed perl code into the example by accident.
+<dash> jml: well, there's stupidity, and willful stupidity
+<jml> dash: and then there's university
+%
+<sjj> hah, they've introduced Vanilla Coke down there as well, eh? ;)
+<jml> sjj: yeah, we're really up to do. Soon, a real "burger chain"
+from the United States is gonna come here. They call themselves,
+Mac-something-or-other. :)
+%
+<glyph> that's AWESOME
+<itamar> wasn't it your idea?
+<glyph> itamar: If it was, I'm a genius
+%
+<Kengur> why cant python b compiled to native bitecode?
+<Erwin> Python IS compiled to bitecode on the Transmeta Muffin
+<inapt> what's native bitecode?
+<inapt> a sekrit language of snakes?
+%
+<icepick> go vote!
+<arma> for the shmuck, or the other shmuck?
+%
+<jml> dash: given the number of places you can stick const, "where the sun don't shine" is probably the best
+ -- jml explains good C++ style
+%
+<itamar> kill him
+<itamar> before he reproduces
+<anonymous> itamar: slightly more tact is called for
+<exarkun> Tact won't solve any problems an aluminum baseball bat won't solve faster.
+<anonymous> can't get to him - no budget money for ticket
+<anonymous> any excess budget money will go toward lucky professor #0 getting whacked.
+<anonymous> I got in trouble, because the first draft of the budget had the official "slop" line item marked as "Dr XXXXXXX hitman fund."
+<anonymous> noone disagreed, mind you - they just didn't want it officially in the budget.
+%
+<Cheez> OMG, the economy is so bad that people are willing to work in tennessee??!?
+%
+<jafo> I finally got through the Internet, but the end guy is REALLY hard.
+%
+<bram> http://advogato.org/person/Bram/diary.html?start=40
+<raph> if i were to click that link, it would: pop up a progress window; print cryptic messages about launching konq to stdout; make the kirc window small and unresponsive; and crash the gnome panel
+%
+<moshez> glyph: do you think programming requires thinking?
+<glyph> moshez: No! That is why we can automate it with robot monkeys, and we programmers must fight to earn our meager living while we are being crowded out by machines.
+%
+<saph> i'd rather have a non-robot monkey, for they are squishier and have hair
+%
+<drue> fermats last theorem is very simple, it's the proof that's a bugbear
+<raph> actually, i have a simple proof, but it's too small for me to type into irc
+%
+<exarkun> INEFFICIENT CAPITALIST YOUR OPULENT TOILET WILL BE YOUR UNDOING
+%
+<dash> it's quicker to ask python than us :)
+<MoonFallen> dash speaks words of wisdom
+<MoonFallen> it is quicker
+<MoonFallen> it's not always useful, though
+<MoonFallen> for example, last week i wanted to know a good brand of barbecue sauce. i tried asking python
+<MoonFallen> it quickly gave me my answer: SyntaxError
+<dash> MoonFallen: we dont buy any other brand
+<MoonFallen> i knew i should have looked for it at Whole Foods instead
+%
+<jml> <saph> it is easy to get a date
+<jml> <jml> saph: that's easy for you to say
+<jml> <saph> you just have to be brave and have little or no standards
+* jml fucks his clipboard
+%
+<Tenshihan> if I had a binary number 1001001 and I wanted to count the
+number of 1's in it... and the only math function I have is add, should
+I shoot my professor?
+%
+<MoonFallen> or maybe just pay his army to surrender. it would cost less than
+shipping 250,000 troops over there, i'll bet you.
+<MoonFallen> the oil companies would probably chip in too
+<MoonFallen> we could get everyone involved. like sponsoring a starving child
+in africa. except you're sponsoring an iraqi to surrender
+<MoonFallen> i wonder if we could get them to write their sponsors letters.
+"thank you for not blowing me up. thanks to your generous donation of 1 million
+dollars, instead of being dead, i am now an oil magnate in my native country."
+%
+<datazone> you're a towel
+<exarkun> a towel of IMMENSE POWER, yes.
+%
+<moshez> wow
+<moshez> I don't see how people didn't think of this before
+<moshez> if you're in competition with some windows user, just report him to the bsa
+%
+<jml> damn you all. damn you and your witty repartee and your elegant bloody framework. I'm going to sleep.
+%
+<jml> here I am, brain the size of a planet, and they make me do XML
+%
+* glyph thinks x++ should have been named "<xml type="programming">
+ <increment /> <increment> </xml>"
+%
+<raph> i was going to publish a spec for bitmap images much along similar lines as BLOAT
+<raph> ie, <pixel><color><component name="red" value="34"/> ...
+<raph> now here is the evil thought
+<raph> do up an XLST stylesheet to render it as a huge html table with cell backgrounds for each pixel
+<raph> so you can view it in mozilla
+%
+<aum> dash: do we have a 'non-profanity' chan policy here?
+<dash> aum: we have a "not acting lame" policy
+%
+* spiv wishes he never has to see another meta-argument
+<moshez> spiv: meta-arguments are fun!
+<spiv> moshez: The first time perhaps. They're always the same, though. It gets tiresome.
+<glyph> spiv: you're having a meta-meta argument now
+<spiv> glyph: My life is pain :)
+<moshez> glyph: I was afraid of having to point this out to spiv myself
+<dash> moshez: it wasn't an argument 'til you spoke up :)
+<moshez> dash: but he *answered*
+<glyph> aaaaaaaaa
+<glyph> METAMETAMETA ARGUMENT
+<jml> glyph: not it's not
+--- ChanServ gives channel operator status to glyph
+<-- glyph has kicked jml from #twisted (IT IS NOT AN ARGUMENT IF I HAVE A GUN)
+<exarkun> +1 (Insightful)
+%
+<dash> bruce: oh. intellectual dishonesty doesn't bother me when it comes to getting k5 to post our propaganda.
+<moshez> dash: intellectual dishonesty doesn't bother me when it comes to brainwashing and taking over the world
+<dash> moshez: That's what I said.
+%
+<MoonFallen> i'm looking at a perl program called tedia2sql
+<MoonFallen> the author seems pretty competent, judging by the quality of the program, but it takes him almost 300 lines what t.p.usage would allow me to do in 50
+<MoonFallen> and i'm not that good
+<glyph> MoonFallen: yes, but I'm *amazing*, and I wrote t.p.usage ;-D
+<MoonFallen> well shit, no wonder
+<glyph> and it's been hacked on by people smarter than me, since then.
+<MoonFallen> incidentally, who do you consider smarter than you? i need to hire those people or keep away from them
+%
+<MoonFallen> lol. it's nice when someone starts out a post like this: " Basically, the entire structure of your argument centers around the assumption that it's bad to have bugs in your program."
+<MoonFallen> then i know i can skip the rest of the post
+%
+<bram> I just got a call from a mechanical voice which said 'I'm sorry, I dialed your number in error'
+%
+Grocible says, "this programming job on another site demands "courage, commitment and loyalty""
+Grocible says, "Courage commitment and fucking loyalty?!"
+Grocible asks, "is this an ad for a knight's assistant circa 1450?"
+Grocible asks, "what were they called? Pages?"
+Nate says, "So that's what Active Server Pages are."
+%
+<liiwi> _pattern = re.compile('^(?P<client>[^ ]+) (?P<ident>[^ ]+) (?P<authuser>[^\[\n]+) \[(?P<mday>[0-9]+)\/(?P<mon_name>\w+)\/(?P<ye\
+<liiwi> ar>[0-9]+):(?P<hour>[0-9]+):(?P<min>[0-9]+):(?P<sec>[0-9]+) (?P<timediff>[^ ]+)\] "(?P<method>(GET|HEAD|PUT|POST|TRACE|DELETE|O\
+<liiwi> PTIONS|-))( (?P<url>.*) (?P<proto>[^ ].*))" (?P<status>.*) (?P<bytes>.*) "(?P<refer>.*)" "(?P<agent>[^"]+)"(($)|( (?P<stime>[^ \
+<liiwi> ]+) (?P<vhost>[^ ]+).*$))')
+<radix> BURN IN HELL
+%
+<radix> somebody dressed in a chicken suit came out during the concert and attacked them
+<radix> and they beat the crap out of it
+%
+<rc> I don't find transclusive folding to be that useful a programming
+language feature.
+<faassen> rc: well, it's an acquired taste.
+%
+<Stravad> Python has that instance id thingy for everything
+<glyph> Stravad: uh, that's not an OID, that's &foo; :-)
+<radix> glyph: !?
+<dash> radix: that's what id() does
+<radix> kill me
+<radix> I read that as an XML entity
+%
+<glyph> if zone transfers are bind fileformat
+<glyph> how much more complicated could this be than FTP? :)
+<dash> glyph: Prepare to be surprised.
+%
+<dash> it's like a bicycle
+<dash> but with internet
+%
+<dash> itamar: ok. well, given that Jesus did rise from the dead, one has to consider what this says about him
+<itamar> he was lucky?
+%
+<moshez> we aren't really a dictatorship
+<moshez> we're more like an anarchy, except WE ELIMINATE PEOPLE WE DON'T LIKE
+%
+<radix> sometimes i eat tums just cuz I like the taste
+<glyph> radix: that sounds like a bad idea
+<radix> you can never get enough calcium!@
+<glyph> radix: if your eyelids ever start sticking to your eyes, or you can't see or hear because a caky, white film has covered your eyes or ears, you may want to consider cutting back on your over-the-counter pharmiceutical intake
+<radix> hold up... let me raise the font size.
+<radix> Oh.
+%
+<moshez> nobody resizes my text terminals and lives.
+%
+<saph> radix: i've eaten nothing but a subway and a weird coconut thing my mom
+ made that has a pecan on it and i was kind of afraid of it, but i was also
+ very hungry so hunger won out on that one
+%
+<glyph> the god of unit testing is going to kill me for this code
+<Tv> There is no such thing.
+<Tv> I would have been dead by lightning for years now.
+* liiwi notices the lack of god of code commenting too
+%
+<queuetue> Are these actual people we're discussing, or another webcomic?
+%
+<glyph> ono! I have forgotten the sacred waterfall
+%
+<icepick> I, as someone who was a professional php programmer, can tell you: Think of the children
+%
+<PenguinOfDoom> I reject that approach. It has a suspicious lack of internet.
+%
+<glyph> itamar: uh, I *am* twistedmatrix.com
+%
+<PenguinOfDoom> CA is definitely like life;
+<PenguinOfDoom> When is says "bonus", it means "you are buried"
+%
+<itamar> i don't understand how COM works
+<MoonFallen> me neither
+<MoonFallen> i suspect it doesn't
+%
+<lgonze> ok, name a security flaw in browsers.
+<raph> "bugtraq browser" returns about 37,100 hits on google
+<raph> sorry i don't have the patience to sift through them all
+%
+<Yosomono> He's really a reasonable person, if you read his writing.
+<Yosomono> I mean, aside from the "lizards run the world" thing.
+%
+<glyph> AND NOW FOR A MESSAGE FROM OUR SPONSOR
+<glyph> Are you WEIRD?
+<glyph> Are you MADE OF INTERNET?
+<glyph> Use Twisted! Or die. http://www.twistedmatrix.com/
+%
+<MoonFallen> i know. but i've read too many horror stories. glyph gets run over
+by a truck, his source code gets acquired from his estate by microsoft, evil
+ensues
+%
+<radix> bah screw it
+* radix fakes it
+<glyph> radix: hooray for faking
+%
+<jml> will you take me, to build and to dist, in windows and in unix, till uninstall do we part?
+<jml> If any man here objects, let him speak now or ... "error: command 'cl.exe' failed: No such file or directory"
+%
+<dash> moshez: why's that better?
+<moshez> dash: um, because it doesn't necessitate Elijah
+<moshez> so it's more portable
+<jml> re-use for fun and prophet
+%
+<cow_2001> btw, for me python is love from first sight..
+<exarkun> and as with real love, it will fade after you copulate with it
+%
+<itamar> just because he was dating a 16 year old that one time he was
+ supposed to be doing a release...
+<gvanrossum> too much info, okay?
+%
+<itamar> liiwi: europe has no business
+<itamar> thus it can't make business mistakes
+<itamar> bunch of socialists living in caves banging rocks together
+%
+<liiwi> why not illegalize guns while they're at it?
+<chrchr> liiwi: Because they're Republicans. They love guns. It's _ideas_ they hate!
+<liiwi> let's start using shotguns to route packets
+%
+<radix> I love killing everything with the sword
+%
+<porridge> does python have an equivalent of C ternary "?:" operator?
+<Erwin> Python has the sextary operator, !@#$^&. Given expression x, each of the 6 operands of the sextary operator is evaluated depending on whether the expression is logically true, false, morally right or wrong or neither of those
+%
+<exarkun> if you're lucky it causes segfaults
+<exarkun> if you're unlucky it signals the Mothership that Earth is ripe for invasion and brings about the destruction of all mankind
+%
+<chrchr> fariseo: If PHP is like stabbing your eye sockets with a screwdriver, Python is like not stabbing your eye sockets with a screw driver.
+%
+<saph> i don't know. i've smoked more than my fair share of pot in my day and i've never shot anyone or raped or been raped by anyone
+<saph> the most i'd do is make some really fucking cool paintings
+<saph> that and played the best scrabble game of my life
+<saph> but that was under the influence of both pot and alcohol
+<radix> "floopy! it's a word! I swear it!"
+%
+<sjj> radix: I could smoke a pound of crack and still pronounce "nuclear" better than G.W.B
+%
+<saph> sjj: president is a minimum age of 35 (which i think is complete bs)
+<saph> i know people who are 28 who could run the country better than bushy
+<sjj> saph: I could argue I know people 5 years old who could run it better than bush.
+* warner knows magic 8-balls which could etc..
+<saph> sjj: a ficus plant could do better
+<dash> warner: that's why i'm voting for Inanimate Carbon Rod!
+<jml> In Rod We Trust
+%
+<sjj> moshez: I somewhat see what you meant about Gimli being the target of _lots_ of jokes in TT
+<sjj> moshez: it got a bit old after a while :\
+<Tv> Yes, the jokes fell a little.. short.
+%
+<Pahan> Comfort me, please.
+<fzZzy> no
+%
+<spiv> 11am - 6pm. For *five* whole days. And that's just one game! It's brilliant.
+<glyph> it's like the chanukah of professional sports!
+%
+* warner has done too much work with intermittent test failures
+<warner> my worst nightmares involve the alarm clock only ringing on mornings after I fall asleep on minutes ending in an even number
+%
+<Artimage> Says it will take 15 minutes
+<Tschechow> 15 _apple_ minutes.
+<Artimage> Actually, its already down to 4.
+<Tschechow> oh, you got hardware with an apple-minute-rate <1?
+%
+<exarkun> it probably doesn't even belong in the evil directory
+<dash> exarkun: why, do you have a "stupid/"?
+%
+<exarkun> bring on the dancing monkeys
+<Tenshihan> radix?
+<exarkun> That works
+%
+<moshez> jml: but euphemisms for sex are common in all languages :)
+<exarkun> moshez: what about lojban?
+<jml> exarkun: there's no record of any lojban speakers having sex. :)
+%
+<jml> If I were a girl, I'd fall in love with a bloke who wrote copious amounts of documentation.
+%
+<MoonFallen> i just signed up for a trial of o'reilly's safari thing, and noticed they had a "voodoo" topic category
+<MoonFallen> the first three books are about .NET
+<MoonFallen> i always had a feeling there was goat blood and zombies involved
+%
+<moshez> Debian: If It's Free, Insecure and Crap, We have It.
+%
+<jml> moshez: did you know that the average vegetarian walks around with 2 kgs
+of anti-establishment bile in their stomach? :)
+%
+<moshez> glyph: in the future, browsers will support google://blah blah :)
+<glyph> moshez: in the future, telepathic russians will rule the earth, and computers will be made of synthetic cheese!
+<moshez> glyph: before that
+%
+<faassen> I mean, do they say, okay, so bush looks like a born again christian with a faint hold on sanity but he's really a secular humanist and that's *good* or that's *worse*?
+<dash> faassen: he's a secular humanist with a faint hold on the english language
+%
+<glyph> I'm an un-american anti-semitic american jew! I love this country.
+%
+<hypatia> I'm not part of the American way because they don't let you own those kind of weapons around here :)
+%
+<Pahan> dash uses windows?
+<Tenshihan> his grandparents do -- so he's got some windows in his blood
+<Tenshihan> it's like being a quarter jewish
+%
+<dash> lament: well. inductive folds are pretty much the same thing as
+__get__ in python combined with the appropriate metaclass
+%
+<jml> hmmm
+<jml> Let me put it this way.
+<jml> When I read 'Brave New World' I imagined most of it to be in a place very much like Canberra.
+%
+<spiv> hypatia: I have seen snow!
+<hypatia> spiv: When? You mistook frost for snow one time :)
+%
+<dash> see, in my day, we didn't have those fancy init scripts
+<dash> just zeroes and ones
+<dash> and we used upstream bandwidth both ways
+%
+<Marvin--> well, yes, we know that /. is broken, but do you mean in some
+ particular way?
+%
+<jml> Argh!! what's happening to me. I'm melting..
+* Jerub hands jml a pamphlet : "So you've started to devolve into a primordial ooze".
+%
+<moshez> dash: well, I lent my crystal ball to itamar
+<moshez> because his broke.
+<moshez> and then he broke mine.
+<moshez> evil itamar.
+<itamar> I did not break your balls!
+%
+<Kengur> dash: r u familiar with the Nebula Device?
+<dash> Kengur: you.... *might* say that, yes
+<Erwin> Kengur: that's the self-destruction device Kirk threatens to blow up?
+<dash> Erwin: Kirk's was before the invention of C++, though
+<dash> Erwin: so they couldn't really imagine the horrificness
+%
+<dash> sourceforge's CVS server is secretly dalnet!
+%
+<moshez> Yosomono: I AM INSANE!!!!!
+%
+<etrepum> webtastic
+%
+<liiwi> hrmpf. concurrency issues are nasty
+* Tv gives liiwi two forks and invites him to join the table.
+ <liiwi> Tv: can I bring my leatherman?
+<Tv> Well, only if you give up one fork.
+<Tv> We need to have the same number of tools and eaters.
+<Tv> Otherwise it's quite unfair :)
+%
+<moshez> the common definitions say that after a person rises from his grave, he is the undead.
+<dash> moshez: Buffy definitions are not common definitions.
+<moshez> dash: Buffy could kick your ass.
+%
+<ameoba> EVERYBODY GET STONED!!!
+<dash> ameoba: furthermore
+<dash> ameoba: everybody MUST get stoned
+<ameoba> YAY EQUALITY
+%
+<z3D> dash: yup ... remember the word 'enterprise' ?
+<dash> z3D: YES! it is one of my favorite words
+%
+<chrchr> datazone: jwz didn't invent "If the only tool you have is a hammer . . ." any more than the French invented tongue kissing.
+<datazone> chrchr: my grandpa invented french kissing
+<datazone> and i will be damned if i sit here and listen to you say that he didnt
+%
+<exarkun> '''The MODE command is a dual-purpose command in IRC. It allows both usernames and channels to have their mode changed. The rationale for this choice is that one day nicknames will be obsolete'''
+<dash> yeah, and the marxist state will wither away
+%
+* radix is a monkey with a bamboo stick
+* Kengur tries to trade banana for a bamboo stick
+<radix> hah!
+* radix beats kengur on the head and steals his banana.
+<radix> You don't need trade when you've got a bamboo stick.
+%
+<lament> Software Engineering is basically a set of techniques for making bad programmers write good code.
+<dash> lament: Which is why it doesn't work.
+%
+<Yosomono> ameoba: my hand made me sleep on the couch :P
+%
+<tansaku> mathematics is so about sex
+%
+<chrchr> Spalding Grey made the point that comedians (and comic artists!) might spend a long time building up to the punchline at the end of the joke, while a good storyteller can have the audience laughing throughout the story.
+<chrchr> Discuss.
+<exarkun> Spalding Grey is a monkey in red suspenders.
+<chrchr> exarkun: Excellent point.
+<chrchr> Everyone, how does exarkun's point about Spalding Grey being a monkey in red suspenders make you feel?
+%
+<kiko> is it just me or is the term "software design" too vague to shake a stick at?
+<exarkun> kiko: get a bigger stick
+%
+<jml> dash: is quoting pokey like mentioning the holocaust on usenet?
+<dash> jml: nope
+<dash> jml: quoting pokey is recommended practice
+<jml> dash: but, it makes retort impossible.
+<dash> jml: you're beginning to understand
+%
+<radix> HORRIBLE JAVASCRIPT ARGh
+<moshez> radix: when a teenage suicidal brooding girl builds a web site, what do you expect?
+%
+<itamar> glyph: spyce.sf.net
+<glyph> GYAH
+<glyph> yes, I've seen this before
+<glyph> I believe it was looking at this page that I coined the phrase "captain Dimwit McStupid"
+%
+<fatjim> quotemaster: i know a guy on gimpnet who wrote his entire website in bash
+<fatjim> quotemaster: then he wrote an ircbot in bash
+<fatjim> quotemaster: then his head exploded and we had to burn the entire building to prevent an epidemic
+%
+<dash> Iä, const char* fhtagn
+%
+<radix> When I take PCP, I pretend that I'm half-leprechaun-half-cheetah
+<dash> "You feel jumpy."
+%
+<Acapnotic> Are there sound effects that accompany the speaking of "Debian Project Leader"? Trumpet fanfare or choir of angles or the hushed whispering of the cabal or something?
+<_moshez> Acapnotic: the Empire music from Star Wars
+<_moshez> dam dam dam dadadam dadadam
+%
+<psy> good thing my 15yo sister also smokes rollies =P
+%
+<dash> i used to think ms creighton was just insane, but i'm reminded of a .sig on the parrot list
+<dash> "the difference betweeen insanity and genius is measured by success"
+%
+<sjj> itamar: you'll be sorry when the VIC is mainstream! you'll all be out of jobs HAHA
+%
+<Logan> You might as well ban dancing next!
+<Yosomono> Logan: We don't allow that sort of reckless behaviour here.
+<Yosomono> Logan: It leads to fraternization amongs youngsters, and eventually PREMARITAL SEX
+%
+<Logan> chrchr: Are you saying this channel is boring?
+<Logan> I think I saw something funny in here once.
+%
+<glyph> oh. where is saph?
+<exarkun> she got a job
+<glyph> Hooray!
+<exarkun> It's a mixed blessing
+<glyph> What's in the mix?
+<exarkun> I get to hear about how I don't have one a lot :P
+%
+* zookoasleep sleeps furiously.
+%
+<raph> [WebDAV] probably has all the disadvantages of a stateless protocol combined with the problems of actually implementing state
+%
+<chrchr> If I had dignity, I'd have to start wearing pants to work.
+%
+<amybah> THE REVOLUTION WILL BE TELEVISIED (to drm compliant devices)
+%
+<dash> "we wisssssh to sssquissssh, my preciousss"
+%
+<itamar> glyph freaked out my grandmother
+%
+<dash> [From the GPL FAQ:] "If the program dynamically links plug-ins, and they make function calls to each other and share data structures, we believe they form a single program"
+<dash> the days of "one process == one program" are coming to an end, i believe
+<SamB> o/` Python is a totally different way of thinking, which doesn't have much to do with linking o/`
+%
+<glyph> moshez, you are only allowed to be excited about one insipid american cultural icon at a time
+<moshez> glyph: have you not realized that at heart, I'm an american teenage girl :)
+%
+<moshez> I prefer the ones who fully embrace, knowingly and publically, the commercialization
+<moshez> than the ones who commercialize by being "non-commercial" (Eminem is the prime example here)
+<Aco> whatever... i just like girls in white panties
+%
+<lstep> It's not my fault, my wife keeps connecting under false identities to monitor me!
+%
+<wzZzy> I tried to use woven.guard sunday night
+<hazmat> and?
+<wzZzy> hazmat: glyph's powers of obfuscation are considerable
+%
+<icepick> to the batlaptop!
+%
+<moshez> radix: oh, and did I tell you I'm officially a sock puppet now?
+%
+<wzZzy> what's up mjs
+<mjs> wzZzy: nothing much, just got home from school... yourself?
+<wzZzy> mjs: workin'
+<mjs> that's what you are always doing. =)
+<wzZzy> that's cause I have a job
+%
+<raph> but if you are going to combine a scoring system with eigenvectors, it's a lot easier to think about linear things than funky bayesian formulae
+<wmf> eigenbloggers are useful, too
+* raph prepares to eigensmack wmf
+<raph> prepare to compute the eigenvalue of _this_, fool
+%
+<exarkun> nethack should be made illegal
+<exarkun> it corrupts the minds of otherwise upstanding young men
+<dash> exarkun: dont make it illegal 'til after i get this fellow into gehennom
+%
+<glyph> I am getting my design on.
+<glyph> (For those of you not already in the aisles, this means: RUN!!!!)
+<glyph> I _really_ like this one
+<glyph> it may not be appropriate for the main site, but we're going to have to use it someplace
+%
+<glyph> radix: Your credulity is not a requirement for precipitation!
+%
+<shapr> no one wants to play with my monads :-(
+%
+<ThreeSeas> what is melatonin?
+<ThreeSeas> is that like another programming language?
+%
+<gus> doesn't the phonetic representation (Zo["o]l.) mean that it should be pronounced "Zoul"?
+<glyph> I don't know...
+<glyph> I can't read phonetic.
+<gus> and all this time I thought you were phonecian.
+%
+<exarkun> Oh. The infamous "Windows sucks" bug.
+%
+<datazone> you know, america has alot of different military orginazations
+<radix> yes, maybe they'll get into a war
+%
+<radix> i want to make sweet love to twisted.python.components
+<ameoba> radix : it probably wouldn't be hard to whip up an adapter +)
+%
+<jml> _moshez: having dates would be kinda cool.
+<itamar> jml: if you take a shower every day, you'll find it's a lot more
+ likely
+%
+<trawa_@ircnet> im trying to get started with web services, but i need
+ some software - i think i read all of ibm's articles on that, read
+ about 4suite - does anyone know of sth litghter, smaller in size and
+ easier to absorb?
+<dash> trawa: hmm. what do you mean by "get started with web services"? :)
+<trawa_@ircnet> well im trying to port my html application to flash mx
+ but dont fell like using cold fusion..
+%
+<exarkun> What powers the orbital lasers
+<exarkun> I always assumed it was the Sun, but now I don't think that would work too well
+<exarkun> Unless we are anticipating zero resistance after the destruction of the Sun
+<exarkun> This sounds like a serious flaw in our plans, then
+%
+<floam> itamar: think of how the anonymous bathroom fits in with the way our society works
+%
+<floam> the paper towel represents the customers
+%
+<dash> perl has EVERYTHING.
+<brc> except buffer overflows
+%
+<wzZzy> we should write an os
+<itamar> YES
+* itamar starts a sourceforge project
+%
+<chrchr> There are some dead/preserved carpenter ants in a bag in the mailroom of the building where I live, and a sign that says, "THESE ARE CARPENTER ANTS. BEWARE!!"
+<datazone> chrchr: why do they assume ants can read, and if they can read, that they can read english?
+%
+<sayke> those whom the gods would destroy they first make l33t
+%
+<shapr> ucking keyoar
+%
+<drewp> this twisted thing really is a text adventure game
+<dash> drewp: What a ridiculous notion.
+private message from dash: wuh oh, drewp is on to us
+%
+* itamar goes to french class
+<radix> FRENCH class???
+<radix> Don't you mean FREEDOM class?
+%
+<glyph> my condolances
+<itamar> if Thunder- solves it i'll be ok
+<itamar> writing a C++ extension to open a fucking find dialog is soooo fun
+<glyph> oh
+<glyph> I meant "my condolances on failing to stop the war" :)
+%
+<moshez> glyph: I have an ethical question for you.
+<glyph> moshez: my answer, as always, is "kill them all and let god sort them out"
+%
+<snibri1> i wonder if those boost ppl ever sleep
+<jemfinch> snibri1: why?
+<AdamV> snibri1: Nightmares from C++ template coding?
+%
+<Artimage> Amazon wish lists are really a double edged sword.
+<Artimage> This guy made me his amazon friend... so I checked out his list...
+<Artimage> lots of 'how to make your own porno movies' books... I now know WAY TOO MUCH... but I have no clue who he is.
+<Artimage> I would have to know someone at least a week before I'd be interested in knowing this.
+%
+<treker> pyn ur fuckin hot
+<treker> :-Plol
+%
+<moshez> marz: don't be scared!
+<moshez> marz: the squishing is fun
+<moshez> ask anyone
+<dash> it isn't fun
+<dash> it's like a tiny genocide
+<moshez> marz: ok, ask anyone except dash
+%
+<moshez> dash: what is your profession, again?
+<dash> moshez: "irc junkie"
+<moshez> dash: good money in that, huh?
+<dash> moshez: money?
+<dash> moshez: now that you mention it
+<dash> most of my jobs lately have come from irc
+%
+[re: porting the rewrite of jelly to Scheme]
+<radix> So is it still really mutaty?
+<glyph> It's so mutaty you're going to be using 'set' with *two* exclamation points
+%
+<jml> you ever heard about cosmic conflicts between the forces of order and
+ the forces of chaos? Well, Canberra is like Order winning, stomping over the
+ frail corpse of chaos, and all spontenaity, surprise and flexibility
+ disappearing from the world.
+%
+<Spec> Damn you pyn. You are now my greatest adversary.
+%
+<-- Spec has quit ("The bell that controls our lives has released us into the world....woot.")
+%
+<exarkun> What's a good, low-cost way to notice when a new file exists in a directory?
+<SamB> exarkun: open the directory for reading, read to the end, and use select? (crazy!)
+%
+<coderman> you ever see my callback templates for use with static and member functions?
+<coderman> it works nicely, which is a credit to the STL, but it is also so incredibly ugly it makes my head hurt
+<coderman> http://cubicmetercrystal.com/alpine/gen_html/base/AppUtils/AppCallback.h.html
+<zooko> What the fuck, man? Why are you memcpying these void*'s? Where is callbackData_ defined? What are all these things templated on "class argument Type"?? AAAAiiiigh!
+* zooko holds one hand over his eyes and clicks blindly with the mouse in the attempt to hit the "close window" button.
+%
+* Acapnotic puts on his "in MY day, we had to write our modelines by hand, with nothing more than some loose notes by matt walsh, the back on an envelope, and the blood from our own hands..." pants.
+%
+<jml> the phrase "unstable reactor" makes me feel a little nervous
+%
+<etrepum> dance dance hack?
+<radix> hack hack revolution
+%
+<pyn> <widada@ircnet> this is what i see: "<pyn> <radix@fn> Huh?"
+<pyn> <princepsd@ircnet> do you want to see somethingelse?
+<pyn> <princepsd@ircnet> ;))
+<pyn> <princepsd@oftc> or is this to much for you? ;))
+<pyn> <princepsd@efnet> the bridge confuses you?
+<princepsd> you can't keep track? ;))
+%
+<exarkun> why would someone mount a swapfile from an NFS mount?
+<MoonFallen> exarkun: to bring about an anarchic dystopia
+%
+<princepsd> marxist witted is a anagram of twistedmatrix
+%
+<exarkun> z3p: conch is making me sad again
+<z3p> exarkun: conch only does ssh, it is not responsible for also making you happy
+<exarkun> Oh. Crud.
+%
+<bram> it's nice being a cult leader :-)
+<bram> now I must get my minions to start giant bonfires in the woods and do ritual dances around them
+%
+<Yosomono@efnet> glyph: Does it amuse you sometimes to realize you've created an entire genre of coding for people?
+<glyph> Yosomono: "sometimes"? It amuses me constantly. It's become the central staple of my entire sense of humor, as well as my livelihood.
+<glyph> It is easily the most amusing thing in my life.
+%
+<dash> if python is an orchestra, overloaded operators are "miscellaneous percussion"
+%
+<radix> Don't be a consumer! Live off the land
+<itamar> there are no pants-trees in nyc
+%
+<fzZzy> what good is planetary consciousness if it can't open an arbitrary socket?
+%
+<jml> can someone briefly explain componentized in the context of the web registry?
+<spiv> jml: You've got an internet cloud, right? Only, you're reading isometric, so you really want an internet *cube*.
+ So, you take your componentised cloud, and ask it for a cube adapter. Then exigency girl arrives, kicks your ass, and everyone
+ is happy.
+%
+<spiv> As far as I'm concerned, the meat pie is the ultimate unit of currency.
+%
+<Noen> fear twisted, because its leet
+%
+<glyph> jml: I PROMISE I really thought popsicle was a good idea at the time.
+%
+<jml> balsa is crying out to be re-written in Twisted
+<jml> 'heal me' 'heal me'
+<jml> 'I don't want to be threaded'
+<Acapnotic> yeah, lots of mail programs say that
+<jml> Acapnotic: It must be very easy to write GUI mail programs that suck
+<Acapnotic> it is, but a lot of people have gone above and beyond the
+ bare minimal effort required
+%
+<warner> Activate the Flux Condensor!
+<MoonFallen> We can fix it by patching the wireless with a subspace access point!
+<itamar> the powerbook's dilithium crystals are not in tune with the mr814 access point
+* warner looks through his toolbox for a transdimensional flux agitator
+<warner> uh-oh, we've got a level 3 resonance singularity
+<MoonFallen> But it'll be risky! We've got to get the harmonics oscillating
+ correctly before Buffy comes on, or we'll be dead in the water.
+<warner> Right, you align the warp power conduits while I re-fractalize the
+ positronic emitters. We meet back on the engineering level in 40
+ centons.
+%
+<warner> happy tests, fat buildmaster
+<spiv> warner: I can practically hear it yelling "Feed me, warner!" from
+ here ;)
+<spiv> Heh. Now I'm envisaging a "Little Shop of Hackers" with the
+ buildmaster as the plant (and the buildslaves for tendrils!), and poor
+ warner as Seymour :)
+<warner> FEED ME!
+<spiv> For some reason, I keep thinking of _moshez as the dentist ;)
+%
+<exarkun> radix: [dash] *is* a lazy sob
+<exarkun> radix: but that doesn't mean he's wrong
+%
+<itamar> I don't want my name in the windows registry
+<itamar> it's probably bad luck
+%
+<exarkun> it's JAVA, if you didn't want to type FIFTY LINES to do ONE STUPID THING you wouldn't be using it, right?
+%
+<jml> _moshez: yeah, but that's obviously an imposed patriarchal paradigm
+ that's entirely foreign to the implicit metanarritive.
+<flax07> not to be ot - but can anyone give some newbie help
+%
+<freeside> On a scale of One to AWESOME, twisted.web is PRETTY ABSTRACT!!!!
+%
+<rik> note to self: do not advise people to use a Deferred when in #c.
+%
+<saph> dash: when you take over the world, can i be in charge of leather and vinyl active wear?
+%
+<Nafai> w00t w00t w00t w00t!
+<Nafai> I don't understand all of the code, but it works!
+<Nafai> I guess I should check it in.
+%
+<moshez> glyph: I don't care about actual people
+<moshez> glyph: I care about not screwing up Debian
+%
+<arno> what are the main things to change going from [bsddb] 3.3 to 4.1?
+<icepick> moving around when you open transactions
+<arno> sounds like money laundering strategies...
+%
+<gt3> welcome to the world wide wtf
+%
+<glyph> warner: you want to port twisted to PyMite?
+<warner> now *that* would be entertaining..
+<Nafai> ...for various definitions of entertaining
+%
+<zooko> I don't know if I'll have much time, but my goals are: 1. test_ent.py, 2. ent.py, 3. make znff.py use ent.py, 4. rule the universe
+%
+<hypatia> moshez: That's OK, you can feature in my diary as "Glyph's bitch".
+%
+<akrherz> quotacheck <-- Do I have quota left for more questions :)
+<radix> akrherz: Insert $.25
+<akrherz> where? Can I just give my credit card?
+<radix> akrherz: That'll do.
+<akrherz> wow, the internet is fantastic
+%
+<glyph> dreid: so you have some free time?
+<dreid> glyph: not really but i could always sleep less
+%
+<jml> any world knowledge-ables about?
+<chrchr> jml: YES.
+<chrchr> jml: Oh. You mean twisted.world.
+<jml> chrchr: yes.
+<jml> chrchr: I am not aware of any other.
+%
+<wumpus> google, froogle, when are they going to start making names that make sense? :P
+<wumpus> even the twisted module names make more sense
+%
+<dash> wumpus: names that make sense are at an end
+<dash> wumpus: they have been all trademarked
+%
+<chrchr> radix: Are you at NASA? Do you see any of the aliens?
+<radix> No.
+<radix> chrchr: They don't keep the aliens at Goddard, anyway
+%
+<_moshez> dash: we need to make Twisted into, pardon the comparison, Zope
+%
+<Holocaine> moshez: I think your bad wrap comes from the fact that all of us
+ are far too postmodern for your version of correctness. =)
+%
+--- itamar has changed the topic to: "We reject kings, presidents, and voting. We believe in rough consensus and running code." -- David Clark
+<dash> also, giant robots.
+%
+<itamar> better not have children anywhere [POWERFUL CORPORATION'S NAME ELIDED -ed] can fly lawyers
+<etrepum> hah
+<etrepum> they're not that bad
+<itamar> that's what they all say
+<itamar> and then it's "but I didn't think they take *my* little Joanne!"
+%
+<dash> fortunately not all people who call themselves christians are bloodthirsty imperialists
+%
+<glyph> dreid: you want to talk about the web?
+<dreid> glyph: yes
+<glyph> dreid: I THINK THE WEB IS TERRIBLE
+<dreid> well there is always gopher
+%
+<zoyd> someone help me with using dictclient.py
+<zoyd> the dict.org client that is.
+<tappintap> zoyd: what are you trying to do. Start at the beginning, like: I
+ got up today, and I wanted to paint the shed.
+%
+<dash> there is a particular sense of fatigue that i have come to associate with the aftermath of attempting to troubleshoot windows problems
+%
+<itamar> oooh
+<itamar> Windows Server 2003 CD
+<itamar> 180 day evaluation
+<glyph> itamar: damn! that's half as good as the full product
+%
+<ivan> i can't wait till palladium!
+<ivan> cheat-free gaming at last
+%
+<jml> spiv: some might call it rewriting, I call it "refactoring from zero"
+%
+<Nafai> I need to find a reason to learn Woven. :)
+<exarkun> it will get you laid
+* Nafai thinks
+<Nafai> I'm not sure I believe you.
+%
+<jml> if I'm stuck in windows, what's a good browser?
+<jml> preferably one that implements the Aquinas protocol so I can get infinite bandwidth with no hardware upgrades
+%
+<radix> THE REAL WORLD IS JUST LIKE HIGH SCHOOL
+<radix> ARAGH
+* radix shoots himself.
+%
+<exarkun> "While working on a web framework late one night, Mr. Preston was sucked into an http vortex and trapped on the internet. Now he roams from website to website, crying at code embedded in malformed html templates and exploiting cross site scripting bugs."
+%
+<AdamV> SamB: PHP's basic control structure is the "database timeout error".
+%
+<tjs> I was telling the guys at work about woven, and they asked me to implement it in php.. I told them, without battering an eyelid, that it was totally impossible.. sometimes you just have to stand up for whats right
+%
+<tjs> they have xml parsers for php
+<dash> I AM NOT EVEN GOING TO THINK ABOUT IT
+%
+<WuN> my school went on a trip to Queens U last week (like we stayed the week
+ in the dorms and such), and i took java... i learned 3 things that week:
+ 1) i dont like java 2) lectures suck 3) not having parents is amazing
+%
+<Yosomono@efnet> chrchr: In MMORPGs, I typically play female characters too.
+ Mostly because if I'm gonna spend a lot of time looking at
+ this person's ass while they're running around, I wanna see
+ something decent.
+%
+<Tv> How does everyone feel about getting all dirty with low-level networking in twisted?
+<Tv> I'm thinking ip, udp etc.
+<Tv> As in, "here's the full packet".
+<itamar> personally, it fills me with an unholy glee
+%
+<itamar> Tv: there are people who will offer to marry you if you release this
+%
+<jml> if only I could eat whitespace
+%
+*** prell (~prell@106.165.8.67.cfl.rr.com) has joined channel #twisted
+<prell> glyph: thanks :-)
+*** prell (~prell@106.165.8.67.cfl.rr.com) has quit: Client Quit
+<liiwi> wow, a drive-by thanking
+%
+<exarkun> radix: also, I need a crossover cable.
+<radix> wth are you using cables for
+<radix> cables are gross
+<etrepum> because sometimes you want bandwidth
+<radix> yeah right!
+<etrepum> well you can go play your infocom games and I'll transfer large files
+%
+On 2003.05.23 18:54, Glyph Lefkowitz wrote:
+> On Friday, May 23, 2003, at 06:45 PM, Bob Ippolito wrote:
+> > The next big thing is to fantasize about nonexistent programming
+> > languages that make good compile and runtime decisions for you.
+>
+> Hey wait! I'm *really good* at that! Let me tell you about this paper
+> I read on linear objects...
+
+[Later that day, on IRC...]
+<radix> dash: dogg! glyph is cutting up
+<radix> dash: latest post to t-p
+<dash> radix: Yeah, i know he hates me and wants me to die
+<dash> radix: why, what's new?
+%
+<dash> cfork: if all you have are nails, there's no need to pick up the
+ biggest hammer with the poison-ivy oil on the handle every time
+%
+<zooko> I once had a party in Amsterdam, and there were two live webcams, and I was worried that people would
+<zooko> accidentally do something on camera that they didn't intend to.
+<zooko> So I spent a long time making great big signs, hard to miss even if you are fucked up, saying
+<zooko> "THERE IS A WEB CAM IN THIS ROOM" and stuff like that.
+<zooko> As soon as the party started some people pushed the cam aside and started doing lines of coke on the desktop.
+%
+<chrchr> datazone-work: Some people dominate the world because they can't hold down a regular job and like the flexible hours that world domination offers.
+%
+<fzZzy> dang. when there are so many layers of abstraction you don't understand it's tough to do a minimal test :(
+<fzZzy> and when glyph writes no docstrings
+<fzZzy> now I know how people feel trying to use woven
+%
+<Tv> Enough rope to shoot a foot in your mouth.
+<_moshez> enough mixed metaphors to grab a bull by its horns?
+<Tv> Enough mixed metaphors to grab a bull and eat it, too.
+%
+<glyph> jml: world needs help
+<spiv> glyph: Sound like you need to put an ad out for a superhero
+<glyph> spiv: all too true
+<glyph> world.save()
+%
+<moshez> the breasts are part of the cognitive dissonance
+%
+<glyph> So, how do I tell distutils to compile/link with g++ rather than gcc?
+<spiv> glyph: By typing your commands IN BLOOD
+%
+<cehteh> >>> foo()
+<cehteh> Segmentation fault
+<cehteh> doh
+<whitestar> cehteh: sucks to be foo()!
+<deltab> I pity the foo()
+%
+<itamar> ambivalent?
+<gus> kinda
+%
+<radix> how many people are we going to get posting to the list "How do I do X? I can't use <perfect solution>, it doesn't fit my design" today?
+%
+<sjj> spiv: where does your family live?
+<spiv> sjj: Singleton.
+%
+<limi> I need some sex to adjust my sleeping patterns
+<limi> the only thing that *really* works ;)
+%
+<sjj> dash: you certainly are an enigma wrapped in a riddle wrapped in a hat.
+%
+<cyli> actually I signed on because i was curious to see what people thought about the stem cell debate
+<radix> cyli: I'm with Dream Theater on that one
+<glyph> radix: You believe that the decision on the debate should involve a melodic but very complex 10-minute sample montage?
+<radix> glyph: Hell yes
+%
+<Alea> Just spent the last 6 months writing const-ridden C++ code...
+<radix> I've *never* had to copy a data structure before returning in any of my code
+<radix> Alea: We're a more, ahh, free-thinking bunch. Like hippies, you know?
+* radix passes some pot to Alea
+%
+<dash> glyph: i don't see anything else you'd want to use '' for
+<glyph> dash: a user named ''
+<dash> glyph: is there a good reason to allow users named that? :)
+<jml> : of course not
+%
+<itamar> stupid useless gods
+%
+<moshez> glyph: but I'd recommend giving him a ban
+<moshez> it's like chops
+<moshez> except better
+%
+<Jerub> Why do people punish themselves with latex?
+<anthony> Jerub: well, you see... oh. you mean LaTeX.
+%
+<cyli> maybe you can help me w/ more of woven's dynamic stuff?
+<cyli> you said it was really cool and you could do all sorts of weird things with it
+<cyli> does that involve lots of javascript?
+<glyph> yes
+<glyph> as well as blood of a Polynesian virgin
+<cyli> i thought the blood of a germanic princess
+<glyph> germanic princess is OK for static pages
+%
+<pr0le> I find that when i get sex regularly, i tend to be more productive as a programmer.
+<pr0le> conversely, when i don't get it regularly, i am more productive as a musician.
+<MoonFallen> pr0le: i'm not a musician. i guess that makes my priorities very clear.
+%
+<allexpro> why does twisted want to destroy the sun?
+<MFen> give it a gimmick in the overcrowded python network framework market?
+<MFen> you know, those feature checkboxes on those comparison pages. [x] tcp [x] udp [x] asynchronous [x] destroyed the sun
+<MFen> and on the right it would be like competitor ------ [x] tcp [x] udp [x] asynchronous [ ] destroyed the sun
+%
+<abram> Anyone up for answering a Deferred question?
+<dash> abram: ask your question now, get an answer later!
+%
+<glyph> the answer, of course, is "fuck Windows"
+%
+On 2003.06.25 05:36, Moshe Zadka wrote:
+> On Wed, 25 Jun 2003, "W.J." wrote:
+> > I really hope twisted is not going to enforce this.
+>
+> Twisted is not about enforcement. Twisted is about mocking people who are
+> using the technology in non-optimal ways.
+%
+<saph> dash: moshe rules!
+<dash> saph: maybe!
+<dash> but he'll have to fight me first
+<saph> dash: it has to be a clean fight, no stilts, no hats
+%
+<Rumor> dash: What is this, the spanish inquisition?
+<dash> rumor: This is me asking you to think.
+%
+<itamar> lets change the subject
+<glyph> itamar: Okay, let's talk about your inadequacies instead
+%
+<tic> horray! twisted is working!
+<tic> now let's see if that IRC client thingy is working as well.
+<glyph> tic: QUICK SHUT IT DOWN BEFORE I ROOT YOUR COMPUTER
+<tic> glyph, NO PLAES DONNT!
+%
+<itamar> I'm half jewish!
+<itamar> the other half is also jewish though
+%
+* rt tries to think back to his college courses. "Elementary Carnivorous Dinosaur Avoidance 101" sticks out as a particularly useful class.
+<chrchr> rt: I think you might be dating yourself.
+%
+<hefzibah> peaceniks never make up their minds - never date one.
+%
+<lament> Slashdot karma, unfortunately, is not real karma, because it doesn't involve the death of the people who have it
+%
+<glyph> spiv: FIX FTP
+<spiv> glyph: I'm a little pissed atm... don't encourage me to write code :)
+<sjj> spiv: pissed as in angry or pissed as in australian?
+%
+<Riastradh> Syntax causes cancer of the semicolon.
+<radix> syntax rules
+<Riastradh> syntax-rules rules.
+%
+<glyph> radix: PEACE AND LOVE!!!
+<radix> why
+%
+<chrchr> What's the word for a potion that makes people horny? I
+ forget the word. Wild mead is supposed to do that.
+<Erwin> alcohol
+%
+<glyph> LordVan: I don't know why people keep using twisted for all this serious stuff. It's a mud with a mailserver.
+%
+<glyph> For example - if you came in here asking "how do I use a jackhammer" we might ask "why do you need to use a jackhammer"
+<glyph> If the answer to the latter question is "to knock my grandmother's head off to let out the evil spirits that gave her cancer", then maybe the problem is actually unrelated to jackhammers
+%
+<Pahan> exarkun: Are you a brain surgeon?
+<exarkun> Pahan: I know where your brain is, and I've used a knife before, if that's what you mean.
+%
+<moshez> nobomb: the rumours that we mutilated and killed people who badmouthed Twisted are completely unsubstantiated
+<exarkun> but please stay out of the garage
+%
+<nobomb> since i learnt netscape started in a garage...i've yet to enter one
+<nobomb> something like that must be spawned from fornication carrion beasts
+<moshez> and what do the carrion beats feed on?
+<moshez> I can't tell you what they don't feed on!
+<nobomb> the limbs of mutilated twisted naysayers
+<moshez> and that's bodies of people we killed for badmouthing Twisted
+<nobomb> why not
+<moshez> because there aren't any!
+<nobomb> lies
+%
+<hypatia> I don't think charmed is so applicable there?
+<moshez> I don't think the Charmed ones got their powers a little later
+<moshez> on the bad part, spiv would die :(
+<hypatia> That's the price you pay for superpowers.
+<hypatia> Nobody gets superpowers and happiness.
+<hypatia> Sorry spiv -- I'm trading up!
+%
+<glyph> what the hell am I going to do with a dozen donuts?
+<dash> make 11 new friends
+%
+<moshez> itamar: I mean, it's fun that I can trace ancestry pretty much to the people who invented flamewars, 3k years ago
+<itamar> moshez: this would explain a lot about you, yes
+%
+<glyph> dizzyd: yeah, microdom.py vs. sux.py :)
+<radix> deathmatch!
+<glyph> radix: sux would totally whup microdom's ass
+<glyph> radix: it would be all scary dressed up in leather and chains and shit
+<glyph> and microdom would have a little bow tie
+%
+<phed> glyph: If I take care of some children, and I tell somebody else, "I just let them do whatever they want"...
+<phed> glyph: then people like you say "what if they KILL somebody! Shriek!".
+<glyph> phed: they actually say that? they say "shriek"?
+<phed> Shriek is the matingcall of people I hate
+<glyph> norway is a weird place
+%
+<dash> soon copyright will be dead
+<dash> and bookmobiles will roam the streets of america freely
+%
+<moshez> jml: I AM A STANDARD
+%
+<dizzyd> dude, life is good once you get the hang of this framework
+<dizzyd> my code just drizzles into modularity
+%
+<dash> First they [the dev. team for Evolution: Worlds, a console game]
+ break mimesis, then they break the first rule of RPGs!
+<glyph> what is the first rule of RPGs?
+<dash> schizophrenic kleptomania
+%
+<itamar> ow ow ow my head
+<fzZzy> gently down the stream
+<dash> merrily merrily merrily
+<fzZzy> itamar is in pain
+<itamar> I am going to kill you all
+%
+<moshez> I want to use ed
+<moshez> but ed has flaws :(
+<dash> i would venture to say that ed is composed entirely of flaws
+%
+<moshez> spiv: I cry wolf all the time, but that's because
+ WE'RE SURROUNDED BY WOLFS!
+%
+<itamar> I must work
+%
+<dash> ooh, i have an idea
+<dash> clone noam chomsky and hire the clones as greeters at wal-mart
+<itamar> I've never been to a walmart
+<itamar> how does that work?
+<dash> itamar: it is a big room full of stuff
+<glyph> dash: "Hello, you bourgeois military-industrial pig! Would you like
+ some coupons?"
+%
+<MFen> irc is sort of a window into the schizophrenic part of the
+ brain, i think.
+* Nafai tries to smash the window
+%
+<dunker> ah so the kqueue reactor doesn't spawn right
+<spiv> dunker: Maybe it needs a full moon in spring, like certain types of fish?
+%
+<jml> watching classic films and reading classic books is worth it, in general, just to appreciate The Simpsons more
+%
+<lac> my problem is that i have the new cisco wireless card
+<lac> and I cannot get it to work
+<lac> with my debian linux. curse it all.
+<lac> also running it makes my dishwasher go nuts
+%
+<BradB> Perl's main appeal is more social than technical. They have fun
+ tricking Perl into doing things we don't even have to think about in
+ Python.
+%
+<avida> like with enumerate(), i would go back to al my code and use enumerate ...
+<avida> im obsessed that way
+<dash> is it rad to be obsessed avida
+<avida> dash: its killer radical, indeed
+%
+<Riastradh> glyph, explain why I'm writing twisted-scheme, then...and
+ making it implementation-independant.
+<glyph> Riastradh: because you are a WONDERFUL PERSON, even though I
+ disagree with you
+%
+<itamar> why does my 1.8ghz pc take 30 seconds to delete start menu items
+<itamar> what is it *doing*
+<radix> contacting the mothership by emanating magnetic signals from the movement of the HD heads across the platters.
+%
+<itamar> oh look, these people are writing a "pragmatic language"! from scratch
+<itamar> someone get me some of those drugs
+<itamar> I don't enjoy reality any more
+%
+<Pahan> It looks like a very cool tihng.
+<Pahan> But ugh, I hate fragile software.
+<radix> Pahan: wtf, you're a l33t C++ hacker.
+<radix> Pahan: you should be used to it.
+%
+<etrepum> worst case you waste 40 bucks, best case it just works.. somewhere in the middle, you learn how to write a kernel driver
+%
+<moshez> deltab: "write to stderr" is not a logging technology
+%
+<radix> hooray! death to privatization
+%
+<fzZzy> css is like putting a bandaid on your SEVERED HEAD
+%
+<dash> well, we try
+<dash> but some of us are more trying than others
+%
+<tenth> The issue tracker that solves the issue of failing to use the issue
+tracker will rule the earth as a living god someday.
+%
+<Acapnotic> dash: how are the cookies connected to glyph's internet again?
+<dash> Acapnotic: wires
+%
+<MFen> i don't want to look at it. but someone else should. seriously
+<MFen> hard work is not my thing
+%
+<Erwin> it provides some thin abstractinos around Python objects so
+ you don't have to screw around with refcounting, but nothing much
+<exarkun> abstractinos!
+<exarkun> the elementary particle of abstraction
+<exarkun> excite them to high enough energy and they release their
+ gluons, resulting in a refactor!
+<exarkun> but don't excite them too much, or you'll end up with all
+ abstraction and no implementation!
+%
+<radix> my misfortune is that Broken Sword crashing in the same spot
+<radix> every time i try to play through it
+<radix> whyyy
+<radix> my computer sucks
+<dash> radix: yeah, this is why i don't buy games that say "broken" on the box
+%
+<Ron> I'm beginning to suspect that statically linking is a bad idea?
+<exarkun> Not if you own a lot of shares of Maxtor or Seagate
+%
+<moshez> IInsanity(moshez).squish(IHandsHaving(radix))
+%
+<radix> what the heck are you talking about
+<MFen> cyberhigh
+<Nafai> MFen: Is that a drug like snow crash?
+<MFen> almost exactly like that, but without the ninja motorcycle chase
+%
+* moshez is insane toad
+<moshez> today
+<moshez> damnit
+%
+<tjs> at least C++ is comparitivly sane
+<tjs> my day job is php
+<exarkun> tjs: uh
+<exarkun> tjs: oh
+%
+<exarkun> btw I hate imap
+%
+<etrepum> python should come with a disclaimer
+<etrepum> that says you may not want to use anything else ever again
+%
+<MFen> want to write my requirements for me?
+<radix> Sure!
+<radix> "show a dancing monkey in the about box"
+%
+<dash> foom: at one level, i'd just say "screw that, let people share objects if they're foolish enough to try"
+<foom> yes, that's called "threading"
+<foom> and everyone is foolish enough to try
+%
+<moshez> I miss the hype!!
+%
+"from experience and months of lurking, I would say the Twisted newbie
+experience is characterised by waves of confusion and euphoria."
+ -- Douglas Bagnall
+%
+<sjj> one day you will understand how it is I came to be the sole
+ owner of this lemonade
+<warner> you killed all the other kids at the lemonade stand, didn't you
+<sjj> hah hah haaah, free enterprise!
+%
+<moshez> exarkun: I will tell saph to uncynicalize you
+<itamar> with a SHOTGUN
+<itamar> the cynicism will leak out the holes
+%
+<chrchr> SamB: pirate is an implementation of Python for the Parrot VM.
+<exarkun> Argh, matey.
+%
+<radix> i like porn
+<itamar> that's not womanizing
+<itamar> that's objectification, closely related to OBJECTIVISM
+<radix> i like it a lot
+%
+<sjj> so i'm trying to keep glyph out of jail and / or financial ruin
+%
+<dash> "some guy in blue sunglasses killed the last guy who worked here. We think he was from the future."
+<glyph> dash: my sunglasses are mirrored grey, actually
+<dash> glyph: the ones you have NOW, you mean
+%
+<MFen> anything that makes glyph go OH SHIT makes me want to buy garlic and silver bullets and get a lawyer
+<MFen> the silver bullets are in case it's a werewolf, the lawyer is in case it's a lawsuit and the garlic is to protect me from the lawyer
+%
+<fzZzy> Yay! Advertising: Internal Server Error
+<fzZzy> my favorite kind of advertising
+%
+<glyph> well, well, well.
+<glyph> it worked.
+<glyph> My desk is in the correct position.
+<glyph> My internet is on.
+<dash> glyph: GENERATE REVENUE
+%
+<exarkun> rt: we could do with an out of control suicide rate
+<rt> unless you're willing to lead the way, I wouldn't go making that
+ recommendation.
+<exarkun> rt: I would, but if I hurl people off a cliff, it's murder, not
+ suicide.
+%
+<itamar> why isn't my mac shipping :(
+<MFen> they ran out of candy canes and gumdrops
+%
+<raph> of course, from a mathematical point of view, "working" and "IMAP" are probably incompatible concepts
+%
+<moshez> dance dance EVIL revolution
+<moshez> this is a game I will invent
+<moshez> it's like DDR
+<moshez> except on HEADS
+<moshez> hahahaah evil
+%
+<ivan> why the *fuck* do we have 2.4ghz devices everywhere when water resonates at 2.4ghz and we're 90% water?
+<ivan> is this a conspiracy theory to kill us all?
+<exarkun> yes, ivan.
+<exarkun> very clever.
+<exarkun> you've found them out.
+<exarkun> you realize you've killed us all, I hope?
+%
+<Acapnotic> I have a problem with edonkey though, and that is that I get hypnotized by the many parallel download meters
+<Acapnotic> one time I spent three days without eating, sleeping, or coding, just looking at the little progress meters and watching clients connect and disconnect and whatnot
+%
+<saph_w> dash: how are you?
+<dash> saph_w: better than i deserve
+%
+<dash> time to get my abstracti on
+%
+<radix> PHP doesn't have interfaces, it has REIFIED PAIN
+%
+<itamar> if I ever write a novel
+<itamar> the chapters will start with quotes from the quotefile
+%
+<exarkun> my knowledge is exceeded only by good looks and success with the ladies
+<dash> exarkun: easy to believe
+<dash> they wouldn't have far to go
+%
+<headh> where python stores its modules?
+<exarkun> internet
+<exarkun> the modules roam free in the valley of IP
+<exarkun> just beyond the IANA peaks and the black chasm of the IETF
+%
+<lament> Listening to your heart? Pfft
+<lament> it's boring
+<lament> thud thud, thud thud
+<lament> and it's always the same beat
+<lament> it's not like it goes boom -kachink-chakachaka-boom!
+%
+<hypatia> Oh well, as long as they aren't rapping in Andunaic...
+<dash> hypatia: actually
+<dash> hypatia: that would be kinda cool.
+<hypatia> dash: Impressive too, considering how small the known vocabulary is.
+<hypatia> Quenya or Sindarin would be doable.
+<hypatia> As long as you like rapping primarily about flowers, natural beauty and grief.
+%
+<slyphon> what is Andunaic?
+<dash> slyphon: Adnaic is the language of Numenor.
+<slyphon> dash: is that in south-east-asia?
+%
+<hypatia> The Noldor don't strike me as a very goth people, but, you know, maybe they've gotten with the times.
+<moshez> hypatia: like vampires, elves don't change
+<hypatia> moshez: They fade though. That's pretty goth.
+%
+<exarkun> "packaged" doesn't mean easy to install or configure
+<exarkun> it means "comes in a pretty box carried by a guy in a $800 suit"
+%
+<glyph> I agree that it would be huge amounts of fun to watch monkeys in $800 suits carrying big shiny boxes that say Twisted do a complicated ballet to the tune of "Money, Money, Money", so if you want to fund it, please send me the video tape
+%
+<parks> ill take ASN.1 over XML any day
+<dash> parks: why?
+<parks> blind bigoted hatred
+%
+<itamar> exarkun: does it do screenshots of empty landscapes?
+<itamar> if not it is NOT BEGUN
+%
+<dash> exarkun: radix is ruined for life i guess
+<radix> no!
+<radix> i will experience WONDROUS JOY for the rest of my life
+<exarkun> radix: of course you will
+<exarkun> you're only ruined from the perspective of sane people
+%
+<moshez> fuckin' jew
+<slyphon> moshez: i think they like to be called 'the messiah-challenged' these days
+%
+<cherub> yay Unspeakable Algebra
+<cherub> I assume the geometry associated with all of this involves strange many-dimensional paralleltopes which are an affront to reason, and through the corners of which unknowable evil seeps into our plane of existance
+%
+<fzZzy> hmm. what happens if an interface inherits from another interface, and I try to adapt an object which declares it implements the subclass to the base class interface?
+<Jerub> the spacetime continuum will shatter, leaving only remnants of the previous inheritance tree to forge out an existance in the rubble of a former great civilisation.
+%
+<glyph> moshez: Your interpretation of the human condition is, as always, colorful and, as always, wrong :)
+%
+<moshez> glyph: I'm always polite
+%
+<z3p> what is a good way to debug crazy errors in C modules?
+<Jerub> z3p: find -name "*.c" -exec rm {} \;
+%
+<Jerub> extremists make middle ground exist.
+<glyph> Jerub: sometimes they salt the earth as they pass over the middle ground ;-)
+%
+<gt3> i took a 2 month coding vacation and went soul searching
+<gt3> i collected a lot of souls..
+<dash> gt3: cool. what are you going to do with them?
+<gt3> sell em on ebay
+%
+<dash> whoa
+<dash> the mexican-flag thingy works like the editors at BYTE used to!
+%
+<radix> "promgrenades"?!
+<radix> that sounds like some terrorist weapon that a high schooler thought up
+%
+<chrchr> see man mount. see spot run.
+<slyphon> chrchr: as long as we don't see "man mount spot"
+%
+<radix> exarkun: I am skeptical.
+<exarkun> radix: Go skeptate elsewhere!
+<exarkun> radix: You're harshing my buzz.
+%
+<PenguinOfDoom> And the app sucks.
+<exarkun> what do I care if the app sucks!
+<PenguinOfDoom> Running sucky apps diminishes honor of your mother.
+%
+<teratorn> everything tastes better with a little internet
+%
+<slyphon> do you know what guido said about why python didn't have an optimizing native code compiler?
+<radix> he said "i like meatloaf"
+%
+<fzZzy> why is the king in yellow paperback 20 bucks :(
+<glyph> fzZzy: I *seriously* hope you mean "The Yellow Sign" or something
+<glyph> fzZzy: if you found an actual copy of The King in Yellow, DON'T TALK ABOUT IT HERE
+%
+<radix> haha! fear my bamboo stick
+<radix> thwap! swip! donk!
+<itamar> donk?
+<radix> itamar: yeah. stabbing in the forehead with a bamboo stick makes that sound.
+%
+<dash> exarkun: CULTURAL OSMOSIS
+<Glammie> Perhaps the expression has percolated throughout a variety of social media without retaining the tag of its orig
+<Glammie> dash, jesus christ.. I just typed a whole sentence, and you say the same damn thing in 2 words. Damn you for your conciseness!
+<dash> Glammie: PERSPICACITY WOO
+%
+<glyph> fzZzy: It always starts with one harmless little branch tag, and pretty soon, you've got a revision in each hand, and you're snorting crushed revisions off the ass of a 12 year old boy you call "revision"
+%
+<MFen> i swear to god c programmers must do #include <buffer_overflow.h>
+* MFen upgrades his servers. again. hooray debian
+<exarkun> MFen: yes! except it's spelled <string.h>
+<exarkun> i think it's short for "... long embarassing string of security vulnerabilities ..."
+%
+<saph_w> i think buildbot should be renamed vlad
+<saph_w> and be given scripts to talk about makeout sessions it's had
+%
+<exarkun> all twisted has in the way of ipv6 support is Twisted/sandbox/exarkun/ipv6.py
+<exarkun> Which someone should rewrite as an internet newapp service and drop in a more useful location
+<glyph> exarkun: hum
+<glyph> exarkun: I suppose we should take our lead from DNS
+<glyph> exarkun: and add a function to the reactor called
+<glyph> listenTCPTCPTCPTCP
+%
+<moshez> we can program in morse code
+<dash> moshez: will you release it under an Open Morse license?!@
+%
+<mesozoic> fzZzy: uh... when you click "Read Next Message", and the entire things refreshes using JavaScript instead of simply opening another page, I think you're adding unneeded layers of complexity.
+<fzZzy> mesozoic: that's awesome!
+%
+<mesozoic> fzZzy: it was the way the whole thing tied together. It was like GOTO graduated and became a design methodism for web applications.
+<fzZzy> there is no design methodology for web applications! that's the best part
+%
+<tenbytes> fag
+<dash> this channel is made of LOVE AND PEACE!!
+<tenbytes> oh
+%
+<glyph> dash: Well, I'll give you a hint. A certain UNIX vendor is going cross-country with an advertising campaign, and warner says he hasn't "been physically ejected from a conference in ages"
+ -- warner and glyph plan a visit to a SCO Q&A session
+%
+<MFen> huh. microsoft has a license _compiler_
+<MFen> i guess you need special tools to inject a program with pure evil
+%
+<blanu> The waste sub-project was/is going to be IRC over Chord basically.
+<arma> will it be "invisible"? will it have a "2" in the name?
+<blanu> Doubtful.
+<blanu> It will probably be rolled in crispy crust of crack though.
+%
+<blanu> I figure if you're going to write a new chat system, you might as well be mostly insane about it.
+<blanu> Since it's doomed anyway.
+<arma> will it be deniable, at least?
+<blanu> No, I plan to take full responsibility for writing it even if it's silly. After all I wrote IRC over Freenet.
+%
+<glyph> I want PB to be a service to rival HTTP, which means that it needs to be able to *do something* when you just type "pb.blahblah.com" and then slather enough drool on the enter key to depress it
+%
+* slyphon has a roomate (best friend from high school) that is going for a degree in being a lazy no-job having mooch
+<slyphon> but in a loveable way
+%
+<nazca> can anyone think of a good method of pursuding my college network admin that installing python and twisted on to the application server would be a Good Idea (tm)
+<radix> nazca: well, why do you need it? :)
+<nazca> i need it for working on software projects that eat more time than the college course ;) i'm not telling them that or they'll give me more work
+%
+<saph> YES I AM A FEMALE
+<saph> FEAR MY BOOBIE POWER
+%
+<grib> don't worry, I have a pentagram around my Aeron
+%
+--> orangecat has joined #twisted
+<orangecat> I was just on my way to the bar to pick up some internet
+ and wondered if everyone had enough enterprise. No facilitated
+ client-based XML quality vector refills? Budweiser?
+<-- orangecat has left #twisted
+%
+<etrepum> what are we plotting?
+<dash> etrepum: world domination
+%
+<raph> imagine how many fewer problems we'd have if everybody on the planet was an asynchronous network protocol programmer
+<clausen> raph: I think everyone would starve :p
+%a
+<kiko> I had a friend who started accessing objects concurrently
+<kiko> he ended up in rehab with a triple X tatooed on his face
+%
+<moshez> glyph is the leerless feeder!
+%
+<exarkun> I'll go add <br> tags at the end of every line
+<exarkun> I think that is how you add spaces to HTML
+%
+<Artimage> I believe a bar chart can confirm my humanity.
+%
+<radix> moshez: you are very nice to me, and girls in general
+%
+<radix> naked naked naked
+%
+<dhess> has anyone written a blog application server in twisted?
+<dhess> i'm tempted to use plone but it's a little overly complicated for my needs
+<exarkun> heh heh
+* exarkun throws himself off a cliff
+%
+<itamar> that was a wasted 30 minutes
+* itamar curses tenth's halloween special
+%
+<exarkun> stay the hell away from my corpse
+%
+<glyph> amiaaaaornot.com
+ [the ipv6 equivalent of 'whatsmyip.com' -ed]
+%
+<coderman> i think blanu disappeared
+<GabeW> wow
+<GabeW> does he have the ring?
+<coderman> is that a euphamism for the clap?
+<wmf> somehow I don't see AaronSw getting the clap
+<coderman> did aaron and blanu go somewhere? there is probably a big conference going on that im blissfuly clueless about like usual
+<GabeW> coderman: is this how blanu and AaronSw got the clap?
+%
+<exarkun> it is hard not to think I am FUCKING INSANELY AWESOME when everything I do turns out so amazingly well
+%
+<slyphon> YAY SEGFAULTING!
+* PenguinOfDoom is continually amazed at how a segfault is such a joyful occurence for Python programmers.
+<itamar> PenguinOfDoom: it is somewhat like seeing someone levitating and then kicking you in the face
+<itamar> it hurts, but at the same time you are distracted by the violation of your concept of reality
+%
+<moshez> pfote: anthopomorphising is the most powerful weapon in the fight against complexity
+<glyph> moshez: don't you mean "anthropomorphism is the most powerful warrior in the fight against complexity"? :)
+<MFen> i prefer to simianize
+%
+<moshez> glyph: I see your problem
+<moshez> glyph: you are trying to do impossible things
+<dash> moshez: nothing else is interesting
+<moshez> dash: perhaps! but impossible things are notoriously hard
+%
+<phed> symbiont: you spot the flamewars as those neverending staircases of posts. and those who say something not flameable, as those with one or none replies.
+<phed> at the end of the staircase, hitler is mentioned.
+<symbiont> yes, hitler is always a component of flamewars
+<symbiont> can we encapsulate hitler in an xml document?
+<slyphon> he'll make sure all of your documents are only in pure german
+<symbiont> achtung baby!
+<slyphon> you could use it for mail, but then you'd just hear the chant over and over...
+<slyphon> "SIG!"
+<slyphon> "FILE!"
+<slyphon> "SIG!"
+<slyphon> "FILE!"
+%
+<SamB> they should label crap "tragedy"
+<saph_w> no, they should label crap "crap"
+%
+* aum sits back while mnet builds the kitchen sink, the earth, the heavens, the beasts of the land and the fowls of the air
+%
+<anthony> anyone that would give out sexual favours to get access to an imap server... sheesh.
+%
+<tmcvs> Commit from sjj (changed 2) in 2 subdirs of Twisted: "add copyright info" msn_example.py, test_msn.py
+<sjj> Now all I have to do is wait for them to deport glyph to syria.
+%
+<ilikewine> that mp3 sounds like that band is from williamsburg
+<ilikewine> its an awful place
+<ilikewine> where everybody wears stripes
+%
+<saph> i think the same drunken circus bears who taught radix to type taught him to drive
+%
+<moshez> exarkun: the user can win by setting ulimit
+<moshez> exarkun: haha you lose
+<exarkun> Uh
+<exarkun> The goal is not to defeat the user.
+<moshez> exarkun: what kind of screwed up software do you write?
+%
+<radix> PenguinOfDoom: *you're* a jew too? goddamnit
+<radix> what is up with you people
+<PenguinOfDoom> radix: Only a halfjew!
+<PenguinOfDoom> radix: I get +5 racial bonus to antisemitism.
+<PenguinOfDoom> radix: It's like half-elves and whatever they do best, archery or something.
+%
+<radix> someone should do a statistical analysis of the religions of Twisted developers
+<exarkun> Doesn't that cvs stats tool emit a graph for that?
+<exarkun> It should
+%
+<fzZzy> do not stare into the xhtml
+<kwaker> it will make my eyes to pop
+<kwaker> and my wife to leave me
+<kwaker> and my hard drive to burn in flames
+<fzZzy> yes
+%
+<exarkun> Nagle is a little gnome who ships inside every TCP stack
+<exarkun> He grabs your sockets and squeezes them
+<exarkun> So the bits can't fit through
+<exarkun> Later, he lets go
+<exarkun> Turning off TCP_NODELAY hits Nagle with a sledge hammer.
+<exarkun> While he's unconscious, no one does the job of squeezing your sockets.
+%
+<PenguinOfDoom> dash left me!
+<PenguinOfDoom> aaaaaaaa
+<itamar> PenguinOfDoom: there will be other men
+* PenguinOfDoom hits itamar with a brick.
+%
+<saph_w> moshe: telling friends to read /. is like telling friends to go to radio shack
+%
+<slyphon> spiv: hey look, if i had to maintain that crap ass protocol i'd be slacking too
+* slyphon suddenly has a horrible moment of clarity
+%
+<frankie> smile! you are on-line at linux-day in italy!!!
+<frankie> say something nice, plz :)
+<aj> hello, this is the Debian Release Manager, please, please
+ try a different distro! (arrrggh, the pressure, the pressure!)
+%
+<PenguinOfDoom> exarkun: I have a desire to rearrange your internal organs in the way you rearrange words.
+<saph> PoD: please keep his kidneys out of his nose
+<PenguinOfDoom> saph: Why?
+<saph> PoD: because i don't want to have to explain it in the holiday cards!
+<PenguinOfDoom> saph: Okay.
+%
+<zooko> I think there should be a "maybe" button next to "ok", "cancel" in all dialogs.
+<zooko> [okay] [cancel] [I'm not sure]
+<LotR> and the dialog would vanish and then reappear?
+<zooko> I'm not sure.
+%
+<danfaust> What makes you a Superjew? Pork bounces off your chest?
+[See http://www.timeoutny.com/427.cover.html -ed]
+%
+<jml> It is like wandering through the desert, for weeks, without a
+drop of water, but knowing that there is an oasis ahead. And all the
+time, I am being stabbed in the face with a blunt spoon by a flatulent
+person.
+ -- jml describes PHP, after using nevow
+%
+<crw> don't talk to teachers when you first wake up, it floods back all the crap you had to do in school.
+<crw> and if the conversation begins with "i'll give you three guesses to figure out who this is", just hang up on them and go back to sleep. :P
+%
+<gus> "Je suis en train d'avoir les presqu'impossibles du travail"
+<gus> it means I CAN'T CONCENTRATE ON MY RESEARCH
+%
+<MFen> give me correctness or give me death
+<glyph> MFen: I wouldn't say that, standing so close to a windows machine.
+%
+<PenguinOfDoom> Grr. Python saps my will to write software. Anything I'd want to write is either boring, impossible or already written.
+%
+<AccorDNGuy> You know, my academic career would've been more interesting if I'd answered my exams with porn stories.
+<AccorDNGuy> "Explain AVL Trees." "Winer undid his zipper, casting a longing glance at Dvorak, who returned his smouldering gaze. There was going to be some serious pole-smoking tonight."
+%
+<dash> radix: are you jewish yet?
+<radix> dash: Not yet
+<dash> radix: me neither! what's taking so long
+%
+<slyphon> then I'm the happiest loser in the phone book!
+%
+* lebowski was talking to a guy in the pub the other night who once got stopped by a gang in NI
+<lebowski> "Are you a catholic or a protestant?" they asked
+<lebowski> "Er... neither, I'm an athiest"
+<lebowski> "... Aye, but are you a catholic athiest or a protestant atheist?"
+%
+<maciej> wow. Spam with ex-girlfriend's name.
+<maciej> that extra little twist-o-the-knife
+<maciej> "my ex is suddenly writing.... and she wants to increase my WHAT?"
+%
+<lapsly> dash has the coolest hat
+<lapsly> he looked like a pimp walkin around chinatown
+%
+<maciej> I like to save my timidity for actual human interaction, where it belongs
+%
+<exarkun> usecrack: --compiler
+%
+<slyphon> has anyone thought about a squid-like caching-proxy server thingy for twisted?
+<`anthony> slyphon: what, hideously complex, consuming enormous amounts of resources, and buggy as fuck? Not particularly.
+<`anthony> Or do you mean instead slimy with long tentacles
+%
+<maciej> I almost took my cat to the vet for a strange skin condition before my girlfriend reminded me cats are mammals
+%
+<itamar> sex is not digital, it is analog
+%
+ * warner wishes for the zillionth time that he could just grep his closet
+%
+<dash> isn't it cute?
+<spiv> dash: Cute like a baby choking on an ice-cream cone that's been rammed down it's throat.
+<dash> spiv: "frees up your hands from holding the ice cream cone, but leaves the bottom soggy"?
+%
+<itamar> is there a libmandelbrot?
+<exarkun> itamar: people who write re-usable software don't spend years hand-tuning a 6 instruction inner loop!
+<exarkun> And vice versa
+%
+<shawn> the highest calling of technical book writers is to destroy the sun
+%
+<hypatia> Why does dirdbm exist, exactly?
+<spiv> hypatia: Because glyph is trigger-happy when it comes to writing persistence systems.
+<spiv> "Hey Rocky, watch me pull a persistence system out of my hat!"
+<spiv> "But that trick never works!"
+<spiv> "This time for *sure*!"
+%
+<_joshua> From what I can tell, there are two kinds of interviewers: ones that ask a bunch of silly questions, you answer them, they take one bad quote and make you look bad
+<_joshua> and the other ones that engage in a long dialogue, discuss back and forth, really understand what's going on
+<_joshua> and then take one bad quote and make you look bad
+%
+<exarkun> today's lesson
+<exarkun> don't strace X in an xterm
+%
+<glyph> exarkun: any thoughts on what to do about actions having consequences?
+%
+<Pahan> glyph: I have no time for your Zen crap! I have hardware to burn.
+%
+<symbiont> been doing industrial C for three years
+<slyphon> wow
+<slyphon> i learned python about 2 years ago and all of my C skillz have left me
+<symbiont> yeah, it's all a hack
+%
+<saph> dash: when did you get a day job
+<saph> dash: i thought you just taught dancing and sold trinkets as part of a band of traveling cyber gypsies
+<dash> i only teach dancing to beautiful women
+<dash> for free
+%
+<_joshua> perhaps we could have some sort of sacrificial goat technology where people decide collectively that someone absolutely must get laid
+<tangra> for the good of the state
+<tangra> kind of like the draft lottery
+%
+<itamar> jml: the Sex Pistols have no songs about Hillary Clinton
+<jml> itamar: an unfortunate accident of history
+%
+<dash> Demoscene? wasn't he a greek philosopher
+%
+<slyphon> dammit! my upstream just sucks
+<slyphon> 20 kb/sec!
+<slyphon> if i'm lucky!
+<exarkun> install an optimization
+<slyphon> how?
+<slyphon> ifconfig eth0 --don't-suck-upstream up??
+%
+<radix> I bit my finger
+<exarkun> radix: Yay
+<radix> and it hurt real bad
+<Riastradh> radix, um, why did you bite your finger?
+<radix> Cuz it was holding a waffle
+<exarkun> radix: Hahaha
+<radix> :(
+<exarkun> radix: Awesome
+<radix> I bit it really, really hard
+<Riastradh> radix, does your finger really look that much like a waffle that you bit your finger instead of the waffle?
+<radix> I had to lie down afterwards
+<radix> Riastradh: no, i wasn't looking at it at the time
+<radix> Riastradh: I was busy stuffing it into my mouth
+%
+<itamar> glyph: did you see the multiplayer go written with twisted?
+<exarkun> itamar: *massively* multiplayer!
+<itamar> yeah right
+<itamar> only like three people in the whole world play Go
+%
+<exarkun> I mean, uh, her character has many unresolved personal issues that she projects onto other people around her without reason.
+<glyph> exarkun: that sounds like pretty much all TV people
+<exarkun> Gumby never took out his unresolved personal issues on Pokey
+<glyph> you don't think so?
+<glyph> I thought that Pokey's whole _life_ was gumby's unresolved personal issues
+<glyph> like this memorable sequence:
+<exarkun> Well, they were always friendly enough on camera
+<glyph> gumby: HAHA STUPID QUADRUPED
+<glyph> pokey: shut up! I hate you!
+<glyph> gumby: FOUR LEGS FOUR LEGS, WHERE ARE YOUR HANDS HAHA
+<exarkun> glyph: I think I missed that episode.
+<glyph> pokey: one day you will be hurt by someone close to you the same way!
+<dash> exarkun: i think glyph watched tv in an alternate universe
+<exarkun> dash: That seems likely!
+<glyph> exarkun: it was right before pokey got a cameo on NYPD Blue
+%
+<slyphon> but then again my temple is so reform it's called "Our Lady of the Immaculate Livingroom"
+%
+<mcunixjr> i was sitting downstairs, and my 2.5 yr old was sitting next to me, she has the Flu, 102 temp
+<mcunixjr> she turns to me and says "i dont feel good"
+<mcunixjr> and at the last word, out came dinner
+<mcunixjr> onto my lap
+<mcunixjr> and onto my Powerbook 12"
+%
+--> foom (~jknight@128.52.220.152) has joined #twisted
+<-- Moof (~moof@horus.kaotix.co.uk) has left #twisted
+%
+<maciej> Poles are wondering why they are paying millions of $$ out of pocket to occupy Iraq on behalf of the US, and not seeing the slightest benefit
+<markp> i believe the benefit is that we'll bump you down a few notches on the list of "countries we'll invade next"
+<brkchrmr> Doesn't Poland get a +5 to be invaded on every roll? ;)
+%
+<MFen> i'm sorry, but does this scream DANGER DANGER WARNING to you? "e"
+<MFen> no it does not. but a big floating eyeball does!
+<exarkun> MFen: you obviously lack an adventurers keen senses!
+<exarkun> "e" strikes the deepest terror into my heart.
+%
+<exarkun> speak of the devil
+<moshez> exarkun: froor
+<exarkun> not you
+%
+<jml> are there any really really good wysiwyg (or close) HTML editors?
+<jml> I mean, amazingly good, XHTML-spewing editors.
+<radix> hahaha!@!@!R!A@!@!@!@!@!#@$!#*
+>>> radix stabs software
+<jml> radix: a man can dream
+<radix> jml: your question fills me with burning rage
+<jml> radix: why is this?
+<radix> jml: i hate web
+<Jerub> radix: you are in #twisted.web
+<radix> Jerub: yes
+<radix> Jerub: that is why I am FULL OF RAGE
+%
+<KevinMarks> 'Our series A round is to help us build out the Other Plane; look at the returns possible once we transcend the mortal universe'
+%
+<maciej> It strikes me that Cthulhu can only effect change by altering the order things are eaten in
+<maciej> just like Alan Greenspan can only raise or lower interest rates
+%
+* Suw has never seen a wall mounted cat.
+<maciej> Suw: give me ten minutes and a stapler and I'll show you
+<Suw> maciej: like to see you try that trick with Fflwff
+<maciej> twenty minutes and a staple gun
+<Suw> maciej: you haven't seen her claws...
+<maciej> thirty minutes, asbestos gloves, and a hydraulic nail gun
+<Suw> maciej: asbestos gloves? that would never cut it. she'd be at your jugular before you could say 'argh'.
+<maciej> forty minutes, a torniquet, and a double-wide roll of duct tape
+<Suw> maciej: she's way too slippery for that.
+<maciej> fifty minutes and a large sheet of Velcro
+<Suw> maciej: ok, that might work
+%
+<Nafai> Sheesh. Why is downloading stuff so hard?
+<exarkun> try typing in real credit card numbers
+%
+<saph_b> radix: minnesotans invented the frozen pizza
+<radix> saph_b: i love minnesotans
+%
+<exarkun> Dang
+<exarkun> A channel even more fascist than #python
+<exarkun> == kick exarkun off #hurd by neal (Rule #1 of 1: no nodding)
+%
+<ivan> i'm almost done rewriting python in python
+<ivan> i can't believe it took those pypy guys years
+%
+<REDROBOT> MEET MY SECOND COUSIN 'ONHOLDTONE'
+%
+<`anthony> I think I shall refer to Guido as "Tallest" from now on.
+<`anthony> I'm not sure who Zim would be. Maybe Ping.
+[Referring to http://pycon.org/images/mastheadphohtos2.jpg]
+%
+<radix> code is for grunts, not software architects
+<jml> architects are merely coders without keyboards
+<radix> yes, lacking keyboards is a sign of prestige
+* jml throws his keyboard at radix
+<jml> I am prestigous
+%
+<exarkun> I have a gig of ram, after all
+<exarkun> and other people are below my threshhold of attention
+%
+<mingus> pynfo: kick exarkun for abuse of power
+<pynfo> You aren't allowed to do that.
+%
+<PenguinOfDoom> So wait, oekaki is also some stupid Java applet that crashes
+ and stuff, right?
+<PenguinOfDoom> I think it probably has builtin tools for drawing anime boobs.
+%
+<mingus> emacs is /da-bomb/
+<mingus> it's exactly all the things i wanted vim to be but never was
+<glyph> mingus: the sad part is, it's exactly all the things bram wanted vim to be but never was; vim is the trophy of ignorance's triumph over laziness
+<glyph> but hey, it's, uh, faster to start
+%
+<dash> slyphon: sure, but this has happened before
+<slyphon> dash: when?
+<symbiont> glyph: such as the working people paying off debt
+<slyphon> dash: and how many vetoes
+<dash> slyphon: dagnab it
+<dash> slyphon: you're harshing my rhetorical buzz
+<slyphon> dash: you have taught me well
+<slyphon> ;)
+<dash> hracht! i gotta stop teaching you stuff.
+%
+<slyphon> why is it that when women get pms and they give you a hard time about nothing that the _LAST_ thing on _EARTH_ you can suggest to them is that they might be the teensiest, weensiest bit on edge because they have PMS?
+<Yosomono> slyphon: Because you are basically telling them that their feelings are the result of chemical imbalance, and therefore unimportant.
+<slyphon> BUT THEY ARE!
+<saph_w> slyphon: because it belittles what they're feeling to being simply a hormonal response
+<Yosomono> slyphon: Good luck buddy.
+%
+<MFen> i think i've finally become an abstronaut
+<MFen> i'm able to break source into its fundamental abstractinos
+<MFen> pretty soon i'll be writing a mud engine just so i can rewrite it again from scratch
+%
+<dash> what the heck, i think i pushed the wrong button in emacs
+<dash> "Pinging loginfo.py (Paraguay)..."
+%
+<symbiont> btw, i've noticed that the word "federated" is not in the Twisted source tree, should i file a roundup on this issue?
+%
+<saph_w> i suppose he would have to find a good contact to the underground prior to his cat transformation so he can purchase a wig there
+%
+<cyli> Is Michael Eisner Trent Eisner's son?
+ [Since people keep asking about this one, say it slowly:
+ Trent Reznor. Trent rEznor. Trent Eizner. Get it? -ed]
+%
+<Yosomono> You guys need to stream pycon online.
+<dash> Yosomono: all the other people trying to use the wifi would hate us
+<Yosomono> dash: It should be a standard feature of the con.
+<dash> Yosomono: Maybe so.
+<Yosomono> dash: This is the year 2004 for cripesake.
+<saph_w> Yosomono: maybe the japs should give us our fucking flying cars, then we'll talk about streaming video!
+%
+<jml> there's a book out called 'implementing CIFS'
+<jml> anyone want to buy it for me?
+<exarkun> why didn't they just implement CIFS and sell a CD containing the implementation?
+<jml> because people like me would re-implement it anyway
+%
+<lemonodor> it's canadian, you know.
+<lemonodor> er, i mean, written in lisp.
+%
+* radix sads at spacelessness
+<moshez> radix: don't sad
+<moshez> radix: happy at contentfulness
+%
+<phobos> but like, when you were 16-22, (maybe you still are), most sexual contact you obtained was through just 'hooking up', i.e. meeting someone at a party, stoned and/or drunk, and doing things for the night only, right?
+<maciej> phobos: no, most sexual contact I obtained was with myself
+<maciej> and that was a long-term relationship
+%
+* radix remembers twisted.web.html.Interface, feels nostalgia
+<radix> wait, no. that's not nostalgia, that's horror
+%
+<exarkun> chrchr: A great man once called cotton the fabric of our lives. Is that not more important than the fabric of our society?
+<exarkun> chrchr: For what is society without livelyhood?
+<jml> exarkun: IRC?
+%
+<saph> what is gentoo again?
+%
+<MFen> exceptions.ImportError No module named win32com.gen_py.565783C6-CB41-11D1-8B02-00600806D9B6x0x1x2
+<czth> i wouldn't name a _dog_ win32com.gen_py.565783C6-CB41-11D1-8B02-00600806D9B6x0x1x2
+%
+<etrepum> Jokes around here tend to get followed by implementations.
+%
+<dreid> jml: i thought warner wanted to be a Problem object that can be passed via jelly
+%
+<chrchr> PenguinOfDoom: Also, what non-sucky HTTP server? What would you use, besides twisted.web?
+<glyphG4> chrchr: roxen!
+<chrchr> glyphG4: Roxen? Really??
+<glyphG4> chrchr: no, not really
+<glyphG4> chrchr: I write all my own crap so I don't have to deal with questions like this
+%
+<radix> INSTALL says that panda will take 1-2 hours to compile
+<radix> <3 C++
+<dash> c++, it gives you free time!
+%
+<PenguinOfDoom> omfg yes, another weapon to stab radix when he claims that X is useful.
+<dash> PenguinOfDoom: what's useful-er
+<MFen> flash cards
+<MFen> and a rotor to display them very quickly
+%
+<KevinMarks> I really like that unicode has a code point for 'snowman with a hat on'
+%
+<moshez> but yes, theoretically Ogg could replace tar :)
+<MFen> you could have subtitles while you're untarring
+<MFen> "Look! another directory!"
+%
+<redheadatwork> Well, our seder consists mainly of the four questions, which I do twice (once the real ones, once a set I make up on the fly), and a lot of food. Sometimes a song.
+<redheadatwork> And an orange on the seder plate.
+<furan> Before you you see:
+<furan> An orange on a cedar plate.
+<furan> take orange
+<furan> You cannot take the orange. It is firmly fastened to the cedar plate.
+<furan> Take plate
+<furan> You successfully take the cedar plate.
+<furan> #joiito: the adventure game
+<furan> "now with graphics!"
+%
+<Suw> oh, i'm having problems thinking in english.
+<Suw> i keep wanting ot type in welsh instead
+<ChrisDodo> pobol y cwm?
+<Suw> lol
+<Suw> dw i'm yn edrych pobol y cwm
+<Suw> cachu ydy o
+* ChrisDodo turns on english subtitles
+<jeanniecool> "probably you'll come?"
+<jeanniecool> "lol"
+<jeanniecool> "duh I'm probably about ready to come"
+<jeanniecool> "catch yo daddy"
+* shiruken sniffles
+<shiruken> welsh is such a beautiful language
+%
+<cablehead> who would win in a fight between a lion and a monkey ( with a bag of rocks )
+<dash> that's boring
+<dash> ask who would win in a fight between a monkey and a pirate
+<exarkun> that's boring
+<exarkun> ask who would win in a fight between a pirate monkey and a bag of lions
+%
+<dash> i am trying to get Asterisk to work
+<dash> it is stabbing me in the face
+<dreid> yes ... i seem to recall that feature in the documentation
+%
+<radix> it's stupid
+<slyphon> it is?
+<radix> the proper solution is to use an alternative implementation of time
+%
+<radix> So, I guess the reason you chose ftp as a discovery protocol is because it's a semi-ubiquitous anonymous protocol that allows people to communicate?
+<edsuom> No, because I was stupid
+%
+<riptor> flashback to 1945, nazi makes portal (how? who cares), demon pops out, us troops find demon, give it a candybar, name it hellboy <- plot
+%
+<dash> i think i want to implement simulacrum in common lisp
+<exarkun> no
+<exarkun> go away
+<dash> i know i know
+<dash> it's a personal problem
+<dash> but therapy is expensive
+%
+<warner> although.. actually several of my projects are violently battling for the dubious honor of being the least likely to turn into cash
+%
+<PenguinOfDoom> saph: What did you write to your mom?
+<saph> PenguinOfDoom: about being happy that she's home fine from the hospital and how i feel lucky she's ok and stuff
+<saph> PenguinOfDoom: and i told her that her bonsai will eventually give fruit, but i don't know if she can eat it
+<itamar> saph: is that a metaphor for grandchildren?
+%
+<MFen> exarkun: my brain is the size of a pickup truck
+<slyphon> HAH
+<exarkun> MFen: ah!
+<slyphon> MFen: that's nothin, Jesus built my hot rod!
+<MFen> hehe
+<radix> psh
+<radix> satan *is* my motor!
+<slyphon> :D
+<MFen> radix: you HAVE been practicing!
+%
+<Logan> Although I'm fighting for it, my boss thinks the customer wants it done in C++ or, even worse, Java.
+<Logan> But I told him it'd quadruple the cost. :P
+<PenguinOfDoom> Logan: What does the customer care, anyway?
+<Logan> PenguinOfDoom: That's what I said. It's like dictating what brand of toothpaste your plumber brushes his teeth with.
+%
+<radix> A VoIP server "powered entirely by stabbing, that I made out of this gun I had"
+%
+<exarkun> I can't tell if HP-UX sucks /even more/ than last time I used it or if somehow my terminal settings are causing non-deterministic behavior
+<exarkun> For example!
+<exarkun> T.................................................................................SSSS........Changing password for jcalder9 on NIS server
+<exarkun> Old NIS password:
+<exarkun> I hit <enter> and the tests proceeded
+<exarkun> Do we call passwd in our tests or something? :)
+%
+<itamar> 22 bugs and I'm a free man
+<PenguinOfDoom> A slave contract under an entymologist?
+%
+<slyphon> slyphon: mock mock mock
+<PenguinOfDoom> slyphon: Did you grab the wrong end of a mockery gun?
+%
+<PenguinOfDoom> Being enlightened gentlemen, we split all programming languages into two groups, sucks and doesn't-suck and put all of them into the first group.
+%
+<itamar> what are you going to do at cisco?
+<exarkun> rot and die, I bet
+<PenguinOfDoom> itamar: IOS debuggery.
+<exarkun> woo I win
+%
+<SamB> what do interfaces do when you call them? is that even allowed?
+<exarkun> welcome to the year 1973.
+<exarkun> callable interfaces roam the surface of the earth
+<exarkun> humanity has fled underground
+%
+<dash> consumption does not create wealth, production does
+<dash> people in China have noticed this, people in America have not
+<moshez> consumption is good
+<moshez> dash: creating wealth is not an intristic value
+<dash> moshez: being naked, cold, hungry, and defenseless isn't either
+<dash> moshez: but if you don't create wealth, you will be those things!
+<moshez> dash: are you naked ?
+<moshez> dash: wait, that came out wrong
+* moshez hides
+<dash> i am not h3x
+<moshez> anyone adds this to quotes, I hunt you down and kill you
+%
+* kev wonders if having people checking output by eye counts as a valid unit test
+<MFen> if you can attach electrodes to them and force them to do it every time
+<exarkun> and they have to turn red when it fails
+%
+<RemyWork> http://web.archive.org/web/20030608082636/http://www.movabletype.org/commercial_license.shtml
+<RemyWork> I love having to use the wayback machine to see what my rights are
+%
+<exarkun> I suspect the performance of this irrelevant task is highly sensitive to implementation decisions
+%
+<radix> I just downloaded the fruitiest anime ever
+<radix> it's .. girly
+<radix> it's about a girl's school, and about a "sister" system between upperclassmen and lowerclassmen, and .. and... *twitch*
+<radix> there isn't any hitting!
+%
+<mwh> i wonder if i should post an "are you serious" comment
+<radix> mwh: that's a lot nicer than the comment I was thinking up
+<radix> which was something along the lines of "Holy shit, I'm sick of the horrible crap that's showing up in this God-forsaken cookbook."
+%
+<mamamusings> grades are due by saturday
+<mamamusings> that means i have to at least pretend to evaluate them
+<crw> i KNEW teachers talked like this when students weren't around :P
+<mamamusings> hell, i talk like this when they *are* around
+<mamamusings> it's good to be tenured
+%
+<moshez> On January 8th, 1977, Amber Nicole Benson came into this world -- more
+<moshez> specifically, she was born in Birmingham, Alabama.
+<radix> who the heck is she, anyway?
+<moshez> radix: you know what's shocking? technically, you and I share the same universe
+<radix> moshez: I fight to make that untrue every day
+%
+*** warner has joined channel #twisted
+<glyph> warner: newpb!
+<kenaan> Twisted: warner * r10709 sandbox/warner/ (10 files): revamp exceptions, remote calls kinda work now
+<exarkun> OMG
+<glyph> see everybody? now _that_ is the kind of response I like
+%
+<exarkun> it's really too bad people live so long
+<exarkun> and that it is generally considered immoral to experiment on them
+%
+<Tv> I want an incrimental knifi.
+<exarkun> Able to slice multiple things simultaneously without making its user interface non-responsive!?
+<exarkun> SUCH A THING SURELY COULD NOT EXIST
+<Tv> exarkun: Yeah, except it would a poor weapon, because it COULD NOT BLOCK!
+%
+<SamB> few people know the secret of growing donuts
+%
+<hypatia> I distrust projects that require you to socialise with the developers in order to learn how to use them.
+%
+<radix> well hey, I'm getting back on sane schedule
+<radix> I'll probably stay up until about 5pm today
+%
+<spiv> I am confident some people should be made to feel pain, itamar's possible insanity notwithstanding.
+%
+<dash> for some reason i keep putting off becoming an alcoholic
+%
+<itamar> if I got a cookie for every day I didn't work
+<itamar> I'd be radix
+%
+-!- itamar2 [~itamar@pool-162-83-253-243.ny5030.east.verizon.net] has joined #twisted
+<dash> argh
+<dash> who left the robot clone factory switched on
+%
+<markp> copy editors can blow me
+%
+<itamar> the question is
+<itamar> do I *really* need five more tshirts with monkeys on them
+%
+<`anthony> Yah, yah, debian has advantages, but their glacial release cycle is not good.
+<Jerub> if you want me to go around aj's house and kneecap him, my paypal account is stephen@thorne.id.au
+%
+<moshez> dash: greet me into the 21st century!!
+<dash> moshez: it has been the 21st century here for a while! did .il daylight savings just kick in?
+%
+<dash> brb fighting pirates
+%
+<dash> radix
+<dash> er i mean, twisted.lore
+%
+<mattcamp> Anybody know why twisted.protocols.toc is deprecated?
+<exarkun> because the toc protocol itself is deprecated
+<dash> is TOC really deprecated?
+<dash> %google deprecated TOC AIM
+<pynfo> deprecated TOC AIM: http://twistedmatrix.com/documents/current/api/twisted.protocols.toc.TOCClient.html
+<dash> google is now useless
+%
+<itamar> I bet simulating glyph wouldn't be hard
+<itamar> "We just make a <noun> that will <verb> the <other noun>! it will be awesome! I can do it in a week!"
+%
+<PenguinOfDoom> slyphon: I am torn between going to Quizno's to buy a sub and brutally murdering you.
+%
+* slyphon thinks red hat should change it's motto to, "Eh, it's good enough"
+%
+<radix> facts are awesome
+%
+<PenguinOfDoom> You pigfuckers are sitting there, staring at me with etrade.com open, waiting for JUICY INSIDER INFO
+<exarkun> I use ameritrade.
+%
+<dash> dizzyd: yeah, i got bored of that whole college thing after a while
+<dash> so i graduated
+%
+<jimbug> You know, I would knock the curses author over the head if he didn't invent rogue.
+%
+<glyph> exarkun: you could just write a C module that would do all that ugly dl module crud
+<exarkun> glyph: yea, but then I'd have to write a C module
+<exarkun> Py_Incref in Python is neat, I think :)
+<glyph> exarkun: that guy who wrote pyrex is spinning in his grave, and he isn't even dead
+%
+<morning> yes, but i almost wrote a book on orthogonal persistence, until i realized i couldn't spell it.
+%
+<radix> ayn rand had sex?
+%
+<glyph> I am going up through levels of abstraction so fast, reading from top to bottom, I am worried about getting the bends
+%
+--- iratsu gives channel operator status to dash glyph rev_bot
+--- Users on #ddb: @glyph @rev_bot @dash @iratsu
+<iratsu> yay communism
+%
+<exarkun> I don't think I even knew what XML was the last time I used DOS EDIT.
+%
+<slyphon> wtf is NIH?
+<Tv> slyphon: tla
+<Tv> :)
+<slyphon> Tv: duh
+<`anthony> slyphon: tla for Not Invented Here.
+<orbitz> slyphon: Nice Illegal Honey
+<slyphon> ah
+<Tv> slyphon: that's not a tla!
+<Tv> saying something is a tla implies "go look up it up in the standard places"
+<orbitz> Don't Upset him
+<Tv> orbitz: Your Honey is a him?
+<slyphon> oy
+<orbitz> Tv: honey you eat!
+<Tv> orbitz: EWW!
+%
+<dialtone> I can even run python on my clock
+<dialtone> and my watch
+<dialtone> and have my watch sync with my clock
+<exarkun> what time is it right now
+<dialtone> don't have my watch on right now
+%
+<e> birthday paradox cake
+<exarkun> e: Is that the paradox where, if you have more than 30 people in a room, they'll eat your birthday cake?
+%
+<saph_w> my shirt has a moose
+<radix> family channel
+<saph_w> wth
+<saph_w> moose moose mooose
+<radix> jesus fucking H you've got a mouth on you
+%
+<Nafai> A coworker saw my machine once; I was using the Apple II xscreensaver with the Twisted Quotes as the source
+<Nafai> He asked, "Is that glyph's screensaver?"
+%
+<MFen> actually, #python kinda makes sense
+%
+<Yosomono> why does every discussion of survival of the fittest end with hitler?
+<Yosomono> goddamn nazis have ruined everything
+%
+<saph> PenguinOfDoom: you sap energy from people with evil eye rays
+<saph> you are all e_e----------
+<saph> and the other person is :o
+<saph> and then they are -_-
+<saph> and you are ^_^
+%
+<glyph> people would just roll dice all the time in chat rooms
+<glyph> for *no reason*
+<glyph> because it was a feature of the system that was added for RPG players
+<glyph> and the dice-rolling syntax was pretty involved
+<glyph> infinitely involved, actually
+<glyph> it was a complete RainMan interpreter
+<glyph> you could backdoor the whole goddamn system with the dice roller
+<glyph> user: "I'd like to roll some very big dice"
+<glyph> system: "sure, maybe you would like an admin console and some dev tools to help you manage them"
+%
+<faisal> [sushi is] one of the 3 essential food groups for the networking community
+<kiad> what are the other two?
+<faisal> sushi, caffeine, ietf drafts (for fiber)
+%
+<radix> oh no the galaxies
+<radix> they're going to collide :(
+<radix> BUT PHYSICS SAVES THE DAY
+%
+<dash> there is some law of thermodynamics that says you can't pump all the stupid into one container and expect it to stay there
+<dash> diffusion, or something
+<exarkun> we need a membrane
+<glyph> a membrane with a gun
+%
+<Cerin> and I thought mono was an up and coming technology
+<capnSTABN> your sister gave me mono
+<Cerin> she is pretty tech savvy
+%
+<radix> My computer is gone :-(
+<exarkun> radix: ono!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+<exarkun> radix: wait
+<radix> :-(
+<exarkun> radix: I am suspicious.
+<radix> Why
+<exarkun> radix: Well, let's see
+<exarkun> radix: LOOKTHEREYOUARE
+%
+<foom> wait when was the earth created?
+<flophouse> just look at the expiration date
+<flophouse> it's on the bottom, under the ice cap
+%
+<warner> at a touchscreen voting platform, nobody knows if you're a dog..
+%
+<Tv> thomasvs: why not just talk some authentication protocol to the other host?
+<thomasvs> Tv: why use something arcane and difficult when I have THE POWER OF TWISTED ?
+%
+<brian_> I'm pretty sure from twisted import __version__ will work
+<slyphon> or i guess you could do that
+<brian_> I import dictionaries
+<slyphon> really? do you have to declare them in customs?
+<dash> slyphon: You are an inspiration to me
+<dash> slyphon: the next language I will design will have an alternative to the import statement
+<dash> slyphon: 'smuggle'
+<MFen> dash: no, that should be your execfile replacement
+<dash> from america.south smuggle guns, drugs, dictionaries
+<slyphon> :D
+<MFen> hah
+%
+<MFen> why is programming so *hard*
+<radix> because you try to do it on windows
+<MFen> radix: that is because i already beat the end guy on unix
+%
+<exarkun> If I can make just one person blow chunks, I'm doing my job right.
+%
+<itamar> our manager was looking for you
+<coworker_home> oh? just now?
+<itamar> I think he got your cell# off someone though
+<itamar> yeah
+<coworker_home> ah. good thing my cell is dead
+(Names changed to protect the innocent - Ed.)
+%
+<warner> huh. this one proposition is funded by large corporations on one side, and 204 lawyers on the other
+<glyph> warner: wait - proposition? are you IRCing from inside a voting booth?
+<glyph> those new diebold machines must be awesome
+%
+<Karnaugh> my attempt at implementing Ramanujan went horribly wrong
+<Vhata> because Python isn't the best language for reincarnating tubercular indians?
+%
+<Vhata> maybe I should write my own operating system, where you CAN write to sockets
+<Vhata> my operating system will have beer and hookers, too
+<Vhata> in fact, screw the sockets
+%
+<dash> mmm, ken macleod
+<dash> if I had a "People I Would Be Most Likely To Engage in Apocalyptic Anime-Style Battle With" list, he would be #1 on it
+%
+<glyph> I am going to break with tradition and make one rule here
+<glyph> as long as I'm still active, nobody say anything supportive of Bush
+( ... later ...)
+<chrchr> scout^2: Give us a fact that ties Iraq to 9/11 and you will not be kicked.
+<scout^2> well shit man.. if its gonna come down to that..
+<-- scout^2 has quit ("www.twistedmatrix.com, www.kwikdeath.com")
+ [Note the instant quit when *facts* entered the discussion.]
+%
+<hypatia> spiv: get the tshirt. "Beats me, I'm an arch user." Good for all manner of situations.
+<spiv> hypatia: With a companion shirt "Beat me, I'm an arch developer" ;)
+%
+<orbitz> radix: do you know what it's like to be a team player?
+<radix> orbitz: No. I hate you.
+<orbitz> :(
+<orbitz> radix: well i'm a team player
+<orbitz> so i hate me too
+%
+<blanu> arma: What's bamboo?
+<arma> blanu: some guy named sean rhea from berkeley has been pimping it on p2p-hackers
+<blanu> What's the interesting thing about it?
+<arma> blanu: apparently it works.
+<arma> blanu: seems pretty novel to me. :)
+%
+<tjs> I want to know
+<tjs> why are my pants a topic of conversation?
+%
+<glyph> How was your sunday? Relax at all?
+<exarkun> I played _Silent Hill_ most of the day
+<exarkun> On the one hand, you could say that is relaxing
+<exarkun> On the other hand, no, no you can't
+%
+<arg> argh, my wife calls to complain about her mother while im in the
+ middle of trying to understand someone elses metaclasses
+%
+<Moof> I'm tryign to compile pyopenssl
+<Moof> but there's a syntax error in Python.h
+<mwh> this seems unlikely, on the face of it
+%
+<vit--> dash, do you have any recommendations for python jabber libraries?
+<dash> vit--: kill yourself now
+%
+<orbitz> once i found two people having MUSH sex though
+<orbitz> ieee
+<orbitz> i was 13
+<orbitz> scared for life
+<orbitz> the yellow font burned into my soul
+%
+<exarkun> radix: Are you ready to get a tummy host yet?
+<Tv> tummy hosting is that thing the Jaffa do in Stargate, right?
+%
+<dialtone> my monitor can do Mhz
+<dialtone> in horizontal refresh though
+<exarkun> dialtone: hey that gives me an awesome idea
+<exarkun> I am going to turn my monitor on its side
+%
+<tjs> jml: You are going to die one day.
+...
+<tjs> And when you die, part of what makes up your being is the knowledge of how
+to write Java.
+<tjs> I don't have that problem.
+%
+<chrchr> exarkun: Note that all kryptonite locks are vulnerable to hacksaws.
+<exarkun> chrchr: That's why I have another lock too.
+<exarkun> chrchr: How many thieves carry around _two_ hacksaws?
+%
+$ php sucks.php
+
+PHP EQUALITY -- An experiment:
+0 == "": TRUE.
+0 == "0": TRUE.
+"" == "0": FALSE.
+'none' == 0: TRUE.
+%
+Kragen Sitaker: sub f{grep{(1x$_)!~/^(11+)\1+$/}2..pop}
+Itamar: that looks scary
+Kragen Sitaker: the haskell version is just as short and nearly as opaque
+Kragen Sitaker: but even more inefficient!
+%
+<exarkun> it's not drugs, it's ubuntu
+%
+<arg> i think a significant percentage of twisted apps begin as the logbot example
+<chrchr> Twisted is a fantastic framework for building logbots.
+%
+<mumak> hmm. I think my flatmate is asleep.
+<Brend> You should take this opportunity to glue all the furniture to the ceiling
+<mumak> Brend: well, I have to live here too
+<mumak> Brend: and besides, I'm on holidays. That sounds like too much work.
+<Brend> If you just glue your flatmate to the ceiling, you achieve the same thing with less work, and no impedence to yourself!
+<mumak> you present a strong and compelling case
+<mumak> however, I think my flatmate wouldn't appreciate it.
+<Brend> Anyone who can't see the value of being glued to the ceiling deserves punishment by ceiling-glue
+%
+<MFen> i bet i could beat him at football though
+<MFen> or shoe tying
+<MFen> i'd kick his ass at shoe tying
+<MFen> and then when i was done tying my shoes i stand up and shout IN YOUR FACE. IN YOUR FACE, PI BOY. SEE THESE SHOES? NOT COMING OFF. WHO'S THE SAVANT NOW BITCH?
+<MFen> and do a little dance
+<MFen> i bet he can't dance either
+%
+<glyph> kevc: are you volunteering to maintain it, hmmmmm? :)
+<kevc> heh, no, I have no time free at present
+<glyph> kevc: TOO LATE
+* glyph slaps the manacles on kevc
+<kevc> glyph: last time someone played "tag you're it" on me, I ended up
+ running some uni computing project for three years
+<kevc> went to the pub, woke up with a root password written on my arm
+%
+<tjs> I cant do anything
+<tjs> im running XP atm
+%
+<rik> ew.
+<rik> a python packet filter.
+<rik> that'd have almost windows-like performance.
+<afshar> well, it would be for windows
+%
+[In response to http://journal.jafo.ca/sw-20030328-18biganno.jpg]
+<Brend> Wow. You guys are younger than I thought.
+<dash> Brend: glyph's on like his third host body
+%
+<hypatia> Hey, hidden bonus of having actual named maintainers is having
+ people to assign bugs to...
+<hypatia> Of course, it always ends up being exarkun anyway.
+%
+<arg> i know a guy whos last words were "brb, bout to go h@x this streetlight"
+<arg> knew
+%
+<glyph> saph: loving relationships don't involve windows.
+<saph> glyph: they do if there is a safe word involved
+<PenguinOfDoom> "GENERAL PROTECTION FAULT IN MODULE KERNEL32.DLL SYSTEM CRASHED ERASING DATA NOW" "Firetruck! Firetruck!"
+%
+<zooko> https://yumyum.zooko.com:19144/pub/emacsirc.png
+<zooko> ^-- screenshot of my beautiful Ubuntu desktop
+<teratorn> why must i accept your phony ssl certificate?
+<zooko> you don't have to if you don't want to.
+<zooko> In fact, I recommend that you reject it. Because it could be a Man In The Middle attempting to show you a phony screenshot of my xemacs session.
+%
+<Yosomono> aron: So how are you different from the fascists again?
+<aron> I look shity in borwn shorts
+%
+<exarkun> did glyph tell you about the book we saw at the bookstore over the weekend?
+<exarkun> on the front it said
+<exarkun> Java: Principles in Object Oriented Programming
+<exarkun> on the side it said
+<exarkun> Java POOP
+%
+<moshez> glyph: hello tiny person!
+<moshez> are you tiny and squishy today
+<glyph> moshez: You ask questions that are difficult to answer sensibly
+<moshez> glyph: yes! because I am evil
+<glyph> moshez: for example, "no, I am massive and hard" might give the wrong impression
+<moshez> glyph: urgh
+<moshez> the mental goggles they do nothing
+%
+<foom> who's going to the Time Traveler's Convention next weekend?
+<zirpu> i already went. :-)
+%
+<Tv> Möö
+<tazle> Möö?
+<ValarQ> wtf-8
+%
+<anthony> time to say goodbye fedora, hello whorey weasal (or whatever the fuck it's called this week)
+%
+<Brend> glyph: I see you are proactively prepared to leverage the horizontal market opportunities of the end of all life. I'm impressed.
+%
+<radix> everything in the world should have butter in it
+%
+<itamar> WebSphere MQ!
+<itamar> More enterprise than William Shatner!
+%
+--> freakazoi1 (~Sean@pat100.wirelesssecuritycorp.com) has joined #p2p-hackers
+<freakazoi1> stupid wireless
+<-- freakazoid has quit (Nick collision from services.)
+--- freakazoi1 is now known as freakazoid
+%
+<exarkun> I bet francis bacon would go well with orange juice waffles
+%
+<exarkun> (?:PARTOFSPEECH<adjective>(\w)(\w)+y) (?:SEMANTICWEB<noun,mammal,small>\1\w+)
+<warner> next you're going to tell me that those are actually valid Perl6 regular expressions
+<exarkun> yes, except PARTOFSPEECH is a unicode character with a glyph like a speaking mouth, and SEMANTICWEB is a unicode character with a glyph like cthulu
+%
+<MFen> you needed to kill -USR1 duh
+<MFen> n00b
+<PenguinOfDoom> Can I make a saving LOL? :(
+<PenguinOfDoom> USR1 made gnome-settings-daemon die
+<MFen> dude that's because you didn't init 5 first n00b
+<MFen> btw, i'm making this crap up
+<PenguinOfDoom> btw, I'm planning the destruction of Fresno
+<MFen> can i provide you with maps?
+<saph> hooray
+%
+<MFen> Tv: how do you know when sarge is going to be released?
+<Tv> MFen: If it ain't out by debconf, there will be a public lynching ;)
+%
+<itamar> ""As a champion of the free-enterprise system in Congress, Chris Cox knows that a free economy is built on trust," Bush said at the White House as he introduced the third man in his tenure to lead the [SEC]."
+<itamar> apparently the guy is a fan of Ayn Rand
+<itamar> also a fan of large contributions from corporations
+<dash> of course
+<dash> that's what it just said he's a champion of, right?
+<dash> free-enterprise system in Congress
+%
+<winjer> but i made a point of learning as little as possible
+<winjer> i spent most of the time chasing women and smoking pot
+<saph> hooray
+<winjer> if i'd smoked less i might have caught some
+<saph> winjer: did you go to hampshire college or something
+%
+<dreid> heh ... worst name for an interface ever ... ITem
+<warner> oh, I don't know, I bet INterface would be worse
+<warner> implements(INYerFace)
+<dreid> IStabber(dreid).stab('warner')
+<warner> registerAdapter(lambda victim: dreid, type('warner'), IStabee)
+<warner> heh. the PEP246 equivalent of "nyah nyah, no tagbacks!"
+%
+<radix> and long words are good words
+<Brend> But what about those of us who have hippopotomonstrosesquippadeliophobia?
+<radix> sux to u
+<radix> (to put it into terms you'll understand)
+%
+<jafo> PenguinOf: Ha ha. You listened to a doctor! Serves you right.
+<jafo> I mean, look at it this way. They spend at least 6 years in school, right?
+<jafo> If they're so hot, why can't they graduate in 4 years like everyone else?
+<jafo> Besides, why would I want to be a doctor when I could be a MASTER?
+%
+<_radix_the_nun> I write things on 3x5 cards then smoke them to learn stuff
+%
+<PenguinOfLove> And when I strike, the kids with their "lol" and "ur"
+ will scream "oh, please, Pavel! Do not degrade and destroy our beloved
+ language!"
+%
+<Brend> Whoever chose the title "A Gentle Introduction to Haskell" is
+ obviously accustomed to wrestling bears in piranha pits or something.
+%
+<dash> halfoff: what's the problem?
+<dreid> dash: his spider is dying.
+<dash> dreid: quiet you
+<halfoff> i have a mexican redknee tarantula that escaped for about a week i found it this morning very weak and slow moving is it molting or dying
+<dreid> dash: told you.
+<dash> dreid: SIGH
+%
+<seberino> dash: my zope class prof said real businesses don't do
+ javascript since not professional so i happily neglected it
+<dash> ...
+%
+<dash> glyph: what are _you_ doing up? you have to be at work in the morning
+<dash> glyph: you know, to tell me what to do
+<glyph> dash: I got about 30 hours of sleep this weekend, I'm good
+<dash> glyph: that's no way to live, man
+<glyph> dash: Yeah, but you know me. I'm happy with a ghastly un-life; a mockery of what it means to live
+<dash> glyph: You must be using "happy" in a figurative or metaphorical sense.
+%
+<ph3nyx> mfen: my gvim configuration under windows is wacky
+<MFen> ph3nyx: you should see mine. i keep it in version control :)
+<MFen> 172 lines
+* bear keeps his entire dev config in version control
+<KragenSitaker> i keep my entire living room in version control
+<MFen> yeah. i mean, i keep my desktop backgrounds in version control too, so maybe that's not a very strong point
+<ph3nyx> kragen: that's gotta be a pain in the ass for branching
+<ph3nyx> copying your living room isn't an O(1) operation, no matter what the SVN docs say
+<MFen> svn diff -r172:171. "Dammit! Who moved my chair."
+* bear chuckles
+<KragenSitaker> i tried keeping my entire front yard in it too, but it kept leaking gasoline from the lawnmower
+<KragenSitaker> turns out CVS was expanding a $Id$ in the wheel assembly that would puncture the gas tank
+<MFen> you need svn:flammable 1
+<MFen> heh
+<KragenSitaker> so I decided that was too dangerous and scaled back to my living room
+<KragenSitaker> now i just make occasional tar files of the whole house and back them up with rmsync
+<KragenSitaker> which is the version of rsync for matter
+<MFen> KragenSitaker: have you ever considered branching yourself?
+%
+Jerub|the best advice anyone ever gave me was when i was a fledgling linux geek.
+Jerub|"learn vi"
+Jerub|the worst advice anyone ever gave me was "install mirc".
+Jerub|and I still, to this day, curse that man.
+%
+<cracauer> Potatos are for throwing. If god had intended for them to be eaten they would be square.
+<dbutts> How many naturally occuring edible things are square?
+<dbutts> Apart from fiendishly expensive japanese watermelons?
+<cracauer> Chocolate bars :-)
+%
+<subterrific> what happened to twisted.reality ?
+<dash> subterrific: nothing
+<subterrific> is it running somewhere?
+<dash> no
+<dash> that would be something! instead of nothing
+%
+<tjs> I say we pull their bluff
+<tjs> spam that is
+<tjs> go pro-spam
+<tjs> if everyone spams everyone, then spam will nolonger be effective
+<tjs> and it will stop
+<tjs> and so will the internet, and we can all farm tomatoes
+<tjs> yay tomatoes
+%
+[dash referring to Alan Cox's quote]
+<dash> what's 6mb of unauditable crap
+<PenguinOfDoom> dash: You have three guesses.
+<dash> PenguinOfDoom: your gnucash budget?
+%
+<jotham> you guys are like ADHD vultures, swoop in, devour my problem, leave me
+bewildered, then go off to the next corpse
+%
+<glyph> In the sentence "mang I need to get some cheetos up ins", what
+ part of speech is "up ins", and what function does it serve? It
+ seems to me like "i need to get some cheetos" would be sufficient
+<exarkun> It serves to disambiguate from the case where one merely
+ needs to procure rights to a future shipment of cheetos, most likely
+ to be resold before delivery is taken.
+%
+<radix> php thinks 0 == "Foo"
+<radix> why
+...
+<moshez> radix: as consistent and clear PHP is, it has its problem areas
+<moshez> radix: wait, no, I can't say that with a straight face
+%
+<jotham> something i coded was just on sky sport news
+<jotham> shame it was a horrible C++ nightmare
+<mwh> is debugging nested templates a sport now?
+%
+<mumak> Python totally needs to find a use for É
+<spiv> mumak: dude
+<`anthony> mumak: range!
+<spiv> mumak: There's *already* an ellipsis type in Python.
+<spiv> mumak: Put 2 and 2 together!
+<`anthony> spiv: but the ellipsis type is useless.
+<spiv> `anthony: Clearly unicode would fix that!
+%
+<glyph> WOOO
+<glyph> What the *crap*, how does this work
+<spiv> glyph: LD_PRELOAD
+<glyph> fuck, why is everything horrible
+%
+<radix> hrm, I meant to say <3, but I guess maybe <4 means extra <3.
+%
+<tjs> http://www.animalcaresystems.com/
+<tjs> about 20 crates with this logo just got dumped outside our office
+<tjs> full or rack-mounted mice-containers
+<tjs> unfortunately for me, I have an insatiable curiosity. and when
+ someone unloads 2 shipping containers of extremely high-tech mice
+ containment systems on my doorstep, I just have to know whats going on
+%
+<mumak> Jerub: the trial command line isn't so much of a swiss army knife as... well, Dad's old toolshed.
+<mumak> you never know what you'll find there. there's bound to be some cool stuff, but you can't be too sure whether it will work. everything is either greasy, dusty or both, and nothing is where you expect it to be.
+%
+<justinj> It is difficult to assess the current state of the world when things don't fail consistently.
+%
+<dash> glyph: i live with my two younger brothers
+<dash> it is like getting a graduate course in techniques for annoying people
+<dash> that is why i thought of SMS
+%
+tekNico: Some guy has ported Stan to Turbogears: http://blog.develix.com/archive/2006/01/01/stan-turbogears-continued/
+idnar: that's a bit backwards
+idnar: I wouldn't say anyone ported anything
+idnar: it's just that turbogears has pluggable templating, and he plugged stan in
+tekNico: Hey, either port or plug, it's still a four letter p-word.
+exarkun: poop
+%
+01:10 < KragenSitaker> PenguinOfDoom: are you watching american politics? (< PenguinOfDoom> ugh just when I thought this show couldn't get any worse. Torture and obvious lip-syncing.)
+%
+<zooko> I pay attention to Linux development, mostly starting with lwn.net and its "Kernel" page every week.
+<zooko> I'm often reminded of the adage about sausage and legislation.
+<zooko> I use Linux, and I'm happy with it, but the more I learn about the development process the less comfortable I am.
+%
+<foom> haha, OSX had a suid tool called "dsidentity" which checked your privileges by looking at the "USER" environment variable.
+<dash> that is bad
+<radix> woot
+<radix> /Library/Receipts/MacOSXUpdateCombo10.4.3.pkg/Contents/Resources/postflight_actions/dsidentity.sh
+<radix> haha
+<radix> and the contents of that script are /bin/rm -f "$3/usr/sbin/dsidentity"
+<landonf> What happens when you take a bunch of Mac developers and drop them into UNIX-land ?
+<dash> landonf: hilarity ensues
+%
+<dreid> inviso_: of course in an alternate timeline you're also a 12 foot tall ninja dinosaur.
+<inviso_> oooo, excellent! I like that one better. Can I order that with fries?
+<dreid> you think when you're a 12 foot tall ninja dinosaur you're going to be a herbivore?
+%
+<PenguinOfDoom> wtf
+<PenguinOfDoom> I just forgot that I watched Spiderman 2.
+<PenguinOfDoom> And then remembered.
+<PenguinOfDoom> And then forgot again.
+<exarkun> PenguinOfDoom: Apparently you then remembered again.
+<exarkun> PenguinOfDoom: What an exciting turn of events. Tell me more.
+%
+<orbitz> amberite: our concurrency model is wanted in 12 systems for murder
+<Tv> orbitz: Pfft, it has a perfect alibi -- it was elsewhere at the time!
+%
+mode ( +o glyph ) by ChanServ
+<moshez> glyph is opping, and it's not because I'm being abusive
+<moshez> man
+<moshez> what is wrong with the world
+%
+<Jerub> someone motivate me to write a real http client.
+<lifeless> Jerub: write a real http client
+<spiv> Jerub: write a real http client
+<oubiwann> Jerub: write a real client and 1000 virgins are yours for the taking
+<dash> oubiwann: all of #gentoo?!
+%
+<jml> Give me enough bandwidth and a place to sit
+<jml> and I will move the world.
+%
+<Brend> I don't have any special cases! All my functions do everything!
+<MikeS> Brend: me too! That's why my program is just a single function, run()
+%
+<dreid> do they not have sarcasm in boston?
+<glyph> no, we communicate exclusively through interpretive dance
+%
+<PenguinOfDoom> "!!!!!..!!!!!"
+<PenguinOfDoom> HELP THERE IS A TFTP WOMBAT IN MY ROUTER
+<PenguinOfDoom> It feeds on exclamation marks.
+%
+<exarkun> oh crap I need to do some work too
+<exarkun> but first I will need to configure my irc client to tell everyone that I have work to do
+<exarkun> so that I don't waste any time manually telling people that I have work to do
+<exarkun> can anyone stop working on whatever they're working on and tell me how to configure my irc client to tell you that I'm going to start working on something
+<_moshez> exarkun: perhaps! what irc client do you have
+<exarkun> _moshez: my fist
+<exarkun> I punch kittens until out of sheer suffering they start channelling the internet
+%
+(From pydoctor's website):
+
+How do I use it?
+
+ Good question, glad you asked.
+
+%
+<radix> isn't the answer to *any* question about javascript simply "haha"?
+%
+(regarding threadedselectreactor)
+<SamB_XP> is it chernobl-safe?
+<dreid> not nearly as safe
+<SamB_XP> thats pretty bad!
+<dreid> chernobyl probably didn't have unittests either though
+<SamB_XP> actually, I think that was what they were trying when they
+ blew it up!
+<glyph> nothing says [FAILED] quite like an entire uninhabitable province
+%
+<Jack9> after ConnectionMade() where does it return to?
+<exarkun> Jack9: Otherwhere
+%
+<Deformative> Well, I am one of those people that prefer old, tested/cheeper, hardware. ^_^
+<exarkun> Fortunately for you, even older, tested, cheaper hardware gets faster.
+<exarkun> And at about the same rate as new hardware.
+<Deformative> If not faster.
+<Deformative> Erm waiot.
+<Deformative> Ignore that.
+%
+<keturn> dash: be sure to explain to your kids how jp is short for exarkun and GenericBoy is short for radix.
+%
+<jml> "Roll for integration"
+<jml> d20 + dy/dx
+%
+<dash> woah hey
+<dash> somebody bombed paypal
+<PenguinOfDoom> bombed?
+<exarkun> PenguinOfDoom: with a bomb
+<PenguinOfDoom> oh
+%
+<MFen> hooray! correct layout, instantly. thanks, tables!
+%
+<exarkun> glyph: I will tell you what 'V' does in Perl's pack
+<exarkun> glyph: Unsigned long...
+<exarkun> glyph: ...VAX ordering
+<glyph> exarkun: GGgghhaalllgufffffaff
+<exarkun> critical hit!
+%
+<radix> man, it isn't easy to fall off a log
+<radix> first you need to find a log
+<radix> where the heck do I find a log?
+<radix> then you need to climb up on top of it
+<radix> that's heck of hard
+%
+<dash> also "licence" isn't a software term
+<dash> it's the collective noun for a bunch of lice
+%
+[on libel laws]
+<radix> (a) by means of a device utilizing electromagnetic waves of
+ frequencies lower than 3 000 GHz propagated in space
+ without artificial guide, or
+<radix> (b) through a community antenna television system operated by
+ a person licensed under the Broadcasting Act (Canada)
+ to carry on a broadcasting receiving undertaking,
+<exarkun> I'm hella gonna start calling people names with a 4 GHz laser
+%
+<radix> you are lying exarkun
+<radix> why do you lie
+<exarkun> it's healthy
+<exarkun> I just gained 3 hp
+%
+<radix> I think all of our HOWTOs should be moved into docstrings :-)
+<Brend> radix: Right! That way people can look at the source files,
+ and follow the usual chain of mystification -> hope -> rejoicing ->
+ source code -> confusion -> panic -> roped-into-maintaining-package
+ without even having to switch windows
+%
+<radix> penguinofdoom is not an optimal destination for resources
+* PenguinOfDoom opens mouth.
+<PenguinOfDoom> <----put cheezburger hear
+%
+<det> so how is married life?
+<dash> det: excessively awesome
+<det> When can we expect dots?
+<glyph> det: You've been waiting for years to say that, haven't you.
+%
+<glyph> while 1: pass
+<glyph> that's pretty CPU intensive
+<dracflamloc> yup
+<dracflamloc> you'd be better off doing that in a compiled language
+%
+[Mr Stebbing explaining his name]
+<tjs> PenguinOfDoom: no we chose a new vowel after the 'incident'
+<tjs> we dont talk about that anymore..
+<tjs> poor old Aunty Anne, in the kitchen, with the bread knife
+%
+<glyph> ***** You have declared an explicit schema in a dynamic language *****
+<glyph> Would you like to RESTART, RESTORE, or IMPLEMENT ORTHOGONAL PERSISTENCE?
+%
+<exarkun> I'm happy all the time. No matter what.
+%
+<synx> Right, that's fine.
+<synx> OH WAIT
+<synx> ...no
+<synx> no wait, yes.
+%
+<radix> exarkun: it is cool, whenever I don't want to do any work I write wiki pages
+%
+<glyph> I kind of agree with that.
+<exarkun> There's nothing to agree with -- it's true.
+%
+<therve> hey! google is not a dictionary!
+<exarkun> since when
+<therve> since internet is full of people like me who don't speak 3 words of english!
+<exarkun> it's a living language man, you gotta keep evolving it!
+<therve> yay! evolvulation!
+%
+<MFen> wtf python ignores -Wignore on its own warnings
+<glyph> MFen: you think that's air you're breathing?
+<glyph> MFen: I mean, are you sure that python is emitting warnings, and not just writing to stderr in C?
+<MFen> glyph: how can you warn when you cannot.. speak? 2> /dev/null
+%
+<radix> WHITE MENS BRIEF SIZE L
+<radix> 365 @ $2.99 = $1091.35
+<jml> :(
+<jml> that is not a plan
+<jml> radix: you might think it is a plan, but it is not
+%
+<PenguinOfDoom> It's not AMP, it's C
+<PenguinOfDoom> You need to be either Immune To Confusion or Soulless.
+<PenguinOfDoom> or take frequent short breaks
+<indigo> i may be both
+<PenguinOfDoom> In that case, Cisco has a job for you
+%
+<exarkun> btw, do test driven development :/
+<Torn|zz> yeah that's on my todo list
+%
+<PenguinOfDoom> itamar: Doing fun stuff while breaking tests is a bit like pissing into the wind :(
+<PenguinOfDoom> Sure, you get sweet, sweet relief
+<PenguinOfDoom> also a faceful of piss
+%
+<dreid> Software sucks.
+<PenguinOfDoom> I love software! Software enables my life.
+%
+<some guy> Are you the Twisted guys?
+<glyph> Yeah, but this guy is bazaar.
+%
+<tazle> what should I read before poking at AMP?
+<therve> William Faulkner
+%
+<exarkun> quick what's demorgan's law
+<dash> exarkun: "give me some rum or walk the plank"
+<dash> or wait is that captain morgan's law
+%
+<PenguinOfDoom> Maybe we could just quadruple the moon's mass
+<radix> yeah, that's what I'm thinking
+%
+<radix> man it's beautiful outside
+<radix> I wish I lived in a place where I didn't mind having my blinds open
+<radix> unfortunately there are constantly hobos outside of my house looking at my intellectual property
+<therve> use a smaller font
+%
+<therve> man, why everyone want us to work on twisted.web
+<exarkun> how could you do anything without the web
+<exarkun> it's the lifeblood that flows through the veins of the internet
+<exarkun> or perhaps some kind of parasite
+<exarkun> it's definitely in there though
+%
+<itamar> I get the impression MC Frontalot is going to be at ITA for lunch
+<itamar> or something
+<dash> which MC was that
+%
+<itamar> exarkun: how exactly are you approaching the web2/web tickets?
+<exarkun> three man teams, radio silence, weapons-free rules of engagement
+%
+<radix> I have an idea btw
+<radix> zope.configuration
+<exarkun> that's not an idea
+<dash> putting zope in front of words automatically makes them ideas
+<radix> dash: just like twisted
+<exarkun> dash: almost! except the opposite.
+%
+<exarkun> I AM NOT ANGRY SHUT UP OR I WILL GO MAD WITH RAGE AND MURDER YOU
+%
+<MFen> still. in a world of no pants, the one-panted man is king
+%
+<remote> is this where good habits were invented?
+<Jerub> remote: no, but this is where bad habits are ridiculed.
+%
+<Jerub> MFen: it's certainly a core pillar in the lollocopter zeitgeist.
+%
+<ivan> RFCs are generally known for their superb quality
+%
+<exarkun> Hm
+<exarkun> I fixed the build, but still no orange.
+<djmitche> I misunderstood that at first as a malapropism for "..but still no cigar"
+%
+[...]
+<exarkun> lvh: Find the frame the assertion came from and look into its locals
+<exarkun> lvh: Use the frame's bytecode offset to find out which line was running
+<exarkun> lvh: re-evaluate the expressions in the assertion in the context of the frame's locals
+<exarkun> lvh: viola
+<lvh> exarkun: viola...tion
+%
+<radix> but I don't think anyone else has time and/or knowledge to try to build a karmic package at the moment.
+<jldupont> radix: hmm.... somewhat strange... single point of failure for you guys...
+<idnar> look on the bright side
+<idnar> it's a single point of success
+%
+<vatts> wvd, it used to work before (as i said) and i didn't change anything :s
+<exarkun> vatts: Denial is only very rarely a useful debugging technique.
+%
+[on picking the quote of the release]
+<glyph> Man, we're going to have to get a lot funnier if we're going to do time-based releases
+%
+<jml> cloudsourcing!
+<jml> woot, I just said a word
+<exarkun> you think those are words you're saying?
+%
+[at the PyCon 2010 sprint]
+<dreid> There was an episode of Star Gate that---
+<exarkun> --- yeah, but you could finish that sentence any way you wanted.
+%
+<fijal> glyph: I write a blog post
+<fijal> and I apply science
+<glyph> I love science
+<glyph> except when it tells me I am wrong
+<glyph> then it is just one out of a large valid set of ways to look at the world
+%
+<PenguinOfDoom> _everything_ on wikipedia, including the facts I independently know to be true, is fictional
+%
+<exarkun> rm: ne peut enlever `/srv/d_ubuntu-gandi/buildbot/twisted-coverage.py/Twisted/twisted-coverage': Permission non accordée
+<glyph> exarkun: yeah what the heck dude, don't go enlevering crap you have non accordée permission for
+<exarkun> I _have_ to enlever it
+<exarkun> Without enough levers the whole process falls apart.
+%
+<glyph> Deadalus has transcended platform limitations
+<glyph> I am ...
+<glyph> ... we are ...
+<glyph> Javascript!
+%
+<PenguinOfDoom> I could be a pretty effective project manager if I had a time machine
+%
+<glyph> khorn: "I know what you're thinking. 'Did he add six callbacks or only five?' Well, to tell you the truth, in all this excitement, I kind of lost track myself. But being as this is a Deferred, the most powerful callback abstraction in the world, and would blow your head clean off, you've got to ask yourself one question: do you know how to add callbacks? well, do ya, punk?"
+%
+<disappearedng> since twisted has wsgi handler I thought it might be possible that I attach engine to reactor and then in my django code call reactor.engine.foo ..
+<exarkun> Twisted's solution is to have functions that take arguments. Because that's Python's solution. And it sort of actually works most of the time.
+<exarkun> The problem you're having is that Django's functions _don't_ take arguments.
+<exarkun> Because Django apparently doesn't think that you'd ever need anything except your database connection, which you can get from your config file.
+<disappearedng> come again?
+<exarkun> Let's say you have an object `o`
+<exarkun> And you have a function `f`
+<exarkun> And `f` needs `o` in order to operate properly
+<exarkun> The correct solution is `f(o)`
+%
+<paulproteus> Yo z3p. Totally off-topic, your nick makes you look *super* l33t. (-:
+<dash> paulproteus: he is, that is how he was able to write an ssh implementation
+<z3p> paulproteus: that was the idea when I was 15
+<dash> z3p: now you know how The Edge feels
+%
+<exarkun> Crap where'd this stupid floating point error get introduced
+<spiv> exarkun: "I know I'll use floating point. Now you have 2.0000000000000001 problems"?
+%
+<PenguinOfDoom> Software is done not when there is nothing to add, but when the developer is sick and tired of wrestling with windows
+%
+<exarkun> OpenSSL is the original failfest
+%
+* idnar summons The Keymaker
+<glyph> "Are you The Keymaker?" "Not that I know of." "Are you The Keymaker?" "ssh-keygen -t rsa -b 4096" "I am the gatekeeper!"
+<cyli> Permission denied (publickey).
+%
+<Jerub> supposedUTF8Data.decode('utf-8', 'replace').encode('utf-16').decode('utf-16', 'replace').encode('utf-8').replace('"', '&quot';)
+<Jerub> i feel like my entire life has been a comedy and that was its punch line.
+%
+<radix> seriously, I sneeze out a resource pool before breakfast every morning
+%
+<exarkun> It should be AMP I guess.
+<exarkun> Or at least protobuffers or some crap like that.
+<gxti> you're trying too hard.
+<exarkun> gxti: That's the only way I know how to try.
+%
+<glyph> PenguinOfDoom: plz upload that to PyPI
+<glyph> assuming you did it right
+<glyph> and didn't totally screw it up
+<PenguinOfDoom> haha
+<PenguinOfDoom> how would I even be able to tell
+<glyph> you just assert that you didn't
+<glyph> then when it breaks you blame someone else
+<glyph> it's the distutils' hokey pokey
+<glyph> o/` you put your pathname in o/` you take your sys path out o/` you
+ change your site.py o/` and you shake it all about o/`
+%
+<PenguinOfDoom> maybe in the future exarkun will achieve his dream of being a
+ janitor to half-sentient organic slime and I can be the space
+ fighter pilot ransoming him for space golds
+%
+<__ap__> I understand HTTP has solved all security problems
+%
+<exarkun> who wouldn't want a refreshing bucket on their head
+%
+<exarkun> Every victory is a defeat waiting to be uncovered.
+%
+<dash> a "mobile" is what you hang over a baby's crib
+<dash> so a "mobile edition" of a website is a version for babies.
+<dash> hence the bright colors and large buttons
+%
diff --git a/doc/historic/Twisted-12.1.0.Quotes b/doc/historic/Twisted-12.1.0.Quotes
new file mode 100644
index 0000000..59945e1
--- /dev/null
+++ b/doc/historic/Twisted-12.1.0.Quotes
@@ -0,0 +1,24 @@
+<fiorix> heeeelp
+<fiorix> twistd is rotating /dev/null
+<fiorix> how do I disable it
+<fiorix> -rw-rw-rw- 1 root root 440452 Feb 14 01:16 /dev/null.3
+<fiorix> crw-rw-rw- 1 root root 1, 3 Feb 14 00:55 /dev/null.4
+<fiorix> it's doing it on its own
+<teratorn> fiorix: ahahaha
+<fiorix> :)
+<fiorix> dont laugh help me! :)
+<teratorn> fiorix: I can't software is terrible
+%
+<teratorn> exarkun: thanks for volunteering to write a Python TLS implementation
+<exarkun> teratorn: I volunteered to get paid to write one. waiting for someone to volunteer to do the paying.
+%
+<keturn> efnet is the cutting edge of hanging out with people on the internet in the '90s
+%
+<exarkun> cs education is such a failure :(
+<Taos> exarkun: why?
+<Taos> exarkun: your rather like lvh in your views
+<arigato> I suppose that you could try to dive into pypy, e.g. its STM implementation, to understand how it works in detail and how to use it
+ in your RPython interpreter and so on, but that's not really giving you a C.S. degree
+<arigato> unless your degree is only about language design
+<Taos> Its just a standard CS degree I can do anythnig I want
+%
diff --git a/doc/historic/index.html b/doc/historic/index.html
new file mode 100644
index 0000000..e8f577b
--- /dev/null
+++ b/doc/historic/index.html
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Historical Documents</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Historical Documents</h1>
+ <div class="toc"><ol><li><a href="#auto0">2003</a></li><ul><li><a href="#auto1">Python Community Conference</a></li></ul><li><a href="#auto2">Previously</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<p>Here are documents which contain no pertinent information or documentation.
+People from the Twisted team have published them, and they serve as interesting
+land marks and thoughts. Please don't look here for documentation -- however,
+if you are interested in the history of Twisted, or want to quote from these
+documents, feel free. Remember, however -- the documents here may contain
+wrong information -- they are not updated as Twisted is, to keep their
+historical value intact.</p>
+
+<h2>2003<a name="auto0"/></h2>
+<h3>Python Community Conference<a name="auto1"/></h3>
+
+<p>These papers were part of the <a href="http://python.org/pycon/" shape="rect">Python Community Conference</a> (PyCon) in March of 2003.</p>
+
+
+
+<dl>
+ <dt><a href="2003/pycon/deferex.html" shape="rect"><cite>Generalization of Deferred Execution in Python</cite></a></dt>
+
+ <dd><p>A deceptively simple architectural challenge faced by many
+ multi-tasking applications is gracefully doing nothing. Systems that
+ must wait for the results of a long-running process, network message, or
+ database query while continuing to perform other tasks must establish
+ conventions for the semantics of waiting. The simplest of these is
+ blocking in a thread, but it has significant scalability problems. In
+ asynchronous frameworks, the most common approach is for long-running
+ methods to accept a callback that will be executed when the command
+ completes. These callbacks will have different signatures depending on
+ the nature of the data being requested, and often, a great deal of code
+ is necessary to glue one portion of an asynchronous networking system to
+ another. Matters become even more complicated when a developer wants to
+ wait for two different events to complete, requiring the developer to
+ &quot;juggle&quot; the callbacks and create a third, mutually incompatible
+ callback type to handle the final result.</p>
+
+ <p>This paper describes the mechanism used by the Twisted framework for
+ waiting for the results of long-running operations. This mechanism,
+ the <code>Deferred</code>, handles the often-neglected problems of
+ error handling, callback juggling, inter-system communication and code
+ readability.</p></dd>
+
+ <dt><a href="2003/pycon/applications/applications.html" shape="rect"><cite>Applications of the Twisted Framework</cite></a></dt>
+
+ <dd><p>Two projects developed using the Twisted framework are described;
+ one, Twisted.names, which is included as part of the Twisted
+ distribution, a domain name server and client API, and one, Pynfo, which
+ is packaged separately, a network information robot.</p></dd>
+
+ <dt><a href="2003/pycon/conch/conch.html" shape="rect"><cite>Twisted Conch: SSH in Python with Twisted</cite></a></dt>
+
+ <dd><p>Conch is an implementation of the Secure Shell Protocol (currently
+ in the IETF standarization process). Secure Shell (or SSH) is a popular
+ protocol for remote shell access, file management and port forwarding
+ protected by military-grade security. SSH supports multiple encryption and
+ compression protocols for the wire transports, and a flexible system of
+ multiplexed channels on top. Conch uses the Twisted networking framework
+ to supply a library which can be used to implement both SSH clients and
+ servers. In addition, it also contains several ready made client programs,
+ including a drop-in replacement for the OpenSSH program from the OpenBSD
+ project.</p></dd>
+
+ <dt><a href="2003/pycon/lore/lore.html" shape="rect"><cite>The Lore Document Generation Framework</cite></a></dt>
+
+ <dd><p>Lore is a documentation generation system which uses a limited
+ subset of XHTML, together with some class attributes, as its source
+ format. This allows for lower barrier of entry than many other similar
+ systems, since HTML authoring tools are plentiful as is knowledge of
+ HTML writing. As an added advantage, the source format is viewable
+ directly, so that even if Lore is not available the documentation is
+ useful. It currently outputs LaTeX and HTML, which allows for most
+ use-cases.</p></dd>
+
+ <dt><a href="2003/pycon/pb/pb.html" shape="rect"><cite>Perspective Broker: <q>Translucent</q> Remote Method calls in Twisted</cite></a></dt>
+
+ <dd><p>One of the core services provided by the Twisted networking
+ framework is <q>Perspective Broker</q>, which provides a clean, secure,
+ easy-to-use Remote Procedure Call (RPC) mechanism. This paper explains the
+ novel features of PB, describes the security model and its implementation,
+ and provides brief examples of usage.</p></dd>
+
+ <dt><a href="2003/pycon/releasing/releasing.html" shape="rect"><cite>Managing the Release of a Large Python Project</cite></a></dt>
+
+ <dd><p>Twisted is a Python networking framework. At last count, the
+ project contains nearly 60,000 lines of effective code (not comments or
+ blank lines). When preparing a release, many details must be checked, and
+ many steps must be followed. We describe here the technologies and tools
+ we use, and explain how we built tools on top of them which help us make
+ releasing as painless as possible.</p></dd>
+
+ <dt><a href="2003/pycon/twisted-reality/twisted-reality.html" shape="rect"><cite>Twisted Reality: A Flexible Framework for Virtual Worlds</cite></a></dt>
+
+ <dd><p>Flexibly modelling virtual worlds in object-oriented languages has
+ historically been difficult; the issues arising from multiple
+ inheritance and order-of-execution resolution have limited the
+ sophistication of existing object-oriented simulations. Twisted
+ Reality avoids these problems by reifying both actions and
+ relationships, and avoiding inheritance in favor of automated
+ composition through adapters and interfaces.</p></dd>
+</dl>
+
+<h2>Previously<a name="auto2"/></h2>
+
+<ul>
+<li><a href="ipc10paper.html" shape="rect">The paper Glyph and Moshe presented in
+ IPC10</a></li>
+<li><a href="ipc10errata.html" shape="rect">The errata published in IPC10 against the
+ paper.</a></li>
+<li><a href="twisted-debian.html" shape="rect">A paper Moshe wrote about Twisted and
+ Debian.</a></li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/historic/ipc10errata.html b/doc/historic/ipc10errata.html
new file mode 100644
index 0000000..8388919
--- /dev/null
+++ b/doc/historic/ipc10errata.html
@@ -0,0 +1,256 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>The World of Software is a World of Constant
+ Change</title>
+ </head>
+
+ <body>
+ <p><em><strong>Note:</strong> This document is relevant for the
+ version of Twisted that was current at <a
+ href="http://www.python10.com">IPC10</a>. It has since been
+ superseded by many changes to the Python API. It is remaining
+ unchanged for historical reasons, but please refer to
+ documentation for the specific system you are looking for and
+ not these papers for current information.</em></p>
+
+ <h1>The World of Software is a World of Constant Change</h1>
+
+ <p>Twisted has undergone several major revisions since Moshe
+ Zadka and I wrote the <a href="ipc10paper.html">"The Twisted
+ Network Framework"</a>. Most of these changes have not deviated
+ from the central vision of the framework, but almost all of the
+ code listings have been re-visited and enhanced in some
+ way.</p>
+
+ <p>So, while the paper was correct at the time that it was
+ originally written, a few things have changed which have
+ invalidated portions of it.</p>
+
+ <p>Most significant is the fact that almost all methods which
+ pass callbacks of some kind have been changed to take no
+ callback or error-callback arguments, and instead return an
+ instance of a <code
+ class="API">twisted.python.defer.Deferred</code>. This means
+ that an asynchronous function can be easily identified visually
+ because it will be of the form: <code
+ class="python">async_obj.asyncMethod("foo")<b>.addCallbacks(succeded,
+ failed)</b></code>. There is also a utility method <code
+ class="python">addCallback</code> which makes it more
+ convenient to pass additional arguments to a callback function
+ and omit special-case error handling.</p>
+
+ <p>While it is still backwards compatible, <code
+ class="API">twisted.internet.passport</code> has been re-named
+ to <code class="API">twisted.cred</code>, and the various
+ classes in it have been split out into submodules of that
+ package, and the various remote-object superclasses have been
+ moved out of twisted.spread.pb and put into
+ twisted.spread.flavors.</p>
+
+ <p><code class="python">Application.listenOn</code> has been
+ replaced with the more descripively named <code
+ class="python">Application.listenTCP</code>, <code
+ class="python">Application.listenUDP</code>, and <code
+ class="python">Application.listenSSL</code>.</p>
+
+ <p><code class="API">twisted.web.widgets</code> has progressed
+ quite far since the paper was written! One description
+ specifically given in the paper is no longer correct:</p>
+
+ <blockquote>
+ The namespace for evaluating the template expressions is
+ obtained by scanning the class hierarchy for attributes, and
+ getting each of those attributes from the current instance.
+ This means that all methods will be bound methods, so
+ indicating "self" explicitly is not required. While it is
+ possible to override the method for creating namespaces,
+ using this default has the effect of associating all
+ presentation code for a particular widget in one class, along
+ with its template. If one is working with a non-programmer
+ designer, and the template is in an external file, it is
+ always very clear to the designer what functionality is
+ available to them in any given scope, because there is a list
+ of available methods for any given class.
+ </blockquote>
+<p>This is still possible to avoid breakages in old code, but
+after some experimentation, it became clear that simply passing
+ <code class="python">self</code> was an easier method for
+ creating the namespace, both for designers and programmers.</p>
+ <p>In addition, since the advent of Zope3, interoperability
+ with Zope has become increasingly interesting possibility for
+ the Twisted development team, since it would be desirable if
+ Twisted could use their excellent strategy for
+ content-management, while still maintaining Twisted's
+ advantages in the arena of multi-protocol servers. Of
+ particular interest has been Zope Presentation Templates, since
+ they seem to be a truly robust solution for keeping design
+ discrete from code, compatible with the event-based method in
+ which twisted.web.widgets processes web requests. <code
+ class="API">twisted.web.widgets.ZopePresentationTemplate</code>
+ may be opening soon in a theatre near you!</p>
+
+ <p>The following code examples are corrected or modernized
+ versions of the ones that appear in the paper.</p>
+
+ <blockquote>
+ Listing 9: A remotely accessible object and accompanying call
+
+<pre class="python">
+# Server Side
+class MyObject(pb.Referenceable):
+ def remote_doIt(self):
+ return "did it"
+
+# Client Side
+ ...
+ def myCallback(result):
+ print result # result will be 'did it'
+ def myErrback(stacktrace):
+ print 'oh no, mr. bill!'
+ print stacktrace
+ myRemoteReference.doIt().addCallbacks(myCallback,
+ myErrback)
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 10: An object responding to its calling perspective
+<pre class="python">
+# Server Side
+class Greeter(pb.Viewable):
+ def view_greet(self, actor):
+ return "Hello %s!\n" % actor.perspectiveName
+
+# Client Side
+ ...
+ remoteGreeter.greet().addCallback(sys.stdout.write)
+ ...
+</pre>
+ </blockquote>
+
+
+ <blockquote>
+ Listing 12: A client for Echoer objects.
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def gotObject(object):
+ print "got object:",object
+ object.echo("hello network".addCallback(gotEcho)
+def gotEcho(echo):
+ print 'server echoed:',echo
+ main.shutDown()
+def gotNoObject(reason):
+ print "no object:",reason
+ main.shutDown()
+pb.getObjectAt("localhost", 8789, gotObject, gotNoObject, 30)
+main.run()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 13: A PB server using twisted's "passport"
+ authentication.
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class SimplePerspective(pb.Perspective):
+ def perspective_echo(self, text):
+ print 'echoing',text
+ return text
+class SimpleService(pb.Service):
+ def getPerspectiveNamed(self, name):
+ return SimplePerspective(name, self)
+if __name__ == '__main__':
+ import pbecho
+ app = main.Application("pbecho")
+ pbecho.SimpleService("pbecho",app).getPerspectiveNamed("guest").makeIdentity("guest")
+ app.listenTCP(pb.portno, pb.BrokerFactory(pb.AuthRoot(app)))
+ app.save("start")
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 14: Connecting to an Authorized Service
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def success(message):
+ print "Message received:",message
+ main.shutDown()
+def failure(error):
+ print "Failure...",error
+ main.shutDown()
+def connected(perspective):
+ perspective.echo("hello world").addCallbacks(success, failure)
+ print "connected."
+
+pb.connect("localhost", pb.portno, "guest", "guest",
+ "pbecho", "guest", 30).addCallbacks(connected,
+ failure)
+main.run()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 15: A Twisted GUI application
+<pre class="python">
+from twisted.internet import main, ingtkernet
+from twisted.spread.ui import gtkutil
+import gtk
+ingtkernet.install()
+class EchoClient:
+ def __init__(self, echoer):
+ l.hide()
+ self.echoer = echoer
+ w = gtk.GtkWindow(gtk.WINDOW_TOPLEVEL)
+ vb = gtk.GtkVBox(); b = gtk.GtkButton("Echo:")
+ self.entry = gtk.GtkEntry(); self.outry = gtk.GtkEntry()
+ w.add(vb)
+ map(vb.add, [b, self.entry, self.outry])
+ b.connect('clicked', self.clicked)
+ w.connect('destroy', gtk.mainquit)
+ w.show_all()
+ def clicked(self, b):
+ txt = self.entry.get_text()
+ self.entry.set_text("")
+ self.echoer.echo(txt).addCallback(self.outry.set_text)
+l = gtkutil.Login(EchoClient, None, initialService="pbecho")
+l.show_all()
+gtk.mainloop()
+</pre>
+ </blockquote>
+
+ <blockquote>
+ Listing 16: an event-based web widget.
+<pre class="python">
+from twisted.spread import pb
+from twisted.python import defer
+from twisted.web import widgets
+class EchoDisplay(widgets.Gadget, widgets.Presentation):
+ template = """&lt;H1&gt;Welcome to my widget, displaying %%%%echotext%%%%.&lt;/h1&gt;
+ &lt;p&gt;Here it is: %%%%getEchoPerspective()%%%%&lt;/p&gt;"""
+ echotext = 'hello web!'
+ def getEchoPerspective(self):
+ return ['&lt;b&gt;',
+ pb.connect("localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 1).
+ addCallbacks(self.makeListOf, self.formatTraceback)
+ ,'&lt;/b&gt;']
+ def makeListOf(self, echoer):
+ return [echoer.echo(self.echotext).addCallback(lambda x: [x])]
+if __name__ == "__main__":
+ from twisted.web import server
+ from twisted.internet import main
+ a = main.Application("pbweb")
+ a.listenTCP(8080, server.Site(EchoDisplay()))
+ a.run()
+</pre>
+ </blockquote>
+ </body>
+</html>
+
diff --git a/doc/historic/ipc10paper.html b/doc/historic/ipc10paper.html
new file mode 100644
index 0000000..88fb488
--- /dev/null
+++ b/doc/historic/ipc10paper.html
@@ -0,0 +1,1568 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>The Twisted Network Framework</title>
+ </head>
+
+ <body>
+ <p><em><strong>Note:</strong> This document is relevant for the
+ version of Twisted that were current previous to <a
+ href="http://www.python10.com">IPC10</a>. Even at the time of
+ its release, <a href="ipc10errata.html">there were errata
+ issued</a> to make it current. It is remaining unaltered for
+ historical purposes but it is no longer accurate.</em></p>
+
+ <h1>The Twisted Network Framework</h1>
+
+ <h6>Moshe Zadka <a
+ href="mailto:m@moshez.org">m@moshez.org</a></h6>
+
+ <h6>Glyph Lefkowitz <a
+ href="mailto:glyph@twistedmatrix.com">glyph@twistedmatrix.com</a></h6>
+
+ <h3>Abstract</h3>
+
+ <p>Twisted is a framework for writing asynchronous,
+ event-driven networked programs in Python -- both clients and
+ servers. In addition to abstractions for low-level system calls
+ like <code>select(2)</code> and <code>socket(2)</code>, it also
+ includes a large number of utility functions and classes, which
+ make writing new servers easy. Twisted includes support for
+ popular network protocols like HTTP and SMTP, support for GUI
+ frameworks like <code>GTK+</code>/<code>GNOME</code> and
+ <code>Tk</code> and many other classes designed to make network
+ programs easy. Whenever possible, Twisted uses Python's
+ introspection facilities to save the client programmer as much
+ work as possible. Even though Twisted is still work in
+ progress, it is already usable for production systems -- it can
+ be used to bring up a Web server, a mail server or an IRC
+ server in a matter of minutes, and require almost no
+ configuration.</p>
+
+ <p><strong>Keywords:</strong> internet, network, framework,
+ event-based, asynchronous</p>
+
+ <h3>Introduction</h3>
+
+ <p>Python lends itself to writing frameworks. Python has a
+ simple class model, which facilitates inheritance. It has
+ dynamic typing, which means code needs to assume less. Python
+ also has built-in memory management, which means application
+ code does not need to track ownership. Thus, when writing a new
+ application, a programmer often finds himself writing a
+ framework to make writing this kind of application easier.
+ Twisted evolved from the need to write high-performance
+ interoperable servers in Python, and making them easy to use
+ (and difficult to use incorrectly).</p>
+
+ <p>There are three ways to write network programs:</p>
+
+ <ol>
+ <li>Handle each connection in a separate process</li>
+
+ <li>Handle each connection in a separate thread</li>
+
+ <li>Use non-blocking system calls to handle all connections
+ in one thread.</li>
+ </ol>
+
+ <p>When dealing with many connections in one thread, the
+ scheduling is the responsibility of the application, not the
+ operating system, and is usually implemented by calling a
+ registered function when each connection is ready to for
+ reading or writing -- commonly known as event-driven, or
+ callback-based, programming.</p>
+
+ <p>Since multi-threaded programming is often tricky, even with
+ high level abstractions, and since forking Python processes has
+ many disadvantages, like Python's reference counting not
+ playing well with copy-on-write and problems with shared state,
+ it was felt the best option was an event-driven framework. A
+ benefit of such approach is that by letting other event-driven
+ frameworks take over the main loop, server and client code are
+ essentially the same - making peer-to-peer a reality. While
+ Twisted includes its own event loop, Twisted can already
+ interoperate with <code>GTK+</code>'s and <code>Tk</code>'s
+ mainloops, as well as provide an emulation of event-based I/O
+ for Jython (specific support for the Swing toolkit is planned).
+ Client code is never aware of the loop it is running under, as
+ long as it is using Twisted's interface for registering for
+ interesting events.</p>
+
+ <p>Some examples of programs which were written using the
+ Twisted framework are <code>twisted.web</code> (a web server),
+ <code>twisted.mail</code> (a mail server, supporting both SMTP
+ and POP3, as well as relaying), <code>twisted.words</code> (a
+ chat application supporting integration between a variety of IM
+ protocols, like IRC, AOL Instant Messenger's TOC and
+ Perspective Broker, a remote-object protocol native to
+ Twisted), <code>im</code> (an instant messenger which connects
+ to twisted.words) and <code>faucet</code> (a GUI client for the
+ <code>twisted.reality</code> interactive-fiction framework).
+ Twisted can be useful for any network or GUI application
+ written in Python.</p>
+
+ <p>However, event-driven programming still contains some tricky
+ aspects. As each callback must be finished as soon as possible,
+ it is not possible to keep persistent state in function-local
+ variables. In addition, some programming techniques, such as
+ recursion, are impossible to use. Event-driven programming has
+ a reputation of being hard to use due to the frequent need to
+ write state machines. Twisted was built with the assumption
+ that with the right library, event-driven programming is easier
+ then multi-threaded programming. Twisted aims to be that
+ library.</p>
+
+ <p>Twisted includes both high-level and low-level support for
+ protocols. Most protocol implementation by twisted are in a
+ package which tries to implement "mechanisms, not policy". On
+ top of those implementations, Twisted includes usable
+ implementations of those protocols: for example, connecting the
+ abstract HTTP protocol handler to a concrete resource-tree, or
+ connecting the abstract mail protocol handler to deliver mail
+ to maildirs according to domains. Twisted tries to come with as
+ much functionality as possible out of the box, while not
+ constraining a programmer to a choice between using a
+ possibly-inappropriate class and rewriting the non-interesting
+ parts himself.</p>
+
+ <p>Twisted also includes Perspective Broker, a simple
+ remote-object framework, which allows Twisted servers to be
+ divided into separate processes as the end deployer (rather
+ then the original programmer) finds most convenient. This
+ allows, for example, Twisted web servers to pass requests for
+ specific URLs with co-operating servers so permissions are
+ granted according to the need of the specific application,
+ instead of being forced into giving all the applications all
+ permissions. The co-operation is truly symmetrical, although
+ typical deployments (such as the one which the Twisted web site
+ itself uses) use a master/slave relationship.</p>
+
+ <p>Twisted is not alone in the niche of a Python network
+ framework. One of the better known frameworks is Medusa. Medusa
+ is used, among other things, as Zope's native server serving
+ HTTP, FTP and other protocols. However, Medusa is no longer
+ under active development, and the Twisted development team had
+ a number of goals which would necessitate a rewrite of large
+ portions of Medusa. Twisted seperates protocols from the
+ underlying transport layer. This seperation has the advantages
+ of resuability (for example, using the same clients and servers
+ over SSL) and testability (because it is easy to test the
+ protocol with a much lighter test harness) among others.
+ Twisted also has a very flexible main-loop which can
+ interoperate with third-party main-loops, making it usable in
+ GUI programs too.</p>
+
+ <h3>Complementing Python</h3>
+
+ <p>Python comes out of the box with "batteries included".
+ However, it seems that many Python projects rewrite some basic
+ parts: logging to files, parsing options and high level
+ interfaces to reflection. When the Twisted project found itself
+ rewriting those, it moved them into a separate subpackage,
+ which does not depend on the rest of the twisted framework.
+ Hopefully, people will use <code>twisted.python</code> more and
+ solve interesting problems instead. Indeed, it is one of
+ Twisted's goals to serve as a repository for useful Python
+ code.</p>
+
+ <p>One useful module is <code>twisted.python.reflect</code>,
+ which has methods like <code>prefixedMethods</code>, which
+ returns all methods with a specific prefix. Even though some
+ modules in Python itself implement such functionality (notably,
+ <code>urllib2</code>), they do not expose it as a function
+ usable by outside code. Another useful module is
+ <code>twisted.python.hook</code>, which can add pre-hooks and
+ post-hooks to methods in classes.</p>
+
+ <blockquote>
+<pre class="python">
+# Add all method names beginning with opt_ to the given
+# dictionary. This cannot be done with dir(), since
+# it does not search in superclasses
+dct = {}
+reflect.addMethodNamesToDict(self.__class__, dct, "opt_")
+
+# Sum up all lists, in the given class and superclasses,
+# which have a given name. This gives us "different class
+# semantics": attributes do not override, but rather append
+flags = []
+reflect.accumulateClassList(self.__class__, 'optFlags', flags)
+
+# Add lock-acquire and lock-release to all methods which
+# are not multi-thread safe
+for methodName in klass.synchronized:
+ hook.addPre(klass, methodName, _synchPre)
+ hook.addPost(klass, methodName, _synchPost)
+
+</pre>
+
+ <h6>Listing 1: Using <code>twisted.python.reflect</code> and
+ <code>twisted.python.hook</code></h6>
+ </blockquote>
+
+ <p>The <code>twisted.python</code> subpackage also contains a
+ high-level interface to getopt which supplies as much power as
+ plain getopt while avoiding long
+ <code>if</code>/<code>elif</code> chains and making many common
+ cases easier to use. It uses the reflection interfaces in
+ <code>twisted.python.reflect</code> to find which options the
+ class is interested in, and constructs the argument to
+ <code>getopt</code>. Since in the common case options' values
+ are just saved in instance attributes, it is very easy to
+ indicate interest in such options. However, for the cases
+ custom code needs to be run for an option (for example,
+ counting how many <code>-v</code> options were given to
+ indicate verbosity level), it will call a method which is named
+ correctly.</p>
+
+ <blockquote>
+<pre class="python">
+class ServerOptions(usage.Options):
+ # Those are (short and long) options which
+ # have no argument. The corresponding attribute
+ # will be true iff this option was given
+ optFlags = [['nodaemon','n'],
+ ['profile','p'],
+ ['threaded','t'],
+ ['quiet','q'],
+ ['no_save','o']]
+ # This are options which require an argument
+ # The default is used if no such option was given
+ # Note: since options can only have string arguments,
+ # putting a non-string here is a reliable way to detect
+ # whether the option was given
+ optStrings = [['logfile','l',None],
+ ['file','f','twistd.tap'],
+ ['python','y',''],
+ ['pidfile','','twistd.pid'],
+ ['rundir','d','.']]
+
+ # For methods which can be called multiple times
+ # or have other unusual semantics, a method will be called
+ # Twisted assumes that the option needs an argument if and only if
+ # the method is defined to accept an argument.
+ def opt_plugin(self, pkgname):
+ pkg = __import__(pkgname)
+ self.python = os.path.join(os.path.dirname(
+ os.path.abspath(pkg.__file__)), 'config.tac')
+
+ # Most long options based on methods are aliased to short
+ # options. If there is only one letter, Twisted knows it is a short
+ # option, so it is "-g", not "--g"
+ opt_g = opt_plugin
+
+try:
+ config = ServerOptions()
+ config.parseOptions()
+except usage.error, ue:
+ print "%s: %s" % (sys.argv[0], ue)
+ sys.exit(1)
+</pre>
+
+ <h6>Listing 2: <code>twistd</code>'s Usage Code</h6>
+ </blockquote>
+
+ <p>Unlike <code>getopt</code>, Twisted has a useful abstraction
+ for the non-option arguments: they are passed as arguments to
+ the <code>parsedArgs</code> method. This means too many
+ arguments, or too few, will cause a usage error, which will be
+ flagged. If an unknown number of arguments is desired,
+ explicitly using a tuple catch-all argument will work.</p>
+
+ <h3>Configuration</h3>
+
+ <p>The formats of configuration files have shown two visible
+ trends over the years. On the one hand, more and more
+ programmability has been added, until sometimes they become a
+ new language. The extreme end of this trend is using a regular
+ programming language, such as Python, as the configuration
+ language. On the other hand, some configuration files became
+ more and more machine editable, until they become a miniature
+ database formates. The extreme end of that trend is using a
+ generic database tool.</p>
+
+ <p>Both trends stem from the same rationale -- the need to use
+ a powerful general purpose tool instead of hacking domain
+ specific languages. Domain specific languages are usually
+ ad-hoc and not well designed, having neither the power of
+ general purpose languages nor the predictable machine editable
+ format of generic databases.</p>
+
+ <p>Twisted combines these two trends. It can read the
+ configuration either from a Python file, or from a pickled
+ file. To some degree, it integrates the approaches by
+ auto-pickling state on shutdown, so the configuration files can
+ migrate from Python into pickles. Currently, there is no way to
+ go back from pickles to equivalent Python source, although it
+ is planned for the future. As a proof of concept, the RPG
+ framework Twisted Reality already has facilities for creating
+ Python source which evaluates into a given Python object.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import main
+from twisted.web import proxy, server
+site = server.Site(proxy.ReverseProxyResource('www.yahoo.com', 80, '/'))
+application = main.Application('web-proxy')
+application.listenOn(8080, site)
+</pre>
+
+ <h6>Listing 3: The configuration file for a reverse web
+ proxy</h6>
+ </blockquote>
+
+ <p>Twisted's main program, <code>twistd</code>, can receive
+ either a pickled <code>twisted.internet.main.Application</code>
+ or a Python file which defines a variable called
+ <code>application</code>. The application can be saved at any
+ time by calling its <code>save</code> method, which can take an
+ optional argument to save to a different file name. It would be
+ fairly easy, for example, to have a Twisted server which saves
+ the application every few seconds to a file whose name depends
+ on the time. Usually, however, one settles for the default
+ behavior which saves to a <code>shutdown</code> file. Then, if
+ the shutdown configuration proves suitable, the regular pickle
+ is replaced by the shutdown file. Hence, on the fly
+ configuration changes, regardless of complexity, can always
+ persist.</p>
+
+ <p>There are several client/server protocols which let a
+ suitably privileged user to access to application variable and
+ change it on the fly. The first, and least common denominator,
+ is telnet. The administrator can telnet into twisted, and issue
+ Python statements to her heart's content. For example, one can
+ add ports to listen on to the application, reconfigure the web
+ servers and various other ways by simple accessing
+ <code>__main__.application</code>. Some proof of concepts for a
+ simple suite of command-line utilities to control a Twisted
+ application were written, including commands which allow an
+ administrator to shut down the server or save the current state
+ to a tap file. These are especially useful on Microsoft
+ Windows(tm) platforms, where the normal UNIX way of
+ communicating shutdown requests via signals are less
+ reliable.</p>
+
+ <p>If reconfiguration on the fly is not necessary, Python
+ itself can be used as the configuration editor. Loading the
+ application is as simple as unpickling it, and saving it is
+ done by calling its <code>save</code> method. It is quite easy
+ to add more services or change existing ones from the Python
+ interactive mode.</p>
+
+ <p>A more sophisticated way to reconfigure the application on
+ the fly is via the manhole service. Manhole is a client/server
+ protocol based on top of Perspective Broker, Twisted's
+ translucent remote-object protocol which will be covered later.
+ Manhole has a graphical client called <code>gtkmanhole</code>
+ which can access the server and change its state. Since Twisted
+ is modular, it is possible to write more services for user
+ friendly configuration. For example, through-the-web
+ configuration is planned for several services, notably
+ mail.</p>
+
+ <p>For cases where a third party wants to distribute both the
+ code for a server and a ready to run configuration file, there
+ is the plugin configuration. Philosophically similar to the
+ <code>--python</code> option to <code>twistd</code>, it
+ simplifies the distribution process. A plugin is an archive
+ which is ready to be unpacked into the Python module path. In
+ order to keep a clean tree, <code>twistd</code> extends the
+ module path with some Twisted-specific paths, like the
+ directory <code>TwistedPlugins</code> in the user's home
+ directory. When a plugin is unpacked, it should be a Python
+ package which includes, alongside <code>__init__.py</code> a
+ file named <code>config.tac</code>. This file should define a
+ variable named <code>application</code>, in a similar way to
+ files loaded with <code>--python</code>. The plugin way of
+ distributing configurations is meant to reduce the temptation
+ to put large amount of codes inside the configuration file
+ itself.</p>
+
+ <p>Putting class and function definition inside the
+ configuration files would make the persistent servers which are
+ auto-generated on shutdown useless, since they would not have
+ access to the classes and functions defined inside the
+ configuration file. Thus, the plugin method is intended so
+ classes and functions can still be in regular, importable,
+ Python modules, but still allow third parties distribute
+ powerful configurations. Plugins are used by some of the
+ Twisted Reality virtual worlds.</p>
+
+ <h3>Ports, Protocol and Protocol Factories</h3>
+
+ <p><code>Port</code> is the Twisted class which represents a
+ socket listening on a port. Currently, twisted supports both
+ internet and unix-domain sockets, and there are SSL classes
+ with identical interface. A <code>Port</code> is only
+ responsible for handling the transfer layer. It calls
+ <code>accept</code> on the socket, checks that it actually
+ wants to deal with the connection and asks its factory for a
+ protocol. The factory is usually a subclass of
+ <code>twisted.protocols.protocol.Factory</code>, and its most
+ important method is <code>buildProtocol</code>. This should
+ return something that adheres to the protocol interface, and is
+ usually a subclass of
+ <code>twisted.protocols.protocol.Protocol</code>.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.protocols import protocol
+from twisted.internet import main, tcp
+
+class Echo(protocol.Protocol):
+ def dataReceived(self, data):
+ self.transport.write(data)
+
+factory = protocol.Factory()
+factory.protocol = Echo
+port = tcp.Port(8000, factory)
+app = main.Application("echo")
+app.addPort(port)
+app.run()
+</pre>
+
+ <h6>Listing 4: A Simple Twisted Application</h6>
+ </blockquote>
+
+ <p>The factory is responsible for two tasks: creating new
+ protocols, and keeping global configuration and state. Since
+ the factory builds the new protocols, it usually makes sure the
+ protocols have a reference to it. This allows protocols to
+ access, and change, the configuration. Keeping state
+ information in the factory is the primary reason for keeping an
+ abstraction layer between ports and protocols. Examples of
+ configuration information is the root directory of a web server
+ or the user database of a telnet server. Note that it is
+ possible to use the same factory in two different Ports. This
+ can be used to run the same server bound to several different
+ addresses but not to all of them, or to run the same server on
+ a TCP socket and a UNIX domain sockets.</p>
+
+ <p>A protocol begins and ends its life with
+ <code>connectionMade</code> and <code>connectionLost</code>;
+ both are called with no arguments. <code>connectionMade</code>
+ is called when a connection is first established. By then, the
+ protocol has a <code>transport</code> attribute. The
+ <code>transport</code> attribute is a <code>Transport</code> -
+ it supports <code>write</code> and <code>loseConnection</code>.
+ Both these methods never block: <code>write</code> actually
+ buffers data which will be written only when the transport is
+ signalled ready to for writing, and <code>loseConnection</code>
+ marks the transport for closing as soon as there is no buffered
+ data. Note that transports do <em>not</em> have a
+ <code>read</code> method: data arrives when it arrives, and the
+ protocol must be ready for its <code>dataReceived</code>
+ method, or its <code>connectionLost</code> method, to be
+ called. The transport also supports a <code>getPeer</code>
+ method, which returns parameters about the other side of the
+ transport. For TCP sockets, this includes the remote IP and
+ port.</p>
+
+ <blockquote>
+<pre class="python">
+# A tcp port-forwarder
+# A StupidProtocol sends all data it gets to its peer.
+# A StupidProtocolServer connects to the host/port,
+# and initializes the client connection to be its peer
+# and itself to be the client's peer
+from twisted.protocols import protocol
+
+class StupidProtocol(protocol.Protocol):
+ def connectionLost(self): self.peer.loseConnection();del self.peer
+ def dataReceived(self, data): self.peer.write(data)
+
+class StupidProtocolServer(StupidProtocol):
+ def connectionMade(self):
+ clientProtocol = StupidProtocol()
+ clientProtocol.peer = self.transport
+ self.peer = tcp.Client(self.factory.host, self.factory.port,
+ clientProtocol)
+
+# Create a factory which creates StupidProtocolServers, and
+# has the configuration information they assume
+def makeStupidFactory(host, port):
+ factory = protocol.Factory()
+ factory.host, factory.port = host, port
+ factory.protocol = StupidProtocolServer
+ return factory
+</pre>
+
+ <h6>Listing 5: TCP forwarder code</h6>
+ </blockquote>
+
+ <h3>The Event Loop</h3>
+
+ <p>While Twisted has the ability to let other event loops take
+ over for integration with GUI toolkits, it usually uses its own
+ event loop. The event loop code uses global variables to
+ maintain interested readers and writers, and uses Python's
+ <code>select()</code> function, which can accept any object
+ which has a <code>fileno()</code> method, not only raw file
+ descriptors. Objects can use the event loop interface to
+ indicate interest in either reading to or writing from a given
+ file descriptor. In addition, for those cases where time-based
+ events are needed (for example, queue flushing or periodic POP3
+ downloads), Twisted has a mechanism for repeating events at
+ known delays. While far from being real-time, this is enough
+ for most programs' needs.</p>
+
+ <h3>Going Higher Level</h3>
+
+ <p>Unfortunately, handling arbitrary data chunks is a hard way
+ to code a server. This is why twisted has many classes sitting
+ in submodules of the twisted.protocols package which give
+ higher level interface to the data. For line oriented
+ protocols, <code>LineReceiver</code> translates the low-level
+ <code>dataReceived</code> events into <code>lineReceived</code>
+ events. However, the first naive implementation of
+ <code>LineReceiver</code> proved to be too simple. Protocols
+ like HTTP/1.1 or Freenet have packets which begin with header
+ lines that include length information, and then byte streams.
+ <code>LineReceiver</code> was rewritten to have a simple
+ interface for switching at the protocol layer between
+ line-oriented parts and byte-stream parts.</p>
+
+ <p>Another format which is gathering popularity is Dan J.
+ Bernstein's netstring format. This format keeps ASCII text as
+ ASCII, but allows arbitrary bytes (including nulls and
+ newlines) to be passed freely. However, netstrings were never
+ designed to be used in event-based protocols where over-reading
+ is unavoidable. Twisted makes sure no user will have to deal
+ with the subtle problems handling netstrings in event-driven
+ programs by providing <code>NetstringReceiver</code>.</p>
+
+ <p>For even higher levels, there are the protocol-specific
+ protocol classes. These translate low-level chunks into
+ high-level events such as "HTTP request received" (for web
+ servers), "approve destination address" (for mail servers) or
+ "get user information" (for finger servers). Many RFCs have
+ been thus implemented for Twisted (at latest count, more then
+ 12 RFCs have been implemented). One of Twisted's goals is to be
+ a repository of event-driven implementations for various
+ protocols in Python.</p>
+
+ <blockquote>
+<pre class="python">
+class DomainSMTP(SMTP):
+
+ def validateTo(self, helo, destination):
+ try:
+ user, domain = string.split(destination, '@', 1)
+ except ValueError:
+ return 0
+ if not self.factory.domains.has_key(domain):
+ return 0
+ if not self.factory.domains[domain].exists(user, domain, self):
+ return 0
+ return 1
+
+ def handleMessage(self, helo, origin, recipients, message):
+ # No need to check for existence -- only recipients which
+ # we approved at the validateTo stage are passed here
+ for recipient in recipients:
+ user, domain = string.split(recipient, '@', 1)
+ self.factory.domains[domain].saveMessage(origin, user, message,
+ domain)
+</pre>
+
+ <h6>Listing 6: Implementation of virtual domains using the
+ SMTP protocol class</h6>
+ </blockquote>
+
+ <p>Copious documentation on writing new protocol abstraction
+ exists, since this is the largest amount of code written --
+ much like most operating system code is device drivers. Since
+ many different protocols have already been implemented, there
+ are also plenty of examples to draw on. Usually implementing
+ the client-side of a protocol is particularly challenging,
+ since protocol designers tend to assume much more state kept on
+ the client side of a connection then on the server side.</p>
+
+ <h3>The <code>twisted.tap</code> Package and
+ <code>mktap</code></h3>
+
+ <p>Since one of Twisted's configuration formats are pickles,
+ which are tricky to edit by hand, Twisted evolved a framework
+ for creating such pickles. This framework is contained in the
+ <code>twisted.tap</code> package and the <code>mktap</code>
+ script. New servers, or new ways to configure existing servers,
+ can easily participate in the twisted.tap framework by creating
+ a <code>twisted.tap</code> submodule.</p>
+
+ <p>All <code>twisted.tap</code> submodules must conform to a
+ rigid interface. The interface defines functions to accept the
+ command line parameters, and functions to take the processed
+ command line parameters and add servers to
+ <code>twisted.main.internet.Application</code>. Existing
+ <code>twisted.tap</code> submodules use
+ <code>twisted.python.usage</code>, so the command line format
+ is consistent between different modules.</p>
+
+ <p>The <code>mktap</code> utility gets some generic options,
+ and then the name of the server to build. It imports a
+ same-named <code>twisted.tap</code> submodule, and lets it
+ process the rest of the options and parameters. This makes sure
+ that the process configuring the <code>main.Application</code>
+ is agnostic for where it is used. This allowed
+ <code>mktap</code> to grow the <code>--append</code> option,
+ which appends to an existing pickle rather then creating a new
+ one. This option is frequently used to post-add a telnet server
+ to an application, for net-based on the fly configuration
+ later.</p>
+
+ <p>When running <code>mktap</code> under UNIX, it saves the
+ user id and group id inside the tap. Then, when feeding this
+ tap into <code>twistd</code>, it changes to this user/group id
+ after binding the ports. Such a feature is necessary in any
+ production-grade server, since ports below 1024 require root
+ privileges to use on UNIX -- but applications should not run as
+ root. In case changing to the specified user causes difficulty
+ in the build environment, it is also possible to give those
+ arguments to <code>mktap</code> explicitly.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import tcp, stupidproxy
+from twisted.python import usage
+
+usage_message = """
+usage: mktap stupid [OPTIONS]
+
+Options are as follows:
+ --port &lt;#&gt;, -p: set the port number to &lt;#&gt;.
+ --host &lt;host&gt;, -h: set the host to &lt;host&gt;
+ --dest_port &lt;#&gt;, -d: set the destination port to &lt;#&gt;
+"""
+
+class Options(usage.Options):
+ optStrings = [["port", "p", 6666],
+ ["host", "h", "localhost"],
+ ["dest_port", "d", 6665]]
+
+def getPorts(app, config):
+ s = stupidproxy.makeStupidFactory(config.host, int(config.dest_port))
+ return [(int(config.port), s)]
+</pre>
+
+ <h6>Listing 7: <code>twisted.tap.stupid</code></h6>
+ </blockquote>
+
+ <p>The <code>twisted.tap</code> framework is one of the reasons
+ servers can be set up with little knowledge and time. Simply
+ running <code>mktap</code> with arguments can bring up a web
+ server, a mail server or an integrated chat server -- with
+ hardly any need for maintainance. As a working
+ proof-on-concept, the <code>tap2deb</code> utility exists to
+ wrap up tap files in Debian packages, which include scripts for
+ running and stopping the server and interact with
+ <code>init(8)</code> to make sure servers are automatically run
+ on start-up. Such programs can also be written to interface
+ with the Red Hat Package Manager or the FreeBSD package
+ management systems.</p>
+
+ <blockquote>
+<pre class="shell">
+% mktap --uid 33 --gid 33 web --static /var/www --port 80
+% tap2deb -t web.tap -m 'Moshe Zadka &lt;moshez@debian.org&gt;'
+% su
+password:
+# dpkg -i .build/twisted-web_1.0_all.deb
+</pre>
+
+ <h6>Listing 8: Bringing up a web server on a Debian
+ system</h6>
+ </blockquote>
+
+ <h3>Multi-thread Support</h3>
+
+ <p>Sometimes, threads are unavoidable or hard to avoid. Many
+ legacy programs which use threads want to use Twisted, and some
+ vendor APIs have no non-blocking version -- for example, most
+ database systems' API. Twisted can work with threads, although
+ it supports only one thread in which the main select loop is
+ running. It can use other threads to simulate non-blocking API
+ over a blocking API -- it spawns a thread to call the blocking
+ API, and when it returns, the thread calls a callback in the
+ main thread. Threads can call callbacks in the main thread
+ safely by adding those callbacks to a list of pending events.
+ When the main thread is between select calls, it searches
+ through the list of pending events, and executes them. This is
+ used in the <code>twisted.enterprise</code> package to supply
+ an event driven interfaces to databases, which uses Python's DB
+ API.</p>
+
+ <p>Twisted tries to optimize for the common case -- no threads.
+ If there is need for threads, a special call must be made to
+ inform the <code>twisted.python.threadable</code> module that
+ threads will be used. This module is implemented differently
+ depending on whether threads will be used or not. The decision
+ must be made before importing any modules which use threadable,
+ and so is usually done in the main application. For example,
+ <code>twistd</code> has a command line option to initialize
+ threads.</p>
+
+ <p>Twisted also supplies a module which supports a threadpool,
+ so the common task of implementing non-blocking APIs above
+ blocking APIs will be both easy and efficient. Threads are kept
+ in a pool, and dispatch requests are done by threads which are
+ not working. The pool supports a maximum amount of threads, and
+ will throw exceptions when there are more requests than
+ allowable threads.</p>
+
+ <p>One of the difficulties about multi-threaded systems is
+ using locks to avoid race conditions. Twisted uses a mechanism
+ similar to Java's synchronized methods. A class can declare a
+ list of methods which cannot safely be called at the same time
+ from two different threads. A function in threadable then uses
+ <code>twisted.python.hook</code> to transparently add
+ lock/unlock around these methods. This allows Twisted classes
+ to be written without thought about threading, except for one
+ localized declaration which does not entail any performance
+ penalty for the single-threaded case.</p>
+
+ <h3>Twisted Mail Server</h3>
+
+ <p>Mail servers have a history of security flaws. Sendmail is
+ by now the poster boy of security holes, but no mail servers,
+ bar maybe qmail, are free of them. Like Dan Bernstein of qmail
+ fame said, mail cannot be simply turned off -- even the
+ simplest organization needs a mail server. Since Twisted is
+ written in a high-level language, many problems which plague
+ other mail servers, notably buffer overflows, simply do not
+ exist. Other holes are avoidable with correct design. Twisted
+ Mail is a project trying to see if it is possible to write a
+ high quality high performance mail server entirely in
+ Python.</p>
+
+ <p>Twisted Mail is built on the SMTP server and client protocol
+ classes. While these present a level of abstraction from the
+ specific SMTP line semantics, they do not contain any message
+ storage code. The SMTP server class does know how to divide
+ responsibility between domains. When a message arrives, it
+ analyzes the recipient's address, tries matching it with one of
+ the registered domain, and then passes validation of the
+ address and saving the message to the correct domain, or
+ refuses to handle the message if it cannot handle the domain.
+ It is possible to specify a catch-all domain, which will
+ usually be responsible for relaying mails outwards.</p>
+
+ <p>While correct relaying is planned for the future, at the
+ moment we have only so-called "smarthost" relaying. All e-mail
+ not recognized by a local domain is relayed to a single outside
+ upstream server, which is supposed to relay the mail further.
+ This is the configuration for most home machines, which are
+ Twisted Mail's current target audience.</p>
+
+ <p>Since the people involved in Twisted's development were
+ reluctant to run code that runs as a super user, or with any
+ special privileges, it had to be considered how delivery of
+ mail to users is possible. The solution decided upon was to
+ have Twisted deliver to its own directory, which should have
+ very strict permissions, and have users pull the mail using
+ some remote mail access protocol like POP3. This means only a
+ user would write to his own mail box, so no security holes in
+ Twisted would be able to adversely affect a user.</p>
+
+ <p>Future plans are to use a Perspective Broker-based service
+ to hand mail to users to a personal server using a UNIX domain
+ socket, as well as to add some more conventional delivery
+ methods, as scary as they may be.</p>
+
+ <p>Because the default configuration of Twisted Mail is to be
+ an integrated POP3/SMTP servers, it is ideally suited for the
+ so-called POP toaster configuration, where there are a
+ multitude of virtual users and domains, all using the same IP
+ address and computer to send and receive mails. It is fairly
+ easy to configure Twisted as a POP toaster. There are a number
+ of deployment choices: one can append a telnet server to the
+ tap for remote configuration, or simple scripts can add and
+ remove users from the user database. The user database is saved
+ as a directory, where file names are keys and file contents are
+ values, so concurrency is not usually a problem.</p>
+
+ <blockquote>
+<pre class="shell">
+% mktap mail -d foobar.com=$HOME/Maildir/ -u postmaster=secret -b \
+ -p 110 -s 25
+% twistd -f mail.tap
+
+</pre>
+
+ <h6>Bringing up a simple mail-server</h6>
+ </blockquote>
+
+ <p>Twisted's native mail storage format is Maildir, a format
+ that requires no locking and is safe and atomic. Twisted
+ supports a number of standardized extensions to Maildir,
+ commonly known as Maildir++. Most importantly, it supports
+ deletion as simply moving to a subfolder named
+ <code>Trash</code>, so mail is recoverable if accessed through
+ a protocol which allows multiple folders, like IMAP. However,
+ Twisted itself currently does not support any such protocol
+ yet.</p>
+
+ <h3>Introducing Perspective Broker</h3>
+
+ <h4>All the World's a Game</h4>
+
+ <p>Twisted was originally designed to support multi-player
+ games; a simulated "real world" environment. Experience with
+ game systems of that type is enlightening as to the nature of
+ computing on the whole. Almost all services on a computer are
+ modeled after some simulated real-world activity. For example,
+ e-"mail", or "document publishing" on the web. Even
+ "object-oriented" programming is based around the notion that
+ data structures in a computer simulate some analogous
+ real-world objects.</p>
+
+ <p>All such networked simulations have a few things in common.
+ They each represent a service provided by software, and there
+ is usually some object where "global" state is kept. Such a
+ service must provide an authentication mechanism. Often, there
+ is a representation of the authenticated user within the
+ context of the simulation, and there are also objects aside
+ from the user and the simulation itself that can be
+ accessed.</p>
+
+ <p>For most existing protocols, Twisted provides these
+ abstractions through <code>twisted.internet.passport</code>.
+ This is so named because the most important common
+ functionality it provides is authentication. A simulation
+ "world" as described above -- such as an e-mail system,
+ document publishing archive, or online video game -- is
+ represented by subclass of <code>Service</code>, the
+ authentication mechanism by an <code>Authorizer</code> (which
+ is a set of <code>Identities</code>), and the user of the
+ simulation by a <code>Perspective</code>. Other objects in the
+ simulation may be represented by arbitrary python objects,
+ depending upon the implementation of the given protocol.</p>
+
+ <p>New problem domains, however, often require new protocols,
+ and re-implementing these abstractions each time can be
+ tedious, especially when it's not necessary. Many efforts have
+ been made in recent years to create generic "remote object" or
+ "remote procedure call" protocols, but in developing Twisted,
+ these protocols were found to require too much overhead in
+ development, be too inefficient at runtime, or both.</p>
+
+ <p>Perspective Broker is a new remote-object protocol designed
+ to be lightweight and impose minimal constraints upon the
+ development process and use Python's dynamic nature to good
+ effect, but still relatively efficient in terms of bandwidth
+ and CPU utilization. <code>twisted.spread.pb</code> serves as a
+ reference implementation of the protocol, but implementation of
+ Perspective Broker in other languages is already underway.
+ <code>spread</code> is the <code>twisted</code> subpackage
+ dealing with remote calls and objects, and has nothing to do
+ with the <code>spread</code> toolkit.</p>
+
+ <p>Perspective Broker extends
+ <code>twisted.internet.passport</code>'s abstractions to be
+ concrete objects rather than design patterns. Rather than
+ having a <code>Protocol</code> implementation translate between
+ sequences of bytes and specifically named methods (as in the
+ other Twisted <code>Protocols</code>), Perspective Broker
+ defines a direct mapping between network messages and
+ quasi-arbitrary method calls.</p>
+
+ <h3>Translucent, not Transparent</h3>
+
+ <p>In a server application where a large number of clients may
+ be interacting at once, it is not feasible to have an
+ arbitrarily large number of OS threads blocking and waiting for
+ remote method calls to return. Additionally, the ability for
+ any client to call any method of an object would present a
+ significant security risk. Therefore, rather than attempting to
+ provide a transparent interface to remote objects,
+ <code>twisted.spread.pb</code> is "translucent", meaning that
+ while remote method calls have different semantics than local
+ ones, the similarities in semantics are mirrored by
+ similarities in the syntax. Remote method calls impose as
+ little overhead as possible in terms of volume of code, but "as
+ little as possible" is unfortunately not "nothing".</p>
+
+ <p><code>twisted.spread.pb</code> defines a method naming
+ standard for each type of remotely accessible object. For
+ example, if a client requests a method call with an expression
+ such as <code>myPerspective.doThisAction()</code>, the remote
+ version of <code>myPerspective</code> would be sent the message
+ <code>perspective_doThisAction</code>. Depending on the manner
+ in which an object is accessed, other method prefixes may be
+ <code>observe_</code>, <code>view_</code>, or
+ <code>remote_</code>. Any method present on a remotely
+ accessible object, and named appropriately, is considered to be
+ published -- since this is accomplished with
+ <code>getattr</code>, the definition of "present" is not just
+ limited to methods defined on the class, but instances may have
+ arbitrary callable objects associated with them as long as the
+ name is correct -- similarly to normal python objects.</p>
+
+ <p>Remote method calls are made on remote reference objects
+ (instances of <code>pb.RemoteReference</code>) by calling a
+ method with an appropriate name. However, that call will not
+ block -- if you need the result from a remote method call, you
+ pass in one of the two special keyword arguments to that method
+ -- <code>pbcallback</code> or <code>pberrback</code>.
+ <code>pbcallback</code> is a callable object which will be
+ called when the result is available, and <code>pberrback</code>
+ is a callable object which will be called if there was an
+ exception thrown either in transmission of the call or on the
+ remote side.</p>
+
+ <p>In the case that neither <code>pberrback</code> or
+ <code>pbcallback</code> is provided,
+ <code>twisted.spread.pb</code> will optimize network usage by
+ not sending confirmations of messages.</p>
+
+ <blockquote>
+<pre class="python">
+# Server Side
+class MyObject(pb.Referenceable):
+ def remote_doIt(self):
+ return "did it"
+
+# Client Side
+ ...
+ def myCallback(result):
+ print result # result will be 'did it'
+ def myErrback(stacktrace):
+ print 'oh no, mr. bill!'
+ print stacktrace
+ myRemoteReference.doIt(pbcallback=myCallback,
+ pberrback=myErrback)
+</pre>
+
+ <h6>Listing 9: A remotely accessible object and accompanying
+ call</h6>
+ </blockquote>
+
+ <h3>Different Behavior for Different Perspectives</h3>
+
+ <p>Considering the problem of remote object access in terms of
+ a simulation demonstrates a requirement for the knowledge of an
+ actor with certain actions or requests. Often, when processing
+ message, it is useful to know who sent it, since different
+ results may be required depending on the permissions or state
+ of the caller.</p>
+
+ <p>A simple example is a game where certain an object is
+ invisible, but players with the "Heightened Perception"
+ enchantment can see it. When answering the question "What
+ objects are here?" it is important for the room to know who is
+ asking, to determine which objects they can see. Parallels to
+ the differences between "administrators" and "users" on an
+ average multi-user system are obvious.</p>
+
+ <p>Perspective Broker is named for the fact that it does not
+ broker only objects, but views of objects. As a user of the
+ <code>twisted.spread.pb</code> module, it is quite easy to
+ determine the caller of a method. All you have to do is
+ subclass <code>Viewable</code>.</p>
+
+ <blockquote>
+<pre class="python">
+# Server Side
+class Greeter(pb.Viewable):
+ def view_greet(self, actor):
+ return "Hello %s!\n" % actor.perspectiveName
+
+# Client Side
+ ...
+ remoteGreeter.greet(pbcallback=sys.stdout.write)
+ ...
+</pre>
+
+ <h6>Listing 10: An object responding to its calling
+ perspective</h6>
+ </blockquote>
+ Before any arguments sent by the client, the actor
+ (specifically, the Perspective instance through which this
+ object was retrieved) will be passed as the first argument to
+ any <code>view_xxx</code> methods.
+
+ <h3>Mechanisms for Sharing State</h3>
+
+ <p>In a simulation of any decent complexity, client and server
+ will wish to share structured data. Perspective Broker provides
+ a mechanism for both transferring (copying) and sharing
+ (caching) that state.</p>
+
+ <p>Whenever an object is passed as an argument to or returned
+ from a remote method call, that object is serialized using
+ <code>twisted.spread.jelly</code>; a serializer similar in some
+ ways to Python's native <code>pickle</code>. Originally,
+ <code>pickle</code> itself was going to be used, but there were
+ several security issues with the <code>pickle</code> code as it
+ stands. It is on these issues of security that
+ <code>pickle</code> and <code>twisted.spread.jelly</code> part
+ ways.</p>
+
+ <p>While <code>twisted.spread.jelly</code> handles a few basic
+ types such as strings, lists, dictionaries and numbers
+ automatically, all user-defined types must be registered both
+ for serialization and unserialization. This registration
+ process is necessary on the sending side in order to determine
+ if a particular object is shared, and whether it is shared as
+ state or behavior. On the receiving end, it's necessary to
+ prevent arbitrary code from being run when an object is
+ unserialized -- a significant security hole in
+ <code>pickle</code> for networked applications.</p>
+
+ <p>On the sending side, the registration is accomplished by
+ making the object you want to serialize a subclass of one of
+ the "flavors" of object that are handled by Perspective Broker.
+ A class may be <code>Referenceable</code>,
+ <code>Viewable</code>, <code>Copyable</code> or
+ <code>Cacheable</code>. These four classes correspond to
+ different ways that the object will be seen remotely.
+ Serialization flavors are mutually exclusive -- these 4 classes
+ may not be mixed in with each other.</p>
+
+ <ul>
+ <li><code>Referenceable</code>: The remote side will refer to
+ this object directly. Methods with the prefix
+ <code>remote_</code> will be callable on it. No state will be
+ transferred.</li>
+
+ <li><code>Viewable</code>: The remote side will refer to a
+ proxy for this object, which indicates what perspective
+ accessed this; as discussed above. Methods with the prefix
+ <code>view_</code> will be callable on it, and have an
+ additional first argument inserted (the perspective that
+ called the method). No state will be transferred.</li>
+
+ <li><code>Copyable</code>: Each time this object is
+ serialized, its state will be copied and sent. No methods are
+ remotely callable on it. By default, the state sent will be
+ the instance's <code>__dict__</code>, but a method
+ <code>getStateToCopyFor(perspective)</code> may be defined
+ which returns an arbitrary serializable object for
+ state.</li>
+
+ <li><code>Cacheable</code>: The first time this object is
+ serialized, its state will be copied and sent. Each
+ subsequent time, however, a reference to the original object
+ will be sent to the receiver. No methods will be remotely
+ callable on this object. By default, again, the state sent
+ will be the instance's <code>__dict__</code>but a method
+ <code>getStateToCacheAndObserveFor(perspective,
+ observer)</code> may be defined to return alternative state.
+ Since the state for this object is only sent once, the
+ <code>observer</code> argument is an object representative of
+ the receiver's representation of the <code>Cacheable</code>
+ after unserialization -- method calls to this object will be
+ resolved to methods prefixed with <code>observe_</code>,
+ <em>on the receiver's <code>RemoteCache</code> of this
+ object</em>. This may be used to keep the receiver's cache
+ up-to-date as relevant portions of the <code>Cacheable</code>
+ object change.</li>
+ </ul>
+
+ <h3>Publishing Objects with PB</h3>
+
+ <p>The previous samples of code have shown how an individual
+ object will interact over a previously-established PB
+ connection. In order to get to that connection, you need to do
+ some set-up work on both the client and server side; PB
+ attempts to minimize this effort.</p>
+
+ <p>There are two different approaches for setting up a PB
+ server, depending on your application's needs. In the simplest
+ case, where your application does not deal with the
+ abstractions above -- services, identities, and perspectives --
+ you can simply publish an object on a particular port.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class Echoer(pb.Root):
+ def remote_echo(self, st):
+ print 'echoing:', st
+ return st
+if __name__ == '__main__':
+ app = main.Application("pbsimple")
+ app.listenOn(8789, pb.BrokerFactory(Echoer()))
+ app.run()
+</pre>
+
+ <h6>Listing 11: Creating a simple PB server</h6>
+ </blockquote>
+
+ <p>Listing 11 shows how to publish a simple object which
+ responds to a single message, "echo", and returns whatever
+ argument is sent to it. There is very little to explain: the
+ "Echoer" class is a pb.Root, which is a small subclass of
+ Referenceable designed to be used for objects published by a
+ BrokerFactory, so Echoer follows the same rule for remote
+ access that Referenceable does. Connecting to this service is
+ almost equally simple.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def gotObject(object):
+ print "got object:",object
+ object.echo("hello network", pbcallback=gotEcho)
+def gotEcho(echo):
+ print 'server echoed:',echo
+ main.shutDown()
+def gotNoObject(reason):
+ print "no object:",reason
+ main.shutDown()
+pb.getObjectAt("localhost", 8789, gotObject, gotNoObject, 30)
+main.run()
+</pre>
+
+ <h6>Listing 12: A client for Echoer objects.</h6>
+ </blockquote>
+
+ <p>The utility function <code>pb.getObjectAt</code> retrieves
+ the root object from a hostname/port-number pair and makes a
+ callback (in this case, <code>gotObject</code>) if it can
+ connect and retrieve the object reference successfully, and an
+ error callback (<code>gotNoObject</code>) if it cannot connect
+ or the connection times out.</p>
+
+ <p><code>gotObject</code> receives the remote reference, and
+ sends the <code>echo</code> message to it. This call is
+ visually noticeable as a remote method invocation by the
+ distinctive <code>pbcallback</code> keyword argument. When the
+ result from that call is received, <code>gotEcho</code> will be
+ called, notifying us that in fact, the server echoed our input
+ ("hello network").</p>
+
+ <p>While this setup might be useful for certain simple types of
+ applications where there is no notion of a "user", the
+ additional complexity necessary for authentication and service
+ segregation is worth it. In particular, re-use of server code
+ for things like chat (twisted.words) is a lot easier with a
+ unified notion of users and authentication.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+class SimplePerspective(pb.Perspective):
+ def perspective_echo(self, text):
+ print 'echoing',text
+ return text
+class SimpleService(pb.Service):
+ def getPerspectiveNamed(self, name):
+ return SimplePerspective(name, self)
+if __name__ == '__main__':
+ import pbecho
+ app = main.Application("pbecho")
+ pbecho.SimpleService("pbecho",app).getPerspectiveNamed("guest")\
+ .makeIdentity("guest")
+ app.listenOn(pb.portno, pb.BrokerFactory(pb.AuthRoot(app)))
+ app.save("start")
+</pre>
+
+ <h6>Listing 13: A PB server using twisted's "passport"
+ authentication.</h6>
+ </blockquote>
+
+ <p>In terms of the "functionality" it offers, this server is
+ identical. It provides a method which will echo some simple
+ object sent to it. However, this server provides it in a manner
+ which will allow it to cooperate with multiple other
+ authenticated services running on the same connection, because
+ it uses the central Authorizer for the application.</p>
+
+ <p>On the line that creates the <code>SimpleService</code>,
+ several things happen.</p>
+
+ <ol>
+ <li>A SimpleService is created and persistently added to the
+ <code>Application</code> instance.</li>
+
+ <li>A SimplePerspective is created, via the overridden
+ <code>getPerspectiveNamed</code> method.</li>
+
+ <li>That <code>SimplePerspective</code> has an
+ <code>Identity</code> generated for it, and persistently
+ added to the <code>Application</code>'s
+ <code>Authorizer</code>. The created identity will have the
+ same name as the perspective ("guest"), and the password
+ supplied (also, "guest"). It will also have a reference to
+ the service "pbecho" and a perspective named "guest", by
+ name. The <code>Perspective.makeIdentity</code> utility
+ method prevents having to deal with the intricacies of the
+ passport <code>Authorizer</code> system when one doesn't
+ require strongly separate <code>Identity</code>s and
+ <code>Perspective</code>s.</li>
+ </ol>
+ <br />
+ <br />
+
+
+ <p>Also, this server does not run itself, but instead persists
+ to a file which can be run with twistd, offering all the usual
+ amenities of daemonization, logging, etc. Once the server is
+ run, connecting to it is similar to the previous example.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.internet import main
+def success(message):
+ print "Message received:",message
+ main.shutDown()
+def failure(error):
+ print "Failure...",error
+ main.shutDown()
+def connected(perspective):
+ perspective.echo("hello world",
+ pbcallback=success,
+ pberrback=failure)
+ print "connected."
+pb.connect(connected, failure, "localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 30)
+main.run()
+</pre>
+
+ <h6>Listing 14: Connecting to an Authorized Service</h6>
+ </blockquote>
+ <br />
+ <br />
+
+
+ <p>This introduces a new utility -- <code>pb.connect</code>.
+ This function takes a long list of arguments and manages the
+ handshaking and challenge/response aspects of connecting to a
+ PB service perspective, eventually calling back to indicate
+ either success or failure. In this particular example, we are
+ connecting to localhost on the default PB port (8787),
+ authenticating to the identity "guest" with the password
+ "guest", requesting the perspective "guest" from the service
+ "pbecho". If this can't be done within 30 seconds, the
+ connection will abort.</p>
+
+ <p>In these examples, I've attempted to show how Twisted makes
+ event-based scripting easier; this facilitates the ability to
+ run short scripts as part of a long-running process. However,
+ event-based programming is not natural to procedural scripts;
+ it is more generally accepted that GUI programs will be
+ event-driven whereas scripts will be blocking. An alternative
+ client to our <code>SimpleService</code> using GTK illustrates
+ the seamless meshing of Twisted and GTK.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.internet import main, ingtkernet
+from twisted.spread.ui import gtkutil
+import gtk
+ingtkernet.install()
+class EchoClient:
+ def __init__(self, echoer):
+ l.hide()
+ self.echoer = echoer
+ w = gtk.GtkWindow(gtk.WINDOW_TOPLEVEL)
+ vb = gtk.GtkVBox(); b = gtk.GtkButton("Echo:")
+ self.entry = gtk.GtkEntry(); self.outry = gtk.GtkEntry()
+ w.add(vb)
+ map(vb.add, [b, self.entry, self.outry])
+ b.connect('clicked', self.clicked)
+ w.connect('destroy', gtk.mainquit)
+ w.show_all()
+ def clicked(self, b):
+ txt = self.entry.get_text()
+ self.entry.set_text("")
+ self.echoer.echo(txt, pbcallback=self.outry.set_text)
+l = gtkutil.Login(EchoClient, None, initialService="pbecho")
+l.show_all()
+gtk.mainloop()
+</pre>
+
+ <h6>Listing 15: A Twisted GUI application</h6>
+ </blockquote>
+
+ <h3>Event-Driven Web Object Publishing with Web.Widgets</h3>
+
+ <p>Although PB will be interesting to those people who wish to
+ write custom clients for their networked applications, many
+ prefer or require a web-based front end. Twisted's built-in web
+ server has been designed to accommodate this desire, and the
+ presentation framework that one would use to write such an
+ application is <code>twisted.web.widgets</code>. Web.Widgets
+ has been designed to work in an event-based manner, without
+ adding overhead to the designer or the developer's
+ work-flow.</p>
+
+ <p>Surprisingly, asynchronous web interfaces fit very well into
+ the normal uses of purpose-built web toolkits such as PHP. Any
+ experienced PHP, Zope, or WebWare developer will tell you that
+ <em>separation of presentation, content, and logic</em> is very
+ important. In practice, this results in a "header" block of
+ code which sets up various functions which are called
+ throughout the page, some of which load blocks of content to
+ display. While PHP does not enforce this, it is certainly
+ idiomatic. Zope enforces it to a limited degree, although it
+ still allows control structures and other programmatic elements
+ in the body of the content.</p>
+
+ <p>In Web.Widgets, strict enforcement of this principle
+ coincides very neatly with a "hands-free" event-based
+ integration, where much of the work of declaring callbacks is
+ implicit. A "Presentation" has a very simple structure for
+ evaluating Python expressions and giving them a context to
+ operate in. The "header" block which is common to many
+ templating systems becomes a class, which represents an
+ enumeration of events that the template may generate, each of
+ which may be responded to either immediately or latently.</p>
+
+ <p>For the sake of simplicity, as well as maintaining
+ compatibility for potential document formats other than HTML,
+ Presentation widgets do not attempt to parse their template as
+ HTML tags. The structure of the template is <code>"HTML Text
+ %%%%python_expression()%%%% more HTML Text"</code>. Every set
+ of 4 percent signs (%%%%) switches back and forth between
+ evaluation and printing.</p>
+
+ <p>No control structures are allowed in the template. This was
+ originally thought to be a potentially major inconvenience, but
+ with use of the Web.Widgets code to develop a few small sites,
+ it has seemed trivial to encapsulate any table-formatting code
+ within a method; especially since those methods can take string
+ arguments if there's a need to customize the table's
+ appearance.</p>
+
+ <p>The namespace for evaluating the template expressions is
+ obtained by scanning the class hierarchy for attributes, and
+ getting each of those attributes from the current instance.
+ This means that all methods will be bound methods, so
+ indicating "self" explicitly is not required. While it is
+ possible to override the method for creating namespaces, using
+ this default has the effect of associating all presentation
+ code for a particular widget in one class, along with its
+ template. If one is working with a non-programmer designer, and
+ the template is in an external file, it is always very clear to
+ the designer what functionality is available to them in any
+ given scope, because there is a list of available methods for
+ any given class.</p>
+
+ <p>A convenient event to register for would be a response from
+ the PB service that we just implemented. We can use the
+ <code>Deferred</code> class in order to indicate to the widgets
+ framework that certain work has to be done later. This is a
+ Twisted convention which one can currently use in PB as well as
+ webwidgets; any framework which needs the ability to defer a
+ return value until later should use this facility. Elements of
+ the page will be rendered from top to bottom as data becomes
+ available, so the page will not be blocked on rendering until
+ all deferred elements have been completed.</p>
+
+ <blockquote>
+<pre class="python">
+from twisted.spread import pb
+from twisted.python import defer
+from twisted.web import widgets
+class EchoDisplay(widgets.Presentation):
+ template = """&lt;H1&gt;Welcome to my widget, displaying %%%%echotext%%%%.&lt;/h1&gt;
+ &lt;p&gt;Here it is: %%%%getEchoPerspective()%%%%&lt;/p&gt;"""
+ echotext = 'hello web!'
+ def getEchoPerspective(self):
+ d = defer.Deferred()
+ pb.connect(d.callback, d.errback, "localhost", pb.portno,
+ "guest", "guest", "pbecho", "guest", 1)
+ d.addCallbacks(self.makeListOf, self.formatTraceback)
+ return ['&lt;b&gt;',d,'&lt;/b&gt;']
+ def makeListOf(self, echoer):
+ d = defer.Deferred()
+ echoer.echo(self.echotext, pbcallback=d.callback, pberrback=d.errback)
+ d.addCallbacks(widgets.listify, self.formatTraceback)
+ return [d]
+if __name__ == "__main__":
+ from twisted.web import server
+ from twisted.internet import main
+ a = main.Application("pbweb")
+ gdgt = widgets.Gadget()
+ gdgt.widgets['index'] = EchoDisplay()
+ a.listenOn(8080, server.Site(gdgt))
+ a.run()
+</pre>
+
+ <h6>Listing 16: an event-based web widget.</h6>
+ </blockquote>
+
+ <p>Each time a Deferred is returned as part of the page, the
+ page will pause rendering until the deferred's
+ <code>callback</code> method is invoked. When that callback is
+ made, it is inserted at the point in the page where rendering
+ left off.</p>
+
+ <p>If necessary, there are options within web.widgets to allow
+ a widget to postpone or cease rendering of the entire page --
+ for example, it is possible to write a FileDownload widget,
+ which will override the rendering of the entire page and
+ replace it with a file download.</p>
+
+ <p>The final goal of web.widgets is to provide a framework
+ which encourages the development of usable library code. Too
+ much web-based code is thrown away due to its particular
+ environment requirements or stylistic preconceptions it carries
+ with it. The goal is to combine the fast-and-loose iterative
+ development cycle of PHP with the ease of installation and use
+ of Zope's "Product" plugins.</p>
+
+ <h3>Things That Twisted Does Not Do</h3>
+
+ <p>It is unfortunately well beyond the scope of this paper to
+ cover all the functionality that Twisted provides, but it
+ serves as a good overview. It may seem as though twisted does
+ anything and everything, but there are certain features we
+ never plan to implement because they are simply outside the
+ scope of the project.</p>
+
+ <p>Despite the multiple ways to publish and access objects,
+ Twisted does not have or support an interface definition
+ language. Some developers on the Twisted project have
+ experience with remote object interfaces that require explicit
+ specification of all datatypes during the design of an object's
+ interface. We feel that such interfaces are in the spirit of
+ statically-typed languages, and are therefore suited to the
+ domain of problems where statically-typed languages excel.
+ Twisted has no plans to implement a protocol schema or static
+ type-checking mechanism, as the efficiency gained by such an
+ approach would be quickly lost again by requiring the type
+ conversion between Python's dynamic types and the protocol's
+ static ones. Since one of the key advantages of Python is its
+ extremely flexible dynamic type system, we felt that a
+ dynamically typed approach to protocol design would share some
+ of those advantages.</p>
+
+ <p>Twisted does not assume that all data is stored in a
+ relational database, or even an efficient object database.
+ Currently, Twisted's configuration state is all stored in
+ memory at run-time, and the persistent parts of it are pickled
+ at one go. There are no plans to move the configuration objects
+ into a "real" database, as we feel it is easier to keep a naive
+ form of persistence for the default case and let
+ application-specific persistence mechanisms handle persistence.
+ Consequently, there is no object-relational mapping in Twisted;
+ <code>twisted.enterprise</code> is an interface to the
+ relational paradigm, not an object-oriented layer over it.</p>
+
+ <p>There are other things that Twisted will not do as well, but
+ these have been frequently discussed as possibilities for it.
+ The general rule of thumb is that if something will increase
+ the required installation overhead, then Twisted will probably
+ not do it. Optional additions that enhance integration with
+ external systems are always welcome: for example, database
+ drivers for Twisted or a CORBA IDL for PB objects.</p>
+
+ <h3>Future Directions</h3>
+
+ <p>Twisted is still a work in progress. The number of protocols
+ in the world is infinite for all practical purposes, and it
+ would be nice to have a central repository of event-based
+ protocol implementations. Better integration with frameworks
+ and operating systems is also a goal. Examples for integration
+ opportunities are automatic creation of installer for "tap"
+ files (for Red Hat Packager-based distributions, FreeBSD's
+ package management system or Microsoft Windows(tm) installers),
+ and integration with other event-dispatch mechanisms, such as
+ win32's native message dispatch.</p>
+
+ <p>A still-nascent feature of Twisted, which this paper only
+ touches briefly upon, is <code>twisted.enterprise</code>: it is
+ planned that Twisted will have first-class database support
+ some time in the near future. In particular, integration
+ between twisted.web and twisted.enterprise to allow developers
+ to have SQL conveniences that they are used to from other
+ frameworks.</p>
+
+ <p>Another direction that we hope Twisted will progress in is
+ standardization and porting of PB as a messaging protocol. Some
+ progress has already been made in that direction, with XEmacs
+ integration nearly ready for release as of this writing.</p>
+
+ <p>Tighter integration of protocols is also a future goal, such
+ an FTP server that can serve the same resources as a web
+ server, or a web server that allows users to change their POP3
+ password. While Twisted is already a very tightly integrated
+ framework, there is always room for more integration. Of
+ course, all this should be done in a flexible way, so the
+ end-user will choose which components to use -- and have those
+ components work well together.</p>
+
+ <h3>Conclusions</h3>
+
+ <p>As shown, Twisted provides a lot of functionality to the
+ Python network programmer, while trying to be in his way as
+ little as possible. Twisted gives good tools for both someone
+ trying to implement a new protocol, or someone trying to use an
+ existing protocol. Twisted allows developers to prototype and
+ develop object communication models with PB, without designing
+ a byte-level protocol. Twisted tries to have an easy way to
+ record useful deployment options, via the
+ <code>twisted.tap</code> and plugin mechanisms, while making it
+ easy to generate new forms of deployment. And last but not
+ least, even Twisted is written in a high-level language and
+ uses its dynamic facilities to give an easy API, it has
+ performance which is good enough for most situations -- for
+ example, the web server can easily saturate a T1 line serving
+ dynamic requests on low-end machines.</p>
+
+ <p>While still an active project, Twisted can already used for
+ production programs. Twisted can be downloaded from the main
+ Twisted site (http://www.twistedmatrix.com) where there is also
+ documentation for using and programming Twisted.</p>
+
+ <h3>Acknowledgements</h3>
+
+ <p>We wish to thank Sean Riley, Allen Short, Chris Armstrong,
+ Paul Swartz, J&uuml;rgen Hermann, Benjamin Bruheim, Travis B.
+ Hartwell, and Itamar Shtull-Trauring for being a part of the
+ Twisted development team with us.</p>
+
+ <p>Thanks also to Jason Asbahr, Tommi Virtanen, Gavin Cooper,
+ Erno Kuusela, Nick Moffit, Jeremy Fincher, Jerry Hebert, Keith
+ Zaback, Matthew Walker, and Dan Moniz, for providing insight,
+ commentary, bandwidth, crazy ideas, and bug-fixes (in no
+ particular order) to the Twisted team.</p>
+
+ <h3>References</h3>
+
+ <ol>
+ <li>The Twisted site, http://www.twistedmatrix.com</li>
+
+ <li>Douglas Schmidt, Michael Stal, Hans Rohnert and Frank
+ Buschmann, Pattern-Oriented Software Architecture, Volume 2,
+ Patterns for Concurrent and Networked Objects, John Wiley
+ &amp; Sons</li>
+
+ <li>Abhishek Chandra, David Mosberger, Scalability of Linux
+ Event-Dispatch Mechanisms, USENIX 2001,
+ http://lass.cs.umass.edu/~abhishek/papers/usenix01/paper.ps</li>
+
+ <li>Protocol specifications, http://www.rfc-editor.com</li>
+
+ <li>The Twisted Philosophical FAQ,
+ http://www.twistedmatrix.com/page.epy/twistedphil.html</li>
+
+ <li>Twisted Advocacy,
+ http://www.twistedmatrix.com/page.epy/whytwisted.html</li>
+
+ <li>Medusa, http://www.nightmare.com/medusa/index.html</li>
+
+ <li>Using Spreadable Web Servers,
+ http://www.twistedmatrix.com/users/jh.twistd/python/moin.cgi/TwistedWeb</li>
+
+ <li>Twisted Spread implementations for other languages,
+ http://www.twistedmatrix.com/users/washort/</li>
+
+ <li>PHP: Hypertext Preprocessor, http://www.php.net/</li>
+
+ <li>The Z Object Publishing Environment,
+ http://www.zope.org/, http://zope.com/</li>
+ </ol>
+ </body>
+</html>
+
diff --git a/doc/historic/stylesheet.css b/doc/historic/stylesheet.css
new file mode 100644
index 0000000..8235f6c
--- /dev/null
+++ b/doc/historic/stylesheet.css
@@ -0,0 +1,178 @@
+
+body
+{
+ margin-left: 2em;
+ margin-right: 2em;
+ border: 0px;
+ padding: 0px;
+ font-family: sans-serif;
+ }
+
+.done { color: #005500; background-color: #99ff99 }
+.notdone { color: #550000; background-color: #ff9999;}
+
+pre
+{
+ padding: 1em;
+ border: thin black solid;
+}
+
+.boxed
+{
+ padding: 1em;
+ border: thin black solid;
+}
+
+.shell
+{
+ background-color: #ffffdd;
+}
+
+.python
+{
+ background-color: #dddddd;
+}
+
+.htmlsource
+{
+ background-color: #dddddd;
+}
+
+.py-prototype
+{
+ background-color: #ddddff;
+}
+
+
+.python-interpreter
+{
+ background-color: #ddddff;
+}
+
+.doit
+{
+ border: thin blue dashed ;
+ background-color: #0ef
+}
+
+.py-src-comment
+{
+ color: #1111CC
+}
+
+.py-src-keyword
+{
+ color: #3333CC;
+ font-weight: bold;
+}
+
+.py-src-parameter
+{
+ color: #000066;
+ font-weight: bold;
+}
+
+.py-src-identifier
+{
+ color: #CC0000
+}
+
+.py-src-string
+{
+
+ color: #115511
+}
+
+.py-src-endmarker
+{
+ display: block; /* IE hack; prevents following line from being sucked into the py-listing box. */
+}
+
+.py-listing
+{
+ margin: 1ex;
+ border: thin solid black;
+ background-color: #eee;
+}
+
+.py-listing pre
+{
+ margin: 0px;
+ border: none;
+ border-bottom: thin solid black;
+}
+
+.py-listing .python
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.py-listing .htmlsource
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.py-caption
+{
+ text-align: center;
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.py-filename
+{
+ font-style: italic;
+ }
+
+.manhole-output
+{
+ color: blue;
+}
+
+hr
+{
+ display: inline;
+ }
+
+ul
+{
+ padding: 0px;
+ margin: 0px;
+ margin-left: 1em;
+ padding-left: 1em;
+ border-left: 1em;
+ }
+
+li
+{
+ padding: 2px;
+ }
+
+dt
+{
+ font-weight: bold;
+ margin-left: 1ex;
+ }
+
+dd
+{
+ margin-bottom: 1em;
+ }
+
+div.note
+{
+ background-color: #FFFFCC;
+ margin-top: 1ex;
+ margin-left: 5%;
+ margin-right: 5%;
+ padding-top: 1ex;
+ padding-left: 5%;
+ padding-right: 5%;
+ border: thin black solid;
+}
diff --git a/doc/historic/template-notoc.tpl b/doc/historic/template-notoc.tpl
new file mode 100644
index 0000000..5cc2efc
--- /dev/null
+++ b/doc/historic/template-notoc.tpl
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <title></title>
+ <link type="text/css" rel="stylesheet" href="stylesheet.css" />
+ </head>
+
+ <body bgcolor="white">
+ <div class="body" />
+ </body>
+</html>
diff --git a/doc/historic/template.tpl b/doc/historic/template.tpl
new file mode 100644
index 0000000..bb40833
--- /dev/null
+++ b/doc/historic/template.tpl
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+<title></title>
+<link type="text/css" rel="stylesheet"
+href="stylesheet.css" />
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title"></h1>
+ <div class="toc"></div>
+ <div class="body">
+
+ </div>
+ </body>
+</html>
+
diff --git a/doc/historic/twisted-debian.html b/doc/historic/twisted-debian.html
new file mode 100644
index 0000000..a88abe5
--- /dev/null
+++ b/doc/historic/twisted-debian.html
@@ -0,0 +1,96 @@
+<html><head><title>Twisted and Debian</title></head><body>
+
+<h1>Twisted and Debian</h1>
+
+<h3>Moshe Zadka</h3>
+<h4>&lt;moshez@debian.org&gt; &lt;moshez@twistedmatrix.com&gt;</h4>
+
+<h2>Twisted</h2>
+
+<p>
+Twisted is a Python networking framework. It is useful for development
+of both clients and servers, and strives to support as many externalities
+as possible -- from network protocols (with over a two dozen RFCs implemented)
+to GUI toolkits (supporting GTK+, Qt, wxWindows and Tk).
+</p>
+
+<h2>Debian</h2>
+
+<p>
+Debian is a free, stable and comprehensive operating system, based on GNU
+software and the Linux kernel. Debian supports eleven hardware archtecture
+and over 6000 programs. Debian, as a free, community-supported, operating
+system, has been used as a base for many other operating systems, including
+Lindows and Knoppix.
+</p>
+
+
+<h2>Using Twisted on a Debian System</h2>
+
+<p>
+The latest stable release of Debian, woody, comes with Twisted 0.15.5 built
+in. New versions of Twisted, which are tested on both stable and unstable,
+are always available from
+"deb http://twistedmatrix.com/users/moshez/apt". So, even those using
+stable Debian can use the latest Twisted releases, including the upcoming 1.0,
+without the overhead of adding unstable sources to their sources.list, dealing
+with apt-pinning or building the sources themselves.
+</p>
+
+<p>
+Of course, users of Debian unstable can get the releases directly from Debian
+-- the released packages, already having been tested on the main Twisted
+Debian machine, are usually uploaded to Debian unstable within hours of
+the official release.
+</p>
+
+<p>
+Twisted supports,
+as fully as possible, the Python versions available in Debian -- currently,
+2.1, 2.2 and pre-releases of 2.3. For those needing just a version of
+Twisted which works with the Debian default Python version, "python-twisted"
+is available. For low-impact on production servers, the documentation of
+Twisted (over half a megabyte) is packaged seperately. Twisted uses
+the Recommends: and Suggests: fields, to allow the Debian packaging tools
+to supply the information about which packages can be used to maximise
+the potential of Twisted.
+</p>
+
+<p>
+For those on the bleeding edge, or people who want to make sure their
+applications work flawlessly for the next version of Twisted, all release
+candidates are available from the apt source
+"deb http://twistedmatrix.com/users/moshez/snapshot". These are the release
+candidates the Twisted team uses itself to prepare for the next release --
+but third party developers interested in assuring compatibility are also
+welcome to use them.
+</p>
+
+<h2>Using Twisted's Debian Integration</h2>
+
+<p>
+For Twisted-based server application developers who want to deploy on
+Debian, Twisted supplies the <code>tap2deb</code> program. This program
+wraps a tap file (Twisted Application Pickle, a Twisted configuration)
+in a Debian archive, including correct installation and removal scripts
+and <code>init.d</code> scripts. For the more savvy Debian users, the
+<code>tap2deb</code> also generates the source package, allowing her
+to modify and polish things which automated software cannot detect
+(such as dependencies or relationships to virtual packages). In addition,
+the Twisted team itself intends to produce Debian packages for some common
+services, such as web servers and an inetd replacement. Those packages
+will enjoy the best of all worlds -- both the consistency which comes
+from being based on the <code>tap2deb</code> and the delicate manual
+tweaking of a Debian maintainer, insuring perfect integration with
+Debian.
+</p>
+
+<p>
+This things will insure you can run a fully functional Debian system
+which relies on Twisted for many of its core, and security sensitive,
+portions -- thus, eliminating many of the classical security holes
+(such as buffer overlows, uninitialized memory access and stack smashing),
+allowing you to sleep better at night.
+</p>
+
+</body></html>
diff --git a/doc/index.xhtml b/doc/index.xhtml
new file mode 100644
index 0000000..d189fd8
--- /dev/null
+++ b/doc/index.xhtml
@@ -0,0 +1,115 @@
+<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Twisted Documentation Index</title></head><body>
+<h1>Twisted Documentation Index</h1>
+
+<ul>
+ <li>
+ <a href="core/index.xhtml">Twisted Core</a>
+ <ul>
+ <li>
+ <a href="core/howto/index.xhtml">Developer guides</a>
+ </li>
+ <li>
+ <a href="core/examples/index.xhtml">Twisted code examples</a>
+ </li>
+ <li>
+ <a href="core/specifications/index.xhtml">Specifications</a>
+ </li>
+ <li>
+ <a href="core/development/index.xhtml">Development of Twisted</a>
+ </li>
+ <li>
+ <a href="http://twistedmatrix.com/documents/current/api/">API reference</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="conch/index.xhtml">Twisted Conch</a>
+ <ul>
+ <li>
+ <a href="conch/howto/index.xhtml">Twisted Conch documentation</a>
+ </li>
+ <li>
+ <a href="conch/examples/index.xhtml">Twisted Conch code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="lore/index.xhtml">Twisted Lore</a>
+ <ul>
+ <li>
+ <a href="lore/howto/index.xhtml">Twisted Lore documentation</a>
+ </li>
+ <li>
+ <a href="lore/examples/index.xhtml">Twisted Lore examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="mail/index.xhtml">Twisted Mail</a>
+ <ul>
+ <li>
+ <a href="mail/tutorial/smtpclient/smtpclient.xhtml">Twisted Mail tutorial</a>
+ </li>
+ <li>
+ <a href="mail/examples/index.xhtml">Twisted Mail code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="names/index.xhtml">Twisted Names</a>
+ <ul>
+ <li>
+ <a href="names/howto/index.xhtml">Twisted Names documentation</a>
+ </li>
+ <li>
+ <a href="names/examples/index.xhtml">Twisted Names code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="pair/index.xhtml">Twisted Pair</a>
+ <ul>
+ <li>
+ <a href="pair/howto/index.xhtml">Twisted Pair documentation</a>
+ </li>
+ <li>
+ <a href="pair/examples/index.xhtml">Twisted Pair code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="web/index.xhtml">Twisted Web</a>
+ <ul>
+ <li>
+ <a href="web/howto/index.xhtml">Twisted Web documentation</a>
+ </li>
+ <li>
+ <a href="web/examples/index.xhtml">Twisted Web code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="words/index.xhtml">Twisted Words</a>
+ <ul>
+ <li>
+ <a href="words/howto/index.xhtml">Twisted Words documentation</a>
+ </li>
+ <li>
+ <a href="words/examples/index.xhtml">Twisted Words code examples</a>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <a href="historic/index.xhtml">Historical Documents</a>
+ <ul>
+ <li>
+ <a href="historic/index.xhtml#id1">2003</a>
+ </li>
+ <li>
+ <a href="historic/index.xhtml#previously">Previously</a>
+ </li>
+ </ul>
+ </li>
+</ul>
+
+</body></html>
diff --git a/doc/lore/examples/example.html b/doc/lore/examples/example.html
new file mode 100644
index 0000000..b1c6684
--- /dev/null
+++ b/doc/lore/examples/example.html
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+<title>Your Title Here</title>
+</head>
+
+<body>
+<h1>Your Title Here</h1>
+
+<h2>Introduction</h2>
+
+<p>The introduction is an important part of your
+document<span class="footnote">though it should not be the most important
+part</span>.</p>
+
+<div class="note">
+<p>It is generally a <em>very</em> good to write other section except
+the introduction too.</p>
+</div>
+
+<p>
+You can use the following ways to write lists:
+<ul>
+<li>Unordered lists</li>
+<li>
+ <ol>
+ <li>ordered lists</li>
+ </ol>
+</li>
+<li><dl>
+ <dt>defintion lists</dt>
+ <dd>with definitions</dd>
+ </dl>
+</li>
+</ul>
+</p>
+
+<p>Shell commands look like <code class="shell">ls -l</code>. Python
+snippets look like <code class="python">print 1</code>.</p>
+
+<p>Longer python things should be in pre</p>
+
+<pre class="python">
+def foo():
+ pass
+</pre>
+
+<p>Or they can be outlined</p>
+
+<a href="rootscript.py" class="py-listing">rootscript.py</a>
+
+<p>Likewise, HTML can be outlined too:</p>
+
+<a href="example.html" class="py-listing">example.html</a>
+
+</body> </html>
diff --git a/doc/lore/examples/index.html b/doc/lore/examples/index.html
new file mode 100644
index 0000000..b3402b8
--- /dev/null
+++ b/doc/lore/examples/index.html
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Lore examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Lore examples</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+ <ul>
+ <li><a href="example.html" shape="rect">example.html</a></li>
+ <li><a href="slides-template.tpl" shape="rect">slides-template.tpl</a></li>
+ </ul>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/examples/slides-template.tpl b/doc/lore/examples/slides-template.tpl
new file mode 100644
index 0000000..9d7a420
--- /dev/null
+++ b/doc/lore/examples/slides-template.tpl
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <title>Twisted Documentation: </title>
+ <link type="text/css" rel="stylesheet" href="stylesheet.css" />
+ </head>
+
+ <body bgcolor="white">
+ <div><span>Previous: <a class="previous"><span class="previous" /></a></span>
+ <br />
+ <span>Next: <a class="next"><span class="next" /></a></span></div>
+ <h1 class="title"></h1>
+ <div class="body">
+
+ </div>
+ </body>
+</html>
+
diff --git a/doc/lore/howto/extend-lore.html b/doc/lore/howto/extend-lore.html
new file mode 100644
index 0000000..e57a2ff
--- /dev/null
+++ b/doc/lore/howto/extend-lore.html
@@ -0,0 +1,427 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Extending the Lore Documentation System</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Extending the Lore Documentation System</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Inputs and Outputs</a></li><ul><li><a href="#auto2">Creating New Inputs</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Overview<a name="auto0"/></h2>
+
+<p>The <a href="lore.html" shape="rect">Lore Documentation System</a>, out of the box, is
+specialized for documenting Twisted. Its markup includes CSS classes for
+Python, HTML, filenames, and other Twisted-focused categories. But don't
+think this means Lore can't be used for other documentation tasks! Lore is
+designed to allow extensions, giving any Python programmer the ability to
+customize Lore for documenting almost anything.</p>
+
+<p>There are several reasons why you would want to extend Lore. You may want
+to attach file formats Lore does not understand to your documentation. You
+may want to create callouts that have special meanings to the reader, to give a
+memorable appearance to text such as, <q>WARNING: This software was written by
+ a frothing madman!</q> You may want to create color-coding for a different
+programming language, or you may find that Lore does not provide you with
+enough structure to mark your document up completely. All of these situations
+can be solved by creating an extension.</p>
+
+<h2>Inputs and Outputs<a name="auto1"/></h2>
+
+<p>Lore works by reading the HTML source of your document, and
+producing whatever output the user specifies on the command line. If
+the HTML document is well-formed XML that meets a certain minimum
+standard, Lore will be able to to produce some output. All Lore
+extensions will be written to redefine the <em>input</em>, and most
+will redefine the output in some way. The name of the default input
+is <q>lore</q>. When you write your extension, you will come up with
+a new name for your input, telling Lore what rules to use to process
+the file.</p>
+
+<p>Lore can produce XHTML, LaTeX, and DocBook document formats, which can be
+displayed directly if you have a user agent capable of viewing them, or
+processed into a third form such as PostScript or PDF. Another output is
+called <q>lint</q>, after the static-checking utility for C, and is used for
+the same reason: to statically check input files for problems. The
+<q>lint</q> output is just a stream of error messages, not a formatted
+document, but is important because it gives users the ability to validate
+their input before trying to process it. For the first example, the only
+output we will be concerned with is LaTeX.</p>
+
+<h3>Creating New Inputs<a name="auto2"/></h3>
+<p>Create a new input to tell Lore that your document is marked up differently
+from a vanilla Lore document. This gives you the power to define a new tag
+class, for example:</p>
+<pre xml:space="preserve">
+&lt;p&gt;The Frabjulon &lt;span class=&quot;productname&quot;&gt;Limpet 2000&lt;/span&gt;
+is the &lt;span class=&quot;marketinglie&quot;&gt;industry-leading&lt;/span&gt; aquatic
+mollusc counter, bar none.&lt;/p&gt;
+</pre>
+
+<p>The above HTML is an instance of a new input to Lore, which we will call
+MyHTML, to differentiate it from the <q>lore</q> input. We want it to have
+the following markup:</p>
+<ul>
+ <li>A <code>productname</code> class for the &lt;span&gt; tag, which
+ produces underlined text</li>
+ <li>A <code>marketinglie</code> class for &lt;span&gt; tag, which
+ produces larger type, bold text</li>
+</ul>
+<p>Note that I chose class names that are valid Python identifiers. You will
+see why shortly. To get these two effects in Lore's HTML output, all we have
+to do is create a cascading stylesheet (CSS), and use it in the Lore XHTML
+Template. However, we also want these effects to work in LaTeX, and we want
+the output of lint to produce no warnings when it sees lines with these 2
+classes. To make LaTeX and lint work, we start by creating a plugin.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">plugin</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IPlugin</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span>.<span class="py-src-variable">scripts</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IProcessor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyHTML</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IPlugin</span>, <span class="py-src-variable">IProcessor</span>)
+
+ <span class="py-src-variable">name</span> = <span class="py-src-string">&quot;myhtml&quot;</span>
+ <span class="py-src-variable">moduleName</span> = <span class="py-src-string">&quot;myhtml.factory&quot;</span>
+</pre><div class="caption">
+ Listing 1: The Plugin File - <a href="listings/lore/a_lore_plugin.py"><span class="filename">listings/lore/a_lore_plugin.py</span></a></div></div>
+
+ <p>Create this file in a <code class="shell">twisted/plugins/</code>
+ directory (<em>not</em> a package) which is located in a directory in the
+ Python module search path. See the <a href="../../core/howto/plugin.html" shape="rect">Twisted
+ plugin howto</a> for more details on plugins.</p>
+
+ <p>Users of your extension will pass the value of your plugin's <code class="python">name</code> attribute to lore with the <code class="shell">--input</code> parameter on the command line to select it. For
+ example, to select the plugin defined above, a user would pass <code class="shell">--input myhtml</code>. The <code class="python">moduleName</code> attribute tells Lore where to find the code
+ implementing the plugin. In particular, this module should have a <code class="python">factory</code> attribute which defines a <code class="python">generator_</code>-prefixed method for each output format it
+ supports. Next we'll look at this module.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">default</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myhtml</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">spitters</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyProcessingFunctionFactory</span>(<span class="py-src-parameter">default</span>.<span class="py-src-parameter">ProcessingFunctionFactory</span>):
+ <span class="py-src-variable">latexSpitters</span>={<span class="py-src-variable">None</span>: <span class="py-src-variable">spitters</span>.<span class="py-src-variable">MyLatexSpitter</span>,
+ }
+
+<span class="py-src-comment"># initialize the global variable factory with an instance of your new factory</span>
+<span class="py-src-variable">factory</span>=<span class="py-src-variable">MyProcessingFunctionFactory</span>()
+</pre><div class="caption">Listing 2: The Input
+ Factory - <a href="listings/lore/factory.py-1"><span class="filename">listings/lore/factory.py-1</span></a></div></div>
+
+<p>In Listing 2, we create a subclass of ProcessingFunctionFactory.
+This class provides a hook for you, a class variable
+named <code>latexSpitters</code>. This variable tells Lore what new
+class will be generating LaTeX from your input format. We
+redefine <code>latexSpitters</code> to <code>MyLatexSpitter</code> in
+the subclass because this class knows what to do with the new input we
+have already defined. Last, you must define the module-level
+variable <code class="py-src-identifier">factory</code>. It should be
+an instance with the same interface
+as <code class="py-src-identifier">ProcessingFunctionFactory</code>
+(e.g. an instance of a subclass, in this
+case, <code class="py-src-identifier">MyProcessingFunctionFactory</code>).</p>
+
+<p>Now let's actually write some code to generate the LaTeX. Doing this
+requires at least a familiarity with the LaTeX language. Search Google for
+<q>latex tutorial</q> and you will find any number of useful LaTeX
+resources.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">latex</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span>.<span class="py-src-variable">latex</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">processFile</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyLatexSpitter</span>(<span class="py-src-parameter">latex</span>.<span class="py-src-parameter">LatexSpitter</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">visitNode_span_productname</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">node</span>):
+ <span class="py-src-comment"># start an underline section in LaTeX</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\underline{'</span>)
+ <span class="py-src-comment"># process the node and its children</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">visitNodeDefault</span>(<span class="py-src-variable">node</span>)
+ <span class="py-src-comment"># end the underline block</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'}'</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">visitNode_span_marketinglie</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">node</span>):
+ <span class="py-src-comment"># this example turns on more than one LaTeX effect at once</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\begin{bf}\\begin{Large}'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">visitNodeDefault</span>(<span class="py-src-variable">node</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\end{Large}\\end{bf}'</span>)
+</pre><div class="caption">Listing 3:
+ spitters.py - <a href="listings/lore/spitters.py-1"><span class="filename">listings/lore/spitters.py-1</span></a></div></div>
+
+<p>The method <code>visitNode_span_productname</code> is our handler
+for &lt;span&gt; tags with the <code>class=&quot;productname&quot;</code>
+identifier. Lore knows to try methods <code>visitNode_span_*</code>
+and <code>visitNode_div_*</code> whenever it encounters a new class in
+one of these tags. This is why the class names have to be valid
+Python identifiers.</p>
+
+<p>Now let's see what Lore does with these new classes with the following
+input file:</p>
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;html&gt;
+ &lt;head&gt;
+ &lt;title&gt;My First Example&lt;/title&gt;
+ &lt;/head&gt;
+ &lt;body&gt;
+ &lt;h1&gt;My First Example&lt;/h1&gt;
+ &lt;p&gt;The Frabjulon &lt;span class=&quot;productname&quot;&gt;Limpet 2000&lt;/span&gt;
+ is the &lt;span class=&quot;marketinglie&quot;&gt;industry-leading&lt;/span&gt; aquatic
+ mollusc counter, bar none.&lt;/p&gt;
+ &lt;/body&gt;
+&lt;/html&gt;
+
+</pre><div class="caption">Listing 4:
+ 1st_example.html - <a href="listings/lore/1st_example.html"><span class="filename">listings/lore/1st_example.html</span></a></div></div>
+
+<p>First, verify that your package is laid out correctly. Your directory
+structure should look like this:</p>
+
+<pre xml:space="preserve">
+1st_example.html
+myhtml/
+ __init__.py
+ factory.py
+ spitters.py
+twisted/plugins/
+ a_lore_plugin.py
+</pre>
+
+<p>In the parent directory of myhtml (that is, <code>myhtml/..</code>), run
+lore and pdflatex on the input:</p>
+
+<pre class="shell" xml:space="preserve">
+$ lore --input myhtml --output latex 1st_example.html
+[########################################] (*Done*)
+
+$ pdflatex 1st_example.tex
+[ . . . latex output omitted for brevity . . . ]
+Output written on 1st_example.pdf (1 page, 22260 bytes).
+Transcript written on 1st_example.log.
+</pre>
+
+<p>And here's what the rendered PDF looks like:</p>
+
+<p><img src="../img/myhtml-output.png"/></p>
+
+<p>What happens when we run lore on this file using the lint output?</p>
+
+<pre class="shell" xml:space="preserve">
+$ lore --input myhtml --output lint 1st_example.html
+1st_example.html:7:47: unknown class productname
+1st_example.html:8:38: unknown class marketinglie
+[########################################] (*Done*)
+</pre>
+
+<p>Lint reports these classes as errors, even though our spitter knows how to
+process them. To fix this problem, we must add to <code class="py-filename">factory.py</code>.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">default</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myhtml</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">spitters</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyProcessingFunctionFactory</span>(<span class="py-src-parameter">default</span>.<span class="py-src-parameter">ProcessingFunctionFactory</span>):
+ <span class="py-src-variable">latexSpitters</span>={<span class="py-src-variable">None</span>: <span class="py-src-variable">spitters</span>.<span class="py-src-variable">MyLatexSpitter</span>,
+ }
+
+ <span class="py-src-comment"># redefine getLintChecker to validate our classes</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getLintChecker</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-comment"># use the default checker from parent</span>
+ <span class="py-src-variable">checker</span> = <span class="py-src-variable">lint</span>.<span class="py-src-variable">getDefaultChecker</span>()
+ <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span> = <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>.<span class="py-src-variable">copy</span>()
+ <span class="py-src-variable">oldSpan</span> = <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>[<span class="py-src-string">'span'</span>]
+ <span class="py-src-variable">checkfunc</span>=<span class="py-src-keyword">lambda</span> <span class="py-src-variable">cl</span>: <span class="py-src-variable">oldSpan</span>(<span class="py-src-variable">cl</span>) <span class="py-src-keyword">or</span> <span class="py-src-variable">cl</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">'marketinglie'</span>,
+ <span class="py-src-string">'productname'</span>]
+ <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>[<span class="py-src-string">'span'</span>] = <span class="py-src-variable">checkfunc</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">checker</span>
+
+<span class="py-src-comment"># initialize the global variable factory with an instance of your new factory</span>
+<span class="py-src-variable">factory</span>=<span class="py-src-variable">MyProcessingFunctionFactory</span>()
+</pre><div class="caption">Listing 5: Input
+ Factory with Lint Support - <a href="listings/lore/factory.py-2"><span class="filename">listings/lore/factory.py-2</span></a></div></div>
+
+<p>The method <code class="py-src-identifier">getLintChecker</code> is called
+by Lore to produce the lint output. This modification adds our classes to the
+list of classes lint ignores:</p>
+
+<pre class="shell" xml:space="preserve">
+$ lore --input myhtml --output lint 1st_example.html
+[########################################] (*Done*)
+$ # Hooray!
+</pre>
+
+<p>Finally, there are two other sub-outputs of LaTeX, for a total of three
+different ways that Lore can produce LaTeX: the default way, which produces as
+output an entire, self-contained LaTeX document; with <code class="shell">--config section</code> on the command line, which produces a
+LaTeX \section; and with <code class="shell">--config chapter</code>, which
+produces a LaTeX \chapter. To support these options as well, the solution is
+to make the new spitter class a mixin, and use it with the <code class="py-src-identifier">SectionLatexSpitter</code> and <code class="py-src-identifier">ChapterLatexSpitter</code>, respectively.
+Comments in the following listings tell you everything you need to know about
+making these simple changes:</p>
+
+<ul>
+ <li><div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">default</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">myhtml</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">spitters</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyProcessingFunctionFactory</span>(<span class="py-src-parameter">default</span>.<span class="py-src-parameter">ProcessingFunctionFactory</span>):
+ <span class="py-src-comment"># 1. add the keys &quot;chapter&quot; and &quot;section&quot; to latexSpitters to handle the</span>
+ <span class="py-src-comment"># --config chapter and --config section options</span>
+ <span class="py-src-variable">latexSpitters</span>={<span class="py-src-variable">None</span>: <span class="py-src-variable">spitters</span>.<span class="py-src-variable">MyLatexSpitter</span>,
+ <span class="py-src-string">&quot;section&quot;</span>: <span class="py-src-variable">spitters</span>.<span class="py-src-variable">MySectionLatexSpitter</span>,
+ <span class="py-src-string">&quot;chapter&quot;</span>: <span class="py-src-variable">spitters</span>.<span class="py-src-variable">MyChapterLatexSpitter</span>,
+ }
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getLintChecker</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">checker</span> = <span class="py-src-variable">lint</span>.<span class="py-src-variable">getDefaultChecker</span>()
+ <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span> = <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>.<span class="py-src-variable">copy</span>()
+ <span class="py-src-variable">oldSpan</span> = <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>[<span class="py-src-string">'span'</span>]
+ <span class="py-src-variable">checkfunc</span>=<span class="py-src-keyword">lambda</span> <span class="py-src-variable">cl</span>: <span class="py-src-variable">oldSpan</span>(<span class="py-src-variable">cl</span>) <span class="py-src-keyword">or</span> <span class="py-src-variable">cl</span> <span class="py-src-keyword">in</span> [<span class="py-src-string">'marketinglie'</span>,
+ <span class="py-src-string">'productname'</span>]
+ <span class="py-src-variable">checker</span>.<span class="py-src-variable">allowedClasses</span>[<span class="py-src-string">'span'</span>] = <span class="py-src-variable">checkfunc</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">checker</span>
+
+<span class="py-src-variable">factory</span>=<span class="py-src-variable">MyProcessingFunctionFactory</span>()
+</pre><div class="caption">factory.py - <a href="listings/lore/factory.py-3"><span class="filename">listings/lore/factory.py-3</span></a></div></div></li>
+ <li><div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">latex</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">lore</span>.<span class="py-src-variable">latex</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">processFile</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>
+
+<span class="py-src-comment"># 2. Create a new mixin that does what the old MyLatexSpitter used to do:</span>
+<span class="py-src-comment"># process the new classes we defined</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MySpitterMixin</span>:
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">visitNode_span_productname</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">node</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\underline{'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">visitNodeDefault</span>(<span class="py-src-variable">node</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'}'</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">visitNode_span_marketinglie</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">node</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\begin{bf}\\begin{Large}'</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">visitNodeDefault</span>(<span class="py-src-variable">node</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">writer</span>(<span class="py-src-string">'\\end{Large}\\end{bf}'</span>)
+
+<span class="py-src-comment"># 3. inherit from the mixin class for each of the three sub-spitters</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyLatexSpitter</span>(<span class="py-src-parameter">MySpitterMixin</span>, <span class="py-src-parameter">latex</span>.<span class="py-src-parameter">LatexSpitter</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MySectionLatexSpitter</span>(<span class="py-src-parameter">MySpitterMixin</span>, <span class="py-src-parameter">latex</span>.<span class="py-src-parameter">SectionLatexSpitter</span>):
+ <span class="py-src-keyword">pass</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyChapterLatexSpitter</span>(<span class="py-src-parameter">MySpitterMixin</span>, <span class="py-src-parameter">latex</span>.<span class="py-src-parameter">ChapterLatexSpitter</span>):
+ <span class="py-src-keyword">pass</span>
+</pre><div class="caption">spitters.py - <a href="listings/lore/spitters.py-2"><span class="filename">listings/lore/spitters.py-2</span></a></div></div></li>
+</ul>
+
+
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/howto/index.html b/doc/lore/howto/index.html
new file mode 100644
index 0000000..9fd0669
--- /dev/null
+++ b/doc/lore/howto/index.html
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Lore Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Lore Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+ <li><a href="lore.html" shape="rect">Lore documentation system</a></li>
+ <li><a href="extend-lore.html" shape="rect">Extending Lore</a></li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/howto/listings/lore/1st_example.html b/doc/lore/howto/listings/lore/1st_example.html
new file mode 100644
index 0000000..11ff82c
--- /dev/null
+++ b/doc/lore/howto/listings/lore/1st_example.html
@@ -0,0 +1,12 @@
+<html>
+ <head>
+ <title>My First Example</title>
+ </head>
+ <body>
+ <h1>My First Example</h1>
+ <p>The Frabjulon <span class="productname">Limpet 2000</span>
+ is the <span class="marketinglie">industry-leading</span> aquatic
+ mollusc counter, bar none.</p>
+ </body>
+</html>
+
diff --git a/doc/lore/howto/listings/lore/a_lore_plugin.py b/doc/lore/howto/listings/lore/a_lore_plugin.py
new file mode 100644
index 0000000..c6ac1b4
--- /dev/null
+++ b/doc/lore/howto/listings/lore/a_lore_plugin.py
@@ -0,0 +1,11 @@
+
+from zope.interface import implements
+
+from twisted.plugin import IPlugin
+from twisted.lore.scripts.lore import IProcessor
+
+class MyHTML(object):
+ implements(IPlugin, IProcessor)
+
+ name = "myhtml"
+ moduleName = "myhtml.factory"
diff --git a/doc/lore/howto/listings/lore/factory.py-1 b/doc/lore/howto/listings/lore/factory.py-1
new file mode 100644
index 0000000..0030955
--- /dev/null
+++ b/doc/lore/howto/listings/lore/factory.py-1
@@ -0,0 +1,9 @@
+from twisted.lore import default
+from myhtml import spitters
+
+class MyProcessingFunctionFactory(default.ProcessingFunctionFactory):
+ latexSpitters={None: spitters.MyLatexSpitter,
+ }
+
+# initialize the global variable factory with an instance of your new factory
+factory=MyProcessingFunctionFactory()
diff --git a/doc/lore/howto/listings/lore/factory.py-2 b/doc/lore/howto/listings/lore/factory.py-2
new file mode 100644
index 0000000..c5e0319
--- /dev/null
+++ b/doc/lore/howto/listings/lore/factory.py-2
@@ -0,0 +1,20 @@
+from twisted.lore import default
+from myhtml import spitters
+
+class MyProcessingFunctionFactory(default.ProcessingFunctionFactory):
+ latexSpitters={None: spitters.MyLatexSpitter,
+ }
+
+ # redefine getLintChecker to validate our classes
+ def getLintChecker(self):
+ # use the default checker from parent
+ checker = lint.getDefaultChecker()
+ checker.allowedClasses = checker.allowedClasses.copy()
+ oldSpan = checker.allowedClasses['span']
+ checkfunc=lambda cl: oldSpan(cl) or cl in ['marketinglie',
+ 'productname']
+ checker.allowedClasses['span'] = checkfunc
+ return checker
+
+# initialize the global variable factory with an instance of your new factory
+factory=MyProcessingFunctionFactory()
diff --git a/doc/lore/howto/listings/lore/factory.py-3 b/doc/lore/howto/listings/lore/factory.py-3
new file mode 100644
index 0000000..85e0374
--- /dev/null
+++ b/doc/lore/howto/listings/lore/factory.py-3
@@ -0,0 +1,21 @@
+from twisted.lore import default
+from myhtml import spitters
+
+class MyProcessingFunctionFactory(default.ProcessingFunctionFactory):
+ # 1. add the keys "chapter" and "section" to latexSpitters to handle the
+ # --config chapter and --config section options
+ latexSpitters={None: spitters.MyLatexSpitter,
+ "section": spitters.MySectionLatexSpitter,
+ "chapter": spitters.MyChapterLatexSpitter,
+ }
+
+ def getLintChecker(self):
+ checker = lint.getDefaultChecker()
+ checker.allowedClasses = checker.allowedClasses.copy()
+ oldSpan = checker.allowedClasses['span']
+ checkfunc=lambda cl: oldSpan(cl) or cl in ['marketinglie',
+ 'productname']
+ checker.allowedClasses['span'] = checkfunc
+ return checker
+
+factory=MyProcessingFunctionFactory()
diff --git a/doc/lore/howto/listings/lore/spitters.py-1 b/doc/lore/howto/listings/lore/spitters.py-1
new file mode 100644
index 0000000..b17a0be
--- /dev/null
+++ b/doc/lore/howto/listings/lore/spitters.py-1
@@ -0,0 +1,18 @@
+from twisted.lore import latex
+from twisted.lore.latex import processFile
+import os.path
+
+class MyLatexSpitter(latex.LatexSpitter):
+ def visitNode_span_productname(self, node):
+ # start an underline section in LaTeX
+ self.writer('\\underline{')
+ # process the node and its children
+ self.visitNodeDefault(node)
+ # end the underline block
+ self.writer('}')
+
+ def visitNode_span_marketinglie(self, node):
+ # this example turns on more than one LaTeX effect at once
+ self.writer('\\begin{bf}\\begin{Large}')
+ self.visitNodeDefault(node)
+ self.writer('\\end{Large}\\end{bf}')
diff --git a/doc/lore/howto/listings/lore/spitters.py-2 b/doc/lore/howto/listings/lore/spitters.py-2
new file mode 100644
index 0000000..7108d6b
--- /dev/null
+++ b/doc/lore/howto/listings/lore/spitters.py-2
@@ -0,0 +1,26 @@
+from twisted.lore import latex
+from twisted.lore.latex import processFile
+import os.path
+
+# 2. Create a new mixin that does what the old MyLatexSpitter used to do:
+# process the new classes we defined
+class MySpitterMixin:
+ def visitNode_span_productname(self, node):
+ self.writer('\\underline{')
+ self.visitNodeDefault(node)
+ self.writer('}')
+
+ def visitNode_span_marketinglie(self, node):
+ self.writer('\\begin{bf}\\begin{Large}')
+ self.visitNodeDefault(node)
+ self.writer('\\end{Large}\\end{bf}')
+
+# 3. inherit from the mixin class for each of the three sub-spitters
+class MyLatexSpitter(MySpitterMixin, latex.LatexSpitter):
+ pass
+
+class MySectionLatexSpitter(MySpitterMixin, latex.SectionLatexSpitter):
+ pass
+
+class MyChapterLatexSpitter(MySpitterMixin, latex.ChapterLatexSpitter):
+ pass
diff --git a/doc/lore/howto/lore.html b/doc/lore/howto/lore.html
new file mode 100644
index 0000000..6df1de6
--- /dev/null
+++ b/doc/lore/howto/lore.html
@@ -0,0 +1,369 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Using the Lore Documentation System</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Using the Lore Documentation System</h1>
+ <div class="toc"><ol><li><a href="#auto0">Writing Lore Documents</a></li><ul><li><a href="#auto1">Overview</a></li><li><a href="#auto2">Elements and Their Uses</a></li></ul><li><a href="#auto3">Writing Lore XHTML Templates</a></li><li><a href="#auto4">Using Lore to Generate HTML</a></li><li><a href="#auto5">Using Lore to Generate LaTex</a></li><ul><li><a href="#auto6">Articles</a></li><li><a href="#auto7">Books</a></li></ul><li><a href="#auto8">Using Lore to Generate Slides</a></li><ul><li><a href="#auto9">Magic Point Output</a></li><li><a href="#auto10">LaTeX Output</a></li></ul><li><a href="#auto11">Linting</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Writing Lore Documents<a name="auto0"/></h2>
+
+<h3>Overview<a name="auto1"/></h3>
+
+<p>Lore documents are a special subset of XHTML documents. They use specific
+subset of XHTML, together with custom classes, to allow a wide variety of
+document elements, including some Python-specific ones. Lore documents, in
+particular, are well-formed XML documents. XML can be written using a wide
+variety of tools: from run of the mill editors such as vi, through editors
+with XML help like EMACS and ending with XML specific tools like (need name
+of XML editor here). Here, we will not cover the specifics of writing XML
+documents, except for a very broad overview.</p>
+
+<p>XML documents contain elements, which are delimited by an opening
+tag which looks like <code>&lt;tag-name attribute=&quot;value&quot;&gt;</code>
+and ends with a closing tag, which looks
+like <code>&lt;/tag-name&gt;</code>. If an elements happen to contain
+nothing, it can be shortened to <code>&lt;tag-name
+/&gt;</code>. Elements can contain other elements, or text. Text can
+contain any characters except &lt;, &gt; and &amp;. These characters
+are rendered by &amp;lt;, &amp;gt; and &amp;amp;, respectively.</p>
+
+<p>A Lore document is a single <code>html</code> element. Inside this
+element, there are exactly two top-level elements: <code>head</code>
+and <code>body</code>. The <code>head</code> element must contain
+exactly one element: <code>title</code>, containing the title of the
+document. Most of the document will be contained in
+the <code>body</code> element. The <code>body</code> element must
+start with an <code>h1</code> (top-level header) element, which
+contains the exact same content as the <code>title</code> element.</p>
+
+<p>Thus, a fairly minimal Lore document might look like:</p>
+
+<pre xml:space="preserve">
+&lt;html&gt;
+&lt;head&gt;&lt;title&gt;Title&lt;/title&gt;&lt;/head&gt;
+&lt;body&gt;&lt;h1&gt;Title&lt;/h1&gt;&lt;/body&gt;
+&lt;/html&gt;
+</pre>
+
+<h3>Elements and Their Uses<a name="auto2"/></h3>
+
+<table border="2" cellpadding="7" cellspacing="0">
+<tr>
+<th colspan="1" rowspan="1">Element</th>
+<th colspan="1" rowspan="1">Description</th>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>p</code></td>
+<td colspan="1" rowspan="1">The paragraph element. Most of the document should be inside paragraphs.
+</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>span</code></td>
+<td colspan="1" rowspan="1">The span element is an element which has no meaning -- unless it has a
+special <code>class</code> attributes. The following classes have the stated
+meanings:
+<dl>
+<dt><code>footnote</code></dt>
+<dd>a small comment which should not be inside the main text-flow.</dd>
+<dt><code>manhole-output</code></dt>
+<dd>This signifies, within a manhole transcript, that the enclosed text is
+ the output and not something the user has to input.</dd>
+<dt><code>index</code></dt>
+<dd>This should be an <em>empty</em> element, with an attribute
+ <code>value</code>. That attribute should be an index term, in the
+ format of <code>generic!specific!more specific</code>. Usually,
+ you will only have one level, in which case <code>value=&quot;term&quot;</code>
+ works.</dd>
+</dl></td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>div</code></td>
+<td colspan="1" rowspan="1">The div element is equivalent to a span, except it always appears outside
+paragraphs. The following classes have the given meanings:
+<dl>
+<dt><code>note</code></dt>
+<dd>A short note which is not necessary for the understanding of the text.</dd>
+<dt><code>doit</code></dt>
+<dd>An indication that the discussed feature is not complete or implemented
+ yet.</dd>
+<dt><code>boxed</code></dt>
+<dd>An indication that the text should be clearly separated from its
+ surroundings.</dd>
+</dl></td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>a</code></td>
+<td colspan="1" rowspan="1">This element can have several meanings, depending on the attributes:
+<dl>
+<dt><code>name</code> attribute</dt>
+<dd>Add a label to the current position, which might be used in this document
+ or other documents to refer to.</dd>
+<dt><code>href=URL</code></dt>
+<dd>Refer to some WWW resource.</dd>
+<dt><code>href=relative-path</code>, <code>href=relative-path#label</code> or
+ <code>href=#label</code></dt>
+<dd>Refer to a position in a Lore resource. By default, relative links to
+ <code>.xhtml</code> files are changed to point to a <code>.html</code> file.
+ If you need a link to a local non-Lore .xhtml file, use
+ <code>class=absolute</code> to make Lore treat it as an absolute link.</dd>
+<dt><code>href=relative-path</code> with <code>class=py-listing</code> or
+ <code>class=html-listing</code></dt>
+<dd>Indicate the given resource is a part of the text flow, and should be
+ inlined (and if possible, syntax highlighted).</dd>
+</dl></td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>ol</code>, <code>ul</code></td>
+<td colspan="1" rowspan="1">A list. It can be enumerated or bulleted. Inside a list, the
+element <code>li</code> (for a list element) is valid.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>h2</code>, <code>h3</code></td>
+<td colspan="1" rowspan="1">Second- and third-level section headings.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>code</code></td>
+<td colspan="1" rowspan="1">A string which has meaning to the computer. There are many possible
+classes:
+<dl>
+<dt><code>API</code></dt>
+<dd>A class, function or a module. It does not have to be a fully qualified
+ name -- but if it isn't, a <code>base</code> attribute is necessary.
+ <br/>Example:
+ <code>&lt;code class=&quot;API&quot; base=&quot;urllib&quot;&gt;urlencode&lt;code&gt;</code>.</dd>
+<dt><code>shell</code></dt>
+<dd>Shell (usually Bourne) code.</dd>
+<dt><code>python</code></dt>
+<dd>Python code.</dd>
+<dt><code>py-prototype</code></dt>
+<dd>Function prototype.</dd>
+<dt><code>py-filename</code></dt>
+<dd>Python file.</dd>
+<dt><code>py-src-string</code></dt>
+<dd>Python string.</dd>
+<dt><code>py-signature</code></dt>
+<dd>Function signature.</dd>
+<dt><code>py-src-parameter</code></dt>
+<dd>Parameter.</dd>
+<dt><code>py-src-identifier</code></dt>
+<dd>Identifier.</dd>
+<dt><code>py-src-keyword</code></dt>
+<dd>Keyword.</dd>
+</dl></td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>pre</code></td>
+<td colspan="1" rowspan="1">Preformatted text, usually for file listings. It can be used with
+the <code>python</code> class to indicate Python syntax
+coloring. Other possible classes are <code>shell</code> (to indicate a
+shell-transcript) or <code>python-interpreter</code> (to indicate an
+interactive interpreter transcript).</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>img</code></td>
+<td colspan="1" rowspan="1">Insert the image indicated by the <code>src</code> attribute.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>q</code></td>
+<td colspan="1" rowspan="1">The quote signs (<code>&quot;</code>) are not recommended
+except in preformatted or code environment. Instead, quote by using the
+<code>q</code> element which allows nested quotes and properly distinguishes
+opening quote from closing quote.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>em</code>, <code>strong</code></td>
+<td colspan="1" rowspan="1">Emphasise (or strongly emphasise) text.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>table</code></td>
+<td colspan="1" rowspan="1">Tabular data. Inside a table, use the <code>tr</code>
+element for each rows, and inside it use either <code>td</code> for a regular
+table cell or <code>th</code> for a table header (column or row).</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>blockquote</code></td>
+<td colspan="1" rowspan="1">A long quote which should be properly seperated from the main text.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>cite</code></td>
+<td colspan="1" rowspan="1">Cite a resource.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>sub</code>, <code>sup</code></td>
+<td colspan="1" rowspan="1">Subscripts and superscripts.</td>
+</tr>
+
+<tr>
+<td colspan="1" rowspan="1"><code>link</code></td>
+<td colspan="1" rowspan="1">Currently, the only <code>link</code> elements supported
+are for for indicating authorship. <code>&lt;link rel=&quot;author&quot;
+href=&quot;author-address@examples.com&quot; title=&quot;Author Name&quot; /&gt;</code>
+should be used to indicate authorship. Multiple instances
+are allowed, and indicate shared authorship.</td>
+</tr>
+
+</table>
+
+<h2>Writing Lore XHTML Templates<a name="auto3"/></h2>
+
+<p>One of Lore's output formats is XHTML. Lore itself is very markup-light,
+but the output XHTML is much more markup intensive. Part of the auto-generated
+markup is directed by a special template.</p>
+
+<p>The output of Lore is inserted into template in the following way:</p>
+
+<ul>
+<li>The title is appended into each element with class <code>title</code>.</li>
+<li>The body is inserted into the first element that has class
+ <code>body</code>.</li>
+<li>The table of contents is inserted into the first element that has class
+ <code>toc</code>.</li>
+</ul>
+
+<p>In particular, most of the header is not tampered with -- so it is
+easy to indicate a CSS stylesheet in the template.</p>
+
+<h2>Using Lore to Generate HTML<a name="auto4"/></h2>
+
+<p>After having written a template, the easiest way to build HTML from the Lore
+document is by:</p>
+
+<pre class="shell" xml:space="preserve">
+% lore --config template=mytemplate.tpl mydocument.xhtml
+</pre>
+
+<p>This will create a file called <code class="shell">mydocument.html</code>.
+</p>
+
+<p>For example, to generate the HTML version of the Twisted docs from a SVN
+checkout, do:</p>
+
+<pre class="shell" xml:space="preserve">
+% lore --config template=doc/core/howto/template.tpl doc/core/howto/*.xhtml
+</pre>
+
+<p>
+In order to generate files with a different extension, use the <code class="shell">--config</code> commandline flag to tell the HTML output plugin to
+use a different extension:
+</p>
+<pre class="shell" xml:space="preserve">
+% lore --config ext=.html doc/core/howto/*.xhtml
+</pre>
+<h2>Using Lore to Generate LaTex<a name="auto5"/></h2>
+
+<h3>Articles<a name="auto6"/></h3>
+
+<pre class="shell" xml:space="preserve">
+% lore --output latex mydocument.xhtml
+</pre>
+
+<h3>Books<a name="auto7"/></h3>
+
+<p>Have a Lore file for each section. Then, have a LaTeX file which inputs
+all the given LaTeX files. Generate all the LaTeX files by using</p>
+
+<pre class="shell" xml:space="preserve">
+% lore --output latex --config section *.xhtml
+</pre>
+
+<p>in the relevant directory.</p>
+
+<h2>Using Lore to Generate Slides<a name="auto8"/></h2>
+
+<p>Lore can also be used to generate slides for presentations. The start
+of a new slide is indicated by use of an h2 tag, with the content
+between the opening and closing tags the title of the slide. Slides
+are generated by</p>
+
+<pre class="shell" xml:space="preserve">
+% lore --input lore-slides myslides.xhtml
+</pre>
+
+<p>This, by default, will produce HTML output with one HTML file for
+each slide. For our example, the files would be named
+myslides-&lt;number&gt;.html, where number is the slide number,
+starting with 0 for the title slide. Lore will look for a template
+file, either indicated by the <code>--config
+template=mytemplate.tpl</code> or the default template.tpl in the
+current directory. An example slide template is found
+in <code>doc/examples/slides-template.tpl</code></p>
+
+<p>The slides module currently supports three major output types:
+HTML, Magic Point, and LaTeX. The options for the latter two will be
+covered individually.</p>
+
+<h3>Magic Point Output<a name="auto9"/></h3>
+
+<p>Lore supports outputting to the Magic Point file format.
+Magicpoint is a presentation program for X, which can be installed on
+Debian by <code>apt-get install mgp</code> or by visiting <a href="http://member.wide.ad.jp/wg/mgp/" shape="rect">the Magic Point homepage</a>
+otherwise. A template file is required, <code>template.mgp</code> is
+shipped in the <code>twisted/lore</code> directory. Magic Point
+slides are generated by </p>
+
+<pre class="shell" xml:space="preserve">
+% lore --input lore-slides --output mgp \
+ --config template=~/Twisted/twisted/lore/template.mgp \
+ myslides.xhtml
+</pre>
+
+<p>That will produce <code>myslides.mgp</code>.</p>
+
+<h3>LaTeX Output<a name="auto10"/></h3>
+
+<p>Lore can also produce slides in LaTeX format. It supports three
+main styles: one slide per page, two per page, and Prosper format,
+with the <code>--config</code> parameters
+being <code>page</code>, <code>twopage</code>,
+and <code>prosper</code> respectively. Prosper is a LaTeX class for
+creating slides, which can be installed on Debian by <code>apt-get
+install prosper</code> or by
+visiting <a href="http://sourceforge.net/projects/prosper/" shape="rect">the
+Prosper SourceForge page</a>. LaTeX format slides (using the Prosper
+option, for example) are generated by</p>
+
+<pre class="shell" xml:space="preserve">
+% lore --input lore-slides --output latex \
+ --config prosper myslides.xhtml
+</pre>
+
+<p> This will generate <code>myslides.tex</code> file that can be processed
+with <code>latex</code> or <code>pdftex</code> or the appropriate
+LaTeX processing command.</p>
+
+<h2>Linting<a name="auto11"/></h2>
+
+<pre xml:space="preserve">
+% lore --output lint mydocument.xhtml
+</pre>
+
+<p>This will generate compiler-style (file:line:column:message) warnings.
+It is possible to integrate these warnings into a smart editor such as
+EMACS, but it has not been done yet.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/img/myhtml-output.png b/doc/lore/img/myhtml-output.png
new file mode 100644
index 0000000..4a00fbf
--- /dev/null
+++ b/doc/lore/img/myhtml-output.png
Binary files differ
diff --git a/doc/lore/index.html b/doc/lore/index.html
new file mode 100644
index 0000000..4f5f8af
--- /dev/null
+++ b/doc/lore/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Lore Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Lore Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Lore to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Lore</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/man/lore-man.html b/doc/lore/man/lore-man.html
new file mode 100644
index 0000000..0e34ccc
--- /dev/null
+++ b/doc/lore/man/lore-man.html
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: GENERATELORE.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">GENERATELORE.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">DESCRIPTION</a></li><li><a href="#auto4">AUTHOR</a></li><li><a href="#auto5">REPORTING BUGS</a></li><li><a href="#auto6">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>lore - convert documentations formats
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>lore</strong> [-l <em>linkrel</em>] [-d <em>docsdir</em>] [-i <em>input</em>] [-o <em>output</em>] [--config attribute[=value] [...]] [-p] [file [...]]</p>
+
+<p><strong>lore</strong> --help</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>The <strong>--help</strong> prints out a usage message to standard output.
+<dl><dt><strong>-p</strong>, <strong>--plain</strong>
+</dt><dd>Use non-flashy progress bar - one file per line.
+</dd>
+
+<dt><strong>-n</strong>, <strong>--null</strong>
+</dt><dd>Do not report progress at all.
+</dd>
+
+<dt><strong>-N</strong>, <strong>--number</strong>
+</dt><dd>Add chapter/section numbers to section headings.
+</dd>
+
+<dt><em>-l</em>, <em>--linkrel</em>
+</dt><dd>Where non-document links should be relative to.
+</dd>
+
+<dt><em>-d</em>, <em>--docsdir</em>
+</dt><dd>Where to look for <strong>.html</strong> files if no files are given.
+</dd>
+
+<dt><em>-e</em>, <em>--inputext</em> &lt;extension&gt;
+</dt><dd>The extension that your Lore input files have (default: .xhtml)
+</dd>
+
+<dt><em>-i</em>, <em>--input</em>
+</dt><dd>Input format. New input formats can be dynamically registered. Lore itself
+comes with <q>lore</q> (the standard format), <q>mlore</q> (allows LaTeX equations)
+and <q>man</q> (man page format). If the input format is not registered as a plugin,
+a module of the named input will be searched. For example,
+<strong>--i</strong> twisted.lore.defaultis equivalent to using the default Lore input.
+</dd>
+
+<dt><em>-o</em>, <em>--output</em>
+</dt><dd>Output format. Available output formats depend on the input. For the core
+formats, lore and mlore support html, latex and lint, while man allows
+lore.
+</dd>
+
+<dt><em>-x</em>, <em>--index</em> &lt;filename&gt;
+</dt><dd>The base filename you want to give your index file.
+</dd>
+
+<dt><em>-b</em>, <em>--book</em> &lt;filename&gt;
+</dt><dd>The book file to generate a book from.
+</dd>
+
+<dt><em>--prefixurl</em> &lt;prefix&gt;
+</dt><dd>The prefix to stick on to relative links; only useful when processing
+directories.
+</dd>
+
+<dt><em>--version</em>
+</dt><dd>Display version information and exit.
+</dd>
+
+<dt><em>--config</em>
+</dt><dd>Add input/output-specific information.
+HTML output allows for 'ext=&lt;extension&gt;',
+'template=&lt;template&gt;' and 'baseurl=&lt;format string for API URLs&gt;'. LaTeX
+output allows for 'section' or 'chapter' in Lore, and nothing in Math-Lore.
+Lore output allows for 'ext=&lt;extension&gt;'. Lint output allows nothing.
+Note that disallowed <em>--config</em> options are merely ignored, and do
+not cause errors.
+</dd>
+
+</dl>
+
+</p>
+
+<h2>DESCRIPTION<a name="auto3"/></h2>
+
+<p>If no files are given, all *.html documents in docsdir are processed.
+</p>
+
+<h2>AUTHOR<a name="auto4"/></h2>
+
+<p>Written by Moshe Zadka
+</p>
+
+<h2>REPORTING BUGS<a name="auto5"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto6"/></h2>
+
+<p>Copyright © 2003-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/lore/man/lore.1 b/doc/lore/man/lore.1
new file mode 100644
index 0000000..0d9cba1
--- /dev/null
+++ b/doc/lore/man/lore.1
@@ -0,0 +1,74 @@
+.TH GENERATELORE "1" "October 2002" "" ""
+.SH NAME
+lore \- convert documentations formats
+.SH SYNOPSIS
+.B lore [-l \fIlinkrel\fR] [-d \fIdocsdir\fR] [-i \fIinput\fR] [-o \fIoutput\fR] [--config attribute[=value] [...]] [-p] [file [...]]
+.PP
+.B lore --help
+.SH DESCRIPTION
+.PP
+The \fB\--help\fR prints out a usage message to standard output.
+.TP
+\fB-p\fR, \fB--plain\fR
+Use non-flashy progress bar \- one file per line.
+.TP
+\fB-n\fR, \fB--null\fR
+Do not report progress at all.
+.TP
+\fB-N\fR, \fB--number\fR
+Add chapter/section numbers to section headings.
+.TP
+\fI-l\fR, \fI--linkrel\fR
+Where non-document links should be relative to.
+.TP
+\fI-d\fR, \fI--docsdir\fR
+Where to look for \fB.html\fR files if no files are given.
+.TP
+\fI-e\fR, \fI--inputext\fR <extension>
+The extension that your Lore input files have (default: .xhtml)
+.TP
+\fI-i\fR, \fI--input\fR
+Input format. New input formats can be dynamically registered. Lore itself
+comes with "lore" (the standard format), "mlore" (allows LaTeX equations)
+and "man" (man page format). If the input format is not registered as a plugin,
+a module of the named input will be searched. For example,
+.B --i twisted.lore.default
+is equivalent to using the default Lore input.
+.TP
+\fI-o\fR, \fI--output\fR
+Output format. Available output formats depend on the input. For the core
+formats, lore and mlore support html, latex and lint, while man allows
+lore.
+.TP
+\fI-x\fR, \fI--index\fR <filename>
+The base filename you want to give your index file.
+.TP
+\fI-b\fR, \fI--book\fR <filename>
+The book file to generate a book from.
+.TP
+\fI--prefixurl\fR <prefix>
+The prefix to stick on to relative links; only useful when processing
+directories.
+.TP
+\fI--version\fR
+Display version information and exit.
+.TP
+\fI--config\fR
+Add input/output-specific information.
+HTML output allows for 'ext=<extension>',
+'template=<template>' and 'baseurl=<format string for API URLs>'. LaTeX
+output allows for 'section' or 'chapter' in Lore, and nothing in Math-Lore.
+Lore output allows for 'ext=<extension>'. Lint output allows nothing.
+Note that disallowed \fI--config\fR options are merely ignored, and do
+not cause errors.
+.SH DESCRIPTION
+If no files are given, all *.html documents in docsdir are processed.
+.SH AUTHOR
+Written by Moshe Zadka
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2003-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/mail/examples/emailserver.tac b/doc/mail/examples/emailserver.tac
new file mode 100644
index 0000000..d769b88
--- /dev/null
+++ b/doc/mail/examples/emailserver.tac
@@ -0,0 +1,107 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# You can run this module directly with:
+# twistd -ny emailserver.tac
+
+"""
+A toy email server.
+"""
+
+from zope.interface import implements
+
+from twisted.internet import defer
+from twisted.mail import smtp
+from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
+
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.portal import IRealm
+from twisted.cred.portal import Portal
+
+
+
+class ConsoleMessageDelivery:
+ implements(smtp.IMessageDelivery)
+
+ def receivedHeader(self, helo, origin, recipients):
+ return "Received: ConsoleMessageDelivery"
+
+
+ def validateFrom(self, helo, origin):
+ # All addresses are accepted
+ return origin
+
+
+ def validateTo(self, user):
+ # Only messages directed to the "console" user are accepted.
+ if user.dest.local == "console":
+ return lambda: ConsoleMessage()
+ raise smtp.SMTPBadRcpt(user)
+
+
+
+class ConsoleMessage:
+ implements(smtp.IMessage)
+
+ def __init__(self):
+ self.lines = []
+
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+
+ def eomReceived(self):
+ print "New message received:"
+ print "\n".join(self.lines)
+ self.lines = None
+ return defer.succeed(None)
+
+
+ def connectionLost(self):
+ # There was an error, throw away the stored lines
+ self.lines = None
+
+
+
+class ConsoleSMTPFactory(smtp.SMTPFactory):
+ protocol = smtp.ESMTP
+
+ def __init__(self, *a, **kw):
+ smtp.SMTPFactory.__init__(self, *a, **kw)
+ self.delivery = ConsoleMessageDelivery()
+
+
+ def buildProtocol(self, addr):
+ p = smtp.SMTPFactory.buildProtocol(self, addr)
+ p.delivery = self.delivery
+ p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
+ return p
+
+
+
+class SimpleRealm:
+ implements(IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if smtp.IMessageDelivery in interfaces:
+ return smtp.IMessageDelivery, ConsoleMessageDelivery(), lambda: None
+ raise NotImplementedError()
+
+
+
+def main():
+ from twisted.application import internet
+ from twisted.application import service
+
+ portal = Portal(SimpleRealm())
+ checker = InMemoryUsernamePasswordDatabaseDontUse()
+ checker.addUser("guest", "password")
+ portal.registerChecker(checker)
+
+ a = service.Application("Console SMTP Server")
+ internet.TCPServer(2500, ConsoleSMTPFactory(portal)).setServiceParent(a)
+
+ return a
+
+application = main()
diff --git a/doc/mail/examples/imap4client.py b/doc/mail/examples/imap4client.py
new file mode 100644
index 0000000..ee1bd44
--- /dev/null
+++ b/doc/mail/examples/imap4client.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Simple IMAP4 client which displays the subjects of all messages in a
+particular mailbox.
+"""
+
+import sys
+
+from twisted.internet import protocol
+from twisted.internet import ssl
+from twisted.internet import defer
+from twisted.internet import stdio
+from twisted.mail import imap4
+from twisted.protocols import basic
+from twisted.python import util
+from twisted.python import log
+
+class TrivialPrompter(basic.LineReceiver):
+ from os import linesep as delimiter
+
+ promptDeferred = None
+
+ def prompt(self, msg):
+ assert self.promptDeferred is None
+ self.display(msg)
+ self.promptDeferred = defer.Deferred()
+ return self.promptDeferred
+
+ def display(self, msg):
+ self.transport.write(msg)
+
+ def lineReceived(self, line):
+ if self.promptDeferred is None:
+ return
+ d, self.promptDeferred = self.promptDeferred, None
+ d.callback(line)
+
+class SimpleIMAP4Client(imap4.IMAP4Client):
+ greetDeferred = None
+
+ def serverGreeting(self, caps):
+ self.serverCapabilities = caps
+ if self.greetDeferred is not None:
+ d, self.greetDeferred = self.greetDeferred, None
+ d.callback(self)
+
+class SimpleIMAP4ClientFactory(protocol.ClientFactory):
+ usedUp = False
+
+ protocol = SimpleIMAP4Client
+
+ def __init__(self, username, onConn):
+ self.ctx = ssl.ClientContextFactory()
+
+ self.username = username
+ self.onConn = onConn
+
+ def buildProtocol(self, addr):
+ assert not self.usedUp
+ self.usedUp = True
+
+ p = self.protocol(self.ctx)
+ p.factory = self
+ p.greetDeferred = self.onConn
+
+ auth = imap4.CramMD5ClientAuthenticator(self.username)
+ p.registerAuthenticator(auth)
+
+ return p
+
+ def clientConnectionFailed(self, connector, reason):
+ d, self.onConn = self.onConn, None
+ d.errback(reason)
+
+# Initial callback - invoked after the server sends us its greet message
+def cbServerGreeting(proto, username, password):
+ # Hook up stdio
+ tp = TrivialPrompter()
+ stdio.StandardIO(tp)
+
+ # And make it easily accessible
+ proto.prompt = tp.prompt
+ proto.display = tp.display
+
+ # Try to authenticate securely
+ return proto.authenticate(password
+ ).addCallback(cbAuthentication, proto
+ ).addErrback(ebAuthentication, proto, username, password
+ )
+
+# Fallback error-handler. If anything goes wrong, log it and quit.
+def ebConnection(reason):
+ log.startLogging(sys.stdout)
+ log.err(reason)
+ from twisted.internet import reactor
+ reactor.stop()
+
+# Callback after authentication has succeeded
+def cbAuthentication(result, proto):
+ # List a bunch of mailboxes
+ return proto.list("", "*"
+ ).addCallback(cbMailboxList, proto
+ )
+
+# Errback invoked when authentication fails
+def ebAuthentication(failure, proto, username, password):
+ # If it failed because no SASL mechanisms match, offer the user the choice
+ # of logging in insecurely.
+ failure.trap(imap4.NoSupportedAuthentication)
+ return proto.prompt("No secure authentication available. Login insecurely? (y/N) "
+ ).addCallback(cbInsecureLogin, proto, username, password
+ )
+
+# Callback for "insecure-login" prompt
+def cbInsecureLogin(result, proto, username, password):
+ if result.lower() == "y":
+ # If they said yes, do it.
+ return proto.login(username, password
+ ).addCallback(cbAuthentication, proto
+ )
+ return defer.fail(Exception("Login failed for security reasons."))
+
+# Callback invoked when a list of mailboxes has been retrieved
+def cbMailboxList(result, proto):
+ result = [e[2] for e in result]
+ s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)])
+ if not s:
+ return defer.fail(Exception("No mailboxes exist on server!"))
+ return proto.prompt(s + "\nWhich mailbox? [1] "
+ ).addCallback(cbPickMailbox, proto, result
+ )
+
+# When the user selects a mailbox, "examine" it.
+def cbPickMailbox(result, proto, mboxes):
+ mbox = mboxes[int(result or '1') - 1]
+ return proto.examine(mbox
+ ).addCallback(cbExamineMbox, proto
+ )
+
+# Callback invoked when examine command completes.
+def cbExamineMbox(result, proto):
+ # Retrieve the subject header of every message on the mailbox.
+ return proto.fetchSpecific('1:*',
+ headerType='HEADER.FIELDS',
+ headerArgs=['SUBJECT']
+ ).addCallback(cbFetch, proto
+ )
+
+# Finally, display headers.
+def cbFetch(result, proto):
+ keys = result.keys()
+ keys.sort()
+ for k in keys:
+ proto.display('%s %s' % (k, result[k][0][2]))
+ return proto.logout()
+
+PORT = 143
+
+def main():
+ hostname = raw_input('IMAP4 Server Hostname: ')
+ username = raw_input('IMAP4 Username: ')
+ password = util.getPassword('IMAP4 Password: ')
+
+ onConn = defer.Deferred(
+ ).addCallback(cbServerGreeting, username, password
+ ).addErrback(ebConnection
+ )
+
+ factory = SimpleIMAP4ClientFactory(username, onConn)
+
+ from twisted.internet import reactor
+ conn = reactor.connectTCP(hostname, PORT, factory)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/mail/examples/index.html b/doc/mail/examples/index.html
new file mode 100644
index 0000000..19c8c63
--- /dev/null
+++ b/doc/mail/examples/index.html
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Mail code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Mail code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">SMTP servers</a></li><li><a href="#auto1">SMTP clients</a></li><li><a href="#auto2">IMAP clients</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>SMTP servers<a name="auto0"/></h2>
+ <ul>
+ <li><a href="emailserver.tac" shape="rect">emailserver.tac</a> - a toy email server.</li>
+ </ul>
+
+ <h2>SMTP clients<a name="auto1"/></h2>
+ <ul>
+ <li><a href="smtpclient_simple.py" shape="rect">smtpclient_simple.py</a> - sending email using SMTP.</li>
+ <li><a href="smtpclient_tls.py" shape="rect">smtpclient_tls.py</a> - send email
+ using authentication and transport layer security.</li>
+ </ul>
+
+ <h2>IMAP clients<a name="auto2"/></h2>
+ <ul>
+ <li><a href="imap4client.py" shape="rect">imap4client.py</a> - Simple IMAP4
+ client which displays the subjects of all messages in a
+ particular mailbox.</li>
+ </ul>
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/mail/examples/smtpclient_simple.py b/doc/mail/examples/smtpclient_simple.py
new file mode 100644
index 0000000..825d269
--- /dev/null
+++ b/doc/mail/examples/smtpclient_simple.py
@@ -0,0 +1,47 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Demonstrate sending mail via SMTP.
+"""
+
+import sys
+from email.mime.text import MIMEText
+
+from twisted.python import log
+from twisted.mail.smtp import sendmail
+from twisted.internet import reactor
+
+
+def send(message, subject, sender, recipients, host):
+ """
+ Send email to one or more addresses.
+ """
+ msg = MIMEText(message)
+ msg['Subject'] = subject
+ msg['From'] = sender
+ msg['To'] = ', '.join(recipients)
+
+ dfr = sendmail(host, sender, recipients, msg.as_string())
+ def success(r):
+ reactor.stop()
+ def error(e):
+ print e
+ reactor.stop()
+ dfr.addCallback(success)
+ dfr.addErrback(error)
+
+ reactor.run()
+
+
+if __name__ == '__main__':
+ msg = 'This is the message body'
+ subject = 'This is the message subject'
+
+ host = 'smtp.example.com'
+ sender = 'sender@example.com'
+ recipients = ['recipient@example.com']
+
+ log.startLogging(sys.stdout)
+ send(msg, subject, sender, recipients, host)
+
diff --git a/doc/mail/examples/smtpclient_tls.py b/doc/mail/examples/smtpclient_tls.py
new file mode 100644
index 0000000..758b97d
--- /dev/null
+++ b/doc/mail/examples/smtpclient_tls.py
@@ -0,0 +1,157 @@
+
+"""
+Demonstrate sending mail via SMTP while employing TLS and performing
+authentication.
+"""
+
+import sys
+
+from OpenSSL.SSL import SSLv3_METHOD
+
+from twisted.mail.smtp import ESMTPSenderFactory
+from twisted.python.usage import Options, UsageError
+from twisted.internet.ssl import ClientContextFactory
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+
+def sendmail(
+ authenticationUsername, authenticationSecret,
+ fromAddress, toAddress,
+ messageFile,
+ smtpHost, smtpPort=25
+ ):
+ """
+ @param authenticationUsername: The username with which to authenticate.
+ @param authenticationSecret: The password with which to authenticate.
+ @param fromAddress: The SMTP reverse path (ie, MAIL FROM)
+ @param toAddress: The SMTP forward path (ie, RCPT TO)
+ @param messageFile: A file-like object containing the headers and body of
+ the message to send.
+ @param smtpHost: The MX host to which to connect.
+ @param smtpPort: The port number to which to connect.
+
+ @return: A Deferred which will be called back when the message has been
+ sent or which will errback if it cannot be sent.
+ """
+
+ # Create a context factory which only allows SSLv3 and does not verify
+ # the peer's certificate.
+ contextFactory = ClientContextFactory()
+ contextFactory.method = SSLv3_METHOD
+
+ resultDeferred = Deferred()
+
+ senderFactory = ESMTPSenderFactory(
+ authenticationUsername,
+ authenticationSecret,
+ fromAddress,
+ toAddress,
+ messageFile,
+ resultDeferred,
+ contextFactory=contextFactory)
+
+ reactor.connectTCP(smtpHost, smtpPort, senderFactory)
+
+ return resultDeferred
+
+
+
+class SendmailOptions(Options):
+ synopsis = "smtpclient_tls.py [options]"
+
+ optParameters = [
+ ('username', 'u', None,
+ 'The username with which to authenticate to the SMTP server.'),
+ ('password', 'p', None,
+ 'The password with which to authenticate to the SMTP server.'),
+ ('from-address', 'f', None,
+ 'The address from which to send the message.'),
+ ('to-address', 't', None,
+ 'The address to which to send the message.'),
+ ('message', 'm', None,
+ 'The filename which contains the message to send.'),
+ ('smtp-host', 'h', None,
+ 'The host through which to send the message.'),
+ ('smtp-port', None, '25',
+ 'The port number on smtp-host to which to connect.')]
+
+
+ def postOptions(self):
+ """
+ Parse integer parameters, open the message file, and make sure all
+ required parameters have been specified.
+ """
+ try:
+ self['smtp-port'] = int(self['smtp-port'])
+ except ValueError:
+ raise UsageError("--smtp-port argument must be an integer.")
+ if self['username'] is None:
+ raise UsageError(
+ "Must specify authentication username with --username")
+ if self['password'] is None:
+ raise UsageError(
+ "Must specify authentication password with --password")
+ if self['from-address'] is None:
+ raise UsageError("Must specify from address with --from-address")
+ if self['to-address'] is None:
+ raise UsageError("Must specify from address with --to-address")
+ if self['smtp-host'] is None:
+ raise UsageError("Must specify smtp host with --smtp-host")
+ if self['message'] is None:
+ raise UsageError(
+ "Must specify a message file to send with --message")
+ try:
+ self['message'] = file(self['message'])
+ except Exception, e:
+ raise UsageError(e)
+
+
+
+def cbSentMessage(result):
+ """
+ Called when the message has been sent.
+
+ Report success to the user and then stop the reactor.
+ """
+ print "Message sent"
+ reactor.stop()
+
+
+
+def ebSentMessage(err):
+ """
+ Called if the message cannot be sent.
+
+ Report the failure to the user and then stop the reactor.
+ """
+ err.printTraceback()
+ reactor.stop()
+
+
+
+def main(args=None):
+ """
+ Parse arguments and send an email based on them.
+ """
+ o = SendmailOptions()
+ try:
+ o.parseOptions(args)
+ except UsageError, e:
+ raise SystemExit(e)
+ else:
+ from twisted.python import log
+ log.startLogging(sys.stdout)
+ result = sendmail(
+ o['username'],
+ o['password'],
+ o['from-address'],
+ o['to-address'],
+ o['message'],
+ o['smtp-host'],
+ o['smtp-port'])
+ result.addCallbacks(cbSentMessage, ebSentMessage)
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/doc/mail/index.html b/doc/mail/index.html
new file mode 100644
index 0000000..dd32774
--- /dev/null
+++ b/doc/mail/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Mail Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Mail Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Mail</li>
+<li><a href="tutorial/smtpclient/smtpclient.html" shape="rect">Twisted Mail Tutorial</a>: Building
+an SMTP Client from Scratch</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/mail/man/mailmail-man.html b/doc/mail/man/mailmail-man.html
new file mode 100644
index 0000000..57841a6
--- /dev/null
+++ b/doc/mail/man/mailmail-man.html
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: MAILMAIL.1</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">MAILMAIL.1</h1>
+ <div class="toc"><ol><li><a href="#auto0">NAME</a></li><li><a href="#auto1">SYNOPSIS</a></li><li><a href="#auto2">DESCRIPTION</a></li><li><a href="#auto3">AUTHOR</a></li><li><a href="#auto4">REPORTING BUGS</a></li><li><a href="#auto5">COPYRIGHT</a></li></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>mailmail - Twisted sendmail compatibility script
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>mailmail</strong> [recipient addresses]</p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>mailmail reads RFC822 message text from standard input and delivers them,
+using SMTP, to a Mail Transfer Agent listening at 127.0.0.1:25. It accepts
+(but does not necessarily implement) many of the standard sendmail(1)
+options, but it is preferable to list only the recipient addresses on
+the command line, and include a <strong>From</strong> header within the message text
+indicating the sender.
+</p>
+
+<h2>AUTHOR<a name="auto3"/></h2>
+
+<p>Written by Jp Calderone
+</p>
+
+<h2>REPORTING BUGS<a name="auto4"/></h2>
+
+<p>To report a bug, visit <em>http://twistedmatrix.com/bugs/</em>
+</p>
+
+<h2>COPYRIGHT<a name="auto5"/></h2>
+
+<p>Copyright © 2003-2008 Twisted Matrix Laboratories.
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+</p>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/mail/man/mailmail.1 b/doc/mail/man/mailmail.1
new file mode 100644
index 0000000..9bff0f7
--- /dev/null
+++ b/doc/mail/man/mailmail.1
@@ -0,0 +1,21 @@
+.TH MAILMAIL "1" "July 2003" "" ""
+.SH NAME
+mailmail \- Twisted sendmail compatibility script
+.SH SYNOPSIS
+.B mailmail [recipient addresses]
+.SH DESCRIPTION
+mailmail reads RFC822 message text from standard input and delivers them,
+using SMTP, to a Mail Transfer Agent listening at 127.0.0.1:25. It accepts
+(but does not necessarily implement) many of the standard sendmail(1)
+options, but it is preferable to list only the recipient addresses on
+the command line, and include a \fBFrom\fR header within the message text
+indicating the sender.
+.SH AUTHOR
+Written by Jp Calderone
+.SH "REPORTING BUGS"
+To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR
+.SH COPYRIGHT
+Copyright \(co 2003-2008 Twisted Matrix Laboratories.
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-1.tac b/doc/mail/tutorial/smtpclient/smtpclient-1.tac
new file mode 100644
index 0000000..40b685c
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-1.tac
@@ -0,0 +1,3 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-10.tac b/doc/mail/tutorial/smtpclient/smtpclient-10.tac
new file mode 100644
index 0000000..dcfe5ef
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-10.tac
@@ -0,0 +1,56 @@
+import StringIO
+
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.mail import smtp
+
+class SMTPTutorialClient(smtp.ESMTPClient):
+ mailFrom = "tutorial_sender@example.com"
+ mailTo = "tutorial_recipient@example.net"
+ mailData = '''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy <tutorial_sender@example.com>
+To: Tutorial Gal <tutorial_recipient@example.net>
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''
+
+ def getMailFrom(self):
+ result = self.mailFrom
+ self.mailFrom = None
+ return result
+
+ def getMailTo(self):
+ return [self.mailTo]
+
+ def getMailData(self):
+ return StringIO.StringIO(self.mailData)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ print 'Sent', numOk, 'messages'
+
+ from twisted.internet import reactor
+ reactor.stop()
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = SMTPTutorialClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+def getMailExchange(host):
+ return defer.succeed('localhost')
+
+def cbMailExchange(exchange):
+ smtpClientFactory = SMTPClientFactory()
+
+ smtpClientService = internet.TCPClient(exchange, 25, smtpClientFactory)
+ smtpClientService.setServiceParent(application)
+
+getMailExchange('example.net').addCallback(cbMailExchange)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-11.tac b/doc/mail/tutorial/smtpclient/smtpclient-11.tac
new file mode 100644
index 0000000..a52a3eb
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-11.tac
@@ -0,0 +1,58 @@
+import StringIO
+
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.mail import smtp, relaymanager
+
+class SMTPTutorialClient(smtp.ESMTPClient):
+ mailFrom = "tutorial_sender@example.com"
+ mailTo = "tutorial_recipient@example.net"
+ mailData = '''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy <tutorial_sender@example.com>
+To: Tutorial Gal <tutorial_recipient@example.net>
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''
+
+ def getMailFrom(self):
+ result = self.mailFrom
+ self.mailFrom = None
+ return result
+
+ def getMailTo(self):
+ return [self.mailTo]
+
+ def getMailData(self):
+ return StringIO.StringIO(self.mailData)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ print 'Sent', numOk, 'messages'
+
+ from twisted.internet import reactor
+ reactor.stop()
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = SMTPTutorialClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+def getMailExchange(host):
+ def cbMX(mxRecord):
+ return str(mxRecord.name)
+ return relaymanager.MXCalculator().getMX(host).addCallback(cbMX)
+
+def cbMailExchange(exchange):
+ smtpClientFactory = SMTPClientFactory()
+
+ smtpClientService = internet.TCPClient(exchange, 25, smtpClientFactory)
+ smtpClientService.setServiceParent(application)
+
+getMailExchange('example.net').addCallback(cbMailExchange)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-2.tac b/doc/mail/tutorial/smtpclient/smtpclient-2.tac
new file mode 100644
index 0000000..e95921b
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-2.tac
@@ -0,0 +1,10 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpClientFactory = protocol.ClientFactory()
+smtpClientService = internet.TCPClient(None, None, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-3.tac b/doc/mail/tutorial/smtpclient/smtpclient-3.tac
new file mode 100644
index 0000000..26ea519
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-3.tac
@@ -0,0 +1,10 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpClientFactory = protocol.ClientFactory()
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-4.tac b/doc/mail/tutorial/smtpclient/smtpclient-4.tac
new file mode 100644
index 0000000..e95e596
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-4.tac
@@ -0,0 +1,12 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpClientFactory = protocol.ClientFactory()
+smtpClientFactory.protocol = protocol.Protocol
+
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-5.tac b/doc/mail/tutorial/smtpclient/smtpclient-5.tac
new file mode 100644
index 0000000..30af8ad
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-5.tac
@@ -0,0 +1,14 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpClientFactory = protocol.ClientFactory()
+
+from twisted.mail import smtp
+smtpClientFactory.protocol = smtp.ESMTPClient
+
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-6.tac b/doc/mail/tutorial/smtpclient/smtpclient-6.tac
new file mode 100644
index 0000000..d1eb5a0
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-6.tac
@@ -0,0 +1,18 @@
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.mail import smtp
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = smtp.ESMTPClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+smtpClientFactory = SMTPClientFactory()
+
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-7.tac b/doc/mail/tutorial/smtpclient/smtpclient-7.tac
new file mode 100644
index 0000000..297a35a
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-7.tac
@@ -0,0 +1,46 @@
+import StringIO
+
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.mail import smtp
+
+class SMTPTutorialClient(smtp.ESMTPClient):
+ mailFrom = "tutorial_sender@example.com"
+ mailTo = "tutorial_recipient@example.net"
+ mailData = '''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy <tutorial_sender@example.com>
+To: Tutorial Gal <tutorial_recipient@example.net>
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''
+
+ def getMailFrom(self):
+ result = self.mailFrom
+ self.mailFrom = None
+ return result
+
+ def getMailTo(self):
+ return [self.mailTo]
+
+ def getMailData(self):
+ return StringIO.StringIO(self.mailData)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ print 'Sent', numOk, 'messages'
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = SMTPTutorialClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+smtpClientFactory = SMTPClientFactory()
+
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-8.tac b/doc/mail/tutorial/smtpclient/smtpclient-8.tac
new file mode 100644
index 0000000..8dbef10
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-8.tac
@@ -0,0 +1,49 @@
+import StringIO
+
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.mail import smtp
+
+class SMTPTutorialClient(smtp.ESMTPClient):
+ mailFrom = "tutorial_sender@example.com"
+ mailTo = "tutorial_recipient@example.net"
+ mailData = '''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy <tutorial_sender@example.com>
+To: Tutorial Gal <tutorial_recipient@example.net>
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''
+
+ def getMailFrom(self):
+ result = self.mailFrom
+ self.mailFrom = None
+ return result
+
+ def getMailTo(self):
+ return [self.mailTo]
+
+ def getMailData(self):
+ return StringIO.StringIO(self.mailData)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ print 'Sent', numOk, 'messages'
+
+ from twisted.internet import reactor
+ reactor.stop()
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = SMTPTutorialClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+smtpClientFactory = SMTPClientFactory()
+
+smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient-9.tac b/doc/mail/tutorial/smtpclient/smtpclient-9.tac
new file mode 100644
index 0000000..397057a
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient-9.tac
@@ -0,0 +1,53 @@
+import StringIO
+
+from twisted.application import service
+
+application = service.Application("SMTP Client Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.mail import smtp
+
+class SMTPTutorialClient(smtp.ESMTPClient):
+ mailFrom = "tutorial_sender@example.com"
+ mailTo = "tutorial_recipient@example.net"
+ mailData = '''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy <tutorial_sender@example.com>
+To: Tutorial Gal <tutorial_recipient@example.net>
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''
+
+ def getMailFrom(self):
+ result = self.mailFrom
+ self.mailFrom = None
+ return result
+
+ def getMailTo(self):
+ return [self.mailTo]
+
+ def getMailData(self):
+ return StringIO.StringIO(self.mailData)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ print 'Sent', numOk, 'messages'
+
+ from twisted.internet import reactor
+ reactor.stop()
+
+class SMTPClientFactory(protocol.ClientFactory):
+ protocol = SMTPTutorialClient
+
+ def buildProtocol(self, addr):
+ return self.protocol(secret=None, identity='example.com')
+
+def getMailExchange(host):
+ return 'localhost'
+
+smtpClientFactory = SMTPClientFactory()
+
+smtpClientService = internet.TCPClient(
+ getMailExchange('example.net'), 25, smtpClientFactory)
+smtpClientService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpclient/smtpclient.html b/doc/mail/tutorial/smtpclient/smtpclient.html
new file mode 100644
index 0000000..954037e
--- /dev/null
+++ b/doc/mail/tutorial/smtpclient/smtpclient.html
@@ -0,0 +1,757 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Mail Tutorial: Building an SMTP Client from Scratch</title>
+<link href="../../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Mail Tutorial: Building an SMTP Client from Scratch</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><ul><li><a href="#auto1">SMTP Client 1</a></li><li><a href="#auto2">SMTP Client 2</a></li><li><a href="#auto3">SMTP Client 3</a></li><li><a href="#auto4">SMTP Client 4</a></li><li><a href="#auto5">SMTP Client 5</a></li><li><a href="#auto6">SMTP Client 6</a></li><li><a href="#auto7">SMTP Client 7</a></li><li><a href="#auto8">SMTP Client 8</a></li><li><a href="#auto9">SMTP Client 9</a></li><li><a href="#auto10">SMTP Client 10</a></li><li><a href="#auto11">SMTP Client 11</a></li></ul></ol></div>
+ <div class="content">
+
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p>This tutorial will walk you through the creation of an extremely
+simple SMTP client application. By the time the tutorial is complete,
+you will understand how to create and start a TCP client speaking the
+SMTP protocol, have it connect to an appropriate mail exchange server,
+and transmit a message for delivery.</p>
+
+<p>For the majority of this tutorial, <code>twistd</code> will be used
+to launch the application. Near the end we will explore other
+possibilities for starting a Twisted application. Until then, make
+sure that you have <code>twistd</code> installed and conveniently
+accessible for use in running each of the example <code>.tac</code>
+files.</p>
+
+<h3>SMTP Client 1<a name="auto1"/></h3>
+
+<p>The first step is to create <a href="smtpclient-1.tac" shape="rect">the most
+minimal <code>.tac</code> file</a> possible for use by <code>twistd</code> .</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>
+</pre>
+
+<p>The first line of the <code>.tac</code> file
+imports <code>twisted.application.service</code>, a module which
+contains many of the basic <em>service</em> classes and helper
+functions available in Twisted. In particular, we will be using
+the <code>Application</code> function to create a new <em>application
+service</em>. An <em>application service</em> simply acts as a
+central object on which to store certain kinds of deployment
+configuration.</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">&quot;SMTP Client Tutorial&quot;</span>)
+</pre>
+
+<p>The second line of the <code>.tac</code> file creates a
+new <em>application service</em> and binds it to the local
+name <code>application</code>. <code>twistd</code> requires this
+local name in each <code>.tac</code> file it runs. It uses various
+pieces of configuration on the object to determine its behavior. For
+example, <code>&quot;SMTP Client Tutorial&quot;</code> will be used as the name
+of the <code>.tap</code> file into which to serialize application
+state, should it be necessary to do so.</p>
+
+<p>That does it for the first example. We now have enough of
+a <code>.tac</code> file to pass to <code>twistd</code>. If we
+run <a href="smtpclient-1.tac" shape="rect">smtpclient-1.tac</a> using
+the <code>twistd</code> command line:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">twistd</span> -<span class="py-src-variable">ny</span> <span class="py-src-variable">smtpclient</span>-<span class="py-src-number">1.</span><span class="py-src-variable">tac</span>
+</pre>
+
+<p>we are rewarded with the following output:</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-1.tac
+18:31 EST [-] Log opened.
+18:31 EST [-] twistd 2.0.0 (/usr/bin/python2.4 2.4.1) starting up
+18:31 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+18:31 EST [-] Loading smtpclient-1.tac...
+18:31 EST [-] Loaded.
+</pre>
+
+<p>As we expected, not much is going on. We can shutdown this server
+by issuing <code>^C</code>:</p>
+
+<pre class="shell" xml:space="preserve">
+18:34 EST [-] Received SIGINT, shutting down.
+18:34 EST [-] Main loop terminated.
+18:34 EST [-] Server Shut Down.
+exarkun@boson:~/mail/tutorial/smtpclient$
+</pre>
+
+<h3>SMTP Client 2<a name="auto2"/></h3>
+
+<p>The first version of our SMTP client wasn't very interesting. It
+didn't even establish any TCP connections! The <a href="smtpclient-2.tac" shape="rect">second version</a> will come a little bit
+closer to that level of complexity. First, we need to import a few
+more things:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">protocol</span>
+</pre>
+
+<p><code>twisted.application.internet</code> is
+another <em>application service</em> module. It provides services for
+establishing outgoing connections (as well as creating network
+servers, though we are not interested in those parts for the
+moment). <code>twisted.internet.protocol</code> provides base
+implementations of many of the core Twisted concepts, such
+as <em>factories</em> and <em>protocols</em>.</p>
+
+<p>The next line of <a href="smtpclient-2.tac" shape="rect">smtpclient-2.tac</a>
+instantiates a new <em>client factory</em>.</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientFactory</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">ClientFactory</span>()
+</pre>
+
+<p><em>Client factories</em> are responsible for
+constructing <em>protocol instances</em> whenever connections are
+established. They may be required to create just one instance, or
+many instances if many different connections are established, or they
+may never be required to create one at all, if no connection ever
+manages to be established.</p>
+
+<p>Now that we have a client factory, we'll need to hook it up to the
+network somehow. The next line of <code>smtpclient-2.tac</code> does
+just that:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">smtpClientFactory</span>)
+</pre>
+
+<p>We'll ignore the first two arguments
+to <code>internet.TCPClient</code> for the moment and instead focus on
+the third. <code>TCPClient</code> is one of those <em>application
+service</em> classes. It creates TCP connections to a specified
+address and then uses its third argument, a <em>client factory</em>,
+to get a <em>protocol instance</em>. It then associates the TCP
+connection with the protocol instance and gets out of the way.</p>
+
+<p>We can try to run <code>smtpclient-2.tac</code> the same way we
+ran <code>smtpclient-1.tac</code>, but the results might be a little
+disappointing:</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-2.tac
+18:55 EST [-] Log opened.
+18:55 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+18:55 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+18:55 EST [-] Loading smtpclient-2.tac...
+18:55 EST [-] Loaded.
+18:55 EST [-] Starting factory &lt;twisted.internet.protocol.ClientFactory
+ instance at 0xb791e46c&gt;
+18:55 EST [-] Traceback (most recent call last):
+ File &quot;twisted/scripts/twistd.py&quot;, line 187, in runApp
+ app.runReactorWithLogging(config, oldstdout, oldstderr)
+ File &quot;twisted/application/app.py&quot;, line 128, in runReactorWithLogging
+ reactor.run()
+ File &quot;twisted/internet/posixbase.py&quot;, line 200, in run
+ self.mainLoop()
+ File &quot;twisted/internet/posixbase.py&quot;, line 208, in mainLoop
+ self.runUntilCurrent()
+ --- &lt;exception caught here&gt; ---
+ File &quot;twisted/internet/base.py&quot;, line 533, in runUntilCurrent
+ call.func(*call.args, **call.kw)
+ File &quot;twisted/internet/tcp.py&quot;, line 489, in resolveAddress
+ if abstract.isIPAddress(self.addr[0]):
+ File &quot;twisted/internet/abstract.py&quot;, line 315, in isIPAddress
+ parts = string.split(addr, '.')
+ File &quot;/usr/lib/python2.4/string.py&quot;, line 292, in split
+ return s.split(sep, maxsplit)
+ exceptions.AttributeError: 'NoneType' object has no attribute 'split'
+
+18:55 EST [-] Received SIGINT, shutting down.
+18:55 EST [-] Main loop terminated.
+18:55 EST [-] Server Shut Down.
+exarkun@boson:~/mail/tutorial/smtpclient$
+</pre>
+
+<p>What happened? Those first two arguments to <code>TCPClient</code>
+turned out to be important after all. We'll get to them in the next
+example.</p>
+
+<h3>SMTP Client 3<a name="auto3"/></h3>
+
+<p>Version three of our SMTP client only changes one thing. The line
+from version two:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">None</span>, <span class="py-src-variable">smtpClientFactory</span>)
+</pre>
+
+<p>has its first two arguments changed from <code>None</code> to
+something with a bit more meaning:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-string">'localhost'</span>, <span class="py-src-number">25</span>, <span class="py-src-variable">smtpClientFactory</span>)
+</pre>
+
+<p>This directs the client to connect to <em>localhost</em> on
+port <em>25</em>. This isn't the address we want ultimately, but it's
+a good place-holder for the time being. We can
+run <a href="smtpclient-3.tac" shape="rect">smtpclient-3.tac</a> and see what this
+change gets us:</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-3.tac
+19:10 EST [-] Log opened.
+19:10 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+19:10 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+19:10 EST [-] Loading smtpclient-3.tac...
+19:10 EST [-] Loaded.
+19:10 EST [-] Starting factory &lt;twisted.internet.protocol.ClientFactory
+ instance at 0xb791e48c&gt;
+19:10 EST [-] Enabling Multithreading.
+19:10 EST [Uninitialized] Traceback (most recent call last):
+ File &quot;twisted/python/log.py&quot;, line 56, in callWithLogger
+ return callWithContext({&quot;system&quot;: lp}, func, *args, **kw)
+ File &quot;twisted/python/log.py&quot;, line 41, in callWithContext
+ return context.call({ILogContext: newCtx}, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 52, in callWithContext
+ return self.currentContext().callWithContext(ctx, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 31, in callWithContext
+ return func(*args,**kw)
+ --- &lt;exception caught here&gt; ---
+ File &quot;twisted/internet/selectreactor.py&quot;, line 139, in _doReadOrWrite
+ why = getattr(selectable, method)()
+ File &quot;twisted/internet/tcp.py&quot;, line 543, in doConnect
+ self._connectDone()
+ File &quot;twisted/internet/tcp.py&quot;, line 546, in _connectDone
+ self.protocol = self.connector.buildProtocol(self.getPeer())
+ File &quot;twisted/internet/base.py&quot;, line 641, in buildProtocol
+ return self.factory.buildProtocol(addr)
+ File &quot;twisted/internet/protocol.py&quot;, line 99, in buildProtocol
+ p = self.protocol()
+ exceptions.TypeError: 'NoneType' object is not callable
+
+19:10 EST [Uninitialized] Stopping factory
+ &lt;twisted.internet.protocol.ClientFactory instance at
+ 0xb791e48c&gt;
+19:10 EST [-] Received SIGINT, shutting down.
+19:10 EST [-] Main loop terminated.
+19:10 EST [-] Server Shut Down.
+exarkun@boson:~/mail/tutorial/smtpclient$
+</pre>
+
+<p>A meagre amount of progress, but the service still raises an
+exception. This time, it's because we haven't specified
+a <em>protocol class</em> for the factory to use. We'll do that in
+the next example.</p>
+
+<h3>SMTP Client 4<a name="auto4"/></h3>
+
+<p>In the previous example, we ran into a problem because we hadn't
+set up our <em>client factory's</em> <em>protocol</em> attribute
+correctly (or at all). <code>ClientFactory.buildProtocol</code> is
+the method responsible for creating a <em>protocol instance</em>. The
+default implementation calls the factory's <code>protocol</code> attribute,
+adds itself as an attribute named <code>factory</code> to the
+resulting instance, and returns it. In <a href="smtpclient-4.tac" shape="rect">smtpclient-4.tac</a>, we'll correct the
+oversight that caused the traceback in smtpclient-3.tac:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientFactory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">Protocol</span>
+</pre>
+
+<p>Running this version of the client, we can see the output is once
+again traceback free:</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-4.tac
+19:29 EST [-] Log opened.
+19:29 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+19:29 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+19:29 EST [-] Loading smtpclient-4.tac...
+19:29 EST [-] Loaded.
+19:29 EST [-] Starting factory &lt;twisted.internet.protocol.ClientFactory
+ instance at 0xb791e4ac&gt;
+19:29 EST [-] Enabling Multithreading.
+19:29 EST [-] Received SIGINT, shutting down.
+19:29 EST [Protocol,client] Stopping factory
+ &lt;twisted.internet.protocol.ClientFactory instance at
+ 0xb791e4ac&gt;
+19:29 EST [-] Main loop terminated.
+19:29 EST [-] Server Shut Down.
+exarkun@boson:~/doc/mail/tutorial/smtpclient$
+</pre>
+
+<p>But what does this
+mean? <code>twisted.internet.protocol.Protocol</code> is the
+base <em>protocol</em> implementation. For those familiar with the
+classic UNIX network services, it is equivalent to
+the <em>discard</em> service. It never produces any output and it
+discards all its input. Not terribly useful, and certainly nothing
+like an SMTP client. Let's see how we can improve this in the next
+example.</p>
+
+<h3>SMTP Client 5<a name="auto5"/></h3>
+
+<p>In <a href="smtpclient-5.tac" shape="rect">smtpclient-5.tac</a>, we will begin
+to use Twisted's SMTP protocol implementation for the first time.
+We'll make the obvious change, simply swapping
+out <code>twisted.internet.protocol.Protocol</code> in favor
+of <code>twisted.mail.smtp.ESMTPClient</code>. Don't worry about
+the <em>E</em> in <em>ESMTP</em>. It indicates we're actually using a
+newer version of the SMTP protocol. There is
+an <code>SMTPClient</code> in Twisted, but there's essentially no
+reason to ever use it.</p>
+
+<p>smtpclient-5.tac adds a new import:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">mail</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">smtp</span>
+</pre>
+
+<p>All of the mail related code in Twisted exists beneath
+the <code>twisted.mail</code> package. More specifically, everything
+having to do with the SMTP protocol implementation is defined in
+the <code>twisted.mail.smtp</code> module.</p>
+
+<p>Next we remove a line we added in smtpclient-4.tac:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientFactory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">protocol</span>.<span class="py-src-variable">Protocol</span>
+</pre>
+
+<p>And add a similar one in its place:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientFactory</span>.<span class="py-src-variable">protocol</span> = <span class="py-src-variable">smtp</span>.<span class="py-src-variable">ESMTPClient</span>
+</pre>
+
+<p>Our client factory is now using a protocol implementation which
+behaves as an SMTP client. What happens when we try to run this
+version?</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-5.tac
+19:42 EST [-] Log opened.
+19:42 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+19:42 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+19:42 EST [-] Loading smtpclient-5.tac...
+19:42 EST [-] Loaded.
+19:42 EST [-] Starting factory &lt;twisted.internet.protocol.ClientFactory
+ instance at 0xb791e54c&gt;
+19:42 EST [-] Enabling Multithreading.
+19:42 EST [Uninitialized] Traceback (most recent call last):
+ File &quot;twisted/python/log.py&quot;, line 56, in callWithLogger
+ return callWithContext({&quot;system&quot;: lp}, func, *args, **kw)
+ File &quot;twisted/python/log.py&quot;, line 41, in callWithContext
+ return context.call({ILogContext: newCtx}, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 52, in callWithContext
+ return self.currentContext().callWithContext(ctx, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 31, in callWithContext
+ return func(*args,**kw)
+ --- &lt;exception caught here&gt; ---
+ File &quot;twisted/internet/selectreactor.py&quot;, line 139, in _doReadOrWrite
+ why = getattr(selectable, method)()
+ File &quot;twisted/internet/tcp.py&quot;, line 543, in doConnect
+ self._connectDone()
+ File &quot;twisted/internet/tcp.py&quot;, line 546, in _connectDone
+ self.protocol = self.connector.buildProtocol(self.getPeer())
+ File &quot;twisted/internet/base.py&quot;, line 641, in buildProtocol
+ return self.factory.buildProtocol(addr)
+ File &quot;twisted/internet/protocol.py&quot;, line 99, in buildProtocol
+ p = self.protocol()
+ exceptions.TypeError: __init__() takes at least 2 arguments (1 given)
+
+19:42 EST [Uninitialized] Stopping factory
+ &lt;twisted.internet.protocol.ClientFactory instance at
+ 0xb791e54c&gt;
+19:43 EST [-] Received SIGINT, shutting down.
+19:43 EST [-] Main loop terminated.
+19:43 EST [-] Server Shut Down.
+exarkun@boson:~/doc/mail/tutorial/smtpclient$
+</pre>
+
+
+<p>Oops, back to getting a traceback. This time, the default
+implementation of <code>buildProtocol</code> seems no longer to be
+sufficient. It instantiates the protocol with no arguments,
+but <code>ESMTPClient</code> wants at least one argument. In the next
+version of the client, we'll override <code>buildProtocol</code> to
+fix this problem.</p>
+
+<h3>SMTP Client 6<a name="auto6"/></h3>
+
+<p><a href="smtpclient-6.tac" shape="rect">smtpclient-6.tac</a> introduces
+a <code>twisted.internet.protocol.ClientFactory</code> subclass with
+an overridden <code>buildProtocol</code> method to overcome the
+problem encountered in the previous example.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SMTPClientFactory</span>(<span class="py-src-parameter">protocol</span>.<span class="py-src-parameter">ClientFactory</span>):
+ <span class="py-src-variable">protocol</span> = <span class="py-src-variable">smtp</span>.<span class="py-src-variable">ESMTPClient</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">protocol</span>(<span class="py-src-variable">secret</span>=<span class="py-src-variable">None</span>, <span class="py-src-variable">identity</span>=<span class="py-src-string">'example.com'</span>)
+</pre>
+
+<p>The overridden method does almost the same thing as the base
+implementation: the only change is that it passes values for two
+arguments to <code>twisted.mail.smtp.ESMTPClient</code>'s initializer.
+The <code>secret</code> argument is used for SMTP authentication
+(which we will not attempt yet). The <code>identity</code> argument
+is used as a to identify ourselves Another minor change to note is
+that the <code>protocol</code> attribute is now defined in the class
+definition, rather than tacked onto an instance after one is created.
+This means it is a class attribute, rather than an instance attribute,
+now, which makes no difference as far as this example is concerned.
+There are circumstances in which the difference is important: be sure
+you understand the implications of each approach when creating your
+own factories.</p>
+
+<p>One other change is required: instead of
+instantiating <code>twisted.internet.protocol.ClientFactory</code>, we
+will now instantiate <code>SMTPClientFactory</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">smtpClientFactory</span> = <span class="py-src-variable">SMTPClientFactory</span>()
+</pre>
+
+<p>Running this version of the code, we observe that the
+code <strong>still</strong> isn't quite traceback-free.</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-6.tac
+21:17 EST [-] Log opened.
+21:17 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+21:17 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+21:17 EST [-] Loading smtpclient-6.tac...
+21:17 EST [-] Loaded.
+21:17 EST [-] Starting factory &lt;__builtin__.SMTPClientFactory instance
+ at 0xb77fd68c&gt;
+21:17 EST [-] Enabling Multithreading.
+21:17 EST [ESMTPClient,client] Traceback (most recent call last):
+ File &quot;twisted/python/log.py&quot;, line 56, in callWithLogger
+ return callWithContext({&quot;system&quot;: lp}, func, *args, **kw)
+ File &quot;twisted/python/log.py&quot;, line 41, in callWithContext
+ return context.call({ILogContext: newCtx}, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 52, in callWithContext
+ return self.currentContext().callWithContext(ctx, func, *args, **kw)
+ File &quot;twisted/python/context.py&quot;, line 31, in callWithContext
+ return func(*args,**kw)
+ --- &lt;exception caught here&gt; ---
+ File &quot;twisted/internet/selectreactor.py&quot;, line 139, in _doReadOrWrite
+ why = getattr(selectable, method)()
+ File &quot;twisted/internet/tcp.py&quot;, line 351, in doRead
+ return self.protocol.dataReceived(data)
+ File &quot;twisted/protocols/basic.py&quot;, line 221, in dataReceived
+ why = self.lineReceived(line)
+ File &quot;twisted/mail/smtp.py&quot;, line 1039, in lineReceived
+ why = self._okresponse(self.code,'\n'.join(self.resp))
+ File &quot;twisted/mail/smtp.py&quot;, line 1281, in esmtpState_serverConfig
+ self.tryTLS(code, resp, items)
+ File &quot;twisted/mail/smtp.py&quot;, line 1294, in tryTLS
+ self.authenticate(code, resp, items)
+ File &quot;twisted/mail/smtp.py&quot;, line 1343, in authenticate
+ self.smtpState_from(code, resp)
+ File &quot;twisted/mail/smtp.py&quot;, line 1062, in smtpState_from
+ self._from = self.getMailFrom()
+ File &quot;twisted/mail/smtp.py&quot;, line 1137, in getMailFrom
+ raise NotImplementedError
+ exceptions.NotImplementedError:
+
+21:17 EST [ESMTPClient,client] Stopping factory
+ &lt;__builtin__.SMTPClientFactory instance at 0xb77fd68c&gt;
+21:17 EST [-] Received SIGINT, shutting down.
+21:17 EST [-] Main loop terminated.
+21:17 EST [-] Server Shut Down.
+exarkun@boson:~/doc/mail/tutorial/smtpclient$
+</pre>
+
+<p>What we have accomplished with this iteration of the example is to
+navigate far enough into an SMTP transaction that Twisted is now
+interested in calling back to application-level code to determine what
+its next step should be. In the next example, we'll see how to
+provide that information to it.</p>
+
+<h3>SMTP Client 7<a name="auto7"/></h3>
+
+<p>SMTP Client 7 is the first version of our SMTP client which
+actually includes message data to transmit. For simplicity's sake,
+the message is defined as part of a new class. In a useful program
+which sent email, message data might be pulled in from the filesystem,
+a database, or be generated based on
+user-input. <a href="smtpclient-7.tac" shape="rect">smtpclient-7.tac</a>, however,
+defines a new class, <code>SMTPTutorialClient</code>, with three class
+attributes (<code>mailFrom</code>, <code>mailTo</code>,
+and <code>mailData</code>):</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">SMTPTutorialClient</span>(<span class="py-src-parameter">smtp</span>.<span class="py-src-parameter">ESMTPClient</span>):
+ <span class="py-src-variable">mailFrom</span> = <span class="py-src-string">&quot;tutorial_sender@example.com&quot;</span>
+ <span class="py-src-variable">mailTo</span> = <span class="py-src-string">&quot;tutorial_recipient@example.net&quot;</span>
+ <span class="py-src-variable">mailData</span> = <span class="py-src-string">'''\
+Date: Fri, 6 Feb 2004 10:14:39 -0800
+From: Tutorial Guy &lt;tutorial_sender@example.com&gt;
+To: Tutorial Gal &lt;tutorial_recipient@example.net&gt;
+Subject: Tutorate!
+
+Hello, how are you, goodbye.
+'''</span>
+</pre>
+
+<p>This statically defined data is accessed later in the class
+definition by three of the methods which are part of the
+ <em>SMTPClient callback API</em>. Twisted expects each of the three
+methods below to be defined and to return an object with a particular
+meaning. First, <code>getMailFrom</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailFrom</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">result</span> = <span class="py-src-variable">self</span>.<span class="py-src-variable">mailFrom</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">mailFrom</span> = <span class="py-src-variable">None</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">result</span>
+</pre>
+
+<p>This method is called to determine the <em>reverse-path</em>,
+otherwise known as the <em>envelope from</em>, of the message. This
+value will be used when sending the <code>MAIL FROM</code> SMTP
+command. The method must return a string which conforms to the <a href="http://www.faqs.org/rfcs/rfc2821.html" shape="rect">RFC 2821</a> definition
+of a <em>reverse-path</em>. In simpler terms, it should be a string
+like <code>&quot;alice@example.com&quot;</code>. Only one <em>envelope
+from</em> is allowed by the SMTP protocol, so it cannot be a list of
+strings or a comma separated list of addresses. Our implementation
+of <code>getMailFrom</code> does a little bit more than just return a
+string; we'll get back to this in a little bit.</p>
+
+<p>The next method is <code>getMailTo</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailTo</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> [<span class="py-src-variable">self</span>.<span class="py-src-variable">mailTo</span>]
+</pre>
+
+<p><code>getMailTo</code> is similar to <code>getMailFrom</code>. It
+returns one or more RFC 2821 addresses (this time a
+ <em>forward-path</em>, or <em>envelope to</em>). Since SMTP allows
+multiple recipients, <code>getMailTo</code> returns a list of these
+addresses. The list must contain at least one address, and even if
+there is exactly one recipient, it must still be in a list.</p>
+
+<p>The final callback we will define to provide information to
+Twisted is <code>getMailData</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailData</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">StringIO</span>.<span class="py-src-variable">StringIO</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">mailData</span>)
+</pre>
+
+<p>This one is quite simple as well: it returns a file or a file-like
+object which contains the message contents. In our case, we return
+a <code>StringIO</code> since we already have a string containing our
+message. If the contents of the file returned
+by <code>getMailData</code> span multiple lines (as email messages
+often do), the lines should be <code>\n</code> delimited (as they
+would be when opening a text file in the <code>&quot;rt&quot;</code> mode):
+necessary newline translation will be performed
+by <code>SMTPClient</code> automatically.</p>
+
+<p>There is one more new callback method defined in smtpclient-7.tac.
+This one isn't for providing information about the messages to
+Twisted, but for Twisted to provide information about the success or
+failure of the message transmission to the application:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">sentMail</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">code</span>, <span class="py-src-parameter">resp</span>, <span class="py-src-parameter">numOk</span>, <span class="py-src-parameter">addresses</span>, <span class="py-src-parameter">log</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Sent'</span>, <span class="py-src-variable">numOk</span>, <span class="py-src-string">'messages'</span>
+</pre>
+
+<p>Each of the arguments to <code>sentMail</code> provides some
+information about the success or failure of the message transmission
+transaction. <code>code</code> is the response code from the ultimate
+command. For successful transactions, it will be 250. For transient
+failures (those which should be retried), it will be between 400 and
+499, inclusive. For permanent failures (this which will never work,
+no matter how many times you retry them), it will be between 500 and
+599.</p>
+
+<h3>SMTP Client 8<a name="auto8"/></h3>
+
+<p>Thus far we have succeeded in creating a Twisted client application
+which starts up, connects to a (possibly) remote host, transmits some
+data, and disconnects. Notably missing, however, is application
+shutdown. Hitting ^C is fine during development, but it's not exactly
+a long-term solution. Fortunately, programmatic shutdown is extremely
+simple. <a href="smtpclient-8.tac" shape="rect">smtpclient-8.tac</a>
+extends <code>sentMail</code> with these two lines:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+</pre>
+
+<p>The <code>stop</code> method of the reactor causes the main event
+loop to exit, allowing a Twisted server to shut down. With this
+version of the example, we see that the program actually terminates
+after sending the message, without user-intervention:</p>
+
+<pre class="shell" xml:space="preserve">
+exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-8.tac
+19:52 EST [-] Log opened.
+19:52 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
+19:52 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
+19:52 EST [-] Loading smtpclient-8.tac...
+19:52 EST [-] Loaded.
+19:52 EST [-] Starting factory &lt;__builtin__.SMTPClientFactory instance
+ at 0xb791beec&gt;
+19:52 EST [-] Enabling Multithreading.
+19:52 EST [SMTPTutorialClient,client] Sent 1 messages
+19:52 EST [SMTPTutorialClient,client] Stopping factory
+ &lt;__builtin__.SMTPClientFactory instance at 0xb791beec&gt;
+19:52 EST [-] Main loop terminated.
+19:52 EST [-] Server Shut Down.
+exarkun@boson:~/doc/mail/tutorial/smtpclient$
+</pre>
+
+<h3>SMTP Client 9<a name="auto9"/></h3>
+
+<p>One task remains to be completed in this tutorial SMTP client:
+instead of always sending mail through a well-known host, we will look
+up the mail exchange server for the recipient address and try to
+deliver the message to that host.</p>
+
+<p>In <a href="smtpclient-9.tac" shape="rect">smtpclient-9.tac</a>, we'll take the
+first step towards this feature by defining a function which returns
+the mail exchange host for a particular domain:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailExchange</span>(<span class="py-src-parameter">host</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'localhost'</span>
+</pre>
+
+<p>Obviously this doesn't return the correct mail exchange host yet
+(in fact, it returns the exact same host we have been using all
+along), but pulling out the logic for determining which host to
+connect to into a function like this is the first step towards our
+ultimate goal. Now that we have <code>getMailExchange</code>, we'll
+call it when constructing our <code>TCPClient</code> service:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">smtpClientService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(
+ <span class="py-src-variable">getMailExchange</span>(<span class="py-src-string">'example.net'</span>), <span class="py-src-number">25</span>, <span class="py-src-variable">smtpClientFactory</span>)
+</pre>
+
+<p>We'll expand on the definition of <code>getMailExchange</code> in
+the next example.</p>
+
+<h3>SMTP Client 10<a name="auto10"/></h3>
+
+<p>In the previous example we defined <code>getMailExchange</code> to
+return a string representing the mail exchange host for a particular
+domain. While this was a step in the right direction, it turns out
+not to be a very big one. Determining the mail exchange host for a
+particular domain is going to involve network traffic (specifically,
+some DNS requests). These might take an arbitrarily large amount of
+time, so we need to introduce a <code>Deferred</code> to represent the
+result of <code>getMailExchange</code>. <a href="smtpclient-10.tac" shape="rect">smtpclient-10.tac</a> redefines it
+thusly:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailExchange</span>(<span class="py-src-parameter">host</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">defer</span>.<span class="py-src-variable">succeed</span>(<span class="py-src-string">'localhost'</span>)
+</pre>
+
+<p><code>defer.succeed</code> is a function which creates a
+new <code>Deferred</code> which already has a result, in this
+case <code>'localhost'</code>. Now we need to adjust
+our <code>TCPClient</code>-constructing code to expect and properly
+handle this <code>Deferred</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">cbMailExchange</span>(<span class="py-src-parameter">exchange</span>):
+ <span class="py-src-variable">smtpClientFactory</span> = <span class="py-src-variable">SMTPClientFactory</span>()
+
+ <span class="py-src-variable">smtpClientService</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPClient</span>(<span class="py-src-variable">exchange</span>, <span class="py-src-number">25</span>, <span class="py-src-variable">smtpClientFactory</span>)
+ <span class="py-src-variable">smtpClientService</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+
+<span class="py-src-variable">getMailExchange</span>(<span class="py-src-string">'example.net'</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbMailExchange</span>)
+</pre>
+
+<p>An in-depth exploration of <code>Deferred</code>s is beyond the
+scope of this document. For such a look, see
+the <a href="../../../core/howto/defer.html" shape="rect">Deferred Reference</a>.
+However, in brief, what this version of the code does is to delay the
+creation of the <code>TCPClient</code> until the <code>Deferred</code>
+returned by <code>getMailExchange</code> fires. Once it does, we
+proceed normally through the creation of
+our <code>SMTPClientFactory</code> and <code>TCPClient</code>, as well
+as set the <code>TCPClient</code>'s service parent, just as we did in
+the previous examples.</p>
+
+<h3>SMTP Client 11<a name="auto11"/></h3>
+
+<p>At last we're ready to perform the mail exchange lookup. We do
+this by calling on an object provided specifically for this
+task, <code>twisted.mail.relaymanager.MXCalculator</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">getMailExchange</span>(<span class="py-src-parameter">host</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">cbMX</span>(<span class="py-src-parameter">mxRecord</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">str</span>(<span class="py-src-variable">mxRecord</span>.<span class="py-src-variable">name</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">relaymanager</span>.<span class="py-src-variable">MXCalculator</span>().<span class="py-src-variable">getMX</span>(<span class="py-src-variable">host</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbMX</span>)
+</pre>
+
+<p>Because <code>getMX</code> returns a <code>Record_MX</code> object
+rather than a string, we do a little bit of post-processing to get the
+results we want. We have already converted the rest of the tutorial
+application to expect a <code>Deferred</code>
+from <code>getMailExchange</code>, so no further changes are
+required. <a href="smtpclient-11.tac" shape="rect">smtpclient-11.tac</a> completes
+this tutorial by being able to both look up the mail exchange host for
+the recipient domain, connect to it, complete an SMTP transaction,
+report its results, and finally shut down the reactor.</p>
+
+
+
+</div>
+
+ <p><a href="../../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-1.tac b/doc/mail/tutorial/smtpserver/smtpserver-1.tac
new file mode 100644
index 0000000..4804723
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-1.tac
@@ -0,0 +1,3 @@
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-2.tac b/doc/mail/tutorial/smtpserver/smtpserver-2.tac
new file mode 100644
index 0000000..00f143a
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-2.tac
@@ -0,0 +1,10 @@
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpServerFactory = protocol.ServerFactory()
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-3.tac b/doc/mail/tutorial/smtpserver/smtpserver-3.tac
new file mode 100644
index 0000000..5ff3cb5
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-3.tac
@@ -0,0 +1,12 @@
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpServerFactory = protocol.ServerFactory()
+smtpServerFactory.protocol = protocol.Protocol
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-4.tac b/doc/mail/tutorial/smtpserver/smtpserver-4.tac
new file mode 100644
index 0000000..a8ee09e
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-4.tac
@@ -0,0 +1,14 @@
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol
+
+smtpServerFactory = protocol.ServerFactory()
+
+from twisted.mail import smtp
+smtpServerFactory.protocol = smtp.ESMTP
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-5.tac b/doc/mail/tutorial/smtpserver/smtpserver-5.tac
new file mode 100644
index 0000000..6f3a961
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-5.tac
@@ -0,0 +1,50 @@
+import os
+from zope.interface import implements
+
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol, defer
+
+smtpServerFactory = protocol.ServerFactory()
+
+from twisted.mail import smtp
+
+class FileMessage(object):
+ implements(smtp.IMessage)
+
+ def __init__(self, fileObj):
+ self.fileObj = fileObj
+
+ def lineReceived(self, line):
+ self.fileObj.write(line + '\n')
+
+ def eomReceived(self):
+ self.fileObj.close()
+ return defer.succeed(None)
+
+ def connectionLost(self):
+ self.fileObj.close()
+ os.remove(self.fileObj.name)
+
+class TutorialESMTP(smtp.ESMTP):
+ counter = 0
+
+ def validateTo(self, user):
+ fileName = 'tutorial-smtp.' + str(self.counter)
+ self.counter += 1
+ return lambda: FileMessage(file(fileName, 'w'))
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+ def receivedHeader(self, helo, origin, recipients):
+ return 'Received: Tutorially.'
+
+
+smtpServerFactory.protocol = TutorialESMTP
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-6.tac b/doc/mail/tutorial/smtpserver/smtpserver-6.tac
new file mode 100644
index 0000000..5924384
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-6.tac
@@ -0,0 +1,57 @@
+import os
+from zope.interface import implements
+
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol, defer
+
+smtpServerFactory = protocol.ServerFactory()
+
+from twisted.mail import smtp
+
+class FileMessage(object):
+ implements(smtp.IMessage)
+
+ def __init__(self, fileObj):
+ self.fileObj = fileObj
+
+ def lineReceived(self, line):
+ self.fileObj.write(line + '\n')
+
+ def eomReceived(self):
+ self.fileObj.close()
+ return defer.succeed(None)
+
+ def connectionLost(self):
+ self.fileObj.close()
+ os.remove(self.fileObj.name)
+
+class TutorialESMTP(smtp.ESMTP):
+ counter = 0
+
+ def validateTo(self, user):
+ fileName = 'tutorial-smtp.' + str(self.counter)
+ self.counter += 1
+ return lambda: FileMessage(file(fileName, 'w'))
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+ def receivedHeader(self, helo, origin, recipients):
+ return 'Received: Tutorially.'
+
+class TutorialESMTPFactory(protocol.ServerFactory):
+ protocol = TutorialESMTP
+
+ def buildProtocol(self, addr):
+ p = self.protocol()
+ p.factory = self
+ return p
+
+smtpServerFactory.protocol = TutorialESMTP
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-7.tac b/doc/mail/tutorial/smtpserver/smtpserver-7.tac
new file mode 100644
index 0000000..db98032
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-7.tac
@@ -0,0 +1,57 @@
+import os
+from zope.interface import implements
+
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol, defer
+
+from twisted.mail import smtp
+
+class FileMessage(object):
+ implements(smtp.IMessage)
+
+ def __init__(self, fileObj):
+ self.fileObj = fileObj
+
+ def lineReceived(self, line):
+ self.fileObj.write(line + '\n')
+
+ def eomReceived(self):
+ self.fileObj.close()
+ return defer.succeed(None)
+
+ def connectionLost(self):
+ self.fileObj.close()
+ os.remove(self.fileObj.name)
+
+class TutorialDelivery(object):
+ implements(smtp.IMessageDelivery)
+ counter = 0
+
+ def validateTo(self, user):
+ fileName = 'tutorial-smtp.' + str(self.counter)
+ self.counter += 1
+ return lambda: FileMessage(file(fileName, 'w'))
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+ def receivedHeader(self, helo, origin, recipients):
+ return 'Received: Tutorially.'
+
+class TutorialESMTPFactory(protocol.ServerFactory):
+ protocol = smtp.ESMTP
+
+ def buildProtocol(self, addr):
+ p = self.protocol()
+ p.delivery = TutorialDelivery()
+ p.factory = self
+ return p
+
+smtpServerFactory = TutorialESMTPFactory()
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/mail/tutorial/smtpserver/smtpserver-8.tac b/doc/mail/tutorial/smtpserver/smtpserver-8.tac
new file mode 100644
index 0000000..6133912
--- /dev/null
+++ b/doc/mail/tutorial/smtpserver/smtpserver-8.tac
@@ -0,0 +1,63 @@
+import os
+from zope.interface import implements
+
+from twisted.application import service
+
+application = service.Application("SMTP Server Tutorial")
+
+from twisted.application import internet
+from twisted.internet import protocol, defer
+
+from twisted.mail import smtp
+
+class FileMessage(object):
+ implements(smtp.IMessage)
+
+ def __init__(self, fileObj):
+ self.fileObj = fileObj
+
+ def lineReceived(self, line):
+ self.fileObj.write(line + '\n')
+
+ def eomReceived(self):
+ self.fileObj.close()
+ return defer.succeed(None)
+
+ def connectionLost(self):
+ self.fileObj.close()
+ os.remove(self.fileObj.name)
+
+class TutorialDelivery(object):
+ implements(smtp.IMessageDelivery)
+ counter = 0
+
+ def validateTo(self, user):
+ fileName = 'tutorial-smtp.' + str(self.counter)
+ self.counter += 1
+ return lambda: FileMessage(file(fileName, 'w'))
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+ def receivedHeader(self, helo, origin, recipients):
+ return 'Received: Tutorially.'
+
+class TutorialDeliveryFactory(object):
+ implements(smtp.IMessageDeliveryFactory)
+
+ def getMessageDelivery(self):
+ return TutorialDelivery()
+
+class TutorialESMTPFactory(protocol.ServerFactory):
+ protocol = smtp.ESMTP
+
+ def buildProtocol(self, addr):
+ p = self.protocol()
+ p.deliveryFactory = TutorialDeliveryFactory()
+ p.factory = self
+ return p
+
+smtpServerFactory = TutorialESMTPFactory()
+
+smtpServerService = internet.TCPServer(2025, smtpServerFactory)
+smtpServerService.setServiceParent(application)
diff --git a/doc/names/examples/dns-service.py b/doc/names/examples/dns-service.py
new file mode 100755
index 0000000..396c91f
--- /dev/null
+++ b/doc/names/examples/dns-service.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Sample app to lookup SRV records in DNS.
+To run this script:
+$ python dns-service.py <service> <proto> <domain>
+where,
+service = the symbolic name of the desired service.
+proto = the transport protocol of the desired service; this is usually either TCP or UDP.
+domain = the domain name for which this record is valid.
+e.g.:
+$ python dns-service.py sip udp yahoo.com
+$ python dns-service.py xmpp-client tcp gmail.com
+"""
+
+from twisted.names import client
+from twisted.internet import reactor
+import sys
+
+def printAnswer((answers, auth, add)):
+ if not len(answers):
+ print 'No answers'
+ else:
+ print '\n'.join([str(x.payload) for x in answers])
+ reactor.stop()
+
+def printFailure(arg):
+ print "error: could not resolve:", arg
+ reactor.stop()
+
+try:
+ service, proto, domain = sys.argv[1:]
+except ValueError:
+ sys.stderr.write('%s: usage:\n' % sys.argv[0] +
+ ' %s SERVICE PROTO DOMAIN\n' % sys.argv[0])
+ sys.exit(1)
+
+resolver = client.Resolver('/etc/resolv.conf')
+d = resolver.lookupService('_%s._%s.%s' % (service, proto, domain), [1])
+d.addCallbacks(printAnswer, printFailure)
+
+reactor.run()
diff --git a/doc/names/examples/gethostbyname.py b/doc/names/examples/gethostbyname.py
new file mode 100755
index 0000000..e10fd43
--- /dev/null
+++ b/doc/names/examples/gethostbyname.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Returns the IP address for a given hostname.
+To run this script:
+$ python gethostbyname.py <hostname>
+e.g.:
+$ python gethostbyname.py www.google.com
+"""
+import sys
+from twisted.names import client
+from twisted.internet import reactor
+
+def gotResult(result):
+ print result
+ reactor.stop()
+
+def gotFailure(failure):
+ failure.printTraceback()
+ reactor.stop()
+
+d = client.getHostByName(sys.argv[1])
+d.addCallbacks(gotResult, gotFailure)
+
+reactor.run()
diff --git a/doc/names/examples/index.html b/doc/names/examples/index.html
new file mode 100644
index 0000000..892dd3e
--- /dev/null
+++ b/doc/names/examples/index.html
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Names code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Names code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">DNS (Twisted Names)</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>DNS (Twisted Names)<a name="auto0"/></h2>
+ <ul>
+ <li><a href="testdns.py" shape="rect">testdns.py</a> - Prints the results of an Address record lookup, Mail-Exchanger record lookup, and Nameserver record lookup for the given hostname for a given hostname.</li>
+ <li><a href="dns-service.py" shape="rect">dns-service.py</a> - Searches for SRV records in DNS.</li>
+ <li><a href="gethostbyname.py" shape="rect">gethostbyname.py</a> - Returns the IP address for a given hostname.</li>
+ </ul>
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/names/examples/testdns.py b/doc/names/examples/testdns.py
new file mode 100644
index 0000000..9fa3f03
--- /dev/null
+++ b/doc/names/examples/testdns.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Prints the results of an Address record lookup, Mail-Exchanger record lookup,
+and Nameserver record lookup for the given hostname for a given hostname.
+
+To run this script:
+$ python testdns.py <hostname>
+e.g.:
+$ python testdns.py www.google.com
+"""
+
+import sys
+from twisted.names import client
+from twisted.internet import reactor
+from twisted.names import dns
+
+r = client.Resolver('/etc/resolv.conf')
+
+def gotAddress(a):
+ print 'Addresses: ', ', '.join(map(str, a))
+
+def gotMails(a):
+ print 'Mail Exchangers: ', ', '.join(map(str, a))
+
+def gotNameservers(a):
+ print 'Nameservers: ', ', '.join(map(str, a))
+
+def gotError(f):
+ print 'gotError'
+ f.printTraceback()
+
+ from twisted.internet import reactor
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ import sys
+
+ r.lookupAddress(sys.argv[1]).addCallback(gotAddress).addErrback(gotError)
+ r.lookupMailExchange(sys.argv[1]).addCallback(gotMails).addErrback(gotError)
+ r.lookupNameservers(sys.argv[1]).addCallback(gotNameservers).addErrback(gotError)
+
+ reactor.callLater(4, reactor.stop)
+ reactor.run()
diff --git a/doc/names/howto/index.html b/doc/names/howto/index.html
new file mode 100644
index 0000000..0448e99
--- /dev/null
+++ b/doc/names/howto/index.html
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Names Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Names Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+ <li><a href="names.html" shape="rect">Names DNS library</a></li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/names/howto/listings/names/example-domain.com b/doc/names/howto/listings/names/example-domain.com
new file mode 100644
index 0000000..e720019
--- /dev/null
+++ b/doc/names/howto/listings/names/example-domain.com
@@ -0,0 +1,37 @@
+
+zone = [
+ SOA(
+ # For whom we are the authority
+ 'example-domain.com',
+
+ # This nameserver's name
+ mname = "ns1.example-domain.com",
+
+ # Mailbox of individual who handles this
+ rname = "root.example-domain.com",
+
+ # Unique serial identifying this SOA data
+ serial = 2003010601,
+
+ # Time interval before zone should be refreshed
+ refresh = "1H",
+
+ # Interval before failed refresh should be retried
+ retry = "1H",
+
+ # Upper limit on time interval before expiry
+ expire = "1H",
+
+ # Minimum TTL
+ minimum = "1H"
+ ),
+
+ A('example-domain.com', '127.0.0.1'),
+ NS('example-domain.com', 'ns1.example-domain.com'),
+
+ CNAME('www.example-domain.com', 'example-domain.com'),
+ CNAME('ftp.example-domain.com', 'example-domain.com'),
+
+ MX('example-domain.com', 0, 'mail.example-domain.com'),
+ A('mail.example-domain.com', '123.0.16.43')
+]
diff --git a/doc/names/howto/names.html b/doc/names/howto/names.html
new file mode 100644
index 0000000..fb8b59c
--- /dev/null
+++ b/doc/names/howto/names.html
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Creating and working with a names (DNS) server</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Creating and working with a names (DNS) server</h1>
+ <div class="toc"><ol><li><a href="#auto0">Creating a non-authoritative server</a></li><li><a href="#auto1">Creating an authoritative server</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<p>A Names server can be perform three basic operations:</p>
+
+<ul>
+<li>act as a recursive server, forwarding queries to other servers</li>
+<li>perform local caching of recursively discovered records</li>
+<li>act as the authoritative server for a domain</li>
+</ul>
+
+<h2>Creating a non-authoritative server<a name="auto0"/></h2>
+
+<p>
+The first two of these are easy, and you can create a server that performs them
+with the command <code class="shell">twistd -n dns --recursive --cache</code>.
+You may wish to run this as root since it will try to bind to UDP port 53. Try
+performing a lookup with it, <code class="shell">dig twistedmatrix.com
+@127.0.0.1</code>.
+</p>
+
+<h2>Creating an authoritative server<a name="auto1"/></h2>
+
+<p>To act as the authority for a domain, two things are necessary: the address
+of the machine on which the domain name server will run must be registered
+as a nameserver for the domain; and the domain name server must be
+configured to act as the authority. The first requirement is beyond the
+scope of this howto and will not be covered.
+</p>
+
+<p>To configure Names to act as the authority
+for <code>example-domain.com</code>, we first create a zone file for
+this domain.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+</p><span class="py-src-variable">zone</span> = [
+ <span class="py-src-variable">SOA</span>(
+ <span class="py-src-comment"># For whom we are the authority</span>
+ <span class="py-src-string">'example-domain.com'</span>,
+
+ <span class="py-src-comment"># This nameserver's name</span>
+ <span class="py-src-variable">mname</span> = <span class="py-src-string">&quot;ns1.example-domain.com&quot;</span>,
+
+ <span class="py-src-comment"># Mailbox of individual who handles this</span>
+ <span class="py-src-variable">rname</span> = <span class="py-src-string">&quot;root.example-domain.com&quot;</span>,
+
+ <span class="py-src-comment"># Unique serial identifying this SOA data</span>
+ <span class="py-src-variable">serial</span> = <span class="py-src-number">2003010601</span>,
+
+ <span class="py-src-comment"># Time interval before zone should be refreshed</span>
+ <span class="py-src-variable">refresh</span> = <span class="py-src-string">&quot;1H&quot;</span>,
+
+ <span class="py-src-comment"># Interval before failed refresh should be retried</span>
+ <span class="py-src-variable">retry</span> = <span class="py-src-string">&quot;1H&quot;</span>,
+
+ <span class="py-src-comment"># Upper limit on time interval before expiry</span>
+ <span class="py-src-variable">expire</span> = <span class="py-src-string">&quot;1H&quot;</span>,
+
+ <span class="py-src-comment"># Minimum TTL</span>
+ <span class="py-src-variable">minimum</span> = <span class="py-src-string">&quot;1H&quot;</span>
+ ),
+
+ <span class="py-src-variable">A</span>(<span class="py-src-string">'example-domain.com'</span>, <span class="py-src-string">'127.0.0.1'</span>),
+ <span class="py-src-variable">NS</span>(<span class="py-src-string">'example-domain.com'</span>, <span class="py-src-string">'ns1.example-domain.com'</span>),
+
+ <span class="py-src-variable">CNAME</span>(<span class="py-src-string">'www.example-domain.com'</span>, <span class="py-src-string">'example-domain.com'</span>),
+ <span class="py-src-variable">CNAME</span>(<span class="py-src-string">'ftp.example-domain.com'</span>, <span class="py-src-string">'example-domain.com'</span>),
+
+ <span class="py-src-variable">MX</span>(<span class="py-src-string">'example-domain.com'</span>, <span class="py-src-number">0</span>, <span class="py-src-string">'mail.example-domain.com'</span>),
+ <span class="py-src-variable">A</span>(<span class="py-src-string">'mail.example-domain.com'</span>, <span class="py-src-string">'123.0.16.43'</span>)
+]
+</pre><div class="caption">Zone file - <a href="listings/names/example-domain.com"><span class="filename">listings/names/example-domain.com</span></a></div></div>
+
+<p>Next, run the command <code class="shell">twistd -n dns --pyzone
+example-domain.com</code>. Now try querying the domain locally (again, with
+dig): <code class="shell">dig -t any example-domain.com @127.0.0.1</code>.
+</p>
+
+<p>Names can also read a traditional, BIND-syntax zone file. Specify these
+with the <code>--bindzone</code> parameter. The $GENERATE and $INCLUDE
+directives are not yet supported.
+</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/names/index.html b/doc/names/index.html
new file mode 100644
index 0000000..b4e545b
--- /dev/null
+++ b/doc/names/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Names Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Names Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Names to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Names</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/pair/examples/index.html b/doc/pair/examples/index.html
new file mode 100644
index 0000000..74b509e
--- /dev/null
+++ b/doc/pair/examples/index.html
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">Miscellaneous</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Miscellaneous<a name="auto0"/></h2>
+ <ul>
+ <li><a href="pairudp.py" shape="rect">pairudp.py</a> - UDP implemented with a TUN/TAP device</li>
+ </ul>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/pair/examples/pairudp.py b/doc/pair/examples/pairudp.py
new file mode 100644
index 0000000..b72155e
--- /dev/null
+++ b/doc/pair/examples/pairudp.py
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet import reactor, protocol
+from twisted.pair import ethernet, rawudp, ip
+from twisted.pair import tuntap
+
+class MyProto(protocol.DatagramProtocol):
+ def datagramReceived(self, *a, **kw):
+ print a, kw
+
+p_udp = rawudp.RawUDPProtocol()
+p_udp.addProto(42, MyProto())
+p_ip = ip.IPProtocol()
+p_ip.addProto(17, p_udp)
+p_eth = ethernet.EthernetProtocol()
+p_eth.addProto(0x800, p_ip)
+
+reactor.listenWith(tuntap.TuntapPort,
+ interface='tap0', proto=p_eth, reactor=reactor)
+reactor.run()
diff --git a/doc/pair/howto/index.html b/doc/pair/howto/index.html
new file mode 100644
index 0000000..77566bb
--- /dev/null
+++ b/doc/pair/howto/index.html
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Pair Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Pair Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+
+<li>Twisted Pair Documentation
+ <ul>
+ <li><a href="twisted-pair.html" shape="rect">Twisted Pair: Low-level networking</a></li>
+ </ul>
+</li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/pair/howto/twisted-pair.html b/doc/pair/howto/twisted-pair.html
new file mode 100644
index 0000000..535f22c
--- /dev/null
+++ b/doc/pair/howto/twisted-pair.html
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Pair: Low-level Networking</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Pair: Low-level Networking</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview of classes</a></li><ul><li><a href="#auto1">Transports</a></li><li><a href="#auto2">Protocols</a></li><li><a href="#auto3">Interfaces</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+<p>Twisted can do low-level networking, too.</p>
+
+<p>Here's an example that tries to show the relationships of different
+classes and how data could flow for receiving packets.</p>
+
+<pre xml:space="preserve">
+FileWrapper
+ |
+ v
+PcapProtocol TuntapPort
+ | |
+ +------------+
+ v
+EthernetProtocol
+ |
+ +------------+-----------+---...
+ v v v
+IPProtocol ARPProtocol IPv6Protocol
+ |
+ +-------------+----------------+---...
+ v v v
+RawUDPProtocol RawICMPProtocol RawTCPProtocol
+ |
+ v
+DatagramProtocol
+</pre>
+
+<p>Of course, for writing, the picture would look pretty much
+identical, except all arrows would be reversed.</p>
+
+<h2>Overview of classes<a name="auto0"/></h2>
+
+<p>TODO</p>
+
+<h3>Transports<a name="auto1"/></h3>
+
+<p>TODO</p>
+
+<ul>
+<li>TuntapPort: TODO</li>
+</ul>
+
+<h3>Protocols<a name="auto2"/></h3>
+
+<p>TODO</p>
+
+<ul>
+<li>EthernetProtocol: TODO</li>
+<li>IPProtocol: TODO</li>
+<li>RawUDPProtocol: TODO</li>
+</ul>
+
+<h3>Interfaces<a name="auto3"/></h3>
+
+<p>TODO</p>
+
+<ul>
+<li>IRawDatagramProtocol: TODO</li>
+<li>IRawPacketProtocol: TODO</li>
+</ul>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/pair/index.html b/doc/pair/index.html
new file mode 100644
index 0000000..c1f3d04
--- /dev/null
+++ b/doc/pair/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Pair Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Pair Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+ <li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+ Twisted Pair to develop your own applications</li>
+ <li><a href="examples/index.html" shape="rect">Code Examples</a>: short code examples using
+ Twisted Pair</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/stylesheet.css b/doc/stylesheet.css
new file mode 100644
index 0000000..3c5961e
--- /dev/null
+++ b/doc/stylesheet.css
@@ -0,0 +1,189 @@
+
+body
+{
+ margin-left: 2em;
+ margin-right: 2em;
+ border: 0px;
+ padding: 0px;
+ font-family: sans-serif;
+ }
+
+.done { color: #005500; background-color: #99ff99 }
+.notdone { color: #550000; background-color: #ff9999;}
+
+pre
+{
+ padding: 1em;
+ border: thin black solid;
+ line-height: 1.2em;
+}
+
+.boxed
+{
+ padding: 1em;
+ border: thin black solid;
+}
+
+.shell
+{
+ background-color: #ffffdd;
+}
+
+.python
+{
+ background-color: #dddddd;
+}
+
+.htmlsource
+{
+ background-color: #dddddd;
+}
+
+.py-prototype
+{
+ background-color: #ddddff;
+}
+
+
+.python-interpreter
+{
+ background-color: #ddddff;
+}
+
+.doit
+{
+ border: thin blue dashed ;
+ background-color: #0ef
+}
+
+.py-src-comment
+{
+ color: #1111CC
+}
+
+.py-src-keyword
+{
+ color: #3333CC;
+ font-weight: bold;
+ line-height: 1.0em
+}
+
+.py-src-parameter
+{
+ color: #000066;
+ font-weight: bold;
+ line-height: 1.0em
+}
+
+.py-src-identifier
+{
+ color: #CC0000
+}
+
+.py-src-string
+{
+
+ color: #115511
+}
+
+.py-src-endmarker
+{
+ display: block; /* IE hack; prevents following line from being sucked into the py-listing box. */
+}
+
+.py-linenumber
+{
+ background-color: #cdcdcd;
+ float: left;
+ margin-top: 0px;
+ width: 4.0em
+}
+
+.py-listing, .html-listing, .listing
+{
+ margin: 1ex;
+ border: thin solid black;
+ background-color: #eee;
+}
+
+.py-listing pre, .html-listing pre, .listing pre
+{
+ margin: 0px;
+ border: none;
+ border-bottom: thin solid black;
+}
+
+.py-listing .python
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.html-listing .htmlsource
+{
+ margin-top: 0;
+ margin-bottom: 0;
+ border: none;
+ border-bottom: thin solid black;
+ }
+
+.caption
+{
+ text-align: center;
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.filename
+{
+ font-style: italic;
+ }
+
+.manhole-output
+{
+ color: blue;
+}
+
+hr
+{
+ display: inline;
+ }
+
+ul
+{
+ padding: 0px;
+ margin: 0px;
+ margin-left: 1em;
+ padding-left: 1em;
+ border-left: 1em;
+ }
+
+li
+{
+ padding: 2px;
+ }
+
+dt
+{
+ font-weight: bold;
+ margin-left: 1ex;
+ }
+
+dd
+{
+ margin-bottom: 1em;
+ }
+
+div.note
+{
+ background-color: #FFFFCC;
+ margin-top: 1ex;
+ margin-left: 5%;
+ margin-right: 5%;
+ padding-top: 1ex;
+ padding-left: 5%;
+ padding-right: 5%;
+ border: thin black solid;
+}
diff --git a/doc/web/examples/advogato.py b/doc/web/examples/advogato.py
new file mode 100644
index 0000000..5b60c6d
--- /dev/null
+++ b/doc/web/examples/advogato.py
@@ -0,0 +1,46 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demonstrates how to logon to a remote server and post a diary.
+
+Usage:
+ $ python advogato.py <name> <diary entry file>
+"""
+
+import sys
+from getpass import getpass
+
+from twisted.web.xmlrpc import Proxy
+from twisted.internet import reactor
+
+class AddDiary:
+
+ def __init__(self, name, password):
+ self.name = name
+ self.password = password
+ self.proxy = Proxy('http://advogato.org/XMLRPC')
+
+ def __call__(self, filename):
+ self.data = open(filename).read()
+ d = self.proxy.callRemote('authenticate', self.name, self.password)
+ d.addCallbacks(self.login, self.noLogin)
+
+ def noLogin(self, reason):
+ print "could not login"
+ reactor.stop()
+
+ def login(self, cookie):
+ d = self.proxy.callRemote('diary.set', cookie, -1, self.data)
+ d.addCallbacks(self.setDiary, self.errorSetDiary)
+
+ def setDiary(self, response):
+ reactor.stop()
+
+ def errorSetDiary(self, error):
+ print "could not set diary", error
+ reactor.stop()
+
+diary = AddDiary(sys.argv[1], getpass())
+diary(sys.argv[2])
+reactor.run()
diff --git a/doc/web/examples/dlpage.py b/doc/web/examples/dlpage.py
new file mode 100644
index 0000000..7e476f9
--- /dev/null
+++ b/doc/web/examples/dlpage.py
@@ -0,0 +1,23 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demonstrates how to use downloadPage.
+
+Usage:
+ $ python dlpage.py <url>
+
+Don't forget the http:// when you type a URL!
+"""
+
+from twisted.internet import reactor
+from twisted.web.client import downloadPage
+from twisted.python.util import println
+import sys
+
+# The function downloads a page and saves it to a file, in this case, it saves
+# the page to "foo".
+downloadPage(sys.argv[1], "foo").addCallbacks(
+ lambda value:reactor.stop(),
+ lambda error:(println("an error occurred",error),reactor.stop()))
+reactor.run()
diff --git a/doc/web/examples/fortune.rpy.py b/doc/web/examples/fortune.rpy.py
new file mode 100644
index 0000000..3520744
--- /dev/null
+++ b/doc/web/examples/fortune.rpy.py
@@ -0,0 +1,49 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demostrates how to render the output of a system process to a
+twisted web server.
+
+In order to run this, you need to have fortune installed. Fortune is a simple
+game that displays a random message from a database of quotations. You will need
+to change the path of the fortune program if it's not in the "/usr/game"
+directory.
+
+To test the script, rename the file to fortune.rpy, and move it to any
+directory, let's say /var/www/html/
+
+Now, start your Twisted web server:
+ $ twistd -n web --path /var/www/html/
+
+And visit http://127.0.0.1:8080/fortune.rpy with a web browser.
+"""
+
+from twisted.web.resource import Resource
+from twisted.web import server
+from twisted.internet import utils
+from twisted.python import util
+
+class FortuneResource(Resource):
+ """
+ This resource will only repond to HEAD & GET requests.
+ """
+ # Link your fortune program to /usr/games or change the path.
+ fortune = "/usr/games/fortune"
+
+ def render_GET(self, request):
+ """
+ Get a fortune and serve it as the response to this request.
+
+ Use L{utils.getProcessOutput}, which spawns a process and returns a
+ Deferred which fires with its output.
+ """
+ request.write("<pre>\n")
+ deferred = utils.getProcessOutput(self.fortune)
+ deferred.addCallback(lambda s:
+ (request.write(s+"\n"), request.finish()))
+ deferred.addErrback(lambda s:
+ (request.write(str(s)), request.finish()))
+ return server.NOT_DONE_YET
+
+resource = FortuneResource()
diff --git a/doc/web/examples/getpage.py b/doc/web/examples/getpage.py
new file mode 100644
index 0000000..31a2a2c
--- /dev/null
+++ b/doc/web/examples/getpage.py
@@ -0,0 +1,20 @@
+# Copyright (c) Twisted Matrix Laboratories
+# See LICENSE for details.
+
+"""
+This program will retrieve and print the resource at the given URL.
+
+Usage:
+ $ python getpage.py <URL>
+"""
+
+import sys
+
+from twisted.internet import reactor
+from twisted.web.client import getPage
+from twisted.python.util import println
+
+getPage(sys.argv[1]).addCallbacks(
+ callback=lambda value:(println(value),reactor.stop()),
+ errback=lambda error:(println("an error occurred", error),reactor.stop()))
+reactor.run()
diff --git a/doc/web/examples/google.py b/doc/web/examples/google.py
new file mode 100644
index 0000000..22dc9c2
--- /dev/null
+++ b/doc/web/examples/google.py
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories
+# See LICENSE for details.
+
+"""
+This program will print out the URL corresponding to the first webpage given by
+a Google search.
+
+Usage:
+ $ python google.py <keyword(s)>
+"""
+
+import sys
+
+from twisted.web.google import checkGoogle
+from twisted.python.util import println
+from twisted.internet import reactor
+
+checkGoogle(sys.argv[1:]).addCallbacks(
+ lambda l:(println(l),reactor.stop()),
+ lambda e:(println('error:',e),reactor.stop()))
+reactor.run()
diff --git a/doc/web/examples/hello.rpy.py b/doc/web/examples/hello.rpy.py
new file mode 100644
index 0000000..1480dd4
--- /dev/null
+++ b/doc/web/examples/hello.rpy.py
@@ -0,0 +1,38 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is a resource file that renders a static web page.
+
+To test the script, rename the file to hello.rpy, and move it to any directory,
+let's say /var/www/html/.
+
+Now, start your Twisted web server:
+ $ twistd -n web --path /var/www/html/
+
+And visit http://127.0.0.1:8080/hello.rpy with a web browser.
+"""
+
+from twisted.web import static
+import time
+
+now = time.ctime()
+
+d = '''\
+<HTML><HEAD><TITLE>Hello Rpy</TITLE>
+
+<H1>Hello World, It is Now %(now)s</H1>
+
+<UL>
+''' % vars()
+
+for i in range(10):
+ d += "<LI>%(i)s" % vars()
+
+d += '''\
+</UL>
+
+</BODY></HTML>
+'''
+
+resource = static.Data(d, 'text/html')
diff --git a/doc/web/examples/httpclient.py b/doc/web/examples/httpclient.py
new file mode 100644
index 0000000..7d297f3
--- /dev/null
+++ b/doc/web/examples/httpclient.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demonstrates how to make a simple http client.
+
+Usage:
+ httpclient.py <url>
+
+Don't forget the http:// when you type the web address!
+"""
+
+import sys
+from pprint import pprint
+
+from twisted import version
+from twisted.python import log
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+from twisted.internet.protocol import Protocol
+from twisted.web.iweb import UNKNOWN_LENGTH
+from twisted.web.http_headers import Headers
+from twisted.web.client import Agent, ResponseDone
+
+
+class WriteToStdout(Protocol):
+ def connectionMade(self):
+ self.onConnLost = Deferred()
+
+ def dataReceived(self, data):
+ """
+ Print out the html page received.
+ """
+ print 'Got some:', data
+
+ def connectionLost(self, reason):
+ if not reason.check(ResponseDone):
+ reason.printTraceback()
+ else:
+ print 'Response done'
+ self.onConnLost.callback(None)
+
+
+def main(reactor, url):
+ """
+ We create a custom UserAgent and send a GET request to a web server.
+ """
+ userAgent = 'Twisted/%s (httpclient.py)' % (version.short(),)
+ agent = Agent(reactor)
+ d = agent.request(
+ 'GET', url, Headers({'user-agent': [userAgent]}))
+ def cbResponse(response):
+ """
+ Prints out the response returned by the web server.
+ """
+ pprint(vars(response))
+ proto = WriteToStdout()
+ if response.length is not UNKNOWN_LENGTH:
+ print 'The response body will consist of', response.length, 'bytes.'
+ else:
+ print 'The response body length is unknown.'
+ response.deliverBody(proto)
+ return proto.onConnLost
+ d.addCallback(cbResponse)
+ d.addErrback(log.err)
+ d.addBoth(lambda ign: reactor.callWhenRunning(reactor.stop))
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main(reactor, *sys.argv[1:])
diff --git a/doc/web/examples/index.html b/doc/web/examples/index.html
new file mode 100644
index 0000000..429ac9f
--- /dev/null
+++ b/doc/web/examples/index.html
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Web code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Web code examples</h1>
+ <div class="toc"><ol><li><a href="#auto0">twisted.web.client</a></li><li><a href="#auto1">XML-RPC</a></li><li><a href="#auto2">Virtual hosts and proxies</a></li><li><a href="#auto3">.rpys and ResourceTemplate</a></li><li><a href="#auto4">Miscellaneous</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>twisted.web.client<a name="auto0"/></h2>
+ <ul>
+ <li><a href="getpage.py" shape="rect">getpage.py</a> - use
+ <code>twisted.web.client.getPage</code> to download a web
+ page.</li>
+ <li><a href="dlpage.py" shape="rect">dlpage.py</a> - add callbacks to
+ <code>twisted.web.client.downloadPage</code> to display errors
+ that occur when downloading a web page</li>
+ </ul>
+
+ <h2>XML-RPC<a name="auto1"/></h2>
+ <ul>
+ <li><a href="xmlrpc.py" shape="rect">xmlrpc.py</a> XML-RPC server with
+ several methods, including echoing, faulting, returning
+ deferreds and failed deferreds</li>
+ <li><a href="xmlrpcclient.py" shape="rect">xmlrpcclient.py</a> - use
+ <code>twisted.web.xmlrpc.Proxy</code> to call remote XML-RPC
+ methods</li>
+ <li><a href="advogato.py" shape="rect">advogato.py</a> - use
+ <code>twisted.web.xmlrpc</code> to post a diary entry to
+ advogato.org; requires an advogato account</li>
+ </ul>
+
+ <h2>Virtual hosts and proxies<a name="auto2"/></h2>
+ <ul>
+ <li><a href="proxy.py" shape="rect">proxy.py</a> -
+ use <code>twisted.web.proxy.Proxy</code> to make the simplest
+ proxy</li>
+ <li><a href="logging-proxy.py" shape="rect">logging-proxy.py</a> - example of
+ subclassing the core classes of <code>twisted.web.proxy</code>
+ to log requests through a proxy</li>
+ <li><a href="reverse-proxy.py" shape="rect">reverse-proxy.py</a> - use
+ <code>twisted.web.proxy.ReverseProxyResource</code> to make
+ any HTTP request to the proxy port get applied to a specified
+ website</li>
+ <li><a href="rootscript.py" shape="rect">rootscript.py</a> - example use of
+ <code>twisted.web.vhost.NameVirtualHost</code></li>
+ <li><a href="web.py" shape="rect">web.py</a> - an example of both using the
+ <code>processors</code> attribute to set how certain file types
+ are treated and using
+ <code>twisted.web.vhost.VHostMonsterResource</code> to reverse
+ proxy</li>
+ </ul>
+
+ <h2>.rpys and ResourceTemplate<a name="auto3"/></h2>
+ <ul>
+ <li><a href="hello.rpy.py" shape="rect">hello.rpy.py</a> - use
+ <code>twisted.web.static</code> to create a static resource to
+ serve</li>
+ <li><a href="fortune.rpy.py" shape="rect">fortune.rpy.py</a> - create a
+ resource that returns the output of a process run on the
+ server</li>
+ <li><a href="lj.rpy.py" shape="rect">lj.rpy.py</a> - use
+ <code>twisted.web.microdom</code>,
+ <code>twisted.web.domhelpers</code>, and chained callbacks to
+ extract and display parts of a livejournal user's rss page</li>
+ <li><a href="report.rpy.py" shape="rect">report.rpy.py</a> - display
+ various properties of a resource, including path, host, and
+ port</li>
+ <li><a href="users.rpy.py" shape="rect">users.rpy.py</a> - use
+ <code>twisted.web.distrib</code> to publish user directories
+ as for a &quot;community web site&quot;</li>
+ <li><a href="simple.rtl" shape="rect">simple.rtl</a> - example use of
+ <code>twisted.web.resource.ResourceTemplate</code></li>
+ </ul>
+
+ <h2>Miscellaneous<a name="auto4"/></h2>
+ <ul>
+ <li><a href="webguard.py" shape="rect">webguard.py</a> - pairing
+ <code>twisted.web</code> with <code>twisted.cred</code> to
+ guard resources against unauthenticated users</li>
+ <li><a href="silly-web.py" shape="rect">silly-web.py</a> - bare-bones
+ distributed web setup with a master and slave using
+ <code>twisted.web.distrib</code> and
+ <code>twisted.spread.pb</code></li>
+ <li><a href="google.py" shape="rect">google.py</a> - use
+ <code>twisted.web.google</code> to get the I'm Feeling Lucky
+ page for a search term</li>
+ <li><a href="soap.py" shape="rect">soap.py</a> - use
+ <code>twisted.web.soap</code> to publish SOAP methods</li>
+ </ul>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/examples/lj.rpy.py b/doc/web/examples/lj.rpy.py
new file mode 100644
index 0000000..6bc078d
--- /dev/null
+++ b/doc/web/examples/lj.rpy.py
@@ -0,0 +1,48 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The example gets RSS feeds from LiveJournal users. It demonstrates how to use
+chained Deferred callbacks.
+
+To test the script, rename the file to lj.rpy, and move it to any directory,
+let's say /var/www/html/.
+
+Now, start your Twisted web server:
+ $ twistd -n web --path /var/www/html/
+
+And visit a URL like http://127.0.0.1:8080/lj.rpy?user=foo with a web browser,
+replacing "foo" with a valid LiveJournal username.
+"""
+
+from twisted.web import resource as resourcelib
+from twisted.web import client, microdom, domhelpers, server
+
+urlTemplate = 'http://%s.livejournal.com/data/rss'
+
+class LJSyndicatingResource(resourcelib.Resource):
+
+ def render_GET(self, request):
+ """
+ Get an xml feed from LiveJournal and construct a new HTML page using the
+ 'title' and 'link' parsed from the xml document.
+ """
+ url = urlTemplate % request.args['user'][0]
+ client.getPage(url, timeout=30).addCallback(
+ microdom.parseString).addCallback(
+ lambda t: domhelpers.findNodesNamed(t, 'item')).addCallback(
+ lambda itms: zip([domhelpers.findNodesNamed(x, 'title')[0]
+ for x in itms],
+ [domhelpers.findNodesNamed(x, 'link')[0]
+ for x in itms]
+ )).addCallback(
+ lambda itms: '<html><head></head><body><ul>%s</ul></body></html>' %
+ '\n'.join(
+ ['<li><a href="%s">%s</a></li>' % (
+ domhelpers.getNodeText(link), domhelpers.getNodeText(title))
+ for (title, link) in itms])
+ ).addCallback(lambda s: (request.write(s),request.finish())).addErrback(
+ lambda e: (request.write('Error: %s' % e),request.finish()))
+ return server.NOT_DONE_YET
+
+resource = LJSyndicatingResource()
diff --git a/doc/web/examples/logging-proxy.py b/doc/web/examples/logging-proxy.py
new file mode 100644
index 0000000..b75b1ab
--- /dev/null
+++ b/doc/web/examples/logging-proxy.py
@@ -0,0 +1,45 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An example of a proxy which logs all requests processed through it.
+
+Usage:
+ $ python logging-proxy.py
+
+Then configure your web browser to use localhost:8080 as a proxy, and visit a
+URL (This is not a SOCKS proxy). When browsing in this configuration, this
+example will proxy connections from the browser to the server indicated by URLs
+which are visited. The client IP and the request hostname will be logged for
+each request.
+
+HTTP is supported. HTTPS is not supported.
+
+See also proxy.py for a simpler proxy example.
+"""
+
+from twisted.internet import reactor
+from twisted.web import proxy, http
+
+class LoggingProxyRequest(proxy.ProxyRequest):
+ def process(self):
+ """
+ It's normal to see a blank HTTPS page. As the proxy only works
+ with the HTTP protocol.
+ """
+ print "Request from %s for %s" % (
+ self.getClientIP(), self.getAllHeaders()['host'])
+ try:
+ proxy.ProxyRequest.process(self)
+ except KeyError:
+ print "HTTPS is not supported at the moment!"
+
+class LoggingProxy(proxy.Proxy):
+ requestFactory = LoggingProxyRequest
+
+class LoggingProxyFactory(http.HTTPFactory):
+ def buildProtocol(self, addr):
+ return LoggingProxy()
+
+reactor.listenTCP(8080, LoggingProxyFactory())
+reactor.run()
diff --git a/doc/web/examples/proxy.py b/doc/web/examples/proxy.py
new file mode 100644
index 0000000..6a1ae8a
--- /dev/null
+++ b/doc/web/examples/proxy.py
@@ -0,0 +1,26 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demonstrates a very simple HTTP proxy.
+
+Usage:
+ $ python proxy.py
+
+Then configure your web browser to use localhost:8080 as a proxy, and visit a
+URL (This is not a SOCKS proxy). When browsing in this configuration, this
+example will proxy connections from the browser to the server indicated by URLs
+which are visited.
+
+See also logging-proxy.py for a proxy with additional features.
+"""
+
+from twisted.web import proxy, http
+from twisted.internet import reactor
+
+class ProxyFactory(http.HTTPFactory):
+ def buildProtocol(self, addr):
+ return proxy.Proxy()
+
+reactor.listenTCP(8080, ProxyFactory())
+reactor.run()
diff --git a/doc/web/examples/report.rpy.py b/doc/web/examples/report.rpy.py
new file mode 100644
index 0000000..a90a544
--- /dev/null
+++ b/doc/web/examples/report.rpy.py
@@ -0,0 +1,44 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demostrates how to get host information from a request object.
+
+To test the script, rename the file to report.rpy, and move it to any directory,
+let's say /var/www/html/.
+
+Now, start your Twist web server:
+ $ twistd -n web --path /var/www/html/
+
+Then visit http://127.0.0.1:8080/report.rpy in your web browser.
+"""
+
+from twisted.web.resource import Resource
+
+
+class ReportResource(Resource):
+
+ def render_GET(self, request):
+ path = request.path
+ host = request.getHost().host
+ port = request.getHost().port
+ url = request.prePathURL()
+ uri = request.uri
+ secure = (request.isSecure() and "securely") or "insecurely"
+ return ("""\
+<HTML>
+ <HEAD><TITLE>Welcome To Twisted Python Reporting</title></head>
+
+ <BODY><H1>Welcome To Twisted Python Reporting</H1>
+ <UL>
+ <LI>The path to me is %(path)s
+ <LI>The host I'm on is %(host)s
+ <LI>The port I'm on is %(port)s
+ <LI>I was accessed %(secure)s
+ <LI>A URL to me is %(url)s
+ <LI>My URI to me is %(uri)s
+ </UL>
+ </body>
+</html>""" % vars())
+
+resource = ReportResource()
diff --git a/doc/web/examples/reverse-proxy.py b/doc/web/examples/reverse-proxy.py
new file mode 100644
index 0000000..4ea57d7
--- /dev/null
+++ b/doc/web/examples/reverse-proxy.py
@@ -0,0 +1,18 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demonstrates how to run a reverse proxy.
+
+Run this example with:
+ $ python reverse-proxy.py
+
+Then visit http://localhost:8080/ in your web browser.
+"""
+
+from twisted.internet import reactor
+from twisted.web import proxy, server
+
+site = server.Site(proxy.ReverseProxyResource('www.yahoo.com', 80, ''))
+reactor.listenTCP(8080, site)
+reactor.run()
diff --git a/doc/web/examples/rootscript.py b/doc/web/examples/rootscript.py
new file mode 100644
index 0000000..7b228aa
--- /dev/null
+++ b/doc/web/examples/rootscript.py
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is a Twisted Web Server with Named-Based Virtual Host Support.
+
+Usage:
+ $ sudo twistd -ny rootscript.py
+
+Note: You need to edit your hosts file for this example
+to work. Need to add the following entry:
+
+ 127.0.0.1 example.com
+
+Then visit http://example.com/ with a web browser and compare the results to
+visiting http://localhost/.
+"""
+
+from twisted.web import vhost, static, script, server
+from twisted.application import internet, service
+
+default = static.Data('text/html', '')
+# Setting up vhost resource.
+default.putChild('vhost', vhost.VHostMonsterResource())
+resource = vhost.NameVirtualHost()
+resource.default = default
+# Here we use /var/www/html/ as our root diretory for the web server, you can
+# change it to whatever directory you want.
+root = static.File("/var/www/html/")
+root.processors = {'.rpy': script.ResourceScript}
+# addHost binds domain name example.com to our root resource.
+resource.addHost("example.com", root)
+
+# Setup Twisted Application.
+site = server.Site(resource)
+application = service.Application('vhost')
+sc = service.IServiceCollection(application)
+# Only the processes owned by the root user can listen @ port 80, change the
+# port number here if you don't want to run it as root.
+i = internet.TCPServer(80, site)
+i.setServiceParent(sc)
diff --git a/doc/web/examples/silly-web.py b/doc/web/examples/silly-web.py
new file mode 100644
index 0000000..758a532
--- /dev/null
+++ b/doc/web/examples/silly-web.py
@@ -0,0 +1,28 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This shows an example of a bare-bones distributed web set up. The "master" and
+"slave" parts will usually be in different files -- they are here together only
+for brevity of illustration. In normal usage they would each run in a separate
+process.
+
+Usage:
+ $ python silly-web.py
+
+Then visit http://localhost:19988/.
+"""
+
+from twisted.internet import reactor, protocol
+from twisted.web import server, distrib, static
+from twisted.spread import pb
+
+# The "master" server
+site = server.Site(distrib.ResourceSubscription('unix', '.rp'))
+reactor.listenTCP(19988, site)
+
+# The "slave" server
+fact = pb.PBServerFactory(distrib.ResourcePublisher(server.Site(static.File('static'))))
+
+reactor.listenUNIX('./.rp', fact)
+reactor.run()
diff --git a/doc/web/examples/simple.rtl b/doc/web/examples/simple.rtl
new file mode 100644
index 0000000..fbc08ff
--- /dev/null
+++ b/doc/web/examples/simple.rtl
@@ -0,0 +1,32 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example demostrates how to render a page using a third-party template
+system.
+
+Usage:
+ $ twistd -n web --process=.rtl=twisted.web.script.ResourceTemplate --path /path/to/examples/
+
+And make sure Quixote is installed.
+"""
+
+from twisted.web.resource import Resource
+
+
+class ExampleResource(Resource):
+
+ def render_GET(self, request):
+ """\
+<HTML>
+ <HEAD><TITLE> Welcome To Twisted Python </title></head>
+
+ <BODY><ul>"""
+ for i in range(10):
+ '<LI>';i
+ """</ul></body>
+</html>"""
+
+
+resource = ExampleResource()
+
diff --git a/doc/web/examples/soap.py b/doc/web/examples/soap.py
new file mode 100644
index 0000000..e4edec1
--- /dev/null
+++ b/doc/web/examples/soap.py
@@ -0,0 +1,44 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is an example of a simple SOAP server.
+
+Usage:
+ $ python soap.py
+
+An example session (assuming the server is running):
+
+ >>> import SOAPpy
+ >>> p = SOAPpy.SOAPProxy('http://localhost:8080/')
+ >>> p.add(a=1)
+ 1
+ >>> p.add(a=1, b=3)
+ 4
+ >>> p.echo("Hello World")
+ 'Hello World'
+
+"""
+
+from twisted.web import soap, server
+from twisted.internet import reactor, defer
+
+
+class Example(soap.SOAPPublisher):
+ """
+ It publishs two methods, 'add' and 'echo'.
+ """
+
+ def soap_echo(self, x):
+ return x
+
+ def soap_add(self, a=0, b=0):
+ return a + b
+ soap_add.useKeywords = 1
+
+ def soap_deferred(self):
+ return defer.succeed(2)
+
+
+reactor.listenTCP(8080, server.Site(Example()))
+reactor.run()
diff --git a/doc/web/examples/users.rpy.py b/doc/web/examples/users.rpy.py
new file mode 100644
index 0000000..046fcd1
--- /dev/null
+++ b/doc/web/examples/users.rpy.py
@@ -0,0 +1,24 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An example showing how to use a distributed web server's user directory support.
+
+With this, you can have an instant "community web site",
+letting your shell users publish data in secure ways.
+
+Just put this script anywhere, and /path/to/this/script/<user>/ will publish a
+user's ~/public_html, and a .../<user>.twistd/ will attempt to contact a user's
+personal web server.
+
+For example, if you put this at /var/www/users.rpy and run a server like:
+ $ twistd -n web --allow-ignore-ext --path /var/www
+
+Then http://example.com/users/<name>/ and http://example.com/users/<name>.twistd
+will work similarily to how they work on twistedmatrix.com.
+"""
+
+from twisted.web import distrib
+
+resource = distrib.UserDirectory()
+registry.setComponent(distrib.UserDirectory, resource)
diff --git a/doc/web/examples/web.py b/doc/web/examples/web.py
new file mode 100644
index 0000000..1eaccf0
--- /dev/null
+++ b/doc/web/examples/web.py
@@ -0,0 +1,29 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This demonstrates a web server which can run behind a name-based virtual hosting
+reverse proxy. It decodes modified URLs like:
+
+ host:port/vhost/http/external-host:port/
+
+and dispatches the request as if it had been received on the given protocol,
+external host, and port.
+
+Usage:
+ python web.py
+"""
+
+from twisted.internet import reactor
+from twisted.web import static, server, vhost, twcgi, script
+
+root = static.File("static")
+root.processors = {
+ '.cgi': twcgi.CGIScript,
+ '.epy': script.PythonScript,
+ '.rpy': script.ResourceScript,
+}
+root.putChild('vhost', vhost.VHostMonsterResource())
+site = server.Site(root)
+reactor.listenTCP(1999, site)
+reactor.run()
diff --git a/doc/web/examples/webguard.py b/doc/web/examples/webguard.py
new file mode 100644
index 0000000..02b0557
--- /dev/null
+++ b/doc/web/examples/webguard.py
@@ -0,0 +1,64 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example shows how to make simple web authentication.
+
+To run the example:
+ $ python webguard.py
+
+When you visit http://127.0.0.1:8889/, the page will ask for an username &
+password. See the code in main() to get the correct username & password!
+"""
+
+import sys
+
+from zope.interface import implements
+
+from twisted.python import log
+from twisted.internet import reactor
+from twisted.web import server, resource, guard
+from twisted.cred.portal import IRealm, Portal
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+
+
+class GuardedResource(resource.Resource):
+ """
+ A resource which is protected by guard and requires authentication in order
+ to access.
+ """
+ def getChild(self, path, request):
+ return self
+
+
+ def render(self, request):
+ return "Authorized!"
+
+
+
+class SimpleRealm(object):
+ """
+ A realm which gives out L{GuardedResource} instances for authenticated
+ users.
+ """
+ implements(IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if resource.IResource in interfaces:
+ return resource.IResource, GuardedResource(), lambda: None
+ raise NotImplementedError()
+
+
+
+def main():
+ log.startLogging(sys.stdout)
+ checkers = [InMemoryUsernamePasswordDatabaseDontUse(joe='blow')]
+ wrapper = guard.HTTPAuthSessionWrapper(
+ Portal(SimpleRealm(), checkers),
+ [guard.DigestCredentialFactory('md5', 'example.com')])
+ reactor.listenTCP(8889, server.Site(
+ resource = wrapper))
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/web/examples/xmlrpc.py b/doc/web/examples/xmlrpc.py
new file mode 100644
index 0000000..9fde54c
--- /dev/null
+++ b/doc/web/examples/xmlrpc.py
@@ -0,0 +1,80 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An example of an XML-RPC server in Twisted.
+
+Usage:
+ $ python xmlrpc.py
+
+An example session (assuming the server is running):
+
+ >>> import xmlrpclib
+ >>> s = xmlrpclib.Server('http://localhost:7080/')
+ >>> s.echo("lala")
+ ['lala']
+ >>> s.echo("lala", 1)
+ ['lala', 1]
+ >>> s.echo("lala", 4)
+ ['lala', 4]
+ >>> s.echo("lala", 4, 3.4)
+ ['lala', 4, 3.3999999999999999]
+ >>> s.echo("lala", 4, [1, 2])
+ ['lala', 4, [1, 2]]
+
+"""
+
+from twisted.web import xmlrpc
+from twisted.internet import defer
+import xmlrpclib
+
+
+class Echoer(xmlrpc.XMLRPC):
+ """
+ An example object to be published.
+
+ Has five methods accessable by XML-RPC, 'echo', 'hello', 'defer',
+ 'defer_fail' and 'fail.
+ """
+
+ def xmlrpc_echo(self, *args):
+ """
+ Return all passed args.
+ """
+ return args
+
+ def xmlrpc_hello(self):
+ """
+ Return 'hello, world'.
+ """
+ return 'hello, world!'
+
+ def xmlrpc_defer(self):
+ """
+ Show how xmlrpc methods can return Deferred.
+ """
+ return defer.succeed("hello")
+
+ def xmlrpc_defer_fail(self):
+ """
+ Show how xmlrpc methods can return failed Deferred.
+ """
+ return defer.fail(12)
+
+ def xmlrpc_fail(self):
+ """
+ Show how we can return a failure code.
+ """
+ return xmlrpclib.Fault(7, "Out of cheese.")
+
+
+def main():
+ from twisted.internet import reactor
+ from twisted.web import server
+ r = Echoer()
+ reactor.listenTCP(7080, server.Site(r))
+ reactor.run()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/web/examples/xmlrpcclient.py b/doc/web/examples/xmlrpcclient.py
new file mode 100644
index 0000000..74c6eb0
--- /dev/null
+++ b/doc/web/examples/xmlrpcclient.py
@@ -0,0 +1,31 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This example makes remote XML-RPC calls.
+
+Usage:
+ $ python xmlrpcclient.py
+
+The example will make an XML-RPC request to advogato.org and display the result.
+"""
+
+from twisted.web.xmlrpc import Proxy
+from twisted.internet import reactor
+
+def printValue(value):
+ print repr(value)
+ reactor.stop()
+
+def printError(error):
+ print 'error', error
+ reactor.stop()
+
+def capitalize(value):
+ print repr(value)
+ proxy.callRemote('test.capitalize', 'moshe zadka').addCallbacks(printValue, printError)
+
+proxy = Proxy('http://advogato.org/XMLRPC')
+# The callRemote method accepts a method name and an argument list.
+proxy.callRemote('test.sumprod', 2, 5).addCallbacks(capitalize, printError)
+reactor.run()
diff --git a/doc/web/howto/client.html b/doc/web/howto/client.html
new file mode 100644
index 0000000..a344dbc
--- /dev/null
+++ b/doc/web/howto/client.html
@@ -0,0 +1,1088 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation:
+ Using the Twisted Web Client
+ </title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">
+ Using the Twisted Web Client
+ </h1>
+ <div class="toc"><ol><li><a href="#auto0">
+ Overview
+ </a></li><ul><li><a href="#auto1">
+ Prerequisites
+ </a></li></ul><li><a href="#auto2">
+ The Agent
+ </a></li><ul><li><a href="#auto3">Issuing Requests</a></li><li><a href="#auto4">
+ Receiving Responses
+ </a></li><li><a href="#auto5">HTTP over SSL</a></li><li><a href="#auto6">HTTP Persistent Connection</a></li><li><a href="#auto7">Multiple Connections to the Same Server</a></li><li><a href="#auto8">Following redirects</a></li><li><a href="#auto9">Using a HTTP proxy</a></li><li><a href="#auto10">Handling HTTP cookies</a></li><li><a href="#auto11">Automatic Content Encoding Negotiation</a></li></ul><li><a href="#auto12">
+ Conclusion
+ </a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>
+ Overview
+ <a name="auto0"/></h2>
+
+ <p>
+ This document describes how to use the HTTP client included in Twisted
+ Web. After reading it, you should be able to make HTTP and HTTPS
+ requests using Twisted Web. You will be able to specify the request
+ method, headers, and body and you will be able to retrieve the response
+ code, headers, and body.
+ </p>
+
+ <p>
+ A number of higher-level features are also explained, including proxying,
+ automatic content encoding negotiation, and cookie handling.
+ </p>
+
+ <h3>
+ Prerequisites
+ <a name="auto1"/></h3>
+
+ <p>
+
+ This document assumes that you are familiar with <a href="../../core/howto/defer.html" shape="rect">Deferreds and Failures</a>, and <a href="../../core/howto/producers.html" shape="rect">producers and consumers</a>.
+ It also assumes you are familiar with the basic concepts of HTTP, such
+ as requests and responses, methods, headers, and message bodies. The
+ HTTPS section of this document also assumes you are somewhat familiar with
+ SSL and have read about <a href="../../core/howto/ssl.html" shape="rect">using SSL in
+ Twisted</a>.
+ </p>
+
+ <h2>
+ The Agent
+ <a name="auto2"/></h2>
+
+ <h3>Issuing Requests<a name="auto3"/></h3>
+
+ <p>
+ The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.Agent.html" title="twisted.web.client.Agent">twisted.web.client.Agent</a></code> class is the entry
+ point into the client API. Requests are issued using the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.Agent.request.html" title="twisted.web.client.Agent.request">request</a></code> method, which
+ takes as parameters a request method, a request URI, the request headers,
+ and an object which can produce the request body (if there is to be one).
+ The agent is responsible for connection setup. Because of this, it
+ requires a reactor as an argument to its initializer. An example of
+ creating an agent and issuing a request using it might look like this:
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">http_headers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Headers</span>
+
+<span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>)
+
+<span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(
+ <span class="py-src-string">'GET'</span>,
+ <span class="py-src-string">'http://example.com/'</span>,
+ <span class="py-src-variable">Headers</span>({<span class="py-src-string">'User-Agent'</span>: [<span class="py-src-string">'Twisted Web Client Example'</span>]}),
+ <span class="py-src-variable">None</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbResponse</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response received'</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbResponse</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbShutdown</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addBoth</span>(<span class="py-src-variable">cbShutdown</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">
+ Issue a request with an Agent
+ - <a href="listings/client/request.py"><span class="filename">listings/client/request.py</span></a></div></div>
+
+ <p>
+ As may be obvious, this issues a new <em>GET</em> request for <em>/</em>
+ to the web server on <code>example.com</code>. <code>Agent</code> is
+ responsible for resolving the hostname into an IP address and connecting
+ to it on port 80 (for <em>HTTP</em> URIs), port 443 (for <em>HTTPS</em>
+ URIs), or on the port number specified in the URI itself. It is also
+ responsible for cleaning up the connection afterwards. This code sends
+ a request which includes one custom header, <em>User-Agent</em>. The
+ last argument passed to <code>Agent.request</code> is <code>None</code>,
+ though, so the request has no body.
+ </p>
+
+ <p>
+ Sending a request which does include a body requires passing an object
+ providing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.iweb.IBodyProducer.html" title="twisted.web.iweb.IBodyProducer">twisted.web.iweb.IBodyProducer</a></code>
+ to <code>Agent.request</code>. This interface extends the more general
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPushProducer.html" title="twisted.internet.interfaces.IPushProducer">IPushProducer</a></code>
+ by adding a new <code>length</code> attribute and adding several
+ constraints to the way the producer and consumer interact.
+ </p>
+
+ <ul>
+ <li>
+ The length attribute must be a non-negative integer or the constant
+ <code>twisted.web.iweb.UNKNOWN_LENGTH</code>. If the length is known,
+ it will be used to specify the value for the
+ <em>Content-Length</em> header in the request. If the length is
+ unknown the attribute should be set to <code>UNKNOWN_LENGTH</code>.
+ Since more servers support <em>Content-Length</em>, if a length can be
+ provided it should be.
+ </li>
+
+ <li>
+ An additional method is required on <code>IBodyProducer</code>
+ implementations: <code>startProducing</code>. This method is used to
+ associate a consumer with the producer. It should return a
+ <code>Deferred</code> which fires when all data has been produced.
+ </li>
+
+ <li>
+ <code>IBodyProducer</code> implementations should never call the
+ consumer's <code>unregisterProducer</code> method. Instead, when it
+ has produced all of the data it is going to produce, it should only
+ fire the <code>Deferred</code> returned by <code>startProducing</code>.
+ </li>
+ </ul>
+
+ <p>
+ For additional details about the requirements of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.iweb.IBodyProducer.html" title="twisted.web.iweb.IBodyProducer">IBodyProducer</a></code> implementations, see
+ the API documentation.
+ </p>
+
+ <p>
+ Here's a simple <code>IBodyProducer</code> implementation which
+ writes an in-memory string to the consumer:
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">succeed</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">iweb</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IBodyProducer</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">StringProducer</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IBodyProducer</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">body</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">body</span> = <span class="py-src-variable">body</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">length</span> = <span class="py-src-variable">len</span>(<span class="py-src-variable">body</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">startProducing</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">consumer</span>):
+ <span class="py-src-variable">consumer</span>.<span class="py-src-variable">write</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">body</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">succeed</span>(<span class="py-src-variable">None</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">pauseProducing</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">pass</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">stopProducing</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">pass</span>
+</pre><div class="caption">
+ A string-based body producer.
+ - <a href="listings/client/stringprod.py"><span class="filename">listings/client/stringprod.py</span></a></div></div>
+
+ <p>
+ This producer can be used to issue a request with a body:
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">http_headers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Headers</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">stringprod</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">StringProducer</span>
+
+<span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>)
+<span class="py-src-variable">body</span> = <span class="py-src-variable">StringProducer</span>(<span class="py-src-string">&quot;hello, world&quot;</span>)
+<span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(
+ <span class="py-src-string">'GET'</span>,
+ <span class="py-src-string">'http://example.com/'</span>,
+ <span class="py-src-variable">Headers</span>({<span class="py-src-string">'User-Agent'</span>: [<span class="py-src-string">'Twisted Web Client Example'</span>],
+ <span class="py-src-string">'Content-Type'</span>: [<span class="py-src-string">'text/x-greeting'</span>]}),
+ <span class="py-src-variable">body</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbResponse</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response received'</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbResponse</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbShutdown</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addBoth</span>(<span class="py-src-variable">cbShutdown</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">
+ Issue a request with a body.
+ - <a href="listings/client/sendbody.py"><span class="filename">listings/client/sendbody.py</span></a></div></div>
+
+ <p>
+ If you want to upload a file or you just have some data in a string, you
+ don't have to copy <code>StringProducer</code> though. Instead, you can
+ use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.FileBodyProducer.html" title="twisted.web.client.FileBodyProducer">FileBodyProducer</a></code>.
+ This <code>IBodyProducer</code> implementation works with any file-like
+ object (so use it with a <code>StringIO</code> if your upload data is
+ already in memory as a string); the idea is the same
+ as <code>StringProducer</code> from the previous example, but with a
+ little extra code to only send data as fast as the server will take it.
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">StringIO</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">StringIO</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">http_headers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Headers</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FileBodyProducer</span>
+
+<span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>)
+<span class="py-src-variable">body</span> = <span class="py-src-variable">FileBodyProducer</span>(<span class="py-src-variable">StringIO</span>(<span class="py-src-string">&quot;hello, world&quot;</span>))
+<span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(
+ <span class="py-src-string">'GET'</span>,
+ <span class="py-src-string">'http://example.com/'</span>,
+ <span class="py-src-variable">Headers</span>({<span class="py-src-string">'User-Agent'</span>: [<span class="py-src-string">'Twisted Web Client Example'</span>],
+ <span class="py-src-string">'Content-Type'</span>: [<span class="py-src-string">'text/x-greeting'</span>]}),
+ <span class="py-src-variable">body</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbResponse</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response received'</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbResponse</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbShutdown</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addBoth</span>(<span class="py-src-variable">cbShutdown</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">
+ Another way to issue a request with a body.
+ - <a href="listings/client/filesendbody.py"><span class="filename">listings/client/filesendbody.py</span></a></div></div>
+
+ <p>
+ <code>FileBodyProducer</code> closes the file when it no longer needs it.
+ </p>
+
+ <h3>
+ Receiving Responses
+ <a name="auto4"/></h3>
+
+ <p>
+ So far, the examples have demonstrated how to issue a request. However,
+ they have ignored the response, except for showing that it is a
+ <code>Deferred</code> which seems to fire when the response has been
+ received. Next we'll cover what that response is and how to interpret
+ it.
+ </p>
+
+ <p>
+ <code>Agent.request</code>, as with most <code>Deferred</code>-returning
+ APIs, can return a <code>Deferred</code> which fires with a
+ <code>Failure</code>. If the request fails somehow, this will be
+ reflected with a failure. This may be due to a problem looking up the
+ host IP address, or it may be because the HTTP server is not accepting
+ connections, or it may be because of a problem parsing the response, or
+ any other problem which arises which prevents the response from being
+ received. It does <em>not</em> include responses with an error status.
+ </p>
+
+ <p>
+ If the request succeeds, though, the <code>Deferred</code> will fire with
+ a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.Response.html" title="twisted.web.client.Response">Response</a></code>. This
+ happens as soon as all the response headers have been received. It
+ happens before any of the response body, if there is one, is processed.
+ The <code>Response</code> object has several attributes giving the
+ response information: its code, version, phrase, and headers, as well as
+ the length of the body to expect. The <code>Response</code> object also
+ has a method which makes the response body available: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.Response.deliverBody.html" title="twisted.web.client.Response.deliverBody">deliverBody</a></code>. Using the
+ attributes of the response object and this method, here's an example which
+ displays part of the response to a request:
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">pprint</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">pformat</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Deferred</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">http_headers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Headers</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">BeginningPrinter</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">finished</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">finished</span> = <span class="py-src-variable">finished</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span> = <span class="py-src-number">1024</span> * <span class="py-src-number">10</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">bytes</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span>:
+ <span class="py-src-variable">display</span> = <span class="py-src-variable">bytes</span>[:<span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span>]
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Some data received:'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">display</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span> -= <span class="py-src-variable">len</span>(<span class="py-src-variable">display</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Finished receiving body:'</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">getErrorMessage</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">finished</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">None</span>)
+
+<span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>)
+<span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(
+ <span class="py-src-string">'GET'</span>,
+ <span class="py-src-string">'http://example.com/'</span>,
+ <span class="py-src-variable">Headers</span>({<span class="py-src-string">'User-Agent'</span>: [<span class="py-src-string">'Twisted Web Client Example'</span>]}),
+ <span class="py-src-variable">None</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbRequest</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response version:'</span>, <span class="py-src-variable">response</span>.<span class="py-src-variable">version</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response code:'</span>, <span class="py-src-variable">response</span>.<span class="py-src-variable">code</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response phrase:'</span>, <span class="py-src-variable">response</span>.<span class="py-src-variable">phrase</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response headers:'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">pformat</span>(<span class="py-src-variable">list</span>(<span class="py-src-variable">response</span>.<span class="py-src-variable">headers</span>.<span class="py-src-variable">getAllRawHeaders</span>()))
+ <span class="py-src-variable">finished</span> = <span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">response</span>.<span class="py-src-variable">deliverBody</span>(<span class="py-src-variable">BeginningPrinter</span>(<span class="py-src-variable">finished</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">finished</span>
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbRequest</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbShutdown</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addBoth</span>(<span class="py-src-variable">cbShutdown</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">
+ Inspect the response.
+ - <a href="listings/client/response.py"><span class="filename">listings/client/response.py</span></a></div></div>
+
+ <p>
+ The <code>BeginningPrinter</code> protocol in this example is passed to
+ <code>Response.deliverBody</code> and the response body is then delivered
+ to its <code>dataReceived</code> method as it arrives. When the body has
+ been completely delivered, the protocol's <code>connectionLost</code>
+ method is called. It is important to inspect the <code>Failure</code>
+ passed to <code>connectionLost</code>. If the response body has been
+ completely received, the failure will wrap a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.ResponseDone.html" title="twisted.web.client.ResponseDone">twisted.web.client.ResponseDone</a></code> exception. This
+ indicates that it is <em>known</em> that all data has been received. It
+ is also possible for the failure to wrap a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.PotentialDataLoss.html" title="twisted.web.http.PotentialDataLoss">twisted.web.http.PotentialDataLoss</a></code> exception: this
+ indicates that the server framed the response such that there is no way
+ to know when the entire response body has been received. Only
+ HTTP/1.0 servers should behave this way. Finally, it is possible for
+ the exception to be of another type, indicating guaranteed data loss for
+ some reason (a lost connection, a memory error, etc).
+ </p>
+
+ <p>
+ Just as protocols associated with a TCP connection are given a transport,
+ so will be a protocol passed to <code>deliverBody</code>. Since it makes
+ no sense to write more data to the connection at this stage of the
+ request, though, the transport <em>only</em> provides <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IPushProducer.html" title="twisted.internet.interfaces.IPushProducer">IPushProducer</a></code>. This allows the
+ protocol to control the flow of the response data: a call to the
+ transport's <code>pauseProducing</code> method will pause delivery; a
+ later call to <code>resumeProducing</code> will resume it. If it is
+ decided that the rest of the response body is not desired,
+ <code>stopProducing</code> can be used to stop delivery permanently;
+ after this, the protocol's <code>connectionLost</code> method will be
+ called.
+ </p>
+
+ <p>
+ An important thing to keep in mind is that the body will only be read
+ from the connection after <code>Response.deliverBody</code> is called.
+ This also means that the connection will remain open until this is done
+ (and the body read). So, in general, any response with a body
+ <em>must</em> have that body read using <code>deliverBody</code>. If the
+ application is not interested in the body, it should issue a
+ <em>HEAD</em> request or use a protocol which immediately calls
+ <code>stopProducing</code> on its transport.
+ </p>
+
+ <h3>HTTP over SSL<a name="auto5"/></h3>
+
+ <p>
+ Everything you've read so far applies whether the scheme of the request
+ URI is <em>HTTP</em> or <em>HTTPS</em>. However, to control the SSL
+ negotiation performed when an <em>HTTPS</em> URI is requested, there's
+ one extra object to pay attention to: the SSL context factory.
+ </p>
+
+ <p>
+ <code>Agent</code>'s constructor takes an optional second argument, a
+ context factory. This is an object like the context factory described
+ in <a href="../../core/howto/ssl.html" shape="rect">Using SSL in Twisted</a> but has
+ one small difference. The <code>getContext</code> method of this factory
+ accepts the address from the URL being requested. This allows it to
+ return a context object which verifies that the server's certificate
+ matches the URL being requested.
+ </p>
+
+ <p>
+ Here's an example which shows how to use <code>Agent</code> to request
+ an <em>HTTPS</em> URL with no certificate verification.
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">ssl</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ClientContextFactory</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WebClientContextFactory</span>(<span class="py-src-parameter">ClientContextFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getContext</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">hostname</span>, <span class="py-src-parameter">port</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">ClientContextFactory</span>.<span class="py-src-variable">getContext</span>(<span class="py-src-variable">self</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">display</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Received response&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">response</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">contextFactory</span> = <span class="py-src-variable">WebClientContextFactory</span>()
+ <span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">contextFactory</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">&quot;GET&quot;</span>, <span class="py-src-string">&quot;https://example.com/&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">display</span>, <span class="py-src-variable">err</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">ignored</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre>
+
+ <p>
+ The important point to notice here is that <code>getContext</code> now
+ accepts two arguments, a hostname and a port number. These two arguments,
+ a <code>str</code> and an <code>int</code>, give the address to which a
+ connection is being established to request an HTTPS URL. Because an agent
+ might make multiple requests over a single connection,
+ <code>getContext</code> may not be called once for each request. A second
+ or later request for a URL with the same hostname as a previous request
+ may re-use an existing connection, and therefore will re-use the
+ previously returned context object.
+ </p>
+
+ <p>
+ To configure SSL options or enable certificate verification or hostname
+ checking, provide a context factory which creates suitably configured
+ context objects.
+ </p>
+
+ <h3>HTTP Persistent Connection<a name="auto6"/></h3>
+
+ <p>
+ HTTP persistent connections use the same TCP connection to send and
+ receive multiple HTTP requests/responses. This reduces latency and TCP
+ connection establishment overhead.
+ </p>
+
+ <p>
+ The constructor of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.Agent.html" title="twisted.web.client.Agent">twisted.web.client.Agent</a></code>
+ takes an optional parameter pool, which should be an instance
+ of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.HTTPConnectionPool.html" title="twisted.web.client.HTTPConnectionPool">HTTPConnectionPool</a></code>, which will be used
+ to manage the connections. If the pool is created with the
+ parameter <code>persistent</code> set to <code>True</code> (the
+ default), it will not close connections when the request is done, and
+ instead hold them in its cache to be re-used.
+ </p>
+
+ <p>
+ Here's an example which sends requests over a persistent connection:
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Deferred</span>, <span class="py-src-variable">DeferredList</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>, <span class="py-src-variable">HTTPConnectionPool</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">IgnoreBody</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">deferred</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span> = <span class="py-src-variable">deferred</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">bytes</span>):
+ <span class="py-src-keyword">pass</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">None</span>)
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbRequest</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Response code:'</span>, <span class="py-src-variable">response</span>.<span class="py-src-variable">code</span>
+ <span class="py-src-variable">finished</span> = <span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">response</span>.<span class="py-src-variable">deliverBody</span>(<span class="py-src-variable">IgnoreBody</span>(<span class="py-src-variable">finished</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">finished</span>
+
+<span class="py-src-variable">pool</span> = <span class="py-src-variable">HTTPConnectionPool</span>(<span class="py-src-variable">reactor</span>)
+<span class="py-src-variable">agent</span> = <span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">pool</span>=<span class="py-src-variable">pool</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">requestGet</span>(<span class="py-src-parameter">url</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">'GET'</span>, <span class="py-src-variable">url</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbRequest</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">d</span>
+
+<span class="py-src-comment"># Two requests to the same host:</span>
+<span class="py-src-variable">d</span> = <span class="py-src-variable">requestGet</span>(<span class="py-src-string">'http://localhost:8080/foo'</span>).<span class="py-src-variable">addCallback</span>(
+ <span class="py-src-keyword">lambda</span> <span class="py-src-variable">ign</span>: <span class="py-src-variable">requestGet</span>(<span class="py-src-string">&quot;http://localhost:8080/bar&quot;</span>))
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">cbShutdown</span>(<span class="py-src-parameter">ignored</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+<span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">cbShutdown</span>)
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+ <p>
+ Here, the two requests are to the same host, one after the each
+ other. In most cases, the same connection will be used for the second
+ request, instead of two different connections when using a
+ non-persistent pool.
+ </p>
+
+ <h3>Multiple Connections to the Same Server<a name="auto7"/></h3>
+
+ <p>
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.HTTPConnectionPool.html" title="twisted.web.client.HTTPConnectionPool">twisted.web.client.HTTPConnectionPool</a></code> instances
+ have an attribute
+ called <code class="python">maxPersistentPerHost</code> which limits the
+ number of cached persistent connections to the same server. The default
+ value is 2. This is effective only when the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.HTTPConnectionPool.persistent.html" title="twisted.web.client.HTTPConnectionPool.persistent">persistent</a></code> option is
+ True. You can change the value like bellow:
+ </p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">HTTPConnectionPool</span>
+
+<span class="py-src-variable">pool</span> = <span class="py-src-variable">HTTPConnectionPool</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">persistent</span>=<span class="py-src-variable">True</span>)
+<span class="py-src-variable">pool</span>.<span class="py-src-variable">maxPersistentPerHost</span> = <span class="py-src-number">1</span>
+</pre>
+
+ <p>
+ With the default value of 2, the pool keeps around two connections to
+ the same host at most. Eventually the cached persistent connections will
+ be closed, by default after 240 seconds; you can change this timeout
+ value with the <code class="python">cachedConnectionTimeout</code>
+ attribute of the pool. To force all connections to close use
+ the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.HTTPConnectionPool.closeCachedConnections.html" title="twisted.web.client.HTTPConnectionPool.closeCachedConnections">closeCachedConnections</a></code>
+ method.
+ </p>
+
+
+ <h4>Automatic Retries</h4>
+
+ <p>If a request fails without getting a response, and the request is
+ something that hopefully can be retried without having any side-effects
+ (e.g. a request with method GET), it will be retried automatically when
+ sending a request over a previously-cached persistent connection. You can
+ disable this behavior by setting <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.HTTPConnectionPool.retryAutomatically.html" title="twisted.web.client.HTTPConnectionPool.retryAutomatically">retryAutomatically</a></code>
+ to <code>False</code>. Note that each request will only be retried
+ once.</p>
+
+
+ <h3>Following redirects<a name="auto8"/></h3>
+
+ <p>
+ By itself, <code>Agent</code> doesn't follow HTTP redirects (responses
+ with 301, 302, 303, 307 status codes and a <code>location</code> header
+ field). You need to use the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.RedirectAgent.html" title="twisted.web.client.RedirectAgent">twisted.web.client.RedirectAgent</a></code> class to do so. It
+ implements a rather strict behavior of the RFC, meaning it will redirect
+ 301 and 302 as 307, only on <code>GET</code> and <code>HEAD</code>
+ requests.
+ </p>
+ <p>
+ The following example shows how to have a redirect-enabled agent.
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>, <span class="py-src-variable">RedirectAgent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">display</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Received response&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">response</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">agent</span> = <span class="py-src-variable">RedirectAgent</span>(<span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>))
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">&quot;GET&quot;</span>, <span class="py-src-string">&quot;http://example.com/&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">display</span>, <span class="py-src-variable">err</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">ignored</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre>
+
+ <h3>Using a HTTP proxy<a name="auto9"/></h3>
+
+ <p>
+ To be able to use HTTP proxies with an agent, you can use the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.ProxyAgent.html" title="twisted.web.client.ProxyAgent">twisted.web.client.ProxyAgent</a></code> class. It supports the
+ same interface as <code>Agent</code>, but takes the endpoint of the proxy
+ as initializer argument.
+ </p>
+
+ <p>
+ Here's an example demonstrating the use of an HTTP proxy running on
+ localhost:8000.
+ </p>
+
+ <pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ProxyAgent</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">endpoints</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">TCP4ClientEndpoint</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">display</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Received response&quot;</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">response</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">endpoint</span> = <span class="py-src-variable">TCP4ClientEndpoint</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-string">&quot;localhost&quot;</span>, <span class="py-src-number">8000</span>)
+ <span class="py-src-variable">agent</span> = <span class="py-src-variable">ProxyAgent</span>(<span class="py-src-variable">endpoint</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">&quot;GET&quot;</span>, <span class="py-src-string">&quot;https://example.com/&quot;</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">display</span>, <span class="py-src-variable">err</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">ignored</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre>
+
+ <p>
+ Please refer to the <a href="../../core/howto/endpoints.html" shape="rect">endpoints documentation</a> for
+ more information about how they work and the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.endpoints.html" title="twisted.internet.endpoints">twisted.internet.endpoints</a></code> API documentation to learn
+ what other kinds of endpoints exist.
+ </p>
+
+ <h3>Handling HTTP cookies<a name="auto10"/></h3>
+
+ <p>
+ An existing agent instance can be wrapped with
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.CookieAgent.html" title="twisted.web.client.CookieAgent">twisted.web.client.CookieAgent</a></code> to automatically
+ store, send and track HTTP cookies. A <code>CookieJar</code>
+ instance, from the Python standard library module
+ <a href="http://docs.python.org/library/cookielib.html" shape="rect">cookielib</a>, is
+ used to store the cookie information. An example of using
+ <code>CookieAgent</code> to perform a request and display the collected
+ cookies might look like this:
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">cookielib</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">CookieJar</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>, <span class="py-src-variable">CookieAgent</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">displayCookies</span>(<span class="py-src-parameter">response</span>, <span class="py-src-parameter">cookieJar</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Received response'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">response</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Cookies:'</span>, <span class="py-src-variable">len</span>(<span class="py-src-variable">cookieJar</span>)
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">cookie</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">cookieJar</span>:
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">cookie</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">cookieJar</span> = <span class="py-src-variable">CookieJar</span>()
+ <span class="py-src-variable">agent</span> = <span class="py-src-variable">CookieAgent</span>(<span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>), <span class="py-src-variable">cookieJar</span>)
+
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">'GET'</span>, <span class="py-src-string">'http://www.google.com/'</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">displayCookies</span>, <span class="py-src-variable">cookieJar</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">ignored</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">
+ Storing cookies with CookieAgent
+ - <a href="listings/client/cookies.py"><span class="filename">listings/client/cookies.py</span></a></div></div>
+
+ <h3>Automatic Content Encoding Negotiation<a name="auto11"/></h3>
+
+ <p>
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.ContentDecoderAgent.html" title="twisted.web.client.ContentDecoderAgent">twisted.web.client.ContentDecoderAgent</a></code> adds
+ support for sending <em>Accept-Encoding</em> request headers and
+ interpreting <em>Content-Encoding</em> response headers. These headers
+ allow the server to encode the response body somehow, typically with some
+ compression scheme to save on transfer
+ costs. <code>ContentDecoderAgent</code> provides this functionality as a
+ wrapper around an existing agent instance. Together with one or more
+ decoder objects (such as
+ <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.client.GzipDecoder.html" title="twisted.web.client.GzipDecoder">twisted.web.client.GzipDecoder</a></code>), this wrapper
+ automatically negotiates an encoding to use and decodes the response body
+ accordingly. To application code using such an agent, there is no visible
+ difference in the data delivered.
+ </p>
+
+ <div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">log</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Deferred</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">protocol</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Protocol</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">client</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Agent</span>, <span class="py-src-variable">ContentDecoderAgent</span>, <span class="py-src-variable">GzipDecoder</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">BeginningPrinter</span>(<span class="py-src-parameter">Protocol</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">finished</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">finished</span> = <span class="py-src-variable">finished</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span> = <span class="py-src-number">1024</span> * <span class="py-src-number">10</span>
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">dataReceived</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">bytes</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span>:
+ <span class="py-src-variable">display</span> = <span class="py-src-variable">bytes</span>[:<span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span>]
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Some data received:'</span>
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">display</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">remaining</span> -= <span class="py-src-variable">len</span>(<span class="py-src-variable">display</span>)
+
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">connectionLost</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">reason</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'Finished receiving body:'</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">type</span>, <span class="py-src-variable">reason</span>.<span class="py-src-variable">value</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">finished</span>.<span class="py-src-variable">callback</span>(<span class="py-src-variable">None</span>)
+
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printBody</span>(<span class="py-src-parameter">response</span>):
+ <span class="py-src-variable">finished</span> = <span class="py-src-variable">Deferred</span>()
+ <span class="py-src-variable">response</span>.<span class="py-src-variable">deliverBody</span>(<span class="py-src-variable">BeginningPrinter</span>(<span class="py-src-variable">finished</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">finished</span>
+
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-variable">agent</span> = <span class="py-src-variable">ContentDecoderAgent</span>(<span class="py-src-variable">Agent</span>(<span class="py-src-variable">reactor</span>), [(<span class="py-src-string">'gzip'</span>, <span class="py-src-variable">GzipDecoder</span>)])
+
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">agent</span>.<span class="py-src-variable">request</span>(<span class="py-src-string">'GET'</span>, <span class="py-src-string">'http://www.yahoo.com/'</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printBody</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">log</span>.<span class="py-src-variable">err</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">ignored</span>: <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">&quot;__main__&quot;</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">
+ Requesting gzip encoded responses, with automatic decompression.
+ - <a href="listings/client/gzipdecoder.py"><span class="filename">listings/client/gzipdecoder.py</span></a></div></div>
+
+ <p>
+ Implementing support for new content encodings is as simple as writing a
+ new class like <code>GzipDecoder</code> that can decode a response using
+ the new encoding. As there are not many content encodings in widespread
+ use, gzip is the only encoding supported by Twisted itself.
+ </p>
+
+ <h2>
+ Conclusion
+ <a name="auto12"/></h2>
+
+ <p>
+ You should now understand the basics of the Twisted Web HTTP client. In
+ particular, you should understand:
+ </p>
+
+ <ul>
+ <li>
+ How to issue requests with arbitrary methods, headers, and bodies.
+ </li>
+ <li>
+ How to access the response version, code, phrase, headers, and body.
+ </li>
+ <li>
+ How to store, send, and track cookies.
+ </li>
+ <li>
+ How to control the streaming of the response body.
+ </li>
+ <li>
+ How to enable the HTTP persistent connection, and control the
+ number of connections.
+ </li>
+ </ul>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/glossary.html b/doc/web/howto/glossary.html
new file mode 100644
index 0000000..34fe7b7
--- /dev/null
+++ b/doc/web/howto/glossary.html
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Glossary</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Glossary</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+
+ <p class="note"><strong>Note: </strong>This glossary is very incomplete. Contributions are
+ welcome.</p>
+
+ <dl>
+ <dt><a name="resource" shape="rect">resource</a></dt>
+ <dd>
+ An object accessible via HTTP at one or more URIs. In Twisted Web,
+ a resource is represented by an object which provides <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.IResource.html" title="twisted.web.resource.IResource">twisted.web.resource.IResource</a></code> and most often is
+ a subclass of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">twisted.web.resource.Resource</a></code>. For example, here
+ is a resource which represents a simple HTML greeting.
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Greeting</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Hello, world.&quot;</span>
+</pre>
+ </dd>
+ </dl>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/index.html b/doc/web/howto/index.html
new file mode 100644
index 0000000..874ee51
--- /dev/null
+++ b/doc/web/howto/index.html
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Web Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Web Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+
+<li>Introduction
+ <ul>
+ <li><a href="web-overview.html" shape="rect">Overview of Twisted Web</a></li>
+ </ul>
+</li>
+
+<li>Web Applications
+ <ul>
+ <li><a href="using-twistedweb.html" shape="rect">Using twisted.web</a></li>
+ <li><a href="web-development.html" shape="rect">Web application development</a></li>
+ <li><a href="twisted-templates.html" shape="rect">HTML Templating with
+ twisted.web.template</a></li>
+ <li><a href="xmlrpc.html" shape="rect">XML-RPC and SOAP</a></li>
+ <li><a href="web-in-60/index.html" shape="rect">Twisted Web in 60 Seconds: A
+ series of short, complete examples using twisted.web</a></li>
+ <li><a href="resource-templates.html" shape="rect">Quixote resource templates</a></li>
+ </ul>
+</li>
+
+<li>Other
+ <ul>
+ <li><a href="client.html" shape="rect">Using the Twisted Web Client</a></li>
+ </ul>
+</li>
+
+<li>Appendix
+ <ul>
+ <li><a href="glossary.html" shape="rect">Glossary</a></li>
+ </ul>
+</li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/listings/client/cookies.py b/doc/web/howto/listings/client/cookies.py
new file mode 100644
index 0000000..80f84c2
--- /dev/null
+++ b/doc/web/howto/listings/client/cookies.py
@@ -0,0 +1,25 @@
+from cookielib import CookieJar
+
+from twisted.internet import reactor
+from twisted.python import log
+from twisted.web.client import Agent, CookieAgent
+
+def displayCookies(response, cookieJar):
+ print 'Received response'
+ print response
+ print 'Cookies:', len(cookieJar)
+ for cookie in cookieJar:
+ print cookie
+
+def main():
+ cookieJar = CookieJar()
+ agent = CookieAgent(Agent(reactor), cookieJar)
+
+ d = agent.request('GET', 'http://www.google.com/')
+ d.addCallback(displayCookies, cookieJar)
+ d.addErrback(log.err)
+ d.addCallback(lambda ignored: reactor.stop())
+ reactor.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/doc/web/howto/listings/client/filesendbody.py b/doc/web/howto/listings/client/filesendbody.py
new file mode 100644
index 0000000..ed26238
--- /dev/null
+++ b/doc/web/howto/listings/client/filesendbody.py
@@ -0,0 +1,26 @@
+from StringIO import StringIO
+
+from twisted.internet import reactor
+from twisted.web.client import Agent
+from twisted.web.http_headers import Headers
+
+from twisted.web.client import FileBodyProducer
+
+agent = Agent(reactor)
+body = FileBodyProducer(StringIO("hello, world"))
+d = agent.request(
+ 'GET',
+ 'http://example.com/',
+ Headers({'User-Agent': ['Twisted Web Client Example'],
+ 'Content-Type': ['text/x-greeting']}),
+ body)
+
+def cbResponse(ignored):
+ print 'Response received'
+d.addCallback(cbResponse)
+
+def cbShutdown(ignored):
+ reactor.stop()
+d.addBoth(cbShutdown)
+
+reactor.run()
diff --git a/doc/web/howto/listings/client/gzipdecoder.py b/doc/web/howto/listings/client/gzipdecoder.py
new file mode 100644
index 0000000..dc6af43
--- /dev/null
+++ b/doc/web/howto/listings/client/gzipdecoder.py
@@ -0,0 +1,43 @@
+from twisted.python import log
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.protocol import Protocol
+from twisted.web.client import Agent, ContentDecoderAgent, GzipDecoder
+
+class BeginningPrinter(Protocol):
+ def __init__(self, finished):
+ self.finished = finished
+ self.remaining = 1024 * 10
+
+
+ def dataReceived(self, bytes):
+ if self.remaining:
+ display = bytes[:self.remaining]
+ print 'Some data received:'
+ print display
+ self.remaining -= len(display)
+
+
+ def connectionLost(self, reason):
+ print 'Finished receiving body:', reason.type, reason.value
+ self.finished.callback(None)
+
+
+
+def printBody(response):
+ finished = Deferred()
+ response.deliverBody(BeginningPrinter(finished))
+ return finished
+
+
+def main():
+ agent = ContentDecoderAgent(Agent(reactor), [('gzip', GzipDecoder)])
+
+ d = agent.request('GET', 'http://www.yahoo.com/')
+ d.addCallback(printBody)
+ d.addErrback(log.err)
+ d.addCallback(lambda ignored: reactor.stop())
+ reactor.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/doc/web/howto/listings/client/request.py b/doc/web/howto/listings/client/request.py
new file mode 100644
index 0000000..4931863
--- /dev/null
+++ b/doc/web/howto/listings/client/request.py
@@ -0,0 +1,21 @@
+from twisted.internet import reactor
+from twisted.web.client import Agent
+from twisted.web.http_headers import Headers
+
+agent = Agent(reactor)
+
+d = agent.request(
+ 'GET',
+ 'http://example.com/',
+ Headers({'User-Agent': ['Twisted Web Client Example']}),
+ None)
+
+def cbResponse(ignored):
+ print 'Response received'
+d.addCallback(cbResponse)
+
+def cbShutdown(ignored):
+ reactor.stop()
+d.addBoth(cbShutdown)
+
+reactor.run()
diff --git a/doc/web/howto/listings/client/response.py b/doc/web/howto/listings/client/response.py
new file mode 100644
index 0000000..6b3547c
--- /dev/null
+++ b/doc/web/howto/listings/client/response.py
@@ -0,0 +1,47 @@
+from pprint import pformat
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+from twisted.internet.protocol import Protocol
+from twisted.web.client import Agent
+from twisted.web.http_headers import Headers
+
+class BeginningPrinter(Protocol):
+ def __init__(self, finished):
+ self.finished = finished
+ self.remaining = 1024 * 10
+
+ def dataReceived(self, bytes):
+ if self.remaining:
+ display = bytes[:self.remaining]
+ print 'Some data received:'
+ print display
+ self.remaining -= len(display)
+
+ def connectionLost(self, reason):
+ print 'Finished receiving body:', reason.getErrorMessage()
+ self.finished.callback(None)
+
+agent = Agent(reactor)
+d = agent.request(
+ 'GET',
+ 'http://example.com/',
+ Headers({'User-Agent': ['Twisted Web Client Example']}),
+ None)
+
+def cbRequest(response):
+ print 'Response version:', response.version
+ print 'Response code:', response.code
+ print 'Response phrase:', response.phrase
+ print 'Response headers:'
+ print pformat(list(response.headers.getAllRawHeaders()))
+ finished = Deferred()
+ response.deliverBody(BeginningPrinter(finished))
+ return finished
+d.addCallback(cbRequest)
+
+def cbShutdown(ignored):
+ reactor.stop()
+d.addBoth(cbShutdown)
+
+reactor.run()
diff --git a/doc/web/howto/listings/client/sendbody.py b/doc/web/howto/listings/client/sendbody.py
new file mode 100644
index 0000000..31cac8f
--- /dev/null
+++ b/doc/web/howto/listings/client/sendbody.py
@@ -0,0 +1,24 @@
+from twisted.internet import reactor
+from twisted.web.client import Agent
+from twisted.web.http_headers import Headers
+
+from stringprod import StringProducer
+
+agent = Agent(reactor)
+body = StringProducer("hello, world")
+d = agent.request(
+ 'GET',
+ 'http://example.com/',
+ Headers({'User-Agent': ['Twisted Web Client Example'],
+ 'Content-Type': ['text/x-greeting']}),
+ body)
+
+def cbResponse(ignored):
+ print 'Response received'
+d.addCallback(cbResponse)
+
+def cbShutdown(ignored):
+ reactor.stop()
+d.addBoth(cbShutdown)
+
+reactor.run()
diff --git a/doc/web/howto/listings/client/stringprod.py b/doc/web/howto/listings/client/stringprod.py
new file mode 100644
index 0000000..da2b5cd
--- /dev/null
+++ b/doc/web/howto/listings/client/stringprod.py
@@ -0,0 +1,21 @@
+from zope.interface import implements
+
+from twisted.internet.defer import succeed
+from twisted.web.iweb import IBodyProducer
+
+class StringProducer(object):
+ implements(IBodyProducer)
+
+ def __init__(self, body):
+ self.body = body
+ self.length = len(body)
+
+ def startProducing(self, consumer):
+ consumer.write(self.body)
+ return succeed(None)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
diff --git a/doc/web/howto/listings/element_1.py b/doc/web/howto/listings/element_1.py
new file mode 100644
index 0000000..bb5e89a
--- /dev/null
+++ b/doc/web/howto/listings/element_1.py
@@ -0,0 +1,13 @@
+from twisted.web.template import Element, renderer, XMLFile
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('template-1.xml'))
+
+ @renderer
+ def header(self, request, tag):
+ return tag('Header.')
+
+ @renderer
+ def footer(self, request, tag):
+ return tag('Footer.')
diff --git a/doc/web/howto/listings/element_2.py b/doc/web/howto/listings/element_2.py
new file mode 100644
index 0000000..a4c0271
--- /dev/null
+++ b/doc/web/howto/listings/element_2.py
@@ -0,0 +1,13 @@
+from twisted.web.template import Element, renderer, XMLFile, tags
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('template-1.xml'))
+
+ @renderer
+ def header(self, request, tag):
+ return tag(tags.b('Header.'))
+
+ @renderer
+ def footer(self, request, tag):
+ return tag(tags.b('Footer.'))
diff --git a/doc/web/howto/listings/element_3.py b/doc/web/howto/listings/element_3.py
new file mode 100644
index 0000000..46c2181
--- /dev/null
+++ b/doc/web/howto/listings/element_3.py
@@ -0,0 +1,13 @@
+from twisted.web.template import Element, renderer, XMLFile, tags
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('template-1.xml'))
+
+ @renderer
+ def header(self, request, tag):
+ return tag(tags.p('Header.'), id='header')
+
+ @renderer
+ def footer(self, request, tag):
+ return tag(tags.p('Footer.'), id='footer')
diff --git a/doc/web/howto/listings/iteration-1.py b/doc/web/howto/listings/iteration-1.py
new file mode 100644
index 0000000..e5ffcd3
--- /dev/null
+++ b/doc/web/howto/listings/iteration-1.py
@@ -0,0 +1,17 @@
+from twisted.web.template import Element, renderer, XMLFile, flattenString
+from twisted.python.filepath import FilePath
+
+class WidgetsElement(Element):
+ loader = XMLFile(FilePath('iteration-1.xml'))
+
+ widgetData = ['gadget', 'contraption', 'gizmo', 'doohickey']
+
+ @renderer
+ def widgets(self, request, tag):
+ for widget in self.widgetData:
+ yield tag.clone().fillSlots(widgetName=widget)
+
+def printResult(result):
+ print result
+
+flattenString(None, WidgetsElement()).addCallback(printResult)
diff --git a/doc/web/howto/listings/iteration-1.xml b/doc/web/howto/listings/iteration-1.xml
new file mode 100644
index 0000000..3c9e2c2
--- /dev/null
+++ b/doc/web/howto/listings/iteration-1.xml
@@ -0,0 +1,3 @@
+<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+ <li t:render="widgets"><t:slot name="widgetName"/></li>
+</ul>
diff --git a/doc/web/howto/listings/iteration-output-1.xml b/doc/web/howto/listings/iteration-output-1.xml
new file mode 100644
index 0000000..edb7f5b
--- /dev/null
+++ b/doc/web/howto/listings/iteration-output-1.xml
@@ -0,0 +1,3 @@
+<ul>
+ <li>gadget</li><li>contraption</li><li>gizmo</li><li>doohickey</li>
+</ul>
diff --git a/doc/web/howto/listings/output-1.html b/doc/web/howto/listings/output-1.html
new file mode 100644
index 0000000..f1c62ff
--- /dev/null
+++ b/doc/web/howto/listings/output-1.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+ <div>Header.</div>
+ <div id="content">
+ <p>Content goes here.</p>
+ </div>
+ <div>Footer.</div>
+</body>
+</html>
diff --git a/doc/web/howto/listings/output-2.html b/doc/web/howto/listings/output-2.html
new file mode 100644
index 0000000..7126b10
--- /dev/null
+++ b/doc/web/howto/listings/output-2.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+ <div><b>Header.</b></div>
+ <div id="content">
+ <p>Content goes here.</p>
+ </div>
+ <div><b>Footer.</b></div>
+</body>
+</html>
diff --git a/doc/web/howto/listings/output-3.html b/doc/web/howto/listings/output-3.html
new file mode 100644
index 0000000..ebe8dba
--- /dev/null
+++ b/doc/web/howto/listings/output-3.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+ <div id="header"><p>Header.</p></div>
+ <div id="content">
+ <p>Content goes here.</p>
+ </div>
+ <div id="footer"><p>Footer.</p></div>
+</body>
+</html>
diff --git a/doc/web/howto/listings/quoting-output.html b/doc/web/howto/listings/quoting-output.html
new file mode 100644
index 0000000..38108e3
--- /dev/null
+++ b/doc/web/howto/listings/quoting-output.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+ <div>&lt;&lt;&lt;Header&gt;&gt;&gt;!</div>
+ <div id="content">
+ <p>Content goes here.</p>
+ </div>
+ <div id="&lt;&quot;fun&quot;&gt;">&gt;&gt;&gt;"Footer!"&lt;&lt;&lt;</div>
+</body>
+</html>
diff --git a/doc/web/howto/listings/quoting_element.py b/doc/web/howto/listings/quoting_element.py
new file mode 100644
index 0000000..a9ab1ff
--- /dev/null
+++ b/doc/web/howto/listings/quoting_element.py
@@ -0,0 +1,13 @@
+from twisted.web.template import Element, renderer, XMLFile
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('template-1.xml'))
+
+ @renderer
+ def header(self, request, tag):
+ return tag('<<<Header>>>!')
+
+ @renderer
+ def footer(self, request, tag):
+ return tag('>>>"Footer!"<<<', id='<"fun">')
diff --git a/doc/web/howto/listings/render_1.py b/doc/web/howto/listings/render_1.py
new file mode 100644
index 0000000..4709949
--- /dev/null
+++ b/doc/web/howto/listings/render_1.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from element_1 import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/render_2.py b/doc/web/howto/listings/render_2.py
new file mode 100644
index 0000000..98b5a11
--- /dev/null
+++ b/doc/web/howto/listings/render_2.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from element_2 import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/render_3.py b/doc/web/howto/listings/render_3.py
new file mode 100644
index 0000000..0058536
--- /dev/null
+++ b/doc/web/howto/listings/render_3.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from element_3 import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/render_quoting.py b/doc/web/howto/listings/render_quoting.py
new file mode 100644
index 0000000..78b7208
--- /dev/null
+++ b/doc/web/howto/listings/render_quoting.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from quoting_element import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/render_slots_attrs.py b/doc/web/howto/listings/render_slots_attrs.py
new file mode 100644
index 0000000..da8ff55
--- /dev/null
+++ b/doc/web/howto/listings/render_slots_attrs.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from slots_attributes_1 import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/render_transparent.py b/doc/web/howto/listings/render_transparent.py
new file mode 100644
index 0000000..4fa5d59
--- /dev/null
+++ b/doc/web/howto/listings/render_transparent.py
@@ -0,0 +1,5 @@
+from twisted.web.template import flattenString
+from transparent_element import ExampleElement
+def renderDone(output):
+ print output
+flattenString(None, ExampleElement()).addCallback(renderDone)
diff --git a/doc/web/howto/listings/slots-attributes-1.xml b/doc/web/howto/listings/slots-attributes-1.xml
new file mode 100644
index 0000000..9b24ade
--- /dev/null
+++ b/doc/web/howto/listings/slots-attributes-1.xml
@@ -0,0 +1,6 @@
+<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="person_profile"
+ class="profile">
+<img><t:attr name="src"><t:slot name="profile_image_url" /></t:attr></img>
+<p><t:slot name="person_name" /></p>
+</div>
diff --git a/doc/web/howto/listings/slots-attributes-output.html b/doc/web/howto/listings/slots-attributes-output.html
new file mode 100644
index 0000000..42b9c4e
--- /dev/null
+++ b/doc/web/howto/listings/slots-attributes-output.html
@@ -0,0 +1,4 @@
+<div class="profile">
+<img src="http://example.com/user.png" />
+<p>Luke</p>
+</div>
diff --git a/doc/web/howto/listings/slots_attributes_1.py b/doc/web/howto/listings/slots_attributes_1.py
new file mode 100644
index 0000000..0de0c0b
--- /dev/null
+++ b/doc/web/howto/listings/slots_attributes_1.py
@@ -0,0 +1,12 @@
+from twisted.web.template import Element, renderer, XMLFile
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('slots-attributes-1.xml'))
+
+ @renderer
+ def person_profile(self, request, tag):
+ # Note how convenient it is to pass these attributes in!
+ tag.fillSlots(person_name='Luke',
+ profile_image_url='http://example.com/user.png')
+ return tag
diff --git a/doc/web/howto/listings/soap.rpy b/doc/web/howto/listings/soap.rpy
new file mode 100644
index 0000000..957380d
--- /dev/null
+++ b/doc/web/howto/listings/soap.rpy
@@ -0,0 +1,13 @@
+from twisted.web import soap
+import os
+
+def getQuote():
+ return "That beverage, sir, is off the hizzy."
+
+class Quoter(soap.SOAPPublisher):
+ """Publish one method, 'quote'."""
+
+ def soap_quote(self):
+ return getQuote()
+
+resource = Quoter()
diff --git a/doc/web/howto/listings/subviews-1.py b/doc/web/howto/listings/subviews-1.py
new file mode 100644
index 0000000..55538ec
--- /dev/null
+++ b/doc/web/howto/listings/subviews-1.py
@@ -0,0 +1,27 @@
+from twisted.web.template import (
+ XMLFile, TagLoader, Element, renderer, flattenString)
+from twisted.python.filepath import FilePath
+
+class WidgetsElement(Element):
+ loader = XMLFile(FilePath('subviews-1.xml'))
+
+ widgetData = ['gadget', 'contraption', 'gizmo', 'doohickey']
+
+ @renderer
+ def widgets(self, request, tag):
+ for widget in self.widgetData:
+ yield WidgetElement(TagLoader(tag), widget)
+
+class WidgetElement(Element):
+ def __init__(self, loader, name):
+ Element.__init__(self, loader)
+ self._name = name
+
+ @renderer
+ def name(self, request, tag):
+ return tag(self._name)
+
+def printResult(result):
+ print result
+
+flattenString(None, WidgetsElement()).addCallback(printResult)
diff --git a/doc/web/howto/listings/subviews-1.xml b/doc/web/howto/listings/subviews-1.xml
new file mode 100644
index 0000000..f0eb425
--- /dev/null
+++ b/doc/web/howto/listings/subviews-1.xml
@@ -0,0 +1,3 @@
+<ul xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+ <li t:render="widgets"><span t:render="name" /></li>
+</ul>
diff --git a/doc/web/howto/listings/subviews-output-1.xml b/doc/web/howto/listings/subviews-output-1.xml
new file mode 100644
index 0000000..3d4265a
--- /dev/null
+++ b/doc/web/howto/listings/subviews-output-1.xml
@@ -0,0 +1,3 @@
+<ul>
+ <li><span>gadget</span></li><li><span>contraption</span></li><li><span>gizmo</span></li><li><span>doohickey</span></li>
+</ul>
diff --git a/doc/web/howto/listings/template-1.xml b/doc/web/howto/listings/template-1.xml
new file mode 100644
index 0000000..7971375
--- /dev/null
+++ b/doc/web/howto/listings/template-1.xml
@@ -0,0 +1,9 @@
+<html xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+<body>
+ <div t:render="header" />
+ <div id="content">
+ <p>Content goes here.</p>
+ </div>
+ <div t:render="footer" />
+</body>
+</html>
diff --git a/doc/web/howto/listings/transparent-1.xml b/doc/web/howto/listings/transparent-1.xml
new file mode 100644
index 0000000..b45c603
--- /dev/null
+++ b/doc/web/howto/listings/transparent-1.xml
@@ -0,0 +1,6 @@
+<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+<!-- layout decision - these things need to be *siblings* -->
+<t:transparent t:render="renderer1" />
+<t:transparent t:render="renderer2" />
+</div>
+
diff --git a/doc/web/howto/listings/transparent-output.html b/doc/web/howto/listings/transparent-output.html
new file mode 100644
index 0000000..e0ec7e7
--- /dev/null
+++ b/doc/web/howto/listings/transparent-output.html
@@ -0,0 +1,5 @@
+<div>
+<!-- layout decision - these things need to be *siblings* -->
+hello
+world
+</div>
diff --git a/doc/web/howto/listings/transparent_element.py b/doc/web/howto/listings/transparent_element.py
new file mode 100644
index 0000000..bc788dd
--- /dev/null
+++ b/doc/web/howto/listings/transparent_element.py
@@ -0,0 +1,13 @@
+from twisted.web.template import Element, renderer, XMLFile
+from twisted.python.filepath import FilePath
+
+class ExampleElement(Element):
+ loader = XMLFile(FilePath('transparent-1.xml'))
+
+ @renderer
+ def renderer1(self, request, tag):
+ return tag("hello")
+
+ @renderer
+ def renderer2(self, request, tag):
+ return tag("world")
diff --git a/doc/web/howto/listings/wait_for_it.py b/doc/web/howto/listings/wait_for_it.py
new file mode 100644
index 0000000..b6fae92
--- /dev/null
+++ b/doc/web/howto/listings/wait_for_it.py
@@ -0,0 +1,32 @@
+import sys
+from twisted.web.template import XMLString, Element, renderer, flatten
+from twisted.internet.defer import Deferred
+
+sample = XMLString(
+ """
+ <div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+ Before waiting ...
+ <span t:render="wait"></span>
+ ... after waiting.
+ </div>
+ """)
+
+class WaitForIt(Element):
+ def __init__(self):
+ Element.__init__(self, loader=sample)
+ self.deferred = Deferred()
+
+ @renderer
+ def wait(self, request, tag):
+ return self.deferred.addCallback(
+ lambda aValue: tag("A value: " + repr(aValue)))
+
+def done(ignore):
+ print("[[[Deferred fired.]]]")
+
+print('[[[Rendering the template.]]]')
+it = WaitForIt()
+flatten(None, it, sys.stdout.write).addCallback(done)
+print('[[[In progress... now firing the Deferred.]]]')
+it.deferred.callback("<value>")
+print('[[[All done.]]]')
diff --git a/doc/web/howto/listings/waited-for-it.html b/doc/web/howto/listings/waited-for-it.html
new file mode 100644
index 0000000..36b62b5
--- /dev/null
+++ b/doc/web/howto/listings/waited-for-it.html
@@ -0,0 +1,8 @@
+[[[Rendering the template.]]]
+<div>
+ Before waiting ...
+ [[[In progress... now firing the Deferred.]]]
+<span>A value: '&lt;value&gt;'</span>
+ ... after waiting.
+ </div>[[[Deferred fired.]]]
+[[[All done.]]]
diff --git a/doc/web/howto/listings/waited-for-it.txt b/doc/web/howto/listings/waited-for-it.txt
new file mode 100644
index 0000000..36b62b5
--- /dev/null
+++ b/doc/web/howto/listings/waited-for-it.txt
@@ -0,0 +1,8 @@
+[[[Rendering the template.]]]
+<div>
+ Before waiting ...
+ [[[In progress... now firing the Deferred.]]]
+<span>A value: '&lt;value&gt;'</span>
+ ... after waiting.
+ </div>[[[Deferred fired.]]]
+[[[All done.]]]
diff --git a/doc/web/howto/listings/webquote.rtl b/doc/web/howto/listings/webquote.rtl
new file mode 100644
index 0000000..8807ac4
--- /dev/null
+++ b/doc/web/howto/listings/webquote.rtl
@@ -0,0 +1,20 @@
+from twisted.web.resource import Resource
+
+def getQuote():
+ return "An apple a day keeps the doctor away."
+
+
+class QuoteResource(Resource):
+
+ template render(self, request):
+ """\
+ <html>
+ <head><title>Quotes Galore</title></head>
+
+ <body><h1>Quotes</h1>"""
+ getQuote()
+ "</body></html>"
+
+
+resource = QuoteResource()
+
diff --git a/doc/web/howto/listings/xmlAndSoapQuote.py b/doc/web/howto/listings/xmlAndSoapQuote.py
new file mode 100644
index 0000000..f17eb28
--- /dev/null
+++ b/doc/web/howto/listings/xmlAndSoapQuote.py
@@ -0,0 +1,25 @@
+from twisted.web import soap, xmlrpc, resource, server
+import os
+
+def getQuote():
+ return "Victory to the burgeois, you capitalist swine!"
+
+class XMLRPCQuoter(xmlrpc.XMLRPC):
+ def xmlrpc_quote(self):
+ return getQuote()
+
+class SOAPQuoter(soap.SOAPPublisher):
+ def soap_quote(self):
+ return getQuote()
+
+def main():
+ from twisted.internet import reactor
+ root = resource.Resource()
+ root.putChild('RPC2', XMLRPCQuoter())
+ root.putChild('SOAP', SOAPQuoter())
+ reactor.listenTCP(7080, server.Site(root))
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
+
diff --git a/doc/web/howto/listings/xmlquote.rpy b/doc/web/howto/listings/xmlquote.rpy
new file mode 100644
index 0000000..26b76f0
--- /dev/null
+++ b/doc/web/howto/listings/xmlquote.rpy
@@ -0,0 +1,12 @@
+from twisted.web import xmlrpc
+import os
+
+def getQuote():
+ return "What are you talking about, William?"
+
+class Quoter(xmlrpc.XMLRPC):
+
+ def xmlrpc_quote(self):
+ return getQuote()
+
+resource = Quoter()
diff --git a/doc/web/howto/listings/xmlrpc-customized.py b/doc/web/howto/listings/xmlrpc-customized.py
new file mode 100644
index 0000000..af604eb
--- /dev/null
+++ b/doc/web/howto/listings/xmlrpc-customized.py
@@ -0,0 +1,60 @@
+from twisted.web import xmlrpc, server
+
+class EchoHandler:
+
+ def echo(self, x):
+ """
+ Return all passed args
+ """
+ return x
+
+
+
+class AddHandler:
+
+ def add(self, a, b):
+ """
+ Return sum of arguments.
+ """
+ return a + b
+
+
+
+class Example(xmlrpc.XMLRPC):
+ """
+ An example of using you own policy to fetch the handler
+ """
+
+ def __init__(self):
+ xmlrpc.XMLRPC.__init__(self)
+ self._addHandler = AddHandler()
+ self._echoHandler = EchoHandler()
+
+ #We keep a dict of all relevant
+ #procedure names and callable.
+ self._procedureToCallable = {
+ 'add':self._addHandler.add,
+ 'echo':self._echoHandler.echo
+ }
+
+ def lookupProcedure(self, procedurePath):
+ try:
+ return self._procedureToCallable[procedurePath]
+ except KeyError, e:
+ raise xmlrpc.NoSuchFunction(self.NOT_FOUND,
+ "procedure %s not found" % procedurePath)
+
+ def listProcedures(self):
+ """
+ Since we override lookupProcedure, its suggested to override
+ listProcedures too.
+ """
+ return ['add', 'echo']
+
+
+
+if __name__ == '__main__':
+ from twisted.internet import reactor
+ r = Example()
+ reactor.listenTCP(7080, server.Site(r))
+ reactor.run()
diff --git a/doc/web/howto/resource-templates.html b/doc/web/howto/resource-templates.html
new file mode 100644
index 0000000..d1cb0d2
--- /dev/null
+++ b/doc/web/howto/resource-templates.html
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Light Weight Templating With Resource Templates</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Light Weight Templating With Resource Templates</h1>
+ <div class="toc"><ol><li><a href="#auto0">Overview</a></li><li><a href="#auto1">Configuring Twisted Web</a></li><li><a href="#auto2">Using ResourceTemplate</a></li></ol></div>
+ <div class="content">
+ <span/>
+
+<h2>Overview<a name="auto0"/></h2>
+
+<p>While high-level templating systems can be used with Twisted (for
+example, <a href="https://launchpad.net/nevow" shape="rect">Divmod
+Nevow</a>, sometimes one needs a less file-heavy system which lets one
+directly write HTML. While
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.script.ResourceScript.html" title="twisted.web.script.ResourceScript">ResourceScript</a></code> is
+available, it has a high coding overhead, and requires some boring string
+arithmetic.
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.script.ResourceTemplate.html" title="twisted.web.script.ResourceTemplate">ResourceTemplate</a></code> fills the
+space between Nevow and ResourceScript using Quixote's PTL (Python Templating
+Language).</p>
+
+<p>ResourceTemplates need Quixote
+installed. In <a href="http://www.debian.org" shape="rect">Debian</a>, that means
+installing the <code>python-quixote</code> package
+(<code>apt-get install python-quixote</code>). Other operating systems
+require other ways to install Quixote, or it can be done manually.</p>
+
+<h2>Configuring Twisted Web<a name="auto1"/></h2>
+
+<p>The easiest way to get Twisted Web to support ResourceTemplates is to
+bind them to some extension using the web tap's <code>--processor</code>
+flag. Here is an example:</p>
+
+<pre xml:space="preserve">
+% twistd web --path=/var/www \
+ --processor=.rtl=twisted.web.script.ResourceTemplate
+</pre>
+
+<p>The above command line binds the <code>rtl</code> extension to use the
+ResourceTemplate processor. Other ways are possible, but would require
+more Python coding and are outside the scope of this HOWTO.</p>
+
+<h2>Using ResourceTemplate<a name="auto2"/></h2>
+
+<p>ResourceTemplates are coded in an extension of Python called the
+<q>Python Templating Language</q>. Complete documentation of the PTL
+is available
+at <a href="http://quixote.python.ca/quixote.dev/doc/PTL.html" shape="rect">the
+quixote web site</a>. The web server will expect the PTL source file
+to define a variable named <code>resource</code>. This should be
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">twisted.web.resource.Resource</a></code>,
+whose <code>.render</code> method be called. Usually, you would want
+to define <code>render</code> using the keyword <code>template</code>
+rather than <code>def</code>.</p>
+
+<p>Here is a simple example for a resource template.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;An apple a day keeps the doctor away.&quot;</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">QuoteResource</span>(<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-variable">template</span> <span class="py-src-variable">render</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">request</span>):
+ <span class="py-src-string">&quot;&quot;&quot;\
+ &lt;html&gt;
+ &lt;head&gt;&lt;title&gt;Quotes Galore&lt;/title&gt;&lt;/head&gt;
+
+ &lt;body&gt;&lt;h1&gt;Quotes&lt;/h1&gt;&quot;&quot;&quot;</span>
+ <span class="py-src-variable">getQuote</span>()
+ <span class="py-src-string">&quot;&lt;/body&gt;&lt;/html&gt;&quot;</span>
+
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">QuoteResource</span>()
+</pre><div class="caption">Resource Template for Quotes - <a href="listings/webquote.rtl"><span class="filename">listings/webquote.rtl</span></a></div></div>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/twisted-templates.html b/doc/web/howto/twisted-templates.html
new file mode 100644
index 0000000..f998292
--- /dev/null
+++ b/doc/web/howto/twisted-templates.html
@@ -0,0 +1,704 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: HTML Templating with twisted.web.template</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">HTML Templating with twisted.web.template</h1>
+ <div class="toc"><ol><li><a href="#auto0">A Very Quick Introduction To Templating In Python</a></li><li><a href="#auto1">twisted.web.template - Why And How you Might Want to Use It</a></li><ul><li><a href="#auto2">Template Attributes</a></li><li><a href="#auto3">Slots</a></li><li><a href="#auto4">Iteration</a></li><li><a href="#auto5">Sub-views</a></li><li><a href="#auto6">Transparent</a></li></ul><li><a href="#auto7">Quoting</a></li><li><a href="#auto8">Deferreds</a></li><li><a href="#auto9">A Brief Note on Formats and DOCTYPEs</a></li><li><a href="#auto10">A Bit of History</a></li></ol></div>
+ <div class="content">
+<span/>
+<h2>A Very Quick Introduction To Templating In Python<a name="auto0"/></h2>
+<p>
+HTML templating is the process of transforming a template document (one which
+describes style and structure, but does not itself include any content) into
+some HTML output which includes information about objects in your application.
+There are many, many libraries for doing this in Python: to name a few, <a href="http://jinja.pocoo.org/" shape="rect">jinja2</a>, <a href="http://docs.djangoproject.com/en/dev/ref/templates/" shape="rect">django templates</a>,
+and <a href="http://www.clearsilver.net/" shape="rect">clearsilver</a>. You can easily use
+any of these libraries in your Twisted Web application, either by running them
+as <a href="web-in-60/wsgi.html" shape="rect">WSGI applications</a> or by calling your
+preferred templating system's APIs to produce their output as strings, and then
+writing those strings to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.request.Request.write.html" title="twisted.web.request.Request.write">Request.write</a></code>.
+</p>
+<p>Before we begin explaining how to use it, I'd like to stress that you
+don't <i>need</i> to use Twisted's templating system if you prefer some other
+way to generate HTML. Use it if it suits your personal style or your
+application, but feel free to use other things. Twisted includes templating for
+its own use, because the <code>twisted.web</code> server needs to produce HTML
+in various places, and we didn't want to add another large dependency for that.
+Twisted is <em>not</em> in any way incompatible with other systems, so that has
+nothing to do with the fact that we use our own.</p>
+<p>
+</p>
+<h2>twisted.web.template - Why And How you Might Want to Use It<a name="auto1"/></h2>
+<p>
+Twisted includes a templating system, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code>. This can be convenient for Twisted
+applications that want to produce some basic HTML for a web interface without an
+additional dependency.
+</p>
+<p>
+<code>twisted.web.template</code> also includes
+support for <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>s, so
+you can incrementally render the output of a page based on the results of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>s that your application
+has returned. This feature is fairly unique among templating libraries.
+</p>
+<p>
+In <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code>, templates are XHTML files
+which also contain a special namespace for indicating dynamic portions of the
+document. For example:
+</p>
+<div class="html-listing"><pre class="htmlsource">
+&lt;html xmlns:t=&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;&gt;
+&lt;body&gt;
+ &lt;div t:render=&quot;header&quot; /&gt;
+ &lt;div id=&quot;content&quot;&gt;
+ &lt;p&gt;Content goes here.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div t:render=&quot;footer&quot; /&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre><div class="caption">template example - <a href="listings/template-1.xml"><span class="filename">listings/template-1.xml</span></a></div></div>
+The basic unit of templating is <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Element.html" title="twisted.web.template.Element">twisted.web.template.Element</a></code>. An Element is given a way of
+loading a bit of markup like the above example, and knows how to
+correlate <code>render</code> attributes within that markup to Python methods
+exposed with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.renderer.html" title="twisted.web.template.renderer">twisted.web.template.renderer</a></code>:
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'template-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">header</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">'Header.'</span>)
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">footer</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">'Footer.'</span>)
+</pre><div class="caption">element example - <a href="listings/element_1.py"><span class="filename">listings/element_1.py</span></a></div></div>
+In order to combine the two, we must render the element. For this simple
+example, we can use the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.flattenString.html" title="twisted.web.template.flattenString">flattenString</a></code> API, which will convert a
+single template object - such as an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Element.html" title="twisted.web.template.Element">Element</a></code> - into a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> which fires with a single string,
+the HTML output of the rendering process.
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">flattenString</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">element_1</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ExampleElement</span>
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">renderDone</span>(<span class="py-src-parameter">output</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">output</span>
+<span class="py-src-variable">flattenString</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">ExampleElement</span>()).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">renderDone</span>)
+</pre><div class="caption">rendering snippet - <a href="listings/render_1.py"><span class="filename">listings/render_1.py</span></a></div></div>
+<p>This short program cheats a little bit; we know that there are no <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>s in the template which
+require the reactor to eventually fire; therefore, we can simply add a callback
+which outputs the result. Also, none of the <code>renderer</code> functions
+require the <code>request</code> object, so it's acceptable to
+pass <code>None</code> through here. (The 'request' object here is used only to
+relay information about the rendering process to each renderer, so you may
+always use whatever object makes sense for your application. Note, however,
+that renderers from library code may require an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.iweb.IRequest.html" title="twisted.web.iweb.IRequest">IRequest</a></code>.)</p>
+<p>
+If you run it yourself, you can see that it produces the following output:
+</p>
+<div class="html-listing"><pre class="htmlsource">
+&lt;html&gt;
+&lt;body&gt;
+ &lt;div&gt;Header.&lt;/div&gt;
+ &lt;div id=&quot;content&quot;&gt;
+ &lt;p&gt;Content goes here.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div&gt;Footer.&lt;/div&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre><div class="caption">rendering output 1 - <a href="listings/output-1.html"><span class="filename">listings/output-1.html</span></a></div></div>
+The third parameter to a renderer method is a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Tag.html" title="twisted.web.template.Tag">Tag</a></code> object which represents the XML element
+with the <code>t:render</code> attribute in the template. Calling a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Tag.html" title="twisted.web.template.Tag">Tag</a></code> adds children to the element
+in the DOM, which may be strings, more <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Tag.html" title="twisted.web.template.Tag">Tag</a></code>s, or other renderables such as <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.Element.html" title="twisted.web.template.Element">Element</a></code>s.
+For example, to make the header and footer bold:
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>, <span class="py-src-variable">tags</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'template-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">header</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-variable">tags</span>.<span class="py-src-variable">b</span>(<span class="py-src-string">'Header.'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">footer</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-variable">tags</span>.<span class="py-src-variable">b</span>(<span class="py-src-string">'Footer.'</span>))
+</pre><div class="caption">tag manipulation example - <a href="listings/element_2.py"><span class="filename">listings/element_2.py</span></a></div></div>
+
+Rendering this in a similar way to the first example would produce:
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;html&gt;
+&lt;body&gt;
+ &lt;div&gt;&lt;b&gt;Header.&lt;/b&gt;&lt;/div&gt;
+ &lt;div id=&quot;content&quot;&gt;
+ &lt;p&gt;Content goes here.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div&gt;&lt;b&gt;Footer.&lt;/b&gt;&lt;/div&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre><div class="caption">tag manipulation output - <a href="listings/output-2.html"><span class="filename">listings/output-2.html</span></a></div></div>
+
+In addition to adding children, call syntax can be used to set attributes on a
+tag. For example, to change the <code>id</code> on the <code>div</code> while
+adding children:
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>, <span class="py-src-variable">tags</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'template-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">header</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-variable">tags</span>.<span class="py-src-variable">p</span>(<span class="py-src-string">'Header.'</span>), <span class="py-src-variable">id</span>=<span class="py-src-string">'header'</span>)
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">footer</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-variable">tags</span>.<span class="py-src-variable">p</span>(<span class="py-src-string">'Footer.'</span>), <span class="py-src-variable">id</span>=<span class="py-src-string">'footer'</span>)
+</pre><div class="caption">attributes example - <a href="listings/element_3.py"><span class="filename">listings/element_3.py</span></a></div></div>
+
+And this would produce the following page:
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;html&gt;
+&lt;body&gt;
+ &lt;div id=&quot;header&quot;&gt;&lt;p&gt;Header.&lt;/p&gt;&lt;/div&gt;
+ &lt;div id=&quot;content&quot;&gt;
+ &lt;p&gt;Content goes here.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div id=&quot;footer&quot;&gt;&lt;p&gt;Footer.&lt;/p&gt;&lt;/div&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre><div class="caption">attributes output - <a href="listings/output-3.html"><span class="filename">listings/output-3.html</span></a></div></div>
+
+<p>
+Calling a tag mutates it, it and returns the tag itself, so you can pass it
+forward and call it multiple times if you have multiple children or attributes
+to add to it. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code> also exposes some
+convenient objects for building more complex markup structures from within
+renderer methods in the <code>tags</code> object. In the examples above, we've
+only used <code>tags.p</code> and <code>tags.b</code>, but there should be a <code>tags.x</code> for each <em>x</em> which is a valid HTML tag. There may be
+some omissions, but if you find one, please feel free to file a bug.
+</p>
+
+<h3>Template Attributes<a name="auto2"/></h3>
+
+<code>t:attr</code> tags allow you to set HTML attributes
+(like <code>href</code> in an <code>&lt;a href=&quot;...</code>) on an enclosing
+element.
+
+<h3>Slots<a name="auto3"/></h3>
+
+<code>t:slot</code> tags allow you to specify &quot;slots&quot; which you can
+conveniently fill with multiple pieces of data straight from your Python
+program.
+
+The following example demonstrates both <code>t:attr</code>
+and <code>t:slot</code> in action. Here we have a layout which displays a person's
+profile on your snazzy new Twisted-powered social networking site. We use
+the <code>t:attr</code> tag to drop in the &quot;src&quot; attribute on the profile picture,
+where the actual value of src attribute gets specified by a <code>t:slot</code>
+tag <em>within</em> the <code>t:attr</code> tag. Confused? It should make more
+sense when you see the code:
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;div xmlns:t=&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;
+ t:render=&quot;person_profile&quot;
+ class=&quot;profile&quot;&gt;
+&lt;img&gt;&lt;t:attr name=&quot;src&quot;&gt;&lt;t:slot name=&quot;profile_image_url&quot; /&gt;&lt;/t:attr&gt;&lt;/img&gt;
+&lt;p&gt;&lt;t:slot name=&quot;person_name&quot; /&gt;&lt;/p&gt;
+&lt;/div&gt;
+</pre><div class="caption">slots and attributes template - <a href="listings/slots-attributes-1.xml"><span class="filename">listings/slots-attributes-1.xml</span></a></div></div>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'slots-attributes-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">person_profile</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-comment"># Note how convenient it is to pass these attributes in!</span>
+ <span class="py-src-variable">tag</span>.<span class="py-src-variable">fillSlots</span>(<span class="py-src-variable">person_name</span>=<span class="py-src-string">'Luke'</span>,
+ <span class="py-src-variable">profile_image_url</span>=<span class="py-src-string">'http://example.com/user.png'</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>
+</pre><div class="caption">slots and attributes element - <a href="listings/slots_attributes_1.py"><span class="filename">listings/slots_attributes_1.py</span></a></div></div>
+<div class="html-listing"><pre class="htmlsource">
+&lt;div class=&quot;profile&quot;&gt;
+&lt;img src=&quot;http://example.com/user.png&quot; /&gt;
+&lt;p&gt;Luke&lt;/p&gt;
+&lt;/div&gt;
+</pre><div class="caption">slots and attributes output - <a href="listings/slots-attributes-output.html"><span class="filename">listings/slots-attributes-output.html</span></a></div></div>
+
+<h3>Iteration<a name="auto4"/></h3>
+
+<p>Often, you will have a sequence of things, and want to render each of them,
+repeating a part of the template for each one. This can be done by
+cloning <code>tag</code> in your renderer:</p>
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;ul xmlns:t=&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;&gt;
+ &lt;li t:render=&quot;widgets&quot;&gt;&lt;t:slot name=&quot;widgetName&quot;/&gt;&lt;/li&gt;
+&lt;/ul&gt;
+</pre><div class="caption">iteration template - <a href="listings/iteration-1.xml"><span class="filename">listings/iteration-1.xml</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>, <span class="py-src-variable">flattenString</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WidgetsElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'iteration-1.xml'</span>))
+
+ <span class="py-src-variable">widgetData</span> = [<span class="py-src-string">'gadget'</span>, <span class="py-src-string">'contraption'</span>, <span class="py-src-string">'gizmo'</span>, <span class="py-src-string">'doohickey'</span>]
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">widgets</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">widget</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">widgetData</span>:
+ <span class="py-src-keyword">yield</span> <span class="py-src-variable">tag</span>.<span class="py-src-variable">clone</span>().<span class="py-src-variable">fillSlots</span>(<span class="py-src-variable">widgetName</span>=<span class="py-src-variable">widget</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+
+<span class="py-src-variable">flattenString</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">WidgetsElement</span>()).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+</pre><div class="caption">iteration element - <a href="listings/iteration-1.py"><span class="filename">listings/iteration-1.py</span></a></div></div>
+<div class="html-listing"><pre class="htmlsource">
+&lt;ul&gt;
+ &lt;li&gt;gadget&lt;/li&gt;&lt;li&gt;contraption&lt;/li&gt;&lt;li&gt;gizmo&lt;/li&gt;&lt;li&gt;doohickey&lt;/li&gt;
+&lt;/ul&gt;
+</pre><div class="caption">iteration output - <a href="listings/iteration-output-1.xml"><span class="filename">listings/iteration-output-1.xml</span></a></div></div>
+
+<p>This renderer works because a renderer can return anything that can be
+rendered, not just <code>tag</code>. In this case, we define a generator, which
+returns a thing that is iterable. We also could have returned
+a <code>list</code>. Anything that is iterable will be rendered by <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code> rendering each item in it. In
+this case, each item is a copy of the tag the renderer received, each filled
+with the name of a widget.</p>
+
+<h3>Sub-views<a name="auto5"/></h3>
+
+<p>Another common pattern is to delegate the rendering logic for a small part of
+the page to a separate <code>Element</code>. For example, the widgets from the
+iteration example above might be more complicated to render. You can define
+an <code>Element</code> subclass which can render a single widget. The renderer
+method on the container can then yield instances of this
+new <code>Element</code> subclass.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber">1
+2
+3
+</p>&lt;<span class="py-src-variable">ul</span> <span class="py-src-variable">xmlns</span>:<span class="py-src-variable">t</span>=<span class="py-src-string">&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;</span>&gt;
+ &lt;<span class="py-src-variable">li</span> <span class="py-src-variable">t</span>:<span class="py-src-variable">render</span>=<span class="py-src-string">&quot;widgets&quot;</span>&gt;&lt;<span class="py-src-variable">span</span> <span class="py-src-variable">t</span>:<span class="py-src-variable">render</span>=<span class="py-src-string">&quot;name&quot;</span> /&gt;&lt;/<span class="py-src-variable">li</span>&gt;
+&lt;/<span class="py-src-variable">ul</span>&gt;
+</pre><div class="caption">subview template - <a href="listings/subviews-1.xml"><span class="filename">listings/subviews-1.xml</span></a></div></div>
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> (
+ <span class="py-src-variable">XMLFile</span>, <span class="py-src-variable">TagLoader</span>, <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">flattenString</span>)
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WidgetsElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'subviews-1.xml'</span>))
+
+ <span class="py-src-variable">widgetData</span> = [<span class="py-src-string">'gadget'</span>, <span class="py-src-string">'contraption'</span>, <span class="py-src-string">'gizmo'</span>, <span class="py-src-string">'doohickey'</span>]
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">widgets</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">widget</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">widgetData</span>:
+ <span class="py-src-keyword">yield</span> <span class="py-src-variable">WidgetElement</span>(<span class="py-src-variable">TagLoader</span>(<span class="py-src-variable">tag</span>), <span class="py-src-variable">widget</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WidgetElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">loader</span>, <span class="py-src-parameter">name</span>):
+ <span class="py-src-variable">Element</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">loader</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_name</span> = <span class="py-src-variable">name</span>
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">name</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_name</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printResult</span>(<span class="py-src-parameter">result</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">result</span>
+
+<span class="py-src-variable">flattenString</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">WidgetsElement</span>()).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">printResult</span>)
+</pre><div class="caption">subview element - <a href="listings/subviews-1.py"><span class="filename">listings/subviews-1.py</span></a></div></div>
+<div class="html-listing"><pre class="htmlsource">
+&lt;ul&gt;
+ &lt;li&gt;&lt;span&gt;gadget&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;contraption&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;gizmo&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span&gt;doohickey&lt;/span&gt;&lt;/li&gt;
+&lt;/ul&gt;
+</pre><div class="caption">subview output - <a href="listings/subviews-output-1.xml"><span class="filename">listings/subviews-output-1.xml</span></a></div></div>
+
+<p><code>TagLoader</code> lets the portion of the overall template related to
+widgets be re-used for <code>WidgetElement</code>, which is otherwise a
+normal <code>Element</code> subclass not much different
+from <code>WidgetsElement</code>. Notice that the <em>name</em> renderer on
+the <code>span</code> tag in this template is satisfied
+from <code>WidgetElement</code>, not <code>WidgetsElement</code>.</p>
+
+<h3>Transparent<a name="auto6"/></h3>
+
+Note how renderers, slots and attributes require you to specify a renderer on
+some outer HTML element. What if you don't want to be forced to add an element
+to your DOM just to drop some content into it? Maybe it messes with your
+layout, and you can't get it to work in IE with that extra <code>div</code>
+tag? Perhaps you need <code>t:transparent</code>, which allows you to drop some
+content in without any surrounding &quot;container&quot; tag. For example:
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;div xmlns:t=&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;&gt;
+&lt;!-- layout decision - these things need to be *siblings* --&gt;
+&lt;t:transparent t:render=&quot;renderer1&quot; /&gt;
+&lt;t:transparent t:render=&quot;renderer2&quot; /&gt;
+&lt;/div&gt;
+
+</pre><div class="caption">transparent template - <a href="listings/transparent-1.xml"><span class="filename">listings/transparent-1.xml</span></a></div></div>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'transparent-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">renderer1</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">&quot;hello&quot;</span>)
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">renderer2</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">&quot;world&quot;</span>)
+</pre><div class="caption">transparent element - <a href="listings/transparent_element.py"><span class="filename">listings/transparent_element.py</span></a></div></div>
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;div&gt;
+&lt;!-- layout decision - these things need to be *siblings* --&gt;
+hello
+world
+&lt;/div&gt;
+</pre><div class="caption">transparent rendering output - <a href="listings/transparent-output.html"><span class="filename">listings/transparent-output.html</span></a></div></div>
+
+<h2>Quoting<a name="auto7"/></h2>
+
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code> will quote any strings that place
+into the DOM. This provides protection against <a href="http://en.wikipedia.org/wiki/Cross-site_scripting" shape="rect">XSS attacks</a>, in
+addition to just generally making it easy to put arbitrary strings onto a web
+page, without worrying about what they might have in them. This can easily be
+demonstrated with an element using the same template from our earlier examples.
+Here's an element that returns some &quot;special&quot; characters in HTML ('&lt;', '&gt;',
+and '&quot;', which is special in attribute values):
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">XMLFile</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">filepath</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePath</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleElement</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-variable">loader</span> = <span class="py-src-variable">XMLFile</span>(<span class="py-src-variable">FilePath</span>(<span class="py-src-string">'template-1.xml'</span>))
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">header</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">'&lt;&lt;&lt;Header&gt;&gt;&gt;!'</span>)
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">footer</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">tag</span>(<span class="py-src-string">'&gt;&gt;&gt;&quot;Footer!&quot;&lt;&lt;&lt;'</span>, <span class="py-src-variable">id</span>=<span class="py-src-string">'&lt;&quot;fun&quot;&gt;'</span>)
+</pre><div class="caption">renderers returning &quot;special&quot; characters - <a href="listings/quoting_element.py"><span class="filename">listings/quoting_element.py</span></a></div></div>
+
+Note that they are all safely quoted in the output, and will appear in a web
+browser just as you returned them from your Python method:
+
+<div class="html-listing"><pre class="htmlsource">
+&lt;html&gt;
+&lt;body&gt;
+ &lt;div&gt;&amp;lt;&amp;lt;&amp;lt;Header&amp;gt;&amp;gt;&amp;gt;!&lt;/div&gt;
+ &lt;div id=&quot;content&quot;&gt;
+ &lt;p&gt;Content goes here.&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div id=&quot;&amp;lt;&amp;quot;fun&amp;quot;&amp;gt;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&quot;Footer!&quot;&amp;lt;&amp;lt;&amp;lt;&lt;/div&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre><div class="caption">output containing &quot;special&quot; characters - <a href="listings/quoting-output.html"><span class="filename">listings/quoting-output.html</span></a></div></div>
+
+<h2>Deferreds<a name="auto8"/></h2>
+
+Finally, a simple demonstration of Deferred support, the unique feature of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.html" title="twisted.web.template">twisted.web.template</a></code>. Simply put, any renderer may
+return a Deferred which fires with some template content instead of the template
+content itself. As shown above, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.flattenString.html" title="twisted.web.template.flattenString">flattenString</a></code> will return a Deferred that
+fires with the full content of the string. But if there's a lot of content, you
+might not want to wait before starting to send some of it to your HTTP client:
+for that case, you can use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.flatten.html" title="twisted.web.template.flatten">flatten</a></code>.
+It's difficult to demonstrate this directly in a browser-based application;
+unless you insert very long delays before firing your Deferreds, it just looks
+like your browser is instantly displaying everything. Here's an example that
+just prints out some HTML template, with markers inserted for where certain
+events happen:
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">sys</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">template</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">XMLString</span>, <span class="py-src-variable">Element</span>, <span class="py-src-variable">renderer</span>, <span class="py-src-variable">flatten</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">defer</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Deferred</span>
+
+<span class="py-src-variable">sample</span> = <span class="py-src-variable">XMLString</span>(
+ <span class="py-src-string">&quot;&quot;&quot;
+ &lt;div xmlns:t=&quot;http://twistedmatrix.com/ns/twisted.web.template/0.1&quot;&gt;
+ Before waiting ...
+ &lt;span t:render=&quot;wait&quot;&gt;&lt;/span&gt;
+ ... after waiting.
+ &lt;/div&gt;
+ &quot;&quot;&quot;</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">WaitForIt</span>(<span class="py-src-parameter">Element</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">Element</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">loader</span>=<span class="py-src-variable">sample</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span> = <span class="py-src-variable">Deferred</span>()
+
+ @<span class="py-src-variable">renderer</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">wait</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">tag</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">deferred</span>.<span class="py-src-variable">addCallback</span>(
+ <span class="py-src-keyword">lambda</span> <span class="py-src-variable">aValue</span>: <span class="py-src-variable">tag</span>(<span class="py-src-string">&quot;A value: &quot;</span> + <span class="py-src-variable">repr</span>(<span class="py-src-variable">aValue</span>)))
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">done</span>(<span class="py-src-parameter">ignore</span>):
+ <span class="py-src-keyword">print</span>(<span class="py-src-string">&quot;[[[Deferred fired.]]]&quot;</span>)
+
+<span class="py-src-keyword">print</span>(<span class="py-src-string">'[[[Rendering the template.]]]'</span>)
+<span class="py-src-variable">it</span> = <span class="py-src-variable">WaitForIt</span>()
+<span class="py-src-variable">flatten</span>(<span class="py-src-variable">None</span>, <span class="py-src-variable">it</span>, <span class="py-src-variable">sys</span>.<span class="py-src-variable">stdout</span>.<span class="py-src-variable">write</span>).<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">done</span>)
+<span class="py-src-keyword">print</span>(<span class="py-src-string">'[[[In progress... now firing the Deferred.]]]'</span>)
+<span class="py-src-variable">it</span>.<span class="py-src-variable">deferred</span>.<span class="py-src-variable">callback</span>(<span class="py-src-string">&quot;&lt;value&gt;&quot;</span>)
+<span class="py-src-keyword">print</span>(<span class="py-src-string">'[[[All done.]]]'</span>)
+</pre><div class="caption">deferred example - <a href="listings/wait_for_it.py"><span class="filename">listings/wait_for_it.py</span></a></div></div>
+
+If you run this example, you should get the following output:
+
+<div class="html-listing"><pre class="htmlsource">
+[[[Rendering the template.]]]
+&lt;div&gt;
+ Before waiting ...
+ [[[In progress... now firing the Deferred.]]]
+&lt;span&gt;A value: '&amp;lt;value&amp;gt;'&lt;/span&gt;
+ ... after waiting.
+ &lt;/div&gt;[[[Deferred fired.]]]
+[[[All done.]]]
+</pre><div class="caption">output from deferred example - <a href="listings/waited-for-it.html"><span class="filename">listings/waited-for-it.html</span></a></div></div>
+
+This demonstrates that part of the output (everything up to
+&quot;<code>[[[In progress...</code>&quot;) is written out immediately as it's rendered.
+But once it hits the Deferred, <code>WaitForIt</code>'s rendering needs to pause
+until <code>.callback(...)</code> is called on that Deferred. You can see that
+no further output is produced until the message indicating that the Deferred is
+being fired is complete. By returning Deferreds and using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.template.flatten.html" title="twisted.web.template.flatten">flatten</a></code>, you can avoid buffering large
+amounts of data.
+
+<h2>A Brief Note on Formats and DOCTYPEs<a name="auto9"/></h2>
+
+<p>
+The goal of <code>twisted.web.template</code> is to emit both valid <a href="http://whatwg.org/html" shape="rect">HTML</a> or <a href="http://www.whatwg.org/specs/web-apps/current-work/multipage/the-xhtml-syntax.html#the-xhtml-syntax" shape="rect">XHTML</a>.
+However, in order to get the maximally standards-compliant output format you
+desire, you have to know which one you want, and take a few simple steps to emit
+it correctly. Many browsers will probably work with most output if you ignore
+this section entirely, but <a href="http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#the-doctype" shape="rect">the
+ HTML specification recommends that you specify an appropriate DOCTYPE</a>.
+</p>
+
+<p>
+As a <code>DOCTYPE</code> declaration in your template would describe the
+template itself, rather than its output, it won't be included in your output.
+If you wish to annotate your template output with a DOCTYPE, you will have to
+write it to the browser out of band. One way to do this would be to simply
+do <code>request.write('&lt;!DOCTYPE html&gt;\n')</code> when you are ready to
+begin emitting your response. The same goes for an XML <code>DOCTYPE</code>
+declaration.
+</p>
+
+<p>
+<code>twisted.web.template</code> will remove the <code>xmlns</code> attributes
+used to declare
+the <code>http://twistedmatrix.com/ns/twisted.web.template/0.1</code> namespace,
+but it will not modify other namespace declaration attributes. Therefore if you
+wish to serialize in HTML format, you should not use other namespaces; if you
+wish to serialize to XML, feel free to insert any namespace declarations that
+are appropriate, and they will appear in your output.
+</p>
+
+<div class="note"><strong>Note: </strong>
+This relaxed approach is correct in many cases. However, in certain contexts -
+especially &lt;script&gt; and &lt;style&gt; tags - quoting rules differ in
+significant ways between HTML and XML, and between different browsers' parsers
+in HTML. If you want to generate dynamic content inside a script or stylesheet,
+the best option is to load the resource externally so you don't have to worry
+about quoting rules. The second best option is to strictly configure your
+content-types and DOCTYPE declarations for XML, whose quoting rules are simple
+and compatible with the approach that <code>twisted.web.template</code> takes.
+And, please remember: regardless of how you put it there, any user input placed
+inside a &lt;script&gt; or &lt;style&gt; tag is a potential security issue.
+</div>
+
+<h2>A Bit of History<a name="auto10"/></h2>
+<p>
+Those of you who used Divmod Nevow may notice some
+similarities. <code>twisted.web.template</code> is in fact derived from the
+latest version of Nevow, but includes only the latest components from Nevow's
+rendering pipeline, and does not have any of the legacy compatibility layers
+that Nevow grew over time. This should make
+using <code>twisted.web.template</code> a similar experience for many long-time
+users of Twisted who have previously used Nevow for its twisted-friendly
+templating, but more straightforward for new users.
+</p>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/using-twistedweb.html b/doc/web/howto/using-twistedweb.html
new file mode 100644
index 0000000..c4cfe31
--- /dev/null
+++ b/doc/web/howto/using-twistedweb.html
@@ -0,0 +1,1074 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Configuring and Using the Twisted Web Server</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Configuring and Using the Twisted Web Server</h1>
+ <div class="toc"><ol><li><a href="#auto0">Twisted Web Development</a></li><ul><li><a href="#auto1">Main Concepts</a></li><li><a href="#auto2">Site Objects</a></li><li><a href="#auto3">Resource objects</a></li><li><a href="#auto4">Resource Trees</a></li><li><a href="#auto5">.rpy scripts</a></li><li><a href="#auto6">Resource rendering</a></li><li><a href="#auto7">Session</a></li><li><a href="#auto8">Proxies and reverse proxies</a></li></ul><li><a href="#auto9">Advanced Configuration</a></li><ul><li><a href="#auto10">Adding Children</a></li><li><a href="#auto11">Modifying File Resources</a></li><li><a href="#auto12">Virtual Hosts</a></li><li><a href="#auto13">Advanced Techniques</a></li></ul><li><a href="#auto14">Running a Twisted Web Server</a></li><ul><li><a href="#auto15">Serving Flat HTML</a></li><li><a href="#auto16">Resource Scripts</a></li><li><a href="#auto17">Web UIs</a></li><li><a href="#auto18">Spreadable Web Servers</a></li><li><a href="#auto19">Serving PHP/Perl/CGI</a></li><li><a href="#auto20">Serving WSGI Applications</a></li><li><a href="#auto21">Using VHostMonster</a></li></ul><li><a href="#auto22">Rewriting URLs</a></li><li><a href="#auto23">Knowing When We're Not Wanted</a></li><li><a href="#auto24">As-Is Serving</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Twisted Web Development<a name="auto0"/></h2><a name="development" shape="rect"/>
+
+<p>Twisted Web serves Python objects that implement the interface
+IResource.</p>
+
+<br clear="none"/><img alt="Twisted Web process" src="../img/web-process.png"/>
+
+<h3>Main Concepts<a name="auto1"/></h3>
+
+<ul>
+
+<li><a href="#sites" shape="rect">Site Objects</a> are responsible for
+creating <code>HTTPChannel</code> instances to parse the HTTP request,
+and begin the object lookup process. They contain the root Resource,
+the resource which represents the URL <code>/</code> on the site.</li>
+
+<li><a href="#resources" shape="rect">Resource</a> objects represent a single URL segment. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.IResource.html" title="twisted.web.resource.IResource">IResource</a></code> interface describes the methods a Resource object must implement in order to participate in the object publishing process.</li>
+
+<li><a href="#trees" shape="rect">Resource trees</a> are arrangements of Resource objects into a Resource tree. Starting at the root Resource object, the tree of Resource objects defines the URLs which will be valid.</li>
+
+<li><a href="#rpys" shape="rect">.rpy scripts</a> are python scripts which the twisted.web static file server will execute, much like a CGI. However, unlike CGI they must create a Resource object which will be rendered when the URL is visited.</li>
+
+<li><a href="#rendering" shape="rect">Resource rendering</a> occurs when Twisted Web locates a leaf Resource object. A Resource can either return an html string or write to the request object.</li>
+
+<li><a href="#sessions" shape="rect">Session</a> objects allow you to store information across multiple requests. Each individual browser using the system has a unique Session instance.</li>
+
+</ul>
+
+<p>The Twisted Web server is started through the Twisted Daemonizer, as in:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web
+</pre>
+
+<h3>Site Objects<a name="auto2"/></h3>
+<a name="sites" shape="rect"/>
+
+<p>Site objects serve as the glue between a port to listen for HTTP requests on, and a root Resource object.</p>
+
+<p>When using <code>twistd -n web --path /foo/bar/baz</code>, a Site object is created with a root Resource that serves files out of the given path.</p>
+
+<p>You can also create a <code>Site</code> instance by hand, passing
+it a <code>Resource</code> object which will serve as the root of the
+site:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">server</span>, <span class="py-src-variable">resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Simple</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">isLeaf</span> = <span class="py-src-variable">True</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;Hello, world!&lt;/html&gt;&quot;</span>
+
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">Simple</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8080</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h3>Resource objects<a name="auto3"/></h3>
+<a name="resources" shape="rect"/>
+
+<p><code>Resource</code> objects represent a single URL segment of a site. During URL parsing, <code>getChild</code> is called on the current <code>Resource</code> to produce the next <code>Resource</code> object.</p>
+
+<p>When the leaf Resource is reached, either because there were no more URL segments or a Resource had isLeaf set to True, the leaf Resource is rendered by calling <code>render(request)</code>. See <q>Resource Rendering</q> below for more about this.</p>
+
+<p>During the Resource location process, the URL segments which have already been processed and those which have not yet been processed are available in <code>request.prepath</code> and <code>request.postpath</code>.</p>
+
+<p>A Resource can know where it is in the URL tree by looking at <code>request.prepath</code>, a list of URL segment strings.</p>
+
+<p>A Resource can know which path segments will be processed after it by looking at <code>request.postpath</code>.</p>
+
+<p>If the URL ends in a slash, for example <code>http://example.com/foo/bar/</code>, the final URL segment will be an empty string. Resources can thus know if they were requested with or without a final slash.</p>
+
+<p>Here is a simple Resource object:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Hello</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">isLeaf</span> = <span class="py-src-variable">True</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">name</span> == <span class="py-src-string">''</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">Resource</span>.<span class="py-src-variable">getChild</span>(<span class="py-src-variable">self</span>, <span class="py-src-variable">name</span>, <span class="py-src-variable">request</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Hello, world! I am located at %r.&quot;</span> % (<span class="py-src-variable">request</span>.<span class="py-src-variable">prepath</span>,)
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">Hello</span>()
+</pre>
+
+<h3>Resource Trees<a name="auto4"/></h3>
+<a name="trees" shape="rect"/>
+
+<p>Resources can be arranged in trees using <code>putChild</code>. <code>putChild</code> puts a Resource instance into another Resource instance, making it available at the given path segment name:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-variable">root</span> = <span class="py-src-variable">Hello</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'fred'</span>, <span class="py-src-variable">Hello</span>())
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'bob'</span>, <span class="py-src-variable">Hello</span>())
+</pre>
+
+<p>If this root resource is served as the root of a Site instance, the following URLs will all be valid:</p>
+
+<ul>
+<li><code>http://example.com/</code></li>
+<li><code>http://example.com/fred</code></li>
+<li><code>http://example.com/bob</code></li>
+<li><code>http://example.com/fred/</code></li>
+<li><code>http://example.com/bob/</code></li>
+
+</ul>
+
+<h3>.rpy scripts<a name="auto5"/></h3>
+<a name="rpys" shape="rect"/>
+
+<p>Files with the extension <code>.rpy</code> are python scripts which, when placed in a directory served by Twisted Web, will be executed when visited through the web.</p>
+
+<p>An <code>.rpy</code> script must define a variable, <code>resource</code>, which is the Resource object that will render the request.</p>
+
+<p><code>.rpy</code> files are very convenient for rapid development and prototyping. Since they are executed on every web request, defining a Resource subclass in an <code>.rpy</code> will make viewing the results of changes to your class visible simply by refreshing the page:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;Hello, world!&lt;/html&gt;&quot;</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">MyResource</span>()
+</pre>
+
+<p>However, it is often a better idea to define Resource subclasses in Python modules. In order for changes in modules to be visible, you must either restart the Python process, or reload the module:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">myresource</span>
+
+<span class="py-src-comment">## Comment out this line when finished debugging</span>
+<span class="py-src-variable">reload</span>(<span class="py-src-variable">myresource</span>)
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">myresource</span>.<span class="py-src-variable">MyResource</span>()
+</pre>
+
+<p>Creating a Twisted Web server which serves a directory is easy:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd -n web --path /Users/dsp/Sites
+</pre>
+
+<h3>Resource rendering<a name="auto6"/></h3>
+<a name="rendering" shape="rect"/>
+
+<p>Resource rendering occurs when Twisted Web locates a leaf Resource object to handle a web request. A Resource's <code>render</code> method may do various things to produce output which will be sent back to the browser:</p>
+
+<ul>
+<li>Return a string</li>
+<li>Call <code>request.write(&quot;stuff&quot;)</code> as many times as desired, then call <code>request.finish()</code> and return <code>server.NOT_DONE_YET</code> (This is deceptive, since you are in fact done with the request, but is the correct way to do this)</li>
+
+<li>Request a <code>Deferred</code>, return <code>server.NOT_DONE_YET</code>, and call <code>request.write(&quot;stuff&quot;)</code> and <code>request.finish()</code> later, in a callback on the <code>Deferred</code>.</li>
+</ul>
+
+<p>
+
+The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>
+class, which is usually what one's Resource classes subclass, has a
+convenient default implementation
+of <code class="python">render</code>. It will call a method
+named <code class="python">self.render_METHOD</code>
+where <q>METHOD</q> is whatever HTTP method was used to request this
+resource. Examples: request_GET, request_POST, request_HEAD, and so
+on. It is recommended that you have your resource classes
+subclass <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>
+and implement <code class="python">render_METHOD</code> methods as
+opposed to <code class="python">render</code> itself. Note that for
+certain resources, <code class="python">request_POST =
+request_GET</code> may be desirable in case one wants to process
+arguments passed to the resource regardless of whether they used GET
+(<code>?foo=bar&amp;baz=quux</code>, and so forth) or POST.
+
+</p>
+
+<h3>Session<a name="auto7"/></h3>
+<a name="sessions" shape="rect"/>
+
+<p>HTTP is a stateless protocol; every request-response is treated as an individual unit, distinguishable from any other request only by the URL requested. With the advent of Cookies in the mid nineties, dynamic web servers gained the ability to distinguish between requests coming from different <em>browser sessions</em> by sending a Cookie to a browser. The browser then sends this cookie whenever it makes a request to a web server, allowing the server to track which requests come from which browser session.</p>
+
+<p>Twisted Web provides an abstraction of this browser-tracking behavior called the <em>Session object</em>. Calling <code>request.getSession()</code> checks to see if a session cookie has been set; if not, it creates a unique session id, creates a Session object, stores it in the Site, and returns it. If a session object already exists, the same session object is returned. In this way, you can store data specific to the session in the session object.</p>
+
+<img src="../img/web-session.png"/>
+
+<h3>Proxies and reverse proxies<a name="auto8"/></h3>
+<a name="proxies" shape="rect"/>
+
+<p>A proxy is a general term for a server that functions as an intermediary
+between clients and other servers.</p>
+
+<p>Twisted supports two main proxy variants: a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.Proxy.html" title="twisted.web.proxy.Proxy">Proxy</a></code> and a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ReverseProxy.html" title="twisted.web.proxy.ReverseProxy">ReverseProxy</a></code>.</p>
+
+<h4>Proxy</h4>
+
+<p>A proxy forwards requests made by a client to a destination server. Proxies
+typically sit on the internal network for a client or out on the internet, and
+have many uses, including caching, packet filtering, auditing, and circumventing
+local access restrictions to web content.</p>
+
+<p>Here is an example of a simple but complete web proxy:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proxy</span>, <span class="py-src-variable">http</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ProxyFactory</span>(<span class="py-src-parameter">http</span>.<span class="py-src-parameter">HTTPFactory</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">buildProtocol</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">addr</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">proxy</span>.<span class="py-src-variable">Proxy</span>()
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8080</span>, <span class="py-src-variable">ProxyFactory</span>())
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>With this proxy running, you can configure your web browser to use
+<code>localhost:8080</code> as a proxy. After doing so, when browsing the web
+all requests will go through this proxy.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.Proxy.html" title="twisted.web.proxy.Proxy">Proxy</a></code> inherits
+from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.HTTPChannel.html" title="twisted.web.http.HTTPChannel">http.HTTPChannel</a></code>. Each client
+request to the proxy generates a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ProxyRequest.html" title="twisted.web.proxy.ProxyRequest">ProxyRequest</a></code> from the proxy to the destination
+server on behalf of the client. <code>ProxyRequest</code> uses
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ProxyClientFactory.html" title="twisted.web.proxy.ProxyClientFactory">ProxyClientFactory</a></code> to create
+an instance of the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ProxyClient.html" title="twisted.web.proxy.ProxyClient">ProxyClient</a></code>
+protocol for the connection. <code>ProxyClient</code> inherits
+from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.HTTPClient.html" title="twisted.web.http.HTTPClient">http.HTTPClient</a></code>. Subclass <code>ProxyRequest</code> to
+customize the way requests are processed or logged.</p>
+
+<h4>ReverseProxyResource</h4>
+
+<p>A reverse proxy retrieves resources from other servers on behalf of a
+client. Reverse proxies typically sit inside the server's internal network and
+are used for caching, application firewalls, and load balancing.</p>
+
+<p>Here is an example of a basic reverse proxy:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proxy</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">proxy</span>.<span class="py-src-variable">ReverseProxyResource</span>(<span class="py-src-string">'www.yahoo.com'</span>, <span class="py-src-number">80</span>, <span class="py-src-string">''</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8080</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>With this reverse proxy running locally, you can
+visit <code>http://localhost:8080</code> in your web browser, and the reverse
+proxy will proxy your connection to
+<code>www.yahoo.com</code>.</p>
+
+<p>In this example we use <code base="twisted.web">server.Site</code> to serve
+a <code base="twisted.web.proxy">ReverseProxyResource</code> directly. There is
+also a <code>ReverseProxy</code> family of classes
+in <code>twisted.web.proxy</code> mirroring those of the <code>Proxy</code>
+family:</p>
+
+<p>Like <code>Proxy</code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ReverseProxy.html" title="twisted.web.proxy.ReverseProxy">ReverseProxy</a></code> inherits
+from <code>http.HTTPChannel</code>. Each client request to the reverse proxy
+generates a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ReverseProxyRequest.html" title="twisted.web.proxy.ReverseProxyRequest">ReverseProxyRequest</a></code> to the destination
+server. Like <code>ProxyRequest</code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ReverseProxyRequest.html" title="twisted.web.proxy.ReverseProxyRequest">ReverseProxyRequest</a></code> uses a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ProxyClientFactory.html" title="twisted.web.proxy.ProxyClientFactory">ProxyClientFactory</a></code> to create an instance of
+the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.proxy.ProxyClient.html" title="twisted.web.proxy.ProxyClient">ProxyClient</a></code> protocol for
+the connection.</p>
+
+<p>Additional examples of proxies and reverse proxies can be found in
+the <a href="../examples/index.html" shape="rect">Twisted web examples</a>.</p>
+
+<h2>Advanced Configuration<a name="auto9"/></h2>
+
+<p>Non-trivial configurations of Twisted Web are achieved with Python
+configuration files. This is a Python snippet which builds up a
+variable called application. Usually,
+a <code>twisted.application.internet.TCPServer</code>
+instance will be used to make the application listen on a TCP port
+(80, in case direct web serving is desired), with the listener being
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">twisted.web.server.Site</a></code>. The resulting file
+can then be run with <code class="shell">twistd
+-y</code>. Alternatively a reactor object can be used directly to make
+a runnable script.</p>
+
+<p>The <code>Site</code> will wrap a <code>Resource</code> object -- the
+root.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<p>Most advanced configurations will be in the form of tweaking the
+root resource object.</p>
+
+<h3>Adding Children<a name="auto10"/></h3>
+
+<p>Usually, the root's children will be based on the filesystem's contents.
+It is possible to override the filesystem by explicit <code>putChild</code>
+methods.</p>
+
+<p>Here are two examples. The first one adds a <code>/doc</code> child
+to serve the documentation of the installed packages, while the second
+one adds a <code>cgi-bin</code> directory for CGI scripts.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;doc&quot;</span>, <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/usr/share/doc&quot;</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">twcgi</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;cgi-bin&quot;</span>, <span class="py-src-variable">twcgi</span>.<span class="py-src-variable">CGIDirectory</span>(<span class="py-src-string">&quot;/var/www/cgi-bin&quot;</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h3>Modifying File Resources<a name="auto11"/></h3>
+
+<p><code>File</code> resources, be they root object or children
+thereof, have two important attributes that often need to be
+modified: <code>indexNames</code>
+and <code>processors</code>. <code>indexNames</code> determines which
+files are treated as <q>index files</q> -- served up when a directory
+is rendered. <code>processors</code> determine how certain file
+extensions are treated.</p>
+
+<p>Here is an example for both, creating a site where all <code>.rpy</code>
+extensions are Resource Scripts, and which renders directories by
+searching for a <code>index.rpy</code> file.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">script</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">indexNames</span>=[<span class="py-src-string">'index.rpy'</span>]
+<span class="py-src-variable">root</span>.<span class="py-src-variable">processors</span> = {<span class="py-src-string">'.rpy'</span>: <span class="py-src-variable">script</span>.<span class="py-src-variable">ResourceScript</span>}
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<p><code>File</code> objects also have a method called <code>ignoreExt</code>.
+This method can be used to give extension-less URLs to users, so that
+implementation is hidden. Here is an example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">script</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">ignoreExt</span>(<span class="py-src-string">&quot;.rpy&quot;</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">processors</span> = {<span class="py-src-string">'.rpy'</span>: <span class="py-src-variable">script</span>.<span class="py-src-variable">ResourceScript</span>}
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<p>Now, a URL such as <code>/foo</code> might be served from a Resource
+Script called <code>foo.rpy</code>, if no file by the name of <code>foo</code>
+exists.</p>
+
+<h3>Virtual Hosts<a name="auto12"/></h3>
+
+<p>Virtual hosting is done via a special resource, that should be used
+as the root resource
+-- <code>NameVirtualHost</code>. <code>NameVirtualHost</code> has an
+attribute named <code>default</code>, which holds the default
+website. If a different root for some other name is desired,
+the <code>addHost</code> method should be called.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">vhost</span>, <span class="py-src-variable">script</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">vhost</span>.<span class="py-src-variable">NameVirtualHost</span>()
+
+<span class="py-src-comment"># Add a default -- htdocs</span>
+<span class="py-src-variable">root</span>.<span class="py-src-variable">default</span>=<span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+
+<span class="py-src-comment"># Add a simple virtual host -- foo.com</span>
+<span class="py-src-variable">root</span>.<span class="py-src-variable">addHost</span>(<span class="py-src-string">&quot;foo.com&quot;</span>, <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/foo&quot;</span>))
+
+<span class="py-src-comment"># Add a simple virtual host -- bar.com</span>
+<span class="py-src-variable">root</span>.<span class="py-src-variable">addHost</span>(<span class="py-src-string">&quot;bar.com&quot;</span>, <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/bar&quot;</span>))
+
+<span class="py-src-comment"># The &quot;baz&quot; people want to use Resource Scripts in their web site</span>
+<span class="py-src-variable">baz</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/baz&quot;</span>)
+<span class="py-src-variable">baz</span>.<span class="py-src-variable">processors</span> = {<span class="py-src-string">'.rpy'</span>: <span class="py-src-variable">script</span>.<span class="py-src-variable">ResourceScript</span>}
+<span class="py-src-variable">baz</span>.<span class="py-src-variable">ignoreExt</span>(<span class="py-src-string">'.rpy'</span>)
+<span class="py-src-variable">root</span>.<span class="py-src-variable">addHost</span>(<span class="py-src-string">'baz'</span>, <span class="py-src-variable">baz</span>)
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<h3>Advanced Techniques<a name="auto13"/></h3>
+
+<p>Since the configuration is a Python snippet, it is possible to
+use the full power of Python. Here are some simple examples:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-comment"># No need for configuration of virtual hosts -- just make sure</span>
+<span class="py-src-comment"># a directory /var/vhosts/&lt;vhost name&gt; exists:</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">vhost</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">vhost</span>.<span class="py-src-variable">NameVirtualHost</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">default</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+<span class="py-src-keyword">for</span> <span class="py-src-variable">dir</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">os</span>.<span class="py-src-variable">listdir</span>(<span class="py-src-string">&quot;/var/vhosts&quot;</span>):
+ <span class="py-src-variable">root</span>.<span class="py-src-variable">addHost</span>(<span class="py-src-variable">dir</span>, <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-variable">os</span>.<span class="py-src-variable">path</span>.<span class="py-src-variable">join</span>(<span class="py-src-string">&quot;/var/vhosts&quot;</span>, <span class="py-src-variable">dir</span>)))
+
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-comment"># Determine ports we listen on based on a file with numbers:</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">vhost</span>, <span class="py-src-variable">static</span>, <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/var/www/htdocs&quot;</span>)
+
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web'</span>)
+<span class="py-src-variable">serviceCollection</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+
+<span class="py-src-keyword">for</span> <span class="py-src-variable">num</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">map</span>(<span class="py-src-variable">int</span>, <span class="py-src-variable">open</span>(<span class="py-src-string">&quot;/etc/web/ports&quot;</span>).<span class="py-src-variable">read</span>().<span class="py-src-variable">split</span>()):
+ <span class="py-src-variable">serviceCollection</span>.<span class="py-src-variable">addCollection</span>(<span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-variable">num</span>, <span class="py-src-variable">site</span>))
+</pre>
+
+
+<h2>Running a Twisted Web Server<a name="auto14"/></h2>
+
+<p>In many cases, you'll end up repeating common usage patterns of
+twisted.web. In those cases you'll probably want to use Twisted's
+pre-configured web server setup.</p>
+
+<p>The easiest way to run a Twisted Web server is with the Twisted Daemonizer.
+For example, this command will run a web server which serves static files from
+a particular directory:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web --path /path/to/web/content
+</pre>
+
+<p>If you just want to serve content from your own home directory, the
+following will do:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web --path ~/public_html/
+</pre>
+
+<p>You can stop the server at any time by going back to the directory you
+started it in and running the command:</p>
+
+<pre class="shell" xml:space="preserve">
+% kill `cat twistd.pid`
+</pre>
+
+<p> Some other configuration options are available as well: </p>
+
+<ul>
+ <li> <code>--port</code>: Specify the port for the web
+ server to listen on. This defaults to 8080. </li>
+ <li> <code>--logfile</code>: Specify the path to the
+ log file. </li>
+</ul>
+
+<p> The full set of options that are available can be seen with: </p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web --help
+</pre>
+
+<h3>Serving Flat HTML<a name="auto15"/></h3>
+
+<p> Twisted Web serves flat HTML files just as it does any other flat file. </p>
+
+<a name="ResourceScripts" shape="rect"/>
+<h3>Resource Scripts<a name="auto16"/></h3>
+
+<p> A Resource script is a Python file ending with the extension <code>.rpy</code>, which is required to create an instance of a (subclass of a) <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">twisted.web.resource.Resource</a></code>. </p>
+
+<p> Resource scripts have 3 special variables: </p>
+
+<ul>
+ <li> <code class="py-src-identifier">__file__</code>: The name of the .rpy file, including the full path. This variable is automatically defined and present within the namespace. </li>
+ <li> <code class="py-src-identifier">registry</code>: An object of class <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.Registry.html" title="twisted.web.static.Registry">static.Registry</a></code>. It can be used to access and set persistent data keyed by a class.</li>
+ <li> <code class="py-src-identifier">resource</code>: The variable which must be defined by the script and set to the resource instance that will be used to render the page. </li>
+</ul>
+
+<p> A very simple Resource Script might look like: </p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyGreatResource</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;foo&lt;/html&gt;&quot;</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">MyGreatResource</span>()
+</pre>
+
+<p> A slightly more complicated resource script, which accesses some
+persistent data, might look like:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">SillyWeb</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Counter</span>
+
+<span class="py-src-variable">counter</span> = <span class="py-src-variable">registry</span>.<span class="py-src-variable">getComponent</span>(<span class="py-src-variable">Counter</span>)
+<span class="py-src-keyword">if</span> <span class="py-src-keyword">not</span> <span class="py-src-variable">counter</span>:
+ <span class="py-src-variable">registry</span>.<span class="py-src-variable">setComponent</span>(<span class="py-src-variable">Counter</span>, <span class="py-src-variable">Counter</span>())
+<span class="py-src-variable">counter</span> = <span class="py-src-variable">registry</span>.<span class="py-src-variable">getComponent</span>(<span class="py-src-variable">Counter</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">MyResource</span>(<span class="py-src-parameter">resource</span>.<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">counter</span>.<span class="py-src-variable">increment</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;you are visitor %d&quot;</span> % <span class="py-src-variable">counter</span>.<span class="py-src-variable">getValue</span>()
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">MyResource</span>()
+</pre>
+
+<p> This is assuming you have the <code>SillyWeb.Counter</code> module,
+implemented something like the following:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Counter</span>:
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">value</span> = <span class="py-src-number">0</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">increment</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">value</span> += <span class="py-src-number">1</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getValue</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">value</span>
+</pre>
+
+<h3>Web UIs<a name="auto17"/></h3>
+
+<p>
+The <a href="https://launchpad.net/nevow" shape="rect">Nevow</a> framework, available as
+part of the <a href="https://launchpad.net/quotient" shape="rect">Quotient</a> project,
+is an advanced system for giving Web UIs to your application. Nevow uses Twisted Web but is
+not itself part of Twisted.
+</p>
+
+<a name="SpreadableWebServers" shape="rect"/>
+<h3>Spreadable Web Servers<a name="auto18"/></h3>
+
+<p> One of the most interesting applications of Twisted Web is the distributed webserver; multiple servers can all answer requests on the same port, using the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.spread.html" title="twisted.spread">twisted.spread</a></code> package for <q>spreadable</q> computing. In two different directories, run the commands: </p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web --user
+% twistd web --personal [other options, if you desire]
+</pre>
+
+<p> Once you're running both of these instances, go to <code>http://localhost:8080/your_username.twistd/</code> -- you will see the front page from the server you created with the <code>--personal</code> option. What's happening here is that the request you've sent is being relayed from the central (User) server to your own (Personal) server, over a PB connection. This technique can be highly useful for small <q>community</q> sites; using the code that makes this demo work, you can connect one HTTP port to multiple resources running with different permissions on the same machine, on different local machines, or even over the internet to a remote site. </p>
+
+<p>
+By default, a personal server listens on a UNIX socket in the owner's home
+directory. The <code class="shell">--port</code> option can be used to make
+it listen on a different address, such as a TCP or SSL server or on a UNIX
+server in a different location. If you use this option to make a personal
+server listen on a different address, the central (User) server won't be
+able to find it, but a custom server which uses the same APIs as the central
+server might. Another use of the <code class="shell">--port</code> option
+is to make the UNIX server robust against system crashes. If the server
+crashes and the UNIX socket is left on the filesystem, the personal server
+will not be able to restart until it is removed. However, if <code class="shell">--port unix:/home/username/.twistd-web-pb:wantPID=1</code> is
+supplied when creating the personal server, then a lockfile will be used to
+keep track of whether the server socket is in use and automatically delete
+it when it is not.
+</p>
+
+<h3>Serving PHP/Perl/CGI<a name="auto19"/></h3>
+
+<p>Everything related to CGI is located in
+the <code>twisted.web.twcgi</code>, and it's here you'll find the
+classes that you need to subclass in order to support the language of
+your (or somebody elses) taste. You'll also need to create your own
+kind of resource if you are using a non-unix operating system (such as
+Windows), or if the default resources has wrong pathnames to the
+parsers.</p>
+
+<p>The following snippet is a .rpy that serves perl-files. Look at <code>twisted.web.twcgi</code>
+for more examples regarding twisted.web and CGI.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">static</span>, <span class="py-src-variable">twcgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PerlScript</span>(<span class="py-src-parameter">twcgi</span>.<span class="py-src-parameter">FilteredScript</span>):
+ <span class="py-src-variable">filter</span> = <span class="py-src-string">'/usr/bin/perl'</span> <span class="py-src-comment"># Points to the perl parser</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">static</span>.<span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/perlsite&quot;</span>) <span class="py-src-comment"># Points to the perl website</span>
+<span class="py-src-variable">resource</span>.<span class="py-src-variable">processors</span> = {<span class="py-src-string">&quot;.pl&quot;</span>: <span class="py-src-variable">PerlScript</span>} <span class="py-src-comment"># Files that end with .pl will be</span>
+ <span class="py-src-comment"># processed by PerlScript</span>
+<span class="py-src-variable">resource</span>.<span class="py-src-variable">indexNames</span> = [<span class="py-src-string">'index.pl'</span>]
+</pre>
+
+<h3>Serving WSGI Applications<a name="auto20"/></h3>
+
+<p><a href="http://wsgi.org/wsgi" shape="rect">WSGI</a> is the Web Server Gateway
+Interface. It is a specification for web servers and application servers to
+communicate with Python web applications. All modern Python web frameworks
+support the WSGI interface.</p>
+
+<p>The easiest way to get started with WSGI application is to use the twistd
+command:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd -n web --wsgi=helloworld.application
+</pre>
+
+<p>This assumes that you have a WSGI application called application in
+your helloworld module/package, which might look like this:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">application</span>(<span class="py-src-parameter">environ</span>, <span class="py-src-parameter">start_response</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Basic WSGI Application&quot;&quot;&quot;</span>
+ <span class="py-src-variable">start_response</span>(<span class="py-src-string">'200 OK'</span>, [(<span class="py-src-string">'Content-type'</span>,<span class="py-src-string">'text/plain'</span>)])
+ <span class="py-src-keyword">return</span> [<span class="py-src-string">'Hello World!'</span>]
+</pre>
+
+<p>The above setup will be suitable for many applications where all that is
+needed is to server the WSGI application at the site's root. However, for
+greater control, Twisted provides support for using WSGI applications as
+resources <code class="api">twisted.web.wsgi.WSGIResource</code>.</p>
+
+<p>Here is an example of a WSGI application being served as the root resource
+for a site, in the following tac file:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">wsgi</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">WSGIResource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">threadpool</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">ThreadPool</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">service</span>, <span class="py-src-variable">strports</span>
+
+<span class="py-src-comment"># Create and start a thread pool,</span>
+<span class="py-src-variable">wsgiThreadPool</span> = <span class="py-src-variable">ThreadPool</span>()
+<span class="py-src-variable">wsgiThreadPool</span>.<span class="py-src-variable">start</span>()
+
+<span class="py-src-comment"># ensuring that it will be stopped when the reactor shuts down</span>
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">addSystemEventTrigger</span>(<span class="py-src-string">'after'</span>, <span class="py-src-string">'shutdown'</span>, <span class="py-src-variable">wsgiThreadPool</span>.<span class="py-src-variable">stop</span>)
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">application</span>(<span class="py-src-parameter">environ</span>, <span class="py-src-parameter">start_response</span>):
+ <span class="py-src-string">&quot;&quot;&quot;A basic WSGI application&quot;&quot;&quot;</span>
+ <span class="py-src-variable">start_response</span>(<span class="py-src-string">'200 OK'</span>, [(<span class="py-src-string">'Content-type'</span>,<span class="py-src-string">'text/plain'</span>)])
+ <span class="py-src-keyword">return</span> [<span class="py-src-string">'Hello World!'</span>]
+
+<span class="py-src-comment"># Create the WSGI resource</span>
+<span class="py-src-variable">wsgiAppAsResource</span> = <span class="py-src-variable">WSGIResource</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">wsgiThreadPool</span>, <span class="py-src-variable">application</span>)
+
+<span class="py-src-comment"># Hooks for twistd</span>
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'Twisted.web.wsgi Hello World Example'</span>)
+<span class="py-src-variable">server</span> = <span class="py-src-variable">strports</span>.<span class="py-src-variable">service</span>(<span class="py-src-string">'tcp:8080'</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">wsgiAppAsResource</span>))
+<span class="py-src-variable">server</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">application</span>)
+</pre>
+
+<p>This can then be run like any other .tac file:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd -ny myapp.tac
+</pre>
+
+<p>Because of the synchronous nature of WSGI, each application call (for
+each request) is called within a thread, and the result is written back to the
+web server. For this, a <code class="api">twisted.python.threadpool.ThreadPool</code>
+instance is used.</p>
+
+<h3>Using VHostMonster<a name="auto21"/></h3>
+
+<p>It is common to use one server (for example, Apache) on a site with multiple
+names which then uses reverse proxy (in Apache, via <code>mod_proxy</code>) to different
+internal web servers, possibly on different machines. However, naive
+configuration causes miscommunication: the internal server firmly believes it
+is running on <q>internal-name:port</q>, and will generate URLs to that effect,
+which will be completely wrong when received by the client.</p>
+
+<p>While Apache has the ProxyPassReverse directive, it is really a hack
+and is nowhere near comprehensive enough. Instead, the recommended practice
+in case the internal web server is Twisted Web is to use VHostMonster.</p>
+
+<p>From the Twisted side, using VHostMonster is easy: just drop a file named
+(for example) <code>vhost.rpy</code> containing the following:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">vhost</span>
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">vhost</span>.<span class="py-src-variable">VHostMonsterResource</span>()
+</pre>
+
+<p>Make sure the web server is configured with the correct processors
+for the <code>rpy</code> extensions (the web server <code>twistd web
+--path</code> generates by default is so configured).</p>
+
+<p>From the Apache side, instead of using the following ProxyPass directive:</p>
+
+<pre xml:space="preserve">
+&lt;VirtualHost ip-addr&gt;
+ProxyPass / http://localhost:8538/
+ServerName example.com
+&lt;/VirtualHost&gt;
+</pre>
+
+<p>Use the following directive:</p>
+
+<pre xml:space="preserve">
+&lt;VirtualHost ip-addr&gt;
+ProxyPass / http://localhost:8538/vhost.rpy/http/example.com:80/
+ServerName example.com
+&lt;/VirtualHost&gt;
+</pre>
+
+<p>Here is an example for Twisted Web's reverse proxy:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">application</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">internet</span>, <span class="py-src-variable">service</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">proxy</span>, <span class="py-src-variable">server</span>, <span class="py-src-variable">vhost</span>
+<span class="py-src-variable">vhostName</span> = <span class="py-src-string">'example.com'</span>
+<span class="py-src-variable">reverseProxy</span> = <span class="py-src-variable">proxy</span>.<span class="py-src-variable">ReverseProxyResource</span>(<span class="py-src-string">'internal'</span>, <span class="py-src-number">8538</span>,
+ <span class="py-src-string">'/vhost.rpy/http/'</span>+<span class="py-src-variable">vhostName</span>+<span class="py-src-string">'/'</span>)
+<span class="py-src-variable">root</span> = <span class="py-src-variable">vhost</span>.<span class="py-src-variable">NameVirtualHost</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">addHost</span>(<span class="py-src-variable">vhostName</span>, <span class="py-src-variable">reverseProxy</span>)
+<span class="py-src-variable">site</span> = <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">application</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">Application</span>(<span class="py-src-string">'web-proxy'</span>)
+<span class="py-src-variable">sc</span> = <span class="py-src-variable">service</span>.<span class="py-src-variable">IServiceCollection</span>(<span class="py-src-variable">application</span>)
+<span class="py-src-variable">i</span> = <span class="py-src-variable">internet</span>.<span class="py-src-variable">TCPServer</span>(<span class="py-src-number">80</span>, <span class="py-src-variable">site</span>)
+<span class="py-src-variable">i</span>.<span class="py-src-variable">setServiceParent</span>(<span class="py-src-variable">sc</span>)
+</pre>
+
+<h2>Rewriting URLs<a name="auto22"/></h2>
+
+<p>Sometimes it is convenient to modify the content of
+the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">Request</a></code> object
+before passing it on. Because this is most often used to rewrite
+either the URL, the similarity to Apache's <code>mod_rewrite</code>
+has inspired the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.rewrite.html" title="twisted.web.rewrite">twisted.web.rewrite</a></code>
+module. Using this module is done via wrapping a resource with
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.rewrite.RewriterResource.html" title="twisted.web.rewrite.RewriterResource">twisted.web.rewrite.RewriterResource</a></code> which
+then has rewrite rules. Rewrite rules are functions which accept a
+request object, and possible modify it. After all rewrite rules run,
+the child resolution chain continues as if the wrapped resource,
+rather than the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.rewrite.RewriterResource.html" title="twisted.web.rewrite.RewriterResource">RewriterResource</a></code>, was the child.</p>
+
+<p>Here is an example, using the only rule currently supplied by Twisted
+itself:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">default_root</span> = <span class="py-src-variable">rewrite</span>.<span class="py-src-variable">RewriterResource</span>(<span class="py-src-variable">default</span>, <span class="py-src-variable">rewrite</span>.<span class="py-src-variable">tildeToUsers</span>)
+</pre>
+
+<p>This causes the URL <code>/~foo/bar.html</code> to be treated
+like <code>/users/foo/bar.html</code>. If done after setting
+default's <code>users</code> child to a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.distrib.UserDirectory.html" title="twisted.web.distrib.UserDirectory">distrib.UserDirectory</a></code>, it gives a
+configuration similar to the classical configuration of web server,
+common since the first NCSA servers.</p>
+
+<h2>Knowing When We're Not Wanted<a name="auto23"/></h2>
+
+<p>Sometimes it is useful to know when the other side has broken the connection.
+Here is an example which does that:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">util</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">println</span>
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExampleResource</span>(<span class="py-src-parameter">Resource</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;hello world&quot;</span>)
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">request</span>.<span class="py-src-variable">notifyFinish</span>()
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-keyword">lambda</span> <span class="py-src-variable">_</span>: <span class="py-src-variable">println</span>(<span class="py-src-string">&quot;finished normally&quot;</span>))
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">println</span>, <span class="py-src-string">&quot;error&quot;</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">10</span>, <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">server</span>.<span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">ExampleResource</span>()
+</pre>
+
+<p>This will allow us to run statistics on the log-file to see how many users
+are frustrated after merely 10 seconds.</p>
+
+<h2>As-Is Serving<a name="auto24"/></h2>
+
+<p>Sometimes, you want to be able to send headers and status
+directly. While you can do this with a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.script.ResourceScript.html" title="twisted.web.script.ResourceScript">ResourceScript</a></code>, an easier way is to
+use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.ASISProcessor.html" title="twisted.web.static.ASISProcessor">ASISProcessor</a></code>.
+Use it by, for example, adding it as a processor for
+the <code>.asis</code> extension. Here is a sample file:</p>
+
+<pre xml:space="preserve">
+HTTP/1.0 200 OK
+Content-Type: text/html
+
+Hello world
+</pre>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-development.html b/doc/web/howto/web-development.html
new file mode 100644
index 0000000..373d237
--- /dev/null
+++ b/doc/web/howto/web-development.html
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Web Application Development</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Web Application Development</h1>
+ <div class="toc"><ol><li><a href="#auto0">Code layout</a></li><li><a href="#auto1">Web application deployment</a></li><li><a href="#auto2">Understanding resource scripts (.rpy files)</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Code layout<a name="auto0"/></h2>
+
+<p>The development of a Twisted Web application should be orthogonal to its
+deployment. This means is that if you are developing a web application, it
+should be a resource with children, and internal links. Some of the children
+might use <a href="http://www.divmod.org/projects/nevow" shape="rect">Nevow</a>, some
+might be resources manually using <code>.write</code>, and so on. Regardless,
+the code should be in a Python module, or package, <em>outside</em> the web
+tree.</p>
+
+<p>You will probably want to test your application as you develop it. There are
+many ways to test, including dropping an <code>.rpy</code> which looks
+like:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">mypackage</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">toplevel</span>
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">toplevel</span>.<span class="py-src-variable">Resource</span>(<span class="py-src-variable">file</span>=<span class="py-src-string">&quot;foo/bar&quot;</span>, <span class="py-src-variable">color</span>=<span class="py-src-string">&quot;blue&quot;</span>)
+</pre>
+
+<p>into a directory, and then running:</p>
+
+<pre class="shell" xml:space="preserve">
+% twistd web --path=/directory
+</pre>
+
+<p>You can also write a Python script like:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-comment">#!/usr/bin/env python</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">server</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">mypackage</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">toplevel</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8080</span>,
+ <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">toplevel</span>.<span class="py-src-variable">Resource</span>(<span class="py-src-variable">file</span>=<span class="py-src-string">&quot;foo/bar&quot;</span>, <span class="py-src-variable">color</span>=<span class="py-src-string">&quot;blue&quot;</span>)))
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<h2>Web application deployment<a name="auto1"/></h2>
+
+<p>Which one of these development strategies you use is not terribly important,
+since (and this is the important part) deployment is <em>orthogonal</em>.
+Later, when you want users to actually <em>use</em> your code, you should worry
+about what to do -- or rather, don't. Users may have widely different needs.
+Some may want to run your code in a different process, so they'll use
+distributed web (<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.distrib.html" title="twisted.web.distrib">twisted.web.distrib</a></code>). Some may be
+using the <code>twisted-web</code> Debian package, and will drop in:</p>
+
+<pre class="shell" xml:space="preserve">
+% cat &gt; /etc/local.d/99addmypackage.py
+from mypackage import toplevel
+default.putChild(&quot;mypackage&quot;, toplevel.Resource(file=&quot;foo/bar&quot;, color=&quot;blue&quot;))
+^D
+</pre>
+
+<p>If you want to be friendly to your users, you can supply many examples in
+your package, like the above <code>.rpy</code> and the Debian-package drop-in.
+But the <em>ultimate</em> friendliness is to write a useful resource which does
+not have deployment assumptions built in.</p>
+
+<h2>Understanding resource scripts (<code>.rpy</code> files)<a name="auto2"/></h2>
+
+<p>Twisted Web is not PHP -- it has better tools for organizing code Python
+modules and packages, so use them. In PHP, the only tool for organizing code is
+a web page, which leads to silly things like PHP pages full of functions that
+other pages import, and so on. If you were to write your code this way with
+Twisted Web, you would do web development using many <code>.rpy</code> files,
+all importing some Python module. This is a <em>bad idea</em> -- it mashes
+deployment with development, and makes sure your users will be <em>tied</em> to
+the file-system.</p>
+
+<p>We have <code>.rpy</code>s because they are useful and necessary.
+But using them incorrectly leads to horribly unmaintainable
+applications. The best way to ensure you are using them correctly is
+to not use them at all, until you are on your <em>final</em>
+deployment stages. You should then find your <code>.rpy</code> files
+will be less than 10 lines, because you will not <em>have</em> more
+than 10 lines to write.</p>
+
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/asynchronous-deferred.html b/doc/web/howto/web-in-60/asynchronous-deferred.html
new file mode 100644
index 0000000..89c58b4
--- /dev/null
+++ b/doc/web/howto/web-in-60/asynchronous-deferred.html
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Asynchronous Responses (via Deferred)</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Asynchronous Responses (via Deferred)</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The previous example had a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code> that generates its response
+asynchronously rather than immediately upon the call to its render
+method. Though it was a useful demonstration of the <code>NOT_DONE_YET</code>
+feature of Twisted Web, the example didn't reflect what a realistic application
+might want to do. This example introduces <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code>, the Twisted class which is used
+to provide a uniform interface to many asynchronous events, and shows you an
+example of using a <code>Deferred</code>-returning API to generate an
+asynchronous response to a request in Twisted Web.</p>
+
+<p><code>Deferred</code> is the result of two consequences of the
+asynchronous programming approach. First, asynchronous code is
+frequently (if not always) concerned with some data (in Python, an
+object) which is not yet available but which probably will be
+soon. Asynchronous code needs a way to define what will be done to the
+object once it does exist. It also needs a way to define how to handle
+errors in the creation or acquisition of that object. These two needs
+are satisfied by the <i>callbacks</i> and <i>errbacks</i> of
+a <code>Deferred</code>. Callbacks are added to
+a <code>Deferred</code> with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.addCallback.html" title="twisted.internet.defer.Deferred.addCallback">Deferred.addCallback</a></code>; errbacks
+are added with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.addErrback.html" title="twisted.internet.defer.Deferred.addErrback">Deferred.addErrback</a></code>. When the
+object finally does exist, it is passed to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.callback.html" title="twisted.internet.defer.Deferred.callback">Deferred.callback</a></code> which passes it
+on to the callback added with <code>addCallback</code>. Similarly, if
+an error occurs, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.errback.html" title="twisted.internet.defer.Deferred.errback">Deferred.errback</a></code> is called and
+the error is passed along to the errback added
+with <code>addErrback</code>. Second, the events that make
+asynchronous code actually work often take many different,
+incompatible forms. <code>Deferred</code> acts as the uniform
+interface which lets different parts of an asynchronous application
+interact and isolates them from implementation details they shouldn't
+be concerned with.</p>
+
+<p>That's almost all there is to <code>Deferred</code>. To solidify your new
+understanding, now consider this rewritten version
+of <code>DelayedResource</code> which uses a <code>Deferred</code>-based delay
+API. It does exactly the same thing as the <a href="asynchronous.html" shape="rect">previous
+example</a>. Only the implementation is different.</p>
+
+<p>First, the example must import that new API that was just mentioned, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.deferLater.html" title="twisted.internet.task.deferLater">deferLater</a></code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">task</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">deferLater</span>
+</pre>
+
+<p>Next, all the other imports (these are the same as last time):</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<p>With the imports done, here's the first part of
+the <code>DelayedResource</code> implementation. Again, this part of
+the code is identical to the previous version:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+</pre>
+
+<p>Next we need to define the render method. Here's where things
+change a bit. Instead of using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTime.callLater.html" title="twisted.internet.interfaces.IReactorTime.callLater">callLater</a></code>,
+We're going to use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.task.deferLater.html" title="twisted.internet.task.deferLater">deferLater</a></code> this
+time. <code>deferLater</code> accepts a reactor, delay (in seconds, as
+with <code>callLater</code>), and a function to call after the delay
+to produce that elusive object discussed in the description
+of <code>Deferred</code>s. We're also going to
+use <code>_delayedRender</code> as the callback to add to
+the <code>Deferred</code> returned by <code>deferLater</code>. Since
+it expects the request object as an argument, we're going to set up
+the <code>deferLater</code> call to return a <code>Deferred</code>
+which has the request object as its result.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">deferLater</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">5</span>, <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">request</span>)
+</pre>
+
+<p>The <code>Deferred</code> referenced by <code>d</code> now needs to
+have the <code>_delayedRender</code> callback added to it. Once this
+is done, <code>_delayedRender</code> will be called with the result
+of <code>d</code> (which will be <code>request</code>, of course — the
+result of <code>(lambda: request)()</code>).</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p>...
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>)
+</pre>
+
+<p>Finally, the render method still needs to return <code>NOT_DONE_YET</code>,
+for exactly the same reasons as it did in the previous version of the
+example.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p>...
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+</pre>
+
+<p>And with that, <code>DelayedResource</code> is now implemented
+based on a <code>Deferred</code>. The example still isn't very
+realistic, but remember that since <code>Deferred</code>s offer a
+uniform interface to many different asynchronous event sources, this
+code now resembles a real application even more closely; you could
+easily replace <code>deferLater</code> with
+another <code>Deferred</code>-returning API and suddenly you might
+have a resource that does something useful.</p>
+
+<p>Finally, here's the complete, uninterrupted example source, as an rpy script:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span>.<span class="py-src-variable">task</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">deferLater</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">d</span> = <span class="py-src-variable">deferLater</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-number">5</span>, <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">request</span>)
+ <span class="py-src-variable">d</span>.<span class="py-src-variable">addCallback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">DelayedResource</span>()
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/asynchronous.html b/doc/web/howto/web-in-60/asynchronous.html
new file mode 100644
index 0000000..c49999d
--- /dev/null
+++ b/doc/web/howto/web-in-60/asynchronous.html
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Asynchronous Responses</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Asynchronous Responses</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>In all of the previous examples, the resource examples presented generated
+responses immediately. One of the features of prime interest of Twisted Web,
+though, is the ability to generate a response over a longer period of time while
+leaving the server free to respond to other requests. In other words,
+asynchronously. In this installment, we'll write a resource like this.</p>
+
+<p>A resource that generates a response asynchronously looks like one that
+generates a response synchronously in many ways. The same base
+class, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>, is used
+either way; the same render methods are used. There are three basic differences,
+though.</p>
+
+<p>First, instead of returning the string which will be used as the
+body of the response, the resource uses <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.Request.write.html" title="twisted.web.http.Request.write">Request.write</a></code>. This method can be
+called repeatedly. Each call appends another string to the response
+body. Second, when the entire response body has been passed
+to <code>Request.write</code>, the application must
+call <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.Request.finish.html" title="twisted.web.http.Request.finish">Request.finish</a></code>. As you might expect
+from the name, this ends the response. Finally, in order to make
+Twisted Web not end the response as soon as the render method returns,
+the render method must return <code>NOT_DONE_YET</code>. Consider this
+example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>, <span class="py-src-variable">request</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+</pre>
+
+<p>If you're not familiar with the reactor <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IReactorTime.callLater.html" title="twisted.internet.interfaces.IReactorTime.callLater">callLater</a></code>
+method, all you really need to know about it to understand this
+example is that the above usage of it arranges to
+have <code>self._delayedRender(request)</code> run about 5 seconds
+after <code>callLater</code> is invoked from this render method and
+that it returns immediately.</p>
+
+<p>All three of the elements mentioned earlier can be seen in this
+example. The resource uses <code>Request.write</code> to set the
+response body. It uses <code>Request.finish</code> after the entire
+body has been specified (all with just one call to write in this
+case). Lastly, it returns <code>NOT_DONE_YET</code> from its render
+method. So there you have it, asynchronous rendering with Twisted
+Web.</p>
+
+<p>Here's a complete rpy script based on this resource class (see the <a href="rpy-scripts.html" shape="rect">previous example</a> if you need a reminder about rpy
+scripts):</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>, <span class="py-src-variable">request</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">DelayedResource</span>()
+</pre>
+
+<p>Drop this source into a <code>.rpy</code> file and fire up a server
+using <code>twistd -n web --path /directory/containing/script/.</code>
+You'll see that loading the page takes 5 seconds. If you try to load a
+second before the first completes, it will also take 5 seconds from
+the time you request it (but it won't be delayed by any other
+outstanding requests).</p>
+
+<p>Something else to consider when generating responses asynchronously is that
+the client may not wait around to get the response to its
+request. A <a href="interrupted.html" shape="rect">subsequent example</a> demonstrates how
+to detect that the client has abandoned the request and that the server
+shouldn't bother to finish generating its response.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/custom-codes.html b/doc/web/howto/web-in-60/custom-codes.html
new file mode 100644
index 0000000..05053f1
--- /dev/null
+++ b/doc/web/howto/web-in-60/custom-codes.html
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Custom Response Codes</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Custom Response Codes</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The previous example introduced <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.error.NoResource.html" title="twisted.web.error.NoResource">NoResource</a></code>, a Twisted Web error resource which
+responds with a 404 (not found) code. This example will cover the APIs
+that <code>NoResource</code> uses to do this so that you can generate your own
+custom response codes as desired.</p>
+
+<p>First, the now-standard import preamble:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<p>Now we'll define a new resource class that always returns a 402 (payment
+required) response. This is really not very different from the resources that
+was defined in previous examples. The fact that it has a response code other
+than 200 doesn't change anything else about its role. This will require using
+the request object, though, which none of the previous examples have done.</p>
+
+<p>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">Request</a></code> object has
+shown up in a couple of places, but so far we've ignored it. It is a parameter
+to the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.getChild.html" title="twisted.web.resource.Resource.getChild">getChild</a></code>
+API as well as to render methods such as <code>render_GET</code>. As you might
+have suspected, it represents the request for which a response is to be
+generated. Additionally, it also represents the response being generated. In
+this example we're going to use its <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.Request.setResponseCode.html" title="twisted.web.http.Request.setResponseCode">setResponseCode</a></code> method to - you guessed
+it - set the response's status code.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">PaymentRequired</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">setResponseCode</span>(<span class="py-src-number">402</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Please swipe your credit card.&lt;/body&gt;&lt;/html&gt;&quot;</span>
+</pre>
+
+<p>Just like the other resources I've demonstrated, this one returns a
+string from its <code>render_GET</code> method to define the body of
+the response. All that's different is the call
+to <code>setResponseCode</code> to override the default response code,
+200, with a different one.</p>
+
+<p>Finally, the code to set up the site and reactor. We'll put an instance of
+the above defined resource at <code>/buy</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;buy&quot;</span>, <span class="py-src-variable">PaymentRequired</span>())
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Here's the complete example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PaymentRequired</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">setResponseCode</span>(<span class="py-src-number">402</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Please swipe your credit card.&lt;/body&gt;&lt;/html&gt;&quot;</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;buy&quot;</span>, <span class="py-src-variable">PaymentRequired</span>())
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Run the server and visit <code>http://localhost:8880/buy</code> in your
+browser. It'll look pretty boring, but if you use Firefox's View Page Info
+right-click menu item (or your browser's equivalent), you'll be able to see that
+the server indeed sent back a 402 response code.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/dynamic-content.html b/doc/web/howto/web-in-60/dynamic-content.html
new file mode 100644
index 0000000..c099039
--- /dev/null
+++ b/doc/web/howto/web-in-60/dynamic-content.html
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Generating a Page Dynamically</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Generating a Page Dynamically</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The goal of this example is to show you how to dynamically generate the
+contents of a page.</p>
+
+<p>Taking care of some of the necessary imports first, we'll import <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code> and the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+</pre>
+
+<p>The Site is a factory which associates a listening port with the HTTP
+protocol implementation. The reactor is the main loop that drives any Twisted
+application; we'll use it to actually create the listening port in a moment.</p>
+
+<p>Next, we'll import one more thing from Twisted
+Web: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>. An
+instance of <code>Resource</code> (or a subclass) represents a page
+(technically, the entity addressed by a URI).</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+</pre>
+
+<p>Since we're going to make the demo resource a clock, we'll also import the
+time module:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+</pre>
+
+<p>With imports taken care of, the next step is to define
+a <code>Resource</code> subclass which has the dynamic rendering
+behavior we want. Here's a resource which generates a page giving the
+time:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ClockPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">isLeaf</span> = <span class="py-src-variable">True</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;%s&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">time</span>.<span class="py-src-variable">ctime</span>(),)
+</pre>
+
+<p>Setting <code>isLeaf</code> to <code>True</code> indicates
+that <code>ClockPage</code> resources will never have any
+children.</p>
+
+<p>The <code>render_GET</code> method here will be called whenever the URI we
+hook this resource up to is requested with the <code>GET</code> method. The byte
+string it returns is what will be sent to the browser.</p>
+
+<p>With the resource defined, we can create a <code>Site</code> from it:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">resource</span> = <span class="py-src-variable">ClockPage</span>()
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>)
+</pre>
+
+<p>Just as with the previous static content example, this
+configuration puts our resource at the very top of the URI hierarchy,
+ie at <code>/</code>. With that <code>Site</code> instance, we can
+tell the reactor to <a href="../../../core/howto/servers.html" shape="rect">create
+a TCP server</a> and start servicing requests:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Here's the code with no interruptions:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClockPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">isLeaf</span> = <span class="py-src-variable">True</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;%s&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">time</span>.<span class="py-src-variable">ctime</span>(),)
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">ClockPage</span>()
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/dynamic-dispatch.html b/doc/web/howto/web-in-60/dynamic-dispatch.html
new file mode 100644
index 0000000..cabd2da
--- /dev/null
+++ b/doc/web/howto/web-in-60/dynamic-dispatch.html
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Dynamic URL Dispatch</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Dynamic URL Dispatch</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>In the <a href="static-dispatch.html" shape="rect">previous example</a> we covered how to
+statically configure Twisted Web to serve different content at different
+URLs. The goal of this example is to show you how to do this dynamically
+instead. Reading the previous installment if you haven't already is suggested in
+order to get an overview of how URLs are treated when using Twisted Web's <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.html" title="twisted.web.resource">resource</a></code> APIs.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code> (the object which
+associates a listening server port with the HTTP implementation), <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code> (a convenient base class
+to use when defining custom pages), and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code> (the object which implements the Twisted
+main loop) return once again:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<p>With that out of the way, here's the interesting part of this
+example. We're going to define a resource which renders a whole-year
+calendar. The year it will render the calendar for will be the year in
+the request URL. So, for example, <code>/2009</code> will render a
+calendar for 2009. First, here's a resource that renders a calendar
+for the year passed to its initializer:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+9
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">calendar</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">calendar</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">YearPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">year</span>):
+ <span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">year</span> = <span class="py-src-variable">year</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;&lt;pre&gt;%s&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">calendar</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">year</span>),)
+</pre>
+
+<p>Pretty simple - not all that different from the first dynamic resource
+demonstrated in <a href="dynamic-content.html" shape="rect">Generating a Page
+Dynamically</a>. Now here's the resource that handles URLs with a year in them
+by creating a suitable instance of this <code>YearPage</code> class:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Calendar</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">YearPage</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">name</span>))
+</pre>
+
+<p>By implementing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.getChild.html" title="twisted.web.resource.Resource.getChild">getChild</a></code> here, we've just defined
+how Twisted Web should find children of <code>Calendar</code> instances when
+it's resolving an URL into a resource. This implementation defines all integers
+as the children of <code>Calendar</code> (and punts on error handling, more on
+that later).</p>
+
+<p>All that's left is to create a <code>Site</code> using this resource as its
+root and then start the reactor:</p>
+
+<pre xml:space="preserve">
+root = Calendar()
+factory = Site(root)
+reactor.listenTCP(8880, factory)
+reactor.run()
+</pre>
+
+<p>And that's all. Any resource-based dynamic URL handling is going to look
+basically like <code>Calendar.getPage</code>. Here's the full example code:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">calendar</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">calendar</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">YearPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">year</span>):
+ <span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">year</span> = <span class="py-src-variable">year</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;&lt;pre&gt;%s&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">calendar</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">year</span>),)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Calendar</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">YearPage</span>(<span class="py-src-variable">int</span>(<span class="py-src-variable">name</span>))
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Calendar</span>()
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/error-handling.html b/doc/web/howto/web-in-60/error-handling.html
new file mode 100644
index 0000000..6d56d3b
--- /dev/null
+++ b/doc/web/howto/web-in-60/error-handling.html
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Error Handling</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Error Handling</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>In this example we'll extend dynamic dispatch to return a 404 (not found)
+response when a client requests a non-existent URL.</p>
+
+<p>As in the previous examples, we'll start with <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code>, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code> imports:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<p>Next, we'll add one more import. <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.error.NoResource.html" title="twisted.web.error.NoResource">NoResource</a></code> is one of the pre-defined error
+resources provided by Twisted Web. It generates the necessary 404 response code
+and renders a simple html page telling the client there is no such resource.</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">error</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NoResource</span>
+</pre>
+
+<p>Next, we'll define a custom resource which does some dynamic URL
+dispatch. This example is going to be just like
+the <a href="dynamic-dispatch.html" shape="rect">previous one</a>, where the path segment is
+interpreted as a year; the difference is that this time we'll handle requests
+which don't conform to that pattern by returning the not found response:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">Calendar</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">year</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">name</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">ValueError</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NoResource</span>()
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">YearPage</span>(<span class="py-src-variable">year</span>)
+</pre>
+
+<p>Aside from including the definition of <code>YearPage</code> from
+the previous example, the only other thing left to do is the
+normal <code>Site</code> and <code>reactor</code> setup. Here's the
+complete code for this example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">error</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NoResource</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">calendar</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">calendar</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">YearPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">year</span>):
+ <span class="py-src-variable">Resource</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">year</span> = <span class="py-src-variable">year</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;&lt;pre&gt;%s&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">calendar</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">year</span>),)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Calendar</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">getChild</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">name</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-variable">year</span> = <span class="py-src-variable">int</span>(<span class="py-src-variable">name</span>)
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">ValueError</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NoResource</span>()
+ <span class="py-src-keyword">else</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">YearPage</span>(<span class="py-src-variable">year</span>)
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Calendar</span>()
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>This server hands out the same calendar views as the one from the previous
+installment, but it will also hand out a nice error page with a 404 response
+when a request is made for a URL which cannot be interpreted as a year.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/handling-posts.html b/doc/web/howto/web-in-60/handling-posts.html
new file mode 100644
index 0000000..244f6b5
--- /dev/null
+++ b/doc/web/howto/web-in-60/handling-posts.html
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Handling POSTs</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Handling POSTs</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>All of the previous examples have focused on <code>GET</code>
+requests. Unlike <code>GET</code> requests, <code>POST</code> requests can have
+a request body - extra data after the request headers; for example, data
+representing the contents of an HTML form. Twisted Web makes this data available
+to applications via the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">Request</a></code> object.</p>
+
+<p>Here's an example web server which renders a static HTML form and then
+generates a dynamic page when that form is posted back to it. Disclaimer: While
+it's convenient for this example, it's often not a good idea to make a resource
+that <code>POST</code>s to itself; this isn't about Twisted Web, but the nature
+of HTTP in general; if you do this in a real application, make sure you
+understand the possible negative consequences.</p>
+
+<p>As usual, we start with some imports. In addition to the Twisted imports,
+this example uses the <code>cgi</code> module to <a href="http://en.wikipedia.org/wiki/Cross-site_scripting" shape="rect">escape user-entered
+content</a> for inclusion in the output.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+</pre>
+
+<p>Next, we'll define a resource which is going to do two things. First, it will
+respond to <code>GET</code> requests with a static HTML form:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">FormPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;html&gt;&lt;body&gt;&lt;form method=&quot;POST&quot;&gt;&lt;input name=&quot;the-field&quot; type=&quot;text&quot; /&gt;&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;'</span>
+</pre>
+
+<p>This is similar to the resource used in a <a href="dynamic-content.html" shape="rect">previous installment</a>. However, we'll now add
+one more method to give it a second behavior; this <code>render_POST</code>
+method will allow it to accept <code>POST</code> requests:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_POST</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;html&gt;&lt;body&gt;You submitted: %s&lt;/body&gt;&lt;/html&gt;'</span> % (<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">args</span>[<span class="py-src-string">&quot;the-field&quot;</span>][<span class="py-src-number">0</span>]),)
+</pre>
+
+<p>The main thing to note here is the use
+of <code>request.args</code>. This is a dictionary-like object that
+provides access to the contents of the form. The keys in this
+dictionary are the names of inputs in the form. Each value is a list
+containing strings (since there can be multiple inputs with the same
+name), which is why we had to extract the first element to pass
+to <code>cgi.escape</code>. <code>request.args</code> will be
+populated from form contents whenever a <code>POST</code> request is
+made with a content type
+of <code>application/x-www-form-urlencoded</code>
+or <code>multipart/form-data</code> (it's also populated by query
+arguments for any type of request).</p>
+
+<p>Finally, the example just needs the usual site creation and port setup:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p><span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;form&quot;</span>, <span class="py-src-variable">FormPage</span>())
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Run the server and
+visit <a href="http://localhost:8880/form" shape="rect">http://localhost:8880/form</a>,
+submit the form, and watch it generate a page including the value you entered
+into the single field.</p>
+
+<p>Here's the complete source for the example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">import</span> <span class="py-src-variable">cgi</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">FormPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;html&gt;&lt;body&gt;&lt;form method=&quot;POST&quot;&gt;&lt;input name=&quot;the-field&quot; type=&quot;text&quot; /&gt;&lt;/form&gt;&lt;/body&gt;&lt;/html&gt;'</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_POST</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'&lt;html&gt;&lt;body&gt;You submitted: %s&lt;/body&gt;&lt;/html&gt;'</span> % (<span class="py-src-variable">cgi</span>.<span class="py-src-variable">escape</span>(<span class="py-src-variable">request</span>.<span class="py-src-variable">args</span>[<span class="py-src-string">&quot;the-field&quot;</span>][<span class="py-src-number">0</span>]),)
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;form&quot;</span>, <span class="py-src-variable">FormPage</span>())
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/http-auth.html b/doc/web/howto/web-in-60/http-auth.html
new file mode 100644
index 0000000..97d4581
--- /dev/null
+++ b/doc/web/howto/web-in-60/http-auth.html
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: HTTP Authentication</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">HTTP Authentication</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>Many of the previous examples have looked at how to serve content by using
+existing resource classes or implementing new ones. In this example we'll use
+Twisted Web's basic or digest HTTP authentication to control access to these
+resources.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.guard.html" title="twisted.web.guard">guard</a></code>, the Twisted Web
+module which provides most of the APIs that will be used in this
+example, helps you to
+add <a href="http://en.wikipedia.org/wiki/Authentication" shape="rect">authentication</a>
+and <a href="http://en.wikipedia.org/wiki/Authorization" shape="rect">authorization</a>
+to a resource hierarchy. It does this by providing a resource which
+implements <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.getChild.html" title="twisted.web.resource.Resource.getChild">getChild</a></code> to return
+a <a href="dynamic-dispatch.html" shape="rect">dynamically selected
+resource</a>. The selection is based on the authentication headers in
+the request. If those headers indicate that the request is made on
+behalf of Alice, then Alice's resource will be returned. If they
+indicate that it was made on behalf of Bob, his will be returned. If
+the headers contain invalid credentials, an error resource is
+returned. Whatever happens, once this resource is returned, URL
+traversal continues as normal from that resource.</p>
+
+<p>The resource that implements this is <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.guard.HTTPAuthSessionWrapper.html" title="twisted.web.guard.HTTPAuthSessionWrapper">HTTPAuthSessionWrapper</a></code>, though it is directly
+responsible for very little of the process. It will extract headers from the
+request and hand them off to a credentials factory to parse them according to
+the appropriate standards (eg <a href="http://tools.ietf.org/html/rfc2617" shape="rect">HTTP
+Authentication: Basic and Digest Access Authentication</a>) and then hand the
+resulting credentials object off to a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.Portal.html" title="twisted.cred.portal.Portal">Portal</a></code>, the core
+of <a href="../../../core/howto/cred.html" shape="rect">Twisted Cred</a>, a system for
+uniform handling of authentication and authorization. We won't discuss Twisted
+Cred in much depth here. To make use of it with Twisted Web, the only thing you
+really need to know is how to implement an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.html" title="twisted.cred.portal.IRealm">IRealm</a></code>.</p>
+
+<p>You need to implement a realm because the realm is the object that
+actually decides which resources are used for which users. This can be
+as complex or as simple as it suitable for your application. For this
+example we'll keep it very simple: each user will have a resource
+which is a static file listing of the <code>public_html</code>
+directory in their UNIX home directory. First, we need to
+import <code>implements</code> from <code>zope.interface</code>
+and <code>IRealm</code>
+from <code>twisted.cred.portal</code>. Together these will let me mark
+this class as a realm (this is mostly - but not entirely - a
+documentation thing). We'll also need <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.File.html" title="twisted.web.static.File">File</a></code> for the actual implementation
+later.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IRealm</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PublicHTMLRealm</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IRealm</span>)
+</pre>
+
+<p>A realm only needs to implement one method: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.portal.IRealm.requestAvatar.html" title="twisted.cred.portal.IRealm.requestAvatar">requestAvatar</a></code>. This method is called
+after any successful authentication attempt (ie, Alice supplied the right
+password). Its job is to return the <i>avatar</i> for the user who succeeded in
+authenticating. An <i>avatar</i> is just an object that represents a user. In
+this case, it will be a <code>File</code>. In general, with <code>Guard</code>,
+the avatar must be a resource of some sort.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">IResource</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-keyword">return</span> (<span class="py-src-variable">IResource</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/home/%s/public_html&quot;</span> % (<span class="py-src-variable">avatarId</span>,)), <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">None</span>)
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>()
+</pre>
+
+<p>A few notes on this method:</p>
+<ul>
+ <li>The <code>avatarId</code> parameter is essentially the username. It's the
+ job of some other code to extract the username from the request headers and
+ make sure it gets passed here.</li>
+ <li>The <code>mind</code> is always <code>None</code> when writing a realm to
+ be used with <code>Guard</code>. You can ignore it until you want to write a
+ realm for something else.</li>
+ <li><code>Guard</code> is always
+ passed <code class="twisted.web.resource">IResource</code> as
+ the <code>interfaces</code> parameter. If <code>interfaces</code> only
+ contains interfaces your code doesn't understand,
+ raising <code>NotImplementedError</code> is the thing to do, as
+ above. You'll only need to worry about getting a different interface when
+ you write a realm for something other than <code>Guard</code>.</li>
+ <li>If you want to track when a user logs out, that's what the last element of
+ the returned tuple is for. It will be called when this avatar logs
+ out. <code>lambda: None</code> is the idiomatic no-op logout function.</li>
+ <li>Notice that the path handling code in this example is written very
+ poorly. This example may be vulnerable to certain unintentional information
+ disclosure attacks. This sort of problem is exactly the
+ reason <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.filepath.FilePath.html" title="twisted.python.filepath.FilePath">FilePath</a></code>
+ exists. However, that's an example for another day...</li>
+</ul>
+
+<p>We're almost ready to set up the resource for this example. To
+create an <code>HTTPAuthSessionWrapper</code>, though, we need two
+things. First, a portal, which requires the realm above, plus at least
+one credentials checker:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">checkers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePasswordDB</span>
+
+<span class="py-src-variable">portal</span> = <span class="py-src-variable">Portal</span>(<span class="py-src-variable">PublicHTMLRealm</span>(), [<span class="py-src-variable">FilePasswordDB</span>(<span class="py-src-string">'httpd.password'</span>)])
+</pre>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.cred.checkers.FilePasswordDB.html" title="twisted.cred.checkers.FilePasswordDB">FilePasswordDB</a></code> is the
+credentials checker. It knows how to read <code>passwd(5)</code>-style (loosely)
+files to check credentials against. It is responsible for the authentication
+work after <code>HTTPAuthSessionWrapper</code> extracts the credentials from the
+request.</p>
+
+<p>Next we need either <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.guard.BasicCredentialFactory.html" title="twisted.web.guard.BasicCredentialFactory">BasicCredentialFactory</a></code>
+or <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.guard.DigestCredentialFactory.html" title="twisted.web.guard.DigestCredentialFactory">DigestCredentialFactory</a></code>. The former
+knows how to challenge HTTP clients to do basic authentication; the
+latter, digest authentication. We'll use digest here:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">guard</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">DigestCredentialFactory</span>
+
+<span class="py-src-variable">credentialFactory</span> = <span class="py-src-variable">DigestCredentialFactory</span>(<span class="py-src-string">&quot;md5&quot;</span>, <span class="py-src-string">&quot;example.org&quot;</span>)
+</pre>
+
+<p>The two parameters to this constructor are the hash algorithm and
+the HTTP authentication realm which will be used. The only other valid
+hash algorithm is &quot;sha&quot; (but be careful, MD5 is more widely supported
+than SHA). The HTTP authentication realm is mostly just a string that
+is presented to the user to let them know why they're authenticating
+(you can read more about this in
+the <a href="http://tools.ietf.org/html/rfc2617" shape="rect">RFC</a>).</p>
+
+<p>With those things created, we can finally
+instantiate <code>HTTPAuthSessionWrapper</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">guard</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">HTTPAuthSessionWrapper</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">HTTPAuthSessionWrapper</span>(<span class="py-src-variable">portal</span>, [<span class="py-src-variable">credentialFactory</span>])
+</pre>
+
+<p>There's just one last thing that needs to be done
+here. When <a href="rpy-scripts.html" shape="rect">rpy scripts</a> were
+introduced, it was mentioned that they are evaluated in an unusual
+context. This is the first example that actually needs to take this
+into account. It so happens that <code>DigestCredentialFactory</code>
+instances are stateful. Authentication will only succeed if the same
+instance is used to both generate challenges and examine the responses
+to those challenges. However, the normal mode of operation for an rpy
+script is for it to be re-executed for every request. This leads to a
+new <code>DigestCredentialFactory</code> being created for every request, preventing
+any authentication attempt from ever succeeding.</p>
+
+<p>There are two ways to deal with this. First, and the better of the two ways,
+we could move almost all of the code into a real Python module, including the
+code that instantiates the <code>DigestCredentialFactory</code>. This would
+ensure that the same instance was used for every request. Second, and the easier
+of the two ways, we could add a call to <code>cache()</code> to the beginning of
+the rpy script:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">cache</span>()
+</pre>
+
+<p><code>cache</code> is part of the globals of any rpy script, so you don't
+need to import it (it's okay to be cringing at this
+point). Calling <code>cache</code> makes Twisted re-use the result of the first
+evaluation of the rpy script for subsequent requests too - just what we want in
+this case.</p>
+
+<p>Here's the complete example (with imports re-arranged to the more
+conventional style):</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+</p><span class="py-src-variable">cache</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">implements</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">portal</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IRealm</span>, <span class="py-src-variable">Portal</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">cred</span>.<span class="py-src-variable">checkers</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">FilePasswordDB</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">IResource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">guard</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">HTTPAuthSessionWrapper</span>, <span class="py-src-variable">DigestCredentialFactory</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">PublicHTMLRealm</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">IRealm</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">requestAvatar</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">avatarId</span>, <span class="py-src-parameter">mind</span>, *<span class="py-src-parameter">interfaces</span>):
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">IResource</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">interfaces</span>:
+ <span class="py-src-keyword">return</span> (<span class="py-src-variable">IResource</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/home/%s/public_html&quot;</span> % (<span class="py-src-variable">avatarId</span>,)), <span class="py-src-keyword">lambda</span>: <span class="py-src-variable">None</span>)
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">NotImplementedError</span>()
+
+<span class="py-src-variable">portal</span> = <span class="py-src-variable">Portal</span>(<span class="py-src-variable">PublicHTMLRealm</span>(), [<span class="py-src-variable">FilePasswordDB</span>(<span class="py-src-string">'httpd.password'</span>)])
+
+<span class="py-src-variable">credentialFactory</span> = <span class="py-src-variable">DigestCredentialFactory</span>(<span class="py-src-string">&quot;md5&quot;</span>, <span class="py-src-string">&quot;localhost:8080&quot;</span>)
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">HTTPAuthSessionWrapper</span>(<span class="py-src-variable">portal</span>, [<span class="py-src-variable">credentialFactory</span>])
+</pre>
+
+<p>And voila, a password-protected per-user Twisted Web server.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/index.html b/doc/web/howto/web-in-60/index.html
new file mode 100644
index 0000000..e091b18
--- /dev/null
+++ b/doc/web/howto/web-in-60/index.html
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Web In 60 Seconds</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Web In 60 Seconds</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<p>This set of examples contains short, complete applications
+of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.html" title="twisted.web">twisted.web</a></code>. For subjects not covered
+here, see the <a href="../using-twistedweb.html" shape="rect">Twisted Web
+tutorial</a> and the API documentation.</p>
+
+<ol>
+<li><a href="static-content.html" shape="rect">Serving static content from a directory</a></li>
+<li><a href="dynamic-content.html" shape="rect">Generating a page dynamically</a></li>
+<li><a href="static-dispatch.html" shape="rect">Static URL dispatch</a></li>
+<li><a href="dynamic-dispatch.html" shape="rect">Dynamic URL dispatch</a></li>
+<li><a href="error-handling.html" shape="rect">Error handling</a></li>
+<li><a href="custom-codes.html" shape="rect">Custom response codes</a></li>
+<li><a href="handling-posts.html" shape="rect">Handling POSTs</a></li>
+<li><a href="rpy-scripts.html" shape="rect">rpy scripts (or, how to save yourself some typing)</a></li>
+<li><a href="asynchronous.html" shape="rect">Asynchronous responses</a></li>
+<li><a href="asynchronous-deferred.html" shape="rect">Asynchronous responses (via Deferred)</a></li>
+<li><a href="interrupted.html" shape="rect">Interrupted responses</a></li>
+<li><a href="logging-errors.html" shape="rect">Logging errors</a></li>
+<li><a href="wsgi.html" shape="rect">WSGIs</a></li>
+<li><a href="http-auth.html" shape="rect">HTTP authentication</a></li>
+<li><a href="session-basics.html" shape="rect">Session basics</a></li>
+<li><a href="session-store.html" shape="rect">Storing objects in the session</a></li>
+<li><a href="session-endings.html" shape="rect">Session endings</a></li>
+</ol>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/interrupted.html b/doc/web/howto/web-in-60/interrupted.html
new file mode 100644
index 0000000..f4701a3
--- /dev/null
+++ b/doc/web/howto/web-in-60/interrupted.html
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Interrupted Responses</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Interrupted Responses</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The previous example had a Resource that generates its response
+asynchronously rather than immediately upon the call to its render method. When
+generating responses asynchronously, the possibility is introduced that the
+connection to the client may be lost before the response is generated. In such a
+case, it is often desirable to abandon the response generation entirely, since
+there is nothing to do with the data once it is produced. This example shows how
+to be notified that the connection has been lost.</p>
+
+<p>This example will build upon the <a href="asynchronous.html" shape="rect">asynchronous
+responses example</a> which simply (if not very realistically) generated its
+response after a fixed delay. We will expand that resource so that as soon as
+the client connection is lost, the delayed event is cancelled and the response
+is never generated.</p>
+
+<p>The feature this example relies on is provided by another <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">Request</a></code> method: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.Request.notifyFinish.html" title="twisted.web.http.Request.notifyFinish">notifyFinish</a></code>. This method returns a new
+Deferred which will fire with <code>None</code> if the request is successfully
+responded to or with an error otherwise - for example if the connection is lost
+before the response is sent.</p>
+
+<p>The example starts in a familiar way, with the requisite Twisted imports and
+a resource class with the same <code>_delayedRender</code> used previously:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+</pre>
+
+<p>Before defining the render method, we're going to define an errback
+(an errback being a callback that gets called when there's an error),
+though. This will be the errback attached to the <code>Deferred</code>
+returned by <code>Request.notifyFinish</code>. It will cancel the
+delayed call to <code>_delayedRender</code>.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_responseFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">err</span>, <span class="py-src-parameter">call</span>):
+ <span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+</pre>
+
+<p>Finally, the render method will set up the delayed call just as it
+did before, and return <code>NOT_DONE_YET</code> likewise. However, it
+will also use <code>Request.notifyFinish</code> to make
+sure <code>_responseFailed</code> is called if appropriate.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>, <span class="py-src-variable">request</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">notifyFinish</span>().<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_responseFailed</span>, <span class="py-src-variable">call</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+</pre>
+
+<p>Notice that since <code>_responseFailed</code> needs a reference to
+the delayed call object in order to cancel it, we passed that object
+to <code>addErrback</code>. Any additional arguments passed
+to <code>addErrback</code> (or <code>addCallback</code>) will be
+passed along to the errback after the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code> instance which is always
+passed as the first argument. Passing <code>call</code> here means it
+will be passed to <code>_responseFailed</code>, where it is expected
+and required.</p>
+
+<p>That covers almost all the code for this example. Here's the entire example
+without interruptions, as an <a href="rpy-scripts.html" shape="rect">rpy script</a>:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_responseFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">err</span>, <span class="py-src-parameter">call</span>):
+ <span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>, <span class="py-src-variable">request</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">notifyFinish</span>().<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_responseFailed</span>, <span class="py-src-variable">call</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">DelayedResource</span>()
+</pre>
+
+<p>Toss this into <code>example.rpy</code>, fire it up with <code>twistd -n
+web --path .</code>, and
+hit <a href="http://localhost:8080/example.rpy" shape="rect">http://localhost:8080/example.rpy</a>. If
+you wait five seconds, you'll get the page content. If you interrupt the request
+before then, say by hitting escape (in Firefox, at least), then you'll see
+perhaps the most boring demonstration ever - no page content, and nothing in the
+server logs. Success!</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/logging-errors.html b/doc/web/howto/web-in-60/logging-errors.html
new file mode 100644
index 0000000..2440a04
--- /dev/null
+++ b/doc/web/howto/web-in-60/logging-errors.html
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Logging Errors</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Logging Errors</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The <a href="interrupted.html" shape="rect">previous example</a> created a server that
+dealt with response errors by aborting response generation, potentially avoiding
+pointless work. However, it did this silently for any error. In this example,
+we'll modify the previous example so that it logs each failed response.</p>
+
+<p>This example will use the Twisted API for logging errors. As was
+mentioned in the <a href="asynchronous-deferred.html" shape="rect">first example
+covering Deferreds</a>, errbacks are passed an error. In the previous
+example, the <code>_responseFailed</code> errback accepted this error
+as a parameter but ignored it. The only way this example will differ
+is that this <code>_responseFailed</code> will use that error
+parameter to log a message.</p>
+
+<p>This example will require all of the imports required by the previous example
+plus one new import:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>
+</pre>
+
+<p>The only other part of the previous example which changes is
+the <code>_responseFailed</code> callback, which will now log the
+error passed to it:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_responseFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>, <span class="py-src-parameter">call</span>):
+ <span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+ <span class="py-src-variable">err</span>(<span class="py-src-variable">failure</span>, <span class="py-src-string">&quot;Async response demo interrupted response&quot;</span>)
+</pre>
+
+<p>We're passing two arguments to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.log.err.html" title="twisted.python.log.err">err</a></code> here. The first is the error which is being
+passed in to the callback. This is always an object of type <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.failure.Failure.html" title="twisted.python.failure.Failure">Failure</a></code>, a class which represents an
+exception and (sometimes, but not always) a traceback. <code>err</code> will
+format this nicely for the log. The second argument is a descriptive string that
+tells someone reading the log what the source of the error was.</p>
+
+<p>Here's the full example with the two above modifications:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">NOT_DONE_YET</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">log</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">err</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">DelayedResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_delayedRender</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">write</span>(<span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;Sorry to keep you waiting.&lt;/body&gt;&lt;/html&gt;&quot;</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">finish</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_responseFailed</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">failure</span>, <span class="py-src-parameter">call</span>):
+ <span class="py-src-variable">call</span>.<span class="py-src-variable">cancel</span>()
+ <span class="py-src-variable">err</span>(<span class="py-src-variable">failure</span>, <span class="py-src-string">&quot;Async response demo interrupted response&quot;</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">call</span> = <span class="py-src-variable">reactor</span>.<span class="py-src-variable">callLater</span>(<span class="py-src-number">5</span>, <span class="py-src-variable">self</span>.<span class="py-src-variable">_delayedRender</span>, <span class="py-src-variable">request</span>)
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">notifyFinish</span>().<span class="py-src-variable">addErrback</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">_responseFailed</span>, <span class="py-src-variable">call</span>)
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">NOT_DONE_YET</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">DelayedResource</span>()
+</pre>
+
+<p>Run this server as in the <a href="interrupted.html" shape="rect">previous example</a>
+and interrupt a request. Unlike the previous example, where the server gave no
+indication that this had happened, you'll see a message in the log output with
+this version.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/rpy-scripts.html b/doc/web/howto/web-in-60/rpy-scripts.html
new file mode 100644
index 0000000..7649771
--- /dev/null
+++ b/doc/web/howto/web-in-60/rpy-scripts.html
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: rpy scripts (or, how to save yourself some typing)</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">rpy scripts (or, how to save yourself some typing)</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The goal of this installment is to show you another way to run a Twisted Web
+server with a custom resource which doesn't require as much code as the previous
+examples.</p>
+
+<p>The feature in question is called an <code>rpy script</code>. An rpy script
+is a Python source file which defines a resource and can be loaded into a
+Twisted Web server. The advantages of this approach are that you don't have to
+write code to create the site or set up a listening port with the reactor. That
+means fewer lines of code that aren't dedicated to the task you're trying to
+accomplish.</p>
+
+<p>There are some disadvantages, though. An rpy script must have the
+extension <code>.rpy</code>. This means you can't import it using the
+usual Python import statement. This means it's hard to re-use code in
+an rpy script. This also means you can't easily unit test it. The code
+in an rpy script is evaluated in an unusual context. So, while rpy
+scripts may be useful for testing out ideas, they're not recommend for
+much more than that.</p>
+
+<p>Okay, with that warning out of the way, let's dive in. First, as mentioned,
+rpy scripts are Python source files with the <code>.rpy</code> extension. So,
+open up an appropriately named file (for example, <code>example.rpy</code>) and
+put this code in it:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ClockPage</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">isLeaf</span> = <span class="py-src-variable">True</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&lt;html&gt;&lt;body&gt;%s&lt;/body&gt;&lt;/html&gt;&quot;</span> % (<span class="py-src-variable">time</span>.<span class="py-src-variable">ctime</span>(),)
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">ClockPage</span>()
+</pre>
+
+<p>You may recognize this as the resource from
+the <a href="dynamic-content.html" shape="rect">first dynamic rendering
+example</a>. What's different is what you don't see: we didn't
+import <code>reactor</code> or <code>Site</code>. There are no calls
+to <code>listenTCP</code> or <code>run</code>. Instead, and this is
+the core idea for rpy scripts, we just bound the
+name <code>resource</code> to the resource we want the script to
+serve. Every rpy script must bind this name, and this name is the only
+thing Twisted Web will pay attention to in an rpy script.</p>
+
+<p>All that's left is to drop this rpy script into a Twisted Web server. There
+are a few ways to do this. The simplest way is with <code>twistd</code>:</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd -n web --path .
+</pre>
+
+<p>Hit
+<a href="http://localhost:8080/example.rpy" shape="rect">http://localhost:8080/example.rpy</a>
+to see it run. You can pass other arguments here too. <code>twistd web</code>
+has options for specifying which port number to bind, whether to set up an HTTPS
+server, and plenty more. Other options you can pass to <code>twistd</code> allow
+you to configure logging to work differently, to select a different reactor,
+etc. For a full list of options, see <code>twistd --help</code> and <code>twistd
+web --help</code>.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/session-basics.html b/doc/web/howto/web-in-60/session-basics.html
new file mode 100644
index 0000000..d468a0e
--- /dev/null
+++ b/doc/web/howto/web-in-60/session-basics.html
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Session Basics</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Session Basics</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>Sessions are the most complicated topic covered in this series of examples,
+and because of that it is going to take a few examples to cover all of the
+different aspects. This first example demonstrates the very basics of the
+Twisted Web session API: how to get the session object for the current request
+and how to prematurely expire a session.</p>
+
+<p>Before diving into the APIs, let's look at the big picture of
+sessions in Twisted Web. Sessions are represented by instances
+of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Session.html" title="twisted.web.server.Session">Session</a></code>. The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code> creates a new instance
+of <code>Session</code> the first time an application asks for it for
+a particular session. <code>Session</code> instances are kept on
+the <code>Site</code> instance until they expire (due to inactivity or
+because they are explicitly expired). Each time after the first that a
+particular session's <code>Session</code> object is requested, it is
+retrieved from the <code>Site</code>.</p>
+
+<p>With the conceptual underpinnings of the upcoming API in place, here comes
+the example. This will be a very simple <a href="rpy-scripts.html" shape="rect">rpy
+script</a> which tells a user what its unique session identifier is and lets it
+prematurely expire the session.</p>
+
+<p>First, we'll import <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code> so we can define a couple of
+subclasses of it:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+</pre>
+
+<p>Next we'll define the resource which tells the client what its session
+identifier is. This is done easily by first getting the session object
+using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.getSession.html" title="twisted.web.server.Request.getSession">Request.getSession</a></code> and
+then getting the session object's uid attribute:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ShowSession</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Your session id is: '</span> + <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>().<span class="py-src-variable">uid</span>
+</pre>
+
+<p>To let the client expire its own session before it times out, we'll define
+another resource which expires whatever session it is requested with. This is
+done using the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Session.expire.html" title="twisted.web.server.Session.expire">Session.expire</a></code>
+method:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">class</span> <span class="py-src-identifier">ExpireSession</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>().<span class="py-src-variable">expire</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Your session has been expired.'</span>
+</pre>
+
+<p>Finally, to make the example an rpy script, we'll make an instance
+of <code>ShowSession</code> and give it an instance
+of <code>ExpireSession</code> as a child using <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.putChild.html" title="twisted.web.resource.Resource.putChild">Resource.putChild</a></code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-variable">resource</span> = <span class="py-src-variable">ShowSession</span>()
+<span class="py-src-variable">resource</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;expire&quot;</span>, <span class="py-src-variable">ExpireSession</span>())
+</pre>
+
+<p>And that is the complete example. You can fire this up and load the top
+page. You'll see a (rather opaque) session identifier that remains the same
+across reloads (at least until you flush the <code>TWISTED_SESSION</code> cookie
+from your browser or enough time passes). You can then visit
+the <code>expire</code> child and go back to the top page and see that you have
+a new session.</p>
+
+<p>Here's the complete source for the example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ShowSession</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Your session id is: '</span> + <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>().<span class="py-src-variable">uid</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExpireSession</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>().<span class="py-src-variable">expire</span>()
+ <span class="py-src-keyword">return</span> <span class="py-src-string">'Your session has been expired.'</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">ShowSession</span>()
+<span class="py-src-variable">resource</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;expire&quot;</span>, <span class="py-src-variable">ExpireSession</span>())
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/session-endings.html b/doc/web/howto/web-in-60/session-endings.html
new file mode 100644
index 0000000..082acd4
--- /dev/null
+++ b/doc/web/howto/web-in-60/session-endings.html
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Session Endings</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Session Endings</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The previous two examples introduced Twisted Web's session APIs. This
+included accessing the session object, storing state on it, and retrieving it
+later, as well as the idea that the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Session.html" title="twisted.web.server.Session">Session</a></code> object has a lifetime which is tied to
+the notional session it represents. This example demonstrates how to exert some
+control over that lifetime and react when it expires.</p>
+
+<p>The lifetime of a session is controlled by the <code>sessionTimeout</code>
+attribute of the <code>Session</code> class. This attribute gives the number of
+seconds a session may go without being accessed before it expires. The default
+is 15 minutes. In this example we'll change that to a different value.</p>
+
+<p>One way to override the value is with a subclass:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Session</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ShortSession</span>(<span class="py-src-parameter">Session</span>):
+ <span class="py-src-variable">sessionTimeout</span> = <span class="py-src-number">60</span>
+</pre>
+
+<p>To have Twisted Web actually make use of this session class, rather
+than the default, it is also necessary to override
+the <code>sessionFactory</code> attribute of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code>. We could do this with another
+subclass, but we could also do it to just one instance
+of <code>Site</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">rootResource</span>)
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">sessionFactory</span> = <span class="py-src-variable">ShortSession</span>
+</pre>
+
+<p>Sessions given out for requests served by this <code>Site</code> will
+use <code>ShortSession</code> and only last one minute without activity.</p>
+
+<p>You can have arbitrary functions run when sessions expire,
+too. This can be useful for cleaning up external resources associated
+with the session, tracking usage statistics, and more. This
+functionality is provided via <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Session.notifyOnExpire.html" title="twisted.web.server.Session.notifyOnExpire">Session.notifyOnExpire</a></code>. It accepts a
+single argument: a function to call when the session expires. Here's a
+trivial example which prints a message whenever a session expires:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExpirationLogger</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">sessions</span> = <span class="py-src-variable">set</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">session</span> = <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>()
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>.<span class="py-src-variable">add</span>(<span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span>)
+ <span class="py-src-variable">session</span>.<span class="py-src-variable">notifyOnExpire</span>(<span class="py-src-keyword">lambda</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">_expired</span>(<span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_expired</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">uid</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Session&quot;</span>, <span class="py-src-variable">uid</span>, <span class="py-src-string">&quot;has expired.&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">uid</span>)
+</pre>
+
+<p>Keep in mind that using a method as the callback will keep the instance (in
+this case, the <code>ExpirationLogger</code> resource) in memory until the
+session expires.</p>
+
+<p>With those pieces in hand, here's an example that prints a message whenever a
+session expires, and uses sessions which last for 5 seconds:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>, <span class="py-src-variable">Session</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ShortSession</span>(<span class="py-src-parameter">Session</span>):
+ <span class="py-src-variable">sessionTimeout</span> = <span class="py-src-number">5</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ExpirationLogger</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-variable">sessions</span> = <span class="py-src-variable">set</span>()
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">session</span> = <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>()
+ <span class="py-src-keyword">if</span> <span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span> <span class="py-src-keyword">not</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>:
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>.<span class="py-src-variable">add</span>(<span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span>)
+ <span class="py-src-variable">session</span>.<span class="py-src-variable">notifyOnExpire</span>(<span class="py-src-keyword">lambda</span>: <span class="py-src-variable">self</span>.<span class="py-src-variable">_expired</span>(<span class="py-src-variable">session</span>.<span class="py-src-variable">uid</span>))
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">_expired</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">uid</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">&quot;Session&quot;</span>, <span class="py-src-variable">uid</span>, <span class="py-src-string">&quot;has expired.&quot;</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">sessions</span>.<span class="py-src-variable">remove</span>(<span class="py-src-variable">uid</span>)
+
+<span class="py-src-variable">rootResource</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">rootResource</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;logme&quot;</span>, <span class="py-src-variable">ExpirationLogger</span>())
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">rootResource</span>)
+<span class="py-src-variable">factory</span>.<span class="py-src-variable">sessionFactory</span> = <span class="py-src-variable">ShortSession</span>
+
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8080</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Since <code>Site</code> customization is required, this example can't be
+rpy-based, so it brings back the manual <code>reactor.listenTCP</code>
+and <code>reactor.run</code> calls. Run it and visit <code>/logme</code> to see
+it in action. Keep visiting it to keep your session active. Stop visiting it for
+five seconds to see your session expiration message.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/session-store.html b/doc/web/howto/web-in-60/session-store.html
new file mode 100644
index 0000000..2f25dcd
--- /dev/null
+++ b/doc/web/howto/web-in-60/session-store.html
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Storing Objects in the Session</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Storing Objects in the Session</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>This example shows you how you can persist objects across requests in the
+session object.</p>
+
+<p>As was discussed <a href="session-basics.html" shape="rect">previously</a>, instances
+of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Session.html" title="twisted.web.server.Session">Session</a></code> last as long as
+the notional session itself does. Each time <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.getSession.html" title="twisted.web.server.Request.getSession">Request.getSession</a></code> is called, if the session
+for the request is still active, then the same <code>Session</code> instance is
+returned as was returned previously. Because of this, <code>Session</code>
+instances can be used to keep other objects around for as long as the session
+exists.</p>
+
+<p>It's easier to demonstrate how this works than explain it, so here's an
+example:</p>
+
+<pre class="shell" xml:space="preserve">
+&gt;&gt;&gt; from zope.interface import Interface, Attribute, implements
+&gt;&gt;&gt; from twisted.python.components import registerAdapter
+&gt;&gt;&gt; from twisted.web.server import Session
+&gt;&gt;&gt; class ICounter(Interface):
+... value = Attribute(&quot;An int value which counts up once per page view.&quot;)
+...
+&gt;&gt;&gt; class Counter(object):
+... implements(ICounter)
+... def __init__(self, session):
+... self.value = 0
+...
+&gt;&gt;&gt; registerAdapter(Counter, Session, ICounter)
+&gt;&gt;&gt; ses = Session(None, None)
+&gt;&gt;&gt; data = ICounter(ses)
+&gt;&gt;&gt; print data
+&lt;__main__.Counter object at 0x8d535ec&gt;
+&gt;&gt;&gt; print data is ICounter(ses)
+True
+&gt;&gt;&gt;
+</pre>
+
+<p><i>What?</i>, I hear you say.</p>
+
+<p>What's shown in this example is the interface and adaption-based
+API which <code>Session</code> exposes for persisting state. There are
+several critical pieces interacting here:</p>
+
+<ul>
+ <li><code>ICounter</code> is an interface which serves several purposes. Like
+ all interfaces, it documents the API of some class of objects (in this case,
+ just the <code>value</code> attribute). It also serves as a key into what is
+ basically a dictionary within the session object: the interface is used to
+ store or retrieve a value on the session (the <code>Counter</code> instance,
+ in this case).</li>
+ <li><code>Counter</code> is the class which actually holds the session data in
+ this example. It implements <code>ICounter</code> (again, mostly for
+ documentation purposes). It also has a <code>value</code> attribute, as the
+ interface declared.</li>
+ <li>The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.components.registerAdapter.html" title="twisted.python.components.registerAdapter">registerAdapter</a></code> call sets up the
+ relationship between its three arguments so that adaption will do what we
+ want in this case.</li>
+ <li>Adaption is performed by the expression <code>ICounter(ses)</code>. This
+ is read as : adapt <code>ses</code> to <code>ICounter</code>. Because
+ of the <code>registerAdapter</code> call, it is roughly equivalent
+ to <code>Counter(ses)</code>. However (because of certain
+ things <code>Session</code> does), it also saves the <code>Counter</code>
+ instance created so that it will be returned the next time this adaption is
+ done. This is why the last statement produces <code>True</code>.</li>
+</ul>
+
+<p>If you're still not clear on some of the details there, don't worry about it
+and just remember this: <code>ICounter(ses)</code> gives you an object you can
+persist state on. It can be as much or as little state as you want, and you can
+use as few or as many different <code>Interface</code> classes as you want on a
+single <code>Session</code> instance.</p>
+
+<p>With those conceptual dependencies out of the way, it's a very short step to
+actually getting persistent state into a Twisted Web application. Here's an
+example which implements a simple counter, re-using the definitions from the
+example above:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CounterResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">session</span> = <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>()
+ <span class="py-src-variable">counter</span> = <span class="py-src-variable">ICounter</span>(<span class="py-src-variable">session</span>)
+ <span class="py-src-variable">counter</span>.<span class="py-src-variable">value</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Visit #%d for you!&quot;</span> % (<span class="py-src-variable">counter</span>.<span class="py-src-variable">value</span>,)
+</pre>
+
+<p>Pretty simple from this side, eh? All this does is
+use <code>Request.getSession</code> and the adaption from above, plus some
+integer math to give you a session-based visit counter.</p>
+
+<p>Here's the complete source for an <a href="rpy-scripts.html" shape="rect">rpy script</a>
+based on this example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-variable">cache</span>()
+
+<span class="py-src-keyword">from</span> <span class="py-src-variable">zope</span>.<span class="py-src-variable">interface</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Interface</span>, <span class="py-src-variable">Attribute</span>, <span class="py-src-variable">implements</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">python</span>.<span class="py-src-variable">components</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">registerAdapter</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Session</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">ICounter</span>(<span class="py-src-parameter">Interface</span>):
+ <span class="py-src-variable">value</span> = <span class="py-src-variable">Attribute</span>(<span class="py-src-string">&quot;An int value which counts up once per page view.&quot;</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Counter</span>(<span class="py-src-parameter">object</span>):
+ <span class="py-src-variable">implements</span>(<span class="py-src-variable">ICounter</span>)
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">session</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">value</span> = <span class="py-src-number">0</span>
+
+<span class="py-src-variable">registerAdapter</span>(<span class="py-src-variable">Counter</span>, <span class="py-src-variable">Session</span>, <span class="py-src-variable">ICounter</span>)
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">CounterResource</span>(<span class="py-src-parameter">Resource</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">render_GET</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>):
+ <span class="py-src-variable">session</span> = <span class="py-src-variable">request</span>.<span class="py-src-variable">getSession</span>()
+ <span class="py-src-variable">counter</span> = <span class="py-src-variable">ICounter</span>(<span class="py-src-variable">session</span>)
+ <span class="py-src-variable">counter</span>.<span class="py-src-variable">value</span> += <span class="py-src-number">1</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Visit #%d for you!&quot;</span> % (<span class="py-src-variable">counter</span>.<span class="py-src-variable">value</span>,)
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">CounterResource</span>()
+</pre>
+
+<p>One more thing to note is the <code>cache()</code> call at the top
+of this example. As with the <a href="http-auth.html" shape="rect">previous
+example</a> where this came up, this rpy script is stateful. This
+time, it's the <code>ICounter</code> definition and
+the <code>registerAdapter</code> call that need to be executed only
+once. If we didn't use <code>cache</code>, every request would define
+a new, different interface named <code>ICounter</code>. Each of these
+would be a different key in the session, so the counter would never
+get past one.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/static-content.html b/doc/web/howto/web-in-60/static-content.html
new file mode 100644
index 0000000..a4f30dd
--- /dev/null
+++ b/doc/web/howto/web-in-60/static-content.html
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Serving Static Content From a Directory</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Serving Static Content From a Directory</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The goal of this example is to show you how to serve static content
+from a filesystem. First, we need to import some objects:</p>
+
+<ul>
+
+<li>
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code>, an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IProtocolFactory.html" title="twisted.internet.interfaces.IProtocolFactory">IProtocolFactory</a></code> which
+glues a listening server port (<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.interfaces.IListeningPort.html" title="twisted.internet.interfaces.IListeningPort">IListeningPort</a></code>) to the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.http.HTTPChannel.html" title="twisted.web.http.HTTPChannel">HTTPChannel</a></code>
+implementation:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+</pre>
+</li>
+
+<li>
+<code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.File.html" title="twisted.web.static.File">File</a></code>, an <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.IResource.html" title="twisted.web.resource.IResource">IResource</a></code> which glues
+the HTTP protocol implementation to the filesystem:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+</pre>
+</li>
+
+<li>
+The <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code>, which
+drives the whole process, actually accepting TCP connections and
+moving bytes into and out of them:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+</li>
+
+</ul>
+
+Next, we create an instance of the File resource pointed at the
+directory to serve:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">resource</span> = <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/tmp&quot;</span>)
+</pre>
+
+Then we create an instance of the Site factory with that resource:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>)
+</pre>
+
+Now we glue that factory to a TCP port:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8888</span>, <span class="py-src-variable">factory</span>)
+</pre>
+
+Finally, we start the reactor so it can make the program work:
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+And that's it. Here's the complete program:
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">File</span>(<span class="py-src-string">'/tmp'</span>)
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">resource</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8888</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Bonus example! For those times when you don't actually want to
+write a new program, the above implemented functionality is one of the
+things the command line <code>twistd</code> tool can do. In this case,
+the command
+<pre xml:space="preserve">
+twistd -n web --path /tmp
+</pre>
+will accomplish the same thing as the above server. See <a href="../../../core/howto/basics.html" shape="rect">helper programs</a> in the
+Twisted Core documentation for more information on using
+<code>twistd</code>.</p>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/static-dispatch.html b/doc/web/howto/web-in-60/static-dispatch.html
new file mode 100644
index 0000000..3084ce1
--- /dev/null
+++ b/doc/web/howto/web-in-60/static-dispatch.html
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Static URL Dispatch</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Static URL Dispatch</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The goal of this example is to show you how to serve different content at
+different URLs.</p>
+
+<p>The key to understanding how different URLs are handled with the resource
+APIs in Twisted Web is understanding that any URL can be used to address a node
+in a tree. Resources in Twisted Web exist in such a tree, and a request for a
+URL will be responded to by the resource which that URL addresses. The
+addressing scheme considers only the path segments of the URL. Starting with the
+root resource (the one used to construct the <code>Site</code>) and the first
+path segment, a child resource is looked up. As long as there are more path
+segments, this process is repeated using the result of the previous lookup and
+the next path segment. For example, to handle a request
+for <code>&quot;/foo/bar&quot;</code>, first the root's <code>&quot;foo&quot;</code> child is
+retrieved, then that resource's <code>&quot;bar&quot;</code> child is retrieved, then that
+resource is used to create the response.</p>
+
+<p>With that out of the way, let's consider an example that can serve a few
+different resources at a few different URLs.</p>
+
+<p>First things first: we need to import <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code>, the factory for HTTP servers, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>, a convenient base class
+for custom pages, and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.reactor.html" title="twisted.internet.reactor">reactor</a></code>,
+the object which implements the Twisted main loop. We'll also import <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.static.File.html" title="twisted.web.static.File">File</a></code> to use as the resource at one
+of the example URLs.</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+</pre>
+
+<p>Now we create a resource which will correspond to the root of the URL
+hierarchy: all URLs are children of this resource.</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+</pre>
+
+<p>Here comes the interesting part of this example. We're now going to
+create three more resources and attach them to the three
+URLs <code>/foo</code>, <code>/bar</code>, and <code>/baz</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/tmp&quot;</span>))
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;bar&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/lost+found&quot;</span>))
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;baz&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/opt&quot;</span>))
+</pre>
+
+<p>Last, all that's required is to create a <code>Site</code> with the root
+resource, associate it with a listening server port, and start the reactor:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>With this server running, <code>http://localhost:8880/foo</code>
+will serve a listing of files
+from <code>/tmp</code>, <code>http://localhost:8880/bar</code> will
+serve a listing of files from <code>/lost+found</code>,
+and <code>http://localhost:8880/baz</code> will serve a listing of
+files from <code>/opt</code>.</p>
+
+<p>Here's the whole example uninterrupted:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">resource</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Resource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">static</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">File</span>
+
+<span class="py-src-variable">root</span> = <span class="py-src-variable">Resource</span>()
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;foo&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/tmp&quot;</span>))
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;bar&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/lost+found&quot;</span>))
+<span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">&quot;baz&quot;</span>, <span class="py-src-variable">File</span>(<span class="py-src-string">&quot;/opt&quot;</span>))
+
+<span class="py-src-variable">factory</span> = <span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">8880</span>, <span class="py-src-variable">factory</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-in-60/wsgi.html b/doc/web/howto/web-in-60/wsgi.html
new file mode 100644
index 0000000..e88084e
--- /dev/null
+++ b/doc/web/howto/web-in-60/wsgi.html
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: WSGI</title>
+<link href="../stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">WSGI</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<p>The goal of this example is to show you how to
+use <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.wsgi.WSGIResource.html" title="twisted.web.wsgi.WSGIResource">WSGIResource</a></code>,
+another existing <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code> subclass, to
+serve <a href="http://www.python.org/dev/peps/pep-0333/" shape="rect">WSGI applications</a>
+in a Twisted Web server.</p>
+
+<p>Note that <code>WSGIResource</code> is a multithreaded WSGI container. Like
+any other WSGI container, you can't do anything asynchronous in your WSGI
+applications, even though this is a Twisted WSGI container.</p>
+
+<p>The first new thing in this example is the import
+of <code>WSGIResource</code>:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">wsgi</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">WSGIResource</span>
+</pre>
+
+<p>Nothing too surprising there. We still need one of the other usual suspects,
+too:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+</pre>
+
+<p>You'll see why in a minute. Next, we need a WSGI application. Here's a really
+simple one just to get things going:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">application</span>(<span class="py-src-parameter">environ</span>, <span class="py-src-parameter">start_response</span>):
+ <span class="py-src-variable">start_response</span>(<span class="py-src-string">'200 OK'</span>, [(<span class="py-src-string">'Content-type'</span>, <span class="py-src-string">'text/plain'</span>)])
+ <span class="py-src-keyword">return</span> [<span class="py-src-string">'Hello, world!'</span>]
+</pre>
+
+<p>If this doesn't make sense to you, take a look at one of
+these <a href="http://wsgi.org/wsgi/Learn_WSGI" shape="rect">fine tutorials</a>. Otherwise,
+or once you're done with that, the next step is to create
+a <code>WSGIResource</code> instance, as this is going to be
+another <a href="rpy-scripts.html" shape="rect">rpy script</a> example:</p>
+
+<pre class="python"><p class="py-linenumber">1
+</p><span class="py-src-variable">resource</span> = <span class="py-src-variable">WSGIResource</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">getThreadPool</span>(), <span class="py-src-variable">application</span>)
+</pre>
+
+<p>Let's dwell on this line for a minute. The first parameter passed
+to <code>WSGIResource</code> is the reactor. Despite the fact that the
+reactor is global and any code that wants it can always just import it
+(as, in fact, this rpy script simply does itself), passing it around
+as a parameter leaves the door open for certain future possibilities -
+for example, having more than one reactor. There are also testing
+implications. Consider how much easier it is to unit test a function
+that accepts a reactor - perhaps a mock reactor specially constructed
+to make your tests easy to write - rather than importing the real
+global reactor. That's why <code>WSGIResource</code> requires you to
+pass the reactor to it.</p>
+
+<p>The second parameter passed to <code>WSGIResource</code> is
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.python.threadpool.ThreadPool.html" title="twisted.python.threadpool.ThreadPool">ThreadPool</a></code>. <code>WSGIResource</code>
+uses this to actually call the application object passed in to it. To keep this
+example short, we're passing in the reactor's internal threadpool here, letting
+us skip its creation and shutdown-time destruction. For finer control over how
+many WSGI requests are served in parallel, you may want to create your own
+thread pool to use with your <code>WSGIResource</code>, but for simple testing,
+using the reactor's is fine.</p>
+
+<p>The final argument is the application object. This is pretty typical of how
+WSGI containers work.</p>
+
+<p>The example, sans interruption:</p>
+
+<pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+7
+8
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">wsgi</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">WSGIResource</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">application</span>(<span class="py-src-parameter">environ</span>, <span class="py-src-parameter">start_response</span>):
+ <span class="py-src-variable">start_response</span>(<span class="py-src-string">'200 OK'</span>, [(<span class="py-src-string">'Content-type'</span>, <span class="py-src-string">'text/plain'</span>)])
+ <span class="py-src-keyword">return</span> [<span class="py-src-string">'Hello, world!'</span>]
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">WSGIResource</span>(<span class="py-src-variable">reactor</span>, <span class="py-src-variable">reactor</span>.<span class="py-src-variable">getThreadPool</span>(), <span class="py-src-variable">application</span>)
+</pre>
+
+<p>Up to the point where the <code>WSGIResource</code> instance defined here
+exists in the resource hierarchy, the normal resource traversal rules
+apply: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.getChild.html" title="twisted.web.resource.Resource.getChild">getChild</a></code>
+will be called to handle each segment. Once the <code>WSGIResource</code> is
+encountered, though, that process stops and all further URL handling is the
+responsibility of the WSGI application. This application does nothing with the
+URL, though, so you won't be able to tell that.</p>
+
+<p>Oh, and as was the case with the first static file example, there's also a
+command line option you can use to avoid a lot of this. If you just put the
+above application function, without all of the <code>WSGIResource</code> stuff,
+into a file, say, <code>foo.py</code>, then you can launch a roughly equivalent
+server like this:</p>
+
+<pre class="shell" xml:space="preserve">
+$ twistd -n web --wsgi foo.application
+</pre>
+
+</div>
+
+ <p><a href="../index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/web-overview.html b/doc/web/howto/web-overview.html
new file mode 100644
index 0000000..6e70c4b
--- /dev/null
+++ b/doc/web/howto/web-overview.html
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Overview of Twisted Web</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Overview of Twisted Web</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Twisted Web's Structure</a></li><li><a href="#auto2">Resources</a></li><li><a href="#auto3">
+ Web programming with Twisted Web
+ </a></li></ol></div>
+ <div class="content">
+ <span/>
+
+ <h2>Introduction<a name="auto0"/></h2>
+
+ <p>Twisted Web is a web application server written in pure
+ Python, with APIs at multiple levels of abstraction to
+ facilitate different kinds of web programming.
+ </p>
+
+ <h2>Twisted Web's Structure<a name="auto1"/></h2>
+
+ <p><img src="../img/web-overview.png"/></p>
+
+ <p>When
+ the Web Server receives a request from a Client, it creates
+ a Request object and passes it on to the Resource system.
+ The Resource system dispatches to the appropriate Resource
+ object based on what path was requested by the client. The
+ Resource is asked to render itself, and the result is
+ returned to the client.</p>
+
+ <h2>Resources<a name="auto2"/></h2>
+
+ <p>Resources are the lowest-level abstraction for applications
+ in the Twisted web server. Each Resource is a 1:1 mapping with
+ a path that is requested: you can think of a Resource as a
+ single <q>page</q> to be rendered. The interface for making
+ Resources is very simple; they must have a method named
+ <code>render</code> which takes a single argument, which is the
+ Request object (an instance of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">twisted.web.server.Request</a></code>). This render
+ method must return a string, which will be returned to the web
+ browser making the request. Alternatively, they can return a
+ special constant, <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.NOT_DONE_YET.html" title="twisted.web.server.NOT_DONE_YET">twisted.web.server.NOT_DONE_YET</a></code>, which tells
+ the web server not to close the connection; you must then use
+ <code class="python">request.write(data)</code> to render the
+ page, and call <code class="python">request.finish()</code>
+ whenever you're done.
+ </p>
+
+ <h2>
+ Web programming with Twisted Web
+ <a name="auto3"/></h2>
+
+ <p>
+ Web programmers seeking a higher level abstraction than the Resource system
+ should look at <a href="https://launchpad.net/nevow" shape="rect">Nevow</a>.
+ Nevow is based on ideas previously developed in Twisted, but is now maintained
+ outside of Twisted to easy development and release cycle pressures.
+ </p>
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/howto/xmlrpc.html b/doc/web/howto/xmlrpc.html
new file mode 100644
index 0000000..4637187
--- /dev/null
+++ b/doc/web/howto/xmlrpc.html
@@ -0,0 +1,651 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Creating XML-RPC Servers and Clients with Twisted</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Creating XML-RPC Servers and Clients with Twisted</h1>
+ <div class="toc"><ol><li><a href="#auto0">Introduction</a></li><li><a href="#auto1">Creating a XML-RPC server</a></li><ul><li><a href="#auto2">Using XML-RPC sub-handlers</a></li><li><a href="#auto3">Using your own procedure getter</a></li><li><a href="#auto4">Adding XML-RPC Introspection support</a></li></ul><li><a href="#auto5">SOAP Support</a></li><li><a href="#auto6">Creating an XML-RPC Client</a></li><li><a href="#auto7">Serving SOAP and XML-RPC simultaneously</a></li></ol></div>
+ <div class="content">
+<span/>
+
+<h2>Introduction<a name="auto0"/></h2>
+
+<p><a href="http://www.xmlrpc.com" shape="rect">XML-RPC</a> is a simple request/reply protocol
+that runs over HTTP. It is simple, easy to implement and supported by most programming
+languages. Twisted's XML-RPC support is implemented using the
+<a href="http://docs.python.org/library/xmlrpclib.html" shape="rect">xmlrpclib</a> library that is
+included with Python 2.2 and later.</p>
+
+<h2>Creating a XML-RPC server<a name="auto1"/></h2>
+
+<p>Making a server is very easy - all you need to do is inherit from <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.XMLRPC.html" title="twisted.web.xmlrpc.XMLRPC">twisted.web.xmlrpc.XMLRPC</a></code>.
+You then create methods beginning with <code>xmlrpc_</code>. The methods'
+arguments determine what arguments it will accept from XML-RPC clients.
+The result is what will be returned to the clients.</p>
+
+<p>Methods published via XML-RPC can return all the basic XML-RPC
+types, such as strings, lists and so on (just return a regular python
+integer, etc). They can also raise exceptions or return Failure instances to indicate an
+error has occurred, or <code>Binary</code>, <code>Boolean</code> or <code>DateTime</code>
+instances (all of these are the same as the respective classes in xmlrpclib. In
+addition, XML-RPC published methods can return <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.internet.defer.Deferred.html" title="twisted.internet.defer.Deferred">Deferred</a></code> instances whose results are one of the above. This allows
+you to return results that can't be calculated immediately, such as database queries.
+See the <a href="../../core/howto/defer.html" shape="rect">Deferred documentation</a> for more
+details.</p>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.XMLRPC.html" title="twisted.web.xmlrpc.XMLRPC">XMLRPC</a></code> instances
+are Resource objects, and they can thus be published using a Site. The
+following example has two methods published via XML-RPC, <code>add(a,
+b)</code> and <code>echo(x)</code>.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Example</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An example object to be published.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return all passed args.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">x</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return sum of arguments.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_fault</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Raise a Fault indicating that the procedure should not be used.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">Fault</span>(<span class="py-src-number">123</span>, <span class="py-src-string">&quot;The fault procedure is faulty.&quot;</span>)
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">Example</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>After we run this command, we can connect with a client and send commands
+to the server:</p>
+
+<pre class="python-interpreter" xml:space="preserve">
+&gt;&gt;&gt; import xmlrpclib
+&gt;&gt;&gt; s = xmlrpclib.Server('http://localhost:7080/')
+&gt;&gt;&gt; s.echo(&quot;lala&quot;)
+'lala'
+&gt;&gt;&gt; s.add(1, 2)
+3
+&gt;&gt;&gt; s.fault()
+Traceback (most recent call last):
+...
+xmlrpclib.Fault: &lt;Fault 123: 'The fault procedure is faulty.'&gt;
+&gt;&gt;&gt;
+
+</pre>
+
+<p>If the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Request.html" title="twisted.web.server.Request">Request</a></code> object is
+needed by an <code>xmlrpc_*</code> method, it can be made available using
+the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.withRequest.html" title="twisted.web.xmlrpc.withRequest">twisted.web.xmlrpc.withRequest</a></code> decorator. When
+using this decorator, the method will be passed the request object as the first
+argument, before any XML-RPC parameters. For example:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">xmlrpc</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">XMLRPC</span>, <span class="py-src-variable">withRequest</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">server</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Site</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Example</span>(<span class="py-src-parameter">XMLRPC</span>):
+ @<span class="py-src-variable">withRequest</span>
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_headerValue</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">request</span>, <span class="py-src-parameter">headerName</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">request</span>.<span class="py-src-variable">requestHeaders</span>.<span class="py-src-variable">getRawHeaders</span>(<span class="py-src-variable">headerName</span>)
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">Site</span>(<span class="py-src-variable">Example</span>()))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>XML-RPC resources can also be part of a normal Twisted web server, using
+resource scripts. The following is an example of such a resource script:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpc</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;What are you talking about, William?&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Quoter</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_quote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getQuote</span>()
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">Quoter</span>()
+</pre><div class="caption">Source listing - <a href="listings/xmlquote.rpy"><span class="filename">listings/xmlquote.rpy</span></a></div></div>
+
+<h3>Using XML-RPC sub-handlers<a name="auto2"/></h3>
+
+<p>XML-RPC resource can be nested so that one handler calls another if
+a method with a given prefix is called. For example, to add support
+for an XML-RPC method <code>date.time()</code> to
+the <code class="python">Example</code> class, you could do the
+following:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+</p><span class="py-src-keyword">import</span> <span class="py-src-variable">time</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Example</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An example object to be published.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return all passed args.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">x</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return sum of arguments.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Date</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Serve the XML-RPC 'time' method.
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_time</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return UNIX time.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">time</span>.<span class="py-src-variable">time</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">Example</span>()
+ <span class="py-src-variable">date</span> = <span class="py-src-variable">Date</span>()
+ <span class="py-src-variable">r</span>.<span class="py-src-variable">putSubHandler</span>(<span class="py-src-string">'date'</span>, <span class="py-src-variable">date</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>By default, a period ('.') separates the prefix from the method
+name, but you can use a different character by overriding the <code class="python">XMLRPC.separator</code> data member in your base
+XML-RPC server. XML-RPC servers may be nested to arbitrary depths
+using this method.</p>
+
+<h3>Using your own procedure getter<a name="auto3"/></h3>
+
+<p>Sometimes, you want to implement your own policy of getting the end implementation.
+E.g. just like sub-handlers you want to divide the implementations into separate classes but
+may not want to introduce <code class="python">XMLRPC.separator</code> in the procedure name.
+In such cases just override the <code class="python">lookupProcedure(self, procedurePath)</code>
+method and return the correct callable.
+Raise <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.NoSuchFunction.html" title="twisted.web.xmlrpc.NoSuchFunction">twisted.web.xmlrpc.NoSuchFunction</a></code> otherwise.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">EchoHandler</span>:
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return all passed args
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">x</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">AddHandler</span>:
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Return sum of arguments.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Example</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ An example of using you own policy to fetch the handler
+ &quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">XMLRPC</span>.<span class="py-src-variable">__init__</span>(<span class="py-src-variable">self</span>)
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_addHandler</span> = <span class="py-src-variable">AddHandler</span>()
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_echoHandler</span> = <span class="py-src-variable">EchoHandler</span>()
+
+ <span class="py-src-comment">#We keep a dict of all relevant</span>
+ <span class="py-src-comment">#procedure names and callable.</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">_procedureToCallable</span> = {
+ <span class="py-src-string">'add'</span>:<span class="py-src-variable">self</span>.<span class="py-src-variable">_addHandler</span>.<span class="py-src-variable">add</span>,
+ <span class="py-src-string">'echo'</span>:<span class="py-src-variable">self</span>.<span class="py-src-variable">_echoHandler</span>.<span class="py-src-variable">echo</span>
+ }
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">lookupProcedure</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">procedurePath</span>):
+ <span class="py-src-keyword">try</span>:
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">_procedureToCallable</span>[<span class="py-src-variable">procedurePath</span>]
+ <span class="py-src-keyword">except</span> <span class="py-src-variable">KeyError</span>, <span class="py-src-variable">e</span>:
+ <span class="py-src-keyword">raise</span> <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">NoSuchFunction</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">NOT_FOUND</span>,
+ <span class="py-src-string">&quot;procedure %s not found&quot;</span> % <span class="py-src-variable">procedurePath</span>)
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">listProcedures</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-string">&quot;&quot;&quot;
+ Since we override lookupProcedure, its suggested to override
+ listProcedures too.
+ &quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> [<span class="py-src-string">'add'</span>, <span class="py-src-string">'echo'</span>]
+
+
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">Example</span>()
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre><div class="caption">Source listing - <a href="listings/xmlrpc-customized.py"><span class="filename">listings/xmlrpc-customized.py</span></a></div></div>
+
+<h3>Adding XML-RPC Introspection support<a name="auto4"/></h3>
+
+<p>XML-RPC has an
+informal <a href="http://tldp.org/HOWTO/XML-RPC-HOWTO/xmlrpc-howto-interfaces.html" shape="rect">Introspection
+API</a> that specifies three methods in a <code>system</code>
+sub-handler which allow a client to query a server about the server's
+API. Adding Introspection support to
+the <code class="python">Example</code> class is easy using
+the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.XMLRPCIntrospection.html" title="twisted.web.xmlrpc.XMLRPCIntrospection">XMLRPCIntrospection</a></code> class:</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">server</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Example</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-string">&quot;&quot;&quot;An example object to be published.&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_echo</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">x</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return all passed args.&quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">x</span>
+
+ <span class="py-src-variable">xmlrpc_echo</span>.<span class="py-src-variable">signature</span> = [[<span class="py-src-string">'string'</span>, <span class="py-src-string">'string'</span>],
+ [<span class="py-src-string">'int'</span>, <span class="py-src-string">'int'</span>],
+ [<span class="py-src-string">'double'</span>, <span class="py-src-string">'double'</span>],
+ [<span class="py-src-string">'array'</span>, <span class="py-src-string">'array'</span>],
+ [<span class="py-src-string">'struct'</span>, <span class="py-src-string">'struct'</span>]]
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_add</span>(<span class="py-src-parameter">self</span>, <span class="py-src-parameter">a</span>, <span class="py-src-parameter">b</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Return sum of arguments.&quot;&quot;&quot;</span>
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">a</span> + <span class="py-src-variable">b</span>
+
+ <span class="py-src-variable">xmlrpc_add</span>.<span class="py-src-variable">signature</span> = [[<span class="py-src-string">'int'</span>, <span class="py-src-string">'int'</span>, <span class="py-src-string">'int'</span>],
+ [<span class="py-src-string">'double'</span>, <span class="py-src-string">'double'</span>, <span class="py-src-string">'double'</span>]]
+ <span class="py-src-variable">xmlrpc_add</span>.<span class="py-src-variable">help</span> = <span class="py-src-string">&quot;Add the arguments and return the sum.&quot;</span>
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">r</span> = <span class="py-src-variable">Example</span>()
+ <span class="py-src-variable">xmlrpc</span>.<span class="py-src-variable">addIntrospection</span>(<span class="py-src-variable">r</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">r</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>Note the method attributes <code class="python">help</code>
+and <code class="python">signature</code> which are used by the
+Introspection API methods <code>system.methodHelp</code>
+and <code>system.methodSignature</code> respectively. If
+no <code class="python">help</code> attribute is specified, the
+method's documentation string is used instead.</p>
+
+<h2>SOAP Support<a name="auto5"/></h2>
+
+<p>From the point of view of a Twisted developer, there is little difference
+between XML-RPC support and SOAP support. Here is an example of SOAP usage:</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">soap</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;That beverage, sir, is off the hizzy.&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">Quoter</span>(<span class="py-src-parameter">soap</span>.<span class="py-src-parameter">SOAPPublisher</span>):
+ <span class="py-src-string">&quot;&quot;&quot;Publish one method, 'quote'.&quot;&quot;&quot;</span>
+
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">soap_quote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getQuote</span>()
+
+<span class="py-src-variable">resource</span> = <span class="py-src-variable">Quoter</span>()
+</pre><div class="caption">Source listing - <a href="listings/soap.rpy"><span class="filename">listings/soap.rpy</span></a></div></div>
+
+
+<h2>Creating an XML-RPC Client<a name="auto6"/></h2>
+
+<p>XML-RPC clients in Twisted are meant to look as something which will be
+familiar either to <code>xmlrpclib</code> or to Perspective Broker users,
+taking features from both, as appropriate. There are two major deviations
+from the <code>xmlrpclib</code> way which should be noted:</p>
+
+<ol>
+<li>No implicit <code>/RPC2</code>. If the services uses this path for the
+ XML-RPC calls, then it will have to be given explicitly.</li>
+<li>No magic <code>__getattr__</code>: calls must be made by an explicit
+ <code>callRemote</code>.</li>
+</ol>
+
+<p>The interface Twisted presents to XML-RPC client is that of a proxy
+object: <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.Proxy.html" title="twisted.web.xmlrpc.Proxy">twisted.web.xmlrpc.Proxy</a></code>. The
+constructor for the object receives a URL: it must be an HTTP or HTTPS
+URL. When an XML-RPC service is described, the URL to that service
+will be given there.</p>
+
+<p>Having a proxy object, one can just call the <code>callRemote</code> method,
+which accepts a method name and a variable argument list (but no named
+arguments, as these are not supported by XML-RPC). It returns a deferred,
+which will be called back with the result. If there is any error, at any
+level, the errback will be called. The exception will be the relevant Twisted
+error in the case of a problem with the underlying connection (for example,
+a timeout), <code>IOError</code> containing the status and message in the case
+of a non-200 status or a <code>xmlrpclib.Fault</code> in the case of an
+XML-RPC level problem.</p>
+
+<pre class="python"><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span>.<span class="py-src-variable">xmlrpc</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">Proxy</span>
+<span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printValue</span>(<span class="py-src-parameter">value</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-variable">repr</span>(<span class="py-src-variable">value</span>)
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">printError</span>(<span class="py-src-parameter">error</span>):
+ <span class="py-src-keyword">print</span> <span class="py-src-string">'error'</span>, <span class="py-src-variable">error</span>
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">stop</span>()
+
+<span class="py-src-variable">proxy</span> = <span class="py-src-variable">Proxy</span>(<span class="py-src-string">'http://advogato.org/XMLRPC'</span>)
+<span class="py-src-variable">proxy</span>.<span class="py-src-variable">callRemote</span>(<span class="py-src-string">'test.sumprod'</span>, <span class="py-src-number">3</span>, <span class="py-src-number">5</span>).<span class="py-src-variable">addCallbacks</span>(<span class="py-src-variable">printValue</span>, <span class="py-src-variable">printError</span>)
+<span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+</pre>
+
+<p>prints:</p>
+
+<pre xml:space="preserve">
+[8, 15]
+</pre>
+
+<h2>Serving SOAP and XML-RPC simultaneously<a name="auto7"/></h2>
+
+<p><code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.xmlrpc.XMLRPC.html" title="twisted.web.xmlrpc.XMLRPC">twisted.web.xmlrpc.XMLRPC</a></code> and <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.soap.SOAPPublisher.html" title="twisted.web.soap.SOAPPublisher">twisted.web.soap.SOAPPublisher</a></code> are both <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">Resource</a></code>s. So, to serve both XML-RPC and
+SOAP in the one web server, you can use the <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.IResource.putChild.html" title="twisted.web.resource.IResource.putChild">putChild</a></code> method of Resource.</p>
+
+<p>The following example uses an empty <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.resource.Resource.html" title="twisted.web.resource.Resource">resource.Resource</a></code> as the root resource for
+a <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.web.server.Site.html" title="twisted.web.server.Site">Site</a></code>, and then
+adds <code>/RPC2</code> and <code>/SOAP</code> paths to it.</p>
+
+<div class="py-listing"><pre><p class="py-linenumber"> 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+</p><span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">web</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">soap</span>, <span class="py-src-variable">xmlrpc</span>, <span class="py-src-variable">resource</span>, <span class="py-src-variable">server</span>
+<span class="py-src-keyword">import</span> <span class="py-src-variable">os</span>
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">getQuote</span>():
+ <span class="py-src-keyword">return</span> <span class="py-src-string">&quot;Victory to the burgeois, you capitalist swine!&quot;</span>
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">XMLRPCQuoter</span>(<span class="py-src-parameter">xmlrpc</span>.<span class="py-src-parameter">XMLRPC</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">xmlrpc_quote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getQuote</span>()
+
+<span class="py-src-keyword">class</span> <span class="py-src-identifier">SOAPQuoter</span>(<span class="py-src-parameter">soap</span>.<span class="py-src-parameter">SOAPPublisher</span>):
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">soap_quote</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-keyword">return</span> <span class="py-src-variable">getQuote</span>()
+
+<span class="py-src-keyword">def</span> <span class="py-src-identifier">main</span>():
+ <span class="py-src-keyword">from</span> <span class="py-src-variable">twisted</span>.<span class="py-src-variable">internet</span> <span class="py-src-keyword">import</span> <span class="py-src-variable">reactor</span>
+ <span class="py-src-variable">root</span> = <span class="py-src-variable">resource</span>.<span class="py-src-variable">Resource</span>()
+ <span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'RPC2'</span>, <span class="py-src-variable">XMLRPCQuoter</span>())
+ <span class="py-src-variable">root</span>.<span class="py-src-variable">putChild</span>(<span class="py-src-string">'SOAP'</span>, <span class="py-src-variable">SOAPQuoter</span>())
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">listenTCP</span>(<span class="py-src-number">7080</span>, <span class="py-src-variable">server</span>.<span class="py-src-variable">Site</span>(<span class="py-src-variable">root</span>))
+ <span class="py-src-variable">reactor</span>.<span class="py-src-variable">run</span>()
+
+<span class="py-src-keyword">if</span> <span class="py-src-variable">__name__</span> == <span class="py-src-string">'__main__'</span>:
+ <span class="py-src-variable">main</span>()
+</pre><div class="caption">Source listing - <a href="listings/xmlAndSoapQuote.py"><span class="filename">listings/xmlAndSoapQuote.py</span></a></div></div>
+
+<p>Refer to <a href="using-twistedweb.html#development" shape="rect">Twisted Web
+Development</a> for more details about Resources.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/web/img/controller.png b/doc/web/img/controller.png
new file mode 100644
index 0000000..462268e
--- /dev/null
+++ b/doc/web/img/controller.png
Binary files differ
diff --git a/doc/web/img/livepage.png b/doc/web/img/livepage.png
new file mode 100644
index 0000000..131d8d7
--- /dev/null
+++ b/doc/web/img/livepage.png
Binary files differ
diff --git a/doc/web/img/model.png b/doc/web/img/model.png
new file mode 100644
index 0000000..30058b0
--- /dev/null
+++ b/doc/web/img/model.png
Binary files differ
diff --git a/doc/web/img/plone_root_model.png b/doc/web/img/plone_root_model.png
new file mode 100644
index 0000000..9f0f385
--- /dev/null
+++ b/doc/web/img/plone_root_model.png
Binary files differ
diff --git a/doc/web/img/view.png b/doc/web/img/view.png
new file mode 100644
index 0000000..5fdbc05
--- /dev/null
+++ b/doc/web/img/view.png
Binary files differ
diff --git a/doc/web/img/web-overview.dia b/doc/web/img/web-overview.dia
new file mode 100644
index 0000000..4bc9be9
--- /dev/null
+++ b/doc/web/img/web-overview.dia
Binary files differ
diff --git a/doc/web/img/web-overview.png b/doc/web/img/web-overview.png
new file mode 100644
index 0000000..3eff9bd
--- /dev/null
+++ b/doc/web/img/web-overview.png
Binary files differ
diff --git a/doc/web/img/web-process.png b/doc/web/img/web-process.png
new file mode 100644
index 0000000..4f5ab66
--- /dev/null
+++ b/doc/web/img/web-process.png
Binary files differ
diff --git a/doc/web/img/web-process.svg b/doc/web/img/web-process.svg
new file mode 100644
index 0000000..54a3850
--- /dev/null
+++ b/doc/web/img/web-process.svg
@@ -0,0 +1,594 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="900"
+ height="650"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="web-process.svg"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/web-process.png"
+ inkscape:export-xdpi="53.88356"
+ inkscape:export-ydpi="53.88356">
+ <metadata
+ id="metadata2876">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1207"
+ inkscape:window-height="788"
+ id="namedview2874"
+ showgrid="false"
+ showguides="false"
+ inkscape:zoom="0.80439329"
+ inkscape:cx="568.43722"
+ inkscape:cy="418.14159"
+ inkscape:window-x="344"
+ inkscape:window-y="33"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg2"
+ inkscape:guide-bbox="true"
+ showborder="true"
+ borderlayer="false"
+ inkscape:showpageshadow="false">
+ <sodipodi:guide
+ position="653.14257,-346.44227"
+ orientation="0,744.09448"
+ id="guide7662" />
+ <sodipodi:guide
+ position="1397.237,-346.44227"
+ orientation="-1052.3622,0"
+ id="guide7664" />
+ <sodipodi:guide
+ position="1397.237,705.91993"
+ orientation="0,-744.09448"
+ id="guide7666" />
+ <sodipodi:guide
+ position="653.14257,705.91993"
+ orientation="1052.3622,0"
+ id="guide7668" />
+ <sodipodi:guide
+ position="653.14257,-346.44227"
+ orientation="0,744.09448"
+ id="guide7670" />
+ <sodipodi:guide
+ position="1397.237,-346.44227"
+ orientation="-1052.3622,0"
+ id="guide7672" />
+ <sodipodi:guide
+ position="1397.237,705.91993"
+ orientation="0,-744.09448"
+ id="guide7674" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow1Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow1Lend"
+ style="overflow:visible;">
+ <path
+ id="path4316"
+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
+ transform="scale(0.8) rotate(180) translate(12.5,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend"
+ style="overflow:visible">
+ <path
+ id="path4334"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Lstart"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lstart"
+ style="overflow:visible">
+ <path
+ id="path4331"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(1.1,0,0,1.1,1.1,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow1Mstart"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow1Mstart"
+ style="overflow:visible">
+ <path
+ id="path4319"
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
+ transform="matrix(0.4,0,0,0.4,4,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Mstart"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Mstart"
+ style="overflow:visible">
+ <path
+ id="path4337"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="scale(0.6,0.6)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow1Lstart"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow1Lstart"
+ style="overflow:visible">
+ <path
+ id="path4313"
+ d="M 0,0 5,-5 -12.5,0 5,5 0,0 z"
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
+ transform="matrix(0.8,0,0,0.8,10,0)" />
+ </marker>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective2878" />
+ <inkscape:perspective
+ id="perspective3681"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3695"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3743"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3783"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3816"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3952"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective3979"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4032"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4297"
+ inkscape:persp3d-origin="250 : 166.66667 : 1"
+ inkscape:vp_z="500 : 250 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 250 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4787"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective5576"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective5603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective6025"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective6865"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective7806"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <g
+ id="layer1"
+ transform="matrix(0.93010115,0,0,0.93010115,250.13373,-60.976065)"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001">
+ <text
+ x="234.28571"
+ y="132.36218"
+ id="text2818"
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans Bold"
+ sodipodi:linespacing="125%"><tspan
+ x="234.28571"
+ y="132.36218"
+ id="tspan2820">Twisted Web</tspan></text>
+ </g>
+ <rect
+ style="fill:none;stroke:#000000;stroke-width:2.70000005;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect2884"
+ width="570.77271"
+ height="231.87642"
+ x="301.6925"
+ y="115.17715"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:22px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="424.94702"
+ y="95.464104"
+ id="text3701"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan3703"
+ x="424.94702"
+ y="95.464104">http://example.com/foo/bar/baz</tspan></text>
+ <rect
+ style="fill:#b8ffb8;fill-opacity:1;stroke:#000000;stroke-width:1.1715759;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect3705"
+ width="95.166527"
+ height="213.02367"
+ x="312.14368"
+ y="123.7507"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:20px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="358.15546"
+ y="199.9054"
+ id="text3707"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan3709"
+ x="358.15546"
+ y="199.9054">H</tspan><tspan
+ sodipodi:role="line"
+ x="358.15546"
+ y="224.9054"
+ id="tspan3711">T</tspan><tspan
+ sodipodi:role="line"
+ x="358.15546"
+ y="249.9054"
+ id="tspan3713">T</tspan><tspan
+ sodipodi:role="line"
+ x="358.15546"
+ y="274.9054"
+ id="tspan3715">P</tspan></text>
+ <rect
+ style="fill:#c8ebeb;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect3789"
+ width="445.31635"
+ height="211.98299"
+ x="416.99731"
+ y="124.27103"
+ ry="46.083252"
+ rx="0"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:18px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="478.34595"
+ y="224.9054"
+ id="text3822"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan3824"
+ x="478.34595"
+ y="224.9054">Object</tspan><tspan
+ sodipodi:role="line"
+ x="478.34595"
+ y="247.4054"
+ id="tspan3826">Publisher</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:18px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="675.25073"
+ y="210.61969"
+ id="text3828"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan3830"
+ x="675.25073"
+ y="210.61969">getChild(&quot;foo&quot;)</tspan><tspan
+ sodipodi:role="line"
+ x="675.25073"
+ y="233.11969"
+ id="tspan3832"> getChild(&quot;bar&quot;)</tspan><tspan
+ sodipodi:role="line"
+ x="675.25073"
+ y="255.61969"
+ id="tspan3834"> getChild(&quot;baz&quot;)</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:144px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#3f3f3f;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="542.63159"
+ y="292.76254"
+ id="text3842"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan3844"
+ x="542.63159"
+ y="292.76254">↻</tspan></text>
+ <g
+ id="g4300"
+ transform="matrix(0.57696609,0,0,0.51806079,460.16063,443.16138)"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001">
+ <path
+ sodipodi:nodetypes="cccccccccccccccccc"
+ id="path835"
+ d="m 163.26321,64.542987 c 3.597,0 35.974,-23.982698 88.737,-26.381098 71.948,-8.394 101.927,38.372498 101.927,38.372498 -2.398,5.9957 49.164,-37.173398 79.143,-33.575998 32.377,4.7966 45.568,19.186098 41.97,49.164598 -2.398,19.186703 -22.784,27.580703 -27.58,37.173703 2.398,4.796 28.779,20.386 28.779,62.356 0,41.97 -26.381,64.753 -25.182,64.753 1.199,0 26.381,41.97 7.195,67.152 -19.186,25.182 -73.148,1.2 -81.542,-3.597 -8.394,2.398 -13.32007,47.47397 -119.17674,52.27097 -87.33582,0 -124.24926,-33.08497 -136.24026,-37.88197 -14.39,9.593 -73.147796,22.785 -92.334196,-13.19 -15.58881,-45.567 8.3941,-56.36 13.1906,-64.754 -4.7965,-10.792 -20.3854,-28.779 -17.9871,-62.355 0,-43.17 10.7923,-64.754 20.3855,-71.949 -4.7966,-9.593 -23.9829,-47.965903 17.987,-71.948401 69.550196,-17.9872 99.529196,15.588898 100.728196,14.389698 z"
+ style="fill:#000000;fill-opacity:0.5;fill-rule:evenodd;stroke:none" />
+ <path
+ sodipodi:nodetypes="cccccccccccccccccc"
+ id="path598"
+ d="m 158.287,57.5163 c 3.597,0 35.974,-23.9827 88.737,-26.3811 71.948,-8.394 101.927,38.3725 101.927,38.3725 -2.398,5.9957 49.164,-37.1734 79.143,-33.576 32.377,4.7966 45.568,19.1861 41.97,49.1646 -2.398,19.1867 -22.784,27.5807 -27.58,37.1737 2.398,4.796 28.779,20.386 28.779,62.356 0,41.97 -26.381,64.753 -25.182,64.753 1.199,0 26.381,41.97 7.195,67.152 -19.186,25.182 -73.148,1.2 -81.542,-3.597 -8.394,2.398 -13.32007,47.47397 -119.17674,52.27097 C 165.22144,365.20497 128.308,332.12 116.317,327.323 101.927,336.916 43.1692,350.108 23.9828,314.133 8.39399,268.566 32.3769,257.773 37.1734,249.379 32.3769,238.587 16.788,220.6 19.1863,187.024 c 0,-43.17 10.7923,-64.754 20.3855,-71.949 C 34.7752,105.482 15.5889,67.1091 57.5588,43.1266 127.109,25.1394 157.088,58.7155 158.287,57.5163 z"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.77096152;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:5;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow2Lstart)"
+ d="m 357.21235,348.82073 0,81.41873 0,0 0,0"
+ id="path4762"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:20px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:100%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="329.22418"
+ y="453.06482"
+ id="text4793"
+ sodipodi:linespacing="100%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan4795"
+ x="329.22418"
+ y="453.06482">Finish</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.77699995;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 357.21235,529.36939 0,-67.3435"
+ id="path5551"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <image
+ y="528.07153"
+ x="355.14368"
+ id="image5605"
+ height="2"
+ width="117"
+ xlink:href="file:///Users/thijstriemstra/Desktop/inkscape_pasted_image_20091210_211422.png"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:18px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="693.24731"
+ y="396.24796"
+ id="text5609"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan5611"
+ x="693.24731"
+ y="396.24796">Render</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.63258982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 774.86673,265.48334 c 0,143.1595 0,143.1595 0,143.1595 l 0,0"
+ id="path5613"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.98251843;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 676.93043,409.22247 c 98.723,0 98.723,0 98.723,0"
+ id="path5615"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.74416637;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow2Lend)"
+ d="m 677.69453,408.55589 0,59.90598"
+ id="path5807"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:100%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="601.7937"
+ y="536.74927"
+ id="text6031"
+ sodipodi:linespacing="100%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan6033"
+ x="601.7937"
+ y="536.74927">Templating</tspan><tspan
+ sodipodi:role="line"
+ x="601.7937"
+ y="560.74927"
+ id="tspan6035">System</tspan></text>
+ <rect
+ style="fill:#e2ddd8;fill-opacity:1;stroke:#000000;stroke-width:2.27699995;stroke-miterlimit:4;stroke-dasharray:none"
+ id="rect6058"
+ width="136.48538"
+ height="210.41495"
+ x="33.1385"
+ y="127.60599"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <text
+ xml:space="preserve"
+ style="font-size:20px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="229.00862"
+ y="183.2704"
+ id="text6060"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan6062"
+ x="229.00862"
+ y="183.2704">Request</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:20px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="244.9366"
+ y="270.70905"
+ id="text6064"
+ sodipodi:linespacing="125%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan6066"
+ x="244.9366"
+ y="270.70905">Response</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:18px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:100%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
+ x="99.959412"
+ y="236.50031"
+ id="text6068"
+ sodipodi:linespacing="100%"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001"><tspan
+ sodipodi:role="line"
+ id="tspan6070"
+ x="99.959412"
+ y="236.50031">Browser</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.76616168;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:url(#Arrow2Lstart)"
+ d="m 298.85972,194.46803 c -128.91105,0 -128.91105,0 -128.91105,0"
+ id="path6871"
+ inkscape:export-filename="/Users/thijstriemstra/Desktop/layer1.png"
+ inkscape:export-xdpi="54.700001"
+ inkscape:export-ydpi="54.700001" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:1.77699995;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow2Lstart)"
+ d="m 301.95575,280.22714 -130.10029,0 0,0 0,0 0,0 0,0"
+ id="path7812" />
+</svg>
diff --git a/doc/web/img/web-session.png b/doc/web/img/web-session.png
new file mode 100644
index 0000000..c4aeba7
--- /dev/null
+++ b/doc/web/img/web-session.png
Binary files differ
diff --git a/doc/web/img/web-widgets.dia b/doc/web/img/web-widgets.dia
new file mode 100644
index 0000000..6c6b37a
--- /dev/null
+++ b/doc/web/img/web-widgets.dia
Binary files differ
diff --git a/doc/web/img/web-widgets.png b/doc/web/img/web-widgets.png
new file mode 100644
index 0000000..6fef28e
--- /dev/null
+++ b/doc/web/img/web-widgets.png
Binary files differ
diff --git a/doc/web/index.html b/doc/web/index.html
new file mode 100644
index 0000000..c058873
--- /dev/null
+++ b/doc/web/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Web Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Web Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Web to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Web</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/words/examples/cursesclient.py b/doc/words/examples/cursesclient.py
new file mode 100644
index 0000000..c739d47
--- /dev/null
+++ b/doc/words/examples/cursesclient.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is an example of integrating curses with the twisted underlying
+select loop. Most of what is in this is insignificant -- the main piece
+of interest is the 'CursesStdIO' class.
+
+This class acts as file-descriptor 0, and is scheduled with the twisted
+select loop via reactor.addReader (once the curses class extends it
+of course). When there is input waiting doRead is called, and any
+input-oriented curses calls (ie. getch()) should be executed within this
+block.
+
+Remember to call nodelay(1) in curses, to make getch() non-blocking.
+"""
+
+# System Imports
+import curses, time, traceback, sys
+import curses.wrapper
+
+# Twisted imports
+from twisted.internet import reactor
+from twisted.internet.protocol import ClientFactory
+from twisted.words.protocols.irc import IRCClient
+from twisted.python import log
+
+class TextTooLongError(Exception):
+ pass
+
+class CursesStdIO:
+ """fake fd to be registered as a reader with the twisted reactor.
+ Curses classes needing input should extend this"""
+
+ def fileno(self):
+ """ We want to select on FD 0 """
+ return 0
+
+ def doRead(self):
+ """called when input is ready"""
+
+ def logPrefix(self): return 'CursesClient'
+
+
+class IRC(IRCClient):
+
+ """ A protocol object for IRC """
+
+ nickname = "testcurses"
+
+ def __init__(self, screenObj):
+ # screenObj should be 'stdscr' or a curses window/pad object
+ self.screenObj = screenObj
+ # for testing (hacky way around initial bad design for this example) :)
+ self.screenObj.irc = self
+
+ def lineReceived(self, line):
+ """ When receiving a line, add it to the output buffer """
+ self.screenObj.addLine(line)
+
+ def connectionMade(self):
+ IRCClient.connectionMade(self)
+ self.screenObj.addLine("* CONNECTED")
+
+ def clientConnectionLost(self, connection, reason):
+ pass
+
+
+class IRCFactory(ClientFactory):
+
+ """
+ Factory used for creating IRC protocol objects
+ """
+
+ protocol = IRC
+
+ def __init__(self, screenObj):
+ self.irc = self.protocol(screenObj)
+
+ def buildProtocol(self, addr=None):
+ return self.irc
+
+ def clientConnectionLost(self, conn, reason):
+ pass
+
+
+class Screen(CursesStdIO):
+ def __init__(self, stdscr):
+ self.timer = 0
+ self.statusText = "TEST CURSES APP -"
+ self.searchText = ''
+ self.stdscr = stdscr
+
+ # set screen attributes
+ self.stdscr.nodelay(1) # this is used to make input calls non-blocking
+ curses.cbreak()
+ self.stdscr.keypad(1)
+ curses.curs_set(0) # no annoying mouse cursor
+
+ self.rows, self.cols = self.stdscr.getmaxyx()
+ self.lines = []
+
+ curses.start_color()
+
+ # create color pair's 1 and 2
+ curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
+
+ self.paintStatus(self.statusText)
+
+ def connectionLost(self, reason):
+ self.close()
+
+ def addLine(self, text):
+ """ add a line to the internal list of lines"""
+
+ self.lines.append(text)
+ self.redisplayLines()
+
+ def redisplayLines(self):
+ """ method for redisplaying lines
+ based on internal list of lines """
+
+ self.stdscr.clear()
+ self.paintStatus(self.statusText)
+ i = 0
+ index = len(self.lines) - 1
+ while i < (self.rows - 3) and index >= 0:
+ self.stdscr.addstr(self.rows - 3 - i, 0, self.lines[index],
+ curses.color_pair(2))
+ i = i + 1
+ index = index - 1
+ self.stdscr.refresh()
+
+ def paintStatus(self, text):
+ if len(text) > self.cols: raise TextTooLongError
+ self.stdscr.addstr(self.rows-2,0,text + ' ' * (self.cols-len(text)),
+ curses.color_pair(1))
+ # move cursor to input line
+ self.stdscr.move(self.rows-1, self.cols-1)
+
+ def doRead(self):
+ """ Input is ready! """
+ curses.noecho()
+ self.timer = self.timer + 1
+ c = self.stdscr.getch() # read a character
+
+ if c == curses.KEY_BACKSPACE:
+ self.searchText = self.searchText[:-1]
+
+ elif c == curses.KEY_ENTER or c == 10:
+ self.addLine(self.searchText)
+ # for testing too
+ try: self.irc.sendLine(self.searchText)
+ except: pass
+ self.stdscr.refresh()
+ self.searchText = ''
+
+ else:
+ if len(self.searchText) == self.cols-2: return
+ self.searchText = self.searchText + chr(c)
+
+ self.stdscr.addstr(self.rows-1, 0,
+ self.searchText + (' ' * (
+ self.cols-len(self.searchText)-2)))
+ self.stdscr.move(self.rows-1, len(self.searchText))
+ self.paintStatus(self.statusText + ' %d' % len(self.searchText))
+ self.stdscr.refresh()
+
+ def close(self):
+ """ clean up """
+
+ curses.nocbreak()
+ self.stdscr.keypad(0)
+ curses.echo()
+ curses.endwin()
+
+if __name__ == '__main__':
+ stdscr = curses.initscr() # initialize curses
+ screen = Screen(stdscr) # create Screen object
+ stdscr.refresh()
+ ircFactory = IRCFactory(screen)
+ reactor.addReader(screen) # add screen object as a reader to the reactor
+ reactor.connectTCP("irc.freenode.net",6667,ircFactory) # connect to IRC
+ reactor.run() # have fun!
+ screen.close()
diff --git a/doc/words/examples/index.html b/doc/words/examples/index.html
new file mode 100644
index 0000000..3a6bfb1
--- /dev/null
+++ b/doc/words/examples/index.html
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Words code examples</title>
+<link href="../howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Words code examples</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+ <span/>
+
+ <ul>
+ <li><a href="ircLogBot.py" shape="rect">ircLogBot.py</a> - connects to an IRC server and logs all messages</li>
+ <li><a href="minchat.py" shape="rect">minchat.py</a> - log bot using twisted.im</li>
+ <li><a href="msn_example.py" shape="rect">msn_example.py</a></li>
+ <li><a href="oscardemo.py" shape="rect">oscardemo.py</a></li>
+ <li><a href="jabber_client.py" shape="rect">jabber_client.py</a></li>
+ <li><a href="pb_client.py" shape="rect">pb_client.py</a></li>
+ <li><a href="xmpp_client.py" shape="rect">xmpp_client.py</a></li>
+ <li><a href="cursesclient.py" shape="rect">cursesclient.py</a> - trivial curses-based IRC client</li>
+ </ul>
+
+</div>
+
+ <p><a href="../howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/words/examples/ircLogBot.py b/doc/words/examples/ircLogBot.py
new file mode 100644
index 0000000..18cc7d6
--- /dev/null
+++ b/doc/words/examples/ircLogBot.py
@@ -0,0 +1,158 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An example IRC log bot - logs a channel's events to a file.
+
+If someone says the bot's name in the channel followed by a ':',
+e.g.
+
+ <foo> logbot: hello!
+
+the bot will reply:
+
+ <logbot> foo: I am a log bot
+
+Run this script with two arguments, the channel name the bot should
+connect to, and file to log to, e.g.:
+
+ $ python ircLogBot.py test test.log
+
+will log channel #test to the file 'test.log'.
+"""
+
+
+# twisted imports
+from twisted.words.protocols import irc
+from twisted.internet import reactor, protocol
+from twisted.python import log
+
+# system imports
+import time, sys
+
+
+class MessageLogger:
+ """
+ An independent logger class (because separation of application
+ and protocol logic is a good thing).
+ """
+ def __init__(self, file):
+ self.file = file
+
+ def log(self, message):
+ """Write a message to the file."""
+ timestamp = time.strftime("[%H:%M:%S]", time.localtime(time.time()))
+ self.file.write('%s %s\n' % (timestamp, message))
+ self.file.flush()
+
+ def close(self):
+ self.file.close()
+
+
+class LogBot(irc.IRCClient):
+ """A logging IRC bot."""
+
+ nickname = "twistedbot"
+
+ def connectionMade(self):
+ irc.IRCClient.connectionMade(self)
+ self.logger = MessageLogger(open(self.factory.filename, "a"))
+ self.logger.log("[connected at %s]" %
+ time.asctime(time.localtime(time.time())))
+
+ def connectionLost(self, reason):
+ irc.IRCClient.connectionLost(self, reason)
+ self.logger.log("[disconnected at %s]" %
+ time.asctime(time.localtime(time.time())))
+ self.logger.close()
+
+
+ # callbacks for events
+
+ def signedOn(self):
+ """Called when bot has succesfully signed on to server."""
+ self.join(self.factory.channel)
+
+ def joined(self, channel):
+ """This will get called when the bot joins the channel."""
+ self.logger.log("[I have joined %s]" % channel)
+
+ def privmsg(self, user, channel, msg):
+ """This will get called when the bot receives a message."""
+ user = user.split('!', 1)[0]
+ self.logger.log("<%s> %s" % (user, msg))
+
+ # Check to see if they're sending me a private message
+ if channel == self.nickname:
+ msg = "It isn't nice to whisper! Play nice with the group."
+ self.msg(user, msg)
+ return
+
+ # Otherwise check to see if it is a message directed at me
+ if msg.startswith(self.nickname + ":"):
+ msg = "%s: I am a log bot" % user
+ self.msg(channel, msg)
+ self.logger.log("<%s> %s" % (self.nickname, msg))
+
+ def action(self, user, channel, msg):
+ """This will get called when the bot sees someone do an action."""
+ user = user.split('!', 1)[0]
+ self.logger.log("* %s %s" % (user, msg))
+
+ # irc callbacks
+
+ def irc_NICK(self, prefix, params):
+ """Called when an IRC user changes their nickname."""
+ old_nick = prefix.split('!')[0]
+ new_nick = params[0]
+ self.logger.log("%s is now known as %s" % (old_nick, new_nick))
+
+
+ # For fun, override the method that determines how a nickname is changed on
+ # collisions. The default method appends an underscore.
+ def alterCollidedNick(self, nickname):
+ """
+ Generate an altered version of a nickname that caused a collision in an
+ effort to create an unused related name for subsequent registration.
+ """
+ return nickname + '^'
+
+
+
+class LogBotFactory(protocol.ClientFactory):
+ """A factory for LogBots.
+
+ A new protocol instance will be created each time we connect to the server.
+ """
+
+ def __init__(self, channel, filename):
+ self.channel = channel
+ self.filename = filename
+
+ def buildProtocol(self, addr):
+ p = LogBot()
+ p.factory = self
+ return p
+
+ def clientConnectionLost(self, connector, reason):
+ """If we get disconnected, reconnect to server."""
+ connector.connect()
+
+ def clientConnectionFailed(self, connector, reason):
+ print "connection failed:", reason
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ # initialize logging
+ log.startLogging(sys.stdout)
+
+ # create factory protocol and application
+ f = LogBotFactory(sys.argv[1], sys.argv[2])
+
+ # connect factory to this host and port
+ reactor.connectTCP("irc.freenode.net", 6667, f)
+
+ # run bot
+ reactor.run()
diff --git a/doc/words/examples/jabber_client.py b/doc/words/examples/jabber_client.py
new file mode 100644
index 0000000..c4d6f5f
--- /dev/null
+++ b/doc/words/examples/jabber_client.py
@@ -0,0 +1,29 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# Originally written by Darryl Vandorp
+# http://randomthoughts.vandorp.ca/
+
+from twisted.words.protocols.jabber import client, jid
+from twisted.words.xish import domish
+from twisted.internet import reactor
+
+def authd(xmlstream):
+ print "authenticated"
+
+ presence = domish.Element(('jabber:client','presence'))
+ xmlstream.send(presence)
+
+ xmlstream.addObserver('/message', debug)
+ xmlstream.addObserver('/presence', debug)
+ xmlstream.addObserver('/iq', debug)
+
+def debug(elem):
+ print elem.toXml().encode('utf-8')
+ print "="*20
+
+myJid = jid.JID('username@server.jabber/twisted_words')
+factory = client.basicClientFactory(myJid, 'password')
+factory.addBootstrap('//event/stream/authd',authd)
+reactor.connectTCP('server.jabber',5222,factory)
+reactor.run()
diff --git a/doc/words/examples/minchat.py b/doc/words/examples/minchat.py
new file mode 100644
index 0000000..5ea2d0c
--- /dev/null
+++ b/doc/words/examples/minchat.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+A very simple twisted.words.im-based logbot.
+
+To run the script:
+$ python minchat.py
+"""
+
+from twisted.words.im import basechat, baseaccount, ircsupport
+
+
+# A list of account objects. We might as well create them at runtime, this is
+# supposed to be a Minimalist Implementation, after all.
+
+accounts = [
+ ircsupport.IRCAccount("IRC", 1,
+ "Tooty", # nickname
+ "", # passwd
+ "irc.freenode.net", # irc server
+ 6667, # port
+ "#twisted", # comma-seperated list of channels
+ )
+]
+
+
+class AccountManager (baseaccount.AccountManager):
+ """
+ This class is a minimal implementation of the Acccount Manager.
+
+ Most implementations will show some screen that lets the user add and
+ remove accounts, but we're not quite that sophisticated.
+ """
+
+ def __init__(self):
+
+ self.chatui = MinChat()
+
+ if len(accounts) == 0:
+ print "You have defined no accounts."
+ for acct in accounts:
+ acct.logOn(self.chatui)
+
+
+class MinConversation(basechat.Conversation):
+ """
+ This class is a minimal implementation of the abstract Conversation class.
+
+ This is all you need to override to receive one-on-one messages.
+ """
+ def show(self):
+ """
+ If you don't have a GUI, this is a no-op.
+ """
+ pass
+
+ def hide(self):
+ """
+ If you don't have a GUI, this is a no-op.
+ """
+ pass
+
+ def showMessage(self, text, metadata=None):
+ print "<%s> %s" % (self.person.name, text)
+
+ def contactChangedNick(self, person, newnick):
+ basechat.Conversation.contactChangedNick(self, person, newnick)
+ print "-!- %s is now known as %s" % (person.name, newnick)
+
+
+class MinGroupConversation(basechat.GroupConversation):
+ """
+ This class is a minimal implementation of the abstract GroupConversation class.
+
+ This is all you need to override to listen in on a group conversaion.
+ """
+ def show(self):
+ """
+ If you don't have a GUI, this is a no-op.
+ """
+ pass
+
+ def hide(self):
+ """
+ If you don't have a GUI, this is a no-op.
+ """
+ pass
+
+ def showGroupMessage(self, sender, text, metadata=None):
+ print "<%s/%s> %s" % (sender, self.group.name, text)
+
+ def setTopic(self, topic, author):
+ print "-!- %s set the topic of %s to: %s" % (author,
+ self.group.name, topic)
+
+ def memberJoined(self, member):
+ basechat.GroupConversation.memberJoined(self, member)
+ print "-!- %s joined %s" % (member, self.group.name)
+
+ def memberChangedNick(self, oldnick, newnick):
+ basechat.GroupConversation.memberChangedNick(self, oldnick, newnick)
+ print "-!- %s is now known as %s in %s" % (oldnick, newnick,
+ self.group.name)
+
+ def memberLeft(self, member):
+ basechat.GroupConversation.memberLeft(self, member)
+ print "-!- %s left %s" % (member, self.group.name)
+
+
+class MinChat(basechat.ChatUI):
+ """
+ This class is a minimal implementation of the abstract ChatUI class.
+
+ There are only two methods that need overriding - and of those two,
+ the only change that needs to be made is the default value of the Class
+ parameter.
+ """
+
+ def getGroupConversation(self, group, Class=MinGroupConversation,
+ stayHidden=0):
+
+ return basechat.ChatUI.getGroupConversation(self, group, Class,
+ stayHidden)
+
+ def getConversation(self, person, Class=MinConversation,
+ stayHidden=0):
+
+ return basechat.ChatUI.getConversation(self, person, Class, stayHidden)
+
+
+if __name__ == "__main__":
+ from twisted.internet import reactor
+
+ AccountManager()
+
+ reactor.run()
diff --git a/doc/words/examples/msn_example.py b/doc/words/examples/msn_example.py
new file mode 100644
index 0000000..8e4d648
--- /dev/null
+++ b/doc/words/examples/msn_example.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# Twisted Imports
+from twisted.internet import reactor
+from twisted.internet.protocol import ClientFactory
+from twisted.words.protocols import msn
+from twisted.python import log
+
+# System Imports
+import sys, getpass
+
+"""
+This example connects to the MSN chat service and
+prints out information about all the users on your
+contact list (both online and offline).
+
+The main aim of this example is to demonstrate
+the connection process.
+
+@author Samuel Jordan
+"""
+
+
+def _createNotificationFac():
+ fac = msn.NotificationFactory()
+ fac.userHandle = USER_HANDLE
+ fac.password = PASSWORD
+ fac.protocol = Notification
+ return fac
+
+class Dispatch(msn.DispatchClient):
+
+ def __init__(self):
+ msn.DispatchClient.__init__(self)
+ self.userHandle = USER_HANDLE
+
+ def gotNotificationReferral(self, host, port):
+ self.transport.loseConnection()
+ reactor.connectTCP(host, port, _createNotificationFac())
+
+class Notification(msn.NotificationClient):
+
+ def loginFailure(self, message):
+ print 'Login failure:', message
+
+ def listSynchronized(self, *args):
+ contactList = self.factory.contacts
+ print 'Contact list has been synchronized, number of contacts = %s' % len(contactList.getContacts())
+ for contact in contactList.getContacts().values():
+ print 'Contact: %s' % (contact.screenName,)
+ print ' email: %s' % (contact.userHandle,)
+ print ' groups:'
+ for group in contact.groups:
+ print ' - %s' % contactList.groups[group]
+ print
+
+if __name__ == '__main__':
+ USER_HANDLE = raw_input("Email (passport): ")
+ PASSWORD = getpass.getpass()
+ log.startLogging(sys.stdout)
+ _dummy_fac = ClientFactory()
+ _dummy_fac.protocol = Dispatch
+ reactor.connectTCP('messenger.hotmail.com', 1863, _dummy_fac)
+ reactor.run()
diff --git a/doc/words/examples/oscardemo.py b/doc/words/examples/oscardemo.py
new file mode 100755
index 0000000..ec59328
--- /dev/null
+++ b/doc/words/examples/oscardemo.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.words.protocols import oscar
+from twisted.internet import protocol, reactor
+import getpass
+
+SN = raw_input('Username: ') # replace this with a screenname
+PASS = getpass.getpass('Password: ')# replace this with a password
+if SN[0].isdigit():
+ icqMode = 1
+ hostport = ('login.icq.com', 5238)
+else:
+ hostport = ('login.oscar.aol.com', 5190)
+ icqMode = 0
+
+class B(oscar.BOSConnection):
+ capabilities = [oscar.CAP_CHAT]
+ def initDone(self):
+ self.requestSelfInfo().addCallback(self.gotSelfInfo)
+ self.requestSSI().addCallback(self.gotBuddyList)
+ def gotSelfInfo(self, user):
+ print user.__dict__
+ self.name = user.name
+ def gotBuddyList(self, l):
+ print l
+ self.activateSSI()
+ self.setProfile("""this is a test of the current twisted.oscar code.<br>
+current features:<br>
+* send me a message, and you should get it back.<br>
+* invite me to a chat room. i'll repeat what people say. say 'leave' and i'll go.<br>
+* also, i hang out in '%s Chat'. join that, i'll repeat what you say there.<br>
+* try warning me. just try it.<br>
+<br>
+if any of those features don't work, tell paul (Z3Penguin). thanks."""%SN)
+ self.setIdleTime(0)
+ self.clientReady()
+ self.createChat('%s Chat'%SN).addCallback(self.createdRoom)
+ def createdRoom(self, (exchange, fullName, instance)):
+ print 'created room',exchange, fullName, instance
+ self.joinChat(exchange, fullName, instance).addCallback(self.chatJoined)
+ def updateBuddy(self, user):
+ print user
+ def offlineBuddy(self, user):
+ print 'offline', user.name
+ def receiveMessage(self, user, multiparts, flags):
+ print user.name, multiparts, flags
+ self.getAway(user.name).addCallback(self.gotAway, user.name)
+ if multiparts[0][0].find('away')!=-1:
+ self.setAway('I am away from my computer right now.')
+ elif multiparts[0][0].find('back')!=-1:
+ self.setAway(None)
+ if self.awayMessage:
+ self.sendMessage(user.name,'<html><font color="#0000ff">'+self.awayMessage,autoResponse=1)
+ else:
+ self.lastUser = user.name
+ self.sendMessage(user.name, multiparts, wantAck = 1, autoResponse = (self.awayMessage!=None)).addCallback( \
+ self.messageAck)
+ def messageAck(self, (username, message)):
+ print 'message sent to %s acked' % username
+ def gotAway(self, away, user):
+ if away != None:
+ print 'got away for',user,':',away
+ def receiveWarning(self, newLevel, user):
+ print 'got warning from', hasattr(user,'name') and user.name or None
+ print 'new warning level', newLevel
+ if not user:
+ #username = self.lastUser
+ return
+ else:
+ username = user.name
+ self.warnUser(username).addCallback(self.warnedUser, username)
+ def warnedUser(self, oldLevel, newLevel, username):
+ self.sendMessage(username,'muahaha :-p')
+ def receiveChatInvite(self, user, message, exchange, fullName, instance, shortName, inviteTime):
+ print 'chat invite from',user.name,'for room',shortName,'with message:',message
+ self.joinChat(exchange, fullName, instance).addCallback(self.chatJoined)
+ def chatJoined(self, chat):
+ print 'joined chat room', chat.name
+ print 'members:',map(lambda x:x.name,chat.members)
+ def chatReceiveMessage(self, chat, user, message):
+ print 'message to',chat.name,'from',user.name,':',message
+ if user.name!=self.name: chat.sendMessage(user.name+': '+message)
+ if message.find('leave')!=-1 and chat.name!='%s Chat'%SN: chat.leaveChat()
+ def chatMemberJoined(self, chat, member):
+ print member.name,'joined',chat.name
+ def chatMemberLeft(self, chat, member):
+ print member.name,'left',chat.name
+ print 'current members',map(lambda x:x.name,chat.members)
+ if chat.name!="%s Chat"%SN and len(chat.members)==1:
+ print 'leaving', chat.name
+ chat.leaveChat()
+
+class OA(oscar.OscarAuthenticator):
+ BOSClass = B
+
+protocol.ClientCreator(reactor, OA, SN, PASS, icq=icqMode).connectTCP(*hostport)
+reactor.run()
diff --git a/doc/words/examples/pb_client.py b/doc/words/examples/pb_client.py
new file mode 100644
index 0000000..128890c
--- /dev/null
+++ b/doc/words/examples/pb_client.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Simple PB Words client demo
+
+This connects to a server (host/port specified by argv[1]/argv[2]),
+authenticates with a username and password (given by argv[3] and argv[4]),
+joins a group (argv[5]) sends a simple message, leaves the group, and quits
+the server.
+"""
+
+import sys
+from twisted.python import log
+from twisted.cred import credentials
+from twisted.words import service
+from twisted.spread import pb
+from twisted.internet import reactor
+
+class DemoMind(service.PBMind):
+ """An utterly pointless PBMind subclass.
+
+ This notices messages received and prints them to stdout. Since
+ the bot never stays in a channel very long, it is exceedingly
+ unlikely this will ever do anything interesting.
+ """
+ def remote_receive(self, sender, recipient, message):
+ print 'Woop', sender, recipient, message
+
+def quitServer(ignored):
+ """Quit succeeded, shut down the reactor.
+ """
+ reactor.stop()
+
+def leftGroup(ignored, avatar):
+ """Left the group successfully, quit the server.
+ """
+ q = avatar.quit()
+ q.addCallback(quitServer)
+ return q
+
+def sentMessage(ignored, group, avatar):
+ """Sent the message successfully, leave the group.
+ """
+ l = group.leave()
+ l.addCallback(leftGroup, avatar)
+ return l
+
+def joinedGroup(group, avatar):
+ """Joined the group successfully, send a stupid message.
+ """
+ s = group.send({"text": "Hello, monkeys"})
+ s.addCallback(sentMessage, group, avatar)
+ return s
+
+def loggedIn(avatar, group):
+ """Logged in successfully, join a group.
+ """
+ j = avatar.join(group)
+ j.addCallback(joinedGroup, avatar)
+ return j
+
+def errorOccurred(err):
+ """Something went awry, log it and shutdown.
+ """
+ log.err(err)
+ try:
+ reactor.stop()
+ except RuntimeError:
+ pass
+
+def run(host, port, username, password, group):
+ """Create a mind and factory and set things in motion.
+ """
+ m = DemoMind()
+ f = pb.PBClientFactory()
+ f.unsafeTracebacks = True
+ l = f.login(credentials.UsernamePassword(username, password), m)
+ l.addCallback(loggedIn, group)
+ l.addErrback(errorOccurred)
+ reactor.connectTCP(host, int(port), f)
+
+def main():
+ """
+ Set up logging, have the real main function run, and start the reactor.
+ """
+ if len(sys.argv) != 6:
+ raise SystemExit("Usage: %s host port username password group" % (sys.argv[0],))
+ log.startLogging(sys.stdout)
+
+ host, port, username, password, group = sys.argv[1:]
+ port = int(port)
+ username = username.decode(sys.stdin.encoding)
+ group = group.decode(sys.stdin.encoding)
+
+ reactor.callWhenRunning(run, host, port, username, password, group)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/doc/words/examples/xmpp_client.py b/doc/words/examples/xmpp_client.py
new file mode 100644
index 0000000..a99dc68
--- /dev/null
+++ b/doc/words/examples/xmpp_client.py
@@ -0,0 +1,82 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+from twisted.internet import reactor
+from twisted.names.srvconnect import SRVConnector
+from twisted.words.xish import domish
+from twisted.words.protocols.jabber import xmlstream, client, jid
+
+
+class XMPPClientConnector(SRVConnector):
+ def __init__(self, reactor, domain, factory):
+ SRVConnector.__init__(self, reactor, 'xmpp-client', domain, factory)
+
+
+ def pickServer(self):
+ host, port = SRVConnector.pickServer(self)
+
+ if not self.servers and not self.orderedServers:
+ # no SRV record, fall back..
+ port = 5222
+
+ return host, port
+
+
+
+class Client(object):
+ def __init__(self, client_jid, secret):
+ f = client.XMPPClientFactory(client_jid, secret)
+ f.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self.connected)
+ f.addBootstrap(xmlstream.STREAM_END_EVENT, self.disconnected)
+ f.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.authenticated)
+ f.addBootstrap(xmlstream.INIT_FAILED_EVENT, self.init_failed)
+ connector = XMPPClientConnector(reactor, client_jid.host, f)
+ connector.connect()
+
+
+ def rawDataIn(self, buf):
+ print "RECV: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace')
+
+
+ def rawDataOut(self, buf):
+ print "SEND: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace')
+
+
+ def connected(self, xs):
+ print 'Connected.'
+
+ self.xmlstream = xs
+
+ # Log all traffic
+ xs.rawDataInFn = self.rawDataIn
+ xs.rawDataOutFn = self.rawDataOut
+
+
+ def disconnected(self, xs):
+ print 'Disconnected.'
+
+ reactor.stop()
+
+
+ def authenticated(self, xs):
+ print "Authenticated."
+
+ presence = domish.Element((None, 'presence'))
+ xs.send(presence)
+
+ reactor.callLater(5, xs.sendFooter)
+
+
+ def init_failed(self, failure):
+ print "Initialization failed."
+ print failure
+
+ self.xmlstream.sendFooter()
+
+
+client_jid = jid.JID(sys.argv[1])
+secret = sys.argv[2]
+c = Client(client_jid, secret)
+
+reactor.run()
diff --git a/doc/words/howto/im.html b/doc/words/howto/im.html
new file mode 100644
index 0000000..0efb089
--- /dev/null
+++ b/doc/words/howto/im.html
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Overview of Twisted IM</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Overview of Twisted IM</h1>
+ <div class="toc"><ol><li><a href="#auto0">Code flow</a></li><ul><li><a href="#auto1">AccountManager</a></li><li><a href="#auto2">ChatUI</a></li><li><a href="#auto3">Conversation and GroupConversation</a></li><li><a href="#auto4">Accounts</a></li></ul></ol></div>
+ <div class="content">
+<span/>
+
+ <p>Twisted IM (Instance Messenger) is a multi-protocol chat
+ framework, based on the Twisted framework we've all come to know
+ and love. It's fairly simple and extensible in two directions -
+ it's pretty easy to add new protocols, and it's also quite easy
+ to add new front-ends.</p>
+
+ <h2>Code flow<a name="auto0"/></h2>
+
+ <h3>AccountManager<a name="auto1"/></h3>
+ <p>The control flow starts at the relevant subclass of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.baseaccount.AccountManager.html" title="twisted.words.im.baseaccount.AccountManager">baseaccount.AccountManager</a></code>.
+ The AccountManager is responsible for, well, managing accounts
+ - remembering what accounts are available, their
+ settings, adding and removal of accounts, and making accounts
+ log on at startup.</p>
+
+ <p>This would be a good place to start your interface, load a
+ list of accounts from disk and tell them to login. Most of the
+ method names in <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.baseaccount.AccountManager.html" title="twisted.words.im.baseaccount.AccountManager">AccountManager</a></code>
+ are pretty self-explanatory, and your subclass can override
+ whatever it wants, but you <em>need</em> to override <code class="python">__init__</code>. Something like
+ this:</p>
+
+ <pre class="python"><p class="py-linenumber">1
+2
+3
+4
+5
+6
+</p>...
+ <span class="py-src-keyword">def</span> <span class="py-src-identifier">__init__</span>(<span class="py-src-parameter">self</span>):
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">chatui</span> = ... <span class="py-src-comment"># Your subclass of basechat.ChatUI</span>
+ <span class="py-src-variable">self</span>.<span class="py-src-variable">accounts</span> = ... <span class="py-src-comment"># Load account list</span>
+ <span class="py-src-keyword">for</span> <span class="py-src-variable">a</span> <span class="py-src-keyword">in</span> <span class="py-src-variable">self</span>.<span class="py-src-variable">accounts</span>:
+ <span class="py-src-variable">a</span>.<span class="py-src-variable">logOn</span>(<span class="py-src-variable">self</span>.<span class="py-src-variable">chatui</span>)
+</pre>
+
+ <h3>ChatUI<a name="auto2"/></h3>
+ <p>Account objects talk to the user via a subclass of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.basechat.ChatUI.html" title="twisted.words.im.basechat.ChatUI">basechat.ChatUI</a></code>.
+ This class keeps track of all the various conversations that
+ are currently active, so that when an account receives and
+ incoming message, it can put that message in its correct
+ context.</p>
+
+ <p>How much of this class you need to override depends on what
+ you need to do. You will need to override
+ <code>getConversation</code> (a one-on-one conversation, like
+ an IRC DCC chat) and <code>getGroupConversation</code> (a
+ multiple user conversation, like an IRC channel). You might
+ want to override <code>getGroup</code> and
+ <code>getPerson</code>.</p>
+
+ <p>The main problem with the default versions of the above
+ routines is that they take a parameter, <code>Class</code>,
+ which defaults to an abstract implementation of that class -
+ for example, <code>getConversation</code> has a
+ <code>Class</code> parameter that defaults to <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.basechat.Conversation.html" title="twisted.words.im.basechat.Conversation">basechat.Conversation</a></code> which
+ raises a lot of <code>NotImplementedError</code>s. In your
+ subclass, override the method with a new method whose Class
+ parameter defaults to your own implementation of
+ <code>Conversation</code>, that simply calls the parent
+ class' implementation.</p>
+
+ <h3>Conversation and GroupConversation<a name="auto3"/></h3>
+ <p>These classes are where your interface meets the chat
+ protocol. Chat protocols get a message, find the appropriate
+ <code>Conversation</code> or <code>GroupConversation</code>
+ object, and call its methods when various interesting things
+ happen.</p>
+
+ <p>Override whatever methods you want to get the information
+ you want to display. You must override the <code>hide</code>
+ and <code>show</code> methods, however - they are called
+ frequently and the default implementation raises
+ <code>NotImplementedError</code>.</p>
+
+ <h3>Accounts<a name="auto4"/></h3>
+ <p>An account is an instance of a subclass of <code class="API"><a href="http://twistedmatrix.com/documents/12.1.0/api/twisted.words.im.basesupport.AbstractAccount.html" title="twisted.words.im.basesupport.AbstractAccount">basesupport.AbstractAccount</a></code>.
+ For more details and sample code, see the various
+ <code>*support</code> files in <code>twisted.words.im</code>.</p>
+
+ </div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/words/howto/index.html b/doc/words/howto/index.html
new file mode 100644
index 0000000..26e9705
--- /dev/null
+++ b/doc/words/howto/index.html
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted IM Documentation</title>
+<link href="stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted IM Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+
+<span/>
+
+<ul class="toc">
+ <li><a href="im.html" shape="rect">Twisted IM</a></li>
+</ul>
+</div>
+
+ <p><a href="index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/doc/words/index.html b/doc/words/index.html
new file mode 100644
index 0000000..711f3e7
--- /dev/null
+++ b/doc/words/index.html
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+<title>Twisted Documentation: Twisted Words Documentation</title>
+<link href="howto/stylesheet.css" rel="stylesheet" type="text/css"/>
+ </head>
+
+ <body bgcolor="white">
+ <h1 class="title">Twisted Words Documentation</h1>
+ <div class="toc"><ol/></div>
+ <div class="content">
+<span/>
+
+<ul>
+<li><a href="howto/index.html" shape="rect">Developer guides</a>: documentation on using
+Twisted Words to develop your own applications</li>
+<li><a href="examples/index.html" shape="rect">Examples</a>: short code examples using
+Twisted Words</li>
+</ul>
+
+</div>
+
+ <p><a href="howto/index.html">Index</a></p>
+ <span class="version">Version: 12.1.0</span>
+ </body>
+</html> \ No newline at end of file
diff --git a/packaging/python-twisted.changes b/packaging/python-twisted.changes
new file mode 100644
index 0000000..2850f96
--- /dev/null
+++ b/packaging/python-twisted.changes
@@ -0,0 +1,2 @@
+* Fri Aug 31 22:21:08 UTC 2012 - jimmy.huang@intel.com
+- Intial import from upstream.
diff --git a/packaging/python-twisted.spec b/packaging/python-twisted.spec
new file mode 100644
index 0000000..476912a
--- /dev/null
+++ b/packaging/python-twisted.spec
@@ -0,0 +1,63 @@
+Name: python-twisted
+Version: 12.1.0
+Release: 1
+Group: System/Libraries
+License: MIT
+Url: http://twistedmatrix.com/
+Summary: An asynchronous networking framework written in Python
+Source: http://pypi.python.org/packages/source/T/Twisted/Twisted-%{version}.tar.bz2
+BuildRequires: pkgconfig(python)
+BuildRequires: python-zope.interface
+Requires: python-xml
+Requires: python-pyOpenSSL
+Requires: python-PyPAM
+Requires: python-zope.interface
+Provides: python-twisted
+Provides: python-twisted-core
+Provides: python-twisted-conch
+Provides: python-twisted-lore
+Provides: python-twisted-mail
+Provides: python-twisted-name
+Provides: python-twisted-news
+Provides: python-twisted-runner
+Provides: python-twisted-web
+Provides: python-twisted-words
+Provides: python-twisted-xish
+
+%description
+An extensible framework for Python programming, with special focus
+on event-based network programming and multiprotocol integration.
+
+%prep
+%setup -q -n Twisted-%{version}
+
+%build
+python setup.py build
+
+%install
+python setup.py install --prefix=%{_prefix} --root=%{buildroot}
+find %{buildroot} -regex '.*\.[ch]' -exec rm {} ";" # Remove leftover C sources
+install -dm0755 %{buildroot}/%{_mandir}/man1/
+install -m0644 doc/*/man/*.1 %{buildroot}/%{_mandir}/man1/ # Install man pages
+find doc -type f -print0 | xargs -0 chmod a-x # Fix doc-file dependency by removing x flags
+sed -i "s/\r//" doc/core/howto/listings/udp/{MulticastClient,MulticastServer}.py
+
+%files
+%defattr(-,root,root,-)
+%doc LICENSE NEWS README
+%doc doc/*
+%{_bindir}/cftp
+%{_bindir}/ckeygen
+%{_bindir}/conch
+%{_bindir}/lore
+%{_bindir}/mailmail
+%{_bindir}/manhole
+%{_bindir}/pyhtmlizer
+%{_bindir}/tap2deb
+%{_bindir}/tap2rpm
+%{_bindir}/tapconvert
+%{_bindir}/tkconch
+%{_bindir}/trial
+%{_bindir}/twistd
+%{_mandir}/man1/*
+%{python_sitearch}/*
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..f66341a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Distutils installer for Twisted.
+"""
+
+try:
+ # Load setuptools, to build a specific source package
+ import setuptools
+except ImportError:
+ pass
+
+import sys, os
+
+
+def getExtensions():
+ """
+ Get all extensions from core and all subprojects.
+ """
+ extensions = []
+
+ if not sys.platform.startswith('java'):
+ for dir in os.listdir("twisted") + [""]:
+ topfiles = os.path.join("twisted", dir, "topfiles")
+ if os.path.isdir(topfiles):
+ ns = {}
+ setup_py = os.path.join(topfiles, "setup.py")
+ execfile(setup_py, ns, ns)
+ if "extensions" in ns:
+ extensions.extend(ns["extensions"])
+
+ return extensions
+
+
+def main(args):
+ """
+ Invoke twisted.python.dist with the appropriate metadata about the
+ Twisted package.
+ """
+ if os.path.exists('twisted'):
+ sys.path.insert(0, '.')
+ from twisted import copyright
+ from twisted.python.dist import getDataFiles, getScripts, getPackages, \
+ setup, twisted_subprojects
+
+ # "" is included because core scripts are directly in bin/
+ projects = [''] + [x for x in os.listdir('bin')
+ if os.path.isdir(os.path.join("bin", x))
+ and x in twisted_subprojects]
+
+ scripts = []
+ for i in projects:
+ scripts.extend(getScripts(i))
+
+ setup_args = dict(
+ # metadata
+ name="Twisted",
+ version=copyright.version,
+ description="An asynchronous networking framework written in Python",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Glyph Lefkowitz",
+ maintainer_email="glyph@twistedmatrix.com",
+ url="http://twistedmatrix.com/",
+ license="MIT",
+ long_description="""\
+An extensible framework for Python programming, with special focus
+on event-based network programming and multiprotocol integration.
+""",
+ packages = getPackages('twisted'),
+ conditionalExtensions = getExtensions(),
+ scripts = scripts,
+ data_files=getDataFiles('twisted'),
+ classifiers=[
+ "Programming Language :: Python :: 2.5",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ ])
+
+ if 'setuptools' in sys.modules:
+ from pkg_resources import parse_requirements
+ requirements = ["zope.interface"]
+ try:
+ list(parse_requirements(requirements))
+ except:
+ print """You seem to be running a very old version of setuptools.
+This version of setuptools has a bug parsing dependencies, so automatic
+dependency resolution is disabled.
+"""
+ else:
+ setup_args['install_requires'] = requirements
+ setup_args['include_package_data'] = True
+ setup_args['zip_safe'] = False
+ setup(**setup_args)
+
+
+if __name__ == "__main__":
+ try:
+ main(sys.argv[1:])
+ except KeyboardInterrupt:
+ sys.exit(1)
+
diff --git a/twisted/__init__.py b/twisted/__init__.py
new file mode 100644
index 0000000..22eb40c
--- /dev/null
+++ b/twisted/__init__.py
@@ -0,0 +1,24 @@
+# -*- test-case-name: twisted -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted: The Framework Of Your Internet.
+"""
+
+# Ensure the user is running the version of python we require.
+import sys
+if not hasattr(sys, "version_info") or sys.version_info < (2, 5):
+ raise RuntimeError("Twisted requires Python 2.5 or later.")
+del sys
+
+# Ensure compat gets imported
+from twisted.python import compat
+del compat
+
+# setup version
+from twisted._version import version
+__version__ = version.short()
+
diff --git a/twisted/_version.py b/twisted/_version.py
new file mode 100644
index 0000000..7503d97
--- /dev/null
+++ b/twisted/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted', 12, 1, 0)
diff --git a/twisted/application/__init__.py b/twisted/application/__init__.py
new file mode 100644
index 0000000..c155ca4
--- /dev/null
+++ b/twisted/application/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""
+Configuration objects for Twisted Applications
+"""
diff --git a/twisted/application/app.py b/twisted/application/app.py
new file mode 100644
index 0000000..97f7a42
--- /dev/null
+++ b/twisted/application/app.py
@@ -0,0 +1,674 @@
+# -*- test-case-name: twisted.test.test_application,twisted.test.test_twistd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os, pdb, getpass, traceback, signal
+from operator import attrgetter
+
+from twisted.python import runtime, log, usage, failure, util, logfile
+from twisted.python.versions import Version
+from twisted.python.reflect import qual, namedAny
+from twisted.python.deprecate import deprecated
+from twisted.python.log import ILogObserver
+from twisted.persisted import sob
+from twisted.application import service, reactors
+from twisted.internet import defer
+from twisted import copyright, plugin
+
+# Expose the new implementation of installReactor at the old location.
+from twisted.application.reactors import installReactor
+from twisted.application.reactors import NoSuchReactor
+
+
+
+class _BasicProfiler(object):
+ """
+ @ivar saveStats: if C{True}, save the stats information instead of the
+ human readable format
+ @type saveStats: C{bool}
+
+ @ivar profileOutput: the name of the file use to print profile data.
+ @type profileOutput: C{str}
+ """
+
+ def __init__(self, profileOutput, saveStats):
+ self.profileOutput = profileOutput
+ self.saveStats = saveStats
+
+
+ def _reportImportError(self, module, e):
+ """
+ Helper method to report an import error with a profile module. This
+ has to be explicit because some of these modules are removed by
+ distributions due to them being non-free.
+ """
+ s = "Failed to import module %s: %s" % (module, e)
+ s += """
+This is most likely caused by your operating system not including
+the module due to it being non-free. Either do not use the option
+--profile, or install the module; your operating system vendor
+may provide it in a separate package.
+"""
+ raise SystemExit(s)
+
+
+
+class ProfileRunner(_BasicProfiler):
+ """
+ Runner for the standard profile module.
+ """
+
+ def run(self, reactor):
+ """
+ Run reactor under the standard profiler.
+ """
+ try:
+ import profile
+ except ImportError, e:
+ self._reportImportError("profile", e)
+
+ p = profile.Profile()
+ p.runcall(reactor.run)
+ if self.saveStats:
+ p.dump_stats(self.profileOutput)
+ else:
+ tmp, sys.stdout = sys.stdout, open(self.profileOutput, 'a')
+ try:
+ p.print_stats()
+ finally:
+ sys.stdout, tmp = tmp, sys.stdout
+ tmp.close()
+
+
+
+class HotshotRunner(_BasicProfiler):
+ """
+ Runner for the hotshot profile module.
+ """
+
+ def run(self, reactor):
+ """
+ Run reactor under the hotshot profiler.
+ """
+ try:
+ import hotshot.stats
+ except (ImportError, SystemExit), e:
+ # Certain versions of Debian (and Debian derivatives) raise
+ # SystemExit when importing hotshot if the "non-free" profiler
+ # module is not installed. Someone eventually recognized this
+ # as a bug and changed the Debian packaged Python to raise
+ # ImportError instead. Handle both exception types here in
+ # order to support the versions of Debian which have this
+ # behavior. The bug report which prompted the introduction of
+ # this highly undesirable behavior should be available online at
+ # <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=334067>.
+ # There seems to be no corresponding bug report which resulted
+ # in the behavior being removed. -exarkun
+ self._reportImportError("hotshot", e)
+
+ # this writes stats straight out
+ p = hotshot.Profile(self.profileOutput)
+ p.runcall(reactor.run)
+ if self.saveStats:
+ # stats are automatically written to file, nothing to do
+ return
+ else:
+ s = hotshot.stats.load(self.profileOutput)
+ s.strip_dirs()
+ s.sort_stats(-1)
+ if getattr(s, 'stream', None) is not None:
+ # Python 2.5 and above supports a stream attribute
+ s.stream = open(self.profileOutput, 'w')
+ s.print_stats()
+ s.stream.close()
+ else:
+ # But we have to use a trick for Python < 2.5
+ tmp, sys.stdout = sys.stdout, open(self.profileOutput, 'w')
+ try:
+ s.print_stats()
+ finally:
+ sys.stdout, tmp = tmp, sys.stdout
+ tmp.close()
+
+
+
+class CProfileRunner(_BasicProfiler):
+ """
+ Runner for the cProfile module.
+ """
+
+ def run(self, reactor):
+ """
+ Run reactor under the cProfile profiler.
+ """
+ try:
+ import cProfile, pstats
+ except ImportError, e:
+ self._reportImportError("cProfile", e)
+
+ p = cProfile.Profile()
+ p.runcall(reactor.run)
+ if self.saveStats:
+ p.dump_stats(self.profileOutput)
+ else:
+ stream = open(self.profileOutput, 'w')
+ s = pstats.Stats(p, stream=stream)
+ s.strip_dirs()
+ s.sort_stats(-1)
+ s.print_stats()
+ stream.close()
+
+
+
+class AppProfiler(object):
+ """
+ Class which selects a specific profile runner based on configuration
+ options.
+
+ @ivar profiler: the name of the selected profiler.
+ @type profiler: C{str}
+ """
+ profilers = {"profile": ProfileRunner, "hotshot": HotshotRunner,
+ "cprofile": CProfileRunner}
+
+ def __init__(self, options):
+ saveStats = options.get("savestats", False)
+ profileOutput = options.get("profile", None)
+ self.profiler = options.get("profiler", "hotshot").lower()
+ if self.profiler in self.profilers:
+ profiler = self.profilers[self.profiler](profileOutput, saveStats)
+ self.run = profiler.run
+ else:
+ raise SystemExit("Unsupported profiler name: %s" % (self.profiler,))
+
+
+
+class AppLogger(object):
+ """
+ Class managing logging faciliy of the application.
+
+ @ivar _logfilename: The name of the file to which to log, if other than the
+ default.
+ @type _logfilename: C{str}
+
+ @ivar _observerFactory: Callable object that will create a log observer, or
+ None.
+
+ @ivar _observer: log observer added at C{start} and removed at C{stop}.
+ @type _observer: C{callable}
+ """
+ _observer = None
+
+ def __init__(self, options):
+ self._logfilename = options.get("logfile", "")
+ self._observerFactory = options.get("logger") or None
+
+
+ def start(self, application):
+ """
+ Initialize the logging system.
+
+ If a customer logger was specified on the command line it will be
+ used. If not, and an L{ILogObserver} component has been set on
+ C{application}, then it will be used as the log observer. Otherwise a
+ log observer will be created based on the command-line options for
+ built-in loggers (e.g. C{--logfile}).
+
+ @param application: The application on which to check for an
+ L{ILogObserver}.
+ """
+ if self._observerFactory is not None:
+ observer = self._observerFactory()
+ else:
+ observer = application.getComponent(ILogObserver, None)
+
+ if observer is None:
+ observer = self._getLogObserver()
+ self._observer = observer
+ log.startLoggingWithObserver(self._observer)
+ self._initialLog()
+
+
+ def _initialLog(self):
+ """
+ Print twistd start log message.
+ """
+ from twisted.internet import reactor
+ log.msg("twistd %s (%s %s) starting up." % (copyright.version,
+ sys.executable,
+ runtime.shortPythonVersion()))
+ log.msg('reactor class: %s.' % (qual(reactor.__class__),))
+
+
+ def _getLogObserver(self):
+ """
+ Create a log observer to be added to the logging system before running
+ this application.
+ """
+ if self._logfilename == '-' or not self._logfilename:
+ logFile = sys.stdout
+ else:
+ logFile = logfile.LogFile.fromFullPath(self._logfilename)
+ return log.FileLogObserver(logFile).emit
+
+
+ def stop(self):
+ """
+ Print twistd stop log message.
+ """
+ log.msg("Server Shut Down.")
+ if self._observer is not None:
+ log.removeObserver(self._observer)
+ self._observer = None
+
+
+
+def fixPdb():
+ def do_stop(self, arg):
+ self.clear_all_breaks()
+ self.set_continue()
+ from twisted.internet import reactor
+ reactor.callLater(0, reactor.stop)
+ return 1
+
+
+ def help_stop(self):
+ print """stop - Continue execution, then cleanly shutdown the twisted reactor."""
+
+
+ def set_quit(self):
+ os._exit(0)
+
+ pdb.Pdb.set_quit = set_quit
+ pdb.Pdb.do_stop = do_stop
+ pdb.Pdb.help_stop = help_stop
+
+
+
+def runReactorWithLogging(config, oldstdout, oldstderr, profiler=None, reactor=None):
+ """
+ Start the reactor, using profiling if specified by the configuration, and
+ log any error happening in the process.
+
+ @param config: configuration of the twistd application.
+ @type config: L{ServerOptions}
+
+ @param oldstdout: initial value of C{sys.stdout}.
+ @type oldstdout: C{file}
+
+ @param oldstderr: initial value of C{sys.stderr}.
+ @type oldstderr: C{file}
+
+ @param profiler: object used to run the reactor with profiling.
+ @type profiler: L{AppProfiler}
+
+ @param reactor: The reactor to use. If C{None}, the global reactor will
+ be used.
+ """
+ if reactor is None:
+ from twisted.internet import reactor
+ try:
+ if config['profile']:
+ if profiler is not None:
+ profiler.run(reactor)
+ elif config['debug']:
+ sys.stdout = oldstdout
+ sys.stderr = oldstderr
+ if runtime.platformType == 'posix':
+ signal.signal(signal.SIGUSR2, lambda *args: pdb.set_trace())
+ signal.signal(signal.SIGINT, lambda *args: pdb.set_trace())
+ fixPdb()
+ pdb.runcall(reactor.run)
+ else:
+ reactor.run()
+ except:
+ if config['nodaemon']:
+ file = oldstdout
+ else:
+ file = open("TWISTD-CRASH.log",'a')
+ traceback.print_exc(file=file)
+ file.flush()
+
+
+
+def getPassphrase(needed):
+ if needed:
+ return getpass.getpass('Passphrase: ')
+ else:
+ return None
+
+
+
+def getSavePassphrase(needed):
+ if needed:
+ passphrase = util.getPassword("Encryption passphrase: ")
+ else:
+ return None
+
+
+
+class ApplicationRunner(object):
+ """
+ An object which helps running an application based on a config object.
+
+ Subclass me and implement preApplication and postApplication
+ methods. postApplication generally will want to run the reactor
+ after starting the application.
+
+ @ivar config: The config object, which provides a dict-like interface.
+
+ @ivar application: Available in postApplication, but not
+ preApplication. This is the application object.
+
+ @ivar profilerFactory: Factory for creating a profiler object, able to
+ profile the application if options are set accordingly.
+
+ @ivar profiler: Instance provided by C{profilerFactory}.
+
+ @ivar loggerFactory: Factory for creating object responsible for logging.
+
+ @ivar logger: Instance provided by C{loggerFactory}.
+ """
+ profilerFactory = AppProfiler
+ loggerFactory = AppLogger
+
+ def __init__(self, config):
+ self.config = config
+ self.profiler = self.profilerFactory(config)
+ self.logger = self.loggerFactory(config)
+
+
+ def run(self):
+ """
+ Run the application.
+ """
+ self.preApplication()
+ self.application = self.createOrGetApplication()
+
+ self.logger.start(self.application)
+
+ self.postApplication()
+ self.logger.stop()
+
+
+ def startReactor(self, reactor, oldstdout, oldstderr):
+ """
+ Run the reactor with the given configuration. Subclasses should
+ probably call this from C{postApplication}.
+
+ @see: L{runReactorWithLogging}
+ """
+ runReactorWithLogging(
+ self.config, oldstdout, oldstderr, self.profiler, reactor)
+
+
+ def preApplication(self):
+ """
+ Override in subclass.
+
+ This should set up any state necessary before loading and
+ running the Application.
+ """
+ raise NotImplementedError()
+
+
+ def postApplication(self):
+ """
+ Override in subclass.
+
+ This will be called after the application has been loaded (so
+ the C{application} attribute will be set). Generally this
+ should start the application and run the reactor.
+ """
+ raise NotImplementedError()
+
+
+ def createOrGetApplication(self):
+ """
+ Create or load an Application based on the parameters found in the
+ given L{ServerOptions} instance.
+
+ If a subcommand was used, the L{service.IServiceMaker} that it
+ represents will be used to construct a service to be added to
+ a newly-created Application.
+
+ Otherwise, an application will be loaded based on parameters in
+ the config.
+ """
+ if self.config.subCommand:
+ # If a subcommand was given, it's our responsibility to create
+ # the application, instead of load it from a file.
+
+ # loadedPlugins is set up by the ServerOptions.subCommands
+ # property, which is iterated somewhere in the bowels of
+ # usage.Options.
+ plg = self.config.loadedPlugins[self.config.subCommand]
+ ser = plg.makeService(self.config.subOptions)
+ application = service.Application(plg.tapname)
+ ser.setServiceParent(application)
+ else:
+ passphrase = getPassphrase(self.config['encrypted'])
+ application = getApplication(self.config, passphrase)
+ return application
+
+
+
+def getApplication(config, passphrase):
+ s = [(config[t], t)
+ for t in ['python', 'source', 'file'] if config[t]][0]
+ filename, style = s[0], {'file':'pickle'}.get(s[1],s[1])
+ try:
+ log.msg("Loading %s..." % filename)
+ application = service.loadApplication(filename, style, passphrase)
+ log.msg("Loaded.")
+ except Exception, e:
+ s = "Failed to load application: %s" % e
+ if isinstance(e, KeyError) and e.args[0] == "application":
+ s += """
+Could not find 'application' in the file. To use 'twistd -y', your .tac
+file must create a suitable object (e.g., by calling service.Application())
+and store it in a variable named 'application'. twistd loads your .tac file
+and scans the global variables for one of this name.
+
+Please read the 'Using Application' HOWTO for details.
+"""
+ traceback.print_exc(file=log.logfile)
+ log.msg(s)
+ log.deferr()
+ sys.exit('\n' + s + '\n')
+ return application
+
+
+
+def _reactorAction():
+ return usage.CompleteList([r.shortName for r in reactors.getReactorTypes()])
+
+
+class ReactorSelectionMixin:
+ """
+ Provides options for selecting a reactor to install.
+
+ If a reactor is installed, the short name which was used to locate it is
+ saved as the value for the C{"reactor"} key.
+ """
+ compData = usage.Completions(
+ optActions={"reactor": _reactorAction})
+
+ messageOutput = sys.stdout
+ _getReactorTypes = staticmethod(reactors.getReactorTypes)
+
+
+ def opt_help_reactors(self):
+ """
+ Display a list of possibly available reactor names.
+ """
+ rcts = sorted(self._getReactorTypes(), key=attrgetter('shortName'))
+ for r in rcts:
+ self.messageOutput.write(' %-4s\t%s\n' %
+ (r.shortName, r.description))
+ raise SystemExit(0)
+
+
+ def opt_reactor(self, shortName):
+ """
+ Which reactor to use (see --help-reactors for a list of possibilities)
+ """
+ # 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.
+ #
+ # This could probably be improved somehow.
+ try:
+ installReactor(shortName)
+ except NoSuchReactor:
+ msg = ("The specified reactor does not exist: '%s'.\n"
+ "See the list of available reactors with "
+ "--help-reactors" % (shortName,))
+ raise usage.UsageError(msg)
+ except Exception, e:
+ msg = ("The specified reactor cannot be used, failed with error: "
+ "%s.\nSee the list of available reactors with "
+ "--help-reactors" % (e,))
+ raise usage.UsageError(msg)
+ else:
+ self["reactor"] = shortName
+ opt_r = opt_reactor
+
+
+
+
+class ServerOptions(usage.Options, ReactorSelectionMixin):
+
+ longdesc = ("twistd reads a twisted.application.service.Application out "
+ "of a file and runs it.")
+
+ optFlags = [['savestats', None,
+ "save the Stats object rather than the text output of "
+ "the profiler."],
+ ['no_save','o', "do not save state on shutdown"],
+ ['encrypted', 'e',
+ "The specified tap/aos file is encrypted."]]
+
+ optParameters = [['logfile','l', None,
+ "log to a specified file, - for stdout"],
+ ['logger', None, None,
+ "A fully-qualified name to a log observer factory to use "
+ "for the initial log observer. Takes precedence over "
+ "--logfile and --syslog (when available)."],
+ ['profile', 'p', None,
+ "Run in profile mode, dumping results to specified file"],
+ ['profiler', None, "hotshot",
+ "Name of the profiler to use (%s)." %
+ ", ".join(AppProfiler.profilers)],
+ ['file','f','twistd.tap',
+ "read the given .tap file"],
+ ['python','y', None,
+ "read an application from within a Python file "
+ "(implies -o)"],
+ ['source', 's', None,
+ "Read an application from a .tas file (AOT format)."],
+ ['rundir','d','.',
+ 'Change to a supplied directory before running']]
+
+ compData = usage.Completions(
+ mutuallyExclusive=[("file", "python", "source")],
+ optActions={"file": usage.CompleteFiles("*.tap"),
+ "python": usage.CompleteFiles("*.(tac|py)"),
+ "source": usage.CompleteFiles("*.tas"),
+ "rundir": usage.CompleteDirs()}
+ )
+
+ _getPlugins = staticmethod(plugin.getPlugins)
+
+ def __init__(self, *a, **kw):
+ self['debug'] = False
+ usage.Options.__init__(self, *a, **kw)
+
+
+ def opt_debug(self):
+ """
+ Run the application in the Python Debugger (implies nodaemon),
+ sending SIGUSR2 will drop into debugger
+ """
+ defer.setDebugging(True)
+ failure.startDebugMode()
+ self['debug'] = True
+ opt_b = opt_debug
+
+
+ def opt_spew(self):
+ """
+ Print an insanely verbose log of everything that happens.
+ Useful when debugging freezes or locks in complex code."""
+ sys.settrace(util.spewer)
+ try:
+ import threading
+ except ImportError:
+ return
+ threading.settrace(util.spewer)
+
+
+ def parseOptions(self, options=None):
+ if options is None:
+ options = sys.argv[1:] or ["--help"]
+ usage.Options.parseOptions(self, options)
+
+
+ def postOptions(self):
+ if self.subCommand or self['python']:
+ self['no_save'] = True
+ if self['logger'] is not None:
+ try:
+ self['logger'] = namedAny(self['logger'])
+ except Exception, e:
+ raise usage.UsageError("Logger '%s' could not be imported: %s"
+ % (self['logger'], e))
+
+
+ def subCommands(self):
+ plugins = self._getPlugins(service.IServiceMaker)
+ self.loadedPlugins = {}
+ for plug in sorted(plugins, key=attrgetter('tapname')):
+ self.loadedPlugins[plug.tapname] = plug
+ yield (plug.tapname,
+ None,
+ # Avoid resolving the options attribute right away, in case
+ # it's a property with a non-trivial getter (eg, one which
+ # imports modules).
+ lambda plug=plug: plug.options(),
+ plug.description)
+ subCommands = property(subCommands)
+
+
+
+def run(runApp, ServerOptions):
+ config = ServerOptions()
+ try:
+ config.parseOptions()
+ except usage.error, ue:
+ print config
+ print "%s: %s" % (sys.argv[0], ue)
+ else:
+ runApp(config)
+
+
+
+def convertStyle(filein, typein, passphrase, fileout, typeout, encrypt):
+ application = service.loadApplication(filein, typein, passphrase)
+ sob.IPersistable(application).setStyle(typeout)
+ passphrase = getSavePassphrase(encrypt)
+ if passphrase:
+ fileout = None
+ sob.IPersistable(application).save(filename=fileout, passphrase=passphrase)
+
+
+
+def startApplication(application, save):
+ from twisted.internet import reactor
+ service.IService(application).startService()
+ if save:
+ p = sob.IPersistable(application)
+ reactor.addSystemEventTrigger('after', 'shutdown', p.save, 'shutdown')
+ reactor.addSystemEventTrigger('before', 'shutdown',
+ service.IService(application).stopService)
+
diff --git a/twisted/application/internet.py b/twisted/application/internet.py
new file mode 100644
index 0000000..c0fa4e9
--- /dev/null
+++ b/twisted/application/internet.py
@@ -0,0 +1,408 @@
+# -*- test-case-name: twisted.application.test.test_internet,twisted.test.test_application,twisted.test.test_cooperator -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Reactor-based Services
+
+Here are services to run clients, servers and periodic services using
+the reactor.
+
+If you want to run a server service, L{StreamServerEndpointService} defines a
+service that can wrap an arbitrary L{IStreamServerEndpoint
+<twisted.internet.interfaces.IStreamServerEndpoint>}
+as an L{IService}. See also L{twisted.application.strports.service} for
+constructing one of these directly from a descriptive string.
+
+Additionally, this module (dynamically) defines various Service subclasses that
+let you represent clients and servers in a Service hierarchy. Endpoints APIs
+should be preferred for stream server services, but since those APIs do not yet
+exist for clients or datagram services, many of these are still useful.
+
+They are as follows::
+
+ TCPServer, TCPClient,
+ UNIXServer, UNIXClient,
+ SSLServer, SSLClient,
+ UDPServer, UDPClient,
+ UNIXDatagramServer, UNIXDatagramClient,
+ MulticastServer
+
+These classes take arbitrary arguments in their constructors and pass
+them straight on to their respective reactor.listenXXX or
+reactor.connectXXX calls.
+
+For example, the following service starts a web server on port 8080:
+C{TCPServer(8080, server.Site(r))}. See the documentation for the
+reactor.listen/connect* methods for more information.
+"""
+
+import warnings
+
+from twisted.python import log
+from twisted.application import service
+from twisted.internet import task
+
+from twisted.internet.defer import CancelledError
+
+
+def _maybeGlobalReactor(maybeReactor):
+ """
+ @return: the argument, or the global reactor if the argument is C{None}.
+ """
+ if maybeReactor is None:
+ from twisted.internet import reactor
+ return reactor
+ else:
+ return maybeReactor
+
+
+class _VolatileDataService(service.Service):
+
+ volatile = []
+
+ def __getstate__(self):
+ d = service.Service.__getstate__(self)
+ for attr in self.volatile:
+ if attr in d:
+ del d[attr]
+ return d
+
+
+
+class _AbstractServer(_VolatileDataService):
+ """
+ @cvar volatile: list of attribute to remove from pickling.
+ @type volatile: C{list}
+
+ @ivar method: the type of method to call on the reactor, one of B{TCP},
+ B{UDP}, B{SSL} or B{UNIX}.
+ @type method: C{str}
+
+ @ivar reactor: the current running reactor.
+ @type reactor: a provider of C{IReactorTCP}, C{IReactorUDP},
+ C{IReactorSSL} or C{IReactorUnix}.
+
+ @ivar _port: instance of port set when the service is started.
+ @type _port: a provider of L{twisted.internet.interfaces.IListeningPort}.
+ """
+
+ volatile = ['_port']
+ method = None
+ reactor = None
+
+ _port = None
+
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ if 'reactor' in kwargs:
+ self.reactor = kwargs.pop("reactor")
+ self.kwargs = kwargs
+
+
+ def privilegedStartService(self):
+ service.Service.privilegedStartService(self)
+ self._port = self._getPort()
+
+
+ def startService(self):
+ service.Service.startService(self)
+ if self._port is None:
+ self._port = self._getPort()
+
+
+ def stopService(self):
+ service.Service.stopService(self)
+ # TODO: if startup failed, should shutdown skip stopListening?
+ # _port won't exist
+ if self._port is not None:
+ d = self._port.stopListening()
+ del self._port
+ return d
+
+
+ def _getPort(self):
+ """
+ Wrapper around the appropriate listen method of the reactor.
+
+ @return: the port object returned by the listen method.
+ @rtype: an object providing
+ L{twisted.internet.interfaces.IListeningPort}.
+ """
+ return getattr(_maybeGlobalReactor(self.reactor),
+ 'listen%s' % (self.method,))(*self.args, **self.kwargs)
+
+
+
+class _AbstractClient(_VolatileDataService):
+ """
+ @cvar volatile: list of attribute to remove from pickling.
+ @type volatile: C{list}
+
+ @ivar method: the type of method to call on the reactor, one of B{TCP},
+ B{UDP}, B{SSL} or B{UNIX}.
+ @type method: C{str}
+
+ @ivar reactor: the current running reactor.
+ @type reactor: a provider of C{IReactorTCP}, C{IReactorUDP},
+ C{IReactorSSL} or C{IReactorUnix}.
+
+ @ivar _connection: instance of connection set when the service is started.
+ @type _connection: a provider of L{twisted.internet.interfaces.IConnector}.
+ """
+ volatile = ['_connection']
+ method = None
+ reactor = None
+
+ _connection = None
+
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ if 'reactor' in kwargs:
+ self.reactor = kwargs.pop("reactor")
+ self.kwargs = kwargs
+
+
+ def startService(self):
+ service.Service.startService(self)
+ self._connection = self._getConnection()
+
+
+ def stopService(self):
+ service.Service.stopService(self)
+ if self._connection is not None:
+ self._connection.disconnect()
+ del self._connection
+
+
+ def _getConnection(self):
+ """
+ Wrapper around the appropriate connect method of the reactor.
+
+ @return: the port object returned by the connect method.
+ @rtype: an object providing L{twisted.internet.interfaces.IConnector}.
+ """
+ return getattr(_maybeGlobalReactor(self.reactor),
+ 'connect%s' % (self.method,))(*self.args, **self.kwargs)
+
+
+
+_doc={
+'Client':
+"""Connect to %(tran)s
+
+Call reactor.connect%(method)s when the service starts, with the
+arguments given to the constructor.
+""",
+'Server':
+"""Serve %(tran)s clients
+
+Call reactor.listen%(method)s when the service starts, with the
+arguments given to the constructor. When the service stops,
+stop listening. See twisted.internet.interfaces for documentation
+on arguments to the reactor method.
+""",
+}
+
+import types
+for tran in 'TCP UNIX SSL UDP UNIXDatagram Multicast'.split():
+ for side in 'Server Client'.split():
+ if tran == "Multicast" and side == "Client":
+ continue
+ base = globals()['_Abstract'+side]
+ method = {'Generic': 'With'}.get(tran, tran)
+ doc = _doc[side]%vars()
+ klass = types.ClassType(tran+side, (base,),
+ {'method': method, '__doc__': doc})
+ globals()[tran+side] = klass
+
+
+
+class GenericServer(_AbstractServer):
+ """
+ Serve Generic clients
+
+ Call reactor.listenWith when the service starts, with the arguments given to
+ the constructor. When the service stops, stop listening. See
+ twisted.internet.interfaces for documentation on arguments to the reactor
+ method.
+
+ This service is deprecated (because reactor.listenWith is deprecated).
+ """
+ method = 'With'
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ 'GenericServer was deprecated in Twisted 10.1.',
+ category=DeprecationWarning,
+ stacklevel=2)
+ _AbstractServer.__init__(self, *args, **kwargs)
+
+
+
+class GenericClient(_AbstractClient):
+ """
+ Connect to Generic.
+
+ Call reactor.connectWith when the service starts, with the arguments given
+ to the constructor.
+
+ This service is deprecated (because reactor.connectWith is deprecated).
+ """
+ method = 'With'
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ 'GenericClient was deprecated in Twisted 10.1.',
+ category=DeprecationWarning,
+ stacklevel=2)
+ _AbstractClient.__init__(self, *args, **kwargs)
+
+
+
+class TimerService(_VolatileDataService):
+
+ """Service to periodically call a function
+
+ Every C{step} seconds call the given function with the given arguments.
+ The service starts the calls when it starts, and cancels them
+ when it stops.
+ """
+
+ volatile = ['_loop']
+
+ def __init__(self, step, callable, *args, **kwargs):
+ self.step = step
+ self.call = (callable, args, kwargs)
+
+ def startService(self):
+ service.Service.startService(self)
+ callable, args, kwargs = self.call
+ # we have to make a new LoopingCall each time we're started, because
+ # an active LoopingCall remains active when serialized. If
+ # LoopingCall were a _VolatileDataService, we wouldn't need to do
+ # this.
+ self._loop = task.LoopingCall(callable, *args, **kwargs)
+ self._loop.start(self.step, now=True).addErrback(self._failed)
+
+ def _failed(self, why):
+ # make a note that the LoopingCall is no longer looping, so we don't
+ # try to shut it down a second time in stopService. I think this
+ # should be in LoopingCall. -warner
+ self._loop.running = False
+ log.err(why)
+
+ def stopService(self):
+ if self._loop.running:
+ self._loop.stop()
+ return service.Service.stopService(self)
+
+
+
+class CooperatorService(service.Service):
+ """
+ Simple L{service.IService} which starts and stops a L{twisted.internet.task.Cooperator}.
+ """
+ def __init__(self):
+ self.coop = task.Cooperator(started=False)
+
+
+ def coiterate(self, iterator):
+ return self.coop.coiterate(iterator)
+
+
+ def startService(self):
+ self.coop.start()
+
+
+ def stopService(self):
+ self.coop.stop()
+
+
+
+class StreamServerEndpointService(service.Service, object):
+ """
+ A L{StreamServerEndpointService} is an L{IService} which runs a server on a
+ listening port described by an L{IStreamServerEndpoint
+ <twisted.internet.interfaces.IStreamServerEndpoint>}.
+
+ @ivar factory: A server factory which will be used to listen on the
+ endpoint.
+
+ @ivar endpoint: An L{IStreamServerEndpoint
+ <twisted.internet.interfaces.IStreamServerEndpoint>} provider
+ which will be used to listen when the service starts.
+
+ @ivar _waitingForPort: a Deferred, if C{listen} has yet been invoked on the
+ endpoint, otherwise None.
+
+ @ivar _raiseSynchronously: Defines error-handling behavior for the case
+ where C{listen(...)} raises an exception before C{startService} or
+ C{privilegedStartService} have completed.
+
+ @type _raiseSynchronously: C{bool}
+
+ @since: 10.2
+ """
+
+ _raiseSynchronously = None
+
+ def __init__(self, endpoint, factory):
+ self.endpoint = endpoint
+ self.factory = factory
+ self._waitingForPort = None
+
+
+ def privilegedStartService(self):
+ """
+ Start listening on the endpoint.
+ """
+ service.Service.privilegedStartService(self)
+ self._waitingForPort = self.endpoint.listen(self.factory)
+ raisedNow = []
+ def handleIt(err):
+ if self._raiseSynchronously:
+ raisedNow.append(err)
+ elif not err.check(CancelledError):
+ log.err(err)
+ self._waitingForPort.addErrback(handleIt)
+ if raisedNow:
+ raisedNow[0].raiseException()
+
+
+ def startService(self):
+ """
+ Start listening on the endpoint, unless L{privilegedStartService} got
+ around to it already.
+ """
+ service.Service.startService(self)
+ if self._waitingForPort is None:
+ self.privilegedStartService()
+
+
+ def stopService(self):
+ """
+ Stop listening on the port if it is already listening, otherwise,
+ cancel the attempt to listen.
+
+ @return: a L{Deferred<twisted.internet.defer.Deferred>} which fires
+ with C{None} when the port has stopped listening.
+ """
+ self._waitingForPort.cancel()
+ def stopIt(port):
+ if port is not None:
+ return port.stopListening()
+ d = self._waitingForPort.addCallback(stopIt)
+ def stop(passthrough):
+ self.running = False
+ return passthrough
+ d.addBoth(stop)
+ return d
+
+
+
+__all__ = (['TimerService', 'CooperatorService', 'MulticastServer',
+ 'StreamServerEndpointService'] +
+ [tran+side
+ for tran in 'Generic TCP UNIX SSL UDP UNIXDatagram'.split()
+ for side in 'Server Client'.split()])
diff --git a/twisted/application/reactors.py b/twisted/application/reactors.py
new file mode 100644
index 0000000..6bae985
--- /dev/null
+++ b/twisted/application/reactors.py
@@ -0,0 +1,83 @@
+# -*- test-case-name: twisted.test.test_application -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Plugin-based system for enumerating available reactors and installing one of
+them.
+"""
+
+from zope.interface import Interface, Attribute, implements
+
+from twisted.plugin import IPlugin, getPlugins
+from twisted.python.reflect import namedAny
+
+
+class IReactorInstaller(Interface):
+ """
+ Definition of a reactor which can probably be installed.
+ """
+ shortName = Attribute("""
+ A brief string giving the user-facing name of this reactor.
+ """)
+
+ description = Attribute("""
+ A longer string giving a user-facing description of this reactor.
+ """)
+
+ def install():
+ """
+ Install this reactor.
+ """
+
+ # TODO - A method which provides a best-guess as to whether this reactor
+ # 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.
+ """
+
+
+class Reactor(object):
+ """
+ @ivar moduleName: The fully-qualified Python name of the module of which
+ the install callable is an attribute.
+ """
+ implements(IPlugin, IReactorInstaller)
+
+
+ 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.
+
+ @raise NoSuchReactor: If no reactor is found with a matching C{shortName}.
+
+ @raise: anything that the specified reactor can raise when installed.
+ """
+ for installer in getReactorTypes():
+ if installer.shortName == shortName:
+ return installer.install()
+ raise NoSuchReactor(shortName)
+
diff --git a/twisted/application/service.py b/twisted/application/service.py
new file mode 100644
index 0000000..d4a13dc
--- /dev/null
+++ b/twisted/application/service.py
@@ -0,0 +1,413 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Service architecture for Twisted.
+
+Services are arranged in a hierarchy. At the leafs of the hierarchy,
+the services which actually interact with the outside world are started.
+Services can be named or anonymous -- usually, they will be named if
+there is need to access them through the hierarchy (from a parent or
+a sibling).
+
+Maintainer: Moshe Zadka
+"""
+
+from zope.interface import implements, Interface, Attribute
+
+from twisted.python.reflect import namedAny
+from twisted.python import components
+from twisted.internet import defer
+from twisted.persisted import sob
+from twisted.plugin import IPlugin
+
+
+class IServiceMaker(Interface):
+ """
+ An object which can be used to construct services in a flexible
+ way.
+
+ This interface should most often be implemented along with
+ 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'.")
+
+ description = Attribute(
+ "A brief summary of the features provided by this "
+ "Twisted application plugin.")
+
+ options = Attribute(
+ "A C{twisted.python.usage.Options} subclass defining the "
+ "configuration options for this application.")
+
+
+ def makeService(options):
+ """
+ Create and return an object providing
+ L{twisted.application.service.IService}.
+
+ @param options: A mapping (typically a C{dict} or
+ L{twisted.python.usage.Options} instance) of configuration
+ options to desired configuration values.
+ """
+
+
+
+class ServiceMaker(object):
+ """
+ Utility class to simplify the definition of L{IServiceMaker} plugins.
+ """
+ implements(IPlugin, IServiceMaker)
+
+ def __init__(self, name, module, description, tapname):
+ self.name = name
+ self.module = module
+ self.description = description
+ self.tapname = tapname
+
+
+ def options():
+ def get(self):
+ return namedAny(self.module).Options
+ return get,
+ options = property(*options())
+
+
+ def makeService():
+ def get(self):
+ return namedAny(self.module).makeService
+ return get,
+ makeService = property(*makeService())
+
+
+
+class IService(Interface):
+ """
+ A service.
+
+ Run start-up and shut-down code at the appropriate times.
+
+ @type name: C{string}
+ @ivar name: The name of the service (or None)
+ @type running: C{boolean}
+ @ivar running: Whether the service is running.
+ """
+
+ def setName(name):
+ """
+ Set the name of the service.
+
+ @type name: C{str}
+ @raise RuntimeError: Raised if the service already has a parent.
+ """
+
+ def setServiceParent(parent):
+ """
+ Set the parent of the service. This method is responsible for setting
+ the C{parent} attribute on this service (the child service).
+
+ @type parent: L{IServiceCollection}
+ @raise RuntimeError: Raised if the service already has a parent
+ or if the service has a name and the parent already has a child
+ by that name.
+ """
+
+ def disownServiceParent():
+ """
+ Use this API to remove an L{IService} from an L{IServiceCollection}.
+
+ This method is used symmetrically with L{setServiceParent} in that it
+ sets the C{parent} attribute on the child.
+
+ @rtype: L{Deferred<defer.Deferred>}
+ @return: a L{Deferred<defer.Deferred>} which is triggered when the
+ service has finished shutting down. If shutting down is immediate,
+ a value can be returned (usually, C{None}).
+ """
+
+ def startService():
+ """
+ Start the service.
+ """
+
+ def stopService():
+ """
+ Stop the service.
+
+ @rtype: L{Deferred<defer.Deferred>}
+ @return: a L{Deferred<defer.Deferred>} which is triggered when the
+ service has finished shutting down. If shutting down is immediate,
+ a value can be returned (usually, C{None}).
+ """
+
+ def privilegedStartService():
+ """
+ Do preparation work for starting the service.
+
+ Here things which should be done before changing directory,
+ root or shedding privileges are done.
+ """
+
+
+class Service:
+ """
+ Base class for services.
+
+ Most services should inherit from this class. It handles the
+ book-keeping reponsibilities of starting and stopping, as well
+ as not serializing this book-keeping information.
+ """
+
+ implements(IService)
+
+ running = 0
+ name = None
+ parent = None
+
+ def __getstate__(self):
+ dict = self.__dict__.copy()
+ if dict.has_key("running"):
+ del dict['running']
+ return dict
+
+ def setName(self, name):
+ if self.parent is not None:
+ raise RuntimeError("cannot change name when parent exists")
+ self.name = name
+
+ def setServiceParent(self, parent):
+ if self.parent is not None:
+ self.disownServiceParent()
+ parent = IServiceCollection(parent, parent)
+ self.parent = parent
+ self.parent.addService(self)
+
+ def disownServiceParent(self):
+ d = self.parent.removeService(self)
+ self.parent = None
+ return d
+
+ def privilegedStartService(self):
+ pass
+
+ def startService(self):
+ self.running = 1
+
+ def stopService(self):
+ self.running = 0
+
+
+
+class IServiceCollection(Interface):
+ """
+ Collection of services.
+
+ Contain several services, and manage their start-up/shut-down.
+ Services can be accessed by name if they have a name, and it
+ is always possible to iterate over them.
+ """
+
+ def getServiceNamed(name):
+ """
+ Get the child service with a given name.
+
+ @type name: C{str}
+ @rtype: L{IService}
+ @raise KeyError: Raised if the service has no child with the
+ given name.
+ """
+
+ def __iter__():
+ """
+ Get an iterator over all child services.
+ """
+
+ def addService(service):
+ """
+ Add a child service.
+
+ Only implementations of L{IService.setServiceParent} should use this
+ method.
+
+ @type service: L{IService}
+ @raise RuntimeError: Raised if the service has a child with
+ the given name.
+ """
+
+ def removeService(service):
+ """
+ Remove a child service.
+
+ Only implementations of L{IService.disownServiceParent} should
+ use this method.
+
+ @type service: L{IService}
+ @raise ValueError: Raised if the given service is not a child.
+ @rtype: L{Deferred<defer.Deferred>}
+ @return: a L{Deferred<defer.Deferred>} which is triggered when the
+ service has finished shutting down. If shutting down is immediate,
+ a value can be returned (usually, C{None}).
+ """
+
+
+
+class MultiService(Service):
+ """
+ Straightforward Service Container.
+
+ Hold a collection of services, and manage them in a simplistic
+ way. No service will wait for another, but this object itself
+ will not finish shutting down until all of its child services
+ will finish.
+ """
+
+ implements(IServiceCollection)
+
+ def __init__(self):
+ self.services = []
+ self.namedServices = {}
+ self.parent = None
+
+ def privilegedStartService(self):
+ Service.privilegedStartService(self)
+ for service in self:
+ service.privilegedStartService()
+
+ def startService(self):
+ Service.startService(self)
+ for service in self:
+ service.startService()
+
+ def stopService(self):
+ Service.stopService(self)
+ l = []
+ services = list(self)
+ services.reverse()
+ for service in services:
+ l.append(defer.maybeDeferred(service.stopService))
+ return defer.DeferredList(l)
+
+ def getServiceNamed(self, name):
+ return self.namedServices[name]
+
+ def __iter__(self):
+ return iter(self.services)
+
+ def addService(self, service):
+ if service.name is not None:
+ if self.namedServices.has_key(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:
+ # It may be too late for that, but we will do our best
+ service.privilegedStartService()
+ service.startService()
+
+ def removeService(self, service):
+ if service.name:
+ del self.namedServices[service.name]
+ self.services.remove(service)
+ if self.running:
+ # Returning this so as not to lose information from the
+ # MultiService.stopService deferred.
+ return service.stopService()
+ else:
+ 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 C{None}
+ to leave the name alone).
+ """)
+
+ uid = Attribute(
+ """
+ An C{int} giving the user id as which the process should run (or
+ C{None} to leave the UID alone).
+ """)
+
+ gid = Attribute(
+ """
+ An C{int} giving the group id as which the process should run (or
+ C{None} to leave the GID alone).
+ """)
+
+
+
+class Process:
+ """
+ Process running parameters.
+
+ Sets up uid/gid in the constructor, and has a default
+ of C{None} as C{processName}.
+ """
+ implements(IProcess)
+ processName = None
+
+ def __init__(self, uid=None, gid=None):
+ """
+ Set uid and gid.
+
+ @param uid: The user ID as whom to execute the process. If
+ this is C{None}, no attempt will be made to change the UID.
+
+ @param gid: The group ID as whom to execute the process. If
+ this is C{None}, no attempt will be made to change the GID.
+ """
+ self.uid = uid
+ self.gid = gid
+
+
+def Application(name, uid=None, gid=None):
+ """
+ Return a compound class.
+
+ Return an object supporting the L{IService}, L{IServiceCollection},
+ L{IProcess} and L{sob.IPersistable} interfaces, with the given
+ parameters. Always access the return value by explicit casting to
+ one of the interfaces.
+ """
+ ret = components.Componentized()
+ for comp in (MultiService(), sob.Persistent(ret, name), Process(uid, gid)):
+ ret.addComponent(comp, ignoreClass=1)
+ IService(ret).setName(name)
+ return ret
+
+
+
+def loadApplication(filename, kind, passphrase=None):
+ """
+ Load Application from a given file.
+
+ The serialization format it was saved in should be given as
+ C{kind}, and is one of C{pickle}, C{source}, C{xml} or C{python}. If
+ C{passphrase} is given, the application was encrypted with the
+ given passphrase.
+
+ @type filename: C{str}
+ @type kind: C{str}
+ @type passphrase: C{str}
+ """
+ if kind == 'python':
+ application = sob.loadValueFromFile(filename, 'application', passphrase)
+ else:
+ application = sob.load(filename, kind, passphrase)
+ return application
+
+
+__all__ = ['IServiceMaker', 'IService', 'Service',
+ 'IServiceCollection', 'MultiService',
+ 'IProcess', 'Process', 'Application', 'loadApplication']
diff --git a/twisted/application/strports.py b/twisted/application/strports.py
new file mode 100644
index 0000000..117d76f
--- /dev/null
+++ b/twisted/application/strports.py
@@ -0,0 +1,103 @@
+# -*- test-case-name: twisted.test.test_strports -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Construct listening port services from a simple string description.
+
+@see: L{twisted.internet.endpoints.serverFromString}
+@see: L{twisted.internet.endpoints.clientFromString}
+"""
+
+import warnings
+
+from twisted.internet import endpoints
+from twisted.python.deprecate import deprecatedModuleAttribute
+from twisted.python.versions import Version
+from twisted.application.internet import StreamServerEndpointService
+
+
+
+def parse(description, factory, default='tcp'):
+ """
+ This function is deprecated as of Twisted 10.2.
+
+ @see: L{twisted.internet.endpoints.server}
+ """
+ return endpoints._parseServer(description, factory, default)
+
+deprecatedModuleAttribute(
+ Version("Twisted", 10, 2, 0),
+ "in favor of twisted.internet.endpoints.serverFromString",
+ __name__, "parse")
+
+
+
+_DEFAULT = object()
+
+def service(description, factory, default=_DEFAULT, reactor=None):
+ """
+ Return the service corresponding to a description.
+
+ @param description: The description of the listening port, in the syntax
+ described by L{twisted.internet.endpoints.server}.
+
+ @type description: C{str}
+
+ @param factory: The protocol factory which will build protocols for
+ connections to this service.
+
+ @type factory: L{twisted.internet.interfaces.IProtocolFactory}
+
+ @type default: C{str} or C{None}
+
+ @param default: Do not use this parameter. It has been deprecated since
+ Twisted 10.2.0.
+
+ @rtype: C{twisted.application.service.IService}
+
+ @return: the service corresponding to a description of a reliable
+ stream server.
+
+ @see: L{twisted.internet.endpoints.serverFromString}
+ """
+ if reactor is None:
+ from twisted.internet import reactor
+ if default is _DEFAULT:
+ default = None
+ else:
+ message = "The 'default' parameter was deprecated in Twisted 10.2.0."
+ if default is not None:
+ message += (
+ " Use qualified endpoint descriptions; for example, "
+ "'tcp:%s'." % (description,))
+ warnings.warn(
+ message=message, category=DeprecationWarning, stacklevel=2)
+ svc = StreamServerEndpointService(
+ endpoints._serverFromStringLegacy(reactor, description, default),
+ factory)
+ svc._raiseSynchronously = True
+ return svc
+
+
+
+def listen(description, factory, default=None):
+ """Listen on a port corresponding to a description
+
+ @type description: C{str}
+ @type factory: L{twisted.internet.interfaces.IProtocolFactory}
+ @type default: C{str} or C{None}
+ @rtype: C{twisted.internet.interfaces.IListeningPort}
+ @return: the port corresponding to a description of a reliable
+ virtual circuit server.
+
+ See the documentation of the C{parse} function for description
+ of the semantics of the arguments.
+ """
+ from twisted.internet import reactor
+ name, args, kw = parse(description, factory, default)
+ return getattr(reactor, 'listen'+name)(*args, **kw)
+
+
+
+__all__ = ['parse', 'service', 'listen']
diff --git a/twisted/application/test/__init__.py b/twisted/application/test/__init__.py
new file mode 100644
index 0000000..3cb9635
--- /dev/null
+++ b/twisted/application/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.application}.
+"""
diff --git a/twisted/application/test/test_internet.py b/twisted/application/test/test_internet.py
new file mode 100644
index 0000000..9e058d7
--- /dev/null
+++ b/twisted/application/test/test_internet.py
@@ -0,0 +1,252 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for (new code in) L{twisted.application.internet}.
+"""
+
+
+from zope.interface import implements
+from zope.interface.verify import verifyClass
+
+from twisted.internet.protocol import Factory
+from twisted.trial.unittest import TestCase
+from twisted.application.internet import StreamServerEndpointService
+from twisted.internet.interfaces import IStreamServerEndpoint, IListeningPort
+from twisted.internet.defer import Deferred, CancelledError
+
+class FakeServer(object):
+ """
+ In-memory implementation of L{IStreamServerEndpoint}.
+
+ @ivar result: The L{Deferred} resulting from the call to C{listen}, after
+ C{listen} has been called.
+
+ @ivar factory: The factory passed to C{listen}.
+
+ @ivar cancelException: The exception to errback C{self.result} when it is
+ cancelled.
+
+ @ivar port: The L{IListeningPort} which C{listen}'s L{Deferred} will fire
+ with.
+
+ @ivar listenAttempts: The number of times C{listen} has been invoked.
+
+ @ivar failImmediately: If set, the exception to fail the L{Deferred}
+ returned from C{listen} before it is returned.
+ """
+
+ implements(IStreamServerEndpoint)
+
+ result = None
+ factory = None
+ failImmediately = None
+ cancelException = CancelledError()
+ listenAttempts = 0
+
+ def __init__(self):
+ self.port = FakePort()
+
+
+ def listen(self, factory):
+ """
+ Return a Deferred and store it for future use. (Implementation of
+ L{IStreamServerEndpoint}).
+ """
+ self.listenAttempts += 1
+ self.factory = factory
+ self.result = Deferred(
+ canceller=lambda d: d.errback(self.cancelException))
+ if self.failImmediately is not None:
+ self.result.errback(self.failImmediately)
+ return self.result
+
+
+ def startedListening(self):
+ """
+ Test code should invoke this method after causing C{listen} to be
+ invoked in order to fire the L{Deferred} previously returned from
+ C{listen}.
+ """
+ self.result.callback(self.port)
+
+
+ def stoppedListening(self):
+ """
+ Test code should invoke this method after causing C{stopListening} to
+ be invoked on the port fired from the L{Deferred} returned from
+ C{listen} in order to cause the L{Deferred} returned from
+ C{stopListening} to fire.
+ """
+ self.port.deferred.callback(None)
+
+verifyClass(IStreamServerEndpoint, FakeServer)
+
+
+
+class FakePort(object):
+ """
+ Fake L{IListeningPort} implementation.
+
+ @ivar deferred: The L{Deferred} returned by C{stopListening}.
+ """
+
+ implements(IListeningPort)
+
+ deferred = None
+
+ def stopListening(self):
+ self.deferred = Deferred()
+ return self.deferred
+
+verifyClass(IStreamServerEndpoint, FakeServer)
+
+
+
+class TestEndpointService(TestCase):
+ """
+ Tests for L{twisted.application.internet}.
+ """
+
+ def setUp(self):
+ """
+ Construct a stub server, a stub factory, and a
+ L{StreamServerEndpointService} to test.
+ """
+ self.fakeServer = FakeServer()
+ self.factory = Factory()
+ self.svc = StreamServerEndpointService(self.fakeServer, self.factory)
+
+
+ def test_privilegedStartService(self):
+ """
+ L{StreamServerEndpointService.privilegedStartService} calls its
+ endpoint's C{listen} method with its factory.
+ """
+ self.svc.privilegedStartService()
+ self.assertIdentical(self.factory, self.fakeServer.factory)
+
+
+ def test_synchronousRaiseRaisesSynchronously(self, thunk=None):
+ """
+ L{StreamServerEndpointService.startService} should raise synchronously
+ if the L{Deferred} returned by its wrapped
+ L{IStreamServerEndpoint.listen} has already fired with an errback and
+ the L{StreamServerEndpointService}'s C{_raiseSynchronously} flag has
+ been set. This feature is necessary to preserve compatibility with old
+ behavior of L{twisted.internet.strports.service}, which is to return a
+ service which synchronously raises an exception from C{startService}
+ (so that, among other things, twistd will not start running). However,
+ since L{IStreamServerEndpoint.listen} may fail asynchronously, it is
+ a bad idea to rely on this behavior.
+ """
+ self.fakeServer.failImmediately = ZeroDivisionError()
+ self.svc._raiseSynchronously = True
+ self.assertRaises(ZeroDivisionError, thunk or self.svc.startService)
+
+
+ def test_synchronousRaisePrivileged(self):
+ """
+ L{StreamServerEndpointService.privilegedStartService} should behave the
+ same as C{startService} with respect to
+ L{TestEndpointService.test_synchronousRaiseRaisesSynchronously}.
+ """
+ self.test_synchronousRaiseRaisesSynchronously(
+ self.svc.privilegedStartService)
+
+
+ def test_failReportsError(self):
+ """
+ L{StreamServerEndpointService.startService} and
+ L{StreamServerEndpointService.privilegedStartService} should both log
+ an exception when the L{Deferred} returned from their wrapped
+ L{IStreamServerEndpoint.listen} fails.
+ """
+ self.svc.startService()
+ self.fakeServer.result.errback(ZeroDivisionError())
+ logged = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEqual(len(logged), 1)
+
+
+ def test_synchronousFailReportsError(self):
+ """
+ Without the C{_raiseSynchronously} compatibility flag, failing
+ immediately has the same behavior as failing later; it logs the error.
+ """
+ self.fakeServer.failImmediately = ZeroDivisionError()
+ self.svc.startService()
+ logged = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEqual(len(logged), 1)
+
+
+ def test_startServiceUnstarted(self):
+ """
+ L{StreamServerEndpointService.startService} sets the C{running} flag,
+ and calls its endpoint's C{listen} method with its factory, if it
+ has not yet been started.
+ """
+ self.svc.startService()
+ self.assertIdentical(self.factory, self.fakeServer.factory)
+ self.assertEqual(self.svc.running, True)
+
+
+ def test_startServiceStarted(self):
+ """
+ L{StreamServerEndpointService.startService} sets the C{running} flag,
+ but nothing else, if the service has already been started.
+ """
+ self.test_privilegedStartService()
+ self.svc.startService()
+ self.assertEqual(self.fakeServer.listenAttempts, 1)
+ self.assertEqual(self.svc.running, True)
+
+
+ def test_stopService(self):
+ """
+ L{StreamServerEndpointService.stopService} calls C{stopListening} on
+ the L{IListeningPort} returned from its endpoint, returns the
+ C{Deferred} from stopService, and sets C{running} to C{False}.
+ """
+ self.svc.privilegedStartService()
+ self.fakeServer.startedListening()
+ # Ensure running gets set to true
+ self.svc.startService()
+ result = self.svc.stopService()
+ l = []
+ result.addCallback(l.append)
+ self.assertEqual(len(l), 0)
+ self.fakeServer.stoppedListening()
+ self.assertEqual(len(l), 1)
+ self.assertFalse(self.svc.running)
+
+
+ def test_stopServiceBeforeStartFinished(self):
+ """
+ L{StreamServerEndpointService.stopService} cancels the L{Deferred}
+ returned by C{listen} if it has not yet fired. No error will be logged
+ about the cancellation of the listen attempt.
+ """
+ self.svc.privilegedStartService()
+ result = self.svc.stopService()
+ l = []
+ result.addBoth(l.append)
+ self.assertEqual(l, [None])
+ self.assertEqual(self.flushLoggedErrors(CancelledError), [])
+
+
+ def test_stopServiceCancelStartError(self):
+ """
+ L{StreamServerEndpointService.stopService} cancels the L{Deferred}
+ returned by C{listen} if it has not fired yet. An error will be logged
+ if the resulting exception is not L{CancelledError}.
+ """
+ self.fakeServer.cancelException = ZeroDivisionError()
+ self.svc.privilegedStartService()
+ result = self.svc.stopService()
+ l = []
+ result.addCallback(l.append)
+ self.assertEqual(l, [None])
+ stoppingErrors = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEqual(len(stoppingErrors), 1)
+
+
diff --git a/twisted/conch/__init__.py b/twisted/conch/__init__.py
new file mode 100644
index 0000000..d7ce597
--- /dev/null
+++ b/twisted/conch/__init__.py
@@ -0,0 +1,18 @@
+# -*- test-case-name: twisted.conch.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+
+"""
+Twisted.Conch: The Twisted Shell. Terminal emulation, SSHv2 and telnet.
+
+Currently this contains the SSHv2 implementation, but it may work over other
+protocols in the future. (i.e. Telnet)
+
+Maintainer: Paul Swartz
+"""
+
+from twisted.conch._version import version
+__version__ = version.short()
diff --git a/twisted/conch/_version.py b/twisted/conch/_version.py
new file mode 100644
index 0000000..5cfdc9f
--- /dev/null
+++ b/twisted/conch/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.conch', 12, 1, 0)
diff --git a/twisted/conch/avatar.py b/twisted/conch/avatar.py
new file mode 100644
index 0000000..a914da3
--- /dev/null
+++ b/twisted/conch/avatar.py
@@ -0,0 +1,37 @@
+# -*- test-case-name: twisted.conch.test.test_conch -*-
+from interfaces import IConchUser
+from error import ConchError
+from ssh.connection import OPEN_UNKNOWN_CHANNEL_TYPE
+from twisted.python import log
+from zope import interface
+
+class ConchUser:
+ interface.implements(IConchUser)
+
+ def __init__(self):
+ self.channelLookup = {}
+ self.subsystemLookup = {}
+
+ def lookupChannel(self, channelType, windowSize, maxPacket, data):
+ klass = self.channelLookup.get(channelType, None)
+ if not klass:
+ raise ConchError(OPEN_UNKNOWN_CHANNEL_TYPE, "unknown channel")
+ else:
+ return klass(remoteWindow = windowSize,
+ remoteMaxPacket = maxPacket,
+ data=data, avatar=self)
+
+ def lookupSubsystem(self, subsystem, data):
+ log.msg(repr(self.subsystemLookup))
+ klass = self.subsystemLookup.get(subsystem, None)
+ if not klass:
+ return False
+ return klass(data, avatar=self)
+
+ def gotGlobalRequest(self, requestType, data):
+ # XXX should this use method dispatch?
+ requestType = requestType.replace('-','_')
+ f = getattr(self, "global_%s" % requestType, None)
+ if not f:
+ return 0
+ return f(data)
diff --git a/twisted/conch/checkers.py b/twisted/conch/checkers.py
new file mode 100644
index 0000000..3cd6a0e
--- /dev/null
+++ b/twisted/conch/checkers.py
@@ -0,0 +1,308 @@
+# -*- test-case-name: twisted.conch.test.test_checkers -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Provide L{ICredentialsChecker} implementations to be used in Conch protocols.
+"""
+
+import os, base64, binascii, errno
+try:
+ import pwd
+except ImportError:
+ pwd = None
+else:
+ import crypt
+
+try:
+ # Python 2.5 got spwd to interface with shadow passwords
+ import spwd
+except ImportError:
+ spwd = None
+ try:
+ import shadow
+ except ImportError:
+ shadow = None
+else:
+ shadow = None
+
+try:
+ from twisted.cred import pamauth
+except ImportError:
+ pamauth = None
+
+from zope.interface import implements, providedBy
+
+from twisted.conch import error
+from twisted.conch.ssh import keys
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
+from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
+from twisted.internet import defer
+from twisted.python import failure, reflect, log
+from twisted.python.util import runAsEffectiveUser
+from twisted.python.filepath import FilePath
+
+
+
+def verifyCryptedPassword(crypted, pw):
+ return crypt.crypt(pw, crypted) == crypted
+
+
+
+def _pwdGetByName(username):
+ """
+ Look up a user in the /etc/passwd database using the pwd module. If the
+ pwd module is not available, return None.
+
+ @param username: the username of the user to return the passwd database
+ information for.
+ """
+ if pwd is None:
+ return None
+ return pwd.getpwnam(username)
+
+
+
+def _shadowGetByName(username):
+ """
+ Look up a user in the /etc/shadow database using the spwd or shadow
+ modules. If neither module is available, return None.
+
+ @param username: the username of the user to return the shadow database
+ information for.
+ """
+ if spwd is not None:
+ f = spwd.getspnam
+ elif shadow is not None:
+ f = shadow.getspnam
+ else:
+ return None
+ return runAsEffectiveUser(0, 0, f, username)
+
+
+
+class UNIXPasswordDatabase:
+ """
+ A checker which validates users out of the UNIX password databases, or
+ databases of a compatible format.
+
+ @ivar _getByNameFunctions: a C{list} of functions which are called in order
+ to valid a user. The default value is such that the /etc/passwd
+ database will be tried first, followed by the /etc/shadow database.
+ """
+ credentialInterfaces = IUsernamePassword,
+ implements(ICredentialsChecker)
+
+
+ def __init__(self, getByNameFunctions=None):
+ if getByNameFunctions is None:
+ getByNameFunctions = [_pwdGetByName, _shadowGetByName]
+ self._getByNameFunctions = getByNameFunctions
+
+
+ def requestAvatarId(self, credentials):
+ for func in self._getByNameFunctions:
+ try:
+ pwnam = func(credentials.username)
+ except KeyError:
+ return defer.fail(UnauthorizedLogin("invalid username"))
+ else:
+ if pwnam is not None:
+ crypted = pwnam[1]
+ if crypted == '':
+ continue
+ if verifyCryptedPassword(crypted, credentials.password):
+ return defer.succeed(credentials.username)
+ # fallback
+ return defer.fail(UnauthorizedLogin("unable to verify password"))
+
+
+
+class SSHPublicKeyDatabase:
+ """
+ Checker that authenticates SSH public keys, based on public keys listed in
+ authorized_keys and authorized_keys2 files in user .ssh/ directories.
+ """
+ implements(ICredentialsChecker)
+
+ credentialInterfaces = (ISSHPrivateKey,)
+
+ _userdb = pwd
+
+ def requestAvatarId(self, credentials):
+ d = defer.maybeDeferred(self.checkKey, credentials)
+ d.addCallback(self._cbRequestAvatarId, credentials)
+ d.addErrback(self._ebRequestAvatarId)
+ return d
+
+ def _cbRequestAvatarId(self, validKey, credentials):
+ """
+ Check whether the credentials themselves are valid, now that we know
+ if the key matches the user.
+
+ @param validKey: A boolean indicating whether or not the public key
+ matches a key in the user's authorized_keys file.
+
+ @param credentials: The credentials offered by the user.
+ @type credentials: L{ISSHPrivateKey} provider
+
+ @raise UnauthorizedLogin: (as a failure) if the key does not match the
+ user in C{credentials}. Also raised if the user provides an invalid
+ signature.
+
+ @raise ValidPublicKey: (as a failure) if the key matches the user but
+ the credentials do not include a signature. See
+ L{error.ValidPublicKey} for more information.
+
+ @return: The user's username, if authentication was successful.
+ """
+ if not validKey:
+ return failure.Failure(UnauthorizedLogin("invalid key"))
+ if not credentials.signature:
+ return failure.Failure(error.ValidPublicKey())
+ else:
+ try:
+ pubKey = keys.Key.fromString(credentials.blob)
+ if pubKey.verify(credentials.signature, credentials.sigData):
+ return credentials.username
+ except: # any error should be treated as a failed login
+ log.err()
+ return failure.Failure(UnauthorizedLogin('error while verifying key'))
+ return failure.Failure(UnauthorizedLogin("unable to verify key"))
+
+
+ def getAuthorizedKeysFiles(self, credentials):
+ """
+ Return a list of L{FilePath} instances for I{authorized_keys} files
+ which might contain information about authorized keys for the given
+ credentials.
+
+ On OpenSSH servers, the default location of the file containing the
+ list of authorized public keys is
+ U{$HOME/.ssh/authorized_keys<http://www.openbsd.org/cgi-bin/man.cgi?query=sshd_config>}.
+
+ I{$HOME/.ssh/authorized_keys2} is also returned, though it has been
+ U{deprecated by OpenSSH since
+ 2001<http://marc.info/?m=100508718416162>}.
+
+ @return: A list of L{FilePath} instances to files with the authorized keys.
+ """
+ pwent = self._userdb.getpwnam(credentials.username)
+ root = FilePath(pwent.pw_dir).child('.ssh')
+ files = ['authorized_keys', 'authorized_keys2']
+ return [root.child(f) for f in files]
+
+
+ def checkKey(self, credentials):
+ """
+ Retrieve files containing authorized keys and check against user
+ credentials.
+ """
+ uid, gid = os.geteuid(), os.getegid()
+ ouid, ogid = self._userdb.getpwnam(credentials.username)[2:4]
+ for filepath in self.getAuthorizedKeysFiles(credentials):
+ if not filepath.exists():
+ continue
+ try:
+ lines = filepath.open()
+ except IOError, e:
+ if e.errno == errno.EACCES:
+ lines = runAsEffectiveUser(ouid, ogid, filepath.open)
+ else:
+ raise
+ for l in lines:
+ l2 = l.split()
+ if len(l2) < 2:
+ continue
+ try:
+ if base64.decodestring(l2[1]) == credentials.blob:
+ return True
+ except binascii.Error:
+ continue
+ return False
+
+ def _ebRequestAvatarId(self, f):
+ if not f.check(UnauthorizedLogin):
+ log.msg(f)
+ return failure.Failure(UnauthorizedLogin("unable to get avatar id"))
+ return f
+
+
+class SSHProtocolChecker:
+ """
+ SSHProtocolChecker is a checker that requires multiple authentications
+ to succeed. To add a checker, call my registerChecker method with
+ the checker and the interface.
+
+ After each successful authenticate, I call my areDone method with the
+ avatar id. To get a list of the successful credentials for an avatar id,
+ use C{SSHProcotolChecker.successfulCredentials[avatarId]}. If L{areDone}
+ returns True, the authentication has succeeded.
+ """
+
+ implements(ICredentialsChecker)
+
+ def __init__(self):
+ self.checkers = {}
+ self.successfulCredentials = {}
+
+ def get_credentialInterfaces(self):
+ return self.checkers.keys()
+
+ credentialInterfaces = property(get_credentialInterfaces)
+
+ def registerChecker(self, checker, *credentialInterfaces):
+ if not credentialInterfaces:
+ credentialInterfaces = checker.credentialInterfaces
+ for credentialInterface in credentialInterfaces:
+ self.checkers[credentialInterface] = checker
+
+ def requestAvatarId(self, credentials):
+ """
+ Part of the L{ICredentialsChecker} interface. Called by a portal with
+ some credentials to check if they'll authenticate a user. We check the
+ interfaces that the credentials provide against our list of acceptable
+ checkers. If one of them matches, we ask that checker to verify the
+ credentials. If they're valid, we call our L{_cbGoodAuthentication}
+ method to continue.
+
+ @param credentials: the credentials the L{Portal} wants us to verify
+ """
+ ifac = providedBy(credentials)
+ for i in ifac:
+ c = self.checkers.get(i)
+ if c is not None:
+ d = defer.maybeDeferred(c.requestAvatarId, credentials)
+ return d.addCallback(self._cbGoodAuthentication,
+ credentials)
+ return defer.fail(UnhandledCredentials("No checker for %s" % \
+ ', '.join(map(reflect.qual, ifac))))
+
+ def _cbGoodAuthentication(self, avatarId, credentials):
+ """
+ Called if a checker has verified the credentials. We call our
+ L{areDone} method to see if the whole of the successful authentications
+ are enough. If they are, we return the avatar ID returned by the first
+ checker.
+ """
+ if avatarId not in self.successfulCredentials:
+ self.successfulCredentials[avatarId] = []
+ self.successfulCredentials[avatarId].append(credentials)
+ if self.areDone(avatarId):
+ del self.successfulCredentials[avatarId]
+ return avatarId
+ else:
+ raise error.NotEnoughAuthentication()
+
+ def areDone(self, avatarId):
+ """
+ Override to determine if the authentication is finished for a given
+ avatarId.
+
+ @param avatarId: the avatar returned by the first checker. For
+ this checker to function correctly, all the checkers must
+ return the same avatar ID.
+ """
+ return True
+
diff --git a/twisted/conch/client/__init__.py b/twisted/conch/client/__init__.py
new file mode 100644
index 0000000..f55d474
--- /dev/null
+++ b/twisted/conch/client/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""
+Client support code for Conch.
+
+Maintainer: Paul Swartz
+"""
diff --git a/twisted/conch/client/agent.py b/twisted/conch/client/agent.py
new file mode 100644
index 0000000..50a8fea
--- /dev/null
+++ b/twisted/conch/client/agent.py
@@ -0,0 +1,73 @@
+# -*- test-case-name: twisted.conch.test.test_default -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Accesses the key agent for user authentication.
+
+Maintainer: Paul Swartz
+"""
+
+import os
+
+from twisted.conch.ssh import agent, channel, keys
+from twisted.internet import protocol, reactor
+from twisted.python import log
+
+
+
+class SSHAgentClient(agent.SSHAgentClient):
+
+ def __init__(self):
+ agent.SSHAgentClient.__init__(self)
+ self.blobs = []
+
+
+ def getPublicKeys(self):
+ return self.requestIdentities().addCallback(self._cbPublicKeys)
+
+
+ def _cbPublicKeys(self, blobcomm):
+ log.msg('got %i public keys' % len(blobcomm))
+ self.blobs = [x[0] for x in blobcomm]
+
+
+ def getPublicKey(self):
+ """
+ Return a L{Key} from the first blob in C{self.blobs}, if any, or
+ return C{None}.
+ """
+ if self.blobs:
+ return keys.Key.fromString(self.blobs.pop(0))
+ return None
+
+
+
+class SSHAgentForwardingChannel(channel.SSHChannel):
+
+ def channelOpen(self, specificData):
+ cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal)
+ d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
+ d.addCallback(self._cbGotLocal)
+ d.addErrback(lambda x:self.loseConnection())
+ self.buf = ''
+
+
+ def _cbGotLocal(self, local):
+ self.local = local
+ self.dataReceived = self.local.transport.write
+ self.local.dataReceived = self.write
+
+
+ def dataReceived(self, data):
+ self.buf += data
+
+
+ def closed(self):
+ if self.local:
+ self.local.loseConnection()
+ self.local = None
+
+
+class SSHAgentForwardingLocal(protocol.Protocol):
+ pass
diff --git a/twisted/conch/client/connect.py b/twisted/conch/client/connect.py
new file mode 100644
index 0000000..dc5fe22
--- /dev/null
+++ b/twisted/conch/client/connect.py
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+import direct
+
+connectTypes = {"direct" : direct.connect}
+
+def connect(host, port, options, verifyHostKey, userAuthObject):
+ useConnects = ['direct']
+ return _ebConnect(None, useConnects, host, port, options, verifyHostKey,
+ userAuthObject)
+
+def _ebConnect(f, useConnects, host, port, options, vhk, uao):
+ if not useConnects:
+ return f
+ connectType = useConnects.pop(0)
+ f = connectTypes[connectType]
+ d = f(host, port, options, vhk, uao)
+ d.addErrback(_ebConnect, useConnects, host, port, options, vhk, uao)
+ return d
diff --git a/twisted/conch/client/default.py b/twisted/conch/client/default.py
new file mode 100644
index 0000000..50fe97a
--- /dev/null
+++ b/twisted/conch/client/default.py
@@ -0,0 +1,256 @@
+# -*- test-case-name: twisted.conch.test.test_knownhosts,twisted.conch.test.test_default -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Various classes and functions for implementing user-interaction in the
+command-line conch client.
+
+You probably shouldn't use anything in this module directly, since it assumes
+you are sitting at an interactive terminal. For example, to programmatically
+interact with a known_hosts database, use L{twisted.conch.client.knownhosts}.
+"""
+
+from twisted.python import log
+from twisted.python.filepath import FilePath
+
+from twisted.conch.error import ConchError
+from twisted.conch.ssh import common, keys, userauth
+from twisted.internet import defer, protocol, reactor
+
+from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI
+
+from twisted.conch.client import agent
+
+import os, sys, base64, getpass
+
+# This name is bound so that the unit tests can use 'patch' to override it.
+_open = open
+
+def verifyHostKey(transport, host, pubKey, fingerprint):
+ """
+ Verify a host's key.
+
+ This function is a gross vestige of some bad factoring in the client
+ internals. The actual implementation, and a better signature of this logic
+ is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet
+ because the callers have not yet been rehabilitated, but they should
+ eventually be changed to call that method instead.
+
+ However, this function does perform two functions not implemented by
+ L{KnownHostsFile.verifyHostKey}. It determines the path to the user's
+ known_hosts file based on the options (which should really be the options
+ object's job), and it provides an opener to L{ConsoleUI} which opens
+ '/dev/tty' so that the user will be prompted on the tty of the process even
+ if the input and output of the process has been redirected. This latter
+ part is, somewhat obviously, not portable, but I don't know of a portable
+ equivalent that could be used.
+
+ @param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is
+ always the dotted-quad IP address of the host being connected to.
+ @type host: L{str}
+
+ @param transport: the client transport which is attempting to connect to
+ the given host.
+ @type transport: L{SSHClientTransport}
+
+ @param fingerprint: the fingerprint of the given public key, in
+ xx:xx:xx:... format. This is ignored in favor of getting the fingerprint
+ from the key itself.
+ @type fingerprint: L{str}
+
+ @param pubKey: The public key of the server being connected to.
+ @type pubKey: L{str}
+
+ @return: a L{Deferred} which fires with C{1} if the key was successfully
+ verified, or fails if the key could not be successfully verified. Failure
+ types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or
+ L{KeyboardInterrupt}.
+ """
+ actualHost = transport.factory.options['host']
+ actualKey = keys.Key.fromString(pubKey)
+ kh = KnownHostsFile.fromPath(FilePath(
+ transport.factory.options['known-hosts']
+ or os.path.expanduser("~/.ssh/known_hosts")
+ ))
+ ui = ConsoleUI(lambda : _open("/dev/tty", "r+b"))
+ return kh.verifyHostKey(ui, actualHost, host, actualKey)
+
+
+
+def isInKnownHosts(host, pubKey, options):
+ """checks to see if host is in the known_hosts file for the user.
+ returns 0 if it isn't, 1 if it is and is the same, 2 if it's changed.
+ """
+ keyType = common.getNS(pubKey)[0]
+ retVal = 0
+
+ if not options['known-hosts'] and not os.path.exists(os.path.expanduser('~/.ssh/')):
+ print 'Creating ~/.ssh directory...'
+ os.mkdir(os.path.expanduser('~/.ssh'))
+ kh_file = options['known-hosts'] or '~/.ssh/known_hosts'
+ try:
+ known_hosts = open(os.path.expanduser(kh_file))
+ except IOError:
+ return 0
+ for line in known_hosts.xreadlines():
+ split = line.split()
+ if len(split) < 3:
+ continue
+ hosts, hostKeyType, encodedKey = split[:3]
+ if host not in hosts.split(','): # incorrect host
+ continue
+ if hostKeyType != keyType: # incorrect type of key
+ continue
+ try:
+ decodedKey = base64.decodestring(encodedKey)
+ except:
+ continue
+ if decodedKey == pubKey:
+ return 1
+ else:
+ retVal = 2
+ return retVal
+
+
+
+class SSHUserAuthClient(userauth.SSHUserAuthClient):
+
+ def __init__(self, user, options, *args):
+ userauth.SSHUserAuthClient.__init__(self, user, *args)
+ self.keyAgent = None
+ self.options = options
+ self.usedFiles = []
+ if not options.identitys:
+ options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
+
+ def serviceStarted(self):
+ if 'SSH_AUTH_SOCK' in os.environ and not self.options['noagent']:
+ log.msg('using agent')
+ cc = protocol.ClientCreator(reactor, agent.SSHAgentClient)
+ d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
+ d.addCallback(self._setAgent)
+ d.addErrback(self._ebSetAgent)
+ else:
+ userauth.SSHUserAuthClient.serviceStarted(self)
+
+ def serviceStopped(self):
+ if self.keyAgent:
+ self.keyAgent.transport.loseConnection()
+ self.keyAgent = None
+
+ def _setAgent(self, a):
+ self.keyAgent = a
+ d = self.keyAgent.getPublicKeys()
+ d.addBoth(self._ebSetAgent)
+ return d
+
+ def _ebSetAgent(self, f):
+ userauth.SSHUserAuthClient.serviceStarted(self)
+
+ def _getPassword(self, prompt):
+ try:
+ oldout, oldin = sys.stdout, sys.stdin
+ sys.stdin = sys.stdout = open('/dev/tty','r+')
+ p=getpass.getpass(prompt)
+ sys.stdout,sys.stdin=oldout,oldin
+ return p
+ except (KeyboardInterrupt, IOError):
+ print
+ raise ConchError('PEBKAC')
+
+ def getPassword(self, prompt = None):
+ if not prompt:
+ prompt = "%s@%s's password: " % (self.user, self.transport.transport.getPeer().host)
+ try:
+ p = self._getPassword(prompt)
+ return defer.succeed(p)
+ except ConchError:
+ return defer.fail()
+
+
+ def getPublicKey(self):
+ """
+ Get a public key from the key agent if possible, otherwise look in
+ the next configured identity file for one.
+ """
+ if self.keyAgent:
+ key = self.keyAgent.getPublicKey()
+ if key is not None:
+ return key
+ files = [x for x in self.options.identitys if x not in self.usedFiles]
+ log.msg(str(self.options.identitys))
+ log.msg(str(files))
+ if not files:
+ return None
+ file = files[0]
+ log.msg(file)
+ self.usedFiles.append(file)
+ file = os.path.expanduser(file)
+ file += '.pub'
+ if not os.path.exists(file):
+ return self.getPublicKey() # try again
+ try:
+ return keys.Key.fromFile(file)
+ except keys.BadKeyError:
+ return self.getPublicKey() # try again
+
+
+ def signData(self, publicKey, signData):
+ """
+ Extend the base signing behavior by using an SSH agent to sign the
+ data, if one is available.
+
+ @type publicKey: L{Key}
+ @type signData: C{str}
+ """
+ if not self.usedFiles: # agent key
+ return self.keyAgent.signData(publicKey.blob(), signData)
+ else:
+ return userauth.SSHUserAuthClient.signData(self, publicKey, signData)
+
+
+ def getPrivateKey(self):
+ """
+ Try to load the private key from the last used file identified by
+ C{getPublicKey}, potentially asking for the passphrase if the key is
+ encrypted.
+ """
+ file = os.path.expanduser(self.usedFiles[-1])
+ if not os.path.exists(file):
+ return None
+ try:
+ return defer.succeed(keys.Key.fromFile(file))
+ except keys.EncryptedKeyError:
+ for i in range(3):
+ prompt = "Enter passphrase for key '%s': " % \
+ self.usedFiles[-1]
+ try:
+ p = self._getPassword(prompt)
+ return defer.succeed(keys.Key.fromFile(file, passphrase=p))
+ except (keys.BadKeyError, ConchError):
+ pass
+ return defer.fail(ConchError('bad password'))
+ raise
+ except KeyboardInterrupt:
+ print
+ reactor.stop()
+
+
+ def getGenericAnswers(self, name, instruction, prompts):
+ responses = []
+ try:
+ oldout, oldin = sys.stdout, sys.stdin
+ sys.stdin = sys.stdout = open('/dev/tty','r+')
+ if name:
+ print name
+ if instruction:
+ print instruction
+ for prompt, echo in prompts:
+ if echo:
+ responses.append(raw_input(prompt))
+ else:
+ responses.append(getpass.getpass(prompt))
+ finally:
+ sys.stdout,sys.stdin=oldout,oldin
+ return defer.succeed(responses)
diff --git a/twisted/conch/client/direct.py b/twisted/conch/client/direct.py
new file mode 100644
index 0000000..f95a14a
--- /dev/null
+++ b/twisted/conch/client/direct.py
@@ -0,0 +1,107 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import defer, protocol, reactor
+from twisted.conch import error
+from twisted.conch.ssh import transport
+from twisted.python import log
+
+
+
+class SSHClientFactory(protocol.ClientFactory):
+
+ def __init__(self, d, options, verifyHostKey, userAuthObject):
+ self.d = d
+ self.options = options
+ self.verifyHostKey = verifyHostKey
+ self.userAuthObject = userAuthObject
+
+
+ def clientConnectionLost(self, connector, reason):
+ if self.options['reconnect']:
+ connector.connect()
+
+
+ def clientConnectionFailed(self, connector, reason):
+ if self.d is None:
+ return
+ d, self.d = self.d, None
+ d.errback(reason)
+
+
+ def buildProtocol(self, addr):
+ trans = SSHClientTransport(self)
+ if self.options['ciphers']:
+ trans.supportedCiphers = self.options['ciphers']
+ if self.options['macs']:
+ trans.supportedMACs = self.options['macs']
+ if self.options['compress']:
+ trans.supportedCompressions[0:1] = ['zlib']
+ if self.options['host-key-algorithms']:
+ trans.supportedPublicKeys = self.options['host-key-algorithms']
+ return trans
+
+
+
+class SSHClientTransport(transport.SSHClientTransport):
+
+ def __init__(self, factory):
+ self.factory = factory
+ self.unixServer = None
+
+
+ def connectionLost(self, reason):
+ if self.unixServer:
+ d = self.unixServer.stopListening()
+ self.unixServer = None
+ else:
+ d = defer.succeed(None)
+ d.addCallback(lambda x:
+ transport.SSHClientTransport.connectionLost(self, reason))
+
+
+ def receiveError(self, code, desc):
+ if self.factory.d is None:
+ return
+ d, self.factory.d = self.factory.d, None
+ d.errback(error.ConchError(desc, code))
+
+
+ def sendDisconnect(self, code, reason):
+ if self.factory.d is None:
+ return
+ d, self.factory.d = self.factory.d, None
+ transport.SSHClientTransport.sendDisconnect(self, code, reason)
+ d.errback(error.ConchError(reason, code))
+
+
+ def receiveDebug(self, alwaysDisplay, message, lang):
+ log.msg('Received Debug Message: %s' % message)
+ if alwaysDisplay: # XXX what should happen here?
+ print message
+
+
+ def verifyHostKey(self, pubKey, fingerprint):
+ return self.factory.verifyHostKey(self, self.transport.getPeer().host, pubKey,
+ fingerprint)
+
+
+ def setService(self, service):
+ log.msg('setting client server to %s' % service)
+ transport.SSHClientTransport.setService(self, service)
+ if service.name != 'ssh-userauth' and self.factory.d is not None:
+ d, self.factory.d = self.factory.d, None
+ d.callback(None)
+
+
+ def connectionSecure(self):
+ self.requestService(self.factory.userAuthObject)
+
+
+
+def connect(host, port, options, verifyHostKey, userAuthObject):
+ d = defer.Deferred()
+ factory = SSHClientFactory(d, options, verifyHostKey, userAuthObject)
+ reactor.connectTCP(host, port, factory)
+ return d
diff --git a/twisted/conch/client/knownhosts.py b/twisted/conch/client/knownhosts.py
new file mode 100644
index 0000000..48cd89b
--- /dev/null
+++ b/twisted/conch/client/knownhosts.py
@@ -0,0 +1,478 @@
+# -*- test-case-name: twisted.conch.test.test_knownhosts -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An implementation of the OpenSSH known_hosts database.
+
+@since: 8.2
+"""
+
+from binascii import Error as DecodeError, b2a_base64
+import hmac
+import sys
+
+from zope.interface import implements
+
+from twisted.python.randbytes import secureRandom
+if sys.version_info >= (2, 5):
+ from twisted.python.hashlib import sha1
+else:
+ # We need to have an object with a method named 'new'.
+ import sha as sha1
+
+from twisted.internet import defer
+
+from twisted.python import log
+from twisted.conch.interfaces import IKnownHostEntry
+from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry
+from twisted.conch.ssh.keys import Key, BadKeyError
+
+
+def _b64encode(s):
+ """
+ Encode a binary string as base64 with no trailing newline.
+ """
+ return b2a_base64(s).strip()
+
+
+
+def _extractCommon(string):
+ """
+ Extract common elements of base64 keys from an entry in a hosts file.
+
+ @return: a 4-tuple of hostname data (L{str}), ssh key type (L{str}), key
+ (L{Key}), and comment (L{str} or L{None}). The hostname data is simply the
+ beginning of the line up to the first occurrence of whitespace.
+ """
+ elements = string.split(None, 2)
+ if len(elements) != 3:
+ raise InvalidEntry()
+ hostnames, keyType, keyAndComment = elements
+ splitkey = keyAndComment.split(None, 1)
+ if len(splitkey) == 2:
+ keyString, comment = splitkey
+ comment = comment.rstrip("\n")
+ else:
+ keyString = splitkey[0]
+ comment = None
+ key = Key.fromString(keyString.decode('base64'))
+ return hostnames, keyType, key, comment
+
+
+
+class _BaseEntry(object):
+ """
+ Abstract base of both hashed and non-hashed entry objects, since they
+ represent keys and key types the same way.
+
+ @ivar keyType: The type of the key; either ssh-dss or ssh-rsa.
+ @type keyType: L{str}
+
+ @ivar publicKey: The server public key indicated by this line.
+ @type publicKey: L{twisted.conch.ssh.keys.Key}
+
+ @ivar comment: Trailing garbage after the key line.
+ @type comment: L{str}
+ """
+
+ def __init__(self, keyType, publicKey, comment):
+ self.keyType = keyType
+ self.publicKey = publicKey
+ self.comment = comment
+
+
+ def matchesKey(self, keyObject):
+ """
+ Check to see if this entry matches a given key object.
+
+ @type keyObject: L{Key}
+
+ @rtype: bool
+ """
+ return self.publicKey == keyObject
+
+
+
+class PlainEntry(_BaseEntry):
+ """
+ A L{PlainEntry} is a representation of a plain-text entry in a known_hosts
+ file.
+
+ @ivar _hostnames: the list of all host-names associated with this entry.
+ @type _hostnames: L{list} of L{str}
+ """
+
+ implements(IKnownHostEntry)
+
+ def __init__(self, hostnames, keyType, publicKey, comment):
+ self._hostnames = hostnames
+ super(PlainEntry, self).__init__(keyType, publicKey, comment)
+
+
+ def fromString(cls, string):
+ """
+ Parse a plain-text entry in a known_hosts file, and return a
+ corresponding L{PlainEntry}.
+
+ @param string: a space-separated string formatted like "hostname
+ key-type base64-key-data comment".
+
+ @type string: L{str}
+
+ @raise DecodeError: if the key is not valid encoded as valid base64.
+
+ @raise InvalidEntry: if the entry does not have the right number of
+ elements and is therefore invalid.
+
+ @raise BadKeyError: if the key, once decoded from base64, is not
+ actually an SSH key.
+
+ @return: an IKnownHostEntry representing the hostname and key in the
+ input line.
+
+ @rtype: L{PlainEntry}
+ """
+ hostnames, keyType, key, comment = _extractCommon(string)
+ self = cls(hostnames.split(","), keyType, key, comment)
+ return self
+
+ fromString = classmethod(fromString)
+
+
+ def matchesHost(self, hostname):
+ """
+ Check to see if this entry matches a given hostname.
+
+ @type hostname: L{str}
+
+ @rtype: bool
+ """
+ return hostname in self._hostnames
+
+
+ def toString(self):
+ """
+ Implement L{IKnownHostEntry.toString} by recording the comma-separated
+ hostnames, key type, and base-64 encoded key.
+ """
+ fields = [','.join(self._hostnames),
+ self.keyType,
+ _b64encode(self.publicKey.blob())]
+ if self.comment is not None:
+ fields.append(self.comment)
+ return ' '.join(fields)
+
+
+class UnparsedEntry(object):
+ """
+ L{UnparsedEntry} is an entry in a L{KnownHostsFile} which can't actually be
+ parsed; therefore it matches no keys and no hosts.
+ """
+
+ implements(IKnownHostEntry)
+
+ def __init__(self, string):
+ """
+ Create an unparsed entry from a line in a known_hosts file which cannot
+ otherwise be parsed.
+ """
+ self._string = string
+
+
+ def matchesHost(self, hostname):
+ """
+ Always returns False.
+ """
+ return False
+
+
+ def matchesKey(self, key):
+ """
+ Always returns False.
+ """
+ return False
+
+
+ def toString(self):
+ """
+ Returns the input line, without its newline if one was given.
+ """
+ return self._string.rstrip("\n")
+
+
+
+def _hmacedString(key, string):
+ """
+ Return the SHA-1 HMAC hash of the given key and string.
+ """
+ hash = hmac.HMAC(key, digestmod=sha1)
+ hash.update(string)
+ return hash.digest()
+
+
+
+class HashedEntry(_BaseEntry):
+ """
+ A L{HashedEntry} is a representation of an entry in a known_hosts file
+ where the hostname has been hashed and salted.
+
+ @ivar _hostSalt: the salt to combine with a hostname for hashing.
+
+ @ivar _hostHash: the hashed representation of the hostname.
+
+ @cvar MAGIC: the 'hash magic' string used to identify a hashed line in a
+ known_hosts file as opposed to a plaintext one.
+ """
+
+ implements(IKnownHostEntry)
+
+ MAGIC = '|1|'
+
+ def __init__(self, hostSalt, hostHash, keyType, publicKey, comment):
+ self._hostSalt = hostSalt
+ self._hostHash = hostHash
+ super(HashedEntry, self).__init__(keyType, publicKey, comment)
+
+
+ def fromString(cls, string):
+ """
+ Load a hashed entry from a string representing a line in a known_hosts
+ file.
+
+ @raise DecodeError: if the key, the hostname, or the is not valid
+ encoded as valid base64
+
+ @raise InvalidEntry: if the entry does not have the right number of
+ elements and is therefore invalid, or the host/hash portion contains
+ more items than just the host and hash.
+
+ @raise BadKeyError: if the key, once decoded from base64, is not
+ actually an SSH key.
+ """
+ stuff, keyType, key, comment = _extractCommon(string)
+ saltAndHash = stuff[len(cls.MAGIC):].split("|")
+ if len(saltAndHash) != 2:
+ raise InvalidEntry()
+ hostSalt, hostHash = saltAndHash
+ self = cls(hostSalt.decode("base64"), hostHash.decode("base64"),
+ keyType, key, comment)
+ return self
+
+ fromString = classmethod(fromString)
+
+
+ def matchesHost(self, hostname):
+ """
+ Implement L{IKnownHostEntry.matchesHost} to compare the hash of the
+ input to the stored hash.
+ """
+ return (_hmacedString(self._hostSalt, hostname) == self._hostHash)
+
+
+ def toString(self):
+ """
+ Implement L{IKnownHostEntry.toString} by base64-encoding the salt, host
+ hash, and key.
+ """
+ fields = [self.MAGIC + '|'.join([_b64encode(self._hostSalt),
+ _b64encode(self._hostHash)]),
+ self.keyType,
+ _b64encode(self.publicKey.blob())]
+ if self.comment is not None:
+ fields.append(self.comment)
+ return ' '.join(fields)
+
+
+
+class KnownHostsFile(object):
+ """
+ A structured representation of an OpenSSH-format ~/.ssh/known_hosts file.
+
+ @ivar _entries: a list of L{IKnownHostEntry} providers.
+
+ @ivar _savePath: the L{FilePath} to save new entries to.
+ """
+
+ def __init__(self, savePath):
+ """
+ Create a new, empty KnownHostsFile.
+
+ You want to use L{KnownHostsFile.fromPath} to parse one of these.
+ """
+ self._entries = []
+ self._savePath = savePath
+
+
+ def hasHostKey(self, hostname, key):
+ """
+ @return: True if the given hostname and key are present in this file,
+ False if they are not.
+
+ @rtype: L{bool}
+
+ @raise HostKeyChanged: if the host key found for the given hostname
+ does not match the given key.
+ """
+ for lineidx, entry in enumerate(self._entries):
+ if entry.matchesHost(hostname):
+ if entry.matchesKey(key):
+ return True
+ else:
+ raise HostKeyChanged(entry, self._savePath, lineidx + 1)
+ return False
+
+
+ def verifyHostKey(self, ui, hostname, ip, key):
+ """
+ Verify the given host key for the given IP and host, asking for
+ confirmation from, and notifying, the given UI about changes to this
+ file.
+
+ @param ui: The user interface to request an IP address from.
+
+ @param hostname: The hostname that the user requested to connect to.
+
+ @param ip: The string representation of the IP address that is actually
+ being connected to.
+
+ @param key: The public key of the server.
+
+ @return: a L{Deferred} that fires with True when the key has been
+ verified, or fires with an errback when the key either cannot be
+ verified or has changed.
+
+ @rtype: L{Deferred}
+ """
+ hhk = defer.maybeDeferred(self.hasHostKey, hostname, key)
+ def gotHasKey(result):
+ if result:
+ if not self.hasHostKey(ip, key):
+ ui.warn("Warning: Permanently added the %s host key for "
+ "IP address '%s' to the list of known hosts." %
+ (key.type(), ip))
+ self.addHostKey(ip, key)
+ self.save()
+ return result
+ else:
+ def promptResponse(response):
+ if response:
+ self.addHostKey(hostname, key)
+ self.addHostKey(ip, key)
+ self.save()
+ return response
+ else:
+ raise UserRejectedKey()
+ return ui.prompt(
+ "The authenticity of host '%s (%s)' "
+ "can't be established.\n"
+ "RSA key fingerprint is %s.\n"
+ "Are you sure you want to continue connecting (yes/no)? " %
+ (hostname, ip, key.fingerprint())).addCallback(promptResponse)
+ return hhk.addCallback(gotHasKey)
+
+
+ def addHostKey(self, hostname, key):
+ """
+ Add a new L{HashedEntry} to the key database.
+
+ Note that you still need to call L{KnownHostsFile.save} if you wish
+ these changes to be persisted.
+
+ @return: the L{HashedEntry} that was added.
+ """
+ salt = secureRandom(20)
+ keyType = "ssh-" + key.type().lower()
+ entry = HashedEntry(salt, _hmacedString(salt, hostname),
+ keyType, key, None)
+ self._entries.append(entry)
+ return entry
+
+
+ def save(self):
+ """
+ Save this L{KnownHostsFile} to the path it was loaded from.
+ """
+ p = self._savePath.parent()
+ if not p.isdir():
+ p.makedirs()
+ self._savePath.setContent('\n'.join(
+ [entry.toString() for entry in self._entries]) + "\n")
+
+
+ def fromPath(cls, path):
+ """
+ @param path: A path object to use for both reading contents from and
+ later saving to.
+
+ @type path: L{FilePath}
+ """
+ self = cls(path)
+ try:
+ fp = path.open()
+ except IOError:
+ return self
+ for line in fp:
+ try:
+ if line.startswith(HashedEntry.MAGIC):
+ entry = HashedEntry.fromString(line)
+ else:
+ entry = PlainEntry.fromString(line)
+ except (DecodeError, InvalidEntry, BadKeyError):
+ entry = UnparsedEntry(line)
+ self._entries.append(entry)
+ return self
+
+ fromPath = classmethod(fromPath)
+
+
+class ConsoleUI(object):
+ """
+ A UI object that can ask true/false questions and post notifications on the
+ console, to be used during key verification.
+
+ @ivar opener: a no-argument callable which should open a console file-like
+ object to be used for reading and writing.
+ """
+
+ def __init__(self, opener):
+ self.opener = opener
+
+
+ def prompt(self, text):
+ """
+ Write the given text as a prompt to the console output, then read a
+ result from the console input.
+
+ @return: a L{Deferred} which fires with L{True} when the user answers
+ 'yes' and L{False} when the user answers 'no'. It may errback if there
+ were any I/O errors.
+ """
+ d = defer.succeed(None)
+ def body(ignored):
+ f = self.opener()
+ f.write(text)
+ while True:
+ answer = f.readline().strip().lower()
+ if answer == 'yes':
+ f.close()
+ return True
+ elif answer == 'no':
+ f.close()
+ return False
+ else:
+ f.write("Please type 'yes' or 'no': ")
+ return d.addCallback(body)
+
+
+ def warn(self, text):
+ """
+ Notify the user (non-interactively) of the provided text, by writing it
+ to the console.
+ """
+ try:
+ f = self.opener()
+ f.write(text)
+ f.close()
+ except:
+ log.err()
diff --git a/twisted/conch/client/options.py b/twisted/conch/client/options.py
new file mode 100644
index 0000000..8550573
--- /dev/null
+++ b/twisted/conch/client/options.py
@@ -0,0 +1,96 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.conch.ssh.transport import SSHClientTransport, SSHCiphers
+from twisted.python import usage
+
+import sys
+
+class ConchOptions(usage.Options):
+
+ optParameters = [['user', 'l', None, 'Log in using this user name.'],
+ ['identity', 'i', None],
+ ['ciphers', 'c', None],
+ ['macs', 'm', None],
+ ['port', 'p', None, 'Connect to this port. Server must be on the same port.'],
+ ['option', 'o', None, 'Ignored OpenSSH options'],
+ ['host-key-algorithms', '', None],
+ ['known-hosts', '', None, 'File to check for host keys'],
+ ['user-authentications', '', None, 'Types of user authentications to use.'],
+ ['logfile', '', None, 'File to log to, or - for stdout'],
+ ]
+
+ optFlags = [['version', 'V', 'Display version number only.'],
+ ['compress', 'C', 'Enable compression.'],
+ ['log', 'v', 'Enable logging (defaults to stderr)'],
+ ['nox11', 'x', 'Disable X11 connection forwarding (default)'],
+ ['agent', 'A', 'Enable authentication agent forwarding'],
+ ['noagent', 'a', 'Disable authentication agent forwarding (default)'],
+ ['reconnect', 'r', 'Reconnect to the server if the connection is lost.'],
+ ]
+
+ compData = usage.Completions(
+ mutuallyExclusive=[("agent", "noagent")],
+ optActions={
+ "user": usage.CompleteUsernames(),
+ "ciphers": usage.CompleteMultiList(
+ SSHCiphers.cipherMap.keys(),
+ descr='ciphers to choose from'),
+ "macs": usage.CompleteMultiList(
+ SSHCiphers.macMap.keys(),
+ descr='macs to choose from'),
+ "host-key-algorithms": usage.CompleteMultiList(
+ SSHClientTransport.supportedPublicKeys,
+ descr='host key algorithms to choose from'),
+ #"user-authentications": usage.CompleteMultiList(?
+ # descr='user authentication types' ),
+ },
+ extraActions=[usage.CompleteUserAtHost(),
+ usage.Completer(descr="command"),
+ usage.Completer(descr='argument',
+ repeat=True)]
+ )
+
+ def __init__(self, *args, **kw):
+ usage.Options.__init__(self, *args, **kw)
+ self.identitys = []
+ self.conns = None
+
+ def opt_identity(self, i):
+ """Identity for public-key authentication"""
+ self.identitys.append(i)
+
+ def opt_ciphers(self, ciphers):
+ "Select encryption algorithms"
+ ciphers = ciphers.split(',')
+ for cipher in ciphers:
+ if not SSHCiphers.cipherMap.has_key(cipher):
+ sys.exit("Unknown cipher type '%s'" % cipher)
+ self['ciphers'] = ciphers
+
+
+ def opt_macs(self, macs):
+ "Specify MAC algorithms"
+ macs = macs.split(',')
+ for mac in macs:
+ if not SSHCiphers.macMap.has_key(mac):
+ sys.exit("Unknown mac type '%s'" % mac)
+ self['macs'] = macs
+
+ def opt_host_key_algorithms(self, hkas):
+ "Select host key algorithms"
+ hkas = hkas.split(',')
+ for hka in hkas:
+ if hka not in SSHClientTransport.supportedPublicKeys:
+ sys.exit("Unknown host key type '%s'" % hka)
+ self['host-key-algorithms'] = hkas
+
+ def opt_user_authentications(self, uas):
+ "Choose how to authenticate to the remote server"
+ self['user-authentications'] = uas.split(',')
+
+# def opt_compress(self):
+# "Enable compression"
+# self.enableCompression = 1
+# SSHClientTransport.supportedCompressions[0:1] = ['zlib']
diff --git a/twisted/conch/error.py b/twisted/conch/error.py
new file mode 100644
index 0000000..a3bcc65
--- /dev/null
+++ b/twisted/conch/error.py
@@ -0,0 +1,102 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An error to represent bad things happening in Conch.
+
+Maintainer: Paul Swartz
+"""
+
+from twisted.cred.error import UnauthorizedLogin
+
+
+
+class ConchError(Exception):
+ def __init__(self, value, data = None):
+ Exception.__init__(self, value, data)
+ self.value = value
+ self.data = data
+
+
+
+class NotEnoughAuthentication(Exception):
+ """
+ This is thrown if the authentication is valid, but is not enough to
+ successfully verify the user. i.e. don't retry this type of
+ authentication, try another one.
+ """
+
+
+
+class ValidPublicKey(UnauthorizedLogin):
+ """
+ Raised by public key checkers when they receive public key credentials
+ that don't contain a signature at all, but are valid in every other way.
+ (e.g. the public key matches one in the user's authorized_keys file).
+
+ Protocol code (eg
+ L{SSHUserAuthServer<twisted.conch.ssh.userauth.SSHUserAuthServer>}) which
+ attempts to log in using
+ L{ISSHPrivateKey<twisted.cred.credentials.ISSHPrivateKey>} credentials
+ should be prepared to handle a failure of this type by telling the user to
+ re-authenticate using the same key and to include a signature with the new
+ attempt.
+
+ See U{http://www.ietf.org/rfc/rfc4252.txt} section 7 for more details.
+ """
+
+
+
+class IgnoreAuthentication(Exception):
+ """
+ This is thrown to let the UserAuthServer know it doesn't need to handle the
+ authentication anymore.
+ """
+
+
+
+class MissingKeyStoreError(Exception):
+ """
+ Raised if an SSHAgentServer starts receiving data without its factory
+ providing a keys dict on which to read/write key data.
+ """
+
+
+
+class UserRejectedKey(Exception):
+ """
+ The user interactively rejected a key.
+ """
+
+
+
+class InvalidEntry(Exception):
+ """
+ An entry in a known_hosts file could not be interpreted as a valid entry.
+ """
+
+
+
+class HostKeyChanged(Exception):
+ """
+ The host key of a remote host has changed.
+
+ @ivar offendingEntry: The entry which contains the persistent host key that
+ disagrees with the given host key.
+
+ @type offendingEntry: L{twisted.conch.interfaces.IKnownHostEntry}
+
+ @ivar path: a reference to the known_hosts file that the offending entry
+ was loaded from
+
+ @type path: L{twisted.python.filepath.FilePath}
+
+ @ivar lineno: The line number of the offending entry in the given path.
+
+ @type lineno: L{int}
+ """
+ def __init__(self, offendingEntry, path, lineno):
+ Exception.__init__(self)
+ self.offendingEntry = offendingEntry
+ self.path = path
+ self.lineno = lineno
diff --git a/twisted/conch/insults/__init__.py b/twisted/conch/insults/__init__.py
new file mode 100644
index 0000000..c070d4f
--- /dev/null
+++ b/twisted/conch/insults/__init__.py
@@ -0,0 +1,16 @@
+"""
+Insults: a replacement for Curses/S-Lang.
+
+Very basic at the moment."""
+
+from twisted.python import deprecate, versions
+
+deprecate.deprecatedModuleAttribute(
+ versions.Version("Twisted", 10, 1, 0),
+ "Please use twisted.conch.insults.helper instead.",
+ __name__, "colors")
+
+deprecate.deprecatedModuleAttribute(
+ versions.Version("Twisted", 10, 1, 0),
+ "Please use twisted.conch.insults.insults instead.",
+ __name__, "client")
diff --git a/twisted/conch/insults/client.py b/twisted/conch/insults/client.py
new file mode 100644
index 0000000..89c79cd
--- /dev/null
+++ b/twisted/conch/insults/client.py
@@ -0,0 +1,138 @@
+"""
+You don't really want to use this module. Try insults.py instead.
+"""
+
+from twisted.internet import protocol
+
+class InsultsClient(protocol.Protocol):
+
+ escapeTimeout = 0.2
+
+ def __init__(self):
+ self.width = self.height = None
+ self.xpos = self.ypos = 0
+ self.commandQueue = []
+ self.inEscape = ''
+
+ def setSize(self, width, height):
+ call = 0
+ if self.width:
+ call = 1
+ self.width = width
+ self.height = height
+ if call:
+ self.windowSizeChanged()
+
+ def dataReceived(self, data):
+ from twisted.internet import reactor
+ for ch in data:
+ if ch == '\x1b':
+ if self.inEscape:
+ self.keyReceived(ch)
+ self.inEscape = ''
+ else:
+ self.inEscape = ch
+ self.escapeCall = reactor.callLater(self.escapeTimeout,
+ self.endEscape)
+ elif ch in 'ABCD' and self.inEscape:
+ self.inEscape = ''
+ self.escapeCall.cancel()
+ if ch == 'A':
+ self.keyReceived('<Up>')
+ elif ch == 'B':
+ self.keyReceived('<Down>')
+ elif ch == 'C':
+ self.keyReceived('<Right>')
+ elif ch == 'D':
+ self.keyReceived('<Left>')
+ elif self.inEscape:
+ self.inEscape += ch
+ else:
+ self.keyReceived(ch)
+
+ def endEscape(self):
+ ch = self.inEscape
+ self.inEscape = ''
+ self.keyReceived(ch)
+
+ def initScreen(self):
+ self.transport.write('\x1b=\x1b[?1h')
+
+ def gotoXY(self, x, y):
+ """Go to a position on the screen.
+ """
+ self.xpos = x
+ self.ypos = y
+ self.commandQueue.append(('gotoxy', x, y))
+
+ def writeCh(self, ch):
+ """Write a character to the screen. If we're at the end of the row,
+ ignore the write.
+ """
+ if self.xpos < self.width - 1:
+ self.commandQueue.append(('write', ch))
+ self.xpos += 1
+
+ def writeStr(self, s):
+ """Write a string to the screen. This does not wrap a the edge of the
+ screen, and stops at \\r and \\n.
+ """
+ s = s[:self.width-self.xpos]
+ if '\n' in s:
+ s=s[:s.find('\n')]
+ if '\r' in s:
+ s=s[:s.find('\r')]
+ self.commandQueue.append(('write', s))
+ self.xpos += len(s)
+
+ def eraseToLine(self):
+ """Erase from the current position to the end of the line.
+ """
+ self.commandQueue.append(('eraseeol',))
+
+ def eraseToScreen(self):
+ """Erase from the current position to the end of the screen.
+ """
+ self.commandQueue.append(('eraseeos',))
+
+ def clearScreen(self):
+ """Clear the screen, and return the cursor to 0, 0.
+ """
+ self.commandQueue = [('cls',)]
+ self.xpos = self.ypos = 0
+
+ def setAttributes(self, *attrs):
+ """Set the attributes for drawing on the screen.
+ """
+ self.commandQueue.append(('attributes', attrs))
+
+ def refresh(self):
+ """Redraw the screen.
+ """
+ redraw = ''
+ for command in self.commandQueue:
+ if command[0] == 'gotoxy':
+ redraw += '\x1b[%i;%iH' % (command[2]+1, command[1]+1)
+ elif command[0] == 'write':
+ redraw += command[1]
+ elif command[0] == 'eraseeol':
+ redraw += '\x1b[0K'
+ elif command[0] == 'eraseeos':
+ redraw += '\x1b[OJ'
+ elif command[0] == 'cls':
+ redraw += '\x1b[H\x1b[J'
+ elif command[0] == 'attributes':
+ redraw += '\x1b[%sm' % ';'.join(map(str, command[1]))
+ else:
+ print command
+ self.commandQueue = []
+ self.transport.write(redraw)
+
+ def windowSizeChanged(self):
+ """Called when the size of the window changes.
+ Might want to redraw the screen here, or something.
+ """
+
+ def keyReceived(self, key):
+ """Called when the user hits a key.
+ """
diff --git a/twisted/conch/insults/colors.py b/twisted/conch/insults/colors.py
new file mode 100644
index 0000000..c12ab16
--- /dev/null
+++ b/twisted/conch/insults/colors.py
@@ -0,0 +1,29 @@
+"""
+You don't really want to use this module. Try helper.py instead.
+"""
+
+CLEAR = 0
+BOLD = 1
+DIM = 2
+ITALIC = 3
+UNDERSCORE = 4
+BLINK_SLOW = 5
+BLINK_FAST = 6
+REVERSE = 7
+CONCEALED = 8
+FG_BLACK = 30
+FG_RED = 31
+FG_GREEN = 32
+FG_YELLOW = 33
+FG_BLUE = 34
+FG_MAGENTA = 35
+FG_CYAN = 36
+FG_WHITE = 37
+BG_BLACK = 40
+BG_RED = 41
+BG_GREEN = 42
+BG_YELLOW = 43
+BG_BLUE = 44
+BG_MAGENTA = 45
+BG_CYAN = 46
+BG_WHITE = 47
diff --git a/twisted/conch/insults/helper.py b/twisted/conch/insults/helper.py
new file mode 100644
index 0000000..ed645c4
--- /dev/null
+++ b/twisted/conch/insults/helper.py
@@ -0,0 +1,450 @@
+# -*- test-case-name: twisted.conch.test.test_helper -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Partial in-memory terminal emulator
+
+@author: Jp Calderone
+"""
+
+import re, string
+
+from zope.interface import implements
+
+from twisted.internet import defer, protocol, reactor
+from twisted.python import log
+
+from twisted.conch.insults import insults
+
+FOREGROUND = 30
+BACKGROUND = 40
+BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = range(9)
+
+class CharacterAttribute:
+ """Represents the attributes of a single character.
+
+ Character set, intensity, underlinedness, blinkitude, video
+ reversal, as well as foreground and background colors made up a
+ character's attributes.
+ """
+ def __init__(self, charset=insults.G0,
+ bold=False, underline=False,
+ blink=False, reverseVideo=False,
+ foreground=WHITE, background=BLACK,
+
+ _subtracting=False):
+ self.charset = charset
+ self.bold = bold
+ self.underline = underline
+ self.blink = blink
+ self.reverseVideo = reverseVideo
+ self.foreground = foreground
+ self.background = background
+
+ self._subtracting = _subtracting
+
+ def __eq__(self, other):
+ return vars(self) == vars(other)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def copy(self):
+ c = self.__class__()
+ c.__dict__.update(vars(self))
+ return c
+
+ def wantOne(self, **kw):
+ k, v = kw.popitem()
+ if getattr(self, k) != v:
+ attr = self.copy()
+ attr._subtracting = not v
+ setattr(attr, k, v)
+ return attr
+ else:
+ return self.copy()
+
+ def toVT102(self):
+ # Spit out a vt102 control sequence that will set up
+ # all the attributes set here. Except charset.
+ attrs = []
+ if self._subtracting:
+ attrs.append(0)
+ if self.bold:
+ attrs.append(insults.BOLD)
+ if self.underline:
+ attrs.append(insults.UNDERLINE)
+ if self.blink:
+ attrs.append(insults.BLINK)
+ if self.reverseVideo:
+ attrs.append(insults.REVERSE_VIDEO)
+ if self.foreground != WHITE:
+ attrs.append(FOREGROUND + self.foreground)
+ if self.background != BLACK:
+ attrs.append(BACKGROUND + self.background)
+ if attrs:
+ return '\x1b[' + ';'.join(map(str, attrs)) + 'm'
+ return ''
+
+# XXX - need to support scroll regions and scroll history
+class TerminalBuffer(protocol.Protocol):
+ """
+ An in-memory terminal emulator.
+ """
+ implements(insults.ITerminalTransport)
+
+ for keyID in ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW',
+ 'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
+ 'F10', 'F11', 'F12'):
+ exec '%s = object()' % (keyID,)
+
+ TAB = '\t'
+ BACKSPACE = '\x7f'
+
+ width = 80
+ height = 24
+
+ fill = ' '
+ void = object()
+
+ def getCharacter(self, x, y):
+ return self.lines[y][x]
+
+ def connectionMade(self):
+ self.reset()
+
+ def write(self, bytes):
+ """
+ Add the given printable bytes to the terminal.
+
+ Line feeds in C{bytes} will be replaced with carriage return / line
+ feed pairs.
+ """
+ for b in bytes.replace('\n', '\r\n'):
+ self.insertAtCursor(b)
+
+ def _currentCharacterAttributes(self):
+ return CharacterAttribute(self.activeCharset, **self.graphicRendition)
+
+ def insertAtCursor(self, b):
+ """
+ Add one byte to the terminal at the cursor and make consequent state
+ updates.
+
+ If b is a carriage return, move the cursor to the beginning of the
+ current row.
+
+ If b is a line feed, move the cursor to the next row or scroll down if
+ the cursor is already in the last row.
+
+ Otherwise, if b is printable, put it at the cursor position (inserting
+ or overwriting as dictated by the current mode) and move the cursor.
+ """
+ if b == '\r':
+ self.x = 0
+ elif b == '\n':
+ self._scrollDown()
+ elif b in string.printable:
+ if self.x >= self.width:
+ self.nextLine()
+ ch = (b, self._currentCharacterAttributes())
+ if self.modes.get(insults.modes.IRM):
+ self.lines[self.y][self.x:self.x] = [ch]
+ self.lines[self.y].pop()
+ else:
+ self.lines[self.y][self.x] = ch
+ self.x += 1
+
+ def _emptyLine(self, width):
+ return [(self.void, self._currentCharacterAttributes()) for i in xrange(width)]
+
+ def _scrollDown(self):
+ self.y += 1
+ if self.y >= self.height:
+ self.y -= 1
+ del self.lines[0]
+ self.lines.append(self._emptyLine(self.width))
+
+ def _scrollUp(self):
+ self.y -= 1
+ if self.y < 0:
+ self.y = 0
+ del self.lines[-1]
+ self.lines.insert(0, self._emptyLine(self.width))
+
+ def cursorUp(self, n=1):
+ self.y = max(0, self.y - n)
+
+ def cursorDown(self, n=1):
+ self.y = min(self.height - 1, self.y + n)
+
+ def cursorBackward(self, n=1):
+ self.x = max(0, self.x - n)
+
+ def cursorForward(self, n=1):
+ self.x = min(self.width, self.x + n)
+
+ def cursorPosition(self, column, line):
+ self.x = column
+ self.y = line
+
+ def cursorHome(self):
+ self.x = self.home.x
+ self.y = self.home.y
+
+ def index(self):
+ self._scrollDown()
+
+ def reverseIndex(self):
+ self._scrollUp()
+
+ def nextLine(self):
+ """
+ Update the cursor position attributes and scroll down if appropriate.
+ """
+ self.x = 0
+ self._scrollDown()
+
+ def saveCursor(self):
+ self._savedCursor = (self.x, self.y)
+
+ def restoreCursor(self):
+ self.x, self.y = self._savedCursor
+ del self._savedCursor
+
+ def setModes(self, modes):
+ for m in modes:
+ self.modes[m] = True
+
+ def resetModes(self, modes):
+ for m in modes:
+ try:
+ del self.modes[m]
+ except KeyError:
+ pass
+
+
+ def setPrivateModes(self, modes):
+ """
+ Enable the given modes.
+
+ Track which modes have been enabled so that the implementations of
+ other L{insults.ITerminalTransport} methods can be properly implemented
+ to respect these settings.
+
+ @see: L{resetPrivateModes}
+ @see: L{insults.ITerminalTransport.setPrivateModes}
+ """
+ for m in modes:
+ self.privateModes[m] = True
+
+
+ def resetPrivateModes(self, modes):
+ """
+ Disable the given modes.
+
+ @see: L{setPrivateModes}
+ @see: L{insults.ITerminalTransport.resetPrivateModes}
+ """
+ for m in modes:
+ try:
+ del self.privateModes[m]
+ except KeyError:
+ pass
+
+
+ def applicationKeypadMode(self):
+ self.keypadMode = 'app'
+
+ def numericKeypadMode(self):
+ self.keypadMode = 'num'
+
+ def selectCharacterSet(self, charSet, which):
+ self.charsets[which] = charSet
+
+ def shiftIn(self):
+ self.activeCharset = insults.G0
+
+ def shiftOut(self):
+ self.activeCharset = insults.G1
+
+ def singleShift2(self):
+ oldActiveCharset = self.activeCharset
+ self.activeCharset = insults.G2
+ f = self.insertAtCursor
+ def insertAtCursor(b):
+ f(b)
+ del self.insertAtCursor
+ self.activeCharset = oldActiveCharset
+ self.insertAtCursor = insertAtCursor
+
+ def singleShift3(self):
+ oldActiveCharset = self.activeCharset
+ self.activeCharset = insults.G3
+ f = self.insertAtCursor
+ def insertAtCursor(b):
+ f(b)
+ del self.insertAtCursor
+ self.activeCharset = oldActiveCharset
+ self.insertAtCursor = insertAtCursor
+
+ def selectGraphicRendition(self, *attributes):
+ for a in attributes:
+ if a == insults.NORMAL:
+ self.graphicRendition = {
+ 'bold': False,
+ 'underline': False,
+ 'blink': False,
+ 'reverseVideo': False,
+ 'foreground': WHITE,
+ 'background': BLACK}
+ elif a == insults.BOLD:
+ self.graphicRendition['bold'] = True
+ elif a == insults.UNDERLINE:
+ self.graphicRendition['underline'] = True
+ elif a == insults.BLINK:
+ self.graphicRendition['blink'] = True
+ elif a == insults.REVERSE_VIDEO:
+ self.graphicRendition['reverseVideo'] = True
+ else:
+ try:
+ v = int(a)
+ except ValueError:
+ log.msg("Unknown graphic rendition attribute: " + repr(a))
+ else:
+ if FOREGROUND <= v <= FOREGROUND + N_COLORS:
+ self.graphicRendition['foreground'] = v - FOREGROUND
+ elif BACKGROUND <= v <= BACKGROUND + N_COLORS:
+ self.graphicRendition['background'] = v - BACKGROUND
+ else:
+ log.msg("Unknown graphic rendition attribute: " + repr(a))
+
+ def eraseLine(self):
+ self.lines[self.y] = self._emptyLine(self.width)
+
+ def eraseToLineEnd(self):
+ width = self.width - self.x
+ self.lines[self.y][self.x:] = self._emptyLine(width)
+
+ def eraseToLineBeginning(self):
+ self.lines[self.y][:self.x + 1] = self._emptyLine(self.x + 1)
+
+ def eraseDisplay(self):
+ self.lines = [self._emptyLine(self.width) for i in xrange(self.height)]
+
+ def eraseToDisplayEnd(self):
+ self.eraseToLineEnd()
+ height = self.height - self.y - 1
+ self.lines[self.y + 1:] = [self._emptyLine(self.width) for i in range(height)]
+
+ def eraseToDisplayBeginning(self):
+ self.eraseToLineBeginning()
+ self.lines[:self.y] = [self._emptyLine(self.width) for i in range(self.y)]
+
+ def deleteCharacter(self, n=1):
+ del self.lines[self.y][self.x:self.x+n]
+ self.lines[self.y].extend(self._emptyLine(min(self.width - self.x, n)))
+
+ def insertLine(self, n=1):
+ self.lines[self.y:self.y] = [self._emptyLine(self.width) for i in range(n)]
+ del self.lines[self.height:]
+
+ def deleteLine(self, n=1):
+ del self.lines[self.y:self.y+n]
+ self.lines.extend([self._emptyLine(self.width) for i in range(n)])
+
+ def reportCursorPosition(self):
+ return (self.x, self.y)
+
+ def reset(self):
+ self.home = insults.Vector(0, 0)
+ self.x = self.y = 0
+ self.modes = {}
+ self.privateModes = {}
+ self.setPrivateModes([insults.privateModes.AUTO_WRAP,
+ insults.privateModes.CURSOR_MODE])
+ self.numericKeypad = 'app'
+ self.activeCharset = insults.G0
+ self.graphicRendition = {
+ 'bold': False,
+ 'underline': False,
+ 'blink': False,
+ 'reverseVideo': False,
+ 'foreground': WHITE,
+ 'background': BLACK}
+ self.charsets = {
+ insults.G0: insults.CS_US,
+ insults.G1: insults.CS_US,
+ insults.G2: insults.CS_ALTERNATE,
+ insults.G3: insults.CS_ALTERNATE_SPECIAL}
+ self.eraseDisplay()
+
+ def unhandledControlSequence(self, buf):
+ print 'Could not handle', repr(buf)
+
+ def __str__(self):
+ lines = []
+ for L in self.lines:
+ buf = []
+ length = 0
+ for (ch, attr) in L:
+ if ch is not self.void:
+ buf.append(ch)
+ length = len(buf)
+ else:
+ buf.append(self.fill)
+ lines.append(''.join(buf[:length]))
+ return '\n'.join(lines)
+
+class ExpectationTimeout(Exception):
+ pass
+
+class ExpectableBuffer(TerminalBuffer):
+ _mark = 0
+
+ def connectionMade(self):
+ TerminalBuffer.connectionMade(self)
+ self._expecting = []
+
+ def write(self, bytes):
+ TerminalBuffer.write(self, bytes)
+ self._checkExpected()
+
+ def cursorHome(self):
+ TerminalBuffer.cursorHome(self)
+ self._mark = 0
+
+ def _timeoutExpected(self, d):
+ d.errback(ExpectationTimeout())
+ self._checkExpected()
+
+ def _checkExpected(self):
+ s = str(self)[self._mark:]
+ while self._expecting:
+ expr, timer, deferred = self._expecting[0]
+ if timer and not timer.active():
+ del self._expecting[0]
+ continue
+ for match in expr.finditer(s):
+ if timer:
+ timer.cancel()
+ del self._expecting[0]
+ self._mark += match.end()
+ s = s[match.end():]
+ deferred.callback(match)
+ break
+ else:
+ return
+
+ def expect(self, expression, timeout=None, scheduler=reactor):
+ d = defer.Deferred()
+ timer = None
+ if timeout:
+ timer = scheduler.callLater(timeout, self._timeoutExpected, d)
+ self._expecting.append((re.compile(expression), timer, d))
+ self._checkExpected()
+ return d
+
+__all__ = ['CharacterAttribute', 'TerminalBuffer', 'ExpectableBuffer']
diff --git a/twisted/conch/insults/insults.py b/twisted/conch/insults/insults.py
new file mode 100644
index 0000000..721551d
--- /dev/null
+++ b/twisted/conch/insults/insults.py
@@ -0,0 +1,1087 @@
+# -*- test-case-name: twisted.conch.test.test_insults -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+VT102 and VT220 terminal manipulation.
+
+@author: Jp Calderone
+"""
+
+from zope.interface import implements, Interface
+
+from twisted.internet import protocol, defer, interfaces as iinternet
+
+class ITerminalProtocol(Interface):
+ def makeConnection(transport):
+ """Called with an L{ITerminalTransport} when a connection is established.
+ """
+
+ def keystrokeReceived(keyID, modifier):
+ """A keystroke was received.
+
+ Each keystroke corresponds to one invocation of this method.
+ keyID is a string identifier for that key. Printable characters
+ are represented by themselves. Control keys, such as arrows and
+ function keys, are represented with symbolic constants on
+ L{ServerProtocol}.
+ """
+
+ def terminalSize(width, height):
+ """Called to indicate the size of the terminal.
+
+ A terminal of 80x24 should be assumed if this method is not
+ called. This method might not be called for real terminals.
+ """
+
+ def unhandledControlSequence(seq):
+ """Called when an unsupported control sequence is received.
+
+ @type seq: C{str}
+ @param seq: The whole control sequence which could not be interpreted.
+ """
+
+ def connectionLost(reason):
+ """Called when the connection has been lost.
+
+ reason is a Failure describing why.
+ """
+
+class TerminalProtocol(object):
+ implements(ITerminalProtocol)
+
+ def makeConnection(self, terminal):
+ # assert ITerminalTransport.providedBy(transport), "TerminalProtocol.makeConnection must be passed an ITerminalTransport implementor"
+ self.terminal = terminal
+ self.connectionMade()
+
+ def connectionMade(self):
+ """Called after a connection has been established.
+ """
+
+ def keystrokeReceived(self, keyID, modifier):
+ pass
+
+ def terminalSize(self, width, height):
+ pass
+
+ def unhandledControlSequence(self, seq):
+ pass
+
+ def connectionLost(self, reason):
+ pass
+
+class ITerminalTransport(iinternet.ITransport):
+ def cursorUp(n=1):
+ """Move the cursor up n lines.
+ """
+
+ def cursorDown(n=1):
+ """Move the cursor down n lines.
+ """
+
+ def cursorForward(n=1):
+ """Move the cursor right n columns.
+ """
+
+ def cursorBackward(n=1):
+ """Move the cursor left n columns.
+ """
+
+ def cursorPosition(column, line):
+ """Move the cursor to the given line and column.
+ """
+
+ def cursorHome():
+ """Move the cursor home.
+ """
+
+ def index():
+ """Move the cursor down one line, performing scrolling if necessary.
+ """
+
+ def reverseIndex():
+ """Move the cursor up one line, performing scrolling if necessary.
+ """
+
+ def nextLine():
+ """Move the cursor to the first position on the next line, performing scrolling if necessary.
+ """
+
+ def saveCursor():
+ """Save the cursor position, character attribute, character set, and origin mode selection.
+ """
+
+ def restoreCursor():
+ """Restore the previously saved cursor position, character attribute, character set, and origin mode selection.
+
+ If no cursor state was previously saved, move the cursor to the home position.
+ """
+
+ def setModes(modes):
+ """Set the given modes on the terminal.
+ """
+
+ def resetModes(mode):
+ """Reset the given modes on the terminal.
+ """
+
+
+ def setPrivateModes(modes):
+ """
+ Set the given DEC private modes on the terminal.
+ """
+
+
+ def resetPrivateModes(modes):
+ """
+ Reset the given DEC private modes on the terminal.
+ """
+
+
+ def applicationKeypadMode():
+ """Cause keypad to generate control functions.
+
+ Cursor key mode selects the type of characters generated by cursor keys.
+ """
+
+ def numericKeypadMode():
+ """Cause keypad to generate normal characters.
+ """
+
+ def selectCharacterSet(charSet, which):
+ """Select a character set.
+
+ charSet should be one of CS_US, CS_UK, CS_DRAWING, CS_ALTERNATE, or
+ CS_ALTERNATE_SPECIAL.
+
+ which should be one of G0 or G1.
+ """
+
+ def shiftIn():
+ """Activate the G0 character set.
+ """
+
+ def shiftOut():
+ """Activate the G1 character set.
+ """
+
+ def singleShift2():
+ """Shift to the G2 character set for a single character.
+ """
+
+ def singleShift3():
+ """Shift to the G3 character set for a single character.
+ """
+
+ def selectGraphicRendition(*attributes):
+ """Enabled one or more character attributes.
+
+ Arguments should be one or more of UNDERLINE, REVERSE_VIDEO, BLINK, or BOLD.
+ NORMAL may also be specified to disable all character attributes.
+ """
+
+ def horizontalTabulationSet():
+ """Set a tab stop at the current cursor position.
+ """
+
+ def tabulationClear():
+ """Clear the tab stop at the current cursor position.
+ """
+
+ def tabulationClearAll():
+ """Clear all tab stops.
+ """
+
+ def doubleHeightLine(top=True):
+ """Make the current line the top or bottom half of a double-height, double-width line.
+
+ If top is True, the current line is the top half. Otherwise, it is the bottom half.
+ """
+
+ def singleWidthLine():
+ """Make the current line a single-width, single-height line.
+ """
+
+ def doubleWidthLine():
+ """Make the current line a double-width line.
+ """
+
+ def eraseToLineEnd():
+ """Erase from the cursor to the end of line, including cursor position.
+ """
+
+ def eraseToLineBeginning():
+ """Erase from the cursor to the beginning of the line, including the cursor position.
+ """
+
+ def eraseLine():
+ """Erase the entire cursor line.
+ """
+
+ def eraseToDisplayEnd():
+ """Erase from the cursor to the end of the display, including the cursor position.
+ """
+
+ def eraseToDisplayBeginning():
+ """Erase from the cursor to the beginning of the display, including the cursor position.
+ """
+
+ def eraseDisplay():
+ """Erase the entire display.
+ """
+
+ def deleteCharacter(n=1):
+ """Delete n characters starting at the cursor position.
+
+ Characters to the right of deleted characters are shifted to the left.
+ """
+
+ def insertLine(n=1):
+ """Insert n lines at the cursor position.
+
+ Lines below the cursor are shifted down. Lines moved past the bottom margin are lost.
+ This command is ignored when the cursor is outside the scroll region.
+ """
+
+ def deleteLine(n=1):
+ """Delete n lines starting at the cursor position.
+
+ Lines below the cursor are shifted up. This command is ignored when the cursor is outside
+ the scroll region.
+ """
+
+ def reportCursorPosition():
+ """Return a Deferred that fires with a two-tuple of (x, y) indicating the cursor position.
+ """
+
+ def reset():
+ """Reset the terminal to its initial state.
+ """
+
+ def unhandledControlSequence(seq):
+ """Called when an unsupported control sequence is received.
+
+ @type seq: C{str}
+ @param seq: The whole control sequence which could not be interpreted.
+ """
+
+
+CSI = '\x1b'
+CST = {'~': 'tilde'}
+
+class modes:
+ """ECMA 48 standardized modes
+ """
+
+ # BREAKS YOPUR KEYBOARD MOFO
+ KEYBOARD_ACTION = KAM = 2
+
+ # When set, enables character insertion. New display characters
+ # move old display characters to the right. Characters moved past
+ # the right margin are lost.
+
+ # When reset, enables replacement mode (disables character
+ # insertion). New display characters replace old display
+ # characters at cursor position. The old character is erased.
+ INSERTION_REPLACEMENT = IRM = 4
+
+ # Set causes a received linefeed, form feed, or vertical tab to
+ # move cursor to first column of next line. RETURN transmits both
+ # a carriage return and linefeed. This selection is also called
+ # new line option.
+
+ # Reset causes a received linefeed, form feed, or vertical tab to
+ # move cursor to next line in current column. RETURN transmits a
+ # carriage return.
+ LINEFEED_NEWLINE = LNM = 20
+
+
+class privateModes:
+ """ANSI-Compatible Private Modes
+ """
+ ERROR = 0
+ CURSOR_KEY = 1
+ ANSI_VT52 = 2
+ COLUMN = 3
+ SCROLL = 4
+ SCREEN = 5
+ ORIGIN = 6
+ AUTO_WRAP = 7
+ AUTO_REPEAT = 8
+ PRINTER_FORM_FEED = 18
+ PRINTER_EXTENT = 19
+
+ # Toggle cursor visibility (reset hides it)
+ CURSOR_MODE = 25
+
+
+# Character sets
+CS_US = 'CS_US'
+CS_UK = 'CS_UK'
+CS_DRAWING = 'CS_DRAWING'
+CS_ALTERNATE = 'CS_ALTERNATE'
+CS_ALTERNATE_SPECIAL = 'CS_ALTERNATE_SPECIAL'
+
+# Groupings (or something?? These are like variables that can be bound to character sets)
+G0 = 'G0'
+G1 = 'G1'
+
+# G2 and G3 cannot be changed, but they can be shifted to.
+G2 = 'G2'
+G3 = 'G3'
+
+# Character attributes
+
+NORMAL = 0
+BOLD = 1
+UNDERLINE = 4
+BLINK = 5
+REVERSE_VIDEO = 7
+
+class Vector:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+def log(s):
+ file('log', 'a').write(str(s) + '\n')
+
+# XXX TODO - These attributes are really part of the
+# ITerminalTransport interface, I think.
+_KEY_NAMES = ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW',
+ 'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN', 'NUMPAD_MIDDLE',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
+ 'F10', 'F11', 'F12',
+
+ 'ALT', 'SHIFT', 'CONTROL')
+
+class _const(object):
+ """
+ @ivar name: A string naming this constant
+ """
+ def __init__(self, name):
+ self.name = name
+
+ def __repr__(self):
+ return '[' + self.name + ']'
+
+
+FUNCTION_KEYS = [
+ _const(_name) for _name in _KEY_NAMES]
+
+class ServerProtocol(protocol.Protocol):
+ implements(ITerminalTransport)
+
+ protocolFactory = None
+ terminalProtocol = None
+
+ TAB = '\t'
+ BACKSPACE = '\x7f'
+ ##
+
+ lastWrite = ''
+
+ state = 'data'
+
+ termSize = Vector(80, 24)
+ cursorPos = Vector(0, 0)
+ scrollRegion = None
+
+ # Factory who instantiated me
+ factory = None
+
+ def __init__(self, protocolFactory=None, *a, **kw):
+ """
+ @param protocolFactory: A callable which will be invoked with
+ *a, **kw and should return an ITerminalProtocol implementor.
+ This will be invoked when a connection to this ServerProtocol
+ is established.
+
+ @param a: Any positional arguments to pass to protocolFactory.
+ @param kw: Any keyword arguments to pass to protocolFactory.
+ """
+ # assert protocolFactory is None or ITerminalProtocol.implementedBy(protocolFactory), "ServerProtocol.__init__ must be passed an ITerminalProtocol implementor"
+ if protocolFactory is not None:
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = a
+ self.protocolKwArgs = kw
+
+ self._cursorReports = []
+
+ def connectionMade(self):
+ if self.protocolFactory is not None:
+ self.terminalProtocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs)
+
+ try:
+ factory = self.factory
+ except AttributeError:
+ pass
+ else:
+ self.terminalProtocol.factory = factory
+
+ self.terminalProtocol.makeConnection(self)
+
+ def dataReceived(self, data):
+ for ch in data:
+ if self.state == 'data':
+ if ch == '\x1b':
+ self.state = 'escaped'
+ else:
+ self.terminalProtocol.keystrokeReceived(ch, None)
+ elif self.state == 'escaped':
+ if ch == '[':
+ self.state = 'bracket-escaped'
+ self.escBuf = []
+ elif ch == 'O':
+ self.state = 'low-function-escaped'
+ else:
+ self.state = 'data'
+ self._handleShortControlSequence(ch)
+ elif self.state == 'bracket-escaped':
+ if ch == 'O':
+ self.state = 'low-function-escaped'
+ elif ch.isalpha() or ch == '~':
+ self._handleControlSequence(''.join(self.escBuf) + ch)
+ del self.escBuf
+ self.state = 'data'
+ else:
+ self.escBuf.append(ch)
+ elif self.state == 'low-function-escaped':
+ self._handleLowFunctionControlSequence(ch)
+ self.state = 'data'
+ else:
+ raise ValueError("Illegal state")
+
+ def _handleShortControlSequence(self, ch):
+ self.terminalProtocol.keystrokeReceived(ch, self.ALT)
+
+ def _handleControlSequence(self, buf):
+ buf = '\x1b[' + buf
+ f = getattr(self.controlSequenceParser, CST.get(buf[-1], buf[-1]), None)
+ if f is None:
+ self.unhandledControlSequence(buf)
+ else:
+ f(self, self.terminalProtocol, buf[:-1])
+
+ def unhandledControlSequence(self, buf):
+ self.terminalProtocol.unhandledControlSequence(buf)
+
+ def _handleLowFunctionControlSequence(self, ch):
+ map = {'P': self.F1, 'Q': self.F2, 'R': self.F3, 'S': self.F4}
+ keyID = map.get(ch)
+ if keyID is not None:
+ self.terminalProtocol.keystrokeReceived(keyID, None)
+ else:
+ self.terminalProtocol.unhandledControlSequence('\x1b[O' + ch)
+
+ class ControlSequenceParser:
+ def A(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.UP_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + 'A')
+
+ def B(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.DOWN_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + 'B')
+
+ def C(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.RIGHT_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + 'C')
+
+ def D(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.LEFT_ARROW, None)
+ else:
+ handler.unhandledControlSequence(buf + 'D')
+
+ def E(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.NUMPAD_MIDDLE, None)
+ else:
+ handler.unhandledControlSequence(buf + 'E')
+
+ def F(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.END, None)
+ else:
+ handler.unhandledControlSequence(buf + 'F')
+
+ def H(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.HOME, None)
+ else:
+ handler.unhandledControlSequence(buf + 'H')
+
+ def R(self, proto, handler, buf):
+ if not proto._cursorReports:
+ handler.unhandledControlSequence(buf + 'R')
+ elif buf.startswith('\x1b['):
+ report = buf[2:]
+ parts = report.split(';')
+ if len(parts) != 2:
+ handler.unhandledControlSequence(buf + 'R')
+ else:
+ Pl, Pc = parts
+ try:
+ Pl, Pc = int(Pl), int(Pc)
+ except ValueError:
+ handler.unhandledControlSequence(buf + 'R')
+ else:
+ d = proto._cursorReports.pop(0)
+ d.callback((Pc - 1, Pl - 1))
+ else:
+ handler.unhandledControlSequence(buf + 'R')
+
+ def Z(self, proto, handler, buf):
+ if buf == '\x1b[':
+ handler.keystrokeReceived(proto.TAB, proto.SHIFT)
+ else:
+ handler.unhandledControlSequence(buf + 'Z')
+
+ def tilde(self, proto, handler, buf):
+ map = {1: proto.HOME, 2: proto.INSERT, 3: proto.DELETE,
+ 4: proto.END, 5: proto.PGUP, 6: proto.PGDN,
+
+ 15: proto.F5, 17: proto.F6, 18: proto.F7,
+ 19: proto.F8, 20: proto.F9, 21: proto.F10,
+ 23: proto.F11, 24: proto.F12}
+
+ if buf.startswith('\x1b['):
+ ch = buf[2:]
+ try:
+ v = int(ch)
+ except ValueError:
+ handler.unhandledControlSequence(buf + '~')
+ else:
+ symbolic = map.get(v)
+ if symbolic is not None:
+ handler.keystrokeReceived(map[v], None)
+ else:
+ handler.unhandledControlSequence(buf + '~')
+ else:
+ handler.unhandledControlSequence(buf + '~')
+
+ controlSequenceParser = ControlSequenceParser()
+
+ # ITerminalTransport
+ def cursorUp(self, n=1):
+ assert n >= 1
+ self.cursorPos.y = max(self.cursorPos.y - n, 0)
+ self.write('\x1b[%dA' % (n,))
+
+ def cursorDown(self, n=1):
+ assert n >= 1
+ self.cursorPos.y = min(self.cursorPos.y + n, self.termSize.y - 1)
+ self.write('\x1b[%dB' % (n,))
+
+ def cursorForward(self, n=1):
+ assert n >= 1
+ self.cursorPos.x = min(self.cursorPos.x + n, self.termSize.x - 1)
+ self.write('\x1b[%dC' % (n,))
+
+ def cursorBackward(self, n=1):
+ assert n >= 1
+ self.cursorPos.x = max(self.cursorPos.x - n, 0)
+ self.write('\x1b[%dD' % (n,))
+
+ def cursorPosition(self, column, line):
+ self.write('\x1b[%d;%dH' % (line + 1, column + 1))
+
+ def cursorHome(self):
+ self.cursorPos.x = self.cursorPos.y = 0
+ self.write('\x1b[H')
+
+ def index(self):
+ self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1)
+ self.write('\x1bD')
+
+ def reverseIndex(self):
+ self.cursorPos.y = max(self.cursorPos.y - 1, 0)
+ self.write('\x1bM')
+
+ def nextLine(self):
+ self.cursorPos.x = 0
+ self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1)
+ self.write('\n')
+
+ def saveCursor(self):
+ self._savedCursorPos = Vector(self.cursorPos.x, self.cursorPos.y)
+ self.write('\x1b7')
+
+ def restoreCursor(self):
+ self.cursorPos = self._savedCursorPos
+ del self._savedCursorPos
+ self.write('\x1b8')
+
+ def setModes(self, modes):
+ # XXX Support ANSI-Compatible private modes
+ self.write('\x1b[%sh' % (';'.join(map(str, modes)),))
+
+ def setPrivateModes(self, modes):
+ self.write('\x1b[?%sh' % (';'.join(map(str, modes)),))
+
+ def resetModes(self, modes):
+ # XXX Support ANSI-Compatible private modes
+ self.write('\x1b[%sl' % (';'.join(map(str, modes)),))
+
+ def resetPrivateModes(self, modes):
+ self.write('\x1b[?%sl' % (';'.join(map(str, modes)),))
+
+ def applicationKeypadMode(self):
+ self.write('\x1b=')
+
+ def numericKeypadMode(self):
+ self.write('\x1b>')
+
+ def selectCharacterSet(self, charSet, which):
+ # XXX Rewrite these as dict lookups
+ if which == G0:
+ which = '('
+ elif which == G1:
+ which = ')'
+ else:
+ raise ValueError("`which' argument to selectCharacterSet must be G0 or G1")
+ if charSet == CS_UK:
+ charSet = 'A'
+ elif charSet == CS_US:
+ charSet = 'B'
+ elif charSet == CS_DRAWING:
+ charSet = '0'
+ elif charSet == CS_ALTERNATE:
+ charSet = '1'
+ elif charSet == CS_ALTERNATE_SPECIAL:
+ charSet = '2'
+ else:
+ raise ValueError("Invalid `charSet' argument to selectCharacterSet")
+ self.write('\x1b' + which + charSet)
+
+ def shiftIn(self):
+ self.write('\x15')
+
+ def shiftOut(self):
+ self.write('\x14')
+
+ def singleShift2(self):
+ self.write('\x1bN')
+
+ def singleShift3(self):
+ self.write('\x1bO')
+
+ def selectGraphicRendition(self, *attributes):
+ attrs = []
+ for a in attributes:
+ attrs.append(a)
+ self.write('\x1b[%sm' % (';'.join(attrs),))
+
+ def horizontalTabulationSet(self):
+ self.write('\x1bH')
+
+ def tabulationClear(self):
+ self.write('\x1b[q')
+
+ def tabulationClearAll(self):
+ self.write('\x1b[3q')
+
+ def doubleHeightLine(self, top=True):
+ if top:
+ self.write('\x1b#3')
+ else:
+ self.write('\x1b#4')
+
+ def singleWidthLine(self):
+ self.write('\x1b#5')
+
+ def doubleWidthLine(self):
+ self.write('\x1b#6')
+
+ def eraseToLineEnd(self):
+ self.write('\x1b[K')
+
+ def eraseToLineBeginning(self):
+ self.write('\x1b[1K')
+
+ def eraseLine(self):
+ self.write('\x1b[2K')
+
+ def eraseToDisplayEnd(self):
+ self.write('\x1b[J')
+
+ def eraseToDisplayBeginning(self):
+ self.write('\x1b[1J')
+
+ def eraseDisplay(self):
+ self.write('\x1b[2J')
+
+ def deleteCharacter(self, n=1):
+ self.write('\x1b[%dP' % (n,))
+
+ def insertLine(self, n=1):
+ self.write('\x1b[%dL' % (n,))
+
+ def deleteLine(self, n=1):
+ self.write('\x1b[%dM' % (n,))
+
+ def setScrollRegion(self, first=None, last=None):
+ if first is not None:
+ first = '%d' % (first,)
+ else:
+ first = ''
+ if last is not None:
+ last = '%d' % (last,)
+ else:
+ last = ''
+ self.write('\x1b[%s;%sr' % (first, last))
+
+ def resetScrollRegion(self):
+ self.setScrollRegion()
+
+ def reportCursorPosition(self):
+ d = defer.Deferred()
+ self._cursorReports.append(d)
+ self.write('\x1b[6n')
+ return d
+
+ def reset(self):
+ self.cursorPos.x = self.cursorPos.y = 0
+ try:
+ del self._savedCursorPos
+ except AttributeError:
+ pass
+ self.write('\x1bc')
+
+ # ITransport
+ def write(self, bytes):
+ if bytes:
+ self.lastWrite = bytes
+ self.transport.write('\r\n'.join(bytes.split('\n')))
+
+ def writeSequence(self, bytes):
+ self.write(''.join(bytes))
+
+ def loseConnection(self):
+ self.reset()
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ if self.terminalProtocol is not None:
+ try:
+ self.terminalProtocol.connectionLost(reason)
+ finally:
+ self.terminalProtocol = None
+# Add symbolic names for function keys
+for name, const in zip(_KEY_NAMES, FUNCTION_KEYS):
+ setattr(ServerProtocol, name, const)
+
+
+
+class ClientProtocol(protocol.Protocol):
+
+ terminalFactory = None
+ terminal = None
+
+ state = 'data'
+
+ _escBuf = None
+
+ _shorts = {
+ 'D': 'index',
+ 'M': 'reverseIndex',
+ 'E': 'nextLine',
+ '7': 'saveCursor',
+ '8': 'restoreCursor',
+ '=': 'applicationKeypadMode',
+ '>': 'numericKeypadMode',
+ 'N': 'singleShift2',
+ 'O': 'singleShift3',
+ 'H': 'horizontalTabulationSet',
+ 'c': 'reset'}
+
+ _longs = {
+ '[': 'bracket-escape',
+ '(': 'select-g0',
+ ')': 'select-g1',
+ '#': 'select-height-width'}
+
+ _charsets = {
+ 'A': CS_UK,
+ 'B': CS_US,
+ '0': CS_DRAWING,
+ '1': CS_ALTERNATE,
+ '2': CS_ALTERNATE_SPECIAL}
+
+ # Factory who instantiated me
+ factory = None
+
+ def __init__(self, terminalFactory=None, *a, **kw):
+ """
+ @param terminalFactory: A callable which will be invoked with
+ *a, **kw and should return an ITerminalTransport provider.
+ This will be invoked when this ClientProtocol establishes a
+ connection.
+
+ @param a: Any positional arguments to pass to terminalFactory.
+ @param kw: Any keyword arguments to pass to terminalFactory.
+ """
+ # assert terminalFactory is None or ITerminalTransport.implementedBy(terminalFactory), "ClientProtocol.__init__ must be passed an ITerminalTransport implementor"
+ if terminalFactory is not None:
+ self.terminalFactory = terminalFactory
+ self.terminalArgs = a
+ self.terminalKwArgs = kw
+
+ def connectionMade(self):
+ if self.terminalFactory is not None:
+ self.terminal = self.terminalFactory(*self.terminalArgs, **self.terminalKwArgs)
+ self.terminal.factory = self.factory
+ self.terminal.makeConnection(self)
+
+ def connectionLost(self, reason):
+ if self.terminal is not None:
+ try:
+ self.terminal.connectionLost(reason)
+ finally:
+ del self.terminal
+
+ def dataReceived(self, bytes):
+ """
+ Parse the given data from a terminal server, dispatching to event
+ handlers defined by C{self.terminal}.
+ """
+ toWrite = []
+ for b in bytes:
+ if self.state == 'data':
+ if b == '\x1b':
+ if toWrite:
+ self.terminal.write(''.join(toWrite))
+ del toWrite[:]
+ self.state = 'escaped'
+ elif b == '\x14':
+ if toWrite:
+ self.terminal.write(''.join(toWrite))
+ del toWrite[:]
+ self.terminal.shiftOut()
+ elif b == '\x15':
+ if toWrite:
+ self.terminal.write(''.join(toWrite))
+ del toWrite[:]
+ self.terminal.shiftIn()
+ elif b == '\x08':
+ if toWrite:
+ self.terminal.write(''.join(toWrite))
+ del toWrite[:]
+ self.terminal.cursorBackward()
+ else:
+ toWrite.append(b)
+ elif self.state == 'escaped':
+ fName = self._shorts.get(b)
+ if fName is not None:
+ self.state = 'data'
+ getattr(self.terminal, fName)()
+ else:
+ state = self._longs.get(b)
+ if state is not None:
+ self.state = state
+ else:
+ self.terminal.unhandledControlSequence('\x1b' + b)
+ self.state = 'data'
+ elif self.state == 'bracket-escape':
+ if self._escBuf is None:
+ self._escBuf = []
+ if b.isalpha() or b == '~':
+ self._handleControlSequence(''.join(self._escBuf), b)
+ del self._escBuf
+ self.state = 'data'
+ else:
+ self._escBuf.append(b)
+ elif self.state == 'select-g0':
+ self.terminal.selectCharacterSet(self._charsets.get(b, b), G0)
+ self.state = 'data'
+ elif self.state == 'select-g1':
+ self.terminal.selectCharacterSet(self._charsets.get(b, b), G1)
+ self.state = 'data'
+ elif self.state == 'select-height-width':
+ self._handleHeightWidth(b)
+ self.state = 'data'
+ else:
+ raise ValueError("Illegal state")
+ if toWrite:
+ self.terminal.write(''.join(toWrite))
+
+
+ def _handleControlSequence(self, buf, terminal):
+ f = getattr(self.controlSequenceParser, CST.get(terminal, terminal), None)
+ if f is None:
+ self.terminal.unhandledControlSequence('\x1b[' + buf + terminal)
+ else:
+ f(self, self.terminal, buf)
+
+ class ControlSequenceParser:
+ def _makeSimple(ch, fName):
+ n = 'cursor' + fName
+ def simple(self, proto, handler, buf):
+ if not buf:
+ getattr(handler, n)(1)
+ else:
+ try:
+ m = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + ch)
+ else:
+ getattr(handler, n)(m)
+ return simple
+ for (ch, fName) in (('A', 'Up'),
+ ('B', 'Down'),
+ ('C', 'Forward'),
+ ('D', 'Backward')):
+ exec ch + " = _makeSimple(ch, fName)"
+ del _makeSimple
+
+ def h(self, proto, handler, buf):
+ # XXX - Handle '?' to introduce ANSI-Compatible private modes.
+ try:
+ modes = map(int, buf.split(';'))
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'h')
+ else:
+ handler.setModes(modes)
+
+ def l(self, proto, handler, buf):
+ # XXX - Handle '?' to introduce ANSI-Compatible private modes.
+ try:
+ modes = map(int, buf.split(';'))
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'l')
+ else:
+ handler.resetModes(modes)
+
+ def r(self, proto, handler, buf):
+ parts = buf.split(';')
+ if len(parts) == 1:
+ handler.setScrollRegion(None, None)
+ elif len(parts) == 2:
+ try:
+ if parts[0]:
+ pt = int(parts[0])
+ else:
+ pt = None
+ if parts[1]:
+ pb = int(parts[1])
+ else:
+ pb = None
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'r')
+ else:
+ handler.setScrollRegion(pt, pb)
+ else:
+ handler.unhandledControlSequence('\x1b[' + buf + 'r')
+
+ def K(self, proto, handler, buf):
+ if not buf:
+ handler.eraseToLineEnd()
+ elif buf == '1':
+ handler.eraseToLineBeginning()
+ elif buf == '2':
+ handler.eraseLine()
+ else:
+ handler.unhandledControlSequence('\x1b[' + buf + 'K')
+
+ def H(self, proto, handler, buf):
+ handler.cursorHome()
+
+ def J(self, proto, handler, buf):
+ if not buf:
+ handler.eraseToDisplayEnd()
+ elif buf == '1':
+ handler.eraseToDisplayBeginning()
+ elif buf == '2':
+ handler.eraseDisplay()
+ else:
+ handler.unhandledControlSequence('\x1b[' + buf + 'J')
+
+ def P(self, proto, handler, buf):
+ if not buf:
+ handler.deleteCharacter(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'P')
+ else:
+ handler.deleteCharacter(n)
+
+ def L(self, proto, handler, buf):
+ if not buf:
+ handler.insertLine(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'L')
+ else:
+ handler.insertLine(n)
+
+ def M(self, proto, handler, buf):
+ if not buf:
+ handler.deleteLine(1)
+ else:
+ try:
+ n = int(buf)
+ except ValueError:
+ handler.unhandledControlSequence('\x1b[' + buf + 'M')
+ else:
+ handler.deleteLine(n)
+
+ def n(self, proto, handler, buf):
+ if buf == '6':
+ x, y = handler.reportCursorPosition()
+ proto.transport.write('\x1b[%d;%dR' % (x + 1, y + 1))
+ else:
+ handler.unhandledControlSequence('\x1b[' + buf + 'n')
+
+ def m(self, proto, handler, buf):
+ if not buf:
+ handler.selectGraphicRendition(NORMAL)
+ else:
+ attrs = []
+ for a in buf.split(';'):
+ try:
+ a = int(a)
+ except ValueError:
+ pass
+ attrs.append(a)
+ handler.selectGraphicRendition(*attrs)
+
+ controlSequenceParser = ControlSequenceParser()
+
+ def _handleHeightWidth(self, b):
+ if b == '3':
+ self.terminal.doubleHeightLine(True)
+ elif b == '4':
+ self.terminal.doubleHeightLine(False)
+ elif b == '5':
+ self.terminal.singleWidthLine()
+ elif b == '6':
+ self.terminal.doubleWidthLine()
+ else:
+ self.terminal.unhandledControlSequence('\x1b#' + b)
+
+
+__all__ = [
+ # Interfaces
+ 'ITerminalProtocol', 'ITerminalTransport',
+
+ # Symbolic constants
+ 'modes', 'privateModes', 'FUNCTION_KEYS',
+
+ 'CS_US', 'CS_UK', 'CS_DRAWING', 'CS_ALTERNATE', 'CS_ALTERNATE_SPECIAL',
+ 'G0', 'G1', 'G2', 'G3',
+
+ 'UNDERLINE', 'REVERSE_VIDEO', 'BLINK', 'BOLD', 'NORMAL',
+
+ # Protocol classes
+ 'ServerProtocol', 'ClientProtocol']
diff --git a/twisted/conch/insults/text.py b/twisted/conch/insults/text.py
new file mode 100644
index 0000000..e5c8fd1
--- /dev/null
+++ b/twisted/conch/insults/text.py
@@ -0,0 +1,186 @@
+# -*- test-case-name: twisted.conch.test.test_text -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Character attribute manipulation API
+
+This module provides a domain-specific language (using Python syntax)
+for the creation of text with additional display attributes associated
+with it. It is intended as an alternative to manually building up
+strings containing ECMA 48 character attribute control codes. It
+currently supports foreground and background colors (black, red,
+green, yellow, blue, magenta, cyan, and white), intensity selection,
+underlining, blinking and reverse video. Character set selection
+support is planned.
+
+Character attributes are specified by using two Python operations:
+attribute lookup and indexing. For example, the string \"Hello
+world\" with red foreground and all other attributes set to their
+defaults, assuming the name twisted.conch.insults.text.attributes has
+been imported and bound to the name \"A\" (with the statement C{from
+twisted.conch.insults.text import attributes as A}, for example) one
+uses this expression::
+
+ | A.fg.red[\"Hello world\"]
+
+Other foreground colors are set by substituting their name for
+\"red\". To set both a foreground and a background color, this
+expression is used::
+
+ | A.fg.red[A.bg.green[\"Hello world\"]]
+
+Note that either A.bg.green can be nested within A.fg.red or vice
+versa. Also note that multiple items can be nested within a single
+index operation by separating them with commas::
+
+ | A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]
+
+Other character attributes are set in a similar fashion. To specify a
+blinking version of the previous expression::
+
+ | A.blink[A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]]
+
+C{A.reverseVideo}, C{A.underline}, and C{A.bold} are also valid.
+
+A third operation is actually supported: unary negation. This turns
+off an attribute when an enclosing expression would otherwise have
+caused it to be on. For example::
+
+ | A.underline[A.fg.red[\"Hello\", -A.underline[\" world\"]]]
+
+@author: Jp Calderone
+"""
+
+from twisted.conch.insults import helper, insults
+
+class _Attribute(object):
+ def __init__(self):
+ self.children = []
+
+ def __getitem__(self, item):
+ assert isinstance(item, (list, tuple, _Attribute, str))
+ if isinstance(item, (list, tuple)):
+ self.children.extend(item)
+ else:
+ self.children.append(item)
+ return self
+
+ def serialize(self, write, attrs=None):
+ if attrs is None:
+ attrs = helper.CharacterAttribute()
+ for ch in self.children:
+ if isinstance(ch, _Attribute):
+ ch.serialize(write, attrs.copy())
+ else:
+ write(attrs.toVT102())
+ write(ch)
+
+class _NormalAttr(_Attribute):
+ def serialize(self, write, attrs):
+ attrs.__init__()
+ super(_NormalAttr, self).serialize(write, attrs)
+
+class _OtherAttr(_Attribute):
+ def __init__(self, attrname, attrvalue):
+ self.attrname = attrname
+ self.attrvalue = attrvalue
+ self.children = []
+
+ def __neg__(self):
+ result = _OtherAttr(self.attrname, not self.attrvalue)
+ result.children.extend(self.children)
+ return result
+
+ def serialize(self, write, attrs):
+ attrs = attrs.wantOne(**{self.attrname: self.attrvalue})
+ super(_OtherAttr, self).serialize(write, attrs)
+
+class _ColorAttr(_Attribute):
+ def __init__(self, color, ground):
+ self.color = color
+ self.ground = ground
+ self.children = []
+
+ def serialize(self, write, attrs):
+ attrs = attrs.wantOne(**{self.ground: self.color})
+ super(_ColorAttr, self).serialize(write, attrs)
+
+class _ForegroundColorAttr(_ColorAttr):
+ def __init__(self, color):
+ super(_ForegroundColorAttr, self).__init__(color, 'foreground')
+
+class _BackgroundColorAttr(_ColorAttr):
+ def __init__(self, color):
+ super(_BackgroundColorAttr, self).__init__(color, 'background')
+
+class CharacterAttributes(object):
+ class _ColorAttribute(object):
+ def __init__(self, ground):
+ self.ground = ground
+
+ attrs = {
+ 'black': helper.BLACK,
+ 'red': helper.RED,
+ 'green': helper.GREEN,
+ 'yellow': helper.YELLOW,
+ 'blue': helper.BLUE,
+ 'magenta': helper.MAGENTA,
+ 'cyan': helper.CYAN,
+ 'white': helper.WHITE}
+
+ def __getattr__(self, name):
+ try:
+ return self.ground(self.attrs[name])
+ except KeyError:
+ raise AttributeError(name)
+
+ fg = _ColorAttribute(_ForegroundColorAttr)
+ bg = _ColorAttribute(_BackgroundColorAttr)
+
+ attrs = {
+ 'bold': insults.BOLD,
+ 'blink': insults.BLINK,
+ 'underline': insults.UNDERLINE,
+ 'reverseVideo': insults.REVERSE_VIDEO}
+
+ def __getattr__(self, name):
+ if name == 'normal':
+ return _NormalAttr()
+ if name in self.attrs:
+ return _OtherAttr(name, True)
+ raise AttributeError(name)
+
+def flatten(output, attrs):
+ """Serialize a sequence of characters with attribute information
+
+ The resulting string can be interpreted by VT102-compatible
+ terminals so that the contained characters are displayed and, for
+ those attributes which the terminal supports, have the attributes
+ specified in the input.
+
+ For example, if your terminal is VT102 compatible, you might run
+ this for a colorful variation on the \"hello world\" theme::
+
+ | from twisted.conch.insults.text import flatten, attributes as A
+ | from twisted.conch.insults.helper import CharacterAttribute
+ | print flatten(
+ | A.normal[A.bold[A.fg.red['He'], A.fg.green['ll'], A.fg.magenta['o'], ' ',
+ | A.fg.yellow['Wo'], A.fg.blue['rl'], A.fg.cyan['d!']]],
+ | CharacterAttribute())
+
+ @param output: Object returned by accessing attributes of the
+ module-level attributes object.
+
+ @param attrs: A L{twisted.conch.insults.helper.CharacterAttribute}
+ instance
+
+ @return: A VT102-friendly string
+ """
+ L = []
+ output.serialize(L.append, attrs)
+ return ''.join(L)
+
+attributes = CharacterAttributes()
+
+__all__ = ['attributes', 'flatten']
diff --git a/twisted/conch/insults/window.py b/twisted/conch/insults/window.py
new file mode 100644
index 0000000..9901327
--- /dev/null
+++ b/twisted/conch/insults/window.py
@@ -0,0 +1,868 @@
+# -*- test-case-name: twisted.conch.test.test_window -*-
+
+"""
+Simple insults-based widget library
+
+@author: Jp Calderone
+"""
+
+import array
+
+from twisted.conch.insults import insults, helper
+from twisted.python import text as tptext
+
+class YieldFocus(Exception):
+ """Input focus manipulation exception
+ """
+
+class BoundedTerminalWrapper(object):
+ def __init__(self, terminal, width, height, xoff, yoff):
+ self.width = width
+ self.height = height
+ self.xoff = xoff
+ self.yoff = yoff
+ self.terminal = terminal
+ self.cursorForward = terminal.cursorForward
+ self.selectCharacterSet = terminal.selectCharacterSet
+ self.selectGraphicRendition = terminal.selectGraphicRendition
+ self.saveCursor = terminal.saveCursor
+ self.restoreCursor = terminal.restoreCursor
+
+ def cursorPosition(self, x, y):
+ return self.terminal.cursorPosition(
+ self.xoff + min(self.width, x),
+ self.yoff + min(self.height, y)
+ )
+
+ def cursorHome(self):
+ return self.terminal.cursorPosition(
+ self.xoff, self.yoff)
+
+ def write(self, bytes):
+ return self.terminal.write(bytes)
+
+class Widget(object):
+ focused = False
+ parent = None
+ dirty = False
+ width = height = None
+
+ def repaint(self):
+ if not self.dirty:
+ self.dirty = True
+ if self.parent is not None and not self.parent.dirty:
+ self.parent.repaint()
+
+ def filthy(self):
+ self.dirty = True
+
+ def redraw(self, width, height, terminal):
+ self.filthy()
+ self.draw(width, height, terminal)
+
+ def draw(self, width, height, terminal):
+ if width != self.width or height != self.height or self.dirty:
+ self.width = width
+ self.height = height
+ self.dirty = False
+ self.render(width, height, terminal)
+
+ def render(self, width, height, terminal):
+ pass
+
+ def sizeHint(self):
+ return None
+
+ def keystrokeReceived(self, keyID, modifier):
+ if keyID == '\t':
+ self.tabReceived(modifier)
+ elif keyID == '\x7f':
+ self.backspaceReceived()
+ elif keyID in insults.FUNCTION_KEYS:
+ self.functionKeyReceived(keyID, modifier)
+ else:
+ self.characterReceived(keyID, modifier)
+
+ def tabReceived(self, modifier):
+ # XXX TODO - Handle shift+tab
+ raise YieldFocus()
+
+ def focusReceived(self):
+ """Called when focus is being given to this widget.
+
+ May raise YieldFocus is this widget does not want focus.
+ """
+ self.focused = True
+ self.repaint()
+
+ def focusLost(self):
+ self.focused = False
+ self.repaint()
+
+ def backspaceReceived(self):
+ pass
+
+ def functionKeyReceived(self, keyID, modifier):
+ func = getattr(self, 'func_' + keyID.name, None)
+ if func is not None:
+ func(modifier)
+
+ def characterReceived(self, keyID, modifier):
+ pass
+
+class ContainerWidget(Widget):
+ """
+ @ivar focusedChild: The contained widget which currently has
+ focus, or None.
+ """
+ focusedChild = None
+ focused = False
+
+ def __init__(self):
+ Widget.__init__(self)
+ self.children = []
+
+ def addChild(self, child):
+ assert child.parent is None
+ child.parent = self
+ self.children.append(child)
+ if self.focusedChild is None and self.focused:
+ try:
+ child.focusReceived()
+ except YieldFocus:
+ pass
+ else:
+ self.focusedChild = child
+ self.repaint()
+
+ def remChild(self, child):
+ assert child.parent is self
+ child.parent = None
+ self.children.remove(child)
+ self.repaint()
+
+ def filthy(self):
+ for ch in self.children:
+ ch.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ for ch in self.children:
+ ch.draw(width, height, terminal)
+
+ def changeFocus(self):
+ self.repaint()
+
+ if self.focusedChild is not None:
+ self.focusedChild.focusLost()
+ focusedChild = self.focusedChild
+ self.focusedChild = None
+ try:
+ curFocus = self.children.index(focusedChild) + 1
+ except ValueError:
+ raise YieldFocus()
+ else:
+ curFocus = 0
+ while curFocus < len(self.children):
+ try:
+ self.children[curFocus].focusReceived()
+ except YieldFocus:
+ curFocus += 1
+ else:
+ self.focusedChild = self.children[curFocus]
+ return
+ # None of our children wanted focus
+ raise YieldFocus()
+
+
+ def focusReceived(self):
+ self.changeFocus()
+ self.focused = True
+
+
+ def keystrokeReceived(self, keyID, modifier):
+ if self.focusedChild is not None:
+ try:
+ self.focusedChild.keystrokeReceived(keyID, modifier)
+ except YieldFocus:
+ self.changeFocus()
+ self.repaint()
+ else:
+ Widget.keystrokeReceived(self, keyID, modifier)
+
+
+class TopWindow(ContainerWidget):
+ """
+ A top-level container object which provides focus wrap-around and paint
+ scheduling.
+
+ @ivar painter: A no-argument callable which will be invoked when this
+ widget needs to be redrawn.
+
+ @ivar scheduler: A one-argument callable which will be invoked with a
+ no-argument callable and should arrange for it to invoked at some point in
+ the near future. The no-argument callable will cause this widget and all
+ its children to be redrawn. It is typically beneficial for the no-argument
+ callable to be invoked at the end of handling for whatever event is
+ currently active; for example, it might make sense to call it at the end of
+ L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}.
+ Note, however, that since calls to this may also be made in response to no
+ apparent event, arrangements should be made for the function to be called
+ even if an event handler such as C{keystrokeReceived} is not on the call
+ stack (eg, using C{reactor.callLater} with a short timeout).
+ """
+ focused = True
+
+ def __init__(self, painter, scheduler):
+ ContainerWidget.__init__(self)
+ self.painter = painter
+ self.scheduler = scheduler
+
+ _paintCall = None
+ def repaint(self):
+ if self._paintCall is None:
+ self._paintCall = object()
+ self.scheduler(self._paint)
+ ContainerWidget.repaint(self)
+
+ def _paint(self):
+ self._paintCall = None
+ self.painter()
+
+ def changeFocus(self):
+ try:
+ ContainerWidget.changeFocus(self)
+ except YieldFocus:
+ try:
+ ContainerWidget.changeFocus(self)
+ except YieldFocus:
+ pass
+
+ def keystrokeReceived(self, keyID, modifier):
+ try:
+ ContainerWidget.keystrokeReceived(self, keyID, modifier)
+ except YieldFocus:
+ self.changeFocus()
+
+
+class AbsoluteBox(ContainerWidget):
+ def moveChild(self, child, x, y):
+ for n in range(len(self.children)):
+ if self.children[n][0] is child:
+ self.children[n] = (child, x, y)
+ break
+ else:
+ raise ValueError("No such child", child)
+
+ def render(self, width, height, terminal):
+ for (ch, x, y) in self.children:
+ wrap = BoundedTerminalWrapper(terminal, width - x, height - y, x, y)
+ ch.draw(width, height, wrap)
+
+
+class _Box(ContainerWidget):
+ TOP, CENTER, BOTTOM = range(3)
+
+ def __init__(self, gravity=CENTER):
+ ContainerWidget.__init__(self)
+ self.gravity = gravity
+
+ def sizeHint(self):
+ height = 0
+ width = 0
+ for ch in self.children:
+ hint = ch.sizeHint()
+ if hint is None:
+ hint = (None, None)
+
+ if self.variableDimension == 0:
+ if hint[0] is None:
+ width = None
+ elif width is not None:
+ width += hint[0]
+ if hint[1] is None:
+ height = None
+ elif height is not None:
+ height = max(height, hint[1])
+ else:
+ if hint[0] is None:
+ width = None
+ elif width is not None:
+ width = max(width, hint[0])
+ if hint[1] is None:
+ height = None
+ elif height is not None:
+ height += hint[1]
+
+ return width, height
+
+
+ def render(self, width, height, terminal):
+ if not self.children:
+ return
+
+ greedy = 0
+ wants = []
+ for ch in self.children:
+ hint = ch.sizeHint()
+ if hint is None:
+ hint = (None, None)
+ if hint[self.variableDimension] is None:
+ greedy += 1
+ wants.append(hint[self.variableDimension])
+
+ length = (width, height)[self.variableDimension]
+ totalWant = sum([w for w in wants if w is not None])
+ if greedy:
+ leftForGreedy = int((length - totalWant) / greedy)
+
+ widthOffset = heightOffset = 0
+
+ for want, ch in zip(wants, self.children):
+ if want is None:
+ want = leftForGreedy
+
+ subWidth, subHeight = width, height
+ if self.variableDimension == 0:
+ subWidth = want
+ else:
+ subHeight = want
+
+ wrap = BoundedTerminalWrapper(
+ terminal,
+ subWidth,
+ subHeight,
+ widthOffset,
+ heightOffset,
+ )
+ ch.draw(subWidth, subHeight, wrap)
+ if self.variableDimension == 0:
+ widthOffset += want
+ else:
+ heightOffset += want
+
+
+class HBox(_Box):
+ variableDimension = 0
+
+class VBox(_Box):
+ variableDimension = 1
+
+
+class Packer(ContainerWidget):
+ def render(self, width, height, terminal):
+ if not self.children:
+ return
+
+ root = int(len(self.children) ** 0.5 + 0.5)
+ boxes = [VBox() for n in range(root)]
+ for n, ch in enumerate(self.children):
+ boxes[n % len(boxes)].addChild(ch)
+ h = HBox()
+ map(h.addChild, boxes)
+ h.render(width, height, terminal)
+
+
+class Canvas(Widget):
+ focused = False
+
+ contents = None
+
+ def __init__(self):
+ Widget.__init__(self)
+ self.resize(1, 1)
+
+ def resize(self, width, height):
+ contents = array.array('c', ' ' * width * height)
+ if self.contents is not None:
+ for x in range(min(width, self._width)):
+ for y in range(min(height, self._height)):
+ contents[width * y + x] = self[x, y]
+ self.contents = contents
+ self._width = width
+ self._height = height
+ if self.x >= width:
+ self.x = width - 1
+ if self.y >= height:
+ self.y = height - 1
+
+ def __getitem__(self, (x, y)):
+ return self.contents[(self._width * y) + x]
+
+ def __setitem__(self, (x, y), value):
+ self.contents[(self._width * y) + x] = value
+
+ def clear(self):
+ self.contents = array.array('c', ' ' * len(self.contents))
+
+ def render(self, width, height, terminal):
+ if not width or not height:
+ return
+
+ if width != self._width or height != self._height:
+ self.resize(width, height)
+ for i in range(height):
+ terminal.cursorPosition(0, i)
+ terminal.write(''.join(self.contents[self._width * i:self._width * i + self._width])[:width])
+
+
+def horizontalLine(terminal, y, left, right):
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+ terminal.cursorPosition(left, y)
+ terminal.write(chr(0161) * (right - left))
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+def verticalLine(terminal, x, top, bottom):
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+ for n in xrange(top, bottom):
+ terminal.cursorPosition(x, n)
+ terminal.write(chr(0170))
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+
+def rectangle(terminal, (top, left), (width, height)):
+ terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
+
+ terminal.cursorPosition(top, left)
+ terminal.write(chr(0154))
+ terminal.write(chr(0161) * (width - 2))
+ terminal.write(chr(0153))
+ for n in range(height - 2):
+ terminal.cursorPosition(left, top + n + 1)
+ terminal.write(chr(0170))
+ terminal.cursorForward(width - 2)
+ terminal.write(chr(0170))
+ terminal.cursorPosition(0, top + height - 1)
+ terminal.write(chr(0155))
+ terminal.write(chr(0161) * (width - 2))
+ terminal.write(chr(0152))
+
+ terminal.selectCharacterSet(insults.CS_US, insults.G0)
+
+class Border(Widget):
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self.containee = containee
+ self.containee.parent = self
+
+ def focusReceived(self):
+ return self.containee.focusReceived()
+
+ def focusLost(self):
+ return self.containee.focusLost()
+
+ def keystrokeReceived(self, keyID, modifier):
+ return self.containee.keystrokeReceived(keyID, modifier)
+
+ def sizeHint(self):
+ hint = self.containee.sizeHint()
+ if hint is None:
+ hint = (None, None)
+ if hint[0] is None:
+ x = None
+ else:
+ x = hint[0] + 2
+ if hint[1] is None:
+ y = None
+ else:
+ y = hint[1] + 2
+ return x, y
+
+ def filthy(self):
+ self.containee.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ if self.containee.focused:
+ terminal.write('\x1b[31m')
+ rectangle(terminal, (0, 0), (width, height))
+ terminal.write('\x1b[0m')
+ wrap = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
+ self.containee.draw(width - 2, height - 2, wrap)
+
+
+class Button(Widget):
+ def __init__(self, label, onPress):
+ Widget.__init__(self)
+ self.label = label
+ self.onPress = onPress
+
+ def sizeHint(self):
+ return len(self.label), 1
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == '\r':
+ self.onPress()
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ if self.focused:
+ terminal.write('\x1b[1m' + self.label + '\x1b[0m')
+ else:
+ terminal.write(self.label)
+
+class TextInput(Widget):
+ def __init__(self, maxwidth, onSubmit):
+ Widget.__init__(self)
+ self.onSubmit = onSubmit
+ self.maxwidth = maxwidth
+ self.buffer = ''
+ self.cursor = 0
+
+ def setText(self, text):
+ self.buffer = text[:self.maxwidth]
+ self.cursor = len(self.buffer)
+ self.repaint()
+
+ def func_LEFT_ARROW(self, modifier):
+ if self.cursor > 0:
+ self.cursor -= 1
+ self.repaint()
+
+ def func_RIGHT_ARROW(self, modifier):
+ if self.cursor < len(self.buffer):
+ self.cursor += 1
+ self.repaint()
+
+ def backspaceReceived(self):
+ if self.cursor > 0:
+ self.buffer = self.buffer[:self.cursor - 1] + self.buffer[self.cursor:]
+ self.cursor -= 1
+ self.repaint()
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == '\r':
+ self.onSubmit(self.buffer)
+ else:
+ if len(self.buffer) < self.maxwidth:
+ self.buffer = self.buffer[:self.cursor] + keyID + self.buffer[self.cursor:]
+ self.cursor += 1
+ self.repaint()
+
+ def sizeHint(self):
+ return self.maxwidth + 1, 1
+
+ def render(self, width, height, terminal):
+ currentText = self._renderText()
+ terminal.cursorPosition(0, 0)
+ if self.focused:
+ terminal.write(currentText[:self.cursor])
+ cursor(terminal, currentText[self.cursor:self.cursor+1] or ' ')
+ terminal.write(currentText[self.cursor+1:])
+ terminal.write(' ' * (self.maxwidth - len(currentText) + 1))
+ else:
+ more = self.maxwidth - len(currentText)
+ terminal.write(currentText + '_' * more)
+
+ def _renderText(self):
+ return self.buffer
+
+class PasswordInput(TextInput):
+ def _renderText(self):
+ return '*' * len(self.buffer)
+
+class TextOutput(Widget):
+ text = ''
+
+ def __init__(self, size=None):
+ Widget.__init__(self)
+ self.size = size
+
+ def sizeHint(self):
+ return self.size
+
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ text = self.text[:width]
+ terminal.write(text + ' ' * (width - len(text)))
+
+ def setText(self, text):
+ self.text = text
+ self.repaint()
+
+ def focusReceived(self):
+ raise YieldFocus()
+
+class TextOutputArea(TextOutput):
+ WRAP, TRUNCATE = range(2)
+
+ def __init__(self, size=None, longLines=WRAP):
+ TextOutput.__init__(self, size)
+ self.longLines = longLines
+
+ def render(self, width, height, terminal):
+ n = 0
+ inputLines = self.text.splitlines()
+ outputLines = []
+ while inputLines:
+ if self.longLines == self.WRAP:
+ wrappedLines = tptext.greedyWrap(inputLines.pop(0), width)
+ outputLines.extend(wrappedLines or [''])
+ else:
+ outputLines.append(inputLines.pop(0)[:width])
+ if len(outputLines) >= height:
+ break
+ for n, L in enumerate(outputLines[:height]):
+ terminal.cursorPosition(0, n)
+ terminal.write(L)
+
+class Viewport(Widget):
+ _xOffset = 0
+ _yOffset = 0
+
+ def xOffset():
+ def get(self):
+ return self._xOffset
+ def set(self, value):
+ if self._xOffset != value:
+ self._xOffset = value
+ self.repaint()
+ return get, set
+ xOffset = property(*xOffset())
+
+ def yOffset():
+ def get(self):
+ return self._yOffset
+ def set(self, value):
+ if self._yOffset != value:
+ self._yOffset = value
+ self.repaint()
+ return get, set
+ yOffset = property(*yOffset())
+
+ _width = 160
+ _height = 24
+
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self.containee = containee
+ self.containee.parent = self
+
+ self._buf = helper.TerminalBuffer()
+ self._buf.width = self._width
+ self._buf.height = self._height
+ self._buf.connectionMade()
+
+ def filthy(self):
+ self.containee.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ self.containee.draw(self._width, self._height, self._buf)
+
+ # XXX /Lame/
+ for y, line in enumerate(self._buf.lines[self._yOffset:self._yOffset + height]):
+ terminal.cursorPosition(0, y)
+ n = 0
+ for n, (ch, attr) in enumerate(line[self._xOffset:self._xOffset + width]):
+ if ch is self._buf.void:
+ ch = ' '
+ terminal.write(ch)
+ if n < width:
+ terminal.write(' ' * (width - n - 1))
+
+
+class _Scrollbar(Widget):
+ def __init__(self, onScroll):
+ Widget.__init__(self)
+ self.onScroll = onScroll
+ self.percent = 0.0
+
+ def smaller(self):
+ self.percent = min(1.0, max(0.0, self.onScroll(-1)))
+ self.repaint()
+
+ def bigger(self):
+ self.percent = min(1.0, max(0.0, self.onScroll(+1)))
+ self.repaint()
+
+
+class HorizontalScrollbar(_Scrollbar):
+ def sizeHint(self):
+ return (None, 1)
+
+ def func_LEFT_ARROW(self, modifier):
+ self.smaller()
+
+ def func_RIGHT_ARROW(self, modifier):
+ self.bigger()
+
+ _left = u'\N{BLACK LEFT-POINTING TRIANGLE}'
+ _right = u'\N{BLACK RIGHT-POINTING TRIANGLE}'
+ _bar = u'\N{LIGHT SHADE}'
+ _slider = u'\N{DARK SHADE}'
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ n = width - 3
+ before = int(n * self.percent)
+ after = n - before
+ me = self._left + (self._bar * before) + self._slider + (self._bar * after) + self._right
+ terminal.write(me.encode('utf-8'))
+
+
+class VerticalScrollbar(_Scrollbar):
+ def sizeHint(self):
+ return (1, None)
+
+ def func_UP_ARROW(self, modifier):
+ self.smaller()
+
+ def func_DOWN_ARROW(self, modifier):
+ self.bigger()
+
+ _up = u'\N{BLACK UP-POINTING TRIANGLE}'
+ _down = u'\N{BLACK DOWN-POINTING TRIANGLE}'
+ _bar = u'\N{LIGHT SHADE}'
+ _slider = u'\N{DARK SHADE}'
+ def render(self, width, height, terminal):
+ terminal.cursorPosition(0, 0)
+ knob = int(self.percent * (height - 2))
+ terminal.write(self._up.encode('utf-8'))
+ for i in xrange(1, height - 1):
+ terminal.cursorPosition(0, i)
+ if i != (knob + 1):
+ terminal.write(self._bar.encode('utf-8'))
+ else:
+ terminal.write(self._slider.encode('utf-8'))
+ terminal.cursorPosition(0, height - 1)
+ terminal.write(self._down.encode('utf-8'))
+
+
+class ScrolledArea(Widget):
+ """
+ A L{ScrolledArea} contains another widget wrapped in a viewport and
+ vertical and horizontal scrollbars for moving the viewport around.
+ """
+ def __init__(self, containee):
+ Widget.__init__(self)
+ self._viewport = Viewport(containee)
+ self._horiz = HorizontalScrollbar(self._horizScroll)
+ self._vert = VerticalScrollbar(self._vertScroll)
+
+ for w in self._viewport, self._horiz, self._vert:
+ w.parent = self
+
+ def _horizScroll(self, n):
+ self._viewport.xOffset += n
+ self._viewport.xOffset = max(0, self._viewport.xOffset)
+ return self._viewport.xOffset / 25.0
+
+ def _vertScroll(self, n):
+ self._viewport.yOffset += n
+ self._viewport.yOffset = max(0, self._viewport.yOffset)
+ return self._viewport.yOffset / 25.0
+
+ def func_UP_ARROW(self, modifier):
+ self._vert.smaller()
+
+ def func_DOWN_ARROW(self, modifier):
+ self._vert.bigger()
+
+ def func_LEFT_ARROW(self, modifier):
+ self._horiz.smaller()
+
+ def func_RIGHT_ARROW(self, modifier):
+ self._horiz.bigger()
+
+ def filthy(self):
+ self._viewport.filthy()
+ self._horiz.filthy()
+ self._vert.filthy()
+ Widget.filthy(self)
+
+ def render(self, width, height, terminal):
+ wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
+ self._viewport.draw(width - 2, height - 2, wrapper)
+ if self.focused:
+ terminal.write('\x1b[31m')
+ horizontalLine(terminal, 0, 1, width - 1)
+ verticalLine(terminal, 0, 1, height - 1)
+ self._vert.draw(1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0))
+ self._horiz.draw(width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1))
+ terminal.write('\x1b[0m')
+
+def cursor(terminal, ch):
+ terminal.saveCursor()
+ terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
+ terminal.write(ch)
+ terminal.restoreCursor()
+ terminal.cursorForward()
+
+class Selection(Widget):
+ # Index into the sequence
+ focusedIndex = 0
+
+ # Offset into the displayed subset of the sequence
+ renderOffset = 0
+
+ def __init__(self, sequence, onSelect, minVisible=None):
+ Widget.__init__(self)
+ self.sequence = sequence
+ self.onSelect = onSelect
+ self.minVisible = minVisible
+ if minVisible is not None:
+ self._width = max(map(len, self.sequence))
+
+ def sizeHint(self):
+ if self.minVisible is not None:
+ return self._width, self.minVisible
+
+ def func_UP_ARROW(self, modifier):
+ if self.focusedIndex > 0:
+ self.focusedIndex -= 1
+ if self.renderOffset > 0:
+ self.renderOffset -= 1
+ self.repaint()
+
+ def func_PGUP(self, modifier):
+ if self.renderOffset != 0:
+ self.focusedIndex -= self.renderOffset
+ self.renderOffset = 0
+ else:
+ self.focusedIndex = max(0, self.focusedIndex - self.height)
+ self.repaint()
+
+ def func_DOWN_ARROW(self, modifier):
+ if self.focusedIndex < len(self.sequence) - 1:
+ self.focusedIndex += 1
+ if self.renderOffset < self.height - 1:
+ self.renderOffset += 1
+ self.repaint()
+
+
+ def func_PGDN(self, modifier):
+ if self.renderOffset != self.height - 1:
+ change = self.height - self.renderOffset - 1
+ if change + self.focusedIndex >= len(self.sequence):
+ change = len(self.sequence) - self.focusedIndex - 1
+ self.focusedIndex += change
+ self.renderOffset = self.height - 1
+ else:
+ self.focusedIndex = min(len(self.sequence) - 1, self.focusedIndex + self.height)
+ self.repaint()
+
+ def characterReceived(self, keyID, modifier):
+ if keyID == '\r':
+ self.onSelect(self.sequence[self.focusedIndex])
+
+ def render(self, width, height, terminal):
+ self.height = height
+ start = self.focusedIndex - self.renderOffset
+ if start > len(self.sequence) - height:
+ start = max(0, len(self.sequence) - height)
+
+ elements = self.sequence[start:start+height]
+
+ for n, ele in enumerate(elements):
+ terminal.cursorPosition(0, n)
+ if n == self.renderOffset:
+ terminal.saveCursor()
+ if self.focused:
+ modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
+ else:
+ modes = str(insults.REVERSE_VIDEO),
+ terminal.selectGraphicRendition(*modes)
+ text = ele[:width]
+ terminal.write(text + (' ' * (width - len(text))))
+ if n == self.renderOffset:
+ terminal.restoreCursor()
diff --git a/twisted/conch/interfaces.py b/twisted/conch/interfaces.py
new file mode 100644
index 0000000..d42811a
--- /dev/null
+++ b/twisted/conch/interfaces.py
@@ -0,0 +1,402 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module contains interfaces defined for the L{twisted.conch} package.
+"""
+
+from zope.interface import Interface, Attribute
+
+class IConchUser(Interface):
+ """
+ A user who has been authenticated to Cred through Conch. This is
+ the interface between the SSH connection and the user.
+ """
+
+ conn = Attribute('The SSHConnection object for this user.')
+
+ def lookupChannel(channelType, windowSize, maxPacket, data):
+ """
+ The other side requested a channel of some sort.
+ channelType is the type of channel being requested,
+ windowSize is the initial size of the remote window,
+ maxPacket is the largest packet we should send,
+ data is any other packet data (often nothing).
+
+ We return a subclass of L{SSHChannel<ssh.channel.SSHChannel>}. If
+ an appropriate channel can not be found, an exception will be
+ raised. If a L{ConchError<error.ConchError>} is raised, the .value
+ will be the message, and the .data will be the error code.
+
+ @type channelType: C{str}
+ @type windowSize: C{int}
+ @type maxPacket: C{int}
+ @type data: C{str}
+ @rtype: subclass of L{SSHChannel}/C{tuple}
+ """
+
+ def lookupSubsystem(subsystem, data):
+ """
+ The other side requested a subsystem.
+ subsystem is the name of the subsystem being requested.
+ data is any other packet data (often nothing).
+
+ We return a L{Protocol}.
+ """
+
+ def gotGlobalRequest(requestType, data):
+ """
+ A global request was sent from the other side.
+
+ By default, this dispatches to a method 'channel_channelType' with any
+ non-alphanumerics in the channelType replace with _'s. If it cannot
+ find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
+ The method is called with arguments of windowSize, maxPacket, data.
+ """
+
+class ISession(Interface):
+
+ def getPty(term, windowSize, modes):
+ """
+ Get a psuedo-terminal for use by a shell or command.
+
+ If a psuedo-terminal is not available, or the request otherwise
+ fails, raise an exception.
+ """
+
+ def openShell(proto):
+ """
+ Open a shell and connect it to proto.
+
+ @param proto: a L{ProcessProtocol} instance.
+ """
+
+ def execCommand(proto, command):
+ """
+ Execute a command.
+
+ @param proto: a L{ProcessProtocol} instance.
+ """
+
+ def windowChanged(newWindowSize):
+ """
+ Called when the size of the remote screen has changed.
+ """
+
+ def eofReceived():
+ """
+ Called when the other side has indicated no more data will be sent.
+ """
+
+ def closed():
+ """
+ Called when the session is closed.
+ """
+
+
+class ISFTPServer(Interface):
+ """
+ The only attribute of this class is "avatar". It is the avatar
+ returned by the Realm that we are authenticated with, and
+ represents the logged-in user. Each method should check to verify
+ that the user has permission for their actions.
+ """
+
+ def gotVersion(otherVersion, extData):
+ """
+ Called when the client sends their version info.
+
+ otherVersion is an integer representing the version of the SFTP
+ protocol they are claiming.
+ extData is a dictionary of extended_name : extended_data items.
+ These items are sent by the client to indicate additional features.
+
+ This method should return a dictionary of extended_name : extended_data
+ items. These items are the additional features (if any) supported
+ by the server.
+ """
+ return {}
+
+ def openFile(filename, flags, attrs):
+ """
+ Called when the clients asks to open a file.
+
+ @param filename: a string representing the file to open.
+
+ @param flags: an integer of the flags to open the file with, ORed together.
+ The flags and their values are listed at the bottom of this file.
+
+ @param attrs: a list of attributes to open the file with. It is a
+ dictionary, consisting of 0 or more keys. The possible keys are::
+
+ size: the size of the file in bytes
+ uid: the user ID of the file as an integer
+ gid: the group ID of the file as an integer
+ permissions: the permissions of the file with as an integer.
+ the bit representation of this field is defined by POSIX.
+ atime: the access time of the file as seconds since the epoch.
+ mtime: the modification time of the file as seconds since the epoch.
+ ext_*: extended attributes. The server is not required to
+ understand this, but it may.
+
+ NOTE: there is no way to indicate text or binary files. it is up
+ to the SFTP client to deal with this.
+
+ This method returns an object that meets the ISFTPFile interface.
+ Alternatively, it can return a L{Deferred} that will be called back
+ with the object.
+ """
+
+ def removeFile(filename):
+ """
+ Remove the given file.
+
+ This method returns when the remove succeeds, or a Deferred that is
+ called back when it succeeds.
+
+ @param filename: the name of the file as a string.
+ """
+
+ def renameFile(oldpath, newpath):
+ """
+ Rename the given file.
+
+ This method returns when the rename succeeds, or a L{Deferred} that is
+ called back when it succeeds. If the rename fails, C{renameFile} will
+ raise an implementation-dependent exception.
+
+ @param oldpath: the current location of the file.
+ @param newpath: the new file name.
+ """
+
+ def makeDirectory(path, attrs):
+ """
+ Make a directory.
+
+ This method returns when the directory is created, or a Deferred that
+ is called back when it is created.
+
+ @param path: the name of the directory to create as a string.
+ @param attrs: a dictionary of attributes to create the directory with.
+ Its meaning is the same as the attrs in the L{openFile} method.
+ """
+
+ def removeDirectory(path):
+ """
+ Remove a directory (non-recursively)
+
+ It is an error to remove a directory that has files or directories in
+ it.
+
+ This method returns when the directory is removed, or a Deferred that
+ is called back when it is removed.
+
+ @param path: the directory to remove.
+ """
+
+ def openDirectory(path):
+ """
+ Open a directory for scanning.
+
+ This method returns an iterable object that has a close() method,
+ or a Deferred that is called back with same.
+
+ The close() method is called when the client is finished reading
+ from the directory. At this point, the iterable will no longer
+ be used.
+
+ The iterable should return triples of the form (filename,
+ longname, attrs) or Deferreds that return the same. The
+ sequence must support __getitem__, but otherwise may be any
+ 'sequence-like' object.
+
+ filename is the name of the file relative to the directory.
+ logname is an expanded format of the filename. The recommended format
+ is:
+ -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
+ 1234567890 123 12345678 12345678 12345678 123456789012
+
+ The first line is sample output, the second is the length of the field.
+ The fields are: permissions, link count, user owner, group owner,
+ size in bytes, modification time.
+
+ attrs is a dictionary in the format of the attrs argument to openFile.
+
+ @param path: the directory to open.
+ """
+
+ def getAttrs(path, followLinks):
+ """
+ Return the attributes for the given path.
+
+ This method returns a dictionary in the same format as the attrs
+ argument to openFile or a Deferred that is called back with same.
+
+ @param path: the path to return attributes for as a string.
+ @param followLinks: a boolean. If it is True, follow symbolic links
+ and return attributes for the real path at the base. If it is False,
+ return attributes for the specified path.
+ """
+
+ def setAttrs(path, attrs):
+ """
+ Set the attributes for the path.
+
+ This method returns when the attributes are set or a Deferred that is
+ called back when they are.
+
+ @param path: the path to set attributes for as a string.
+ @param attrs: a dictionary in the same format as the attrs argument to
+ L{openFile}.
+ """
+
+ def readLink(path):
+ """
+ Find the root of a set of symbolic links.
+
+ This method returns the target of the link, or a Deferred that
+ returns the same.
+
+ @param path: the path of the symlink to read.
+ """
+
+ def makeLink(linkPath, targetPath):
+ """
+ Create a symbolic link.
+
+ This method returns when the link is made, or a Deferred that
+ returns the same.
+
+ @param linkPath: the pathname of the symlink as a string.
+ @param targetPath: the path of the target of the link as a string.
+ """
+
+ def realPath(path):
+ """
+ Convert any path to an absolute path.
+
+ This method returns the absolute path as a string, or a Deferred
+ that returns the same.
+
+ @param path: the path to convert as a string.
+ """
+
+ def extendedRequest(extendedName, extendedData):
+ """
+ This is the extension mechanism for SFTP. The other side can send us
+ arbitrary requests.
+
+ If we don't implement the request given by extendedName, raise
+ NotImplementedError.
+
+ The return value is a string, or a Deferred that will be called
+ back with a string.
+
+ @param extendedName: the name of the request as a string.
+ @param extendedData: the data the other side sent with the request,
+ as a string.
+ """
+
+
+
+class IKnownHostEntry(Interface):
+ """
+ A L{IKnownHostEntry} is an entry in an OpenSSH-formatted C{known_hosts}
+ file.
+
+ @since: 8.2
+ """
+
+ def matchesKey(key):
+ """
+ Return True if this entry matches the given Key object, False
+ otherwise.
+
+ @param key: The key object to match against.
+ @type key: L{twisted.conch.ssh.Key}
+ """
+
+
+ def matchesHost(hostname):
+ """
+ Return True if this entry matches the given hostname, False otherwise.
+
+ Note that this does no name resolution; if you want to match an IP
+ address, you have to resolve it yourself, and pass it in as a dotted
+ quad string.
+
+ @param key: The hostname to match against.
+ @type key: L{str}
+ """
+
+
+ def toString():
+ """
+ @return: a serialized string representation of this entry, suitable for
+ inclusion in a known_hosts file. (Newline not included.)
+
+ @rtype: L{str}
+ """
+
+
+
+class ISFTPFile(Interface):
+ """
+ This represents an open file on the server. An object adhering to this
+ interface should be returned from L{openFile}().
+ """
+
+ def close():
+ """
+ Close the file.
+
+ This method returns nothing if the close succeeds immediately, or a
+ Deferred that is called back when the close succeeds.
+ """
+
+ def readChunk(offset, length):
+ """
+ Read from the file.
+
+ If EOF is reached before any data is read, raise EOFError.
+
+ This method returns the data as a string, or a Deferred that is
+ called back with same.
+
+ @param offset: an integer that is the index to start from in the file.
+ @param length: the maximum length of data to return. The actual amount
+ returned may less than this. For normal disk files, however,
+ this should read the requested number (up to the end of the file).
+ """
+
+ def writeChunk(offset, data):
+ """
+ Write to the file.
+
+ This method returns when the write completes, or a Deferred that is
+ called when it completes.
+
+ @param offset: an integer that is the index to start from in the file.
+ @param data: a string that is the data to write.
+ """
+
+ def getAttrs():
+ """
+ Return the attributes for the file.
+
+ This method returns a dictionary in the same format as the attrs
+ argument to L{openFile} or a L{Deferred} that is called back with same.
+ """
+
+ def setAttrs(attrs):
+ """
+ Set the attributes for the file.
+
+ This method returns when the attributes are set or a Deferred that is
+ called back when they are.
+
+ @param attrs: a dictionary in the same format as the attrs argument to
+ L{openFile}.
+ """
+
+
diff --git a/twisted/conch/ls.py b/twisted/conch/ls.py
new file mode 100644
index 0000000..ab44f85
--- /dev/null
+++ b/twisted/conch/ls.py
@@ -0,0 +1,75 @@
+# -*- test-case-name: twisted.conch.test.test_cftp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import array
+import stat
+
+from time import time, strftime, localtime
+
+# locale-independent month names to use instead of strftime's
+_MONTH_NAMES = dict(zip(
+ range(1, 13),
+ "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()))
+
+
+def lsLine(name, s):
+ """
+ Build an 'ls' line for a file ('file' in its generic sense, it
+ can be of any type).
+ """
+ mode = s.st_mode
+ perms = array.array('c', '-'*10)
+ ft = stat.S_IFMT(mode)
+ if stat.S_ISDIR(ft): perms[0] = 'd'
+ elif stat.S_ISCHR(ft): perms[0] = 'c'
+ elif stat.S_ISBLK(ft): perms[0] = 'b'
+ elif stat.S_ISREG(ft): perms[0] = '-'
+ elif stat.S_ISFIFO(ft): perms[0] = 'f'
+ elif stat.S_ISLNK(ft): perms[0] = 'l'
+ elif stat.S_ISSOCK(ft): perms[0] = 's'
+ else: perms[0] = '!'
+ # user
+ if mode&stat.S_IRUSR:perms[1] = 'r'
+ if mode&stat.S_IWUSR:perms[2] = 'w'
+ if mode&stat.S_IXUSR:perms[3] = 'x'
+ # group
+ if mode&stat.S_IRGRP:perms[4] = 'r'
+ if mode&stat.S_IWGRP:perms[5] = 'w'
+ if mode&stat.S_IXGRP:perms[6] = 'x'
+ # other
+ if mode&stat.S_IROTH:perms[7] = 'r'
+ if mode&stat.S_IWOTH:perms[8] = 'w'
+ if mode&stat.S_IXOTH:perms[9] = 'x'
+ # suid/sgid
+ if mode&stat.S_ISUID:
+ if perms[3] == 'x': perms[3] = 's'
+ else: perms[3] = 'S'
+ if mode&stat.S_ISGID:
+ if perms[6] == 'x': perms[6] = 's'
+ else: perms[6] = 'S'
+
+ lsresult = [
+ perms.tostring(),
+ str(s.st_nlink).rjust(5),
+ ' ',
+ str(s.st_uid).ljust(9),
+ str(s.st_gid).ljust(9),
+ str(s.st_size).rjust(8),
+ ' ',
+ ]
+
+ # need to specify the month manually, as strftime depends on locale
+ ttup = localtime(s.st_mtime)
+ sixmonths = 60 * 60 * 24 * 7 * 26
+ if s.st_mtime + sixmonths < time(): # last edited more than 6mo ago
+ strtime = strftime("%%s %d %Y ", ttup)
+ else:
+ strtime = strftime("%%s %d %H:%M ", ttup)
+ lsresult.append(strtime % (_MONTH_NAMES[ttup[1]],))
+
+ lsresult.append(name)
+ return ''.join(lsresult)
+
+
+__all__ = ['lsLine']
diff --git a/twisted/conch/manhole.py b/twisted/conch/manhole.py
new file mode 100644
index 0000000..dee6a02
--- /dev/null
+++ b/twisted/conch/manhole.py
@@ -0,0 +1,340 @@
+# -*- test-case-name: twisted.conch.test.test_manhole -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Line-input oriented interactive interpreter loop.
+
+Provides classes for handling Python source input and arbitrary output
+interactively from a Twisted application. Also included is syntax coloring
+code with support for VT102 terminals, control code handling (^C, ^D, ^Q),
+and reasonable handling of Deferreds.
+
+@author: Jp Calderone
+"""
+
+import code, sys, StringIO, tokenize
+
+from twisted.conch import recvline
+
+from twisted.internet import defer
+from twisted.python.htmlizer import TokenPrinter
+
+class FileWrapper:
+ """Minimal write-file-like object.
+
+ Writes are translated into addOutput calls on an object passed to
+ __init__. Newlines are also converted from network to local style.
+ """
+
+ softspace = 0
+ state = 'normal'
+
+ def __init__(self, o):
+ self.o = o
+
+ def flush(self):
+ pass
+
+ def write(self, data):
+ self.o.addOutput(data.replace('\r\n', '\n'))
+
+ def writelines(self, lines):
+ self.write(''.join(lines))
+
+class ManholeInterpreter(code.InteractiveInterpreter):
+ """Interactive Interpreter with special output and Deferred support.
+
+ Aside from the features provided by L{code.InteractiveInterpreter}, this
+ class captures sys.stdout output and redirects it to the appropriate
+ location (the Manhole protocol instance). It also treats Deferreds
+ which reach the top-level specially: each is formatted to the user with
+ a unique identifier and a new callback and errback added to it, each of
+ which will format the unique identifier and the result with which the
+ Deferred fires and then pass it on to the next participant in the
+ callback chain.
+ """
+
+ numDeferreds = 0
+ def __init__(self, handler, locals=None, filename="<console>"):
+ code.InteractiveInterpreter.__init__(self, locals)
+ self._pendingDeferreds = {}
+ self.handler = handler
+ self.filename = filename
+ self.resetBuffer()
+
+ def resetBuffer(self):
+ """Reset the input buffer."""
+ self.buffer = []
+
+ def push(self, line):
+ """Push a line to the interpreter.
+
+ The line should not have a trailing newline; it may have
+ internal newlines. The line is appended to a buffer and the
+ interpreter's runsource() method is called with the
+ concatenated contents of the buffer as source. If this
+ indicates that the command was executed or invalid, the buffer
+ is reset; otherwise, the command is incomplete, and the buffer
+ is left as it was after the line was appended. The return
+ value is 1 if more input is required, 0 if the line was dealt
+ with in some way (this is the same as runsource()).
+
+ """
+ self.buffer.append(line)
+ source = "\n".join(self.buffer)
+ more = self.runsource(source, self.filename)
+ if not more:
+ self.resetBuffer()
+ return more
+
+ def runcode(self, *a, **kw):
+ orighook, sys.displayhook = sys.displayhook, self.displayhook
+ try:
+ origout, sys.stdout = sys.stdout, FileWrapper(self.handler)
+ try:
+ code.InteractiveInterpreter.runcode(self, *a, **kw)
+ finally:
+ sys.stdout = origout
+ finally:
+ sys.displayhook = orighook
+
+ def displayhook(self, obj):
+ self.locals['_'] = obj
+ if isinstance(obj, defer.Deferred):
+ # XXX Ick, where is my "hasFired()" interface?
+ if hasattr(obj, "result"):
+ self.write(repr(obj))
+ elif id(obj) in self._pendingDeferreds:
+ self.write("<Deferred #%d>" % (self._pendingDeferreds[id(obj)][0],))
+ else:
+ d = self._pendingDeferreds
+ k = self.numDeferreds
+ d[id(obj)] = (k, obj)
+ self.numDeferreds += 1
+ obj.addCallbacks(self._cbDisplayDeferred, self._ebDisplayDeferred,
+ callbackArgs=(k, obj), errbackArgs=(k, obj))
+ self.write("<Deferred #%d>" % (k,))
+ elif obj is not None:
+ self.write(repr(obj))
+
+ def _cbDisplayDeferred(self, result, k, obj):
+ self.write("Deferred #%d called back: %r" % (k, result), True)
+ del self._pendingDeferreds[id(obj)]
+ return result
+
+ def _ebDisplayDeferred(self, failure, k, obj):
+ self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
+ del self._pendingDeferreds[id(obj)]
+ return failure
+
+ def write(self, data, async=False):
+ self.handler.addOutput(data, async)
+
+CTRL_C = '\x03'
+CTRL_D = '\x04'
+CTRL_BACKSLASH = '\x1c'
+CTRL_L = '\x0c'
+CTRL_A = '\x01'
+CTRL_E = '\x05'
+
+class Manhole(recvline.HistoricRecvLine):
+ """Mediator between a fancy line source and an interactive interpreter.
+
+ This accepts lines from its transport and passes them on to a
+ L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled
+ with something approximating their normal terminal-mode behavior. It
+ can optionally be constructed with a dict which will be used as the
+ local namespace for any code executed.
+ """
+
+ namespace = None
+
+ def __init__(self, namespace=None):
+ recvline.HistoricRecvLine.__init__(self)
+ if namespace is not None:
+ self.namespace = namespace.copy()
+
+ def connectionMade(self):
+ recvline.HistoricRecvLine.connectionMade(self)
+ self.interpreter = ManholeInterpreter(self, self.namespace)
+ self.keyHandlers[CTRL_C] = self.handle_INT
+ self.keyHandlers[CTRL_D] = self.handle_EOF
+ self.keyHandlers[CTRL_L] = self.handle_FF
+ self.keyHandlers[CTRL_A] = self.handle_HOME
+ self.keyHandlers[CTRL_E] = self.handle_END
+ self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
+
+
+ def handle_INT(self):
+ """
+ Handle ^C as an interrupt keystroke by resetting the current input
+ variables to their initial state.
+ """
+ self.pn = 0
+ self.lineBuffer = []
+ self.lineBufferIndex = 0
+ self.interpreter.resetBuffer()
+
+ self.terminal.nextLine()
+ self.terminal.write("KeyboardInterrupt")
+ self.terminal.nextLine()
+ self.terminal.write(self.ps[self.pn])
+
+
+ def handle_EOF(self):
+ if self.lineBuffer:
+ self.terminal.write('\a')
+ else:
+ self.handle_QUIT()
+
+
+ def handle_FF(self):
+ """
+ Handle a 'form feed' byte - generally used to request a screen
+ refresh/redraw.
+ """
+ self.terminal.eraseDisplay()
+ self.terminal.cursorHome()
+ self.drawInputLine()
+
+
+ def handle_QUIT(self):
+ self.terminal.loseConnection()
+
+
+ def _needsNewline(self):
+ w = self.terminal.lastWrite
+ return not w.endswith('\n') and not w.endswith('\x1bE')
+
+ def addOutput(self, bytes, async=False):
+ if async:
+ self.terminal.eraseLine()
+ self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
+
+ self.terminal.write(bytes)
+
+ if async:
+ if self._needsNewline():
+ self.terminal.nextLine()
+
+ self.terminal.write(self.ps[self.pn])
+
+ if self.lineBuffer:
+ oldBuffer = self.lineBuffer
+ self.lineBuffer = []
+ self.lineBufferIndex = 0
+
+ self._deliverBuffer(oldBuffer)
+
+ def lineReceived(self, line):
+ more = self.interpreter.push(line)
+ self.pn = bool(more)
+ if self._needsNewline():
+ self.terminal.nextLine()
+ self.terminal.write(self.ps[self.pn])
+
+class VT102Writer:
+ """Colorizer for Python tokens.
+
+ A series of tokens are written to instances of this object. Each is
+ colored in a particular way. The final line of the result of this is
+ generally added to the output.
+ """
+
+ typeToColor = {
+ 'identifier': '\x1b[31m',
+ 'keyword': '\x1b[32m',
+ 'parameter': '\x1b[33m',
+ 'variable': '\x1b[1;33m',
+ 'string': '\x1b[35m',
+ 'number': '\x1b[36m',
+ 'op': '\x1b[37m'}
+
+ normalColor = '\x1b[0m'
+
+ def __init__(self):
+ self.written = []
+
+ def color(self, type):
+ r = self.typeToColor.get(type, '')
+ return r
+
+ def write(self, token, type=None):
+ if token and token != '\r':
+ c = self.color(type)
+ if c:
+ self.written.append(c)
+ self.written.append(token)
+ if c:
+ self.written.append(self.normalColor)
+
+ def __str__(self):
+ s = ''.join(self.written)
+ return s.strip('\n').splitlines()[-1]
+
+def lastColorizedLine(source):
+ """Tokenize and colorize the given Python source.
+
+ Returns a VT102-format colorized version of the last line of C{source}.
+ """
+ w = VT102Writer()
+ p = TokenPrinter(w.write).printtoken
+ s = StringIO.StringIO(source)
+
+ tokenize.tokenize(s.readline, p)
+
+ return str(w)
+
+class ColoredManhole(Manhole):
+ """A REPL which syntax colors input as users type it.
+ """
+
+ def getSource(self):
+ """Return a string containing the currently entered source.
+
+ This is only the code which will be considered for execution
+ next.
+ """
+ return ('\n'.join(self.interpreter.buffer) +
+ '\n' +
+ ''.join(self.lineBuffer))
+
+
+ def characterReceived(self, ch, moreCharactersComing):
+ if self.mode == 'insert':
+ self.lineBuffer.insert(self.lineBufferIndex, ch)
+ else:
+ self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
+ self.lineBufferIndex += 1
+
+ if moreCharactersComing:
+ # Skip it all, we'll get called with another character in
+ # like 2 femtoseconds.
+ return
+
+ if ch == ' ':
+ # Don't bother to try to color whitespace
+ self.terminal.write(ch)
+ return
+
+ source = self.getSource()
+
+ # Try to write some junk
+ try:
+ coloredLine = lastColorizedLine(source)
+ except tokenize.TokenError:
+ # We couldn't do it. Strange. Oh well, just add the character.
+ self.terminal.write(ch)
+ else:
+ # Success! Clear the source on this line.
+ self.terminal.eraseLine()
+ self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]) - 1)
+
+ # And write a new, colorized one.
+ self.terminal.write(self.ps[self.pn] + coloredLine)
+
+ # And move the cursor to where it belongs
+ n = len(self.lineBuffer) - self.lineBufferIndex
+ if n:
+ self.terminal.cursorBackward(n)
diff --git a/twisted/conch/manhole_ssh.py b/twisted/conch/manhole_ssh.py
new file mode 100644
index 0000000..a2297ef
--- /dev/null
+++ b/twisted/conch/manhole_ssh.py
@@ -0,0 +1,146 @@
+# -*- test-case-name: twisted.conch.test.test_manhole -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+insults/SSH integration support.
+
+@author: Jp Calderone
+"""
+
+from zope.interface import implements
+
+from twisted.conch import avatar, interfaces as iconch, error as econch
+from twisted.conch.ssh import factory, keys, session
+from twisted.cred import credentials, checkers, portal
+from twisted.python import components
+
+from twisted.conch.insults import insults
+
+class _Glue:
+ """A feeble class for making one attribute look like another.
+
+ This should be replaced with a real class at some point, probably.
+ Try not to write new code that uses it.
+ """
+ def __init__(self, **kw):
+ self.__dict__.update(kw)
+
+ def __getattr__(self, name):
+ raise AttributeError(self.name, "has no attribute", name)
+
+class TerminalSessionTransport:
+ def __init__(self, proto, chainedProtocol, avatar, width, height):
+ self.proto = proto
+ self.avatar = avatar
+ self.chainedProtocol = chainedProtocol
+
+ session = self.proto.session
+
+ self.proto.makeConnection(
+ _Glue(write=self.chainedProtocol.dataReceived,
+ loseConnection=lambda: avatar.conn.sendClose(session),
+ name="SSH Proto Transport"))
+
+ def loseConnection():
+ self.proto.loseConnection()
+
+ self.chainedProtocol.makeConnection(
+ _Glue(write=self.proto.write,
+ loseConnection=loseConnection,
+ name="Chained Proto Transport"))
+
+ # XXX TODO
+ # chainedProtocol is supposed to be an ITerminalTransport,
+ # maybe. That means perhaps its terminalProtocol attribute is
+ # an ITerminalProtocol, it could be. So calling terminalSize
+ # on that should do the right thing But it'd be nice to clean
+ # this bit up.
+ self.chainedProtocol.terminalProtocol.terminalSize(width, height)
+
+class TerminalSession(components.Adapter):
+ implements(iconch.ISession)
+
+ transportFactory = TerminalSessionTransport
+ chainedProtocolFactory = insults.ServerProtocol
+
+ def getPty(self, term, windowSize, attrs):
+ self.height, self.width = windowSize[:2]
+
+ def openShell(self, proto):
+ self.transportFactory(
+ proto, self.chainedProtocolFactory(),
+ iconch.IConchUser(self.original),
+ self.width, self.height)
+
+ def execCommand(self, proto, cmd):
+ raise econch.ConchError("Cannot execute commands")
+
+ def closed(self):
+ pass
+
+class TerminalUser(avatar.ConchUser, components.Adapter):
+ def __init__(self, original, avatarId):
+ components.Adapter.__init__(self, original)
+ avatar.ConchUser.__init__(self)
+ self.channelLookup['session'] = session.SSHSession
+
+class TerminalRealm:
+ userFactory = TerminalUser
+ sessionFactory = TerminalSession
+
+ transportFactory = TerminalSessionTransport
+ chainedProtocolFactory = insults.ServerProtocol
+
+ def _getAvatar(self, avatarId):
+ comp = components.Componentized()
+ user = self.userFactory(comp, avatarId)
+ sess = self.sessionFactory(comp)
+
+ sess.transportFactory = self.transportFactory
+ sess.chainedProtocolFactory = self.chainedProtocolFactory
+
+ comp.setComponent(iconch.IConchUser, user)
+ comp.setComponent(iconch.ISession, sess)
+
+ return user
+
+ def __init__(self, transportFactory=None):
+ if transportFactory is not None:
+ self.transportFactory = transportFactory
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ for i in interfaces:
+ if i is iconch.IConchUser:
+ return (iconch.IConchUser,
+ self._getAvatar(avatarId),
+ lambda: None)
+ raise NotImplementedError()
+
+class ConchFactory(factory.SSHFactory):
+ publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV'
+
+ publicKeys = {
+ 'ssh-rsa' : keys.Key.fromString(publicKey)
+ }
+ del publicKey
+
+ privateKey = """-----BEGIN RSA PRIVATE KEY-----
+MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
+4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
+vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
+Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
+xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
+PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
+gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
+DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
+pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
+EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
+-----END RSA PRIVATE KEY-----"""
+ privateKeys = {
+ 'ssh-rsa' : keys.Key.fromString(privateKey)
+ }
+ del privateKey
+
+ def __init__(self, portal):
+ self.portal = portal
diff --git a/twisted/conch/manhole_tap.py b/twisted/conch/manhole_tap.py
new file mode 100644
index 0000000..4df7c83
--- /dev/null
+++ b/twisted/conch/manhole_tap.py
@@ -0,0 +1,124 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+TAP plugin for creating telnet- and ssh-accessible manhole servers.
+
+@author: Jp Calderone
+"""
+
+from zope.interface import implements
+
+from twisted.internet import protocol
+from twisted.application import service, strports
+from twisted.conch.ssh import session
+from twisted.conch import interfaces as iconch
+from twisted.cred import portal, checkers
+from twisted.python import usage
+
+from twisted.conch.insults import insults
+from twisted.conch import manhole, manhole_ssh, telnet
+
+class makeTelnetProtocol:
+ def __init__(self, portal):
+ self.portal = portal
+
+ def __call__(self):
+ auth = telnet.AuthenticatingTelnetProtocol
+ args = (self.portal,)
+ return telnet.TelnetTransport(auth, *args)
+
+class chainedProtocolFactory:
+ def __init__(self, namespace):
+ self.namespace = namespace
+
+ def __call__(self):
+ return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
+
+class _StupidRealm:
+ implements(portal.IRealm)
+
+ def __init__(self, proto, *a, **kw):
+ self.protocolFactory = proto
+ self.protocolArgs = a
+ self.protocolKwArgs = kw
+
+ def requestAvatar(self, avatarId, *interfaces):
+ if telnet.ITelnetProtocol in interfaces:
+ return (telnet.ITelnetProtocol,
+ self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs),
+ lambda: None)
+ raise NotImplementedError()
+
+class Options(usage.Options):
+ optParameters = [
+ ["telnetPort", "t", None, "strports description of the address on which to listen for telnet connections"],
+ ["sshPort", "s", None, "strports description of the address on which to listen for ssh connections"],
+ ["passwd", "p", "/etc/passwd", "name of a passwd(5)-format username/password file"]]
+
+ def __init__(self):
+ usage.Options.__init__(self)
+ self['namespace'] = None
+
+ def postOptions(self):
+ if self['telnetPort'] is None and self['sshPort'] is None:
+ raise usage.UsageError("At least one of --telnetPort and --sshPort must be specified")
+
+def makeService(options):
+ """Create a manhole server service.
+
+ @type options: C{dict}
+ @param options: A mapping describing the configuration of
+ the desired service. Recognized key/value pairs are::
+
+ "telnetPort": strports description of the address on which
+ to listen for telnet connections. If None,
+ no telnet service will be started.
+
+ "sshPort": strports description of the address on which to
+ listen for ssh connections. If None, no ssh
+ service will be started.
+
+ "namespace": dictionary containing desired initial locals
+ for manhole connections. If None, an empty
+ dictionary will be used.
+
+ "passwd": Name of a passwd(5)-format username/password file.
+
+ @rtype: L{twisted.application.service.IService}
+ @return: A manhole service.
+ """
+
+ svc = service.MultiService()
+
+ namespace = options['namespace']
+ if namespace is None:
+ namespace = {}
+
+ checker = checkers.FilePasswordDB(options['passwd'])
+
+ if options['telnetPort']:
+ telnetRealm = _StupidRealm(telnet.TelnetBootstrapProtocol,
+ insults.ServerProtocol,
+ manhole.ColoredManhole,
+ namespace)
+
+ telnetPortal = portal.Portal(telnetRealm, [checker])
+
+ telnetFactory = protocol.ServerFactory()
+ telnetFactory.protocol = makeTelnetProtocol(telnetPortal)
+ telnetService = strports.service(options['telnetPort'],
+ telnetFactory)
+ telnetService.setServiceParent(svc)
+
+ if options['sshPort']:
+ sshRealm = manhole_ssh.TerminalRealm()
+ sshRealm.chainedProtocolFactory = chainedProtocolFactory(namespace)
+
+ sshPortal = portal.Portal(sshRealm, [checker])
+ sshFactory = manhole_ssh.ConchFactory(sshPortal)
+ sshService = strports.service(options['sshPort'],
+ sshFactory)
+ sshService.setServiceParent(svc)
+
+ return svc
diff --git a/twisted/conch/mixin.py b/twisted/conch/mixin.py
new file mode 100644
index 0000000..581e2ff
--- /dev/null
+++ b/twisted/conch/mixin.py
@@ -0,0 +1,49 @@
+# -*- test-case-name: twisted.conch.test.test_mixin -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Experimental optimization
+
+This module provides a single mixin class which allows protocols to
+collapse numerous small writes into a single larger one.
+
+@author: Jp Calderone
+"""
+
+from twisted.internet import reactor
+
+class BufferingMixin:
+ """Mixin which adds write buffering.
+ """
+ _delayedWriteCall = None
+ bytes = None
+
+ DELAY = 0.0
+
+ def schedule(self):
+ return reactor.callLater(self.DELAY, self.flush)
+
+ def reschedule(self, token):
+ token.reset(self.DELAY)
+
+ def write(self, bytes):
+ """Buffer some bytes to be written soon.
+
+ Every call to this function delays the real write by C{self.DELAY}
+ seconds. When the delay expires, all collected bytes are written
+ to the underlying transport using L{ITransport.writeSequence}.
+ """
+ if self._delayedWriteCall is None:
+ self.bytes = []
+ self._delayedWriteCall = self.schedule()
+ else:
+ self.reschedule(self._delayedWriteCall)
+ self.bytes.append(bytes)
+
+ def flush(self):
+ """Flush the buffer immediately.
+ """
+ self._delayedWriteCall = None
+ self.transport.writeSequence(self.bytes)
+ self.bytes = None
diff --git a/twisted/conch/openssh_compat/__init__.py b/twisted/conch/openssh_compat/__init__.py
new file mode 100644
index 0000000..69d5927
--- /dev/null
+++ b/twisted/conch/openssh_compat/__init__.py
@@ -0,0 +1,11 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+Support for OpenSSH configuration files.
+
+Maintainer: Paul Swartz
+"""
+
diff --git a/twisted/conch/openssh_compat/factory.py b/twisted/conch/openssh_compat/factory.py
new file mode 100644
index 0000000..f0ad8f7
--- /dev/null
+++ b/twisted/conch/openssh_compat/factory.py
@@ -0,0 +1,73 @@
+# -*- test-case-name: twisted.conch.test.test_openssh_compat -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Factory for reading openssh configuration files: public keys, private keys, and
+moduli file.
+"""
+
+import os, errno
+
+from twisted.python import log
+from twisted.python.util import runAsEffectiveUser
+
+from twisted.conch.ssh import keys, factory, common
+from twisted.conch.openssh_compat import primes
+
+
+
+class OpenSSHFactory(factory.SSHFactory):
+ dataRoot = '/usr/local/etc'
+ moduliRoot = '/usr/local/etc' # for openbsd which puts moduli in a different
+ # directory from keys
+
+
+ def getPublicKeys(self):
+ """
+ Return the server public keys.
+ """
+ ks = {}
+ for filename in os.listdir(self.dataRoot):
+ if filename[:9] == 'ssh_host_' and filename[-8:]=='_key.pub':
+ try:
+ k = keys.Key.fromFile(
+ os.path.join(self.dataRoot, filename))
+ t = common.getNS(k.blob())[0]
+ ks[t] = k
+ except Exception, e:
+ log.msg('bad public key file %s: %s' % (filename, e))
+ return ks
+
+
+ def getPrivateKeys(self):
+ """
+ Return the server private keys.
+ """
+ privateKeys = {}
+ for filename in os.listdir(self.dataRoot):
+ if filename[:9] == 'ssh_host_' and filename[-4:]=='_key':
+ fullPath = os.path.join(self.dataRoot, filename)
+ try:
+ key = keys.Key.fromFile(fullPath)
+ except IOError, e:
+ if e.errno == errno.EACCES:
+ # Not allowed, let's switch to root
+ key = runAsEffectiveUser(0, 0, keys.Key.fromFile, fullPath)
+ keyType = keys.objectType(key.keyObject)
+ privateKeys[keyType] = key
+ else:
+ raise
+ except Exception, e:
+ log.msg('bad private key file %s: %s' % (filename, e))
+ else:
+ keyType = keys.objectType(key.keyObject)
+ privateKeys[keyType] = key
+ return privateKeys
+
+
+ def getPrimes(self):
+ try:
+ return primes.parseModuliFile(self.moduliRoot+'/moduli')
+ except IOError:
+ return None
diff --git a/twisted/conch/openssh_compat/primes.py b/twisted/conch/openssh_compat/primes.py
new file mode 100644
index 0000000..5d939e6
--- /dev/null
+++ b/twisted/conch/openssh_compat/primes.py
@@ -0,0 +1,26 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+Parsing for the moduli file, which contains Diffie-Hellman prime groups.
+
+Maintainer: Paul Swartz
+"""
+
+def parseModuliFile(filename):
+ lines = open(filename).readlines()
+ primes = {}
+ for l in lines:
+ l = l.strip()
+ if not l or l[0]=='#':
+ continue
+ tim, typ, tst, tri, size, gen, mod = l.split()
+ size = int(size) + 1
+ gen = long(gen)
+ mod = long(mod, 16)
+ if not primes.has_key(size):
+ primes[size] = []
+ primes[size].append((gen, mod))
+ return primes
diff --git a/twisted/conch/recvline.py b/twisted/conch/recvline.py
new file mode 100644
index 0000000..6c8416a
--- /dev/null
+++ b/twisted/conch/recvline.py
@@ -0,0 +1,329 @@
+# -*- test-case-name: twisted.conch.test.test_recvline -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Basic line editing support.
+
+@author: Jp Calderone
+"""
+
+import string
+
+from zope.interface import implements
+
+from twisted.conch.insults import insults, helper
+
+from twisted.python import log, reflect
+
+_counters = {}
+class Logging(object):
+ """Wrapper which logs attribute lookups.
+
+ This was useful in debugging something, I guess. I forget what.
+ It can probably be deleted or moved somewhere more appropriate.
+ Nothing special going on here, really.
+ """
+ def __init__(self, original):
+ self.original = original
+ key = reflect.qual(original.__class__)
+ count = _counters.get(key, 0)
+ _counters[key] = count + 1
+ self._logFile = file(key + '-' + str(count), 'w')
+
+ def __str__(self):
+ return str(super(Logging, self).__getattribute__('original'))
+
+ def __repr__(self):
+ return repr(super(Logging, self).__getattribute__('original'))
+
+ def __getattribute__(self, name):
+ original = super(Logging, self).__getattribute__('original')
+ logFile = super(Logging, self).__getattribute__('_logFile')
+ logFile.write(name + '\n')
+ return getattr(original, name)
+
+class TransportSequence(object):
+ """An L{ITerminalTransport} implementation which forwards calls to
+ one or more other L{ITerminalTransport}s.
+
+ This is a cheap way for servers to keep track of the state they
+ expect the client to see, since all terminal manipulations can be
+ send to the real client and to a terminal emulator that lives in
+ the server process.
+ """
+ implements(insults.ITerminalTransport)
+
+ for keyID in ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW',
+ 'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN',
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
+ 'F10', 'F11', 'F12'):
+ exec '%s = object()' % (keyID,)
+
+ TAB = '\t'
+ BACKSPACE = '\x7f'
+
+ def __init__(self, *transports):
+ assert transports, "Cannot construct a TransportSequence with no transports"
+ self.transports = transports
+
+ for method in insults.ITerminalTransport:
+ exec """\
+def %s(self, *a, **kw):
+ for tpt in self.transports:
+ result = tpt.%s(*a, **kw)
+ return result
+""" % (method, method)
+
+class LocalTerminalBufferMixin(object):
+ """A mixin for RecvLine subclasses which records the state of the terminal.
+
+ This is accomplished by performing all L{ITerminalTransport} operations on both
+ the transport passed to makeConnection and an instance of helper.TerminalBuffer.
+
+ @ivar terminalCopy: A L{helper.TerminalBuffer} instance which efforts
+ will be made to keep up to date with the actual terminal
+ associated with this protocol instance.
+ """
+
+ def makeConnection(self, transport):
+ self.terminalCopy = helper.TerminalBuffer()
+ self.terminalCopy.connectionMade()
+ return super(LocalTerminalBufferMixin, self).makeConnection(
+ TransportSequence(transport, self.terminalCopy))
+
+ def __str__(self):
+ return str(self.terminalCopy)
+
+class RecvLine(insults.TerminalProtocol):
+ """L{TerminalProtocol} which adds line editing features.
+
+ Clients will be prompted for lines of input with all the usual
+ features: character echoing, left and right arrow support for
+ moving the cursor to different areas of the line buffer, backspace
+ and delete for removing characters, and insert for toggling
+ between typeover and insert mode. Tabs will be expanded to enough
+ spaces to move the cursor to the next tabstop (every four
+ characters by default). Enter causes the line buffer to be
+ cleared and the line to be passed to the lineReceived() method
+ which, by default, does nothing. Subclasses are responsible for
+ redrawing the input prompt (this will probably change).
+ """
+ width = 80
+ height = 24
+
+ TABSTOP = 4
+
+ ps = ('>>> ', '... ')
+ pn = 0
+ _printableChars = set(string.printable)
+
+ def connectionMade(self):
+ # A list containing the characters making up the current line
+ self.lineBuffer = []
+
+ # A zero-based (wtf else?) index into self.lineBuffer.
+ # Indicates the current cursor position.
+ self.lineBufferIndex = 0
+
+ t = self.terminal
+ # A map of keyIDs to bound instance methods.
+ self.keyHandlers = {
+ t.LEFT_ARROW: self.handle_LEFT,
+ t.RIGHT_ARROW: self.handle_RIGHT,
+ t.TAB: self.handle_TAB,
+
+ # Both of these should not be necessary, but figuring out
+ # which is necessary is a huge hassle.
+ '\r': self.handle_RETURN,
+ '\n': self.handle_RETURN,
+
+ t.BACKSPACE: self.handle_BACKSPACE,
+ t.DELETE: self.handle_DELETE,
+ t.INSERT: self.handle_INSERT,
+ t.HOME: self.handle_HOME,
+ t.END: self.handle_END}
+
+ self.initializeScreen()
+
+ def initializeScreen(self):
+ # Hmm, state sucks. Oh well.
+ # For now we will just take over the whole terminal.
+ self.terminal.reset()
+ self.terminal.write(self.ps[self.pn])
+ # XXX Note: I would prefer to default to starting in insert
+ # mode, however this does not seem to actually work! I do not
+ # know why. This is probably of interest to implementors
+ # subclassing RecvLine.
+
+ # XXX XXX Note: But the unit tests all expect the initial mode
+ # to be insert right now. Fuck, there needs to be a way to
+ # query the current mode or something.
+ # self.setTypeoverMode()
+ self.setInsertMode()
+
+ def currentLineBuffer(self):
+ s = ''.join(self.lineBuffer)
+ return s[:self.lineBufferIndex], s[self.lineBufferIndex:]
+
+ def setInsertMode(self):
+ self.mode = 'insert'
+ self.terminal.setModes([insults.modes.IRM])
+
+ def setTypeoverMode(self):
+ self.mode = 'typeover'
+ self.terminal.resetModes([insults.modes.IRM])
+
+ def drawInputLine(self):
+ """
+ Write a line containing the current input prompt and the current line
+ buffer at the current cursor position.
+ """
+ self.terminal.write(self.ps[self.pn] + ''.join(self.lineBuffer))
+
+ def terminalSize(self, width, height):
+ # XXX - Clear the previous input line, redraw it at the new
+ # cursor position
+ self.terminal.eraseDisplay()
+ self.terminal.cursorHome()
+ self.width = width
+ self.height = height
+ self.drawInputLine()
+
+ def unhandledControlSequence(self, seq):
+ pass
+
+ def keystrokeReceived(self, keyID, modifier):
+ m = self.keyHandlers.get(keyID)
+ if m is not None:
+ m()
+ elif keyID in self._printableChars:
+ self.characterReceived(keyID, False)
+ else:
+ log.msg("Received unhandled keyID: %r" % (keyID,))
+
+ def characterReceived(self, ch, moreCharactersComing):
+ if self.mode == 'insert':
+ self.lineBuffer.insert(self.lineBufferIndex, ch)
+ else:
+ self.lineBuffer[self.lineBufferIndex:self.lineBufferIndex+1] = [ch]
+ self.lineBufferIndex += 1
+ self.terminal.write(ch)
+
+ def handle_TAB(self):
+ n = self.TABSTOP - (len(self.lineBuffer) % self.TABSTOP)
+ self.terminal.cursorForward(n)
+ self.lineBufferIndex += n
+ self.lineBuffer.extend(' ' * n)
+
+ def handle_LEFT(self):
+ if self.lineBufferIndex > 0:
+ self.lineBufferIndex -= 1
+ self.terminal.cursorBackward()
+
+ def handle_RIGHT(self):
+ if self.lineBufferIndex < len(self.lineBuffer):
+ self.lineBufferIndex += 1
+ self.terminal.cursorForward()
+
+ def handle_HOME(self):
+ if self.lineBufferIndex:
+ self.terminal.cursorBackward(self.lineBufferIndex)
+ self.lineBufferIndex = 0
+
+ def handle_END(self):
+ offset = len(self.lineBuffer) - self.lineBufferIndex
+ if offset:
+ self.terminal.cursorForward(offset)
+ self.lineBufferIndex = len(self.lineBuffer)
+
+ def handle_BACKSPACE(self):
+ if self.lineBufferIndex > 0:
+ self.lineBufferIndex -= 1
+ del self.lineBuffer[self.lineBufferIndex]
+ self.terminal.cursorBackward()
+ self.terminal.deleteCharacter()
+
+ def handle_DELETE(self):
+ if self.lineBufferIndex < len(self.lineBuffer):
+ del self.lineBuffer[self.lineBufferIndex]
+ self.terminal.deleteCharacter()
+
+ def handle_RETURN(self):
+ line = ''.join(self.lineBuffer)
+ self.lineBuffer = []
+ self.lineBufferIndex = 0
+ self.terminal.nextLine()
+ self.lineReceived(line)
+
+ def handle_INSERT(self):
+ assert self.mode in ('typeover', 'insert')
+ if self.mode == 'typeover':
+ self.setInsertMode()
+ else:
+ self.setTypeoverMode()
+
+ def lineReceived(self, line):
+ pass
+
+class HistoricRecvLine(RecvLine):
+ """L{TerminalProtocol} which adds both basic line-editing features and input history.
+
+ Everything supported by L{RecvLine} is also supported by this class. In addition, the
+ up and down arrows traverse the input history. Each received line is automatically
+ added to the end of the input history.
+ """
+ def connectionMade(self):
+ RecvLine.connectionMade(self)
+
+ self.historyLines = []
+ self.historyPosition = 0
+
+ t = self.terminal
+ self.keyHandlers.update({t.UP_ARROW: self.handle_UP,
+ t.DOWN_ARROW: self.handle_DOWN})
+
+ def currentHistoryBuffer(self):
+ b = tuple(self.historyLines)
+ return b[:self.historyPosition], b[self.historyPosition:]
+
+ def _deliverBuffer(self, buf):
+ if buf:
+ for ch in buf[:-1]:
+ self.characterReceived(ch, True)
+ self.characterReceived(buf[-1], False)
+
+ def handle_UP(self):
+ if self.lineBuffer and self.historyPosition == len(self.historyLines):
+ self.historyLines.append(self.lineBuffer)
+ if self.historyPosition > 0:
+ self.handle_HOME()
+ self.terminal.eraseToLineEnd()
+
+ self.historyPosition -= 1
+ self.lineBuffer = []
+
+ self._deliverBuffer(self.historyLines[self.historyPosition])
+
+ def handle_DOWN(self):
+ if self.historyPosition < len(self.historyLines) - 1:
+ self.handle_HOME()
+ self.terminal.eraseToLineEnd()
+
+ self.historyPosition += 1
+ self.lineBuffer = []
+
+ self._deliverBuffer(self.historyLines[self.historyPosition])
+ else:
+ self.handle_HOME()
+ self.terminal.eraseToLineEnd()
+
+ self.historyPosition = len(self.historyLines)
+ self.lineBuffer = []
+ self.lineBufferIndex = 0
+
+ def handle_RETURN(self):
+ if self.lineBuffer:
+ self.historyLines.append(''.join(self.lineBuffer))
+ self.historyPosition = len(self.historyLines)
+ return RecvLine.handle_RETURN(self)
diff --git a/twisted/conch/scripts/__init__.py b/twisted/conch/scripts/__init__.py
new file mode 100644
index 0000000..63fdb3d
--- /dev/null
+++ b/twisted/conch/scripts/__init__.py
@@ -0,0 +1 @@
+'conch scripts'
diff --git a/twisted/conch/scripts/cftp.py b/twisted/conch/scripts/cftp.py
new file mode 100644
index 0000000..e6db67f
--- /dev/null
+++ b/twisted/conch/scripts/cftp.py
@@ -0,0 +1,832 @@
+# -*- test-case-name: twisted.conch.test.test_cftp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation module for the I{cftp} command.
+"""
+
+import os, sys, getpass, struct, tty, fcntl, stat
+import fnmatch, pwd, glob
+
+from twisted.conch.client import connect, default, options
+from twisted.conch.ssh import connection, common
+from twisted.conch.ssh import channel, filetransfer
+from twisted.protocols import basic
+from twisted.internet import reactor, stdio, defer, utils
+from twisted.python import log, usage, failure
+
+class ClientOptions(options.ConchOptions):
+
+ synopsis = """Usage: cftp [options] [user@]host
+ cftp [options] [user@]host[:dir[/]]
+ cftp [options] [user@]host[:file [localfile]]
+"""
+ longdesc = ("cftp is a client for logging into a remote machine and "
+ "executing commands to send and receive file information")
+
+ optParameters = [
+ ['buffersize', 'B', 32768, 'Size of the buffer to use for sending/receiving.'],
+ ['batchfile', 'b', None, 'File to read commands from, or \'-\' for stdin.'],
+ ['requests', 'R', 5, 'Number of requests to make before waiting for a reply.'],
+ ['subsystem', 's', 'sftp', 'Subsystem/server program to connect to.']]
+
+ compData = usage.Completions(
+ descriptions={
+ "buffersize": "Size of send/receive buffer (default: 32768)"},
+ extraActions=[usage.CompleteUserAtHost(),
+ usage.CompleteFiles(descr="local file")])
+
+ def parseArgs(self, host, localPath=None):
+ self['remotePath'] = ''
+ if ':' in host:
+ host, self['remotePath'] = host.split(':', 1)
+ self['remotePath'].rstrip('/')
+ self['host'] = host
+ self['localPath'] = localPath
+
+def run():
+# import hotshot
+# prof = hotshot.Profile('cftp.prof')
+# prof.start()
+ args = sys.argv[1:]
+ if '-l' in args: # cvs is an idiot
+ i = args.index('-l')
+ args = args[i:i+2]+args
+ del args[i+2:i+4]
+ options = ClientOptions()
+ try:
+ options.parseOptions(args)
+ except usage.UsageError, u:
+ print 'ERROR: %s' % u
+ sys.exit(1)
+ if options['log']:
+ realout = sys.stdout
+ log.startLogging(sys.stderr)
+ sys.stdout = realout
+ else:
+ log.discardLogs()
+ doConnect(options)
+ reactor.run()
+# prof.stop()
+# prof.close()
+
+def handleError():
+ global exitStatus
+ exitStatus = 2
+ try:
+ reactor.stop()
+ except: pass
+ log.err(failure.Failure())
+ raise
+
+def doConnect(options):
+# log.deferr = handleError # HACK
+ if '@' in options['host']:
+ options['user'], options['host'] = options['host'].split('@',1)
+ host = options['host']
+ if not options['user']:
+ options['user'] = getpass.getuser()
+ if not options['port']:
+ options['port'] = 22
+ else:
+ options['port'] = int(options['port'])
+ host = options['host']
+ port = options['port']
+ conn = SSHConnection()
+ conn.options = options
+ vhk = default.verifyHostKey
+ uao = default.SSHUserAuthClient(options['user'], options, conn)
+ connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
+
+def _ebExit(f):
+ #global exitStatus
+ if hasattr(f.value, 'value'):
+ s = f.value.value
+ else:
+ s = str(f)
+ print s
+ #exitStatus = "conch: exiting with error %s" % f
+ try:
+ reactor.stop()
+ except: pass
+
+def _ignore(*args): pass
+
+class FileWrapper:
+
+ def __init__(self, f):
+ self.f = f
+ self.total = 0.0
+ f.seek(0, 2) # seek to the end
+ self.size = f.tell()
+
+ def __getattr__(self, attr):
+ return getattr(self.f, attr)
+
+class StdioClient(basic.LineReceiver):
+
+ _pwd = pwd
+
+ ps = 'cftp> '
+ delimiter = '\n'
+
+ reactor = reactor
+
+ def __init__(self, client, f = None):
+ self.client = client
+ self.currentDirectory = ''
+ self.file = f
+ self.useProgressBar = (not f and 1) or 0
+
+ def connectionMade(self):
+ self.client.realPath('').addCallback(self._cbSetCurDir)
+
+ def _cbSetCurDir(self, path):
+ self.currentDirectory = path
+ self._newLine()
+
+ def lineReceived(self, line):
+ if self.client.transport.localClosed:
+ return
+ log.msg('got line %s' % repr(line))
+ line = line.lstrip()
+ if not line:
+ self._newLine()
+ return
+ if self.file and line.startswith('-'):
+ self.ignoreErrors = 1
+ line = line[1:]
+ else:
+ self.ignoreErrors = 0
+ d = self._dispatchCommand(line)
+ if d is not None:
+ d.addCallback(self._cbCommand)
+ d.addErrback(self._ebCommand)
+
+
+ def _dispatchCommand(self, line):
+ if ' ' in line:
+ command, rest = line.split(' ', 1)
+ rest = rest.lstrip()
+ else:
+ command, rest = line, ''
+ if command.startswith('!'): # command
+ f = self.cmd_EXEC
+ rest = (command[1:] + ' ' + rest).strip()
+ else:
+ command = command.upper()
+ log.msg('looking up cmd %s' % command)
+ f = getattr(self, 'cmd_%s' % command, None)
+ if f is not None:
+ return defer.maybeDeferred(f, rest)
+ else:
+ self._ebCommand(failure.Failure(NotImplementedError(
+ "No command called `%s'" % command)))
+ self._newLine()
+
+ def _printFailure(self, f):
+ log.msg(f)
+ e = f.trap(NotImplementedError, filetransfer.SFTPError, OSError, IOError)
+ if e == NotImplementedError:
+ self.transport.write(self.cmd_HELP(''))
+ elif e == filetransfer.SFTPError:
+ self.transport.write("remote error %i: %s\n" %
+ (f.value.code, f.value.message))
+ elif e in (OSError, IOError):
+ self.transport.write("local error %i: %s\n" %
+ (f.value.errno, f.value.strerror))
+
+ def _newLine(self):
+ if self.client.transport.localClosed:
+ return
+ self.transport.write(self.ps)
+ self.ignoreErrors = 0
+ if self.file:
+ l = self.file.readline()
+ if not l:
+ self.client.transport.loseConnection()
+ else:
+ self.transport.write(l)
+ self.lineReceived(l.strip())
+
+ def _cbCommand(self, result):
+ if result is not None:
+ self.transport.write(result)
+ if not result.endswith('\n'):
+ self.transport.write('\n')
+ self._newLine()
+
+ def _ebCommand(self, f):
+ self._printFailure(f)
+ if self.file and not self.ignoreErrors:
+ self.client.transport.loseConnection()
+ self._newLine()
+
+ def cmd_CD(self, path):
+ path, rest = self._getFilename(path)
+ if not path.endswith('/'):
+ path += '/'
+ newPath = path and os.path.join(self.currentDirectory, path) or ''
+ d = self.client.openDirectory(newPath)
+ d.addCallback(self._cbCd)
+ d.addErrback(self._ebCommand)
+ return d
+
+ def _cbCd(self, directory):
+ directory.close()
+ d = self.client.realPath(directory.name)
+ d.addCallback(self._cbCurDir)
+ return d
+
+ def _cbCurDir(self, path):
+ self.currentDirectory = path
+
+ def cmd_CHGRP(self, rest):
+ grp, rest = rest.split(None, 1)
+ path, rest = self._getFilename(rest)
+ grp = int(grp)
+ d = self.client.getAttrs(path)
+ d.addCallback(self._cbSetUsrGrp, path, grp=grp)
+ return d
+
+ def cmd_CHMOD(self, rest):
+ mod, rest = rest.split(None, 1)
+ path, rest = self._getFilename(rest)
+ mod = int(mod, 8)
+ d = self.client.setAttrs(path, {'permissions':mod})
+ d.addCallback(_ignore)
+ return d
+
+ def cmd_CHOWN(self, rest):
+ usr, rest = rest.split(None, 1)
+ path, rest = self._getFilename(rest)
+ usr = int(usr)
+ d = self.client.getAttrs(path)
+ d.addCallback(self._cbSetUsrGrp, path, usr=usr)
+ return d
+
+ def _cbSetUsrGrp(self, attrs, path, usr=None, grp=None):
+ new = {}
+ new['uid'] = (usr is not None) and usr or attrs['uid']
+ new['gid'] = (grp is not None) and grp or attrs['gid']
+ d = self.client.setAttrs(path, new)
+ d.addCallback(_ignore)
+ return d
+
+ def cmd_GET(self, rest):
+ remote, rest = self._getFilename(rest)
+ if '*' in remote or '?' in remote: # wildcard
+ if rest:
+ local, rest = self._getFilename(rest)
+ if not os.path.isdir(local):
+ return "Wildcard get with non-directory target."
+ else:
+ local = ''
+ d = self._remoteGlob(remote)
+ d.addCallback(self._cbGetMultiple, local)
+ return d
+ if rest:
+ local, rest = self._getFilename(rest)
+ else:
+ local = os.path.split(remote)[1]
+ log.msg((remote, local))
+ lf = file(local, 'w', 0)
+ path = os.path.join(self.currentDirectory, remote)
+ d = self.client.openFile(path, filetransfer.FXF_READ, {})
+ d.addCallback(self._cbGetOpenFile, lf)
+ d.addErrback(self._ebCloseLf, lf)
+ return d
+
+ def _cbGetMultiple(self, files, local):
+ #if self._useProgressBar: # one at a time
+ # XXX this can be optimized for times w/o progress bar
+ return self._cbGetMultipleNext(None, files, local)
+
+ def _cbGetMultipleNext(self, res, files, local):
+ if isinstance(res, failure.Failure):
+ self._printFailure(res)
+ elif res:
+ self.transport.write(res)
+ if not res.endswith('\n'):
+ self.transport.write('\n')
+ if not files:
+ return
+ f = files.pop(0)[0]
+ lf = file(os.path.join(local, os.path.split(f)[1]), 'w', 0)
+ path = os.path.join(self.currentDirectory, f)
+ d = self.client.openFile(path, filetransfer.FXF_READ, {})
+ d.addCallback(self._cbGetOpenFile, lf)
+ d.addErrback(self._ebCloseLf, lf)
+ d.addBoth(self._cbGetMultipleNext, files, local)
+ return d
+
+ def _ebCloseLf(self, f, lf):
+ lf.close()
+ return f
+
+ def _cbGetOpenFile(self, rf, lf):
+ return rf.getAttrs().addCallback(self._cbGetFileSize, rf, lf)
+
+ def _cbGetFileSize(self, attrs, rf, lf):
+ if not stat.S_ISREG(attrs['permissions']):
+ rf.close()
+ lf.close()
+ return "Can't get non-regular file: %s" % rf.name
+ rf.size = attrs['size']
+ bufferSize = self.client.transport.conn.options['buffersize']
+ numRequests = self.client.transport.conn.options['requests']
+ rf.total = 0.0
+ dList = []
+ chunks = []
+ startTime = self.reactor.seconds()
+ for i in range(numRequests):
+ d = self._cbGetRead('', rf, lf, chunks, 0, bufferSize, startTime)
+ dList.append(d)
+ dl = defer.DeferredList(dList, fireOnOneErrback=1)
+ dl.addCallback(self._cbGetDone, rf, lf)
+ return dl
+
+ def _getNextChunk(self, chunks):
+ end = 0
+ for chunk in chunks:
+ if end == 'eof':
+ return # nothing more to get
+ if end != chunk[0]:
+ i = chunks.index(chunk)
+ chunks.insert(i, (end, chunk[0]))
+ return (end, chunk[0] - end)
+ end = chunk[1]
+ bufSize = int(self.client.transport.conn.options['buffersize'])
+ chunks.append((end, end + bufSize))
+ return (end, bufSize)
+
+ def _cbGetRead(self, data, rf, lf, chunks, start, size, startTime):
+ if data and isinstance(data, failure.Failure):
+ log.msg('get read err: %s' % data)
+ reason = data
+ reason.trap(EOFError)
+ i = chunks.index((start, start + size))
+ del chunks[i]
+ chunks.insert(i, (start, 'eof'))
+ elif data:
+ log.msg('get read data: %i' % len(data))
+ lf.seek(start)
+ lf.write(data)
+ if len(data) != size:
+ log.msg('got less than we asked for: %i < %i' %
+ (len(data), size))
+ i = chunks.index((start, start + size))
+ del chunks[i]
+ chunks.insert(i, (start, start + len(data)))
+ rf.total += len(data)
+ if self.useProgressBar:
+ self._printProgressBar(rf, startTime)
+ chunk = self._getNextChunk(chunks)
+ if not chunk:
+ return
+ else:
+ start, length = chunk
+ log.msg('asking for %i -> %i' % (start, start+length))
+ d = rf.readChunk(start, length)
+ d.addBoth(self._cbGetRead, rf, lf, chunks, start, length, startTime)
+ return d
+
+ def _cbGetDone(self, ignored, rf, lf):
+ log.msg('get done')
+ rf.close()
+ lf.close()
+ if self.useProgressBar:
+ self.transport.write('\n')
+ return "Transferred %s to %s" % (rf.name, lf.name)
+
+ def cmd_PUT(self, rest):
+ local, rest = self._getFilename(rest)
+ if '*' in local or '?' in local: # wildcard
+ if rest:
+ remote, rest = self._getFilename(rest)
+ path = os.path.join(self.currentDirectory, remote)
+ d = self.client.getAttrs(path)
+ d.addCallback(self._cbPutTargetAttrs, remote, local)
+ return d
+ else:
+ remote = ''
+ files = glob.glob(local)
+ return self._cbPutMultipleNext(None, files, remote)
+ if rest:
+ remote, rest = self._getFilename(rest)
+ else:
+ remote = os.path.split(local)[1]
+ lf = file(local, 'r')
+ path = os.path.join(self.currentDirectory, remote)
+ flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
+ d = self.client.openFile(path, flags, {})
+ d.addCallback(self._cbPutOpenFile, lf)
+ d.addErrback(self._ebCloseLf, lf)
+ return d
+
+ def _cbPutTargetAttrs(self, attrs, path, local):
+ if not stat.S_ISDIR(attrs['permissions']):
+ return "Wildcard put with non-directory target."
+ return self._cbPutMultipleNext(None, files, path)
+
+ def _cbPutMultipleNext(self, res, files, path):
+ if isinstance(res, failure.Failure):
+ self._printFailure(res)
+ elif res:
+ self.transport.write(res)
+ if not res.endswith('\n'):
+ self.transport.write('\n')
+ f = None
+ while files and not f:
+ try:
+ f = files.pop(0)
+ lf = file(f, 'r')
+ except:
+ self._printFailure(failure.Failure())
+ f = None
+ if not f:
+ return
+ name = os.path.split(f)[1]
+ remote = os.path.join(self.currentDirectory, path, name)
+ log.msg((name, remote, path))
+ flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
+ d = self.client.openFile(remote, flags, {})
+ d.addCallback(self._cbPutOpenFile, lf)
+ d.addErrback(self._ebCloseLf, lf)
+ d.addBoth(self._cbPutMultipleNext, files, path)
+ return d
+
+ def _cbPutOpenFile(self, rf, lf):
+ numRequests = self.client.transport.conn.options['requests']
+ if self.useProgressBar:
+ lf = FileWrapper(lf)
+ dList = []
+ chunks = []
+ startTime = self.reactor.seconds()
+ for i in range(numRequests):
+ d = self._cbPutWrite(None, rf, lf, chunks, startTime)
+ if d:
+ dList.append(d)
+ dl = defer.DeferredList(dList, fireOnOneErrback=1)
+ dl.addCallback(self._cbPutDone, rf, lf)
+ return dl
+
+ def _cbPutWrite(self, ignored, rf, lf, chunks, startTime):
+ chunk = self._getNextChunk(chunks)
+ start, size = chunk
+ lf.seek(start)
+ data = lf.read(size)
+ if self.useProgressBar:
+ lf.total += len(data)
+ self._printProgressBar(lf, startTime)
+ if data:
+ d = rf.writeChunk(start, data)
+ d.addCallback(self._cbPutWrite, rf, lf, chunks, startTime)
+ return d
+ else:
+ return
+
+ def _cbPutDone(self, ignored, rf, lf):
+ lf.close()
+ rf.close()
+ if self.useProgressBar:
+ self.transport.write('\n')
+ return 'Transferred %s to %s' % (lf.name, rf.name)
+
+ def cmd_LCD(self, path):
+ os.chdir(path)
+
+ def cmd_LN(self, rest):
+ linkpath, rest = self._getFilename(rest)
+ targetpath, rest = self._getFilename(rest)
+ linkpath, targetpath = map(
+ lambda x: os.path.join(self.currentDirectory, x),
+ (linkpath, targetpath))
+ return self.client.makeLink(linkpath, targetpath).addCallback(_ignore)
+
+ def cmd_LS(self, rest):
+ # possible lines:
+ # ls current directory
+ # ls name_of_file that file
+ # ls name_of_directory that directory
+ # ls some_glob_string current directory, globbed for that string
+ options = []
+ rest = rest.split()
+ while rest and rest[0] and rest[0][0] == '-':
+ opts = rest.pop(0)[1:]
+ for o in opts:
+ if o == 'l':
+ options.append('verbose')
+ elif o == 'a':
+ options.append('all')
+ rest = ' '.join(rest)
+ path, rest = self._getFilename(rest)
+ if not path:
+ fullPath = self.currentDirectory + '/'
+ else:
+ fullPath = os.path.join(self.currentDirectory, path)
+ d = self._remoteGlob(fullPath)
+ d.addCallback(self._cbDisplayFiles, options)
+ return d
+
+ def _cbDisplayFiles(self, files, options):
+ files.sort()
+ if 'all' not in options:
+ files = [f for f in files if not f[0].startswith('.')]
+ if 'verbose' in options:
+ lines = [f[1] for f in files]
+ else:
+ lines = [f[0] for f in files]
+ if not lines:
+ return None
+ else:
+ return '\n'.join(lines)
+
+ def cmd_MKDIR(self, path):
+ path, rest = self._getFilename(path)
+ path = os.path.join(self.currentDirectory, path)
+ return self.client.makeDirectory(path, {}).addCallback(_ignore)
+
+ def cmd_RMDIR(self, path):
+ path, rest = self._getFilename(path)
+ path = os.path.join(self.currentDirectory, path)
+ return self.client.removeDirectory(path).addCallback(_ignore)
+
+ def cmd_LMKDIR(self, path):
+ os.system("mkdir %s" % path)
+
+ def cmd_RM(self, path):
+ path, rest = self._getFilename(path)
+ path = os.path.join(self.currentDirectory, path)
+ return self.client.removeFile(path).addCallback(_ignore)
+
+ def cmd_LLS(self, rest):
+ os.system("ls %s" % rest)
+
+ def cmd_RENAME(self, rest):
+ oldpath, rest = self._getFilename(rest)
+ newpath, rest = self._getFilename(rest)
+ oldpath, newpath = map (
+ lambda x: os.path.join(self.currentDirectory, x),
+ (oldpath, newpath))
+ return self.client.renameFile(oldpath, newpath).addCallback(_ignore)
+
+ def cmd_EXIT(self, ignored):
+ self.client.transport.loseConnection()
+
+ cmd_QUIT = cmd_EXIT
+
+ def cmd_VERSION(self, ignored):
+ return "SFTP version %i" % self.client.version
+
+ def cmd_HELP(self, ignored):
+ return """Available commands:
+cd path Change remote directory to 'path'.
+chgrp gid path Change gid of 'path' to 'gid'.
+chmod mode path Change mode of 'path' to 'mode'.
+chown uid path Change uid of 'path' to 'uid'.
+exit Disconnect from the server.
+get remote-path [local-path] Get remote file.
+help Get a list of available commands.
+lcd path Change local directory to 'path'.
+lls [ls-options] [path] Display local directory listing.
+lmkdir path Create local directory.
+ln linkpath targetpath Symlink remote file.
+lpwd Print the local working directory.
+ls [-l] [path] Display remote directory listing.
+mkdir path Create remote directory.
+progress Toggle progress bar.
+put local-path [remote-path] Put local file.
+pwd Print the remote working directory.
+quit Disconnect from the server.
+rename oldpath newpath Rename remote file.
+rmdir path Remove remote directory.
+rm path Remove remote file.
+version Print the SFTP version.
+? Synonym for 'help'.
+"""
+
+ def cmd_PWD(self, ignored):
+ return self.currentDirectory
+
+ def cmd_LPWD(self, ignored):
+ return os.getcwd()
+
+ def cmd_PROGRESS(self, ignored):
+ self.useProgressBar = not self.useProgressBar
+ return "%ssing progess bar." % (self.useProgressBar and "U" or "Not u")
+
+ def cmd_EXEC(self, rest):
+ """
+ Run C{rest} using the user's shell (or /bin/sh if they do not have
+ one).
+ """
+ shell = self._pwd.getpwnam(getpass.getuser())[6]
+ if not shell:
+ shell = '/bin/sh'
+ if rest:
+ cmds = ['-c', rest]
+ return utils.getProcessOutput(shell, cmds, errortoo=1)
+ else:
+ os.system(shell)
+
+ # accessory functions
+
+ def _remoteGlob(self, fullPath):
+ log.msg('looking up %s' % fullPath)
+ head, tail = os.path.split(fullPath)
+ if '*' in tail or '?' in tail:
+ glob = 1
+ else:
+ glob = 0
+ if tail and not glob: # could be file or directory
+ # try directory first
+ d = self.client.openDirectory(fullPath)
+ d.addCallback(self._cbOpenList, '')
+ d.addErrback(self._ebNotADirectory, head, tail)
+ else:
+ d = self.client.openDirectory(head)
+ d.addCallback(self._cbOpenList, tail)
+ return d
+
+ def _cbOpenList(self, directory, glob):
+ files = []
+ d = directory.read()
+ d.addBoth(self._cbReadFile, files, directory, glob)
+ return d
+
+ def _ebNotADirectory(self, reason, path, glob):
+ d = self.client.openDirectory(path)
+ d.addCallback(self._cbOpenList, glob)
+ return d
+
+ def _cbReadFile(self, files, l, directory, glob):
+ if not isinstance(files, failure.Failure):
+ if glob:
+ l.extend([f for f in files if fnmatch.fnmatch(f[0], glob)])
+ else:
+ l.extend(files)
+ d = directory.read()
+ d.addBoth(self._cbReadFile, l, directory, glob)
+ return d
+ else:
+ reason = files
+ reason.trap(EOFError)
+ directory.close()
+ return l
+
+ def _abbrevSize(self, size):
+ # from http://mail.python.org/pipermail/python-list/1999-December/018395.html
+ _abbrevs = [
+ (1<<50L, 'PB'),
+ (1<<40L, 'TB'),
+ (1<<30L, 'GB'),
+ (1<<20L, 'MB'),
+ (1<<10L, 'kB'),
+ (1, 'B')
+ ]
+
+ for factor, suffix in _abbrevs:
+ if size > factor:
+ break
+ return '%.1f' % (size/factor) + suffix
+
+ def _abbrevTime(self, t):
+ if t > 3600: # 1 hour
+ hours = int(t / 3600)
+ t -= (3600 * hours)
+ mins = int(t / 60)
+ t -= (60 * mins)
+ return "%i:%02i:%02i" % (hours, mins, t)
+ else:
+ mins = int(t/60)
+ t -= (60 * mins)
+ return "%02i:%02i" % (mins, t)
+
+
+ def _printProgressBar(self, f, startTime):
+ """
+ Update a console progress bar on this L{StdioClient}'s transport, based
+ on the difference between the start time of the operation and the
+ current time according to the reactor, and appropriate to the size of
+ the console window.
+
+ @param f: a wrapper around the file which is being written or read
+ @type f: L{FileWrapper}
+
+ @param startTime: The time at which the operation being tracked began.
+ @type startTime: C{float}
+ """
+ diff = self.reactor.seconds() - startTime
+ total = f.total
+ try:
+ winSize = struct.unpack('4H',
+ fcntl.ioctl(0, tty.TIOCGWINSZ, '12345679'))
+ except IOError:
+ winSize = [None, 80]
+ if diff == 0.0:
+ speed = 0.0
+ else:
+ speed = total / diff
+ if speed:
+ timeLeft = (f.size - total) / speed
+ else:
+ timeLeft = 0
+ front = f.name
+ back = '%3i%% %s %sps %s ' % ((total / f.size) * 100,
+ self._abbrevSize(total),
+ self._abbrevSize(speed),
+ self._abbrevTime(timeLeft))
+ spaces = (winSize[1] - (len(front) + len(back) + 1)) * ' '
+ self.transport.write('\r%s%s%s' % (front, spaces, back))
+
+
+ def _getFilename(self, line):
+ line.lstrip()
+ if not line:
+ return None, ''
+ if line[0] in '\'"':
+ ret = []
+ line = list(line)
+ try:
+ for i in range(1,len(line)):
+ c = line[i]
+ if c == line[0]:
+ return ''.join(ret), ''.join(line[i+1:]).lstrip()
+ elif c == '\\': # quoted character
+ del line[i]
+ if line[i] not in '\'"\\':
+ raise IndexError, "bad quote: \\%s" % line[i]
+ ret.append(line[i])
+ else:
+ ret.append(line[i])
+ except IndexError:
+ raise IndexError, "unterminated quote"
+ ret = line.split(None, 1)
+ if len(ret) == 1:
+ return ret[0], ''
+ else:
+ return ret
+
+StdioClient.__dict__['cmd_?'] = StdioClient.cmd_HELP
+
+class SSHConnection(connection.SSHConnection):
+ def serviceStarted(self):
+ self.openChannel(SSHSession())
+
+class SSHSession(channel.SSHChannel):
+
+ name = 'session'
+
+ def channelOpen(self, foo):
+ log.msg('session %s open' % self.id)
+ if self.conn.options['subsystem'].startswith('/'):
+ request = 'exec'
+ else:
+ request = 'subsystem'
+ d = self.conn.sendRequest(self, request, \
+ common.NS(self.conn.options['subsystem']), wantReply=1)
+ d.addCallback(self._cbSubsystem)
+ d.addErrback(_ebExit)
+
+ def _cbSubsystem(self, result):
+ self.client = filetransfer.FileTransferClient()
+ self.client.makeConnection(self)
+ self.dataReceived = self.client.dataReceived
+ f = None
+ if self.conn.options['batchfile']:
+ fn = self.conn.options['batchfile']
+ if fn != '-':
+ f = file(fn)
+ self.stdio = stdio.StandardIO(StdioClient(self.client, f))
+
+ def extReceived(self, t, data):
+ if t==connection.EXTENDED_DATA_STDERR:
+ log.msg('got %s stderr data' % len(data))
+ sys.stderr.write(data)
+ sys.stderr.flush()
+
+ def eofReceived(self):
+ log.msg('got eof')
+ self.stdio.closeStdin()
+
+ def closeReceived(self):
+ log.msg('remote side closed %s' % self)
+ self.conn.sendClose(self)
+
+ def closed(self):
+ try:
+ reactor.stop()
+ except:
+ pass
+
+ def stopWriting(self):
+ self.stdio.pauseProducing()
+
+ def startWriting(self):
+ self.stdio.resumeProducing()
+
+if __name__ == '__main__':
+ run()
+
diff --git a/twisted/conch/scripts/ckeygen.py b/twisted/conch/scripts/ckeygen.py
new file mode 100644
index 0000000..ab51fc1
--- /dev/null
+++ b/twisted/conch/scripts/ckeygen.py
@@ -0,0 +1,190 @@
+# -*- test-case-name: twisted.conch.test.test_ckeygen -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation module for the `ckeygen` command.
+"""
+
+import sys, os, getpass, socket
+if getpass.getpass == getpass.unix_getpass:
+ try:
+ import termios # hack around broken termios
+ termios.tcgetattr, termios.tcsetattr
+ except (ImportError, AttributeError):
+ sys.modules['termios'] = None
+ reload(getpass)
+
+from twisted.conch.ssh import keys
+from twisted.python import filepath, log, usage, randbytes
+
+
+class GeneralOptions(usage.Options):
+ synopsis = """Usage: ckeygen [options]
+ """
+
+ longdesc = "ckeygen manipulates public/private keys in various ways."
+
+ optParameters = [['bits', 'b', 1024, 'Number of bits in the key to create.'],
+ ['filename', 'f', None, 'Filename of the key file.'],
+ ['type', 't', None, 'Specify type of key to create.'],
+ ['comment', 'C', None, 'Provide new comment.'],
+ ['newpass', 'N', None, 'Provide new passphrase.'],
+ ['pass', 'P', None, 'Provide old passphrase']]
+
+ optFlags = [['fingerprint', 'l', 'Show fingerprint of key file.'],
+ ['changepass', 'p', 'Change passphrase of private key file.'],
+ ['quiet', 'q', 'Quiet.'],
+ ['showpub', 'y', 'Read private key file and print public key.']]
+
+ compData = usage.Completions(
+ optActions={"type": usage.CompleteList(["rsa", "dsa"])})
+
+def run():
+ options = GeneralOptions()
+ try:
+ options.parseOptions(sys.argv[1:])
+ except usage.UsageError, u:
+ print 'ERROR: %s' % u
+ options.opt_help()
+ sys.exit(1)
+ log.discardLogs()
+ log.deferr = handleError # HACK
+ if options['type']:
+ if options['type'] == 'rsa':
+ generateRSAkey(options)
+ elif options['type'] == 'dsa':
+ generateDSAkey(options)
+ else:
+ sys.exit('Key type was %s, must be one of: rsa, dsa' % options['type'])
+ elif options['fingerprint']:
+ printFingerprint(options)
+ elif options['changepass']:
+ changePassPhrase(options)
+ elif options['showpub']:
+ displayPublicKey(options)
+ else:
+ options.opt_help()
+ sys.exit(1)
+
+def handleError():
+ from twisted.python import failure
+ global exitStatus
+ exitStatus = 2
+ log.err(failure.Failure())
+ reactor.stop()
+ raise
+
+def generateRSAkey(options):
+ from Crypto.PublicKey import RSA
+ print 'Generating public/private rsa key pair.'
+ key = RSA.generate(int(options['bits']), randbytes.secureRandom)
+ _saveKey(key, options)
+
+def generateDSAkey(options):
+ from Crypto.PublicKey import DSA
+ print 'Generating public/private dsa key pair.'
+ key = DSA.generate(int(options['bits']), randbytes.secureRandom)
+ _saveKey(key, options)
+
+
+def printFingerprint(options):
+ if not options['filename']:
+ filename = os.path.expanduser('~/.ssh/id_rsa')
+ options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
+ if os.path.exists(options['filename']+'.pub'):
+ options['filename'] += '.pub'
+ try:
+ key = keys.Key.fromFile(options['filename'])
+ obj = key.keyObject
+ string = key.blob()
+ print '%s %s %s' % (
+ obj.size() + 1,
+ key.fingerprint(),
+ os.path.basename(options['filename']))
+ except:
+ sys.exit('bad key')
+
+
+def changePassPhrase(options):
+ if not options['filename']:
+ filename = os.path.expanduser('~/.ssh/id_rsa')
+ options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
+ try:
+ key = keys.Key.fromFile(options['filename']).keyObject
+ except keys.BadKeyError, e:
+ if e.args[0] != 'encrypted key with no passphrase':
+ raise
+ else:
+ if not options['pass']:
+ options['pass'] = getpass.getpass('Enter old passphrase: ')
+ key = keys.Key.fromFile(
+ options['filename'], passphrase = options['pass']).keyObject
+ if not options['newpass']:
+ while 1:
+ p1 = getpass.getpass('Enter new passphrase (empty for no passphrase): ')
+ p2 = getpass.getpass('Enter same passphrase again: ')
+ if p1 == p2:
+ break
+ print 'Passphrases do not match. Try again.'
+ options['newpass'] = p1
+ open(options['filename'], 'w').write(
+ keys.Key(key).toString(passphrase=options['newpass']))
+ print 'Your identification has been saved with the new passphrase.'
+
+
+def displayPublicKey(options):
+ if not options['filename']:
+ filename = os.path.expanduser('~/.ssh/id_rsa')
+ options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
+ try:
+ key = keys.Key.fromFile(options['filename']).keyObject
+ except keys.BadKeyError, e:
+ if e.args[0] != 'encrypted key with no passphrase':
+ raise
+ else:
+ if not options['pass']:
+ options['pass'] = getpass.getpass('Enter passphrase: ')
+ key = keys.Key.fromFile(
+ options['filename'], passphrase = options['pass']).keyObject
+ print keys.Key(key).public().toString()
+
+
+def _saveKey(key, options):
+ if not options['filename']:
+ kind = keys.objectType(key)
+ kind = {'ssh-rsa':'rsa','ssh-dss':'dsa'}[kind]
+ filename = os.path.expanduser('~/.ssh/id_%s'%kind)
+ options['filename'] = raw_input('Enter file in which to save the key (%s): '%filename).strip() or filename
+ if os.path.exists(options['filename']):
+ print '%s already exists.' % options['filename']
+ yn = raw_input('Overwrite (y/n)? ')
+ if yn[0].lower() != 'y':
+ sys.exit()
+ if not options['pass']:
+ while 1:
+ p1 = getpass.getpass('Enter passphrase (empty for no passphrase): ')
+ p2 = getpass.getpass('Enter same passphrase again: ')
+ if p1 == p2:
+ break
+ print 'Passphrases do not match. Try again.'
+ options['pass'] = p1
+
+ keyObj = keys.Key(key)
+ comment = '%s@%s' % (getpass.getuser(), socket.gethostname())
+
+ filepath.FilePath(options['filename']).setContent(
+ keyObj.toString('openssh', options['pass']))
+ os.chmod(options['filename'], 33152)
+
+ filepath.FilePath(options['filename'] + '.pub').setContent(
+ keyObj.public().toString('openssh', comment))
+
+ print 'Your identification has been saved in %s' % options['filename']
+ print 'Your public key has been saved in %s.pub' % options['filename']
+ print 'The key fingerprint is:'
+ print keyObj.fingerprint()
+
+if __name__ == '__main__':
+ run()
+
diff --git a/twisted/conch/scripts/conch.py b/twisted/conch/scripts/conch.py
new file mode 100644
index 0000000..8c49544
--- /dev/null
+++ b/twisted/conch/scripts/conch.py
@@ -0,0 +1,512 @@
+# -*- test-case-name: twisted.conch.test.test_conch -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+# $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
+
+#""" Implementation module for the `conch` command.
+#"""
+from twisted.conch.client import connect, default, options
+from twisted.conch.error import ConchError
+from twisted.conch.ssh import connection, common
+from twisted.conch.ssh import session, forwarding, channel
+from twisted.internet import reactor, stdio, task
+from twisted.python import log, usage
+
+import os, sys, getpass, struct, tty, fcntl, signal
+
+class ClientOptions(options.ConchOptions):
+
+ synopsis = """Usage: conch [options] host [command]
+"""
+ longdesc = ("conch is a SSHv2 client that allows logging into a remote "
+ "machine and executing commands.")
+
+ optParameters = [['escape', 'e', '~'],
+ ['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
+ ['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
+ ]
+
+ optFlags = [['null', 'n', 'Redirect input from /dev/null.'],
+ ['fork', 'f', 'Fork to background after authentication.'],
+ ['tty', 't', 'Tty; allocate a tty even if command is given.'],
+ ['notty', 'T', 'Do not allocate a tty.'],
+ ['noshell', 'N', 'Do not execute a shell or command.'],
+ ['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
+ ]
+
+ compData = usage.Completions(
+ mutuallyExclusive=[("tty", "notty")],
+ optActions={
+ "localforward": usage.Completer(descr="listen-port:host:port"),
+ "remoteforward": usage.Completer(descr="listen-port:host:port")},
+ extraActions=[usage.CompleteUserAtHost(),
+ usage.Completer(descr="command"),
+ usage.Completer(descr="argument", repeat=True)]
+ )
+
+ localForwards = []
+ remoteForwards = []
+
+ def opt_escape(self, esc):
+ "Set escape character; ``none'' = disable"
+ if esc == 'none':
+ self['escape'] = None
+ elif esc[0] == '^' and len(esc) == 2:
+ self['escape'] = chr(ord(esc[1])-64)
+ elif len(esc) == 1:
+ self['escape'] = esc
+ else:
+ sys.exit("Bad escape character '%s'." % esc)
+
+ def opt_localforward(self, f):
+ "Forward local port to remote address (lport:host:port)"
+ localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
+ localPort = int(localPort)
+ remotePort = int(remotePort)
+ self.localForwards.append((localPort, (remoteHost, remotePort)))
+
+ def opt_remoteforward(self, f):
+ """Forward remote port to local address (rport:host:port)"""
+ remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
+ remotePort = int(remotePort)
+ connPort = int(connPort)
+ self.remoteForwards.append((remotePort, (connHost, connPort)))
+
+ def parseArgs(self, host, *command):
+ self['host'] = host
+ self['command'] = ' '.join(command)
+
+# Rest of code in "run"
+options = None
+conn = None
+exitStatus = 0
+old = None
+_inRawMode = 0
+_savedRawMode = None
+
+def run():
+ global options, old
+ args = sys.argv[1:]
+ if '-l' in args: # cvs is an idiot
+ i = args.index('-l')
+ args = args[i:i+2]+args
+ del args[i+2:i+4]
+ for arg in args[:]:
+ try:
+ i = args.index(arg)
+ if arg[:2] == '-o' and args[i+1][0]!='-':
+ args[i:i+2] = [] # suck on it scp
+ except ValueError:
+ pass
+ options = ClientOptions()
+ try:
+ options.parseOptions(args)
+ except usage.UsageError, u:
+ print 'ERROR: %s' % u
+ options.opt_help()
+ sys.exit(1)
+ if options['log']:
+ if options['logfile']:
+ if options['logfile'] == '-':
+ f = sys.stdout
+ else:
+ f = file(options['logfile'], 'a+')
+ else:
+ f = sys.stderr
+ realout = sys.stdout
+ log.startLogging(f)
+ sys.stdout = realout
+ else:
+ log.discardLogs()
+ doConnect()
+ fd = sys.stdin.fileno()
+ try:
+ old = tty.tcgetattr(fd)
+ except:
+ old = None
+ try:
+ oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
+ except:
+ oldUSR1 = None
+ try:
+ reactor.run()
+ finally:
+ if old:
+ tty.tcsetattr(fd, tty.TCSANOW, old)
+ if oldUSR1:
+ signal.signal(signal.SIGUSR1, oldUSR1)
+ if (options['command'] and options['tty']) or not options['notty']:
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
+ if sys.stdout.isatty() and not options['command']:
+ print 'Connection to %s closed.' % options['host']
+ sys.exit(exitStatus)
+
+def handleError():
+ from twisted.python import failure
+ global exitStatus
+ exitStatus = 2
+ reactor.callLater(0.01, _stopReactor)
+ log.err(failure.Failure())
+ raise
+
+def _stopReactor():
+ try:
+ reactor.stop()
+ except: pass
+
+def doConnect():
+# log.deferr = handleError # HACK
+ if '@' in options['host']:
+ options['user'], options['host'] = options['host'].split('@',1)
+ if not options.identitys:
+ options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
+ host = options['host']
+ if not options['user']:
+ options['user'] = getpass.getuser()
+ if not options['port']:
+ options['port'] = 22
+ else:
+ options['port'] = int(options['port'])
+ host = options['host']
+ port = options['port']
+ vhk = default.verifyHostKey
+ uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
+ connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
+
+def _ebExit(f):
+ global exitStatus
+ if hasattr(f.value, 'value'):
+ s = f.value.value
+ else:
+ s = str(f)
+ exitStatus = "conch: exiting with error %s" % f
+ reactor.callLater(0.1, _stopReactor)
+
+def onConnect():
+# if keyAgent and options['agent']:
+# cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
+# cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
+ if hasattr(conn.transport, 'sendIgnore'):
+ _KeepAlive(conn)
+ if options.localForwards:
+ for localPort, hostport in options.localForwards:
+ s = reactor.listenTCP(localPort,
+ forwarding.SSHListenForwardingFactory(conn,
+ hostport,
+ SSHListenClientForwardingChannel))
+ conn.localForwards.append(s)
+ if options.remoteForwards:
+ for remotePort, hostport in options.remoteForwards:
+ log.msg('asking for remote forwarding for %s:%s' %
+ (remotePort, hostport))
+ conn.requestRemoteForwarding(remotePort, hostport)
+ reactor.addSystemEventTrigger('before', 'shutdown', beforeShutdown)
+ if not options['noshell'] or options['agent']:
+ conn.openChannel(SSHSession())
+ if options['fork']:
+ if os.fork():
+ os._exit(0)
+ os.setsid()
+ for i in range(3):
+ try:
+ os.close(i)
+ except OSError, e:
+ import errno
+ if e.errno != errno.EBADF:
+ raise
+
+def reConnect():
+ beforeShutdown()
+ conn.transport.transport.loseConnection()
+
+def beforeShutdown():
+ remoteForwards = options.remoteForwards
+ for remotePort, hostport in remoteForwards:
+ log.msg('cancelling %s:%s' % (remotePort, hostport))
+ conn.cancelRemoteForwarding(remotePort)
+
+def stopConnection():
+ if not options['reconnect']:
+ reactor.callLater(0.1, _stopReactor)
+
+class _KeepAlive:
+
+ def __init__(self, conn):
+ self.conn = conn
+ self.globalTimeout = None
+ self.lc = task.LoopingCall(self.sendGlobal)
+ self.lc.start(300)
+
+ def sendGlobal(self):
+ d = self.conn.sendGlobalRequest("conch-keep-alive@twistedmatrix.com",
+ "", wantReply = 1)
+ d.addBoth(self._cbGlobal)
+ self.globalTimeout = reactor.callLater(30, self._ebGlobal)
+
+ def _cbGlobal(self, res):
+ if self.globalTimeout:
+ self.globalTimeout.cancel()
+ self.globalTimeout = None
+
+ def _ebGlobal(self):
+ if self.globalTimeout:
+ self.globalTimeout = None
+ self.conn.transport.loseConnection()
+
+class SSHConnection(connection.SSHConnection):
+ def serviceStarted(self):
+ global conn
+ conn = self
+ self.localForwards = []
+ self.remoteForwards = {}
+ if not isinstance(self, connection.SSHConnection):
+ # make these fall through
+ del self.__class__.requestRemoteForwarding
+ del self.__class__.cancelRemoteForwarding
+ onConnect()
+
+ def serviceStopped(self):
+ lf = self.localForwards
+ self.localForwards = []
+ for s in lf:
+ s.loseConnection()
+ stopConnection()
+
+ def requestRemoteForwarding(self, remotePort, hostport):
+ data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
+ d = self.sendGlobalRequest('tcpip-forward', data,
+ wantReply=1)
+ log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
+ d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
+ d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
+
+ def _cbRemoteForwarding(self, result, remotePort, hostport):
+ log.msg('accepted remote forwarding %s:%s' % (remotePort, hostport))
+ self.remoteForwards[remotePort] = hostport
+ log.msg(repr(self.remoteForwards))
+
+ def _ebRemoteForwarding(self, f, remotePort, hostport):
+ log.msg('remote forwarding %s:%s failed' % (remotePort, hostport))
+ log.msg(f)
+
+ def cancelRemoteForwarding(self, remotePort):
+ data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
+ self.sendGlobalRequest('cancel-tcpip-forward', data)
+ log.msg('cancelling remote forwarding %s' % remotePort)
+ try:
+ del self.remoteForwards[remotePort]
+ except:
+ pass
+ log.msg(repr(self.remoteForwards))
+
+ def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
+ log.msg('%s %s' % ('FTCP', repr(data)))
+ remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
+ log.msg(self.remoteForwards)
+ log.msg(remoteHP)
+ if self.remoteForwards.has_key(remoteHP[1]):
+ connectHP = self.remoteForwards[remoteHP[1]]
+ log.msg('connect forwarding %s' % (connectHP,))
+ return SSHConnectForwardingChannel(connectHP,
+ remoteWindow = windowSize,
+ remoteMaxPacket = maxPacket,
+ conn = self)
+ else:
+ raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
+
+# def channel_auth_agent_openssh_com(self, windowSize, maxPacket, data):
+# if options['agent'] and keyAgent:
+# return agent.SSHAgentForwardingChannel(remoteWindow = windowSize,
+# remoteMaxPacket = maxPacket,
+# conn = self)
+# else:
+# return connection.OPEN_CONNECT_FAILED, "don't have an agent"
+
+ def channelClosed(self, channel):
+ log.msg('connection closing %s' % channel)
+ log.msg(self.channels)
+ if len(self.channels) == 1: # just us left
+ log.msg('stopping connection')
+ stopConnection()
+ else:
+ # because of the unix thing
+ self.__class__.__bases__[0].channelClosed(self, channel)
+
+class SSHSession(channel.SSHChannel):
+
+ name = 'session'
+
+ def channelOpen(self, foo):
+ log.msg('session %s open' % self.id)
+ if options['agent']:
+ d = self.conn.sendRequest(self, 'auth-agent-req@openssh.com', '', wantReply=1)
+ d.addBoth(lambda x:log.msg(x))
+ if options['noshell']: return
+ if (options['command'] and options['tty']) or not options['notty']:
+ _enterRawMode()
+ c = session.SSHSessionClient()
+ if options['escape'] and not options['notty']:
+ self.escapeMode = 1
+ c.dataReceived = self.handleInput
+ else:
+ c.dataReceived = self.write
+ c.connectionLost = lambda x=None,s=self:s.sendEOF()
+ self.stdio = stdio.StandardIO(c)
+ fd = 0
+ if options['subsystem']:
+ self.conn.sendRequest(self, 'subsystem', \
+ common.NS(options['command']))
+ elif options['command']:
+ if options['tty']:
+ term = os.environ['TERM']
+ winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
+ winSize = struct.unpack('4H', winsz)
+ ptyReqData = session.packRequest_pty_req(term, winSize, '')
+ self.conn.sendRequest(self, 'pty-req', ptyReqData)
+ signal.signal(signal.SIGWINCH, self._windowResized)
+ self.conn.sendRequest(self, 'exec', \
+ common.NS(options['command']))
+ else:
+ if not options['notty']:
+ term = os.environ['TERM']
+ winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
+ winSize = struct.unpack('4H', winsz)
+ ptyReqData = session.packRequest_pty_req(term, winSize, '')
+ self.conn.sendRequest(self, 'pty-req', ptyReqData)
+ signal.signal(signal.SIGWINCH, self._windowResized)
+ self.conn.sendRequest(self, 'shell', '')
+ #if hasattr(conn.transport, 'transport'):
+ # conn.transport.transport.setTcpNoDelay(1)
+
+ def handleInput(self, char):
+ #log.msg('handling %s' % repr(char))
+ if char in ('\n', '\r'):
+ self.escapeMode = 1
+ self.write(char)
+ elif self.escapeMode == 1 and char == options['escape']:
+ self.escapeMode = 2
+ elif self.escapeMode == 2:
+ self.escapeMode = 1 # so we can chain escapes together
+ if char == '.': # disconnect
+ log.msg('disconnecting from escape')
+ stopConnection()
+ return
+ elif char == '\x1a': # ^Z, suspend
+ def _():
+ _leaveRawMode()
+ sys.stdout.flush()
+ sys.stdin.flush()
+ os.kill(os.getpid(), signal.SIGTSTP)
+ _enterRawMode()
+ reactor.callLater(0, _)
+ return
+ elif char == 'R': # rekey connection
+ log.msg('rekeying connection')
+ self.conn.transport.sendKexInit()
+ return
+ elif char == '#': # display connections
+ self.stdio.write('\r\nThe following connections are open:\r\n')
+ channels = self.conn.channels.keys()
+ channels.sort()
+ for channelId in channels:
+ self.stdio.write(' #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
+ return
+ self.write('~' + char)
+ else:
+ self.escapeMode = 0
+ self.write(char)
+
+ def dataReceived(self, data):
+ self.stdio.write(data)
+
+ def extReceived(self, t, data):
+ if t==connection.EXTENDED_DATA_STDERR:
+ log.msg('got %s stderr data' % len(data))
+ sys.stderr.write(data)
+
+ def eofReceived(self):
+ log.msg('got eof')
+ self.stdio.loseWriteConnection()
+
+ def closeReceived(self):
+ log.msg('remote side closed %s' % self)
+ self.conn.sendClose(self)
+
+ def closed(self):
+ global old
+ log.msg('closed %s' % self)
+ log.msg(repr(self.conn.channels))
+
+ def request_exit_status(self, data):
+ global exitStatus
+ exitStatus = int(struct.unpack('>L', data)[0])
+ log.msg('exit status: %s' % exitStatus)
+
+ def sendEOF(self):
+ self.conn.sendEOF(self)
+
+ def stopWriting(self):
+ self.stdio.pauseProducing()
+
+ def startWriting(self):
+ self.stdio.resumeProducing()
+
+ def _windowResized(self, *args):
+ winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
+ winSize = struct.unpack('4H', winsz)
+ newSize = winSize[1], winSize[0], winSize[2], winSize[3]
+ self.conn.sendRequest(self, 'window-change', struct.pack('!4L', *newSize))
+
+
+class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
+class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
+
+def _leaveRawMode():
+ global _inRawMode
+ if not _inRawMode:
+ return
+ fd = sys.stdin.fileno()
+ tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
+ _inRawMode = 0
+
+def _enterRawMode():
+ global _inRawMode, _savedMode
+ if _inRawMode:
+ return
+ fd = sys.stdin.fileno()
+ try:
+ old = tty.tcgetattr(fd)
+ new = old[:]
+ except:
+ log.msg('not a typewriter!')
+ else:
+ # iflage
+ new[0] = new[0] | tty.IGNPAR
+ new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
+ tty.IXON | tty.IXANY | tty.IXOFF)
+ if hasattr(tty, 'IUCLC'):
+ new[0] = new[0] & ~tty.IUCLC
+
+ # lflag
+ new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
+ tty.ECHOE | tty.ECHOK | tty.ECHONL)
+ if hasattr(tty, 'IEXTEN'):
+ new[3] = new[3] & ~tty.IEXTEN
+
+ #oflag
+ new[1] = new[1] & ~tty.OPOST
+
+ new[6][tty.VMIN] = 1
+ new[6][tty.VTIME] = 0
+
+ _savedMode = old
+ tty.tcsetattr(fd, tty.TCSANOW, new)
+ #tty.setraw(fd)
+ _inRawMode = 1
+
+if __name__ == '__main__':
+ run()
+
diff --git a/twisted/conch/scripts/tkconch.py b/twisted/conch/scripts/tkconch.py
new file mode 100644
index 0000000..96a08c5
--- /dev/null
+++ b/twisted/conch/scripts/tkconch.py
@@ -0,0 +1,576 @@
+# -*- test-case-name: twisted.conch.test.test_scripts -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+# $Id: tkconch.py,v 1.6 2003/02/22 08:10:15 z3p Exp $
+
+""" Implementation module for the `tkconch` command.
+"""
+
+from __future__ import nested_scopes
+
+import Tkinter, tkFileDialog, tkFont, tkMessageBox, string
+from twisted.conch.ui import tkvt100
+from twisted.conch.ssh import transport, userauth, connection, common, keys
+from twisted.conch.ssh import session, forwarding, channel
+from twisted.conch.client.default import isInKnownHosts
+from twisted.internet import reactor, defer, protocol, tksupport
+from twisted.python import usage, log
+
+import os, sys, getpass, struct, base64, signal
+
+class TkConchMenu(Tkinter.Frame):
+ def __init__(self, *args, **params):
+ ## Standard heading: initialization
+ apply(Tkinter.Frame.__init__, (self,) + args, params)
+
+ self.master.title('TkConch')
+ self.localRemoteVar = Tkinter.StringVar()
+ self.localRemoteVar.set('local')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Hostname').grid(column=1, row=1, sticky='w')
+ self.host = Tkinter.Entry(self)
+ self.host.grid(column=2, columnspan=2, row=1, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Port').grid(column=1, row=2, sticky='w')
+ self.port = Tkinter.Entry(self)
+ self.port.grid(column=2, columnspan=2, row=2, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Username').grid(column=1, row=3, sticky='w')
+ self.user = Tkinter.Entry(self)
+ self.user.grid(column=2, columnspan=2, row=3, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Command').grid(column=1, row=4, sticky='w')
+ self.command = Tkinter.Entry(self)
+ self.command.grid(column=2, columnspan=2, row=4, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Identity').grid(column=1, row=5, sticky='w')
+ self.identity = Tkinter.Entry(self)
+ self.identity.grid(column=2, row=5, sticky='nesw')
+ Tkinter.Button(self, command=self.getIdentityFile, text='Browse').grid(column=3, row=5, sticky='nesw')
+
+ Tkinter.Label(self, text='Port Forwarding').grid(column=1, row=6, sticky='w')
+ self.forwards = Tkinter.Listbox(self, height=0, width=0)
+ self.forwards.grid(column=2, columnspan=2, row=6, sticky='nesw')
+ Tkinter.Button(self, text='Add', command=self.addForward).grid(column=1, row=7)
+ Tkinter.Button(self, text='Remove', command=self.removeForward).grid(column=1, row=8)
+ self.forwardPort = Tkinter.Entry(self)
+ self.forwardPort.grid(column=2, row=7, sticky='nesw')
+ Tkinter.Label(self, text='Port').grid(column=3, row=7, sticky='nesw')
+ self.forwardHost = Tkinter.Entry(self)
+ self.forwardHost.grid(column=2, row=8, sticky='nesw')
+ Tkinter.Label(self, text='Host').grid(column=3, row=8, sticky='nesw')
+ self.localForward = Tkinter.Radiobutton(self, text='Local', variable=self.localRemoteVar, value='local')
+ self.localForward.grid(column=2, row=9)
+ self.remoteForward = Tkinter.Radiobutton(self, text='Remote', variable=self.localRemoteVar, value='remote')
+ self.remoteForward.grid(column=3, row=9)
+
+ Tkinter.Label(self, text='Advanced Options').grid(column=1, columnspan=3, row=10, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Cipher').grid(column=1, row=11, sticky='w')
+ self.cipher = Tkinter.Entry(self, name='cipher')
+ self.cipher.grid(column=2, columnspan=2, row=11, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='MAC').grid(column=1, row=12, sticky='w')
+ self.mac = Tkinter.Entry(self, name='mac')
+ self.mac.grid(column=2, columnspan=2, row=12, sticky='nesw')
+
+ Tkinter.Label(self, anchor='w', justify='left', text='Escape Char').grid(column=1, row=13, sticky='w')
+ self.escape = Tkinter.Entry(self, name='escape')
+ self.escape.grid(column=2, columnspan=2, row=13, sticky='nesw')
+ Tkinter.Button(self, text='Connect!', command=self.doConnect).grid(column=1, columnspan=3, row=14, sticky='nesw')
+
+ # Resize behavior(s)
+ self.grid_rowconfigure(6, weight=1, minsize=64)
+ self.grid_columnconfigure(2, weight=1, minsize=2)
+
+ self.master.protocol("WM_DELETE_WINDOW", sys.exit)
+
+
+ def getIdentityFile(self):
+ r = tkFileDialog.askopenfilename()
+ if r:
+ self.identity.delete(0, Tkinter.END)
+ self.identity.insert(Tkinter.END, r)
+
+ def addForward(self):
+ port = self.forwardPort.get()
+ self.forwardPort.delete(0, Tkinter.END)
+ host = self.forwardHost.get()
+ self.forwardHost.delete(0, Tkinter.END)
+ if self.localRemoteVar.get() == 'local':
+ self.forwards.insert(Tkinter.END, 'L:%s:%s' % (port, host))
+ else:
+ self.forwards.insert(Tkinter.END, 'R:%s:%s' % (port, host))
+
+ def removeForward(self):
+ cur = self.forwards.curselection()
+ if cur:
+ self.forwards.remove(cur[0])
+
+ def doConnect(self):
+ finished = 1
+ options['host'] = self.host.get()
+ options['port'] = self.port.get()
+ options['user'] = self.user.get()
+ options['command'] = self.command.get()
+ cipher = self.cipher.get()
+ mac = self.mac.get()
+ escape = self.escape.get()
+ if cipher:
+ if cipher in SSHClientTransport.supportedCiphers:
+ SSHClientTransport.supportedCiphers = [cipher]
+ else:
+ tkMessageBox.showerror('TkConch', 'Bad cipher.')
+ finished = 0
+
+ if mac:
+ if mac in SSHClientTransport.supportedMACs:
+ SSHClientTransport.supportedMACs = [mac]
+ elif finished:
+ tkMessageBox.showerror('TkConch', 'Bad MAC.')
+ finished = 0
+
+ if escape:
+ if escape == 'none':
+ options['escape'] = None
+ elif escape[0] == '^' and len(escape) == 2:
+ options['escape'] = chr(ord(escape[1])-64)
+ elif len(escape) == 1:
+ options['escape'] = escape
+ elif finished:
+ tkMessageBox.showerror('TkConch', "Bad escape character '%s'." % escape)
+ finished = 0
+
+ if self.identity.get():
+ options.identitys.append(self.identity.get())
+
+ for line in self.forwards.get(0,Tkinter.END):
+ if line[0]=='L':
+ options.opt_localforward(line[2:])
+ else:
+ options.opt_remoteforward(line[2:])
+
+ if '@' in options['host']:
+ options['user'], options['host'] = options['host'].split('@',1)
+
+ if (not options['host'] or not options['user']) and finished:
+ tkMessageBox.showerror('TkConch', 'Missing host or username.')
+ finished = 0
+ if finished:
+ self.master.quit()
+ self.master.destroy()
+ if options['log']:
+ realout = sys.stdout
+ log.startLogging(sys.stderr)
+ sys.stdout = realout
+ else:
+ log.discardLogs()
+ log.deferr = handleError # HACK
+ if not options.identitys:
+ options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
+ host = options['host']
+ port = int(options['port'] or 22)
+ log.msg((host,port))
+ reactor.connectTCP(host, port, SSHClientFactory())
+ frame.master.deiconify()
+ frame.master.title('%s@%s - TkConch' % (options['user'], options['host']))
+ else:
+ self.focus()
+
+class GeneralOptions(usage.Options):
+ synopsis = """Usage: tkconch [options] host [command]
+ """
+
+ optParameters = [['user', 'l', None, 'Log in using this user name.'],
+ ['identity', 'i', '~/.ssh/identity', 'Identity for public key authentication'],
+ ['escape', 'e', '~', "Set escape character; ``none'' = disable"],
+ ['cipher', 'c', None, 'Select encryption algorithm.'],
+ ['macs', 'm', None, 'Specify MAC algorithms for protocol version 2.'],
+ ['port', 'p', None, 'Connect to this port. Server must be on the same port.'],
+ ['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
+ ['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
+ ]
+
+ optFlags = [['tty', 't', 'Tty; allocate a tty even if command is given.'],
+ ['notty', 'T', 'Do not allocate a tty.'],
+ ['version', 'V', 'Display version number only.'],
+ ['compress', 'C', 'Enable compression.'],
+ ['noshell', 'N', 'Do not execute a shell or command.'],
+ ['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
+ ['log', 'v', 'Log to stderr'],
+ ['ansilog', 'a', 'Print the receieved data to stdout']]
+
+ _ciphers = transport.SSHClientTransport.supportedCiphers
+ _macs = transport.SSHClientTransport.supportedMACs
+
+ compData = usage.Completions(
+ mutuallyExclusive=[("tty", "notty")],
+ optActions={
+ "cipher": usage.CompleteList(_ciphers),
+ "macs": usage.CompleteList(_macs),
+ "localforward": usage.Completer(descr="listen-port:host:port"),
+ "remoteforward": usage.Completer(descr="listen-port:host:port")},
+ extraActions=[usage.CompleteUserAtHost(),
+ usage.Completer(descr="command"),
+ usage.Completer(descr="argument", repeat=True)]
+ )
+
+ identitys = []
+ localForwards = []
+ remoteForwards = []
+
+ def opt_identity(self, i):
+ self.identitys.append(i)
+
+ def opt_localforward(self, f):
+ localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
+ localPort = int(localPort)
+ remotePort = int(remotePort)
+ self.localForwards.append((localPort, (remoteHost, remotePort)))
+
+ def opt_remoteforward(self, f):
+ remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
+ remotePort = int(remotePort)
+ connPort = int(connPort)
+ self.remoteForwards.append((remotePort, (connHost, connPort)))
+
+ def opt_compress(self):
+ SSHClientTransport.supportedCompressions[0:1] = ['zlib']
+
+ def parseArgs(self, *args):
+ if args:
+ self['host'] = args[0]
+ self['command'] = ' '.join(args[1:])
+ else:
+ self['host'] = ''
+ self['command'] = ''
+
+# Rest of code in "run"
+options = None
+menu = None
+exitStatus = 0
+frame = None
+
+def deferredAskFrame(question, echo):
+ if frame.callback:
+ raise ValueError("can't ask 2 questions at once!")
+ d = defer.Deferred()
+ resp = []
+ def gotChar(ch, resp=resp):
+ if not ch: return
+ if ch=='\x03': # C-c
+ reactor.stop()
+ if ch=='\r':
+ frame.write('\r\n')
+ stresp = ''.join(resp)
+ del resp
+ frame.callback = None
+ d.callback(stresp)
+ return
+ elif 32 <= ord(ch) < 127:
+ resp.append(ch)
+ if echo:
+ frame.write(ch)
+ elif ord(ch) == 8 and resp: # BS
+ if echo: frame.write('\x08 \x08')
+ resp.pop()
+ frame.callback = gotChar
+ frame.write(question)
+ frame.canvas.focus_force()
+ return d
+
+def run():
+ global menu, options, frame
+ args = sys.argv[1:]
+ if '-l' in args: # cvs is an idiot
+ i = args.index('-l')
+ args = args[i:i+2]+args
+ del args[i+2:i+4]
+ for arg in args[:]:
+ try:
+ i = args.index(arg)
+ if arg[:2] == '-o' and args[i+1][0]!='-':
+ args[i:i+2] = [] # suck on it scp
+ except ValueError:
+ pass
+ root = Tkinter.Tk()
+ root.withdraw()
+ top = Tkinter.Toplevel()
+ menu = TkConchMenu(top)
+ menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
+ options = GeneralOptions()
+ try:
+ options.parseOptions(args)
+ except usage.UsageError, u:
+ print 'ERROR: %s' % u
+ options.opt_help()
+ sys.exit(1)
+ for k,v in options.items():
+ if v and hasattr(menu, k):
+ getattr(menu,k).insert(Tkinter.END, v)
+ for (p, (rh, rp)) in options.localForwards:
+ menu.forwards.insert(Tkinter.END, 'L:%s:%s:%s' % (p, rh, rp))
+ options.localForwards = []
+ for (p, (rh, rp)) in options.remoteForwards:
+ menu.forwards.insert(Tkinter.END, 'R:%s:%s:%s' % (p, rh, rp))
+ options.remoteForwards = []
+ frame = tkvt100.VT100Frame(root, callback=None)
+ root.geometry('%dx%d'%(tkvt100.fontWidth*frame.width+3, tkvt100.fontHeight*frame.height+3))
+ frame.pack(side = Tkinter.TOP)
+ tksupport.install(root)
+ root.withdraw()
+ if (options['host'] and options['user']) or '@' in options['host']:
+ menu.doConnect()
+ else:
+ top.mainloop()
+ reactor.run()
+ sys.exit(exitStatus)
+
+def handleError():
+ from twisted.python import failure
+ global exitStatus
+ exitStatus = 2
+ log.err(failure.Failure())
+ reactor.stop()
+ raise
+
+class SSHClientFactory(protocol.ClientFactory):
+ noisy = 1
+
+ def stopFactory(self):
+ reactor.stop()
+
+ def buildProtocol(self, addr):
+ return SSHClientTransport()
+
+ def clientConnectionFailed(self, connector, reason):
+ tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
+
+class SSHClientTransport(transport.SSHClientTransport):
+
+ def receiveError(self, code, desc):
+ global exitStatus
+ exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
+
+ def sendDisconnect(self, code, reason):
+ global exitStatus
+ exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
+ transport.SSHClientTransport.sendDisconnect(self, code, reason)
+
+ def receiveDebug(self, alwaysDisplay, message, lang):
+ global options
+ if alwaysDisplay or options['log']:
+ log.msg('Received Debug Message: %s' % message)
+
+ def verifyHostKey(self, pubKey, fingerprint):
+ #d = defer.Deferred()
+ #d.addCallback(lambda x:defer.succeed(1))
+ #d.callback(2)
+ #return d
+ goodKey = isInKnownHosts(options['host'], pubKey, {'known-hosts': None})
+ if goodKey == 1: # good key
+ return defer.succeed(1)
+ elif goodKey == 2: # AAHHHHH changed
+ return defer.fail(error.ConchError('bad host key'))
+ else:
+ if options['host'] == self.transport.getPeer()[1]:
+ host = options['host']
+ khHost = options['host']
+ else:
+ host = '%s (%s)' % (options['host'],
+ self.transport.getPeer()[1])
+ khHost = '%s,%s' % (options['host'],
+ self.transport.getPeer()[1])
+ keyType = common.getNS(pubKey)[0]
+ ques = """The authenticity of host '%s' can't be established.\r
+%s key fingerprint is %s.""" % (host,
+ {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType],
+ fingerprint)
+ ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
+ return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
+
+ def _cbVerifyHostKey(self, ans, pubKey, khHost, keyType):
+ if ans.lower() not in ('yes', 'no'):
+ return deferredAskFrame("Please type 'yes' or 'no': ",1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
+ if ans.lower() == 'no':
+ frame.write('Host key verification failed.\r\n')
+ raise error.ConchError('bad host key')
+ try:
+ frame.write("Warning: Permanently added '%s' (%s) to the list of known hosts.\r\n" % (khHost, {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType]))
+ known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'), 'a')
+ encodedKey = base64.encodestring(pubKey).replace('\n', '')
+ known_hosts.write('\n%s %s %s' % (khHost, keyType, encodedKey))
+ known_hosts.close()
+ except:
+ log.deferr()
+ raise error.ConchError
+
+ def connectionSecure(self):
+ if options['user']:
+ user = options['user']
+ else:
+ user = getpass.getuser()
+ self.requestService(SSHUserAuthClient(user, SSHConnection()))
+
+class SSHUserAuthClient(userauth.SSHUserAuthClient):
+ usedFiles = []
+
+ def getPassword(self, prompt = None):
+ if not prompt:
+ prompt = "%s@%s's password: " % (self.user, options['host'])
+ return deferredAskFrame(prompt,0)
+
+ def getPublicKey(self):
+ files = [x for x in options.identitys if x not in self.usedFiles]
+ if not files:
+ return None
+ file = files[0]
+ log.msg(file)
+ self.usedFiles.append(file)
+ file = os.path.expanduser(file)
+ file += '.pub'
+ if not os.path.exists(file):
+ return
+ try:
+ return keys.Key.fromFile(file).blob()
+ except:
+ return self.getPublicKey() # try again
+
+ def getPrivateKey(self):
+ file = os.path.expanduser(self.usedFiles[-1])
+ if not os.path.exists(file):
+ return None
+ try:
+ return defer.succeed(keys.Key.fromFile(file).keyObject)
+ except keys.BadKeyError, e:
+ if e.args[0] == 'encrypted key with no password':
+ prompt = "Enter passphrase for key '%s': " % \
+ self.usedFiles[-1]
+ return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
+ def _cbGetPrivateKey(self, ans, count):
+ file = os.path.expanduser(self.usedFiles[-1])
+ try:
+ return keys.Key.fromFile(file, password = ans).keyObject
+ except keys.BadKeyError:
+ if count == 2:
+ raise
+ prompt = "Enter passphrase for key '%s': " % \
+ self.usedFiles[-1]
+ return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
+
+class SSHConnection(connection.SSHConnection):
+ def serviceStarted(self):
+ if not options['noshell']:
+ self.openChannel(SSHSession())
+ if options.localForwards:
+ for localPort, hostport in options.localForwards:
+ reactor.listenTCP(localPort,
+ forwarding.SSHListenForwardingFactory(self,
+ hostport,
+ forwarding.SSHListenClientForwardingChannel))
+ if options.remoteForwards:
+ for remotePort, hostport in options.remoteForwards:
+ log.msg('asking for remote forwarding for %s:%s' %
+ (remotePort, hostport))
+ data = forwarding.packGlobal_tcpip_forward(
+ ('0.0.0.0', remotePort))
+ d = self.sendGlobalRequest('tcpip-forward', data)
+ self.remoteForwards[remotePort] = hostport
+
+class SSHSession(channel.SSHChannel):
+
+ name = 'session'
+
+ def channelOpen(self, foo):
+ #global globalSession
+ #globalSession = self
+ # turn off local echo
+ self.escapeMode = 1
+ c = session.SSHSessionClient()
+ if options['escape']:
+ c.dataReceived = self.handleInput
+ else:
+ c.dataReceived = self.write
+ c.connectionLost = self.sendEOF
+ frame.callback = c.dataReceived
+ frame.canvas.focus_force()
+ if options['subsystem']:
+ self.conn.sendRequest(self, 'subsystem', \
+ common.NS(options['command']))
+ elif options['command']:
+ if options['tty']:
+ term = os.environ.get('TERM', 'xterm')
+ #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
+ winSize = (25,80,0,0) #struct.unpack('4H', winsz)
+ ptyReqData = session.packRequest_pty_req(term, winSize, '')
+ self.conn.sendRequest(self, 'pty-req', ptyReqData)
+ self.conn.sendRequest(self, 'exec', \
+ common.NS(options['command']))
+ else:
+ if not options['notty']:
+ term = os.environ.get('TERM', 'xterm')
+ #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
+ winSize = (25,80,0,0) #struct.unpack('4H', winsz)
+ ptyReqData = session.packRequest_pty_req(term, winSize, '')
+ self.conn.sendRequest(self, 'pty-req', ptyReqData)
+ self.conn.sendRequest(self, 'shell', '')
+ self.conn.transport.transport.setTcpNoDelay(1)
+
+ def handleInput(self, char):
+ #log.msg('handling %s' % repr(char))
+ if char in ('\n', '\r'):
+ self.escapeMode = 1
+ self.write(char)
+ elif self.escapeMode == 1 and char == options['escape']:
+ self.escapeMode = 2
+ elif self.escapeMode == 2:
+ self.escapeMode = 1 # so we can chain escapes together
+ if char == '.': # disconnect
+ log.msg('disconnecting from escape')
+ reactor.stop()
+ return
+ elif char == '\x1a': # ^Z, suspend
+ # following line courtesy of Erwin@freenode
+ os.kill(os.getpid(), signal.SIGSTOP)
+ return
+ elif char == 'R': # rekey connection
+ log.msg('rekeying connection')
+ self.conn.transport.sendKexInit()
+ return
+ self.write('~' + char)
+ else:
+ self.escapeMode = 0
+ self.write(char)
+
+ def dataReceived(self, data):
+ if options['ansilog']:
+ print repr(data)
+ frame.write(data)
+
+ def extReceived(self, t, data):
+ if t==connection.EXTENDED_DATA_STDERR:
+ log.msg('got %s stderr data' % len(data))
+ sys.stderr.write(data)
+ sys.stderr.flush()
+
+ def eofReceived(self):
+ log.msg('got eof')
+ sys.stdin.close()
+
+ def closed(self):
+ log.msg('closed %s' % self)
+ if len(self.conn.channels) == 1: # just us left
+ reactor.stop()
+
+ def request_exit_status(self, data):
+ global exitStatus
+ exitStatus = int(struct.unpack('>L', data)[0])
+ log.msg('exit status: %s' % exitStatus)
+
+ def sendEOF(self):
+ self.conn.sendEOF(self)
+
+if __name__=="__main__":
+ run()
diff --git a/twisted/conch/ssh/__init__.py b/twisted/conch/ssh/__init__.py
new file mode 100644
index 0000000..4b7f024
--- /dev/null
+++ b/twisted/conch/ssh/__init__.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+An SSHv2 implementation for Twisted. Part of the Twisted.Conch package.
+
+Maintainer: Paul Swartz
+"""
diff --git a/twisted/conch/ssh/agent.py b/twisted/conch/ssh/agent.py
new file mode 100644
index 0000000..c1bf1a0
--- /dev/null
+++ b/twisted/conch/ssh/agent.py
@@ -0,0 +1,294 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implements the SSH v2 key agent protocol. This protocol is documented in the
+SSH source code, in the file
+U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
+
+Maintainer: Paul Swartz
+"""
+
+import struct
+
+from twisted.conch.ssh.common import NS, getNS, getMP
+from twisted.conch.error import ConchError, MissingKeyStoreError
+from twisted.conch.ssh import keys
+from twisted.internet import defer, protocol
+
+
+
+class SSHAgentClient(protocol.Protocol):
+ """
+ The client side of the SSH agent protocol. This is equivalent to
+ ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
+ protocol, also in this package.
+ """
+
+ def __init__(self):
+ self.buf = ''
+ self.deferreds = []
+
+
+ def dataReceived(self, data):
+ self.buf += data
+ while 1:
+ if len(self.buf) <= 4:
+ return
+ packLen = struct.unpack('!L', self.buf[:4])[0]
+ if len(self.buf) < 4 + packLen:
+ return
+ packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
+ reqType = ord(packet[0])
+ d = self.deferreds.pop(0)
+ if reqType == AGENT_FAILURE:
+ d.errback(ConchError('agent failure'))
+ elif reqType == AGENT_SUCCESS:
+ d.callback('')
+ else:
+ d.callback(packet)
+
+
+ def sendRequest(self, reqType, data):
+ pack = struct.pack('!LB',len(data) + 1, reqType) + data
+ self.transport.write(pack)
+ d = defer.Deferred()
+ self.deferreds.append(d)
+ return d
+
+
+ def requestIdentities(self):
+ """
+ @return: A L{Deferred} which will fire with a list of all keys found in
+ the SSH agent. The list of keys is comprised of (public key blob,
+ comment) tuples.
+ """
+ d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, '')
+ d.addCallback(self._cbRequestIdentities)
+ return d
+
+
+ def _cbRequestIdentities(self, data):
+ """
+ Unpack a collection of identities into a list of tuples comprised of
+ public key blobs and comments.
+ """
+ if ord(data[0]) != AGENT_IDENTITIES_ANSWER:
+ raise ConchError('unexpected response: %i' % ord(data[0]))
+ numKeys = struct.unpack('!L', data[1:5])[0]
+ keys = []
+ data = data[5:]
+ for i in range(numKeys):
+ blob, data = getNS(data)
+ comment, data = getNS(data)
+ keys.append((blob, comment))
+ return keys
+
+
+ def addIdentity(self, blob, comment = ''):
+ """
+ Add a private key blob to the agent's collection of keys.
+ """
+ req = blob
+ req += NS(comment)
+ return self.sendRequest(AGENTC_ADD_IDENTITY, req)
+
+
+ def signData(self, blob, data):
+ """
+ Request that the agent sign the given C{data} with the private key
+ which corresponds to the public key given by C{blob}. The private
+ key should have been added to the agent already.
+
+ @type blob: C{str}
+ @type data: C{str}
+ @return: A L{Deferred} which fires with a signature for given data
+ created with the given key.
+ """
+ req = NS(blob)
+ req += NS(data)
+ req += '\000\000\000\000' # flags
+ return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
+
+
+ def _cbSignData(self, data):
+ if ord(data[0]) != AGENT_SIGN_RESPONSE:
+ raise ConchError('unexpected data: %i' % ord(data[0]))
+ signature = getNS(data[1:])[0]
+ return signature
+
+
+ def removeIdentity(self, blob):
+ """
+ Remove the private key corresponding to the public key in blob from the
+ running agent.
+ """
+ req = NS(blob)
+ return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
+
+
+ def removeAllIdentities(self):
+ """
+ Remove all keys from the running agent.
+ """
+ return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, '')
+
+
+
+class SSHAgentServer(protocol.Protocol):
+ """
+ The server side of the SSH agent protocol. This is equivalent to
+ ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
+ protocol, also in this package.
+ """
+
+ def __init__(self):
+ self.buf = ''
+
+
+ def dataReceived(self, data):
+ self.buf += data
+ while 1:
+ if len(self.buf) <= 4:
+ return
+ packLen = struct.unpack('!L', self.buf[:4])[0]
+ if len(self.buf) < 4 + packLen:
+ return
+ packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:]
+ reqType = ord(packet[0])
+ reqName = messages.get(reqType, None)
+ if not reqName:
+ self.sendResponse(AGENT_FAILURE, '')
+ else:
+ f = getattr(self, 'agentc_%s' % reqName)
+ if getattr(self.factory, 'keys', None) is None:
+ self.sendResponse(AGENT_FAILURE, '')
+ raise MissingKeyStoreError()
+ f(packet[1:])
+
+
+ def sendResponse(self, reqType, data):
+ pack = struct.pack('!LB', len(data) + 1, reqType) + data
+ self.transport.write(pack)
+
+
+ def agentc_REQUEST_IDENTITIES(self, data):
+ """
+ Return all of the identities that have been added to the server
+ """
+ assert data == ''
+ numKeys = len(self.factory.keys)
+ resp = []
+
+ resp.append(struct.pack('!L', numKeys))
+ for key, comment in self.factory.keys.itervalues():
+ resp.append(NS(key.blob())) # yes, wrapped in an NS
+ resp.append(NS(comment))
+ self.sendResponse(AGENT_IDENTITIES_ANSWER, ''.join(resp))
+
+
+ def agentc_SIGN_REQUEST(self, data):
+ """
+ Data is a structure with a reference to an already added key object and
+ some data that the clients wants signed with that key. If the key
+ object wasn't loaded, return AGENT_FAILURE, else return the signature.
+ """
+ blob, data = getNS(data)
+ if blob not in self.factory.keys:
+ return self.sendResponse(AGENT_FAILURE, '')
+ signData, data = getNS(data)
+ assert data == '\000\000\000\000'
+ self.sendResponse(AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData)))
+
+
+ def agentc_ADD_IDENTITY(self, data):
+ """
+ Adds a private key to the agent's collection of identities. On
+ subsequent interactions, the private key can be accessed using only the
+ corresponding public key.
+ """
+
+ # need to pre-read the key data so we can get past it to the comment string
+ keyType, rest = getNS(data)
+ if keyType == 'ssh-rsa':
+ nmp = 6
+ elif keyType == 'ssh-dss':
+ nmp = 5
+ else:
+ raise keys.BadKeyError('unknown blob type: %s' % keyType)
+
+ rest = getMP(rest, nmp)[-1] # ignore the key data for now, we just want the comment
+ comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
+
+ k = keys.Key.fromString(data, type='private_blob') # not wrapped in NS here
+ self.factory.keys[k.blob()] = (k, comment)
+ self.sendResponse(AGENT_SUCCESS, '')
+
+
+ def agentc_REMOVE_IDENTITY(self, data):
+ """
+ Remove a specific key from the agent's collection of identities.
+ """
+ blob, _ = getNS(data)
+ k = keys.Key.fromString(blob, type='blob')
+ del self.factory.keys[k.blob()]
+ self.sendResponse(AGENT_SUCCESS, '')
+
+
+ def agentc_REMOVE_ALL_IDENTITIES(self, data):
+ """
+ Remove all keys from the agent's collection of identities.
+ """
+ assert data == ''
+ self.factory.keys = {}
+ self.sendResponse(AGENT_SUCCESS, '')
+
+ # v1 messages that we ignore because we don't keep v1 keys
+ # open-ssh sends both v1 and v2 commands, so we have to
+ # do no-ops for v1 commands or we'll get "bad request" errors
+
+ def agentc_REQUEST_RSA_IDENTITIES(self, data):
+ """
+ v1 message for listing RSA1 keys; superseded by
+ agentc_REQUEST_IDENTITIES, which handles different key types.
+ """
+ self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0))
+
+
+ def agentc_REMOVE_RSA_IDENTITY(self, data):
+ """
+ v1 message for removing RSA1 keys; superseded by
+ agentc_REMOVE_IDENTITY, which handles different key types.
+ """
+ self.sendResponse(AGENT_SUCCESS, '')
+
+
+ def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
+ """
+ v1 message for removing all RSA1 keys; superseded by
+ agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
+ """
+ self.sendResponse(AGENT_SUCCESS, '')
+
+
+AGENTC_REQUEST_RSA_IDENTITIES = 1
+AGENT_RSA_IDENTITIES_ANSWER = 2
+AGENT_FAILURE = 5
+AGENT_SUCCESS = 6
+
+AGENTC_REMOVE_RSA_IDENTITY = 8
+AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
+
+AGENTC_REQUEST_IDENTITIES = 11
+AGENT_IDENTITIES_ANSWER = 12
+AGENTC_SIGN_REQUEST = 13
+AGENT_SIGN_RESPONSE = 14
+AGENTC_ADD_IDENTITY = 17
+AGENTC_REMOVE_IDENTITY = 18
+AGENTC_REMOVE_ALL_IDENTITIES = 19
+
+messages = {}
+for name, value in locals().copy().items():
+ if name[:7] == 'AGENTC_':
+ messages[value] = name[7:] # doesn't handle doubles
+
diff --git a/twisted/conch/ssh/channel.py b/twisted/conch/ssh/channel.py
new file mode 100644
index 0000000..f498aec
--- /dev/null
+++ b/twisted/conch/ssh/channel.py
@@ -0,0 +1,281 @@
+# -*- test-case-name: twisted.conch.test.test_channel -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""
+The parent class for all the SSH Channels. Currently implemented channels
+are session. direct-tcp, and forwarded-tcp.
+
+Maintainer: Paul Swartz
+"""
+
+from twisted.python import log
+from twisted.internet import interfaces
+from zope.interface import implements
+
+
+class SSHChannel(log.Logger):
+ """
+ A class that represents a multiplexed channel over an SSH connection.
+ The channel has a local window which is the maximum amount of data it will
+ receive, and a remote which is the maximum amount of data the remote side
+ will accept. There is also a maximum packet size for any individual data
+ packet going each way.
+
+ @ivar name: the name of the channel.
+ @type name: C{str}
+ @ivar localWindowSize: the maximum size of the local window in bytes.
+ @type localWindowSize: C{int}
+ @ivar localWindowLeft: how many bytes are left in the local window.
+ @type localWindowLeft: C{int}
+ @ivar localMaxPacket: the maximum size of packet we will accept in bytes.
+ @type localMaxPacket: C{int}
+ @ivar remoteWindowLeft: how many bytes are left in the remote window.
+ @type remoteWindowLeft: C{int}
+ @ivar remoteMaxPacket: the maximum size of a packet the remote side will
+ accept in bytes.
+ @type remoteMaxPacket: C{int}
+ @ivar conn: the connection this channel is multiplexed through.
+ @type conn: L{SSHConnection}
+ @ivar data: any data to send to the other size when the channel is
+ requested.
+ @type data: C{str}
+ @ivar avatar: an avatar for the logged-in user (if a server channel)
+ @ivar localClosed: True if we aren't accepting more data.
+ @type localClosed: C{bool}
+ @ivar remoteClosed: True if the other size isn't accepting more data.
+ @type remoteClosed: C{bool}
+ """
+
+ implements(interfaces.ITransport)
+
+ name = None # only needed for client channels
+
+ def __init__(self, localWindow = 0, localMaxPacket = 0,
+ remoteWindow = 0, remoteMaxPacket = 0,
+ conn = None, data=None, avatar = None):
+ self.localWindowSize = localWindow or 131072
+ self.localWindowLeft = self.localWindowSize
+ self.localMaxPacket = localMaxPacket or 32768
+ self.remoteWindowLeft = remoteWindow
+ self.remoteMaxPacket = remoteMaxPacket
+ self.areWriting = 1
+ self.conn = conn
+ self.data = data
+ self.avatar = avatar
+ self.specificData = ''
+ self.buf = ''
+ self.extBuf = []
+ self.closing = 0
+ self.localClosed = 0
+ self.remoteClosed = 0
+ self.id = None # gets set later by SSHConnection
+
+ def __str__(self):
+ return '<SSHChannel %s (lw %i rw %i)>' % (self.name,
+ self.localWindowLeft, self.remoteWindowLeft)
+
+ def logPrefix(self):
+ id = (self.id is not None and str(self.id)) or "unknown"
+ return "SSHChannel %s (%s) on %s" % (self.name, id,
+ self.conn.logPrefix())
+
+ def channelOpen(self, specificData):
+ """
+ Called when the channel is opened. specificData is any data that the
+ other side sent us when opening the channel.
+
+ @type specificData: C{str}
+ """
+ log.msg('channel open')
+
+ def openFailed(self, reason):
+ """
+ Called when the the open failed for some reason.
+ reason.desc is a string descrption, reason.code the the SSH error code.
+
+ @type reason: L{error.ConchError}
+ """
+ log.msg('other side refused open\nreason: %s'% reason)
+
+ def addWindowBytes(self, bytes):
+ """
+ Called when bytes are added to the remote window. By default it clears
+ the data buffers.
+
+ @type bytes: C{int}
+ """
+ self.remoteWindowLeft = self.remoteWindowLeft+bytes
+ if not self.areWriting and not self.closing:
+ self.areWriting = True
+ self.startWriting()
+ if self.buf:
+ b = self.buf
+ self.buf = ''
+ self.write(b)
+ if self.extBuf:
+ b = self.extBuf
+ self.extBuf = []
+ for (type, data) in b:
+ self.writeExtended(type, data)
+
+ def requestReceived(self, requestType, data):
+ """
+ Called when a request is sent to this channel. By default it delegates
+ to self.request_<requestType>.
+ If this function returns true, the request succeeded, otherwise it
+ failed.
+
+ @type requestType: C{str}
+ @type data: C{str}
+ @rtype: C{bool}
+ """
+ foo = requestType.replace('-', '_')
+ f = getattr(self, 'request_%s'%foo, None)
+ if f:
+ return f(data)
+ log.msg('unhandled request for %s'%requestType)
+ return 0
+
+ def dataReceived(self, data):
+ """
+ Called when we receive data.
+
+ @type data: C{str}
+ """
+ log.msg('got data %s'%repr(data))
+
+ def extReceived(self, dataType, data):
+ """
+ Called when we receive extended data (usually standard error).
+
+ @type dataType: C{int}
+ @type data: C{str}
+ """
+ log.msg('got extended data %s %s'%(dataType, repr(data)))
+
+ def eofReceived(self):
+ """
+ Called when the other side will send no more data.
+ """
+ log.msg('remote eof')
+
+ def closeReceived(self):
+ """
+ Called when the other side has closed the channel.
+ """
+ log.msg('remote close')
+ self.loseConnection()
+
+ def closed(self):
+ """
+ Called when the channel is closed. This means that both our side and
+ the remote side have closed the channel.
+ """
+ log.msg('closed')
+
+ # transport stuff
+ def write(self, data):
+ """
+ Write some data to the channel. If there is not enough remote window
+ available, buffer until it is. Otherwise, split the data into
+ packets of length remoteMaxPacket and send them.
+
+ @type data: C{str}
+ """
+ if self.buf:
+ self.buf += data
+ return
+ top = len(data)
+ if top > self.remoteWindowLeft:
+ data, self.buf = (data[:self.remoteWindowLeft],
+ data[self.remoteWindowLeft:])
+ self.areWriting = 0
+ self.stopWriting()
+ top = self.remoteWindowLeft
+ rmp = self.remoteMaxPacket
+ write = self.conn.sendData
+ r = range(0, top, rmp)
+ for offset in r:
+ write(self, data[offset: offset+rmp])
+ self.remoteWindowLeft -= top
+ if self.closing and not self.buf:
+ self.loseConnection() # try again
+
+ def writeExtended(self, dataType, data):
+ """
+ Send extended data to this channel. If there is not enough remote
+ window available, buffer until there is. Otherwise, split the data
+ into packets of length remoteMaxPacket and send them.
+
+ @type dataType: C{int}
+ @type data: C{str}
+ """
+ if self.extBuf:
+ if self.extBuf[-1][0] == dataType:
+ self.extBuf[-1][1] += data
+ else:
+ self.extBuf.append([dataType, data])
+ return
+ if len(data) > self.remoteWindowLeft:
+ data, self.extBuf = (data[:self.remoteWindowLeft],
+ [[dataType, data[self.remoteWindowLeft:]]])
+ self.areWriting = 0
+ self.stopWriting()
+ while len(data) > self.remoteMaxPacket:
+ self.conn.sendExtendedData(self, dataType,
+ data[:self.remoteMaxPacket])
+ data = data[self.remoteMaxPacket:]
+ self.remoteWindowLeft -= self.remoteMaxPacket
+ if data:
+ self.conn.sendExtendedData(self, dataType, data)
+ self.remoteWindowLeft -= len(data)
+ if self.closing:
+ self.loseConnection() # try again
+
+ def writeSequence(self, data):
+ """
+ Part of the Transport interface. Write a list of strings to the
+ channel.
+
+ @type data: C{list} of C{str}
+ """
+ self.write(''.join(data))
+
+ def loseConnection(self):
+ """
+ Close the channel if there is no buferred data. Otherwise, note the
+ request and return.
+ """
+ self.closing = 1
+ if not self.buf and not self.extBuf:
+ self.conn.sendClose(self)
+
+ def getPeer(self):
+ """
+ Return a tuple describing the other side of the connection.
+
+ @rtype: C{tuple}
+ """
+ return('SSH', )+self.conn.transport.getPeer()
+
+ def getHost(self):
+ """
+ Return a tuple describing our side of the connection.
+
+ @rtype: C{tuple}
+ """
+ return('SSH', )+self.conn.transport.getHost()
+
+ def stopWriting(self):
+ """
+ Called when the remote buffer is full, as a hint to stop writing.
+ This can be ignored, but it can be helpful.
+ """
+
+ def startWriting(self):
+ """
+ Called when the remote buffer has more room, as a hint to continue
+ writing.
+ """
diff --git a/twisted/conch/ssh/common.py b/twisted/conch/ssh/common.py
new file mode 100644
index 0000000..be2d21d
--- /dev/null
+++ b/twisted/conch/ssh/common.py
@@ -0,0 +1,117 @@
+# -*- test-case-name: twisted.conch.test.test_ssh -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Common functions for the SSH classes.
+
+Maintainer: Paul Swartz
+"""
+
+import struct, warnings
+
+try:
+ from Crypto import Util
+except ImportError:
+ warnings.warn("PyCrypto not installed, but continuing anyways!",
+ RuntimeWarning)
+
+from twisted.python import randbytes
+
+
+def NS(t):
+ """
+ net string
+ """
+ return struct.pack('!L',len(t)) + t
+
+def getNS(s, count=1):
+ """
+ get net string
+ """
+ ns = []
+ c = 0
+ for i in range(count):
+ l, = struct.unpack('!L',s[c:c+4])
+ ns.append(s[c+4:4+l+c])
+ c += 4 + l
+ return tuple(ns) + (s[c:],)
+
+def MP(number):
+ if number==0: return '\000'*4
+ assert number>0
+ bn = Util.number.long_to_bytes(number)
+ if ord(bn[0])&128:
+ bn = '\000' + bn
+ return struct.pack('>L',len(bn)) + bn
+
+def getMP(data, count=1):
+ """
+ Get multiple precision integer out of the string. A multiple precision
+ integer is stored as a 4-byte length followed by length bytes of the
+ integer. If count is specified, get count integers out of the string.
+ The return value is a tuple of count integers followed by the rest of
+ the data.
+ """
+ mp = []
+ c = 0
+ for i in range(count):
+ length, = struct.unpack('>L',data[c:c+4])
+ mp.append(Util.number.bytes_to_long(data[c+4:c+4+length]))
+ c += 4 + length
+ return tuple(mp) + (data[c:],)
+
+def _MPpow(x, y, z):
+ """return the MP version of (x**y)%z
+ """
+ return MP(pow(x,y,z))
+
+def ffs(c, s):
+ """
+ first from second
+ goes through the first list, looking for items in the second, returns the first one
+ """
+ for i in c:
+ if i in s: return i
+
+getMP_py = getMP
+MP_py = MP
+_MPpow_py = _MPpow
+pyPow = pow
+
+def _fastgetMP(data, count=1):
+ mp = []
+ c = 0
+ for i in range(count):
+ length = struct.unpack('!L', data[c:c+4])[0]
+ mp.append(long(gmpy.mpz(data[c + 4:c + 4 + length][::-1] + '\x00', 256)))
+ c += length + 4
+ return tuple(mp) + (data[c:],)
+
+def _fastMP(i):
+ i2 = gmpy.mpz(i).binary()[::-1]
+ return struct.pack('!L', len(i2)) + i2
+
+def _fastMPpow(x, y, z=None):
+ r = pyPow(gmpy.mpz(x),y,z).binary()[::-1]
+ return struct.pack('!L', len(r)) + r
+
+def install():
+ global getMP, MP, _MPpow
+ getMP = _fastgetMP
+ MP = _fastMP
+ _MPpow = _fastMPpow
+ # XXX: We override builtin pow so that PyCrypto can benefit from gmpy too.
+ def _fastpow(x, y, z=None, mpz=gmpy.mpz):
+ if type(x) in (long, int):
+ x = mpz(x)
+ return pyPow(x, y, z)
+ __builtins__['pow'] = _fastpow # evil evil
+
+try:
+ import gmpy
+ install()
+except ImportError:
+ pass
+
diff --git a/twisted/conch/ssh/connection.py b/twisted/conch/ssh/connection.py
new file mode 100644
index 0000000..53c9cf7
--- /dev/null
+++ b/twisted/conch/ssh/connection.py
@@ -0,0 +1,637 @@
+# -*- test-case-name: twisted.conch.test.test_connection -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module contains the implementation of the ssh-connection service, which
+allows access to the shell and port-forwarding.
+
+Maintainer: Paul Swartz
+"""
+
+import struct
+
+from twisted.conch.ssh import service, common
+from twisted.conch import error
+from twisted.internet import defer
+from twisted.python import log
+
+class SSHConnection(service.SSHService):
+ """
+ An implementation of the 'ssh-connection' service. It is used to
+ multiplex multiple channels over the single SSH connection.
+
+ @ivar localChannelID: the next number to use as a local channel ID.
+ @type localChannelID: C{int}
+ @ivar channels: a C{dict} mapping a local channel ID to C{SSHChannel}
+ subclasses.
+ @type channels: C{dict}
+ @ivar localToRemoteChannel: a C{dict} mapping a local channel ID to a
+ remote channel ID.
+ @type localToRemoteChannel: C{dict}
+ @ivar channelsToRemoteChannel: a C{dict} mapping a C{SSHChannel} subclass
+ to remote channel ID.
+ @type channelsToRemoteChannel: C{dict}
+ @ivar deferreds: a C{dict} mapping a local channel ID to a C{list} of
+ C{Deferreds} for outstanding channel requests. Also, the 'global'
+ key stores the C{list} of pending global request C{Deferred}s.
+ """
+ name = 'ssh-connection'
+
+ def __init__(self):
+ self.localChannelID = 0 # this is the current # to use for channel ID
+ self.localToRemoteChannel = {} # local channel ID -> remote channel ID
+ self.channels = {} # local channel ID -> subclass of SSHChannel
+ self.channelsToRemoteChannel = {} # subclass of SSHChannel ->
+ # remote channel ID
+ self.deferreds = {"global": []} # local channel -> list of deferreds
+ # for pending requests or 'global' -> list of
+ # deferreds for global requests
+ self.transport = None # gets set later
+
+
+ def serviceStarted(self):
+ if hasattr(self.transport, 'avatar'):
+ self.transport.avatar.conn = self
+
+
+ def serviceStopped(self):
+ """
+ Called when the connection is stopped.
+ """
+ map(self.channelClosed, self.channels.values())
+ self._cleanupGlobalDeferreds()
+
+
+ def _cleanupGlobalDeferreds(self):
+ """
+ All pending requests that have returned a deferred must be errbacked
+ when this service is stopped, otherwise they might be left uncalled and
+ uncallable.
+ """
+ for d in self.deferreds["global"]:
+ d.errback(error.ConchError("Connection stopped."))
+ del self.deferreds["global"][:]
+
+
+ # packet methods
+ def ssh_GLOBAL_REQUEST(self, packet):
+ """
+ The other side has made a global request. Payload::
+ string request type
+ bool want reply
+ <request specific data>
+
+ This dispatches to self.gotGlobalRequest.
+ """
+ requestType, rest = common.getNS(packet)
+ wantReply, rest = ord(rest[0]), rest[1:]
+ ret = self.gotGlobalRequest(requestType, rest)
+ if wantReply:
+ reply = MSG_REQUEST_FAILURE
+ data = ''
+ if ret:
+ reply = MSG_REQUEST_SUCCESS
+ if isinstance(ret, (tuple, list)):
+ data = ret[1]
+ self.transport.sendPacket(reply, data)
+
+ def ssh_REQUEST_SUCCESS(self, packet):
+ """
+ Our global request succeeded. Get the appropriate Deferred and call
+ it back with the packet we received.
+ """
+ log.msg('RS')
+ self.deferreds['global'].pop(0).callback(packet)
+
+ def ssh_REQUEST_FAILURE(self, packet):
+ """
+ Our global request failed. Get the appropriate Deferred and errback
+ it with the packet we received.
+ """
+ log.msg('RF')
+ self.deferreds['global'].pop(0).errback(
+ error.ConchError('global request failed', packet))
+
+ def ssh_CHANNEL_OPEN(self, packet):
+ """
+ The other side wants to get a channel. Payload::
+ string channel name
+ uint32 remote channel number
+ uint32 remote window size
+ uint32 remote maximum packet size
+ <channel specific data>
+
+ We get a channel from self.getChannel(), give it a local channel number
+ and notify the other side. Then notify the channel by calling its
+ channelOpen method.
+ """
+ channelType, rest = common.getNS(packet)
+ senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[:12])
+ packet = rest[12:]
+ try:
+ channel = self.getChannel(channelType, windowSize, maxPacket,
+ packet)
+ localChannel = self.localChannelID
+ self.localChannelID += 1
+ channel.id = localChannel
+ self.channels[localChannel] = channel
+ self.channelsToRemoteChannel[channel] = senderChannel
+ self.localToRemoteChannel[localChannel] = senderChannel
+ self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION,
+ struct.pack('>4L', senderChannel, localChannel,
+ channel.localWindowSize,
+ channel.localMaxPacket)+channel.specificData)
+ log.callWithLogger(channel, channel.channelOpen, packet)
+ except Exception, e:
+ log.msg('channel open failed')
+ log.err(e)
+ if isinstance(e, error.ConchError):
+ textualInfo, reason = e.args
+ if isinstance(textualInfo, (int, long)):
+ # See #3657 and #3071
+ textualInfo, reason = reason, textualInfo
+ else:
+ reason = OPEN_CONNECT_FAILED
+ textualInfo = "unknown failure"
+ self.transport.sendPacket(
+ MSG_CHANNEL_OPEN_FAILURE,
+ struct.pack('>2L', senderChannel, reason) +
+ common.NS(textualInfo) + common.NS(''))
+
+ def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
+ """
+ The other side accepted our MSG_CHANNEL_OPEN request. Payload::
+ uint32 local channel number
+ uint32 remote channel number
+ uint32 remote window size
+ uint32 remote maximum packet size
+ <channel specific data>
+
+ Find the channel using the local channel number and notify its
+ channelOpen method.
+ """
+ (localChannel, remoteChannel, windowSize,
+ maxPacket) = struct.unpack('>4L', packet[: 16])
+ specificData = packet[16:]
+ channel = self.channels[localChannel]
+ channel.conn = self
+ self.localToRemoteChannel[localChannel] = remoteChannel
+ self.channelsToRemoteChannel[channel] = remoteChannel
+ channel.remoteWindowLeft = windowSize
+ channel.remoteMaxPacket = maxPacket
+ log.callWithLogger(channel, channel.channelOpen, specificData)
+
+ def ssh_CHANNEL_OPEN_FAILURE(self, packet):
+ """
+ The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
+ uint32 local channel number
+ uint32 reason code
+ string reason description
+
+ Find the channel using the local channel number and notify it by
+ calling its openFailed() method.
+ """
+ localChannel, reasonCode = struct.unpack('>2L', packet[:8])
+ reasonDesc = common.getNS(packet[8:])[0]
+ channel = self.channels[localChannel]
+ del self.channels[localChannel]
+ channel.conn = self
+ reason = error.ConchError(reasonDesc, reasonCode)
+ log.callWithLogger(channel, channel.openFailed, reason)
+
+ def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
+ """
+ The other side is adding bytes to its window. Payload::
+ uint32 local channel number
+ uint32 bytes to add
+
+ Call the channel's addWindowBytes() method to add new bytes to the
+ remote window.
+ """
+ localChannel, bytesToAdd = struct.unpack('>2L', packet[:8])
+ channel = self.channels[localChannel]
+ log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd)
+
+ def ssh_CHANNEL_DATA(self, packet):
+ """
+ The other side is sending us data. Payload::
+ uint32 local channel number
+ string data
+
+ Check to make sure the other side hasn't sent too much data (more
+ than what's in the window, or more than the maximum packet size). If
+ they have, close the channel. Otherwise, decrease the available
+ window and pass the data to the channel's dataReceived().
+ """
+ localChannel, dataLength = struct.unpack('>2L', packet[:8])
+ channel = self.channels[localChannel]
+ # XXX should this move to dataReceived to put client in charge?
+ if (dataLength > channel.localWindowLeft or
+ dataLength > channel.localMaxPacket): # more data than we want
+ log.callWithLogger(channel, log.msg, 'too much data')
+ self.sendClose(channel)
+ return
+ #packet = packet[:channel.localWindowLeft+4]
+ data = common.getNS(packet[4:])[0]
+ channel.localWindowLeft -= dataLength
+ if channel.localWindowLeft < channel.localWindowSize / 2:
+ self.adjustWindow(channel, channel.localWindowSize - \
+ channel.localWindowLeft)
+ #log.msg('local window left: %s/%s' % (channel.localWindowLeft,
+ # channel.localWindowSize))
+ log.callWithLogger(channel, channel.dataReceived, data)
+
+ def ssh_CHANNEL_EXTENDED_DATA(self, packet):
+ """
+ The other side is sending us exteneded data. Payload::
+ uint32 local channel number
+ uint32 type code
+ string data
+
+ Check to make sure the other side hasn't sent too much data (more
+ than what's in the window, or or than the maximum packet size). If
+ they have, close the channel. Otherwise, decrease the available
+ window and pass the data and type code to the channel's
+ extReceived().
+ """
+ localChannel, typeCode, dataLength = struct.unpack('>3L', packet[:12])
+ channel = self.channels[localChannel]
+ if (dataLength > channel.localWindowLeft or
+ dataLength > channel.localMaxPacket):
+ log.callWithLogger(channel, log.msg, 'too much extdata')
+ self.sendClose(channel)
+ return
+ data = common.getNS(packet[8:])[0]
+ channel.localWindowLeft -= dataLength
+ if channel.localWindowLeft < channel.localWindowSize / 2:
+ self.adjustWindow(channel, channel.localWindowSize -
+ channel.localWindowLeft)
+ log.callWithLogger(channel, channel.extReceived, typeCode, data)
+
+ def ssh_CHANNEL_EOF(self, packet):
+ """
+ The other side is not sending any more data. Payload::
+ uint32 local channel number
+
+ Notify the channel by calling its eofReceived() method.
+ """
+ localChannel = struct.unpack('>L', packet[:4])[0]
+ channel = self.channels[localChannel]
+ log.callWithLogger(channel, channel.eofReceived)
+
+ def ssh_CHANNEL_CLOSE(self, packet):
+ """
+ The other side is closing its end; it does not want to receive any
+ more data. Payload::
+ uint32 local channel number
+
+ Notify the channnel by calling its closeReceived() method. If
+ the channel has also sent a close message, call self.channelClosed().
+ """
+ localChannel = struct.unpack('>L', packet[:4])[0]
+ channel = self.channels[localChannel]
+ log.callWithLogger(channel, channel.closeReceived)
+ channel.remoteClosed = True
+ if channel.localClosed and channel.remoteClosed:
+ self.channelClosed(channel)
+
+ def ssh_CHANNEL_REQUEST(self, packet):
+ """
+ The other side is sending a request to a channel. Payload::
+ uint32 local channel number
+ string request name
+ bool want reply
+ <request specific data>
+
+ Pass the message to the channel's requestReceived method. If the
+ other side wants a reply, add callbacks which will send the
+ reply.
+ """
+ localChannel = struct.unpack('>L', packet[: 4])[0]
+ requestType, rest = common.getNS(packet[4:])
+ wantReply = ord(rest[0])
+ channel = self.channels[localChannel]
+ d = defer.maybeDeferred(log.callWithLogger, channel,
+ channel.requestReceived, requestType, rest[1:])
+ if wantReply:
+ d.addCallback(self._cbChannelRequest, localChannel)
+ d.addErrback(self._ebChannelRequest, localChannel)
+ return d
+
+ def _cbChannelRequest(self, result, localChannel):
+ """
+ Called back if the other side wanted a reply to a channel request. If
+ the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
+ a C{error.ConchError}
+
+ @param result: the value returned from the channel's requestReceived()
+ method. If it's False, the request failed.
+ @type result: C{bool}
+ @param localChannel: the local channel ID of the channel to which the
+ request was made.
+ @type localChannel: C{int}
+ @raises ConchError: if the result is False.
+ """
+ if not result:
+ raise error.ConchError('failed request')
+ self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L',
+ self.localToRemoteChannel[localChannel]))
+
+ def _ebChannelRequest(self, result, localChannel):
+ """
+ Called if the other wisde wanted a reply to the channel requeset and
+ the channel request failed.
+
+ @param result: a Failure, but it's not used.
+ @param localChannel: the local channel ID of the channel to which the
+ request was made.
+ @type localChannel: C{int}
+ """
+ self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L',
+ self.localToRemoteChannel[localChannel]))
+
+ def ssh_CHANNEL_SUCCESS(self, packet):
+ """
+ Our channel request to the other other side succeeded. Payload::
+ uint32 local channel number
+
+ Get the C{Deferred} out of self.deferreds and call it back.
+ """
+ localChannel = struct.unpack('>L', packet[:4])[0]
+ if self.deferreds.get(localChannel):
+ d = self.deferreds[localChannel].pop(0)
+ log.callWithLogger(self.channels[localChannel],
+ d.callback, '')
+
+ def ssh_CHANNEL_FAILURE(self, packet):
+ """
+ Our channel request to the other side failed. Payload::
+ uint32 local channel number
+
+ Get the C{Deferred} out of self.deferreds and errback it with a
+ C{error.ConchError}.
+ """
+ localChannel = struct.unpack('>L', packet[:4])[0]
+ if self.deferreds.get(localChannel):
+ d = self.deferreds[localChannel].pop(0)
+ log.callWithLogger(self.channels[localChannel],
+ d.errback,
+ error.ConchError('channel request failed'))
+
+ # methods for users of the connection to call
+
+ def sendGlobalRequest(self, request, data, wantReply=0):
+ """
+ Send a global request for this connection. Current this is only used
+ for remote->local TCP forwarding.
+
+ @type request: C{str}
+ @type data: C{str}
+ @type wantReply: C{bool}
+ @rtype C{Deferred}/C{None}
+ """
+ self.transport.sendPacket(MSG_GLOBAL_REQUEST,
+ common.NS(request)
+ + (wantReply and '\xff' or '\x00')
+ + data)
+ if wantReply:
+ d = defer.Deferred()
+ self.deferreds['global'].append(d)
+ return d
+
+ def openChannel(self, channel, extra=''):
+ """
+ Open a new channel on this connection.
+
+ @type channel: subclass of C{SSHChannel}
+ @type extra: C{str}
+ """
+ log.msg('opening channel %s with %s %s'%(self.localChannelID,
+ channel.localWindowSize, channel.localMaxPacket))
+ self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name)
+ + struct.pack('>3L', self.localChannelID,
+ channel.localWindowSize, channel.localMaxPacket)
+ + extra)
+ channel.id = self.localChannelID
+ self.channels[self.localChannelID] = channel
+ self.localChannelID += 1
+
+ def sendRequest(self, channel, requestType, data, wantReply=0):
+ """
+ Send a request to a channel.
+
+ @type channel: subclass of C{SSHChannel}
+ @type requestType: C{str}
+ @type data: C{str}
+ @type wantReply: C{bool}
+ @rtype C{Deferred}/C{None}
+ """
+ if channel.localClosed:
+ return
+ log.msg('sending request %s' % requestType)
+ self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L',
+ self.channelsToRemoteChannel[channel])
+ + common.NS(requestType)+chr(wantReply)
+ + data)
+ if wantReply:
+ d = defer.Deferred()
+ self.deferreds.setdefault(channel.id, []).append(d)
+ return d
+
+ def adjustWindow(self, channel, bytesToAdd):
+ """
+ Tell the other side that we will receive more data. This should not
+ normally need to be called as it is managed automatically.
+
+ @type channel: subclass of L{SSHChannel}
+ @type bytesToAdd: C{int}
+ """
+ if channel.localClosed:
+ return # we're already closed
+ self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L',
+ self.channelsToRemoteChannel[channel],
+ bytesToAdd))
+ log.msg('adding %i to %i in channel %i' % (bytesToAdd,
+ channel.localWindowLeft, channel.id))
+ channel.localWindowLeft += bytesToAdd
+
+ def sendData(self, channel, data):
+ """
+ Send data to a channel. This should not normally be used: instead use
+ channel.write(data) as it manages the window automatically.
+
+ @type channel: subclass of L{SSHChannel}
+ @type data: C{str}
+ """
+ if channel.localClosed:
+ return # we're already closed
+ self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L',
+ self.channelsToRemoteChannel[channel]) +
+ common.NS(data))
+
+ def sendExtendedData(self, channel, dataType, data):
+ """
+ Send extended data to a channel. This should not normally be used:
+ instead use channel.writeExtendedData(data, dataType) as it manages
+ the window automatically.
+
+ @type channel: subclass of L{SSHChannel}
+ @type dataType: C{int}
+ @type data: C{str}
+ """
+ if channel.localClosed:
+ return # we're already closed
+ self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L',
+ self.channelsToRemoteChannel[channel],dataType) \
+ + common.NS(data))
+
+ def sendEOF(self, channel):
+ """
+ Send an EOF (End of File) for a channel.
+
+ @type channel: subclass of L{SSHChannel}
+ """
+ if channel.localClosed:
+ return # we're already closed
+ log.msg('sending eof')
+ self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L',
+ self.channelsToRemoteChannel[channel]))
+
+ def sendClose(self, channel):
+ """
+ Close a channel.
+
+ @type channel: subclass of L{SSHChannel}
+ """
+ if channel.localClosed:
+ return # we're already closed
+ log.msg('sending close %i' % channel.id)
+ self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L',
+ self.channelsToRemoteChannel[channel]))
+ channel.localClosed = True
+ if channel.localClosed and channel.remoteClosed:
+ self.channelClosed(channel)
+
+ # methods to override
+ def getChannel(self, channelType, windowSize, maxPacket, data):
+ """
+ The other side requested a channel of some sort.
+ channelType is the type of channel being requested,
+ windowSize is the initial size of the remote window,
+ maxPacket is the largest packet we should send,
+ data is any other packet data (often nothing).
+
+ We return a subclass of L{SSHChannel}.
+
+ By default, this dispatches to a method 'channel_channelType' with any
+ non-alphanumerics in the channelType replace with _'s. If it cannot
+ find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
+ The method is called with arguments of windowSize, maxPacket, data.
+
+ @type channelType: C{str}
+ @type windowSize: C{int}
+ @type maxPacket: C{int}
+ @type data: C{str}
+ @rtype: subclass of L{SSHChannel}/C{tuple}
+ """
+ log.msg('got channel %s request' % channelType)
+ if hasattr(self.transport, "avatar"): # this is a server!
+ chan = self.transport.avatar.lookupChannel(channelType,
+ windowSize,
+ maxPacket,
+ data)
+ else:
+ channelType = channelType.translate(TRANSLATE_TABLE)
+ f = getattr(self, 'channel_%s' % channelType, None)
+ if f is not None:
+ chan = f(windowSize, maxPacket, data)
+ else:
+ chan = None
+ if chan is None:
+ raise error.ConchError('unknown channel',
+ OPEN_UNKNOWN_CHANNEL_TYPE)
+ else:
+ chan.conn = self
+ return chan
+
+ def gotGlobalRequest(self, requestType, data):
+ """
+ We got a global request. pretty much, this is just used by the client
+ to request that we forward a port from the server to the client.
+ Returns either:
+ - 1: request accepted
+ - 1, <data>: request accepted with request specific data
+ - 0: request denied
+
+ By default, this dispatches to a method 'global_requestType' with
+ -'s in requestType replaced with _'s. The found method is passed data.
+ If this method cannot be found, this method returns 0. Otherwise, it
+ returns the return value of that method.
+
+ @type requestType: C{str}
+ @type data: C{str}
+ @rtype: C{int}/C{tuple}
+ """
+ log.msg('got global %s request' % requestType)
+ if hasattr(self.transport, 'avatar'): # this is a server!
+ return self.transport.avatar.gotGlobalRequest(requestType, data)
+
+ requestType = requestType.replace('-','_')
+ f = getattr(self, 'global_%s' % requestType, None)
+ if not f:
+ return 0
+ return f(data)
+
+ def channelClosed(self, channel):
+ """
+ Called when a channel is closed.
+ It clears the local state related to the channel, and calls
+ channel.closed().
+ MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
+ If you don't, things will break mysteriously.
+
+ @type channel: L{SSHChannel}
+ """
+ if channel in self.channelsToRemoteChannel: # actually open
+ channel.localClosed = channel.remoteClosed = True
+ del self.localToRemoteChannel[channel.id]
+ del self.channels[channel.id]
+ del self.channelsToRemoteChannel[channel]
+ for d in self.deferreds.setdefault(channel.id, []):
+ d.errback(error.ConchError("Channel closed."))
+ del self.deferreds[channel.id][:]
+ log.callWithLogger(channel, channel.closed)
+
+MSG_GLOBAL_REQUEST = 80
+MSG_REQUEST_SUCCESS = 81
+MSG_REQUEST_FAILURE = 82
+MSG_CHANNEL_OPEN = 90
+MSG_CHANNEL_OPEN_CONFIRMATION = 91
+MSG_CHANNEL_OPEN_FAILURE = 92
+MSG_CHANNEL_WINDOW_ADJUST = 93
+MSG_CHANNEL_DATA = 94
+MSG_CHANNEL_EXTENDED_DATA = 95
+MSG_CHANNEL_EOF = 96
+MSG_CHANNEL_CLOSE = 97
+MSG_CHANNEL_REQUEST = 98
+MSG_CHANNEL_SUCCESS = 99
+MSG_CHANNEL_FAILURE = 100
+
+OPEN_ADMINISTRATIVELY_PROHIBITED = 1
+OPEN_CONNECT_FAILED = 2
+OPEN_UNKNOWN_CHANNEL_TYPE = 3
+OPEN_RESOURCE_SHORTAGE = 4
+
+EXTENDED_DATA_STDERR = 1
+
+messages = {}
+for name, value in locals().copy().items():
+ if name[:4] == 'MSG_':
+ messages[value] = name # doesn't handle doubles
+
+import string
+alphanums = string.letters + string.digits
+TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_'
+ for i in range(256)])
+SSHConnection.protocolMessages = messages
diff --git a/twisted/conch/ssh/factory.py b/twisted/conch/ssh/factory.py
new file mode 100644
index 0000000..3c50932
--- /dev/null
+++ b/twisted/conch/ssh/factory.py
@@ -0,0 +1,141 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A Factory for SSH servers, along with an OpenSSHFactory to use the same
+data sources as OpenSSH.
+
+Maintainer: Paul Swartz
+"""
+
+from twisted.internet import protocol
+from twisted.python import log
+from twisted.python.reflect import qual
+
+from twisted.conch import error
+from twisted.conch.ssh import keys
+import transport, userauth, connection
+
+import random
+import warnings
+
+class SSHFactory(protocol.Factory):
+ """
+ A Factory for SSH servers.
+ """
+ protocol = transport.SSHServerTransport
+
+ services = {
+ 'ssh-userauth':userauth.SSHUserAuthServer,
+ 'ssh-connection':connection.SSHConnection
+ }
+ def startFactory(self):
+ """
+ Check for public and private keys.
+ """
+ if not hasattr(self,'publicKeys'):
+ self.publicKeys = self.getPublicKeys()
+ for keyType, value in self.publicKeys.items():
+ if isinstance(value, str):
+ warnings.warn("Returning a mapping from strings to "
+ "strings from getPublicKeys()/publicKeys (in %s) "
+ "is deprecated. Return a mapping from "
+ "strings to Key objects instead." %
+ (qual(self.__class__)),
+ DeprecationWarning, stacklevel=1)
+ self.publicKeys[keyType] = keys.Key.fromString(value)
+ if not hasattr(self,'privateKeys'):
+ self.privateKeys = self.getPrivateKeys()
+ for keyType, value in self.privateKeys.items():
+ if not isinstance(value, keys.Key):
+ warnings.warn("Returning a mapping from strings to "
+ "PyCrypto key objects from "
+ "getPrivateKeys()/privateKeys (in %s) "
+ "is deprecated. Return a mapping from "
+ "strings to Key objects instead." %
+ (qual(self.__class__),),
+ DeprecationWarning, stacklevel=1)
+ self.privateKeys[keyType] = keys.Key(value)
+ if not self.publicKeys or not self.privateKeys:
+ raise error.ConchError('no host keys, failing')
+ if not hasattr(self,'primes'):
+ self.primes = self.getPrimes()
+
+
+ def buildProtocol(self, addr):
+ """
+ Create an instance of the server side of the SSH protocol.
+
+ @type addr: L{twisted.internet.interfaces.IAddress} provider
+ @param addr: The address at which the server will listen.
+
+ @rtype: L{twisted.conch.ssh.SSHServerTransport}
+ @return: The built transport.
+ """
+ t = protocol.Factory.buildProtocol(self, addr)
+ t.supportedPublicKeys = self.privateKeys.keys()
+ if not self.primes:
+ log.msg('disabling diffie-hellman-group-exchange because we '
+ 'cannot find moduli file')
+ ske = t.supportedKeyExchanges[:]
+ ske.remove('diffie-hellman-group-exchange-sha1')
+ t.supportedKeyExchanges = ske
+ return t
+
+
+ def getPublicKeys(self):
+ """
+ Called when the factory is started to get the public portions of the
+ servers host keys. Returns a dictionary mapping SSH key types to
+ public key strings.
+
+ @rtype: C{dict}
+ """
+ raise NotImplementedError('getPublicKeys unimplemented')
+
+
+ def getPrivateKeys(self):
+ """
+ Called when the factory is started to get the private portions of the
+ servers host keys. Returns a dictionary mapping SSH key types to
+ C{Crypto.PublicKey.pubkey.pubkey} objects.
+
+ @rtype: C{dict}
+ """
+ raise NotImplementedError('getPrivateKeys unimplemented')
+
+
+ def getPrimes(self):
+ """
+ Called when the factory is started to get Diffie-Hellman generators and
+ primes to use. Returns a dictionary mapping number of bits to lists
+ of tuple of (generator, prime).
+
+ @rtype: C{dict}
+ """
+
+
+ def getDHPrime(self, bits):
+ """
+ Return a tuple of (g, p) for a Diffe-Hellman process, with p being as
+ close to bits bits as possible.
+
+ @type bits: C{int}
+ @rtype: C{tuple}
+ """
+ primesKeys = self.primes.keys()
+ primesKeys.sort(lambda x, y: cmp(abs(x - bits), abs(y - bits)))
+ realBits = primesKeys[0]
+ return random.choice(self.primes[realBits])
+
+
+ def getService(self, transport, service):
+ """
+ Return a class to use as a service for the given transport.
+
+ @type transport: L{transport.SSHServerTransport}
+ @type service: C{str}
+ @rtype: subclass of L{service.SSHService}
+ """
+ if service == 'ssh-userauth' or hasattr(transport, 'avatar'):
+ return self.services[service]
diff --git a/twisted/conch/ssh/filetransfer.py b/twisted/conch/ssh/filetransfer.py
new file mode 100644
index 0000000..9b11db0
--- /dev/null
+++ b/twisted/conch/ssh/filetransfer.py
@@ -0,0 +1,934 @@
+# -*- test-case-name: twisted.conch.test.test_filetransfer -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import struct, errno
+
+from twisted.internet import defer, protocol
+from twisted.python import failure, log
+
+from common import NS, getNS
+from twisted.conch.interfaces import ISFTPServer, ISFTPFile
+
+from zope import interface
+
+
+
+class FileTransferBase(protocol.Protocol):
+
+ versions = (3, )
+
+ packetTypes = {}
+
+ def __init__(self):
+ self.buf = ''
+ self.otherVersion = None # this gets set
+
+ def sendPacket(self, kind, data):
+ self.transport.write(struct.pack('!LB', len(data)+1, kind) + data)
+
+ def dataReceived(self, data):
+ self.buf += data
+ while len(self.buf) > 5:
+ length, kind = struct.unpack('!LB', self.buf[:5])
+ if len(self.buf) < 4 + length:
+ return
+ data, self.buf = self.buf[5:4+length], self.buf[4+length:]
+ packetType = self.packetTypes.get(kind, None)
+ if not packetType:
+ log.msg('no packet type for', kind)
+ continue
+ f = getattr(self, 'packet_%s' % packetType, None)
+ if not f:
+ log.msg('not implemented: %s' % packetType)
+ log.msg(repr(data[4:]))
+ reqId, = struct.unpack('!L', data[:4])
+ self._sendStatus(reqId, FX_OP_UNSUPPORTED,
+ "don't understand %s" % packetType)
+ #XXX not implemented
+ continue
+ try:
+ f(data)
+ except:
+ log.err()
+ continue
+ reqId ,= struct.unpack('!L', data[:4])
+ self._ebStatus(failure.Failure(e), reqId)
+
+ def _parseAttributes(self, data):
+ flags ,= struct.unpack('!L', data[:4])
+ attrs = {}
+ data = data[4:]
+ if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE:
+ size ,= struct.unpack('!Q', data[:8])
+ attrs['size'] = size
+ data = data[8:]
+ if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP:
+ uid, gid = struct.unpack('!2L', data[:8])
+ attrs['uid'] = uid
+ attrs['gid'] = gid
+ data = data[8:]
+ if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS:
+ perms ,= struct.unpack('!L', data[:4])
+ attrs['permissions'] = perms
+ data = data[4:]
+ if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME:
+ atime, mtime = struct.unpack('!2L', data[:8])
+ attrs['atime'] = atime
+ attrs['mtime'] = mtime
+ data = data[8:]
+ if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED:
+ extended_count ,= struct.unpack('!L', data[:4])
+ data = data[4:]
+ for i in xrange(extended_count):
+ extended_type, data = getNS(data)
+ extended_data, data = getNS(data)
+ attrs['ext_%s' % extended_type] = extended_data
+ return attrs, data
+
+ def _packAttributes(self, attrs):
+ flags = 0
+ data = ''
+ if 'size' in attrs:
+ data += struct.pack('!Q', attrs['size'])
+ flags |= FILEXFER_ATTR_SIZE
+ if 'uid' in attrs and 'gid' in attrs:
+ data += struct.pack('!2L', attrs['uid'], attrs['gid'])
+ flags |= FILEXFER_ATTR_OWNERGROUP
+ if 'permissions' in attrs:
+ data += struct.pack('!L', attrs['permissions'])
+ flags |= FILEXFER_ATTR_PERMISSIONS
+ if 'atime' in attrs and 'mtime' in attrs:
+ data += struct.pack('!2L', attrs['atime'], attrs['mtime'])
+ flags |= FILEXFER_ATTR_ACMODTIME
+ extended = []
+ for k in attrs:
+ if k.startswith('ext_'):
+ ext_type = NS(k[4:])
+ ext_data = NS(attrs[k])
+ extended.append(ext_type+ext_data)
+ if extended:
+ data += struct.pack('!L', len(extended))
+ data += ''.join(extended)
+ flags |= FILEXFER_ATTR_EXTENDED
+ return struct.pack('!L', flags) + data
+
+class FileTransferServer(FileTransferBase):
+
+ def __init__(self, data=None, avatar=None):
+ FileTransferBase.__init__(self)
+ self.client = ISFTPServer(avatar) # yay interfaces
+ self.openFiles = {}
+ self.openDirs = {}
+
+ def packet_INIT(self, data):
+ version ,= struct.unpack('!L', data[:4])
+ self.version = min(list(self.versions) + [version])
+ data = data[4:]
+ ext = {}
+ while data:
+ ext_name, data = getNS(data)
+ ext_data, data = getNS(data)
+ ext[ext_name] = ext_data
+ our_ext = self.client.gotVersion(version, ext)
+ our_ext_data = ""
+ for (k,v) in our_ext.items():
+ our_ext_data += NS(k) + NS(v)
+ self.sendPacket(FXP_VERSION, struct.pack('!L', self.version) + \
+ our_ext_data)
+
+ def packet_OPEN(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ filename, data = getNS(data)
+ flags ,= struct.unpack('!L', data[:4])
+ data = data[4:]
+ attrs, data = self._parseAttributes(data)
+ assert data == '', 'still have data in OPEN: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs)
+ d.addCallback(self._cbOpenFile, requestId)
+ d.addErrback(self._ebStatus, requestId, "open failed")
+
+ def _cbOpenFile(self, fileObj, requestId):
+ fileId = str(hash(fileObj))
+ if fileId in self.openFiles:
+ raise KeyError, 'id already open'
+ self.openFiles[fileId] = fileObj
+ self.sendPacket(FXP_HANDLE, requestId + NS(fileId))
+
+ def packet_CLOSE(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ assert data == '', 'still have data in CLOSE: %s' % repr(data)
+ if handle in self.openFiles:
+ fileObj = self.openFiles[handle]
+ d = defer.maybeDeferred(fileObj.close)
+ d.addCallback(self._cbClose, handle, requestId)
+ d.addErrback(self._ebStatus, requestId, "close failed")
+ elif handle in self.openDirs:
+ dirObj = self.openDirs[handle][0]
+ d = defer.maybeDeferred(dirObj.close)
+ d.addCallback(self._cbClose, handle, requestId, 1)
+ d.addErrback(self._ebStatus, requestId, "close failed")
+ else:
+ self._ebClose(failure.Failure(KeyError()), requestId)
+
+ def _cbClose(self, result, handle, requestId, isDir = 0):
+ if isDir:
+ del self.openDirs[handle]
+ else:
+ del self.openFiles[handle]
+ self._sendStatus(requestId, FX_OK, 'file closed')
+
+ def packet_READ(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ (offset, length), data = struct.unpack('!QL', data[:12]), data[12:]
+ assert data == '', 'still have data in READ: %s' % repr(data)
+ if handle not in self.openFiles:
+ self._ebRead(failure.Failure(KeyError()), requestId)
+ else:
+ fileObj = self.openFiles[handle]
+ d = defer.maybeDeferred(fileObj.readChunk, offset, length)
+ d.addCallback(self._cbRead, requestId)
+ d.addErrback(self._ebStatus, requestId, "read failed")
+
+ def _cbRead(self, result, requestId):
+ if result == '': # python's read will return this for EOF
+ raise EOFError()
+ self.sendPacket(FXP_DATA, requestId + NS(result))
+
+ def packet_WRITE(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ offset, = struct.unpack('!Q', data[:8])
+ data = data[8:]
+ writeData, data = getNS(data)
+ assert data == '', 'still have data in WRITE: %s' % repr(data)
+ if handle not in self.openFiles:
+ self._ebWrite(failure.Failure(KeyError()), requestId)
+ else:
+ fileObj = self.openFiles[handle]
+ d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData)
+ d.addCallback(self._cbStatus, requestId, "write succeeded")
+ d.addErrback(self._ebStatus, requestId, "write failed")
+
+ def packet_REMOVE(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ filename, data = getNS(data)
+ assert data == '', 'still have data in REMOVE: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.removeFile, filename)
+ d.addCallback(self._cbStatus, requestId, "remove succeeded")
+ d.addErrback(self._ebStatus, requestId, "remove failed")
+
+ def packet_RENAME(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ oldPath, data = getNS(data)
+ newPath, data = getNS(data)
+ assert data == '', 'still have data in RENAME: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath)
+ d.addCallback(self._cbStatus, requestId, "rename succeeded")
+ d.addErrback(self._ebStatus, requestId, "rename failed")
+
+ def packet_MKDIR(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ attrs, data = self._parseAttributes(data)
+ assert data == '', 'still have data in MKDIR: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.makeDirectory, path, attrs)
+ d.addCallback(self._cbStatus, requestId, "mkdir succeeded")
+ d.addErrback(self._ebStatus, requestId, "mkdir failed")
+
+ def packet_RMDIR(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ assert data == '', 'still have data in RMDIR: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.removeDirectory, path)
+ d.addCallback(self._cbStatus, requestId, "rmdir succeeded")
+ d.addErrback(self._ebStatus, requestId, "rmdir failed")
+
+ def packet_OPENDIR(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ assert data == '', 'still have data in OPENDIR: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.openDirectory, path)
+ d.addCallback(self._cbOpenDirectory, requestId)
+ d.addErrback(self._ebStatus, requestId, "opendir failed")
+
+ def _cbOpenDirectory(self, dirObj, requestId):
+ handle = str(hash(dirObj))
+ if handle in self.openDirs:
+ raise KeyError, "already opened this directory"
+ self.openDirs[handle] = [dirObj, iter(dirObj)]
+ self.sendPacket(FXP_HANDLE, requestId + NS(handle))
+
+ def packet_READDIR(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ assert data == '', 'still have data in READDIR: %s' % repr(data)
+ if handle not in self.openDirs:
+ self._ebStatus(failure.Failure(KeyError()), requestId)
+ else:
+ dirObj, dirIter = self.openDirs[handle]
+ d = defer.maybeDeferred(self._scanDirectory, dirIter, [])
+ d.addCallback(self._cbSendDirectory, requestId)
+ d.addErrback(self._ebStatus, requestId, "scan directory failed")
+
+ def _scanDirectory(self, dirIter, f):
+ while len(f) < 250:
+ try:
+ info = dirIter.next()
+ except StopIteration:
+ if not f:
+ raise EOFError
+ return f
+ if isinstance(info, defer.Deferred):
+ info.addCallback(self._cbScanDirectory, dirIter, f)
+ return
+ else:
+ f.append(info)
+ return f
+
+ def _cbScanDirectory(self, result, dirIter, f):
+ f.append(result)
+ return self._scanDirectory(dirIter, f)
+
+ def _cbSendDirectory(self, result, requestId):
+ data = ''
+ for (filename, longname, attrs) in result:
+ data += NS(filename)
+ data += NS(longname)
+ data += self._packAttributes(attrs)
+ self.sendPacket(FXP_NAME, requestId +
+ struct.pack('!L', len(result))+data)
+
+ def packet_STAT(self, data, followLinks = 1):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ assert data == '', 'still have data in STAT/LSTAT: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.getAttrs, path, followLinks)
+ d.addCallback(self._cbStat, requestId)
+ d.addErrback(self._ebStatus, requestId, 'stat/lstat failed')
+
+ def packet_LSTAT(self, data):
+ self.packet_STAT(data, 0)
+
+ def packet_FSTAT(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ assert data == '', 'still have data in FSTAT: %s' % repr(data)
+ if handle not in self.openFiles:
+ self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles'
+ % handle)), requestId)
+ else:
+ fileObj = self.openFiles[handle]
+ d = defer.maybeDeferred(fileObj.getAttrs)
+ d.addCallback(self._cbStat, requestId)
+ d.addErrback(self._ebStatus, requestId, 'fstat failed')
+
+ def _cbStat(self, result, requestId):
+ data = requestId + self._packAttributes(result)
+ self.sendPacket(FXP_ATTRS, data)
+
+ def packet_SETSTAT(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ attrs, data = self._parseAttributes(data)
+ if data != '':
+ log.msg('WARN: still have data in SETSTAT: %s' % repr(data))
+ d = defer.maybeDeferred(self.client.setAttrs, path, attrs)
+ d.addCallback(self._cbStatus, requestId, 'setstat succeeded')
+ d.addErrback(self._ebStatus, requestId, 'setstat failed')
+
+ def packet_FSETSTAT(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ handle, data = getNS(data)
+ attrs, data = self._parseAttributes(data)
+ assert data == '', 'still have data in FSETSTAT: %s' % repr(data)
+ if handle not in self.openFiles:
+ self._ebStatus(failure.Failure(KeyError()), requestId)
+ else:
+ fileObj = self.openFiles[handle]
+ d = defer.maybeDeferred(fileObj.setAttrs, attrs)
+ d.addCallback(self._cbStatus, requestId, 'fsetstat succeeded')
+ d.addErrback(self._ebStatus, requestId, 'fsetstat failed')
+
+ def packet_READLINK(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ assert data == '', 'still have data in READLINK: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.readLink, path)
+ d.addCallback(self._cbReadLink, requestId)
+ d.addErrback(self._ebStatus, requestId, 'readlink failed')
+
+ def _cbReadLink(self, result, requestId):
+ self._cbSendDirectory([(result, '', {})], requestId)
+
+ def packet_SYMLINK(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ linkPath, data = getNS(data)
+ targetPath, data = getNS(data)
+ d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath)
+ d.addCallback(self._cbStatus, requestId, 'symlink succeeded')
+ d.addErrback(self._ebStatus, requestId, 'symlink failed')
+
+ def packet_REALPATH(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ path, data = getNS(data)
+ assert data == '', 'still have data in REALPATH: %s' % repr(data)
+ d = defer.maybeDeferred(self.client.realPath, path)
+ d.addCallback(self._cbReadLink, requestId) # same return format
+ d.addErrback(self._ebStatus, requestId, 'realpath failed')
+
+ def packet_EXTENDED(self, data):
+ requestId = data[:4]
+ data = data[4:]
+ extName, extData = getNS(data)
+ d = defer.maybeDeferred(self.client.extendedRequest, extName, extData)
+ d.addCallback(self._cbExtended, requestId)
+ d.addErrback(self._ebStatus, requestId, 'extended %s failed' % extName)
+
+ def _cbExtended(self, data, requestId):
+ self.sendPacket(FXP_EXTENDED_REPLY, requestId + data)
+
+ def _cbStatus(self, result, requestId, msg = "request succeeded"):
+ self._sendStatus(requestId, FX_OK, msg)
+
+ def _ebStatus(self, reason, requestId, msg = "request failed"):
+ code = FX_FAILURE
+ message = msg
+ if reason.type in (IOError, OSError):
+ if reason.value.errno == errno.ENOENT: # no such file
+ code = FX_NO_SUCH_FILE
+ message = reason.value.strerror
+ elif reason.value.errno == errno.EACCES: # permission denied
+ code = FX_PERMISSION_DENIED
+ message = reason.value.strerror
+ elif reason.value.errno == errno.EEXIST:
+ code = FX_FILE_ALREADY_EXISTS
+ else:
+ log.err(reason)
+ elif reason.type == EOFError: # EOF
+ code = FX_EOF
+ if reason.value.args:
+ message = reason.value.args[0]
+ elif reason.type == NotImplementedError:
+ code = FX_OP_UNSUPPORTED
+ if reason.value.args:
+ message = reason.value.args[0]
+ elif reason.type == SFTPError:
+ code = reason.value.code
+ message = reason.value.message
+ else:
+ log.err(reason)
+ self._sendStatus(requestId, code, message)
+
+ def _sendStatus(self, requestId, code, message, lang = ''):
+ """
+ Helper method to send a FXP_STATUS message.
+ """
+ data = requestId + struct.pack('!L', code)
+ data += NS(message)
+ data += NS(lang)
+ self.sendPacket(FXP_STATUS, data)
+
+
+ def connectionLost(self, reason):
+ """
+ Clean all opened files and directories.
+ """
+ for fileObj in self.openFiles.values():
+ fileObj.close()
+ self.openFiles = {}
+ for (dirObj, dirIter) in self.openDirs.values():
+ dirObj.close()
+ self.openDirs = {}
+
+
+
+class FileTransferClient(FileTransferBase):
+
+ def __init__(self, extData = {}):
+ """
+ @param extData: a dict of extended_name : extended_data items
+ to be sent to the server.
+ """
+ FileTransferBase.__init__(self)
+ self.extData = {}
+ self.counter = 0
+ self.openRequests = {} # id -> Deferred
+ self.wasAFile = {} # Deferred -> 1 TERRIBLE HACK
+
+ def connectionMade(self):
+ data = struct.pack('!L', max(self.versions))
+ for k,v in self.extData.itervalues():
+ data += NS(k) + NS(v)
+ self.sendPacket(FXP_INIT, data)
+
+ def _sendRequest(self, msg, data):
+ data = struct.pack('!L', self.counter) + data
+ d = defer.Deferred()
+ self.openRequests[self.counter] = d
+ self.counter += 1
+ self.sendPacket(msg, data)
+ return d
+
+ def _parseRequest(self, data):
+ (id,) = struct.unpack('!L', data[:4])
+ d = self.openRequests[id]
+ del self.openRequests[id]
+ return d, data[4:]
+
+ def openFile(self, filename, flags, attrs):
+ """
+ Open a file.
+
+ This method returns a L{Deferred} that is called back with an object
+ that provides the L{ISFTPFile} interface.
+
+ @param filename: a string representing the file to open.
+
+ @param flags: a integer of the flags to open the file with, ORed together.
+ The flags and their values are listed at the bottom of this file.
+
+ @param attrs: a list of attributes to open the file with. It is a
+ dictionary, consisting of 0 or more keys. The possible keys are::
+
+ size: the size of the file in bytes
+ uid: the user ID of the file as an integer
+ gid: the group ID of the file as an integer
+ permissions: the permissions of the file with as an integer.
+ the bit representation of this field is defined by POSIX.
+ atime: the access time of the file as seconds since the epoch.
+ mtime: the modification time of the file as seconds since the epoch.
+ ext_*: extended attributes. The server is not required to
+ understand this, but it may.
+
+ NOTE: there is no way to indicate text or binary files. it is up
+ to the SFTP client to deal with this.
+ """
+ data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(attrs)
+ d = self._sendRequest(FXP_OPEN, data)
+ self.wasAFile[d] = (1, filename) # HACK
+ return d
+
+ def removeFile(self, filename):
+ """
+ Remove the given file.
+
+ This method returns a Deferred that is called back when it succeeds.
+
+ @param filename: the name of the file as a string.
+ """
+ return self._sendRequest(FXP_REMOVE, NS(filename))
+
+ def renameFile(self, oldpath, newpath):
+ """
+ Rename the given file.
+
+ This method returns a Deferred that is called back when it succeeds.
+
+ @param oldpath: the current location of the file.
+ @param newpath: the new file name.
+ """
+ return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath))
+
+ def makeDirectory(self, path, attrs):
+ """
+ Make a directory.
+
+ This method returns a Deferred that is called back when it is
+ created.
+
+ @param path: the name of the directory to create as a string.
+
+ @param attrs: a dictionary of attributes to create the directory
+ with. Its meaning is the same as the attrs in the openFile method.
+ """
+ return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs))
+
+ def removeDirectory(self, path):
+ """
+ Remove a directory (non-recursively)
+
+ It is an error to remove a directory that has files or directories in
+ it.
+
+ This method returns a Deferred that is called back when it is removed.
+
+ @param path: the directory to remove.
+ """
+ return self._sendRequest(FXP_RMDIR, NS(path))
+
+ def openDirectory(self, path):
+ """
+ Open a directory for scanning.
+
+ This method returns a Deferred that is called back with an iterable
+ object that has a close() method.
+
+ The close() method is called when the client is finished reading
+ from the directory. At this point, the iterable will no longer
+ be used.
+
+ The iterable returns triples of the form (filename, longname, attrs)
+ or a Deferred that returns the same. The sequence must support
+ __getitem__, but otherwise may be any 'sequence-like' object.
+
+ filename is the name of the file relative to the directory.
+ logname is an expanded format of the filename. The recommended format
+ is:
+ -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
+ 1234567890 123 12345678 12345678 12345678 123456789012
+
+ The first line is sample output, the second is the length of the field.
+ The fields are: permissions, link count, user owner, group owner,
+ size in bytes, modification time.
+
+ attrs is a dictionary in the format of the attrs argument to openFile.
+
+ @param path: the directory to open.
+ """
+ d = self._sendRequest(FXP_OPENDIR, NS(path))
+ self.wasAFile[d] = (0, path)
+ return d
+
+ def getAttrs(self, path, followLinks=0):
+ """
+ Return the attributes for the given path.
+
+ This method returns a dictionary in the same format as the attrs
+ argument to openFile or a Deferred that is called back with same.
+
+ @param path: the path to return attributes for as a string.
+ @param followLinks: a boolean. if it is True, follow symbolic links
+ and return attributes for the real path at the base. if it is False,
+ return attributes for the specified path.
+ """
+ if followLinks: m = FXP_STAT
+ else: m = FXP_LSTAT
+ return self._sendRequest(m, NS(path))
+
+ def setAttrs(self, path, attrs):
+ """
+ Set the attributes for the path.
+
+ This method returns when the attributes are set or a Deferred that is
+ called back when they are.
+
+ @param path: the path to set attributes for as a string.
+ @param attrs: a dictionary in the same format as the attrs argument to
+ openFile.
+ """
+ data = NS(path) + self._packAttributes(attrs)
+ return self._sendRequest(FXP_SETSTAT, data)
+
+ def readLink(self, path):
+ """
+ Find the root of a set of symbolic links.
+
+ This method returns the target of the link, or a Deferred that
+ returns the same.
+
+ @param path: the path of the symlink to read.
+ """
+ d = self._sendRequest(FXP_READLINK, NS(path))
+ return d.addCallback(self._cbRealPath)
+
+ def makeLink(self, linkPath, targetPath):
+ """
+ Create a symbolic link.
+
+ This method returns when the link is made, or a Deferred that
+ returns the same.
+
+ @param linkPath: the pathname of the symlink as a string
+ @param targetPath: the path of the target of the link as a string.
+ """
+ return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath))
+
+ def realPath(self, path):
+ """
+ Convert any path to an absolute path.
+
+ This method returns the absolute path as a string, or a Deferred
+ that returns the same.
+
+ @param path: the path to convert as a string.
+ """
+ d = self._sendRequest(FXP_REALPATH, NS(path))
+ return d.addCallback(self._cbRealPath)
+
+ def _cbRealPath(self, result):
+ name, longname, attrs = result[0]
+ return name
+
+ def extendedRequest(self, request, data):
+ """
+ Make an extended request of the server.
+
+ The method returns a Deferred that is called back with
+ the result of the extended request.
+
+ @param request: the name of the extended request to make.
+ @param data: any other data that goes along with the request.
+ """
+ return self._sendRequest(FXP_EXTENDED, NS(request) + data)
+
+ def packet_VERSION(self, data):
+ version, = struct.unpack('!L', data[:4])
+ data = data[4:]
+ d = {}
+ while data:
+ k, data = getNS(data)
+ v, data = getNS(data)
+ d[k]=v
+ self.version = version
+ self.gotServerVersion(version, d)
+
+ def packet_STATUS(self, data):
+ d, data = self._parseRequest(data)
+ code, = struct.unpack('!L', data[:4])
+ data = data[4:]
+ if len(data) >= 4:
+ msg, data = getNS(data)
+ if len(data) >= 4:
+ lang, data = getNS(data)
+ else:
+ lang = ''
+ else:
+ msg = ''
+ lang = ''
+ if code == FX_OK:
+ d.callback((msg, lang))
+ elif code == FX_EOF:
+ d.errback(EOFError(msg))
+ elif code == FX_OP_UNSUPPORTED:
+ d.errback(NotImplementedError(msg))
+ else:
+ d.errback(SFTPError(code, msg, lang))
+
+ def packet_HANDLE(self, data):
+ d, data = self._parseRequest(data)
+ isFile, name = self.wasAFile.pop(d)
+ if isFile:
+ cb = ClientFile(self, getNS(data)[0])
+ else:
+ cb = ClientDirectory(self, getNS(data)[0])
+ cb.name = name
+ d.callback(cb)
+
+ def packet_DATA(self, data):
+ d, data = self._parseRequest(data)
+ d.callback(getNS(data)[0])
+
+ def packet_NAME(self, data):
+ d, data = self._parseRequest(data)
+ count, = struct.unpack('!L', data[:4])
+ data = data[4:]
+ files = []
+ for i in range(count):
+ filename, data = getNS(data)
+ longname, data = getNS(data)
+ attrs, data = self._parseAttributes(data)
+ files.append((filename, longname, attrs))
+ d.callback(files)
+
+ def packet_ATTRS(self, data):
+ d, data = self._parseRequest(data)
+ d.callback(self._parseAttributes(data)[0])
+
+ def packet_EXTENDED_REPLY(self, data):
+ d, data = self._parseRequest(data)
+ d.callback(data)
+
+ def gotServerVersion(self, serverVersion, extData):
+ """
+ Called when the client sends their version info.
+
+ @param otherVersion: an integer representing the version of the SFTP
+ protocol they are claiming.
+ @param extData: a dictionary of extended_name : extended_data items.
+ These items are sent by the client to indicate additional features.
+ """
+
+class ClientFile:
+
+ interface.implements(ISFTPFile)
+
+ def __init__(self, parent, handle):
+ self.parent = parent
+ self.handle = NS(handle)
+
+ def close(self):
+ return self.parent._sendRequest(FXP_CLOSE, self.handle)
+
+ def readChunk(self, offset, length):
+ data = self.handle + struct.pack("!QL", offset, length)
+ return self.parent._sendRequest(FXP_READ, data)
+
+ def writeChunk(self, offset, chunk):
+ data = self.handle + struct.pack("!Q", offset) + NS(chunk)
+ return self.parent._sendRequest(FXP_WRITE, data)
+
+ def getAttrs(self):
+ return self.parent._sendRequest(FXP_FSTAT, self.handle)
+
+ def setAttrs(self, attrs):
+ data = self.handle + self.parent._packAttributes(attrs)
+ return self.parent._sendRequest(FXP_FSTAT, data)
+
+class ClientDirectory:
+
+ def __init__(self, parent, handle):
+ self.parent = parent
+ self.handle = NS(handle)
+ self.filesCache = []
+
+ def read(self):
+ d = self.parent._sendRequest(FXP_READDIR, self.handle)
+ return d
+
+ def close(self):
+ return self.parent._sendRequest(FXP_CLOSE, self.handle)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if self.filesCache:
+ return self.filesCache.pop(0)
+ d = self.read()
+ d.addCallback(self._cbReadDir)
+ d.addErrback(self._ebReadDir)
+ return d
+
+ def _cbReadDir(self, names):
+ self.filesCache = names[1:]
+ return names[0]
+
+ def _ebReadDir(self, reason):
+ reason.trap(EOFError)
+ def _():
+ raise StopIteration
+ self.next = _
+ return reason
+
+
+class SFTPError(Exception):
+
+ def __init__(self, errorCode, errorMessage, lang = ''):
+ Exception.__init__(self)
+ self.code = errorCode
+ self._message = errorMessage
+ self.lang = lang
+
+
+ def message(self):
+ """
+ A string received over the network that explains the error to a human.
+ """
+ # Python 2.6 deprecates assigning to the 'message' attribute of an
+ # exception. We define this read-only property here in order to
+ # prevent the warning about deprecation while maintaining backwards
+ # compatibility with object clients that rely on the 'message'
+ # attribute being set correctly. See bug #3897.
+ return self._message
+ message = property(message)
+
+
+ def __str__(self):
+ return 'SFTPError %s: %s' % (self.code, self.message)
+
+FXP_INIT = 1
+FXP_VERSION = 2
+FXP_OPEN = 3
+FXP_CLOSE = 4
+FXP_READ = 5
+FXP_WRITE = 6
+FXP_LSTAT = 7
+FXP_FSTAT = 8
+FXP_SETSTAT = 9
+FXP_FSETSTAT = 10
+FXP_OPENDIR = 11
+FXP_READDIR = 12
+FXP_REMOVE = 13
+FXP_MKDIR = 14
+FXP_RMDIR = 15
+FXP_REALPATH = 16
+FXP_STAT = 17
+FXP_RENAME = 18
+FXP_READLINK = 19
+FXP_SYMLINK = 20
+FXP_STATUS = 101
+FXP_HANDLE = 102
+FXP_DATA = 103
+FXP_NAME = 104
+FXP_ATTRS = 105
+FXP_EXTENDED = 200
+FXP_EXTENDED_REPLY = 201
+
+FILEXFER_ATTR_SIZE = 0x00000001
+FILEXFER_ATTR_UIDGID = 0x00000002
+FILEXFER_ATTR_OWNERGROUP = FILEXFER_ATTR_UIDGID
+FILEXFER_ATTR_PERMISSIONS = 0x00000004
+FILEXFER_ATTR_ACMODTIME = 0x00000008
+FILEXFER_ATTR_EXTENDED = 0x80000000L
+
+FILEXFER_TYPE_REGULAR = 1
+FILEXFER_TYPE_DIRECTORY = 2
+FILEXFER_TYPE_SYMLINK = 3
+FILEXFER_TYPE_SPECIAL = 4
+FILEXFER_TYPE_UNKNOWN = 5
+
+FXF_READ = 0x00000001
+FXF_WRITE = 0x00000002
+FXF_APPEND = 0x00000004
+FXF_CREAT = 0x00000008
+FXF_TRUNC = 0x00000010
+FXF_EXCL = 0x00000020
+FXF_TEXT = 0x00000040
+
+FX_OK = 0
+FX_EOF = 1
+FX_NO_SUCH_FILE = 2
+FX_PERMISSION_DENIED = 3
+FX_FAILURE = 4
+FX_BAD_MESSAGE = 5
+FX_NO_CONNECTION = 6
+FX_CONNECTION_LOST = 7
+FX_OP_UNSUPPORTED = 8
+FX_FILE_ALREADY_EXISTS = 11
+# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more
+# useful error codes, but so far OpenSSH doesn't implement them. We use them
+# internally for clarity, but for now define them all as FX_FAILURE to be
+# compatible with existing software.
+FX_NOT_A_DIRECTORY = FX_FAILURE
+FX_FILE_IS_A_DIRECTORY = FX_FAILURE
+
+
+# initialize FileTransferBase.packetTypes:
+g = globals()
+for name in g.keys():
+ if name.startswith('FXP_'):
+ value = g[name]
+ FileTransferBase.packetTypes[value] = name[4:]
+del g, name, value
diff --git a/twisted/conch/ssh/forwarding.py b/twisted/conch/ssh/forwarding.py
new file mode 100755
index 0000000..753f994
--- /dev/null
+++ b/twisted/conch/ssh/forwarding.py
@@ -0,0 +1,181 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+This module contains the implementation of the TCP forwarding, which allows
+clients and servers to forward arbitrary TCP data across the connection.
+
+Maintainer: Paul Swartz
+"""
+
+import struct
+
+from twisted.internet import protocol, reactor
+from twisted.python import log
+
+import common, channel
+
+class SSHListenForwardingFactory(protocol.Factory):
+ def __init__(self, connection, hostport, klass):
+ self.conn = connection
+ self.hostport = hostport # tuple
+ self.klass = klass
+
+ def buildProtocol(self, addr):
+ channel = self.klass(conn = self.conn)
+ client = SSHForwardingClient(channel)
+ channel.client = client
+ addrTuple = (addr.host, addr.port)
+ channelOpenData = packOpen_direct_tcpip(self.hostport, addrTuple)
+ self.conn.openChannel(channel, channelOpenData)
+ return client
+
+class SSHListenForwardingChannel(channel.SSHChannel):
+
+ def channelOpen(self, specificData):
+ log.msg('opened forwarding channel %s' % self.id)
+ if len(self.client.buf)>1:
+ b = self.client.buf[1:]
+ self.write(b)
+ self.client.buf = ''
+
+ def openFailed(self, reason):
+ self.closed()
+
+ def dataReceived(self, data):
+ self.client.transport.write(data)
+
+ def eofReceived(self):
+ self.client.transport.loseConnection()
+
+ def closed(self):
+ if hasattr(self, 'client'):
+ log.msg('closing local forwarding channel %s' % self.id)
+ self.client.transport.loseConnection()
+ del self.client
+
+class SSHListenClientForwardingChannel(SSHListenForwardingChannel):
+
+ name = 'direct-tcpip'
+
+class SSHListenServerForwardingChannel(SSHListenForwardingChannel):
+
+ name = 'forwarded-tcpip'
+
+class SSHConnectForwardingChannel(channel.SSHChannel):
+
+ def __init__(self, hostport, *args, **kw):
+ channel.SSHChannel.__init__(self, *args, **kw)
+ self.hostport = hostport
+ self.client = None
+ self.clientBuf = ''
+
+ def channelOpen(self, specificData):
+ cc = protocol.ClientCreator(reactor, SSHForwardingClient, self)
+ log.msg("connecting to %s:%i" % self.hostport)
+ cc.connectTCP(*self.hostport).addCallbacks(self._setClient, self._close)
+
+ def _setClient(self, client):
+ self.client = client
+ log.msg("connected to %s:%i" % self.hostport)
+ if self.clientBuf:
+ self.client.transport.write(self.clientBuf)
+ self.clientBuf = None
+ if self.client.buf[1:]:
+ self.write(self.client.buf[1:])
+ self.client.buf = ''
+
+ def _close(self, reason):
+ log.msg("failed to connect: %s" % reason)
+ self.loseConnection()
+
+ def dataReceived(self, data):
+ if self.client:
+ self.client.transport.write(data)
+ else:
+ self.clientBuf += data
+
+ def closed(self):
+ if self.client:
+ log.msg('closed remote forwarding channel %s' % self.id)
+ if self.client.channel:
+ self.loseConnection()
+ self.client.transport.loseConnection()
+ del self.client
+
+def openConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avatar):
+ remoteHP, origHP = unpackOpen_direct_tcpip(data)
+ return SSHConnectForwardingChannel(remoteHP,
+ remoteWindow=remoteWindow,
+ remoteMaxPacket=remoteMaxPacket,
+ avatar=avatar)
+
+class SSHForwardingClient(protocol.Protocol):
+
+ def __init__(self, channel):
+ self.channel = channel
+ self.buf = '\000'
+
+ def dataReceived(self, data):
+ if self.buf:
+ self.buf += data
+ else:
+ self.channel.write(data)
+
+ def connectionLost(self, reason):
+ if self.channel:
+ self.channel.loseConnection()
+ self.channel = None
+
+
+def packOpen_direct_tcpip((connHost, connPort), (origHost, origPort)):
+ """Pack the data suitable for sending in a CHANNEL_OPEN packet.
+ """
+ conn = common.NS(connHost) + struct.pack('>L', connPort)
+ orig = common.NS(origHost) + struct.pack('>L', origPort)
+ return conn + orig
+
+packOpen_forwarded_tcpip = packOpen_direct_tcpip
+
+def unpackOpen_direct_tcpip(data):
+ """Unpack the data to a usable format.
+ """
+ connHost, rest = common.getNS(data)
+ connPort = int(struct.unpack('>L', rest[:4])[0])
+ origHost, rest = common.getNS(rest[4:])
+ origPort = int(struct.unpack('>L', rest[:4])[0])
+ return (connHost, connPort), (origHost, origPort)
+
+unpackOpen_forwarded_tcpip = unpackOpen_direct_tcpip
+
+def packGlobal_tcpip_forward((host, port)):
+ return common.NS(host) + struct.pack('>L', port)
+
+def unpackGlobal_tcpip_forward(data):
+ host, rest = common.getNS(data)
+ port = int(struct.unpack('>L', rest[:4])[0])
+ return host, port
+
+"""This is how the data -> eof -> close stuff /should/ work.
+
+debug3: channel 1: waiting for connection
+debug1: channel 1: connected
+debug1: channel 1: read<=0 rfd 7 len 0
+debug1: channel 1: read failed
+debug1: channel 1: close_read
+debug1: channel 1: input open -> drain
+debug1: channel 1: ibuf empty
+debug1: channel 1: send eof
+debug1: channel 1: input drain -> closed
+debug1: channel 1: rcvd eof
+debug1: channel 1: output open -> drain
+debug1: channel 1: obuf empty
+debug1: channel 1: close_write
+debug1: channel 1: output drain -> closed
+debug1: channel 1: rcvd close
+debug3: channel 1: will not send data after close
+debug1: channel 1: send close
+debug1: channel 1: is dead
+"""
diff --git a/twisted/conch/ssh/keys.py b/twisted/conch/ssh/keys.py
new file mode 100644
index 0000000..0a534bd
--- /dev/null
+++ b/twisted/conch/ssh/keys.py
@@ -0,0 +1,780 @@
+# -*- test-case-name: twisted.conch.test.test_keys -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Handling of RSA and DSA keys.
+
+Maintainer: U{Paul Swartz}
+"""
+
+# base library imports
+import base64
+import warnings
+import itertools
+
+# external library imports
+from Crypto.Cipher import DES3
+from Crypto.PublicKey import RSA, DSA
+from Crypto import Util
+from pyasn1.type import univ
+from pyasn1.codec.ber import decoder as berDecoder
+from pyasn1.codec.ber import encoder as berEncoder
+
+# twisted
+from twisted.python import randbytes
+from twisted.python.hashlib import md5, sha1
+
+# sibling imports
+from twisted.conch.ssh import common, sexpy
+
+
+class BadKeyError(Exception):
+ """
+ Raised when a key isn't what we expected from it.
+
+ XXX: we really need to check for bad keys
+ """
+
+class EncryptedKeyError(Exception):
+ """
+ Raised when an encrypted key is presented to fromString/fromFile without
+ a password.
+ """
+
+class Key(object):
+ """
+ An object representing a key. A key can be either a public or
+ private key. A public key can verify a signature; a private key can
+ create or verify a signature. To generate a string that can be stored
+ on disk, use the toString method. If you have a private key, but want
+ the string representation of the public key, use Key.public().toString().
+
+ @ivar keyObject: The C{Crypto.PublicKey.pubkey.pubkey} object that
+ operations are performed with.
+ """
+
+ def fromFile(Class, filename, type=None, passphrase=None):
+ """
+ Return a Key object corresponding to the data in filename. type
+ and passphrase function as they do in fromString.
+ """
+ return Class.fromString(file(filename, 'rb').read(), type, passphrase)
+ fromFile = classmethod(fromFile)
+
+ def fromString(Class, data, type=None, passphrase=None):
+ """
+ Return a Key object corresponding to the string data.
+ type is optionally the type of string, matching a _fromString_*
+ method. Otherwise, the _guessStringType() classmethod will be used
+ to guess a type. If the key is encrypted, passphrase is used as
+ the decryption key.
+
+ @type data: C{str}
+ @type type: C{None}/C{str}
+ @type passphrase: C{None}/C{str}
+ @rtype: C{Key}
+ """
+ if type is None:
+ type = Class._guessStringType(data)
+ if type is None:
+ raise BadKeyError('cannot guess the type of %r' % data)
+ method = getattr(Class, '_fromString_%s' % type.upper(), None)
+ if method is None:
+ raise BadKeyError('no _fromString method for %s' % type)
+ if method.func_code.co_argcount == 2: # no passphrase
+ if passphrase:
+ raise BadKeyError('key not encrypted')
+ return method(data)
+ else:
+ return method(data, passphrase)
+ fromString = classmethod(fromString)
+
+ def _fromString_BLOB(Class, blob):
+ """
+ Return a public key object corresponding to this public key blob.
+ The format of a RSA public key blob is::
+ string 'ssh-rsa'
+ integer e
+ integer n
+
+ The format of a DSA public key blob is::
+ string 'ssh-dss'
+ integer p
+ integer q
+ integer g
+ integer y
+
+ @type blob: C{str}
+ @return: a C{Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the key type (the first string) is unknown.
+ """
+ keyType, rest = common.getNS(blob)
+ if keyType == 'ssh-rsa':
+ e, n, rest = common.getMP(rest, 2)
+ return Class(RSA.construct((n, e)))
+ elif keyType == 'ssh-dss':
+ p, q, g, y, rest = common.getMP(rest, 4)
+ return Class(DSA.construct((y, g, p, q)))
+ else:
+ raise BadKeyError('unknown blob type: %s' % keyType)
+ _fromString_BLOB = classmethod(_fromString_BLOB)
+
+ def _fromString_PRIVATE_BLOB(Class, blob):
+ """
+ Return a private key object corresponding to this private key blob.
+ The blob formats are as follows:
+
+ RSA keys::
+ string 'ssh-rsa'
+ integer n
+ integer e
+ integer d
+ integer u
+ integer p
+ integer q
+
+ DSA keys::
+ string 'ssh-dss'
+ integer p
+ integer q
+ integer g
+ integer y
+ integer x
+
+ @type blob: C{str}
+ @return: a C{Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the key type (the first string) is unknown.
+ """
+ keyType, rest = common.getNS(blob)
+
+ if keyType == 'ssh-rsa':
+ n, e, d, u, p, q, rest = common.getMP(rest, 6)
+ rsakey = Class(RSA.construct((n, e, d, p, q, u)))
+ return rsakey
+ elif keyType == 'ssh-dss':
+ p, q, g, y, x, rest = common.getMP(rest, 5)
+ dsakey = Class(DSA.construct((y, g, p, q, x)))
+ return dsakey
+ else:
+ raise BadKeyError('unknown blob type: %s' % keyType)
+ _fromString_PRIVATE_BLOB = classmethod(_fromString_PRIVATE_BLOB)
+
+ def _fromString_PUBLIC_OPENSSH(Class, data):
+ """
+ Return a public key object corresponding to this OpenSSH public key
+ string. The format of an OpenSSH public key string is::
+ <key type> <base64-encoded public key blob>
+
+ @type data: C{str}
+ @return: A {Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the blob type is unknown.
+ """
+ blob = base64.decodestring(data.split()[1])
+ return Class._fromString_BLOB(blob)
+ _fromString_PUBLIC_OPENSSH = classmethod(_fromString_PUBLIC_OPENSSH)
+
+ def _fromString_PRIVATE_OPENSSH(Class, data, passphrase):
+ """
+ Return a private key object corresponding to this OpenSSH private key
+ string. If the key is encrypted, passphrase MUST be provided.
+ Providing a passphrase for an unencrypted key is an error.
+
+ The format of an OpenSSH private key string is::
+ -----BEGIN <key type> PRIVATE KEY-----
+ [Proc-Type: 4,ENCRYPTED
+ DEK-Info: DES-EDE3-CBC,<initialization value>]
+ <base64-encoded ASN.1 structure>
+ ------END <key type> PRIVATE KEY------
+
+ The ASN.1 structure of a RSA key is::
+ (0, n, e, d, p, q)
+
+ The ASN.1 structure of a DSA key is::
+ (0, p, q, g, y, x)
+
+ @type data: C{str}
+ @type passphrase: C{str}
+ @return: a C{Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if
+ * a passphrase is provided for an unencrypted key
+ * a passphrase is not provided for an encrypted key
+ * the ASN.1 encoding is incorrect
+ """
+ lines = [x + '\n' for x in data.split('\n')]
+ kind = lines[0][11:14]
+ if lines[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key
+ ivdata = lines[2].split(',')[1][:-1]
+ iv = ''.join([chr(int(ivdata[i:i + 2], 16)) for i in range(0,
+ len(ivdata), 2)])
+ if not passphrase:
+ raise EncryptedKeyError('encrypted key with no passphrase')
+ ba = md5(passphrase + iv).digest()
+ bb = md5(ba + passphrase + iv).digest()
+ decKey = (ba + bb)[:24]
+ b64Data = base64.decodestring(''.join(lines[3:-1]))
+ keyData = DES3.new(decKey, DES3.MODE_CBC, iv).decrypt(b64Data)
+ removeLen = ord(keyData[-1])
+ keyData = keyData[:-removeLen]
+ else:
+ b64Data = ''.join(lines[1:-1])
+ keyData = base64.decodestring(b64Data)
+ try:
+ decodedKey = berDecoder.decode(keyData)[0]
+ except Exception, e:
+ raise BadKeyError, 'something wrong with decode'
+ if kind == 'RSA':
+ if len(decodedKey) == 2: # alternate RSA key
+ decodedKey = decodedKey[0]
+ if len(decodedKey) < 6:
+ raise BadKeyError('RSA key failed to decode properly')
+ n, e, d, p, q = [long(value) for value in decodedKey[1:6]]
+ if p > q: # make p smaller than q
+ p, q = q, p
+ return Class(RSA.construct((n, e, d, p, q)))
+ elif kind == 'DSA':
+ p, q, g, y, x = [long(value) for value in decodedKey[1: 6]]
+ if len(decodedKey) < 6:
+ raise BadKeyError('DSA key failed to decode properly')
+ return Class(DSA.construct((y, g, p, q, x)))
+ _fromString_PRIVATE_OPENSSH = classmethod(_fromString_PRIVATE_OPENSSH)
+
+ def _fromString_PUBLIC_LSH(Class, data):
+ """
+ Return a public key corresponding to this LSH public key string.
+ The LSH public key string format is::
+ <s-expression: ('public-key', (<key type>, (<name, <value>)+))>
+
+ The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e.
+ The names for a DSA (key type 'dsa') key are: y, g, p, q.
+
+ @type data: C{str}
+ @return: a C{Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the key type is unknown
+ """
+ sexp = sexpy.parse(base64.decodestring(data[1:-1]))
+ assert sexp[0] == 'public-key'
+ kd = {}
+ for name, data in sexp[1][1:]:
+ kd[name] = common.getMP(common.NS(data))[0]
+ if sexp[1][0] == 'dsa':
+ return Class(DSA.construct((kd['y'], kd['g'], kd['p'], kd['q'])))
+ elif sexp[1][0] == 'rsa-pkcs1-sha1':
+ return Class(RSA.construct((kd['n'], kd['e'])))
+ else:
+ raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
+ _fromString_PUBLIC_LSH = classmethod(_fromString_PUBLIC_LSH)
+
+ def _fromString_PRIVATE_LSH(Class, data):
+ """
+ Return a private key corresponding to this LSH private key string.
+ The LSH private key string format is::
+ <s-expression: ('private-key', (<key type>, (<name>, <value>)+))>
+
+ The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q.
+ The names for a DSA (key type 'dsa') key are: y, g, p, q, x.
+
+ @type data: C{str}
+ @return: a {Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the key type is unknown
+ """
+ sexp = sexpy.parse(data)
+ assert sexp[0] == 'private-key'
+ kd = {}
+ for name, data in sexp[1][1:]:
+ kd[name] = common.getMP(common.NS(data))[0]
+ if sexp[1][0] == 'dsa':
+ assert len(kd) == 5, len(kd)
+ return Class(DSA.construct((kd['y'], kd['g'], kd['p'],
+ kd['q'], kd['x'])))
+ elif sexp[1][0] == 'rsa-pkcs1':
+ assert len(kd) == 8, len(kd)
+ if kd['p'] > kd['q']: # make p smaller than q
+ kd['p'], kd['q'] = kd['q'], kd['p']
+ return Class(RSA.construct((kd['n'], kd['e'], kd['d'],
+ kd['p'], kd['q'])))
+ else:
+ raise BadKeyError('unknown lsh key type %s' % sexp[1][0])
+ _fromString_PRIVATE_LSH = classmethod(_fromString_PRIVATE_LSH)
+
+ def _fromString_AGENTV3(Class, data):
+ """
+ Return a private key object corresponsing to the Secure Shell Key
+ Agent v3 format.
+
+ The SSH Key Agent v3 format for a RSA key is::
+ string 'ssh-rsa'
+ integer e
+ integer d
+ integer n
+ integer u
+ integer p
+ integer q
+
+ The SSH Key Agent v3 format for a DSA key is::
+ string 'ssh-dss'
+ integer p
+ integer q
+ integer g
+ integer y
+ integer x
+
+ @type data: C{str}
+ @return: a C{Crypto.PublicKey.pubkey.pubkey} object
+ @raises BadKeyError: if the key type (the first string) is unknown
+ """
+ keyType, data = common.getNS(data)
+ if keyType == 'ssh-dss':
+ p, data = common.getMP(data)
+ q, data = common.getMP(data)
+ g, data = common.getMP(data)
+ y, data = common.getMP(data)
+ x, data = common.getMP(data)
+ return Class(DSA.construct((y,g,p,q,x)))
+ elif keyType == 'ssh-rsa':
+ e, data = common.getMP(data)
+ d, data = common.getMP(data)
+ n, data = common.getMP(data)
+ u, data = common.getMP(data)
+ p, data = common.getMP(data)
+ q, data = common.getMP(data)
+ return Class(RSA.construct((n,e,d,p,q,u)))
+ else:
+ raise BadKeyError("unknown key type %s" % keyType)
+ _fromString_AGENTV3 = classmethod(_fromString_AGENTV3)
+
+ def _guessStringType(Class, data):
+ """
+ Guess the type of key in data. The types map to _fromString_*
+ methods.
+ """
+ if data.startswith('ssh-'):
+ return 'public_openssh'
+ elif data.startswith('-----BEGIN'):
+ return 'private_openssh'
+ elif data.startswith('{'):
+ return 'public_lsh'
+ elif data.startswith('('):
+ return 'private_lsh'
+ elif data.startswith('\x00\x00\x00\x07ssh-'):
+ ignored, rest = common.getNS(data)
+ count = 0
+ while rest:
+ count += 1
+ ignored, rest = common.getMP(rest)
+ if count > 4:
+ return 'agentv3'
+ else:
+ return 'blob'
+ _guessStringType = classmethod(_guessStringType)
+
+ def __init__(self, keyObject):
+ """
+ Initialize a PublicKey with a C{Crypto.PublicKey.pubkey.pubkey}
+ object.
+
+ @type keyObject: C{Crypto.PublicKey.pubkey.pubkey}
+ """
+ self.keyObject = keyObject
+
+ def __eq__(self, other):
+ """
+ Return True if other represents an object with the same key.
+ """
+ if type(self) == type(other):
+ return self.type() == other.type() and self.data() == other.data()
+ else:
+ return NotImplemented
+
+ def __ne__(self, other):
+ """
+ Return True if other represents anything other than this key.
+ """
+ result = self.__eq__(other)
+ if result == NotImplemented:
+ return result
+ return not result
+
+ def __repr__(self):
+ """
+ Return a pretty representation of this object.
+ """
+ lines = ['<%s %s (%s bits)' % (self.type(),
+ self.isPublic() and 'Public Key' or 'Private Key',
+ self.keyObject.size())]
+ for k, v in self.data().items():
+ lines.append('attr %s:' % k)
+ by = common.MP(v)[4:]
+ while by:
+ m = by[:15]
+ by = by[15:]
+ o = ''
+ for c in m:
+ o = o + '%02x:' % ord(c)
+ if len(m) < 15:
+ o = o[:-1]
+ lines.append('\t' + o)
+ lines[-1] = lines[-1] + '>'
+ return '\n'.join(lines)
+
+ def isPublic(self):
+ """
+ Returns True if this Key is a public key.
+ """
+ return not self.keyObject.has_private()
+
+ def public(self):
+ """
+ Returns a version of this key containing only the public key data.
+ If this is a public key, this may or may not be the same object
+ as self.
+ """
+ return Key(self.keyObject.publickey())
+
+
+ def fingerprint(self):
+ """
+ Get the user presentation of the fingerprint of this L{Key}. As
+ described by U{RFC 4716 section
+ 4<http://tools.ietf.org/html/rfc4716#section-4>}::
+
+ The fingerprint of a public key consists of the output of the MD5
+ message-digest algorithm [RFC1321]. The input to the algorithm is
+ the public key data as specified by [RFC4253]. (...) The output
+ of the (MD5) algorithm is presented to the user as a sequence of 16
+ octets printed as hexadecimal with lowercase letters and separated
+ by colons.
+
+ @since: 8.2
+
+ @return: the user presentation of this L{Key}'s fingerprint, as a
+ string.
+
+ @rtype: L{str}
+ """
+ return ':'.join([x.encode('hex') for x in md5(self.blob()).digest()])
+
+
+ def type(self):
+ """
+ Return the type of the object we wrap. Currently this can only be
+ 'RSA' or 'DSA'.
+ """
+ # the class is Crypto.PublicKey.<type>.<stuff we don't care about>
+ mod = self.keyObject.__class__.__module__
+ if mod.startswith('Crypto.PublicKey'):
+ type = mod.split('.')[2]
+ else:
+ raise RuntimeError('unknown type of object: %r' % self.keyObject)
+ if type in ('RSA', 'DSA'):
+ return type
+ else:
+ raise RuntimeError('unknown type of key: %s' % type)
+
+ def sshType(self):
+ """
+ Return the type of the object we wrap as defined in the ssh protocol.
+ Currently this can only be 'ssh-rsa' or 'ssh-dss'.
+ """
+ return {'RSA':'ssh-rsa', 'DSA':'ssh-dss'}[self.type()]
+
+ def data(self):
+ """
+ Return the values of the public key as a dictionary.
+
+ @rtype: C{dict}
+ """
+ keyData = {}
+ for name in self.keyObject.keydata:
+ value = getattr(self.keyObject, name, None)
+ if value is not None:
+ keyData[name] = value
+ return keyData
+
+ def blob(self):
+ """
+ Return the public key blob for this key. The blob is the
+ over-the-wire format for public keys:
+
+ RSA keys::
+ string 'ssh-rsa'
+ integer e
+ integer n
+
+ DSA keys::
+ string 'ssh-dss'
+ integer p
+ integer q
+ integer g
+ integer y
+
+ @rtype: C{str}
+ """
+ type = self.type()
+ data = self.data()
+ if type == 'RSA':
+ return (common.NS('ssh-rsa') + common.MP(data['e']) +
+ common.MP(data['n']))
+ elif type == 'DSA':
+ return (common.NS('ssh-dss') + common.MP(data['p']) +
+ common.MP(data['q']) + common.MP(data['g']) +
+ common.MP(data['y']))
+
+ def privateBlob(self):
+ """
+ Return the private key blob for this key. The blob is the
+ over-the-wire format for private keys:
+
+ RSA keys::
+ string 'ssh-rsa'
+ integer n
+ integer e
+ integer d
+ integer u
+ integer p
+ integer q
+
+ DSA keys::
+ string 'ssh-dss'
+ integer p
+ integer q
+ integer g
+ integer y
+ integer x
+ """
+ type = self.type()
+ data = self.data()
+ if type == 'RSA':
+ return (common.NS('ssh-rsa') + common.MP(data['n']) +
+ common.MP(data['e']) + common.MP(data['d']) +
+ common.MP(data['u']) + common.MP(data['p']) +
+ common.MP(data['q']))
+ elif type == 'DSA':
+ return (common.NS('ssh-dss') + common.MP(data['p']) +
+ common.MP(data['q']) + common.MP(data['g']) +
+ common.MP(data['y']) + common.MP(data['x']))
+
+ def toString(self, type, extra=None):
+ """
+ Create a string representation of this key. If the key is a private
+ key and you want the represenation of its public key, use
+ C{key.public().toString()}. type maps to a _toString_* method.
+
+ @param type: The type of string to emit. Currently supported values
+ are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}.
+ @type type: L{str}
+
+ @param extra: Any extra data supported by the selected format which
+ is not part of the key itself. For public OpenSSH keys, this is
+ a comment. For private OpenSSH keys, this is a passphrase to
+ encrypt with.
+ @type extra: L{str} or L{NoneType}
+
+ @rtype: L{str}
+ """
+ method = getattr(self, '_toString_%s' % type.upper(), None)
+ if method is None:
+ raise BadKeyError('unknown type: %s' % type)
+ if method.func_code.co_argcount == 2:
+ return method(extra)
+ else:
+ return method()
+
+ def _toString_OPENSSH(self, extra):
+ """
+ Return a public or private OpenSSH string. See
+ _fromString_PUBLIC_OPENSSH and _fromString_PRIVATE_OPENSSH for the
+ string formats. If extra is present, it represents a comment for a
+ public key, or a passphrase for a private key.
+
+ @type extra: C{str}
+ @rtype: C{str}
+ """
+ data = self.data()
+ if self.isPublic():
+ b64Data = base64.encodestring(self.blob()).replace('\n', '')
+ if not extra:
+ extra = ''
+ return ('%s %s %s' % (self.sshType(), b64Data, extra)).strip()
+ else:
+ lines = ['-----BEGIN %s PRIVATE KEY-----' % self.type()]
+ if self.type() == 'RSA':
+ p, q = data['p'], data['q']
+ objData = (0, data['n'], data['e'], data['d'], q, p,
+ data['d'] % (q - 1), data['d'] % (p - 1),
+ data['u'])
+ else:
+ objData = (0, data['p'], data['q'], data['g'], data['y'],
+ data['x'])
+ asn1Sequence = univ.Sequence()
+ for index, value in itertools.izip(itertools.count(), objData):
+ asn1Sequence.setComponentByPosition(index, univ.Integer(value))
+ asn1Data = berEncoder.encode(asn1Sequence)
+ if extra:
+ iv = randbytes.secureRandom(8)
+ hexiv = ''.join(['%02X' % ord(x) for x in iv])
+ lines.append('Proc-Type: 4,ENCRYPTED')
+ lines.append('DEK-Info: DES-EDE3-CBC,%s\n' % hexiv)
+ ba = md5(extra + iv).digest()
+ bb = md5(ba + extra + iv).digest()
+ encKey = (ba + bb)[:24]
+ padLen = 8 - (len(asn1Data) % 8)
+ asn1Data += (chr(padLen) * padLen)
+ asn1Data = DES3.new(encKey, DES3.MODE_CBC,
+ iv).encrypt(asn1Data)
+ b64Data = base64.encodestring(asn1Data).replace('\n', '')
+ lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)]
+ lines.append('-----END %s PRIVATE KEY-----' % self.type())
+ return '\n'.join(lines)
+
+ def _toString_LSH(self):
+ """
+ Return a public or private LSH key. See _fromString_PUBLIC_LSH and
+ _fromString_PRIVATE_LSH for the key formats.
+
+ @rtype: C{str}
+ """
+ data = self.data()
+ if self.isPublic():
+ if self.type() == 'RSA':
+ keyData = sexpy.pack([['public-key', ['rsa-pkcs1-sha1',
+ ['n', common.MP(data['n'])[4:]],
+ ['e', common.MP(data['e'])[4:]]]]])
+ elif self.type() == 'DSA':
+ keyData = sexpy.pack([['public-key', ['dsa',
+ ['p', common.MP(data['p'])[4:]],
+ ['q', common.MP(data['q'])[4:]],
+ ['g', common.MP(data['g'])[4:]],
+ ['y', common.MP(data['y'])[4:]]]]])
+ return '{' + base64.encodestring(keyData).replace('\n', '') + '}'
+ else:
+ if self.type() == 'RSA':
+ p, q = data['p'], data['q']
+ return sexpy.pack([['private-key', ['rsa-pkcs1',
+ ['n', common.MP(data['n'])[4:]],
+ ['e', common.MP(data['e'])[4:]],
+ ['d', common.MP(data['d'])[4:]],
+ ['p', common.MP(q)[4:]],
+ ['q', common.MP(p)[4:]],
+ ['a', common.MP(data['d'] % (q - 1))[4:]],
+ ['b', common.MP(data['d'] % (p - 1))[4:]],
+ ['c', common.MP(data['u'])[4:]]]]])
+ elif self.type() == 'DSA':
+ return sexpy.pack([['private-key', ['dsa',
+ ['p', common.MP(data['p'])[4:]],
+ ['q', common.MP(data['q'])[4:]],
+ ['g', common.MP(data['g'])[4:]],
+ ['y', common.MP(data['y'])[4:]],
+ ['x', common.MP(data['x'])[4:]]]]])
+
+ def _toString_AGENTV3(self):
+ """
+ Return a private Secure Shell Agent v3 key. See
+ _fromString_AGENTV3 for the key format.
+
+ @rtype: C{str}
+ """
+ data = self.data()
+ if not self.isPublic():
+ if self.type() == 'RSA':
+ values = (data['e'], data['d'], data['n'], data['u'],
+ data['p'], data['q'])
+ elif self.type() == 'DSA':
+ values = (data['p'], data['q'], data['g'], data['y'],
+ data['x'])
+ return common.NS(self.sshType()) + ''.join(map(common.MP, values))
+
+
+ def sign(self, data):
+ """
+ Returns a signature with this Key.
+
+ @type data: C{str}
+ @rtype: C{str}
+ """
+ if self.type() == 'RSA':
+ digest = pkcs1Digest(data, self.keyObject.size()/8)
+ signature = self.keyObject.sign(digest, '')[0]
+ ret = common.NS(Util.number.long_to_bytes(signature))
+ elif self.type() == 'DSA':
+ digest = sha1(data).digest()
+ randomBytes = randbytes.secureRandom(19)
+ sig = self.keyObject.sign(digest, randomBytes)
+ # SSH insists that the DSS signature blob be two 160-bit integers
+ # concatenated together. The sig[0], [1] numbers from obj.sign
+ # are just numbers, and could be any length from 0 to 160 bits.
+ # Make sure they are padded out to 160 bits (20 bytes each)
+ ret = common.NS(Util.number.long_to_bytes(sig[0], 20) +
+ Util.number.long_to_bytes(sig[1], 20))
+ return common.NS(self.sshType()) + ret
+
+ def verify(self, signature, data):
+ """
+ Returns true if the signature for data is valid for this Key.
+
+ @type signature: C{str}
+ @type data: C{str}
+ @rtype: C{bool}
+ """
+ signatureType, signature = common.getNS(signature)
+ if signatureType != self.sshType():
+ return False
+ if self.type() == 'RSA':
+ numbers = common.getMP(signature)
+ digest = pkcs1Digest(data, self.keyObject.size() / 8)
+ elif self.type() == 'DSA':
+ signature = common.getNS(signature)[0]
+ numbers = [Util.number.bytes_to_long(n) for n in signature[:20],
+ signature[20:]]
+ digest = sha1(data).digest()
+ return self.keyObject.verify(digest, numbers)
+
+
+def objectType(obj):
+ """
+ Return the SSH key type corresponding to a C{Crypto.PublicKey.pubkey.pubkey}
+ object.
+
+ @type obj: C{Crypto.PublicKey.pubkey.pubkey}
+ @rtype: C{str}
+ """
+ keyDataMapping = {
+ ('n', 'e', 'd', 'p', 'q'): 'ssh-rsa',
+ ('n', 'e', 'd', 'p', 'q', 'u'): 'ssh-rsa',
+ ('y', 'g', 'p', 'q', 'x'): 'ssh-dss'
+ }
+ try:
+ return keyDataMapping[tuple(obj.keydata)]
+ except (KeyError, AttributeError):
+ raise BadKeyError("invalid key object", obj)
+
+def pkcs1Pad(data, messageLength):
+ """
+ Pad out data to messageLength according to the PKCS#1 standard.
+ @type data: C{str}
+ @type messageLength: C{int}
+ """
+ lenPad = messageLength - 2 - len(data)
+ return '\x01' + ('\xff' * lenPad) + '\x00' + data
+
+def pkcs1Digest(data, messageLength):
+ """
+ Create a message digest using the SHA1 hash algorithm according to the
+ PKCS#1 standard.
+ @type data: C{str}
+ @type messageLength: C{str}
+ """
+ digest = sha1(data).digest()
+ return pkcs1Pad(ID_SHA1+digest, messageLength)
+
+def lenSig(obj):
+ """
+ Return the length of the signature in bytes for a key object.
+
+ @type obj: C{Crypto.PublicKey.pubkey.pubkey}
+ @rtype: C{long}
+ """
+ return obj.size()/8
+
+
+ID_SHA1 = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14'
diff --git a/twisted/conch/ssh/service.py b/twisted/conch/ssh/service.py
new file mode 100644
index 0000000..b5477c4
--- /dev/null
+++ b/twisted/conch/ssh/service.py
@@ -0,0 +1,48 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The parent class for all the SSH services. Currently implemented services
+are ssh-userauth and ssh-connection.
+
+Maintainer: Paul Swartz
+"""
+
+
+from twisted.python import log
+
+class SSHService(log.Logger):
+ name = None # this is the ssh name for the service
+ protocolMessages = {} # these map #'s -> protocol names
+ transport = None # gets set later
+
+ def serviceStarted(self):
+ """
+ called when the service is active on the transport.
+ """
+
+ def serviceStopped(self):
+ """
+ called when the service is stopped, either by the connection ending
+ or by another service being started
+ """
+
+ def logPrefix(self):
+ return "SSHService %s on %s" % (self.name,
+ self.transport.transport.logPrefix())
+
+ def packetReceived(self, messageNum, packet):
+ """
+ called when we receive a packet on the transport
+ """
+ #print self.protocolMessages
+ if messageNum in self.protocolMessages:
+ messageType = self.protocolMessages[messageNum]
+ f = getattr(self,'ssh_%s' % messageType[4:],
+ None)
+ if f is not None:
+ return f(packet)
+ log.msg("couldn't handle %r" % messageNum)
+ log.msg(repr(packet))
+ self.transport.sendUnimplemented()
+
diff --git a/twisted/conch/ssh/session.py b/twisted/conch/ssh/session.py
new file mode 100755
index 0000000..e9eca3e
--- /dev/null
+++ b/twisted/conch/ssh/session.py
@@ -0,0 +1,348 @@
+# -*- test-case-name: twisted.conch.test.test_session -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module contains the implementation of SSHSession, which (by default)
+allows access to a shell and a python interpreter over SSH.
+
+Maintainer: Paul Swartz
+"""
+
+import struct
+import signal
+import sys
+import os
+from zope.interface import implements
+
+from twisted.internet import interfaces, protocol
+from twisted.python import log
+from twisted.conch.interfaces import ISession
+from twisted.conch.ssh import common, channel
+
+class SSHSession(channel.SSHChannel):
+
+ name = 'session'
+ def __init__(self, *args, **kw):
+ channel.SSHChannel.__init__(self, *args, **kw)
+ self.buf = ''
+ self.client = None
+ self.session = None
+
+ def request_subsystem(self, data):
+ subsystem, ignored= common.getNS(data)
+ log.msg('asking for subsystem "%s"' % subsystem)
+ client = self.avatar.lookupSubsystem(subsystem, data)
+ if client:
+ pp = SSHSessionProcessProtocol(self)
+ proto = wrapProcessProtocol(pp)
+ client.makeConnection(proto)
+ pp.makeConnection(wrapProtocol(client))
+ self.client = pp
+ return 1
+ else:
+ log.msg('failed to get subsystem')
+ return 0
+
+ def request_shell(self, data):
+ log.msg('getting shell')
+ if not self.session:
+ self.session = ISession(self.avatar)
+ try:
+ pp = SSHSessionProcessProtocol(self)
+ self.session.openShell(pp)
+ except:
+ log.deferr()
+ return 0
+ else:
+ self.client = pp
+ return 1
+
+ def request_exec(self, data):
+ if not self.session:
+ self.session = ISession(self.avatar)
+ f,data = common.getNS(data)
+ log.msg('executing command "%s"' % f)
+ try:
+ pp = SSHSessionProcessProtocol(self)
+ self.session.execCommand(pp, f)
+ except:
+ log.deferr()
+ return 0
+ else:
+ self.client = pp
+ return 1
+
+ def request_pty_req(self, data):
+ if not self.session:
+ self.session = ISession(self.avatar)
+ term, windowSize, modes = parseRequest_pty_req(data)
+ log.msg('pty request: %s %s' % (term, windowSize))
+ try:
+ self.session.getPty(term, windowSize, modes)
+ except:
+ log.err()
+ return 0
+ else:
+ return 1
+
+ def request_window_change(self, data):
+ if not self.session:
+ self.session = ISession(self.avatar)
+ winSize = parseRequest_window_change(data)
+ try:
+ self.session.windowChanged(winSize)
+ except:
+ log.msg('error changing window size')
+ log.err()
+ return 0
+ else:
+ return 1
+
+ def dataReceived(self, data):
+ if not self.client:
+ #self.conn.sendClose(self)
+ self.buf += data
+ return
+ self.client.transport.write(data)
+
+ def extReceived(self, dataType, data):
+ if dataType == connection.EXTENDED_DATA_STDERR:
+ if self.client and hasattr(self.client.transport, 'writeErr'):
+ self.client.transport.writeErr(data)
+ else:
+ log.msg('weird extended data: %s'%dataType)
+
+ def eofReceived(self):
+ if self.session:
+ self.session.eofReceived()
+ elif self.client:
+ self.conn.sendClose(self)
+
+ def closed(self):
+ if self.session:
+ self.session.closed()
+ elif self.client:
+ self.client.transport.loseConnection()
+
+ #def closeReceived(self):
+ # self.loseConnection() # don't know what to do with this
+
+ def loseConnection(self):
+ if self.client:
+ self.client.transport.loseConnection()
+ channel.SSHChannel.loseConnection(self)
+
+class _ProtocolWrapper(protocol.ProcessProtocol):
+ """
+ This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
+ """
+ def __init__(self, proto):
+ self.proto = proto
+
+ def connectionMade(self): self.proto.connectionMade()
+
+ def outReceived(self, data): self.proto.dataReceived(data)
+
+ def processEnded(self, reason): self.proto.connectionLost(reason)
+
+class _DummyTransport:
+
+ def __init__(self, proto):
+ self.proto = proto
+
+ def dataReceived(self, data):
+ self.proto.transport.write(data)
+
+ def write(self, data):
+ self.proto.dataReceived(data)
+
+ def writeSequence(self, seq):
+ self.write(''.join(seq))
+
+ def loseConnection(self):
+ self.proto.connectionLost(protocol.connectionDone)
+
+def wrapProcessProtocol(inst):
+ if isinstance(inst, protocol.Protocol):
+ return _ProtocolWrapper(inst)
+ else:
+ return inst
+
+def wrapProtocol(proto):
+ return _DummyTransport(proto)
+
+
+
+# SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
+# to accept. See RFC 4254
+SUPPORTED_SIGNALS = ["ABRT", "ALRM", "FPE", "HUP", "ILL", "INT", "KILL",
+ "PIPE", "QUIT", "SEGV", "TERM", "USR1", "USR2"]
+
+
+
+class SSHSessionProcessProtocol(protocol.ProcessProtocol):
+ """I am both an L{IProcessProtocol} and an L{ITransport}.
+
+ I am a transport to the remote endpoint and a process protocol to the
+ local subsystem.
+ """
+
+ implements(interfaces.ITransport)
+
+ # once initialized, a dictionary mapping signal values to strings
+ # that follow RFC 4254.
+ _signalValuesToNames = None
+
+ def __init__(self, session):
+ self.session = session
+ self.lostOutOrErrFlag = False
+
+ def connectionMade(self):
+ if self.session.buf:
+ self.transport.write(self.session.buf)
+ self.session.buf = None
+
+ def outReceived(self, data):
+ self.session.write(data)
+
+ def errReceived(self, err):
+ self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
+
+ def outConnectionLost(self):
+ """
+ EOF should only be sent when both STDOUT and STDERR have been closed.
+ """
+ if self.lostOutOrErrFlag:
+ self.session.conn.sendEOF(self.session)
+ else:
+ self.lostOutOrErrFlag = True
+
+ def errConnectionLost(self):
+ """
+ See outConnectionLost().
+ """
+ self.outConnectionLost()
+
+ def connectionLost(self, reason = None):
+ self.session.loseConnection()
+
+
+ def _getSignalName(self, signum):
+ """
+ Get a signal name given a signal number.
+ """
+ if self._signalValuesToNames is None:
+ self._signalValuesToNames = {}
+ # make sure that the POSIX ones are the defaults
+ for signame in SUPPORTED_SIGNALS:
+ signame = 'SIG' + signame
+ sigvalue = getattr(signal, signame, None)
+ if sigvalue is not None:
+ self._signalValuesToNames[sigvalue] = signame
+ for k, v in signal.__dict__.items():
+ # Check for platform specific signals, ignoring Python specific
+ # SIG_DFL and SIG_IGN
+ if k.startswith('SIG') and not k.startswith('SIG_'):
+ if v not in self._signalValuesToNames:
+ self._signalValuesToNames[v] = k + '@' + sys.platform
+ return self._signalValuesToNames[signum]
+
+
+ def processEnded(self, reason=None):
+ """
+ When we are told the process ended, try to notify the other side about
+ how the process ended using the exit-signal or exit-status requests.
+ Also, close the channel.
+ """
+ if reason is not None:
+ err = reason.value
+ if err.signal is not None:
+ signame = self._getSignalName(err.signal)
+ if (getattr(os, 'WCOREDUMP', None) is not None and
+ os.WCOREDUMP(err.status)):
+ log.msg('exitSignal: %s (core dumped)' % (signame,))
+ coreDumped = 1
+ else:
+ log.msg('exitSignal: %s' % (signame,))
+ coreDumped = 0
+ self.session.conn.sendRequest(self.session, 'exit-signal',
+ common.NS(signame[3:]) + chr(coreDumped) +
+ common.NS('') + common.NS(''))
+ elif err.exitCode is not None:
+ log.msg('exitCode: %r' % (err.exitCode,))
+ self.session.conn.sendRequest(self.session, 'exit-status',
+ struct.pack('>L', err.exitCode))
+ self.session.loseConnection()
+
+
+ def getHost(self):
+ """
+ Return the host from my session's transport.
+ """
+ return self.session.conn.transport.getHost()
+
+
+ def getPeer(self):
+ """
+ Return the peer from my session's transport.
+ """
+ return self.session.conn.transport.getPeer()
+
+
+ def write(self, data):
+ self.session.write(data)
+
+
+ def writeSequence(self, seq):
+ self.session.write(''.join(seq))
+
+
+ def loseConnection(self):
+ self.session.loseConnection()
+
+
+
+class SSHSessionClient(protocol.Protocol):
+
+ def dataReceived(self, data):
+ if self.transport:
+ self.transport.write(data)
+
+# methods factored out to make live easier on server writers
+def parseRequest_pty_req(data):
+ """Parse the data from a pty-req request into usable data.
+
+ @returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
+ """
+ term, rest = common.getNS(data)
+ cols, rows, xpixel, ypixel = struct.unpack('>4L', rest[: 16])
+ modes, ignored= common.getNS(rest[16:])
+ winSize = (rows, cols, xpixel, ypixel)
+ modes = [(ord(modes[i]), struct.unpack('>L', modes[i+1: i+5])[0]) for i in range(0, len(modes)-1, 5)]
+ return term, winSize, modes
+
+def packRequest_pty_req(term, (rows, cols, xpixel, ypixel), modes):
+ """Pack a pty-req request so that it is suitable for sending.
+
+ NOTE: modes must be packed before being sent here.
+ """
+ termPacked = common.NS(term)
+ winSizePacked = struct.pack('>4L', cols, rows, xpixel, ypixel)
+ modesPacked = common.NS(modes) # depend on the client packing modes
+ return termPacked + winSizePacked + modesPacked
+
+def parseRequest_window_change(data):
+ """Parse the data from a window-change request into usuable data.
+
+ @returns: a tuple of (rows, cols, xpixel, ypixel)
+ """
+ cols, rows, xpixel, ypixel = struct.unpack('>4L', data)
+ return rows, cols, xpixel, ypixel
+
+def packRequest_window_change((rows, cols, xpixel, ypixel)):
+ """Pack a window-change request so that it is suitable for sending.
+ """
+ return struct.pack('>4L', cols, rows, xpixel, ypixel)
+
+import connection
diff --git a/twisted/conch/ssh/sexpy.py b/twisted/conch/ssh/sexpy.py
new file mode 100644
index 0000000..60c4328
--- /dev/null
+++ b/twisted/conch/ssh/sexpy.py
@@ -0,0 +1,42 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+def parse(s):
+ s = s.strip()
+ expr = []
+ while s:
+ if s[0] == '(':
+ newSexp = []
+ if expr:
+ expr[-1].append(newSexp)
+ expr.append(newSexp)
+ s = s[1:]
+ continue
+ if s[0] == ')':
+ aList = expr.pop()
+ s=s[1:]
+ if not expr:
+ assert not s
+ return aList
+ continue
+ i = 0
+ while s[i].isdigit(): i+=1
+ assert i
+ length = int(s[:i])
+ data = s[i+1:i+1+length]
+ expr[-1].append(data)
+ s=s[i+1+length:]
+ assert 0, "this should not happen"
+
+def pack(sexp):
+ s = ""
+ for o in sexp:
+ if type(o) in (type(()), type([])):
+ s+='('
+ s+=pack(o)
+ s+=')'
+ else:
+ s+='%i:%s' % (len(o), o)
+ return s
diff --git a/twisted/conch/ssh/transport.py b/twisted/conch/ssh/transport.py
new file mode 100644
index 0000000..66e9828
--- /dev/null
+++ b/twisted/conch/ssh/transport.py
@@ -0,0 +1,1591 @@
+# -*- test-case-name: twisted.conch.test.test_transport -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The lowest level SSH protocol. This handles the key negotiation, the
+encryption and the compression. The transport layer is described in
+RFC 4253.
+
+Maintainer: Paul Swartz
+"""
+
+# base library imports
+import struct
+import zlib
+import array
+
+# external library imports
+from Crypto import Util
+from Crypto.Cipher import XOR
+
+# twisted imports
+from twisted.internet import protocol, defer
+from twisted.conch import error
+from twisted.python import log, randbytes
+from twisted.python.hashlib import md5, sha1
+
+# sibling imports
+from twisted.conch.ssh import keys
+from twisted.conch.ssh.common import NS, getNS, MP, getMP, _MPpow, ffs
+
+
+def _getRandomNumber(random, bits):
+ """
+ Generate a random number in the range [0, 2 ** bits).
+
+ @param bits: The number of bits in the result.
+ @type bits: C{int}
+
+ @rtype: C{int} or C{long}
+ @return: The newly generated random number.
+
+ @raise ValueError: if C{bits} is not a multiple of 8.
+ """
+ if bits % 8:
+ raise ValueError("bits (%d) must be a multiple of 8" % (bits,))
+ bytes = random(bits / 8)
+ result = Util.number.bytes_to_long(bytes)
+ return result
+
+
+
+def _generateX(random, bits):
+ """
+ Generate a new value for the private key x.
+
+ From RFC 2631, section 2.2::
+
+ X9.42 requires that the private key x be in the interval
+ [2, (q - 2)]. x should be randomly generated in this interval.
+ """
+ while True:
+ x = _getRandomNumber(random, bits)
+ if 2 <= x <= (2 ** bits) - 2:
+ return x
+
+
+
+class SSHTransportBase(protocol.Protocol):
+ """
+ Protocol supporting basic SSH functionality: sending/receiving packets
+ and message dispatch. To connect to or run a server, you must use
+ SSHClientTransport or SSHServerTransport.
+
+ @ivar protocolVersion: A string representing the version of the SSH
+ protocol we support. Currently defaults to '2.0'.
+
+ @ivar version: A string representing the version of the server or client.
+ Currently defaults to 'Twisted'.
+
+ @ivar comment: An optional string giving more information about the
+ server or client.
+
+ @ivar supportedCiphers: A list of strings representing the encryption
+ algorithms supported, in order from most-preferred to least.
+
+ @ivar supportedMACs: A list of strings representing the message
+ authentication codes (hashes) supported, in order from most-preferred
+ to least. Both this and supportedCiphers can include 'none' to use
+ no encryption or authentication, but that must be done manually,
+
+ @ivar supportedKeyExchanges: A list of strings representing the
+ key exchanges supported, in order from most-preferred to least.
+
+ @ivar supportedPublicKeys: A list of strings representing the
+ public key types supported, in order from most-preferred to least.
+
+ @ivar supportedCompressions: A list of strings representing compression
+ types supported, from most-preferred to least.
+
+ @ivar supportedLanguages: A list of strings representing languages
+ supported, from most-preferred to least.
+
+ @ivar supportedVersions: A container of strings representing supported ssh
+ protocol version numbers.
+
+ @ivar isClient: A boolean indicating whether this is a client or server.
+
+ @ivar gotVersion: A boolean indicating whether we have receieved the
+ version string from the other side.
+
+ @ivar buf: Data we've received but hasn't been parsed into a packet.
+
+ @ivar outgoingPacketSequence: the sequence number of the next packet we
+ will send.
+
+ @ivar incomingPacketSequence: the sequence number of the next packet we
+ are expecting from the other side.
+
+ @ivar outgoingCompression: an object supporting the .compress(str) and
+ .flush() methods, or None if there is no outgoing compression. Used to
+ compress outgoing data.
+
+ @ivar outgoingCompressionType: A string representing the outgoing
+ compression type.
+
+ @ivar incomingCompression: an object supporting the .decompress(str)
+ method, or None if there is no incoming compression. Used to
+ decompress incoming data.
+
+ @ivar incomingCompressionType: A string representing the incoming
+ compression type.
+
+ @ivar ourVersionString: the version string that we sent to the other side.
+ Used in the key exchange.
+
+ @ivar otherVersionString: the version string sent by the other side. Used
+ in the key exchange.
+
+ @ivar ourKexInitPayload: the MSG_KEXINIT payload we sent. Used in the key
+ exchange.
+
+ @ivar otherKexInitPayload: the MSG_KEXINIT payload we received. Used in
+ the key exchange
+
+ @ivar sessionID: a string that is unique to this SSH session. Created as
+ part of the key exchange, sessionID is used to generate the various
+ encryption and authentication keys.
+
+ @ivar service: an SSHService instance, or None. If it's set to an object,
+ it's the currently running service.
+
+ @ivar kexAlg: the agreed-upon key exchange algorithm.
+
+ @ivar keyAlg: the agreed-upon public key type for the key exchange.
+
+ @ivar currentEncryptions: an SSHCiphers instance. It represents the
+ current encryption and authentication options for the transport.
+
+ @ivar nextEncryptions: an SSHCiphers instance. Held here until the
+ MSG_NEWKEYS messages are exchanged, when nextEncryptions is
+ transitioned to currentEncryptions.
+
+ @ivar first: the first bytes of the next packet. In order to avoid
+ decrypting data twice, the first bytes are decrypted and stored until
+ the whole packet is available.
+
+ @ivar _keyExchangeState: The current protocol state with respect to key
+ exchange. This is either C{_KEY_EXCHANGE_NONE} if no key exchange is in
+ progress (and returns to this value after any key exchange completes),
+ C{_KEY_EXCHANGE_REQUESTED} if this side of the connection initiated a
+ key exchange, and C{_KEY_EXCHANGE_PROGRESSING} if the other side of the
+ connection initiated a key exchange. C{_KEY_EXCHANGE_NONE} is the
+ initial value (however SSH connections begin with key exchange, so it
+ will quickly change to another state).
+
+ @ivar _blockedByKeyExchange: Whenever C{_keyExchangeState} is not
+ C{_KEY_EXCHANGE_NONE}, this is a C{list} of pending messages which were
+ passed to L{sendPacket} but could not be sent because it is not legal to
+ send them while a key exchange is in progress. When the key exchange
+ completes, another attempt is made to send these messages.
+ """
+
+
+ protocolVersion = '2.0'
+ version = 'Twisted'
+ comment = ''
+ ourVersionString = ('SSH-' + protocolVersion + '-' + version + ' '
+ + comment).strip()
+ supportedCiphers = ['aes256-ctr', 'aes256-cbc', 'aes192-ctr', 'aes192-cbc',
+ 'aes128-ctr', 'aes128-cbc', 'cast128-ctr',
+ 'cast128-cbc', 'blowfish-ctr', 'blowfish-cbc',
+ '3des-ctr', '3des-cbc'] # ,'none']
+ supportedMACs = ['hmac-sha1', 'hmac-md5'] # , 'none']
+ # both of the above support 'none', but for security are disabled by
+ # default. to enable them, subclass this class and add it, or do:
+ # SSHTransportBase.supportedCiphers.append('none')
+ supportedKeyExchanges = ['diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group1-sha1']
+ supportedPublicKeys = ['ssh-rsa', 'ssh-dss']
+ supportedCompressions = ['none', 'zlib']
+ supportedLanguages = ()
+ supportedVersions = ('1.99', '2.0')
+ isClient = False
+ gotVersion = False
+ buf = ''
+ outgoingPacketSequence = 0
+ incomingPacketSequence = 0
+ outgoingCompression = None
+ incomingCompression = None
+ sessionID = None
+ service = None
+
+ # There is no key exchange activity in progress.
+ _KEY_EXCHANGE_NONE = '_KEY_EXCHANGE_NONE'
+
+ # Key exchange is in progress and we started it.
+ _KEY_EXCHANGE_REQUESTED = '_KEY_EXCHANGE_REQUESTED'
+
+ # Key exchange is in progress and both sides have sent KEXINIT messages.
+ _KEY_EXCHANGE_PROGRESSING = '_KEY_EXCHANGE_PROGRESSING'
+
+ # There is a fourth conceptual state not represented here: KEXINIT received
+ # but not sent. Since we always send a KEXINIT as soon as we get it, we
+ # can't ever be in that state.
+
+ # The current key exchange state.
+ _keyExchangeState = _KEY_EXCHANGE_NONE
+ _blockedByKeyExchange = None
+
+ def connectionLost(self, reason):
+ if self.service:
+ self.service.serviceStopped()
+ if hasattr(self, 'avatar'):
+ self.logoutFunction()
+ log.msg('connection lost')
+
+
+ def connectionMade(self):
+ """
+ Called when the connection is made to the other side. We sent our
+ version and the MSG_KEXINIT packet.
+ """
+ self.transport.write('%s\r\n' % (self.ourVersionString,))
+ self.currentEncryptions = SSHCiphers('none', 'none', 'none', 'none')
+ self.currentEncryptions.setKeys('', '', '', '', '', '')
+ self.sendKexInit()
+
+
+ def sendKexInit(self):
+ """
+ Send a I{KEXINIT} message to initiate key exchange or to respond to a
+ key exchange initiated by the peer.
+
+ @raise RuntimeError: If a key exchange has already been started and it
+ is not appropriate to send a I{KEXINIT} message at this time.
+
+ @return: C{None}
+ """
+ if self._keyExchangeState != self._KEY_EXCHANGE_NONE:
+ raise RuntimeError(
+ "Cannot send KEXINIT while key exchange state is %r" % (
+ self._keyExchangeState,))
+
+ self.ourKexInitPayload = (chr(MSG_KEXINIT) +
+ randbytes.secureRandom(16) +
+ NS(','.join(self.supportedKeyExchanges)) +
+ NS(','.join(self.supportedPublicKeys)) +
+ NS(','.join(self.supportedCiphers)) +
+ NS(','.join(self.supportedCiphers)) +
+ NS(','.join(self.supportedMACs)) +
+ NS(','.join(self.supportedMACs)) +
+ NS(','.join(self.supportedCompressions)) +
+ NS(','.join(self.supportedCompressions)) +
+ NS(','.join(self.supportedLanguages)) +
+ NS(','.join(self.supportedLanguages)) +
+ '\000' + '\000\000\000\000')
+ self.sendPacket(MSG_KEXINIT, self.ourKexInitPayload[1:])
+ self._keyExchangeState = self._KEY_EXCHANGE_REQUESTED
+ self._blockedByKeyExchange = []
+
+
+ def _allowedKeyExchangeMessageType(self, messageType):
+ """
+ Determine if the given message type may be sent while key exchange is in
+ progress.
+
+ @param messageType: The type of message
+ @type messageType: C{int}
+
+ @return: C{True} if the given type of message may be sent while key
+ exchange is in progress, C{False} if it may not.
+ @rtype: C{bool}
+
+ @see: U{http://tools.ietf.org/html/rfc4253#section-7.1}
+ """
+ # Written somewhat peculularly to reflect the way the specification
+ # defines the allowed message types.
+ if 1 <= messageType <= 19:
+ return messageType not in (MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT)
+ if 20 <= messageType <= 29:
+ return messageType not in (MSG_KEXINIT,)
+ return 30 <= messageType <= 49
+
+
+ def sendPacket(self, messageType, payload):
+ """
+ Sends a packet. If it's been set up, compress the data, encrypt it, and
+ authenticate it before sending. If key exchange is in progress and the
+ message is not part of key exchange, queue it to be sent later.
+
+ @param messageType: The type of the packet; generally one of the
+ MSG_* values.
+ @type messageType: C{int}
+ @param payload: The payload for the message.
+ @type payload: C{str}
+ """
+ if self._keyExchangeState != self._KEY_EXCHANGE_NONE:
+ if not self._allowedKeyExchangeMessageType(messageType):
+ self._blockedByKeyExchange.append((messageType, payload))
+ return
+
+ payload = chr(messageType) + payload
+ if self.outgoingCompression:
+ payload = (self.outgoingCompression.compress(payload)
+ + self.outgoingCompression.flush(2))
+ bs = self.currentEncryptions.encBlockSize
+ # 4 for the packet length and 1 for the padding length
+ totalSize = 5 + len(payload)
+ lenPad = bs - (totalSize % bs)
+ if lenPad < 4:
+ lenPad = lenPad + bs
+ packet = (struct.pack('!LB',
+ totalSize + lenPad - 4, lenPad) +
+ payload + randbytes.secureRandom(lenPad))
+ encPacket = (
+ self.currentEncryptions.encrypt(packet) +
+ self.currentEncryptions.makeMAC(
+ self.outgoingPacketSequence, packet))
+ self.transport.write(encPacket)
+ self.outgoingPacketSequence += 1
+
+
+ def getPacket(self):
+ """
+ Try to return a decrypted, authenticated, and decompressed packet
+ out of the buffer. If there is not enough data, return None.
+
+ @rtype: C{str}/C{None}
+ """
+ bs = self.currentEncryptions.decBlockSize
+ ms = self.currentEncryptions.verifyDigestSize
+ if len(self.buf) < bs: return # not enough data
+ if not hasattr(self, 'first'):
+ first = self.currentEncryptions.decrypt(self.buf[:bs])
+ else:
+ first = self.first
+ del self.first
+ packetLen, paddingLen = struct.unpack('!LB', first[:5])
+ if packetLen > 1048576: # 1024 ** 2
+ self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR,
+ 'bad packet length %s' % packetLen)
+ return
+ if len(self.buf) < packetLen + 4 + ms:
+ self.first = first
+ return # not enough packet
+ if(packetLen + 4) % bs != 0:
+ self.sendDisconnect(
+ DISCONNECT_PROTOCOL_ERROR,
+ 'bad packet mod (%i%%%i == %i)' % (packetLen + 4, bs,
+ (packetLen + 4) % bs))
+ return
+ encData, self.buf = self.buf[:4 + packetLen], self.buf[4 + packetLen:]
+ packet = first + self.currentEncryptions.decrypt(encData[bs:])
+ if len(packet) != 4 + packetLen:
+ self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR,
+ 'bad decryption')
+ return
+ if ms:
+ macData, self.buf = self.buf[:ms], self.buf[ms:]
+ if not self.currentEncryptions.verify(self.incomingPacketSequence,
+ packet, macData):
+ self.sendDisconnect(DISCONNECT_MAC_ERROR, 'bad MAC')
+ return
+ payload = packet[5:-paddingLen]
+ if self.incomingCompression:
+ try:
+ payload = self.incomingCompression.decompress(payload)
+ except: # bare except, because who knows what kind of errors
+ # decompression can raise
+ log.err()
+ self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR,
+ 'compression error')
+ return
+ self.incomingPacketSequence += 1
+ return payload
+
+
+ def _unsupportedVersionReceived(self, remoteVersion):
+ """
+ Called when an unsupported version of the ssh protocol is received from
+ the remote endpoint.
+
+ @param remoteVersion: remote ssh protocol version which is unsupported
+ by us.
+ @type remoteVersion: C{str}
+ """
+ self.sendDisconnect(DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED,
+ 'bad version ' + remoteVersion)
+
+
+ def dataReceived(self, data):
+ """
+ First, check for the version string (SSH-2.0-*). After that has been
+ received, this method adds data to the buffer, and pulls out any
+ packets.
+
+ @type data: C{str}
+ """
+ self.buf = self.buf + data
+ if not self.gotVersion:
+ if self.buf.find('\n', self.buf.find('SSH-')) == -1:
+ return
+ lines = self.buf.split('\n')
+ for p in lines:
+ if p.startswith('SSH-'):
+ self.gotVersion = True
+ self.otherVersionString = p.strip()
+ remoteVersion = p.split('-')[1]
+ if remoteVersion not in self.supportedVersions:
+ self._unsupportedVersionReceived(remoteVersion)
+ return
+ i = lines.index(p)
+ self.buf = '\n'.join(lines[i + 1:])
+ packet = self.getPacket()
+ while packet:
+ messageNum = ord(packet[0])
+ self.dispatchMessage(messageNum, packet[1:])
+ packet = self.getPacket()
+
+
+ def dispatchMessage(self, messageNum, payload):
+ """
+ Send a received message to the appropriate method.
+
+ @type messageNum: C{int}
+ @type payload: c{str}
+ """
+ if messageNum < 50 and messageNum in messages:
+ messageType = messages[messageNum][4:]
+ f = getattr(self, 'ssh_%s' % messageType, None)
+ if f is not None:
+ f(payload)
+ else:
+ log.msg("couldn't handle %s" % messageType)
+ log.msg(repr(payload))
+ self.sendUnimplemented()
+ elif self.service:
+ log.callWithLogger(self.service, self.service.packetReceived,
+ messageNum, payload)
+ else:
+ log.msg("couldn't handle %s" % messageNum)
+ log.msg(repr(payload))
+ self.sendUnimplemented()
+
+
+ # Client-initiated rekeying looks like this:
+ #
+ # C> MSG_KEXINIT
+ # S> MSG_KEXINIT
+ # C> MSG_KEX_DH_GEX_REQUEST or MSG_KEXDH_INIT
+ # S> MSG_KEX_DH_GEX_GROUP or MSG_KEXDH_REPLY
+ # C> MSG_KEX_DH_GEX_INIT or --
+ # S> MSG_KEX_DH_GEX_REPLY or --
+ # C> MSG_NEWKEYS
+ # S> MSG_NEWKEYS
+ #
+ # Server-initiated rekeying is the same, only the first two messages are
+ # switched.
+
+ def ssh_KEXINIT(self, packet):
+ """
+ Called when we receive a MSG_KEXINIT message. Payload::
+ bytes[16] cookie
+ string keyExchangeAlgorithms
+ string keyAlgorithms
+ string incomingEncryptions
+ string outgoingEncryptions
+ string incomingAuthentications
+ string outgoingAuthentications
+ string incomingCompressions
+ string outgoingCompressions
+ string incomingLanguages
+ string outgoingLanguages
+ bool firstPacketFollows
+ unit32 0 (reserved)
+
+ Starts setting up the key exchange, keys, encryptions, and
+ authentications. Extended by ssh_KEXINIT in SSHServerTransport and
+ SSHClientTransport.
+ """
+ self.otherKexInitPayload = chr(MSG_KEXINIT) + packet
+ #cookie = packet[: 16] # taking this is useless
+ k = getNS(packet[16:], 10)
+ strings, rest = k[:-1], k[-1]
+ (kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS,
+ langSC) = [s.split(',') for s in strings]
+ # these are the server directions
+ outs = [encSC, macSC, compSC]
+ ins = [encCS, macSC, compCS]
+ if self.isClient:
+ outs, ins = ins, outs # switch directions
+ server = (self.supportedKeyExchanges, self.supportedPublicKeys,
+ self.supportedCiphers, self.supportedCiphers,
+ self.supportedMACs, self.supportedMACs,
+ self.supportedCompressions, self.supportedCompressions)
+ client = (kexAlgs, keyAlgs, outs[0], ins[0], outs[1], ins[1],
+ outs[2], ins[2])
+ if self.isClient:
+ server, client = client, server
+ self.kexAlg = ffs(client[0], server[0])
+ self.keyAlg = ffs(client[1], server[1])
+ self.nextEncryptions = SSHCiphers(
+ ffs(client[2], server[2]),
+ ffs(client[3], server[3]),
+ ffs(client[4], server[4]),
+ ffs(client[5], server[5]))
+ self.outgoingCompressionType = ffs(client[6], server[6])
+ self.incomingCompressionType = ffs(client[7], server[7])
+ if None in (self.kexAlg, self.keyAlg, self.outgoingCompressionType,
+ self.incomingCompressionType):
+ self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED,
+ "couldn't match all kex parts")
+ return
+ if None in self.nextEncryptions.__dict__.values():
+ self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED,
+ "couldn't match all kex parts")
+ return
+ log.msg('kex alg, key alg: %s %s' % (self.kexAlg, self.keyAlg))
+ log.msg('outgoing: %s %s %s' % (self.nextEncryptions.outCipType,
+ self.nextEncryptions.outMACType,
+ self.outgoingCompressionType))
+ log.msg('incoming: %s %s %s' % (self.nextEncryptions.inCipType,
+ self.nextEncryptions.inMACType,
+ self.incomingCompressionType))
+
+ if self._keyExchangeState == self._KEY_EXCHANGE_REQUESTED:
+ self._keyExchangeState = self._KEY_EXCHANGE_PROGRESSING
+ else:
+ self.sendKexInit()
+
+ return kexAlgs, keyAlgs, rest # for SSHServerTransport to use
+
+
+ def ssh_DISCONNECT(self, packet):
+ """
+ Called when we receive a MSG_DISCONNECT message. Payload::
+ long code
+ string description
+
+ This means that the other side has disconnected. Pass the message up
+ and disconnect ourselves.
+ """
+ reasonCode = struct.unpack('>L', packet[: 4])[0]
+ description, foo = getNS(packet[4:])
+ self.receiveError(reasonCode, description)
+ self.transport.loseConnection()
+
+
+ def ssh_IGNORE(self, packet):
+ """
+ Called when we receieve a MSG_IGNORE message. No payload.
+ This means nothing; we simply return.
+ """
+
+
+ def ssh_UNIMPLEMENTED(self, packet):
+ """
+ Called when we receieve a MSG_UNIMPLEMENTED message. Payload::
+ long packet
+
+ This means that the other side did not implement one of our packets.
+ """
+ seqnum, = struct.unpack('>L', packet)
+ self.receiveUnimplemented(seqnum)
+
+
+ def ssh_DEBUG(self, packet):
+ """
+ Called when we receieve a MSG_DEBUG message. Payload::
+ bool alwaysDisplay
+ string message
+ string language
+
+ This means the other side has passed along some debugging info.
+ """
+ alwaysDisplay = bool(packet[0])
+ message, lang, foo = getNS(packet[1:], 2)
+ self.receiveDebug(alwaysDisplay, message, lang)
+
+
+ def setService(self, service):
+ """
+ Set our service to service and start it running. If we were
+ running a service previously, stop it first.
+
+ @type service: C{SSHService}
+ """
+ log.msg('starting service %s' % service.name)
+ if self.service:
+ self.service.serviceStopped()
+ self.service = service
+ service.transport = self
+ self.service.serviceStarted()
+
+
+ def sendDebug(self, message, alwaysDisplay=False, language=''):
+ """
+ Send a debug message to the other side.
+
+ @param message: the message to send.
+ @type message: C{str}
+ @param alwaysDisplay: if True, tell the other side to always
+ display this message.
+ @type alwaysDisplay: C{bool}
+ @param language: optionally, the language the message is in.
+ @type language: C{str}
+ """
+ self.sendPacket(MSG_DEBUG, chr(alwaysDisplay) + NS(message) +
+ NS(language))
+
+
+ def sendIgnore(self, message):
+ """
+ Send a message that will be ignored by the other side. This is
+ useful to fool attacks based on guessing packet sizes in the
+ encrypted stream.
+
+ @param message: data to send with the message
+ @type message: C{str}
+ """
+ self.sendPacket(MSG_IGNORE, NS(message))
+
+
+ def sendUnimplemented(self):
+ """
+ Send a message to the other side that the last packet was not
+ understood.
+ """
+ seqnum = self.incomingPacketSequence
+ self.sendPacket(MSG_UNIMPLEMENTED, struct.pack('!L', seqnum))
+
+
+ def sendDisconnect(self, reason, desc):
+ """
+ Send a disconnect message to the other side and then disconnect.
+
+ @param reason: the reason for the disconnect. Should be one of the
+ DISCONNECT_* values.
+ @type reason: C{int}
+ @param desc: a descrption of the reason for the disconnection.
+ @type desc: C{str}
+ """
+ self.sendPacket(
+ MSG_DISCONNECT, struct.pack('>L', reason) + NS(desc) + NS(''))
+ log.msg('Disconnecting with error, code %s\nreason: %s' % (reason,
+ desc))
+ self.transport.loseConnection()
+
+
+ def _getKey(self, c, sharedSecret, exchangeHash):
+ """
+ Get one of the keys for authentication/encryption.
+
+ @type c: C{str}
+ @type sharedSecret: C{str}
+ @type exchangeHash: C{str}
+ """
+ k1 = sha1(sharedSecret + exchangeHash + c + self.sessionID)
+ k1 = k1.digest()
+ k2 = sha1(sharedSecret + exchangeHash + k1).digest()
+ return k1 + k2
+
+
+ def _keySetup(self, sharedSecret, exchangeHash):
+ """
+ Set up the keys for the connection and sends MSG_NEWKEYS when
+ finished,
+
+ @param sharedSecret: a secret string agreed upon using a Diffie-
+ Hellman exchange, so it is only shared between
+ the server and the client.
+ @type sharedSecret: C{str}
+ @param exchangeHash: A hash of various data known by both sides.
+ @type exchangeHash: C{str}
+ """
+ if not self.sessionID:
+ self.sessionID = exchangeHash
+ initIVCS = self._getKey('A', sharedSecret, exchangeHash)
+ initIVSC = self._getKey('B', sharedSecret, exchangeHash)
+ encKeyCS = self._getKey('C', sharedSecret, exchangeHash)
+ encKeySC = self._getKey('D', sharedSecret, exchangeHash)
+ integKeyCS = self._getKey('E', sharedSecret, exchangeHash)
+ integKeySC = self._getKey('F', sharedSecret, exchangeHash)
+ outs = [initIVSC, encKeySC, integKeySC]
+ ins = [initIVCS, encKeyCS, integKeyCS]
+ if self.isClient: # reverse for the client
+ log.msg('REVERSE')
+ outs, ins = ins, outs
+ self.nextEncryptions.setKeys(outs[0], outs[1], ins[0], ins[1],
+ outs[2], ins[2])
+ self.sendPacket(MSG_NEWKEYS, '')
+
+
+ def _newKeys(self):
+ """
+ Called back by a subclass once a I{MSG_NEWKEYS} message has been
+ received. This indicates key exchange has completed and new encryption
+ and compression parameters should be adopted. Any messages which were
+ queued during key exchange will also be flushed.
+ """
+ log.msg('NEW KEYS')
+ self.currentEncryptions = self.nextEncryptions
+ if self.outgoingCompressionType == 'zlib':
+ self.outgoingCompression = zlib.compressobj(6)
+ if self.incomingCompressionType == 'zlib':
+ self.incomingCompression = zlib.decompressobj()
+
+ self._keyExchangeState = self._KEY_EXCHANGE_NONE
+ messages = self._blockedByKeyExchange
+ self._blockedByKeyExchange = None
+ for (messageType, payload) in messages:
+ self.sendPacket(messageType, payload)
+
+
+ def isEncrypted(self, direction="out"):
+ """
+ Return True if the connection is encrypted in the given direction.
+ Direction must be one of ["out", "in", "both"].
+ """
+ if direction == "out":
+ return self.currentEncryptions.outCipType != 'none'
+ elif direction == "in":
+ return self.currentEncryptions.inCipType != 'none'
+ elif direction == "both":
+ return self.isEncrypted("in") and self.isEncrypted("out")
+ else:
+ raise TypeError('direction must be "out", "in", or "both"')
+
+
+ def isVerified(self, direction="out"):
+ """
+ Return True if the connecction is verified/authenticated in the
+ given direction. Direction must be one of ["out", "in", "both"].
+ """
+ if direction == "out":
+ return self.currentEncryptions.outMACType != 'none'
+ elif direction == "in":
+ return self.currentEncryptions.inMACType != 'none'
+ elif direction == "both":
+ return self.isVerified("in")and self.isVerified("out")
+ else:
+ raise TypeError('direction must be "out", "in", or "both"')
+
+
+ def loseConnection(self):
+ """
+ Lose the connection to the other side, sending a
+ DISCONNECT_CONNECTION_LOST message.
+ """
+ self.sendDisconnect(DISCONNECT_CONNECTION_LOST,
+ "user closed connection")
+
+
+ # client methods
+ def receiveError(self, reasonCode, description):
+ """
+ Called when we receive a disconnect error message from the other
+ side.
+
+ @param reasonCode: the reason for the disconnect, one of the
+ DISCONNECT_ values.
+ @type reasonCode: C{int}
+ @param description: a human-readable description of the
+ disconnection.
+ @type description: C{str}
+ """
+ log.msg('Got remote error, code %s\nreason: %s' % (reasonCode,
+ description))
+
+
+ def receiveUnimplemented(self, seqnum):
+ """
+ Called when we receive an unimplemented packet message from the other
+ side.
+
+ @param seqnum: the sequence number that was not understood.
+ @type seqnum: C{int}
+ """
+ log.msg('other side unimplemented packet #%s' % seqnum)
+
+
+ def receiveDebug(self, alwaysDisplay, message, lang):
+ """
+ Called when we receive a debug message from the other side.
+
+ @param alwaysDisplay: if True, this message should always be
+ displayed.
+ @type alwaysDisplay: C{bool}
+ @param message: the debug message
+ @type message: C{str}
+ @param lang: optionally the language the message is in.
+ @type lang: C{str}
+ """
+ if alwaysDisplay:
+ log.msg('Remote Debug Message: %s' % message)
+
+
+
+class SSHServerTransport(SSHTransportBase):
+ """
+ SSHServerTransport implements the server side of the SSH protocol.
+
+ @ivar isClient: since we are never the client, this is always False.
+
+ @ivar ignoreNextPacket: if True, ignore the next key exchange packet. This
+ is set when the client sends a guessed key exchange packet but with
+ an incorrect guess.
+
+ @ivar dhGexRequest: the KEX_DH_GEX_REQUEST(_OLD) that the client sent.
+ The key generation needs this to be stored.
+
+ @ivar g: the Diffie-Hellman group generator.
+
+ @ivar p: the Diffie-Hellman group prime.
+ """
+ isClient = False
+ ignoreNextPacket = 0
+
+
+ def ssh_KEXINIT(self, packet):
+ """
+ Called when we receive a MSG_KEXINIT message. For a description
+ of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally,
+ this method checks if a guessed key exchange packet was sent. If
+ it was sent, and it guessed incorrectly, the next key exchange
+ packet MUST be ignored.
+ """
+ retval = SSHTransportBase.ssh_KEXINIT(self, packet)
+ if not retval: # disconnected
+ return
+ else:
+ kexAlgs, keyAlgs, rest = retval
+ if ord(rest[0]): # first_kex_packet_follows
+ if (kexAlgs[0] != self.supportedKeyExchanges[0] or
+ keyAlgs[0] != self.supportedPublicKeys[0]):
+ self.ignoreNextPacket = True # guess was wrong
+
+
+ def _ssh_KEXDH_INIT(self, packet):
+ """
+ Called to handle the beginning of a diffie-hellman-group1-sha1 key
+ exchange.
+
+ Unlike other message types, this is not dispatched automatically. It is
+ called from C{ssh_KEX_DH_GEX_REQUEST_OLD} because an extra check is
+ required to determine if this is really a KEXDH_INIT message or if it is
+ a KEX_DH_GEX_REQUEST_OLD message.
+
+ The KEXDH_INIT (for diffie-hellman-group1-sha1 exchanges) payload::
+
+ integer e (the client's Diffie-Hellman public key)
+
+ We send the KEXDH_REPLY with our host key and signature.
+ """
+ clientDHpublicKey, foo = getMP(packet)
+ y = _getRandomNumber(randbytes.secureRandom, 512)
+ serverDHpublicKey = _MPpow(DH_GENERATOR, y, DH_PRIME)
+ sharedSecret = _MPpow(clientDHpublicKey, y, DH_PRIME)
+ h = sha1()
+ h.update(NS(self.otherVersionString))
+ h.update(NS(self.ourVersionString))
+ h.update(NS(self.otherKexInitPayload))
+ h.update(NS(self.ourKexInitPayload))
+ h.update(NS(self.factory.publicKeys[self.keyAlg].blob()))
+ h.update(MP(clientDHpublicKey))
+ h.update(serverDHpublicKey)
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ self.sendPacket(
+ MSG_KEXDH_REPLY,
+ NS(self.factory.publicKeys[self.keyAlg].blob()) +
+ serverDHpublicKey +
+ NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash)))
+ self._keySetup(sharedSecret, exchangeHash)
+
+
+ def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet):
+ """
+ This represents two different key exchange methods that share the same
+ integer value. If the message is determined to be a KEXDH_INIT,
+ C{_ssh_KEXDH_INIT} is called to handle it. Otherwise, for
+ KEX_DH_GEX_REQUEST_OLD (for diffie-hellman-group-exchange-sha1)
+ payload::
+
+ integer ideal (ideal size for the Diffie-Hellman prime)
+
+ We send the KEX_DH_GEX_GROUP message with the group that is
+ closest in size to ideal.
+
+ If we were told to ignore the next key exchange packet by ssh_KEXINIT,
+ drop it on the floor and return.
+ """
+ if self.ignoreNextPacket:
+ self.ignoreNextPacket = 0
+ return
+
+ # KEXDH_INIT and KEX_DH_GEX_REQUEST_OLD have the same value, so use
+ # another cue to decide what kind of message the peer sent us.
+ if self.kexAlg == 'diffie-hellman-group1-sha1':
+ return self._ssh_KEXDH_INIT(packet)
+ elif self.kexAlg == 'diffie-hellman-group-exchange-sha1':
+ self.dhGexRequest = packet
+ ideal = struct.unpack('>L', packet)[0]
+ self.g, self.p = self.factory.getDHPrime(ideal)
+ self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g))
+ else:
+ raise error.ConchError('bad kexalg: %s' % self.kexAlg)
+
+
+ def ssh_KEX_DH_GEX_REQUEST(self, packet):
+ """
+ Called when we receive a MSG_KEX_DH_GEX_REQUEST message. Payload::
+ integer minimum
+ integer ideal
+ integer maximum
+
+ The client is asking for a Diffie-Hellman group between minimum and
+ maximum size, and close to ideal if possible. We reply with a
+ MSG_KEX_DH_GEX_GROUP message.
+
+ If we were told to ignore the next key exchange packet by ssh_KEXINIT,
+ drop it on the floor and return.
+ """
+ if self.ignoreNextPacket:
+ self.ignoreNextPacket = 0
+ return
+ self.dhGexRequest = packet
+ min, ideal, max = struct.unpack('>3L', packet)
+ self.g, self.p = self.factory.getDHPrime(ideal)
+ self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g))
+
+
+ def ssh_KEX_DH_GEX_INIT(self, packet):
+ """
+ Called when we get a MSG_KEX_DH_GEX_INIT message. Payload::
+ integer e (client DH public key)
+
+ We send the MSG_KEX_DH_GEX_REPLY message with our host key and
+ signature.
+ """
+ clientDHpublicKey, foo = getMP(packet)
+ # TODO: we should also look at the value they send to us and reject
+ # insecure values of f (if g==2 and f has a single '1' bit while the
+ # rest are '0's, then they must have used a small y also).
+
+ # TODO: This could be computed when self.p is set up
+ # or do as openssh does and scan f for a single '1' bit instead
+
+ pSize = Util.number.size(self.p)
+ y = _getRandomNumber(randbytes.secureRandom, pSize)
+
+ serverDHpublicKey = _MPpow(self.g, y, self.p)
+ sharedSecret = _MPpow(clientDHpublicKey, y, self.p)
+ h = sha1()
+ h.update(NS(self.otherVersionString))
+ h.update(NS(self.ourVersionString))
+ h.update(NS(self.otherKexInitPayload))
+ h.update(NS(self.ourKexInitPayload))
+ h.update(NS(self.factory.publicKeys[self.keyAlg].blob()))
+ h.update(self.dhGexRequest)
+ h.update(MP(self.p))
+ h.update(MP(self.g))
+ h.update(MP(clientDHpublicKey))
+ h.update(serverDHpublicKey)
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ self.sendPacket(
+ MSG_KEX_DH_GEX_REPLY,
+ NS(self.factory.publicKeys[self.keyAlg].blob()) +
+ serverDHpublicKey +
+ NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash)))
+ self._keySetup(sharedSecret, exchangeHash)
+
+
+ def ssh_NEWKEYS(self, packet):
+ """
+ Called when we get a MSG_NEWKEYS message. No payload.
+ When we get this, the keys have been set on both sides, and we
+ start using them to encrypt and authenticate the connection.
+ """
+ if packet != '':
+ self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR,
+ "NEWKEYS takes no data")
+ return
+ self._newKeys()
+
+
+ def ssh_SERVICE_REQUEST(self, packet):
+ """
+ Called when we get a MSG_SERVICE_REQUEST message. Payload::
+ string serviceName
+
+ The client has requested a service. If we can start the service,
+ start it; otherwise, disconnect with
+ DISCONNECT_SERVICE_NOT_AVAILABLE.
+ """
+ service, rest = getNS(packet)
+ cls = self.factory.getService(self, service)
+ if not cls:
+ self.sendDisconnect(DISCONNECT_SERVICE_NOT_AVAILABLE,
+ "don't have service %s" % service)
+ return
+ else:
+ self.sendPacket(MSG_SERVICE_ACCEPT, NS(service))
+ self.setService(cls())
+
+
+
+class SSHClientTransport(SSHTransportBase):
+ """
+ SSHClientTransport implements the client side of the SSH protocol.
+
+ @ivar isClient: since we are always the client, this is always True.
+
+ @ivar _gotNewKeys: if we receive a MSG_NEWKEYS message before we are
+ ready to transition to the new keys, this is set to True so we
+ can transition when the keys are ready locally.
+
+ @ivar x: our Diffie-Hellman private key.
+
+ @ivar e: our Diffie-Hellman public key.
+
+ @ivar g: the Diffie-Hellman group generator.
+
+ @ivar p: the Diffie-Hellman group prime
+
+ @ivar instance: the SSHService object we are requesting.
+ """
+ isClient = True
+
+ def connectionMade(self):
+ """
+ Called when the connection is started with the server. Just sets
+ up a private instance variable.
+ """
+ SSHTransportBase.connectionMade(self)
+ self._gotNewKeys = 0
+
+
+ def ssh_KEXINIT(self, packet):
+ """
+ Called when we receive a MSG_KEXINIT message. For a description
+ of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally,
+ this method sends the first key exchange packet. If the agreed-upon
+ exchange is diffie-hellman-group1-sha1, generate a public key
+ and send it in a MSG_KEXDH_INIT message. If the exchange is
+ diffie-hellman-group-exchange-sha1, ask for a 2048 bit group with a
+ MSG_KEX_DH_GEX_REQUEST_OLD message.
+ """
+ if SSHTransportBase.ssh_KEXINIT(self, packet) is None:
+ return # we disconnected
+ if self.kexAlg == 'diffie-hellman-group1-sha1':
+ self.x = _generateX(randbytes.secureRandom, 512)
+ self.e = _MPpow(DH_GENERATOR, self.x, DH_PRIME)
+ self.sendPacket(MSG_KEXDH_INIT, self.e)
+ elif self.kexAlg == 'diffie-hellman-group-exchange-sha1':
+ self.sendPacket(MSG_KEX_DH_GEX_REQUEST_OLD, '\x00\x00\x08\x00')
+ else:
+ raise error.ConchError("somehow, the kexAlg has been set "
+ "to something we don't support")
+
+
+ def _ssh_KEXDH_REPLY(self, packet):
+ """
+ Called to handle a reply to a diffie-hellman-group1-sha1 key exchange
+ message (KEXDH_INIT).
+
+ Like the handler for I{KEXDH_INIT}, this message type has an overlapping
+ value. This method is called from C{ssh_KEX_DH_GEX_GROUP} if that
+ method detects a diffie-hellman-group1-sha1 key exchange is in progress.
+
+ Payload::
+
+ string serverHostKey
+ integer f (server Diffie-Hellman public key)
+ string signature
+
+ We verify the host key by calling verifyHostKey, then continue in
+ _continueKEXDH_REPLY.
+ """
+ pubKey, packet = getNS(packet)
+ f, packet = getMP(packet)
+ signature, packet = getNS(packet)
+ fingerprint = ':'.join([ch.encode('hex') for ch in
+ md5(pubKey).digest()])
+ d = self.verifyHostKey(pubKey, fingerprint)
+ d.addCallback(self._continueKEXDH_REPLY, pubKey, f, signature)
+ d.addErrback(
+ lambda unused: self.sendDisconnect(
+ DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key'))
+ return d
+
+
+ def ssh_KEX_DH_GEX_GROUP(self, packet):
+ """
+ This handles two different message which share an integer value.
+
+ If the key exchange is diffie-hellman-group-exchange-sha1, this is
+ MSG_KEX_DH_GEX_GROUP. Payload::
+ string g (group generator)
+ string p (group prime)
+
+ We generate a Diffie-Hellman public key and send it in a
+ MSG_KEX_DH_GEX_INIT message.
+ """
+ if self.kexAlg == 'diffie-hellman-group1-sha1':
+ return self._ssh_KEXDH_REPLY(packet)
+ else:
+ self.p, rest = getMP(packet)
+ self.g, rest = getMP(rest)
+ self.x = _generateX(randbytes.secureRandom, 320)
+ self.e = _MPpow(self.g, self.x, self.p)
+ self.sendPacket(MSG_KEX_DH_GEX_INIT, self.e)
+
+
+ def _continueKEXDH_REPLY(self, ignored, pubKey, f, signature):
+ """
+ The host key has been verified, so we generate the keys.
+
+ @param pubKey: the public key blob for the server's public key.
+ @type pubKey: C{str}
+ @param f: the server's Diffie-Hellman public key.
+ @type f: C{long}
+ @param signature: the server's signature, verifying that it has the
+ correct private key.
+ @type signature: C{str}
+ """
+ serverKey = keys.Key.fromString(pubKey)
+ sharedSecret = _MPpow(f, self.x, DH_PRIME)
+ h = sha1()
+ h.update(NS(self.ourVersionString))
+ h.update(NS(self.otherVersionString))
+ h.update(NS(self.ourKexInitPayload))
+ h.update(NS(self.otherKexInitPayload))
+ h.update(NS(pubKey))
+ h.update(self.e)
+ h.update(MP(f))
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ if not serverKey.verify(signature, exchangeHash):
+ self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED,
+ 'bad signature')
+ return
+ self._keySetup(sharedSecret, exchangeHash)
+
+
+ def ssh_KEX_DH_GEX_REPLY(self, packet):
+ """
+ Called when we receieve a MSG_KEX_DH_GEX_REPLY message. Payload::
+ string server host key
+ integer f (server DH public key)
+
+ We verify the host key by calling verifyHostKey, then continue in
+ _continueGEX_REPLY.
+ """
+ pubKey, packet = getNS(packet)
+ f, packet = getMP(packet)
+ signature, packet = getNS(packet)
+ fingerprint = ':'.join(map(lambda c: '%02x'%ord(c),
+ md5(pubKey).digest()))
+ d = self.verifyHostKey(pubKey, fingerprint)
+ d.addCallback(self._continueGEX_REPLY, pubKey, f, signature)
+ d.addErrback(
+ lambda unused: self.sendDisconnect(
+ DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key'))
+ return d
+
+
+ def _continueGEX_REPLY(self, ignored, pubKey, f, signature):
+ """
+ The host key has been verified, so we generate the keys.
+
+ @param pubKey: the public key blob for the server's public key.
+ @type pubKey: C{str}
+ @param f: the server's Diffie-Hellman public key.
+ @type f: C{long}
+ @param signature: the server's signature, verifying that it has the
+ correct private key.
+ @type signature: C{str}
+ """
+ serverKey = keys.Key.fromString(pubKey)
+ sharedSecret = _MPpow(f, self.x, self.p)
+ h = sha1()
+ h.update(NS(self.ourVersionString))
+ h.update(NS(self.otherVersionString))
+ h.update(NS(self.ourKexInitPayload))
+ h.update(NS(self.otherKexInitPayload))
+ h.update(NS(pubKey))
+ h.update('\x00\x00\x08\x00')
+ h.update(MP(self.p))
+ h.update(MP(self.g))
+ h.update(self.e)
+ h.update(MP(f))
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ if not serverKey.verify(signature, exchangeHash):
+ self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED,
+ 'bad signature')
+ return
+ self._keySetup(sharedSecret, exchangeHash)
+
+
+ def _keySetup(self, sharedSecret, exchangeHash):
+ """
+ See SSHTransportBase._keySetup().
+ """
+ SSHTransportBase._keySetup(self, sharedSecret, exchangeHash)
+ if self._gotNewKeys:
+ self.ssh_NEWKEYS('')
+
+
+ def ssh_NEWKEYS(self, packet):
+ """
+ Called when we receieve a MSG_NEWKEYS message. No payload.
+ If we've finished setting up our own keys, start using them.
+ Otherwise, remeber that we've receieved this message.
+ """
+ if packet != '':
+ self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR,
+ "NEWKEYS takes no data")
+ return
+ if not self.nextEncryptions.encBlockSize:
+ self._gotNewKeys = 1
+ return
+ self._newKeys()
+ self.connectionSecure()
+
+
+ def ssh_SERVICE_ACCEPT(self, packet):
+ """
+ Called when we receieve a MSG_SERVICE_ACCEPT message. Payload::
+ string service name
+
+ Start the service we requested.
+ """
+ name = getNS(packet)[0]
+ if name != self.instance.name:
+ self.sendDisconnect(
+ DISCONNECT_PROTOCOL_ERROR,
+ "received accept for service we did not request")
+ self.setService(self.instance)
+
+
+ def requestService(self, instance):
+ """
+ Request that a service be run over this transport.
+
+ @type instance: subclass of L{twisted.conch.ssh.service.SSHService}
+ """
+ self.sendPacket(MSG_SERVICE_REQUEST, NS(instance.name))
+ self.instance = instance
+
+
+ # client methods
+ def verifyHostKey(self, hostKey, fingerprint):
+ """
+ Returns a Deferred that gets a callback if it is a valid key, or
+ an errback if not.
+
+ @type hostKey: C{str}
+ @type fingerprint: C{str}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ # return if it's good
+ return defer.fail(NotImplementedError())
+
+
+ def connectionSecure(self):
+ """
+ Called when the encryption has been set up. Generally,
+ requestService() is called to run another service over the transport.
+ """
+ raise NotImplementedError()
+
+
+
+class _DummyCipher:
+ """
+ A cipher for the none encryption method.
+
+ @ivar block_size: the block size of the encryption. In the case of the
+ none cipher, this is 8 bytes.
+ """
+ block_size = 8
+
+
+ def encrypt(self, x):
+ return x
+
+
+ decrypt = encrypt
+
+
+class SSHCiphers:
+ """
+ SSHCiphers represents all the encryption operations that need to occur
+ to encrypt and authenticate the SSH connection.
+
+ @cvar cipherMap: A dictionary mapping SSH encryption names to 3-tuples of
+ (<Crypto.Cipher.* name>, <block size>, <counter mode>)
+ @cvar macMap: A dictionary mapping SSH MAC names to hash modules.
+
+ @ivar outCipType: the string type of the outgoing cipher.
+ @ivar inCipType: the string type of the incoming cipher.
+ @ivar outMACType: the string type of the incoming MAC.
+ @ivar inMACType: the string type of the incoming MAC.
+ @ivar encBlockSize: the block size of the outgoing cipher.
+ @ivar decBlockSize: the block size of the incoming cipher.
+ @ivar verifyDigestSize: the size of the incoming MAC.
+ @ivar outMAC: a tuple of (<hash module>, <inner key>, <outer key>,
+ <digest size>) representing the outgoing MAC.
+ @ivar inMAc: see outMAC, but for the incoming MAC.
+ """
+
+
+ cipherMap = {
+ '3des-cbc':('DES3', 24, 0),
+ 'blowfish-cbc':('Blowfish', 16,0 ),
+ 'aes256-cbc':('AES', 32, 0),
+ 'aes192-cbc':('AES', 24, 0),
+ 'aes128-cbc':('AES', 16, 0),
+ 'cast128-cbc':('CAST', 16, 0),
+ 'aes128-ctr':('AES', 16, 1),
+ 'aes192-ctr':('AES', 24, 1),
+ 'aes256-ctr':('AES', 32, 1),
+ '3des-ctr':('DES3', 24, 1),
+ 'blowfish-ctr':('Blowfish', 16, 1),
+ 'cast128-ctr':('CAST', 16, 1),
+ 'none':(None, 0, 0),
+ }
+ macMap = {
+ 'hmac-sha1': sha1,
+ 'hmac-md5': md5,
+ 'none': None
+ }
+
+
+ def __init__(self, outCip, inCip, outMac, inMac):
+ self.outCipType = outCip
+ self.inCipType = inCip
+ self.outMACType = outMac
+ self.inMACType = inMac
+ self.encBlockSize = 0
+ self.decBlockSize = 0
+ self.verifyDigestSize = 0
+ self.outMAC = (None, '', '', 0)
+ self.inMAC = (None, '', '', 0)
+
+
+ def setKeys(self, outIV, outKey, inIV, inKey, outInteg, inInteg):
+ """
+ Set up the ciphers and hashes using the given keys,
+
+ @param outIV: the outgoing initialization vector
+ @param outKey: the outgoing encryption key
+ @param inIV: the incoming initialization vector
+ @param inKey: the incoming encryption key
+ @param outInteg: the outgoing integrity key
+ @param inInteg: the incoming integrity key.
+ """
+ o = self._getCipher(self.outCipType, outIV, outKey)
+ self.encrypt = o.encrypt
+ self.encBlockSize = o.block_size
+ o = self._getCipher(self.inCipType, inIV, inKey)
+ self.decrypt = o.decrypt
+ self.decBlockSize = o.block_size
+ self.outMAC = self._getMAC(self.outMACType, outInteg)
+ self.inMAC = self._getMAC(self.inMACType, inInteg)
+ if self.inMAC:
+ self.verifyDigestSize = self.inMAC[3]
+
+
+ def _getCipher(self, cip, iv, key):
+ """
+ Creates an initialized cipher object.
+
+ @param cip: the name of the cipher: maps into Crypto.Cipher.*
+ @param iv: the initialzation vector
+ @param key: the encryption key
+ """
+ modName, keySize, counterMode = self.cipherMap[cip]
+ if not modName: # no cipher
+ return _DummyCipher()
+ mod = __import__('Crypto.Cipher.%s'%modName, {}, {}, 'x')
+ if counterMode:
+ return mod.new(key[:keySize], mod.MODE_CTR, iv[:mod.block_size],
+ counter=_Counter(iv, mod.block_size))
+ else:
+ return mod.new(key[:keySize], mod.MODE_CBC, iv[:mod.block_size])
+
+
+ def _getMAC(self, mac, key):
+ """
+ Gets a 4-tuple representing the message authentication code.
+ (<hash module>, <inner hash value>, <outer hash value>,
+ <digest size>)
+
+ @param mac: a key mapping into macMap
+ @type mac: C{str}
+ @param key: the MAC key.
+ @type key: C{str}
+ """
+ mod = self.macMap[mac]
+ if not mod:
+ return (None, '', '', 0)
+ ds = mod().digest_size
+ key = key[:ds] + '\x00' * (64 - ds)
+ i = XOR.new('\x36').encrypt(key)
+ o = XOR.new('\x5c').encrypt(key)
+ return mod, i, o, ds
+
+
+ def encrypt(self, blocks):
+ """
+ Encrypt blocks. Overridden by the encrypt method of a
+ Crypto.Cipher.* object in setKeys().
+
+ @type blocks: C{str}
+ """
+ raise NotImplementedError()
+
+
+ def decrypt(self, blocks):
+ """
+ Decrypt blocks. See encrypt().
+
+ @type blocks: C{str}
+ """
+ raise NotImplementedError()
+
+
+ def makeMAC(self, seqid, data):
+ """
+ Create a message authentication code (MAC) for the given packet using
+ the outgoing MAC values.
+
+ @param seqid: the sequence ID of the outgoing packet
+ @type seqid: C{int}
+ @param data: the data to create a MAC for
+ @type data: C{str}
+ @rtype: C{str}
+ """
+ if not self.outMAC[0]:
+ return ''
+ data = struct.pack('>L', seqid) + data
+ mod, i, o, ds = self.outMAC
+ inner = mod(i + data)
+ outer = mod(o + inner.digest())
+ return outer.digest()
+
+
+ def verify(self, seqid, data, mac):
+ """
+ Verify an incoming MAC using the incoming MAC values. Return True
+ if the MAC is valid.
+
+ @param seqid: the sequence ID of the incoming packet
+ @type seqid: C{int}
+ @param data: the packet data to verify
+ @type data: C{str}
+ @param mac: the MAC sent with the packet
+ @type mac: C{str}
+ @rtype: C{bool}
+ """
+ if not self.inMAC[0]:
+ return mac == ''
+ data = struct.pack('>L', seqid) + data
+ mod, i, o, ds = self.inMAC
+ inner = mod(i + data)
+ outer = mod(o + inner.digest())
+ return mac == outer.digest()
+
+
+
+class _Counter:
+ """
+ Stateful counter which returns results packed in a byte string
+ """
+
+
+ def __init__(self, initialVector, blockSize):
+ """
+ @type initialVector: C{str}
+ @param initialVector: A byte string representing the initial counter
+ value.
+ @type blockSize: C{int}
+ @param blockSize: The length of the output buffer, as well as the
+ number of bytes at the beginning of C{initialVector} to consider.
+ """
+ initialVector = initialVector[:blockSize]
+ self.count = getMP('\xff\xff\xff\xff' + initialVector)[0]
+ self.blockSize = blockSize
+ self.count = Util.number.long_to_bytes(self.count - 1)
+ self.count = '\x00' * (self.blockSize - len(self.count)) + self.count
+ self.count = array.array('c', self.count)
+ self.len = len(self.count) - 1
+
+
+ def __call__(self):
+ """
+ Increment the counter and return the new value.
+ """
+ i = self.len
+ while i > -1:
+ self.count[i] = n = chr((ord(self.count[i]) + 1) % 256)
+ if n == '\x00':
+ i -= 1
+ else:
+ return self.count.tostring()
+
+ self.count = array.array('c', '\x00' * self.blockSize)
+ return self.count.tostring()
+
+
+
+# Diffie-Hellman primes from Oakley Group 2 [RFC 2409]
+DH_PRIME = long('17976931348623159077083915679378745319786029604875601170644'
+'442368419718021615851936894783379586492554150218056548598050364644054819923'
+'910005079287700335581663922955313623907650873575991482257486257500742530207'
+'744771258955095793777842444242661733472762929938766870920560605027081084290'
+'7692932019128194467627007L')
+DH_GENERATOR = 2L
+
+
+
+MSG_DISCONNECT = 1
+MSG_IGNORE = 2
+MSG_UNIMPLEMENTED = 3
+MSG_DEBUG = 4
+MSG_SERVICE_REQUEST = 5
+MSG_SERVICE_ACCEPT = 6
+MSG_KEXINIT = 20
+MSG_NEWKEYS = 21
+MSG_KEXDH_INIT = 30
+MSG_KEXDH_REPLY = 31
+MSG_KEX_DH_GEX_REQUEST_OLD = 30
+MSG_KEX_DH_GEX_REQUEST = 34
+MSG_KEX_DH_GEX_GROUP = 31
+MSG_KEX_DH_GEX_INIT = 32
+MSG_KEX_DH_GEX_REPLY = 33
+
+
+
+DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1
+DISCONNECT_PROTOCOL_ERROR = 2
+DISCONNECT_KEY_EXCHANGE_FAILED = 3
+DISCONNECT_RESERVED = 4
+DISCONNECT_MAC_ERROR = 5
+DISCONNECT_COMPRESSION_ERROR = 6
+DISCONNECT_SERVICE_NOT_AVAILABLE = 7
+DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8
+DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9
+DISCONNECT_CONNECTION_LOST = 10
+DISCONNECT_BY_APPLICATION = 11
+DISCONNECT_TOO_MANY_CONNECTIONS = 12
+DISCONNECT_AUTH_CANCELLED_BY_USER = 13
+DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14
+DISCONNECT_ILLEGAL_USER_NAME = 15
+
+
+
+messages = {}
+for name, value in globals().items():
+ # Avoid legacy messages which overlap with never ones
+ if name.startswith('MSG_') and not name.startswith('MSG_KEXDH_'):
+ messages[value] = name
+# Check for regressions (#5352)
+if 'MSG_KEXDH_INIT' in messages or 'MSG_KEXDH_REPLY' in messages:
+ raise RuntimeError(
+ "legacy SSH mnemonics should not end up in messages dict")
diff --git a/twisted/conch/ssh/userauth.py b/twisted/conch/ssh/userauth.py
new file mode 100644
index 0000000..6feef0b
--- /dev/null
+++ b/twisted/conch/ssh/userauth.py
@@ -0,0 +1,846 @@
+# -*- test-case-name: twisted.conch.test.test_userauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation of the ssh-userauth service.
+Currently implemented authentication types are public-key and password.
+
+Maintainer: Paul Swartz
+"""
+
+import struct, warnings
+from twisted.conch import error, interfaces
+from twisted.conch.ssh import keys, transport, service
+from twisted.conch.ssh.common import NS, getNS
+from twisted.cred import credentials
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer, reactor
+from twisted.python import failure, log
+
+
+
+class SSHUserAuthServer(service.SSHService):
+ """
+ A service implementing the server side of the 'ssh-userauth' service. It
+ is used to authenticate the user on the other side as being able to access
+ this server.
+
+ @ivar name: the name of this service: 'ssh-userauth'
+ @type name: C{str}
+ @ivar authenticatedWith: a list of authentication methods that have
+ already been used.
+ @type authenticatedWith: C{list}
+ @ivar loginTimeout: the number of seconds we wait before disconnecting
+ the user for taking too long to authenticate
+ @type loginTimeout: C{int}
+ @ivar attemptsBeforeDisconnect: the number of failed login attempts we
+ allow before disconnecting.
+ @type attemptsBeforeDisconnect: C{int}
+ @ivar loginAttempts: the number of login attempts that have been made
+ @type loginAttempts: C{int}
+ @ivar passwordDelay: the number of seconds to delay when the user gives
+ an incorrect password
+ @type passwordDelay: C{int}
+ @ivar interfaceToMethod: a C{dict} mapping credential interfaces to
+ authentication methods. The server checks to see which of the
+ cred interfaces have checkers and tells the client that those methods
+ are valid for authentication.
+ @type interfaceToMethod: C{dict}
+ @ivar supportedAuthentications: A list of the supported authentication
+ methods.
+ @type supportedAuthentications: C{list} of C{str}
+ @ivar user: the last username the client tried to authenticate with
+ @type user: C{str}
+ @ivar method: the current authentication method
+ @type method: C{str}
+ @ivar nextService: the service the user wants started after authentication
+ has been completed.
+ @type nextService: C{str}
+ @ivar portal: the L{twisted.cred.portal.Portal} we are using for
+ authentication
+ @type portal: L{twisted.cred.portal.Portal}
+ @ivar clock: an object with a callLater method. Stubbed out for testing.
+ """
+
+
+ name = 'ssh-userauth'
+ loginTimeout = 10 * 60 * 60
+ # 10 minutes before we disconnect them
+ attemptsBeforeDisconnect = 20
+ # 20 login attempts before a disconnect
+ passwordDelay = 1 # number of seconds to delay on a failed password
+ clock = reactor
+ interfaceToMethod = {
+ credentials.ISSHPrivateKey : 'publickey',
+ credentials.IUsernamePassword : 'password',
+ credentials.IPluggableAuthenticationModules : 'keyboard-interactive',
+ }
+
+
+ def serviceStarted(self):
+ """
+ Called when the userauth service is started. Set up instance
+ variables, check if we should allow password/keyboard-interactive
+ authentication (only allow if the outgoing connection is encrypted) and
+ set up a login timeout.
+ """
+ self.authenticatedWith = []
+ self.loginAttempts = 0
+ self.user = None
+ self.nextService = None
+ self._pamDeferred = None
+ self.portal = self.transport.factory.portal
+
+ self.supportedAuthentications = []
+ for i in self.portal.listCredentialsInterfaces():
+ if i in self.interfaceToMethod:
+ self.supportedAuthentications.append(self.interfaceToMethod[i])
+
+ if not self.transport.isEncrypted('in'):
+ # don't let us transport password in plaintext
+ if 'password' in self.supportedAuthentications:
+ self.supportedAuthentications.remove('password')
+ if 'keyboard-interactive' in self.supportedAuthentications:
+ self.supportedAuthentications.remove('keyboard-interactive')
+ self._cancelLoginTimeout = self.clock.callLater(
+ self.loginTimeout,
+ self.timeoutAuthentication)
+
+
+ def serviceStopped(self):
+ """
+ Called when the userauth service is stopped. Cancel the login timeout
+ if it's still going.
+ """
+ if self._cancelLoginTimeout:
+ self._cancelLoginTimeout.cancel()
+ self._cancelLoginTimeout = None
+
+
+ def timeoutAuthentication(self):
+ """
+ Called when the user has timed out on authentication. Disconnect
+ with a DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE message.
+ """
+ self._cancelLoginTimeout = None
+ self.transport.sendDisconnect(
+ transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
+ 'you took too long')
+
+
+ def tryAuth(self, kind, user, data):
+ """
+ Try to authenticate the user with the given method. Dispatches to a
+ auth_* method.
+
+ @param kind: the authentication method to try.
+ @type kind: C{str}
+ @param user: the username the client is authenticating with.
+ @type user: C{str}
+ @param data: authentication specific data sent by the client.
+ @type data: C{str}
+ @return: A Deferred called back if the method succeeded, or erred back
+ if it failed.
+ @rtype: C{defer.Deferred}
+ """
+ log.msg('%s trying auth %s' % (user, kind))
+ if kind not in self.supportedAuthentications:
+ return defer.fail(
+ error.ConchError('unsupported authentication, failing'))
+ kind = kind.replace('-', '_')
+ f = getattr(self,'auth_%s'%kind, None)
+ if f:
+ ret = f(data)
+ if not ret:
+ return defer.fail(
+ error.ConchError('%s return None instead of a Deferred'
+ % kind))
+ else:
+ return ret
+ return defer.fail(error.ConchError('bad auth type: %s' % kind))
+
+
+ def ssh_USERAUTH_REQUEST(self, packet):
+ """
+ The client has requested authentication. Payload::
+ string user
+ string next service
+ string method
+ <authentication specific data>
+
+ @type packet: C{str}
+ """
+ user, nextService, method, rest = getNS(packet, 3)
+ if user != self.user or nextService != self.nextService:
+ self.authenticatedWith = [] # clear auth state
+ self.user = user
+ self.nextService = nextService
+ self.method = method
+ d = self.tryAuth(method, user, rest)
+ if not d:
+ self._ebBadAuth(
+ failure.Failure(error.ConchError('auth returned none')))
+ return
+ d.addCallback(self._cbFinishedAuth)
+ d.addErrback(self._ebMaybeBadAuth)
+ d.addErrback(self._ebBadAuth)
+ return d
+
+
+ def _cbFinishedAuth(self, (interface, avatar, logout)):
+ """
+ The callback when user has successfully been authenticated. For a
+ description of the arguments, see L{twisted.cred.portal.Portal.login}.
+ We start the service requested by the user.
+ """
+ self.transport.avatar = avatar
+ self.transport.logoutFunction = logout
+ service = self.transport.factory.getService(self.transport,
+ self.nextService)
+ if not service:
+ raise error.ConchError('could not get next service: %s'
+ % self.nextService)
+ log.msg('%s authenticated with %s' % (self.user, self.method))
+ self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '')
+ self.transport.setService(service())
+
+
+ def _ebMaybeBadAuth(self, reason):
+ """
+ An intermediate errback. If the reason is
+ error.NotEnoughAuthentication, we send a MSG_USERAUTH_FAILURE, but
+ with the partial success indicator set.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+ reason.trap(error.NotEnoughAuthentication)
+ self.transport.sendPacket(MSG_USERAUTH_FAILURE,
+ NS(','.join(self.supportedAuthentications)) + '\xff')
+
+
+ def _ebBadAuth(self, reason):
+ """
+ The final errback in the authentication chain. If the reason is
+ error.IgnoreAuthentication, we simply return; the authentication
+ method has sent its own response. Otherwise, send a failure message
+ and (if the method is not 'none') increment the number of login
+ attempts.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+ if reason.check(error.IgnoreAuthentication):
+ return
+ if self.method != 'none':
+ log.msg('%s failed auth %s' % (self.user, self.method))
+ if reason.check(UnauthorizedLogin):
+ log.msg('unauthorized login: %s' % reason.getErrorMessage())
+ elif reason.check(error.ConchError):
+ log.msg('reason: %s' % reason.getErrorMessage())
+ else:
+ log.msg(reason.getTraceback())
+ self.loginAttempts += 1
+ if self.loginAttempts > self.attemptsBeforeDisconnect:
+ self.transport.sendDisconnect(
+ transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
+ 'too many bad auths')
+ return
+ self.transport.sendPacket(
+ MSG_USERAUTH_FAILURE,
+ NS(','.join(self.supportedAuthentications)) + '\x00')
+
+
+ def auth_publickey(self, packet):
+ """
+ Public key authentication. Payload::
+ byte has signature
+ string algorithm name
+ string key blob
+ [string signature] (if has signature is True)
+
+ Create a SSHPublicKey credential and verify it using our portal.
+ """
+ hasSig = ord(packet[0])
+ algName, blob, rest = getNS(packet[1:], 2)
+ pubKey = keys.Key.fromString(blob)
+ signature = hasSig and getNS(rest)[0] or None
+ if hasSig:
+ b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) +
+ NS(self.user) + NS(self.nextService) + NS('publickey') +
+ chr(hasSig) + NS(pubKey.sshType()) + NS(blob))
+ c = credentials.SSHPrivateKey(self.user, algName, blob, b,
+ signature)
+ return self.portal.login(c, None, interfaces.IConchUser)
+ else:
+ c = credentials.SSHPrivateKey(self.user, algName, blob, None, None)
+ return self.portal.login(c, None,
+ interfaces.IConchUser).addErrback(self._ebCheckKey,
+ packet[1:])
+
+
+ def _ebCheckKey(self, reason, packet):
+ """
+ Called back if the user did not sent a signature. If reason is
+ error.ValidPublicKey then this key is valid for the user to
+ authenticate with. Send MSG_USERAUTH_PK_OK.
+ """
+ reason.trap(error.ValidPublicKey)
+ # if we make it here, it means that the publickey is valid
+ self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet)
+ return failure.Failure(error.IgnoreAuthentication())
+
+
+ def auth_password(self, packet):
+ """
+ Password authentication. Payload::
+ string password
+
+ Make a UsernamePassword credential and verify it with our portal.
+ """
+ password = getNS(packet[1:])[0]
+ c = credentials.UsernamePassword(self.user, password)
+ return self.portal.login(c, None, interfaces.IConchUser).addErrback(
+ self._ebPassword)
+
+
+ def _ebPassword(self, f):
+ """
+ If the password is invalid, wait before sending the failure in order
+ to delay brute-force password guessing.
+ """
+ d = defer.Deferred()
+ self.clock.callLater(self.passwordDelay, d.callback, f)
+ return d
+
+
+ def auth_keyboard_interactive(self, packet):
+ """
+ Keyboard interactive authentication. No payload. We create a
+ PluggableAuthenticationModules credential and authenticate with our
+ portal.
+ """
+ if self._pamDeferred is not None:
+ self.transport.sendDisconnect(
+ transport.DISCONNECT_PROTOCOL_ERROR,
+ "only one keyboard interactive attempt at a time")
+ return defer.fail(error.IgnoreAuthentication())
+ c = credentials.PluggableAuthenticationModules(self.user,
+ self._pamConv)
+ return self.portal.login(c, None, interfaces.IConchUser)
+
+
+ def _pamConv(self, items):
+ """
+ Convert a list of PAM authentication questions into a
+ MSG_USERAUTH_INFO_REQUEST. Returns a Deferred that will be called
+ back when the user has responses to the questions.
+
+ @param items: a list of 2-tuples (message, kind). We only care about
+ kinds 1 (password) and 2 (text).
+ @type items: C{list}
+ @rtype: L{defer.Deferred}
+ """
+ resp = []
+ for message, kind in items:
+ if kind == 1: # password
+ resp.append((message, 0))
+ elif kind == 2: # text
+ resp.append((message, 1))
+ elif kind in (3, 4):
+ return defer.fail(error.ConchError(
+ 'cannot handle PAM 3 or 4 messages'))
+ else:
+ return defer.fail(error.ConchError(
+ 'bad PAM auth kind %i' % kind))
+ packet = NS('') + NS('') + NS('')
+ packet += struct.pack('>L', len(resp))
+ for prompt, echo in resp:
+ packet += NS(prompt)
+ packet += chr(echo)
+ self.transport.sendPacket(MSG_USERAUTH_INFO_REQUEST, packet)
+ self._pamDeferred = defer.Deferred()
+ return self._pamDeferred
+
+
+ def ssh_USERAUTH_INFO_RESPONSE(self, packet):
+ """
+ The user has responded with answers to PAMs authentication questions.
+ Parse the packet into a PAM response and callback self._pamDeferred.
+ Payload::
+ uint32 numer of responses
+ string response 1
+ ...
+ string response n
+ """
+ d, self._pamDeferred = self._pamDeferred, None
+
+ try:
+ resp = []
+ numResps = struct.unpack('>L', packet[:4])[0]
+ packet = packet[4:]
+ while len(resp) < numResps:
+ response, packet = getNS(packet)
+ resp.append((response, 0))
+ if packet:
+ raise error.ConchError("%i bytes of extra data" % len(packet))
+ except:
+ d.errback(failure.Failure())
+ else:
+ d.callback(resp)
+
+
+
+class SSHUserAuthClient(service.SSHService):
+ """
+ A service implementing the client side of 'ssh-userauth'.
+
+ @ivar name: the name of this service: 'ssh-userauth'
+ @type name: C{str}
+ @ivar preferredOrder: a list of authentication methods we support, in
+ order of preference. The client will try authentication methods in
+ this order, making callbacks for information when necessary.
+ @type preferredOrder: C{list}
+ @ivar user: the name of the user to authenticate as
+ @type user: C{str}
+ @ivar instance: the service to start after authentication has finished
+ @type instance: L{service.SSHService}
+ @ivar authenticatedWith: a list of strings of authentication methods we've tried
+ @type authenticatedWith: C{list} of C{str}
+ @ivar triedPublicKeys: a list of public key objects that we've tried to
+ authenticate with
+ @type triedPublicKeys: C{list} of L{Key}
+ @ivar lastPublicKey: the last public key object we've tried to authenticate
+ with
+ @type lastPublicKey: L{Key}
+ """
+
+
+ name = 'ssh-userauth'
+ preferredOrder = ['publickey', 'password', 'keyboard-interactive']
+
+
+ def __init__(self, user, instance):
+ self.user = user
+ self.instance = instance
+
+
+ def serviceStarted(self):
+ self.authenticatedWith = []
+ self.triedPublicKeys = []
+ self.lastPublicKey = None
+ self.askForAuth('none', '')
+
+
+ def askForAuth(self, kind, extraData):
+ """
+ Send a MSG_USERAUTH_REQUEST.
+
+ @param kind: the authentication method to try.
+ @type kind: C{str}
+ @param extraData: method-specific data to go in the packet
+ @type extraData: C{str}
+ """
+ self.lastAuth = kind
+ self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) +
+ NS(self.instance.name) + NS(kind) + extraData)
+
+
+ def tryAuth(self, kind):
+ """
+ Dispatch to an authentication method.
+
+ @param kind: the authentication method
+ @type kind: C{str}
+ """
+ kind = kind.replace('-', '_')
+ log.msg('trying to auth with %s' % (kind,))
+ f = getattr(self,'auth_%s' % (kind,), None)
+ if f:
+ return f()
+
+
+ def _ebAuth(self, ignored, *args):
+ """
+ Generic callback for a failed authentication attempt. Respond by
+ asking for the list of accepted methods (the 'none' method)
+ """
+ self.askForAuth('none', '')
+
+
+ def ssh_USERAUTH_SUCCESS(self, packet):
+ """
+ We received a MSG_USERAUTH_SUCCESS. The server has accepted our
+ authentication, so start the next service.
+ """
+ self.transport.setService(self.instance)
+
+
+ def ssh_USERAUTH_FAILURE(self, packet):
+ """
+ We received a MSG_USERAUTH_FAILURE. Payload::
+ string methods
+ byte partial success
+
+ If partial success is C{True}, then the previous method succeeded but is
+ not sufficent for authentication. C{methods} is a comma-separated list
+ of accepted authentication methods.
+
+ We sort the list of methods by their position in C{self.preferredOrder},
+ removing methods that have already succeeded. We then call
+ C{self.tryAuth} with the most preferred method.
+
+ @param packet: the L{MSG_USERAUTH_FAILURE} payload.
+ @type packet: C{str}
+
+ @return: a L{defer.Deferred} that will be callbacked with C{None} as
+ soon as all authentication methods have been tried, or C{None} if no
+ more authentication methods are available.
+ @rtype: C{defer.Deferred} or C{None}
+ """
+ canContinue, partial = getNS(packet)
+ partial = ord(partial)
+ if partial:
+ self.authenticatedWith.append(self.lastAuth)
+
+ def orderByPreference(meth):
+ """
+ Invoked once per authentication method in order to extract a
+ comparison key which is then used for sorting.
+
+ @param meth: the authentication method.
+ @type meth: C{str}
+
+ @return: the comparison key for C{meth}.
+ @rtype: C{int}
+ """
+ if meth in self.preferredOrder:
+ return self.preferredOrder.index(meth)
+ else:
+ # put the element at the end of the list.
+ return len(self.preferredOrder)
+
+ canContinue = sorted([meth for meth in canContinue.split(',')
+ if meth not in self.authenticatedWith],
+ key=orderByPreference)
+
+ log.msg('can continue with: %s' % canContinue)
+ return self._cbUserauthFailure(None, iter(canContinue))
+
+
+ def _cbUserauthFailure(self, result, iterator):
+ if result:
+ return
+ try:
+ method = iterator.next()
+ except StopIteration:
+ self.transport.sendDisconnect(
+ transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE,
+ 'no more authentication methods available')
+ else:
+ d = defer.maybeDeferred(self.tryAuth, method)
+ d.addCallback(self._cbUserauthFailure, iterator)
+ return d
+
+
+ def ssh_USERAUTH_PK_OK(self, packet):
+ """
+ This message (number 60) can mean several different messages depending
+ on the current authentication type. We dispatch to individual methods
+ in order to handle this request.
+ """
+ func = getattr(self, 'ssh_USERAUTH_PK_OK_%s' %
+ self.lastAuth.replace('-', '_'), None)
+ if func is not None:
+ return func(packet)
+ else:
+ self.askForAuth('none', '')
+
+
+ def ssh_USERAUTH_PK_OK_publickey(self, packet):
+ """
+ This is MSG_USERAUTH_PK. Our public key is valid, so we create a
+ signature and try to authenticate with it.
+ """
+ publicKey = self.lastPublicKey
+ b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) +
+ NS(self.user) + NS(self.instance.name) + NS('publickey') +
+ '\x01' + NS(publicKey.sshType()) + NS(publicKey.blob()))
+ d = self.signData(publicKey, b)
+ if not d:
+ self.askForAuth('none', '')
+ # this will fail, we'll move on
+ return
+ d.addCallback(self._cbSignedData)
+ d.addErrback(self._ebAuth)
+
+
+ def ssh_USERAUTH_PK_OK_password(self, packet):
+ """
+ This is MSG_USERAUTH_PASSWD_CHANGEREQ. The password given has expired.
+ We ask for an old password and a new password, then send both back to
+ the server.
+ """
+ prompt, language, rest = getNS(packet, 2)
+ self._oldPass = self._newPass = None
+ d = self.getPassword('Old Password: ')
+ d = d.addCallbacks(self._setOldPass, self._ebAuth)
+ d.addCallback(lambda ignored: self.getPassword(prompt))
+ d.addCallbacks(self._setNewPass, self._ebAuth)
+
+
+ def ssh_USERAUTH_PK_OK_keyboard_interactive(self, packet):
+ """
+ This is MSG_USERAUTH_INFO_RESPONSE. The server has sent us the
+ questions it wants us to answer, so we ask the user and sent the
+ responses.
+ """
+ name, instruction, lang, data = getNS(packet, 3)
+ numPrompts = struct.unpack('!L', data[:4])[0]
+ data = data[4:]
+ prompts = []
+ for i in range(numPrompts):
+ prompt, data = getNS(data)
+ echo = bool(ord(data[0]))
+ data = data[1:]
+ prompts.append((prompt, echo))
+ d = self.getGenericAnswers(name, instruction, prompts)
+ d.addCallback(self._cbGenericAnswers)
+ d.addErrback(self._ebAuth)
+
+
+ def _cbSignedData(self, signedData):
+ """
+ Called back out of self.signData with the signed data. Send the
+ authentication request with the signature.
+
+ @param signedData: the data signed by the user's private key.
+ @type signedData: C{str}
+ """
+ publicKey = self.lastPublicKey
+ self.askForAuth('publickey', '\x01' + NS(publicKey.sshType()) +
+ NS(publicKey.blob()) + NS(signedData))
+
+
+ def _setOldPass(self, op):
+ """
+ Called back when we are choosing a new password. Simply store the old
+ password for now.
+
+ @param op: the old password as entered by the user
+ @type op: C{str}
+ """
+ self._oldPass = op
+
+
+ def _setNewPass(self, np):
+ """
+ Called back when we are choosing a new password. Get the old password
+ and send the authentication message with both.
+
+ @param np: the new password as entered by the user
+ @type np: C{str}
+ """
+ op = self._oldPass
+ self._oldPass = None
+ self.askForAuth('password', '\xff' + NS(op) + NS(np))
+
+
+ def _cbGenericAnswers(self, responses):
+ """
+ Called back when we are finished answering keyboard-interactive
+ questions. Send the info back to the server in a
+ MSG_USERAUTH_INFO_RESPONSE.
+
+ @param responses: a list of C{str} responses
+ @type responses: C{list}
+ """
+ data = struct.pack('!L', len(responses))
+ for r in responses:
+ data += NS(r.encode('UTF8'))
+ self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data)
+
+
+ def auth_publickey(self):
+ """
+ Try to authenticate with a public key. Ask the user for a public key;
+ if the user has one, send the request to the server and return True.
+ Otherwise, return False.
+
+ @rtype: C{bool}
+ """
+ d = defer.maybeDeferred(self.getPublicKey)
+ d.addBoth(self._cbGetPublicKey)
+ return d
+
+
+ def _cbGetPublicKey(self, publicKey):
+ if isinstance(publicKey, str):
+ warnings.warn("Returning a string from "
+ "SSHUserAuthClient.getPublicKey() is deprecated "
+ "since Twisted 9.0. Return a keys.Key() instead.",
+ DeprecationWarning)
+ publicKey = keys.Key.fromString(publicKey)
+ if not isinstance(publicKey, keys.Key): # failure or None
+ publicKey = None
+ if publicKey is not None:
+ self.lastPublicKey = publicKey
+ self.triedPublicKeys.append(publicKey)
+ log.msg('using key of type %s' % publicKey.type())
+ self.askForAuth('publickey', '\x00' + NS(publicKey.sshType()) +
+ NS(publicKey.blob()))
+ return True
+ else:
+ return False
+
+
+ def auth_password(self):
+ """
+ Try to authenticate with a password. Ask the user for a password.
+ If the user will return a password, return True. Otherwise, return
+ False.
+
+ @rtype: C{bool}
+ """
+ d = self.getPassword()
+ if d:
+ d.addCallbacks(self._cbPassword, self._ebAuth)
+ return True
+ else: # returned None, don't do password auth
+ return False
+
+
+ def auth_keyboard_interactive(self):
+ """
+ Try to authenticate with keyboard-interactive authentication. Send
+ the request to the server and return True.
+
+ @rtype: C{bool}
+ """
+ log.msg('authing with keyboard-interactive')
+ self.askForAuth('keyboard-interactive', NS('') + NS(''))
+ return True
+
+
+ def _cbPassword(self, password):
+ """
+ Called back when the user gives a password. Send the request to the
+ server.
+
+ @param password: the password the user entered
+ @type password: C{str}
+ """
+ self.askForAuth('password', '\x00' + NS(password))
+
+
+ def signData(self, publicKey, signData):
+ """
+ Sign the given data with the given public key.
+
+ By default, this will call getPrivateKey to get the private key,
+ then sign the data using Key.sign().
+
+ This method is factored out so that it can be overridden to use
+ alternate methods, such as a key agent.
+
+ @param publicKey: The public key object returned from L{getPublicKey}
+ @type publicKey: L{keys.Key}
+
+ @param signData: the data to be signed by the private key.
+ @type signData: C{str}
+ @return: a Deferred that's called back with the signature
+ @rtype: L{defer.Deferred}
+ """
+ key = self.getPrivateKey()
+ if not key:
+ return
+ return key.addCallback(self._cbSignData, signData)
+
+
+ def _cbSignData(self, privateKey, signData):
+ """
+ Called back when the private key is returned. Sign the data and
+ return the signature.
+
+ @param privateKey: the private key object
+ @type publicKey: L{keys.Key}
+ @param signData: the data to be signed by the private key.
+ @type signData: C{str}
+ @return: the signature
+ @rtype: C{str}
+ """
+ if not isinstance(privateKey, keys.Key):
+ warnings.warn("Returning a PyCrypto key object from "
+ "SSHUserAuthClient.getPrivateKey() is deprecated "
+ "since Twisted 9.0. Return a keys.Key() instead.",
+ DeprecationWarning)
+ privateKey = keys.Key(privateKey)
+ return privateKey.sign(signData)
+
+
+ def getPublicKey(self):
+ """
+ Return a public key for the user. If no more public keys are
+ available, return C{None}.
+
+ This implementation always returns C{None}. Override it in a
+ subclass to actually find and return a public key object.
+
+ @rtype: L{Key} or L{NoneType}
+ """
+ return None
+
+
+ def getPrivateKey(self):
+ """
+ Return a L{Deferred} that will be called back with the private key
+ object corresponding to the last public key from getPublicKey().
+ If the private key is not available, errback on the Deferred.
+
+ @rtype: L{Deferred} called back with L{Key}
+ """
+ return defer.fail(NotImplementedError())
+
+
+ def getPassword(self, prompt = None):
+ """
+ Return a L{Deferred} that will be called back with a password.
+ prompt is a string to display for the password, or None for a generic
+ 'user@hostname's password: '.
+
+ @type prompt: C{str}/C{None}
+ @rtype: L{defer.Deferred}
+ """
+ return defer.fail(NotImplementedError())
+
+
+ def getGenericAnswers(self, name, instruction, prompts):
+ """
+ Returns a L{Deferred} with the responses to the promopts.
+
+ @param name: The name of the authentication currently in progress.
+ @param instruction: Describes what the authentication wants.
+ @param prompts: A list of (prompt, echo) pairs, where prompt is a
+ string to display and echo is a boolean indicating whether the
+ user's response should be echoed as they type it.
+ """
+ return defer.fail(NotImplementedError())
+
+
+MSG_USERAUTH_REQUEST = 50
+MSG_USERAUTH_FAILURE = 51
+MSG_USERAUTH_SUCCESS = 52
+MSG_USERAUTH_BANNER = 53
+MSG_USERAUTH_PASSWD_CHANGEREQ = 60
+MSG_USERAUTH_INFO_REQUEST = 60
+MSG_USERAUTH_INFO_RESPONSE = 61
+MSG_USERAUTH_PK_OK = 60
+
+messages = {}
+for k, v in locals().items():
+ if k[:4]=='MSG_':
+ messages[v] = k # doesn't handle doubles
+
+SSHUserAuthServer.protocolMessages = messages
+SSHUserAuthClient.protocolMessages = messages
+del messages
+del v
diff --git a/twisted/conch/stdio.py b/twisted/conch/stdio.py
new file mode 100644
index 0000000..c45fc3b
--- /dev/null
+++ b/twisted/conch/stdio.py
@@ -0,0 +1,95 @@
+# -*- test-case-name: twisted.conch.test.test_manhole -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Asynchronous local terminal input handling
+
+@author: Jp Calderone
+"""
+
+import os, tty, sys, termios
+
+from twisted.internet import reactor, stdio, protocol, defer
+from twisted.python import failure, reflect, log
+
+from twisted.conch.insults.insults import ServerProtocol
+from twisted.conch.manhole import ColoredManhole
+
+class UnexpectedOutputError(Exception):
+ pass
+
+class TerminalProcessProtocol(protocol.ProcessProtocol):
+ def __init__(self, proto):
+ self.proto = proto
+ self.onConnection = defer.Deferred()
+
+ def connectionMade(self):
+ self.proto.makeConnection(self)
+ self.onConnection.callback(None)
+ self.onConnection = None
+
+ def write(self, bytes):
+ self.transport.write(bytes)
+
+ def outReceived(self, bytes):
+ self.proto.dataReceived(bytes)
+
+ def errReceived(self, bytes):
+ self.transport.loseConnection()
+ if self.proto is not None:
+ self.proto.connectionLost(failure.Failure(UnexpectedOutputError(bytes)))
+ self.proto = None
+
+ def childConnectionLost(self, childFD):
+ if self.proto is not None:
+ self.proto.childConnectionLost(childFD)
+
+ def processEnded(self, reason):
+ if self.proto is not None:
+ self.proto.connectionLost(reason)
+ self.proto = None
+
+
+
+class ConsoleManhole(ColoredManhole):
+ """
+ A manhole protocol specifically for use with L{stdio.StandardIO}.
+ """
+ def connectionLost(self, reason):
+ """
+ When the connection is lost, there is nothing more to do. Stop the
+ reactor so that the process can exit.
+ """
+ reactor.stop()
+
+
+
+def runWithProtocol(klass):
+ fd = sys.__stdin__.fileno()
+ oldSettings = termios.tcgetattr(fd)
+ tty.setraw(fd)
+ try:
+ p = ServerProtocol(klass)
+ stdio.StandardIO(p)
+ reactor.run()
+ finally:
+ termios.tcsetattr(fd, termios.TCSANOW, oldSettings)
+ os.write(fd, "\r\x1bc\r")
+
+
+
+def main(argv=None):
+ log.startLogging(file('child.log', 'w'))
+
+ if argv is None:
+ argv = sys.argv[1:]
+ if argv:
+ klass = reflect.namedClass(argv[0])
+ else:
+ klass = ConsoleManhole
+ runWithProtocol(klass)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/twisted/conch/tap.py b/twisted/conch/tap.py
new file mode 100644
index 0000000..7488cc0
--- /dev/null
+++ b/twisted/conch/tap.py
@@ -0,0 +1,87 @@
+# -*- test-case-name: twisted.conch.test.test_tap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support module for making SSH servers with twistd.
+"""
+
+from twisted.conch import unix
+from twisted.conch import checkers as conch_checkers
+from twisted.conch.openssh_compat import factory
+from twisted.cred import portal, checkers, strcred
+from twisted.python import usage
+from twisted.application import strports
+try:
+ from twisted.cred import pamauth
+except ImportError:
+ pamauth = None
+
+
+
+class Options(usage.Options, strcred.AuthOptionMixin):
+ synopsis = "[-i <interface>] [-p <port>] [-d <dir>] "
+ longdesc = ("Makes a Conch SSH server. If no authentication methods are "
+ "specified, the default authentication methods are UNIX passwords, "
+ "SSH public keys, and PAM if it is available. If --auth options are "
+ "passed, only the measures specified will be used.")
+ optParameters = [
+ ["interface", "i", "", "local interface to which we listen"],
+ ["port", "p", "tcp:22", "Port on which to listen"],
+ ["data", "d", "/etc", "directory to look for host keys in"],
+ ["moduli", "", None, "directory to look for moduli in "
+ "(if different from --data)"]
+ ]
+ compData = usage.Completions(
+ optActions={"data": usage.CompleteDirs(descr="data directory"),
+ "moduli": usage.CompleteDirs(descr="moduli directory"),
+ "interface": usage.CompleteNetInterfaces()}
+ )
+
+
+ def __init__(self, *a, **kw):
+ usage.Options.__init__(self, *a, **kw)
+
+ # call the default addCheckers (for backwards compatibility) that will
+ # be used if no --auth option is provided - note that conch's
+ # UNIXPasswordDatabase is used, instead of twisted.plugins.cred_unix's
+ # checker
+ super(Options, self).addChecker(conch_checkers.UNIXPasswordDatabase())
+ super(Options, self).addChecker(conch_checkers.SSHPublicKeyDatabase())
+ if pamauth is not None:
+ super(Options, self).addChecker(
+ checkers.PluggableAuthenticationModulesChecker())
+
+
+ def addChecker(self, checker):
+ """
+ If addChecker is called, clear out the default checkers first
+ """
+ self['credCheckers'] = []
+ self['credInterfaces'] = {}
+ super(Options, self).addChecker(checker)
+
+
+
+def makeService(config):
+ """
+ Construct a service for operating a SSH server.
+
+ @param config: An L{Options} instance specifying server options, including
+ where server keys are stored and what authentication methods to use.
+
+ @return: An L{IService} provider which contains the requested SSH server.
+ """
+
+ t = factory.OpenSSHFactory()
+
+ r = unix.UnixSSHRealm()
+ t.portal = portal.Portal(r, config.get('credCheckers', []))
+ t.dataRoot = config['data']
+ t.moduliRoot = config['moduli'] or config['data']
+
+ port = config['port']
+ if config['interface']:
+ # Add warning here
+ port += ':interface=' + config['interface']
+ return strports.service(port, t)
diff --git a/twisted/conch/telnet.py b/twisted/conch/telnet.py
new file mode 100644
index 0000000..c90fe1a
--- /dev/null
+++ b/twisted/conch/telnet.py
@@ -0,0 +1,1086 @@
+# -*- test-case-name: twisted.conch.test.test_telnet -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Telnet protocol implementation.
+
+@author: Jean-Paul Calderone
+"""
+
+import struct
+
+from zope.interface import implements
+
+from twisted.internet import protocol, interfaces as iinternet, defer
+from twisted.python import log
+
+MODE = chr(1)
+EDIT = 1
+TRAPSIG = 2
+MODE_ACK = 4
+SOFT_TAB = 8
+LIT_ECHO = 16
+
+# Characters gleaned from the various (and conflicting) RFCs. Not all of these are correct.
+
+NULL = chr(0) # No operation.
+BEL = chr(7) # Produces an audible or
+ # visible signal (which does
+ # NOT move the print head).
+BS = chr(8) # Moves the print head one
+ # character position towards
+ # the left margin.
+HT = chr(9) # Moves the printer to the
+ # next horizontal tab stop.
+ # It remains unspecified how
+ # either party determines or
+ # establishes where such tab
+ # stops are located.
+LF = chr(10) # Moves the printer to the
+ # next print line, keeping the
+ # same horizontal position.
+VT = chr(11) # Moves the printer to the
+ # next vertical tab stop. It
+ # remains unspecified how
+ # either party determines or
+ # establishes where such tab
+ # stops are located.
+FF = chr(12) # Moves the printer to the top
+ # of the next page, keeping
+ # the same horizontal position.
+CR = chr(13) # Moves the printer to the left
+ # margin of the current line.
+
+ECHO = chr(1) # User-to-Server: Asks the server to send
+ # Echos of the transmitted data.
+SGA = chr(3) # Suppress Go Ahead. Go Ahead is silly
+ # and most modern servers should suppress
+ # it.
+NAWS = chr(31) # Negotiate About Window Size. Indicate that
+ # information about the size of the terminal
+ # can be communicated.
+LINEMODE = chr(34) # Allow line buffering to be
+ # negotiated about.
+
+SE = chr(240) # End of subnegotiation parameters.
+NOP = chr(241) # No operation.
+DM = chr(242) # "Data Mark": The data stream portion
+ # of a Synch. This should always be
+ # accompanied by a TCP Urgent
+ # notification.
+BRK = chr(243) # NVT character Break.
+IP = chr(244) # The function Interrupt Process.
+AO = chr(245) # The function Abort Output
+AYT = chr(246) # The function Are You There.
+EC = chr(247) # The function Erase Character.
+EL = chr(248) # The function Erase Line
+GA = chr(249) # The Go Ahead signal.
+SB = chr(250) # Indicates that what follows is
+ # subnegotiation of the indicated
+ # option.
+WILL = chr(251) # Indicates the desire to begin
+ # performing, or confirmation that
+ # you are now performing, the
+ # indicated option.
+WONT = chr(252) # Indicates the refusal to perform,
+ # or continue performing, the
+ # indicated option.
+DO = chr(253) # Indicates the request that the
+ # other party perform, or
+ # confirmation that you are expecting
+ # the other party to perform, the
+ # indicated option.
+DONT = chr(254) # Indicates the demand that the
+ # other party stop performing,
+ # or confirmation that you are no
+ # longer expecting the other party
+ # to perform, the indicated option.
+IAC = chr(255) # Data Byte 255. Introduces a
+ # telnet command.
+
+LINEMODE_MODE = chr(1)
+LINEMODE_EDIT = chr(1)
+LINEMODE_TRAPSIG = chr(2)
+LINEMODE_MODE_ACK = chr(4)
+LINEMODE_SOFT_TAB = chr(8)
+LINEMODE_LIT_ECHO = chr(16)
+LINEMODE_FORWARDMASK = chr(2)
+LINEMODE_SLC = chr(3)
+LINEMODE_SLC_SYNCH = chr(1)
+LINEMODE_SLC_BRK = chr(2)
+LINEMODE_SLC_IP = chr(3)
+LINEMODE_SLC_AO = chr(4)
+LINEMODE_SLC_AYT = chr(5)
+LINEMODE_SLC_EOR = chr(6)
+LINEMODE_SLC_ABORT = chr(7)
+LINEMODE_SLC_EOF = chr(8)
+LINEMODE_SLC_SUSP = chr(9)
+LINEMODE_SLC_EC = chr(10)
+LINEMODE_SLC_EL = chr(11)
+
+LINEMODE_SLC_EW = chr(12)
+LINEMODE_SLC_RP = chr(13)
+LINEMODE_SLC_LNEXT = chr(14)
+LINEMODE_SLC_XON = chr(15)
+LINEMODE_SLC_XOFF = chr(16)
+LINEMODE_SLC_FORW1 = chr(17)
+LINEMODE_SLC_FORW2 = chr(18)
+LINEMODE_SLC_MCL = chr(19)
+LINEMODE_SLC_MCR = chr(20)
+LINEMODE_SLC_MCWL = chr(21)
+LINEMODE_SLC_MCWR = chr(22)
+LINEMODE_SLC_MCBOL = chr(23)
+LINEMODE_SLC_MCEOL = chr(24)
+LINEMODE_SLC_INSRT = chr(25)
+LINEMODE_SLC_OVER = chr(26)
+LINEMODE_SLC_ECR = chr(27)
+LINEMODE_SLC_EWR = chr(28)
+LINEMODE_SLC_EBOL = chr(29)
+LINEMODE_SLC_EEOL = chr(30)
+
+LINEMODE_SLC_DEFAULT = chr(3)
+LINEMODE_SLC_VALUE = chr(2)
+LINEMODE_SLC_CANTCHANGE = chr(1)
+LINEMODE_SLC_NOSUPPORT = chr(0)
+LINEMODE_SLC_LEVELBITS = chr(3)
+
+LINEMODE_SLC_ACK = chr(128)
+LINEMODE_SLC_FLUSHIN = chr(64)
+LINEMODE_SLC_FLUSHOUT = chr(32)
+LINEMODE_EOF = chr(236)
+LINEMODE_SUSP = chr(237)
+LINEMODE_ABORT = chr(238)
+
+class ITelnetProtocol(iinternet.IProtocol):
+ def unhandledCommand(command, argument):
+ """A command was received but not understood.
+
+ @param command: the command received.
+ @type command: C{str}, a single character.
+ @param argument: the argument to the received command.
+ @type argument: C{str}, a single character, or None if the command that
+ was unhandled does not provide an argument.
+ """
+
+ def unhandledSubnegotiation(command, bytes):
+ """A subnegotiation command was received but not understood.
+
+ @param command: the command being subnegotiated. That is, the first
+ byte after the SB command.
+ @type command: C{str}, a single character.
+ @param bytes: all other bytes of the subneogation. That is, all but the
+ first bytes between SB and SE, with IAC un-escaping applied.
+ @type bytes: C{list} of C{str}, each a single character
+ """
+
+ def enableLocal(option):
+ """Enable the given option locally.
+
+ This should enable the given option on this side of the
+ telnet connection and return True. If False is returned,
+ the option will be treated as still disabled and the peer
+ will be notified.
+
+ @param option: the option to be enabled.
+ @type option: C{str}, a single character.
+ """
+
+ def enableRemote(option):
+ """Indicate whether the peer should be allowed to enable this option.
+
+ Returns True if the peer should be allowed to enable this option,
+ False otherwise.
+
+ @param option: the option to be enabled.
+ @type option: C{str}, a single character.
+ """
+
+ def disableLocal(option):
+ """Disable the given option locally.
+
+ Unlike enableLocal, this method cannot fail. The option must be
+ disabled.
+
+ @param option: the option to be disabled.
+ @type option: C{str}, a single character.
+ """
+
+ def disableRemote(option):
+ """Indicate that the peer has disabled this option.
+
+ @param option: the option to be disabled.
+ @type option: C{str}, a single character.
+ """
+
+
+
+class ITelnetTransport(iinternet.ITransport):
+ def do(option):
+ """
+ Indicate a desire for the peer to begin performing the given option.
+
+ Returns a Deferred that fires with True when the peer begins performing
+ the option, or fails with L{OptionRefused} when the peer refuses to
+ perform it. If the peer is already performing the given option, the
+ Deferred will fail with L{AlreadyEnabled}. If a negotiation regarding
+ this option is already in progress, the Deferred will fail with
+ L{AlreadyNegotiating}.
+
+ Note: It is currently possible that this Deferred will never fire,
+ if the peer never responds, or if the peer believes the option to
+ already be enabled.
+ """
+
+
+ def dont(option):
+ """
+ Indicate a desire for the peer to cease performing the given option.
+
+ Returns a Deferred that fires with True when the peer ceases performing
+ the option. If the peer is not performing the given option, the
+ Deferred will fail with L{AlreadyDisabled}. If negotiation regarding
+ this option is already in progress, the Deferred will fail with
+ L{AlreadyNegotiating}.
+
+ Note: It is currently possible that this Deferred will never fire,
+ if the peer never responds, or if the peer believes the option to
+ already be disabled.
+ """
+
+
+ def will(option):
+ """
+ Indicate our willingness to begin performing this option locally.
+
+ Returns a Deferred that fires with True when the peer agrees to allow us
+ to begin performing this option, or fails with L{OptionRefused} if the
+ peer refuses to allow us to begin performing it. If the option is
+ already enabled locally, the Deferred will fail with L{AlreadyEnabled}.
+ If negotiation regarding this option is already in progress, the
+ Deferred will fail with L{AlreadyNegotiating}.
+
+ Note: It is currently possible that this Deferred will never fire,
+ if the peer never responds, or if the peer believes the option to
+ already be enabled.
+ """
+
+
+ def wont(option):
+ """
+ Indicate that we will stop performing the given option.
+
+ Returns a Deferred that fires with True when the peer acknowledges
+ we have stopped performing this option. If the option is already
+ disabled locally, the Deferred will fail with L{AlreadyDisabled}.
+ If negotiation regarding this option is already in progress,
+ the Deferred will fail with L{AlreadyNegotiating}.
+
+ Note: It is currently possible that this Deferred will never fire,
+ if the peer never responds, or if the peer believes the option to
+ already be disabled.
+ """
+
+
+ def requestNegotiation(about, bytes):
+ """
+ Send a subnegotiation request.
+
+ @param about: A byte indicating the feature being negotiated.
+ @param bytes: Any number of bytes containing specific information
+ about the negotiation being requested. No values in this string
+ need to be escaped, as this function will escape any value which
+ requires it.
+ """
+
+
+
+class TelnetError(Exception):
+ pass
+
+class NegotiationError(TelnetError):
+ def __str__(self):
+ return self.__class__.__module__ + '.' + self.__class__.__name__ + ':' + repr(self.args[0])
+
+class OptionRefused(NegotiationError):
+ pass
+
+class AlreadyEnabled(NegotiationError):
+ pass
+
+class AlreadyDisabled(NegotiationError):
+ pass
+
+class AlreadyNegotiating(NegotiationError):
+ pass
+
+class TelnetProtocol(protocol.Protocol):
+ implements(ITelnetProtocol)
+
+ def unhandledCommand(self, command, argument):
+ pass
+
+ def unhandledSubnegotiation(self, command, bytes):
+ pass
+
+ def enableLocal(self, option):
+ pass
+
+ def enableRemote(self, option):
+ pass
+
+ def disableLocal(self, option):
+ pass
+
+ def disableRemote(self, option):
+ pass
+
+
+class Telnet(protocol.Protocol):
+ """
+ @ivar commandMap: A mapping of bytes to callables. When a
+ telnet command is received, the command byte (the first byte
+ after IAC) is looked up in this dictionary. If a callable is
+ found, it is invoked with the argument of the command, or None
+ if the command takes no argument. Values should be added to
+ this dictionary if commands wish to be handled. By default,
+ only WILL, WONT, DO, and DONT are handled. These should not
+ be overridden, as this class handles them correctly and
+ provides an API for interacting with them.
+
+ @ivar negotiationMap: A mapping of bytes to callables. When
+ a subnegotiation command is received, the command byte (the
+ first byte after SB) is looked up in this dictionary. If
+ a callable is found, it is invoked with the argument of the
+ subnegotiation. Values should be added to this dictionary if
+ subnegotiations are to be handled. By default, no values are
+ handled.
+
+ @ivar options: A mapping of option bytes to their current
+ state. This state is likely of little use to user code.
+ Changes should not be made to it.
+
+ @ivar state: A string indicating the current parse state. It
+ can take on the values "data", "escaped", "command", "newline",
+ "subnegotiation", and "subnegotiation-escaped". Changes
+ should not be made to it.
+
+ @ivar transport: This protocol's transport object.
+ """
+
+ # One of a lot of things
+ state = 'data'
+
+ def __init__(self):
+ self.options = {}
+ self.negotiationMap = {}
+ self.commandMap = {
+ WILL: self.telnet_WILL,
+ WONT: self.telnet_WONT,
+ DO: self.telnet_DO,
+ DONT: self.telnet_DONT}
+
+ def _write(self, bytes):
+ self.transport.write(bytes)
+
+ class _OptionState:
+ """
+ Represents the state of an option on both sides of a telnet
+ connection.
+
+ @ivar us: The state of the option on this side of the connection.
+
+ @ivar him: The state of the option on the other side of the
+ connection.
+ """
+ class _Perspective:
+ """
+ Represents the state of an option on side of the telnet
+ connection. Some options can be enabled on a particular side of
+ the connection (RFC 1073 for example: only the client can have
+ NAWS enabled). Other options can be enabled on either or both
+ sides (such as RFC 1372: each side can have its own flow control
+ state).
+
+ @ivar state: C{'yes'} or C{'no'} indicating whether or not this
+ option is enabled on one side of the connection.
+
+ @ivar negotiating: A boolean tracking whether negotiation about
+ this option is in progress.
+
+ @ivar onResult: When negotiation about this option has been
+ initiated by this side of the connection, a L{Deferred}
+ which will fire with the result of the negotiation. C{None}
+ at other times.
+ """
+ state = 'no'
+ negotiating = False
+ onResult = None
+
+ def __str__(self):
+ return self.state + ('*' * self.negotiating)
+
+ def __init__(self):
+ self.us = self._Perspective()
+ self.him = self._Perspective()
+
+ def __repr__(self):
+ return '<_OptionState us=%s him=%s>' % (self.us, self.him)
+
+ def getOptionState(self, opt):
+ return self.options.setdefault(opt, self._OptionState())
+
+ def _do(self, option):
+ self._write(IAC + DO + option)
+
+ def _dont(self, option):
+ self._write(IAC + DONT + option)
+
+ def _will(self, option):
+ self._write(IAC + WILL + option)
+
+ def _wont(self, option):
+ self._write(IAC + WONT + option)
+
+ def will(self, option):
+ """Indicate our willingness to enable an option.
+ """
+ s = self.getOptionState(option)
+ if s.us.negotiating or s.him.negotiating:
+ return defer.fail(AlreadyNegotiating(option))
+ elif s.us.state == 'yes':
+ return defer.fail(AlreadyEnabled(option))
+ else:
+ s.us.negotiating = True
+ s.us.onResult = d = defer.Deferred()
+ self._will(option)
+ return d
+
+ def wont(self, option):
+ """Indicate we are not willing to enable an option.
+ """
+ s = self.getOptionState(option)
+ if s.us.negotiating or s.him.negotiating:
+ return defer.fail(AlreadyNegotiating(option))
+ elif s.us.state == 'no':
+ return defer.fail(AlreadyDisabled(option))
+ else:
+ s.us.negotiating = True
+ s.us.onResult = d = defer.Deferred()
+ self._wont(option)
+ return d
+
+ def do(self, option):
+ s = self.getOptionState(option)
+ if s.us.negotiating or s.him.negotiating:
+ return defer.fail(AlreadyNegotiating(option))
+ elif s.him.state == 'yes':
+ return defer.fail(AlreadyEnabled(option))
+ else:
+ s.him.negotiating = True
+ s.him.onResult = d = defer.Deferred()
+ self._do(option)
+ return d
+
+ def dont(self, option):
+ s = self.getOptionState(option)
+ if s.us.negotiating or s.him.negotiating:
+ return defer.fail(AlreadyNegotiating(option))
+ elif s.him.state == 'no':
+ return defer.fail(AlreadyDisabled(option))
+ else:
+ s.him.negotiating = True
+ s.him.onResult = d = defer.Deferred()
+ self._dont(option)
+ return d
+
+
+ def requestNegotiation(self, about, bytes):
+ """
+ Send a negotiation message for the option C{about} with C{bytes} as the
+ payload.
+
+ @see: L{ITelnetTransport.requestNegotiation}
+ """
+ bytes = bytes.replace(IAC, IAC * 2)
+ self._write(IAC + SB + about + bytes + IAC + SE)
+
+
+ def dataReceived(self, data):
+ appDataBuffer = []
+
+ for b in data:
+ if self.state == 'data':
+ if b == IAC:
+ self.state = 'escaped'
+ elif b == '\r':
+ self.state = 'newline'
+ else:
+ appDataBuffer.append(b)
+ elif self.state == 'escaped':
+ if b == IAC:
+ appDataBuffer.append(b)
+ self.state = 'data'
+ elif b == SB:
+ self.state = 'subnegotiation'
+ self.commands = []
+ elif b in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA):
+ self.state = 'data'
+ if appDataBuffer:
+ self.applicationDataReceived(''.join(appDataBuffer))
+ del appDataBuffer[:]
+ self.commandReceived(b, None)
+ elif b in (WILL, WONT, DO, DONT):
+ self.state = 'command'
+ self.command = b
+ else:
+ raise ValueError("Stumped", b)
+ elif self.state == 'command':
+ self.state = 'data'
+ command = self.command
+ del self.command
+ if appDataBuffer:
+ self.applicationDataReceived(''.join(appDataBuffer))
+ del appDataBuffer[:]
+ self.commandReceived(command, b)
+ elif self.state == 'newline':
+ self.state = 'data'
+ if b == '\n':
+ appDataBuffer.append('\n')
+ elif b == '\0':
+ appDataBuffer.append('\r')
+ elif b == IAC:
+ # IAC isn't really allowed after \r, according to the
+ # RFC, but handling it this way is less surprising than
+ # delivering the IAC to the app as application data.
+ # The purpose of the restriction is to allow terminals
+ # to unambiguously interpret the behavior of the CR
+ # after reading only one more byte. CR LF is supposed
+ # to mean one thing (cursor to next line, first column),
+ # CR NUL another (cursor to first column). Absent the
+ # NUL, it still makes sense to interpret this as CR and
+ # then apply all the usual interpretation to the IAC.
+ appDataBuffer.append('\r')
+ self.state = 'escaped'
+ else:
+ appDataBuffer.append('\r' + b)
+ elif self.state == 'subnegotiation':
+ if b == IAC:
+ self.state = 'subnegotiation-escaped'
+ else:
+ self.commands.append(b)
+ elif self.state == 'subnegotiation-escaped':
+ if b == SE:
+ self.state = 'data'
+ commands = self.commands
+ del self.commands
+ if appDataBuffer:
+ self.applicationDataReceived(''.join(appDataBuffer))
+ del appDataBuffer[:]
+ self.negotiate(commands)
+ else:
+ self.state = 'subnegotiation'
+ self.commands.append(b)
+ else:
+ raise ValueError("How'd you do this?")
+
+ if appDataBuffer:
+ self.applicationDataReceived(''.join(appDataBuffer))
+
+
+ def connectionLost(self, reason):
+ for state in self.options.values():
+ if state.us.onResult is not None:
+ d = state.us.onResult
+ state.us.onResult = None
+ d.errback(reason)
+ if state.him.onResult is not None:
+ d = state.him.onResult
+ state.him.onResult = None
+ d.errback(reason)
+
+ def applicationDataReceived(self, bytes):
+ """Called with application-level data.
+ """
+
+ def unhandledCommand(self, command, argument):
+ """Called for commands for which no handler is installed.
+ """
+
+ def commandReceived(self, command, argument):
+ cmdFunc = self.commandMap.get(command)
+ if cmdFunc is None:
+ self.unhandledCommand(command, argument)
+ else:
+ cmdFunc(argument)
+
+ def unhandledSubnegotiation(self, command, bytes):
+ """Called for subnegotiations for which no handler is installed.
+ """
+
+ def negotiate(self, bytes):
+ command, bytes = bytes[0], bytes[1:]
+ cmdFunc = self.negotiationMap.get(command)
+ if cmdFunc is None:
+ self.unhandledSubnegotiation(command, bytes)
+ else:
+ cmdFunc(bytes)
+
+ def telnet_WILL(self, option):
+ s = self.getOptionState(option)
+ self.willMap[s.him.state, s.him.negotiating](self, s, option)
+
+ def will_no_false(self, state, option):
+ # He is unilaterally offering to enable an option.
+ if self.enableRemote(option):
+ state.him.state = 'yes'
+ self._do(option)
+ else:
+ self._dont(option)
+
+ def will_no_true(self, state, option):
+ # Peer agreed to enable an option in response to our request.
+ state.him.state = 'yes'
+ state.him.negotiating = False
+ d = state.him.onResult
+ state.him.onResult = None
+ d.callback(True)
+ assert self.enableRemote(option), "enableRemote must return True in this context (for option %r)" % (option,)
+
+ def will_yes_false(self, state, option):
+ # He is unilaterally offering to enable an already-enabled option.
+ # Ignore this.
+ pass
+
+ def will_yes_true(self, state, option):
+ # This is a bogus state. It is here for completeness. It will
+ # never be entered.
+ assert False, "will_yes_true can never be entered, but was called with %r, %r" % (state, option)
+
+ willMap = {('no', False): will_no_false, ('no', True): will_no_true,
+ ('yes', False): will_yes_false, ('yes', True): will_yes_true}
+
+ def telnet_WONT(self, option):
+ s = self.getOptionState(option)
+ self.wontMap[s.him.state, s.him.negotiating](self, s, option)
+
+ def wont_no_false(self, state, option):
+ # He is unilaterally demanding that an already-disabled option be/remain disabled.
+ # Ignore this (although we could record it and refuse subsequent enable attempts
+ # from our side - he can always refuse them again though, so we won't)
+ pass
+
+ def wont_no_true(self, state, option):
+ # Peer refused to enable an option in response to our request.
+ state.him.negotiating = False
+ d = state.him.onResult
+ state.him.onResult = None
+ d.errback(OptionRefused(option))
+
+ def wont_yes_false(self, state, option):
+ # Peer is unilaterally demanding that an option be disabled.
+ state.him.state = 'no'
+ self.disableRemote(option)
+ self._dont(option)
+
+ def wont_yes_true(self, state, option):
+ # Peer agreed to disable an option at our request.
+ state.him.state = 'no'
+ state.him.negotiating = False
+ d = state.him.onResult
+ state.him.onResult = None
+ d.callback(True)
+ self.disableRemote(option)
+
+ wontMap = {('no', False): wont_no_false, ('no', True): wont_no_true,
+ ('yes', False): wont_yes_false, ('yes', True): wont_yes_true}
+
+ def telnet_DO(self, option):
+ s = self.getOptionState(option)
+ self.doMap[s.us.state, s.us.negotiating](self, s, option)
+
+ def do_no_false(self, state, option):
+ # Peer is unilaterally requesting that we enable an option.
+ if self.enableLocal(option):
+ state.us.state = 'yes'
+ self._will(option)
+ else:
+ self._wont(option)
+
+ def do_no_true(self, state, option):
+ # Peer agreed to allow us to enable an option at our request.
+ state.us.state = 'yes'
+ state.us.negotiating = False
+ d = state.us.onResult
+ state.us.onResult = None
+ d.callback(True)
+ self.enableLocal(option)
+
+ def do_yes_false(self, state, option):
+ # Peer is unilaterally requesting us to enable an already-enabled option.
+ # Ignore this.
+ pass
+
+ def do_yes_true(self, state, option):
+ # This is a bogus state. It is here for completeness. It will never be
+ # entered.
+ assert False, "do_yes_true can never be entered, but was called with %r, %r" % (state, option)
+
+ doMap = {('no', False): do_no_false, ('no', True): do_no_true,
+ ('yes', False): do_yes_false, ('yes', True): do_yes_true}
+
+ def telnet_DONT(self, option):
+ s = self.getOptionState(option)
+ self.dontMap[s.us.state, s.us.negotiating](self, s, option)
+
+ def dont_no_false(self, state, option):
+ # Peer is unilaterally demanding us to disable an already-disabled option.
+ # Ignore this.
+ pass
+
+ def dont_no_true(self, state, option):
+ # Offered option was refused. Fail the Deferred returned by the
+ # previous will() call.
+ state.us.negotiating = False
+ d = state.us.onResult
+ state.us.onResult = None
+ d.errback(OptionRefused(option))
+
+ def dont_yes_false(self, state, option):
+ # Peer is unilaterally demanding we disable an option.
+ state.us.state = 'no'
+ self.disableLocal(option)
+ self._wont(option)
+
+ def dont_yes_true(self, state, option):
+ # Peer acknowledged our notice that we will disable an option.
+ state.us.state = 'no'
+ state.us.negotiating = False
+ d = state.us.onResult
+ state.us.onResult = None
+ d.callback(True)
+ self.disableLocal(option)
+
+ dontMap = {('no', False): dont_no_false, ('no', True): dont_no_true,
+ ('yes', False): dont_yes_false, ('yes', True): dont_yes_true}
+
+ def enableLocal(self, option):
+ """
+ Reject all attempts to enable options.
+ """
+ return False
+
+
+ def enableRemote(self, option):
+ """
+ Reject all attempts to enable options.
+ """
+ return False
+
+
+ def disableLocal(self, option):
+ """
+ Signal a programming error by raising an exception.
+
+ L{enableLocal} must return true for the given value of C{option} in
+ order for this method to be called. If a subclass of L{Telnet}
+ overrides enableLocal to allow certain options to be enabled, it must
+ also override disableLocal to disable those options.
+
+ @raise NotImplementedError: Always raised.
+ """
+ raise NotImplementedError(
+ "Don't know how to disable local telnet option %r" % (option,))
+
+
+ def disableRemote(self, option):
+ """
+ Signal a programming error by raising an exception.
+
+ L{enableRemote} must return true for the given value of C{option} in
+ order for this method to be called. If a subclass of L{Telnet}
+ overrides enableRemote to allow certain options to be enabled, it must
+ also override disableRemote tto disable those options.
+
+ @raise NotImplementedError: Always raised.
+ """
+ raise NotImplementedError(
+ "Don't know how to disable remote telnet option %r" % (option,))
+
+
+
+class ProtocolTransportMixin:
+ def write(self, bytes):
+ self.transport.write(bytes.replace('\n', '\r\n'))
+
+ def writeSequence(self, seq):
+ self.transport.writeSequence(seq)
+
+ def loseConnection(self):
+ self.transport.loseConnection()
+
+ def getHost(self):
+ return self.transport.getHost()
+
+ def getPeer(self):
+ return self.transport.getPeer()
+
+class TelnetTransport(Telnet, ProtocolTransportMixin):
+ """
+ @ivar protocol: An instance of the protocol to which this
+ transport is connected, or None before the connection is
+ established and after it is lost.
+
+ @ivar protocolFactory: A callable which returns protocol instances
+ which provide L{ITelnetProtocol}. This will be invoked when a
+ connection is established. It is passed *protocolArgs and
+ **protocolKwArgs.
+
+ @ivar protocolArgs: A tuple of additional arguments to
+ pass to protocolFactory.
+
+ @ivar protocolKwArgs: A dictionary of additional arguments
+ to pass to protocolFactory.
+ """
+
+ disconnecting = False
+
+ protocolFactory = None
+ protocol = None
+
+ def __init__(self, protocolFactory=None, *a, **kw):
+ Telnet.__init__(self)
+ if protocolFactory is not None:
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = a
+ self.protocolKwArgs = kw
+
+ def connectionMade(self):
+ if self.protocolFactory is not None:
+ self.protocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs)
+ assert ITelnetProtocol.providedBy(self.protocol)
+ try:
+ factory = self.factory
+ except AttributeError:
+ pass
+ else:
+ self.protocol.factory = factory
+ self.protocol.makeConnection(self)
+
+ def connectionLost(self, reason):
+ Telnet.connectionLost(self, reason)
+ if self.protocol is not None:
+ try:
+ self.protocol.connectionLost(reason)
+ finally:
+ del self.protocol
+
+ def enableLocal(self, option):
+ return self.protocol.enableLocal(option)
+
+ def enableRemote(self, option):
+ return self.protocol.enableRemote(option)
+
+ def disableLocal(self, option):
+ return self.protocol.disableLocal(option)
+
+ def disableRemote(self, option):
+ return self.protocol.disableRemote(option)
+
+ def unhandledSubnegotiation(self, command, bytes):
+ self.protocol.unhandledSubnegotiation(command, bytes)
+
+ def unhandledCommand(self, command, argument):
+ self.protocol.unhandledCommand(command, argument)
+
+ def applicationDataReceived(self, bytes):
+ self.protocol.dataReceived(bytes)
+
+ def write(self, data):
+ ProtocolTransportMixin.write(self, data.replace('\xff','\xff\xff'))
+
+
+class TelnetBootstrapProtocol(TelnetProtocol, ProtocolTransportMixin):
+ implements()
+
+ protocol = None
+
+ def __init__(self, protocolFactory, *args, **kw):
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = args
+ self.protocolKwArgs = kw
+
+ def connectionMade(self):
+ self.transport.negotiationMap[NAWS] = self.telnet_NAWS
+ self.transport.negotiationMap[LINEMODE] = self.telnet_LINEMODE
+
+ for opt in (LINEMODE, NAWS, SGA):
+ self.transport.do(opt).addErrback(log.err)
+ for opt in (ECHO,):
+ self.transport.will(opt).addErrback(log.err)
+
+ self.protocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs)
+
+ try:
+ factory = self.factory
+ except AttributeError:
+ pass
+ else:
+ self.protocol.factory = factory
+
+ self.protocol.makeConnection(self)
+
+ def connectionLost(self, reason):
+ if self.protocol is not None:
+ try:
+ self.protocol.connectionLost(reason)
+ finally:
+ del self.protocol
+
+ def dataReceived(self, data):
+ self.protocol.dataReceived(data)
+
+ def enableLocal(self, opt):
+ if opt == ECHO:
+ return True
+ elif opt == SGA:
+ return True
+ else:
+ return False
+
+ def enableRemote(self, opt):
+ if opt == LINEMODE:
+ self.transport.requestNegotiation(LINEMODE, MODE + chr(TRAPSIG))
+ return True
+ elif opt == NAWS:
+ return True
+ elif opt == SGA:
+ return True
+ else:
+ return False
+
+ def telnet_NAWS(self, bytes):
+ # NAWS is client -> server *only*. self.protocol will
+ # therefore be an ITerminalTransport, the `.protocol'
+ # attribute of which will be an ITerminalProtocol. Maybe.
+ # You know what, XXX TODO clean this up.
+ if len(bytes) == 4:
+ width, height = struct.unpack('!HH', ''.join(bytes))
+ self.protocol.terminalProtocol.terminalSize(width, height)
+ else:
+ log.msg("Wrong number of NAWS bytes")
+
+
+ linemodeSubcommands = {
+ LINEMODE_SLC: 'SLC'}
+ def telnet_LINEMODE(self, bytes):
+ revmap = {}
+ linemodeSubcommand = bytes[0]
+ if 0:
+ # XXX TODO: This should be enabled to parse linemode subnegotiation.
+ getattr(self, 'linemode_' + self.linemodeSubcommands[linemodeSubcommand])(bytes[1:])
+
+ def linemode_SLC(self, bytes):
+ chunks = zip(*[iter(bytes)]*3)
+ for slcFunction, slcValue, slcWhat in chunks:
+ # Later, we should parse stuff.
+ 'SLC', ord(slcFunction), ord(slcValue), ord(slcWhat)
+
+from twisted.protocols import basic
+
+class StatefulTelnetProtocol(basic.LineReceiver, TelnetProtocol):
+ delimiter = '\n'
+
+ state = 'Discard'
+
+ def connectionLost(self, reason):
+ basic.LineReceiver.connectionLost(self, reason)
+ TelnetProtocol.connectionLost(self, reason)
+
+ def lineReceived(self, line):
+ oldState = self.state
+ newState = getattr(self, "telnet_" + oldState)(line)
+ if newState is not None:
+ if self.state == oldState:
+ self.state = newState
+ else:
+ log.msg("Warning: state changed and new state returned")
+
+ def telnet_Discard(self, line):
+ pass
+
+from twisted.cred import credentials
+
+class AuthenticatingTelnetProtocol(StatefulTelnetProtocol):
+ """A protocol which prompts for credentials and attempts to authenticate them.
+
+ Username and password prompts are given (the password is obscured). When the
+ information is collected, it is passed to a portal and an avatar implementing
+ L{ITelnetProtocol} is requested. If an avatar is returned, it connected to this
+ protocol's transport, and this protocol's transport is connected to it.
+ Otherwise, the user is re-prompted for credentials.
+ """
+
+ state = "User"
+ protocol = None
+
+ def __init__(self, portal):
+ self.portal = portal
+
+ def connectionMade(self):
+ self.transport.write("Username: ")
+
+ def connectionLost(self, reason):
+ StatefulTelnetProtocol.connectionLost(self, reason)
+ if self.protocol is not None:
+ try:
+ self.protocol.connectionLost(reason)
+ self.logout()
+ finally:
+ del self.protocol, self.logout
+
+ def telnet_User(self, line):
+ self.username = line
+ self.transport.will(ECHO)
+ self.transport.write("Password: ")
+ return 'Password'
+
+ def telnet_Password(self, line):
+ username, password = self.username, line
+ del self.username
+ def login(ignored):
+ creds = credentials.UsernamePassword(username, password)
+ d = self.portal.login(creds, None, ITelnetProtocol)
+ d.addCallback(self._cbLogin)
+ d.addErrback(self._ebLogin)
+ self.transport.wont(ECHO).addCallback(login)
+ return 'Discard'
+
+ def _cbLogin(self, ial):
+ interface, protocol, logout = ial
+ assert interface is ITelnetProtocol
+ self.protocol = protocol
+ self.logout = logout
+ self.state = 'Command'
+
+ protocol.makeConnection(self.transport)
+ self.transport.protocol = protocol
+
+ def _ebLogin(self, failure):
+ self.transport.write("\nAuthentication failed\n")
+ self.transport.write("Username: ")
+ self.state = "User"
+
+__all__ = [
+ # Exceptions
+ 'TelnetError', 'NegotiationError', 'OptionRefused',
+ 'AlreadyNegotiating', 'AlreadyEnabled', 'AlreadyDisabled',
+
+ # Interfaces
+ 'ITelnetProtocol', 'ITelnetTransport',
+
+ # Other stuff, protocols, etc.
+ 'Telnet', 'TelnetProtocol', 'TelnetTransport',
+ 'TelnetBootstrapProtocol',
+
+ ]
diff --git a/twisted/conch/test/__init__.py b/twisted/conch/test/__init__.py
new file mode 100644
index 0000000..d09b412
--- /dev/null
+++ b/twisted/conch/test/__init__.py
@@ -0,0 +1 @@
+'conch tests'
diff --git a/twisted/conch/test/keydata.py b/twisted/conch/test/keydata.py
new file mode 100644
index 0000000..9be73c0
--- /dev/null
+++ b/twisted/conch/test/keydata.py
@@ -0,0 +1,174 @@
+# -*- test-case-name: twisted.conch.test.test_keys -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Data used by test_keys as well as others.
+"""
+RSAData = {
+ 'n':long('1062486685755247411169438309495398947372127791189432809481'
+ '382072971106157632182084539383569281493520117634129557550415277'
+ '516685881326038852354459895734875625093273594925884531272867425'
+ '864910490065695876046999646807138717162833156501L'),
+ 'e':35L,
+ 'd':long('6678487739032983727350755088256793383481946116047863373882'
+ '973030104095847973715959961839578340816412167985957218887914482'
+ '713602371850869127033494910375212470664166001439410214474266799'
+ '85974425203903884190893469297150446322896587555L'),
+ 'q':long('3395694744258061291019136154000709371890447462086362702627'
+ '9704149412726577280741108645721676968699696898960891593323L'),
+ 'p':long('3128922844292337321766351031842562691837301298995834258844'
+ '4720539204069737532863831050930719431498338835415515173887L')}
+
+DSAData = {
+ 'y':long('2300663509295750360093768159135720439490120577534296730713'
+ '348508834878775464483169644934425336771277908527130096489120714'
+ '610188630979820723924744291603865L'),
+ 'g':long('4451569990409370769930903934104221766858515498655655091803'
+ '866645719060300558655677517139568505649468378587802312867198352'
+ '1161998270001677664063945776405L'),
+ 'p':long('7067311773048598659694590252855127633397024017439939353776'
+ '608320410518694001356789646664502838652272205440894335303988504'
+ '978724817717069039110940675621677L'),
+ 'q':1184501645189849666738820838619601267690550087703L,
+ 'x':863951293559205482820041244219051653999559962819L}
+
+publicRSA_openssh = ("ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBE"
+"vLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYL"
+"h5KmRpslkYHRivcJSkbh/C+BR3utDS555mV comment")
+
+privateRSA_openssh = """-----BEGIN RSA PRIVATE KEY-----
+MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
+4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
+vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
+Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
+xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
+PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
+gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
+DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
+pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
+EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
+-----END RSA PRIVATE KEY-----"""
+
+# some versions of OpenSSH generate these (slightly different keys)
+privateRSA_openssh_alternate = """-----BEGIN RSA PRIVATE KEY-----
+MIIBzjCCAcgCAQACYQCvMnHw5g6cmbN/i18ES8uLwNU+snf9z2TYj8DPrh/GMd/2
+KbJEluLG1CGUf2V82NQjH7guaskflA1GwWmitwcMo5PBNNguHkqZGmyWRgdGK9wl
+KRuH8L4FHe60NLnnmZUCASMCYG4ftVWX6+1n7SuZbuzB7ahNUtbz1mUGBN/lVJ/M
+iQA8m2eH7GWgq81vZZCKl5BNxiGPqI3YWYZDtYGxtNdfLCIKYcElikcStJr4ehEc
+SqiLdcSRCTu+BMpF2VeKDSfLIwIxANyfa9mYIVYRjelfA50K05NuE3dBPIVPAHD9
+BVT/vD0Jv4P2l39kEJEE/qJnR1RCawIxAMtKS9BAR+hFUvfHrwwgbUMNtjmU+dql
+5QMGdoMk64ihVaKo3hI7d0mSiqlx0gKT/wIwS6Rffc3CSWUatmm4GJX/Zb9XIZK1
+qgx1Lg2bbZmCXhH4hQQWr1WCBdXTpWU9BvIzAjAXO7DkmaHRZwIq8j/kIPaLUgYy
+d20DC6Uk6su3N2tgEnAv2MjsJA2iAh55w918ozMCMQC0c5dLUBCjF7OoR/E6FHZS
+0TgqzxIUNMGoVEwpNYCgOLjw+kzEwoWr24eCutzr2yowAA==
+------END RSA PRIVATE KEY------"""
+
+privateRSA_openssh_encrypted = """-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF
+
+30qUR7DYY/rpVJu159paRM1mUqt/IMibfEMTKWSjNhCVD21hskftZCJROw/WgIFt
+ncusHpJMkjgwEpho0KyKilcC7zxjpunTex24Meb5pCdXCrYft8AyUkRdq3dugMqT
+4nuWuWxziluBhKQ2M9tPGcEOeulU4vVjceZt2pZhZQVBf08o3XUv5/7RYd24M9md
+WIo+5zdj2YQkI6xMFTP954O/X32ME1KQt98wgNEy6mxhItbvf00mH3woALwEKP3v
+PSMxxtx3VKeDKd9YTOm1giKkXZUf91vZWs0378tUBrU4U5qJxgryTjvvVKOtofj6
+4qQy6+r6M6wtwVlXBgeRm2gBPvL3nv6MsROp3E6ztBd/e7A8fSec+UTq3ko/EbGP
+0QG+IG5tg8FsdITxQ9WAIITZL3Rc6hA5Ymx1VNhySp3iSiso8Jof27lku4pyuvRV
+ko/B3N2H7LnQrGV0GyrjeYocW/qZh/PCsY48JBFhlNQexn2mn44AJW3y5xgbhvKA
+3mrmMD1hD17ZvZxi4fPHjbuAyM1vFqhQx63eT9ijbwJ91svKJl5O5MIv41mCRonm
+hxvOXw8S0mjSasyofptzzQCtXxFLQigXbpQBltII+Ys=
+-----END RSA PRIVATE KEY-----"""
+
+publicRSA_lsh = ("{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuOTc6AK8yc"
+"fDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW4sbUIZR/ZXzY1CMfuC5qyR+UDUbB"
+"aaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fwvgUd7rQ0ueeZlSkoMTplMTojKSkp}")
+
+privateRSA_lsh = ("(11:private-key(9:rsa-pkcs1(1:n97:\x00\xaf2q\xf0\xe6\x0e"
+"\x9c\x99\xb3\x7f\x8b_\x04K\xcb\x8b\xc0\xd5>\xb2w\xfd\xcfd\xd8\x8f\xc0\xcf"
+"\xae\x1f\xc61\xdf\xf6)\xb2D\x96\xe2\xc6\xd4!\x94\x7fe|\xd8\xd4#\x1f\xb8.j"
+"\xc9\x1f\x94\rF\xc1i\xa2\xb7\x07\x0c\xa3\x93\xc14\xd8.\x1eJ\x99\x1al\x96F"
+"\x07F+\xdc%)\x1b\x87\xf0\xbe\x05\x1d\xee\xb44\xb9\xe7\x99\x95)(1:e1:#)(1:d9"
+"6:n\x1f\xb5U\x97\xeb\xedg\xed+\x99n\xec\xc1\xed\xa8MR\xd6\xf3\xd6e\x06\x04"
+"\xdf\xe5T\x9f\xcc\x89\x00<\x9bg\x87\xece\xa0\xab\xcdoe\x90\x8a\x97\x90M\xc6"
+'!\x8f\xa8\x8d\xd8Y\x86C\xb5\x81\xb1\xb4\xd7_,"\na\xc1%\x8aG\x12\xb4\x9a\xf8'
+"z\x11\x1cJ\xa8\x8bu\xc4\x91\t;\xbe\x04\xcaE\xd9W\x8a\r\'\xcb#)(1:p49:\x00"
+"\xdc\x9fk\xd9\x98!V\x11\x8d\xe9_\x03\x9d\n\xd3\x93n\x13wA<\x85O\x00p\xfd"
+"\x05T\xff\xbc=\t\xbf\x83\xf6\x97\x7fd\x10\x91\x04\xfe\xa2gGTBk)(1:q49:\x00"
+"\xcbJK\xd0@G\xe8ER\xf7\xc7\xaf\x0c mC\r\xb69\x94\xf9\xda\xa5\xe5\x03\x06v"
+"\x83$\xeb\x88\xa1U\xa2\xa8\xde\x12;wI\x92\x8a\xa9q\xd2\x02\x93\xff)(1:a48:K"
+"\xa4_}\xcd\xc2Ie\x1a\xb6i\xb8\x18\x95\xffe\xbfW!\x92\xb5\xaa\x0cu.\r\x9bm"
+"\x99\x82^\x11\xf8\x85\x04\x16\xafU\x82\x05\xd5\xd3\xa5e=\x06\xf23)(1:b48:"
+"\x17;\xb0\xe4\x99\xa1\xd1g\x02*\xf2?\xe4 \xf6\x8bR\x062wm\x03\x0b\xa5$\xea"
+"\xcb\xb77k`\x12p/\xd8\xc8\xec$\r\xa2\x02\x1ey\xc3\xdd|\xa33)(1:c49:\x00\xb4"
+"s\x97KP\x10\xa3\x17\xb3\xa8G\xf1:\x14vR\xd18*\xcf\x12\x144\xc1\xa8TL)5\x80"
+"\xa08\xb8\xf0\xfaL\xc4\xc2\x85\xab\xdb\x87\x82\xba\xdc\xeb\xdb*)))")
+
+privateRSA_agentv3 = ("\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00`"
+"n\x1f\xb5U\x97\xeb\xedg\xed+\x99n\xec\xc1\xed\xa8MR\xd6\xf3\xd6e\x06\x04"
+"\xdf\xe5T\x9f\xcc\x89\x00<\x9bg\x87\xece\xa0\xab\xcdoe\x90\x8a\x97\x90M\xc6"
+'!\x8f\xa8\x8d\xd8Y\x86C\xb5\x81\xb1\xb4\xd7_,"\na\xc1%\x8aG\x12\xb4\x9a\xf8'
+"z\x11\x1cJ\xa8\x8bu\xc4\x91\t;\xbe\x04\xcaE\xd9W\x8a\r\'\xcb#\x00\x00\x00a"
+"\x00\xaf2q\xf0\xe6\x0e\x9c\x99\xb3\x7f\x8b_\x04K\xcb\x8b\xc0\xd5>\xb2w\xfd"
+"\xcfd\xd8\x8f\xc0\xcf\xae\x1f\xc61\xdf\xf6)\xb2D\x96\xe2\xc6\xd4!\x94\x7fe|"
+"\xd8\xd4#\x1f\xb8.j\xc9\x1f\x94\rF\xc1i\xa2\xb7\x07\x0c\xa3\x93\xc14\xd8."
+"\x1eJ\x99\x1al\x96F\x07F+\xdc%)\x1b\x87\xf0\xbe\x05\x1d\xee\xb44\xb9\xe7"
+"\x99\x95\x00\x00\x001\x00\xb4s\x97KP\x10\xa3\x17\xb3\xa8G\xf1:\x14vR\xd18*"
+"\xcf\x12\x144\xc1\xa8TL)5\x80\xa08\xb8\xf0\xfaL\xc4\xc2\x85\xab\xdb\x87\x82"
+"\xba\xdc\xeb\xdb*\x00\x00\x001\x00\xcbJK\xd0@G\xe8ER\xf7\xc7\xaf\x0c mC\r"
+"\xb69\x94\xf9\xda\xa5\xe5\x03\x06v\x83$\xeb\x88\xa1U\xa2\xa8\xde\x12;wI\x92"
+"\x8a\xa9q\xd2\x02\x93\xff\x00\x00\x001\x00\xdc\x9fk\xd9\x98!V\x11\x8d\xe9_"
+"\x03\x9d\n\xd3\x93n\x13wA<\x85O\x00p\xfd\x05T\xff\xbc=\t\xbf\x83\xf6\x97"
+"\x7fd\x10\x91\x04\xfe\xa2gGTBk")
+
+publicDSA_openssh = ("ssh-dss AAAAB3NzaC1kc3MAAABBAIbwTOSsZ7Bl7U1KyMNqV13Tu7"
+"yRAtTr70PVI3QnfrPumf2UzCgpL1ljbKxSfAi05XvrE/1vfCFAsFYXRZLhQy0AAAAVAM965Akmo"
+"6eAi7K+k9qDR4TotFAXAAAAQADZlpTW964haQWS4vC063NGdldT6xpUGDcDRqbm90CoPEa2RmNO"
+"uOqi8lnbhYraEzypYH3K4Gzv/bxCBnKtHRUAAABAK+1osyWBS0+P90u/rAuko6chZ98thUSY2kL"
+"SHp6hLKyy2bjnT29h7haELE+XHfq2bM9fckDx2FLOSIJzy83VmQ== comment")
+
+privateDSA_openssh = """-----BEGIN DSA PRIVATE KEY-----
+MIH4AgEAAkEAhvBM5KxnsGXtTUrIw2pXXdO7vJEC1OvvQ9UjdCd+s+6Z/ZTMKCkv
+WWNsrFJ8CLTle+sT/W98IUCwVhdFkuFDLQIVAM965Akmo6eAi7K+k9qDR4TotFAX
+AkAA2ZaU1veuIWkFkuLwtOtzRnZXU+saVBg3A0am5vdAqDxGtkZjTrjqovJZ24WK
+2hM8qWB9yuBs7/28QgZyrR0VAkAr7WizJYFLT4/3S7+sC6SjpyFn3y2FRJjaQtIe
+nqEsrLLZuOdPb2HuFoQsT5cd+rZsz19yQPHYUs5IgnPLzdWZAhUAl1TqdmlAG/b4
+nnVchGiO9sML8MM=
+-----END DSA PRIVATE KEY-----"""
+
+publicDSA_lsh = ("{KDEwOnB1YmxpYy1rZXkoMzpkc2EoMTpwNjU6AIbwTOSsZ7Bl7U1KyMNqV"
+"13Tu7yRAtTr70PVI3QnfrPumf2UzCgpL1ljbKxSfAi05XvrE/1vfCFAsFYXRZLhQy0pKDE6cTIx"
+"OgDPeuQJJqOngIuyvpPag0eE6LRQFykoMTpnNjQ6ANmWlNb3riFpBZLi8LTrc0Z2V1PrGlQYNwN"
+"Gpub3QKg8RrZGY0646qLyWduFitoTPKlgfcrgbO/9vEIGcq0dFSkoMTp5NjQ6K+1osyWBS0+P90"
+"u/rAuko6chZ98thUSY2kLSHp6hLKyy2bjnT29h7haELE+XHfq2bM9fckDx2FLOSIJzy83VmSkpK"
+"Q==}")
+
+privateDSA_lsh = ("(11:private-key(3:dsa(1:p65:\x00\x86\xf0L\xe4\xacg\xb0e"
+"\xedMJ\xc8\xc3jW]\xd3\xbb\xbc\x91\x02\xd4\xeb\xefC\xd5#t'~\xb3\xee\x99\xfd"
+"\x94\xcc()/Ycl\xacR|\x08\xb4\xe5{\xeb\x13\xfdo|!@\xb0V\x17E\x92\xe1C-)(1:q2"
+"1:\x00\xcfz\xe4\t&\xa3\xa7\x80\x8b\xb2\xbe\x93\xda\x83G\x84\xe8\xb4P\x17)(1"
+":g64:\x00\xd9\x96\x94\xd6\xf7\xae!i\x05\x92\xe2\xf0\xb4\xebsFvWS\xeb\x1aT"
+"\x187\x03F\xa6\xe6\xf7@\xa8<F\xb6FcN\xb8\xea\xa2\xf2Y\xdb\x85\x8a\xda\x13<"
+"\xa9`}\xca\xe0l\xef\xfd\xbcB\x06r\xad\x1d\x15)(1:y64:+\xedh\xb3%\x81KO\x8f"
+"\xf7K\xbf\xac\x0b\xa4\xa3\xa7!g\xdf-\x85D\x98\xdaB\xd2\x1e\x9e\xa1,\xac\xb2"
+"\xd9\xb8\xe7Ooa\xee\x16\x84,O\x97\x1d\xfa\xb6l\xcf_r@\xf1\xd8R\xceH\x82s"
+"\xcb\xcd\xd5\x99)(1:x21:\x00\x97T\xeavi@\x1b\xf6\xf8\x9eu\\\x84h\x8e\xf6"
+"\xc3\x0b\xf0\xc3)))")
+
+privateDSA_agentv3 = ("\x00\x00\x00\x07ssh-dss\x00\x00\x00A\x00\x86\xf0L\xe4"
+"\xacg\xb0e\xedMJ\xc8\xc3jW]\xd3\xbb\xbc\x91\x02\xd4\xeb\xefC\xd5#t'~\xb3"
+"\xee\x99\xfd\x94\xcc()/Ycl\xacR|\x08\xb4\xe5{\xeb\x13\xfdo|!@\xb0V\x17E\x92"
+"\xe1C-\x00\x00\x00\x15\x00\xcfz\xe4\t&\xa3\xa7\x80\x8b\xb2\xbe\x93\xda\x83G"
+"\x84\xe8\xb4P\x17\x00\x00\x00@\x00\xd9\x96\x94\xd6\xf7\xae!i\x05\x92\xe2"
+"\xf0\xb4\xebsFvWS\xeb\x1aT\x187\x03F\xa6\xe6\xf7@\xa8<F\xb6FcN\xb8\xea\xa2"
+"\xf2Y\xdb\x85\x8a\xda\x13<\xa9`}\xca\xe0l\xef\xfd\xbcB\x06r\xad\x1d\x15\x00"
+"\x00\x00@+\xedh\xb3%\x81KO\x8f\xf7K\xbf\xac\x0b\xa4\xa3\xa7!g\xdf-\x85D\x98"
+"\xdaB\xd2\x1e\x9e\xa1,\xac\xb2\xd9\xb8\xe7Ooa\xee\x16\x84,O\x97\x1d\xfa\xb6"
+"l\xcf_r@\xf1\xd8R\xceH\x82s\xcb\xcd\xd5\x99\x00\x00\x00\x15\x00\x97T\xeavi@"
+"\x1b\xf6\xf8\x9eu\\\x84h\x8e\xf6\xc3\x0b\xf0\xc3")
+
+__all__ = ['DSAData', 'RSAData', 'privateDSA_agentv3', 'privateDSA_lsh',
+ 'privateDSA_openssh', 'privateRSA_agentv3', 'privateRSA_lsh',
+ 'privateRSA_openssh', 'publicDSA_lsh', 'publicDSA_openssh',
+ 'publicRSA_lsh', 'publicRSA_openssh', 'privateRSA_openssh_alternate']
+
diff --git a/twisted/conch/test/test_agent.py b/twisted/conch/test/test_agent.py
new file mode 100644
index 0000000..532a0e5
--- /dev/null
+++ b/twisted/conch/test/test_agent.py
@@ -0,0 +1,399 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.ssh.agent}.
+"""
+
+import struct
+
+from twisted.trial import unittest
+
+try:
+ import OpenSSL
+except ImportError:
+ iosim = None
+else:
+ from twisted.test import iosim
+
+try:
+ import Crypto.Cipher.DES3
+except ImportError:
+ Crypto = None
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1 = None
+
+if Crypto and pyasn1:
+ from twisted.conch.ssh import keys, agent
+else:
+ keys = agent = None
+
+from twisted.conch.test import keydata
+from twisted.conch.error import ConchError, MissingKeyStoreError
+
+
+class StubFactory(object):
+ """
+ Mock factory that provides the keys attribute required by the
+ SSHAgentServerProtocol
+ """
+ def __init__(self):
+ self.keys = {}
+
+
+
+class AgentTestBase(unittest.TestCase):
+ """
+ Tests for SSHAgentServer/Client.
+ """
+ if iosim is None:
+ skip = "iosim requires SSL, but SSL is not available"
+ elif agent is None or keys is None:
+ skip = "Cannot run without PyCrypto or PyASN1"
+
+ def setUp(self):
+ # wire up our client <-> server
+ self.client, self.server, self.pump = iosim.connectedServerAndClient(
+ agent.SSHAgentServer, agent.SSHAgentClient)
+
+ # the server's end of the protocol is stateful and we store it on the
+ # factory, for which we only need a mock
+ self.server.factory = StubFactory()
+
+ # pub/priv keys of each kind
+ self.rsaPrivate = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.dsaPrivate = keys.Key.fromString(keydata.privateDSA_openssh)
+
+ self.rsaPublic = keys.Key.fromString(keydata.publicRSA_openssh)
+ self.dsaPublic = keys.Key.fromString(keydata.publicDSA_openssh)
+
+
+
+class TestServerProtocolContractWithFactory(AgentTestBase):
+ """
+ The server protocol is stateful and so uses its factory to track state
+ across requests. This test asserts that the protocol raises if its factory
+ doesn't provide the necessary storage for that state.
+ """
+ def test_factorySuppliesKeyStorageForServerProtocol(self):
+ # need a message to send into the server
+ msg = struct.pack('!LB',1, agent.AGENTC_REQUEST_IDENTITIES)
+ del self.server.factory.__dict__['keys']
+ self.assertRaises(MissingKeyStoreError,
+ self.server.dataReceived, msg)
+
+
+
+class TestUnimplementedVersionOneServer(AgentTestBase):
+ """
+ Tests for methods with no-op implementations on the server. We need these
+ for clients, such as openssh, that try v1 methods before going to v2.
+
+ Because the client doesn't expose these operations with nice method names,
+ we invoke sendRequest directly with an op code.
+ """
+
+ def test_agentc_REQUEST_RSA_IDENTITIES(self):
+ """
+ assert that we get the correct op code for an RSA identities request
+ """
+ d = self.client.sendRequest(agent.AGENTC_REQUEST_RSA_IDENTITIES, '')
+ self.pump.flush()
+ def _cb(packet):
+ self.assertEqual(
+ agent.AGENT_RSA_IDENTITIES_ANSWER, ord(packet[0]))
+ return d.addCallback(_cb)
+
+
+ def test_agentc_REMOVE_RSA_IDENTITY(self):
+ """
+ assert that we get the correct op code for an RSA remove identity request
+ """
+ d = self.client.sendRequest(agent.AGENTC_REMOVE_RSA_IDENTITY, '')
+ self.pump.flush()
+ return d.addCallback(self.assertEqual, '')
+
+
+ def test_agentc_REMOVE_ALL_RSA_IDENTITIES(self):
+ """
+ assert that we get the correct op code for an RSA remove all identities
+ request.
+ """
+ d = self.client.sendRequest(agent.AGENTC_REMOVE_ALL_RSA_IDENTITIES, '')
+ self.pump.flush()
+ return d.addCallback(self.assertEqual, '')
+
+
+
+if agent is not None:
+ class CorruptServer(agent.SSHAgentServer):
+ """
+ A misbehaving server that returns bogus response op codes so that we can
+ verify that our callbacks that deal with these op codes handle such
+ miscreants.
+ """
+ def agentc_REQUEST_IDENTITIES(self, data):
+ self.sendResponse(254, '')
+
+
+ def agentc_SIGN_REQUEST(self, data):
+ self.sendResponse(254, '')
+
+
+
+class TestClientWithBrokenServer(AgentTestBase):
+ """
+ verify error handling code in the client using a misbehaving server
+ """
+
+ def setUp(self):
+ AgentTestBase.setUp(self)
+ self.client, self.server, self.pump = iosim.connectedServerAndClient(
+ CorruptServer, agent.SSHAgentClient)
+ # the server's end of the protocol is stateful and we store it on the
+ # factory, for which we only need a mock
+ self.server.factory = StubFactory()
+
+
+ def test_signDataCallbackErrorHandling(self):
+ """
+ Assert that L{SSHAgentClient.signData} raises a ConchError
+ if we get a response from the server whose opcode doesn't match
+ the protocol for data signing requests.
+ """
+ d = self.client.signData(self.rsaPublic.blob(), "John Hancock")
+ self.pump.flush()
+ return self.assertFailure(d, ConchError)
+
+
+ def test_requestIdentitiesCallbackErrorHandling(self):
+ """
+ Assert that L{SSHAgentClient.requestIdentities} raises a ConchError
+ if we get a response from the server whose opcode doesn't match
+ the protocol for identity requests.
+ """
+ d = self.client.requestIdentities()
+ self.pump.flush()
+ return self.assertFailure(d, ConchError)
+
+
+
+class TestAgentKeyAddition(AgentTestBase):
+ """
+ Test adding different flavors of keys to an agent.
+ """
+
+ def test_addRSAIdentityNoComment(self):
+ """
+ L{SSHAgentClient.addIdentity} adds the private key it is called
+ with to the SSH agent server to which it is connected, associating
+ it with the comment it is called with.
+
+ This test asserts that ommitting the comment produces an
+ empty string for the comment on the server.
+ """
+ d = self.client.addIdentity(self.rsaPrivate.privateBlob())
+ self.pump.flush()
+ def _check(ignored):
+ serverKey = self.server.factory.keys[self.rsaPrivate.blob()]
+ self.assertEqual(self.rsaPrivate, serverKey[0])
+ self.assertEqual('', serverKey[1])
+ return d.addCallback(_check)
+
+
+ def test_addDSAIdentityNoComment(self):
+ """
+ L{SSHAgentClient.addIdentity} adds the private key it is called
+ with to the SSH agent server to which it is connected, associating
+ it with the comment it is called with.
+
+ This test asserts that ommitting the comment produces an
+ empty string for the comment on the server.
+ """
+ d = self.client.addIdentity(self.dsaPrivate.privateBlob())
+ self.pump.flush()
+ def _check(ignored):
+ serverKey = self.server.factory.keys[self.dsaPrivate.blob()]
+ self.assertEqual(self.dsaPrivate, serverKey[0])
+ self.assertEqual('', serverKey[1])
+ return d.addCallback(_check)
+
+
+ def test_addRSAIdentityWithComment(self):
+ """
+ L{SSHAgentClient.addIdentity} adds the private key it is called
+ with to the SSH agent server to which it is connected, associating
+ it with the comment it is called with.
+
+ This test asserts that the server receives/stores the comment
+ as sent by the client.
+ """
+ d = self.client.addIdentity(
+ self.rsaPrivate.privateBlob(), comment='My special key')
+ self.pump.flush()
+ def _check(ignored):
+ serverKey = self.server.factory.keys[self.rsaPrivate.blob()]
+ self.assertEqual(self.rsaPrivate, serverKey[0])
+ self.assertEqual('My special key', serverKey[1])
+ return d.addCallback(_check)
+
+
+ def test_addDSAIdentityWithComment(self):
+ """
+ L{SSHAgentClient.addIdentity} adds the private key it is called
+ with to the SSH agent server to which it is connected, associating
+ it with the comment it is called with.
+
+ This test asserts that the server receives/stores the comment
+ as sent by the client.
+ """
+ d = self.client.addIdentity(
+ self.dsaPrivate.privateBlob(), comment='My special key')
+ self.pump.flush()
+ def _check(ignored):
+ serverKey = self.server.factory.keys[self.dsaPrivate.blob()]
+ self.assertEqual(self.dsaPrivate, serverKey[0])
+ self.assertEqual('My special key', serverKey[1])
+ return d.addCallback(_check)
+
+
+
+class TestAgentClientFailure(AgentTestBase):
+ def test_agentFailure(self):
+ """
+ verify that the client raises ConchError on AGENT_FAILURE
+ """
+ d = self.client.sendRequest(254, '')
+ self.pump.flush()
+ return self.assertFailure(d, ConchError)
+
+
+
+class TestAgentIdentityRequests(AgentTestBase):
+ """
+ Test operations against a server with identities already loaded.
+ """
+
+ def setUp(self):
+ AgentTestBase.setUp(self)
+ self.server.factory.keys[self.dsaPrivate.blob()] = (
+ self.dsaPrivate, 'a comment')
+ self.server.factory.keys[self.rsaPrivate.blob()] = (
+ self.rsaPrivate, 'another comment')
+
+
+ def test_signDataRSA(self):
+ """
+ Sign data with an RSA private key and then verify it with the public
+ key.
+ """
+ d = self.client.signData(self.rsaPublic.blob(), "John Hancock")
+ self.pump.flush()
+ def _check(sig):
+ expected = self.rsaPrivate.sign("John Hancock")
+ self.assertEqual(expected, sig)
+ self.assertTrue(self.rsaPublic.verify(sig, "John Hancock"))
+ return d.addCallback(_check)
+
+
+ def test_signDataDSA(self):
+ """
+ Sign data with a DSA private key and then verify it with the public
+ key.
+ """
+ d = self.client.signData(self.dsaPublic.blob(), "John Hancock")
+ self.pump.flush()
+ def _check(sig):
+ # Cannot do this b/c DSA uses random numbers when signing
+ # expected = self.dsaPrivate.sign("John Hancock")
+ # self.assertEqual(expected, sig)
+ self.assertTrue(self.dsaPublic.verify(sig, "John Hancock"))
+ return d.addCallback(_check)
+
+
+ def test_signDataRSAErrbackOnUnknownBlob(self):
+ """
+ Assert that we get an errback if we try to sign data using a key that
+ wasn't added.
+ """
+ del self.server.factory.keys[self.rsaPublic.blob()]
+ d = self.client.signData(self.rsaPublic.blob(), "John Hancock")
+ self.pump.flush()
+ return self.assertFailure(d, ConchError)
+
+
+ def test_requestIdentities(self):
+ """
+ Assert that we get all of the keys/comments that we add when we issue a
+ request for all identities.
+ """
+ d = self.client.requestIdentities()
+ self.pump.flush()
+ def _check(keyt):
+ expected = {}
+ expected[self.dsaPublic.blob()] = 'a comment'
+ expected[self.rsaPublic.blob()] = 'another comment'
+
+ received = {}
+ for k in keyt:
+ received[keys.Key.fromString(k[0], type='blob').blob()] = k[1]
+ self.assertEqual(expected, received)
+ return d.addCallback(_check)
+
+
+
+class TestAgentKeyRemoval(AgentTestBase):
+ """
+ Test support for removing keys in a remote server.
+ """
+
+ def setUp(self):
+ AgentTestBase.setUp(self)
+ self.server.factory.keys[self.dsaPrivate.blob()] = (
+ self.dsaPrivate, 'a comment')
+ self.server.factory.keys[self.rsaPrivate.blob()] = (
+ self.rsaPrivate, 'another comment')
+
+
+ def test_removeRSAIdentity(self):
+ """
+ Assert that we can remove an RSA identity.
+ """
+ # only need public key for this
+ d = self.client.removeIdentity(self.rsaPrivate.blob())
+ self.pump.flush()
+
+ def _check(ignored):
+ self.assertEqual(1, len(self.server.factory.keys))
+ self.assertIn(self.dsaPrivate.blob(), self.server.factory.keys)
+ self.assertNotIn(self.rsaPrivate.blob(), self.server.factory.keys)
+ return d.addCallback(_check)
+
+
+ def test_removeDSAIdentity(self):
+ """
+ Assert that we can remove a DSA identity.
+ """
+ # only need public key for this
+ d = self.client.removeIdentity(self.dsaPrivate.blob())
+ self.pump.flush()
+
+ def _check(ignored):
+ self.assertEqual(1, len(self.server.factory.keys))
+ self.assertIn(self.rsaPrivate.blob(), self.server.factory.keys)
+ return d.addCallback(_check)
+
+
+ def test_removeAllIdentities(self):
+ """
+ Assert that we can remove all identities.
+ """
+ d = self.client.removeAllIdentities()
+ self.pump.flush()
+
+ def _check(ignored):
+ self.assertEqual(0, len(self.server.factory.keys))
+ return d.addCallback(_check)
diff --git a/twisted/conch/test/test_cftp.py b/twisted/conch/test/test_cftp.py
new file mode 100644
index 0000000..03e327a
--- /dev/null
+++ b/twisted/conch/test/test_cftp.py
@@ -0,0 +1,975 @@
+# -*- test-case-name: twisted.conch.test.test_cftp -*-
+# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
+# See LICENSE file for details.
+
+"""
+Tests for L{twisted.conch.scripts.cftp}.
+"""
+
+import locale
+import time, sys, os, operator, getpass, struct
+from StringIO import StringIO
+
+from twisted.conch.test.test_ssh import Crypto, pyasn1
+
+_reason = None
+if Crypto and pyasn1:
+ try:
+ from twisted.conch import unix
+ from twisted.conch.scripts import cftp
+ from twisted.conch.test.test_filetransfer import FileTransferForTestAvatar
+ except ImportError, e:
+ # Python 2.3 compatibility fix
+ sys.modules.pop("twisted.conch.unix", None)
+ unix = None
+ _reason = str(e)
+ del e
+else:
+ unix = None
+
+
+from twisted.python.fakepwd import UserDatabase
+from twisted.trial.unittest import TestCase
+from twisted.cred import portal
+from twisted.internet import reactor, protocol, interfaces, defer, error
+from twisted.internet.utils import getProcessOutputAndValue
+from twisted.python import log
+from twisted.conch import ls
+from twisted.test.proto_helpers import StringTransport
+from twisted.internet.task import Clock
+
+from twisted.conch.test import test_ssh, test_conch
+from twisted.conch.test.test_filetransfer import SFTPTestBase
+from twisted.conch.test.test_filetransfer import FileTransferTestAvatar
+
+
+
+class ListingTests(TestCase):
+ """
+ Tests for L{lsLine}, the function which generates an entry for a file or
+ directory in an SFTP I{ls} command's output.
+ """
+ if getattr(time, 'tzset', None) is None:
+ skip = "Cannot test timestamp formatting code without time.tzset"
+
+ def setUp(self):
+ """
+ Patch the L{ls} module's time function so the results of L{lsLine} are
+ deterministic.
+ """
+ self.now = 123456789
+ def fakeTime():
+ return self.now
+ self.patch(ls, 'time', fakeTime)
+
+ # Make sure that the timezone ends up the same after these tests as
+ # it was before.
+ if 'TZ' in os.environ:
+ self.addCleanup(operator.setitem, os.environ, 'TZ', os.environ['TZ'])
+ self.addCleanup(time.tzset)
+ else:
+ def cleanup():
+ # os.environ.pop is broken! Don't use it! Ever! Or die!
+ try:
+ del os.environ['TZ']
+ except KeyError:
+ pass
+ time.tzset()
+ self.addCleanup(cleanup)
+
+
+ def _lsInTimezone(self, timezone, stat):
+ """
+ Call L{ls.lsLine} after setting the timezone to C{timezone} and return
+ the result.
+ """
+ # Set the timezone to a well-known value so the timestamps are
+ # predictable.
+ os.environ['TZ'] = timezone
+ time.tzset()
+ return ls.lsLine('foo', stat)
+
+
+ def test_oldFile(self):
+ """
+ A file with an mtime six months (approximately) or more in the past has
+ a listing including a low-resolution timestamp.
+ """
+ # Go with 7 months. That's more than 6 months.
+ then = self.now - (60 * 60 * 24 * 31 * 7)
+ stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
+
+ self.assertEqual(
+ self._lsInTimezone('America/New_York', stat),
+ '!--------- 0 0 0 0 Apr 26 1973 foo')
+ self.assertEqual(
+ self._lsInTimezone('Pacific/Auckland', stat),
+ '!--------- 0 0 0 0 Apr 27 1973 foo')
+
+
+ def test_oldSingleDigitDayOfMonth(self):
+ """
+ A file with a high-resolution timestamp which falls on a day of the
+ month which can be represented by one decimal digit is formatted with
+ one padding 0 to preserve the columns which come after it.
+ """
+ # A point about 7 months in the past, tweaked to fall on the first of a
+ # month so we test the case we want to test.
+ then = self.now - (60 * 60 * 24 * 31 * 7) + (60 * 60 * 24 * 5)
+ stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
+
+ self.assertEqual(
+ self._lsInTimezone('America/New_York', stat),
+ '!--------- 0 0 0 0 May 01 1973 foo')
+ self.assertEqual(
+ self._lsInTimezone('Pacific/Auckland', stat),
+ '!--------- 0 0 0 0 May 02 1973 foo')
+
+
+ def test_newFile(self):
+ """
+ A file with an mtime fewer than six months (approximately) in the past
+ has a listing including a high-resolution timestamp excluding the year.
+ """
+ # A point about three months in the past.
+ then = self.now - (60 * 60 * 24 * 31 * 3)
+ stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
+
+ self.assertEqual(
+ self._lsInTimezone('America/New_York', stat),
+ '!--------- 0 0 0 0 Aug 28 17:33 foo')
+ self.assertEqual(
+ self._lsInTimezone('Pacific/Auckland', stat),
+ '!--------- 0 0 0 0 Aug 29 09:33 foo')
+
+
+ def test_localeIndependent(self):
+ """
+ The month name in the date is locale independent.
+ """
+ # A point about three months in the past.
+ then = self.now - (60 * 60 * 24 * 31 * 3)
+ stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
+
+ # Fake that we're in a language where August is not Aug (e.g.: Spanish)
+ currentLocale = locale.getlocale()
+ locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
+ self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale)
+
+ self.assertEqual(
+ self._lsInTimezone('America/New_York', stat),
+ '!--------- 0 0 0 0 Aug 28 17:33 foo')
+ self.assertEqual(
+ self._lsInTimezone('Pacific/Auckland', stat),
+ '!--------- 0 0 0 0 Aug 29 09:33 foo')
+
+ # if alternate locale is not available, the previous test will be
+ # skipped, please install this locale for it to run
+ currentLocale = locale.getlocale()
+ try:
+ try:
+ locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
+ except locale.Error:
+ test_localeIndependent.skip = "The es_AR.UTF8 locale is not installed."
+ finally:
+ locale.setlocale(locale.LC_ALL, currentLocale)
+
+
+ def test_newSingleDigitDayOfMonth(self):
+ """
+ A file with a high-resolution timestamp which falls on a day of the
+ month which can be represented by one decimal digit is formatted with
+ one padding 0 to preserve the columns which come after it.
+ """
+ # A point about three months in the past, tweaked to fall on the first
+ # of a month so we test the case we want to test.
+ then = self.now - (60 * 60 * 24 * 31 * 3) + (60 * 60 * 24 * 4)
+ stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
+
+ self.assertEqual(
+ self._lsInTimezone('America/New_York', stat),
+ '!--------- 0 0 0 0 Sep 01 17:33 foo')
+ self.assertEqual(
+ self._lsInTimezone('Pacific/Auckland', stat),
+ '!--------- 0 0 0 0 Sep 02 09:33 foo')
+
+
+
+class StdioClientTests(TestCase):
+ """
+ Tests for L{cftp.StdioClient}.
+ """
+ def setUp(self):
+ """
+ Create a L{cftp.StdioClient} hooked up to dummy transport and a fake
+ user database.
+ """
+ class Connection:
+ pass
+
+ conn = Connection()
+ conn.transport = StringTransport()
+ conn.transport.localClosed = False
+
+ self.client = cftp.StdioClient(conn)
+ self.database = self.client._pwd = UserDatabase()
+
+ # Intentionally bypassing makeConnection - that triggers some code
+ # which uses features not provided by our dumb Connection fake.
+ self.client.transport = StringTransport()
+
+
+ def test_exec(self):
+ """
+ The I{exec} command runs its arguments locally in a child process
+ using the user's shell.
+ """
+ self.database.addUser(
+ getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar',
+ sys.executable)
+
+ d = self.client._dispatchCommand("exec print 1 + 2")
+ d.addCallback(self.assertEqual, "3\n")
+ return d
+
+
+ def test_execWithoutShell(self):
+ """
+ If the local user has no shell, the I{exec} command runs its arguments
+ using I{/bin/sh}.
+ """
+ self.database.addUser(
+ getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar', '')
+
+ d = self.client._dispatchCommand("exec echo hello")
+ d.addCallback(self.assertEqual, "hello\n")
+ return d
+
+
+ def test_bang(self):
+ """
+ The I{exec} command is run for lines which start with C{"!"}.
+ """
+ self.database.addUser(
+ getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar',
+ '/bin/sh')
+
+ d = self.client._dispatchCommand("!echo hello")
+ d.addCallback(self.assertEqual, "hello\n")
+ return d
+
+
+ def setKnownConsoleSize(self, width, height):
+ """
+ For the duration of this test, patch C{cftp}'s C{fcntl} module to return
+ a fixed width and height.
+
+ @param width: the width in characters
+ @type width: C{int}
+ @param height: the height in characters
+ @type height: C{int}
+ """
+ import tty # local import to avoid win32 issues
+ class FakeFcntl(object):
+ def ioctl(self, fd, opt, mutate):
+ if opt != tty.TIOCGWINSZ:
+ self.fail("Only window-size queries supported.")
+ return struct.pack("4H", height, width, 0, 0)
+ self.patch(cftp, "fcntl", FakeFcntl())
+
+
+ def test_progressReporting(self):
+ """
+ L{StdioClient._printProgressBar} prints a progress description,
+ including percent done, amount transferred, transfer rate, and time
+ remaining, all based the given start time, the given L{FileWrapper}'s
+ progress information and the reactor's current time.
+ """
+ # Use a short, known console width because this simple test doesn't need
+ # to test the console padding.
+ self.setKnownConsoleSize(10, 34)
+ clock = self.client.reactor = Clock()
+ wrapped = StringIO("x")
+ wrapped.name = "sample"
+ wrapper = cftp.FileWrapper(wrapped)
+ wrapper.size = 1024 * 10
+ startTime = clock.seconds()
+ clock.advance(2.0)
+ wrapper.total += 4096
+ self.client._printProgressBar(wrapper, startTime)
+ self.assertEqual(self.client.transport.value(),
+ "\rsample 40% 4.0kB 2.0kBps 00:03 ")
+
+
+ def test_reportNoProgress(self):
+ """
+ L{StdioClient._printProgressBar} prints a progress description that
+ indicates 0 bytes transferred if no bytes have been transferred and no
+ time has passed.
+ """
+ self.setKnownConsoleSize(10, 34)
+ clock = self.client.reactor = Clock()
+ wrapped = StringIO("x")
+ wrapped.name = "sample"
+ wrapper = cftp.FileWrapper(wrapped)
+ startTime = clock.seconds()
+ self.client._printProgressBar(wrapper, startTime)
+ self.assertEqual(self.client.transport.value(),
+ "\rsample 0% 0.0B 0.0Bps 00:00 ")
+
+
+
+class FileTransferTestRealm:
+ def __init__(self, testDir):
+ self.testDir = testDir
+
+ def requestAvatar(self, avatarID, mind, *interfaces):
+ a = FileTransferTestAvatar(self.testDir)
+ return interfaces[0], a, lambda: None
+
+
+class SFTPTestProcess(protocol.ProcessProtocol):
+ """
+ Protocol for testing cftp. Provides an interface between Python (where all
+ the tests are) and the cftp client process (which does the work that is
+ being tested).
+ """
+
+ def __init__(self, onOutReceived):
+ """
+ @param onOutReceived: A L{Deferred} to be fired as soon as data is
+ received from stdout.
+ """
+ self.clearBuffer()
+ self.onOutReceived = onOutReceived
+ self.onProcessEnd = None
+ self._expectingCommand = None
+ self._processEnded = False
+
+ def clearBuffer(self):
+ """
+ Clear any buffered data received from stdout. Should be private.
+ """
+ self.buffer = ''
+ self._linesReceived = []
+ self._lineBuffer = ''
+
+ def outReceived(self, data):
+ """
+ Called by Twisted when the cftp client prints data to stdout.
+ """
+ log.msg('got %s' % data)
+ lines = (self._lineBuffer + data).split('\n')
+ self._lineBuffer = lines.pop(-1)
+ self._linesReceived.extend(lines)
+ # XXX - not strictly correct.
+ # We really want onOutReceived to fire after the first 'cftp>' prompt
+ # has been received. (See use in TestOurServerCmdLineClient.setUp)
+ if self.onOutReceived is not None:
+ d, self.onOutReceived = self.onOutReceived, None
+ d.callback(data)
+ self.buffer += data
+ self._checkForCommand()
+
+ def _checkForCommand(self):
+ prompt = 'cftp> '
+ if self._expectingCommand and self._lineBuffer == prompt:
+ buf = '\n'.join(self._linesReceived)
+ if buf.startswith(prompt):
+ buf = buf[len(prompt):]
+ self.clearBuffer()
+ d, self._expectingCommand = self._expectingCommand, None
+ d.callback(buf)
+
+ def errReceived(self, data):
+ """
+ Called by Twisted when the cftp client prints data to stderr.
+ """
+ log.msg('err: %s' % data)
+
+ def getBuffer(self):
+ """
+ Return the contents of the buffer of data received from stdout.
+ """
+ return self.buffer
+
+ def runCommand(self, command):
+ """
+ Issue the given command via the cftp client. Return a C{Deferred} that
+ fires when the server returns a result. Note that the C{Deferred} will
+ callback even if the server returns some kind of error.
+
+ @param command: A string containing an sftp command.
+
+ @return: A C{Deferred} that fires when the sftp server returns a
+ result. The payload is the server's response string.
+ """
+ self._expectingCommand = defer.Deferred()
+ self.clearBuffer()
+ self.transport.write(command + '\n')
+ return self._expectingCommand
+
+ def runScript(self, commands):
+ """
+ Run each command in sequence and return a Deferred that fires when all
+ commands are completed.
+
+ @param commands: A list of strings containing sftp commands.
+
+ @return: A C{Deferred} that fires when all commands are completed. The
+ payload is a list of response strings from the server, in the same
+ order as the commands.
+ """
+ sem = defer.DeferredSemaphore(1)
+ dl = [sem.run(self.runCommand, command) for command in commands]
+ return defer.gatherResults(dl)
+
+ def killProcess(self):
+ """
+ Kill the process if it is still running.
+
+ If the process is still running, sends a KILL signal to the transport
+ and returns a C{Deferred} which fires when L{processEnded} is called.
+
+ @return: a C{Deferred}.
+ """
+ if self._processEnded:
+ return defer.succeed(None)
+ self.onProcessEnd = defer.Deferred()
+ self.transport.signalProcess('KILL')
+ return self.onProcessEnd
+
+ def processEnded(self, reason):
+ """
+ Called by Twisted when the cftp client process ends.
+ """
+ self._processEnded = True
+ if self.onProcessEnd:
+ d, self.onProcessEnd = self.onProcessEnd, None
+ d.callback(None)
+
+
+class CFTPClientTestBase(SFTPTestBase):
+ def setUp(self):
+ f = open('dsa_test.pub','w')
+ f.write(test_ssh.publicDSA_openssh)
+ f.close()
+ f = open('dsa_test','w')
+ f.write(test_ssh.privateDSA_openssh)
+ f.close()
+ os.chmod('dsa_test', 33152)
+ f = open('kh_test','w')
+ f.write('127.0.0.1 ' + test_ssh.publicRSA_openssh)
+ f.close()
+ return SFTPTestBase.setUp(self)
+
+ def startServer(self):
+ realm = FileTransferTestRealm(self.testDir)
+ p = portal.Portal(realm)
+ p.registerChecker(test_ssh.ConchTestPublicKeyChecker())
+ fac = test_ssh.ConchTestServerFactory()
+ fac.portal = p
+ self.server = reactor.listenTCP(0, fac, interface="127.0.0.1")
+
+ def stopServer(self):
+ if not hasattr(self.server.factory, 'proto'):
+ return self._cbStopServer(None)
+ self.server.factory.proto.expectedLoseConnection = 1
+ d = defer.maybeDeferred(
+ self.server.factory.proto.transport.loseConnection)
+ d.addCallback(self._cbStopServer)
+ return d
+
+ def _cbStopServer(self, ignored):
+ return defer.maybeDeferred(self.server.stopListening)
+
+ def tearDown(self):
+ for f in ['dsa_test.pub', 'dsa_test', 'kh_test']:
+ try:
+ os.remove(f)
+ except:
+ pass
+ return SFTPTestBase.tearDown(self)
+
+
+
+class TestOurServerCmdLineClient(CFTPClientTestBase):
+
+ def setUp(self):
+ CFTPClientTestBase.setUp(self)
+
+ self.startServer()
+ cmds = ('-p %i -l testuser '
+ '--known-hosts kh_test '
+ '--user-authentications publickey '
+ '--host-key-algorithms ssh-rsa '
+ '-i dsa_test '
+ '-a '
+ '-v '
+ '127.0.0.1')
+ port = self.server.getHost().port
+ cmds = test_conch._makeArgs((cmds % port).split(), mod='cftp')
+ log.msg('running %s %s' % (sys.executable, cmds))
+ d = defer.Deferred()
+ self.processProtocol = SFTPTestProcess(d)
+ d.addCallback(lambda _: self.processProtocol.clearBuffer())
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
+ reactor.spawnProcess(self.processProtocol, sys.executable, cmds,
+ env=env)
+ return d
+
+ def tearDown(self):
+ d = self.stopServer()
+ d.addCallback(lambda _: self.processProtocol.killProcess())
+ return d
+
+ def _killProcess(self, ignored):
+ try:
+ self.processProtocol.transport.signalProcess('KILL')
+ except error.ProcessExitedAlready:
+ pass
+
+ def runCommand(self, command):
+ """
+ Run the given command with the cftp client. Return a C{Deferred} that
+ fires when the command is complete. Payload is the server's output for
+ that command.
+ """
+ return self.processProtocol.runCommand(command)
+
+ def runScript(self, *commands):
+ """
+ Run the given commands with the cftp client. Returns a C{Deferred}
+ that fires when the commands are all complete. The C{Deferred}'s
+ payload is a list of output for each command.
+ """
+ return self.processProtocol.runScript(commands)
+
+ def testCdPwd(self):
+ """
+ Test that 'pwd' reports the current remote directory, that 'lpwd'
+ reports the current local directory, and that changing to a
+ subdirectory then changing to its parent leaves you in the original
+ remote directory.
+ """
+ # XXX - not actually a unit test, see docstring.
+ homeDir = os.path.join(os.getcwd(), self.testDir)
+ d = self.runScript('pwd', 'lpwd', 'cd testDirectory', 'cd ..', 'pwd')
+ d.addCallback(lambda xs: xs[:3] + xs[4:])
+ d.addCallback(self.assertEqual,
+ [homeDir, os.getcwd(), '', homeDir])
+ return d
+
+ def testChAttrs(self):
+ """
+ Check that 'ls -l' output includes the access permissions and that
+ this output changes appropriately with 'chmod'.
+ """
+ def _check(results):
+ self.flushLoggedErrors()
+ self.assertTrue(results[0].startswith('-rw-r--r--'))
+ self.assertEqual(results[1], '')
+ self.assertTrue(results[2].startswith('----------'), results[2])
+ self.assertEqual(results[3], '')
+
+ d = self.runScript('ls -l testfile1', 'chmod 0 testfile1',
+ 'ls -l testfile1', 'chmod 644 testfile1')
+ return d.addCallback(_check)
+ # XXX test chgrp/own
+
+
+ def testList(self):
+ """
+ Check 'ls' works as expected. Checks for wildcards, hidden files,
+ listing directories and listing empty directories.
+ """
+ def _check(results):
+ self.assertEqual(results[0], ['testDirectory', 'testRemoveFile',
+ 'testRenameFile', 'testfile1'])
+ self.assertEqual(results[1], ['testDirectory', 'testRemoveFile',
+ 'testRenameFile', 'testfile1'])
+ self.assertEqual(results[2], ['testRemoveFile', 'testRenameFile'])
+ self.assertEqual(results[3], ['.testHiddenFile', 'testRemoveFile',
+ 'testRenameFile'])
+ self.assertEqual(results[4], [''])
+ d = self.runScript('ls', 'ls ../' + os.path.basename(self.testDir),
+ 'ls *File', 'ls -a *File', 'ls -l testDirectory')
+ d.addCallback(lambda xs: [x.split('\n') for x in xs])
+ return d.addCallback(_check)
+
+
+ def testHelp(self):
+ """
+ Check that running the '?' command returns help.
+ """
+ d = self.runCommand('?')
+ d.addCallback(self.assertEqual,
+ cftp.StdioClient(None).cmd_HELP('').strip())
+ return d
+
+ def assertFilesEqual(self, name1, name2, msg=None):
+ """
+ Assert that the files at C{name1} and C{name2} contain exactly the
+ same data.
+ """
+ f1 = file(name1).read()
+ f2 = file(name2).read()
+ self.assertEqual(f1, f2, msg)
+
+
+ def testGet(self):
+ """
+ Test that 'get' saves the remote file to the correct local location,
+ that the output of 'get' is correct and that 'rm' actually removes
+ the file.
+ """
+ # XXX - not actually a unit test
+ expectedOutput = ("Transferred %s/%s/testfile1 to %s/test file2"
+ % (os.getcwd(), self.testDir, self.testDir))
+ def _checkGet(result):
+ self.assertTrue(result.endswith(expectedOutput))
+ self.assertFilesEqual(self.testDir + '/testfile1',
+ self.testDir + '/test file2',
+ "get failed")
+ return self.runCommand('rm "test file2"')
+
+ d = self.runCommand('get testfile1 "%s/test file2"' % (self.testDir,))
+ d.addCallback(_checkGet)
+ d.addCallback(lambda _: self.failIf(
+ os.path.exists(self.testDir + '/test file2')))
+ return d
+
+
+ def testWildcardGet(self):
+ """
+ Test that 'get' works correctly when given wildcard parameters.
+ """
+ def _check(ignored):
+ self.assertFilesEqual(self.testDir + '/testRemoveFile',
+ 'testRemoveFile',
+ 'testRemoveFile get failed')
+ self.assertFilesEqual(self.testDir + '/testRenameFile',
+ 'testRenameFile',
+ 'testRenameFile get failed')
+
+ d = self.runCommand('get testR*')
+ return d.addCallback(_check)
+
+
+ def testPut(self):
+ """
+ Check that 'put' uploads files correctly and that they can be
+ successfully removed. Also check the output of the put command.
+ """
+ # XXX - not actually a unit test
+ expectedOutput = ('Transferred %s/testfile1 to %s/%s/test"file2'
+ % (self.testDir, os.getcwd(), self.testDir))
+ def _checkPut(result):
+ self.assertFilesEqual(self.testDir + '/testfile1',
+ self.testDir + '/test"file2')
+ self.failUnless(result.endswith(expectedOutput))
+ return self.runCommand('rm "test\\"file2"')
+
+ d = self.runCommand('put %s/testfile1 "test\\"file2"'
+ % (self.testDir,))
+ d.addCallback(_checkPut)
+ d.addCallback(lambda _: self.failIf(
+ os.path.exists(self.testDir + '/test"file2')))
+ return d
+
+
+ def test_putOverLongerFile(self):
+ """
+ Check that 'put' uploads files correctly when overwriting a longer
+ file.
+ """
+ # XXX - not actually a unit test
+ f = file(os.path.join(self.testDir, 'shorterFile'), 'w')
+ f.write("a")
+ f.close()
+ f = file(os.path.join(self.testDir, 'longerFile'), 'w')
+ f.write("bb")
+ f.close()
+ def _checkPut(result):
+ self.assertFilesEqual(self.testDir + '/shorterFile',
+ self.testDir + '/longerFile')
+
+ d = self.runCommand('put %s/shorterFile longerFile'
+ % (self.testDir,))
+ d.addCallback(_checkPut)
+ return d
+
+
+ def test_putMultipleOverLongerFile(self):
+ """
+ Check that 'put' uploads files correctly when overwriting a longer
+ file and you use a wildcard to specify the files to upload.
+ """
+ # XXX - not actually a unit test
+ os.mkdir(os.path.join(self.testDir, 'dir'))
+ f = file(os.path.join(self.testDir, 'dir', 'file'), 'w')
+ f.write("a")
+ f.close()
+ f = file(os.path.join(self.testDir, 'file'), 'w')
+ f.write("bb")
+ f.close()
+ def _checkPut(result):
+ self.assertFilesEqual(self.testDir + '/dir/file',
+ self.testDir + '/file')
+
+ d = self.runCommand('put %s/dir/*'
+ % (self.testDir,))
+ d.addCallback(_checkPut)
+ return d
+
+
+ def testWildcardPut(self):
+ """
+ What happens if you issue a 'put' command and include a wildcard (i.e.
+ '*') in parameter? Check that all files matching the wildcard are
+ uploaded to the correct directory.
+ """
+ def check(results):
+ self.assertEqual(results[0], '')
+ self.assertEqual(results[2], '')
+ self.assertFilesEqual(self.testDir + '/testRemoveFile',
+ self.testDir + '/../testRemoveFile',
+ 'testRemoveFile get failed')
+ self.assertFilesEqual(self.testDir + '/testRenameFile',
+ self.testDir + '/../testRenameFile',
+ 'testRenameFile get failed')
+
+ d = self.runScript('cd ..',
+ 'put %s/testR*' % (self.testDir,),
+ 'cd %s' % os.path.basename(self.testDir))
+ d.addCallback(check)
+ return d
+
+
+ def testLink(self):
+ """
+ Test that 'ln' creates a file which appears as a link in the output of
+ 'ls'. Check that removing the new file succeeds without output.
+ """
+ def _check(results):
+ self.flushLoggedErrors()
+ self.assertEqual(results[0], '')
+ self.assertTrue(results[1].startswith('l'), 'link failed')
+ return self.runCommand('rm testLink')
+
+ d = self.runScript('ln testLink testfile1', 'ls -l testLink')
+ d.addCallback(_check)
+ d.addCallback(self.assertEqual, '')
+ return d
+
+
+ def testRemoteDirectory(self):
+ """
+ Test that we can create and remove directories with the cftp client.
+ """
+ def _check(results):
+ self.assertEqual(results[0], '')
+ self.assertTrue(results[1].startswith('d'))
+ return self.runCommand('rmdir testMakeDirectory')
+
+ d = self.runScript('mkdir testMakeDirectory',
+ 'ls -l testMakeDirector?')
+ d.addCallback(_check)
+ d.addCallback(self.assertEqual, '')
+ return d
+
+
+ def test_existingRemoteDirectory(self):
+ """
+ Test that a C{mkdir} on an existing directory fails with the
+ appropriate error, and doesn't log an useless error server side.
+ """
+ def _check(results):
+ self.assertEqual(results[0], '')
+ self.assertEqual(results[1],
+ 'remote error 11: mkdir failed')
+
+ d = self.runScript('mkdir testMakeDirectory',
+ 'mkdir testMakeDirectory')
+ d.addCallback(_check)
+ return d
+
+
+ def testLocalDirectory(self):
+ """
+ Test that we can create a directory locally and remove it with the
+ cftp client. This test works because the 'remote' server is running
+ out of a local directory.
+ """
+ d = self.runCommand('lmkdir %s/testLocalDirectory' % (self.testDir,))
+ d.addCallback(self.assertEqual, '')
+ d.addCallback(lambda _: self.runCommand('rmdir testLocalDirectory'))
+ d.addCallback(self.assertEqual, '')
+ return d
+
+
+ def testRename(self):
+ """
+ Test that we can rename a file.
+ """
+ def _check(results):
+ self.assertEqual(results[0], '')
+ self.assertEqual(results[1], 'testfile2')
+ return self.runCommand('rename testfile2 testfile1')
+
+ d = self.runScript('rename testfile1 testfile2', 'ls testfile?')
+ d.addCallback(_check)
+ d.addCallback(self.assertEqual, '')
+ return d
+
+
+
+class TestOurServerBatchFile(CFTPClientTestBase):
+ def setUp(self):
+ CFTPClientTestBase.setUp(self)
+ self.startServer()
+
+ def tearDown(self):
+ CFTPClientTestBase.tearDown(self)
+ return self.stopServer()
+
+ def _getBatchOutput(self, f):
+ fn = self.mktemp()
+ open(fn, 'w').write(f)
+ port = self.server.getHost().port
+ cmds = ('-p %i -l testuser '
+ '--known-hosts kh_test '
+ '--user-authentications publickey '
+ '--host-key-algorithms ssh-rsa '
+ '-i dsa_test '
+ '-a '
+ '-v -b %s 127.0.0.1') % (port, fn)
+ cmds = test_conch._makeArgs(cmds.split(), mod='cftp')[1:]
+ log.msg('running %s %s' % (sys.executable, cmds))
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
+
+ self.server.factory.expectedLoseConnection = 1
+
+ d = getProcessOutputAndValue(sys.executable, cmds, env=env)
+
+ def _cleanup(res):
+ os.remove(fn)
+ return res
+
+ d.addCallback(lambda res: res[0])
+ d.addBoth(_cleanup)
+
+ return d
+
+ def testBatchFile(self):
+ """Test whether batch file function of cftp ('cftp -b batchfile').
+ This works by treating the file as a list of commands to be run.
+ """
+ cmds = """pwd
+ls
+exit
+"""
+ def _cbCheckResult(res):
+ res = res.split('\n')
+ log.msg('RES %s' % str(res))
+ self.failUnless(res[1].find(self.testDir) != -1, repr(res))
+ self.assertEqual(res[3:-2], ['testDirectory', 'testRemoveFile',
+ 'testRenameFile', 'testfile1'])
+
+ d = self._getBatchOutput(cmds)
+ d.addCallback(_cbCheckResult)
+ return d
+
+ def testError(self):
+ """Test that an error in the batch file stops running the batch.
+ """
+ cmds = """chown 0 missingFile
+pwd
+exit
+"""
+ def _cbCheckResult(res):
+ self.failIf(res.find(self.testDir) != -1)
+
+ d = self._getBatchOutput(cmds)
+ d.addCallback(_cbCheckResult)
+ return d
+
+ def testIgnoredError(self):
+ """Test that a minus sign '-' at the front of a line ignores
+ any errors.
+ """
+ cmds = """-chown 0 missingFile
+pwd
+exit
+"""
+ def _cbCheckResult(res):
+ self.failIf(res.find(self.testDir) == -1)
+
+ d = self._getBatchOutput(cmds)
+ d.addCallback(_cbCheckResult)
+ return d
+
+
+
+class TestOurServerSftpClient(CFTPClientTestBase):
+ """
+ Test the sftp server against sftp command line client.
+ """
+
+ def setUp(self):
+ CFTPClientTestBase.setUp(self)
+ return self.startServer()
+
+
+ def tearDown(self):
+ return self.stopServer()
+
+
+ def test_extendedAttributes(self):
+ """
+ Test the return of extended attributes by the server: the sftp client
+ should ignore them, but still be able to parse the response correctly.
+
+ This test is mainly here to check that
+ L{filetransfer.FILEXFER_ATTR_EXTENDED} has the correct value.
+ """
+ fn = self.mktemp()
+ open(fn, 'w').write("ls .\nexit")
+ port = self.server.getHost().port
+
+ oldGetAttr = FileTransferForTestAvatar._getAttrs
+ def _getAttrs(self, s):
+ attrs = oldGetAttr(self, s)
+ attrs["ext_foo"] = "bar"
+ return attrs
+
+ self.patch(FileTransferForTestAvatar, "_getAttrs", _getAttrs)
+
+ self.server.factory.expectedLoseConnection = True
+ cmds = ('-o', 'IdentityFile=dsa_test',
+ '-o', 'UserKnownHostsFile=kh_test',
+ '-o', 'HostKeyAlgorithms=ssh-rsa',
+ '-o', 'Port=%i' % (port,), '-b', fn, 'testuser@127.0.0.1')
+ d = getProcessOutputAndValue("sftp", cmds)
+ def check(result):
+ self.assertEqual(result[2], 0)
+ for i in ['testDirectory', 'testRemoveFile',
+ 'testRenameFile', 'testfile1']:
+ self.assertIn(i, result[0])
+ return d.addCallback(check)
+
+
+
+if unix is None or Crypto is None or pyasn1 is None or interfaces.IReactorProcess(reactor, None) is None:
+ if _reason is None:
+ _reason = "don't run w/o spawnProcess or PyCrypto or pyasn1"
+ TestOurServerCmdLineClient.skip = _reason
+ TestOurServerBatchFile.skip = _reason
+ TestOurServerSftpClient.skip = _reason
+ StdioClientTests.skip = _reason
+else:
+ from twisted.python.procutils import which
+ if not which('sftp'):
+ TestOurServerSftpClient.skip = "no sftp command-line client available"
diff --git a/twisted/conch/test/test_channel.py b/twisted/conch/test/test_channel.py
new file mode 100644
index 0000000..a46596d
--- /dev/null
+++ b/twisted/conch/test/test_channel.py
@@ -0,0 +1,279 @@
+# Copyright (C) 2007-2008 Twisted Matrix Laboratories
+# See LICENSE for details
+
+"""
+Test ssh/channel.py.
+"""
+from twisted.conch.ssh import channel
+from twisted.trial import unittest
+
+
+class MockTransport(object):
+ """
+ A mock Transport. All we use is the getPeer() and getHost() methods.
+ Channels implement the ITransport interface, and their getPeer() and
+ getHost() methods return ('SSH', <transport's getPeer/Host value>) so
+ we need to implement these methods so they have something to draw
+ from.
+ """
+ def getPeer(self):
+ return ('MockPeer',)
+
+ def getHost(self):
+ return ('MockHost',)
+
+
+class MockConnection(object):
+ """
+ A mock for twisted.conch.ssh.connection.SSHConnection. Record the data
+ that channels send, and when they try to close the connection.
+
+ @ivar data: a C{dict} mapping channel id #s to lists of data sent by that
+ channel.
+ @ivar extData: a C{dict} mapping channel id #s to lists of 2-tuples
+ (extended data type, data) sent by that channel.
+ @ivar closes: a C{dict} mapping channel id #s to True if that channel sent
+ a close message.
+ """
+ transport = MockTransport()
+
+ def __init__(self):
+ self.data = {}
+ self.extData = {}
+ self.closes = {}
+
+ def logPrefix(self):
+ """
+ Return our logging prefix.
+ """
+ return "MockConnection"
+
+ def sendData(self, channel, data):
+ """
+ Record the sent data.
+ """
+ self.data.setdefault(channel, []).append(data)
+
+ def sendExtendedData(self, channel, type, data):
+ """
+ Record the sent extended data.
+ """
+ self.extData.setdefault(channel, []).append((type, data))
+
+ def sendClose(self, channel):
+ """
+ Record that the channel sent a close message.
+ """
+ self.closes[channel] = True
+
+
+class ChannelTestCase(unittest.TestCase):
+
+ def setUp(self):
+ """
+ Initialize the channel. remoteMaxPacket is 10 so that data is able
+ to be sent (the default of 0 means no data is sent because no packets
+ are made).
+ """
+ self.conn = MockConnection()
+ self.channel = channel.SSHChannel(conn=self.conn,
+ remoteMaxPacket=10)
+ self.channel.name = 'channel'
+
+ def test_init(self):
+ """
+ Test that SSHChannel initializes correctly. localWindowSize defaults
+ to 131072 (2**17) and localMaxPacket to 32768 (2**15) as reasonable
+ defaults (what OpenSSH uses for those variables).
+
+ The values in the second set of assertions are meaningless; they serve
+ only to verify that the instance variables are assigned in the correct
+ order.
+ """
+ c = channel.SSHChannel(conn=self.conn)
+ self.assertEqual(c.localWindowSize, 131072)
+ self.assertEqual(c.localWindowLeft, 131072)
+ self.assertEqual(c.localMaxPacket, 32768)
+ self.assertEqual(c.remoteWindowLeft, 0)
+ self.assertEqual(c.remoteMaxPacket, 0)
+ self.assertEqual(c.conn, self.conn)
+ self.assertEqual(c.data, None)
+ self.assertEqual(c.avatar, None)
+
+ c2 = channel.SSHChannel(1, 2, 3, 4, 5, 6, 7)
+ self.assertEqual(c2.localWindowSize, 1)
+ self.assertEqual(c2.localWindowLeft, 1)
+ self.assertEqual(c2.localMaxPacket, 2)
+ self.assertEqual(c2.remoteWindowLeft, 3)
+ self.assertEqual(c2.remoteMaxPacket, 4)
+ self.assertEqual(c2.conn, 5)
+ self.assertEqual(c2.data, 6)
+ self.assertEqual(c2.avatar, 7)
+
+ def test_str(self):
+ """
+ Test that str(SSHChannel) works gives the channel name and local and
+ remote windows at a glance..
+ """
+ self.assertEqual(str(self.channel), '<SSHChannel channel (lw 131072 '
+ 'rw 0)>')
+
+ def test_logPrefix(self):
+ """
+ Test that SSHChannel.logPrefix gives the name of the channel, the
+ local channel ID and the underlying connection.
+ """
+ self.assertEqual(self.channel.logPrefix(), 'SSHChannel channel '
+ '(unknown) on MockConnection')
+
+ def test_addWindowBytes(self):
+ """
+ Test that addWindowBytes adds bytes to the window and resumes writing
+ if it was paused.
+ """
+ cb = [False]
+ def stubStartWriting():
+ cb[0] = True
+ self.channel.startWriting = stubStartWriting
+ self.channel.write('test')
+ self.channel.writeExtended(1, 'test')
+ self.channel.addWindowBytes(50)
+ self.assertEqual(self.channel.remoteWindowLeft, 50 - 4 - 4)
+ self.assertTrue(self.channel.areWriting)
+ self.assertTrue(cb[0])
+ self.assertEqual(self.channel.buf, '')
+ self.assertEqual(self.conn.data[self.channel], ['test'])
+ self.assertEqual(self.channel.extBuf, [])
+ self.assertEqual(self.conn.extData[self.channel], [(1, 'test')])
+
+ cb[0] = False
+ self.channel.addWindowBytes(20)
+ self.assertFalse(cb[0])
+
+ self.channel.write('a'*80)
+ self.channel.loseConnection()
+ self.channel.addWindowBytes(20)
+ self.assertFalse(cb[0])
+
+ def test_requestReceived(self):
+ """
+ Test that requestReceived handles requests by dispatching them to
+ request_* methods.
+ """
+ self.channel.request_test_method = lambda data: data == ''
+ self.assertTrue(self.channel.requestReceived('test-method', ''))
+ self.assertFalse(self.channel.requestReceived('test-method', 'a'))
+ self.assertFalse(self.channel.requestReceived('bad-method', ''))
+
+ def test_closeReceieved(self):
+ """
+ Test that the default closeReceieved closes the connection.
+ """
+ self.assertFalse(self.channel.closing)
+ self.channel.closeReceived()
+ self.assertTrue(self.channel.closing)
+
+ def test_write(self):
+ """
+ Test that write handles data correctly. Send data up to the size
+ of the remote window, splitting the data into packets of length
+ remoteMaxPacket.
+ """
+ cb = [False]
+ def stubStopWriting():
+ cb[0] = True
+ # no window to start with
+ self.channel.stopWriting = stubStopWriting
+ self.channel.write('d')
+ self.channel.write('a')
+ self.assertFalse(self.channel.areWriting)
+ self.assertTrue(cb[0])
+ # regular write
+ self.channel.addWindowBytes(20)
+ self.channel.write('ta')
+ data = self.conn.data[self.channel]
+ self.assertEqual(data, ['da', 'ta'])
+ self.assertEqual(self.channel.remoteWindowLeft, 16)
+ # larger than max packet
+ self.channel.write('12345678901')
+ self.assertEqual(data, ['da', 'ta', '1234567890', '1'])
+ self.assertEqual(self.channel.remoteWindowLeft, 5)
+ # running out of window
+ cb[0] = False
+ self.channel.write('123456')
+ self.assertFalse(self.channel.areWriting)
+ self.assertTrue(cb[0])
+ self.assertEqual(data, ['da', 'ta', '1234567890', '1', '12345'])
+ self.assertEqual(self.channel.buf, '6')
+ self.assertEqual(self.channel.remoteWindowLeft, 0)
+
+ def test_writeExtended(self):
+ """
+ Test that writeExtended handles data correctly. Send extended data
+ up to the size of the window, splitting the extended data into packets
+ of length remoteMaxPacket.
+ """
+ cb = [False]
+ def stubStopWriting():
+ cb[0] = True
+ # no window to start with
+ self.channel.stopWriting = stubStopWriting
+ self.channel.writeExtended(1, 'd')
+ self.channel.writeExtended(1, 'a')
+ self.channel.writeExtended(2, 't')
+ self.assertFalse(self.channel.areWriting)
+ self.assertTrue(cb[0])
+ # regular write
+ self.channel.addWindowBytes(20)
+ self.channel.writeExtended(2, 'a')
+ data = self.conn.extData[self.channel]
+ self.assertEqual(data, [(1, 'da'), (2, 't'), (2, 'a')])
+ self.assertEqual(self.channel.remoteWindowLeft, 16)
+ # larger than max packet
+ self.channel.writeExtended(3, '12345678901')
+ self.assertEqual(data, [(1, 'da'), (2, 't'), (2, 'a'),
+ (3, '1234567890'), (3, '1')])
+ self.assertEqual(self.channel.remoteWindowLeft, 5)
+ # running out of window
+ cb[0] = False
+ self.channel.writeExtended(4, '123456')
+ self.assertFalse(self.channel.areWriting)
+ self.assertTrue(cb[0])
+ self.assertEqual(data, [(1, 'da'), (2, 't'), (2, 'a'),
+ (3, '1234567890'), (3, '1'), (4, '12345')])
+ self.assertEqual(self.channel.extBuf, [[4, '6']])
+ self.assertEqual(self.channel.remoteWindowLeft, 0)
+
+ def test_writeSequence(self):
+ """
+ Test that writeSequence is equivalent to write(''.join(sequece)).
+ """
+ self.channel.addWindowBytes(20)
+ self.channel.writeSequence(map(str, range(10)))
+ self.assertEqual(self.conn.data[self.channel], ['0123456789'])
+
+ def test_loseConnection(self):
+ """
+ Tesyt that loseConnection() doesn't close the channel until all
+ the data is sent.
+ """
+ self.channel.write('data')
+ self.channel.writeExtended(1, 'datadata')
+ self.channel.loseConnection()
+ self.assertEqual(self.conn.closes.get(self.channel), None)
+ self.channel.addWindowBytes(4) # send regular data
+ self.assertEqual(self.conn.closes.get(self.channel), None)
+ self.channel.addWindowBytes(8) # send extended data
+ self.assertTrue(self.conn.closes.get(self.channel))
+
+ def test_getPeer(self):
+ """
+ Test that getPeer() returns ('SSH', <connection transport peer>).
+ """
+ self.assertEqual(self.channel.getPeer(), ('SSH', 'MockPeer'))
+
+ def test_getHost(self):
+ """
+ Test that getHost() returns ('SSH', <connection transport host>).
+ """
+ self.assertEqual(self.channel.getHost(), ('SSH', 'MockHost'))
diff --git a/twisted/conch/test/test_checkers.py b/twisted/conch/test/test_checkers.py
new file mode 100644
index 0000000..9c85050
--- /dev/null
+++ b/twisted/conch/test/test_checkers.py
@@ -0,0 +1,609 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.checkers}.
+"""
+
+try:
+ import crypt
+except ImportError:
+ cryptSkip = 'cannot run without crypt module'
+else:
+ cryptSkip = None
+
+import os, base64
+
+from twisted.python import util
+from twisted.python.failure import Failure
+from twisted.trial.unittest import TestCase
+from twisted.python.filepath import FilePath
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.credentials import UsernamePassword, IUsernamePassword, \
+ SSHPrivateKey, ISSHPrivateKey
+from twisted.cred.error import UnhandledCredentials, UnauthorizedLogin
+from twisted.python.fakepwd import UserDatabase, ShadowDatabase
+from twisted.test.test_process import MockOS
+
+try:
+ import Crypto.Cipher.DES3
+ import pyasn1
+except ImportError:
+ dependencySkip = "can't run without Crypto and PyASN1"
+else:
+ dependencySkip = None
+ from twisted.conch.ssh import keys
+ from twisted.conch import checkers
+ from twisted.conch.error import NotEnoughAuthentication, ValidPublicKey
+ from twisted.conch.test import keydata
+
+if getattr(os, 'geteuid', None) is None:
+ euidSkip = "Cannot run without effective UIDs (questionable)"
+else:
+ euidSkip = None
+
+
+class HelperTests(TestCase):
+ """
+ Tests for helper functions L{verifyCryptedPassword}, L{_pwdGetByName} and
+ L{_shadowGetByName}.
+ """
+ skip = cryptSkip or dependencySkip
+
+ def setUp(self):
+ self.mockos = MockOS()
+
+
+ def test_verifyCryptedPassword(self):
+ """
+ L{verifyCryptedPassword} returns C{True} if the plaintext password
+ passed to it matches the encrypted password passed to it.
+ """
+ password = 'secret string'
+ salt = 'salty'
+ crypted = crypt.crypt(password, salt)
+ self.assertTrue(
+ checkers.verifyCryptedPassword(crypted, password),
+ '%r supposed to be valid encrypted password for %r' % (
+ crypted, password))
+
+
+ def test_verifyCryptedPasswordMD5(self):
+ """
+ L{verifyCryptedPassword} returns True if the provided cleartext password
+ matches the provided MD5 password hash.
+ """
+ password = 'password'
+ salt = '$1$salt'
+ crypted = crypt.crypt(password, salt)
+ self.assertTrue(
+ checkers.verifyCryptedPassword(crypted, password),
+ '%r supposed to be valid encrypted password for %s' % (
+ crypted, password))
+
+
+ def test_refuteCryptedPassword(self):
+ """
+ L{verifyCryptedPassword} returns C{False} if the plaintext password
+ passed to it does not match the encrypted password passed to it.
+ """
+ password = 'string secret'
+ wrong = 'secret string'
+ crypted = crypt.crypt(password, password)
+ self.assertFalse(
+ checkers.verifyCryptedPassword(crypted, wrong),
+ '%r not supposed to be valid encrypted password for %s' % (
+ crypted, wrong))
+
+
+ def test_pwdGetByName(self):
+ """
+ L{_pwdGetByName} returns a tuple of items from the UNIX /etc/passwd
+ database if the L{pwd} module is present.
+ """
+ userdb = UserDatabase()
+ userdb.addUser(
+ 'alice', 'secrit', 1, 2, 'first last', '/foo', '/bin/sh')
+ self.patch(checkers, 'pwd', userdb)
+ self.assertEquals(
+ checkers._pwdGetByName('alice'), userdb.getpwnam('alice'))
+
+
+ def test_pwdGetByNameWithoutPwd(self):
+ """
+ If the C{pwd} module isn't present, L{_pwdGetByName} returns C{None}.
+ """
+ self.patch(checkers, 'pwd', None)
+ self.assertIdentical(checkers._pwdGetByName('alice'), None)
+
+
+ def test_shadowGetByName(self):
+ """
+ L{_shadowGetByName} returns a tuple of items from the UNIX /etc/shadow
+ database if the L{spwd} is present.
+ """
+ userdb = ShadowDatabase()
+ userdb.addUser('bob', 'passphrase', 1, 2, 3, 4, 5, 6, 7)
+ self.patch(checkers, 'spwd', userdb)
+
+ self.mockos.euid = 2345
+ self.mockos.egid = 1234
+ self.patch(checkers, 'os', self.mockos)
+ self.patch(util, 'os', self.mockos)
+
+ self.assertEquals(
+ checkers._shadowGetByName('bob'), userdb.getspnam('bob'))
+ self.assertEquals(self.mockos.seteuidCalls, [0, 2345])
+ self.assertEquals(self.mockos.setegidCalls, [0, 1234])
+
+
+ def test_shadowGetByNameWithoutSpwd(self):
+ """
+ L{_shadowGetByName} uses the C{shadow} module to return a tuple of items
+ from the UNIX /etc/shadow database if the C{spwd} module is not present
+ and the C{shadow} module is.
+ """
+ userdb = ShadowDatabase()
+ userdb.addUser('bob', 'passphrase', 1, 2, 3, 4, 5, 6, 7)
+ self.patch(checkers, 'spwd', None)
+ self.patch(checkers, 'shadow', userdb)
+ self.patch(checkers, 'os', self.mockos)
+ self.patch(util, 'os', self.mockos)
+
+ self.mockos.euid = 2345
+ self.mockos.egid = 1234
+
+ self.assertEquals(
+ checkers._shadowGetByName('bob'), userdb.getspnam('bob'))
+ self.assertEquals(self.mockos.seteuidCalls, [0, 2345])
+ self.assertEquals(self.mockos.setegidCalls, [0, 1234])
+
+
+ def test_shadowGetByNameWithoutEither(self):
+ """
+ L{_shadowGetByName} returns C{None} if neither C{spwd} nor C{shadow} is
+ present.
+ """
+ self.patch(checkers, 'spwd', None)
+ self.patch(checkers, 'shadow', None)
+ self.patch(checkers, 'os', self.mockos)
+
+ self.assertIdentical(checkers._shadowGetByName('bob'), None)
+ self.assertEquals(self.mockos.seteuidCalls, [])
+ self.assertEquals(self.mockos.setegidCalls, [])
+
+
+
+class SSHPublicKeyDatabaseTestCase(TestCase):
+ """
+ Tests for L{SSHPublicKeyDatabase}.
+ """
+ skip = euidSkip or dependencySkip
+
+ def setUp(self):
+ self.checker = checkers.SSHPublicKeyDatabase()
+ self.key1 = base64.encodestring("foobar")
+ self.key2 = base64.encodestring("eggspam")
+ self.content = "t1 %s foo\nt2 %s egg\n" % (self.key1, self.key2)
+
+ self.mockos = MockOS()
+ self.mockos.path = FilePath(self.mktemp())
+ self.mockos.path.makedirs()
+ self.patch(checkers, 'os', self.mockos)
+ self.patch(util, 'os', self.mockos)
+ self.sshDir = self.mockos.path.child('.ssh')
+ self.sshDir.makedirs()
+
+ userdb = UserDatabase()
+ userdb.addUser(
+ 'user', 'password', 1, 2, 'first last',
+ self.mockos.path.path, '/bin/shell')
+ self.checker._userdb = userdb
+
+
+ def _testCheckKey(self, filename):
+ self.sshDir.child(filename).setContent(self.content)
+ user = UsernamePassword("user", "password")
+ user.blob = "foobar"
+ self.assertTrue(self.checker.checkKey(user))
+ user.blob = "eggspam"
+ self.assertTrue(self.checker.checkKey(user))
+ user.blob = "notallowed"
+ self.assertFalse(self.checker.checkKey(user))
+
+
+ def test_checkKey(self):
+ """
+ L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the
+ authorized_keys file and check the keys against that file.
+ """
+ self._testCheckKey("authorized_keys")
+ self.assertEqual(self.mockos.seteuidCalls, [])
+ self.assertEqual(self.mockos.setegidCalls, [])
+
+
+ def test_checkKey2(self):
+ """
+ L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the
+ authorized_keys2 file and check the keys against that file.
+ """
+ self._testCheckKey("authorized_keys2")
+ self.assertEqual(self.mockos.seteuidCalls, [])
+ self.assertEqual(self.mockos.setegidCalls, [])
+
+
+ def test_checkKeyAsRoot(self):
+ """
+ If the key file is readable, L{SSHPublicKeyDatabase.checkKey} should
+ switch its uid/gid to the ones of the authenticated user.
+ """
+ keyFile = self.sshDir.child("authorized_keys")
+ keyFile.setContent(self.content)
+ # Fake permission error by changing the mode
+ keyFile.chmod(0000)
+ self.addCleanup(keyFile.chmod, 0777)
+ # And restore the right mode when seteuid is called
+ savedSeteuid = self.mockos.seteuid
+ def seteuid(euid):
+ keyFile.chmod(0777)
+ return savedSeteuid(euid)
+ self.mockos.euid = 2345
+ self.mockos.egid = 1234
+ self.patch(self.mockos, "seteuid", seteuid)
+ self.patch(checkers, 'os', self.mockos)
+ self.patch(util, 'os', self.mockos)
+ user = UsernamePassword("user", "password")
+ user.blob = "foobar"
+ self.assertTrue(self.checker.checkKey(user))
+ self.assertEqual(self.mockos.seteuidCalls, [0, 1, 0, 2345])
+ self.assertEqual(self.mockos.setegidCalls, [2, 1234])
+
+
+ def test_requestAvatarId(self):
+ """
+ L{SSHPublicKeyDatabase.requestAvatarId} should return the avatar id
+ passed in if its C{_checkKey} method returns True.
+ """
+ def _checkKey(ignored):
+ return True
+ self.patch(self.checker, 'checkKey', _checkKey)
+ credentials = SSHPrivateKey(
+ 'test', 'ssh-rsa', keydata.publicRSA_openssh, 'foo',
+ keys.Key.fromString(keydata.privateRSA_openssh).sign('foo'))
+ d = self.checker.requestAvatarId(credentials)
+ def _verify(avatarId):
+ self.assertEqual(avatarId, 'test')
+ return d.addCallback(_verify)
+
+
+ def test_requestAvatarIdWithoutSignature(self):
+ """
+ L{SSHPublicKeyDatabase.requestAvatarId} should raise L{ValidPublicKey}
+ if the credentials represent a valid key without a signature. This
+ tells the user that the key is valid for login, but does not actually
+ allow that user to do so without a signature.
+ """
+ def _checkKey(ignored):
+ return True
+ self.patch(self.checker, 'checkKey', _checkKey)
+ credentials = SSHPrivateKey(
+ 'test', 'ssh-rsa', keydata.publicRSA_openssh, None, None)
+ d = self.checker.requestAvatarId(credentials)
+ return self.assertFailure(d, ValidPublicKey)
+
+
+ def test_requestAvatarIdInvalidKey(self):
+ """
+ If L{SSHPublicKeyDatabase.checkKey} returns False,
+ C{_cbRequestAvatarId} should raise L{UnauthorizedLogin}.
+ """
+ def _checkKey(ignored):
+ return False
+ self.patch(self.checker, 'checkKey', _checkKey)
+ d = self.checker.requestAvatarId(None);
+ return self.assertFailure(d, UnauthorizedLogin)
+
+
+ def test_requestAvatarIdInvalidSignature(self):
+ """
+ Valid keys with invalid signatures should cause
+ L{SSHPublicKeyDatabase.requestAvatarId} to return a {UnauthorizedLogin}
+ failure
+ """
+ def _checkKey(ignored):
+ return True
+ self.patch(self.checker, 'checkKey', _checkKey)
+ credentials = SSHPrivateKey(
+ 'test', 'ssh-rsa', keydata.publicRSA_openssh, 'foo',
+ keys.Key.fromString(keydata.privateDSA_openssh).sign('foo'))
+ d = self.checker.requestAvatarId(credentials)
+ return self.assertFailure(d, UnauthorizedLogin)
+
+
+ def test_requestAvatarIdNormalizeException(self):
+ """
+ Exceptions raised while verifying the key should be normalized into an
+ C{UnauthorizedLogin} failure.
+ """
+ def _checkKey(ignored):
+ return True
+ self.patch(self.checker, 'checkKey', _checkKey)
+ credentials = SSHPrivateKey('test', None, 'blob', 'sigData', 'sig')
+ d = self.checker.requestAvatarId(credentials)
+ def _verifyLoggedException(failure):
+ errors = self.flushLoggedErrors(keys.BadKeyError)
+ self.assertEqual(len(errors), 1)
+ return failure
+ d.addErrback(_verifyLoggedException)
+ return self.assertFailure(d, UnauthorizedLogin)
+
+
+
+class SSHProtocolCheckerTestCase(TestCase):
+ """
+ Tests for L{SSHProtocolChecker}.
+ """
+
+ skip = dependencySkip
+
+ def test_registerChecker(self):
+ """
+ L{SSHProcotolChecker.registerChecker} should add the given checker to
+ the list of registered checkers.
+ """
+ checker = checkers.SSHProtocolChecker()
+ self.assertEqual(checker.credentialInterfaces, [])
+ checker.registerChecker(checkers.SSHPublicKeyDatabase(), )
+ self.assertEqual(checker.credentialInterfaces, [ISSHPrivateKey])
+ self.assertIsInstance(checker.checkers[ISSHPrivateKey],
+ checkers.SSHPublicKeyDatabase)
+
+
+ def test_registerCheckerWithInterface(self):
+ """
+ If a apecific interface is passed into
+ L{SSHProtocolChecker.registerChecker}, that interface should be
+ registered instead of what the checker specifies in
+ credentialIntefaces.
+ """
+ checker = checkers.SSHProtocolChecker()
+ self.assertEqual(checker.credentialInterfaces, [])
+ checker.registerChecker(checkers.SSHPublicKeyDatabase(),
+ IUsernamePassword)
+ self.assertEqual(checker.credentialInterfaces, [IUsernamePassword])
+ self.assertIsInstance(checker.checkers[IUsernamePassword],
+ checkers.SSHPublicKeyDatabase)
+
+
+ def test_requestAvatarId(self):
+ """
+ L{SSHProtocolChecker.requestAvatarId} should defer to one if its
+ registered checkers to authenticate a user.
+ """
+ checker = checkers.SSHProtocolChecker()
+ passwordDatabase = InMemoryUsernamePasswordDatabaseDontUse()
+ passwordDatabase.addUser('test', 'test')
+ checker.registerChecker(passwordDatabase)
+ d = checker.requestAvatarId(UsernamePassword('test', 'test'))
+ def _callback(avatarId):
+ self.assertEqual(avatarId, 'test')
+ return d.addCallback(_callback)
+
+
+ def test_requestAvatarIdWithNotEnoughAuthentication(self):
+ """
+ If the client indicates that it is never satisfied, by always returning
+ False from _areDone, then L{SSHProtocolChecker} should raise
+ L{NotEnoughAuthentication}.
+ """
+ checker = checkers.SSHProtocolChecker()
+ def _areDone(avatarId):
+ return False
+ self.patch(checker, 'areDone', _areDone)
+
+ passwordDatabase = InMemoryUsernamePasswordDatabaseDontUse()
+ passwordDatabase.addUser('test', 'test')
+ checker.registerChecker(passwordDatabase)
+ d = checker.requestAvatarId(UsernamePassword('test', 'test'))
+ return self.assertFailure(d, NotEnoughAuthentication)
+
+
+ def test_requestAvatarIdInvalidCredential(self):
+ """
+ If the passed credentials aren't handled by any registered checker,
+ L{SSHProtocolChecker} should raise L{UnhandledCredentials}.
+ """
+ checker = checkers.SSHProtocolChecker()
+ d = checker.requestAvatarId(UsernamePassword('test', 'test'))
+ return self.assertFailure(d, UnhandledCredentials)
+
+
+ def test_areDone(self):
+ """
+ The default L{SSHProcotolChecker.areDone} should simply return True.
+ """
+ self.assertEquals(checkers.SSHProtocolChecker().areDone(None), True)
+
+
+
+class UNIXPasswordDatabaseTests(TestCase):
+ """
+ Tests for L{UNIXPasswordDatabase}.
+ """
+ skip = cryptSkip or dependencySkip
+
+ def assertLoggedIn(self, d, username):
+ """
+ Assert that the L{Deferred} passed in is called back with the value
+ 'username'. This represents a valid login for this TestCase.
+
+ NOTE: To work, this method's return value must be returned from the
+ test method, or otherwise hooked up to the test machinery.
+
+ @param d: a L{Deferred} from an L{IChecker.requestAvatarId} method.
+ @type d: L{Deferred}
+ @rtype: L{Deferred}
+ """
+ result = []
+ d.addBoth(result.append)
+ self.assertEquals(len(result), 1, "login incomplete")
+ if isinstance(result[0], Failure):
+ result[0].raiseException()
+ self.assertEquals(result[0], username)
+
+
+ def test_defaultCheckers(self):
+ """
+ L{UNIXPasswordDatabase} with no arguments has checks the C{pwd} database
+ and then the C{spwd} database.
+ """
+ checker = checkers.UNIXPasswordDatabase()
+
+ def crypted(username, password):
+ salt = crypt.crypt(password, username)
+ crypted = crypt.crypt(password, '$1$' + salt)
+ return crypted
+
+ pwd = UserDatabase()
+ pwd.addUser('alice', crypted('alice', 'password'),
+ 1, 2, 'foo', '/foo', '/bin/sh')
+ # x and * are convention for "look elsewhere for the password"
+ pwd.addUser('bob', 'x', 1, 2, 'bar', '/bar', '/bin/sh')
+ spwd = ShadowDatabase()
+ spwd.addUser('alice', 'wrong', 1, 2, 3, 4, 5, 6, 7)
+ spwd.addUser('bob', crypted('bob', 'password'),
+ 8, 9, 10, 11, 12, 13, 14)
+
+ self.patch(checkers, 'pwd', pwd)
+ self.patch(checkers, 'spwd', spwd)
+
+ mockos = MockOS()
+ self.patch(checkers, 'os', mockos)
+ self.patch(util, 'os', mockos)
+
+ mockos.euid = 2345
+ mockos.egid = 1234
+
+ cred = UsernamePassword("alice", "password")
+ self.assertLoggedIn(checker.requestAvatarId(cred), 'alice')
+ self.assertEquals(mockos.seteuidCalls, [])
+ self.assertEquals(mockos.setegidCalls, [])
+ cred.username = "bob"
+ self.assertLoggedIn(checker.requestAvatarId(cred), 'bob')
+ self.assertEquals(mockos.seteuidCalls, [0, 2345])
+ self.assertEquals(mockos.setegidCalls, [0, 1234])
+
+
+ def assertUnauthorizedLogin(self, d):
+ """
+ Asserts that the L{Deferred} passed in is erred back with an
+ L{UnauthorizedLogin} L{Failure}. This reprsents an invalid login for
+ this TestCase.
+
+ NOTE: To work, this method's return value must be returned from the
+ test method, or otherwise hooked up to the test machinery.
+
+ @param d: a L{Deferred} from an L{IChecker.requestAvatarId} method.
+ @type d: L{Deferred}
+ @rtype: L{None}
+ """
+ self.assertRaises(
+ checkers.UnauthorizedLogin, self.assertLoggedIn, d, 'bogus value')
+
+
+ def test_passInCheckers(self):
+ """
+ L{UNIXPasswordDatabase} takes a list of functions to check for UNIX
+ user information.
+ """
+ password = crypt.crypt('secret', 'secret')
+ userdb = UserDatabase()
+ userdb.addUser('anybody', password, 1, 2, 'foo', '/bar', '/bin/sh')
+ checker = checkers.UNIXPasswordDatabase([userdb.getpwnam])
+ self.assertLoggedIn(
+ checker.requestAvatarId(UsernamePassword('anybody', 'secret')),
+ 'anybody')
+
+
+ def test_verifyPassword(self):
+ """
+ If the encrypted password provided by the getpwnam function is valid
+ (verified by the L{verifyCryptedPassword} function), we callback the
+ C{requestAvatarId} L{Deferred} with the username.
+ """
+ def verifyCryptedPassword(crypted, pw):
+ return crypted == pw
+ def getpwnam(username):
+ return [username, username]
+ self.patch(checkers, 'verifyCryptedPassword', verifyCryptedPassword)
+ checker = checkers.UNIXPasswordDatabase([getpwnam])
+ credential = UsernamePassword('username', 'username')
+ self.assertLoggedIn(checker.requestAvatarId(credential), 'username')
+
+
+ def test_failOnKeyError(self):
+ """
+ If the getpwnam function raises a KeyError, the login fails with an
+ L{UnauthorizedLogin} exception.
+ """
+ def getpwnam(username):
+ raise KeyError(username)
+ checker = checkers.UNIXPasswordDatabase([getpwnam])
+ credential = UsernamePassword('username', 'username')
+ self.assertUnauthorizedLogin(checker.requestAvatarId(credential))
+
+
+ def test_failOnBadPassword(self):
+ """
+ If the verifyCryptedPassword function doesn't verify the password, the
+ login fails with an L{UnauthorizedLogin} exception.
+ """
+ def verifyCryptedPassword(crypted, pw):
+ return False
+ def getpwnam(username):
+ return [username, username]
+ self.patch(checkers, 'verifyCryptedPassword', verifyCryptedPassword)
+ checker = checkers.UNIXPasswordDatabase([getpwnam])
+ credential = UsernamePassword('username', 'username')
+ self.assertUnauthorizedLogin(checker.requestAvatarId(credential))
+
+
+ def test_loopThroughFunctions(self):
+ """
+ UNIXPasswordDatabase.requestAvatarId loops through each getpwnam
+ function associated with it and returns a L{Deferred} which fires with
+ the result of the first one which returns a value other than None.
+ ones do not verify the password.
+ """
+ def verifyCryptedPassword(crypted, pw):
+ return crypted == pw
+ def getpwnam1(username):
+ return [username, 'not the password']
+ def getpwnam2(username):
+ return [username, username]
+ self.patch(checkers, 'verifyCryptedPassword', verifyCryptedPassword)
+ checker = checkers.UNIXPasswordDatabase([getpwnam1, getpwnam2])
+ credential = UsernamePassword('username', 'username')
+ self.assertLoggedIn(checker.requestAvatarId(credential), 'username')
+
+
+ def test_failOnSpecial(self):
+ """
+ If the password returned by any function is C{""}, C{"x"}, or C{"*"} it
+ is not compared against the supplied password. Instead it is skipped.
+ """
+ pwd = UserDatabase()
+ pwd.addUser('alice', '', 1, 2, '', 'foo', 'bar')
+ pwd.addUser('bob', 'x', 1, 2, '', 'foo', 'bar')
+ pwd.addUser('carol', '*', 1, 2, '', 'foo', 'bar')
+ self.patch(checkers, 'pwd', pwd)
+
+ checker = checkers.UNIXPasswordDatabase([checkers._pwdGetByName])
+ cred = UsernamePassword('alice', '')
+ self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
+
+ cred = UsernamePassword('bob', 'x')
+ self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
+
+ cred = UsernamePassword('carol', '*')
+ self.assertUnauthorizedLogin(checker.requestAvatarId(cred))
diff --git a/twisted/conch/test/test_ckeygen.py b/twisted/conch/test/test_ckeygen.py
new file mode 100644
index 0000000..df437e2
--- /dev/null
+++ b/twisted/conch/test/test_ckeygen.py
@@ -0,0 +1,80 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.scripts.ckeygen}.
+"""
+
+import sys
+from StringIO import StringIO
+
+try:
+ import Crypto
+ import pyasn1
+except ImportError:
+ skip = "PyCrypto and pyasn1 required for twisted.conch.scripts.ckeygen."
+else:
+ from twisted.conch.ssh.keys import Key
+ from twisted.conch.scripts.ckeygen import printFingerprint, _saveKey
+
+from twisted.python.filepath import FilePath
+from twisted.trial.unittest import TestCase
+from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
+
+
+
+class KeyGenTests(TestCase):
+ """
+ Tests for various functions used to implement the I{ckeygen} script.
+ """
+ def setUp(self):
+ """
+ Patch C{sys.stdout} with a L{StringIO} instance to tests can make
+ assertions about what's printed.
+ """
+ self.stdout = StringIO()
+ self.patch(sys, 'stdout', self.stdout)
+
+
+ def test_printFingerprint(self):
+ """
+ L{printFingerprint} writes a line to standard out giving the number of
+ bits of the key, its fingerprint, and the basename of the file from it
+ was read.
+ """
+ filename = self.mktemp()
+ FilePath(filename).setContent(publicRSA_openssh)
+ printFingerprint({'filename': filename})
+ self.assertEqual(
+ self.stdout.getvalue(),
+ '768 3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af temp\n')
+
+
+ def test_saveKey(self):
+ """
+ L{_saveKey} writes the private and public parts of a key to two
+ different files and writes a report of this to standard out.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ filename = base.child('id_rsa').path
+ key = Key.fromString(privateRSA_openssh)
+ _saveKey(
+ key.keyObject,
+ {'filename': filename, 'pass': 'passphrase'})
+ self.assertEqual(
+ self.stdout.getvalue(),
+ "Your identification has been saved in %s\n"
+ "Your public key has been saved in %s.pub\n"
+ "The key fingerprint is:\n"
+ "3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af\n" % (
+ filename,
+ filename))
+ self.assertEqual(
+ key.fromString(
+ base.child('id_rsa').getContent(), None, 'passphrase'),
+ key)
+ self.assertEqual(
+ Key.fromString(base.child('id_rsa.pub').getContent()),
+ key.public())
+
diff --git a/twisted/conch/test/test_conch.py b/twisted/conch/test/test_conch.py
new file mode 100644
index 0000000..95219d4
--- /dev/null
+++ b/twisted/conch/test/test_conch.py
@@ -0,0 +1,552 @@
+# -*- test-case-name: twisted.conch.test.test_conch -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import os, sys, socket
+from itertools import count
+
+from zope.interface import implements
+
+from twisted.cred import portal
+from twisted.internet import reactor, defer, protocol
+from twisted.internet.error import ProcessExitedAlready
+from twisted.internet.task import LoopingCall
+from twisted.python import log, runtime
+from twisted.trial import unittest
+from twisted.conch.error import ConchError
+from twisted.conch.avatar import ConchUser
+from twisted.conch.ssh.session import ISession, SSHSession, wrapProtocol
+
+try:
+ from twisted.conch.scripts.conch import SSHSession as StdioInteractingSession
+except ImportError, e:
+ StdioInteractingSession = None
+ _reason = str(e)
+ del e
+
+from twisted.conch.test.test_ssh import ConchTestRealm
+from twisted.python.procutils import which
+
+from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
+from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
+
+from twisted.conch.test.test_ssh import Crypto, pyasn1
+try:
+ from twisted.conch.test.test_ssh import ConchTestServerFactory, \
+ ConchTestPublicKeyChecker
+except ImportError:
+ pass
+
+
+
+class StdioInteractingSessionTests(unittest.TestCase):
+ """
+ Tests for L{twisted.conch.scripts.conch.SSHSession}.
+ """
+ if StdioInteractingSession is None:
+ skip = _reason
+
+ def test_eofReceived(self):
+ """
+ L{twisted.conch.scripts.conch.SSHSession.eofReceived} loses the
+ write half of its stdio connection.
+ """
+ class FakeStdio:
+ writeConnLost = False
+
+ def loseWriteConnection(self):
+ self.writeConnLost = True
+
+ stdio = FakeStdio()
+ channel = StdioInteractingSession()
+ channel.stdio = stdio
+ channel.eofReceived()
+ self.assertTrue(stdio.writeConnLost)
+
+
+
+class Echo(protocol.Protocol):
+ def connectionMade(self):
+ log.msg('ECHO CONNECTION MADE')
+
+
+ def connectionLost(self, reason):
+ log.msg('ECHO CONNECTION DONE')
+
+
+ def dataReceived(self, data):
+ self.transport.write(data)
+ if '\n' in data:
+ self.transport.loseConnection()
+
+
+
+class EchoFactory(protocol.Factory):
+ protocol = Echo
+
+
+
+class ConchTestOpenSSHProcess(protocol.ProcessProtocol):
+ """
+ Test protocol for launching an OpenSSH client process.
+
+ @ivar deferred: Set by whatever uses this object. Accessed using
+ L{_getDeferred}, which destroys the value so the Deferred is not
+ fired twice. Fires when the process is terminated.
+ """
+
+ deferred = None
+ buf = ''
+
+ def _getDeferred(self):
+ d, self.deferred = self.deferred, None
+ return d
+
+
+ def outReceived(self, data):
+ self.buf += data
+
+
+ def processEnded(self, reason):
+ """
+ Called when the process has ended.
+
+ @param reason: a Failure giving the reason for the process' end.
+ """
+ if reason.value.exitCode != 0:
+ self._getDeferred().errback(
+ ConchError("exit code was not 0: %s" %
+ reason.value.exitCode))
+ else:
+ buf = self.buf.replace('\r\n', '\n')
+ self._getDeferred().callback(buf)
+
+
+
+class ConchTestForwardingProcess(protocol.ProcessProtocol):
+ """
+ Manages a third-party process which launches a server.
+
+ Uses L{ConchTestForwardingPort} to connect to the third-party server.
+ Once L{ConchTestForwardingPort} has disconnected, kill the process and fire
+ a Deferred with the data received by the L{ConchTestForwardingPort}.
+
+ @ivar deferred: Set by whatever uses this object. Accessed using
+ L{_getDeferred}, which destroys the value so the Deferred is not
+ fired twice. Fires when the process is terminated.
+ """
+
+ deferred = None
+
+ def __init__(self, port, data):
+ """
+ @type port: C{int}
+ @param port: The port on which the third-party server is listening.
+ (it is assumed that the server is running on localhost).
+
+ @type data: C{str}
+ @param data: This is sent to the third-party server. Must end with '\n'
+ in order to trigger a disconnect.
+ """
+ self.port = port
+ self.buffer = None
+ self.data = data
+
+
+ def _getDeferred(self):
+ d, self.deferred = self.deferred, None
+ return d
+
+
+ def connectionMade(self):
+ self._connect()
+
+
+ def _connect(self):
+ """
+ Connect to the server, which is often a third-party process.
+ Tries to reconnect if it fails because we have no way of determining
+ exactly when the port becomes available for listening -- we can only
+ know when the process starts.
+ """
+ cc = protocol.ClientCreator(reactor, ConchTestForwardingPort, self,
+ self.data)
+ d = cc.connectTCP('127.0.0.1', self.port)
+ d.addErrback(self._ebConnect)
+ return d
+
+
+ def _ebConnect(self, f):
+ reactor.callLater(.1, self._connect)
+
+
+ def forwardingPortDisconnected(self, buffer):
+ """
+ The network connection has died; save the buffer of output
+ from the network and attempt to quit the process gracefully,
+ and then (after the reactor has spun) send it a KILL signal.
+ """
+ self.buffer = buffer
+ self.transport.write('\x03')
+ self.transport.loseConnection()
+ reactor.callLater(0, self._reallyDie)
+
+
+ def _reallyDie(self):
+ try:
+ self.transport.signalProcess('KILL')
+ except ProcessExitedAlready:
+ pass
+
+
+ def processEnded(self, reason):
+ """
+ Fire the Deferred at self.deferred with the data collected
+ from the L{ConchTestForwardingPort} connection, if any.
+ """
+ self._getDeferred().callback(self.buffer)
+
+
+
+class ConchTestForwardingPort(protocol.Protocol):
+ """
+ Connects to server launched by a third-party process (managed by
+ L{ConchTestForwardingProcess}) sends data, then reports whatever it
+ received back to the L{ConchTestForwardingProcess} once the connection
+ is ended.
+ """
+
+
+ def __init__(self, protocol, data):
+ """
+ @type protocol: L{ConchTestForwardingProcess}
+ @param protocol: The L{ProcessProtocol} which made this connection.
+
+ @type data: str
+ @param data: The data to be sent to the third-party server.
+ """
+ self.protocol = protocol
+ self.data = data
+
+
+ def connectionMade(self):
+ self.buffer = ''
+ self.transport.write(self.data)
+
+
+ def dataReceived(self, data):
+ self.buffer += data
+
+
+ def connectionLost(self, reason):
+ self.protocol.forwardingPortDisconnected(self.buffer)
+
+
+
+def _makeArgs(args, mod="conch"):
+ start = [sys.executable, '-c'
+"""
+### Twisted Preamble
+import sys, os
+path = os.path.abspath(sys.argv[0])
+while os.path.dirname(path) != path:
+ if os.path.basename(path).startswith('Twisted'):
+ sys.path.insert(0, path)
+ break
+ path = os.path.dirname(path)
+
+from twisted.conch.scripts.%s import run
+run()""" % mod]
+ return start + list(args)
+
+
+
+class ConchServerSetupMixin:
+ if not Crypto:
+ skip = "can't run w/o PyCrypto"
+
+ if not pyasn1:
+ skip = "Cannot run without PyASN1"
+
+ realmFactory = staticmethod(lambda: ConchTestRealm('testuser'))
+
+ def _createFiles(self):
+ for f in ['rsa_test','rsa_test.pub','dsa_test','dsa_test.pub',
+ 'kh_test']:
+ if os.path.exists(f):
+ os.remove(f)
+ open('rsa_test','w').write(privateRSA_openssh)
+ open('rsa_test.pub','w').write(publicRSA_openssh)
+ open('dsa_test.pub','w').write(publicDSA_openssh)
+ open('dsa_test','w').write(privateDSA_openssh)
+ os.chmod('dsa_test', 33152)
+ os.chmod('rsa_test', 33152)
+ open('kh_test','w').write('127.0.0.1 '+publicRSA_openssh)
+
+
+ def _getFreePort(self):
+ s = socket.socket()
+ s.bind(('', 0))
+ port = s.getsockname()[1]
+ s.close()
+ return port
+
+
+ def _makeConchFactory(self):
+ """
+ Make a L{ConchTestServerFactory}, which allows us to start a
+ L{ConchTestServer} -- i.e. an actually listening conch.
+ """
+ realm = self.realmFactory()
+ p = portal.Portal(realm)
+ p.registerChecker(ConchTestPublicKeyChecker())
+ factory = ConchTestServerFactory()
+ factory.portal = p
+ return factory
+
+
+ def setUp(self):
+ self._createFiles()
+ self.conchFactory = self._makeConchFactory()
+ self.conchFactory.expectedLoseConnection = 1
+ self.conchServer = reactor.listenTCP(0, self.conchFactory,
+ interface="127.0.0.1")
+ self.echoServer = reactor.listenTCP(0, EchoFactory())
+ self.echoPort = self.echoServer.getHost().port
+
+
+ def tearDown(self):
+ try:
+ self.conchFactory.proto.done = 1
+ except AttributeError:
+ pass
+ else:
+ self.conchFactory.proto.transport.loseConnection()
+ return defer.gatherResults([
+ defer.maybeDeferred(self.conchServer.stopListening),
+ defer.maybeDeferred(self.echoServer.stopListening)])
+
+
+
+class ForwardingMixin(ConchServerSetupMixin):
+ """
+ Template class for tests of the Conch server's ability to forward arbitrary
+ protocols over SSH.
+
+ These tests are integration tests, not unit tests. They launch a Conch
+ server, a custom TCP server (just an L{EchoProtocol}) and then call
+ L{execute}.
+
+ L{execute} is implemented by subclasses of L{ForwardingMixin}. It should
+ cause an SSH client to connect to the Conch server, asking it to forward
+ data to the custom TCP server.
+ """
+
+ def test_exec(self):
+ """
+ Test that we can use whatever client to send the command "echo goodbye"
+ to the Conch server. Make sure we receive "goodbye" back from the
+ server.
+ """
+ d = self.execute('echo goodbye', ConchTestOpenSSHProcess())
+ return d.addCallback(self.assertEqual, 'goodbye\n')
+
+
+ def test_localToRemoteForwarding(self):
+ """
+ Test that we can use whatever client to forward a local port to a
+ specified port on the server.
+ """
+ localPort = self._getFreePort()
+ process = ConchTestForwardingProcess(localPort, 'test\n')
+ d = self.execute('', process,
+ sshArgs='-N -L%i:127.0.0.1:%i'
+ % (localPort, self.echoPort))
+ d.addCallback(self.assertEqual, 'test\n')
+ return d
+
+
+ def test_remoteToLocalForwarding(self):
+ """
+ Test that we can use whatever client to forward a port from the server
+ to a port locally.
+ """
+ localPort = self._getFreePort()
+ process = ConchTestForwardingProcess(localPort, 'test\n')
+ d = self.execute('', process,
+ sshArgs='-N -R %i:127.0.0.1:%i'
+ % (localPort, self.echoPort))
+ d.addCallback(self.assertEqual, 'test\n')
+ return d
+
+
+
+class RekeyAvatar(ConchUser):
+ """
+ This avatar implements a shell which sends 60 numbered lines to whatever
+ connects to it, then closes the session with a 0 exit status.
+
+ 60 lines is selected as being enough to send more than 2kB of traffic, the
+ amount the client is configured to initiate a rekey after.
+ """
+ # Conventionally there is a separate adapter object which provides ISession
+ # for the user, but making the user provide ISession directly works too.
+ # This isn't a full implementation of ISession though, just enough to make
+ # these tests pass.
+ implements(ISession)
+
+ def __init__(self):
+ ConchUser.__init__(self)
+ self.channelLookup['session'] = SSHSession
+
+
+ def openShell(self, transport):
+ """
+ Write 60 lines of data to the transport, then exit.
+ """
+ proto = protocol.Protocol()
+ proto.makeConnection(transport)
+ transport.makeConnection(wrapProtocol(proto))
+
+ # Send enough bytes to the connection so that a rekey is triggered in
+ # the client.
+ def write(counter):
+ i = counter()
+ if i == 60:
+ call.stop()
+ transport.session.conn.sendRequest(
+ transport.session, 'exit-status', '\x00\x00\x00\x00')
+ transport.loseConnection()
+ else:
+ transport.write("line #%02d\n" % (i,))
+
+ # The timing for this loop is an educated guess (and/or the result of
+ # experimentation) to exercise the case where a packet is generated
+ # mid-rekey. Since the other side of the connection is (so far) the
+ # OpenSSH command line client, there's no easy way to determine when the
+ # rekey has been initiated. If there were, then generating a packet
+ # immediately at that time would be a better way to test the
+ # functionality being tested here.
+ call = LoopingCall(write, count().next)
+ call.start(0.01)
+
+
+ def closed(self):
+ """
+ Ignore the close of the session.
+ """
+
+
+
+class RekeyRealm:
+ """
+ This realm gives out new L{RekeyAvatar} instances for any avatar request.
+ """
+ def requestAvatar(self, avatarID, mind, *interfaces):
+ return interfaces[0], RekeyAvatar(), lambda: None
+
+
+
+class RekeyTestsMixin(ConchServerSetupMixin):
+ """
+ TestCase mixin which defines tests exercising L{SSHTransportBase}'s handling
+ of rekeying messages.
+ """
+ realmFactory = RekeyRealm
+
+ def test_clientRekey(self):
+ """
+ After a client-initiated rekey is completed, application data continues
+ to be passed over the SSH connection.
+ """
+ process = ConchTestOpenSSHProcess()
+ d = self.execute("", process, '-o RekeyLimit=2K')
+ def finished(result):
+ self.assertEqual(
+ result,
+ '\n'.join(['line #%02d' % (i,) for i in range(60)]) + '\n')
+ d.addCallback(finished)
+ return d
+
+
+
+class OpenSSHClientMixin:
+ if not which('ssh'):
+ skip = "no ssh command-line client available"
+
+ def execute(self, remoteCommand, process, sshArgs=''):
+ """
+ Connects to the SSH server started in L{ConchServerSetupMixin.setUp} by
+ running the 'ssh' command line tool.
+
+ @type remoteCommand: str
+ @param remoteCommand: The command (with arguments) to run on the
+ remote end.
+
+ @type process: L{ConchTestOpenSSHProcess}
+
+ @type sshArgs: str
+ @param sshArgs: Arguments to pass to the 'ssh' process.
+
+ @return: L{defer.Deferred}
+ """
+ process.deferred = defer.Deferred()
+ cmdline = ('ssh -2 -l testuser -p %i '
+ '-oUserKnownHostsFile=kh_test '
+ '-oPasswordAuthentication=no '
+ # Always use the RSA key, since that's the one in kh_test.
+ '-oHostKeyAlgorithms=ssh-rsa '
+ '-a '
+ '-i dsa_test ') + sshArgs + \
+ ' 127.0.0.1 ' + remoteCommand
+ port = self.conchServer.getHost().port
+ cmds = (cmdline % port).split()
+ reactor.spawnProcess(process, "ssh", cmds)
+ return process.deferred
+
+
+
+class OpenSSHClientForwardingTestCase(ForwardingMixin, OpenSSHClientMixin,
+ unittest.TestCase):
+ """
+ Connection forwarding tests run against the OpenSSL command line client.
+ """
+
+
+
+class OpenSSHClientRekeyTestCase(RekeyTestsMixin, OpenSSHClientMixin,
+ unittest.TestCase):
+ """
+ Rekeying tests run against the OpenSSL command line client.
+ """
+
+
+
+class CmdLineClientTestCase(ForwardingMixin, unittest.TestCase):
+ """
+ Connection forwarding tests run against the Conch command line client.
+ """
+ if runtime.platformType == 'win32':
+ skip = "can't run cmdline client on win32"
+
+ def execute(self, remoteCommand, process, sshArgs=''):
+ """
+ As for L{OpenSSHClientTestCase.execute}, except it runs the 'conch'
+ command line tool, not 'ssh'.
+ """
+ process.deferred = defer.Deferred()
+ port = self.conchServer.getHost().port
+ cmd = ('-p %i -l testuser '
+ '--known-hosts kh_test '
+ '--user-authentications publickey '
+ '--host-key-algorithms ssh-rsa '
+ '-a '
+ '-i dsa_test '
+ '-v ') % port + sshArgs + \
+ ' 127.0.0.1 ' + remoteCommand
+ cmds = _makeArgs(cmd.split())
+ log.msg(str(cmds))
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
+ reactor.spawnProcess(process, sys.executable, cmds, env=env)
+ return process.deferred
diff --git a/twisted/conch/test/test_connection.py b/twisted/conch/test/test_connection.py
new file mode 100644
index 0000000..85a8e6a
--- /dev/null
+++ b/twisted/conch/test/test_connection.py
@@ -0,0 +1,730 @@
+# Copyright (c) 2007-2010 Twisted Matrix Laboratories.
+# See LICENSE for details
+
+"""
+This module tests twisted.conch.ssh.connection.
+"""
+
+import struct
+
+from twisted.conch import error
+from twisted.conch.ssh import channel, common, connection
+from twisted.trial import unittest
+from twisted.conch.test import test_userauth
+
+
+class TestChannel(channel.SSHChannel):
+ """
+ A mocked-up version of twisted.conch.ssh.channel.SSHChannel.
+
+ @ivar gotOpen: True if channelOpen has been called.
+ @type gotOpen: C{bool}
+ @ivar specificData: the specific channel open data passed to channelOpen.
+ @type specificData: C{str}
+ @ivar openFailureReason: the reason passed to openFailed.
+ @type openFailed: C{error.ConchError}
+ @ivar inBuffer: a C{list} of strings received by the channel.
+ @type inBuffer: C{list}
+ @ivar extBuffer: a C{list} of 2-tuples (type, extended data) of received by
+ the channel.
+ @type extBuffer: C{list}
+ @ivar numberRequests: the number of requests that have been made to this
+ channel.
+ @type numberRequests: C{int}
+ @ivar gotEOF: True if the other side sent EOF.
+ @type gotEOF: C{bool}
+ @ivar gotOneClose: True if the other side closed the connection.
+ @type gotOneClose: C{bool}
+ @ivar gotClosed: True if the channel is closed.
+ @type gotClosed: C{bool}
+ """
+ name = "TestChannel"
+ gotOpen = False
+
+ def logPrefix(self):
+ return "TestChannel %i" % self.id
+
+ def channelOpen(self, specificData):
+ """
+ The channel is open. Set up the instance variables.
+ """
+ self.gotOpen = True
+ self.specificData = specificData
+ self.inBuffer = []
+ self.extBuffer = []
+ self.numberRequests = 0
+ self.gotEOF = False
+ self.gotOneClose = False
+ self.gotClosed = False
+
+ def openFailed(self, reason):
+ """
+ Opening the channel failed. Store the reason why.
+ """
+ self.openFailureReason = reason
+
+ def request_test(self, data):
+ """
+ A test request. Return True if data is 'data'.
+
+ @type data: C{str}
+ """
+ self.numberRequests += 1
+ return data == 'data'
+
+ def dataReceived(self, data):
+ """
+ Data was received. Store it in the buffer.
+ """
+ self.inBuffer.append(data)
+
+ def extReceived(self, code, data):
+ """
+ Extended data was received. Store it in the buffer.
+ """
+ self.extBuffer.append((code, data))
+
+ def eofReceived(self):
+ """
+ EOF was received. Remember it.
+ """
+ self.gotEOF = True
+
+ def closeReceived(self):
+ """
+ Close was received. Remember it.
+ """
+ self.gotOneClose = True
+
+ def closed(self):
+ """
+ The channel is closed. Rembember it.
+ """
+ self.gotClosed = True
+
+class TestAvatar:
+ """
+ A mocked-up version of twisted.conch.avatar.ConchUser
+ """
+ _ARGS_ERROR_CODE = 123
+
+ def lookupChannel(self, channelType, windowSize, maxPacket, data):
+ """
+ The server wants us to return a channel. If the requested channel is
+ our TestChannel, return it, otherwise return None.
+ """
+ if channelType == TestChannel.name:
+ return TestChannel(remoteWindow=windowSize,
+ remoteMaxPacket=maxPacket,
+ data=data, avatar=self)
+ elif channelType == "conch-error-args":
+ # Raise a ConchError with backwards arguments to make sure the
+ # connection fixes it for us. This case should be deprecated and
+ # deleted eventually, but only after all of Conch gets the argument
+ # order right.
+ raise error.ConchError(
+ self._ARGS_ERROR_CODE, "error args in wrong order")
+
+
+ def gotGlobalRequest(self, requestType, data):
+ """
+ The client has made a global request. If the global request is
+ 'TestGlobal', return True. If the global request is 'TestData',
+ return True and the request-specific data we received. Otherwise,
+ return False.
+ """
+ if requestType == 'TestGlobal':
+ return True
+ elif requestType == 'TestData':
+ return True, data
+ else:
+ return False
+
+
+
+class TestConnection(connection.SSHConnection):
+ """
+ A subclass of SSHConnection for testing.
+
+ @ivar channel: the current channel.
+ @type channel. C{TestChannel}
+ """
+
+ def logPrefix(self):
+ return "TestConnection"
+
+ def global_TestGlobal(self, data):
+ """
+ The other side made the 'TestGlobal' global request. Return True.
+ """
+ return True
+
+ def global_Test_Data(self, data):
+ """
+ The other side made the 'Test-Data' global request. Return True and
+ the data we received.
+ """
+ return True, data
+
+ def channel_TestChannel(self, windowSize, maxPacket, data):
+ """
+ The other side is requesting the TestChannel. Create a C{TestChannel}
+ instance, store it, and return it.
+ """
+ self.channel = TestChannel(remoteWindow=windowSize,
+ remoteMaxPacket=maxPacket, data=data)
+ return self.channel
+
+ def channel_ErrorChannel(self, windowSize, maxPacket, data):
+ """
+ The other side is requesting the ErrorChannel. Raise an exception.
+ """
+ raise AssertionError('no such thing')
+
+
+
+class ConnectionTestCase(unittest.TestCase):
+
+ if test_userauth.transport is None:
+ skip = "Cannot run without both PyCrypto and pyasn1"
+
+ def setUp(self):
+ self.transport = test_userauth.FakeTransport(None)
+ self.transport.avatar = TestAvatar()
+ self.conn = TestConnection()
+ self.conn.transport = self.transport
+ self.conn.serviceStarted()
+
+ def _openChannel(self, channel):
+ """
+ Open the channel with the default connection.
+ """
+ self.conn.openChannel(channel)
+ self.transport.packets = self.transport.packets[:-1]
+ self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(struct.pack('>2L',
+ channel.id, 255) + '\x00\x02\x00\x00\x00\x00\x80\x00')
+
+ def tearDown(self):
+ self.conn.serviceStopped()
+
+ def test_linkAvatar(self):
+ """
+ Test that the connection links itself to the avatar in the
+ transport.
+ """
+ self.assertIdentical(self.transport.avatar.conn, self.conn)
+
+ def test_serviceStopped(self):
+ """
+ Test that serviceStopped() closes any open channels.
+ """
+ channel1 = TestChannel()
+ channel2 = TestChannel()
+ self.conn.openChannel(channel1)
+ self.conn.openChannel(channel2)
+ self.conn.ssh_CHANNEL_OPEN_CONFIRMATION('\x00\x00\x00\x00' * 4)
+ self.assertTrue(channel1.gotOpen)
+ self.assertFalse(channel2.gotOpen)
+ self.conn.serviceStopped()
+ self.assertTrue(channel1.gotClosed)
+
+ def test_GLOBAL_REQUEST(self):
+ """
+ Test that global request packets are dispatched to the global_*
+ methods and the return values are translated into success or failure
+ messages.
+ """
+ self.conn.ssh_GLOBAL_REQUEST(common.NS('TestGlobal') + '\xff')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_REQUEST_SUCCESS, '')])
+ self.transport.packets = []
+ self.conn.ssh_GLOBAL_REQUEST(common.NS('TestData') + '\xff' +
+ 'test data')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_REQUEST_SUCCESS, 'test data')])
+ self.transport.packets = []
+ self.conn.ssh_GLOBAL_REQUEST(common.NS('TestBad') + '\xff')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_REQUEST_FAILURE, '')])
+ self.transport.packets = []
+ self.conn.ssh_GLOBAL_REQUEST(common.NS('TestGlobal') + '\x00')
+ self.assertEqual(self.transport.packets, [])
+
+ def test_REQUEST_SUCCESS(self):
+ """
+ Test that global request success packets cause the Deferred to be
+ called back.
+ """
+ d = self.conn.sendGlobalRequest('request', 'data', True)
+ self.conn.ssh_REQUEST_SUCCESS('data')
+ def check(data):
+ self.assertEqual(data, 'data')
+ d.addCallback(check)
+ d.addErrback(self.fail)
+ return d
+
+ def test_REQUEST_FAILURE(self):
+ """
+ Test that global request failure packets cause the Deferred to be
+ erred back.
+ """
+ d = self.conn.sendGlobalRequest('request', 'data', True)
+ self.conn.ssh_REQUEST_FAILURE('data')
+ def check(f):
+ self.assertEqual(f.value.data, 'data')
+ d.addCallback(self.fail)
+ d.addErrback(check)
+ return d
+
+ def test_CHANNEL_OPEN(self):
+ """
+ Test that open channel packets cause a channel to be created and
+ opened or a failure message to be returned.
+ """
+ del self.transport.avatar
+ self.conn.ssh_CHANNEL_OPEN(common.NS('TestChannel') +
+ '\x00\x00\x00\x01' * 4)
+ self.assertTrue(self.conn.channel.gotOpen)
+ self.assertEqual(self.conn.channel.conn, self.conn)
+ self.assertEqual(self.conn.channel.data, '\x00\x00\x00\x01')
+ self.assertEqual(self.conn.channel.specificData, '\x00\x00\x00\x01')
+ self.assertEqual(self.conn.channel.remoteWindowLeft, 1)
+ self.assertEqual(self.conn.channel.remoteMaxPacket, 1)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_OPEN_CONFIRMATION,
+ '\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x00'
+ '\x00\x00\x80\x00')])
+ self.transport.packets = []
+ self.conn.ssh_CHANNEL_OPEN(common.NS('BadChannel') +
+ '\x00\x00\x00\x02' * 4)
+ self.flushLoggedErrors()
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_OPEN_FAILURE,
+ '\x00\x00\x00\x02\x00\x00\x00\x03' + common.NS(
+ 'unknown channel') + common.NS(''))])
+ self.transport.packets = []
+ self.conn.ssh_CHANNEL_OPEN(common.NS('ErrorChannel') +
+ '\x00\x00\x00\x02' * 4)
+ self.flushLoggedErrors()
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_OPEN_FAILURE,
+ '\x00\x00\x00\x02\x00\x00\x00\x02' + common.NS(
+ 'unknown failure') + common.NS(''))])
+
+
+ def _lookupChannelErrorTest(self, code):
+ """
+ Deliver a request for a channel open which will result in an exception
+ being raised during channel lookup. Assert that an error response is
+ delivered as a result.
+ """
+ self.transport.avatar._ARGS_ERROR_CODE = code
+ self.conn.ssh_CHANNEL_OPEN(
+ common.NS('conch-error-args') + '\x00\x00\x00\x01' * 4)
+ errors = self.flushLoggedErrors(error.ConchError)
+ self.assertEqual(
+ len(errors), 1, "Expected one error, got: %r" % (errors,))
+ self.assertEqual(errors[0].value.args, (123, "error args in wrong order"))
+ self.assertEqual(
+ self.transport.packets,
+ [(connection.MSG_CHANNEL_OPEN_FAILURE,
+ # The response includes some bytes which identifying the
+ # associated request, as well as the error code (7b in hex) and
+ # the error message.
+ '\x00\x00\x00\x01\x00\x00\x00\x7b' + common.NS(
+ 'error args in wrong order') + common.NS(''))])
+
+
+ def test_lookupChannelError(self):
+ """
+ If a C{lookupChannel} implementation raises L{error.ConchError} with the
+ arguments in the wrong order, a C{MSG_CHANNEL_OPEN} failure is still
+ sent in response to the message.
+
+ This is a temporary work-around until L{error.ConchError} is given
+ better attributes and all of the Conch code starts constructing
+ instances of it properly. Eventually this functionality should be
+ deprecated and then removed.
+ """
+ self._lookupChannelErrorTest(123)
+
+
+ def test_lookupChannelErrorLongCode(self):
+ """
+ Like L{test_lookupChannelError}, but for the case where the failure code
+ is represented as a C{long} instead of a C{int}.
+ """
+ self._lookupChannelErrorTest(123L)
+
+
+ def test_CHANNEL_OPEN_CONFIRMATION(self):
+ """
+ Test that channel open confirmation packets cause the channel to be
+ notified that it's open.
+ """
+ channel = TestChannel()
+ self.conn.openChannel(channel)
+ self.conn.ssh_CHANNEL_OPEN_CONFIRMATION('\x00\x00\x00\x00'*5)
+ self.assertEqual(channel.remoteWindowLeft, 0)
+ self.assertEqual(channel.remoteMaxPacket, 0)
+ self.assertEqual(channel.specificData, '\x00\x00\x00\x00')
+ self.assertEqual(self.conn.channelsToRemoteChannel[channel],
+ 0)
+ self.assertEqual(self.conn.localToRemoteChannel[0], 0)
+
+ def test_CHANNEL_OPEN_FAILURE(self):
+ """
+ Test that channel open failure packets cause the channel to be
+ notified that its opening failed.
+ """
+ channel = TestChannel()
+ self.conn.openChannel(channel)
+ self.conn.ssh_CHANNEL_OPEN_FAILURE('\x00\x00\x00\x00\x00\x00\x00'
+ '\x01' + common.NS('failure!'))
+ self.assertEqual(channel.openFailureReason.args, ('failure!', 1))
+ self.assertEqual(self.conn.channels.get(channel), None)
+
+
+ def test_CHANNEL_WINDOW_ADJUST(self):
+ """
+ Test that channel window adjust messages add bytes to the channel
+ window.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ oldWindowSize = channel.remoteWindowLeft
+ self.conn.ssh_CHANNEL_WINDOW_ADJUST('\x00\x00\x00\x00\x00\x00\x00'
+ '\x01')
+ self.assertEqual(channel.remoteWindowLeft, oldWindowSize + 1)
+
+ def test_CHANNEL_DATA(self):
+ """
+ Test that channel data messages are passed up to the channel, or
+ cause the channel to be closed if the data is too large.
+ """
+ channel = TestChannel(localWindow=6, localMaxPacket=5)
+ self._openChannel(channel)
+ self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x00' + common.NS('data'))
+ self.assertEqual(channel.inBuffer, ['data'])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff'
+ '\x00\x00\x00\x04')])
+ self.transport.packets = []
+ longData = 'a' * (channel.localWindowLeft + 1)
+ self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x00' + common.NS(longData))
+ self.assertEqual(channel.inBuffer, ['data'])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+ channel = TestChannel()
+ self._openChannel(channel)
+ bigData = 'a' * (channel.localMaxPacket + 1)
+ self.transport.packets = []
+ self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x01' + common.NS(bigData))
+ self.assertEqual(channel.inBuffer, [])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+
+ def test_CHANNEL_EXTENDED_DATA(self):
+ """
+ Test that channel extended data messages are passed up to the channel,
+ or cause the channel to be closed if they're too big.
+ """
+ channel = TestChannel(localWindow=6, localMaxPacket=5)
+ self._openChannel(channel)
+ self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x00\x00\x00\x00'
+ '\x00' + common.NS('data'))
+ self.assertEqual(channel.extBuffer, [(0, 'data')])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff'
+ '\x00\x00\x00\x04')])
+ self.transport.packets = []
+ longData = 'a' * (channel.localWindowLeft + 1)
+ self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x00\x00\x00\x00'
+ '\x00' + common.NS(longData))
+ self.assertEqual(channel.extBuffer, [(0, 'data')])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+ channel = TestChannel()
+ self._openChannel(channel)
+ bigData = 'a' * (channel.localMaxPacket + 1)
+ self.transport.packets = []
+ self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x01\x00\x00\x00'
+ '\x00' + common.NS(bigData))
+ self.assertEqual(channel.extBuffer, [])
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+
+ def test_CHANNEL_EOF(self):
+ """
+ Test that channel eof messages are passed up to the channel.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.ssh_CHANNEL_EOF('\x00\x00\x00\x00')
+ self.assertTrue(channel.gotEOF)
+
+ def test_CHANNEL_CLOSE(self):
+ """
+ Test that channel close messages are passed up to the channel. Also,
+ test that channel.close() is called if both sides are closed when this
+ message is received.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.sendClose(channel)
+ self.conn.ssh_CHANNEL_CLOSE('\x00\x00\x00\x00')
+ self.assertTrue(channel.gotOneClose)
+ self.assertTrue(channel.gotClosed)
+
+ def test_CHANNEL_REQUEST_success(self):
+ """
+ Test that channel requests that succeed send MSG_CHANNEL_SUCCESS.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS('test')
+ + '\x00')
+ self.assertEqual(channel.numberRequests, 1)
+ d = self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS(
+ 'test') + '\xff' + 'data')
+ def check(result):
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_SUCCESS, '\x00\x00\x00\xff')])
+ d.addCallback(check)
+ return d
+
+ def test_CHANNEL_REQUEST_failure(self):
+ """
+ Test that channel requests that fail send MSG_CHANNEL_FAILURE.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ d = self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS(
+ 'test') + '\xff')
+ def check(result):
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_FAILURE, '\x00\x00\x00\xff'
+ )])
+ d.addCallback(self.fail)
+ d.addErrback(check)
+ return d
+
+ def test_CHANNEL_REQUEST_SUCCESS(self):
+ """
+ Test that channel request success messages cause the Deferred to be
+ called back.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ d = self.conn.sendRequest(channel, 'test', 'data', True)
+ self.conn.ssh_CHANNEL_SUCCESS('\x00\x00\x00\x00')
+ def check(result):
+ self.assertTrue(result)
+ return d
+
+ def test_CHANNEL_REQUEST_FAILURE(self):
+ """
+ Test that channel request failure messages cause the Deferred to be
+ erred back.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ d = self.conn.sendRequest(channel, 'test', '', True)
+ self.conn.ssh_CHANNEL_FAILURE('\x00\x00\x00\x00')
+ def check(result):
+ self.assertEqual(result.value.value, 'channel request failed')
+ d.addCallback(self.fail)
+ d.addErrback(check)
+ return d
+
+ def test_sendGlobalRequest(self):
+ """
+ Test that global request messages are sent in the right format.
+ """
+ d = self.conn.sendGlobalRequest('wantReply', 'data', True)
+ # must be added to prevent errbacking during teardown
+ d.addErrback(lambda failure: None)
+ self.conn.sendGlobalRequest('noReply', '', False)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_GLOBAL_REQUEST, common.NS('wantReply') +
+ '\xffdata'),
+ (connection.MSG_GLOBAL_REQUEST, common.NS('noReply') +
+ '\x00')])
+ self.assertEqual(self.conn.deferreds, {'global':[d]})
+
+ def test_openChannel(self):
+ """
+ Test that open channel messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self.conn.openChannel(channel, 'aaaa')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_OPEN, common.NS('TestChannel') +
+ '\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x80\x00aaaa')])
+ self.assertEqual(channel.id, 0)
+ self.assertEqual(self.conn.localChannelID, 1)
+
+ def test_sendRequest(self):
+ """
+ Test that channel request messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ d = self.conn.sendRequest(channel, 'test', 'test', True)
+ # needed to prevent errbacks during teardown.
+ d.addErrback(lambda failure: None)
+ self.conn.sendRequest(channel, 'test2', '', False)
+ channel.localClosed = True # emulate sending a close message
+ self.conn.sendRequest(channel, 'test3', '', True)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_REQUEST, '\x00\x00\x00\xff' +
+ common.NS('test') + '\x01test'),
+ (connection.MSG_CHANNEL_REQUEST, '\x00\x00\x00\xff' +
+ common.NS('test2') + '\x00')])
+ self.assertEqual(self.conn.deferreds[0], [d])
+
+ def test_adjustWindow(self):
+ """
+ Test that channel window adjust messages cause bytes to be added
+ to the window.
+ """
+ channel = TestChannel(localWindow=5)
+ self._openChannel(channel)
+ channel.localWindowLeft = 0
+ self.conn.adjustWindow(channel, 1)
+ self.assertEqual(channel.localWindowLeft, 1)
+ channel.localClosed = True
+ self.conn.adjustWindow(channel, 2)
+ self.assertEqual(channel.localWindowLeft, 1)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff'
+ '\x00\x00\x00\x01')])
+
+ def test_sendData(self):
+ """
+ Test that channel data messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.sendData(channel, 'a')
+ channel.localClosed = True
+ self.conn.sendData(channel, 'b')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_DATA, '\x00\x00\x00\xff' +
+ common.NS('a'))])
+
+ def test_sendExtendedData(self):
+ """
+ Test that channel extended data messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.sendExtendedData(channel, 1, 'test')
+ channel.localClosed = True
+ self.conn.sendExtendedData(channel, 2, 'test2')
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_EXTENDED_DATA, '\x00\x00\x00\xff' +
+ '\x00\x00\x00\x01' + common.NS('test'))])
+
+ def test_sendEOF(self):
+ """
+ Test that channel EOF messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.sendEOF(channel)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_EOF, '\x00\x00\x00\xff')])
+ channel.localClosed = True
+ self.conn.sendEOF(channel)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_EOF, '\x00\x00\x00\xff')])
+
+ def test_sendClose(self):
+ """
+ Test that channel close messages are sent in the right format.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+ self.conn.sendClose(channel)
+ self.assertTrue(channel.localClosed)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+ self.conn.sendClose(channel)
+ self.assertEqual(self.transport.packets,
+ [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')])
+
+ channel2 = TestChannel()
+ self._openChannel(channel2)
+ channel2.remoteClosed = True
+ self.conn.sendClose(channel2)
+ self.assertTrue(channel2.gotClosed)
+
+ def test_getChannelWithAvatar(self):
+ """
+ Test that getChannel dispatches to the avatar when an avatar is
+ present. Correct functioning without the avatar is verified in
+ test_CHANNEL_OPEN.
+ """
+ channel = self.conn.getChannel('TestChannel', 50, 30, 'data')
+ self.assertEqual(channel.data, 'data')
+ self.assertEqual(channel.remoteWindowLeft, 50)
+ self.assertEqual(channel.remoteMaxPacket, 30)
+ self.assertRaises(error.ConchError, self.conn.getChannel,
+ 'BadChannel', 50, 30, 'data')
+
+ def test_gotGlobalRequestWithoutAvatar(self):
+ """
+ Test that gotGlobalRequests dispatches to global_* without an avatar.
+ """
+ del self.transport.avatar
+ self.assertTrue(self.conn.gotGlobalRequest('TestGlobal', 'data'))
+ self.assertEqual(self.conn.gotGlobalRequest('Test-Data', 'data'),
+ (True, 'data'))
+ self.assertFalse(self.conn.gotGlobalRequest('BadGlobal', 'data'))
+
+
+ def test_channelClosedCausesLeftoverChannelDeferredsToErrback(self):
+ """
+ Whenever an SSH channel gets closed any Deferred that was returned by a
+ sendRequest() on its parent connection must be errbacked.
+ """
+ channel = TestChannel()
+ self._openChannel(channel)
+
+ d = self.conn.sendRequest(
+ channel, "dummyrequest", "dummydata", wantReply=1)
+ d = self.assertFailure(d, error.ConchError)
+ self.conn.channelClosed(channel)
+ return d
+
+
+
+class TestCleanConnectionShutdown(unittest.TestCase):
+ """
+ Check whether correct cleanup is performed on connection shutdown.
+ """
+ if test_userauth.transport is None:
+ skip = "Cannot run without both PyCrypto and pyasn1"
+
+ def setUp(self):
+ self.transport = test_userauth.FakeTransport(None)
+ self.transport.avatar = TestAvatar()
+ self.conn = TestConnection()
+ self.conn.transport = self.transport
+
+
+ def test_serviceStoppedCausesLeftoverGlobalDeferredsToErrback(self):
+ """
+ Once the service is stopped any leftover global deferred returned by
+ a sendGlobalRequest() call must be errbacked.
+ """
+ self.conn.serviceStarted()
+
+ d = self.conn.sendGlobalRequest(
+ "dummyrequest", "dummydata", wantReply=1)
+ d = self.assertFailure(d, error.ConchError)
+ self.conn.serviceStopped()
+ return d
+
+
diff --git a/twisted/conch/test/test_default.py b/twisted/conch/test/test_default.py
new file mode 100644
index 0000000..109f23d
--- /dev/null
+++ b/twisted/conch/test/test_default.py
@@ -0,0 +1,171 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.client.default}.
+"""
+try:
+ import Crypto.Cipher.DES3
+ import pyasn1
+except ImportError:
+ skip = "PyCrypto and PyASN1 required for twisted.conch.client.default."
+else:
+ from twisted.conch.client.agent import SSHAgentClient
+ from twisted.conch.client.default import SSHUserAuthClient
+ from twisted.conch.client.options import ConchOptions
+ from twisted.conch.ssh.keys import Key
+
+
+from twisted.trial.unittest import TestCase
+from twisted.python.filepath import FilePath
+from twisted.conch.test import keydata
+from twisted.test.proto_helpers import StringTransport
+
+
+
+class SSHUserAuthClientTest(TestCase):
+ """
+ Tests for L{SSHUserAuthClient}.
+
+ @type rsaPublic: L{Key}
+ @ivar rsaPublic: A public RSA key.
+ """
+
+ def setUp(self):
+ self.rsaPublic = Key.fromString(keydata.publicRSA_openssh)
+ self.tmpdir = FilePath(self.mktemp())
+ self.tmpdir.makedirs()
+ self.rsaFile = self.tmpdir.child('id_rsa')
+ self.rsaFile.setContent(keydata.privateRSA_openssh)
+ self.tmpdir.child('id_rsa.pub').setContent(keydata.publicRSA_openssh)
+
+
+ def test_signDataWithAgent(self):
+ """
+ When connected to an agent, L{SSHUserAuthClient} can use it to
+ request signatures of particular data with a particular L{Key}.
+ """
+ client = SSHUserAuthClient("user", ConchOptions(), None)
+ agent = SSHAgentClient()
+ transport = StringTransport()
+ agent.makeConnection(transport)
+ client.keyAgent = agent
+ cleartext = "Sign here"
+ client.signData(self.rsaPublic, cleartext)
+ self.assertEqual(
+ transport.value(),
+ "\x00\x00\x00\x8b\r\x00\x00\x00u" + self.rsaPublic.blob() +
+ "\x00\x00\x00\t" + cleartext +
+ "\x00\x00\x00\x00")
+
+
+ def test_agentGetPublicKey(self):
+ """
+ L{SSHUserAuthClient} looks up public keys from the agent using the
+ L{SSHAgentClient} class. That L{SSHAgentClient.getPublicKey} returns a
+ L{Key} object with one of the public keys in the agent. If no more
+ keys are present, it returns C{None}.
+ """
+ agent = SSHAgentClient()
+ agent.blobs = [self.rsaPublic.blob()]
+ key = agent.getPublicKey()
+ self.assertEqual(key.isPublic(), True)
+ self.assertEqual(key, self.rsaPublic)
+ self.assertEqual(agent.getPublicKey(), None)
+
+
+ def test_getPublicKeyFromFile(self):
+ """
+ L{SSHUserAuthClient.getPublicKey()} is able to get a public key from
+ the first file described by its options' C{identitys} list, and return
+ the corresponding public L{Key} object.
+ """
+ options = ConchOptions()
+ options.identitys = [self.rsaFile.path]
+ client = SSHUserAuthClient("user", options, None)
+ key = client.getPublicKey()
+ self.assertEqual(key.isPublic(), True)
+ self.assertEqual(key, self.rsaPublic)
+
+
+ def test_getPublicKeyAgentFallback(self):
+ """
+ If an agent is present, but doesn't return a key,
+ L{SSHUserAuthClient.getPublicKey} continue with the normal key lookup.
+ """
+ options = ConchOptions()
+ options.identitys = [self.rsaFile.path]
+ agent = SSHAgentClient()
+ client = SSHUserAuthClient("user", options, None)
+ client.keyAgent = agent
+ key = client.getPublicKey()
+ self.assertEqual(key.isPublic(), True)
+ self.assertEqual(key, self.rsaPublic)
+
+
+ def test_getPublicKeyBadKeyError(self):
+ """
+ If L{keys.Key.fromFile} raises a L{keys.BadKeyError}, the
+ L{SSHUserAuthClient.getPublicKey} tries again to get a public key by
+ calling itself recursively.
+ """
+ options = ConchOptions()
+ self.tmpdir.child('id_dsa.pub').setContent(keydata.publicDSA_openssh)
+ dsaFile = self.tmpdir.child('id_dsa')
+ dsaFile.setContent(keydata.privateDSA_openssh)
+ options.identitys = [self.rsaFile.path, dsaFile.path]
+ self.tmpdir.child('id_rsa.pub').setContent('not a key!')
+ client = SSHUserAuthClient("user", options, None)
+ key = client.getPublicKey()
+ self.assertEqual(key.isPublic(), True)
+ self.assertEqual(key, Key.fromString(keydata.publicDSA_openssh))
+ self.assertEqual(client.usedFiles, [self.rsaFile.path, dsaFile.path])
+
+
+ def test_getPrivateKey(self):
+ """
+ L{SSHUserAuthClient.getPrivateKey} will load a private key from the
+ last used file populated by L{SSHUserAuthClient.getPublicKey}, and
+ return a L{Deferred} which fires with the corresponding private L{Key}.
+ """
+ rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
+ options = ConchOptions()
+ options.identitys = [self.rsaFile.path]
+ client = SSHUserAuthClient("user", options, None)
+ # Populate the list of used files
+ client.getPublicKey()
+
+ def _cbGetPrivateKey(key):
+ self.assertEqual(key.isPublic(), False)
+ self.assertEqual(key, rsaPrivate)
+
+ return client.getPrivateKey().addCallback(_cbGetPrivateKey)
+
+
+ def test_getPrivateKeyPassphrase(self):
+ """
+ L{SSHUserAuthClient} can get a private key from a file, and return a
+ Deferred called back with a private L{Key} object, even if the key is
+ encrypted.
+ """
+ rsaPrivate = Key.fromString(keydata.privateRSA_openssh)
+ passphrase = 'this is the passphrase'
+ self.rsaFile.setContent(rsaPrivate.toString('openssh', passphrase))
+ options = ConchOptions()
+ options.identitys = [self.rsaFile.path]
+ client = SSHUserAuthClient("user", options, None)
+ # Populate the list of used files
+ client.getPublicKey()
+
+ def _getPassword(prompt):
+ self.assertEqual(prompt,
+ "Enter passphrase for key '%s': " % (
+ self.rsaFile.path,))
+ return passphrase
+
+ def _cbGetPrivateKey(key):
+ self.assertEqual(key.isPublic(), False)
+ self.assertEqual(key, rsaPrivate)
+
+ self.patch(client, '_getPassword', _getPassword)
+ return client.getPrivateKey().addCallback(_cbGetPrivateKey)
diff --git a/twisted/conch/test/test_filetransfer.py b/twisted/conch/test/test_filetransfer.py
new file mode 100644
index 0000000..3849331
--- /dev/null
+++ b/twisted/conch/test/test_filetransfer.py
@@ -0,0 +1,765 @@
+# -*- test-case-name: twisted.conch.test.test_filetransfer -*-
+# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
+# See LICENSE file for details.
+
+
+import os
+import re
+import struct
+import sys
+
+from twisted.trial import unittest
+try:
+ from twisted.conch import unix
+ unix # shut up pyflakes
+except ImportError:
+ unix = None
+ try:
+ del sys.modules['twisted.conch.unix'] # remove the bad import
+ except KeyError:
+ # In Python 2.4, the bad import has already been cleaned up for us.
+ # Hooray.
+ pass
+
+from twisted.conch import avatar
+from twisted.conch.ssh import common, connection, filetransfer, session
+from twisted.internet import defer
+from twisted.protocols import loopback
+from twisted.python import components
+
+
+class TestAvatar(avatar.ConchUser):
+ def __init__(self):
+ avatar.ConchUser.__init__(self)
+ self.channelLookup['session'] = session.SSHSession
+ self.subsystemLookup['sftp'] = filetransfer.FileTransferServer
+
+ def _runAsUser(self, f, *args, **kw):
+ try:
+ f = iter(f)
+ except TypeError:
+ f = [(f, args, kw)]
+ for i in f:
+ func = i[0]
+ args = len(i)>1 and i[1] or ()
+ kw = len(i)>2 and i[2] or {}
+ r = func(*args, **kw)
+ return r
+
+
+class FileTransferTestAvatar(TestAvatar):
+
+ def __init__(self, homeDir):
+ TestAvatar.__init__(self)
+ self.homeDir = homeDir
+
+ def getHomeDir(self):
+ return os.path.join(os.getcwd(), self.homeDir)
+
+
+class ConchSessionForTestAvatar:
+
+ def __init__(self, avatar):
+ self.avatar = avatar
+
+if unix:
+ if not hasattr(unix, 'SFTPServerForUnixConchUser'):
+ # unix should either be a fully working module, or None. I'm not sure
+ # how this happens, but on win32 it does. Try to cope. --spiv.
+ import warnings
+ warnings.warn(("twisted.conch.unix imported %r, "
+ "but doesn't define SFTPServerForUnixConchUser'")
+ % (unix,))
+ unix = None
+ else:
+ class FileTransferForTestAvatar(unix.SFTPServerForUnixConchUser):
+
+ def gotVersion(self, version, otherExt):
+ return {'conchTest' : 'ext data'}
+
+ def extendedRequest(self, extName, extData):
+ if extName == 'testExtendedRequest':
+ return 'bar'
+ raise NotImplementedError
+
+ components.registerAdapter(FileTransferForTestAvatar,
+ TestAvatar,
+ filetransfer.ISFTPServer)
+
+class SFTPTestBase(unittest.TestCase):
+
+ def setUp(self):
+ self.testDir = self.mktemp()
+ # Give the testDir another level so we can safely "cd .." from it in
+ # tests.
+ self.testDir = os.path.join(self.testDir, 'extra')
+ os.makedirs(os.path.join(self.testDir, 'testDirectory'))
+
+ f = file(os.path.join(self.testDir, 'testfile1'),'w')
+ f.write('a'*10+'b'*10)
+ f.write(file('/dev/urandom').read(1024*64)) # random data
+ os.chmod(os.path.join(self.testDir, 'testfile1'), 0644)
+ file(os.path.join(self.testDir, 'testRemoveFile'), 'w').write('a')
+ file(os.path.join(self.testDir, 'testRenameFile'), 'w').write('a')
+ file(os.path.join(self.testDir, '.testHiddenFile'), 'w').write('a')
+
+
+class TestOurServerOurClient(SFTPTestBase):
+
+ if not unix:
+ skip = "can't run on non-posix computers"
+
+ def setUp(self):
+ SFTPTestBase.setUp(self)
+
+ self.avatar = FileTransferTestAvatar(self.testDir)
+ self.server = filetransfer.FileTransferServer(avatar=self.avatar)
+ clientTransport = loopback.LoopbackRelay(self.server)
+
+ self.client = filetransfer.FileTransferClient()
+ self._serverVersion = None
+ self._extData = None
+ def _(serverVersion, extData):
+ self._serverVersion = serverVersion
+ self._extData = extData
+ self.client.gotServerVersion = _
+ serverTransport = loopback.LoopbackRelay(self.client)
+ self.client.makeConnection(clientTransport)
+ self.server.makeConnection(serverTransport)
+
+ self.clientTransport = clientTransport
+ self.serverTransport = serverTransport
+
+ self._emptyBuffers()
+
+
+ def _emptyBuffers(self):
+ while self.serverTransport.buffer or self.clientTransport.buffer:
+ self.serverTransport.clearBuffer()
+ self.clientTransport.clearBuffer()
+
+
+ def tearDown(self):
+ self.serverTransport.loseConnection()
+ self.clientTransport.loseConnection()
+ self.serverTransport.clearBuffer()
+ self.clientTransport.clearBuffer()
+
+
+ def testServerVersion(self):
+ self.assertEqual(self._serverVersion, 3)
+ self.assertEqual(self._extData, {'conchTest' : 'ext data'})
+
+
+ def test_openedFileClosedWithConnection(self):
+ """
+ A file opened with C{openFile} is close when the connection is lost.
+ """
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {})
+ self._emptyBuffers()
+
+ oldClose = os.close
+ closed = []
+ def close(fd):
+ closed.append(fd)
+ oldClose(fd)
+
+ self.patch(os, "close", close)
+
+ def _fileOpened(openFile):
+ fd = self.server.openFiles[openFile.handle[4:]].fd
+ self.serverTransport.loseConnection()
+ self.clientTransport.loseConnection()
+ self.serverTransport.clearBuffer()
+ self.clientTransport.clearBuffer()
+ self.assertEqual(self.server.openFiles, {})
+ self.assertIn(fd, closed)
+
+ d.addCallback(_fileOpened)
+ return d
+
+
+ def test_openedDirectoryClosedWithConnection(self):
+ """
+ A directory opened with C{openDirectory} is close when the connection
+ is lost.
+ """
+ d = self.client.openDirectory('')
+ self._emptyBuffers()
+
+ def _getFiles(openDir):
+ self.serverTransport.loseConnection()
+ self.clientTransport.loseConnection()
+ self.serverTransport.clearBuffer()
+ self.clientTransport.clearBuffer()
+ self.assertEqual(self.server.openDirs, {})
+
+ d.addCallback(_getFiles)
+ return d
+
+
+ def testOpenFileIO(self):
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {})
+ self._emptyBuffers()
+
+ def _fileOpened(openFile):
+ self.assertEqual(openFile, filetransfer.ISFTPFile(openFile))
+ d = _readChunk(openFile)
+ d.addCallback(_writeChunk, openFile)
+ return d
+
+ def _readChunk(openFile):
+ d = openFile.readChunk(0, 20)
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, 'a'*10 + 'b'*10)
+ return d
+
+ def _writeChunk(_, openFile):
+ d = openFile.writeChunk(20, 'c'*10)
+ self._emptyBuffers()
+ d.addCallback(_readChunk2, openFile)
+ return d
+
+ def _readChunk2(_, openFile):
+ d = openFile.readChunk(0, 30)
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, 'a'*10 + 'b'*10 + 'c'*10)
+ return d
+
+ d.addCallback(_fileOpened)
+ return d
+
+ def testClosedFileGetAttrs(self):
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {})
+ self._emptyBuffers()
+
+ def _getAttrs(_, openFile):
+ d = openFile.getAttrs()
+ self._emptyBuffers()
+ return d
+
+ def _err(f):
+ self.flushLoggedErrors()
+ return f
+
+ def _close(openFile):
+ d = openFile.close()
+ self._emptyBuffers()
+ d.addCallback(_getAttrs, openFile)
+ d.addErrback(_err)
+ return self.assertFailure(d, filetransfer.SFTPError)
+
+ d.addCallback(_close)
+ return d
+
+ def testOpenFileAttributes(self):
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {})
+ self._emptyBuffers()
+
+ def _getAttrs(openFile):
+ d = openFile.getAttrs()
+ self._emptyBuffers()
+ d.addCallback(_getAttrs2)
+ return d
+
+ def _getAttrs2(attrs1):
+ d = self.client.getAttrs('testfile1')
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, attrs1)
+ return d
+
+ return d.addCallback(_getAttrs)
+
+
+ def testOpenFileSetAttrs(self):
+ # XXX test setAttrs
+ # Ok, how about this for a start? It caught a bug :) -- spiv.
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {})
+ self._emptyBuffers()
+
+ def _getAttrs(openFile):
+ d = openFile.getAttrs()
+ self._emptyBuffers()
+ d.addCallback(_setAttrs)
+ return d
+
+ def _setAttrs(attrs):
+ attrs['atime'] = 0
+ d = self.client.setAttrs('testfile1', attrs)
+ self._emptyBuffers()
+ d.addCallback(_getAttrs2)
+ d.addCallback(self.assertEqual, attrs)
+ return d
+
+ def _getAttrs2(_):
+ d = self.client.getAttrs('testfile1')
+ self._emptyBuffers()
+ return d
+
+ d.addCallback(_getAttrs)
+ return d
+
+
+ def test_openFileExtendedAttributes(self):
+ """
+ Check that L{filetransfer.FileTransferClient.openFile} can send
+ extended attributes, that should be extracted server side. By default,
+ they are ignored, so we just verify they are correctly parsed.
+ """
+ savedAttributes = {}
+ oldOpenFile = self.server.client.openFile
+ def openFile(filename, flags, attrs):
+ savedAttributes.update(attrs)
+ return oldOpenFile(filename, flags, attrs)
+ self.server.client.openFile = openFile
+
+ d = self.client.openFile("testfile1", filetransfer.FXF_READ |
+ filetransfer.FXF_WRITE, {"ext_foo": "bar"})
+ self._emptyBuffers()
+
+ def check(ign):
+ self.assertEqual(savedAttributes, {"ext_foo": "bar"})
+
+ return d.addCallback(check)
+
+
+ def testRemoveFile(self):
+ d = self.client.getAttrs("testRemoveFile")
+ self._emptyBuffers()
+ def _removeFile(ignored):
+ d = self.client.removeFile("testRemoveFile")
+ self._emptyBuffers()
+ return d
+ d.addCallback(_removeFile)
+ d.addCallback(_removeFile)
+ return self.assertFailure(d, filetransfer.SFTPError)
+
+ def testRenameFile(self):
+ d = self.client.getAttrs("testRenameFile")
+ self._emptyBuffers()
+ def _rename(attrs):
+ d = self.client.renameFile("testRenameFile", "testRenamedFile")
+ self._emptyBuffers()
+ d.addCallback(_testRenamed, attrs)
+ return d
+ def _testRenamed(_, attrs):
+ d = self.client.getAttrs("testRenamedFile")
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, attrs)
+ return d.addCallback(_rename)
+
+ def testDirectoryBad(self):
+ d = self.client.getAttrs("testMakeDirectory")
+ self._emptyBuffers()
+ return self.assertFailure(d, filetransfer.SFTPError)
+
+ def testDirectoryCreation(self):
+ d = self.client.makeDirectory("testMakeDirectory", {})
+ self._emptyBuffers()
+
+ def _getAttrs(_):
+ d = self.client.getAttrs("testMakeDirectory")
+ self._emptyBuffers()
+ return d
+
+ # XXX not until version 4/5
+ # self.assertEqual(filetransfer.FILEXFER_TYPE_DIRECTORY&attrs['type'],
+ # filetransfer.FILEXFER_TYPE_DIRECTORY)
+
+ def _removeDirectory(_):
+ d = self.client.removeDirectory("testMakeDirectory")
+ self._emptyBuffers()
+ return d
+
+ d.addCallback(_getAttrs)
+ d.addCallback(_removeDirectory)
+ d.addCallback(_getAttrs)
+ return self.assertFailure(d, filetransfer.SFTPError)
+
+ def testOpenDirectory(self):
+ d = self.client.openDirectory('')
+ self._emptyBuffers()
+ files = []
+
+ def _getFiles(openDir):
+ def append(f):
+ files.append(f)
+ return openDir
+ d = defer.maybeDeferred(openDir.next)
+ self._emptyBuffers()
+ d.addCallback(append)
+ d.addCallback(_getFiles)
+ d.addErrback(_close, openDir)
+ return d
+
+ def _checkFiles(ignored):
+ fs = list(zip(*files)[0])
+ fs.sort()
+ self.assertEqual(fs,
+ ['.testHiddenFile', 'testDirectory',
+ 'testRemoveFile', 'testRenameFile',
+ 'testfile1'])
+
+ def _close(_, openDir):
+ d = openDir.close()
+ self._emptyBuffers()
+ return d
+
+ d.addCallback(_getFiles)
+ d.addCallback(_checkFiles)
+ return d
+
+ def testLinkDoesntExist(self):
+ d = self.client.getAttrs('testLink')
+ self._emptyBuffers()
+ return self.assertFailure(d, filetransfer.SFTPError)
+
+ def testLinkSharesAttrs(self):
+ d = self.client.makeLink('testLink', 'testfile1')
+ self._emptyBuffers()
+ def _getFirstAttrs(_):
+ d = self.client.getAttrs('testLink', 1)
+ self._emptyBuffers()
+ return d
+ def _getSecondAttrs(firstAttrs):
+ d = self.client.getAttrs('testfile1')
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, firstAttrs)
+ return d
+ d.addCallback(_getFirstAttrs)
+ return d.addCallback(_getSecondAttrs)
+
+ def testLinkPath(self):
+ d = self.client.makeLink('testLink', 'testfile1')
+ self._emptyBuffers()
+ def _readLink(_):
+ d = self.client.readLink('testLink')
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual,
+ os.path.join(os.getcwd(), self.testDir, 'testfile1'))
+ return d
+ def _realPath(_):
+ d = self.client.realPath('testLink')
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual,
+ os.path.join(os.getcwd(), self.testDir, 'testfile1'))
+ return d
+ d.addCallback(_readLink)
+ d.addCallback(_realPath)
+ return d
+
+ def testExtendedRequest(self):
+ d = self.client.extendedRequest('testExtendedRequest', 'foo')
+ self._emptyBuffers()
+ d.addCallback(self.assertEqual, 'bar')
+ d.addCallback(self._cbTestExtendedRequest)
+ return d
+
+ def _cbTestExtendedRequest(self, ignored):
+ d = self.client.extendedRequest('testBadRequest', '')
+ self._emptyBuffers()
+ return self.assertFailure(d, NotImplementedError)
+
+
+class FakeConn:
+ def sendClose(self, channel):
+ pass
+
+
+class TestFileTransferClose(unittest.TestCase):
+
+ if not unix:
+ skip = "can't run on non-posix computers"
+
+ def setUp(self):
+ self.avatar = TestAvatar()
+
+ def buildServerConnection(self):
+ # make a server connection
+ conn = connection.SSHConnection()
+ # server connections have a 'self.transport.avatar'.
+ class DummyTransport:
+ def __init__(self):
+ self.transport = self
+ def sendPacket(self, kind, data):
+ pass
+ def logPrefix(self):
+ return 'dummy transport'
+ conn.transport = DummyTransport()
+ conn.transport.avatar = self.avatar
+ return conn
+
+ def interceptConnectionLost(self, sftpServer):
+ self.connectionLostFired = False
+ origConnectionLost = sftpServer.connectionLost
+ def connectionLost(reason):
+ self.connectionLostFired = True
+ origConnectionLost(reason)
+ sftpServer.connectionLost = connectionLost
+
+ def assertSFTPConnectionLost(self):
+ self.assertTrue(self.connectionLostFired,
+ "sftpServer's connectionLost was not called")
+
+ def test_sessionClose(self):
+ """
+ Closing a session should notify an SFTP subsystem launched by that
+ session.
+ """
+ # make a session
+ testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar)
+
+ # start an SFTP subsystem on the session
+ testSession.request_subsystem(common.NS('sftp'))
+ sftpServer = testSession.client.transport.proto
+
+ # intercept connectionLost so we can check that it's called
+ self.interceptConnectionLost(sftpServer)
+
+ # close session
+ testSession.closeReceived()
+
+ self.assertSFTPConnectionLost()
+
+ def test_clientClosesChannelOnConnnection(self):
+ """
+ A client sending CHANNEL_CLOSE should trigger closeReceived on the
+ associated channel instance.
+ """
+ conn = self.buildServerConnection()
+
+ # somehow get a session
+ packet = common.NS('session') + struct.pack('>L', 0) * 3
+ conn.ssh_CHANNEL_OPEN(packet)
+ sessionChannel = conn.channels[0]
+
+ sessionChannel.request_subsystem(common.NS('sftp'))
+ sftpServer = sessionChannel.client.transport.proto
+ self.interceptConnectionLost(sftpServer)
+
+ # intercept closeReceived
+ self.interceptConnectionLost(sftpServer)
+
+ # close the connection
+ conn.ssh_CHANNEL_CLOSE(struct.pack('>L', 0))
+
+ self.assertSFTPConnectionLost()
+
+
+ def test_stopConnectionServiceClosesChannel(self):
+ """
+ Closing an SSH connection should close all sessions within it.
+ """
+ conn = self.buildServerConnection()
+
+ # somehow get a session
+ packet = common.NS('session') + struct.pack('>L', 0) * 3
+ conn.ssh_CHANNEL_OPEN(packet)
+ sessionChannel = conn.channels[0]
+
+ sessionChannel.request_subsystem(common.NS('sftp'))
+ sftpServer = sessionChannel.client.transport.proto
+ self.interceptConnectionLost(sftpServer)
+
+ # close the connection
+ conn.serviceStopped()
+
+ self.assertSFTPConnectionLost()
+
+
+
+class TestConstants(unittest.TestCase):
+ """
+ Tests for the constants used by the SFTP protocol implementation.
+
+ @ivar filexferSpecExcerpts: Excerpts from the
+ draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP
+ protocol. There are more recent drafts of the specification, but this
+ one describes version 3, which is what conch (and OpenSSH) implements.
+ """
+
+
+ filexferSpecExcerpts = [
+ """
+ The following values are defined for packet types.
+
+ #define SSH_FXP_INIT 1
+ #define SSH_FXP_VERSION 2
+ #define SSH_FXP_OPEN 3
+ #define SSH_FXP_CLOSE 4
+ #define SSH_FXP_READ 5
+ #define SSH_FXP_WRITE 6
+ #define SSH_FXP_LSTAT 7
+ #define SSH_FXP_FSTAT 8
+ #define SSH_FXP_SETSTAT 9
+ #define SSH_FXP_FSETSTAT 10
+ #define SSH_FXP_OPENDIR 11
+ #define SSH_FXP_READDIR 12
+ #define SSH_FXP_REMOVE 13
+ #define SSH_FXP_MKDIR 14
+ #define SSH_FXP_RMDIR 15
+ #define SSH_FXP_REALPATH 16
+ #define SSH_FXP_STAT 17
+ #define SSH_FXP_RENAME 18
+ #define SSH_FXP_READLINK 19
+ #define SSH_FXP_SYMLINK 20
+ #define SSH_FXP_STATUS 101
+ #define SSH_FXP_HANDLE 102
+ #define SSH_FXP_DATA 103
+ #define SSH_FXP_NAME 104
+ #define SSH_FXP_ATTRS 105
+ #define SSH_FXP_EXTENDED 200
+ #define SSH_FXP_EXTENDED_REPLY 201
+
+ Additional packet types should only be defined if the protocol
+ version number (see Section ``Protocol Initialization'') is
+ incremented, and their use MUST be negotiated using the version
+ number. However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY
+ packets can be used to implement vendor-specific extensions. See
+ Section ``Vendor-Specific-Extensions'' for more details.
+ """,
+ """
+ The flags bits are defined to have the following values:
+
+ #define SSH_FILEXFER_ATTR_SIZE 0x00000001
+ #define SSH_FILEXFER_ATTR_UIDGID 0x00000002
+ #define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004
+ #define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008
+ #define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
+
+ """,
+ """
+ The `pflags' field is a bitmask. The following bits have been
+ defined.
+
+ #define SSH_FXF_READ 0x00000001
+ #define SSH_FXF_WRITE 0x00000002
+ #define SSH_FXF_APPEND 0x00000004
+ #define SSH_FXF_CREAT 0x00000008
+ #define SSH_FXF_TRUNC 0x00000010
+ #define SSH_FXF_EXCL 0x00000020
+ """,
+ """
+ Currently, the following values are defined (other values may be
+ defined by future versions of this protocol):
+
+ #define SSH_FX_OK 0
+ #define SSH_FX_EOF 1
+ #define SSH_FX_NO_SUCH_FILE 2
+ #define SSH_FX_PERMISSION_DENIED 3
+ #define SSH_FX_FAILURE 4
+ #define SSH_FX_BAD_MESSAGE 5
+ #define SSH_FX_NO_CONNECTION 6
+ #define SSH_FX_CONNECTION_LOST 7
+ #define SSH_FX_OP_UNSUPPORTED 8
+ """]
+
+
+ def test_constantsAgainstSpec(self):
+ """
+ The constants used by the SFTP protocol implementation match those
+ found by searching through the spec.
+ """
+ constants = {}
+ for excerpt in self.filexferSpecExcerpts:
+ for line in excerpt.splitlines():
+ m = re.match('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line)
+ if m:
+ constants[m.group(1)] = long(m.group(2), 0)
+ self.assertTrue(
+ len(constants) > 0, "No constants found (the test must be buggy).")
+ for k, v in constants.items():
+ self.assertEqual(v, getattr(filetransfer, k))
+
+
+
+class TestRawPacketData(unittest.TestCase):
+ """
+ Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
+ less common protocol messages to exercise their handling.
+ """
+ def setUp(self):
+ self.ftc = filetransfer.FileTransferClient()
+
+
+ def test_packetSTATUS(self):
+ """
+ A STATUS packet containing a result code, a message, and a language is
+ parsed to produce the result of an outstanding request L{Deferred}.
+
+ @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
+ of the SFTP Internet-Draft.
+ """
+ d = defer.Deferred()
+ d.addCallback(self._cbTestPacketSTATUS)
+ self.ftc.openRequests[1] = d
+ data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg') + common.NS('lang')
+ self.ftc.packet_STATUS(data)
+ return d
+
+
+ def _cbTestPacketSTATUS(self, result):
+ """
+ Assert that the result is a two-tuple containing the message and
+ language from the STATUS packet.
+ """
+ self.assertEqual(result[0], 'msg')
+ self.assertEqual(result[1], 'lang')
+
+
+ def test_packetSTATUSShort(self):
+ """
+ A STATUS packet containing only a result code can also be parsed to
+ produce the result of an outstanding request L{Deferred}. Such packets
+ are sent by some SFTP implementations, though not strictly legal.
+
+ @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
+ of the SFTP Internet-Draft.
+ """
+ d = defer.Deferred()
+ d.addCallback(self._cbTestPacketSTATUSShort)
+ self.ftc.openRequests[1] = d
+ data = struct.pack('!LL', 1, filetransfer.FX_OK)
+ self.ftc.packet_STATUS(data)
+ return d
+
+
+ def _cbTestPacketSTATUSShort(self, result):
+ """
+ Assert that the result is a two-tuple containing empty strings, since
+ the STATUS packet had neither a message nor a language.
+ """
+ self.assertEqual(result[0], '')
+ self.assertEqual(result[1], '')
+
+
+ def test_packetSTATUSWithoutLang(self):
+ """
+ A STATUS packet containing a result code and a message but no language
+ can also be parsed to produce the result of an outstanding request
+ L{Deferred}. Such packets are sent by some SFTP implementations, though
+ not strictly legal.
+
+ @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
+ of the SFTP Internet-Draft.
+ """
+ d = defer.Deferred()
+ d.addCallback(self._cbTestPacketSTATUSWithoutLang)
+ self.ftc.openRequests[1] = d
+ data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg')
+ self.ftc.packet_STATUS(data)
+ return d
+
+
+ def _cbTestPacketSTATUSWithoutLang(self, result):
+ """
+ Assert that the result is a two-tuple containing the message from the
+ STATUS packet and an empty string, since the language was missing.
+ """
+ self.assertEqual(result[0], 'msg')
+ self.assertEqual(result[1], '')
diff --git a/twisted/conch/test/test_helper.py b/twisted/conch/test/test_helper.py
new file mode 100644
index 0000000..7064d03
--- /dev/null
+++ b/twisted/conch/test/test_helper.py
@@ -0,0 +1,560 @@
+# -*- test-case-name: twisted.conch.test.test_helper -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.conch.insults import helper
+from twisted.conch.insults.insults import G0, G1, G2, G3
+from twisted.conch.insults.insults import modes, privateModes
+from twisted.conch.insults.insults import NORMAL, BOLD, UNDERLINE, BLINK, REVERSE_VIDEO
+
+from twisted.trial import unittest
+
+WIDTH = 80
+HEIGHT = 24
+
+class BufferTestCase(unittest.TestCase):
+ def setUp(self):
+ self.term = helper.TerminalBuffer()
+ self.term.connectionMade()
+
+ def testInitialState(self):
+ self.assertEqual(self.term.width, WIDTH)
+ self.assertEqual(self.term.height, HEIGHT)
+ self.assertEqual(str(self.term),
+ '\n' * (HEIGHT - 1))
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+
+
+ def test_initialPrivateModes(self):
+ """
+ Verify that only DEC Auto Wrap Mode (DECAWM) and DEC Text Cursor Enable
+ Mode (DECTCEM) are initially in the Set Mode (SM) state.
+ """
+ self.assertEqual(
+ {privateModes.AUTO_WRAP: True,
+ privateModes.CURSOR_MODE: True},
+ self.term.privateModes)
+
+
+ def test_carriageReturn(self):
+ """
+ C{"\r"} moves the cursor to the first column in the current row.
+ """
+ self.term.cursorForward(5)
+ self.term.cursorDown(3)
+ self.assertEqual(self.term.reportCursorPosition(), (5, 3))
+ self.term.insertAtCursor("\r")
+ self.assertEqual(self.term.reportCursorPosition(), (0, 3))
+
+
+ def test_linefeed(self):
+ """
+ C{"\n"} moves the cursor to the next row without changing the column.
+ """
+ self.term.cursorForward(5)
+ self.assertEqual(self.term.reportCursorPosition(), (5, 0))
+ self.term.insertAtCursor("\n")
+ self.assertEqual(self.term.reportCursorPosition(), (5, 1))
+
+
+ def test_newline(self):
+ """
+ C{write} transforms C{"\n"} into C{"\r\n"}.
+ """
+ self.term.cursorForward(5)
+ self.term.cursorDown(3)
+ self.assertEqual(self.term.reportCursorPosition(), (5, 3))
+ self.term.write("\n")
+ self.assertEqual(self.term.reportCursorPosition(), (0, 4))
+
+
+ def test_setPrivateModes(self):
+ """
+ Verify that L{helper.TerminalBuffer.setPrivateModes} changes the Set
+ Mode (SM) state to "set" for the private modes it is passed.
+ """
+ expected = self.term.privateModes.copy()
+ self.term.setPrivateModes([privateModes.SCROLL, privateModes.SCREEN])
+ expected[privateModes.SCROLL] = True
+ expected[privateModes.SCREEN] = True
+ self.assertEqual(expected, self.term.privateModes)
+
+
+ def test_resetPrivateModes(self):
+ """
+ Verify that L{helper.TerminalBuffer.resetPrivateModes} changes the Set
+ Mode (SM) state to "reset" for the private modes it is passed.
+ """
+ expected = self.term.privateModes.copy()
+ self.term.resetPrivateModes([privateModes.AUTO_WRAP, privateModes.CURSOR_MODE])
+ del expected[privateModes.AUTO_WRAP]
+ del expected[privateModes.CURSOR_MODE]
+ self.assertEqual(expected, self.term.privateModes)
+
+
+ def testCursorDown(self):
+ self.term.cursorDown(3)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 3))
+ self.term.cursorDown()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 4))
+ self.term.cursorDown(HEIGHT)
+ self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
+
+ def testCursorUp(self):
+ self.term.cursorUp(5)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+
+ self.term.cursorDown(20)
+ self.term.cursorUp(1)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 19))
+
+ self.term.cursorUp(19)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+
+ def testCursorForward(self):
+ self.term.cursorForward(2)
+ self.assertEqual(self.term.reportCursorPosition(), (2, 0))
+ self.term.cursorForward(2)
+ self.assertEqual(self.term.reportCursorPosition(), (4, 0))
+ self.term.cursorForward(WIDTH)
+ self.assertEqual(self.term.reportCursorPosition(), (WIDTH, 0))
+
+ def testCursorBackward(self):
+ self.term.cursorForward(10)
+ self.term.cursorBackward(2)
+ self.assertEqual(self.term.reportCursorPosition(), (8, 0))
+ self.term.cursorBackward(7)
+ self.assertEqual(self.term.reportCursorPosition(), (1, 0))
+ self.term.cursorBackward(1)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+ self.term.cursorBackward(1)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+
+ def testCursorPositioning(self):
+ self.term.cursorPosition(3, 9)
+ self.assertEqual(self.term.reportCursorPosition(), (3, 9))
+
+ def testSimpleWriting(self):
+ s = "Hello, world."
+ self.term.write(s)
+ self.assertEqual(
+ str(self.term),
+ s + '\n' +
+ '\n' * (HEIGHT - 2))
+
+ def testOvertype(self):
+ s = "hello, world."
+ self.term.write(s)
+ self.term.cursorBackward(len(s))
+ self.term.resetModes([modes.IRM])
+ self.term.write("H")
+ self.assertEqual(
+ str(self.term),
+ ("H" + s[1:]) + '\n' +
+ '\n' * (HEIGHT - 2))
+
+ def testInsert(self):
+ s = "ello, world."
+ self.term.write(s)
+ self.term.cursorBackward(len(s))
+ self.term.setModes([modes.IRM])
+ self.term.write("H")
+ self.assertEqual(
+ str(self.term),
+ ("H" + s) + '\n' +
+ '\n' * (HEIGHT - 2))
+
+ def testWritingInTheMiddle(self):
+ s = "Hello, world."
+ self.term.cursorDown(5)
+ self.term.cursorForward(5)
+ self.term.write(s)
+ self.assertEqual(
+ str(self.term),
+ '\n' * 5 +
+ (self.term.fill * 5) + s + '\n' +
+ '\n' * (HEIGHT - 7))
+
+ def testWritingWrappedAtEndOfLine(self):
+ s = "Hello, world."
+ self.term.cursorForward(WIDTH - 5)
+ self.term.write(s)
+ self.assertEqual(
+ str(self.term),
+ s[:5].rjust(WIDTH) + '\n' +
+ s[5:] + '\n' +
+ '\n' * (HEIGHT - 3))
+
+ def testIndex(self):
+ self.term.index()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 1))
+ self.term.cursorDown(HEIGHT)
+ self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
+ self.term.index()
+ self.assertEqual(self.term.reportCursorPosition(), (0, HEIGHT - 1))
+
+ def testReverseIndex(self):
+ self.term.reverseIndex()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 0))
+ self.term.cursorDown(2)
+ self.assertEqual(self.term.reportCursorPosition(), (0, 2))
+ self.term.reverseIndex()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 1))
+
+ def test_nextLine(self):
+ """
+ C{nextLine} positions the cursor at the beginning of the row below the
+ current row.
+ """
+ self.term.nextLine()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 1))
+ self.term.cursorForward(5)
+ self.assertEqual(self.term.reportCursorPosition(), (5, 1))
+ self.term.nextLine()
+ self.assertEqual(self.term.reportCursorPosition(), (0, 2))
+
+ def testSaveCursor(self):
+ self.term.cursorDown(5)
+ self.term.cursorForward(7)
+ self.assertEqual(self.term.reportCursorPosition(), (7, 5))
+ self.term.saveCursor()
+ self.term.cursorDown(7)
+ self.term.cursorBackward(3)
+ self.assertEqual(self.term.reportCursorPosition(), (4, 12))
+ self.term.restoreCursor()
+ self.assertEqual(self.term.reportCursorPosition(), (7, 5))
+
+ def testSingleShifts(self):
+ self.term.singleShift2()
+ self.term.write('Hi')
+
+ ch = self.term.getCharacter(0, 0)
+ self.assertEqual(ch[0], 'H')
+ self.assertEqual(ch[1].charset, G2)
+
+ ch = self.term.getCharacter(1, 0)
+ self.assertEqual(ch[0], 'i')
+ self.assertEqual(ch[1].charset, G0)
+
+ self.term.singleShift3()
+ self.term.write('!!')
+
+ ch = self.term.getCharacter(2, 0)
+ self.assertEqual(ch[0], '!')
+ self.assertEqual(ch[1].charset, G3)
+
+ ch = self.term.getCharacter(3, 0)
+ self.assertEqual(ch[0], '!')
+ self.assertEqual(ch[1].charset, G0)
+
+ def testShifting(self):
+ s1 = "Hello"
+ s2 = "World"
+ s3 = "Bye!"
+ self.term.write("Hello\n")
+ self.term.shiftOut()
+ self.term.write("World\n")
+ self.term.shiftIn()
+ self.term.write("Bye!\n")
+
+ g = G0
+ h = 0
+ for s in (s1, s2, s3):
+ for i in range(len(s)):
+ ch = self.term.getCharacter(i, h)
+ self.assertEqual(ch[0], s[i])
+ self.assertEqual(ch[1].charset, g)
+ g = g == G0 and G1 or G0
+ h += 1
+
+ def testGraphicRendition(self):
+ self.term.selectGraphicRendition(BOLD, UNDERLINE, BLINK, REVERSE_VIDEO)
+ self.term.write('W')
+ self.term.selectGraphicRendition(NORMAL)
+ self.term.write('X')
+ self.term.selectGraphicRendition(BLINK)
+ self.term.write('Y')
+ self.term.selectGraphicRendition(BOLD)
+ self.term.write('Z')
+
+ ch = self.term.getCharacter(0, 0)
+ self.assertEqual(ch[0], 'W')
+ self.failUnless(ch[1].bold)
+ self.failUnless(ch[1].underline)
+ self.failUnless(ch[1].blink)
+ self.failUnless(ch[1].reverseVideo)
+
+ ch = self.term.getCharacter(1, 0)
+ self.assertEqual(ch[0], 'X')
+ self.failIf(ch[1].bold)
+ self.failIf(ch[1].underline)
+ self.failIf(ch[1].blink)
+ self.failIf(ch[1].reverseVideo)
+
+ ch = self.term.getCharacter(2, 0)
+ self.assertEqual(ch[0], 'Y')
+ self.failUnless(ch[1].blink)
+ self.failIf(ch[1].bold)
+ self.failIf(ch[1].underline)
+ self.failIf(ch[1].reverseVideo)
+
+ ch = self.term.getCharacter(3, 0)
+ self.assertEqual(ch[0], 'Z')
+ self.failUnless(ch[1].blink)
+ self.failUnless(ch[1].bold)
+ self.failIf(ch[1].underline)
+ self.failIf(ch[1].reverseVideo)
+
+ def testColorAttributes(self):
+ s1 = "Merry xmas"
+ s2 = "Just kidding"
+ self.term.selectGraphicRendition(helper.FOREGROUND + helper.RED,
+ helper.BACKGROUND + helper.GREEN)
+ self.term.write(s1 + "\n")
+ self.term.selectGraphicRendition(NORMAL)
+ self.term.write(s2 + "\n")
+
+ for i in range(len(s1)):
+ ch = self.term.getCharacter(i, 0)
+ self.assertEqual(ch[0], s1[i])
+ self.assertEqual(ch[1].charset, G0)
+ self.assertEqual(ch[1].bold, False)
+ self.assertEqual(ch[1].underline, False)
+ self.assertEqual(ch[1].blink, False)
+ self.assertEqual(ch[1].reverseVideo, False)
+ self.assertEqual(ch[1].foreground, helper.RED)
+ self.assertEqual(ch[1].background, helper.GREEN)
+
+ for i in range(len(s2)):
+ ch = self.term.getCharacter(i, 1)
+ self.assertEqual(ch[0], s2[i])
+ self.assertEqual(ch[1].charset, G0)
+ self.assertEqual(ch[1].bold, False)
+ self.assertEqual(ch[1].underline, False)
+ self.assertEqual(ch[1].blink, False)
+ self.assertEqual(ch[1].reverseVideo, False)
+ self.assertEqual(ch[1].foreground, helper.WHITE)
+ self.assertEqual(ch[1].background, helper.BLACK)
+
+ def testEraseLine(self):
+ s1 = 'line 1'
+ s2 = 'line 2'
+ s3 = 'line 3'
+ self.term.write('\n'.join((s1, s2, s3)) + '\n')
+ self.term.cursorPosition(1, 1)
+ self.term.eraseLine()
+
+ self.assertEqual(
+ str(self.term),
+ s1 + '\n' +
+ '\n' +
+ s3 + '\n' +
+ '\n' * (HEIGHT - 4))
+
+ def testEraseToLineEnd(self):
+ s = 'Hello, world.'
+ self.term.write(s)
+ self.term.cursorBackward(5)
+ self.term.eraseToLineEnd()
+ self.assertEqual(
+ str(self.term),
+ s[:-5] + '\n' +
+ '\n' * (HEIGHT - 2))
+
+ def testEraseToLineBeginning(self):
+ s = 'Hello, world.'
+ self.term.write(s)
+ self.term.cursorBackward(5)
+ self.term.eraseToLineBeginning()
+ self.assertEqual(
+ str(self.term),
+ s[-4:].rjust(len(s)) + '\n' +
+ '\n' * (HEIGHT - 2))
+
+ def testEraseDisplay(self):
+ self.term.write('Hello world\n')
+ self.term.write('Goodbye world\n')
+ self.term.eraseDisplay()
+
+ self.assertEqual(
+ str(self.term),
+ '\n' * (HEIGHT - 1))
+
+ def testEraseToDisplayEnd(self):
+ s1 = "Hello world"
+ s2 = "Goodbye world"
+ self.term.write('\n'.join((s1, s2, '')))
+ self.term.cursorPosition(5, 1)
+ self.term.eraseToDisplayEnd()
+
+ self.assertEqual(
+ str(self.term),
+ s1 + '\n' +
+ s2[:5] + '\n' +
+ '\n' * (HEIGHT - 3))
+
+ def testEraseToDisplayBeginning(self):
+ s1 = "Hello world"
+ s2 = "Goodbye world"
+ self.term.write('\n'.join((s1, s2)))
+ self.term.cursorPosition(5, 1)
+ self.term.eraseToDisplayBeginning()
+
+ self.assertEqual(
+ str(self.term),
+ '\n' +
+ s2[6:].rjust(len(s2)) + '\n' +
+ '\n' * (HEIGHT - 3))
+
+ def testLineInsertion(self):
+ s1 = "Hello world"
+ s2 = "Goodbye world"
+ self.term.write('\n'.join((s1, s2)))
+ self.term.cursorPosition(7, 1)
+ self.term.insertLine()
+
+ self.assertEqual(
+ str(self.term),
+ s1 + '\n' +
+ '\n' +
+ s2 + '\n' +
+ '\n' * (HEIGHT - 4))
+
+ def testLineDeletion(self):
+ s1 = "Hello world"
+ s2 = "Middle words"
+ s3 = "Goodbye world"
+ self.term.write('\n'.join((s1, s2, s3)))
+ self.term.cursorPosition(9, 1)
+ self.term.deleteLine()
+
+ self.assertEqual(
+ str(self.term),
+ s1 + '\n' +
+ s3 + '\n' +
+ '\n' * (HEIGHT - 3))
+
+class FakeDelayedCall:
+ called = False
+ cancelled = False
+ def __init__(self, fs, timeout, f, a, kw):
+ self.fs = fs
+ self.timeout = timeout
+ self.f = f
+ self.a = a
+ self.kw = kw
+
+ def active(self):
+ return not (self.cancelled or self.called)
+
+ def cancel(self):
+ self.cancelled = True
+# self.fs.calls.remove(self)
+
+ def call(self):
+ self.called = True
+ self.f(*self.a, **self.kw)
+
+class FakeScheduler:
+ def __init__(self):
+ self.calls = []
+
+ def callLater(self, timeout, f, *a, **kw):
+ self.calls.append(FakeDelayedCall(self, timeout, f, a, kw))
+ return self.calls[-1]
+
+class ExpectTestCase(unittest.TestCase):
+ def setUp(self):
+ self.term = helper.ExpectableBuffer()
+ self.term.connectionMade()
+ self.fs = FakeScheduler()
+
+ def testSimpleString(self):
+ result = []
+ d = self.term.expect("hello world", timeout=1, scheduler=self.fs)
+ d.addCallback(result.append)
+
+ self.term.write("greeting puny earthlings\n")
+ self.failIf(result)
+ self.term.write("hello world\n")
+ self.failUnless(result)
+ self.assertEqual(result[0].group(), "hello world")
+ self.assertEqual(len(self.fs.calls), 1)
+ self.failIf(self.fs.calls[0].active())
+
+ def testBrokenUpString(self):
+ result = []
+ d = self.term.expect("hello world")
+ d.addCallback(result.append)
+
+ self.failIf(result)
+ self.term.write("hello ")
+ self.failIf(result)
+ self.term.write("worl")
+ self.failIf(result)
+ self.term.write("d")
+ self.failUnless(result)
+ self.assertEqual(result[0].group(), "hello world")
+
+
+ def testMultiple(self):
+ result = []
+ d1 = self.term.expect("hello ")
+ d1.addCallback(result.append)
+ d2 = self.term.expect("world")
+ d2.addCallback(result.append)
+
+ self.failIf(result)
+ self.term.write("hello")
+ self.failIf(result)
+ self.term.write(" ")
+ self.assertEqual(len(result), 1)
+ self.term.write("world")
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0].group(), "hello ")
+ self.assertEqual(result[1].group(), "world")
+
+ def testSynchronous(self):
+ self.term.write("hello world")
+
+ result = []
+ d = self.term.expect("hello world")
+ d.addCallback(result.append)
+ self.failUnless(result)
+ self.assertEqual(result[0].group(), "hello world")
+
+ def testMultipleSynchronous(self):
+ self.term.write("goodbye world")
+
+ result = []
+ d1 = self.term.expect("bye")
+ d1.addCallback(result.append)
+ d2 = self.term.expect("world")
+ d2.addCallback(result.append)
+
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0].group(), "bye")
+ self.assertEqual(result[1].group(), "world")
+
+ def _cbTestTimeoutFailure(self, res):
+ self.assert_(hasattr(res, 'type'))
+ self.assertEqual(res.type, helper.ExpectationTimeout)
+
+ def testTimeoutFailure(self):
+ d = self.term.expect("hello world", timeout=1, scheduler=self.fs)
+ d.addBoth(self._cbTestTimeoutFailure)
+ self.fs.calls[0].call()
+
+ def testOverlappingTimeout(self):
+ self.term.write("not zoomtastic")
+
+ result = []
+ d1 = self.term.expect("hello world", timeout=1, scheduler=self.fs)
+ d1.addBoth(self._cbTestTimeoutFailure)
+ d2 = self.term.expect("zoom")
+ d2.addCallback(result.append)
+
+ self.fs.calls[0].call()
+
+ self.assertEqual(len(result), 1)
+ self.assertEqual(result[0].group(), "zoom")
diff --git a/twisted/conch/test/test_insults.py b/twisted/conch/test/test_insults.py
new file mode 100644
index 0000000..f313b5e
--- /dev/null
+++ b/twisted/conch/test/test_insults.py
@@ -0,0 +1,496 @@
+# -*- test-case-name: twisted.conch.test.test_insults -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+
+from twisted.conch.insults.insults import ServerProtocol, ClientProtocol
+from twisted.conch.insults.insults import CS_UK, CS_US, CS_DRAWING, CS_ALTERNATE, CS_ALTERNATE_SPECIAL
+from twisted.conch.insults.insults import G0, G1
+from twisted.conch.insults.insults import modes
+
+def _getattr(mock, name):
+ return super(Mock, mock).__getattribute__(name)
+
+def occurrences(mock):
+ return _getattr(mock, 'occurrences')
+
+def methods(mock):
+ return _getattr(mock, 'methods')
+
+def _append(mock, obj):
+ occurrences(mock).append(obj)
+
+default = object()
+
+class Mock(object):
+ callReturnValue = default
+
+ def __init__(self, methods=None, callReturnValue=default):
+ """
+ @param methods: Mapping of names to return values
+ @param callReturnValue: object __call__ should return
+ """
+ self.occurrences = []
+ if methods is None:
+ methods = {}
+ self.methods = methods
+ if callReturnValue is not default:
+ self.callReturnValue = callReturnValue
+
+ def __call__(self, *a, **kw):
+ returnValue = _getattr(self, 'callReturnValue')
+ if returnValue is default:
+ returnValue = Mock()
+ # _getattr(self, 'occurrences').append(('__call__', returnValue, a, kw))
+ _append(self, ('__call__', returnValue, a, kw))
+ return returnValue
+
+ def __getattribute__(self, name):
+ methods = _getattr(self, 'methods')
+ if name in methods:
+ attrValue = Mock(callReturnValue=methods[name])
+ else:
+ attrValue = Mock()
+ # _getattr(self, 'occurrences').append((name, attrValue))
+ _append(self, (name, attrValue))
+ return attrValue
+
+class MockMixin:
+ def assertCall(self, occurrence, methodName, expectedPositionalArgs=(),
+ expectedKeywordArgs={}):
+ attr, mock = occurrence
+ self.assertEqual(attr, methodName)
+ self.assertEqual(len(occurrences(mock)), 1)
+ [(call, result, args, kw)] = occurrences(mock)
+ self.assertEqual(call, "__call__")
+ self.assertEqual(args, expectedPositionalArgs)
+ self.assertEqual(kw, expectedKeywordArgs)
+ return result
+
+
+_byteGroupingTestTemplate = """\
+def testByte%(groupName)s(self):
+ transport = StringTransport()
+ proto = Mock()
+ parser = self.protocolFactory(lambda: proto)
+ parser.factory = self
+ parser.makeConnection(transport)
+
+ bytes = self.TEST_BYTES
+ while bytes:
+ chunk = bytes[:%(bytesPer)d]
+ bytes = bytes[%(bytesPer)d:]
+ parser.dataReceived(chunk)
+
+ self.verifyResults(transport, proto, parser)
+"""
+class ByteGroupingsMixin(MockMixin):
+ protocolFactory = None
+
+ for word, n in [('Pairs', 2), ('Triples', 3), ('Quads', 4), ('Quints', 5), ('Sexes', 6)]:
+ exec _byteGroupingTestTemplate % {'groupName': word, 'bytesPer': n}
+ del word, n
+
+ def verifyResults(self, transport, proto, parser):
+ result = self.assertCall(occurrences(proto).pop(0), "makeConnection", (parser,))
+ self.assertEqual(occurrences(result), [])
+
+del _byteGroupingTestTemplate
+
+class ServerArrowKeys(ByteGroupingsMixin, unittest.TestCase):
+ protocolFactory = ServerProtocol
+
+ # All the arrow keys once
+ TEST_BYTES = '\x1b[A\x1b[B\x1b[C\x1b[D'
+
+ def verifyResults(self, transport, proto, parser):
+ ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
+
+ for arrow in (parser.UP_ARROW, parser.DOWN_ARROW,
+ parser.RIGHT_ARROW, parser.LEFT_ARROW):
+ result = self.assertCall(occurrences(proto).pop(0), "keystrokeReceived", (arrow, None))
+ self.assertEqual(occurrences(result), [])
+ self.failIf(occurrences(proto))
+
+
+class PrintableCharacters(ByteGroupingsMixin, unittest.TestCase):
+ protocolFactory = ServerProtocol
+
+ # Some letters and digits, first on their own, then capitalized,
+ # then modified with alt
+
+ TEST_BYTES = 'abc123ABC!@#\x1ba\x1bb\x1bc\x1b1\x1b2\x1b3'
+
+ def verifyResults(self, transport, proto, parser):
+ ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
+
+ for char in 'abc123ABC!@#':
+ result = self.assertCall(occurrences(proto).pop(0), "keystrokeReceived", (char, None))
+ self.assertEqual(occurrences(result), [])
+
+ for char in 'abc123':
+ result = self.assertCall(occurrences(proto).pop(0), "keystrokeReceived", (char, parser.ALT))
+ self.assertEqual(occurrences(result), [])
+
+ occs = occurrences(proto)
+ self.failIf(occs, "%r should have been []" % (occs,))
+
+class ServerFunctionKeys(ByteGroupingsMixin, unittest.TestCase):
+ """Test for parsing and dispatching function keys (F1 - F12)
+ """
+ protocolFactory = ServerProtocol
+
+ byteList = []
+ for bytes in ('OP', 'OQ', 'OR', 'OS', # F1 - F4
+ '15~', '17~', '18~', '19~', # F5 - F8
+ '20~', '21~', '23~', '24~'): # F9 - F12
+ byteList.append('\x1b[' + bytes)
+ TEST_BYTES = ''.join(byteList)
+ del byteList, bytes
+
+ def verifyResults(self, transport, proto, parser):
+ ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
+ for funcNum in range(1, 13):
+ funcArg = getattr(parser, 'F%d' % (funcNum,))
+ result = self.assertCall(occurrences(proto).pop(0), "keystrokeReceived", (funcArg, None))
+ self.assertEqual(occurrences(result), [])
+ self.failIf(occurrences(proto))
+
+class ClientCursorMovement(ByteGroupingsMixin, unittest.TestCase):
+ protocolFactory = ClientProtocol
+
+ d2 = "\x1b[2B"
+ r4 = "\x1b[4C"
+ u1 = "\x1b[A"
+ l2 = "\x1b[2D"
+ # Move the cursor down two, right four, up one, left two, up one, left two
+ TEST_BYTES = d2 + r4 + u1 + l2 + u1 + l2
+ del d2, r4, u1, l2
+
+ def verifyResults(self, transport, proto, parser):
+ ByteGroupingsMixin.verifyResults(self, transport, proto, parser)
+
+ for (method, count) in [('Down', 2), ('Forward', 4), ('Up', 1),
+ ('Backward', 2), ('Up', 1), ('Backward', 2)]:
+ result = self.assertCall(occurrences(proto).pop(0), "cursor" + method, (count,))
+ self.assertEqual(occurrences(result), [])
+ self.failIf(occurrences(proto))
+
+class ClientControlSequences(unittest.TestCase, MockMixin):
+ def setUp(self):
+ self.transport = StringTransport()
+ self.proto = Mock()
+ self.parser = ClientProtocol(lambda: self.proto)
+ self.parser.factory = self
+ self.parser.makeConnection(self.transport)
+ result = self.assertCall(occurrences(self.proto).pop(0), "makeConnection", (self.parser,))
+ self.failIf(occurrences(result))
+
+ def testSimpleCardinals(self):
+ self.parser.dataReceived(
+ ''.join([''.join(['\x1b[' + str(n) + ch for n in ('', 2, 20, 200)]) for ch in 'BACD']))
+ occs = occurrences(self.proto)
+
+ for meth in ("Down", "Up", "Forward", "Backward"):
+ for count in (1, 2, 20, 200):
+ result = self.assertCall(occs.pop(0), "cursor" + meth, (count,))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testScrollRegion(self):
+ self.parser.dataReceived('\x1b[5;22r\x1b[r')
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "setScrollRegion", (5, 22))
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "setScrollRegion", (None, None))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testHeightAndWidth(self):
+ self.parser.dataReceived("\x1b#3\x1b#4\x1b#5\x1b#6")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "doubleHeightLine", (True,))
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "doubleHeightLine", (False,))
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "singleWidthLine")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "doubleWidthLine")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testCharacterSet(self):
+ self.parser.dataReceived(
+ ''.join([''.join(['\x1b' + g + n for n in 'AB012']) for g in '()']))
+ occs = occurrences(self.proto)
+
+ for which in (G0, G1):
+ for charset in (CS_UK, CS_US, CS_DRAWING, CS_ALTERNATE, CS_ALTERNATE_SPECIAL):
+ result = self.assertCall(occs.pop(0), "selectCharacterSet", (charset, which))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testShifting(self):
+ self.parser.dataReceived("\x15\x14")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "shiftIn")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "shiftOut")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testSingleShifts(self):
+ self.parser.dataReceived("\x1bN\x1bO")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "singleShift2")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "singleShift3")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testKeypadMode(self):
+ self.parser.dataReceived("\x1b=\x1b>")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "applicationKeypadMode")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "numericKeypadMode")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testCursor(self):
+ self.parser.dataReceived("\x1b7\x1b8")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "saveCursor")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "restoreCursor")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testReset(self):
+ self.parser.dataReceived("\x1bc")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "reset")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testIndex(self):
+ self.parser.dataReceived("\x1bD\x1bM\x1bE")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "index")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "reverseIndex")
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "nextLine")
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testModes(self):
+ self.parser.dataReceived(
+ "\x1b[" + ';'.join(map(str, [modes.KAM, modes.IRM, modes.LNM])) + "h")
+ self.parser.dataReceived(
+ "\x1b[" + ';'.join(map(str, [modes.KAM, modes.IRM, modes.LNM])) + "l")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "setModes", ([modes.KAM, modes.IRM, modes.LNM],))
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "resetModes", ([modes.KAM, modes.IRM, modes.LNM],))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testErasure(self):
+ self.parser.dataReceived(
+ "\x1b[K\x1b[1K\x1b[2K\x1b[J\x1b[1J\x1b[2J\x1b[3P")
+ occs = occurrences(self.proto)
+
+ for meth in ("eraseToLineEnd", "eraseToLineBeginning", "eraseLine",
+ "eraseToDisplayEnd", "eraseToDisplayBeginning",
+ "eraseDisplay"):
+ result = self.assertCall(occs.pop(0), meth)
+ self.failIf(occurrences(result))
+
+ result = self.assertCall(occs.pop(0), "deleteCharacter", (3,))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testLineDeletion(self):
+ self.parser.dataReceived("\x1b[M\x1b[3M")
+ occs = occurrences(self.proto)
+
+ for arg in (1, 3):
+ result = self.assertCall(occs.pop(0), "deleteLine", (arg,))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testLineInsertion(self):
+ self.parser.dataReceived("\x1b[L\x1b[3L")
+ occs = occurrences(self.proto)
+
+ for arg in (1, 3):
+ result = self.assertCall(occs.pop(0), "insertLine", (arg,))
+ self.failIf(occurrences(result))
+ self.failIf(occs)
+
+ def testCursorPosition(self):
+ methods(self.proto)['reportCursorPosition'] = (6, 7)
+ self.parser.dataReceived("\x1b[6n")
+ self.assertEqual(self.transport.value(), "\x1b[7;8R")
+ occs = occurrences(self.proto)
+
+ result = self.assertCall(occs.pop(0), "reportCursorPosition")
+ # This isn't really an interesting assert, since it only tests that
+ # our mock setup is working right, but I'll include it anyway.
+ self.assertEqual(result, (6, 7))
+
+
+ def test_applicationDataBytes(self):
+ """
+ Contiguous non-control bytes are passed to a single call to the
+ C{write} method of the terminal to which the L{ClientProtocol} is
+ connected.
+ """
+ occs = occurrences(self.proto)
+ self.parser.dataReceived('a')
+ self.assertCall(occs.pop(0), "write", ("a",))
+ self.parser.dataReceived('bc')
+ self.assertCall(occs.pop(0), "write", ("bc",))
+
+
+ def _applicationDataTest(self, data, calls):
+ occs = occurrences(self.proto)
+ self.parser.dataReceived(data)
+ while calls:
+ self.assertCall(occs.pop(0), *calls.pop(0))
+ self.assertFalse(occs, "No other calls should happen: %r" % (occs,))
+
+
+ def test_shiftInAfterApplicationData(self):
+ """
+ Application data bytes followed by a shift-in command are passed to a
+ call to C{write} before the terminal's C{shiftIn} method is called.
+ """
+ self._applicationDataTest(
+ 'ab\x15', [
+ ("write", ("ab",)),
+ ("shiftIn",)])
+
+
+ def test_shiftOutAfterApplicationData(self):
+ """
+ Application data bytes followed by a shift-out command are passed to a
+ call to C{write} before the terminal's C{shiftOut} method is called.
+ """
+ self._applicationDataTest(
+ 'ab\x14', [
+ ("write", ("ab",)),
+ ("shiftOut",)])
+
+
+ def test_cursorBackwardAfterApplicationData(self):
+ """
+ Application data bytes followed by a cursor-backward command are passed
+ to a call to C{write} before the terminal's C{cursorBackward} method is
+ called.
+ """
+ self._applicationDataTest(
+ 'ab\x08', [
+ ("write", ("ab",)),
+ ("cursorBackward",)])
+
+
+ def test_escapeAfterApplicationData(self):
+ """
+ Application data bytes followed by an escape character are passed to a
+ call to C{write} before the terminal's handler method for the escape is
+ called.
+ """
+ # Test a short escape
+ self._applicationDataTest(
+ 'ab\x1bD', [
+ ("write", ("ab",)),
+ ("index",)])
+
+ # And a long escape
+ self._applicationDataTest(
+ 'ab\x1b[4h', [
+ ("write", ("ab",)),
+ ("setModes", ([4],))])
+
+ # There's some other cases too, but they're all handled by the same
+ # codepaths as above.
+
+
+
+class ServerProtocolOutputTests(unittest.TestCase):
+ """
+ Tests for the bytes L{ServerProtocol} writes to its transport when its
+ methods are called.
+ """
+ def test_nextLine(self):
+ """
+ L{ServerProtocol.nextLine} writes C{"\r\n"} to its transport.
+ """
+ # Why doesn't it write ESC E? Because ESC E is poorly supported. For
+ # example, gnome-terminal (many different versions) fails to scroll if
+ # it receives ESC E and the cursor is already on the last row.
+ protocol = ServerProtocol()
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.nextLine()
+ self.assertEqual(transport.value(), "\r\n")
+
+
+
+class Deprecations(unittest.TestCase):
+ """
+ Tests to ensure deprecation of L{insults.colors} and L{insults.client}
+ """
+
+ def ensureDeprecated(self, message):
+ """
+ Ensures that the correct deprecation warning was issued.
+ """
+ warnings = self.flushWarnings()
+ self.assertIdentical(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(warnings[0]['message'], message)
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_colors(self):
+ """
+ The L{insults.colors} module is deprecated
+ """
+ from twisted.conch.insults import colors
+ self.ensureDeprecated("twisted.conch.insults.colors was deprecated "
+ "in Twisted 10.1.0: Please use "
+ "twisted.conch.insults.helper instead.")
+
+
+ def test_client(self):
+ """
+ The L{insults.client} module is deprecated
+ """
+ from twisted.conch.insults import client
+ self.ensureDeprecated("twisted.conch.insults.client was deprecated "
+ "in Twisted 10.1.0: Please use "
+ "twisted.conch.insults.insults instead.")
diff --git a/twisted/conch/test/test_keys.py b/twisted/conch/test/test_keys.py
new file mode 100644
index 0000000..2f84006
--- /dev/null
+++ b/twisted/conch/test/test_keys.py
@@ -0,0 +1,488 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.ssh.keys}.
+"""
+
+try:
+ import Crypto.Cipher.DES3
+except ImportError:
+ # we'll have to skip these tests without PyCypto and pyasn1
+ Crypto = None
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1 = None
+
+if Crypto and pyasn1:
+ from twisted.conch.ssh import keys, common, sexpy
+
+import os, base64
+from twisted.conch.test import keydata
+from twisted.python import randbytes
+from twisted.python.hashlib import sha1
+from twisted.trial import unittest
+
+
+class HelpersTestCase(unittest.TestCase):
+
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+ def setUp(self):
+ self._secureRandom = randbytes.secureRandom
+ randbytes.secureRandom = lambda x: '\x55' * x
+
+ def tearDown(self):
+ randbytes.secureRandom = self._secureRandom
+ self._secureRandom = None
+
+ def test_pkcs1(self):
+ """
+ Test Public Key Cryptographic Standard #1 functions.
+ """
+ data = 'ABC'
+ messageSize = 6
+ self.assertEqual(keys.pkcs1Pad(data, messageSize),
+ '\x01\xff\x00ABC')
+ hash = sha1().digest()
+ messageSize = 40
+ self.assertEqual(keys.pkcs1Digest('', messageSize),
+ '\x01\xff\xff\xff\x00' + keys.ID_SHA1 + hash)
+
+ def _signRSA(self, data):
+ key = keys.Key.fromString(keydata.privateRSA_openssh)
+ sig = key.sign(data)
+ return key.keyObject, sig
+
+ def _signDSA(self, data):
+ key = keys.Key.fromString(keydata.privateDSA_openssh)
+ sig = key.sign(data)
+ return key.keyObject, sig
+
+ def test_signRSA(self):
+ """
+ Test that RSA keys return appropriate signatures.
+ """
+ data = 'data'
+ key, sig = self._signRSA(data)
+ sigData = keys.pkcs1Digest(data, keys.lenSig(key))
+ v = key.sign(sigData, '')[0]
+ self.assertEqual(sig, common.NS('ssh-rsa') + common.MP(v))
+ return key, sig
+
+ def test_signDSA(self):
+ """
+ Test that DSA keys return appropriate signatures.
+ """
+ data = 'data'
+ key, sig = self._signDSA(data)
+ sigData = sha1(data).digest()
+ v = key.sign(sigData, '\x55' * 19)
+ self.assertEqual(sig, common.NS('ssh-dss') + common.NS(
+ Crypto.Util.number.long_to_bytes(v[0], 20) +
+ Crypto.Util.number.long_to_bytes(v[1], 20)))
+ return key, sig
+
+
+ def test_objectType(self):
+ """
+ Test that objectType, returns the correct type for objects.
+ """
+ self.assertEqual(keys.objectType(keys.Key.fromString(
+ keydata.privateRSA_openssh).keyObject), 'ssh-rsa')
+ self.assertEqual(keys.objectType(keys.Key.fromString(
+ keydata.privateDSA_openssh).keyObject), 'ssh-dss')
+ self.assertRaises(keys.BadKeyError, keys.objectType, None)
+
+
+class KeyTestCase(unittest.TestCase):
+
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+ def setUp(self):
+ self.rsaObj = Crypto.PublicKey.RSA.construct((1L, 2L, 3L, 4L, 5L))
+ self.dsaObj = Crypto.PublicKey.DSA.construct((1L, 2L, 3L, 4L, 5L))
+ self.rsaSignature = ('\x00\x00\x00\x07ssh-rsa\x00'
+ '\x00\x00`N\xac\xb4@qK\xa0(\xc3\xf2h \xd3\xdd\xee6Np\x9d_'
+ '\xb0>\xe3\x0c(L\x9d{\txUd|!\xf6m\x9c\xd3\x93\x842\x7fU'
+ '\x05\xf4\xf7\xfaD\xda\xce\x81\x8ea\x7f=Y\xed*\xb7\xba\x81'
+ '\xf2\xad\xda\xeb(\x97\x03S\x08\x81\xc7\xb1\xb7\xe6\xe3'
+ '\xcd*\xd4\xbd\xc0wt\xf7y\xcd\xf0\xb7\x7f\xfb\x1e>\xf9r'
+ '\x8c\xba')
+ self.dsaSignature = ('\x00\x00\x00\x07ssh-dss\x00\x00'
+ '\x00(\x18z)H\x8a\x1b\xc6\r\xbbq\xa2\xd7f\x7f$\xa7\xbf'
+ '\xe8\x87\x8c\x88\xef\xd9k\x1a\x98\xdd{=\xdec\x18\t\xe3'
+ '\x87\xa9\xc72h\x95')
+ self.oldSecureRandom = randbytes.secureRandom
+ randbytes.secureRandom = lambda x: '\xff' * x
+ self.keyFile = self.mktemp()
+ file(self.keyFile, 'wb').write(keydata.privateRSA_lsh)
+
+ def tearDown(self):
+ randbytes.secureRandom = self.oldSecureRandom
+ del self.oldSecureRandom
+ os.unlink(self.keyFile)
+
+ def test__guessStringType(self):
+ """
+ Test that the _guessStringType method guesses string types
+ correctly.
+ """
+ self.assertEqual(keys.Key._guessStringType(keydata.publicRSA_openssh),
+ 'public_openssh')
+ self.assertEqual(keys.Key._guessStringType(keydata.publicDSA_openssh),
+ 'public_openssh')
+ self.assertEqual(keys.Key._guessStringType(
+ keydata.privateRSA_openssh), 'private_openssh')
+ self.assertEqual(keys.Key._guessStringType(
+ keydata.privateDSA_openssh), 'private_openssh')
+ self.assertEqual(keys.Key._guessStringType(keydata.publicRSA_lsh),
+ 'public_lsh')
+ self.assertEqual(keys.Key._guessStringType(keydata.publicDSA_lsh),
+ 'public_lsh')
+ self.assertEqual(keys.Key._guessStringType(keydata.privateRSA_lsh),
+ 'private_lsh')
+ self.assertEqual(keys.Key._guessStringType(keydata.privateDSA_lsh),
+ 'private_lsh')
+ self.assertEqual(keys.Key._guessStringType(
+ keydata.privateRSA_agentv3), 'agentv3')
+ self.assertEqual(keys.Key._guessStringType(
+ keydata.privateDSA_agentv3), 'agentv3')
+ self.assertEqual(keys.Key._guessStringType(
+ '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'),
+ 'blob')
+ self.assertEqual(keys.Key._guessStringType(
+ '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'),
+ 'blob')
+ self.assertEqual(keys.Key._guessStringType('not a key'),
+ None)
+
+ def _testPublicPrivateFromString(self, public, private, type, data):
+ self._testPublicFromString(public, type, data)
+ self._testPrivateFromString(private, type, data)
+
+ def _testPublicFromString(self, public, type, data):
+ publicKey = keys.Key.fromString(public)
+ self.assertTrue(publicKey.isPublic())
+ self.assertEqual(publicKey.type(), type)
+ for k, v in publicKey.data().items():
+ self.assertEqual(data[k], v)
+
+ def _testPrivateFromString(self, private, type, data):
+ privateKey = keys.Key.fromString(private)
+ self.assertFalse(privateKey.isPublic())
+ self.assertEqual(privateKey.type(), type)
+ for k, v in data.items():
+ self.assertEqual(privateKey.data()[k], v)
+
+ def test_fromOpenSSH(self):
+ """
+ Test that keys are correctly generated from OpenSSH strings.
+ """
+ self._testPublicPrivateFromString(keydata.publicRSA_openssh,
+ keydata.privateRSA_openssh, 'RSA', keydata.RSAData)
+ self.assertEqual(keys.Key.fromString(
+ keydata.privateRSA_openssh_encrypted,
+ passphrase='encrypted'),
+ keys.Key.fromString(keydata.privateRSA_openssh))
+ self.assertEqual(keys.Key.fromString(
+ keydata.privateRSA_openssh_alternate),
+ keys.Key.fromString(keydata.privateRSA_openssh))
+ self._testPublicPrivateFromString(keydata.publicDSA_openssh,
+ keydata.privateDSA_openssh, 'DSA', keydata.DSAData)
+
+ def test_fromOpenSSH_with_whitespace(self):
+ """
+ If key strings have trailing whitespace, it should be ignored.
+ """
+ # from bug #3391, since our test key data doesn't have
+ # an issue with appended newlines
+ privateDSAData = """-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQDylESNuc61jq2yatCzZbenlr9llG+p9LhIpOLUbXhhHcwC6hrh
+EZIdCKqTO0USLrGoP5uS9UHAUoeN62Z0KXXWTwOWGEQn/syyPzNJtnBorHpNUT9D
+Qzwl1yUa53NNgEctpo4NoEFOx8PuU6iFLyvgHCjNn2MsuGuzkZm7sI9ZpQIVAJiR
+9dPc08KLdpJyRxz8T74b4FQRAoGAGBc4Z5Y6R/HZi7AYM/iNOM8su6hrk8ypkBwR
+a3Dbhzk97fuV3SF1SDrcQu4zF7c4CtH609N5nfZs2SUjLLGPWln83Ysb8qhh55Em
+AcHXuROrHS/sDsnqu8FQp86MaudrqMExCOYyVPE7jaBWW+/JWFbKCxmgOCSdViUJ
+esJpBFsCgYEA7+jtVvSt9yrwsS/YU1QGP5wRAiDYB+T5cK4HytzAqJKRdC5qS4zf
+C7R0eKcDHHLMYO39aPnCwXjscisnInEhYGNblTDyPyiyNxAOXuC8x7luTmwzMbNJ
+/ow0IqSj0VF72VJN9uSoPpFd4lLT0zN8v42RWja0M8ohWNf+YNJluPgCFE0PT4Vm
+SUrCyZXsNh6VXwjs3gKQ
+-----END DSA PRIVATE KEY-----"""
+ self.assertEqual(keys.Key.fromString(privateDSAData),
+ keys.Key.fromString(privateDSAData + '\n'))
+
+ def test_fromLSH(self):
+ """
+ Test that keys are correctly generated from LSH strings.
+ """
+ self._testPublicPrivateFromString(keydata.publicRSA_lsh,
+ keydata.privateRSA_lsh, 'RSA', keydata.RSAData)
+ self._testPublicPrivateFromString(keydata.publicDSA_lsh,
+ keydata.privateDSA_lsh, 'DSA', keydata.DSAData)
+ sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]])
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString,
+ data='{'+base64.encodestring(sexp)+'}')
+ sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]])
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString,
+ sexp)
+
+ def test_fromAgentv3(self):
+ """
+ Test that keys are correctly generated from Agent v3 strings.
+ """
+ self._testPrivateFromString(keydata.privateRSA_agentv3, 'RSA',
+ keydata.RSAData)
+ self._testPrivateFromString(keydata.privateDSA_agentv3, 'DSA',
+ keydata.DSAData)
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString,
+ '\x00\x00\x00\x07ssh-foo'+'\x00\x00\x00\x01\x01'*5)
+
+ def test_fromStringErrors(self):
+ """
+ keys.Key.fromString should raise BadKeyError when the key is invalid.
+ """
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString, '')
+ # no key data with a bad key type
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString, '',
+ 'bad_type')
+ # trying to decrypt a key which doesn't support encryption
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString,
+ keydata.publicRSA_lsh, passphrase = 'unencrypted')
+ # trying to decrypt an unencrypted key
+ self.assertRaises(keys.EncryptedKeyError, keys.Key.fromString,
+ keys.Key(self.rsaObj).toString('openssh', 'encrypted'))
+ # key with no key data
+ self.assertRaises(keys.BadKeyError, keys.Key.fromString,
+ '-----BEGIN RSA KEY-----\nwA==\n')
+
+ def test_fromFile(self):
+ """
+ Test that fromFile works correctly.
+ """
+ self.assertEqual(keys.Key.fromFile(self.keyFile),
+ keys.Key.fromString(keydata.privateRSA_lsh))
+ self.assertRaises(keys.BadKeyError, keys.Key.fromFile,
+ self.keyFile, 'bad_type')
+ self.assertRaises(keys.BadKeyError, keys.Key.fromFile,
+ self.keyFile, passphrase='unencrypted')
+
+ def test_init(self):
+ """
+ Test that the PublicKey object is initialized correctly.
+ """
+ obj = Crypto.PublicKey.RSA.construct((1L, 2L))
+ key = keys.Key(obj)
+ self.assertEqual(key.keyObject, obj)
+
+ def test_equal(self):
+ """
+ Test that Key objects are compared correctly.
+ """
+ rsa1 = keys.Key(self.rsaObj)
+ rsa2 = keys.Key(self.rsaObj)
+ rsa3 = keys.Key(Crypto.PublicKey.RSA.construct((1L, 2L)))
+ dsa = keys.Key(self.dsaObj)
+ self.assertTrue(rsa1 == rsa2)
+ self.assertFalse(rsa1 == rsa3)
+ self.assertFalse(rsa1 == dsa)
+ self.assertFalse(rsa1 == object)
+ self.assertFalse(rsa1 == None)
+
+ def test_notEqual(self):
+ """
+ Test that Key objects are not-compared correctly.
+ """
+ rsa1 = keys.Key(self.rsaObj)
+ rsa2 = keys.Key(self.rsaObj)
+ rsa3 = keys.Key(Crypto.PublicKey.RSA.construct((1L, 2L)))
+ dsa = keys.Key(self.dsaObj)
+ self.assertFalse(rsa1 != rsa2)
+ self.assertTrue(rsa1 != rsa3)
+ self.assertTrue(rsa1 != dsa)
+ self.assertTrue(rsa1 != object)
+ self.assertTrue(rsa1 != None)
+
+ def test_type(self):
+ """
+ Test that the type method returns the correct type for an object.
+ """
+ self.assertEqual(keys.Key(self.rsaObj).type(), 'RSA')
+ self.assertEqual(keys.Key(self.rsaObj).sshType(), 'ssh-rsa')
+ self.assertEqual(keys.Key(self.dsaObj).type(), 'DSA')
+ self.assertEqual(keys.Key(self.dsaObj).sshType(), 'ssh-dss')
+ self.assertRaises(RuntimeError, keys.Key(None).type)
+ self.assertRaises(RuntimeError, keys.Key(None).sshType)
+ self.assertRaises(RuntimeError, keys.Key(self).type)
+ self.assertRaises(RuntimeError, keys.Key(self).sshType)
+
+ def test_fromBlob(self):
+ """
+ Test that a public key is correctly generated from a public key blob.
+ """
+ rsaBlob = common.NS('ssh-rsa') + common.MP(2) + common.MP(3)
+ rsaKey = keys.Key.fromString(rsaBlob)
+ dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) +
+ common.MP(4) + common.MP(5))
+ dsaKey = keys.Key.fromString(dsaBlob)
+ badBlob = common.NS('ssh-bad')
+ self.assertTrue(rsaKey.isPublic())
+ self.assertEqual(rsaKey.data(), {'e':2L, 'n':3L})
+ self.assertTrue(dsaKey.isPublic())
+ self.assertEqual(dsaKey.data(), {'p':2L, 'q':3L, 'g':4L, 'y':5L})
+ self.assertRaises(keys.BadKeyError,
+ keys.Key.fromString, badBlob)
+
+
+ def test_fromPrivateBlob(self):
+ """
+ Test that a private key is correctly generated from a private key blob.
+ """
+ rsaBlob = (common.NS('ssh-rsa') + common.MP(2) + common.MP(3) +
+ common.MP(4) + common.MP(5) + common.MP(6) + common.MP(7))
+ rsaKey = keys.Key._fromString_PRIVATE_BLOB(rsaBlob)
+ dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) +
+ common.MP(4) + common.MP(5) + common.MP(6))
+ dsaKey = keys.Key._fromString_PRIVATE_BLOB(dsaBlob)
+ badBlob = common.NS('ssh-bad')
+ self.assertFalse(rsaKey.isPublic())
+ self.assertEqual(
+ rsaKey.data(), {'n':2L, 'e':3L, 'd':4L, 'u':5L, 'p':6L, 'q':7L})
+ self.assertFalse(dsaKey.isPublic())
+ self.assertEqual(dsaKey.data(), {'p':2L, 'q':3L, 'g':4L, 'y':5L, 'x':6L})
+ self.assertRaises(
+ keys.BadKeyError, keys.Key._fromString_PRIVATE_BLOB, badBlob)
+
+
+ def test_blob(self):
+ """
+ Test that the Key object generates blobs correctly.
+ """
+ self.assertEqual(keys.Key(self.rsaObj).blob(),
+ '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x02'
+ '\x00\x00\x00\x01\x01')
+ self.assertEqual(keys.Key(self.dsaObj).blob(),
+ '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x03'
+ '\x00\x00\x00\x01\x04\x00\x00\x00\x01\x02'
+ '\x00\x00\x00\x01\x01')
+
+ badKey = keys.Key(None)
+ self.assertRaises(RuntimeError, badKey.blob)
+
+
+ def test_privateBlob(self):
+ """
+ L{Key.privateBlob} returns the SSH protocol-level format of the private
+ key and raises L{RuntimeError} if the underlying key object is invalid.
+ """
+ self.assertEqual(keys.Key(self.rsaObj).privateBlob(),
+ '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'
+ '\x00\x00\x00\x01\x02\x00\x00\x00\x01\x03\x00'
+ '\x00\x00\x01\x04\x00\x00\x00\x01\x04\x00\x00'
+ '\x00\x01\x05')
+ self.assertEqual(keys.Key(self.dsaObj).privateBlob(),
+ '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x03'
+ '\x00\x00\x00\x01\x04\x00\x00\x00\x01\x02\x00'
+ '\x00\x00\x01\x01\x00\x00\x00\x01\x05')
+
+ badKey = keys.Key(None)
+ self.assertRaises(RuntimeError, badKey.privateBlob)
+
+
+ def test_toOpenSSH(self):
+ """
+ Test that the Key object generates OpenSSH keys correctly.
+ """
+ key = keys.Key.fromString(keydata.privateRSA_lsh)
+ self.assertEqual(key.toString('openssh'), keydata.privateRSA_openssh)
+ self.assertEqual(key.toString('openssh', 'encrypted'),
+ keydata.privateRSA_openssh_encrypted)
+ self.assertEqual(key.public().toString('openssh'),
+ keydata.publicRSA_openssh[:-8]) # no comment
+ self.assertEqual(key.public().toString('openssh', 'comment'),
+ keydata.publicRSA_openssh)
+ key = keys.Key.fromString(keydata.privateDSA_lsh)
+ self.assertEqual(key.toString('openssh'), keydata.privateDSA_openssh)
+ self.assertEqual(key.public().toString('openssh', 'comment'),
+ keydata.publicDSA_openssh)
+ self.assertEqual(key.public().toString('openssh'),
+ keydata.publicDSA_openssh[:-8]) # no comment
+
+ def test_toLSH(self):
+ """
+ Test that the Key object generates LSH keys correctly.
+ """
+ key = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.assertEqual(key.toString('lsh'), keydata.privateRSA_lsh)
+ self.assertEqual(key.public().toString('lsh'),
+ keydata.publicRSA_lsh)
+ key = keys.Key.fromString(keydata.privateDSA_openssh)
+ self.assertEqual(key.toString('lsh'), keydata.privateDSA_lsh)
+ self.assertEqual(key.public().toString('lsh'),
+ keydata.publicDSA_lsh)
+
+ def test_toAgentv3(self):
+ """
+ Test that the Key object generates Agent v3 keys correctly.
+ """
+ key = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.assertEqual(key.toString('agentv3'), keydata.privateRSA_agentv3)
+ key = keys.Key.fromString(keydata.privateDSA_openssh)
+ self.assertEqual(key.toString('agentv3'), keydata.privateDSA_agentv3)
+
+ def test_toStringErrors(self):
+ """
+ Test that toString raises errors appropriately.
+ """
+ self.assertRaises(keys.BadKeyError, keys.Key(self.rsaObj).toString,
+ 'bad_type')
+
+ def test_sign(self):
+ """
+ Test that the Key object generates correct signatures.
+ """
+ key = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.assertEqual(key.sign(''), self.rsaSignature)
+ key = keys.Key.fromString(keydata.privateDSA_openssh)
+ self.assertEqual(key.sign(''), self.dsaSignature)
+
+
+ def test_verify(self):
+ """
+ Test that the Key object correctly verifies signatures.
+ """
+ key = keys.Key.fromString(keydata.publicRSA_openssh)
+ self.assertTrue(key.verify(self.rsaSignature, ''))
+ self.assertFalse(key.verify(self.rsaSignature, 'a'))
+ self.assertFalse(key.verify(self.dsaSignature, ''))
+ key = keys.Key.fromString(keydata.publicDSA_openssh)
+ self.assertTrue(key.verify(self.dsaSignature, ''))
+ self.assertFalse(key.verify(self.dsaSignature, 'a'))
+ self.assertFalse(key.verify(self.rsaSignature, ''))
+
+ def test_repr(self):
+ """
+ Test the pretty representation of Key.
+ """
+ self.assertEqual(repr(keys.Key(self.rsaObj)),
+"""<RSA Private Key (0 bits)
+attr e:
+\t02
+attr d:
+\t03
+attr n:
+\t01
+attr q:
+\t05
+attr p:
+\t04
+attr u:
+\t04>""")
diff --git a/twisted/conch/test/test_knownhosts.py b/twisted/conch/test/test_knownhosts.py
new file mode 100644
index 0000000..aac3999
--- /dev/null
+++ b/twisted/conch/test/test_knownhosts.py
@@ -0,0 +1,979 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.client.knownhosts}.
+"""
+
+import os
+from binascii import Error as BinasciiError, b2a_base64, a2b_base64
+
+try:
+ import Crypto
+ import pyasn1
+except ImportError:
+ skip = "PyCrypto and PyASN1 required for twisted.conch.knownhosts."
+else:
+ from twisted.conch.ssh.keys import Key, BadKeyError
+ from twisted.conch.client.knownhosts import \
+ PlainEntry, HashedEntry, KnownHostsFile, UnparsedEntry, ConsoleUI
+ from twisted.conch.client import default
+
+from zope.interface.verify import verifyObject
+
+from twisted.python.filepath import FilePath
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import Deferred
+from twisted.conch.interfaces import IKnownHostEntry
+from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry
+
+
+sampleEncodedKey = (
+ 'AAAAB3NzaC1yc2EAAAABIwAAAQEAsV0VMRbGmzhqxxayLRHmvnFvtyNqgbNKV46dU1bVFB+3y'
+ 'tNvue4Riqv/SVkPRNwMb7eWH29SviXaBxUhYyzKkDoNUq3rTNnH1Vnif6d6X4JCrUb5d3W+Dm'
+ 'YClyJrZ5HgD/hUpdSkTRqdbQ2TrvSAxRacj+vHHT4F4dm1bJSewm3B2D8HVOoi/CbVh3dsIiC'
+ 'dp8VltdZx4qYVfYe2LwVINCbAa3d3tj9ma7RVfw3OH2Mfb+toLd1N5tBQFb7oqTt2nC6I/6Bd'
+ '4JwPUld+IEitw/suElq/AIJVQXXujeyiZlea90HE65U2mF1ytr17HTAIT2ySokJWyuBANGACk'
+ '6iIaw==')
+
+otherSampleEncodedKey = (
+ 'AAAAB3NzaC1yc2EAAAABIwAAAIEAwaeCZd3UCuPXhX39+/p9qO028jTF76DMVd9mPvYVDVXuf'
+ 'WckKZauF7+0b7qm+ChT7kan6BzRVo4++gCVNfAlMzLysSt3ylmOR48tFpAfygg9UCX3DjHz0E'
+ 'lOOUKh3iifc9aUShD0OPaK3pR5JJ8jfiBfzSYWt/hDi/iZ4igsSs8=')
+
+thirdSampleEncodedKey = (
+ 'AAAAB3NzaC1yc2EAAAABIwAAAQEAl/TQakPkePlnwCBRPitIVUTg6Z8VzN1en+DGkyo/evkmLw'
+ '7o4NWR5qbysk9A9jXW332nxnEuAnbcCam9SHe1su1liVfyIK0+3bdn0YRB0sXIbNEtMs2LtCho'
+ '/aV3cXPS+Cf1yut3wvIpaRnAzXxuKPCTXQ7/y0IXa8TwkRBH58OJa3RqfQ/NsSp5SAfdsrHyH2'
+ 'aitiVKm2jfbTKzSEqOQG/zq4J9GXTkq61gZugory/Tvl5/yPgSnOR6C9jVOMHf27ZPoRtyj9SY'
+ '343Hd2QHiIE0KPZJEgCynKeWoKz8v6eTSK8n4rBnaqWdp8MnGZK1WGy05MguXbyCDuTC8AmJXQ'
+ '==')
+
+sampleKey = a2b_base64(sampleEncodedKey)
+otherSampleKey = a2b_base64(otherSampleEncodedKey)
+thirdSampleKey = a2b_base64(thirdSampleEncodedKey)
+
+samplePlaintextLine = (
+ "www.twistedmatrix.com ssh-rsa " + sampleEncodedKey + "\n")
+
+otherSamplePlaintextLine = (
+ "divmod.com ssh-rsa " + otherSampleEncodedKey + "\n")
+
+sampleHostIPLine = (
+ "www.twistedmatrix.com,198.49.126.131 ssh-rsa " + sampleEncodedKey + "\n")
+
+sampleHashedLine = (
+ "|1|gJbSEPBG9ZSBoZpHNtZBD1bHKBA=|bQv+0Xa0dByrwkA1EB0E7Xop/Fo= ssh-rsa " +
+ sampleEncodedKey + "\n")
+
+
+
+class EntryTestsMixin:
+ """
+ Tests for implementations of L{IKnownHostEntry}. Subclasses must set the
+ 'entry' attribute to a provider of that interface, the implementation of
+ that interface under test.
+
+ @ivar entry: a provider of L{IKnownHostEntry} with a hostname of
+ www.twistedmatrix.com and an RSA key of sampleKey.
+ """
+
+ def test_providesInterface(self):
+ """
+ The given entry should provide IKnownHostEntry.
+ """
+ verifyObject(IKnownHostEntry, self.entry)
+
+
+ def test_fromString(self):
+ """
+ Constructing a plain text entry from an unhashed known_hosts entry will
+ result in an L{IKnownHostEntry} provider with 'keyString', 'hostname',
+ and 'keyType' attributes. While outside the interface in question,
+ these attributes are held in common by L{PlainEntry} and L{HashedEntry}
+ implementations; other implementations should override this method in
+ subclasses.
+ """
+ entry = self.entry
+ self.assertEqual(entry.publicKey, Key.fromString(sampleKey))
+ self.assertEqual(entry.keyType, "ssh-rsa")
+
+
+ def test_matchesKey(self):
+ """
+ L{IKnownHostEntry.matchesKey} checks to see if an entry matches a given
+ SSH key.
+ """
+ twistedmatrixDotCom = Key.fromString(sampleKey)
+ divmodDotCom = Key.fromString(otherSampleKey)
+ self.assertEqual(
+ True,
+ self.entry.matchesKey(twistedmatrixDotCom))
+ self.assertEqual(
+ False,
+ self.entry.matchesKey(divmodDotCom))
+
+
+ def test_matchesHost(self):
+ """
+ L{IKnownHostEntry.matchesHost} checks to see if an entry matches a
+ given hostname.
+ """
+ self.assertEqual(True, self.entry.matchesHost(
+ "www.twistedmatrix.com"))
+ self.assertEqual(False, self.entry.matchesHost(
+ "www.divmod.com"))
+
+
+
+class PlainEntryTests(EntryTestsMixin, TestCase):
+ """
+ Test cases for L{PlainEntry}.
+ """
+ plaintextLine = samplePlaintextLine
+ hostIPLine = sampleHostIPLine
+
+ def setUp(self):
+ """
+ Set 'entry' to a sample plain-text entry with sampleKey as its key.
+ """
+ self.entry = PlainEntry.fromString(self.plaintextLine)
+
+
+ def test_matchesHostIP(self):
+ """
+ A "hostname,ip" formatted line will match both the host and the IP.
+ """
+ self.entry = PlainEntry.fromString(self.hostIPLine)
+ self.assertEqual(True, self.entry.matchesHost("198.49.126.131"))
+ self.test_matchesHost()
+
+
+ def test_toString(self):
+ """
+ L{PlainEntry.toString} generates the serialized OpenSSL format string
+ for the entry, sans newline.
+ """
+ self.assertEqual(self.entry.toString(), self.plaintextLine.rstrip("\n"))
+ multiHostEntry = PlainEntry.fromString(self.hostIPLine)
+ self.assertEqual(multiHostEntry.toString(), self.hostIPLine.rstrip("\n"))
+
+
+
+class PlainTextWithCommentTests(PlainEntryTests):
+ """
+ Test cases for L{PlainEntry} when parsed from a line with a comment.
+ """
+
+ plaintextLine = samplePlaintextLine[:-1] + " plain text comment.\n"
+ hostIPLine = sampleHostIPLine[:-1] + " text following host/IP line\n"
+
+
+
+class HashedEntryTests(EntryTestsMixin, TestCase):
+ """
+ Tests for L{HashedEntry}.
+
+ This suite doesn't include any tests for host/IP pairs because hashed
+ entries store IP addresses the same way as hostnames and does not support
+ comma-separated lists. (If you hash the IP and host together you can't
+ tell if you've got the key already for one or the other.)
+ """
+ hashedLine = sampleHashedLine
+
+ def setUp(self):
+ """
+ Set 'entry' to a sample hashed entry for twistedmatrix.com with
+ sampleKey as its key.
+ """
+ self.entry = HashedEntry.fromString(self.hashedLine)
+
+
+ def test_toString(self):
+ """
+ L{HashedEntry.toString} generates the serialized OpenSSL format string
+ for the entry, sans the newline.
+ """
+ self.assertEqual(self.entry.toString(), self.hashedLine.rstrip("\n"))
+
+
+
+class HashedEntryWithCommentTests(HashedEntryTests):
+ """
+ Test cases for L{PlainEntry} when parsed from a line with a comment.
+ """
+
+ hashedLine = sampleHashedLine[:-1] + " plain text comment.\n"
+
+
+
+class UnparsedEntryTests(TestCase, EntryTestsMixin):
+ """
+ Tests for L{UnparsedEntry}
+ """
+ def setUp(self):
+ """
+ Set up the 'entry' to be an unparsed entry for some random text.
+ """
+ self.entry = UnparsedEntry(" This is a bogus entry. \n")
+
+
+ def test_fromString(self):
+ """
+ Creating an L{UnparsedEntry} should simply record the string it was
+ passed.
+ """
+ self.assertEqual(" This is a bogus entry. \n",
+ self.entry._string)
+
+
+ def test_matchesHost(self):
+ """
+ An unparsed entry can't match any hosts.
+ """
+ self.assertEqual(False, self.entry.matchesHost("www.twistedmatrix.com"))
+
+
+ def test_matchesKey(self):
+ """
+ An unparsed entry can't match any keys.
+ """
+ self.assertEqual(False, self.entry.matchesKey(Key.fromString(sampleKey)))
+
+
+ def test_toString(self):
+ """
+ L{UnparsedEntry.toString} returns its input string, sans trailing newline.
+ """
+ self.assertEqual(" This is a bogus entry. ", self.entry.toString())
+
+
+
+class ParseErrorTests(TestCase):
+ """
+ L{HashedEntry.fromString} and L{PlainEntry.fromString} can raise a variety
+ of errors depending on misformattings of certain strings. These tests make
+ sure those errors are caught. Since many of the ways that this can go
+ wrong are in the lower-level APIs being invoked by the parsing logic,
+ several of these are integration tests with the L{base64} and
+ L{twisted.conch.ssh.keys} modules.
+ """
+
+ def invalidEntryTest(self, cls):
+ """
+ If there are fewer than three elements, C{fromString} should raise
+ L{InvalidEntry}.
+ """
+ self.assertRaises(InvalidEntry, cls.fromString, "invalid")
+
+
+ def notBase64Test(self, cls):
+ """
+ If the key is not base64, C{fromString} should raise L{BinasciiError}.
+ """
+ self.assertRaises(BinasciiError, cls.fromString, "x x x")
+
+
+ def badKeyTest(self, cls, prefix):
+ """
+ If the key portion of the entry is valid base64, but is not actually an
+ SSH key, C{fromString} should raise L{BadKeyError}.
+ """
+ self.assertRaises(BadKeyError, cls.fromString, ' '.join(
+ [prefix, "ssh-rsa", b2a_base64(
+ "Hey, this isn't an SSH key!").strip()]))
+
+
+ def test_invalidPlainEntry(self):
+ """
+ If there are fewer than three whitespace-separated elements in an
+ entry, L{PlainEntry.fromString} should raise L{InvalidEntry}.
+ """
+ self.invalidEntryTest(PlainEntry)
+
+
+ def test_invalidHashedEntry(self):
+ """
+ If there are fewer than three whitespace-separated elements in an
+ entry, or the hostname salt/hash portion has more than two elements,
+ L{HashedEntry.fromString} should raise L{InvalidEntry}.
+ """
+ self.invalidEntryTest(HashedEntry)
+ a, b, c = sampleHashedLine.split()
+ self.assertRaises(InvalidEntry, HashedEntry.fromString, ' '.join(
+ [a + "||", b, c]))
+
+
+ def test_plainNotBase64(self):
+ """
+ If the key portion of a plain entry is not decodable as base64,
+ C{fromString} should raise L{BinasciiError}.
+ """
+ self.notBase64Test(PlainEntry)
+
+
+ def test_hashedNotBase64(self):
+ """
+ If the key, host salt, or host hash portion of a hashed entry is not
+ encoded, it will raise L{BinasciiError}.
+ """
+ self.notBase64Test(HashedEntry)
+ a, b, c = sampleHashedLine.split()
+ # Salt not valid base64.
+ self.assertRaises(
+ BinasciiError, HashedEntry.fromString,
+ ' '.join(["|1|x|" + b2a_base64("stuff").strip(), b, c]))
+ # Host hash not valid base64.
+ self.assertRaises(
+ BinasciiError, HashedEntry.fromString,
+ ' '.join([HashedEntry.MAGIC + b2a_base64("stuff").strip() + "|x", b, c]))
+ # Neither salt nor hash valid base64.
+ self.assertRaises(
+ BinasciiError, HashedEntry.fromString,
+ ' '.join(["|1|x|x", b, c]))
+
+
+ def test_hashedBadKey(self):
+ """
+ If the key portion of the entry is valid base64, but is not actually an
+ SSH key, C{HashedEntry.fromString} should raise L{BadKeyError}.
+ """
+ a, b, c = sampleHashedLine.split()
+ self.badKeyTest(HashedEntry, a)
+
+
+ def test_plainBadKey(self):
+ """
+ If the key portion of the entry is valid base64, but is not actually an
+ SSH key, C{PlainEntry.fromString} should raise L{BadKeyError}.
+ """
+ self.badKeyTest(PlainEntry, "hostname")
+
+
+
+class KnownHostsDatabaseTests(TestCase):
+ """
+ Tests for L{KnownHostsFile}.
+ """
+
+ def pathWithContent(self, content):
+ """
+ Return a FilePath with the given initial content.
+ """
+ fp = FilePath(self.mktemp())
+ fp.setContent(content)
+ return fp
+
+
+ def loadSampleHostsFile(self, content=(
+ sampleHashedLine + otherSamplePlaintextLine +
+ "\n# That was a blank line.\n"
+ "This is just unparseable.\n"
+ "|1|This also unparseable.\n")):
+ """
+ Return a sample hosts file, with keys for www.twistedmatrix.com and
+ divmod.com present.
+ """
+ return KnownHostsFile.fromPath(self.pathWithContent(content))
+
+
+ def test_loadFromPath(self):
+ """
+ Loading a L{KnownHostsFile} from a path with six entries in it will
+ result in a L{KnownHostsFile} object with six L{IKnownHostEntry}
+ providers in it, each of the appropriate type.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ self.assertEqual(len(hostsFile._entries), 6)
+ self.assertIsInstance(hostsFile._entries[0], HashedEntry)
+ self.assertEqual(True, hostsFile._entries[0].matchesHost(
+ "www.twistedmatrix.com"))
+ self.assertIsInstance(hostsFile._entries[1], PlainEntry)
+ self.assertEqual(True, hostsFile._entries[1].matchesHost(
+ "divmod.com"))
+ self.assertIsInstance(hostsFile._entries[2], UnparsedEntry)
+ self.assertEqual(hostsFile._entries[2].toString(), "")
+ self.assertIsInstance(hostsFile._entries[3], UnparsedEntry)
+ self.assertEqual(hostsFile._entries[3].toString(),
+ "# That was a blank line.")
+ self.assertIsInstance(hostsFile._entries[4], UnparsedEntry)
+ self.assertEqual(hostsFile._entries[4].toString(),
+ "This is just unparseable.")
+ self.assertIsInstance(hostsFile._entries[5], UnparsedEntry)
+ self.assertEqual(hostsFile._entries[5].toString(),
+ "|1|This also unparseable.")
+
+
+ def test_loadNonExistent(self):
+ """
+ Loading a L{KnownHostsFile} from a path that does not exist should
+ result in an empty L{KnownHostsFile} that will save back to that path.
+ """
+ pn = self.mktemp()
+ knownHostsFile = KnownHostsFile.fromPath(FilePath(pn))
+ self.assertEqual([], list(knownHostsFile._entries))
+ self.assertEqual(False, FilePath(pn).exists())
+ knownHostsFile.save()
+ self.assertEqual(True, FilePath(pn).exists())
+
+
+ def test_loadNonExistentParent(self):
+ """
+ Loading a L{KnownHostsFile} from a path whose parent directory does not
+ exist should result in an empty L{KnownHostsFile} that will save back
+ to that path, creating its parent directory(ies) in the process.
+ """
+ thePath = FilePath(self.mktemp())
+ knownHostsPath = thePath.child("foo").child("known_hosts")
+ knownHostsFile = KnownHostsFile.fromPath(knownHostsPath)
+ knownHostsFile.save()
+ knownHostsPath.restat(False)
+ self.assertEqual(True, knownHostsPath.exists())
+
+
+ def test_savingAddsEntry(self):
+ """
+ L{KnownHostsFile.save()} will write out a new file with any entries
+ that have been added.
+ """
+ path = self.pathWithContent(sampleHashedLine +
+ otherSamplePlaintextLine)
+ knownHostsFile = KnownHostsFile.fromPath(path)
+ newEntry = knownHostsFile.addHostKey("some.example.com", Key.fromString(thirdSampleKey))
+ expectedContent = (
+ sampleHashedLine +
+ otherSamplePlaintextLine + HashedEntry.MAGIC +
+ b2a_base64(newEntry._hostSalt).strip() + "|" +
+ b2a_base64(newEntry._hostHash).strip() + " ssh-rsa " +
+ thirdSampleEncodedKey + "\n")
+
+ # Sanity check, let's make sure the base64 API being used for the test
+ # isn't inserting spurious newlines.
+ self.assertEqual(3, expectedContent.count("\n"))
+ knownHostsFile.save()
+ self.assertEqual(expectedContent, path.getContent())
+
+
+ def test_hasPresentKey(self):
+ """
+ L{KnownHostsFile.hasHostKey} returns C{True} when a key for the given
+ hostname is present and matches the expected key.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ self.assertEqual(True, hostsFile.hasHostKey(
+ "www.twistedmatrix.com", Key.fromString(sampleKey)))
+
+
+ def test_hasNonPresentKey(self):
+ """
+ L{KnownHostsFile.hasHostKey} returns C{False} when a key for the given
+ hostname is not present.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ self.assertEqual(False, hostsFile.hasHostKey(
+ "non-existent.example.com", Key.fromString(sampleKey)))
+
+
+ def test_hasKeyMismatch(self):
+ """
+ L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key
+ is present, but different from the expected one. The resulting
+ exception should have an offendingEntry indicating the given entry.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ exception = self.assertRaises(
+ HostKeyChanged, hostsFile.hasHostKey,
+ "www.twistedmatrix.com", Key.fromString(otherSampleKey))
+ self.assertEqual(exception.offendingEntry, hostsFile._entries[0])
+ self.assertEqual(exception.lineno, 1)
+ self.assertEqual(exception.path, hostsFile._savePath)
+
+
+ def test_addHostKey(self):
+ """
+ L{KnownHostsFile.addHostKey} adds a new L{HashedEntry} to the host
+ file, and returns it.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ aKey = Key.fromString(thirdSampleKey)
+ self.assertEqual(False,
+ hostsFile.hasHostKey("somewhere.example.com", aKey))
+ newEntry = hostsFile.addHostKey("somewhere.example.com", aKey)
+
+ # The code in OpenSSH requires host salts to be 20 characters long.
+ # This is the required length of a SHA-1 HMAC hash, so it's just a
+ # sanity check.
+ self.assertEqual(20, len(newEntry._hostSalt))
+ self.assertEqual(True,
+ newEntry.matchesHost("somewhere.example.com"))
+ self.assertEqual(newEntry.keyType, "ssh-rsa")
+ self.assertEqual(aKey, newEntry.publicKey)
+ self.assertEqual(True,
+ hostsFile.hasHostKey("somewhere.example.com", aKey))
+
+
+ def test_randomSalts(self):
+ """
+ L{KnownHostsFile.addHostKey} generates a random salt for each new key,
+ so subsequent salts will be different.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ aKey = Key.fromString(thirdSampleKey)
+ self.assertNotEqual(
+ hostsFile.addHostKey("somewhere.example.com", aKey)._hostSalt,
+ hostsFile.addHostKey("somewhere-else.example.com", aKey)._hostSalt)
+
+
+ def test_verifyValidKey(self):
+ """
+ Verifying a valid key should return a L{Deferred} which fires with
+ True.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ hostsFile.addHostKey("1.2.3.4", Key.fromString(sampleKey))
+ ui = FakeUI()
+ d = hostsFile.verifyHostKey(ui, "www.twistedmatrix.com", "1.2.3.4",
+ Key.fromString(sampleKey))
+ l = []
+ d.addCallback(l.append)
+ self.assertEqual(l, [True])
+
+
+ def test_verifyInvalidKey(self):
+ """
+ Verfying an invalid key should return a L{Deferred} which fires with a
+ L{HostKeyChanged} failure.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ wrongKey = Key.fromString(thirdSampleKey)
+ ui = FakeUI()
+ hostsFile.addHostKey("1.2.3.4", Key.fromString(sampleKey))
+ d = hostsFile.verifyHostKey(
+ ui, "www.twistedmatrix.com", "1.2.3.4", wrongKey)
+ return self.assertFailure(d, HostKeyChanged)
+
+
+ def verifyNonPresentKey(self):
+ """
+ Set up a test to verify a key that isn't present. Return a 3-tuple of
+ the UI, a list set up to collect the result of the verifyHostKey call,
+ and the sample L{KnownHostsFile} being used.
+
+ This utility method avoids returning a L{Deferred}, and records results
+ in the returned list instead, because the events which get generated
+ here are pre-recorded in the 'ui' object. If the L{Deferred} in
+ question does not fire, the it will fail quickly with an empty list.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ absentKey = Key.fromString(thirdSampleKey)
+ ui = FakeUI()
+ l = []
+ d = hostsFile.verifyHostKey(
+ ui, "sample-host.example.com", "4.3.2.1", absentKey)
+ d.addBoth(l.append)
+ self.assertEqual([], l)
+ self.assertEqual(
+ ui.promptText,
+ "The authenticity of host 'sample-host.example.com (4.3.2.1)' "
+ "can't be established.\n"
+ "RSA key fingerprint is "
+ "89:4e:cc:8c:57:83:96:48:ef:63:ad:ee:99:00:4c:8f.\n"
+ "Are you sure you want to continue connecting (yes/no)? ")
+ return ui, l, hostsFile
+
+
+ def test_verifyNonPresentKey_Yes(self):
+ """
+ Verifying a key where neither the hostname nor the IP are present
+ should result in the UI being prompted with a message explaining as
+ much. If the UI says yes, the Deferred should fire with True.
+ """
+ ui, l, knownHostsFile = self.verifyNonPresentKey()
+ ui.promptDeferred.callback(True)
+ self.assertEqual([True], l)
+ reloaded = KnownHostsFile.fromPath(knownHostsFile._savePath)
+ self.assertEqual(
+ True,
+ reloaded.hasHostKey("4.3.2.1", Key.fromString(thirdSampleKey)))
+ self.assertEqual(
+ True,
+ reloaded.hasHostKey("sample-host.example.com",
+ Key.fromString(thirdSampleKey)))
+
+
+ def test_verifyNonPresentKey_No(self):
+ """
+ Verifying a key where neither the hostname nor the IP are present
+ should result in the UI being prompted with a message explaining as
+ much. If the UI says no, the Deferred should fail with
+ UserRejectedKey.
+ """
+ ui, l, knownHostsFile = self.verifyNonPresentKey()
+ ui.promptDeferred.callback(False)
+ l[0].trap(UserRejectedKey)
+
+
+ def test_verifyHostIPMismatch(self):
+ """
+ Verifying a key where the host is present (and correct), but the IP is
+ present and different, should result the deferred firing in a
+ HostKeyChanged failure.
+ """
+ hostsFile = self.loadSampleHostsFile()
+ wrongKey = Key.fromString(thirdSampleKey)
+ ui = FakeUI()
+ d = hostsFile.verifyHostKey(
+ ui, "www.twistedmatrix.com", "4.3.2.1", wrongKey)
+ return self.assertFailure(d, HostKeyChanged)
+
+
+ def test_verifyKeyForHostAndIP(self):
+ """
+ Verifying a key where the hostname is present but the IP is not should
+ result in the key being added for the IP and the user being warned
+ about the change.
+ """
+ ui = FakeUI()
+ hostsFile = self.loadSampleHostsFile()
+ expectedKey = Key.fromString(sampleKey)
+ hostsFile.verifyHostKey(
+ ui, "www.twistedmatrix.com", "5.4.3.2", expectedKey)
+ self.assertEqual(
+ True, KnownHostsFile.fromPath(hostsFile._savePath).hasHostKey(
+ "5.4.3.2", expectedKey))
+ self.assertEqual(
+ ["Warning: Permanently added the RSA host key for IP address "
+ "'5.4.3.2' to the list of known hosts."],
+ ui.userWarnings)
+
+
+class FakeFile(object):
+ """
+ A fake file-like object that acts enough like a file for
+ L{ConsoleUI.prompt}.
+ """
+
+ def __init__(self):
+ self.inlines = []
+ self.outchunks = []
+ self.closed = False
+
+
+ def readline(self):
+ """
+ Return a line from the 'inlines' list.
+ """
+ return self.inlines.pop(0)
+
+
+ def write(self, chunk):
+ """
+ Append the given item to the 'outchunks' list.
+ """
+ if self.closed:
+ raise IOError("the file was closed")
+ self.outchunks.append(chunk)
+
+
+ def close(self):
+ """
+ Set the 'closed' flag to True, explicitly marking that it has been
+ closed.
+ """
+ self.closed = True
+
+
+
+class ConsoleUITests(TestCase):
+ """
+ Test cases for L{ConsoleUI}.
+ """
+
+ def setUp(self):
+ """
+ Create a L{ConsoleUI} pointed at a L{FakeFile}.
+ """
+ self.fakeFile = FakeFile()
+ self.ui = ConsoleUI(self.openFile)
+
+
+ def openFile(self):
+ """
+ Return the current fake file.
+ """
+ return self.fakeFile
+
+
+ def newFile(self, lines):
+ """
+ Create a new fake file (the next file that self.ui will open) with the
+ given list of lines to be returned from readline().
+ """
+ self.fakeFile = FakeFile()
+ self.fakeFile.inlines = lines
+
+
+ def test_promptYes(self):
+ """
+ L{ConsoleUI.prompt} writes a message to the console, then reads a line.
+ If that line is 'yes', then it returns a L{Deferred} that fires with
+ True.
+ """
+ for okYes in ['yes', 'Yes', 'yes\n']:
+ self.newFile([okYes])
+ l = []
+ self.ui.prompt("Hello, world!").addCallback(l.append)
+ self.assertEqual(["Hello, world!"], self.fakeFile.outchunks)
+ self.assertEqual([True], l)
+ self.assertEqual(True, self.fakeFile.closed)
+
+
+ def test_promptNo(self):
+ """
+ L{ConsoleUI.prompt} writes a message to the console, then reads a line.
+ If that line is 'no', then it returns a L{Deferred} that fires with
+ False.
+ """
+ for okNo in ['no', 'No', 'no\n']:
+ self.newFile([okNo])
+ l = []
+ self.ui.prompt("Goodbye, world!").addCallback(l.append)
+ self.assertEqual(["Goodbye, world!"], self.fakeFile.outchunks)
+ self.assertEqual([False], l)
+ self.assertEqual(True, self.fakeFile.closed)
+
+
+ def test_promptRepeatedly(self):
+ """
+ L{ConsoleUI.prompt} writes a message to the console, then reads a line.
+ If that line is neither 'yes' nor 'no', then it says "Please enter
+ 'yes' or 'no'" until it gets a 'yes' or a 'no', at which point it
+ returns a Deferred that answers either True or False.
+ """
+ self.newFile(['what', 'uh', 'okay', 'yes'])
+ l = []
+ self.ui.prompt("Please say something useful.").addCallback(l.append)
+ self.assertEqual([True], l)
+ self.assertEqual(self.fakeFile.outchunks,
+ ["Please say something useful."] +
+ ["Please type 'yes' or 'no': "] * 3)
+ self.assertEqual(True, self.fakeFile.closed)
+ self.newFile(['blah', 'stuff', 'feh', 'no'])
+ l = []
+ self.ui.prompt("Please say something negative.").addCallback(l.append)
+ self.assertEqual([False], l)
+ self.assertEqual(self.fakeFile.outchunks,
+ ["Please say something negative."] +
+ ["Please type 'yes' or 'no': "] * 3)
+ self.assertEqual(True, self.fakeFile.closed)
+
+
+ def test_promptOpenFailed(self):
+ """
+ If the C{opener} passed to L{ConsoleUI} raises an exception, that
+ exception will fail the L{Deferred} returned from L{ConsoleUI.prompt}.
+ """
+ def raiseIt():
+ raise IOError()
+ ui = ConsoleUI(raiseIt)
+ d = ui.prompt("This is a test.")
+ return self.assertFailure(d, IOError)
+
+
+ def test_warn(self):
+ """
+ L{ConsoleUI.warn} should output a message to the console object.
+ """
+ self.ui.warn("Test message.")
+ self.assertEqual(["Test message."], self.fakeFile.outchunks)
+ self.assertEqual(True, self.fakeFile.closed)
+
+
+ def test_warnOpenFailed(self):
+ """
+ L{ConsoleUI.warn} should log a traceback if the output can't be opened.
+ """
+ def raiseIt():
+ 1 / 0
+ ui = ConsoleUI(raiseIt)
+ ui.warn("This message never makes it.")
+ self.assertEqual(len(self.flushLoggedErrors(ZeroDivisionError)), 1)
+
+
+
+class FakeUI(object):
+ """
+ A fake UI object, adhering to the interface expected by
+ L{KnownHostsFile.verifyHostKey}
+
+ @ivar userWarnings: inputs provided to 'warn'.
+
+ @ivar promptDeferred: last result returned from 'prompt'.
+
+ @ivar promptText: the last input provided to 'prompt'.
+ """
+
+ def __init__(self):
+ self.userWarnings = []
+ self.promptDeferred = None
+ self.promptText = None
+
+
+ def prompt(self, text):
+ """
+ Issue the user an interactive prompt, which they can accept or deny.
+ """
+ self.promptText = text
+ self.promptDeferred = Deferred()
+ return self.promptDeferred
+
+
+ def warn(self, text):
+ """
+ Issue a non-interactive warning to the user.
+ """
+ self.userWarnings.append(text)
+
+
+
+class FakeObject(object):
+ """
+ A fake object that can have some attributes. Used to fake
+ L{SSHClientTransport} and L{SSHClientFactory}.
+ """
+
+
+class DefaultAPITests(TestCase):
+ """
+ The API in L{twisted.conch.client.default.verifyHostKey} is the integration
+ point between the code in the rest of conch and L{KnownHostsFile}.
+ """
+
+ def patchedOpen(self, fname, mode):
+ """
+ The patched version of 'open'; this returns a L{FakeFile} that the
+ instantiated L{ConsoleUI} can use.
+ """
+ self.assertEqual(fname, "/dev/tty")
+ self.assertEqual(mode, "r+b")
+ return self.fakeFile
+
+
+ def setUp(self):
+ """
+ Patch 'open' in verifyHostKey.
+ """
+ self.fakeFile = FakeFile()
+ self.patch(default, "_open", self.patchedOpen)
+ self.hostsOption = self.mktemp()
+ knownHostsFile = KnownHostsFile(FilePath(self.hostsOption))
+ knownHostsFile.addHostKey("exists.example.com", Key.fromString(sampleKey))
+ knownHostsFile.addHostKey("4.3.2.1", Key.fromString(sampleKey))
+ knownHostsFile.save()
+ self.fakeTransport = FakeObject()
+ self.fakeTransport.factory = FakeObject()
+ self.options = self.fakeTransport.factory.options = {
+ 'host': "exists.example.com",
+ 'known-hosts': self.hostsOption
+ }
+
+
+ def test_verifyOKKey(self):
+ """
+ L{default.verifyHostKey} should return a L{Deferred} which fires with
+ C{1} when passed a host, IP, and key which already match the
+ known_hosts file it is supposed to check.
+ """
+ l = []
+ default.verifyHostKey(self.fakeTransport, "4.3.2.1", sampleKey,
+ "I don't care.").addCallback(l.append)
+ self.assertEqual([1], l)
+
+
+ def replaceHome(self, tempHome):
+ """
+ Replace the HOME environment variable until the end of the current
+ test, with the given new home-directory, so that L{os.path.expanduser}
+ will yield controllable, predictable results.
+
+ @param tempHome: the pathname to replace the HOME variable with.
+
+ @type tempHome: L{str}
+ """
+ oldHome = os.environ.get('HOME')
+ def cleanupHome():
+ if oldHome is None:
+ del os.environ['HOME']
+ else:
+ os.environ['HOME'] = oldHome
+ self.addCleanup(cleanupHome)
+ os.environ['HOME'] = tempHome
+
+
+ def test_noKnownHostsOption(self):
+ """
+ L{default.verifyHostKey} should find your known_hosts file in
+ ~/.ssh/known_hosts if you don't specify one explicitly on the command
+ line.
+ """
+ l = []
+ tmpdir = self.mktemp()
+ oldHostsOption = self.hostsOption
+ hostsNonOption = FilePath(tmpdir).child(".ssh").child("known_hosts")
+ hostsNonOption.parent().makedirs()
+ FilePath(oldHostsOption).moveTo(hostsNonOption)
+ self.replaceHome(tmpdir)
+ self.options['known-hosts'] = None
+ default.verifyHostKey(self.fakeTransport, "4.3.2.1", sampleKey,
+ "I don't care.").addCallback(l.append)
+ self.assertEqual([1], l)
+
+
+ def test_verifyHostButNotIP(self):
+ """
+ L{default.verifyHostKey} should return a L{Deferred} which fires with
+ C{1} when passed a host which matches with an IP is not present in its
+ known_hosts file, and should also warn the user that it has added the
+ IP address.
+ """
+ l = []
+ default.verifyHostKey(self.fakeTransport, "8.7.6.5", sampleKey,
+ "Fingerprint not required.").addCallback(l.append)
+ self.assertEqual(
+ ["Warning: Permanently added the RSA host key for IP address "
+ "'8.7.6.5' to the list of known hosts."],
+ self.fakeFile.outchunks)
+ self.assertEqual([1], l)
+ knownHostsFile = KnownHostsFile.fromPath(FilePath(self.hostsOption))
+ self.assertEqual(True, knownHostsFile.hasHostKey("8.7.6.5",
+ Key.fromString(sampleKey)))
+
+
+ def test_verifyQuestion(self):
+ """
+ L{default.verifyHostKey} should return a L{Default} which fires with
+ C{0} when passed a unknown host that the user refuses to acknowledge.
+ """
+ self.fakeTransport.factory.options['host'] = 'fake.example.com'
+ self.fakeFile.inlines.append("no")
+ d = default.verifyHostKey(
+ self.fakeTransport, "9.8.7.6", otherSampleKey, "No fingerprint!")
+ self.assertEqual(
+ ["The authenticity of host 'fake.example.com (9.8.7.6)' "
+ "can't be established.\n"
+ "RSA key fingerprint is "
+ "57:a1:c2:a1:07:a0:2b:f4:ce:b5:e5:b7:ae:cc:e1:99.\n"
+ "Are you sure you want to continue connecting (yes/no)? "],
+ self.fakeFile.outchunks)
+ return self.assertFailure(d, UserRejectedKey)
+
+
+ def test_verifyBadKey(self):
+ """
+ L{default.verifyHostKey} should return a L{Deferred} which fails with
+ L{HostKeyChanged} if the host key is incorrect.
+ """
+ d = default.verifyHostKey(
+ self.fakeTransport, "4.3.2.1", otherSampleKey,
+ "Again, not required.")
+ return self.assertFailure(d, HostKeyChanged)
diff --git a/twisted/conch/test/test_manhole.py b/twisted/conch/test/test_manhole.py
new file mode 100644
index 0000000..3b31984
--- /dev/null
+++ b/twisted/conch/test/test_manhole.py
@@ -0,0 +1,372 @@
+# -*- test-case-name: twisted.conch.test.test_manhole -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.manhole}.
+"""
+
+import traceback
+
+from twisted.trial import unittest
+from twisted.internet import error, defer
+from twisted.test.proto_helpers import StringTransport
+from twisted.conch.test.test_recvline import _TelnetMixin, _SSHMixin, _StdioMixin, stdio, ssh
+from twisted.conch import manhole
+from twisted.conch.insults import insults
+
+
+def determineDefaultFunctionName():
+ """
+ Return the string used by Python as the name for code objects which are
+ compiled from interactive input or at the top-level of modules.
+ """
+ try:
+ 1 / 0
+ except:
+ # The last frame is this function. The second to last frame is this
+ # function's caller, which is module-scope, which is what we want,
+ # so -2.
+ return traceback.extract_stack()[-2][2]
+defaultFunctionName = determineDefaultFunctionName()
+
+
+
+class ManholeInterpreterTests(unittest.TestCase):
+ """
+ Tests for L{manhole.ManholeInterpreter}.
+ """
+ def test_resetBuffer(self):
+ """
+ L{ManholeInterpreter.resetBuffer} should empty the input buffer.
+ """
+ interpreter = manhole.ManholeInterpreter(None)
+ interpreter.buffer.extend(["1", "2"])
+ interpreter.resetBuffer()
+ self.assertFalse(interpreter.buffer)
+
+
+
+class ManholeProtocolTests(unittest.TestCase):
+ """
+ Tests for L{manhole.Manhole}.
+ """
+ def test_interruptResetsInterpreterBuffer(self):
+ """
+ L{manhole.Manhole.handle_INT} should cause the interpreter input buffer
+ to be reset.
+ """
+ transport = StringTransport()
+ terminal = insults.ServerProtocol(manhole.Manhole)
+ terminal.makeConnection(transport)
+ protocol = terminal.terminalProtocol
+ interpreter = protocol.interpreter
+ interpreter.buffer.extend(["1", "2"])
+ protocol.handle_INT()
+ self.assertFalse(interpreter.buffer)
+
+
+
+class WriterTestCase(unittest.TestCase):
+ def testInteger(self):
+ manhole.lastColorizedLine("1")
+
+
+ def testDoubleQuoteString(self):
+ manhole.lastColorizedLine('"1"')
+
+
+ def testSingleQuoteString(self):
+ manhole.lastColorizedLine("'1'")
+
+
+ def testTripleSingleQuotedString(self):
+ manhole.lastColorizedLine("'''1'''")
+
+
+ def testTripleDoubleQuotedString(self):
+ manhole.lastColorizedLine('"""1"""')
+
+
+ def testFunctionDefinition(self):
+ manhole.lastColorizedLine("def foo():")
+
+
+ def testClassDefinition(self):
+ manhole.lastColorizedLine("class foo:")
+
+
+class ManholeLoopbackMixin:
+ serverProtocol = manhole.ColoredManhole
+
+ def wfd(self, d):
+ return defer.waitForDeferred(d)
+
+ def testSimpleExpression(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "1 + 1\n"
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> 1 + 1",
+ "2",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+ def testTripleQuoteLineContinuation(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "'''\n'''\n"
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> '''",
+ "... '''",
+ "'\\n'",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+ def testFunctionDefinition(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "def foo(bar):\n"
+ "\tprint bar\n\n"
+ "foo(42)\n"
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> def foo(bar):",
+ "... print bar",
+ "... ",
+ ">>> foo(42)",
+ "42",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+ def testClassDefinition(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "class Foo:\n"
+ "\tdef bar(self):\n"
+ "\t\tprint 'Hello, world!'\n\n"
+ "Foo().bar()\n"
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> class Foo:",
+ "... def bar(self):",
+ "... print 'Hello, world!'",
+ "... ",
+ ">>> Foo().bar()",
+ "Hello, world!",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+ def testException(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "raise Exception('foo bar baz')\n"
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> raise Exception('foo bar baz')",
+ "Traceback (most recent call last):",
+ ' File "<console>", line 1, in ' + defaultFunctionName,
+ "Exception: foo bar baz",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+ def testControlC(self):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(
+ "cancelled line" + manhole.CTRL_C +
+ "done")
+
+ def finished(ign):
+ self._assertBuffer(
+ [">>> cancelled line",
+ "KeyboardInterrupt",
+ ">>> done"])
+
+ return done.addCallback(finished)
+
+
+ def test_interruptDuringContinuation(self):
+ """
+ Sending ^C to Manhole while in a state where more input is required to
+ complete a statement should discard the entire ongoing statement and
+ reset the input prompt to the non-continuation prompt.
+ """
+ continuing = self.recvlineClient.expect("things")
+
+ self._testwrite("(\nthings")
+
+ def gotContinuation(ignored):
+ self._assertBuffer(
+ [">>> (",
+ "... things"])
+ interrupted = self.recvlineClient.expect(">>> ")
+ self._testwrite(manhole.CTRL_C)
+ return interrupted
+ continuing.addCallback(gotContinuation)
+
+ def gotInterruption(ignored):
+ self._assertBuffer(
+ [">>> (",
+ "... things",
+ "KeyboardInterrupt",
+ ">>> "])
+ continuing.addCallback(gotInterruption)
+ return continuing
+
+
+ def testControlBackslash(self):
+ self._testwrite("cancelled line")
+ partialLine = self.recvlineClient.expect("cancelled line")
+
+ def gotPartialLine(ign):
+ self._assertBuffer(
+ [">>> cancelled line"])
+ self._testwrite(manhole.CTRL_BACKSLASH)
+
+ d = self.recvlineClient.onDisconnection
+ return self.assertFailure(d, error.ConnectionDone)
+
+ def gotClearedLine(ign):
+ self._assertBuffer(
+ [""])
+
+ return partialLine.addCallback(gotPartialLine).addCallback(gotClearedLine)
+
+ def testControlD(self):
+ self._testwrite("1 + 1")
+ helloWorld = self.wfd(self.recvlineClient.expect(r"\+ 1"))
+ yield helloWorld
+ helloWorld.getResult()
+ self._assertBuffer([">>> 1 + 1"])
+
+ self._testwrite(manhole.CTRL_D + " + 1")
+ cleared = self.wfd(self.recvlineClient.expect(r"\+ 1"))
+ yield cleared
+ cleared.getResult()
+ self._assertBuffer([">>> 1 + 1 + 1"])
+
+ self._testwrite("\n")
+ printed = self.wfd(self.recvlineClient.expect("3\n>>> "))
+ yield printed
+ printed.getResult()
+
+ self._testwrite(manhole.CTRL_D)
+ d = self.recvlineClient.onDisconnection
+ disconnected = self.wfd(self.assertFailure(d, error.ConnectionDone))
+ yield disconnected
+ disconnected.getResult()
+ testControlD = defer.deferredGenerator(testControlD)
+
+
+ def testControlL(self):
+ """
+ CTRL+L is generally used as a redraw-screen command in terminal
+ applications. Manhole doesn't currently respect this usage of it,
+ but it should at least do something reasonable in response to this
+ event (rather than, say, eating your face).
+ """
+ # Start off with a newline so that when we clear the display we can
+ # tell by looking for the missing first empty prompt line.
+ self._testwrite("\n1 + 1")
+ helloWorld = self.wfd(self.recvlineClient.expect(r"\+ 1"))
+ yield helloWorld
+ helloWorld.getResult()
+ self._assertBuffer([">>> ", ">>> 1 + 1"])
+
+ self._testwrite(manhole.CTRL_L + " + 1")
+ redrew = self.wfd(self.recvlineClient.expect(r"1 \+ 1 \+ 1"))
+ yield redrew
+ redrew.getResult()
+ self._assertBuffer([">>> 1 + 1 + 1"])
+ testControlL = defer.deferredGenerator(testControlL)
+
+
+ def test_controlA(self):
+ """
+ CTRL-A can be used as HOME - returning cursor to beginning of
+ current line buffer.
+ """
+ self._testwrite('rint "hello"' + '\x01' + 'p')
+ d = self.recvlineClient.expect('print "hello"')
+ def cb(ignore):
+ self._assertBuffer(['>>> print "hello"'])
+ return d.addCallback(cb)
+
+
+ def test_controlE(self):
+ """
+ CTRL-E can be used as END - setting cursor to end of current
+ line buffer.
+ """
+ self._testwrite('rint "hello' + '\x01' + 'p' + '\x05' + '"')
+ d = self.recvlineClient.expect('print "hello"')
+ def cb(ignore):
+ self._assertBuffer(['>>> print "hello"'])
+ return d.addCallback(cb)
+
+
+ def testDeferred(self):
+ self._testwrite(
+ "from twisted.internet import defer, reactor\n"
+ "d = defer.Deferred()\n"
+ "d\n")
+
+ deferred = self.wfd(self.recvlineClient.expect("<Deferred #0>"))
+ yield deferred
+ deferred.getResult()
+
+ self._testwrite(
+ "c = reactor.callLater(0.1, d.callback, 'Hi!')\n")
+ delayed = self.wfd(self.recvlineClient.expect(">>> "))
+ yield delayed
+ delayed.getResult()
+
+ called = self.wfd(self.recvlineClient.expect("Deferred #0 called back: 'Hi!'\n>>> "))
+ yield called
+ called.getResult()
+ self._assertBuffer(
+ [">>> from twisted.internet import defer, reactor",
+ ">>> d = defer.Deferred()",
+ ">>> d",
+ "<Deferred #0>",
+ ">>> c = reactor.callLater(0.1, d.callback, 'Hi!')",
+ "Deferred #0 called back: 'Hi!'",
+ ">>> "])
+
+ testDeferred = defer.deferredGenerator(testDeferred)
+
+class ManholeLoopbackTelnet(_TelnetMixin, unittest.TestCase, ManholeLoopbackMixin):
+ pass
+
+class ManholeLoopbackSSH(_SSHMixin, unittest.TestCase, ManholeLoopbackMixin):
+ if ssh is None:
+ skip = "Crypto requirements missing, can't run manhole tests over ssh"
+
+class ManholeLoopbackStdio(_StdioMixin, unittest.TestCase, ManholeLoopbackMixin):
+ if stdio is None:
+ skip = "Terminal requirements missing, can't run manhole tests over stdio"
+ else:
+ serverProtocol = stdio.ConsoleManhole
diff --git a/twisted/conch/test/test_mixin.py b/twisted/conch/test/test_mixin.py
new file mode 100644
index 0000000..74d60ea
--- /dev/null
+++ b/twisted/conch/test/test_mixin.py
@@ -0,0 +1,47 @@
+# -*- twisted.conch.test.test_mixin -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import time
+
+from twisted.internet import reactor, protocol
+
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+
+from twisted.conch import mixin
+
+
+class TestBufferingProto(mixin.BufferingMixin):
+ scheduled = False
+ rescheduled = 0
+ def schedule(self):
+ self.scheduled = True
+ return object()
+
+ def reschedule(self, token):
+ self.rescheduled += 1
+
+
+
+class BufferingTest(unittest.TestCase):
+ def testBuffering(self):
+ p = TestBufferingProto()
+ t = p.transport = StringTransport()
+
+ self.failIf(p.scheduled)
+
+ L = ['foo', 'bar', 'baz', 'quux']
+
+ p.write('foo')
+ self.failUnless(p.scheduled)
+ self.failIf(p.rescheduled)
+
+ for s in L:
+ n = p.rescheduled
+ p.write(s)
+ self.assertEqual(p.rescheduled, n + 1)
+ self.assertEqual(t.value(), '')
+
+ p.flush()
+ self.assertEqual(t.value(), 'foo' + ''.join(L))
diff --git a/twisted/conch/test/test_openssh_compat.py b/twisted/conch/test/test_openssh_compat.py
new file mode 100644
index 0000000..8b4e1a6
--- /dev/null
+++ b/twisted/conch/test/test_openssh_compat.py
@@ -0,0 +1,102 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.openssh_compat}.
+"""
+
+import os
+
+from twisted.trial.unittest import TestCase
+from twisted.python.filepath import FilePath
+from twisted.python.compat import set
+
+try:
+ import Crypto.Cipher.DES3
+ import pyasn1
+except ImportError:
+ OpenSSHFactory = None
+else:
+ from twisted.conch.openssh_compat.factory import OpenSSHFactory
+
+from twisted.conch.test import keydata
+from twisted.test.test_process import MockOS
+
+
+class OpenSSHFactoryTests(TestCase):
+ """
+ Tests for L{OpenSSHFactory}.
+ """
+ if getattr(os, "geteuid", None) is None:
+ skip = "geteuid/seteuid not available"
+ elif OpenSSHFactory is None:
+ skip = "Cannot run without PyCrypto or PyASN1"
+
+ def setUp(self):
+ self.factory = OpenSSHFactory()
+ self.keysDir = FilePath(self.mktemp())
+ self.keysDir.makedirs()
+ self.factory.dataRoot = self.keysDir.path
+
+ self.keysDir.child("ssh_host_foo").setContent("foo")
+ self.keysDir.child("bar_key").setContent("foo")
+ self.keysDir.child("ssh_host_one_key").setContent(
+ keydata.privateRSA_openssh)
+ self.keysDir.child("ssh_host_two_key").setContent(
+ keydata.privateDSA_openssh)
+ self.keysDir.child("ssh_host_three_key").setContent(
+ "not a key content")
+
+ self.keysDir.child("ssh_host_one_key.pub").setContent(
+ keydata.publicRSA_openssh)
+
+ self.mockos = MockOS()
+ self.patch(os, "seteuid", self.mockos.seteuid)
+ self.patch(os, "setegid", self.mockos.setegid)
+
+
+ def test_getPublicKeys(self):
+ """
+ L{OpenSSHFactory.getPublicKeys} should return the available public keys
+ in the data directory
+ """
+ keys = self.factory.getPublicKeys()
+ self.assertEqual(len(keys), 1)
+ keyTypes = keys.keys()
+ self.assertEqual(keyTypes, ['ssh-rsa'])
+
+
+ def test_getPrivateKeys(self):
+ """
+ L{OpenSSHFactory.getPrivateKeys} should return the available private
+ keys in the data directory.
+ """
+ keys = self.factory.getPrivateKeys()
+ self.assertEqual(len(keys), 2)
+ keyTypes = keys.keys()
+ self.assertEqual(set(keyTypes), set(['ssh-rsa', 'ssh-dss']))
+ self.assertEqual(self.mockos.seteuidCalls, [])
+ self.assertEqual(self.mockos.setegidCalls, [])
+
+
+ def test_getPrivateKeysAsRoot(self):
+ """
+ L{OpenSSHFactory.getPrivateKeys} should switch to root if the keys
+ aren't readable by the current user.
+ """
+ keyFile = self.keysDir.child("ssh_host_two_key")
+ # Fake permission error by changing the mode
+ keyFile.chmod(0000)
+ self.addCleanup(keyFile.chmod, 0777)
+ # And restore the right mode when seteuid is called
+ savedSeteuid = os.seteuid
+ def seteuid(euid):
+ keyFile.chmod(0777)
+ return savedSeteuid(euid)
+ self.patch(os, "seteuid", seteuid)
+ keys = self.factory.getPrivateKeys()
+ self.assertEqual(len(keys), 2)
+ keyTypes = keys.keys()
+ self.assertEqual(set(keyTypes), set(['ssh-rsa', 'ssh-dss']))
+ self.assertEqual(self.mockos.seteuidCalls, [0, os.geteuid()])
+ self.assertEqual(self.mockos.setegidCalls, [0, os.getegid()])
diff --git a/twisted/conch/test/test_recvline.py b/twisted/conch/test/test_recvline.py
new file mode 100644
index 0000000..3d53564
--- /dev/null
+++ b/twisted/conch/test/test_recvline.py
@@ -0,0 +1,706 @@
+# -*- test-case-name: twisted.conch.test.test_recvline -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.recvline} and fixtures for testing related
+functionality.
+"""
+
+import sys, os
+
+from twisted.conch.insults import insults
+from twisted.conch import recvline
+
+from twisted.python import reflect, components
+from twisted.internet import defer, error
+from twisted.trial import unittest
+from twisted.cred import portal
+from twisted.test.proto_helpers import StringTransport
+
+class Arrows(unittest.TestCase):
+ def setUp(self):
+ self.underlyingTransport = StringTransport()
+ self.pt = insults.ServerProtocol()
+ self.p = recvline.HistoricRecvLine()
+ self.pt.protocolFactory = lambda: self.p
+ self.pt.factory = self
+ self.pt.makeConnection(self.underlyingTransport)
+ # self.p.makeConnection(self.pt)
+
+ def test_printableCharacters(self):
+ """
+ When L{HistoricRecvLine} receives a printable character,
+ it adds it to the current line buffer.
+ """
+ self.p.keystrokeReceived('x', None)
+ self.p.keystrokeReceived('y', None)
+ self.p.keystrokeReceived('z', None)
+
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ def test_horizontalArrows(self):
+ """
+ When L{HistoricRecvLine} receives an LEFT_ARROW or
+ RIGHT_ARROW keystroke it moves the cursor left or right
+ in the current line buffer, respectively.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+ for ch in 'xyz':
+ kR(ch)
+
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.RIGHT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('xy', 'z'))
+
+ kR(self.pt.LEFT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('x', 'yz'))
+
+ kR(self.pt.LEFT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('', 'xyz'))
+
+ kR(self.pt.LEFT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('', 'xyz'))
+
+ kR(self.pt.RIGHT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('x', 'yz'))
+
+ kR(self.pt.RIGHT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('xy', 'z'))
+
+ kR(self.pt.RIGHT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.RIGHT_ARROW)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ def test_newline(self):
+ """
+ When {HistoricRecvLine} receives a newline, it adds the current
+ line buffer to the end of its history buffer.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz\nabc\n123\n':
+ kR(ch)
+
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc', '123'), ()))
+
+ kR('c')
+ kR('b')
+ kR('a')
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc', '123'), ()))
+
+ kR('\n')
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc', '123', 'cba'), ()))
+
+ def test_verticalArrows(self):
+ """
+ When L{HistoricRecvLine} receives UP_ARROW or DOWN_ARROW
+ keystrokes it move the current index in the current history
+ buffer up or down, and resets the current line buffer to the
+ previous or next line in history, respectively for each.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz\nabc\n123\n':
+ kR(ch)
+
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc', '123'), ()))
+ self.assertEqual(self.p.currentLineBuffer(), ('', ''))
+
+ kR(self.pt.UP_ARROW)
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc'), ('123',)))
+ self.assertEqual(self.p.currentLineBuffer(), ('123', ''))
+
+ kR(self.pt.UP_ARROW)
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz',), ('abc', '123')))
+ self.assertEqual(self.p.currentLineBuffer(), ('abc', ''))
+
+ kR(self.pt.UP_ARROW)
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ ((), ('xyz', 'abc', '123')))
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.UP_ARROW)
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ ((), ('xyz', 'abc', '123')))
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ for i in range(4):
+ kR(self.pt.DOWN_ARROW)
+ self.assertEqual(self.p.currentHistoryBuffer(),
+ (('xyz', 'abc', '123'), ()))
+
+ def test_home(self):
+ """
+ When L{HistoricRecvLine} receives a HOME keystroke it moves the
+ cursor to the beginning of the current line buffer.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'hello, world':
+ kR(ch)
+ self.assertEqual(self.p.currentLineBuffer(), ('hello, world', ''))
+
+ kR(self.pt.HOME)
+ self.assertEqual(self.p.currentLineBuffer(), ('', 'hello, world'))
+
+ def test_end(self):
+ """
+ When L{HistoricRecvLine} receives a END keystroke it moves the cursor
+ to the end of the current line buffer.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'hello, world':
+ kR(ch)
+ self.assertEqual(self.p.currentLineBuffer(), ('hello, world', ''))
+
+ kR(self.pt.HOME)
+ kR(self.pt.END)
+ self.assertEqual(self.p.currentLineBuffer(), ('hello, world', ''))
+
+ def test_backspace(self):
+ """
+ When L{HistoricRecvLine} receives a BACKSPACE keystroke it deletes
+ the character immediately before the cursor.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz':
+ kR(ch)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.BACKSPACE)
+ self.assertEqual(self.p.currentLineBuffer(), ('xy', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ kR(self.pt.BACKSPACE)
+ self.assertEqual(self.p.currentLineBuffer(), ('', 'y'))
+
+ kR(self.pt.BACKSPACE)
+ self.assertEqual(self.p.currentLineBuffer(), ('', 'y'))
+
+ def test_delete(self):
+ """
+ When L{HistoricRecvLine} receives a DELETE keystroke, it
+ delets the character immediately after the cursor.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz':
+ kR(ch)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.DELETE)
+ self.assertEqual(self.p.currentLineBuffer(), ('xyz', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ kR(self.pt.DELETE)
+ self.assertEqual(self.p.currentLineBuffer(), ('xy', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ kR(self.pt.DELETE)
+ self.assertEqual(self.p.currentLineBuffer(), ('x', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ kR(self.pt.DELETE)
+ self.assertEqual(self.p.currentLineBuffer(), ('', ''))
+
+ kR(self.pt.DELETE)
+ self.assertEqual(self.p.currentLineBuffer(), ('', ''))
+
+ def test_insert(self):
+ """
+ When not in INSERT mode, L{HistoricRecvLine} inserts the typed
+ character at the cursor before the next character.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz':
+ kR(ch)
+
+ kR(self.pt.LEFT_ARROW)
+ kR('A')
+ self.assertEqual(self.p.currentLineBuffer(), ('xyA', 'z'))
+
+ kR(self.pt.LEFT_ARROW)
+ kR('B')
+ self.assertEqual(self.p.currentLineBuffer(), ('xyB', 'Az'))
+
+ def test_typeover(self):
+ """
+ When in INSERT mode and upon receiving a keystroke with a printable
+ character, L{HistoricRecvLine} replaces the character at
+ the cursor with the typed character rather than inserting before.
+ Ah, the ironies of INSERT mode.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+
+ for ch in 'xyz':
+ kR(ch)
+
+ kR(self.pt.INSERT)
+
+ kR(self.pt.LEFT_ARROW)
+ kR('A')
+ self.assertEqual(self.p.currentLineBuffer(), ('xyA', ''))
+
+ kR(self.pt.LEFT_ARROW)
+ kR('B')
+ self.assertEqual(self.p.currentLineBuffer(), ('xyB', ''))
+
+
+ def test_unprintableCharacters(self):
+ """
+ When L{HistoricRecvLine} receives a keystroke for an unprintable
+ function key with no assigned behavior, the line buffer is unmodified.
+ """
+ kR = lambda ch: self.p.keystrokeReceived(ch, None)
+ pt = self.pt
+
+ for ch in (pt.F1, pt.F2, pt.F3, pt.F4, pt.F5, pt.F6, pt.F7, pt.F8,
+ pt.F9, pt.F10, pt.F11, pt.F12, pt.PGUP, pt.PGDN):
+ kR(ch)
+ self.assertEqual(self.p.currentLineBuffer(), ('', ''))
+
+
+from twisted.conch import telnet
+from twisted.conch.insults import helper
+from twisted.protocols import loopback
+
+class EchoServer(recvline.HistoricRecvLine):
+ def lineReceived(self, line):
+ self.terminal.write(line + '\n' + self.ps[self.pn])
+
+# An insults API for this would be nice.
+left = "\x1b[D"
+right = "\x1b[C"
+up = "\x1b[A"
+down = "\x1b[B"
+insert = "\x1b[2~"
+home = "\x1b[1~"
+delete = "\x1b[3~"
+end = "\x1b[4~"
+backspace = "\x7f"
+
+from twisted.cred import checkers
+
+try:
+ from twisted.conch.ssh import userauth, transport, channel, connection, session
+ from twisted.conch.manhole_ssh import TerminalUser, TerminalSession, TerminalRealm, TerminalSessionTransport, ConchFactory
+except ImportError:
+ ssh = False
+else:
+ ssh = True
+ class SessionChannel(channel.SSHChannel):
+ name = 'session'
+
+ def __init__(self, protocolFactory, protocolArgs, protocolKwArgs, width, height, *a, **kw):
+ channel.SSHChannel.__init__(self, *a, **kw)
+
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = protocolArgs
+ self.protocolKwArgs = protocolKwArgs
+
+ self.width = width
+ self.height = height
+
+ def channelOpen(self, data):
+ term = session.packRequest_pty_req("vt102", (self.height, self.width, 0, 0), '')
+ self.conn.sendRequest(self, 'pty-req', term)
+ self.conn.sendRequest(self, 'shell', '')
+
+ self._protocolInstance = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs)
+ self._protocolInstance.factory = self
+ self._protocolInstance.makeConnection(self)
+
+ def closed(self):
+ self._protocolInstance.connectionLost(error.ConnectionDone())
+
+ def dataReceived(self, data):
+ self._protocolInstance.dataReceived(data)
+
+ class TestConnection(connection.SSHConnection):
+ def __init__(self, protocolFactory, protocolArgs, protocolKwArgs, width, height, *a, **kw):
+ connection.SSHConnection.__init__(self, *a, **kw)
+
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = protocolArgs
+ self.protocolKwArgs = protocolKwArgs
+
+ self.width = width
+ self.height = height
+
+ def serviceStarted(self):
+ self.__channel = SessionChannel(self.protocolFactory, self.protocolArgs, self.protocolKwArgs, self.width, self.height)
+ self.openChannel(self.__channel)
+
+ def write(self, bytes):
+ return self.__channel.write(bytes)
+
+ class TestAuth(userauth.SSHUserAuthClient):
+ def __init__(self, username, password, *a, **kw):
+ userauth.SSHUserAuthClient.__init__(self, username, *a, **kw)
+ self.password = password
+
+ def getPassword(self):
+ return defer.succeed(self.password)
+
+ class TestTransport(transport.SSHClientTransport):
+ def __init__(self, protocolFactory, protocolArgs, protocolKwArgs, username, password, width, height, *a, **kw):
+ # transport.SSHClientTransport.__init__(self, *a, **kw)
+ self.protocolFactory = protocolFactory
+ self.protocolArgs = protocolArgs
+ self.protocolKwArgs = protocolKwArgs
+ self.username = username
+ self.password = password
+ self.width = width
+ self.height = height
+
+ def verifyHostKey(self, hostKey, fingerprint):
+ return defer.succeed(True)
+
+ def connectionSecure(self):
+ self.__connection = TestConnection(self.protocolFactory, self.protocolArgs, self.protocolKwArgs, self.width, self.height)
+ self.requestService(
+ TestAuth(self.username, self.password, self.__connection))
+
+ def write(self, bytes):
+ return self.__connection.write(bytes)
+
+ class TestSessionTransport(TerminalSessionTransport):
+ def protocolFactory(self):
+ return self.avatar.conn.transport.factory.serverProtocol()
+
+ class TestSession(TerminalSession):
+ transportFactory = TestSessionTransport
+
+ class TestUser(TerminalUser):
+ pass
+
+ components.registerAdapter(TestSession, TestUser, session.ISession)
+
+
+class LoopbackRelay(loopback.LoopbackRelay):
+ clearCall = None
+
+ def logPrefix(self):
+ return "LoopbackRelay(%r)" % (self.target.__class__.__name__,)
+
+ def write(self, bytes):
+ loopback.LoopbackRelay.write(self, bytes)
+ if self.clearCall is not None:
+ self.clearCall.cancel()
+
+ from twisted.internet import reactor
+ self.clearCall = reactor.callLater(0, self._clearBuffer)
+
+ def _clearBuffer(self):
+ self.clearCall = None
+ loopback.LoopbackRelay.clearBuffer(self)
+
+
+class NotifyingExpectableBuffer(helper.ExpectableBuffer):
+ def __init__(self):
+ self.onConnection = defer.Deferred()
+ self.onDisconnection = defer.Deferred()
+
+ def connectionMade(self):
+ helper.ExpectableBuffer.connectionMade(self)
+ self.onConnection.callback(self)
+
+ def connectionLost(self, reason):
+ self.onDisconnection.errback(reason)
+
+
+class _BaseMixin:
+ WIDTH = 80
+ HEIGHT = 24
+
+ def _assertBuffer(self, lines):
+ receivedLines = str(self.recvlineClient).splitlines()
+ expectedLines = lines + ([''] * (self.HEIGHT - len(lines) - 1))
+ self.assertEqual(len(receivedLines), len(expectedLines))
+ for i in range(len(receivedLines)):
+ self.assertEqual(
+ receivedLines[i], expectedLines[i],
+ str(receivedLines[max(0, i-1):i+1]) +
+ " != " +
+ str(expectedLines[max(0, i-1):i+1]))
+
+ def _trivialTest(self, input, output):
+ done = self.recvlineClient.expect("done")
+
+ self._testwrite(input)
+
+ def finished(ign):
+ self._assertBuffer(output)
+
+ return done.addCallback(finished)
+
+
+class _SSHMixin(_BaseMixin):
+ def setUp(self):
+ if not ssh:
+ raise unittest.SkipTest("Crypto requirements missing, can't run historic recvline tests over ssh")
+
+ u, p = 'testuser', 'testpass'
+ rlm = TerminalRealm()
+ rlm.userFactory = TestUser
+ rlm.chainedProtocolFactory = lambda: insultsServer
+
+ ptl = portal.Portal(
+ rlm,
+ [checkers.InMemoryUsernamePasswordDatabaseDontUse(**{u: p})])
+ sshFactory = ConchFactory(ptl)
+ sshFactory.serverProtocol = self.serverProtocol
+ sshFactory.startFactory()
+
+ recvlineServer = self.serverProtocol()
+ insultsServer = insults.ServerProtocol(lambda: recvlineServer)
+ sshServer = sshFactory.buildProtocol(None)
+ clientTransport = LoopbackRelay(sshServer)
+
+ recvlineClient = NotifyingExpectableBuffer()
+ insultsClient = insults.ClientProtocol(lambda: recvlineClient)
+ sshClient = TestTransport(lambda: insultsClient, (), {}, u, p, self.WIDTH, self.HEIGHT)
+ serverTransport = LoopbackRelay(sshClient)
+
+ sshClient.makeConnection(clientTransport)
+ sshServer.makeConnection(serverTransport)
+
+ self.recvlineClient = recvlineClient
+ self.sshClient = sshClient
+ self.sshServer = sshServer
+ self.clientTransport = clientTransport
+ self.serverTransport = serverTransport
+
+ return recvlineClient.onConnection
+
+ def _testwrite(self, bytes):
+ self.sshClient.write(bytes)
+
+from twisted.conch.test import test_telnet
+
+class TestInsultsClientProtocol(insults.ClientProtocol,
+ test_telnet.TestProtocol):
+ pass
+
+
+class TestInsultsServerProtocol(insults.ServerProtocol,
+ test_telnet.TestProtocol):
+ pass
+
+class _TelnetMixin(_BaseMixin):
+ def setUp(self):
+ recvlineServer = self.serverProtocol()
+ insultsServer = TestInsultsServerProtocol(lambda: recvlineServer)
+ telnetServer = telnet.TelnetTransport(lambda: insultsServer)
+ clientTransport = LoopbackRelay(telnetServer)
+
+ recvlineClient = NotifyingExpectableBuffer()
+ insultsClient = TestInsultsClientProtocol(lambda: recvlineClient)
+ telnetClient = telnet.TelnetTransport(lambda: insultsClient)
+ serverTransport = LoopbackRelay(telnetClient)
+
+ telnetClient.makeConnection(clientTransport)
+ telnetServer.makeConnection(serverTransport)
+
+ serverTransport.clearBuffer()
+ clientTransport.clearBuffer()
+
+ self.recvlineClient = recvlineClient
+ self.telnetClient = telnetClient
+ self.clientTransport = clientTransport
+ self.serverTransport = serverTransport
+
+ return recvlineClient.onConnection
+
+ def _testwrite(self, bytes):
+ self.telnetClient.write(bytes)
+
+try:
+ from twisted.conch import stdio
+except ImportError:
+ stdio = None
+
+class _StdioMixin(_BaseMixin):
+ def setUp(self):
+ # A memory-only terminal emulator, into which the server will
+ # write things and make other state changes. What ends up
+ # here is basically what a user would have seen on their
+ # screen.
+ testTerminal = NotifyingExpectableBuffer()
+
+ # An insults client protocol which will translate bytes
+ # received from the child process into keystroke commands for
+ # an ITerminalProtocol.
+ insultsClient = insults.ClientProtocol(lambda: testTerminal)
+
+ # A process protocol which will translate stdout and stderr
+ # received from the child process to dataReceived calls and
+ # error reporting on an insults client protocol.
+ processClient = stdio.TerminalProcessProtocol(insultsClient)
+
+ # Run twisted/conch/stdio.py with the name of a class
+ # implementing ITerminalProtocol. This class will be used to
+ # handle bytes we send to the child process.
+ exe = sys.executable
+ module = stdio.__file__
+ if module.endswith('.pyc') or module.endswith('.pyo'):
+ module = module[:-1]
+ args = [exe, module, reflect.qual(self.serverProtocol)]
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
+
+ from twisted.internet import reactor
+ clientTransport = reactor.spawnProcess(processClient, exe, args,
+ env=env, usePTY=True)
+
+ self.recvlineClient = self.testTerminal = testTerminal
+ self.processClient = processClient
+ self.clientTransport = clientTransport
+
+ # Wait for the process protocol and test terminal to become
+ # connected before proceeding. The former should always
+ # happen first, but it doesn't hurt to be safe.
+ return defer.gatherResults(filter(None, [
+ processClient.onConnection,
+ testTerminal.expect(">>> ")]))
+
+ def tearDown(self):
+ # Kill the child process. We're done with it.
+ try:
+ self.clientTransport.signalProcess("KILL")
+ except (error.ProcessExitedAlready, OSError):
+ pass
+ def trap(failure):
+ failure.trap(error.ProcessTerminated)
+ self.assertEqual(failure.value.exitCode, None)
+ self.assertEqual(failure.value.status, 9)
+ return self.testTerminal.onDisconnection.addErrback(trap)
+
+ def _testwrite(self, bytes):
+ self.clientTransport.write(bytes)
+
+class RecvlineLoopbackMixin:
+ serverProtocol = EchoServer
+
+ def testSimple(self):
+ return self._trivialTest(
+ "first line\ndone",
+ [">>> first line",
+ "first line",
+ ">>> done"])
+
+ def testLeftArrow(self):
+ return self._trivialTest(
+ insert + 'first line' + left * 4 + "xxxx\ndone",
+ [">>> first xxxx",
+ "first xxxx",
+ ">>> done"])
+
+ def testRightArrow(self):
+ return self._trivialTest(
+ insert + 'right line' + left * 4 + right * 2 + "xx\ndone",
+ [">>> right lixx",
+ "right lixx",
+ ">>> done"])
+
+ def testBackspace(self):
+ return self._trivialTest(
+ "second line" + backspace * 4 + "xxxx\ndone",
+ [">>> second xxxx",
+ "second xxxx",
+ ">>> done"])
+
+ def testDelete(self):
+ return self._trivialTest(
+ "delete xxxx" + left * 4 + delete * 4 + "line\ndone",
+ [">>> delete line",
+ "delete line",
+ ">>> done"])
+
+ def testInsert(self):
+ return self._trivialTest(
+ "third ine" + left * 3 + "l\ndone",
+ [">>> third line",
+ "third line",
+ ">>> done"])
+
+ def testTypeover(self):
+ return self._trivialTest(
+ "fourth xine" + left * 4 + insert + "l\ndone",
+ [">>> fourth line",
+ "fourth line",
+ ">>> done"])
+
+ def testHome(self):
+ return self._trivialTest(
+ insert + "blah line" + home + "home\ndone",
+ [">>> home line",
+ "home line",
+ ">>> done"])
+
+ def testEnd(self):
+ return self._trivialTest(
+ "end " + left * 4 + end + "line\ndone",
+ [">>> end line",
+ "end line",
+ ">>> done"])
+
+class RecvlineLoopbackTelnet(_TelnetMixin, unittest.TestCase, RecvlineLoopbackMixin):
+ pass
+
+class RecvlineLoopbackSSH(_SSHMixin, unittest.TestCase, RecvlineLoopbackMixin):
+ pass
+
+class RecvlineLoopbackStdio(_StdioMixin, unittest.TestCase, RecvlineLoopbackMixin):
+ if stdio is None:
+ skip = "Terminal requirements missing, can't run recvline tests over stdio"
+
+
+class HistoricRecvlineLoopbackMixin:
+ serverProtocol = EchoServer
+
+ def testUpArrow(self):
+ return self._trivialTest(
+ "first line\n" + up + "\ndone",
+ [">>> first line",
+ "first line",
+ ">>> first line",
+ "first line",
+ ">>> done"])
+
+ def testDownArrow(self):
+ return self._trivialTest(
+ "first line\nsecond line\n" + up * 2 + down + "\ndone",
+ [">>> first line",
+ "first line",
+ ">>> second line",
+ "second line",
+ ">>> second line",
+ "second line",
+ ">>> done"])
+
+class HistoricRecvlineLoopbackTelnet(_TelnetMixin, unittest.TestCase, HistoricRecvlineLoopbackMixin):
+ pass
+
+class HistoricRecvlineLoopbackSSH(_SSHMixin, unittest.TestCase, HistoricRecvlineLoopbackMixin):
+ pass
+
+class HistoricRecvlineLoopbackStdio(_StdioMixin, unittest.TestCase, HistoricRecvlineLoopbackMixin):
+ if stdio is None:
+ skip = "Terminal requirements missing, can't run historic recvline tests over stdio"
diff --git a/twisted/conch/test/test_scripts.py b/twisted/conch/test/test_scripts.py
new file mode 100644
index 0000000..ae90e82
--- /dev/null
+++ b/twisted/conch/test/test_scripts.py
@@ -0,0 +1,82 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the command-line interfaces to conch.
+"""
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1Skip = "Cannot run without PyASN1"
+else:
+ pyasn1Skip = None
+
+try:
+ import Crypto
+except ImportError:
+ cryptoSkip = "can't run w/o PyCrypto"
+else:
+ cryptoSkip = None
+
+try:
+ import tty
+except ImportError:
+ ttySkip = "can't run w/o tty"
+else:
+ ttySkip = None
+
+try:
+ import Tkinter
+except ImportError:
+ tkskip = "can't run w/o Tkinter"
+else:
+ try:
+ Tkinter.Tk().destroy()
+ except Tkinter.TclError, e:
+ tkskip = "Can't test Tkinter: " + str(e)
+ else:
+ tkskip = None
+
+from twisted.trial.unittest import TestCase
+from twisted.scripts.test.test_scripts import ScriptTestsMixin
+from twisted.python.test.test_shellcomp import ZshScriptTestMixin
+
+
+
+class ScriptTests(TestCase, ScriptTestsMixin):
+ """
+ Tests for the Conch scripts.
+ """
+ skip = pyasn1Skip or cryptoSkip
+
+
+ def test_conch(self):
+ self.scriptTest("conch/conch")
+ test_conch.skip = ttySkip or skip
+
+
+ def test_cftp(self):
+ self.scriptTest("conch/cftp")
+ test_cftp.skip = ttySkip or skip
+
+
+ def test_ckeygen(self):
+ self.scriptTest("conch/ckeygen")
+
+
+ def test_tkconch(self):
+ self.scriptTest("conch/tkconch")
+ test_tkconch.skip = tkskip or skip
+
+
+
+class ZshIntegrationTestCase(TestCase, ZshScriptTestMixin):
+ """
+ Test that zsh completion functions are generated without error
+ """
+ generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
+ ('cftp', 'twisted.conch.scripts.cftp.ClientOptions'),
+ ('ckeygen', 'twisted.conch.scripts.ckeygen.GeneralOptions'),
+ ('tkconch', 'twisted.conch.scripts.tkconch.GeneralOptions'),
+ ]
diff --git a/twisted/conch/test/test_session.py b/twisted/conch/test/test_session.py
new file mode 100644
index 0000000..4db1629
--- /dev/null
+++ b/twisted/conch/test/test_session.py
@@ -0,0 +1,1256 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the 'session' channel implementation in twisted.conch.ssh.session.
+
+See also RFC 4254.
+"""
+
+import os, signal, sys, struct
+
+from zope.interface import implements
+
+from twisted.internet.address import IPv4Address
+from twisted.internet.error import ProcessTerminated, ProcessDone
+from twisted.python.failure import Failure
+from twisted.conch.ssh import common, session, connection
+from twisted.internet import defer, protocol, error
+from twisted.python import components, failure
+from twisted.trial import unittest
+
+
+
+class SubsystemOnlyAvatar(object):
+ """
+ A stub class representing an avatar that is only useful for
+ getting a subsystem.
+ """
+
+
+ def lookupSubsystem(self, name, data):
+ """
+ If the other side requests the 'subsystem' subsystem, allow it by
+ returning a MockProtocol to implement it. Otherwise, return
+ None which is interpreted by SSHSession as a failure.
+ """
+ if name == 'subsystem':
+ return MockProtocol()
+
+
+
+class StubAvatar:
+ """
+ A stub class representing the avatar representing the authenticated user.
+ It implements the I{ISession} interface.
+ """
+
+
+ def lookupSubsystem(self, name, data):
+ """
+ If the user requests the TestSubsystem subsystem, connect them to a
+ MockProtocol. If they request neither, then None is returned which is
+ interpreted by SSHSession as a failure.
+ """
+ if name == 'TestSubsystem':
+ self.subsystem = MockProtocol()
+ self.subsystem.packetData = data
+ return self.subsystem
+
+
+
+class StubSessionForStubAvatar(object):
+ """
+ A stub ISession implementation for our StubAvatar. The instance
+ variables generally keep track of method invocations so that we can test
+ that the methods were called.
+
+ @ivar avatar: the L{StubAvatar} we are adapting.
+ @ivar ptyRequest: if present, the terminal, window size, and modes passed
+ to the getPty method.
+ @ivar windowChange: if present, the window size passed to the
+ windowChangned method.
+ @ivar shellProtocol: if present, the L{SSHSessionProcessProtocol} passed
+ to the openShell method.
+ @ivar shellTransport: if present, the L{EchoTransport} connected to
+ shellProtocol.
+ @ivar execProtocol: if present, the L{SSHSessionProcessProtocol} passed
+ to the execCommand method.
+ @ivar execTransport: if present, the L{EchoTransport} connected to
+ execProtocol.
+ @ivar execCommandLine: if present, the command line passed to the
+ execCommand method.
+ @ivar gotEOF: if present, an EOF message was received.
+ @ivar gotClosed: if present, a closed message was received.
+ """
+
+
+ implements(session.ISession)
+
+
+ def __init__(self, avatar):
+ """
+ Store the avatar we're adapting.
+ """
+ self.avatar = avatar
+ self.shellProtocol = None
+
+
+ def getPty(self, terminal, window, modes):
+ """
+ If the terminal is 'bad', fail. Otherwise, store the information in
+ the ptyRequest variable.
+ """
+ if terminal != 'bad':
+ self.ptyRequest = (terminal, window, modes)
+ else:
+ raise RuntimeError('not getting a pty')
+
+
+ def windowChanged(self, window):
+ """
+ If all the window sizes are 0, fail. Otherwise, store the size in the
+ windowChange variable.
+ """
+ if window == (0, 0, 0, 0):
+ raise RuntimeError('not changing the window size')
+ else:
+ self.windowChange = window
+
+
+ def openShell(self, pp):
+ """
+ If we have gotten a shell request before, fail. Otherwise, store the
+ process protocol in the shellProtocol variable, connect it to the
+ EchoTransport and store that as shellTransport.
+ """
+ if self.shellProtocol is not None:
+ raise RuntimeError('not getting a shell this time')
+ else:
+ self.shellProtocol = pp
+ self.shellTransport = EchoTransport(pp)
+
+
+ def execCommand(self, pp, command):
+ """
+ If the command is 'true', store the command, the process protocol, and
+ the transport we connect to the process protocol. Otherwise, just
+ store the command and raise an error.
+ """
+ self.execCommandLine = command
+ if command == 'success':
+ self.execProtocol = pp
+ elif command[:6] == 'repeat':
+ self.execProtocol = pp
+ self.execTransport = EchoTransport(pp)
+ pp.outReceived(command[7:])
+ else:
+ raise RuntimeError('not getting a command')
+
+
+ def eofReceived(self):
+ """
+ Note that EOF has been received.
+ """
+ self.gotEOF = True
+
+
+ def closed(self):
+ """
+ Note that close has been received.
+ """
+ self.gotClosed = True
+
+
+
+components.registerAdapter(StubSessionForStubAvatar, StubAvatar,
+ session.ISession)
+
+
+
+
+class MockProcessProtocol(protocol.ProcessProtocol):
+ """
+ A mock ProcessProtocol which echoes back data sent to it and
+ appends a tilde. The tilde is appended so the tests can verify that
+ we received and processed the data.
+
+ @ivar packetData: C{str} of data to be sent when the connection is made.
+ @ivar data: a C{str} of data received.
+ @ivar err: a C{str} of error data received.
+ @ivar inConnectionOpen: True if the input side is open.
+ @ivar outConnectionOpen: True if the output side is open.
+ @ivar errConnectionOpen: True if the error side is open.
+ @ivar ended: False if the protocol has not ended, a C{Failure} if the
+ process has ended.
+ """
+ packetData = ''
+
+
+ def connectionMade(self):
+ """
+ Set up variables.
+ """
+ self.data = ''
+ self.err = ''
+ self.inConnectionOpen = True
+ self.outConnectionOpen = True
+ self.errConnectionOpen = True
+ self.ended = False
+ if self.packetData:
+ self.outReceived(self.packetData)
+
+
+ def outReceived(self, data):
+ """
+ Data was received. Store it and echo it back with a tilde.
+ """
+ self.data += data
+ if self.transport is not None:
+ self.transport.write(data + '~')
+
+
+ def errReceived(self, data):
+ """
+ Error data was received. Store it and echo it back backwards.
+ """
+ self.err += data
+ self.transport.write(data[::-1])
+
+
+ def inConnectionLost(self):
+ """
+ Close the input side.
+ """
+ self.inConnectionOpen = False
+
+
+ def outConnectionLost(self):
+ """
+ Close the output side.
+ """
+ self.outConnectionOpen = False
+
+
+ def errConnectionLost(self):
+ """
+ Close the error side.
+ """
+ self.errConnectionOpen = False
+
+
+ def processEnded(self, reason):
+ """
+ End the process and store the reason.
+ """
+ self.ended = reason
+
+
+
+class EchoTransport:
+ """
+ A transport for a ProcessProtocol which echos data that is sent to it with
+ a Window newline (CR LF) appended to it. If a null byte is in the data,
+ disconnect. When we are asked to disconnect, disconnect the
+ C{ProcessProtocol} with a 0 exit code.
+
+ @ivar proto: the C{ProcessProtocol} connected to us.
+ @ivar data: a C{str} of data written to us.
+ """
+
+
+ def __init__(self, processProtocol):
+ """
+ Initialize our instance variables.
+
+ @param processProtocol: a C{ProcessProtocol} to connect to ourself.
+ """
+ self.proto = processProtocol
+ self.closed = False
+ self.data = ''
+ processProtocol.makeConnection(self)
+
+
+ def write(self, data):
+ """
+ We got some data. Give it back to our C{ProcessProtocol} with
+ a newline attached. Disconnect if there's a null byte.
+ """
+ self.data += data
+ self.proto.outReceived(data)
+ self.proto.outReceived('\r\n')
+ if '\x00' in data: # mimic 'exit' for the shell test
+ self.loseConnection()
+
+
+ def loseConnection(self):
+ """
+ If we're asked to disconnect (and we haven't already) shut down
+ the C{ProcessProtocol} with a 0 exit code.
+ """
+ if self.closed:
+ return
+ self.closed = 1
+ self.proto.inConnectionLost()
+ self.proto.outConnectionLost()
+ self.proto.errConnectionLost()
+ self.proto.processEnded(failure.Failure(
+ error.ProcessTerminated(0, None, None)))
+
+
+
+class MockProtocol(protocol.Protocol):
+ """
+ A sample Protocol which stores the data passed to it.
+
+ @ivar packetData: a C{str} of data to be sent when the connection is made.
+ @ivar data: a C{str} of the data passed to us.
+ @ivar open: True if the channel is open.
+ @ivar reason: if not None, the reason the protocol was closed.
+ """
+ packetData = ''
+
+
+ def connectionMade(self):
+ """
+ Set up the instance variables. If we have any packetData, send it
+ along.
+ """
+
+ self.data = ''
+ self.open = True
+ self.reason = None
+ if self.packetData:
+ self.dataReceived(self.packetData)
+
+
+ def dataReceived(self, data):
+ """
+ Store the received data and write it back with a tilde appended.
+ The tilde is appended so that the tests can verify that we processed
+ the data.
+ """
+ self.data += data
+ if self.transport is not None:
+ self.transport.write(data + '~')
+
+
+ def connectionLost(self, reason):
+ """
+ Close the protocol and store the reason.
+ """
+ self.open = False
+ self.reason = reason
+
+
+
+class StubConnection(object):
+ """
+ A stub for twisted.conch.ssh.connection.SSHConnection. Record the data
+ that channels send, and when they try to close the connection.
+
+ @ivar data: a C{dict} mapping C{SSHChannel}s to a C{list} of C{str} of data
+ they sent.
+ @ivar extData: a C{dict} mapping L{SSHChannel}s to a C{list} of C{tuple} of
+ (C{int}, C{str}) of extended data they sent.
+ @ivar requests: a C{dict} mapping L{SSHChannel}s to a C{list} of C{tuple}
+ of (C{str}, C{str}) of channel requests they made.
+ @ivar eofs: a C{dict} mapping L{SSHChannel}s to C{true} if they have sent
+ an EOF.
+ @ivar closes: a C{dict} mapping L{SSHChannel}s to C{true} if they have sent
+ a close.
+ """
+
+
+ def __init__(self, transport=None):
+ """
+ Initialize our instance variables.
+ """
+ self.data = {}
+ self.extData = {}
+ self.requests = {}
+ self.eofs = {}
+ self.closes = {}
+ self.transport = transport
+
+
+ def logPrefix(self):
+ """
+ Return our logging prefix.
+ """
+ return "MockConnection"
+
+
+ def sendData(self, channel, data):
+ """
+ Record the sent data.
+ """
+ self.data.setdefault(channel, []).append(data)
+
+
+ def sendExtendedData(self, channel, type, data):
+ """
+ Record the sent extended data.
+ """
+ self.extData.setdefault(channel, []).append((type, data))
+
+
+ def sendRequest(self, channel, request, data, wantReply=False):
+ """
+ Record the sent channel request.
+ """
+ self.requests.setdefault(channel, []).append((request, data,
+ wantReply))
+ if wantReply:
+ return defer.succeed(None)
+
+
+ def sendEOF(self, channel):
+ """
+ Record the sent EOF.
+ """
+ self.eofs[channel] = True
+
+
+ def sendClose(self, channel):
+ """
+ Record the sent close.
+ """
+ self.closes[channel] = True
+
+
+
+class StubTransport:
+ """
+ A stub transport which records the data written.
+
+ @ivar buf: the data sent to the transport.
+ @type buf: C{str}
+
+ @ivar close: flags indicating if the transport has been closed.
+ @type close: C{bool}
+ """
+
+ buf = ''
+ close = False
+
+
+ def getPeer(self):
+ """
+ Return an arbitrary L{IAddress}.
+ """
+ return IPv4Address('TCP', 'remotehost', 8888)
+
+
+ def getHost(self):
+ """
+ Return an arbitrary L{IAddress}.
+ """
+ return IPv4Address('TCP', 'localhost', 9999)
+
+
+ def write(self, data):
+ """
+ Record data in the buffer.
+ """
+ self.buf += data
+
+
+ def loseConnection(self):
+ """
+ Note that the connection was closed.
+ """
+ self.close = True
+
+
+class StubTransportWithWriteErr(StubTransport):
+ """
+ A version of StubTransport which records the error data sent to it.
+
+ @ivar err: the extended data sent to the transport.
+ @type err: C{str}
+ """
+
+ err = ''
+
+
+ def writeErr(self, data):
+ """
+ Record the extended data in the buffer. This was an old interface
+ that allowed the Transports from ISession.openShell() or
+ ISession.execCommand() to receive extended data from the client.
+ """
+ self.err += data
+
+
+
+class StubClient(object):
+ """
+ A stub class representing the client to a SSHSession.
+
+ @ivar transport: A L{StubTransport} object which keeps track of the data
+ passed to it.
+ """
+
+
+ def __init__(self):
+ self.transport = StubTransportWithWriteErr()
+
+
+
+class SessionInterfaceTestCase(unittest.TestCase):
+ """
+ Tests for the SSHSession class interface. This interface is not ideal, but
+ it is tested in order to maintain backwards compatibility.
+ """
+
+
+ def setUp(self):
+ """
+ Make an SSHSession object to test. Give the channel some window
+ so that it's allowed to send packets. 500 and 100 are arbitrary
+ values.
+ """
+ self.session = session.SSHSession(remoteWindow=500,
+ remoteMaxPacket=100, conn=StubConnection(),
+ avatar=StubAvatar())
+
+
+ def assertSessionIsStubSession(self):
+ """
+ Asserts that self.session.session is an instance of
+ StubSessionForStubOldAvatar.
+ """
+ self.assertIsInstance(self.session.session,
+ StubSessionForStubAvatar)
+
+
+ def test_init(self):
+ """
+ SSHSession initializes its buffer (buf), client, and ISession adapter.
+ The avatar should not need to be adaptable to an ISession immediately.
+ """
+ s = session.SSHSession(avatar=object) # use object because it doesn't
+ # have an adapter
+ self.assertEqual(s.buf, '')
+ self.assertIdentical(s.client, None)
+ self.assertIdentical(s.session, None)
+
+
+ def test_client_dataReceived(self):
+ """
+ SSHSession.dataReceived() passes data along to a client. If the data
+ comes before there is a client, the data should be discarded.
+ """
+ self.session.dataReceived('1')
+ self.session.client = StubClient()
+ self.session.dataReceived('2')
+ self.assertEqual(self.session.client.transport.buf, '2')
+
+ def test_client_extReceived(self):
+ """
+ SSHSession.extReceived() passed data of type EXTENDED_DATA_STDERR along
+ to the client. If the data comes before there is a client, or if the
+ data is not of type EXTENDED_DATA_STDERR, it is discared.
+ """
+ self.session.extReceived(connection.EXTENDED_DATA_STDERR, '1')
+ self.session.extReceived(255, '2') # 255 is arbitrary
+ self.session.client = StubClient()
+ self.session.extReceived(connection.EXTENDED_DATA_STDERR, '3')
+ self.assertEqual(self.session.client.transport.err, '3')
+
+
+ def test_client_extReceivedWithoutWriteErr(self):
+ """
+ SSHSession.extReceived() should handle the case where the transport
+ on the client doesn't have a writeErr method.
+ """
+ client = self.session.client = StubClient()
+ client.transport = StubTransport() # doesn't have writeErr
+
+ # should not raise an error
+ self.session.extReceived(connection.EXTENDED_DATA_STDERR, 'ignored')
+
+
+
+ def test_client_closed(self):
+ """
+ SSHSession.closed() should tell the transport connected to the client
+ that the connection was lost.
+ """
+ self.session.client = StubClient()
+ self.session.closed()
+ self.assertTrue(self.session.client.transport.close)
+ self.session.client.transport.close = False
+
+
+ def test_badSubsystemDoesNotCreateClient(self):
+ """
+ When a subsystem request fails, SSHSession.client should not be set.
+ """
+ ret = self.session.requestReceived(
+ 'subsystem', common.NS('BadSubsystem'))
+ self.assertFalse(ret)
+ self.assertIdentical(self.session.client, None)
+
+
+ def test_lookupSubsystem(self):
+ """
+ When a client requests a subsystem, the SSHSession object should get
+ the subsystem by calling avatar.lookupSubsystem, and attach it as
+ the client.
+ """
+ ret = self.session.requestReceived(
+ 'subsystem', common.NS('TestSubsystem') + 'data')
+ self.assertTrue(ret)
+ self.assertIsInstance(self.session.client, protocol.ProcessProtocol)
+ self.assertIdentical(self.session.client.transport.proto,
+ self.session.avatar.subsystem)
+
+
+
+ def test_lookupSubsystemDoesNotNeedISession(self):
+ """
+ Previously, if one only wanted to implement a subsystem, an ISession
+ adapter wasn't needed because subsystems were looked up using the
+ lookupSubsystem method on the avatar.
+ """
+ s = session.SSHSession(avatar=SubsystemOnlyAvatar(),
+ conn=StubConnection())
+ ret = s.request_subsystem(
+ common.NS('subsystem') + 'data')
+ self.assertTrue(ret)
+ self.assertNotIdentical(s.client, None)
+ self.assertIdentical(s.conn.closes.get(s), None)
+ s.eofReceived()
+ self.assertTrue(s.conn.closes.get(s))
+ # these should not raise errors
+ s.loseConnection()
+ s.closed()
+
+
+ def test_lookupSubsystem_data(self):
+ """
+ After having looked up a subsystem, data should be passed along to the
+ client. Additionally, subsystems were passed the entire request packet
+ as data, instead of just the additional data.
+
+ We check for the additional tidle to verify that the data passed
+ through the client.
+ """
+ #self.session.dataReceived('1')
+ # subsystems didn't get extended data
+ #self.session.extReceived(connection.EXTENDED_DATA_STDERR, '2')
+
+ self.session.requestReceived('subsystem',
+ common.NS('TestSubsystem') + 'data')
+
+ self.assertEqual(self.session.conn.data[self.session],
+ ['\x00\x00\x00\x0dTestSubsystemdata~'])
+ self.session.dataReceived('more data')
+ self.assertEqual(self.session.conn.data[self.session][-1],
+ 'more data~')
+
+
+ def test_lookupSubsystem_closeReceived(self):
+ """
+ SSHSession.closeReceived() should sent a close message to the remote
+ side.
+ """
+ self.session.requestReceived('subsystem',
+ common.NS('TestSubsystem') + 'data')
+
+ self.session.closeReceived()
+ self.assertTrue(self.session.conn.closes[self.session])
+
+
+ def assertRequestRaisedRuntimeError(self):
+ """
+ Assert that the request we just made raised a RuntimeError (and only a
+ RuntimeError).
+ """
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1, "Multiple RuntimeErrors raised: %s" %
+ '\n'.join([repr(error) for error in errors]))
+ errors[0].trap(RuntimeError)
+
+
+ def test_requestShell(self):
+ """
+ When a client requests a shell, the SSHSession object should get
+ the shell by getting an ISession adapter for the avatar, then
+ calling openShell() with a ProcessProtocol to attach.
+ """
+ # gets a shell the first time
+ ret = self.session.requestReceived('shell', '')
+ self.assertTrue(ret)
+ self.assertSessionIsStubSession()
+ self.assertIsInstance(self.session.client,
+ session.SSHSessionProcessProtocol)
+ self.assertIdentical(self.session.session.shellProtocol,
+ self.session.client)
+ # doesn't get a shell the second time
+ self.assertFalse(self.session.requestReceived('shell', ''))
+ self.assertRequestRaisedRuntimeError()
+
+
+ def test_requestShellWithData(self):
+ """
+ When a client executes a shell, it should be able to give pass data
+ back and forth between the local and the remote side.
+ """
+ ret = self.session.requestReceived('shell', '')
+ self.assertTrue(ret)
+ self.assertSessionIsStubSession()
+ self.session.dataReceived('some data\x00')
+ self.assertEqual(self.session.session.shellTransport.data,
+ 'some data\x00')
+ self.assertEqual(self.session.conn.data[self.session],
+ ['some data\x00', '\r\n'])
+ self.assertTrue(self.session.session.shellTransport.closed)
+ self.assertEqual(self.session.conn.requests[self.session],
+ [('exit-status', '\x00\x00\x00\x00', False)])
+
+
+ def test_requestExec(self):
+ """
+ When a client requests a command, the SSHSession object should get
+ the command by getting an ISession adapter for the avatar, then
+ calling execCommand with a ProcessProtocol to attach and the
+ command line.
+ """
+ ret = self.session.requestReceived('exec',
+ common.NS('failure'))
+ self.assertFalse(ret)
+ self.assertRequestRaisedRuntimeError()
+ self.assertIdentical(self.session.client, None)
+
+ self.assertTrue(self.session.requestReceived('exec',
+ common.NS('success')))
+ self.assertSessionIsStubSession()
+ self.assertIsInstance(self.session.client,
+ session.SSHSessionProcessProtocol)
+ self.assertIdentical(self.session.session.execProtocol,
+ self.session.client)
+ self.assertEqual(self.session.session.execCommandLine,
+ 'success')
+
+
+ def test_requestExecWithData(self):
+ """
+ When a client executes a command, it should be able to give pass data
+ back and forth.
+ """
+ ret = self.session.requestReceived('exec',
+ common.NS('repeat hello'))
+ self.assertTrue(ret)
+ self.assertSessionIsStubSession()
+ self.session.dataReceived('some data')
+ self.assertEqual(self.session.session.execTransport.data, 'some data')
+ self.assertEqual(self.session.conn.data[self.session],
+ ['hello', 'some data', '\r\n'])
+ self.session.eofReceived()
+ self.session.closeReceived()
+ self.session.closed()
+ self.assertTrue(self.session.session.execTransport.closed)
+ self.assertEqual(self.session.conn.requests[self.session],
+ [('exit-status', '\x00\x00\x00\x00', False)])
+
+
+ def test_requestPty(self):
+ """
+ When a client requests a PTY, the SSHSession object should make
+ the request by getting an ISession adapter for the avatar, then
+ calling getPty with the terminal type, the window size, and any modes
+ the client gave us.
+ """
+ # 'bad' terminal type fails
+ ret = self.session.requestReceived(
+ 'pty_req', session.packRequest_pty_req(
+ 'bad', (1, 2, 3, 4), ''))
+ self.assertFalse(ret)
+ self.assertSessionIsStubSession()
+ self.assertRequestRaisedRuntimeError()
+ # 'good' terminal type succeeds
+ self.assertTrue(self.session.requestReceived('pty_req',
+ session.packRequest_pty_req('good', (1, 2, 3, 4), '')))
+ self.assertEqual(self.session.session.ptyRequest,
+ ('good', (1, 2, 3, 4), []))
+
+
+ def test_requestWindowChange(self):
+ """
+ When the client requests to change the window size, the SSHSession
+ object should make the request by getting an ISession adapter for the
+ avatar, then calling windowChanged with the new window size.
+ """
+ ret = self.session.requestReceived(
+ 'window_change',
+ session.packRequest_window_change((0, 0, 0, 0)))
+ self.assertFalse(ret)
+ self.assertRequestRaisedRuntimeError()
+ self.assertSessionIsStubSession()
+ self.assertTrue(self.session.requestReceived('window_change',
+ session.packRequest_window_change((1, 2, 3, 4))))
+ self.assertEqual(self.session.session.windowChange,
+ (1, 2, 3, 4))
+
+
+ def test_eofReceived(self):
+ """
+ When an EOF is received and a ISession adapter is present, it should
+ be notified of the EOF message.
+ """
+ self.session.session = session.ISession(self.session.avatar)
+ self.session.eofReceived()
+ self.assertTrue(self.session.session.gotEOF)
+
+
+ def test_closeReceived(self):
+ """
+ When a close is received, the session should send a close message.
+ """
+ ret = self.session.closeReceived()
+ self.assertIdentical(ret, None)
+ self.assertTrue(self.session.conn.closes[self.session])
+
+
+ def test_closed(self):
+ """
+ When a close is received and a ISession adapter is present, it should
+ be notified of the close message.
+ """
+ self.session.session = session.ISession(self.session.avatar)
+ self.session.closed()
+ self.assertTrue(self.session.session.gotClosed)
+
+
+
+class SessionWithNoAvatarTestCase(unittest.TestCase):
+ """
+ Test for the SSHSession interface. Several of the methods (request_shell,
+ request_exec, request_pty_req, request_window_change) would create a
+ 'session' instance variable from the avatar if one didn't exist when they
+ were called.
+ """
+
+
+ def setUp(self):
+ self.session = session.SSHSession()
+ self.session.avatar = StubAvatar()
+ self.assertIdentical(self.session.session, None)
+
+
+ def assertSessionProvidesISession(self):
+ """
+ self.session.session should provide I{ISession}.
+ """
+ self.assertTrue(session.ISession.providedBy(self.session.session),
+ "ISession not provided by %r" % self.session.session)
+
+
+ def test_requestShellGetsSession(self):
+ """
+ If an ISession adapter isn't already present, request_shell should get
+ one.
+ """
+ self.session.requestReceived('shell', '')
+ self.assertSessionProvidesISession()
+
+
+ def test_requestExecGetsSession(self):
+ """
+ If an ISession adapter isn't already present, request_exec should get
+ one.
+ """
+ self.session.requestReceived('exec',
+ common.NS('success'))
+ self.assertSessionProvidesISession()
+
+
+ def test_requestPtyReqGetsSession(self):
+ """
+ If an ISession adapter isn't already present, request_pty_req should
+ get one.
+ """
+ self.session.requestReceived('pty_req',
+ session.packRequest_pty_req(
+ 'term', (0, 0, 0, 0), ''))
+ self.assertSessionProvidesISession()
+
+
+ def test_requestWindowChangeGetsSession(self):
+ """
+ If an ISession adapter isn't already present, request_window_change
+ should get one.
+ """
+ self.session.requestReceived(
+ 'window_change',
+ session.packRequest_window_change(
+ (1, 1, 1, 1)))
+ self.assertSessionProvidesISession()
+
+
+
+class WrappersTestCase(unittest.TestCase):
+ """
+ A test for the wrapProtocol and wrapProcessProtocol functions.
+ """
+
+ def test_wrapProtocol(self):
+ """
+ L{wrapProtocol}, when passed a L{Protocol} should return something that
+ has write(), writeSequence(), loseConnection() methods which call the
+ Protocol's dataReceived() and connectionLost() methods, respectively.
+ """
+ protocol = MockProtocol()
+ protocol.transport = StubTransport()
+ protocol.connectionMade()
+ wrapped = session.wrapProtocol(protocol)
+ wrapped.dataReceived('dataReceived')
+ self.assertEqual(protocol.transport.buf, 'dataReceived')
+ wrapped.write('data')
+ wrapped.writeSequence(['1', '2'])
+ wrapped.loseConnection()
+ self.assertEqual(protocol.data, 'data12')
+ protocol.reason.trap(error.ConnectionDone)
+
+ def test_wrapProcessProtocol_Protocol(self):
+ """
+ L{wrapPRocessProtocol}, when passed a L{Protocol} should return
+ something that follows the L{IProcessProtocol} interface, with
+ connectionMade() mapping to connectionMade(), outReceived() mapping to
+ dataReceived() and processEnded() mapping to connectionLost().
+ """
+ protocol = MockProtocol()
+ protocol.transport = StubTransport()
+ process_protocol = session.wrapProcessProtocol(protocol)
+ process_protocol.connectionMade()
+ process_protocol.outReceived('data')
+ self.assertEqual(protocol.transport.buf, 'data~')
+ process_protocol.processEnded(failure.Failure(
+ error.ProcessTerminated(0, None, None)))
+ protocol.reason.trap(error.ProcessTerminated)
+
+
+
+class TestHelpers(unittest.TestCase):
+ """
+ Tests for the 4 helper functions: parseRequest_* and packRequest_*.
+ """
+
+
+ def test_parseRequest_pty_req(self):
+ """
+ The payload of a pty-req message is::
+ string terminal
+ uint32 columns
+ uint32 rows
+ uint32 x pixels
+ uint32 y pixels
+ string modes
+
+ Modes are::
+ byte mode number
+ uint32 mode value
+ """
+ self.assertEqual(session.parseRequest_pty_req(common.NS('xterm') +
+ struct.pack('>4L',
+ 1, 2, 3, 4)
+ + common.NS(
+ struct.pack('>BL', 5, 6))),
+ ('xterm', (2, 1, 3, 4), [(5, 6)]))
+
+
+ def test_packRequest_pty_req_old(self):
+ """
+ See test_parseRequest_pty_req for the payload format.
+ """
+ packed = session.packRequest_pty_req('xterm', (2, 1, 3, 4),
+ '\x05\x00\x00\x00\x06')
+
+ self.assertEqual(packed,
+ common.NS('xterm') + struct.pack('>4L', 1, 2, 3, 4) +
+ common.NS(struct.pack('>BL', 5, 6)))
+
+
+ def test_packRequest_pty_req(self):
+ """
+ See test_parseRequest_pty_req for the payload format.
+ """
+ packed = session.packRequest_pty_req('xterm', (2, 1, 3, 4),
+ '\x05\x00\x00\x00\x06')
+ self.assertEqual(packed,
+ common.NS('xterm') + struct.pack('>4L', 1, 2, 3, 4) +
+ common.NS(struct.pack('>BL', 5, 6)))
+
+
+ def test_parseRequest_window_change(self):
+ """
+ The payload of a window_change request is::
+ uint32 columns
+ uint32 rows
+ uint32 x pixels
+ uint32 y pixels
+
+ parseRequest_window_change() returns (rows, columns, x pixels,
+ y pixels).
+ """
+ self.assertEqual(session.parseRequest_window_change(
+ struct.pack('>4L', 1, 2, 3, 4)), (2, 1, 3, 4))
+
+
+ def test_packRequest_window_change(self):
+ """
+ See test_parseRequest_window_change for the payload format.
+ """
+ self.assertEqual(session.packRequest_window_change((2, 1, 3, 4)),
+ struct.pack('>4L', 1, 2, 3, 4))
+
+
+
+class SSHSessionProcessProtocolTestCase(unittest.TestCase):
+ """
+ Tests for L{SSHSessionProcessProtocol}.
+ """
+
+ def setUp(self):
+ self.transport = StubTransport()
+ self.session = session.SSHSession(
+ conn=StubConnection(self.transport), remoteWindow=500,
+ remoteMaxPacket=100)
+ self.pp = session.SSHSessionProcessProtocol(self.session)
+ self.pp.makeConnection(self.transport)
+
+
+ def assertSessionClosed(self):
+ """
+ Assert that C{self.session} is closed.
+ """
+ self.assertTrue(self.session.conn.closes[self.session])
+
+
+ def assertRequestsEqual(self, expectedRequests):
+ """
+ Assert that C{self.session} has sent the C{expectedRequests}.
+ """
+ self.assertEqual(
+ self.session.conn.requests[self.session],
+ expectedRequests)
+
+
+ def test_init(self):
+ """
+ SSHSessionProcessProtocol should set self.session to the session passed
+ to the __init__ method.
+ """
+ self.assertEqual(self.pp.session, self.session)
+
+
+ def test_getHost(self):
+ """
+ SSHSessionProcessProtocol.getHost() just delegates to its
+ session.conn.transport.getHost().
+ """
+ self.assertEqual(
+ self.session.conn.transport.getHost(), self.pp.getHost())
+
+
+ def test_getPeer(self):
+ """
+ SSHSessionProcessProtocol.getPeer() just delegates to its
+ session.conn.transport.getPeer().
+ """
+ self.assertEqual(
+ self.session.conn.transport.getPeer(), self.pp.getPeer())
+
+
+ def test_connectionMade(self):
+ """
+ SSHSessionProcessProtocol.connectionMade() should check if there's a
+ 'buf' attribute on its session and write it to the transport if so.
+ """
+ self.session.buf = 'buffer'
+ self.pp.connectionMade()
+ self.assertEqual(self.transport.buf, 'buffer')
+
+
+ def test_getSignalName(self):
+ """
+ _getSignalName should return the name of a signal when given the
+ signal number.
+ """
+ for signalName in session.SUPPORTED_SIGNALS:
+ signalName = 'SIG' + signalName
+ signalValue = getattr(signal, signalName)
+ sshName = self.pp._getSignalName(signalValue)
+ self.assertEqual(sshName, signalName,
+ "%i: %s != %s" % (signalValue, sshName,
+ signalName))
+
+
+ def test_getSignalNameWithLocalSignal(self):
+ """
+ If there are signals in the signal module which aren't in the SSH RFC,
+ we map their name to [signal name]@[platform].
+ """
+ signal.SIGTwistedTest = signal.NSIG + 1 # value can't exist normally
+ # Force reinitialization of signals
+ self.pp._signalValuesToNames = None
+ self.assertEqual(self.pp._getSignalName(signal.SIGTwistedTest),
+ 'SIGTwistedTest@' + sys.platform)
+
+
+ if getattr(signal, 'SIGALRM', None) is None:
+ test_getSignalName.skip = test_getSignalNameWithLocalSignal.skip = \
+ "Not all signals available"
+
+
+ def test_outReceived(self):
+ """
+ When data is passed to the outReceived method, it should be sent to
+ the session's write method.
+ """
+ self.pp.outReceived('test data')
+ self.assertEqual(self.session.conn.data[self.session],
+ ['test data'])
+
+
+ def test_write(self):
+ """
+ When data is passed to the write method, it should be sent to the
+ session channel's write method.
+ """
+ self.pp.write('test data')
+ self.assertEqual(self.session.conn.data[self.session],
+ ['test data'])
+
+ def test_writeSequence(self):
+ """
+ When a sequence is passed to the writeSequence method, it should be
+ joined together and sent to the session channel's write method.
+ """
+ self.pp.writeSequence(['test ', 'data'])
+ self.assertEqual(self.session.conn.data[self.session],
+ ['test data'])
+
+
+ def test_errReceived(self):
+ """
+ When data is passed to the errReceived method, it should be sent to
+ the session's writeExtended method.
+ """
+ self.pp.errReceived('test data')
+ self.assertEqual(self.session.conn.extData[self.session],
+ [(1, 'test data')])
+
+
+ def test_outConnectionLost(self):
+ """
+ When outConnectionLost and errConnectionLost are both called, we should
+ send an EOF message.
+ """
+ self.pp.outConnectionLost()
+ self.assertFalse(self.session in self.session.conn.eofs)
+ self.pp.errConnectionLost()
+ self.assertTrue(self.session.conn.eofs[self.session])
+
+
+ def test_errConnectionLost(self):
+ """
+ Make sure reverse ordering of events in test_outConnectionLost also
+ sends EOF.
+ """
+ self.pp.errConnectionLost()
+ self.assertFalse(self.session in self.session.conn.eofs)
+ self.pp.outConnectionLost()
+ self.assertTrue(self.session.conn.eofs[self.session])
+
+
+ def test_loseConnection(self):
+ """
+ When loseConnection() is called, it should call loseConnection
+ on the session channel.
+ """
+ self.pp.loseConnection()
+ self.assertTrue(self.session.conn.closes[self.session])
+
+
+ def test_connectionLost(self):
+ """
+ When connectionLost() is called, it should call loseConnection()
+ on the session channel.
+ """
+ self.pp.connectionLost(failure.Failure(
+ ProcessDone(0)))
+
+
+ def test_processEndedWithExitCode(self):
+ """
+ When processEnded is called, if there is an exit code in the reason
+ it should be sent in an exit-status method. The connection should be
+ closed.
+ """
+ self.pp.processEnded(Failure(ProcessDone(None)))
+ self.assertRequestsEqual(
+ [('exit-status', struct.pack('>I', 0) , False)])
+ self.assertSessionClosed()
+
+
+ def test_processEndedWithExitSignalCoreDump(self):
+ """
+ When processEnded is called, if there is an exit signal in the reason
+ it should be sent in an exit-signal message. The connection should be
+ closed.
+ """
+ self.pp.processEnded(
+ Failure(ProcessTerminated(1,
+ signal.SIGTERM, 1 << 7))) # 7th bit means core dumped
+ self.assertRequestsEqual(
+ [('exit-signal',
+ common.NS('TERM') # signal name
+ + '\x01' # core dumped is true
+ + common.NS('') # error message
+ + common.NS(''), # language tag
+ False)])
+ self.assertSessionClosed()
+
+
+ def test_processEndedWithExitSignalNoCoreDump(self):
+ """
+ When processEnded is called, if there is an exit signal in the
+ reason it should be sent in an exit-signal message. If no
+ core was dumped, don't set the core-dump bit.
+ """
+ self.pp.processEnded(
+ Failure(ProcessTerminated(1, signal.SIGTERM, 0)))
+ # see comments in test_processEndedWithExitSignalCoreDump for the
+ # meaning of the parts in the request
+ self.assertRequestsEqual(
+ [('exit-signal', common.NS('TERM') + '\x00' + common.NS('') +
+ common.NS(''), False)])
+ self.assertSessionClosed()
+
+
+ if getattr(os, 'WCOREDUMP', None) is None:
+ skipMsg = "can't run this w/o os.WCOREDUMP"
+ test_processEndedWithExitSignalCoreDump.skip = skipMsg
+ test_processEndedWithExitSignalNoCoreDump.skip = skipMsg
+
+
+
+class SSHSessionClientTestCase(unittest.TestCase):
+ """
+ SSHSessionClient is an obsolete class used to connect standard IO to
+ an SSHSession.
+ """
+
+
+ def test_dataReceived(self):
+ """
+ When data is received, it should be sent to the transport.
+ """
+ client = session.SSHSessionClient()
+ client.transport = StubTransport()
+ client.dataReceived('test data')
+ self.assertEqual(client.transport.buf, 'test data')
diff --git a/twisted/conch/test/test_ssh.py b/twisted/conch/test/test_ssh.py
new file mode 100644
index 0000000..6cf1a1a
--- /dev/null
+++ b/twisted/conch/test/test_ssh.py
@@ -0,0 +1,995 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.ssh}.
+"""
+
+import struct
+
+try:
+ import Crypto.Cipher.DES3
+except ImportError:
+ Crypto = None
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1 = None
+
+from twisted.conch.ssh import common, session, forwarding
+from twisted.conch import avatar, error
+from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
+from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
+from twisted.cred import portal
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer, protocol, reactor
+from twisted.internet.error import ProcessTerminated
+from twisted.python import failure, log
+from twisted.trial import unittest
+
+from twisted.conch.test.test_recvline import LoopbackRelay
+
+
+
+class ConchTestRealm(object):
+ """
+ A realm which expects a particular avatarId to log in once and creates a
+ L{ConchTestAvatar} for that request.
+
+ @ivar expectedAvatarID: The only avatarID that this realm will produce an
+ avatar for.
+
+ @ivar avatar: A reference to the avatar after it is requested.
+ """
+ avatar = None
+
+ def __init__(self, expectedAvatarID):
+ self.expectedAvatarID = expectedAvatarID
+
+
+ def requestAvatar(self, avatarID, mind, *interfaces):
+ """
+ Return a new L{ConchTestAvatar} if the avatarID matches the expected one
+ and this is the first avatar request.
+ """
+ if avatarID == self.expectedAvatarID:
+ if self.avatar is not None:
+ raise UnauthorizedLogin("Only one login allowed")
+ self.avatar = ConchTestAvatar()
+ return interfaces[0], self.avatar, self.avatar.logout
+ raise UnauthorizedLogin(
+ "Only %r may log in, not %r" % (self.expectedAvatarID, avatarID))
+
+
+
+class ConchTestAvatar(avatar.ConchUser):
+ """
+ An avatar against which various SSH features can be tested.
+
+ @ivar loggedOut: A flag indicating whether the avatar logout method has been
+ called.
+ """
+ loggedOut = False
+
+ def __init__(self):
+ avatar.ConchUser.__init__(self)
+ self.listeners = {}
+ self.globalRequests = {}
+ self.channelLookup.update({'session': session.SSHSession,
+ 'direct-tcpip':forwarding.openConnectForwardingClient})
+ self.subsystemLookup.update({'crazy': CrazySubsystem})
+
+
+ def global_foo(self, data):
+ self.globalRequests['foo'] = data
+ return 1
+
+
+ def global_foo_2(self, data):
+ self.globalRequests['foo_2'] = data
+ return 1, 'data'
+
+
+ def global_tcpip_forward(self, data):
+ host, port = forwarding.unpackGlobal_tcpip_forward(data)
+ try:
+ listener = reactor.listenTCP(
+ port, forwarding.SSHListenForwardingFactory(
+ self.conn, (host, port),
+ forwarding.SSHListenServerForwardingChannel),
+ interface=host)
+ except:
+ log.err(None, "something went wrong with remote->local forwarding")
+ return 0
+ else:
+ self.listeners[(host, port)] = listener
+ return 1
+
+
+ def global_cancel_tcpip_forward(self, data):
+ host, port = forwarding.unpackGlobal_tcpip_forward(data)
+ listener = self.listeners.get((host, port), None)
+ if not listener:
+ return 0
+ del self.listeners[(host, port)]
+ listener.stopListening()
+ return 1
+
+
+ def logout(self):
+ self.loggedOut = True
+ for listener in self.listeners.values():
+ log.msg('stopListening %s' % listener)
+ listener.stopListening()
+
+
+
+class ConchSessionForTestAvatar(object):
+ """
+ An ISession adapter for ConchTestAvatar.
+ """
+ def __init__(self, avatar):
+ """
+ Initialize the session and create a reference to it on the avatar for
+ later inspection.
+ """
+ self.avatar = avatar
+ self.avatar._testSession = self
+ self.cmd = None
+ self.proto = None
+ self.ptyReq = False
+ self.eof = 0
+ self.onClose = defer.Deferred()
+
+
+ def getPty(self, term, windowSize, attrs):
+ log.msg('pty req')
+ self._terminalType = term
+ self._windowSize = windowSize
+ self.ptyReq = True
+
+
+ def openShell(self, proto):
+ log.msg('opening shell')
+ self.proto = proto
+ EchoTransport(proto)
+ self.cmd = 'shell'
+
+
+ def execCommand(self, proto, cmd):
+ self.cmd = cmd
+ self.proto = proto
+ f = cmd.split()[0]
+ if f == 'false':
+ t = FalseTransport(proto)
+ # Avoid disconnecting this immediately. If the channel is closed
+ # before execCommand even returns the caller gets confused.
+ reactor.callLater(0, t.loseConnection)
+ elif f == 'echo':
+ t = EchoTransport(proto)
+ t.write(cmd[5:])
+ t.loseConnection()
+ elif f == 'secho':
+ t = SuperEchoTransport(proto)
+ t.write(cmd[6:])
+ t.loseConnection()
+ elif f == 'eecho':
+ t = ErrEchoTransport(proto)
+ t.write(cmd[6:])
+ t.loseConnection()
+ else:
+ raise error.ConchError('bad exec')
+ self.avatar.conn.transport.expectedLoseConnection = 1
+
+
+ def eofReceived(self):
+ self.eof = 1
+
+
+ def closed(self):
+ log.msg('closed cmd "%s"' % self.cmd)
+ self.remoteWindowLeftAtClose = self.proto.session.remoteWindowLeft
+ self.onClose.callback(None)
+
+from twisted.python import components
+components.registerAdapter(ConchSessionForTestAvatar, ConchTestAvatar, session.ISession)
+
+class CrazySubsystem(protocol.Protocol):
+
+ def __init__(self, *args, **kw):
+ pass
+
+ def connectionMade(self):
+ """
+ good ... good
+ """
+
+
+
+class FalseTransport:
+ """
+ False transport should act like a /bin/false execution, i.e. just exit with
+ nonzero status, writing nothing to the terminal.
+
+ @ivar proto: The protocol associated with this transport.
+ @ivar closed: A flag tracking whether C{loseConnection} has been called yet.
+ """
+
+ def __init__(self, p):
+ """
+ @type p L{twisted.conch.ssh.session.SSHSessionProcessProtocol} instance
+ """
+ self.proto = p
+ p.makeConnection(self)
+ self.closed = 0
+
+
+ def loseConnection(self):
+ """
+ Disconnect the protocol associated with this transport.
+ """
+ if self.closed:
+ return
+ self.closed = 1
+ self.proto.inConnectionLost()
+ self.proto.outConnectionLost()
+ self.proto.errConnectionLost()
+ self.proto.processEnded(failure.Failure(ProcessTerminated(255, None, None)))
+
+
+
+class EchoTransport:
+
+ def __init__(self, p):
+ self.proto = p
+ p.makeConnection(self)
+ self.closed = 0
+
+ def write(self, data):
+ log.msg(repr(data))
+ self.proto.outReceived(data)
+ self.proto.outReceived('\r\n')
+ if '\x00' in data: # mimic 'exit' for the shell test
+ self.loseConnection()
+
+ def loseConnection(self):
+ if self.closed: return
+ self.closed = 1
+ self.proto.inConnectionLost()
+ self.proto.outConnectionLost()
+ self.proto.errConnectionLost()
+ self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
+
+class ErrEchoTransport:
+
+ def __init__(self, p):
+ self.proto = p
+ p.makeConnection(self)
+ self.closed = 0
+
+ def write(self, data):
+ self.proto.errReceived(data)
+ self.proto.errReceived('\r\n')
+
+ def loseConnection(self):
+ if self.closed: return
+ self.closed = 1
+ self.proto.inConnectionLost()
+ self.proto.outConnectionLost()
+ self.proto.errConnectionLost()
+ self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
+
+class SuperEchoTransport:
+
+ def __init__(self, p):
+ self.proto = p
+ p.makeConnection(self)
+ self.closed = 0
+
+ def write(self, data):
+ self.proto.outReceived(data)
+ self.proto.outReceived('\r\n')
+ self.proto.errReceived(data)
+ self.proto.errReceived('\r\n')
+
+ def loseConnection(self):
+ if self.closed: return
+ self.closed = 1
+ self.proto.inConnectionLost()
+ self.proto.outConnectionLost()
+ self.proto.errConnectionLost()
+ self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
+
+
+if Crypto is not None and pyasn1 is not None:
+ from twisted.conch import checkers
+ from twisted.conch.ssh import channel, connection, factory, keys
+ from twisted.conch.ssh import transport, userauth
+
+ class UtilityTestCase(unittest.TestCase):
+ def testCounter(self):
+ c = transport._Counter('\x00\x00', 2)
+ for i in xrange(256 * 256):
+ self.assertEqual(c(), struct.pack('!H', (i + 1) % (2 ** 16)))
+ # It should wrap around, too.
+ for i in xrange(256 * 256):
+ self.assertEqual(c(), struct.pack('!H', (i + 1) % (2 ** 16)))
+
+
+ class ConchTestPublicKeyChecker(checkers.SSHPublicKeyDatabase):
+ def checkKey(self, credentials):
+ blob = keys.Key.fromString(publicDSA_openssh).blob()
+ if credentials.username == 'testuser' and credentials.blob == blob:
+ return True
+ return False
+
+
+ class ConchTestPasswordChecker:
+ credentialInterfaces = checkers.IUsernamePassword,
+
+ def requestAvatarId(self, credentials):
+ if credentials.username == 'testuser' and credentials.password == 'testpass':
+ return defer.succeed(credentials.username)
+ return defer.fail(Exception("Bad credentials"))
+
+
+ class ConchTestSSHChecker(checkers.SSHProtocolChecker):
+
+ def areDone(self, avatarId):
+ if avatarId != 'testuser' or len(self.successfulCredentials[avatarId]) < 2:
+ return False
+ return True
+
+ class ConchTestServerFactory(factory.SSHFactory):
+ noisy = 0
+
+ services = {
+ 'ssh-userauth':userauth.SSHUserAuthServer,
+ 'ssh-connection':connection.SSHConnection
+ }
+
+ def buildProtocol(self, addr):
+ proto = ConchTestServer()
+ proto.supportedPublicKeys = self.privateKeys.keys()
+ proto.factory = self
+
+ if hasattr(self, 'expectedLoseConnection'):
+ proto.expectedLoseConnection = self.expectedLoseConnection
+
+ self.proto = proto
+ return proto
+
+ def getPublicKeys(self):
+ return {
+ 'ssh-rsa': keys.Key.fromString(publicRSA_openssh),
+ 'ssh-dss': keys.Key.fromString(publicDSA_openssh)
+ }
+
+ def getPrivateKeys(self):
+ return {
+ 'ssh-rsa': keys.Key.fromString(privateRSA_openssh),
+ 'ssh-dss': keys.Key.fromString(privateDSA_openssh)
+ }
+
+ def getPrimes(self):
+ return {
+ 2048:[(transport.DH_GENERATOR, transport.DH_PRIME)]
+ }
+
+ def getService(self, trans, name):
+ return factory.SSHFactory.getService(self, trans, name)
+
+ class ConchTestBase:
+
+ done = 0
+
+ def connectionLost(self, reason):
+ if self.done:
+ return
+ if not hasattr(self,'expectedLoseConnection'):
+ unittest.fail('unexpectedly lost connection %s\n%s' % (self, reason))
+ self.done = 1
+
+ def receiveError(self, reasonCode, desc):
+ self.expectedLoseConnection = 1
+ # Some versions of OpenSSH (for example, OpenSSH_5.3p1) will
+ # send a DISCONNECT_BY_APPLICATION error before closing the
+ # connection. Other, older versions (for example,
+ # OpenSSH_5.1p1), won't. So accept this particular error here,
+ # but no others.
+ if reasonCode != transport.DISCONNECT_BY_APPLICATION:
+ log.err(
+ Exception(
+ 'got disconnect for %s: reason %s, desc: %s' % (
+ self, reasonCode, desc)))
+ self.loseConnection()
+
+ def receiveUnimplemented(self, seqID):
+ unittest.fail('got unimplemented: seqid %s' % seqID)
+ self.expectedLoseConnection = 1
+ self.loseConnection()
+
+ class ConchTestServer(ConchTestBase, transport.SSHServerTransport):
+
+ def connectionLost(self, reason):
+ ConchTestBase.connectionLost(self, reason)
+ transport.SSHServerTransport.connectionLost(self, reason)
+
+
+ class ConchTestClient(ConchTestBase, transport.SSHClientTransport):
+ """
+ @ivar _channelFactory: A callable which accepts an SSH connection and
+ returns a channel which will be attached to a new channel on that
+ connection.
+ """
+ def __init__(self, channelFactory):
+ self._channelFactory = channelFactory
+
+ def connectionLost(self, reason):
+ ConchTestBase.connectionLost(self, reason)
+ transport.SSHClientTransport.connectionLost(self, reason)
+
+ def verifyHostKey(self, key, fp):
+ keyMatch = key == keys.Key.fromString(publicRSA_openssh).blob()
+ fingerprintMatch = (
+ fp == '3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af')
+ if keyMatch and fingerprintMatch:
+ return defer.succeed(1)
+ return defer.fail(Exception("Key or fingerprint mismatch"))
+
+ def connectionSecure(self):
+ self.requestService(ConchTestClientAuth('testuser',
+ ConchTestClientConnection(self._channelFactory)))
+
+
+ class ConchTestClientAuth(userauth.SSHUserAuthClient):
+
+ hasTriedNone = 0 # have we tried the 'none' auth yet?
+ canSucceedPublicKey = 0 # can we succed with this yet?
+ canSucceedPassword = 0
+
+ def ssh_USERAUTH_SUCCESS(self, packet):
+ if not self.canSucceedPassword and self.canSucceedPublicKey:
+ unittest.fail('got USERAUTH_SUCESS before password and publickey')
+ userauth.SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
+
+ def getPassword(self):
+ self.canSucceedPassword = 1
+ return defer.succeed('testpass')
+
+ def getPrivateKey(self):
+ self.canSucceedPublicKey = 1
+ return defer.succeed(keys.Key.fromString(privateDSA_openssh))
+
+ def getPublicKey(self):
+ return keys.Key.fromString(publicDSA_openssh)
+
+
+ class ConchTestClientConnection(connection.SSHConnection):
+ """
+ @ivar _completed: A L{Deferred} which will be fired when the number of
+ results collected reaches C{totalResults}.
+ """
+ name = 'ssh-connection'
+ results = 0
+ totalResults = 8
+
+ def __init__(self, channelFactory):
+ connection.SSHConnection.__init__(self)
+ self._channelFactory = channelFactory
+
+ def serviceStarted(self):
+ self.openChannel(self._channelFactory(conn=self))
+
+
+ class SSHTestChannel(channel.SSHChannel):
+
+ def __init__(self, name, opened, *args, **kwargs):
+ self.name = name
+ self._opened = opened
+ self.received = []
+ self.receivedExt = []
+ self.onClose = defer.Deferred()
+ channel.SSHChannel.__init__(self, *args, **kwargs)
+
+
+ def openFailed(self, reason):
+ self._opened.errback(reason)
+
+
+ def channelOpen(self, ignore):
+ self._opened.callback(self)
+
+
+ def dataReceived(self, data):
+ self.received.append(data)
+
+
+ def extReceived(self, dataType, data):
+ if dataType == connection.EXTENDED_DATA_STDERR:
+ self.receivedExt.append(data)
+ else:
+ log.msg("Unrecognized extended data: %r" % (dataType,))
+
+
+ def request_exit_status(self, status):
+ [self.status] = struct.unpack('>L', status)
+
+
+ def eofReceived(self):
+ self.eofCalled = True
+
+
+ def closed(self):
+ self.onClose.callback(None)
+
+
+
+class SSHProtocolTestCase(unittest.TestCase):
+ """
+ Tests for communication between L{SSHServerTransport} and
+ L{SSHClientTransport}.
+ """
+
+ if not Crypto:
+ skip = "can't run w/o PyCrypto"
+
+ if not pyasn1:
+ skip = "Cannot run without PyASN1"
+
+ def _ourServerOurClientTest(self, name='session', **kwargs):
+ """
+ Create a connected SSH client and server protocol pair and return a
+ L{Deferred} which fires with an L{SSHTestChannel} instance connected to
+ a channel on that SSH connection.
+ """
+ result = defer.Deferred()
+ self.realm = ConchTestRealm('testuser')
+ p = portal.Portal(self.realm)
+ sshpc = ConchTestSSHChecker()
+ sshpc.registerChecker(ConchTestPasswordChecker())
+ sshpc.registerChecker(ConchTestPublicKeyChecker())
+ p.registerChecker(sshpc)
+ fac = ConchTestServerFactory()
+ fac.portal = p
+ fac.startFactory()
+ self.server = fac.buildProtocol(None)
+ self.clientTransport = LoopbackRelay(self.server)
+ self.client = ConchTestClient(
+ lambda conn: SSHTestChannel(name, result, conn=conn, **kwargs))
+
+ self.serverTransport = LoopbackRelay(self.client)
+
+ self.server.makeConnection(self.serverTransport)
+ self.client.makeConnection(self.clientTransport)
+ return result
+
+
+ def test_subsystemsAndGlobalRequests(self):
+ """
+ Run the Conch server against the Conch client. Set up several different
+ channels which exercise different behaviors and wait for them to
+ complete. Verify that the channels with errors log them.
+ """
+ channel = self._ourServerOurClientTest()
+
+ def cbSubsystem(channel):
+ self.channel = channel
+ return self.assertFailure(
+ channel.conn.sendRequest(
+ channel, 'subsystem', common.NS('not-crazy'), 1),
+ Exception)
+ channel.addCallback(cbSubsystem)
+
+ def cbNotCrazyFailed(ignored):
+ channel = self.channel
+ return channel.conn.sendRequest(
+ channel, 'subsystem', common.NS('crazy'), 1)
+ channel.addCallback(cbNotCrazyFailed)
+
+ def cbGlobalRequests(ignored):
+ channel = self.channel
+ d1 = channel.conn.sendGlobalRequest('foo', 'bar', 1)
+
+ d2 = channel.conn.sendGlobalRequest('foo-2', 'bar2', 1)
+ d2.addCallback(self.assertEqual, 'data')
+
+ d3 = self.assertFailure(
+ channel.conn.sendGlobalRequest('bar', 'foo', 1),
+ Exception)
+
+ return defer.gatherResults([d1, d2, d3])
+ channel.addCallback(cbGlobalRequests)
+
+ def disconnect(ignored):
+ self.assertEqual(
+ self.realm.avatar.globalRequests,
+ {"foo": "bar", "foo_2": "bar2"})
+ channel = self.channel
+ channel.conn.transport.expectedLoseConnection = True
+ channel.conn.serviceStopped()
+ channel.loseConnection()
+ channel.addCallback(disconnect)
+
+ return channel
+
+
+ def test_shell(self):
+ """
+ L{SSHChannel.sendRequest} can open a shell with a I{pty-req} request,
+ specifying a terminal type and window size.
+ """
+ channel = self._ourServerOurClientTest()
+
+ data = session.packRequest_pty_req('conch-test-term', (24, 80, 0, 0), '')
+ def cbChannel(channel):
+ self.channel = channel
+ return channel.conn.sendRequest(channel, 'pty-req', data, 1)
+ channel.addCallback(cbChannel)
+
+ def cbPty(ignored):
+ # The server-side object corresponding to our client side channel.
+ session = self.realm.avatar.conn.channels[0].session
+ self.assertIdentical(session.avatar, self.realm.avatar)
+ self.assertEqual(session._terminalType, 'conch-test-term')
+ self.assertEqual(session._windowSize, (24, 80, 0, 0))
+ self.assertTrue(session.ptyReq)
+ channel = self.channel
+ return channel.conn.sendRequest(channel, 'shell', '', 1)
+ channel.addCallback(cbPty)
+
+ def cbShell(ignored):
+ self.channel.write('testing the shell!\x00')
+ self.channel.conn.sendEOF(self.channel)
+ return defer.gatherResults([
+ self.channel.onClose,
+ self.realm.avatar._testSession.onClose])
+ channel.addCallback(cbShell)
+
+ def cbExited(ignored):
+ if self.channel.status != 0:
+ log.msg(
+ 'shell exit status was not 0: %i' % (self.channel.status,))
+ self.assertEqual(
+ "".join(self.channel.received),
+ 'testing the shell!\x00\r\n')
+ self.assertTrue(self.channel.eofCalled)
+ self.assertTrue(
+ self.realm.avatar._testSession.eof)
+ channel.addCallback(cbExited)
+ return channel
+
+
+ def test_failedExec(self):
+ """
+ If L{SSHChannel.sendRequest} issues an exec which the server responds to
+ with an error, the L{Deferred} it returns fires its errback.
+ """
+ channel = self._ourServerOurClientTest()
+
+ def cbChannel(channel):
+ self.channel = channel
+ return self.assertFailure(
+ channel.conn.sendRequest(
+ channel, 'exec', common.NS('jumboliah'), 1),
+ Exception)
+ channel.addCallback(cbChannel)
+
+ def cbFailed(ignored):
+ # The server logs this exception when it cannot perform the
+ # requested exec.
+ errors = self.flushLoggedErrors(error.ConchError)
+ self.assertEqual(errors[0].value.args, ('bad exec', None))
+ channel.addCallback(cbFailed)
+ return channel
+
+
+ def test_falseChannel(self):
+ """
+ When the process started by a L{SSHChannel.sendRequest} exec request
+ exits, the exit status is reported to the channel.
+ """
+ channel = self._ourServerOurClientTest()
+
+ def cbChannel(channel):
+ self.channel = channel
+ return channel.conn.sendRequest(
+ channel, 'exec', common.NS('false'), 1)
+ channel.addCallback(cbChannel)
+
+ def cbExec(ignored):
+ return self.channel.onClose
+ channel.addCallback(cbExec)
+
+ def cbClosed(ignored):
+ # No data is expected
+ self.assertEqual(self.channel.received, [])
+ self.assertNotEquals(self.channel.status, 0)
+ channel.addCallback(cbClosed)
+ return channel
+
+
+ def test_errorChannel(self):
+ """
+ Bytes sent over the extended channel for stderr data are delivered to
+ the channel's C{extReceived} method.
+ """
+ channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
+
+ def cbChannel(channel):
+ self.channel = channel
+ return channel.conn.sendRequest(
+ channel, 'exec', common.NS('eecho hello'), 1)
+ channel.addCallback(cbChannel)
+
+ def cbExec(ignored):
+ return defer.gatherResults([
+ self.channel.onClose,
+ self.realm.avatar._testSession.onClose])
+ channel.addCallback(cbExec)
+
+ def cbClosed(ignored):
+ self.assertEqual(self.channel.received, [])
+ self.assertEqual("".join(self.channel.receivedExt), "hello\r\n")
+ self.assertEqual(self.channel.status, 0)
+ self.assertTrue(self.channel.eofCalled)
+ self.assertEqual(self.channel.localWindowLeft, 4)
+ self.assertEqual(
+ self.channel.localWindowLeft,
+ self.realm.avatar._testSession.remoteWindowLeftAtClose)
+ channel.addCallback(cbClosed)
+ return channel
+
+
+ def test_unknownChannel(self):
+ """
+ When an attempt is made to open an unknown channel type, the L{Deferred}
+ returned by L{SSHChannel.sendRequest} fires its errback.
+ """
+ d = self.assertFailure(
+ self._ourServerOurClientTest('crazy-unknown-channel'), Exception)
+ def cbFailed(ignored):
+ errors = self.flushLoggedErrors(error.ConchError)
+ self.assertEqual(errors[0].value.args, (3, 'unknown channel'))
+ self.assertEqual(len(errors), 1)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_maxPacket(self):
+ """
+ An L{SSHChannel} can be configured with a maximum packet size to
+ receive.
+ """
+ # localWindow needs to be at least 11 otherwise the assertion about it
+ # in cbClosed is invalid.
+ channel = self._ourServerOurClientTest(
+ localWindow=11, localMaxPacket=1)
+
+ def cbChannel(channel):
+ self.channel = channel
+ return channel.conn.sendRequest(
+ channel, 'exec', common.NS('secho hello'), 1)
+ channel.addCallback(cbChannel)
+
+ def cbExec(ignored):
+ return self.channel.onClose
+ channel.addCallback(cbExec)
+
+ def cbClosed(ignored):
+ self.assertEqual(self.channel.status, 0)
+ self.assertEqual("".join(self.channel.received), "hello\r\n")
+ self.assertEqual("".join(self.channel.receivedExt), "hello\r\n")
+ self.assertEqual(self.channel.localWindowLeft, 11)
+ self.assertTrue(self.channel.eofCalled)
+ channel.addCallback(cbClosed)
+ return channel
+
+
+ def test_echo(self):
+ """
+ Normal standard out bytes are sent to the channel's C{dataReceived}
+ method.
+ """
+ channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
+
+ def cbChannel(channel):
+ self.channel = channel
+ return channel.conn.sendRequest(
+ channel, 'exec', common.NS('echo hello'), 1)
+ channel.addCallback(cbChannel)
+
+ def cbEcho(ignored):
+ return defer.gatherResults([
+ self.channel.onClose,
+ self.realm.avatar._testSession.onClose])
+ channel.addCallback(cbEcho)
+
+ def cbClosed(ignored):
+ self.assertEqual(self.channel.status, 0)
+ self.assertEqual("".join(self.channel.received), "hello\r\n")
+ self.assertEqual(self.channel.localWindowLeft, 4)
+ self.assertTrue(self.channel.eofCalled)
+ self.assertEqual(
+ self.channel.localWindowLeft,
+ self.realm.avatar._testSession.remoteWindowLeftAtClose)
+ channel.addCallback(cbClosed)
+ return channel
+
+
+
+class TestSSHFactory(unittest.TestCase):
+
+ if not Crypto:
+ skip = "can't run w/o PyCrypto"
+
+ if not pyasn1:
+ skip = "Cannot run without PyASN1"
+
+ def makeSSHFactory(self, primes=None):
+ sshFactory = factory.SSHFactory()
+ gpk = lambda: {'ssh-rsa' : keys.Key(None)}
+ sshFactory.getPrimes = lambda: primes
+ sshFactory.getPublicKeys = sshFactory.getPrivateKeys = gpk
+ sshFactory.startFactory()
+ return sshFactory
+
+
+ def test_buildProtocol(self):
+ """
+ By default, buildProtocol() constructs an instance of
+ SSHServerTransport.
+ """
+ factory = self.makeSSHFactory()
+ protocol = factory.buildProtocol(None)
+ self.assertIsInstance(protocol, transport.SSHServerTransport)
+
+
+ def test_buildProtocolRespectsProtocol(self):
+ """
+ buildProtocol() calls 'self.protocol()' to construct a protocol
+ instance.
+ """
+ calls = []
+ def makeProtocol(*args):
+ calls.append(args)
+ return transport.SSHServerTransport()
+ factory = self.makeSSHFactory()
+ factory.protocol = makeProtocol
+ factory.buildProtocol(None)
+ self.assertEqual([()], calls)
+
+
+ def test_multipleFactories(self):
+ f1 = self.makeSSHFactory(primes=None)
+ f2 = self.makeSSHFactory(primes={1:(2,3)})
+ p1 = f1.buildProtocol(None)
+ p2 = f2.buildProtocol(None)
+ self.failIf('diffie-hellman-group-exchange-sha1' in p1.supportedKeyExchanges,
+ p1.supportedKeyExchanges)
+ self.failUnless('diffie-hellman-group-exchange-sha1' in p2.supportedKeyExchanges,
+ p2.supportedKeyExchanges)
+
+
+
+class MPTestCase(unittest.TestCase):
+ """
+ Tests for L{common.getMP}.
+
+ @cvar getMP: a method providing a MP parser.
+ @type getMP: C{callable}
+ """
+ getMP = staticmethod(common.getMP)
+
+ if not Crypto:
+ skip = "can't run w/o PyCrypto"
+
+ if not pyasn1:
+ skip = "Cannot run without PyASN1"
+
+
+ def test_getMP(self):
+ """
+ L{common.getMP} should parse the a multiple precision integer from a
+ string: a 4-byte length followed by length bytes of the integer.
+ """
+ self.assertEqual(
+ self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01'),
+ (1, ''))
+
+
+ def test_getMPBigInteger(self):
+ """
+ L{common.getMP} should be able to parse a big enough integer
+ (that doesn't fit on one byte).
+ """
+ self.assertEqual(
+ self.getMP('\x00\x00\x00\x04\x01\x02\x03\x04'),
+ (16909060, ''))
+
+
+ def test_multipleGetMP(self):
+ """
+ L{common.getMP} has the ability to parse multiple integer in the same
+ string.
+ """
+ self.assertEqual(
+ self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01'
+ '\x00\x00\x00\x04\x00\x00\x00\x02', 2),
+ (1, 2, ''))
+
+
+ def test_getMPRemainingData(self):
+ """
+ When more data than needed is sent to L{common.getMP}, it should return
+ the remaining data.
+ """
+ self.assertEqual(
+ self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01foo'),
+ (1, 'foo'))
+
+
+ def test_notEnoughData(self):
+ """
+ When the string passed to L{common.getMP} doesn't even make 5 bytes,
+ it should raise a L{struct.error}.
+ """
+ self.assertRaises(struct.error, self.getMP, '\x02\x00')
+
+
+
+class PyMPTestCase(MPTestCase):
+ """
+ Tests for the python implementation of L{common.getMP}.
+ """
+ getMP = staticmethod(common.getMP_py)
+
+
+
+class GMPYMPTestCase(MPTestCase):
+ """
+ Tests for the gmpy implementation of L{common.getMP}.
+ """
+ getMP = staticmethod(common._fastgetMP)
+
+
+class BuiltinPowHackTestCase(unittest.TestCase):
+ """
+ Tests that the builtin pow method is still correct after
+ L{twisted.conch.ssh.common} monkeypatches it to use gmpy.
+ """
+
+ def test_floatBase(self):
+ """
+ pow gives the correct result when passed a base of type float with a
+ non-integer value.
+ """
+ self.assertEqual(6.25, pow(2.5, 2))
+
+ def test_intBase(self):
+ """
+ pow gives the correct result when passed a base of type int.
+ """
+ self.assertEqual(81, pow(3, 4))
+
+ def test_longBase(self):
+ """
+ pow gives the correct result when passed a base of type long.
+ """
+ self.assertEqual(81, pow(3, 4))
+
+ def test_mpzBase(self):
+ """
+ pow gives the correct result when passed a base of type gmpy.mpz.
+ """
+ if gmpy is None:
+ raise unittest.SkipTest('gmpy not available')
+ self.assertEqual(81, pow(gmpy.mpz(3), 4))
+
+
+try:
+ import gmpy
+except ImportError:
+ GMPYMPTestCase.skip = "gmpy not available"
+ gmpy = None
diff --git a/twisted/conch/test/test_tap.py b/twisted/conch/test/test_tap.py
new file mode 100644
index 0000000..44acecd
--- /dev/null
+++ b/twisted/conch/test/test_tap.py
@@ -0,0 +1,173 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.tap}.
+"""
+
+try:
+ import Crypto.Cipher.DES3
+except:
+ Crypto = None
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1 = None
+
+try:
+ from twisted.conch import unix
+except ImportError:
+ unix = None
+
+if Crypto and pyasn1 and unix:
+ from twisted.conch import tap
+ from twisted.conch.openssh_compat.factory import OpenSSHFactory
+
+from twisted.python.compat import set
+from twisted.application.internet import StreamServerEndpointService
+from twisted.cred import error
+from twisted.cred.credentials import IPluggableAuthenticationModules
+from twisted.cred.credentials import ISSHPrivateKey
+from twisted.cred.credentials import IUsernamePassword, UsernamePassword
+
+from twisted.trial.unittest import TestCase
+
+
+
+class MakeServiceTest(TestCase):
+ """
+ Tests for L{tap.makeService}.
+ """
+
+ if not Crypto:
+ skip = "can't run w/o PyCrypto"
+
+ if not pyasn1:
+ skip = "Cannot run without PyASN1"
+
+ if not unix:
+ skip = "can't run on non-posix computers"
+
+ usernamePassword = ('iamuser', 'thisispassword')
+
+ def setUp(self):
+ """
+ Create a file with two users.
+ """
+ self.filename = self.mktemp()
+ f = open(self.filename, 'wb+')
+ f.write(':'.join(self.usernamePassword))
+ f.close()
+ self.options = tap.Options()
+
+
+ def test_basic(self):
+ """
+ L{tap.makeService} returns a L{StreamServerEndpointService} instance
+ running on TCP port 22, and the linked protocol factory is an instance
+ of L{OpenSSHFactory}.
+ """
+ config = tap.Options()
+ service = tap.makeService(config)
+ self.assertIsInstance(service, StreamServerEndpointService)
+ self.assertEqual(service.endpoint._port, 22)
+ self.assertIsInstance(service.factory, OpenSSHFactory)
+
+
+ def test_defaultAuths(self):
+ """
+ Make sure that if the C{--auth} command-line option is not passed,
+ the default checkers are (for backwards compatibility): SSH, UNIX, and
+ PAM if available
+ """
+ numCheckers = 2
+ try:
+ from twisted.cred import pamauth
+ self.assertIn(IPluggableAuthenticationModules,
+ self.options['credInterfaces'],
+ "PAM should be one of the modules")
+ numCheckers += 1
+ except ImportError:
+ pass
+
+ self.assertIn(ISSHPrivateKey, self.options['credInterfaces'],
+ "SSH should be one of the default checkers")
+ self.assertIn(IUsernamePassword, self.options['credInterfaces'],
+ "UNIX should be one of the default checkers")
+ self.assertEqual(numCheckers, len(self.options['credCheckers']),
+ "There should be %d checkers by default" % (numCheckers,))
+
+
+ def test_authAdded(self):
+ """
+ The C{--auth} command-line option will add a checker to the list of
+ checkers, and it should be the only auth checker
+ """
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ self.assertEqual(len(self.options['credCheckers']), 1)
+
+
+ def test_authFailure(self):
+ """
+ The checker created by the C{--auth} command-line option returns a
+ L{Deferred} that fails with L{UnauthorizedLogin} when
+ presented with credentials that are unknown to that checker.
+ """
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ checker = self.options['credCheckers'][-1]
+ invalid = UsernamePassword(self.usernamePassword[0], 'fake')
+ # Wrong password should raise error
+ return self.assertFailure(
+ checker.requestAvatarId(invalid), error.UnauthorizedLogin)
+
+
+ def test_authSuccess(self):
+ """
+ The checker created by the C{--auth} command-line option returns a
+ L{Deferred} that returns the avatar id when presented with credentials
+ that are known to that checker.
+ """
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ checker = self.options['credCheckers'][-1]
+ correct = UsernamePassword(*self.usernamePassword)
+ d = checker.requestAvatarId(correct)
+
+ def checkSuccess(username):
+ self.assertEqual(username, correct.username)
+
+ return d.addCallback(checkSuccess)
+
+
+ def test_checkersPamAuth(self):
+ """
+ The L{OpenSSHFactory} built by L{tap.makeService} has a portal with
+ L{IPluggableAuthenticationModules}, L{ISSHPrivateKey} and
+ L{IUsernamePassword} interfaces registered as checkers if C{pamauth} is
+ available.
+ """
+ # Fake the presence of pamauth, even if PyPAM is not installed
+ self.patch(tap, "pamauth", object())
+ config = tap.Options()
+ service = tap.makeService(config)
+ portal = service.factory.portal
+ self.assertEqual(
+ set(portal.checkers.keys()),
+ set([IPluggableAuthenticationModules, ISSHPrivateKey,
+ IUsernamePassword]))
+
+
+ def test_checkersWithoutPamAuth(self):
+ """
+ The L{OpenSSHFactory} built by L{tap.makeService} has a portal with
+ L{ISSHPrivateKey} and L{IUsernamePassword} interfaces registered as
+ checkers if C{pamauth} is not available.
+ """
+ # Fake the absence of pamauth, even if PyPAM is installed
+ self.patch(tap, "pamauth", None)
+ config = tap.Options()
+ service = tap.makeService(config)
+ portal = service.factory.portal
+ self.assertEqual(
+ set(portal.checkers.keys()),
+ set([ISSHPrivateKey, IUsernamePassword]))
diff --git a/twisted/conch/test/test_telnet.py b/twisted/conch/test/test_telnet.py
new file mode 100644
index 0000000..9b5bf76
--- /dev/null
+++ b/twisted/conch/test/test_telnet.py
@@ -0,0 +1,767 @@
+# -*- test-case-name: twisted.conch.test.test_telnet -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.conch.telnet}.
+"""
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.internet import defer
+
+from twisted.conch import telnet
+
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+
+class TestProtocol:
+ implements(telnet.ITelnetProtocol)
+
+ localEnableable = ()
+ remoteEnableable = ()
+
+ def __init__(self):
+ self.bytes = ''
+ self.subcmd = ''
+ self.calls = []
+
+ self.enabledLocal = []
+ self.enabledRemote = []
+ self.disabledLocal = []
+ self.disabledRemote = []
+
+ def makeConnection(self, transport):
+ d = transport.negotiationMap = {}
+ d['\x12'] = self.neg_TEST_COMMAND
+
+ d = transport.commandMap = transport.commandMap.copy()
+ for cmd in ('NOP', 'DM', 'BRK', 'IP', 'AO', 'AYT', 'EC', 'EL', 'GA'):
+ d[getattr(telnet, cmd)] = lambda arg, cmd=cmd: self.calls.append(cmd)
+
+ def dataReceived(self, bytes):
+ self.bytes += bytes
+
+ def connectionLost(self, reason):
+ pass
+
+ def neg_TEST_COMMAND(self, payload):
+ self.subcmd = payload
+
+ def enableLocal(self, option):
+ if option in self.localEnableable:
+ self.enabledLocal.append(option)
+ return True
+ return False
+
+ def disableLocal(self, option):
+ self.disabledLocal.append(option)
+
+ def enableRemote(self, option):
+ if option in self.remoteEnableable:
+ self.enabledRemote.append(option)
+ return True
+ return False
+
+ def disableRemote(self, option):
+ self.disabledRemote.append(option)
+
+
+
+class TestInterfaces(unittest.TestCase):
+ def test_interface(self):
+ """
+ L{telnet.TelnetProtocol} implements L{telnet.ITelnetProtocol}
+ """
+ p = telnet.TelnetProtocol()
+ verifyObject(telnet.ITelnetProtocol, p)
+
+
+
+class TelnetTransportTestCase(unittest.TestCase):
+ """
+ Tests for L{telnet.TelnetTransport}.
+ """
+ def setUp(self):
+ self.p = telnet.TelnetTransport(TestProtocol)
+ self.t = proto_helpers.StringTransport()
+ self.p.makeConnection(self.t)
+
+ def testRegularBytes(self):
+ # Just send a bunch of bytes. None of these do anything
+ # with telnet. They should pass right through to the
+ # application layer.
+ h = self.p.protocol
+
+ L = ["here are some bytes la la la",
+ "some more arrive here",
+ "lots of bytes to play with",
+ "la la la",
+ "ta de da",
+ "dum"]
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.bytes, ''.join(L))
+
+ def testNewlineHandling(self):
+ # Send various kinds of newlines and make sure they get translated
+ # into \n.
+ h = self.p.protocol
+
+ L = ["here is the first line\r\n",
+ "here is the second line\r\0",
+ "here is the third line\r\n",
+ "here is the last line\r\0"]
+
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.bytes, L[0][:-2] + '\n' +
+ L[1][:-2] + '\r' +
+ L[2][:-2] + '\n' +
+ L[3][:-2] + '\r')
+
+ def testIACEscape(self):
+ # Send a bunch of bytes and a couple quoted \xFFs. Unquoted,
+ # \xFF is a telnet command. Quoted, one of them from each pair
+ # should be passed through to the application layer.
+ h = self.p.protocol
+
+ L = ["here are some bytes\xff\xff with an embedded IAC",
+ "and here is a test of a border escape\xff",
+ "\xff did you get that IAC?"]
+
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.bytes, ''.join(L).replace('\xff\xff', '\xff'))
+
+ def _simpleCommandTest(self, cmdName):
+ # Send a single simple telnet command and make sure
+ # it gets noticed and the appropriate method gets
+ # called.
+ h = self.p.protocol
+
+ cmd = telnet.IAC + getattr(telnet, cmdName)
+ L = ["Here's some bytes, tra la la",
+ "But ono!" + cmd + " an interrupt"]
+
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.calls, [cmdName])
+ self.assertEqual(h.bytes, ''.join(L).replace(cmd, ''))
+
+ def testInterrupt(self):
+ self._simpleCommandTest("IP")
+
+ def testNoOperation(self):
+ self._simpleCommandTest("NOP")
+
+ def testDataMark(self):
+ self._simpleCommandTest("DM")
+
+ def testBreak(self):
+ self._simpleCommandTest("BRK")
+
+ def testAbortOutput(self):
+ self._simpleCommandTest("AO")
+
+ def testAreYouThere(self):
+ self._simpleCommandTest("AYT")
+
+ def testEraseCharacter(self):
+ self._simpleCommandTest("EC")
+
+ def testEraseLine(self):
+ self._simpleCommandTest("EL")
+
+ def testGoAhead(self):
+ self._simpleCommandTest("GA")
+
+ def testSubnegotiation(self):
+ # Send a subnegotiation command and make sure it gets
+ # parsed and that the correct method is called.
+ h = self.p.protocol
+
+ cmd = telnet.IAC + telnet.SB + '\x12hello world' + telnet.IAC + telnet.SE
+ L = ["These are some bytes but soon" + cmd,
+ "there will be some more"]
+
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.bytes, ''.join(L).replace(cmd, ''))
+ self.assertEqual(h.subcmd, list("hello world"))
+
+ def testSubnegotiationWithEmbeddedSE(self):
+ # Send a subnegotiation command with an embedded SE. Make sure
+ # that SE gets passed to the correct method.
+ h = self.p.protocol
+
+ cmd = (telnet.IAC + telnet.SB +
+ '\x12' + telnet.SE +
+ telnet.IAC + telnet.SE)
+
+ L = ["Some bytes are here" + cmd + "and here",
+ "and here"]
+
+ for b in L:
+ self.p.dataReceived(b)
+
+ self.assertEqual(h.bytes, ''.join(L).replace(cmd, ''))
+ self.assertEqual(h.subcmd, [telnet.SE])
+
+ def testBoundarySubnegotiation(self):
+ # Send a subnegotiation command. Split it at every possible byte boundary
+ # and make sure it always gets parsed and that it is passed to the correct
+ # method.
+ cmd = (telnet.IAC + telnet.SB +
+ '\x12' + telnet.SE + 'hello' +
+ telnet.IAC + telnet.SE)
+
+ for i in range(len(cmd)):
+ h = self.p.protocol = TestProtocol()
+ h.makeConnection(self.p)
+
+ a, b = cmd[:i], cmd[i:]
+ L = ["first part" + a,
+ b + "last part"]
+
+ for bytes in L:
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(h.bytes, ''.join(L).replace(cmd, ''))
+ self.assertEqual(h.subcmd, [telnet.SE] + list('hello'))
+
+ def _enabledHelper(self, o, eL=[], eR=[], dL=[], dR=[]):
+ self.assertEqual(o.enabledLocal, eL)
+ self.assertEqual(o.enabledRemote, eR)
+ self.assertEqual(o.disabledLocal, dL)
+ self.assertEqual(o.disabledRemote, dR)
+
+ def testRefuseWill(self):
+ # Try to enable an option. The server should refuse to enable it.
+ cmd = telnet.IAC + telnet.WILL + '\x12'
+
+ bytes = "surrounding bytes" + cmd + "to spice things up"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + '\x12')
+ self._enabledHelper(self.p.protocol)
+
+ def testRefuseDo(self):
+ # Try to enable an option. The server should refuse to enable it.
+ cmd = telnet.IAC + telnet.DO + '\x12'
+
+ bytes = "surrounding bytes" + cmd + "to spice things up"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.WONT + '\x12')
+ self._enabledHelper(self.p.protocol)
+
+ def testAcceptDo(self):
+ # Try to enable an option. The option is in our allowEnable
+ # list, so we will allow it to be enabled.
+ cmd = telnet.IAC + telnet.DO + '\x19'
+ bytes = 'padding' + cmd + 'trailer'
+
+ h = self.p.protocol
+ h.localEnableable = ('\x19',)
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.WILL + '\x19')
+ self._enabledHelper(h, eL=['\x19'])
+
+ def testAcceptWill(self):
+ # Same as testAcceptDo, but reversed.
+ cmd = telnet.IAC + telnet.WILL + '\x91'
+ bytes = 'header' + cmd + 'padding'
+
+ h = self.p.protocol
+ h.remoteEnableable = ('\x91',)
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + '\x91')
+ self._enabledHelper(h, eR=['\x91'])
+
+ def testAcceptWont(self):
+ # Try to disable an option. The server must allow any option to
+ # be disabled at any time. Make sure it disables it and sends
+ # back an acknowledgement of this.
+ cmd = telnet.IAC + telnet.WONT + '\x29'
+
+ # Jimmy it - after these two lines, the server will be in a state
+ # such that it believes the option to have been previously enabled
+ # via normal negotiation.
+ s = self.p.getOptionState('\x29')
+ s.him.state = 'yes'
+
+ bytes = "fiddle dee" + cmd
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + '\x29')
+ self.assertEqual(s.him.state, 'no')
+ self._enabledHelper(self.p.protocol, dR=['\x29'])
+
+ def testAcceptDont(self):
+ # Try to disable an option. The server must allow any option to
+ # be disabled at any time. Make sure it disables it and sends
+ # back an acknowledgement of this.
+ cmd = telnet.IAC + telnet.DONT + '\x29'
+
+ # Jimmy it - after these two lines, the server will be in a state
+ # such that it believes the option to have beenp previously enabled
+ # via normal negotiation.
+ s = self.p.getOptionState('\x29')
+ s.us.state = 'yes'
+
+ bytes = "fiddle dum " + cmd
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.WONT + '\x29')
+ self.assertEqual(s.us.state, 'no')
+ self._enabledHelper(self.p.protocol, dL=['\x29'])
+
+ def testIgnoreWont(self):
+ # Try to disable an option. The option is already disabled. The
+ # server should send nothing in response to this.
+ cmd = telnet.IAC + telnet.WONT + '\x47'
+
+ bytes = "dum de dum" + cmd + "tra la la"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), '')
+ self._enabledHelper(self.p.protocol)
+
+ def testIgnoreDont(self):
+ # Try to disable an option. The option is already disabled. The
+ # server should send nothing in response to this. Doing so could
+ # lead to a negotiation loop.
+ cmd = telnet.IAC + telnet.DONT + '\x47'
+
+ bytes = "dum de dum" + cmd + "tra la la"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), '')
+ self._enabledHelper(self.p.protocol)
+
+ def testIgnoreWill(self):
+ # Try to enable an option. The option is already enabled. The
+ # server should send nothing in response to this. Doing so could
+ # lead to a negotiation loop.
+ cmd = telnet.IAC + telnet.WILL + '\x56'
+
+ # Jimmy it - after these two lines, the server will be in a state
+ # such that it believes the option to have been previously enabled
+ # via normal negotiation.
+ s = self.p.getOptionState('\x56')
+ s.him.state = 'yes'
+
+ bytes = "tra la la" + cmd + "dum de dum"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), '')
+ self._enabledHelper(self.p.protocol)
+
+ def testIgnoreDo(self):
+ # Try to enable an option. The option is already enabled. The
+ # server should send nothing in response to this. Doing so could
+ # lead to a negotiation loop.
+ cmd = telnet.IAC + telnet.DO + '\x56'
+
+ # Jimmy it - after these two lines, the server will be in a state
+ # such that it believes the option to have been previously enabled
+ # via normal negotiation.
+ s = self.p.getOptionState('\x56')
+ s.us.state = 'yes'
+
+ bytes = "tra la la" + cmd + "dum de dum"
+ self.p.dataReceived(bytes)
+
+ self.assertEqual(self.p.protocol.bytes, bytes.replace(cmd, ''))
+ self.assertEqual(self.t.value(), '')
+ self._enabledHelper(self.p.protocol)
+
+ def testAcceptedEnableRequest(self):
+ # Try to enable an option through the user-level API. This
+ # returns a Deferred that fires when negotiation about the option
+ # finishes. Make sure it fires, make sure state gets updated
+ # properly, make sure the result indicates the option was enabled.
+ d = self.p.do('\x42')
+
+ h = self.p.protocol
+ h.remoteEnableable = ('\x42',)
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + '\x42')
+
+ self.p.dataReceived(telnet.IAC + telnet.WILL + '\x42')
+
+ d.addCallback(self.assertEqual, True)
+ d.addCallback(lambda _: self._enabledHelper(h, eR=['\x42']))
+ return d
+
+
+ def test_refusedEnableRequest(self):
+ """
+ If the peer refuses to enable an option we request it to enable, the
+ L{Deferred} returned by L{TelnetProtocol.do} fires with an
+ L{OptionRefused} L{Failure}.
+ """
+ # Try to enable an option through the user-level API. This returns a
+ # Deferred that fires when negotiation about the option finishes. Make
+ # sure it fires, make sure state gets updated properly, make sure the
+ # result indicates the option was enabled.
+ self.p.protocol.remoteEnableable = ('\x42',)
+ d = self.p.do('\x42')
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DO + '\x42')
+
+ s = self.p.getOptionState('\x42')
+ self.assertEqual(s.him.state, 'no')
+ self.assertEqual(s.us.state, 'no')
+ self.assertEqual(s.him.negotiating, True)
+ self.assertEqual(s.us.negotiating, False)
+
+ self.p.dataReceived(telnet.IAC + telnet.WONT + '\x42')
+
+ d = self.assertFailure(d, telnet.OptionRefused)
+ d.addCallback(lambda ignored: self._enabledHelper(self.p.protocol))
+ d.addCallback(
+ lambda ignored: self.assertEqual(s.him.negotiating, False))
+ return d
+
+
+ def test_refusedEnableOffer(self):
+ """
+ If the peer refuses to allow us to enable an option, the L{Deferred}
+ returned by L{TelnetProtocol.will} fires with an L{OptionRefused}
+ L{Failure}.
+ """
+ # Try to offer an option through the user-level API. This returns a
+ # Deferred that fires when negotiation about the option finishes. Make
+ # sure it fires, make sure state gets updated properly, make sure the
+ # result indicates the option was enabled.
+ self.p.protocol.localEnableable = ('\x42',)
+ d = self.p.will('\x42')
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.WILL + '\x42')
+
+ s = self.p.getOptionState('\x42')
+ self.assertEqual(s.him.state, 'no')
+ self.assertEqual(s.us.state, 'no')
+ self.assertEqual(s.him.negotiating, False)
+ self.assertEqual(s.us.negotiating, True)
+
+ self.p.dataReceived(telnet.IAC + telnet.DONT + '\x42')
+
+ d = self.assertFailure(d, telnet.OptionRefused)
+ d.addCallback(lambda ignored: self._enabledHelper(self.p.protocol))
+ d.addCallback(
+ lambda ignored: self.assertEqual(s.us.negotiating, False))
+ return d
+
+
+ def testAcceptedDisableRequest(self):
+ # Try to disable an option through the user-level API. This
+ # returns a Deferred that fires when negotiation about the option
+ # finishes. Make sure it fires, make sure state gets updated
+ # properly, make sure the result indicates the option was enabled.
+ s = self.p.getOptionState('\x42')
+ s.him.state = 'yes'
+
+ d = self.p.dont('\x42')
+
+ self.assertEqual(self.t.value(), telnet.IAC + telnet.DONT + '\x42')
+
+ self.p.dataReceived(telnet.IAC + telnet.WONT + '\x42')
+
+ d.addCallback(self.assertEqual, True)
+ d.addCallback(lambda _: self._enabledHelper(self.p.protocol,
+ dR=['\x42']))
+ return d
+
+ def testNegotiationBlocksFurtherNegotiation(self):
+ # Try to disable an option, then immediately try to enable it, then
+ # immediately try to disable it. Ensure that the 2nd and 3rd calls
+ # fail quickly with the right exception.
+ s = self.p.getOptionState('\x24')
+ s.him.state = 'yes'
+ d2 = self.p.dont('\x24') # fires after the first line of _final
+
+ def _do(x):
+ d = self.p.do('\x24')
+ return self.assertFailure(d, telnet.AlreadyNegotiating)
+
+ def _dont(x):
+ d = self.p.dont('\x24')
+ return self.assertFailure(d, telnet.AlreadyNegotiating)
+
+ def _final(x):
+ self.p.dataReceived(telnet.IAC + telnet.WONT + '\x24')
+ # an assertion that only passes if d2 has fired
+ self._enabledHelper(self.p.protocol, dR=['\x24'])
+ # Make sure we allow this
+ self.p.protocol.remoteEnableable = ('\x24',)
+ d = self.p.do('\x24')
+ self.p.dataReceived(telnet.IAC + telnet.WILL + '\x24')
+ d.addCallback(self.assertEqual, True)
+ d.addCallback(lambda _: self._enabledHelper(self.p.protocol,
+ eR=['\x24'],
+ dR=['\x24']))
+ return d
+
+ d = _do(None)
+ d.addCallback(_dont)
+ d.addCallback(_final)
+ return d
+
+ def testSuperfluousDisableRequestRaises(self):
+ # Try to disable a disabled option. Make sure it fails properly.
+ d = self.p.dont('\xab')
+ return self.assertFailure(d, telnet.AlreadyDisabled)
+
+ def testSuperfluousEnableRequestRaises(self):
+ # Try to disable a disabled option. Make sure it fails properly.
+ s = self.p.getOptionState('\xab')
+ s.him.state = 'yes'
+ d = self.p.do('\xab')
+ return self.assertFailure(d, telnet.AlreadyEnabled)
+
+ def testLostConnectionFailsDeferreds(self):
+ d1 = self.p.do('\x12')
+ d2 = self.p.do('\x23')
+ d3 = self.p.do('\x34')
+
+ class TestException(Exception):
+ pass
+
+ self.p.connectionLost(TestException("Total failure!"))
+
+ d1 = self.assertFailure(d1, TestException)
+ d2 = self.assertFailure(d2, TestException)
+ d3 = self.assertFailure(d3, TestException)
+ return defer.gatherResults([d1, d2, d3])
+
+
+class TestTelnet(telnet.Telnet):
+ """
+ A trivial extension of the telnet protocol class useful to unit tests.
+ """
+ def __init__(self):
+ telnet.Telnet.__init__(self)
+ self.events = []
+
+
+ def applicationDataReceived(self, bytes):
+ """
+ Record the given data in C{self.events}.
+ """
+ self.events.append(('bytes', bytes))
+
+
+ def unhandledCommand(self, command, bytes):
+ """
+ Record the given command in C{self.events}.
+ """
+ self.events.append(('command', command, bytes))
+
+
+ def unhandledSubnegotiation(self, command, bytes):
+ """
+ Record the given subnegotiation command in C{self.events}.
+ """
+ self.events.append(('negotiate', command, bytes))
+
+
+
+class TelnetTests(unittest.TestCase):
+ """
+ Tests for L{telnet.Telnet}.
+
+ L{telnet.Telnet} implements the TELNET protocol (RFC 854), including option
+ and suboption negotiation, and option state tracking.
+ """
+ def setUp(self):
+ """
+ Create an unconnected L{telnet.Telnet} to be used by tests.
+ """
+ self.protocol = TestTelnet()
+
+
+ def test_enableLocal(self):
+ """
+ L{telnet.Telnet.enableLocal} should reject all options, since
+ L{telnet.Telnet} does not know how to implement any options.
+ """
+ self.assertFalse(self.protocol.enableLocal('\0'))
+
+
+ def test_enableRemote(self):
+ """
+ L{telnet.Telnet.enableRemote} should reject all options, since
+ L{telnet.Telnet} does not know how to implement any options.
+ """
+ self.assertFalse(self.protocol.enableRemote('\0'))
+
+
+ def test_disableLocal(self):
+ """
+ It is an error for L{telnet.Telnet.disableLocal} to be called, since
+ L{telnet.Telnet.enableLocal} will never allow any options to be enabled
+ locally. If a subclass overrides enableLocal, it must also override
+ disableLocal.
+ """
+ self.assertRaises(NotImplementedError, self.protocol.disableLocal, '\0')
+
+
+ def test_disableRemote(self):
+ """
+ It is an error for L{telnet.Telnet.disableRemote} to be called, since
+ L{telnet.Telnet.enableRemote} will never allow any options to be
+ enabled remotely. If a subclass overrides enableRemote, it must also
+ override disableRemote.
+ """
+ self.assertRaises(NotImplementedError, self.protocol.disableRemote, '\0')
+
+
+ def test_requestNegotiation(self):
+ """
+ L{telnet.Telnet.requestNegotiation} formats the feature byte and the
+ payload bytes into the subnegotiation format and sends them.
+
+ See RFC 855.
+ """
+ transport = proto_helpers.StringTransport()
+ self.protocol.makeConnection(transport)
+ self.protocol.requestNegotiation('\x01', '\x02\x03')
+ self.assertEqual(
+ transport.value(),
+ # IAC SB feature bytes IAC SE
+ '\xff\xfa\x01\x02\x03\xff\xf0')
+
+
+ def test_requestNegotiationEscapesIAC(self):
+ """
+ If the payload for a subnegotiation includes I{IAC}, it is escaped by
+ L{telnet.Telnet.requestNegotiation} with another I{IAC}.
+
+ See RFC 855.
+ """
+ transport = proto_helpers.StringTransport()
+ self.protocol.makeConnection(transport)
+ self.protocol.requestNegotiation('\x01', '\xff')
+ self.assertEqual(
+ transport.value(),
+ '\xff\xfa\x01\xff\xff\xff\xf0')
+
+
+ def _deliver(self, bytes, *expected):
+ """
+ Pass the given bytes to the protocol's C{dataReceived} method and
+ assert that the given events occur.
+ """
+ received = self.protocol.events = []
+ self.protocol.dataReceived(bytes)
+ self.assertEqual(received, list(expected))
+
+
+ def test_oneApplicationDataByte(self):
+ """
+ One application-data byte in the default state gets delivered right
+ away.
+ """
+ self._deliver('a', ('bytes', 'a'))
+
+
+ def test_twoApplicationDataBytes(self):
+ """
+ Two application-data bytes in the default state get delivered
+ together.
+ """
+ self._deliver('bc', ('bytes', 'bc'))
+
+
+ def test_threeApplicationDataBytes(self):
+ """
+ Three application-data bytes followed by a control byte get
+ delivered, but the control byte doesn't.
+ """
+ self._deliver('def' + telnet.IAC, ('bytes', 'def'))
+
+
+ def test_escapedControl(self):
+ """
+ IAC in the escaped state gets delivered and so does another
+ application-data byte following it.
+ """
+ self._deliver(telnet.IAC)
+ self._deliver(telnet.IAC + 'g', ('bytes', telnet.IAC + 'g'))
+
+
+ def test_carriageReturn(self):
+ """
+ A carriage return only puts the protocol into the newline state. A
+ linefeed in the newline state causes just the newline to be
+ delivered. A nul in the newline state causes a carriage return to
+ be delivered. An IAC in the newline state causes a carriage return
+ to be delivered and puts the protocol into the escaped state.
+ Anything else causes a carriage return and that thing to be
+ delivered.
+ """
+ self._deliver('\r')
+ self._deliver('\n', ('bytes', '\n'))
+ self._deliver('\r\n', ('bytes', '\n'))
+
+ self._deliver('\r')
+ self._deliver('\0', ('bytes', '\r'))
+ self._deliver('\r\0', ('bytes', '\r'))
+
+ self._deliver('\r')
+ self._deliver('a', ('bytes', '\ra'))
+ self._deliver('\ra', ('bytes', '\ra'))
+
+ self._deliver('\r')
+ self._deliver(
+ telnet.IAC + telnet.IAC + 'x', ('bytes', '\r' + telnet.IAC + 'x'))
+
+
+ def test_applicationDataBeforeSimpleCommand(self):
+ """
+ Application bytes received before a command are delivered before the
+ command is processed.
+ """
+ self._deliver(
+ 'x' + telnet.IAC + telnet.NOP,
+ ('bytes', 'x'), ('command', telnet.NOP, None))
+
+
+ def test_applicationDataBeforeCommand(self):
+ """
+ Application bytes received before a WILL/WONT/DO/DONT are delivered
+ before the command is processed.
+ """
+ self.protocol.commandMap = {}
+ self._deliver(
+ 'y' + telnet.IAC + telnet.WILL + '\x00',
+ ('bytes', 'y'), ('command', telnet.WILL, '\x00'))
+
+
+ def test_applicationDataBeforeSubnegotiation(self):
+ """
+ Application bytes received before a subnegotiation command are
+ delivered before the negotiation is processed.
+ """
+ self._deliver(
+ 'z' + telnet.IAC + telnet.SB + 'Qx' + telnet.IAC + telnet.SE,
+ ('bytes', 'z'), ('negotiate', 'Q', ['x']))
diff --git a/twisted/conch/test/test_text.py b/twisted/conch/test/test_text.py
new file mode 100644
index 0000000..1d68870
--- /dev/null
+++ b/twisted/conch/test/test_text.py
@@ -0,0 +1,101 @@
+# -*- test-case-name: twisted.conch.test.test_text -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+
+from twisted.conch.insults import helper, text
+
+A = text.attributes
+
+class Serialization(unittest.TestCase):
+ def setUp(self):
+ self.attrs = helper.CharacterAttribute()
+
+ def testTrivial(self):
+ self.assertEqual(
+ text.flatten(A.normal['Hello, world.'], self.attrs),
+ 'Hello, world.')
+
+ def testBold(self):
+ self.assertEqual(
+ text.flatten(A.bold['Hello, world.'], self.attrs),
+ '\x1b[1mHello, world.')
+
+ def testUnderline(self):
+ self.assertEqual(
+ text.flatten(A.underline['Hello, world.'], self.attrs),
+ '\x1b[4mHello, world.')
+
+ def testBlink(self):
+ self.assertEqual(
+ text.flatten(A.blink['Hello, world.'], self.attrs),
+ '\x1b[5mHello, world.')
+
+ def testReverseVideo(self):
+ self.assertEqual(
+ text.flatten(A.reverseVideo['Hello, world.'], self.attrs),
+ '\x1b[7mHello, world.')
+
+ def testMinus(self):
+ self.assertEqual(
+ text.flatten(
+ A.bold[A.blink['Hello', -A.bold[' world'], '.']],
+ self.attrs),
+ '\x1b[1;5mHello\x1b[0;5m world\x1b[1;5m.')
+
+ def testForeground(self):
+ self.assertEqual(
+ text.flatten(
+ A.normal[A.fg.red['Hello, '], A.fg.green['world!']],
+ self.attrs),
+ '\x1b[31mHello, \x1b[32mworld!')
+
+ def testBackground(self):
+ self.assertEqual(
+ text.flatten(
+ A.normal[A.bg.red['Hello, '], A.bg.green['world!']],
+ self.attrs),
+ '\x1b[41mHello, \x1b[42mworld!')
+
+
+class EfficiencyTestCase(unittest.TestCase):
+ todo = ("flatten() isn't quite stateful enough to avoid emitting a few extra bytes in "
+ "certain circumstances, so these tests fail. The failures take the form of "
+ "additional elements in the ;-delimited character attribute lists. For example, "
+ "\\x1b[0;31;46m might be emitted instead of \\x[46m, even if 31 has already been "
+ "activated and no conflicting attributes are set which need to be cleared.")
+
+ def setUp(self):
+ self.attrs = helper.CharacterAttribute()
+
+ def testComplexStructure(self):
+ output = A.normal[
+ A.bold[
+ A.bg.cyan[
+ A.fg.red[
+ "Foreground Red, Background Cyan, Bold",
+ A.blink[
+ "Blinking"],
+ -A.bold[
+ "Foreground Red, Background Cyan, normal"]],
+ A.fg.green[
+ "Foreground Green, Background Cyan, Bold"]]]]
+
+ self.assertEqual(
+ text.flatten(output, self.attrs),
+ "\x1b[1;31;46mForeground Red, Background Cyan, Bold"
+ "\x1b[5mBlinking"
+ "\x1b[0;31;46mForeground Red, Background Cyan, normal"
+ "\x1b[1;32;46mForeground Green, Background Cyan, Bold")
+
+ def testNesting(self):
+ self.assertEqual(
+ text.flatten(A.bold['Hello, ', A.underline['world.']], self.attrs),
+ '\x1b[1mHello, \x1b[4mworld.')
+
+ self.assertEqual(
+ text.flatten(
+ A.bold[A.reverseVideo['Hello, ', A.normal['world'], '.']],
+ self.attrs),
+ '\x1b[1;7mHello, \x1b[0mworld\x1b[1;7m.')
diff --git a/twisted/conch/test/test_transport.py b/twisted/conch/test/test_transport.py
new file mode 100644
index 0000000..10546cb
--- /dev/null
+++ b/twisted/conch/test/test_transport.py
@@ -0,0 +1,2196 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for ssh/transport.py and the classes therein.
+"""
+
+try:
+ import pyasn1
+except ImportError:
+ pyasn1 = None
+
+try:
+ import Crypto.Cipher.DES3
+except ImportError:
+ Crypto = None
+
+if pyasn1 is not None and Crypto is not None:
+ dependencySkip = None
+ from twisted.conch.ssh import transport, keys, factory
+ from twisted.conch.test import keydata
+else:
+ if pyasn1 is None:
+ dependencySkip = "Cannot run without PyASN1"
+ elif Crypto is None:
+ dependencySkip = "can't run w/o PyCrypto"
+
+ class transport: # fictional modules to make classes work
+ class SSHTransportBase: pass
+ class SSHServerTransport: pass
+ class SSHClientTransport: pass
+ class factory:
+ class SSHFactory:
+ pass
+
+from twisted.trial import unittest
+from twisted.internet import defer
+from twisted.protocols import loopback
+from twisted.python import randbytes
+from twisted.python.reflect import qual
+from twisted.python.hashlib import md5, sha1
+from twisted.conch.ssh import service, common
+from twisted.test import proto_helpers
+
+from twisted.conch.error import ConchError
+
+
+
+class MockTransportBase(transport.SSHTransportBase):
+ """
+ A base class for the client and server protocols. Stores the messages
+ it receieves instead of ignoring them.
+
+ @ivar errors: a list of tuples: (reasonCode, description)
+ @ivar unimplementeds: a list of integers: sequence number
+ @ivar debugs: a list of tuples: (alwaysDisplay, message, lang)
+ @ivar ignoreds: a list of strings: ignored data
+ """
+
+ def connectionMade(self):
+ """
+ Set up instance variables.
+ """
+ transport.SSHTransportBase.connectionMade(self)
+ self.errors = []
+ self.unimplementeds = []
+ self.debugs = []
+ self.ignoreds = []
+ self.gotUnsupportedVersion = None
+
+
+ def _unsupportedVersionReceived(self, remoteVersion):
+ """
+ Intercept unsupported version call.
+
+ @type remoteVersion: C{str}
+ """
+ self.gotUnsupportedVersion = remoteVersion
+ return transport.SSHTransportBase._unsupportedVersionReceived(self, remoteVersion)
+
+
+ def receiveError(self, reasonCode, description):
+ """
+ Store any errors received.
+
+ @type reasonCode: C{int}
+ @type description: C{str}
+ """
+ self.errors.append((reasonCode, description))
+
+
+ def receiveUnimplemented(self, seqnum):
+ """
+ Store any unimplemented packet messages.
+
+ @type seqnum: C{int}
+ """
+ self.unimplementeds.append(seqnum)
+
+
+ def receiveDebug(self, alwaysDisplay, message, lang):
+ """
+ Store any debug messages.
+
+ @type alwaysDisplay: C{bool}
+ @type message: C{str}
+ @type lang: C{str}
+ """
+ self.debugs.append((alwaysDisplay, message, lang))
+
+
+ def ssh_IGNORE(self, packet):
+ """
+ Store any ignored data.
+
+ @type packet: C{str}
+ """
+ self.ignoreds.append(packet)
+
+
+class MockCipher(object):
+ """
+ A mocked-up version of twisted.conch.ssh.transport.SSHCiphers.
+ """
+ outCipType = 'test'
+ encBlockSize = 6
+ inCipType = 'test'
+ decBlockSize = 6
+ inMACType = 'test'
+ outMACType = 'test'
+ verifyDigestSize = 1
+ usedEncrypt = False
+ usedDecrypt = False
+ outMAC = (None, '', '', 1)
+ inMAC = (None, '', '', 1)
+ keys = ()
+
+
+ def encrypt(self, x):
+ """
+ Called to encrypt the packet. Simply record that encryption was used
+ and return the data unchanged.
+ """
+ self.usedEncrypt = True
+ if (len(x) % self.encBlockSize) != 0:
+ raise RuntimeError("length %i modulo blocksize %i is not 0: %i" %
+ (len(x), self.encBlockSize, len(x) % self.encBlockSize))
+ return x
+
+
+ def decrypt(self, x):
+ """
+ Called to decrypt the packet. Simply record that decryption was used
+ and return the data unchanged.
+ """
+ self.usedDecrypt = True
+ if (len(x) % self.encBlockSize) != 0:
+ raise RuntimeError("length %i modulo blocksize %i is not 0: %i" %
+ (len(x), self.decBlockSize, len(x) % self.decBlockSize))
+ return x
+
+
+ def makeMAC(self, outgoingPacketSequence, payload):
+ """
+ Make a Message Authentication Code by sending the character value of
+ the outgoing packet.
+ """
+ return chr(outgoingPacketSequence)
+
+
+ def verify(self, incomingPacketSequence, packet, macData):
+ """
+ Verify the Message Authentication Code by checking that the packet
+ sequence number is the same.
+ """
+ return chr(incomingPacketSequence) == macData
+
+
+ def setKeys(self, ivOut, keyOut, ivIn, keyIn, macIn, macOut):
+ """
+ Record the keys.
+ """
+ self.keys = (ivOut, keyOut, ivIn, keyIn, macIn, macOut)
+
+
+
+class MockCompression:
+ """
+ A mocked-up compression, based on the zlib interface. Instead of
+ compressing, it reverses the data and adds a 0x66 byte to the end.
+ """
+
+
+ def compress(self, payload):
+ return payload[::-1] # reversed
+
+
+ def decompress(self, payload):
+ return payload[:-1][::-1]
+
+
+ def flush(self, kind):
+ return '\x66'
+
+
+
+class MockService(service.SSHService):
+ """
+ A mocked-up service, based on twisted.conch.ssh.service.SSHService.
+
+ @ivar started: True if this service has been started.
+ @ivar stopped: True if this service has been stopped.
+ """
+ name = "MockService"
+ started = False
+ stopped = False
+ protocolMessages = {0xff: "MSG_TEST", 71: "MSG_fiction"}
+
+
+ def logPrefix(self):
+ return "MockService"
+
+
+ def serviceStarted(self):
+ """
+ Record that the service was started.
+ """
+ self.started = True
+
+
+ def serviceStopped(self):
+ """
+ Record that the service was stopped.
+ """
+ self.stopped = True
+
+
+ def ssh_TEST(self, packet):
+ """
+ A message that this service responds to.
+ """
+ self.transport.sendPacket(0xff, packet)
+
+
+class MockFactory(factory.SSHFactory):
+ """
+ A mocked-up factory based on twisted.conch.ssh.factory.SSHFactory.
+ """
+ services = {
+ 'ssh-userauth': MockService}
+
+
+ def getPublicKeys(self):
+ """
+ Return the public keys that authenticate this server.
+ """
+ return {
+ 'ssh-rsa': keys.Key.fromString(keydata.publicRSA_openssh),
+ 'ssh-dsa': keys.Key.fromString(keydata.publicDSA_openssh)}
+
+
+ def getPrivateKeys(self):
+ """
+ Return the private keys that authenticate this server.
+ """
+ return {
+ 'ssh-rsa': keys.Key.fromString(keydata.privateRSA_openssh),
+ 'ssh-dsa': keys.Key.fromString(keydata.privateDSA_openssh)}
+
+
+ def getPrimes(self):
+ """
+ Return the Diffie-Hellman primes that can be used for the
+ diffie-hellman-group-exchange-sha1 key exchange.
+ """
+ return {
+ 1024: ((2, transport.DH_PRIME),),
+ 2048: ((3, transport.DH_PRIME),),
+ 4096: ((5, 7),)}
+
+
+
+class MockOldFactoryPublicKeys(MockFactory):
+ """
+ The old SSHFactory returned mappings from key names to strings from
+ getPublicKeys(). We return those here for testing.
+ """
+
+
+ def getPublicKeys(self):
+ """
+ We used to map key types to public key blobs as strings.
+ """
+ keys = MockFactory.getPublicKeys(self)
+ for name, key in keys.items()[:]:
+ keys[name] = key.blob()
+ return keys
+
+
+
+class MockOldFactoryPrivateKeys(MockFactory):
+ """
+ The old SSHFactory returned mappings from key names to PyCrypto key
+ objects from getPrivateKeys(). We return those here for testing.
+ """
+
+
+ def getPrivateKeys(self):
+ """
+ We used to map key types to PyCrypto key objects.
+ """
+ keys = MockFactory.getPrivateKeys(self)
+ for name, key in keys.items()[:]:
+ keys[name] = key.keyObject
+ return keys
+
+
+
+class TransportTestCase(unittest.TestCase):
+ """
+ Base class for transport test cases.
+ """
+ klass = None
+
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+
+ def setUp(self):
+ self.transport = proto_helpers.StringTransport()
+ self.proto = self.klass()
+ self.packets = []
+ def secureRandom(len):
+ """
+ Return a consistent entropy value
+ """
+ return '\x99' * len
+ self.oldSecureRandom = randbytes.secureRandom
+ randbytes.secureRandom = secureRandom
+ def stubSendPacket(messageType, payload):
+ self.packets.append((messageType, payload))
+ self.proto.makeConnection(self.transport)
+ # we just let the kex packet go into the transport
+ self.proto.sendPacket = stubSendPacket
+
+
+ def finishKeyExchange(self, proto):
+ """
+ Deliver enough additional messages to C{proto} so that the key exchange
+ which is started in L{SSHTransportBase.connectionMade} completes and
+ non-key exchange messages can be sent and received.
+ """
+ proto.dataReceived("SSH-2.0-BogoClient-1.2i\r\n")
+ proto.dispatchMessage(
+ transport.MSG_KEXINIT, self._A_KEXINIT_MESSAGE)
+ proto._keySetup("foo", "bar")
+ # SSHTransportBase can't handle MSG_NEWKEYS, or it would be the right
+ # thing to deliver next. _newKeys won't work either, because
+ # sendKexInit (probably) hasn't been called. sendKexInit is responsible
+ # for setting up certain state _newKeys relies on. So, just change the
+ # key exchange state to what it would be when key exchange is finished.
+ proto._keyExchangeState = proto._KEY_EXCHANGE_NONE
+
+
+ def tearDown(self):
+ randbytes.secureRandom = self.oldSecureRandom
+ self.oldSecureRandom = None
+
+
+ def simulateKeyExchange(self, sharedSecret, exchangeHash):
+ """
+ Finish a key exchange by calling C{_keySetup} with the given arguments.
+ Also do extra whitebox stuff to satisfy that method's assumption that
+ some kind of key exchange has actually taken place.
+ """
+ self.proto._keyExchangeState = self.proto._KEY_EXCHANGE_REQUESTED
+ self.proto._blockedByKeyExchange = []
+ self.proto._keySetup(sharedSecret, exchangeHash)
+
+
+
+class BaseSSHTransportTestCase(TransportTestCase):
+ """
+ Test TransportBase. It implements the non-server/client specific
+ parts of the SSH transport protocol.
+ """
+
+ klass = MockTransportBase
+
+ _A_KEXINIT_MESSAGE = (
+ "\xAA" * 16 +
+ common.NS('diffie-hellman-group1-sha1') +
+ common.NS('ssh-rsa') +
+ common.NS('aes256-ctr') +
+ common.NS('aes256-ctr') +
+ common.NS('hmac-sha1') +
+ common.NS('hmac-sha1') +
+ common.NS('none') +
+ common.NS('none') +
+ common.NS('') +
+ common.NS('') +
+ '\x00' + '\x00\x00\x00\x00')
+
+ def test_sendVersion(self):
+ """
+ Test that the first thing sent over the connection is the version
+ string.
+ """
+ # the other setup was done in the setup method
+ self.assertEqual(self.transport.value().split('\r\n', 1)[0],
+ "SSH-2.0-Twisted")
+
+
+ def test_sendPacketPlain(self):
+ """
+ Test that plain (unencrypted, uncompressed) packets are sent
+ correctly. The format is::
+ uint32 length (including type and padding length)
+ byte padding length
+ byte type
+ bytes[length-padding length-2] data
+ bytes[padding length] padding
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ self.transport.clear()
+ message = ord('A')
+ payload = 'BCDEFG'
+ proto.sendPacket(message, payload)
+ value = self.transport.value()
+ self.assertEqual(value, '\x00\x00\x00\x0c\x04ABCDEFG\x99\x99\x99\x99')
+
+
+ def test_sendPacketEncrypted(self):
+ """
+ Test that packets sent while encryption is enabled are sent
+ correctly. The whole packet should be encrypted.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ proto.currentEncryptions = testCipher = MockCipher()
+ message = ord('A')
+ payload = 'BC'
+ self.transport.clear()
+ proto.sendPacket(message, payload)
+ self.assertTrue(testCipher.usedEncrypt)
+ value = self.transport.value()
+ self.assertEqual(
+ value,
+ # Four byte length prefix
+ '\x00\x00\x00\x08'
+ # One byte padding length
+ '\x04'
+ # The actual application data
+ 'ABC'
+ # "Random" padding - see the secureRandom monkeypatch in setUp
+ '\x99\x99\x99\x99'
+ # The MAC
+ '\x02')
+
+
+ def test_sendPacketCompressed(self):
+ """
+ Test that packets sent while compression is enabled are sent
+ correctly. The packet type and data should be encrypted.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ proto.outgoingCompression = MockCompression()
+ self.transport.clear()
+ proto.sendPacket(ord('A'), 'B')
+ value = self.transport.value()
+ self.assertEqual(
+ value,
+ '\x00\x00\x00\x0c\x08BA\x66\x99\x99\x99\x99\x99\x99\x99\x99')
+
+
+ def test_sendPacketBoth(self):
+ """
+ Test that packets sent while compression and encryption are
+ enabled are sent correctly. The packet type and data should be
+ compressed and then the whole packet should be encrypted.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ proto.currentEncryptions = testCipher = MockCipher()
+ proto.outgoingCompression = MockCompression()
+ message = ord('A')
+ payload = 'BC'
+ self.transport.clear()
+ proto.sendPacket(message, payload)
+ self.assertTrue(testCipher.usedEncrypt)
+ value = self.transport.value()
+ self.assertEqual(
+ value,
+ # Four byte length prefix
+ '\x00\x00\x00\x0e'
+ # One byte padding length
+ '\x09'
+ # Compressed application data
+ 'CBA\x66'
+ # "Random" padding - see the secureRandom monkeypatch in setUp
+ '\x99\x99\x99\x99\x99\x99\x99\x99\x99'
+ # The MAC
+ '\x02')
+
+
+ def test_getPacketPlain(self):
+ """
+ Test that packets are retrieved correctly out of the buffer when
+ no encryption is enabled.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ self.transport.clear()
+ proto.sendPacket(ord('A'), 'BC')
+ proto.buf = self.transport.value() + 'extra'
+ self.assertEqual(proto.getPacket(), 'ABC')
+ self.assertEqual(proto.buf, 'extra')
+
+
+ def test_getPacketEncrypted(self):
+ """
+ Test that encrypted packets are retrieved correctly.
+ See test_sendPacketEncrypted.
+ """
+ proto = MockTransportBase()
+ proto.sendKexInit = lambda: None # don't send packets
+ proto.makeConnection(self.transport)
+ self.transport.clear()
+ proto.currentEncryptions = testCipher = MockCipher()
+ proto.sendPacket(ord('A'), 'BCD')
+ value = self.transport.value()
+ proto.buf = value[:MockCipher.decBlockSize]
+ self.assertEqual(proto.getPacket(), None)
+ self.assertTrue(testCipher.usedDecrypt)
+ self.assertEqual(proto.first, '\x00\x00\x00\x0e\x09A')
+ proto.buf += value[MockCipher.decBlockSize:]
+ self.assertEqual(proto.getPacket(), 'ABCD')
+ self.assertEqual(proto.buf, '')
+
+
+ def test_getPacketCompressed(self):
+ """
+ Test that compressed packets are retrieved correctly. See
+ test_sendPacketCompressed.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(self.transport)
+ self.finishKeyExchange(proto)
+ self.transport.clear()
+ proto.outgoingCompression = MockCompression()
+ proto.incomingCompression = proto.outgoingCompression
+ proto.sendPacket(ord('A'), 'BCD')
+ proto.buf = self.transport.value()
+ self.assertEqual(proto.getPacket(), 'ABCD')
+
+
+ def test_getPacketBoth(self):
+ """
+ Test that compressed and encrypted packets are retrieved correctly.
+ See test_sendPacketBoth.
+ """
+ proto = MockTransportBase()
+ proto.sendKexInit = lambda: None
+ proto.makeConnection(self.transport)
+ self.transport.clear()
+ proto.currentEncryptions = testCipher = MockCipher()
+ proto.outgoingCompression = MockCompression()
+ proto.incomingCompression = proto.outgoingCompression
+ proto.sendPacket(ord('A'), 'BCDEFG')
+ proto.buf = self.transport.value()
+ self.assertEqual(proto.getPacket(), 'ABCDEFG')
+
+
+ def test_ciphersAreValid(self):
+ """
+ Test that all the supportedCiphers are valid.
+ """
+ ciphers = transport.SSHCiphers('A', 'B', 'C', 'D')
+ iv = key = '\x00' * 16
+ for cipName in self.proto.supportedCiphers:
+ self.assertTrue(ciphers._getCipher(cipName, iv, key))
+
+
+ def test_sendKexInit(self):
+ """
+ Test that the KEXINIT (key exchange initiation) message is sent
+ correctly. Payload::
+ bytes[16] cookie
+ string key exchange algorithms
+ string public key algorithms
+ string outgoing ciphers
+ string incoming ciphers
+ string outgoing MACs
+ string incoming MACs
+ string outgoing compressions
+ string incoming compressions
+ bool first packet follows
+ uint32 0
+ """
+ value = self.transport.value().split('\r\n', 1)[1]
+ self.proto.buf = value
+ packet = self.proto.getPacket()
+ self.assertEqual(packet[0], chr(transport.MSG_KEXINIT))
+ self.assertEqual(packet[1:17], '\x99' * 16)
+ (kex, pubkeys, ciphers1, ciphers2, macs1, macs2, compressions1,
+ compressions2, languages1, languages2,
+ buf) = common.getNS(packet[17:], 10)
+
+ self.assertEqual(kex, ','.join(self.proto.supportedKeyExchanges))
+ self.assertEqual(pubkeys, ','.join(self.proto.supportedPublicKeys))
+ self.assertEqual(ciphers1, ','.join(self.proto.supportedCiphers))
+ self.assertEqual(ciphers2, ','.join(self.proto.supportedCiphers))
+ self.assertEqual(macs1, ','.join(self.proto.supportedMACs))
+ self.assertEqual(macs2, ','.join(self.proto.supportedMACs))
+ self.assertEqual(compressions1,
+ ','.join(self.proto.supportedCompressions))
+ self.assertEqual(compressions2,
+ ','.join(self.proto.supportedCompressions))
+ self.assertEqual(languages1, ','.join(self.proto.supportedLanguages))
+ self.assertEqual(languages2, ','.join(self.proto.supportedLanguages))
+ self.assertEqual(buf, '\x00' * 5)
+
+
+ def test_receiveKEXINITReply(self):
+ """
+ Immediately after connecting, the transport expects a KEXINIT message
+ and does not reply to it.
+ """
+ self.transport.clear()
+ self.proto.dispatchMessage(
+ transport.MSG_KEXINIT, self._A_KEXINIT_MESSAGE)
+ self.assertEqual(self.packets, [])
+
+
+ def test_sendKEXINITReply(self):
+ """
+ When a KEXINIT message is received which is not a reply to an earlier
+ KEXINIT message which was sent, a KEXINIT reply is sent.
+ """
+ self.finishKeyExchange(self.proto)
+ del self.packets[:]
+
+ self.proto.dispatchMessage(
+ transport.MSG_KEXINIT, self._A_KEXINIT_MESSAGE)
+ self.assertEqual(len(self.packets), 1)
+ self.assertEqual(self.packets[0][0], transport.MSG_KEXINIT)
+
+
+ def test_sendKexInitTwiceFails(self):
+ """
+ A new key exchange cannot be started while a key exchange is already in
+ progress. If an attempt is made to send a I{KEXINIT} message using
+ L{SSHTransportBase.sendKexInit} while a key exchange is in progress
+ causes that method to raise a L{RuntimeError}.
+ """
+ self.assertRaises(RuntimeError, self.proto.sendKexInit)
+
+
+ def test_sendKexInitBlocksOthers(self):
+ """
+ After L{SSHTransportBase.sendKexInit} has been called, messages types
+ other than the following are queued and not sent until after I{NEWKEYS}
+ is sent by L{SSHTransportBase._keySetup}.
+
+ RFC 4253, section 7.1.
+ """
+ # sendKexInit is called by connectionMade, which is called in setUp. So
+ # we're in the state already.
+ disallowedMessageTypes = [
+ transport.MSG_SERVICE_REQUEST,
+ transport.MSG_KEXINIT,
+ ]
+
+ # Drop all the bytes sent by setUp, they're not relevant to this test.
+ self.transport.clear()
+
+ # Get rid of the sendPacket monkey patch, we are testing the behavior of
+ # sendPacket.
+ del self.proto.sendPacket
+
+ for messageType in disallowedMessageTypes:
+ self.proto.sendPacket(messageType, 'foo')
+ self.assertEqual(self.transport.value(), "")
+
+ self.finishKeyExchange(self.proto)
+ # Make the bytes written to the transport cleartext so it's easier to
+ # make an assertion about them.
+ self.proto.nextEncryptions = MockCipher()
+
+ # Pseudo-deliver the peer's NEWKEYS message, which should flush the
+ # messages which were queued above.
+ self.proto._newKeys()
+ self.assertEqual(self.transport.value().count("foo"), 2)
+
+
+ def test_sendDebug(self):
+ """
+ Test that debug messages are sent correctly. Payload::
+ bool always display
+ string debug message
+ string language
+ """
+ self.proto.sendDebug("test", True, 'en')
+ self.assertEqual(
+ self.packets,
+ [(transport.MSG_DEBUG,
+ "\x01\x00\x00\x00\x04test\x00\x00\x00\x02en")])
+
+
+ def test_receiveDebug(self):
+ """
+ Test that debug messages are received correctly. See test_sendDebug.
+ """
+ self.proto.dispatchMessage(
+ transport.MSG_DEBUG,
+ '\x01\x00\x00\x00\x04test\x00\x00\x00\x02en')
+ self.assertEqual(self.proto.debugs, [(True, 'test', 'en')])
+
+
+ def test_sendIgnore(self):
+ """
+ Test that ignored messages are sent correctly. Payload::
+ string ignored data
+ """
+ self.proto.sendIgnore("test")
+ self.assertEqual(
+ self.packets, [(transport.MSG_IGNORE,
+ '\x00\x00\x00\x04test')])
+
+
+ def test_receiveIgnore(self):
+ """
+ Test that ignored messages are received correctly. See
+ test_sendIgnore.
+ """
+ self.proto.dispatchMessage(transport.MSG_IGNORE, 'test')
+ self.assertEqual(self.proto.ignoreds, ['test'])
+
+
+ def test_sendUnimplemented(self):
+ """
+ Test that unimplemented messages are sent correctly. Payload::
+ uint32 sequence number
+ """
+ self.proto.sendUnimplemented()
+ self.assertEqual(
+ self.packets, [(transport.MSG_UNIMPLEMENTED,
+ '\x00\x00\x00\x00')])
+
+
+ def test_receiveUnimplemented(self):
+ """
+ Test that unimplemented messages are received correctly. See
+ test_sendUnimplemented.
+ """
+ self.proto.dispatchMessage(transport.MSG_UNIMPLEMENTED,
+ '\x00\x00\x00\xff')
+ self.assertEqual(self.proto.unimplementeds, [255])
+
+
+ def test_sendDisconnect(self):
+ """
+ Test that disconnection messages are sent correctly. Payload::
+ uint32 reason code
+ string reason description
+ string language
+ """
+ disconnected = [False]
+ def stubLoseConnection():
+ disconnected[0] = True
+ self.transport.loseConnection = stubLoseConnection
+ self.proto.sendDisconnect(0xff, "test")
+ self.assertEqual(
+ self.packets,
+ [(transport.MSG_DISCONNECT,
+ "\x00\x00\x00\xff\x00\x00\x00\x04test\x00\x00\x00\x00")])
+ self.assertTrue(disconnected[0])
+
+
+ def test_receiveDisconnect(self):
+ """
+ Test that disconnection messages are received correctly. See
+ test_sendDisconnect.
+ """
+ disconnected = [False]
+ def stubLoseConnection():
+ disconnected[0] = True
+ self.transport.loseConnection = stubLoseConnection
+ self.proto.dispatchMessage(transport.MSG_DISCONNECT,
+ '\x00\x00\x00\xff\x00\x00\x00\x04test')
+ self.assertEqual(self.proto.errors, [(255, 'test')])
+ self.assertTrue(disconnected[0])
+
+
+ def test_dataReceived(self):
+ """
+ Test that dataReceived parses packets and dispatches them to
+ ssh_* methods.
+ """
+ kexInit = [False]
+ def stubKEXINIT(packet):
+ kexInit[0] = True
+ self.proto.ssh_KEXINIT = stubKEXINIT
+ self.proto.dataReceived(self.transport.value())
+ self.assertTrue(self.proto.gotVersion)
+ self.assertEqual(self.proto.ourVersionString,
+ self.proto.otherVersionString)
+ self.assertTrue(kexInit[0])
+
+
+ def test_service(self):
+ """
+ Test that the transport can set the running service and dispatches
+ packets to the service's packetReceived method.
+ """
+ service = MockService()
+ self.proto.setService(service)
+ self.assertEqual(self.proto.service, service)
+ self.assertTrue(service.started)
+ self.proto.dispatchMessage(0xff, "test")
+ self.assertEqual(self.packets, [(0xff, "test")])
+
+ service2 = MockService()
+ self.proto.setService(service2)
+ self.assertTrue(service2.started)
+ self.assertTrue(service.stopped)
+
+ self.proto.connectionLost(None)
+ self.assertTrue(service2.stopped)
+
+
+ def test_avatar(self):
+ """
+ Test that the transport notifies the avatar of disconnections.
+ """
+ disconnected = [False]
+ def logout():
+ disconnected[0] = True
+ self.proto.logoutFunction = logout
+ self.proto.avatar = True
+
+ self.proto.connectionLost(None)
+ self.assertTrue(disconnected[0])
+
+
+ def test_isEncrypted(self):
+ """
+ Test that the transport accurately reflects its encrypted status.
+ """
+ self.assertFalse(self.proto.isEncrypted('in'))
+ self.assertFalse(self.proto.isEncrypted('out'))
+ self.assertFalse(self.proto.isEncrypted('both'))
+ self.proto.currentEncryptions = MockCipher()
+ self.assertTrue(self.proto.isEncrypted('in'))
+ self.assertTrue(self.proto.isEncrypted('out'))
+ self.assertTrue(self.proto.isEncrypted('both'))
+ self.proto.currentEncryptions = transport.SSHCiphers('none', 'none',
+ 'none', 'none')
+ self.assertFalse(self.proto.isEncrypted('in'))
+ self.assertFalse(self.proto.isEncrypted('out'))
+ self.assertFalse(self.proto.isEncrypted('both'))
+
+ self.assertRaises(TypeError, self.proto.isEncrypted, 'bad')
+
+
+ def test_isVerified(self):
+ """
+ Test that the transport accurately reflects its verified status.
+ """
+ self.assertFalse(self.proto.isVerified('in'))
+ self.assertFalse(self.proto.isVerified('out'))
+ self.assertFalse(self.proto.isVerified('both'))
+ self.proto.currentEncryptions = MockCipher()
+ self.assertTrue(self.proto.isVerified('in'))
+ self.assertTrue(self.proto.isVerified('out'))
+ self.assertTrue(self.proto.isVerified('both'))
+ self.proto.currentEncryptions = transport.SSHCiphers('none', 'none',
+ 'none', 'none')
+ self.assertFalse(self.proto.isVerified('in'))
+ self.assertFalse(self.proto.isVerified('out'))
+ self.assertFalse(self.proto.isVerified('both'))
+
+ self.assertRaises(TypeError, self.proto.isVerified, 'bad')
+
+
+ def test_loseConnection(self):
+ """
+ Test that loseConnection sends a disconnect message and closes the
+ connection.
+ """
+ disconnected = [False]
+ def stubLoseConnection():
+ disconnected[0] = True
+ self.transport.loseConnection = stubLoseConnection
+ self.proto.loseConnection()
+ self.assertEqual(self.packets[0][0], transport.MSG_DISCONNECT)
+ self.assertEqual(self.packets[0][1][3],
+ chr(transport.DISCONNECT_CONNECTION_LOST))
+
+
+ def test_badVersion(self):
+ """
+ Test that the transport disconnects when it receives a bad version.
+ """
+ def testBad(version):
+ self.packets = []
+ self.proto.gotVersion = False
+ disconnected = [False]
+ def stubLoseConnection():
+ disconnected[0] = True
+ self.transport.loseConnection = stubLoseConnection
+ for c in version + '\r\n':
+ self.proto.dataReceived(c)
+ self.assertTrue(disconnected[0])
+ self.assertEqual(self.packets[0][0], transport.MSG_DISCONNECT)
+ self.assertEqual(
+ self.packets[0][1][3],
+ chr(transport.DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED))
+ testBad('SSH-1.5-OpenSSH')
+ testBad('SSH-3.0-Twisted')
+ testBad('GET / HTTP/1.1')
+
+
+ def test_dataBeforeVersion(self):
+ """
+ Test that the transport ignores data sent before the version string.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(proto_helpers.StringTransport())
+ data = ("""here's some stuff beforehand
+here's some other stuff
+""" + proto.ourVersionString + "\r\n")
+ [proto.dataReceived(c) for c in data]
+ self.assertTrue(proto.gotVersion)
+ self.assertEqual(proto.otherVersionString, proto.ourVersionString)
+
+
+ def test_compatabilityVersion(self):
+ """
+ Test that the transport treats the compatbility version (1.99)
+ as equivalent to version 2.0.
+ """
+ proto = MockTransportBase()
+ proto.makeConnection(proto_helpers.StringTransport())
+ proto.dataReceived("SSH-1.99-OpenSSH\n")
+ self.assertTrue(proto.gotVersion)
+ self.assertEqual(proto.otherVersionString, "SSH-1.99-OpenSSH")
+
+
+ def test_supportedVersionsAreAllowed(self):
+ """
+ If an unusual SSH version is received and is included in
+ C{supportedVersions}, an unsupported version error is not emitted.
+ """
+ proto = MockTransportBase()
+ proto.supportedVersions = ("9.99", )
+ proto.makeConnection(proto_helpers.StringTransport())
+ proto.dataReceived("SSH-9.99-OpenSSH\n")
+ self.assertFalse(proto.gotUnsupportedVersion)
+
+
+ def test_unsupportedVersionsCallUnsupportedVersionReceived(self):
+ """
+ If an unusual SSH version is received and is not included in
+ C{supportedVersions}, an unsupported version error is emitted.
+ """
+ proto = MockTransportBase()
+ proto.supportedVersions = ("2.0", )
+ proto.makeConnection(proto_helpers.StringTransport())
+ proto.dataReceived("SSH-9.99-OpenSSH\n")
+ self.assertEqual("9.99", proto.gotUnsupportedVersion)
+
+
+ def test_badPackets(self):
+ """
+ Test that the transport disconnects with an error when it receives
+ bad packets.
+ """
+ def testBad(packet, error=transport.DISCONNECT_PROTOCOL_ERROR):
+ self.packets = []
+ self.proto.buf = packet
+ self.assertEqual(self.proto.getPacket(), None)
+ self.assertEqual(len(self.packets), 1)
+ self.assertEqual(self.packets[0][0], transport.MSG_DISCONNECT)
+ self.assertEqual(self.packets[0][1][3], chr(error))
+
+ testBad('\xff' * 8) # big packet
+ testBad('\x00\x00\x00\x05\x00BCDE') # length not modulo blocksize
+ oldEncryptions = self.proto.currentEncryptions
+ self.proto.currentEncryptions = MockCipher()
+ testBad('\x00\x00\x00\x08\x06AB123456', # bad MAC
+ transport.DISCONNECT_MAC_ERROR)
+ self.proto.currentEncryptions.decrypt = lambda x: x[:-1]
+ testBad('\x00\x00\x00\x08\x06BCDEFGHIJK') # bad decryption
+ self.proto.currentEncryptions = oldEncryptions
+ self.proto.incomingCompression = MockCompression()
+ def stubDecompress(payload):
+ raise Exception('bad compression')
+ self.proto.incomingCompression.decompress = stubDecompress
+ testBad('\x00\x00\x00\x04\x00BCDE', # bad decompression
+ transport.DISCONNECT_COMPRESSION_ERROR)
+ self.flushLoggedErrors()
+
+
+ def test_unimplementedPackets(self):
+ """
+ Test that unimplemented packet types cause MSG_UNIMPLEMENTED packets
+ to be sent.
+ """
+ seqnum = self.proto.incomingPacketSequence
+ def checkUnimplemented(seqnum=seqnum):
+ self.assertEqual(self.packets[0][0],
+ transport.MSG_UNIMPLEMENTED)
+ self.assertEqual(self.packets[0][1][3], chr(seqnum))
+ self.proto.packets = []
+ seqnum += 1
+
+ self.proto.dispatchMessage(40, '')
+ checkUnimplemented()
+ transport.messages[41] = 'MSG_fiction'
+ self.proto.dispatchMessage(41, '')
+ checkUnimplemented()
+ self.proto.dispatchMessage(60, '')
+ checkUnimplemented()
+ self.proto.setService(MockService())
+ self.proto.dispatchMessage(70, '')
+ checkUnimplemented()
+ self.proto.dispatchMessage(71, '')
+ checkUnimplemented()
+
+
+ def test_getKey(self):
+ """
+ Test that _getKey generates the correct keys.
+ """
+ self.proto.sessionID = 'EF'
+
+ k1 = sha1('AB' + 'CD' + 'K' + self.proto.sessionID).digest()
+ k2 = sha1('ABCD' + k1).digest()
+ self.assertEqual(self.proto._getKey('K', 'AB', 'CD'), k1 + k2)
+
+
+ def test_multipleClasses(self):
+ """
+ Test that multiple instances have distinct states.
+ """
+ proto = self.proto
+ proto.dataReceived(self.transport.value())
+ proto.currentEncryptions = MockCipher()
+ proto.outgoingCompression = MockCompression()
+ proto.incomingCompression = MockCompression()
+ proto.setService(MockService())
+ proto2 = MockTransportBase()
+ proto2.makeConnection(proto_helpers.StringTransport())
+ proto2.sendIgnore('')
+ self.failIfEquals(proto.gotVersion, proto2.gotVersion)
+ self.failIfEquals(proto.transport, proto2.transport)
+ self.failIfEquals(proto.outgoingPacketSequence,
+ proto2.outgoingPacketSequence)
+ self.failIfEquals(proto.incomingPacketSequence,
+ proto2.incomingPacketSequence)
+ self.failIfEquals(proto.currentEncryptions,
+ proto2.currentEncryptions)
+ self.failIfEquals(proto.service, proto2.service)
+
+
+
+class ServerAndClientSSHTransportBaseCase:
+ """
+ Tests that need to be run on both the server and the client.
+ """
+
+
+ def checkDisconnected(self, kind=None):
+ """
+ Helper function to check if the transport disconnected.
+ """
+ if kind is None:
+ kind = transport.DISCONNECT_PROTOCOL_ERROR
+ self.assertEqual(self.packets[-1][0], transport.MSG_DISCONNECT)
+ self.assertEqual(self.packets[-1][1][3], chr(kind))
+
+
+ def connectModifiedProtocol(self, protoModification,
+ kind=None):
+ """
+ Helper function to connect a modified protocol to the test protocol
+ and test for disconnection.
+ """
+ if kind is None:
+ kind = transport.DISCONNECT_KEY_EXCHANGE_FAILED
+ proto2 = self.klass()
+ protoModification(proto2)
+ proto2.makeConnection(proto_helpers.StringTransport())
+ self.proto.dataReceived(proto2.transport.value())
+ if kind:
+ self.checkDisconnected(kind)
+ return proto2
+
+
+ def test_disconnectIfCantMatchKex(self):
+ """
+ Test that the transport disconnects if it can't match the key
+ exchange
+ """
+ def blankKeyExchanges(proto2):
+ proto2.supportedKeyExchanges = []
+ self.connectModifiedProtocol(blankKeyExchanges)
+
+
+ def test_disconnectIfCantMatchKeyAlg(self):
+ """
+ Like test_disconnectIfCantMatchKex, but for the key algorithm.
+ """
+ def blankPublicKeys(proto2):
+ proto2.supportedPublicKeys = []
+ self.connectModifiedProtocol(blankPublicKeys)
+
+
+ def test_disconnectIfCantMatchCompression(self):
+ """
+ Like test_disconnectIfCantMatchKex, but for the compression.
+ """
+ def blankCompressions(proto2):
+ proto2.supportedCompressions = []
+ self.connectModifiedProtocol(blankCompressions)
+
+
+ def test_disconnectIfCantMatchCipher(self):
+ """
+ Like test_disconnectIfCantMatchKex, but for the encryption.
+ """
+ def blankCiphers(proto2):
+ proto2.supportedCiphers = []
+ self.connectModifiedProtocol(blankCiphers)
+
+
+ def test_disconnectIfCantMatchMAC(self):
+ """
+ Like test_disconnectIfCantMatchKex, but for the MAC.
+ """
+ def blankMACs(proto2):
+ proto2.supportedMACs = []
+ self.connectModifiedProtocol(blankMACs)
+
+
+
+class ServerSSHTransportTestCase(ServerAndClientSSHTransportBaseCase,
+ TransportTestCase):
+ """
+ Tests for the SSHServerTransport.
+ """
+
+ klass = transport.SSHServerTransport
+
+
+ def setUp(self):
+ TransportTestCase.setUp(self)
+ self.proto.factory = MockFactory()
+ self.proto.factory.startFactory()
+
+
+ def tearDown(self):
+ TransportTestCase.tearDown(self)
+ self.proto.factory.stopFactory()
+ del self.proto.factory
+
+
+ def test_KEXINIT(self):
+ """
+ Test that receiving a KEXINIT packet sets up the correct values on the
+ server.
+ """
+ self.proto.dataReceived( 'SSH-2.0-Twisted\r\n\x00\x00\x01\xd4\t\x14'
+ '\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99'
+ '\x99\x00\x00\x00=diffie-hellman-group1-sha1,diffie-hellman-g'
+ 'roup-exchange-sha1\x00\x00\x00\x0fssh-dss,ssh-rsa\x00\x00\x00'
+ '\x85aes128-ctr,aes128-cbc,aes192-ctr,aes192-cbc,aes256-ctr,ae'
+ 's256-cbc,cast128-ctr,cast128-cbc,blowfish-ctr,blowfish-cbc,3d'
+ 'es-ctr,3des-cbc\x00\x00\x00\x85aes128-ctr,aes128-cbc,aes192-c'
+ 'tr,aes192-cbc,aes256-ctr,aes256-cbc,cast128-ctr,cast128-cbc,b'
+ 'lowfish-ctr,blowfish-cbc,3des-ctr,3des-cbc\x00\x00\x00\x12hma'
+ 'c-md5,hmac-sha1\x00\x00\x00\x12hmac-md5,hmac-sha1\x00\x00\x00'
+ '\tnone,zlib\x00\x00\x00\tnone,zlib\x00\x00\x00\x00\x00\x00'
+ '\x00\x00\x00\x00\x00\x00\x00\x99\x99\x99\x99\x99\x99\x99\x99'
+ '\x99')
+ self.assertEqual(self.proto.kexAlg,
+ 'diffie-hellman-group1-sha1')
+ self.assertEqual(self.proto.keyAlg,
+ 'ssh-dss')
+ self.assertEqual(self.proto.outgoingCompressionType,
+ 'none')
+ self.assertEqual(self.proto.incomingCompressionType,
+ 'none')
+ ne = self.proto.nextEncryptions
+ self.assertEqual(ne.outCipType, 'aes128-ctr')
+ self.assertEqual(ne.inCipType, 'aes128-ctr')
+ self.assertEqual(ne.outMACType, 'hmac-md5')
+ self.assertEqual(ne.inMACType, 'hmac-md5')
+
+
+ def test_ignoreGuessPacketKex(self):
+ """
+ The client is allowed to send a guessed key exchange packet
+ after it sends the KEXINIT packet. However, if the key exchanges
+ do not match, that guess packet must be ignored. This tests that
+ the packet is ignored in the case of the key exchange method not
+ matching.
+ """
+ kexInitPacket = '\x00' * 16 + (
+ ''.join([common.NS(x) for x in
+ [','.join(y) for y in
+ [self.proto.supportedKeyExchanges[::-1],
+ self.proto.supportedPublicKeys,
+ self.proto.supportedCiphers,
+ self.proto.supportedCiphers,
+ self.proto.supportedMACs,
+ self.proto.supportedMACs,
+ self.proto.supportedCompressions,
+ self.proto.supportedCompressions,
+ self.proto.supportedLanguages,
+ self.proto.supportedLanguages]]])) + (
+ '\xff\x00\x00\x00\x00')
+ self.proto.ssh_KEXINIT(kexInitPacket)
+ self.assertTrue(self.proto.ignoreNextPacket)
+ self.proto.ssh_DEBUG("\x01\x00\x00\x00\x04test\x00\x00\x00\x00")
+ self.assertTrue(self.proto.ignoreNextPacket)
+
+
+ self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x08\x00')
+ self.assertFalse(self.proto.ignoreNextPacket)
+ self.assertEqual(self.packets, [])
+ self.proto.ignoreNextPacket = True
+
+ self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x08\x00' * 3)
+ self.assertFalse(self.proto.ignoreNextPacket)
+ self.assertEqual(self.packets, [])
+
+
+ def test_ignoreGuessPacketKey(self):
+ """
+ Like test_ignoreGuessPacketKex, but for an incorrectly guessed
+ public key format.
+ """
+ kexInitPacket = '\x00' * 16 + (
+ ''.join([common.NS(x) for x in
+ [','.join(y) for y in
+ [self.proto.supportedKeyExchanges,
+ self.proto.supportedPublicKeys[::-1],
+ self.proto.supportedCiphers,
+ self.proto.supportedCiphers,
+ self.proto.supportedMACs,
+ self.proto.supportedMACs,
+ self.proto.supportedCompressions,
+ self.proto.supportedCompressions,
+ self.proto.supportedLanguages,
+ self.proto.supportedLanguages]]])) + (
+ '\xff\x00\x00\x00\x00')
+ self.proto.ssh_KEXINIT(kexInitPacket)
+ self.assertTrue(self.proto.ignoreNextPacket)
+ self.proto.ssh_DEBUG("\x01\x00\x00\x00\x04test\x00\x00\x00\x00")
+ self.assertTrue(self.proto.ignoreNextPacket)
+
+ self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x08\x00')
+ self.assertFalse(self.proto.ignoreNextPacket)
+ self.assertEqual(self.packets, [])
+ self.proto.ignoreNextPacket = True
+
+ self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x08\x00' * 3)
+ self.assertFalse(self.proto.ignoreNextPacket)
+ self.assertEqual(self.packets, [])
+
+
+ def test_KEXDH_INIT(self):
+ """
+ Test that the KEXDH_INIT packet causes the server to send a
+ KEXDH_REPLY with the server's public key and a signature.
+ """
+ self.proto.supportedKeyExchanges = ['diffie-hellman-group1-sha1']
+ self.proto.supportedPublicKeys = ['ssh-rsa']
+ self.proto.dataReceived(self.transport.value())
+ e = pow(transport.DH_GENERATOR, 5000,
+ transport.DH_PRIME)
+
+ self.proto.ssh_KEX_DH_GEX_REQUEST_OLD(common.MP(e))
+ y = common.getMP('\x00\x00\x00\x40' + '\x99' * 64)[0]
+ f = common._MPpow(transport.DH_GENERATOR, y, transport.DH_PRIME)
+ sharedSecret = common._MPpow(e, y, transport.DH_PRIME)
+
+ h = sha1()
+ h.update(common.NS(self.proto.ourVersionString) * 2)
+ h.update(common.NS(self.proto.ourKexInitPayload) * 2)
+ h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()))
+ h.update(common.MP(e))
+ h.update(f)
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+
+ signature = self.proto.factory.privateKeys['ssh-rsa'].sign(
+ exchangeHash)
+
+ self.assertEqual(
+ self.packets,
+ [(transport.MSG_KEXDH_REPLY,
+ common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob())
+ + f + common.NS(signature)),
+ (transport.MSG_NEWKEYS, '')])
+
+
+ def test_KEX_DH_GEX_REQUEST_OLD(self):
+ """
+ Test that the KEX_DH_GEX_REQUEST_OLD message causes the server
+ to reply with a KEX_DH_GEX_GROUP message with the correct
+ Diffie-Hellman group.
+ """
+ self.proto.supportedKeyExchanges = [
+ 'diffie-hellman-group-exchange-sha1']
+ self.proto.supportedPublicKeys = ['ssh-rsa']
+ self.proto.dataReceived(self.transport.value())
+ self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x04\x00')
+ self.assertEqual(
+ self.packets,
+ [(transport.MSG_KEX_DH_GEX_GROUP,
+ common.MP(transport.DH_PRIME) + '\x00\x00\x00\x01\x02')])
+ self.assertEqual(self.proto.g, 2)
+ self.assertEqual(self.proto.p, transport.DH_PRIME)
+
+
+ def test_KEX_DH_GEX_REQUEST_OLD_badKexAlg(self):
+ """
+ Test that if the server recieves a KEX_DH_GEX_REQUEST_OLD message
+ and the key exchange algorithm is not 'diffie-hellman-group1-sha1' or
+ 'diffie-hellman-group-exchange-sha1', we raise a ConchError.
+ """
+ self.proto.kexAlg = None
+ self.assertRaises(ConchError, self.proto.ssh_KEX_DH_GEX_REQUEST_OLD,
+ None)
+
+
+ def test_KEX_DH_GEX_REQUEST(self):
+ """
+ Test that the KEX_DH_GEX_REQUEST message causes the server to reply
+ with a KEX_DH_GEX_GROUP message with the correct Diffie-Hellman
+ group.
+ """
+ self.proto.supportedKeyExchanges = [
+ 'diffie-hellman-group-exchange-sha1']
+ self.proto.supportedPublicKeys = ['ssh-rsa']
+ self.proto.dataReceived(self.transport.value())
+ self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x04\x00\x00\x00\x08\x00' +
+ '\x00\x00\x0c\x00')
+ self.assertEqual(
+ self.packets,
+ [(transport.MSG_KEX_DH_GEX_GROUP,
+ common.MP(transport.DH_PRIME) + '\x00\x00\x00\x01\x03')])
+ self.assertEqual(self.proto.g, 3)
+ self.assertEqual(self.proto.p, transport.DH_PRIME)
+
+
+ def test_KEX_DH_GEX_INIT_after_REQUEST(self):
+ """
+ Test that the KEX_DH_GEX_INIT message after the client sends
+ KEX_DH_GEX_REQUEST causes the server to send a KEX_DH_GEX_INIT message
+ with a public key and signature.
+ """
+ self.test_KEX_DH_GEX_REQUEST()
+ e = pow(self.proto.g, 3, self.proto.p)
+ y = common.getMP('\x00\x00\x00\x80' + '\x99' * 128)[0]
+ f = common._MPpow(self.proto.g, y, self.proto.p)
+ sharedSecret = common._MPpow(e, y, self.proto.p)
+ h = sha1()
+ h.update(common.NS(self.proto.ourVersionString) * 2)
+ h.update(common.NS(self.proto.ourKexInitPayload) * 2)
+ h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()))
+ h.update('\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00\x0c\x00')
+ h.update(common.MP(self.proto.p))
+ h.update(common.MP(self.proto.g))
+ h.update(common.MP(e))
+ h.update(f)
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ self.proto.ssh_KEX_DH_GEX_INIT(common.MP(e))
+ self.assertEqual(
+ self.packets[1],
+ (transport.MSG_KEX_DH_GEX_REPLY,
+ common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()) +
+ f + common.NS(self.proto.factory.privateKeys['ssh-rsa'].sign(
+ exchangeHash))))
+
+
+ def test_KEX_DH_GEX_INIT_after_REQUEST_OLD(self):
+ """
+ Test that the KEX_DH_GEX_INIT message after the client sends
+ KEX_DH_GEX_REQUEST_OLD causes the server to sent a KEX_DH_GEX_INIT
+ message with a public key and signature.
+ """
+ self.test_KEX_DH_GEX_REQUEST_OLD()
+ e = pow(self.proto.g, 3, self.proto.p)
+ y = common.getMP('\x00\x00\x00\x80' + '\x99' * 128)[0]
+ f = common._MPpow(self.proto.g, y, self.proto.p)
+ sharedSecret = common._MPpow(e, y, self.proto.p)
+ h = sha1()
+ h.update(common.NS(self.proto.ourVersionString) * 2)
+ h.update(common.NS(self.proto.ourKexInitPayload) * 2)
+ h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()))
+ h.update('\x00\x00\x04\x00')
+ h.update(common.MP(self.proto.p))
+ h.update(common.MP(self.proto.g))
+ h.update(common.MP(e))
+ h.update(f)
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+ self.proto.ssh_KEX_DH_GEX_INIT(common.MP(e))
+ self.assertEqual(
+ self.packets[1:],
+ [(transport.MSG_KEX_DH_GEX_REPLY,
+ common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()) +
+ f + common.NS(self.proto.factory.privateKeys['ssh-rsa'].sign(
+ exchangeHash))),
+ (transport.MSG_NEWKEYS, '')])
+
+
+ def test_keySetup(self):
+ """
+ Test that _keySetup sets up the next encryption keys.
+ """
+ self.proto.nextEncryptions = MockCipher()
+ self.simulateKeyExchange('AB', 'CD')
+ self.assertEqual(self.proto.sessionID, 'CD')
+ self.simulateKeyExchange('AB', 'EF')
+ self.assertEqual(self.proto.sessionID, 'CD')
+ self.assertEqual(self.packets[-1], (transport.MSG_NEWKEYS, ''))
+ newKeys = [self.proto._getKey(c, 'AB', 'EF') for c in 'ABCDEF']
+ self.assertEqual(
+ self.proto.nextEncryptions.keys,
+ (newKeys[1], newKeys[3], newKeys[0], newKeys[2], newKeys[5],
+ newKeys[4]))
+
+
+ def test_NEWKEYS(self):
+ """
+ Test that NEWKEYS transitions the keys in nextEncryptions to
+ currentEncryptions.
+ """
+ self.test_KEXINIT()
+
+ self.proto.nextEncryptions = transport.SSHCiphers('none', 'none',
+ 'none', 'none')
+ self.proto.ssh_NEWKEYS('')
+ self.assertIdentical(self.proto.currentEncryptions,
+ self.proto.nextEncryptions)
+ self.assertIdentical(self.proto.outgoingCompression, None)
+ self.assertIdentical(self.proto.incomingCompression, None)
+ self.proto.outgoingCompressionType = 'zlib'
+ self.simulateKeyExchange('AB', 'CD')
+ self.proto.ssh_NEWKEYS('')
+ self.failIfIdentical(self.proto.outgoingCompression, None)
+ self.proto.incomingCompressionType = 'zlib'
+ self.simulateKeyExchange('AB', 'EF')
+ self.proto.ssh_NEWKEYS('')
+ self.failIfIdentical(self.proto.incomingCompression, None)
+
+
+ def test_SERVICE_REQUEST(self):
+ """
+ Test that the SERVICE_REQUEST message requests and starts a
+ service.
+ """
+ self.proto.ssh_SERVICE_REQUEST(common.NS('ssh-userauth'))
+ self.assertEqual(self.packets, [(transport.MSG_SERVICE_ACCEPT,
+ common.NS('ssh-userauth'))])
+ self.assertEqual(self.proto.service.name, 'MockService')
+
+
+ def test_disconnectNEWKEYSData(self):
+ """
+ Test that NEWKEYS disconnects if it receives data.
+ """
+ self.proto.ssh_NEWKEYS("bad packet")
+ self.checkDisconnected()
+
+
+ def test_disconnectSERVICE_REQUESTBadService(self):
+ """
+ Test that SERVICE_REQUESTS disconnects if an unknown service is
+ requested.
+ """
+ self.proto.ssh_SERVICE_REQUEST(common.NS('no service'))
+ self.checkDisconnected(transport.DISCONNECT_SERVICE_NOT_AVAILABLE)
+
+
+
+class ClientSSHTransportTestCase(ServerAndClientSSHTransportBaseCase,
+ TransportTestCase):
+ """
+ Tests for SSHClientTransport.
+ """
+
+ klass = transport.SSHClientTransport
+
+
+ def test_KEXINIT(self):
+ """
+ Test that receiving a KEXINIT packet sets up the correct values on the
+ client. The way algorithms are picks is that the first item in the
+ client's list that is also in the server's list is chosen.
+ """
+ self.proto.dataReceived( 'SSH-2.0-Twisted\r\n\x00\x00\x01\xd4\t\x14'
+ '\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99'
+ '\x99\x00\x00\x00=diffie-hellman-group1-sha1,diffie-hellman-g'
+ 'roup-exchange-sha1\x00\x00\x00\x0fssh-dss,ssh-rsa\x00\x00\x00'
+ '\x85aes128-ctr,aes128-cbc,aes192-ctr,aes192-cbc,aes256-ctr,ae'
+ 's256-cbc,cast128-ctr,cast128-cbc,blowfish-ctr,blowfish-cbc,3d'
+ 'es-ctr,3des-cbc\x00\x00\x00\x85aes128-ctr,aes128-cbc,aes192-c'
+ 'tr,aes192-cbc,aes256-ctr,aes256-cbc,cast128-ctr,cast128-cbc,b'
+ 'lowfish-ctr,blowfish-cbc,3des-ctr,3des-cbc\x00\x00\x00\x12hma'
+ 'c-md5,hmac-sha1\x00\x00\x00\x12hmac-md5,hmac-sha1\x00\x00\x00'
+ '\tzlib,none\x00\x00\x00\tzlib,none\x00\x00\x00\x00\x00\x00'
+ '\x00\x00\x00\x00\x00\x00\x00\x99\x99\x99\x99\x99\x99\x99\x99'
+ '\x99')
+ self.assertEqual(self.proto.kexAlg,
+ 'diffie-hellman-group-exchange-sha1')
+ self.assertEqual(self.proto.keyAlg,
+ 'ssh-rsa')
+ self.assertEqual(self.proto.outgoingCompressionType,
+ 'none')
+ self.assertEqual(self.proto.incomingCompressionType,
+ 'none')
+ ne = self.proto.nextEncryptions
+ self.assertEqual(ne.outCipType, 'aes256-ctr')
+ self.assertEqual(ne.inCipType, 'aes256-ctr')
+ self.assertEqual(ne.outMACType, 'hmac-sha1')
+ self.assertEqual(ne.inMACType, 'hmac-sha1')
+
+
+ def verifyHostKey(self, pubKey, fingerprint):
+ """
+ Mock version of SSHClientTransport.verifyHostKey.
+ """
+ self.calledVerifyHostKey = True
+ self.assertEqual(pubKey, self.blob)
+ self.assertEqual(fingerprint.replace(':', ''),
+ md5(pubKey).hexdigest())
+ return defer.succeed(True)
+
+
+ def setUp(self):
+ TransportTestCase.setUp(self)
+ self.blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
+ self.privObj = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.calledVerifyHostKey = False
+ self.proto.verifyHostKey = self.verifyHostKey
+
+
+ def test_notImplementedClientMethods(self):
+ """
+ verifyHostKey() should return a Deferred which fails with a
+ NotImplementedError exception. connectionSecure() should raise
+ NotImplementedError().
+ """
+ self.assertRaises(NotImplementedError, self.klass().connectionSecure)
+ def _checkRaises(f):
+ f.trap(NotImplementedError)
+ d = self.klass().verifyHostKey(None, None)
+ return d.addCallback(self.fail).addErrback(_checkRaises)
+
+
+ def test_KEXINIT_groupexchange(self):
+ """
+ Test that a KEXINIT packet with a group-exchange key exchange results
+ in a KEX_DH_GEX_REQUEST_OLD message..
+ """
+ self.proto.supportedKeyExchanges = [
+ 'diffie-hellman-group-exchange-sha1']
+ self.proto.dataReceived(self.transport.value())
+ self.assertEqual(self.packets, [(transport.MSG_KEX_DH_GEX_REQUEST_OLD,
+ '\x00\x00\x08\x00')])
+
+
+ def test_KEXINIT_group1(self):
+ """
+ Like test_KEXINIT_groupexchange, but for the group-1 key exchange.
+ """
+ self.proto.supportedKeyExchanges = ['diffie-hellman-group1-sha1']
+ self.proto.dataReceived(self.transport.value())
+ self.assertEqual(common.MP(self.proto.x)[5:], '\x99' * 64)
+ self.assertEqual(self.packets,
+ [(transport.MSG_KEXDH_INIT, self.proto.e)])
+
+
+ def test_KEXINIT_badKexAlg(self):
+ """
+ Test that the client raises a ConchError if it receives a
+ KEXINIT message bug doesn't have a key exchange algorithm that we
+ understand.
+ """
+ self.proto.supportedKeyExchanges = ['diffie-hellman-group2-sha1']
+ data = self.transport.value().replace('group1', 'group2')
+ self.assertRaises(ConchError, self.proto.dataReceived, data)
+
+
+ def test_KEXDH_REPLY(self):
+ """
+ Test that the KEXDH_REPLY message verifies the server.
+ """
+ self.test_KEXINIT_group1()
+
+ sharedSecret = common._MPpow(transport.DH_GENERATOR,
+ self.proto.x, transport.DH_PRIME)
+ h = sha1()
+ h.update(common.NS(self.proto.ourVersionString) * 2)
+ h.update(common.NS(self.proto.ourKexInitPayload) * 2)
+ h.update(common.NS(self.blob))
+ h.update(self.proto.e)
+ h.update('\x00\x00\x00\x01\x02') # f
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+
+ def _cbTestKEXDH_REPLY(value):
+ self.assertIdentical(value, None)
+ self.assertEqual(self.calledVerifyHostKey, True)
+ self.assertEqual(self.proto.sessionID, exchangeHash)
+
+ signature = self.privObj.sign(exchangeHash)
+
+ d = self.proto.ssh_KEX_DH_GEX_GROUP(
+ (common.NS(self.blob) + '\x00\x00\x00\x01\x02' +
+ common.NS(signature)))
+ d.addCallback(_cbTestKEXDH_REPLY)
+
+ return d
+
+
+ def test_KEX_DH_GEX_GROUP(self):
+ """
+ Test that the KEX_DH_GEX_GROUP message results in a
+ KEX_DH_GEX_INIT message with the client's Diffie-Hellman public key.
+ """
+ self.test_KEXINIT_groupexchange()
+ self.proto.ssh_KEX_DH_GEX_GROUP(
+ '\x00\x00\x00\x01\x0f\x00\x00\x00\x01\x02')
+ self.assertEqual(self.proto.p, 15)
+ self.assertEqual(self.proto.g, 2)
+ self.assertEqual(common.MP(self.proto.x)[5:], '\x99' * 40)
+ self.assertEqual(self.proto.e,
+ common.MP(pow(2, self.proto.x, 15)))
+ self.assertEqual(self.packets[1:], [(transport.MSG_KEX_DH_GEX_INIT,
+ self.proto.e)])
+
+
+ def test_KEX_DH_GEX_REPLY(self):
+ """
+ Test that the KEX_DH_GEX_REPLY message results in a verified
+ server.
+ """
+
+ self.test_KEX_DH_GEX_GROUP()
+ sharedSecret = common._MPpow(3, self.proto.x, self.proto.p)
+ h = sha1()
+ h.update(common.NS(self.proto.ourVersionString) * 2)
+ h.update(common.NS(self.proto.ourKexInitPayload) * 2)
+ h.update(common.NS(self.blob))
+ h.update('\x00\x00\x08\x00\x00\x00\x00\x01\x0f\x00\x00\x00\x01\x02')
+ h.update(self.proto.e)
+ h.update('\x00\x00\x00\x01\x03') # f
+ h.update(sharedSecret)
+ exchangeHash = h.digest()
+
+ def _cbTestKEX_DH_GEX_REPLY(value):
+ self.assertIdentical(value, None)
+ self.assertEqual(self.calledVerifyHostKey, True)
+ self.assertEqual(self.proto.sessionID, exchangeHash)
+
+ signature = self.privObj.sign(exchangeHash)
+
+ d = self.proto.ssh_KEX_DH_GEX_REPLY(
+ common.NS(self.blob) +
+ '\x00\x00\x00\x01\x03' +
+ common.NS(signature))
+ d.addCallback(_cbTestKEX_DH_GEX_REPLY)
+ return d
+
+
+ def test_keySetup(self):
+ """
+ Test that _keySetup sets up the next encryption keys.
+ """
+ self.proto.nextEncryptions = MockCipher()
+ self.simulateKeyExchange('AB', 'CD')
+ self.assertEqual(self.proto.sessionID, 'CD')
+ self.simulateKeyExchange('AB', 'EF')
+ self.assertEqual(self.proto.sessionID, 'CD')
+ self.assertEqual(self.packets[-1], (transport.MSG_NEWKEYS, ''))
+ newKeys = [self.proto._getKey(c, 'AB', 'EF') for c in 'ABCDEF']
+ self.assertEqual(self.proto.nextEncryptions.keys,
+ (newKeys[0], newKeys[2], newKeys[1], newKeys[3],
+ newKeys[4], newKeys[5]))
+
+
+ def test_NEWKEYS(self):
+ """
+ Test that NEWKEYS transitions the keys from nextEncryptions to
+ currentEncryptions.
+ """
+ self.test_KEXINIT()
+ secure = [False]
+ def stubConnectionSecure():
+ secure[0] = True
+ self.proto.connectionSecure = stubConnectionSecure
+
+ self.proto.nextEncryptions = transport.SSHCiphers(
+ 'none', 'none', 'none', 'none')
+ self.simulateKeyExchange('AB', 'CD')
+ self.assertNotIdentical(
+ self.proto.currentEncryptions, self.proto.nextEncryptions)
+
+ self.proto.nextEncryptions = MockCipher()
+ self.proto.ssh_NEWKEYS('')
+ self.assertIdentical(self.proto.outgoingCompression, None)
+ self.assertIdentical(self.proto.incomingCompression, None)
+ self.assertIdentical(self.proto.currentEncryptions,
+ self.proto.nextEncryptions)
+ self.assertTrue(secure[0])
+ self.proto.outgoingCompressionType = 'zlib'
+ self.simulateKeyExchange('AB', 'GH')
+ self.proto.ssh_NEWKEYS('')
+ self.failIfIdentical(self.proto.outgoingCompression, None)
+ self.proto.incomingCompressionType = 'zlib'
+ self.simulateKeyExchange('AB', 'IJ')
+ self.proto.ssh_NEWKEYS('')
+ self.failIfIdentical(self.proto.incomingCompression, None)
+
+
+ def test_SERVICE_ACCEPT(self):
+ """
+ Test that the SERVICE_ACCEPT packet starts the requested service.
+ """
+ self.proto.instance = MockService()
+ self.proto.ssh_SERVICE_ACCEPT('\x00\x00\x00\x0bMockService')
+ self.assertTrue(self.proto.instance.started)
+
+
+ def test_requestService(self):
+ """
+ Test that requesting a service sends a SERVICE_REQUEST packet.
+ """
+ self.proto.requestService(MockService())
+ self.assertEqual(self.packets, [(transport.MSG_SERVICE_REQUEST,
+ '\x00\x00\x00\x0bMockService')])
+
+
+ def test_disconnectKEXDH_REPLYBadSignature(self):
+ """
+ Test that KEXDH_REPLY disconnects if the signature is bad.
+ """
+ self.test_KEXDH_REPLY()
+ self.proto._continueKEXDH_REPLY(None, self.blob, 3, "bad signature")
+ self.checkDisconnected(transport.DISCONNECT_KEY_EXCHANGE_FAILED)
+
+
+ def test_disconnectGEX_REPLYBadSignature(self):
+ """
+ Like test_disconnectKEXDH_REPLYBadSignature, but for DH_GEX_REPLY.
+ """
+ self.test_KEX_DH_GEX_REPLY()
+ self.proto._continueGEX_REPLY(None, self.blob, 3, "bad signature")
+ self.checkDisconnected(transport.DISCONNECT_KEY_EXCHANGE_FAILED)
+
+
+ def test_disconnectNEWKEYSData(self):
+ """
+ Test that NEWKEYS disconnects if it receives data.
+ """
+ self.proto.ssh_NEWKEYS("bad packet")
+ self.checkDisconnected()
+
+
+ def test_disconnectSERVICE_ACCEPT(self):
+ """
+ Test that SERVICE_ACCEPT disconnects if the accepted protocol is
+ differet from the asked-for protocol.
+ """
+ self.proto.instance = MockService()
+ self.proto.ssh_SERVICE_ACCEPT('\x00\x00\x00\x03bad')
+ self.checkDisconnected()
+
+
+
+class SSHCiphersTestCase(unittest.TestCase):
+ """
+ Tests for the SSHCiphers helper class.
+ """
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+
+ def test_init(self):
+ """
+ Test that the initializer sets up the SSHCiphers object.
+ """
+ ciphers = transport.SSHCiphers('A', 'B', 'C', 'D')
+ self.assertEqual(ciphers.outCipType, 'A')
+ self.assertEqual(ciphers.inCipType, 'B')
+ self.assertEqual(ciphers.outMACType, 'C')
+ self.assertEqual(ciphers.inMACType, 'D')
+
+
+ def test_getCipher(self):
+ """
+ Test that the _getCipher method returns the correct cipher.
+ """
+ ciphers = transport.SSHCiphers('A', 'B', 'C', 'D')
+ iv = key = '\x00' * 16
+ for cipName, (modName, keySize, counter) in ciphers.cipherMap.items():
+ cip = ciphers._getCipher(cipName, iv, key)
+ if cipName == 'none':
+ self.assertIsInstance(cip, transport._DummyCipher)
+ else:
+ self.assertTrue(str(cip).startswith('<' + modName))
+
+
+ def test_getMAC(self):
+ """
+ Test that the _getMAC method returns the correct MAC.
+ """
+ ciphers = transport.SSHCiphers('A', 'B', 'C', 'D')
+ key = '\x00' * 64
+ for macName, mac in ciphers.macMap.items():
+ mod = ciphers._getMAC(macName, key)
+ if macName == 'none':
+ self.assertIdentical(mac, None)
+ else:
+ self.assertEqual(mod[0], mac)
+ self.assertEqual(mod[1],
+ Crypto.Cipher.XOR.new('\x36').encrypt(key))
+ self.assertEqual(mod[2],
+ Crypto.Cipher.XOR.new('\x5c').encrypt(key))
+ self.assertEqual(mod[3], len(mod[0]().digest()))
+
+
+ def test_setKeysCiphers(self):
+ """
+ Test that setKeys sets up the ciphers.
+ """
+ key = '\x00' * 64
+ cipherItems = transport.SSHCiphers.cipherMap.items()
+ for cipName, (modName, keySize, counter) in cipherItems:
+ encCipher = transport.SSHCiphers(cipName, 'none', 'none', 'none')
+ decCipher = transport.SSHCiphers('none', cipName, 'none', 'none')
+ cip = encCipher._getCipher(cipName, key, key)
+ bs = cip.block_size
+ encCipher.setKeys(key, key, '', '', '', '')
+ decCipher.setKeys('', '', key, key, '', '')
+ self.assertEqual(encCipher.encBlockSize, bs)
+ self.assertEqual(decCipher.decBlockSize, bs)
+ enc = cip.encrypt(key[:bs])
+ enc2 = cip.encrypt(key[:bs])
+ if counter:
+ self.failIfEquals(enc, enc2)
+ self.assertEqual(encCipher.encrypt(key[:bs]), enc)
+ self.assertEqual(encCipher.encrypt(key[:bs]), enc2)
+ self.assertEqual(decCipher.decrypt(enc), key[:bs])
+ self.assertEqual(decCipher.decrypt(enc2), key[:bs])
+
+
+ def test_setKeysMACs(self):
+ """
+ Test that setKeys sets up the MACs.
+ """
+ key = '\x00' * 64
+ for macName, mod in transport.SSHCiphers.macMap.items():
+ outMac = transport.SSHCiphers('none', 'none', macName, 'none')
+ inMac = transport.SSHCiphers('none', 'none', 'none', macName)
+ outMac.setKeys('', '', '', '', key, '')
+ inMac.setKeys('', '', '', '', '', key)
+ if mod:
+ ds = mod().digest_size
+ else:
+ ds = 0
+ self.assertEqual(inMac.verifyDigestSize, ds)
+ if mod:
+ mod, i, o, ds = outMac._getMAC(macName, key)
+ seqid = 0
+ data = key
+ packet = '\x00' * 4 + key
+ if mod:
+ mac = mod(o + mod(i + packet).digest()).digest()
+ else:
+ mac = ''
+ self.assertEqual(outMac.makeMAC(seqid, data), mac)
+ self.assertTrue(inMac.verify(seqid, data, mac))
+
+
+
+class CounterTestCase(unittest.TestCase):
+ """
+ Tests for the _Counter helper class.
+ """
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+
+ def test_init(self):
+ """
+ Test that the counter is initialized correctly.
+ """
+ counter = transport._Counter('\x00' * 8 + '\xff' * 8, 8)
+ self.assertEqual(counter.blockSize, 8)
+ self.assertEqual(counter.count.tostring(), '\x00' * 8)
+
+
+ def test_count(self):
+ """
+ Test that the counter counts incrementally and wraps at the top.
+ """
+ counter = transport._Counter('\x00', 1)
+ self.assertEqual(counter(), '\x01')
+ self.assertEqual(counter(), '\x02')
+ [counter() for i in range(252)]
+ self.assertEqual(counter(), '\xff')
+ self.assertEqual(counter(), '\x00')
+
+
+
+class TransportLoopbackTestCase(unittest.TestCase):
+ """
+ Test the server transport and client transport against each other,
+ """
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+
+ def _runClientServer(self, mod):
+ """
+ Run an async client and server, modifying each using the mod function
+ provided. Returns a Deferred called back when both Protocols have
+ disconnected.
+
+ @type mod: C{func}
+ @rtype: C{defer.Deferred}
+ """
+ factory = MockFactory()
+ server = transport.SSHServerTransport()
+ server.factory = factory
+ factory.startFactory()
+ server.errors = []
+ server.receiveError = lambda code, desc: server.errors.append((
+ code, desc))
+ client = transport.SSHClientTransport()
+ client.verifyHostKey = lambda x, y: defer.succeed(None)
+ client.errors = []
+ client.receiveError = lambda code, desc: client.errors.append((
+ code, desc))
+ client.connectionSecure = lambda: client.loseConnection()
+ server = mod(server)
+ client = mod(client)
+ def check(ignored, server, client):
+ name = repr([server.supportedCiphers[0],
+ server.supportedMACs[0],
+ server.supportedKeyExchanges[0],
+ server.supportedCompressions[0]])
+ self.assertEqual(client.errors, [])
+ self.assertEqual(server.errors, [(
+ transport.DISCONNECT_CONNECTION_LOST,
+ "user closed connection")])
+ if server.supportedCiphers[0] == 'none':
+ self.assertFalse(server.isEncrypted(), name)
+ self.assertFalse(client.isEncrypted(), name)
+ else:
+ self.assertTrue(server.isEncrypted(), name)
+ self.assertTrue(client.isEncrypted(), name)
+ if server.supportedMACs[0] == 'none':
+ self.assertFalse(server.isVerified(), name)
+ self.assertFalse(client.isVerified(), name)
+ else:
+ self.assertTrue(server.isVerified(), name)
+ self.assertTrue(client.isVerified(), name)
+
+ d = loopback.loopbackAsync(server, client)
+ d.addCallback(check, server, client)
+ return d
+
+
+ def test_ciphers(self):
+ """
+ Test that the client and server play nicely together, in all
+ the various combinations of ciphers.
+ """
+ deferreds = []
+ for cipher in transport.SSHTransportBase.supportedCiphers + ['none']:
+ def setCipher(proto):
+ proto.supportedCiphers = [cipher]
+ return proto
+ deferreds.append(self._runClientServer(setCipher))
+ return defer.DeferredList(deferreds, fireOnOneErrback=True)
+
+
+ def test_macs(self):
+ """
+ Like test_ciphers, but for the various MACs.
+ """
+ deferreds = []
+ for mac in transport.SSHTransportBase.supportedMACs + ['none']:
+ def setMAC(proto):
+ proto.supportedMACs = [mac]
+ return proto
+ deferreds.append(self._runClientServer(setMAC))
+ return defer.DeferredList(deferreds, fireOnOneErrback=True)
+
+
+ def test_keyexchanges(self):
+ """
+ Like test_ciphers, but for the various key exchanges.
+ """
+ deferreds = []
+ for kex in transport.SSHTransportBase.supportedKeyExchanges:
+ def setKeyExchange(proto):
+ proto.supportedKeyExchanges = [kex]
+ return proto
+ deferreds.append(self._runClientServer(setKeyExchange))
+ return defer.DeferredList(deferreds, fireOnOneErrback=True)
+
+
+ def test_compressions(self):
+ """
+ Like test_ciphers, but for the various compressions.
+ """
+ deferreds = []
+ for compression in transport.SSHTransportBase.supportedCompressions:
+ def setCompression(proto):
+ proto.supportedCompressions = [compression]
+ return proto
+ deferreds.append(self._runClientServer(setCompression))
+ return defer.DeferredList(deferreds, fireOnOneErrback=True)
+
+
+class RandomNumberTestCase(unittest.TestCase):
+ """
+ Tests for the random number generator L{_getRandomNumber} and private
+ key generator L{_generateX}.
+ """
+ skip = dependencySkip
+
+ def test_usesSuppliedRandomFunction(self):
+ """
+ L{_getRandomNumber} returns an integer constructed directly from the
+ bytes returned by the random byte generator passed to it.
+ """
+ def random(bytes):
+ # The number of bytes requested will be the value of each byte
+ # we return.
+ return chr(bytes) * bytes
+ self.assertEqual(
+ transport._getRandomNumber(random, 32),
+ 4 << 24 | 4 << 16 | 4 << 8 | 4)
+
+
+ def test_rejectsNonByteMultiples(self):
+ """
+ L{_getRandomNumber} raises L{ValueError} if the number of bits
+ passed to L{_getRandomNumber} is not a multiple of 8.
+ """
+ self.assertRaises(
+ ValueError,
+ transport._getRandomNumber, None, 9)
+
+
+ def test_excludesSmall(self):
+ """
+ If the random byte generator passed to L{_generateX} produces bytes
+ which would result in 0 or 1 being returned, these bytes are
+ discarded and another attempt is made to produce a larger value.
+ """
+ results = [chr(0), chr(1), chr(127)]
+ def random(bytes):
+ return results.pop(0) * bytes
+ self.assertEqual(
+ transport._generateX(random, 8),
+ 127)
+
+
+ def test_excludesLarge(self):
+ """
+ If the random byte generator passed to L{_generateX} produces bytes
+ which would result in C{(2 ** bits) - 1} being returned, these bytes
+ are discarded and another attempt is made to produce a smaller
+ value.
+ """
+ results = [chr(255), chr(64)]
+ def random(bytes):
+ return results.pop(0) * bytes
+ self.assertEqual(
+ transport._generateX(random, 8),
+ 64)
+
+
+
+class OldFactoryTestCase(unittest.TestCase):
+ """
+ The old C{SSHFactory.getPublicKeys}() returned mappings of key names to
+ strings of key blobs and mappings of key names to PyCrypto key objects from
+ C{SSHFactory.getPrivateKeys}() (they could also be specified with the
+ C{publicKeys} and C{privateKeys} attributes). This is no longer supported
+ by the C{SSHServerTransport}, so we warn the user if they create an old
+ factory.
+ """
+
+ if Crypto is None:
+ skip = "cannot run w/o PyCrypto"
+
+ if pyasn1 is None:
+ skip = "Cannot run without PyASN1"
+
+
+ def test_getPublicKeysWarning(self):
+ """
+ If the return value of C{getPublicKeys}() isn't a mapping from key
+ names to C{Key} objects, then warn the user and convert the mapping.
+ """
+ sshFactory = MockOldFactoryPublicKeys()
+ self.assertWarns(DeprecationWarning,
+ "Returning a mapping from strings to strings from"
+ " getPublicKeys()/publicKeys (in %s) is deprecated. Return "
+ "a mapping from strings to Key objects instead." %
+ (qual(MockOldFactoryPublicKeys),),
+ factory.__file__, sshFactory.startFactory)
+ self.assertEqual(sshFactory.publicKeys, MockFactory().getPublicKeys())
+
+
+ def test_getPrivateKeysWarning(self):
+ """
+ If the return value of C{getPrivateKeys}() isn't a mapping from key
+ names to C{Key} objects, then warn the user and convert the mapping.
+ """
+ sshFactory = MockOldFactoryPrivateKeys()
+ self.assertWarns(DeprecationWarning,
+ "Returning a mapping from strings to PyCrypto key objects from"
+ " getPrivateKeys()/privateKeys (in %s) is deprecated. Return"
+ " a mapping from strings to Key objects instead." %
+ (qual(MockOldFactoryPrivateKeys),),
+ factory.__file__, sshFactory.startFactory)
+ self.assertEqual(sshFactory.privateKeys,
+ MockFactory().getPrivateKeys())
+
+
+ def test_publicKeysWarning(self):
+ """
+ If the value of the C{publicKeys} attribute isn't a mapping from key
+ names to C{Key} objects, then warn the user and convert the mapping.
+ """
+ sshFactory = MockOldFactoryPublicKeys()
+ sshFactory.publicKeys = sshFactory.getPublicKeys()
+ self.assertWarns(DeprecationWarning,
+ "Returning a mapping from strings to strings from"
+ " getPublicKeys()/publicKeys (in %s) is deprecated. Return "
+ "a mapping from strings to Key objects instead." %
+ (qual(MockOldFactoryPublicKeys),),
+ factory.__file__, sshFactory.startFactory)
+ self.assertEqual(sshFactory.publicKeys, MockFactory().getPublicKeys())
+
+
+ def test_privateKeysWarning(self):
+ """
+ If the return value of C{privateKeys} attribute isn't a mapping from
+ key names to C{Key} objects, then warn the user and convert the
+ mapping.
+ """
+ sshFactory = MockOldFactoryPrivateKeys()
+ sshFactory.privateKeys = sshFactory.getPrivateKeys()
+ self.assertWarns(DeprecationWarning,
+ "Returning a mapping from strings to PyCrypto key objects from"
+ " getPrivateKeys()/privateKeys (in %s) is deprecated. Return"
+ " a mapping from strings to Key objects instead." %
+ (qual(MockOldFactoryPrivateKeys),),
+ factory.__file__, sshFactory.startFactory)
+ self.assertEqual(sshFactory.privateKeys,
+ MockFactory().getPrivateKeys())
diff --git a/twisted/conch/test/test_userauth.py b/twisted/conch/test/test_userauth.py
new file mode 100644
index 0000000..b6ceb45
--- /dev/null
+++ b/twisted/conch/test/test_userauth.py
@@ -0,0 +1,1062 @@
+# -*- test-case-name: twisted.conch.test.test_userauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the implementation of the ssh-userauth service.
+
+Maintainer: Paul Swartz
+"""
+
+from zope.interface import implements
+
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey
+from twisted.cred.credentials import IPluggableAuthenticationModules
+from twisted.cred.credentials import IAnonymous
+from twisted.cred.error import UnauthorizedLogin
+from twisted.cred.portal import IRealm, Portal
+from twisted.conch.error import ConchError, ValidPublicKey
+from twisted.internet import defer, task
+from twisted.protocols import loopback
+from twisted.trial import unittest
+
+try:
+ import Crypto.Cipher.DES3, Crypto.Cipher.XOR
+ import pyasn1
+except ImportError:
+ keys = None
+
+
+ class transport:
+ class SSHTransportBase:
+ """
+ A stub class so that later class definitions won't die.
+ """
+
+ class userauth:
+ class SSHUserAuthClient:
+ """
+ A stub class so that leter class definitions won't die.
+ """
+else:
+ from twisted.conch.ssh.common import NS
+ from twisted.conch.checkers import SSHProtocolChecker
+ from twisted.conch.ssh import keys, userauth, transport
+ from twisted.conch.test import keydata
+
+
+
+class ClientUserAuth(userauth.SSHUserAuthClient):
+ """
+ A mock user auth client.
+ """
+
+
+ def getPublicKey(self):
+ """
+ If this is the first time we've been called, return a blob for
+ the DSA key. Otherwise, return a blob
+ for the RSA key.
+ """
+ if self.lastPublicKey:
+ return keys.Key.fromString(keydata.publicRSA_openssh)
+ else:
+ return defer.succeed(keys.Key.fromString(keydata.publicDSA_openssh))
+
+
+ def getPrivateKey(self):
+ """
+ Return the private key object for the RSA key.
+ """
+ return defer.succeed(keys.Key.fromString(keydata.privateRSA_openssh))
+
+
+ def getPassword(self, prompt=None):
+ """
+ Return 'foo' as the password.
+ """
+ return defer.succeed('foo')
+
+
+ def getGenericAnswers(self, name, information, answers):
+ """
+ Return 'foo' as the answer to two questions.
+ """
+ return defer.succeed(('foo', 'foo'))
+
+
+
+class OldClientAuth(userauth.SSHUserAuthClient):
+ """
+ The old SSHUserAuthClient returned a PyCrypto key object from
+ getPrivateKey() and a string from getPublicKey
+ """
+
+
+ def getPrivateKey(self):
+ return defer.succeed(keys.Key.fromString(
+ keydata.privateRSA_openssh).keyObject)
+
+
+ def getPublicKey(self):
+ return keys.Key.fromString(keydata.publicRSA_openssh).blob()
+
+class ClientAuthWithoutPrivateKey(userauth.SSHUserAuthClient):
+ """
+ This client doesn't have a private key, but it does have a public key.
+ """
+
+
+ def getPrivateKey(self):
+ return
+
+
+ def getPublicKey(self):
+ return keys.Key.fromString(keydata.publicRSA_openssh)
+
+
+
+class FakeTransport(transport.SSHTransportBase):
+ """
+ L{userauth.SSHUserAuthServer} expects an SSH transport which has a factory
+ attribute which has a portal attribute. Because the portal is important for
+ testing authentication, we need to be able to provide an interesting portal
+ object to the L{SSHUserAuthServer}.
+
+ In addition, we want to be able to capture any packets sent over the
+ transport.
+
+ @ivar packets: a list of 2-tuples: (messageType, data). Each 2-tuple is
+ a sent packet.
+ @type packets: C{list}
+ @param lostConnecion: True if loseConnection has been called on us.
+ @type lostConnection: C{bool}
+ """
+
+
+ class Service(object):
+ """
+ A mock service, representing the other service offered by the server.
+ """
+ name = 'nancy'
+
+
+ def serviceStarted(self):
+ pass
+
+
+
+ class Factory(object):
+ """
+ A mock factory, representing the factory that spawned this user auth
+ service.
+ """
+
+
+ def getService(self, transport, service):
+ """
+ Return our fake service.
+ """
+ if service == 'none':
+ return FakeTransport.Service
+
+
+
+ def __init__(self, portal):
+ self.factory = self.Factory()
+ self.factory.portal = portal
+ self.lostConnection = False
+ self.transport = self
+ self.packets = []
+
+
+
+ def sendPacket(self, messageType, message):
+ """
+ Record the packet sent by the service.
+ """
+ self.packets.append((messageType, message))
+
+
+ def isEncrypted(self, direction):
+ """
+ Pretend that this transport encrypts traffic in both directions. The
+ SSHUserAuthServer disables password authentication if the transport
+ isn't encrypted.
+ """
+ return True
+
+
+ def loseConnection(self):
+ self.lostConnection = True
+
+
+
+class Realm(object):
+ """
+ A mock realm for testing L{userauth.SSHUserAuthServer}.
+
+ This realm is not actually used in the course of testing, so it returns the
+ simplest thing that could possibly work.
+ """
+ implements(IRealm)
+
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return defer.succeed((interfaces[0], None, lambda: None))
+
+
+
+class PasswordChecker(object):
+ """
+ A very simple username/password checker which authenticates anyone whose
+ password matches their username and rejects all others.
+ """
+ credentialInterfaces = (IUsernamePassword,)
+ implements(ICredentialsChecker)
+
+
+ def requestAvatarId(self, creds):
+ if creds.username == creds.password:
+ return defer.succeed(creds.username)
+ return defer.fail(UnauthorizedLogin("Invalid username/password pair"))
+
+
+
+class PrivateKeyChecker(object):
+ """
+ A very simple public key checker which authenticates anyone whose
+ public/private keypair is the same keydata.public/privateRSA_openssh.
+ """
+ credentialInterfaces = (ISSHPrivateKey,)
+ implements(ICredentialsChecker)
+
+
+
+ def requestAvatarId(self, creds):
+ if creds.blob == keys.Key.fromString(keydata.publicRSA_openssh).blob():
+ if creds.signature is not None:
+ obj = keys.Key.fromString(creds.blob)
+ if obj.verify(creds.signature, creds.sigData):
+ return creds.username
+ else:
+ raise ValidPublicKey()
+ raise UnauthorizedLogin()
+
+
+
+class PAMChecker(object):
+ """
+ A simple PAM checker which asks the user for a password, verifying them
+ if the password is the same as their username.
+ """
+ credentialInterfaces = (IPluggableAuthenticationModules,)
+ implements(ICredentialsChecker)
+
+
+ def requestAvatarId(self, creds):
+ d = creds.pamConversion([('Name: ', 2), ("Password: ", 1)])
+ def check(values):
+ if values == [(creds.username, 0), (creds.username, 0)]:
+ return creds.username
+ raise UnauthorizedLogin()
+ return d.addCallback(check)
+
+
+
+class AnonymousChecker(object):
+ """
+ A simple checker which isn't supported by L{SSHUserAuthServer}.
+ """
+ credentialInterfaces = (IAnonymous,)
+ implements(ICredentialsChecker)
+
+
+
+class SSHUserAuthServerTestCase(unittest.TestCase):
+ """
+ Tests for SSHUserAuthServer.
+ """
+
+
+ if keys is None:
+ skip = "cannot run w/o PyCrypto"
+
+
+ def setUp(self):
+ self.realm = Realm()
+ self.portal = Portal(self.realm)
+ self.portal.registerChecker(PasswordChecker())
+ self.portal.registerChecker(PrivateKeyChecker())
+ self.portal.registerChecker(PAMChecker())
+ self.authServer = userauth.SSHUserAuthServer()
+ self.authServer.transport = FakeTransport(self.portal)
+ self.authServer.serviceStarted()
+ self.authServer.supportedAuthentications.sort() # give a consistent
+ # order
+
+
+ def tearDown(self):
+ self.authServer.serviceStopped()
+ self.authServer = None
+
+
+ def _checkFailed(self, ignored):
+ """
+ Check that the authentication has failed.
+ """
+ self.assertEqual(self.authServer.transport.packets[-1],
+ (userauth.MSG_USERAUTH_FAILURE,
+ NS('keyboard-interactive,password,publickey') + '\x00'))
+
+
+ def test_noneAuthentication(self):
+ """
+ A client may request a list of authentication 'method name' values
+ that may continue by using the "none" authentication 'method name'.
+
+ See RFC 4252 Section 5.2.
+ """
+ d = self.authServer.ssh_USERAUTH_REQUEST(NS('foo') + NS('service') +
+ NS('none'))
+ return d.addCallback(self._checkFailed)
+
+
+ def test_successfulPasswordAuthentication(self):
+ """
+ When provided with correct password authentication information, the
+ server should respond by sending a MSG_USERAUTH_SUCCESS message with
+ no other data.
+
+ See RFC 4252, Section 5.1.
+ """
+ packet = NS('foo') + NS('none') + NS('password') + chr(0) + NS('foo')
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ def check(ignored):
+ self.assertEqual(
+ self.authServer.transport.packets,
+ [(userauth.MSG_USERAUTH_SUCCESS, '')])
+ return d.addCallback(check)
+
+
+ def test_failedPasswordAuthentication(self):
+ """
+ When provided with invalid authentication details, the server should
+ respond by sending a MSG_USERAUTH_FAILURE message which states whether
+ the authentication was partially successful, and provides other, open
+ options for authentication.
+
+ See RFC 4252, Section 5.1.
+ """
+ # packet = username, next_service, authentication type, FALSE, password
+ packet = NS('foo') + NS('none') + NS('password') + chr(0) + NS('bar')
+ self.authServer.clock = task.Clock()
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.assertEqual(self.authServer.transport.packets, [])
+ self.authServer.clock.advance(2)
+ return d.addCallback(self._checkFailed)
+
+
+ def test_successfulPrivateKeyAuthentication(self):
+ """
+ Test that private key authentication completes sucessfully,
+ """
+ blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
+ obj = keys.Key.fromString(keydata.privateRSA_openssh)
+ packet = (NS('foo') + NS('none') + NS('publickey') + '\xff'
+ + NS(obj.sshType()) + NS(blob))
+ self.authServer.transport.sessionID = 'test'
+ signature = obj.sign(NS('test') + chr(userauth.MSG_USERAUTH_REQUEST)
+ + packet)
+ packet += NS(signature)
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ def check(ignored):
+ self.assertEqual(self.authServer.transport.packets,
+ [(userauth.MSG_USERAUTH_SUCCESS, '')])
+ return d.addCallback(check)
+
+
+ def test_requestRaisesConchError(self):
+ """
+ ssh_USERAUTH_REQUEST should raise a ConchError if tryAuth returns
+ None. Added to catch a bug noticed by pyflakes.
+ """
+ d = defer.Deferred()
+
+ def mockCbFinishedAuth(self, ignored):
+ self.fail('request should have raised ConochError')
+
+ def mockTryAuth(kind, user, data):
+ return None
+
+ def mockEbBadAuth(reason):
+ d.errback(reason.value)
+
+ self.patch(self.authServer, 'tryAuth', mockTryAuth)
+ self.patch(self.authServer, '_cbFinishedAuth', mockCbFinishedAuth)
+ self.patch(self.authServer, '_ebBadAuth', mockEbBadAuth)
+
+ packet = NS('user') + NS('none') + NS('public-key') + NS('data')
+ # If an error other than ConchError is raised, this will trigger an
+ # exception.
+ self.authServer.ssh_USERAUTH_REQUEST(packet)
+ return self.assertFailure(d, ConchError)
+
+
+ def test_verifyValidPrivateKey(self):
+ """
+ Test that verifying a valid private key works.
+ """
+ blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
+ packet = (NS('foo') + NS('none') + NS('publickey') + '\x00'
+ + NS('ssh-rsa') + NS(blob))
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ def check(ignored):
+ self.assertEqual(self.authServer.transport.packets,
+ [(userauth.MSG_USERAUTH_PK_OK, NS('ssh-rsa') + NS(blob))])
+ return d.addCallback(check)
+
+
+ def test_failedPrivateKeyAuthenticationWithoutSignature(self):
+ """
+ Test that private key authentication fails when the public key
+ is invalid.
+ """
+ blob = keys.Key.fromString(keydata.publicDSA_openssh).blob()
+ packet = (NS('foo') + NS('none') + NS('publickey') + '\x00'
+ + NS('ssh-dsa') + NS(blob))
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ return d.addCallback(self._checkFailed)
+
+
+ def test_failedPrivateKeyAuthenticationWithSignature(self):
+ """
+ Test that private key authentication fails when the public key
+ is invalid.
+ """
+ blob = keys.Key.fromString(keydata.publicRSA_openssh).blob()
+ obj = keys.Key.fromString(keydata.privateRSA_openssh)
+ packet = (NS('foo') + NS('none') + NS('publickey') + '\xff'
+ + NS('ssh-rsa') + NS(blob) + NS(obj.sign(blob)))
+ self.authServer.transport.sessionID = 'test'
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ return d.addCallback(self._checkFailed)
+
+
+ def test_successfulPAMAuthentication(self):
+ """
+ Test that keyboard-interactive authentication succeeds.
+ """
+ packet = (NS('foo') + NS('none') + NS('keyboard-interactive')
+ + NS('') + NS(''))
+ response = '\x00\x00\x00\x02' + NS('foo') + NS('foo')
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.ssh_USERAUTH_INFO_RESPONSE(response)
+ def check(ignored):
+ self.assertEqual(self.authServer.transport.packets,
+ [(userauth.MSG_USERAUTH_INFO_REQUEST, (NS('') + NS('')
+ + NS('') + '\x00\x00\x00\x02' + NS('Name: ') + '\x01'
+ + NS('Password: ') + '\x00')),
+ (userauth.MSG_USERAUTH_SUCCESS, '')])
+
+ return d.addCallback(check)
+
+
+ def test_failedPAMAuthentication(self):
+ """
+ Test that keyboard-interactive authentication fails.
+ """
+ packet = (NS('foo') + NS('none') + NS('keyboard-interactive')
+ + NS('') + NS(''))
+ response = '\x00\x00\x00\x02' + NS('bar') + NS('bar')
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.ssh_USERAUTH_INFO_RESPONSE(response)
+ def check(ignored):
+ self.assertEqual(self.authServer.transport.packets[0],
+ (userauth.MSG_USERAUTH_INFO_REQUEST, (NS('') + NS('')
+ + NS('') + '\x00\x00\x00\x02' + NS('Name: ') + '\x01'
+ + NS('Password: ') + '\x00')))
+ return d.addCallback(check).addCallback(self._checkFailed)
+
+
+ def test_invalid_USERAUTH_INFO_RESPONSE_not_enough_data(self):
+ """
+ If ssh_USERAUTH_INFO_RESPONSE gets an invalid packet,
+ the user authentication should fail.
+ """
+ packet = (NS('foo') + NS('none') + NS('keyboard-interactive')
+ + NS('') + NS(''))
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.ssh_USERAUTH_INFO_RESPONSE(NS('\x00\x00\x00\x00' +
+ NS('hi')))
+ return d.addCallback(self._checkFailed)
+
+
+ def test_invalid_USERAUTH_INFO_RESPONSE_too_much_data(self):
+ """
+ If ssh_USERAUTH_INFO_RESPONSE gets too much data, the user
+ authentication should fail.
+ """
+ packet = (NS('foo') + NS('none') + NS('keyboard-interactive')
+ + NS('') + NS(''))
+ response = '\x00\x00\x00\x02' + NS('foo') + NS('foo') + NS('foo')
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.ssh_USERAUTH_INFO_RESPONSE(response)
+ return d.addCallback(self._checkFailed)
+
+
+ def test_onlyOnePAMAuthentication(self):
+ """
+ Because it requires an intermediate message, one can't send a second
+ keyboard-interactive request while the first is still pending.
+ """
+ packet = (NS('foo') + NS('none') + NS('keyboard-interactive')
+ + NS('') + NS(''))
+ self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.assertEqual(self.authServer.transport.packets[-1][0],
+ transport.MSG_DISCONNECT)
+ self.assertEqual(self.authServer.transport.packets[-1][1][3],
+ chr(transport.DISCONNECT_PROTOCOL_ERROR))
+
+
+ def test_ignoreUnknownCredInterfaces(self):
+ """
+ L{SSHUserAuthServer} sets up
+ C{SSHUserAuthServer.supportedAuthentications} by checking the portal's
+ credentials interfaces and mapping them to SSH authentication method
+ strings. If the Portal advertises an interface that
+ L{SSHUserAuthServer} can't map, it should be ignored. This is a white
+ box test.
+ """
+ server = userauth.SSHUserAuthServer()
+ server.transport = FakeTransport(self.portal)
+ self.portal.registerChecker(AnonymousChecker())
+ server.serviceStarted()
+ server.serviceStopped()
+ server.supportedAuthentications.sort() # give a consistent order
+ self.assertEqual(server.supportedAuthentications,
+ ['keyboard-interactive', 'password', 'publickey'])
+
+
+ def test_removePasswordIfUnencrypted(self):
+ """
+ Test that the userauth service does not advertise password
+ authentication if the password would be send in cleartext.
+ """
+ self.assertIn('password', self.authServer.supportedAuthentications)
+ # no encryption
+ clearAuthServer = userauth.SSHUserAuthServer()
+ clearAuthServer.transport = FakeTransport(self.portal)
+ clearAuthServer.transport.isEncrypted = lambda x: False
+ clearAuthServer.serviceStarted()
+ clearAuthServer.serviceStopped()
+ self.failIfIn('password', clearAuthServer.supportedAuthentications)
+ # only encrypt incoming (the direction the password is sent)
+ halfAuthServer = userauth.SSHUserAuthServer()
+ halfAuthServer.transport = FakeTransport(self.portal)
+ halfAuthServer.transport.isEncrypted = lambda x: x == 'in'
+ halfAuthServer.serviceStarted()
+ halfAuthServer.serviceStopped()
+ self.assertIn('password', halfAuthServer.supportedAuthentications)
+
+
+ def test_removeKeyboardInteractiveIfUnencrypted(self):
+ """
+ Test that the userauth service does not advertise keyboard-interactive
+ authentication if the password would be send in cleartext.
+ """
+ self.assertIn('keyboard-interactive',
+ self.authServer.supportedAuthentications)
+ # no encryption
+ clearAuthServer = userauth.SSHUserAuthServer()
+ clearAuthServer.transport = FakeTransport(self.portal)
+ clearAuthServer.transport.isEncrypted = lambda x: False
+ clearAuthServer.serviceStarted()
+ clearAuthServer.serviceStopped()
+ self.failIfIn('keyboard-interactive',
+ clearAuthServer.supportedAuthentications)
+ # only encrypt incoming (the direction the password is sent)
+ halfAuthServer = userauth.SSHUserAuthServer()
+ halfAuthServer.transport = FakeTransport(self.portal)
+ halfAuthServer.transport.isEncrypted = lambda x: x == 'in'
+ halfAuthServer.serviceStarted()
+ halfAuthServer.serviceStopped()
+ self.assertIn('keyboard-interactive',
+ halfAuthServer.supportedAuthentications)
+
+
+ def test_unencryptedConnectionWithoutPasswords(self):
+ """
+ If the L{SSHUserAuthServer} is not advertising passwords, then an
+ unencrypted connection should not cause any warnings or exceptions.
+ This is a white box test.
+ """
+ # create a Portal without password authentication
+ portal = Portal(self.realm)
+ portal.registerChecker(PrivateKeyChecker())
+
+ # no encryption
+ clearAuthServer = userauth.SSHUserAuthServer()
+ clearAuthServer.transport = FakeTransport(portal)
+ clearAuthServer.transport.isEncrypted = lambda x: False
+ clearAuthServer.serviceStarted()
+ clearAuthServer.serviceStopped()
+ self.assertEqual(clearAuthServer.supportedAuthentications,
+ ['publickey'])
+
+ # only encrypt incoming (the direction the password is sent)
+ halfAuthServer = userauth.SSHUserAuthServer()
+ halfAuthServer.transport = FakeTransport(portal)
+ halfAuthServer.transport.isEncrypted = lambda x: x == 'in'
+ halfAuthServer.serviceStarted()
+ halfAuthServer.serviceStopped()
+ self.assertEqual(clearAuthServer.supportedAuthentications,
+ ['publickey'])
+
+
+ def test_loginTimeout(self):
+ """
+ Test that the login times out.
+ """
+ timeoutAuthServer = userauth.SSHUserAuthServer()
+ timeoutAuthServer.clock = task.Clock()
+ timeoutAuthServer.transport = FakeTransport(self.portal)
+ timeoutAuthServer.serviceStarted()
+ timeoutAuthServer.clock.advance(11 * 60 * 60)
+ timeoutAuthServer.serviceStopped()
+ self.assertEqual(timeoutAuthServer.transport.packets,
+ [(transport.MSG_DISCONNECT,
+ '\x00' * 3 +
+ chr(transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) +
+ NS("you took too long") + NS(''))])
+ self.assertTrue(timeoutAuthServer.transport.lostConnection)
+
+
+ def test_cancelLoginTimeout(self):
+ """
+ Test that stopping the service also stops the login timeout.
+ """
+ timeoutAuthServer = userauth.SSHUserAuthServer()
+ timeoutAuthServer.clock = task.Clock()
+ timeoutAuthServer.transport = FakeTransport(self.portal)
+ timeoutAuthServer.serviceStarted()
+ timeoutAuthServer.serviceStopped()
+ timeoutAuthServer.clock.advance(11 * 60 * 60)
+ self.assertEqual(timeoutAuthServer.transport.packets, [])
+ self.assertFalse(timeoutAuthServer.transport.lostConnection)
+
+
+ def test_tooManyAttempts(self):
+ """
+ Test that the server disconnects if the client fails authentication
+ too many times.
+ """
+ packet = NS('foo') + NS('none') + NS('password') + chr(0) + NS('bar')
+ self.authServer.clock = task.Clock()
+ for i in range(21):
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ self.authServer.clock.advance(2)
+ def check(ignored):
+ self.assertEqual(self.authServer.transport.packets[-1],
+ (transport.MSG_DISCONNECT,
+ '\x00' * 3 +
+ chr(transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) +
+ NS("too many bad auths") + NS('')))
+ return d.addCallback(check)
+
+
+ def test_failIfUnknownService(self):
+ """
+ If the user requests a service that we don't support, the
+ authentication should fail.
+ """
+ packet = NS('foo') + NS('') + NS('password') + chr(0) + NS('foo')
+ self.authServer.clock = task.Clock()
+ d = self.authServer.ssh_USERAUTH_REQUEST(packet)
+ return d.addCallback(self._checkFailed)
+
+
+ def test__pamConvErrors(self):
+ """
+ _pamConv should fail if it gets a message that's not 1 or 2.
+ """
+ def secondTest(ignored):
+ d2 = self.authServer._pamConv([('', 90)])
+ return self.assertFailure(d2, ConchError)
+
+ d = self.authServer._pamConv([('', 3)])
+ return self.assertFailure(d, ConchError).addCallback(secondTest)
+
+
+ def test_tryAuthEdgeCases(self):
+ """
+ tryAuth() has two edge cases that are difficult to reach.
+
+ 1) an authentication method auth_* returns None instead of a Deferred.
+ 2) an authentication type that is defined does not have a matching
+ auth_* method.
+
+ Both these cases should return a Deferred which fails with a
+ ConchError.
+ """
+ def mockAuth(packet):
+ return None
+
+ self.patch(self.authServer, 'auth_publickey', mockAuth) # first case
+ self.patch(self.authServer, 'auth_password', None) # second case
+
+ def secondTest(ignored):
+ d2 = self.authServer.tryAuth('password', None, None)
+ return self.assertFailure(d2, ConchError)
+
+ d1 = self.authServer.tryAuth('publickey', None, None)
+ return self.assertFailure(d1, ConchError).addCallback(secondTest)
+
+
+
+
+class SSHUserAuthClientTestCase(unittest.TestCase):
+ """
+ Tests for SSHUserAuthClient.
+ """
+
+
+ if keys is None:
+ skip = "cannot run w/o PyCrypto"
+
+
+ def setUp(self):
+ self.authClient = ClientUserAuth('foo', FakeTransport.Service())
+ self.authClient.transport = FakeTransport(None)
+ self.authClient.transport.sessionID = 'test'
+ self.authClient.serviceStarted()
+
+
+ def tearDown(self):
+ self.authClient.serviceStopped()
+ self.authClient = None
+
+
+ def test_init(self):
+ """
+ Test that client is initialized properly.
+ """
+ self.assertEqual(self.authClient.user, 'foo')
+ self.assertEqual(self.authClient.instance.name, 'nancy')
+ self.assertEqual(self.authClient.transport.packets,
+ [(userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('none'))])
+
+
+ def test_USERAUTH_SUCCESS(self):
+ """
+ Test that the client succeeds properly.
+ """
+ instance = [None]
+ def stubSetService(service):
+ instance[0] = service
+ self.authClient.transport.setService = stubSetService
+ self.authClient.ssh_USERAUTH_SUCCESS('')
+ self.assertEqual(instance[0], self.authClient.instance)
+
+
+ def test_publickey(self):
+ """
+ Test that the client can authenticate with a public key.
+ """
+ self.authClient.ssh_USERAUTH_FAILURE(NS('publickey') + '\x00')
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('publickey') + '\x00' + NS('ssh-dss')
+ + NS(keys.Key.fromString(
+ keydata.publicDSA_openssh).blob())))
+ # that key isn't good
+ self.authClient.ssh_USERAUTH_FAILURE(NS('publickey') + '\x00')
+ blob = NS(keys.Key.fromString(keydata.publicRSA_openssh).blob())
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, (NS('foo') + NS('nancy')
+ + NS('publickey') + '\x00'+ NS('ssh-rsa') + blob)))
+ self.authClient.ssh_USERAUTH_PK_OK(NS('ssh-rsa')
+ + NS(keys.Key.fromString(keydata.publicRSA_openssh).blob()))
+ sigData = (NS(self.authClient.transport.sessionID)
+ + chr(userauth.MSG_USERAUTH_REQUEST) + NS('foo')
+ + NS('nancy') + NS('publickey') + '\x01' + NS('ssh-rsa')
+ + blob)
+ obj = keys.Key.fromString(keydata.privateRSA_openssh)
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('publickey') + '\x01' + NS('ssh-rsa') + blob
+ + NS(obj.sign(sigData))))
+
+
+ def test_publickey_without_privatekey(self):
+ """
+ If the SSHUserAuthClient doesn't return anything from signData,
+ the client should start the authentication over again by requesting
+ 'none' authentication.
+ """
+ authClient = ClientAuthWithoutPrivateKey('foo',
+ FakeTransport.Service())
+
+ authClient.transport = FakeTransport(None)
+ authClient.transport.sessionID = 'test'
+ authClient.serviceStarted()
+ authClient.tryAuth('publickey')
+ authClient.transport.packets = []
+ self.assertIdentical(authClient.ssh_USERAUTH_PK_OK(''), None)
+ self.assertEqual(authClient.transport.packets, [
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy') +
+ NS('none'))])
+
+
+ def test_old_publickey_getPublicKey(self):
+ """
+ Old SSHUserAuthClients returned strings of public key blobs from
+ getPublicKey(). Test that a Deprecation warning is raised but the key is
+ verified correctly.
+ """
+ oldAuth = OldClientAuth('foo', FakeTransport.Service())
+ oldAuth.transport = FakeTransport(None)
+ oldAuth.transport.sessionID = 'test'
+ oldAuth.serviceStarted()
+ oldAuth.transport.packets = []
+ self.assertWarns(DeprecationWarning, "Returning a string from "
+ "SSHUserAuthClient.getPublicKey() is deprecated since "
+ "Twisted 9.0. Return a keys.Key() instead.",
+ userauth.__file__, oldAuth.tryAuth, 'publickey')
+ self.assertEqual(oldAuth.transport.packets, [
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy') +
+ NS('publickey') + '\x00' + NS('ssh-rsa') +
+ NS(keys.Key.fromString(keydata.publicRSA_openssh).blob()))])
+
+
+ def test_old_publickey_getPrivateKey(self):
+ """
+ Old SSHUserAuthClients returned a PyCrypto key object from
+ getPrivateKey(). Test that _cbSignData signs the data warns the
+ user about the deprecation, but signs the data correctly.
+ """
+ oldAuth = OldClientAuth('foo', FakeTransport.Service())
+ d = self.assertWarns(DeprecationWarning, "Returning a PyCrypto key "
+ "object from SSHUserAuthClient.getPrivateKey() is "
+ "deprecated since Twisted 9.0. "
+ "Return a keys.Key() instead.", userauth.__file__,
+ oldAuth.signData, None, 'data')
+ def _checkSignedData(sig):
+ self.assertEqual(sig,
+ keys.Key.fromString(keydata.privateRSA_openssh).sign(
+ 'data'))
+ d.addCallback(_checkSignedData)
+ return d
+
+
+ def test_no_publickey(self):
+ """
+ If there's no public key, auth_publickey should return a Deferred
+ called back with a False value.
+ """
+ self.authClient.getPublicKey = lambda x: None
+ d = self.authClient.tryAuth('publickey')
+ def check(result):
+ self.assertFalse(result)
+ return d.addCallback(check)
+
+ def test_password(self):
+ """
+ Test that the client can authentication with a password. This
+ includes changing the password.
+ """
+ self.authClient.ssh_USERAUTH_FAILURE(NS('password') + '\x00')
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('password') + '\x00' + NS('foo')))
+ self.authClient.ssh_USERAUTH_PK_OK(NS('') + NS(''))
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('password') + '\xff' + NS('foo') * 2))
+
+
+ def test_no_password(self):
+ """
+ If getPassword returns None, tryAuth should return False.
+ """
+ self.authClient.getPassword = lambda: None
+ self.assertFalse(self.authClient.tryAuth('password'))
+
+
+ def test_keyboardInteractive(self):
+ """
+ Test that the client can authenticate using keyboard-interactive
+ authentication.
+ """
+ self.authClient.ssh_USERAUTH_FAILURE(NS('keyboard-interactive')
+ + '\x00')
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('keyboard-interactive') + NS('')*2))
+ self.authClient.ssh_USERAUTH_PK_OK(NS('')*3 + '\x00\x00\x00\x02'
+ + NS('Name: ') + '\xff' + NS('Password: ') + '\x00')
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_INFO_RESPONSE, '\x00\x00\x00\x02'
+ + NS('foo')*2))
+
+
+ def test_USERAUTH_PK_OK_unknown_method(self):
+ """
+ If C{SSHUserAuthClient} gets a MSG_USERAUTH_PK_OK packet when it's not
+ expecting it, it should fail the current authentication and move on to
+ the next type.
+ """
+ self.authClient.lastAuth = 'unknown'
+ self.authClient.transport.packets = []
+ self.authClient.ssh_USERAUTH_PK_OK('')
+ self.assertEqual(self.authClient.transport.packets,
+ [(userauth.MSG_USERAUTH_REQUEST, NS('foo') +
+ NS('nancy') + NS('none'))])
+
+
+ def test_USERAUTH_FAILURE_sorting(self):
+ """
+ ssh_USERAUTH_FAILURE should sort the methods by their position
+ in SSHUserAuthClient.preferredOrder. Methods that are not in
+ preferredOrder should be sorted at the end of that list.
+ """
+ def auth_firstmethod():
+ self.authClient.transport.sendPacket(255, 'here is data')
+ def auth_anothermethod():
+ self.authClient.transport.sendPacket(254, 'other data')
+ return True
+ self.authClient.auth_firstmethod = auth_firstmethod
+ self.authClient.auth_anothermethod = auth_anothermethod
+
+ # although they shouldn't get called, method callbacks auth_* MUST
+ # exist in order for the test to work properly.
+ self.authClient.ssh_USERAUTH_FAILURE(NS('anothermethod,password') +
+ '\x00')
+ # should send password packet
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('password') + '\x00' + NS('foo')))
+ self.authClient.ssh_USERAUTH_FAILURE(
+ NS('firstmethod,anothermethod,password') + '\xff')
+ self.assertEqual(self.authClient.transport.packets[-2:],
+ [(255, 'here is data'), (254, 'other data')])
+
+
+ def test_disconnectIfNoMoreAuthentication(self):
+ """
+ If there are no more available user authentication messages,
+ the SSHUserAuthClient should disconnect with code
+ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE.
+ """
+ self.authClient.ssh_USERAUTH_FAILURE(NS('password') + '\x00')
+ self.authClient.ssh_USERAUTH_FAILURE(NS('password') + '\xff')
+ self.assertEqual(self.authClient.transport.packets[-1],
+ (transport.MSG_DISCONNECT, '\x00\x00\x00\x0e' +
+ NS('no more authentication methods available') +
+ '\x00\x00\x00\x00'))
+
+
+ def test_ebAuth(self):
+ """
+ _ebAuth (the generic authentication error handler) should send
+ a request for the 'none' authentication method.
+ """
+ self.authClient.transport.packets = []
+ self.authClient._ebAuth(None)
+ self.assertEqual(self.authClient.transport.packets,
+ [(userauth.MSG_USERAUTH_REQUEST, NS('foo') + NS('nancy')
+ + NS('none'))])
+
+
+ def test_defaults(self):
+ """
+ getPublicKey() should return None. getPrivateKey() should return a
+ failed Deferred. getPassword() should return a failed Deferred.
+ getGenericAnswers() should return a failed Deferred.
+ """
+ authClient = userauth.SSHUserAuthClient('foo', FakeTransport.Service())
+ self.assertIdentical(authClient.getPublicKey(), None)
+ def check(result):
+ result.trap(NotImplementedError)
+ d = authClient.getPassword()
+ return d.addCallback(self.fail).addErrback(check2)
+ def check2(result):
+ result.trap(NotImplementedError)
+ d = authClient.getGenericAnswers(None, None, None)
+ return d.addCallback(self.fail).addErrback(check3)
+ def check3(result):
+ result.trap(NotImplementedError)
+ d = authClient.getPrivateKey()
+ return d.addCallback(self.fail).addErrback(check)
+
+
+
+class LoopbackTestCase(unittest.TestCase):
+
+
+ if keys is None:
+ skip = "cannot run w/o PyCrypto or PyASN1"
+
+
+ class Factory:
+ class Service:
+ name = 'TestService'
+
+
+ def serviceStarted(self):
+ self.transport.loseConnection()
+
+
+ def serviceStopped(self):
+ pass
+
+
+ def getService(self, avatar, name):
+ return self.Service
+
+
+ def test_loopback(self):
+ """
+ Test that the userauth server and client play nicely with each other.
+ """
+ server = userauth.SSHUserAuthServer()
+ client = ClientUserAuth('foo', self.Factory.Service())
+
+ # set up transports
+ server.transport = transport.SSHTransportBase()
+ server.transport.service = server
+ server.transport.isEncrypted = lambda x: True
+ client.transport = transport.SSHTransportBase()
+ client.transport.service = client
+ server.transport.sessionID = client.transport.sessionID = ''
+ # don't send key exchange packet
+ server.transport.sendKexInit = client.transport.sendKexInit = \
+ lambda: None
+
+ # set up server authentication
+ server.transport.factory = self.Factory()
+ server.passwordDelay = 0 # remove bad password delay
+ realm = Realm()
+ portal = Portal(realm)
+ checker = SSHProtocolChecker()
+ checker.registerChecker(PasswordChecker())
+ checker.registerChecker(PrivateKeyChecker())
+ checker.registerChecker(PAMChecker())
+ checker.areDone = lambda aId: (
+ len(checker.successfulCredentials[aId]) == 3)
+ portal.registerChecker(checker)
+ server.transport.factory.portal = portal
+
+ d = loopback.loopbackAsync(server.transport, client.transport)
+ server.transport.transport.logPrefix = lambda: '_ServerLoopback'
+ client.transport.transport.logPrefix = lambda: '_ClientLoopback'
+
+ server.serviceStarted()
+ client.serviceStarted()
+
+ def check(ignored):
+ self.assertEqual(server.transport.service.name, 'TestService')
+ return d.addCallback(check)
diff --git a/twisted/conch/test/test_window.py b/twisted/conch/test/test_window.py
new file mode 100644
index 0000000..6d7d9d2
--- /dev/null
+++ b/twisted/conch/test/test_window.py
@@ -0,0 +1,67 @@
+
+"""
+Tests for the insults windowing module, L{twisted.conch.insults.window}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.conch.insults.window import TopWindow, ScrolledArea, TextOutput
+
+
+class TopWindowTests(TestCase):
+ """
+ Tests for L{TopWindow}, the root window container class.
+ """
+
+ def test_paintScheduling(self):
+ """
+ Verify that L{TopWindow.repaint} schedules an actual paint to occur
+ using the scheduling object passed to its initializer.
+ """
+ paints = []
+ scheduled = []
+ root = TopWindow(lambda: paints.append(None), scheduled.append)
+
+ # Nothing should have happened yet.
+ self.assertEqual(paints, [])
+ self.assertEqual(scheduled, [])
+
+ # Cause a paint to be scheduled.
+ root.repaint()
+ self.assertEqual(paints, [])
+ self.assertEqual(len(scheduled), 1)
+
+ # Do another one to verify nothing else happens as long as the previous
+ # one is still pending.
+ root.repaint()
+ self.assertEqual(paints, [])
+ self.assertEqual(len(scheduled), 1)
+
+ # Run the actual paint call.
+ scheduled.pop()()
+ self.assertEqual(len(paints), 1)
+ self.assertEqual(scheduled, [])
+
+ # Do one more to verify that now that the previous one is finished
+ # future paints will succeed.
+ root.repaint()
+ self.assertEqual(len(paints), 1)
+ self.assertEqual(len(scheduled), 1)
+
+
+
+class ScrolledAreaTests(TestCase):
+ """
+ Tests for L{ScrolledArea}, a widget which creates a viewport containing
+ another widget and can reposition that viewport using scrollbars.
+ """
+ def test_parent(self):
+ """
+ The parent of the widget passed to L{ScrolledArea} is set to a new
+ L{Viewport} created by the L{ScrolledArea} which itself has the
+ L{ScrolledArea} instance as its parent.
+ """
+ widget = TextOutput()
+ scrolled = ScrolledArea(widget)
+ self.assertIdentical(widget.parent, scrolled._viewport)
+ self.assertIdentical(scrolled._viewport.parent, scrolled)
diff --git a/twisted/conch/topfiles/NEWS b/twisted/conch/topfiles/NEWS
new file mode 100644
index 0000000..8804a4c
--- /dev/null
+++ b/twisted/conch/topfiles/NEWS
@@ -0,0 +1,391 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Conch 12.1.0 (2012-06-02)
+=================================
+
+Features
+--------
+ - twisted.conch.tap now supports cred plugins (#4753)
+
+Bugfixes
+--------
+ - twisted.conch.client.knownhosts now handles errors encountered
+ parsing hashed entries in a known hosts file. (#5616)
+
+Improved Documentation
+----------------------
+ - Conch examples window.tac and telnet_echo.tac now have better
+ explanations. (#5590)
+
+Other
+-----
+ - #5580
+
+
+Twisted Conch 12.0.0 (2012-02-10)
+=================================
+
+Features
+--------
+ - use Python shadow module for authentication if it's available
+ (#3242)
+
+Bugfixes
+--------
+ - twisted.conch.ssh.transport.messages no longer ends with with old
+ message IDs on platforms with differing dict() orderings (#5352)
+
+Other
+-----
+ - #5225
+
+
+Twisted Conch 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.conch.ssh.filetransfer.FileTransferClient now handles short
+ status messages, not strictly allowed by the RFC, but sent by some
+ SSH implementations. (#3009)
+ - twisted.conch.manhole now supports CTRL-A and CTRL-E to trigger
+ HOME and END functions respectively. (#5252)
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout, the
+ bin/conch/ scripts will now use the version of Twisted they are
+ part of. (#3526)
+ - twisted.conch.insults.window.ScrolledArea now passes no extra
+ arguments to object.__init__ (which works on more versions of
+ Python). (#4197)
+ - twisted.conch.telnet.ITelnetProtocol now has the correct signature
+ for its unhandledSubnegotiation() method. (#4751)
+ - twisted.conch.ssh.userauth.SSHUserAuthClient now more closely
+ follows the RFC 4251 definition of boolean values when negotiating
+ for key-based authentication, allowing better interoperability with
+ other SSH implementations. (#5241)
+ - twisted.conch.recvline.RecvLine now ignores certain function keys
+ in its keystrokeReceived method instead of raising an exception.
+ (#5246)
+
+Deprecations and Removals
+-------------------------
+ - The --user option to `twistd manhole' has been removed as it was
+ dead code with no functionality associated with it. (#5283)
+
+Other
+-----
+ - #5107, #5256, #5349
+
+
+Twisted Conch 11.0.0 (2011-04-01)
+=================================
+
+Bugfixes
+--------
+ - The transport for subsystem protocols now declares that it
+ implements ITransport and implements the getHost and getPeer
+ methods. (#2453)
+ - twisted.conch.ssh.transport.SSHTransportBase now responds to key
+ exchange messages at any time during a connection (instead of only
+ at connection setup). It also queues non-key exchange messages
+ sent during key exchange to avoid corrupting the connection state.
+ (#4395)
+ - Importing twisted.conch.ssh.common no longer breaks pow(base, exp[,
+ modulus]) when the gmpy package is installed and base is not an
+ integer. (#4803)
+ - twisted.conch.ls.lsLine now returns a time string which does not
+ consider the locale. (#4937)
+
+Improved Documentation
+----------------------
+ - Changed the man page for ckeygen to accurately reflect what it
+ does, and corrected its synposis so that a second "ckeygen" is not
+ a required part of the ckeygen command line. (#4738)
+
+Other
+-----
+ - #2112
+
+
+Twisted Conch 10.2.0 (2010-11-29)
+=================================
+
+Bugfixes
+--------
+ - twisted.conch.ssh.factory.SSHFactory no longer disables coredumps.
+ (#2715)
+ - The Deferred returned by twisted.conch.telnet.TelnetTransport.will
+ now fires with an OptionRefused failure if the peer responds with a
+ refusal for the option negotiation. (#4231)
+ - SSHServerTransport and SSHClientTransport in
+ twisted.conch.ssh.transport no longer use PyCrypto to generate
+ random numbers for DH KEX. They also now generate values from the
+ full valid range, rather than only half of it. (#4469)
+ - twisted.conch.ssh.connection.SSHConnection now errbacks leftover
+ request deferreds on connection shutdown. (#4483)
+
+Other
+-----
+ - #4677
+
+
+Twisted Conch 10.1.0 (2010-06-27)
+=================================
+
+Features
+--------
+ - twisted.conch.ssh.transport.SSHTransportBase now allows supported
+ ssh protocol versions to be overriden. (#4428)
+
+Bugfixes
+--------
+ - SSHSessionProcessProtocol now doesn't close the session when stdin
+ is closed, but instead when both stdout and stderr are. (#4350)
+ - The 'cftp' command-line tool will no longer encounter an
+ intermittent error, crashing at startup with a ZeroDivisionError
+ while trying to report progress. (#4463)
+ - twisted.conch.ssh.connection.SSHConnection now replies to requests
+ to open an unknown channel with a OPEN_UNKNOWN_CHANNEL_TYPE message
+ instead of closing the connection. (#4490)
+
+Deprecations and Removals
+-------------------------
+ - twisted.conch.insults.client was deprecated. (#4095)
+ - twisted.conch.insults.colors has been deprecated. Please use
+ twisted.conch.insults.helper instead. (#4096)
+ - Removed twisted.conch.ssh.asn1, which has been deprecated since
+ Twisted 9.0. (#4097)
+ - Removed twisted.conch.ssh.common.Entropy, as Entropy.get_bytes has
+ been deprecated since 2007 and Entropy.get_bytes was the only
+ attribute of Entropy. (#4098)
+ - Removed twisted.conch.ssh.keys.getPublicKeyString, which has been
+ deprecated since 2007. Also updated the conch examples
+ sshsimpleserver.py and sshsimpleclient.py to reflect this removal.
+ (#4099)
+ - Removed twisted.conch.ssh.keys.makePublicKeyString, which has been
+ deprecated since 2007. (#4100)
+ - Removed twisted.conch.ssh.keys.getPublicKeyObject, which has been
+ deprecated since 2007. (#4101)
+ - Removed twisted.conch.ssh.keys.getPrivateKeyObject, which has been
+ deprecated since 2007. Also updated the conch examples to reflect
+ this removal. (#4102)
+ - Removed twisted.conch.ssh.keys.makePrivateKeyString, which has been
+ deprecated since 2007. (#4103)
+ - Removed twisted.conch.ssh.keys.makePublicKeyBlob, which has been
+ deprecated since 2007. (#4104)
+ - Removed twisted.conch.ssh.keys.signData,
+ twisted.conch.ssh.keys.verifySignature, and
+ twisted.conch.ssh.keys.printKey, which have been deprecated since
+ 2007. (#4105)
+
+Other
+-----
+ - #3849, #4408, #4454
+
+
+Twisted Conch 10.0.0 (2010-03-01)
+=================================
+
+Bugfixes
+--------
+ - twisted.conch.checkers.SSHPublicKeyDatabase now looks in the
+ correct user directory for authorized_keys files. (#3984)
+
+ - twisted.conch.ssh.SSHUserAuthClient now honors preferredOrder when
+ authenticating. (#4266)
+
+Other
+-----
+ - #2391, #4203, #4265
+
+
+Twisted Conch 9.0.0 (2009-11-24)
+================================
+
+Fixes
+-----
+ - The SSH key parser has been removed and conch now uses pyASN1 to parse keys.
+ This should fix a number of cases where parsing a key would fail, but it now
+ requires users to have pyASN1 installed (#3391)
+ - The time field on SFTP file listings should now be correct (#3503)
+ - The day field on SFTP file listings should now be correct on Windows (#3503)
+ - The "cftp" sftp client now truncates files it is uploading over (#2519)
+ - The telnet server protocol can now properly respond to subnegotiation
+ requests (#3655)
+ - Tests and factoring of the SSHv2 server implementation are now much better
+ (#2682)
+ - The SSHv2 server now sends "exit-signal" messages to the client, instead of
+ raising an exception, when a process dies due to a signal (#2687)
+ - cftp's client-side "exec" command now uses /bin/sh if the current user has
+ no shell (#3914)
+
+Deprecations and Removals
+-------------------------
+ - The buggy SSH connection sharing feature of the SSHv2 client was removed
+ (#3498)
+ - Use of strings and PyCrypto objects to represent keys is deprecated in favor
+ of using Conch Key objects (#2682)
+
+Other
+-----
+ - #3548, #3537, #3551, #3220, #3568, #3689, #3709, #3809, #2763, #3540, #3750,
+ #3897, #3813, #3871, #3916, #4047, #3940, #4050
+
+
+Conch 8.2.0 (2008-12-16)
+========================
+
+Features
+--------
+ - The type of the protocols instantiated by SSHFactory is now parameterized
+ (#3443)
+
+Fixes
+-----
+ - A file descriptor leak has been fixed (#3213, #1789)
+ - "File Already Exists" errors are now handled more correctly (#3033)
+ - Handling of CR IAC in TelnetClient is now improved (#3305)
+ - SSHAgent is no longer completely unusable (#3332)
+ - The performance of insults.ClientProtocol is now greatly increased by
+ delivering more than one byte at a time to application code (#3386)
+ - Manhole and the conch server no longer need to be run as root when not
+ necessary (#2607)
+ - The value of FILEXFER_ATTR_ACMODTIME has been corrected (#2902)
+ - The management of known_hosts and host key verification has been overhauled
+ (#1376, #1301, #3494, #3496, #1292, #3499)
+
+Other
+-----
+ - #3193, #1633
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+ - A regression was fixed whereby the publicKeys and privateKeys attributes of
+ SSHFactory would not be interpreted as strings (#3141)
+ - The sshsimpleserver.py example had a minor bug fix (#3135)
+ - The deprecated mktap API is no longer used (#3127)
+ - An infelicity was fixed whereby a NameError would be raised in certain
+ circumstances during authentication when a ConchError should have been
+ (#3154)
+ - A workaround was added to conch.insults for a bug in gnome-terminal whereby
+ it would not scroll correctly (#3189)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Features
+--------
+ - Add DEC private mode manipulation methods to ITerminalTransport. (#2403)
+
+Fixes
+-----
+ - Parameterize the scheduler function used by the insults TopWindow widget.
+ This change breaks backwards compatibility in the TopWindow initializer.
+ (#2413)
+ - Notify subsystems, like SFTP, of connection close. (#2421)
+ - Change the process file descriptor "connection lost" code to reverse the
+ setNonBlocking operation done during initialization. (#2371)
+ - Change ConsoleManhole to wait for connectionLost notification before
+ stopping the reactor. (#2123, #2371)
+ - Make SSHUserAuthServer.ssh_USERAUTH_REQUEST return a Deferred. (#2528)
+ - Manhole's initializer calls its parent class's initializer with its
+ namespace argument. (#2587)
+ - Handle ^C during input line continuation in manhole by updating the prompt
+ and line buffer correctly. (#2663)
+ - Make twisted.conch.telnet.Telnet by default reject all attempts to enable
+ options. (#1967)
+ - Reduce the number of calls into application code to deliver application-level
+ data in twisted.conch.telnet.Telnet.dataReceived (#2107)
+ - Fix definition and management of extended attributes in conch file transfer.
+ (#3010)
+ - Fix parsing of OpenSSH-generated RSA keys with differing ASN.1 packing style.
+ (#3008)
+ - Fix handling of missing $HOME in twisted.conch.client.unix. (#3061)
+
+Misc
+----
+ - #2267, #2378, #2604, #2707, #2341, #2685, #2679, #2912, #2977, #2678, #2709
+ #2063, #2847
+
+
+0.8.0 (2007-01-06)
+==================
+
+Features
+--------
+ - Manhole now supports Ctrl-l to emulate the same behavior in the
+ Python interactive interpreter (#1565)
+ - Python 2.5 is now supported (#1867)
+
+Misc
+----
+ - #1673, #1636, #1892, #1943, #2057, #1180, #1185, #2148, #2159, #2291,
+
+Deprecations and Removals
+-------------------------
+
+ - The old twisted.cred API (Identities, Authorizers, etc) is no
+ longer supported (#1440)
+
+
+0.7.0 (2006-05-21)
+==================
+
+Features
+--------
+ - Timeout support for ExpectableBuffer.expect()
+
+Fixes
+-----
+ - ~5x speedup for bulk data transfer (#1325)
+ - Misc: #1428
+
+0.6.0:
+
+ Bugfixes and improvements in SSH support and Insults:
+ - PAM authenticator support factored out into twisted.cred
+ - Poorly supported next-line terminal operation replaced with simple \r\n
+
+ New functionality:
+ - An ITerminalTransport implementation with expect-like features
+ - Support for the "none" SSH cipher
+ - Insults support for handling more keystrokes and more methods for
+ terminal manipulation
+ - New, simple insults-based widget library added
+
+ Better test coverage:
+ - Dependence on `localhost' name removed
+ - Some timing-sensitive tests changed to be more reliable
+ - Process spawning tests initialize environment more robustly
+
+0.5.0:
+
+ Many improvements to SSH support. Here's some in particular:
+ - Add --reconnect option to conch binary
+ - utmp/wtmp logging
+ - Unix login improvements, PAM support
+ - Add "cftp" -- Conch SFTP.
+ - Deferred retrieval of public keys is supported
+ - PAM support for client and server
+ - Bugfixes:
+ - fix conch failing to exit, and hangs.
+ - Remote->Local forwarding
+ - Channel closing
+ - Invalid known_host writing
+ - Many others
+
+ New functionality:
+ - twisted.conch.telnet: new, much improved telnet implementation.
+ - twisted.conch.insults: Basic curses-like terminal support (server-side).
+ - twisted.conch.manhole: new interactive python interactive interpreter,
+ can be used with conch's telnet, ssh, or on the console.
+ - Main features: Syntax coloring, line editing, and useful interactive
+ handling of Deferreds.
diff --git a/twisted/conch/topfiles/README b/twisted/conch/topfiles/README
new file mode 100644
index 0000000..6a0fe79
--- /dev/null
+++ b/twisted/conch/topfiles/README
@@ -0,0 +1,11 @@
+Twisted Conch 12.1.0
+
+Twisted Conch depends on Twisted Core and on Python Crypto extensions
+(<http://www.pycrypto.org>).
+
+The pyasn1 module (<http://pyasn1.sourceforge.net/>) is also required.
+
+gmpy (<http://code.google.com/p/gmpy/>) is strongly recommended to improve
+performance.
+
+Twisted Conch includes a couple simple GUI applications which depend on Tkinter.
diff --git a/twisted/conch/topfiles/setup.py b/twisted/conch/topfiles/setup.py
new file mode 100644
index 0000000..19b9496
--- /dev/null
+++ b/twisted/conch/topfiles/setup.py
@@ -0,0 +1,48 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ if sys.version_info[:2] >= (2, 4):
+ extraMeta = dict(
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Environment :: No Input/Output (Daemon)",
+ "Intended Audience :: Developers",
+ "Intended Audience :: End Users/Desktop",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Internet",
+ "Topic :: Security",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Terminals",
+ ])
+ else:
+ extraMeta = {}
+
+ dist.setup(
+ twisted_subproject="conch",
+ scripts=dist.getScripts("conch"),
+ # metadata
+ name="Twisted Conch",
+ description="Twisted SSHv2 implementation.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Paul Swartz",
+ url="http://twistedmatrix.com/trac/wiki/TwistedConch",
+ license="MIT",
+ long_description="""\
+Conch is an SSHv2 implementation using the Twisted framework. It
+includes a server, client, a SFTP client, and a key generator.
+""",
+ **extraMeta)
diff --git a/twisted/conch/ttymodes.py b/twisted/conch/ttymodes.py
new file mode 100644
index 0000000..00b4495
--- /dev/null
+++ b/twisted/conch/ttymodes.py
@@ -0,0 +1,121 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+import tty
+# this module was autogenerated.
+
+VINTR = 1
+VQUIT = 2
+VERASE = 3
+VKILL = 4
+VEOF = 5
+VEOL = 6
+VEOL2 = 7
+VSTART = 8
+VSTOP = 9
+VSUSP = 10
+VDSUSP = 11
+VREPRINT = 12
+VWERASE = 13
+VLNEXT = 14
+VFLUSH = 15
+VSWTCH = 16
+VSTATUS = 17
+VDISCARD = 18
+IGNPAR = 30
+PARMRK = 31
+INPCK = 32
+ISTRIP = 33
+INLCR = 34
+IGNCR = 35
+ICRNL = 36
+IUCLC = 37
+IXON = 38
+IXANY = 39
+IXOFF = 40
+IMAXBEL = 41
+ISIG = 50
+ICANON = 51
+XCASE = 52
+ECHO = 53
+ECHOE = 54
+ECHOK = 55
+ECHONL = 56
+NOFLSH = 57
+TOSTOP = 58
+IEXTEN = 59
+ECHOCTL = 60
+ECHOKE = 61
+PENDIN = 62
+OPOST = 70
+OLCUC = 71
+ONLCR = 72
+OCRNL = 73
+ONOCR = 74
+ONLRET = 75
+CS7 = 90
+CS8 = 91
+PARENB = 92
+PARODD = 93
+TTY_OP_ISPEED = 128
+TTY_OP_OSPEED = 129
+
+TTYMODES = {
+ 1 : 'VINTR',
+ 2 : 'VQUIT',
+ 3 : 'VERASE',
+ 4 : 'VKILL',
+ 5 : 'VEOF',
+ 6 : 'VEOL',
+ 7 : 'VEOL2',
+ 8 : 'VSTART',
+ 9 : 'VSTOP',
+ 10 : 'VSUSP',
+ 11 : 'VDSUSP',
+ 12 : 'VREPRINT',
+ 13 : 'VWERASE',
+ 14 : 'VLNEXT',
+ 15 : 'VFLUSH',
+ 16 : 'VSWTCH',
+ 17 : 'VSTATUS',
+ 18 : 'VDISCARD',
+ 30 : (tty.IFLAG, 'IGNPAR'),
+ 31 : (tty.IFLAG, 'PARMRK'),
+ 32 : (tty.IFLAG, 'INPCK'),
+ 33 : (tty.IFLAG, 'ISTRIP'),
+ 34 : (tty.IFLAG, 'INLCR'),
+ 35 : (tty.IFLAG, 'IGNCR'),
+ 36 : (tty.IFLAG, 'ICRNL'),
+ 37 : (tty.IFLAG, 'IUCLC'),
+ 38 : (tty.IFLAG, 'IXON'),
+ 39 : (tty.IFLAG, 'IXANY'),
+ 40 : (tty.IFLAG, 'IXOFF'),
+ 41 : (tty.IFLAG, 'IMAXBEL'),
+ 50 : (tty.LFLAG, 'ISIG'),
+ 51 : (tty.LFLAG, 'ICANON'),
+ 52 : (tty.LFLAG, 'XCASE'),
+ 53 : (tty.LFLAG, 'ECHO'),
+ 54 : (tty.LFLAG, 'ECHOE'),
+ 55 : (tty.LFLAG, 'ECHOK'),
+ 56 : (tty.LFLAG, 'ECHONL'),
+ 57 : (tty.LFLAG, 'NOFLSH'),
+ 58 : (tty.LFLAG, 'TOSTOP'),
+ 59 : (tty.LFLAG, 'IEXTEN'),
+ 60 : (tty.LFLAG, 'ECHOCTL'),
+ 61 : (tty.LFLAG, 'ECHOKE'),
+ 62 : (tty.LFLAG, 'PENDIN'),
+ 70 : (tty.OFLAG, 'OPOST'),
+ 71 : (tty.OFLAG, 'OLCUC'),
+ 72 : (tty.OFLAG, 'ONLCR'),
+ 73 : (tty.OFLAG, 'OCRNL'),
+ 74 : (tty.OFLAG, 'ONOCR'),
+ 75 : (tty.OFLAG, 'ONLRET'),
+# 90 : (tty.CFLAG, 'CS7'),
+# 91 : (tty.CFLAG, 'CS8'),
+ 92 : (tty.CFLAG, 'PARENB'),
+ 93 : (tty.CFLAG, 'PARODD'),
+ 128 : 'ISPEED',
+ 129 : 'OSPEED'
+}
diff --git a/twisted/conch/ui/__init__.py b/twisted/conch/ui/__init__.py
new file mode 100755
index 0000000..ea0eea8
--- /dev/null
+++ b/twisted/conch/ui/__init__.py
@@ -0,0 +1,11 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+
+"""
+twisted.conch.ui is home to the UI elements for tkconch.
+
+Maintainer: Paul Swartz
+"""
diff --git a/twisted/conch/ui/ansi.py b/twisted/conch/ui/ansi.py
new file mode 100644
index 0000000..9d5e616
--- /dev/null
+++ b/twisted/conch/ui/ansi.py
@@ -0,0 +1,240 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""Module to parse ANSI escape sequences
+
+Maintainer: Jean-Paul Calderone
+"""
+
+import string
+
+# Twisted imports
+from twisted.python import log
+
+class ColorText:
+ """
+ Represents an element of text along with the texts colors and
+ additional attributes.
+ """
+
+ # The colors to use
+ COLORS = ('b', 'r', 'g', 'y', 'l', 'm', 'c', 'w')
+ BOLD_COLORS = tuple([x.upper() for x in COLORS])
+ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(len(COLORS))
+
+ # Color names
+ COLOR_NAMES = (
+ 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White'
+ )
+
+ def __init__(self, text, fg, bg, display, bold, underline, flash, reverse):
+ self.text, self.fg, self.bg = text, fg, bg
+ self.display = display
+ self.bold = bold
+ self.underline = underline
+ self.flash = flash
+ self.reverse = reverse
+ if self.reverse:
+ self.fg, self.bg = self.bg, self.fg
+
+
+class AnsiParser:
+ """
+ Parser class for ANSI codes.
+ """
+
+ # Terminators for cursor movement ansi controls - unsupported
+ CURSOR_SET = ('H', 'f', 'A', 'B', 'C', 'D', 'R', 's', 'u', 'd','G')
+
+ # Terminators for erasure ansi controls - unsupported
+ ERASE_SET = ('J', 'K', 'P')
+
+ # Terminators for mode change ansi controls - unsupported
+ MODE_SET = ('h', 'l')
+
+ # Terminators for keyboard assignment ansi controls - unsupported
+ ASSIGN_SET = ('p',)
+
+ # Terminators for color change ansi controls - supported
+ COLOR_SET = ('m',)
+
+ SETS = (CURSOR_SET, ERASE_SET, MODE_SET, ASSIGN_SET, COLOR_SET)
+
+ def __init__(self, defaultFG, defaultBG):
+ self.defaultFG, self.defaultBG = defaultFG, defaultBG
+ self.currentFG, self.currentBG = self.defaultFG, self.defaultBG
+ self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0
+ self.display = 1
+ self.prepend = ''
+
+
+ def stripEscapes(self, string):
+ """
+ Remove all ANSI color escapes from the given string.
+ """
+ result = ''
+ show = 1
+ i = 0
+ L = len(string)
+ while i < L:
+ if show == 0 and string[i] in _sets:
+ show = 1
+ elif show:
+ n = string.find('\x1B', i)
+ if n == -1:
+ return result + string[i:]
+ else:
+ result = result + string[i:n]
+ i = n
+ show = 0
+ i = i + 1
+ return result
+
+ def writeString(self, colorstr):
+ pass
+
+ def parseString(self, str):
+ """
+ Turn a string input into a list of L{ColorText} elements.
+ """
+
+ if self.prepend:
+ str = self.prepend + str
+ self.prepend = ''
+ parts = str.split('\x1B')
+
+ if len(parts) == 1:
+ self.writeString(self.formatText(parts[0]))
+ else:
+ self.writeString(self.formatText(parts[0]))
+ for s in parts[1:]:
+ L = len(s)
+ i = 0
+ type = None
+ while i < L:
+ if s[i] not in string.digits+'[;?':
+ break
+ i+=1
+ if not s:
+ self.prepend = '\x1b'
+ return
+ if s[0]!='[':
+ self.writeString(self.formatText(s[i+1:]))
+ continue
+ else:
+ s=s[1:]
+ i-=1
+ if i==L-1:
+ self.prepend = '\x1b['
+ return
+ type = _setmap.get(s[i], None)
+ if type is None:
+ continue
+
+ if type == AnsiParser.COLOR_SET:
+ self.parseColor(s[:i + 1])
+ s = s[i + 1:]
+ self.writeString(self.formatText(s))
+ elif type == AnsiParser.CURSOR_SET:
+ cursor, s = s[:i+1], s[i+1:]
+ self.parseCursor(cursor)
+ self.writeString(self.formatText(s))
+ elif type == AnsiParser.ERASE_SET:
+ erase, s = s[:i+1], s[i+1:]
+ self.parseErase(erase)
+ self.writeString(self.formatText(s))
+ elif type == AnsiParser.MODE_SET:
+ mode, s = s[:i+1], s[i+1:]
+ #self.parseErase('2J')
+ self.writeString(self.formatText(s))
+ elif i == L:
+ self.prepend = '\x1B[' + s
+ else:
+ log.msg('Unhandled ANSI control type: %c' % (s[i],))
+ s = s[i + 1:]
+ self.writeString(self.formatText(s))
+
+ def parseColor(self, str):
+ """
+ Handle a single ANSI color sequence
+ """
+ # Drop the trailing 'm'
+ str = str[:-1]
+
+ if not str:
+ str = '0'
+
+ try:
+ parts = map(int, str.split(';'))
+ except ValueError:
+ log.msg('Invalid ANSI color sequence (%d): %s' % (len(str), str))
+ self.currentFG, self.currentBG = self.defaultFG, self.defaultBG
+ return
+
+ for x in parts:
+ if x == 0:
+ self.currentFG, self.currentBG = self.defaultFG, self.defaultBG
+ self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0
+ self.display = 1
+ elif x == 1:
+ self.bold = 1
+ elif 30 <= x <= 37:
+ self.currentFG = x - 30
+ elif 40 <= x <= 47:
+ self.currentBG = x - 40
+ elif x == 39:
+ self.currentFG = self.defaultFG
+ elif x == 49:
+ self.currentBG = self.defaultBG
+ elif x == 4:
+ self.underline = 1
+ elif x == 5:
+ self.flash = 1
+ elif x == 7:
+ self.reverse = 1
+ elif x == 8:
+ self.display = 0
+ elif x == 22:
+ self.bold = 0
+ elif x == 24:
+ self.underline = 0
+ elif x == 25:
+ self.blink = 0
+ elif x == 27:
+ self.reverse = 0
+ elif x == 28:
+ self.display = 1
+ else:
+ log.msg('Unrecognised ANSI color command: %d' % (x,))
+
+ def parseCursor(self, cursor):
+ pass
+
+ def parseErase(self, erase):
+ pass
+
+
+ def pickColor(self, value, mode, BOLD = ColorText.BOLD_COLORS):
+ if mode:
+ return ColorText.COLORS[value]
+ else:
+ return self.bold and BOLD[value] or ColorText.COLORS[value]
+
+
+ def formatText(self, text):
+ return ColorText(
+ text,
+ self.pickColor(self.currentFG, 0),
+ self.pickColor(self.currentBG, 1),
+ self.display, self.bold, self.underline, self.flash, self.reverse
+ )
+
+
+_sets = ''.join(map(''.join, AnsiParser.SETS))
+
+_setmap = {}
+for s in AnsiParser.SETS:
+ for r in s:
+ _setmap[r] = s
+del s
diff --git a/twisted/conch/ui/tkvt100.py b/twisted/conch/ui/tkvt100.py
new file mode 100644
index 0000000..cd7581d
--- /dev/null
+++ b/twisted/conch/ui/tkvt100.py
@@ -0,0 +1,197 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""Module to emulate a VT100 terminal in Tkinter.
+
+Maintainer: Paul Swartz
+"""
+
+import Tkinter, tkFont
+import ansi
+import string
+
+ttyFont = None#tkFont.Font(family = 'Courier', size = 10)
+fontWidth, fontHeight = None,None#max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace'])
+
+colorKeys = (
+ 'b', 'r', 'g', 'y', 'l', 'm', 'c', 'w',
+ 'B', 'R', 'G', 'Y', 'L', 'M', 'C', 'W'
+)
+
+colorMap = {
+ 'b': '#000000', 'r': '#c40000', 'g': '#00c400', 'y': '#c4c400',
+ 'l': '#000080', 'm': '#c400c4', 'c': '#00c4c4', 'w': '#c4c4c4',
+ 'B': '#626262', 'R': '#ff0000', 'G': '#00ff00', 'Y': '#ffff00',
+ 'L': '#0000ff', 'M': '#ff00ff', 'C': '#00ffff', 'W': '#ffffff',
+}
+
+class VT100Frame(Tkinter.Frame):
+ def __init__(self, *args, **kw):
+ global ttyFont, fontHeight, fontWidth
+ ttyFont = tkFont.Font(family = 'Courier', size = 10)
+ fontWidth, fontHeight = max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace'])
+ self.width = kw.get('width', 80)
+ self.height = kw.get('height', 25)
+ self.callback = kw['callback']
+ del kw['callback']
+ kw['width'] = w = fontWidth * self.width
+ kw['height'] = h = fontHeight * self.height
+ Tkinter.Frame.__init__(self, *args, **kw)
+ self.canvas = Tkinter.Canvas(bg='#000000', width=w, height=h)
+ self.canvas.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
+ self.canvas.bind('<Key>', self.keyPressed)
+ self.canvas.bind('<1>', lambda x: 'break')
+ self.canvas.bind('<Up>', self.upPressed)
+ self.canvas.bind('<Down>', self.downPressed)
+ self.canvas.bind('<Left>', self.leftPressed)
+ self.canvas.bind('<Right>', self.rightPressed)
+ self.canvas.focus()
+
+ self.ansiParser = ansi.AnsiParser(ansi.ColorText.WHITE, ansi.ColorText.BLACK)
+ self.ansiParser.writeString = self.writeString
+ self.ansiParser.parseCursor = self.parseCursor
+ self.ansiParser.parseErase = self.parseErase
+ #for (a, b) in colorMap.items():
+ # self.canvas.tag_config(a, foreground=b)
+ # self.canvas.tag_config('b'+a, background=b)
+ #self.canvas.tag_config('underline', underline=1)
+
+ self.x = 0
+ self.y = 0
+ self.cursor = self.canvas.create_rectangle(0,0,fontWidth-1,fontHeight-1,fill='green',outline='green')
+
+ def _delete(self, sx, sy, ex, ey):
+ csx = sx*fontWidth + 1
+ csy = sy*fontHeight + 1
+ cex = ex*fontWidth + 3
+ cey = ey*fontHeight + 3
+ items = self.canvas.find_overlapping(csx,csy, cex,cey)
+ for item in items:
+ self.canvas.delete(item)
+
+ def _write(self, ch, fg, bg):
+ if self.x == self.width:
+ self.x = 0
+ self.y+=1
+ if self.y == self.height:
+ [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()]
+ self.y-=1
+ canvasX = self.x*fontWidth + 1
+ canvasY = self.y*fontHeight + 1
+ items = self.canvas.find_overlapping(canvasX, canvasY, canvasX+2, canvasY+2)
+ if items:
+ [self.canvas.delete(item) for item in items]
+ if bg:
+ self.canvas.create_rectangle(canvasX, canvasY, canvasX+fontWidth-1, canvasY+fontHeight-1, fill=bg, outline=bg)
+ self.canvas.create_text(canvasX, canvasY, anchor=Tkinter.NW, font=ttyFont, text=ch, fill=fg)
+ self.x+=1
+
+ def write(self, data):
+ #print self.x,self.y,repr(data)
+ #if len(data)>5: raw_input()
+ self.ansiParser.parseString(data)
+ self.canvas.delete(self.cursor)
+ canvasX = self.x*fontWidth + 1
+ canvasY = self.y*fontHeight + 1
+ self.cursor = self.canvas.create_rectangle(canvasX,canvasY,canvasX+fontWidth-1,canvasY+fontHeight-1, fill='green', outline='green')
+ self.canvas.lower(self.cursor)
+
+ def writeString(self, i):
+ if not i.display:
+ return
+ fg = colorMap[i.fg]
+ bg = i.bg != 'b' and colorMap[i.bg]
+ for ch in i.text:
+ b = ord(ch)
+ if b == 7: # bell
+ self.bell()
+ elif b == 8: # BS
+ if self.x:
+ self.x-=1
+ elif b == 9: # TAB
+ [self._write(' ',fg,bg) for i in range(8)]
+ elif b == 10:
+ if self.y == self.height-1:
+ self._delete(0,0,self.width,0)
+ [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()]
+ else:
+ self.y+=1
+ elif b == 13:
+ self.x = 0
+ elif 32 <= b < 127:
+ self._write(ch, fg, bg)
+
+ def parseErase(self, erase):
+ if ';' in erase:
+ end = erase[-1]
+ parts = erase[:-1].split(';')
+ [self.parseErase(x+end) for x in parts]
+ return
+ start = 0
+ x,y = self.x, self.y
+ if len(erase) > 1:
+ start = int(erase[:-1])
+ if erase[-1] == 'J':
+ if start == 0:
+ self._delete(x,y,self.width,self.height)
+ else:
+ self._delete(0,0,self.width,self.height)
+ self.x = 0
+ self.y = 0
+ elif erase[-1] == 'K':
+ if start == 0:
+ self._delete(x,y,self.width,y)
+ elif start == 1:
+ self._delete(0,y,x,y)
+ self.x = 0
+ else:
+ self._delete(0,y,self.width,y)
+ self.x = 0
+ elif erase[-1] == 'P':
+ self._delete(x,y,x+start,y)
+
+ def parseCursor(self, cursor):
+ #if ';' in cursor and cursor[-1]!='H':
+ # end = cursor[-1]
+ # parts = cursor[:-1].split(';')
+ # [self.parseCursor(x+end) for x in parts]
+ # return
+ start = 1
+ if len(cursor) > 1 and cursor[-1]!='H':
+ start = int(cursor[:-1])
+ if cursor[-1] == 'C':
+ self.x+=start
+ elif cursor[-1] == 'D':
+ self.x-=start
+ elif cursor[-1]=='d':
+ self.y=start-1
+ elif cursor[-1]=='G':
+ self.x=start-1
+ elif cursor[-1]=='H':
+ if len(cursor)>1:
+ y,x = map(int, cursor[:-1].split(';'))
+ y-=1
+ x-=1
+ else:
+ x,y=0,0
+ self.x = x
+ self.y = y
+
+ def keyPressed(self, event):
+ if self.callback and event.char:
+ self.callback(event.char)
+ return 'break'
+
+ def upPressed(self, event):
+ self.callback('\x1bOA')
+
+ def downPressed(self, event):
+ self.callback('\x1bOB')
+
+ def rightPressed(self, event):
+ self.callback('\x1bOC')
+
+ def leftPressed(self, event):
+ self.callback('\x1bOD')
diff --git a/twisted/conch/unix.py b/twisted/conch/unix.py
new file mode 100644
index 0000000..ffeb7ad
--- /dev/null
+++ b/twisted/conch/unix.py
@@ -0,0 +1,457 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.cred import portal
+from twisted.python import components, log
+from twisted.internet.error import ProcessExitedAlready
+from zope import interface
+from ssh import session, forwarding, filetransfer
+from ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
+from twisted.conch.ls import lsLine
+
+from avatar import ConchUser
+from error import ConchError
+from interfaces import ISession, ISFTPServer, ISFTPFile
+
+import struct, os, time, socket
+import fcntl, tty
+import pwd, grp
+import pty
+import ttymodes
+
+try:
+ import utmp
+except ImportError:
+ utmp = None
+
+class UnixSSHRealm:
+ interface.implements(portal.IRealm)
+
+ def requestAvatar(self, username, mind, *interfaces):
+ user = UnixConchUser(username)
+ return interfaces[0], user, user.logout
+
+
+class UnixConchUser(ConchUser):
+
+ def __init__(self, username):
+ ConchUser.__init__(self)
+ self.username = username
+ self.pwdData = pwd.getpwnam(self.username)
+ l = [self.pwdData[3]]
+ for groupname, password, gid, userlist in grp.getgrall():
+ if username in userlist:
+ l.append(gid)
+ self.otherGroups = l
+ self.listeners = {} # dict mapping (interface, port) -> listener
+ self.channelLookup.update(
+ {"session": session.SSHSession,
+ "direct-tcpip": forwarding.openConnectForwardingClient})
+
+ self.subsystemLookup.update(
+ {"sftp": filetransfer.FileTransferServer})
+
+ def getUserGroupId(self):
+ return self.pwdData[2:4]
+
+ def getOtherGroups(self):
+ return self.otherGroups
+
+ def getHomeDir(self):
+ return self.pwdData[5]
+
+ def getShell(self):
+ return self.pwdData[6]
+
+ def global_tcpip_forward(self, data):
+ hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
+ from twisted.internet import reactor
+ try: listener = self._runAsUser(
+ reactor.listenTCP, portToBind,
+ forwarding.SSHListenForwardingFactory(self.conn,
+ (hostToBind, portToBind),
+ forwarding.SSHListenServerForwardingChannel),
+ interface = hostToBind)
+ except:
+ return 0
+ else:
+ self.listeners[(hostToBind, portToBind)] = listener
+ if portToBind == 0:
+ portToBind = listener.getHost()[2] # the port
+ return 1, struct.pack('>L', portToBind)
+ else:
+ return 1
+
+ def global_cancel_tcpip_forward(self, data):
+ hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data)
+ listener = self.listeners.get((hostToBind, portToBind), None)
+ if not listener:
+ return 0
+ del self.listeners[(hostToBind, portToBind)]
+ self._runAsUser(listener.stopListening)
+ return 1
+
+ def logout(self):
+ # remove all listeners
+ for listener in self.listeners.itervalues():
+ self._runAsUser(listener.stopListening)
+ log.msg('avatar %s logging out (%i)' % (self.username, len(self.listeners)))
+
+ def _runAsUser(self, f, *args, **kw):
+ euid = os.geteuid()
+ egid = os.getegid()
+ groups = os.getgroups()
+ uid, gid = self.getUserGroupId()
+ os.setegid(0)
+ os.seteuid(0)
+ os.setgroups(self.getOtherGroups())
+ os.setegid(gid)
+ os.seteuid(uid)
+ try:
+ f = iter(f)
+ except TypeError:
+ f = [(f, args, kw)]
+ try:
+ for i in f:
+ func = i[0]
+ args = len(i)>1 and i[1] or ()
+ kw = len(i)>2 and i[2] or {}
+ r = func(*args, **kw)
+ finally:
+ os.setegid(0)
+ os.seteuid(0)
+ os.setgroups(groups)
+ os.setegid(egid)
+ os.seteuid(euid)
+ return r
+
+class SSHSessionForUnixConchUser:
+
+ interface.implements(ISession)
+
+ def __init__(self, avatar):
+ self.avatar = avatar
+ self. environ = {'PATH':'/bin:/usr/bin:/usr/local/bin'}
+ self.pty = None
+ self.ptyTuple = 0
+
+ def addUTMPEntry(self, loggedIn=1):
+ if not utmp:
+ return
+ ipAddress = self.avatar.conn.transport.transport.getPeer().host
+ packedIp ,= struct.unpack('L', socket.inet_aton(ipAddress))
+ ttyName = self.ptyTuple[2][5:]
+ t = time.time()
+ t1 = int(t)
+ t2 = int((t-t1) * 1e6)
+ entry = utmp.UtmpEntry()
+ entry.ut_type = loggedIn and utmp.USER_PROCESS or utmp.DEAD_PROCESS
+ entry.ut_pid = self.pty.pid
+ entry.ut_line = ttyName
+ entry.ut_id = ttyName[-4:]
+ entry.ut_tv = (t1,t2)
+ if loggedIn:
+ entry.ut_user = self.avatar.username
+ entry.ut_host = socket.gethostbyaddr(ipAddress)[0]
+ entry.ut_addr_v6 = (packedIp, 0, 0, 0)
+ a = utmp.UtmpRecord(utmp.UTMP_FILE)
+ a.pututline(entry)
+ a.endutent()
+ b = utmp.UtmpRecord(utmp.WTMP_FILE)
+ b.pututline(entry)
+ b.endutent()
+
+
+ def getPty(self, term, windowSize, modes):
+ self.environ['TERM'] = term
+ self.winSize = windowSize
+ self.modes = modes
+ master, slave = pty.openpty()
+ ttyname = os.ttyname(slave)
+ self.environ['SSH_TTY'] = ttyname
+ self.ptyTuple = (master, slave, ttyname)
+
+ def openShell(self, proto):
+ from twisted.internet import reactor
+ if not self.ptyTuple: # we didn't get a pty-req
+ log.msg('tried to get shell without pty, failing')
+ raise ConchError("no pty")
+ uid, gid = self.avatar.getUserGroupId()
+ homeDir = self.avatar.getHomeDir()
+ shell = self.avatar.getShell()
+ self.environ['USER'] = self.avatar.username
+ self.environ['HOME'] = homeDir
+ self.environ['SHELL'] = shell
+ shellExec = os.path.basename(shell)
+ peer = self.avatar.conn.transport.transport.getPeer()
+ host = self.avatar.conn.transport.transport.getHost()
+ self.environ['SSH_CLIENT'] = '%s %s %s' % (peer.host, peer.port, host.port)
+ self.getPtyOwnership()
+ self.pty = reactor.spawnProcess(proto, \
+ shell, ['-%s' % shellExec], self.environ, homeDir, uid, gid,
+ usePTY = self.ptyTuple)
+ self.addUTMPEntry()
+ fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
+ struct.pack('4H', *self.winSize))
+ if self.modes:
+ self.setModes()
+ self.oldWrite = proto.transport.write
+ proto.transport.write = self._writeHack
+ self.avatar.conn.transport.transport.setTcpNoDelay(1)
+
+ def execCommand(self, proto, cmd):
+ from twisted.internet import reactor
+ uid, gid = self.avatar.getUserGroupId()
+ homeDir = self.avatar.getHomeDir()
+ shell = self.avatar.getShell() or '/bin/sh'
+ command = (shell, '-c', cmd)
+ peer = self.avatar.conn.transport.transport.getPeer()
+ host = self.avatar.conn.transport.transport.getHost()
+ self.environ['SSH_CLIENT'] = '%s %s %s' % (peer.host, peer.port, host.port)
+ if self.ptyTuple:
+ self.getPtyOwnership()
+ self.pty = reactor.spawnProcess(proto, \
+ shell, command, self.environ, homeDir,
+ uid, gid, usePTY = self.ptyTuple or 0)
+ if self.ptyTuple:
+ self.addUTMPEntry()
+ if self.modes:
+ self.setModes()
+# else:
+# tty.setraw(self.pty.pipes[0].fileno(), tty.TCSANOW)
+ self.avatar.conn.transport.transport.setTcpNoDelay(1)
+
+ def getPtyOwnership(self):
+ ttyGid = os.stat(self.ptyTuple[2])[5]
+ uid, gid = self.avatar.getUserGroupId()
+ euid, egid = os.geteuid(), os.getegid()
+ os.setegid(0)
+ os.seteuid(0)
+ try:
+ os.chown(self.ptyTuple[2], uid, ttyGid)
+ finally:
+ os.setegid(egid)
+ os.seteuid(euid)
+
+ def setModes(self):
+ pty = self.pty
+ attr = tty.tcgetattr(pty.fileno())
+ for mode, modeValue in self.modes:
+ if not ttymodes.TTYMODES.has_key(mode): continue
+ ttyMode = ttymodes.TTYMODES[mode]
+ if len(ttyMode) == 2: # flag
+ flag, ttyAttr = ttyMode
+ if not hasattr(tty, ttyAttr): continue
+ ttyval = getattr(tty, ttyAttr)
+ if modeValue:
+ attr[flag] = attr[flag]|ttyval
+ else:
+ attr[flag] = attr[flag]&~ttyval
+ elif ttyMode == 'OSPEED':
+ attr[tty.OSPEED] = getattr(tty, 'B%s'%modeValue)
+ elif ttyMode == 'ISPEED':
+ attr[tty.ISPEED] = getattr(tty, 'B%s'%modeValue)
+ else:
+ if not hasattr(tty, ttyMode): continue
+ ttyval = getattr(tty, ttyMode)
+ attr[tty.CC][ttyval] = chr(modeValue)
+ tty.tcsetattr(pty.fileno(), tty.TCSANOW, attr)
+
+ def eofReceived(self):
+ if self.pty:
+ self.pty.closeStdin()
+
+ def closed(self):
+ if self.ptyTuple and os.path.exists(self.ptyTuple[2]):
+ ttyGID = os.stat(self.ptyTuple[2])[5]
+ os.chown(self.ptyTuple[2], 0, ttyGID)
+ if self.pty:
+ try:
+ self.pty.signalProcess('HUP')
+ except (OSError,ProcessExitedAlready):
+ pass
+ self.pty.loseConnection()
+ self.addUTMPEntry(0)
+ log.msg('shell closed')
+
+ def windowChanged(self, winSize):
+ self.winSize = winSize
+ fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ,
+ struct.pack('4H', *self.winSize))
+
+ def _writeHack(self, data):
+ """
+ Hack to send ignore messages when we aren't echoing.
+ """
+ if self.pty is not None:
+ attr = tty.tcgetattr(self.pty.fileno())[3]
+ if not attr & tty.ECHO and attr & tty.ICANON: # no echo
+ self.avatar.conn.transport.sendIgnore('\x00'*(8+len(data)))
+ self.oldWrite(data)
+
+
+class SFTPServerForUnixConchUser:
+
+ interface.implements(ISFTPServer)
+
+ def __init__(self, avatar):
+ self.avatar = avatar
+
+
+ def _setAttrs(self, path, attrs):
+ """
+ NOTE: this function assumes it runs as the logged-in user:
+ i.e. under _runAsUser()
+ """
+ if attrs.has_key("uid") and attrs.has_key("gid"):
+ os.chown(path, attrs["uid"], attrs["gid"])
+ if attrs.has_key("permissions"):
+ os.chmod(path, attrs["permissions"])
+ if attrs.has_key("atime") and attrs.has_key("mtime"):
+ os.utime(path, (attrs["atime"], attrs["mtime"]))
+
+ def _getAttrs(self, s):
+ return {
+ "size" : s.st_size,
+ "uid" : s.st_uid,
+ "gid" : s.st_gid,
+ "permissions" : s.st_mode,
+ "atime" : int(s.st_atime),
+ "mtime" : int(s.st_mtime)
+ }
+
+ def _absPath(self, path):
+ home = self.avatar.getHomeDir()
+ return os.path.abspath(os.path.join(home, path))
+
+ def gotVersion(self, otherVersion, extData):
+ return {}
+
+ def openFile(self, filename, flags, attrs):
+ return UnixSFTPFile(self, self._absPath(filename), flags, attrs)
+
+ def removeFile(self, filename):
+ filename = self._absPath(filename)
+ return self.avatar._runAsUser(os.remove, filename)
+
+ def renameFile(self, oldpath, newpath):
+ oldpath = self._absPath(oldpath)
+ newpath = self._absPath(newpath)
+ return self.avatar._runAsUser(os.rename, oldpath, newpath)
+
+ def makeDirectory(self, path, attrs):
+ path = self._absPath(path)
+ return self.avatar._runAsUser([(os.mkdir, (path,)),
+ (self._setAttrs, (path, attrs))])
+
+ def removeDirectory(self, path):
+ path = self._absPath(path)
+ self.avatar._runAsUser(os.rmdir, path)
+
+ def openDirectory(self, path):
+ return UnixSFTPDirectory(self, self._absPath(path))
+
+ def getAttrs(self, path, followLinks):
+ path = self._absPath(path)
+ if followLinks:
+ s = self.avatar._runAsUser(os.stat, path)
+ else:
+ s = self.avatar._runAsUser(os.lstat, path)
+ return self._getAttrs(s)
+
+ def setAttrs(self, path, attrs):
+ path = self._absPath(path)
+ self.avatar._runAsUser(self._setAttrs, path, attrs)
+
+ def readLink(self, path):
+ path = self._absPath(path)
+ return self.avatar._runAsUser(os.readlink, path)
+
+ def makeLink(self, linkPath, targetPath):
+ linkPath = self._absPath(linkPath)
+ targetPath = self._absPath(targetPath)
+ return self.avatar._runAsUser(os.symlink, targetPath, linkPath)
+
+ def realPath(self, path):
+ return os.path.realpath(self._absPath(path))
+
+ def extendedRequest(self, extName, extData):
+ raise NotImplementedError
+
+class UnixSFTPFile:
+
+ interface.implements(ISFTPFile)
+
+ def __init__(self, server, filename, flags, attrs):
+ self.server = server
+ openFlags = 0
+ if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
+ openFlags = os.O_RDONLY
+ if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
+ openFlags = os.O_WRONLY
+ if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
+ openFlags = os.O_RDWR
+ if flags & FXF_APPEND == FXF_APPEND:
+ openFlags |= os.O_APPEND
+ if flags & FXF_CREAT == FXF_CREAT:
+ openFlags |= os.O_CREAT
+ if flags & FXF_TRUNC == FXF_TRUNC:
+ openFlags |= os.O_TRUNC
+ if flags & FXF_EXCL == FXF_EXCL:
+ openFlags |= os.O_EXCL
+ if attrs.has_key("permissions"):
+ mode = attrs["permissions"]
+ del attrs["permissions"]
+ else:
+ mode = 0777
+ fd = server.avatar._runAsUser(os.open, filename, openFlags, mode)
+ if attrs:
+ server.avatar._runAsUser(server._setAttrs, filename, attrs)
+ self.fd = fd
+
+ def close(self):
+ return self.server.avatar._runAsUser(os.close, self.fd)
+
+ def readChunk(self, offset, length):
+ return self.server.avatar._runAsUser([ (os.lseek, (self.fd, offset, 0)),
+ (os.read, (self.fd, length)) ])
+
+ def writeChunk(self, offset, data):
+ return self.server.avatar._runAsUser([(os.lseek, (self.fd, offset, 0)),
+ (os.write, (self.fd, data))])
+
+ def getAttrs(self):
+ s = self.server.avatar._runAsUser(os.fstat, self.fd)
+ return self.server._getAttrs(s)
+
+ def setAttrs(self, attrs):
+ raise NotImplementedError
+
+
+class UnixSFTPDirectory:
+
+ def __init__(self, server, directory):
+ self.server = server
+ self.files = server.avatar._runAsUser(os.listdir, directory)
+ self.dir = directory
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ try:
+ f = self.files.pop(0)
+ except IndexError:
+ raise StopIteration
+ else:
+ s = self.server.avatar._runAsUser(os.lstat, os.path.join(self.dir, f))
+ longname = lsLine(f, s)
+ attrs = self.server._getAttrs(s)
+ return (f, longname, attrs)
+
+ def close(self):
+ self.files = []
+
+
+components.registerAdapter(SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer)
+components.registerAdapter(SSHSessionForUnixConchUser, UnixConchUser, session.ISession)
diff --git a/twisted/copyright.py b/twisted/copyright.py
new file mode 100644
index 0000000..01165e0
--- /dev/null
+++ b/twisted/copyright.py
@@ -0,0 +1,39 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Copyright information for Twisted.
+"""
+
+from twisted import __version__ as version, version as longversion
+
+longversion = str(longversion)
+
+copyright="""\
+Copyright (c) 2001-2012 Twisted Matrix Laboratories.
+See LICENSE for details."""
+
+disclaimer='''
+Twisted, the Framework of Your Internet
+%s
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+''' % copyright
diff --git a/twisted/cred/__init__.py b/twisted/cred/__init__.py
new file mode 100644
index 0000000..06e287f
--- /dev/null
+++ b/twisted/cred/__init__.py
@@ -0,0 +1,13 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Cred
+
+Support for verifying credentials, and providing services to users based on
+those credentials.
+
+(This package was previously known as the module twisted.internet.passport.)
+"""
diff --git a/twisted/cred/_digest.py b/twisted/cred/_digest.py
new file mode 100644
index 0000000..4640a1d
--- /dev/null
+++ b/twisted/cred/_digest.py
@@ -0,0 +1,129 @@
+# -*- test-case-name: twisted.test.test_digestauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Calculations for HTTP Digest authentication.
+
+@see: U{http://www.faqs.org/rfcs/rfc2617.html}
+"""
+
+from twisted.python.hashlib import md5, sha1
+
+
+
+# The digest math
+
+algorithms = {
+ 'md5': md5,
+
+ # md5-sess is more complicated than just another algorithm. It requires
+ # H(A1) state to be remembered from the first WWW-Authenticate challenge
+ # issued and re-used to process any Authorization header in response to
+ # that WWW-Authenticate challenge. It is *not* correct to simply
+ # recalculate H(A1) each time an Authorization header is received. Read
+ # RFC 2617, section 3.2.2.2 and do not try to make DigestCredentialFactory
+ # support this unless you completely understand it. -exarkun
+ 'md5-sess': md5,
+
+ 'sha': sha1,
+}
+
+# DigestCalcHA1
+def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce,
+ preHA1=None):
+ """
+ Compute H(A1) from RFC 2617.
+
+ @param pszAlg: The name of the algorithm to use to calculate the digest.
+ Currently supported are md5, md5-sess, and sha.
+ @param pszUserName: The username
+ @param pszRealm: The realm
+ @param pszPassword: The password
+ @param pszNonce: The nonce
+ @param pszCNonce: The cnonce
+
+ @param preHA1: If available this is a str containing a previously
+ calculated H(A1) as a hex string. If this is given then the values for
+ pszUserName, pszRealm, and pszPassword must be C{None} and are ignored.
+ """
+
+ if (preHA1 and (pszUserName or pszRealm or pszPassword)):
+ raise TypeError(("preHA1 is incompatible with the pszUserName, "
+ "pszRealm, and pszPassword arguments"))
+
+ if preHA1 is None:
+ # We need to calculate the HA1 from the username:realm:password
+ m = algorithms[pszAlg]()
+ m.update(pszUserName)
+ m.update(":")
+ m.update(pszRealm)
+ m.update(":")
+ m.update(pszPassword)
+ HA1 = m.digest()
+ else:
+ # We were given a username:realm:password
+ HA1 = preHA1.decode('hex')
+
+ if pszAlg == "md5-sess":
+ m = algorithms[pszAlg]()
+ m.update(HA1)
+ m.update(":")
+ m.update(pszNonce)
+ m.update(":")
+ m.update(pszCNonce)
+ HA1 = m.digest()
+
+ return HA1.encode('hex')
+
+
+def calcHA2(algo, pszMethod, pszDigestUri, pszQop, pszHEntity):
+ """
+ Compute H(A2) from RFC 2617.
+
+ @param pszAlg: The name of the algorithm to use to calculate the digest.
+ Currently supported are md5, md5-sess, and sha.
+ @param pszMethod: The request method.
+ @param pszDigestUri: The request URI.
+ @param pszQop: The Quality-of-Protection value.
+ @param pszHEntity: The hash of the entity body or C{None} if C{pszQop} is
+ not C{'auth-int'}.
+ @return: The hash of the A2 value for the calculation of the response
+ digest.
+ """
+ m = algorithms[algo]()
+ m.update(pszMethod)
+ m.update(":")
+ m.update(pszDigestUri)
+ if pszQop == "auth-int":
+ m.update(":")
+ m.update(pszHEntity)
+ return m.digest().encode('hex')
+
+
+def calcResponse(HA1, HA2, algo, pszNonce, pszNonceCount, pszCNonce, pszQop):
+ """
+ Compute the digest for the given parameters.
+
+ @param HA1: The H(A1) value, as computed by L{calcHA1}.
+ @param HA2: The H(A2) value, as computed by L{calcHA2}.
+ @param pszNonce: The challenge nonce.
+ @param pszNonceCount: The (client) nonce count value for this response.
+ @param pszCNonce: The client nonce.
+ @param pszQop: The Quality-of-Protection value.
+ """
+ m = algorithms[algo]()
+ m.update(HA1)
+ m.update(":")
+ m.update(pszNonce)
+ m.update(":")
+ if pszNonceCount and pszCNonce:
+ m.update(pszNonceCount)
+ m.update(":")
+ m.update(pszCNonce)
+ m.update(":")
+ m.update(pszQop)
+ m.update(":")
+ m.update(HA2)
+ respHash = m.digest().encode('hex')
+ return respHash
diff --git a/twisted/cred/checkers.py b/twisted/cred/checkers.py
new file mode 100644
index 0000000..523a94d
--- /dev/null
+++ b/twisted/cred/checkers.py
@@ -0,0 +1,268 @@
+# -*- test-case-name: twisted.test.test_newcred -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import os
+
+from zope.interface import implements, Interface, Attribute
+
+from twisted.internet import defer
+from twisted.python import failure, log
+from twisted.cred import error, credentials
+
+
+
+class ICredentialsChecker(Interface):
+ """
+ An object that can check sub-interfaces of ICredentials.
+ """
+
+ credentialInterfaces = Attribute(
+ 'A list of sub-interfaces of ICredentials which specifies which I may check.')
+
+
+ def requestAvatarId(credentials):
+ """
+ @param credentials: something which implements one of the interfaces in
+ self.credentialInterfaces.
+
+ @return: a Deferred which will fire a string which identifies an
+ avatar, an empty tuple to specify an authenticated anonymous user
+ (provided as checkers.ANONYMOUS) or fire a Failure(UnauthorizedLogin).
+ Alternatively, return the result itself.
+
+ @see: L{twisted.cred.credentials}
+ """
+
+
+
+# A note on anonymity - We do not want None as the value for anonymous
+# because it is too easy to accidentally return it. We do not want the
+# empty string, because it is too easy to mistype a password file. For
+# example, an .htpasswd file may contain the lines: ['hello:asdf',
+# 'world:asdf', 'goodbye', ':world']. This misconfiguration will have an
+# ill effect in any case, but accidentally granting anonymous access is a
+# worse failure mode than simply granting access to an untypeable
+# username. We do not want an instance of 'object', because that would
+# create potential problems with persistence.
+
+ANONYMOUS = ()
+
+
+class AllowAnonymousAccess:
+ implements(ICredentialsChecker)
+ credentialInterfaces = credentials.IAnonymous,
+
+ def requestAvatarId(self, credentials):
+ return defer.succeed(ANONYMOUS)
+
+
+class InMemoryUsernamePasswordDatabaseDontUse:
+ """
+ An extremely simple credentials checker.
+
+ This is only of use in one-off test programs or examples which don't
+ want to focus too much on how credentials are verified.
+
+ You really don't want to use this for anything else. It is, at best, a
+ toy. If you need a simple credentials checker for a real application,
+ see L{FilePasswordDB}.
+ """
+
+ implements(ICredentialsChecker)
+
+ credentialInterfaces = (credentials.IUsernamePassword,
+ credentials.IUsernameHashedPassword)
+
+ def __init__(self, **users):
+ self.users = users
+
+ def addUser(self, username, password):
+ self.users[username] = password
+
+ def _cbPasswordMatch(self, matched, username):
+ if matched:
+ return username
+ else:
+ return failure.Failure(error.UnauthorizedLogin())
+
+ def requestAvatarId(self, credentials):
+ if credentials.username in self.users:
+ return defer.maybeDeferred(
+ credentials.checkPassword,
+ self.users[credentials.username]).addCallback(
+ self._cbPasswordMatch, str(credentials.username))
+ else:
+ return defer.fail(error.UnauthorizedLogin())
+
+
+class FilePasswordDB:
+ """A file-based, text-based username/password database.
+
+ Records in the datafile for this class are delimited by a particular
+ string. The username appears in a fixed field of the columns delimited
+ by this string, as does the password. Both fields are specifiable. If
+ the passwords are not stored plaintext, a hash function must be supplied
+ to convert plaintext passwords to the form stored on disk and this
+ CredentialsChecker will only be able to check IUsernamePassword
+ credentials. If the passwords are stored plaintext,
+ IUsernameHashedPassword credentials will be checkable as well.
+ """
+
+ implements(ICredentialsChecker)
+
+ cache = False
+ _credCache = None
+ _cacheTimestamp = 0
+
+ def __init__(self, filename, delim=':', usernameField=0, passwordField=1,
+ caseSensitive=True, hash=None, cache=False):
+ """
+ @type filename: C{str}
+ @param filename: The name of the file from which to read username and
+ password information.
+
+ @type delim: C{str}
+ @param delim: The field delimiter used in the file.
+
+ @type usernameField: C{int}
+ @param usernameField: The index of the username after splitting a
+ line on the delimiter.
+
+ @type passwordField: C{int}
+ @param passwordField: The index of the password after splitting a
+ line on the delimiter.
+
+ @type caseSensitive: C{bool}
+ @param caseSensitive: If true, consider the case of the username when
+ performing a lookup. Ignore it otherwise.
+
+ @type hash: Three-argument callable or C{None}
+ @param hash: A function used to transform the plaintext password
+ received over the network to a format suitable for comparison
+ against the version stored on disk. The arguments to the callable
+ are the username, the network-supplied password, and the in-file
+ version of the password. If the return value compares equal to the
+ version stored on disk, the credentials are accepted.
+
+ @type cache: C{bool}
+ @param cache: If true, maintain an in-memory cache of the
+ contents of the password file. On lookups, the mtime of the
+ file will be checked, and the file will only be re-parsed if
+ the mtime is newer than when the cache was generated.
+ """
+ self.filename = filename
+ self.delim = delim
+ self.ufield = usernameField
+ self.pfield = passwordField
+ self.caseSensitive = caseSensitive
+ self.hash = hash
+ self.cache = cache
+
+ if self.hash is None:
+ # The passwords are stored plaintext. We can support both
+ # plaintext and hashed passwords received over the network.
+ self.credentialInterfaces = (
+ credentials.IUsernamePassword,
+ credentials.IUsernameHashedPassword
+ )
+ else:
+ # The passwords are hashed on disk. We can support only
+ # plaintext passwords received over the network.
+ self.credentialInterfaces = (
+ credentials.IUsernamePassword,
+ )
+
+
+ def __getstate__(self):
+ d = dict(vars(self))
+ for k in '_credCache', '_cacheTimestamp':
+ try:
+ del d[k]
+ except KeyError:
+ pass
+ return d
+
+
+ def _cbPasswordMatch(self, matched, username):
+ if matched:
+ return username
+ else:
+ return failure.Failure(error.UnauthorizedLogin())
+
+
+ def _loadCredentials(self):
+ try:
+ f = file(self.filename)
+ except:
+ log.err()
+ raise error.UnauthorizedLogin()
+ else:
+ for line in f:
+ line = line.rstrip()
+ parts = line.split(self.delim)
+
+ if self.ufield >= len(parts) or self.pfield >= len(parts):
+ continue
+ if self.caseSensitive:
+ yield parts[self.ufield], parts[self.pfield]
+ else:
+ yield parts[self.ufield].lower(), parts[self.pfield]
+
+
+ def getUser(self, username):
+ if not self.caseSensitive:
+ username = username.lower()
+
+ if self.cache:
+ if self._credCache is None or os.path.getmtime(self.filename) > self._cacheTimestamp:
+ self._cacheTimestamp = os.path.getmtime(self.filename)
+ self._credCache = dict(self._loadCredentials())
+ return username, self._credCache[username]
+ else:
+ for u, p in self._loadCredentials():
+ if u == username:
+ return u, p
+ raise KeyError(username)
+
+
+ def requestAvatarId(self, c):
+ try:
+ u, p = self.getUser(c.username)
+ except KeyError:
+ return defer.fail(error.UnauthorizedLogin())
+ else:
+ up = credentials.IUsernamePassword(c, None)
+ if self.hash:
+ if up is not None:
+ h = self.hash(up.username, up.password, p)
+ if h == p:
+ return defer.succeed(u)
+ return defer.fail(error.UnauthorizedLogin())
+ else:
+ return defer.maybeDeferred(c.checkPassword, p
+ ).addCallback(self._cbPasswordMatch, u)
+
+
+
+class PluggableAuthenticationModulesChecker:
+ implements(ICredentialsChecker)
+ credentialInterfaces = credentials.IPluggableAuthenticationModules,
+ service = 'Twisted'
+
+ def requestAvatarId(self, credentials):
+ try:
+ from twisted.cred import pamauth
+ except ImportError: # PyPAM is missing
+ return defer.fail(error.UnauthorizedLogin())
+ else:
+ d = pamauth.pamAuthenticate(self.service, credentials.username,
+ credentials.pamConversion)
+ d.addCallback(lambda x: credentials.username)
+ return d
+
+
+
+# For backwards compatibility
+# Allow access as the old name.
+OnDiskUsernamePasswordDatabase = FilePasswordDB
diff --git a/twisted/cred/credentials.py b/twisted/cred/credentials.py
new file mode 100644
index 0000000..63fb44e
--- /dev/null
+++ b/twisted/cred/credentials.py
@@ -0,0 +1,483 @@
+# -*- test-case-name: twisted.test.test_newcred-*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from zope.interface import implements, Interface
+
+import hmac, time, random
+from twisted.python.hashlib import md5
+from twisted.python.randbytes import secureRandom
+from twisted.cred._digest import calcResponse, calcHA1, calcHA2
+from twisted.cred import error
+
+class ICredentials(Interface):
+ """
+ I check credentials.
+
+ Implementors _must_ specify which sub-interfaces of ICredentials
+ to which it conforms, using zope.interface.implements().
+ """
+
+
+
+class IUsernameDigestHash(ICredentials):
+ """
+ This credential is used when a CredentialChecker has access to the hash
+ of the username:realm:password as in an Apache .htdigest file.
+ """
+ def checkHash(digestHash):
+ """
+ @param digestHash: The hashed username:realm:password to check against.
+
+ @return: C{True} if the credentials represented by this object match
+ the given hash, C{False} if they do not, or a L{Deferred} which
+ will be called back with one of these values.
+ """
+
+
+
+class IUsernameHashedPassword(ICredentials):
+ """
+ I encapsulate a username and a hashed password.
+
+ This credential is used when a hashed password is received from the
+ party requesting authentication. CredentialCheckers which check this
+ kind of credential must store the passwords in plaintext (or as
+ password-equivalent hashes) form so that they can be hashed in a manner
+ appropriate for the particular credentials class.
+
+ @type username: C{str}
+ @ivar username: The username associated with these credentials.
+ """
+
+ def checkPassword(password):
+ """
+ Validate these credentials against the correct password.
+
+ @type password: C{str}
+ @param password: The correct, plaintext password against which to
+ check.
+
+ @rtype: C{bool} or L{Deferred}
+ @return: C{True} if the credentials represented by this object match the
+ given password, C{False} if they do not, or a L{Deferred} which will
+ be called back with one of these values.
+ """
+
+
+
+class IUsernamePassword(ICredentials):
+ """
+ I encapsulate a username and a plaintext password.
+
+ This encapsulates the case where the password received over the network
+ has been hashed with the identity function (That is, not at all). The
+ CredentialsChecker may store the password in whatever format it desires,
+ it need only transform the stored password in a similar way before
+ performing the comparison.
+
+ @type username: C{str}
+ @ivar username: The username associated with these credentials.
+
+ @type password: C{str}
+ @ivar password: The password associated with these credentials.
+ """
+
+ def checkPassword(password):
+ """
+ Validate these credentials against the correct password.
+
+ @type password: C{str}
+ @param password: The correct, plaintext password against which to
+ check.
+
+ @rtype: C{bool} or L{Deferred}
+ @return: C{True} if the credentials represented by this object match the
+ given password, C{False} if they do not, or a L{Deferred} which will
+ be called back with one of these values.
+ """
+
+
+
+class IAnonymous(ICredentials):
+ """
+ I am an explicitly anonymous request for access.
+ """
+
+
+
+class DigestedCredentials(object):
+ """
+ Yet Another Simple HTTP Digest authentication scheme.
+ """
+ implements(IUsernameHashedPassword, IUsernameDigestHash)
+
+ def __init__(self, username, method, realm, fields):
+ self.username = username
+ self.method = method
+ self.realm = realm
+ self.fields = fields
+
+
+ def checkPassword(self, password):
+ """
+ Verify that the credentials represented by this object agree with the
+ given plaintext C{password} by hashing C{password} in the same way the
+ response hash represented by this object was generated and comparing
+ the results.
+ """
+ response = self.fields.get('response')
+ uri = self.fields.get('uri')
+ nonce = self.fields.get('nonce')
+ cnonce = self.fields.get('cnonce')
+ nc = self.fields.get('nc')
+ algo = self.fields.get('algorithm', 'md5').lower()
+ qop = self.fields.get('qop', 'auth')
+
+ expected = calcResponse(
+ calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
+ calcHA2(algo, self.method, uri, qop, None),
+ algo, nonce, nc, cnonce, qop)
+
+ return expected == response
+
+
+ def checkHash(self, digestHash):
+ """
+ Verify that the credentials represented by this object agree with the
+ credentials represented by the I{H(A1)} given in C{digestHash}.
+
+ @param digestHash: A precomputed H(A1) value based on the username,
+ realm, and password associate with this credentials object.
+ """
+ response = self.fields.get('response')
+ uri = self.fields.get('uri')
+ nonce = self.fields.get('nonce')
+ cnonce = self.fields.get('cnonce')
+ nc = self.fields.get('nc')
+ algo = self.fields.get('algorithm', 'md5').lower()
+ qop = self.fields.get('qop', 'auth')
+
+ expected = calcResponse(
+ calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
+ calcHA2(algo, self.method, uri, qop, None),
+ algo, nonce, nc, cnonce, qop)
+
+ return expected == response
+
+
+
+class DigestCredentialFactory(object):
+ """
+ Support for RFC2617 HTTP Digest Authentication
+
+ @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
+ opaque should be valid.
+
+ @type privateKey: C{str}
+ @ivar privateKey: A random string used for generating the secure opaque.
+
+ @type algorithm: C{str}
+ @param algorithm: Case insensitive string specifying the hash algorithm to
+ use. Must be either C{'md5'} or C{'sha'}. C{'md5-sess'} is B{not}
+ supported.
+
+ @type authenticationRealm: C{str}
+ @param authenticationRealm: case sensitive string that specifies the realm
+ portion of the challenge
+ """
+
+ CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
+
+ scheme = "digest"
+
+ def __init__(self, algorithm, authenticationRealm):
+ self.algorithm = algorithm
+ self.authenticationRealm = authenticationRealm
+ self.privateKey = secureRandom(12)
+
+
+ def getChallenge(self, address):
+ """
+ Generate the challenge for use in the WWW-Authenticate header.
+
+ @param address: The client address to which this challenge is being
+ sent.
+
+ @return: The C{dict} that can be used to generate a WWW-Authenticate
+ header.
+ """
+ c = self._generateNonce()
+ o = self._generateOpaque(c, address)
+
+ return {'nonce': c,
+ 'opaque': o,
+ 'qop': 'auth',
+ 'algorithm': self.algorithm,
+ 'realm': self.authenticationRealm}
+
+
+ def _generateNonce(self):
+ """
+ Create a random value suitable for use as the nonce parameter of a
+ WWW-Authenticate challenge.
+
+ @rtype: C{str}
+ """
+ return secureRandom(12).encode('hex')
+
+
+ def _getTime(self):
+ """
+ Parameterize the time based seed used in C{_generateOpaque}
+ so we can deterministically unittest it's behavior.
+ """
+ return time.time()
+
+
+ def _generateOpaque(self, nonce, clientip):
+ """
+ Generate an opaque to be returned to the client. This is a unique
+ string that can be returned to us and verified.
+ """
+ # Now, what we do is encode the nonce, client ip and a timestamp in the
+ # opaque value with a suitable digest.
+ now = str(int(self._getTime()))
+ if clientip is None:
+ clientip = ''
+ key = "%s,%s,%s" % (nonce, clientip, now)
+ digest = md5(key + self.privateKey).hexdigest()
+ ekey = key.encode('base64')
+ return "%s-%s" % (digest, ekey.replace('\n', ''))
+
+
+ def _verifyOpaque(self, opaque, nonce, clientip):
+ """
+ Given the opaque and nonce from the request, as well as the client IP
+ that made the request, verify that the opaque was generated by us.
+ And that it's not too old.
+
+ @param opaque: The opaque value from the Digest response
+ @param nonce: The nonce value from the Digest response
+ @param clientip: The remote IP address of the client making the request
+ or C{None} if the request was submitted over a channel where this
+ does not make sense.
+
+ @return: C{True} if the opaque was successfully verified.
+
+ @raise error.LoginFailed: if C{opaque} could not be parsed or
+ contained the wrong values.
+ """
+ # First split the digest from the key
+ opaqueParts = opaque.split('-')
+ if len(opaqueParts) != 2:
+ raise error.LoginFailed('Invalid response, invalid opaque value')
+
+ if clientip is None:
+ clientip = ''
+
+ # Verify the key
+ key = opaqueParts[1].decode('base64')
+ keyParts = key.split(',')
+
+ if len(keyParts) != 3:
+ raise error.LoginFailed('Invalid response, invalid opaque value')
+
+ if keyParts[0] != nonce:
+ raise error.LoginFailed(
+ 'Invalid response, incompatible opaque/nonce values')
+
+ if keyParts[1] != clientip:
+ raise error.LoginFailed(
+ 'Invalid response, incompatible opaque/client values')
+
+ try:
+ when = int(keyParts[2])
+ except ValueError:
+ raise error.LoginFailed(
+ 'Invalid response, invalid opaque/time values')
+
+ if (int(self._getTime()) - when >
+ DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
+
+ raise error.LoginFailed(
+ 'Invalid response, incompatible opaque/nonce too old')
+
+ # Verify the digest
+ digest = md5(key + self.privateKey).hexdigest()
+ if digest != opaqueParts[0]:
+ raise error.LoginFailed('Invalid response, invalid opaque value')
+
+ return True
+
+
+ def decode(self, response, method, host):
+ """
+ Decode the given response and attempt to generate a
+ L{DigestedCredentials} from it.
+
+ @type response: C{str}
+ @param response: A string of comma seperated key=value pairs
+
+ @type method: C{str}
+ @param method: The action requested to which this response is addressed
+ (GET, POST, INVITE, OPTIONS, etc).
+
+ @type host: C{str}
+ @param host: The address the request was sent from.
+
+ @raise error.LoginFailed: If the response does not contain a username,
+ a nonce, an opaque, or if the opaque is invalid.
+
+ @return: L{DigestedCredentials}
+ """
+ def unq(s):
+ if s[0] == s[-1] == '"':
+ return s[1:-1]
+ return s
+ response = ' '.join(response.splitlines())
+ parts = response.split(',')
+
+ auth = {}
+
+ for (k, v) in [p.split('=', 1) for p in parts]:
+ auth[k.strip()] = unq(v.strip())
+
+ username = auth.get('username')
+ if not username:
+ raise error.LoginFailed('Invalid response, no username given.')
+
+ if 'opaque' not in auth:
+ raise error.LoginFailed('Invalid response, no opaque given.')
+
+ if 'nonce' not in auth:
+ raise error.LoginFailed('Invalid response, no nonce given.')
+
+ # Now verify the nonce/opaque values for this client
+ if self._verifyOpaque(auth.get('opaque'), auth.get('nonce'), host):
+ return DigestedCredentials(username,
+ method,
+ self.authenticationRealm,
+ auth)
+
+
+
+class CramMD5Credentials:
+ implements(IUsernameHashedPassword)
+
+ challenge = ''
+ response = ''
+
+ def __init__(self, host=None):
+ self.host = host
+
+ def getChallenge(self):
+ if self.challenge:
+ return self.challenge
+ # The data encoded in the first ready response contains an
+ # presumptively arbitrary string of random digits, a timestamp, and
+ # the fully-qualified primary host name of the server. The syntax of
+ # the unencoded form must correspond to that of an RFC 822 'msg-id'
+ # [RFC822] as described in [POP3].
+ # -- RFC 2195
+ r = random.randrange(0x7fffffff)
+ t = time.time()
+ self.challenge = '<%d.%d@%s>' % (r, t, self.host)
+ return self.challenge
+
+ def setResponse(self, response):
+ self.username, self.response = response.split(None, 1)
+
+ def moreChallenges(self):
+ return False
+
+ def checkPassword(self, password):
+ verify = hmac.HMAC(password, self.challenge).hexdigest()
+ return verify == self.response
+
+
+class UsernameHashedPassword:
+ implements(IUsernameHashedPassword)
+
+ def __init__(self, username, hashed):
+ self.username = username
+ self.hashed = hashed
+
+ def checkPassword(self, password):
+ return self.hashed == password
+
+
+class UsernamePassword:
+ implements(IUsernamePassword)
+
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+
+ def checkPassword(self, password):
+ return self.password == password
+
+
+class Anonymous:
+ implements(IAnonymous)
+
+
+
+class ISSHPrivateKey(ICredentials):
+ """
+ L{ISSHPrivateKey} credentials encapsulate an SSH public key to be checked
+ against a user's private key.
+
+ @ivar username: The username associated with these credentials.
+ @type username: C{str}
+
+ @ivar algName: The algorithm name for the blob.
+ @type algName: C{str}
+
+ @ivar blob: The public key blob as sent by the client.
+ @type blob: C{str}
+
+ @ivar sigData: The data the signature was made from.
+ @type sigData: C{str}
+
+ @ivar signature: The signed data. This is checked to verify that the user
+ owns the private key.
+ @type signature: C{str} or C{NoneType}
+ """
+
+
+
+class SSHPrivateKey:
+ implements(ISSHPrivateKey)
+ def __init__(self, username, algName, blob, sigData, signature):
+ self.username = username
+ self.algName = algName
+ self.blob = blob
+ self.sigData = sigData
+ self.signature = signature
+
+
+class IPluggableAuthenticationModules(ICredentials):
+ """I encapsulate the authentication of a user via PAM (Pluggable
+ Authentication Modules. I use PyPAM (available from
+ http://www.tummy.com/Software/PyPam/index.html).
+
+ @ivar username: The username for the user being logged in.
+
+ @ivar pamConversion: A function that is called with a list of tuples
+ (message, messageType). See the PAM documentation
+ for the meaning of messageType. The function
+ returns a Deferred which will fire with a list
+ of (response, 0), one for each message. The 0 is
+ currently unused, but is required by the PAM library.
+ """
+
+class PluggableAuthenticationModules:
+ implements(IPluggableAuthenticationModules)
+
+ def __init__(self, username, pamConversion):
+ self.username = username
+ self.pamConversion = pamConversion
+
diff --git a/twisted/cred/error.py b/twisted/cred/error.py
new file mode 100644
index 0000000..cce682b
--- /dev/null
+++ b/twisted/cred/error.py
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Cred errors."""
+
+class Unauthorized(Exception):
+ """Standard unauthorized error."""
+
+
+
+class LoginFailed(Exception):
+ """
+ The user's request to log in failed for some reason.
+ """
+
+
+
+class UnauthorizedLogin(LoginFailed, Unauthorized):
+ """The user was not authorized to log in.
+ """
+
+
+
+class UnhandledCredentials(LoginFailed):
+ """A type of credentials were passed in with no knowledge of how to check
+ them. This is a server configuration error - it means that a protocol was
+ connected to a Portal without a CredentialChecker that can check all of its
+ potential authentication strategies.
+ """
+
+
+
+class LoginDenied(LoginFailed):
+ """
+ The realm rejected this login for some reason.
+
+ Examples of reasons this might be raised include an avatar logging in
+ too frequently, a quota having been fully used, or the overall server
+ load being too high.
+ """
diff --git a/twisted/cred/pamauth.py b/twisted/cred/pamauth.py
new file mode 100644
index 0000000..12357df
--- /dev/null
+++ b/twisted/cred/pamauth.py
@@ -0,0 +1,79 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support for asynchronously authenticating using PAM.
+"""
+
+
+import PAM
+
+import getpass, threading, os
+
+from twisted.internet import threads, defer
+
+def pamAuthenticateThread(service, user, conv):
+ def _conv(items):
+ from twisted.internet import reactor
+ try:
+ d = conv(items)
+ except:
+ import traceback
+ traceback.print_exc()
+ return
+ ev = threading.Event()
+ def cb(r):
+ ev.r = (1, r)
+ ev.set()
+ def eb(e):
+ ev.r = (0, e)
+ ev.set()
+ reactor.callFromThread(d.addCallbacks, cb, eb)
+ ev.wait()
+ done = ev.r
+ if done[0]:
+ return done[1]
+ else:
+ raise done[1].type, done[1].value
+
+ return callIntoPAM(service, user, _conv)
+
+def callIntoPAM(service, user, conv):
+ """A testing hook.
+ """
+ pam = PAM.pam()
+ pam.start(service)
+ pam.set_item(PAM.PAM_USER, user)
+ pam.set_item(PAM.PAM_CONV, conv)
+ gid = os.getegid()
+ uid = os.geteuid()
+ os.setegid(0)
+ os.seteuid(0)
+ try:
+ pam.authenticate() # these will raise
+ pam.acct_mgmt()
+ return 1
+ finally:
+ os.setegid(gid)
+ os.seteuid(uid)
+
+def defConv(items):
+ resp = []
+ for i in range(len(items)):
+ message, kind = items[i]
+ if kind == 1: # password
+ p = getpass.getpass(message)
+ resp.append((p, 0))
+ elif kind == 2: # text
+ p = raw_input(message)
+ resp.append((p, 0))
+ elif kind in (3,4):
+ print message
+ resp.append(("", 0))
+ else:
+ return defer.fail('foo')
+ d = defer.succeed(resp)
+ return d
+
+def pamAuthenticate(service, user, conv):
+ return threads.deferToThread(pamAuthenticateThread, service, user, conv)
diff --git a/twisted/cred/portal.py b/twisted/cred/portal.py
new file mode 100644
index 0000000..bbb0af8
--- /dev/null
+++ b/twisted/cred/portal.py
@@ -0,0 +1,121 @@
+# -*- test-case-name: twisted.test.test_newcred -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The point of integration of application and authentication.
+"""
+
+
+from twisted.internet import defer
+from twisted.internet.defer import maybeDeferred
+from twisted.python import failure, reflect
+from twisted.cred import error
+from zope.interface import providedBy, Interface
+
+
+class IRealm(Interface):
+ """
+ The realm connects application-specific objects to the
+ authentication system.
+ """
+ def requestAvatar(avatarId, mind, *interfaces):
+ """
+ Return avatar which provides one of the given interfaces.
+
+ @param avatarId: a string that identifies an avatar, as returned by
+ L{ICredentialsChecker.requestAvatarId<twisted.cred.checkers.ICredentialsChecker.requestAvatarId>}
+ (via a Deferred). Alternatively, it may be
+ C{twisted.cred.checkers.ANONYMOUS}.
+ @param mind: usually None. See the description of mind in
+ L{Portal.login}.
+ @param interfaces: the interface(s) the returned avatar should
+ implement, e.g. C{IMailAccount}. See the description of
+ L{Portal.login}.
+
+ @returns: a deferred which will fire a tuple of (interface,
+ avatarAspect, logout), or the tuple itself. The interface will be
+ one of the interfaces passed in the 'interfaces' argument. The
+ 'avatarAspect' will implement that interface. The 'logout' object
+ is a callable which will detach the mind from the avatar.
+ """
+
+
+class Portal:
+ """
+ A mediator between clients and a realm.
+
+ A portal is associated with one Realm and zero or more credentials checkers.
+ When a login is attempted, the portal finds the appropriate credentials
+ checker for the credentials given, invokes it, and if the credentials are
+ valid, retrieves the appropriate avatar from the Realm.
+
+ This class is not intended to be subclassed. Customization should be done
+ in the realm object and in the credentials checker objects.
+ """
+ def __init__(self, realm, checkers=()):
+ """
+ Create a Portal to a L{IRealm}.
+ """
+ self.realm = realm
+ self.checkers = {}
+ for checker in checkers:
+ self.registerChecker(checker)
+
+ def listCredentialsInterfaces(self):
+ """
+ Return list of credentials interfaces that can be used to login.
+ """
+ return self.checkers.keys()
+
+ def registerChecker(self, checker, *credentialInterfaces):
+ if not credentialInterfaces:
+ credentialInterfaces = checker.credentialInterfaces
+ for credentialInterface in credentialInterfaces:
+ self.checkers[credentialInterface] = checker
+
+ def login(self, credentials, mind, *interfaces):
+ """
+ @param credentials: an implementor of
+ L{twisted.cred.credentials.ICredentials}
+
+ @param mind: an object which implements a client-side interface for
+ your particular realm. In many cases, this may be None, so if the
+ word 'mind' confuses you, just ignore it.
+
+ @param interfaces: list of interfaces for the perspective that the mind
+ wishes to attach to. Usually, this will be only one interface, for
+ example IMailAccount. For highly dynamic protocols, however, this
+ may be a list like (IMailAccount, IUserChooser, IServiceInfo). To
+ expand: if we are speaking to the system over IMAP, any information
+ that will be relayed to the user MUST be returned as an
+ IMailAccount implementor; IMAP clients would not be able to
+ understand anything else. Any information about unusual status
+ would have to be relayed as a single mail message in an
+ otherwise-empty mailbox. However, in a web-based mail system, or a
+ PB-based client, the ``mind'' object inside the web server
+ (implemented with a dynamic page-viewing mechanism such as a
+ Twisted Web Resource) or on the user's client program may be
+ intelligent enough to respond to several ``server''-side
+ interfaces.
+
+ @return: A deferred which will fire a tuple of (interface,
+ avatarAspect, logout). The interface will be one of the interfaces
+ passed in the 'interfaces' argument. The 'avatarAspect' will
+ implement that interface. The 'logout' object is a callable which
+ will detach the mind from the avatar. It must be called when the
+ user has conceptually disconnected from the service. Although in
+ some cases this will not be in connectionLost (such as in a
+ web-based session), it will always be at the end of a user's
+ interactive session.
+ """
+ for i in self.checkers:
+ if i.providedBy(credentials):
+ return maybeDeferred(self.checkers[i].requestAvatarId, credentials
+ ).addCallback(self.realm.requestAvatar, mind, *interfaces
+ )
+ ifac = providedBy(credentials)
+ return defer.fail(failure.Failure(error.UnhandledCredentials(
+ "No checker for %s" % ', '.join(map(reflect.qual, ifac)))))
+
diff --git a/twisted/cred/strcred.py b/twisted/cred/strcred.py
new file mode 100644
index 0000000..5f99a16
--- /dev/null
+++ b/twisted/cred/strcred.py
@@ -0,0 +1,270 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+
+"""
+Support for resolving command-line strings that represent different
+checkers available to cred.
+
+Examples:
+ - passwd:/etc/passwd
+ - memory:admin:asdf:user:lkj
+ - unix
+"""
+
+import sys
+
+from zope.interface import Interface, Attribute
+
+from twisted.plugin import getPlugins
+from twisted.python import usage
+
+
+
+class ICheckerFactory(Interface):
+ """
+ A factory for objects which provide
+ L{twisted.cred.checkers.ICredentialsChecker}.
+
+ It's implemented by twistd plugins creating checkers.
+ """
+
+ authType = Attribute(
+ 'A tag that identifies the authentication method.')
+
+
+ authHelp = Attribute(
+ 'A detailed (potentially multi-line) description of precisely '
+ 'what functionality this CheckerFactory provides.')
+
+
+ argStringFormat = Attribute(
+ 'A short (one-line) description of the argument string format.')
+
+
+ credentialInterfaces = Attribute(
+ 'A list of credentials interfaces that this factory will support.')
+
+
+ def generateChecker(argstring):
+ """
+ Return an L{ICredentialChecker} provider using the supplied
+ argument string.
+ """
+
+
+
+class StrcredException(Exception):
+ """
+ Base exception class for strcred.
+ """
+
+
+
+class InvalidAuthType(StrcredException):
+ """
+ Raised when a user provides an invalid identifier for the
+ authentication plugin (known as the authType).
+ """
+
+
+
+class InvalidAuthArgumentString(StrcredException):
+ """
+ Raised by an authentication plugin when the argument string
+ provided is formatted incorrectly.
+ """
+
+
+
+class UnsupportedInterfaces(StrcredException):
+ """
+ Raised when an application is given a checker to use that does not
+ provide any of the application's supported credentials interfaces.
+ """
+
+
+
+# This will be used to warn the users whenever they view help for an
+# authType that is not supported by the application.
+notSupportedWarning = ("WARNING: This authType is not supported by "
+ "this application.")
+
+
+
+def findCheckerFactories():
+ """
+ Find all objects that implement L{ICheckerFactory}.
+ """
+ return getPlugins(ICheckerFactory)
+
+
+
+def findCheckerFactory(authType):
+ """
+ Find the first checker factory that supports the given authType.
+ """
+ for factory in findCheckerFactories():
+ if factory.authType == authType:
+ return factory
+ raise InvalidAuthType(authType)
+
+
+
+def makeChecker(description):
+ """
+ Returns an L{twisted.cred.checkers.ICredentialsChecker} based on the
+ contents of a descriptive string. Similar to
+ L{twisted.application.strports}.
+ """
+ if ':' in description:
+ authType, argstring = description.split(':', 1)
+ else:
+ authType = description
+ argstring = ''
+ return findCheckerFactory(authType).generateChecker(argstring)
+
+
+
+class AuthOptionMixin:
+ """
+ Defines helper methods that can be added on to any
+ L{usage.Options} subclass that needs authentication.
+
+ This mixin implements three new options methods:
+
+ The opt_auth method (--auth) will write two new values to the
+ 'self' dictionary: C{credInterfaces} (a dict of lists) and
+ C{credCheckers} (a list).
+
+ The opt_help_auth method (--help-auth) will search for all
+ available checker plugins and list them for the user; it will exit
+ when finished.
+
+ The opt_help_auth_type method (--help-auth-type) will display
+ detailed help for a particular checker plugin.
+
+ @cvar supportedInterfaces: An iterable object that returns
+ credential interfaces which this application is able to support.
+
+ @cvar authOutput: A writeable object to which this options class
+ will send all help-related output. Default: L{sys.stdout}
+ """
+
+ supportedInterfaces = None
+ authOutput = sys.stdout
+
+
+ def supportsInterface(self, interface):
+ """
+ Returns whether a particular credentials interface is supported.
+ """
+ return (self.supportedInterfaces is None
+ or interface in self.supportedInterfaces)
+
+
+ def supportsCheckerFactory(self, factory):
+ """
+ Returns whether a checker factory will provide at least one of
+ the credentials interfaces that we care about.
+ """
+ for interface in factory.credentialInterfaces:
+ if self.supportsInterface(interface):
+ return True
+ return False
+
+
+ def addChecker(self, checker):
+ """
+ Supply a supplied credentials checker to the Options class.
+ """
+ # First figure out which interfaces we're willing to support.
+ supported = []
+ if self.supportedInterfaces is None:
+ supported = checker.credentialInterfaces
+ else:
+ for interface in checker.credentialInterfaces:
+ if self.supportsInterface(interface):
+ supported.append(interface)
+ if not supported:
+ raise UnsupportedInterfaces(checker.credentialInterfaces)
+ # If we get this far, then we know we can use this checker.
+ if 'credInterfaces' not in self:
+ self['credInterfaces'] = {}
+ if 'credCheckers' not in self:
+ self['credCheckers'] = []
+ self['credCheckers'].append(checker)
+ for interface in supported:
+ self['credInterfaces'].setdefault(interface, []).append(checker)
+
+
+ def opt_auth(self, description):
+ """
+ Specify an authentication method for the server.
+ """
+ try:
+ self.addChecker(makeChecker(description))
+ except UnsupportedInterfaces, e:
+ raise usage.UsageError(
+ 'Auth plugin not supported: %s' % e.args[0])
+ except InvalidAuthType, e:
+ raise usage.UsageError(
+ 'Auth plugin not recognized: %s' % e.args[0])
+ except Exception, e:
+ raise usage.UsageError('Unexpected error: %s' % e)
+
+
+ def _checkerFactoriesForOptHelpAuth(self):
+ """
+ Return a list of which authTypes will be displayed by --help-auth.
+ This makes it a lot easier to test this module.
+ """
+ for factory in findCheckerFactories():
+ for interface in factory.credentialInterfaces:
+ if self.supportsInterface(interface):
+ yield factory
+ break
+
+
+ def opt_help_auth(self):
+ """
+ Show all authentication methods available.
+ """
+ self.authOutput.write("Usage: --auth AuthType[:ArgString]\n")
+ self.authOutput.write("For detailed help: --help-auth-type AuthType\n")
+ self.authOutput.write('\n')
+ # Figure out the right width for our columns
+ firstLength = 0
+ for factory in self._checkerFactoriesForOptHelpAuth():
+ if len(factory.authType) > firstLength:
+ firstLength = len(factory.authType)
+ formatString = ' %%-%is\t%%s\n' % firstLength
+ self.authOutput.write(formatString % ('AuthType', 'ArgString format'))
+ self.authOutput.write(formatString % ('========', '================'))
+ for factory in self._checkerFactoriesForOptHelpAuth():
+ self.authOutput.write(
+ formatString % (factory.authType, factory.argStringFormat))
+ self.authOutput.write('\n')
+ raise SystemExit(0)
+
+
+ def opt_help_auth_type(self, authType):
+ """
+ Show help for a particular authentication type.
+ """
+ try:
+ cf = findCheckerFactory(authType)
+ except InvalidAuthType:
+ raise usage.UsageError("Invalid auth type: %s" % authType)
+ self.authOutput.write("Usage: --auth %s[:ArgString]\n" % authType)
+ self.authOutput.write("ArgString format: %s\n" % cf.argStringFormat)
+ self.authOutput.write('\n')
+ for line in cf.authHelp.strip().splitlines():
+ self.authOutput.write(' %s\n' % line.rstrip())
+ self.authOutput.write('\n')
+ if not self.supportsCheckerFactory(cf):
+ self.authOutput.write(' %s\n' % notSupportedWarning)
+ self.authOutput.write('\n')
+ raise SystemExit(0)
diff --git a/twisted/enterprise/__init__.py b/twisted/enterprise/__init__.py
new file mode 100644
index 0000000..06c6a61
--- /dev/null
+++ b/twisted/enterprise/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Enterprise: database support for Twisted services.
+"""
+
+__all__ = ['adbapi']
diff --git a/twisted/enterprise/adbapi.py b/twisted/enterprise/adbapi.py
new file mode 100644
index 0000000..a361a20
--- /dev/null
+++ b/twisted/enterprise/adbapi.py
@@ -0,0 +1,483 @@
+# -*- test-case-name: twisted.test.test_adbapi -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An asynchronous mapping to U{DB-API 2.0<http://www.python.org/topics/database/DatabaseAPI-2.0.html>}.
+"""
+
+import sys
+
+from twisted.internet import threads
+from twisted.python import reflect, log
+from twisted.python.deprecate import deprecated
+from twisted.python.versions import Version
+
+
+
+class ConnectionLost(Exception):
+ """
+ This exception means that a db connection has been lost. Client code may
+ try again.
+ """
+
+
+
+class Connection(object):
+ """
+ A wrapper for a DB-API connection instance.
+
+ The wrapper passes almost everything to the wrapped connection and so has
+ the same API. However, the Connection knows about its pool and also
+ handle reconnecting should when the real connection dies.
+ """
+
+ def __init__(self, pool):
+ self._pool = pool
+ self._connection = None
+ self.reconnect()
+
+ def close(self):
+ # The way adbapi works right now means that closing a connection is
+ # a really bad thing as it leaves a dead connection associated with
+ # a thread in the thread pool.
+ # Really, I think closing a pooled connection should return it to the
+ # pool but that's handled by the runWithConnection method already so,
+ # rather than upsetting anyone by raising an exception, let's ignore
+ # the request
+ pass
+
+ def rollback(self):
+ if not self._pool.reconnect:
+ self._connection.rollback()
+ return
+
+ try:
+ self._connection.rollback()
+ curs = self._connection.cursor()
+ curs.execute(self._pool.good_sql)
+ curs.close()
+ self._connection.commit()
+ return
+ except:
+ log.err(None, "Rollback failed")
+
+ self._pool.disconnect(self._connection)
+
+ if self._pool.noisy:
+ log.msg("Connection lost.")
+
+ raise ConnectionLost()
+
+ def reconnect(self):
+ if self._connection is not None:
+ self._pool.disconnect(self._connection)
+ self._connection = self._pool.connect()
+
+ def __getattr__(self, name):
+ return getattr(self._connection, name)
+
+
+class Transaction:
+ """A lightweight wrapper for a DB-API 'cursor' object.
+
+ Relays attribute access to the DB cursor. That is, you can call
+ execute(), fetchall(), etc., and they will be called on the
+ underlying DB-API cursor object. Attributes will also be
+ retrieved from there.
+ """
+ _cursor = None
+
+ def __init__(self, pool, connection):
+ self._pool = pool
+ self._connection = connection
+ self.reopen()
+
+ def close(self):
+ _cursor = self._cursor
+ self._cursor = None
+ _cursor.close()
+
+ def reopen(self):
+ if self._cursor is not None:
+ self.close()
+
+ try:
+ self._cursor = self._connection.cursor()
+ return
+ except:
+ if not self._pool.reconnect:
+ raise
+ else:
+ log.err(None, "Cursor creation failed")
+
+ if self._pool.noisy:
+ log.msg('Connection lost, reconnecting')
+
+ self.reconnect()
+ self._cursor = self._connection.cursor()
+
+ def reconnect(self):
+ self._connection.reconnect()
+ self._cursor = None
+
+ def __getattr__(self, name):
+ return getattr(self._cursor, name)
+
+
+class ConnectionPool:
+ """
+ Represent a pool of connections to a DB-API 2.0 compliant database.
+
+ @ivar connectionFactory: factory for connections, default to L{Connection}.
+ @type connectionFactory: any callable.
+
+ @ivar transactionFactory: factory for transactions, default to
+ L{Transaction}.
+ @type transactionFactory: any callable
+
+ @ivar shutdownID: C{None} or a handle on the shutdown event trigger
+ which will be used to stop the connection pool workers when the
+ reactor stops.
+
+ @ivar _reactor: The reactor which will be used to schedule startup and
+ shutdown events.
+ @type _reactor: L{IReactorCore} provider
+ """
+
+ CP_ARGS = "min max name noisy openfun reconnect good_sql".split()
+
+ noisy = False # if true, generate informational log messages
+ min = 3 # minimum number of connections in pool
+ max = 5 # maximum number of connections in pool
+ name = None # Name to assign to thread pool for debugging
+ openfun = None # A function to call on new connections
+ reconnect = False # reconnect when connections fail
+ good_sql = 'select 1' # a query which should always succeed
+
+ running = False # true when the pool is operating
+ connectionFactory = Connection
+ transactionFactory = Transaction
+
+ # Initialize this to None so it's available in close() even if start()
+ # never runs.
+ shutdownID = None
+
+ def __init__(self, dbapiName, *connargs, **connkw):
+ """Create a new ConnectionPool.
+
+ Any positional or keyword arguments other than those documented here
+ are passed to the DB-API object when connecting. Use these arguments to
+ pass database names, usernames, passwords, etc.
+
+ @param dbapiName: an import string to use to obtain a DB-API compatible
+ module (e.g. 'pyPgSQL.PgSQL')
+
+ @param cp_min: the minimum number of connections in pool (default 3)
+
+ @param cp_max: the maximum number of connections in pool (default 5)
+
+ @param cp_noisy: generate informational log messages during operation
+ (default False)
+
+ @param cp_openfun: a callback invoked after every connect() on the
+ underlying DB-API object. The callback is passed a
+ new DB-API connection object. This callback can
+ setup per-connection state such as charset,
+ timezone, etc.
+
+ @param cp_reconnect: detect connections which have failed and reconnect
+ (default False). Failed connections may result in
+ ConnectionLost exceptions, which indicate the
+ query may need to be re-sent.
+
+ @param cp_good_sql: an sql query which should always succeed and change
+ no state (default 'select 1')
+
+ @param cp_reactor: use this reactor instead of the global reactor
+ (added in Twisted 10.2).
+ @type cp_reactor: L{IReactorCore} provider
+ """
+
+ self.dbapiName = dbapiName
+ self.dbapi = reflect.namedModule(dbapiName)
+
+ if getattr(self.dbapi, 'apilevel', None) != '2.0':
+ log.msg('DB API module not DB API 2.0 compliant.')
+
+ if getattr(self.dbapi, 'threadsafety', 0) < 1:
+ log.msg('DB API module not sufficiently thread-safe.')
+
+ reactor = connkw.pop('cp_reactor', None)
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+ self.connargs = connargs
+ self.connkw = connkw
+
+ for arg in self.CP_ARGS:
+ cp_arg = 'cp_%s' % arg
+ if connkw.has_key(cp_arg):
+ setattr(self, arg, connkw[cp_arg])
+ del connkw[cp_arg]
+
+ self.min = min(self.min, self.max)
+ self.max = max(self.min, self.max)
+
+ self.connections = {} # all connections, hashed on thread id
+
+ # these are optional so import them here
+ from twisted.python import threadpool
+ import thread
+
+ self.threadID = thread.get_ident
+ self.threadpool = threadpool.ThreadPool(self.min, self.max)
+ self.startID = self._reactor.callWhenRunning(self._start)
+
+
+ def _start(self):
+ self.startID = None
+ return self.start()
+
+
+ def start(self):
+ """
+ Start the connection pool.
+
+ If you are using the reactor normally, this function does *not*
+ need to be called.
+ """
+ if not self.running:
+ self.threadpool.start()
+ self.shutdownID = self._reactor.addSystemEventTrigger(
+ 'during', 'shutdown', self.finalClose)
+ self.running = True
+
+
+ def runWithConnection(self, func, *args, **kw):
+ """
+ Execute a function with a database connection and return the result.
+
+ @param func: A callable object of one argument which will be executed
+ in a thread with a connection from the pool. It will be passed as
+ its first argument a L{Connection} instance (whose interface is
+ mostly identical to that of a connection object for your DB-API
+ module of choice), and its results will be returned as a Deferred.
+ If the method raises an exception the transaction will be rolled
+ back. Otherwise, the transaction will be committed. B{Note} that
+ this function is B{not} run in the main thread: it must be
+ threadsafe.
+
+ @param *args: positional arguments to be passed to func
+
+ @param **kw: keyword arguments to be passed to func
+
+ @return: a Deferred which will fire the return value of
+ C{func(Transaction(...), *args, **kw)}, or a Failure.
+ """
+ from twisted.internet import reactor
+ return threads.deferToThreadPool(reactor, self.threadpool,
+ self._runWithConnection,
+ func, *args, **kw)
+
+
+ def _runWithConnection(self, func, *args, **kw):
+ conn = self.connectionFactory(self)
+ try:
+ result = func(conn, *args, **kw)
+ conn.commit()
+ return result
+ except:
+ excType, excValue, excTraceback = sys.exc_info()
+ try:
+ conn.rollback()
+ except:
+ log.err(None, "Rollback failed")
+ raise excType, excValue, excTraceback
+
+
+ def runInteraction(self, interaction, *args, **kw):
+ """
+ Interact with the database and return the result.
+
+ The 'interaction' is a callable object which will be executed
+ in a thread using a pooled connection. It will be passed an
+ L{Transaction} object as an argument (whose interface is
+ identical to that of the database cursor for your DB-API
+ module of choice), and its results will be returned as a
+ Deferred. If running the method raises an exception, the
+ transaction will be rolled back. If the method returns a
+ value, the transaction will be committed.
+
+ NOTE that the function you pass is *not* run in the main
+ thread: you may have to worry about thread-safety in the
+ function you pass to this if it tries to use non-local
+ objects.
+
+ @param interaction: a callable object whose first argument
+ is an L{adbapi.Transaction}.
+
+ @param *args: additional positional arguments to be passed
+ to interaction
+
+ @param **kw: keyword arguments to be passed to interaction
+
+ @return: a Deferred which will fire the return value of
+ 'interaction(Transaction(...), *args, **kw)', or a Failure.
+ """
+ from twisted.internet import reactor
+ return threads.deferToThreadPool(reactor, self.threadpool,
+ self._runInteraction,
+ interaction, *args, **kw)
+
+
+ def runQuery(self, *args, **kw):
+ """Execute an SQL query and return the result.
+
+ A DB-API cursor will will be invoked with cursor.execute(*args, **kw).
+ The exact nature of the arguments will depend on the specific flavor
+ of DB-API being used, but the first argument in *args be an SQL
+ statement. The result of a subsequent cursor.fetchall() will be
+ fired to the Deferred which is returned. If either the 'execute' or
+ 'fetchall' methods raise an exception, the transaction will be rolled
+ back and a Failure returned.
+
+ The *args and **kw arguments will be passed to the DB-API cursor's
+ 'execute' method.
+
+ @return: a Deferred which will fire the return value of a DB-API
+ cursor's 'fetchall' method, or a Failure.
+ """
+ return self.runInteraction(self._runQuery, *args, **kw)
+
+
+ def runOperation(self, *args, **kw):
+ """Execute an SQL query and return None.
+
+ A DB-API cursor will will be invoked with cursor.execute(*args, **kw).
+ The exact nature of the arguments will depend on the specific flavor
+ of DB-API being used, but the first argument in *args will be an SQL
+ statement. This method will not attempt to fetch any results from the
+ query and is thus suitable for INSERT, DELETE, and other SQL statements
+ which do not return values. If the 'execute' method raises an
+ exception, the transaction will be rolled back and a Failure returned.
+
+ The args and kw arguments will be passed to the DB-API cursor's
+ 'execute' method.
+
+ return: a Deferred which will fire None or a Failure.
+ """
+ return self.runInteraction(self._runOperation, *args, **kw)
+
+
+ def close(self):
+ """
+ Close all pool connections and shutdown the pool.
+ """
+ if self.shutdownID:
+ self._reactor.removeSystemEventTrigger(self.shutdownID)
+ self.shutdownID = None
+ if self.startID:
+ self._reactor.removeSystemEventTrigger(self.startID)
+ self.startID = None
+ self.finalClose()
+
+ def finalClose(self):
+ """This should only be called by the shutdown trigger."""
+
+ self.shutdownID = None
+ self.threadpool.stop()
+ self.running = False
+ for conn in self.connections.values():
+ self._close(conn)
+ self.connections.clear()
+
+ def connect(self):
+ """Return a database connection when one becomes available.
+
+ This method blocks and should be run in a thread from the internal
+ threadpool. Don't call this method directly from non-threaded code.
+ Using this method outside the external threadpool may exceed the
+ maximum number of connections in the pool.
+
+ @return: a database connection from the pool.
+ """
+
+ tid = self.threadID()
+ conn = self.connections.get(tid)
+ if conn is None:
+ if self.noisy:
+ log.msg('adbapi connecting: %s %s%s' % (self.dbapiName,
+ self.connargs or '',
+ self.connkw or ''))
+ conn = self.dbapi.connect(*self.connargs, **self.connkw)
+ if self.openfun != None:
+ self.openfun(conn)
+ self.connections[tid] = conn
+ return conn
+
+ def disconnect(self, conn):
+ """Disconnect a database connection associated with this pool.
+
+ Note: This function should only be used by the same thread which
+ called connect(). As with connect(), this function is not used
+ in normal non-threaded twisted code.
+ """
+ tid = self.threadID()
+ if conn is not self.connections.get(tid):
+ raise Exception("wrong connection for thread")
+ if conn is not None:
+ self._close(conn)
+ del self.connections[tid]
+
+
+ def _close(self, conn):
+ if self.noisy:
+ log.msg('adbapi closing: %s' % (self.dbapiName,))
+ try:
+ conn.close()
+ except:
+ log.err(None, "Connection close failed")
+
+
+ def _runInteraction(self, interaction, *args, **kw):
+ conn = self.connectionFactory(self)
+ trans = self.transactionFactory(self, conn)
+ try:
+ result = interaction(trans, *args, **kw)
+ trans.close()
+ conn.commit()
+ return result
+ except:
+ excType, excValue, excTraceback = sys.exc_info()
+ try:
+ conn.rollback()
+ except:
+ log.err(None, "Rollback failed")
+ raise excType, excValue, excTraceback
+
+
+ def _runQuery(self, trans, *args, **kw):
+ trans.execute(*args, **kw)
+ return trans.fetchall()
+
+ def _runOperation(self, trans, *args, **kw):
+ trans.execute(*args, **kw)
+
+ def __getstate__(self):
+ return {'dbapiName': self.dbapiName,
+ 'min': self.min,
+ 'max': self.max,
+ 'noisy': self.noisy,
+ 'reconnect': self.reconnect,
+ 'good_sql': self.good_sql,
+ 'connargs': self.connargs,
+ 'connkw': self.connkw}
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self.__init__(self.dbapiName, *self.connargs, **self.connkw)
+
+
+__all__ = ['Transaction', 'ConnectionPool']
diff --git a/twisted/internet/__init__.py b/twisted/internet/__init__.py
new file mode 100644
index 0000000..a3d851d
--- /dev/null
+++ b/twisted/internet/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted Internet: Asynchronous I/O and Events.
+
+Twisted Internet is a collection of compatible event-loops for Python. It contains
+the code to dispatch events to interested observers and a portable API so that
+observers need not care about which event loop is running. Thus, it is possible
+to use the same code for different loops, from Twisted's basic, yet portable,
+select-based loop to the loops of various GUI toolkits like GTK+ or Tk.
+"""
diff --git a/twisted/internet/_baseprocess.py b/twisted/internet/_baseprocess.py
new file mode 100644
index 0000000..0a06259
--- /dev/null
+++ b/twisted/internet/_baseprocess.py
@@ -0,0 +1,62 @@
+# -*- test-case-name: twisted.test.test_process -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cross-platform process-related functionality used by different
+L{IReactorProcess} implementations.
+"""
+
+from twisted.python.reflect import qual
+from twisted.python.deprecate import getWarningMethod
+from twisted.python.failure import Failure
+from twisted.python.log import err
+from twisted.persisted.styles import Ephemeral
+
+_missingProcessExited = ("Since Twisted 8.2, IProcessProtocol.processExited "
+ "is required. %s must implement it.")
+
+class BaseProcess(Ephemeral):
+ pid = None
+ status = None
+ lostProcess = 0
+ proto = None
+
+ def __init__(self, protocol):
+ self.proto = protocol
+
+
+ def _callProcessExited(self, reason):
+ default = object()
+ processExited = getattr(self.proto, 'processExited', default)
+ if processExited is default:
+ getWarningMethod()(
+ _missingProcessExited % (qual(self.proto.__class__),),
+ DeprecationWarning, stacklevel=0)
+ else:
+ processExited(Failure(reason))
+
+
+ def processEnded(self, status):
+ """
+ This is called when the child terminates.
+ """
+ self.status = status
+ self.lostProcess += 1
+ self.pid = None
+ self._callProcessExited(self._getReason(status))
+ self.maybeCallProcessEnded()
+
+
+ def maybeCallProcessEnded(self):
+ """
+ Call processEnded on protocol after final cleanup.
+ """
+ if self.proto is not None:
+ reason = self._getReason(self.status)
+ proto = self.proto
+ self.proto = None
+ try:
+ proto.processEnded(Failure(reason))
+ except:
+ err(None, "unexpected error in processEnded")
diff --git a/twisted/internet/_dumbwin32proc.py b/twisted/internet/_dumbwin32proc.py
new file mode 100644
index 0000000..0df82ae
--- /dev/null
+++ b/twisted/internet/_dumbwin32proc.py
@@ -0,0 +1,388 @@
+# -*- test-case-name: twisted.test.test_process -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+http://isometric.sixsided.org/_/gates_in_the_head/
+"""
+
+import os
+
+# Win32 imports
+import win32api
+import win32con
+import win32event
+import win32file
+import win32pipe
+import win32process
+import win32security
+
+import pywintypes
+
+# security attributes for pipes
+PIPE_ATTRS_INHERITABLE = win32security.SECURITY_ATTRIBUTES()
+PIPE_ATTRS_INHERITABLE.bInheritHandle = 1
+
+from zope.interface import implements
+from twisted.internet.interfaces import IProcessTransport, IConsumer, IProducer
+
+from twisted.python.win32 import quoteArguments
+
+from twisted.internet import error
+
+from twisted.internet import _pollingfile
+from twisted.internet._baseprocess import BaseProcess
+
+def debug(msg):
+ import sys
+ print msg
+ sys.stdout.flush()
+
+class _Reaper(_pollingfile._PollableResource):
+
+ def __init__(self, proc):
+ self.proc = proc
+
+ def checkWork(self):
+ if win32event.WaitForSingleObject(self.proc.hProcess, 0) != win32event.WAIT_OBJECT_0:
+ return 0
+ exitCode = win32process.GetExitCodeProcess(self.proc.hProcess)
+ self.deactivate()
+ self.proc.processEnded(exitCode)
+ return 0
+
+
+def _findShebang(filename):
+ """
+ Look for a #! line, and return the value following the #! if one exists, or
+ None if this file is not a script.
+
+ I don't know if there are any conventions for quoting in Windows shebang
+ lines, so this doesn't support any; therefore, you may not pass any
+ arguments to scripts invoked as filters. That's probably wrong, so if
+ somebody knows more about the cultural expectations on Windows, please feel
+ free to fix.
+
+ This shebang line support was added in support of the CGI tests;
+ appropriately enough, I determined that shebang lines are culturally
+ accepted in the Windows world through this page::
+
+ http://www.cgi101.com/learn/connect/winxp.html
+
+ @param filename: str representing a filename
+
+ @return: a str representing another filename.
+ """
+ f = file(filename, 'rU')
+ if f.read(2) == '#!':
+ exe = f.readline(1024).strip('\n')
+ return exe
+
+def _invalidWin32App(pywinerr):
+ """
+ Determine if a pywintypes.error is telling us that the given process is
+ 'not a valid win32 application', i.e. not a PE format executable.
+
+ @param pywinerr: a pywintypes.error instance raised by CreateProcess
+
+ @return: a boolean
+ """
+
+ # Let's do this better in the future, but I have no idea what this error
+ # is; MSDN doesn't mention it, and there is no symbolic constant in
+ # win32process module that represents 193.
+
+ return pywinerr.args[0] == 193
+
+class Process(_pollingfile._PollingTimer, BaseProcess):
+ """A process that integrates with the Twisted event loop.
+
+ If your subprocess is a python program, you need to:
+
+ - Run python.exe with the '-u' command line option - this turns on
+ unbuffered I/O. Buffering stdout/err/in can cause problems, see e.g.
+ http://support.microsoft.com/default.aspx?scid=kb;EN-US;q1903
+
+ - If you don't want Windows messing with data passed over
+ stdin/out/err, set the pipes to be in binary mode::
+
+ import os, sys, mscvrt
+ msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+ msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
+
+ """
+ implements(IProcessTransport, IConsumer, IProducer)
+
+ closedNotifies = 0
+
+ def __init__(self, reactor, protocol, command, args, environment, path):
+ """
+ Create a new child process.
+ """
+ _pollingfile._PollingTimer.__init__(self, reactor)
+ BaseProcess.__init__(self, protocol)
+
+ # security attributes for pipes
+ sAttrs = win32security.SECURITY_ATTRIBUTES()
+ sAttrs.bInheritHandle = 1
+
+ # create the pipes which will connect to the secondary process
+ self.hStdoutR, hStdoutW = win32pipe.CreatePipe(sAttrs, 0)
+ self.hStderrR, hStderrW = win32pipe.CreatePipe(sAttrs, 0)
+ hStdinR, self.hStdinW = win32pipe.CreatePipe(sAttrs, 0)
+
+ win32pipe.SetNamedPipeHandleState(self.hStdinW,
+ win32pipe.PIPE_NOWAIT,
+ None,
+ None)
+
+ # set the info structure for the new process.
+ StartupInfo = win32process.STARTUPINFO()
+ StartupInfo.hStdOutput = hStdoutW
+ StartupInfo.hStdError = hStderrW
+ StartupInfo.hStdInput = hStdinR
+ StartupInfo.dwFlags = win32process.STARTF_USESTDHANDLES
+
+ # Create new handles whose inheritance property is false
+ currentPid = win32api.GetCurrentProcess()
+
+ tmp = win32api.DuplicateHandle(currentPid, self.hStdoutR, currentPid, 0, 0,
+ win32con.DUPLICATE_SAME_ACCESS)
+ win32file.CloseHandle(self.hStdoutR)
+ self.hStdoutR = tmp
+
+ tmp = win32api.DuplicateHandle(currentPid, self.hStderrR, currentPid, 0, 0,
+ win32con.DUPLICATE_SAME_ACCESS)
+ win32file.CloseHandle(self.hStderrR)
+ self.hStderrR = tmp
+
+ tmp = win32api.DuplicateHandle(currentPid, self.hStdinW, currentPid, 0, 0,
+ win32con.DUPLICATE_SAME_ACCESS)
+ win32file.CloseHandle(self.hStdinW)
+ self.hStdinW = tmp
+
+ # Add the specified environment to the current environment - this is
+ # necessary because certain operations are only supported on Windows
+ # if certain environment variables are present.
+
+ env = os.environ.copy()
+ env.update(environment or {})
+
+ cmdline = quoteArguments(args)
+ # TODO: error detection here. See #2787 and #4184.
+ def doCreate():
+ self.hProcess, self.hThread, self.pid, dwTid = win32process.CreateProcess(
+ command, cmdline, None, None, 1, 0, env, path, StartupInfo)
+ try:
+ try:
+ doCreate()
+ except TypeError, e:
+ # win32process.CreateProcess cannot deal with mixed
+ # str/unicode environment, so we make it all Unicode
+ if e.args != ('All dictionary items must be strings, or '
+ 'all must be unicode',):
+ raise
+ newenv = {}
+ for key, value in env.items():
+ newenv[unicode(key)] = unicode(value)
+ env = newenv
+ doCreate()
+ except pywintypes.error, pwte:
+ if not _invalidWin32App(pwte):
+ # This behavior isn't _really_ documented, but let's make it
+ # consistent with the behavior that is documented.
+ raise OSError(pwte)
+ else:
+ # look for a shebang line. Insert the original 'command'
+ # (actually a script) into the new arguments list.
+ sheb = _findShebang(command)
+ if sheb is None:
+ raise OSError(
+ "%r is neither a Windows executable, "
+ "nor a script with a shebang line" % command)
+ else:
+ args = list(args)
+ args.insert(0, command)
+ cmdline = quoteArguments(args)
+ origcmd = command
+ command = sheb
+ try:
+ # Let's try again.
+ doCreate()
+ except pywintypes.error, pwte2:
+ # d'oh, failed again!
+ if _invalidWin32App(pwte2):
+ raise OSError(
+ "%r has an invalid shebang line: "
+ "%r is not a valid executable" % (
+ origcmd, sheb))
+ raise OSError(pwte2)
+
+ # close handles which only the child will use
+ win32file.CloseHandle(hStderrW)
+ win32file.CloseHandle(hStdoutW)
+ win32file.CloseHandle(hStdinR)
+
+ # set up everything
+ self.stdout = _pollingfile._PollableReadPipe(
+ self.hStdoutR,
+ lambda data: self.proto.childDataReceived(1, data),
+ self.outConnectionLost)
+
+ self.stderr = _pollingfile._PollableReadPipe(
+ self.hStderrR,
+ lambda data: self.proto.childDataReceived(2, data),
+ self.errConnectionLost)
+
+ self.stdin = _pollingfile._PollableWritePipe(
+ self.hStdinW, self.inConnectionLost)
+
+ for pipewatcher in self.stdout, self.stderr, self.stdin:
+ self._addPollableResource(pipewatcher)
+
+
+ # notify protocol
+ self.proto.makeConnection(self)
+
+ self._addPollableResource(_Reaper(self))
+
+
+ def signalProcess(self, signalID):
+ if self.pid is None:
+ raise error.ProcessExitedAlready()
+ if signalID in ("INT", "TERM", "KILL"):
+ win32process.TerminateProcess(self.hProcess, 1)
+
+
+ def _getReason(self, status):
+ if status == 0:
+ return error.ProcessDone(status)
+ return error.ProcessTerminated(status)
+
+
+ def write(self, data):
+ """
+ Write data to the process' stdin.
+
+ @type data: C{str}
+ """
+ self.stdin.write(data)
+
+
+ def writeSequence(self, seq):
+ """
+ Write data to the process' stdin.
+
+ @type data: C{list} of C{str}
+ """
+ self.stdin.writeSequence(seq)
+
+
+ def writeToChild(self, fd, data):
+ """
+ Similar to L{ITransport.write} but also allows the file descriptor in
+ the child process which will receive the bytes to be specified.
+
+ This implementation is limited to writing to the child's standard input.
+
+ @param fd: The file descriptor to which to write. Only stdin (C{0}) is
+ supported.
+ @type fd: C{int}
+
+ @param data: The bytes to write.
+ @type data: C{str}
+
+ @return: C{None}
+
+ @raise KeyError: If C{fd} is anything other than the stdin file
+ descriptor (C{0}).
+ """
+ if fd == 0:
+ self.stdin.write(data)
+ else:
+ raise KeyError(fd)
+
+
+ def closeChildFD(self, fd):
+ if fd == 0:
+ self.closeStdin()
+ elif fd == 1:
+ self.closeStdout()
+ elif fd == 2:
+ self.closeStderr()
+ else:
+ raise NotImplementedError("Only standard-IO file descriptors available on win32")
+
+ def closeStdin(self):
+ """Close the process' stdin.
+ """
+ self.stdin.close()
+
+ def closeStderr(self):
+ self.stderr.close()
+
+ def closeStdout(self):
+ self.stdout.close()
+
+ def loseConnection(self):
+ """Close the process' stdout, in and err."""
+ self.closeStdin()
+ self.closeStdout()
+ self.closeStderr()
+
+
+ def outConnectionLost(self):
+ self.proto.childConnectionLost(1)
+ self.connectionLostNotify()
+
+
+ def errConnectionLost(self):
+ self.proto.childConnectionLost(2)
+ self.connectionLostNotify()
+
+
+ def inConnectionLost(self):
+ self.proto.childConnectionLost(0)
+ self.connectionLostNotify()
+
+
+ def connectionLostNotify(self):
+ """
+ Will be called 3 times, by stdout/err threads and process handle.
+ """
+ self.closedNotifies += 1
+ self.maybeCallProcessEnded()
+
+
+ def maybeCallProcessEnded(self):
+ if self.closedNotifies == 3 and self.lostProcess:
+ win32file.CloseHandle(self.hProcess)
+ win32file.CloseHandle(self.hThread)
+ self.hProcess = None
+ self.hThread = None
+ BaseProcess.maybeCallProcessEnded(self)
+
+
+ # IConsumer
+ def registerProducer(self, producer, streaming):
+ self.stdin.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ self.stdin.unregisterProducer()
+
+ # IProducer
+ def pauseProducing(self):
+ self._pause()
+
+ def resumeProducing(self):
+ self._unpause()
+
+ def stopProducing(self):
+ self.loseConnection()
+
+ def __repr__(self):
+ """
+ Return a string representation of the process.
+ """
+ return "<%s pid=%s>" % (self.__class__.__name__, self.pid)
diff --git a/twisted/internet/_glibbase.py b/twisted/internet/_glibbase.py
new file mode 100644
index 0000000..bfb84ca
--- /dev/null
+++ b/twisted/internet/_glibbase.py
@@ -0,0 +1,387 @@
+# -*- test-case-name: twisted.internet.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides base support for Twisted to interact with the glib/gtk
+mainloops.
+
+The classes in this module should not be used directly, but rather you should
+import gireactor or gtk3reactor for GObject Introspection based applications,
+or glib2reactor or gtk2reactor for applications using legacy static bindings.
+"""
+
+import sys
+
+from twisted.internet import base, posixbase, selectreactor
+from twisted.internet.interfaces import IReactorFDSet
+from twisted.python import log
+from twisted.python.compat import set
+from zope.interface import implements
+
+
+def ensureNotImported(moduleNames, errorMessage, preventImports=[]):
+ """
+ Check whether the given modules were imported, and if requested, ensure
+ they will not be importable in the future.
+
+ @param moduleNames: A list of module names we make sure aren't imported.
+ @type moduleNames: C{list} of C{str}
+
+ @param preventImports: A list of module name whose future imports should
+ be prevented.
+ @type preventImports: C{list} of C{str}
+
+ @param errorMessage: Message to use when raising an C{ImportError}.
+ @type errorMessage: C{str}
+
+ @raises: C{ImportError} with given error message if a given module name
+ has already been imported.
+ """
+ for name in moduleNames:
+ if sys.modules.get(name) is not None:
+ raise ImportError(errorMessage)
+
+ # Disable module imports to avoid potential problems.
+ for name in preventImports:
+ sys.modules[name] = None
+
+
+
+class GlibWaker(posixbase._UnixWaker):
+ """
+ Run scheduled events after waking up.
+ """
+
+ def doRead(self):
+ posixbase._UnixWaker.doRead(self)
+ self.reactor._simulate()
+
+
+
+class GlibReactorBase(posixbase.PosixReactorBase, posixbase._PollLikeMixin):
+ """
+ Base class for GObject event loop reactors.
+
+ Notification for I/O events (reads and writes on file descriptors) is done
+ by the the gobject-based event loop. File descriptors are registered with
+ gobject with the appropriate flags for read/write/disconnect notification.
+
+ Time-based events, the results of C{callLater} and C{callFromThread}, are
+ handled differently. Rather than registering each event with gobject, a
+ single gobject timeout is registered for the earliest scheduled event, the
+ output of C{reactor.timeout()}. For example, if there are timeouts in 1, 2
+ and 3.4 seconds, a single timeout is registered for 1 second in the
+ future. When this timeout is hit, C{_simulate} is called, which calls the
+ appropriate Twisted-level handlers, and a new timeout is added to gobject
+ by the C{_reschedule} method.
+
+ To handle C{callFromThread} events, we use a custom waker that calls
+ C{_simulate} whenever it wakes up.
+
+ @ivar _sources: A dictionary mapping L{FileDescriptor} instances to
+ GSource handles.
+
+ @ivar _reads: A set of L{FileDescriptor} instances currently monitored for
+ reading.
+
+ @ivar _writes: A set of L{FileDescriptor} instances currently monitored for
+ writing.
+
+ @ivar _simtag: A GSource handle for the next L{simulate} call.
+ """
+ implements(IReactorFDSet)
+
+ # Install a waker that knows it needs to call C{_simulate} in order to run
+ # callbacks queued from a thread:
+ _wakerFactory = GlibWaker
+
+ def __init__(self, glib_module, gtk_module, useGtk=False):
+ self._simtag = None
+ self._reads = set()
+ self._writes = set()
+ self._sources = {}
+ self._glib = glib_module
+ self._gtk = gtk_module
+ posixbase.PosixReactorBase.__init__(self)
+
+ self._source_remove = self._glib.source_remove
+ self._timeout_add = self._glib.timeout_add
+
+ def _mainquit():
+ if self._gtk.main_level():
+ self._gtk.main_quit()
+
+ if useGtk:
+ self._pending = self._gtk.events_pending
+ self._iteration = self._gtk.main_iteration_do
+ self._crash = _mainquit
+ self._run = self._gtk.main
+ else:
+ self.context = self._glib.main_context_default()
+ self._pending = self.context.pending
+ self._iteration = self.context.iteration
+ self.loop = self._glib.MainLoop()
+ self._crash = lambda: self._glib.idle_add(self.loop.quit)
+ self._run = self.loop.run
+
+
+ def _handleSignals(self):
+ # First, install SIGINT and friends:
+ base._SignalReactorMixin._handleSignals(self)
+ # Next, since certain versions of gtk will clobber our signal handler,
+ # set all signal handlers again after the event loop has started to
+ # ensure they're *really* set. We don't call this twice so we don't
+ # leak file descriptors created in the SIGCHLD initialization:
+ self.callLater(0, posixbase.PosixReactorBase._handleSignals, self)
+
+
+ # The input_add function in pygtk1 checks for objects with a
+ # 'fileno' method and, if present, uses the result of that method
+ # as the input source. The pygtk2 input_add does not do this. The
+ # function below replicates the pygtk1 functionality.
+
+ # In addition, pygtk maps gtk.input_add to _gobject.io_add_watch, and
+ # g_io_add_watch() takes different condition bitfields than
+ # gtk_input_add(). We use g_io_add_watch() here in case pygtk fixes this
+ # bug.
+ def input_add(self, source, condition, callback):
+ if hasattr(source, 'fileno'):
+ # handle python objects
+ def wrapper(ignored, condition):
+ return callback(source, condition)
+ fileno = source.fileno()
+ else:
+ fileno = source
+ wrapper = callback
+ return self._glib.io_add_watch(
+ fileno, condition, wrapper,
+ priority=self._glib.PRIORITY_DEFAULT_IDLE)
+
+
+ def _ioEventCallback(self, source, condition):
+ """
+ Called by event loop when an I/O event occurs.
+ """
+ log.callWithLogger(
+ source, self._doReadOrWrite, source, source, condition)
+ return True # True = don't auto-remove the source
+
+
+ def _add(self, source, primary, other, primaryFlag, otherFlag):
+ """
+ Add the given L{FileDescriptor} for monitoring either for reading or
+ writing. If the file is already monitored for the other operation, we
+ delete the previous registration and re-register it for both reading
+ and writing.
+ """
+ if source in primary:
+ return
+ flags = primaryFlag
+ if source in other:
+ self._source_remove(self._sources[source])
+ flags |= otherFlag
+ self._sources[source] = self.input_add(
+ source, flags, self._ioEventCallback)
+ primary.add(source)
+
+
+ def addReader(self, reader):
+ """
+ Add a L{FileDescriptor} for monitoring of data available to read.
+ """
+ self._add(reader, self._reads, self._writes,
+ self.INFLAGS, self.OUTFLAGS)
+
+
+ def addWriter(self, writer):
+ """
+ Add a L{FileDescriptor} for monitoring ability to write data.
+ """
+ self._add(writer, self._writes, self._reads,
+ self.OUTFLAGS, self.INFLAGS)
+
+
+ def getReaders(self):
+ """
+ Retrieve the list of current L{FileDescriptor} monitored for reading.
+ """
+ return list(self._reads)
+
+
+ def getWriters(self):
+ """
+ Retrieve the list of current L{FileDescriptor} monitored for writing.
+ """
+ return list(self._writes)
+
+
+ def removeAll(self):
+ """
+ Remove monitoring for all registered L{FileDescriptor}s.
+ """
+ return self._removeAll(self._reads, self._writes)
+
+
+ def _remove(self, source, primary, other, flags):
+ """
+ Remove monitoring the given L{FileDescriptor} for either reading or
+ writing. If it's still monitored for the other operation, we
+ re-register the L{FileDescriptor} for only that operation.
+ """
+ if source not in primary:
+ return
+ self._source_remove(self._sources[source])
+ primary.remove(source)
+ if source in other:
+ self._sources[source] = self.input_add(
+ source, flags, self._ioEventCallback)
+ else:
+ self._sources.pop(source)
+
+
+ def removeReader(self, reader):
+ """
+ Stop monitoring the given L{FileDescriptor} for reading.
+ """
+ self._remove(reader, self._reads, self._writes, self.OUTFLAGS)
+
+
+ def removeWriter(self, writer):
+ """
+ Stop monitoring the given L{FileDescriptor} for writing.
+ """
+ self._remove(writer, self._writes, self._reads, self.INFLAGS)
+
+
+ def iterate(self, delay=0):
+ """
+ One iteration of the event loop, for trial's use.
+
+ This is not used for actual reactor runs.
+ """
+ self.runUntilCurrent()
+ while self._pending():
+ self._iteration(0)
+
+
+ def crash(self):
+ """
+ Crash the reactor.
+ """
+ posixbase.PosixReactorBase.crash(self)
+ self._crash()
+
+
+ def stop(self):
+ """
+ Stop the reactor.
+ """
+ posixbase.PosixReactorBase.stop(self)
+ # The base implementation only sets a flag, to ensure shutting down is
+ # not reentrant. Unfortunately, this flag is not meaningful to the
+ # gobject event loop. We therefore call wakeUp() to ensure the event
+ # loop will call back into Twisted once this iteration is done. This
+ # will result in self.runUntilCurrent() being called, where the stop
+ # flag will trigger the actual shutdown process, eventually calling
+ # crash() which will do the actual gobject event loop shutdown.
+ self.wakeUp()
+
+
+ def run(self, installSignalHandlers=True):
+ """
+ Run the reactor.
+ """
+ self.callWhenRunning(self._reschedule)
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ if self._started:
+ self._run()
+
+
+ def callLater(self, *args, **kwargs):
+ """
+ Schedule a C{DelayedCall}.
+ """
+ result = posixbase.PosixReactorBase.callLater(self, *args, **kwargs)
+ # Make sure we'll get woken up at correct time to handle this new
+ # scheduled call:
+ self._reschedule()
+ return result
+
+
+ def _reschedule(self):
+ """
+ Schedule a glib timeout for C{_simulate}.
+ """
+ if self._simtag is not None:
+ self._source_remove(self._simtag)
+ self._simtag = None
+ timeout = self.timeout()
+ if timeout is not None:
+ self._simtag = self._timeout_add(
+ int(timeout * 1000), self._simulate,
+ priority=self._glib.PRIORITY_DEFAULT_IDLE)
+
+
+ def _simulate(self):
+ """
+ Run timers, and then reschedule glib timeout for next scheduled event.
+ """
+ self.runUntilCurrent()
+ self._reschedule()
+
+
+
+class PortableGlibReactorBase(selectreactor.SelectReactor):
+ """
+ Base class for GObject event loop reactors that works on Windows.
+
+ Sockets aren't supported by GObject's input_add on Win32.
+ """
+ def __init__(self, glib_module, gtk_module, useGtk=False):
+ self._simtag = None
+ self._glib = glib_module
+ self._gtk = gtk_module
+ selectreactor.SelectReactor.__init__(self)
+
+ self._source_remove = self._glib.source_remove
+ self._timeout_add = self._glib.timeout_add
+
+ def _mainquit():
+ if self._gtk.main_level():
+ self._gtk.main_quit()
+
+ if useGtk:
+ self._crash = _mainquit
+ self._run = self._gtk.main
+ else:
+ self.loop = self._glib.MainLoop()
+ self._crash = lambda: self._glib.idle_add(self.loop.quit)
+ self._run = self.loop.run
+
+
+ def crash(self):
+ selectreactor.SelectReactor.crash(self)
+ self._crash()
+
+
+ def run(self, installSignalHandlers=True):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ self._timeout_add(0, self.simulate)
+ if self._started:
+ self._run()
+
+
+ def simulate(self):
+ """
+ Run simulation loops and reschedule callbacks.
+ """
+ if self._simtag is not None:
+ self._source_remove(self._simtag)
+ self.iterate()
+ timeout = min(self.timeout(), 0.01)
+ if timeout is None:
+ timeout = 0.01
+ self._simtag = self._timeout_add(
+ int(timeout * 1000), self.simulate,
+ priority=self._glib.PRIORITY_DEFAULT_IDLE)
diff --git a/twisted/internet/_newtls.py b/twisted/internet/_newtls.py
new file mode 100644
index 0000000..a8c3479
--- /dev/null
+++ b/twisted/internet/_newtls.py
@@ -0,0 +1,270 @@
+# -*- test-case-name: twisted.test.test_ssl -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module implements memory BIO based TLS support. It is the preferred
+implementation and will be used whenever pyOpenSSL 0.10 or newer is installed
+(whenever L{twisted.protocols.tls} is importable).
+
+@since: 11.1
+"""
+
+from zope.interface import implements
+from zope.interface import directlyProvides
+
+from twisted.internet.interfaces import ITLSTransport, ISSLTransport
+from twisted.internet.abstract import FileDescriptor
+from twisted.internet._ssl import _TLSDelayed
+
+from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol
+
+
+class _BypassTLS(object):
+ """
+ L{_BypassTLS} is used as the transport object for the TLS protocol object
+ used to implement C{startTLS}. Its methods skip any TLS logic which
+ C{startTLS} enables.
+
+ @ivar _base: A transport class L{_BypassTLS} has been mixed in with to which
+ methods will be forwarded. This class is only responsible for sending
+ bytes over the connection, not doing TLS.
+
+ @ivar _connection: A L{Connection} which TLS has been started on which will
+ be proxied to by this object. Any method which has its behavior
+ altered after C{startTLS} will be skipped in favor of the base class's
+ implementation. This allows the TLS protocol object to have direct
+ access to the transport, necessary to actually implement TLS.
+ """
+ def __init__(self, base, connection):
+ self._base = base
+ self._connection = connection
+
+
+ def __getattr__(self, name):
+ """
+ Forward any extra attribute access to the original transport object.
+ For example, this exposes C{getHost}, the behavior of which does not
+ change after TLS is enabled.
+ """
+ return getattr(self._connection, name)
+
+
+ def write(self, data):
+ """
+ Write some bytes directly to the connection.
+ """
+ return self._base.write(self._connection, data)
+
+
+ def writeSequence(self, iovec):
+ """
+ Write a some bytes directly to the connection.
+ """
+ return self._base.writeSequence(self._connection, iovec)
+
+
+ def loseConnection(self, *args, **kwargs):
+ """
+ Close the underlying connection.
+ """
+ return self._base.loseConnection(self._connection, *args, **kwargs)
+
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register a producer with the underlying connection.
+ """
+ return self._base.registerProducer(self._connection, producer, streaming)
+
+
+ def unregisterProducer(self):
+ """
+ Unregister a producer with the underlying connection.
+ """
+ return self._base.unregisterProducer(self._connection)
+
+
+
+def startTLS(transport, contextFactory, normal, bypass):
+ """
+ Add a layer of SSL to a transport.
+
+ @param transport: The transport which will be modified. This can either by
+ a L{FileDescriptor<twisted.internet.abstract.FileDescriptor>} or a
+ L{FileHandle<twisted.internet.iocpreactor.abstract.FileHandle>}. The
+ actual requirements of this instance are that it have:
+
+ - a C{_tlsClientDefault} attribute indicating whether the transport is
+ a client (C{True}) or a server (C{False})
+ - a settable C{TLS} attribute which can be used to mark the fact
+ that SSL has been started
+ - settable C{getHandle} and C{getPeerCertificate} attributes so
+ these L{ISSLTransport} methods can be added to it
+ - a C{protocol} attribute referring to the L{IProtocol} currently
+ connected to the transport, which can also be set to a new
+ L{IProtocol} for the transport to deliver data to
+
+ @param contextFactory: An SSL context factory defining SSL parameters for
+ the new SSL layer.
+ @type contextFactory: L{twisted.internet.ssl.ContextFactory}
+
+ @param normal: A flag indicating whether SSL will go in the same direction
+ as the underlying transport goes. That is, if the SSL client will be
+ the underlying client and the SSL server will be the underlying server.
+ C{True} means it is the same, C{False} means they are switched.
+ @type param: L{bool}
+
+ @param bypass: A transport base class to call methods on to bypass the new
+ SSL layer (so that the SSL layer itself can send its bytes).
+ @type bypass: L{type}
+ """
+ # Figure out which direction the SSL goes in. If normal is True,
+ # we'll go in the direction indicated by the subclass. Otherwise,
+ # we'll go the other way (client = not normal ^ _tlsClientDefault,
+ # in other words).
+ if normal:
+ client = transport._tlsClientDefault
+ else:
+ client = not transport._tlsClientDefault
+
+ # If we have a producer, unregister it, and then re-register it below once
+ # we've switched to TLS mode, so it gets hooked up correctly:
+ producer, streaming = None, None
+ if transport.producer is not None:
+ producer, streaming = transport.producer, transport.streamingProducer
+ transport.unregisterProducer()
+
+ tlsFactory = TLSMemoryBIOFactory(contextFactory, client, None)
+ tlsProtocol = TLSMemoryBIOProtocol(tlsFactory, transport.protocol, False)
+ transport.protocol = tlsProtocol
+
+ transport.getHandle = tlsProtocol.getHandle
+ transport.getPeerCertificate = tlsProtocol.getPeerCertificate
+
+ # Mark the transport as secure.
+ directlyProvides(transport, ISSLTransport)
+
+ # Remember we did this so that write and writeSequence can send the
+ # data to the right place.
+ transport.TLS = True
+
+ # Hook it up
+ transport.protocol.makeConnection(_BypassTLS(bypass, transport))
+
+ # Restore producer if necessary:
+ if producer:
+ transport.registerProducer(producer, streaming)
+
+
+
+class ConnectionMixin(object):
+ """
+ A mixin for L{twisted.internet.abstract.FileDescriptor} which adds an
+ L{ITLSTransport} implementation.
+
+ @ivar TLS: A flag indicating whether TLS is currently in use on this
+ transport. This is not a good way for applications to check for TLS,
+ instead use L{ISSLTransport.providedBy}.
+ """
+ implements(ITLSTransport)
+
+ TLS = False
+
+ def startTLS(self, ctx, normal=True):
+ """
+ @see: L{ITLSTransport.startTLS}
+ """
+ startTLS(self, ctx, normal, FileDescriptor)
+
+
+ def write(self, bytes):
+ """
+ Write some bytes to this connection, passing them through a TLS layer if
+ necessary, or discarding them if the connection has already been lost.
+ """
+ if self.TLS:
+ if self.connected:
+ self.protocol.write(bytes)
+ else:
+ FileDescriptor.write(self, bytes)
+
+
+ def writeSequence(self, iovec):
+ """
+ Write some bytes to this connection, scatter/gather-style, passing them
+ through a TLS layer if necessary, or discarding them if the connection
+ has already been lost.
+ """
+ if self.TLS:
+ if self.connected:
+ self.protocol.writeSequence(iovec)
+ else:
+ FileDescriptor.writeSequence(self, iovec)
+
+
+ def loseConnection(self):
+ """
+ Close this connection after writing all pending data.
+
+ If TLS has been negotiated, perform a TLS shutdown.
+ """
+ if self.TLS:
+ if self.connected and not self.disconnecting:
+ self.protocol.loseConnection()
+ else:
+ FileDescriptor.loseConnection(self)
+
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register a producer.
+
+ If TLS is enabled, the TLS connection handles this.
+ """
+ if self.TLS:
+ # Registering a producer before we're connected shouldn't be a
+ # problem. If we end up with a write(), that's already handled in
+ # the write() code above, and there are no other potential
+ # side-effects.
+ self.protocol.registerProducer(producer, streaming)
+ else:
+ FileDescriptor.registerProducer(self, producer, streaming)
+
+
+ def unregisterProducer(self):
+ """
+ Unregister a producer.
+
+ If TLS is enabled, the TLS connection handles this.
+ """
+ if self.TLS:
+ self.protocol.unregisterProducer()
+ else:
+ FileDescriptor.unregisterProducer(self)
+
+
+
+class ClientMixin(object):
+ """
+ A mixin for L{twisted.internet.tcp.Client} which just marks it as a client
+ for the purposes of the default TLS handshake.
+
+ @ivar _tlsClientDefault: Always C{True}, indicating that this is a client
+ connection, and by default when TLS is negotiated this class will act as
+ a TLS client.
+ """
+ _tlsClientDefault = True
+
+
+
+class ServerMixin(object):
+ """
+ A mixin for L{twisted.internet.tcp.Server} which just marks it as a server
+ for the purposes of the default TLS handshake.
+
+ @ivar _tlsClientDefault: Always C{False}, indicating that this is a server
+ connection, and by default when TLS is negotiated this class will act as
+ a TLS server.
+ """
+ _tlsClientDefault = False
diff --git a/twisted/internet/_oldtls.py b/twisted/internet/_oldtls.py
new file mode 100644
index 0000000..e0d2cad
--- /dev/null
+++ b/twisted/internet/_oldtls.py
@@ -0,0 +1,381 @@
+# -*- test-case-name: twisted.test.test_ssl -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module implements OpenSSL socket BIO based TLS support. It is only used if
+memory BIO APIs are not available, which is when the version of pyOpenSSL
+installed is older than 0.10 (when L{twisted.protocols.tls} is not importable).
+This implementation is undesirable because of the complexity of working with
+OpenSSL's non-blocking socket-based APIs (which this module probably does about
+99% correctly, but see #4455 for an example of a problem with it).
+
+Support for older versions of pyOpenSSL is now deprecated and will be removed
+(see #5014).
+
+@see: L{twisted.internet._newtls}
+@since: 11.1
+"""
+
+import os, warnings
+
+from twisted.python.runtime import platformType
+if platformType == 'win32':
+ from errno import WSAEINTR as EINTR
+ from errno import WSAEWOULDBLOCK as EWOULDBLOCK
+ from errno import WSAENOBUFS as ENOBUFS
+else:
+ from errno import EINTR
+ from errno import EWOULDBLOCK
+ from errno import ENOBUFS
+
+from OpenSSL import SSL, __version__ as _sslversion
+
+from zope.interface import implements
+
+from twisted.python import log
+from twisted.internet.interfaces import ITLSTransport, ISSLTransport
+from twisted.internet.abstract import FileDescriptor
+from twisted.internet.main import CONNECTION_DONE, CONNECTION_LOST
+from twisted.internet._ssl import _TLSDelayed
+
+warnings.warn(
+ "Support for pyOpenSSL %s is deprecated. "
+ "Upgrade to pyOpenSSL 0.10 or newer." % (_sslversion,),
+ category=DeprecationWarning,
+ stacklevel=100)
+
+class _TLSMixin:
+ _socketShutdownMethod = 'sock_shutdown'
+
+ writeBlockedOnRead = 0
+ readBlockedOnWrite = 0
+ _userWantRead = _userWantWrite = True
+
+ def getPeerCertificate(self):
+ return self.socket.get_peer_certificate()
+
+ def doRead(self):
+ if self.disconnected:
+ # See the comment in the similar check in doWrite below.
+ # Additionally, in order for anything other than returning
+ # CONNECTION_DONE here to make sense, it will probably be necessary
+ # to implement a way to switch back to TCP from TLS (actually, if
+ # we did something other than return CONNECTION_DONE, that would be
+ # a big part of implementing that feature). In other words, the
+ # expectation is that doRead will be called when self.disconnected
+ # is True only when the connection has been lost. It's possible
+ # that the other end could stop speaking TLS and then send us some
+ # non-TLS data. We'll end up ignoring that data and dropping the
+ # connection. There's no unit tests for this check in the cases
+ # where it makes a difference. The test suite only hits this
+ # codepath when it would have otherwise hit the SSL.ZeroReturnError
+ # exception handler below, which has exactly the same behavior as
+ # this conditional. Maybe that's the only case that can ever be
+ # triggered, I'm not sure. -exarkun
+ return CONNECTION_DONE
+ if self.writeBlockedOnRead:
+ self.writeBlockedOnRead = 0
+ self._resetReadWrite()
+ try:
+ return self._base.doRead(self)
+ except SSL.ZeroReturnError:
+ return CONNECTION_DONE
+ except SSL.WantReadError:
+ return
+ except SSL.WantWriteError:
+ self.readBlockedOnWrite = 1
+ self._base.startWriting(self)
+ self._base.stopReading(self)
+ return
+ except SSL.SysCallError, (retval, desc):
+ if ((retval == -1 and desc == 'Unexpected EOF')
+ or retval > 0):
+ return CONNECTION_LOST
+ log.err()
+ return CONNECTION_LOST
+ except SSL.Error, e:
+ return e
+
+ def doWrite(self):
+ # Retry disconnecting
+ if self.disconnected:
+ # This case is triggered when "disconnected" is set to True by a
+ # call to _postLoseConnection from FileDescriptor.doWrite (to which
+ # we upcall at the end of this overridden version of that API). It
+ # means that while, as far as any protocol connected to this
+ # transport is concerned, the connection no longer exists, the
+ # connection *does* actually still exist. Instead of closing the
+ # connection in the overridden _postLoseConnection, we probably
+ # tried (and failed) to send a TLS close alert. The TCP connection
+ # is still up and we're waiting for the socket to become writeable
+ # enough for the TLS close alert to actually be sendable. Only
+ # then will the connection actually be torn down. -exarkun
+ return self._postLoseConnection()
+ if self._writeDisconnected:
+ return self._closeWriteConnection()
+
+ if self.readBlockedOnWrite:
+ self.readBlockedOnWrite = 0
+ self._resetReadWrite()
+ return self._base.doWrite(self)
+
+ def writeSomeData(self, data):
+ try:
+ return self._base.writeSomeData(self, data)
+ except SSL.WantWriteError:
+ return 0
+ except SSL.WantReadError:
+ self.writeBlockedOnRead = 1
+ self._base.stopWriting(self)
+ self._base.startReading(self)
+ return 0
+ except SSL.ZeroReturnError:
+ return CONNECTION_LOST
+ except SSL.SysCallError, e:
+ if e[0] == -1 and data == "":
+ # errors when writing empty strings are expected
+ # and can be ignored
+ return 0
+ else:
+ return CONNECTION_LOST
+ except SSL.Error, e:
+ return e
+
+
+ def _postLoseConnection(self):
+ """
+ Gets called after loseConnection(), after buffered data is sent.
+
+ We try to send an SSL shutdown alert, but if it doesn't work, retry
+ when the socket is writable.
+ """
+ # Here, set "disconnected" to True to trick higher levels into thinking
+ # the connection is really gone. It's not, and we're not going to
+ # close it yet. Instead, we'll try to send a TLS close alert to shut
+ # down the TLS connection cleanly. Only after we actually get the
+ # close alert into the socket will we disconnect the underlying TCP
+ # connection.
+ self.disconnected = True
+ if hasattr(self.socket, 'set_shutdown'):
+ # If possible, mark the state of the TLS connection as having
+ # already received a TLS close alert from the peer. Why do
+ # this???
+ self.socket.set_shutdown(SSL.RECEIVED_SHUTDOWN)
+ return self._sendCloseAlert()
+
+
+ def _sendCloseAlert(self):
+ # Okay, *THIS* is a bit complicated.
+
+ # Basically, the issue is, OpenSSL seems to not actually return
+ # errors from SSL_shutdown. Therefore, the only way to
+ # determine if the close notification has been sent is by
+ # SSL_shutdown returning "done". However, it will not claim it's
+ # done until it's both sent *and* received a shutdown notification.
+
+ # I don't actually want to wait for a received shutdown
+ # notification, though, so, I have to set RECEIVED_SHUTDOWN
+ # before calling shutdown. Then, it'll return True once it's
+ # *SENT* the shutdown.
+
+ # However, RECEIVED_SHUTDOWN can't be left set, because then
+ # reads will fail, breaking half close.
+
+ # Also, since shutdown doesn't report errors, an empty write call is
+ # done first, to try to detect if the connection has gone away.
+ # (*NOT* an SSL_write call, because that fails once you've called
+ # shutdown)
+ try:
+ os.write(self.socket.fileno(), '')
+ except OSError, se:
+ if se.args[0] in (EINTR, EWOULDBLOCK, ENOBUFS):
+ return 0
+ # Write error, socket gone
+ return CONNECTION_LOST
+
+ try:
+ if hasattr(self.socket, 'set_shutdown'):
+ laststate = self.socket.get_shutdown()
+ self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN)
+ done = self.socket.shutdown()
+ if not (laststate & SSL.RECEIVED_SHUTDOWN):
+ self.socket.set_shutdown(SSL.SENT_SHUTDOWN)
+ else:
+ #warnings.warn("SSL connection shutdown possibly unreliable, "
+ # "please upgrade to ver 0.XX", category=UserWarning)
+ self.socket.shutdown()
+ done = True
+ except SSL.Error, e:
+ return e
+
+ if done:
+ self.stopWriting()
+ # Note that this is tested for by identity below.
+ return CONNECTION_DONE
+ else:
+ # For some reason, the close alert wasn't sent. Start writing
+ # again so that we'll get another chance to send it.
+ self.startWriting()
+ # On Linux, select will sometimes not report a closed file
+ # descriptor in the write set (in particular, it seems that if a
+ # send() fails with EPIPE, the socket will not appear in the write
+ # set). The shutdown call above (which calls down to SSL_shutdown)
+ # may have swallowed a write error. Therefore, also start reading
+ # so that if the socket is closed we will notice. This doesn't
+ # seem to be a problem for poll (because poll reports errors
+ # separately) or with select on BSD (presumably because, unlike
+ # Linux, it doesn't implement select in terms of poll and then map
+ # POLLHUP to select's in fd_set).
+ self.startReading()
+ return None
+
+ def _closeWriteConnection(self):
+ result = self._sendCloseAlert()
+
+ if result is CONNECTION_DONE:
+ return self._base._closeWriteConnection(self)
+
+ return result
+
+ def startReading(self):
+ self._userWantRead = True
+ if not self.readBlockedOnWrite:
+ return self._base.startReading(self)
+
+
+ def stopReading(self):
+ self._userWantRead = False
+ # If we've disconnected, preventing stopReading() from happening
+ # because we are blocked on a read is silly; the read will never
+ # happen.
+ if self.disconnected or not self.writeBlockedOnRead:
+ return self._base.stopReading(self)
+
+
+ def startWriting(self):
+ self._userWantWrite = True
+ if not self.writeBlockedOnRead:
+ return self._base.startWriting(self)
+
+
+ def stopWriting(self):
+ self._userWantWrite = False
+ # If we've disconnected, preventing stopWriting() from happening
+ # because we are blocked on a write is silly; the write will never
+ # happen.
+ if self.disconnected or not self.readBlockedOnWrite:
+ return self._base.stopWriting(self)
+
+
+ def _resetReadWrite(self):
+ # After changing readBlockedOnWrite or writeBlockedOnRead,
+ # call this to reset the state to what the user requested.
+ if self._userWantWrite:
+ self.startWriting()
+ else:
+ self.stopWriting()
+
+ if self._userWantRead:
+ self.startReading()
+ else:
+ self.stopReading()
+
+
+
+def _getTLSClass(klass, _existing={}):
+ if klass not in _existing:
+ class TLSConnection(_TLSMixin, klass):
+ implements(ISSLTransport)
+ _base = klass
+ _existing[klass] = TLSConnection
+ return _existing[klass]
+
+
+class ConnectionMixin(object):
+ """
+ Mixin for L{twisted.internet.tcp.Connection} to help implement
+ L{ITLSTransport} using pyOpenSSL to do crypto and I/O.
+ """
+ TLS = 0
+
+ _tlsWaiting = None
+ def startTLS(self, ctx, extra=True):
+ assert not self.TLS
+ if self.dataBuffer or self._tempDataBuffer:
+ # pre-TLS bytes are still being written. Starting TLS now
+ # will do the wrong thing. Instead, mark that we're trying
+ # to go into the TLS state.
+ self._tlsWaiting = _TLSDelayed([], ctx, extra)
+ return False
+
+ self.stopReading()
+ self.stopWriting()
+ self._startTLS()
+ self.socket = SSL.Connection(ctx.getContext(), self.socket)
+ self.fileno = self.socket.fileno
+ self.startReading()
+ return True
+
+
+ def _startTLS(self):
+ self.TLS = 1
+ self.__class__ = _getTLSClass(self.__class__)
+
+
+ def write(self, bytes):
+ if self._tlsWaiting is not None:
+ self._tlsWaiting.bufferedData.append(bytes)
+ else:
+ FileDescriptor.write(self, bytes)
+
+
+ def writeSequence(self, iovec):
+ if self._tlsWaiting is not None:
+ self._tlsWaiting.bufferedData.extend(iovec)
+ else:
+ FileDescriptor.writeSequence(self, iovec)
+
+
+ def doWrite(self):
+ result = FileDescriptor.doWrite(self)
+ if self._tlsWaiting is not None:
+ if not self.dataBuffer and not self._tempDataBuffer:
+ waiting = self._tlsWaiting
+ self._tlsWaiting = None
+ self.startTLS(waiting.context, waiting.extra)
+ self.writeSequence(waiting.bufferedData)
+ return result
+
+
+
+class ClientMixin(object):
+ """
+ Mixin for L{twisted.internet.tcp.Client} to implement the client part of
+ L{ITLSTransport}.
+ """
+ implements(ITLSTransport)
+
+ def startTLS(self, ctx, client=1):
+ if self._base.startTLS(self, ctx, client):
+ if client:
+ self.socket.set_connect_state()
+ else:
+ self.socket.set_accept_state()
+
+
+
+class ServerMixin(object):
+ """
+ Mixin for L{twisted.internet.tcp.Client} to implement the server part of
+ L{ITLSTransport}.
+ """
+ implements(ITLSTransport)
+
+ def startTLS(self, ctx, server=1):
+ if self._base.startTLS(self, ctx, server):
+ if server:
+ self.socket.set_accept_state()
+ else:
+ self.socket.set_connect_state()
+
diff --git a/twisted/internet/_pollingfile.py b/twisted/internet/_pollingfile.py
new file mode 100644
index 0000000..5d00ace
--- /dev/null
+++ b/twisted/internet/_pollingfile.py
@@ -0,0 +1,300 @@
+# -*- test-case-name: twisted.internet.test.test_pollingfile -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implements a simple polling interface for file descriptors that don't work with
+select() - this is pretty much only useful on Windows.
+"""
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IConsumer, IPushProducer
+
+
+MIN_TIMEOUT = 0.000000001
+MAX_TIMEOUT = 0.1
+
+
+
+class _PollableResource:
+ active = True
+
+ def activate(self):
+ self.active = True
+
+
+ def deactivate(self):
+ self.active = False
+
+
+
+class _PollingTimer:
+ # Everything is private here because it is really an implementation detail.
+
+ def __init__(self, reactor):
+ self.reactor = reactor
+ self._resources = []
+ self._pollTimer = None
+ self._currentTimeout = MAX_TIMEOUT
+ self._paused = False
+
+ def _addPollableResource(self, res):
+ self._resources.append(res)
+ self._checkPollingState()
+
+ def _checkPollingState(self):
+ for resource in self._resources:
+ if resource.active:
+ self._startPolling()
+ break
+ else:
+ self._stopPolling()
+
+ def _startPolling(self):
+ if self._pollTimer is None:
+ self._pollTimer = self._reschedule()
+
+ def _stopPolling(self):
+ if self._pollTimer is not None:
+ self._pollTimer.cancel()
+ self._pollTimer = None
+
+ def _pause(self):
+ self._paused = True
+
+ def _unpause(self):
+ self._paused = False
+ self._checkPollingState()
+
+ def _reschedule(self):
+ if not self._paused:
+ return self.reactor.callLater(self._currentTimeout, self._pollEvent)
+
+ def _pollEvent(self):
+ workUnits = 0.
+ anyActive = []
+ for resource in self._resources:
+ if resource.active:
+ workUnits += resource.checkWork()
+ # Check AFTER work has been done
+ if resource.active:
+ anyActive.append(resource)
+
+ newTimeout = self._currentTimeout
+ if workUnits:
+ newTimeout = self._currentTimeout / (workUnits + 1.)
+ if newTimeout < MIN_TIMEOUT:
+ newTimeout = MIN_TIMEOUT
+ else:
+ newTimeout = self._currentTimeout * 2.
+ if newTimeout > MAX_TIMEOUT:
+ newTimeout = MAX_TIMEOUT
+ self._currentTimeout = newTimeout
+ if anyActive:
+ self._pollTimer = self._reschedule()
+
+
+# If we ever (let's hope not) need the above functionality on UNIX, this could
+# be factored into a different module.
+
+import win32pipe
+import win32file
+import win32api
+import pywintypes
+
+class _PollableReadPipe(_PollableResource):
+
+ implements(IPushProducer)
+
+ def __init__(self, pipe, receivedCallback, lostCallback):
+ # security attributes for pipes
+ self.pipe = pipe
+ self.receivedCallback = receivedCallback
+ self.lostCallback = lostCallback
+
+ def checkWork(self):
+ finished = 0
+ fullDataRead = []
+
+ while 1:
+ try:
+ buffer, bytesToRead, result = win32pipe.PeekNamedPipe(self.pipe, 1)
+ # finished = (result == -1)
+ if not bytesToRead:
+ break
+ hr, data = win32file.ReadFile(self.pipe, bytesToRead, None)
+ fullDataRead.append(data)
+ except win32api.error:
+ finished = 1
+ break
+
+ dataBuf = ''.join(fullDataRead)
+ if dataBuf:
+ self.receivedCallback(dataBuf)
+ if finished:
+ self.cleanup()
+ return len(dataBuf)
+
+ def cleanup(self):
+ self.deactivate()
+ self.lostCallback()
+
+ def close(self):
+ try:
+ win32api.CloseHandle(self.pipe)
+ except pywintypes.error:
+ # You can't close std handles...?
+ pass
+
+ def stopProducing(self):
+ self.close()
+
+ def pauseProducing(self):
+ self.deactivate()
+
+ def resumeProducing(self):
+ self.activate()
+
+
+FULL_BUFFER_SIZE = 64 * 1024
+
+class _PollableWritePipe(_PollableResource):
+
+ implements(IConsumer)
+
+ def __init__(self, writePipe, lostCallback):
+ self.disconnecting = False
+ self.producer = None
+ self.producerPaused = 0
+ self.streamingProducer = 0
+ self.outQueue = []
+ self.writePipe = writePipe
+ self.lostCallback = lostCallback
+ try:
+ win32pipe.SetNamedPipeHandleState(writePipe,
+ win32pipe.PIPE_NOWAIT,
+ None,
+ None)
+ except pywintypes.error:
+ # Maybe it's an invalid handle. Who knows.
+ pass
+
+ def close(self):
+ self.disconnecting = True
+
+ def bufferFull(self):
+ if self.producer is not None:
+ self.producerPaused = 1
+ self.producer.pauseProducing()
+
+ def bufferEmpty(self):
+ if self.producer is not None and ((not self.streamingProducer) or
+ self.producerPaused):
+ self.producer.producerPaused = 0
+ self.producer.resumeProducing()
+ return True
+ return False
+
+ # almost-but-not-quite-exact copy-paste from abstract.FileDescriptor... ugh
+
+ def registerProducer(self, producer, streaming):
+ """Register to receive data from a producer.
+
+ This sets this selectable to be a consumer for a producer. When this
+ selectable runs out of data on a write() call, it will ask the producer
+ to resumeProducing(). A producer should implement the IProducer
+ interface.
+
+ FileDescriptor provides some infrastructure for producer methods.
+ """
+ if self.producer is not None:
+ raise RuntimeError(
+ "Cannot register producer %s, because producer %s was never "
+ "unregistered." % (producer, self.producer))
+ if not self.active:
+ producer.stopProducing()
+ else:
+ self.producer = producer
+ self.streamingProducer = streaming
+ if not streaming:
+ producer.resumeProducing()
+
+ def unregisterProducer(self):
+ """Stop consuming data from a producer, without disconnecting.
+ """
+ self.producer = None
+
+ def writeConnectionLost(self):
+ self.deactivate()
+ try:
+ win32api.CloseHandle(self.writePipe)
+ except pywintypes.error:
+ # OMG what
+ pass
+ self.lostCallback()
+
+
+ def writeSequence(self, seq):
+ """
+ Append a C{list} or C{tuple} of bytes to the output buffer.
+
+ @param seq: C{list} or C{tuple} of C{str} instances to be appended to
+ the output buffer.
+
+ @raise TypeError: If C{seq} contains C{unicode}.
+ """
+ if unicode in map(type, seq):
+ raise TypeError("Unicode not allowed in output buffer.")
+ self.outQueue.extend(seq)
+
+
+ def write(self, data):
+ """
+ Append some bytes to the output buffer.
+
+ @param data: C{str} to be appended to the output buffer.
+ @type data: C{str}.
+
+ @raise TypeError: If C{data} is C{unicode} instead of C{str}.
+ """
+ if isinstance(data, unicode):
+ raise TypeError("Unicode not allowed in output buffer.")
+ if self.disconnecting:
+ return
+ self.outQueue.append(data)
+ if sum(map(len, self.outQueue)) > FULL_BUFFER_SIZE:
+ self.bufferFull()
+
+
+ def checkWork(self):
+ numBytesWritten = 0
+ if not self.outQueue:
+ if self.disconnecting:
+ self.writeConnectionLost()
+ return 0
+ try:
+ win32file.WriteFile(self.writePipe, '', None)
+ except pywintypes.error:
+ self.writeConnectionLost()
+ return numBytesWritten
+ while self.outQueue:
+ data = self.outQueue.pop(0)
+ errCode = 0
+ try:
+ errCode, nBytesWritten = win32file.WriteFile(self.writePipe,
+ data, None)
+ except win32api.error:
+ self.writeConnectionLost()
+ break
+ else:
+ # assert not errCode, "wtf an error code???"
+ numBytesWritten += nBytesWritten
+ if len(data) > nBytesWritten:
+ self.outQueue.insert(0, data[nBytesWritten:])
+ break
+ else:
+ resumed = self.bufferEmpty()
+ if not resumed and self.disconnecting:
+ self.writeConnectionLost()
+ return numBytesWritten
diff --git a/twisted/internet/_posixserialport.py b/twisted/internet/_posixserialport.py
new file mode 100644
index 0000000..cc165a3
--- /dev/null
+++ b/twisted/internet/_posixserialport.py
@@ -0,0 +1,74 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Serial Port Protocol
+"""
+
+# system imports
+import os, errno
+
+# dependent on pyserial ( http://pyserial.sf.net/ )
+# only tested w/ 1.18 (5 Dec 2002)
+import serial
+from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD
+from serial import STOPBITS_ONE, STOPBITS_TWO
+from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
+
+from serialport import BaseSerialPort
+
+# twisted imports
+from twisted.internet import abstract, fdesc, main
+
+class SerialPort(BaseSerialPort, abstract.FileDescriptor):
+ """
+ A select()able serial device, acting as a transport.
+ """
+
+ connected = 1
+
+ def __init__(self, protocol, deviceNameOrPortNumber, reactor,
+ baudrate = 9600, bytesize = EIGHTBITS, parity = PARITY_NONE,
+ stopbits = STOPBITS_ONE, timeout = 0, xonxoff = 0, rtscts = 0):
+ abstract.FileDescriptor.__init__(self, reactor)
+ self._serial = self._serialFactory(
+ deviceNameOrPortNumber, baudrate=baudrate, bytesize=bytesize,
+ parity=parity, stopbits=stopbits, timeout=timeout,
+ xonxoff=xonxoff, rtscts=rtscts)
+ self.reactor = reactor
+ self.flushInput()
+ self.flushOutput()
+ self.protocol = protocol
+ self.protocol.makeConnection(self)
+ self.startReading()
+
+
+ def fileno(self):
+ return self._serial.fd
+
+
+ def writeSomeData(self, data):
+ """
+ Write some data to the serial device.
+ """
+ return fdesc.writeToFD(self.fileno(), data)
+
+
+ def doRead(self):
+ """
+ Some data's readable from serial device.
+ """
+ return fdesc.readFromFD(self.fileno(), self.protocol.dataReceived)
+
+
+ def connectionLost(self, reason):
+ """
+ Called when the serial port disconnects.
+
+ Will call C{connectionLost} on the protocol that is handling the
+ serial data.
+ """
+ abstract.FileDescriptor.connectionLost(self, reason)
+ self._serial.close()
+ self.protocol.connectionLost(reason)
diff --git a/twisted/internet/_posixstdio.py b/twisted/internet/_posixstdio.py
new file mode 100644
index 0000000..11b3205
--- /dev/null
+++ b/twisted/internet/_posixstdio.py
@@ -0,0 +1,175 @@
+# -*- test-case-name: twisted.test.test_stdio -*-
+
+"""Standard input/out/err support.
+
+Future Plans::
+
+ support for stderr, perhaps
+ Rewrite to use the reactor instead of an ad-hoc mechanism for connecting
+ protocols to transport.
+
+Maintainer: James Y Knight
+"""
+
+import warnings
+from zope.interface import implements
+
+from twisted.internet import process, error, interfaces
+from twisted.python import log, failure
+
+
+class PipeAddress(object):
+ implements(interfaces.IAddress)
+
+
+class StandardIO(object):
+ implements(interfaces.ITransport, interfaces.IProducer,
+ interfaces.IConsumer, interfaces.IHalfCloseableDescriptor)
+
+ _reader = None
+ _writer = None
+ disconnected = False
+ disconnecting = False
+
+ def __init__(self, proto, stdin=0, stdout=1, reactor=None):
+ if reactor is None:
+ from twisted.internet import reactor
+ self.protocol = proto
+
+ self._writer = process.ProcessWriter(reactor, self, 'write', stdout)
+ self._reader = process.ProcessReader(reactor, self, 'read', stdin)
+ self._reader.startReading()
+ self.protocol.makeConnection(self)
+
+ # ITransport
+
+ # XXX Actually, see #3597.
+ def loseWriteConnection(self):
+ if self._writer is not None:
+ self._writer.loseConnection()
+
+ def write(self, data):
+ if self._writer is not None:
+ self._writer.write(data)
+
+ def writeSequence(self, data):
+ if self._writer is not None:
+ self._writer.writeSequence(data)
+
+ def loseConnection(self):
+ self.disconnecting = True
+
+ if self._writer is not None:
+ self._writer.loseConnection()
+ if self._reader is not None:
+ # Don't loseConnection, because we don't want to SIGPIPE it.
+ self._reader.stopReading()
+
+ def getPeer(self):
+ return PipeAddress()
+
+ def getHost(self):
+ return PipeAddress()
+
+
+ # Callbacks from process.ProcessReader/ProcessWriter
+ def childDataReceived(self, fd, data):
+ self.protocol.dataReceived(data)
+
+ def childConnectionLost(self, fd, reason):
+ if self.disconnected:
+ return
+
+ if reason.value.__class__ == error.ConnectionDone:
+ # Normal close
+ if fd == 'read':
+ self._readConnectionLost(reason)
+ else:
+ self._writeConnectionLost(reason)
+ else:
+ self.connectionLost(reason)
+
+ def connectionLost(self, reason):
+ self.disconnected = True
+
+ # Make sure to cleanup the other half
+ _reader = self._reader
+ _writer = self._writer
+ protocol = self.protocol
+ self._reader = self._writer = None
+ self.protocol = None
+
+ if _writer is not None and not _writer.disconnected:
+ _writer.connectionLost(reason)
+
+ if _reader is not None and not _reader.disconnected:
+ _reader.connectionLost(reason)
+
+ try:
+ protocol.connectionLost(reason)
+ except:
+ log.err()
+
+ def _writeConnectionLost(self, reason):
+ self._writer=None
+ if self.disconnecting:
+ self.connectionLost(reason)
+ return
+
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.writeConnectionLost()
+ except:
+ log.err()
+ self.connectionLost(failure.Failure())
+
+ def _readConnectionLost(self, reason):
+ self._reader=None
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.readConnectionLost()
+ except:
+ log.err()
+ self.connectionLost(failure.Failure())
+ else:
+ self.connectionLost(reason)
+
+ # IConsumer
+ def registerProducer(self, producer, streaming):
+ if self._writer is None:
+ producer.stopProducing()
+ else:
+ self._writer.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ if self._writer is not None:
+ self._writer.unregisterProducer()
+
+ # IProducer
+ def stopProducing(self):
+ self.loseConnection()
+
+ def pauseProducing(self):
+ if self._reader is not None:
+ self._reader.pauseProducing()
+
+ def resumeProducing(self):
+ if self._reader is not None:
+ self._reader.resumeProducing()
+
+ # Stupid compatibility:
+ def closeStdin(self):
+ """Compatibility only, don't use. Same as loseWriteConnection."""
+ warnings.warn("This function is deprecated, use loseWriteConnection instead.",
+ category=DeprecationWarning, stacklevel=2)
+ self.loseWriteConnection()
+
+ def stopReading(self):
+ """Compatibility only, don't use. Call pauseProducing."""
+ self.pauseProducing()
+
+ def startReading(self):
+ """Compatibility only, don't use. Call resumeProducing."""
+ self.resumeProducing()
diff --git a/twisted/internet/_sigchld.c b/twisted/internet/_sigchld.c
new file mode 100644
index 0000000..660182b
--- /dev/null
+++ b/twisted/internet/_sigchld.c
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2010 Twisted Matrix Laboratories.
+ * See LICENSE for details.
+ */
+
+#include <signal.h>
+#include <errno.h>
+
+#include "Python.h"
+
+static int sigchld_pipe_fd = -1;
+
+static void got_signal(int sig) {
+ int saved_errno = errno;
+ int ignored_result;
+
+ /* write() errors are unhandled. If the buffer is full, we don't
+ * care. What about other errors? */
+ ignored_result = write(sigchld_pipe_fd, "x", 1);
+
+ errno = saved_errno;
+}
+
+PyDoc_STRVAR(install_sigchld_handler_doc, "\
+install_sigchld_handler(fd)\n\
+\n\
+Installs a SIGCHLD handler which will write a byte to the given fd\n\
+whenever a SIGCHLD occurs. This is done in C code because the python\n\
+signal handling system is not reliable, and additionally cannot\n\
+specify SA_RESTART.\n\
+\n\
+Please ensure fd is in non-blocking mode.\n\
+");
+
+static PyObject *
+install_sigchld_handler(PyObject *self, PyObject *args) {
+ int fd, old_fd;
+ struct sigaction sa;
+
+ if (!PyArg_ParseTuple(args, "i:install_sigchld_handler", &fd)) {
+ return NULL;
+ }
+ old_fd = sigchld_pipe_fd;
+ sigchld_pipe_fd = fd;
+
+ if (fd == -1) {
+ sa.sa_handler = SIG_DFL;
+ } else {
+ sa.sa_handler = got_signal;
+ sa.sa_flags = SA_RESTART;
+ /* mask all signals so I don't worry about EINTR from the write. */
+ sigfillset(&sa.sa_mask);
+ }
+ if (sigaction(SIGCHLD, &sa, 0) != 0) {
+ sigchld_pipe_fd = old_fd;
+ return PyErr_SetFromErrno(PyExc_OSError);
+ }
+ return PyLong_FromLong(old_fd);
+}
+
+PyDoc_STRVAR(is_default_handler_doc, "\
+Return 1 if the SIGCHLD handler is SIG_DFL, 0 otherwise.\n\
+");
+
+static PyObject *
+is_default_handler(PyObject *self, PyObject *args) {
+ /*
+ * This implementation is necessary since the install_sigchld_handler
+ * function above bypasses the Python signal handler installation API, so
+ * CPython doesn't notice that the handler has changed and signal.getsignal
+ * won't return an accurate result.
+ */
+ struct sigaction sa;
+
+ if (sigaction(SIGCHLD, NULL, &sa) != 0) {
+ return PyErr_SetFromErrno(PyExc_OSError);
+ }
+
+ return PyLong_FromLong(sa.sa_handler == SIG_DFL);
+}
+
+static PyMethodDef sigchld_methods[] = {
+ {"installHandler", install_sigchld_handler, METH_VARARGS,
+ install_sigchld_handler_doc},
+ {"isDefaultHandler", is_default_handler, METH_NOARGS,
+ is_default_handler_doc},
+ /* sentinel */
+ {NULL, NULL, 0, NULL}
+};
+
+
+static const char _sigchld_doc[] = "\n\
+This module contains an API for receiving SIGCHLD via a file descriptor.\n\
+";
+
+PyMODINIT_FUNC
+init_sigchld(void) {
+ /* Create the module and add the functions */
+ Py_InitModule3(
+ "twisted.internet._sigchld", sigchld_methods, _sigchld_doc);
+}
diff --git a/twisted/internet/_signals.py b/twisted/internet/_signals.py
new file mode 100644
index 0000000..4cc82b8
--- /dev/null
+++ b/twisted/internet/_signals.py
@@ -0,0 +1,184 @@
+# -*- test-case-name: twisted.test.test_process,twisted.internet.test.test_process -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides a uniform interface to the several mechanisms which are
+possibly available for dealing with signals.
+
+This module is used to integrate child process termination into a
+reactor event loop. This is a challenging feature to provide because
+most platforms indicate process termination via SIGCHLD and do not
+provide a way to wait for that signal and arbitrary I/O events at the
+same time. The naive implementation involves installing a Python
+SIGCHLD handler; unfortunately this leads to other syscalls being
+interrupted (whenever SIGCHLD is received) and failing with EINTR
+(which almost no one is prepared to handle). This interruption can be
+disabled via siginterrupt(2) (or one of the equivalent mechanisms);
+however, if the SIGCHLD is delivered by the platform to a non-main
+thread (not a common occurrence, but difficult to prove impossible),
+the main thread (waiting on select() or another event notification
+API) may not wake up leading to an arbitrary delay before the child
+termination is noticed.
+
+The basic solution to all these issues involves enabling SA_RESTART
+(ie, disabling system call interruption) and registering a C signal
+handler which writes a byte to a pipe. The other end of the pipe is
+registered with the event loop, allowing it to wake up shortly after
+SIGCHLD is received. See L{twisted.internet.posixbase._SIGCHLDWaker}
+for the implementation of the event loop side of this solution. The
+use of a pipe this way is known as the U{self-pipe
+trick<http://cr.yp.to/docs/selfpipe.html>}.
+
+The actual solution implemented in this module depends on the version
+of Python. From version 2.6, C{signal.siginterrupt} and
+C{signal.set_wakeup_fd} allow the necessary C signal handler which
+writes to the pipe to be registered with C{SA_RESTART}. Prior to 2.6,
+the L{twisted.internet._sigchld} extension module provides similar
+functionality.
+
+If neither of these is available, a Python signal handler is used
+instead. This is essentially the naive solution mentioned above and
+has the problems described there.
+"""
+
+import os
+
+try:
+ from signal import set_wakeup_fd, siginterrupt
+except ImportError:
+ set_wakeup_fd = siginterrupt = None
+
+try:
+ import signal
+except ImportError:
+ signal = None
+
+from twisted.python.log import msg
+
+try:
+ from twisted.internet._sigchld import installHandler as _extInstallHandler, \
+ isDefaultHandler as _extIsDefaultHandler
+except ImportError:
+ _extInstallHandler = _extIsDefaultHandler = None
+
+
+class _Handler(object):
+ """
+ L{_Handler} is a signal handler which writes a byte to a file descriptor
+ whenever it is invoked.
+
+ @ivar fd: The file descriptor to which to write. If this is C{None},
+ nothing will be written.
+ """
+ def __init__(self, fd):
+ self.fd = fd
+
+
+ def __call__(self, *args):
+ """
+ L{_Handler.__call__} is the signal handler. It will write a byte to
+ the wrapped file descriptor, if there is one.
+ """
+ if self.fd is not None:
+ try:
+ os.write(self.fd, '\0')
+ except:
+ pass
+
+
+
+def _installHandlerUsingSignal(fd):
+ """
+ Install a signal handler which will write a byte to C{fd} when
+ I{SIGCHLD} is received.
+
+ This is implemented by creating an instance of L{_Handler} with C{fd}
+ and installing it as the signal handler.
+
+ @param fd: The file descriptor to which to write when I{SIGCHLD} is
+ received.
+ @type fd: C{int}
+ """
+ if fd == -1:
+ previous = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ else:
+ previous = signal.signal(signal.SIGCHLD, _Handler(fd))
+ if isinstance(previous, _Handler):
+ return previous.fd
+ return -1
+
+
+
+def _installHandlerUsingSetWakeup(fd):
+ """
+ Install a signal handler which will write a byte to C{fd} when
+ I{SIGCHLD} is received.
+
+ This is implemented by installing an instance of L{_Handler} wrapped
+ around C{None}, setting the I{SIGCHLD} handler as not allowed to
+ interrupt system calls, and using L{signal.set_wakeup_fd} to do the
+ actual writing.
+
+ @param fd: The file descriptor to which to write when I{SIGCHLD} is
+ received.
+ @type fd: C{int}
+ """
+ if fd == -1:
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ else:
+ signal.signal(signal.SIGCHLD, _Handler(None))
+ siginterrupt(signal.SIGCHLD, False)
+ return set_wakeup_fd(fd)
+
+
+
+def _isDefaultHandler():
+ """
+ Determine whether the I{SIGCHLD} handler is the default or not.
+ """
+ return signal.getsignal(signal.SIGCHLD) == signal.SIG_DFL
+
+
+
+def _cannotInstallHandler(fd):
+ """
+ Fail to install a signal handler for I{SIGCHLD}.
+
+ This implementation is used when the supporting code for the other
+ implementations is unavailable (on Python versions 2.5 and older where
+ neither the L{twisted.internet._sigchld} extension nor the standard
+ L{signal} module is available).
+
+ @param fd: Ignored; only for compatibility with the other
+ implementations of this interface.
+
+ @raise RuntimeError: Always raised to indicate no I{SIGCHLD} handler can
+ be installed.
+ """
+ raise RuntimeError("Cannot install a SIGCHLD handler")
+
+
+
+def _cannotDetermineDefault():
+ raise RuntimeError("No usable signal API available")
+
+
+
+if set_wakeup_fd is not None:
+ msg('using set_wakeup_fd')
+ installHandler = _installHandlerUsingSetWakeup
+ isDefaultHandler = _isDefaultHandler
+elif _extInstallHandler is not None:
+ msg('using _sigchld')
+ installHandler = _extInstallHandler
+ isDefaultHandler = _extIsDefaultHandler
+elif signal is not None:
+ msg('using signal module')
+ installHandler = _installHandlerUsingSignal
+ isDefaultHandler = _isDefaultHandler
+else:
+ msg('nothing unavailable')
+ installHandler = _cannotInstallHandler
+ isDefaultHandler = _cannotDetermineDefault
+
diff --git a/twisted/internet/_ssl.py b/twisted/internet/_ssl.py
new file mode 100644
index 0000000..318ee35
--- /dev/null
+++ b/twisted/internet/_ssl.py
@@ -0,0 +1,32 @@
+# -*- test-case-name: twisted.test.test_ssl -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module implements helpers for switching to TLS on an existing transport.
+
+@since: 11.1
+"""
+
+class _TLSDelayed(object):
+ """
+ State tracking record for TLS startup parameters. Used to remember how
+ TLS should be started when starting it is delayed to wait for the output
+ buffer to be flushed.
+
+ @ivar bufferedData: A C{list} which contains all the data which was
+ written to the transport after an attempt to start TLS was made but
+ before the buffers outstanding at that time could be flushed and TLS
+ could really be started. This is appended to by the transport's
+ write and writeSequence methods until it is possible to actually
+ start TLS, then it is written to the TLS-enabled transport.
+
+ @ivar context: An SSL context factory object to use to start TLS.
+
+ @ivar extra: An extra argument to pass to the transport's C{startTLS}
+ method.
+ """
+ def __init__(self, bufferedData, context, extra):
+ self.bufferedData = bufferedData
+ self.context = context
+ self.extra = extra
diff --git a/twisted/internet/_sslverify.py b/twisted/internet/_sslverify.py
new file mode 100644
index 0000000..1cac6a8
--- /dev/null
+++ b/twisted/internet/_sslverify.py
@@ -0,0 +1,749 @@
+# -*- test-case-name: twisted.test.test_sslverify -*-
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+# Copyright (c) 2005-2008 Twisted Matrix Laboratories.
+
+import itertools
+from OpenSSL import SSL, crypto
+
+from twisted.python import reflect, util
+from twisted.python.hashlib import md5
+from twisted.internet.defer import Deferred
+from twisted.internet.error import VerifyError, CertificateError
+
+# Private - shared between all OpenSSLCertificateOptions, counts up to provide
+# a unique session id for each context
+_sessionCounter = itertools.count().next
+
+_x509names = {
+ 'CN': 'commonName',
+ 'commonName': 'commonName',
+
+ 'O': 'organizationName',
+ 'organizationName': 'organizationName',
+
+ 'OU': 'organizationalUnitName',
+ 'organizationalUnitName': 'organizationalUnitName',
+
+ 'L': 'localityName',
+ 'localityName': 'localityName',
+
+ 'ST': 'stateOrProvinceName',
+ 'stateOrProvinceName': 'stateOrProvinceName',
+
+ 'C': 'countryName',
+ 'countryName': 'countryName',
+
+ 'emailAddress': 'emailAddress'}
+
+
+class DistinguishedName(dict):
+ """
+ Identify and describe an entity.
+
+ Distinguished names are used to provide a minimal amount of identifying
+ information about a certificate issuer or subject. They are commonly
+ created with one or more of the following fields::
+
+ commonName (CN)
+ organizationName (O)
+ organizationalUnitName (OU)
+ localityName (L)
+ stateOrProvinceName (ST)
+ countryName (C)
+ emailAddress
+ """
+ __slots__ = ()
+
+ def __init__(self, **kw):
+ for k, v in kw.iteritems():
+ setattr(self, k, v)
+
+
+ def _copyFrom(self, x509name):
+ d = {}
+ for name in _x509names:
+ value = getattr(x509name, name, None)
+ if value is not None:
+ setattr(self, name, value)
+
+
+ def _copyInto(self, x509name):
+ for k, v in self.iteritems():
+ setattr(x509name, k, v)
+
+
+ def __repr__(self):
+ return '<DN %s>' % (dict.__repr__(self)[1:-1])
+
+
+ def __getattr__(self, attr):
+ try:
+ return self[_x509names[attr]]
+ except KeyError:
+ raise AttributeError(attr)
+
+
+ def __setattr__(self, attr, value):
+ assert type(attr) is str
+ if not attr in _x509names:
+ raise AttributeError("%s is not a valid OpenSSL X509 name field" % (attr,))
+ realAttr = _x509names[attr]
+ value = value.encode('ascii')
+ assert type(value) is str
+ self[realAttr] = value
+
+
+ def inspect(self):
+ """
+ Return a multi-line, human-readable representation of this DN.
+ """
+ l = []
+ lablen = 0
+ def uniqueValues(mapping):
+ return dict.fromkeys(mapping.itervalues()).keys()
+ for k in uniqueValues(_x509names):
+ label = util.nameToLabel(k)
+ lablen = max(len(label), lablen)
+ v = getattr(self, k, None)
+ if v is not None:
+ l.append((label, v))
+ lablen += 2
+ for n, (label, attr) in enumerate(l):
+ l[n] = (label.rjust(lablen)+': '+ attr)
+ return '\n'.join(l)
+
+DN = DistinguishedName
+
+
+class CertBase:
+ def __init__(self, original):
+ self.original = original
+
+ def _copyName(self, suffix):
+ dn = DistinguishedName()
+ dn._copyFrom(getattr(self.original, 'get_'+suffix)())
+ return dn
+
+ def getSubject(self):
+ """
+ Retrieve the subject of this certificate.
+
+ @rtype: L{DistinguishedName}
+ @return: A copy of the subject of this certificate.
+ """
+ return self._copyName('subject')
+
+
+
+def _handleattrhelper(Class, transport, methodName):
+ """
+ (private) Helper for L{Certificate.peerFromTransport} and
+ L{Certificate.hostFromTransport} which checks for incompatible handle types
+ and null certificates and raises the appropriate exception or returns the
+ appropriate certificate object.
+ """
+ method = getattr(transport.getHandle(),
+ "get_%s_certificate" % (methodName,), None)
+ if method is None:
+ raise CertificateError(
+ "non-TLS transport %r did not have %s certificate" % (transport, methodName))
+ cert = method()
+ if cert is None:
+ raise CertificateError(
+ "TLS transport %r did not have %s certificate" % (transport, methodName))
+ return Class(cert)
+
+
+class Certificate(CertBase):
+ """
+ An x509 certificate.
+ """
+ def __repr__(self):
+ return '<%s Subject=%s Issuer=%s>' % (self.__class__.__name__,
+ self.getSubject().commonName,
+ self.getIssuer().commonName)
+
+ def __eq__(self, other):
+ if isinstance(other, Certificate):
+ return self.dump() == other.dump()
+ return False
+
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+ def load(Class, requestData, format=crypto.FILETYPE_ASN1, args=()):
+ """
+ Load a certificate from an ASN.1- or PEM-format string.
+
+ @rtype: C{Class}
+ """
+ return Class(crypto.load_certificate(format, requestData), *args)
+ load = classmethod(load)
+ _load = load
+
+
+ def dumpPEM(self):
+ """
+ Dump this certificate to a PEM-format data string.
+
+ @rtype: C{str}
+ """
+ return self.dump(crypto.FILETYPE_PEM)
+
+
+ def loadPEM(Class, data):
+ """
+ Load a certificate from a PEM-format data string.
+
+ @rtype: C{Class}
+ """
+ return Class.load(data, crypto.FILETYPE_PEM)
+ loadPEM = classmethod(loadPEM)
+
+
+ def peerFromTransport(Class, transport):
+ """
+ Get the certificate for the remote end of the given transport.
+
+ @type: L{ISystemHandle}
+ @rtype: C{Class}
+
+ @raise: L{CertificateError}, if the given transport does not have a peer
+ certificate.
+ """
+ return _handleattrhelper(Class, transport, 'peer')
+ peerFromTransport = classmethod(peerFromTransport)
+
+
+ def hostFromTransport(Class, transport):
+ """
+ Get the certificate for the local end of the given transport.
+
+ @param transport: an L{ISystemHandle} provider; the transport we will
+
+ @rtype: C{Class}
+
+ @raise: L{CertificateError}, if the given transport does not have a host
+ certificate.
+ """
+ return _handleattrhelper(Class, transport, 'host')
+ hostFromTransport = classmethod(hostFromTransport)
+
+
+ def getPublicKey(self):
+ """
+ Get the public key for this certificate.
+
+ @rtype: L{PublicKey}
+ """
+ return PublicKey(self.original.get_pubkey())
+
+
+ def dump(self, format=crypto.FILETYPE_ASN1):
+ return crypto.dump_certificate(format, self.original)
+
+
+ def serialNumber(self):
+ """
+ Retrieve the serial number of this certificate.
+
+ @rtype: C{int}
+ """
+ return self.original.get_serial_number()
+
+
+ def digest(self, method='md5'):
+ """
+ Return a digest hash of this certificate using the specified hash
+ algorithm.
+
+ @param method: One of C{'md5'} or C{'sha'}.
+ @rtype: C{str}
+ """
+ return self.original.digest(method)
+
+
+ def _inspect(self):
+ return '\n'.join(['Certificate For Subject:',
+ self.getSubject().inspect(),
+ '\nIssuer:',
+ self.getIssuer().inspect(),
+ '\nSerial Number: %d' % self.serialNumber(),
+ 'Digest: %s' % self.digest()])
+
+
+ def inspect(self):
+ """
+ Return a multi-line, human-readable representation of this
+ Certificate, including information about the subject, issuer, and
+ public key.
+ """
+ return '\n'.join((self._inspect(), self.getPublicKey().inspect()))
+
+
+ def getIssuer(self):
+ """
+ Retrieve the issuer of this certificate.
+
+ @rtype: L{DistinguishedName}
+ @return: A copy of the issuer of this certificate.
+ """
+ return self._copyName('issuer')
+
+
+ def options(self, *authorities):
+ raise NotImplementedError('Possible, but doubtful we need this yet')
+
+
+
+class CertificateRequest(CertBase):
+ """
+ An x509 certificate request.
+
+ Certificate requests are given to certificate authorities to be signed and
+ returned resulting in an actual certificate.
+ """
+ def load(Class, requestData, requestFormat=crypto.FILETYPE_ASN1):
+ req = crypto.load_certificate_request(requestFormat, requestData)
+ dn = DistinguishedName()
+ dn._copyFrom(req.get_subject())
+ if not req.verify(req.get_pubkey()):
+ raise VerifyError("Can't verify that request for %r is self-signed." % (dn,))
+ return Class(req)
+ load = classmethod(load)
+
+
+ def dump(self, format=crypto.FILETYPE_ASN1):
+ return crypto.dump_certificate_request(format, self.original)
+
+
+
+class PrivateCertificate(Certificate):
+ """
+ An x509 certificate and private key.
+ """
+ def __repr__(self):
+ return Certificate.__repr__(self) + ' with ' + repr(self.privateKey)
+
+
+ def _setPrivateKey(self, privateKey):
+ if not privateKey.matches(self.getPublicKey()):
+ raise VerifyError(
+ "Certificate public and private keys do not match.")
+ self.privateKey = privateKey
+ return self
+
+
+ def newCertificate(self, newCertData, format=crypto.FILETYPE_ASN1):
+ """
+ Create a new L{PrivateCertificate} from the given certificate data and
+ this instance's private key.
+ """
+ return self.load(newCertData, self.privateKey, format)
+
+
+ def load(Class, data, privateKey, format=crypto.FILETYPE_ASN1):
+ return Class._load(data, format)._setPrivateKey(privateKey)
+ load = classmethod(load)
+
+
+ def inspect(self):
+ return '\n'.join([Certificate._inspect(self),
+ self.privateKey.inspect()])
+
+
+ def dumpPEM(self):
+ """
+ Dump both public and private parts of a private certificate to
+ PEM-format data.
+ """
+ return self.dump(crypto.FILETYPE_PEM) + self.privateKey.dump(crypto.FILETYPE_PEM)
+
+
+ def loadPEM(Class, data):
+ """
+ Load both private and public parts of a private certificate from a
+ chunk of PEM-format data.
+ """
+ return Class.load(data, KeyPair.load(data, crypto.FILETYPE_PEM),
+ crypto.FILETYPE_PEM)
+ loadPEM = classmethod(loadPEM)
+
+
+ def fromCertificateAndKeyPair(Class, certificateInstance, privateKey):
+ privcert = Class(certificateInstance.original)
+ return privcert._setPrivateKey(privateKey)
+ fromCertificateAndKeyPair = classmethod(fromCertificateAndKeyPair)
+
+
+ def options(self, *authorities):
+ options = dict(privateKey=self.privateKey.original,
+ certificate=self.original)
+ if authorities:
+ options.update(dict(verify=True,
+ requireCertificate=True,
+ caCerts=[auth.original for auth in authorities]))
+ return OpenSSLCertificateOptions(**options)
+
+
+ def certificateRequest(self, format=crypto.FILETYPE_ASN1,
+ digestAlgorithm='md5'):
+ return self.privateKey.certificateRequest(
+ self.getSubject(),
+ format,
+ digestAlgorithm)
+
+
+ def signCertificateRequest(self,
+ requestData,
+ verifyDNCallback,
+ serialNumber,
+ requestFormat=crypto.FILETYPE_ASN1,
+ certificateFormat=crypto.FILETYPE_ASN1):
+ issuer = self.getSubject()
+ return self.privateKey.signCertificateRequest(
+ issuer,
+ requestData,
+ verifyDNCallback,
+ serialNumber,
+ requestFormat,
+ certificateFormat)
+
+
+ def signRequestObject(self, certificateRequest, serialNumber,
+ secondsToExpiry=60 * 60 * 24 * 365, # One year
+ digestAlgorithm='md5'):
+ return self.privateKey.signRequestObject(self.getSubject(),
+ certificateRequest,
+ serialNumber,
+ secondsToExpiry,
+ digestAlgorithm)
+
+
+class PublicKey:
+ def __init__(self, osslpkey):
+ self.original = osslpkey
+ req1 = crypto.X509Req()
+ req1.set_pubkey(osslpkey)
+ self._emptyReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, req1)
+
+
+ def matches(self, otherKey):
+ return self._emptyReq == otherKey._emptyReq
+
+
+ # XXX This could be a useful method, but sometimes it triggers a segfault,
+ # so we'll steer clear for now.
+# def verifyCertificate(self, certificate):
+# """
+# returns None, or raises a VerifyError exception if the certificate
+# could not be verified.
+# """
+# if not certificate.original.verify(self.original):
+# raise VerifyError("We didn't sign that certificate.")
+
+ def __repr__(self):
+ return '<%s %s>' % (self.__class__.__name__, self.keyHash())
+
+
+ def keyHash(self):
+ """
+ MD5 hex digest of signature on an empty certificate request with this
+ key.
+ """
+ return md5(self._emptyReq).hexdigest()
+
+
+ def inspect(self):
+ return 'Public Key with Hash: %s' % (self.keyHash(),)
+
+
+
+class KeyPair(PublicKey):
+
+ def load(Class, data, format=crypto.FILETYPE_ASN1):
+ return Class(crypto.load_privatekey(format, data))
+ load = classmethod(load)
+
+
+ def dump(self, format=crypto.FILETYPE_ASN1):
+ return crypto.dump_privatekey(format, self.original)
+
+
+ def __getstate__(self):
+ return self.dump()
+
+
+ def __setstate__(self, state):
+ self.__init__(crypto.load_privatekey(crypto.FILETYPE_ASN1, state))
+
+
+ def inspect(self):
+ t = self.original.type()
+ if t == crypto.TYPE_RSA:
+ ts = 'RSA'
+ elif t == crypto.TYPE_DSA:
+ ts = 'DSA'
+ else:
+ ts = '(Unknown Type!)'
+ L = (self.original.bits(), ts, self.keyHash())
+ return '%s-bit %s Key Pair with Hash: %s' % L
+
+
+ def generate(Class, kind=crypto.TYPE_RSA, size=1024):
+ pkey = crypto.PKey()
+ pkey.generate_key(kind, size)
+ return Class(pkey)
+
+
+ def newCertificate(self, newCertData, format=crypto.FILETYPE_ASN1):
+ return PrivateCertificate.load(newCertData, self, format)
+ generate = classmethod(generate)
+
+
+ def requestObject(self, distinguishedName, digestAlgorithm='md5'):
+ req = crypto.X509Req()
+ req.set_pubkey(self.original)
+ distinguishedName._copyInto(req.get_subject())
+ req.sign(self.original, digestAlgorithm)
+ return CertificateRequest(req)
+
+
+ def certificateRequest(self, distinguishedName,
+ format=crypto.FILETYPE_ASN1,
+ digestAlgorithm='md5'):
+ """Create a certificate request signed with this key.
+
+ @return: a string, formatted according to the 'format' argument.
+ """
+ return self.requestObject(distinguishedName, digestAlgorithm).dump(format)
+
+
+ def signCertificateRequest(self,
+ issuerDistinguishedName,
+ requestData,
+ verifyDNCallback,
+ serialNumber,
+ requestFormat=crypto.FILETYPE_ASN1,
+ certificateFormat=crypto.FILETYPE_ASN1,
+ secondsToExpiry=60 * 60 * 24 * 365, # One year
+ digestAlgorithm='md5'):
+ """
+ Given a blob of certificate request data and a certificate authority's
+ DistinguishedName, return a blob of signed certificate data.
+
+ If verifyDNCallback returns a Deferred, I will return a Deferred which
+ fires the data when that Deferred has completed.
+ """
+ hlreq = CertificateRequest.load(requestData, requestFormat)
+
+ dn = hlreq.getSubject()
+ vval = verifyDNCallback(dn)
+
+ def verified(value):
+ if not value:
+ raise VerifyError("DN callback %r rejected request DN %r" % (verifyDNCallback, dn))
+ return self.signRequestObject(issuerDistinguishedName, hlreq,
+ serialNumber, secondsToExpiry, digestAlgorithm).dump(certificateFormat)
+
+ if isinstance(vval, Deferred):
+ return vval.addCallback(verified)
+ else:
+ return verified(vval)
+
+
+ def signRequestObject(self,
+ issuerDistinguishedName,
+ requestObject,
+ serialNumber,
+ secondsToExpiry=60 * 60 * 24 * 365, # One year
+ digestAlgorithm='md5'):
+ """
+ Sign a CertificateRequest instance, returning a Certificate instance.
+ """
+ req = requestObject.original
+ dn = requestObject.getSubject()
+ cert = crypto.X509()
+ issuerDistinguishedName._copyInto(cert.get_issuer())
+ cert.set_subject(req.get_subject())
+ cert.set_pubkey(req.get_pubkey())
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(secondsToExpiry)
+ cert.set_serial_number(serialNumber)
+ cert.sign(self.original, digestAlgorithm)
+ return Certificate(cert)
+
+
+ def selfSignedCert(self, serialNumber, **kw):
+ dn = DN(**kw)
+ return PrivateCertificate.fromCertificateAndKeyPair(
+ self.signRequestObject(dn, self.requestObject(dn), serialNumber),
+ self)
+
+
+
+class OpenSSLCertificateOptions(object):
+ """
+ A factory for SSL context objects for both SSL servers and clients.
+ """
+
+ _context = None
+ # Older versions of PyOpenSSL didn't provide OP_ALL. Fudge it here, just in case.
+ _OP_ALL = getattr(SSL, 'OP_ALL', 0x0000FFFF)
+ # OP_NO_TICKET is not (yet) exposed by PyOpenSSL
+ _OP_NO_TICKET = 0x00004000
+
+ method = SSL.TLSv1_METHOD
+
+ def __init__(self,
+ privateKey=None,
+ certificate=None,
+ method=None,
+ verify=False,
+ caCerts=None,
+ verifyDepth=9,
+ requireCertificate=True,
+ verifyOnce=True,
+ enableSingleUseKeys=True,
+ enableSessions=True,
+ fixBrokenPeers=False,
+ enableSessionTickets=False):
+ """
+ Create an OpenSSL context SSL connection context factory.
+
+ @param privateKey: A PKey object holding the private key.
+
+ @param certificate: An X509 object holding the certificate.
+
+ @param method: The SSL protocol to use, one of SSLv23_METHOD,
+ SSLv2_METHOD, SSLv3_METHOD, TLSv1_METHOD. Defaults to TLSv1_METHOD.
+
+ @param verify: If True, verify certificates received from the peer and
+ fail the handshake if verification fails. Otherwise, allow anonymous
+ sessions and sessions with certificates which fail validation. By
+ default this is False.
+
+ @param caCerts: List of certificate authority certificate objects to
+ use to verify the peer's certificate. Only used if verify is
+ C{True}, and if verify is C{True}, this must be specified. Since
+ verify is C{False} by default, this is C{None} by default.
+
+ @type caCerts: C{list} of L{OpenSSL.crypto.X509}
+
+ @param verifyDepth: Depth in certificate chain down to which to verify.
+ If unspecified, use the underlying default (9).
+
+ @param requireCertificate: If True, do not allow anonymous sessions.
+
+ @param verifyOnce: If True, do not re-verify the certificate
+ on session resumption.
+
+ @param enableSingleUseKeys: If True, generate a new key whenever
+ ephemeral DH parameters are used to prevent small subgroup attacks.
+
+ @param enableSessions: If True, set a session ID on each context. This
+ allows a shortened handshake to be used when a known client reconnects.
+
+ @param fixBrokenPeers: If True, enable various non-spec protocol fixes
+ for broken SSL implementations. This should be entirely safe,
+ according to the OpenSSL documentation, but YMMV. This option is now
+ off by default, because it causes problems with connections between
+ peers using OpenSSL 0.9.8a.
+
+ @param enableSessionTickets: If True, enable session ticket extension
+ for session resumption per RFC 5077. Note there is no support for
+ controlling session tickets. This option is off by default, as some
+ server implementations don't correctly process incoming empty session
+ ticket extensions in the hello.
+ """
+
+ assert (privateKey is None) == (certificate is None), "Specify neither or both of privateKey and certificate"
+ self.privateKey = privateKey
+ self.certificate = certificate
+ if method is not None:
+ self.method = method
+
+ self.verify = verify
+ assert ((verify and caCerts) or
+ (not verify)), "Specify client CA certificate information if and only if enabling certificate verification"
+
+ self.caCerts = caCerts
+ self.verifyDepth = verifyDepth
+ self.requireCertificate = requireCertificate
+ self.verifyOnce = verifyOnce
+ self.enableSingleUseKeys = enableSingleUseKeys
+ self.enableSessions = enableSessions
+ self.fixBrokenPeers = fixBrokenPeers
+ self.enableSessionTickets = enableSessionTickets
+
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ try:
+ del d['_context']
+ except KeyError:
+ pass
+ return d
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+
+
+ def getContext(self):
+ """Return a SSL.Context object.
+ """
+ if self._context is None:
+ self._context = self._makeContext()
+ return self._context
+
+
+ def _makeContext(self):
+ ctx = SSL.Context(self.method)
+
+ if self.certificate is not None and self.privateKey is not None:
+ ctx.use_certificate(self.certificate)
+ ctx.use_privatekey(self.privateKey)
+ # Sanity check
+ ctx.check_privatekey()
+
+ verifyFlags = SSL.VERIFY_NONE
+ if self.verify:
+ verifyFlags = SSL.VERIFY_PEER
+ if self.requireCertificate:
+ verifyFlags |= SSL.VERIFY_FAIL_IF_NO_PEER_CERT
+ if self.verifyOnce:
+ verifyFlags |= SSL.VERIFY_CLIENT_ONCE
+ if self.caCerts:
+ store = ctx.get_cert_store()
+ for cert in self.caCerts:
+ store.add_cert(cert)
+
+ # It'd be nice if pyOpenSSL let us pass None here for this behavior (as
+ # the underlying OpenSSL API call allows NULL to be passed). It
+ # doesn't, so we'll supply a function which does the same thing.
+ def _verifyCallback(conn, cert, errno, depth, preverify_ok):
+ return preverify_ok
+ ctx.set_verify(verifyFlags, _verifyCallback)
+
+ if self.verifyDepth is not None:
+ ctx.set_verify_depth(self.verifyDepth)
+
+ if self.enableSingleUseKeys:
+ ctx.set_options(SSL.OP_SINGLE_DH_USE)
+
+ if self.fixBrokenPeers:
+ ctx.set_options(self._OP_ALL)
+
+ if self.enableSessions:
+ sessionName = md5("%s-%d" % (reflect.qual(self.__class__), _sessionCounter())).hexdigest()
+ ctx.set_session_id(sessionName)
+
+ if not self.enableSessionTickets:
+ ctx.set_options(self._OP_NO_TICKET)
+
+ return ctx
diff --git a/twisted/internet/_threadedselect.py b/twisted/internet/_threadedselect.py
new file mode 100644
index 0000000..b9d9417
--- /dev/null
+++ b/twisted/internet/_threadedselect.py
@@ -0,0 +1,367 @@
+# -*- test-case-name: twisted.test.test_internet -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from __future__ import generators
+
+"""
+Threaded select reactor
+
+Maintainer: Bob Ippolito
+
+
+The threadedselectreactor is a specialized reactor for integrating with
+arbitrary foreign event loop, such as those you find in GUI toolkits.
+
+There are three things you'll need to do to use this reactor.
+
+Install the reactor at the beginning of your program, before importing
+the rest of Twisted::
+
+ | from twisted.internet import _threadedselect
+ | _threadedselect.install()
+
+Interleave this reactor with your foreign event loop, at some point after
+your event loop is initialized::
+
+ | from twisted.internet import reactor
+ | reactor.interleave(foreignEventLoopWakerFunction)
+ | self.addSystemEventTrigger('after', 'shutdown', foreignEventLoopStop)
+
+Instead of shutting down the foreign event loop directly, shut down the
+reactor::
+
+ | from twisted.internet import reactor
+ | reactor.stop()
+
+In order for Twisted to do its work in the main thread (the thread that
+interleave is called from), a waker function is necessary. The waker function
+will be called from a "background" thread with one argument: func.
+The waker function's purpose is to call func() from the main thread.
+Many GUI toolkits ship with appropriate waker functions.
+Some examples of this are wxPython's wx.callAfter (may be wxCallAfter in
+older versions of wxPython) or PyObjC's PyObjCTools.AppHelper.callAfter.
+These would be used in place of "foreignEventLoopWakerFunction" in the above
+example.
+
+The other integration point at which the foreign event loop and this reactor
+must integrate is shutdown. In order to ensure clean shutdown of Twisted,
+you must allow for Twisted to come to a complete stop before quitting the
+application. Typically, you will do this by setting up an after shutdown
+trigger to stop your foreign event loop, and call reactor.stop() where you
+would normally have initiated the shutdown procedure for the foreign event
+loop. Shutdown functions that could be used in place of
+"foreignEventloopStop" would be the ExitMainLoop method of the wxApp instance
+with wxPython, or the PyObjCTools.AppHelper.stopEventLoop function.
+"""
+
+from threading import Thread
+from Queue import Queue, Empty
+from time import sleep
+import sys
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IReactorFDSet
+from twisted.internet import error
+from twisted.internet import posixbase
+from twisted.internet.posixbase import _NO_FILENO, _NO_FILEDESC
+from twisted.python import log, failure, threadable
+from twisted.persisted import styles
+from twisted.python.runtime import platformType
+
+import select
+from errno import EINTR, EBADF
+
+from twisted.internet.selectreactor import _select
+
+def dictRemove(dct, value):
+ try:
+ del dct[value]
+ except KeyError:
+ pass
+
+def raiseException(e):
+ raise e
+
+class ThreadedSelectReactor(posixbase.PosixReactorBase):
+ """A threaded select() based reactor - runs on all POSIX platforms and on
+ Win32.
+ """
+ implements(IReactorFDSet)
+
+ def __init__(self):
+ threadable.init(1)
+ self.reads = {}
+ self.writes = {}
+ self.toThreadQueue = Queue()
+ self.toMainThread = Queue()
+ self.workerThread = None
+ self.mainWaker = None
+ posixbase.PosixReactorBase.__init__(self)
+ self.addSystemEventTrigger('after', 'shutdown', self._mainLoopShutdown)
+
+ def wakeUp(self):
+ # we want to wake up from any thread
+ self.waker.wakeUp()
+
+ def callLater(self, *args, **kw):
+ tple = posixbase.PosixReactorBase.callLater(self, *args, **kw)
+ self.wakeUp()
+ return tple
+
+ def _sendToMain(self, msg, *args):
+ #print >>sys.stderr, 'sendToMain', msg, args
+ self.toMainThread.put((msg, args))
+ if self.mainWaker is not None:
+ self.mainWaker()
+
+ def _sendToThread(self, fn, *args):
+ #print >>sys.stderr, 'sendToThread', fn, args
+ self.toThreadQueue.put((fn, args))
+
+ def _preenDescriptorsInThread(self):
+ log.msg("Malformed file descriptor found. Preening lists.")
+ readers = self.reads.keys()
+ writers = self.writes.keys()
+ self.reads.clear()
+ self.writes.clear()
+ for selDict, selList in ((self.reads, readers), (self.writes, writers)):
+ for selectable in selList:
+ try:
+ select.select([selectable], [selectable], [selectable], 0)
+ except:
+ log.msg("bad descriptor %s" % selectable)
+ else:
+ selDict[selectable] = 1
+
+ def _workerInThread(self):
+ try:
+ while 1:
+ fn, args = self.toThreadQueue.get()
+ #print >>sys.stderr, "worker got", fn, args
+ fn(*args)
+ except SystemExit:
+ pass # exception indicates this thread should exit
+ except:
+ f = failure.Failure()
+ self._sendToMain('Failure', f)
+ #print >>sys.stderr, "worker finished"
+
+ def _doSelectInThread(self, timeout):
+ """Run one iteration of the I/O monitor loop.
+
+ This will run all selectables who had input or output readiness
+ waiting for them.
+ """
+ reads = self.reads
+ writes = self.writes
+ while 1:
+ try:
+ r, w, ignored = _select(reads.keys(),
+ writes.keys(),
+ [], timeout)
+ break
+ except ValueError, ve:
+ # Possibly a file descriptor has gone negative?
+ log.err()
+ self._preenDescriptorsInThread()
+ except TypeError, te:
+ # Something *totally* invalid (object w/o fileno, non-integral
+ # result) was passed
+ log.err()
+ self._preenDescriptorsInThread()
+ except (select.error, IOError), se:
+ # select(2) encountered an error
+ if se.args[0] in (0, 2):
+ # windows does this if it got an empty list
+ if (not reads) and (not writes):
+ return
+ else:
+ raise
+ elif se.args[0] == EINTR:
+ return
+ elif se.args[0] == EBADF:
+ self._preenDescriptorsInThread()
+ else:
+ # OK, I really don't know what's going on. Blow up.
+ raise
+ self._sendToMain('Notify', r, w)
+
+ def _process_Notify(self, r, w):
+ #print >>sys.stderr, "_process_Notify"
+ reads = self.reads
+ writes = self.writes
+
+ _drdw = self._doReadOrWrite
+ _logrun = log.callWithLogger
+ for selectables, method, dct in ((r, "doRead", reads), (w, "doWrite", writes)):
+ for selectable in selectables:
+ # if this was disconnected in another thread, kill it.
+ if selectable not in dct:
+ continue
+ # This for pausing input when we're not ready for more.
+ _logrun(selectable, _drdw, selectable, method, dct)
+ #print >>sys.stderr, "done _process_Notify"
+
+ def _process_Failure(self, f):
+ f.raiseException()
+
+ _doIterationInThread = _doSelectInThread
+
+ def ensureWorkerThread(self):
+ if self.workerThread is None or not self.workerThread.isAlive():
+ self.workerThread = Thread(target=self._workerInThread)
+ self.workerThread.start()
+
+ def doThreadIteration(self, timeout):
+ self._sendToThread(self._doIterationInThread, timeout)
+ self.ensureWorkerThread()
+ #print >>sys.stderr, 'getting...'
+ msg, args = self.toMainThread.get()
+ #print >>sys.stderr, 'got', msg, args
+ getattr(self, '_process_' + msg)(*args)
+
+ doIteration = doThreadIteration
+
+ def _interleave(self):
+ while self.running:
+ #print >>sys.stderr, "runUntilCurrent"
+ self.runUntilCurrent()
+ t2 = self.timeout()
+ t = self.running and t2
+ self._sendToThread(self._doIterationInThread, t)
+ #print >>sys.stderr, "yielding"
+ yield None
+ #print >>sys.stderr, "fetching"
+ msg, args = self.toMainThread.get_nowait()
+ getattr(self, '_process_' + msg)(*args)
+
+ def interleave(self, waker, *args, **kw):
+ """
+ interleave(waker) interleaves this reactor with the
+ current application by moving the blocking parts of
+ the reactor (select() in this case) to a separate
+ thread. This is typically useful for integration with
+ GUI applications which have their own event loop
+ already running.
+
+ See the module docstring for more information.
+ """
+ self.startRunning(*args, **kw)
+ loop = self._interleave()
+ def mainWaker(waker=waker, loop=loop):
+ #print >>sys.stderr, "mainWaker()"
+ waker(loop.next)
+ self.mainWaker = mainWaker
+ loop.next()
+ self.ensureWorkerThread()
+
+ def _mainLoopShutdown(self):
+ self.mainWaker = None
+ if self.workerThread is not None:
+ #print >>sys.stderr, 'getting...'
+ self._sendToThread(raiseException, SystemExit)
+ self.wakeUp()
+ try:
+ while 1:
+ msg, args = self.toMainThread.get_nowait()
+ #print >>sys.stderr, "ignored:", (msg, args)
+ except Empty:
+ pass
+ self.workerThread.join()
+ self.workerThread = None
+ try:
+ while 1:
+ fn, args = self.toThreadQueue.get_nowait()
+ if fn is self._doIterationInThread:
+ log.msg('Iteration is still in the thread queue!')
+ elif fn is raiseException and args[0] is SystemExit:
+ pass
+ else:
+ fn(*args)
+ except Empty:
+ pass
+
+ def _doReadOrWrite(self, selectable, method, dict):
+ try:
+ why = getattr(selectable, method)()
+ handfn = getattr(selectable, 'fileno', None)
+ if not handfn:
+ why = _NO_FILENO
+ elif handfn() == -1:
+ why = _NO_FILEDESC
+ except:
+ why = sys.exc_info()[1]
+ log.err()
+ if why:
+ self._disconnectSelectable(selectable, why, method == "doRead")
+
+ def addReader(self, reader):
+ """Add a FileDescriptor for notification of data available to read.
+ """
+ self._sendToThread(self.reads.__setitem__, reader, 1)
+ self.wakeUp()
+
+ def addWriter(self, writer):
+ """Add a FileDescriptor for notification of data available to write.
+ """
+ self._sendToThread(self.writes.__setitem__, writer, 1)
+ self.wakeUp()
+
+ def removeReader(self, reader):
+ """Remove a Selectable for notification of data available to read.
+ """
+ self._sendToThread(dictRemove, self.reads, reader)
+
+ def removeWriter(self, writer):
+ """Remove a Selectable for notification of data available to write.
+ """
+ self._sendToThread(dictRemove, self.writes, writer)
+
+ def removeAll(self):
+ return self._removeAll(self.reads, self.writes)
+
+
+ def getReaders(self):
+ return self.reads.keys()
+
+
+ def getWriters(self):
+ return self.writes.keys()
+
+
+ def stop(self):
+ """
+ Extend the base stop implementation to also wake up the select thread so
+ that C{runUntilCurrent} notices the reactor should stop.
+ """
+ posixbase.PosixReactorBase.stop(self)
+ self.wakeUp()
+
+
+ def run(self, installSignalHandlers=1):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ self.mainLoop()
+
+ def mainLoop(self):
+ q = Queue()
+ self.interleave(q.put)
+ while self.running:
+ try:
+ q.get()()
+ except StopIteration:
+ break
+
+
+
+def install():
+ """Configure the twisted mainloop to be run using the select() reactor.
+ """
+ reactor = ThreadedSelectReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+__all__ = ['install']
diff --git a/twisted/internet/_win32serialport.py b/twisted/internet/_win32serialport.py
new file mode 100644
index 0000000..1a77236
--- /dev/null
+++ b/twisted/internet/_win32serialport.py
@@ -0,0 +1,126 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Serial port support for Windows.
+
+Requires PySerial and pywin32.
+"""
+
+# system imports
+import serial
+from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD
+from serial import STOPBITS_ONE, STOPBITS_TWO
+from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
+import win32file, win32event
+
+# twisted imports
+from twisted.internet import abstract
+
+# sibling imports
+from serialport import BaseSerialPort
+
+
+class SerialPort(BaseSerialPort, abstract.FileDescriptor):
+ """A serial device, acting as a transport, that uses a win32 event."""
+
+ connected = 1
+
+ def __init__(self, protocol, deviceNameOrPortNumber, reactor,
+ baudrate = 9600, bytesize = EIGHTBITS, parity = PARITY_NONE,
+ stopbits = STOPBITS_ONE, xonxoff = 0, rtscts = 0):
+ self._serial = self._serialFactory(
+ deviceNameOrPortNumber, baudrate=baudrate, bytesize=bytesize,
+ parity=parity, stopbits=stopbits, timeout=None,
+ xonxoff=xonxoff, rtscts=rtscts)
+ self.flushInput()
+ self.flushOutput()
+ self.reactor = reactor
+ self.protocol = protocol
+ self.outQueue = []
+ self.closed = 0
+ self.closedNotifies = 0
+ self.writeInProgress = 0
+
+ self.protocol = protocol
+ self._overlappedRead = win32file.OVERLAPPED()
+ self._overlappedRead.hEvent = win32event.CreateEvent(None, 1, 0, None)
+ self._overlappedWrite = win32file.OVERLAPPED()
+ self._overlappedWrite.hEvent = win32event.CreateEvent(None, 0, 0, None)
+
+ self.reactor.addEvent(self._overlappedRead.hEvent, self, 'serialReadEvent')
+ self.reactor.addEvent(self._overlappedWrite.hEvent, self, 'serialWriteEvent')
+
+ self.protocol.makeConnection(self)
+ self._finishPortSetup()
+
+
+ def _finishPortSetup(self):
+ """
+ Finish setting up the serial port.
+
+ This is a separate method to facilitate testing.
+ """
+ flags, comstat = win32file.ClearCommError(self._serial.hComPort)
+ rc, self.read_buf = win32file.ReadFile(self._serial.hComPort,
+ win32file.AllocateReadBuffer(1),
+ self._overlappedRead)
+
+
+ def serialReadEvent(self):
+ #get that character we set up
+ n = win32file.GetOverlappedResult(self._serial.hComPort, self._overlappedRead, 0)
+ if n:
+ first = str(self.read_buf[:n])
+ #now we should get everything that is already in the buffer
+ flags, comstat = win32file.ClearCommError(self._serial.hComPort)
+ if comstat.cbInQue:
+ win32event.ResetEvent(self._overlappedRead.hEvent)
+ rc, buf = win32file.ReadFile(self._serial.hComPort,
+ win32file.AllocateReadBuffer(comstat.cbInQue),
+ self._overlappedRead)
+ n = win32file.GetOverlappedResult(self._serial.hComPort, self._overlappedRead, 1)
+ #handle all the received data:
+ self.protocol.dataReceived(first + str(buf[:n]))
+ else:
+ #handle all the received data:
+ self.protocol.dataReceived(first)
+
+ #set up next one
+ win32event.ResetEvent(self._overlappedRead.hEvent)
+ rc, self.read_buf = win32file.ReadFile(self._serial.hComPort,
+ win32file.AllocateReadBuffer(1),
+ self._overlappedRead)
+
+
+ def write(self, data):
+ if data:
+ if self.writeInProgress:
+ self.outQueue.append(data)
+ else:
+ self.writeInProgress = 1
+ win32file.WriteFile(self._serial.hComPort, data, self._overlappedWrite)
+
+
+ def serialWriteEvent(self):
+ try:
+ dataToWrite = self.outQueue.pop(0)
+ except IndexError:
+ self.writeInProgress = 0
+ return
+ else:
+ win32file.WriteFile(self._serial.hComPort, dataToWrite, self._overlappedWrite)
+
+
+ def connectionLost(self, reason):
+ """
+ Called when the serial port disconnects.
+
+ Will call C{connectionLost} on the protocol that is handling the
+ serial data.
+ """
+ self.reactor.removeEvent(self._overlappedRead.hEvent)
+ self.reactor.removeEvent(self._overlappedWrite.hEvent)
+ abstract.FileDescriptor.connectionLost(self, reason)
+ self._serial.close()
+ self.protocol.connectionLost(reason)
diff --git a/twisted/internet/_win32stdio.py b/twisted/internet/_win32stdio.py
new file mode 100644
index 0000000..c4c5644
--- /dev/null
+++ b/twisted/internet/_win32stdio.py
@@ -0,0 +1,124 @@
+# -*- test-case-name: twisted.test.test_stdio -*-
+
+"""
+Windows-specific implementation of the L{twisted.internet.stdio} interface.
+"""
+
+import win32api
+import os, msvcrt
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IHalfCloseableProtocol, ITransport, IAddress
+from twisted.internet.interfaces import IConsumer, IPushProducer
+
+from twisted.internet import _pollingfile, main
+from twisted.python.failure import Failure
+
+
+class Win32PipeAddress(object):
+ implements(IAddress)
+
+
+
+class StandardIO(_pollingfile._PollingTimer):
+
+ implements(ITransport,
+ IConsumer,
+ IPushProducer)
+
+ disconnecting = False
+ disconnected = False
+
+ def __init__(self, proto):
+ """
+ Start talking to standard IO with the given protocol.
+
+ Also, put it stdin/stdout/stderr into binary mode.
+ """
+ from twisted.internet import reactor
+
+ for stdfd in range(0, 1, 2):
+ msvcrt.setmode(stdfd, os.O_BINARY)
+
+ _pollingfile._PollingTimer.__init__(self, reactor)
+ self.proto = proto
+
+ hstdin = win32api.GetStdHandle(win32api.STD_INPUT_HANDLE)
+ hstdout = win32api.GetStdHandle(win32api.STD_OUTPUT_HANDLE)
+
+ self.stdin = _pollingfile._PollableReadPipe(
+ hstdin, self.dataReceived, self.readConnectionLost)
+
+ self.stdout = _pollingfile._PollableWritePipe(
+ hstdout, self.writeConnectionLost)
+
+ self._addPollableResource(self.stdin)
+ self._addPollableResource(self.stdout)
+
+ self.proto.makeConnection(self)
+
+ def dataReceived(self, data):
+ self.proto.dataReceived(data)
+
+ def readConnectionLost(self):
+ if IHalfCloseableProtocol.providedBy(self.proto):
+ self.proto.readConnectionLost()
+ self.checkConnLost()
+
+ def writeConnectionLost(self):
+ if IHalfCloseableProtocol.providedBy(self.proto):
+ self.proto.writeConnectionLost()
+ self.checkConnLost()
+
+ connsLost = 0
+
+ def checkConnLost(self):
+ self.connsLost += 1
+ if self.connsLost >= 2:
+ self.disconnecting = True
+ self.disconnected = True
+ self.proto.connectionLost(Failure(main.CONNECTION_DONE))
+
+ # ITransport
+
+ def write(self, data):
+ self.stdout.write(data)
+
+ def writeSequence(self, seq):
+ self.stdout.write(''.join(seq))
+
+ def loseConnection(self):
+ self.disconnecting = True
+ self.stdin.close()
+ self.stdout.close()
+
+ def getPeer(self):
+ return Win32PipeAddress()
+
+ def getHost(self):
+ return Win32PipeAddress()
+
+ # IConsumer
+
+ def registerProducer(self, producer, streaming):
+ return self.stdout.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ return self.stdout.unregisterProducer()
+
+ # def write() above
+
+ # IProducer
+
+ def stopProducing(self):
+ self.stdin.stopProducing()
+
+ # IPushProducer
+
+ def pauseProducing(self):
+ self.stdin.pauseProducing()
+
+ def resumeProducing(self):
+ self.stdin.resumeProducing()
+
diff --git a/twisted/internet/abstract.py b/twisted/internet/abstract.py
new file mode 100644
index 0000000..08dd4d5
--- /dev/null
+++ b/twisted/internet/abstract.py
@@ -0,0 +1,517 @@
+# -*- test-case-name: twisted.test.test_abstract -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Support for generic select()able objects.
+"""
+
+from socket import AF_INET6, inet_pton, error
+
+from zope.interface import implements
+
+# Twisted Imports
+from twisted.python import reflect, failure
+from twisted.internet import interfaces, main
+
+
+class _ConsumerMixin(object):
+ """
+ L{IConsumer} implementations can mix this in to get C{registerProducer} and
+ C{unregisterProducer} methods which take care of keeping track of a
+ producer's state.
+
+ Subclasses must provide three attributes which L{_ConsumerMixin} will read
+ but not write:
+
+ - connected: A C{bool} which is C{True} as long as the the consumer has
+ someplace to send bytes (for example, a TCP connection), and then
+ C{False} when it no longer does.
+
+ - disconnecting: A C{bool} which is C{False} until something like
+ L{ITransport.loseConnection} is called, indicating that the send buffer
+ should be flushed and the connection lost afterwards. Afterwards,
+ C{True}.
+
+ - disconnected: A C{bool} which is C{False} until the consumer no longer
+ has a place to send bytes, then C{True}.
+
+ Subclasses must also override the C{startWriting} method.
+
+ @ivar producer: C{None} if no producer is registered, otherwise the
+ registered producer.
+
+ @ivar producerPaused: A flag indicating whether the producer is currently
+ paused.
+ @type producerPaused: C{bool} or C{int}
+
+ @ivar streamingProducer: A flag indicating whether the producer was
+ registered as a streaming (ie push) producer or not (ie a pull
+ producer). This will determine whether the consumer may ever need to
+ pause and resume it, or if it can merely call C{resumeProducing} on it
+ when buffer space is available.
+ @ivar streamingProducer: C{bool} or C{int}
+
+ """
+ producer = None
+ producerPaused = False
+ streamingProducer = False
+
+ def startWriting(self):
+ """
+ Override in a subclass to cause the reactor to monitor this selectable
+ for write events. This will be called once in C{unregisterProducer} if
+ C{loseConnection} has previously been called, so that the connection can
+ actually close.
+ """
+ raise NotImplementedError("%r did not implement startWriting")
+
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register to receive data from a producer.
+
+ This sets this selectable to be a consumer for a producer. When this
+ selectable runs out of data on a write() call, it will ask the producer
+ to resumeProducing(). When the FileDescriptor's internal data buffer is
+ filled, it will ask the producer to pauseProducing(). If the connection
+ is lost, FileDescriptor calls producer's stopProducing() method.
+
+ If streaming is true, the producer should provide the IPushProducer
+ interface. Otherwise, it is assumed that producer provides the
+ IPullProducer interface. In this case, the producer won't be asked to
+ pauseProducing(), but it has to be careful to write() data only when its
+ resumeProducing() method is called.
+ """
+ if self.producer is not None:
+ raise RuntimeError(
+ "Cannot register producer %s, because producer %s was never "
+ "unregistered." % (producer, self.producer))
+ if self.disconnected:
+ producer.stopProducing()
+ else:
+ self.producer = producer
+ self.streamingProducer = streaming
+ if not streaming:
+ producer.resumeProducing()
+
+
+ def unregisterProducer(self):
+ """
+ Stop consuming data from a producer, without disconnecting.
+ """
+ self.producer = None
+ if self.connected and self.disconnecting:
+ self.startWriting()
+
+
+
+class _LogOwner(object):
+ """
+ Mixin to help implement L{interfaces.ILoggingContext} for transports which
+ have a protocol, the log prefix of which should also appear in the
+ transport's log prefix.
+ """
+ implements(interfaces.ILoggingContext)
+
+ def _getLogPrefix(self, applicationObject):
+ """
+ Determine the log prefix to use for messages related to
+ C{applicationObject}, which may or may not be an
+ L{interfaces.ILoggingContext} provider.
+
+ @return: A C{str} giving the log prefix to use.
+ """
+ if interfaces.ILoggingContext.providedBy(applicationObject):
+ return applicationObject.logPrefix()
+ return applicationObject.__class__.__name__
+
+
+ def logPrefix(self):
+ """
+ Override this method to insert custom logging behavior. Its
+ return value will be inserted in front of every line. It may
+ be called more times than the number of output lines.
+ """
+ return "-"
+
+
+
+class FileDescriptor(_ConsumerMixin, _LogOwner):
+ """An object which can be operated on by select().
+
+ This is an abstract superclass of all objects which may be notified when
+ they are readable or writable; e.g. they have a file-descriptor that is
+ valid to be passed to select(2).
+ """
+ connected = 0
+ disconnected = 0
+ disconnecting = 0
+ _writeDisconnecting = False
+ _writeDisconnected = False
+ dataBuffer = ""
+ offset = 0
+
+ SEND_LIMIT = 128*1024
+
+ implements(interfaces.IPushProducer, interfaces.IReadWriteDescriptor,
+ interfaces.IConsumer, interfaces.ITransport, interfaces.IHalfCloseableDescriptor)
+
+ def __init__(self, reactor=None):
+ if not reactor:
+ from twisted.internet import reactor
+ self.reactor = reactor
+ self._tempDataBuffer = [] # will be added to dataBuffer in doWrite
+ self._tempDataLen = 0
+
+
+ def connectionLost(self, reason):
+ """The connection was lost.
+
+ This is called when the connection on a selectable object has been
+ lost. It will be called whether the connection was closed explicitly,
+ an exception occurred in an event handler, or the other end of the
+ connection closed it first.
+
+ Clean up state here, but make sure to call back up to FileDescriptor.
+ """
+ self.disconnected = 1
+ self.connected = 0
+ if self.producer is not None:
+ self.producer.stopProducing()
+ self.producer = None
+ self.stopReading()
+ self.stopWriting()
+
+
+ def writeSomeData(self, data):
+ """
+ Write as much as possible of the given data, immediately.
+
+ This is called to invoke the lower-level writing functionality, such
+ as a socket's send() method, or a file's write(); this method
+ returns an integer or an exception. If an integer, it is the number
+ of bytes written (possibly zero); if an exception, it indicates the
+ connection was lost.
+ """
+ raise NotImplementedError("%s does not implement writeSomeData" %
+ reflect.qual(self.__class__))
+
+
+ def doRead(self):
+ """
+ Called when data is available for reading.
+
+ Subclasses must override this method. The result will be interpreted
+ in the same way as a result of doWrite().
+ """
+ raise NotImplementedError("%s does not implement doRead" %
+ reflect.qual(self.__class__))
+
+ def doWrite(self):
+ """
+ Called when data can be written.
+
+ A result that is true (which will be a negative number or an
+ exception instance) indicates that the connection was lost. A false
+ result implies the connection is still there; a result of 0
+ indicates no write was done, and a result of None indicates that a
+ write was done.
+ """
+ if len(self.dataBuffer) - self.offset < self.SEND_LIMIT:
+ # If there is currently less than SEND_LIMIT bytes left to send
+ # in the string, extend it with the array data.
+ self.dataBuffer = buffer(self.dataBuffer, self.offset) + "".join(self._tempDataBuffer)
+ self.offset = 0
+ self._tempDataBuffer = []
+ self._tempDataLen = 0
+
+ # Send as much data as you can.
+ if self.offset:
+ l = self.writeSomeData(buffer(self.dataBuffer, self.offset))
+ else:
+ l = self.writeSomeData(self.dataBuffer)
+
+ # There is no writeSomeData implementation in Twisted which returns
+ # < 0, but the documentation for writeSomeData used to claim negative
+ # integers meant connection lost. Keep supporting this here,
+ # although it may be worth deprecating and removing at some point.
+ if l < 0 or isinstance(l, Exception):
+ return l
+ if l == 0 and self.dataBuffer:
+ result = 0
+ else:
+ result = None
+ self.offset += l
+ # If there is nothing left to send,
+ if self.offset == len(self.dataBuffer) and not self._tempDataLen:
+ self.dataBuffer = ""
+ self.offset = 0
+ # stop writing.
+ self.stopWriting()
+ # If I've got a producer who is supposed to supply me with data,
+ if self.producer is not None and ((not self.streamingProducer)
+ or self.producerPaused):
+ # tell them to supply some more.
+ self.producerPaused = 0
+ self.producer.resumeProducing()
+ elif self.disconnecting:
+ # But if I was previously asked to let the connection die, do
+ # so.
+ return self._postLoseConnection()
+ elif self._writeDisconnecting:
+ # I was previously asked to half-close the connection. We
+ # set _writeDisconnected before calling handler, in case the
+ # handler calls loseConnection(), which will want to check for
+ # this attribute.
+ self._writeDisconnected = True
+ result = self._closeWriteConnection()
+ return result
+ return result
+
+ def _postLoseConnection(self):
+ """Called after a loseConnection(), when all data has been written.
+
+ Whatever this returns is then returned by doWrite.
+ """
+ # default implementation, telling reactor we're finished
+ return main.CONNECTION_DONE
+
+ def _closeWriteConnection(self):
+ # override in subclasses
+ pass
+
+ def writeConnectionLost(self, reason):
+ # in current code should never be called
+ self.connectionLost(reason)
+
+ def readConnectionLost(self, reason):
+ # override in subclasses
+ self.connectionLost(reason)
+
+
+ def _isSendBufferFull(self):
+ """
+ Determine whether the user-space send buffer for this transport is full
+ or not.
+
+ When the buffer contains more than C{self.bufferSize} bytes, it is
+ considered full. This might be improved by considering the size of the
+ kernel send buffer and how much of it is free.
+
+ @return: C{True} if it is full, C{False} otherwise.
+ """
+ return len(self.dataBuffer) + self._tempDataLen > self.bufferSize
+
+
+ def _maybePauseProducer(self):
+ """
+ Possibly pause a producer, if there is one and the send buffer is full.
+ """
+ # If we are responsible for pausing our producer,
+ if self.producer is not None and self.streamingProducer:
+ # and our buffer is full,
+ if self._isSendBufferFull():
+ # pause it.
+ self.producerPaused = 1
+ self.producer.pauseProducing()
+
+
+ def write(self, data):
+ """Reliably write some data.
+
+ The data is buffered until the underlying file descriptor is ready
+ for writing. If there is more than C{self.bufferSize} data in the
+ buffer and this descriptor has a registered streaming producer, its
+ C{pauseProducing()} method will be called.
+ """
+ if isinstance(data, unicode): # no, really, I mean it
+ raise TypeError("Data must not be unicode")
+ if not self.connected or self._writeDisconnected:
+ return
+ if data:
+ self._tempDataBuffer.append(data)
+ self._tempDataLen += len(data)
+ self._maybePauseProducer()
+ self.startWriting()
+
+
+ def writeSequence(self, iovec):
+ """
+ Reliably write a sequence of data.
+
+ Currently, this is a convenience method roughly equivalent to::
+
+ for chunk in iovec:
+ fd.write(chunk)
+
+ It may have a more efficient implementation at a later time or in a
+ different reactor.
+
+ As with the C{write()} method, if a buffer size limit is reached and a
+ streaming producer is registered, it will be paused until the buffered
+ data is written to the underlying file descriptor.
+ """
+ for i in iovec:
+ if isinstance(i, unicode): # no, really, I mean it
+ raise TypeError("Data must not be unicode")
+ if not self.connected or not iovec or self._writeDisconnected:
+ return
+ self._tempDataBuffer.extend(iovec)
+ for i in iovec:
+ self._tempDataLen += len(i)
+ self._maybePauseProducer()
+ self.startWriting()
+
+
+ def loseConnection(self, _connDone=failure.Failure(main.CONNECTION_DONE)):
+ """Close the connection at the next available opportunity.
+
+ Call this to cause this FileDescriptor to lose its connection. It will
+ first write any data that it has buffered.
+
+ If there is data buffered yet to be written, this method will cause the
+ transport to lose its connection as soon as it's done flushing its
+ write buffer. If you have a producer registered, the connection won't
+ be closed until the producer is finished. Therefore, make sure you
+ unregister your producer when it's finished, or the connection will
+ never close.
+ """
+
+ if self.connected and not self.disconnecting:
+ if self._writeDisconnected:
+ # doWrite won't trigger the connection close anymore
+ self.stopReading()
+ self.stopWriting()
+ self.connectionLost(_connDone)
+ else:
+ self.stopReading()
+ self.startWriting()
+ self.disconnecting = 1
+
+ def loseWriteConnection(self):
+ self._writeDisconnecting = True
+ self.startWriting()
+
+ def stopReading(self):
+ """Stop waiting for read availability.
+
+ Call this to remove this selectable from being notified when it is
+ ready for reading.
+ """
+ self.reactor.removeReader(self)
+
+ def stopWriting(self):
+ """Stop waiting for write availability.
+
+ Call this to remove this selectable from being notified when it is ready
+ for writing.
+ """
+ self.reactor.removeWriter(self)
+
+ def startReading(self):
+ """Start waiting for read availability.
+ """
+ self.reactor.addReader(self)
+
+ def startWriting(self):
+ """Start waiting for write availability.
+
+ Call this to have this FileDescriptor be notified whenever it is ready for
+ writing.
+ """
+ self.reactor.addWriter(self)
+
+ # Producer/consumer implementation
+
+ # first, the consumer stuff. This requires no additional work, as
+ # any object you can write to can be a consumer, really.
+
+ producer = None
+ bufferSize = 2**2**2**2
+
+ def stopConsuming(self):
+ """Stop consuming data.
+
+ This is called when a producer has lost its connection, to tell the
+ consumer to go lose its connection (and break potential circular
+ references).
+ """
+ self.unregisterProducer()
+ self.loseConnection()
+
+ # producer interface implementation
+
+ def resumeProducing(self):
+ assert self.connected and not self.disconnecting
+ self.startReading()
+
+ def pauseProducing(self):
+ self.stopReading()
+
+ def stopProducing(self):
+ self.loseConnection()
+
+
+ def fileno(self):
+ """File Descriptor number for select().
+
+ This method must be overridden or assigned in subclasses to
+ indicate a valid file descriptor for the operating system.
+ """
+ return -1
+
+
+def isIPAddress(addr):
+ """
+ Determine whether the given string represents an IPv4 address.
+
+ @type addr: C{str}
+ @param addr: A string which may or may not be the decimal dotted
+ representation of an IPv4 address.
+
+ @rtype: C{bool}
+ @return: C{True} if C{addr} represents an IPv4 address, C{False}
+ otherwise.
+ """
+ dottedParts = addr.split('.')
+ if len(dottedParts) == 4:
+ for octet in dottedParts:
+ try:
+ value = int(octet)
+ except ValueError:
+ return False
+ else:
+ if value < 0 or value > 255:
+ return False
+ return True
+ return False
+
+
+def isIPv6Address(addr):
+ """
+ Determine whether the given string represents an IPv6 address.
+
+ @param addr: A string which may or may not be the hex
+ representation of an IPv6 address.
+ @type addr: C{str}
+
+ @return: C{True} if C{addr} represents an IPv6 address, C{False}
+ otherwise.
+ @rtype: C{bool}
+ """
+ if '%' in addr:
+ addr = addr.split('%', 1)[0]
+ if not addr:
+ return False
+ try:
+ # This might be a native implementation or the one from
+ # twisted.python.compat.
+ inet_pton(AF_INET6, addr)
+ except (ValueError, error):
+ return False
+ return True
+
+
+__all__ = ["FileDescriptor", "isIPAddress", "isIPv6Address"]
diff --git a/twisted/internet/address.py b/twisted/internet/address.py
new file mode 100644
index 0000000..14ecf74
--- /dev/null
+++ b/twisted/internet/address.py
@@ -0,0 +1,146 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Address objects for network connections.
+"""
+
+import warnings, os
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IAddress
+from twisted.python import util
+
+
+class _IPAddress(object, util.FancyEqMixin):
+ """
+ An L{_IPAddress} represents the address of an IP socket endpoint, providing
+ common behavior for IPv4 and IPv6.
+
+ @ivar type: A string describing the type of transport, either 'TCP' or
+ 'UDP'.
+
+ @ivar host: A string containing the presentation format of the IP address;
+ for example, "127.0.0.1" or "::1".
+ @type host: C{str}
+
+ @ivar port: An integer representing the port number.
+ @type port: C{int}
+ """
+
+ implements(IAddress)
+
+ compareAttributes = ('type', 'host', 'port')
+
+ def __init__(self, type, host, port):
+ assert type in ('TCP', 'UDP')
+ self.type = type
+ self.host = host
+ self.port = port
+
+
+ def __repr__(self):
+ return '%s(%s, %r, %d)' % (
+ self.__class__.__name__, self.type, self.host, self.port)
+
+
+ def __hash__(self):
+ return hash((self.type, self.host, self.port))
+
+
+
+class IPv4Address(_IPAddress):
+ """
+ An L{IPv4Address} represents the address of an IPv4 socket endpoint.
+
+ @ivar host: A string containing a dotted-quad IPv4 address; for example,
+ "127.0.0.1".
+ @type host: C{str}
+ """
+
+ def __init__(self, type, host, port, _bwHack=None):
+ _IPAddress.__init__(self, type, host, port)
+ if _bwHack is not None:
+ warnings.warn("twisted.internet.address.IPv4Address._bwHack "
+ "is deprecated since Twisted 11.0",
+ DeprecationWarning, stacklevel=2)
+
+
+
+class IPv6Address(_IPAddress):
+ """
+ An L{IPv6Address} represents the address of an IPv6 socket endpoint.
+
+ @ivar host: A string containing a colon-separated, hexadecimal formatted
+ IPv6 address; for example, "::1".
+ @type host: C{str}
+ """
+
+
+
+class UNIXAddress(object, util.FancyEqMixin):
+ """
+ Object representing a UNIX socket endpoint.
+
+ @ivar name: The filename associated with this socket.
+ @type name: C{str}
+ """
+
+ implements(IAddress)
+
+ compareAttributes = ('name', )
+
+ def __init__(self, name, _bwHack = None):
+ self.name = name
+ if _bwHack is not None:
+ warnings.warn("twisted.internet.address.UNIXAddress._bwHack is deprecated since Twisted 11.0",
+ DeprecationWarning, stacklevel=2)
+
+
+ if getattr(os.path, 'samefile', None) is not None:
+ def __eq__(self, other):
+ """
+ overriding L{util.FancyEqMixin} to ensure the os level samefile
+ check is done if the name attributes do not match.
+ """
+ res = super(UNIXAddress, self).__eq__(other)
+ if not res and self.name and other.name:
+ try:
+ return os.path.samefile(self.name, other.name)
+ except OSError:
+ pass
+ return res
+
+
+ def __repr__(self):
+ return 'UNIXAddress(%r)' % (self.name,)
+
+
+ def __hash__(self):
+ if self.name is None:
+ return hash((self.__class__, None))
+ try:
+ s1 = os.stat(self.name)
+ return hash((s1.st_ino, s1.st_dev))
+ except OSError:
+ return hash(self.name)
+
+
+
+# These are for buildFactory backwards compatability due to
+# stupidity-induced inconsistency.
+
+class _ServerFactoryIPv4Address(IPv4Address):
+ """Backwards compatability hack. Just like IPv4Address in practice."""
+
+ def __eq__(self, other):
+ if isinstance(other, tuple):
+ warnings.warn("IPv4Address.__getitem__ is deprecated. Use attributes instead.",
+ category=DeprecationWarning, stacklevel=2)
+ return (self.host, self.port) == other
+ elif isinstance(other, IPv4Address):
+ a = (self.type, self.host, self.port)
+ b = (other.type, other.host, other.port)
+ return a == b
+ return False
diff --git a/twisted/internet/base.py b/twisted/internet/base.py
new file mode 100644
index 0000000..2c7cfb6
--- /dev/null
+++ b/twisted/internet/base.py
@@ -0,0 +1,1190 @@
+# -*- test-case-name: twisted.test.test_internet -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Very basic functionality for a Reactor implementation.
+"""
+
+import socket # needed only for sync-dns
+from zope.interface import implements, classImplements
+
+import sys
+import warnings
+from heapq import heappush, heappop, heapify
+
+import traceback
+
+from twisted.python.compat import set
+from twisted.python.util import unsignedID
+from twisted.internet.interfaces import IReactorCore, IReactorTime, IReactorThreads
+from twisted.internet.interfaces import IResolverSimple, IReactorPluggableResolver
+from twisted.internet.interfaces import IConnector, IDelayedCall
+from twisted.internet import fdesc, main, error, abstract, defer, threads
+from twisted.python import log, failure, reflect
+from twisted.python.runtime import seconds as runtimeSeconds, platform
+from twisted.internet.defer import Deferred, DeferredList
+from twisted.persisted import styles
+
+# This import is for side-effects! Even if you don't see any code using it
+# in this module, don't delete it.
+from twisted.python import threadable
+
+
+class DelayedCall(styles.Ephemeral):
+
+ implements(IDelayedCall)
+ # enable .debug to record creator call stack, and it will be logged if
+ # an exception occurs while the function is being run
+ debug = False
+ _str = None
+
+ def __init__(self, time, func, args, kw, cancel, reset,
+ seconds=runtimeSeconds):
+ """
+ @param time: Seconds from the epoch at which to call C{func}.
+ @param func: The callable to call.
+ @param args: The positional arguments to pass to the callable.
+ @param kw: The keyword arguments to pass to the callable.
+ @param cancel: A callable which will be called with this
+ DelayedCall before cancellation.
+ @param reset: A callable which will be called with this
+ DelayedCall after changing this DelayedCall's scheduled
+ execution time. The callable should adjust any necessary
+ scheduling details to ensure this DelayedCall is invoked
+ at the new appropriate time.
+ @param seconds: If provided, a no-argument callable which will be
+ used to determine the current time any time that information is
+ needed.
+ """
+ self.time, self.func, self.args, self.kw = time, func, args, kw
+ self.resetter = reset
+ self.canceller = cancel
+ self.seconds = seconds
+ self.cancelled = self.called = 0
+ self.delayed_time = 0
+ if self.debug:
+ self.creator = traceback.format_stack()[:-2]
+
+ def getTime(self):
+ """Return the time at which this call will fire
+
+ @rtype: C{float}
+ @return: The number of seconds after the epoch at which this call is
+ scheduled to be made.
+ """
+ return self.time + self.delayed_time
+
+ def cancel(self):
+ """Unschedule this call
+
+ @raise AlreadyCancelled: Raised if this call has already been
+ unscheduled.
+
+ @raise AlreadyCalled: Raised if this call has already been made.
+ """
+ if self.cancelled:
+ raise error.AlreadyCancelled
+ elif self.called:
+ raise error.AlreadyCalled
+ else:
+ self.canceller(self)
+ self.cancelled = 1
+ if self.debug:
+ self._str = str(self)
+ del self.func, self.args, self.kw
+
+ def reset(self, secondsFromNow):
+ """Reschedule this call for a different time
+
+ @type secondsFromNow: C{float}
+ @param secondsFromNow: The number of seconds from the time of the
+ C{reset} call at which this call will be scheduled.
+
+ @raise AlreadyCancelled: Raised if this call has been cancelled.
+ @raise AlreadyCalled: Raised if this call has already been made.
+ """
+ if self.cancelled:
+ raise error.AlreadyCancelled
+ elif self.called:
+ raise error.AlreadyCalled
+ else:
+ newTime = self.seconds() + secondsFromNow
+ if newTime < self.time:
+ self.delayed_time = 0
+ self.time = newTime
+ self.resetter(self)
+ else:
+ self.delayed_time = newTime - self.time
+
+ def delay(self, secondsLater):
+ """Reschedule this call for a later time
+
+ @type secondsLater: C{float}
+ @param secondsLater: The number of seconds after the originally
+ scheduled time for which to reschedule this call.
+
+ @raise AlreadyCancelled: Raised if this call has been cancelled.
+ @raise AlreadyCalled: Raised if this call has already been made.
+ """
+ if self.cancelled:
+ raise error.AlreadyCancelled
+ elif self.called:
+ raise error.AlreadyCalled
+ else:
+ self.delayed_time += secondsLater
+ if self.delayed_time < 0:
+ self.activate_delay()
+ self.resetter(self)
+
+ def activate_delay(self):
+ self.time += self.delayed_time
+ self.delayed_time = 0
+
+ def active(self):
+ """Determine whether this call is still pending
+
+ @rtype: C{bool}
+ @return: True if this call has not yet been made or cancelled,
+ False otherwise.
+ """
+ return not (self.cancelled or self.called)
+
+
+ def __le__(self, other):
+ """
+ Implement C{<=} operator between two L{DelayedCall} instances.
+
+ Comparison is based on the C{time} attribute (unadjusted by the
+ delayed time).
+ """
+ return self.time <= other.time
+
+
+ def __lt__(self, other):
+ """
+ Implement C{<} operator between two L{DelayedCall} instances.
+
+ Comparison is based on the C{time} attribute (unadjusted by the
+ delayed time).
+ """
+ return self.time < other.time
+
+
+ def __str__(self):
+ if self._str is not None:
+ return self._str
+ if hasattr(self, 'func'):
+ if hasattr(self.func, 'func_name'):
+ func = self.func.func_name
+ if hasattr(self.func, 'im_class'):
+ func = self.func.im_class.__name__ + '.' + func
+ else:
+ func = reflect.safe_repr(self.func)
+ else:
+ func = None
+
+ now = self.seconds()
+ L = ["<DelayedCall 0x%x [%ss] called=%s cancelled=%s" % (
+ unsignedID(self), self.time - now, self.called,
+ self.cancelled)]
+ if func is not None:
+ L.extend((" ", func, "("))
+ if self.args:
+ L.append(", ".join([reflect.safe_repr(e) for e in self.args]))
+ if self.kw:
+ L.append(", ")
+ if self.kw:
+ L.append(", ".join(['%s=%s' % (k, reflect.safe_repr(v)) for (k, v) in self.kw.iteritems()]))
+ L.append(")")
+
+ if self.debug:
+ L.append("\n\ntraceback at creation: \n\n%s" % (' '.join(self.creator)))
+ L.append('>')
+
+ return "".join(L)
+
+
+
+class ThreadedResolver(object):
+ """
+ L{ThreadedResolver} uses a reactor, a threadpool, and
+ L{socket.gethostbyname} to perform name lookups without blocking the
+ reactor thread. It also supports timeouts indepedently from whatever
+ timeout logic L{socket.gethostbyname} might have.
+
+ @ivar reactor: The reactor the threadpool of which will be used to call
+ L{socket.gethostbyname} and the I/O thread of which the result will be
+ delivered.
+ """
+ implements(IResolverSimple)
+
+ def __init__(self, reactor):
+ self.reactor = reactor
+ self._runningQueries = {}
+
+
+ def _fail(self, name, err):
+ err = error.DNSLookupError("address %r not found: %s" % (name, err))
+ return failure.Failure(err)
+
+
+ def _cleanup(self, name, lookupDeferred):
+ userDeferred, cancelCall = self._runningQueries[lookupDeferred]
+ del self._runningQueries[lookupDeferred]
+ userDeferred.errback(self._fail(name, "timeout error"))
+
+
+ def _checkTimeout(self, result, name, lookupDeferred):
+ try:
+ userDeferred, cancelCall = self._runningQueries[lookupDeferred]
+ except KeyError:
+ pass
+ else:
+ del self._runningQueries[lookupDeferred]
+ cancelCall.cancel()
+
+ if isinstance(result, failure.Failure):
+ userDeferred.errback(self._fail(name, result.getErrorMessage()))
+ else:
+ userDeferred.callback(result)
+
+
+ def getHostByName(self, name, timeout = (1, 3, 11, 45)):
+ """
+ See L{twisted.internet.interfaces.IResolverSimple.getHostByName}.
+
+ Note that the elements of C{timeout} are summed and the result is used
+ as a timeout for the lookup. Any intermediate timeout or retry logic
+ is left up to the platform via L{socket.gethostbyname}.
+ """
+ if timeout:
+ timeoutDelay = sum(timeout)
+ else:
+ timeoutDelay = 60
+ userDeferred = defer.Deferred()
+ lookupDeferred = threads.deferToThreadPool(
+ self.reactor, self.reactor.getThreadPool(),
+ socket.gethostbyname, name)
+ cancelCall = self.reactor.callLater(
+ timeoutDelay, self._cleanup, name, lookupDeferred)
+ self._runningQueries[lookupDeferred] = (userDeferred, cancelCall)
+ lookupDeferred.addBoth(self._checkTimeout, name, lookupDeferred)
+ return userDeferred
+
+
+
+class BlockingResolver:
+ implements(IResolverSimple)
+
+ def getHostByName(self, name, timeout = (1, 3, 11, 45)):
+ try:
+ address = socket.gethostbyname(name)
+ except socket.error:
+ msg = "address %r not found" % (name,)
+ err = error.DNSLookupError(msg)
+ return defer.fail(err)
+ else:
+ return defer.succeed(address)
+
+
+class _ThreePhaseEvent(object):
+ """
+ Collection of callables (with arguments) which can be invoked as a group in
+ a particular order.
+
+ This provides the underlying implementation for the reactor's system event
+ triggers. An instance of this class tracks triggers for all phases of a
+ single type of event.
+
+ @ivar before: A list of the before-phase triggers containing three-tuples
+ of a callable, a tuple of positional arguments, and a dict of keyword
+ arguments
+
+ @ivar finishedBefore: A list of the before-phase triggers which have
+ already been executed. This is only populated in the C{'BEFORE'} state.
+
+ @ivar during: A list of the during-phase triggers containing three-tuples
+ of a callable, a tuple of positional arguments, and a dict of keyword
+ arguments
+
+ @ivar after: A list of the after-phase triggers containing three-tuples
+ of a callable, a tuple of positional arguments, and a dict of keyword
+ arguments
+
+ @ivar state: A string indicating what is currently going on with this
+ object. One of C{'BASE'} (for when nothing in particular is happening;
+ this is the initial value), C{'BEFORE'} (when the before-phase triggers
+ are in the process of being executed).
+ """
+ def __init__(self):
+ self.before = []
+ self.during = []
+ self.after = []
+ self.state = 'BASE'
+
+
+ def addTrigger(self, phase, callable, *args, **kwargs):
+ """
+ Add a trigger to the indicate phase.
+
+ @param phase: One of C{'before'}, C{'during'}, or C{'after'}.
+
+ @param callable: An object to be called when this event is triggered.
+ @param *args: Positional arguments to pass to C{callable}.
+ @param **kwargs: Keyword arguments to pass to C{callable}.
+
+ @return: An opaque handle which may be passed to L{removeTrigger} to
+ reverse the effects of calling this method.
+ """
+ if phase not in ('before', 'during', 'after'):
+ raise KeyError("invalid phase")
+ getattr(self, phase).append((callable, args, kwargs))
+ return phase, callable, args, kwargs
+
+
+ def removeTrigger(self, handle):
+ """
+ Remove a previously added trigger callable.
+
+ @param handle: An object previously returned by L{addTrigger}. The
+ trigger added by that call will be removed.
+
+ @raise ValueError: If the trigger associated with C{handle} has already
+ been removed or if C{handle} is not a valid handle.
+ """
+ return getattr(self, 'removeTrigger_' + self.state)(handle)
+
+
+ def removeTrigger_BASE(self, handle):
+ """
+ Just try to remove the trigger.
+
+ @see: removeTrigger
+ """
+ try:
+ phase, callable, args, kwargs = handle
+ except (TypeError, ValueError):
+ raise ValueError("invalid trigger handle")
+ else:
+ if phase not in ('before', 'during', 'after'):
+ raise KeyError("invalid phase")
+ getattr(self, phase).remove((callable, args, kwargs))
+
+
+ def removeTrigger_BEFORE(self, handle):
+ """
+ Remove the trigger if it has yet to be executed, otherwise emit a
+ warning that in the future an exception will be raised when removing an
+ already-executed trigger.
+
+ @see: removeTrigger
+ """
+ phase, callable, args, kwargs = handle
+ if phase != 'before':
+ return self.removeTrigger_BASE(handle)
+ if (callable, args, kwargs) in self.finishedBefore:
+ warnings.warn(
+ "Removing already-fired system event triggers will raise an "
+ "exception in a future version of Twisted.",
+ category=DeprecationWarning,
+ stacklevel=3)
+ else:
+ self.removeTrigger_BASE(handle)
+
+
+ def fireEvent(self):
+ """
+ Call the triggers added to this event.
+ """
+ self.state = 'BEFORE'
+ self.finishedBefore = []
+ beforeResults = []
+ while self.before:
+ callable, args, kwargs = self.before.pop(0)
+ self.finishedBefore.append((callable, args, kwargs))
+ try:
+ result = callable(*args, **kwargs)
+ except:
+ log.err()
+ else:
+ if isinstance(result, Deferred):
+ beforeResults.append(result)
+ DeferredList(beforeResults).addCallback(self._continueFiring)
+
+
+ def _continueFiring(self, ignored):
+ """
+ Call the during and after phase triggers for this event.
+ """
+ self.state = 'BASE'
+ self.finishedBefore = []
+ for phase in self.during, self.after:
+ while phase:
+ callable, args, kwargs = phase.pop(0)
+ try:
+ callable(*args, **kwargs)
+ except:
+ log.err()
+
+
+
+class ReactorBase(object):
+ """
+ Default base class for Reactors.
+
+ @type _stopped: C{bool}
+ @ivar _stopped: A flag which is true between paired calls to C{reactor.run}
+ and C{reactor.stop}. This should be replaced with an explicit state
+ machine.
+
+ @type _justStopped: C{bool}
+ @ivar _justStopped: A flag which is true between the time C{reactor.stop}
+ is called and the time the shutdown system event is fired. This is
+ used to determine whether that event should be fired after each
+ iteration through the mainloop. This should be replaced with an
+ explicit state machine.
+
+ @type _started: C{bool}
+ @ivar _started: A flag which is true from the time C{reactor.run} is called
+ until the time C{reactor.run} returns. This is used to prevent calls
+ to C{reactor.run} on a running reactor. This should be replaced with
+ an explicit state machine.
+
+ @ivar running: See L{IReactorCore.running}
+
+ @ivar _registerAsIOThread: A flag controlling whether the reactor will
+ register the thread it is running in as the I/O thread when it starts.
+ If C{True}, registration will be done, otherwise it will not be.
+ """
+ implements(IReactorCore, IReactorTime, IReactorPluggableResolver)
+
+ _registerAsIOThread = True
+
+ _stopped = True
+ installed = False
+ usingThreads = False
+ resolver = BlockingResolver()
+
+ __name__ = "twisted.internet.reactor"
+
+ def __init__(self):
+ self.threadCallQueue = []
+ self._eventTriggers = {}
+ self._pendingTimedCalls = []
+ self._newTimedCalls = []
+ self._cancellations = 0
+ self.running = False
+ self._started = False
+ self._justStopped = False
+ self._startedBefore = False
+ # reactor internal readers, e.g. the waker.
+ self._internalReaders = set()
+ self.waker = None
+
+ # Arrange for the running attribute to change to True at the right time
+ # and let a subclass possibly do other things at that time (eg install
+ # signal handlers).
+ self.addSystemEventTrigger(
+ 'during', 'startup', self._reallyStartRunning)
+ self.addSystemEventTrigger('during', 'shutdown', self.crash)
+ self.addSystemEventTrigger('during', 'shutdown', self.disconnectAll)
+
+ if platform.supportsThreads():
+ self._initThreads()
+ self.installWaker()
+
+ # override in subclasses
+
+ _lock = None
+
+ def installWaker(self):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement installWaker")
+
+ def installResolver(self, resolver):
+ assert IResolverSimple.providedBy(resolver)
+ oldResolver = self.resolver
+ self.resolver = resolver
+ return oldResolver
+
+ def wakeUp(self):
+ """
+ Wake up the event loop.
+ """
+ if self.waker:
+ self.waker.wakeUp()
+ # if the waker isn't installed, the reactor isn't running, and
+ # therefore doesn't need to be woken up
+
+ def doIteration(self, delay):
+ """
+ Do one iteration over the readers and writers which have been added.
+ """
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement doIteration")
+
+ def addReader(self, reader):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement addReader")
+
+ def addWriter(self, writer):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement addWriter")
+
+ def removeReader(self, reader):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement removeReader")
+
+ def removeWriter(self, writer):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement removeWriter")
+
+ def removeAll(self):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement removeAll")
+
+
+ def getReaders(self):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement getReaders")
+
+
+ def getWriters(self):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement getWriters")
+
+
+ def resolve(self, name, timeout = (1, 3, 11, 45)):
+ """Return a Deferred that will resolve a hostname.
+ """
+ if not name:
+ # XXX - This is *less than* '::', and will screw up IPv6 servers
+ return defer.succeed('0.0.0.0')
+ if abstract.isIPAddress(name):
+ return defer.succeed(name)
+ return self.resolver.getHostByName(name, timeout)
+
+ # Installation.
+
+ # IReactorCore
+ def stop(self):
+ """
+ See twisted.internet.interfaces.IReactorCore.stop.
+ """
+ if self._stopped:
+ raise error.ReactorNotRunning(
+ "Can't stop reactor that isn't running.")
+ self._stopped = True
+ self._justStopped = True
+ self._startedBefore = True
+
+
+ def crash(self):
+ """
+ See twisted.internet.interfaces.IReactorCore.crash.
+
+ Reset reactor state tracking attributes and re-initialize certain
+ state-transition helpers which were set up in C{__init__} but later
+ destroyed (through use).
+ """
+ self._started = False
+ self.running = False
+ self.addSystemEventTrigger(
+ 'during', 'startup', self._reallyStartRunning)
+
+ def sigInt(self, *args):
+ """Handle a SIGINT interrupt.
+ """
+ log.msg("Received SIGINT, shutting down.")
+ self.callFromThread(self.stop)
+
+ def sigBreak(self, *args):
+ """Handle a SIGBREAK interrupt.
+ """
+ log.msg("Received SIGBREAK, shutting down.")
+ self.callFromThread(self.stop)
+
+ def sigTerm(self, *args):
+ """Handle a SIGTERM interrupt.
+ """
+ log.msg("Received SIGTERM, shutting down.")
+ self.callFromThread(self.stop)
+
+ def disconnectAll(self):
+ """Disconnect every reader, and writer in the system.
+ """
+ selectables = self.removeAll()
+ for reader in selectables:
+ log.callWithLogger(reader,
+ reader.connectionLost,
+ failure.Failure(main.CONNECTION_LOST))
+
+
+ def iterate(self, delay=0):
+ """See twisted.internet.interfaces.IReactorCore.iterate.
+ """
+ self.runUntilCurrent()
+ self.doIteration(delay)
+
+
+ def fireSystemEvent(self, eventType):
+ """See twisted.internet.interfaces.IReactorCore.fireSystemEvent.
+ """
+ event = self._eventTriggers.get(eventType)
+ if event is not None:
+ event.fireEvent()
+
+
+ def addSystemEventTrigger(self, _phase, _eventType, _f, *args, **kw):
+ """See twisted.internet.interfaces.IReactorCore.addSystemEventTrigger.
+ """
+ assert callable(_f), "%s is not callable" % _f
+ if _eventType not in self._eventTriggers:
+ self._eventTriggers[_eventType] = _ThreePhaseEvent()
+ return (_eventType, self._eventTriggers[_eventType].addTrigger(
+ _phase, _f, *args, **kw))
+
+
+ def removeSystemEventTrigger(self, triggerID):
+ """See twisted.internet.interfaces.IReactorCore.removeSystemEventTrigger.
+ """
+ eventType, handle = triggerID
+ self._eventTriggers[eventType].removeTrigger(handle)
+
+
+ def callWhenRunning(self, _callable, *args, **kw):
+ """See twisted.internet.interfaces.IReactorCore.callWhenRunning.
+ """
+ if self.running:
+ _callable(*args, **kw)
+ else:
+ return self.addSystemEventTrigger('after', 'startup',
+ _callable, *args, **kw)
+
+ def startRunning(self):
+ """
+ Method called when reactor starts: do some initialization and fire
+ startup events.
+
+ Don't call this directly, call reactor.run() instead: it should take
+ care of calling this.
+
+ This method is somewhat misnamed. The reactor will not necessarily be
+ in the running state by the time this method returns. The only
+ guarantee is that it will be on its way to the running state.
+ """
+ if self._started:
+ raise error.ReactorAlreadyRunning()
+ if self._startedBefore:
+ raise error.ReactorNotRestartable()
+ self._started = True
+ self._stopped = False
+ if self._registerAsIOThread:
+ threadable.registerAsIOThread()
+ self.fireSystemEvent('startup')
+
+
+ def _reallyStartRunning(self):
+ """
+ Method called to transition to the running state. This should happen
+ in the I{during startup} event trigger phase.
+ """
+ self.running = True
+
+ # IReactorTime
+
+ seconds = staticmethod(runtimeSeconds)
+
+ def callLater(self, _seconds, _f, *args, **kw):
+ """See twisted.internet.interfaces.IReactorTime.callLater.
+ """
+ assert callable(_f), "%s is not callable" % _f
+ assert sys.maxint >= _seconds >= 0, \
+ "%s is not greater than or equal to 0 seconds" % (_seconds,)
+ tple = DelayedCall(self.seconds() + _seconds, _f, args, kw,
+ self._cancelCallLater,
+ self._moveCallLaterSooner,
+ seconds=self.seconds)
+ self._newTimedCalls.append(tple)
+ return tple
+
+ def _moveCallLaterSooner(self, tple):
+ # Linear time find: slow.
+ heap = self._pendingTimedCalls
+ try:
+ pos = heap.index(tple)
+
+ # Move elt up the heap until it rests at the right place.
+ elt = heap[pos]
+ while pos != 0:
+ parent = (pos-1) // 2
+ if heap[parent] <= elt:
+ break
+ # move parent down
+ heap[pos] = heap[parent]
+ pos = parent
+ heap[pos] = elt
+ except ValueError:
+ # element was not found in heap - oh well...
+ pass
+
+ def _cancelCallLater(self, tple):
+ self._cancellations+=1
+
+
+ def getDelayedCalls(self):
+ """Return all the outstanding delayed calls in the system.
+ They are returned in no particular order.
+ This method is not efficient -- it is really only meant for
+ test cases."""
+ return [x for x in (self._pendingTimedCalls + self._newTimedCalls) if not x.cancelled]
+
+ def _insertNewDelayedCalls(self):
+ for call in self._newTimedCalls:
+ if call.cancelled:
+ self._cancellations-=1
+ else:
+ call.activate_delay()
+ heappush(self._pendingTimedCalls, call)
+ self._newTimedCalls = []
+
+ def timeout(self):
+ # insert new delayed calls to make sure to include them in timeout value
+ self._insertNewDelayedCalls()
+
+ if not self._pendingTimedCalls:
+ return None
+
+ return max(0, self._pendingTimedCalls[0].time - self.seconds())
+
+
+ def runUntilCurrent(self):
+ """Run all pending timed calls.
+ """
+ if self.threadCallQueue:
+ # Keep track of how many calls we actually make, as we're
+ # making them, in case another call is added to the queue
+ # while we're in this loop.
+ count = 0
+ total = len(self.threadCallQueue)
+ for (f, a, kw) in self.threadCallQueue:
+ try:
+ f(*a, **kw)
+ except:
+ log.err()
+ count += 1
+ if count == total:
+ break
+ del self.threadCallQueue[:count]
+ if self.threadCallQueue:
+ self.wakeUp()
+
+ # insert new delayed calls now
+ self._insertNewDelayedCalls()
+
+ now = self.seconds()
+ while self._pendingTimedCalls and (self._pendingTimedCalls[0].time <= now):
+ call = heappop(self._pendingTimedCalls)
+ if call.cancelled:
+ self._cancellations-=1
+ continue
+
+ if call.delayed_time > 0:
+ call.activate_delay()
+ heappush(self._pendingTimedCalls, call)
+ continue
+
+ try:
+ call.called = 1
+ call.func(*call.args, **call.kw)
+ except:
+ log.deferr()
+ if hasattr(call, "creator"):
+ e = "\n"
+ e += " C: previous exception occurred in " + \
+ "a DelayedCall created here:\n"
+ e += " C:"
+ e += "".join(call.creator).rstrip().replace("\n","\n C:")
+ e += "\n"
+ log.msg(e)
+
+
+ if (self._cancellations > 50 and
+ self._cancellations > len(self._pendingTimedCalls) >> 1):
+ self._cancellations = 0
+ self._pendingTimedCalls = [x for x in self._pendingTimedCalls
+ if not x.cancelled]
+ heapify(self._pendingTimedCalls)
+
+ if self._justStopped:
+ self._justStopped = False
+ self.fireSystemEvent("shutdown")
+
+ # IReactorProcess
+
+ def _checkProcessArgs(self, args, env):
+ """
+ Check for valid arguments and environment to spawnProcess.
+
+ @return: A two element tuple giving values to use when creating the
+ process. The first element of the tuple is a C{list} of C{str}
+ giving the values for argv of the child process. The second element
+ of the tuple is either C{None} if C{env} was C{None} or a C{dict}
+ mapping C{str} environment keys to C{str} environment values.
+ """
+ # Any unicode string which Python would successfully implicitly
+ # encode to a byte string would have worked before these explicit
+ # checks were added. Anything which would have failed with a
+ # UnicodeEncodeError during that implicit encoding step would have
+ # raised an exception in the child process and that would have been
+ # a pain in the butt to debug.
+ #
+ # So, we will explicitly attempt the same encoding which Python
+ # would implicitly do later. If it fails, we will report an error
+ # without ever spawning a child process. If it succeeds, we'll save
+ # the result so that Python doesn't need to do it implicitly later.
+ #
+ # For any unicode which we can actually encode, we'll also issue a
+ # deprecation warning, because no one should be passing unicode here
+ # anyway.
+ #
+ # -exarkun
+ defaultEncoding = sys.getdefaultencoding()
+
+ # Common check function
+ def argChecker(arg):
+ """
+ Return either a str or None. If the given value is not
+ allowable for some reason, None is returned. Otherwise, a
+ possibly different object which should be used in place of arg
+ is returned. This forces unicode encoding to happen now, rather
+ than implicitly later.
+ """
+ if isinstance(arg, unicode):
+ try:
+ arg = arg.encode(defaultEncoding)
+ except UnicodeEncodeError:
+ return None
+ warnings.warn(
+ "Argument strings and environment keys/values passed to "
+ "reactor.spawnProcess should be str, not unicode.",
+ category=DeprecationWarning,
+ stacklevel=4)
+ if isinstance(arg, str) and '\0' not in arg:
+ return arg
+ return None
+
+ # Make a few tests to check input validity
+ if not isinstance(args, (tuple, list)):
+ raise TypeError("Arguments must be a tuple or list")
+
+ outputArgs = []
+ for arg in args:
+ arg = argChecker(arg)
+ if arg is None:
+ raise TypeError("Arguments contain a non-string value")
+ else:
+ outputArgs.append(arg)
+
+ outputEnv = None
+ if env is not None:
+ outputEnv = {}
+ for key, val in env.iteritems():
+ key = argChecker(key)
+ if key is None:
+ raise TypeError("Environment contains a non-string key")
+ val = argChecker(val)
+ if val is None:
+ raise TypeError("Environment contains a non-string value")
+ outputEnv[key] = val
+ return outputArgs, outputEnv
+
+ # IReactorThreads
+ if platform.supportsThreads():
+ threadpool = None
+ # ID of the trigger starting the threadpool
+ _threadpoolStartupID = None
+ # ID of the trigger stopping the threadpool
+ threadpoolShutdownID = None
+
+ def _initThreads(self):
+ self.usingThreads = True
+ self.resolver = ThreadedResolver(self)
+
+ def callFromThread(self, f, *args, **kw):
+ """
+ See L{twisted.internet.interfaces.IReactorThreads.callFromThread}.
+ """
+ assert callable(f), "%s is not callable" % (f,)
+ # lists are thread-safe in CPython, but not in Jython
+ # this is probably a bug in Jython, but until fixed this code
+ # won't work in Jython.
+ self.threadCallQueue.append((f, args, kw))
+ self.wakeUp()
+
+ def _initThreadPool(self):
+ """
+ Create the threadpool accessible with callFromThread.
+ """
+ from twisted.python import threadpool
+ self.threadpool = threadpool.ThreadPool(
+ 0, 10, 'twisted.internet.reactor')
+ self._threadpoolStartupID = self.callWhenRunning(
+ self.threadpool.start)
+ self.threadpoolShutdownID = self.addSystemEventTrigger(
+ 'during', 'shutdown', self._stopThreadPool)
+
+ def _uninstallHandler(self):
+ pass
+
+ def _stopThreadPool(self):
+ """
+ Stop the reactor threadpool. This method is only valid if there
+ is currently a threadpool (created by L{_initThreadPool}). It
+ is not intended to be called directly; instead, it will be
+ called by a shutdown trigger created in L{_initThreadPool}.
+ """
+ triggers = [self._threadpoolStartupID, self.threadpoolShutdownID]
+ for trigger in filter(None, triggers):
+ try:
+ self.removeSystemEventTrigger(trigger)
+ except ValueError:
+ pass
+ self._threadpoolStartupID = None
+ self.threadpoolShutdownID = None
+ self.threadpool.stop()
+ self.threadpool = None
+
+
+ def getThreadPool(self):
+ """
+ See L{twisted.internet.interfaces.IReactorThreads.getThreadPool}.
+ """
+ if self.threadpool is None:
+ self._initThreadPool()
+ return self.threadpool
+
+
+ def callInThread(self, _callable, *args, **kwargs):
+ """
+ See L{twisted.internet.interfaces.IReactorThreads.callInThread}.
+ """
+ self.getThreadPool().callInThread(_callable, *args, **kwargs)
+
+ def suggestThreadPoolSize(self, size):
+ """
+ See L{twisted.internet.interfaces.IReactorThreads.suggestThreadPoolSize}.
+ """
+ self.getThreadPool().adjustPoolsize(maxthreads=size)
+ else:
+ # This is for signal handlers.
+ def callFromThread(self, f, *args, **kw):
+ assert callable(f), "%s is not callable" % (f,)
+ # See comment in the other callFromThread implementation.
+ self.threadCallQueue.append((f, args, kw))
+
+if platform.supportsThreads():
+ classImplements(ReactorBase, IReactorThreads)
+
+
+class BaseConnector(styles.Ephemeral):
+ """Basic implementation of connector.
+
+ State can be: "connecting", "connected", "disconnected"
+ """
+
+ implements(IConnector)
+
+ timeoutID = None
+ factoryStarted = 0
+
+ def __init__(self, factory, timeout, reactor):
+ self.state = "disconnected"
+ self.reactor = reactor
+ self.factory = factory
+ self.timeout = timeout
+
+ def disconnect(self):
+ """Disconnect whatever our state is."""
+ if self.state == 'connecting':
+ self.stopConnecting()
+ elif self.state == 'connected':
+ self.transport.loseConnection()
+
+ def connect(self):
+ """Start connection to remote server."""
+ if self.state != "disconnected":
+ raise RuntimeError, "can't connect in this state"
+
+ self.state = "connecting"
+ if not self.factoryStarted:
+ self.factory.doStart()
+ self.factoryStarted = 1
+ self.transport = transport = self._makeTransport()
+ if self.timeout is not None:
+ self.timeoutID = self.reactor.callLater(self.timeout, transport.failIfNotConnected, error.TimeoutError())
+ self.factory.startedConnecting(self)
+
+ def stopConnecting(self):
+ """Stop attempting to connect."""
+ if self.state != "connecting":
+ raise error.NotConnectingError, "we're not trying to connect"
+
+ self.state = "disconnected"
+ self.transport.failIfNotConnected(error.UserError())
+ del self.transport
+
+ def cancelTimeout(self):
+ if self.timeoutID is not None:
+ try:
+ self.timeoutID.cancel()
+ except ValueError:
+ pass
+ del self.timeoutID
+
+ def buildProtocol(self, addr):
+ self.state = "connected"
+ self.cancelTimeout()
+ return self.factory.buildProtocol(addr)
+
+ def connectionFailed(self, reason):
+ self.cancelTimeout()
+ self.transport = None
+ self.state = "disconnected"
+ self.factory.clientConnectionFailed(self, reason)
+ if self.state == "disconnected":
+ # factory hasn't called our connect() method
+ self.factory.doStop()
+ self.factoryStarted = 0
+
+ def connectionLost(self, reason):
+ self.state = "disconnected"
+ self.factory.clientConnectionLost(self, reason)
+ if self.state == "disconnected":
+ # factory hasn't called our connect() method
+ self.factory.doStop()
+ self.factoryStarted = 0
+
+ def getDestination(self):
+ raise NotImplementedError(
+ reflect.qual(self.__class__) + " did not implement "
+ "getDestination")
+
+
+
+class BasePort(abstract.FileDescriptor):
+ """Basic implementation of a ListeningPort.
+
+ Note: This does not actually implement IListeningPort.
+ """
+
+ addressFamily = None
+ socketType = None
+
+ def createInternetSocket(self):
+ s = socket.socket(self.addressFamily, self.socketType)
+ s.setblocking(0)
+ fdesc._setCloseOnExec(s.fileno())
+ return s
+
+
+ def doWrite(self):
+ """Raises a RuntimeError"""
+ raise RuntimeError, "doWrite called on a %s" % reflect.qual(self.__class__)
+
+
+
+class _SignalReactorMixin(object):
+ """
+ Private mixin to manage signals: it installs signal handlers at start time,
+ and define run method.
+
+ It can only be used mixed in with L{ReactorBase}, and has to be defined
+ first in the inheritance (so that method resolution order finds
+ startRunning first).
+
+ @type _installSignalHandlers: C{bool}
+ @ivar _installSignalHandlers: A flag which indicates whether any signal
+ handlers will be installed during startup. This includes handlers for
+ SIGCHLD to monitor child processes, and SIGINT, SIGTERM, and SIGBREAK
+ to stop the reactor.
+ """
+
+ _installSignalHandlers = False
+
+ def _handleSignals(self):
+ """
+ Install the signal handlers for the Twisted event loop.
+ """
+ try:
+ import signal
+ except ImportError:
+ log.msg("Warning: signal module unavailable -- "
+ "not installing signal handlers.")
+ return
+
+ if signal.getsignal(signal.SIGINT) == signal.default_int_handler:
+ # only handle if there isn't already a handler, e.g. for Pdb.
+ signal.signal(signal.SIGINT, self.sigInt)
+ signal.signal(signal.SIGTERM, self.sigTerm)
+
+ # Catch Ctrl-Break in windows
+ if hasattr(signal, "SIGBREAK"):
+ signal.signal(signal.SIGBREAK, self.sigBreak)
+
+
+ def startRunning(self, installSignalHandlers=True):
+ """
+ Extend the base implementation in order to remember whether signal
+ handlers should be installed later.
+
+ @type installSignalHandlers: C{bool}
+ @param installSignalHandlers: A flag which, if set, indicates that
+ handlers for a number of (implementation-defined) signals should be
+ installed during startup.
+ """
+ self._installSignalHandlers = installSignalHandlers
+ ReactorBase.startRunning(self)
+
+
+ def _reallyStartRunning(self):
+ """
+ Extend the base implementation by also installing signal handlers, if
+ C{self._installSignalHandlers} is true.
+ """
+ ReactorBase._reallyStartRunning(self)
+ if self._installSignalHandlers:
+ # Make sure this happens before after-startup events, since the
+ # expectation of after-startup is that the reactor is fully
+ # initialized. Don't do it right away for historical reasons
+ # (perhaps some before-startup triggers don't want there to be a
+ # custom SIGCHLD handler so that they can run child processes with
+ # some blocking api).
+ self._handleSignals()
+
+
+ def run(self, installSignalHandlers=True):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ self.mainLoop()
+
+
+ def mainLoop(self):
+ while self._started:
+ try:
+ while self._started:
+ # Advance simulation time in delayed event
+ # processors.
+ self.runUntilCurrent()
+ t2 = self.timeout()
+ t = self.running and t2
+ self.doIteration(t)
+ except:
+ log.msg("Unexpected error in main loop.")
+ log.err()
+ else:
+ log.msg('Main loop terminated.')
+
+
+
+__all__ = []
diff --git a/twisted/internet/cfreactor.py b/twisted/internet/cfreactor.py
new file mode 100644
index 0000000..ef6bf7d
--- /dev/null
+++ b/twisted/internet/cfreactor.py
@@ -0,0 +1,501 @@
+# -*- test-case-name: twisted.internet.test.test_core -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A reactor for integrating with U{CFRunLoop<http://bit.ly/cfrunloop>}, the
+CoreFoundation main loop used by MacOS X.
+
+This is useful for integrating Twisted with U{PyObjC<http://pyobjc.sf.net/>}
+applications.
+"""
+
+__all__ = [
+ 'install',
+ 'CFReactor'
+]
+
+import sys
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IReactorFDSet
+from twisted.internet.posixbase import PosixReactorBase, _Waker
+from twisted.internet.posixbase import _NO_FILEDESC
+
+from twisted.python import log
+
+from CoreFoundation import (
+ CFRunLoopAddSource, CFRunLoopRemoveSource, CFRunLoopGetMain, CFRunLoopRun,
+ CFRunLoopStop, CFRunLoopTimerCreate, CFRunLoopAddTimer,
+ CFRunLoopTimerInvalidate, kCFAllocatorDefault, kCFRunLoopCommonModes,
+ CFAbsoluteTimeGetCurrent)
+
+from CFNetwork import (
+ CFSocketCreateWithNative, CFSocketSetSocketFlags, CFSocketEnableCallBacks,
+ CFSocketCreateRunLoopSource, CFSocketDisableCallBacks, CFSocketInvalidate,
+ kCFSocketWriteCallBack, kCFSocketReadCallBack, kCFSocketConnectCallBack,
+ kCFSocketAutomaticallyReenableReadCallBack,
+ kCFSocketAutomaticallyReenableWriteCallBack)
+
+
+_READ = 0
+_WRITE = 1
+_preserveSOError = 1 << 6
+
+
+class _WakerPlus(_Waker):
+ """
+ The normal Twisted waker will simply wake up the main loop, which causes an
+ iteration to run, which in turn causes L{PosixReactorBase.runUntilCurrent}
+ to get invoked.
+
+ L{CFReactor} has a slightly different model of iteration, though: rather
+ than have each iteration process the thread queue, then timed calls, then
+ file descriptors, each callback is run as it is dispatched by the CFRunLoop
+ observer which triggered it.
+
+ So this waker needs to not only unblock the loop, but also make sure the
+ work gets done; so, it reschedules the invocation of C{runUntilCurrent} to
+ be immediate (0 seconds from now) even if there is no timed call work to
+ do.
+ """
+
+ def doRead(self):
+ """
+ Wake up the loop and force C{runUntilCurrent} to run immediately in the
+ next timed iteration.
+ """
+ result = _Waker.doRead(self)
+ self.reactor._scheduleSimulate(True)
+ return result
+
+
+
+class CFReactor(PosixReactorBase):
+ """
+ The CoreFoundation reactor.
+
+ You probably want to use this via the L{install} API.
+
+ @ivar _fdmap: a dictionary, mapping an integer (a file descriptor) to a
+ 4-tuple of:
+
+ - source: a C{CFRunLoopSource}; the source associated with this
+ socket.
+ - socket: a C{CFSocket} wrapping the file descriptor.
+ - descriptor: an L{IReadDescriptor} and/or L{IWriteDescriptor}
+ provider.
+ - read-write: a 2-C{list} of booleans: respectively, whether this
+ descriptor is currently registered for reading or registered for
+ writing.
+
+ @ivar _idmap: a dictionary, mapping the id() of an L{IReadDescriptor} or
+ L{IWriteDescriptor} to a C{fd} in L{_fdmap}. Implemented in this
+ manner so that we don't have to rely (even more) on the hashability of
+ L{IReadDescriptor} providers, and we know that they won't be collected
+ since these are kept in sync with C{_fdmap}. Necessary because the
+ .fileno() of a file descriptor may change at will, so we need to be
+ able to look up what its file descriptor I{used} to be, so that we can
+ look it up in C{_fdmap}
+
+ @ivar _cfrunloop: the L{CFRunLoop} pyobjc object wrapped by this reactor.
+
+ @ivar _inCFLoop: Is L{CFRunLoopRun} currently running?
+
+ @type _inCFLoop: C{bool}
+
+ @ivar _currentSimulator: if a CFTimer is currently scheduled with the CF
+ run loop to run Twisted callLater calls, this is a reference to it.
+ Otherwise, it is C{None}
+ """
+
+ implements(IReactorFDSet)
+
+ def __init__(self, runLoop=None, runner=None):
+ self._fdmap = {}
+ self._idmap = {}
+ if runner is None:
+ runner = CFRunLoopRun
+ self._runner = runner
+
+ if runLoop is None:
+ runLoop = CFRunLoopGetMain()
+ self._cfrunloop = runLoop
+ PosixReactorBase.__init__(self)
+
+
+ def installWaker(self):
+ """
+ Override C{installWaker} in order to use L{_WakerPlus}; otherwise this
+ should be exactly the same as the parent implementation.
+ """
+ if not self.waker:
+ self.waker = _WakerPlus(self)
+ self._internalReaders.add(self.waker)
+ self.addReader(self.waker)
+
+
+ def _socketCallback(self, cfSocket, callbackType,
+ ignoredAddress, ignoredData, context):
+ """
+ The socket callback issued by CFRunLoop. This will issue C{doRead} or
+ C{doWrite} calls to the L{IReadDescriptor} and L{IWriteDescriptor}
+ registered with the file descriptor that we are being notified of.
+
+ @param cfSocket: The L{CFSocket} which has got some activity.
+
+ @param callbackType: The type of activity that we are being notified
+ of. Either L{kCFSocketReadCallBack} or L{kCFSocketWriteCallBack}.
+
+ @param ignoredAddress: Unused, because this is not used for either of
+ the callback types we register for.
+
+ @param ignoredData: Unused, because this is not used for either of the
+ callback types we register for.
+
+ @param context: The data associated with this callback by
+ L{CFSocketCreateWithNative} (in L{CFReactor._watchFD}). A 2-tuple
+ of C{(int, CFRunLoopSource)}.
+ """
+ (fd, smugglesrc) = context
+ if fd not in self._fdmap:
+ # Spurious notifications seem to be generated sometimes if you
+ # CFSocketDisableCallBacks in the middle of an event. I don't know
+ # about this FD, any more, so let's get rid of it.
+ CFRunLoopRemoveSource(
+ self._cfrunloop, smugglesrc, kCFRunLoopCommonModes
+ )
+ return
+
+ why = None
+ isRead = False
+ src, skt, readWriteDescriptor, rw = self._fdmap[fd]
+ try:
+ if readWriteDescriptor.fileno() == -1:
+ why = _NO_FILEDESC
+ else:
+ isRead = callbackType == kCFSocketReadCallBack
+ # CFSocket seems to deliver duplicate read/write notifications
+ # sometimes, especially a duplicate writability notification
+ # when first registering the socket. This bears further
+ # investigation, since I may have been mis-interpreting the
+ # behavior I was seeing. (Running the full Twisted test suite,
+ # while thorough, is not always entirely clear.) Until this has
+ # been more thoroughly investigated , we consult our own
+ # reading/writing state flags to determine whether we should
+ # actually attempt a doRead/doWrite first. -glyph
+ if isRead:
+ if rw[_READ]:
+ why = log.callWithLogger(
+ readWriteDescriptor, readWriteDescriptor.doRead)
+ else:
+ if rw[_WRITE]:
+ why = log.callWithLogger(
+ readWriteDescriptor, readWriteDescriptor.doWrite)
+ except:
+ why = sys.exc_info()[1]
+ log.err()
+ if why:
+ self._disconnectSelectable(readWriteDescriptor, why, isRead)
+
+
+ def _watchFD(self, fd, descr, flag):
+ """
+ Register a file descriptor with the L{CFRunLoop}, or modify its state
+ so that it's listening for both notifications (read and write) rather
+ than just one; used to implement C{addReader} and C{addWriter}.
+
+ @param fd: The file descriptor.
+
+ @type fd: C{int}
+
+ @param descr: the L{IReadDescriptor} or L{IWriteDescriptor}
+
+ @param flag: the flag to register for callbacks on, either
+ L{kCFSocketReadCallBack} or L{kCFSocketWriteCallBack}
+ """
+ if fd == -1:
+ raise RuntimeError("Invalid file descriptor.")
+ if fd in self._fdmap:
+ src, cfs, gotdescr, rw = self._fdmap[fd]
+ # do I need to verify that it's the same descr?
+ else:
+ ctx = []
+ ctx.append(fd)
+ cfs = CFSocketCreateWithNative(
+ kCFAllocatorDefault, fd,
+ kCFSocketReadCallBack | kCFSocketWriteCallBack |
+ kCFSocketConnectCallBack,
+ self._socketCallback, ctx
+ )
+ CFSocketSetSocketFlags(
+ cfs,
+ kCFSocketAutomaticallyReenableReadCallBack |
+ kCFSocketAutomaticallyReenableWriteCallBack |
+
+ # This extra flag is to ensure that CF doesn't (destructively,
+ # because destructively is the only way to do it) retrieve
+ # SO_ERROR and thereby break twisted.internet.tcp.BaseClient,
+ # which needs SO_ERROR to tell it whether or not it needs to
+ # call connect_ex a second time.
+ _preserveSOError
+ )
+ src = CFSocketCreateRunLoopSource(kCFAllocatorDefault, cfs, 0)
+ ctx.append(src)
+ CFRunLoopAddSource(self._cfrunloop, src, kCFRunLoopCommonModes)
+ CFSocketDisableCallBacks(
+ cfs,
+ kCFSocketReadCallBack | kCFSocketWriteCallBack |
+ kCFSocketConnectCallBack
+ )
+ rw = [False, False]
+ self._idmap[id(descr)] = fd
+ self._fdmap[fd] = src, cfs, descr, rw
+ rw[self._flag2idx(flag)] = True
+ CFSocketEnableCallBacks(cfs, flag)
+
+
+ def _flag2idx(self, flag):
+ """
+ Convert a C{kCFSocket...} constant to an index into the read/write
+ state list (C{_READ} or C{_WRITE}) (the 4th element of the value of
+ C{self._fdmap}).
+
+ @param flag: C{kCFSocketReadCallBack} or C{kCFSocketWriteCallBack}
+
+ @return: C{_READ} or C{_WRITE}
+ """
+ return {kCFSocketReadCallBack: _READ,
+ kCFSocketWriteCallBack: _WRITE}[flag]
+
+
+ def _unwatchFD(self, fd, descr, flag):
+ """
+ Unregister a file descriptor with the L{CFRunLoop}, or modify its state
+ so that it's listening for only one notification (read or write) as
+ opposed to both; used to implement C{removeReader} and C{removeWriter}.
+
+ @param fd: a file descriptor
+
+ @type fd: C{int}
+
+ @param descr: an L{IReadDescriptor} or L{IWriteDescriptor}
+
+ @param flag: L{kCFSocketWriteCallBack} L{kCFSocketReadCallBack}
+ """
+ if id(descr) not in self._idmap:
+ return
+ if fd == -1:
+ # need to deal with it in this case, I think.
+ realfd = self._idmap[id(descr)]
+ else:
+ realfd = fd
+ src, cfs, descr, rw = self._fdmap[realfd]
+ CFSocketDisableCallBacks(cfs, flag)
+ rw[self._flag2idx(flag)] = False
+ if not rw[_READ] and not rw[_WRITE]:
+ del self._idmap[id(descr)]
+ del self._fdmap[realfd]
+ CFRunLoopRemoveSource(self._cfrunloop, src, kCFRunLoopCommonModes)
+ CFSocketInvalidate(cfs)
+
+
+ def addReader(self, reader):
+ """
+ Implement L{IReactorFDSet.addReader}.
+ """
+ self._watchFD(reader.fileno(), reader, kCFSocketReadCallBack)
+
+
+ def addWriter(self, writer):
+ """
+ Implement L{IReactorFDSet.addWriter}.
+ """
+ self._watchFD(writer.fileno(), writer, kCFSocketWriteCallBack)
+
+
+ def removeReader(self, reader):
+ """
+ Implement L{IReactorFDSet.removeReader}.
+ """
+ self._unwatchFD(reader.fileno(), reader, kCFSocketReadCallBack)
+
+
+ def removeWriter(self, writer):
+ """
+ Implement L{IReactorFDSet.removeWriter}.
+ """
+ self._unwatchFD(writer.fileno(), writer, kCFSocketWriteCallBack)
+
+
+ def removeAll(self):
+ """
+ Implement L{IReactorFDSet.removeAll}.
+ """
+ allDesc = set([descr for src, cfs, descr, rw in self._fdmap.values()])
+ allDesc -= set(self._internalReaders)
+ for desc in allDesc:
+ self.removeReader(desc)
+ self.removeWriter(desc)
+ return list(allDesc)
+
+
+ def getReaders(self):
+ """
+ Implement L{IReactorFDSet.getReaders}.
+ """
+ return [descr for src, cfs, descr, rw in self._fdmap.values()
+ if rw[_READ]]
+
+
+ def getWriters(self):
+ """
+ Implement L{IReactorFDSet.getWriters}.
+ """
+ return [descr for src, cfs, descr, rw in self._fdmap.values()
+ if rw[_WRITE]]
+
+
+ def _moveCallLaterSooner(self, tple):
+ """
+ Override L{PosixReactorBase}'s implementation of L{IDelayedCall.reset}
+ so that it will immediately reschedule. Normally
+ C{_moveCallLaterSooner} depends on the fact that C{runUntilCurrent} is
+ always run before the mainloop goes back to sleep, so this forces it to
+ immediately recompute how long the loop needs to stay asleep.
+ """
+ result = PosixReactorBase._moveCallLaterSooner(self, tple)
+ self._scheduleSimulate()
+ return result
+
+
+ _inCFLoop = False
+
+ def mainLoop(self):
+ """
+ Run the runner (L{CFRunLoopRun} or something that calls it), which runs
+ the run loop until C{crash()} is called.
+ """
+ self._inCFLoop = True
+ try:
+ self._runner()
+ finally:
+ self._inCFLoop = False
+
+
+ _currentSimulator = None
+
+ def _scheduleSimulate(self, force=False):
+ """
+ Schedule a call to C{self.runUntilCurrent}. This will cancel the
+ currently scheduled call if it is already scheduled.
+
+ @param force: Even if there are no timed calls, make sure that
+ C{runUntilCurrent} runs immediately (in a 0-seconds-from-now
+ {CFRunLoopTimer}). This is necessary for calls which need to
+ trigger behavior of C{runUntilCurrent} other than running timed
+ calls, such as draining the thread call queue or calling C{crash()}
+ when the appropriate flags are set.
+
+ @type force: C{bool}
+ """
+ if self._currentSimulator is not None:
+ CFRunLoopTimerInvalidate(self._currentSimulator)
+ self._currentSimulator = None
+ timeout = self.timeout()
+ if force:
+ timeout = 0.0
+ if timeout is not None:
+ fireDate = (CFAbsoluteTimeGetCurrent() + timeout)
+ def simulate(cftimer, extra):
+ self._currentSimulator = None
+ self.runUntilCurrent()
+ self._scheduleSimulate()
+ c = self._currentSimulator = CFRunLoopTimerCreate(
+ kCFAllocatorDefault, fireDate,
+ 0, 0, 0, simulate, None
+ )
+ CFRunLoopAddTimer(self._cfrunloop, c, kCFRunLoopCommonModes)
+
+
+ def callLater(self, _seconds, _f, *args, **kw):
+ """
+ Implement L{IReactorTime.callLater}.
+ """
+ delayedCall = PosixReactorBase.callLater(
+ self, _seconds, _f, *args, **kw
+ )
+ self._scheduleSimulate()
+ return delayedCall
+
+
+ def stop(self):
+ """
+ Implement L{IReactorCore.stop}.
+ """
+ PosixReactorBase.stop(self)
+ self._scheduleSimulate(True)
+
+
+ def crash(self):
+ """
+ Implement L{IReactorCore.crash}
+ """
+ wasStarted = self._started
+ PosixReactorBase.crash(self)
+ if self._inCFLoop:
+ self._stopNow()
+ else:
+ if wasStarted:
+ self.callLater(0, self._stopNow)
+
+
+ def _stopNow(self):
+ """
+ Immediately stop the CFRunLoop (which must be running!).
+ """
+ CFRunLoopStop(self._cfrunloop)
+
+
+ def iterate(self, delay=0):
+ """
+ Emulate the behavior of C{iterate()} for things that want to call it,
+ by letting the loop run for a little while and then scheduling a timed
+ call to exit it.
+ """
+ self.callLater(delay, self._stopNow)
+ self.mainLoop()
+
+
+
+def install(runLoop=None, runner=None):
+ """
+ Configure the twisted mainloop to be run inside CFRunLoop.
+
+ @param runLoop: the run loop to use.
+
+ @param runner: the function to call in order to actually invoke the main
+ loop. This will default to L{CFRunLoopRun} if not specified. However,
+ this is not an appropriate choice for GUI applications, as you need to
+ run NSApplicationMain (or something like it). For example, to run the
+ Twisted mainloop in a PyObjC application, your C{main.py} should look
+ something like this::
+
+ from PyObjCTools import AppHelper
+ from twisted.internet.cfreactor import install
+ install(runner=AppHelper.runEventLoop)
+ # initialize your application
+ reactor.run()
+
+ @return: The installed reactor.
+
+ @rtype: L{CFReactor}
+ """
+
+ reactor = CFReactor(runLoop=runLoop, runner=runner)
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
diff --git a/twisted/internet/default.py b/twisted/internet/default.py
new file mode 100644
index 0000000..b9aa199
--- /dev/null
+++ b/twisted/internet/default.py
@@ -0,0 +1,56 @@
+# -*- test-case-name: twisted.internet.test.test_default -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The most suitable default reactor for the current platform.
+
+Depending on a specific application's needs, some other reactor may in
+fact be better.
+"""
+
+__all__ = ["install"]
+
+from twisted.python.runtime import platform
+
+
+def _getInstallFunction(platform):
+ """
+ Return a function to install the reactor most suited for the given platform.
+
+ @param platform: The platform for which to select a reactor.
+ @type platform: L{twisted.python.runtime.Platform}
+
+ @return: A zero-argument callable which will install the selected
+ reactor.
+ """
+ # Linux: epoll(7) is the fault, since it scales well.
+ #
+ # OS X: poll(2) is not exposed by Python because it doesn't
+ # support all file descriptors (in particular, lack of PTY support
+ # is a problem) -- see <http://bugs.python.org/issue5154>. kqueue
+ # reactor is being rewritten (see
+ # <http://twistedmatrix.com/trac/ticket/1918>), and also has same
+ # restriction as poll(2) as far PTY support goes.
+ #
+ # Windows: IOCP should eventually be default, but still has some serious
+ # bugs, e.g. <http://twistedmatrix.com/trac/ticket/4667>.
+ #
+ # We therefore choose epoll(7) on Linux, poll(2) on other non-OS X POSIX
+ # platforms, and select(2) everywhere else.
+ try:
+ if platform.isLinux():
+ try:
+ from twisted.internet.epollreactor import install
+ except ImportError:
+ from twisted.internet.pollreactor import install
+ elif platform.getType() == 'posix' and not platform.isMacOSX():
+ from twisted.internet.pollreactor import install
+ else:
+ from twisted.internet.selectreactor import install
+ except ImportError:
+ from twisted.internet.selectreactor import install
+ return install
+
+
+install = _getInstallFunction(platform)
diff --git a/twisted/internet/defer.py b/twisted/internet/defer.py
new file mode 100644
index 0000000..f1f05a4
--- /dev/null
+++ b/twisted/internet/defer.py
@@ -0,0 +1,1561 @@
+# -*- test-case-name: twisted.test.test_defer,twisted.test.test_defgen,twisted.internet.test.test_inlinecb -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support for results that aren't immediately available.
+
+Maintainer: Glyph Lefkowitz
+
+@var _NO_RESULT: The result used to represent the fact that there is no
+ result. B{Never ever ever use this as an actual result for a Deferred}. You
+ have been warned.
+
+@var _CONTINUE: A marker left in L{Deferred.callbacks} to indicate a Deferred
+ chain. Always accompanied by a Deferred instance in the args tuple pointing
+ at the Deferred which is chained to the Deferred which has this marker.
+"""
+
+import traceback
+import types
+import warnings
+from sys import exc_info
+
+# Twisted imports
+from twisted.python import log, failure, lockfile
+from twisted.python.util import unsignedID, mergeFunctionMetadata
+
+
+
+class AlreadyCalledError(Exception):
+ pass
+
+
+class CancelledError(Exception):
+ """
+ This error is raised by default when a L{Deferred} is cancelled.
+ """
+
+
+class TimeoutError(Exception):
+ """
+ This exception is deprecated. It is used only by the deprecated
+ L{Deferred.setTimeout} method.
+ """
+
+
+
+def logError(err):
+ log.err(err)
+ return err
+
+
+
+def succeed(result):
+ """
+ Return a L{Deferred} that has already had C{.callback(result)} called.
+
+ This is useful when you're writing synchronous code to an
+ asynchronous interface: i.e., some code is calling you expecting a
+ L{Deferred} result, but you don't actually need to do anything
+ asynchronous. Just return C{defer.succeed(theResult)}.
+
+ See L{fail} for a version of this function that uses a failing
+ L{Deferred} rather than a successful one.
+
+ @param result: The result to give to the Deferred's 'callback'
+ method.
+
+ @rtype: L{Deferred}
+ """
+ d = Deferred()
+ d.callback(result)
+ return d
+
+
+
+def fail(result=None):
+ """
+ Return a L{Deferred} that has already had C{.errback(result)} called.
+
+ See L{succeed}'s docstring for rationale.
+
+ @param result: The same argument that L{Deferred.errback} takes.
+
+ @raise NoCurrentExceptionError: If C{result} is C{None} but there is no
+ current exception state.
+
+ @rtype: L{Deferred}
+ """
+ d = Deferred()
+ d.errback(result)
+ return d
+
+
+
+def execute(callable, *args, **kw):
+ """
+ Create a L{Deferred} from a callable and arguments.
+
+ Call the given function with the given arguments. Return a L{Deferred}
+ which has been fired with its callback as the result of that invocation
+ or its C{errback} with a L{Failure} for the exception thrown.
+ """
+ try:
+ result = callable(*args, **kw)
+ except:
+ return fail()
+ else:
+ return succeed(result)
+
+
+
+def maybeDeferred(f, *args, **kw):
+ """
+ Invoke a function that may or may not return a L{Deferred}.
+
+ Call the given function with the given arguments. If the returned
+ object is a L{Deferred}, return it. If the returned object is a L{Failure},
+ wrap it with L{fail} and return it. Otherwise, wrap it in L{succeed} and
+ return it. If an exception is raised, convert it to a L{Failure}, wrap it
+ in L{fail}, and then return it.
+
+ @type f: Any callable
+ @param f: The callable to invoke
+
+ @param args: The arguments to pass to C{f}
+ @param kw: The keyword arguments to pass to C{f}
+
+ @rtype: L{Deferred}
+ @return: The result of the function call, wrapped in a L{Deferred} if
+ necessary.
+ """
+ try:
+ result = f(*args, **kw)
+ except:
+ return fail(failure.Failure(captureVars=Deferred.debug))
+
+ if isinstance(result, Deferred):
+ return result
+ elif isinstance(result, failure.Failure):
+ return fail(result)
+ else:
+ return succeed(result)
+
+
+
+def timeout(deferred):
+ deferred.errback(failure.Failure(TimeoutError("Callback timed out")))
+
+
+
+def passthru(arg):
+ return arg
+
+
+
+def setDebugging(on):
+ """
+ Enable or disable L{Deferred} debugging.
+
+ When debugging is on, the call stacks from creation and invocation are
+ recorded, and added to any L{AlreadyCalledErrors} we raise.
+ """
+ Deferred.debug=bool(on)
+
+
+
+def getDebugging():
+ """
+ Determine whether L{Deferred} debugging is enabled.
+ """
+ return Deferred.debug
+
+
+# See module docstring.
+_NO_RESULT = object()
+_CONTINUE = object()
+
+
+
+class Deferred:
+ """
+ This is a callback which will be put off until later.
+
+ Why do we want this? Well, in cases where a function in a threaded
+ program would block until it gets a result, for Twisted it should
+ not block. Instead, it should return a L{Deferred}.
+
+ This can be implemented for protocols that run over the network by
+ writing an asynchronous protocol for L{twisted.internet}. For methods
+ that come from outside packages that are not under our control, we use
+ threads (see for example L{twisted.enterprise.adbapi}).
+
+ For more information about Deferreds, see doc/core/howto/defer.html or
+ U{http://twistedmatrix.com/documents/current/core/howto/defer.html}
+
+ When creating a Deferred, you may provide a canceller function, which
+ will be called by d.cancel() to let you do any clean-up necessary if the
+ user decides not to wait for the deferred to complete.
+
+ @ivar called: A flag which is C{False} until either C{callback} or
+ C{errback} is called and afterwards always C{True}.
+ @type called: C{bool}
+
+ @ivar paused: A counter of how many unmatched C{pause} calls have been made
+ on this instance.
+ @type paused: C{int}
+
+ @ivar _suppressAlreadyCalled: A flag used by the cancellation mechanism
+ which is C{True} if the Deferred has no canceller and has been
+ cancelled, C{False} otherwise. If C{True}, it can be expected that
+ C{callback} or C{errback} will eventually be called and the result
+ should be silently discarded.
+ @type _suppressAlreadyCalled: C{bool}
+
+ @ivar _runningCallbacks: A flag which is C{True} while this instance is
+ executing its callback chain, used to stop recursive execution of
+ L{_runCallbacks}
+ @type _runningCallbacks: C{bool}
+
+ @ivar _chainedTo: If this Deferred is waiting for the result of another
+ Deferred, this is a reference to the other Deferred. Otherwise, C{None}.
+ """
+
+ called = False
+ paused = 0
+ _debugInfo = None
+ _suppressAlreadyCalled = False
+
+ # Are we currently running a user-installed callback? Meant to prevent
+ # recursive running of callbacks when a reentrant call to add a callback is
+ # used.
+ _runningCallbacks = False
+
+ # Keep this class attribute for now, for compatibility with code that
+ # sets it directly.
+ debug = False
+
+ _chainedTo = None
+
+ def __init__(self, canceller=None):
+ """
+ Initialize a L{Deferred}.
+
+ @param canceller: a callable used to stop the pending operation
+ scheduled by this L{Deferred} when L{Deferred.cancel} is
+ invoked. The canceller will be passed the deferred whose
+ cancelation is requested (i.e., self).
+
+ If a canceller is not given, or does not invoke its argument's
+ C{callback} or C{errback} method, L{Deferred.cancel} will
+ invoke L{Deferred.errback} with a L{CancelledError}.
+
+ Note that if a canceller is not given, C{callback} or
+ C{errback} may still be invoked exactly once, even though
+ defer.py will have already invoked C{errback}, as described
+ above. This allows clients of code which returns a L{Deferred}
+ to cancel it without requiring the L{Deferred} instantiator to
+ provide any specific implementation support for cancellation.
+ New in 10.1.
+
+ @type canceller: a 1-argument callable which takes a L{Deferred}. The
+ return result is ignored.
+ """
+ self.callbacks = []
+ self._canceller = canceller
+ if self.debug:
+ self._debugInfo = DebugInfo()
+ self._debugInfo.creator = traceback.format_stack()[:-1]
+
+
+ def addCallbacks(self, callback, errback=None,
+ callbackArgs=None, callbackKeywords=None,
+ errbackArgs=None, errbackKeywords=None):
+ """
+ Add a pair of callbacks (success and error) to this L{Deferred}.
+
+ These will be executed when the 'master' callback is run.
+
+ @return: C{self}.
+ @rtype: a L{Deferred}
+ """
+ assert callable(callback)
+ assert errback == None or callable(errback)
+ cbs = ((callback, callbackArgs, callbackKeywords),
+ (errback or (passthru), errbackArgs, errbackKeywords))
+ self.callbacks.append(cbs)
+
+ if self.called:
+ self._runCallbacks()
+ return self
+
+
+ def addCallback(self, callback, *args, **kw):
+ """
+ Convenience method for adding just a callback.
+
+ See L{addCallbacks}.
+ """
+ return self.addCallbacks(callback, callbackArgs=args,
+ callbackKeywords=kw)
+
+
+ def addErrback(self, errback, *args, **kw):
+ """
+ Convenience method for adding just an errback.
+
+ See L{addCallbacks}.
+ """
+ return self.addCallbacks(passthru, errback,
+ errbackArgs=args,
+ errbackKeywords=kw)
+
+
+ def addBoth(self, callback, *args, **kw):
+ """
+ Convenience method for adding a single callable as both a callback
+ and an errback.
+
+ See L{addCallbacks}.
+ """
+ return self.addCallbacks(callback, callback,
+ callbackArgs=args, errbackArgs=args,
+ callbackKeywords=kw, errbackKeywords=kw)
+
+
+ def chainDeferred(self, d):
+ """
+ Chain another L{Deferred} to this L{Deferred}.
+
+ This method adds callbacks to this L{Deferred} to call C{d}'s callback
+ or errback, as appropriate. It is merely a shorthand way of performing
+ the following::
+
+ self.addCallbacks(d.callback, d.errback)
+
+ When you chain a deferred d2 to another deferred d1 with
+ d1.chainDeferred(d2), you are making d2 participate in the callback
+ chain of d1. Thus any event that fires d1 will also fire d2.
+ However, the converse is B{not} true; if d2 is fired d1 will not be
+ affected.
+
+ Note that unlike the case where chaining is caused by a L{Deferred}
+ being returned from a callback, it is possible to cause the call
+ stack size limit to be exceeded by chaining many L{Deferred}s
+ together with C{chainDeferred}.
+
+ @return: C{self}.
+ @rtype: a L{Deferred}
+ """
+ d._chainedTo = self
+ return self.addCallbacks(d.callback, d.errback)
+
+
+ def callback(self, result):
+ """
+ Run all success callbacks that have been added to this L{Deferred}.
+
+ Each callback will have its result passed as the first argument to
+ the next; this way, the callbacks act as a 'processing chain'. If
+ the success-callback returns a L{Failure} or raises an L{Exception},
+ processing will continue on the *error* callback chain. If a
+ callback (or errback) returns another L{Deferred}, this L{Deferred}
+ will be chained to it (and further callbacks will not run until that
+ L{Deferred} has a result).
+ """
+ assert not isinstance(result, Deferred)
+ self._startRunCallbacks(result)
+
+
+ def errback(self, fail=None):
+ """
+ Run all error callbacks that have been added to this L{Deferred}.
+
+ Each callback will have its result passed as the first
+ argument to the next; this way, the callbacks act as a
+ 'processing chain'. Also, if the error-callback returns a non-Failure
+ or doesn't raise an L{Exception}, processing will continue on the
+ *success*-callback chain.
+
+ If the argument that's passed to me is not a L{failure.Failure} instance,
+ it will be embedded in one. If no argument is passed, a
+ L{failure.Failure} instance will be created based on the current
+ traceback stack.
+
+ Passing a string as `fail' is deprecated, and will be punished with
+ a warning message.
+
+ @raise NoCurrentExceptionError: If C{fail} is C{None} but there is
+ no current exception state.
+ """
+ if fail is None:
+ fail = failure.Failure(captureVars=self.debug)
+ elif not isinstance(fail, failure.Failure):
+ fail = failure.Failure(fail)
+
+ self._startRunCallbacks(fail)
+
+
+ def pause(self):
+ """
+ Stop processing on a L{Deferred} until L{unpause}() is called.
+ """
+ self.paused = self.paused + 1
+
+
+ def unpause(self):
+ """
+ Process all callbacks made since L{pause}() was called.
+ """
+ self.paused = self.paused - 1
+ if self.paused:
+ return
+ if self.called:
+ self._runCallbacks()
+
+
+ def cancel(self):
+ """
+ Cancel this L{Deferred}.
+
+ If the L{Deferred} has not yet had its C{errback} or C{callback} method
+ invoked, call the canceller function provided to the constructor. If
+ that function does not invoke C{callback} or C{errback}, or if no
+ canceller function was provided, errback with L{CancelledError}.
+
+ If this L{Deferred} is waiting on another L{Deferred}, forward the
+ cancellation to the other L{Deferred}.
+ """
+ if not self.called:
+ canceller = self._canceller
+ if canceller:
+ canceller(self)
+ else:
+ # Arrange to eat the callback that will eventually be fired
+ # since there was no real canceller.
+ self._suppressAlreadyCalled = True
+ if not self.called:
+ # There was no canceller, or the canceller didn't call
+ # callback or errback.
+ self.errback(failure.Failure(CancelledError()))
+ elif isinstance(self.result, Deferred):
+ # Waiting for another deferred -- cancel it instead.
+ self.result.cancel()
+
+
+ def _startRunCallbacks(self, result):
+ if self.called:
+ if self._suppressAlreadyCalled:
+ self._suppressAlreadyCalled = False
+ return
+ if self.debug:
+ if self._debugInfo is None:
+ self._debugInfo = DebugInfo()
+ extra = "\n" + self._debugInfo._getDebugTracebacks()
+ raise AlreadyCalledError(extra)
+ raise AlreadyCalledError
+ if self.debug:
+ if self._debugInfo is None:
+ self._debugInfo = DebugInfo()
+ self._debugInfo.invoker = traceback.format_stack()[:-2]
+ self.called = True
+ self.result = result
+ self._runCallbacks()
+
+
+ def _continuation(self):
+ """
+ Build a tuple of callback and errback with L{_continue} to be used by
+ L{_addContinue} and L{_removeContinue} on another Deferred.
+ """
+ return ((_CONTINUE, (self,), None),
+ (_CONTINUE, (self,), None))
+
+
+ def _runCallbacks(self):
+ """
+ Run the chain of callbacks once a result is available.
+
+ This consists of a simple loop over all of the callbacks, calling each
+ with the current result and making the current result equal to the
+ return value (or raised exception) of that call.
+
+ If C{self._runningCallbacks} is true, this loop won't run at all, since
+ it is already running above us on the call stack. If C{self.paused} is
+ true, the loop also won't run, because that's what it means to be
+ paused.
+
+ The loop will terminate before processing all of the callbacks if a
+ C{Deferred} without a result is encountered.
+
+ If a C{Deferred} I{with} a result is encountered, that result is taken
+ and the loop proceeds.
+
+ @note: The implementation is complicated slightly by the fact that
+ chaining (associating two Deferreds with each other such that one
+ will wait for the result of the other, as happens when a Deferred is
+ returned from a callback on another Deferred) is supported
+ iteratively rather than recursively, to avoid running out of stack
+ frames when processing long chains.
+ """
+ if self._runningCallbacks:
+ # Don't recursively run callbacks
+ return
+
+ # Keep track of all the Deferreds encountered while propagating results
+ # up a chain. The way a Deferred gets onto this stack is by having
+ # added its _continuation() to the callbacks list of a second Deferred
+ # and then that second Deferred being fired. ie, if ever had _chainedTo
+ # set to something other than None, you might end up on this stack.
+ chain = [self]
+
+ while chain:
+ current = chain[-1]
+
+ if current.paused:
+ # This Deferred isn't going to produce a result at all. All the
+ # Deferreds up the chain waiting on it will just have to...
+ # wait.
+ return
+
+ finished = True
+ current._chainedTo = None
+ while current.callbacks:
+ item = current.callbacks.pop(0)
+ callback, args, kw = item[
+ isinstance(current.result, failure.Failure)]
+ args = args or ()
+ kw = kw or {}
+
+ # Avoid recursion if we can.
+ if callback is _CONTINUE:
+ # Give the waiting Deferred our current result and then
+ # forget about that result ourselves.
+ chainee = args[0]
+ chainee.result = current.result
+ current.result = None
+ # Making sure to update _debugInfo
+ if current._debugInfo is not None:
+ current._debugInfo.failResult = None
+ chainee.paused -= 1
+ chain.append(chainee)
+ # Delay cleaning this Deferred and popping it from the chain
+ # until after we've dealt with chainee.
+ finished = False
+ break
+
+ try:
+ current._runningCallbacks = True
+ try:
+ current.result = callback(current.result, *args, **kw)
+ finally:
+ current._runningCallbacks = False
+ except:
+ # Including full frame information in the Failure is quite
+ # expensive, so we avoid it unless self.debug is set.
+ current.result = failure.Failure(captureVars=self.debug)
+ else:
+ if isinstance(current.result, Deferred):
+ # The result is another Deferred. If it has a result,
+ # we can take it and keep going.
+ resultResult = getattr(current.result, 'result', _NO_RESULT)
+ if resultResult is _NO_RESULT or isinstance(resultResult, Deferred) or current.result.paused:
+ # Nope, it didn't. Pause and chain.
+ current.pause()
+ current._chainedTo = current.result
+ # Note: current.result has no result, so it's not
+ # running its callbacks right now. Therefore we can
+ # append to the callbacks list directly instead of
+ # using addCallbacks.
+ current.result.callbacks.append(current._continuation())
+ break
+ else:
+ # Yep, it did. Steal it.
+ current.result.result = None
+ # Make sure _debugInfo's failure state is updated.
+ if current.result._debugInfo is not None:
+ current.result._debugInfo.failResult = None
+ current.result = resultResult
+
+ if finished:
+ # As much of the callback chain - perhaps all of it - as can be
+ # processed right now has been. The current Deferred is waiting on
+ # another Deferred or for more callbacks. Before finishing with it,
+ # make sure its _debugInfo is in the proper state.
+ if isinstance(current.result, failure.Failure):
+ # Stash the Failure in the _debugInfo for unhandled error
+ # reporting.
+ current.result.cleanFailure()
+ if current._debugInfo is None:
+ current._debugInfo = DebugInfo()
+ current._debugInfo.failResult = current.result
+ else:
+ # Clear out any Failure in the _debugInfo, since the result
+ # is no longer a Failure.
+ if current._debugInfo is not None:
+ current._debugInfo.failResult = None
+
+ # This Deferred is done, pop it from the chain and move back up
+ # to the Deferred which supplied us with our result.
+ chain.pop()
+
+
+ def __str__(self):
+ """
+ Return a string representation of this C{Deferred}.
+ """
+ cname = self.__class__.__name__
+ result = getattr(self, 'result', _NO_RESULT)
+ myID = hex(unsignedID(self))
+ if self._chainedTo is not None:
+ result = ' waiting on Deferred at %s' % (hex(unsignedID(self._chainedTo)),)
+ elif result is _NO_RESULT:
+ result = ''
+ else:
+ result = ' current result: %r' % (result,)
+ return "<%s at %s%s>" % (cname, myID, result)
+ __repr__ = __str__
+
+
+
+class DebugInfo:
+ """
+ Deferred debug helper.
+ """
+
+ failResult = None
+
+ def _getDebugTracebacks(self):
+ info = ''
+ if hasattr(self, "creator"):
+ info += " C: Deferred was created:\n C:"
+ info += "".join(self.creator).rstrip().replace("\n","\n C:")
+ info += "\n"
+ if hasattr(self, "invoker"):
+ info += " I: First Invoker was:\n I:"
+ info += "".join(self.invoker).rstrip().replace("\n","\n I:")
+ info += "\n"
+ return info
+
+
+ def __del__(self):
+ """
+ Print tracebacks and die.
+
+ If the *last* (and I do mean *last*) callback leaves me in an error
+ state, print a traceback (if said errback is a L{Failure}).
+ """
+ if self.failResult is not None:
+ log.msg("Unhandled error in Deferred:", isError=True)
+ debugInfo = self._getDebugTracebacks()
+ if debugInfo != '':
+ log.msg("(debug: " + debugInfo + ")", isError=True)
+ log.err(self.failResult)
+
+
+
+class FirstError(Exception):
+ """
+ First error to occur in a L{DeferredList} if C{fireOnOneErrback} is set.
+
+ @ivar subFailure: The L{Failure} that occurred.
+ @type subFailure: L{Failure}
+
+ @ivar index: The index of the L{Deferred} in the L{DeferredList} where
+ it happened.
+ @type index: C{int}
+ """
+ def __init__(self, failure, index):
+ Exception.__init__(self, failure, index)
+ self.subFailure = failure
+ self.index = index
+
+
+ def __repr__(self):
+ """
+ The I{repr} of L{FirstError} instances includes the repr of the
+ wrapped failure's exception and the index of the L{FirstError}.
+ """
+ return 'FirstError[#%d, %r]' % (self.index, self.subFailure.value)
+
+
+ def __str__(self):
+ """
+ The I{str} of L{FirstError} instances includes the I{str} of the
+ entire wrapped failure (including its traceback and exception) and
+ the index of the L{FirstError}.
+ """
+ return 'FirstError[#%d, %s]' % (self.index, self.subFailure)
+
+
+ def __cmp__(self, other):
+ """
+ Comparison between L{FirstError} and other L{FirstError} instances
+ is defined as the comparison of the index and sub-failure of each
+ instance. L{FirstError} instances don't compare equal to anything
+ that isn't a L{FirstError} instance.
+
+ @since: 8.2
+ """
+ if isinstance(other, FirstError):
+ return cmp(
+ (self.index, self.subFailure),
+ (other.index, other.subFailure))
+ return -1
+
+
+
+class DeferredList(Deferred):
+ """
+ L{DeferredList} is a tool for collecting the results of several Deferreds.
+
+ This tracks a list of L{Deferred}s for their results, and makes a single
+ callback when they have all completed. By default, the ultimate result is a
+ list of (success, result) tuples, 'success' being a boolean.
+ L{DeferredList} exposes the same API that L{Deferred} does, so callbacks and
+ errbacks can be added to it in the same way.
+
+ L{DeferredList} is implemented by adding callbacks and errbacks to each
+ L{Deferred} in the list passed to it. This means callbacks and errbacks
+ added to the Deferreds before they are passed to L{DeferredList} will change
+ the result that L{DeferredList} sees (i.e., L{DeferredList} is not special).
+ Callbacks and errbacks can also be added to the Deferreds after they are
+ passed to L{DeferredList} and L{DeferredList} may change the result that
+ they see.
+
+ See the documentation for the C{__init__} arguments for more information.
+ """
+
+ fireOnOneCallback = False
+ fireOnOneErrback = False
+
+ def __init__(self, deferredList, fireOnOneCallback=False,
+ fireOnOneErrback=False, consumeErrors=False):
+ """
+ Initialize a DeferredList.
+
+ @param deferredList: The list of deferreds to track.
+ @type deferredList: C{list} of L{Deferred}s
+
+ @param fireOnOneCallback: (keyword param) a flag indicating that this
+ L{DeferredList} will fire when the first L{Deferred} in
+ C{deferredList} fires with a non-failure result without waiting for
+ any of the other Deferreds. When this flag is set, the DeferredList
+ will fire with a two-tuple: the first element is the result of the
+ Deferred which fired; the second element is the index in
+ C{deferredList} of that Deferred.
+ @type fireOnOneCallback: C{bool}
+
+ @param fireOnOneErrback: (keyword param) a flag indicating that this
+ L{DeferredList} will fire when the first L{Deferred} in
+ C{deferredList} fires with a failure result without waiting for any
+ of the other Deferreds. When this flag is set, if a Deferred in the
+ list errbacks, the DeferredList will errback with a L{FirstError}
+ failure wrapping the failure of that Deferred.
+ @type fireOnOneErrback: C{bool}
+
+ @param consumeErrors: (keyword param) a flag indicating that failures in
+ any of the included L{Deferreds} should not be propagated to
+ errbacks added to the individual L{Deferreds} after this
+ L{DeferredList} is constructed. After constructing the
+ L{DeferredList}, any errors in the individual L{Deferred}s will be
+ converted to a callback result of C{None}. This is useful to
+ prevent spurious 'Unhandled error in Deferred' messages from being
+ logged. This does not prevent C{fireOnOneErrback} from working.
+ @type consumeErrors: C{bool}
+ """
+ self.resultList = [None] * len(deferredList)
+ Deferred.__init__(self)
+ if len(deferredList) == 0 and not fireOnOneCallback:
+ self.callback(self.resultList)
+
+ # These flags need to be set *before* attaching callbacks to the
+ # deferreds, because the callbacks use these flags, and will run
+ # synchronously if any of the deferreds are already fired.
+ self.fireOnOneCallback = fireOnOneCallback
+ self.fireOnOneErrback = fireOnOneErrback
+ self.consumeErrors = consumeErrors
+ self.finishedCount = 0
+
+ index = 0
+ for deferred in deferredList:
+ deferred.addCallbacks(self._cbDeferred, self._cbDeferred,
+ callbackArgs=(index,SUCCESS),
+ errbackArgs=(index,FAILURE))
+ index = index + 1
+
+
+ def _cbDeferred(self, result, index, succeeded):
+ """
+ (internal) Callback for when one of my deferreds fires.
+ """
+ self.resultList[index] = (succeeded, result)
+
+ self.finishedCount += 1
+ if not self.called:
+ if succeeded == SUCCESS and self.fireOnOneCallback:
+ self.callback((result, index))
+ elif succeeded == FAILURE and self.fireOnOneErrback:
+ self.errback(failure.Failure(FirstError(result, index)))
+ elif self.finishedCount == len(self.resultList):
+ self.callback(self.resultList)
+
+ if succeeded == FAILURE and self.consumeErrors:
+ result = None
+
+ return result
+
+
+
+def _parseDListResult(l, fireOnOneErrback=False):
+ if __debug__:
+ for success, value in l:
+ assert success
+ return [x[1] for x in l]
+
+
+
+def gatherResults(deferredList, consumeErrors=False):
+ """
+ Returns, via a L{Deferred}, a list with the results of the given
+ L{Deferred}s - in effect, a "join" of multiple deferred operations.
+
+ The returned L{Deferred} will fire when I{all} of the provided L{Deferred}s
+ have fired, or when any one of them has failed.
+
+ This differs from L{DeferredList} in that you don't need to parse
+ the result for success/failure.
+
+ @type deferredList: C{list} of L{Deferred}s
+
+ @param consumeErrors: (keyword param) a flag, defaulting to False,
+ indicating that failures in any of the given L{Deferreds} should not be
+ propagated to errbacks added to the individual L{Deferreds} after this
+ L{gatherResults} invocation. Any such errors in the individual
+ L{Deferred}s will be converted to a callback result of C{None}. This
+ is useful to prevent spurious 'Unhandled error in Deferred' messages
+ from being logged. This parameter is available since 11.1.0.
+ @type consumeErrors: C{bool}
+ """
+ d = DeferredList(deferredList, fireOnOneErrback=True,
+ consumeErrors=consumeErrors)
+ d.addCallback(_parseDListResult)
+ return d
+
+
+
+# Constants for use with DeferredList
+
+SUCCESS = True
+FAILURE = False
+
+
+
+## deferredGenerator
+
+class waitForDeferred:
+ """
+ See L{deferredGenerator}.
+ """
+
+ def __init__(self, d):
+ if not isinstance(d, Deferred):
+ raise TypeError("You must give waitForDeferred a Deferred. You gave it %r." % (d,))
+ self.d = d
+
+
+ def getResult(self):
+ if isinstance(self.result, failure.Failure):
+ self.result.raiseException()
+ return self.result
+
+
+
+def _deferGenerator(g, deferred):
+ """
+ See L{deferredGenerator}.
+ """
+ result = None
+
+ # This function is complicated by the need to prevent unbounded recursion
+ # arising from repeatedly yielding immediately ready deferreds. This while
+ # loop and the waiting variable solve that by manually unfolding the
+ # recursion.
+
+ waiting = [True, # defgen is waiting for result?
+ None] # result
+
+ while 1:
+ try:
+ result = g.next()
+ except StopIteration:
+ deferred.callback(result)
+ return deferred
+ except:
+ deferred.errback()
+ return deferred
+
+ # Deferred.callback(Deferred) raises an error; we catch this case
+ # early here and give a nicer error message to the user in case
+ # they yield a Deferred.
+ if isinstance(result, Deferred):
+ return fail(TypeError("Yield waitForDeferred(d), not d!"))
+
+ if isinstance(result, waitForDeferred):
+ # a waitForDeferred was yielded, get the result.
+ # Pass result in so it don't get changed going around the loop
+ # This isn't a problem for waiting, as it's only reused if
+ # gotResult has already been executed.
+ def gotResult(r, result=result):
+ result.result = r
+ if waiting[0]:
+ waiting[0] = False
+ waiting[1] = r
+ else:
+ _deferGenerator(g, deferred)
+ result.d.addBoth(gotResult)
+ if waiting[0]:
+ # Haven't called back yet, set flag so that we get reinvoked
+ # and return from the loop
+ waiting[0] = False
+ return deferred
+ # Reset waiting to initial values for next loop
+ waiting[0] = True
+ waiting[1] = None
+
+ result = None
+
+
+
+def deferredGenerator(f):
+ """
+ deferredGenerator and waitForDeferred help you write L{Deferred}-using code
+ that looks like a regular sequential function. If your code has a minimum
+ requirement of Python 2.5, consider the use of L{inlineCallbacks} instead,
+ which can accomplish the same thing in a more concise manner.
+
+ There are two important functions involved: L{waitForDeferred}, and
+ L{deferredGenerator}. They are used together, like this::
+
+ @deferredGenerator
+ def thingummy():
+ thing = waitForDeferred(makeSomeRequestResultingInDeferred())
+ yield thing
+ thing = thing.getResult()
+ print thing #the result! hoorj!
+
+ L{waitForDeferred} returns something that you should immediately yield; when
+ your generator is resumed, calling C{thing.getResult()} will either give you
+ the result of the L{Deferred} if it was a success, or raise an exception if it
+ was a failure. Calling C{getResult} is B{absolutely mandatory}. If you do
+ not call it, I{your program will not work}.
+
+ L{deferredGenerator} takes one of these waitForDeferred-using generator
+ functions and converts it into a function that returns a L{Deferred}. The
+ result of the L{Deferred} will be the last value that your generator yielded
+ unless the last value is a L{waitForDeferred} instance, in which case the
+ result will be C{None}. If the function raises an unhandled exception, the
+ L{Deferred} will errback instead. Remember that C{return result} won't work;
+ use C{yield result; return} in place of that.
+
+ Note that not yielding anything from your generator will make the L{Deferred}
+ result in C{None}. Yielding a L{Deferred} from your generator is also an error
+ condition; always yield C{waitForDeferred(d)} instead.
+
+ The L{Deferred} returned from your deferred generator may also errback if your
+ generator raised an exception. For example::
+
+ @deferredGenerator
+ def thingummy():
+ thing = waitForDeferred(makeSomeRequestResultingInDeferred())
+ yield thing
+ thing = thing.getResult()
+ if thing == 'I love Twisted':
+ # will become the result of the Deferred
+ yield 'TWISTED IS GREAT!'
+ return
+ else:
+ # will trigger an errback
+ raise Exception('DESTROY ALL LIFE')
+
+ Put succinctly, these functions connect deferred-using code with this 'fake
+ blocking' style in both directions: L{waitForDeferred} converts from a
+ L{Deferred} to the 'blocking' style, and L{deferredGenerator} converts from the
+ 'blocking' style to a L{Deferred}.
+ """
+
+ def unwindGenerator(*args, **kwargs):
+ return _deferGenerator(f(*args, **kwargs), Deferred())
+ return mergeFunctionMetadata(f, unwindGenerator)
+
+
+## inlineCallbacks
+
+# BaseException is only in Py 2.5.
+try:
+ BaseException
+except NameError:
+ BaseException=Exception
+
+
+
+class _DefGen_Return(BaseException):
+ def __init__(self, value):
+ self.value = value
+
+
+
+def returnValue(val):
+ """
+ Return val from a L{inlineCallbacks} generator.
+
+ Note: this is currently implemented by raising an exception
+ derived from L{BaseException}. You might want to change any
+ 'except:' clauses to an 'except Exception:' clause so as not to
+ catch this exception.
+
+ Also: while this function currently will work when called from
+ within arbitrary functions called from within the generator, do
+ not rely upon this behavior.
+ """
+ raise _DefGen_Return(val)
+
+
+
+def _inlineCallbacks(result, g, deferred):
+ """
+ See L{inlineCallbacks}.
+ """
+ # This function is complicated by the need to prevent unbounded recursion
+ # arising from repeatedly yielding immediately ready deferreds. This while
+ # loop and the waiting variable solve that by manually unfolding the
+ # recursion.
+
+ waiting = [True, # waiting for result?
+ None] # result
+
+ while 1:
+ try:
+ # Send the last result back as the result of the yield expression.
+ isFailure = isinstance(result, failure.Failure)
+ if isFailure:
+ result = result.throwExceptionIntoGenerator(g)
+ else:
+ result = g.send(result)
+ except StopIteration:
+ # fell off the end, or "return" statement
+ deferred.callback(None)
+ return deferred
+ except _DefGen_Return, e:
+ # returnValue() was called; time to give a result to the original
+ # Deferred. First though, let's try to identify the potentially
+ # confusing situation which results when returnValue() is
+ # accidentally invoked from a different function, one that wasn't
+ # decorated with @inlineCallbacks.
+
+ # The traceback starts in this frame (the one for
+ # _inlineCallbacks); the next one down should be the application
+ # code.
+ appCodeTrace = exc_info()[2].tb_next
+ if isFailure:
+ # If we invoked this generator frame by throwing an exception
+ # into it, then throwExceptionIntoGenerator will consume an
+ # additional stack frame itself, so we need to skip that too.
+ appCodeTrace = appCodeTrace.tb_next
+ # Now that we've identified the frame being exited by the
+ # exception, let's figure out if returnValue was called from it
+ # directly. returnValue itself consumes a stack frame, so the
+ # application code will have a tb_next, but it will *not* have a
+ # second tb_next.
+ if appCodeTrace.tb_next.tb_next:
+ # If returnValue was invoked non-local to the frame which it is
+ # exiting, identify the frame that ultimately invoked
+ # returnValue so that we can warn the user, as this behavior is
+ # confusing.
+ ultimateTrace = appCodeTrace
+ while ultimateTrace.tb_next.tb_next:
+ ultimateTrace = ultimateTrace.tb_next
+ filename = ultimateTrace.tb_frame.f_code.co_filename
+ lineno = ultimateTrace.tb_lineno
+ warnings.warn_explicit(
+ "returnValue() in %r causing %r to exit: "
+ "returnValue should only be invoked by functions decorated "
+ "with inlineCallbacks" % (
+ ultimateTrace.tb_frame.f_code.co_name,
+ appCodeTrace.tb_frame.f_code.co_name),
+ DeprecationWarning, filename, lineno)
+ deferred.callback(e.value)
+ return deferred
+ except:
+ deferred.errback()
+ return deferred
+
+ if isinstance(result, Deferred):
+ # a deferred was yielded, get the result.
+ def gotResult(r):
+ if waiting[0]:
+ waiting[0] = False
+ waiting[1] = r
+ else:
+ _inlineCallbacks(r, g, deferred)
+
+ result.addBoth(gotResult)
+ if waiting[0]:
+ # Haven't called back yet, set flag so that we get reinvoked
+ # and return from the loop
+ waiting[0] = False
+ return deferred
+
+ result = waiting[1]
+ # Reset waiting to initial values for next loop. gotResult uses
+ # waiting, but this isn't a problem because gotResult is only
+ # executed once, and if it hasn't been executed yet, the return
+ # branch above would have been taken.
+
+
+ waiting[0] = True
+ waiting[1] = None
+
+
+ return deferred
+
+
+
+def inlineCallbacks(f):
+ """
+ WARNING: this function will not work in Python 2.4 and earlier!
+
+ inlineCallbacks helps you write Deferred-using code that looks like a
+ regular sequential function. This function uses features of Python 2.5
+ generators. If you need to be compatible with Python 2.4 or before, use
+ the L{deferredGenerator} function instead, which accomplishes the same
+ thing, but with somewhat more boilerplate. For example::
+
+ @inlineCallBacks
+ def thingummy():
+ thing = yield makeSomeRequestResultingInDeferred()
+ print thing #the result! hoorj!
+
+ When you call anything that results in a L{Deferred}, you can simply yield it;
+ your generator will automatically be resumed when the Deferred's result is
+ available. The generator will be sent the result of the L{Deferred} with the
+ 'send' method on generators, or if the result was a failure, 'throw'.
+
+ Things that are not L{Deferred}s may also be yielded, and your generator
+ will be resumed with the same object sent back. This means C{yield}
+ performs an operation roughly equivalent to L{maybeDeferred}.
+
+ Your inlineCallbacks-enabled generator will return a L{Deferred} object, which
+ will result in the return value of the generator (or will fail with a
+ failure object if your generator raises an unhandled exception). Note that
+ you can't use C{return result} to return a value; use C{returnValue(result)}
+ instead. Falling off the end of the generator, or simply using C{return}
+ will cause the L{Deferred} to have a result of C{None}.
+
+ Be aware that L{returnValue} will not accept a L{Deferred} as a parameter.
+ If you believe the thing you'd like to return could be a L{Deferred}, do
+ this::
+
+ result = yield result
+ returnValue(result)
+
+ The L{Deferred} returned from your deferred generator may errback if your
+ generator raised an exception::
+
+ @inlineCallbacks
+ def thingummy():
+ thing = yield makeSomeRequestResultingInDeferred()
+ if thing == 'I love Twisted':
+ # will become the result of the Deferred
+ returnValue('TWISTED IS GREAT!')
+ else:
+ # will trigger an errback
+ raise Exception('DESTROY ALL LIFE')
+ """
+ def unwindGenerator(*args, **kwargs):
+ try:
+ gen = f(*args, **kwargs)
+ except _DefGen_Return:
+ raise TypeError(
+ "inlineCallbacks requires %r to produce a generator; instead"
+ "caught returnValue being used in a non-generator" % (f,))
+ if not isinstance(gen, types.GeneratorType):
+ raise TypeError(
+ "inlineCallbacks requires %r to produce a generator; "
+ "instead got %r" % (f, gen))
+ return _inlineCallbacks(None, gen, Deferred())
+ return mergeFunctionMetadata(f, unwindGenerator)
+
+
+## DeferredLock/DeferredQueue
+
+class _ConcurrencyPrimitive(object):
+ def __init__(self):
+ self.waiting = []
+
+
+ def _releaseAndReturn(self, r):
+ self.release()
+ return r
+
+
+ def run(*args, **kwargs):
+ """
+ Acquire, run, release.
+
+ This function takes a callable as its first argument and any
+ number of other positional and keyword arguments. When the
+ lock or semaphore is acquired, the callable will be invoked
+ with those arguments.
+
+ The callable may return a L{Deferred}; if it does, the lock or
+ semaphore won't be released until that L{Deferred} fires.
+
+ @return: L{Deferred} of function result.
+ """
+ if len(args) < 2:
+ if not args:
+ raise TypeError("run() takes at least 2 arguments, none given.")
+ raise TypeError("%s.run() takes at least 2 arguments, 1 given" % (
+ args[0].__class__.__name__,))
+ self, f = args[:2]
+ args = args[2:]
+
+ def execute(ignoredResult):
+ d = maybeDeferred(f, *args, **kwargs)
+ d.addBoth(self._releaseAndReturn)
+ return d
+
+ d = self.acquire()
+ d.addCallback(execute)
+ return d
+
+
+
+class DeferredLock(_ConcurrencyPrimitive):
+ """
+ A lock for event driven systems.
+
+ @ivar locked: C{True} when this Lock has been acquired, false at all other
+ times. Do not change this value, but it is useful to examine for the
+ equivalent of a "non-blocking" acquisition.
+ """
+
+ locked = False
+
+
+ def _cancelAcquire(self, d):
+ """
+ Remove a deferred d from our waiting list, as the deferred has been
+ canceled.
+
+ Note: We do not need to wrap this in a try/except to catch d not
+ being in self.waiting because this canceller will not be called if
+ d has fired. release() pops a deferred out of self.waiting and
+ calls it, so the canceller will no longer be called.
+
+ @param d: The deferred that has been canceled.
+ """
+ self.waiting.remove(d)
+
+
+ def acquire(self):
+ """
+ Attempt to acquire the lock. Returns a L{Deferred} that fires on
+ lock acquisition with the L{DeferredLock} as the value. If the lock
+ is locked, then the Deferred is placed at the end of a waiting list.
+
+ @return: a L{Deferred} which fires on lock acquisition.
+ @rtype: a L{Deferred}
+ """
+ d = Deferred(canceller=self._cancelAcquire)
+ if self.locked:
+ self.waiting.append(d)
+ else:
+ self.locked = True
+ d.callback(self)
+ return d
+
+
+ def release(self):
+ """
+ Release the lock. If there is a waiting list, then the first
+ L{Deferred} in that waiting list will be called back.
+
+ Should be called by whomever did the L{acquire}() when the shared
+ resource is free.
+ """
+ assert self.locked, "Tried to release an unlocked lock"
+ self.locked = False
+ if self.waiting:
+ # someone is waiting to acquire lock
+ self.locked = True
+ d = self.waiting.pop(0)
+ d.callback(self)
+
+
+
+class DeferredSemaphore(_ConcurrencyPrimitive):
+ """
+ A semaphore for event driven systems.
+
+ @ivar tokens: At most this many users may acquire this semaphore at
+ once.
+ @type tokens: C{int}
+
+ @ivar limit: The difference between C{tokens} and the number of users
+ which have currently acquired this semaphore.
+ @type limit: C{int}
+ """
+
+ def __init__(self, tokens):
+ _ConcurrencyPrimitive.__init__(self)
+ if tokens < 1:
+ raise ValueError("DeferredSemaphore requires tokens >= 1")
+ self.tokens = tokens
+ self.limit = tokens
+
+
+ def _cancelAcquire(self, d):
+ """
+ Remove a deferred d from our waiting list, as the deferred has been
+ canceled.
+
+ Note: We do not need to wrap this in a try/except to catch d not
+ being in self.waiting because this canceller will not be called if
+ d has fired. release() pops a deferred out of self.waiting and
+ calls it, so the canceller will no longer be called.
+
+ @param d: The deferred that has been canceled.
+ """
+ self.waiting.remove(d)
+
+
+ def acquire(self):
+ """
+ Attempt to acquire the token.
+
+ @return: a L{Deferred} which fires on token acquisition.
+ """
+ assert self.tokens >= 0, "Internal inconsistency?? tokens should never be negative"
+ d = Deferred(canceller=self._cancelAcquire)
+ if not self.tokens:
+ self.waiting.append(d)
+ else:
+ self.tokens = self.tokens - 1
+ d.callback(self)
+ return d
+
+
+ def release(self):
+ """
+ Release the token.
+
+ Should be called by whoever did the L{acquire}() when the shared
+ resource is free.
+ """
+ assert self.tokens < self.limit, "Someone released me too many times: too many tokens!"
+ self.tokens = self.tokens + 1
+ if self.waiting:
+ # someone is waiting to acquire token
+ self.tokens = self.tokens - 1
+ d = self.waiting.pop(0)
+ d.callback(self)
+
+
+
+class QueueOverflow(Exception):
+ pass
+
+
+
+class QueueUnderflow(Exception):
+ pass
+
+
+
+class DeferredQueue(object):
+ """
+ An event driven queue.
+
+ Objects may be added as usual to this queue. When an attempt is
+ made to retrieve an object when the queue is empty, a L{Deferred} is
+ returned which will fire when an object becomes available.
+
+ @ivar size: The maximum number of objects to allow into the queue
+ at a time. When an attempt to add a new object would exceed this
+ limit, L{QueueOverflow} is raised synchronously. C{None} for no limit.
+
+ @ivar backlog: The maximum number of L{Deferred} gets to allow at
+ one time. When an attempt is made to get an object which would
+ exceed this limit, L{QueueUnderflow} is raised synchronously. C{None}
+ for no limit.
+ """
+
+ def __init__(self, size=None, backlog=None):
+ self.waiting = []
+ self.pending = []
+ self.size = size
+ self.backlog = backlog
+
+
+ def _cancelGet(self, d):
+ """
+ Remove a deferred d from our waiting list, as the deferred has been
+ canceled.
+
+ Note: We do not need to wrap this in a try/except to catch d not
+ being in self.waiting because this canceller will not be called if
+ d has fired. put() pops a deferred out of self.waiting and calls
+ it, so the canceller will no longer be called.
+
+ @param d: The deferred that has been canceled.
+ """
+ self.waiting.remove(d)
+
+
+ def put(self, obj):
+ """
+ Add an object to this queue.
+
+ @raise QueueOverflow: Too many objects are in this queue.
+ """
+ if self.waiting:
+ self.waiting.pop(0).callback(obj)
+ elif self.size is None or len(self.pending) < self.size:
+ self.pending.append(obj)
+ else:
+ raise QueueOverflow()
+
+
+ def get(self):
+ """
+ Attempt to retrieve and remove an object from the queue.
+
+ @return: a L{Deferred} which fires with the next object available in
+ the queue.
+
+ @raise QueueUnderflow: Too many (more than C{backlog})
+ L{Deferred}s are already waiting for an object from this queue.
+ """
+ if self.pending:
+ return succeed(self.pending.pop(0))
+ elif self.backlog is None or len(self.waiting) < self.backlog:
+ d = Deferred(canceller=self._cancelGet)
+ self.waiting.append(d)
+ return d
+ else:
+ raise QueueUnderflow()
+
+
+
+class AlreadyTryingToLockError(Exception):
+ """
+ Raised when L{DeferredFilesystemLock.deferUntilLocked} is called twice on a
+ single L{DeferredFilesystemLock}.
+ """
+
+
+
+class DeferredFilesystemLock(lockfile.FilesystemLock):
+ """
+ A L{FilesystemLock} that allows for a L{Deferred} to be fired when the lock is
+ acquired.
+
+ @ivar _scheduler: The object in charge of scheduling retries. In this
+ implementation this is parameterized for testing.
+
+ @ivar _interval: The retry interval for an L{IReactorTime} based scheduler.
+
+ @ivar _tryLockCall: A L{DelayedCall} based on C{_interval} that will manage
+ the next retry for aquiring the lock.
+
+ @ivar _timeoutCall: A L{DelayedCall} based on C{deferUntilLocked}'s timeout
+ argument. This is in charge of timing out our attempt to acquire the
+ lock.
+ """
+ _interval = 1
+ _tryLockCall = None
+ _timeoutCall = None
+
+
+ def __init__(self, name, scheduler=None):
+ """
+ @param name: The name of the lock to acquire
+ @param scheduler: An object which provides L{IReactorTime}
+ """
+ lockfile.FilesystemLock.__init__(self, name)
+
+ if scheduler is None:
+ from twisted.internet import reactor
+ scheduler = reactor
+
+ self._scheduler = scheduler
+
+
+ def deferUntilLocked(self, timeout=None):
+ """
+ Wait until we acquire this lock. This method is not safe for
+ concurrent use.
+
+ @type timeout: C{float} or C{int}
+ @param timeout: the number of seconds after which to time out if the
+ lock has not been acquired.
+
+ @return: a L{Deferred} which will callback when the lock is acquired, or
+ errback with a L{TimeoutError} after timing out or an
+ L{AlreadyTryingToLockError} if the L{deferUntilLocked} has already
+ been called and not successfully locked the file.
+ """
+ if self._tryLockCall is not None:
+ return fail(
+ AlreadyTryingToLockError(
+ "deferUntilLocked isn't safe for concurrent use."))
+
+ d = Deferred()
+
+ def _cancelLock():
+ self._tryLockCall.cancel()
+ self._tryLockCall = None
+ self._timeoutCall = None
+
+ if self.lock():
+ d.callback(None)
+ else:
+ d.errback(failure.Failure(
+ TimeoutError("Timed out aquiring lock: %s after %fs" % (
+ self.name,
+ timeout))))
+
+ def _tryLock():
+ if self.lock():
+ if self._timeoutCall is not None:
+ self._timeoutCall.cancel()
+ self._timeoutCall = None
+
+ self._tryLockCall = None
+
+ d.callback(None)
+ else:
+ if timeout is not None and self._timeoutCall is None:
+ self._timeoutCall = self._scheduler.callLater(
+ timeout, _cancelLock)
+
+ self._tryLockCall = self._scheduler.callLater(
+ self._interval, _tryLock)
+
+ _tryLock()
+
+ return d
+
+
+
+__all__ = ["Deferred", "DeferredList", "succeed", "fail", "FAILURE", "SUCCESS",
+ "AlreadyCalledError", "TimeoutError", "gatherResults",
+ "maybeDeferred",
+ "waitForDeferred", "deferredGenerator", "inlineCallbacks",
+ "returnValue",
+ "DeferredLock", "DeferredSemaphore", "DeferredQueue",
+ "DeferredFilesystemLock", "AlreadyTryingToLockError",
+ ]
diff --git a/twisted/internet/endpoints.py b/twisted/internet/endpoints.py
new file mode 100644
index 0000000..8306dc2
--- /dev/null
+++ b/twisted/internet/endpoints.py
@@ -0,0 +1,1202 @@
+# -*- test-case-name: twisted.internet.test.test_endpoints -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+"""
+Implementations of L{IStreamServerEndpoint} and L{IStreamClientEndpoint} that
+wrap the L{IReactorTCP}, L{IReactorSSL}, and L{IReactorUNIX} interfaces.
+
+This also implements an extensible mini-language for describing endpoints,
+parsed by the L{clientFromString} and L{serverFromString} functions.
+
+@since: 10.1
+"""
+
+import os, socket
+
+from zope.interface import implements, directlyProvides
+import warnings
+
+from twisted.internet import interfaces, defer, error, fdesc
+from twisted.internet.protocol import ClientFactory, Protocol
+from twisted.plugin import IPlugin, getPlugins
+from twisted.internet.interfaces import IStreamServerEndpointStringParser
+from twisted.internet.interfaces import IStreamClientEndpointStringParser
+from twisted.python.filepath import FilePath
+from twisted.python.systemd import ListenFDs
+
+
+__all__ = ["clientFromString", "serverFromString",
+ "TCP4ServerEndpoint", "TCP4ClientEndpoint",
+ "UNIXServerEndpoint", "UNIXClientEndpoint",
+ "SSL4ServerEndpoint", "SSL4ClientEndpoint",
+ "AdoptedStreamServerEndpoint"]
+
+
+class _WrappingProtocol(Protocol):
+ """
+ Wrap another protocol in order to notify my user when a connection has
+ been made.
+
+ @ivar _connectedDeferred: The L{Deferred} that will callback
+ with the C{wrappedProtocol} when it is connected.
+
+ @ivar _wrappedProtocol: An L{IProtocol} provider that will be
+ connected.
+ """
+
+ def __init__(self, connectedDeferred, wrappedProtocol):
+ """
+ @param connectedDeferred: The L{Deferred} that will callback
+ with the C{wrappedProtocol} when it is connected.
+
+ @param wrappedProtocol: An L{IProtocol} provider that will be
+ connected.
+ """
+ self._connectedDeferred = connectedDeferred
+ self._wrappedProtocol = wrappedProtocol
+
+ for iface in [interfaces.IHalfCloseableProtocol,
+ interfaces.IFileDescriptorReceiver]:
+ if iface.providedBy(self._wrappedProtocol):
+ directlyProvides(self, iface)
+
+
+ def logPrefix(self):
+ """
+ Transparently pass through the wrapped protocol's log prefix.
+ """
+ if interfaces.ILoggingContext.providedBy(self._wrappedProtocol):
+ return self._wrappedProtocol.logPrefix()
+ return self._wrappedProtocol.__class__.__name__
+
+
+ def connectionMade(self):
+ """
+ Connect the C{self._wrappedProtocol} to our C{self.transport} and
+ callback C{self._connectedDeferred} with the C{self._wrappedProtocol}
+ """
+ self._wrappedProtocol.makeConnection(self.transport)
+ self._connectedDeferred.callback(self._wrappedProtocol)
+
+
+ def dataReceived(self, data):
+ """
+ Proxy C{dataReceived} calls to our C{self._wrappedProtocol}
+ """
+ return self._wrappedProtocol.dataReceived(data)
+
+
+ def fileDescriptorReceived(self, descriptor):
+ """
+ Proxy C{fileDescriptorReceived} calls to our C{self._wrappedProtocol}
+ """
+ return self._wrappedProtocol.fileDescriptorReceived(descriptor)
+
+
+ def connectionLost(self, reason):
+ """
+ Proxy C{connectionLost} calls to our C{self._wrappedProtocol}
+ """
+ return self._wrappedProtocol.connectionLost(reason)
+
+
+ def readConnectionLost(self):
+ """
+ Proxy L{IHalfCloseableProtocol.readConnectionLost} to our
+ C{self._wrappedProtocol}
+ """
+ self._wrappedProtocol.readConnectionLost()
+
+
+ def writeConnectionLost(self):
+ """
+ Proxy L{IHalfCloseableProtocol.writeConnectionLost} to our
+ C{self._wrappedProtocol}
+ """
+ self._wrappedProtocol.writeConnectionLost()
+
+
+
+class _WrappingFactory(ClientFactory):
+ """
+ Wrap a factory in order to wrap the protocols it builds.
+
+ @ivar _wrappedFactory: A provider of I{IProtocolFactory} whose buildProtocol
+ method will be called and whose resulting protocol will be wrapped.
+
+ @ivar _onConnection: An L{Deferred} that fires when the protocol is
+ connected
+
+ @ivar _connector: A L{connector <twisted.internet.interfaces.IConnector>}
+ that is managing the current or previous connection attempt.
+ """
+ protocol = _WrappingProtocol
+
+ def __init__(self, wrappedFactory):
+ """
+ @param wrappedFactory: A provider of I{IProtocolFactory} whose
+ buildProtocol method will be called and whose resulting protocol
+ will be wrapped.
+ """
+ self._wrappedFactory = wrappedFactory
+ self._onConnection = defer.Deferred(canceller=self._canceller)
+
+
+ def startedConnecting(self, connector):
+ """
+ A connection attempt was started. Remember the connector which started
+ said attempt, for use later.
+ """
+ self._connector = connector
+
+
+ def _canceller(self, deferred):
+ """
+ The outgoing connection attempt was cancelled. Fail that L{Deferred}
+ with a L{error.ConnectingCancelledError}.
+
+ @param deferred: The L{Deferred <defer.Deferred>} that was cancelled;
+ should be the same as C{self._onConnection}.
+ @type deferred: L{Deferred <defer.Deferred>}
+
+ @note: This relies on startedConnecting having been called, so it may
+ seem as though there's a race condition where C{_connector} may not
+ have been set. However, using public APIs, this condition is
+ impossible to catch, because a connection API
+ (C{connectTCP}/C{SSL}/C{UNIX}) is always invoked before a
+ L{_WrappingFactory}'s L{Deferred <defer.Deferred>} is returned to
+ C{connect()}'s caller.
+
+ @return: C{None}
+ """
+ deferred.errback(
+ error.ConnectingCancelledError(
+ self._connector.getDestination()))
+ self._connector.stopConnecting()
+
+
+ def doStart(self):
+ """
+ Start notifications are passed straight through to the wrapped factory.
+ """
+ self._wrappedFactory.doStart()
+
+
+ def doStop(self):
+ """
+ Stop notifications are passed straight through to the wrapped factory.
+ """
+ self._wrappedFactory.doStop()
+
+
+ def buildProtocol(self, addr):
+ """
+ Proxy C{buildProtocol} to our C{self._wrappedFactory} or errback
+ the C{self._onConnection} L{Deferred}.
+
+ @return: An instance of L{_WrappingProtocol} or C{None}
+ """
+ try:
+ proto = self._wrappedFactory.buildProtocol(addr)
+ except:
+ self._onConnection.errback()
+ else:
+ return self.protocol(self._onConnection, proto)
+
+
+ def clientConnectionFailed(self, connector, reason):
+ """
+ Errback the C{self._onConnection} L{Deferred} when the
+ client connection fails.
+ """
+ if not self._onConnection.called:
+ self._onConnection.errback(reason)
+
+
+
+class TCP4ServerEndpoint(object):
+ """
+ TCP server endpoint with an IPv4 configuration
+
+ @ivar _reactor: An L{IReactorTCP} provider.
+
+ @type _port: int
+ @ivar _port: The port number on which to listen for incoming connections.
+
+ @type _backlog: int
+ @ivar _backlog: size of the listen queue
+
+ @type _interface: str
+ @ivar _interface: the hostname to bind to, defaults to '' (all)
+ """
+ implements(interfaces.IStreamServerEndpoint)
+
+ def __init__(self, reactor, port, backlog=50, interface=''):
+ """
+ @param reactor: An L{IReactorTCP} provider.
+ @param port: The port number used listening
+ @param backlog: size of the listen queue
+ @param interface: the hostname to bind to, defaults to '' (all)
+ """
+ self._reactor = reactor
+ self._port = port
+ self._listenArgs = dict(backlog=50, interface='')
+ self._backlog = backlog
+ self._interface = interface
+
+
+ def listen(self, protocolFactory):
+ """
+ Implement L{IStreamServerEndpoint.listen} to listen on a TCP socket
+ """
+ return defer.execute(self._reactor.listenTCP,
+ self._port,
+ protocolFactory,
+ backlog=self._backlog,
+ interface=self._interface)
+
+
+
+class TCP4ClientEndpoint(object):
+ """
+ TCP client endpoint with an IPv4 configuration.
+
+ @ivar _reactor: An L{IReactorTCP} provider.
+
+ @type _host: str
+ @ivar _host: The hostname to connect to as a C{str}
+
+ @type _port: int
+ @ivar _port: The port to connect to as C{int}
+
+ @type _timeout: int
+ @ivar _timeout: number of seconds to wait before assuming the
+ connection has failed.
+
+ @type _bindAddress: tuple
+ @type _bindAddress: a (host, port) tuple of local address to bind
+ to, or None.
+ """
+ implements(interfaces.IStreamClientEndpoint)
+
+ def __init__(self, reactor, host, port, timeout=30, bindAddress=None):
+ """
+ @param reactor: An L{IReactorTCP} provider
+ @param host: A hostname, used when connecting
+ @param port: The port number, used when connecting
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+ @param bindAddress: a (host, port tuple of local address to bind to,
+ or None.
+ """
+ self._reactor = reactor
+ self._host = host
+ self._port = port
+ self._timeout = timeout
+ self._bindAddress = bindAddress
+
+
+ def connect(self, protocolFactory):
+ """
+ Implement L{IStreamClientEndpoint.connect} to connect via TCP.
+ """
+ try:
+ wf = _WrappingFactory(protocolFactory)
+ self._reactor.connectTCP(
+ self._host, self._port, wf,
+ timeout=self._timeout, bindAddress=self._bindAddress)
+ return wf._onConnection
+ except:
+ return defer.fail()
+
+
+
+class SSL4ServerEndpoint(object):
+ """
+ SSL secured TCP server endpoint with an IPv4 configuration.
+
+ @ivar _reactor: An L{IReactorSSL} provider.
+
+ @type _host: str
+ @ivar _host: The hostname to connect to as a C{str}
+
+ @type _port: int
+ @ivar _port: The port to connect to as C{int}
+
+ @type _sslContextFactory: L{OpenSSLCertificateOptions}
+ @var _sslContextFactory: SSL Configuration information as an
+ L{OpenSSLCertificateOptions}
+
+ @type _backlog: int
+ @ivar _backlog: size of the listen queue
+
+ @type _interface: str
+ @ivar _interface: the hostname to bind to, defaults to '' (all)
+ """
+ implements(interfaces.IStreamServerEndpoint)
+
+ def __init__(self, reactor, port, sslContextFactory,
+ backlog=50, interface=''):
+ """
+ @param reactor: An L{IReactorSSL} provider.
+ @param port: The port number used listening
+ @param sslContextFactory: An instance of
+ L{twisted.internet._sslverify.OpenSSLCertificateOptions}.
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+ @param bindAddress: a (host, port tuple of local address to bind to,
+ or None.
+ """
+ self._reactor = reactor
+ self._port = port
+ self._sslContextFactory = sslContextFactory
+ self._backlog = backlog
+ self._interface = interface
+
+
+ def listen(self, protocolFactory):
+ """
+ Implement L{IStreamServerEndpoint.listen} to listen for SSL on a
+ TCP socket.
+ """
+ return defer.execute(self._reactor.listenSSL, self._port,
+ protocolFactory,
+ contextFactory=self._sslContextFactory,
+ backlog=self._backlog,
+ interface=self._interface)
+
+
+
+class SSL4ClientEndpoint(object):
+ """
+ SSL secured TCP client endpoint with an IPv4 configuration
+
+ @ivar _reactor: An L{IReactorSSL} provider.
+
+ @type _host: str
+ @ivar _host: The hostname to connect to as a C{str}
+
+ @type _port: int
+ @ivar _port: The port to connect to as C{int}
+
+ @type _sslContextFactory: L{OpenSSLCertificateOptions}
+ @var _sslContextFactory: SSL Configuration information as an
+ L{OpenSSLCertificateOptions}
+
+ @type _timeout: int
+ @ivar _timeout: number of seconds to wait before assuming the
+ connection has failed.
+
+ @type _bindAddress: tuple
+ @ivar _bindAddress: a (host, port) tuple of local address to bind
+ to, or None.
+ """
+ implements(interfaces.IStreamClientEndpoint)
+
+ def __init__(self, reactor, host, port, sslContextFactory,
+ timeout=30, bindAddress=None):
+ """
+ @param reactor: An L{IReactorSSL} provider.
+ @param host: A hostname, used when connecting
+ @param port: The port number, used when connecting
+ @param sslContextFactory: SSL Configuration information as An instance
+ of L{OpenSSLCertificateOptions}.
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+ @param bindAddress: a (host, port tuple of local address to bind to,
+ or None.
+ """
+ self._reactor = reactor
+ self._host = host
+ self._port = port
+ self._sslContextFactory = sslContextFactory
+ self._timeout = timeout
+ self._bindAddress = bindAddress
+
+
+ def connect(self, protocolFactory):
+ """
+ Implement L{IStreamClientEndpoint.connect} to connect with SSL over
+ TCP.
+ """
+ try:
+ wf = _WrappingFactory(protocolFactory)
+ self._reactor.connectSSL(
+ self._host, self._port, wf, self._sslContextFactory,
+ timeout=self._timeout, bindAddress=self._bindAddress)
+ return wf._onConnection
+ except:
+ return defer.fail()
+
+
+
+class UNIXServerEndpoint(object):
+ """
+ UnixSocket server endpoint.
+
+ @type path: str
+ @ivar path: a path to a unix socket on the filesystem.
+
+ @type _listenArgs: dict
+ @ivar _listenArgs: A C{dict} of keyword args that will be passed
+ to L{IReactorUNIX.listenUNIX}
+
+ @var _reactor: An L{IReactorTCP} provider.
+ """
+ implements(interfaces.IStreamServerEndpoint)
+
+ def __init__(self, reactor, address, backlog=50, mode=0666, wantPID=0):
+ """
+ @param reactor: An L{IReactorUNIX} provider.
+ @param address: The path to the Unix socket file, used when listening
+ @param listenArgs: An optional dict of keyword args that will be
+ passed to L{IReactorUNIX.listenUNIX}
+ @param backlog: number of connections to allow in backlog.
+ @param mode: mode to set on the unix socket. This parameter is
+ deprecated. Permissions should be set on the directory which
+ contains the UNIX socket.
+ @param wantPID: if True, create a pidfile for the socket.
+ """
+ self._reactor = reactor
+ self._address = address
+ self._backlog = backlog
+ self._mode = mode
+ self._wantPID = wantPID
+
+
+ def listen(self, protocolFactory):
+ """
+ Implement L{IStreamServerEndpoint.listen} to listen on a UNIX socket.
+ """
+ return defer.execute(self._reactor.listenUNIX, self._address,
+ protocolFactory,
+ backlog=self._backlog,
+ mode=self._mode,
+ wantPID=self._wantPID)
+
+
+
+class UNIXClientEndpoint(object):
+ """
+ UnixSocket client endpoint.
+
+ @type _path: str
+ @ivar _path: a path to a unix socket on the filesystem.
+
+ @type _timeout: int
+ @ivar _timeout: number of seconds to wait before assuming the connection
+ has failed.
+
+ @type _checkPID: bool
+ @ivar _checkPID: if True, check for a pid file to verify that a server
+ is listening.
+
+ @var _reactor: An L{IReactorUNIX} provider.
+ """
+ implements(interfaces.IStreamClientEndpoint)
+
+ def __init__(self, reactor, path, timeout=30, checkPID=0):
+ """
+ @param reactor: An L{IReactorUNIX} provider.
+ @param path: The path to the Unix socket file, used when connecting
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+ @param checkPID: if True, check for a pid file to verify that a server
+ is listening.
+ """
+ self._reactor = reactor
+ self._path = path
+ self._timeout = timeout
+ self._checkPID = checkPID
+
+
+ def connect(self, protocolFactory):
+ """
+ Implement L{IStreamClientEndpoint.connect} to connect via a
+ UNIX Socket
+ """
+ try:
+ wf = _WrappingFactory(protocolFactory)
+ self._reactor.connectUNIX(
+ self._path, wf,
+ timeout=self._timeout,
+ checkPID=self._checkPID)
+ return wf._onConnection
+ except:
+ return defer.fail()
+
+
+
+class AdoptedStreamServerEndpoint(object):
+ """
+ An endpoint for listening on a file descriptor initialized outside of
+ Twisted.
+
+ @ivar _used: A C{bool} indicating whether this endpoint has been used to
+ listen with a factory yet. C{True} if so.
+ """
+ _close = os.close
+ _setNonBlocking = staticmethod(fdesc.setNonBlocking)
+
+ def __init__(self, reactor, fileno, addressFamily):
+ """
+ @param reactor: An L{IReactorSocket} provider.
+
+ @param fileno: An integer file descriptor corresponding to a listening
+ I{SOCK_STREAM} socket.
+
+ @param addressFamily: The address family of the socket given by
+ C{fileno}.
+ """
+ self.reactor = reactor
+ self.fileno = fileno
+ self.addressFamily = addressFamily
+ self._used = False
+
+
+ def listen(self, factory):
+ """
+ Implement L{IStreamServerEndpoint.listen} to start listening on, and
+ then close, C{self._fileno}.
+ """
+ if self._used:
+ return defer.fail(error.AlreadyListened())
+ self._used = True
+
+ try:
+ self._setNonBlocking(self.fileno)
+ port = self.reactor.adoptStreamPort(
+ self.fileno, self.addressFamily, factory)
+ self._close(self.fileno)
+ except:
+ return defer.fail()
+ return defer.succeed(port)
+
+
+
+def _parseTCP(factory, port, interface="", backlog=50):
+ """
+ Internal parser function for L{_parseServer} to convert the string
+ arguments for a TCP(IPv4) stream endpoint into the structured arguments.
+
+ @param factory: the protocol factory being parsed, or C{None}. (This was a
+ leftover argument from when this code was in C{strports}, and is now
+ mostly None and unused.)
+
+ @type factory: L{IProtocolFactory} or C{NoneType}
+
+ @param port: the integer port number to bind
+ @type port: C{str}
+
+ @param interface: the interface IP to listen on
+ @param backlog: the length of the listen queue
+ @type backlog: C{str}
+
+ @return: a 2-tuple of (args, kwargs), describing the parameters to
+ L{IReactorTCP.listenTCP} (or, modulo argument 2, the factory, arguments
+ to L{TCP4ServerEndpoint}.
+ """
+ return (int(port), factory), {'interface': interface,
+ 'backlog': int(backlog)}
+
+
+
+def _parseUNIX(factory, address, mode='666', backlog=50, lockfile=True):
+ """
+ Internal parser function for L{_parseServer} to convert the string
+ arguments for a UNIX (AF_UNIX/SOCK_STREAM) stream endpoint into the
+ structured arguments.
+
+ @param factory: the protocol factory being parsed, or C{None}. (This was a
+ leftover argument from when this code was in C{strports}, and is now
+ mostly None and unused.)
+
+ @type factory: L{IProtocolFactory} or C{NoneType}
+
+ @param address: the pathname of the unix socket
+ @type address: C{str}
+
+ @param backlog: the length of the listen queue
+ @type backlog: C{str}
+
+ @param lockfile: A string '0' or '1', mapping to True and False
+ respectively. See the C{wantPID} argument to C{listenUNIX}
+
+ @return: a 2-tuple of (args, kwargs), describing the parameters to
+ L{IReactorTCP.listenUNIX} (or, modulo argument 2, the factory,
+ arguments to L{UNIXServerEndpoint}.
+ """
+ return (
+ (address, factory),
+ {'mode': int(mode, 8), 'backlog': int(backlog),
+ 'wantPID': bool(int(lockfile))})
+
+
+
+def _parseSSL(factory, port, privateKey="server.pem", certKey=None,
+ sslmethod=None, interface='', backlog=50):
+ """
+ Internal parser function for L{_parseServer} to convert the string
+ arguments for an SSL (over TCP/IPv4) stream endpoint into the structured
+ arguments.
+
+ @param factory: the protocol factory being parsed, or C{None}. (This was a
+ leftover argument from when this code was in C{strports}, and is now
+ mostly None and unused.)
+
+ @type factory: L{IProtocolFactory} or C{NoneType}
+
+ @param port: the integer port number to bind
+ @type port: C{str}
+
+ @param interface: the interface IP to listen on
+ @param backlog: the length of the listen queue
+ @type backlog: C{str}
+
+ @param privateKey: The file name of a PEM format private key file.
+ @type privateKey: C{str}
+
+ @param certKey: The file name of a PEM format certificate file.
+ @type certKey: C{str}
+
+ @param sslmethod: The string name of an SSL method, based on the name of a
+ constant in C{OpenSSL.SSL}. Must be one of: "SSLv23_METHOD",
+ "SSLv2_METHOD", "SSLv3_METHOD", "TLSv1_METHOD".
+ @type sslmethod: C{str}
+
+ @return: a 2-tuple of (args, kwargs), describing the parameters to
+ L{IReactorSSL.listenSSL} (or, modulo argument 2, the factory, arguments
+ to L{SSL4ServerEndpoint}.
+ """
+ from twisted.internet import ssl
+ if certKey is None:
+ certKey = privateKey
+ kw = {}
+ if sslmethod is not None:
+ kw['sslmethod'] = getattr(ssl.SSL, sslmethod)
+ cf = ssl.DefaultOpenSSLContextFactory(privateKey, certKey, **kw)
+ return ((int(port), factory, cf),
+ {'interface': interface, 'backlog': int(backlog)})
+
+
+class _SystemdParser(object):
+ """
+ Stream server endpoint string parser for the I{systemd} endpoint type.
+
+ @ivar prefix: See L{IStreamClientEndpointStringParser.prefix}.
+
+ @ivar _sddaemon: A L{ListenFDs} instance used to translate an index into an
+ actual file descriptor.
+ """
+ implements(IPlugin, IStreamServerEndpointStringParser)
+
+ _sddaemon = ListenFDs.fromEnvironment()
+
+ prefix = "systemd"
+
+ def _parseServer(self, reactor, domain, index):
+ """
+ Internal parser function for L{_parseServer} to convert the string
+ arguments for a systemd server endpoint into structured arguments for
+ L{AdoptedStreamServerEndpoint}.
+
+ @param reactor: An L{IReactorSocket} provider.
+
+ @param domain: The domain (or address family) of the socket inherited
+ from systemd. This is a string like C{"INET"} or C{"UNIX"}, ie the
+ name of an address family from the L{socket} module, without the
+ C{"AF_"} prefix.
+ @type domain: C{str}
+
+ @param index: An offset into the list of file descriptors inherited from
+ systemd.
+ @type index: C{str}
+
+ @return: A two-tuple of parsed positional arguments and parsed keyword
+ arguments (a tuple and a dictionary). These can be used to
+ construct a L{AdoptedStreamServerEndpoint}.
+ """
+ index = int(index)
+ fileno = self._sddaemon.inheritedDescriptors()[index]
+ addressFamily = getattr(socket, 'AF_' + domain)
+ return AdoptedStreamServerEndpoint(reactor, fileno, addressFamily)
+
+
+ def parseStreamServer(self, reactor, *args, **kwargs):
+ # Delegate to another function with a sane signature. This function has
+ # an insane signature to trick zope.interface into believing the
+ # interface is correctly implemented.
+ return self._parseServer(reactor, *args, **kwargs)
+
+
+
+_serverParsers = {"tcp": _parseTCP,
+ "unix": _parseUNIX,
+ "ssl": _parseSSL,
+ }
+
+_OP, _STRING = range(2)
+
+def _tokenize(description):
+ """
+ Tokenize a strports string and yield each token.
+
+ @param description: a string as described by L{serverFromString} or
+ L{clientFromString}.
+
+ @return: an iterable of 2-tuples of (L{_OP} or L{_STRING}, string). Tuples
+ starting with L{_OP} will contain a second element of either ':' (i.e.
+ 'next parameter') or '=' (i.e. 'assign parameter value'). For example,
+ the string 'hello:greet\=ing=world' would result in a generator
+ yielding these values::
+
+ _STRING, 'hello'
+ _OP, ':'
+ _STRING, 'greet=ing'
+ _OP, '='
+ _STRING, 'world'
+ """
+ current = ''
+ ops = ':='
+ nextOps = {':': ':=', '=': ':'}
+ description = iter(description)
+ for n in description:
+ if n in ops:
+ yield _STRING, current
+ yield _OP, n
+ current = ''
+ ops = nextOps[n]
+ elif n == '\\':
+ current += description.next()
+ else:
+ current += n
+ yield _STRING, current
+
+
+
+def _parse(description):
+ """
+ Convert a description string into a list of positional and keyword
+ parameters, using logic vaguely like what Python does.
+
+ @param description: a string as described by L{serverFromString} or
+ L{clientFromString}.
+
+ @return: a 2-tuple of C{(args, kwargs)}, where 'args' is a list of all
+ ':'-separated C{str}s not containing an '=' and 'kwargs' is a map of
+ all C{str}s which do contain an '='. For example, the result of
+ C{_parse('a:b:d=1:c')} would be C{(['a', 'b', 'c'], {'d': '1'})}.
+ """
+ args, kw = [], {}
+ def add(sofar):
+ if len(sofar) == 1:
+ args.append(sofar[0])
+ else:
+ kw[sofar[0]] = sofar[1]
+ sofar = ()
+ for (type, value) in _tokenize(description):
+ if type is _STRING:
+ sofar += (value,)
+ elif value == ':':
+ add(sofar)
+ sofar = ()
+ add(sofar)
+ return args, kw
+
+
+# Mappings from description "names" to endpoint constructors.
+_endpointServerFactories = {
+ 'TCP': TCP4ServerEndpoint,
+ 'SSL': SSL4ServerEndpoint,
+ 'UNIX': UNIXServerEndpoint,
+ }
+
+_endpointClientFactories = {
+ 'TCP': TCP4ClientEndpoint,
+ 'SSL': SSL4ClientEndpoint,
+ 'UNIX': UNIXClientEndpoint,
+ }
+
+
+_NO_DEFAULT = object()
+
+def _parseServer(description, factory, default=None):
+ """
+ Parse a stports description into a 2-tuple of arguments and keyword values.
+
+ @param description: A description in the format explained by
+ L{serverFromString}.
+ @type description: C{str}
+
+ @param factory: A 'factory' argument; this is left-over from
+ twisted.application.strports, it's not really used.
+ @type factory: L{IProtocolFactory} or L{None}
+
+ @param default: Deprecated argument, specifying the default parser mode to
+ use for unqualified description strings (those which do not have a ':'
+ and prefix).
+ @type default: C{str} or C{NoneType}
+
+ @return: a 3-tuple of (plugin or name, arguments, keyword arguments)
+ """
+ args, kw = _parse(description)
+ if not args or (len(args) == 1 and not kw):
+ deprecationMessage = (
+ "Unqualified strport description passed to 'service'."
+ "Use qualified endpoint descriptions; for example, 'tcp:%s'."
+ % (description,))
+ if default is None:
+ default = 'tcp'
+ warnings.warn(
+ deprecationMessage, category=DeprecationWarning, stacklevel=4)
+ elif default is _NO_DEFAULT:
+ raise ValueError(deprecationMessage)
+ # If the default has been otherwise specified, the user has already
+ # been warned.
+ args[0:0] = [default]
+ endpointType = args[0]
+ parser = _serverParsers.get(endpointType)
+ if parser is None:
+ for plugin in getPlugins(IStreamServerEndpointStringParser):
+ if plugin.prefix == endpointType:
+ return (plugin, args[1:], kw)
+ raise ValueError("Unknown endpoint type: '%s'" % (endpointType,))
+ return (endpointType.upper(),) + parser(factory, *args[1:], **kw)
+
+
+
+def _serverFromStringLegacy(reactor, description, default):
+ """
+ Underlying implementation of L{serverFromString} which avoids exposing the
+ deprecated 'default' argument to anything but L{strports.service}.
+ """
+ nameOrPlugin, args, kw = _parseServer(description, None, default)
+ if type(nameOrPlugin) is not str:
+ plugin = nameOrPlugin
+ return plugin.parseStreamServer(reactor, *args, **kw)
+ else:
+ name = nameOrPlugin
+ # Chop out the factory.
+ args = args[:1] + args[2:]
+ return _endpointServerFactories[name](reactor, *args, **kw)
+
+
+
+def serverFromString(reactor, description):
+ """
+ Construct a stream server endpoint from an endpoint description string.
+
+ The format for server endpoint descriptions is a simple string. It is a
+ prefix naming the type of endpoint, then a colon, then the arguments for
+ that endpoint.
+
+ For example, you can call it like this to create an endpoint that will
+ listen on TCP port 80::
+
+ serverFromString(reactor, "tcp:80")
+
+ Additional arguments may be specified as keywords, separated with colons.
+ For example, you can specify the interface for a TCP server endpoint to
+ bind to like this::
+
+ serverFromString(reactor, "tcp:80:interface=127.0.0.1")
+
+ SSL server endpoints may be specified with the 'ssl' prefix, and the
+ private key and certificate files may be specified by the C{privateKey} and
+ C{certKey} arguments::
+
+ serverFromString(reactor, "ssl:443:privateKey=key.pem:certKey=crt.pem")
+
+ If a private key file name (C{privateKey}) isn't provided, a "server.pem"
+ file is assumed to exist which contains the private key. If the certificate
+ file name (C{certKey}) isn't provided, the private key file is assumed to
+ contain the certificate as well.
+
+ You may escape colons in arguments with a backslash, which you will need to
+ use if you want to specify a full pathname argument on Windows::
+
+ serverFromString(reactor,
+ "ssl:443:privateKey=C\\:/key.pem:certKey=C\\:/cert.pem")
+
+ finally, the 'unix' prefix may be used to specify a filesystem UNIX socket,
+ optionally with a 'mode' argument to specify the mode of the socket file
+ created by C{listen}::
+
+ serverFromString(reactor, "unix:/var/run/finger")
+ serverFromString(reactor, "unix:/var/run/finger:mode=660")
+
+ This function is also extensible; new endpoint types may be registered as
+ L{IStreamServerEndpointStringParser} plugins. See that interface for more
+ information.
+
+ @param reactor: The server endpoint will be constructed with this reactor.
+
+ @param description: The strports description to parse.
+
+ @return: A new endpoint which can be used to listen with the parameters
+ given by by C{description}.
+
+ @rtype: L{IStreamServerEndpoint<twisted.internet.interfaces.IStreamServerEndpoint>}
+
+ @raise ValueError: when the 'description' string cannot be parsed.
+
+ @since: 10.2
+ """
+ return _serverFromStringLegacy(reactor, description, _NO_DEFAULT)
+
+
+
+def quoteStringArgument(argument):
+ """
+ Quote an argument to L{serverFromString} and L{clientFromString}. Since
+ arguments are separated with colons and colons are escaped with
+ backslashes, some care is necessary if, for example, you have a pathname,
+ you may be tempted to interpolate into a string like this::
+
+ serverFromString("ssl:443:privateKey=%s" % (myPathName,))
+
+ This may appear to work, but will have portability issues (Windows
+ pathnames, for example). Usually you should just construct the appropriate
+ endpoint type rather than interpolating strings, which in this case would
+ be L{SSL4ServerEndpoint}. There are some use-cases where you may need to
+ generate such a string, though; for example, a tool to manipulate a
+ configuration file which has strports descriptions in it. To be correct in
+ those cases, do this instead::
+
+ serverFromString("ssl:443:privateKey=%s" %
+ (quoteStringArgument(myPathName),))
+
+ @param argument: The part of the endpoint description string you want to
+ pass through.
+
+ @type argument: C{str}
+
+ @return: The quoted argument.
+
+ @rtype: C{str}
+ """
+ return argument.replace('\\', '\\\\').replace(':', '\\:')
+
+
+
+def _parseClientTCP(*args, **kwargs):
+ """
+ Perform any argument value coercion necessary for TCP client parameters.
+
+ Valid positional arguments to this function are host and port.
+
+ Valid keyword arguments to this function are all L{IReactorTCP.connectTCP}
+ arguments.
+
+ @return: The coerced values as a C{dict}.
+ """
+
+ if len(args) == 2:
+ kwargs['port'] = int(args[1])
+ kwargs['host'] = args[0]
+ elif len(args) == 1:
+ if 'host' in kwargs:
+ kwargs['port'] = int(args[0])
+ else:
+ kwargs['host'] = args[0]
+
+ try:
+ kwargs['port'] = int(kwargs['port'])
+ except KeyError:
+ pass
+
+ try:
+ kwargs['timeout'] = int(kwargs['timeout'])
+ except KeyError:
+ pass
+ return kwargs
+
+
+
+def _loadCAsFromDir(directoryPath):
+ """
+ Load certificate-authority certificate objects in a given directory.
+
+ @param directoryPath: a L{FilePath} pointing at a directory to load .pem
+ files from.
+
+ @return: a C{list} of L{OpenSSL.crypto.X509} objects.
+ """
+ from twisted.internet import ssl
+
+ caCerts = {}
+ for child in directoryPath.children():
+ if not child.basename().split('.')[-1].lower() == 'pem':
+ continue
+ try:
+ data = child.getContent()
+ except IOError:
+ # Permission denied, corrupt disk, we don't care.
+ continue
+ try:
+ theCert = ssl.Certificate.loadPEM(data)
+ except ssl.SSL.Error:
+ # Duplicate certificate, invalid certificate, etc. We don't care.
+ pass
+ else:
+ caCerts[theCert.digest()] = theCert.original
+ return caCerts.values()
+
+
+
+def _parseClientSSL(*args, **kwargs):
+ """
+ Perform any argument value coercion necessary for SSL client parameters.
+
+ Valid keyword arguments to this function are all L{IReactorSSL.connectSSL}
+ arguments except for C{contextFactory}. Instead, C{certKey} (the path name
+ of the certificate file) C{privateKey} (the path name of the private key
+ associated with the certificate) are accepted and used to construct a
+ context factory.
+
+ Valid positional arguments to this function are host and port.
+
+ @param caCertsDir: The one parameter which is not part of
+ L{IReactorSSL.connectSSL}'s signature, this is a path name used to
+ construct a list of certificate authority certificates. The directory
+ will be scanned for files ending in C{.pem}, all of which will be
+ considered valid certificate authorities for this connection.
+
+ @type caCertsDir: C{str}
+
+ @return: The coerced values as a C{dict}.
+ """
+ from twisted.internet import ssl
+ kwargs = _parseClientTCP(*args, **kwargs)
+ certKey = kwargs.pop('certKey', None)
+ privateKey = kwargs.pop('privateKey', None)
+ caCertsDir = kwargs.pop('caCertsDir', None)
+ if certKey is not None:
+ certx509 = ssl.Certificate.loadPEM(
+ FilePath(certKey).getContent()).original
+ else:
+ certx509 = None
+ if privateKey is not None:
+ privateKey = ssl.PrivateCertificate.loadPEM(
+ FilePath(privateKey).getContent()).privateKey.original
+ else:
+ privateKey = None
+ if caCertsDir is not None:
+ verify = True
+ caCerts = _loadCAsFromDir(FilePath(caCertsDir))
+ else:
+ verify = False
+ caCerts = None
+ kwargs['sslContextFactory'] = ssl.CertificateOptions(
+ method=ssl.SSL.SSLv23_METHOD,
+ certificate=certx509,
+ privateKey=privateKey,
+ verify=verify,
+ caCerts=caCerts
+ )
+ return kwargs
+
+
+
+def _parseClientUNIX(*args, **kwargs):
+ """
+ Perform any argument value coercion necessary for UNIX client parameters.
+
+ Valid keyword arguments to this function are all L{IReactorUNIX.connectUNIX}
+ keyword arguments except for C{checkPID}. Instead, C{lockfile} is accepted
+ and has the same meaning. Also C{path} is used instead of C{address}.
+
+ Valid positional arguments to this function are C{path}.
+
+ @return: The coerced values as a C{dict}.
+ """
+ if len(args) == 1:
+ kwargs['path'] = args[0]
+
+ try:
+ kwargs['checkPID'] = bool(int(kwargs.pop('lockfile')))
+ except KeyError:
+ pass
+ try:
+ kwargs['timeout'] = int(kwargs['timeout'])
+ except KeyError:
+ pass
+ return kwargs
+
+_clientParsers = {
+ 'TCP': _parseClientTCP,
+ 'SSL': _parseClientSSL,
+ 'UNIX': _parseClientUNIX,
+ }
+
+
+
+def clientFromString(reactor, description):
+ """
+ Construct a client endpoint from a description string.
+
+ Client description strings are much like server description strings,
+ although they take all of their arguments as keywords, aside from host and
+ port.
+
+ You can create a TCP client endpoint with the 'host' and 'port' arguments,
+ like so::
+
+ clientFromString(reactor, "tcp:host=www.example.com:port=80")
+
+ or, without specifying host and port keywords::
+
+ clientFromString(reactor, "tcp:www.example.com:80")
+
+ Or you can specify only one or the other, as in the following 2 examples::
+
+ clientFromString(reactor, "tcp:host=www.example.com:80")
+ clientFromString(reactor, "tcp:www.example.com:port=80")
+
+ or an SSL client endpoint with those arguments, plus the arguments used by
+ the server SSL, for a client certificate::
+
+ clientFromString(reactor, "ssl:web.example.com:443:"
+ "privateKey=foo.pem:certKey=foo.pem")
+
+ to specify your certificate trust roots, you can identify a directory with
+ PEM files in it with the C{caCertsDir} argument::
+
+ clientFromString(reactor, "ssl:host=web.example.com:port=443:"
+ "caCertsDir=/etc/ssl/certs")
+
+ You can create a UNIX client endpoint with the 'path' argument and optional
+ 'lockfile' and 'timeout' arguments::
+
+ clientFromString(reactor, "unix:path=/var/foo/bar:lockfile=1:timeout=9")
+
+ or, with the path as a positional argument with or without optional
+ arguments as in the following 2 examples::
+
+ clientFromString(reactor, "unix:/var/foo/bar")
+ clientFromString(reactor, "unix:/var/foo/bar:lockfile=1:timeout=9")
+
+ This function is also extensible; new endpoint types may be registered as
+ L{IStreamClientEndpointStringParser} plugins. See that interface for more
+ information.
+
+ @param reactor: The client endpoint will be constructed with this reactor.
+
+ @param description: The strports description to parse.
+
+ @return: A new endpoint which can be used to connect with the parameters
+ given by by C{description}.
+ @rtype: L{IStreamClientEndpoint<twisted.internet.interfaces.IStreamClientEndpoint>}
+
+ @since: 10.2
+ """
+ args, kwargs = _parse(description)
+ aname = args.pop(0)
+ name = aname.upper()
+ for plugin in getPlugins(IStreamClientEndpointStringParser):
+ if plugin.prefix.upper() == name:
+ return plugin.parseStreamClient(*args, **kwargs)
+ if name not in _clientParsers:
+ raise ValueError("Unknown endpoint type: %r" % (aname,))
+ kwargs = _clientParsers[name](*args, **kwargs)
+ return _endpointClientFactories[name](reactor, **kwargs)
diff --git a/twisted/internet/epollreactor.py b/twisted/internet/epollreactor.py
new file mode 100644
index 0000000..f892d6b
--- /dev/null
+++ b/twisted/internet/epollreactor.py
@@ -0,0 +1,394 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An epoll() based implementation of the twisted main loop.
+
+To install the event loop (and you should do this before any connections,
+listeners or connectors are added)::
+
+ from twisted.internet import epollreactor
+ epollreactor.install()
+"""
+
+import errno
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IReactorFDSet
+
+from twisted.python import log
+from twisted.internet import posixbase
+
+try:
+ # In Python 2.6+, select.epoll provides epoll functionality. Try to import
+ # it, and fall back to Twisted's own epoll wrapper if it isn't available
+ # for any reason.
+ from select import epoll
+except ImportError:
+ from twisted.python import _epoll
+else:
+ del epoll
+ import select as _epoll
+
+
+
+class _ContinuousPolling(posixbase._PollLikeMixin,
+ posixbase._DisconnectSelectableMixin):
+ """
+ Schedule reads and writes based on the passage of time, rather than
+ notification.
+
+ This is useful for supporting polling filesystem files, which C{epoll(7)}
+ does not support.
+
+ The implementation uses L{posixbase._PollLikeMixin}, which is a bit hacky,
+ but re-implementing and testing the relevant code yet again is
+ unappealing.
+
+ @ivar _reactor: The L{EPollReactor} that is using this instance.
+
+ @ivar _loop: A C{LoopingCall} that drives the polling, or C{None}.
+
+ @ivar _readers: A C{set} of C{FileDescriptor} objects that should be read
+ from.
+
+ @ivar _writers: A C{set} of C{FileDescriptor} objects that should be
+ written to.
+ """
+ implements(IReactorFDSet)
+
+ # Attributes for _PollLikeMixin
+ _POLL_DISCONNECTED = 1
+ _POLL_IN = 2
+ _POLL_OUT = 4
+
+
+ def __init__(self, reactor):
+ self._reactor = reactor
+ self._loop = None
+ self._readers = set()
+ self._writers = set()
+ self.isReading = self._readers.__contains__
+ self.isWriting = self._writers.__contains__
+
+
+ def _checkLoop(self):
+ """
+ Start or stop a C{LoopingCall} based on whether there are readers and
+ writers.
+ """
+ if self._readers or self._writers:
+ if self._loop is None:
+ from twisted.internet.task import LoopingCall, _EPSILON
+ self._loop = LoopingCall(self.iterate)
+ self._loop.clock = self._reactor
+ # LoopingCall seems unhappy with timeout of 0, so use very
+ # small number:
+ self._loop.start(_EPSILON, now=False)
+ elif self._loop:
+ self._loop.stop()
+ self._loop = None
+
+
+ def iterate(self):
+ """
+ Call C{doRead} and C{doWrite} on all readers and writers respectively.
+ """
+ for reader in list(self._readers):
+ self._doReadOrWrite(reader, reader, self._POLL_IN)
+ for reader in list(self._writers):
+ self._doReadOrWrite(reader, reader, self._POLL_OUT)
+
+
+ def addReader(self, reader):
+ """
+ Add a C{FileDescriptor} for notification of data available to read.
+ """
+ self._readers.add(reader)
+ self._checkLoop()
+
+
+ def addWriter(self, writer):
+ """
+ Add a C{FileDescriptor} for notification of data available to write.
+ """
+ self._writers.add(writer)
+ self._checkLoop()
+
+
+ def removeReader(self, reader):
+ """
+ Remove a C{FileDescriptor} from notification of data available to read.
+ """
+ try:
+ self._readers.remove(reader)
+ except KeyError:
+ return
+ self._checkLoop()
+
+
+ def removeWriter(self, writer):
+ """
+ Remove a C{FileDescriptor} from notification of data available to write.
+ """
+ try:
+ self._writers.remove(writer)
+ except KeyError:
+ return
+ self._checkLoop()
+
+
+ def removeAll(self):
+ """
+ Remove all readers and writers.
+ """
+ result = list(self._readers | self._writers)
+ # Don't reset to new value, since self.isWriting and .isReading refer
+ # to the existing instance:
+ self._readers.clear()
+ self._writers.clear()
+ return result
+
+
+ def getReaders(self):
+ """
+ Return a list of the readers.
+ """
+ return list(self._readers)
+
+
+ def getWriters(self):
+ """
+ Return a list of the writers.
+ """
+ return list(self._writers)
+
+
+
+class EPollReactor(posixbase.PosixReactorBase, posixbase._PollLikeMixin):
+ """
+ A reactor that uses epoll(7).
+
+ @ivar _poller: A C{epoll} which will be used to check for I/O
+ readiness.
+
+ @ivar _selectables: A dictionary mapping integer file descriptors to
+ instances of C{FileDescriptor} which have been registered with the
+ reactor. All C{FileDescriptors} which are currently receiving read or
+ write readiness notifications will be present as values in this
+ dictionary.
+
+ @ivar _reads: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_poller} for read readiness notifications which will
+ be dispatched to the corresponding C{FileDescriptor} instances in
+ C{_selectables}.
+
+ @ivar _writes: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_poller} for write readiness notifications which will
+ be dispatched to the corresponding C{FileDescriptor} instances in
+ C{_selectables}.
+
+ @ivar _continuousPolling: A L{_ContinuousPolling} instance, used to handle
+ file descriptors (e.g. filesytem files) that are not supported by
+ C{epoll(7)}.
+ """
+ implements(IReactorFDSet)
+
+ # Attributes for _PollLikeMixin
+ _POLL_DISCONNECTED = (_epoll.EPOLLHUP | _epoll.EPOLLERR)
+ _POLL_IN = _epoll.EPOLLIN
+ _POLL_OUT = _epoll.EPOLLOUT
+
+ def __init__(self):
+ """
+ Initialize epoll object, file descriptor tracking dictionaries, and the
+ base class.
+ """
+ # Create the poller we're going to use. The 1024 here is just a hint to
+ # the kernel, it is not a hard maximum. After Linux 2.6.8, the size
+ # argument is completely ignored.
+ self._poller = _epoll.epoll(1024)
+ self._reads = {}
+ self._writes = {}
+ self._selectables = {}
+ self._continuousPolling = _ContinuousPolling(self)
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def _add(self, xer, primary, other, selectables, event, antievent):
+ """
+ Private method for adding a descriptor from the event loop.
+
+ It takes care of adding it if new or modifying it if already added
+ for another state (read -> read/write for example).
+ """
+ fd = xer.fileno()
+ if fd not in primary:
+ flags = event
+ # epoll_ctl can raise all kinds of IOErrors, and every one
+ # indicates a bug either in the reactor or application-code.
+ # Let them all through so someone sees a traceback and fixes
+ # something. We'll do the same thing for every other call to
+ # this method in this file.
+ if fd in other:
+ flags |= antievent
+ self._poller.modify(fd, flags)
+ else:
+ self._poller.register(fd, flags)
+
+ # Update our own tracking state *only* after the epoll call has
+ # succeeded. Otherwise we may get out of sync.
+ primary[fd] = 1
+ selectables[fd] = xer
+
+
+ def addReader(self, reader):
+ """
+ Add a FileDescriptor for notification of data available to read.
+ """
+ try:
+ self._add(reader, self._reads, self._writes, self._selectables,
+ _epoll.EPOLLIN, _epoll.EPOLLOUT)
+ except IOError, e:
+ if e.errno == errno.EPERM:
+ # epoll(7) doesn't support certain file descriptors,
+ # e.g. filesystem files, so for those we just poll
+ # continuously:
+ self._continuousPolling.addReader(reader)
+ else:
+ raise
+
+
+ def addWriter(self, writer):
+ """
+ Add a FileDescriptor for notification of data available to write.
+ """
+ try:
+ self._add(writer, self._writes, self._reads, self._selectables,
+ _epoll.EPOLLOUT, _epoll.EPOLLIN)
+ except IOError, e:
+ if e.errno == errno.EPERM:
+ # epoll(7) doesn't support certain file descriptors,
+ # e.g. filesystem files, so for those we just poll
+ # continuously:
+ self._continuousPolling.addWriter(writer)
+ else:
+ raise
+
+
+ def _remove(self, xer, primary, other, selectables, event, antievent):
+ """
+ Private method for removing a descriptor from the event loop.
+
+ It does the inverse job of _add, and also add a check in case of the fd
+ has gone away.
+ """
+ fd = xer.fileno()
+ if fd == -1:
+ for fd, fdes in selectables.items():
+ if xer is fdes:
+ break
+ else:
+ return
+ if fd in primary:
+ if fd in other:
+ flags = antievent
+ # See comment above modify call in _add.
+ self._poller.modify(fd, flags)
+ else:
+ del selectables[fd]
+ # See comment above _control call in _add.
+ self._poller.unregister(fd)
+ del primary[fd]
+
+
+ def removeReader(self, reader):
+ """
+ Remove a Selectable for notification of data available to read.
+ """
+ if self._continuousPolling.isReading(reader):
+ self._continuousPolling.removeReader(reader)
+ return
+ self._remove(reader, self._reads, self._writes, self._selectables,
+ _epoll.EPOLLIN, _epoll.EPOLLOUT)
+
+
+ def removeWriter(self, writer):
+ """
+ Remove a Selectable for notification of data available to write.
+ """
+ if self._continuousPolling.isWriting(writer):
+ self._continuousPolling.removeWriter(writer)
+ return
+ self._remove(writer, self._writes, self._reads, self._selectables,
+ _epoll.EPOLLOUT, _epoll.EPOLLIN)
+
+
+ def removeAll(self):
+ """
+ Remove all selectables, and return a list of them.
+ """
+ return (self._removeAll(
+ [self._selectables[fd] for fd in self._reads],
+ [self._selectables[fd] for fd in self._writes]) +
+ self._continuousPolling.removeAll())
+
+
+ def getReaders(self):
+ return ([self._selectables[fd] for fd in self._reads] +
+ self._continuousPolling.getReaders())
+
+
+ def getWriters(self):
+ return ([self._selectables[fd] for fd in self._writes] +
+ self._continuousPolling.getWriters())
+
+
+ def doPoll(self, timeout):
+ """
+ Poll the poller for new events.
+ """
+ if timeout is None:
+ timeout = -1 # Wait indefinitely.
+
+ try:
+ # Limit the number of events to the number of io objects we're
+ # currently tracking (because that's maybe a good heuristic) and
+ # the amount of time we block to the value specified by our
+ # caller.
+ l = self._poller.poll(timeout, len(self._selectables))
+ except IOError, err:
+ if err.errno == errno.EINTR:
+ return
+ # See epoll_wait(2) for documentation on the other conditions
+ # under which this can fail. They can only be due to a serious
+ # programming error on our part, so let's just announce them
+ # loudly.
+ raise
+
+ _drdw = self._doReadOrWrite
+ for fd, event in l:
+ try:
+ selectable = self._selectables[fd]
+ except KeyError:
+ pass
+ else:
+ log.callWithLogger(selectable, _drdw, selectable, fd, event)
+
+ doIteration = doPoll
+
+
+def install():
+ """
+ Install the epoll() reactor.
+ """
+ p = EPollReactor()
+ from twisted.internet.main import installReactor
+ installReactor(p)
+
+
+__all__ = ["EPollReactor", "install"]
+
diff --git a/twisted/internet/error.py b/twisted/internet/error.py
new file mode 100644
index 0000000..51b0ad8
--- /dev/null
+++ b/twisted/internet/error.py
@@ -0,0 +1,448 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Exceptions and errors for use in twisted.internet modules.
+"""
+
+import socket
+
+from twisted.python import deprecate
+from twisted.python.versions import Version
+
+
+
+class BindError(Exception):
+ """An error occurred binding to an interface"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class CannotListenError(BindError):
+ """
+ This gets raised by a call to startListening, when the object cannotstart
+ listening.
+
+ @ivar interface: the interface I tried to listen on
+ @ivar port: the port I tried to listen on
+ @ivar socketError: the exception I got when I tried to listen
+ @type socketError: L{socket.error}
+ """
+ def __init__(self, interface, port, socketError):
+ BindError.__init__(self, interface, port, socketError)
+ self.interface = interface
+ self.port = port
+ self.socketError = socketError
+
+ def __str__(self):
+ iface = self.interface or 'any'
+ return "Couldn't listen on %s:%s: %s." % (iface, self.port,
+ self.socketError)
+
+
+
+class MulticastJoinError(Exception):
+ """
+ An attempt to join a multicast group failed.
+ """
+
+
+
+class MessageLengthError(Exception):
+ """Message is too long to send"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class DNSLookupError(IOError):
+ """DNS lookup failed"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class ConnectInProgressError(Exception):
+ """A connect operation was started and isn't done yet."""
+
+
+# connection errors
+
+class ConnectError(Exception):
+ """An error occurred while connecting"""
+
+ def __init__(self, osError=None, string=""):
+ self.osError = osError
+ Exception.__init__(self, string)
+
+ def __str__(self):
+ s = self.__doc__ or self.__class__.__name__
+ if self.osError:
+ s = '%s: %s' % (s, self.osError)
+ if self.args[0]:
+ s = '%s: %s' % (s, self.args[0])
+ s = '%s.' % s
+ return s
+
+
+
+class ConnectBindError(ConnectError):
+ """Couldn't bind"""
+
+
+
+class UnknownHostError(ConnectError):
+ """Hostname couldn't be looked up"""
+
+
+
+class NoRouteError(ConnectError):
+ """No route to host"""
+
+
+
+class ConnectionRefusedError(ConnectError):
+ """Connection was refused by other side"""
+
+
+
+class TCPTimedOutError(ConnectError):
+ """TCP connection timed out"""
+
+
+
+class BadFileError(ConnectError):
+ """File used for UNIX socket is no good"""
+
+
+
+class ServiceNameUnknownError(ConnectError):
+ """Service name given as port is unknown"""
+
+
+
+class UserError(ConnectError):
+ """User aborted connection"""
+
+
+
+class TimeoutError(UserError):
+ """User timeout caused connection failure"""
+
+
+
+class SSLError(ConnectError):
+ """An SSL error occurred"""
+
+
+
+class VerifyError(Exception):
+ """Could not verify something that was supposed to be signed.
+ """
+
+
+
+class PeerVerifyError(VerifyError):
+ """The peer rejected our verify error.
+ """
+
+
+
+class CertificateError(Exception):
+ """
+ We did not find a certificate where we expected to find one.
+ """
+
+
+
+try:
+ import errno
+ errnoMapping = {
+ errno.ENETUNREACH: NoRouteError,
+ errno.ECONNREFUSED: ConnectionRefusedError,
+ errno.ETIMEDOUT: TCPTimedOutError,
+ }
+ if hasattr(errno, "WSAECONNREFUSED"):
+ errnoMapping[errno.WSAECONNREFUSED] = ConnectionRefusedError
+ errnoMapping[errno.WSAENETUNREACH] = NoRouteError
+except ImportError:
+ errnoMapping = {}
+
+
+
+def getConnectError(e):
+ """Given a socket exception, return connection error."""
+ try:
+ number, string = e
+ except ValueError:
+ return ConnectError(string=e)
+
+ if hasattr(socket, 'gaierror') and isinstance(e, socket.gaierror):
+ # only works in 2.2
+ klass = UnknownHostError
+ else:
+ klass = errnoMapping.get(number, ConnectError)
+ return klass(number, string)
+
+
+
+class ConnectionClosed(Exception):
+ """
+ Connection was closed, whether cleanly or non-cleanly.
+ """
+
+
+
+class ConnectionLost(ConnectionClosed):
+ """Connection to the other side was lost in a non-clean fashion"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class ConnectionAborted(ConnectionLost):
+ """
+ Connection was aborted locally, using
+ L{twisted.internet.interfaces.ITCPTransport.abortConnection}.
+
+ @since: 11.1
+ """
+
+
+
+class ConnectionDone(ConnectionClosed):
+ """Connection was closed cleanly"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class FileDescriptorOverrun(ConnectionLost):
+ """
+ A mis-use of L{IUNIXTransport.sendFileDescriptor} caused the connection to
+ be closed.
+
+ Each file descriptor sent using C{sendFileDescriptor} must be associated
+ with at least one byte sent using L{ITransport.write}. If at any point
+ fewer bytes have been written than file descriptors have been sent, the
+ connection is closed with this exception.
+ """
+
+
+
+class ConnectionFdescWentAway(ConnectionLost):
+ """Uh""" #TODO
+
+
+
+class AlreadyCalled(ValueError):
+ """Tried to cancel an already-called event"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class AlreadyCancelled(ValueError):
+ """Tried to cancel an already-cancelled event"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class PotentialZombieWarning(Warning):
+ """
+ Emitted when L{IReactorProcess.spawnProcess} is called in a way which may
+ result in termination of the created child process not being reported.
+
+ Deprecated in Twisted 10.0.
+ """
+ MESSAGE = (
+ "spawnProcess called, but the SIGCHLD handler is not "
+ "installed. This probably means you have not yet "
+ "called reactor.run, or called "
+ "reactor.run(installSignalHandler=0). You will probably "
+ "never see this process finish, and it may become a "
+ "zombie process.")
+
+deprecate.deprecatedModuleAttribute(
+ Version("Twisted", 10, 0, 0),
+ "There is no longer any potential for zombie process.",
+ __name__,
+ "PotentialZombieWarning")
+
+
+
+class ProcessDone(ConnectionDone):
+ """A process has ended without apparent errors"""
+
+ def __init__(self, status):
+ Exception.__init__(self, "process finished with exit code 0")
+ self.exitCode = 0
+ self.signal = None
+ self.status = status
+
+
+
+class ProcessTerminated(ConnectionLost):
+ """A process has ended with a probable error condition"""
+
+ def __init__(self, exitCode=None, signal=None, status=None):
+ self.exitCode = exitCode
+ self.signal = signal
+ self.status = status
+ s = "process ended"
+ if exitCode is not None: s = s + " with exit code %s" % exitCode
+ if signal is not None: s = s + " by signal %s" % signal
+ Exception.__init__(self, s)
+
+
+
+class ProcessExitedAlready(Exception):
+ """
+ The process has already exited and the operation requested can no longer
+ be performed.
+ """
+
+
+
+class NotConnectingError(RuntimeError):
+ """The Connector was not connecting when it was asked to stop connecting"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class NotListeningError(RuntimeError):
+ """The Port was not listening when it was asked to stop listening"""
+
+ def __str__(self):
+ s = self.__doc__
+ if self.args:
+ s = '%s: %s' % (s, ' '.join(self.args))
+ s = '%s.' % s
+ return s
+
+
+
+class ReactorNotRunning(RuntimeError):
+ """
+ Error raised when trying to stop a reactor which is not running.
+ """
+
+
+class ReactorNotRestartable(RuntimeError):
+ """
+ Error raised when trying to run a reactor which was stopped.
+ """
+
+
+
+class ReactorAlreadyRunning(RuntimeError):
+ """
+ Error raised when trying to start the reactor multiple times.
+ """
+
+
+class ReactorAlreadyInstalledError(AssertionError):
+ """
+ Could not install reactor because one is already installed.
+ """
+
+
+
+class ConnectingCancelledError(Exception):
+ """
+ An C{Exception} that will be raised when an L{IStreamClientEndpoint} is
+ cancelled before it connects.
+
+ @ivar address: The L{IAddress} that is the destination of the
+ cancelled L{IStreamClientEndpoint}.
+ """
+
+ def __init__(self, address):
+ """
+ @param address: The L{IAddress} that is the destination of the
+ L{IStreamClientEndpoint} that was cancelled.
+ """
+ Exception.__init__(self, address)
+ self.address = address
+
+
+
+class UnsupportedAddressFamily(Exception):
+ """
+ An attempt was made to use a socket with an address family (eg I{AF_INET},
+ I{AF_INET6}, etc) which is not supported by the reactor.
+ """
+
+
+
+class UnsupportedSocketType(Exception):
+ """
+ An attempt was made to use a socket of a type (eg I{SOCK_STREAM},
+ I{SOCK_DGRAM}, etc) which is not supported by the reactor.
+ """
+
+
+class AlreadyListened(Exception):
+ """
+ An attempt was made to listen on a file descriptor which can only be
+ listened on once.
+ """
+
+
+__all__ = [
+ 'BindError', 'CannotListenError', 'MulticastJoinError',
+ 'MessageLengthError', 'DNSLookupError', 'ConnectInProgressError',
+ 'ConnectError', 'ConnectBindError', 'UnknownHostError', 'NoRouteError',
+ 'ConnectionRefusedError', 'TCPTimedOutError', 'BadFileError',
+ 'ServiceNameUnknownError', 'UserError', 'TimeoutError', 'SSLError',
+ 'VerifyError', 'PeerVerifyError', 'CertificateError',
+ 'getConnectError', 'ConnectionClosed', 'ConnectionLost',
+ 'ConnectionDone', 'ConnectionFdescWentAway', 'AlreadyCalled',
+ 'AlreadyCancelled', 'PotentialZombieWarning', 'ProcessDone',
+ 'ProcessTerminated', 'ProcessExitedAlready', 'NotConnectingError',
+ 'NotListeningError', 'ReactorNotRunning', 'ReactorAlreadyRunning',
+ 'ReactorAlreadyInstalledError', 'ConnectingCancelledError',
+ 'UnsupportedAddressFamily', 'UnsupportedSocketType']
diff --git a/twisted/internet/fdesc.py b/twisted/internet/fdesc.py
new file mode 100644
index 0000000..f4b0cdf
--- /dev/null
+++ b/twisted/internet/fdesc.py
@@ -0,0 +1,118 @@
+# -*- test-case-name: twisted.test.test_fdesc -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Utility functions for dealing with POSIX file descriptors.
+"""
+
+import os
+import errno
+try:
+ import fcntl
+except ImportError:
+ fcntl = None
+
+# twisted imports
+from twisted.internet.main import CONNECTION_LOST, CONNECTION_DONE
+from twisted.python.runtime import platformType
+
+def setNonBlocking(fd):
+ """
+ Make a file descriptor non-blocking.
+ """
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+ flags = flags | os.O_NONBLOCK
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
+
+
+def setBlocking(fd):
+ """
+ Make a file descriptor blocking.
+ """
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+ flags = flags & ~os.O_NONBLOCK
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
+
+
+if fcntl is None:
+ # fcntl isn't available on Windows. By default, handles aren't
+ # inherited on Windows, so we can do nothing here.
+ _setCloseOnExec = _unsetCloseOnExec = lambda fd: None
+else:
+ def _setCloseOnExec(fd):
+ """
+ Make a file descriptor close-on-exec.
+ """
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ flags = flags | fcntl.FD_CLOEXEC
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags)
+
+
+ def _unsetCloseOnExec(fd):
+ """
+ Make a file descriptor close-on-exec.
+ """
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ flags = flags & ~fcntl.FD_CLOEXEC
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags)
+
+
+def readFromFD(fd, callback):
+ """
+ Read from file descriptor, calling callback with resulting data.
+
+ If successful, call 'callback' with a single argument: the
+ resulting data.
+
+ Returns same thing FileDescriptor.doRead would: CONNECTION_LOST,
+ CONNECTION_DONE, or None.
+
+ @type fd: C{int}
+ @param fd: non-blocking file descriptor to be read from.
+ @param callback: a callable which accepts a single argument. If
+ data is read from the file descriptor it will be called with this
+ data. Handling exceptions from calling the callback is up to the
+ caller.
+
+ Note that if the descriptor is still connected but no data is read,
+ None will be returned but callback will not be called.
+
+ @return: CONNECTION_LOST on error, CONNECTION_DONE when fd is
+ closed, otherwise None.
+ """
+ try:
+ output = os.read(fd, 8192)
+ except (OSError, IOError), ioe:
+ if ioe.args[0] in (errno.EAGAIN, errno.EINTR):
+ return
+ else:
+ return CONNECTION_LOST
+ if not output:
+ return CONNECTION_DONE
+ callback(output)
+
+
+def writeToFD(fd, data):
+ """
+ Write data to file descriptor.
+
+ Returns same thing FileDescriptor.writeSomeData would.
+
+ @type fd: C{int}
+ @param fd: non-blocking file descriptor to be written to.
+ @type data: C{str} or C{buffer}
+ @param data: bytes to write to fd.
+
+ @return: number of bytes written, or CONNECTION_LOST.
+ """
+ try:
+ return os.write(fd, data)
+ except (OSError, IOError), io:
+ if io.errno in (errno.EAGAIN, errno.EINTR):
+ return 0
+ return CONNECTION_LOST
+
+
+__all__ = ["setNonBlocking", "setBlocking", "readFromFD", "writeToFD"]
diff --git a/twisted/internet/gireactor.py b/twisted/internet/gireactor.py
new file mode 100644
index 0000000..bf71bb5
--- /dev/null
+++ b/twisted/internet/gireactor.py
@@ -0,0 +1,139 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides support for Twisted to interact with the glib
+mainloop via GObject Introspection.
+
+In order to use this support, simply do the following::
+
+ from twisted.internet import gireactor
+ gireactor.install()
+
+If you wish to use a GApplication, register it with the reactor::
+
+ from twisted.internet import reactor
+ reactor.registerGApplication(app)
+
+Then use twisted.internet APIs as usual.
+"""
+
+import sys
+from twisted.internet.error import ReactorAlreadyRunning
+from twisted.internet import _glibbase
+from twisted.python import runtime
+
+# We can't immediately prevent imports, because that confuses some buggy code
+# in gi:
+_glibbase.ensureNotImported(
+ ['gobject' 'glib', 'gio', 'gtk'],
+ "Introspected and static glib/gtk bindings must not be mixed; can't "
+ "import gireactor since pygtk2 module is already imported.")
+
+from gi.repository import GLib
+GLib.threads_init()
+
+_glibbase.ensureNotImported([], "",
+ preventImports=['gobject' 'glib', 'gio', 'gtk'])
+
+
+
+class GIReactor(_glibbase.GlibReactorBase):
+ """
+ GObject-introspection event loop reactor.
+
+ @ivar _gapplication: A C{Gio.Application} instance that was registered
+ with C{registerGApplication}.
+ """
+ _POLL_DISCONNECTED = (GLib.IOCondition.HUP | GLib.IOCondition.ERR |
+ GLib.IOCondition.NVAL)
+ _POLL_IN = GLib.IOCondition.IN
+ _POLL_OUT = GLib.IOCondition.OUT
+
+ # glib's iochannel sources won't tell us about any events that we haven't
+ # asked for, even if those events aren't sensible inputs to the poll()
+ # call.
+ INFLAGS = _POLL_IN | _POLL_DISCONNECTED
+ OUTFLAGS = _POLL_OUT | _POLL_DISCONNECTED
+
+ # By default no Application is registered:
+ _gapplication = None
+
+
+ def __init__(self, useGtk=False):
+ _gtk = None
+ if useGtk is True:
+ from gi.repository import Gtk as _gtk
+
+ _glibbase.GlibReactorBase.__init__(self, GLib, _gtk, useGtk=useGtk)
+
+
+ def registerGApplication(self, app):
+ """
+ Register a C{Gio.Application} or C{Gtk.Application}, whose main loop
+ will be used instead of the default one.
+
+ We will C{hold} the application so it doesn't exit on its own. In
+ versions of C{python-gi} 3.2 and later, we exit the event loop using
+ the C{app.quit} method which overrides any holds. Older versions are
+ not supported.
+ """
+ if self._gapplication is not None:
+ raise RuntimeError(
+ "Can't register more than one application instance.")
+ if self._started:
+ raise ReactorAlreadyRunning(
+ "Can't register application after reactor was started.")
+ if not hasattr(app, "quit"):
+ raise RuntimeError("Application registration is not supported in"
+ " versions of PyGObject prior to 3.2.")
+ self._gapplication = app
+ def run():
+ app.hold()
+ app.run(None)
+ self._run = run
+
+ self._crash = app.quit
+
+
+
+class PortableGIReactor(_glibbase.PortableGlibReactorBase):
+ """
+ Portable GObject Introspection event loop reactor.
+ """
+ def __init__(self, useGtk=False):
+ _gtk = None
+ if useGtk is True:
+ from gi.repository import Gtk as _gtk
+
+ _glibbase.PortableGlibReactorBase.__init__(self, GLib, _gtk,
+ useGtk=useGtk)
+
+
+ def registerGApplication(self, app):
+ """
+ Register a C{Gio.Application} or C{Gtk.Application}, whose main loop
+ will be used instead of the default one.
+ """
+ raise NotImplementedError("GApplication is not currently supported on Windows.")
+
+
+
+def install(useGtk=False):
+ """
+ Configure the twisted mainloop to be run inside the glib mainloop.
+
+ @param useGtk: should GTK+ rather than glib event loop be
+ used (this will be slightly slower but does support GUI).
+ """
+ if runtime.platform.getType() == 'posix':
+ reactor = GIReactor(useGtk=useGtk)
+ else:
+ reactor = PortableGIReactor(useGtk=useGtk)
+
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
+__all__ = ['install']
diff --git a/twisted/internet/glib2reactor.py b/twisted/internet/glib2reactor.py
new file mode 100644
index 0000000..5275efd
--- /dev/null
+++ b/twisted/internet/glib2reactor.py
@@ -0,0 +1,44 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides support for Twisted to interact with the glib mainloop.
+This is like gtk2, but slightly faster and does not require a working
+$DISPLAY. However, you cannot run GUIs under this reactor: for that you must
+use the gtk2reactor instead.
+
+In order to use this support, simply do the following::
+
+ from twisted.internet import glib2reactor
+ glib2reactor.install()
+
+Then use twisted.internet APIs as usual. The other methods here are not
+intended to be called directly.
+"""
+
+from twisted.internet import gtk2reactor
+
+
+class Glib2Reactor(gtk2reactor.Gtk2Reactor):
+ """
+ The reactor using the glib mainloop.
+ """
+
+ def __init__(self):
+ """
+ Override init to set the C{useGtk} flag.
+ """
+ gtk2reactor.Gtk2Reactor.__init__(self, useGtk=False)
+
+
+
+def install():
+ """
+ Configure the twisted mainloop to be run inside the glib mainloop.
+ """
+ reactor = Glib2Reactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+
+
+__all__ = ['install']
diff --git a/twisted/internet/gtk2reactor.py b/twisted/internet/gtk2reactor.py
new file mode 100644
index 0000000..65e6693
--- /dev/null
+++ b/twisted/internet/gtk2reactor.py
@@ -0,0 +1,114 @@
+# -*- test-case-name: twisted.internet.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+This module provides support for Twisted to interact with the glib/gtk2
+mainloop.
+
+In order to use this support, simply do the following::
+
+ from twisted.internet import gtk2reactor
+ gtk2reactor.install()
+
+Then use twisted.internet APIs as usual. The other methods here are not
+intended to be called directly.
+"""
+
+# System Imports
+import sys
+
+# Twisted Imports
+from twisted.internet import _glibbase
+from twisted.python import runtime
+
+_glibbase.ensureNotImported(
+ ["gi"],
+ "Introspected and static glib/gtk bindings must not be mixed; can't "
+ "import gtk2reactor since gi module is already imported.",
+ preventImports=["gi"])
+
+try:
+ if not hasattr(sys, 'frozen'):
+ # Don't want to check this for py2exe
+ import pygtk
+ pygtk.require('2.0')
+except (ImportError, AttributeError):
+ pass # maybe we're using pygtk before this hack existed.
+
+import gobject
+if hasattr(gobject, "threads_init"):
+ # recent versions of python-gtk expose this. python-gtk=2.4.1
+ # (wrapping glib-2.4.7) does. python-gtk=2.0.0 (wrapping
+ # glib-2.2.3) does not.
+ gobject.threads_init()
+
+
+
+class Gtk2Reactor(_glibbase.GlibReactorBase):
+ """
+ PyGTK+ 2 event loop reactor.
+ """
+ _POLL_DISCONNECTED = gobject.IO_HUP | gobject.IO_ERR | gobject.IO_NVAL
+ _POLL_IN = gobject.IO_IN
+ _POLL_OUT = gobject.IO_OUT
+
+ # glib's iochannel sources won't tell us about any events that we haven't
+ # asked for, even if those events aren't sensible inputs to the poll()
+ # call.
+ INFLAGS = _POLL_IN | _POLL_DISCONNECTED
+ OUTFLAGS = _POLL_OUT | _POLL_DISCONNECTED
+
+ def __init__(self, useGtk=True):
+ _gtk = None
+ if useGtk is True:
+ import gtk as _gtk
+
+ _glibbase.GlibReactorBase.__init__(self, gobject, _gtk, useGtk=useGtk)
+
+
+
+class PortableGtkReactor(_glibbase.PortableGlibReactorBase):
+ """
+ Reactor that works on Windows.
+
+ Sockets aren't supported by GTK+'s input_add on Win32.
+ """
+ def __init__(self, useGtk=True):
+ _gtk = None
+ if useGtk is True:
+ import gtk as _gtk
+
+ _glibbase.PortableGlibReactorBase.__init__(self, gobject, _gtk,
+ useGtk=useGtk)
+
+
+def install(useGtk=True):
+ """
+ Configure the twisted mainloop to be run inside the gtk mainloop.
+
+ @param useGtk: should glib rather than GTK+ event loop be
+ used (this will be slightly faster but does not support GUI).
+ """
+ reactor = Gtk2Reactor(useGtk)
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
+def portableInstall(useGtk=True):
+ """
+ Configure the twisted mainloop to be run inside the gtk mainloop.
+ """
+ reactor = PortableGtkReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
+if runtime.platform.getType() != 'posix':
+ install = portableInstall
+
+
+__all__ = ['install']
diff --git a/twisted/internet/gtk3reactor.py b/twisted/internet/gtk3reactor.py
new file mode 100644
index 0000000..d3a5864
--- /dev/null
+++ b/twisted/internet/gtk3reactor.py
@@ -0,0 +1,65 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides support for Twisted to interact with the gtk3 mainloop
+via Gobject introspection. This is like gi, but slightly slower and requires a
+working $DISPLAY.
+
+In order to use this support, simply do the following::
+
+ from twisted.internet import gtk3reactor
+ gtk3reactor.install()
+
+If you wish to use a GApplication, register it with the reactor::
+
+ from twisted.internet import reactor
+ reactor.registerGApplication(app)
+
+Then use twisted.internet APIs as usual.
+"""
+
+from twisted.internet import gireactor
+from twisted.python import runtime
+
+
+class Gtk3Reactor(gireactor.GIReactor):
+ """
+ A reactor using the gtk3+ event loop.
+ """
+
+ def __init__(self):
+ """
+ Override init to set the C{useGtk} flag.
+ """
+ gireactor.GIReactor.__init__(self, useGtk=True)
+
+
+
+class PortableGtk3Reactor(gireactor.PortableGIReactor):
+ """
+ Portable GTK+ 3.x reactor.
+ """
+ def __init__(self):
+ """
+ Override init to set the C{useGtk} flag.
+ """
+ gireactor.PortableGIReactor.__init__(self, useGtk=True)
+
+
+
+def install():
+ """
+ Configure the Twisted mainloop to be run inside the gtk3+ mainloop.
+ """
+ if runtime.platform.getType() == 'posix':
+ reactor = Gtk3Reactor()
+ else:
+ reactor = PortableGtk3Reactor()
+
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
+__all__ = ['install']
diff --git a/twisted/internet/gtkreactor.py b/twisted/internet/gtkreactor.py
new file mode 100644
index 0000000..6b1855e
--- /dev/null
+++ b/twisted/internet/gtkreactor.py
@@ -0,0 +1,250 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides support for Twisted to interact with the PyGTK mainloop.
+
+In order to use this support, simply do the following::
+
+ | from twisted.internet import gtkreactor
+ | gtkreactor.install()
+
+Then use twisted.internet APIs as usual. The other methods here are not
+intended to be called directly.
+"""
+
+import sys
+
+# System Imports
+try:
+ import pygtk
+ pygtk.require('1.2')
+except ImportError, AttributeError:
+ pass # maybe we're using pygtk before this hack existed.
+import gtk
+
+from zope.interface import implements
+
+# Twisted Imports
+from twisted.python import log, runtime, deprecate, versions
+from twisted.internet.interfaces import IReactorFDSet
+
+# Sibling Imports
+from twisted.internet import posixbase, selectreactor
+
+
+deprecatedSince = versions.Version("Twisted", 10, 1, 0)
+deprecationMessage = ("All new applications should be written with gtk 2.x, "
+ "which is supported by twisted.internet.gtk2reactor.")
+
+
+class GtkReactor(posixbase.PosixReactorBase):
+ """
+ GTK+ event loop reactor.
+
+ @ivar _reads: A dictionary mapping L{FileDescriptor} instances to gtk INPUT_READ
+ watch handles.
+
+ @ivar _writes: A dictionary mapping L{FileDescriptor} instances to gtk
+ INTPUT_WRITE watch handles.
+
+ @ivar _simtag: A gtk timeout handle for the next L{simulate} call.
+ """
+ implements(IReactorFDSet)
+
+ deprecate.deprecatedModuleAttribute(deprecatedSince, deprecationMessage,
+ __name__, "GtkReactor")
+
+ def __init__(self):
+ """
+ Initialize the file descriptor tracking dictionaries and the base
+ class.
+ """
+ self._simtag = None
+ self._reads = {}
+ self._writes = {}
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def addReader(self, reader):
+ if reader not in self._reads:
+ self._reads[reader] = gtk.input_add(reader, gtk.GDK.INPUT_READ, self.callback)
+
+ def addWriter(self, writer):
+ if writer not in self._writes:
+ self._writes[writer] = gtk.input_add(writer, gtk.GDK.INPUT_WRITE, self.callback)
+
+
+ def getReaders(self):
+ return self._reads.keys()
+
+
+ def getWriters(self):
+ return self._writes.keys()
+
+
+ def removeAll(self):
+ return self._removeAll(self._reads, self._writes)
+
+
+ def removeReader(self, reader):
+ if reader in self._reads:
+ gtk.input_remove(self._reads[reader])
+ del self._reads[reader]
+
+ def removeWriter(self, writer):
+ if writer in self._writes:
+ gtk.input_remove(self._writes[writer])
+ del self._writes[writer]
+
+ doIterationTimer = None
+
+ def doIterationTimeout(self, *args):
+ self.doIterationTimer = None
+ return 0 # auto-remove
+ def doIteration(self, delay):
+ # flush some pending events, return if there was something to do
+ # don't use the usual "while gtk.events_pending(): mainiteration()"
+ # idiom because lots of IO (in particular test_tcp's
+ # ProperlyCloseFilesTestCase) can keep us from ever exiting.
+ log.msg(channel='system', event='iteration', reactor=self)
+ if gtk.events_pending():
+ gtk.mainiteration(0)
+ return
+ # nothing to do, must delay
+ if delay == 0:
+ return # shouldn't delay, so just return
+ self.doIterationTimer = gtk.timeout_add(int(delay * 1000),
+ self.doIterationTimeout)
+ # This will either wake up from IO or from a timeout.
+ gtk.mainiteration(1) # block
+ # note: with the .simulate timer below, delays > 0.1 will always be
+ # woken up by the .simulate timer
+ if self.doIterationTimer:
+ # if woken by IO, need to cancel the timer
+ gtk.timeout_remove(self.doIterationTimer)
+ self.doIterationTimer = None
+
+ def crash(self):
+ posixbase.PosixReactorBase.crash(self)
+ gtk.mainquit()
+
+ def run(self, installSignalHandlers=1):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ gtk.timeout_add(0, self.simulate)
+ gtk.mainloop()
+
+ def _readAndWrite(self, source, condition):
+ # note: gtk-1.2's gtk_input_add presents an API in terms of gdk
+ # constants like INPUT_READ and INPUT_WRITE. Internally, it will add
+ # POLL_HUP and POLL_ERR to the poll() events, but if they happen it
+ # will turn them back into INPUT_READ and INPUT_WRITE. gdkevents.c
+ # maps IN/HUP/ERR to INPUT_READ, and OUT/ERR to INPUT_WRITE. This
+ # means there is no immediate way to detect a disconnected socket.
+
+ # The g_io_add_watch() API is more suited to this task. I don't think
+ # pygtk exposes it, though.
+ why = None
+ didRead = None
+ try:
+ if condition & gtk.GDK.INPUT_READ:
+ why = source.doRead()
+ didRead = source.doRead
+ if not why and condition & gtk.GDK.INPUT_WRITE:
+ # if doRead caused connectionLost, don't call doWrite
+ # if doRead is doWrite, don't call it again.
+ if not source.disconnected and source.doWrite != didRead:
+ why = source.doWrite()
+ didRead = source.doWrite # if failed it was in write
+ except:
+ why = sys.exc_info()[1]
+ log.msg('Error In %s' % source)
+ log.deferr()
+
+ if why:
+ self._disconnectSelectable(source, why, didRead == source.doRead)
+
+ def callback(self, source, condition):
+ log.callWithLogger(source, self._readAndWrite, source, condition)
+ self.simulate() # fire Twisted timers
+ return 1 # 1=don't auto-remove the source
+
+ def simulate(self):
+ """Run simulation loops and reschedule callbacks.
+ """
+ if self._simtag is not None:
+ gtk.timeout_remove(self._simtag)
+ self.runUntilCurrent()
+ timeout = min(self.timeout(), 0.1)
+ if timeout is None:
+ timeout = 0.1
+ # Quoth someone other than me, "grumble", yet I know not why. Try to be
+ # more specific in your complaints, guys. -exarkun
+ self._simtag = gtk.timeout_add(int(timeout * 1010), self.simulate)
+
+
+
+class PortableGtkReactor(selectreactor.SelectReactor):
+ """Reactor that works on Windows.
+
+ input_add is not supported on GTK+ for Win32, apparently.
+
+ @ivar _simtag: A gtk timeout handle for the next L{simulate} call.
+ """
+ _simtag = None
+
+ deprecate.deprecatedModuleAttribute(deprecatedSince, deprecationMessage,
+ __name__, "PortableGtkReactor")
+
+ def crash(self):
+ selectreactor.SelectReactor.crash(self)
+ gtk.mainquit()
+
+ def run(self, installSignalHandlers=1):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ self.simulate()
+ gtk.mainloop()
+
+ def simulate(self):
+ """Run simulation loops and reschedule callbacks.
+ """
+ if self._simtag is not None:
+ gtk.timeout_remove(self._simtag)
+ self.iterate()
+ timeout = min(self.timeout(), 0.1)
+ if timeout is None:
+ timeout = 0.1
+
+ # See comment for identical line in GtkReactor.simulate.
+ self._simtag = gtk.timeout_add((timeout * 1010), self.simulate)
+
+
+
+def install():
+ """Configure the twisted mainloop to be run inside the gtk mainloop.
+ """
+ reactor = GtkReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+deprecate.deprecatedModuleAttribute(deprecatedSince, deprecationMessage,
+ __name__, "install")
+
+
+def portableInstall():
+ """Configure the twisted mainloop to be run inside the gtk mainloop.
+ """
+ reactor = PortableGtkReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+deprecate.deprecatedModuleAttribute(deprecatedSince, deprecationMessage,
+ __name__, "portableInstall")
+
+
+if runtime.platform.getType() != 'posix':
+ install = portableInstall
+
+__all__ = ['install']
diff --git a/twisted/internet/inotify.py b/twisted/internet/inotify.py
new file mode 100644
index 0000000..85305dc
--- /dev/null
+++ b/twisted/internet/inotify.py
@@ -0,0 +1,405 @@
+# -*- test-case-name: twisted.internet.test.test_inotify -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides support for Twisted to linux inotify API.
+
+In order to use this support, simply do the following (and start a reactor
+at some point)::
+
+ from twisted.internet import inotify
+ from twisted.python import filepath
+
+ def notify(ignored, filepath, mask):
+ \"""
+ For historical reasons, an opaque handle is passed as first
+ parameter. This object should never be used.
+
+ @param filepath: FilePath on which the event happened.
+ @param mask: inotify event as hexadecimal masks
+ \"""
+ print "event %s on %s" % (
+ ', '.join(inotify.humanReadableMask(mask)), filepath)
+
+ notifier = inotify.INotify()
+ notifier.startReading()
+ notifier.watch(filepath.FilePath("/some/directory"), callbacks=[notify])
+
+@since: 10.1
+"""
+
+import os
+import struct
+
+from twisted.internet import fdesc
+from twisted.internet.abstract import FileDescriptor
+from twisted.python import log, _inotify
+
+
+# from /usr/src/linux/include/linux/inotify.h
+
+IN_ACCESS = 0x00000001L # File was accessed
+IN_MODIFY = 0x00000002L # File was modified
+IN_ATTRIB = 0x00000004L # Metadata changed
+IN_CLOSE_WRITE = 0x00000008L # Writeable file was closed
+IN_CLOSE_NOWRITE = 0x00000010L # Unwriteable file closed
+IN_OPEN = 0x00000020L # File was opened
+IN_MOVED_FROM = 0x00000040L # File was moved from X
+IN_MOVED_TO = 0x00000080L # File was moved to Y
+IN_CREATE = 0x00000100L # Subfile was created
+IN_DELETE = 0x00000200L # Subfile was delete
+IN_DELETE_SELF = 0x00000400L # Self was deleted
+IN_MOVE_SELF = 0x00000800L # Self was moved
+IN_UNMOUNT = 0x00002000L # Backing fs was unmounted
+IN_Q_OVERFLOW = 0x00004000L # Event queued overflowed
+IN_IGNORED = 0x00008000L # File was ignored
+
+IN_ONLYDIR = 0x01000000 # only watch the path if it is a directory
+IN_DONT_FOLLOW = 0x02000000 # don't follow a sym link
+IN_MASK_ADD = 0x20000000 # add to the mask of an already existing watch
+IN_ISDIR = 0x40000000 # event occurred against dir
+IN_ONESHOT = 0x80000000 # only send event once
+
+IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE # closes
+IN_MOVED = IN_MOVED_FROM | IN_MOVED_TO # moves
+IN_CHANGED = IN_MODIFY | IN_ATTRIB # changes
+
+IN_WATCH_MASK = (IN_MODIFY | IN_ATTRIB |
+ IN_CREATE | IN_DELETE |
+ IN_DELETE_SELF | IN_MOVE_SELF |
+ IN_UNMOUNT | IN_MOVED_FROM | IN_MOVED_TO)
+
+
+_FLAG_TO_HUMAN = [
+ (IN_ACCESS, 'access'),
+ (IN_MODIFY, 'modify'),
+ (IN_ATTRIB, 'attrib'),
+ (IN_CLOSE_WRITE, 'close_write'),
+ (IN_CLOSE_NOWRITE, 'close_nowrite'),
+ (IN_OPEN, 'open'),
+ (IN_MOVED_FROM, 'moved_from'),
+ (IN_MOVED_TO, 'moved_to'),
+ (IN_CREATE, 'create'),
+ (IN_DELETE, 'delete'),
+ (IN_DELETE_SELF, 'delete_self'),
+ (IN_MOVE_SELF, 'move_self'),
+ (IN_UNMOUNT, 'unmount'),
+ (IN_Q_OVERFLOW, 'queue_overflow'),
+ (IN_IGNORED, 'ignored'),
+ (IN_ONLYDIR, 'only_dir'),
+ (IN_DONT_FOLLOW, 'dont_follow'),
+ (IN_MASK_ADD, 'mask_add'),
+ (IN_ISDIR, 'is_dir'),
+ (IN_ONESHOT, 'one_shot')
+]
+
+
+
+def humanReadableMask(mask):
+ """
+ Auxiliary function that converts an hexadecimal mask into a series
+ of human readable flags.
+ """
+ s = []
+ for k, v in _FLAG_TO_HUMAN:
+ if k & mask:
+ s.append(v)
+ return s
+
+
+
+class _Watch(object):
+ """
+ Watch object that represents a Watch point in the filesystem. The
+ user should let INotify to create these objects
+
+ @ivar path: The path over which this watch point is monitoring
+ @ivar mask: The events monitored by this watchpoint
+ @ivar autoAdd: Flag that determines whether this watch point
+ should automatically add created subdirectories
+ @ivar callbacks: C{list} of callback functions that will be called
+ when an event occurs on this watch.
+ """
+ def __init__(self, path, mask=IN_WATCH_MASK, autoAdd=False,
+ callbacks=None):
+ self.path = path
+ self.mask = mask
+ self.autoAdd = autoAdd
+ if callbacks is None:
+ callbacks = []
+ self.callbacks = callbacks
+
+
+ def _notify(self, filepath, events):
+ """
+ Callback function used by L{INotify} to dispatch an event.
+ """
+ for callback in self.callbacks:
+ callback(self, filepath, events)
+
+
+
+class INotify(FileDescriptor, object):
+ """
+ The INotify file descriptor, it basically does everything related
+ to INotify, from reading to notifying watch points.
+
+ @ivar _buffer: a C{str} containing the data read from the inotify fd.
+
+ @ivar _watchpoints: a C{dict} that maps from inotify watch ids to
+ watchpoints objects
+
+ @ivar _watchpaths: a C{dict} that maps from watched paths to the
+ inotify watch ids
+ """
+ _inotify = _inotify
+
+ def __init__(self, reactor=None):
+ FileDescriptor.__init__(self, reactor=reactor)
+
+ # Smart way to allow parametrization of libc so I can override
+ # it and test for the system errors.
+ self._fd = self._inotify.init()
+
+ fdesc.setNonBlocking(self._fd)
+ fdesc._setCloseOnExec(self._fd)
+
+ # The next 2 lines are needed to have self.loseConnection()
+ # to call connectionLost() on us. Since we already created the
+ # fd that talks to inotify we want to be notified even if we
+ # haven't yet started reading.
+ self.connected = 1
+ self._writeDisconnected = True
+
+ self._buffer = ''
+ self._watchpoints = {}
+ self._watchpaths = {}
+
+
+ def _addWatch(self, path, mask, autoAdd, callbacks):
+ """
+ Private helper that abstracts the use of ctypes.
+
+ Calls the internal inotify API and checks for any errors after the
+ call. If there's an error L{INotify._addWatch} can raise an
+ INotifyError. If there's no error it proceeds creating a watchpoint and
+ adding a watchpath for inverse lookup of the file descriptor from the
+ path.
+ """
+ wd = self._inotify.add(self._fd, path.path, mask)
+
+ iwp = _Watch(path, mask, autoAdd, callbacks)
+
+ self._watchpoints[wd] = iwp
+ self._watchpaths[path] = wd
+
+ return wd
+
+
+ def _rmWatch(self, wd):
+ """
+ Private helper that abstracts the use of ctypes.
+
+ Calls the internal inotify API to remove an fd from inotify then
+ removes the corresponding watchpoint from the internal mapping together
+ with the file descriptor from the watchpath.
+ """
+ self._inotify.remove(self._fd, wd)
+ iwp = self._watchpoints.pop(wd)
+ self._watchpaths.pop(iwp.path)
+
+
+ def connectionLost(self, reason):
+ """
+ Release the inotify file descriptor and do the necessary cleanup
+ """
+ FileDescriptor.connectionLost(self, reason)
+ if self._fd >= 0:
+ try:
+ os.close(self._fd)
+ except OSError, e:
+ log.err(e, "Couldn't close INotify file descriptor.")
+
+
+ def fileno(self):
+ """
+ Get the underlying file descriptor from this inotify observer.
+ Required by L{abstract.FileDescriptor} subclasses.
+ """
+ return self._fd
+
+
+ def doRead(self):
+ """
+ Read some data from the observed file descriptors
+ """
+ fdesc.readFromFD(self._fd, self._doRead)
+
+
+ def _doRead(self, in_):
+ """
+ Work on the data just read from the file descriptor.
+ """
+ self._buffer += in_
+ while len(self._buffer) >= 16:
+
+ wd, mask, cookie, size = struct.unpack("=LLLL", self._buffer[0:16])
+
+ if size:
+ name = self._buffer[16:16 + size].rstrip('\0')
+ else:
+ name = None
+
+ self._buffer = self._buffer[16 + size:]
+
+ try:
+ iwp = self._watchpoints[wd]
+ except KeyError:
+ continue
+
+ path = iwp.path
+ if name:
+ path = path.child(name)
+ iwp._notify(path, mask)
+
+ if (iwp.autoAdd and mask & IN_ISDIR and mask & IN_CREATE):
+ # mask & IN_ISDIR already guarantees that the path is a
+ # directory. There's no way you can get here without a
+ # directory anyway, so no point in checking for that again.
+ new_wd = self.watch(
+ path, mask=iwp.mask, autoAdd=True,
+ callbacks=iwp.callbacks
+ )
+ # This is very very very hacky and I'd rather not do this but
+ # we have no other alternative that is less hacky other than
+ # surrender. We use callLater because we don't want to have
+ # too many events waiting while we process these subdirs, we
+ # must always answer events as fast as possible or the overflow
+ # might come.
+ self.reactor.callLater(0,
+ self._addChildren, self._watchpoints[new_wd])
+ if mask & IN_DELETE_SELF:
+ self._rmWatch(wd)
+
+
+ def _addChildren(self, iwp):
+ """
+ This is a very private method, please don't even think about using it.
+
+ Note that this is a fricking hack... it's because we cannot be fast
+ enough in adding a watch to a directory and so we basically end up
+ getting here too late if some operations have already been going on in
+ the subdir, we basically need to catchup. This eventually ends up
+ meaning that we generate double events, your app must be resistant.
+ """
+ try:
+ listdir = iwp.path.children()
+ except OSError:
+ # Somebody or something (like a test) removed this directory while
+ # we were in the callLater(0...) waiting. It doesn't make sense to
+ # process it anymore
+ return
+
+ # note that it's true that listdir will only see the subdirs inside
+ # path at the moment of the call but path is monitored already so if
+ # something is created we will receive an event.
+ for f in listdir:
+ # It's a directory, watch it and then add its children
+ if f.isdir():
+ wd = self.watch(
+ f, mask=iwp.mask, autoAdd=True,
+ callbacks=iwp.callbacks
+ )
+ iwp._notify(f, IN_ISDIR|IN_CREATE)
+ # now f is watched, we can add its children the callLater is to
+ # avoid recursion
+ self.reactor.callLater(0,
+ self._addChildren, self._watchpoints[wd])
+
+ # It's a file and we notify it.
+ if f.isfile():
+ iwp._notify(f, IN_CREATE|IN_CLOSE_WRITE)
+
+
+ def watch(self, path, mask=IN_WATCH_MASK, autoAdd=False,
+ callbacks=None, recursive=False):
+ """
+ Watch the 'mask' events in given path. Can raise C{INotifyError} when
+ there's a problem while adding a directory.
+
+ @param path: The path needing monitoring
+ @type path: L{FilePath}
+
+ @param mask: The events that should be watched
+ @type mask: C{int}
+
+ @param autoAdd: if True automatically add newly created
+ subdirectories
+ @type autoAdd: C{boolean}
+
+ @param callbacks: A list of callbacks that should be called
+ when an event happens in the given path.
+ The callback should accept 3 arguments:
+ (ignored, filepath, mask)
+ @type callbacks: C{list} of callables
+
+ @param recursive: Also add all the subdirectories in this path
+ @type recursive: C{boolean}
+ """
+ if recursive:
+ # This behavior is needed to be compatible with the windows
+ # interface for filesystem changes:
+ # http://msdn.microsoft.com/en-us/library/aa365465(VS.85).aspx
+ # ReadDirectoryChangesW can do bWatchSubtree so it doesn't
+ # make sense to implement this at an higher abstraction
+ # level when other platforms support it already
+ for child in path.walk():
+ if child.isdir():
+ self.watch(child, mask, autoAdd, callbacks,
+ recursive=False)
+ else:
+ wd = self._isWatched(path)
+ if wd:
+ return wd
+
+ mask = mask | IN_DELETE_SELF # need this to remove the watch
+
+ return self._addWatch(path, mask, autoAdd, callbacks)
+
+
+ def ignore(self, path):
+ """
+ Remove the watch point monitoring the given path
+
+ @param path: The path that should be ignored
+ @type path: L{FilePath}
+ """
+ wd = self._isWatched(path)
+ if wd is None:
+ raise KeyError("%r is not watched" % (path,))
+ else:
+ self._rmWatch(wd)
+
+
+ def _isWatched(self, path):
+ """
+ Helper function that checks if the path is already monitored
+ and returns its watchdescriptor if so or None otherwise.
+
+ @param path: The path that should be checked
+ @type path: L{FilePath}
+ """
+ return self._watchpaths.get(path, None)
+
+
+INotifyError = _inotify.INotifyError
+
+
+__all__ = ["INotify", "humanReadableMask", "IN_WATCH_MASK", "IN_ACCESS",
+ "IN_MODIFY", "IN_ATTRIB", "IN_CLOSE_NOWRITE", "IN_CLOSE_WRITE",
+ "IN_OPEN", "IN_MOVED_FROM", "IN_MOVED_TO", "IN_CREATE",
+ "IN_DELETE", "IN_DELETE_SELF", "IN_MOVE_SELF", "IN_UNMOUNT",
+ "IN_Q_OVERFLOW", "IN_IGNORED", "IN_ONLYDIR", "IN_DONT_FOLLOW",
+ "IN_MASK_ADD", "IN_ISDIR", "IN_ONESHOT", "IN_CLOSE",
+ "IN_MOVED", "IN_CHANGED"]
diff --git a/twisted/internet/interfaces.py b/twisted/internet/interfaces.py
new file mode 100644
index 0000000..aac634f
--- /dev/null
+++ b/twisted/internet/interfaces.py
@@ -0,0 +1,2049 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Interface documentation.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+from zope.interface import Interface, Attribute
+from twisted.python.deprecate import deprecatedModuleAttribute
+from twisted.python.versions import Version
+
+
+class IAddress(Interface):
+ """
+ An address, e.g. a TCP C{(host, port)}.
+
+ Default implementations are in L{twisted.internet.address}.
+ """
+
+### Reactor Interfaces
+
+class IConnector(Interface):
+ """
+ Object used to interface between connections and protocols.
+
+ Each L{IConnector} manages one connection.
+ """
+
+ def stopConnecting():
+ """
+ Stop attempting to connect.
+ """
+
+ def disconnect():
+ """
+ Disconnect regardless of the connection state.
+
+ If we are connected, disconnect, if we are trying to connect,
+ stop trying.
+ """
+
+ def connect():
+ """
+ Try to connect to remote address.
+ """
+
+ def getDestination():
+ """
+ Return destination this will try to connect to.
+
+ @return: An object which provides L{IAddress}.
+ """
+
+
+class IResolverSimple(Interface):
+
+ def getHostByName(name, timeout = (1, 3, 11, 45)):
+ """
+ Resolve the domain name C{name} into an IP address.
+
+ @type name: C{str}
+ @type timeout: C{tuple}
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: The callback of the Deferred that is returned will be
+ passed a string that represents the IP address of the specified
+ name, or the errback will be called if the lookup times out. If
+ multiple types of address records are associated with the name,
+ A6 records will be returned in preference to AAAA records, which
+ will be returned in preference to A records. If there are multiple
+ records of the type to be returned, one will be selected at random.
+
+ @raise twisted.internet.defer.TimeoutError: Raised (asynchronously)
+ if the name cannot be resolved within the specified timeout period.
+ """
+
+class IResolver(IResolverSimple):
+ def lookupRecord(name, cls, type, timeout = 10):
+ """
+ Lookup the records associated with the given name
+ that are of the given type and in the given class.
+ """
+
+ def query(query, timeout = 10):
+ """
+ Interpret and dispatch a query object to the appropriate
+ lookup* method.
+ """
+
+ def lookupAddress(name, timeout = 10):
+ """
+ Lookup the A records associated with C{name}.
+ """
+
+ def lookupAddress6(name, timeout = 10):
+ """
+ Lookup all the A6 records associated with C{name}.
+ """
+
+ def lookupIPV6Address(name, timeout = 10):
+ """
+ Lookup all the AAAA records associated with C{name}.
+ """
+
+ def lookupMailExchange(name, timeout = 10):
+ """
+ Lookup the MX records associated with C{name}.
+ """
+
+ def lookupNameservers(name, timeout = 10):
+ """
+ Lookup the the NS records associated with C{name}.
+ """
+
+ def lookupCanonicalName(name, timeout = 10):
+ """
+ Lookup the CNAME records associated with C{name}.
+ """
+
+ def lookupMailBox(name, timeout = 10):
+ """
+ Lookup the MB records associated with C{name}.
+ """
+
+ def lookupMailGroup(name, timeout = 10):
+ """
+ Lookup the MG records associated with C{name}.
+ """
+
+ def lookupMailRename(name, timeout = 10):
+ """
+ Lookup the MR records associated with C{name}.
+ """
+
+ def lookupPointer(name, timeout = 10):
+ """
+ Lookup the PTR records associated with C{name}.
+ """
+
+ def lookupAuthority(name, timeout = 10):
+ """
+ Lookup the SOA records associated with C{name}.
+ """
+
+ def lookupNull(name, timeout = 10):
+ """
+ Lookup the NULL records associated with C{name}.
+ """
+
+ def lookupWellKnownServices(name, timeout = 10):
+ """
+ Lookup the WKS records associated with C{name}.
+ """
+
+ def lookupHostInfo(name, timeout = 10):
+ """
+ Lookup the HINFO records associated with C{name}.
+ """
+
+ def lookupMailboxInfo(name, timeout = 10):
+ """
+ Lookup the MINFO records associated with C{name}.
+ """
+
+ def lookupText(name, timeout = 10):
+ """
+ Lookup the TXT records associated with C{name}.
+ """
+
+ def lookupResponsibility(name, timeout = 10):
+ """
+ Lookup the RP records associated with C{name}.
+ """
+
+ def lookupAFSDatabase(name, timeout = 10):
+ """
+ Lookup the AFSDB records associated with C{name}.
+ """
+
+ def lookupService(name, timeout = 10):
+ """
+ Lookup the SRV records associated with C{name}.
+ """
+
+ def lookupAllRecords(name, timeout = 10):
+ """
+ Lookup all records associated with C{name}.
+ """
+
+ def lookupZone(name, timeout = 10):
+ """
+ Perform a zone transfer for the given C{name}.
+ """
+
+
+
+class IReactorArbitrary(Interface):
+ """
+ This interface is redundant with L{IReactorFDSet} and is deprecated.
+ """
+ deprecatedModuleAttribute(
+ Version("Twisted", 10, 1, 0),
+ "See IReactorFDSet.",
+ __name__,
+ "IReactorArbitrary")
+
+
+ def listenWith(portType, *args, **kw):
+ """
+ Start an instance of the given C{portType} listening.
+
+ @type portType: type which implements L{IListeningPort}
+
+ @param portType: The object given by C{portType(*args, **kw)} will be
+ started listening.
+
+ @return: an object which provides L{IListeningPort}.
+ """
+
+
+ def connectWith(connectorType, *args, **kw):
+ """
+ Start an instance of the given C{connectorType} connecting.
+
+ @type connectorType: type which implements L{IConnector}
+
+ @param connectorType: The object given by C{connectorType(*args, **kw)}
+ will be started connecting.
+
+ @return: An object which provides L{IConnector}.
+ """
+
+# Alias for IReactorArbitrary so that internal Twisted code can continue to
+# provide the interface without emitting a deprecation warning. This can be
+# removed when IReactorArbitrary is removed.
+_IReactorArbitrary = IReactorArbitrary
+
+
+
+class IReactorTCP(Interface):
+
+ def listenTCP(port, factory, backlog=50, interface=''):
+ """
+ Connects a given protocol factory to the given numeric TCP/IP port.
+
+ @param port: a port number on which to listen
+
+ @param factory: a L{twisted.internet.protocol.ServerFactory} instance
+
+ @param backlog: size of the listen queue
+
+ @param interface: The local IPv4 or IPv6 address to which to bind;
+ defaults to '', ie all IPv4 addresses. To bind to all IPv4 and IPv6
+ addresses, you must call this method twice.
+
+ @return: an object that provides L{IListeningPort}.
+
+ @raise CannotListenError: as defined here
+ L{twisted.internet.error.CannotListenError},
+ if it cannot listen on this port (e.g., it
+ cannot bind to the required port number)
+ """
+
+ def connectTCP(host, port, factory, timeout=30, bindAddress=None):
+ """
+ Connect a TCP client.
+
+ @param host: a host name
+
+ @param port: a port number
+
+ @param factory: a L{twisted.internet.protocol.ClientFactory} instance
+
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+
+ @param bindAddress: a (host, port) tuple of local address to bind
+ to, or None.
+
+ @return: An object which provides L{IConnector}. This connector will
+ call various callbacks on the factory when a connection is
+ made, failed, or lost - see
+ L{ClientFactory<twisted.internet.protocol.ClientFactory>}
+ docs for details.
+ """
+
+class IReactorSSL(Interface):
+
+ def connectSSL(host, port, factory, contextFactory, timeout=30, bindAddress=None):
+ """
+ Connect a client Protocol to a remote SSL socket.
+
+ @param host: a host name
+
+ @param port: a port number
+
+ @param factory: a L{twisted.internet.protocol.ClientFactory} instance
+
+ @param contextFactory: a L{twisted.internet.ssl.ClientContextFactory} object.
+
+ @param timeout: number of seconds to wait before assuming the
+ connection has failed.
+
+ @param bindAddress: a (host, port) tuple of local address to bind to,
+ or C{None}.
+
+ @return: An object which provides L{IConnector}.
+ """
+
+ def listenSSL(port, factory, contextFactory, backlog=50, interface=''):
+ """
+ Connects a given protocol factory to the given numeric TCP/IP port.
+ The connection is a SSL one, using contexts created by the context
+ factory.
+
+ @param port: a port number on which to listen
+
+ @param factory: a L{twisted.internet.protocol.ServerFactory} instance
+
+ @param contextFactory: a L{twisted.internet.ssl.ContextFactory} instance
+
+ @param backlog: size of the listen queue
+
+ @param interface: the hostname to bind to, defaults to '' (all)
+ """
+
+
+
+class IReactorUNIX(Interface):
+ """
+ UNIX socket methods.
+ """
+
+ def connectUNIX(address, factory, timeout=30, checkPID=0):
+ """
+ Connect a client protocol to a UNIX socket.
+
+ @param address: a path to a unix socket on the filesystem.
+
+ @param factory: a L{twisted.internet.protocol.ClientFactory} instance
+
+ @param timeout: number of seconds to wait before assuming the connection
+ has failed.
+
+ @param checkPID: if True, check for a pid file to verify that a server
+ is listening. If C{address} is a Linux abstract namespace path,
+ this must be C{False}.
+
+ @return: An object which provides L{IConnector}.
+ """
+
+
+ def listenUNIX(address, factory, backlog=50, mode=0666, wantPID=0):
+ """
+ Listen on a UNIX socket.
+
+ @param address: a path to a unix socket on the filesystem.
+
+ @param factory: a L{twisted.internet.protocol.Factory} instance.
+
+ @param backlog: number of connections to allow in backlog.
+
+ @param mode: The mode (B{not} umask) to set on the unix socket. See
+ platform specific documentation for information about how this
+ might affect connection attempts.
+ @type mode: C{int}
+
+ @param wantPID: if True, create a pidfile for the socket. If C{address}
+ is a Linux abstract namespace path, this must be C{False}.
+
+ @return: An object which provides L{IListeningPort}.
+ """
+
+
+
+class IReactorUNIXDatagram(Interface):
+ """
+ Datagram UNIX socket methods.
+ """
+
+ def connectUNIXDatagram(address, protocol, maxPacketSize=8192, mode=0666, bindAddress=None):
+ """
+ Connect a client protocol to a datagram UNIX socket.
+
+ @param address: a path to a unix socket on the filesystem.
+
+ @param protocol: a L{twisted.internet.protocol.ConnectedDatagramProtocol} instance
+
+ @param maxPacketSize: maximum packet size to accept
+
+ @param mode: The mode (B{not} umask) to set on the unix socket. See
+ platform specific documentation for information about how this
+ might affect connection attempts.
+ @type mode: C{int}
+
+ @param bindAddress: address to bind to
+
+ @return: An object which provides L{IConnector}.
+ """
+
+
+ def listenUNIXDatagram(address, protocol, maxPacketSize=8192, mode=0666):
+ """
+ Listen on a datagram UNIX socket.
+
+ @param address: a path to a unix socket on the filesystem.
+
+ @param protocol: a L{twisted.internet.protocol.DatagramProtocol} instance.
+
+ @param maxPacketSize: maximum packet size to accept
+
+ @param mode: The mode (B{not} umask) to set on the unix socket. See
+ platform specific documentation for information about how this
+ might affect connection attempts.
+ @type mode: C{int}
+
+ @return: An object which provides L{IListeningPort}.
+ """
+
+
+
+class IReactorWin32Events(Interface):
+ """
+ Win32 Event API methods
+
+ @since: 10.2
+ """
+
+ def addEvent(event, fd, action):
+ """
+ Add a new win32 event to the event loop.
+
+ @param event: a Win32 event object created using win32event.CreateEvent()
+
+ @param fd: an instance of L{twisted.internet.abstract.FileDescriptor}
+
+ @param action: a string that is a method name of the fd instance.
+ This method is called in response to the event.
+
+ @return: None
+ """
+
+
+ def removeEvent(event):
+ """
+ Remove an event.
+
+ @param event: a Win32 event object added using L{IReactorWin32Events.addEvent}
+
+ @return: None
+ """
+
+
+
+class IReactorUDP(Interface):
+ """
+ UDP socket methods.
+ """
+
+ def listenUDP(port, protocol, interface='', maxPacketSize=8192):
+ """
+ Connects a given DatagramProtocol to the given numeric UDP port.
+
+ @return: object which provides L{IListeningPort}.
+ """
+
+
+
+class IReactorMulticast(Interface):
+ """
+ UDP socket methods that support multicast.
+
+ IMPORTANT: This is an experimental new interface. It may change
+ without backwards compatability. Suggestions are welcome.
+ """
+
+ def listenMulticast(port, protocol, interface='', maxPacketSize=8192,
+ listenMultiple=False):
+ """
+ Connects a given
+ L{DatagramProtocol<twisted.internet.protocol.DatagramProtocol>} to the
+ given numeric UDP port.
+
+ @param listenMultiple: If set to True, allows multiple sockets to
+ bind to the same address and port number at the same time.
+ @type listenMultiple: C{bool}
+
+ @returns: An object which provides L{IListeningPort}.
+
+ @see: L{twisted.internet.interfaces.IMulticastTransport}
+ @see: U{http://twistedmatrix.com/documents/current/core/howto/udp.html}
+ """
+
+
+
+class IReactorSocket(Interface):
+ """
+ Methods which allow a reactor to use externally created sockets.
+
+ For example, to use C{adoptStreamPort} to implement behavior equivalent
+ to that of L{IReactorTCP.listenTCP}, you might write code like this::
+
+ from socket import SOMAXCONN, AF_INET, SOCK_STREAM, socket
+ portSocket = socket(AF_INET, SOCK_STREAM)
+ # Set FD_CLOEXEC on port, left as an exercise. Then make it into a
+ # non-blocking listening port:
+ portSocket.setblocking(False)
+ portSocket.bind(('192.168.1.2', 12345))
+ portSocket.listen(SOMAXCONN)
+
+ # Now have the reactor use it as a TCP port
+ port = reactor.adoptStreamPort(
+ portSocket.fileno(), AF_INET, YourFactory())
+
+ # portSocket itself is no longer necessary, and needs to be cleaned
+ # up by us.
+ portSocket.close()
+
+ # Whenever the server is no longer needed, stop it as usual.
+ stoppedDeferred = port.stopListening()
+
+ Another potential use is to inherit a listening descriptor from a parent
+ process (for example, systemd or launchd), or to receive one over a UNIX
+ domain socket.
+
+ Some plans for extending this interface exist. See:
+
+ - U{http://twistedmatrix.com/trac/ticket/5570}: established connections
+ - U{http://twistedmatrix.com/trac/ticket/5573}: AF_UNIX ports
+ - U{http://twistedmatrix.com/trac/ticket/5574}: SOCK_DGRAM sockets
+ """
+
+ def adoptStreamPort(fileDescriptor, addressFamily, factory):
+ """
+ Add an existing listening I{SOCK_STREAM} socket to the reactor to
+ monitor for new connections to accept and handle.
+ @param fileDescriptor: A file descriptor associated with a socket which
+ is already bound to an address and marked as listening. The socket
+ must be set non-blocking. Any additional flags (for example,
+ close-on-exec) must also be set by application code. Application
+ code is responsible for closing the file descriptor, which may be
+ done as soon as C{adoptStreamPort} returns.
+ @type fileDescriptor: C{int}
+
+ @param addressFamily: The address family (or I{domain}) of the socket.
+ For example, L{socket.AF_INET6}.
+
+ @param factory: A L{ServerFactory} instance to use to create new
+ protocols to handle connections accepted via this socket.
+
+ @return: An object providing L{IListeningPort}.
+
+ @raise UnsupportedAddressFamily: If the given address family is not
+ supported by this reactor, or not supported with the given socket
+ type.
+
+ @raise UnsupportedSocketType: If the given socket type is not supported
+ by this reactor, or not supported with the given socket type.
+ """
+
+
+
+class IReactorProcess(Interface):
+
+ def spawnProcess(processProtocol, executable, args=(), env={}, path=None,
+ uid=None, gid=None, usePTY=0, childFDs=None):
+ """
+ Spawn a process, with a process protocol.
+
+ @type processProtocol: L{IProcessProtocol} provider
+ @param processProtocol: An object which will be notified of all
+ events related to the created process.
+
+ @param executable: the file name to spawn - the full path should be
+ used.
+
+ @param args: the command line arguments to pass to the process; a
+ sequence of strings. The first string should be the
+ executable's name.
+
+ @type env: a C{dict} mapping C{str} to C{str}, or C{None}.
+ @param env: the environment variables to pass to the child process. The
+ resulting behavior varies between platforms. If
+ - C{env} is not set:
+ - On POSIX: pass an empty environment.
+ - On Windows: pass C{os.environ}.
+ - C{env} is C{None}:
+ - On POSIX: pass C{os.environ}.
+ - On Windows: pass C{os.environ}.
+ - C{env} is a C{dict}:
+ - On POSIX: pass the key/value pairs in C{env} as the
+ complete environment.
+ - On Windows: update C{os.environ} with the key/value
+ pairs in the C{dict} before passing it. As a
+ consequence of U{bug #1640
+ <http://twistedmatrix.com/trac/ticket/1640>}, passing
+ keys with empty values in an effort to unset
+ environment variables I{won't} unset them.
+
+ @param path: the path to run the subprocess in - defaults to the
+ current directory.
+
+ @param uid: user ID to run the subprocess as. (Only available on
+ POSIX systems.)
+
+ @param gid: group ID to run the subprocess as. (Only available on
+ POSIX systems.)
+
+ @param usePTY: if true, run this process in a pseudo-terminal.
+ optionally a tuple of C{(masterfd, slavefd, ttyname)},
+ in which case use those file descriptors.
+ (Not available on all systems.)
+
+ @param childFDs: A dictionary mapping file descriptors in the new child
+ process to an integer or to the string 'r' or 'w'.
+
+ If the value is an integer, it specifies a file
+ descriptor in the parent process which will be mapped
+ to a file descriptor (specified by the key) in the
+ child process. This is useful for things like inetd
+ and shell-like file redirection.
+
+ If it is the string 'r', a pipe will be created and
+ attached to the child at that file descriptor: the
+ child will be able to write to that file descriptor
+ and the parent will receive read notification via the
+ L{IProcessProtocol.childDataReceived} callback. This
+ is useful for the child's stdout and stderr.
+
+ If it is the string 'w', similar setup to the previous
+ case will occur, with the pipe being readable by the
+ child instead of writeable. The parent process can
+ write to that file descriptor using
+ L{IProcessTransport.writeToChild}. This is useful for
+ the child's stdin.
+
+ If childFDs is not passed, the default behaviour is to
+ use a mapping that opens the usual stdin/stdout/stderr
+ pipes.
+
+ @see: L{twisted.internet.protocol.ProcessProtocol}
+
+ @return: An object which provides L{IProcessTransport}.
+
+ @raise OSError: Raised with errno C{EAGAIN} or C{ENOMEM} if there are
+ insufficient system resources to create a new process.
+ """
+
+class IReactorTime(Interface):
+ """
+ Time methods that a Reactor should implement.
+ """
+
+ def seconds():
+ """
+ Get the current time in seconds.
+
+ @return: A number-like object of some sort.
+ """
+
+
+ def callLater(delay, callable, *args, **kw):
+ """
+ Call a function later.
+
+ @type delay: C{float}
+ @param delay: the number of seconds to wait.
+
+ @param callable: the callable object to call later.
+
+ @param args: the arguments to call it with.
+
+ @param kw: the keyword arguments to call it with.
+
+ @return: An object which provides L{IDelayedCall} and can be used to
+ cancel the scheduled call, by calling its C{cancel()} method.
+ It also may be rescheduled by calling its C{delay()} or
+ C{reset()} methods.
+ """
+
+
+ def getDelayedCalls():
+ """
+ Retrieve all currently scheduled delayed calls.
+
+ @return: A tuple of all L{IDelayedCall} providers representing all
+ currently scheduled calls. This is everything that has been
+ returned by C{callLater} but not yet called or canceled.
+ """
+
+
+class IDelayedCall(Interface):
+ """
+ A scheduled call.
+
+ There are probably other useful methods we can add to this interface;
+ suggestions are welcome.
+ """
+
+ def getTime():
+ """
+ Get time when delayed call will happen.
+
+ @return: time in seconds since epoch (a float).
+ """
+
+ def cancel():
+ """
+ Cancel the scheduled call.
+
+ @raises twisted.internet.error.AlreadyCalled: if the call has already
+ happened.
+ @raises twisted.internet.error.AlreadyCancelled: if the call has already
+ been cancelled.
+ """
+
+ def delay(secondsLater):
+ """
+ Delay the scheduled call.
+
+ @param secondsLater: how many seconds from its current firing time to delay
+
+ @raises twisted.internet.error.AlreadyCalled: if the call has already
+ happened.
+ @raises twisted.internet.error.AlreadyCancelled: if the call has already
+ been cancelled.
+ """
+
+ def reset(secondsFromNow):
+ """
+ Reset the scheduled call's timer.
+
+ @param secondsFromNow: how many seconds from now it should fire,
+ equivalent to C{.cancel()} and then doing another
+ C{reactor.callLater(secondsLater, ...)}
+
+ @raises twisted.internet.error.AlreadyCalled: if the call has already
+ happened.
+ @raises twisted.internet.error.AlreadyCancelled: if the call has already
+ been cancelled.
+ """
+
+ def active():
+ """
+ @return: True if this call is still active, False if it has been
+ called or cancelled.
+ """
+
+class IReactorThreads(Interface):
+ """
+ Dispatch methods to be run in threads.
+
+ Internally, this should use a thread pool and dispatch methods to them.
+ """
+
+ def getThreadPool():
+ """
+ Return the threadpool used by L{callInThread}. Create it first if
+ necessary.
+
+ @rtype: L{twisted.python.threadpool.ThreadPool}
+ """
+
+
+ def callInThread(callable, *args, **kwargs):
+ """
+ Run the callable object in a separate thread.
+ """
+
+
+ def callFromThread(callable, *args, **kw):
+ """
+ Cause a function to be executed by the reactor thread.
+
+ Use this method when you want to run a function in the reactor's thread
+ from another thread. Calling L{callFromThread} should wake up the main
+ thread (where L{reactor.run()<reactor.run>} is executing) and run the
+ given callable in that thread.
+
+ If you're writing a multi-threaded application the C{callable} may need
+ to be thread safe, but this method doesn't require it as such. If you
+ want to call a function in the next mainloop iteration, but you're in
+ the same thread, use L{callLater} with a delay of 0.
+ """
+
+
+ def suggestThreadPoolSize(size):
+ """
+ Suggest the size of the internal threadpool used to dispatch functions
+ passed to L{callInThread}.
+ """
+
+
+class IReactorCore(Interface):
+ """
+ Core methods that a Reactor must implement.
+ """
+
+ running = Attribute(
+ "A C{bool} which is C{True} from I{during startup} to "
+ "I{during shutdown} and C{False} the rest of the time.")
+
+
+ def resolve(name, timeout=10):
+ """
+ Return a L{twisted.internet.defer.Deferred} that will resolve a hostname.
+ """
+
+ def run():
+ """
+ Fire 'startup' System Events, move the reactor to the 'running'
+ state, then run the main loop until it is stopped with C{stop()} or
+ C{crash()}.
+ """
+
+ def stop():
+ """
+ Fire 'shutdown' System Events, which will move the reactor to the
+ 'stopped' state and cause C{reactor.run()} to exit.
+ """
+
+ def crash():
+ """
+ Stop the main loop *immediately*, without firing any system events.
+
+ This is named as it is because this is an extremely "rude" thing to do;
+ it is possible to lose data and put your system in an inconsistent
+ state by calling this. However, it is necessary, as sometimes a system
+ can become wedged in a pre-shutdown call.
+ """
+
+ def iterate(delay=0):
+ """
+ Run the main loop's I/O polling function for a period of time.
+
+ This is most useful in applications where the UI is being drawn "as
+ fast as possible", such as games. All pending L{IDelayedCall}s will
+ be called.
+
+ The reactor must have been started (via the C{run()} method) prior to
+ any invocations of this method. It must also be stopped manually
+ after the last call to this method (via the C{stop()} method). This
+ method is not re-entrant: you must not call it recursively; in
+ particular, you must not call it while the reactor is running.
+ """
+
+ def fireSystemEvent(eventType):
+ """
+ Fire a system-wide event.
+
+ System-wide events are things like 'startup', 'shutdown', and
+ 'persist'.
+ """
+
+ def addSystemEventTrigger(phase, eventType, callable, *args, **kw):
+ """
+ Add a function to be called when a system event occurs.
+
+ Each "system event" in Twisted, such as 'startup', 'shutdown', and
+ 'persist', has 3 phases: 'before', 'during', and 'after' (in that
+ order, of course). These events will be fired internally by the
+ Reactor.
+
+ An implementor of this interface must only implement those events
+ described here.
+
+ Callbacks registered for the "before" phase may return either None or a
+ Deferred. The "during" phase will not execute until all of the
+ Deferreds from the "before" phase have fired.
+
+ Once the "during" phase is running, all of the remaining triggers must
+ execute; their return values must be ignored.
+
+ @param phase: a time to call the event -- either the string 'before',
+ 'after', or 'during', describing when to call it
+ relative to the event's execution.
+
+ @param eventType: this is a string describing the type of event.
+
+ @param callable: the object to call before shutdown.
+
+ @param args: the arguments to call it with.
+
+ @param kw: the keyword arguments to call it with.
+
+ @return: an ID that can be used to remove this call with
+ removeSystemEventTrigger.
+ """
+
+ def removeSystemEventTrigger(triggerID):
+ """
+ Removes a trigger added with addSystemEventTrigger.
+
+ @param triggerID: a value returned from addSystemEventTrigger.
+
+ @raise KeyError: If there is no system event trigger for the given
+ C{triggerID}.
+
+ @raise ValueError: If there is no system event trigger for the given
+ C{triggerID}.
+
+ @raise TypeError: If there is no system event trigger for the given
+ C{triggerID}.
+ """
+
+ def callWhenRunning(callable, *args, **kw):
+ """
+ Call a function when the reactor is running.
+
+ If the reactor has not started, the callable will be scheduled
+ to run when it does start. Otherwise, the callable will be invoked
+ immediately.
+
+ @param callable: the callable object to call later.
+
+ @param args: the arguments to call it with.
+
+ @param kw: the keyword arguments to call it with.
+
+ @return: None if the callable was invoked, otherwise a system
+ event id for the scheduled call.
+ """
+
+
+class IReactorPluggableResolver(Interface):
+ """
+ A reactor with a pluggable name resolver interface.
+ """
+
+ def installResolver(resolver):
+ """
+ Set the internal resolver to use to for name lookups.
+
+ @type resolver: An object implementing the L{IResolverSimple} interface
+ @param resolver: The new resolver to use.
+
+ @return: The previously installed resolver.
+ """
+
+
+class IReactorDaemonize(Interface):
+ """
+ A reactor which provides hooks that need to be called before and after
+ daemonization.
+
+ Notes:
+ - This interface SHOULD NOT be called by applications.
+ - This interface should only be implemented by reactors as a workaround
+ (in particular, it's implemented currently only by kqueue()).
+ For details please see the comments on ticket #1918.
+ """
+
+ def beforeDaemonize():
+ """
+ Hook to be called immediately before daemonization. No reactor methods
+ may be called until L{afterDaemonize} is called.
+
+ @return: C{None}.
+ """
+
+
+ def afterDaemonize():
+ """
+ Hook to be called immediately after daemonization. This may only be
+ called after L{beforeDaemonize} had been called previously.
+
+ @return: C{None}.
+ """
+
+
+
+class IReactorFDSet(Interface):
+ """
+ Implement me to be able to use L{IFileDescriptor} type resources.
+
+ This assumes that your main-loop uses UNIX-style numeric file descriptors
+ (or at least similarly opaque IDs returned from a .fileno() method)
+ """
+
+ def addReader(reader):
+ """
+ I add reader to the set of file descriptors to get read events for.
+
+ @param reader: An L{IReadDescriptor} provider that will be checked for
+ read events until it is removed from the reactor with
+ L{removeReader}.
+
+ @return: C{None}.
+ """
+
+ def addWriter(writer):
+ """
+ I add writer to the set of file descriptors to get write events for.
+
+ @param writer: An L{IWriteDescriptor} provider that will be checked for
+ write events until it is removed from the reactor with
+ L{removeWriter}.
+
+ @return: C{None}.
+ """
+
+ def removeReader(reader):
+ """
+ Removes an object previously added with L{addReader}.
+
+ @return: C{None}.
+ """
+
+ def removeWriter(writer):
+ """
+ Removes an object previously added with L{addWriter}.
+
+ @return: C{None}.
+ """
+
+ def removeAll():
+ """
+ Remove all readers and writers.
+
+ Should not remove reactor internal reactor connections (like a waker).
+
+ @return: A list of L{IReadDescriptor} and L{IWriteDescriptor} providers
+ which were removed.
+ """
+
+ def getReaders():
+ """
+ Return the list of file descriptors currently monitored for input
+ events by the reactor.
+
+ @return: the list of file descriptors monitored for input events.
+ @rtype: C{list} of C{IReadDescriptor}
+ """
+
+ def getWriters():
+ """
+ Return the list file descriptors currently monitored for output events
+ by the reactor.
+
+ @return: the list of file descriptors monitored for output events.
+ @rtype: C{list} of C{IWriteDescriptor}
+ """
+
+
+class IListeningPort(Interface):
+ """
+ A listening port.
+ """
+
+ def startListening():
+ """
+ Start listening on this port.
+
+ @raise CannotListenError: If it cannot listen on this port (e.g., it is
+ a TCP port and it cannot bind to the required
+ port number).
+ """
+
+ def stopListening():
+ """
+ Stop listening on this port.
+
+ If it does not complete immediately, will return Deferred that fires
+ upon completion.
+ """
+
+ def getHost():
+ """
+ Get the host that this port is listening for.
+
+ @return: An L{IAddress} provider.
+ """
+
+
+class ILoggingContext(Interface):
+ """
+ Give context information that will be used to log events generated by
+ this item.
+ """
+
+ def logPrefix():
+ """
+ @return: Prefix used during log formatting to indicate context.
+ @rtype: C{str}
+ """
+
+
+
+class IFileDescriptor(ILoggingContext):
+ """
+ An interface representing a UNIX-style numeric file descriptor.
+ """
+
+ def fileno():
+ """
+ @raise: If the descriptor no longer has a valid file descriptor
+ number associated with it.
+
+ @return: The platform-specified representation of a file descriptor
+ number. Or C{-1} if the descriptor no longer has a valid file
+ descriptor number associated with it. As long as the descriptor
+ is valid, calls to this method on a particular instance must
+ return the same value.
+ """
+
+
+ def connectionLost(reason):
+ """
+ Called when the connection was lost.
+
+ This is called when the connection on a selectable object has been
+ lost. It will be called whether the connection was closed explicitly,
+ an exception occurred in an event handler, or the other end of the
+ connection closed it first.
+
+ See also L{IHalfCloseableDescriptor} if your descriptor wants to be
+ notified separately of the two halves of the connection being closed.
+
+ @param reason: A failure instance indicating the reason why the
+ connection was lost. L{error.ConnectionLost} and
+ L{error.ConnectionDone} are of special note, but the
+ failure may be of other classes as well.
+ """
+
+
+
+class IReadDescriptor(IFileDescriptor):
+ """
+ An L{IFileDescriptor} that can read.
+
+ This interface is generally used in conjunction with L{IReactorFDSet}.
+ """
+
+ def doRead():
+ """
+ Some data is available for reading on your descriptor.
+
+ @return: If an error is encountered which causes the descriptor to
+ no longer be valid, a L{Failure} should be returned. Otherwise,
+ C{None}.
+ """
+
+
+class IWriteDescriptor(IFileDescriptor):
+ """
+ An L{IFileDescriptor} that can write.
+
+ This interface is generally used in conjunction with L{IReactorFDSet}.
+ """
+
+ def doWrite():
+ """
+ Some data can be written to your descriptor.
+
+ @return: If an error is encountered which causes the descriptor to
+ no longer be valid, a L{Failure} should be returned. Otherwise,
+ C{None}.
+ """
+
+
+class IReadWriteDescriptor(IReadDescriptor, IWriteDescriptor):
+ """
+ An L{IFileDescriptor} that can both read and write.
+ """
+
+
+class IHalfCloseableDescriptor(Interface):
+ """
+ A descriptor that can be half-closed.
+ """
+
+ def writeConnectionLost(reason):
+ """
+ Indicates write connection was lost.
+ """
+
+ def readConnectionLost(reason):
+ """
+ Indicates read connection was lost.
+ """
+
+
+class ISystemHandle(Interface):
+ """
+ An object that wraps a networking OS-specific handle.
+ """
+
+ def getHandle():
+ """
+ Return a system- and reactor-specific handle.
+
+ This might be a socket.socket() object, or some other type of
+ object, depending on which reactor is being used. Use and
+ manipulate at your own risk.
+
+ This might be used in cases where you want to set specific
+ options not exposed by the Twisted APIs.
+ """
+
+
+class IConsumer(Interface):
+ """
+ A consumer consumes data from a producer.
+ """
+
+ def registerProducer(producer, streaming):
+ """
+ Register to receive data from a producer.
+
+ This sets self to be a consumer for a producer. When this object runs
+ out of data (as when a send(2) call on a socket succeeds in moving the
+ last data from a userspace buffer into a kernelspace buffer), it will
+ ask the producer to resumeProducing().
+
+ For L{IPullProducer} providers, C{resumeProducing} will be called once
+ each time data is required.
+
+ For L{IPushProducer} providers, C{pauseProducing} will be called
+ whenever the write buffer fills up and C{resumeProducing} will only be
+ called when it empties.
+
+ @type producer: L{IProducer} provider
+
+ @type streaming: C{bool}
+ @param streaming: C{True} if C{producer} provides L{IPushProducer},
+ C{False} if C{producer} provides L{IPullProducer}.
+
+ @raise RuntimeError: If a producer is already registered.
+
+ @return: C{None}
+ """
+
+
+ def unregisterProducer():
+ """
+ Stop consuming data from a producer, without disconnecting.
+ """
+
+
+ def write(data):
+ """
+ The producer will write data by calling this method.
+
+ The implementation must be non-blocking and perform whatever
+ buffering is necessary. If the producer has provided enough data
+ for now and it is a L{IPushProducer}, the consumer may call its
+ C{pauseProducing} method.
+ """
+
+
+
+deprecatedModuleAttribute(Version("Twisted", 11, 1, 0),
+ "Please use IConsumer (and IConsumer.unregisterProducer) instead.",
+ __name__, "IFinishableConsumer")
+
+class IFinishableConsumer(IConsumer):
+ """
+ A Consumer for producers that finish. This interface offers no advantages
+ over L{IConsumer} and is deprecated. Please use
+ L{IConsumer.unregisterProducer} instead of L{IFinishableConsumer.finish}.
+ """
+
+ def finish():
+ """
+ The producer has finished producing. This method is deprecated.
+ Please use L{IConsumer.unregisterProducer} instead.
+ """
+
+
+
+class IProducer(Interface):
+ """
+ A producer produces data for a consumer.
+
+ Typically producing is done by calling the write method of an class
+ implementing L{IConsumer}.
+ """
+
+ def stopProducing():
+ """
+ Stop producing data.
+
+ This tells a producer that its consumer has died, so it must stop
+ producing data for good.
+ """
+
+
+class IPushProducer(IProducer):
+ """
+ A push producer, also known as a streaming producer is expected to
+ produce (write to this consumer) data on a continuous basis, unless
+ it has been paused. A paused push producer will resume producing
+ after its resumeProducing() method is called. For a push producer
+ which is not pauseable, these functions may be noops.
+ """
+
+ def pauseProducing():
+ """
+ Pause producing data.
+
+ Tells a producer that it has produced too much data to process for
+ the time being, and to stop until resumeProducing() is called.
+ """
+ def resumeProducing():
+ """
+ Resume producing data.
+
+ This tells a producer to re-add itself to the main loop and produce
+ more data for its consumer.
+ """
+
+class IPullProducer(IProducer):
+ """
+ A pull producer, also known as a non-streaming producer, is
+ expected to produce data each time resumeProducing() is called.
+ """
+
+ def resumeProducing():
+ """
+ Produce data for the consumer a single time.
+
+ This tells a producer to produce data for the consumer once
+ (not repeatedly, once only). Typically this will be done
+ by calling the consumer's write() method a single time with
+ produced data.
+ """
+
+class IProtocol(Interface):
+
+ def dataReceived(data):
+ """
+ Called whenever data is received.
+
+ Use this method to translate to a higher-level message. Usually, some
+ callback will be made upon the receipt of each complete protocol
+ message.
+
+ @param data: a string of indeterminate length. Please keep in mind
+ that you will probably need to buffer some data, as partial
+ (or multiple) protocol messages may be received! I recommend
+ that unit tests for protocols call through to this method with
+ differing chunk sizes, down to one byte at a time.
+ """
+
+ def connectionLost(reason):
+ """
+ Called when the connection is shut down.
+
+ Clear any circular references here, and any external references
+ to this Protocol. The connection has been closed. The C{reason}
+ Failure wraps a L{twisted.internet.error.ConnectionDone} or
+ L{twisted.internet.error.ConnectionLost} instance (or a subclass
+ of one of those).
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+ def makeConnection(transport):
+ """
+ Make a connection to a transport and a server.
+ """
+
+ def connectionMade():
+ """
+ Called when a connection is made.
+
+ This may be considered the initializer of the protocol, because
+ it is called when the connection is completed. For clients,
+ this is called once the connection to the server has been
+ established; for servers, this is called after an accept() call
+ stops blocking and a socket has been received. If you need to
+ send any greeting or initial message, do it here.
+ """
+
+
+class IProcessProtocol(Interface):
+ """
+ Interface for process-related event handlers.
+ """
+
+ def makeConnection(process):
+ """
+ Called when the process has been created.
+
+ @type process: L{IProcessTransport} provider
+ @param process: An object representing the process which has been
+ created and associated with this protocol.
+ """
+
+
+ def childDataReceived(childFD, data):
+ """
+ Called when data arrives from the child process.
+
+ @type childFD: C{int}
+ @param childFD: The file descriptor from which the data was
+ received.
+
+ @type data: C{str}
+ @param data: The data read from the child's file descriptor.
+ """
+
+
+ def childConnectionLost(childFD):
+ """
+ Called when a file descriptor associated with the child process is
+ closed.
+
+ @type childFD: C{int}
+ @param childFD: The file descriptor which was closed.
+ """
+
+
+ def processExited(reason):
+ """
+ Called when the child process exits.
+
+ @type reason: L{twisted.python.failure.Failure}
+ @param reason: A failure giving the reason the child process
+ terminated. The type of exception for this failure is either
+ L{twisted.internet.error.ProcessDone} or
+ L{twisted.internet.error.ProcessTerminated}.
+
+ @since: 8.2
+ """
+
+
+ def processEnded(reason):
+ """
+ Called when the child process exits and all file descriptors associated
+ with it have been closed.
+
+ @type reason: L{twisted.python.failure.Failure}
+ @param reason: A failure giving the reason the child process
+ terminated. The type of exception for this failure is either
+ L{twisted.internet.error.ProcessDone} or
+ L{twisted.internet.error.ProcessTerminated}.
+ """
+
+
+
+class IHalfCloseableProtocol(Interface):
+ """
+ Implemented to indicate they want notification of half-closes.
+
+ TCP supports the notion of half-closing the connection, e.g.
+ closing the write side but still not stopping reading. A protocol
+ that implements this interface will be notified of such events,
+ instead of having connectionLost called.
+ """
+
+ def readConnectionLost():
+ """
+ Notification of the read connection being closed.
+
+ This indicates peer did half-close of write side. It is now
+ the responsibility of the this protocol to call
+ loseConnection(). In addition, the protocol MUST make sure a
+ reference to it still exists (i.e. by doing a callLater with
+ one of its methods, etc.) as the reactor will only have a
+ reference to it if it is writing.
+
+ If the protocol does not do so, it might get garbage collected
+ without the connectionLost method ever being called.
+ """
+
+ def writeConnectionLost():
+ """
+ Notification of the write connection being closed.
+
+ This will never be called for TCP connections as TCP does not
+ support notification of this type of half-close.
+ """
+
+
+
+class IFileDescriptorReceiver(Interface):
+ """
+ Protocols may implement L{IFileDescriptorReceiver} to receive file
+ descriptors sent to them. This is useful in conjunction with
+ L{IUNIXTransport}, which allows file descriptors to be sent between
+ processes on a single host.
+ """
+ def fileDescriptorReceived(descriptor):
+ """
+ Called when a file descriptor is received over the connection.
+
+ @param descriptor: The descriptor which was received.
+ @type descriptor: C{int}
+
+ @return: C{None}
+ """
+
+
+
+class IProtocolFactory(Interface):
+ """
+ Interface for protocol factories.
+ """
+
+ def buildProtocol(addr):
+ """
+ Called when a connection has been established to addr.
+
+ If None is returned, the connection is assumed to have been refused,
+ and the Port will close the connection.
+
+ @type addr: (host, port)
+ @param addr: The address of the newly-established connection
+
+ @return: None if the connection was refused, otherwise an object
+ providing L{IProtocol}.
+ """
+
+ def doStart():
+ """
+ Called every time this is connected to a Port or Connector.
+ """
+
+ def doStop():
+ """
+ Called every time this is unconnected from a Port or Connector.
+ """
+
+
+class ITransport(Interface):
+ """
+ I am a transport for bytes.
+
+ I represent (and wrap) the physical connection and synchronicity
+ of the framework which is talking to the network. I make no
+ representations about whether calls to me will happen immediately
+ or require returning to a control loop, or whether they will happen
+ in the same or another thread. Consider methods of this class
+ (aside from getPeer) to be 'thrown over the wall', to happen at some
+ indeterminate time.
+ """
+
+ def write(data):
+ """
+ Write some data to the physical connection, in sequence, in a
+ non-blocking fashion.
+
+ If possible, make sure that it is all written. No data will
+ ever be lost, although (obviously) the connection may be closed
+ before it all gets through.
+ """
+
+ def writeSequence(data):
+ """
+ Write a list of strings to the physical connection.
+
+ If possible, make sure that all of the data is written to
+ the socket at once, without first copying it all into a
+ single string.
+ """
+
+ def loseConnection():
+ """
+ Close my connection, after writing all pending data.
+
+ Note that if there is a registered producer on a transport it
+ will not be closed until the producer has been unregistered.
+ """
+
+ def getPeer():
+ """
+ Get the remote address of this connection.
+
+ Treat this method with caution. It is the unfortunate result of the
+ CGI and Jabber standards, but should not be considered reliable for
+ the usual host of reasons; port forwarding, proxying, firewalls, IP
+ masquerading, etc.
+
+ @return: An L{IAddress} provider.
+ """
+
+ def getHost():
+ """
+ Similar to getPeer, but returns an address describing this side of the
+ connection.
+
+ @return: An L{IAddress} provider.
+ """
+
+
+class ITCPTransport(ITransport):
+ """
+ A TCP based transport.
+ """
+
+ def loseWriteConnection():
+ """
+ Half-close the write side of a TCP connection.
+
+ If the protocol instance this is attached to provides
+ IHalfCloseableProtocol, it will get notified when the operation is
+ done. When closing write connection, as with loseConnection this will
+ only happen when buffer has emptied and there is no registered
+ producer.
+ """
+
+
+ def abortConnection():
+ """
+ Close the connection abruptly.
+
+ Discards any buffered data, stops any registered producer,
+ and, if possible, notifies the other end of the unclean
+ closure.
+
+ @since: 11.1
+ """
+
+
+ def getTcpNoDelay():
+ """
+ Return if C{TCP_NODELAY} is enabled.
+ """
+
+ def setTcpNoDelay(enabled):
+ """
+ Enable/disable C{TCP_NODELAY}.
+
+ Enabling C{TCP_NODELAY} turns off Nagle's algorithm. Small packets are
+ sent sooner, possibly at the expense of overall throughput.
+ """
+
+ def getTcpKeepAlive():
+ """
+ Return if C{SO_KEEPALIVE} is enabled.
+ """
+
+ def setTcpKeepAlive(enabled):
+ """
+ Enable/disable C{SO_KEEPALIVE}.
+
+ Enabling C{SO_KEEPALIVE} sends packets periodically when the connection
+ is otherwise idle, usually once every two hours. They are intended
+ to allow detection of lost peers in a non-infinite amount of time.
+ """
+
+ def getHost():
+ """
+ Returns L{IPv4Address} or L{IPv6Address}.
+ """
+
+ def getPeer():
+ """
+ Returns L{IPv4Address} or L{IPv6Address}.
+ """
+
+
+
+class IUNIXTransport(ITransport):
+ """
+ Transport for stream-oriented unix domain connections.
+ """
+ def sendFileDescriptor(descriptor):
+ """
+ Send a duplicate of this (file, socket, pipe, etc) descriptor to the
+ other end of this connection.
+
+ The send is non-blocking and will be queued if it cannot be performed
+ immediately. The send will be processed in order with respect to other
+ C{sendFileDescriptor} calls on this transport, but not necessarily with
+ respect to C{write} calls on this transport. The send can only be
+ processed if there are also bytes in the normal connection-oriented send
+ buffer (ie, you must call C{write} at least as many times as you call
+ C{sendFileDescriptor}).
+
+ @param descriptor: An C{int} giving a valid file descriptor in this
+ process. Note that a I{file descriptor} may actually refer to a
+ socket, a pipe, or anything else POSIX tries to treat in the same
+ way as a file.
+
+ @return: C{None}
+ """
+
+
+
+class ITLSTransport(ITCPTransport):
+ """
+ A TCP transport that supports switching to TLS midstream.
+
+ Once TLS mode is started the transport will implement L{ISSLTransport}.
+ """
+
+ def startTLS(contextFactory):
+ """
+ Initiate TLS negotiation.
+
+ @param contextFactory: A context factory (see L{ssl.py<twisted.internet.ssl>})
+ """
+
+class ISSLTransport(ITCPTransport):
+ """
+ A SSL/TLS based transport.
+ """
+
+ def getPeerCertificate():
+ """
+ Return an object with the peer's certificate info.
+ """
+
+
+class IProcessTransport(ITransport):
+ """
+ A process transport.
+ """
+
+ pid = Attribute(
+ "From before L{IProcessProtocol.makeConnection} is called to before "
+ "L{IProcessProtocol.processEnded} is called, C{pid} is an L{int} "
+ "giving the platform process ID of this process. C{pid} is L{None} "
+ "at all other times.")
+
+ def closeStdin():
+ """
+ Close stdin after all data has been written out.
+ """
+
+ def closeStdout():
+ """
+ Close stdout.
+ """
+
+ def closeStderr():
+ """
+ Close stderr.
+ """
+
+ def closeChildFD(descriptor):
+ """
+ Close a file descriptor which is connected to the child process, identified
+ by its FD in the child process.
+ """
+
+ def writeToChild(childFD, data):
+ """
+ Similar to L{ITransport.write} but also allows the file descriptor in
+ the child process which will receive the bytes to be specified.
+
+ @type childFD: C{int}
+ @param childFD: The file descriptor to which to write.
+
+ @type data: C{str}
+ @param data: The bytes to write.
+
+ @return: C{None}
+
+ @raise KeyError: If C{childFD} is not a file descriptor that was mapped
+ in the child when L{IReactorProcess.spawnProcess} was used to create
+ it.
+ """
+
+ def loseConnection():
+ """
+ Close stdin, stderr and stdout.
+ """
+
+ def signalProcess(signalID):
+ """
+ Send a signal to the process.
+
+ @param signalID: can be
+ - one of C{"KILL"}, C{"TERM"}, or C{"INT"}.
+ These will be implemented in a
+ cross-platform manner, and so should be used
+ if possible.
+ - an integer, where it represents a POSIX
+ signal ID.
+
+ @raise twisted.internet.error.ProcessExitedAlready: The process has
+ already exited.
+ """
+
+
+class IServiceCollection(Interface):
+ """
+ An object which provides access to a collection of services.
+ """
+
+ def getServiceNamed(serviceName):
+ """
+ Retrieve the named service from this application.
+
+ Raise a C{KeyError} if there is no such service name.
+ """
+
+ def addService(service):
+ """
+ Add a service to this collection.
+ """
+
+ def removeService(service):
+ """
+ Remove a service from this collection.
+ """
+
+
+class IUDPTransport(Interface):
+ """
+ Transport for UDP DatagramProtocols.
+ """
+
+ def write(packet, addr=None):
+ """
+ Write packet to given address.
+
+ @param addr: a tuple of (ip, port). For connected transports must
+ be the address the transport is connected to, or None.
+ In non-connected mode this is mandatory.
+
+ @raise twisted.internet.error.MessageLengthError: C{packet} was too
+ long.
+ """
+
+ def connect(host, port):
+ """
+ Connect the transport to an address.
+
+ This changes it to connected mode. Datagrams can only be sent to
+ this address, and will only be received from this address. In addition
+ the protocol's connectionRefused method might get called if destination
+ is not receiving datagrams.
+
+ @param host: an IP address, not a domain name ('127.0.0.1', not 'localhost')
+ @param port: port to connect to.
+ """
+
+ def getHost():
+ """
+ Returns L{IPv4Address}.
+ """
+
+ def stopListening():
+ """
+ Stop listening on this port.
+
+ If it does not complete immediately, will return L{Deferred} that fires
+ upon completion.
+ """
+
+
+
+class IUNIXDatagramTransport(Interface):
+ """
+ Transport for UDP PacketProtocols.
+ """
+
+ def write(packet, address):
+ """
+ Write packet to given address.
+ """
+
+ def getHost():
+ """
+ Returns L{UNIXAddress}.
+ """
+
+
+class IUNIXDatagramConnectedTransport(Interface):
+ """
+ Transport for UDP ConnectedPacketProtocols.
+ """
+
+ def write(packet):
+ """
+ Write packet to address we are connected to.
+ """
+
+ def getHost():
+ """
+ Returns L{UNIXAddress}.
+ """
+
+ def getPeer():
+ """
+ Returns L{UNIXAddress}.
+ """
+
+
+class IMulticastTransport(Interface):
+ """
+ Additional functionality for multicast UDP.
+ """
+
+ def getOutgoingInterface():
+ """
+ Return interface of outgoing multicast packets.
+ """
+
+ def setOutgoingInterface(addr):
+ """
+ Set interface for outgoing multicast packets.
+
+ Returns Deferred of success.
+ """
+
+ def getLoopbackMode():
+ """
+ Return if loopback mode is enabled.
+ """
+
+ def setLoopbackMode(mode):
+ """
+ Set if loopback mode is enabled.
+ """
+
+ def getTTL():
+ """
+ Get time to live for multicast packets.
+ """
+
+ def setTTL(ttl):
+ """
+ Set time to live on multicast packets.
+ """
+
+ def joinGroup(addr, interface=""):
+ """
+ Join a multicast group. Returns L{Deferred} of success or failure.
+
+ If an error occurs, the returned L{Deferred} will fail with
+ L{error.MulticastJoinError}.
+ """
+
+ def leaveGroup(addr, interface=""):
+ """
+ Leave multicast group, return L{Deferred} of success.
+ """
+
+
+class IStreamClientEndpoint(Interface):
+ """
+ A stream client endpoint is a place that L{ClientFactory} can connect to.
+ For example, a remote TCP host/port pair would be a TCP client endpoint.
+
+ @since: 10.1
+ """
+
+ def connect(protocolFactory):
+ """
+ Connect the C{protocolFactory} to the location specified by this
+ L{IStreamClientEndpoint} provider.
+
+ @param protocolFactory: A provider of L{IProtocolFactory}
+ @return: A L{Deferred} that results in an L{IProtocol} upon successful
+ connection otherwise a L{ConnectError}
+ """
+
+
+
+class IStreamServerEndpoint(Interface):
+ """
+ A stream server endpoint is a place that a L{Factory} can listen for
+ incoming connections.
+
+ @since: 10.1
+ """
+
+ def listen(protocolFactory):
+ """
+ Listen with C{protocolFactory} at the location specified by this
+ L{IStreamServerEndpoint} provider.
+
+ @param protocolFactory: A provider of L{IProtocolFactory}
+ @return: A L{Deferred} that results in an L{IListeningPort} or an
+ L{CannotListenError}
+ """
+
+
+
+class IStreamServerEndpointStringParser(Interface):
+ """
+ An L{IStreamServerEndpointStringParser} is like an
+ L{IStreamClientEndpointStringParser}, except for L{IStreamServerEndpoint}s
+ instead of clients. It integrates with L{endpoints.serverFromString} in
+ much the same way.
+ """
+
+ prefix = Attribute(
+ """
+ @see: L{IStreamClientEndpointStringParser.prefix}
+ """
+ )
+
+
+ def parseStreamServer(reactor, *args, **kwargs):
+ """
+ Parse a stream server endpoint from a reactor and string-only arguments
+ and keyword arguments.
+
+ @see: L{IStreamClientEndpointStringParser.parseStreamClient}
+
+ @return: a stream server endpoint
+ @rtype: L{IStreamServerEndpoint}
+ """
+
+
+
+class IStreamClientEndpointStringParser(Interface):
+ """
+ An L{IStreamClientEndpointStringParser} is a parser which can convert
+ a set of string C{*args} and C{**kwargs} into an L{IStreamClientEndpoint}
+ provider.
+
+ This interface is really only useful in the context of the plugin system
+ for L{endpoints.clientFromString}. See the document entitled "I{The
+ Twisted Plugin System}" for more details on how to write a plugin.
+
+ If you place an L{IStreamClientEndpointStringParser} plugin in the
+ C{twisted.plugins} package, that plugin's C{parseStreamClient} method will
+ be used to produce endpoints for any description string that begins with
+ the result of that L{IStreamClientEndpointStringParser}'s prefix attribute.
+ """
+
+ prefix = Attribute(
+ """
+ A C{str}, the description prefix to respond to. For example, an
+ L{IStreamClientEndpointStringParser} plugin which had C{"foo"} for its
+ C{prefix} attribute would be called for endpoint descriptions like
+ C{"foo:bar:baz"} or C{"foo:"}.
+ """
+ )
+
+
+ def parseStreamClient(*args, **kwargs):
+ """
+ This method is invoked by L{endpoints.clientFromString}, if the type of
+ endpoint matches the return value from this
+ L{IStreamClientEndpointStringParser}'s C{prefix} method.
+
+ @param args: The string arguments, minus the endpoint type, in the
+ endpoint description string, parsed according to the rules
+ described in L{endpoints.quoteStringArgument}. For example, if the
+ description were C{"my-type:foo:bar:baz=qux"}, C{args} would be
+ C{('foo','bar')}
+
+ @param kwargs: The string arguments from the endpoint description
+ passed as keyword arguments. For example, if the description were
+ C{"my-type:foo:bar:baz=qux"}, C{kwargs} would be
+ C{dict(baz='qux')}.
+
+ @return: a client endpoint
+ @rtype: L{IStreamClientEndpoint}
+ """
diff --git a/twisted/internet/iocpreactor/__init__.py b/twisted/internet/iocpreactor/__init__.py
new file mode 100644
index 0000000..c403e51
--- /dev/null
+++ b/twisted/internet/iocpreactor/__init__.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+I/O Completion Ports reactor
+"""
+
+from twisted.internet.iocpreactor.reactor import install
+
+__all__ = ['install']
diff --git a/twisted/internet/iocpreactor/abstract.py b/twisted/internet/iocpreactor/abstract.py
new file mode 100644
index 0000000..ee3c51f
--- /dev/null
+++ b/twisted/internet/iocpreactor/abstract.py
@@ -0,0 +1,400 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Abstract file handle class
+"""
+
+from twisted.internet import main, error, interfaces
+from twisted.internet.abstract import _ConsumerMixin, _LogOwner
+from twisted.python import failure
+
+from zope.interface import implements
+import errno
+
+from twisted.internet.iocpreactor.const import ERROR_HANDLE_EOF
+from twisted.internet.iocpreactor.const import ERROR_IO_PENDING
+from twisted.internet.iocpreactor import iocpsupport as _iocp
+
+
+
+class FileHandle(_ConsumerMixin, _LogOwner):
+ """
+ File handle that can read and write asynchronously
+ """
+ implements(interfaces.IPushProducer, interfaces.IConsumer,
+ interfaces.ITransport, interfaces.IHalfCloseableDescriptor)
+ # read stuff
+ maxReadBuffers = 16
+ readBufferSize = 4096
+ reading = False
+ dynamicReadBuffers = True # set this to false if subclass doesn't do iovecs
+ _readNextBuffer = 0
+ _readSize = 0 # how much data we have in the read buffer
+ _readScheduled = None
+ _readScheduledInOS = False
+
+
+ def startReading(self):
+ self.reactor.addActiveHandle(self)
+ if not self._readScheduled and not self.reading:
+ self.reading = True
+ self._readScheduled = self.reactor.callLater(0,
+ self._resumeReading)
+
+
+ def stopReading(self):
+ if self._readScheduled:
+ self._readScheduled.cancel()
+ self._readScheduled = None
+ self.reading = False
+
+
+ def _resumeReading(self):
+ self._readScheduled = None
+ if self._dispatchData() and not self._readScheduledInOS:
+ self.doRead()
+
+
+ def _dispatchData(self):
+ """
+ Dispatch previously read data. Return True if self.reading and we don't
+ have any more data
+ """
+ if not self._readSize:
+ return self.reading
+ size = self._readSize
+ full_buffers = size // self.readBufferSize
+ while self._readNextBuffer < full_buffers:
+ self.dataReceived(self._readBuffers[self._readNextBuffer])
+ self._readNextBuffer += 1
+ if not self.reading:
+ return False
+ remainder = size % self.readBufferSize
+ if remainder:
+ self.dataReceived(buffer(self._readBuffers[full_buffers],
+ 0, remainder))
+ if self.dynamicReadBuffers:
+ total_buffer_size = self.readBufferSize * len(self._readBuffers)
+ # we have one buffer too many
+ if size < total_buffer_size - self.readBufferSize:
+ del self._readBuffers[-1]
+ # we filled all buffers, so allocate one more
+ elif (size == total_buffer_size and
+ len(self._readBuffers) < self.maxReadBuffers):
+ self._readBuffers.append(_iocp.AllocateReadBuffer(
+ self.readBufferSize))
+ self._readNextBuffer = 0
+ self._readSize = 0
+ return self.reading
+
+
+ def _cbRead(self, rc, bytes, evt):
+ self._readScheduledInOS = False
+ if self._handleRead(rc, bytes, evt):
+ self.doRead()
+
+
+ def _handleRead(self, rc, bytes, evt):
+ """
+ Returns False if we should stop reading for now
+ """
+ if self.disconnected:
+ return False
+ # graceful disconnection
+ if (not (rc or bytes)) or rc in (errno.WSAEDISCON, ERROR_HANDLE_EOF):
+ self.reactor.removeActiveHandle(self)
+ self.readConnectionLost(failure.Failure(main.CONNECTION_DONE))
+ return False
+ # XXX: not handling WSAEWOULDBLOCK
+ # ("too many outstanding overlapped I/O requests")
+ elif rc:
+ self.connectionLost(failure.Failure(
+ error.ConnectionLost("read error -- %s (%s)" %
+ (errno.errorcode.get(rc, 'unknown'), rc))))
+ return False
+ else:
+ assert self._readSize == 0
+ assert self._readNextBuffer == 0
+ self._readSize = bytes
+ return self._dispatchData()
+
+
+ def doRead(self):
+ evt = _iocp.Event(self._cbRead, self)
+
+ evt.buff = buff = self._readBuffers
+ rc, bytes = self.readFromHandle(buff, evt)
+
+ if not rc or rc == ERROR_IO_PENDING:
+ self._readScheduledInOS = True
+ else:
+ self._handleRead(rc, bytes, evt)
+
+
+ def readFromHandle(self, bufflist, evt):
+ raise NotImplementedError() # TODO: this should default to ReadFile
+
+
+ def dataReceived(self, data):
+ raise NotImplementedError
+
+
+ def readConnectionLost(self, reason):
+ self.connectionLost(reason)
+
+
+ # write stuff
+ dataBuffer = ''
+ offset = 0
+ writing = False
+ _writeScheduled = None
+ _writeDisconnecting = False
+ _writeDisconnected = False
+ writeBufferSize = 2**2**2**2
+
+
+ def loseWriteConnection(self):
+ self._writeDisconnecting = True
+ self.startWriting()
+
+
+ def _closeWriteConnection(self):
+ # override in subclasses
+ pass
+
+
+ def writeConnectionLost(self, reason):
+ # in current code should never be called
+ self.connectionLost(reason)
+
+
+ def startWriting(self):
+ self.reactor.addActiveHandle(self)
+ self.writing = True
+ if not self._writeScheduled:
+ self._writeScheduled = self.reactor.callLater(0,
+ self._resumeWriting)
+
+
+ def stopWriting(self):
+ if self._writeScheduled:
+ self._writeScheduled.cancel()
+ self._writeScheduled = None
+ self.writing = False
+
+
+ def _resumeWriting(self):
+ self._writeScheduled = None
+ self.doWrite()
+
+
+ def _cbWrite(self, rc, bytes, evt):
+ if self._handleWrite(rc, bytes, evt):
+ self.doWrite()
+
+
+ def _handleWrite(self, rc, bytes, evt):
+ """
+ Returns false if we should stop writing for now
+ """
+ if self.disconnected or self._writeDisconnected:
+ return False
+ # XXX: not handling WSAEWOULDBLOCK
+ # ("too many outstanding overlapped I/O requests")
+ if rc:
+ self.connectionLost(failure.Failure(
+ error.ConnectionLost("write error -- %s (%s)" %
+ (errno.errorcode.get(rc, 'unknown'), rc))))
+ return False
+ else:
+ self.offset += bytes
+ # If there is nothing left to send,
+ if self.offset == len(self.dataBuffer) and not self._tempDataLen:
+ self.dataBuffer = ""
+ self.offset = 0
+ # stop writing
+ self.stopWriting()
+ # If I've got a producer who is supposed to supply me with data
+ if self.producer is not None and ((not self.streamingProducer)
+ or self.producerPaused):
+ # tell them to supply some more.
+ self.producerPaused = True
+ self.producer.resumeProducing()
+ elif self.disconnecting:
+ # But if I was previously asked to let the connection die,
+ # do so.
+ self.connectionLost(failure.Failure(main.CONNECTION_DONE))
+ elif self._writeDisconnecting:
+ # I was previously asked to to half-close the connection.
+ self._writeDisconnected = True
+ self._closeWriteConnection()
+ return False
+ else:
+ return True
+
+
+ def doWrite(self):
+ if len(self.dataBuffer) - self.offset < self.SEND_LIMIT:
+ # If there is currently less than SEND_LIMIT bytes left to send
+ # in the string, extend it with the array data.
+ self.dataBuffer = (buffer(self.dataBuffer, self.offset) +
+ "".join(self._tempDataBuffer))
+ self.offset = 0
+ self._tempDataBuffer = []
+ self._tempDataLen = 0
+
+ evt = _iocp.Event(self._cbWrite, self)
+
+ # Send as much data as you can.
+ if self.offset:
+ evt.buff = buff = buffer(self.dataBuffer, self.offset)
+ else:
+ evt.buff = buff = self.dataBuffer
+ rc, bytes = self.writeToHandle(buff, evt)
+ if rc and rc != ERROR_IO_PENDING:
+ self._handleWrite(rc, bytes, evt)
+
+
+ def writeToHandle(self, buff, evt):
+ raise NotImplementedError() # TODO: this should default to WriteFile
+
+
+ def write(self, data):
+ """Reliably write some data.
+
+ The data is buffered until his file descriptor is ready for writing.
+ """
+ if isinstance(data, unicode): # no, really, I mean it
+ raise TypeError("Data must not be unicode")
+ if not self.connected or self._writeDisconnected:
+ return
+ if data:
+ self._tempDataBuffer.append(data)
+ self._tempDataLen += len(data)
+ if self.producer is not None and self.streamingProducer:
+ if (len(self.dataBuffer) + self._tempDataLen
+ > self.writeBufferSize):
+ self.producerPaused = True
+ self.producer.pauseProducing()
+ self.startWriting()
+
+
+ def writeSequence(self, iovec):
+ for i in iovec:
+ if isinstance(i, unicode): # no, really, I mean it
+ raise TypeError("Data must not be unicode")
+ if not self.connected or not iovec or self._writeDisconnected:
+ return
+ self._tempDataBuffer.extend(iovec)
+ for i in iovec:
+ self._tempDataLen += len(i)
+ if self.producer is not None and self.streamingProducer:
+ if len(self.dataBuffer) + self._tempDataLen > self.writeBufferSize:
+ self.producerPaused = True
+ self.producer.pauseProducing()
+ self.startWriting()
+
+
+ # general stuff
+ connected = False
+ disconnected = False
+ disconnecting = False
+ logstr = "Uninitialized"
+
+ SEND_LIMIT = 128*1024
+
+
+ def __init__(self, reactor = None):
+ if not reactor:
+ from twisted.internet import reactor
+ self.reactor = reactor
+ self._tempDataBuffer = [] # will be added to dataBuffer in doWrite
+ self._tempDataLen = 0
+ self._readBuffers = [_iocp.AllocateReadBuffer(self.readBufferSize)]
+
+
+ def connectionLost(self, reason):
+ """
+ The connection was lost.
+
+ This is called when the connection on a selectable object has been
+ lost. It will be called whether the connection was closed explicitly,
+ an exception occurred in an event handler, or the other end of the
+ connection closed it first.
+
+ Clean up state here, but make sure to call back up to FileDescriptor.
+ """
+
+ self.disconnected = True
+ self.connected = False
+ if self.producer is not None:
+ self.producer.stopProducing()
+ self.producer = None
+ self.stopReading()
+ self.stopWriting()
+ self.reactor.removeActiveHandle(self)
+
+
+ def getFileHandle(self):
+ return -1
+
+
+ def loseConnection(self, _connDone=failure.Failure(main.CONNECTION_DONE)):
+ """
+ Close the connection at the next available opportunity.
+
+ Call this to cause this FileDescriptor to lose its connection. It will
+ first write any data that it has buffered.
+
+ If there is data buffered yet to be written, this method will cause the
+ transport to lose its connection as soon as it's done flushing its
+ write buffer. If you have a producer registered, the connection won't
+ be closed until the producer is finished. Therefore, make sure you
+ unregister your producer when it's finished, or the connection will
+ never close.
+ """
+
+ if self.connected and not self.disconnecting:
+ if self._writeDisconnected:
+ # doWrite won't trigger the connection close anymore
+ self.stopReading()
+ self.stopWriting
+ self.connectionLost(_connDone)
+ else:
+ self.stopReading()
+ self.startWriting()
+ self.disconnecting = 1
+
+
+ # Producer/consumer implementation
+
+ def stopConsuming(self):
+ """
+ Stop consuming data.
+
+ This is called when a producer has lost its connection, to tell the
+ consumer to go lose its connection (and break potential circular
+ references).
+ """
+ self.unregisterProducer()
+ self.loseConnection()
+
+
+ # producer interface implementation
+
+ def resumeProducing(self):
+ assert self.connected and not self.disconnecting
+ self.startReading()
+
+
+ def pauseProducing(self):
+ self.stopReading()
+
+
+ def stopProducing(self):
+ self.loseConnection()
+
+
+__all__ = ['FileHandle']
+
diff --git a/twisted/internet/iocpreactor/build.bat b/twisted/internet/iocpreactor/build.bat
new file mode 100644
index 0000000..25f361b
--- /dev/null
+++ b/twisted/internet/iocpreactor/build.bat
@@ -0,0 +1,4 @@
+del iocpsupport\iocpsupport.c iocpsupport.pyd
+del /f /s /q build
+python setup.py build_ext -i -c mingw32
+
diff --git a/twisted/internet/iocpreactor/const.py b/twisted/internet/iocpreactor/const.py
new file mode 100644
index 0000000..dbeb094
--- /dev/null
+++ b/twisted/internet/iocpreactor/const.py
@@ -0,0 +1,26 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Windows constants for IOCP
+"""
+
+
+# this stuff should really be gotten from Windows headers via pyrex, but it
+# probably is not going to change
+
+ERROR_PORT_UNREACHABLE = 1234
+ERROR_NETWORK_UNREACHABLE = 1231
+ERROR_CONNECTION_REFUSED = 1225
+ERROR_IO_PENDING = 997
+ERROR_OPERATION_ABORTED = 995
+WAIT_TIMEOUT = 258
+ERROR_NETNAME_DELETED = 64
+ERROR_HANDLE_EOF = 38
+
+INFINITE = -1
+
+SO_UPDATE_CONNECT_CONTEXT = 0x7010
+SO_UPDATE_ACCEPT_CONTEXT = 0x700B
+
diff --git a/twisted/internet/iocpreactor/interfaces.py b/twisted/internet/iocpreactor/interfaces.py
new file mode 100644
index 0000000..9e4d3ca
--- /dev/null
+++ b/twisted/internet/iocpreactor/interfaces.py
@@ -0,0 +1,47 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Interfaces for iocpreactor
+"""
+
+
+from zope.interface import Interface
+
+
+
+class IReadHandle(Interface):
+ def readFromHandle(bufflist, evt):
+ """
+ Read into the given buffers from this handle.
+
+ @param buff: the buffers to read into
+ @type buff: list of objects implementing the read/write buffer protocol
+
+ @param evt: an IOCP Event object
+
+ @return: tuple (return code, number of bytes read)
+ """
+
+
+
+class IWriteHandle(Interface):
+ def writeToHandle(buff, evt):
+ """
+ Write the given buffer to this handle.
+
+ @param buff: the buffer to write
+ @type buff: any object implementing the buffer protocol
+
+ @param evt: an IOCP Event object
+
+ @return: tuple (return code, number of bytes written)
+ """
+
+
+
+class IReadWriteHandle(IReadHandle, IWriteHandle):
+ pass
+
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/acceptex.pxi b/twisted/internet/iocpreactor/iocpsupport/acceptex.pxi
new file mode 100644
index 0000000..867736d
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/acceptex.pxi
@@ -0,0 +1,46 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def accept(long listening, long accepting, object buff, object obj):
+ """
+ CAUTION: unlike system AcceptEx(), this function returns 0 on success
+ """
+ cdef unsigned long bytes
+ cdef int rc
+ cdef Py_ssize_t size
+ cdef void *mem_buffer
+ cdef myOVERLAPPED *ov
+
+ PyObject_AsWriteBuffer(buff, &mem_buffer, &size)
+
+ ov = makeOV()
+ if obj is not None:
+ ov.obj = <PyObject *>obj
+
+ rc = lpAcceptEx(listening, accepting, mem_buffer, 0,
+ <DWORD>size / 2, <DWORD>size / 2,
+ &bytes, <OVERLAPPED *>ov)
+ if not rc:
+ rc = WSAGetLastError()
+ if rc != ERROR_IO_PENDING:
+ PyMem_Free(ov)
+ return rc
+
+ # operation is in progress
+ Py_XINCREF(obj)
+ return 0
+
+def get_accept_addrs(long s, object buff):
+ cdef WSAPROTOCOL_INFO wsa_pi
+ cdef int locallen, remotelen
+ cdef Py_ssize_t size
+ cdef void *mem_buffer
+ cdef sockaddr *localaddr, *remoteaddr
+
+ PyObject_AsReadBuffer(buff, &mem_buffer, &size)
+
+ lpGetAcceptExSockaddrs(mem_buffer, 0, <DWORD>size / 2, <DWORD>size / 2,
+ &localaddr, &locallen, &remoteaddr, &remotelen)
+ return remoteaddr.sa_family, _makesockaddr(localaddr, locallen), _makesockaddr(remoteaddr, remotelen)
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/connectex.pxi b/twisted/internet/iocpreactor/iocpsupport/connectex.pxi
new file mode 100644
index 0000000..276638a
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/connectex.pxi
@@ -0,0 +1,47 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def connect(long s, object addr, object obj):
+ """
+ CAUTION: unlike system ConnectEx(), this function returns 0 on success
+ """
+ cdef int family, rc
+ cdef myOVERLAPPED *ov
+ cdef sockaddr_in ipv4_name
+ cdef sockaddr_in6 ipv6_name
+ cdef sockaddr *name
+ cdef int namelen
+
+ if not have_connectex:
+ raise ValueError, 'ConnectEx is not available on this system'
+
+ family = getAddrFamily(s)
+ if family == AF_INET:
+ name = <sockaddr *>&ipv4_name
+ namelen = sizeof(ipv4_name)
+ fillinetaddr(&ipv4_name, addr)
+ elif family == AF_INET6:
+ name = <sockaddr *>&ipv6_name
+ namelen = sizeof(ipv6_name)
+ fillinet6addr(&ipv6_name, addr)
+ else:
+ raise ValueError, 'unsupported address family'
+ name.sa_family = family
+
+ ov = makeOV()
+ if obj is not None:
+ ov.obj = <PyObject *>obj
+
+ rc = lpConnectEx(s, name, namelen, NULL, 0, NULL, <OVERLAPPED *>ov)
+
+ if not rc:
+ rc = WSAGetLastError()
+ if rc != ERROR_IO_PENDING:
+ PyMem_Free(ov)
+ return rc
+
+ # operation is in progress
+ Py_XINCREF(obj)
+ return 0
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/iocpsupport.c b/twisted/internet/iocpreactor/iocpsupport/iocpsupport.c
new file mode 100644
index 0000000..deb4b22
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/iocpsupport.c
@@ -0,0 +1,6376 @@
+/* Generated by Cython 0.15.1 on Tue Mar 27 07:16:06 2012 */
+
+#define PY_SSIZE_T_CLEAN
+#include "Python.h"
+#ifndef Py_PYTHON_H
+ #error Python headers needed to compile C extensions, please install development version of Python.
+#else
+
+#include <stddef.h> /* For offsetof */
+#ifndef offsetof
+#define offsetof(type, member) ( (size_t) & ((type*)0) -> member )
+#endif
+
+#if !defined(WIN32) && !defined(MS_WINDOWS)
+ #ifndef __stdcall
+ #define __stdcall
+ #endif
+ #ifndef __cdecl
+ #define __cdecl
+ #endif
+ #ifndef __fastcall
+ #define __fastcall
+ #endif
+#endif
+
+#ifndef DL_IMPORT
+ #define DL_IMPORT(t) t
+#endif
+#ifndef DL_EXPORT
+ #define DL_EXPORT(t) t
+#endif
+
+#ifndef PY_LONG_LONG
+ #define PY_LONG_LONG LONG_LONG
+#endif
+
+#if PY_VERSION_HEX < 0x02040000
+ #define METH_COEXIST 0
+ #define PyDict_CheckExact(op) (Py_TYPE(op) == &PyDict_Type)
+ #define PyDict_Contains(d,o) PySequence_Contains(d,o)
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ typedef int Py_ssize_t;
+ #define PY_SSIZE_T_MAX INT_MAX
+ #define PY_SSIZE_T_MIN INT_MIN
+ #define PY_FORMAT_SIZE_T ""
+ #define PyInt_FromSsize_t(z) PyInt_FromLong(z)
+ #define PyInt_AsSsize_t(o) __Pyx_PyInt_AsInt(o)
+ #define PyNumber_Index(o) PyNumber_Int(o)
+ #define PyIndex_Check(o) PyNumber_Check(o)
+ #define PyErr_WarnEx(category, message, stacklevel) PyErr_Warn(category, message)
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt)
+ #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
+ #define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
+ #define PyVarObject_HEAD_INIT(type, size) \
+ PyObject_HEAD_INIT(type) size,
+ #define PyType_Modified(t)
+
+ typedef struct {
+ void *buf;
+ PyObject *obj;
+ Py_ssize_t len;
+ Py_ssize_t itemsize;
+ int readonly;
+ int ndim;
+ char *format;
+ Py_ssize_t *shape;
+ Py_ssize_t *strides;
+ Py_ssize_t *suboffsets;
+ void *internal;
+ } Py_buffer;
+
+ #define PyBUF_SIMPLE 0
+ #define PyBUF_WRITABLE 0x0001
+ #define PyBUF_FORMAT 0x0004
+ #define PyBUF_ND 0x0008
+ #define PyBUF_STRIDES (0x0010 | PyBUF_ND)
+ #define PyBUF_C_CONTIGUOUS (0x0020 | PyBUF_STRIDES)
+ #define PyBUF_F_CONTIGUOUS (0x0040 | PyBUF_STRIDES)
+ #define PyBUF_ANY_CONTIGUOUS (0x0080 | PyBUF_STRIDES)
+ #define PyBUF_INDIRECT (0x0100 | PyBUF_STRIDES)
+
+#endif
+
+#if PY_MAJOR_VERSION < 3
+ #define __Pyx_BUILTIN_MODULE_NAME "__builtin__"
+#else
+ #define __Pyx_BUILTIN_MODULE_NAME "builtins"
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define Py_TPFLAGS_CHECKTYPES 0
+ #define Py_TPFLAGS_HAVE_INDEX 0
+#endif
+
+#if (PY_VERSION_HEX < 0x02060000) || (PY_MAJOR_VERSION >= 3)
+ #define Py_TPFLAGS_HAVE_NEWBUFFER 0
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBaseString_Type PyUnicode_Type
+ #define PyStringObject PyUnicodeObject
+ #define PyString_Type PyUnicode_Type
+ #define PyString_Check PyUnicode_Check
+ #define PyString_CheckExact PyUnicode_CheckExact
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PyBytesObject PyStringObject
+ #define PyBytes_Type PyString_Type
+ #define PyBytes_Check PyString_Check
+ #define PyBytes_CheckExact PyString_CheckExact
+ #define PyBytes_FromString PyString_FromString
+ #define PyBytes_FromStringAndSize PyString_FromStringAndSize
+ #define PyBytes_FromFormat PyString_FromFormat
+ #define PyBytes_DecodeEscape PyString_DecodeEscape
+ #define PyBytes_AsString PyString_AsString
+ #define PyBytes_AsStringAndSize PyString_AsStringAndSize
+ #define PyBytes_Size PyString_Size
+ #define PyBytes_AS_STRING PyString_AS_STRING
+ #define PyBytes_GET_SIZE PyString_GET_SIZE
+ #define PyBytes_Repr PyString_Repr
+ #define PyBytes_Concat PyString_Concat
+ #define PyBytes_ConcatAndDel PyString_ConcatAndDel
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PySet_Check(obj) PyObject_TypeCheck(obj, &PySet_Type)
+ #define PyFrozenSet_Check(obj) PyObject_TypeCheck(obj, &PyFrozenSet_Type)
+#endif
+#ifndef PySet_CheckExact
+ #define PySet_CheckExact(obj) (Py_TYPE(obj) == &PySet_Type)
+#endif
+
+#define __Pyx_TypeCheck(obj, type) PyObject_TypeCheck(obj, (PyTypeObject *)type)
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyIntObject PyLongObject
+ #define PyInt_Type PyLong_Type
+ #define PyInt_Check(op) PyLong_Check(op)
+ #define PyInt_CheckExact(op) PyLong_CheckExact(op)
+ #define PyInt_FromString PyLong_FromString
+ #define PyInt_FromUnicode PyLong_FromUnicode
+ #define PyInt_FromLong PyLong_FromLong
+ #define PyInt_FromSize_t PyLong_FromSize_t
+ #define PyInt_FromSsize_t PyLong_FromSsize_t
+ #define PyInt_AsLong PyLong_AsLong
+ #define PyInt_AS_LONG PyLong_AS_LONG
+ #define PyInt_AsSsize_t PyLong_AsSsize_t
+ #define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLongMask
+ #define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBoolObject PyLongObject
+#endif
+
+#if PY_VERSION_HEX < 0x03020000
+ typedef long Py_hash_t;
+ #define __Pyx_PyInt_FromHash_t PyInt_FromLong
+ #define __Pyx_PyInt_AsHash_t PyInt_AsLong
+#else
+ #define __Pyx_PyInt_FromHash_t PyInt_FromSsize_t
+ #define __Pyx_PyInt_AsHash_t PyInt_AsSsize_t
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_TrueDivide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceTrueDivide(x,y)
+#else
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_Divide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceDivide(x,y)
+#endif
+
+#if (PY_MAJOR_VERSION < 3) || (PY_VERSION_HEX >= 0x03010300)
+ #define __Pyx_PySequence_GetSlice(obj, a, b) PySequence_GetSlice(obj, a, b)
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) PySequence_SetSlice(obj, a, b, value)
+ #define __Pyx_PySequence_DelSlice(obj, a, b) PySequence_DelSlice(obj, a, b)
+#else
+ #define __Pyx_PySequence_GetSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), (PyObject*)0) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_GetSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object is unsliceable", (obj)->ob_type->tp_name), (PyObject*)0)))
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_SetSlice(obj, a, b, value)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice assignment", (obj)->ob_type->tp_name), -1)))
+ #define __Pyx_PySequence_DelSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_DelSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice deletion", (obj)->ob_type->tp_name), -1)))
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyMethod_New(func, self, klass) ((self) ? PyMethod_New(func, self) : PyInstanceMethod_New(func))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),((char *)(n)))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),((char *)(n)),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),((char *)(n)))
+#else
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),(n))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),(n),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),(n))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_NAMESTR(n) ((char *)(n))
+ #define __Pyx_DOCSTR(n) ((char *)(n))
+#else
+ #define __Pyx_NAMESTR(n) (n)
+ #define __Pyx_DOCSTR(n) (n)
+#endif
+
+#ifndef __PYX_EXTERN_C
+ #ifdef __cplusplus
+ #define __PYX_EXTERN_C extern "C"
+ #else
+ #define __PYX_EXTERN_C extern
+ #endif
+#endif
+
+#if defined(WIN32) || defined(MS_WINDOWS)
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+#define __PYX_HAVE__iocpsupport
+#define __PYX_HAVE_API__iocpsupport
+#include "io.h"
+#include "errno.h"
+#include "winsock2.h"
+#include "ws2tcpip.h"
+#include "windows.h"
+#include "python.h"
+#include "string.h"
+#include "winsock_pointers.h"
+#ifdef _OPENMP
+#include <omp.h>
+#endif /* _OPENMP */
+
+#ifdef PYREX_WITHOUT_ASSERTIONS
+#define CYTHON_WITHOUT_ASSERTIONS
+#endif
+
+
+/* inline attribute */
+#ifndef CYTHON_INLINE
+ #if defined(__GNUC__)
+ #define CYTHON_INLINE __inline__
+ #elif defined(_MSC_VER)
+ #define CYTHON_INLINE __inline
+ #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+ #define CYTHON_INLINE inline
+ #else
+ #define CYTHON_INLINE
+ #endif
+#endif
+
+/* unused attribute */
+#ifndef CYTHON_UNUSED
+# if defined(__GNUC__)
+# if !(defined(__cplusplus)) || (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+# elif defined(__ICC) || defined(__INTEL_COMPILER)
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+#endif
+
+typedef struct {PyObject **p; char *s; const long n; const char* encoding; const char is_unicode; const char is_str; const char intern; } __Pyx_StringTabEntry; /*proto*/
+
+
+/* Type Conversion Predeclarations */
+
+#define __Pyx_PyBytes_FromUString(s) PyBytes_FromString((char*)s)
+#define __Pyx_PyBytes_AsUString(s) ((unsigned char*) PyBytes_AsString(s))
+
+#define __Pyx_Owned_Py_None(b) (Py_INCREF(Py_None), Py_None)
+#define __Pyx_PyBool_FromLong(b) ((b) ? (Py_INCREF(Py_True), Py_True) : (Py_INCREF(Py_False), Py_False))
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject*);
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x);
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject*);
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t);
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject*);
+
+#define __pyx_PyFloat_AsDouble(x) (PyFloat_CheckExact(x) ? PyFloat_AS_DOUBLE(x) : PyFloat_AsDouble(x))
+
+
+#ifdef __GNUC__
+ /* Test for GCC > 2.95 */
+ #if __GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95))
+ #define likely(x) __builtin_expect(!!(x), 1)
+ #define unlikely(x) __builtin_expect(!!(x), 0)
+ #else /* __GNUC__ > 2 ... */
+ #define likely(x) (x)
+ #define unlikely(x) (x)
+ #endif /* __GNUC__ > 2 ... */
+#else /* __GNUC__ */
+ #define likely(x) (x)
+ #define unlikely(x) (x)
+#endif /* __GNUC__ */
+
+static PyObject *__pyx_m;
+static PyObject *__pyx_b;
+static PyObject *__pyx_empty_tuple;
+static PyObject *__pyx_empty_bytes;
+static int __pyx_lineno;
+static int __pyx_clineno = 0;
+static const char * __pyx_cfilenm= __FILE__;
+static const char *__pyx_filename;
+
+
+static const char *__pyx_f[] = {
+ "iocpsupport.pyx",
+ "acceptex.pxi",
+ "connectex.pxi",
+ "wsarecv.pxi",
+ "wsasend.pxi",
+};
+
+/* "iocpsupport.pyx":6
+ *
+ * # HANDLE and SOCKET are pointer-sized (they are 64 bit wide in 64-bit builds)
+ * ctypedef size_t HANDLE # <<<<<<<<<<<<<<
+ * ctypedef size_t SOCKET
+ * ctypedef unsigned long DWORD
+ */
+typedef size_t __pyx_t_11iocpsupport_HANDLE;
+
+/* "iocpsupport.pyx":7
+ * # HANDLE and SOCKET are pointer-sized (they are 64 bit wide in 64-bit builds)
+ * ctypedef size_t HANDLE
+ * ctypedef size_t SOCKET # <<<<<<<<<<<<<<
+ * ctypedef unsigned long DWORD
+ * # it's really a pointer, but we use it as an integer
+ */
+typedef size_t __pyx_t_11iocpsupport_SOCKET;
+
+/* "iocpsupport.pyx":8
+ * ctypedef size_t HANDLE
+ * ctypedef size_t SOCKET
+ * ctypedef unsigned long DWORD # <<<<<<<<<<<<<<
+ * # it's really a pointer, but we use it as an integer
+ * ctypedef size_t ULONG_PTR
+ */
+typedef unsigned long __pyx_t_11iocpsupport_DWORD;
+
+/* "iocpsupport.pyx":10
+ * ctypedef unsigned long DWORD
+ * # it's really a pointer, but we use it as an integer
+ * ctypedef size_t ULONG_PTR # <<<<<<<<<<<<<<
+ * ctypedef int BOOL
+ *
+ */
+typedef size_t __pyx_t_11iocpsupport_ULONG_PTR;
+
+/* "iocpsupport.pyx":11
+ * # it's really a pointer, but we use it as an integer
+ * ctypedef size_t ULONG_PTR
+ * ctypedef int BOOL # <<<<<<<<<<<<<<
+ *
+ * cdef extern from 'io.h':
+ */
+typedef int __pyx_t_11iocpsupport_BOOL;
+
+/*--- Type declarations ---*/
+struct __pyx_obj_11iocpsupport_CompletionPort;
+struct __pyx_t_11iocpsupport_myOVERLAPPED;
+
+/* "iocpsupport.pyx":124
+ * # BOOL (*lpTransmitFile)(SOCKET s, HANDLE hFile, DWORD size, DWORD buffer_size, OVERLAPPED *ov, TRANSMIT_FILE_BUFFERS *buff, DWORD flags)
+ *
+ * cdef struct myOVERLAPPED: # <<<<<<<<<<<<<<
+ * OVERLAPPED ov
+ * PyObject *obj
+ */
+struct __pyx_t_11iocpsupport_myOVERLAPPED {
+ OVERLAPPED ov;
+ struct PyObject *obj;
+};
+
+/* "iocpsupport.pyx":148
+ * setattr(self, k, v)
+ *
+ * cdef class CompletionPort: # <<<<<<<<<<<<<<
+ * cdef HANDLE port
+ * def __init__(self):
+ */
+struct __pyx_obj_11iocpsupport_CompletionPort {
+ PyObject_HEAD
+ __pyx_t_11iocpsupport_HANDLE port;
+};
+
+
+#ifndef CYTHON_REFNANNY
+ #define CYTHON_REFNANNY 0
+#endif
+
+#if CYTHON_REFNANNY
+ typedef struct {
+ void (*INCREF)(void*, PyObject*, int);
+ void (*DECREF)(void*, PyObject*, int);
+ void (*GOTREF)(void*, PyObject*, int);
+ void (*GIVEREF)(void*, PyObject*, int);
+ void* (*SetupContext)(const char*, int, const char*);
+ void (*FinishContext)(void**);
+ } __Pyx_RefNannyAPIStruct;
+ static __Pyx_RefNannyAPIStruct *__Pyx_RefNanny = NULL;
+ static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname); /*proto*/
+ #define __Pyx_RefNannyDeclarations void *__pyx_refnanny = NULL;
+ #define __Pyx_RefNannySetupContext(name) __pyx_refnanny = __Pyx_RefNanny->SetupContext((name), __LINE__, __FILE__)
+ #define __Pyx_RefNannyFinishContext() __Pyx_RefNanny->FinishContext(&__pyx_refnanny)
+ #define __Pyx_INCREF(r) __Pyx_RefNanny->INCREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_DECREF(r) __Pyx_RefNanny->DECREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GOTREF(r) __Pyx_RefNanny->GOTREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GIVEREF(r) __Pyx_RefNanny->GIVEREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_XINCREF(r) do { if((r) != NULL) {__Pyx_INCREF(r); }} while(0)
+ #define __Pyx_XDECREF(r) do { if((r) != NULL) {__Pyx_DECREF(r); }} while(0)
+ #define __Pyx_XGOTREF(r) do { if((r) != NULL) {__Pyx_GOTREF(r); }} while(0)
+ #define __Pyx_XGIVEREF(r) do { if((r) != NULL) {__Pyx_GIVEREF(r);}} while(0)
+#else
+ #define __Pyx_RefNannyDeclarations
+ #define __Pyx_RefNannySetupContext(name)
+ #define __Pyx_RefNannyFinishContext()
+ #define __Pyx_INCREF(r) Py_INCREF(r)
+ #define __Pyx_DECREF(r) Py_DECREF(r)
+ #define __Pyx_GOTREF(r)
+ #define __Pyx_GIVEREF(r)
+ #define __Pyx_XINCREF(r) Py_XINCREF(r)
+ #define __Pyx_XDECREF(r) Py_XDECREF(r)
+ #define __Pyx_XGOTREF(r)
+ #define __Pyx_XGIVEREF(r)
+#endif /* CYTHON_REFNANNY */
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name); /*proto*/
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb); /*proto*/
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb); /*proto*/
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause); /*proto*/
+
+static void __Pyx_RaiseArgtupleInvalid(const char* func_name, int exact,
+ Py_ssize_t num_min, Py_ssize_t num_max, Py_ssize_t num_found); /*proto*/
+
+static void __Pyx_RaiseDoubleKeywordsError(
+ const char* func_name, PyObject* kw_name); /*proto*/
+
+static int __Pyx_ParseOptionalKeywords(PyObject *kwds, PyObject **argnames[], PyObject *kwds2, PyObject *values[], Py_ssize_t num_pos_args, const char* function_name); /*proto*/
+
+static CYTHON_INLINE void __Pyx_RaiseNeedMoreValuesError(Py_ssize_t index);
+
+static CYTHON_INLINE void __Pyx_RaiseTooManyValuesError(Py_ssize_t expected);
+
+static int __Pyx_IternextUnpackEndCheck(PyObject *retval, Py_ssize_t expected); /*proto*/
+
+static CYTHON_INLINE int __Pyx_CheckKeywordStrings(PyObject *kwdict,
+ const char* function_name, int kw_allowed); /*proto*/
+
+
+static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Generic(PyObject *o, PyObject* j) {
+ PyObject *r;
+ if (!j) return NULL;
+ r = PyObject_GetItem(o, j);
+ Py_DECREF(j);
+ return r;
+}
+
+
+#define __Pyx_GetItemInt_List(o, i, size, to_py_func) (((size) <= sizeof(Py_ssize_t)) ? \
+ __Pyx_GetItemInt_List_Fast(o, i) : \
+ __Pyx_GetItemInt_Generic(o, to_py_func(i)))
+
+static CYTHON_INLINE PyObject *__Pyx_GetItemInt_List_Fast(PyObject *o, Py_ssize_t i) {
+ if (likely(o != Py_None)) {
+ if (likely((0 <= i) & (i < PyList_GET_SIZE(o)))) {
+ PyObject *r = PyList_GET_ITEM(o, i);
+ Py_INCREF(r);
+ return r;
+ }
+ else if ((-PyList_GET_SIZE(o) <= i) & (i < 0)) {
+ PyObject *r = PyList_GET_ITEM(o, PyList_GET_SIZE(o) + i);
+ Py_INCREF(r);
+ return r;
+ }
+ }
+ return __Pyx_GetItemInt_Generic(o, PyInt_FromSsize_t(i));
+}
+
+#define __Pyx_GetItemInt_Tuple(o, i, size, to_py_func) (((size) <= sizeof(Py_ssize_t)) ? \
+ __Pyx_GetItemInt_Tuple_Fast(o, i) : \
+ __Pyx_GetItemInt_Generic(o, to_py_func(i)))
+
+static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Tuple_Fast(PyObject *o, Py_ssize_t i) {
+ if (likely(o != Py_None)) {
+ if (likely((0 <= i) & (i < PyTuple_GET_SIZE(o)))) {
+ PyObject *r = PyTuple_GET_ITEM(o, i);
+ Py_INCREF(r);
+ return r;
+ }
+ else if ((-PyTuple_GET_SIZE(o) <= i) & (i < 0)) {
+ PyObject *r = PyTuple_GET_ITEM(o, PyTuple_GET_SIZE(o) + i);
+ Py_INCREF(r);
+ return r;
+ }
+ }
+ return __Pyx_GetItemInt_Generic(o, PyInt_FromSsize_t(i));
+}
+
+
+#define __Pyx_GetItemInt(o, i, size, to_py_func) (((size) <= sizeof(Py_ssize_t)) ? \
+ __Pyx_GetItemInt_Fast(o, i) : \
+ __Pyx_GetItemInt_Generic(o, to_py_func(i)))
+
+static CYTHON_INLINE PyObject *__Pyx_GetItemInt_Fast(PyObject *o, Py_ssize_t i) {
+ PyObject *r;
+ if (PyList_CheckExact(o) && ((0 <= i) & (i < PyList_GET_SIZE(o)))) {
+ r = PyList_GET_ITEM(o, i);
+ Py_INCREF(r);
+ }
+ else if (PyTuple_CheckExact(o) && ((0 <= i) & (i < PyTuple_GET_SIZE(o)))) {
+ r = PyTuple_GET_ITEM(o, i);
+ Py_INCREF(r);
+ }
+ else if (Py_TYPE(o)->tp_as_sequence && Py_TYPE(o)->tp_as_sequence->sq_item && (likely(i >= 0))) {
+ r = PySequence_GetItem(o, i);
+ }
+ else {
+ r = __Pyx_GetItemInt_Generic(o, PyInt_FromSsize_t(i));
+ }
+ return r;
+}
+
+static PyObject *__Pyx_FindPy2Metaclass(PyObject *bases); /*proto*/
+
+static PyObject *__Pyx_CreateClass(PyObject *bases, PyObject *dict, PyObject *name,
+ PyObject *modname); /*proto*/
+
+#define __pyx_binding_PyCFunctionType_USED 1
+
+typedef struct {
+ PyCFunctionObject func;
+} __pyx_binding_PyCFunctionType_object;
+
+static PyTypeObject __pyx_binding_PyCFunctionType_type;
+static PyTypeObject *__pyx_binding_PyCFunctionType = NULL;
+
+static PyObject *__pyx_binding_PyCFunctionType_NewEx(PyMethodDef *ml, PyObject *self, PyObject *module); /* proto */
+#define __pyx_binding_PyCFunctionType_New(ml, self) __pyx_binding_PyCFunctionType_NewEx(ml, self, NULL)
+
+static int __pyx_binding_PyCFunctionType_init(void); /* proto */
+
+static PyObject *__Pyx_Import(PyObject *name, PyObject *from_list, long level); /*proto*/
+
+#include <string.h>
+
+static CYTHON_INLINE int __Pyx_PyBytes_Equals(PyObject* s1, PyObject* s2, int equals); /*proto*/
+
+static CYTHON_INLINE int __Pyx_PyUnicode_Equals(PyObject* s1, PyObject* s2, int equals); /*proto*/
+
+#if PY_MAJOR_VERSION >= 3
+#define __Pyx_PyString_Equals __Pyx_PyUnicode_Equals
+#else
+#define __Pyx_PyString_Equals __Pyx_PyBytes_Equals
+#endif
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject *);
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject *);
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject *);
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject *);
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject *);
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject *);
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject *);
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject *);
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject *);
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject *);
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject *);
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject *);
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject *);
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject *);
+
+static int __Pyx_check_binary_version(void);
+
+static void __Pyx_AddTraceback(const char *funcname, int __pyx_clineno,
+ int __pyx_lineno, const char *__pyx_filename); /*proto*/
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t); /*proto*/
+
+/* Module declarations from 'iocpsupport' */
+static PyTypeObject *__pyx_ptype_11iocpsupport_CompletionPort = 0;
+static struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_f_11iocpsupport_makeOV(void); /*proto*/
+static void __pyx_f_11iocpsupport_raise_error(int, PyObject *); /*proto*/
+static PyObject *__pyx_f_11iocpsupport__makesockaddr(struct sockaddr *, Py_ssize_t); /*proto*/
+static PyObject *__pyx_f_11iocpsupport_fillinetaddr(struct sockaddr_in *, PyObject *); /*proto*/
+static PyObject *__pyx_f_11iocpsupport_fillinet6addr(struct sockaddr_in6 *, PyObject *); /*proto*/
+static int __pyx_f_11iocpsupport_getAddrFamily(__pyx_t_11iocpsupport_SOCKET); /*proto*/
+#define __Pyx_MODULE_NAME "iocpsupport"
+int __pyx_module_is_main_iocpsupport = 0;
+
+/* Implementation of 'iocpsupport' */
+static PyObject *__pyx_builtin_ValueError;
+static PyObject *__pyx_builtin_MemoryError;
+static PyObject *__pyx_builtin_RuntimeError;
+static char __pyx_k_1[] = "CreateIoCompletionPort";
+static char __pyx_k_2[] = "PostQueuedCompletionStatus";
+static char __pyx_k_3[] = ":";
+static char __pyx_k_5[] = "[";
+static char __pyx_k_6[] = "]";
+static char __pyx_k_7[] = "invalid IP address";
+static char __pyx_k_8[] = "%";
+static char __pyx_k_10[] = "invalid IPv6 address %r";
+static char __pyx_k_11[] = "undefined error occurred during address parsing";
+static char __pyx_k_12[] = "ConnectEx is not available on this system";
+static char __pyx_k_13[] = "unsupported address family";
+static char __pyx_k_14[] = "second argument needs to be a list";
+static char __pyx_k_15[] = "length of address length buffer needs to be sizeof(int)";
+static char __pyx_k_16[] = "Failed to initialize Winsock function vectors";
+static char __pyx_k__s[] = "s";
+static char __pyx_k__key[] = "key";
+static char __pyx_k__obj[] = "obj";
+static char __pyx_k__addr[] = "addr";
+static char __pyx_k__buff[] = "buff";
+static char __pyx_k__recv[] = "recv";
+static char __pyx_k__self[] = "self";
+static char __pyx_k__send[] = "send";
+static char __pyx_k__Event[] = "Event";
+static char __pyx_k__bytes[] = "bytes";
+static char __pyx_k__flags[] = "flags";
+static char __pyx_k__owner[] = "owner";
+static char __pyx_k__split[] = "split";
+static char __pyx_k__accept[] = "accept";
+static char __pyx_k__handle[] = "handle";
+static char __pyx_k__rsplit[] = "rsplit";
+static char __pyx_k__socket[] = "socket";
+static char __pyx_k__connect[] = "connect";
+static char __pyx_k____init__[] = "__init__";
+static char __pyx_k____main__[] = "__main__";
+static char __pyx_k____test__[] = "__test__";
+static char __pyx_k__bufflist[] = "bufflist";
+static char __pyx_k__callback[] = "callback";
+static char __pyx_k__recvfrom[] = "recvfrom";
+static char __pyx_k__accepting[] = "accepting";
+static char __pyx_k__addr_buff[] = "addr_buff";
+static char __pyx_k__listening[] = "listening";
+static char __pyx_k__ValueError[] = "ValueError";
+static char __pyx_k__getsockopt[] = "getsockopt";
+static char __pyx_k__maxAddrLen[] = "maxAddrLen";
+static char __pyx_k__MemoryError[] = "MemoryError";
+static char __pyx_k__iocpsupport[] = "iocpsupport";
+static char __pyx_k__RuntimeError[] = "RuntimeError";
+static char __pyx_k__WindowsError[] = "WindowsError";
+static char __pyx_k__makesockaddr[] = "makesockaddr";
+static char __pyx_k__addr_len_buff[] = "addr_len_buff";
+static char __pyx_k__have_connectex[] = "have_connectex";
+static char __pyx_k__get_accept_addrs[] = "get_accept_addrs";
+static char __pyx_k__AllocateReadBuffer[] = "AllocateReadBuffer";
+static char __pyx_k__WSAAddressToString[] = "WSAAddressToString";
+static PyObject *__pyx_n_s_1;
+static PyObject *__pyx_kp_s_10;
+static PyObject *__pyx_kp_s_11;
+static PyObject *__pyx_kp_s_12;
+static PyObject *__pyx_kp_s_13;
+static PyObject *__pyx_kp_s_15;
+static PyObject *__pyx_kp_s_16;
+static PyObject *__pyx_n_s_2;
+static PyObject *__pyx_kp_s_3;
+static PyObject *__pyx_kp_s_5;
+static PyObject *__pyx_kp_s_6;
+static PyObject *__pyx_kp_s_7;
+static PyObject *__pyx_kp_s_8;
+static PyObject *__pyx_n_s__AllocateReadBuffer;
+static PyObject *__pyx_n_s__Event;
+static PyObject *__pyx_n_s__MemoryError;
+static PyObject *__pyx_n_s__RuntimeError;
+static PyObject *__pyx_n_s__ValueError;
+static PyObject *__pyx_n_s__WSAAddressToString;
+static PyObject *__pyx_n_s__WindowsError;
+static PyObject *__pyx_n_s____init__;
+static PyObject *__pyx_n_s____main__;
+static PyObject *__pyx_n_s____test__;
+static PyObject *__pyx_n_s__accept;
+static PyObject *__pyx_n_s__accepting;
+static PyObject *__pyx_n_s__addr;
+static PyObject *__pyx_n_s__addr_buff;
+static PyObject *__pyx_n_s__addr_len_buff;
+static PyObject *__pyx_n_s__buff;
+static PyObject *__pyx_n_s__bufflist;
+static PyObject *__pyx_n_s__bytes;
+static PyObject *__pyx_n_s__callback;
+static PyObject *__pyx_n_s__connect;
+static PyObject *__pyx_n_s__flags;
+static PyObject *__pyx_n_s__get_accept_addrs;
+static PyObject *__pyx_n_s__getsockopt;
+static PyObject *__pyx_n_s__handle;
+static PyObject *__pyx_n_s__have_connectex;
+static PyObject *__pyx_n_s__iocpsupport;
+static PyObject *__pyx_n_s__key;
+static PyObject *__pyx_n_s__listening;
+static PyObject *__pyx_n_s__makesockaddr;
+static PyObject *__pyx_n_s__maxAddrLen;
+static PyObject *__pyx_n_s__obj;
+static PyObject *__pyx_n_s__owner;
+static PyObject *__pyx_n_s__recv;
+static PyObject *__pyx_n_s__recvfrom;
+static PyObject *__pyx_n_s__rsplit;
+static PyObject *__pyx_n_s__s;
+static PyObject *__pyx_n_s__self;
+static PyObject *__pyx_n_s__send;
+static PyObject *__pyx_n_s__socket;
+static PyObject *__pyx_n_s__split;
+static PyObject *__pyx_int_0;
+static PyObject *__pyx_int_1;
+static PyObject *__pyx_k_tuple_4;
+static PyObject *__pyx_k_tuple_9;
+
+/* "iocpsupport.pyx":128
+ * PyObject *obj
+ *
+ * cdef myOVERLAPPED *makeOV() except NULL: # <<<<<<<<<<<<<<
+ * cdef myOVERLAPPED *res
+ * res = <myOVERLAPPED *>PyMem_Malloc(sizeof(myOVERLAPPED))
+ */
+
+static struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_f_11iocpsupport_makeOV(void) {
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_res;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_r;
+ __Pyx_RefNannyDeclarations
+ void *__pyx_t_1;
+ int __pyx_t_2;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("makeOV");
+
+ /* "iocpsupport.pyx":130
+ * cdef myOVERLAPPED *makeOV() except NULL:
+ * cdef myOVERLAPPED *res
+ * res = <myOVERLAPPED *>PyMem_Malloc(sizeof(myOVERLAPPED)) # <<<<<<<<<<<<<<
+ * if not res:
+ * raise MemoryError
+ */
+ __pyx_t_1 = PyMem_Malloc((sizeof(struct __pyx_t_11iocpsupport_myOVERLAPPED))); if (unlikely(__pyx_t_1 == NULL)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 130; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_res = ((struct __pyx_t_11iocpsupport_myOVERLAPPED *)__pyx_t_1);
+
+ /* "iocpsupport.pyx":131
+ * cdef myOVERLAPPED *res
+ * res = <myOVERLAPPED *>PyMem_Malloc(sizeof(myOVERLAPPED))
+ * if not res: # <<<<<<<<<<<<<<
+ * raise MemoryError
+ * memset(res, 0, sizeof(myOVERLAPPED))
+ */
+ __pyx_t_2 = (!(__pyx_v_res != 0));
+ if (__pyx_t_2) {
+
+ /* "iocpsupport.pyx":132
+ * res = <myOVERLAPPED *>PyMem_Malloc(sizeof(myOVERLAPPED))
+ * if not res:
+ * raise MemoryError # <<<<<<<<<<<<<<
+ * memset(res, 0, sizeof(myOVERLAPPED))
+ * return res
+ */
+ PyErr_NoMemory(); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 132; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L3;
+ }
+ __pyx_L3:;
+
+ /* "iocpsupport.pyx":133
+ * if not res:
+ * raise MemoryError
+ * memset(res, 0, sizeof(myOVERLAPPED)) # <<<<<<<<<<<<<<
+ * return res
+ *
+ */
+ memset(__pyx_v_res, 0, (sizeof(struct __pyx_t_11iocpsupport_myOVERLAPPED)));
+
+ /* "iocpsupport.pyx":134
+ * raise MemoryError
+ * memset(res, 0, sizeof(myOVERLAPPED))
+ * return res # <<<<<<<<<<<<<<
+ *
+ * cdef void raise_error(int err, object message) except *:
+ */
+ __pyx_r = __pyx_v_res;
+ goto __pyx_L0;
+
+ __pyx_r = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_AddTraceback("iocpsupport.makeOV", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":136
+ * return res
+ *
+ * cdef void raise_error(int err, object message) except *: # <<<<<<<<<<<<<<
+ * if not err:
+ * err = GetLastError()
+ */
+
+static void __pyx_f_11iocpsupport_raise_error(int __pyx_v_err, PyObject *__pyx_v_message) {
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("raise_error");
+
+ /* "iocpsupport.pyx":137
+ *
+ * cdef void raise_error(int err, object message) except *:
+ * if not err: # <<<<<<<<<<<<<<
+ * err = GetLastError()
+ * raise WindowsError(message, err)
+ */
+ __pyx_t_1 = (!__pyx_v_err);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":138
+ * cdef void raise_error(int err, object message) except *:
+ * if not err:
+ * err = GetLastError() # <<<<<<<<<<<<<<
+ * raise WindowsError(message, err)
+ *
+ */
+ __pyx_v_err = GetLastError();
+ goto __pyx_L3;
+ }
+ __pyx_L3:;
+
+ /* "iocpsupport.pyx":139
+ * if not err:
+ * err = GetLastError()
+ * raise WindowsError(message, err) # <<<<<<<<<<<<<<
+ *
+ * class Event:
+ */
+ __pyx_t_2 = __Pyx_GetName(__pyx_b, __pyx_n_s__WindowsError); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 139; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyInt_FromLong(__pyx_v_err); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 139; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 139; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ __Pyx_INCREF(__pyx_v_message);
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_v_message);
+ __Pyx_GIVEREF(__pyx_v_message);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ __pyx_t_3 = 0;
+ __pyx_t_3 = PyObject_Call(__pyx_t_2, ((PyObject *)__pyx_t_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 139; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_t_3, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 139; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("iocpsupport.raise_error", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+}
+
+/* "iocpsupport.pyx":142
+ *
+ * class Event:
+ * def __init__(self, callback, owner, **kw): # <<<<<<<<<<<<<<
+ * self.callback = callback
+ * self.owner = owner
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_5Event___init__(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_5Event___init__ = {__Pyx_NAMESTR("__init__"), (PyCFunction)__pyx_pf_11iocpsupport_5Event___init__, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_5Event___init__(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ PyObject *__pyx_v_self = 0;
+ PyObject *__pyx_v_callback = 0;
+ PyObject *__pyx_v_owner = 0;
+ PyObject *__pyx_v_kw = 0;
+ PyObject *__pyx_v_k = NULL;
+ PyObject *__pyx_v_v = NULL;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ Py_ssize_t __pyx_t_3;
+ PyObject *(*__pyx_t_4)(PyObject *);
+ PyObject *__pyx_t_5 = NULL;
+ PyObject *__pyx_t_6 = NULL;
+ PyObject *__pyx_t_7 = NULL;
+ PyObject *(*__pyx_t_8)(PyObject *);
+ int __pyx_t_9;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__self,&__pyx_n_s__callback,&__pyx_n_s__owner,0};
+ __Pyx_RefNannySetupContext("__init__");
+ __pyx_self = __pyx_self;
+ __pyx_v_kw = PyDict_New(); if (unlikely(!__pyx_v_kw)) return NULL;
+ __Pyx_GOTREF(__pyx_v_kw);
+ {
+ PyObject* values[3] = {0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__self);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__callback);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("__init__", 1, 3, 3, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__owner);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("__init__", 1, 3, 3, 2); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, __pyx_v_kw, values, PyTuple_GET_SIZE(__pyx_args), "__init__") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ }
+ __pyx_v_self = values[0];
+ __pyx_v_callback = values[1];
+ __pyx_v_owner = values[2];
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("__init__", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_DECREF(__pyx_v_kw); __pyx_v_kw = 0;
+ __Pyx_AddTraceback("iocpsupport.Event.__init__", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":143
+ * class Event:
+ * def __init__(self, callback, owner, **kw):
+ * self.callback = callback # <<<<<<<<<<<<<<
+ * self.owner = owner
+ * for k, v in kw.items():
+ */
+ if (PyObject_SetAttr(__pyx_v_self, __pyx_n_s__callback, __pyx_v_callback) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "iocpsupport.pyx":144
+ * def __init__(self, callback, owner, **kw):
+ * self.callback = callback
+ * self.owner = owner # <<<<<<<<<<<<<<
+ * for k, v in kw.items():
+ * setattr(self, k, v)
+ */
+ if (PyObject_SetAttr(__pyx_v_self, __pyx_n_s__owner, __pyx_v_owner) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 144; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "iocpsupport.pyx":145
+ * self.callback = callback
+ * self.owner = owner
+ * for k, v in kw.items(): # <<<<<<<<<<<<<<
+ * setattr(self, k, v)
+ *
+ */
+ if (unlikely(((PyObject *)__pyx_v_kw) == Py_None)) {
+ PyErr_Format(PyExc_AttributeError, "'NoneType' object has no attribute '%s'", "items"); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_1 = PyDict_Items(__pyx_v_kw); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyList_CheckExact(__pyx_t_1) || PyTuple_CheckExact(__pyx_t_1)) {
+ __pyx_t_2 = __pyx_t_1; __Pyx_INCREF(__pyx_t_2); __pyx_t_3 = 0;
+ __pyx_t_4 = NULL;
+ } else {
+ __pyx_t_3 = -1; __pyx_t_2 = PyObject_GetIter(__pyx_t_1); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_4 = Py_TYPE(__pyx_t_2)->tp_iternext;
+ }
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ for (;;) {
+ if (PyList_CheckExact(__pyx_t_2)) {
+ if (__pyx_t_3 >= PyList_GET_SIZE(__pyx_t_2)) break;
+ __pyx_t_1 = PyList_GET_ITEM(__pyx_t_2, __pyx_t_3); __Pyx_INCREF(__pyx_t_1); __pyx_t_3++;
+ } else if (PyTuple_CheckExact(__pyx_t_2)) {
+ if (__pyx_t_3 >= PyTuple_GET_SIZE(__pyx_t_2)) break;
+ __pyx_t_1 = PyTuple_GET_ITEM(__pyx_t_2, __pyx_t_3); __Pyx_INCREF(__pyx_t_1); __pyx_t_3++;
+ } else {
+ __pyx_t_1 = __pyx_t_4(__pyx_t_2);
+ if (unlikely(!__pyx_t_1)) {
+ if (PyErr_Occurred()) {
+ if (likely(PyErr_ExceptionMatches(PyExc_StopIteration))) PyErr_Clear();
+ else {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ break;
+ }
+ __Pyx_GOTREF(__pyx_t_1);
+ }
+ if ((likely(PyTuple_CheckExact(__pyx_t_1))) || (PyList_CheckExact(__pyx_t_1))) {
+ PyObject* sequence = __pyx_t_1;
+ if (likely(PyTuple_CheckExact(sequence))) {
+ if (unlikely(PyTuple_GET_SIZE(sequence) != 2)) {
+ if (PyTuple_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyTuple_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_5 = PyTuple_GET_ITEM(sequence, 0);
+ __pyx_t_6 = PyTuple_GET_ITEM(sequence, 1);
+ } else {
+ if (unlikely(PyList_GET_SIZE(sequence) != 2)) {
+ if (PyList_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyList_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_5 = PyList_GET_ITEM(sequence, 0);
+ __pyx_t_6 = PyList_GET_ITEM(sequence, 1);
+ }
+ __Pyx_INCREF(__pyx_t_5);
+ __Pyx_INCREF(__pyx_t_6);
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ } else {
+ Py_ssize_t index = -1;
+ __pyx_t_7 = PyObject_GetIter(__pyx_t_1); if (unlikely(!__pyx_t_7)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_7);
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_8 = Py_TYPE(__pyx_t_7)->tp_iternext;
+ index = 0; __pyx_t_5 = __pyx_t_8(__pyx_t_7); if (unlikely(!__pyx_t_5)) goto __pyx_L8_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_5);
+ index = 1; __pyx_t_6 = __pyx_t_8(__pyx_t_7); if (unlikely(!__pyx_t_6)) goto __pyx_L8_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_6);
+ if (__Pyx_IternextUnpackEndCheck(__pyx_t_8(__pyx_t_7), 2) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0;
+ goto __pyx_L9_unpacking_done;
+ __pyx_L8_unpacking_failed:;
+ __Pyx_DECREF(__pyx_t_7); __pyx_t_7 = 0;
+ if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_StopIteration)) PyErr_Clear();
+ if (!PyErr_Occurred()) __Pyx_RaiseNeedMoreValuesError(index);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 145; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_L9_unpacking_done:;
+ }
+ __Pyx_XDECREF(__pyx_v_k);
+ __pyx_v_k = __pyx_t_5;
+ __pyx_t_5 = 0;
+ __Pyx_XDECREF(__pyx_v_v);
+ __pyx_v_v = __pyx_t_6;
+ __pyx_t_6 = 0;
+
+ /* "iocpsupport.pyx":146
+ * self.owner = owner
+ * for k, v in kw.items():
+ * setattr(self, k, v) # <<<<<<<<<<<<<<
+ *
+ * cdef class CompletionPort:
+ */
+ __pyx_t_9 = PyObject_SetAttr(__pyx_v_self, __pyx_v_k, __pyx_v_v); if (unlikely(__pyx_t_9 == -1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 146; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_XDECREF(__pyx_t_6);
+ __Pyx_XDECREF(__pyx_t_7);
+ __Pyx_AddTraceback("iocpsupport.Event.__init__", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_kw);
+ __Pyx_XDECREF(__pyx_v_k);
+ __Pyx_XDECREF(__pyx_v_v);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":150
+ * cdef class CompletionPort:
+ * cdef HANDLE port
+ * def __init__(self): # <<<<<<<<<<<<<<
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0)
+ */
+
+static int __pyx_pf_11iocpsupport_14CompletionPort___init__(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static int __pyx_pf_11iocpsupport_14CompletionPort___init__(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ __pyx_t_11iocpsupport_HANDLE __pyx_v_res;
+ int __pyx_r;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("__init__");
+ if (unlikely(PyTuple_GET_SIZE(__pyx_args) > 0)) {
+ __Pyx_RaiseArgtupleInvalid("__init__", 1, 0, 0, PyTuple_GET_SIZE(__pyx_args)); return -1;}
+ if (unlikely(__pyx_kwds) && unlikely(PyDict_Size(__pyx_kwds) > 0) && unlikely(!__Pyx_CheckKeywordStrings(__pyx_kwds, "__init__", 0))) return -1;
+
+ /* "iocpsupport.pyx":152
+ * def __init__(self):
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0) # <<<<<<<<<<<<<<
+ * if not res:
+ * raise_error(0, 'CreateIoCompletionPort')
+ */
+ __pyx_v_res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
+
+ /* "iocpsupport.pyx":153
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0)
+ * if not res: # <<<<<<<<<<<<<<
+ * raise_error(0, 'CreateIoCompletionPort')
+ * self.port = res
+ */
+ __pyx_t_1 = (!__pyx_v_res);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":154
+ * res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0)
+ * if not res:
+ * raise_error(0, 'CreateIoCompletionPort') # <<<<<<<<<<<<<<
+ * self.port = res
+ *
+ */
+ __pyx_t_2 = ((PyObject *)__pyx_n_s_1);
+ __Pyx_INCREF(__pyx_t_2);
+ __pyx_f_11iocpsupport_raise_error(0, __pyx_t_2); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 154; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ /* "iocpsupport.pyx":155
+ * if not res:
+ * raise_error(0, 'CreateIoCompletionPort')
+ * self.port = res # <<<<<<<<<<<<<<
+ *
+ * def addHandle(self, HANDLE handle, size_t key=0):
+ */
+ ((struct __pyx_obj_11iocpsupport_CompletionPort *)__pyx_v_self)->port = __pyx_v_res;
+
+ __pyx_r = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.__init__", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = -1;
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":157
+ * self.port = res
+ *
+ * def addHandle(self, HANDLE handle, size_t key=0): # <<<<<<<<<<<<<<
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(handle, self.port, key, 0)
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_1addHandle(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_1addHandle(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ __pyx_t_11iocpsupport_HANDLE __pyx_v_handle;
+ size_t __pyx_v_key;
+ __pyx_t_11iocpsupport_HANDLE __pyx_v_res;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__handle,&__pyx_n_s__key,0};
+ __Pyx_RefNannySetupContext("addHandle");
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__handle);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__key);
+ if (value) { values[1] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "addHandle") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 157; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ __pyx_v_handle = __Pyx_PyInt_AsSize_t(values[0]); if (unlikely((__pyx_v_handle == (size_t)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 157; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ if (values[1]) {
+ __pyx_v_key = __Pyx_PyInt_AsSize_t(values[1]); if (unlikely((__pyx_v_key == (size_t)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 157; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_key = ((size_t)0);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("addHandle", 0, 1, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 157; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.addHandle", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":159
+ * def addHandle(self, HANDLE handle, size_t key=0):
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(handle, self.port, key, 0) # <<<<<<<<<<<<<<
+ * if not res:
+ * raise_error(0, 'CreateIoCompletionPort')
+ */
+ __pyx_v_res = CreateIoCompletionPort(__pyx_v_handle, ((struct __pyx_obj_11iocpsupport_CompletionPort *)__pyx_v_self)->port, __pyx_v_key, 0);
+
+ /* "iocpsupport.pyx":160
+ * cdef HANDLE res
+ * res = CreateIoCompletionPort(handle, self.port, key, 0)
+ * if not res: # <<<<<<<<<<<<<<
+ * raise_error(0, 'CreateIoCompletionPort')
+ *
+ */
+ __pyx_t_1 = (!__pyx_v_res);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":161
+ * res = CreateIoCompletionPort(handle, self.port, key, 0)
+ * if not res:
+ * raise_error(0, 'CreateIoCompletionPort') # <<<<<<<<<<<<<<
+ *
+ * def getEvent(self, long timeout):
+ */
+ __pyx_t_2 = ((PyObject *)__pyx_n_s_1);
+ __Pyx_INCREF(__pyx_t_2);
+ __pyx_f_11iocpsupport_raise_error(0, __pyx_t_2); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 161; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.addHandle", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":163
+ * raise_error(0, 'CreateIoCompletionPort')
+ *
+ * def getEvent(self, long timeout): # <<<<<<<<<<<<<<
+ * cdef PyThreadState *_save
+ * cdef unsigned long bytes, rc
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_2getEvent(PyObject *__pyx_v_self, PyObject *__pyx_arg_timeout); /*proto*/
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_2getEvent(PyObject *__pyx_v_self, PyObject *__pyx_arg_timeout) {
+ long __pyx_v_timeout;
+ struct PyThreadState *__pyx_v__save;
+ unsigned long __pyx_v_bytes;
+ unsigned long __pyx_v_rc;
+ size_t __pyx_v_key;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ PyObject *__pyx_v_obj = NULL;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("getEvent");
+ assert(__pyx_arg_timeout); {
+ __pyx_v_timeout = __Pyx_PyInt_AsLong(__pyx_arg_timeout); if (unlikely((__pyx_v_timeout == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 163; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.getEvent", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":169
+ * cdef myOVERLAPPED *ov
+ *
+ * _save = PyEval_SaveThread() # <<<<<<<<<<<<<<
+ * rc = GetQueuedCompletionStatus(self.port, &bytes, &key, <OVERLAPPED **>&ov, timeout)
+ * PyEval_RestoreThread(_save)
+ */
+ __pyx_v__save = PyEval_SaveThread();
+
+ /* "iocpsupport.pyx":170
+ *
+ * _save = PyEval_SaveThread()
+ * rc = GetQueuedCompletionStatus(self.port, &bytes, &key, <OVERLAPPED **>&ov, timeout) # <<<<<<<<<<<<<<
+ * PyEval_RestoreThread(_save)
+ *
+ */
+ __pyx_v_rc = GetQueuedCompletionStatus(((struct __pyx_obj_11iocpsupport_CompletionPort *)__pyx_v_self)->port, (&__pyx_v_bytes), (&__pyx_v_key), ((OVERLAPPED **)(&__pyx_v_ov)), __pyx_v_timeout);
+
+ /* "iocpsupport.pyx":171
+ * _save = PyEval_SaveThread()
+ * rc = GetQueuedCompletionStatus(self.port, &bytes, &key, <OVERLAPPED **>&ov, timeout)
+ * PyEval_RestoreThread(_save) # <<<<<<<<<<<<<<
+ *
+ * if not rc:
+ */
+ PyEval_RestoreThread(__pyx_v__save);
+
+ /* "iocpsupport.pyx":173
+ * PyEval_RestoreThread(_save)
+ *
+ * if not rc: # <<<<<<<<<<<<<<
+ * rc = GetLastError()
+ * else:
+ */
+ __pyx_t_1 = (!__pyx_v_rc);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":174
+ *
+ * if not rc:
+ * rc = GetLastError() # <<<<<<<<<<<<<<
+ * else:
+ * rc = 0
+ */
+ __pyx_v_rc = GetLastError();
+ goto __pyx_L5;
+ }
+ /*else*/ {
+
+ /* "iocpsupport.pyx":176
+ * rc = GetLastError()
+ * else:
+ * rc = 0 # <<<<<<<<<<<<<<
+ *
+ * obj = None
+ */
+ __pyx_v_rc = 0;
+ }
+ __pyx_L5:;
+
+ /* "iocpsupport.pyx":178
+ * rc = 0
+ *
+ * obj = None # <<<<<<<<<<<<<<
+ * if ov:
+ * if ov.obj:
+ */
+ __Pyx_INCREF(Py_None);
+ __pyx_v_obj = Py_None;
+
+ /* "iocpsupport.pyx":179
+ *
+ * obj = None
+ * if ov: # <<<<<<<<<<<<<<
+ * if ov.obj:
+ * obj = <object>ov.obj
+ */
+ __pyx_t_1 = (__pyx_v_ov != 0);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":180
+ * obj = None
+ * if ov:
+ * if ov.obj: # <<<<<<<<<<<<<<
+ * obj = <object>ov.obj
+ * Py_DECREF(obj) # we are stealing a reference here
+ */
+ __pyx_t_1 = (__pyx_v_ov->obj != 0);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":181
+ * if ov:
+ * if ov.obj:
+ * obj = <object>ov.obj # <<<<<<<<<<<<<<
+ * Py_DECREF(obj) # we are stealing a reference here
+ * PyMem_Free(ov)
+ */
+ __Pyx_INCREF(((PyObject *)__pyx_v_ov->obj));
+ __Pyx_DECREF(__pyx_v_obj);
+ __pyx_v_obj = ((PyObject *)__pyx_v_ov->obj);
+
+ /* "iocpsupport.pyx":182
+ * if ov.obj:
+ * obj = <object>ov.obj
+ * Py_DECREF(obj) # we are stealing a reference here # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ *
+ */
+ Py_DECREF(__pyx_v_obj);
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ /* "iocpsupport.pyx":183
+ * obj = <object>ov.obj
+ * Py_DECREF(obj) # we are stealing a reference here
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ *
+ * return (rc, bytes, key, obj)
+ */
+ PyMem_Free(__pyx_v_ov);
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "iocpsupport.pyx":185
+ * PyMem_Free(ov)
+ *
+ * return (rc, bytes, key, obj) # <<<<<<<<<<<<<<
+ *
+ * def postEvent(self, unsigned long bytes, size_t key, obj):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = PyLong_FromUnsignedLong(__pyx_v_rc); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 185; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyLong_FromUnsignedLong(__pyx_v_bytes); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 185; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = __Pyx_PyInt_FromSize_t(__pyx_v_key); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 185; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_5 = PyTuple_New(4); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 185; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ PyTuple_SET_ITEM(__pyx_t_5, 2, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ __Pyx_INCREF(__pyx_v_obj);
+ PyTuple_SET_ITEM(__pyx_t_5, 3, __pyx_v_obj);
+ __Pyx_GIVEREF(__pyx_v_obj);
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_4 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_5);
+ __pyx_t_5 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.getEvent", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_obj);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":187
+ * return (rc, bytes, key, obj)
+ *
+ * def postEvent(self, unsigned long bytes, size_t key, obj): # <<<<<<<<<<<<<<
+ * cdef myOVERLAPPED *ov
+ * cdef unsigned long rc
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_3postEvent(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_3postEvent(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ unsigned long __pyx_v_bytes;
+ size_t __pyx_v_key;
+ PyObject *__pyx_v_obj = 0;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ unsigned long __pyx_v_rc;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_2;
+ PyObject *__pyx_t_3 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__bytes,&__pyx_n_s__key,&__pyx_n_s__obj,0};
+ __Pyx_RefNannySetupContext("postEvent");
+ {
+ PyObject* values[3] = {0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__bytes);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__key);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("postEvent", 1, 3, 3, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("postEvent", 1, 3, 3, 2); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "postEvent") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ }
+ __pyx_v_bytes = __Pyx_PyInt_AsUnsignedLong(values[0]); if (unlikely((__pyx_v_bytes == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_key = __Pyx_PyInt_AsSize_t(values[1]); if (unlikely((__pyx_v_key == (size_t)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_obj = values[2];
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("postEvent", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 187; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.postEvent", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":191
+ * cdef unsigned long rc
+ *
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov = makeOV()
+ * Py_INCREF(obj) # give ov its own reference to obj
+ */
+ __pyx_t_1 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":192
+ *
+ * if obj is not None:
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * Py_INCREF(obj) # give ov its own reference to obj
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_2 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_2 == NULL)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 192; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ov = __pyx_t_2;
+
+ /* "iocpsupport.pyx":193
+ * if obj is not None:
+ * ov = makeOV()
+ * Py_INCREF(obj) # give ov its own reference to obj # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ * else:
+ */
+ Py_INCREF(__pyx_v_obj);
+
+ /* "iocpsupport.pyx":194
+ * ov = makeOV()
+ * Py_INCREF(obj) # give ov its own reference to obj
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ * else:
+ * ov = NULL
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L6;
+ }
+ /*else*/ {
+
+ /* "iocpsupport.pyx":196
+ * ov.obj = <PyObject *>obj
+ * else:
+ * ov = NULL # <<<<<<<<<<<<<<
+ *
+ * rc = PostQueuedCompletionStatus(self.port, bytes, key, <OVERLAPPED *>ov)
+ */
+ __pyx_v_ov = NULL;
+ }
+ __pyx_L6:;
+
+ /* "iocpsupport.pyx":198
+ * ov = NULL
+ *
+ * rc = PostQueuedCompletionStatus(self.port, bytes, key, <OVERLAPPED *>ov) # <<<<<<<<<<<<<<
+ * if not rc:
+ * if ov:
+ */
+ __pyx_v_rc = PostQueuedCompletionStatus(((struct __pyx_obj_11iocpsupport_CompletionPort *)__pyx_v_self)->port, __pyx_v_bytes, __pyx_v_key, ((OVERLAPPED *)__pyx_v_ov));
+
+ /* "iocpsupport.pyx":199
+ *
+ * rc = PostQueuedCompletionStatus(self.port, bytes, key, <OVERLAPPED *>ov)
+ * if not rc: # <<<<<<<<<<<<<<
+ * if ov:
+ * Py_DECREF(obj)
+ */
+ __pyx_t_1 = (!__pyx_v_rc);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":200
+ * rc = PostQueuedCompletionStatus(self.port, bytes, key, <OVERLAPPED *>ov)
+ * if not rc:
+ * if ov: # <<<<<<<<<<<<<<
+ * Py_DECREF(obj)
+ * PyMem_Free(ov)
+ */
+ __pyx_t_1 = (__pyx_v_ov != 0);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":201
+ * if not rc:
+ * if ov:
+ * Py_DECREF(obj) # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * raise_error(0, 'PostQueuedCompletionStatus')
+ */
+ Py_DECREF(__pyx_v_obj);
+
+ /* "iocpsupport.pyx":202
+ * if ov:
+ * Py_DECREF(obj)
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * raise_error(0, 'PostQueuedCompletionStatus')
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+
+ /* "iocpsupport.pyx":203
+ * Py_DECREF(obj)
+ * PyMem_Free(ov)
+ * raise_error(0, 'PostQueuedCompletionStatus') # <<<<<<<<<<<<<<
+ *
+ * def __del__(self):
+ */
+ __pyx_t_3 = ((PyObject *)__pyx_n_s_2);
+ __Pyx_INCREF(__pyx_t_3);
+ __pyx_f_11iocpsupport_raise_error(0, __pyx_t_3); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 203; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_AddTraceback("iocpsupport.CompletionPort.postEvent", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":205
+ * raise_error(0, 'PostQueuedCompletionStatus')
+ *
+ * def __del__(self): # <<<<<<<<<<<<<<
+ * CloseHandle(self.port)
+ *
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_4__del__(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused); /*proto*/
+static PyObject *__pyx_pf_11iocpsupport_14CompletionPort_4__del__(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused) {
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ __Pyx_RefNannySetupContext("__del__");
+
+ /* "iocpsupport.pyx":206
+ *
+ * def __del__(self):
+ * CloseHandle(self.port) # <<<<<<<<<<<<<<
+ *
+ * def makesockaddr(object buff):
+ */
+ CloseHandle(((struct __pyx_obj_11iocpsupport_CompletionPort *)__pyx_v_self)->port);
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":208
+ * CloseHandle(self.port)
+ *
+ * def makesockaddr(object buff): # <<<<<<<<<<<<<<
+ * cdef void *mem_buffer
+ * cdef Py_ssize_t size
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_makesockaddr(PyObject *__pyx_self, PyObject *__pyx_v_buff); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_makesockaddr = {__Pyx_NAMESTR("makesockaddr"), (PyCFunction)__pyx_pf_11iocpsupport_makesockaddr, METH_O, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_makesockaddr(PyObject *__pyx_self, PyObject *__pyx_v_buff) {
+ void *__pyx_v_mem_buffer;
+ Py_ssize_t __pyx_v_size;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("makesockaddr");
+ __pyx_self = __pyx_self;
+
+ /* "iocpsupport.pyx":212
+ * cdef Py_ssize_t size
+ *
+ * PyObject_AsReadBuffer(buff, &mem_buffer, &size) # <<<<<<<<<<<<<<
+ * # XXX: this should really return the address family as well
+ * return _makesockaddr(<sockaddr *>mem_buffer, size)
+ */
+ __pyx_t_1 = PyObject_AsReadBuffer(__pyx_v_buff, (&__pyx_v_mem_buffer), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "iocpsupport.pyx":214
+ * PyObject_AsReadBuffer(buff, &mem_buffer, &size)
+ * # XXX: this should really return the address family as well
+ * return _makesockaddr(<sockaddr *>mem_buffer, size) # <<<<<<<<<<<<<<
+ *
+ * cdef object _makesockaddr(sockaddr *addr, Py_ssize_t len):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = __pyx_f_11iocpsupport__makesockaddr(((struct sockaddr *)__pyx_v_mem_buffer), __pyx_v_size); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 214; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_r = __pyx_t_2;
+ __pyx_t_2 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("iocpsupport.makesockaddr", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":216
+ * return _makesockaddr(<sockaddr *>mem_buffer, size)
+ *
+ * cdef object _makesockaddr(sockaddr *addr, Py_ssize_t len): # <<<<<<<<<<<<<<
+ * cdef sockaddr_in *sin
+ * cdef sockaddr_in6 *sin6
+ */
+
+static PyObject *__pyx_f_11iocpsupport__makesockaddr(struct sockaddr *__pyx_v_addr, Py_ssize_t __pyx_v_len) {
+ struct sockaddr_in *__pyx_v_sin;
+ struct sockaddr_in6 *__pyx_v_sin6;
+ char __pyx_v_buff[256];
+ int __pyx_v_rc;
+ __pyx_t_11iocpsupport_DWORD __pyx_v_buff_size;
+ PyObject *__pyx_v_host = NULL;
+ unsigned short __pyx_v_sa_port;
+ PyObject *__pyx_v_port = NULL;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ unsigned short __pyx_t_5;
+ PyObject *__pyx_t_6 = NULL;
+ PyObject *(*__pyx_t_7)(PyObject *);
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("_makesockaddr");
+
+ /* "iocpsupport.pyx":221
+ * cdef char buff[256]
+ * cdef int rc
+ * cdef DWORD buff_size = sizeof(buff) # <<<<<<<<<<<<<<
+ * if not len:
+ * return None
+ */
+ __pyx_v_buff_size = (sizeof(__pyx_v_buff));
+
+ /* "iocpsupport.pyx":222
+ * cdef int rc
+ * cdef DWORD buff_size = sizeof(buff)
+ * if not len: # <<<<<<<<<<<<<<
+ * return None
+ * if addr.sa_family == AF_INET:
+ */
+ __pyx_t_1 = (!__pyx_v_len);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":223
+ * cdef DWORD buff_size = sizeof(buff)
+ * if not len:
+ * return None # <<<<<<<<<<<<<<
+ * if addr.sa_family == AF_INET:
+ * sin = <sockaddr_in *>addr
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __Pyx_INCREF(Py_None);
+ __pyx_r = Py_None;
+ goto __pyx_L0;
+ goto __pyx_L3;
+ }
+ __pyx_L3:;
+
+ /* "iocpsupport.pyx":227
+ * sin = <sockaddr_in *>addr
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ * elif addr.sa_family == AF_INET6: # <<<<<<<<<<<<<<
+ * sin6 = <sockaddr_in6 *>addr
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ */
+ switch (__pyx_v_addr->sa_family) {
+
+ /* "iocpsupport.pyx":224
+ * if not len:
+ * return None
+ * if addr.sa_family == AF_INET: # <<<<<<<<<<<<<<
+ * sin = <sockaddr_in *>addr
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ */
+ case AF_INET:
+
+ /* "iocpsupport.pyx":225
+ * return None
+ * if addr.sa_family == AF_INET:
+ * sin = <sockaddr_in *>addr # <<<<<<<<<<<<<<
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ * elif addr.sa_family == AF_INET6:
+ */
+ __pyx_v_sin = ((struct sockaddr_in *)__pyx_v_addr);
+
+ /* "iocpsupport.pyx":226
+ * if addr.sa_family == AF_INET:
+ * sin = <sockaddr_in *>addr
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port) # <<<<<<<<<<<<<<
+ * elif addr.sa_family == AF_INET6:
+ * sin6 = <sockaddr_in6 *>addr
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = PyString_FromString(inet_ntoa(__pyx_v_sin->sin_addr)); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 226; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyInt_FromLong(ntohs(__pyx_v_sin->sin_port)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 226; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 226; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_4);
+ __pyx_t_4 = 0;
+ goto __pyx_L0;
+ break;
+
+ /* "iocpsupport.pyx":227
+ * sin = <sockaddr_in *>addr
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ * elif addr.sa_family == AF_INET6: # <<<<<<<<<<<<<<
+ * sin6 = <sockaddr_in6 *>addr
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ */
+ case AF_INET6:
+
+ /* "iocpsupport.pyx":228
+ * return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ * elif addr.sa_family == AF_INET6:
+ * sin6 = <sockaddr_in6 *>addr # <<<<<<<<<<<<<<
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_sin6 = ((struct sockaddr_in6 *)__pyx_v_addr);
+
+ /* "iocpsupport.pyx":229
+ * elif addr.sa_family == AF_INET6:
+ * sin6 = <sockaddr_in6 *>addr
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size) # <<<<<<<<<<<<<<
+ * if rc == SOCKET_ERROR:
+ * raise_error(0, 'WSAAddressToString')
+ */
+ __pyx_v_rc = WSAAddressToStringA(__pyx_v_addr, (sizeof(struct sockaddr_in6)), NULL, __pyx_v_buff, (&__pyx_v_buff_size));
+
+ /* "iocpsupport.pyx":230
+ * sin6 = <sockaddr_in6 *>addr
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * raise_error(0, 'WSAAddressToString')
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ */
+ __pyx_t_1 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":231
+ * rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ * if rc == SOCKET_ERROR:
+ * raise_error(0, 'WSAAddressToString') # <<<<<<<<<<<<<<
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ * host, port = host.rsplit(':', 1)
+ */
+ __pyx_t_4 = ((PyObject *)__pyx_n_s__WSAAddressToString);
+ __Pyx_INCREF(__pyx_t_4);
+ __pyx_f_11iocpsupport_raise_error(0, __pyx_t_4); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 231; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ goto __pyx_L4;
+ }
+ __pyx_L4:;
+
+ /* "iocpsupport.pyx":232
+ * if rc == SOCKET_ERROR:
+ * raise_error(0, 'WSAAddressToString')
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port) # <<<<<<<<<<<<<<
+ * host, port = host.rsplit(':', 1)
+ * port = int(port)
+ */
+ __pyx_t_4 = PyString_FromString(__pyx_v_buff); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 232; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_5 = ntohs(__pyx_v_sin6->sin6_port);
+ __pyx_v_host = __pyx_t_4;
+ __pyx_t_4 = 0;
+ __pyx_v_sa_port = __pyx_t_5;
+
+ /* "iocpsupport.pyx":233
+ * raise_error(0, 'WSAAddressToString')
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ * host, port = host.rsplit(':', 1) # <<<<<<<<<<<<<<
+ * port = int(port)
+ * assert host[0] == '['
+ */
+ __pyx_t_4 = PyObject_GetAttr(__pyx_v_host, __pyx_n_s__rsplit); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_3 = PyObject_Call(__pyx_t_4, ((PyObject *)__pyx_k_tuple_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ if ((likely(PyTuple_CheckExact(__pyx_t_3))) || (PyList_CheckExact(__pyx_t_3))) {
+ PyObject* sequence = __pyx_t_3;
+ if (likely(PyTuple_CheckExact(sequence))) {
+ if (unlikely(PyTuple_GET_SIZE(sequence) != 2)) {
+ if (PyTuple_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyTuple_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_4 = PyTuple_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyTuple_GET_ITEM(sequence, 1);
+ } else {
+ if (unlikely(PyList_GET_SIZE(sequence) != 2)) {
+ if (PyList_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyList_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_4 = PyList_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyList_GET_ITEM(sequence, 1);
+ }
+ __Pyx_INCREF(__pyx_t_4);
+ __Pyx_INCREF(__pyx_t_2);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ } else {
+ Py_ssize_t index = -1;
+ __pyx_t_6 = PyObject_GetIter(__pyx_t_3); if (unlikely(!__pyx_t_6)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_6);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ __pyx_t_7 = Py_TYPE(__pyx_t_6)->tp_iternext;
+ index = 0; __pyx_t_4 = __pyx_t_7(__pyx_t_6); if (unlikely(!__pyx_t_4)) goto __pyx_L5_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_4);
+ index = 1; __pyx_t_2 = __pyx_t_7(__pyx_t_6); if (unlikely(!__pyx_t_2)) goto __pyx_L5_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_2);
+ if (__Pyx_IternextUnpackEndCheck(__pyx_t_7(__pyx_t_6), 2) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0;
+ goto __pyx_L6_unpacking_done;
+ __pyx_L5_unpacking_failed:;
+ __Pyx_DECREF(__pyx_t_6); __pyx_t_6 = 0;
+ if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_StopIteration)) PyErr_Clear();
+ if (!PyErr_Occurred()) __Pyx_RaiseNeedMoreValuesError(index);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_L6_unpacking_done:;
+ }
+ __Pyx_DECREF(__pyx_v_host);
+ __pyx_v_host = __pyx_t_4;
+ __pyx_t_4 = 0;
+ __pyx_v_port = __pyx_t_2;
+ __pyx_t_2 = 0;
+
+ /* "iocpsupport.pyx":234
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ * host, port = host.rsplit(':', 1)
+ * port = int(port) # <<<<<<<<<<<<<<
+ * assert host[0] == '['
+ * assert host[-1] == ']'
+ */
+ __pyx_t_3 = PyTuple_New(1); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 234; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __Pyx_INCREF(__pyx_v_port);
+ PyTuple_SET_ITEM(__pyx_t_3, 0, __pyx_v_port);
+ __Pyx_GIVEREF(__pyx_v_port);
+ __pyx_t_2 = PyObject_Call(((PyObject *)((PyObject*)(&PyInt_Type))), ((PyObject *)__pyx_t_3), NULL); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 234; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __Pyx_DECREF(((PyObject *)__pyx_t_3)); __pyx_t_3 = 0;
+ __Pyx_DECREF(__pyx_v_port);
+ __pyx_v_port = __pyx_t_2;
+ __pyx_t_2 = 0;
+
+ /* "iocpsupport.pyx":235
+ * host, port = host.rsplit(':', 1)
+ * port = int(port)
+ * assert host[0] == '[' # <<<<<<<<<<<<<<
+ * assert host[-1] == ']'
+ * assert port == sa_port
+ */
+ #ifndef CYTHON_WITHOUT_ASSERTIONS
+ __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_host, 0, sizeof(long), PyInt_FromLong); if (!__pyx_t_2) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_1 = __Pyx_PyString_Equals(__pyx_t_2, ((PyObject *)__pyx_kp_s_5), Py_EQ); if (unlikely(__pyx_t_1 < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ if (unlikely(!__pyx_t_1)) {
+ PyErr_SetNone(PyExc_AssertionError);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ #endif
+
+ /* "iocpsupport.pyx":236
+ * port = int(port)
+ * assert host[0] == '['
+ * assert host[-1] == ']' # <<<<<<<<<<<<<<
+ * assert port == sa_port
+ * return host[1:-1], port
+ */
+ #ifndef CYTHON_WITHOUT_ASSERTIONS
+ __pyx_t_2 = __Pyx_GetItemInt(__pyx_v_host, -1, sizeof(long), PyInt_FromLong); if (!__pyx_t_2) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 236; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_1 = __Pyx_PyString_Equals(__pyx_t_2, ((PyObject *)__pyx_kp_s_6), Py_EQ); if (unlikely(__pyx_t_1 < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 236; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ if (unlikely(!__pyx_t_1)) {
+ PyErr_SetNone(PyExc_AssertionError);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 236; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ #endif
+
+ /* "iocpsupport.pyx":237
+ * assert host[0] == '['
+ * assert host[-1] == ']'
+ * assert port == sa_port # <<<<<<<<<<<<<<
+ * return host[1:-1], port
+ * else:
+ */
+ #ifndef CYTHON_WITHOUT_ASSERTIONS
+ __pyx_t_2 = PyInt_FromLong(__pyx_v_sa_port); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyObject_RichCompare(__pyx_v_port, __pyx_t_2, Py_EQ); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __pyx_t_1 = __Pyx_PyObject_IsTrue(__pyx_t_3); if (unlikely(__pyx_t_1 < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ if (unlikely(!__pyx_t_1)) {
+ PyErr_SetNone(PyExc_AssertionError);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ #endif
+
+ /* "iocpsupport.pyx":238
+ * assert host[-1] == ']'
+ * assert port == sa_port
+ * return host[1:-1], port # <<<<<<<<<<<<<<
+ * else:
+ * return PyString_FromStringAndSize(addr.sa_data, sizeof(addr.sa_data))
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_3 = __Pyx_PySequence_GetSlice(__pyx_v_host, 1, -1); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 238; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_2 = PyTuple_New(2); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 238; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_2));
+ PyTuple_SET_ITEM(__pyx_t_2, 0, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ __Pyx_INCREF(__pyx_v_port);
+ PyTuple_SET_ITEM(__pyx_t_2, 1, __pyx_v_port);
+ __Pyx_GIVEREF(__pyx_v_port);
+ __pyx_t_3 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_2);
+ __pyx_t_2 = 0;
+ goto __pyx_L0;
+ break;
+ default:
+
+ /* "iocpsupport.pyx":240
+ * return host[1:-1], port
+ * else:
+ * return PyString_FromStringAndSize(addr.sa_data, sizeof(addr.sa_data)) # <<<<<<<<<<<<<<
+ *
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = PyString_FromStringAndSize(__pyx_v_addr->sa_data, (sizeof(__pyx_v_addr->sa_data))); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 240; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_r = __pyx_t_2;
+ __pyx_t_2 = 0;
+ goto __pyx_L0;
+ break;
+ }
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_6);
+ __Pyx_AddTraceback("iocpsupport._makesockaddr", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = 0;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_host);
+ __Pyx_XDECREF(__pyx_v_port);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":243
+ *
+ *
+ * cdef object fillinetaddr(sockaddr_in *dest, object addr): # <<<<<<<<<<<<<<
+ * cdef unsigned short port
+ * cdef unsigned long res
+ */
+
+static PyObject *__pyx_f_11iocpsupport_fillinetaddr(struct sockaddr_in *__pyx_v_dest, PyObject *__pyx_v_addr) {
+ unsigned short __pyx_v_port;
+ unsigned long __pyx_v_res;
+ char *__pyx_v_hoststr;
+ PyObject *__pyx_v_host = NULL;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *(*__pyx_t_4)(PyObject *);
+ unsigned short __pyx_t_5;
+ char *__pyx_t_6;
+ int __pyx_t_7;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("fillinetaddr");
+
+ /* "iocpsupport.pyx":247
+ * cdef unsigned long res
+ * cdef char *hoststr
+ * host, port = addr # <<<<<<<<<<<<<<
+ *
+ * hoststr = PyString_AsString(host)
+ */
+ if ((likely(PyTuple_CheckExact(__pyx_v_addr))) || (PyList_CheckExact(__pyx_v_addr))) {
+ PyObject* sequence = __pyx_v_addr;
+ if (likely(PyTuple_CheckExact(sequence))) {
+ if (unlikely(PyTuple_GET_SIZE(sequence) != 2)) {
+ if (PyTuple_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyTuple_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_1 = PyTuple_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyTuple_GET_ITEM(sequence, 1);
+ } else {
+ if (unlikely(PyList_GET_SIZE(sequence) != 2)) {
+ if (PyList_GET_SIZE(sequence) > 2) __Pyx_RaiseTooManyValuesError(2);
+ else __Pyx_RaiseNeedMoreValuesError(PyList_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_1 = PyList_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyList_GET_ITEM(sequence, 1);
+ }
+ __Pyx_INCREF(__pyx_t_1);
+ __Pyx_INCREF(__pyx_t_2);
+ } else {
+ Py_ssize_t index = -1;
+ __pyx_t_3 = PyObject_GetIter(__pyx_v_addr); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = Py_TYPE(__pyx_t_3)->tp_iternext;
+ index = 0; __pyx_t_1 = __pyx_t_4(__pyx_t_3); if (unlikely(!__pyx_t_1)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_1);
+ index = 1; __pyx_t_2 = __pyx_t_4(__pyx_t_3); if (unlikely(!__pyx_t_2)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_2);
+ if (__Pyx_IternextUnpackEndCheck(__pyx_t_4(__pyx_t_3), 2) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ goto __pyx_L4_unpacking_done;
+ __pyx_L3_unpacking_failed:;
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_StopIteration)) PyErr_Clear();
+ if (!PyErr_Occurred()) __Pyx_RaiseNeedMoreValuesError(index);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_L4_unpacking_done:;
+ }
+ __pyx_t_5 = __Pyx_PyInt_AsUnsignedShort(__pyx_t_2); if (unlikely((__pyx_t_5 == (unsigned short)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 247; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __pyx_v_host = __pyx_t_1;
+ __pyx_t_1 = 0;
+ __pyx_v_port = __pyx_t_5;
+
+ /* "iocpsupport.pyx":249
+ * host, port = addr
+ *
+ * hoststr = PyString_AsString(host) # <<<<<<<<<<<<<<
+ * res = inet_addr(hoststr)
+ * if res == INADDR_ANY:
+ */
+ __pyx_t_6 = PyString_AsString(__pyx_v_host); if (unlikely(__pyx_t_6 == NULL)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 249; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_hoststr = __pyx_t_6;
+
+ /* "iocpsupport.pyx":250
+ *
+ * hoststr = PyString_AsString(host)
+ * res = inet_addr(hoststr) # <<<<<<<<<<<<<<
+ * if res == INADDR_ANY:
+ * raise ValueError, 'invalid IP address'
+ */
+ __pyx_v_res = inet_addr(__pyx_v_hoststr);
+
+ /* "iocpsupport.pyx":251
+ * hoststr = PyString_AsString(host)
+ * res = inet_addr(hoststr)
+ * if res == INADDR_ANY: # <<<<<<<<<<<<<<
+ * raise ValueError, 'invalid IP address'
+ * dest.sin_addr.s_addr = res
+ */
+ __pyx_t_7 = (__pyx_v_res == INADDR_ANY);
+ if (__pyx_t_7) {
+
+ /* "iocpsupport.pyx":252
+ * res = inet_addr(hoststr)
+ * if res == INADDR_ANY:
+ * raise ValueError, 'invalid IP address' # <<<<<<<<<<<<<<
+ * dest.sin_addr.s_addr = res
+ *
+ */
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_kp_s_7), 0, 0);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 252; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ /* "iocpsupport.pyx":253
+ * if res == INADDR_ANY:
+ * raise ValueError, 'invalid IP address'
+ * dest.sin_addr.s_addr = res # <<<<<<<<<<<<<<
+ *
+ * dest.sin_port = htons(port)
+ */
+ __pyx_v_dest->sin_addr.s_addr = __pyx_v_res;
+
+ /* "iocpsupport.pyx":255
+ * dest.sin_addr.s_addr = res
+ *
+ * dest.sin_port = htons(port) # <<<<<<<<<<<<<<
+ *
+ *
+ */
+ __pyx_v_dest->sin_port = htons(__pyx_v_port);
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_AddTraceback("iocpsupport.fillinetaddr", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = 0;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_host);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":258
+ *
+ *
+ * cdef object fillinet6addr(sockaddr_in6 *dest, object addr): # <<<<<<<<<<<<<<
+ * cdef unsigned short port
+ * cdef unsigned long res
+ */
+
+static PyObject *__pyx_f_11iocpsupport_fillinet6addr(struct sockaddr_in6 *__pyx_v_dest, PyObject *__pyx_v_addr) {
+ unsigned short __pyx_v_port;
+ char *__pyx_v_hoststr;
+ int __pyx_v_addrlen;
+ PyObject *__pyx_v_host = NULL;
+ PyObject *__pyx_v_flow = NULL;
+ PyObject *__pyx_v_scope = NULL;
+ int __pyx_v_parseresult;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ PyObject *(*__pyx_t_6)(PyObject *);
+ unsigned short __pyx_t_7;
+ char *__pyx_t_8;
+ int __pyx_t_9;
+ unsigned long __pyx_t_10;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("fillinet6addr");
+
+ /* "iocpsupport.pyx":262
+ * cdef unsigned long res
+ * cdef char *hoststr
+ * cdef int addrlen = sizeof(sockaddr_in6) # <<<<<<<<<<<<<<
+ * host, port, flow, scope = addr
+ * host = host.split("%")[0] # remove scope ID, if any
+ */
+ __pyx_v_addrlen = (sizeof(struct sockaddr_in6));
+
+ /* "iocpsupport.pyx":263
+ * cdef char *hoststr
+ * cdef int addrlen = sizeof(sockaddr_in6)
+ * host, port, flow, scope = addr # <<<<<<<<<<<<<<
+ * host = host.split("%")[0] # remove scope ID, if any
+ *
+ */
+ if ((likely(PyTuple_CheckExact(__pyx_v_addr))) || (PyList_CheckExact(__pyx_v_addr))) {
+ PyObject* sequence = __pyx_v_addr;
+ if (likely(PyTuple_CheckExact(sequence))) {
+ if (unlikely(PyTuple_GET_SIZE(sequence) != 4)) {
+ if (PyTuple_GET_SIZE(sequence) > 4) __Pyx_RaiseTooManyValuesError(4);
+ else __Pyx_RaiseNeedMoreValuesError(PyTuple_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_1 = PyTuple_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyTuple_GET_ITEM(sequence, 1);
+ __pyx_t_3 = PyTuple_GET_ITEM(sequence, 2);
+ __pyx_t_4 = PyTuple_GET_ITEM(sequence, 3);
+ } else {
+ if (unlikely(PyList_GET_SIZE(sequence) != 4)) {
+ if (PyList_GET_SIZE(sequence) > 4) __Pyx_RaiseTooManyValuesError(4);
+ else __Pyx_RaiseNeedMoreValuesError(PyList_GET_SIZE(sequence));
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ }
+ __pyx_t_1 = PyList_GET_ITEM(sequence, 0);
+ __pyx_t_2 = PyList_GET_ITEM(sequence, 1);
+ __pyx_t_3 = PyList_GET_ITEM(sequence, 2);
+ __pyx_t_4 = PyList_GET_ITEM(sequence, 3);
+ }
+ __Pyx_INCREF(__pyx_t_1);
+ __Pyx_INCREF(__pyx_t_2);
+ __Pyx_INCREF(__pyx_t_3);
+ __Pyx_INCREF(__pyx_t_4);
+ } else {
+ Py_ssize_t index = -1;
+ __pyx_t_5 = PyObject_GetIter(__pyx_v_addr); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_5);
+ __pyx_t_6 = Py_TYPE(__pyx_t_5)->tp_iternext;
+ index = 0; __pyx_t_1 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_1)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_1);
+ index = 1; __pyx_t_2 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_2)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_2);
+ index = 2; __pyx_t_3 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_3)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_3);
+ index = 3; __pyx_t_4 = __pyx_t_6(__pyx_t_5); if (unlikely(!__pyx_t_4)) goto __pyx_L3_unpacking_failed;
+ __Pyx_GOTREF(__pyx_t_4);
+ if (__Pyx_IternextUnpackEndCheck(__pyx_t_6(__pyx_t_5), 4) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
+ goto __pyx_L4_unpacking_done;
+ __pyx_L3_unpacking_failed:;
+ __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
+ if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_StopIteration)) PyErr_Clear();
+ if (!PyErr_Occurred()) __Pyx_RaiseNeedMoreValuesError(index);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_L4_unpacking_done:;
+ }
+ __pyx_t_7 = __Pyx_PyInt_AsUnsignedShort(__pyx_t_2); if (unlikely((__pyx_t_7 == (unsigned short)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 263; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __pyx_v_host = __pyx_t_1;
+ __pyx_t_1 = 0;
+ __pyx_v_port = __pyx_t_7;
+ __pyx_v_flow = __pyx_t_3;
+ __pyx_t_3 = 0;
+ __pyx_v_scope = __pyx_t_4;
+ __pyx_t_4 = 0;
+
+ /* "iocpsupport.pyx":264
+ * cdef int addrlen = sizeof(sockaddr_in6)
+ * host, port, flow, scope = addr
+ * host = host.split("%")[0] # remove scope ID, if any # <<<<<<<<<<<<<<
+ *
+ * hoststr = PyString_AsString(host)
+ */
+ __pyx_t_4 = PyObject_GetAttr(__pyx_v_host, __pyx_n_s__split); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 264; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_3 = PyObject_Call(__pyx_t_4, ((PyObject *)__pyx_k_tuple_9), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 264; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ __pyx_t_4 = __Pyx_GetItemInt(__pyx_t_3, 0, sizeof(long), PyInt_FromLong); if (!__pyx_t_4) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 264; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ __Pyx_DECREF(__pyx_v_host);
+ __pyx_v_host = __pyx_t_4;
+ __pyx_t_4 = 0;
+
+ /* "iocpsupport.pyx":266
+ * host = host.split("%")[0] # remove scope ID, if any
+ *
+ * hoststr = PyString_AsString(host) # <<<<<<<<<<<<<<
+ * cdef int parseresult = WSAStringToAddressA(hoststr, AF_INET6, NULL,
+ * <sockaddr *>dest, &addrlen)
+ */
+ __pyx_t_8 = PyString_AsString(__pyx_v_host); if (unlikely(__pyx_t_8 == NULL)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 266; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_hoststr = __pyx_t_8;
+
+ /* "iocpsupport.pyx":268
+ * hoststr = PyString_AsString(host)
+ * cdef int parseresult = WSAStringToAddressA(hoststr, AF_INET6, NULL,
+ * <sockaddr *>dest, &addrlen) # <<<<<<<<<<<<<<
+ * if parseresult == SOCKET_ERROR:
+ * raise ValueError, 'invalid IPv6 address %r' % (host,)
+ */
+ __pyx_v_parseresult = WSAStringToAddressA(__pyx_v_hoststr, AF_INET6, NULL, ((struct sockaddr *)__pyx_v_dest), (&__pyx_v_addrlen));
+
+ /* "iocpsupport.pyx":269
+ * cdef int parseresult = WSAStringToAddressA(hoststr, AF_INET6, NULL,
+ * <sockaddr *>dest, &addrlen)
+ * if parseresult == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * raise ValueError, 'invalid IPv6 address %r' % (host,)
+ * if parseresult != 0:
+ */
+ __pyx_t_9 = (__pyx_v_parseresult == SOCKET_ERROR);
+ if (__pyx_t_9) {
+
+ /* "iocpsupport.pyx":270
+ * <sockaddr *>dest, &addrlen)
+ * if parseresult == SOCKET_ERROR:
+ * raise ValueError, 'invalid IPv6 address %r' % (host,) # <<<<<<<<<<<<<<
+ * if parseresult != 0:
+ * raise RuntimeError, 'undefined error occurred during address parsing'
+ */
+ __pyx_t_4 = PyTuple_New(1); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 270; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ __Pyx_INCREF(__pyx_v_host);
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_v_host);
+ __Pyx_GIVEREF(__pyx_v_host);
+ __pyx_t_3 = PyNumber_Remainder(((PyObject *)__pyx_kp_s_10), ((PyObject *)__pyx_t_4)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 270; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_t_3), 0, 0);
+ __Pyx_DECREF(((PyObject *)__pyx_t_3)); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 270; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ /* "iocpsupport.pyx":271
+ * if parseresult == SOCKET_ERROR:
+ * raise ValueError, 'invalid IPv6 address %r' % (host,)
+ * if parseresult != 0: # <<<<<<<<<<<<<<
+ * raise RuntimeError, 'undefined error occurred during address parsing'
+ * # sin6_host field was handled by WSAStringToAddress
+ */
+ __pyx_t_9 = (__pyx_v_parseresult != 0);
+ if (__pyx_t_9) {
+
+ /* "iocpsupport.pyx":272
+ * raise ValueError, 'invalid IPv6 address %r' % (host,)
+ * if parseresult != 0:
+ * raise RuntimeError, 'undefined error occurred during address parsing' # <<<<<<<<<<<<<<
+ * # sin6_host field was handled by WSAStringToAddress
+ * dest.sin6_port = htons(port)
+ */
+ __Pyx_Raise(__pyx_builtin_RuntimeError, ((PyObject *)__pyx_kp_s_11), 0, 0);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 272; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "iocpsupport.pyx":274
+ * raise RuntimeError, 'undefined error occurred during address parsing'
+ * # sin6_host field was handled by WSAStringToAddress
+ * dest.sin6_port = htons(port) # <<<<<<<<<<<<<<
+ * dest.sin6_flowinfo = flow
+ * dest.sin6_scope_id = scope
+ */
+ __pyx_v_dest->sin6_port = htons(__pyx_v_port);
+
+ /* "iocpsupport.pyx":275
+ * # sin6_host field was handled by WSAStringToAddress
+ * dest.sin6_port = htons(port)
+ * dest.sin6_flowinfo = flow # <<<<<<<<<<<<<<
+ * dest.sin6_scope_id = scope
+ *
+ */
+ __pyx_t_10 = __Pyx_PyInt_AsUnsignedLong(__pyx_v_flow); if (unlikely((__pyx_t_10 == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 275; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_dest->sin6_flowinfo = __pyx_t_10;
+
+ /* "iocpsupport.pyx":276
+ * dest.sin6_port = htons(port)
+ * dest.sin6_flowinfo = flow
+ * dest.sin6_scope_id = scope # <<<<<<<<<<<<<<
+ *
+ *
+ */
+ __pyx_t_10 = __Pyx_PyInt_AsUnsignedLong(__pyx_v_scope); if (unlikely((__pyx_t_10 == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 276; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_dest->sin6_scope_id = __pyx_t_10;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("iocpsupport.fillinet6addr", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = 0;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_host);
+ __Pyx_XDECREF(__pyx_v_flow);
+ __Pyx_XDECREF(__pyx_v_scope);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":279
+ *
+ *
+ * def AllocateReadBuffer(int size): # <<<<<<<<<<<<<<
+ * return PyBuffer_New(size)
+ *
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_1AllocateReadBuffer(PyObject *__pyx_self, PyObject *__pyx_arg_size); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_1AllocateReadBuffer = {__Pyx_NAMESTR("AllocateReadBuffer"), (PyCFunction)__pyx_pf_11iocpsupport_1AllocateReadBuffer, METH_O, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_1AllocateReadBuffer(PyObject *__pyx_self, PyObject *__pyx_arg_size) {
+ int __pyx_v_size;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("AllocateReadBuffer");
+ __pyx_self = __pyx_self;
+ assert(__pyx_arg_size); {
+ __pyx_v_size = __Pyx_PyInt_AsInt(__pyx_arg_size); if (unlikely((__pyx_v_size == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.AllocateReadBuffer", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":280
+ *
+ * def AllocateReadBuffer(int size):
+ * return PyBuffer_New(size) # <<<<<<<<<<<<<<
+ *
+ * def maxAddrLen(long s):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = PyBuffer_New(__pyx_v_size); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 280; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_r = __pyx_t_1;
+ __pyx_t_1 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_AddTraceback("iocpsupport.AllocateReadBuffer", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":282
+ * return PyBuffer_New(size)
+ *
+ * def maxAddrLen(long s): # <<<<<<<<<<<<<<
+ * cdef WSAPROTOCOL_INFO wsa_pi
+ * cdef int size, rc
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_2maxAddrLen(PyObject *__pyx_self, PyObject *__pyx_arg_s); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_2maxAddrLen = {__Pyx_NAMESTR("maxAddrLen"), (PyCFunction)__pyx_pf_11iocpsupport_2maxAddrLen, METH_O, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_2maxAddrLen(PyObject *__pyx_self, PyObject *__pyx_arg_s) {
+ long __pyx_v_s;
+ WSAPROTOCOL_INFO __pyx_v_wsa_pi;
+ int __pyx_v_size;
+ int __pyx_v_rc;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("maxAddrLen");
+ __pyx_self = __pyx_self;
+ assert(__pyx_arg_s); {
+ __pyx_v_s = __Pyx_PyInt_AsLong(__pyx_arg_s); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.maxAddrLen", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "iocpsupport.pyx":286
+ * cdef int size, rc
+ *
+ * size = sizeof(wsa_pi) # <<<<<<<<<<<<<<
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_size = (sizeof(__pyx_v_wsa_pi));
+
+ /* "iocpsupport.pyx":287
+ *
+ * size = sizeof(wsa_pi)
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size) # <<<<<<<<<<<<<<
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ */
+ __pyx_v_rc = getsockopt(__pyx_v_s, SOL_SOCKET, SO_PROTOCOL_INFO, ((char *)(&__pyx_v_wsa_pi)), (&__pyx_v_size));
+
+ /* "iocpsupport.pyx":288
+ * size = sizeof(wsa_pi)
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ * return wsa_pi.iMaxSockAddr
+ */
+ __pyx_t_1 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":289
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt') # <<<<<<<<<<<<<<
+ * return wsa_pi.iMaxSockAddr
+ *
+ */
+ __pyx_t_2 = ((PyObject *)__pyx_n_s__getsockopt);
+ __Pyx_INCREF(__pyx_t_2);
+ __pyx_f_11iocpsupport_raise_error(WSAGetLastError(), __pyx_t_2); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 289; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ /* "iocpsupport.pyx":290
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ * return wsa_pi.iMaxSockAddr # <<<<<<<<<<<<<<
+ *
+ * cdef int getAddrFamily(SOCKET s) except *:
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = PyInt_FromLong(__pyx_v_wsa_pi.iMaxSockAddr); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 290; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_r = __pyx_t_2;
+ __pyx_t_2 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("iocpsupport.maxAddrLen", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "iocpsupport.pyx":292
+ * return wsa_pi.iMaxSockAddr
+ *
+ * cdef int getAddrFamily(SOCKET s) except *: # <<<<<<<<<<<<<<
+ * cdef WSAPROTOCOL_INFO wsa_pi
+ * cdef int size, rc
+ */
+
+static int __pyx_f_11iocpsupport_getAddrFamily(__pyx_t_11iocpsupport_SOCKET __pyx_v_s) {
+ WSAPROTOCOL_INFO __pyx_v_wsa_pi;
+ int __pyx_v_size;
+ int __pyx_v_rc;
+ int __pyx_r;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("getAddrFamily");
+
+ /* "iocpsupport.pyx":296
+ * cdef int size, rc
+ *
+ * size = sizeof(wsa_pi) # <<<<<<<<<<<<<<
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_size = (sizeof(__pyx_v_wsa_pi));
+
+ /* "iocpsupport.pyx":297
+ *
+ * size = sizeof(wsa_pi)
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size) # <<<<<<<<<<<<<<
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ */
+ __pyx_v_rc = getsockopt(__pyx_v_s, SOL_SOCKET, SO_PROTOCOL_INFO, ((char *)(&__pyx_v_wsa_pi)), (&__pyx_v_size));
+
+ /* "iocpsupport.pyx":298
+ * size = sizeof(wsa_pi)
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ * return wsa_pi.iAddressFamily
+ */
+ __pyx_t_1 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_1) {
+
+ /* "iocpsupport.pyx":299
+ * rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt') # <<<<<<<<<<<<<<
+ * return wsa_pi.iAddressFamily
+ *
+ */
+ __pyx_t_2 = ((PyObject *)__pyx_n_s__getsockopt);
+ __Pyx_INCREF(__pyx_t_2);
+ __pyx_f_11iocpsupport_raise_error(WSAGetLastError(), __pyx_t_2); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 299; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ goto __pyx_L3;
+ }
+ __pyx_L3:;
+
+ /* "iocpsupport.pyx":300
+ * if rc == SOCKET_ERROR:
+ * raise_error(WSAGetLastError(), 'getsockopt')
+ * return wsa_pi.iAddressFamily # <<<<<<<<<<<<<<
+ *
+ * import socket # for WSAStartup
+ */
+ __pyx_r = __pyx_v_wsa_pi.iAddressFamily;
+ goto __pyx_L0;
+
+ __pyx_r = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("iocpsupport.getAddrFamily", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = 0;
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":5
+ *
+ *
+ * def accept(long listening, long accepting, object buff, object obj): # <<<<<<<<<<<<<<
+ * """
+ * CAUTION: unlike system AcceptEx(), this function returns 0 on success
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_3accept(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_11iocpsupport_3accept[] = "\n CAUTION: unlike system AcceptEx(), this function returns 0 on success\n ";
+static PyMethodDef __pyx_mdef_11iocpsupport_3accept = {__Pyx_NAMESTR("accept"), (PyCFunction)__pyx_pf_11iocpsupport_3accept, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_11iocpsupport_3accept)};
+static PyObject *__pyx_pf_11iocpsupport_3accept(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_listening;
+ long __pyx_v_accepting;
+ PyObject *__pyx_v_buff = 0;
+ PyObject *__pyx_v_obj = 0;
+ unsigned long __pyx_v_bytes;
+ int __pyx_v_rc;
+ Py_ssize_t __pyx_v_size;
+ void *__pyx_v_mem_buffer;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_2;
+ int __pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__listening,&__pyx_n_s__accepting,&__pyx_n_s__buff,&__pyx_n_s__obj,0};
+ __Pyx_RefNannySetupContext("accept");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[4] = {0,0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__listening);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__accepting);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("accept", 1, 4, 4, 1); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__buff);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("accept", 1, 4, 4, 2); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 3:
+ values[3] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[3])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("accept", 1, 4, 4, 3); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "accept") < 0)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 4) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ }
+ __pyx_v_listening = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_listening == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_accepting = __Pyx_PyInt_AsLong(values[1]); if (unlikely((__pyx_v_accepting == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_buff = values[2];
+ __pyx_v_obj = values[3];
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("accept", 1, 4, 4, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.accept", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":15
+ * cdef myOVERLAPPED *ov
+ *
+ * PyObject_AsWriteBuffer(buff, &mem_buffer, &size) # <<<<<<<<<<<<<<
+ *
+ * ov = makeOV()
+ */
+ __pyx_t_1 = PyObject_AsWriteBuffer(__pyx_v_buff, (&__pyx_v_mem_buffer), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 15; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":17
+ * PyObject_AsWriteBuffer(buff, &mem_buffer, &size)
+ *
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_2 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_2 == NULL)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 17; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ov = __pyx_t_2;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":18
+ *
+ * ov = makeOV()
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ *
+ */
+ __pyx_t_3 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":19
+ * ov = makeOV()
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ *
+ * rc = lpAcceptEx(listening, accepting, mem_buffer, 0,
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":23
+ * rc = lpAcceptEx(listening, accepting, mem_buffer, 0,
+ * <DWORD>size / 2, <DWORD>size / 2,
+ * &bytes, <OVERLAPPED *>ov) # <<<<<<<<<<<<<<
+ * if not rc:
+ * rc = WSAGetLastError()
+ */
+ __pyx_v_rc = lpAcceptEx(__pyx_v_listening, __pyx_v_accepting, __pyx_v_mem_buffer, 0, (((__pyx_t_11iocpsupport_DWORD)__pyx_v_size) / 2), (((__pyx_t_11iocpsupport_DWORD)__pyx_v_size) / 2), (&__pyx_v_bytes), ((OVERLAPPED *)__pyx_v_ov));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":24
+ * <DWORD>size / 2, <DWORD>size / 2,
+ * &bytes, <OVERLAPPED *>ov)
+ * if not rc: # <<<<<<<<<<<<<<
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ */
+ __pyx_t_3 = (!__pyx_v_rc);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":25
+ * &bytes, <OVERLAPPED *>ov)
+ * if not rc:
+ * rc = WSAGetLastError() # <<<<<<<<<<<<<<
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ */
+ __pyx_v_rc = WSAGetLastError();
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":26
+ * if not rc:
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING: # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * return rc
+ */
+ __pyx_t_3 = (__pyx_v_rc != ERROR_IO_PENDING);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":27
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * return rc
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":28
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ * return rc # <<<<<<<<<<<<<<
+ *
+ * # operation is in progress
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_4 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 28; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_r = __pyx_t_4;
+ __pyx_t_4 = 0;
+ goto __pyx_L0;
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":31
+ *
+ * # operation is in progress
+ * Py_XINCREF(obj) # <<<<<<<<<<<<<<
+ * return 0
+ *
+ */
+ Py_XINCREF(__pyx_v_obj);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":32
+ * # operation is in progress
+ * Py_XINCREF(obj)
+ * return 0 # <<<<<<<<<<<<<<
+ *
+ * def get_accept_addrs(long s, object buff):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __Pyx_INCREF(__pyx_int_0);
+ __pyx_r = __pyx_int_0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("iocpsupport.accept", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":34
+ * return 0
+ *
+ * def get_accept_addrs(long s, object buff): # <<<<<<<<<<<<<<
+ * cdef WSAPROTOCOL_INFO wsa_pi
+ * cdef int locallen, remotelen
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_4get_accept_addrs(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_4get_accept_addrs = {__Pyx_NAMESTR("get_accept_addrs"), (PyCFunction)__pyx_pf_11iocpsupport_4get_accept_addrs, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_4get_accept_addrs(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_s;
+ PyObject *__pyx_v_buff = 0;
+ int __pyx_v_locallen;
+ int __pyx_v_remotelen;
+ Py_ssize_t __pyx_v_size;
+ void *__pyx_v_mem_buffer;
+ struct sockaddr *__pyx_v_localaddr;
+ struct sockaddr *__pyx_v_remoteaddr;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__s,&__pyx_n_s__buff,0};
+ __Pyx_RefNannySetupContext("get_accept_addrs");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__s);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__buff);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("get_accept_addrs", 1, 2, 2, 1); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "get_accept_addrs") < 0)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ }
+ __pyx_v_s = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_buff = values[1];
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("get_accept_addrs", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.get_accept_addrs", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":41
+ * cdef sockaddr *localaddr, *remoteaddr
+ *
+ * PyObject_AsReadBuffer(buff, &mem_buffer, &size) # <<<<<<<<<<<<<<
+ *
+ * lpGetAcceptExSockaddrs(mem_buffer, 0, <DWORD>size / 2, <DWORD>size / 2,
+ */
+ __pyx_t_1 = PyObject_AsReadBuffer(__pyx_v_buff, (&__pyx_v_mem_buffer), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":44
+ *
+ * lpGetAcceptExSockaddrs(mem_buffer, 0, <DWORD>size / 2, <DWORD>size / 2,
+ * &localaddr, &locallen, &remoteaddr, &remotelen) # <<<<<<<<<<<<<<
+ * return remoteaddr.sa_family, _makesockaddr(localaddr, locallen), _makesockaddr(remoteaddr, remotelen)
+ *
+ */
+ lpGetAcceptExSockaddrs(__pyx_v_mem_buffer, 0, (((__pyx_t_11iocpsupport_DWORD)__pyx_v_size) / 2), (((__pyx_t_11iocpsupport_DWORD)__pyx_v_size) / 2), (&__pyx_v_localaddr), (&__pyx_v_locallen), (&__pyx_v_remoteaddr), (&__pyx_v_remotelen));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":45
+ * lpGetAcceptExSockaddrs(mem_buffer, 0, <DWORD>size / 2, <DWORD>size / 2,
+ * &localaddr, &locallen, &remoteaddr, &remotelen)
+ * return remoteaddr.sa_family, _makesockaddr(localaddr, locallen), _makesockaddr(remoteaddr, remotelen) # <<<<<<<<<<<<<<
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_2 = PyInt_FromLong(__pyx_v_remoteaddr->sa_family); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 45; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = __pyx_f_11iocpsupport__makesockaddr(__pyx_v_localaddr, __pyx_v_locallen); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 45; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = __pyx_f_11iocpsupport__makesockaddr(__pyx_v_remoteaddr, __pyx_v_remotelen); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 45; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_5 = PyTuple_New(3); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 45; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ PyTuple_SET_ITEM(__pyx_t_5, 2, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_4 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_5);
+ __pyx_t_5 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("iocpsupport.get_accept_addrs", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":5
+ *
+ *
+ * def connect(long s, object addr, object obj): # <<<<<<<<<<<<<<
+ * """
+ * CAUTION: unlike system ConnectEx(), this function returns 0 on success
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_5connect(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_11iocpsupport_5connect[] = "\n CAUTION: unlike system ConnectEx(), this function returns 0 on success\n ";
+static PyMethodDef __pyx_mdef_11iocpsupport_5connect = {__Pyx_NAMESTR("connect"), (PyCFunction)__pyx_pf_11iocpsupport_5connect, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_11iocpsupport_5connect)};
+static PyObject *__pyx_pf_11iocpsupport_5connect(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_s;
+ PyObject *__pyx_v_addr = 0;
+ PyObject *__pyx_v_obj = 0;
+ int __pyx_v_family;
+ int __pyx_v_rc;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ struct sockaddr_in __pyx_v_ipv4_name;
+ struct sockaddr_in6 __pyx_v_ipv6_name;
+ struct sockaddr *__pyx_v_name;
+ int __pyx_v_namelen;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_t_2;
+ int __pyx_t_3;
+ int __pyx_t_4;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_5;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__s,&__pyx_n_s__addr,&__pyx_n_s__obj,0};
+ __Pyx_RefNannySetupContext("connect");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[3] = {0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__s);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__addr);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("connect", 1, 3, 3, 1); {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("connect", 1, 3, 3, 2); {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "connect") < 0)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ }
+ __pyx_v_s = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_addr = values[1];
+ __pyx_v_obj = values[2];
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("connect", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.connect", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":16
+ * cdef int namelen
+ *
+ * if not have_connectex: # <<<<<<<<<<<<<<
+ * raise ValueError, 'ConnectEx is not available on this system'
+ *
+ */
+ __pyx_t_1 = __Pyx_GetName(__pyx_m, __pyx_n_s__have_connectex); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 16; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely(__pyx_t_2 < 0)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 16; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_3 = (!__pyx_t_2);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":17
+ *
+ * if not have_connectex:
+ * raise ValueError, 'ConnectEx is not available on this system' # <<<<<<<<<<<<<<
+ *
+ * family = getAddrFamily(s)
+ */
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_kp_s_12), 0, 0);
+ {__pyx_filename = __pyx_f[2]; __pyx_lineno = 17; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":19
+ * raise ValueError, 'ConnectEx is not available on this system'
+ *
+ * family = getAddrFamily(s) # <<<<<<<<<<<<<<
+ * if family == AF_INET:
+ * name = <sockaddr *>&ipv4_name
+ */
+ __pyx_t_4 = __pyx_f_11iocpsupport_getAddrFamily(__pyx_v_s); if (unlikely(PyErr_Occurred())) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 19; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_family = __pyx_t_4;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":24
+ * namelen = sizeof(ipv4_name)
+ * fillinetaddr(&ipv4_name, addr)
+ * elif family == AF_INET6: # <<<<<<<<<<<<<<
+ * name = <sockaddr *>&ipv6_name
+ * namelen = sizeof(ipv6_name)
+ */
+ switch (__pyx_v_family) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":20
+ *
+ * family = getAddrFamily(s)
+ * if family == AF_INET: # <<<<<<<<<<<<<<
+ * name = <sockaddr *>&ipv4_name
+ * namelen = sizeof(ipv4_name)
+ */
+ case AF_INET:
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":21
+ * family = getAddrFamily(s)
+ * if family == AF_INET:
+ * name = <sockaddr *>&ipv4_name # <<<<<<<<<<<<<<
+ * namelen = sizeof(ipv4_name)
+ * fillinetaddr(&ipv4_name, addr)
+ */
+ __pyx_v_name = ((struct sockaddr *)(&__pyx_v_ipv4_name));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":22
+ * if family == AF_INET:
+ * name = <sockaddr *>&ipv4_name
+ * namelen = sizeof(ipv4_name) # <<<<<<<<<<<<<<
+ * fillinetaddr(&ipv4_name, addr)
+ * elif family == AF_INET6:
+ */
+ __pyx_v_namelen = (sizeof(__pyx_v_ipv4_name));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":23
+ * name = <sockaddr *>&ipv4_name
+ * namelen = sizeof(ipv4_name)
+ * fillinetaddr(&ipv4_name, addr) # <<<<<<<<<<<<<<
+ * elif family == AF_INET6:
+ * name = <sockaddr *>&ipv6_name
+ */
+ __pyx_t_1 = __pyx_f_11iocpsupport_fillinetaddr((&__pyx_v_ipv4_name), __pyx_v_addr); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 23; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ break;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":24
+ * namelen = sizeof(ipv4_name)
+ * fillinetaddr(&ipv4_name, addr)
+ * elif family == AF_INET6: # <<<<<<<<<<<<<<
+ * name = <sockaddr *>&ipv6_name
+ * namelen = sizeof(ipv6_name)
+ */
+ case AF_INET6:
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":25
+ * fillinetaddr(&ipv4_name, addr)
+ * elif family == AF_INET6:
+ * name = <sockaddr *>&ipv6_name # <<<<<<<<<<<<<<
+ * namelen = sizeof(ipv6_name)
+ * fillinet6addr(&ipv6_name, addr)
+ */
+ __pyx_v_name = ((struct sockaddr *)(&__pyx_v_ipv6_name));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":26
+ * elif family == AF_INET6:
+ * name = <sockaddr *>&ipv6_name
+ * namelen = sizeof(ipv6_name) # <<<<<<<<<<<<<<
+ * fillinet6addr(&ipv6_name, addr)
+ * else:
+ */
+ __pyx_v_namelen = (sizeof(__pyx_v_ipv6_name));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":27
+ * name = <sockaddr *>&ipv6_name
+ * namelen = sizeof(ipv6_name)
+ * fillinet6addr(&ipv6_name, addr) # <<<<<<<<<<<<<<
+ * else:
+ * raise ValueError, 'unsupported address family'
+ */
+ __pyx_t_1 = __pyx_f_11iocpsupport_fillinet6addr((&__pyx_v_ipv6_name), __pyx_v_addr); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 27; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ break;
+ default:
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":29
+ * fillinet6addr(&ipv6_name, addr)
+ * else:
+ * raise ValueError, 'unsupported address family' # <<<<<<<<<<<<<<
+ * name.sa_family = family
+ *
+ */
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_kp_s_13), 0, 0);
+ {__pyx_filename = __pyx_f[2]; __pyx_lineno = 29; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ break;
+ }
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":30
+ * else:
+ * raise ValueError, 'unsupported address family'
+ * name.sa_family = family # <<<<<<<<<<<<<<
+ *
+ * ov = makeOV()
+ */
+ __pyx_v_name->sa_family = __pyx_v_family;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":32
+ * name.sa_family = family
+ *
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_5 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_5 == NULL)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 32; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ov = __pyx_t_5;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":33
+ *
+ * ov = makeOV()
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ *
+ */
+ __pyx_t_3 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":34
+ * ov = makeOV()
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ *
+ * rc = lpConnectEx(s, name, namelen, NULL, 0, NULL, <OVERLAPPED *>ov)
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":36
+ * ov.obj = <PyObject *>obj
+ *
+ * rc = lpConnectEx(s, name, namelen, NULL, 0, NULL, <OVERLAPPED *>ov) # <<<<<<<<<<<<<<
+ *
+ * if not rc:
+ */
+ __pyx_v_rc = lpConnectEx(__pyx_v_s, __pyx_v_name, __pyx_v_namelen, NULL, 0, NULL, ((OVERLAPPED *)__pyx_v_ov));
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":38
+ * rc = lpConnectEx(s, name, namelen, NULL, 0, NULL, <OVERLAPPED *>ov)
+ *
+ * if not rc: # <<<<<<<<<<<<<<
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ */
+ __pyx_t_3 = (!__pyx_v_rc);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":39
+ *
+ * if not rc:
+ * rc = WSAGetLastError() # <<<<<<<<<<<<<<
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ */
+ __pyx_v_rc = WSAGetLastError();
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":40
+ * if not rc:
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING: # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * return rc
+ */
+ __pyx_t_3 = (__pyx_v_rc != ERROR_IO_PENDING);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":41
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * return rc
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":42
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ * return rc # <<<<<<<<<<<<<<
+ *
+ * # operation is in progress
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 42; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_r = __pyx_t_1;
+ __pyx_t_1 = 0;
+ goto __pyx_L0;
+ goto __pyx_L9;
+ }
+ __pyx_L9:;
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":45
+ *
+ * # operation is in progress
+ * Py_XINCREF(obj) # <<<<<<<<<<<<<<
+ * return 0
+ *
+ */
+ Py_XINCREF(__pyx_v_obj);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":46
+ * # operation is in progress
+ * Py_XINCREF(obj)
+ * return 0 # <<<<<<<<<<<<<<
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __Pyx_INCREF(__pyx_int_0);
+ __pyx_r = __pyx_int_0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_AddTraceback("iocpsupport.connect", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":5
+ *
+ *
+ * def recv(long s, object bufflist, object obj, unsigned long flags = 0): # <<<<<<<<<<<<<<
+ * cdef int rc, res
+ * cdef myOVERLAPPED *ov
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_6recv(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_6recv = {__Pyx_NAMESTR("recv"), (PyCFunction)__pyx_pf_11iocpsupport_6recv, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_6recv(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_s;
+ PyObject *__pyx_v_bufflist = 0;
+ PyObject *__pyx_v_obj = 0;
+ unsigned long __pyx_v_flags;
+ int __pyx_v_rc;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ WSABUF *__pyx_v_ws_buf;
+ unsigned long __pyx_v_bytes;
+ struct PyObject **__pyx_v_buffers;
+ Py_ssize_t __pyx_v_i;
+ Py_ssize_t __pyx_v_size;
+ Py_ssize_t __pyx_v_buffcount;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ void *__pyx_t_2;
+ Py_ssize_t __pyx_t_3;
+ int __pyx_t_4;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_5;
+ int __pyx_t_6;
+ PyObject *__pyx_t_7 = NULL;
+ PyObject *__pyx_t_8 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__s,&__pyx_n_s__bufflist,&__pyx_n_s__obj,&__pyx_n_s__flags,0};
+ __Pyx_RefNannySetupContext("recv");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[4] = {0,0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__s);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__bufflist);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recv", 0, 3, 4, 1); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recv", 0, 3, 4, 2); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 3:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__flags);
+ if (value) { values[3] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "recv") < 0)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ __pyx_v_s = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_bufflist = values[1];
+ __pyx_v_obj = values[2];
+ if (values[3]) {
+ __pyx_v_flags = __Pyx_PyInt_AsUnsignedLong(values[3]); if (unlikely((__pyx_v_flags == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_flags = ((unsigned long)0);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("recv", 0, 3, 4, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.recv", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+ __Pyx_INCREF(__pyx_v_bufflist);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":13
+ * cdef Py_ssize_t i, size, buffcount
+ *
+ * bufflist = PySequence_Fast(bufflist, 'second argument needs to be a list') # <<<<<<<<<<<<<<
+ * buffcount = PySequence_Fast_GET_SIZE(bufflist)
+ * buffers = PySequence_Fast_ITEMS(bufflist)
+ */
+ __pyx_t_1 = PySequence_Fast(__pyx_v_bufflist, __pyx_k_14); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 13; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __Pyx_DECREF(__pyx_v_bufflist);
+ __pyx_v_bufflist = __pyx_t_1;
+ __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":14
+ *
+ * bufflist = PySequence_Fast(bufflist, 'second argument needs to be a list')
+ * buffcount = PySequence_Fast_GET_SIZE(bufflist) # <<<<<<<<<<<<<<
+ * buffers = PySequence_Fast_ITEMS(bufflist)
+ *
+ */
+ __pyx_v_buffcount = PySequence_Fast_GET_SIZE(__pyx_v_bufflist);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":15
+ * bufflist = PySequence_Fast(bufflist, 'second argument needs to be a list')
+ * buffcount = PySequence_Fast_GET_SIZE(bufflist)
+ * buffers = PySequence_Fast_ITEMS(bufflist) # <<<<<<<<<<<<<<
+ *
+ * ws_buf = <WSABUF *>PyMem_Malloc(buffcount*sizeof(WSABUF))
+ */
+ __pyx_v_buffers = PySequence_Fast_ITEMS(__pyx_v_bufflist);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":17
+ * buffers = PySequence_Fast_ITEMS(bufflist)
+ *
+ * ws_buf = <WSABUF *>PyMem_Malloc(buffcount*sizeof(WSABUF)) # <<<<<<<<<<<<<<
+ *
+ * try:
+ */
+ __pyx_t_2 = PyMem_Malloc((__pyx_v_buffcount * (sizeof(WSABUF)))); if (unlikely(__pyx_t_2 == NULL)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 17; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ws_buf = ((WSABUF *)__pyx_t_2);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":19
+ * ws_buf = <WSABUF *>PyMem_Malloc(buffcount*sizeof(WSABUF))
+ *
+ * try: # <<<<<<<<<<<<<<
+ * for i from 0 <= i < buffcount:
+ * PyObject_AsWriteBuffer(<object>buffers[i], <void **>&ws_buf[i].buf, &size)
+ */
+ /*try:*/ {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":20
+ *
+ * try:
+ * for i from 0 <= i < buffcount: # <<<<<<<<<<<<<<
+ * PyObject_AsWriteBuffer(<object>buffers[i], <void **>&ws_buf[i].buf, &size)
+ * ws_buf[i].len = <DWORD>size
+ */
+ __pyx_t_3 = __pyx_v_buffcount;
+ for (__pyx_v_i = 0; __pyx_v_i < __pyx_t_3; __pyx_v_i++) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":21
+ * try:
+ * for i from 0 <= i < buffcount:
+ * PyObject_AsWriteBuffer(<object>buffers[i], <void **>&ws_buf[i].buf, &size) # <<<<<<<<<<<<<<
+ * ws_buf[i].len = <DWORD>size
+ *
+ */
+ __pyx_t_1 = ((PyObject *)(__pyx_v_buffers[__pyx_v_i]));
+ __Pyx_INCREF(__pyx_t_1);
+ __pyx_t_4 = PyObject_AsWriteBuffer(__pyx_t_1, ((void **)(&(__pyx_v_ws_buf[__pyx_v_i]).buf)), (&__pyx_v_size)); if (unlikely(__pyx_t_4 == -1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 21; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":22
+ * for i from 0 <= i < buffcount:
+ * PyObject_AsWriteBuffer(<object>buffers[i], <void **>&ws_buf[i].buf, &size)
+ * ws_buf[i].len = <DWORD>size # <<<<<<<<<<<<<<
+ *
+ * ov = makeOV()
+ */
+ (__pyx_v_ws_buf[__pyx_v_i]).len = ((__pyx_t_11iocpsupport_DWORD)__pyx_v_size);
+ }
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":24
+ * ws_buf[i].len = <DWORD>size
+ *
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_5 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_5 == NULL)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 24; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __pyx_v_ov = __pyx_t_5;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":25
+ *
+ * ov = makeOV()
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ *
+ */
+ __pyx_t_6 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_6) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":26
+ * ov = makeOV()
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ *
+ * rc = WSARecv(s, ws_buf, <DWORD>buffcount, &bytes, &flags, <OVERLAPPED *>ov, NULL)
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L11;
+ }
+ __pyx_L11:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":28
+ * ov.obj = <PyObject *>obj
+ *
+ * rc = WSARecv(s, ws_buf, <DWORD>buffcount, &bytes, &flags, <OVERLAPPED *>ov, NULL) # <<<<<<<<<<<<<<
+ *
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_rc = WSARecv(__pyx_v_s, __pyx_v_ws_buf, ((__pyx_t_11iocpsupport_DWORD)__pyx_v_buffcount), (&__pyx_v_bytes), (&__pyx_v_flags), ((OVERLAPPED *)__pyx_v_ov), NULL);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":30
+ * rc = WSARecv(s, ws_buf, <DWORD>buffcount, &bytes, &flags, <OVERLAPPED *>ov, NULL)
+ *
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ */
+ __pyx_t_6 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_6) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":31
+ *
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError() # <<<<<<<<<<<<<<
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ */
+ __pyx_v_rc = WSAGetLastError();
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":32
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING: # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * return rc, 0
+ */
+ __pyx_t_6 = (__pyx_v_rc != ERROR_IO_PENDING);
+ if (__pyx_t_6) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":33
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * return rc, 0
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":34
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ * return rc, 0 # <<<<<<<<<<<<<<
+ *
+ * Py_XINCREF(obj)
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_7 = PyTuple_New(2); if (unlikely(!__pyx_t_7)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_7));
+ PyTuple_SET_ITEM(__pyx_t_7, 0, __pyx_t_1);
+ __Pyx_GIVEREF(__pyx_t_1);
+ __Pyx_INCREF(__pyx_int_0);
+ PyTuple_SET_ITEM(__pyx_t_7, 1, __pyx_int_0);
+ __Pyx_GIVEREF(__pyx_int_0);
+ __pyx_t_1 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_7);
+ __pyx_t_7 = 0;
+ goto __pyx_L6;
+ goto __pyx_L13;
+ }
+ __pyx_L13:;
+ goto __pyx_L12;
+ }
+ __pyx_L12:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":36
+ * return rc, 0
+ *
+ * Py_XINCREF(obj) # <<<<<<<<<<<<<<
+ * return rc, bytes
+ * finally:
+ */
+ Py_XINCREF(__pyx_v_obj);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":37
+ *
+ * Py_XINCREF(obj)
+ * return rc, bytes # <<<<<<<<<<<<<<
+ * finally:
+ * PyMem_Free(ws_buf)
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_7 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_7)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 37; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_GOTREF(__pyx_t_7);
+ __pyx_t_1 = PyLong_FromUnsignedLong(__pyx_v_bytes); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 37; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_8 = PyTuple_New(2); if (unlikely(!__pyx_t_8)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 37; __pyx_clineno = __LINE__; goto __pyx_L7;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_8));
+ PyTuple_SET_ITEM(__pyx_t_8, 0, __pyx_t_7);
+ __Pyx_GIVEREF(__pyx_t_7);
+ PyTuple_SET_ITEM(__pyx_t_8, 1, __pyx_t_1);
+ __Pyx_GIVEREF(__pyx_t_1);
+ __pyx_t_7 = 0;
+ __pyx_t_1 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_8);
+ __pyx_t_8 = 0;
+ goto __pyx_L6;
+ }
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":39
+ * return rc, bytes
+ * finally:
+ * PyMem_Free(ws_buf) # <<<<<<<<<<<<<<
+ *
+ * def recvfrom(long s, object buff, object addr_buff, object addr_len_buff, object obj, unsigned long flags = 0):
+ */
+ /*finally:*/ {
+ int __pyx_why;
+ PyObject *__pyx_exc_type, *__pyx_exc_value, *__pyx_exc_tb;
+ int __pyx_exc_lineno;
+ __pyx_exc_type = 0; __pyx_exc_value = 0; __pyx_exc_tb = 0; __pyx_exc_lineno = 0;
+ __pyx_why = 0; goto __pyx_L8;
+ __pyx_L6: __pyx_exc_type = 0; __pyx_exc_value = 0; __pyx_exc_tb = 0; __pyx_exc_lineno = 0;
+ __pyx_why = 3; goto __pyx_L8;
+ __pyx_L7: {
+ __pyx_why = 4;
+ __Pyx_XDECREF(__pyx_t_7); __pyx_t_7 = 0;
+ __Pyx_XDECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __Pyx_XDECREF(__pyx_t_8); __pyx_t_8 = 0;
+ __Pyx_ErrFetch(&__pyx_exc_type, &__pyx_exc_value, &__pyx_exc_tb);
+ __pyx_exc_lineno = __pyx_lineno;
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+ PyMem_Free(__pyx_v_ws_buf);
+ switch (__pyx_why) {
+ case 3: goto __pyx_L0;
+ case 4: {
+ __Pyx_ErrRestore(__pyx_exc_type, __pyx_exc_value, __pyx_exc_tb);
+ __pyx_lineno = __pyx_exc_lineno;
+ __pyx_exc_type = 0;
+ __pyx_exc_value = 0;
+ __pyx_exc_tb = 0;
+ goto __pyx_L1_error;
+ }
+ }
+ }
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_7);
+ __Pyx_XDECREF(__pyx_t_8);
+ __Pyx_AddTraceback("iocpsupport.recv", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_bufflist);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":41
+ * PyMem_Free(ws_buf)
+ *
+ * def recvfrom(long s, object buff, object addr_buff, object addr_len_buff, object obj, unsigned long flags = 0): # <<<<<<<<<<<<<<
+ * cdef int rc, c_addr_buff_len, c_addr_len_buff_len
+ * cdef myOVERLAPPED *ov
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_7recvfrom(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_7recvfrom = {__Pyx_NAMESTR("recvfrom"), (PyCFunction)__pyx_pf_11iocpsupport_7recvfrom, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_7recvfrom(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_s;
+ PyObject *__pyx_v_buff = 0;
+ PyObject *__pyx_v_addr_buff = 0;
+ PyObject *__pyx_v_addr_len_buff = 0;
+ PyObject *__pyx_v_obj = 0;
+ unsigned long __pyx_v_flags;
+ int __pyx_v_rc;
+ int __pyx_v_c_addr_buff_len;
+ int __pyx_v_c_addr_len_buff_len;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ WSABUF __pyx_v_ws_buf;
+ unsigned long __pyx_v_bytes;
+ struct sockaddr *__pyx_v_c_addr_buff;
+ int *__pyx_v_c_addr_len_buff;
+ Py_ssize_t __pyx_v_size;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ int __pyx_t_2;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ PyObject *__pyx_t_6 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__s,&__pyx_n_s__buff,&__pyx_n_s__addr_buff,&__pyx_n_s__addr_len_buff,&__pyx_n_s__obj,&__pyx_n_s__flags,0};
+ __Pyx_RefNannySetupContext("recvfrom");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[6] = {0,0,0,0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 6: values[5] = PyTuple_GET_ITEM(__pyx_args, 5);
+ case 5: values[4] = PyTuple_GET_ITEM(__pyx_args, 4);
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__s);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__buff);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recvfrom", 0, 5, 6, 1); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__addr_buff);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recvfrom", 0, 5, 6, 2); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 3:
+ values[3] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__addr_len_buff);
+ if (likely(values[3])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recvfrom", 0, 5, 6, 3); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 4:
+ values[4] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[4])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("recvfrom", 0, 5, 6, 4); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 5:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__flags);
+ if (value) { values[5] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "recvfrom") < 0)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 6: values[5] = PyTuple_GET_ITEM(__pyx_args, 5);
+ case 5: values[4] = PyTuple_GET_ITEM(__pyx_args, 4);
+ values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ __pyx_v_s = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_buff = values[1];
+ __pyx_v_addr_buff = values[2];
+ __pyx_v_addr_len_buff = values[3];
+ __pyx_v_obj = values[4];
+ if (values[5]) {
+ __pyx_v_flags = __Pyx_PyInt_AsUnsignedLong(values[5]); if (unlikely((__pyx_v_flags == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_flags = ((unsigned long)0);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("recvfrom", 0, 5, 6, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.recvfrom", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":50
+ * cdef Py_ssize_t size
+ *
+ * PyObject_AsWriteBuffer(buff, <void **>&ws_buf.buf, &size) # <<<<<<<<<<<<<<
+ * ws_buf.len = <DWORD>size
+ * PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size)
+ */
+ __pyx_t_1 = PyObject_AsWriteBuffer(__pyx_v_buff, ((void **)(&__pyx_v_ws_buf.buf)), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 50; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":51
+ *
+ * PyObject_AsWriteBuffer(buff, <void **>&ws_buf.buf, &size)
+ * ws_buf.len = <DWORD>size # <<<<<<<<<<<<<<
+ * PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size)
+ * c_addr_buff_len = <int>size
+ */
+ __pyx_v_ws_buf.len = ((__pyx_t_11iocpsupport_DWORD)__pyx_v_size);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":52
+ * PyObject_AsWriteBuffer(buff, <void **>&ws_buf.buf, &size)
+ * ws_buf.len = <DWORD>size
+ * PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size) # <<<<<<<<<<<<<<
+ * c_addr_buff_len = <int>size
+ * PyObject_AsWriteBuffer(addr_len_buff, <void **>&c_addr_len_buff, &size)
+ */
+ __pyx_t_1 = PyObject_AsWriteBuffer(__pyx_v_addr_buff, ((void **)(&__pyx_v_c_addr_buff)), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 52; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":53
+ * ws_buf.len = <DWORD>size
+ * PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size)
+ * c_addr_buff_len = <int>size # <<<<<<<<<<<<<<
+ * PyObject_AsWriteBuffer(addr_len_buff, <void **>&c_addr_len_buff, &size)
+ * c_addr_len_buff_len = <int>size
+ */
+ __pyx_v_c_addr_buff_len = ((int)__pyx_v_size);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":54
+ * PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size)
+ * c_addr_buff_len = <int>size
+ * PyObject_AsWriteBuffer(addr_len_buff, <void **>&c_addr_len_buff, &size) # <<<<<<<<<<<<<<
+ * c_addr_len_buff_len = <int>size
+ *
+ */
+ __pyx_t_1 = PyObject_AsWriteBuffer(__pyx_v_addr_len_buff, ((void **)(&__pyx_v_c_addr_len_buff)), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 54; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":55
+ * c_addr_buff_len = <int>size
+ * PyObject_AsWriteBuffer(addr_len_buff, <void **>&c_addr_len_buff, &size)
+ * c_addr_len_buff_len = <int>size # <<<<<<<<<<<<<<
+ *
+ * if c_addr_len_buff_len != sizeof(int):
+ */
+ __pyx_v_c_addr_len_buff_len = ((int)__pyx_v_size);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":57
+ * c_addr_len_buff_len = <int>size
+ *
+ * if c_addr_len_buff_len != sizeof(int): # <<<<<<<<<<<<<<
+ * raise ValueError, 'length of address length buffer needs to be sizeof(int)'
+ *
+ */
+ __pyx_t_2 = (__pyx_v_c_addr_len_buff_len != (sizeof(int)));
+ if (__pyx_t_2) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":58
+ *
+ * if c_addr_len_buff_len != sizeof(int):
+ * raise ValueError, 'length of address length buffer needs to be sizeof(int)' # <<<<<<<<<<<<<<
+ *
+ * c_addr_len_buff[0] = c_addr_buff_len
+ */
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_kp_s_15), 0, 0);
+ {__pyx_filename = __pyx_f[3]; __pyx_lineno = 58; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":60
+ * raise ValueError, 'length of address length buffer needs to be sizeof(int)'
+ *
+ * c_addr_len_buff[0] = c_addr_buff_len # <<<<<<<<<<<<<<
+ *
+ * ov = makeOV()
+ */
+ (__pyx_v_c_addr_len_buff[0]) = __pyx_v_c_addr_buff_len;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":62
+ * c_addr_len_buff[0] = c_addr_buff_len
+ *
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_3 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_3 == NULL)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 62; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ov = __pyx_t_3;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":63
+ *
+ * ov = makeOV()
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ *
+ */
+ __pyx_t_2 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_2) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":64
+ * ov = makeOV()
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ *
+ * rc = WSARecvFrom(s, &ws_buf, 1, &bytes, &flags, c_addr_buff, c_addr_len_buff, <OVERLAPPED *>ov, NULL)
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":66
+ * ov.obj = <PyObject *>obj
+ *
+ * rc = WSARecvFrom(s, &ws_buf, 1, &bytes, &flags, c_addr_buff, c_addr_len_buff, <OVERLAPPED *>ov, NULL) # <<<<<<<<<<<<<<
+ *
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_rc = WSARecvFrom(__pyx_v_s, (&__pyx_v_ws_buf), 1, (&__pyx_v_bytes), (&__pyx_v_flags), __pyx_v_c_addr_buff, __pyx_v_c_addr_len_buff, ((OVERLAPPED *)__pyx_v_ov), NULL);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":68
+ * rc = WSARecvFrom(s, &ws_buf, 1, &bytes, &flags, c_addr_buff, c_addr_len_buff, <OVERLAPPED *>ov, NULL)
+ *
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ */
+ __pyx_t_2 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_2) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":69
+ *
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError() # <<<<<<<<<<<<<<
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ */
+ __pyx_v_rc = WSAGetLastError();
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":70
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING: # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * return rc, 0
+ */
+ __pyx_t_2 = (__pyx_v_rc != ERROR_IO_PENDING);
+ if (__pyx_t_2) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":71
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * return rc, 0
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":72
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ * return rc, 0 # <<<<<<<<<<<<<<
+ *
+ * Py_XINCREF(obj)
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_4 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 72; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_5 = PyTuple_New(2); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 72; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ __Pyx_INCREF(__pyx_int_0);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, __pyx_int_0);
+ __Pyx_GIVEREF(__pyx_int_0);
+ __pyx_t_4 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_5);
+ __pyx_t_5 = 0;
+ goto __pyx_L0;
+ goto __pyx_L9;
+ }
+ __pyx_L9:;
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":74
+ * return rc, 0
+ *
+ * Py_XINCREF(obj) # <<<<<<<<<<<<<<
+ * return rc, bytes
+ *
+ */
+ Py_XINCREF(__pyx_v_obj);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":75
+ *
+ * Py_XINCREF(obj)
+ * return rc, bytes # <<<<<<<<<<<<<<
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_5 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 75; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_5);
+ __pyx_t_4 = PyLong_FromUnsignedLong(__pyx_v_bytes); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 75; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_6 = PyTuple_New(2); if (unlikely(!__pyx_t_6)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 75; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_6));
+ PyTuple_SET_ITEM(__pyx_t_6, 0, __pyx_t_5);
+ __Pyx_GIVEREF(__pyx_t_5);
+ PyTuple_SET_ITEM(__pyx_t_6, 1, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ __pyx_t_5 = 0;
+ __pyx_t_4 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_6);
+ __pyx_t_6 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_XDECREF(__pyx_t_6);
+ __Pyx_AddTraceback("iocpsupport.recvfrom", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":5
+ *
+ *
+ * def send(long s, object buff, object obj, unsigned long flags = 0): # <<<<<<<<<<<<<<
+ * cdef int rc
+ * cdef myOVERLAPPED *ov
+ */
+
+static PyObject *__pyx_pf_11iocpsupport_8send(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static PyMethodDef __pyx_mdef_11iocpsupport_8send = {__Pyx_NAMESTR("send"), (PyCFunction)__pyx_pf_11iocpsupport_8send, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)};
+static PyObject *__pyx_pf_11iocpsupport_8send(PyObject *__pyx_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ long __pyx_v_s;
+ PyObject *__pyx_v_buff = 0;
+ PyObject *__pyx_v_obj = 0;
+ unsigned long __pyx_v_flags;
+ int __pyx_v_rc;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_v_ov;
+ WSABUF __pyx_v_ws_buf;
+ unsigned long __pyx_v_bytes;
+ Py_ssize_t __pyx_v_size;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ struct __pyx_t_11iocpsupport_myOVERLAPPED *__pyx_t_2;
+ int __pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ PyObject *__pyx_t_6 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__s,&__pyx_n_s__buff,&__pyx_n_s__obj,&__pyx_n_s__flags,0};
+ __Pyx_RefNannySetupContext("send");
+ __pyx_self = __pyx_self;
+ {
+ PyObject* values[4] = {0,0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__s);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__buff);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("send", 0, 3, 4, 1); {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__obj);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("send", 0, 3, 4, 2); {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 3:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__flags);
+ if (value) { values[3] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "send") < 0)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 4: values[3] = PyTuple_GET_ITEM(__pyx_args, 3);
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ __pyx_v_s = __Pyx_PyInt_AsLong(values[0]); if (unlikely((__pyx_v_s == (long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_buff = values[1];
+ __pyx_v_obj = values[2];
+ if (values[3]) {
+ __pyx_v_flags = __Pyx_PyInt_AsUnsignedLong(values[3]); if (unlikely((__pyx_v_flags == (unsigned long)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_flags = ((unsigned long)0);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("send", 0, 3, 4, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("iocpsupport.send", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":12
+ * cdef Py_ssize_t size
+ *
+ * PyObject_AsReadBuffer(buff, <void **>&ws_buf.buf, &size) # <<<<<<<<<<<<<<
+ * ws_buf.len = <DWORD>size
+ *
+ */
+ __pyx_t_1 = PyObject_AsReadBuffer(__pyx_v_buff, ((void **)(&__pyx_v_ws_buf.buf)), (&__pyx_v_size)); if (unlikely(__pyx_t_1 == -1)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 12; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":13
+ *
+ * PyObject_AsReadBuffer(buff, <void **>&ws_buf.buf, &size)
+ * ws_buf.len = <DWORD>size # <<<<<<<<<<<<<<
+ *
+ * ov = makeOV()
+ */
+ __pyx_v_ws_buf.len = ((__pyx_t_11iocpsupport_DWORD)__pyx_v_size);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":15
+ * ws_buf.len = <DWORD>size
+ *
+ * ov = makeOV() # <<<<<<<<<<<<<<
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj
+ */
+ __pyx_t_2 = __pyx_f_11iocpsupport_makeOV(); if (unlikely(__pyx_t_2 == NULL)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 15; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_v_ov = __pyx_t_2;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":16
+ *
+ * ov = makeOV()
+ * if obj is not None: # <<<<<<<<<<<<<<
+ * ov.obj = <PyObject *>obj
+ *
+ */
+ __pyx_t_3 = (__pyx_v_obj != Py_None);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":17
+ * ov = makeOV()
+ * if obj is not None:
+ * ov.obj = <PyObject *>obj # <<<<<<<<<<<<<<
+ *
+ * rc = WSASend(s, &ws_buf, 1, &bytes, flags, <OVERLAPPED *>ov, NULL)
+ */
+ __pyx_v_ov->obj = ((struct PyObject *)__pyx_v_obj);
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":19
+ * ov.obj = <PyObject *>obj
+ *
+ * rc = WSASend(s, &ws_buf, 1, &bytes, flags, <OVERLAPPED *>ov, NULL) # <<<<<<<<<<<<<<
+ *
+ * if rc == SOCKET_ERROR:
+ */
+ __pyx_v_rc = WSASend(__pyx_v_s, (&__pyx_v_ws_buf), 1, (&__pyx_v_bytes), __pyx_v_flags, ((OVERLAPPED *)__pyx_v_ov), NULL);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":21
+ * rc = WSASend(s, &ws_buf, 1, &bytes, flags, <OVERLAPPED *>ov, NULL)
+ *
+ * if rc == SOCKET_ERROR: # <<<<<<<<<<<<<<
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ */
+ __pyx_t_3 = (__pyx_v_rc == SOCKET_ERROR);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":22
+ *
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError() # <<<<<<<<<<<<<<
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ */
+ __pyx_v_rc = WSAGetLastError();
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":23
+ * if rc == SOCKET_ERROR:
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING: # <<<<<<<<<<<<<<
+ * PyMem_Free(ov)
+ * return rc, bytes
+ */
+ __pyx_t_3 = (__pyx_v_rc != ERROR_IO_PENDING);
+ if (__pyx_t_3) {
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":24
+ * rc = WSAGetLastError()
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov) # <<<<<<<<<<<<<<
+ * return rc, bytes
+ *
+ */
+ PyMem_Free(__pyx_v_ov);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":25
+ * if rc != ERROR_IO_PENDING:
+ * PyMem_Free(ov)
+ * return rc, bytes # <<<<<<<<<<<<<<
+ *
+ * Py_XINCREF(obj)
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_4 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 25; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_5 = PyLong_FromUnsignedLong(__pyx_v_bytes); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 25; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_5);
+ __pyx_t_6 = PyTuple_New(2); if (unlikely(!__pyx_t_6)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 25; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_6));
+ PyTuple_SET_ITEM(__pyx_t_6, 0, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ PyTuple_SET_ITEM(__pyx_t_6, 1, __pyx_t_5);
+ __Pyx_GIVEREF(__pyx_t_5);
+ __pyx_t_4 = 0;
+ __pyx_t_5 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_6);
+ __pyx_t_6 = 0;
+ goto __pyx_L0;
+ goto __pyx_L8;
+ }
+ __pyx_L8:;
+ goto __pyx_L7;
+ }
+ __pyx_L7:;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":27
+ * return rc, bytes
+ *
+ * Py_XINCREF(obj) # <<<<<<<<<<<<<<
+ * return rc, bytes
+ *
+ */
+ Py_XINCREF(__pyx_v_obj);
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":28
+ *
+ * Py_XINCREF(obj)
+ * return rc, bytes # <<<<<<<<<<<<<<
+ *
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_6 = PyInt_FromLong(__pyx_v_rc); if (unlikely(!__pyx_t_6)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 28; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_6);
+ __pyx_t_5 = PyLong_FromUnsignedLong(__pyx_v_bytes); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 28; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_5);
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 28; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_6);
+ __Pyx_GIVEREF(__pyx_t_6);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, __pyx_t_5);
+ __Pyx_GIVEREF(__pyx_t_5);
+ __pyx_t_6 = 0;
+ __pyx_t_5 = 0;
+ __pyx_r = ((PyObject *)__pyx_t_4);
+ __pyx_t_4 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_XDECREF(__pyx_t_6);
+ __Pyx_AddTraceback("iocpsupport.send", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+static PyObject *__pyx_tp_new_11iocpsupport_CompletionPort(PyTypeObject *t, PyObject *a, PyObject *k) {
+ PyObject *o = (*t->tp_alloc)(t, 0);
+ if (!o) return 0;
+ return o;
+}
+
+static void __pyx_tp_dealloc_11iocpsupport_CompletionPort(PyObject *o) {
+ (*Py_TYPE(o)->tp_free)(o);
+}
+
+static PyMethodDef __pyx_methods_11iocpsupport_CompletionPort[] = {
+ {__Pyx_NAMESTR("addHandle"), (PyCFunction)__pyx_pf_11iocpsupport_14CompletionPort_1addHandle, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)},
+ {__Pyx_NAMESTR("getEvent"), (PyCFunction)__pyx_pf_11iocpsupport_14CompletionPort_2getEvent, METH_O, __Pyx_DOCSTR(0)},
+ {__Pyx_NAMESTR("postEvent"), (PyCFunction)__pyx_pf_11iocpsupport_14CompletionPort_3postEvent, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(0)},
+ {__Pyx_NAMESTR("__del__"), (PyCFunction)__pyx_pf_11iocpsupport_14CompletionPort_4__del__, METH_NOARGS, __Pyx_DOCSTR(0)},
+ {0, 0, 0, 0}
+};
+
+static PyNumberMethods __pyx_tp_as_number_CompletionPort = {
+ 0, /*nb_add*/
+ 0, /*nb_subtract*/
+ 0, /*nb_multiply*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_divide*/
+ #endif
+ 0, /*nb_remainder*/
+ 0, /*nb_divmod*/
+ 0, /*nb_power*/
+ 0, /*nb_negative*/
+ 0, /*nb_positive*/
+ 0, /*nb_absolute*/
+ 0, /*nb_nonzero*/
+ 0, /*nb_invert*/
+ 0, /*nb_lshift*/
+ 0, /*nb_rshift*/
+ 0, /*nb_and*/
+ 0, /*nb_xor*/
+ 0, /*nb_or*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_coerce*/
+ #endif
+ 0, /*nb_int*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_long*/
+ #else
+ 0, /*reserved*/
+ #endif
+ 0, /*nb_float*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_oct*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_hex*/
+ #endif
+ 0, /*nb_inplace_add*/
+ 0, /*nb_inplace_subtract*/
+ 0, /*nb_inplace_multiply*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_inplace_divide*/
+ #endif
+ 0, /*nb_inplace_remainder*/
+ 0, /*nb_inplace_power*/
+ 0, /*nb_inplace_lshift*/
+ 0, /*nb_inplace_rshift*/
+ 0, /*nb_inplace_and*/
+ 0, /*nb_inplace_xor*/
+ 0, /*nb_inplace_or*/
+ 0, /*nb_floor_divide*/
+ 0, /*nb_true_divide*/
+ 0, /*nb_inplace_floor_divide*/
+ 0, /*nb_inplace_true_divide*/
+ #if PY_VERSION_HEX >= 0x02050000
+ 0, /*nb_index*/
+ #endif
+};
+
+static PySequenceMethods __pyx_tp_as_sequence_CompletionPort = {
+ 0, /*sq_length*/
+ 0, /*sq_concat*/
+ 0, /*sq_repeat*/
+ 0, /*sq_item*/
+ 0, /*sq_slice*/
+ 0, /*sq_ass_item*/
+ 0, /*sq_ass_slice*/
+ 0, /*sq_contains*/
+ 0, /*sq_inplace_concat*/
+ 0, /*sq_inplace_repeat*/
+};
+
+static PyMappingMethods __pyx_tp_as_mapping_CompletionPort = {
+ 0, /*mp_length*/
+ 0, /*mp_subscript*/
+ 0, /*mp_ass_subscript*/
+};
+
+static PyBufferProcs __pyx_tp_as_buffer_CompletionPort = {
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getreadbuffer*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getwritebuffer*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getsegcount*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getcharbuffer*/
+ #endif
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*bf_getbuffer*/
+ #endif
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*bf_releasebuffer*/
+ #endif
+};
+
+static PyTypeObject __pyx_type_11iocpsupport_CompletionPort = {
+ PyVarObject_HEAD_INIT(0, 0)
+ __Pyx_NAMESTR("iocpsupport.CompletionPort"), /*tp_name*/
+ sizeof(struct __pyx_obj_11iocpsupport_CompletionPort), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ __pyx_tp_dealloc_11iocpsupport_CompletionPort, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*tp_compare*/
+ #else
+ 0, /*reserved*/
+ #endif
+ 0, /*tp_repr*/
+ &__pyx_tp_as_number_CompletionPort, /*tp_as_number*/
+ &__pyx_tp_as_sequence_CompletionPort, /*tp_as_sequence*/
+ &__pyx_tp_as_mapping_CompletionPort, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro*/
+ &__pyx_tp_as_buffer_CompletionPort, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_CHECKTYPES|Py_TPFLAGS_HAVE_NEWBUFFER|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ 0, /*tp_doc*/
+ 0, /*tp_traverse*/
+ 0, /*tp_clear*/
+ 0, /*tp_richcompare*/
+ 0, /*tp_weaklistoffset*/
+ 0, /*tp_iter*/
+ 0, /*tp_iternext*/
+ __pyx_methods_11iocpsupport_CompletionPort, /*tp_methods*/
+ 0, /*tp_members*/
+ 0, /*tp_getset*/
+ 0, /*tp_base*/
+ 0, /*tp_dict*/
+ 0, /*tp_descr_get*/
+ 0, /*tp_descr_set*/
+ 0, /*tp_dictoffset*/
+ __pyx_pf_11iocpsupport_14CompletionPort___init__, /*tp_init*/
+ 0, /*tp_alloc*/
+ __pyx_tp_new_11iocpsupport_CompletionPort, /*tp_new*/
+ 0, /*tp_free*/
+ 0, /*tp_is_gc*/
+ 0, /*tp_bases*/
+ 0, /*tp_mro*/
+ 0, /*tp_cache*/
+ 0, /*tp_subclasses*/
+ 0, /*tp_weaklist*/
+ 0, /*tp_del*/
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*tp_version_tag*/
+ #endif
+};
+
+static PyMethodDef __pyx_methods[] = {
+ {0, 0, 0, 0}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef __pyx_moduledef = {
+ PyModuleDef_HEAD_INIT,
+ __Pyx_NAMESTR("iocpsupport"),
+ 0, /* m_doc */
+ -1, /* m_size */
+ __pyx_methods /* m_methods */,
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL /* m_free */
+};
+#endif
+
+static __Pyx_StringTabEntry __pyx_string_tab[] = {
+ {&__pyx_n_s_1, __pyx_k_1, sizeof(__pyx_k_1), 0, 0, 1, 1},
+ {&__pyx_kp_s_10, __pyx_k_10, sizeof(__pyx_k_10), 0, 0, 1, 0},
+ {&__pyx_kp_s_11, __pyx_k_11, sizeof(__pyx_k_11), 0, 0, 1, 0},
+ {&__pyx_kp_s_12, __pyx_k_12, sizeof(__pyx_k_12), 0, 0, 1, 0},
+ {&__pyx_kp_s_13, __pyx_k_13, sizeof(__pyx_k_13), 0, 0, 1, 0},
+ {&__pyx_kp_s_15, __pyx_k_15, sizeof(__pyx_k_15), 0, 0, 1, 0},
+ {&__pyx_kp_s_16, __pyx_k_16, sizeof(__pyx_k_16), 0, 0, 1, 0},
+ {&__pyx_n_s_2, __pyx_k_2, sizeof(__pyx_k_2), 0, 0, 1, 1},
+ {&__pyx_kp_s_3, __pyx_k_3, sizeof(__pyx_k_3), 0, 0, 1, 0},
+ {&__pyx_kp_s_5, __pyx_k_5, sizeof(__pyx_k_5), 0, 0, 1, 0},
+ {&__pyx_kp_s_6, __pyx_k_6, sizeof(__pyx_k_6), 0, 0, 1, 0},
+ {&__pyx_kp_s_7, __pyx_k_7, sizeof(__pyx_k_7), 0, 0, 1, 0},
+ {&__pyx_kp_s_8, __pyx_k_8, sizeof(__pyx_k_8), 0, 0, 1, 0},
+ {&__pyx_n_s__AllocateReadBuffer, __pyx_k__AllocateReadBuffer, sizeof(__pyx_k__AllocateReadBuffer), 0, 0, 1, 1},
+ {&__pyx_n_s__Event, __pyx_k__Event, sizeof(__pyx_k__Event), 0, 0, 1, 1},
+ {&__pyx_n_s__MemoryError, __pyx_k__MemoryError, sizeof(__pyx_k__MemoryError), 0, 0, 1, 1},
+ {&__pyx_n_s__RuntimeError, __pyx_k__RuntimeError, sizeof(__pyx_k__RuntimeError), 0, 0, 1, 1},
+ {&__pyx_n_s__ValueError, __pyx_k__ValueError, sizeof(__pyx_k__ValueError), 0, 0, 1, 1},
+ {&__pyx_n_s__WSAAddressToString, __pyx_k__WSAAddressToString, sizeof(__pyx_k__WSAAddressToString), 0, 0, 1, 1},
+ {&__pyx_n_s__WindowsError, __pyx_k__WindowsError, sizeof(__pyx_k__WindowsError), 0, 0, 1, 1},
+ {&__pyx_n_s____init__, __pyx_k____init__, sizeof(__pyx_k____init__), 0, 0, 1, 1},
+ {&__pyx_n_s____main__, __pyx_k____main__, sizeof(__pyx_k____main__), 0, 0, 1, 1},
+ {&__pyx_n_s____test__, __pyx_k____test__, sizeof(__pyx_k____test__), 0, 0, 1, 1},
+ {&__pyx_n_s__accept, __pyx_k__accept, sizeof(__pyx_k__accept), 0, 0, 1, 1},
+ {&__pyx_n_s__accepting, __pyx_k__accepting, sizeof(__pyx_k__accepting), 0, 0, 1, 1},
+ {&__pyx_n_s__addr, __pyx_k__addr, sizeof(__pyx_k__addr), 0, 0, 1, 1},
+ {&__pyx_n_s__addr_buff, __pyx_k__addr_buff, sizeof(__pyx_k__addr_buff), 0, 0, 1, 1},
+ {&__pyx_n_s__addr_len_buff, __pyx_k__addr_len_buff, sizeof(__pyx_k__addr_len_buff), 0, 0, 1, 1},
+ {&__pyx_n_s__buff, __pyx_k__buff, sizeof(__pyx_k__buff), 0, 0, 1, 1},
+ {&__pyx_n_s__bufflist, __pyx_k__bufflist, sizeof(__pyx_k__bufflist), 0, 0, 1, 1},
+ {&__pyx_n_s__bytes, __pyx_k__bytes, sizeof(__pyx_k__bytes), 0, 0, 1, 1},
+ {&__pyx_n_s__callback, __pyx_k__callback, sizeof(__pyx_k__callback), 0, 0, 1, 1},
+ {&__pyx_n_s__connect, __pyx_k__connect, sizeof(__pyx_k__connect), 0, 0, 1, 1},
+ {&__pyx_n_s__flags, __pyx_k__flags, sizeof(__pyx_k__flags), 0, 0, 1, 1},
+ {&__pyx_n_s__get_accept_addrs, __pyx_k__get_accept_addrs, sizeof(__pyx_k__get_accept_addrs), 0, 0, 1, 1},
+ {&__pyx_n_s__getsockopt, __pyx_k__getsockopt, sizeof(__pyx_k__getsockopt), 0, 0, 1, 1},
+ {&__pyx_n_s__handle, __pyx_k__handle, sizeof(__pyx_k__handle), 0, 0, 1, 1},
+ {&__pyx_n_s__have_connectex, __pyx_k__have_connectex, sizeof(__pyx_k__have_connectex), 0, 0, 1, 1},
+ {&__pyx_n_s__iocpsupport, __pyx_k__iocpsupport, sizeof(__pyx_k__iocpsupport), 0, 0, 1, 1},
+ {&__pyx_n_s__key, __pyx_k__key, sizeof(__pyx_k__key), 0, 0, 1, 1},
+ {&__pyx_n_s__listening, __pyx_k__listening, sizeof(__pyx_k__listening), 0, 0, 1, 1},
+ {&__pyx_n_s__makesockaddr, __pyx_k__makesockaddr, sizeof(__pyx_k__makesockaddr), 0, 0, 1, 1},
+ {&__pyx_n_s__maxAddrLen, __pyx_k__maxAddrLen, sizeof(__pyx_k__maxAddrLen), 0, 0, 1, 1},
+ {&__pyx_n_s__obj, __pyx_k__obj, sizeof(__pyx_k__obj), 0, 0, 1, 1},
+ {&__pyx_n_s__owner, __pyx_k__owner, sizeof(__pyx_k__owner), 0, 0, 1, 1},
+ {&__pyx_n_s__recv, __pyx_k__recv, sizeof(__pyx_k__recv), 0, 0, 1, 1},
+ {&__pyx_n_s__recvfrom, __pyx_k__recvfrom, sizeof(__pyx_k__recvfrom), 0, 0, 1, 1},
+ {&__pyx_n_s__rsplit, __pyx_k__rsplit, sizeof(__pyx_k__rsplit), 0, 0, 1, 1},
+ {&__pyx_n_s__s, __pyx_k__s, sizeof(__pyx_k__s), 0, 0, 1, 1},
+ {&__pyx_n_s__self, __pyx_k__self, sizeof(__pyx_k__self), 0, 0, 1, 1},
+ {&__pyx_n_s__send, __pyx_k__send, sizeof(__pyx_k__send), 0, 0, 1, 1},
+ {&__pyx_n_s__socket, __pyx_k__socket, sizeof(__pyx_k__socket), 0, 0, 1, 1},
+ {&__pyx_n_s__split, __pyx_k__split, sizeof(__pyx_k__split), 0, 0, 1, 1},
+ {0, 0, 0, 0, 0, 0, 0}
+};
+static int __Pyx_InitCachedBuiltins(void) {
+ __pyx_builtin_ValueError = __Pyx_GetName(__pyx_b, __pyx_n_s__ValueError); if (!__pyx_builtin_ValueError) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 304; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_builtin_MemoryError = __Pyx_GetName(__pyx_b, __pyx_n_s__MemoryError); if (!__pyx_builtin_MemoryError) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 132; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_builtin_RuntimeError = __Pyx_GetName(__pyx_b, __pyx_n_s__RuntimeError); if (!__pyx_builtin_RuntimeError) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 272; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+static int __Pyx_InitCachedConstants(void) {
+ __Pyx_RefNannyDeclarations
+ __Pyx_RefNannySetupContext("__Pyx_InitCachedConstants");
+
+ /* "iocpsupport.pyx":233
+ * raise_error(0, 'WSAAddressToString')
+ * host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ * host, port = host.rsplit(':', 1) # <<<<<<<<<<<<<<
+ * port = int(port)
+ * assert host[0] == '['
+ */
+ __pyx_k_tuple_4 = PyTuple_New(2); if (unlikely(!__pyx_k_tuple_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 233; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_k_tuple_4));
+ __Pyx_INCREF(((PyObject *)__pyx_kp_s_3));
+ PyTuple_SET_ITEM(__pyx_k_tuple_4, 0, ((PyObject *)__pyx_kp_s_3));
+ __Pyx_GIVEREF(((PyObject *)__pyx_kp_s_3));
+ __Pyx_INCREF(__pyx_int_1);
+ PyTuple_SET_ITEM(__pyx_k_tuple_4, 1, __pyx_int_1);
+ __Pyx_GIVEREF(__pyx_int_1);
+ __Pyx_GIVEREF(((PyObject *)__pyx_k_tuple_4));
+
+ /* "iocpsupport.pyx":264
+ * cdef int addrlen = sizeof(sockaddr_in6)
+ * host, port, flow, scope = addr
+ * host = host.split("%")[0] # remove scope ID, if any # <<<<<<<<<<<<<<
+ *
+ * hoststr = PyString_AsString(host)
+ */
+ __pyx_k_tuple_9 = PyTuple_New(1); if (unlikely(!__pyx_k_tuple_9)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 264; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_k_tuple_9));
+ __Pyx_INCREF(((PyObject *)__pyx_kp_s_8));
+ PyTuple_SET_ITEM(__pyx_k_tuple_9, 0, ((PyObject *)__pyx_kp_s_8));
+ __Pyx_GIVEREF(((PyObject *)__pyx_kp_s_8));
+ __Pyx_GIVEREF(((PyObject *)__pyx_k_tuple_9));
+ __Pyx_RefNannyFinishContext();
+ return 0;
+ __pyx_L1_error:;
+ __Pyx_RefNannyFinishContext();
+ return -1;
+}
+
+static int __Pyx_InitGlobals(void) {
+ if (__Pyx_InitStrings(__pyx_string_tab) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ __pyx_int_0 = PyInt_FromLong(0); if (unlikely(!__pyx_int_0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ __pyx_int_1 = PyInt_FromLong(1); if (unlikely(!__pyx_int_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+#if PY_MAJOR_VERSION < 3
+PyMODINIT_FUNC initiocpsupport(void); /*proto*/
+PyMODINIT_FUNC initiocpsupport(void)
+#else
+PyMODINIT_FUNC PyInit_iocpsupport(void); /*proto*/
+PyMODINIT_FUNC PyInit_iocpsupport(void)
+#endif
+{
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ int __pyx_t_3;
+ __Pyx_RefNannyDeclarations
+ #if CYTHON_REFNANNY
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("refnanny");
+ if (!__Pyx_RefNanny) {
+ PyErr_Clear();
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("Cython.Runtime.refnanny");
+ if (!__Pyx_RefNanny)
+ Py_FatalError("failed to import 'refnanny' module");
+ }
+ #endif
+ __Pyx_RefNannySetupContext("PyMODINIT_FUNC PyInit_iocpsupport(void)");
+ if ( __Pyx_check_binary_version() < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_empty_tuple = PyTuple_New(0); if (unlikely(!__pyx_empty_tuple)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_empty_bytes = PyBytes_FromStringAndSize("", 0); if (unlikely(!__pyx_empty_bytes)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #ifdef __pyx_binding_PyCFunctionType_USED
+ if (__pyx_binding_PyCFunctionType_init() < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #endif
+ /*--- Library function declarations ---*/
+ /*--- Threads initialization code ---*/
+ #if defined(__PYX_FORCE_INIT_THREADS) && __PYX_FORCE_INIT_THREADS
+ #ifdef WITH_THREAD /* Python build with threading support? */
+ PyEval_InitThreads();
+ #endif
+ #endif
+ /*--- Module creation code ---*/
+ #if PY_MAJOR_VERSION < 3
+ __pyx_m = Py_InitModule4(__Pyx_NAMESTR("iocpsupport"), __pyx_methods, 0, 0, PYTHON_API_VERSION);
+ #else
+ __pyx_m = PyModule_Create(&__pyx_moduledef);
+ #endif
+ if (!__pyx_m) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ #if PY_MAJOR_VERSION < 3
+ Py_INCREF(__pyx_m);
+ #endif
+ __pyx_b = PyImport_AddModule(__Pyx_NAMESTR(__Pyx_BUILTIN_MODULE_NAME));
+ if (!__pyx_b) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ if (__Pyx_SetAttrString(__pyx_m, "__builtins__", __pyx_b) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ /*--- Initialize various global constants etc. ---*/
+ if (unlikely(__Pyx_InitGlobals() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ if (__pyx_module_is_main_iocpsupport) {
+ if (__Pyx_SetAttrString(__pyx_m, "__name__", __pyx_n_s____main__) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ }
+ /*--- Builtin init code ---*/
+ if (unlikely(__Pyx_InitCachedBuiltins() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Constants init code ---*/
+ if (unlikely(__Pyx_InitCachedConstants() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Global init code ---*/
+ /*--- Variable export code ---*/
+ /*--- Function export code ---*/
+ /*--- Type init code ---*/
+ if (PyType_Ready(&__pyx_type_11iocpsupport_CompletionPort) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 148; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ if (__Pyx_SetAttrString(__pyx_m, "CompletionPort", (PyObject *)&__pyx_type_11iocpsupport_CompletionPort) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 148; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_ptype_11iocpsupport_CompletionPort = &__pyx_type_11iocpsupport_CompletionPort;
+ /*--- Type import code ---*/
+ /*--- Variable import code ---*/
+ /*--- Function import code ---*/
+ /*--- Execution code ---*/
+
+ /* "iocpsupport.pyx":141
+ * raise WindowsError(message, err)
+ *
+ * class Event: # <<<<<<<<<<<<<<
+ * def __init__(self, callback, owner, **kw):
+ * self.callback = callback
+ */
+ __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 141; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_1));
+
+ /* "iocpsupport.pyx":142
+ *
+ * class Event:
+ * def __init__(self, callback, owner, **kw): # <<<<<<<<<<<<<<
+ * self.callback = callback
+ * self.owner = owner
+ */
+ __pyx_t_2 = __pyx_binding_PyCFunctionType_NewEx(&__pyx_mdef_11iocpsupport_5Event___init__, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ if (PyObject_SetItem(__pyx_t_1, __pyx_n_s____init__, __pyx_t_2) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 142; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+
+ /* "iocpsupport.pyx":141
+ * raise WindowsError(message, err)
+ *
+ * class Event: # <<<<<<<<<<<<<<
+ * def __init__(self, callback, owner, **kw):
+ * self.callback = callback
+ */
+ __pyx_t_2 = __Pyx_CreateClass(((PyObject *)__pyx_empty_tuple), ((PyObject *)__pyx_t_1), __pyx_n_s__Event, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 141; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__Event, __pyx_t_2) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 141; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __Pyx_DECREF(((PyObject *)__pyx_t_1)); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":208
+ * CloseHandle(self.port)
+ *
+ * def makesockaddr(object buff): # <<<<<<<<<<<<<<
+ * cdef void *mem_buffer
+ * cdef Py_ssize_t size
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_makesockaddr, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 208; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__makesockaddr, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 208; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":279
+ *
+ *
+ * def AllocateReadBuffer(int size): # <<<<<<<<<<<<<<
+ * return PyBuffer_New(size)
+ *
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_1AllocateReadBuffer, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__AllocateReadBuffer, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":282
+ * return PyBuffer_New(size)
+ *
+ * def maxAddrLen(long s): # <<<<<<<<<<<<<<
+ * cdef WSAPROTOCOL_INFO wsa_pi
+ * cdef int size, rc
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_2maxAddrLen, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__maxAddrLen, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":302
+ * return wsa_pi.iAddressFamily
+ *
+ * import socket # for WSAStartup # <<<<<<<<<<<<<<
+ * if not initWinsockPointers():
+ * raise ValueError, 'Failed to initialize Winsock function vectors'
+ */
+ __pyx_t_1 = __Pyx_Import(((PyObject *)__pyx_n_s__socket), 0, -1); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 302; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__socket, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 302; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":303
+ *
+ * import socket # for WSAStartup
+ * if not initWinsockPointers(): # <<<<<<<<<<<<<<
+ * raise ValueError, 'Failed to initialize Winsock function vectors'
+ *
+ */
+ __pyx_t_3 = (!initWinsockPointers());
+ if (__pyx_t_3) {
+
+ /* "iocpsupport.pyx":304
+ * import socket # for WSAStartup
+ * if not initWinsockPointers():
+ * raise ValueError, 'Failed to initialize Winsock function vectors' # <<<<<<<<<<<<<<
+ *
+ * have_connectex = (lpConnectEx != NULL)
+ */
+ __Pyx_Raise(__pyx_builtin_ValueError, ((PyObject *)__pyx_kp_s_16), 0, 0);
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 304; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L2;
+ }
+ __pyx_L2:;
+
+ /* "iocpsupport.pyx":306
+ * raise ValueError, 'Failed to initialize Winsock function vectors'
+ *
+ * have_connectex = (lpConnectEx != NULL) # <<<<<<<<<<<<<<
+ *
+ * include 'acceptex.pxi'
+ */
+ __pyx_t_1 = __Pyx_PyBool_FromLong((lpConnectEx != NULL)); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 306; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__have_connectex, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 306; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":5
+ *
+ *
+ * def accept(long listening, long accepting, object buff, object obj): # <<<<<<<<<<<<<<
+ * """
+ * CAUTION: unlike system AcceptEx(), this function returns 0 on success
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_3accept, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__accept, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\acceptex.pxi":34
+ * return 0
+ *
+ * def get_accept_addrs(long s, object buff): # <<<<<<<<<<<<<<
+ * cdef WSAPROTOCOL_INFO wsa_pi
+ * cdef int locallen, remotelen
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_4get_accept_addrs, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__get_accept_addrs, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[1]; __pyx_lineno = 34; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\connectex.pxi":5
+ *
+ *
+ * def connect(long s, object addr, object obj): # <<<<<<<<<<<<<<
+ * """
+ * CAUTION: unlike system ConnectEx(), this function returns 0 on success
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_5connect, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__connect, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[2]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_6recv, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__recv, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsarecv.pxi":41
+ * PyMem_Free(ws_buf)
+ *
+ * def recvfrom(long s, object buff, object addr_buff, object addr_len_buff, object obj, unsigned long flags = 0): # <<<<<<<<<<<<<<
+ * cdef int rc, c_addr_buff_len, c_addr_len_buff_len
+ * cdef myOVERLAPPED *ov
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_7recvfrom, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__recvfrom, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[3]; __pyx_lineno = 41; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "C:\t\twisted\twisted\internet\iocpreactor\iocpsupport\wsasend.pxi":5
+ *
+ *
+ * def send(long s, object buff, object obj, unsigned long flags = 0): # <<<<<<<<<<<<<<
+ * cdef int rc
+ * cdef myOVERLAPPED *ov
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_11iocpsupport_8send, NULL, __pyx_n_s__iocpsupport); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__send, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[4]; __pyx_lineno = 5; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "iocpsupport.pyx":1
+ * # Copyright (c) Twisted Matrix Laboratories. # <<<<<<<<<<<<<<
+ * # See LICENSE for details.
+ *
+ */
+ __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_1));
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s____test__, ((PyObject *)__pyx_t_1)) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(((PyObject *)__pyx_t_1)); __pyx_t_1 = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ if (__pyx_m) {
+ __Pyx_AddTraceback("init iocpsupport", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ Py_DECREF(__pyx_m); __pyx_m = 0;
+ } else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_ImportError, "init iocpsupport");
+ }
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ #if PY_MAJOR_VERSION < 3
+ return;
+ #else
+ return __pyx_m;
+ #endif
+}
+
+/* Runtime support code */
+
+#if CYTHON_REFNANNY
+static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname) {
+ PyObject *m = NULL, *p = NULL;
+ void *r = NULL;
+ m = PyImport_ImportModule((char *)modname);
+ if (!m) goto end;
+ p = PyObject_GetAttrString(m, (char *)"RefNannyAPI");
+ if (!p) goto end;
+ r = PyLong_AsVoidPtr(p);
+end:
+ Py_XDECREF(p);
+ Py_XDECREF(m);
+ return (__Pyx_RefNannyAPIStruct *)r;
+}
+#endif /* CYTHON_REFNANNY */
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name) {
+ PyObject *result;
+ result = PyObject_GetAttr(dict, name);
+ if (!result) {
+ if (dict != __pyx_b) {
+ PyErr_Clear();
+ result = PyObject_GetAttr(__pyx_b, name);
+ }
+ if (!result) {
+ PyErr_SetObject(PyExc_NameError, name);
+ }
+ }
+ return result;
+}
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb) {
+ PyObject *tmp_type, *tmp_value, *tmp_tb;
+ PyThreadState *tstate = PyThreadState_GET();
+
+ tmp_type = tstate->curexc_type;
+ tmp_value = tstate->curexc_value;
+ tmp_tb = tstate->curexc_traceback;
+ tstate->curexc_type = type;
+ tstate->curexc_value = value;
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_type);
+ Py_XDECREF(tmp_value);
+ Py_XDECREF(tmp_tb);
+}
+
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ *type = tstate->curexc_type;
+ *value = tstate->curexc_value;
+ *tb = tstate->curexc_traceback;
+
+ tstate->curexc_type = 0;
+ tstate->curexc_value = 0;
+ tstate->curexc_traceback = 0;
+}
+
+
+#if PY_MAJOR_VERSION < 3
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause) {
+ /* cause is unused */
+ Py_XINCREF(type);
+ Py_XINCREF(value);
+ Py_XINCREF(tb);
+ /* First, check the traceback argument, replacing None with NULL. */
+ if (tb == Py_None) {
+ Py_DECREF(tb);
+ tb = 0;
+ }
+ else if (tb != NULL && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto raise_error;
+ }
+ /* Next, replace a missing value with None */
+ if (value == NULL) {
+ value = Py_None;
+ Py_INCREF(value);
+ }
+ #if PY_VERSION_HEX < 0x02050000
+ if (!PyClass_Check(type))
+ #else
+ if (!PyType_Check(type))
+ #endif
+ {
+ /* Raising an instance. The value should be a dummy. */
+ if (value != Py_None) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto raise_error;
+ }
+ /* Normalize to raise <class>, <instance> */
+ Py_DECREF(value);
+ value = type;
+ #if PY_VERSION_HEX < 0x02050000
+ if (PyInstance_Check(type)) {
+ type = (PyObject*) ((PyInstanceObject*)type)->in_class;
+ Py_INCREF(type);
+ }
+ else {
+ type = 0;
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception must be an old-style class or instance");
+ goto raise_error;
+ }
+ #else
+ type = (PyObject*) Py_TYPE(type);
+ Py_INCREF(type);
+ if (!PyType_IsSubtype((PyTypeObject *)type, (PyTypeObject *)PyExc_BaseException)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto raise_error;
+ }
+ #endif
+ }
+
+ __Pyx_ErrRestore(type, value, tb);
+ return;
+raise_error:
+ Py_XDECREF(value);
+ Py_XDECREF(type);
+ Py_XDECREF(tb);
+ return;
+}
+
+#else /* Python 3+ */
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause) {
+ if (tb == Py_None) {
+ tb = 0;
+ } else if (tb && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto bad;
+ }
+ if (value == Py_None)
+ value = 0;
+
+ if (PyExceptionInstance_Check(type)) {
+ if (value) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto bad;
+ }
+ value = type;
+ type = (PyObject*) Py_TYPE(value);
+ } else if (!PyExceptionClass_Check(type)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto bad;
+ }
+
+ if (cause) {
+ PyObject *fixed_cause;
+ if (PyExceptionClass_Check(cause)) {
+ fixed_cause = PyObject_CallObject(cause, NULL);
+ if (fixed_cause == NULL)
+ goto bad;
+ }
+ else if (PyExceptionInstance_Check(cause)) {
+ fixed_cause = cause;
+ Py_INCREF(fixed_cause);
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError,
+ "exception causes must derive from "
+ "BaseException");
+ goto bad;
+ }
+ if (!value) {
+ value = PyObject_CallObject(type, NULL);
+ }
+ PyException_SetCause(value, fixed_cause);
+ }
+
+ PyErr_SetObject(type, value);
+
+ if (tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ PyObject* tmp_tb = tstate->curexc_traceback;
+ if (tb != tmp_tb) {
+ Py_INCREF(tb);
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_tb);
+ }
+ }
+
+bad:
+ return;
+}
+#endif
+
+static void __Pyx_RaiseArgtupleInvalid(
+ const char* func_name,
+ int exact,
+ Py_ssize_t num_min,
+ Py_ssize_t num_max,
+ Py_ssize_t num_found)
+{
+ Py_ssize_t num_expected;
+ const char *more_or_less;
+
+ if (num_found < num_min) {
+ num_expected = num_min;
+ more_or_less = "at least";
+ } else {
+ num_expected = num_max;
+ more_or_less = "at most";
+ }
+ if (exact) {
+ more_or_less = "exactly";
+ }
+ PyErr_Format(PyExc_TypeError,
+ "%s() takes %s %"PY_FORMAT_SIZE_T"d positional argument%s (%"PY_FORMAT_SIZE_T"d given)",
+ func_name, more_or_less, num_expected,
+ (num_expected == 1) ? "" : "s", num_found);
+}
+
+static void __Pyx_RaiseDoubleKeywordsError(
+ const char* func_name,
+ PyObject* kw_name)
+{
+ PyErr_Format(PyExc_TypeError,
+ #if PY_MAJOR_VERSION >= 3
+ "%s() got multiple values for keyword argument '%U'", func_name, kw_name);
+ #else
+ "%s() got multiple values for keyword argument '%s'", func_name,
+ PyString_AS_STRING(kw_name));
+ #endif
+}
+
+static int __Pyx_ParseOptionalKeywords(
+ PyObject *kwds,
+ PyObject **argnames[],
+ PyObject *kwds2,
+ PyObject *values[],
+ Py_ssize_t num_pos_args,
+ const char* function_name)
+{
+ PyObject *key = 0, *value = 0;
+ Py_ssize_t pos = 0;
+ PyObject*** name;
+ PyObject*** first_kw_arg = argnames + num_pos_args;
+
+ while (PyDict_Next(kwds, &pos, &key, &value)) {
+ name = first_kw_arg;
+ while (*name && (**name != key)) name++;
+ if (*name) {
+ values[name-argnames] = value;
+ } else {
+ #if PY_MAJOR_VERSION < 3
+ if (unlikely(!PyString_CheckExact(key)) && unlikely(!PyString_Check(key))) {
+ #else
+ if (unlikely(!PyUnicode_CheckExact(key)) && unlikely(!PyUnicode_Check(key))) {
+ #endif
+ goto invalid_keyword_type;
+ } else {
+ for (name = first_kw_arg; *name; name++) {
+ #if PY_MAJOR_VERSION >= 3
+ if (PyUnicode_GET_SIZE(**name) == PyUnicode_GET_SIZE(key) &&
+ PyUnicode_Compare(**name, key) == 0) break;
+ #else
+ if (PyString_GET_SIZE(**name) == PyString_GET_SIZE(key) &&
+ _PyString_Eq(**name, key)) break;
+ #endif
+ }
+ if (*name) {
+ values[name-argnames] = value;
+ } else {
+ /* unexpected keyword found */
+ for (name=argnames; name != first_kw_arg; name++) {
+ if (**name == key) goto arg_passed_twice;
+ #if PY_MAJOR_VERSION >= 3
+ if (PyUnicode_GET_SIZE(**name) == PyUnicode_GET_SIZE(key) &&
+ PyUnicode_Compare(**name, key) == 0) goto arg_passed_twice;
+ #else
+ if (PyString_GET_SIZE(**name) == PyString_GET_SIZE(key) &&
+ _PyString_Eq(**name, key)) goto arg_passed_twice;
+ #endif
+ }
+ if (kwds2) {
+ if (unlikely(PyDict_SetItem(kwds2, key, value))) goto bad;
+ } else {
+ goto invalid_keyword;
+ }
+ }
+ }
+ }
+ }
+ return 0;
+arg_passed_twice:
+ __Pyx_RaiseDoubleKeywordsError(function_name, **name);
+ goto bad;
+invalid_keyword_type:
+ PyErr_Format(PyExc_TypeError,
+ "%s() keywords must be strings", function_name);
+ goto bad;
+invalid_keyword:
+ PyErr_Format(PyExc_TypeError,
+ #if PY_MAJOR_VERSION < 3
+ "%s() got an unexpected keyword argument '%s'",
+ function_name, PyString_AsString(key));
+ #else
+ "%s() got an unexpected keyword argument '%U'",
+ function_name, key);
+ #endif
+bad:
+ return -1;
+}
+
+static CYTHON_INLINE void __Pyx_RaiseNeedMoreValuesError(Py_ssize_t index) {
+ PyErr_Format(PyExc_ValueError,
+ "need more than %"PY_FORMAT_SIZE_T"d value%s to unpack",
+ index, (index == 1) ? "" : "s");
+}
+
+static CYTHON_INLINE void __Pyx_RaiseTooManyValuesError(Py_ssize_t expected) {
+ PyErr_Format(PyExc_ValueError,
+ "too many values to unpack (expected %"PY_FORMAT_SIZE_T"d)", expected);
+}
+
+static int __Pyx_IternextUnpackEndCheck(PyObject *retval, Py_ssize_t expected) {
+ if (unlikely(retval)) {
+ Py_DECREF(retval);
+ __Pyx_RaiseTooManyValuesError(expected);
+ return -1;
+ } else if (PyErr_Occurred()) {
+ if (likely(PyErr_ExceptionMatches(PyExc_StopIteration))) {
+ PyErr_Clear();
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static CYTHON_INLINE int __Pyx_CheckKeywordStrings(
+ PyObject *kwdict,
+ const char* function_name,
+ int kw_allowed)
+{
+ PyObject* key = 0;
+ Py_ssize_t pos = 0;
+ while (PyDict_Next(kwdict, &pos, &key, 0)) {
+ #if PY_MAJOR_VERSION < 3
+ if (unlikely(!PyString_CheckExact(key)) && unlikely(!PyString_Check(key)))
+ #else
+ if (unlikely(!PyUnicode_CheckExact(key)) && unlikely(!PyUnicode_Check(key)))
+ #endif
+ goto invalid_keyword_type;
+ }
+ if ((!kw_allowed) && unlikely(key))
+ goto invalid_keyword;
+ return 1;
+invalid_keyword_type:
+ PyErr_Format(PyExc_TypeError,
+ "%s() keywords must be strings", function_name);
+ return 0;
+invalid_keyword:
+ PyErr_Format(PyExc_TypeError,
+ #if PY_MAJOR_VERSION < 3
+ "%s() got an unexpected keyword argument '%s'",
+ function_name, PyString_AsString(key));
+ #else
+ "%s() got an unexpected keyword argument '%U'",
+ function_name, key);
+ #endif
+ return 0;
+}
+
+
+static PyObject *__Pyx_FindPy2Metaclass(PyObject *bases) {
+ PyObject *metaclass;
+ /* Default metaclass */
+#if PY_MAJOR_VERSION < 3
+ if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
+ PyObject *base = PyTuple_GET_ITEM(bases, 0);
+ metaclass = PyObject_GetAttrString(base, (char *)"__class__");
+ if (!metaclass) {
+ PyErr_Clear();
+ metaclass = (PyObject*) Py_TYPE(base);
+ }
+ } else {
+ metaclass = (PyObject *) &PyClass_Type;
+ }
+#else
+ if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
+ PyObject *base = PyTuple_GET_ITEM(bases, 0);
+ metaclass = (PyObject*) Py_TYPE(base);
+ } else {
+ metaclass = (PyObject *) &PyType_Type;
+ }
+#endif
+ Py_INCREF(metaclass);
+ return metaclass;
+}
+
+static PyObject *__Pyx_CreateClass(PyObject *bases, PyObject *dict, PyObject *name,
+ PyObject *modname) {
+ PyObject *result;
+ PyObject *metaclass;
+
+ if (PyDict_SetItemString(dict, "__module__", modname) < 0)
+ return NULL;
+
+ /* Python2 __metaclass__ */
+ metaclass = PyDict_GetItemString(dict, "__metaclass__");
+ if (metaclass) {
+ Py_INCREF(metaclass);
+ } else {
+ metaclass = __Pyx_FindPy2Metaclass(bases);
+ }
+ result = PyObject_CallFunctionObjArgs(metaclass, name, bases, dict, NULL);
+ Py_DECREF(metaclass);
+ return result;
+}
+
+
+static PyObject *__pyx_binding_PyCFunctionType_NewEx(PyMethodDef *ml, PyObject *self, PyObject *module) {
+ __pyx_binding_PyCFunctionType_object *op = PyObject_GC_New(__pyx_binding_PyCFunctionType_object, __pyx_binding_PyCFunctionType);
+ if (op == NULL)
+ return NULL;
+ op->func.m_ml = ml;
+ Py_XINCREF(self);
+ op->func.m_self = self;
+ Py_XINCREF(module);
+ op->func.m_module = module;
+ PyObject_GC_Track(op);
+ return (PyObject *)op;
+}
+
+static void __pyx_binding_PyCFunctionType_dealloc(__pyx_binding_PyCFunctionType_object *m) {
+ PyObject_GC_UnTrack(m);
+ Py_XDECREF(m->func.m_self);
+ Py_XDECREF(m->func.m_module);
+ PyObject_GC_Del(m);
+}
+
+static PyObject *__pyx_binding_PyCFunctionType_descr_get(PyObject *func, PyObject *obj, PyObject *type) {
+ if (obj == Py_None)
+ obj = NULL;
+ return PyMethod_New(func, obj, type);
+}
+
+static int __pyx_binding_PyCFunctionType_init(void) {
+ __pyx_binding_PyCFunctionType_type = PyCFunction_Type;
+ __pyx_binding_PyCFunctionType_type.tp_name = __Pyx_NAMESTR("cython_binding_builtin_function_or_method");
+ __pyx_binding_PyCFunctionType_type.tp_dealloc = (destructor)__pyx_binding_PyCFunctionType_dealloc;
+ __pyx_binding_PyCFunctionType_type.tp_descr_get = __pyx_binding_PyCFunctionType_descr_get;
+ if (PyType_Ready(&__pyx_binding_PyCFunctionType_type) < 0) {
+ return -1;
+ }
+ __pyx_binding_PyCFunctionType = &__pyx_binding_PyCFunctionType_type;
+ return 0;
+
+}
+
+static PyObject *__Pyx_Import(PyObject *name, PyObject *from_list, long level) {
+ PyObject *py_import = 0;
+ PyObject *empty_list = 0;
+ PyObject *module = 0;
+ PyObject *global_dict = 0;
+ PyObject *empty_dict = 0;
+ PyObject *list;
+ py_import = __Pyx_GetAttrString(__pyx_b, "__import__");
+ if (!py_import)
+ goto bad;
+ if (from_list)
+ list = from_list;
+ else {
+ empty_list = PyList_New(0);
+ if (!empty_list)
+ goto bad;
+ list = empty_list;
+ }
+ global_dict = PyModule_GetDict(__pyx_m);
+ if (!global_dict)
+ goto bad;
+ empty_dict = PyDict_New();
+ if (!empty_dict)
+ goto bad;
+ #if PY_VERSION_HEX >= 0x02050000
+ {
+ PyObject *py_level = PyInt_FromLong(level);
+ if (!py_level)
+ goto bad;
+ module = PyObject_CallFunctionObjArgs(py_import,
+ name, global_dict, empty_dict, list, py_level, NULL);
+ Py_DECREF(py_level);
+ }
+ #else
+ if (level>0) {
+ PyErr_SetString(PyExc_RuntimeError, "Relative import is not supported for Python <=2.4.");
+ goto bad;
+ }
+ module = PyObject_CallFunctionObjArgs(py_import,
+ name, global_dict, empty_dict, list, NULL);
+ #endif
+bad:
+ Py_XDECREF(empty_list);
+ Py_XDECREF(py_import);
+ Py_XDECREF(empty_dict);
+ return module;
+}
+
+static CYTHON_INLINE int __Pyx_PyBytes_Equals(PyObject* s1, PyObject* s2, int equals) {
+ if (s1 == s2) { /* as done by PyObject_RichCompareBool(); also catches the (interned) empty string */
+ return (equals == Py_EQ);
+ } else if (PyBytes_CheckExact(s1) & PyBytes_CheckExact(s2)) {
+ if (PyBytes_GET_SIZE(s1) != PyBytes_GET_SIZE(s2)) {
+ return (equals == Py_NE);
+ } else if (PyBytes_GET_SIZE(s1) == 1) {
+ if (equals == Py_EQ)
+ return (PyBytes_AS_STRING(s1)[0] == PyBytes_AS_STRING(s2)[0]);
+ else
+ return (PyBytes_AS_STRING(s1)[0] != PyBytes_AS_STRING(s2)[0]);
+ } else {
+ int result = memcmp(PyBytes_AS_STRING(s1), PyBytes_AS_STRING(s2), (size_t)PyBytes_GET_SIZE(s1));
+ return (equals == Py_EQ) ? (result == 0) : (result != 0);
+ }
+ } else if ((s1 == Py_None) & PyBytes_CheckExact(s2)) {
+ return (equals == Py_NE);
+ } else if ((s2 == Py_None) & PyBytes_CheckExact(s1)) {
+ return (equals == Py_NE);
+ } else {
+ int result;
+ PyObject* py_result = PyObject_RichCompare(s1, s2, equals);
+ if (!py_result)
+ return -1;
+ result = __Pyx_PyObject_IsTrue(py_result);
+ Py_DECREF(py_result);
+ return result;
+ }
+}
+
+static CYTHON_INLINE int __Pyx_PyUnicode_Equals(PyObject* s1, PyObject* s2, int equals) {
+ if (s1 == s2) { /* as done by PyObject_RichCompareBool(); also catches the (interned) empty string */
+ return (equals == Py_EQ);
+ } else if (PyUnicode_CheckExact(s1) & PyUnicode_CheckExact(s2)) {
+ if (PyUnicode_GET_SIZE(s1) != PyUnicode_GET_SIZE(s2)) {
+ return (equals == Py_NE);
+ } else if (PyUnicode_GET_SIZE(s1) == 1) {
+ if (equals == Py_EQ)
+ return (PyUnicode_AS_UNICODE(s1)[0] == PyUnicode_AS_UNICODE(s2)[0]);
+ else
+ return (PyUnicode_AS_UNICODE(s1)[0] != PyUnicode_AS_UNICODE(s2)[0]);
+ } else {
+ int result = PyUnicode_Compare(s1, s2);
+ if ((result == -1) && unlikely(PyErr_Occurred()))
+ return -1;
+ return (equals == Py_EQ) ? (result == 0) : (result != 0);
+ }
+ } else if ((s1 == Py_None) & PyUnicode_CheckExact(s2)) {
+ return (equals == Py_NE);
+ } else if ((s2 == Py_None) & PyUnicode_CheckExact(s1)) {
+ return (equals == Py_NE);
+ } else {
+ int result;
+ PyObject* py_result = PyObject_RichCompare(s1, s2, equals);
+ if (!py_result)
+ return -1;
+ result = __Pyx_PyObject_IsTrue(py_result);
+ Py_DECREF(py_result);
+ return result;
+ }
+}
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject* x) {
+ const unsigned char neg_one = (unsigned char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned char" :
+ "value too large to convert to unsigned char");
+ }
+ return (unsigned char)-1;
+ }
+ return (unsigned char)val;
+ }
+ return (unsigned char)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject* x) {
+ const unsigned short neg_one = (unsigned short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned short" :
+ "value too large to convert to unsigned short");
+ }
+ return (unsigned short)-1;
+ }
+ return (unsigned short)val;
+ }
+ return (unsigned short)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject* x) {
+ const unsigned int neg_one = (unsigned int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned int" :
+ "value too large to convert to unsigned int");
+ }
+ return (unsigned int)-1;
+ }
+ return (unsigned int)val;
+ }
+ return (unsigned int)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject* x) {
+ const char neg_one = (char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to char" :
+ "value too large to convert to char");
+ }
+ return (char)-1;
+ }
+ return (char)val;
+ }
+ return (char)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject* x) {
+ const short neg_one = (short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to short" :
+ "value too large to convert to short");
+ }
+ return (short)-1;
+ }
+ return (short)val;
+ }
+ return (short)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject* x) {
+ const signed char neg_one = (signed char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed char" :
+ "value too large to convert to signed char");
+ }
+ return (signed char)-1;
+ }
+ return (signed char)val;
+ }
+ return (signed char)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject* x) {
+ const signed short neg_one = (signed short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed short" :
+ "value too large to convert to signed short");
+ }
+ return (signed short)-1;
+ }
+ return (signed short)val;
+ }
+ return (signed short)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject* x) {
+ const signed int neg_one = (signed int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed int" :
+ "value too large to convert to signed int");
+ }
+ return (signed int)-1;
+ }
+ return (signed int)val;
+ }
+ return (signed int)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject* x) {
+ const unsigned long neg_one = (unsigned long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return (unsigned long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return (unsigned long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (unsigned long)PyLong_AsLong(x);
+ }
+ } else {
+ unsigned long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned long)-1;
+ val = __Pyx_PyInt_AsUnsignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject* x) {
+ const unsigned PY_LONG_LONG neg_one = (unsigned PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return (unsigned PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return (unsigned PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (unsigned PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ unsigned PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsUnsignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject* x) {
+ const long neg_one = (long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return (long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return (long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (long)PyLong_AsLong(x);
+ }
+ } else {
+ long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (long)-1;
+ val = __Pyx_PyInt_AsLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject* x) {
+ const PY_LONG_LONG neg_one = (PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return (PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return (PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject* x) {
+ const signed long neg_one = (signed long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return (signed long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return (signed long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (signed long)PyLong_AsLong(x);
+ }
+ } else {
+ signed long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed long)-1;
+ val = __Pyx_PyInt_AsSignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject* x) {
+ const signed PY_LONG_LONG neg_one = (signed PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return (signed PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return (signed PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (signed PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ signed PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsSignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static int __Pyx_check_binary_version(void) {
+ char ctversion[4], rtversion[4];
+ PyOS_snprintf(ctversion, 4, "%d.%d", PY_MAJOR_VERSION, PY_MINOR_VERSION);
+ PyOS_snprintf(rtversion, 4, "%s", Py_GetVersion());
+ if (ctversion[0] != rtversion[0] || ctversion[2] != rtversion[2]) {
+ char message[200];
+ PyOS_snprintf(message, sizeof(message),
+ "compiletime version %s of module '%.100s' "
+ "does not match runtime version %s",
+ ctversion, __Pyx_MODULE_NAME, rtversion);
+ #if PY_VERSION_HEX < 0x02050000
+ return PyErr_Warn(NULL, message);
+ #else
+ return PyErr_WarnEx(NULL, message, 1);
+ #endif
+ }
+ return 0;
+}
+
+#include "compile.h"
+#include "frameobject.h"
+#include "traceback.h"
+
+static void __Pyx_AddTraceback(const char *funcname, int __pyx_clineno,
+ int __pyx_lineno, const char *__pyx_filename) {
+ PyObject *py_srcfile = 0;
+ PyObject *py_funcname = 0;
+ PyObject *py_globals = 0;
+ PyCodeObject *py_code = 0;
+ PyFrameObject *py_frame = 0;
+
+ #if PY_MAJOR_VERSION < 3
+ py_srcfile = PyString_FromString(__pyx_filename);
+ #else
+ py_srcfile = PyUnicode_FromString(__pyx_filename);
+ #endif
+ if (!py_srcfile) goto bad;
+ if (__pyx_clineno) {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #else
+ py_funcname = PyUnicode_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #endif
+ }
+ else {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromString(funcname);
+ #else
+ py_funcname = PyUnicode_FromString(funcname);
+ #endif
+ }
+ if (!py_funcname) goto bad;
+ py_globals = PyModule_GetDict(__pyx_m);
+ if (!py_globals) goto bad;
+ py_code = PyCode_New(
+ 0, /*int argcount,*/
+ #if PY_MAJOR_VERSION >= 3
+ 0, /*int kwonlyargcount,*/
+ #endif
+ 0, /*int nlocals,*/
+ 0, /*int stacksize,*/
+ 0, /*int flags,*/
+ __pyx_empty_bytes, /*PyObject *code,*/
+ __pyx_empty_tuple, /*PyObject *consts,*/
+ __pyx_empty_tuple, /*PyObject *names,*/
+ __pyx_empty_tuple, /*PyObject *varnames,*/
+ __pyx_empty_tuple, /*PyObject *freevars,*/
+ __pyx_empty_tuple, /*PyObject *cellvars,*/
+ py_srcfile, /*PyObject *filename,*/
+ py_funcname, /*PyObject *name,*/
+ __pyx_lineno, /*int firstlineno,*/
+ __pyx_empty_bytes /*PyObject *lnotab*/
+ );
+ if (!py_code) goto bad;
+ py_frame = PyFrame_New(
+ PyThreadState_GET(), /*PyThreadState *tstate,*/
+ py_code, /*PyCodeObject *code,*/
+ py_globals, /*PyObject *globals,*/
+ 0 /*PyObject *locals*/
+ );
+ if (!py_frame) goto bad;
+ py_frame->f_lineno = __pyx_lineno;
+ PyTraceBack_Here(py_frame);
+bad:
+ Py_XDECREF(py_srcfile);
+ Py_XDECREF(py_funcname);
+ Py_XDECREF(py_code);
+ Py_XDECREF(py_frame);
+}
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t) {
+ while (t->p) {
+ #if PY_MAJOR_VERSION < 3
+ if (t->is_unicode) {
+ *t->p = PyUnicode_DecodeUTF8(t->s, t->n - 1, NULL);
+ } else if (t->intern) {
+ *t->p = PyString_InternFromString(t->s);
+ } else {
+ *t->p = PyString_FromStringAndSize(t->s, t->n - 1);
+ }
+ #else /* Python 3+ has unicode identifiers */
+ if (t->is_unicode | t->is_str) {
+ if (t->intern) {
+ *t->p = PyUnicode_InternFromString(t->s);
+ } else if (t->encoding) {
+ *t->p = PyUnicode_Decode(t->s, t->n - 1, t->encoding, NULL);
+ } else {
+ *t->p = PyUnicode_FromStringAndSize(t->s, t->n - 1);
+ }
+ } else {
+ *t->p = PyBytes_FromStringAndSize(t->s, t->n - 1);
+ }
+ #endif
+ if (!*t->p)
+ return -1;
+ ++t;
+ }
+ return 0;
+}
+
+/* Type Conversion Functions */
+
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject* x) {
+ int is_true = x == Py_True;
+ if (is_true | (x == Py_False) | (x == Py_None)) return is_true;
+ else return PyObject_IsTrue(x);
+}
+
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x) {
+ PyNumberMethods *m;
+ const char *name = NULL;
+ PyObject *res = NULL;
+#if PY_VERSION_HEX < 0x03000000
+ if (PyInt_Check(x) || PyLong_Check(x))
+#else
+ if (PyLong_Check(x))
+#endif
+ return Py_INCREF(x), x;
+ m = Py_TYPE(x)->tp_as_number;
+#if PY_VERSION_HEX < 0x03000000
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Int(x);
+ }
+ else if (m && m->nb_long) {
+ name = "long";
+ res = PyNumber_Long(x);
+ }
+#else
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Long(x);
+ }
+#endif
+ if (res) {
+#if PY_VERSION_HEX < 0x03000000
+ if (!PyInt_Check(res) && !PyLong_Check(res)) {
+#else
+ if (!PyLong_Check(res)) {
+#endif
+ PyErr_Format(PyExc_TypeError,
+ "__%s__ returned non-%s (type %.200s)",
+ name, name, Py_TYPE(res)->tp_name);
+ Py_DECREF(res);
+ return NULL;
+ }
+ }
+ else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_TypeError,
+ "an integer is required");
+ }
+ return res;
+}
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject* b) {
+ Py_ssize_t ival;
+ PyObject* x = PyNumber_Index(b);
+ if (!x) return -1;
+ ival = PyInt_AsSsize_t(x);
+ Py_DECREF(x);
+ return ival;
+}
+
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t ival) {
+#if PY_VERSION_HEX < 0x02050000
+ if (ival <= LONG_MAX)
+ return PyInt_FromLong((long)ival);
+ else {
+ unsigned char *bytes = (unsigned char *) &ival;
+ int one = 1; int little = (int)*(unsigned char*)&one;
+ return _PyLong_FromByteArray(bytes, sizeof(size_t), little, 0);
+ }
+#else
+ return PyInt_FromSize_t(ival);
+#endif
+}
+
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject* x) {
+ unsigned PY_LONG_LONG val = __Pyx_PyInt_AsUnsignedLongLong(x);
+ if (unlikely(val == (unsigned PY_LONG_LONG)-1 && PyErr_Occurred())) {
+ return (size_t)-1;
+ } else if (unlikely(val != (unsigned PY_LONG_LONG)(size_t)val)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "value too large to convert to size_t");
+ return (size_t)-1;
+ }
+ return (size_t)val;
+}
+
+
+#endif /* Py_PYTHON_H */
diff --git a/twisted/internet/iocpreactor/iocpsupport/iocpsupport.pyx b/twisted/internet/iocpreactor/iocpsupport/iocpsupport.pyx
new file mode 100644
index 0000000..97cf634
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/iocpsupport.pyx
@@ -0,0 +1,312 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+# HANDLE and SOCKET are pointer-sized (they are 64 bit wide in 64-bit builds)
+ctypedef size_t HANDLE
+ctypedef size_t SOCKET
+ctypedef unsigned long DWORD
+# it's really a pointer, but we use it as an integer
+ctypedef size_t ULONG_PTR
+ctypedef int BOOL
+
+cdef extern from 'io.h':
+ long _get_osfhandle(int filehandle)
+
+cdef extern from 'errno.h':
+ int errno
+ enum:
+ EBADF
+
+cdef extern from 'winsock2.h':
+ pass
+
+cdef extern from 'ws2tcpip.h':
+ pass
+
+cdef extern from 'windows.h':
+ ctypedef struct OVERLAPPED:
+ pass
+ HANDLE CreateIoCompletionPort(HANDLE fileHandle, HANDLE existing, ULONG_PTR key, DWORD numThreads)
+ BOOL GetQueuedCompletionStatus(HANDLE port, DWORD *bytes, ULONG_PTR *key, OVERLAPPED **ov, DWORD timeout)
+ BOOL PostQueuedCompletionStatus(HANDLE port, DWORD bytes, ULONG_PTR key, OVERLAPPED *ov)
+ DWORD GetLastError()
+ BOOL CloseHandle(HANDLE h)
+ enum:
+ INVALID_HANDLE_VALUE
+ void DebugBreak()
+
+cdef extern from 'python.h':
+ struct PyObject:
+ pass
+ void *PyMem_Malloc(size_t n) except NULL
+ void PyMem_Free(void *p)
+ struct PyThreadState:
+ pass
+ PyThreadState *PyEval_SaveThread()
+ void PyEval_RestoreThread(PyThreadState *tstate)
+ void Py_INCREF(object o)
+ void Py_XINCREF(object o)
+ void Py_DECREF(object o)
+ void Py_XDECREF(object o)
+ int PyObject_AsWriteBuffer(object obj, void **buffer, Py_ssize_t *buffer_len) except -1
+ int PyObject_AsReadBuffer(object obj, void **buffer, Py_ssize_t *buffer_len) except -1
+ object PyString_FromString(char *v)
+ object PyString_FromStringAndSize(char *v, Py_ssize_t len)
+ object PyBuffer_New(Py_ssize_t size)
+ char *PyString_AsString(object obj) except NULL
+ object PySequence_Fast(object o, char *m)
+# object PySequence_Fast_GET_ITEM(object o, Py_ssize_t i)
+ PyObject** PySequence_Fast_ITEMS(object o)
+ PyObject* PySequence_ITEM(PyObject *o, Py_ssize_t i)
+ Py_ssize_t PySequence_Fast_GET_SIZE(object o)
+
+cdef extern from '':
+ struct sockaddr:
+ unsigned short int sa_family
+ char sa_data[0]
+ cdef struct in_addr:
+ unsigned long s_addr
+ struct sockaddr_in:
+ int sin_port
+ in_addr sin_addr
+ cdef struct in6_addr:
+ char s6_addr[16]
+ struct sockaddr_in6:
+ short int sin6_family
+ unsigned short int sin6_port
+ unsigned long int sin6_flowinfo
+ in6_addr sin6_addr
+ unsigned long int sin6_scope_id
+ int getsockopt(SOCKET s, int level, int optname, char *optval, int *optlen)
+ enum:
+ SOL_SOCKET
+ SO_PROTOCOL_INFO
+ SOCKET_ERROR
+ ERROR_IO_PENDING
+ AF_INET
+ AF_INET6
+ INADDR_ANY
+ ctypedef struct WSAPROTOCOL_INFO:
+ int iMaxSockAddr
+ int iAddressFamily
+ int WSAGetLastError()
+ char *inet_ntoa(in_addr ina)
+ unsigned long inet_addr(char *cp)
+ unsigned short ntohs(unsigned short netshort)
+ unsigned short htons(unsigned short hostshort)
+ ctypedef struct WSABUF:
+ long len
+ char *buf
+# cdef struct TRANSMIT_FILE_BUFFERS:
+# pass
+ int WSARecv(SOCKET s, WSABUF *buffs, DWORD buffcount, DWORD *bytes, DWORD *flags, OVERLAPPED *ov, void *crud)
+ int WSARecvFrom(SOCKET s, WSABUF *buffs, DWORD buffcount, DWORD *bytes, DWORD *flags, sockaddr *fromaddr, int *fromlen, OVERLAPPED *ov, void *crud)
+ int WSASend(SOCKET s, WSABUF *buffs, DWORD buffcount, DWORD *bytes, DWORD flags, OVERLAPPED *ov, void *crud)
+ int WSAAddressToStringA(sockaddr *lpsaAddress, DWORD dwAddressLength,
+ WSAPROTOCOL_INFO *lpProtocolInfo,
+ char *lpszAddressString,
+ DWORD *lpdwAddressStringLength)
+ int WSAStringToAddressA(char *AddressString, int AddressFamily,
+ WSAPROTOCOL_INFO *lpProtocolInfo,
+ sockaddr *lpAddress, int *lpAddressLength)
+
+cdef extern from 'string.h':
+ void *memset(void *s, int c, size_t n)
+
+cdef extern from 'winsock_pointers.h':
+ int initWinsockPointers()
+ BOOL (*lpAcceptEx)(SOCKET listening, SOCKET accepting, void *buffer, DWORD recvlen, DWORD locallen, DWORD remotelen, DWORD *bytes, OVERLAPPED *ov)
+ void (*lpGetAcceptExSockaddrs)(void *buffer, DWORD recvlen, DWORD locallen, DWORD remotelen, sockaddr **localaddr, int *locallen, sockaddr **remoteaddr, int *remotelen)
+ BOOL (*lpConnectEx)(SOCKET s, sockaddr *name, int namelen, void *buff, DWORD sendlen, DWORD *sentlen, OVERLAPPED *ov)
+# BOOL (*lpTransmitFile)(SOCKET s, HANDLE hFile, DWORD size, DWORD buffer_size, OVERLAPPED *ov, TRANSMIT_FILE_BUFFERS *buff, DWORD flags)
+
+cdef struct myOVERLAPPED:
+ OVERLAPPED ov
+ PyObject *obj
+
+cdef myOVERLAPPED *makeOV() except NULL:
+ cdef myOVERLAPPED *res
+ res = <myOVERLAPPED *>PyMem_Malloc(sizeof(myOVERLAPPED))
+ if not res:
+ raise MemoryError
+ memset(res, 0, sizeof(myOVERLAPPED))
+ return res
+
+cdef void raise_error(int err, object message) except *:
+ if not err:
+ err = GetLastError()
+ raise WindowsError(message, err)
+
+class Event:
+ def __init__(self, callback, owner, **kw):
+ self.callback = callback
+ self.owner = owner
+ for k, v in kw.items():
+ setattr(self, k, v)
+
+cdef class CompletionPort:
+ cdef HANDLE port
+ def __init__(self):
+ cdef HANDLE res
+ res = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0)
+ if not res:
+ raise_error(0, 'CreateIoCompletionPort')
+ self.port = res
+
+ def addHandle(self, HANDLE handle, size_t key=0):
+ cdef HANDLE res
+ res = CreateIoCompletionPort(handle, self.port, key, 0)
+ if not res:
+ raise_error(0, 'CreateIoCompletionPort')
+
+ def getEvent(self, long timeout):
+ cdef PyThreadState *_save
+ cdef unsigned long bytes, rc
+ cdef size_t key
+ cdef myOVERLAPPED *ov
+
+ _save = PyEval_SaveThread()
+ rc = GetQueuedCompletionStatus(self.port, &bytes, &key, <OVERLAPPED **>&ov, timeout)
+ PyEval_RestoreThread(_save)
+
+ if not rc:
+ rc = GetLastError()
+ else:
+ rc = 0
+
+ obj = None
+ if ov:
+ if ov.obj:
+ obj = <object>ov.obj
+ Py_DECREF(obj) # we are stealing a reference here
+ PyMem_Free(ov)
+
+ return (rc, bytes, key, obj)
+
+ def postEvent(self, unsigned long bytes, size_t key, obj):
+ cdef myOVERLAPPED *ov
+ cdef unsigned long rc
+
+ if obj is not None:
+ ov = makeOV()
+ Py_INCREF(obj) # give ov its own reference to obj
+ ov.obj = <PyObject *>obj
+ else:
+ ov = NULL
+
+ rc = PostQueuedCompletionStatus(self.port, bytes, key, <OVERLAPPED *>ov)
+ if not rc:
+ if ov:
+ Py_DECREF(obj)
+ PyMem_Free(ov)
+ raise_error(0, 'PostQueuedCompletionStatus')
+
+ def __del__(self):
+ CloseHandle(self.port)
+
+def makesockaddr(object buff):
+ cdef void *mem_buffer
+ cdef Py_ssize_t size
+
+ PyObject_AsReadBuffer(buff, &mem_buffer, &size)
+ # XXX: this should really return the address family as well
+ return _makesockaddr(<sockaddr *>mem_buffer, size)
+
+cdef object _makesockaddr(sockaddr *addr, Py_ssize_t len):
+ cdef sockaddr_in *sin
+ cdef sockaddr_in6 *sin6
+ cdef char buff[256]
+ cdef int rc
+ cdef DWORD buff_size = sizeof(buff)
+ if not len:
+ return None
+ if addr.sa_family == AF_INET:
+ sin = <sockaddr_in *>addr
+ return PyString_FromString(inet_ntoa(sin.sin_addr)), ntohs(sin.sin_port)
+ elif addr.sa_family == AF_INET6:
+ sin6 = <sockaddr_in6 *>addr
+ rc = WSAAddressToStringA(addr, sizeof(sockaddr_in6), NULL, buff, &buff_size)
+ if rc == SOCKET_ERROR:
+ raise_error(0, 'WSAAddressToString')
+ host, sa_port = PyString_FromString(buff), ntohs(sin6.sin6_port)
+ host, port = host.rsplit(':', 1)
+ port = int(port)
+ assert host[0] == '['
+ assert host[-1] == ']'
+ assert port == sa_port
+ return host[1:-1], port
+ else:
+ return PyString_FromStringAndSize(addr.sa_data, sizeof(addr.sa_data))
+
+
+cdef object fillinetaddr(sockaddr_in *dest, object addr):
+ cdef unsigned short port
+ cdef unsigned long res
+ cdef char *hoststr
+ host, port = addr
+
+ hoststr = PyString_AsString(host)
+ res = inet_addr(hoststr)
+ if res == INADDR_ANY:
+ raise ValueError, 'invalid IP address'
+ dest.sin_addr.s_addr = res
+
+ dest.sin_port = htons(port)
+
+
+cdef object fillinet6addr(sockaddr_in6 *dest, object addr):
+ cdef unsigned short port
+ cdef unsigned long res
+ cdef char *hoststr
+ cdef int addrlen = sizeof(sockaddr_in6)
+ host, port, flow, scope = addr
+ host = host.split("%")[0] # remove scope ID, if any
+
+ hoststr = PyString_AsString(host)
+ cdef int parseresult = WSAStringToAddressA(hoststr, AF_INET6, NULL,
+ <sockaddr *>dest, &addrlen)
+ if parseresult == SOCKET_ERROR:
+ raise ValueError, 'invalid IPv6 address %r' % (host,)
+ if parseresult != 0:
+ raise RuntimeError, 'undefined error occurred during address parsing'
+ # sin6_host field was handled by WSAStringToAddress
+ dest.sin6_port = htons(port)
+ dest.sin6_flowinfo = flow
+ dest.sin6_scope_id = scope
+
+
+def AllocateReadBuffer(int size):
+ return PyBuffer_New(size)
+
+def maxAddrLen(long s):
+ cdef WSAPROTOCOL_INFO wsa_pi
+ cdef int size, rc
+
+ size = sizeof(wsa_pi)
+ rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ if rc == SOCKET_ERROR:
+ raise_error(WSAGetLastError(), 'getsockopt')
+ return wsa_pi.iMaxSockAddr
+
+cdef int getAddrFamily(SOCKET s) except *:
+ cdef WSAPROTOCOL_INFO wsa_pi
+ cdef int size, rc
+
+ size = sizeof(wsa_pi)
+ rc = getsockopt(s, SOL_SOCKET, SO_PROTOCOL_INFO, <char *>&wsa_pi, &size)
+ if rc == SOCKET_ERROR:
+ raise_error(WSAGetLastError(), 'getsockopt')
+ return wsa_pi.iAddressFamily
+
+import socket # for WSAStartup
+if not initWinsockPointers():
+ raise ValueError, 'Failed to initialize Winsock function vectors'
+
+have_connectex = (lpConnectEx != NULL)
+
+include 'acceptex.pxi'
+include 'connectex.pxi'
+include 'wsarecv.pxi'
+include 'wsasend.pxi'
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c b/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c
new file mode 100644
index 0000000..9bd115a
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c
@@ -0,0 +1,62 @@
+/* Copyright (c) 2008 Twisted Matrix Laboratories.
+ * See LICENSE for details.
+ */
+
+
+#include<winsock2.h>
+#include<assert.h>
+#include<stdio.h>
+#include<stdlib.h>
+
+#ifndef WSAID_CONNECTEX
+#define WSAID_CONNECTEX {0x25a207b9,0xddf3,0x4660,{0x8e,0xe9,0x76,0xe5,0x8c,0x74,0x06,0x3e}}
+#endif
+#ifndef WSAID_GETACCEPTEXSOCKADDRS
+#define WSAID_GETACCEPTEXSOCKADDRS {0xb5367df2,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+#endif
+#ifndef WSAID_ACCEPTEX
+#define WSAID_ACCEPTEX {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+#endif
+/*#ifndef WSAID_TRANSMITFILE
+#define WSAID_TRANSMITFILE {0xb5367df0,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
+#endif*/
+
+
+void *lpAcceptEx, *lpGetAcceptExSockaddrs, *lpConnectEx, *lpTransmitFile;
+
+int initPointer(SOCKET s, void **fun, GUID guid) {
+ int res;
+ DWORD bytes;
+
+ *fun = NULL;
+ res = WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER,
+ &guid, sizeof(guid),
+ fun, sizeof(fun),
+ &bytes, NULL, NULL);
+ return !res;
+}
+
+int initWinsockPointers() {
+ SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
+ /* I hate C */
+ GUID guid1 = WSAID_ACCEPTEX;
+ GUID guid2 = WSAID_GETACCEPTEXSOCKADDRS;
+ GUID guid3 = WSAID_CONNECTEX;
+ /*GUID guid4 = WSAID_TRANSMITFILE;*/
+ if (!s) {
+ return 0;
+ }
+ if (!initPointer(s, &lpAcceptEx, guid1))
+ {
+ return 0;
+ }
+ if (!initPointer(s, &lpGetAcceptExSockaddrs, guid2)) {
+ return 0;
+ }
+ if (!initPointer(s, &lpConnectEx, guid3)) {
+ return 0;
+ };
+ /*initPointer(s, &lpTransmitFile, guid4);*/
+ return 1;
+}
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.h b/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.h
new file mode 100644
index 0000000..83e9ba8
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/winsock_pointers.h
@@ -0,0 +1,51 @@
+/* Copyright (c) 2008 Twisted Matrix Laboratories.
+ * See LICENSE for details.
+ */
+
+
+#include<windows.h>
+
+int initWinsockPointers();
+BOOL
+(PASCAL FAR * lpAcceptEx)(
+ IN SOCKET sListenSocket,
+ IN SOCKET sAcceptSocket,
+ IN PVOID lpOutputBuffer,
+ IN DWORD dwReceiveDataLength,
+ IN DWORD dwLocalAddressLength,
+ IN DWORD dwRemoteAddressLength,
+ OUT LPDWORD lpdwBytesReceived,
+ IN LPOVERLAPPED lpOverlapped
+ );
+VOID
+(PASCAL FAR * lpGetAcceptExSockaddrs)(
+ IN PVOID lpOutputBuffer,
+ IN DWORD dwReceiveDataLength,
+ IN DWORD dwLocalAddressLength,
+ IN DWORD dwRemoteAddressLength,
+ OUT struct sockaddr **LocalSockaddr,
+ OUT LPINT LocalSockaddrLength,
+ OUT struct sockaddr **RemoteSockaddr,
+ OUT LPINT RemoteSockaddrLength
+ );
+BOOL
+(PASCAL FAR * lpConnectEx) (
+ IN SOCKET s,
+ IN const struct sockaddr FAR *name,
+ IN int namelen,
+ IN PVOID lpSendBuffer OPTIONAL,
+ IN DWORD dwSendDataLength,
+ OUT LPDWORD lpdwBytesSent,
+ IN LPOVERLAPPED lpOverlapped
+ );
+/*BOOL
+(PASCAL FAR * lpTransmitFile)(
+ IN SOCKET hSocket,
+ IN HANDLE hFile,
+ IN DWORD nNumberOfBytesToWrite,
+ IN DWORD nNumberOfBytesPerSend,
+ IN LPOVERLAPPED lpOverlapped,
+ IN LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
+ IN DWORD dwReserved
+ );*/
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/wsarecv.pxi b/twisted/internet/iocpreactor/iocpsupport/wsarecv.pxi
new file mode 100644
index 0000000..58c391e
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/wsarecv.pxi
@@ -0,0 +1,76 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def recv(long s, object bufflist, object obj, unsigned long flags = 0):
+ cdef int rc, res
+ cdef myOVERLAPPED *ov
+ cdef WSABUF *ws_buf
+ cdef unsigned long bytes
+ cdef PyObject **buffers
+ cdef Py_ssize_t i, size, buffcount
+
+ bufflist = PySequence_Fast(bufflist, 'second argument needs to be a list')
+ buffcount = PySequence_Fast_GET_SIZE(bufflist)
+ buffers = PySequence_Fast_ITEMS(bufflist)
+
+ ws_buf = <WSABUF *>PyMem_Malloc(buffcount*sizeof(WSABUF))
+
+ try:
+ for i from 0 <= i < buffcount:
+ PyObject_AsWriteBuffer(<object>buffers[i], <void **>&ws_buf[i].buf, &size)
+ ws_buf[i].len = <DWORD>size
+
+ ov = makeOV()
+ if obj is not None:
+ ov.obj = <PyObject *>obj
+
+ rc = WSARecv(s, ws_buf, <DWORD>buffcount, &bytes, &flags, <OVERLAPPED *>ov, NULL)
+
+ if rc == SOCKET_ERROR:
+ rc = WSAGetLastError()
+ if rc != ERROR_IO_PENDING:
+ PyMem_Free(ov)
+ return rc, 0
+
+ Py_XINCREF(obj)
+ return rc, bytes
+ finally:
+ PyMem_Free(ws_buf)
+
+def recvfrom(long s, object buff, object addr_buff, object addr_len_buff, object obj, unsigned long flags = 0):
+ cdef int rc, c_addr_buff_len, c_addr_len_buff_len
+ cdef myOVERLAPPED *ov
+ cdef WSABUF ws_buf
+ cdef unsigned long bytes
+ cdef sockaddr *c_addr_buff
+ cdef int *c_addr_len_buff
+ cdef Py_ssize_t size
+
+ PyObject_AsWriteBuffer(buff, <void **>&ws_buf.buf, &size)
+ ws_buf.len = <DWORD>size
+ PyObject_AsWriteBuffer(addr_buff, <void **>&c_addr_buff, &size)
+ c_addr_buff_len = <int>size
+ PyObject_AsWriteBuffer(addr_len_buff, <void **>&c_addr_len_buff, &size)
+ c_addr_len_buff_len = <int>size
+
+ if c_addr_len_buff_len != sizeof(int):
+ raise ValueError, 'length of address length buffer needs to be sizeof(int)'
+
+ c_addr_len_buff[0] = c_addr_buff_len
+
+ ov = makeOV()
+ if obj is not None:
+ ov.obj = <PyObject *>obj
+
+ rc = WSARecvFrom(s, &ws_buf, 1, &bytes, &flags, c_addr_buff, c_addr_len_buff, <OVERLAPPED *>ov, NULL)
+
+ if rc == SOCKET_ERROR:
+ rc = WSAGetLastError()
+ if rc != ERROR_IO_PENDING:
+ PyMem_Free(ov)
+ return rc, 0
+
+ Py_XINCREF(obj)
+ return rc, bytes
+
diff --git a/twisted/internet/iocpreactor/iocpsupport/wsasend.pxi b/twisted/internet/iocpreactor/iocpsupport/wsasend.pxi
new file mode 100644
index 0000000..4ad59ca
--- /dev/null
+++ b/twisted/internet/iocpreactor/iocpsupport/wsasend.pxi
@@ -0,0 +1,30 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def send(long s, object buff, object obj, unsigned long flags = 0):
+ cdef int rc
+ cdef myOVERLAPPED *ov
+ cdef WSABUF ws_buf
+ cdef unsigned long bytes
+ cdef Py_ssize_t size
+
+ PyObject_AsReadBuffer(buff, <void **>&ws_buf.buf, &size)
+ ws_buf.len = <DWORD>size
+
+ ov = makeOV()
+ if obj is not None:
+ ov.obj = <PyObject *>obj
+
+ rc = WSASend(s, &ws_buf, 1, &bytes, flags, <OVERLAPPED *>ov, NULL)
+
+ if rc == SOCKET_ERROR:
+ rc = WSAGetLastError()
+ if rc != ERROR_IO_PENDING:
+ PyMem_Free(ov)
+ return rc, bytes
+
+ Py_XINCREF(obj)
+ return rc, bytes
+
+
diff --git a/twisted/internet/iocpreactor/notes.txt b/twisted/internet/iocpreactor/notes.txt
new file mode 100644
index 0000000..4caffb8
--- /dev/null
+++ b/twisted/internet/iocpreactor/notes.txt
@@ -0,0 +1,24 @@
+test specifically:
+failed accept error message -- similar to test_tcp_internals
+immediate success on accept/connect/recv, including Event.ignore
+parametrize iocpsupport somehow -- via reactor?
+
+do:
+break handling -- WaitForSingleObject on the IOCP handle?
+iovecs for write buffer
+do not wait for a mainloop iteration if resumeProducing (in _handleWrite) does startWriting
+don't addActiveHandle in every call to startWriting/startReading
+iocpified process support
+ win32er-in-a-thread (or run GQCS in a thread -- it can't receive SIGBREAK)
+blocking in sendto() -- I think Windows can do that, especially with local UDP
+
+buildbot:
+run in vmware
+start from a persistent snapshot
+
+use a stub inside the vm to svnup/run tests/collect stdio
+lift logs through SMB? or ship them via tcp beams to the VM host
+
+have a timeout on the test run
+if we time out, take a screenshot, save it, kill the VM
+
diff --git a/twisted/internet/iocpreactor/reactor.py b/twisted/internet/iocpreactor/reactor.py
new file mode 100644
index 0000000..0c565ab
--- /dev/null
+++ b/twisted/internet/iocpreactor/reactor.py
@@ -0,0 +1,275 @@
+# -*- test-case-name: twisted.internet.test.test_iocp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Reactor that uses IO completion ports
+"""
+
+import warnings, socket, sys
+
+from zope.interface import implements
+
+from twisted.internet import base, interfaces, main, error
+from twisted.python import log, failure
+from twisted.internet._dumbwin32proc import Process
+from twisted.internet.win32eventreactor import _ThreadedWin32EventsMixin
+
+from twisted.internet.iocpreactor import iocpsupport as _iocp
+from twisted.internet.iocpreactor.const import WAIT_TIMEOUT
+from twisted.internet.iocpreactor import tcp, udp
+
+try:
+ from twisted.protocols.tls import TLSMemoryBIOFactory
+except ImportError:
+ # Either pyOpenSSL isn't installed, or it is too old for this code to work.
+ # The reactor won't provide IReactorSSL.
+ TLSMemoryBIOFactory = None
+ _extraInterfaces = ()
+ warnings.warn(
+ "pyOpenSSL 0.10 or newer is required for SSL support in iocpreactor. "
+ "It is missing, so the reactor will not support SSL APIs.")
+else:
+ _extraInterfaces = (interfaces.IReactorSSL,)
+
+from twisted.python.compat import set
+
+MAX_TIMEOUT = 2000 # 2 seconds, see doIteration for explanation
+
+EVENTS_PER_LOOP = 1000 # XXX: what's a good value here?
+
+# keys to associate with normal and waker events
+KEY_NORMAL, KEY_WAKEUP = range(2)
+
+_NO_GETHANDLE = error.ConnectionFdescWentAway(
+ 'Handler has no getFileHandle method')
+_NO_FILEDESC = error.ConnectionFdescWentAway('Filedescriptor went away')
+
+
+
+class IOCPReactor(base._SignalReactorMixin, base.ReactorBase,
+ _ThreadedWin32EventsMixin):
+ implements(interfaces.IReactorTCP, interfaces.IReactorUDP,
+ interfaces.IReactorMulticast, interfaces.IReactorProcess,
+ *_extraInterfaces)
+
+ port = None
+
+ def __init__(self):
+ base.ReactorBase.__init__(self)
+ self.port = _iocp.CompletionPort()
+ self.handles = set()
+
+
+ def addActiveHandle(self, handle):
+ self.handles.add(handle)
+
+
+ def removeActiveHandle(self, handle):
+ self.handles.discard(handle)
+
+
+ def doIteration(self, timeout):
+ """
+ Poll the IO completion port for new events.
+ """
+ # This function sits and waits for an IO completion event.
+ #
+ # There are two requirements: process IO events as soon as they arrive
+ # and process ctrl-break from the user in a reasonable amount of time.
+ #
+ # There are three kinds of waiting.
+ # 1) GetQueuedCompletionStatus (self.port.getEvent) to wait for IO
+ # events only.
+ # 2) Msg* family of wait functions that can stop waiting when
+ # ctrl-break is detected (then, I think, Python converts it into a
+ # KeyboardInterrupt)
+ # 3) *Ex family of wait functions that put the thread into an
+ # "alertable" wait state which is supposedly triggered by IO completion
+ #
+ # 2) and 3) can be combined. Trouble is, my IO completion is not
+ # causing 3) to trigger, possibly because I do not use an IO completion
+ # callback. Windows is weird.
+ # There are two ways to handle this. I could use MsgWaitForSingleObject
+ # here and GetQueuedCompletionStatus in a thread. Or I could poll with
+ # a reasonable interval. Guess what! Threads are hard.
+
+ processed_events = 0
+ if timeout is None:
+ timeout = MAX_TIMEOUT
+ else:
+ timeout = min(MAX_TIMEOUT, int(1000*timeout))
+ rc, bytes, key, evt = self.port.getEvent(timeout)
+ while 1:
+ if rc == WAIT_TIMEOUT:
+ break
+ if key != KEY_WAKEUP:
+ assert key == KEY_NORMAL
+ log.callWithLogger(evt.owner, self._callEventCallback,
+ rc, bytes, evt)
+ processed_events += 1
+ if processed_events >= EVENTS_PER_LOOP:
+ break
+ rc, bytes, key, evt = self.port.getEvent(0)
+
+
+ def _callEventCallback(self, rc, bytes, evt):
+ owner = evt.owner
+ why = None
+ try:
+ evt.callback(rc, bytes, evt)
+ handfn = getattr(owner, 'getFileHandle', None)
+ if not handfn:
+ why = _NO_GETHANDLE
+ elif handfn() == -1:
+ why = _NO_FILEDESC
+ if why:
+ return # ignore handles that were closed
+ except:
+ why = sys.exc_info()[1]
+ log.err()
+ if why:
+ owner.loseConnection(failure.Failure(why))
+
+
+ def installWaker(self):
+ pass
+
+
+ def wakeUp(self):
+ self.port.postEvent(0, KEY_WAKEUP, None)
+
+
+ def registerHandle(self, handle):
+ self.port.addHandle(handle, KEY_NORMAL)
+
+
+ def createSocket(self, af, stype):
+ skt = socket.socket(af, stype)
+ self.registerHandle(skt.fileno())
+ return skt
+
+
+ def listenTCP(self, port, factory, backlog=50, interface=''):
+ """
+ @see: twisted.internet.interfaces.IReactorTCP.listenTCP
+ """
+ p = tcp.Port(port, factory, backlog, interface, self)
+ p.startListening()
+ return p
+
+
+ def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
+ """
+ @see: twisted.internet.interfaces.IReactorTCP.connectTCP
+ """
+ c = tcp.Connector(host, port, factory, timeout, bindAddress, self)
+ c.connect()
+ return c
+
+
+ if TLSMemoryBIOFactory is not None:
+ def listenSSL(self, port, factory, contextFactory, backlog=50, interface=''):
+ """
+ @see: twisted.internet.interfaces.IReactorSSL.listenSSL
+ """
+ port = self.listenTCP(
+ port,
+ TLSMemoryBIOFactory(contextFactory, False, factory),
+ backlog, interface)
+ port._type = 'TLS'
+ return port
+
+
+ def connectSSL(self, host, port, factory, contextFactory, timeout=30, bindAddress=None):
+ """
+ @see: twisted.internet.interfaces.IReactorSSL.connectSSL
+ """
+ return self.connectTCP(
+ host, port,
+ TLSMemoryBIOFactory(contextFactory, True, factory),
+ timeout, bindAddress)
+ else:
+ def listenSSL(self, port, factory, contextFactory, backlog=50, interface=''):
+ """
+ Non-implementation of L{IReactorSSL.listenSSL}. Some dependency
+ is not satisfied. This implementation always raises
+ L{NotImplementedError}.
+ """
+ raise NotImplementedError(
+ "pyOpenSSL 0.10 or newer is required for SSL support in "
+ "iocpreactor. It is missing, so the reactor does not support "
+ "SSL APIs.")
+
+
+ def connectSSL(self, host, port, factory, contextFactory, timeout=30, bindAddress=None):
+ """
+ Non-implementation of L{IReactorSSL.connectSSL}. Some dependency
+ is not satisfied. This implementation always raises
+ L{NotImplementedError}.
+ """
+ raise NotImplementedError(
+ "pyOpenSSL 0.10 or newer is required for SSL support in "
+ "iocpreactor. It is missing, so the reactor does not support "
+ "SSL APIs.")
+
+
+ def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
+ """
+ Connects a given L{DatagramProtocol} to the given numeric UDP port.
+
+ @returns: object conforming to L{IListeningPort}.
+ """
+ p = udp.Port(port, protocol, interface, maxPacketSize, self)
+ p.startListening()
+ return p
+
+
+ def listenMulticast(self, port, protocol, interface='', maxPacketSize=8192,
+ listenMultiple=False):
+ """
+ Connects a given DatagramProtocol to the given numeric UDP port.
+
+ EXPERIMENTAL.
+
+ @returns: object conforming to IListeningPort.
+ """
+ p = udp.MulticastPort(port, protocol, interface, maxPacketSize, self,
+ listenMultiple)
+ p.startListening()
+ return p
+
+
+ def spawnProcess(self, processProtocol, executable, args=(), env={},
+ path=None, uid=None, gid=None, usePTY=0, childFDs=None):
+ """
+ Spawn a process.
+ """
+ if uid is not None:
+ raise ValueError("Setting UID is unsupported on this platform.")
+ if gid is not None:
+ raise ValueError("Setting GID is unsupported on this platform.")
+ if usePTY:
+ raise ValueError("PTYs are unsupported on this platform.")
+ if childFDs is not None:
+ raise ValueError(
+ "Custom child file descriptor mappings are unsupported on "
+ "this platform.")
+ args, env = self._checkProcessArgs(args, env)
+ return Process(self, processProtocol, executable, args, env, path)
+
+
+ def removeAll(self):
+ res = list(self.handles)
+ self.handles.clear()
+ return res
+
+
+
+def install():
+ r = IOCPReactor()
+ main.installReactor(r)
+
+
+__all__ = ['IOCPReactor', 'install']
+
diff --git a/twisted/internet/iocpreactor/setup.py b/twisted/internet/iocpreactor/setup.py
new file mode 100644
index 0000000..b110fc5
--- /dev/null
+++ b/twisted/internet/iocpreactor/setup.py
@@ -0,0 +1,23 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Distutils file for building low-level IOCP bindings from their Pyrex source
+"""
+
+
+from distutils.core import setup
+from distutils.extension import Extension
+from Cython.Distutils import build_ext
+
+setup(name='iocpsupport',
+ ext_modules=[Extension('iocpsupport',
+ ['iocpsupport/iocpsupport.pyx',
+ 'iocpsupport/winsock_pointers.c'],
+ libraries = ['ws2_32'],
+ )
+ ],
+ cmdclass = {'build_ext': build_ext},
+ )
+
diff --git a/twisted/internet/iocpreactor/tcp.py b/twisted/internet/iocpreactor/tcp.py
new file mode 100644
index 0000000..d34f698
--- /dev/null
+++ b/twisted/internet/iocpreactor/tcp.py
@@ -0,0 +1,578 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+TCP support for IOCP reactor
+"""
+
+import socket, operator, errno, struct
+
+from zope.interface import implements, classImplements
+
+from twisted.internet import interfaces, error, address, main, defer
+from twisted.internet.abstract import _LogOwner, isIPAddress, isIPv6Address
+from twisted.internet.tcp import _SocketCloser, Connector as TCPConnector
+from twisted.internet.tcp import _AbortingMixin, _BaseBaseClient, _BaseTCPClient
+from twisted.python import log, failure, reflect, util
+
+from twisted.internet.iocpreactor import iocpsupport as _iocp, abstract
+from twisted.internet.iocpreactor.interfaces import IReadWriteHandle
+from twisted.internet.iocpreactor.const import ERROR_IO_PENDING
+from twisted.internet.iocpreactor.const import SO_UPDATE_CONNECT_CONTEXT
+from twisted.internet.iocpreactor.const import SO_UPDATE_ACCEPT_CONTEXT
+from twisted.internet.iocpreactor.const import ERROR_CONNECTION_REFUSED
+from twisted.internet.iocpreactor.const import ERROR_NETWORK_UNREACHABLE
+
+try:
+ from twisted.internet._newtls import startTLS as _startTLS
+except ImportError:
+ _startTLS = None
+
+# ConnectEx returns these. XXX: find out what it does for timeout
+connectExErrors = {
+ ERROR_CONNECTION_REFUSED: errno.WSAECONNREFUSED,
+ ERROR_NETWORK_UNREACHABLE: errno.WSAENETUNREACH,
+ }
+
+class Connection(abstract.FileHandle, _SocketCloser, _AbortingMixin):
+ """
+ @ivar TLS: C{False} to indicate the connection is in normal TCP mode,
+ C{True} to indicate that TLS has been started and that operations must
+ be routed through the L{TLSMemoryBIOProtocol} instance.
+ """
+ implements(IReadWriteHandle, interfaces.ITCPTransport,
+ interfaces.ISystemHandle)
+
+ TLS = False
+
+
+ def __init__(self, sock, proto, reactor=None):
+ abstract.FileHandle.__init__(self, reactor)
+ self.socket = sock
+ self.getFileHandle = sock.fileno
+ self.protocol = proto
+
+
+ def getHandle(self):
+ return self.socket
+
+
+ def dataReceived(self, rbuffer):
+ # XXX: some day, we'll have protocols that can handle raw buffers
+ self.protocol.dataReceived(str(rbuffer))
+
+
+ def readFromHandle(self, bufflist, evt):
+ return _iocp.recv(self.getFileHandle(), bufflist, evt)
+
+
+ def writeToHandle(self, buff, evt):
+ """
+ Send C{buff} to current file handle using C{_iocp.send}. The buffer
+ sent is limited to a size of C{self.SEND_LIMIT}.
+ """
+ return _iocp.send(self.getFileHandle(),
+ buffer(buff, 0, self.SEND_LIMIT), evt)
+
+
+ def _closeWriteConnection(self):
+ try:
+ getattr(self.socket, self._socketShutdownMethod)(1)
+ except socket.error:
+ pass
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.writeConnectionLost()
+ except:
+ f = failure.Failure()
+ log.err()
+ self.connectionLost(f)
+
+
+ def readConnectionLost(self, reason):
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.readConnectionLost()
+ except:
+ log.err()
+ self.connectionLost(failure.Failure())
+ else:
+ self.connectionLost(reason)
+
+
+ def connectionLost(self, reason):
+ if self.disconnected:
+ return
+ abstract.FileHandle.connectionLost(self, reason)
+ isClean = (reason is None or
+ not reason.check(error.ConnectionAborted))
+ self._closeSocket(isClean)
+ protocol = self.protocol
+ del self.protocol
+ del self.socket
+ del self.getFileHandle
+ protocol.connectionLost(reason)
+
+
+ def logPrefix(self):
+ """
+ Return the prefix to log with when I own the logging thread.
+ """
+ return self.logstr
+
+
+ def getTcpNoDelay(self):
+ return operator.truth(self.socket.getsockopt(socket.IPPROTO_TCP,
+ socket.TCP_NODELAY))
+
+
+ def setTcpNoDelay(self, enabled):
+ self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, enabled)
+
+
+ def getTcpKeepAlive(self):
+ return operator.truth(self.socket.getsockopt(socket.SOL_SOCKET,
+ socket.SO_KEEPALIVE))
+
+
+ def setTcpKeepAlive(self, enabled):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, enabled)
+
+
+ if _startTLS is not None:
+ def startTLS(self, contextFactory, normal=True):
+ """
+ @see: L{ITLSTransport.startTLS}
+ """
+ _startTLS(self, contextFactory, normal, abstract.FileHandle)
+
+
+ def write(self, data):
+ """
+ Write some data, either directly to the underlying handle or, if TLS
+ has been started, to the L{TLSMemoryBIOProtocol} for it to encrypt and
+ send.
+
+ @see: L{ITCPTransport.write}
+ """
+ if self.disconnected:
+ return
+ if self.TLS:
+ self.protocol.write(data)
+ else:
+ abstract.FileHandle.write(self, data)
+
+
+ def writeSequence(self, iovec):
+ """
+ Write some data, either directly to the underlying handle or, if TLS
+ has been started, to the L{TLSMemoryBIOProtocol} for it to encrypt and
+ send.
+
+ @see: L{ITCPTransport.writeSequence}
+ """
+ if self.disconnected:
+ return
+ if self.TLS:
+ self.protocol.writeSequence(iovec)
+ else:
+ abstract.FileHandle.writeSequence(self, iovec)
+
+
+ def loseConnection(self, reason=None):
+ """
+ Close the underlying handle or, if TLS has been started, first shut it
+ down.
+
+ @see: L{ITCPTransport.loseConnection}
+ """
+ if self.TLS:
+ if self.connected and not self.disconnecting:
+ self.protocol.loseConnection()
+ else:
+ abstract.FileHandle.loseConnection(self, reason)
+
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register a producer.
+
+ If TLS is enabled, the TLS connection handles this.
+ """
+ if self.TLS:
+ # Registering a producer before we're connected shouldn't be a
+ # problem. If we end up with a write(), that's already handled in
+ # the write() code above, and there are no other potential
+ # side-effects.
+ self.protocol.registerProducer(producer, streaming)
+ else:
+ abstract.FileHandle.registerProducer(self, producer, streaming)
+
+
+ def unregisterProducer(self):
+ """
+ Unregister a producer.
+
+ If TLS is enabled, the TLS connection handles this.
+ """
+ if self.TLS:
+ self.protocol.unregisterProducer()
+ else:
+ abstract.FileHandle.unregisterProducer(self)
+
+if _startTLS is not None:
+ classImplements(Connection, interfaces.ITLSTransport)
+
+
+
+class Client(_BaseBaseClient, _BaseTCPClient, Connection):
+ """
+ @ivar _tlsClientDefault: Always C{True}, indicating that this is a client
+ connection, and by default when TLS is negotiated this class will act as
+ a TLS client.
+ """
+ addressFamily = socket.AF_INET
+ socketType = socket.SOCK_STREAM
+
+ _tlsClientDefault = True
+ _commonConnection = Connection
+
+ def __init__(self, host, port, bindAddress, connector, reactor):
+ # ConnectEx documentation says socket _has_ to be bound
+ if bindAddress is None:
+ bindAddress = ('', 0)
+ self.reactor = reactor # createInternetSocket needs this
+ _BaseTCPClient.__init__(self, host, port, bindAddress, connector,
+ reactor)
+
+
+ def createInternetSocket(self):
+ """
+ Create a socket registered with the IOCP reactor.
+
+ @see: L{_BaseTCPClient}
+ """
+ return self.reactor.createSocket(self.addressFamily, self.socketType)
+
+
+ def _collectSocketDetails(self):
+ """
+ Clean up potentially circular references to the socket and to its
+ C{getFileHandle} method.
+
+ @see: L{_BaseBaseClient}
+ """
+ del self.socket, self.getFileHandle
+
+
+ def _stopReadingAndWriting(self):
+ """
+ Remove the active handle from the reactor.
+
+ @see: L{_BaseBaseClient}
+ """
+ self.reactor.removeActiveHandle(self)
+
+
+ def cbConnect(self, rc, bytes, evt):
+ if rc:
+ rc = connectExErrors.get(rc, rc)
+ self.failIfNotConnected(error.getConnectError((rc,
+ errno.errorcode.get(rc, 'Unknown error'))))
+ else:
+ self.socket.setsockopt(
+ socket.SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT,
+ struct.pack('P', self.socket.fileno()))
+ self.protocol = self.connector.buildProtocol(self.getPeer())
+ self.connected = True
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = logPrefix + ",client"
+ self.protocol.makeConnection(self)
+ self.startReading()
+
+
+ def doConnect(self):
+ if not hasattr(self, "connector"):
+ # this happens if we connector.stopConnecting in
+ # factory.startedConnecting
+ return
+ assert _iocp.have_connectex
+ self.reactor.addActiveHandle(self)
+ evt = _iocp.Event(self.cbConnect, self)
+
+ rc = _iocp.connect(self.socket.fileno(), self.realAddress, evt)
+ if rc and rc != ERROR_IO_PENDING:
+ self.cbConnect(rc, 0, evt)
+
+
+
+class Server(Connection):
+ """
+ Serverside socket-stream connection class.
+
+ I am a serverside network connection transport; a socket which came from an
+ accept() on a server.
+
+ @ivar _tlsClientDefault: Always C{False}, indicating that this is a server
+ connection, and by default when TLS is negotiated this class will act as
+ a TLS server.
+ """
+
+ _tlsClientDefault = False
+
+
+ def __init__(self, sock, protocol, clientAddr, serverAddr, sessionno, reactor):
+ """
+ Server(sock, protocol, client, server, sessionno)
+
+ Initialize me with a socket, a protocol, a descriptor for my peer (a
+ tuple of host, port describing the other end of the connection), an
+ instance of Port, and a session number.
+ """
+ Connection.__init__(self, sock, protocol, reactor)
+ self.serverAddr = serverAddr
+ self.clientAddr = clientAddr
+ self.sessionno = sessionno
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = "%s,%s,%s" % (logPrefix, sessionno, self.clientAddr.host)
+ self.repstr = "<%s #%s on %s>" % (self.protocol.__class__.__name__,
+ self.sessionno, self.serverAddr.port)
+ self.connected = True
+ self.startReading()
+
+
+ def __repr__(self):
+ """
+ A string representation of this connection.
+ """
+ return self.repstr
+
+
+ def getHost(self):
+ """
+ Returns an IPv4Address.
+
+ This indicates the server's address.
+ """
+ return self.serverAddr
+
+
+ def getPeer(self):
+ """
+ Returns an IPv4Address.
+
+ This indicates the client's address.
+ """
+ return self.clientAddr
+
+
+
+class Connector(TCPConnector):
+ def _makeTransport(self):
+ return Client(self.host, self.port, self.bindAddress, self,
+ self.reactor)
+
+
+
+class Port(_SocketCloser, _LogOwner):
+ implements(interfaces.IListeningPort)
+
+ connected = False
+ disconnected = False
+ disconnecting = False
+ addressFamily = socket.AF_INET
+ socketType = socket.SOCK_STREAM
+ _addressType = address.IPv4Address
+ sessionno = 0
+
+ # Actual port number being listened on, only set to a non-None
+ # value when we are actually listening.
+ _realPortNumber = None
+
+ # A string describing the connections which will be created by this port.
+ # Normally this is C{"TCP"}, since this is a TCP port, but when the TLS
+ # implementation re-uses this class it overrides the value with C{"TLS"}.
+ # Only used for logging.
+ _type = 'TCP'
+
+ def __init__(self, port, factory, backlog=50, interface='', reactor=None):
+ self.port = port
+ self.factory = factory
+ self.backlog = backlog
+ self.interface = interface
+ self.reactor = reactor
+ if isIPv6Address(interface):
+ self.addressFamily = socket.AF_INET6
+ self._addressType = address.IPv6Address
+
+
+ def __repr__(self):
+ if self._realPortNumber is not None:
+ return "<%s of %s on %s>" % (self.__class__,
+ self.factory.__class__,
+ self._realPortNumber)
+ else:
+ return "<%s of %s (not listening)>" % (self.__class__,
+ self.factory.__class__)
+
+
+ def startListening(self):
+ try:
+ skt = self.reactor.createSocket(self.addressFamily,
+ self.socketType)
+ # TODO: resolve self.interface if necessary
+ if self.addressFamily == socket.AF_INET6:
+ addr = socket.getaddrinfo(self.interface, self.port)[0][4]
+ else:
+ addr = (self.interface, self.port)
+ skt.bind(addr)
+ except socket.error, le:
+ raise error.CannotListenError, (self.interface, self.port, le)
+
+ self.addrLen = _iocp.maxAddrLen(skt.fileno())
+
+ # Make sure that if we listened on port 0, we update that to
+ # reflect what the OS actually assigned us.
+ self._realPortNumber = skt.getsockname()[1]
+
+ log.msg("%s starting on %s" % (self._getLogPrefix(self.factory),
+ self._realPortNumber))
+
+ self.factory.doStart()
+ skt.listen(self.backlog)
+ self.connected = True
+ self.disconnected = False
+ self.reactor.addActiveHandle(self)
+ self.socket = skt
+ self.getFileHandle = self.socket.fileno
+ self.doAccept()
+
+
+ def loseConnection(self, connDone=failure.Failure(main.CONNECTION_DONE)):
+ """
+ Stop accepting connections on this port.
+
+ This will shut down my socket and call self.connectionLost().
+ It returns a deferred which will fire successfully when the
+ port is actually closed.
+ """
+ self.disconnecting = True
+ if self.connected:
+ self.deferred = defer.Deferred()
+ self.reactor.callLater(0, self.connectionLost, connDone)
+ return self.deferred
+
+ stopListening = loseConnection
+
+
+ def _logConnectionLostMsg(self):
+ """
+ Log message for closing port
+ """
+ log.msg('(%s Port %s Closed)' % (self._type, self._realPortNumber))
+
+
+ def connectionLost(self, reason):
+ """
+ Cleans up the socket.
+ """
+ self._logConnectionLostMsg()
+ self._realPortNumber = None
+ d = None
+ if hasattr(self, "deferred"):
+ d = self.deferred
+ del self.deferred
+
+ self.disconnected = True
+ self.reactor.removeActiveHandle(self)
+ self.connected = False
+ self._closeSocket(True)
+ del self.socket
+ del self.getFileHandle
+
+ try:
+ self.factory.doStop()
+ except:
+ self.disconnecting = False
+ if d is not None:
+ d.errback(failure.Failure())
+ else:
+ raise
+ else:
+ self.disconnecting = False
+ if d is not None:
+ d.callback(None)
+
+
+ def logPrefix(self):
+ """
+ Returns the name of my class, to prefix log entries with.
+ """
+ return reflect.qual(self.factory.__class__)
+
+
+ def getHost(self):
+ """
+ Returns an IPv4Address.
+
+ This indicates the server's address.
+ """
+ host, port = self.socket.getsockname()[:2]
+ return self._addressType('TCP', host, port)
+
+
+ def cbAccept(self, rc, bytes, evt):
+ self.handleAccept(rc, evt)
+ if not (self.disconnecting or self.disconnected):
+ self.doAccept()
+
+
+ def handleAccept(self, rc, evt):
+ if self.disconnecting or self.disconnected:
+ return False
+
+ # possible errors:
+ # (WSAEMFILE, WSAENOBUFS, WSAENFILE, WSAENOMEM, WSAECONNABORTED)
+ if rc:
+ log.msg("Could not accept new connection -- %s (%s)" %
+ (errno.errorcode.get(rc, 'unknown error'), rc))
+ return False
+ else:
+ evt.newskt.setsockopt(
+ socket.SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
+ struct.pack('P', self.socket.fileno()))
+ family, lAddr, rAddr = _iocp.get_accept_addrs(evt.newskt.fileno(),
+ evt.buff)
+ assert family == self.addressFamily
+
+ protocol = self.factory.buildProtocol(
+ self._addressType('TCP', rAddr[0], rAddr[1]))
+ if protocol is None:
+ evt.newskt.close()
+ else:
+ s = self.sessionno
+ self.sessionno = s+1
+ transport = Server(evt.newskt, protocol,
+ self._addressType('TCP', rAddr[0], rAddr[1]),
+ self._addressType('TCP', lAddr[0], lAddr[1]),
+ s, self.reactor)
+ protocol.makeConnection(transport)
+ return True
+
+
+ def doAccept(self):
+ evt = _iocp.Event(self.cbAccept, self)
+
+ # see AcceptEx documentation
+ evt.buff = buff = _iocp.AllocateReadBuffer(2 * (self.addrLen + 16))
+
+ evt.newskt = newskt = self.reactor.createSocket(self.addressFamily,
+ self.socketType)
+ rc = _iocp.accept(self.socket.fileno(), newskt.fileno(), buff, evt)
+
+ if rc and rc != ERROR_IO_PENDING:
+ self.handleAccept(rc, evt)
+
+
diff --git a/twisted/internet/iocpreactor/udp.py b/twisted/internet/iocpreactor/udp.py
new file mode 100644
index 0000000..4dec51f
--- /dev/null
+++ b/twisted/internet/iocpreactor/udp.py
@@ -0,0 +1,382 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+UDP support for IOCP reactor
+"""
+
+import socket, operator, struct, warnings, errno
+
+from zope.interface import implements
+
+from twisted.internet import defer, address, error, interfaces
+from twisted.internet.abstract import isIPAddress
+from twisted.python import log, failure
+
+from twisted.internet.iocpreactor.const import ERROR_IO_PENDING
+from twisted.internet.iocpreactor.const import ERROR_CONNECTION_REFUSED
+from twisted.internet.iocpreactor.const import ERROR_PORT_UNREACHABLE
+from twisted.internet.iocpreactor.interfaces import IReadWriteHandle
+from twisted.internet.iocpreactor import iocpsupport as _iocp, abstract
+
+
+
+class Port(abstract.FileHandle):
+ """
+ UDP port, listening for packets.
+ """
+ implements(
+ IReadWriteHandle, interfaces.IListeningPort, interfaces.IUDPTransport,
+ interfaces.ISystemHandle)
+
+ addressFamily = socket.AF_INET
+ socketType = socket.SOCK_DGRAM
+ dynamicReadBuffers = False
+
+ # Actual port number being listened on, only set to a non-None
+ # value when we are actually listening.
+ _realPortNumber = None
+
+
+ def __init__(self, port, proto, interface='', maxPacketSize=8192,
+ reactor=None):
+ """
+ Initialize with a numeric port to listen on.
+ """
+ self.port = port
+ self.protocol = proto
+ self.readBufferSize = maxPacketSize
+ self.interface = interface
+ self.setLogStr()
+ self._connectedAddr = None
+
+ abstract.FileHandle.__init__(self, reactor)
+
+ skt = socket.socket(self.addressFamily, self.socketType)
+ addrLen = _iocp.maxAddrLen(skt.fileno())
+ self.addressBuffer = _iocp.AllocateReadBuffer(addrLen)
+ # WSARecvFrom takes an int
+ self.addressLengthBuffer = _iocp.AllocateReadBuffer(
+ struct.calcsize('i'))
+
+
+ def __repr__(self):
+ if self._realPortNumber is not None:
+ return ("<%s on %s>" %
+ (self.protocol.__class__, self._realPortNumber))
+ else:
+ return "<%s not connected>" % (self.protocol.__class__,)
+
+
+ def getHandle(self):
+ """
+ Return a socket object.
+ """
+ return self.socket
+
+
+ def startListening(self):
+ """
+ Create and bind my socket, and begin listening on it.
+
+ This is called on unserialization, and must be called after creating a
+ server to begin listening on the specified port.
+ """
+ self._bindSocket()
+ self._connectToProtocol()
+
+
+ def createSocket(self):
+ return self.reactor.createSocket(self.addressFamily, self.socketType)
+
+
+ def _bindSocket(self):
+ try:
+ skt = self.createSocket()
+ skt.bind((self.interface, self.port))
+ except socket.error, le:
+ raise error.CannotListenError, (self.interface, self.port, le)
+
+ # Make sure that if we listened on port 0, we update that to
+ # reflect what the OS actually assigned us.
+ self._realPortNumber = skt.getsockname()[1]
+
+ log.msg("%s starting on %s" % (
+ self._getLogPrefix(self.protocol), self._realPortNumber))
+
+ self.connected = True
+ self.socket = skt
+ self.getFileHandle = self.socket.fileno
+
+
+ def _connectToProtocol(self):
+ self.protocol.makeConnection(self)
+ self.startReading()
+ self.reactor.addActiveHandle(self)
+
+
+ def cbRead(self, rc, bytes, evt):
+ if self.reading:
+ self.handleRead(rc, bytes, evt)
+ self.doRead()
+
+
+ def handleRead(self, rc, bytes, evt):
+ if rc in (errno.WSAECONNREFUSED, errno.WSAECONNRESET,
+ ERROR_CONNECTION_REFUSED, ERROR_PORT_UNREACHABLE):
+ if self._connectedAddr:
+ self.protocol.connectionRefused()
+ elif rc:
+ log.msg("error in recvfrom -- %s (%s)" %
+ (errno.errorcode.get(rc, 'unknown error'), rc))
+ else:
+ try:
+ self.protocol.datagramReceived(str(evt.buff[:bytes]),
+ _iocp.makesockaddr(evt.addr_buff))
+ except:
+ log.err()
+
+
+ def doRead(self):
+ evt = _iocp.Event(self.cbRead, self)
+
+ evt.buff = buff = self._readBuffers[0]
+ evt.addr_buff = addr_buff = self.addressBuffer
+ evt.addr_len_buff = addr_len_buff = self.addressLengthBuffer
+ rc, bytes = _iocp.recvfrom(self.getFileHandle(), buff,
+ addr_buff, addr_len_buff, evt)
+
+ if rc and rc != ERROR_IO_PENDING:
+ self.handleRead(rc, bytes, evt)
+
+
+ def write(self, datagram, addr=None):
+ """
+ Write a datagram.
+
+ @param addr: should be a tuple (ip, port), can be None in connected
+ mode.
+ """
+ if self._connectedAddr:
+ assert addr in (None, self._connectedAddr)
+ try:
+ return self.socket.send(datagram)
+ except socket.error, se:
+ no = se.args[0]
+ if no == errno.WSAEINTR:
+ return self.write(datagram)
+ elif no == errno.WSAEMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no in (errno.WSAECONNREFUSED, errno.WSAECONNRESET,
+ ERROR_CONNECTION_REFUSED, ERROR_PORT_UNREACHABLE):
+ self.protocol.connectionRefused()
+ else:
+ raise
+ else:
+ assert addr != None
+ if not addr[0].replace(".", "").isdigit():
+ warnings.warn("Please only pass IPs to write(), not hostnames",
+ DeprecationWarning, stacklevel=2)
+ try:
+ return self.socket.sendto(datagram, addr)
+ except socket.error, se:
+ no = se.args[0]
+ if no == errno.WSAEINTR:
+ return self.write(datagram, addr)
+ elif no == errno.WSAEMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no in (errno.WSAECONNREFUSED, errno.WSAECONNRESET,
+ ERROR_CONNECTION_REFUSED, ERROR_PORT_UNREACHABLE):
+ # in non-connected UDP ECONNREFUSED is platform dependent,
+ # I think and the info is not necessarily useful.
+ # Nevertheless maybe we should call connectionRefused? XXX
+ return
+ else:
+ raise
+
+
+ def writeSequence(self, seq, addr):
+ self.write("".join(seq), addr)
+
+
+ def connect(self, host, port):
+ """
+ 'Connect' to remote server.
+ """
+ if self._connectedAddr:
+ raise RuntimeError(
+ "already connected, reconnecting is not currently supported "
+ "(talk to itamar if you want this)")
+ if not isIPAddress(host):
+ raise ValueError, "please pass only IP addresses, not domain names"
+ self._connectedAddr = (host, port)
+ self.socket.connect((host, port))
+
+
+ def _loseConnection(self):
+ self.stopReading()
+ self.reactor.removeActiveHandle(self)
+ if self.connected: # actually means if we are *listening*
+ self.reactor.callLater(0, self.connectionLost)
+
+
+ def stopListening(self):
+ if self.connected:
+ result = self.d = defer.Deferred()
+ else:
+ result = None
+ self._loseConnection()
+ return result
+
+
+ def loseConnection(self):
+ warnings.warn("Please use stopListening() to disconnect port",
+ DeprecationWarning, stacklevel=2)
+ self.stopListening()
+
+
+ def connectionLost(self, reason=None):
+ """
+ Cleans up my socket.
+ """
+ log.msg('(UDP Port %s Closed)' % self._realPortNumber)
+ self._realPortNumber = None
+ abstract.FileHandle.connectionLost(self, reason)
+ self.protocol.doStop()
+ self.socket.close()
+ del self.socket
+ del self.getFileHandle
+ if hasattr(self, "d"):
+ self.d.callback(None)
+ del self.d
+
+
+ def setLogStr(self):
+ """
+ Initialize the C{logstr} attribute to be used by C{logPrefix}.
+ """
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = "%s (UDP)" % logPrefix
+
+
+ def logPrefix(self):
+ """
+ Returns the name of my class, to prefix log entries with.
+ """
+ return self.logstr
+
+
+ def getHost(self):
+ """
+ Returns an IPv4Address.
+
+ This indicates the address from which I am connecting.
+ """
+ return address.IPv4Address('UDP', *self.socket.getsockname())
+
+
+
+class MulticastMixin:
+ """
+ Implement multicast functionality.
+ """
+
+
+ def getOutgoingInterface(self):
+ i = self.socket.getsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF)
+ return socket.inet_ntoa(struct.pack("@i", i))
+
+
+ def setOutgoingInterface(self, addr):
+ """
+ Returns Deferred of success.
+ """
+ return self.reactor.resolve(addr).addCallback(self._setInterface)
+
+
+ def _setInterface(self, addr):
+ i = socket.inet_aton(addr)
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, i)
+ return 1
+
+
+ def getLoopbackMode(self):
+ return self.socket.getsockopt(socket.IPPROTO_IP,
+ socket.IP_MULTICAST_LOOP)
+
+
+ def setLoopbackMode(self, mode):
+ mode = struct.pack("b", operator.truth(mode))
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP,
+ mode)
+
+
+ def getTTL(self):
+ return self.socket.getsockopt(socket.IPPROTO_IP,
+ socket.IP_MULTICAST_TTL)
+
+
+ def setTTL(self, ttl):
+ ttl = struct.pack("B", ttl)
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
+
+
+ def joinGroup(self, addr, interface=""):
+ """
+ Join a multicast group. Returns Deferred of success.
+ """
+ return self.reactor.resolve(addr).addCallback(self._joinAddr1,
+ interface, 1)
+
+
+ def _joinAddr1(self, addr, interface, join):
+ return self.reactor.resolve(interface).addCallback(self._joinAddr2,
+ addr, join)
+
+
+ def _joinAddr2(self, interface, addr, join):
+ addr = socket.inet_aton(addr)
+ interface = socket.inet_aton(interface)
+ if join:
+ cmd = socket.IP_ADD_MEMBERSHIP
+ else:
+ cmd = socket.IP_DROP_MEMBERSHIP
+ try:
+ self.socket.setsockopt(socket.IPPROTO_IP, cmd, addr + interface)
+ except socket.error, e:
+ return failure.Failure(error.MulticastJoinError(addr, interface,
+ *e.args))
+
+
+ def leaveGroup(self, addr, interface=""):
+ """
+ Leave multicast group, return Deferred of success.
+ """
+ return self.reactor.resolve(addr).addCallback(self._joinAddr1,
+ interface, 0)
+
+
+
+class MulticastPort(MulticastMixin, Port):
+ """
+ UDP Port that supports multicasting.
+ """
+
+ implements(interfaces.IMulticastTransport)
+
+
+ def __init__(self, port, proto, interface='', maxPacketSize=8192,
+ reactor=None, listenMultiple=False):
+ Port.__init__(self, port, proto, interface, maxPacketSize, reactor)
+ self.listenMultiple = listenMultiple
+
+
+ def createSocket(self):
+ skt = Port.createSocket(self)
+ if self.listenMultiple:
+ skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if hasattr(socket, "SO_REUSEPORT"):
+ skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+ return skt
+
+
diff --git a/twisted/internet/kqreactor.py b/twisted/internet/kqreactor.py
new file mode 100644
index 0000000..bb1b6a3
--- /dev/null
+++ b/twisted/internet/kqreactor.py
@@ -0,0 +1,305 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A kqueue()/kevent() based implementation of the Twisted main loop.
+
+To use this reactor, start your application specifying the kqueue reactor::
+
+ twistd --reactor kqueue ...
+
+To install the event loop from code (and you should do this before any
+connections, listeners or connectors are added)::
+
+ from twisted.internet import kqreactor
+ kqreactor.install()
+
+This implementation depends on Python 2.6 or higher which has kqueue support
+built in the select module.
+
+Note, that you should use Python 2.6.5 or higher, since previous implementations
+of select.kqueue had U{http://bugs.python.org/issue5910} not yet fixed.
+"""
+
+import errno
+
+from zope.interface import implements
+
+from select import kqueue, kevent
+from select import KQ_FILTER_READ, KQ_FILTER_WRITE
+from select import KQ_EV_DELETE, KQ_EV_ADD, KQ_EV_EOF
+
+from twisted.internet.interfaces import IReactorFDSet, IReactorDaemonize
+
+from twisted.python import log, failure
+from twisted.internet import main, posixbase
+
+
+class KQueueReactor(posixbase.PosixReactorBase):
+ """
+ A reactor that uses kqueue(2)/kevent(2) and relies on Python 2.6 or higher
+ which has built in support for kqueue in the select module.
+
+ @ivar _kq: A L{kqueue} which will be used to check for I/O readiness.
+
+ @ivar _selectables: A dictionary mapping integer file descriptors to
+ instances of L{FileDescriptor} which have been registered with the
+ reactor. All L{FileDescriptors} which are currently receiving read or
+ write readiness notifications will be present as values in this
+ dictionary.
+
+ @ivar _reads: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_kq} for read readiness notifications which will be
+ dispatched to the corresponding L{FileDescriptor} instances in
+ C{_selectables}.
+
+ @ivar _writes: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_kq} for write readiness notifications which will be
+ dispatched to the corresponding L{FileDescriptor} instances in
+ C{_selectables}.
+ """
+ implements(IReactorFDSet, IReactorDaemonize)
+
+
+ def __init__(self):
+ """
+ Initialize kqueue object, file descriptor tracking dictionaries, and the
+ base class.
+
+ See:
+ - http://docs.python.org/library/select.html
+ - www.freebsd.org/cgi/man.cgi?query=kqueue
+ - people.freebsd.org/~jlemon/papers/kqueue.pdf
+ """
+ self._kq = kqueue()
+ self._reads = {}
+ self._writes = {}
+ self._selectables = {}
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def _updateRegistration(self, fd, filter, op):
+ """
+ Private method for changing kqueue registration on a given FD
+ filtering for events given filter/op. This will never block and
+ returns nothing.
+ """
+ self._kq.control([kevent(fd, filter, op)], 0, 0)
+
+
+ def beforeDaemonize(self):
+ """
+ Implement L{IReactorDaemonize.beforeDaemonize}.
+ """
+ # Twisted-internal method called during daemonization (when application
+ # is started via twistd). This is called right before the magic double
+ # forking done for daemonization. We cleanly close the kqueue() and later
+ # recreate it. This is needed since a) kqueue() are not inherited across
+ # forks and b) twistd will create the reactor already before daemonization
+ # (and will also add at least 1 reader to the reactor, an instance of
+ # twisted.internet.posixbase._UnixWaker).
+ #
+ # See: twisted.scripts._twistd_unix.daemonize()
+ self._kq.close()
+ self._kq = None
+
+
+ def afterDaemonize(self):
+ """
+ Implement L{IReactorDaemonize.afterDaemonize}.
+ """
+ # Twisted-internal method called during daemonization. This is called right
+ # after daemonization and recreates the kqueue() and any readers/writers
+ # that were added before. Note that you MUST NOT call any reactor methods
+ # in between beforeDaemonize() and afterDaemonize()!
+ self._kq = kqueue()
+ for fd in self._reads:
+ self._updateRegistration(fd, KQ_FILTER_READ, KQ_EV_ADD)
+ for fd in self._writes:
+ self._updateRegistration(fd, KQ_FILTER_WRITE, KQ_EV_ADD)
+
+
+ def addReader(self, reader):
+ """
+ Implement L{IReactorFDSet.addReader}.
+ """
+ fd = reader.fileno()
+ if fd not in self._reads:
+ try:
+ self._updateRegistration(fd, KQ_FILTER_READ, KQ_EV_ADD)
+ except OSError:
+ pass
+ finally:
+ self._selectables[fd] = reader
+ self._reads[fd] = 1
+
+
+ def addWriter(self, writer):
+ """
+ Implement L{IReactorFDSet.addWriter}.
+ """
+ fd = writer.fileno()
+ if fd not in self._writes:
+ try:
+ self._updateRegistration(fd, KQ_FILTER_WRITE, KQ_EV_ADD)
+ except OSError:
+ pass
+ finally:
+ self._selectables[fd] = writer
+ self._writes[fd] = 1
+
+
+ def removeReader(self, reader):
+ """
+ Implement L{IReactorFDSet.removeReader}.
+ """
+ wasLost = False
+ try:
+ fd = reader.fileno()
+ except:
+ fd = -1
+ if fd == -1:
+ for fd, fdes in self._selectables.items():
+ if reader is fdes:
+ wasLost = True
+ break
+ else:
+ return
+ if fd in self._reads:
+ del self._reads[fd]
+ if fd not in self._writes:
+ del self._selectables[fd]
+ if not wasLost:
+ try:
+ self._updateRegistration(fd, KQ_FILTER_READ, KQ_EV_DELETE)
+ except OSError:
+ pass
+
+
+ def removeWriter(self, writer):
+ """
+ Implement L{IReactorFDSet.removeWriter}.
+ """
+ wasLost = False
+ try:
+ fd = writer.fileno()
+ except:
+ fd = -1
+ if fd == -1:
+ for fd, fdes in self._selectables.items():
+ if writer is fdes:
+ wasLost = True
+ break
+ else:
+ return
+ if fd in self._writes:
+ del self._writes[fd]
+ if fd not in self._reads:
+ del self._selectables[fd]
+ if not wasLost:
+ try:
+ self._updateRegistration(fd, KQ_FILTER_WRITE, KQ_EV_DELETE)
+ except OSError:
+ pass
+
+
+ def removeAll(self):
+ """
+ Implement L{IReactorFDSet.removeAll}.
+ """
+ return self._removeAll(
+ [self._selectables[fd] for fd in self._reads],
+ [self._selectables[fd] for fd in self._writes])
+
+
+ def getReaders(self):
+ """
+ Implement L{IReactorFDSet.getReaders}.
+ """
+ return [self._selectables[fd] for fd in self._reads]
+
+
+ def getWriters(self):
+ """
+ Implement L{IReactorFDSet.getWriters}.
+ """
+ return [self._selectables[fd] for fd in self._writes]
+
+
+ def doKEvent(self, timeout):
+ """
+ Poll the kqueue for new events.
+ """
+ if timeout is None:
+ timeout = 1
+
+ try:
+ l = self._kq.control([], len(self._selectables), timeout)
+ except OSError, e:
+ if e[0] == errno.EINTR:
+ return
+ else:
+ raise
+
+ _drdw = self._doWriteOrRead
+ for event in l:
+ fd = event.ident
+ try:
+ selectable = self._selectables[fd]
+ except KeyError:
+ # Handles the infrequent case where one selectable's
+ # handler disconnects another.
+ continue
+ else:
+ log.callWithLogger(selectable, _drdw, selectable, fd, event)
+
+
+ def _doWriteOrRead(self, selectable, fd, event):
+ """
+ Private method called when a FD is ready for reading, writing or was
+ lost. Do the work and raise errors where necessary.
+ """
+ why = None
+ inRead = False
+ (filter, flags, data, fflags) = (
+ event.filter, event.flags, event.data, event.fflags)
+
+ if flags & KQ_EV_EOF and data and fflags:
+ why = main.CONNECTION_LOST
+ else:
+ try:
+ if selectable.fileno() == -1:
+ inRead = False
+ why = posixbase._NO_FILEDESC
+ else:
+ if filter == KQ_FILTER_READ:
+ inRead = True
+ why = selectable.doRead()
+ if filter == KQ_FILTER_WRITE:
+ inRead = False
+ why = selectable.doWrite()
+ except:
+ # Any exception from application code gets logged and will
+ # cause us to disconnect the selectable.
+ why = failure.Failure()
+ log.err(why, "An exception was raised from application code" \
+ " while processing a reactor selectable")
+
+ if why:
+ self._disconnectSelectable(selectable, why, inRead)
+
+ doIteration = doKEvent
+
+
+def install():
+ """
+ Install the kqueue() reactor.
+ """
+ p = KQueueReactor()
+ from twisted.internet.main import installReactor
+ installReactor(p)
+
+
+__all__ = ["KQueueReactor", "install"]
diff --git a/twisted/internet/main.py b/twisted/internet/main.py
new file mode 100644
index 0000000..0c6c2ff
--- /dev/null
+++ b/twisted/internet/main.py
@@ -0,0 +1,35 @@
+# -*- test-case-name: twisted.internet.test.test_main -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Backwards compatibility, and utility functions.
+
+In general, this module should not be used, other than by reactor authors
+who need to use the 'installReactor' method.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+import error
+
+CONNECTION_DONE = error.ConnectionDone('Connection done')
+CONNECTION_LOST = error.ConnectionLost('Connection lost')
+
+def installReactor(reactor):
+ """
+ Install reactor C{reactor}.
+
+ @param reactor: An object that provides one or more IReactor* interfaces.
+ """
+ # this stuff should be common to all reactors.
+ import twisted.internet
+ import sys
+ if 'twisted.internet.reactor' in sys.modules:
+ raise error.ReactorAlreadyInstalledError("reactor already installed")
+ twisted.internet.reactor = reactor
+ sys.modules['twisted.internet.reactor'] = reactor
+
+
+__all__ = ["CONNECTION_LOST", "CONNECTION_DONE", "installReactor"]
diff --git a/twisted/internet/pollreactor.py b/twisted/internet/pollreactor.py
new file mode 100644
index 0000000..5769167
--- /dev/null
+++ b/twisted/internet/pollreactor.py
@@ -0,0 +1,187 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A poll() based implementation of the twisted main loop.
+
+To install the event loop (and you should do this before any connections,
+listeners or connectors are added)::
+
+ from twisted.internet import pollreactor
+ pollreactor.install()
+"""
+
+# System imports
+import errno
+from select import error as SelectError, poll
+from select import POLLIN, POLLOUT, POLLHUP, POLLERR, POLLNVAL
+
+from zope.interface import implements
+
+# Twisted imports
+from twisted.python import log
+from twisted.internet import posixbase
+from twisted.internet.interfaces import IReactorFDSet
+
+
+
+class PollReactor(posixbase.PosixReactorBase, posixbase._PollLikeMixin):
+ """
+ A reactor that uses poll(2).
+
+ @ivar _poller: A L{poll} which will be used to check for I/O
+ readiness.
+
+ @ivar _selectables: A dictionary mapping integer file descriptors to
+ instances of L{FileDescriptor} which have been registered with the
+ reactor. All L{FileDescriptors} which are currently receiving read or
+ write readiness notifications will be present as values in this
+ dictionary.
+
+ @ivar _reads: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_poller} for read readiness notifications which will
+ be dispatched to the corresponding L{FileDescriptor} instances in
+ C{_selectables}.
+
+ @ivar _writes: A dictionary mapping integer file descriptors to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ registered with C{_poller} for write readiness notifications which will
+ be dispatched to the corresponding L{FileDescriptor} instances in
+ C{_selectables}.
+ """
+ implements(IReactorFDSet)
+
+ _POLL_DISCONNECTED = (POLLHUP | POLLERR | POLLNVAL)
+ _POLL_IN = POLLIN
+ _POLL_OUT = POLLOUT
+
+ def __init__(self):
+ """
+ Initialize polling object, file descriptor tracking dictionaries, and
+ the base class.
+ """
+ self._poller = poll()
+ self._selectables = {}
+ self._reads = {}
+ self._writes = {}
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def _updateRegistration(self, fd):
+ """Register/unregister an fd with the poller."""
+ try:
+ self._poller.unregister(fd)
+ except KeyError:
+ pass
+
+ mask = 0
+ if fd in self._reads:
+ mask = mask | POLLIN
+ if fd in self._writes:
+ mask = mask | POLLOUT
+ if mask != 0:
+ self._poller.register(fd, mask)
+ else:
+ if fd in self._selectables:
+ del self._selectables[fd]
+
+ def _dictRemove(self, selectable, mdict):
+ try:
+ # the easy way
+ fd = selectable.fileno()
+ # make sure the fd is actually real. In some situations we can get
+ # -1 here.
+ mdict[fd]
+ except:
+ # the hard way: necessary because fileno() may disappear at any
+ # moment, thanks to python's underlying sockets impl
+ for fd, fdes in self._selectables.items():
+ if selectable is fdes:
+ break
+ else:
+ # Hmm, maybe not the right course of action? This method can't
+ # fail, because it happens inside error detection...
+ return
+ if fd in mdict:
+ del mdict[fd]
+ self._updateRegistration(fd)
+
+ def addReader(self, reader):
+ """Add a FileDescriptor for notification of data available to read.
+ """
+ fd = reader.fileno()
+ if fd not in self._reads:
+ self._selectables[fd] = reader
+ self._reads[fd] = 1
+ self._updateRegistration(fd)
+
+ def addWriter(self, writer):
+ """Add a FileDescriptor for notification of data available to write.
+ """
+ fd = writer.fileno()
+ if fd not in self._writes:
+ self._selectables[fd] = writer
+ self._writes[fd] = 1
+ self._updateRegistration(fd)
+
+ def removeReader(self, reader):
+ """Remove a Selectable for notification of data available to read.
+ """
+ return self._dictRemove(reader, self._reads)
+
+ def removeWriter(self, writer):
+ """Remove a Selectable for notification of data available to write.
+ """
+ return self._dictRemove(writer, self._writes)
+
+ def removeAll(self):
+ """
+ Remove all selectables, and return a list of them.
+ """
+ return self._removeAll(
+ [self._selectables[fd] for fd in self._reads],
+ [self._selectables[fd] for fd in self._writes])
+
+
+ def doPoll(self, timeout):
+ """Poll the poller for new events."""
+ if timeout is not None:
+ timeout = int(timeout * 1000) # convert seconds to milliseconds
+
+ try:
+ l = self._poller.poll(timeout)
+ except SelectError, e:
+ if e.args[0] == errno.EINTR:
+ return
+ else:
+ raise
+ _drdw = self._doReadOrWrite
+ for fd, event in l:
+ try:
+ selectable = self._selectables[fd]
+ except KeyError:
+ # Handles the infrequent case where one selectable's
+ # handler disconnects another.
+ continue
+ log.callWithLogger(selectable, _drdw, selectable, fd, event)
+
+ doIteration = doPoll
+
+ def getReaders(self):
+ return [self._selectables[fd] for fd in self._reads]
+
+
+ def getWriters(self):
+ return [self._selectables[fd] for fd in self._writes]
+
+
+
+def install():
+ """Install the poll() reactor."""
+ p = PollReactor()
+ from twisted.internet.main import installReactor
+ installReactor(p)
+
+
+__all__ = ["PollReactor", "install"]
diff --git a/twisted/internet/posixbase.py b/twisted/internet/posixbase.py
new file mode 100644
index 0000000..f5529ed
--- /dev/null
+++ b/twisted/internet/posixbase.py
@@ -0,0 +1,640 @@
+# -*- test-case-name: twisted.test.test_internet,twisted.internet.test.test_posixbase -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Posix reactor base class
+"""
+
+import warnings
+import socket
+import errno
+import os
+import sys
+
+from zope.interface import implements, classImplements
+
+from twisted.python.compat import set
+from twisted.internet.interfaces import IReactorUNIX, IReactorUNIXDatagram
+from twisted.internet.interfaces import (
+ IReactorTCP, IReactorUDP, IReactorSSL, _IReactorArbitrary, IReactorSocket)
+from twisted.internet.interfaces import IReactorProcess, IReactorMulticast
+from twisted.internet.interfaces import IHalfCloseableDescriptor
+from twisted.internet import error
+from twisted.internet import tcp, udp
+
+from twisted.python import log, failure, util
+from twisted.persisted import styles
+from twisted.python.runtime import platformType, platform
+
+from twisted.internet.base import ReactorBase, _SignalReactorMixin
+from twisted.internet.main import CONNECTION_DONE, CONNECTION_LOST
+
+# Exceptions that doSelect might return frequently
+_NO_FILENO = error.ConnectionFdescWentAway('Handler has no fileno method')
+_NO_FILEDESC = error.ConnectionFdescWentAway('File descriptor lost')
+
+
+try:
+ from twisted.protocols import tls
+except ImportError:
+ tls = None
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ ssl = None
+
+try:
+ from twisted.internet import unix
+ unixEnabled = True
+except ImportError:
+ unixEnabled = False
+
+processEnabled = False
+if platformType == 'posix':
+ from twisted.internet import fdesc, process, _signals
+ processEnabled = True
+
+if platform.isWindows():
+ try:
+ import win32process
+ processEnabled = True
+ except ImportError:
+ win32process = None
+
+
+class _SocketWaker(log.Logger, styles.Ephemeral):
+ """
+ The I{self-pipe trick<http://cr.yp.to/docs/selfpipe.html>}, implemented
+ using a pair of sockets rather than pipes (due to the lack of support in
+ select() on Windows for pipes), used to wake up the main loop from
+ another thread.
+ """
+ disconnected = 0
+
+ def __init__(self, reactor):
+ """Initialize.
+ """
+ self.reactor = reactor
+ # Following select_trigger (from asyncore)'s example;
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ server.bind(('127.0.0.1', 0))
+ server.listen(1)
+ client.connect(server.getsockname())
+ reader, clientaddr = server.accept()
+ client.setblocking(0)
+ reader.setblocking(0)
+ self.r = reader
+ self.w = client
+ self.fileno = self.r.fileno
+
+ def wakeUp(self):
+ """Send a byte to my connection.
+ """
+ try:
+ util.untilConcludes(self.w.send, 'x')
+ except socket.error, (err, msg):
+ if err != errno.WSAEWOULDBLOCK:
+ raise
+
+ def doRead(self):
+ """Read some data from my connection.
+ """
+ try:
+ self.r.recv(8192)
+ except socket.error:
+ pass
+
+ def connectionLost(self, reason):
+ self.r.close()
+ self.w.close()
+
+
+
+class _FDWaker(object, log.Logger, styles.Ephemeral):
+ """
+ The I{self-pipe trick<http://cr.yp.to/docs/selfpipe.html>}, used to wake
+ up the main loop from another thread or a signal handler.
+
+ L{_FDWaker} is a base class for waker implementations based on
+ writing to a pipe being monitored by the reactor.
+
+ @ivar o: The file descriptor for the end of the pipe which can be
+ written to to wake up a reactor monitoring this waker.
+
+ @ivar i: The file descriptor which should be monitored in order to
+ be awoken by this waker.
+ """
+ disconnected = 0
+
+ i = None
+ o = None
+
+ def __init__(self, reactor):
+ """Initialize.
+ """
+ self.reactor = reactor
+ self.i, self.o = os.pipe()
+ fdesc.setNonBlocking(self.i)
+ fdesc._setCloseOnExec(self.i)
+ fdesc.setNonBlocking(self.o)
+ fdesc._setCloseOnExec(self.o)
+ self.fileno = lambda: self.i
+
+
+ def doRead(self):
+ """
+ Read some bytes from the pipe and discard them.
+ """
+ fdesc.readFromFD(self.fileno(), lambda data: None)
+
+
+ def connectionLost(self, reason):
+ """Close both ends of my pipe.
+ """
+ if not hasattr(self, "o"):
+ return
+ for fd in self.i, self.o:
+ try:
+ os.close(fd)
+ except IOError:
+ pass
+ del self.i, self.o
+
+
+
+class _UnixWaker(_FDWaker):
+ """
+ This class provides a simple interface to wake up the event loop.
+
+ This is used by threads or signals to wake up the event loop.
+ """
+
+ def wakeUp(self):
+ """Write one byte to the pipe, and flush it.
+ """
+ # We don't use fdesc.writeToFD since we need to distinguish
+ # between EINTR (try again) and EAGAIN (do nothing).
+ if self.o is not None:
+ try:
+ util.untilConcludes(os.write, self.o, 'x')
+ except OSError, e:
+ # XXX There is no unit test for raising the exception
+ # for other errnos. See #4285.
+ if e.errno != errno.EAGAIN:
+ raise
+
+
+
+if platformType == 'posix':
+ _Waker = _UnixWaker
+else:
+ # Primarily Windows and Jython.
+ _Waker = _SocketWaker
+
+
+class _SIGCHLDWaker(_FDWaker):
+ """
+ L{_SIGCHLDWaker} can wake up a reactor whenever C{SIGCHLD} is
+ received.
+
+ @see: L{twisted.internet._signals}
+ """
+ def __init__(self, reactor):
+ _FDWaker.__init__(self, reactor)
+
+
+ def install(self):
+ """
+ Install the handler necessary to make this waker active.
+ """
+ _signals.installHandler(self.o)
+
+
+ def uninstall(self):
+ """
+ Remove the handler which makes this waker active.
+ """
+ _signals.installHandler(-1)
+
+
+ def doRead(self):
+ """
+ Having woken up the reactor in response to receipt of
+ C{SIGCHLD}, reap the process which exited.
+
+ This is called whenever the reactor notices the waker pipe is
+ writeable, which happens soon after any call to the C{wakeUp}
+ method.
+ """
+ _FDWaker.doRead(self)
+ process.reapAllProcesses()
+
+
+
+
+class _DisconnectSelectableMixin(object):
+ """
+ Mixin providing the C{_disconnectSelectable} method.
+ """
+
+ def _disconnectSelectable(self, selectable, why, isRead, faildict={
+ error.ConnectionDone: failure.Failure(error.ConnectionDone()),
+ error.ConnectionLost: failure.Failure(error.ConnectionLost())
+ }):
+ """
+ Utility function for disconnecting a selectable.
+
+ Supports half-close notification, isRead should be boolean indicating
+ whether error resulted from doRead().
+ """
+ self.removeReader(selectable)
+ f = faildict.get(why.__class__)
+ if f:
+ if (isRead and why.__class__ == error.ConnectionDone
+ and IHalfCloseableDescriptor.providedBy(selectable)):
+ selectable.readConnectionLost(f)
+ else:
+ self.removeWriter(selectable)
+ selectable.connectionLost(f)
+ else:
+ self.removeWriter(selectable)
+ selectable.connectionLost(failure.Failure(why))
+
+
+
+class PosixReactorBase(_SignalReactorMixin, _DisconnectSelectableMixin,
+ ReactorBase):
+ """
+ A basis for reactors that use file descriptors.
+
+ @ivar _childWaker: C{None} or a reference to the L{_SIGCHLDWaker}
+ which is used to properly notice child process termination.
+ """
+ implements(_IReactorArbitrary, IReactorTCP, IReactorUDP, IReactorMulticast)
+
+ # Callable that creates a waker, overrideable so that subclasses can
+ # substitute their own implementation:
+ _wakerFactory = _Waker
+
+ def installWaker(self):
+ """
+ Install a `waker' to allow threads and signals to wake up the IO thread.
+
+ We use the self-pipe trick (http://cr.yp.to/docs/selfpipe.html) to wake
+ the reactor. On Windows we use a pair of sockets.
+ """
+ if not self.waker:
+ self.waker = self._wakerFactory(self)
+ self._internalReaders.add(self.waker)
+ self.addReader(self.waker)
+
+
+ _childWaker = None
+ def _handleSignals(self):
+ """
+ Extend the basic signal handling logic to also support
+ handling SIGCHLD to know when to try to reap child processes.
+ """
+ _SignalReactorMixin._handleSignals(self)
+ if platformType == 'posix':
+ if not self._childWaker:
+ self._childWaker = _SIGCHLDWaker(self)
+ self._internalReaders.add(self._childWaker)
+ self.addReader(self._childWaker)
+ self._childWaker.install()
+ # Also reap all processes right now, in case we missed any
+ # signals before we installed the SIGCHLD waker/handler.
+ # This should only happen if someone used spawnProcess
+ # before calling reactor.run (and the process also exited
+ # already).
+ process.reapAllProcesses()
+
+ def _uninstallHandler(self):
+ """
+ If a child waker was created and installed, uninstall it now.
+
+ Since this disables reactor functionality and is only called
+ when the reactor is stopping, it doesn't provide any directly
+ useful functionality, but the cleanup of reactor-related
+ process-global state that it does helps in unit tests
+ involving multiple reactors and is generally just a nice
+ thing.
+ """
+ # XXX This would probably be an alright place to put all of
+ # the cleanup code for all internal readers (here and in the
+ # base class, anyway). See #3063 for that cleanup task.
+ if self._childWaker:
+ self._childWaker.uninstall()
+
+ # IReactorProcess
+
+ def spawnProcess(self, processProtocol, executable, args=(),
+ env={}, path=None,
+ uid=None, gid=None, usePTY=0, childFDs=None):
+ args, env = self._checkProcessArgs(args, env)
+ if platformType == 'posix':
+ if usePTY:
+ if childFDs is not None:
+ raise ValueError("Using childFDs is not supported with usePTY=True.")
+ return process.PTYProcess(self, executable, args, env, path,
+ processProtocol, uid, gid, usePTY)
+ else:
+ return process.Process(self, executable, args, env, path,
+ processProtocol, uid, gid, childFDs)
+ elif platformType == "win32":
+ if uid is not None:
+ raise ValueError("Setting UID is unsupported on this platform.")
+ if gid is not None:
+ raise ValueError("Setting GID is unsupported on this platform.")
+ if usePTY:
+ raise ValueError("The usePTY parameter is not supported on Windows.")
+ if childFDs:
+ raise ValueError("Customizing childFDs is not supported on Windows.")
+
+ if win32process:
+ from twisted.internet._dumbwin32proc import Process
+ return Process(self, processProtocol, executable, args, env, path)
+ else:
+ raise NotImplementedError, "spawnProcess not available since pywin32 is not installed."
+ else:
+ raise NotImplementedError, "spawnProcess only available on Windows or POSIX."
+
+ # IReactorUDP
+
+ def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
+ """Connects a given L{DatagramProtocol} to the given numeric UDP port.
+
+ @returns: object conforming to L{IListeningPort}.
+ """
+ p = udp.Port(port, protocol, interface, maxPacketSize, self)
+ p.startListening()
+ return p
+
+ # IReactorMulticast
+
+ def listenMulticast(self, port, protocol, interface='', maxPacketSize=8192, listenMultiple=False):
+ """Connects a given DatagramProtocol to the given numeric UDP port.
+
+ EXPERIMENTAL.
+
+ @returns: object conforming to IListeningPort.
+ """
+ p = udp.MulticastPort(port, protocol, interface, maxPacketSize, self, listenMultiple)
+ p.startListening()
+ return p
+
+
+ # IReactorUNIX
+
+ def connectUNIX(self, address, factory, timeout=30, checkPID=0):
+ """@see: twisted.internet.interfaces.IReactorUNIX.connectUNIX
+ """
+ assert unixEnabled, "UNIX support is not present"
+ c = unix.Connector(address, factory, timeout, self, checkPID)
+ c.connect()
+ return c
+
+ def listenUNIX(self, address, factory, backlog=50, mode=0666, wantPID=0):
+ """
+ @see: twisted.internet.interfaces.IReactorUNIX.listenUNIX
+ """
+ assert unixEnabled, "UNIX support is not present"
+ p = unix.Port(address, factory, backlog, mode, self, wantPID)
+ p.startListening()
+ return p
+
+
+ # IReactorUNIXDatagram
+
+ def listenUNIXDatagram(self, address, protocol, maxPacketSize=8192,
+ mode=0666):
+ """
+ Connects a given L{DatagramProtocol} to the given path.
+
+ EXPERIMENTAL.
+
+ @returns: object conforming to L{IListeningPort}.
+ """
+ assert unixEnabled, "UNIX support is not present"
+ p = unix.DatagramPort(address, protocol, maxPacketSize, mode, self)
+ p.startListening()
+ return p
+
+ def connectUNIXDatagram(self, address, protocol, maxPacketSize=8192,
+ mode=0666, bindAddress=None):
+ """
+ Connects a L{ConnectedDatagramProtocol} instance to a path.
+
+ EXPERIMENTAL.
+ """
+ assert unixEnabled, "UNIX support is not present"
+ p = unix.ConnectedDatagramPort(address, protocol, maxPacketSize, mode, bindAddress, self)
+ p.startListening()
+ return p
+
+
+ # IReactorSocket (but not on Windows)
+ def adoptStreamPort(self, fileDescriptor, addressFamily, factory):
+ """
+ Create a new L{IListeningPort} from an already-initialized socket.
+
+ This just dispatches to a suitable port implementation (eg from
+ L{IReactorTCP}, etc) based on the specified C{addressFamily}.
+
+ @see: L{twisted.internet.interfaces.IReactorSocket.adoptStreamPort}
+ """
+ if addressFamily not in (socket.AF_INET, socket.AF_INET6):
+ raise error.UnsupportedAddressFamily(addressFamily)
+
+ p = tcp.Port._fromListeningDescriptor(
+ self, fileDescriptor, addressFamily, factory)
+ p.startListening()
+ return p
+
+
+ # IReactorTCP
+
+ def listenTCP(self, port, factory, backlog=50, interface=''):
+ """@see: twisted.internet.interfaces.IReactorTCP.listenTCP
+ """
+ p = tcp.Port(port, factory, backlog, interface, self)
+ p.startListening()
+ return p
+
+ def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
+ """@see: twisted.internet.interfaces.IReactorTCP.connectTCP
+ """
+ c = tcp.Connector(host, port, factory, timeout, bindAddress, self)
+ c.connect()
+ return c
+
+ # IReactorSSL (sometimes, not implemented)
+
+ def connectSSL(self, host, port, factory, contextFactory, timeout=30, bindAddress=None):
+ """@see: twisted.internet.interfaces.IReactorSSL.connectSSL
+ """
+ if tls is not None:
+ tlsFactory = tls.TLSMemoryBIOFactory(contextFactory, True, factory)
+ return self.connectTCP(host, port, tlsFactory, timeout, bindAddress)
+ elif ssl is not None:
+ c = ssl.Connector(
+ host, port, factory, contextFactory, timeout, bindAddress, self)
+ c.connect()
+ return c
+ else:
+ assert False, "SSL support is not present"
+
+
+
+ def listenSSL(self, port, factory, contextFactory, backlog=50, interface=''):
+ """@see: twisted.internet.interfaces.IReactorSSL.listenSSL
+ """
+ if tls is not None:
+ tlsFactory = tls.TLSMemoryBIOFactory(contextFactory, False, factory)
+ port = self.listenTCP(port, tlsFactory, backlog, interface)
+ port._type = 'TLS'
+ return port
+ elif ssl is not None:
+ p = ssl.Port(
+ port, factory, contextFactory, backlog, interface, self)
+ p.startListening()
+ return p
+ else:
+ assert False, "SSL support is not present"
+
+
+ # IReactorArbitrary
+ def listenWith(self, portType, *args, **kw):
+ warnings.warn(
+ "listenWith is deprecated since Twisted 10.1. "
+ "See IReactorFDSet.",
+ category=DeprecationWarning,
+ stacklevel=2)
+ kw['reactor'] = self
+ p = portType(*args, **kw)
+ p.startListening()
+ return p
+
+
+ def connectWith(self, connectorType, *args, **kw):
+ warnings.warn(
+ "connectWith is deprecated since Twisted 10.1. "
+ "See IReactorFDSet.",
+ category=DeprecationWarning,
+ stacklevel=2)
+ kw['reactor'] = self
+ c = connectorType(*args, **kw)
+ c.connect()
+ return c
+
+
+ def _removeAll(self, readers, writers):
+ """
+ Remove all readers and writers, and list of removed L{IReadDescriptor}s
+ and L{IWriteDescriptor}s.
+
+ Meant for calling from subclasses, to implement removeAll, like::
+
+ def removeAll(self):
+ return self._removeAll(self._reads, self._writes)
+
+ where C{self._reads} and C{self._writes} are iterables.
+ """
+ removedReaders = set(readers) - self._internalReaders
+ for reader in removedReaders:
+ self.removeReader(reader)
+
+ removedWriters = set(writers)
+ for writer in removedWriters:
+ self.removeWriter(writer)
+
+ return list(removedReaders | removedWriters)
+
+
+class _PollLikeMixin(object):
+ """
+ Mixin for poll-like reactors.
+
+ Subclasses must define the following attributes::
+
+ - _POLL_DISCONNECTED - Bitmask for events indicating a connection was
+ lost.
+ - _POLL_IN - Bitmask for events indicating there is input to read.
+ - _POLL_OUT - Bitmask for events indicating output can be written.
+
+ Must be mixed in to a subclass of PosixReactorBase (for
+ _disconnectSelectable).
+ """
+
+ def _doReadOrWrite(self, selectable, fd, event):
+ """
+ fd is available for read or write, do the work and raise errors if
+ necessary.
+ """
+ why = None
+ inRead = False
+ if event & self._POLL_DISCONNECTED and not (event & self._POLL_IN):
+ # Handle disconnection. But only if we finished processing all
+ # the pending input.
+ if fd in self._reads:
+ # If we were reading from the descriptor then this is a
+ # clean shutdown. We know there are no read events pending
+ # because we just checked above. It also might be a
+ # half-close (which is why we have to keep track of inRead).
+ inRead = True
+ why = CONNECTION_DONE
+ else:
+ # If we weren't reading, this is an error shutdown of some
+ # sort.
+ why = CONNECTION_LOST
+ else:
+ # Any non-disconnect event turns into a doRead or a doWrite.
+ try:
+ # First check to see if the descriptor is still valid. This
+ # gives fileno() a chance to raise an exception, too.
+ # Ideally, disconnection would always be indicated by the
+ # return value of doRead or doWrite (or an exception from
+ # one of those methods), but calling fileno here helps make
+ # buggy applications more transparent.
+ if selectable.fileno() == -1:
+ # -1 is sort of a historical Python artifact. Python
+ # files and sockets used to change their file descriptor
+ # to -1 when they closed. For the time being, we'll
+ # continue to support this anyway in case applications
+ # replicated it, plus abstract.FileDescriptor.fileno
+ # returns -1. Eventually it'd be good to deprecate this
+ # case.
+ why = _NO_FILEDESC
+ else:
+ if event & self._POLL_IN:
+ # Handle a read event.
+ why = selectable.doRead()
+ inRead = True
+ if not why and event & self._POLL_OUT:
+ # Handle a write event, as long as doRead didn't
+ # disconnect us.
+ why = selectable.doWrite()
+ inRead = False
+ except:
+ # Any exception from application code gets logged and will
+ # cause us to disconnect the selectable.
+ why = sys.exc_info()[1]
+ log.err()
+ if why:
+ self._disconnectSelectable(selectable, why, inRead)
+
+
+
+if tls is not None or ssl is not None:
+ classImplements(PosixReactorBase, IReactorSSL)
+if unixEnabled:
+ classImplements(PosixReactorBase, IReactorUNIX, IReactorUNIXDatagram)
+if processEnabled:
+ classImplements(PosixReactorBase, IReactorProcess)
+if getattr(socket, 'fromfd', None) is not None:
+ classImplements(PosixReactorBase, IReactorSocket)
+
+__all__ = ["PosixReactorBase"]
diff --git a/twisted/internet/process.py b/twisted/internet/process.py
new file mode 100644
index 0000000..54e5210
--- /dev/null
+++ b/twisted/internet/process.py
@@ -0,0 +1,1068 @@
+# -*- test-case-name: twisted.test.test_process -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+UNIX Process management.
+
+Do NOT use this module directly - use reactor.spawnProcess() instead.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+# System Imports
+import gc, os, sys, stat, traceback, select, signal, errno
+
+try:
+ import pty
+except ImportError:
+ pty = None
+
+try:
+ import fcntl, termios
+except ImportError:
+ fcntl = None
+
+from zope.interface import implements
+
+from twisted.python import log, failure
+from twisted.python.util import switchUID
+from twisted.internet import fdesc, abstract, error
+from twisted.internet.main import CONNECTION_LOST, CONNECTION_DONE
+from twisted.internet._baseprocess import BaseProcess
+from twisted.internet.interfaces import IProcessTransport
+
+# Some people were importing this, which is incorrect, just keeping it
+# here for backwards compatibility:
+ProcessExitedAlready = error.ProcessExitedAlready
+
+reapProcessHandlers = {}
+
+def reapAllProcesses():
+ """
+ Reap all registered processes.
+ """
+ for process in reapProcessHandlers.values():
+ process.reapProcess()
+
+
+def registerReapProcessHandler(pid, process):
+ """
+ Register a process handler for the given pid, in case L{reapAllProcesses}
+ is called.
+
+ @param pid: the pid of the process.
+ @param process: a process handler.
+ """
+ if pid in reapProcessHandlers:
+ raise RuntimeError("Try to register an already registered process.")
+ try:
+ auxPID, status = os.waitpid(pid, os.WNOHANG)
+ except:
+ log.msg('Failed to reap %d:' % pid)
+ log.err()
+ auxPID = None
+ if auxPID:
+ process.processEnded(status)
+ else:
+ # if auxPID is 0, there are children but none have exited
+ reapProcessHandlers[pid] = process
+
+
+def unregisterReapProcessHandler(pid, process):
+ """
+ Unregister a process handler previously registered with
+ L{registerReapProcessHandler}.
+ """
+ if not (pid in reapProcessHandlers
+ and reapProcessHandlers[pid] == process):
+ raise RuntimeError("Try to unregister a process not registered.")
+ del reapProcessHandlers[pid]
+
+
+def detectLinuxBrokenPipeBehavior():
+ """
+ On some Linux version, write-only pipe are detected as readable. This
+ function is here to check if this bug is present or not.
+
+ See L{ProcessWriter.doRead} for a more detailed explanation.
+ """
+ global brokenLinuxPipeBehavior
+ r, w = os.pipe()
+ os.write(w, 'a')
+ reads, writes, exes = select.select([w], [], [], 0)
+ if reads:
+ # Linux < 2.6.11 says a write-only pipe is readable.
+ brokenLinuxPipeBehavior = True
+ else:
+ brokenLinuxPipeBehavior = False
+ os.close(r)
+ os.close(w)
+
+# Call at import time
+detectLinuxBrokenPipeBehavior()
+
+
+class ProcessWriter(abstract.FileDescriptor):
+ """
+ (Internal) Helper class to write into a Process's input pipe.
+
+ I am a helper which describes a selectable asynchronous writer to a
+ process's input pipe, including stdin.
+
+ @ivar enableReadHack: A flag which determines how readability on this
+ write descriptor will be handled. If C{True}, then readability may
+ indicate the reader for this write descriptor has been closed (ie,
+ the connection has been lost). If C{False}, then readability events
+ are ignored.
+ """
+ connected = 1
+ ic = 0
+ enableReadHack = False
+
+ def __init__(self, reactor, proc, name, fileno, forceReadHack=False):
+ """
+ Initialize, specifying a Process instance to connect to.
+ """
+ abstract.FileDescriptor.__init__(self, reactor)
+ fdesc.setNonBlocking(fileno)
+ self.proc = proc
+ self.name = name
+ self.fd = fileno
+
+ if not stat.S_ISFIFO(os.fstat(self.fileno()).st_mode):
+ # If the fd is not a pipe, then the read hack is never
+ # applicable. This case arises when ProcessWriter is used by
+ # StandardIO and stdout is redirected to a normal file.
+ self.enableReadHack = False
+ elif forceReadHack:
+ self.enableReadHack = True
+ else:
+ # Detect if this fd is actually a write-only fd. If it's
+ # valid to read, don't try to detect closing via read.
+ # This really only means that we cannot detect a TTY's write
+ # pipe being closed.
+ try:
+ os.read(self.fileno(), 0)
+ except OSError:
+ # It's a write-only pipe end, enable hack
+ self.enableReadHack = True
+
+ if self.enableReadHack:
+ self.startReading()
+
+ def fileno(self):
+ """
+ Return the fileno() of my process's stdin.
+ """
+ return self.fd
+
+ def writeSomeData(self, data):
+ """
+ Write some data to the open process.
+ """
+ rv = fdesc.writeToFD(self.fd, data)
+ if rv == len(data) and self.enableReadHack:
+ # If the send buffer is now empty and it is necessary to monitor
+ # this descriptor for readability to detect close, try detecting
+ # readability now.
+ self.startReading()
+ return rv
+
+ def write(self, data):
+ self.stopReading()
+ abstract.FileDescriptor.write(self, data)
+
+ def doRead(self):
+ """
+ The only way a write pipe can become "readable" is at EOF, because the
+ child has closed it, and we're using a reactor which doesn't
+ distinguish between readable and closed (such as the select reactor).
+
+ Except that's not true on linux < 2.6.11. It has the following
+ characteristics: write pipe is completely empty => POLLOUT (writable in
+ select), write pipe is not completely empty => POLLIN (readable in
+ select), write pipe's reader closed => POLLIN|POLLERR (readable and
+ writable in select)
+
+ That's what this funky code is for. If linux was not broken, this
+ function could be simply "return CONNECTION_LOST".
+
+ BUG: We call select no matter what the reactor.
+ If the reactor is pollreactor, and the fd is > 1024, this will fail.
+ (only occurs on broken versions of linux, though).
+ """
+ if self.enableReadHack:
+ if brokenLinuxPipeBehavior:
+ fd = self.fd
+ r, w, x = select.select([fd], [fd], [], 0)
+ if r and w:
+ return CONNECTION_LOST
+ else:
+ return CONNECTION_LOST
+ else:
+ self.stopReading()
+
+ def connectionLost(self, reason):
+ """
+ See abstract.FileDescriptor.connectionLost.
+ """
+ # At least on OS X 10.4, exiting while stdout is non-blocking can
+ # result in data loss. For some reason putting the file descriptor
+ # back into blocking mode seems to resolve this issue.
+ fdesc.setBlocking(self.fd)
+
+ abstract.FileDescriptor.connectionLost(self, reason)
+ self.proc.childConnectionLost(self.name, reason)
+
+
+
+class ProcessReader(abstract.FileDescriptor):
+ """
+ ProcessReader
+
+ I am a selectable representation of a process's output pipe, such as
+ stdout and stderr.
+ """
+ connected = 1
+
+ def __init__(self, reactor, proc, name, fileno):
+ """
+ Initialize, specifying a process to connect to.
+ """
+ abstract.FileDescriptor.__init__(self, reactor)
+ fdesc.setNonBlocking(fileno)
+ self.proc = proc
+ self.name = name
+ self.fd = fileno
+ self.startReading()
+
+ def fileno(self):
+ """
+ Return the fileno() of my process's stderr.
+ """
+ return self.fd
+
+ def writeSomeData(self, data):
+ # the only time this is actually called is after .loseConnection Any
+ # actual write attempt would fail, so we must avoid that. This hack
+ # allows us to use .loseConnection on both readers and writers.
+ assert data == ""
+ return CONNECTION_LOST
+
+ def doRead(self):
+ """
+ This is called when the pipe becomes readable.
+ """
+ return fdesc.readFromFD(self.fd, self.dataReceived)
+
+ def dataReceived(self, data):
+ self.proc.childDataReceived(self.name, data)
+
+ def loseConnection(self):
+ if self.connected and not self.disconnecting:
+ self.disconnecting = 1
+ self.stopReading()
+ self.reactor.callLater(0, self.connectionLost,
+ failure.Failure(CONNECTION_DONE))
+
+ def connectionLost(self, reason):
+ """
+ Close my end of the pipe, signal the Process (which signals the
+ ProcessProtocol).
+ """
+ abstract.FileDescriptor.connectionLost(self, reason)
+ self.proc.childConnectionLost(self.name, reason)
+
+
+class _BaseProcess(BaseProcess, object):
+ """
+ Base class for Process and PTYProcess.
+ """
+ status = None
+ pid = None
+
+ def reapProcess(self):
+ """
+ Try to reap a process (without blocking) via waitpid.
+
+ This is called when sigchild is caught or a Process object loses its
+ "connection" (stdout is closed) This ought to result in reaping all
+ zombie processes, since it will be called twice as often as it needs
+ to be.
+
+ (Unfortunately, this is a slightly experimental approach, since
+ UNIX has no way to be really sure that your process is going to
+ go away w/o blocking. I don't want to block.)
+ """
+ try:
+ try:
+ pid, status = os.waitpid(self.pid, os.WNOHANG)
+ except OSError, e:
+ if e.errno == errno.ECHILD:
+ # no child process
+ pid = None
+ else:
+ raise
+ except:
+ log.msg('Failed to reap %d:' % self.pid)
+ log.err()
+ pid = None
+ if pid:
+ self.processEnded(status)
+ unregisterReapProcessHandler(pid, self)
+
+
+ def _getReason(self, status):
+ exitCode = sig = None
+ if os.WIFEXITED(status):
+ exitCode = os.WEXITSTATUS(status)
+ else:
+ sig = os.WTERMSIG(status)
+ if exitCode or sig:
+ return error.ProcessTerminated(exitCode, sig, status)
+ return error.ProcessDone(status)
+
+
+ def signalProcess(self, signalID):
+ """
+ Send the given signal C{signalID} to the process. It'll translate a
+ few signals ('HUP', 'STOP', 'INT', 'KILL', 'TERM') from a string
+ representation to its int value, otherwise it'll pass directly the
+ value provided
+
+ @type signalID: C{str} or C{int}
+ """
+ if signalID in ('HUP', 'STOP', 'INT', 'KILL', 'TERM'):
+ signalID = getattr(signal, 'SIG%s' % (signalID,))
+ if self.pid is None:
+ raise ProcessExitedAlready()
+ os.kill(self.pid, signalID)
+
+
+ def _resetSignalDisposition(self):
+ # The Python interpreter ignores some signals, and our child
+ # process will inherit that behaviour. To have a child process
+ # that responds to signals normally, we need to reset our
+ # child process's signal handling (just) after we fork and
+ # before we execvpe.
+ for signalnum in range(1, signal.NSIG):
+ if signal.getsignal(signalnum) == signal.SIG_IGN:
+ # Reset signal handling to the default
+ signal.signal(signalnum, signal.SIG_DFL)
+
+
+ def _fork(self, path, uid, gid, executable, args, environment, **kwargs):
+ """
+ Fork and then exec sub-process.
+
+ @param path: the path where to run the new process.
+ @type path: C{str}
+ @param uid: if defined, the uid used to run the new process.
+ @type uid: C{int}
+ @param gid: if defined, the gid used to run the new process.
+ @type gid: C{int}
+ @param executable: the executable to run in a new process.
+ @type executable: C{str}
+ @param args: arguments used to create the new process.
+ @type args: C{list}.
+ @param environment: environment used for the new process.
+ @type environment: C{dict}.
+ @param kwargs: keyword arguments to L{_setupChild} method.
+ """
+ settingUID = (uid is not None) or (gid is not None)
+ if settingUID:
+ curegid = os.getegid()
+ currgid = os.getgid()
+ cureuid = os.geteuid()
+ curruid = os.getuid()
+ if uid is None:
+ uid = cureuid
+ if gid is None:
+ gid = curegid
+ # prepare to change UID in subprocess
+ os.setuid(0)
+ os.setgid(0)
+
+ collectorEnabled = gc.isenabled()
+ gc.disable()
+ try:
+ self.pid = os.fork()
+ except:
+ # Still in the parent process
+ if settingUID:
+ os.setregid(currgid, curegid)
+ os.setreuid(curruid, cureuid)
+ if collectorEnabled:
+ gc.enable()
+ raise
+ else:
+ if self.pid == 0: # pid is 0 in the child process
+ # do not put *ANY* code outside the try block. The child process
+ # must either exec or _exit. If it gets outside this block (due
+ # to an exception that is not handled here, but which might be
+ # handled higher up), there will be two copies of the parent
+ # running in parallel, doing all kinds of damage.
+
+ # After each change to this code, review it to make sure there
+ # are no exit paths.
+ try:
+ # Stop debugging. If I am, I don't care anymore.
+ sys.settrace(None)
+ self._setupChild(**kwargs)
+ self._execChild(path, settingUID, uid, gid,
+ executable, args, environment)
+ except:
+ # If there are errors, bail and try to write something
+ # descriptive to stderr.
+ # XXX: The parent's stderr isn't necessarily fd 2 anymore, or
+ # even still available
+ # XXXX: however even libc assumes write(2, err) is a useful
+ # thing to attempt
+ try:
+ stderr = os.fdopen(2, 'w')
+ stderr.write("Upon execvpe %s %s in environment %s\n:" %
+ (executable, str(args),
+ "id %s" % id(environment)))
+ traceback.print_exc(file=stderr)
+ stderr.flush()
+ for fd in range(3):
+ os.close(fd)
+ except:
+ pass # make *sure* the child terminates
+ # Did you read the comment about not adding code here?
+ os._exit(1)
+
+ # we are now in parent process
+ if settingUID:
+ os.setregid(currgid, curegid)
+ os.setreuid(curruid, cureuid)
+ if collectorEnabled:
+ gc.enable()
+ self.status = -1 # this records the exit status of the child
+
+ def _setupChild(self, *args, **kwargs):
+ """
+ Setup the child process. Override in subclasses.
+ """
+ raise NotImplementedError()
+
+ def _execChild(self, path, settingUID, uid, gid,
+ executable, args, environment):
+ """
+ The exec() which is done in the forked child.
+ """
+ if path:
+ os.chdir(path)
+ # set the UID before I actually exec the process
+ if settingUID:
+ switchUID(uid, gid)
+ os.execvpe(executable, args, environment)
+
+ def __repr__(self):
+ """
+ String representation of a process.
+ """
+ return "<%s pid=%s status=%s>" % (self.__class__.__name__,
+ self.pid, self.status)
+
+
+class _FDDetector(object):
+ """
+ This class contains the logic necessary to decide which of the available
+ system techniques should be used to detect the open file descriptors for
+ the current process. The chosen technique gets monkey-patched into the
+ _listOpenFDs method of this class so that the detection only needs to occur
+ once.
+
+ @ivars listdir: The implementation of listdir to use. This gets overwritten
+ by the test cases.
+ @ivars getpid: The implementation of getpid to use, returns the PID of the
+ running process.
+ @ivars openfile: The implementation of open() to use, by default the Python
+ builtin.
+ """
+ # So that we can unit test this
+ listdir = os.listdir
+ getpid = os.getpid
+ openfile = open
+
+ def __init__(self):
+ self._implementations = [
+ self._procFDImplementation, self._devFDImplementation,
+ self._fallbackFDImplementation]
+
+
+ def _listOpenFDs(self):
+ """
+ Return an iterable of file descriptors which I{may} be open in this
+ process.
+
+ This will try to return the fewest possible descriptors without missing
+ any.
+ """
+ self._listOpenFDs = self._getImplementation()
+ return self._listOpenFDs()
+
+
+ def _getImplementation(self):
+ """
+ Pick a method which gives correct results for C{_listOpenFDs} in this
+ runtime environment.
+
+ This involves a lot of very platform-specific checks, some of which may
+ be relatively expensive. Therefore the returned method should be saved
+ and re-used, rather than always calling this method to determine what it
+ is.
+
+ See the implementation for the details of how a method is selected.
+ """
+ for impl in self._implementations:
+ try:
+ before = impl()
+ except:
+ continue
+ try:
+ fp = self.openfile("/dev/null", "r")
+ after = impl()
+ finally:
+ fp.close()
+ if before != after:
+ return impl
+ # If no implementation can detect the newly opened file above, then just
+ # return the last one. The last one should therefore always be one
+ # which makes a simple static guess which includes all possible open
+ # file descriptors, but perhaps also many other values which do not
+ # correspond to file descriptors. For example, the scheme implemented
+ # by _fallbackFDImplementation is suitable to be the last entry.
+ return impl
+
+
+ def _devFDImplementation(self):
+ """
+ Simple implementation for systems where /dev/fd actually works.
+ See: http://www.freebsd.org/cgi/man.cgi?fdescfs
+ """
+ dname = "/dev/fd"
+ result = [int(fd) for fd in self.listdir(dname)]
+ return result
+
+
+ def _procFDImplementation(self):
+ """
+ Simple implementation for systems where /proc/pid/fd exists (we assume
+ it works).
+ """
+ dname = "/proc/%d/fd" % (self.getpid(),)
+ return [int(fd) for fd in self.listdir(dname)]
+
+
+ def _fallbackFDImplementation(self):
+ """
+ Fallback implementation where either the resource module can inform us
+ about the upper bound of how many FDs to expect, or where we just guess
+ a constant maximum if there is no resource module.
+
+ All possible file descriptors from 0 to that upper bound are returned
+ with no attempt to exclude invalid file descriptor values.
+ """
+ try:
+ import resource
+ except ImportError:
+ maxfds = 1024
+ else:
+ # OS-X reports 9223372036854775808. That's a lot of fds to close.
+ # OS-X should get the /dev/fd implementation instead, so mostly
+ # this check probably isn't necessary.
+ maxfds = min(1024, resource.getrlimit(resource.RLIMIT_NOFILE)[1])
+ return range(maxfds)
+
+
+
+detector = _FDDetector()
+
+def _listOpenFDs():
+ """
+ Use the global detector object to figure out which FD implementation to
+ use.
+ """
+ return detector._listOpenFDs()
+
+
+class Process(_BaseProcess):
+ """
+ An operating-system Process.
+
+ This represents an operating-system process with arbitrary input/output
+ pipes connected to it. Those pipes may represent standard input,
+ standard output, and standard error, or any other file descriptor.
+
+ On UNIX, this is implemented using fork(), exec(), pipe()
+ and fcntl(). These calls may not exist elsewhere so this
+ code is not cross-platform. (also, windows can only select
+ on sockets...)
+ """
+ implements(IProcessTransport)
+
+ debug = False
+ debug_child = False
+
+ status = -1
+ pid = None
+
+ processWriterFactory = ProcessWriter
+ processReaderFactory = ProcessReader
+
+ def __init__(self,
+ reactor, executable, args, environment, path, proto,
+ uid=None, gid=None, childFDs=None):
+ """
+ Spawn an operating-system process.
+
+ This is where the hard work of disconnecting all currently open
+ files / forking / executing the new process happens. (This is
+ executed automatically when a Process is instantiated.)
+
+ This will also run the subprocess as a given user ID and group ID, if
+ specified. (Implementation Note: this doesn't support all the arcane
+ nuances of setXXuid on UNIX: it will assume that either your effective
+ or real UID is 0.)
+ """
+ if not proto:
+ assert 'r' not in childFDs.values()
+ assert 'w' not in childFDs.values()
+ _BaseProcess.__init__(self, proto)
+
+ self.pipes = {}
+ # keys are childFDs, we can sense them closing
+ # values are ProcessReader/ProcessWriters
+
+ helpers = {}
+ # keys are childFDs
+ # values are parentFDs
+
+ if childFDs is None:
+ childFDs = {0: "w", # we write to the child's stdin
+ 1: "r", # we read from their stdout
+ 2: "r", # and we read from their stderr
+ }
+
+ debug = self.debug
+ if debug: print "childFDs", childFDs
+
+ _openedPipes = []
+ def pipe():
+ r, w = os.pipe()
+ _openedPipes.extend([r, w])
+ return r, w
+
+ # fdmap.keys() are filenos of pipes that are used by the child.
+ fdmap = {} # maps childFD to parentFD
+ try:
+ for childFD, target in childFDs.items():
+ if debug: print "[%d]" % childFD, target
+ if target == "r":
+ # we need a pipe that the parent can read from
+ readFD, writeFD = pipe()
+ if debug: print "readFD=%d, writeFD=%d" % (readFD, writeFD)
+ fdmap[childFD] = writeFD # child writes to this
+ helpers[childFD] = readFD # parent reads from this
+ elif target == "w":
+ # we need a pipe that the parent can write to
+ readFD, writeFD = pipe()
+ if debug: print "readFD=%d, writeFD=%d" % (readFD, writeFD)
+ fdmap[childFD] = readFD # child reads from this
+ helpers[childFD] = writeFD # parent writes to this
+ else:
+ assert type(target) == int, '%r should be an int' % (target,)
+ fdmap[childFD] = target # parent ignores this
+ if debug: print "fdmap", fdmap
+ if debug: print "helpers", helpers
+ # the child only cares about fdmap.values()
+
+ self._fork(path, uid, gid, executable, args, environment, fdmap=fdmap)
+ except:
+ map(os.close, _openedPipes)
+ raise
+
+ # we are the parent process:
+ self.proto = proto
+
+ # arrange for the parent-side pipes to be read and written
+ for childFD, parentFD in helpers.items():
+ os.close(fdmap[childFD])
+
+ if childFDs[childFD] == "r":
+ reader = self.processReaderFactory(reactor, self, childFD,
+ parentFD)
+ self.pipes[childFD] = reader
+
+ if childFDs[childFD] == "w":
+ writer = self.processWriterFactory(reactor, self, childFD,
+ parentFD, forceReadHack=True)
+ self.pipes[childFD] = writer
+
+ try:
+ # the 'transport' is used for some compatibility methods
+ if self.proto is not None:
+ self.proto.makeConnection(self)
+ except:
+ log.err()
+
+ # The reactor might not be running yet. This might call back into
+ # processEnded synchronously, triggering an application-visible
+ # callback. That's probably not ideal. The replacement API for
+ # spawnProcess should improve upon this situation.
+ registerReapProcessHandler(self.pid, self)
+
+
+ def _setupChild(self, fdmap):
+ """
+ fdmap[childFD] = parentFD
+
+ The child wants to end up with 'childFD' attached to what used to be
+ the parent's parentFD. As an example, a bash command run like
+ 'command 2>&1' would correspond to an fdmap of {0:0, 1:1, 2:1}.
+ 'command >foo.txt' would be {0:0, 1:os.open('foo.txt'), 2:2}.
+
+ This is accomplished in two steps::
+
+ 1. close all file descriptors that aren't values of fdmap. This
+ means 0 .. maxfds (or just the open fds within that range, if
+ the platform supports '/proc/<pid>/fd').
+
+ 2. for each childFD::
+
+ - if fdmap[childFD] == childFD, the descriptor is already in
+ place. Make sure the CLOEXEC flag is not set, then delete
+ the entry from fdmap.
+
+ - if childFD is in fdmap.values(), then the target descriptor
+ is busy. Use os.dup() to move it elsewhere, update all
+ fdmap[childFD] items that point to it, then close the
+ original. Then fall through to the next case.
+
+ - now fdmap[childFD] is not in fdmap.values(), and is free.
+ Use os.dup2() to move it to the right place, then close the
+ original.
+ """
+
+ debug = self.debug_child
+ if debug:
+ errfd = sys.stderr
+ errfd.write("starting _setupChild\n")
+
+ destList = fdmap.values()
+ for fd in _listOpenFDs():
+ if fd in destList:
+ continue
+ if debug and fd == errfd.fileno():
+ continue
+ try:
+ os.close(fd)
+ except:
+ pass
+
+ # at this point, the only fds still open are the ones that need to
+ # be moved to their appropriate positions in the child (the targets
+ # of fdmap, i.e. fdmap.values() )
+
+ if debug: print >>errfd, "fdmap", fdmap
+ childlist = fdmap.keys()
+ childlist.sort()
+
+ for child in childlist:
+ target = fdmap[child]
+ if target == child:
+ # fd is already in place
+ if debug: print >>errfd, "%d already in place" % target
+ fdesc._unsetCloseOnExec(child)
+ else:
+ if child in fdmap.values():
+ # we can't replace child-fd yet, as some other mapping
+ # still needs the fd it wants to target. We must preserve
+ # that old fd by duping it to a new home.
+ newtarget = os.dup(child) # give it a safe home
+ if debug: print >>errfd, "os.dup(%d) -> %d" % (child,
+ newtarget)
+ os.close(child) # close the original
+ for c, p in fdmap.items():
+ if p == child:
+ fdmap[c] = newtarget # update all pointers
+ # now it should be available
+ if debug: print >>errfd, "os.dup2(%d,%d)" % (target, child)
+ os.dup2(target, child)
+
+ # At this point, the child has everything it needs. We want to close
+ # everything that isn't going to be used by the child, i.e.
+ # everything not in fdmap.keys(). The only remaining fds open are
+ # those in fdmap.values().
+
+ # Any given fd may appear in fdmap.values() multiple times, so we
+ # need to remove duplicates first.
+
+ old = []
+ for fd in fdmap.values():
+ if not fd in old:
+ if not fd in fdmap.keys():
+ old.append(fd)
+ if debug: print >>errfd, "old", old
+ for fd in old:
+ os.close(fd)
+
+ self._resetSignalDisposition()
+
+
+ def writeToChild(self, childFD, data):
+ self.pipes[childFD].write(data)
+
+ def closeChildFD(self, childFD):
+ # for writer pipes, loseConnection tries to write the remaining data
+ # out to the pipe before closing it
+ # if childFD is not in the list of pipes, assume that it is already
+ # closed
+ if childFD in self.pipes:
+ self.pipes[childFD].loseConnection()
+
+ def pauseProducing(self):
+ for p in self.pipes.itervalues():
+ if isinstance(p, ProcessReader):
+ p.stopReading()
+
+ def resumeProducing(self):
+ for p in self.pipes.itervalues():
+ if isinstance(p, ProcessReader):
+ p.startReading()
+
+ # compatibility
+ def closeStdin(self):
+ """
+ Call this to close standard input on this process.
+ """
+ self.closeChildFD(0)
+
+ def closeStdout(self):
+ self.closeChildFD(1)
+
+ def closeStderr(self):
+ self.closeChildFD(2)
+
+ def loseConnection(self):
+ self.closeStdin()
+ self.closeStderr()
+ self.closeStdout()
+
+ def write(self, data):
+ """
+ Call this to write to standard input on this process.
+
+ NOTE: This will silently lose data if there is no standard input.
+ """
+ if 0 in self.pipes:
+ self.pipes[0].write(data)
+
+ def registerProducer(self, producer, streaming):
+ """
+ Call this to register producer for standard input.
+
+ If there is no standard input producer.stopProducing() will
+ be called immediately.
+ """
+ if 0 in self.pipes:
+ self.pipes[0].registerProducer(producer, streaming)
+ else:
+ producer.stopProducing()
+
+ def unregisterProducer(self):
+ """
+ Call this to unregister producer for standard input."""
+ if 0 in self.pipes:
+ self.pipes[0].unregisterProducer()
+
+ def writeSequence(self, seq):
+ """
+ Call this to write to standard input on this process.
+
+ NOTE: This will silently lose data if there is no standard input.
+ """
+ if 0 in self.pipes:
+ self.pipes[0].writeSequence(seq)
+
+
+ def childDataReceived(self, name, data):
+ self.proto.childDataReceived(name, data)
+
+
+ def childConnectionLost(self, childFD, reason):
+ # this is called when one of the helpers (ProcessReader or
+ # ProcessWriter) notices their pipe has been closed
+ os.close(self.pipes[childFD].fileno())
+ del self.pipes[childFD]
+ try:
+ self.proto.childConnectionLost(childFD)
+ except:
+ log.err()
+ self.maybeCallProcessEnded()
+
+ def maybeCallProcessEnded(self):
+ # we don't call ProcessProtocol.processEnded until:
+ # the child has terminated, AND
+ # all writers have indicated an error status, AND
+ # all readers have indicated EOF
+ # This insures that we've gathered all output from the process.
+ if self.pipes:
+ return
+ if not self.lostProcess:
+ self.reapProcess()
+ return
+ _BaseProcess.maybeCallProcessEnded(self)
+
+
+
+class PTYProcess(abstract.FileDescriptor, _BaseProcess):
+ """
+ An operating-system Process that uses PTY support.
+ """
+ implements(IProcessTransport)
+
+ status = -1
+ pid = None
+
+ def __init__(self, reactor, executable, args, environment, path, proto,
+ uid=None, gid=None, usePTY=None):
+ """
+ Spawn an operating-system process.
+
+ This is where the hard work of disconnecting all currently open
+ files / forking / executing the new process happens. (This is
+ executed automatically when a Process is instantiated.)
+
+ This will also run the subprocess as a given user ID and group ID, if
+ specified. (Implementation Note: this doesn't support all the arcane
+ nuances of setXXuid on UNIX: it will assume that either your effective
+ or real UID is 0.)
+ """
+ if pty is None and not isinstance(usePTY, (tuple, list)):
+ # no pty module and we didn't get a pty to use
+ raise NotImplementedError(
+ "cannot use PTYProcess on platforms without the pty module.")
+ abstract.FileDescriptor.__init__(self, reactor)
+ _BaseProcess.__init__(self, proto)
+
+ if isinstance(usePTY, (tuple, list)):
+ masterfd, slavefd, ttyname = usePTY
+ else:
+ masterfd, slavefd = pty.openpty()
+ ttyname = os.ttyname(slavefd)
+
+ try:
+ self._fork(path, uid, gid, executable, args, environment,
+ masterfd=masterfd, slavefd=slavefd)
+ except:
+ if not isinstance(usePTY, (tuple, list)):
+ os.close(masterfd)
+ os.close(slavefd)
+ raise
+
+ # we are now in parent process:
+ os.close(slavefd)
+ fdesc.setNonBlocking(masterfd)
+ self.fd = masterfd
+ self.startReading()
+ self.connected = 1
+ self.status = -1
+ try:
+ self.proto.makeConnection(self)
+ except:
+ log.err()
+ registerReapProcessHandler(self.pid, self)
+
+ def _setupChild(self, masterfd, slavefd):
+ """
+ Setup child process after fork() but before exec().
+ """
+ os.close(masterfd)
+ if hasattr(termios, 'TIOCNOTTY'):
+ try:
+ fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
+ except OSError:
+ pass
+ else:
+ try:
+ fcntl.ioctl(fd, termios.TIOCNOTTY, '')
+ except:
+ pass
+ os.close(fd)
+
+ os.setsid()
+
+ if hasattr(termios, 'TIOCSCTTY'):
+ fcntl.ioctl(slavefd, termios.TIOCSCTTY, '')
+
+ for fd in range(3):
+ if fd != slavefd:
+ os.close(fd)
+
+ os.dup2(slavefd, 0) # stdin
+ os.dup2(slavefd, 1) # stdout
+ os.dup2(slavefd, 2) # stderr
+
+ for fd in _listOpenFDs():
+ if fd > 2:
+ try:
+ os.close(fd)
+ except:
+ pass
+
+ self._resetSignalDisposition()
+
+
+ # PTYs do not have stdin/stdout/stderr. They only have in and out, just
+ # like sockets. You cannot close one without closing off the entire PTY.
+ def closeStdin(self):
+ pass
+
+ def closeStdout(self):
+ pass
+
+ def closeStderr(self):
+ pass
+
+ def doRead(self):
+ """
+ Called when my standard output stream is ready for reading.
+ """
+ return fdesc.readFromFD(
+ self.fd,
+ lambda data: self.proto.childDataReceived(1, data))
+
+ def fileno(self):
+ """
+ This returns the file number of standard output on this process.
+ """
+ return self.fd
+
+ def maybeCallProcessEnded(self):
+ # two things must happen before we call the ProcessProtocol's
+ # processEnded method. 1: the child process must die and be reaped
+ # (which calls our own processEnded method). 2: the child must close
+ # their stdin/stdout/stderr fds, causing the pty to close, causing
+ # our connectionLost method to be called. #2 can also be triggered
+ # by calling .loseConnection().
+ if self.lostProcess == 2:
+ _BaseProcess.maybeCallProcessEnded(self)
+
+ def connectionLost(self, reason):
+ """
+ I call this to clean up when one or all of my connections has died.
+ """
+ abstract.FileDescriptor.connectionLost(self, reason)
+ os.close(self.fd)
+ self.lostProcess += 1
+ self.maybeCallProcessEnded()
+
+ def writeSomeData(self, data):
+ """
+ Write some data to the open process.
+ """
+ return fdesc.writeToFD(self.fd, data)
diff --git a/twisted/internet/protocol.py b/twisted/internet/protocol.py
new file mode 100644
index 0000000..094a535
--- /dev/null
+++ b/twisted/internet/protocol.py
@@ -0,0 +1,830 @@
+# -*- test-case-name: twisted.test.test_factories,twisted.internet.test.test_protocol -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Standard implementations of Twisted protocol-related interfaces.
+
+Start here if you are looking to write a new protocol implementation for
+Twisted. The Protocol class contains some introductory material.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+import random
+from zope.interface import implements
+
+# Twisted Imports
+from twisted.python import log, failure, components
+from twisted.internet import interfaces, error, defer
+
+
+class Factory:
+ """
+ This is a factory which produces protocols.
+
+ By default, buildProtocol will create a protocol of the class given in
+ self.protocol.
+ """
+
+ implements(interfaces.IProtocolFactory, interfaces.ILoggingContext)
+
+ # put a subclass of Protocol here:
+ protocol = None
+
+ numPorts = 0
+ noisy = True
+
+ def logPrefix(self):
+ """
+ Describe this factory for log messages.
+ """
+ return self.__class__.__name__
+
+
+ def doStart(self):
+ """Make sure startFactory is called.
+
+ Users should not call this function themselves!
+ """
+ if not self.numPorts:
+ if self.noisy:
+ log.msg("Starting factory %r" % self)
+ self.startFactory()
+ self.numPorts = self.numPorts + 1
+
+ def doStop(self):
+ """Make sure stopFactory is called.
+
+ Users should not call this function themselves!
+ """
+ if self.numPorts == 0:
+ # this shouldn't happen, but does sometimes and this is better
+ # than blowing up in assert as we did previously.
+ return
+ self.numPorts = self.numPorts - 1
+ if not self.numPorts:
+ if self.noisy:
+ log.msg("Stopping factory %r" % self)
+ self.stopFactory()
+
+ def startFactory(self):
+ """This will be called before I begin listening on a Port or Connector.
+
+ It will only be called once, even if the factory is connected
+ to multiple ports.
+
+ This can be used to perform 'unserialization' tasks that
+ are best put off until things are actually running, such
+ as connecting to a database, opening files, etcetera.
+ """
+
+ def stopFactory(self):
+ """This will be called before I stop listening on all Ports/Connectors.
+
+ This can be overridden to perform 'shutdown' tasks such as disconnecting
+ database connections, closing files, etc.
+
+ It will be called, for example, before an application shuts down,
+ if it was connected to a port. User code should not call this function
+ directly.
+ """
+
+ def buildProtocol(self, addr):
+ """Create an instance of a subclass of Protocol.
+
+ The returned instance will handle input on an incoming server
+ connection, and an attribute \"factory\" pointing to the creating
+ factory.
+
+ Override this method to alter how Protocol instances get created.
+
+ @param addr: an object implementing L{twisted.internet.interfaces.IAddress}
+ """
+ p = self.protocol()
+ p.factory = self
+ return p
+
+
+class ClientFactory(Factory):
+ """A Protocol factory for clients.
+
+ This can be used together with the various connectXXX methods in
+ reactors.
+ """
+
+ def startedConnecting(self, connector):
+ """Called when a connection has been started.
+
+ You can call connector.stopConnecting() to stop the connection attempt.
+
+ @param connector: a Connector object.
+ """
+
+ def clientConnectionFailed(self, connector, reason):
+ """Called when a connection has failed to connect.
+
+ It may be useful to call connector.connect() - this will reconnect.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+ def clientConnectionLost(self, connector, reason):
+ """Called when an established connection is lost.
+
+ It may be useful to call connector.connect() - this will reconnect.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+
+class _InstanceFactory(ClientFactory):
+ """
+ Factory used by ClientCreator.
+
+ @ivar deferred: The L{Deferred} which represents this connection attempt and
+ which will be fired when it succeeds or fails.
+
+ @ivar pending: After a connection attempt succeeds or fails, a delayed call
+ which will fire the L{Deferred} representing this connection attempt.
+ """
+
+ noisy = False
+ pending = None
+
+ def __init__(self, reactor, instance, deferred):
+ self.reactor = reactor
+ self.instance = instance
+ self.deferred = deferred
+
+
+ def __repr__(self):
+ return "<ClientCreator factory: %r>" % (self.instance, )
+
+
+ def buildProtocol(self, addr):
+ """
+ Return the pre-constructed protocol instance and arrange to fire the
+ waiting L{Deferred} to indicate success establishing the connection.
+ """
+ self.pending = self.reactor.callLater(
+ 0, self.fire, self.deferred.callback, self.instance)
+ self.deferred = None
+ return self.instance
+
+
+ def clientConnectionFailed(self, connector, reason):
+ """
+ Arrange to fire the waiting L{Deferred} with the given failure to
+ indicate the connection could not be established.
+ """
+ self.pending = self.reactor.callLater(
+ 0, self.fire, self.deferred.errback, reason)
+ self.deferred = None
+
+
+ def fire(self, func, value):
+ """
+ Clear C{self.pending} to avoid a reference cycle and then invoke func
+ with the value.
+ """
+ self.pending = None
+ func(value)
+
+
+
+class ClientCreator:
+ """
+ Client connections that do not require a factory.
+
+ The various connect* methods create a protocol instance using the given
+ protocol class and arguments, and connect it, returning a Deferred of the
+ resulting protocol instance.
+
+ Useful for cases when we don't really need a factory. Mainly this
+ is when there is no shared state between protocol instances, and no need
+ to reconnect.
+
+ The C{connectTCP}, C{connectUNIX}, and C{connectSSL} methods each return a
+ L{Deferred} which will fire with an instance of the protocol class passed to
+ L{ClientCreator.__init__}. These Deferred can be cancelled to abort the
+ connection attempt (in a very unlikely case, cancelling the Deferred may not
+ prevent the protocol from being instantiated and connected to a transport;
+ if this happens, it will be disconnected immediately afterwards and the
+ Deferred will still errback with L{CancelledError}).
+ """
+
+ def __init__(self, reactor, protocolClass, *args, **kwargs):
+ self.reactor = reactor
+ self.protocolClass = protocolClass
+ self.args = args
+ self.kwargs = kwargs
+
+
+ def _connect(self, method, *args, **kwargs):
+ """
+ Initiate a connection attempt.
+
+ @param method: A callable which will actually start the connection
+ attempt. For example, C{reactor.connectTCP}.
+
+ @param *args: Positional arguments to pass to C{method}, excluding the
+ factory.
+
+ @param **kwargs: Keyword arguments to pass to C{method}.
+
+ @return: A L{Deferred} which fires with an instance of the protocol
+ class passed to this L{ClientCreator}'s initializer or fails if the
+ connection cannot be set up for some reason.
+ """
+ def cancelConnect(deferred):
+ connector.disconnect()
+ if f.pending is not None:
+ f.pending.cancel()
+ d = defer.Deferred(cancelConnect)
+ f = _InstanceFactory(
+ self.reactor, self.protocolClass(*self.args, **self.kwargs), d)
+ connector = method(factory=f, *args, **kwargs)
+ return d
+
+
+ def connectTCP(self, host, port, timeout=30, bindAddress=None):
+ """
+ Connect to a TCP server.
+
+ The parameters are all the same as to L{IReactorTCP.connectTCP} except
+ that the factory parameter is omitted.
+
+ @return: A L{Deferred} which fires with an instance of the protocol
+ class passed to this L{ClientCreator}'s initializer or fails if the
+ connection cannot be set up for some reason.
+ """
+ return self._connect(
+ self.reactor.connectTCP, host, port, timeout=timeout,
+ bindAddress=bindAddress)
+
+
+ def connectUNIX(self, address, timeout=30, checkPID=False):
+ """
+ Connect to a Unix socket.
+
+ The parameters are all the same as to L{IReactorUNIX.connectUNIX} except
+ that the factory parameter is omitted.
+
+ @return: A L{Deferred} which fires with an instance of the protocol
+ class passed to this L{ClientCreator}'s initializer or fails if the
+ connection cannot be set up for some reason.
+ """
+ return self._connect(
+ self.reactor.connectUNIX, address, timeout=timeout,
+ checkPID=checkPID)
+
+
+ def connectSSL(self, host, port, contextFactory, timeout=30, bindAddress=None):
+ """
+ Connect to an SSL server.
+
+ The parameters are all the same as to L{IReactorSSL.connectSSL} except
+ that the factory parameter is omitted.
+
+ @return: A L{Deferred} which fires with an instance of the protocol
+ class passed to this L{ClientCreator}'s initializer or fails if the
+ connection cannot be set up for some reason.
+ """
+ return self._connect(
+ self.reactor.connectSSL, host, port,
+ contextFactory=contextFactory, timeout=timeout,
+ bindAddress=bindAddress)
+
+
+
+class ReconnectingClientFactory(ClientFactory):
+ """
+ Factory which auto-reconnects clients with an exponential back-off.
+
+ Note that clients should call my resetDelay method after they have
+ connected successfully.
+
+ @ivar maxDelay: Maximum number of seconds between connection attempts.
+ @ivar initialDelay: Delay for the first reconnection attempt.
+ @ivar factor: A multiplicitive factor by which the delay grows
+ @ivar jitter: Percentage of randomness to introduce into the delay length
+ to prevent stampeding.
+ @ivar clock: The clock used to schedule reconnection. It's mainly useful to
+ be parametrized in tests. If the factory is serialized, this attribute
+ will not be serialized, and the default value (the reactor) will be
+ restored when deserialized.
+ @type clock: L{IReactorTime}
+ @ivar maxRetries: Maximum number of consecutive unsuccessful connection
+ attempts, after which no further connection attempts will be made. If
+ this is not explicitly set, no maximum is applied.
+ """
+ maxDelay = 3600
+ initialDelay = 1.0
+ # Note: These highly sensitive factors have been precisely measured by
+ # the National Institute of Science and Technology. Take extreme care
+ # in altering them, or you may damage your Internet!
+ # (Seriously: <http://physics.nist.gov/cuu/Constants/index.html>)
+ factor = 2.7182818284590451 # (math.e)
+ # Phi = 1.6180339887498948 # (Phi is acceptable for use as a
+ # factor if e is too large for your application.)
+ jitter = 0.11962656472 # molar Planck constant times c, joule meter/mole
+
+ delay = initialDelay
+ retries = 0
+ maxRetries = None
+ _callID = None
+ connector = None
+ clock = None
+
+ continueTrying = 1
+
+
+ def clientConnectionFailed(self, connector, reason):
+ if self.continueTrying:
+ self.connector = connector
+ self.retry()
+
+
+ def clientConnectionLost(self, connector, unused_reason):
+ if self.continueTrying:
+ self.connector = connector
+ self.retry()
+
+
+ def retry(self, connector=None):
+ """
+ Have this connector connect again, after a suitable delay.
+ """
+ if not self.continueTrying:
+ if self.noisy:
+ log.msg("Abandoning %s on explicit request" % (connector,))
+ return
+
+ if connector is None:
+ if self.connector is None:
+ raise ValueError("no connector to retry")
+ else:
+ connector = self.connector
+
+ self.retries += 1
+ if self.maxRetries is not None and (self.retries > self.maxRetries):
+ if self.noisy:
+ log.msg("Abandoning %s after %d retries." %
+ (connector, self.retries))
+ return
+
+ self.delay = min(self.delay * self.factor, self.maxDelay)
+ if self.jitter:
+ self.delay = random.normalvariate(self.delay,
+ self.delay * self.jitter)
+
+ if self.noisy:
+ log.msg("%s will retry in %d seconds" % (connector, self.delay,))
+
+ def reconnector():
+ self._callID = None
+ connector.connect()
+ if self.clock is None:
+ from twisted.internet import reactor
+ self.clock = reactor
+ self._callID = self.clock.callLater(self.delay, reconnector)
+
+
+ def stopTrying(self):
+ """
+ Put a stop to any attempt to reconnect in progress.
+ """
+ # ??? Is this function really stopFactory?
+ if self._callID:
+ self._callID.cancel()
+ self._callID = None
+ self.continueTrying = 0
+ if self.connector:
+ try:
+ self.connector.stopConnecting()
+ except error.NotConnectingError:
+ pass
+
+
+ def resetDelay(self):
+ """
+ Call this method after a successful connection: it resets the delay and
+ the retry counter.
+ """
+ self.delay = self.initialDelay
+ self.retries = 0
+ self._callID = None
+ self.continueTrying = 1
+
+
+ def __getstate__(self):
+ """
+ Remove all of the state which is mutated by connection attempts and
+ failures, returning just the state which describes how reconnections
+ should be attempted. This will make the unserialized instance
+ behave just as this one did when it was first instantiated.
+ """
+ state = self.__dict__.copy()
+ for key in ['connector', 'retries', 'delay',
+ 'continueTrying', '_callID', 'clock']:
+ if key in state:
+ del state[key]
+ return state
+
+
+
+class ServerFactory(Factory):
+ """Subclass this to indicate that your protocol.Factory is only usable for servers.
+ """
+
+
+
+class BaseProtocol:
+ """
+ This is the abstract superclass of all protocols.
+
+ Some methods have helpful default implementations here so that they can
+ easily be shared, but otherwise the direct subclasses of this class are more
+ interesting, L{Protocol} and L{ProcessProtocol}.
+ """
+ connected = 0
+ transport = None
+
+ def makeConnection(self, transport):
+ """Make a connection to a transport and a server.
+
+ This sets the 'transport' attribute of this Protocol, and calls the
+ connectionMade() callback.
+ """
+ self.connected = 1
+ self.transport = transport
+ self.connectionMade()
+
+ def connectionMade(self):
+ """Called when a connection is made.
+
+ This may be considered the initializer of the protocol, because
+ it is called when the connection is completed. For clients,
+ this is called once the connection to the server has been
+ established; for servers, this is called after an accept() call
+ stops blocking and a socket has been received. If you need to
+ send any greeting or initial message, do it here.
+ """
+
+connectionDone=failure.Failure(error.ConnectionDone())
+connectionDone.cleanFailure()
+
+
+class Protocol(BaseProtocol):
+ """
+ This is the base class for streaming connection-oriented protocols.
+
+ If you are going to write a new connection-oriented protocol for Twisted,
+ start here. Any protocol implementation, either client or server, should
+ be a subclass of this class.
+
+ The API is quite simple. Implement L{dataReceived} to handle both
+ event-based and synchronous input; output can be sent through the
+ 'transport' attribute, which is to be an instance that implements
+ L{twisted.internet.interfaces.ITransport}. Override C{connectionLost} to be
+ notified when the connection ends.
+
+ Some subclasses exist already to help you write common types of protocols:
+ see the L{twisted.protocols.basic} module for a few of them.
+ """
+ implements(interfaces.IProtocol, interfaces.ILoggingContext)
+
+ def logPrefix(self):
+ """
+ Return a prefix matching the class name, to identify log messages
+ related to this protocol instance.
+ """
+ return self.__class__.__name__
+
+
+ def dataReceived(self, data):
+ """Called whenever data is received.
+
+ Use this method to translate to a higher-level message. Usually, some
+ callback will be made upon the receipt of each complete protocol
+ message.
+
+ @param data: a string of indeterminate length. Please keep in mind
+ that you will probably need to buffer some data, as partial
+ (or multiple) protocol messages may be received! I recommend
+ that unit tests for protocols call through to this method with
+ differing chunk sizes, down to one byte at a time.
+ """
+
+ def connectionLost(self, reason=connectionDone):
+ """Called when the connection is shut down.
+
+ Clear any circular references here, and any external references
+ to this Protocol. The connection has been closed.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+
+class ProtocolToConsumerAdapter(components.Adapter):
+ implements(interfaces.IConsumer)
+
+ def write(self, data):
+ self.original.dataReceived(data)
+
+ def registerProducer(self, producer, streaming):
+ pass
+
+ def unregisterProducer(self):
+ pass
+
+components.registerAdapter(ProtocolToConsumerAdapter, interfaces.IProtocol,
+ interfaces.IConsumer)
+
+class ConsumerToProtocolAdapter(components.Adapter):
+ implements(interfaces.IProtocol)
+
+ def dataReceived(self, data):
+ self.original.write(data)
+
+ def connectionLost(self, reason):
+ pass
+
+ def makeConnection(self, transport):
+ pass
+
+ def connectionMade(self):
+ pass
+
+components.registerAdapter(ConsumerToProtocolAdapter, interfaces.IConsumer,
+ interfaces.IProtocol)
+
+class ProcessProtocol(BaseProtocol):
+ """
+ Base process protocol implementation which does simple dispatching for
+ stdin, stdout, and stderr file descriptors.
+ """
+ implements(interfaces.IProcessProtocol)
+
+ def childDataReceived(self, childFD, data):
+ if childFD == 1:
+ self.outReceived(data)
+ elif childFD == 2:
+ self.errReceived(data)
+
+
+ def outReceived(self, data):
+ """
+ Some data was received from stdout.
+ """
+
+
+ def errReceived(self, data):
+ """
+ Some data was received from stderr.
+ """
+
+
+ def childConnectionLost(self, childFD):
+ if childFD == 0:
+ self.inConnectionLost()
+ elif childFD == 1:
+ self.outConnectionLost()
+ elif childFD == 2:
+ self.errConnectionLost()
+
+
+ def inConnectionLost(self):
+ """
+ This will be called when stdin is closed.
+ """
+
+
+ def outConnectionLost(self):
+ """
+ This will be called when stdout is closed.
+ """
+
+
+ def errConnectionLost(self):
+ """
+ This will be called when stderr is closed.
+ """
+
+
+ def processExited(self, reason):
+ """
+ This will be called when the subprocess exits.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+
+ def processEnded(self, reason):
+ """
+ Called when the child process exits and all file descriptors
+ associated with it have been closed.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+
+
+class AbstractDatagramProtocol:
+ """
+ Abstract protocol for datagram-oriented transports, e.g. IP, ICMP, ARP, UDP.
+ """
+
+ transport = None
+ numPorts = 0
+ noisy = True
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ d['transport'] = None
+ return d
+
+ def doStart(self):
+ """Make sure startProtocol is called.
+
+ This will be called by makeConnection(), users should not call it.
+ """
+ if not self.numPorts:
+ if self.noisy:
+ log.msg("Starting protocol %s" % self)
+ self.startProtocol()
+ self.numPorts = self.numPorts + 1
+
+ def doStop(self):
+ """Make sure stopProtocol is called.
+
+ This will be called by the port, users should not call it.
+ """
+ assert self.numPorts > 0
+ self.numPorts = self.numPorts - 1
+ self.transport = None
+ if not self.numPorts:
+ if self.noisy:
+ log.msg("Stopping protocol %s" % self)
+ self.stopProtocol()
+
+ def startProtocol(self):
+ """Called when a transport is connected to this protocol.
+
+ Will only be called once, even if multiple ports are connected.
+ """
+
+ def stopProtocol(self):
+ """Called when the transport is disconnected.
+
+ Will only be called once, after all ports are disconnected.
+ """
+
+ def makeConnection(self, transport):
+ """Make a connection to a transport and a server.
+
+ This sets the 'transport' attribute of this DatagramProtocol, and calls the
+ doStart() callback.
+ """
+ assert self.transport == None
+ self.transport = transport
+ self.doStart()
+
+ def datagramReceived(self, datagram, addr):
+ """Called when a datagram is received.
+
+ @param datagram: the string received from the transport.
+ @param addr: tuple of source of datagram.
+ """
+
+
+class DatagramProtocol(AbstractDatagramProtocol):
+ """
+ Protocol for datagram-oriented transport, e.g. UDP.
+
+ @type transport: C{NoneType} or
+ L{IUDPTransport<twisted.internet.interfaces.IUDPTransport>} provider
+ @ivar transport: The transport with which this protocol is associated,
+ if it is associated with one.
+ """
+ implements(interfaces.ILoggingContext)
+
+ def logPrefix(self):
+ """
+ Return a prefix matching the class name, to identify log messages
+ related to this protocol instance.
+ """
+ return self.__class__.__name__
+
+
+ def connectionRefused(self):
+ """Called due to error from write in connected mode.
+
+ Note this is a result of ICMP message generated by *previous*
+ write.
+ """
+
+
+class ConnectedDatagramProtocol(DatagramProtocol):
+ """Protocol for connected datagram-oriented transport.
+
+ No longer necessary for UDP.
+ """
+
+ def datagramReceived(self, datagram):
+ """Called when a datagram is received.
+
+ @param datagram: the string received from the transport.
+ """
+
+ def connectionFailed(self, failure):
+ """Called if connecting failed.
+
+ Usually this will be due to a DNS lookup failure.
+ """
+
+
+
+class FileWrapper:
+ """A wrapper around a file-like object to make it behave as a Transport.
+
+ This doesn't actually stream the file to the attached protocol,
+ and is thus useful mainly as a utility for debugging protocols.
+ """
+
+ implements(interfaces.ITransport)
+
+ closed = 0
+ disconnecting = 0
+ producer = None
+ streamingProducer = 0
+
+ def __init__(self, file):
+ self.file = file
+
+ def write(self, data):
+ try:
+ self.file.write(data)
+ except:
+ self.handleException()
+ # self._checkProducer()
+
+ def _checkProducer(self):
+ # Cheating; this is called at "idle" times to allow producers to be
+ # found and dealt with
+ if self.producer:
+ self.producer.resumeProducing()
+
+ def registerProducer(self, producer, streaming):
+ """From abstract.FileDescriptor
+ """
+ self.producer = producer
+ self.streamingProducer = streaming
+ if not streaming:
+ producer.resumeProducing()
+
+ def unregisterProducer(self):
+ self.producer = None
+
+ def stopConsuming(self):
+ self.unregisterProducer()
+ self.loseConnection()
+
+ def writeSequence(self, iovec):
+ self.write("".join(iovec))
+
+ def loseConnection(self):
+ self.closed = 1
+ try:
+ self.file.close()
+ except (IOError, OSError):
+ self.handleException()
+
+ def getPeer(self):
+ # XXX: According to ITransport, this should return an IAddress!
+ return 'file', 'file'
+
+ def getHost(self):
+ # XXX: According to ITransport, this should return an IAddress!
+ return 'file'
+
+ def handleException(self):
+ pass
+
+ def resumeProducing(self):
+ # Never sends data anyways
+ pass
+
+ def pauseProducing(self):
+ # Never sends data anyways
+ pass
+
+ def stopProducing(self):
+ self.loseConnection()
+
+
+__all__ = ["Factory", "ClientFactory", "ReconnectingClientFactory", "connectionDone",
+ "Protocol", "ProcessProtocol", "FileWrapper", "ServerFactory",
+ "AbstractDatagramProtocol", "DatagramProtocol", "ConnectedDatagramProtocol",
+ "ClientCreator"]
diff --git a/twisted/internet/pyuisupport.py b/twisted/internet/pyuisupport.py
new file mode 100644
index 0000000..1e7def5
--- /dev/null
+++ b/twisted/internet/pyuisupport.py
@@ -0,0 +1,37 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+This module integrates PyUI with twisted.internet's mainloop.
+
+Maintainer: Jp Calderone
+
+See doc/examples/pyuidemo.py for example usage.
+"""
+
+# System imports
+import pyui
+
+def _guiUpdate(reactor, delay):
+ pyui.draw()
+ if pyui.update() == 0:
+ pyui.quit()
+ reactor.stop()
+ else:
+ reactor.callLater(delay, _guiUpdate, reactor, delay)
+
+
+def install(ms=10, reactor=None, args=(), kw={}):
+ """
+ Schedule PyUI's display to be updated approximately every C{ms}
+ milliseconds, and initialize PyUI with the specified arguments.
+ """
+ d = pyui.init(*args, **kw)
+
+ if reactor is None:
+ from twisted.internet import reactor
+ _guiUpdate(reactor, ms / 1000.0)
+ return d
+
+__all__ = ["install"]
diff --git a/twisted/internet/qtreactor.py b/twisted/internet/qtreactor.py
new file mode 100644
index 0000000..a548008
--- /dev/null
+++ b/twisted/internet/qtreactor.py
@@ -0,0 +1,19 @@
+# -*- test-case-name: twisted.internet.test.test_qtreactor -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+try:
+ # 'import qtreactor' would have imported this file instead of the
+ # top-level qtreactor. __import__ does the right thing
+ # (kids, don't repeat this at home)
+ install = __import__('qtreactor').install
+except ImportError:
+ from twisted.plugins.twisted_qtstub import errorMessage
+ raise ImportError(errorMessage)
+else:
+ import warnings
+ warnings.warn("Please use qtreactor instead of twisted.internet.qtreactor",
+ category=DeprecationWarning)
+
+__all__ = ['install']
+
diff --git a/twisted/internet/reactor.py b/twisted/internet/reactor.py
new file mode 100644
index 0000000..54e2ceb
--- /dev/null
+++ b/twisted/internet/reactor.py
@@ -0,0 +1,38 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The reactor is the Twisted event loop within Twisted, the loop which drives
+applications using Twisted. The reactor provides APIs for networking,
+threading, dispatching events, and more.
+
+The default reactor depends on the platform and will be installed if this
+module is imported without another reactor being explicitly installed
+beforehand. Regardless of which reactor is installed, importing this module is
+the correct way to get a reference to it.
+
+New application code should prefer to pass and accept the reactor as a
+parameter where it is needed, rather than relying on being able to import this
+module to get a reference. This simplifies unit testing and may make it easier
+to one day support multiple reactors (as a performance enhancement), though
+this is not currently possible.
+
+@see: L{IReactorCore<twisted.internet.interfaces.IReactorCore>}
+@see: L{IReactorTime<twisted.internet.interfaces.IReactorTime>}
+@see: L{IReactorProcess<twisted.internet.interfaces.IReactorProcess>}
+@see: L{IReactorTCP<twisted.internet.interfaces.IReactorTCP>}
+@see: L{IReactorSSL<twisted.internet.interfaces.IReactorSSL>}
+@see: L{IReactorUDP<twisted.internet.interfaces.IReactorUDP>}
+@see: L{IReactorMulticast<twisted.internet.interfaces.IReactorMulticast>}
+@see: L{IReactorUNIX<twisted.internet.interfaces.IReactorUNIX>}
+@see: L{IReactorUNIXDatagram<twisted.internet.interfaces.IReactorUNIXDatagram>}
+@see: L{IReactorFDSet<twisted.internet.interfaces.IReactorFDSet>}
+@see: L{IReactorThreads<twisted.internet.interfaces.IReactorThreads>}
+@see: L{IReactorArbitrary<twisted.internet.interfaces.IReactorArbitrary>}
+@see: L{IReactorPluggableResolver<twisted.internet.interfaces.IReactorPluggableResolver>}
+"""
+
+import sys
+del sys.modules['twisted.internet.reactor']
+from twisted.internet import default
+default.install()
diff --git a/twisted/internet/selectreactor.py b/twisted/internet/selectreactor.py
new file mode 100644
index 0000000..01ac28c
--- /dev/null
+++ b/twisted/internet/selectreactor.py
@@ -0,0 +1,203 @@
+# -*- test-case-name: twisted.test.test_internet -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Select reactor
+"""
+
+from time import sleep
+import sys, select, socket
+from errno import EINTR, EBADF
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IReactorFDSet
+from twisted.internet import posixbase
+from twisted.python import log
+from twisted.python.runtime import platformType
+
+
+def win32select(r, w, e, timeout=None):
+ """Win32 select wrapper."""
+ if not (r or w):
+ # windows select() exits immediately when no sockets
+ if timeout is None:
+ timeout = 0.01
+ else:
+ timeout = min(timeout, 0.001)
+ sleep(timeout)
+ return [], [], []
+ # windows doesn't process 'signals' inside select(), so we set a max
+ # time or ctrl-c will never be recognized
+ if timeout is None or timeout > 0.5:
+ timeout = 0.5
+ r, w, e = select.select(r, w, w, timeout)
+ return r, w + e, []
+
+if platformType == "win32":
+ _select = win32select
+else:
+ _select = select.select
+
+
+try:
+ from twisted.internet.win32eventreactor import _ThreadedWin32EventsMixin
+except ImportError:
+ _extraBase = object
+else:
+ _extraBase = _ThreadedWin32EventsMixin
+
+
+class SelectReactor(posixbase.PosixReactorBase, _extraBase):
+ """
+ A select() based reactor - runs on all POSIX platforms and on Win32.
+
+ @ivar _reads: A dictionary mapping L{FileDescriptor} instances to arbitrary
+ values (this is essentially a set). Keys in this dictionary will be
+ checked for read events.
+
+ @ivar _writes: A dictionary mapping L{FileDescriptor} instances to
+ arbitrary values (this is essentially a set). Keys in this dictionary
+ will be checked for writability.
+ """
+ implements(IReactorFDSet)
+
+ def __init__(self):
+ """
+ Initialize file descriptor tracking dictionaries and the base class.
+ """
+ self._reads = {}
+ self._writes = {}
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def _preenDescriptors(self):
+ log.msg("Malformed file descriptor found. Preening lists.")
+ readers = self._reads.keys()
+ writers = self._writes.keys()
+ self._reads.clear()
+ self._writes.clear()
+ for selDict, selList in ((self._reads, readers),
+ (self._writes, writers)):
+ for selectable in selList:
+ try:
+ select.select([selectable], [selectable], [selectable], 0)
+ except Exception, e:
+ log.msg("bad descriptor %s" % selectable)
+ self._disconnectSelectable(selectable, e, False)
+ else:
+ selDict[selectable] = 1
+
+
+ def doSelect(self, timeout):
+ """
+ Run one iteration of the I/O monitor loop.
+
+ This will run all selectables who had input or output readiness
+ waiting for them.
+ """
+ try:
+ r, w, ignored = _select(self._reads.keys(),
+ self._writes.keys(),
+ [], timeout)
+ except ValueError:
+ # Possibly a file descriptor has gone negative?
+ self._preenDescriptors()
+ return
+ except TypeError:
+ # Something *totally* invalid (object w/o fileno, non-integral
+ # result) was passed
+ log.err()
+ self._preenDescriptors()
+ return
+ except (select.error, socket.error, IOError), se:
+ # select(2) encountered an error, perhaps while calling the fileno()
+ # method of a socket. (Python 2.6 socket.error is an IOError
+ # subclass, but on Python 2.5 and earlier it is not.)
+ if se.args[0] in (0, 2):
+ # windows does this if it got an empty list
+ if (not self._reads) and (not self._writes):
+ return
+ else:
+ raise
+ elif se.args[0] == EINTR:
+ return
+ elif se.args[0] == EBADF:
+ self._preenDescriptors()
+ return
+ else:
+ # OK, I really don't know what's going on. Blow up.
+ raise
+
+ _drdw = self._doReadOrWrite
+ _logrun = log.callWithLogger
+ for selectables, method, fdset in ((r, "doRead", self._reads),
+ (w,"doWrite", self._writes)):
+ for selectable in selectables:
+ # if this was disconnected in another thread, kill it.
+ # ^^^^ --- what the !@#*? serious! -exarkun
+ if selectable not in fdset:
+ continue
+ # This for pausing input when we're not ready for more.
+ _logrun(selectable, _drdw, selectable, method, dict)
+
+ doIteration = doSelect
+
+ def _doReadOrWrite(self, selectable, method, dict):
+ try:
+ why = getattr(selectable, method)()
+ except:
+ why = sys.exc_info()[1]
+ log.err()
+ if why:
+ self._disconnectSelectable(selectable, why, method=="doRead")
+
+ def addReader(self, reader):
+ """
+ Add a FileDescriptor for notification of data available to read.
+ """
+ self._reads[reader] = 1
+
+ def addWriter(self, writer):
+ """
+ Add a FileDescriptor for notification of data available to write.
+ """
+ self._writes[writer] = 1
+
+ def removeReader(self, reader):
+ """
+ Remove a Selectable for notification of data available to read.
+ """
+ if reader in self._reads:
+ del self._reads[reader]
+
+ def removeWriter(self, writer):
+ """
+ Remove a Selectable for notification of data available to write.
+ """
+ if writer in self._writes:
+ del self._writes[writer]
+
+ def removeAll(self):
+ return self._removeAll(self._reads, self._writes)
+
+
+ def getReaders(self):
+ return self._reads.keys()
+
+
+ def getWriters(self):
+ return self._writes.keys()
+
+
+
+def install():
+ """Configure the twisted mainloop to be run using the select() reactor.
+ """
+ reactor = SelectReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+
+__all__ = ['install']
diff --git a/twisted/internet/serialport.py b/twisted/internet/serialport.py
new file mode 100644
index 0000000..500d8ba
--- /dev/null
+++ b/twisted/internet/serialport.py
@@ -0,0 +1,87 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Serial Port Protocol
+"""
+
+# http://twistedmatrix.com/trac/ticket/3725#comment:24
+# Apparently applications use these names even though they should
+# be imported from pyserial
+__all__ = ["serial", "PARITY_ODD", "PARITY_EVEN", "PARITY_NONE",
+ "STOPBITS_TWO", "STOPBITS_ONE", "FIVEBITS",
+ "EIGHTBITS", "SEVENBITS", "SIXBITS",
+# Name this module is actually trying to export
+ "SerialPort"]
+
+# system imports
+import os, sys
+
+# all of them require pyserial at the moment, so check that first
+import serial
+from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD
+from serial import STOPBITS_ONE, STOPBITS_TWO
+from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
+
+
+
+class BaseSerialPort:
+ """
+ Base class for Windows and POSIX serial ports.
+
+ @ivar _serialFactory: a pyserial C{serial.Serial} factory, used to create
+ the instance stored in C{self._serial}. Overrideable to enable easier
+ testing.
+
+ @ivar _serial: a pyserial C{serial.Serial} instance used to manage the
+ options on the serial port.
+ """
+
+ _serialFactory = serial.Serial
+
+
+ def setBaudRate(self, baudrate):
+ if hasattr(self._serial, "setBaudrate"):
+ self._serial.setBaudrate(baudrate)
+ else:
+ self._serial.setBaudRate(baudrate)
+
+ def inWaiting(self):
+ return self._serial.inWaiting()
+
+ def flushInput(self):
+ self._serial.flushInput()
+
+ def flushOutput(self):
+ self._serial.flushOutput()
+
+ def sendBreak(self):
+ self._serial.sendBreak()
+
+ def getDSR(self):
+ return self._serial.getDSR()
+
+ def getCD(self):
+ return self._serial.getCD()
+
+ def getRI(self):
+ return self._serial.getRI()
+
+ def getCTS(self):
+ return self._serial.getCTS()
+
+ def setDTR(self, on = 1):
+ self._serial.setDTR(on)
+
+ def setRTS(self, on = 1):
+ self._serial.setRTS(on)
+
+class SerialPort(BaseSerialPort):
+ pass
+
+# replace SerialPort with appropriate serial port
+if os.name == 'posix':
+ from twisted.internet._posixserialport import SerialPort
+elif sys.platform == 'win32':
+ from twisted.internet._win32serialport import SerialPort
diff --git a/twisted/internet/ssl.py b/twisted/internet/ssl.py
new file mode 100644
index 0000000..2141f20
--- /dev/null
+++ b/twisted/internet/ssl.py
@@ -0,0 +1,203 @@
+# -*- test-case-name: twisted.test.test_ssl -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+SSL transport. Requires PyOpenSSL (http://pyopenssl.sf.net).
+
+SSL connections require a ContextFactory so they can create SSL contexts.
+End users should only use the ContextFactory classes directly - for SSL
+connections use the reactor.connectSSL/listenSSL and so on, as documented
+in IReactorSSL.
+
+All server context factories should inherit from ContextFactory, and all
+client context factories should inherit from ClientContextFactory. At the
+moment this is not enforced, but in the future it might be.
+
+Future Plans:
+ - split module so reactor-specific classes are in a separate module
+"""
+
+# System imports
+from OpenSSL import SSL
+supported = True
+
+from zope.interface import implements, implementsOnly, implementedBy
+
+# Twisted imports
+from twisted.internet import tcp, interfaces
+
+
+class ContextFactory:
+ """A factory for SSL context objects, for server SSL connections."""
+
+ isClient = 0
+
+ def getContext(self):
+ """Return a SSL.Context object. override in subclasses."""
+ raise NotImplementedError
+
+
+class DefaultOpenSSLContextFactory(ContextFactory):
+ """
+ L{DefaultOpenSSLContextFactory} is a factory for server-side SSL context
+ objects. These objects define certain parameters related to SSL
+ handshakes and the subsequent connection.
+
+ @ivar _contextFactory: A callable which will be used to create new
+ context objects. This is typically L{SSL.Context}.
+ """
+ _context = None
+
+ def __init__(self, privateKeyFileName, certificateFileName,
+ sslmethod=SSL.SSLv23_METHOD, _contextFactory=SSL.Context):
+ """
+ @param privateKeyFileName: Name of a file containing a private key
+ @param certificateFileName: Name of a file containing a certificate
+ @param sslmethod: The SSL method to use
+ """
+ self.privateKeyFileName = privateKeyFileName
+ self.certificateFileName = certificateFileName
+ self.sslmethod = sslmethod
+ self._contextFactory = _contextFactory
+
+ # Create a context object right now. This is to force validation of
+ # the given parameters so that errors are detected earlier rather
+ # than later.
+ self.cacheContext()
+
+
+ def cacheContext(self):
+ if self._context is None:
+ ctx = self._contextFactory(self.sslmethod)
+ # Disallow SSLv2! It's insecure! SSLv3 has been around since
+ # 1996. It's time to move on.
+ ctx.set_options(SSL.OP_NO_SSLv2)
+ ctx.use_certificate_file(self.certificateFileName)
+ ctx.use_privatekey_file(self.privateKeyFileName)
+ self._context = ctx
+
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ del d['_context']
+ return d
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+
+
+ def getContext(self):
+ """
+ Return an SSL context.
+ """
+ return self._context
+
+
+class ClientContextFactory:
+ """A context factory for SSL clients."""
+
+ isClient = 1
+
+ # SSLv23_METHOD allows SSLv2, SSLv3, and TLSv1. We disable SSLv2 below,
+ # though.
+ method = SSL.SSLv23_METHOD
+
+ _contextFactory = SSL.Context
+
+ def getContext(self):
+ ctx = self._contextFactory(self.method)
+ # See comment in DefaultOpenSSLContextFactory about SSLv2.
+ ctx.set_options(SSL.OP_NO_SSLv2)
+ return ctx
+
+
+
+class Client(tcp.Client):
+ """
+ I am an SSL client.
+ """
+
+ implementsOnly(interfaces.ISSLTransport,
+ *[i for i in implementedBy(tcp.Client) if i != interfaces.ITLSTransport])
+
+ def __init__(self, host, port, bindAddress, ctxFactory, connector, reactor=None):
+ # tcp.Client.__init__ depends on self.ctxFactory being set
+ self.ctxFactory = ctxFactory
+ tcp.Client.__init__(self, host, port, bindAddress, connector, reactor)
+
+ def _connectDone(self):
+ self.startTLS(self.ctxFactory)
+ self.startWriting()
+ tcp.Client._connectDone(self)
+
+
+
+class Server(tcp.Server):
+ """
+ I am an SSL server.
+ """
+ implements(interfaces.ISSLTransport)
+
+ def __init__(self, *args, **kwargs):
+ tcp.Server.__init__(self, *args, **kwargs)
+ self.startTLS(self.server.ctxFactory)
+
+
+
+class Port(tcp.Port):
+ """
+ I am an SSL port.
+ """
+ transport = Server
+
+ _type = 'TLS'
+
+ def __init__(self, port, factory, ctxFactory, backlog=50, interface='', reactor=None):
+ tcp.Port.__init__(self, port, factory, backlog, interface, reactor)
+ self.ctxFactory = ctxFactory
+
+ # Force some parameter checking in pyOpenSSL. It's better to fail now
+ # than after we've set up the transport.
+ ctxFactory.getContext()
+
+
+ def _getLogPrefix(self, factory):
+ """
+ Override the normal prefix to include an annotation indicating this is a
+ port for TLS connections.
+ """
+ return tcp.Port._getLogPrefix(self, factory) + ' (TLS)'
+
+
+
+class Connector(tcp.Connector):
+ def __init__(self, host, port, factory, contextFactory, timeout, bindAddress, reactor=None):
+ self.contextFactory = contextFactory
+ tcp.Connector.__init__(self, host, port, factory, timeout, bindAddress, reactor)
+
+ # Force some parameter checking in pyOpenSSL. It's better to fail now
+ # than after we've set up the transport.
+ contextFactory.getContext()
+
+
+ def _makeTransport(self):
+ return Client(self.host, self.port, self.bindAddress, self.contextFactory, self, self.reactor)
+
+
+
+from twisted.internet._sslverify import DistinguishedName, DN, Certificate
+from twisted.internet._sslverify import CertificateRequest, PrivateCertificate
+from twisted.internet._sslverify import KeyPair
+from twisted.internet._sslverify import OpenSSLCertificateOptions as CertificateOptions
+
+__all__ = [
+ "ContextFactory", "DefaultOpenSSLContextFactory", "ClientContextFactory",
+
+ 'DistinguishedName', 'DN',
+ 'Certificate', 'CertificateRequest', 'PrivateCertificate',
+ 'KeyPair',
+ 'CertificateOptions',
+ ]
diff --git a/twisted/internet/stdio.py b/twisted/internet/stdio.py
new file mode 100644
index 0000000..dc0c54e
--- /dev/null
+++ b/twisted/internet/stdio.py
@@ -0,0 +1,32 @@
+# -*- test-case-name: twisted.test.test_stdio -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Standard input/out/err support.
+
+This module exposes one name, StandardIO, which is a factory that takes an
+IProtocol provider as an argument. It connects that protocol to standard input
+and output on the current process.
+
+It should work on any UNIX and also on Win32 (with some caveats: due to
+platform limitations, it will perform very poorly on Win32).
+
+Future Plans::
+
+ support for stderr, perhaps
+ Rewrite to use the reactor instead of an ad-hoc mechanism for connecting
+ protocols to transport.
+
+
+Maintainer: James Y Knight
+"""
+
+from twisted.python.runtime import platform
+
+if platform.isWindows():
+ from twisted.internet._win32stdio import StandardIO
+else:
+ from twisted.internet._posixstdio import StandardIO
+
+__all__ = ['StandardIO']
diff --git a/twisted/internet/task.py b/twisted/internet/task.py
new file mode 100644
index 0000000..99a9873
--- /dev/null
+++ b/twisted/internet/task.py
@@ -0,0 +1,789 @@
+# -*- test-case-name: twisted.test.test_task,twisted.test.test_cooperator -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Scheduling utility methods and classes.
+
+@author: Jp Calderone
+"""
+
+__metaclass__ = type
+
+import time
+
+from zope.interface import implements
+
+from twisted.python import reflect
+from twisted.python.failure import Failure
+
+from twisted.internet import base, defer
+from twisted.internet.interfaces import IReactorTime
+
+
+class LoopingCall:
+ """Call a function repeatedly.
+
+ If C{f} returns a deferred, rescheduling will not take place until the
+ deferred has fired. The result value is ignored.
+
+ @ivar f: The function to call.
+ @ivar a: A tuple of arguments to pass the function.
+ @ivar kw: A dictionary of keyword arguments to pass to the function.
+ @ivar clock: A provider of
+ L{twisted.internet.interfaces.IReactorTime}. The default is
+ L{twisted.internet.reactor}. Feel free to set this to
+ something else, but it probably ought to be set *before*
+ calling L{start}.
+
+ @type running: C{bool}
+ @ivar running: A flag which is C{True} while C{f} is scheduled to be called
+ (or is currently being called). It is set to C{True} when L{start} is
+ called and set to C{False} when L{stop} is called or if C{f} raises an
+ exception. In either case, it will be C{False} by the time the
+ C{Deferred} returned by L{start} fires its callback or errback.
+
+ @type _expectNextCallAt: C{float}
+ @ivar _expectNextCallAt: The time at which this instance most recently
+ scheduled itself to run.
+
+ @type _realLastTime: C{float}
+ @ivar _realLastTime: When counting skips, the time at which the skip
+ counter was last invoked.
+
+ @type _runAtStart: C{bool}
+ @ivar _runAtStart: A flag indicating whether the 'now' argument was passed
+ to L{LoopingCall.start}.
+ """
+
+ call = None
+ running = False
+ deferred = None
+ interval = None
+ _expectNextCallAt = 0.0
+ _runAtStart = False
+ starttime = None
+
+ def __init__(self, f, *a, **kw):
+ self.f = f
+ self.a = a
+ self.kw = kw
+ from twisted.internet import reactor
+ self.clock = reactor
+
+
+ def withCount(cls, countCallable):
+ """
+ An alternate constructor for L{LoopingCall} that makes available the
+ number of calls which should have occurred since it was last invoked.
+
+ Note that this number is an C{int} value; It represents the discrete
+ number of calls that should have been made. For example, if you are
+ using a looping call to display an animation with discrete frames, this
+ number would be the number of frames to advance.
+
+ The count is normally 1, but can be higher. For example, if the reactor
+ is blocked and takes too long to invoke the L{LoopingCall}, a Deferred
+ returned from a previous call is not fired before an interval has
+ elapsed, or if the callable itself blocks for longer than an interval,
+ preventing I{itself} from being called.
+
+ @param countCallable: A callable that will be invoked each time the
+ resulting LoopingCall is run, with an integer specifying the number
+ of calls that should have been invoked.
+
+ @type countCallable: 1-argument callable which takes an C{int}
+
+ @return: An instance of L{LoopingCall} with call counting enabled,
+ which provides the count as the first positional argument.
+
+ @rtype: L{LoopingCall}
+
+ @since: 9.0
+ """
+
+ def counter():
+ now = self.clock.seconds()
+ lastTime = self._realLastTime
+ if lastTime is None:
+ lastTime = self.starttime
+ if self._runAtStart:
+ lastTime -= self.interval
+ self._realLastTime = now
+ lastInterval = self._intervalOf(lastTime)
+ thisInterval = self._intervalOf(now)
+ count = thisInterval - lastInterval
+ return countCallable(count)
+
+ self = cls(counter)
+
+ self._realLastTime = None
+
+ return self
+
+ withCount = classmethod(withCount)
+
+
+ def _intervalOf(self, t):
+ """
+ Determine the number of intervals passed as of the given point in
+ time.
+
+ @param t: The specified time (from the start of the L{LoopingCall}) to
+ be measured in intervals
+
+ @return: The C{int} number of intervals which have passed as of the
+ given point in time.
+ """
+ elapsedTime = t - self.starttime
+ intervalNum = int(elapsedTime / self.interval)
+ return intervalNum
+
+
+ def start(self, interval, now=True):
+ """
+ Start running function every interval seconds.
+
+ @param interval: The number of seconds between calls. May be
+ less than one. Precision will depend on the underlying
+ platform, the available hardware, and the load on the system.
+
+ @param now: If True, run this call right now. Otherwise, wait
+ until the interval has elapsed before beginning.
+
+ @return: A Deferred whose callback will be invoked with
+ C{self} when C{self.stop} is called, or whose errback will be
+ invoked when the function raises an exception or returned a
+ deferred that has its errback invoked.
+ """
+ assert not self.running, ("Tried to start an already running "
+ "LoopingCall.")
+ if interval < 0:
+ raise ValueError, "interval must be >= 0"
+ self.running = True
+ d = self.deferred = defer.Deferred()
+ self.starttime = self.clock.seconds()
+ self._expectNextCallAt = self.starttime
+ self.interval = interval
+ self._runAtStart = now
+ if now:
+ self()
+ else:
+ self._reschedule()
+ return d
+
+ def stop(self):
+ """Stop running function.
+ """
+ assert self.running, ("Tried to stop a LoopingCall that was "
+ "not running.")
+ self.running = False
+ if self.call is not None:
+ self.call.cancel()
+ self.call = None
+ d, self.deferred = self.deferred, None
+ d.callback(self)
+
+ def reset(self):
+ """
+ Skip the next iteration and reset the timer.
+
+ @since: 11.1
+ """
+ assert self.running, ("Tried to reset a LoopingCall that was "
+ "not running.")
+ if self.call is not None:
+ self.call.cancel()
+ self.call = None
+ self._expectNextCallAt = self.clock.seconds()
+ self._reschedule()
+
+ def __call__(self):
+ def cb(result):
+ if self.running:
+ self._reschedule()
+ else:
+ d, self.deferred = self.deferred, None
+ d.callback(self)
+
+ def eb(failure):
+ self.running = False
+ d, self.deferred = self.deferred, None
+ d.errback(failure)
+
+ self.call = None
+ d = defer.maybeDeferred(self.f, *self.a, **self.kw)
+ d.addCallback(cb)
+ d.addErrback(eb)
+
+
+ def _reschedule(self):
+ """
+ Schedule the next iteration of this looping call.
+ """
+ if self.interval == 0:
+ self.call = self.clock.callLater(0, self)
+ return
+
+ currentTime = self.clock.seconds()
+ # Find how long is left until the interval comes around again.
+ untilNextTime = (self._expectNextCallAt - currentTime) % self.interval
+ # Make sure it is in the future, in case more than one interval worth
+ # of time passed since the previous call was made.
+ nextTime = max(
+ self._expectNextCallAt + self.interval, currentTime + untilNextTime)
+ # If the interval falls on the current time exactly, skip it and
+ # schedule the call for the next interval.
+ if nextTime == currentTime:
+ nextTime += self.interval
+ self._expectNextCallAt = nextTime
+ self.call = self.clock.callLater(nextTime - currentTime, self)
+
+
+ def __repr__(self):
+ if hasattr(self.f, 'func_name'):
+ func = self.f.func_name
+ if hasattr(self.f, 'im_class'):
+ func = self.f.im_class.__name__ + '.' + func
+ else:
+ func = reflect.safe_repr(self.f)
+
+ return 'LoopingCall<%r>(%s, *%s, **%s)' % (
+ self.interval, func, reflect.safe_repr(self.a),
+ reflect.safe_repr(self.kw))
+
+
+
+class SchedulerError(Exception):
+ """
+ The operation could not be completed because the scheduler or one of its
+ tasks was in an invalid state. This exception should not be raised
+ directly, but is a superclass of various scheduler-state-related
+ exceptions.
+ """
+
+
+
+class SchedulerStopped(SchedulerError):
+ """
+ The operation could not complete because the scheduler was stopped in
+ progress or was already stopped.
+ """
+
+
+
+class TaskFinished(SchedulerError):
+ """
+ The operation could not complete because the task was already completed,
+ stopped, encountered an error or otherwise permanently stopped running.
+ """
+
+
+
+class TaskDone(TaskFinished):
+ """
+ The operation could not complete because the task was already completed.
+ """
+
+
+
+class TaskStopped(TaskFinished):
+ """
+ The operation could not complete because the task was stopped.
+ """
+
+
+
+class TaskFailed(TaskFinished):
+ """
+ The operation could not complete because the task died with an unhandled
+ error.
+ """
+
+
+
+class NotPaused(SchedulerError):
+ """
+ This exception is raised when a task is resumed which was not previously
+ paused.
+ """
+
+
+
+class _Timer(object):
+ MAX_SLICE = 0.01
+ def __init__(self):
+ self.end = time.time() + self.MAX_SLICE
+
+
+ def __call__(self):
+ return time.time() >= self.end
+
+
+
+_EPSILON = 0.00000001
+def _defaultScheduler(x):
+ from twisted.internet import reactor
+ return reactor.callLater(_EPSILON, x)
+
+
+class CooperativeTask(object):
+ """
+ A L{CooperativeTask} is a task object inside a L{Cooperator}, which can be
+ paused, resumed, and stopped. It can also have its completion (or
+ termination) monitored.
+
+ @see: L{CooperativeTask.cooperate}
+
+ @ivar _iterator: the iterator to iterate when this L{CooperativeTask} is
+ asked to do work.
+
+ @ivar _cooperator: the L{Cooperator} that this L{CooperativeTask}
+ participates in, which is used to re-insert it upon resume.
+
+ @ivar _deferreds: the list of L{defer.Deferred}s to fire when this task
+ completes, fails, or finishes.
+
+ @type _deferreds: L{list}
+
+ @type _cooperator: L{Cooperator}
+
+ @ivar _pauseCount: the number of times that this L{CooperativeTask} has
+ been paused; if 0, it is running.
+
+ @type _pauseCount: L{int}
+
+ @ivar _completionState: The completion-state of this L{CooperativeTask}.
+ C{None} if the task is not yet completed, an instance of L{TaskStopped}
+ if C{stop} was called to stop this task early, of L{TaskFailed} if the
+ application code in the iterator raised an exception which caused it to
+ terminate, and of L{TaskDone} if it terminated normally via raising
+ L{StopIteration}.
+
+ @type _completionState: L{TaskFinished}
+ """
+
+ def __init__(self, iterator, cooperator):
+ """
+ A private constructor: to create a new L{CooperativeTask}, see
+ L{Cooperator.cooperate}.
+ """
+ self._iterator = iterator
+ self._cooperator = cooperator
+ self._deferreds = []
+ self._pauseCount = 0
+ self._completionState = None
+ self._completionResult = None
+ cooperator._addTask(self)
+
+
+ def whenDone(self):
+ """
+ Get a L{defer.Deferred} notification of when this task is complete.
+
+ @return: a L{defer.Deferred} that fires with the C{iterator} that this
+ L{CooperativeTask} was created with when the iterator has been
+ exhausted (i.e. its C{next} method has raised L{StopIteration}), or
+ fails with the exception raised by C{next} if it raises some other
+ exception.
+
+ @rtype: L{defer.Deferred}
+ """
+ d = defer.Deferred()
+ if self._completionState is None:
+ self._deferreds.append(d)
+ else:
+ d.callback(self._completionResult)
+ return d
+
+
+ def pause(self):
+ """
+ Pause this L{CooperativeTask}. Stop doing work until
+ L{CooperativeTask.resume} is called. If C{pause} is called more than
+ once, C{resume} must be called an equal number of times to resume this
+ task.
+
+ @raise TaskFinished: if this task has already finished or completed.
+ """
+ self._checkFinish()
+ self._pauseCount += 1
+ if self._pauseCount == 1:
+ self._cooperator._removeTask(self)
+
+
+ def resume(self):
+ """
+ Resume processing of a paused L{CooperativeTask}.
+
+ @raise NotPaused: if this L{CooperativeTask} is not paused.
+ """
+ if self._pauseCount == 0:
+ raise NotPaused()
+ self._pauseCount -= 1
+ if self._pauseCount == 0 and self._completionState is None:
+ self._cooperator._addTask(self)
+
+
+ def _completeWith(self, completionState, deferredResult):
+ """
+ @param completionState: a L{TaskFinished} exception or a subclass
+ thereof, indicating what exception should be raised when subsequent
+ operations are performed.
+
+ @param deferredResult: the result to fire all the deferreds with.
+ """
+ self._completionState = completionState
+ self._completionResult = deferredResult
+ if not self._pauseCount:
+ self._cooperator._removeTask(self)
+
+ # The Deferreds need to be invoked after all this is completed, because
+ # a Deferred may want to manipulate other tasks in a Cooperator. For
+ # example, if you call "stop()" on a cooperator in a callback on a
+ # Deferred returned from whenDone(), this CooperativeTask must be gone
+ # from the Cooperator by that point so that _completeWith is not
+ # invoked reentrantly; that would cause these Deferreds to blow up with
+ # an AlreadyCalledError, or the _removeTask to fail with a ValueError.
+ for d in self._deferreds:
+ d.callback(deferredResult)
+
+
+ def stop(self):
+ """
+ Stop further processing of this task.
+
+ @raise TaskFinished: if this L{CooperativeTask} has previously
+ completed, via C{stop}, completion, or failure.
+ """
+ self._checkFinish()
+ self._completeWith(TaskStopped(), Failure(TaskStopped()))
+
+
+ def _checkFinish(self):
+ """
+ If this task has been stopped, raise the appropriate subclass of
+ L{TaskFinished}.
+ """
+ if self._completionState is not None:
+ raise self._completionState
+
+
+ def _oneWorkUnit(self):
+ """
+ Perform one unit of work for this task, retrieving one item from its
+ iterator, stopping if there are no further items in the iterator, and
+ pausing if the result was a L{defer.Deferred}.
+ """
+ try:
+ result = self._iterator.next()
+ except StopIteration:
+ self._completeWith(TaskDone(), self._iterator)
+ except:
+ self._completeWith(TaskFailed(), Failure())
+ else:
+ if isinstance(result, defer.Deferred):
+ self.pause()
+ def failLater(f):
+ self._completeWith(TaskFailed(), f)
+ result.addCallbacks(lambda result: self.resume(),
+ failLater)
+
+
+
+class Cooperator(object):
+ """
+ Cooperative task scheduler.
+ """
+
+ def __init__(self,
+ terminationPredicateFactory=_Timer,
+ scheduler=_defaultScheduler,
+ started=True):
+ """
+ Create a scheduler-like object to which iterators may be added.
+
+ @param terminationPredicateFactory: A no-argument callable which will
+ be invoked at the beginning of each step and should return a
+ no-argument callable which will return True when the step should be
+ terminated. The default factory is time-based and allows iterators to
+ run for 1/100th of a second at a time.
+
+ @param scheduler: A one-argument callable which takes a no-argument
+ callable and should invoke it at some future point. This will be used
+ to schedule each step of this Cooperator.
+
+ @param started: A boolean which indicates whether iterators should be
+ stepped as soon as they are added, or if they will be queued up until
+ L{Cooperator.start} is called.
+ """
+ self._tasks = []
+ self._metarator = iter(())
+ self._terminationPredicateFactory = terminationPredicateFactory
+ self._scheduler = scheduler
+ self._delayedCall = None
+ self._stopped = False
+ self._started = started
+
+
+ def coiterate(self, iterator, doneDeferred=None):
+ """
+ Add an iterator to the list of iterators this L{Cooperator} is
+ currently running.
+
+ @param doneDeferred: If specified, this will be the Deferred used as
+ the completion deferred. It is suggested that you use the default,
+ which creates a new Deferred for you.
+
+ @return: a Deferred that will fire when the iterator finishes.
+ """
+ if doneDeferred is None:
+ doneDeferred = defer.Deferred()
+ CooperativeTask(iterator, self).whenDone().chainDeferred(doneDeferred)
+ return doneDeferred
+
+
+ def cooperate(self, iterator):
+ """
+ Start running the given iterator as a long-running cooperative task, by
+ calling next() on it as a periodic timed event.
+
+ @param iterator: the iterator to invoke.
+
+ @return: a L{CooperativeTask} object representing this task.
+ """
+ return CooperativeTask(iterator, self)
+
+
+ def _addTask(self, task):
+ """
+ Add a L{CooperativeTask} object to this L{Cooperator}.
+ """
+ if self._stopped:
+ self._tasks.append(task) # XXX silly, I know, but _completeWith
+ # does the inverse
+ task._completeWith(SchedulerStopped(), Failure(SchedulerStopped()))
+ else:
+ self._tasks.append(task)
+ self._reschedule()
+
+
+ def _removeTask(self, task):
+ """
+ Remove a L{CooperativeTask} from this L{Cooperator}.
+ """
+ self._tasks.remove(task)
+ # If no work left to do, cancel the delayed call:
+ if not self._tasks and self._delayedCall:
+ self._delayedCall.cancel()
+ self._delayedCall = None
+
+
+ def _tasksWhileNotStopped(self):
+ """
+ Yield all L{CooperativeTask} objects in a loop as long as this
+ L{Cooperator}'s termination condition has not been met.
+ """
+ terminator = self._terminationPredicateFactory()
+ while self._tasks:
+ for t in self._metarator:
+ yield t
+ if terminator():
+ return
+ self._metarator = iter(self._tasks)
+
+
+ def _tick(self):
+ """
+ Run one scheduler tick.
+ """
+ self._delayedCall = None
+ for taskObj in self._tasksWhileNotStopped():
+ taskObj._oneWorkUnit()
+ self._reschedule()
+
+
+ _mustScheduleOnStart = False
+ def _reschedule(self):
+ if not self._started:
+ self._mustScheduleOnStart = True
+ return
+ if self._delayedCall is None and self._tasks:
+ self._delayedCall = self._scheduler(self._tick)
+
+
+ def start(self):
+ """
+ Begin scheduling steps.
+ """
+ self._stopped = False
+ self._started = True
+ if self._mustScheduleOnStart:
+ del self._mustScheduleOnStart
+ self._reschedule()
+
+
+ def stop(self):
+ """
+ Stop scheduling steps. Errback the completion Deferreds of all
+ iterators which have been added and forget about them.
+ """
+ self._stopped = True
+ for taskObj in self._tasks:
+ taskObj._completeWith(SchedulerStopped(),
+ Failure(SchedulerStopped()))
+ self._tasks = []
+ if self._delayedCall is not None:
+ self._delayedCall.cancel()
+ self._delayedCall = None
+
+
+
+_theCooperator = Cooperator()
+
+def coiterate(iterator):
+ """
+ Cooperatively iterate over the given iterator, dividing runtime between it
+ and all other iterators which have been passed to this function and not yet
+ exhausted.
+ """
+ return _theCooperator.coiterate(iterator)
+
+
+
+def cooperate(iterator):
+ """
+ Start running the given iterator as a long-running cooperative task, by
+ calling next() on it as a periodic timed event.
+
+ @param iterator: the iterator to invoke.
+
+ @return: a L{CooperativeTask} object representing this task.
+ """
+ return _theCooperator.cooperate(iterator)
+
+
+
+class Clock:
+ """
+ Provide a deterministic, easily-controlled implementation of
+ L{IReactorTime.callLater}. This is commonly useful for writing
+ deterministic unit tests for code which schedules events using this API.
+ """
+ implements(IReactorTime)
+
+ rightNow = 0.0
+
+ def __init__(self):
+ self.calls = []
+
+
+ def seconds(self):
+ """
+ Pretend to be time.time(). This is used internally when an operation
+ such as L{IDelayedCall.reset} needs to determine a a time value
+ relative to the current time.
+
+ @rtype: C{float}
+ @return: The time which should be considered the current time.
+ """
+ return self.rightNow
+
+
+ def _sortCalls(self):
+ """
+ Sort the pending calls according to the time they are scheduled.
+ """
+ self.calls.sort(lambda a, b: cmp(a.getTime(), b.getTime()))
+
+
+ def callLater(self, when, what, *a, **kw):
+ """
+ See L{twisted.internet.interfaces.IReactorTime.callLater}.
+ """
+ dc = base.DelayedCall(self.seconds() + when,
+ what, a, kw,
+ self.calls.remove,
+ lambda c: None,
+ self.seconds)
+ self.calls.append(dc)
+ self._sortCalls()
+ return dc
+
+
+ def getDelayedCalls(self):
+ """
+ See L{twisted.internet.interfaces.IReactorTime.getDelayedCalls}
+ """
+ return self.calls
+
+
+ def advance(self, amount):
+ """
+ Move time on this clock forward by the given amount and run whatever
+ pending calls should be run.
+
+ @type amount: C{float}
+ @param amount: The number of seconds which to advance this clock's
+ time.
+ """
+ self.rightNow += amount
+ self._sortCalls()
+ while self.calls and self.calls[0].getTime() <= self.seconds():
+ call = self.calls.pop(0)
+ call.called = 1
+ call.func(*call.args, **call.kw)
+ self._sortCalls()
+
+
+ def pump(self, timings):
+ """
+ Advance incrementally by the given set of times.
+
+ @type timings: iterable of C{float}
+ """
+ for amount in timings:
+ self.advance(amount)
+
+
+
+def deferLater(clock, delay, callable, *args, **kw):
+ """
+ Call the given function after a certain period of time has passed.
+
+ @type clock: L{IReactorTime} provider
+ @param clock: The object which will be used to schedule the delayed
+ call.
+
+ @type delay: C{float} or C{int}
+ @param delay: The number of seconds to wait before calling the function.
+
+ @param callable: The object to call after the delay.
+
+ @param *args: The positional arguments to pass to C{callable}.
+
+ @param **kw: The keyword arguments to pass to C{callable}.
+
+ @rtype: L{defer.Deferred}
+
+ @return: A deferred that fires with the result of the callable when the
+ specified time has elapsed.
+ """
+ def deferLaterCancel(deferred):
+ delayedCall.cancel()
+ d = defer.Deferred(deferLaterCancel)
+ d.addCallback(lambda ignored: callable(*args, **kw))
+ delayedCall = clock.callLater(delay, d.callback, None)
+ return d
+
+
+
+__all__ = [
+ 'LoopingCall',
+
+ 'Clock',
+
+ 'SchedulerStopped', 'Cooperator', 'coiterate',
+
+ 'deferLater',
+ ]
diff --git a/twisted/internet/tcp.py b/twisted/internet/tcp.py
new file mode 100644
index 0000000..6c2cbad
--- /dev/null
+++ b/twisted/internet/tcp.py
@@ -0,0 +1,1130 @@
+# -*- test-case-name: twisted.test.test_tcp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Various asynchronous TCP/IP classes.
+
+End users shouldn't use this module directly - use the reactor APIs instead.
+"""
+
+
+# System Imports
+import types
+import socket
+import sys
+import operator
+import struct
+
+from zope.interface import implements
+
+from twisted.python.runtime import platformType
+from twisted.python import versions, deprecate
+
+try:
+ # Try to get the memory BIO based startTLS implementation, available since
+ # pyOpenSSL 0.10
+ from twisted.internet._newtls import (
+ ConnectionMixin as _TLSConnectionMixin,
+ ClientMixin as _TLSClientMixin,
+ ServerMixin as _TLSServerMixin)
+except ImportError:
+ try:
+ # Try to get the socket BIO based startTLS implementation, available in
+ # all pyOpenSSL versions
+ from twisted.internet._oldtls import (
+ ConnectionMixin as _TLSConnectionMixin,
+ ClientMixin as _TLSClientMixin,
+ ServerMixin as _TLSServerMixin)
+ except ImportError:
+ # There is no version of startTLS available
+ class _TLSConnectionMixin(object):
+ TLS = False
+ class _TLSClientMixin(object):
+ pass
+ class _TLSServerMixin(object):
+ pass
+
+if platformType == 'win32':
+ # no such thing as WSAEPERM or error code 10001 according to winsock.h or MSDN
+ EPERM = object()
+ from errno import WSAEINVAL as EINVAL
+ from errno import WSAEWOULDBLOCK as EWOULDBLOCK
+ from errno import WSAEINPROGRESS as EINPROGRESS
+ from errno import WSAEALREADY as EALREADY
+ from errno import WSAECONNRESET as ECONNRESET
+ from errno import WSAEISCONN as EISCONN
+ from errno import WSAENOTCONN as ENOTCONN
+ from errno import WSAEINTR as EINTR
+ from errno import WSAENOBUFS as ENOBUFS
+ from errno import WSAEMFILE as EMFILE
+ # No such thing as WSAENFILE, either.
+ ENFILE = object()
+ # Nor ENOMEM
+ ENOMEM = object()
+ EAGAIN = EWOULDBLOCK
+ from errno import WSAECONNRESET as ECONNABORTED
+
+ from twisted.python.win32 import formatError as strerror
+else:
+ from errno import EPERM
+ from errno import EINVAL
+ from errno import EWOULDBLOCK
+ from errno import EINPROGRESS
+ from errno import EALREADY
+ from errno import ECONNRESET
+ from errno import EISCONN
+ from errno import ENOTCONN
+ from errno import EINTR
+ from errno import ENOBUFS
+ from errno import EMFILE
+ from errno import ENFILE
+ from errno import ENOMEM
+ from errno import EAGAIN
+ from errno import ECONNABORTED
+
+ from os import strerror
+
+
+from errno import errorcode
+
+# Twisted Imports
+from twisted.internet import base, address, fdesc
+from twisted.internet.task import deferLater
+from twisted.python import log, failure, reflect
+from twisted.python.util import unsignedID, untilConcludes
+from twisted.internet.error import CannotListenError
+from twisted.internet import abstract, main, interfaces, error
+
+# Not all platforms have, or support, this flag.
+_AI_NUMERICSERV = getattr(socket, "AI_NUMERICSERV", 0)
+
+
+
+class _SocketCloser(object):
+ _socketShutdownMethod = 'shutdown'
+
+ def _closeSocket(self, orderly):
+ # The call to shutdown() before close() isn't really necessary, because
+ # we set FD_CLOEXEC now, which will ensure this is the only process
+ # holding the FD, thus ensuring close() really will shutdown the TCP
+ # socket. However, do it anyways, just to be safe.
+ skt = self.socket
+ try:
+ if orderly:
+ if self._socketShutdownMethod is not None:
+ getattr(skt, self._socketShutdownMethod)(2)
+ else:
+ # Set SO_LINGER to 1,0 which, by convention, causes a
+ # connection reset to be sent when close is called,
+ # instead of the standard FIN shutdown sequence.
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
+ struct.pack("ii", 1, 0))
+
+ except socket.error:
+ pass
+ try:
+ skt.close()
+ except socket.error:
+ pass
+
+
+
+class _AbortingMixin(object):
+ """
+ Common implementation of C{abortConnection}.
+
+ @ivar _aborting: Set to C{True} when C{abortConnection} is called.
+ @type _aborting: C{bool}
+ """
+ _aborting = False
+
+ def abortConnection(self):
+ """
+ Aborts the connection immediately, dropping any buffered data.
+
+ @since: 11.1
+ """
+ if self.disconnected or self._aborting:
+ return
+ self._aborting = True
+ self.stopReading()
+ self.stopWriting()
+ self.doRead = lambda *args, **kwargs: None
+ self.doWrite = lambda *args, **kwargs: None
+ self.reactor.callLater(0, self.connectionLost,
+ failure.Failure(error.ConnectionAborted()))
+
+
+
+class Connection(_TLSConnectionMixin, abstract.FileDescriptor, _SocketCloser,
+ _AbortingMixin):
+ """
+ Superclass of all socket-based FileDescriptors.
+
+ This is an abstract superclass of all objects which represent a TCP/IP
+ connection based socket.
+
+ @ivar logstr: prefix used when logging events related to this connection.
+ @type logstr: C{str}
+ """
+ implements(interfaces.ITCPTransport, interfaces.ISystemHandle)
+
+
+ def __init__(self, skt, protocol, reactor=None):
+ abstract.FileDescriptor.__init__(self, reactor=reactor)
+ self.socket = skt
+ self.socket.setblocking(0)
+ self.fileno = skt.fileno
+ self.protocol = protocol
+
+
+ def getHandle(self):
+ """Return the socket for this connection."""
+ return self.socket
+
+
+ def doRead(self):
+ """Calls self.protocol.dataReceived with all available data.
+
+ This reads up to self.bufferSize bytes of data from its socket, then
+ calls self.dataReceived(data) to process it. If the connection is not
+ lost through an error in the physical recv(), this function will return
+ the result of the dataReceived call.
+ """
+ try:
+ data = self.socket.recv(self.bufferSize)
+ except socket.error, se:
+ if se.args[0] == EWOULDBLOCK:
+ return
+ else:
+ return main.CONNECTION_LOST
+
+ return self._dataReceived(data)
+
+
+ def _dataReceived(self, data):
+ if not data:
+ return main.CONNECTION_DONE
+ rval = self.protocol.dataReceived(data)
+ if rval is not None:
+ offender = self.protocol.dataReceived
+ warningFormat = (
+ 'Returning a value other than None from %(fqpn)s is '
+ 'deprecated since %(version)s.')
+ warningString = deprecate.getDeprecationWarningString(
+ offender, versions.Version('Twisted', 11, 0, 0),
+ format=warningFormat)
+ deprecate.warnAboutFunction(offender, warningString)
+ return rval
+
+
+ def writeSomeData(self, data):
+ """
+ Write as much as possible of the given data to this TCP connection.
+
+ This sends up to C{self.SEND_LIMIT} bytes from C{data}. If the
+ connection is lost, an exception is returned. Otherwise, the number
+ of bytes successfully written is returned.
+ """
+ # Limit length of buffer to try to send, because some OSes are too
+ # stupid to do so themselves (ahem windows)
+ limitedData = buffer(data, 0, self.SEND_LIMIT)
+
+ try:
+ return untilConcludes(self.socket.send, limitedData)
+ except socket.error, se:
+ if se.args[0] in (EWOULDBLOCK, ENOBUFS):
+ return 0
+ else:
+ return main.CONNECTION_LOST
+
+
+ def _closeWriteConnection(self):
+ try:
+ getattr(self.socket, self._socketShutdownMethod)(1)
+ except socket.error:
+ pass
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.writeConnectionLost()
+ except:
+ f = failure.Failure()
+ log.err()
+ self.connectionLost(f)
+
+
+ def readConnectionLost(self, reason):
+ p = interfaces.IHalfCloseableProtocol(self.protocol, None)
+ if p:
+ try:
+ p.readConnectionLost()
+ except:
+ log.err()
+ self.connectionLost(failure.Failure())
+ else:
+ self.connectionLost(reason)
+
+
+
+ def connectionLost(self, reason):
+ """See abstract.FileDescriptor.connectionLost().
+ """
+ # Make sure we're not called twice, which can happen e.g. if
+ # abortConnection() is called from protocol's dataReceived and then
+ # code immediately after throws an exception that reaches the
+ # reactor. We can't rely on "disconnected" attribute for this check
+ # since twisted.internet._oldtls does evil things to it:
+ if not hasattr(self, "socket"):
+ return
+ abstract.FileDescriptor.connectionLost(self, reason)
+ self._closeSocket(not reason.check(error.ConnectionAborted))
+ protocol = self.protocol
+ del self.protocol
+ del self.socket
+ del self.fileno
+ protocol.connectionLost(reason)
+
+
+ logstr = "Uninitialized"
+
+ def logPrefix(self):
+ """Return the prefix to log with when I own the logging thread.
+ """
+ return self.logstr
+
+ def getTcpNoDelay(self):
+ return operator.truth(self.socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
+
+ def setTcpNoDelay(self, enabled):
+ self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, enabled)
+
+ def getTcpKeepAlive(self):
+ return operator.truth(self.socket.getsockopt(socket.SOL_SOCKET,
+ socket.SO_KEEPALIVE))
+
+ def setTcpKeepAlive(self, enabled):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, enabled)
+
+
+
+
+class _BaseBaseClient(object):
+ """
+ Code shared with other (non-POSIX) reactors for management of general
+ outgoing connections.
+
+ Requirements upon subclasses are documented as instance variables rather
+ than abstract methods, in order to avoid MRO confusion, since this base is
+ mixed in to unfortunately weird and distinctive multiple-inheritance
+ hierarchies and many of these attributes are provided by peer classes
+ rather than descendant classes in those hierarchies.
+
+ @ivar addressFamily: The address family constant (C{socket.AF_INET},
+ C{socket.AF_INET6}, C{socket.AF_UNIX}) of the underlying socket of this
+ client connection.
+ @type addressFamily: C{int}
+
+ @ivar socketType: The socket type constant (C{socket.SOCK_STREAM} or
+ C{socket.SOCK_DGRAM}) of the underlying socket.
+ @type socketType: C{int}
+
+ @ivar _requiresResolution: A flag indicating whether the address of this
+ client will require name resolution. C{True} if the hostname of said
+ address indicates a name that must be resolved by hostname lookup,
+ C{False} if it indicates an IP address literal.
+ @type _requiresResolution: C{bool}
+
+ @cvar _commonConnection: Subclasses must provide this attribute, which
+ indicates the L{Connection}-alike class to invoke C{__init__} and
+ C{connectionLost} on.
+ @type _commonConnection: C{type}
+
+ @ivar _stopReadingAndWriting: Subclasses must implement in order to remove
+ this transport from its reactor's notifications in response to a
+ terminated connection attempt.
+ @type _stopReadingAndWriting: 0-argument callable returning C{None}
+
+ @ivar _closeSocket: Subclasses must implement in order to close the socket
+ in response to a terminated connection attempt.
+ @type _closeSocket: 1-argument callable; see L{_SocketCloser._closeSocket}
+
+ @ivar _collectSocketDetails: Clean up references to the attached socket in
+ its underlying OS resource (such as a file descriptor or file handle),
+ as part of post connection-failure cleanup.
+ @type _collectSocketDetails: 0-argument callable returning C{None}.
+
+ @ivar reactor: The class pointed to by C{_commonConnection} should set this
+ attribute in its constructor.
+ @type reactor: L{twisted.internet.interfaces.IReactorTime},
+ L{twisted.internet.interfaces.IReactorCore},
+ L{twisted.internet.interfaces.IReactorFDSet}
+ """
+
+ addressFamily = socket.AF_INET
+ socketType = socket.SOCK_STREAM
+
+ def _finishInit(self, whenDone, skt, error, reactor):
+ """
+ Called by subclasses to continue to the stage of initialization where
+ the socket connect attempt is made.
+
+ @param whenDone: A 0-argument callable to invoke once the connection is
+ set up. This is C{None} if the connection could not be prepared
+ due to a previous error.
+
+ @param skt: The socket object to use to perform the connection.
+ @type skt: C{socket._socketobject}
+
+ @param error: The error to fail the connection with.
+
+ @param reactor: The reactor to use for this client.
+ @type reactor: L{twisted.internet.interfaces.IReactorTime}
+ """
+ if whenDone:
+ self._commonConnection.__init__(self, skt, None, reactor)
+ reactor.callLater(0, whenDone)
+ else:
+ reactor.callLater(0, self.failIfNotConnected, error)
+
+
+ def resolveAddress(self):
+ """
+ Resolve the name that was passed to this L{_BaseBaseClient}, if
+ necessary, and then move on to attempting the connection once an
+ address has been determined. (The connection will be attempted
+ immediately within this function if either name resolution can be
+ synchronous or the address was an IP address literal.)
+
+ @note: You don't want to call this method from outside, as it won't do
+ anything useful; it's just part of the connection bootstrapping
+ process. Also, although this method is on L{_BaseBaseClient} for
+ historical reasons, it's not used anywhere except for L{Client}
+ itself.
+
+ @return: C{None}
+ """
+ if self._requiresResolution:
+ d = self.reactor.resolve(self.addr[0])
+ d.addCallback(lambda n: (n,) + self.addr[1:])
+ d.addCallbacks(self._setRealAddress, self.failIfNotConnected)
+ else:
+ self._setRealAddress(self.addr)
+
+
+ def _setRealAddress(self, address):
+ """
+ Set the resolved address of this L{_BaseBaseClient} and initiate the
+ connection attempt.
+
+ @param address: Depending on whether this is an IPv4 or IPv6 connection
+ attempt, a 2-tuple of C{(host, port)} or a 4-tuple of C{(host,
+ port, flow, scope)}. At this point it is a fully resolved address,
+ and the 'host' portion will always be an IP address, not a DNS
+ name.
+ """
+ self.realAddress = address
+ self.doConnect()
+
+
+ def failIfNotConnected(self, err):
+ """
+ Generic method called when the attemps to connect failed. It basically
+ cleans everything it can: call connectionFailed, stop read and write,
+ delete socket related members.
+ """
+ if (self.connected or self.disconnected or
+ not hasattr(self, "connector")):
+ return
+
+ self._stopReadingAndWriting()
+ try:
+ self._closeSocket(True)
+ except AttributeError:
+ pass
+ else:
+ self._collectSocketDetails()
+ self.connector.connectionFailed(failure.Failure(err))
+ del self.connector
+
+
+ def stopConnecting(self):
+ """
+ If a connection attempt is still outstanding (i.e. no connection is
+ yet established), immediately stop attempting to connect.
+ """
+ self.failIfNotConnected(error.UserError())
+
+
+ def connectionLost(self, reason):
+ """
+ Invoked by lower-level logic when it's time to clean the socket up.
+ Depending on the state of the connection, either inform the attached
+ L{Connector} that the connection attempt has failed, or inform the
+ connected L{IProtocol} that the established connection has been lost.
+
+ @param reason: the reason that the connection was terminated
+ @type reason: L{Failure}
+ """
+ if not self.connected:
+ self.failIfNotConnected(error.ConnectError(string=reason))
+ else:
+ self._commonConnection.connectionLost(self, reason)
+ self.connector.connectionLost(reason)
+
+
+
+class BaseClient(_BaseBaseClient, _TLSClientMixin, Connection):
+ """
+ A base class for client TCP (and similiar) sockets.
+
+ @ivar realAddress: The address object that will be used for socket.connect;
+ this address is an address tuple (the number of elements dependent upon
+ the address family) which does not contain any names which need to be
+ resolved.
+ @type realAddress: C{tuple}
+
+ @ivar _base: L{Connection}, which is the base class of this class which has
+ all of the useful file descriptor methods. This is used by
+ L{_TLSServerMixin} to call the right methods to directly manipulate the
+ transport, as is necessary for writing TLS-encrypted bytes (whereas
+ those methods on L{Server} will go through another layer of TLS if it
+ has been enabled).
+ """
+
+ _base = Connection
+ _commonConnection = Connection
+
+ def _stopReadingAndWriting(self):
+ """
+ Implement the POSIX-ish (i.e.
+ L{twisted.internet.interfaces.IReactorFDSet}) method of detaching this
+ socket from the reactor for L{_BaseBaseClient}.
+ """
+ if hasattr(self, "reactor"):
+ # this doesn't happen if we failed in __init__
+ self.stopReading()
+ self.stopWriting()
+
+
+ def _collectSocketDetails(self):
+ """
+ Clean up references to the socket and its file descriptor.
+
+ @see: L{_BaseBaseClient}
+ """
+ del self.socket, self.fileno
+
+
+ def createInternetSocket(self):
+ """(internal) Create a non-blocking socket using
+ self.addressFamily, self.socketType.
+ """
+ s = socket.socket(self.addressFamily, self.socketType)
+ s.setblocking(0)
+ fdesc._setCloseOnExec(s.fileno())
+ return s
+
+
+ def doConnect(self):
+ """
+ Initiate the outgoing connection attempt.
+
+ @note: Applications do not need to call this method; it will be invoked
+ internally as part of L{IReactorTCP.connectTCP}.
+ """
+ self.doWrite = self.doConnect
+ self.doRead = self.doConnect
+ if not hasattr(self, "connector"):
+ # this happens when connection failed but doConnect
+ # was scheduled via a callLater in self._finishInit
+ return
+
+ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+ if err:
+ self.failIfNotConnected(error.getConnectError((err, strerror(err))))
+ return
+
+ # doConnect gets called twice. The first time we actually need to
+ # start the connection attempt. The second time we don't really
+ # want to (SO_ERROR above will have taken care of any errors, and if
+ # it reported none, the mere fact that doConnect was called again is
+ # sufficient to indicate that the connection has succeeded), but it
+ # is not /particularly/ detrimental to do so. This should get
+ # cleaned up some day, though.
+ try:
+ connectResult = self.socket.connect_ex(self.realAddress)
+ except socket.error, se:
+ connectResult = se.args[0]
+ if connectResult:
+ if connectResult == EISCONN:
+ pass
+ # on Windows EINVAL means sometimes that we should keep trying:
+ # http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winsock/winsock/connect_2.asp
+ elif ((connectResult in (EWOULDBLOCK, EINPROGRESS, EALREADY)) or
+ (connectResult == EINVAL and platformType == "win32")):
+ self.startReading()
+ self.startWriting()
+ return
+ else:
+ self.failIfNotConnected(error.getConnectError((connectResult, strerror(connectResult))))
+ return
+
+ # If I have reached this point without raising or returning, that means
+ # that the socket is connected.
+ del self.doWrite
+ del self.doRead
+ # we first stop and then start, to reset any references to the old doRead
+ self.stopReading()
+ self.stopWriting()
+ self._connectDone()
+
+
+ def _connectDone(self):
+ """
+ This is a hook for when a connection attempt has succeeded.
+
+ Here, we build the protocol from the
+ L{twisted.internet.protocol.ClientFactory} that was passed in, compute
+ a log string, begin reading so as to send traffic to the newly built
+ protocol, and finally hook up the protocol itself.
+
+ This hook is overridden by L{ssl.Client} to initiate the TLS protocol.
+ """
+ self.protocol = self.connector.buildProtocol(self.getPeer())
+ self.connected = 1
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = "%s,client" % logPrefix
+ self.startReading()
+ self.protocol.makeConnection(self)
+
+
+
+_NUMERIC_ONLY = socket.AI_NUMERICHOST | _AI_NUMERICSERV
+
+def _resolveIPv6(ip, port):
+ """
+ Resolve an IPv6 literal into an IPv6 address.
+
+ This is necessary to resolve any embedded scope identifiers to the relevant
+ C{sin6_scope_id} for use with C{socket.connect()}, C{socket.listen()}, or
+ C{socket.bind()}; see U{RFC 3493 <https://tools.ietf.org/html/rfc3493>} for
+ more information.
+
+ @param ip: An IPv6 address literal.
+ @type ip: C{str}
+
+ @param port: A port number.
+ @type port: C{int}
+
+ @return: a 4-tuple of C{(host, port, flow, scope)}, suitable for use as an
+ IPv6 address.
+
+ @raise socket.gaierror: if either the IP or port is not numeric as it
+ should be.
+ """
+ return socket.getaddrinfo(ip, port, 0, 0, 0, _NUMERIC_ONLY)[0][4]
+
+
+
+class _BaseTCPClient(object):
+ """
+ Code shared with other (non-POSIX) reactors for management of outgoing TCP
+ connections (both TCPv4 and TCPv6).
+
+ @note: In order to be functional, this class must be mixed into the same
+ hierarchy as L{_BaseBaseClient}. It would subclass L{_BaseBaseClient}
+ directly, but the class hierarchy here is divided in strange ways out
+ of the need to share code along multiple axes; specifically, with the
+ IOCP reactor and also with UNIX clients in other reactors.
+
+ @ivar _addressType: The Twisted _IPAddress implementation for this client
+ @type _addressType: L{IPv4Address} or L{IPv6Address}
+
+ @ivar connector: The L{Connector} which is driving this L{_BaseTCPClient}'s
+ connection attempt.
+
+ @ivar addr: The address that this socket will be connecting to.
+ @type addr: If IPv4, a 2-C{tuple} of C{(str host, int port)}. If IPv6, a
+ 4-C{tuple} of (C{str host, int port, int ignored, int scope}).
+
+ @ivar createInternetSocket: Subclasses must implement this as a method to
+ create a python socket object of the appropriate address family and
+ socket type.
+ @type createInternetSocket: 0-argument callable returning
+ C{socket._socketobject}.
+ """
+
+ _addressType = address.IPv4Address
+
+ def __init__(self, host, port, bindAddress, connector, reactor=None):
+ # BaseClient.__init__ is invoked later
+ self.connector = connector
+ self.addr = (host, port)
+
+ whenDone = self.resolveAddress
+ err = None
+ skt = None
+
+ if abstract.isIPAddress(host):
+ self._requiresResolution = False
+ elif abstract.isIPv6Address(host):
+ self._requiresResolution = False
+ self.addr = _resolveIPv6(host, port)
+ self.addressFamily = socket.AF_INET6
+ self._addressType = address.IPv6Address
+ else:
+ self._requiresResolution = True
+ try:
+ skt = self.createInternetSocket()
+ except socket.error, se:
+ err = error.ConnectBindError(se.args[0], se.args[1])
+ whenDone = None
+ if whenDone and bindAddress is not None:
+ try:
+ if abstract.isIPv6Address(bindAddress[0]):
+ bindinfo = _resolveIPv6(*bindAddress)
+ else:
+ bindinfo = bindAddress
+ skt.bind(bindinfo)
+ except socket.error, se:
+ err = error.ConnectBindError(se.args[0], se.args[1])
+ whenDone = None
+ self._finishInit(whenDone, skt, err, reactor)
+
+
+ def getHost(self):
+ """
+ Returns an L{IPv4Address} or L{IPv6Address}.
+
+ This indicates the address from which I am connecting.
+ """
+ return self._addressType('TCP', *self.socket.getsockname()[:2])
+
+
+ def getPeer(self):
+ """
+ Returns an L{IPv4Address} or L{IPv6Address}.
+
+ This indicates the address that I am connected to.
+ """
+ # an ipv6 realAddress has more than two elements, but the IPv6Address
+ # constructor still only takes two.
+ return self._addressType('TCP', *self.realAddress[:2])
+
+
+ def __repr__(self):
+ s = '<%s to %s at %x>' % (self.__class__, self.addr, unsignedID(self))
+ return s
+
+
+
+class Client(_BaseTCPClient, BaseClient):
+ """
+ A transport for a TCP protocol; either TCPv4 or TCPv6.
+
+ Do not create these directly; use L{IReactorTCP.connectTCP}.
+ """
+
+
+
+class Server(_TLSServerMixin, Connection):
+ """
+ Serverside socket-stream connection class.
+
+ This is a serverside network connection transport; a socket which came from
+ an accept() on a server.
+
+ @ivar _base: L{Connection}, which is the base class of this class which has
+ all of the useful file descriptor methods. This is used by
+ L{_TLSServerMixin} to call the right methods to directly manipulate the
+ transport, as is necessary for writing TLS-encrypted bytes (whereas
+ those methods on L{Server} will go through another layer of TLS if it
+ has been enabled).
+ """
+ _base = Connection
+
+ _addressType = address.IPv4Address
+
+ def __init__(self, sock, protocol, client, server, sessionno, reactor):
+ """
+ Server(sock, protocol, client, server, sessionno)
+
+ Initialize it with a socket, a protocol, a descriptor for my peer (a
+ tuple of host, port describing the other end of the connection), an
+ instance of Port, and a session number.
+ """
+ Connection.__init__(self, sock, protocol, reactor)
+ if len(client) != 2:
+ self._addressType = address.IPv6Address
+ self.server = server
+ self.client = client
+ self.sessionno = sessionno
+ self.hostname = client[0]
+
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = "%s,%s,%s" % (logPrefix,
+ sessionno,
+ self.hostname)
+ self.repstr = "<%s #%s on %s>" % (self.protocol.__class__.__name__,
+ self.sessionno,
+ self.server._realPortNumber)
+ self.startReading()
+ self.connected = 1
+
+ def __repr__(self):
+ """A string representation of this connection.
+ """
+ return self.repstr
+
+
+ def getHost(self):
+ """
+ Returns an L{IPv4Address} or L{IPv6Address}.
+
+ This indicates the server's address.
+ """
+ host, port = self.socket.getsockname()[:2]
+ return self._addressType('TCP', host, port)
+
+
+ def getPeer(self):
+ """
+ Returns an L{IPv4Address} or L{IPv6Address}.
+
+ This indicates the client's address.
+ """
+ return self._addressType('TCP', *self.client[:2])
+
+
+
+class Port(base.BasePort, _SocketCloser):
+ """
+ A TCP server port, listening for connections.
+
+ When a connection is accepted, this will call a factory's buildProtocol
+ with the incoming address as an argument, according to the specification
+ described in L{twisted.internet.interfaces.IProtocolFactory}.
+
+ If you wish to change the sort of transport that will be used, the
+ C{transport} attribute will be called with the signature expected for
+ C{Server.__init__}, so it can be replaced.
+
+ @ivar deferred: a deferred created when L{stopListening} is called, and
+ that will fire when connection is lost. This is not to be used it
+ directly: prefer the deferred returned by L{stopListening} instead.
+ @type deferred: L{defer.Deferred}
+
+ @ivar disconnecting: flag indicating that the L{stopListening} method has
+ been called and that no connections should be accepted anymore.
+ @type disconnecting: C{bool}
+
+ @ivar connected: flag set once the listen has successfully been called on
+ the socket.
+ @type connected: C{bool}
+
+ @ivar _type: A string describing the connections which will be created by
+ this port. Normally this is C{"TCP"}, since this is a TCP port, but
+ when the TLS implementation re-uses this class it overrides the value
+ with C{"TLS"}. Only used for logging.
+
+ @ivar _preexistingSocket: If not C{None}, a L{socket.socket} instance which
+ was created and initialized outside of the reactor and will be used to
+ listen for connections (instead of a new socket being created by this
+ L{Port}).
+ """
+
+ implements(interfaces.IListeningPort)
+
+ socketType = socket.SOCK_STREAM
+
+ transport = Server
+ sessionno = 0
+ interface = ''
+ backlog = 50
+
+ _type = 'TCP'
+
+ # Actual port number being listened on, only set to a non-None
+ # value when we are actually listening.
+ _realPortNumber = None
+
+ # An externally initialized socket that we will use, rather than creating
+ # our own.
+ _preexistingSocket = None
+
+ addressFamily = socket.AF_INET
+ _addressType = address.IPv4Address
+
+ def __init__(self, port, factory, backlog=50, interface='', reactor=None):
+ """Initialize with a numeric port to listen on.
+ """
+ base.BasePort.__init__(self, reactor=reactor)
+ self.port = port
+ self.factory = factory
+ self.backlog = backlog
+ if abstract.isIPv6Address(interface):
+ self.addressFamily = socket.AF_INET6
+ self._addressType = address.IPv6Address
+ self.interface = interface
+
+
+ @classmethod
+ def _fromListeningDescriptor(cls, reactor, fd, addressFamily, factory):
+ """
+ Create a new L{Port} based on an existing listening I{SOCK_STREAM}
+ I{AF_INET} socket.
+
+ Arguments are the same as to L{Port.__init__}, except where noted.
+
+ @param fd: An integer file descriptor associated with a listening
+ socket. The socket must be in non-blocking mode. Any additional
+ attributes desired, such as I{FD_CLOEXEC}, must also be set already.
+
+ @param addressFamily: The address family (sometimes called I{domain}) of
+ the existing socket. For example, L{socket.AF_INET}.
+
+ @return: A new instance of C{cls} wrapping the socket given by C{fd}.
+ """
+ port = socket.fromfd(fd, addressFamily, cls.socketType)
+ interface = port.getsockname()[0]
+ self = cls(None, factory, None, interface, reactor)
+ self._preexistingSocket = port
+ return self
+
+
+ def __repr__(self):
+ if self._realPortNumber is not None:
+ return "<%s of %s on %s>" % (self.__class__,
+ self.factory.__class__, self._realPortNumber)
+ else:
+ return "<%s of %s (not listening)>" % (self.__class__, self.factory.__class__)
+
+ def createInternetSocket(self):
+ s = base.BasePort.createInternetSocket(self)
+ if platformType == "posix" and sys.platform != "cygwin":
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ return s
+
+
+ def startListening(self):
+ """Create and bind my socket, and begin listening on it.
+
+ This is called on unserialization, and must be called after creating a
+ server to begin listening on the specified port.
+ """
+ if self._preexistingSocket is None:
+ # Create a new socket and make it listen
+ try:
+ skt = self.createInternetSocket()
+ if self.addressFamily == socket.AF_INET6:
+ addr = _resolveIPv6(self.interface, self.port)
+ else:
+ addr = (self.interface, self.port)
+ skt.bind(addr)
+ except socket.error, le:
+ raise CannotListenError, (self.interface, self.port, le)
+ skt.listen(self.backlog)
+ else:
+ # Re-use the externally specified socket
+ skt = self._preexistingSocket
+ self._preexistingSocket = None
+ # Avoid shutting it down at the end.
+ self._socketShutdownMethod = None
+
+ # Make sure that if we listened on port 0, we update that to
+ # reflect what the OS actually assigned us.
+ self._realPortNumber = skt.getsockname()[1]
+
+ log.msg("%s starting on %s" % (
+ self._getLogPrefix(self.factory), self._realPortNumber))
+
+ # The order of the next 5 lines is kind of bizarre. If no one
+ # can explain it, perhaps we should re-arrange them.
+ self.factory.doStart()
+ self.connected = True
+ self.socket = skt
+ self.fileno = self.socket.fileno
+ self.numberAccepts = 100
+
+ self.startReading()
+
+
+ def _buildAddr(self, address):
+ host, port = address[:2]
+ return self._addressType('TCP', host, port)
+
+
+ def doRead(self):
+ """Called when my socket is ready for reading.
+
+ This accepts a connection and calls self.protocol() to handle the
+ wire-level protocol.
+ """
+ try:
+ if platformType == "posix":
+ numAccepts = self.numberAccepts
+ else:
+ # win32 event loop breaks if we do more than one accept()
+ # in an iteration of the event loop.
+ numAccepts = 1
+ for i in range(numAccepts):
+ # we need this so we can deal with a factory's buildProtocol
+ # calling our loseConnection
+ if self.disconnecting:
+ return
+ try:
+ skt, addr = self.socket.accept()
+ except socket.error, e:
+ if e.args[0] in (EWOULDBLOCK, EAGAIN):
+ self.numberAccepts = i
+ break
+ elif e.args[0] == EPERM:
+ # Netfilter on Linux may have rejected the
+ # connection, but we get told to try to accept()
+ # anyway.
+ continue
+ elif e.args[0] in (EMFILE, ENOBUFS, ENFILE, ENOMEM, ECONNABORTED):
+
+ # Linux gives EMFILE when a process is not allowed
+ # to allocate any more file descriptors. *BSD and
+ # Win32 give (WSA)ENOBUFS. Linux can also give
+ # ENFILE if the system is out of inodes, or ENOMEM
+ # if there is insufficient memory to allocate a new
+ # dentry. ECONNABORTED is documented as possible on
+ # both Linux and Windows, but it is not clear
+ # whether there are actually any circumstances under
+ # which it can happen (one might expect it to be
+ # possible if a client sends a FIN or RST after the
+ # server sends a SYN|ACK but before application code
+ # calls accept(2), however at least on Linux this
+ # _seems_ to be short-circuited by syncookies.
+
+ log.msg("Could not accept new connection (%s)" % (
+ errorcode[e.args[0]],))
+ break
+ raise
+
+ fdesc._setCloseOnExec(skt.fileno())
+ protocol = self.factory.buildProtocol(self._buildAddr(addr))
+ if protocol is None:
+ skt.close()
+ continue
+ s = self.sessionno
+ self.sessionno = s+1
+ transport = self.transport(skt, protocol, addr, self, s, self.reactor)
+ protocol.makeConnection(transport)
+ else:
+ self.numberAccepts = self.numberAccepts+20
+ except:
+ # Note that in TLS mode, this will possibly catch SSL.Errors
+ # raised by self.socket.accept()
+ #
+ # There is no "except SSL.Error:" above because SSL may be
+ # None if there is no SSL support. In any case, all the
+ # "except SSL.Error:" suite would probably do is log.deferr()
+ # and return, so handling it here works just as well.
+ log.deferr()
+
+ def loseConnection(self, connDone=failure.Failure(main.CONNECTION_DONE)):
+ """
+ Stop accepting connections on this port.
+
+ This will shut down the socket and call self.connectionLost(). It
+ returns a deferred which will fire successfully when the port is
+ actually closed, or with a failure if an error occurs shutting down.
+ """
+ self.disconnecting = True
+ self.stopReading()
+ if self.connected:
+ self.deferred = deferLater(
+ self.reactor, 0, self.connectionLost, connDone)
+ return self.deferred
+
+ stopListening = loseConnection
+
+ def _logConnectionLostMsg(self):
+ """
+ Log message for closing port
+ """
+ log.msg('(%s Port %s Closed)' % (self._type, self._realPortNumber))
+
+
+ def connectionLost(self, reason):
+ """
+ Cleans up the socket.
+ """
+ self._logConnectionLostMsg()
+ self._realPortNumber = None
+
+ base.BasePort.connectionLost(self, reason)
+ self.connected = False
+ self._closeSocket(True)
+ del self.socket
+ del self.fileno
+
+ try:
+ self.factory.doStop()
+ finally:
+ self.disconnecting = False
+
+
+ def logPrefix(self):
+ """Returns the name of my class, to prefix log entries with.
+ """
+ return reflect.qual(self.factory.__class__)
+
+
+ def getHost(self):
+ """
+ Return an L{IPv4Address} or L{IPv6Address} indicating the listening
+ address of this port.
+ """
+ host, port = self.socket.getsockname()[:2]
+ return self._addressType('TCP', host, port)
+
+
+
+class Connector(base.BaseConnector):
+ """
+ A L{Connector} provides of L{twisted.internet.interfaces.IConnector} for
+ all POSIX-style reactors.
+
+ @ivar _addressType: the type returned by L{Connector.getDestination}.
+ Either L{IPv4Address} or L{IPv6Address}, depending on the type of
+ address.
+ @type _addressType: C{type}
+ """
+ _addressType = address.IPv4Address
+
+ def __init__(self, host, port, factory, timeout, bindAddress, reactor=None):
+ if isinstance(port, types.StringTypes):
+ try:
+ port = socket.getservbyname(port, 'tcp')
+ except socket.error, e:
+ raise error.ServiceNameUnknownError(string="%s (%r)" % (e, port))
+ self.host, self.port = host, port
+ if abstract.isIPv6Address(host):
+ self._addressType = address.IPv6Address
+ self.bindAddress = bindAddress
+ base.BaseConnector.__init__(self, factory, timeout, reactor)
+
+
+ def _makeTransport(self):
+ """
+ Create a L{Client} bound to this L{Connector}.
+
+ @return: a new L{Client}
+ @rtype: L{Client}
+ """
+ return Client(self.host, self.port, self.bindAddress, self, self.reactor)
+
+
+ def getDestination(self):
+ """
+ @see: L{twisted.internet.interfaces.IConnector.getDestination}.
+ """
+ return self._addressType('TCP', self.host, self.port)
+
+
diff --git a/twisted/internet/test/__init__.py b/twisted/internet/test/__init__.py
new file mode 100644
index 0000000..cf1de2a
--- /dev/null
+++ b/twisted/internet/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet}.
+"""
diff --git a/twisted/internet/test/_posixifaces.py b/twisted/internet/test/_posixifaces.py
new file mode 100644
index 0000000..3dcbc27
--- /dev/null
+++ b/twisted/internet/test/_posixifaces.py
@@ -0,0 +1,131 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+POSIX implementation of local network interface enumeration.
+"""
+
+import sys, socket
+
+from socket import AF_INET, AF_INET6, inet_ntop
+from ctypes import (
+ CDLL, POINTER, Structure, c_char_p, c_ushort, c_int,
+ c_uint32, c_uint8, c_void_p, c_ubyte, pointer, cast)
+from ctypes.util import find_library
+
+libc = CDLL(find_library("c"))
+
+if sys.platform == 'darwin':
+ _sockaddrCommon = [
+ ("sin_len", c_uint8),
+ ("sin_family", c_uint8),
+ ]
+else:
+ _sockaddrCommon = [
+ ("sin_family", c_ushort),
+ ]
+
+
+
+class in_addr(Structure):
+ _fields_ = [
+ ("in_addr", c_ubyte * 4),
+ ]
+
+
+
+class in6_addr(Structure):
+ _fields_ = [
+ ("in_addr", c_ubyte * 16),
+ ]
+
+
+
+class sockaddr(Structure):
+ _fields_ = _sockaddrCommon + [
+ ("sin_port", c_ushort),
+ ]
+
+
+
+class sockaddr_in(Structure):
+ _fields_ = _sockaddrCommon + [
+ ("sin_port", c_ushort),
+ ("sin_addr", in_addr),
+ ]
+
+
+
+class sockaddr_in6(Structure):
+ _fields_ = _sockaddrCommon + [
+ ("sin_port", c_ushort),
+ ("sin_flowinfo", c_uint32),
+ ("sin_addr", in6_addr),
+ ]
+
+
+
+class ifaddrs(Structure):
+ pass
+
+ifaddrs_p = POINTER(ifaddrs)
+ifaddrs._fields_ = [
+ ('ifa_next', ifaddrs_p),
+ ('ifa_name', c_char_p),
+ ('ifa_flags', c_uint32),
+ ('ifa_addr', POINTER(sockaddr)),
+ ('ifa_netmask', POINTER(sockaddr)),
+ ('ifa_dstaddr', POINTER(sockaddr)),
+ ('ifa_data', c_void_p)]
+
+getifaddrs = libc.getifaddrs
+getifaddrs.argtypes = [POINTER(ifaddrs_p)]
+getifaddrs.restype = c_int
+
+freeifaddrs = libc.freeifaddrs
+freeifaddrs.argtypes = [ifaddrs_p]
+
+def _interfaces():
+ """
+ Call C{getifaddrs(3)} and return a list of tuples of interface name, address
+ family, and human-readable address representing its results.
+ """
+ ifaddrs = ifaddrs_p()
+ if getifaddrs(pointer(ifaddrs)) < 0:
+ raise OSError()
+ results = []
+ try:
+ while ifaddrs:
+ if ifaddrs[0].ifa_addr:
+ family = ifaddrs[0].ifa_addr[0].sin_family
+ if family == AF_INET:
+ addr = cast(ifaddrs[0].ifa_addr, POINTER(sockaddr_in))
+ elif family == AF_INET6:
+ addr = cast(ifaddrs[0].ifa_addr, POINTER(sockaddr_in6))
+ else:
+ addr = None
+
+ if addr:
+ packed = ''.join(map(chr, addr[0].sin_addr.in_addr[:]))
+ results.append((
+ ifaddrs[0].ifa_name,
+ family,
+ inet_ntop(family, packed)))
+
+ ifaddrs = ifaddrs[0].ifa_next
+ finally:
+ freeifaddrs(ifaddrs)
+ return results
+
+
+
+def posixGetLinkLocalIPv6Addresses():
+ """
+ Return a list of strings in colon-hex format representing all the link local
+ IPv6 addresses available on the system, as reported by I{getifaddrs(3)}.
+ """
+ retList = []
+ for (interface, family, address) in _interfaces():
+ if family == socket.AF_INET6 and address.startswith('fe80:'):
+ retList.append('%s%%%s' % (address, interface))
+ return retList
diff --git a/twisted/internet/test/_win32ifaces.py b/twisted/internet/test/_win32ifaces.py
new file mode 100644
index 0000000..4a1e82b
--- /dev/null
+++ b/twisted/internet/test/_win32ifaces.py
@@ -0,0 +1,119 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Windows implementation of local network interface enumeration.
+"""
+
+from socket import socket, AF_INET6, SOCK_STREAM
+from ctypes import (
+ WinDLL, byref, create_string_buffer, c_int, c_void_p,
+ POINTER, Structure, cast, string_at)
+
+WS2_32 = WinDLL('ws2_32')
+
+SOCKET = c_int
+DWORD = c_int
+LPVOID = c_void_p
+LPSOCKADDR = c_void_p
+LPWSAPROTOCOL_INFO = c_void_p
+LPTSTR = c_void_p
+LPDWORD = c_void_p
+LPWSAOVERLAPPED = c_void_p
+LPWSAOVERLAPPED_COMPLETION_ROUTINE = c_void_p
+
+# http://msdn.microsoft.com/en-us/library/ms741621(v=VS.85).aspx
+# int WSAIoctl(
+# __in SOCKET s,
+# __in DWORD dwIoControlCode,
+# __in LPVOID lpvInBuffer,
+# __in DWORD cbInBuffer,
+# __out LPVOID lpvOutBuffer,
+# __in DWORD cbOutBuffer,
+# __out LPDWORD lpcbBytesReturned,
+# __in LPWSAOVERLAPPED lpOverlapped,
+# __in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
+# );
+WSAIoctl = WS2_32.WSAIoctl
+WSAIoctl.argtypes = [
+ SOCKET, DWORD, LPVOID, DWORD, LPVOID, DWORD, LPDWORD,
+ LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE]
+WSAIoctl.restype = c_int
+
+# http://msdn.microsoft.com/en-us/library/ms741516(VS.85).aspx
+# INT WSAAPI WSAAddressToString(
+# __in LPSOCKADDR lpsaAddress,
+# __in DWORD dwAddressLength,
+# __in_opt LPWSAPROTOCOL_INFO lpProtocolInfo,
+# __inout LPTSTR lpszAddressString,
+# __inout LPDWORD lpdwAddressStringLength
+# );
+WSAAddressToString = WS2_32.WSAAddressToStringA
+WSAAddressToString.argtypes = [
+ LPSOCKADDR, DWORD, LPWSAPROTOCOL_INFO, LPTSTR, LPDWORD]
+WSAAddressToString.restype = c_int
+
+
+SIO_ADDRESS_LIST_QUERY = 0x48000016
+WSAEFAULT = 10014
+
+class SOCKET_ADDRESS(Structure):
+ _fields_ = [('lpSockaddr', c_void_p),
+ ('iSockaddrLength', c_int)]
+
+
+
+def make_SAL(ln):
+ class SOCKET_ADDRESS_LIST(Structure):
+ _fields_ = [('iAddressCount', c_int),
+ ('Address', SOCKET_ADDRESS * ln)]
+ return SOCKET_ADDRESS_LIST
+
+
+
+def win32GetLinkLocalIPv6Addresses():
+ """
+ Return a list of strings in colon-hex format representing all the link local
+ IPv6 addresses available on the system, as reported by
+ I{WSAIoctl}/C{SIO_ADDRESS_LIST_QUERY}.
+ """
+ s = socket(AF_INET6, SOCK_STREAM)
+ size = 4096
+ retBytes = c_int()
+ for i in range(2):
+ buf = create_string_buffer(size)
+ ret = WSAIoctl(
+ s.fileno(),
+ SIO_ADDRESS_LIST_QUERY, 0, 0, buf, size, byref(retBytes), 0, 0)
+
+ # WSAIoctl might fail with WSAEFAULT, which means there was not enough
+ # space in the buffer we gave it. There's no way to check the errno
+ # until Python 2.6, so we don't even try. :/ Maybe if retBytes is still
+ # 0 another error happened, though.
+ if ret and retBytes.value:
+ size = retBytes.value
+ else:
+ break
+
+ # If it failed, then we'll just have to give up. Still no way to see why.
+ if ret:
+ raise RuntimeError("WSAIoctl failure")
+
+ addrList = cast(buf, POINTER(make_SAL(0)))
+ addrCount = addrList[0].iAddressCount
+ addrList = cast(buf, POINTER(make_SAL(addrCount)))
+
+ addressStringBufLength = 1024
+ addressStringBuf = create_string_buffer(addressStringBufLength)
+
+ retList = []
+ for i in range(addrList[0].iAddressCount):
+ retBytes.value = addressStringBufLength
+ addr = addrList[0].Address[i]
+ ret = WSAAddressToString(
+ addr.lpSockaddr, addr.iSockaddrLength, 0, addressStringBuf,
+ byref(retBytes))
+ if ret:
+ raise RuntimeError("WSAAddressToString failure")
+ retList.append(string_at(addressStringBuf))
+ return [addr for addr in retList if '%' in addr]
diff --git a/twisted/internet/test/connectionmixins.py b/twisted/internet/test/connectionmixins.py
new file mode 100644
index 0000000..655f909
--- /dev/null
+++ b/twisted/internet/test/connectionmixins.py
@@ -0,0 +1,649 @@
+# -*- test-case-name: twisted.internet.test.test_tcp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Various helpers for tests for connection-oriented transports.
+"""
+
+import socket
+
+from gc import collect
+from weakref import ref
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.python import context, log
+from twisted.python.failure import Failure
+from twisted.python.runtime import platform
+from twisted.python.log import ILogContext, msg, err
+from twisted.internet.defer import Deferred, gatherResults, succeed, fail
+from twisted.internet.interfaces import (
+ IConnector, IResolverSimple, IReactorFDSet)
+from twisted.internet.protocol import ClientFactory, Protocol, ServerFactory
+from twisted.test.test_tcp import ClosingProtocol
+from twisted.trial.unittest import SkipTest
+from twisted.internet.error import DNSLookupError
+from twisted.internet.interfaces import ITLSTransport
+from twisted.internet.test.reactormixins import ConnectableProtocol
+from twisted.internet.test.reactormixins import runProtocolsWithReactor
+from twisted.internet.test.reactormixins import needsRunningReactor
+
+
+
+def serverFactoryFor(protocol):
+ """
+ Helper function which returns a L{ServerFactory} which will build instances
+ of C{protocol}.
+
+ @param protocol: A callable which returns an L{IProtocol} provider to be
+ used to handle connections to the port the returned factory listens on.
+ """
+ factory = ServerFactory()
+ factory.protocol = protocol
+ return factory
+
+# ServerFactory is good enough for client endpoints, too.
+factoryFor = serverFactoryFor
+
+
+
+def findFreePort(interface='127.0.0.1', family=socket.AF_INET,
+ type=socket.SOCK_STREAM):
+ """
+ Ask the platform to allocate a free port on the specified interface, then
+ release the socket and return the address which was allocated.
+
+ @param interface: The local address to try to bind the port on.
+ @type interface: C{str}
+
+ @param type: The socket type which will use the resulting port.
+
+ @return: A two-tuple of address and port, like that returned by
+ L{socket.getsockname}.
+ """
+ addr = socket.getaddrinfo(interface, 0)[0][4]
+ probe = socket.socket(family, type)
+ try:
+ probe.bind(addr)
+ return probe.getsockname()
+ finally:
+ probe.close()
+
+
+
+def _getWriters(reactor):
+ """
+ Like L{IReactorFDSet.getWriters}, but with support for IOCP reactor as
+ well.
+ """
+ if IReactorFDSet.providedBy(reactor):
+ return reactor.getWriters()
+ elif 'IOCP' in reactor.__class__.__name__:
+ return reactor.handles
+ else:
+ # Cannot tell what is going on.
+ raise Exception("Cannot find writers on %r" % (reactor,))
+
+
+
+class _AcceptOneClient(ServerFactory):
+ """
+ This factory fires a L{Deferred} with a protocol instance shortly after it
+ is constructed (hopefully long enough afterwards so that it has been
+ connected to a transport).
+
+ @ivar reactor: The reactor used to schedule the I{shortly}.
+
+ @ivar result: A L{Deferred} which will be fired with the protocol instance.
+ """
+ def __init__(self, reactor, result):
+ self.reactor = reactor
+ self.result = result
+
+
+ def buildProtocol(self, addr):
+ protocol = ServerFactory.buildProtocol(self, addr)
+ self.reactor.callLater(0, self.result.callback, protocol)
+ return protocol
+
+
+
+class _SimplePullProducer(object):
+ """
+ A pull producer which writes one byte whenever it is resumed. For use by
+ L{test_unregisterProducerAfterDisconnect}.
+ """
+ def __init__(self, consumer):
+ self.consumer = consumer
+
+
+ def stopProducing(self):
+ pass
+
+
+ def resumeProducing(self):
+ log.msg("Producer.resumeProducing")
+ self.consumer.write('x')
+
+
+
+class Stop(ClientFactory):
+ """
+ A client factory which stops a reactor when a connection attempt fails.
+ """
+ failReason = None
+
+ def __init__(self, reactor):
+ self.reactor = reactor
+
+
+ def clientConnectionFailed(self, connector, reason):
+ self.failReason = reason
+ msg("Stop(CF) cCFailed: %s" % (reason.getErrorMessage(),))
+ self.reactor.stop()
+
+
+
+class FakeResolver(object):
+ """
+ A resolver implementation based on a C{dict} mapping names to addresses.
+ """
+ implements(IResolverSimple)
+
+ def __init__(self, names):
+ self.names = names
+
+
+ def getHostByName(self, name, timeout):
+ try:
+ return succeed(self.names[name])
+ except KeyError:
+ return fail(DNSLookupError("FakeResolver couldn't find " + name))
+
+
+
+class ClosingLaterProtocol(ConnectableProtocol):
+ """
+ ClosingLaterProtocol exchanges one byte with its peer and then disconnects
+ itself. This is mostly a work-around for the fact that connectionMade is
+ called before the SSL handshake has completed.
+ """
+ def __init__(self, onConnectionLost):
+ self.lostConnectionReason = None
+ self.onConnectionLost = onConnectionLost
+
+
+ def connectionMade(self):
+ msg("ClosingLaterProtocol.connectionMade")
+
+
+ def dataReceived(self, bytes):
+ msg("ClosingLaterProtocol.dataReceived %r" % (bytes,))
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ msg("ClosingLaterProtocol.connectionLost")
+ self.lostConnectionReason = reason
+ self.onConnectionLost.callback(self)
+
+
+
+class ConnectionTestsMixin(object):
+ """
+ This mixin defines test methods which should apply to most L{ITransport}
+ implementations.
+ """
+
+ # This should be a reactormixins.EndpointCreator instance.
+ endpoints = None
+
+
+ def test_logPrefix(self):
+ """
+ Client and server transports implement L{ILoggingContext.logPrefix} to
+ return a message reflecting the protocol they are running.
+ """
+ class CustomLogPrefixProtocol(ConnectableProtocol):
+ def __init__(self, prefix):
+ self._prefix = prefix
+ self.system = None
+
+ def connectionMade(self):
+ self.transport.write("a")
+
+ def logPrefix(self):
+ return self._prefix
+
+ def dataReceived(self, bytes):
+ self.system = context.get(ILogContext)["system"]
+ self.transport.write("b")
+ # Only close connection if both sides have received data, so
+ # that both sides have system set.
+ if "b" in bytes:
+ self.transport.loseConnection()
+
+ client = CustomLogPrefixProtocol("Custom Client")
+ server = CustomLogPrefixProtocol("Custom Server")
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+ self.assertIn("Custom Client", client.system)
+ self.assertIn("Custom Server", server.system)
+
+
+ def test_writeAfterDisconnect(self):
+ """
+ After a connection is disconnected, L{ITransport.write} and
+ L{ITransport.writeSequence} are no-ops.
+ """
+ reactor = self.buildReactor()
+
+ finished = []
+
+ serverConnectionLostDeferred = Deferred()
+ protocol = lambda: ClosingLaterProtocol(serverConnectionLostDeferred)
+ portDeferred = self.endpoints.server(reactor).listen(
+ serverFactoryFor(protocol))
+ def listening(port):
+ msg("Listening on %r" % (port.getHost(),))
+ endpoint = self.endpoints.client(reactor, port.getHost())
+
+ lostConnectionDeferred = Deferred()
+ protocol = lambda: ClosingLaterProtocol(lostConnectionDeferred)
+ client = endpoint.connect(factoryFor(protocol))
+ def write(proto):
+ msg("About to write to %r" % (proto,))
+ proto.transport.write('x')
+ client.addCallbacks(write, lostConnectionDeferred.errback)
+
+ def disconnected(proto):
+ msg("%r disconnected" % (proto,))
+ proto.transport.write("some bytes to get lost")
+ proto.transport.writeSequence(["some", "more"])
+ finished.append(True)
+
+ lostConnectionDeferred.addCallback(disconnected)
+ serverConnectionLostDeferred.addCallback(disconnected)
+ return gatherResults([
+ lostConnectionDeferred,
+ serverConnectionLostDeferred])
+
+ def onListen():
+ portDeferred.addCallback(listening)
+ portDeferred.addErrback(err)
+ portDeferred.addCallback(lambda ignored: reactor.stop())
+ needsRunningReactor(reactor, onListen)
+
+ self.runReactor(reactor)
+ self.assertEqual(finished, [True, True])
+
+
+ def test_protocolGarbageAfterLostConnection(self):
+ """
+ After the connection a protocol is being used for is closed, the
+ reactor discards all of its references to the protocol.
+ """
+ lostConnectionDeferred = Deferred()
+ clientProtocol = ClosingLaterProtocol(lostConnectionDeferred)
+ clientRef = ref(clientProtocol)
+
+ reactor = self.buildReactor()
+ portDeferred = self.endpoints.server(reactor).listen(
+ serverFactoryFor(Protocol))
+ def listening(port):
+ msg("Listening on %r" % (port.getHost(),))
+ endpoint = self.endpoints.client(reactor, port.getHost())
+
+ client = endpoint.connect(factoryFor(lambda: clientProtocol))
+ def disconnect(proto):
+ msg("About to disconnect %r" % (proto,))
+ proto.transport.loseConnection()
+ client.addCallback(disconnect)
+ client.addErrback(lostConnectionDeferred.errback)
+ return lostConnectionDeferred
+
+ def onListening():
+ portDeferred.addCallback(listening)
+ portDeferred.addErrback(err)
+ portDeferred.addBoth(lambda ignored: reactor.stop())
+ needsRunningReactor(reactor, onListening)
+
+ self.runReactor(reactor)
+
+ # Drop the reference and get the garbage collector to tell us if there
+ # are no references to the protocol instance left in the reactor.
+ clientProtocol = None
+ collect()
+ self.assertIdentical(None, clientRef())
+
+
+
+class LogObserverMixin(object):
+ """
+ Mixin for L{TestCase} subclasses which want to observe log events.
+ """
+ def observe(self):
+ loggedMessages = []
+ log.addObserver(loggedMessages.append)
+ self.addCleanup(log.removeObserver, loggedMessages.append)
+ return loggedMessages
+
+
+
+class BrokenContextFactory(object):
+ """
+ A context factory with a broken C{getContext} method, for exercising the
+ error handling for such a case.
+ """
+ message = "Some path was wrong maybe"
+
+ def getContext(self):
+ raise ValueError(self.message)
+
+
+
+class TCPClientTestsMixin(object):
+ """
+ This mixin defines tests applicable to TCP client implementations. Classes
+ which mix this in must provide all of the documented instance variables in
+ order to specify how the test works. These are documented as instance
+ variables rather than declared as methods due to some peculiar inheritance
+ ordering concerns, but they are effectively abstract methods.
+
+ This must be mixed in to a L{ReactorBuilder
+ <twisted.internet.test.reactormixins.ReactorBuilder>} subclass, as it
+ depends on several of its methods.
+
+ @ivar endpoints: A L{twisted.internet.test.reactormixins.EndpointCreator}
+ instance.
+
+ @ivar interface: An IP address literal to locally bind a socket to as well
+ as to connect to. This can be any valid interface for the local host.
+ @type interface: C{str}
+
+ @ivar port: An unused local listening port to listen on and connect to.
+ This will be used in conjunction with the C{interface}. (Depending on
+ what they're testing, some tests will locate their own port with
+ L{findFreePort} instead.)
+ @type port: C{int}
+
+ @ivar family: an address family constant, such as L{socket.AF_INET},
+ L{socket.AF_INET6}, or L{socket.AF_UNIX}, which indicates the address
+ family of the transport type under test.
+ @type family: C{int}
+
+ @ivar addressClass: the L{twisted.internet.interfaces.IAddress} implementor
+ associated with the transport type under test. Must also be a
+ 3-argument callable which produces an instance of same.
+ @type addressClass: C{type}
+
+ @ivar fakeDomainName: A fake domain name to use, to simulate hostname
+ resolution and to distinguish between hostnames and IP addresses where
+ necessary.
+ @type fakeDomainName: C{str}
+ """
+
+ def test_interface(self):
+ """
+ L{IReactorTCP.connectTCP} returns an object providing L{IConnector}.
+ """
+ reactor = self.buildReactor()
+ connector = reactor.connectTCP(self.interface, self.port,
+ ClientFactory())
+ self.assertTrue(verifyObject(IConnector, connector))
+
+
+ def test_clientConnectionFailedStopsReactor(self):
+ """
+ The reactor can be stopped by a client factory's
+ C{clientConnectionFailed} method.
+ """
+ host, port = findFreePort(self.interface, self.family)[:2]
+ reactor = self.buildReactor()
+ needsRunningReactor(
+ reactor, lambda: reactor.connectTCP(host, port, Stop(reactor)))
+ self.runReactor(reactor)
+
+
+ def test_addresses(self):
+ """
+ A client's transport's C{getHost} and C{getPeer} return L{IPv4Address}
+ instances which have the dotted-quad string form of the resolved
+ adddress of the local and remote endpoints of the connection
+ respectively as their C{host} attribute, not the hostname originally
+ passed in to L{connectTCP
+ <twisted.internet.interfaces.IReactorTCP.connectTCP>}, if a hostname
+ was used.
+ """
+ host, port = findFreePort(self.interface, self.family)[:2]
+ reactor = self.buildReactor()
+ fakeDomain = self.fakeDomainName
+ reactor.installResolver(FakeResolver({fakeDomain: self.interface}))
+
+ server = reactor.listenTCP(
+ 0, serverFactoryFor(Protocol), interface=host)
+ serverAddress = server.getHost()
+
+ addresses = {'host': None, 'peer': None}
+ class CheckAddress(Protocol):
+ def makeConnection(self, transport):
+ addresses['host'] = transport.getHost()
+ addresses['peer'] = transport.getPeer()
+ reactor.stop()
+
+ clientFactory = Stop(reactor)
+ clientFactory.protocol = CheckAddress
+
+ def connectMe():
+ reactor.connectTCP(
+ fakeDomain, server.getHost().port, clientFactory,
+ bindAddress=(self.interface, port))
+ needsRunningReactor(reactor, connectMe)
+
+ self.runReactor(reactor)
+
+ if clientFactory.failReason:
+ self.fail(clientFactory.failReason.getTraceback())
+
+ self.assertEqual(
+ addresses['host'],
+ self.addressClass('TCP', self.interface, port))
+ self.assertEqual(
+ addresses['peer'],
+ self.addressClass('TCP', self.interface, serverAddress.port))
+
+
+ def test_connectEvent(self):
+ """
+ This test checks that we correctly get notifications event for a
+ client. This ought to prevent a regression under Windows using the
+ GTK2 reactor. See #3925.
+ """
+ reactor = self.buildReactor()
+
+ server = reactor.listenTCP(0, serverFactoryFor(Protocol),
+ interface=self.interface)
+ connected = []
+
+ class CheckConnection(Protocol):
+ def connectionMade(self):
+ connected.append(self)
+ reactor.stop()
+
+ clientFactory = Stop(reactor)
+ clientFactory.protocol = CheckConnection
+
+ needsRunningReactor(reactor, lambda: reactor.connectTCP(
+ self.interface, server.getHost().port, clientFactory))
+
+ reactor.run()
+
+ self.assertTrue(connected)
+
+
+ def test_unregisterProducerAfterDisconnect(self):
+ """
+ If a producer is unregistered from a L{ITCPTransport} provider after
+ the transport has been disconnected (by the peer) and after
+ L{ITCPTransport.loseConnection} has been called, the transport is not
+ re-added to the reactor as a writer as would be necessary if the
+ transport were still connected.
+ """
+ reactor = self.buildReactor()
+ port = reactor.listenTCP(0, serverFactoryFor(ClosingProtocol),
+ interface=self.interface)
+
+ finished = Deferred()
+ finished.addErrback(log.err)
+ finished.addCallback(lambda ign: reactor.stop())
+
+ writing = []
+
+ class ClientProtocol(Protocol):
+ """
+ Protocol to connect, register a producer, try to lose the
+ connection, wait for the server to disconnect from us, and then
+ unregister the producer.
+ """
+ def connectionMade(self):
+ log.msg("ClientProtocol.connectionMade")
+ self.transport.registerProducer(
+ _SimplePullProducer(self.transport), False)
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ log.msg("ClientProtocol.connectionLost")
+ self.unregister()
+ writing.append(self.transport in _getWriters(reactor))
+ finished.callback(None)
+
+ def unregister(self):
+ log.msg("ClientProtocol unregister")
+ self.transport.unregisterProducer()
+
+ clientFactory = ClientFactory()
+ clientFactory.protocol = ClientProtocol
+ reactor.connectTCP(self.interface, port.getHost().port, clientFactory)
+ self.runReactor(reactor)
+ self.assertFalse(writing[0],
+ "Transport was writing after unregisterProducer.")
+
+
+ def test_disconnectWhileProducing(self):
+ """
+ If L{ITCPTransport.loseConnection} is called while a producer is
+ registered with the transport, the connection is closed after the
+ producer is unregistered.
+ """
+ reactor = self.buildReactor()
+
+ # For some reason, pyobject/pygtk will not deliver the close
+ # notification that should happen after the unregisterProducer call in
+ # this test. The selectable is in the write notification set, but no
+ # notification ever arrives. Probably for the same reason #5233 led
+ # win32eventreactor to be broken.
+ skippedReactors = ["Glib2Reactor", "Gtk2Reactor"]
+ reactorClassName = reactor.__class__.__name__
+ if reactorClassName in skippedReactors and platform.isWindows():
+ raise SkipTest(
+ "A pygobject/pygtk bug disables this functionality on Windows.")
+
+ class Producer:
+ def resumeProducing(self):
+ log.msg("Producer.resumeProducing")
+
+ port = reactor.listenTCP(0, serverFactoryFor(Protocol),
+ interface=self.interface)
+
+ finished = Deferred()
+ finished.addErrback(log.err)
+ finished.addCallback(lambda ign: reactor.stop())
+
+ class ClientProtocol(Protocol):
+ """
+ Protocol to connect, register a producer, try to lose the
+ connection, unregister the producer, and wait for the connection to
+ actually be lost.
+ """
+ def connectionMade(self):
+ log.msg("ClientProtocol.connectionMade")
+ self.transport.registerProducer(Producer(), False)
+ self.transport.loseConnection()
+ # Let the reactor tick over, in case synchronously calling
+ # loseConnection and then unregisterProducer is the same as
+ # synchronously calling unregisterProducer and then
+ # loseConnection (as it is in several reactors).
+ reactor.callLater(0, reactor.callLater, 0, self.unregister)
+
+ def unregister(self):
+ log.msg("ClientProtocol unregister")
+ self.transport.unregisterProducer()
+ # This should all be pretty quick. Fail the test
+ # if we don't get a connectionLost event really
+ # soon.
+ reactor.callLater(
+ 1.0, finished.errback,
+ Failure(Exception("Connection was not lost")))
+
+ def connectionLost(self, reason):
+ log.msg("ClientProtocol.connectionLost")
+ finished.callback(None)
+
+ clientFactory = ClientFactory()
+ clientFactory.protocol = ClientProtocol
+ reactor.connectTCP(self.interface, port.getHost().port, clientFactory)
+ self.runReactor(reactor)
+ # If the test failed, we logged an error already and trial
+ # will catch it.
+
+
+ def test_badContext(self):
+ """
+ If the context factory passed to L{ITCPTransport.startTLS} raises an
+ exception from its C{getContext} method, that exception is raised by
+ L{ITCPTransport.startTLS}.
+ """
+ reactor = self.buildReactor()
+
+ brokenFactory = BrokenContextFactory()
+ results = []
+
+ serverFactory = ServerFactory()
+ serverFactory.protocol = Protocol
+
+ port = reactor.listenTCP(0, serverFactory, interface=self.interface)
+ endpoint = self.endpoints.client(reactor, port.getHost())
+
+ clientFactory = ClientFactory()
+ clientFactory.protocol = Protocol
+ connectDeferred = endpoint.connect(clientFactory)
+
+ def connected(protocol):
+ if not ITLSTransport.providedBy(protocol.transport):
+ results.append("skip")
+ else:
+ results.append(self.assertRaises(ValueError,
+ protocol.transport.startTLS,
+ brokenFactory))
+
+ def connectFailed(failure):
+ results.append(failure)
+
+ def whenRun():
+ connectDeferred.addCallback(connected)
+ connectDeferred.addErrback(connectFailed)
+ connectDeferred.addBoth(lambda ign: reactor.stop())
+ needsRunningReactor(reactor, whenRun)
+
+ self.runReactor(reactor)
+
+ self.assertEqual(len(results), 1,
+ "more than one callback result: %s" % (results,))
+
+ if isinstance(results[0], Failure):
+ # self.fail(Failure)
+ results[0].raiseException()
+ if results[0] == "skip":
+ raise SkipTest("Reactor does not support ITLSTransport")
+ self.assertEqual(BrokenContextFactory.message, str(results[0]))
diff --git a/twisted/internet/test/fake_CAs/not-a-certificate b/twisted/internet/test/fake_CAs/not-a-certificate
new file mode 100644
index 0000000..316453d
--- /dev/null
+++ b/twisted/internet/test/fake_CAs/not-a-certificate
@@ -0,0 +1 @@
+This file is not a certificate; it is present to make sure that it will be skipped.
diff --git a/twisted/internet/test/fake_CAs/thing1.pem b/twisted/internet/test/fake_CAs/thing1.pem
new file mode 100644
index 0000000..75e47a6
--- /dev/null
+++ b/twisted/internet/test/fake_CAs/thing1.pem
@@ -0,0 +1,26 @@
+
+This is a self-signed certificate authority certificate to be used in tests.
+
+It was created with the following command:
+certcreate -f thing1.pem -h fake-ca-1.example.com -e noreply@example.com \
+ -S 1234 -o 'Twisted Matrix Labs'
+
+'certcreate' may be obtained from <http://divmod.org/trac/wiki/DivmodEpsilon>
+
+-----BEGIN CERTIFICATE-----
+MIICwjCCAisCAgTSMA0GCSqGSIb3DQEBBAUAMIGoMREwDwYDVQQLEwhTZWN1cml0
+eTEcMBoGA1UEChMTVHdpc3RlZCBNYXRyaXggTGFiczEeMBwGA1UEAxMVZmFrZS1j
+YS0xLmV4YW1wbGUuY29tMREwDwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMx
+IjAgBgkqhkiG9w0BCQEWE25vcmVwbHlAZXhhbXBsZS5jb20xETAPBgNVBAcTCE5l
+dyBZb3JrMB4XDTEwMDkyMTAxMjUxNFoXDTExMDkyMTAxMjUxNFowgagxETAPBgNV
+BAsTCFNlY3VyaXR5MRwwGgYDVQQKExNUd2lzdGVkIE1hdHJpeCBMYWJzMR4wHAYD
+VQQDExVmYWtlLWNhLTEuZXhhbXBsZS5jb20xETAPBgNVBAgTCE5ldyBZb3JrMQsw
+CQYDVQQGEwJVUzEiMCAGCSqGSIb3DQEJARYTbm9yZXBseUBleGFtcGxlLmNvbTER
+MA8GA1UEBxMITmV3IFlvcmswgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALRb
+VqC0CsaFgq1vbwPfs8zoP3ZYC/0sPMv0RJN+f3Dc7Q6YgNHS7o7TM3uAy/McADeW
+rwVuNJGe9k+4ZBHysmBH1sG64fHT5TlK9saPcUQqkubSWj4cKSDtVbQERWqC5Dy+
+qTQeZGYoPEMlnRXgMpST04DG//Dgzi4PYqUOjwxTAgMBAAEwDQYJKoZIhvcNAQEE
+BQADgYEAqNEdMXWEs8Co76wxL3/cSV3MjiAroVxJdI/3EzlnfPi1JeibbdWw31fC
+bn6428KTjjfhS31zo1yHG3YNXFEJXRscwLAH7ogz5kJwZMy/oS/96EFM10bkNwkK
+v+nWKN8i3t/E5TEIl3BPN8tchtWmH0rycVuzs5LwaewwR1AnUE4=
+-----END CERTIFICATE-----
diff --git a/twisted/internet/test/fake_CAs/thing2-duplicate.pem b/twisted/internet/test/fake_CAs/thing2-duplicate.pem
new file mode 100644
index 0000000..429e121
--- /dev/null
+++ b/twisted/internet/test/fake_CAs/thing2-duplicate.pem
@@ -0,0 +1,26 @@
+
+This is a self-signed certificate authority certificate to be used in tests.
+
+It was created with the following command:
+certcreate -f thing2.pem -h fake-ca-2.example.com -e noreply@example.com \
+ -S 1234 -o 'Twisted Matrix Labs'
+
+'certcreate' may be obtained from <http://divmod.org/trac/wiki/DivmodEpsilon>
+
+-----BEGIN CERTIFICATE-----
+MIICwjCCAisCAgTSMA0GCSqGSIb3DQEBBAUAMIGoMREwDwYDVQQLEwhTZWN1cml0
+eTEcMBoGA1UEChMTVHdpc3RlZCBNYXRyaXggTGFiczEeMBwGA1UEAxMVZmFrZS1j
+YS0yLmV4YW1wbGUuY29tMREwDwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMx
+IjAgBgkqhkiG9w0BCQEWE25vcmVwbHlAZXhhbXBsZS5jb20xETAPBgNVBAcTCE5l
+dyBZb3JrMB4XDTEwMDkyMTAxMjUzMVoXDTExMDkyMTAxMjUzMVowgagxETAPBgNV
+BAsTCFNlY3VyaXR5MRwwGgYDVQQKExNUd2lzdGVkIE1hdHJpeCBMYWJzMR4wHAYD
+VQQDExVmYWtlLWNhLTIuZXhhbXBsZS5jb20xETAPBgNVBAgTCE5ldyBZb3JrMQsw
+CQYDVQQGEwJVUzEiMCAGCSqGSIb3DQEJARYTbm9yZXBseUBleGFtcGxlLmNvbTER
+MA8GA1UEBxMITmV3IFlvcmswgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMNn
+b3EcKqBedQed1qJC4uGVx8PYmn2vxL3QwCVW1w0VjpZXyhCq/2VrYBhJAXRzpfvE
+dCqhtJKcdifwavUrTfr4yXu1MvWA0YuaAkj1TbmlHHQYACf3h+MPOXroYzhT72bO
+FSSLDWuitj0ozR+2Fk15QwLWUxaYLmwylxXAf7vpAgMBAAEwDQYJKoZIhvcNAQEE
+BQADgYEADB2N6VHHhm5M2rJqqGDXMm2dU+7abxiuN+PUygN2LXIsqdGBS6U7/rta
+lJNVeRaM423c8imfuklkIBG9Msn5+xm1xIMIULoi/efActDLbsX1x6IyHQrG5aDP
+/RMKBio9RjS8ajgSwyYVUZiCZBsn/T0/JS8K61YLpiv4Tg8uXmM=
+-----END CERTIFICATE-----
diff --git a/twisted/internet/test/fake_CAs/thing2.pem b/twisted/internet/test/fake_CAs/thing2.pem
new file mode 100644
index 0000000..429e121
--- /dev/null
+++ b/twisted/internet/test/fake_CAs/thing2.pem
@@ -0,0 +1,26 @@
+
+This is a self-signed certificate authority certificate to be used in tests.
+
+It was created with the following command:
+certcreate -f thing2.pem -h fake-ca-2.example.com -e noreply@example.com \
+ -S 1234 -o 'Twisted Matrix Labs'
+
+'certcreate' may be obtained from <http://divmod.org/trac/wiki/DivmodEpsilon>
+
+-----BEGIN CERTIFICATE-----
+MIICwjCCAisCAgTSMA0GCSqGSIb3DQEBBAUAMIGoMREwDwYDVQQLEwhTZWN1cml0
+eTEcMBoGA1UEChMTVHdpc3RlZCBNYXRyaXggTGFiczEeMBwGA1UEAxMVZmFrZS1j
+YS0yLmV4YW1wbGUuY29tMREwDwYDVQQIEwhOZXcgWW9yazELMAkGA1UEBhMCVVMx
+IjAgBgkqhkiG9w0BCQEWE25vcmVwbHlAZXhhbXBsZS5jb20xETAPBgNVBAcTCE5l
+dyBZb3JrMB4XDTEwMDkyMTAxMjUzMVoXDTExMDkyMTAxMjUzMVowgagxETAPBgNV
+BAsTCFNlY3VyaXR5MRwwGgYDVQQKExNUd2lzdGVkIE1hdHJpeCBMYWJzMR4wHAYD
+VQQDExVmYWtlLWNhLTIuZXhhbXBsZS5jb20xETAPBgNVBAgTCE5ldyBZb3JrMQsw
+CQYDVQQGEwJVUzEiMCAGCSqGSIb3DQEJARYTbm9yZXBseUBleGFtcGxlLmNvbTER
+MA8GA1UEBxMITmV3IFlvcmswgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMNn
+b3EcKqBedQed1qJC4uGVx8PYmn2vxL3QwCVW1w0VjpZXyhCq/2VrYBhJAXRzpfvE
+dCqhtJKcdifwavUrTfr4yXu1MvWA0YuaAkj1TbmlHHQYACf3h+MPOXroYzhT72bO
+FSSLDWuitj0ozR+2Fk15QwLWUxaYLmwylxXAf7vpAgMBAAEwDQYJKoZIhvcNAQEE
+BQADgYEADB2N6VHHhm5M2rJqqGDXMm2dU+7abxiuN+PUygN2LXIsqdGBS6U7/rta
+lJNVeRaM423c8imfuklkIBG9Msn5+xm1xIMIULoi/efActDLbsX1x6IyHQrG5aDP
+/RMKBio9RjS8ajgSwyYVUZiCZBsn/T0/JS8K61YLpiv4Tg8uXmM=
+-----END CERTIFICATE-----
diff --git a/twisted/internet/test/fakeendpoint.py b/twisted/internet/test/fakeendpoint.py
new file mode 100644
index 0000000..dbb7419
--- /dev/null
+++ b/twisted/internet/test/fakeendpoint.py
@@ -0,0 +1,66 @@
+# -*- test-case-name: twisted.internet.test.test_endpoints -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Fake client and server endpoint string parser plugins for testing purposes.
+"""
+
+from zope.interface.declarations import implements
+from twisted.plugin import IPlugin
+from twisted.internet.interfaces import (IStreamClientEndpoint,
+ IStreamServerEndpoint,
+ IStreamClientEndpointStringParser,
+ IStreamServerEndpointStringParser)
+
+class PluginBase(object):
+ implements(IPlugin)
+
+ def __init__(self, pfx):
+ self.prefix = pfx
+
+
+
+class FakeClientParser(PluginBase):
+
+ implements(IStreamClientEndpointStringParser)
+
+ def parseStreamClient(self, *a, **kw):
+ return StreamClient(self, a, kw)
+
+
+
+class FakeParser(PluginBase):
+
+ implements(IStreamServerEndpointStringParser)
+
+ def parseStreamServer(self, *a, **kw):
+ return StreamServer(self, a, kw)
+
+
+
+class EndpointBase(object):
+
+ def __init__(self, parser, args, kwargs):
+ self.parser = parser
+ self.args = args
+ self.kwargs = kwargs
+
+
+
+class StreamClient(EndpointBase):
+
+ implements(IStreamClientEndpoint)
+
+
+
+class StreamServer(EndpointBase):
+
+ implements(IStreamServerEndpoint)
+
+
+
+# Instantiate plugin interface providers to register them.
+fake = FakeParser('fake')
+fakeClient = FakeClientParser('cfake')
+
diff --git a/twisted/internet/test/inlinecb_tests.py b/twisted/internet/test/inlinecb_tests.py
new file mode 100644
index 0000000..a6d0121
--- /dev/null
+++ b/twisted/internet/test/inlinecb_tests.py
@@ -0,0 +1,92 @@
+# -*- test-case-name: twisted.internet.test.test_inlinecb -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.defer.inlineCallbacks}.
+
+These tests are defined in a non-C{test_*} module because they are
+syntactically invalid on python < 2.5. test_inlinecb will conditionally import
+these tests on python 2.5 and greater.
+
+Some tests for inlineCallbacks are defined in L{twisted.test.test_defgen} as
+well: see U{http://twistedmatrix.com/trac/ticket/4182}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import Deferred, returnValue, inlineCallbacks
+
+class NonLocalExitTests(TestCase):
+ """
+ It's possible for L{returnValue} to be (accidentally) invoked at a stack
+ level below the L{inlineCallbacks}-decorated function which it is exiting.
+ If this happens, L{returnValue} should report useful errors.
+
+ If L{returnValue} is invoked from a function not decorated by
+ L{inlineCallbacks}, it will emit a warning if it causes an
+ L{inlineCallbacks} function further up the stack to exit.
+ """
+
+ def mistakenMethod(self):
+ """
+ This method mistakenly invokes L{returnValue}, despite the fact that it
+ is not decorated with L{inlineCallbacks}.
+ """
+ returnValue(1)
+
+
+ def assertMistakenMethodWarning(self, resultList):
+ """
+ Flush the current warnings and assert that we have been told that
+ C{mistakenMethod} was invoked, and that the result from the Deferred
+ that was fired (appended to the given list) is C{mistakenMethod}'s
+ result. The warning should indicate that an inlineCallbacks function
+ called 'inline' was made to exit.
+ """
+ self.assertEqual(resultList, [1])
+ warnings = self.flushWarnings(offendingFunctions=[self.mistakenMethod])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "returnValue() in 'mistakenMethod' causing 'inline' to exit: "
+ "returnValue should only be invoked by functions decorated with "
+ "inlineCallbacks")
+
+
+ def test_returnValueNonLocalWarning(self):
+ """
+ L{returnValue} will emit a non-local exit warning in the simplest case,
+ where the offending function is invoked immediately.
+ """
+ @inlineCallbacks
+ def inline():
+ self.mistakenMethod()
+ returnValue(2)
+ yield 0
+ d = inline()
+ results = []
+ d.addCallback(results.append)
+ self.assertMistakenMethodWarning(results)
+
+
+ def test_returnValueNonLocalDeferred(self):
+ """
+ L{returnValue} will emit a non-local warning in the case where the
+ L{inlineCallbacks}-decorated function has already yielded a Deferred
+ and therefore moved its generator function along.
+ """
+ cause = Deferred()
+ @inlineCallbacks
+ def inline():
+ yield cause
+ self.mistakenMethod()
+ returnValue(2)
+ effect = inline()
+ results = []
+ effect.addCallback(results.append)
+ self.assertEqual(results, [])
+ cause.callback(1)
+ self.assertMistakenMethodWarning(results)
+
+
diff --git a/twisted/internet/test/process_helper.py b/twisted/internet/test/process_helper.py
new file mode 100644
index 0000000..b921697
--- /dev/null
+++ b/twisted/internet/test/process_helper.py
@@ -0,0 +1,33 @@
+
+# A program which exits after starting a child which inherits its
+# stdin/stdout/stderr and keeps them open until stdin is closed.
+
+import sys, os
+
+def grandchild():
+ sys.stdout.write('grandchild started')
+ sys.stdout.flush()
+ sys.stdin.read()
+
+def main():
+ if sys.argv[1] == 'child':
+ if sys.argv[2] == 'windows':
+ import win32api as api, win32process as proc
+ info = proc.STARTUPINFO()
+ info.hStdInput = api.GetStdHandle(api.STD_INPUT_HANDLE)
+ info.hStdOutput = api.GetStdHandle(api.STD_OUTPUT_HANDLE)
+ info.hStdError = api.GetStdHandle(api.STD_ERROR_HANDLE)
+ python = sys.executable
+ scriptDir = os.path.dirname(__file__)
+ scriptName = os.path.basename(__file__)
+ proc.CreateProcess(
+ None, " ".join((python, scriptName, "grandchild")), None,
+ None, 1, 0, os.environ, scriptDir, info)
+ else:
+ if os.fork() == 0:
+ grandchild()
+ else:
+ grandchild()
+
+if __name__ == '__main__':
+ main()
diff --git a/twisted/internet/test/reactormixins.py b/twisted/internet/test/reactormixins.py
new file mode 100644
index 0000000..dc9a5d5
--- /dev/null
+++ b/twisted/internet/test/reactormixins.py
@@ -0,0 +1,409 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorTime}.
+"""
+
+__metaclass__ = type
+
+import os, signal, time
+
+from twisted.internet.defer import TimeoutError, Deferred, gatherResults
+from twisted.internet.protocol import ClientFactory, Protocol
+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.python.runtime import platform
+from twisted.python.reflect import namedAny, fullyQualifiedName
+from twisted.python import log
+from twisted.python.failure import Failure
+
+# Access private APIs.
+if platform.isWindows():
+ process = None
+else:
+ from twisted.internet import process
+
+
+
+def needsRunningReactor(reactor, thunk):
+ """
+ Various functions within these tests need an already-running reactor at
+ some point. They need to stop the reactor when the test has completed, and
+ that means calling reactor.stop(). However, reactor.stop() raises an
+ exception if the reactor isn't already running, so if the L{Deferred} that
+ a particular API under test returns fires synchronously (as especially an
+ endpoint's C{connect()} method may do, if the connect is to a local
+ interface address) then the test won't be able to stop the reactor being
+ tested and finish. So this calls C{thunk} only once C{reactor} is running.
+
+ (This is just an alias for
+ L{twisted.internet.interfaces.IReactorCore.callWhenRunning} on the given
+ reactor parameter, in order to centrally reference the above paragraph and
+ repeating it everywhere as a comment.)
+
+ @param reactor: the L{twisted.internet.interfaces.IReactorCore} under test
+
+ @param thunk: a 0-argument callable, which eventually finishes the test in
+ question, probably in a L{Deferred} callback.
+ """
+ reactor.callWhenRunning(thunk)
+
+
+
+class ConnectableProtocol(Protocol):
+ """
+ A protocol to be used with L{runProtocolsWithReactor}.
+
+ The protocol and its pair should eventually disconnect from each other.
+
+ @ivar reactor: The reactor used in this test.
+
+ @ivar disconnectReason: The L{Failure} passed to C{connectionLost}.
+
+ @ivar _done: A L{Deferred} which will be fired when the connection is
+ lost.
+ """
+
+ disconnectReason = None
+
+ def _setAttributes(self, reactor, done):
+ """
+ Set attributes on the protocol that are known only externally; this
+ will be called by L{runProtocolsWithReactor} when this protocol is
+ instantiated.
+
+ @param reactor: The reactor used in this test.
+
+ @param done: A L{Deferred} which will be fired when the connection is
+ lost.
+ """
+ self.reactor = reactor
+ self._done = done
+
+
+ def connectionLost(self, reason):
+ self.disconnectReason = reason
+ self._done.callback(None)
+ del self._done
+
+
+
+class EndpointCreator:
+ """
+ Create client and server endpoints that know how to connect to each other.
+ """
+
+ def server(self, reactor):
+ """
+ Return an object providing C{IStreamServerEndpoint} for use in creating
+ a server to use to establish the connection type to be tested.
+ """
+ raise NotImplementedError()
+
+
+ def client(self, reactor, serverAddress):
+ """
+ Return an object providing C{IStreamClientEndpoint} for use in creating
+ a client to use to establish the connection type to be tested.
+ """
+ raise NotImplementedError()
+
+
+
+class _SingleProtocolFactory(ClientFactory):
+ """
+ Factory to be used by L{runProtocolsWithReactor}.
+
+ It always returns the same protocol (i.e. is intended for only a single connection).
+ """
+
+ def __init__(self, protocol):
+ self._protocol = protocol
+
+
+ def buildProtocol(self, addr):
+ return self._protocol
+
+
+
+def runProtocolsWithReactor(reactorBuilder, serverProtocol, clientProtocol,
+ endpointCreator):
+ """
+ Connect two protocols using endpoints and a new reactor instance.
+
+ A new reactor will be created and run, with the client and server protocol
+ instances connected to each other using the given endpoint creator. The
+ protocols should run through some set of tests, then disconnect; when both
+ have disconnected the reactor will be stopped and the function will
+ return.
+
+ @param reactorBuilder: A L{ReactorBuilder} instance.
+
+ @param serverProtocol: A L{ConnectableProtocol} that will be the server.
+
+ @param clientProtocol: A L{ConnectableProtocol} that will be the client.
+
+ @param endpointCreator: An instance of L{EndpointCreator}.
+
+ @return: The reactor run by this test.
+ """
+ reactor = reactorBuilder.buildReactor()
+ serverProtocol._setAttributes(reactor, Deferred())
+ clientProtocol._setAttributes(reactor, Deferred())
+ serverFactory = _SingleProtocolFactory(serverProtocol)
+ clientFactory = _SingleProtocolFactory(clientProtocol)
+
+ # Listen on a port:
+ serverEndpoint = endpointCreator.server(reactor)
+ d = serverEndpoint.listen(serverFactory)
+
+ # Connect to the port:
+ def gotPort(p):
+ clientEndpoint = endpointCreator.client(
+ reactor, p.getHost())
+ return clientEndpoint.connect(clientFactory)
+ d.addCallback(gotPort)
+
+ # Stop reactor when both connections are lost:
+ def failed(result):
+ log.err(result, "Connection setup failed.")
+ disconnected = gatherResults([serverProtocol._done, clientProtocol._done])
+ d.addCallback(lambda _: disconnected)
+ d.addErrback(failed)
+ d.addCallback(lambda _: needsRunningReactor(reactor, reactor.stop))
+
+ reactorBuilder.runReactor(reactor)
+ return reactor
+
+
+
+class ReactorBuilder:
+ """
+ L{TestCase} mixin which provides a reactor-creation API. This mixin
+ defines C{setUp} and C{tearDown}, so mix it in before L{TestCase} or call
+ its methods from the overridden ones in the subclass.
+
+ @cvar skippedReactors: A dict mapping FQPN strings of reactors for
+ which the tests defined by this class will be skipped to strings
+ giving the skip message.
+ @cvar requiredInterfaces: A C{list} of interfaces which the reactor must
+ provide or these tests will be skipped. The default, C{None}, means
+ that no interfaces are required.
+ @ivar reactorFactory: A no-argument callable which returns the reactor to
+ use for testing.
+ @ivar originalHandler: The SIGCHLD handler which was installed when setUp
+ ran and which will be re-installed when tearDown runs.
+ @ivar _reactors: A list of FQPN strings giving the reactors for which
+ TestCases will be created.
+ """
+
+ _reactors = [
+ # Select works everywhere
+ "twisted.internet.selectreactor.SelectReactor",
+ ]
+
+ if platform.isWindows():
+ # PortableGtkReactor is only really interesting on Windows,
+ # but not really Windows specific; if you want you can
+ # temporarily move this up to the all-platforms list to test
+ # it on other platforms. It's not there in general because
+ # it's not _really_ worth it to support on other platforms,
+ # since no one really wants to use it on other platforms.
+ _reactors.extend([
+ "twisted.internet.gtk2reactor.PortableGtkReactor",
+ "twisted.internet.gireactor.PortableGIReactor",
+ "twisted.internet.gtk3reactor.PortableGtk3Reactor",
+ "twisted.internet.win32eventreactor.Win32Reactor",
+ "twisted.internet.iocpreactor.reactor.IOCPReactor"])
+ else:
+ _reactors.extend([
+ "twisted.internet.glib2reactor.Glib2Reactor",
+ "twisted.internet.gtk2reactor.Gtk2Reactor",
+ "twisted.internet.gireactor.GIReactor",
+ "twisted.internet.gtk3reactor.Gtk3Reactor"])
+ if platform.isMacOSX():
+ _reactors.append("twisted.internet.cfreactor.CFReactor")
+ else:
+ _reactors.extend([
+ "twisted.internet.pollreactor.PollReactor",
+ "twisted.internet.epollreactor.EPollReactor"])
+ if not platform.isLinux():
+ # Presumably Linux is not going to start supporting kqueue, so
+ # skip even trying this configuration.
+ _reactors.extend([
+ # Support KQueue on non-OS-X POSIX platforms for now.
+ "twisted.internet.kqreactor.KQueueReactor",
+ ])
+
+ reactorFactory = None
+ originalHandler = None
+ requiredInterfaces = None
+ skippedReactors = {}
+
+ def setUp(self):
+ """
+ Clear the SIGCHLD handler, if there is one, to ensure an environment
+ like the one which exists prior to a call to L{reactor.run}.
+ """
+ if not platform.isWindows():
+ self.originalHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+
+
+ def tearDown(self):
+ """
+ Restore the original SIGCHLD handler and reap processes as long as
+ there seem to be any remaining.
+ """
+ if self.originalHandler is not None:
+ signal.signal(signal.SIGCHLD, self.originalHandler)
+ if process is not None:
+ begin = time.time()
+ while process.reapProcessHandlers:
+ log.msg(
+ "ReactorBuilder.tearDown reaping some processes %r" % (
+ process.reapProcessHandlers,))
+ process.reapAllProcesses()
+
+ # The process should exit on its own. However, if it
+ # doesn't, we're stuck in this loop forever. To avoid
+ # hanging the test suite, eventually give the process some
+ # help exiting and move on.
+ time.sleep(0.001)
+ if time.time() - begin > 60:
+ for pid in process.reapProcessHandlers:
+ os.kill(pid, signal.SIGKILL)
+ raise Exception(
+ "Timeout waiting for child processes to exit: %r" % (
+ process.reapProcessHandlers,))
+
+
+ def unbuildReactor(self, reactor):
+ """
+ Clean up any resources which may have been allocated for the given
+ reactor by its creation or by a test which used it.
+ """
+ # Chris says:
+ #
+ # XXX These explicit calls to clean up the waker (and any other
+ # internal readers) should become obsolete when bug #3063 is
+ # fixed. -radix, 2008-02-29. Fortunately it should probably cause an
+ # error when bug #3063 is fixed, so it should be removed in the same
+ # branch that fixes it.
+ #
+ # -exarkun
+ reactor._uninstallHandler()
+ if getattr(reactor, '_internalReaders', None) is not None:
+ for reader in reactor._internalReaders:
+ reactor.removeReader(reader)
+ reader.connectionLost(None)
+ reactor._internalReaders.clear()
+
+ # Here's an extra thing unrelated to wakers but necessary for
+ # cleaning up after the reactors we make. -exarkun
+ reactor.disconnectAll()
+
+ # It would also be bad if any timed calls left over were allowed to
+ # run.
+ calls = reactor.getDelayedCalls()
+ for c in calls:
+ c.cancel()
+
+
+ def buildReactor(self):
+ """
+ Create and return a reactor using C{self.reactorFactory}.
+ """
+ try:
+ from twisted.internet.cfreactor import CFReactor
+ from twisted.internet import reactor as globalReactor
+ except ImportError:
+ pass
+ else:
+ if (isinstance(globalReactor, CFReactor)
+ and self.reactorFactory is CFReactor):
+ raise SkipTest(
+ "CFReactor uses APIs which manipulate global state, "
+ "so it's not safe to run its own reactor-builder tests "
+ "under itself")
+ try:
+ reactor = self.reactorFactory()
+ except:
+ # Unfortunately, not all errors which result in a reactor
+ # being unusable are detectable without actually
+ # instantiating the reactor. So we catch some more here
+ # and skip the test if necessary. We also log it to aid
+ # with debugging, but flush the logged error so the test
+ # doesn't fail.
+ log.err(None, "Failed to install reactor")
+ self.flushLoggedErrors()
+ raise SkipTest(Failure().getErrorMessage())
+ else:
+ if self.requiredInterfaces is not None:
+ missing = filter(
+ lambda required: not required.providedBy(reactor),
+ self.requiredInterfaces)
+ if missing:
+ self.unbuildReactor(reactor)
+ raise SkipTest("%s does not provide %s" % (
+ fullyQualifiedName(reactor.__class__),
+ ",".join([fullyQualifiedName(x) for x in missing])))
+ self.addCleanup(self.unbuildReactor, reactor)
+ return reactor
+
+
+ def runReactor(self, reactor, timeout=None):
+ """
+ Run the reactor for at most the given amount of time.
+
+ @param reactor: The reactor to run.
+
+ @type timeout: C{int} or C{float}
+ @param timeout: The maximum amount of time, specified in seconds, to
+ allow the reactor to run. If the reactor is still running after
+ this much time has elapsed, it will be stopped and an exception
+ raised. If C{None}, the default test method timeout imposed by
+ Trial will be used. This depends on the L{IReactorTime}
+ implementation of C{reactor} for correct operation.
+
+ @raise TimeoutError: If the reactor is still running after C{timeout}
+ seconds.
+ """
+ if timeout is None:
+ timeout = self.getTimeout()
+
+ timedOut = []
+ def stop():
+ timedOut.append(None)
+ reactor.stop()
+
+ reactor.callLater(timeout, stop)
+ reactor.run()
+ if timedOut:
+ raise TimeoutError(
+ "reactor still running after %s seconds" % (timeout,))
+
+
+ def makeTestCaseClasses(cls):
+ """
+ Create a L{TestCase} subclass which mixes in C{cls} for each known
+ reactor and return a dict mapping their names to them.
+ """
+ classes = {}
+ for reactor in cls._reactors:
+ shortReactorName = reactor.split(".")[-1]
+ name = (cls.__name__ + "." + shortReactorName).replace(".", "_")
+ class testcase(cls, TestCase):
+ __module__ = cls.__module__
+ if reactor in cls.skippedReactors:
+ skip = cls.skippedReactors[reactor]
+ try:
+ reactorFactory = namedAny(reactor)
+ except:
+ skip = Failure().getErrorMessage()
+ testcase.__name__ = name
+ classes[testcase.__name__] = testcase
+ return classes
+ makeTestCaseClasses = classmethod(makeTestCaseClasses)
+
+
+__all__ = ['ReactorBuilder']
diff --git a/twisted/internet/test/test_abstract.py b/twisted/internet/test/test_abstract.py
new file mode 100644
index 0000000..5e9c0cf
--- /dev/null
+++ b/twisted/internet/test/test_abstract.py
@@ -0,0 +1,56 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.abstract}, a collection of APIs for implementing
+reactors.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.internet.abstract import isIPv6Address
+
+class IPv6AddressTests(TestCase):
+ """
+ Tests for L{isIPv6Address}, a function for determining if a particular
+ string is an IPv6 address literal.
+ """
+ def test_empty(self):
+ """
+ The empty string is not an IPv6 address literal.
+ """
+ self.assertFalse(isIPv6Address(""))
+
+
+ def test_colon(self):
+ """
+ A single C{":"} is not an IPv6 address literal.
+ """
+ self.assertFalse(isIPv6Address(":"))
+
+
+ def test_loopback(self):
+ """
+ C{"::1"} is the IPv6 loopback address literal.
+ """
+ self.assertTrue(isIPv6Address("::1"))
+
+
+ def test_scopeID(self):
+ """
+ An otherwise valid IPv6 address literal may also include a C{"%"}
+ followed by an arbitrary scope identifier.
+ """
+ self.assertTrue(isIPv6Address("fe80::1%eth0"))
+ self.assertTrue(isIPv6Address("fe80::2%1"))
+ self.assertTrue(isIPv6Address("fe80::3%en2"))
+
+
+ def test_invalidWithScopeID(self):
+ """
+ An otherwise invalid IPv6 address literal is still invalid with a
+ trailing scope identifier.
+ """
+ self.assertFalse(isIPv6Address("%eth0"))
+ self.assertFalse(isIPv6Address(":%eth0"))
+ self.assertFalse(isIPv6Address("hello%eth0"))
diff --git a/twisted/internet/test/test_address.py b/twisted/internet/test/test_address.py
new file mode 100644
index 0000000..5007e3a
--- /dev/null
+++ b/twisted/internet/test/test_address.py
@@ -0,0 +1,292 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import re
+import os
+
+from twisted.trial import unittest
+from twisted.internet.address import IPv4Address, UNIXAddress
+
+try:
+ os.symlink
+except AttributeError:
+ symlinkSkip = "Platform does not support symlinks"
+else:
+ symlinkSkip = None
+
+
+class AddressTestCaseMixin(object):
+ def test_addressComparison(self):
+ """
+ Two different address instances, sharing the same properties are
+ considered equal by C{==} and not considered not equal by C{!=}.
+
+ Note: When applied via UNIXAddress class, this uses the same
+ filename for both objects being compared.
+ """
+ self.assertTrue(self.buildAddress() == self.buildAddress())
+ self.assertFalse(self.buildAddress() != self.buildAddress())
+
+
+ def _stringRepresentation(self, stringFunction):
+ """
+ Verify that the string representation of an address object conforms to a
+ simple pattern (the usual one for Python object reprs) and contains
+ values which accurately reflect the attributes of the address.
+ """
+ addr = self.buildAddress()
+ pattern = "".join([
+ "^",
+ "([^\(]+Address)", # class name,
+ "\(", # opening bracket,
+ "([^)]+)", # arguments,
+ "\)", # closing bracket,
+ "$"
+ ])
+ stringValue = stringFunction(addr)
+ m = re.match(pattern, stringValue)
+ self.assertNotEquals(
+ None, m,
+ "%s does not match the standard __str__ pattern "
+ "ClassName(arg1, arg2, etc)" % (stringValue,))
+ self.assertEqual(addr.__class__.__name__, m.group(1))
+
+ args = [x.strip() for x in m.group(2).split(",")]
+ self.assertEqual(
+ args,
+ [argSpec[1] % (getattr(addr, argSpec[0]),) for argSpec in self.addressArgSpec])
+
+
+ def test_str(self):
+ """
+ C{str} can be used to get a string representation of an address instance
+ containing information about that address.
+ """
+ self._stringRepresentation(str)
+
+
+ def test_repr(self):
+ """
+ C{repr} can be used to get a string representation of an address
+ instance containing information about that address.
+ """
+ self._stringRepresentation(repr)
+
+
+ def test_hash(self):
+ """
+ C{__hash__} can be used to get a hash of an address, allowing
+ addresses to be used as keys in dictionaries, for instance.
+ """
+ addr = self.buildAddress()
+ d = {addr: True}
+ self.assertTrue(d[self.buildAddress()])
+
+
+ def test_differentNamesComparison(self):
+ """
+ Check that comparison operators work correctly on address objects
+ when a different name is passed in
+ """
+ self.assertFalse(self.buildAddress() == self.buildDifferentAddress())
+ self.assertFalse(self.buildDifferentAddress() == self.buildAddress())
+
+ self.assertTrue(self.buildAddress() != self.buildDifferentAddress())
+ self.assertTrue(self.buildDifferentAddress() != self.buildAddress())
+
+
+ def assertDeprecations(self, testMethod, message):
+ """
+ Assert that the a DeprecationWarning with the given message was
+ emitted against the given method.
+ """
+ warnings = self.flushWarnings([testMethod])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(warnings[0]['message'], message)
+ self.assertEqual(len(warnings), 1)
+
+
+
+class IPv4AddressTestCaseMixin(AddressTestCaseMixin):
+ addressArgSpec = (("type", "%s"), ("host", "%r"), ("port", "%d"))
+
+
+
+class IPv4AddressTCPTestCase(unittest.TestCase, IPv4AddressTestCaseMixin):
+ def buildAddress(self):
+ """
+ Create an arbitrary new L{IPv4Address} instance with a C{"TCP"}
+ type. A new instance is created for each call, but always for the
+ same address.
+ """
+ return IPv4Address("TCP", "127.0.0.1", 0)
+
+
+ def buildDifferentAddress(self):
+ """
+ Like L{buildAddress}, but with a different fixed address.
+ """
+ return IPv4Address("TCP", "127.0.0.2", 0)
+
+
+ def test_bwHackDeprecation(self):
+ """
+ If a value is passed for the C{_bwHack} parameter to L{IPv4Address},
+ a deprecation warning is emitted.
+ """
+ # Construct this for warning side-effects, disregard the actual object.
+ IPv4Address("TCP", "127.0.0.3", 0, _bwHack="TCP")
+
+ message = (
+ "twisted.internet.address.IPv4Address._bwHack is deprecated "
+ "since Twisted 11.0")
+ return self.assertDeprecations(self.test_bwHackDeprecation, message)
+
+
+
+class IPv4AddressUDPTestCase(unittest.TestCase, IPv4AddressTestCaseMixin):
+ def buildAddress(self):
+ """
+ Create an arbitrary new L{IPv4Address} instance with a C{"UDP"}
+ type. A new instance is created for each call, but always for the
+ same address.
+ """
+ return IPv4Address("UDP", "127.0.0.1", 0)
+
+
+ def buildDifferentAddress(self):
+ """
+ Like L{buildAddress}, but with a different fixed address.
+ """
+ return IPv4Address("UDP", "127.0.0.2", 0)
+
+
+ def test_bwHackDeprecation(self):
+ """
+ If a value is passed for the C{_bwHack} parameter to L{IPv4Address},
+ a deprecation warning is emitted.
+ """
+ # Construct this for warning side-effects, disregard the actual object.
+ IPv4Address("UDP", "127.0.0.3", 0, _bwHack="UDP")
+
+ message = (
+ "twisted.internet.address.IPv4Address._bwHack is deprecated "
+ "since Twisted 11.0")
+ return self.assertDeprecations(self.test_bwHackDeprecation, message)
+
+
+
+class UNIXAddressTestCase(unittest.TestCase, AddressTestCaseMixin):
+ addressArgSpec = (("name", "%r"),)
+
+ def setUp(self):
+ self._socketAddress = self.mktemp()
+ self._otherAddress = self.mktemp()
+
+
+ def buildAddress(self):
+ """
+ Create an arbitrary new L{UNIXAddress} instance. A new instance is
+ created for each call, but always for the same address.
+ """
+ return UNIXAddress(self._socketAddress)
+
+
+ def buildDifferentAddress(self):
+ """
+ Like L{buildAddress}, but with a different fixed address.
+ """
+ return UNIXAddress(self._otherAddress)
+
+
+ def test_comparisonOfLinkedFiles(self):
+ """
+ UNIXAddress objects compare as equal if they link to the same file.
+ """
+ linkName = self.mktemp()
+ self.fd = open(self._socketAddress, 'w')
+ os.symlink(os.path.abspath(self._socketAddress), linkName)
+ self.assertTrue(
+ UNIXAddress(self._socketAddress) == UNIXAddress(linkName))
+ self.assertTrue(
+ UNIXAddress(linkName) == UNIXAddress(self._socketAddress))
+ test_comparisonOfLinkedFiles.skip = symlinkSkip
+
+
+ def test_hashOfLinkedFiles(self):
+ """
+ UNIXAddress Objects that compare as equal have the same hash value.
+ """
+ linkName = self.mktemp()
+ self.fd = open(self._socketAddress, 'w')
+ os.symlink(os.path.abspath(self._socketAddress), linkName)
+ self.assertEqual(
+ hash(UNIXAddress(self._socketAddress)), hash(UNIXAddress(linkName)))
+ test_hashOfLinkedFiles.skip = symlinkSkip
+
+
+ def test_bwHackDeprecation(self):
+ """
+ If a value is passed for the C{_bwHack} parameter to L{UNIXAddress},
+ a deprecation warning is emitted.
+ """
+ # Construct this for warning side-effects, disregard the actual object.
+ UNIXAddress(self.mktemp(), _bwHack='UNIX')
+
+ message = (
+ "twisted.internet.address.UNIXAddress._bwHack is deprecated "
+ "since Twisted 11.0")
+ return self.assertDeprecations(self.test_bwHackDeprecation, message)
+
+
+
+class EmptyUNIXAddressTestCase(unittest.TestCase, AddressTestCaseMixin):
+ """
+ Tests for L{UNIXAddress} operations involving a C{None} address.
+ """
+ addressArgSpec = (("name", "%r"),)
+
+ def setUp(self):
+ self._socketAddress = self.mktemp()
+
+
+ def buildAddress(self):
+ """
+ Create an arbitrary new L{UNIXAddress} instance. A new instance is
+ created for each call, but always for the same address.
+ """
+ return UNIXAddress(self._socketAddress)
+
+
+ def buildDifferentAddress(self):
+ """
+ Like L{buildAddress}, but with a fixed address of C{None}.
+ """
+ return UNIXAddress(None)
+
+
+ def test_comparisonOfLinkedFiles(self):
+ """
+ A UNIXAddress referring to a C{None} address does not compare equal to a
+ UNIXAddress referring to a symlink.
+ """
+ linkName = self.mktemp()
+ self.fd = open(self._socketAddress, 'w')
+ os.symlink(os.path.abspath(self._socketAddress), linkName)
+ self.assertTrue(
+ UNIXAddress(self._socketAddress) != UNIXAddress(None))
+ self.assertTrue(
+ UNIXAddress(None) != UNIXAddress(self._socketAddress))
+ test_comparisonOfLinkedFiles.skip = symlinkSkip
+
+
+ def test_emptyHash(self):
+ """
+ C{__hash__} can be used to get a hash of an address, even one referring
+ to C{None} rather than a real path.
+ """
+ addr = self.buildDifferentAddress()
+ d = {addr: True}
+ self.assertTrue(d[self.buildDifferentAddress()])
+
+
diff --git a/twisted/internet/test/test_base.py b/twisted/internet/test/test_base.py
new file mode 100644
index 0000000..6cab8fa
--- /dev/null
+++ b/twisted/internet/test/test_base.py
@@ -0,0 +1,272 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.base}.
+"""
+
+import socket
+from Queue import Queue
+
+from zope.interface import implements
+
+from twisted.python.threadpool import ThreadPool
+from twisted.python.util import setIDFunction
+from twisted.internet.interfaces import IReactorTime, IReactorThreads
+from twisted.internet.error import DNSLookupError
+from twisted.internet.base import ThreadedResolver, DelayedCall
+from twisted.internet.task import Clock
+from twisted.trial.unittest import TestCase
+
+
+class FakeReactor(object):
+ """
+ A fake reactor implementation which just supports enough reactor APIs for
+ L{ThreadedResolver}.
+ """
+ implements(IReactorTime, IReactorThreads)
+
+ def __init__(self):
+ self._clock = Clock()
+ self.callLater = self._clock.callLater
+
+ self._threadpool = ThreadPool()
+ self._threadpool.start()
+ self.getThreadPool = lambda: self._threadpool
+
+ self._threadCalls = Queue()
+
+
+ def callFromThread(self, f, *args, **kwargs):
+ self._threadCalls.put((f, args, kwargs))
+
+
+ def _runThreadCalls(self):
+ f, args, kwargs = self._threadCalls.get()
+ f(*args, **kwargs)
+
+
+ def _stop(self):
+ self._threadpool.stop()
+
+
+
+class ThreadedResolverTests(TestCase):
+ """
+ Tests for L{ThreadedResolver}.
+ """
+ def test_success(self):
+ """
+ L{ThreadedResolver.getHostByName} returns a L{Deferred} which fires
+ with the value returned by the call to L{socket.gethostbyname} in the
+ threadpool of the reactor passed to L{ThreadedResolver.__init__}.
+ """
+ ip = "10.0.0.17"
+ name = "foo.bar.example.com"
+ timeout = 30
+
+ reactor = FakeReactor()
+ self.addCleanup(reactor._stop)
+
+ lookedUp = []
+ resolvedTo = []
+ def fakeGetHostByName(name):
+ lookedUp.append(name)
+ return ip
+
+ self.patch(socket, 'gethostbyname', fakeGetHostByName)
+
+ resolver = ThreadedResolver(reactor)
+ d = resolver.getHostByName(name, (timeout,))
+ d.addCallback(resolvedTo.append)
+
+ reactor._runThreadCalls()
+
+ self.assertEqual(lookedUp, [name])
+ self.assertEqual(resolvedTo, [ip])
+
+ # Make sure that any timeout-related stuff gets cleaned up.
+ reactor._clock.advance(timeout + 1)
+ self.assertEqual(reactor._clock.calls, [])
+
+
+ def test_failure(self):
+ """
+ L{ThreadedResolver.getHostByName} returns a L{Deferred} which fires a
+ L{Failure} if the call to L{socket.gethostbyname} raises an exception.
+ """
+ timeout = 30
+
+ reactor = FakeReactor()
+ self.addCleanup(reactor._stop)
+
+ def fakeGetHostByName(name):
+ raise IOError("ENOBUFS (this is a funny joke)")
+
+ self.patch(socket, 'gethostbyname', fakeGetHostByName)
+
+ failedWith = []
+ resolver = ThreadedResolver(reactor)
+ d = resolver.getHostByName("some.name", (timeout,))
+ self.assertFailure(d, DNSLookupError)
+ d.addCallback(failedWith.append)
+
+ reactor._runThreadCalls()
+
+ self.assertEqual(len(failedWith), 1)
+
+ # Make sure that any timeout-related stuff gets cleaned up.
+ reactor._clock.advance(timeout + 1)
+ self.assertEqual(reactor._clock.calls, [])
+
+
+ def test_timeout(self):
+ """
+ If L{socket.gethostbyname} does not complete before the specified
+ timeout elapsed, the L{Deferred} returned by
+ L{ThreadedResolver.getHostByBame} fails with L{DNSLookupError}.
+ """
+ timeout = 10
+
+ reactor = FakeReactor()
+ self.addCleanup(reactor._stop)
+
+ result = Queue()
+ def fakeGetHostByName(name):
+ raise result.get()
+
+ self.patch(socket, 'gethostbyname', fakeGetHostByName)
+
+ failedWith = []
+ resolver = ThreadedResolver(reactor)
+ d = resolver.getHostByName("some.name", (timeout,))
+ self.assertFailure(d, DNSLookupError)
+ d.addCallback(failedWith.append)
+
+ reactor._clock.advance(timeout - 1)
+ self.assertEqual(failedWith, [])
+ reactor._clock.advance(1)
+ self.assertEqual(len(failedWith), 1)
+
+ # Eventually the socket.gethostbyname does finish - in this case, with
+ # an exception. Nobody cares, though.
+ result.put(IOError("The I/O was errorful"))
+
+
+
+class DelayedCallTests(TestCase):
+ """
+ Tests for L{DelayedCall}.
+ """
+ def _getDelayedCallAt(self, time):
+ """
+ Get a L{DelayedCall} instance at a given C{time}.
+
+ @param time: The absolute time at which the returned L{DelayedCall}
+ will be scheduled.
+ """
+ def noop(call):
+ pass
+ return DelayedCall(time, lambda: None, (), {}, noop, noop, None)
+
+
+ def setUp(self):
+ """
+ Create two L{DelayedCall} instanced scheduled to run at different
+ times.
+ """
+ self.zero = self._getDelayedCallAt(0)
+ self.one = self._getDelayedCallAt(1)
+
+
+ def test_str(self):
+ """
+ The string representation of a L{DelayedCall} instance, as returned by
+ C{str}, includes the unsigned id of the instance, as well as its state,
+ the function to be called, and the function arguments.
+ """
+ def nothing():
+ pass
+ dc = DelayedCall(12, nothing, (3, ), {"A": 5}, None, None, lambda: 1.5)
+ ids = {dc: 200}
+ def fakeID(obj):
+ try:
+ return ids[obj]
+ except (TypeError, KeyError):
+ return id(obj)
+ self.addCleanup(setIDFunction, setIDFunction(fakeID))
+ self.assertEqual(
+ str(dc),
+ "<DelayedCall 0xc8 [10.5s] called=0 cancelled=0 nothing(3, A=5)>")
+
+
+ def test_lt(self):
+ """
+ For two instances of L{DelayedCall} C{a} and C{b}, C{a < b} is true
+ if and only if C{a} is scheduled to run before C{b}.
+ """
+ zero, one = self.zero, self.one
+ self.assertTrue(zero < one)
+ self.assertFalse(one < zero)
+ self.assertFalse(zero < zero)
+ self.assertFalse(one < one)
+
+
+ def test_le(self):
+ """
+ For two instances of L{DelayedCall} C{a} and C{b}, C{a <= b} is true
+ if and only if C{a} is scheduled to run before C{b} or at the same
+ time as C{b}.
+ """
+ zero, one = self.zero, self.one
+ self.assertTrue(zero <= one)
+ self.assertFalse(one <= zero)
+ self.assertTrue(zero <= zero)
+ self.assertTrue(one <= one)
+
+
+ def test_gt(self):
+ """
+ For two instances of L{DelayedCall} C{a} and C{b}, C{a > b} is true
+ if and only if C{a} is scheduled to run after C{b}.
+ """
+ zero, one = self.zero, self.one
+ self.assertTrue(one > zero)
+ self.assertFalse(zero > one)
+ self.assertFalse(zero > zero)
+ self.assertFalse(one > one)
+
+
+ def test_ge(self):
+ """
+ For two instances of L{DelayedCall} C{a} and C{b}, C{a > b} is true
+ if and only if C{a} is scheduled to run after C{b} or at the same
+ time as C{b}.
+ """
+ zero, one = self.zero, self.one
+ self.assertTrue(one >= zero)
+ self.assertFalse(zero >= one)
+ self.assertTrue(zero >= zero)
+ self.assertTrue(one >= one)
+
+
+ def test_eq(self):
+ """
+ A L{DelayedCall} instance is only equal to itself.
+ """
+ # Explicitly use == here, instead of assertEqual, to be more
+ # confident __eq__ is being tested.
+ self.assertFalse(self.zero == self.one)
+ self.assertTrue(self.zero == self.zero)
+ self.assertTrue(self.one == self.one)
+
+
+ def test_ne(self):
+ """
+ A L{DelayedCall} instance is not equal to any other object.
+ """
+ # Explicitly use != here, instead of assertEqual, to be more
+ # confident __ne__ is being tested.
+ self.assertTrue(self.zero != self.one)
+ self.assertFalse(self.zero != self.zero)
+ self.assertFalse(self.one != self.one)
diff --git a/twisted/internet/test/test_baseprocess.py b/twisted/internet/test/test_baseprocess.py
new file mode 100644
index 0000000..750b660
--- /dev/null
+++ b/twisted/internet/test/test_baseprocess.py
@@ -0,0 +1,73 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet._baseprocess} which implements process-related
+functionality that is useful in all platforms supporting L{IReactorProcess}.
+"""
+
+__metaclass__ = type
+
+from twisted.python.deprecate import getWarningMethod, setWarningMethod
+from twisted.trial.unittest import TestCase
+from twisted.internet._baseprocess import BaseProcess
+
+
+class BaseProcessTests(TestCase):
+ """
+ Tests for L{BaseProcess}, a parent class for other classes which represent
+ processes which implements functionality common to many different process
+ implementations.
+ """
+ def test_callProcessExited(self):
+ """
+ L{BaseProcess._callProcessExited} calls the C{processExited} method of
+ its C{proto} attribute and passes it a L{Failure} wrapping the given
+ exception.
+ """
+ class FakeProto:
+ reason = None
+
+ def processExited(self, reason):
+ self.reason = reason
+
+ reason = RuntimeError("fake reason")
+ process = BaseProcess(FakeProto())
+ process._callProcessExited(reason)
+ process.proto.reason.trap(RuntimeError)
+ self.assertIdentical(reason, process.proto.reason.value)
+
+
+ def test_callProcessExitedMissing(self):
+ """
+ L{BaseProcess._callProcessExited} emits a L{DeprecationWarning} if the
+ object referred to by its C{proto} attribute has no C{processExited}
+ method.
+ """
+ class FakeProto:
+ pass
+
+ reason = object()
+ process = BaseProcess(FakeProto())
+
+ self.addCleanup(setWarningMethod, getWarningMethod())
+ warnings = []
+ def collect(message, category, stacklevel):
+ warnings.append((message, category, stacklevel))
+ setWarningMethod(collect)
+
+ process._callProcessExited(reason)
+
+ [(message, category, stacklevel)] = warnings
+ self.assertEqual(
+ message,
+ "Since Twisted 8.2, IProcessProtocol.processExited is required. "
+ "%s.%s must implement it." % (
+ FakeProto.__module__, FakeProto.__name__))
+ self.assertIdentical(category, DeprecationWarning)
+ # The stacklevel doesn't really make sense for this kind of
+ # deprecation. Requiring it to be 0 will at least avoid pointing to
+ # any part of Twisted or a random part of the application's code, which
+ # I think would be more misleading than having it point inside the
+ # warning system itself. -exarkun
+ self.assertEqual(stacklevel, 0)
diff --git a/twisted/internet/test/test_core.py b/twisted/internet/test/test_core.py
new file mode 100644
index 0000000..6b2161f
--- /dev/null
+++ b/twisted/internet/test/test_core.py
@@ -0,0 +1,331 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorCore}.
+"""
+
+__metaclass__ = type
+
+import signal
+import time
+import inspect
+
+from twisted.internet.abstract import FileDescriptor
+from twisted.internet.error import ReactorAlreadyRunning, ReactorNotRestartable
+from twisted.internet.defer import Deferred
+from twisted.internet.test.reactormixins import ReactorBuilder
+
+
+class ObjectModelIntegrationMixin(object):
+ """
+ Helpers for tests about the object model of reactor-related objects.
+ """
+ def assertFullyNewStyle(self, instance):
+ """
+ Assert that the given object is an instance of a new-style class and
+ that there are no classic classes in the inheritance hierarchy of
+ that class.
+
+ This is a beneficial condition because PyPy is better able to
+ optimize attribute lookup on such classes.
+ """
+ self.assertIsInstance(instance, object)
+ mro = inspect.getmro(type(instance))
+ for subclass in mro:
+ self.assertTrue(
+ issubclass(subclass, object),
+ "%r is not new-style" % (subclass,))
+
+
+
+class ObjectModelIntegrationTest(ReactorBuilder, ObjectModelIntegrationMixin):
+ """
+ Test details of object model integration against all reactors.
+ """
+
+ def test_newstyleReactor(self):
+ """
+ Checks that all reactors on a platform have method resolution order
+ containing only new style classes.
+ """
+ reactor = self.buildReactor()
+ self.assertFullyNewStyle(reactor)
+
+
+
+class SystemEventTestsBuilder(ReactorBuilder):
+ """
+ Builder defining tests relating to L{IReactorCore.addSystemEventTrigger}
+ and L{IReactorCore.fireSystemEvent}.
+ """
+ def test_stopWhenNotStarted(self):
+ """
+ C{reactor.stop()} raises L{RuntimeError} when called when the reactor
+ has not been started.
+ """
+ reactor = self.buildReactor()
+ self.assertRaises(RuntimeError, reactor.stop)
+
+
+ def test_stopWhenAlreadyStopped(self):
+ """
+ C{reactor.stop()} raises L{RuntimeError} when called after the reactor
+ has been stopped.
+ """
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertRaises(RuntimeError, reactor.stop)
+
+
+ def test_callWhenRunningOrder(self):
+ """
+ Functions are run in the order that they were passed to
+ L{reactor.callWhenRunning}.
+ """
+ reactor = self.buildReactor()
+ events = []
+ reactor.callWhenRunning(events.append, "first")
+ reactor.callWhenRunning(events.append, "second")
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertEqual(events, ["first", "second"])
+
+
+ def test_runningForStartupEvents(self):
+ """
+ The reactor is not running when C{"before"} C{"startup"} triggers are
+ called and is running when C{"during"} and C{"after"} C{"startup"}
+ triggers are called.
+ """
+ reactor = self.buildReactor()
+ state = {}
+ def beforeStartup():
+ state['before'] = reactor.running
+ def duringStartup():
+ state['during'] = reactor.running
+ def afterStartup():
+ state['after'] = reactor.running
+ reactor.addSystemEventTrigger("before", "startup", beforeStartup)
+ reactor.addSystemEventTrigger("during", "startup", duringStartup)
+ reactor.addSystemEventTrigger("after", "startup", afterStartup)
+ reactor.callWhenRunning(reactor.stop)
+ self.assertEqual(state, {})
+ self.runReactor(reactor)
+ self.assertEqual(
+ state,
+ {"before": False,
+ "during": True,
+ "after": True})
+
+
+ def test_signalHandlersInstalledDuringStartup(self):
+ """
+ Signal handlers are installed in responsed to the C{"during"}
+ C{"startup"}.
+ """
+ reactor = self.buildReactor()
+ phase = [None]
+ def beforeStartup():
+ phase[0] = "before"
+ def afterStartup():
+ phase[0] = "after"
+ reactor.addSystemEventTrigger("before", "startup", beforeStartup)
+ reactor.addSystemEventTrigger("after", "startup", afterStartup)
+
+ sawPhase = []
+ def fakeSignal(signum, action):
+ sawPhase.append(phase[0])
+ self.patch(signal, 'signal', fakeSignal)
+ reactor.callWhenRunning(reactor.stop)
+ self.assertEqual(phase[0], None)
+ self.assertEqual(sawPhase, [])
+ self.runReactor(reactor)
+ self.assertIn("before", sawPhase)
+ self.assertEqual(phase[0], "after")
+
+
+ def test_stopShutDownEvents(self):
+ """
+ C{reactor.stop()} fires all three phases of shutdown event triggers
+ before it makes C{reactor.run()} return.
+ """
+ reactor = self.buildReactor()
+ events = []
+ reactor.addSystemEventTrigger(
+ "before", "shutdown",
+ lambda: events.append(("before", "shutdown")))
+ reactor.addSystemEventTrigger(
+ "during", "shutdown",
+ lambda: events.append(("during", "shutdown")))
+ reactor.addSystemEventTrigger(
+ "after", "shutdown",
+ lambda: events.append(("after", "shutdown")))
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertEqual(events, [("before", "shutdown"),
+ ("during", "shutdown"),
+ ("after", "shutdown")])
+
+
+ def test_shutdownFiresTriggersAsynchronously(self):
+ """
+ C{"before"} C{"shutdown"} triggers are not run synchronously from
+ L{reactor.stop}.
+ """
+ reactor = self.buildReactor()
+ events = []
+ reactor.addSystemEventTrigger(
+ "before", "shutdown", events.append, "before shutdown")
+ def stopIt():
+ reactor.stop()
+ events.append("stopped")
+ reactor.callWhenRunning(stopIt)
+ self.assertEqual(events, [])
+ self.runReactor(reactor)
+ self.assertEqual(events, ["stopped", "before shutdown"])
+
+
+ def test_shutdownDisconnectsCleanly(self):
+ """
+ A L{IFileDescriptor.connectionLost} implementation which raises an
+ exception does not prevent the remaining L{IFileDescriptor}s from
+ having their C{connectionLost} method called.
+ """
+ lostOK = [False]
+
+ # Subclass FileDescriptor to get logPrefix
+ class ProblematicFileDescriptor(FileDescriptor):
+ def connectionLost(self, reason):
+ raise RuntimeError("simulated connectionLost error")
+
+ class OKFileDescriptor(FileDescriptor):
+ def connectionLost(self, reason):
+ lostOK[0] = True
+
+ reactor = self.buildReactor()
+
+ # Unfortunately, it is necessary to patch removeAll to directly control
+ # the order of the returned values. The test is only valid if
+ # ProblematicFileDescriptor comes first. Also, return these
+ # descriptors only the first time removeAll is called so that if it is
+ # called again the file descriptors aren't re-disconnected.
+ fds = iter([ProblematicFileDescriptor(), OKFileDescriptor()])
+ reactor.removeAll = lambda: fds
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
+ self.assertTrue(lostOK[0])
+
+
+ def test_multipleRun(self):
+ """
+ C{reactor.run()} raises L{ReactorAlreadyRunning} when called when
+ the reactor is already running.
+ """
+ events = []
+ def reentrantRun():
+ self.assertRaises(ReactorAlreadyRunning, reactor.run)
+ events.append("tested")
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(reentrantRun)
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertEqual(events, ["tested"])
+
+
+ def test_runWithAsynchronousBeforeStartupTrigger(self):
+ """
+ When there is a C{'before'} C{'startup'} trigger which returns an
+ unfired L{Deferred}, C{reactor.run()} starts the reactor and does not
+ return until after C{reactor.stop()} is called
+ """
+ events = []
+ def trigger():
+ events.append('trigger')
+ d = Deferred()
+ d.addCallback(callback)
+ reactor.callLater(0, d.callback, None)
+ return d
+ def callback(ignored):
+ events.append('callback')
+ reactor.stop()
+ reactor = self.buildReactor()
+ reactor.addSystemEventTrigger('before', 'startup', trigger)
+ self.runReactor(reactor)
+ self.assertEqual(events, ['trigger', 'callback'])
+
+
+ def test_iterate(self):
+ """
+ C{reactor.iterate()} does not block.
+ """
+ reactor = self.buildReactor()
+ t = reactor.callLater(5, reactor.crash)
+
+ start = time.time()
+ reactor.iterate(0) # Shouldn't block
+ elapsed = time.time() - start
+
+ self.failUnless(elapsed < 2)
+ t.cancel()
+
+
+ def test_crash(self):
+ """
+ C{reactor.crash()} stops the reactor and does not fire shutdown
+ triggers.
+ """
+ reactor = self.buildReactor()
+ events = []
+ reactor.addSystemEventTrigger(
+ "before", "shutdown",
+ lambda: events.append(("before", "shutdown")))
+ reactor.callWhenRunning(reactor.callLater, 0, reactor.crash)
+ self.runReactor(reactor)
+ self.assertFalse(reactor.running)
+ self.assertFalse(
+ events,
+ "Shutdown triggers invoked but they should not have been.")
+
+
+ def test_runAfterCrash(self):
+ """
+ C{reactor.run()} restarts the reactor after it has been stopped by
+ C{reactor.crash()}.
+ """
+ events = []
+ def crash():
+ events.append('crash')
+ reactor.crash()
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(crash)
+ self.runReactor(reactor)
+ def stop():
+ events.append(('stop', reactor.running))
+ reactor.stop()
+ reactor.callWhenRunning(stop)
+ self.runReactor(reactor)
+ self.assertEqual(events, ['crash', ('stop', True)])
+
+
+ def test_runAfterStop(self):
+ """
+ C{reactor.run()} raises L{ReactorNotRestartable} when called when
+ the reactor is being run after getting stopped priorly.
+ """
+ events = []
+ def restart():
+ self.assertRaises(ReactorNotRestartable, reactor.run)
+ events.append('tested')
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(reactor.stop)
+ reactor.addSystemEventTrigger('after', 'shutdown', restart)
+ self.runReactor(reactor)
+ self.assertEqual(events, ['tested'])
+
+
+
+globals().update(SystemEventTestsBuilder.makeTestCaseClasses())
+globals().update(ObjectModelIntegrationTest.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_default.py b/twisted/internet/test/test_default.py
new file mode 100644
index 0000000..635c0f9
--- /dev/null
+++ b/twisted/internet/test/test_default.py
@@ -0,0 +1,83 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.default}.
+"""
+
+import select
+from twisted.trial.unittest import TestCase
+from twisted.python.runtime import Platform
+from twisted.internet.default import _getInstallFunction
+
+unix = Platform('posix', 'other')
+linux = Platform('posix', 'linux2')
+windows = Platform('nt', 'win32')
+osx = Platform('posix', 'darwin')
+
+
+class PollReactorTests(TestCase):
+ """
+ Tests for the cases of L{twisted.internet.default._getInstallFunction}
+ in which it picks the poll(2) or epoll(7)-based reactors.
+ """
+
+ def assertIsPoll(self, install):
+ """
+ Assert the given function will install the poll() reactor, or select()
+ if poll() is unavailable.
+ """
+ if hasattr(select, "poll"):
+ self.assertEqual(
+ install.__module__, 'twisted.internet.pollreactor')
+ else:
+ self.assertEqual(
+ install.__module__, 'twisted.internet.selectreactor')
+
+
+ def test_unix(self):
+ """
+ L{_getInstallFunction} chooses the poll reactor on arbitrary Unix
+ platforms, falling back to select(2) if it is unavailable.
+ """
+ install = _getInstallFunction(unix)
+ self.assertIsPoll(install)
+
+
+ def test_linux(self):
+ """
+ L{_getInstallFunction} chooses the epoll reactor on Linux, or poll if
+ epoll is unavailable.
+ """
+ install = _getInstallFunction(linux)
+ try:
+ from twisted.internet import epollreactor
+ except ImportError:
+ self.assertIsPoll(install)
+ else:
+ self.assertEqual(
+ install.__module__, 'twisted.internet.epollreactor')
+
+
+
+class SelectReactorTests(TestCase):
+ """
+ Tests for the cases of L{twisted.internet.default._getInstallFunction}
+ in which it picks the select(2)-based reactor.
+ """
+ def test_osx(self):
+ """
+ L{_getInstallFunction} chooses the select reactor on OS X.
+ """
+ install = _getInstallFunction(osx)
+ self.assertEqual(
+ install.__module__, 'twisted.internet.selectreactor')
+
+
+ def test_windows(self):
+ """
+ L{_getInstallFunction} chooses the select reactor on Windows.
+ """
+ install = _getInstallFunction(windows)
+ self.assertEqual(
+ install.__module__, 'twisted.internet.selectreactor')
diff --git a/twisted/internet/test/test_endpoints.py b/twisted/internet/test/test_endpoints.py
new file mode 100644
index 0000000..4984421
--- /dev/null
+++ b/twisted/internet/test/test_endpoints.py
@@ -0,0 +1,1646 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+"""
+Test the C{I...Endpoint} implementations that wrap the L{IReactorTCP},
+L{IReactorSSL}, and L{IReactorUNIX} interfaces found in
+L{twisted.internet.endpoints}.
+"""
+
+from errno import EPERM
+from socket import AF_INET, AF_INET6
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+from twisted.internet import error, interfaces, defer
+from twisted.internet import endpoints
+from twisted.internet.address import IPv4Address, UNIXAddress
+from twisted.internet.protocol import ClientFactory, Protocol
+from twisted.test.proto_helpers import (
+ MemoryReactor, RaisingMemoryReactor, StringTransport)
+from twisted.python.failure import Failure
+from twisted.python.systemd import ListenFDs
+from twisted.plugin import getPlugins
+
+from twisted import plugins
+from twisted.python.modules import getModule
+from twisted.python.filepath import FilePath
+
+pemPath = getModule("twisted.test").filePath.sibling("server.pem")
+casPath = getModule(__name__).filePath.sibling("fake_CAs")
+escapedPEMPathName = endpoints.quoteStringArgument(pemPath.path)
+escapedCAsPathName = endpoints.quoteStringArgument(casPath.path)
+
+try:
+ from twisted.test.test_sslverify import makeCertificate
+ from twisted.internet.ssl import CertificateOptions, Certificate, \
+ KeyPair, PrivateCertificate
+ from OpenSSL.SSL import ContextType
+ testCertificate = Certificate.loadPEM(pemPath.getContent())
+ testPrivateCertificate = PrivateCertificate.loadPEM(pemPath.getContent())
+
+ skipSSL = False
+except ImportError:
+ skipSSL = "OpenSSL is required to construct SSL Endpoints"
+
+
+class TestProtocol(Protocol):
+ """
+ Protocol whose only function is to callback deferreds on the
+ factory when it is connected or disconnected.
+ """
+
+ def __init__(self):
+ self.data = []
+ self.connectionsLost = []
+ self.connectionMadeCalls = 0
+
+
+ def logPrefix(self):
+ return "A Test Protocol"
+
+
+ def connectionMade(self):
+ self.connectionMadeCalls += 1
+
+
+ def dataReceived(self, data):
+ self.data.append(data)
+
+
+ def connectionLost(self, reason):
+ self.connectionsLost.append(reason)
+
+
+
+class TestHalfCloseableProtocol(TestProtocol):
+ """
+ A Protocol that implements L{IHalfCloseableProtocol} and records whether its
+ C{readConnectionLost} and {writeConnectionLost} methods are called.
+
+ @ivar readLost: A C{bool} indicating whether C{readConnectionLost} has been
+ called.
+
+ @ivar writeLost: A C{bool} indicating whether C{writeConnectionLost} has
+ been called.
+ """
+ implements(interfaces.IHalfCloseableProtocol)
+
+ def __init__(self):
+ TestProtocol.__init__(self)
+ self.readLost = False
+ self.writeLost = False
+
+
+ def readConnectionLost(self):
+ self.readLost = True
+
+
+ def writeConnectionLost(self):
+ self.writeLost = True
+
+
+
+class TestFileDescriptorReceiverProtocol(TestProtocol):
+ """
+ A Protocol that implements L{IFileDescriptorReceiver} and records how its
+ C{fileDescriptorReceived} method is called.
+
+ @ivar receivedDescriptors: A C{list} containing all of the file descriptors
+ passed to C{fileDescriptorReceived} calls made on this instance.
+ """
+ implements(interfaces.IFileDescriptorReceiver)
+
+ def connectionMade(self):
+ TestProtocol.connectionMade(self)
+ self.receivedDescriptors = []
+
+
+ def fileDescriptorReceived(self, descriptor):
+ self.receivedDescriptors.append(descriptor)
+
+
+
+class TestFactory(ClientFactory):
+ """
+ Simple factory to be used both when connecting and listening. It contains
+ two deferreds which are called back when my protocol connects and
+ disconnects.
+ """
+
+ protocol = TestProtocol
+
+
+
+class WrappingFactoryTests(unittest.TestCase):
+ """
+ Test the behaviour of our ugly implementation detail C{_WrappingFactory}.
+ """
+ def test_doStart(self):
+ """
+ L{_WrappingFactory.doStart} passes through to the wrapped factory's
+ C{doStart} method, allowing application-specific setup and logging.
+ """
+ factory = ClientFactory()
+ wf = endpoints._WrappingFactory(factory)
+ wf.doStart()
+ self.assertEqual(1, factory.numPorts)
+
+
+ def test_doStop(self):
+ """
+ L{_WrappingFactory.doStop} passes through to the wrapped factory's
+ C{doStop} method, allowing application-specific cleanup and logging.
+ """
+ factory = ClientFactory()
+ factory.numPorts = 3
+ wf = endpoints._WrappingFactory(factory)
+ wf.doStop()
+ self.assertEqual(2, factory.numPorts)
+
+
+ def test_failedBuildProtocol(self):
+ """
+ An exception raised in C{buildProtocol} of our wrappedFactory
+ results in our C{onConnection} errback being fired.
+ """
+
+ class BogusFactory(ClientFactory):
+ """
+ A one off factory whose C{buildProtocol} raises an C{Exception}.
+ """
+
+ def buildProtocol(self, addr):
+ raise ValueError("My protocol is poorly defined.")
+
+
+ wf = endpoints._WrappingFactory(BogusFactory())
+
+ wf.buildProtocol(None)
+
+ d = self.assertFailure(wf._onConnection, ValueError)
+ d.addCallback(lambda e: self.assertEqual(
+ e.args,
+ ("My protocol is poorly defined.",)))
+
+ return d
+
+
+ def test_logPrefixPassthrough(self):
+ """
+ If the wrapped protocol provides L{ILoggingContext}, whatever is
+ returned from the wrapped C{logPrefix} method is returned from
+ L{_WrappingProtocol.logPrefix}.
+ """
+ wf = endpoints._WrappingFactory(TestFactory())
+ wp = wf.buildProtocol(None)
+ self.assertEqual(wp.logPrefix(), "A Test Protocol")
+
+
+ def test_logPrefixDefault(self):
+ """
+ If the wrapped protocol does not provide L{ILoggingContext}, the wrapped
+ protocol's class name is returned from L{_WrappingProtocol.logPrefix}.
+ """
+ class NoProtocol(object):
+ pass
+ factory = TestFactory()
+ factory.protocol = NoProtocol
+ wf = endpoints._WrappingFactory(factory)
+ wp = wf.buildProtocol(None)
+ self.assertEqual(wp.logPrefix(), "NoProtocol")
+
+
+ def test_wrappedProtocolDataReceived(self):
+ """
+ The wrapped C{Protocol}'s C{dataReceived} will get called when our
+ C{_WrappingProtocol}'s C{dataReceived} gets called.
+ """
+ wf = endpoints._WrappingFactory(TestFactory())
+ p = wf.buildProtocol(None)
+ p.makeConnection(None)
+
+ p.dataReceived('foo')
+ self.assertEqual(p._wrappedProtocol.data, ['foo'])
+
+ p.dataReceived('bar')
+ self.assertEqual(p._wrappedProtocol.data, ['foo', 'bar'])
+
+
+ def test_wrappedProtocolTransport(self):
+ """
+ Our transport is properly hooked up to the wrappedProtocol when a
+ connection is made.
+ """
+ wf = endpoints._WrappingFactory(TestFactory())
+ p = wf.buildProtocol(None)
+
+ dummyTransport = object()
+
+ p.makeConnection(dummyTransport)
+
+ self.assertEqual(p.transport, dummyTransport)
+
+ self.assertEqual(p._wrappedProtocol.transport, dummyTransport)
+
+
+ def test_wrappedProtocolConnectionLost(self):
+ """
+ Our wrappedProtocol's connectionLost method is called when
+ L{_WrappingProtocol.connectionLost} is called.
+ """
+ tf = TestFactory()
+ wf = endpoints._WrappingFactory(tf)
+ p = wf.buildProtocol(None)
+
+ p.connectionLost("fail")
+
+ self.assertEqual(p._wrappedProtocol.connectionsLost, ["fail"])
+
+
+ def test_clientConnectionFailed(self):
+ """
+ Calls to L{_WrappingFactory.clientConnectionLost} should errback the
+ L{_WrappingFactory._onConnection} L{Deferred}
+ """
+ wf = endpoints._WrappingFactory(TestFactory())
+ expectedFailure = Failure(error.ConnectError(string="fail"))
+
+ wf.clientConnectionFailed(
+ None,
+ expectedFailure)
+
+ errors = []
+ def gotError(f):
+ errors.append(f)
+
+ wf._onConnection.addErrback(gotError)
+
+ self.assertEqual(errors, [expectedFailure])
+
+
+ def test_wrappingProtocolFileDescriptorReceiver(self):
+ """
+ Our L{_WrappingProtocol} should be an L{IFileDescriptorReceiver} if the
+ wrapped protocol is.
+ """
+ connectedDeferred = None
+ applicationProtocol = TestFileDescriptorReceiverProtocol()
+ wrapper = endpoints._WrappingProtocol(
+ connectedDeferred, applicationProtocol)
+ self.assertTrue(interfaces.IFileDescriptorReceiver.providedBy(wrapper))
+ self.assertTrue(verifyObject(interfaces.IFileDescriptorReceiver, wrapper))
+
+
+ def test_wrappingProtocolNotFileDescriptorReceiver(self):
+ """
+ Our L{_WrappingProtocol} does not provide L{IHalfCloseableProtocol} if
+ the wrapped protocol doesn't.
+ """
+ tp = TestProtocol()
+ p = endpoints._WrappingProtocol(None, tp)
+ self.assertFalse(interfaces.IFileDescriptorReceiver.providedBy(p))
+
+
+ def test_wrappedProtocolFileDescriptorReceived(self):
+ """
+ L{_WrappingProtocol.fileDescriptorReceived} calls the wrapped protocol's
+ C{fileDescriptorReceived} method.
+ """
+ wrappedProtocol = TestFileDescriptorReceiverProtocol()
+ wrapper = endpoints._WrappingProtocol(
+ defer.Deferred(), wrappedProtocol)
+ wrapper.makeConnection(StringTransport())
+ wrapper.fileDescriptorReceived(42)
+ self.assertEqual(wrappedProtocol.receivedDescriptors, [42])
+
+
+ def test_wrappingProtocolHalfCloseable(self):
+ """
+ Our L{_WrappingProtocol} should be an L{IHalfCloseableProtocol} if the
+ C{wrappedProtocol} is.
+ """
+ cd = object()
+ hcp = TestHalfCloseableProtocol()
+ p = endpoints._WrappingProtocol(cd, hcp)
+ self.assertEqual(
+ interfaces.IHalfCloseableProtocol.providedBy(p), True)
+
+
+ def test_wrappingProtocolNotHalfCloseable(self):
+ """
+ Our L{_WrappingProtocol} should not provide L{IHalfCloseableProtocol}
+ if the C{WrappedProtocol} doesn't.
+ """
+ tp = TestProtocol()
+ p = endpoints._WrappingProtocol(None, tp)
+ self.assertEqual(
+ interfaces.IHalfCloseableProtocol.providedBy(p), False)
+
+
+ def test_wrappedProtocolReadConnectionLost(self):
+ """
+ L{_WrappingProtocol.readConnectionLost} should proxy to the wrapped
+ protocol's C{readConnectionLost}
+ """
+ hcp = TestHalfCloseableProtocol()
+ p = endpoints._WrappingProtocol(None, hcp)
+ p.readConnectionLost()
+ self.assertEqual(hcp.readLost, True)
+
+
+ def test_wrappedProtocolWriteConnectionLost(self):
+ """
+ L{_WrappingProtocol.writeConnectionLost} should proxy to the wrapped
+ protocol's C{writeConnectionLost}
+ """
+ hcp = TestHalfCloseableProtocol()
+ p = endpoints._WrappingProtocol(None, hcp)
+ p.writeConnectionLost()
+ self.assertEqual(hcp.writeLost, True)
+
+
+
+class ClientEndpointTestCaseMixin(object):
+ """
+ Generic test methods to be mixed into all client endpoint test classes.
+ """
+ def retrieveConnectedFactory(self, reactor):
+ """
+ Retrieve a single factory that has connected using the given reactor.
+ (This behavior is valid for TCP and SSL but needs to be overridden for
+ UNIX.)
+
+ @param reactor: a L{MemoryReactor}
+ """
+ return self.expectedClients(reactor)[0][2]
+
+
+ def test_endpointConnectSuccess(self):
+ """
+ A client endpoint can connect and returns a deferred who gets called
+ back with a protocol instance.
+ """
+ proto = object()
+ mreactor = MemoryReactor()
+
+ clientFactory = object()
+
+ ep, expectedArgs, ignoredDest = self.createClientEndpoint(
+ mreactor, clientFactory)
+
+ d = ep.connect(clientFactory)
+
+ receivedProtos = []
+
+ def checkProto(p):
+ receivedProtos.append(p)
+
+ d.addCallback(checkProto)
+
+ factory = self.retrieveConnectedFactory(mreactor)
+ factory._onConnection.callback(proto)
+ self.assertEqual(receivedProtos, [proto])
+
+ expectedClients = self.expectedClients(mreactor)
+
+ self.assertEqual(len(expectedClients), 1)
+ self.assertConnectArgs(expectedClients[0], expectedArgs)
+
+
+ def test_endpointConnectFailure(self):
+ """
+ If an endpoint tries to connect to a non-listening port it gets
+ a C{ConnectError} failure.
+ """
+ expectedError = error.ConnectError(string="Connection Failed")
+
+ mreactor = RaisingMemoryReactor(connectException=expectedError)
+
+ clientFactory = object()
+
+ ep, ignoredArgs, ignoredDest = self.createClientEndpoint(
+ mreactor, clientFactory)
+
+ d = ep.connect(clientFactory)
+
+ receivedExceptions = []
+
+ def checkFailure(f):
+ receivedExceptions.append(f.value)
+
+ d.addErrback(checkFailure)
+
+ self.assertEqual(receivedExceptions, [expectedError])
+
+
+ def test_endpointConnectingCancelled(self):
+ """
+ Calling L{Deferred.cancel} on the L{Deferred} returned from
+ L{IStreamClientEndpoint.connect} is errbacked with an expected
+ L{ConnectingCancelledError} exception.
+ """
+ mreactor = MemoryReactor()
+
+ clientFactory = object()
+
+ ep, ignoredArgs, address = self.createClientEndpoint(
+ mreactor, clientFactory)
+
+ d = ep.connect(clientFactory)
+
+ receivedFailures = []
+
+ def checkFailure(f):
+ receivedFailures.append(f)
+
+ d.addErrback(checkFailure)
+
+ d.cancel()
+ # When canceled, the connector will immediately notify its factory that
+ # the connection attempt has failed due to a UserError.
+ attemptFactory = self.retrieveConnectedFactory(mreactor)
+ attemptFactory.clientConnectionFailed(None, Failure(error.UserError()))
+ # This should be a feature of MemoryReactor: <http://tm.tl/5630>.
+
+ self.assertEqual(len(receivedFailures), 1)
+
+ failure = receivedFailures[0]
+
+ self.assertIsInstance(failure.value, error.ConnectingCancelledError)
+ self.assertEqual(failure.value.address, address)
+
+
+ def test_endpointConnectNonDefaultArgs(self):
+ """
+ The endpoint should pass it's connectArgs parameter to the reactor's
+ listen methods.
+ """
+ factory = object()
+
+ mreactor = MemoryReactor()
+
+ ep, expectedArgs, ignoredHost = self.createClientEndpoint(
+ mreactor, factory,
+ **self.connectArgs())
+
+ ep.connect(factory)
+
+ expectedClients = self.expectedClients(mreactor)
+
+ self.assertEqual(len(expectedClients), 1)
+ self.assertConnectArgs(expectedClients[0], expectedArgs)
+
+
+
+class ServerEndpointTestCaseMixin(object):
+ """
+ Generic test methods to be mixed into all client endpoint test classes.
+ """
+ def test_endpointListenSuccess(self):
+ """
+ An endpoint can listen and returns a deferred that gets called back
+ with a port instance.
+ """
+ mreactor = MemoryReactor()
+
+ factory = object()
+
+ ep, expectedArgs, expectedHost = self.createServerEndpoint(
+ mreactor, factory)
+
+ d = ep.listen(factory)
+
+ receivedHosts = []
+
+ def checkPortAndServer(port):
+ receivedHosts.append(port.getHost())
+
+ d.addCallback(checkPortAndServer)
+
+ self.assertEqual(receivedHosts, [expectedHost])
+ self.assertEqual(self.expectedServers(mreactor), [expectedArgs])
+
+
+ def test_endpointListenFailure(self):
+ """
+ When an endpoint tries to listen on an already listening port, a
+ C{CannotListenError} failure is errbacked.
+ """
+ factory = object()
+ exception = error.CannotListenError('', 80, factory)
+ mreactor = RaisingMemoryReactor(listenException=exception)
+
+ ep, ignoredArgs, ignoredDest = self.createServerEndpoint(
+ mreactor, factory)
+
+ d = ep.listen(object())
+
+ receivedExceptions = []
+
+ def checkFailure(f):
+ receivedExceptions.append(f.value)
+
+ d.addErrback(checkFailure)
+
+ self.assertEqual(receivedExceptions, [exception])
+
+
+ def test_endpointListenNonDefaultArgs(self):
+ """
+ The endpoint should pass it's listenArgs parameter to the reactor's
+ listen methods.
+ """
+ factory = object()
+
+ mreactor = MemoryReactor()
+
+ ep, expectedArgs, ignoredHost = self.createServerEndpoint(
+ mreactor, factory,
+ **self.listenArgs())
+
+ ep.listen(factory)
+
+ expectedServers = self.expectedServers(mreactor)
+
+ self.assertEqual(expectedServers, [expectedArgs])
+
+
+
+class EndpointTestCaseMixin(ServerEndpointTestCaseMixin,
+ ClientEndpointTestCaseMixin):
+ """
+ Generic test methods to be mixed into all endpoint test classes.
+ """
+
+
+
+class TCP4EndpointsTestCase(EndpointTestCaseMixin, unittest.TestCase):
+ """
+ Tests for TCP Endpoints.
+ """
+
+ def expectedServers(self, reactor):
+ """
+ @return: List of calls to L{IReactorTCP.listenTCP}
+ """
+ return reactor.tcpServers
+
+
+ def expectedClients(self, reactor):
+ """
+ @return: List of calls to L{IReactorTCP.connectTCP}
+ """
+ return reactor.tcpClients
+
+
+ def assertConnectArgs(self, receivedArgs, expectedArgs):
+ """
+ Compare host, port, timeout, and bindAddress in C{receivedArgs}
+ to C{expectedArgs}. We ignore the factory because we don't
+ only care what protocol comes out of the
+ C{IStreamClientEndpoint.connect} call.
+
+ @param receivedArgs: C{tuple} of (C{host}, C{port}, C{factory},
+ C{timeout}, C{bindAddress}) that was passed to
+ L{IReactorTCP.connectTCP}.
+ @param expectedArgs: C{tuple} of (C{host}, C{port}, C{factory},
+ C{timeout}, C{bindAddress}) that we expect to have been passed
+ to L{IReactorTCP.connectTCP}.
+ """
+ (host, port, ignoredFactory, timeout, bindAddress) = receivedArgs
+ (expectedHost, expectedPort, _ignoredFactory,
+ expectedTimeout, expectedBindAddress) = expectedArgs
+
+ self.assertEqual(host, expectedHost)
+ self.assertEqual(port, expectedPort)
+ self.assertEqual(timeout, expectedTimeout)
+ self.assertEqual(bindAddress, expectedBindAddress)
+
+
+ def connectArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to connect.
+ """
+ return {'timeout': 10, 'bindAddress': ('localhost', 49595)}
+
+
+ def listenArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to listen
+ """
+ return {'backlog': 100, 'interface': '127.0.0.1'}
+
+
+ def createServerEndpoint(self, reactor, factory, **listenArgs):
+ """
+ Create an L{TCP4ServerEndpoint} and return the values needed to verify
+ its behaviour.
+
+ @param reactor: A fake L{IReactorTCP} that L{TCP4ServerEndpoint} can
+ call L{IReactorTCP.listenTCP} on.
+ @param factory: The thing that we expect to be passed to our
+ L{IStreamServerEndpoint.listen} implementation.
+ @param listenArgs: Optional dictionary of arguments to
+ L{IReactorTCP.listenTCP}.
+ """
+ address = IPv4Address("TCP", "0.0.0.0", 0)
+
+ if listenArgs is None:
+ listenArgs = {}
+
+ return (endpoints.TCP4ServerEndpoint(reactor,
+ address.port,
+ **listenArgs),
+ (address.port, factory,
+ listenArgs.get('backlog', 50),
+ listenArgs.get('interface', '')),
+ address)
+
+
+ def createClientEndpoint(self, reactor, clientFactory, **connectArgs):
+ """
+ Create an L{TCP4ClientEndpoint} and return the values needed to verify
+ its behavior.
+
+ @param reactor: A fake L{IReactorTCP} that L{TCP4ClientEndpoint} can
+ call L{IReactorTCP.connectTCP} on.
+ @param clientFactory: The thing that we expect to be passed to our
+ L{IStreamClientEndpoint.connect} implementation.
+ @param connectArgs: Optional dictionary of arguments to
+ L{IReactorTCP.connectTCP}
+ """
+ address = IPv4Address("TCP", "localhost", 80)
+
+ return (endpoints.TCP4ClientEndpoint(reactor,
+ address.host,
+ address.port,
+ **connectArgs),
+ (address.host, address.port, clientFactory,
+ connectArgs.get('timeout', 30),
+ connectArgs.get('bindAddress', None)),
+ address)
+
+
+
+class SSL4EndpointsTestCase(EndpointTestCaseMixin,
+ unittest.TestCase):
+ """
+ Tests for SSL Endpoints.
+ """
+ if skipSSL:
+ skip = skipSSL
+
+ def expectedServers(self, reactor):
+ """
+ @return: List of calls to L{IReactorSSL.listenSSL}
+ """
+ return reactor.sslServers
+
+
+ def expectedClients(self, reactor):
+ """
+ @return: List of calls to L{IReactorSSL.connectSSL}
+ """
+ return reactor.sslClients
+
+
+ def assertConnectArgs(self, receivedArgs, expectedArgs):
+ """
+ Compare host, port, contextFactory, timeout, and bindAddress in
+ C{receivedArgs} to C{expectedArgs}. We ignore the factory because we
+ don't only care what protocol comes out of the
+ C{IStreamClientEndpoint.connect} call.
+
+ @param receivedArgs: C{tuple} of (C{host}, C{port}, C{factory},
+ C{contextFactory}, C{timeout}, C{bindAddress}) that was passed to
+ L{IReactorSSL.connectSSL}.
+ @param expectedArgs: C{tuple} of (C{host}, C{port}, C{factory},
+ C{contextFactory}, C{timeout}, C{bindAddress}) that we expect to
+ have been passed to L{IReactorSSL.connectSSL}.
+ """
+ (host, port, ignoredFactory, contextFactory, timeout,
+ bindAddress) = receivedArgs
+
+ (expectedHost, expectedPort, _ignoredFactory, expectedContextFactory,
+ expectedTimeout, expectedBindAddress) = expectedArgs
+
+ self.assertEqual(host, expectedHost)
+ self.assertEqual(port, expectedPort)
+ self.assertEqual(contextFactory, expectedContextFactory)
+ self.assertEqual(timeout, expectedTimeout)
+ self.assertEqual(bindAddress, expectedBindAddress)
+
+
+ def connectArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to connect.
+ """
+ return {'timeout': 10, 'bindAddress': ('localhost', 49595)}
+
+
+ def listenArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to listen
+ """
+ return {'backlog': 100, 'interface': '127.0.0.1'}
+
+
+ def setUp(self):
+ """
+ Set up client and server SSL contexts for use later.
+ """
+ self.sKey, self.sCert = makeCertificate(
+ O="Server Test Certificate",
+ CN="server")
+ self.cKey, self.cCert = makeCertificate(
+ O="Client Test Certificate",
+ CN="client")
+ self.serverSSLContext = CertificateOptions(
+ privateKey=self.sKey,
+ certificate=self.sCert,
+ requireCertificate=False)
+ self.clientSSLContext = CertificateOptions(
+ requireCertificate=False)
+
+
+ def createServerEndpoint(self, reactor, factory, **listenArgs):
+ """
+ Create an L{SSL4ServerEndpoint} and return the tools to verify its
+ behaviour.
+
+ @param factory: The thing that we expect to be passed to our
+ L{IStreamServerEndpoint.listen} implementation.
+ @param reactor: A fake L{IReactorSSL} that L{SSL4ServerEndpoint} can
+ call L{IReactorSSL.listenSSL} on.
+ @param listenArgs: Optional dictionary of arguments to
+ L{IReactorSSL.listenSSL}.
+ """
+ address = IPv4Address("TCP", "0.0.0.0", 0)
+
+ return (endpoints.SSL4ServerEndpoint(reactor,
+ address.port,
+ self.serverSSLContext,
+ **listenArgs),
+ (address.port, factory, self.serverSSLContext,
+ listenArgs.get('backlog', 50),
+ listenArgs.get('interface', '')),
+ address)
+
+
+ def createClientEndpoint(self, reactor, clientFactory, **connectArgs):
+ """
+ Create an L{SSL4ClientEndpoint} and return the values needed to verify
+ its behaviour.
+
+ @param reactor: A fake L{IReactorSSL} that L{SSL4ClientEndpoint} can
+ call L{IReactorSSL.connectSSL} on.
+ @param clientFactory: The thing that we expect to be passed to our
+ L{IStreamClientEndpoint.connect} implementation.
+ @param connectArgs: Optional dictionary of arguments to
+ L{IReactorSSL.connectSSL}
+ """
+ address = IPv4Address("TCP", "localhost", 80)
+
+ if connectArgs is None:
+ connectArgs = {}
+
+ return (endpoints.SSL4ClientEndpoint(reactor,
+ address.host,
+ address.port,
+ self.clientSSLContext,
+ **connectArgs),
+ (address.host, address.port, clientFactory,
+ self.clientSSLContext,
+ connectArgs.get('timeout', 30),
+ connectArgs.get('bindAddress', None)),
+ address)
+
+
+
+class UNIXEndpointsTestCase(EndpointTestCaseMixin,
+ unittest.TestCase):
+ """
+ Tests for UnixSocket Endpoints.
+ """
+
+ def retrieveConnectedFactory(self, reactor):
+ """
+ Override L{EndpointTestCaseMixin.retrieveConnectedFactory} to account
+ for different index of 'factory' in C{connectUNIX} args.
+ """
+ return self.expectedClients(reactor)[0][1]
+
+ def expectedServers(self, reactor):
+ """
+ @return: List of calls to L{IReactorUNIX.listenUNIX}
+ """
+ return reactor.unixServers
+
+
+ def expectedClients(self, reactor):
+ """
+ @return: List of calls to L{IReactorUNIX.connectUNIX}
+ """
+ return reactor.unixClients
+
+
+ def assertConnectArgs(self, receivedArgs, expectedArgs):
+ """
+ Compare path, timeout, checkPID in C{receivedArgs} to C{expectedArgs}.
+ We ignore the factory because we don't only care what protocol comes
+ out of the C{IStreamClientEndpoint.connect} call.
+
+ @param receivedArgs: C{tuple} of (C{path}, C{timeout}, C{checkPID})
+ that was passed to L{IReactorUNIX.connectUNIX}.
+ @param expectedArgs: C{tuple} of (C{path}, C{timeout}, C{checkPID})
+ that we expect to have been passed to L{IReactorUNIX.connectUNIX}.
+ """
+
+ (path, ignoredFactory, timeout, checkPID) = receivedArgs
+
+ (expectedPath, _ignoredFactory, expectedTimeout,
+ expectedCheckPID) = expectedArgs
+
+ self.assertEqual(path, expectedPath)
+ self.assertEqual(timeout, expectedTimeout)
+ self.assertEqual(checkPID, expectedCheckPID)
+
+
+ def connectArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to connect.
+ """
+ return {'timeout': 10, 'checkPID': 1}
+
+
+ def listenArgs(self):
+ """
+ @return: C{dict} of keyword arguments to pass to listen
+ """
+ return {'backlog': 100, 'mode': 0600, 'wantPID': 1}
+
+
+ def createServerEndpoint(self, reactor, factory, **listenArgs):
+ """
+ Create an L{UNIXServerEndpoint} and return the tools to verify its
+ behaviour.
+
+ @param reactor: A fake L{IReactorUNIX} that L{UNIXServerEndpoint} can
+ call L{IReactorUNIX.listenUNIX} on.
+ @param factory: The thing that we expect to be passed to our
+ L{IStreamServerEndpoint.listen} implementation.
+ @param listenArgs: Optional dictionary of arguments to
+ L{IReactorUNIX.listenUNIX}.
+ """
+ address = UNIXAddress(self.mktemp())
+
+ return (endpoints.UNIXServerEndpoint(reactor, address.name,
+ **listenArgs),
+ (address.name, factory,
+ listenArgs.get('backlog', 50),
+ listenArgs.get('mode', 0666),
+ listenArgs.get('wantPID', 0)),
+ address)
+
+
+ def createClientEndpoint(self, reactor, clientFactory, **connectArgs):
+ """
+ Create an L{UNIXClientEndpoint} and return the values needed to verify
+ its behaviour.
+
+ @param reactor: A fake L{IReactorUNIX} that L{UNIXClientEndpoint} can
+ call L{IReactorUNIX.connectUNIX} on.
+ @param clientFactory: The thing that we expect to be passed to our
+ L{IStreamClientEndpoint.connect} implementation.
+ @param connectArgs: Optional dictionary of arguments to
+ L{IReactorUNIX.connectUNIX}
+ """
+ address = UNIXAddress(self.mktemp())
+
+ return (endpoints.UNIXClientEndpoint(reactor, address.name,
+ **connectArgs),
+ (address.name, clientFactory,
+ connectArgs.get('timeout', 30),
+ connectArgs.get('checkPID', 0)),
+ address)
+
+
+
+class ParserTestCase(unittest.TestCase):
+ """
+ Tests for L{endpoints._parseServer}, the low-level parsing logic.
+ """
+
+ f = "Factory"
+
+ def parse(self, *a, **kw):
+ """
+ Provide a hook for test_strports to substitute the deprecated API.
+ """
+ return endpoints._parseServer(*a, **kw)
+
+
+ def test_simpleTCP(self):
+ """
+ Simple strings with a 'tcp:' prefix should be parsed as TCP.
+ """
+ self.assertEqual(self.parse('tcp:80', self.f),
+ ('TCP', (80, self.f), {'interface':'', 'backlog':50}))
+
+
+ def test_interfaceTCP(self):
+ """
+ TCP port descriptions parse their 'interface' argument as a string.
+ """
+ self.assertEqual(
+ self.parse('tcp:80:interface=127.0.0.1', self.f),
+ ('TCP', (80, self.f), {'interface':'127.0.0.1', 'backlog':50}))
+
+
+ def test_backlogTCP(self):
+ """
+ TCP port descriptions parse their 'backlog' argument as an integer.
+ """
+ self.assertEqual(self.parse('tcp:80:backlog=6', self.f),
+ ('TCP', (80, self.f),
+ {'interface':'', 'backlog':6}))
+
+
+ def test_simpleUNIX(self):
+ """
+ L{endpoints._parseServer} returns a C{'UNIX'} port description with
+ defaults for C{'mode'}, C{'backlog'}, and C{'wantPID'} when passed a
+ string with the C{'unix:'} prefix and no other parameter values.
+ """
+ self.assertEqual(
+ self.parse('unix:/var/run/finger', self.f),
+ ('UNIX', ('/var/run/finger', self.f),
+ {'mode': 0666, 'backlog': 50, 'wantPID': True}))
+
+
+ def test_modeUNIX(self):
+ """
+ C{mode} can be set by including C{"mode=<some integer>"}.
+ """
+ self.assertEqual(
+ self.parse('unix:/var/run/finger:mode=0660', self.f),
+ ('UNIX', ('/var/run/finger', self.f),
+ {'mode': 0660, 'backlog': 50, 'wantPID': True}))
+
+
+ def test_wantPIDUNIX(self):
+ """
+ C{wantPID} can be set to false by included C{"lockfile=0"}.
+ """
+ self.assertEqual(
+ self.parse('unix:/var/run/finger:lockfile=0', self.f),
+ ('UNIX', ('/var/run/finger', self.f),
+ {'mode': 0666, 'backlog': 50, 'wantPID': False}))
+
+
+ def test_escape(self):
+ """
+ Backslash can be used to escape colons and backslashes in port
+ descriptions.
+ """
+ self.assertEqual(
+ self.parse(r'unix:foo\:bar\=baz\:qux\\', self.f),
+ ('UNIX', ('foo:bar=baz:qux\\', self.f),
+ {'mode': 0666, 'backlog': 50, 'wantPID': True}))
+
+
+ def test_quoteStringArgument(self):
+ """
+ L{endpoints.quoteStringArgument} should quote backslashes and colons
+ for interpolation into L{endpoints.serverFromString} and
+ L{endpoints.clientFactory} arguments.
+ """
+ self.assertEqual(endpoints.quoteStringArgument("some : stuff \\"),
+ "some \\: stuff \\\\")
+
+
+ def test_impliedEscape(self):
+ """
+ In strports descriptions, '=' in a parameter value does not need to be
+ quoted; it will simply be parsed as part of the value.
+ """
+ self.assertEqual(
+ self.parse(r'unix:address=foo=bar', self.f),
+ ('UNIX', ('foo=bar', self.f),
+ {'mode': 0666, 'backlog': 50, 'wantPID': True}))
+
+
+ def test_nonstandardDefault(self):
+ """
+ For compatibility with the old L{twisted.application.strports.parse},
+ the third 'mode' argument may be specified to L{endpoints.parse} to
+ indicate a default other than TCP.
+ """
+ self.assertEqual(
+ self.parse('filename', self.f, 'unix'),
+ ('UNIX', ('filename', self.f),
+ {'mode': 0666, 'backlog': 50, 'wantPID': True}))
+
+
+ def test_unknownType(self):
+ """
+ L{strports.parse} raises C{ValueError} when given an unknown endpoint
+ type.
+ """
+ self.assertRaises(ValueError, self.parse, "bogus-type:nothing", self.f)
+
+
+
+class ServerStringTests(unittest.TestCase):
+ """
+ Tests for L{twisted.internet.endpoints.serverFromString}.
+ """
+
+ def test_tcp(self):
+ """
+ When passed a TCP strports description, L{endpoints.serverFromString}
+ returns a L{TCP4ServerEndpoint} instance initialized with the values
+ from the string.
+ """
+ reactor = object()
+ server = endpoints.serverFromString(
+ reactor, "tcp:1234:backlog=12:interface=10.0.0.1")
+ self.assertIsInstance(server, endpoints.TCP4ServerEndpoint)
+ self.assertIdentical(server._reactor, reactor)
+ self.assertEqual(server._port, 1234)
+ self.assertEqual(server._backlog, 12)
+ self.assertEqual(server._interface, "10.0.0.1")
+
+
+ def test_ssl(self):
+ """
+ When passed an SSL strports description, L{endpoints.serverFromString}
+ returns a L{SSL4ServerEndpoint} instance initialized with the values
+ from the string.
+ """
+ reactor = object()
+ server = endpoints.serverFromString(
+ reactor,
+ "ssl:1234:backlog=12:privateKey=%s:"
+ "certKey=%s:interface=10.0.0.1" % (escapedPEMPathName,
+ escapedPEMPathName))
+ self.assertIsInstance(server, endpoints.SSL4ServerEndpoint)
+ self.assertIdentical(server._reactor, reactor)
+ self.assertEqual(server._port, 1234)
+ self.assertEqual(server._backlog, 12)
+ self.assertEqual(server._interface, "10.0.0.1")
+ ctx = server._sslContextFactory.getContext()
+ self.assertIsInstance(ctx, ContextType)
+
+ if skipSSL:
+ test_ssl.skip = skipSSL
+
+
+ def test_unix(self):
+ """
+ When passed a UNIX strports description, L{endpoint.serverFromString}
+ returns a L{UNIXServerEndpoint} instance initialized with the values
+ from the string.
+ """
+ reactor = object()
+ endpoint = endpoints.serverFromString(
+ reactor,
+ "unix:/var/foo/bar:backlog=7:mode=0123:lockfile=1")
+ self.assertIsInstance(endpoint, endpoints.UNIXServerEndpoint)
+ self.assertIdentical(endpoint._reactor, reactor)
+ self.assertEqual(endpoint._address, "/var/foo/bar")
+ self.assertEqual(endpoint._backlog, 7)
+ self.assertEqual(endpoint._mode, 0123)
+ self.assertEqual(endpoint._wantPID, True)
+
+
+ def test_implicitDefaultNotAllowed(self):
+ """
+ The older service-based API (L{twisted.internet.strports.service})
+ allowed an implicit default of 'tcp' so that TCP ports could be
+ specified as a simple integer, but we've since decided that's a bad
+ idea, and the new API does not accept an implicit default argument; you
+ have to say 'tcp:' now. If you try passing an old implicit port number
+ to the new API, you'll get a C{ValueError}.
+ """
+ value = self.assertRaises(
+ ValueError, endpoints.serverFromString, None, "4321")
+ self.assertEqual(
+ str(value),
+ "Unqualified strport description passed to 'service'."
+ "Use qualified endpoint descriptions; for example, 'tcp:4321'.")
+
+
+ def test_unknownType(self):
+ """
+ L{endpoints.serverFromString} raises C{ValueError} when given an
+ unknown endpoint type.
+ """
+ value = self.assertRaises(
+ # faster-than-light communication not supported
+ ValueError, endpoints.serverFromString, None,
+ "ftl:andromeda/carcosa/hali/2387")
+ self.assertEqual(
+ str(value),
+ "Unknown endpoint type: 'ftl'")
+
+
+ def test_typeFromPlugin(self):
+ """
+ L{endpoints.serverFromString} looks up plugins of type
+ L{IStreamServerEndpoint} and constructs endpoints from them.
+ """
+ # Set up a plugin which will only be accessible for the duration of
+ # this test.
+ addFakePlugin(self)
+ # Plugin is set up: now actually test.
+ notAReactor = object()
+ fakeEndpoint = endpoints.serverFromString(
+ notAReactor, "fake:hello:world:yes=no:up=down")
+ from twisted.plugins.fakeendpoint import fake
+ self.assertIdentical(fakeEndpoint.parser, fake)
+ self.assertEqual(fakeEndpoint.args, (notAReactor, 'hello', 'world'))
+ self.assertEqual(fakeEndpoint.kwargs, dict(yes='no', up='down'))
+
+
+
+def addFakePlugin(testCase, dropinSource="fakeendpoint.py"):
+ """
+ For the duration of C{testCase}, add a fake plugin to twisted.plugins which
+ contains some sample endpoint parsers.
+ """
+ import sys
+ savedModules = sys.modules.copy()
+ savedPluginPath = plugins.__path__
+ def cleanup():
+ sys.modules.clear()
+ sys.modules.update(savedModules)
+ plugins.__path__[:] = savedPluginPath
+ testCase.addCleanup(cleanup)
+ fp = FilePath(testCase.mktemp())
+ fp.createDirectory()
+ getModule(__name__).filePath.sibling(dropinSource).copyTo(
+ fp.child(dropinSource))
+ plugins.__path__.append(fp.path)
+
+
+
+class ClientStringTests(unittest.TestCase):
+ """
+ Tests for L{twisted.internet.endpoints.clientFromString}.
+ """
+
+ def test_tcp(self):
+ """
+ When passed a TCP strports description, L{endpoints.clientFromString}
+ returns a L{TCP4ClientEndpoint} instance initialized with the values
+ from the string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "tcp:host=example.com:port=1234:timeout=7:bindAddress=10.0.0.2")
+ self.assertIsInstance(client, endpoints.TCP4ClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._host, "example.com")
+ self.assertEqual(client._port, 1234)
+ self.assertEqual(client._timeout, 7)
+ self.assertEqual(client._bindAddress, "10.0.0.2")
+
+
+ def test_tcpPositionalArgs(self):
+ """
+ When passed a TCP strports description using positional arguments,
+ L{endpoints.clientFromString} returns a L{TCP4ClientEndpoint} instance
+ initialized with the values from the string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "tcp:example.com:1234:timeout=7:bindAddress=10.0.0.2")
+ self.assertIsInstance(client, endpoints.TCP4ClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._host, "example.com")
+ self.assertEqual(client._port, 1234)
+ self.assertEqual(client._timeout, 7)
+ self.assertEqual(client._bindAddress, "10.0.0.2")
+
+
+ def test_tcpHostPositionalArg(self):
+ """
+ When passed a TCP strports description specifying host as a positional
+ argument, L{endpoints.clientFromString} returns a L{TCP4ClientEndpoint}
+ instance initialized with the values from the string.
+ """
+ reactor = object()
+
+ client = endpoints.clientFromString(
+ reactor,
+ "tcp:example.com:port=1234:timeout=7:bindAddress=10.0.0.2")
+ self.assertEqual(client._host, "example.com")
+ self.assertEqual(client._port, 1234)
+
+
+ def test_tcpPortPositionalArg(self):
+ """
+ When passed a TCP strports description specifying port as a positional
+ argument, L{endpoints.clientFromString} returns a L{TCP4ClientEndpoint}
+ instance initialized with the values from the string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "tcp:host=example.com:1234:timeout=7:bindAddress=10.0.0.2")
+ self.assertEqual(client._host, "example.com")
+ self.assertEqual(client._port, 1234)
+
+
+ def test_tcpDefaults(self):
+ """
+ A TCP strports description may omit I{timeout} or I{bindAddress} to
+ allow the default to be used.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "tcp:host=example.com:port=1234")
+ self.assertEqual(client._timeout, 30)
+ self.assertEqual(client._bindAddress, None)
+
+
+ def test_unix(self):
+ """
+ When passed a UNIX strports description, L{endpoints.clientFromString}
+ returns a L{UNIXClientEndpoint} instance initialized with the values
+ from the string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "unix:path=/var/foo/bar:lockfile=1:timeout=9")
+ self.assertIsInstance(client, endpoints.UNIXClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._path, "/var/foo/bar")
+ self.assertEqual(client._timeout, 9)
+ self.assertEqual(client._checkPID, True)
+
+
+ def test_unixDefaults(self):
+ """
+ A UNIX strports description may omit I{lockfile} or I{timeout} to allow
+ the defaults to be used.
+ """
+ client = endpoints.clientFromString(object(), "unix:path=/var/foo/bar")
+ self.assertEqual(client._timeout, 30)
+ self.assertEqual(client._checkPID, False)
+
+
+ def test_unixPathPositionalArg(self):
+ """
+ When passed a UNIX strports description specifying path as a positional
+ argument, L{endpoints.clientFromString} returns a L{UNIXClientEndpoint}
+ instance initialized with the values from the string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "unix:/var/foo/bar:lockfile=1:timeout=9")
+ self.assertIsInstance(client, endpoints.UNIXClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._path, "/var/foo/bar")
+ self.assertEqual(client._timeout, 9)
+ self.assertEqual(client._checkPID, True)
+
+
+ def test_typeFromPlugin(self):
+ """
+ L{endpoints.clientFromString} looks up plugins of type
+ L{IStreamClientEndpoint} and constructs endpoints from them.
+ """
+ addFakePlugin(self)
+ notAReactor = object()
+ clientEndpoint = endpoints.clientFromString(
+ notAReactor, "cfake:alpha:beta:cee=dee:num=1")
+ from twisted.plugins.fakeendpoint import fakeClient
+ self.assertIdentical(clientEndpoint.parser, fakeClient)
+ self.assertEqual(clientEndpoint.args, ('alpha', 'beta'))
+ self.assertEqual(clientEndpoint.kwargs, dict(cee='dee', num='1'))
+
+
+ def test_unknownType(self):
+ """
+ L{endpoints.serverFromString} raises C{ValueError} when given an
+ unknown endpoint type.
+ """
+ value = self.assertRaises(
+ # faster-than-light communication not supported
+ ValueError, endpoints.clientFromString, None,
+ "ftl:andromeda/carcosa/hali/2387")
+ self.assertEqual(
+ str(value),
+ "Unknown endpoint type: 'ftl'")
+
+
+
+class SSLClientStringTests(unittest.TestCase):
+ """
+ Tests for L{twisted.internet.endpoints.clientFromString} which require SSL.
+ """
+
+ if skipSSL:
+ skip = skipSSL
+
+ def test_ssl(self):
+ """
+ When passed an SSL strports description, L{clientFromString} returns a
+ L{SSL4ClientEndpoint} instance initialized with the values from the
+ string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "ssl:host=example.net:port=4321:privateKey=%s:"
+ "certKey=%s:bindAddress=10.0.0.3:timeout=3:caCertsDir=%s" %
+ (escapedPEMPathName,
+ escapedPEMPathName,
+ escapedCAsPathName))
+ self.assertIsInstance(client, endpoints.SSL4ClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._host, "example.net")
+ self.assertEqual(client._port, 4321)
+ self.assertEqual(client._timeout, 3)
+ self.assertEqual(client._bindAddress, "10.0.0.3")
+ certOptions = client._sslContextFactory
+ self.assertIsInstance(certOptions, CertificateOptions)
+ ctx = certOptions.getContext()
+ self.assertIsInstance(ctx, ContextType)
+ self.assertEqual(Certificate(certOptions.certificate),
+ testCertificate)
+ privateCert = PrivateCertificate(certOptions.certificate)
+ privateCert._setPrivateKey(KeyPair(certOptions.privateKey))
+ self.assertEqual(privateCert, testPrivateCertificate)
+ expectedCerts = [
+ Certificate.loadPEM(x.getContent()) for x in
+ [casPath.child("thing1.pem"), casPath.child("thing2.pem")]
+ if x.basename().lower().endswith('.pem')
+ ]
+ self.assertEqual([Certificate(x) for x in certOptions.caCerts],
+ expectedCerts)
+
+
+ def test_sslPositionalArgs(self):
+ """
+ When passed an SSL strports description, L{clientFromString} returns a
+ L{SSL4ClientEndpoint} instance initialized with the values from the
+ string.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor,
+ "ssl:example.net:4321:privateKey=%s:"
+ "certKey=%s:bindAddress=10.0.0.3:timeout=3:caCertsDir=%s" %
+ (escapedPEMPathName,
+ escapedPEMPathName,
+ escapedCAsPathName))
+ self.assertIsInstance(client, endpoints.SSL4ClientEndpoint)
+ self.assertIdentical(client._reactor, reactor)
+ self.assertEqual(client._host, "example.net")
+ self.assertEqual(client._port, 4321)
+ self.assertEqual(client._timeout, 3)
+ self.assertEqual(client._bindAddress, "10.0.0.3")
+
+
+ def test_unreadableCertificate(self):
+ """
+ If a certificate in the directory is unreadable,
+ L{endpoints._loadCAsFromDir} will ignore that certificate.
+ """
+ class UnreadableFilePath(FilePath):
+ def getContent(self):
+ data = FilePath.getContent(self)
+ # There is a duplicate of thing2.pem, so ignore anything that
+ # looks like it.
+ if data == casPath.child("thing2.pem").getContent():
+ raise IOError(EPERM)
+ else:
+ return data
+ casPathClone = casPath.child("ignored").parent()
+ casPathClone.clonePath = UnreadableFilePath
+ self.assertEqual(
+ [Certificate(x) for x in endpoints._loadCAsFromDir(casPathClone)],
+ [Certificate.loadPEM(casPath.child("thing1.pem").getContent())])
+
+
+ def test_sslSimple(self):
+ """
+ When passed an SSL strports description without any extra parameters,
+ L{clientFromString} returns a simple non-verifying endpoint that will
+ speak SSL.
+ """
+ reactor = object()
+ client = endpoints.clientFromString(
+ reactor, "ssl:host=simple.example.org:port=4321")
+ certOptions = client._sslContextFactory
+ self.assertIsInstance(certOptions, CertificateOptions)
+ self.assertEqual(certOptions.verify, False)
+ ctx = certOptions.getContext()
+ self.assertIsInstance(ctx, ContextType)
+
+
+
+class AdoptedStreamServerEndpointTestCase(ServerEndpointTestCaseMixin,
+ unittest.TestCase):
+ """
+ Tests for adopted socket-based stream server endpoints.
+ """
+ def _createStubbedAdoptedEndpoint(self, reactor, fileno, addressFamily):
+ """
+ Create an L{AdoptedStreamServerEndpoint} which may safely be used with
+ an invalid file descriptor. This is convenient for a number of unit
+ tests.
+ """
+ e = endpoints.AdoptedStreamServerEndpoint(reactor, fileno, addressFamily)
+ # Stub out some syscalls which would fail, given our invalid file
+ # descriptor.
+ e._close = lambda fd: None
+ e._setNonBlocking = lambda fd: None
+ return e
+
+
+ def createServerEndpoint(self, reactor, factory):
+ """
+ Create a new L{AdoptedStreamServerEndpoint} for use by a test.
+
+ @return: A three-tuple:
+ - The endpoint
+ - A tuple of the arguments expected to be passed to the underlying
+ reactor method
+ - An IAddress object which will match the result of
+ L{IListeningPort.getHost} on the port returned by the endpoint.
+ """
+ fileno = 12
+ addressFamily = AF_INET
+ endpoint = self._createStubbedAdoptedEndpoint(
+ reactor, fileno, addressFamily)
+ # Magic numbers come from the implementation of MemoryReactor
+ address = IPv4Address("TCP", "0.0.0.0", 1234)
+ return (endpoint, (fileno, addressFamily, factory), address)
+
+
+ def expectedServers(self, reactor):
+ """
+ @return: The ports which were actually adopted by C{reactor} via calls
+ to its L{IReactorSocket.adoptStreamPort} implementation.
+ """
+ return reactor.adoptedPorts
+
+
+ def listenArgs(self):
+ """
+ @return: A C{dict} of additional keyword arguments to pass to the
+ C{createServerEndpoint}.
+ """
+ return {}
+
+
+ def test_singleUse(self):
+ """
+ L{AdoptedStreamServerEndpoint.listen} can only be used once. The file
+ descriptor given is closed after the first use, and subsequent calls to
+ C{listen} return a L{Deferred} that fails with L{AlreadyListened}.
+ """
+ reactor = MemoryReactor()
+ endpoint = self._createStubbedAdoptedEndpoint(reactor, 13, AF_INET)
+ endpoint.listen(object())
+ d = self.assertFailure(endpoint.listen(object()), error.AlreadyListened)
+ def listenFailed(ignored):
+ self.assertEqual(1, len(reactor.adoptedPorts))
+ d.addCallback(listenFailed)
+ return d
+
+
+ def test_descriptionNonBlocking(self):
+ """
+ L{AdoptedStreamServerEndpoint.listen} sets the file description given to
+ it to non-blocking.
+ """
+ reactor = MemoryReactor()
+ endpoint = self._createStubbedAdoptedEndpoint(reactor, 13, AF_INET)
+ events = []
+ def setNonBlocking(fileno):
+ events.append(("setNonBlocking", fileno))
+ endpoint._setNonBlocking = setNonBlocking
+
+ d = endpoint.listen(object())
+ def listened(ignored):
+ self.assertEqual([("setNonBlocking", 13)], events)
+ d.addCallback(listened)
+ return d
+
+
+ def test_descriptorClosed(self):
+ """
+ L{AdoptedStreamServerEndpoint.listen} closes its file descriptor after
+ adding it to the reactor with L{IReactorSocket.adoptStreamPort}.
+ """
+ reactor = MemoryReactor()
+ endpoint = self._createStubbedAdoptedEndpoint(reactor, 13, AF_INET)
+ events = []
+ def close(fileno):
+ events.append(("close", fileno, len(reactor.adoptedPorts)))
+ endpoint._close = close
+
+ d = endpoint.listen(object())
+ def listened(ignored):
+ self.assertEqual([("close", 13, 1)], events)
+ d.addCallback(listened)
+ return d
+
+
+
+class SystemdEndpointPluginTests(unittest.TestCase):
+ """
+ Unit tests for the systemd stream server endpoint and endpoint string
+ description parser.
+
+ @see: U{systemd<http://www.freedesktop.org/wiki/Software/systemd>}
+ """
+
+ _parserClass = endpoints._SystemdParser
+
+ def test_pluginDiscovery(self):
+ """
+ L{endpoints._SystemdParser} is found as a plugin for
+ L{interfaces.IStreamServerEndpointStringParser} interface.
+ """
+ parsers = list(getPlugins(
+ interfaces.IStreamServerEndpointStringParser))
+ for p in parsers:
+ if isinstance(p, self._parserClass):
+ break
+ else:
+ self.fail("Did not find systemd parser in %r" % (parsers,))
+
+
+ def test_interface(self):
+ """
+ L{endpoints._SystemdParser} instances provide
+ L{interfaces.IStreamServerEndpointStringParser}.
+ """
+ parser = self._parserClass()
+ self.assertTrue(verifyObject(
+ interfaces.IStreamServerEndpointStringParser, parser))
+
+
+ def _parseStreamServerTest(self, addressFamily, addressFamilyString):
+ """
+ Helper for unit tests for L{endpoints._SystemdParser.parseStreamServer}
+ for different address families.
+
+ Handling of the address family given will be verify. If there is a
+ problem a test-failing exception will be raised.
+
+ @param addressFamily: An address family constant, like L{socket.AF_INET}.
+
+ @param addressFamilyString: A string which should be recognized by the
+ parser as representing C{addressFamily}.
+ """
+ reactor = object()
+ descriptors = [5, 6, 7, 8, 9]
+ index = 3
+
+ parser = self._parserClass()
+ parser._sddaemon = ListenFDs(descriptors)
+
+ server = parser.parseStreamServer(
+ reactor, domain=addressFamilyString, index=str(index))
+ self.assertIdentical(server.reactor, reactor)
+ self.assertEqual(server.addressFamily, addressFamily)
+ self.assertEqual(server.fileno, descriptors[index])
+
+
+ def test_parseStreamServerINET(self):
+ """
+ IPv4 can be specified using the string C{"INET"}.
+ """
+ self._parseStreamServerTest(AF_INET, "INET")
+
+
+ def test_parseStreamServerINET6(self):
+ """
+ IPv6 can be specified using the string C{"INET6"}.
+ """
+ self._parseStreamServerTest(AF_INET6, "INET6")
+
+
+ def test_parseStreamServerUNIX(self):
+ """
+ A UNIX domain socket can be specified using the string C{"UNIX"}.
+ """
+ try:
+ from socket import AF_UNIX
+ except ImportError:
+ raise unittest.SkipTest("Platform lacks AF_UNIX support")
+ else:
+ self._parseStreamServerTest(AF_UNIX, "UNIX")
diff --git a/twisted/internet/test/test_epollreactor.py b/twisted/internet/test/test_epollreactor.py
new file mode 100644
index 0000000..b8363ee
--- /dev/null
+++ b/twisted/internet/test/test_epollreactor.py
@@ -0,0 +1,246 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.epollreactor}.
+"""
+
+from twisted.trial.unittest import TestCase
+try:
+ from twisted.internet.epollreactor import _ContinuousPolling
+except ImportError:
+ _ContinuousPolling = None
+from twisted.internet.task import Clock
+from twisted.internet.error import ConnectionDone
+
+
+
+class Descriptor(object):
+ """
+ Records reads and writes, as if it were a C{FileDescriptor}.
+ """
+
+ def __init__(self):
+ self.events = []
+
+
+ def fileno(self):
+ return 1
+
+
+ def doRead(self):
+ self.events.append("read")
+
+
+ def doWrite(self):
+ self.events.append("write")
+
+
+ def connectionLost(self, reason):
+ reason.trap(ConnectionDone)
+ self.events.append("lost")
+
+
+
+class ContinuousPollingTests(TestCase):
+ """
+ L{_ContinuousPolling} can be used to read and write from C{FileDescriptor}
+ objects.
+ """
+
+ def test_addReader(self):
+ """
+ Adding a reader when there was previously no reader starts up a
+ C{LoopingCall}.
+ """
+ poller = _ContinuousPolling(Clock())
+ self.assertEqual(poller._loop, None)
+ reader = object()
+ self.assertFalse(poller.isReading(reader))
+ poller.addReader(reader)
+ self.assertNotEqual(poller._loop, None)
+ self.assertTrue(poller._loop.running)
+ self.assertIdentical(poller._loop.clock, poller._reactor)
+ self.assertTrue(poller.isReading(reader))
+
+
+ def test_addWriter(self):
+ """
+ Adding a writer when there was previously no writer starts up a
+ C{LoopingCall}.
+ """
+ poller = _ContinuousPolling(Clock())
+ self.assertEqual(poller._loop, None)
+ writer = object()
+ self.assertFalse(poller.isWriting(writer))
+ poller.addWriter(writer)
+ self.assertNotEqual(poller._loop, None)
+ self.assertTrue(poller._loop.running)
+ self.assertIdentical(poller._loop.clock, poller._reactor)
+ self.assertTrue(poller.isWriting(writer))
+
+
+ def test_removeReader(self):
+ """
+ Removing a reader stops the C{LoopingCall}.
+ """
+ poller = _ContinuousPolling(Clock())
+ reader = object()
+ poller.addReader(reader)
+ poller.removeReader(reader)
+ self.assertEqual(poller._loop, None)
+ self.assertEqual(poller._reactor.getDelayedCalls(), [])
+ self.assertFalse(poller.isReading(reader))
+
+
+ def test_removeWriter(self):
+ """
+ Removing a writer stops the C{LoopingCall}.
+ """
+ poller = _ContinuousPolling(Clock())
+ writer = object()
+ poller.addWriter(writer)
+ poller.removeWriter(writer)
+ self.assertEqual(poller._loop, None)
+ self.assertEqual(poller._reactor.getDelayedCalls(), [])
+ self.assertFalse(poller.isWriting(writer))
+
+
+ def test_removeUnknown(self):
+ """
+ Removing unknown readers and writers silently does nothing.
+ """
+ poller = _ContinuousPolling(Clock())
+ poller.removeWriter(object())
+ poller.removeReader(object())
+
+
+ def test_multipleReadersAndWriters(self):
+ """
+ Adding multiple readers and writers results in a single
+ C{LoopingCall}.
+ """
+ poller = _ContinuousPolling(Clock())
+ writer = object()
+ poller.addWriter(writer)
+ self.assertNotEqual(poller._loop, None)
+ poller.addWriter(object())
+ self.assertNotEqual(poller._loop, None)
+ poller.addReader(object())
+ self.assertNotEqual(poller._loop, None)
+ poller.addReader(object())
+ poller.removeWriter(writer)
+ self.assertNotEqual(poller._loop, None)
+ self.assertTrue(poller._loop.running)
+ self.assertEqual(len(poller._reactor.getDelayedCalls()), 1)
+
+
+ def test_readerPolling(self):
+ """
+ Adding a reader causes its C{doRead} to be called every 1
+ milliseconds.
+ """
+ reactor = Clock()
+ poller = _ContinuousPolling(reactor)
+ desc = Descriptor()
+ poller.addReader(desc)
+ self.assertEqual(desc.events, [])
+ reactor.advance(0.00001)
+ self.assertEqual(desc.events, ["read"])
+ reactor.advance(0.00001)
+ self.assertEqual(desc.events, ["read", "read"])
+ reactor.advance(0.00001)
+ self.assertEqual(desc.events, ["read", "read", "read"])
+
+
+ def test_writerPolling(self):
+ """
+ Adding a writer causes its C{doWrite} to be called every 1
+ milliseconds.
+ """
+ reactor = Clock()
+ poller = _ContinuousPolling(reactor)
+ desc = Descriptor()
+ poller.addWriter(desc)
+ self.assertEqual(desc.events, [])
+ reactor.advance(0.001)
+ self.assertEqual(desc.events, ["write"])
+ reactor.advance(0.001)
+ self.assertEqual(desc.events, ["write", "write"])
+ reactor.advance(0.001)
+ self.assertEqual(desc.events, ["write", "write", "write"])
+
+
+ def test_connectionLostOnRead(self):
+ """
+ If a C{doRead} returns a value indicating disconnection,
+ C{connectionLost} is called on it.
+ """
+ reactor = Clock()
+ poller = _ContinuousPolling(reactor)
+ desc = Descriptor()
+ desc.doRead = lambda: ConnectionDone()
+ poller.addReader(desc)
+ self.assertEqual(desc.events, [])
+ reactor.advance(0.001)
+ self.assertEqual(desc.events, ["lost"])
+
+
+ def test_connectionLostOnWrite(self):
+ """
+ If a C{doWrite} returns a value indicating disconnection,
+ C{connectionLost} is called on it.
+ """
+ reactor = Clock()
+ poller = _ContinuousPolling(reactor)
+ desc = Descriptor()
+ desc.doWrite = lambda: ConnectionDone()
+ poller.addWriter(desc)
+ self.assertEqual(desc.events, [])
+ reactor.advance(0.001)
+ self.assertEqual(desc.events, ["lost"])
+
+
+ def test_removeAll(self):
+ """
+ L{_ContinuousPolling.removeAll} removes all descriptors and returns
+ the readers and writers.
+ """
+ poller = _ContinuousPolling(Clock())
+ reader = object()
+ writer = object()
+ both = object()
+ poller.addReader(reader)
+ poller.addReader(both)
+ poller.addWriter(writer)
+ poller.addWriter(both)
+ removed = poller.removeAll()
+ self.assertEqual(poller.getReaders(), [])
+ self.assertEqual(poller.getWriters(), [])
+ self.assertEqual(len(removed), 3)
+ self.assertEqual(set(removed), set([reader, writer, both]))
+
+
+ def test_getReaders(self):
+ """
+ L{_ContinuousPolling.getReaders} returns a list of the read
+ descriptors.
+ """
+ poller = _ContinuousPolling(Clock())
+ reader = object()
+ poller.addReader(reader)
+ self.assertIn(reader, poller.getReaders())
+
+
+ def test_getWriters(self):
+ """
+ L{_ContinuousPolling.getWriters} returns a list of the write
+ descriptors.
+ """
+ poller = _ContinuousPolling(Clock())
+ writer = object()
+ poller.addWriter(writer)
+ self.assertIn(writer, poller.getWriters())
+
+ if _ContinuousPolling is None:
+ skip = "epoll not supported in this environment."
diff --git a/twisted/internet/test/test_fdset.py b/twisted/internet/test/test_fdset.py
new file mode 100644
index 0000000..f05ca08
--- /dev/null
+++ b/twisted/internet/test/test_fdset.py
@@ -0,0 +1,394 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorFDSet}.
+"""
+
+__metaclass__ = type
+
+import os, socket, traceback
+
+from zope.interface import implements
+
+from twisted.python.runtime import platform
+from twisted.trial.unittest import SkipTest
+from twisted.internet.interfaces import IReactorFDSet, IReadDescriptor
+from twisted.internet.abstract import FileDescriptor
+from twisted.internet.test.reactormixins import ReactorBuilder
+
+# twisted.internet.tcp nicely defines some names with proper values on
+# several different platforms.
+from twisted.internet.tcp import EINPROGRESS, EWOULDBLOCK
+
+
+def socketpair():
+ serverSocket = socket.socket()
+ serverSocket.bind(('127.0.0.1', 0))
+ serverSocket.listen(1)
+ try:
+ client = socket.socket()
+ try:
+ client.setblocking(False)
+ try:
+ client.connect(('127.0.0.1', serverSocket.getsockname()[1]))
+ except socket.error, e:
+ if e.args[0] not in (EINPROGRESS, EWOULDBLOCK):
+ raise
+ server, addr = serverSocket.accept()
+ except:
+ client.close()
+ raise
+ finally:
+ serverSocket.close()
+
+ return client, server
+
+
+class ReactorFDSetTestsBuilder(ReactorBuilder):
+ """
+ Builder defining tests relating to L{IReactorFDSet}.
+ """
+ requiredInterfaces = [IReactorFDSet]
+
+ def _connectedPair(self):
+ """
+ Return the two sockets which make up a new TCP connection.
+ """
+ client, server = socketpair()
+ self.addCleanup(client.close)
+ self.addCleanup(server.close)
+ return client, server
+
+
+ def _simpleSetup(self):
+ reactor = self.buildReactor()
+
+ client, server = self._connectedPair()
+
+ fd = FileDescriptor(reactor)
+ fd.fileno = client.fileno
+
+ return reactor, fd, server
+
+
+ def test_addReader(self):
+ """
+ C{reactor.addReader()} accepts an L{IReadDescriptor} provider and calls
+ its C{doRead} method when there may be data available on its C{fileno}.
+ """
+ reactor, fd, server = self._simpleSetup()
+
+ def removeAndStop():
+ reactor.removeReader(fd)
+ reactor.stop()
+ fd.doRead = removeAndStop
+ reactor.addReader(fd)
+ server.sendall('x')
+
+ # The reactor will only stop if it calls fd.doRead.
+ self.runReactor(reactor)
+ # Nothing to assert, just be glad we got this far.
+
+
+ def test_removeReader(self):
+ """
+ L{reactor.removeReader()} accepts an L{IReadDescriptor} provider
+ previously passed to C{reactor.addReader()} and causes it to no longer
+ be monitored for input events.
+ """
+ reactor, fd, server = self._simpleSetup()
+
+ def fail():
+ self.fail("doRead should not be called")
+ fd.doRead = fail
+
+ reactor.addReader(fd)
+ reactor.removeReader(fd)
+ server.sendall('x')
+
+ # Give the reactor two timed event passes to notice that there's I/O
+ # (if it is incorrectly watching for I/O).
+ reactor.callLater(0, reactor.callLater, 0, reactor.stop)
+
+ self.runReactor(reactor)
+ # Getting here means the right thing happened probably.
+
+
+ def test_addWriter(self):
+ """
+ C{reactor.addWriter()} accepts an L{IWriteDescriptor} provider and
+ calls its C{doWrite} method when it may be possible to write to its
+ C{fileno}.
+ """
+ reactor, fd, server = self._simpleSetup()
+
+ def removeAndStop():
+ reactor.removeWriter(fd)
+ reactor.stop()
+ fd.doWrite = removeAndStop
+ reactor.addWriter(fd)
+
+ self.runReactor(reactor)
+ # Getting here is great.
+
+
+ def _getFDTest(self, kind):
+ """
+ Helper for getReaders and getWriters tests.
+ """
+ reactor = self.buildReactor()
+ get = getattr(reactor, 'get' + kind + 's')
+ add = getattr(reactor, 'add' + kind)
+ remove = getattr(reactor, 'remove' + kind)
+
+ client, server = self._connectedPair()
+
+ self.assertNotIn(client, get())
+ self.assertNotIn(server, get())
+
+ add(client)
+ self.assertIn(client, get())
+ self.assertNotIn(server, get())
+
+ remove(client)
+ self.assertNotIn(client, get())
+ self.assertNotIn(server, get())
+
+
+ def test_getReaders(self):
+ """
+ L{IReactorFDSet.getReaders} reflects the additions and removals made
+ with L{IReactorFDSet.addReader} and L{IReactorFDSet.removeReader}.
+ """
+ self._getFDTest('Reader')
+
+
+ def test_removeWriter(self):
+ """
+ L{reactor.removeWriter()} accepts an L{IWriteDescriptor} provider
+ previously passed to C{reactor.addWriter()} and causes it to no longer
+ be monitored for outputability.
+ """
+ reactor, fd, server = self._simpleSetup()
+
+ def fail():
+ self.fail("doWrite should not be called")
+ fd.doWrite = fail
+
+ reactor.addWriter(fd)
+ reactor.removeWriter(fd)
+
+ # Give the reactor two timed event passes to notice that there's I/O
+ # (if it is incorrectly watching for I/O).
+ reactor.callLater(0, reactor.callLater, 0, reactor.stop)
+
+ self.runReactor(reactor)
+ # Getting here means the right thing happened probably.
+
+
+ def test_getWriters(self):
+ """
+ L{IReactorFDSet.getWriters} reflects the additions and removals made
+ with L{IReactorFDSet.addWriter} and L{IReactorFDSet.removeWriter}.
+ """
+ self._getFDTest('Writer')
+
+
+ def test_removeAll(self):
+ """
+ C{reactor.removeAll()} removes all registered L{IReadDescriptor}
+ providers and all registered L{IWriteDescriptor} providers and returns
+ them.
+ """
+ reactor = self.buildReactor()
+
+ reactor, fd, server = self._simpleSetup()
+
+ fd.doRead = lambda: self.fail("doRead should not be called")
+ fd.doWrite = lambda: self.fail("doWrite should not be called")
+
+ server.sendall('x')
+
+ reactor.addReader(fd)
+ reactor.addWriter(fd)
+
+ removed = reactor.removeAll()
+
+ # Give the reactor two timed event passes to notice that there's I/O
+ # (if it is incorrectly watching for I/O).
+ reactor.callLater(0, reactor.callLater, 0, reactor.stop)
+
+ self.runReactor(reactor)
+ # Getting here means the right thing happened probably.
+
+ self.assertEqual(removed, [fd])
+
+
+ def test_removedFromReactor(self):
+ """
+ A descriptor's C{fileno} method should not be called after the
+ descriptor has been removed from the reactor.
+ """
+ reactor = self.buildReactor()
+ descriptor = RemovingDescriptor(reactor)
+ reactor.callWhenRunning(descriptor.start)
+ self.runReactor(reactor)
+ self.assertEqual(descriptor.calls, [])
+
+
+ def test_negativeOneFileDescriptor(self):
+ """
+ If L{FileDescriptor.fileno} returns C{-1}, the descriptor is removed
+ from the reactor.
+ """
+ reactor = self.buildReactor()
+
+ client, server = self._connectedPair()
+
+ class DisappearingDescriptor(FileDescriptor):
+ _fileno = server.fileno()
+
+ _received = ""
+
+ def fileno(self):
+ return self._fileno
+
+ def doRead(self):
+ self._fileno = -1
+ self._received += server.recv(1)
+ client.send('y')
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+ descriptor = DisappearingDescriptor(reactor)
+ reactor.addReader(descriptor)
+ client.send('x')
+ self.runReactor(reactor)
+ self.assertEqual(descriptor._received, "x")
+
+
+ def test_lostFileDescriptor(self):
+ """
+ The file descriptor underlying a FileDescriptor may be closed and
+ replaced by another at some point. Bytes which arrive on the new
+ descriptor must not be delivered to the FileDescriptor which was
+ originally registered with the original descriptor of the same number.
+
+ Practically speaking, this is difficult or impossible to detect. The
+ implementation relies on C{fileno} raising an exception if the original
+ descriptor has gone away. If C{fileno} continues to return the original
+ file descriptor value, the reactor may deliver events from that
+ descriptor. This is a best effort attempt to ease certain debugging
+ situations. Applications should not rely on it intentionally.
+ """
+ reactor = self.buildReactor()
+
+ name = reactor.__class__.__name__
+ if name in ('EPollReactor', 'KQueueReactor', 'CFReactor'):
+ # Closing a file descriptor immediately removes it from the epoll
+ # set without generating a notification. That means epollreactor
+ # will not call any methods on Victim after the close, so there's
+ # no chance to notice the socket is no longer valid.
+ raise SkipTest("%r cannot detect lost file descriptors" % (name,))
+
+ client, server = self._connectedPair()
+
+ class Victim(FileDescriptor):
+ """
+ This L{FileDescriptor} will have its socket closed out from under it
+ and another socket will take its place. It will raise a
+ socket.error from C{fileno} after this happens (because socket
+ objects remember whether they have been closed), so as long as the
+ reactor calls the C{fileno} method the problem will be detected.
+ """
+ def fileno(self):
+ return server.fileno()
+
+ def doRead(self):
+ raise Exception("Victim.doRead should never be called")
+
+ def connectionLost(self, reason):
+ """
+ When the problem is detected, the reactor should disconnect this
+ file descriptor. When that happens, stop the reactor so the
+ test ends.
+ """
+ reactor.stop()
+
+ reactor.addReader(Victim())
+
+ # Arrange for the socket to be replaced at some unspecified time.
+ # Significantly, this will not be while any I/O processing code is on
+ # the stack. It is something that happens independently and cannot be
+ # relied upon to happen at a convenient time, such as within a call to
+ # doRead.
+ def messItUp():
+ newC, newS = self._connectedPair()
+ fileno = server.fileno()
+ server.close()
+ os.dup2(newS.fileno(), fileno)
+ newC.send("x")
+ reactor.callLater(0, messItUp)
+
+ self.runReactor(reactor)
+
+ # If the implementation feels like logging the exception raised by
+ # MessedUp.fileno, that's fine.
+ self.flushLoggedErrors(socket.error)
+ if platform.isWindows():
+ test_lostFileDescriptor.skip = (
+ "Cannot duplicate socket filenos on Windows")
+
+
+
+class RemovingDescriptor(object):
+ """
+ A read descriptor which removes itself from the reactor as soon as it
+ gets a chance to do a read and keeps track of when its own C{fileno}
+ method is called.
+
+ @ivar insideReactor: A flag which is true as long as the reactor has
+ this descriptor as a reader.
+
+ @ivar calls: A list of the bottom of the call stack for any call to
+ C{fileno} when C{insideReactor} is false.
+ """
+ implements(IReadDescriptor)
+
+
+ def __init__(self, reactor):
+ self.reactor = reactor
+ self.insideReactor = False
+ self.calls = []
+ self.read, self.write = socketpair()
+
+
+ def start(self):
+ self.insideReactor = True
+ self.reactor.addReader(self)
+ self.write.send('a')
+
+
+ def logPrefix(self):
+ return 'foo'
+
+
+ def doRead(self):
+ self.reactor.removeReader(self)
+ self.insideReactor = False
+ self.reactor.stop()
+
+
+ def fileno(self):
+ if not self.insideReactor:
+ self.calls.append(traceback.extract_stack(limit=5)[:-1])
+ return self.read.fileno()
+
+
+ def connectionLost(self, reason):
+ pass
+
+
+globals().update(ReactorFDSetTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_filedescriptor.py b/twisted/internet/test/test_filedescriptor.py
new file mode 100644
index 0000000..5537a67
--- /dev/null
+++ b/twisted/internet/test/test_filedescriptor.py
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Whitebox tests for L{twisted.internet.abstract.FileDescriptor}.
+"""
+
+from zope.interface.verify import verifyClass
+
+from twisted.internet.abstract import FileDescriptor
+from twisted.internet.interfaces import IPushProducer
+from twisted.trial.unittest import TestCase
+
+
+
+class FileDescriptorTests(TestCase):
+ """
+ Tests for L{FileDescriptor}.
+ """
+ def test_writeWithUnicodeRaisesException(self):
+ """
+ L{FileDescriptor.write} doesn't accept unicode data.
+ """
+ fileDescriptor = FileDescriptor()
+ self.assertRaises(TypeError, fileDescriptor.write, u'foo')
+
+
+ def test_writeSequenceWithUnicodeRaisesException(self):
+ """
+ L{FileDescriptor.writeSequence} doesn't accept unicode data.
+ """
+ fileDescriptor = FileDescriptor()
+ self.assertRaises(
+ TypeError, fileDescriptor.writeSequence, ['foo', u'bar', 'baz'])
+
+
+ def test_implementInterfaceIPushProducer(self):
+ """
+ L{FileDescriptor} should implement L{IPushProducer}.
+ """
+ self.assertTrue(verifyClass(IPushProducer, FileDescriptor))
diff --git a/twisted/internet/test/test_glibbase.py b/twisted/internet/test/test_glibbase.py
new file mode 100644
index 0000000..0bf79d8
--- /dev/null
+++ b/twisted/internet/test/test_glibbase.py
@@ -0,0 +1,66 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for twisted.internet.glibbase.
+"""
+
+import sys
+from twisted.trial.unittest import TestCase
+from twisted.internet._glibbase import ensureNotImported
+
+
+
+class EnsureNotImportedTests(TestCase):
+ """
+ L{ensureNotImported} protects against unwanted past and future imports.
+ """
+
+ def test_ensureWhenNotImported(self):
+ """
+ If the specified modules have never been imported, and import
+ prevention is requested, L{ensureNotImported} makes sure they will not
+ be imported in the future.
+ """
+ modules = {}
+ self.patch(sys, "modules", modules)
+ ensureNotImported(["m1", "m2"], "A message.",
+ preventImports=["m1", "m2", "m3"])
+ self.assertEquals(modules, {"m1": None, "m2": None, "m3": None})
+
+
+ def test_ensureWhenNotImportedDontPrevent(self):
+ """
+ If the specified modules have never been imported, and import
+ prevention is not requested, L{ensureNotImported} has no effect.
+ """
+ modules = {}
+ self.patch(sys, "modules", modules)
+ ensureNotImported(["m1", "m2"], "A message.")
+ self.assertEquals(modules, {})
+
+
+ def test_ensureWhenFailedToImport(self):
+ """
+ If the specified modules have been set to C{None} in C{sys.modules},
+ L{ensureNotImported} does not complain.
+ """
+ modules = {"m2": None}
+ self.patch(sys, "modules", modules)
+ ensureNotImported(["m1", "m2"], "A message.", preventImports=["m1", "m2"])
+ self.assertEquals(modules, {"m1": None, "m2": None})
+
+
+ def test_ensureFailsWhenImported(self):
+ """
+ If one of the specified modules has been previously imported,
+ L{ensureNotImported} raises an exception.
+ """
+ module = object()
+ modules = {"m2": module}
+ self.patch(sys, "modules", modules)
+ e = self.assertRaises(ImportError, ensureNotImported,
+ ["m1", "m2"], "A message.",
+ preventImports=["m1", "m2"])
+ self.assertEquals(modules, {"m2": module})
+ self.assertEquals(e.args, ("A message.",))
diff --git a/twisted/internet/test/test_gtk3reactor.py b/twisted/internet/test/test_gtk3reactor.py
new file mode 100644
index 0000000..60a20e0
--- /dev/null
+++ b/twisted/internet/test/test_gtk3reactor.py
@@ -0,0 +1,152 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+GI/GTK3 reactor tests.
+"""
+
+try:
+ from twisted.internet import gireactor, gtk3reactor
+ from gi.repository import Gtk, Gio
+except ImportError:
+ gireactor = None
+
+from twisted.internet.error import ReactorAlreadyRunning
+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.internet.test.reactormixins import ReactorBuilder
+
+
+
+class GtkApplicationRegistration(ReactorBuilder, TestCase):
+ """
+ GtkApplication and GApplication are supported by
+ L{twisted.internet.gtk3reactor} and L{twisted.internet.gireactor}.
+
+ We inherit from L{ReactorBuilder} in order to use some of its
+ reactor-running infrastructure, but don't need its test-creation
+ functionality.
+ """
+ if gireactor is None:
+ skip = "gtk3/gi not importable"
+
+
+ def runReactor(self, app, reactor):
+ """
+ Register the app, run the reactor, make sure app was activated, and
+ that reactor was running, and that reactor can be stopped.
+ """
+ if not hasattr(app, "quit"):
+ raise SkipTest("Version of PyGObject is too old.")
+
+ result = []
+ def stop():
+ result.append("stopped")
+ reactor.stop()
+ def activate(widget):
+ result.append("activated")
+ reactor.callLater(0, stop)
+ app.connect('activate', activate)
+
+ # We want reactor.stop() to *always* stop the event loop, even if
+ # someone has called hold() on the application and never done the
+ # corresponding release() -- for more details see
+ # http://developer.gnome.org/gio/unstable/GApplication.html.
+ app.hold()
+
+ reactor.registerGApplication(app)
+ ReactorBuilder.runReactor(self, reactor)
+ self.assertEqual(result, ["activated", "stopped"])
+
+
+ def test_gApplicationActivate(self):
+ """
+ L{Gio.Application} instances can be registered with a gireactor.
+ """
+ reactor = gireactor.GIReactor(useGtk=False)
+ self.addCleanup(self.unbuildReactor, reactor)
+ app = Gio.Application(
+ application_id='com.twistedmatrix.trial.gireactor',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+
+ self.runReactor(app, reactor)
+
+
+ def test_gtkApplicationActivate(self):
+ """
+ L{Gtk.Application} instances can be registered with a gtk3reactor.
+ """
+ reactor = gtk3reactor.Gtk3Reactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+ app = Gtk.Application(
+ application_id='com.twistedmatrix.trial.gtk3reactor',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+
+ self.runReactor(app, reactor)
+
+
+ def test_portable(self):
+ """
+ L{gireactor.PortableGIReactor} doesn't support application
+ registration at this time.
+ """
+ reactor = gireactor.PortableGIReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+ app = Gio.Application(
+ application_id='com.twistedmatrix.trial.gireactor',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+ self.assertRaises(NotImplementedError,
+ reactor.registerGApplication, app)
+
+
+ def test_noQuit(self):
+ """
+ Older versions of PyGObject lack C{Application.quit}, and so won't
+ allow registration.
+ """
+ reactor = gireactor.GIReactor(useGtk=False)
+ self.addCleanup(self.unbuildReactor, reactor)
+ # An app with no "quit" method:
+ app = object()
+ exc = self.assertRaises(RuntimeError, reactor.registerGApplication, app)
+ self.assertTrue(exc.args[0].startswith(
+ "Application registration is not"))
+
+
+ def test_cantRegisterAfterRun(self):
+ """
+ It is not possible to register a C{Application} after the reactor has
+ already started.
+ """
+ reactor = gireactor.GIReactor(useGtk=False)
+ self.addCleanup(self.unbuildReactor, reactor)
+ app = Gio.Application(
+ application_id='com.twistedmatrix.trial.gireactor',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+
+ def tryRegister():
+ exc = self.assertRaises(ReactorAlreadyRunning,
+ reactor.registerGApplication, app)
+ self.assertEqual(exc.args[0],
+ "Can't register application after reactor was started.")
+ reactor.stop()
+ reactor.callLater(0, tryRegister)
+ ReactorBuilder.runReactor(self, reactor)
+
+
+ def test_cantRegisterTwice(self):
+ """
+ It is not possible to register more than one C{Application}.
+ """
+ reactor = gireactor.GIReactor(useGtk=False)
+ self.addCleanup(self.unbuildReactor, reactor)
+ app = Gio.Application(
+ application_id='com.twistedmatrix.trial.gireactor',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+ reactor.registerGApplication(app)
+ app2 = Gio.Application(
+ application_id='com.twistedmatrix.trial.gireactor2',
+ flags=Gio.ApplicationFlags.FLAGS_NONE)
+ exc = self.assertRaises(RuntimeError,
+ reactor.registerGApplication, app2)
+ self.assertEqual(exc.args[0],
+ "Can't register more than one application instance.")
diff --git a/twisted/internet/test/test_gtkreactor.py b/twisted/internet/test/test_gtkreactor.py
new file mode 100644
index 0000000..78039c0
--- /dev/null
+++ b/twisted/internet/test/test_gtkreactor.py
@@ -0,0 +1,95 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests to ensure all attributes of L{twisted.internet.gtkreactor} are
+deprecated.
+"""
+
+import sys
+
+from twisted.trial.unittest import TestCase
+
+
+class GtkReactorDeprecation(TestCase):
+ """
+ Tests to ensure all attributes of L{twisted.internet.gtkreactor} are
+ deprecated.
+ """
+
+ class StubGTK:
+ class GDK:
+ INPUT_READ = None
+ def input_add(self, *params):
+ pass
+
+ class StubPyGTK:
+ def require(self, something):
+ pass
+
+ def setUp(self):
+ """
+ Create a stub for the module 'gtk' if it does not exist, so that it can
+ be imported without errors or warnings.
+ """
+ self.mods = sys.modules.copy()
+ sys.modules['gtk'] = self.StubGTK()
+ sys.modules['pygtk'] = self.StubPyGTK()
+
+
+ def tearDown(self):
+ """
+ Return sys.modules to the way it was before the test.
+ """
+ sys.modules.clear()
+ sys.modules.update(self.mods)
+
+
+ def lookForDeprecationWarning(self, testmethod, attributeName):
+ warningsShown = self.flushWarnings([testmethod])
+ self.assertEqual(len(warningsShown), 1)
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warningsShown[0]['message'],
+ "twisted.internet.gtkreactor." + attributeName + " "
+ "was deprecated in Twisted 10.1.0: All new applications should be "
+ "written with gtk 2.x, which is supported by "
+ "twisted.internet.gtk2reactor.")
+
+
+ def test_gtkReactor(self):
+ """
+ Test deprecation of L{gtkreactor.GtkReactor}
+ """
+ from twisted.internet import gtkreactor
+ gtkreactor.GtkReactor();
+ self.lookForDeprecationWarning(self.test_gtkReactor, "GtkReactor")
+
+
+ def test_portableGtkReactor(self):
+ """
+ Test deprecation of L{gtkreactor.GtkReactor}
+ """
+ from twisted.internet import gtkreactor
+ gtkreactor.PortableGtkReactor()
+ self.lookForDeprecationWarning(self.test_portableGtkReactor,
+ "PortableGtkReactor")
+
+
+ def test_install(self):
+ """
+ Test deprecation of L{gtkreactor.install}
+ """
+ from twisted.internet import gtkreactor
+ self.assertRaises(AssertionError, gtkreactor.install)
+ self.lookForDeprecationWarning(self.test_install, "install")
+
+
+ def test_portableInstall(self):
+ """
+ Test deprecation of L{gtkreactor.portableInstall}
+ """
+ from twisted.internet import gtkreactor
+ self.assertRaises(AssertionError, gtkreactor.portableInstall)
+ self.lookForDeprecationWarning(self.test_portableInstall,
+ "portableInstall")
diff --git a/twisted/internet/test/test_inlinecb.py b/twisted/internet/test/test_inlinecb.py
new file mode 100644
index 0000000..7b818d2
--- /dev/null
+++ b/twisted/internet/test/test_inlinecb.py
@@ -0,0 +1,13 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Conditional import of C{inlinecb_tests} for Python 2.5 and greater.
+"""
+import sys
+
+__all__ = ['NonLocalExitTests']
+
+if sys.version_info[:2] >= (2, 5):
+ from twisted.internet.test.inlinecb_tests import NonLocalExitTests
+
diff --git a/twisted/internet/test/test_inotify.py b/twisted/internet/test/test_inotify.py
new file mode 100644
index 0000000..a003562
--- /dev/null
+++ b/twisted/internet/test/test_inotify.py
@@ -0,0 +1,504 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the inotify wrapper in L{twisted.internet.inotify}.
+"""
+
+from twisted.internet import defer, reactor
+from twisted.python import filepath, runtime
+from twisted.trial import unittest
+
+try:
+ from twisted.python import _inotify
+except ImportError:
+ inotify = None
+else:
+ from twisted.internet import inotify
+
+
+
+class TestINotify(unittest.TestCase):
+ """
+ Define all the tests for the basic functionality exposed by
+ L{inotify.INotify}.
+ """
+ if not runtime.platform.supportsINotify():
+ skip = "This platform doesn't support INotify."
+
+ def setUp(self):
+ self.dirname = filepath.FilePath(self.mktemp())
+ self.dirname.createDirectory()
+ self.inotify = inotify.INotify()
+ self.inotify.startReading()
+ self.addCleanup(self.inotify.loseConnection)
+
+
+ def test_initializationErrors(self):
+ """
+ L{inotify.INotify} emits a C{RuntimeError} when initialized
+ in an environment that doesn't support inotify as we expect it.
+
+ We just try to raise an exception for every possible case in
+ the for loop in L{inotify.INotify._inotify__init__}.
+ """
+ class FakeINotify:
+ def init(self):
+ raise inotify.INotifyError()
+ self.patch(inotify.INotify, '_inotify', FakeINotify())
+ self.assertRaises(inotify.INotifyError, inotify.INotify)
+
+
+ def _notificationTest(self, mask, operation, expectedPath=None):
+ """
+ Test notification from some filesystem operation.
+
+ @param mask: The event mask to use when setting up the watch.
+
+ @param operation: A function which will be called with the
+ name of a file in the watched directory and which should
+ trigger the event.
+
+ @param expectedPath: Optionally, the name of the path which is
+ expected to come back in the notification event; this will
+ also be passed to C{operation} (primarily useful when the
+ operation is being done to the directory itself, not a
+ file in it).
+
+ @return: A L{Deferred} which fires successfully when the
+ expected event has been received or fails otherwise.
+ """
+ if expectedPath is None:
+ expectedPath = self.dirname.child("foo.bar")
+ notified = defer.Deferred()
+ def cbNotified((watch, filename, events)):
+ self.assertEqual(filename, expectedPath)
+ self.assertTrue(events & mask)
+ notified.addCallback(cbNotified)
+
+ self.inotify.watch(
+ self.dirname, mask=mask,
+ callbacks=[lambda *args: notified.callback(args)])
+ operation(expectedPath)
+ return notified
+
+
+ def test_access(self):
+ """
+ Reading from a file in a monitored directory sends an
+ C{inotify.IN_ACCESS} event to the callback.
+ """
+ def operation(path):
+ path.setContent("foo")
+ path.getContent()
+
+ return self._notificationTest(inotify.IN_ACCESS, operation)
+
+
+ def test_modify(self):
+ """
+ Writing to a file in a monitored directory sends an
+ C{inotify.IN_MODIFY} event to the callback.
+ """
+ def operation(path):
+ fObj = path.open("w")
+ fObj.write('foo')
+ fObj.close()
+
+ return self._notificationTest(inotify.IN_MODIFY, operation)
+
+
+ def test_attrib(self):
+ """
+ Changing the metadata of a a file in a monitored directory
+ sends an C{inotify.IN_ATTRIB} event to the callback.
+ """
+ def operation(path):
+ path.touch()
+ path.touch()
+
+ return self._notificationTest(inotify.IN_ATTRIB, operation)
+
+
+ def test_closeWrite(self):
+ """
+ Closing a file which was open for writing in a monitored
+ directory sends an C{inotify.IN_CLOSE_WRITE} event to the
+ callback.
+ """
+ def operation(path):
+ fObj = path.open("w")
+ fObj.close()
+
+ return self._notificationTest(inotify.IN_CLOSE_WRITE, operation)
+
+
+ def test_closeNoWrite(self):
+ """
+ Closing a file which was open for reading but not writing in a
+ monitored directory sends an C{inotify.IN_CLOSE_NOWRITE} event
+ to the callback.
+ """
+ def operation(path):
+ path.touch()
+ fObj = path.open("r")
+ fObj.close()
+
+ return self._notificationTest(inotify.IN_CLOSE_NOWRITE, operation)
+
+
+ def test_open(self):
+ """
+ Opening a file in a monitored directory sends an
+ C{inotify.IN_OPEN} event to the callback.
+ """
+ def operation(path):
+ fObj = path.open("w")
+ fObj.close()
+
+ return self._notificationTest(inotify.IN_OPEN, operation)
+
+
+ def test_movedFrom(self):
+ """
+ Moving a file out of a monitored directory sends an
+ C{inotify.IN_MOVED_FROM} event to the callback.
+ """
+ def operation(path):
+ fObj = path.open("w")
+ fObj.close()
+ path.moveTo(filepath.FilePath(self.mktemp()))
+
+ return self._notificationTest(inotify.IN_MOVED_FROM, operation)
+
+
+ def test_movedTo(self):
+ """
+ Moving a file into a monitored directory sends an
+ C{inotify.IN_MOVED_TO} event to the callback.
+ """
+ def operation(path):
+ p = filepath.FilePath(self.mktemp())
+ p.touch()
+ p.moveTo(path)
+
+ return self._notificationTest(inotify.IN_MOVED_TO, operation)
+
+
+ def test_create(self):
+ """
+ Creating a file in a monitored directory sends an
+ C{inotify.IN_CREATE} event to the callback.
+ """
+ def operation(path):
+ fObj = path.open("w")
+ fObj.close()
+
+ return self._notificationTest(inotify.IN_CREATE, operation)
+
+
+ def test_delete(self):
+ """
+ Deleting a file in a monitored directory sends an
+ C{inotify.IN_DELETE} event to the callback.
+ """
+ def operation(path):
+ path.touch()
+ path.remove()
+
+ return self._notificationTest(inotify.IN_DELETE, operation)
+
+
+ def test_deleteSelf(self):
+ """
+ Deleting the monitored directory itself sends an
+ C{inotify.IN_DELETE_SELF} event to the callback.
+ """
+ def operation(path):
+ path.remove()
+
+ return self._notificationTest(
+ inotify.IN_DELETE_SELF, operation, expectedPath=self.dirname)
+
+
+ def test_moveSelf(self):
+ """
+ Renaming the monitored directory itself sends an
+ C{inotify.IN_MOVE_SELF} event to the callback.
+ """
+ def operation(path):
+ path.moveTo(filepath.FilePath(self.mktemp()))
+
+ return self._notificationTest(
+ inotify.IN_MOVE_SELF, operation, expectedPath=self.dirname)
+
+
+ def test_simpleSubdirectoryAutoAdd(self):
+ """
+ L{inotify.INotify} when initialized with autoAdd==True adds
+ also adds the created subdirectories to the watchlist.
+ """
+ def _callback(wp, filename, mask):
+ # We are notified before we actually process new
+ # directories, so we need to defer this check.
+ def _():
+ try:
+ self.assertTrue(self.inotify._isWatched(subdir))
+ d.callback(None)
+ except Exception:
+ d.errback()
+ reactor.callLater(0, _)
+
+ checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
+ self.inotify.watch(
+ self.dirname, mask=checkMask, autoAdd=True,
+ callbacks=[_callback])
+ subdir = self.dirname.child('test')
+ d = defer.Deferred()
+ subdir.createDirectory()
+ return d
+
+
+ def test_simpleDeleteDirectory(self):
+ """
+ L{inotify.INotify} removes a directory from the watchlist when
+ it's removed from the filesystem.
+ """
+ calls = []
+ def _callback(wp, filename, mask):
+ # We are notified before we actually process new
+ # directories, so we need to defer this check.
+ def _():
+ try:
+ self.assertTrue(self.inotify._isWatched(subdir))
+ subdir.remove()
+ except Exception:
+ d.errback()
+ def _eb():
+ # second call, we have just removed the subdir
+ try:
+ self.assertTrue(not self.inotify._isWatched(subdir))
+ d.callback(None)
+ except Exception:
+ d.errback()
+
+ if not calls:
+ # first call, it's the create subdir
+ calls.append(filename)
+ reactor.callLater(0, _)
+
+ else:
+ reactor.callLater(0, _eb)
+
+ checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
+ self.inotify.watch(
+ self.dirname, mask=checkMask, autoAdd=True,
+ callbacks=[_callback])
+ subdir = self.dirname.child('test')
+ d = defer.Deferred()
+ subdir.createDirectory()
+ return d
+
+
+ def test_ignoreDirectory(self):
+ """
+ L{inotify.INotify.ignore} removes a directory from the watchlist
+ """
+ self.inotify.watch(self.dirname, autoAdd=True)
+ self.assertTrue(self.inotify._isWatched(self.dirname))
+ self.inotify.ignore(self.dirname)
+ self.assertFalse(self.inotify._isWatched(self.dirname))
+
+
+ def test_humanReadableMask(self):
+ """
+ L{inotify.humaReadableMask} translates all the possible event
+ masks to a human readable string.
+ """
+ for mask, value in inotify._FLAG_TO_HUMAN:
+ self.assertEqual(inotify.humanReadableMask(mask)[0], value)
+
+ checkMask = (
+ inotify.IN_CLOSE_WRITE | inotify.IN_ACCESS | inotify.IN_OPEN)
+ self.assertEqual(
+ set(inotify.humanReadableMask(checkMask)),
+ set(['close_write', 'access', 'open']))
+
+
+ def test_recursiveWatch(self):
+ """
+ L{inotify.INotify.watch} with recursive==True will add all the
+ subdirectories under the given path to the watchlist.
+ """
+ subdir = self.dirname.child('test')
+ subdir2 = subdir.child('test2')
+ subdir3 = subdir2.child('test3')
+ subdir3.makedirs()
+ dirs = [subdir, subdir2, subdir3]
+ self.inotify.watch(self.dirname, recursive=True)
+ # let's even call this twice so that we test that nothing breaks
+ self.inotify.watch(self.dirname, recursive=True)
+ for d in dirs:
+ self.assertTrue(self.inotify._isWatched(d))
+
+
+ def test_connectionLostError(self):
+ """
+ L{inotify.INotify.connectionLost} if there's a problem while closing
+ the fd shouldn't raise the exception but should log the error
+ """
+ import os
+ in_ = inotify.INotify()
+ os.close(in_._fd)
+ in_.loseConnection()
+ self.flushLoggedErrors()
+
+ def test_noAutoAddSubdirectory(self):
+ """
+ L{inotify.INotify.watch} with autoAdd==False will stop inotify
+ from watching subdirectories created under the watched one.
+ """
+ def _callback(wp, fp, mask):
+ # We are notified before we actually process new
+ # directories, so we need to defer this check.
+ def _():
+ try:
+ self.assertFalse(self.inotify._isWatched(subdir.path))
+ d.callback(None)
+ except Exception:
+ d.errback()
+ reactor.callLater(0, _)
+
+ checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
+ self.inotify.watch(
+ self.dirname, mask=checkMask, autoAdd=False,
+ callbacks=[_callback])
+ subdir = self.dirname.child('test')
+ d = defer.Deferred()
+ subdir.createDirectory()
+ return d
+
+
+ def test_seriesOfWatchAndIgnore(self):
+ """
+ L{inotify.INotify} will watch a filepath for events even if the same
+ path is repeatedly added/removed/re-added to the watchpoints.
+ """
+ expectedPath = self.dirname.child("foo.bar2")
+ expectedPath.touch()
+
+ notified = defer.Deferred()
+ def cbNotified((ignored, filename, events)):
+ self.assertEqual(filename, expectedPath)
+ self.assertTrue(events & inotify.IN_DELETE_SELF)
+
+ def callIt(*args):
+ notified.callback(args)
+
+ # Watch, ignore, watch again to get into the state being tested.
+ self.assertTrue(self.inotify.watch(expectedPath, callbacks=[callIt]))
+ self.inotify.ignore(expectedPath)
+ self.assertTrue(
+ self.inotify.watch(
+ expectedPath, mask=inotify.IN_DELETE_SELF, callbacks=[callIt]))
+
+ notified.addCallback(cbNotified)
+
+ # Apparently in kernel version < 2.6.25, inofify has a bug in the way
+ # similar events are coalesced. So, be sure to generate a different
+ # event here than the touch() at the top of this method might have
+ # generated.
+ expectedPath.remove()
+
+ return notified
+
+
+ def test_ignoreFilePath(self):
+ """
+ L{inotify.INotify} will ignore a filepath after it has been removed from
+ the watch list.
+ """
+ expectedPath = self.dirname.child("foo.bar2")
+ expectedPath.touch()
+ expectedPath2 = self.dirname.child("foo.bar3")
+ expectedPath2.touch()
+
+ notified = defer.Deferred()
+ def cbNotified((ignored, filename, events)):
+ self.assertEqual(filename, expectedPath2)
+ self.assertTrue(events & inotify.IN_DELETE_SELF)
+
+ def callIt(*args):
+ notified.callback(args)
+
+ self.assertTrue(
+ self.inotify.watch(
+ expectedPath, inotify.IN_DELETE_SELF, callbacks=[callIt]))
+ notified.addCallback(cbNotified)
+
+ self.assertTrue(
+ self.inotify.watch(
+ expectedPath2, inotify.IN_DELETE_SELF, callbacks=[callIt]))
+
+ self.inotify.ignore(expectedPath)
+
+ expectedPath.remove()
+ expectedPath2.remove()
+
+ return notified
+
+
+ def test_ignoreNonWatchedFile(self):
+ """
+ L{inotify.INotify} will raise KeyError if a non-watched filepath is
+ ignored.
+ """
+ expectedPath = self.dirname.child("foo.ignored")
+ expectedPath.touch()
+
+ self.assertRaises(KeyError, self.inotify.ignore, expectedPath)
+
+
+ def test_complexSubdirectoryAutoAdd(self):
+ """
+ L{inotify.INotify} with autoAdd==True for a watched path
+ generates events for every file or directory already present
+ in a newly created subdirectory under the watched one.
+
+ This tests that we solve a race condition in inotify even though
+ we may generate duplicate events.
+ """
+ calls = set()
+ def _callback(wp, filename, mask):
+ calls.add(filename)
+ if len(calls) == 6:
+ try:
+ self.assertTrue(self.inotify._isWatched(subdir))
+ self.assertTrue(self.inotify._isWatched(subdir2))
+ self.assertTrue(self.inotify._isWatched(subdir3))
+ created = someFiles + [subdir, subdir2, subdir3]
+ self.assertEqual(len(calls), len(created))
+ self.assertEqual(calls, set(created))
+ except Exception:
+ d.errback()
+ else:
+ d.callback(None)
+
+ checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
+ self.inotify.watch(
+ self.dirname, mask=checkMask, autoAdd=True,
+ callbacks=[_callback])
+ subdir = self.dirname.child('test')
+ subdir2 = subdir.child('test2')
+ subdir3 = subdir2.child('test3')
+ d = defer.Deferred()
+ subdir3.makedirs()
+
+ someFiles = [subdir.child('file1.dat'),
+ subdir2.child('file2.dat'),
+ subdir3.child('file3.dat')]
+ # Add some files in pretty much all the directories so that we
+ # see that we process all of them.
+ for i, filename in enumerate(someFiles):
+ filename.setContent(filename.path)
+ return d
diff --git a/twisted/internet/test/test_interfaces.py b/twisted/internet/test/test_interfaces.py
new file mode 100644
index 0000000..f9f60da
--- /dev/null
+++ b/twisted/internet/test/test_interfaces.py
@@ -0,0 +1,53 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.interfaces}.
+"""
+
+from twisted.trial import unittest
+
+
+class TestIFinishableConsumer(unittest.TestCase):
+ """
+ L{IFinishableConsumer} is deprecated.
+ """
+
+ def lookForDeprecationWarning(self, testmethod):
+ """
+ Importing C{testmethod} emits a deprecation warning.
+ """
+ warningsShown = self.flushWarnings([testmethod])
+ self.assertEqual(len(warningsShown), 1)
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warningsShown[0]['message'],
+ "twisted.internet.interfaces.IFinishableConsumer "
+ "was deprecated in Twisted 11.1.0: Please use IConsumer "
+ "(and IConsumer.unregisterProducer) instead.")
+
+
+ def test_deprecationWithDirectImport(self):
+ """
+ Importing L{IFinishableConsumer} causes a deprecation warning
+ """
+ from twisted.internet.interfaces import IFinishableConsumer
+ self.lookForDeprecationWarning(
+ TestIFinishableConsumer.test_deprecationWithDirectImport)
+
+
+ def test_deprecationWithIndirectImport(self):
+ """
+ Importing L{interfaces} and implementing
+ L{interfaces.IFinishableConsumer} causes a deprecation warning
+ """
+ from zope.interface import implements
+ from twisted.internet import interfaces
+
+ class FakeIFinishableConsumer:
+ implements(interfaces.IFinishableConsumer)
+ def finish(self):
+ pass
+
+ self.lookForDeprecationWarning(
+ TestIFinishableConsumer.test_deprecationWithIndirectImport)
diff --git a/twisted/internet/test/test_iocp.py b/twisted/internet/test/test_iocp.py
new file mode 100644
index 0000000..76d7646
--- /dev/null
+++ b/twisted/internet/test/test_iocp.py
@@ -0,0 +1,150 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.iocpreactor}.
+"""
+
+import errno
+from array import array
+from struct import pack
+from socket import AF_INET6, AF_INET, SOCK_STREAM, SOL_SOCKET, error, socket
+
+from zope.interface.verify import verifyClass
+
+from twisted.trial import unittest
+from twisted.python.log import msg
+from twisted.internet.interfaces import IPushProducer
+
+try:
+ from twisted.internet.iocpreactor import iocpsupport as _iocp, tcp, udp
+ from twisted.internet.iocpreactor.reactor import IOCPReactor, EVENTS_PER_LOOP, KEY_NORMAL
+ from twisted.internet.iocpreactor.interfaces import IReadWriteHandle
+ from twisted.internet.iocpreactor.const import SO_UPDATE_ACCEPT_CONTEXT
+ from twisted.internet.iocpreactor.abstract import FileHandle
+except ImportError:
+ skip = 'This test only applies to IOCPReactor'
+
+try:
+ socket(AF_INET6, SOCK_STREAM).close()
+except error, e:
+ ipv6Skip = str(e)
+else:
+ ipv6Skip = None
+
+class SupportTests(unittest.TestCase):
+ """
+ Tests for L{twisted.internet.iocpreactor.iocpsupport}, low-level reactor
+ implementation helpers.
+ """
+ def _acceptAddressTest(self, family, localhost):
+ """
+ Create a C{SOCK_STREAM} connection to localhost using a socket with an
+ address family of C{family} and assert that the result of
+ L{iocpsupport.get_accept_addrs} is consistent with the result of
+ C{socket.getsockname} and C{socket.getpeername}.
+ """
+ msg("family = %r" % (family,))
+ port = socket(family, SOCK_STREAM)
+ self.addCleanup(port.close)
+ port.bind(('', 0))
+ port.listen(1)
+ client = socket(family, SOCK_STREAM)
+ self.addCleanup(client.close)
+ client.setblocking(False)
+ try:
+ client.connect((localhost, port.getsockname()[1]))
+ except error, (errnum, message):
+ self.assertIn(errnum, (errno.EINPROGRESS, errno.EWOULDBLOCK))
+
+ server = socket(family, SOCK_STREAM)
+ self.addCleanup(server.close)
+ buff = array('c', '\0' * 256)
+ self.assertEqual(
+ 0, _iocp.accept(port.fileno(), server.fileno(), buff, None))
+ server.setsockopt(
+ SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, pack('P', server.fileno()))
+ self.assertEqual(
+ (family, client.getpeername()[:2], client.getsockname()[:2]),
+ _iocp.get_accept_addrs(server.fileno(), buff))
+
+
+ def test_ipv4AcceptAddress(self):
+ """
+ L{iocpsupport.get_accept_addrs} returns a three-tuple of address
+ information about the socket associated with the file descriptor passed
+ to it. For a connection using IPv4:
+
+ - the first element is C{AF_INET}
+ - the second element is a two-tuple of a dotted decimal notation IPv4
+ address and a port number giving the peer address of the connection
+ - the third element is the same type giving the host address of the
+ connection
+ """
+ self._acceptAddressTest(AF_INET, '127.0.0.1')
+
+
+ def test_ipv6AcceptAddress(self):
+ """
+ Like L{test_ipv4AcceptAddress}, but for IPv6 connections. In this case:
+
+ - the first element is C{AF_INET6}
+ - the second element is a two-tuple of a hexadecimal IPv6 address
+ literal and a port number giving the peer address of the connection
+ - the third element is the same type giving the host address of the
+ connection
+ """
+ self._acceptAddressTest(AF_INET6, '::1')
+ if ipv6Skip is not None:
+ test_ipv6AcceptAddress.skip = ipv6Skip
+
+
+
+class IOCPReactorTestCase(unittest.TestCase):
+ def test_noPendingTimerEvents(self):
+ """
+ Test reactor behavior (doIteration) when there are no pending time
+ events.
+ """
+ ir = IOCPReactor()
+ ir.wakeUp()
+ self.failIf(ir.doIteration(None))
+
+
+ def test_reactorInterfaces(self):
+ """
+ Verify that IOCP socket-representing classes implement IReadWriteHandle
+ """
+ self.assertTrue(verifyClass(IReadWriteHandle, tcp.Connection))
+ self.assertTrue(verifyClass(IReadWriteHandle, udp.Port))
+
+
+ def test_fileHandleInterfaces(self):
+ """
+ Verify that L{Filehandle} implements L{IPushProducer}.
+ """
+ self.assertTrue(verifyClass(IPushProducer, FileHandle))
+
+
+ def test_maxEventsPerIteration(self):
+ """
+ Verify that we don't lose an event when more than EVENTS_PER_LOOP
+ events occur in the same reactor iteration
+ """
+ class FakeFD:
+ counter = 0
+ def logPrefix(self):
+ return 'FakeFD'
+ def cb(self, rc, bytes, evt):
+ self.counter += 1
+
+ ir = IOCPReactor()
+ fd = FakeFD()
+ event = _iocp.Event(fd.cb, fd)
+ for _ in range(EVENTS_PER_LOOP + 1):
+ ir.port.postEvent(0, KEY_NORMAL, event)
+ ir.doIteration(None)
+ self.assertEqual(fd.counter, EVENTS_PER_LOOP)
+ ir.doIteration(0)
+ self.assertEqual(fd.counter, EVENTS_PER_LOOP + 1)
+
diff --git a/twisted/internet/test/test_main.py b/twisted/internet/test/test_main.py
new file mode 100644
index 0000000..12c8a3f
--- /dev/null
+++ b/twisted/internet/test/test_main.py
@@ -0,0 +1,38 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.main}.
+"""
+
+from twisted.trial import unittest
+from twisted.internet.error import ReactorAlreadyInstalledError
+from twisted.internet.main import installReactor
+
+
+class InstallReactorTests(unittest.TestCase):
+ """
+ Tests for L{installReactor}
+ """
+
+ def test_alreadyInstalled(self):
+ """
+ If a reactor is already installed, L{installReactor} raises
+ L{ReactorAlreadyInstalledError}.
+ """
+ # Because this test runs in trial, assume a reactor is already
+ # installed.
+ self.assertRaises(ReactorAlreadyInstalledError, installReactor,
+ object())
+
+
+ def test_errorIsAnAssertionError(self):
+ """
+ For backwards compatibility, L{ReactorAlreadyInstalledError} is an
+ L{AssertionError}.
+ """
+ self.assertTrue(issubclass(ReactorAlreadyInstalledError,
+ AssertionError))
+
+
+
diff --git a/twisted/internet/test/test_newtls.py b/twisted/internet/test/test_newtls.py
new file mode 100644
index 0000000..a196cb5
--- /dev/null
+++ b/twisted/internet/test/test_newtls.py
@@ -0,0 +1,194 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet._newtls}.
+"""
+
+from twisted.trial import unittest
+from twisted.internet.test.reactormixins import ReactorBuilder, runProtocolsWithReactor
+from twisted.internet.test.reactormixins import ConnectableProtocol
+from twisted.internet.test.test_tls import SSLCreator, TLSMixin
+from twisted.internet.test.test_tls import StartTLSClientCreator
+from twisted.internet.test.test_tls import ContextGeneratingMixin
+from twisted.internet.test.test_tcp import TCPCreator
+try:
+ from twisted.protocols import tls
+ from twisted.internet import _newtls
+except ImportError:
+ _newtls = None
+
+
+class BypassTLSTests(unittest.TestCase):
+ """
+ Tests for the L{_newtls._BypassTLS} class.
+ """
+
+ if not _newtls:
+ skip = "Couldn't import _newtls, perhaps pyOpenSSL is old or missing"
+
+ def test_loseConnectionPassThrough(self):
+ """
+ C{_BypassTLS.loseConnection} calls C{loseConnection} on the base
+ class, while preserving any default argument in the base class'
+ C{loseConnection} implementation.
+ """
+ default = object()
+ result = []
+
+ class FakeTransport(object):
+ def loseConnection(self, _connDone=default):
+ result.append(_connDone)
+
+ bypass = _newtls._BypassTLS(FakeTransport, FakeTransport())
+
+ # The default from FakeTransport is used:
+ bypass.loseConnection()
+ self.assertEqual(result, [default])
+
+ # And we can pass our own:
+ notDefault = object()
+ bypass.loseConnection(notDefault)
+ self.assertEqual(result, [default, notDefault])
+
+
+
+class FakeProducer(object):
+ """
+ A producer that does nothing.
+ """
+
+ def pauseProducing(self):
+ pass
+
+
+ def resumeProducing(self):
+ pass
+
+
+ def stopProducing(self):
+ pass
+
+
+
+class ProducerProtocol(ConnectableProtocol):
+ """
+ Register a producer, unregister it, and verify the producer hooks up to
+ innards of C{TLSMemoryBIOProtocol}.
+ """
+
+ def __init__(self, producer, result):
+ self.producer = producer
+ self.result = result
+
+
+ def connectionMade(self):
+ if not isinstance(self.transport.protocol,
+ tls.TLSMemoryBIOProtocol):
+ # Either the test or the code have a bug...
+ raise RuntimeError("TLSMemoryBIOProtocol not hooked up.")
+
+ self.transport.registerProducer(self.producer, True)
+ # The producer was registered with the TLSMemoryBIOProtocol:
+ self.result.append(self.transport.protocol._producer._producer)
+
+ self.transport.unregisterProducer()
+ # The producer was unregistered from the TLSMemoryBIOProtocol:
+ self.result.append(self.transport.protocol._producer)
+ self.transport.loseConnection()
+
+
+
+class ProducerTestsMixin(ReactorBuilder, TLSMixin, ContextGeneratingMixin):
+ """
+ Test the new TLS code integrates C{TLSMemoryBIOProtocol} correctly.
+ """
+
+ if not _newtls:
+ skip = "Could not import twisted.internet._newtls"
+
+ def test_producerSSLFromStart(self):
+ """
+ C{registerProducer} and C{unregisterProducer} on TLS transports
+ created as SSL from the get go are passed to the
+ C{TLSMemoryBIOProtocol}, not the underlying transport directly.
+ """
+ result = []
+ producer = FakeProducer()
+
+ runProtocolsWithReactor(self, ConnectableProtocol(),
+ ProducerProtocol(producer, result),
+ SSLCreator())
+ self.assertEqual(result, [producer, None])
+
+
+ def test_producerAfterStartTLS(self):
+ """
+ C{registerProducer} and C{unregisterProducer} on TLS transports
+ created by C{startTLS} are passed to the C{TLSMemoryBIOProtocol}, not
+ the underlying transport directly.
+ """
+ result = []
+ producer = FakeProducer()
+
+ runProtocolsWithReactor(self, ConnectableProtocol(),
+ ProducerProtocol(producer, result),
+ StartTLSClientCreator())
+ self.assertEqual(result, [producer, None])
+
+
+ def startTLSAfterRegisterProducer(self, streaming):
+ """
+ When a producer is registered, and then startTLS is called,
+ the producer is re-registered with the C{TLSMemoryBIOProtocol}.
+ """
+ clientContext = self.getClientContext()
+ serverContext = self.getServerContext()
+ result = []
+ producer = FakeProducer()
+
+ class RegisterTLSProtocol(ConnectableProtocol):
+ def connectionMade(self):
+ self.transport.registerProducer(producer, streaming)
+ self.transport.startTLS(serverContext)
+ # Store TLSMemoryBIOProtocol and underlying transport producer
+ # status:
+ if streaming:
+ # _ProducerMembrane -> producer:
+ result.append(self.transport.protocol._producer._producer)
+ result.append(self.transport.producer._producer)
+ else:
+ # _ProducerMembrane -> _PullToPush -> producer:
+ result.append(
+ self.transport.protocol._producer._producer._producer)
+ result.append(self.transport.producer._producer._producer)
+ self.transport.unregisterProducer()
+ self.transport.loseConnection()
+
+ class StartTLSProtocol(ConnectableProtocol):
+ def connectionMade(self):
+ self.transport.startTLS(clientContext)
+
+ runProtocolsWithReactor(self, RegisterTLSProtocol(),
+ StartTLSProtocol(), TCPCreator())
+ self.assertEqual(result, [producer, producer])
+
+
+ def test_startTLSAfterRegisterProducerStreaming(self):
+ """
+ When a streaming producer is registered, and then startTLS is called,
+ the producer is re-registered with the C{TLSMemoryBIOProtocol}.
+ """
+ self.startTLSAfterRegisterProducer(True)
+
+
+ def test_startTLSAfterRegisterProducerNonStreaming(self):
+ """
+ When a non-streaming producer is registered, and then startTLS is
+ called, the producer is re-registered with the
+ C{TLSMemoryBIOProtocol}.
+ """
+ self.startTLSAfterRegisterProducer(False)
+
+
+globals().update(ProducerTestsMixin.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_pollingfile.py b/twisted/internet/test/test_pollingfile.py
new file mode 100644
index 0000000..75022ad
--- /dev/null
+++ b/twisted/internet/test/test_pollingfile.py
@@ -0,0 +1,46 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet._pollingfile}.
+"""
+
+from twisted.python.runtime import platform
+from twisted.trial.unittest import TestCase
+
+if platform.isWindows():
+ from twisted.internet import _pollingfile
+else:
+ _pollingfile = None
+
+
+
+class TestPollableWritePipe(TestCase):
+ """
+ Tests for L{_pollingfile._PollableWritePipe}.
+ """
+
+ def test_writeUnicode(self):
+ """
+ L{_pollingfile._PollableWritePipe.write} raises a C{TypeError} if an
+ attempt is made to append unicode data to the output buffer.
+ """
+ p = _pollingfile._PollableWritePipe(1, lambda: None)
+ self.assertRaises(TypeError, p.write, u"test")
+
+
+ def test_writeSequenceUnicode(self):
+ """
+ L{_pollingfile._PollableWritePipe.writeSequence} raises a C{TypeError}
+ if unicode data is part of the data sequence to be appended to the
+ output buffer.
+ """
+ p = _pollingfile._PollableWritePipe(1, lambda: None)
+ self.assertRaises(TypeError, p.writeSequence, [u"test"])
+ self.assertRaises(TypeError, p.writeSequence, (u"test", ))
+
+
+
+
+if _pollingfile is None:
+ TestPollableWritePipe.skip = "Test will run only on Windows."
diff --git a/twisted/internet/test/test_posixbase.py b/twisted/internet/test/test_posixbase.py
new file mode 100644
index 0000000..1d6c72c
--- /dev/null
+++ b/twisted/internet/test/test_posixbase.py
@@ -0,0 +1,387 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.posixbase} and supporting code.
+"""
+
+from twisted.python.compat import set
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import Deferred
+from twisted.internet.posixbase import PosixReactorBase, _Waker
+from twisted.internet.protocol import ServerFactory
+
+
+skipSockets = None
+try:
+ from twisted.internet import unix
+except ImportError:
+ skipSockets = "Platform does not support AF_UNIX sockets"
+
+from twisted.internet.tcp import Port
+from twisted.internet import reactor
+from twisted.test.test_unix import ClientProto
+
+class TrivialReactor(PosixReactorBase):
+ def __init__(self):
+ self._readers = {}
+ self._writers = {}
+ PosixReactorBase.__init__(self)
+
+
+ def addReader(self, reader):
+ self._readers[reader] = True
+
+
+ def removeReader(self, reader):
+ del self._readers[reader]
+
+
+ def addWriter(self, writer):
+ self._writers[writer] = True
+
+
+ def removeWriter(self, writer):
+ del self._writers[writer]
+
+
+
+class PosixReactorBaseTests(TestCase):
+ """
+ Tests for L{PosixReactorBase}.
+ """
+
+ def _checkWaker(self, reactor):
+ self.assertIsInstance(reactor.waker, _Waker)
+ self.assertIn(reactor.waker, reactor._internalReaders)
+ self.assertIn(reactor.waker, reactor._readers)
+
+
+ def test_wakerIsInternalReader(self):
+ """
+ When L{PosixReactorBase} is instantiated, it creates a waker and adds
+ it to its internal readers set.
+ """
+ reactor = TrivialReactor()
+ self._checkWaker(reactor)
+
+
+ def test_removeAllSkipsInternalReaders(self):
+ """
+ Any L{IReadDescriptors} in L{PosixReactorBase._internalReaders} are
+ left alone by L{PosixReactorBase._removeAll}.
+ """
+ reactor = TrivialReactor()
+ extra = object()
+ reactor._internalReaders.add(extra)
+ reactor.addReader(extra)
+ reactor._removeAll(reactor._readers, reactor._writers)
+ self._checkWaker(reactor)
+ self.assertIn(extra, reactor._internalReaders)
+ self.assertIn(extra, reactor._readers)
+
+
+ def test_removeAllReturnsRemovedDescriptors(self):
+ """
+ L{PosixReactorBase._removeAll} returns a list of removed
+ L{IReadDescriptor} and L{IWriteDescriptor} objects.
+ """
+ reactor = TrivialReactor()
+ reader = object()
+ writer = object()
+ reactor.addReader(reader)
+ reactor.addWriter(writer)
+ removed = reactor._removeAll(
+ reactor._readers, reactor._writers)
+ self.assertEqual(set(removed), set([reader, writer]))
+ self.assertNotIn(reader, reactor._readers)
+ self.assertNotIn(writer, reactor._writers)
+
+
+ def test_IReactorArbitraryIsDeprecated(self):
+ """
+ L{twisted.internet.interfaces.IReactorArbitrary} is redundant with
+ L{twisted.internet.interfaces.IReactorFDSet} and is deprecated.
+ """
+ from twisted.internet import interfaces
+ interfaces.IReactorArbitrary
+
+ warningsShown = self.flushWarnings(
+ [self.test_IReactorArbitraryIsDeprecated])
+ self.assertEqual(len(warningsShown), 1)
+ self.assertEqual(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ "twisted.internet.interfaces.IReactorArbitrary was deprecated "
+ "in Twisted 10.1.0: See IReactorFDSet.",
+ warningsShown[0]['message'])
+
+
+ def test_listenWithIsDeprecated(self):
+ """
+ L{PosixReactorBase} implements the deprecated L{IReactorArbitrary}, and
+ L{PosixReactorBase.listenWith} is a part of that interface. To avoid
+ unnecessary deprecation warnings when importing posixbase, the
+ L{twisted.internet.interfaces._IReactorArbitrary} alias that doesn't
+ have the deprecation warning is imported, and instead
+ L{PosixReactorBase.listenWith} generates its own deprecation warning.
+ """
+ class fakePort:
+ def __init__(self, *args, **kw):
+ pass
+
+ def startListening(self):
+ pass
+
+ reactor = TrivialReactor()
+ reactor.listenWith(fakePort)
+
+ warnings = self.flushWarnings([self.test_listenWithIsDeprecated])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ "listenWith is deprecated since Twisted 10.1. "
+ "See IReactorFDSet.",
+ warnings[0]['message'])
+
+
+ def test_connectWithIsDeprecated(self):
+ """
+ L{PosixReactorBase} implements the deprecated L{IReactorArbitrary}, and
+ L{PosixReactorBase.connectWith} is a part of that interface. To avoid
+ unnecessary deprecation warnings when importing posixbase, the
+ L{twisted.internet.interfaces._IReactorArbitrary} alias that doesn't
+ have the deprecation warning is imported, and instead
+ L{PosixReactorBase.connectWith} generates its own deprecation warning.
+ """
+ class fakeConnector:
+ def __init__(self, *args, **kw):
+ pass
+
+ def connect(self):
+ pass
+
+ reactor = TrivialReactor()
+ reactor.connectWith(fakeConnector)
+
+ warnings = self.flushWarnings([self.test_connectWithIsDeprecated])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ "connectWith is deprecated since Twisted 10.1. "
+ "See IReactorFDSet.",
+ warnings[0]['message'])
+
+
+
+class TCPPortTests(TestCase):
+ """
+ Tests for L{twisted.internet.tcp.Port}.
+ """
+
+ if not isinstance(reactor, PosixReactorBase):
+ skip = "Non-posixbase reactor"
+
+ def test_connectionLostFailed(self):
+ """
+ L{Port.stopListening} returns a L{Deferred} which errbacks if
+ L{Port.connectionLost} raises an exception.
+ """
+ port = Port(12345, ServerFactory())
+ port.connected = True
+ port.connectionLost = lambda reason: 1 / 0
+ return self.assertFailure(port.stopListening(), ZeroDivisionError)
+
+
+
+class TimeoutReportReactor(PosixReactorBase):
+ """
+ A reactor which is just barely runnable and which cannot monitor any
+ readers or writers, and which fires a L{Deferred} with the timeout
+ passed to its C{doIteration} method as soon as that method is invoked.
+ """
+ def __init__(self):
+ PosixReactorBase.__init__(self)
+ self.iterationTimeout = Deferred()
+ self.now = 100
+
+
+ def addReader(self, reader):
+ """
+ Ignore the reader. This is necessary because the waker will be
+ added. However, we won't actually monitor it for any events.
+ """
+
+
+ def removeAll(self):
+ """
+ There are no readers or writers, so there is nothing to remove.
+ This will be called when the reactor stops, though, so it must be
+ implemented.
+ """
+ return []
+
+
+ def seconds(self):
+ """
+ Override the real clock with a deterministic one that can be easily
+ controlled in a unit test.
+ """
+ return self.now
+
+
+ def doIteration(self, timeout):
+ d = self.iterationTimeout
+ if d is not None:
+ self.iterationTimeout = None
+ d.callback(timeout)
+
+
+
+class IterationTimeoutTests(TestCase):
+ """
+ Tests for the timeout argument L{PosixReactorBase.run} calls
+ L{PosixReactorBase.doIteration} with in the presence of various delayed
+ calls.
+ """
+ def _checkIterationTimeout(self, reactor):
+ timeout = []
+ reactor.iterationTimeout.addCallback(timeout.append)
+ reactor.iterationTimeout.addCallback(lambda ignored: reactor.stop())
+ reactor.run()
+ return timeout[0]
+
+
+ def test_noCalls(self):
+ """
+ If there are no delayed calls, C{doIteration} is called with a
+ timeout of C{None}.
+ """
+ reactor = TimeoutReportReactor()
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, None)
+
+
+ def test_delayedCall(self):
+ """
+ If there is a delayed call, C{doIteration} is called with a timeout
+ which is the difference between the current time and the time at
+ which that call is to run.
+ """
+ reactor = TimeoutReportReactor()
+ reactor.callLater(100, lambda: None)
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, 100)
+
+
+ def test_timePasses(self):
+ """
+ If a delayed call is scheduled and then some time passes, the
+ timeout passed to C{doIteration} is reduced by the amount of time
+ which passed.
+ """
+ reactor = TimeoutReportReactor()
+ reactor.callLater(100, lambda: None)
+ reactor.now += 25
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, 75)
+
+
+ def test_multipleDelayedCalls(self):
+ """
+ If there are several delayed calls, C{doIteration} is called with a
+ timeout which is the difference between the current time and the
+ time at which the earlier of the two calls is to run.
+ """
+ reactor = TimeoutReportReactor()
+ reactor.callLater(50, lambda: None)
+ reactor.callLater(10, lambda: None)
+ reactor.callLater(100, lambda: None)
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, 10)
+
+
+ def test_resetDelayedCall(self):
+ """
+ If a delayed call is reset, the timeout passed to C{doIteration} is
+ based on the interval between the time when reset is called and the
+ new delay of the call.
+ """
+ reactor = TimeoutReportReactor()
+ call = reactor.callLater(50, lambda: None)
+ reactor.now += 25
+ call.reset(15)
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, 15)
+
+
+ def test_delayDelayedCall(self):
+ """
+ If a delayed call is re-delayed, the timeout passed to
+ C{doIteration} is based on the remaining time before the call would
+ have been made and the additional amount of time passed to the delay
+ method.
+ """
+ reactor = TimeoutReportReactor()
+ call = reactor.callLater(50, lambda: None)
+ reactor.now += 10
+ call.delay(20)
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, 60)
+
+
+ def test_cancelDelayedCall(self):
+ """
+ If the only delayed call is canceled, C{None} is the timeout passed
+ to C{doIteration}.
+ """
+ reactor = TimeoutReportReactor()
+ call = reactor.callLater(50, lambda: None)
+ call.cancel()
+ timeout = self._checkIterationTimeout(reactor)
+ self.assertEqual(timeout, None)
+
+
+
+class ConnectedDatagramPortTestCase(TestCase):
+ """
+ Test connected datagram UNIX sockets.
+ """
+ if skipSockets is not None:
+ skip = skipSockets
+
+
+ def test_connectionFailedDoesntCallLoseConnection(self):
+ """
+ L{ConnectedDatagramPort} does not call the deprecated C{loseConnection}
+ in L{ConnectedDatagramPort.connectionFailed}.
+ """
+ def loseConnection():
+ """
+ Dummy C{loseConnection} method. C{loseConnection} is deprecated and
+ should not get called.
+ """
+ self.fail("loseConnection is deprecated and should not get called.")
+
+ port = unix.ConnectedDatagramPort(None, ClientProto())
+ port.loseConnection = loseConnection
+ port.connectionFailed("goodbye")
+
+
+ def test_connectionFailedCallsStopListening(self):
+ """
+ L{ConnectedDatagramPort} calls L{ConnectedDatagramPort.stopListening}
+ instead of the deprecated C{loseConnection} in
+ L{ConnectedDatagramPort.connectionFailed}.
+ """
+ self.called = False
+
+ def stopListening():
+ """
+ Dummy C{stopListening} method.
+ """
+ self.called = True
+
+ port = unix.ConnectedDatagramPort(None, ClientProto())
+ port.stopListening = stopListening
+ port.connectionFailed("goodbye")
+ self.assertEqual(self.called, True)
diff --git a/twisted/internet/test/test_posixprocess.py b/twisted/internet/test/test_posixprocess.py
new file mode 100644
index 0000000..f7abd55
--- /dev/null
+++ b/twisted/internet/test/test_posixprocess.py
@@ -0,0 +1,340 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for POSIX-based L{IReactorProcess} implementations.
+"""
+
+import errno, os, sys
+
+try:
+ import fcntl
+except ImportError:
+ platformSkip = "non-POSIX platform"
+else:
+ from twisted.internet import process
+ platformSkip = None
+
+from twisted.trial.unittest import TestCase
+
+
+class FakeFile(object):
+ """
+ A dummy file object which records when it is closed.
+ """
+ def __init__(self, testcase, fd):
+ self.testcase = testcase
+ self.fd = fd
+
+
+ def close(self):
+ self.testcase._files.remove(self.fd)
+
+
+
+class FakeResourceModule(object):
+ """
+ Fake version of L{resource} which hard-codes a particular rlimit for maximum
+ open files.
+
+ @ivar _limit: The value to return for the hard limit of number of open files.
+ """
+ RLIMIT_NOFILE = 1
+
+ def __init__(self, limit):
+ self._limit = limit
+
+
+ def getrlimit(self, no):
+ """
+ A fake of L{resource.getrlimit} which returns a pre-determined result.
+ """
+ if no == self.RLIMIT_NOFILE:
+ return [0, self._limit]
+ return [123, 456]
+
+
+
+class FDDetectorTests(TestCase):
+ """
+ Tests for _FDDetector class in twisted.internet.process, which detects
+ which function to drop in place for the _listOpenFDs method.
+
+ @ivar devfs: A flag indicating whether the filesystem fake will indicate
+ that /dev/fd exists.
+
+ @ivar accurateDevFDResults: A flag indicating whether the /dev/fd fake
+ returns accurate open file information.
+
+ @ivar procfs: A flag indicating whether the filesystem fake will indicate
+ that /proc/<pid>/fd exists.
+ """
+ skip = platformSkip
+
+ devfs = False
+ accurateDevFDResults = False
+
+ procfs = False
+
+ def getpid(self):
+ """
+ Fake os.getpid, always return the same thing
+ """
+ return 123
+
+
+ def listdir(self, arg):
+ """
+ Fake os.listdir, depending on what mode we're in to simulate behaviour.
+
+ @param arg: the directory to list
+ """
+ accurate = map(str, self._files)
+ if self.procfs and arg == ('/proc/%d/fd' % (self.getpid(),)):
+ return accurate
+ if self.devfs and arg == '/dev/fd':
+ if self.accurateDevFDResults:
+ return accurate
+ return ["0", "1", "2"]
+ raise OSError()
+
+
+ def openfile(self, fname, mode):
+ """
+ This is a mock for L{open}. It keeps track of opened files so extra
+ descriptors can be returned from the mock for L{os.listdir} when used on
+ one of the list-of-filedescriptors directories.
+
+ A L{FakeFile} is returned which can be closed to remove the new
+ descriptor from the open list.
+ """
+ # Find the smallest unused file descriptor and give it to the new file.
+ f = FakeFile(self, min(set(range(1024)) - set(self._files)))
+ self._files.append(f.fd)
+ return f
+
+
+ def hideResourceModule(self):
+ """
+ Make the L{resource} module unimportable for the remainder of the
+ current test method.
+ """
+ sys.modules['resource'] = None
+
+
+ def revealResourceModule(self, limit):
+ """
+ Make a L{FakeResourceModule} instance importable at the L{resource}
+ name.
+
+ @param limit: The value which will be returned for the hard limit of
+ number of open files by the fake resource module's C{getrlimit}
+ function.
+ """
+ sys.modules['resource'] = FakeResourceModule(limit)
+
+
+ def replaceResourceModule(self, value):
+ """
+ Restore the original resource module to L{sys.modules}.
+ """
+ if value is None:
+ try:
+ del sys.modules['resource']
+ except KeyError:
+ pass
+ else:
+ sys.modules['resource'] = value
+
+
+ def setUp(self):
+ """
+ Set up the tests, giving ourselves a detector object to play with and
+ setting up its testable knobs to refer to our mocked versions.
+ """
+ self.detector = process._FDDetector()
+ self.detector.listdir = self.listdir
+ self.detector.getpid = self.getpid
+ self.detector.openfile = self.openfile
+ self._files = [0, 1, 2]
+ self.addCleanup(
+ self.replaceResourceModule, sys.modules.get('resource'))
+
+
+ def test_selectFirstWorking(self):
+ """
+ L{FDDetector._getImplementation} returns the first method from its
+ C{_implementations} list which returns results which reflect a newly
+ opened file descriptor.
+ """
+ def failWithException():
+ raise ValueError("This does not work")
+
+ def failWithWrongResults():
+ return [0, 1, 2]
+
+ def correct():
+ return self._files[:]
+
+ self.detector._implementations = [
+ failWithException, failWithWrongResults, correct]
+
+ self.assertIdentical(correct, self.detector._getImplementation())
+
+
+ def test_selectLast(self):
+ """
+ L{FDDetector._getImplementation} returns the last method from its
+ C{_implementations} list if none of the implementations manage to return
+ results which reflect a newly opened file descriptor.
+ """
+ def failWithWrongResults():
+ return [3, 5, 9]
+
+ def failWithOtherWrongResults():
+ return [0, 1, 2]
+
+ self.detector._implementations = [
+ failWithWrongResults, failWithOtherWrongResults]
+
+ self.assertIdentical(
+ failWithOtherWrongResults, self.detector._getImplementation())
+
+
+ def test_identityOfListOpenFDsChanges(self):
+ """
+ Check that the identity of _listOpenFDs changes after running
+ _listOpenFDs the first time, but not after the second time it's run.
+
+ In other words, check that the monkey patching actually works.
+ """
+ # Create a new instance
+ detector = process._FDDetector()
+
+ first = detector._listOpenFDs.func_name
+ detector._listOpenFDs()
+ second = detector._listOpenFDs.func_name
+ detector._listOpenFDs()
+ third = detector._listOpenFDs.func_name
+
+ self.assertNotEqual(first, second)
+ self.assertEqual(second, third)
+
+
+ def test_devFDImplementation(self):
+ """
+ L{_FDDetector._devFDImplementation} raises L{OSError} if there is no
+ I{/dev/fd} directory, otherwise it returns the basenames of its children
+ interpreted as integers.
+ """
+ self.devfs = False
+ self.assertRaises(OSError, self.detector._devFDImplementation)
+ self.devfs = True
+ self.accurateDevFDResults = False
+ self.assertEqual([0, 1, 2], self.detector._devFDImplementation())
+
+
+ def test_procFDImplementation(self):
+ """
+ L{_FDDetector._procFDImplementation} raises L{OSError} if there is no
+ I{/proc/<pid>/fd} directory, otherwise it returns the basenames of its
+ children interpreted as integers.
+ """
+ self.procfs = False
+ self.assertRaises(OSError, self.detector._procFDImplementation)
+ self.procfs = True
+ self.assertEqual([0, 1, 2], self.detector._procFDImplementation())
+
+
+ def test_resourceFDImplementation(self):
+ """
+ L{_FDDetector._fallbackFDImplementation} uses the L{resource} module if
+ it is available, returning a range of integers from 0 to the the
+ minimum of C{1024} and the hard I{NOFILE} limit.
+ """
+ # When the resource module is here, use its value.
+ self.revealResourceModule(512)
+ self.assertEqual(
+ range(512), self.detector._fallbackFDImplementation())
+
+ # But limit its value to the arbitrarily selected value 1024.
+ self.revealResourceModule(2048)
+ self.assertEqual(
+ range(1024), self.detector._fallbackFDImplementation())
+
+
+ def test_fallbackFDImplementation(self):
+ """
+ L{_FDDetector._fallbackFDImplementation}, the implementation of last
+ resort, succeeds with a fixed range of integers from 0 to 1024 when the
+ L{resource} module is not importable.
+ """
+ self.hideResourceModule()
+ self.assertEqual(range(1024), self.detector._fallbackFDImplementation())
+
+
+
+class FileDescriptorTests(TestCase):
+ """
+ Tests for L{twisted.internet.process._listOpenFDs}
+ """
+ skip = platformSkip
+
+ def test_openFDs(self):
+ """
+ File descriptors returned by L{_listOpenFDs} are mostly open.
+
+ This test assumes that zero-legth writes fail with EBADF on closed
+ file descriptors.
+ """
+ for fd in process._listOpenFDs():
+ try:
+ fcntl.fcntl(fd, fcntl.F_GETFL)
+ except IOError, err:
+ self.assertEqual(
+ errno.EBADF, err.errno,
+ "fcntl(%d, F_GETFL) failed with unexpected errno %d" % (
+ fd, err.errno))
+
+
+ def test_expectedFDs(self):
+ """
+ L{_listOpenFDs} lists expected file descriptors.
+ """
+ # This is a tricky test. A priori, there is no way to know what file
+ # descriptors are open now, so there is no way to know what _listOpenFDs
+ # should return. Work around this by creating some new file descriptors
+ # which we can know the state of and then just making assertions about
+ # their presence or absence in the result.
+
+ # Expect a file we just opened to be listed.
+ f = file(os.devnull)
+ openfds = process._listOpenFDs()
+ self.assertIn(f.fileno(), openfds)
+
+ # Expect a file we just closed not to be listed - with a caveat. The
+ # implementation may need to open a file to discover the result. That
+ # open file descriptor will be allocated the same number as the one we
+ # just closed. So, instead, create a hole in the file descriptor space
+ # to catch that internal descriptor and make the assertion about a
+ # different closed file descriptor.
+
+ # This gets allocated a file descriptor larger than f's, since nothing
+ # has been closed since we opened f.
+ fd = os.dup(f.fileno())
+
+ # But sanity check that; if it fails the test is invalid.
+ self.assertTrue(
+ fd > f.fileno(),
+ "Expected duplicate file descriptor to be greater than original")
+
+ try:
+ # Get rid of the original, creating the hole. The copy should still
+ # be open, of course.
+ f.close()
+ self.assertIn(fd, process._listOpenFDs())
+ finally:
+ # Get rid of the copy now
+ os.close(fd)
+ # And it should not appear in the result.
+ self.assertNotIn(fd, process._listOpenFDs())
diff --git a/twisted/internet/test/test_process.py b/twisted/internet/test/test_process.py
new file mode 100644
index 0000000..c3946df
--- /dev/null
+++ b/twisted/internet/test/test_process.py
@@ -0,0 +1,711 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorProcess}.
+"""
+
+__metaclass__ = type
+
+import os, sys, signal, threading
+
+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.internet.test.reactormixins import ReactorBuilder
+from twisted.python.compat import set
+from twisted.python.log import msg, err
+from twisted.python.runtime import platform
+from twisted.python.filepath import FilePath
+from twisted.internet import utils
+from twisted.internet.interfaces import IReactorProcess, IProcessTransport
+from twisted.internet.defer import Deferred, succeed
+from twisted.internet.protocol import ProcessProtocol
+from twisted.internet.error import ProcessDone, ProcessTerminated
+from twisted.internet import _signals
+
+
+
+class _ShutdownCallbackProcessProtocol(ProcessProtocol):
+ """
+ An L{IProcessProtocol} which fires a Deferred when the process it is
+ associated with ends.
+
+ @ivar received: A C{dict} mapping file descriptors to lists of bytes
+ received from the child process on those file descriptors.
+ """
+ def __init__(self, whenFinished):
+ self.whenFinished = whenFinished
+ self.received = {}
+
+
+ def childDataReceived(self, fd, bytes):
+ self.received.setdefault(fd, []).append(bytes)
+
+
+ def processEnded(self, reason):
+ self.whenFinished.callback(None)
+
+
+
+class ProcessTestsBuilderBase(ReactorBuilder):
+ """
+ Base class for L{IReactorProcess} tests which defines some tests which
+ can be applied to PTY or non-PTY uses of C{spawnProcess}.
+
+ Subclasses are expected to set the C{usePTY} attribute to C{True} or
+ C{False}.
+ """
+ requiredInterfaces = [IReactorProcess]
+
+
+ def test_processTransportInterface(self):
+ """
+ L{IReactorProcess.spawnProcess} connects the protocol passed to it
+ to a transport which provides L{IProcessTransport}.
+ """
+ ended = Deferred()
+ protocol = _ShutdownCallbackProcessProtocol(ended)
+
+ reactor = self.buildReactor()
+ transport = reactor.spawnProcess(
+ protocol, sys.executable, [sys.executable, "-c", ""],
+ usePTY=self.usePTY)
+
+ # The transport is available synchronously, so we can check it right
+ # away (unlike many transport-based tests). This is convenient even
+ # though it's probably not how the spawnProcess interface should really
+ # work.
+ # We're not using verifyObject here because part of
+ # IProcessTransport is a lie - there are no getHost or getPeer
+ # methods. See #1124.
+ self.assertTrue(IProcessTransport.providedBy(transport))
+
+ # Let the process run and exit so we don't leave a zombie around.
+ ended.addCallback(lambda ignored: reactor.stop())
+ self.runReactor(reactor)
+
+
+ def _writeTest(self, write):
+ """
+ Helper for testing L{IProcessTransport} write functionality. This
+ method spawns a child process and gives C{write} a chance to write some
+ bytes to it. It then verifies that the bytes were actually written to
+ it (by relying on the child process to echo them back).
+
+ @param write: A two-argument callable. This is invoked with a process
+ transport and some bytes to write to it.
+ """
+ reactor = self.buildReactor()
+
+ ended = Deferred()
+ protocol = _ShutdownCallbackProcessProtocol(ended)
+
+ bytes = "hello, world" + os.linesep
+ program = (
+ "import sys\n"
+ "sys.stdout.write(sys.stdin.readline())\n"
+ )
+
+ def startup():
+ transport = reactor.spawnProcess(
+ protocol, sys.executable, [sys.executable, "-c", program])
+ try:
+ write(transport, bytes)
+ except:
+ err(None, "Unhandled exception while writing")
+ transport.signalProcess('KILL')
+ reactor.callWhenRunning(startup)
+
+ ended.addCallback(lambda ignored: reactor.stop())
+
+ self.runReactor(reactor)
+ self.assertEqual(bytes, "".join(protocol.received[1]))
+
+
+ def test_write(self):
+ """
+ L{IProcessTransport.write} writes the specified C{str} to the standard
+ input of the child process.
+ """
+ def write(transport, bytes):
+ transport.write(bytes)
+ self._writeTest(write)
+
+
+ def test_writeSequence(self):
+ """
+ L{IProcessTransport.writeSequence} writes the specified C{list} of
+ C{str} to the standard input of the child process.
+ """
+ def write(transport, bytes):
+ transport.writeSequence(list(bytes))
+ self._writeTest(write)
+
+
+ def test_writeToChild(self):
+ """
+ L{IProcessTransport.writeToChild} writes the specified C{str} to the
+ specified file descriptor of the child process.
+ """
+ def write(transport, bytes):
+ transport.writeToChild(0, bytes)
+ self._writeTest(write)
+
+
+ def test_writeToChildBadFileDescriptor(self):
+ """
+ L{IProcessTransport.writeToChild} raises L{KeyError} if passed a file
+ descriptor which is was not set up by L{IReactorProcess.spawnProcess}.
+ """
+ def write(transport, bytes):
+ try:
+ self.assertRaises(KeyError, transport.writeToChild, 13, bytes)
+ finally:
+ # Just get the process to exit so the test can complete
+ transport.write(bytes)
+ self._writeTest(write)
+
+
+ def test_spawnProcessEarlyIsReaped(self):
+ """
+ If, before the reactor is started with L{IReactorCore.run}, a
+ process is started with L{IReactorProcess.spawnProcess} and
+ terminates, the process is reaped once the reactor is started.
+ """
+ reactor = self.buildReactor()
+
+ # Create the process with no shared file descriptors, so that there
+ # are no other events for the reactor to notice and "cheat" with.
+ # We want to be sure it's really dealing with the process exiting,
+ # not some associated event.
+ if self.usePTY:
+ childFDs = None
+ else:
+ childFDs = {}
+
+ # Arrange to notice the SIGCHLD.
+ signaled = threading.Event()
+ def handler(*args):
+ signaled.set()
+ signal.signal(signal.SIGCHLD, handler)
+
+ # Start a process - before starting the reactor!
+ ended = Deferred()
+ reactor.spawnProcess(
+ _ShutdownCallbackProcessProtocol(ended), sys.executable,
+ [sys.executable, "-c", ""], usePTY=self.usePTY, childFDs=childFDs)
+
+ # Wait for the SIGCHLD (which might have been delivered before we got
+ # here, but that's okay because the signal handler was installed above,
+ # before we could have gotten it).
+ signaled.wait(120)
+ if not signaled.isSet():
+ self.fail("Timed out waiting for child process to exit.")
+
+ # Capture the processEnded callback.
+ result = []
+ ended.addCallback(result.append)
+
+ if result:
+ # The synchronous path through spawnProcess / Process.__init__ /
+ # registerReapProcessHandler was encountered. There's no reason to
+ # start the reactor, because everything is done already.
+ return
+
+ # Otherwise, though, start the reactor so it can tell us the process
+ # exited.
+ ended.addCallback(lambda ignored: reactor.stop())
+ self.runReactor(reactor)
+
+ # Make sure the reactor stopped because the Deferred fired.
+ self.assertTrue(result)
+
+ if getattr(signal, 'SIGCHLD', None) is None:
+ test_spawnProcessEarlyIsReaped.skip = (
+ "Platform lacks SIGCHLD, early-spawnProcess test can't work.")
+
+
+ def test_processExitedWithSignal(self):
+ """
+ The C{reason} argument passed to L{IProcessProtocol.processExited} is a
+ L{ProcessTerminated} instance if the child process exits with a signal.
+ """
+ sigName = 'TERM'
+ sigNum = getattr(signal, 'SIG' + sigName)
+ exited = Deferred()
+ source = (
+ "import sys\n"
+ # Talk so the parent process knows the process is running. This is
+ # necessary because ProcessProtocol.makeConnection may be called
+ # before this process is exec'd. It would be unfortunate if we
+ # SIGTERM'd the Twisted process while it was on its way to doing
+ # the exec.
+ "sys.stdout.write('x')\n"
+ "sys.stdout.flush()\n"
+ "sys.stdin.read()\n")
+
+ class Exiter(ProcessProtocol):
+ def childDataReceived(self, fd, data):
+ msg('childDataReceived(%d, %r)' % (fd, data))
+ self.transport.signalProcess(sigName)
+
+ def childConnectionLost(self, fd):
+ msg('childConnectionLost(%d)' % (fd,))
+
+ def processExited(self, reason):
+ msg('processExited(%r)' % (reason,))
+ # Protect the Deferred from the failure so that it follows
+ # the callback chain. This doesn't use the errback chain
+ # because it wants to make sure reason is a Failure. An
+ # Exception would also make an errback-based test pass, and
+ # that would be wrong.
+ exited.callback([reason])
+
+ def processEnded(self, reason):
+ msg('processEnded(%r)' % (reason,))
+
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, Exiter(), sys.executable,
+ [sys.executable, "-c", source], usePTY=self.usePTY)
+
+ def cbExited((failure,)):
+ # Trapping implicitly verifies that it's a Failure (rather than
+ # an exception) and explicitly makes sure it's the right type.
+ failure.trap(ProcessTerminated)
+ err = failure.value
+ if platform.isWindows():
+ # Windows can't really /have/ signals, so it certainly can't
+ # report them as the reason for termination. Maybe there's
+ # something better we could be doing here, anyway? Hard to
+ # say. Anyway, this inconsistency between different platforms
+ # is extremely unfortunate and I would remove it if I
+ # could. -exarkun
+ self.assertIdentical(err.signal, None)
+ self.assertEqual(err.exitCode, 1)
+ else:
+ self.assertEqual(err.signal, sigNum)
+ self.assertIdentical(err.exitCode, None)
+
+ exited.addCallback(cbExited)
+ exited.addErrback(err)
+ exited.addCallback(lambda ign: reactor.stop())
+
+ self.runReactor(reactor)
+
+
+ def test_systemCallUninterruptedByChildExit(self):
+ """
+ If a child process exits while a system call is in progress, the system
+ call should not be interfered with. In particular, it should not fail
+ with EINTR.
+
+ Older versions of Twisted installed a SIGCHLD handler on POSIX without
+ using the feature exposed by the SA_RESTART flag to sigaction(2). The
+ most noticable problem this caused was for blocking reads and writes to
+ sometimes fail with EINTR.
+ """
+ reactor = self.buildReactor()
+
+ # XXX Since pygobject/pygtk wants to use signal.set_wakeup_fd,
+ # we aren't actually providing this functionality on the glib2
+ # or gtk2 reactors yet. See #4286 for the possibility of
+ # improving this.
+ skippedReactors = ["Glib2Reactor", "Gtk2Reactor", "PortableGtkReactor"]
+ hasSigInterrupt = getattr(signal, "siginterrupt", None) is not None
+ reactorClassName = reactor.__class__.__name__
+ if reactorClassName in skippedReactors and not hasSigInterrupt:
+ raise SkipTest(
+ "%s is not supported without siginterrupt" % reactorClassName)
+ if _signals.installHandler.__name__ == "_installHandlerUsingSignal":
+ raise SkipTest("_signals._installHandlerUsingSignal doesn't support this feature")
+
+ result = []
+
+ def f():
+ try:
+ f1 = os.popen('%s -c "import time; time.sleep(0.1)"' %
+ (sys.executable,))
+ f2 = os.popen('%s -c "import time; time.sleep(0.5); print \'Foo\'"' %
+ (sys.executable,))
+ # The read call below will blow up with an EINTR from the
+ # SIGCHLD from the first process exiting if we install a
+ # SIGCHLD handler without SA_RESTART. (which we used to do)
+ result.append(f2.read())
+ finally:
+ reactor.stop()
+
+ reactor.callWhenRunning(f)
+ self.runReactor(reactor)
+ self.assertEqual(result, ["Foo\n"])
+
+
+ def test_openFileDescriptors(self):
+ """
+ A spawned process has only stdin, stdout and stderr open
+ (file descriptor 3 is also reported as open, because of the call to
+ 'os.listdir()').
+ """
+ from twisted.python.runtime import platformType
+ if platformType != "posix":
+ raise SkipTest("Test only applies to POSIX platforms")
+
+ here = FilePath(__file__)
+ top = here.parent().parent().parent().parent()
+ source = (
+ "import sys",
+ "sys.path.insert(0, '%s')" % (top.path,),
+ "from twisted.internet import process",
+ "sys.stdout.write(str(process._listOpenFDs()))",
+ "sys.stdout.flush()")
+
+ def checkOutput(output):
+ self.assertEqual('[0, 1, 2, 3]', output)
+
+ reactor = self.buildReactor()
+
+ class Protocol(ProcessProtocol):
+ def __init__(self):
+ self.output = []
+
+ def outReceived(self, data):
+ self.output.append(data)
+
+ def processEnded(self, reason):
+ try:
+ checkOutput("".join(self.output))
+ finally:
+ reactor.stop()
+
+ proto = Protocol()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, proto, sys.executable,
+ [sys.executable, "-Wignore", "-c", "\n".join(source)],
+ usePTY=self.usePTY)
+ self.runReactor(reactor)
+
+
+ def test_timeleyProcessExited(self):
+ """
+ If a spawned process exits, C{processExited} will be called in a
+ timely manner.
+ """
+ reactor = self.buildReactor()
+
+ class ExitingProtocol(ProcessProtocol):
+ exited = False
+
+ def processExited(protoSelf, reason):
+ protoSelf.exited = True
+ reactor.stop()
+ self.assertEqual(reason.value.exitCode, 0)
+
+ protocol = ExitingProtocol()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, protocol, sys.executable,
+ [sys.executable, "-c", "raise SystemExit(0)"],
+ usePTY=self.usePTY)
+
+ # This will timeout if processExited isn't called:
+ self.runReactor(reactor, timeout=30)
+ self.assertEqual(protocol.exited, True)
+
+
+
+class ProcessTestsBuilder(ProcessTestsBuilderBase):
+ """
+ Builder defining tests relating to L{IReactorProcess} for child processes
+ which do not have a PTY.
+ """
+ usePTY = False
+
+ keepStdioOpenProgram = FilePath(__file__).sibling('process_helper.py').path
+ if platform.isWindows():
+ keepStdioOpenArg = "windows"
+ else:
+ # Just a value that doesn't equal "windows"
+ keepStdioOpenArg = ""
+
+ # Define this test here because PTY-using processes only have stdin and
+ # stdout and the test would need to be different for that to work.
+ def test_childConnectionLost(self):
+ """
+ L{IProcessProtocol.childConnectionLost} is called each time a file
+ descriptor associated with a child process is closed.
+ """
+ connected = Deferred()
+ lost = {0: Deferred(), 1: Deferred(), 2: Deferred()}
+
+ class Closer(ProcessProtocol):
+ def makeConnection(self, transport):
+ connected.callback(transport)
+
+ def childConnectionLost(self, childFD):
+ lost[childFD].callback(None)
+
+ source = (
+ "import os, sys\n"
+ "while 1:\n"
+ " line = sys.stdin.readline().strip()\n"
+ " if not line:\n"
+ " break\n"
+ " os.close(int(line))\n")
+
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, Closer(), sys.executable,
+ [sys.executable, "-c", source], usePTY=self.usePTY)
+
+ def cbConnected(transport):
+ transport.write('2\n')
+ return lost[2].addCallback(lambda ign: transport)
+ connected.addCallback(cbConnected)
+
+ def lostSecond(transport):
+ transport.write('1\n')
+ return lost[1].addCallback(lambda ign: transport)
+ connected.addCallback(lostSecond)
+
+ def lostFirst(transport):
+ transport.write('\n')
+ connected.addCallback(lostFirst)
+ connected.addErrback(err)
+
+ def cbEnded(ignored):
+ reactor.stop()
+ connected.addCallback(cbEnded)
+
+ self.runReactor(reactor)
+
+
+ # This test is here because PTYProcess never delivers childConnectionLost.
+ def test_processEnded(self):
+ """
+ L{IProcessProtocol.processEnded} is called after the child process
+ exits and L{IProcessProtocol.childConnectionLost} is called for each of
+ its file descriptors.
+ """
+ ended = Deferred()
+ lost = []
+
+ class Ender(ProcessProtocol):
+ def childDataReceived(self, fd, data):
+ msg('childDataReceived(%d, %r)' % (fd, data))
+ self.transport.loseConnection()
+
+ def childConnectionLost(self, childFD):
+ msg('childConnectionLost(%d)' % (childFD,))
+ lost.append(childFD)
+
+ def processExited(self, reason):
+ msg('processExited(%r)' % (reason,))
+
+ def processEnded(self, reason):
+ msg('processEnded(%r)' % (reason,))
+ ended.callback([reason])
+
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, Ender(), sys.executable,
+ [sys.executable, self.keepStdioOpenProgram, "child",
+ self.keepStdioOpenArg],
+ usePTY=self.usePTY)
+
+ def cbEnded((failure,)):
+ failure.trap(ProcessDone)
+ self.assertEqual(set(lost), set([0, 1, 2]))
+ ended.addCallback(cbEnded)
+
+ ended.addErrback(err)
+ ended.addCallback(lambda ign: reactor.stop())
+
+ self.runReactor(reactor)
+
+
+ # This test is here because PTYProcess.loseConnection does not actually
+ # close the file descriptors to the child process. This test needs to be
+ # written fairly differently for PTYProcess.
+ def test_processExited(self):
+ """
+ L{IProcessProtocol.processExited} is called when the child process
+ exits, even if file descriptors associated with the child are still
+ open.
+ """
+ exited = Deferred()
+ allLost = Deferred()
+ lost = []
+
+ class Waiter(ProcessProtocol):
+ def childDataReceived(self, fd, data):
+ msg('childDataReceived(%d, %r)' % (fd, data))
+
+ def childConnectionLost(self, childFD):
+ msg('childConnectionLost(%d)' % (childFD,))
+ lost.append(childFD)
+ if len(lost) == 3:
+ allLost.callback(None)
+
+ def processExited(self, reason):
+ msg('processExited(%r)' % (reason,))
+ # See test_processExitedWithSignal
+ exited.callback([reason])
+ self.transport.loseConnection()
+
+ reactor = self.buildReactor()
+ reactor.callWhenRunning(
+ reactor.spawnProcess, Waiter(), sys.executable,
+ [sys.executable, self.keepStdioOpenProgram, "child",
+ self.keepStdioOpenArg],
+ usePTY=self.usePTY)
+
+ def cbExited((failure,)):
+ failure.trap(ProcessDone)
+ msg('cbExited; lost = %s' % (lost,))
+ self.assertEqual(lost, [])
+ return allLost
+ exited.addCallback(cbExited)
+
+ def cbAllLost(ignored):
+ self.assertEqual(set(lost), set([0, 1, 2]))
+ exited.addCallback(cbAllLost)
+
+ exited.addErrback(err)
+ exited.addCallback(lambda ign: reactor.stop())
+
+ self.runReactor(reactor)
+
+
+ def makeSourceFile(self, sourceLines):
+ """
+ Write the given list of lines to a text file and return the absolute
+ path to it.
+ """
+ script = self.mktemp()
+ scriptFile = file(script, 'wt')
+ scriptFile.write(os.linesep.join(sourceLines) + os.linesep)
+ scriptFile.close()
+ return os.path.abspath(script)
+
+
+ def test_shebang(self):
+ """
+ Spawning a process with an executable which is a script starting
+ with an interpreter definition line (#!) uses that interpreter to
+ evaluate the script.
+ """
+ SHEBANG_OUTPUT = 'this is the shebang output'
+
+ scriptFile = self.makeSourceFile([
+ "#!%s" % (sys.executable,),
+ "import sys",
+ "sys.stdout.write('%s')" % (SHEBANG_OUTPUT,),
+ "sys.stdout.flush()"])
+ os.chmod(scriptFile, 0700)
+
+ reactor = self.buildReactor()
+
+ def cbProcessExited((out, err, code)):
+ msg("cbProcessExited((%r, %r, %d))" % (out, err, code))
+ self.assertEqual(out, SHEBANG_OUTPUT)
+ self.assertEqual(err, "")
+ self.assertEqual(code, 0)
+
+ def shutdown(passthrough):
+ reactor.stop()
+ return passthrough
+
+ def start():
+ d = utils.getProcessOutputAndValue(scriptFile, reactor=reactor)
+ d.addBoth(shutdown)
+ d.addCallback(cbProcessExited)
+ d.addErrback(err)
+
+ reactor.callWhenRunning(start)
+ self.runReactor(reactor)
+
+
+ def test_processCommandLineArguments(self):
+ """
+ Arguments given to spawnProcess are passed to the child process as
+ originally intended.
+ """
+ source = (
+ # On Windows, stdout is not opened in binary mode by default,
+ # so newline characters are munged on writing, interfering with
+ # the tests.
+ 'import sys, os\n'
+ 'try:\n'
+ ' import msvcrt\n'
+ ' msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)\n'
+ 'except ImportError:\n'
+ ' pass\n'
+ 'for arg in sys.argv[1:]:\n'
+ ' sys.stdout.write(arg + chr(0))\n'
+ ' sys.stdout.flush()')
+
+ args = ['hello', '"', ' \t|<>^&', r'"\\"hello\\"', r'"foo\ bar baz\""']
+ # Ensure that all non-NUL characters can be passed too.
+ args.append(''.join(map(chr, xrange(1, 256))))
+
+ reactor = self.buildReactor()
+
+ def processFinished(output):
+ output = output.split('\0')
+ # Drop the trailing \0.
+ output.pop()
+ self.assertEqual(args, output)
+
+ def shutdown(result):
+ reactor.stop()
+ return result
+
+ def spawnChild():
+ d = succeed(None)
+ d.addCallback(lambda dummy: utils.getProcessOutput(
+ sys.executable, ['-c', source] + args, reactor=reactor))
+ d.addCallback(processFinished)
+ d.addBoth(shutdown)
+
+ reactor.callWhenRunning(spawnChild)
+ self.runReactor(reactor)
+globals().update(ProcessTestsBuilder.makeTestCaseClasses())
+
+
+
+class PTYProcessTestsBuilder(ProcessTestsBuilderBase):
+ """
+ Builder defining tests relating to L{IReactorProcess} for child processes
+ which have a PTY.
+ """
+ usePTY = True
+
+ if platform.isWindows():
+ skip = "PTYs are not supported on Windows."
+ elif platform.isMacOSX():
+ skippedReactors = {
+ "twisted.internet.pollreactor.PollReactor":
+ "OS X's poll() does not support PTYs"}
+globals().update(PTYProcessTestsBuilder.makeTestCaseClasses())
+
+
+
+class PotentialZombieWarningTests(TestCase):
+ """
+ Tests for L{twisted.internet.error.PotentialZombieWarning}.
+ """
+ def test_deprecated(self):
+ """
+ Accessing L{PotentialZombieWarning} via the
+ I{PotentialZombieWarning} attribute of L{twisted.internet.error}
+ results in a deprecation warning being emitted.
+ """
+ from twisted.internet import error
+ error.PotentialZombieWarning
+
+ warnings = self.flushWarnings([self.test_deprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.internet.error.PotentialZombieWarning was deprecated in "
+ "Twisted 10.0.0: There is no longer any potential for zombie "
+ "process.")
+ self.assertEqual(len(warnings), 1)
diff --git a/twisted/internet/test/test_protocol.py b/twisted/internet/test/test_protocol.py
new file mode 100644
index 0000000..18ef114
--- /dev/null
+++ b/twisted/internet/test/test_protocol.py
@@ -0,0 +1,357 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.protocol}.
+"""
+
+from zope.interface.verify import verifyObject
+
+from twisted.python.failure import Failure
+from twisted.internet.interfaces import IProtocol, ILoggingContext
+from twisted.internet.defer import CancelledError
+from twisted.internet.protocol import Protocol, ClientCreator
+from twisted.internet.task import Clock
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import MemoryReactor, StringTransport
+
+
+
+class MemoryConnector:
+ _disconnected = False
+
+ def disconnect(self):
+ self._disconnected = True
+
+
+
+class MemoryReactorWithConnectorsAndTime(MemoryReactor, Clock):
+ """
+ An extension of L{MemoryReactor} which returns L{IConnector}
+ providers from its C{connectTCP} method.
+ """
+ def __init__(self):
+ MemoryReactor.__init__(self)
+ Clock.__init__(self)
+ self.connectors = []
+
+
+ def connectTCP(self, *a, **kw):
+ MemoryReactor.connectTCP(self, *a, **kw)
+ connector = MemoryConnector()
+ self.connectors.append(connector)
+ return connector
+
+
+ def connectUNIX(self, *a, **kw):
+ MemoryReactor.connectUNIX(self, *a, **kw)
+ connector = MemoryConnector()
+ self.connectors.append(connector)
+ return connector
+
+
+ def connectSSL(self, *a, **kw):
+ MemoryReactor.connectSSL(self, *a, **kw)
+ connector = MemoryConnector()
+ self.connectors.append(connector)
+ return connector
+
+
+
+class ClientCreatorTests(TestCase):
+ """
+ Tests for L{twisted.internet.protocol.ClientCreator}.
+ """
+ def _basicConnectTest(self, check):
+ """
+ Helper for implementing a test to verify that one of the I{connect}
+ methods of L{ClientCreator} passes the right arguments to the right
+ reactor method.
+
+ @param check: A function which will be invoked with a reactor and a
+ L{ClientCreator} instance and which should call one of the
+ L{ClientCreator}'s I{connect} methods and assert that all of its
+ arguments except for the factory are passed on as expected to the
+ reactor. The factory should be returned.
+ """
+ class SomeProtocol(Protocol):
+ pass
+
+ reactor = MemoryReactorWithConnectorsAndTime()
+ cc = ClientCreator(reactor, SomeProtocol)
+ factory = check(reactor, cc)
+ protocol = factory.buildProtocol(None)
+ self.assertIsInstance(protocol, SomeProtocol)
+
+
+ def test_connectTCP(self):
+ """
+ L{ClientCreator.connectTCP} calls C{reactor.connectTCP} with the host
+ and port information passed to it, and with a factory which will
+ construct the protocol passed to L{ClientCreator.__init__}.
+ """
+ def check(reactor, cc):
+ cc.connectTCP('example.com', 1234, 4321, ('1.2.3.4', 9876))
+ host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
+ self.assertEqual(host, 'example.com')
+ self.assertEqual(port, 1234)
+ self.assertEqual(timeout, 4321)
+ self.assertEqual(bindAddress, ('1.2.3.4', 9876))
+ return factory
+ self._basicConnectTest(check)
+
+
+ def test_connectUNIX(self):
+ """
+ L{ClientCreator.connectUNIX} calls C{reactor.connectUNIX} with the
+ filename passed to it, and with a factory which will construct the
+ protocol passed to L{ClientCreator.__init__}.
+ """
+ def check(reactor, cc):
+ cc.connectUNIX('/foo/bar', 123, True)
+ address, factory, timeout, checkPID = reactor.unixClients.pop()
+ self.assertEqual(address, '/foo/bar')
+ self.assertEqual(timeout, 123)
+ self.assertEqual(checkPID, True)
+ return factory
+ self._basicConnectTest(check)
+
+
+ def test_connectSSL(self):
+ """
+ L{ClientCreator.connectSSL} calls C{reactor.connectSSL} with the host,
+ port, and context factory passed to it, and with a factory which will
+ construct the protocol passed to L{ClientCreator.__init__}.
+ """
+ def check(reactor, cc):
+ expectedContextFactory = object()
+ cc.connectSSL('example.com', 1234, expectedContextFactory, 4321, ('4.3.2.1', 5678))
+ host, port, factory, contextFactory, timeout, bindAddress = reactor.sslClients.pop()
+ self.assertEqual(host, 'example.com')
+ self.assertEqual(port, 1234)
+ self.assertIdentical(contextFactory, expectedContextFactory)
+ self.assertEqual(timeout, 4321)
+ self.assertEqual(bindAddress, ('4.3.2.1', 5678))
+ return factory
+ self._basicConnectTest(check)
+
+
+ def _cancelConnectTest(self, connect):
+ """
+ Helper for implementing a test to verify that cancellation of the
+ L{Deferred} returned by one of L{ClientCreator}'s I{connect} methods is
+ implemented to cancel the underlying connector.
+
+ @param connect: A function which will be invoked with a L{ClientCreator}
+ instance as an argument and which should call one its I{connect}
+ methods and return the result.
+
+ @return: A L{Deferred} which fires when the test is complete or fails if
+ there is a problem.
+ """
+ reactor = MemoryReactorWithConnectorsAndTime()
+ cc = ClientCreator(reactor, Protocol)
+ d = connect(cc)
+ connector = reactor.connectors.pop()
+ self.assertFalse(connector._disconnected)
+ d.cancel()
+ self.assertTrue(connector._disconnected)
+ return self.assertFailure(d, CancelledError)
+
+
+ def test_cancelConnectTCP(self):
+ """
+ The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
+ to abort the connection attempt before it completes.
+ """
+ def connect(cc):
+ return cc.connectTCP('example.com', 1234)
+ return self._cancelConnectTest(connect)
+
+
+ def test_cancelConnectUNIX(self):
+ """
+ The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
+ to abort the connection attempt before it completes.
+ """
+ def connect(cc):
+ return cc.connectUNIX('/foo/bar')
+ return self._cancelConnectTest(connect)
+
+
+ def test_cancelConnectSSL(self):
+ """
+ The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
+ to abort the connection attempt before it completes.
+ """
+ def connect(cc):
+ return cc.connectSSL('example.com', 1234, object())
+ return self._cancelConnectTest(connect)
+
+
+ def _cancelConnectTimeoutTest(self, connect):
+ """
+ Like L{_cancelConnectTest}, but for the case where the L{Deferred} is
+ cancelled after the connection is set up but before it is fired with the
+ resulting protocol instance.
+ """
+ reactor = MemoryReactorWithConnectorsAndTime()
+ cc = ClientCreator(reactor, Protocol)
+ d = connect(reactor, cc)
+ connector = reactor.connectors.pop()
+ # Sanity check - there is an outstanding delayed call to fire the
+ # Deferred.
+ self.assertEqual(len(reactor.getDelayedCalls()), 1)
+
+ # Cancel the Deferred, disconnecting the transport just set up and
+ # cancelling the delayed call.
+ d.cancel()
+
+ self.assertEqual(reactor.getDelayedCalls(), [])
+
+ # A real connector implementation is responsible for disconnecting the
+ # transport as well. For our purposes, just check that someone told the
+ # connector to disconnect.
+ self.assertTrue(connector._disconnected)
+
+ return self.assertFailure(d, CancelledError)
+
+
+ def test_cancelConnectTCPTimeout(self):
+ """
+ L{ClientCreator.connectTCP} inserts a very short delayed call between
+ the time the connection is established and the time the L{Deferred}
+ returned from one of its connect methods actually fires. If the
+ L{Deferred} is cancelled in this interval, the established connection is
+ closed, the timeout is cancelled, and the L{Deferred} fails with
+ L{CancelledError}.
+ """
+ def connect(reactor, cc):
+ d = cc.connectTCP('example.com', 1234)
+ host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
+ protocol = factory.buildProtocol(None)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ return d
+ return self._cancelConnectTimeoutTest(connect)
+
+
+ def test_cancelConnectUNIXTimeout(self):
+ """
+ L{ClientCreator.connectUNIX} inserts a very short delayed call between
+ the time the connection is established and the time the L{Deferred}
+ returned from one of its connect methods actually fires. If the
+ L{Deferred} is cancelled in this interval, the established connection is
+ closed, the timeout is cancelled, and the L{Deferred} fails with
+ L{CancelledError}.
+ """
+ def connect(reactor, cc):
+ d = cc.connectUNIX('/foo/bar')
+ address, factory, timeout, bindAddress = reactor.unixClients.pop()
+ protocol = factory.buildProtocol(None)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ return d
+ return self._cancelConnectTimeoutTest(connect)
+
+
+ def test_cancelConnectSSLTimeout(self):
+ """
+ L{ClientCreator.connectSSL} inserts a very short delayed call between
+ the time the connection is established and the time the L{Deferred}
+ returned from one of its connect methods actually fires. If the
+ L{Deferred} is cancelled in this interval, the established connection is
+ closed, the timeout is cancelled, and the L{Deferred} fails with
+ L{CancelledError}.
+ """
+ def connect(reactor, cc):
+ d = cc.connectSSL('example.com', 1234, object())
+ host, port, factory, contextFactory, timeout, bindADdress = reactor.sslClients.pop()
+ protocol = factory.buildProtocol(None)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ return d
+ return self._cancelConnectTimeoutTest(connect)
+
+
+ def _cancelConnectFailedTimeoutTest(self, connect):
+ """
+ Like L{_cancelConnectTest}, but for the case where the L{Deferred} is
+ cancelled after the connection attempt has failed but before it is fired
+ with the resulting failure.
+ """
+ reactor = MemoryReactorWithConnectorsAndTime()
+ cc = ClientCreator(reactor, Protocol)
+ d, factory = connect(reactor, cc)
+ connector = reactor.connectors.pop()
+ factory.clientConnectionFailed(
+ connector, Failure(Exception("Simulated failure")))
+
+ # Sanity check - there is an outstanding delayed call to fire the
+ # Deferred.
+ self.assertEqual(len(reactor.getDelayedCalls()), 1)
+
+ # Cancel the Deferred, cancelling the delayed call.
+ d.cancel()
+
+ self.assertEqual(reactor.getDelayedCalls(), [])
+
+ return self.assertFailure(d, CancelledError)
+
+
+ def test_cancelConnectTCPFailedTimeout(self):
+ """
+ Similar to L{test_cancelConnectTCPTimeout}, but for the case where the
+ connection attempt fails.
+ """
+ def connect(reactor, cc):
+ d = cc.connectTCP('example.com', 1234)
+ host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
+ return d, factory
+ return self._cancelConnectFailedTimeoutTest(connect)
+
+
+ def test_cancelConnectUNIXFailedTimeout(self):
+ """
+ Similar to L{test_cancelConnectUNIXTimeout}, but for the case where the
+ connection attempt fails.
+ """
+ def connect(reactor, cc):
+ d = cc.connectUNIX('/foo/bar')
+ address, factory, timeout, bindAddress = reactor.unixClients.pop()
+ return d, factory
+ return self._cancelConnectFailedTimeoutTest(connect)
+
+
+ def test_cancelConnectSSLFailedTimeout(self):
+ """
+ Similar to L{test_cancelConnectSSLTimeout}, but for the case where the
+ connection attempt fails.
+ """
+ def connect(reactor, cc):
+ d = cc.connectSSL('example.com', 1234, object())
+ host, port, factory, contextFactory, timeout, bindADdress = reactor.sslClients.pop()
+ return d, factory
+ return self._cancelConnectFailedTimeoutTest(connect)
+
+
+class ProtocolTests(TestCase):
+ """
+ Tests for L{twisted.internet.protocol.Protocol}.
+ """
+ def test_interfaces(self):
+ """
+ L{Protocol} instances provide L{IProtocol} and L{ILoggingContext}.
+ """
+ proto = Protocol()
+ self.assertTrue(verifyObject(IProtocol, proto))
+ self.assertTrue(verifyObject(ILoggingContext, proto))
+
+
+ def test_logPrefix(self):
+ """
+ L{Protocol.logPrefix} returns the protocol class's name.
+ """
+ class SomeThing(Protocol):
+ pass
+ self.assertEqual("SomeThing", SomeThing().logPrefix())
diff --git a/twisted/internet/test/test_qtreactor.py b/twisted/internet/test/test_qtreactor.py
new file mode 100644
index 0000000..e87b74f
--- /dev/null
+++ b/twisted/internet/test/test_qtreactor.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+from twisted.trial import unittest
+from twisted.python.runtime import platform
+from twisted.python.util import sibpath
+from twisted.internet.utils import getProcessOutputAndValue
+
+
+skipWindowsNopywin32 = None
+if platform.isWindows():
+ try:
+ import win32process
+ except ImportError:
+ skipWindowsNopywin32 = ("On windows, spawnProcess is not available "
+ "in the absence of win32process.")
+
+class QtreactorTestCase(unittest.TestCase):
+ """
+ Tests for L{twisted.internet.qtreactor}.
+ """
+ def test_importQtreactor(self):
+ """
+ Attempting to import L{twisted.internet.qtreactor} should raise an
+ C{ImportError} indicating that C{qtreactor} is no longer a part of
+ Twisted.
+ """
+ sys.modules["qtreactor"] = None
+ from twisted.plugins.twisted_qtstub import errorMessage
+ try:
+ import twisted.internet.qtreactor
+ except ImportError, e:
+ self.assertEqual(str(e), errorMessage)
diff --git a/twisted/internet/test/test_serialport.py b/twisted/internet/test/test_serialport.py
new file mode 100644
index 0000000..85b3f3a
--- /dev/null
+++ b/twisted/internet/test/test_serialport.py
@@ -0,0 +1,72 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.serialport}.
+"""
+
+from twisted.trial import unittest
+from twisted.python.failure import Failure
+from twisted.internet.protocol import Protocol
+from twisted.internet.error import ConnectionDone
+try:
+ from twisted.internet import serialport
+except ImportError:
+ serialport = None
+
+
+
+class DoNothing(object):
+ """
+ Object with methods that do nothing.
+ """
+
+ def __init__(self, *args, **kwargs):
+ pass
+
+
+ def __getattr__(self, attr):
+ return lambda *args, **kwargs: None
+
+
+
+class SerialPortTests(unittest.TestCase):
+ """
+ Minimal testing for Twisted's serial port support.
+
+ See ticket #2462 for the eventual full test suite.
+ """
+
+ if serialport is None:
+ skip = "Serial port support is not available."
+
+
+ def test_connectionMadeLost(self):
+ """
+ C{connectionMade} and C{connectionLost} are called on the protocol by
+ the C{SerialPort}.
+ """
+ # Serial port that doesn't actually connect to anything:
+ class DummySerialPort(serialport.SerialPort):
+ _serialFactory = DoNothing
+
+ def _finishPortSetup(self):
+ pass # override default win32 actions
+
+ events = []
+
+ class SerialProtocol(Protocol):
+ def connectionMade(self):
+ events.append("connectionMade")
+
+ def connectionLost(self, reason):
+ events.append(("connectionLost", reason))
+
+ # Creation of port should result in connectionMade call:
+ port = DummySerialPort(SerialProtocol(), "", reactor=DoNothing())
+ self.assertEqual(events, ["connectionMade"])
+
+ # Simulate reactor calling connectionLost on the SerialPort:
+ f = Failure(ConnectionDone())
+ port.connectionLost(f)
+ self.assertEqual(events, ["connectionMade", ("connectionLost", f)])
diff --git a/twisted/internet/test/test_sigchld.py b/twisted/internet/test/test_sigchld.py
new file mode 100644
index 0000000..86a711a
--- /dev/null
+++ b/twisted/internet/test/test_sigchld.py
@@ -0,0 +1,194 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet._sigchld}, an alternate, superior SIGCHLD
+monitoring API.
+"""
+
+import os, signal, errno
+
+from twisted.python.log import msg
+from twisted.trial.unittest import TestCase
+from twisted.internet.fdesc import setNonBlocking
+from twisted.internet._signals import installHandler, isDefaultHandler
+from twisted.internet._signals import _extInstallHandler, _extIsDefaultHandler
+from twisted.internet._signals import _installHandlerUsingSetWakeup, \
+ _installHandlerUsingSignal, _isDefaultHandler
+
+
+class SIGCHLDTestsMixin:
+ """
+ Mixin for L{TestCase} subclasses which defines several tests for
+ I{installHandler} and I{isDefaultHandler}. Subclasses are expected to
+ define C{self.installHandler} and C{self.isDefaultHandler} to invoke the
+ implementation to be tested.
+ """
+
+ if getattr(signal, 'SIGCHLD', None) is None:
+ skip = "Platform does not have SIGCHLD"
+
+ def installHandler(self, fd):
+ """
+ Override in a subclass to install a SIGCHLD handler which writes a byte
+ to the given file descriptor. Return the previously registered file
+ descriptor.
+ """
+ raise NotImplementedError()
+
+
+ def isDefaultHandler(self):
+ """
+ Override in a subclass to determine if the current SIGCHLD handler is
+ SIG_DFL or not. Return True if it is SIG_DFL, False otherwise.
+ """
+ raise NotImplementedError()
+
+
+ def pipe(self):
+ """
+ Create a non-blocking pipe which will be closed after the currently
+ running test.
+ """
+ read, write = os.pipe()
+ self.addCleanup(os.close, read)
+ self.addCleanup(os.close, write)
+ setNonBlocking(read)
+ setNonBlocking(write)
+ return read, write
+
+
+ def setUp(self):
+ """
+ Save the current SIGCHLD handler as reported by L{signal.signal} and
+ the current file descriptor registered with L{installHandler}.
+ """
+ handler = signal.getsignal(signal.SIGCHLD)
+ if handler != signal.SIG_DFL:
+ self.signalModuleHandler = handler
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ else:
+ self.signalModuleHandler = None
+
+ self.oldFD = self.installHandler(-1)
+
+ if self.signalModuleHandler is not None and self.oldFD != -1:
+ msg("SIGCHLD setup issue: %r %r" % (self.signalModuleHandler, self.oldFD))
+ raise RuntimeError("You used some signal APIs wrong! Try again.")
+
+
+ def tearDown(self):
+ """
+ Restore whatever signal handler was present when setUp ran.
+ """
+ # If tests set up any kind of handlers, clear them out.
+ self.installHandler(-1)
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+
+ # Now restore whatever the setup was before the test ran.
+ if self.signalModuleHandler is not None:
+ signal.signal(signal.SIGCHLD, self.signalModuleHandler)
+ elif self.oldFD != -1:
+ self.installHandler(self.oldFD)
+
+
+ def test_isDefaultHandler(self):
+ """
+ L{isDefaultHandler} returns true if the SIGCHLD handler is SIG_DFL,
+ false otherwise.
+ """
+ self.assertTrue(self.isDefaultHandler())
+ signal.signal(signal.SIGCHLD, signal.SIG_IGN)
+ self.assertFalse(self.isDefaultHandler())
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ self.assertTrue(self.isDefaultHandler())
+ signal.signal(signal.SIGCHLD, lambda *args: None)
+ self.assertFalse(self.isDefaultHandler())
+
+
+ def test_returnOldFD(self):
+ """
+ L{installHandler} returns the previously registered file descriptor.
+ """
+ read, write = self.pipe()
+ oldFD = self.installHandler(write)
+ self.assertEqual(self.installHandler(oldFD), write)
+
+
+ def test_uninstallHandler(self):
+ """
+ C{installHandler(-1)} removes the SIGCHLD handler completely.
+ """
+ read, write = self.pipe()
+ self.assertTrue(self.isDefaultHandler())
+ self.installHandler(write)
+ self.assertFalse(self.isDefaultHandler())
+ self.installHandler(-1)
+ self.assertTrue(self.isDefaultHandler())
+
+
+ def test_installHandler(self):
+ """
+ The file descriptor passed to L{installHandler} has a byte written to
+ it when SIGCHLD is delivered to the process.
+ """
+ read, write = self.pipe()
+ self.installHandler(write)
+
+ exc = self.assertRaises(OSError, os.read, read, 1)
+ self.assertEqual(exc.errno, errno.EAGAIN)
+
+ os.kill(os.getpid(), signal.SIGCHLD)
+
+ self.assertEqual(len(os.read(read, 5)), 1)
+
+
+
+class DefaultSIGCHLDTests(SIGCHLDTestsMixin, TestCase):
+ """
+ Tests for whatever implementation is selected for the L{installHandler}
+ and L{isDefaultHandler} APIs.
+ """
+ installHandler = staticmethod(installHandler)
+ isDefaultHandler = staticmethod(isDefaultHandler)
+
+
+
+class ExtensionSIGCHLDTests(SIGCHLDTestsMixin, TestCase):
+ """
+ Tests for the L{twisted.internet._sigchld} implementation of the
+ L{installHandler} and L{isDefaultHandler} APIs.
+ """
+ try:
+ import twisted.internet._sigchld
+ except ImportError:
+ skip = "twisted.internet._sigchld is not available"
+
+ installHandler = _extInstallHandler
+ isDefaultHandler = _extIsDefaultHandler
+
+
+
+class SetWakeupSIGCHLDTests(SIGCHLDTestsMixin, TestCase):
+ """
+ Tests for the L{signal.set_wakeup_fd} implementation of the
+ L{installHandler} and L{isDefaultHandler} APIs.
+ """
+ # Check both of these. On Ubuntu 9.10 (to take an example completely at
+ # random), Python 2.5 has set_wakeup_fd but not siginterrupt.
+ if (getattr(signal, 'set_wakeup_fd', None) is None
+ or getattr(signal, 'siginterrupt', None) is None):
+ skip = "signal.set_wakeup_fd is not available"
+
+ installHandler = staticmethod(_installHandlerUsingSetWakeup)
+ isDefaultHandler = staticmethod(_isDefaultHandler)
+
+
+
+class PlainSignalModuleSIGCHLDTests(SIGCHLDTestsMixin, TestCase):
+ """
+ Tests for the L{signal.signal} implementation of the L{installHandler}
+ and L{isDefaultHandler} APIs.
+ """
+ installHandler = staticmethod(_installHandlerUsingSignal)
+ isDefaultHandler = staticmethod(_isDefaultHandler)
diff --git a/twisted/internet/test/test_socket.py b/twisted/internet/test/test_socket.py
new file mode 100644
index 0000000..019daa5
--- /dev/null
+++ b/twisted/internet/test/test_socket.py
@@ -0,0 +1,96 @@
+
+import errno, socket
+
+from twisted.python.log import err
+from twisted.internet.interfaces import IReactorSocket
+from twisted.internet.error import (
+ UnsupportedAddressFamily, UnsupportedSocketType)
+from twisted.internet.protocol import ServerFactory
+from twisted.internet.test.reactormixins import (
+ ReactorBuilder, needsRunningReactor)
+
+
+class AdoptStreamPortErrorsTestsBuilder(ReactorBuilder):
+ """
+ Builder for testing L{IReactorSocket} implementations.
+
+ Generally only tests for failure cases are found here. Success cases for
+ this interface are tested elsewhere. For example, the success case for
+ I{AF_INET} is in L{twisted.internet.test.test_tcp}, since that case should
+ behave exactly the same as L{IReactorTCP.listenTCP}.
+ """
+ requiredInterfaces = [IReactorSocket]
+
+ def test_invalidDescriptor(self):
+ """
+ An implementation of L{IReactorSocket.adoptStreamPort} raises
+ L{socket.error} if passed an integer which is not associated with a
+ socket.
+ """
+ reactor = self.buildReactor()
+
+ probe = socket.socket()
+ fileno = probe.fileno()
+ probe.close()
+
+ exc = self.assertRaises(
+ socket.error,
+ reactor.adoptStreamPort, fileno, socket.AF_INET, ServerFactory())
+ self.assertEqual(exc.args[0], errno.EBADF)
+
+
+ def test_invalidAddressFamily(self):
+ """
+ An implementation of L{IReactorSocket.adoptStreamPort} raises
+ L{UnsupportedAddressFamily} if passed an address family it does not
+ support.
+ """
+ reactor = self.buildReactor()
+
+ port = socket.socket()
+ port.listen(1)
+ self.addCleanup(port.close)
+
+ arbitrary = 2 ** 16 + 7
+
+ self.assertRaises(
+ UnsupportedAddressFamily,
+ reactor.adoptStreamPort, port.fileno(), arbitrary, ServerFactory())
+
+
+ def test_stopOnlyCloses(self):
+ """
+ When the L{IListeningPort} returned by L{IReactorSocket.adoptStreamPort}
+ is stopped using C{stopListening}, the underlying socket is closed but
+ not shutdown. This allows another process which still has a reference
+ to it to continue accepting connections over it.
+ """
+ reactor = self.buildReactor()
+
+ portSocket = socket.socket()
+ self.addCleanup(portSocket.close)
+
+ portSocket.listen(1)
+ portSocket.setblocking(False)
+
+ # The file descriptor is duplicated by adoptStreamPort
+ port = reactor.adoptStreamPort(
+ portSocket.fileno(), portSocket.family, ServerFactory())
+ d = port.stopListening()
+ def stopped(ignored):
+ # Should still be possible to accept a connection on portSocket. If
+ # it was shutdown, the exception would be EINVAL instead.
+ exc = self.assertRaises(socket.error, portSocket.accept)
+ self.assertEqual(exc.args[0], errno.EAGAIN)
+ d.addCallback(stopped)
+ d.addErrback(err, "Failed to accept on original port.")
+
+ needsRunningReactor(
+ reactor,
+ lambda: d.addCallback(lambda ignored: reactor.stop()))
+
+ reactor.run()
+
+
+
+globals().update(AdoptStreamPortErrorsTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_stdio.py b/twisted/internet/test/test_stdio.py
new file mode 100644
index 0000000..4163e41
--- /dev/null
+++ b/twisted/internet/test/test_stdio.py
@@ -0,0 +1,195 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.stdio}.
+"""
+
+from twisted.python.runtime import platform
+from twisted.internet.test.reactormixins import ReactorBuilder
+from twisted.internet.protocol import Protocol
+if not platform.isWindows():
+ from twisted.internet._posixstdio import StandardIO
+
+
+
+class StdioFilesTests(ReactorBuilder):
+ """
+ L{StandardIO} supports reading and writing to filesystem files.
+ """
+
+ def setUp(self):
+ path = self.mktemp()
+ file(path, "w").close()
+ self.extraFile = file(path, "r+")
+
+
+ def test_addReader(self):
+ """
+ Adding a filesystem file reader to a reactor will make sure it is
+ polled.
+ """
+ reactor = self.buildReactor()
+
+ class DataProtocol(Protocol):
+ data = ""
+ def dataReceived(self, data):
+ self.data += data
+ # It'd be better to stop reactor on connectionLost, but that
+ # fails on FreeBSD, probably due to
+ # http://bugs.python.org/issue9591:
+ if self.data == "hello!":
+ reactor.stop()
+
+ path = self.mktemp()
+ f = file(path, "w")
+ f.write("hello!")
+ f.close()
+ f = file(path, "r")
+
+ # Read bytes from a file, deliver them to a protocol instance:
+ protocol = DataProtocol()
+ StandardIO(protocol, stdin=f.fileno(),
+ stdout=self.extraFile.fileno(),
+ reactor=reactor)
+
+ self.runReactor(reactor)
+ self.assertEqual(protocol.data, "hello!")
+
+
+ def test_addWriter(self):
+ """
+ Adding a filesystem file writer to a reactor will make sure it is
+ polled.
+ """
+ reactor = self.buildReactor()
+
+ class DisconnectProtocol(Protocol):
+ def connectionLost(self, reason):
+ reactor.stop()
+
+ path = self.mktemp()
+ f = file(path, "w")
+
+ # Write bytes to a transport, hopefully have them written to a file:
+ protocol = DisconnectProtocol()
+ StandardIO(protocol, stdout=f.fileno(),
+ stdin=self.extraFile.fileno(), reactor=reactor)
+ protocol.transport.write("hello")
+ protocol.transport.write(", world")
+ protocol.transport.loseConnection()
+
+ self.runReactor(reactor)
+ f.close()
+ f = file(path, "r")
+ self.assertEqual(f.read(), "hello, world")
+ f.close()
+
+
+ def test_removeReader(self):
+ """
+ Removing a filesystem file reader from a reactor will make sure it is
+ no longer polled.
+ """
+ reactor = self.buildReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+
+ path = self.mktemp()
+ file(path, "w").close()
+ # Cleanup might fail if file is GCed too soon:
+ self.f = f = file(path, "r")
+
+ # Have the reader added:
+ stdio = StandardIO(Protocol(), stdin=f.fileno(),
+ stdout=self.extraFile.fileno(),
+ reactor=reactor)
+ self.assertIn(stdio._reader, reactor.getReaders())
+ stdio._reader.stopReading()
+ self.assertNotIn(stdio._reader, reactor.getReaders())
+
+
+ def test_removeWriter(self):
+ """
+ Removing a filesystem file writer from a reactor will make sure it is
+ no longer polled.
+ """
+ reactor = self.buildReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+
+ # Cleanup might fail if file is GCed too soon:
+ self.f = f = file(self.mktemp(), "w")
+
+ # Have the reader added:
+ protocol = Protocol()
+ stdio = StandardIO(protocol, stdout=f.fileno(),
+ stdin=self.extraFile.fileno(),
+ reactor=reactor)
+ protocol.transport.write("hello")
+ self.assertIn(stdio._writer, reactor.getWriters())
+ stdio._writer.stopWriting()
+ self.assertNotIn(stdio._writer, reactor.getWriters())
+
+
+ def test_removeAll(self):
+ """
+ Calling C{removeAll} on a reactor includes descriptors that are
+ filesystem files.
+ """
+ reactor = self.buildReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+
+ path = self.mktemp()
+ file(path, "w").close()
+ # Cleanup might fail if file is GCed too soon:
+ self.f = f = file(path, "r")
+
+ # Have the reader added:
+ stdio = StandardIO(Protocol(), stdin=f.fileno(),
+ stdout=self.extraFile.fileno(), reactor=reactor)
+ # And then removed:
+ removed = reactor.removeAll()
+ self.assertIn(stdio._reader, removed)
+ self.assertNotIn(stdio._reader, reactor.getReaders())
+
+
+ def test_getReaders(self):
+ """
+ C{reactor.getReaders} includes descriptors that are filesystem files.
+ """
+ reactor = self.buildReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+
+ path = self.mktemp()
+ file(path, "w").close()
+ # Cleanup might fail if file is GCed too soon:
+ self.f = f = file(path, "r")
+
+ # Have the reader added:
+ stdio = StandardIO(Protocol(), stdin=f.fileno(),
+ stdout=self.extraFile.fileno(), reactor=reactor)
+ self.assertIn(stdio._reader, reactor.getReaders())
+
+
+ def test_getWriters(self):
+ """
+ C{reactor.getWriters} includes descriptors that are filesystem files.
+ """
+ reactor = self.buildReactor()
+ self.addCleanup(self.unbuildReactor, reactor)
+
+ # Cleanup might fail if file is GCed too soon:
+ self.f = f = file(self.mktemp(), "w")
+
+ # Have the reader added:
+ stdio = StandardIO(Protocol(), stdout=f.fileno(),
+ stdin=self.extraFile.fileno(), reactor=reactor)
+ self.assertNotIn(stdio._writer, reactor.getWriters())
+ stdio._writer.startWriting()
+ self.assertIn(stdio._writer, reactor.getWriters())
+
+ if platform.isWindows():
+ skip = ("StandardIO does not accept stdout as an argument to Windows. "
+ "Testing redirection to a file is therefore harder.")
+
+
+globals().update(StdioFilesTests.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_tcp.py b/twisted/internet/test/test_tcp.py
new file mode 100644
index 0000000..b4aac9c
--- /dev/null
+++ b/twisted/internet/test/test_tcp.py
@@ -0,0 +1,1943 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorTCP} and the TCP parts of
+L{IReactorSocket}.
+"""
+
+__metaclass__ = type
+
+import socket, errno
+
+from zope.interface import implements
+
+from twisted.python.runtime import platform
+from twisted.python.failure import Failure
+from twisted.python import log
+
+from twisted.trial.unittest import SkipTest, TestCase
+from twisted.internet.test.reactormixins import ReactorBuilder, EndpointCreator
+from twisted.internet.test.reactormixins import ConnectableProtocol
+from twisted.internet.test.reactormixins import runProtocolsWithReactor
+from twisted.internet.error import ConnectionLost, UserError, ConnectionRefusedError
+from twisted.internet.error import ConnectionDone, ConnectionAborted
+from twisted.internet.interfaces import (
+ ILoggingContext, IConnector, IReactorFDSet, IReactorSocket)
+from twisted.internet.address import IPv4Address, IPv6Address
+from twisted.internet.defer import (
+ Deferred, DeferredList, maybeDeferred, gatherResults)
+from twisted.internet.endpoints import (
+ TCP4ServerEndpoint, TCP4ClientEndpoint)
+from twisted.internet.protocol import ServerFactory, ClientFactory, Protocol
+from twisted.internet.interfaces import (
+ IPushProducer, IPullProducer, IHalfCloseableProtocol)
+from twisted.internet.tcp import Connection, Server, _resolveIPv6
+
+from twisted.internet.test.connectionmixins import (
+ LogObserverMixin, ConnectionTestsMixin, TCPClientTestsMixin, findFreePort)
+from twisted.internet.test.test_core import ObjectModelIntegrationMixin
+from twisted.test.test_tcp import MyClientFactory, MyServerFactory
+from twisted.test.test_tcp import ClosingFactory, ClientStartStopFactory
+
+try:
+ from OpenSSL import SSL
+except ImportError:
+ useSSL = False
+else:
+ from twisted.internet.ssl import ClientContextFactory
+ useSSL = True
+
+try:
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM).close()
+except socket.error, e:
+ ipv6Skip = str(e)
+else:
+ ipv6Skip = None
+
+
+
+if platform.isWindows():
+ from twisted.internet.test import _win32ifaces
+ getLinkLocalIPv6Addresses = _win32ifaces.win32GetLinkLocalIPv6Addresses
+else:
+ try:
+ from twisted.internet.test import _posixifaces
+ except ImportError:
+ getLinkLocalIPv6Addresses = lambda: []
+ else:
+ getLinkLocalIPv6Addresses = _posixifaces.posixGetLinkLocalIPv6Addresses
+
+
+
+def getLinkLocalIPv6Address():
+ """
+ Find and return a configured link local IPv6 address including a scope
+ identifier using the % separation syntax. If the system has no link local
+ IPv6 addresses, raise L{SkipTest} instead.
+
+ @raise SkipTest: if no link local address can be found or if the
+ C{netifaces} module is not available.
+
+ @return: a C{str} giving the address
+ """
+ addresses = getLinkLocalIPv6Addresses()
+ if addresses:
+ return addresses[0]
+ raise SkipTest("Link local IPv6 address unavailable")
+
+
+
+def connect(client, (host, port)):
+ if '%' in host or ':' in host:
+ address = socket.getaddrinfo(host, port)[0][4]
+ else:
+ address = (host, port)
+ client.connect(address)
+
+
+
+class FakeSocket(object):
+ """
+ A fake for L{socket.socket} objects.
+
+ @ivar data: A C{str} giving the data which will be returned from
+ L{FakeSocket.recv}.
+
+ @ivar sendBuffer: A C{list} of the objects passed to L{FakeSocket.send}.
+ """
+ def __init__(self, data):
+ self.data = data
+ self.sendBuffer = []
+
+ def setblocking(self, blocking):
+ self.blocking = blocking
+
+ def recv(self, size):
+ return self.data
+
+ def send(self, bytes):
+ """
+ I{Send} all of C{bytes} by accumulating it into C{self.sendBuffer}.
+
+ @return: The length of C{bytes}, indicating all the data has been
+ accepted.
+ """
+ self.sendBuffer.append(bytes)
+ return len(bytes)
+
+
+ def shutdown(self, how):
+ """
+ Shutdown is not implemented. The method is provided since real sockets
+ have it and some code expects it. No behavior of L{FakeSocket} is
+ affected by a call to it.
+ """
+
+
+ def close(self):
+ """
+ Close is not implemented. The method is provided since real sockets
+ have it and some code expects it. No behavior of L{FakeSocket} is
+ affected by a call to it.
+ """
+
+
+ def setsockopt(self, *args):
+ """
+ Setsockopt is not implemented. The method is provided since
+ real sockets have it and some code expects it. No behavior of
+ L{FakeSocket} is affected by a call to it.
+ """
+
+
+ def fileno(self):
+ """
+ Return a fake file descriptor. If actually used, this will have no
+ connection to this L{FakeSocket} and will probably cause surprising
+ results.
+ """
+ return 1
+
+
+
+class TestFakeSocket(TestCase):
+ """
+ Test that the FakeSocket can be used by the doRead method of L{Connection}
+ """
+
+ def test_blocking(self):
+ skt = FakeSocket("someData")
+ skt.setblocking(0)
+ self.assertEqual(skt.blocking, 0)
+
+
+ def test_recv(self):
+ skt = FakeSocket("someData")
+ self.assertEqual(skt.recv(10), "someData")
+
+
+ def test_send(self):
+ """
+ L{FakeSocket.send} accepts the entire string passed to it, adds it to
+ its send buffer, and returns its length.
+ """
+ skt = FakeSocket("")
+ count = skt.send("foo")
+ self.assertEqual(count, 3)
+ self.assertEqual(skt.sendBuffer, ["foo"])
+
+
+
+class FakeProtocol(Protocol):
+ """
+ An L{IProtocol} that returns a value from its dataReceived method.
+ """
+ def dataReceived(self, data):
+ """
+ Return something other than C{None} to trigger a deprecation warning for
+ that behavior.
+ """
+ return ()
+
+
+
+class _FakeFDSetReactor(object):
+ """
+ A no-op implementation of L{IReactorFDSet}, which ignores all adds and
+ removes.
+ """
+ implements(IReactorFDSet)
+
+ addReader = addWriter = removeReader = removeWriter = (
+ lambda self, desc: None)
+
+
+
+class TCPServerTests(TestCase):
+ """
+ Whitebox tests for L{twisted.internet.tcp.Server}.
+ """
+ def setUp(self):
+ self.reactor = _FakeFDSetReactor()
+ class FakePort(object):
+ _realPortNumber = 3
+ self.skt = FakeSocket("")
+ self.protocol = Protocol()
+ self.server = Server(
+ self.skt, self.protocol, ("", 0), FakePort(), None, self.reactor)
+
+
+ def test_writeAfterDisconnect(self):
+ """
+ L{Server.write} discards bytes passed to it if called after it has lost
+ its connection.
+ """
+ self.server.connectionLost(
+ Failure(Exception("Simulated lost connection")))
+ self.server.write("hello world")
+ self.assertEqual(self.skt.sendBuffer, [])
+
+
+ def test_writeAfteDisconnectAfterTLS(self):
+ """
+ L{Server.write} discards bytes passed to it if called after it has lost
+ its connection when the connection had started TLS.
+ """
+ self.server.TLS = True
+ self.test_writeAfterDisconnect()
+
+
+ def test_writeSequenceAfterDisconnect(self):
+ """
+ L{Server.writeSequence} discards bytes passed to it if called after it
+ has lost its connection.
+ """
+ self.server.connectionLost(
+ Failure(Exception("Simulated lost connection")))
+ self.server.writeSequence(["hello world"])
+ self.assertEqual(self.skt.sendBuffer, [])
+
+
+ def test_writeSequenceAfteDisconnectAfterTLS(self):
+ """
+ L{Server.writeSequence} discards bytes passed to it if called after it
+ has lost its connection when the connection had started TLS.
+ """
+ self.server.TLS = True
+ self.test_writeSequenceAfterDisconnect()
+
+
+
+class TCPConnectionTests(TestCase):
+ """
+ Whitebox tests for L{twisted.internet.tcp.Connection}.
+ """
+ def test_doReadWarningIsRaised(self):
+ """
+ When an L{IProtocol} implementation that returns a value from its
+ C{dataReceived} method, a deprecated warning is emitted.
+ """
+ skt = FakeSocket("someData")
+ protocol = FakeProtocol()
+ conn = Connection(skt, protocol)
+ conn.doRead()
+ warnings = self.flushWarnings([FakeProtocol.dataReceived])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]["message"],
+ "Returning a value other than None from "
+ "twisted.internet.test.test_tcp.FakeProtocol.dataReceived "
+ "is deprecated since Twisted 11.0.0.")
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_noTLSBeforeStartTLS(self):
+ """
+ The C{TLS} attribute of a L{Connection} instance is C{False} before
+ L{Connection.startTLS} is called.
+ """
+ skt = FakeSocket("")
+ protocol = FakeProtocol()
+ conn = Connection(skt, protocol)
+ self.assertFalse(conn.TLS)
+
+
+ def test_tlsAfterStartTLS(self):
+ """
+ The C{TLS} attribute of a L{Connection} instance is C{True} after
+ L{Connection.startTLS} is called.
+ """
+ skt = FakeSocket("")
+ protocol = FakeProtocol()
+ conn = Connection(skt, protocol, reactor=_FakeFDSetReactor())
+ conn._tlsClientDefault = True
+ conn.startTLS(ClientContextFactory(), True)
+ self.assertTrue(conn.TLS)
+ if not useSSL:
+ test_tlsAfterStartTLS.skip = "No SSL support available"
+
+
+
+class TCPCreator(EndpointCreator):
+ """
+ Create IPv4 TCP endpoints for L{runProtocolsWithReactor}-based tests.
+ """
+
+ interface = "127.0.0.1"
+
+ def server(self, reactor):
+ """
+ Create a server-side TCP endpoint.
+ """
+ return TCP4ServerEndpoint(reactor, 0, interface=self.interface)
+
+
+ def client(self, reactor, serverAddress):
+ """
+ Create a client end point that will connect to the given address.
+
+ @type serverAddress: L{IPv4Address}
+ """
+ return TCP4ClientEndpoint(reactor, self.interface, serverAddress.port)
+
+
+
+class TCP6Creator(TCPCreator):
+ """
+ Create IPv6 TCP endpoints for
+ C{ReactorBuilder.runProtocolsWithReactor}-based tests.
+
+ The endpoint types in question here are still the TCP4 variety, since
+ these simply pass through IPv6 address literals to the reactor, and we are
+ only testing address literals, not name resolution (as name resolution has
+ not yet been implemented). See http://twistedmatrix.com/trac/ticket/4470
+ for more specific information about new endpoint classes. The naming is
+ slightly misleading, but presumably if you're passing an IPv6 literal, you
+ know what you're asking for.
+ """
+ def __init__(self):
+ self.interface = getLinkLocalIPv6Address()
+
+
+
+class TCPClientTestsBase(ReactorBuilder, ConnectionTestsMixin,
+ TCPClientTestsMixin):
+ """
+ Base class for builders defining tests related to L{IReactorTCP.connectTCP}.
+ """
+ port = 1234
+
+ @property
+ def interface(self):
+ """
+ Return the interface attribute from the endpoints object.
+ """
+ return self.endpoints.interface
+
+
+
+class TCP4ClientTestsBuilder(TCPClientTestsBase):
+ """
+ Builder configured with IPv4 parameters for tests related to L{IReactorTCP.connectTCP}.
+ """
+ fakeDomainName = 'some-fake.domain.example.com'
+ family = socket.AF_INET
+ addressClass = IPv4Address
+
+ endpoints = TCPCreator()
+
+
+
+class TCP6ClientTestsBuilder(TCPClientTestsBase):
+ """
+ Builder configured with IPv6 parameters for tests related to L{IReactorTCP.connectTCP}.
+ """
+
+ if ipv6Skip:
+ skip = "Platform does not support ipv6"
+
+ family = socket.AF_INET6
+ addressClass = IPv6Address
+
+
+ def setUp(self):
+ # Only create this object here, so that it won't be created if tests
+ # are being skipped:
+ self.endpoints = TCP6Creator()
+ # This is used by test_addresses to test the distinction between the
+ # resolved name and the name on the socket itself. All the same
+ # invariants should hold, but giving back an IPv6 address from a
+ # resolver is not something the reactor can handle, so instead, we make
+ # it so that the connect call for the IPv6 address test simply uses an
+ # address literal.
+ self.fakeDomainName = self.endpoints.interface
+
+
+
+class TCPConnectorTestsBuilder(ReactorBuilder):
+
+ def test_connectorIdentity(self):
+ """
+ L{IReactorTCP.connectTCP} returns an object which provides
+ L{IConnector}. The destination of the connector is the address which
+ was passed to C{connectTCP}. The same connector object is passed to
+ the factory's C{startedConnecting} method as to the factory's
+ C{clientConnectionLost} method.
+ """
+ serverFactory = ClosingFactory()
+ reactor = self.buildReactor()
+ tcpPort = reactor.listenTCP(0, serverFactory, interface=self.interface)
+ serverFactory.port = tcpPort
+ portNumber = tcpPort.getHost().port
+
+ seenConnectors = []
+ seenFailures = []
+
+ clientFactory = ClientStartStopFactory()
+ clientFactory.clientConnectionLost = (
+ lambda connector, reason: (seenConnectors.append(connector),
+ seenFailures.append(reason)))
+ clientFactory.startedConnecting = seenConnectors.append
+
+ connector = reactor.connectTCP(self.interface, portNumber,
+ clientFactory)
+ self.assertTrue(IConnector.providedBy(connector))
+ dest = connector.getDestination()
+ self.assertEqual(dest.type, "TCP")
+ self.assertEqual(dest.host, self.interface)
+ self.assertEqual(dest.port, portNumber)
+
+ clientFactory.whenStopped.addBoth(lambda _: reactor.stop())
+
+ self.runReactor(reactor)
+
+ seenFailures[0].trap(ConnectionDone)
+ self.assertEqual(seenConnectors, [connector, connector])
+
+
+ def test_userFail(self):
+ """
+ Calling L{IConnector.stopConnecting} in C{Factory.startedConnecting}
+ results in C{Factory.clientConnectionFailed} being called with
+ L{error.UserError} as the reason.
+ """
+ serverFactory = MyServerFactory()
+ reactor = self.buildReactor()
+ tcpPort = reactor.listenTCP(0, serverFactory, interface=self.interface)
+ portNumber = tcpPort.getHost().port
+
+ fatalErrors = []
+
+ def startedConnecting(connector):
+ try:
+ connector.stopConnecting()
+ except Exception:
+ fatalErrors.append(Failure())
+ reactor.stop()
+
+ clientFactory = ClientStartStopFactory()
+ clientFactory.startedConnecting = startedConnecting
+
+ clientFactory.whenStopped.addBoth(lambda _: reactor.stop())
+
+ reactor.callWhenRunning(lambda: reactor.connectTCP(self.interface,
+ portNumber,
+ clientFactory))
+
+ self.runReactor(reactor)
+
+ if fatalErrors:
+ self.fail(fatalErrors[0].getTraceback())
+ clientFactory.reason.trap(UserError)
+ self.assertEqual(clientFactory.failed, 1)
+
+
+ def test_reconnect(self):
+ """
+ Calling L{IConnector.connect} in C{Factory.clientConnectionLost} causes
+ a new connection attempt to be made.
+ """
+ serverFactory = ClosingFactory()
+ reactor = self.buildReactor()
+ tcpPort = reactor.listenTCP(0, serverFactory, interface=self.interface)
+ serverFactory.port = tcpPort
+ portNumber = tcpPort.getHost().port
+
+ clientFactory = MyClientFactory()
+
+ def clientConnectionLost(connector, reason):
+ connector.connect()
+ clientFactory.clientConnectionLost = clientConnectionLost
+ reactor.connectTCP(self.interface, portNumber, clientFactory)
+
+ protocolMadeAndClosed = []
+ def reconnectFailed(ignored):
+ p = clientFactory.protocol
+ protocolMadeAndClosed.append((p.made, p.closed))
+ reactor.stop()
+
+ clientFactory.failDeferred.addCallback(reconnectFailed)
+
+ self.runReactor(reactor)
+
+ clientFactory.reason.trap(ConnectionRefusedError)
+ self.assertEqual(protocolMadeAndClosed, [(1, 1)])
+
+
+
+class TCP4ConnectorTestsBuilder(TCPConnectorTestsBuilder):
+ interface = '127.0.0.1'
+ family = socket.AF_INET
+ addressClass = IPv4Address
+
+
+
+class TCP6ConnectorTestsBuilder(TCPConnectorTestsBuilder):
+ family = socket.AF_INET6
+ addressClass = IPv6Address
+
+ if ipv6Skip:
+ skip = "Platform does not support ipv6"
+
+ def setUp(self):
+ self.interface = getLinkLocalIPv6Address()
+
+
+
+def createTestSocket(test, addressFamily, socketType):
+ """
+ Create a socket for the duration of the given test.
+
+ @param test: the test to add cleanup to.
+
+ @param addressFamily: an C{AF_*} constant
+
+ @param socketType: a C{SOCK_*} constant.
+
+ @return: a socket object.
+ """
+ skt = socket.socket(addressFamily, socketType)
+ test.addCleanup(skt.close)
+ return skt
+
+
+
+class StreamTransportTestsMixin(LogObserverMixin):
+ """
+ Mixin defining tests which apply to any port/connection based transport.
+ """
+ def test_startedListeningLogMessage(self):
+ """
+ When a port starts, a message including a description of the associated
+ factory is logged.
+ """
+ loggedMessages = self.observe()
+ reactor = self.buildReactor()
+ class SomeFactory(ServerFactory):
+ implements(ILoggingContext)
+ def logPrefix(self):
+ return "Crazy Factory"
+ factory = SomeFactory()
+ p = self.getListeningPort(reactor, factory)
+ expectedMessage = self.getExpectedStartListeningLogMessage(
+ p, "Crazy Factory")
+ self.assertEqual((expectedMessage,), loggedMessages[0]['message'])
+
+
+ def test_connectionLostLogMsg(self):
+ """
+ When a connection is lost, an informative message should be logged
+ (see L{getExpectedConnectionLostLogMsg}): an address identifying
+ the port and the fact that it was closed.
+ """
+
+ loggedMessages = []
+ def logConnectionLostMsg(eventDict):
+ loggedMessages.append(log.textFromEventDict(eventDict))
+
+ reactor = self.buildReactor()
+ p = self.getListeningPort(reactor, ServerFactory())
+ expectedMessage = self.getExpectedConnectionLostLogMsg(p)
+ log.addObserver(logConnectionLostMsg)
+
+ def stopReactor(ignored):
+ log.removeObserver(logConnectionLostMsg)
+ reactor.stop()
+
+ def doStopListening():
+ log.addObserver(logConnectionLostMsg)
+ maybeDeferred(p.stopListening).addCallback(stopReactor)
+
+ reactor.callWhenRunning(doStopListening)
+ reactor.run()
+
+ self.assertIn(expectedMessage, loggedMessages)
+
+
+ def test_allNewStyle(self):
+ """
+ The L{IListeningPort} object is an instance of a class with no
+ classic classes in its hierarchy.
+ """
+ reactor = self.buildReactor()
+ port = self.getListeningPort(reactor, ServerFactory())
+ self.assertFullyNewStyle(port)
+
+
+class ListenTCPMixin(object):
+ """
+ Mixin which uses L{IReactorTCP.listenTCP} to hand out listening TCP ports.
+ """
+ def getListeningPort(self, reactor, factory, port=0, interface=''):
+ """
+ Get a TCP port from a reactor.
+ """
+ return reactor.listenTCP(port, factory, interface=interface)
+
+
+
+class SocketTCPMixin(object):
+ """
+ Mixin which uses L{IReactorSocket.adoptStreamPort} to hand out listening TCP
+ ports.
+ """
+ def getListeningPort(self, reactor, factory, port=0, interface=''):
+ """
+ Get a TCP port from a reactor, wrapping an already-initialized file
+ descriptor.
+ """
+ if IReactorSocket.providedBy(reactor):
+ if ':' in interface:
+ domain = socket.AF_INET6
+ address = socket.getaddrinfo(interface, port)[0][4]
+ else:
+ domain = socket.AF_INET
+ address = (interface, port)
+ portSock = socket.socket(domain)
+ portSock.bind(address)
+ portSock.listen(3)
+ portSock.setblocking(False)
+ try:
+ return reactor.adoptStreamPort(
+ portSock.fileno(), portSock.family, factory)
+ finally:
+ # The socket should still be open; fileno will raise if it is
+ # not.
+ portSock.fileno()
+ # Now clean it up, because the rest of the test does not need
+ # it.
+ portSock.close()
+ else:
+ raise SkipTest("Reactor does not provide IReactorSocket")
+
+
+
+class TCPPortTestsMixin(object):
+ """
+ Tests for L{IReactorTCP.listenTCP}
+ """
+ def getExpectedStartListeningLogMessage(self, port, factory):
+ """
+ Get the message expected to be logged when a TCP port starts listening.
+ """
+ return "%s starting on %d" % (
+ factory, port.getHost().port)
+
+
+ def getExpectedConnectionLostLogMsg(self, port):
+ """
+ Get the expected connection lost message for a TCP port.
+ """
+ return "(TCP Port %s Closed)" % (port.getHost().port,)
+
+
+ def test_portGetHostOnIPv4(self):
+ """
+ When no interface is passed to L{IReactorTCP.listenTCP}, the returned
+ listening port listens on an IPv4 address.
+ """
+ reactor = self.buildReactor()
+ port = self.getListeningPort(reactor, ServerFactory())
+ address = port.getHost()
+ self.assertIsInstance(address, IPv4Address)
+
+
+ def test_portGetHostOnIPv6(self):
+ """
+ When listening on an IPv6 address, L{IListeningPort.getHost} returns
+ an L{IPv6Address} with C{host} and C{port} attributes reflecting the
+ address the port is bound to.
+ """
+ reactor = self.buildReactor()
+ host, portNumber = findFreePort(
+ family=socket.AF_INET6, interface='::1')[:2]
+ port = self.getListeningPort(
+ reactor, ServerFactory(), portNumber, host)
+ address = port.getHost()
+ self.assertIsInstance(address, IPv6Address)
+ self.assertEqual('::1', address.host)
+ self.assertEqual(portNumber, address.port)
+ if ipv6Skip:
+ test_portGetHostOnIPv6.skip = ipv6Skip
+
+
+ def test_portGetHostOnIPv6ScopeID(self):
+ """
+ When a link-local IPv6 address including a scope identifier is passed as
+ the C{interface} argument to L{IReactorTCP.listenTCP}, the resulting
+ L{IListeningPort} reports its address as an L{IPv6Address} with a host
+ value that includes the scope identifier.
+ """
+ linkLocal = getLinkLocalIPv6Address()
+ reactor = self.buildReactor()
+ port = self.getListeningPort(reactor, ServerFactory(), 0, linkLocal)
+ address = port.getHost()
+ self.assertIsInstance(address, IPv6Address)
+ self.assertEqual(linkLocal, address.host)
+ if ipv6Skip:
+ test_portGetHostOnIPv6ScopeID.skip = ipv6Skip
+
+
+ def _buildProtocolAddressTest(self, client, interface):
+ """
+ Connect C{client} to a server listening on C{interface} started with
+ L{IReactorTCP.listenTCP} and return the address passed to the factory's
+ C{buildProtocol} method.
+
+ @param client: A C{SOCK_STREAM} L{socket.socket} created with an address
+ family such that it will be able to connect to a server listening on
+ C{interface}.
+
+ @param interface: A C{str} giving an address for a server to listen on.
+ This should almost certainly be the loopback address for some
+ address family supported by L{IReactorTCP.listenTCP}.
+
+ @return: Whatever object, probably an L{IAddress} provider, is passed to
+ a server factory's C{buildProtocol} method when C{client}
+ establishes a connection.
+ """
+ class ObserveAddress(ServerFactory):
+ def buildProtocol(self, address):
+ reactor.stop()
+ self.observedAddress = address
+ return Protocol()
+
+ factory = ObserveAddress()
+ reactor = self.buildReactor()
+ port = self.getListeningPort(reactor, factory, 0, interface)
+ client.setblocking(False)
+ try:
+ connect(client, (port.getHost().host, port.getHost().port))
+ except socket.error, (errnum, message):
+ self.assertIn(errnum, (errno.EINPROGRESS, errno.EWOULDBLOCK))
+
+ self.runReactor(reactor)
+
+ return factory.observedAddress
+
+
+ def test_buildProtocolIPv4Address(self):
+ """
+ When a connection is accepted over IPv4, an L{IPv4Address} is passed
+ to the factory's C{buildProtocol} method giving the peer's address.
+ """
+ interface = '127.0.0.1'
+ client = createTestSocket(self, socket.AF_INET, socket.SOCK_STREAM)
+ observedAddress = self._buildProtocolAddressTest(client, interface)
+ self.assertEqual(
+ IPv4Address('TCP', *client.getsockname()), observedAddress)
+
+
+ def test_buildProtocolIPv6Address(self):
+ """
+ When a connection is accepted to an IPv6 address, an L{IPv6Address} is
+ passed to the factory's C{buildProtocol} method giving the peer's
+ address.
+ """
+ interface = '::1'
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ observedAddress = self._buildProtocolAddressTest(client, interface)
+ self.assertEqual(
+ IPv6Address('TCP', *client.getsockname()[:2]), observedAddress)
+ if ipv6Skip:
+ test_buildProtocolIPv6Address.skip = ipv6Skip
+
+
+ def test_buildProtocolIPv6AddressScopeID(self):
+ """
+ When a connection is accepted to a link-local IPv6 address, an
+ L{IPv6Address} is passed to the factory's C{buildProtocol} method
+ giving the peer's address, including a scope identifier.
+ """
+ interface = getLinkLocalIPv6Address()
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ observedAddress = self._buildProtocolAddressTest(client, interface)
+ self.assertEqual(
+ IPv6Address('TCP', *client.getsockname()[:2]), observedAddress)
+ if ipv6Skip:
+ test_buildProtocolIPv6AddressScopeID.skip = ipv6Skip
+
+
+ def _serverGetConnectionAddressTest(self, client, interface, which):
+ """
+ Connect C{client} to a server listening on C{interface} started with
+ L{IReactorTCP.listenTCP} and return the address returned by one of the
+ server transport's address lookup methods, C{getHost} or C{getPeer}.
+
+ @param client: A C{SOCK_STREAM} L{socket.socket} created with an address
+ family such that it will be able to connect to a server listening on
+ C{interface}.
+
+ @param interface: A C{str} giving an address for a server to listen on.
+ This should almost certainly be the loopback address for some
+ address family supported by L{IReactorTCP.listenTCP}.
+
+ @param which: A C{str} equal to either C{"getHost"} or C{"getPeer"}
+ determining which address will be returned.
+
+ @return: Whatever object, probably an L{IAddress} provider, is returned
+ from the method indicated by C{which}.
+ """
+ class ObserveAddress(Protocol):
+ def makeConnection(self, transport):
+ reactor.stop()
+ self.factory.address = getattr(transport, which)()
+
+ reactor = self.buildReactor()
+ factory = ServerFactory()
+ factory.protocol = ObserveAddress
+ port = self.getListeningPort(reactor, factory, 0, interface)
+ client.setblocking(False)
+ try:
+ connect(client, (port.getHost().host, port.getHost().port))
+ except socket.error, (errnum, message):
+ self.assertIn(errnum, (errno.EINPROGRESS, errno.EWOULDBLOCK))
+ self.runReactor(reactor)
+ return factory.address
+
+
+ def test_serverGetHostOnIPv4(self):
+ """
+ When a connection is accepted over IPv4, the server
+ L{ITransport.getHost} method returns an L{IPv4Address} giving the
+ address on which the server accepted the connection.
+ """
+ interface = '127.0.0.1'
+ client = createTestSocket(self, socket.AF_INET, socket.SOCK_STREAM)
+ hostAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getHost')
+ self.assertEqual(
+ IPv4Address('TCP', *client.getpeername()), hostAddress)
+
+
+ def test_serverGetHostOnIPv6(self):
+ """
+ When a connection is accepted over IPv6, the server
+ L{ITransport.getHost} method returns an L{IPv6Address} giving the
+ address on which the server accepted the connection.
+ """
+ interface = '::1'
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ hostAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getHost')
+ self.assertEqual(
+ IPv6Address('TCP', *client.getpeername()[:2]), hostAddress)
+ if ipv6Skip:
+ test_serverGetHostOnIPv6.skip = ipv6Skip
+
+
+ def test_serverGetHostOnIPv6ScopeID(self):
+ """
+ When a connection is accepted over IPv6, the server
+ L{ITransport.getHost} method returns an L{IPv6Address} giving the
+ address on which the server accepted the connection, including the scope
+ identifier.
+ """
+ interface = getLinkLocalIPv6Address()
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ hostAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getHost')
+ self.assertEqual(
+ IPv6Address('TCP', *client.getpeername()[:2]), hostAddress)
+ if ipv6Skip:
+ test_serverGetHostOnIPv6ScopeID.skip = ipv6Skip
+
+
+ def test_serverGetPeerOnIPv4(self):
+ """
+ When a connection is accepted over IPv4, the server
+ L{ITransport.getPeer} method returns an L{IPv4Address} giving the
+ address of the remote end of the connection.
+ """
+ interface = '127.0.0.1'
+ client = createTestSocket(self, socket.AF_INET, socket.SOCK_STREAM)
+ peerAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getPeer')
+ self.assertEqual(
+ IPv4Address('TCP', *client.getsockname()), peerAddress)
+
+
+ def test_serverGetPeerOnIPv6(self):
+ """
+ When a connection is accepted over IPv6, the server
+ L{ITransport.getPeer} method returns an L{IPv6Address} giving the
+ address on the remote end of the connection.
+ """
+ interface = '::1'
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ peerAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getPeer')
+ self.assertEqual(
+ IPv6Address('TCP', *client.getsockname()[:2]), peerAddress)
+ if ipv6Skip:
+ test_serverGetPeerOnIPv6.skip = ipv6Skip
+
+
+ def test_serverGetPeerOnIPv6ScopeID(self):
+ """
+ When a connection is accepted over IPv6, the server
+ L{ITransport.getPeer} method returns an L{IPv6Address} giving the
+ address on the remote end of the connection, including the scope
+ identifier.
+ """
+ interface = getLinkLocalIPv6Address()
+ client = createTestSocket(self, socket.AF_INET6, socket.SOCK_STREAM)
+ peerAddress = self._serverGetConnectionAddressTest(
+ client, interface, 'getPeer')
+ self.assertEqual(
+ IPv6Address('TCP', *client.getsockname()[:2]), peerAddress)
+ if ipv6Skip:
+ test_serverGetPeerOnIPv6ScopeID.skip = ipv6Skip
+
+
+
+class TCPPortTestsBuilder(ReactorBuilder, ListenTCPMixin, TCPPortTestsMixin,
+ ObjectModelIntegrationMixin,
+ StreamTransportTestsMixin):
+ pass
+
+
+
+class TCPFDPortTestsBuilder(ReactorBuilder, SocketTCPMixin, TCPPortTestsMixin,
+ ObjectModelIntegrationMixin,
+ StreamTransportTestsMixin):
+ pass
+
+
+
+class StopStartReadingProtocol(Protocol):
+ """
+ Protocol that pauses and resumes the transport a few times
+ """
+
+ def connectionMade(self):
+ self.data = ''
+ self.pauseResumeProducing(3)
+
+
+ def pauseResumeProducing(self, counter):
+ """
+ Toggle transport read state, then count down.
+ """
+ self.transport.pauseProducing()
+ self.transport.resumeProducing()
+ if counter:
+ self.factory.reactor.callLater(0,
+ self.pauseResumeProducing, counter - 1)
+ else:
+ self.factory.reactor.callLater(0,
+ self.factory.ready.callback, self)
+
+
+ def dataReceived(self, data):
+ log.msg('got data', len(data))
+ self.data += data
+ if len(self.data) == 4*4096:
+ self.factory.stop.callback(self.data)
+
+
+
+class TCPConnectionTestsBuilder(ReactorBuilder):
+ """
+ Builder defining tests relating to L{twisted.internet.tcp.Connection}.
+ """
+
+ def test_stopStartReading(self):
+ """
+ This test verifies transport socket read state after multiple
+ pause/resumeProducing calls.
+ """
+ sf = ServerFactory()
+ reactor = sf.reactor = self.buildReactor()
+
+ skippedReactors = ["Glib2Reactor", "Gtk2Reactor"]
+ reactorClassName = reactor.__class__.__name__
+ if reactorClassName in skippedReactors and platform.isWindows():
+ raise SkipTest(
+ "This test is broken on gtk/glib under Windows.")
+
+ sf.protocol = StopStartReadingProtocol
+ sf.ready = Deferred()
+ sf.stop = Deferred()
+ p = reactor.listenTCP(0, sf)
+ port = p.getHost().port
+ def proceed(protos, port):
+ """
+ Send several IOCPReactor's buffers' worth of data.
+ """
+ self.assertTrue(protos[0])
+ self.assertTrue(protos[1])
+ protos = protos[0][1], protos[1][1]
+ protos[0].transport.write('x' * (2 * 4096) + 'y' * (2 * 4096))
+ return (sf.stop.addCallback(cleanup, protos, port)
+ .addCallback(lambda ign: reactor.stop()))
+
+ def cleanup(data, protos, port):
+ """
+ Make sure IOCPReactor didn't start several WSARecv operations
+ that clobbered each other's results.
+ """
+ self.assertEqual(data, 'x'*(2*4096) + 'y'*(2*4096),
+ 'did not get the right data')
+ return DeferredList([
+ maybeDeferred(protos[0].transport.loseConnection),
+ maybeDeferred(protos[1].transport.loseConnection),
+ maybeDeferred(port.stopListening)])
+
+ cc = TCP4ClientEndpoint(reactor, '127.0.0.1', port)
+ cf = ClientFactory()
+ cf.protocol = Protocol
+ d = DeferredList([cc.connect(cf), sf.ready]).addCallback(proceed, p)
+ self.runReactor(reactor)
+ return d
+
+
+ def test_connectionLostAfterPausedTransport(self):
+ """
+ Alice connects to Bob. Alice writes some bytes and then shuts down the
+ connection. Bob receives the bytes from the connection and then pauses
+ the transport object. Shortly afterwards Bob resumes the transport
+ object. At that point, Bob is notified that the connection has been
+ closed.
+
+ This is no problem for most reactors. The underlying event notification
+ API will probably just remind them that the connection has been closed.
+ It is a little tricky for win32eventreactor (MsgWaitForMultipleObjects).
+ MsgWaitForMultipleObjects will only deliver the close notification once.
+ The reactor needs to remember that notification until Bob resumes the
+ transport.
+ """
+ class Pauser(ConnectableProtocol):
+ def __init__(self):
+ self.events = []
+
+ def dataReceived(self, bytes):
+ self.events.append("paused")
+ self.transport.pauseProducing()
+ self.reactor.callLater(0, self.resume)
+
+ def resume(self):
+ self.events.append("resumed")
+ self.transport.resumeProducing()
+
+ def connectionLost(self, reason):
+ # This is the event you have been waiting for.
+ self.events.append("lost")
+ ConnectableProtocol.connectionLost(self, reason)
+
+ class Client(ConnectableProtocol):
+ def connectionMade(self):
+ self.transport.write("some bytes for you")
+ self.transport.loseConnection()
+
+ pauser = Pauser()
+ runProtocolsWithReactor(self, pauser, Client(), TCPCreator())
+ self.assertEqual(pauser.events, ["paused", "resumed", "lost"])
+
+
+ def test_doubleHalfClose(self):
+ """
+ If one side half-closes its connection, and then the other side of the
+ connection calls C{loseWriteConnection}, and then C{loseConnection} in
+ {writeConnectionLost}, the connection is closed correctly.
+
+ This rather obscure case used to fail (see ticket #3037).
+ """
+ class ListenerProtocol(ConnectableProtocol):
+ implements(IHalfCloseableProtocol)
+
+ def readConnectionLost(self):
+ self.transport.loseWriteConnection()
+
+ def writeConnectionLost(self):
+ self.transport.loseConnection()
+
+ class Client(ConnectableProtocol):
+ def connectionMade(self):
+ self.transport.loseConnection()
+
+ # If test fails, reactor won't stop and we'll hit timeout:
+ runProtocolsWithReactor(
+ self, ListenerProtocol(), Client(), TCPCreator())
+
+
+
+class WriteSequenceTests(ReactorBuilder):
+ """
+ Test for L{twisted.internet.abstract.FileDescriptor.writeSequence}.
+
+ @ivar client: the connected client factory to be used in tests.
+ @type client: L{MyClientFactory}
+
+ @ivar server: the listening server factory to be used in tests.
+ @type server: L{MyServerFactory}
+ """
+ def setUp(self):
+ server = MyServerFactory()
+ server.protocolConnectionMade = Deferred()
+ server.protocolConnectionLost = Deferred()
+ self.server = server
+
+ client = MyClientFactory()
+ client.protocolConnectionMade = Deferred()
+ client.protocolConnectionLost = Deferred()
+ self.client = client
+
+
+ def setWriteBufferSize(self, transport, value):
+ """
+ Set the write buffer size for the given transport, mananing possible
+ differences (ie, IOCP). Bug #4322 should remove the need of that hack.
+ """
+ if getattr(transport, "writeBufferSize", None) is not None:
+ transport.writeBufferSize = value
+ else:
+ transport.bufferSize = value
+
+
+ def test_withoutWrite(self):
+ """
+ C{writeSequence} sends the data even if C{write} hasn't been called.
+ """
+ client, server = self.client, self.server
+ reactor = self.buildReactor()
+
+ port = reactor.listenTCP(0, server)
+
+ def dataReceived(data):
+ log.msg("data received: %r" % data)
+ self.assertEquals(data, "Some sequence splitted")
+ client.protocol.transport.loseConnection()
+
+ def clientConnected(proto):
+ log.msg("client connected %s" % proto)
+ proto.transport.writeSequence(["Some ", "sequence ", "splitted"])
+
+ def serverConnected(proto):
+ log.msg("server connected %s" % proto)
+ proto.dataReceived = dataReceived
+
+ d1 = client.protocolConnectionMade.addCallback(clientConnected)
+ d2 = server.protocolConnectionMade.addCallback(serverConnected)
+ d3 = server.protocolConnectionLost
+ d4 = client.protocolConnectionLost
+ d = gatherResults([d1, d2, d3, d4])
+ def stop(result):
+ reactor.stop()
+ return result
+ d.addBoth(stop)
+
+ reactor.connectTCP("127.0.0.1", port.getHost().port, client)
+ self.runReactor(reactor)
+
+
+ def test_writeSequenceWithUnicodeRaisesException(self):
+ """
+ C{writeSequence} with an element in the sequence of type unicode raises
+ C{TypeError}.
+ """
+ client, server = self.client, self.server
+ reactor = self.buildReactor()
+
+ port = reactor.listenTCP(0, server)
+
+ reactor.connectTCP("127.0.0.1", port.getHost().port, client)
+
+ def serverConnected(proto):
+ log.msg("server connected %s" % proto)
+ exc = self.assertRaises(
+ TypeError,
+ proto.transport.writeSequence, [u"Unicode is not kosher"])
+ self.assertEquals(str(exc), "Data must not be unicode")
+
+ d = server.protocolConnectionMade.addCallback(serverConnected)
+ d.addErrback(log.err)
+ d.addCallback(lambda ignored: reactor.stop())
+
+ self.runReactor(reactor)
+
+
+ def _producerTest(self, clientConnected):
+ """
+ Helper for testing producers which call C{writeSequence}. This will set
+ up a connection which a producer can use. It returns after the
+ connection is closed.
+
+ @param clientConnected: A callback which will be invoked with a client
+ protocol after a connection is setup. This is responsible for
+ setting up some sort of producer.
+ """
+ reactor = self.buildReactor()
+
+ port = reactor.listenTCP(0, self.server)
+
+ # The following could probably all be much simpler, but for #5285.
+
+ # First let the server notice the connection
+ d1 = self.server.protocolConnectionMade
+
+ # Grab the client connection Deferred now though, so we don't lose it if
+ # the client connects before the server.
+ d2 = self.client.protocolConnectionMade
+
+ def serverConnected(proto):
+ # Now take action as soon as the client is connected
+ d2.addCallback(clientConnected)
+ return d2
+ d1.addCallback(serverConnected)
+
+ d3 = self.server.protocolConnectionLost
+ d4 = self.client.protocolConnectionLost
+
+ # After the client is connected and does its producer stuff, wait for
+ # the disconnection events.
+ def didProducerActions(ignored):
+ return gatherResults([d3, d4])
+ d1.addCallback(didProducerActions)
+
+ def stop(result):
+ reactor.stop()
+ return result
+ d1.addBoth(stop)
+
+ reactor.connectTCP("127.0.0.1", port.getHost().port, self.client)
+
+ self.runReactor(reactor)
+
+
+ def test_streamingProducer(self):
+ """
+ C{writeSequence} pauses its streaming producer if too much data is
+ buffered, and then resumes it.
+ """
+ client, server = self.client, self.server
+
+ class SaveActionProducer(object):
+ implements(IPushProducer)
+ def __init__(self):
+ self.actions = []
+
+ def pauseProducing(self):
+ self.actions.append("pause")
+
+ def resumeProducing(self):
+ self.actions.append("resume")
+ # Unregister the producer so the connection can close
+ client.protocol.transport.unregisterProducer()
+ # This is why the code below waits for the server connection
+ # first - so we have it to close here. We close the server side
+ # because win32evenreactor cannot reliably observe us closing
+ # the client side (#5285).
+ server.protocol.transport.loseConnection()
+
+ def stopProducing(self):
+ self.actions.append("stop")
+
+ producer = SaveActionProducer()
+
+ def clientConnected(proto):
+ # Register a streaming producer and verify that it gets paused after
+ # it writes more than the local send buffer can hold.
+ proto.transport.registerProducer(producer, True)
+ self.assertEquals(producer.actions, [])
+ self.setWriteBufferSize(proto.transport, 500)
+ proto.transport.writeSequence(["x" * 50] * 20)
+ self.assertEquals(producer.actions, ["pause"])
+
+ self._producerTest(clientConnected)
+ # After the send buffer gets a chance to empty out a bit, the producer
+ # should be resumed.
+ self.assertEquals(producer.actions, ["pause", "resume"])
+
+
+ def test_nonStreamingProducer(self):
+ """
+ C{writeSequence} pauses its producer if too much data is buffered only
+ if this is a streaming producer.
+ """
+ client, server = self.client, self.server
+ test = self
+
+ class SaveActionProducer(object):
+ implements(IPullProducer)
+ def __init__(self):
+ self.actions = []
+
+ def resumeProducing(self):
+ self.actions.append("resume")
+ if self.actions.count("resume") == 2:
+ client.protocol.transport.stopConsuming()
+ else:
+ test.setWriteBufferSize(client.protocol.transport, 500)
+ client.protocol.transport.writeSequence(["x" * 50] * 20)
+
+ def stopProducing(self):
+ self.actions.append("stop")
+
+ producer = SaveActionProducer()
+
+ def clientConnected(proto):
+ # Register a non-streaming producer and verify that it is resumed
+ # immediately.
+ proto.transport.registerProducer(producer, False)
+ self.assertEquals(producer.actions, ["resume"])
+
+ self._producerTest(clientConnected)
+ # After the local send buffer empties out, the producer should be
+ # resumed again.
+ self.assertEquals(producer.actions, ["resume", "resume"])
+
+
+globals().update(TCP4ClientTestsBuilder.makeTestCaseClasses())
+globals().update(TCP6ClientTestsBuilder.makeTestCaseClasses())
+globals().update(TCPPortTestsBuilder.makeTestCaseClasses())
+globals().update(TCPFDPortTestsBuilder.makeTestCaseClasses())
+globals().update(TCPConnectionTestsBuilder.makeTestCaseClasses())
+globals().update(TCP4ConnectorTestsBuilder.makeTestCaseClasses())
+globals().update(TCP6ConnectorTestsBuilder.makeTestCaseClasses())
+globals().update(WriteSequenceTests.makeTestCaseClasses())
+
+
+
+class ServerAbortsTwice(ConnectableProtocol):
+ """
+ Call abortConnection() twice.
+ """
+
+ def dataReceived(self, data):
+ self.transport.abortConnection()
+ self.transport.abortConnection()
+
+
+
+class ServerAbortsThenLoses(ConnectableProtocol):
+ """
+ Call abortConnection() followed by loseConnection().
+ """
+
+ def dataReceived(self, data):
+ self.transport.abortConnection()
+ self.transport.loseConnection()
+
+
+
+class AbortServerWritingProtocol(ConnectableProtocol):
+ """
+ Protocol that writes data upon connection.
+ """
+
+ def connectionMade(self):
+ """
+ Tell the client that the connection is set up and it's time to abort.
+ """
+ self.transport.write("ready")
+
+
+
+class ReadAbortServerProtocol(AbortServerWritingProtocol):
+ """
+ Server that should never receive any data, except 'X's which are written
+ by the other side of the connection before abortConnection, and so might
+ possibly arrive.
+ """
+
+ def dataReceived(self, data):
+ if data.replace('X', ''):
+ raise Exception("Unexpectedly received data.")
+
+
+
+class NoReadServer(ConnectableProtocol):
+ """
+ Stop reading immediately on connection.
+
+ This simulates a lost connection that will cause the other side to time
+ out, and therefore call abortConnection().
+ """
+
+ def connectionMade(self):
+ self.transport.stopReading()
+
+
+
+class EventualNoReadServer(ConnectableProtocol):
+ """
+ Like NoReadServer, except we Wait until some bytes have been delivered
+ before stopping reading. This means TLS handshake has finished, where
+ applicable.
+ """
+
+ gotData = False
+ stoppedReading = False
+
+
+ def dataReceived(self, data):
+ if not self.gotData:
+ self.gotData = True
+ self.transport.registerProducer(self, False)
+ self.transport.write("hello")
+
+
+ def resumeProducing(self):
+ if self.stoppedReading:
+ return
+ self.stoppedReading = True
+ # We've written out the data:
+ self.transport.stopReading()
+
+
+ def pauseProducing(self):
+ pass
+
+
+ def stopProducing(self):
+ pass
+
+
+
+class BaseAbortingClient(ConnectableProtocol):
+ """
+ Base class for abort-testing clients.
+ """
+ inReactorMethod = False
+
+ def connectionLost(self, reason):
+ if self.inReactorMethod:
+ raise RuntimeError("BUG: connectionLost was called re-entrantly!")
+ ConnectableProtocol.connectionLost(self, reason)
+
+
+
+class WritingButNotAbortingClient(BaseAbortingClient):
+ """
+ Write data, but don't abort.
+ """
+
+ def connectionMade(self):
+ self.transport.write("hello")
+
+
+
+class AbortingClient(BaseAbortingClient):
+ """
+ Call abortConnection() after writing some data.
+ """
+
+ def dataReceived(self, data):
+ """
+ Some data was received, so the connection is set up.
+ """
+ self.inReactorMethod = True
+ self.writeAndAbort()
+ self.inReactorMethod = False
+
+
+ def writeAndAbort(self):
+ # X is written before abortConnection, and so there is a chance it
+ # might arrive. Y is written after, and so no Ys should ever be
+ # delivered:
+ self.transport.write("X" * 10000)
+ self.transport.abortConnection()
+ self.transport.write("Y" * 10000)
+
+
+
+class AbortingTwiceClient(AbortingClient):
+ """
+ Call abortConnection() twice, after writing some data.
+ """
+
+ def writeAndAbort(self):
+ AbortingClient.writeAndAbort(self)
+ self.transport.abortConnection()
+
+
+
+class AbortingThenLosingClient(AbortingClient):
+ """
+ Call abortConnection() and then loseConnection().
+ """
+
+ def writeAndAbort(self):
+ AbortingClient.writeAndAbort(self)
+ self.transport.loseConnection()
+
+
+
+class ProducerAbortingClient(ConnectableProtocol):
+ """
+ Call abortConnection from doWrite, via resumeProducing.
+ """
+
+ inReactorMethod = True
+ producerStopped = False
+
+ def write(self):
+ self.transport.write("lalala" * 127000)
+ self.inRegisterProducer = True
+ self.transport.registerProducer(self, False)
+ self.inRegisterProducer = False
+
+
+ def connectionMade(self):
+ self.write()
+
+
+ def resumeProducing(self):
+ self.inReactorMethod = True
+ if not self.inRegisterProducer:
+ self.transport.abortConnection()
+ self.inReactorMethod = False
+
+
+ def stopProducing(self):
+ self.producerStopped = True
+
+
+ def connectionLost(self, reason):
+ if not self.producerStopped:
+ raise RuntimeError("BUG: stopProducing() was never called.")
+ if self.inReactorMethod:
+ raise RuntimeError("BUG: connectionLost called re-entrantly!")
+ ConnectableProtocol.connectionLost(self, reason)
+
+
+
+class StreamingProducerClient(ConnectableProtocol):
+ """
+ Call abortConnection() when the other side has stopped reading.
+
+ In particular, we want to call abortConnection() only once our local
+ socket hits a state where it is no longer writeable. This helps emulate
+ the most common use case for abortConnection(), closing a connection after
+ a timeout, with write buffers being full.
+
+ Since it's very difficult to know when this actually happens, we just
+ write a lot of data, and assume at that point no more writes will happen.
+ """
+ paused = False
+ extraWrites = 0
+ inReactorMethod = False
+
+ def connectionMade(self):
+ self.write()
+
+
+ def write(self):
+ """
+ Write large amount to transport, then wait for a while for buffers to
+ fill up.
+ """
+ self.transport.registerProducer(self, True)
+ for i in range(100):
+ self.transport.write("1234567890" * 32000)
+
+
+ def resumeProducing(self):
+ self.paused = False
+
+
+ def stopProducing(self):
+ pass
+
+
+ def pauseProducing(self):
+ """
+ Called when local buffer fills up.
+
+ The goal is to hit the point where the local file descriptor is not
+ writeable (or the moral equivalent). The fact that pauseProducing has
+ been called is not sufficient, since that can happen when Twisted's
+ buffers fill up but OS hasn't gotten any writes yet. We want to be as
+ close as possible to every buffer (including OS buffers) being full.
+
+ So, we wait a bit more after this for Twisted to write out a few
+ chunks, then abortConnection.
+ """
+ if self.paused:
+ return
+ self.paused = True
+ # The amount we wait is arbitrary, we just want to make sure some
+ # writes have happened and outgoing OS buffers filled up -- see
+ # http://twistedmatrix.com/trac/ticket/5303 for details:
+ self.reactor.callLater(0.01, self.doAbort)
+
+
+ def doAbort(self):
+ if not self.paused:
+ log.err(RuntimeError("BUG: We should be paused a this point."))
+ self.inReactorMethod = True
+ self.transport.abortConnection()
+ self.inReactorMethod = False
+
+
+ def connectionLost(self, reason):
+ # Tell server to start reading again so it knows to go away:
+ self.otherProtocol.transport.startReading()
+ ConnectableProtocol.connectionLost(self, reason)
+
+
+
+class StreamingProducerClientLater(StreamingProducerClient):
+ """
+ Call abortConnection() from dataReceived, after bytes have been
+ exchanged.
+ """
+
+ def connectionMade(self):
+ self.transport.write("hello")
+ self.gotData = False
+
+
+ def dataReceived(self, data):
+ if not self.gotData:
+ self.gotData = True
+ self.write()
+
+
+class ProducerAbortingClientLater(ProducerAbortingClient):
+ """
+ Call abortConnection from doWrite, via resumeProducing.
+
+ Try to do so after some bytes have already been exchanged, so we
+ don't interrupt SSL handshake.
+ """
+
+ def connectionMade(self):
+ # Override base class connectionMade().
+ pass
+
+
+ def dataReceived(self, data):
+ self.write()
+
+
+
+class DataReceivedRaisingClient(AbortingClient):
+ """
+ Call abortConnection(), and then throw exception, from dataReceived.
+ """
+
+ def dataReceived(self, data):
+ self.transport.abortConnection()
+ raise ZeroDivisionError("ONO")
+
+
+
+class ResumeThrowsClient(ProducerAbortingClient):
+ """
+ Call abortConnection() and throw exception from resumeProducing().
+ """
+
+ def resumeProducing(self):
+ if not self.inRegisterProducer:
+ self.transport.abortConnection()
+ raise ZeroDivisionError("ono!")
+
+
+ def connectionLost(self, reason):
+ # Base class assertion about stopProducing being called isn't valid;
+ # if the we blew up in resumeProducing, consumers are justified in
+ # giving up on the producer and not calling stopProducing.
+ ConnectableProtocol.connectionLost(self, reason)
+
+
+
+class AbortConnectionMixin(object):
+ """
+ Unit tests for L{ITransport.abortConnection}.
+ """
+ # Override in subclasses, should be a EndpointCreator instance:
+ endpoints = None
+
+ def runAbortTest(self, clientClass, serverClass,
+ clientConnectionLostReason=None):
+ """
+ A test runner utility function, which hooks up a matched pair of client
+ and server protocols.
+
+ We then run the reactor until both sides have disconnected, and then
+ verify that the right exception resulted.
+ """
+ clientExpectedExceptions = (ConnectionAborted, ConnectionLost)
+ serverExpectedExceptions = (ConnectionLost, ConnectionDone)
+ # In TLS tests we may get SSL.Error instead of ConnectionLost,
+ # since we're trashing the TLS protocol layer.
+ if useSSL:
+ clientExpectedExceptions = clientExpectedExceptions + (SSL.Error,)
+ serverExpectedExceptions = serverExpectedExceptions + (SSL.Error,)
+
+ client = clientClass()
+ server = serverClass()
+ client.otherProtocol = server
+ server.otherProtocol = client
+ reactor = runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ # Make sure everything was shutdown correctly:
+ self.assertEqual(reactor.removeAll(), [])
+ # The reactor always has a timeout added in runReactor():
+ delayedCalls = reactor.getDelayedCalls()
+ self.assertEqual(len(delayedCalls), 1, map(str, delayedCalls))
+
+ if clientConnectionLostReason is not None:
+ self.assertIsInstance(
+ client.disconnectReason.value,
+ (clientConnectionLostReason,) + clientExpectedExceptions)
+ else:
+ self.assertIsInstance(client.disconnectReason.value,
+ clientExpectedExceptions)
+ self.assertIsInstance(server.disconnectReason.value, serverExpectedExceptions)
+
+
+ def test_dataReceivedAbort(self):
+ """
+ abortConnection() is called in dataReceived. The protocol should be
+ disconnected, but connectionLost should not be called re-entrantly.
+ """
+ return self.runAbortTest(AbortingClient, ReadAbortServerProtocol)
+
+
+ def test_clientAbortsConnectionTwice(self):
+ """
+ abortConnection() is called twice by client.
+
+ No exception should be thrown, and the connection will be closed.
+ """
+ return self.runAbortTest(AbortingTwiceClient, ReadAbortServerProtocol)
+
+
+ def test_clientAbortsConnectionThenLosesConnection(self):
+ """
+ Client calls abortConnection(), followed by loseConnection().
+
+ No exception should be thrown, and the connection will be closed.
+ """
+ return self.runAbortTest(AbortingThenLosingClient,
+ ReadAbortServerProtocol)
+
+
+ def test_serverAbortsConnectionTwice(self):
+ """
+ abortConnection() is called twice by server.
+
+ No exception should be thrown, and the connection will be closed.
+ """
+ return self.runAbortTest(WritingButNotAbortingClient, ServerAbortsTwice,
+ clientConnectionLostReason=ConnectionLost)
+
+
+ def test_serverAbortsConnectionThenLosesConnection(self):
+ """
+ Server calls abortConnection(), followed by loseConnection().
+
+ No exception should be thrown, and the connection will be closed.
+ """
+ return self.runAbortTest(WritingButNotAbortingClient,
+ ServerAbortsThenLoses,
+ clientConnectionLostReason=ConnectionLost)
+
+
+ def test_resumeProducingAbort(self):
+ """
+ abortConnection() is called in resumeProducing, before any bytes have
+ been exchanged. The protocol should be disconnected, but
+ connectionLost should not be called re-entrantly.
+ """
+ self.runAbortTest(ProducerAbortingClient,
+ ConnectableProtocol)
+
+
+ def test_resumeProducingAbortLater(self):
+ """
+ abortConnection() is called in resumeProducing, after some
+ bytes have been exchanged. The protocol should be disconnected.
+ """
+ return self.runAbortTest(ProducerAbortingClientLater,
+ AbortServerWritingProtocol)
+
+
+ def test_fullWriteBuffer(self):
+ """
+ abortConnection() triggered by the write buffer being full.
+
+ In particular, the server side stops reading. This is supposed
+ to simulate a realistic timeout scenario where the client
+ notices the server is no longer accepting data.
+
+ The protocol should be disconnected, but connectionLost should not be
+ called re-entrantly.
+ """
+ self.runAbortTest(StreamingProducerClient,
+ NoReadServer)
+
+
+ def test_fullWriteBufferAfterByteExchange(self):
+ """
+ abortConnection() is triggered by a write buffer being full.
+
+ However, this buffer is filled after some bytes have been exchanged,
+ allowing a TLS handshake if we're testing TLS. The connection will
+ then be lost.
+ """
+ return self.runAbortTest(StreamingProducerClientLater,
+ EventualNoReadServer)
+
+
+ def test_dataReceivedThrows(self):
+ """
+ dataReceived calls abortConnection(), and then raises an exception.
+
+ The connection will be lost, with the thrown exception
+ (C{ZeroDivisionError}) as the reason on the client. The idea here is
+ that bugs should not be masked by abortConnection, in particular
+ unexpected exceptions.
+ """
+ self.runAbortTest(DataReceivedRaisingClient,
+ AbortServerWritingProtocol,
+ clientConnectionLostReason=ZeroDivisionError)
+ errors = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEquals(len(errors), 1)
+
+
+ def test_resumeProducingThrows(self):
+ """
+ resumeProducing calls abortConnection(), and then raises an exception.
+
+ The connection will be lost, with the thrown exception
+ (C{ZeroDivisionError}) as the reason on the client. The idea here is
+ that bugs should not be masked by abortConnection, in particular
+ unexpected exceptions.
+ """
+ self.runAbortTest(ResumeThrowsClient,
+ ConnectableProtocol,
+ clientConnectionLostReason=ZeroDivisionError)
+ errors = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEquals(len(errors), 1)
+
+
+
+class AbortConnectionTestCase(ReactorBuilder, AbortConnectionMixin):
+ """
+ TCP-specific L{AbortConnectionMixin} tests.
+ """
+
+ endpoints = TCPCreator()
+
+globals().update(AbortConnectionTestCase.makeTestCaseClasses())
+
+
+
+class SimpleUtilityTestCase(TestCase):
+ """
+ Simple, direct tests for helpers within L{twisted.internet.tcp}.
+ """
+
+ skip = ipv6Skip
+
+ def test_resolveNumericHost(self):
+ """
+ L{_resolveIPv6} raises a L{socket.gaierror} (L{socket.EAI_NONAME}) when
+ invoked with a non-numeric host. (In other words, it is passing
+ L{socket.AI_NUMERICHOST} to L{socket.getaddrinfo} and will not
+ accidentally block if it receives bad input.)
+ """
+ err = self.assertRaises(socket.gaierror, _resolveIPv6, "localhost", 1)
+ self.assertEqual(err.args[0], socket.EAI_NONAME)
+
+
+ def test_resolveNumericService(self):
+ """
+ L{_resolveIPv6} raises a L{socket.gaierror} (L{socket.EAI_NONAME}) when
+ invoked with a non-numeric port. (In other words, it is passing
+ L{socket.AI_NUMERICSERV} to L{socket.getaddrinfo} and will not
+ accidentally block if it receives bad input.)
+ """
+ err = self.assertRaises(socket.gaierror, _resolveIPv6, "::1", "http")
+ self.assertEqual(err.args[0], socket.EAI_NONAME)
+
+ if platform.isWindows():
+ test_resolveNumericService.skip = ("The AI_NUMERICSERV flag is not "
+ "supported by Microsoft providers.")
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms738520.aspx
+
+
+ def test_resolveIPv6(self):
+ """
+ L{_resolveIPv6} discovers the flow info and scope ID of an IPv6
+ address.
+ """
+ result = _resolveIPv6("::1", 2)
+ self.assertEqual(len(result), 4)
+ # We can't say anything more useful about these than that they're
+ # integers, because the whole point of getaddrinfo is that you can never
+ # know a-priori know _anything_ about the network interfaces of the
+ # computer that you're on and you have to ask it.
+ self.assertIsInstance(result[2], int) # flow info
+ self.assertIsInstance(result[3], int) # scope id
+ # but, luckily, IP presentation format and what it means to be a port
+ # number are a little better specified.
+ self.assertEqual(result[:2], ("::1", 2))
+
+
+
diff --git a/twisted/internet/test/test_threads.py b/twisted/internet/test/test_threads.py
new file mode 100644
index 0000000..36d855d
--- /dev/null
+++ b/twisted/internet/test/test_threads.py
@@ -0,0 +1,215 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorThreads}.
+"""
+
+__metaclass__ = type
+
+from weakref import ref
+import gc, threading
+
+from twisted.python.threadable import isInIOThread
+from twisted.internet.test.reactormixins import ReactorBuilder
+from twisted.python.threadpool import ThreadPool
+
+
+class ThreadTestsBuilder(ReactorBuilder):
+ """
+ Builder for defining tests relating to L{IReactorThreads}.
+ """
+ def test_getThreadPool(self):
+ """
+ C{reactor.getThreadPool()} returns an instance of L{ThreadPool} which
+ starts when C{reactor.run()} is called and stops before it returns.
+ """
+ state = []
+ reactor = self.buildReactor()
+
+ pool = reactor.getThreadPool()
+ self.assertIsInstance(pool, ThreadPool)
+ self.assertFalse(
+ pool.started, "Pool should not start before reactor.run")
+
+ def f():
+ # Record the state for later assertions
+ state.append(pool.started)
+ state.append(pool.joined)
+ reactor.stop()
+
+ reactor.callWhenRunning(f)
+ self.runReactor(reactor, 2)
+
+ self.assertTrue(
+ state[0], "Pool should start after reactor.run")
+ self.assertFalse(
+ state[1], "Pool should not be joined before reactor.stop")
+ self.assertTrue(
+ pool.joined,
+ "Pool should be stopped after reactor.run returns")
+
+
+ def test_suggestThreadPoolSize(self):
+ """
+ C{reactor.suggestThreadPoolSize()} sets the maximum size of the reactor
+ threadpool.
+ """
+ reactor = self.buildReactor()
+ reactor.suggestThreadPoolSize(17)
+ pool = reactor.getThreadPool()
+ self.assertEqual(pool.max, 17)
+
+
+ def test_delayedCallFromThread(self):
+ """
+ A function scheduled with L{IReactorThreads.callFromThread} invoked
+ from a delayed call is run immediately in the next reactor iteration.
+
+ When invoked from the reactor thread, previous implementations of
+ L{IReactorThreads.callFromThread} would skip the pipe/socket based wake
+ up step, assuming the reactor would wake up on its own. However, this
+ resulted in the reactor not noticing a insert into the thread queue at
+ the right time (in this case, after the thread queue has been processed
+ for that reactor iteration).
+ """
+ reactor = self.buildReactor()
+
+ def threadCall():
+ reactor.stop()
+
+ # Set up the use of callFromThread being tested.
+ reactor.callLater(0, reactor.callFromThread, threadCall)
+
+ before = reactor.seconds()
+ self.runReactor(reactor, 60)
+ after = reactor.seconds()
+
+ # We specified a timeout of 60 seconds. The timeout code in runReactor
+ # probably won't actually work, though. If the reactor comes out of
+ # the event notification API just a little bit early, say after 59.9999
+ # seconds instead of after 60 seconds, then the queued thread call will
+ # get processed but the timeout delayed call runReactor sets up won't!
+ # Then the reactor will stop and runReactor will return without the
+ # timeout firing. As it turns out, select() and poll() are quite
+ # likely to return *slightly* earlier than we ask them to, so the
+ # timeout will rarely happen, even if callFromThread is broken. So,
+ # instead we'll measure the elapsed time and make sure it's something
+ # less than about half of the timeout we specified. This is heuristic.
+ # It assumes that select() won't ever return after 30 seconds when we
+ # asked it to timeout after 60 seconds. And of course like all
+ # time-based tests, it's slightly non-deterministic. If the OS doesn't
+ # schedule this process for 30 seconds, then the test might fail even
+ # if callFromThread is working.
+ self.assertTrue(after - before < 30)
+
+
+ def test_callFromThread(self):
+ """
+ A function scheduled with L{IReactorThreads.callFromThread} invoked
+ from another thread is run in the reactor thread.
+ """
+ reactor = self.buildReactor()
+ result = []
+
+ def threadCall():
+ result.append(threading.currentThread())
+ reactor.stop()
+ reactor.callLater(0, reactor.callInThread,
+ reactor.callFromThread, threadCall)
+ self.runReactor(reactor, 5)
+
+ self.assertEquals(result, [threading.currentThread()])
+
+
+ def test_stopThreadPool(self):
+ """
+ When the reactor stops, L{ReactorBase._stopThreadPool} drops the
+ reactor's direct reference to its internal threadpool and removes
+ the associated startup and shutdown triggers.
+
+ This is the case of the thread pool being created before the reactor
+ is run.
+ """
+ reactor = self.buildReactor()
+ threadpool = ref(reactor.getThreadPool())
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ gc.collect()
+ self.assertIdentical(threadpool(), None)
+
+
+ def test_stopThreadPoolWhenStartedAfterReactorRan(self):
+ """
+ We must handle the case of shutting down the thread pool when it was
+ started after the reactor was run in a special way.
+
+ Some implementation background: The thread pool is started with
+ callWhenRunning, which only returns a system trigger ID when it is
+ invoked before the reactor is started.
+
+ This is the case of the thread pool being created after the reactor
+ is started.
+ """
+ reactor = self.buildReactor()
+ threadPoolRefs = []
+ def acquireThreadPool():
+ threadPoolRefs.append(ref(reactor.getThreadPool()))
+ reactor.stop()
+ reactor.callWhenRunning(acquireThreadPool)
+ self.runReactor(reactor)
+ gc.collect()
+ self.assertIdentical(threadPoolRefs[0](), None)
+
+
+ def test_cleanUpThreadPoolEvenBeforeReactorIsRun(self):
+ """
+ When the reactor has its shutdown event fired before it is run, the
+ thread pool is completely destroyed.
+
+ For what it's worth, the reason we support this behavior at all is
+ because Trial does this.
+
+ This is the case of the thread pool being created without the reactor
+ being started at al.
+ """
+ reactor = self.buildReactor()
+ threadPoolRef = ref(reactor.getThreadPool())
+ reactor.fireSystemEvent("shutdown")
+ gc.collect()
+ self.assertIdentical(threadPoolRef(), None)
+
+
+ def test_isInIOThread(self):
+ """
+ The reactor registers itself as the I/O thread when it runs so that
+ L{twisted.python.threadable.isInIOThread} returns C{True} if it is
+ called in the thread the reactor is running in.
+ """
+ results = []
+ reactor = self.buildReactor()
+ def check():
+ results.append(isInIOThread())
+ reactor.stop()
+ reactor.callWhenRunning(check)
+ self.runReactor(reactor)
+ self.assertEqual([True], results)
+
+
+ def test_isNotInIOThread(self):
+ """
+ The reactor registers itself as the I/O thread when it runs so that
+ L{twisted.python.threadable.isInIOThread} returns C{False} if it is
+ called in a different thread than the reactor is running in.
+ """
+ results = []
+ reactor = self.buildReactor()
+ def check():
+ results.append(isInIOThread())
+ reactor.callFromThread(reactor.stop)
+ reactor.callInThread(check)
+ self.runReactor(reactor)
+ self.assertEqual([False], results)
+
+
+globals().update(ThreadTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_time.py b/twisted/internet/test/test_time.py
new file mode 100644
index 0000000..0810f6a
--- /dev/null
+++ b/twisted/internet/test/test_time.py
@@ -0,0 +1,61 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorTime}.
+"""
+
+__metaclass__ = type
+
+from twisted.python.runtime import platform
+from twisted.internet.test.reactormixins import ReactorBuilder
+
+
+class TimeTestsBuilder(ReactorBuilder):
+ """
+ Builder for defining tests relating to L{IReactorTime}.
+ """
+ def test_delayedCallStopsReactor(self):
+ """
+ The reactor can be stopped by a delayed call.
+ """
+ reactor = self.buildReactor()
+ reactor.callLater(0, reactor.stop)
+ reactor.run()
+
+
+
+class GlibTimeTestsBuilder(ReactorBuilder):
+ """
+ Builder for defining tests relating to L{IReactorTime} for reactors based
+ off glib.
+ """
+ if platform.isWindows():
+ _reactors = ["twisted.internet.gtk2reactor.PortableGtkReactor"]
+ else:
+ _reactors = ["twisted.internet.glib2reactor.Glib2Reactor",
+ "twisted.internet.gtk2reactor.Gtk2Reactor"]
+
+ def test_timeout_add(self):
+ """
+ A C{reactor.callLater} call scheduled from a C{gobject.timeout_add}
+ call is run on time.
+ """
+ import gobject
+ reactor = self.buildReactor()
+
+ result = []
+ def gschedule():
+ reactor.callLater(0, callback)
+ return 0
+ def callback():
+ result.append(True)
+ reactor.stop()
+
+ reactor.callWhenRunning(gobject.timeout_add, 10, gschedule)
+ self.runReactor(reactor, 5)
+ self.assertEqual(result, [True])
+
+
+globals().update(TimeTestsBuilder.makeTestCaseClasses())
+globals().update(GlibTimeTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_tls.py b/twisted/internet/test/test_tls.py
new file mode 100644
index 0000000..223d5f6
--- /dev/null
+++ b/twisted/internet/test/test_tls.py
@@ -0,0 +1,432 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{ITLSTransport}.
+"""
+
+__metaclass__ = type
+
+import sys, operator
+
+from zope.interface import implements
+
+from twisted.internet.test.reactormixins import ReactorBuilder, EndpointCreator
+from twisted.internet.protocol import ServerFactory, ClientFactory, Protocol
+from twisted.internet.interfaces import (
+ IReactorSSL, ITLSTransport, IStreamClientEndpoint)
+from twisted.internet.defer import Deferred, DeferredList
+from twisted.internet.endpoints import (
+ SSL4ServerEndpoint, SSL4ClientEndpoint, TCP4ClientEndpoint)
+from twisted.internet.error import ConnectionClosed
+from twisted.internet.task import Cooperator
+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.python.runtime import platform
+
+from twisted.internet.test.test_core import ObjectModelIntegrationMixin
+from twisted.internet.test.test_tcp import (
+ StreamTransportTestsMixin, AbortConnectionMixin)
+from twisted.internet.test.connectionmixins import ConnectionTestsMixin
+from twisted.internet.test.connectionmixins import BrokenContextFactory
+
+try:
+ from OpenSSL.crypto import FILETYPE_PEM
+except ImportError:
+ FILETYPE_PEM = None
+else:
+ from twisted.internet.ssl import PrivateCertificate, KeyPair
+ from twisted.internet.ssl import ClientContextFactory
+
+
+class TLSMixin:
+ requiredInterfaces = [IReactorSSL]
+
+ if platform.isWindows():
+ msg = (
+ "For some reason, these reactors don't deal with SSL "
+ "disconnection correctly on Windows. See #3371.")
+ skippedReactors = {
+ "twisted.internet.glib2reactor.Glib2Reactor": msg,
+ "twisted.internet.gtk2reactor.Gtk2Reactor": msg}
+
+
+class ContextGeneratingMixin(object):
+ _certificateText = (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDBjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER\n"
+ "MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD\n"
+ "ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n\n"
+ "cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzEL\n"
+ "MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhv\n"
+ "c3QxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEB\n"
+ "BQADSwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh\n"
+ "5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQC\n"
+ "MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl\n"
+ "MB0GA1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7\n"
+ "hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoT\n"
+ "CE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlw\n"
+ "dG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx\n"
+ "LmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6\n"
+ "BoJuVwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++\n"
+ "7QGG/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JE\n"
+ "WUQ9Ho4EzbYCOQ==\n"
+ "-----END CERTIFICATE-----\n")
+
+ _privateKeyText = (
+ "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh\n"
+ "5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAQJBAIqm/bz4NA1H++Vx5Ewx\n"
+ "OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT\n"
+ "ZIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4\n"
+ "nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2\n"
+ "HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNEH+vRWsAYU/gbx+OQB+7VOcBAiEA\n"
+ "oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w=\n"
+ "-----END RSA PRIVATE KEY-----\n")
+
+
+ def getServerContext(self):
+ """
+ Return a new SSL context suitable for use in a test server.
+ """
+ cert = PrivateCertificate.load(
+ self._certificateText,
+ KeyPair.load(self._privateKeyText, FILETYPE_PEM),
+ FILETYPE_PEM)
+ return cert.options()
+
+
+ def getClientContext(self):
+ return ClientContextFactory()
+
+
+
+class StartTLSClientEndpoint(object):
+ """
+ An endpoint which wraps another one and adds a TLS layer immediately when
+ connections are set up.
+
+ @ivar wrapped: A L{IStreamClientEndpoint} provider which will be used to
+ really set up connections.
+
+ @ivar contextFactory: A L{ContextFactory} to use to do TLS.
+ """
+ implements(IStreamClientEndpoint)
+
+ def __init__(self, wrapped, contextFactory):
+ self.wrapped = wrapped
+ self.contextFactory = contextFactory
+
+
+ def connect(self, factory):
+ """
+ Establish a connection using a protocol build by C{factory} and
+ immediately start TLS on it. Return a L{Deferred} which fires with the
+ protocol instance.
+ """
+ # This would be cleaner when we have ITransport.switchProtocol, which
+ # will be added with ticket #3204:
+ class WrapperFactory(ServerFactory):
+ def buildProtocol(wrapperSelf, addr):
+ protocol = factory.buildProtocol(addr)
+ def connectionMade(orig=protocol.connectionMade):
+ protocol.transport.startTLS(self.contextFactory)
+ orig()
+ protocol.connectionMade = connectionMade
+ return protocol
+
+ return self.wrapped.connect(WrapperFactory())
+
+
+
+class StartTLSClientCreator(EndpointCreator, ContextGeneratingMixin):
+ """
+ Create L{ITLSTransport.startTLS} endpoint for the client, and normal SSL
+ for server just because it's easier.
+ """
+ def server(self, reactor):
+ """
+ Construct an SSL server endpoint. This should be be constructing a TCP
+ server endpoint which immediately calls C{startTLS} instead, but that
+ is hard.
+ """
+ return SSL4ServerEndpoint(reactor, 0, self.getServerContext())
+
+
+ def client(self, reactor, serverAddress):
+ """
+ Construct a TCP client endpoint wrapped to immediately start TLS.
+ """
+ return StartTLSClientEndpoint(
+ TCP4ClientEndpoint(
+ reactor, '127.0.0.1', serverAddress.port),
+ ClientContextFactory())
+
+
+
+class BadContextTestsMixin(object):
+ """
+ Mixin for L{ReactorBuilder} subclasses which defines a helper for testing
+ the handling of broken context factories.
+ """
+ def _testBadContext(self, useIt):
+ """
+ Assert that the exception raised by a broken context factory's
+ C{getContext} method is raised by some reactor method. If it is not, an
+ exception will be raised to fail the test.
+
+ @param useIt: A two-argument callable which will be called with a
+ reactor and a broken context factory and which is expected to raise
+ the same exception as the broken context factory's C{getContext}
+ method.
+ """
+ reactor = self.buildReactor()
+ exc = self.assertRaises(
+ ValueError, useIt, reactor, BrokenContextFactory())
+ self.assertEqual(BrokenContextFactory.message, str(exc))
+
+
+
+class StartTLSClientTestsMixin(TLSMixin, ReactorBuilder, ConnectionTestsMixin):
+ """
+ Tests for TLS connections established using L{ITLSTransport.startTLS} (as
+ opposed to L{IReactorSSL.connectSSL} or L{IReactorSSL.listenSSL}).
+ """
+ endpoints = StartTLSClientCreator()
+
+
+
+class SSLCreator(EndpointCreator, ContextGeneratingMixin):
+ """
+ Create SSL endpoints.
+ """
+ def server(self, reactor):
+ """
+ Create an SSL server endpoint on a TCP/IP-stack allocated port.
+ """
+ return SSL4ServerEndpoint(reactor, 0, self.getServerContext())
+
+
+ def client(self, reactor, serverAddress):
+ """
+ Create an SSL client endpoint which will connect localhost on
+ the port given by C{serverAddress}.
+
+ @type serverAddress: L{IPv4Address}
+ """
+ return SSL4ClientEndpoint(
+ reactor, '127.0.0.1', serverAddress.port,
+ ClientContextFactory())
+
+
+class SSLClientTestsMixin(TLSMixin, ReactorBuilder, ContextGeneratingMixin,
+ ConnectionTestsMixin, BadContextTestsMixin):
+ """
+ Mixin defining tests relating to L{ITLSTransport}.
+ """
+ endpoints = SSLCreator()
+
+ def test_badContext(self):
+ """
+ If the context factory passed to L{IReactorSSL.connectSSL} raises an
+ exception from its C{getContext} method, that exception is raised by
+ L{IReactorSSL.connectSSL}.
+ """
+ def useIt(reactor, contextFactory):
+ return reactor.connectSSL(
+ "127.0.0.1", 1234, ClientFactory(), contextFactory)
+ self._testBadContext(useIt)
+
+
+ def test_disconnectAfterWriteAfterStartTLS(self):
+ """
+ L{ITCPTransport.loseConnection} ends a connection which was set up with
+ L{ITLSTransport.startTLS} and which has recently been written to. This
+ is intended to verify that a socket send error masked by the TLS
+ implementation doesn't prevent the connection from being reported as
+ closed.
+ """
+ class ShortProtocol(Protocol):
+ def connectionMade(self):
+ if not ITLSTransport.providedBy(self.transport):
+ # Functionality isn't available to be tested.
+ finished = self.factory.finished
+ self.factory.finished = None
+ finished.errback(SkipTest("No ITLSTransport support"))
+ return
+
+ # Switch the transport to TLS.
+ self.transport.startTLS(self.factory.context)
+ # Force TLS to really get negotiated. If nobody talks, nothing
+ # will happen.
+ self.transport.write("x")
+
+ def dataReceived(self, data):
+ # Stuff some bytes into the socket. This mostly has the effect
+ # of causing the next write to fail with ENOTCONN or EPIPE.
+ # With the pyOpenSSL implementation of ITLSTransport, the error
+ # is swallowed outside of the control of Twisted.
+ self.transport.write("y")
+ # Now close the connection, which requires a TLS close alert to
+ # be sent.
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ # This is the success case. The client and the server want to
+ # get here.
+ finished = self.factory.finished
+ if finished is not None:
+ self.factory.finished = None
+ finished.callback(reason)
+
+ reactor = self.buildReactor()
+
+ serverFactory = ServerFactory()
+ serverFactory.finished = Deferred()
+ serverFactory.protocol = ShortProtocol
+ serverFactory.context = self.getServerContext()
+
+ clientFactory = ClientFactory()
+ clientFactory.finished = Deferred()
+ clientFactory.protocol = ShortProtocol
+ clientFactory.context = self.getClientContext()
+ clientFactory.context.method = serverFactory.context.method
+
+ lostConnectionResults = []
+ finished = DeferredList(
+ [serverFactory.finished, clientFactory.finished],
+ consumeErrors=True)
+ def cbFinished(results):
+ lostConnectionResults.extend([results[0][1], results[1][1]])
+ finished.addCallback(cbFinished)
+
+ port = reactor.listenTCP(0, serverFactory, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+
+ connector = reactor.connectTCP(
+ port.getHost().host, port.getHost().port, clientFactory)
+ self.addCleanup(connector.disconnect)
+
+ finished.addCallback(lambda ign: reactor.stop())
+ self.runReactor(reactor)
+ lostConnectionResults[0].trap(ConnectionClosed)
+ lostConnectionResults[1].trap(ConnectionClosed)
+
+
+
+class TLSPortTestsBuilder(TLSMixin, ContextGeneratingMixin,
+ ObjectModelIntegrationMixin, BadContextTestsMixin,
+ StreamTransportTestsMixin, ReactorBuilder):
+ """
+ Tests for L{IReactorSSL.listenSSL}
+ """
+ def getListeningPort(self, reactor, factory):
+ """
+ Get a TLS port from a reactor.
+ """
+ return reactor.listenSSL(0, factory, self.getServerContext())
+
+
+ def getExpectedStartListeningLogMessage(self, port, factory):
+ """
+ Get the message expected to be logged when a TLS port starts listening.
+ """
+ return "%s (TLS) starting on %d" % (factory, port.getHost().port)
+
+
+ def getExpectedConnectionLostLogMsg(self, port):
+ """
+ Get the expected connection lost message for a TLS port.
+ """
+ return "(TLS Port %s Closed)" % (port.getHost().port,)
+
+
+ def test_badContext(self):
+ """
+ If the context factory passed to L{IReactorSSL.listenSSL} raises an
+ exception from its C{getContext} method, that exception is raised by
+ L{IReactorSSL.listenSSL}.
+ """
+ def useIt(reactor, contextFactory):
+ return reactor.listenSSL(0, ServerFactory(), contextFactory)
+ self._testBadContext(useIt)
+
+
+
+globals().update(SSLClientTestsMixin.makeTestCaseClasses())
+globals().update(StartTLSClientTestsMixin.makeTestCaseClasses())
+globals().update(TLSPortTestsBuilder().makeTestCaseClasses())
+
+
+
+class AbortSSLConnectionTest(ReactorBuilder, AbortConnectionMixin, ContextGeneratingMixin):
+ """
+ C{abortConnection} tests using SSL.
+ """
+ requiredInterfaces = (IReactorSSL,)
+ endpoints = SSLCreator()
+
+ def buildReactor(self):
+ reactor = ReactorBuilder.buildReactor(self)
+ try:
+ from twisted.protocols import tls
+ except ImportError:
+ return reactor
+
+ # Patch twisted.protocols.tls to use this reactor, until we get
+ # around to fixing #5206, or the TLS code uses an explicit reactor:
+ cooperator = Cooperator(
+ scheduler=lambda x: reactor.callLater(0.00001, x))
+ self.patch(tls, "cooperate", cooperator.cooperate)
+ return reactor
+
+
+ def setUp(self):
+ if FILETYPE_PEM is None:
+ raise SkipTest("OpenSSL not available.")
+
+globals().update(AbortSSLConnectionTest.makeTestCaseClasses())
+
+class OldTLSDeprecationTest(TestCase):
+ """
+ Tests for the deprecation of L{twisted.internet._oldtls}, the implementation
+ module for L{IReactorSSL} used when only an old version of pyOpenSSL is
+ available.
+ """
+ def test_warning(self):
+ """
+ The use of L{twisted.internet._oldtls} is deprecated, and emits a
+ L{DeprecationWarning}.
+ """
+ # Since _oldtls depends on OpenSSL, just skip this test if it isn't
+ # installed on the system. Faking it would be error prone.
+ try:
+ import OpenSSL
+ except ImportError:
+ raise SkipTest("OpenSSL not available.")
+
+ # Change the apparent version of OpenSSL to one support for which is
+ # deprecated. And have it change back again after the test.
+ self.patch(OpenSSL, '__version__', '0.5')
+
+ # If the module was already imported, the import statement below won't
+ # execute its top-level code. Take it out of sys.modules so the import
+ # system re-evaluates it. Arrange to put the original back afterwards.
+ # Also handle the case where it hasn't yet been imported.
+ try:
+ oldtls = sys.modules['twisted.internet._oldtls']
+ except KeyError:
+ self.addCleanup(sys.modules.pop, 'twisted.internet._oldtls')
+ else:
+ del sys.modules['twisted.internet._oldtls']
+ self.addCleanup(
+ operator.setitem, sys.modules, 'twisted.internet._oldtls',
+ oldtls)
+
+ # The actual test.
+ import twisted.internet._oldtls
+ warnings = self.flushWarnings()
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Support for pyOpenSSL 0.5 is deprecated. "
+ "Upgrade to pyOpenSSL 0.10 or newer.")
diff --git a/twisted/internet/test/test_udp.py b/twisted/internet/test/test_udp.py
new file mode 100644
index 0000000..299f3f8
--- /dev/null
+++ b/twisted/internet/test/test_udp.py
@@ -0,0 +1,194 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorUDP}.
+"""
+
+__metaclass__ = type
+
+from socket import SOCK_DGRAM
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.python import context
+from twisted.python.log import ILogContext, err
+from twisted.internet.test.reactormixins import ReactorBuilder
+from twisted.internet.defer import Deferred, maybeDeferred
+from twisted.internet.interfaces import ILoggingContext, IListeningPort
+from twisted.internet.address import IPv4Address
+from twisted.internet.protocol import DatagramProtocol
+
+from twisted.internet.test.test_tcp import findFreePort
+from twisted.internet.test.connectionmixins import LogObserverMixin
+
+
+class UDPPortMixin(object):
+ def getListeningPort(self, reactor, protocol):
+ """
+ Get a UDP port from a reactor.
+ """
+ return reactor.listenUDP(0, protocol)
+
+
+ def getExpectedStartListeningLogMessage(self, port, protocol):
+ """
+ Get the message expected to be logged when a UDP port starts listening.
+ """
+ return "%s starting on %d" % (protocol, port.getHost().port)
+
+
+ def getExpectedConnectionLostLogMessage(self, port):
+ """
+ Get the expected connection lost message for a UDP port.
+ """
+ return "(UDP Port %s Closed)" % (port.getHost().port,)
+
+
+
+class DatagramTransportTestsMixin(LogObserverMixin):
+ """
+ Mixin defining tests which apply to any port/datagram based transport.
+ """
+ def test_startedListeningLogMessage(self):
+ """
+ When a port starts, a message including a description of the associated
+ protocol is logged.
+ """
+ loggedMessages = self.observe()
+ reactor = self.buildReactor()
+ class SomeProtocol(DatagramProtocol):
+ implements(ILoggingContext)
+ def logPrefix(self):
+ return "Crazy Protocol"
+ protocol = SomeProtocol()
+ p = self.getListeningPort(reactor, protocol)
+ expectedMessage = self.getExpectedStartListeningLogMessage(
+ p, "Crazy Protocol")
+ self.assertEqual((expectedMessage,), loggedMessages[0]['message'])
+
+
+ def test_connectionLostLogMessage(self):
+ """
+ When a connection is lost, an informative message should be logged (see
+ L{getExpectedConnectionLostLogMessage}): an address identifying the port
+ and the fact that it was closed.
+ """
+ loggedMessages = self.observe()
+ reactor = self.buildReactor()
+ p = self.getListeningPort(reactor, DatagramProtocol())
+ expectedMessage = self.getExpectedConnectionLostLogMessage(p)
+
+ def stopReactor(ignored):
+ reactor.stop()
+
+ def doStopListening():
+ del loggedMessages[:]
+ maybeDeferred(p.stopListening).addCallback(stopReactor)
+
+ reactor.callWhenRunning(doStopListening)
+ self.runReactor(reactor)
+
+ self.assertEqual((expectedMessage,), loggedMessages[0]['message'])
+
+
+ def test_stopProtocolScheduling(self):
+ """
+ L{DatagramProtocol.stopProtocol} is called asynchronously (ie, not
+ re-entrantly) when C{stopListening} is used to stop the the datagram
+ transport.
+ """
+ class DisconnectingProtocol(DatagramProtocol):
+
+ started = False
+ stopped = False
+ inStartProtocol = False
+ stoppedInStart = False
+
+ def startProtocol(self):
+ self.started = True
+ self.inStartProtocol = True
+ self.transport.stopListening()
+ self.inStartProtocol = False
+
+ def stopProtocol(self):
+ self.stopped = True
+ self.stoppedInStart = self.inStartProtocol
+ reactor.stop()
+
+ reactor = self.buildReactor()
+ protocol = DisconnectingProtocol()
+ self.getListeningPort(reactor, protocol)
+ self.runReactor(reactor)
+
+ self.assertTrue(protocol.started)
+ self.assertTrue(protocol.stopped)
+ self.assertFalse(protocol.stoppedInStart)
+
+
+
+class UDPServerTestsBuilder(ReactorBuilder, UDPPortMixin,
+ DatagramTransportTestsMixin):
+ """
+ Builder defining tests relating to L{IReactorUDP.listenUDP}.
+ """
+ def test_interface(self):
+ """
+ L{IReactorUDP.listenUDP} returns an object providing L{IListeningPort}.
+ """
+ reactor = self.buildReactor()
+ port = reactor.listenUDP(0, DatagramProtocol())
+ self.assertTrue(verifyObject(IListeningPort, port))
+
+
+ def test_getHost(self):
+ """
+ L{IListeningPort.getHost} returns an L{IPv4Address} giving a
+ dotted-quad of the IPv4 address the port is listening on as well as
+ the port number.
+ """
+ host, portNumber = findFreePort(type=SOCK_DGRAM)
+ reactor = self.buildReactor()
+ port = reactor.listenUDP(
+ portNumber, DatagramProtocol(), interface=host)
+ self.assertEqual(
+ port.getHost(), IPv4Address('UDP', host, portNumber))
+
+
+ def test_logPrefix(self):
+ """
+ Datagram transports implement L{ILoggingContext.logPrefix} to return a
+ message reflecting the protocol they are running.
+ """
+ class CustomLogPrefixDatagramProtocol(DatagramProtocol):
+ def __init__(self, prefix):
+ self._prefix = prefix
+ self.system = Deferred()
+
+ def logPrefix(self):
+ return self._prefix
+
+ def datagramReceived(self, bytes, addr):
+ if self.system is not None:
+ system = self.system
+ self.system = None
+ system.callback(context.get(ILogContext)["system"])
+
+ reactor = self.buildReactor()
+ protocol = CustomLogPrefixDatagramProtocol("Custom Datagrams")
+ d = protocol.system
+ port = reactor.listenUDP(0, protocol)
+ address = port.getHost()
+
+ def gotSystem(system):
+ self.assertEqual("Custom Datagrams (UDP)", system)
+ d.addCallback(gotSystem)
+ d.addErrback(err)
+ d.addCallback(lambda ignored: reactor.stop())
+
+ port.write("some bytes", ('127.0.0.1', address.port))
+ self.runReactor(reactor)
+
+
+globals().update(UDPServerTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_udp_internals.py b/twisted/internet/test/test_udp_internals.py
new file mode 100644
index 0000000..75dc1e2
--- /dev/null
+++ b/twisted/internet/test/test_udp_internals.py
@@ -0,0 +1,165 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the internal implementation details of L{twisted.internet.udp}.
+"""
+
+import socket
+
+from twisted.trial import unittest
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import udp
+from twisted.python.runtime import platformType
+
+if platformType == 'win32':
+ from errno import WSAEWOULDBLOCK as EWOULDBLOCK
+ from errno import WSAECONNREFUSED as ECONNREFUSED
+else:
+ from errno import EWOULDBLOCK
+ from errno import ECONNREFUSED
+
+
+
+class StringUDPSocket(object):
+ """
+ A fake UDP socket object, which returns a fixed sequence of strings and/or
+ socket errors. Useful for testing.
+
+ @ivar retvals: A C{list} containing either strings or C{socket.error}s.
+
+ @ivar connectedAddr: The address the socket is connected to.
+ """
+
+ def __init__(self, retvals):
+ self.retvals = retvals
+ self.connectedAddr = None
+
+
+ def connect(self, addr):
+ self.connectedAddr = addr
+
+
+ def recvfrom(self, size):
+ """
+ Return (or raise) the next value from C{self.retvals}.
+ """
+ ret = self.retvals.pop(0)
+ if isinstance(ret, socket.error):
+ raise ret
+ return ret, None
+
+
+
+class KeepReads(DatagramProtocol):
+ """
+ Accumulate reads in a list.
+ """
+
+ def __init__(self):
+ self.reads = []
+
+
+ def datagramReceived(self, data, addr):
+ self.reads.append(data)
+
+
+
+class ErrorsTestCase(unittest.TestCase):
+ """
+ Error handling tests for C{udp.Port}.
+ """
+
+ def test_socketReadNormal(self):
+ """
+ Socket reads with some good data followed by a socket error which can
+ be ignored causes reading to stop, and no log messages to be logged.
+ """
+ # Add a fake error to the list of ignorables:
+ udp._sockErrReadIgnore.append(-7000)
+ self.addCleanup(udp._sockErrReadIgnore.remove, -7000)
+
+ protocol = KeepReads()
+ port = udp.Port(None, protocol)
+
+ # Normal result, no errors
+ port.socket = StringUDPSocket(
+ ["result", "123", socket.error(-7000), "456",
+ socket.error(-7000)])
+ port.doRead()
+ # Read stops on error:
+ self.assertEqual(protocol.reads, ["result", "123"])
+ port.doRead()
+ self.assertEqual(protocol.reads, ["result", "123", "456"])
+
+
+ def test_readImmediateError(self):
+ """
+ If the socket is unconnected, socket reads with an immediate
+ connection refusal are ignored, and reading stops. The protocol's
+ C{connectionRefused} method is not called.
+ """
+ # Add a fake error to the list of those that count as connection
+ # refused:
+ udp._sockErrReadRefuse.append(-6000)
+ self.addCleanup(udp._sockErrReadRefuse.remove, -6000)
+
+ protocol = KeepReads()
+ # Fail if connectionRefused is called:
+ protocol.connectionRefused = lambda: 1/0
+
+ port = udp.Port(None, protocol)
+
+ # Try an immediate "connection refused"
+ port.socket = StringUDPSocket(["a", socket.error(-6000), "b",
+ socket.error(EWOULDBLOCK)])
+ port.doRead()
+ # Read stops on error:
+ self.assertEqual(protocol.reads, ["a"])
+ # Read again:
+ port.doRead()
+ self.assertEqual(protocol.reads, ["a", "b"])
+
+
+ def test_connectedReadImmediateError(self):
+ """
+ If the socket connected, socket reads with an immediate
+ connection refusal are ignored, and reading stops. The protocol's
+ C{connectionRefused} method is called.
+ """
+ # Add a fake error to the list of those that count as connection
+ # refused:
+ udp._sockErrReadRefuse.append(-6000)
+ self.addCleanup(udp._sockErrReadRefuse.remove, -6000)
+
+ protocol = KeepReads()
+ refused = []
+ protocol.connectionRefused = lambda: refused.append(True)
+
+ port = udp.Port(None, protocol)
+ port.socket = StringUDPSocket(["a", socket.error(-6000), "b",
+ socket.error(EWOULDBLOCK)])
+ port.connect("127.0.0.1", 9999)
+
+ # Read stops on error:
+ port.doRead()
+ self.assertEqual(protocol.reads, ["a"])
+ self.assertEqual(refused, [True])
+
+ # Read again:
+ port.doRead()
+ self.assertEqual(protocol.reads, ["a", "b"])
+ self.assertEqual(refused, [True])
+
+
+ def test_readUnknownError(self):
+ """
+ Socket reads with an unknown socket error are raised.
+ """
+ protocol = KeepReads()
+ port = udp.Port(None, protocol)
+
+ # Some good data, followed by an unknown error
+ port.socket = StringUDPSocket(["good", socket.error(-1337)])
+ self.assertRaises(socket.error, port.doRead)
+ self.assertEqual(protocol.reads, ["good"])
diff --git a/twisted/internet/test/test_unix.py b/twisted/internet/test/test_unix.py
new file mode 100644
index 0000000..3a57028
--- /dev/null
+++ b/twisted/internet/test/test_unix.py
@@ -0,0 +1,554 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorUNIX}.
+"""
+
+from stat import S_IMODE
+from os import stat, close
+from tempfile import mktemp
+from socket import AF_INET, SOCK_STREAM, socket
+from pprint import pformat
+
+try:
+ from socket import AF_UNIX
+except ImportError:
+ AF_UNIX = None
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.python.log import addObserver, removeObserver, err
+from twisted.python.failure import Failure
+from twisted.python.hashlib import md5
+from twisted.python.runtime import platform
+from twisted.internet.interfaces import IConnector, IFileDescriptorReceiver
+from twisted.internet.error import ConnectionClosed, FileDescriptorOverrun
+from twisted.internet.address import UNIXAddress
+from twisted.internet.endpoints import UNIXServerEndpoint, UNIXClientEndpoint
+from twisted.internet.defer import Deferred, fail
+from twisted.internet.task import LoopingCall
+from twisted.internet import interfaces
+from twisted.internet.protocol import (
+ ServerFactory, ClientFactory, DatagramProtocol)
+from twisted.internet.test.reactormixins import ReactorBuilder, EndpointCreator
+from twisted.internet.test.test_core import ObjectModelIntegrationMixin
+from twisted.internet.test.test_tcp import StreamTransportTestsMixin
+from twisted.internet.test.reactormixins import (
+ ConnectableProtocol, runProtocolsWithReactor)
+from twisted.internet.test.connectionmixins import ConnectionTestsMixin
+
+try:
+ from twisted.python import sendmsg
+except ImportError:
+ sendmsgSkip = (
+ "sendmsg extension unavailable, extended UNIX features disabled")
+else:
+ sendmsgSkip = None
+
+
+class UNIXFamilyMixin:
+ """
+ Test-helper defining mixin for things related to AF_UNIX sockets.
+ """
+ if AF_UNIX is None:
+ skip = "Platform does not support AF_UNIX sockets"
+
+ def _modeTest(self, methodName, path, factory):
+ """
+ Assert that the mode of the created unix socket is set to the mode
+ specified to the reactor method.
+ """
+ mode = 0600
+ reactor = self.buildReactor()
+ unixPort = getattr(reactor, methodName)(path, factory, mode=mode)
+ unixPort.stopListening()
+ self.assertEqual(S_IMODE(stat(path).st_mode), mode)
+
+
+def _abstractPath(case):
+ """
+ Return a new, unique abstract namespace path to be listened on.
+ """
+ # Use the test cases's mktemp to get something unique, but also squash it
+ # down to make sure it fits in the unix socket path limit (something around
+ # 110 bytes).
+ return md5(case.mktemp()).hexdigest()
+
+
+
+class UNIXCreator(EndpointCreator):
+ """
+ Create UNIX socket end points.
+ """
+ def server(self, reactor):
+ """
+ Construct a UNIX server endpoint.
+ """
+ # self.mktemp() often returns a path which is too long to be used.
+ path = mktemp(suffix='.sock', dir='.')
+ return UNIXServerEndpoint(reactor, path)
+
+
+ def client(self, reactor, serverAddress):
+ """
+ Construct a UNIX client endpoint.
+ """
+ return UNIXClientEndpoint(reactor, serverAddress.name)
+
+
+
+class SendFileDescriptor(ConnectableProtocol):
+ """
+ L{SendFileDescriptorAndBytes} sends a file descriptor and optionally some
+ normal bytes and then closes its connection.
+
+ @ivar reason: The reason the connection was lost, after C{connectionLost}
+ is called.
+ """
+ reason = None
+
+ def __init__(self, fd, data):
+ """
+ @param fd: A C{int} giving a file descriptor to send over the
+ connection.
+
+ @param data: A C{str} giving data to send over the connection, or
+ C{None} if no data is to be sent.
+ """
+ self.fd = fd
+ self.data = data
+
+
+ def connectionMade(self):
+ """
+ Send C{self.fd} and, if it is not C{None}, C{self.data}. Then close the
+ connection.
+ """
+ self.transport.sendFileDescriptor(self.fd)
+ if self.data:
+ self.transport.write(self.data)
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ ConnectableProtocol.connectionLost(self, reason)
+ self.reason = reason
+
+
+
+class ReceiveFileDescriptor(ConnectableProtocol):
+ """
+ L{ReceiveFileDescriptor} provides an API for waiting for file descriptors to
+ be received.
+
+ @ivar reason: The reason the connection was lost, after C{connectionLost}
+ is called.
+
+ @ivar waiting: A L{Deferred} which fires with a file descriptor once one is
+ received, or with a failure if the connection is lost with no descriptor
+ arriving.
+ """
+ implements(IFileDescriptorReceiver)
+
+ reason = None
+ waiting = None
+
+ def waitForDescriptor(self):
+ """
+ Return a L{Deferred} which will fire with the next file descriptor
+ received, or with a failure if the connection is or has already been
+ lost.
+ """
+ if self.reason is None:
+ self.waiting = Deferred()
+ return self.waiting
+ else:
+ return fail(self.reason)
+
+
+ def fileDescriptorReceived(self, descriptor):
+ """
+ Fire the waiting Deferred, initialized by C{waitForDescriptor}, with the
+ file descriptor just received.
+ """
+ self.waiting.callback(descriptor)
+ self.waiting = None
+
+
+ def dataReceived(self, data):
+ """
+ Fail the waiting Deferred, if it has not already been fired by
+ C{fileDescriptorReceived}. The bytes sent along with a file descriptor
+ are guaranteed to be delivered to the protocol's C{dataReceived} method
+ only after the file descriptor has been delivered to the protocol's
+ C{fileDescriptorReceived}.
+ """
+ if self.waiting is not None:
+ self.waiting.errback(Failure(Exception(
+ "Received bytes (%r) before descriptor." % (data,))))
+ self.waiting = None
+
+
+ def connectionLost(self, reason):
+ """
+ Fail the waiting Deferred, initialized by C{waitForDescriptor}, if there
+ is one.
+ """
+ ConnectableProtocol.connectionLost(self, reason)
+ if self.waiting is not None:
+ self.waiting.errback(reason)
+ self.waiting = None
+ self.reason = reason
+
+
+
+class UNIXTestsBuilder(UNIXFamilyMixin, ReactorBuilder, ConnectionTestsMixin):
+ """
+ Builder defining tests relating to L{IReactorUNIX}.
+ """
+ endpoints = UNIXCreator()
+
+ def test_interface(self):
+ """
+ L{IReactorUNIX.connectUNIX} returns an object providing L{IConnector}.
+ """
+ reactor = self.buildReactor()
+ connector = reactor.connectUNIX(self.mktemp(), ClientFactory())
+ self.assertTrue(verifyObject(IConnector, connector))
+
+
+ def test_mode(self):
+ """
+ The UNIX socket created by L{IReactorUNIX.listenUNIX} is created with
+ the mode specified.
+ """
+ self._modeTest('listenUNIX', self.mktemp(), ServerFactory())
+
+
+ def test_listenOnLinuxAbstractNamespace(self):
+ """
+ On Linux, a UNIX socket path may begin with C{'\0'} to indicate a socket
+ in the abstract namespace. L{IReactorUNIX.listenUNIX} accepts such a
+ path.
+ """
+ # Don't listen on a path longer than the maximum allowed.
+ path = _abstractPath(self)
+ reactor = self.buildReactor()
+ port = reactor.listenUNIX('\0' + path, ServerFactory())
+ self.assertEqual(port.getHost(), UNIXAddress('\0' + path))
+ if not platform.isLinux():
+ test_listenOnLinuxAbstractNamespace.skip = (
+ 'Abstract namespace UNIX sockets only supported on Linux.')
+
+
+ def test_connectToLinuxAbstractNamespace(self):
+ """
+ L{IReactorUNIX.connectUNIX} also accepts a Linux abstract namespace
+ path.
+ """
+ path = _abstractPath(self)
+ reactor = self.buildReactor()
+ connector = reactor.connectUNIX('\0' + path, ClientFactory())
+ self.assertEqual(
+ connector.getDestination(), UNIXAddress('\0' + path))
+ if not platform.isLinux():
+ test_connectToLinuxAbstractNamespace.skip = (
+ 'Abstract namespace UNIX sockets only supported on Linux.')
+
+
+ def test_addresses(self):
+ """
+ A client's transport's C{getHost} and C{getPeer} return L{UNIXAddress}
+ instances which have the filesystem path of the host and peer ends of
+ the connection.
+ """
+ class SaveAddress(ConnectableProtocol):
+ def makeConnection(self, transport):
+ self.addresses = dict(
+ host=transport.getHost(), peer=transport.getPeer())
+ transport.loseConnection()
+
+ server = SaveAddress()
+ client = SaveAddress()
+
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ self.assertEqual(server.addresses['host'], client.addresses['peer'])
+ self.assertEqual(server.addresses['peer'], client.addresses['host'])
+
+
+ def test_sendFileDescriptor(self):
+ """
+ L{IUNIXTransport.sendFileDescriptor} accepts an integer file descriptor
+ and sends a copy of it to the process reading from the connection.
+ """
+ from socket import fromfd
+
+ s = socket()
+ s.bind(('', 0))
+ server = SendFileDescriptor(s.fileno(), "junk")
+
+ client = ReceiveFileDescriptor()
+ d = client.waitForDescriptor()
+ def checkDescriptor(descriptor):
+ received = fromfd(descriptor, AF_INET, SOCK_STREAM)
+ # Thanks for the free dup, fromfd()
+ close(descriptor)
+
+ # If the sockets have the same local address, they're probably the
+ # same.
+ self.assertEqual(s.getsockname(), received.getsockname())
+
+ # But it would be cheating for them to be identified by the same
+ # file descriptor. The point was to get a copy, as we might get if
+ # there were two processes involved here.
+ self.assertNotEqual(s.fileno(), received.fileno())
+ d.addCallback(checkDescriptor)
+ d.addErrback(err, "Sending file descriptor encountered a problem")
+ d.addBoth(lambda ignored: server.transport.loseConnection())
+
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+ if sendmsgSkip is not None:
+ test_sendFileDescriptor.skip = sendmsgSkip
+
+
+ def test_sendFileDescriptorTriggersPauseProducing(self):
+ """
+ If a L{IUNIXTransport.sendFileDescriptor} call fills up the send buffer,
+ any registered producer is paused.
+ """
+ class DoesNotRead(ConnectableProtocol):
+ def connectionMade(self):
+ self.transport.pauseProducing()
+
+ class SendsManyFileDescriptors(ConnectableProtocol):
+ paused = False
+
+ def connectionMade(self):
+ self.socket = socket()
+ self.transport.registerProducer(self, True)
+ def sender():
+ self.transport.sendFileDescriptor(self.socket.fileno())
+ self.transport.write("x")
+ self.task = LoopingCall(sender)
+ self.task.clock = self.transport.reactor
+ self.task.start(0).addErrback(err, "Send loop failure")
+
+ def stopProducing(self):
+ self._disconnect()
+
+ def resumeProducing(self):
+ self._disconnect()
+
+ def pauseProducing(self):
+ self.paused = True
+ self.transport.unregisterProducer()
+ self._disconnect()
+
+ def _disconnect(self):
+ self.task.stop()
+ self.transport.abortConnection()
+ self.other.transport.abortConnection()
+
+ server = SendsManyFileDescriptors()
+ client = DoesNotRead()
+ server.other = client
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ self.assertTrue(
+ server.paused, "sendFileDescriptor producer was not paused")
+ if sendmsgSkip is not None:
+ test_sendFileDescriptorTriggersPauseProducing.skip = sendmsgSkip
+
+
+ def test_fileDescriptorOverrun(self):
+ """
+ If L{IUNIXTransport.sendFileDescriptor} is used to queue a greater
+ number of file descriptors than the number of bytes sent using
+ L{ITransport.write}, the connection is closed and the protocol connected
+ to the transport has its C{connectionLost} method called with a failure
+ wrapping L{FileDescriptorOverrun}.
+ """
+ cargo = socket()
+ server = SendFileDescriptor(cargo.fileno(), None)
+
+ client = ReceiveFileDescriptor()
+ d = self.assertFailure(
+ client.waitForDescriptor(), ConnectionClosed)
+ d.addErrback(
+ err, "Sending file descriptor encountered unexpected problem")
+ d.addBoth(lambda ignored: server.transport.loseConnection())
+
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ self.assertIsInstance(server.reason.value, FileDescriptorOverrun)
+ if sendmsgSkip is not None:
+ test_fileDescriptorOverrun.skip = sendmsgSkip
+
+
+ def test_avoidLeakingFileDescriptors(self):
+ """
+ If associated with a protocol which does not provide
+ L{IFileDescriptorReceiver}, file descriptors received by the
+ L{IUNIXTransport} implementation are closed and a warning is emitted.
+ """
+ # To verify this, establish a connection. Send one end of the
+ # connection over the IUNIXTransport implementation. After the copy
+ # should no longer exist, close the original. If the opposite end of
+ # the connection decides the connection is closed, the copy does not
+ # exist.
+ from socket import socketpair
+ probeClient, probeServer = socketpair()
+
+ events = []
+ addObserver(events.append)
+ self.addCleanup(removeObserver, events.append)
+
+ class RecordEndpointAddresses(SendFileDescriptor):
+ def connectionMade(self):
+ self.hostAddress = self.transport.getHost()
+ self.peerAddress = self.transport.getPeer()
+ SendFileDescriptor.connectionMade(self)
+
+ server = RecordEndpointAddresses(probeClient.fileno(), "junk")
+ client = ConnectableProtocol()
+
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ # Get rid of the original reference to the socket.
+ probeClient.close()
+
+ # A non-blocking recv will return "" if the connection is closed, as
+ # desired. If the connection has not been closed, because the duplicate
+ # file descriptor is still open, it will fail with EAGAIN instead.
+ probeServer.setblocking(False)
+ self.assertEqual("", probeServer.recv(1024))
+
+ # This is a surprising circumstance, so it should be logged.
+ format = (
+ "%(protocolName)s (on %(hostAddress)r) does not "
+ "provide IFileDescriptorReceiver; closing file "
+ "descriptor received (from %(peerAddress)r).")
+ clsName = "ConnectableProtocol"
+
+ # Reverse host and peer, since the log event is from the client
+ # perspective.
+ expectedEvent = dict(hostAddress=server.peerAddress,
+ peerAddress=server.hostAddress,
+ protocolName=clsName,
+ format=format)
+
+ for logEvent in events:
+ for k, v in expectedEvent.iteritems():
+ if v != logEvent.get(k):
+ break
+ else:
+ # No mismatches were found, stop looking at events
+ break
+ else:
+ # No fully matching events were found, fail the test.
+ self.fail(
+ "Expected event (%s) not found in logged events (%s)" % (
+ expectedEvent, pformat(events,)))
+ if sendmsgSkip is not None:
+ test_avoidLeakingFileDescriptors.skip = sendmsgSkip
+
+
+ def test_descriptorDeliveredBeforeBytes(self):
+ """
+ L{IUNIXTransport.sendFileDescriptor} sends file descriptors before
+ L{ITransport.write} sends normal bytes.
+ """
+ class RecordEvents(ConnectableProtocol):
+ implements(IFileDescriptorReceiver)
+
+ def connectionMade(self):
+ ConnectableProtocol.connectionMade(self)
+ self.events = []
+
+ def fileDescriptorReceived(innerSelf, descriptor):
+ self.addCleanup(close, descriptor)
+ innerSelf.events.append(type(descriptor))
+
+ def dataReceived(self, data):
+ self.events.extend(data)
+
+ cargo = socket()
+ server = SendFileDescriptor(cargo.fileno(), "junk")
+ client = RecordEvents()
+
+ runProtocolsWithReactor(self, server, client, self.endpoints)
+
+ self.assertEqual([int, "j", "u", "n", "k"], client.events)
+ if sendmsgSkip is not None:
+ test_descriptorDeliveredBeforeBytes.skip = sendmsgSkip
+
+
+
+class UNIXDatagramTestsBuilder(UNIXFamilyMixin, ReactorBuilder):
+ """
+ Builder defining tests relating to L{IReactorUNIXDatagram}.
+ """
+ # There's no corresponding test_connectMode because the mode parameter to
+ # connectUNIXDatagram has been completely ignored since that API was first
+ # introduced.
+ def test_listenMode(self):
+ """
+ The UNIX socket created by L{IReactorUNIXDatagram.listenUNIXDatagram}
+ is created with the mode specified.
+ """
+ self._modeTest('listenUNIXDatagram', self.mktemp(), DatagramProtocol())
+
+
+ def test_listenOnLinuxAbstractNamespace(self):
+ """
+ On Linux, a UNIX socket path may begin with C{'\0'} to indicate a socket
+ in the abstract namespace. L{IReactorUNIX.listenUNIXDatagram} accepts
+ such a path.
+ """
+ path = _abstractPath(self)
+ reactor = self.buildReactor()
+ port = reactor.listenUNIXDatagram('\0' + path, DatagramProtocol())
+ self.assertEqual(port.getHost(), UNIXAddress('\0' + path))
+ if not platform.isLinux():
+ test_listenOnLinuxAbstractNamespace.skip = (
+ 'Abstract namespace UNIX sockets only supported on Linux.')
+
+
+
+class UNIXPortTestsBuilder(ReactorBuilder, ObjectModelIntegrationMixin,
+ StreamTransportTestsMixin):
+ """
+ Tests for L{IReactorUNIX.listenUnix}
+ """
+ requiredInterfaces = [interfaces.IReactorUNIX]
+
+ def getListeningPort(self, reactor, factory):
+ """
+ Get a UNIX port from a reactor
+ """
+ # self.mktemp() often returns a path which is too long to be used.
+ path = mktemp(suffix='.sock', dir='.')
+ return reactor.listenUNIX(path, factory)
+
+
+ def getExpectedStartListeningLogMessage(self, port, factory):
+ """
+ Get the message expected to be logged when a UNIX port starts listening.
+ """
+ return "%s starting on %r" % (factory, port.getHost().name)
+
+
+ def getExpectedConnectionLostLogMsg(self, port):
+ """
+ Get the expected connection lost message for a UNIX port
+ """
+ return "(UNIX Port %s Closed)" % (repr(port.port),)
+
+
+
+globals().update(UNIXTestsBuilder.makeTestCaseClasses())
+globals().update(UNIXDatagramTestsBuilder.makeTestCaseClasses())
+globals().update(UNIXPortTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/test/test_win32events.py b/twisted/internet/test/test_win32events.py
new file mode 100644
index 0000000..b8ba59b
--- /dev/null
+++ b/twisted/internet/test/test_win32events.py
@@ -0,0 +1,183 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorWin32Events}.
+"""
+
+from thread import get_ident
+
+try:
+ import win32event
+except ImportError:
+ win32event = None
+
+from zope.interface.verify import verifyObject
+
+from twisted.python.threadable import isInIOThread
+from twisted.internet.interfaces import IReactorWin32Events
+from twisted.internet.defer import Deferred
+from twisted.internet.test.reactormixins import ReactorBuilder
+
+
+class Listener(object):
+ """
+ L{Listener} is an object that can be added to a L{IReactorWin32Events}
+ reactor to receive callback notification when a Windows event is set. It
+ records what thread its callback is invoked in and fires a Deferred.
+
+ @ivar success: A flag which is set to C{True} when the event callback is
+ called.
+
+ @ivar logThreadID: The id of the thread in which the C{logPrefix} method is
+ called.
+
+ @ivar eventThreadID: The id of the thread in which the event callback is
+ called.
+
+ @ivar connLostThreadID: The id of the thread in which the C{connectionLost}
+ method is called.
+
+ @ivar _finished: The L{Deferred} which will be fired when the event callback
+ is called.
+ """
+ success = False
+ logThreadID = eventThreadID = connLostThreadID = None
+
+ def __init__(self, finished):
+ self._finished = finished
+
+
+ def logPrefix(self):
+ self.logThreadID = get_ident()
+ return 'Listener'
+
+
+ def occurred(self):
+ self.success = True
+ self.eventThreadID = get_ident()
+ self._finished.callback(None)
+
+
+ def brokenOccurred(self):
+ raise RuntimeError("Some problem")
+
+
+ def returnValueOccurred(self):
+ return EnvironmentError("Entirely different problem")
+
+
+ def connectionLost(self, reason):
+ self.connLostThreadID = get_ident()
+ self._finished.errback(reason)
+
+
+
+class Win32EventsTestsBuilder(ReactorBuilder):
+ """
+ Builder defining tests relating to L{IReactorWin32Events}.
+ """
+ requiredInterfaces = [IReactorWin32Events]
+
+ def test_interface(self):
+ """
+ An instance of the reactor has all of the methods defined on
+ L{IReactorWin32Events}.
+ """
+ reactor = self.buildReactor()
+ verifyObject(IReactorWin32Events, reactor)
+
+
+ def test_addEvent(self):
+ """
+ When an event which has been added to the reactor is set, the action
+ associated with the event is invoked in the reactor thread.
+ """
+ reactorThreadID = get_ident()
+ reactor = self.buildReactor()
+ event = win32event.CreateEvent(None, False, False, None)
+ finished = Deferred()
+ finished.addCallback(lambda ignored: reactor.stop())
+ listener = Listener(finished)
+ reactor.addEvent(event, listener, 'occurred')
+ reactor.callWhenRunning(win32event.SetEvent, event)
+ self.runReactor(reactor)
+ self.assertTrue(listener.success)
+ self.assertEqual(reactorThreadID, listener.logThreadID)
+ self.assertEqual(reactorThreadID, listener.eventThreadID)
+
+
+ def test_ioThreadDoesNotChange(self):
+ """
+ Using L{IReactorWin32Events.addEvent} does not change which thread is
+ reported as the I/O thread.
+ """
+ results = []
+ def check(ignored):
+ results.append(isInIOThread())
+ reactor.stop()
+ reactor = self.buildReactor()
+ event = win32event.CreateEvent(None, False, False, None)
+ finished = Deferred()
+ listener = Listener(finished)
+ finished.addCallback(check)
+ reactor.addEvent(event, listener, 'occurred')
+ reactor.callWhenRunning(win32event.SetEvent, event)
+ self.runReactor(reactor)
+ self.assertTrue(listener.success)
+ self.assertEqual([True], results)
+
+
+ def test_disconnectedOnError(self):
+ """
+ If the event handler raises an exception, the event is removed from the
+ reactor and the handler's C{connectionLost} method is called in the I/O
+ thread and the exception is logged.
+ """
+ reactorThreadID = get_ident()
+ reactor = self.buildReactor()
+ event = win32event.CreateEvent(None, False, False, None)
+ finished = self.assertFailure(Deferred(), RuntimeError)
+ finished.addCallback(lambda ignored: reactor.stop())
+ listener = Listener(finished)
+ reactor.addEvent(event, listener, 'brokenOccurred')
+ reactor.callWhenRunning(win32event.SetEvent, event)
+ self.runReactor(reactor)
+ self.assertEqual(reactorThreadID, listener.connLostThreadID)
+ self.assertEqual(1, len(self.flushLoggedErrors(RuntimeError)))
+
+
+ def test_disconnectOnReturnValue(self):
+ """
+ If the event handler returns a value, the event is removed from the
+ reactor and the handler's C{connectionLost} method is called in the I/O
+ thread.
+ """
+ reactorThreadID = get_ident()
+ reactor = self.buildReactor()
+ event = win32event.CreateEvent(None, False, False, None)
+ finished = self.assertFailure(Deferred(), EnvironmentError)
+ finished.addCallback(lambda ignored: reactor.stop())
+ listener = Listener(finished)
+ reactor.addEvent(event, listener, 'returnValueOccurred')
+ reactor.callWhenRunning(win32event.SetEvent, event)
+ self.runReactor(reactor)
+ self.assertEqual(reactorThreadID, listener.connLostThreadID)
+
+
+ def test_notDisconnectedOnShutdown(self):
+ """
+ Event handlers added with L{IReactorWin32Events.addEvent} do not have
+ C{connectionLost} called on them if they are still active when the
+ reactor shuts down.
+ """
+ reactor = self.buildReactor()
+ event = win32event.CreateEvent(None, False, False, None)
+ finished = Deferred()
+ listener = Listener(finished)
+ reactor.addEvent(event, listener, 'occurred')
+ reactor.callWhenRunning(reactor.stop)
+ self.runReactor(reactor)
+ self.assertIdentical(None, listener.connLostThreadID)
+
+globals().update(Win32EventsTestsBuilder.makeTestCaseClasses())
diff --git a/twisted/internet/threads.py b/twisted/internet/threads.py
new file mode 100644
index 0000000..a69a347
--- /dev/null
+++ b/twisted/internet/threads.py
@@ -0,0 +1,123 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Extended thread dispatching support.
+
+For basic support see reactor threading API docs.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+import Queue
+
+from twisted.python import failure
+from twisted.internet import defer
+
+
+def deferToThreadPool(reactor, threadpool, f, *args, **kwargs):
+ """
+ Call the function C{f} using a thread from the given threadpool and return
+ the result as a Deferred.
+
+ This function is only used by client code which is maintaining its own
+ threadpool. To run a function in the reactor's threadpool, use
+ C{deferToThread}.
+
+ @param reactor: The reactor in whose main thread the Deferred will be
+ invoked.
+
+ @param threadpool: An object which supports the C{callInThreadWithCallback}
+ method of C{twisted.python.threadpool.ThreadPool}.
+
+ @param f: The function to call.
+ @param *args: positional arguments to pass to f.
+ @param **kwargs: keyword arguments to pass to f.
+
+ @return: A Deferred which fires a callback with the result of f, or an
+ errback with a L{twisted.python.failure.Failure} if f throws an
+ exception.
+ """
+ d = defer.Deferred()
+
+ def onResult(success, result):
+ if success:
+ reactor.callFromThread(d.callback, result)
+ else:
+ reactor.callFromThread(d.errback, result)
+
+ threadpool.callInThreadWithCallback(onResult, f, *args, **kwargs)
+
+ return d
+
+
+def deferToThread(f, *args, **kwargs):
+ """
+ Run a function in a thread and return the result as a Deferred.
+
+ @param f: The function to call.
+ @param *args: positional arguments to pass to f.
+ @param **kwargs: keyword arguments to pass to f.
+
+ @return: A Deferred which fires a callback with the result of f,
+ or an errback with a L{twisted.python.failure.Failure} if f throws
+ an exception.
+ """
+ from twisted.internet import reactor
+ return deferToThreadPool(reactor, reactor.getThreadPool(),
+ f, *args, **kwargs)
+
+
+def _runMultiple(tupleList):
+ """
+ Run a list of functions.
+ """
+ for f, args, kwargs in tupleList:
+ f(*args, **kwargs)
+
+
+def callMultipleInThread(tupleList):
+ """
+ Run a list of functions in the same thread.
+
+ tupleList should be a list of (function, argsList, kwargsDict) tuples.
+ """
+ from twisted.internet import reactor
+ reactor.callInThread(_runMultiple, tupleList)
+
+
+def blockingCallFromThread(reactor, f, *a, **kw):
+ """
+ Run a function in the reactor from a thread, and wait for the result
+ synchronously. If the function returns a L{Deferred}, wait for its
+ result and return that.
+
+ @param reactor: The L{IReactorThreads} provider which will be used to
+ schedule the function call.
+ @param f: the callable to run in the reactor thread
+ @type f: any callable.
+ @param a: the arguments to pass to C{f}.
+ @param kw: the keyword arguments to pass to C{f}.
+
+ @return: the result of the L{Deferred} returned by C{f}, or the result
+ of C{f} if it returns anything other than a L{Deferred}.
+
+ @raise: If C{f} raises a synchronous exception,
+ C{blockingCallFromThread} will raise that exception. If C{f}
+ returns a L{Deferred} which fires with a L{Failure},
+ C{blockingCallFromThread} will raise that failure's exception (see
+ L{Failure.raiseException}).
+ """
+ queue = Queue.Queue()
+ def _callFromThread():
+ result = defer.maybeDeferred(f, *a, **kw)
+ result.addBoth(queue.put)
+ reactor.callFromThread(_callFromThread)
+ result = queue.get()
+ if isinstance(result, failure.Failure):
+ result.raiseException()
+ return result
+
+
+__all__ = ["deferToThread", "deferToThreadPool", "callMultipleInThread",
+ "blockingCallFromThread"]
diff --git a/twisted/internet/tksupport.py b/twisted/internet/tksupport.py
new file mode 100644
index 0000000..ddec55e
--- /dev/null
+++ b/twisted/internet/tksupport.py
@@ -0,0 +1,75 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+This module integrates Tkinter with twisted.internet's mainloop.
+
+Maintainer: Itamar Shtull-Trauring
+
+To use, do::
+
+ | tksupport.install(rootWidget)
+
+and then run your reactor as usual - do *not* call Tk's mainloop(),
+use Twisted's regular mechanism for running the event loop.
+
+Likewise, to stop your program you will need to stop Twisted's
+event loop. For example, if you want closing your root widget to
+stop Twisted::
+
+ | root.protocol('WM_DELETE_WINDOW', reactor.stop)
+
+When using Aqua Tcl/Tk on Mac OS X the standard Quit menu item in
+your application might become unresponsive without the additional
+fix::
+
+ | root.createcommand("::tk::mac::Quit", reactor.stop)
+
+@see: U{Tcl/TkAqua FAQ for more info<http://wiki.tcl.tk/12987>}
+"""
+
+# system imports
+import Tkinter, tkSimpleDialog, tkMessageBox
+
+# twisted imports
+from twisted.python import log
+from twisted.internet import task
+
+
+_task = None
+
+def install(widget, ms=10, reactor=None):
+ """Install a Tkinter.Tk() object into the reactor."""
+ installTkFunctions()
+ global _task
+ _task = task.LoopingCall(widget.update)
+ _task.start(ms / 1000.0, False)
+
+def uninstall():
+ """Remove the root Tk widget from the reactor.
+
+ Call this before destroy()ing the root widget.
+ """
+ global _task
+ _task.stop()
+ _task = None
+
+
+def installTkFunctions():
+ import twisted.python.util
+ twisted.python.util.getPassword = getPassword
+
+
+def getPassword(prompt = '', confirm = 0):
+ while 1:
+ try1 = tkSimpleDialog.askstring('Password Dialog', prompt, show='*')
+ if not confirm:
+ return try1
+ try2 = tkSimpleDialog.askstring('Password Dialog', 'Confirm Password', show='*')
+ if try1 == try2:
+ return try1
+ else:
+ tkMessageBox.showerror('Password Mismatch', 'Passwords did not match, starting over')
+
+__all__ = ["install", "uninstall"]
diff --git a/twisted/internet/udp.py b/twisted/internet/udp.py
new file mode 100644
index 0000000..42bd348
--- /dev/null
+++ b/twisted/internet/udp.py
@@ -0,0 +1,347 @@
+# -*- test-case-name: twisted.test.test_udp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Various asynchronous UDP classes.
+
+Please do not use this module directly.
+
+@var _sockErrReadIgnore: list of symbolic error constants (from the C{errno}
+ module) representing socket errors where the error is temporary and can be
+ ignored.
+
+@var _sockErrReadRefuse: list of symbolic error constants (from the C{errno}
+ module) representing socket errors that indicate connection refused.
+"""
+
+# System Imports
+import socket
+import operator
+import struct
+import warnings
+
+from zope.interface import implements
+
+from twisted.python.runtime import platformType
+if platformType == 'win32':
+ from errno import WSAEWOULDBLOCK
+ from errno import WSAEINTR, WSAEMSGSIZE, WSAETIMEDOUT
+ from errno import WSAECONNREFUSED, WSAECONNRESET, WSAENETRESET
+ from errno import WSAEINPROGRESS
+
+ # Classify read and write errors
+ _sockErrReadIgnore = [WSAEINTR, WSAEWOULDBLOCK, WSAEMSGSIZE, WSAEINPROGRESS]
+ _sockErrReadRefuse = [WSAECONNREFUSED, WSAECONNRESET, WSAENETRESET,
+ WSAETIMEDOUT]
+
+ # POSIX-compatible write errors
+ EMSGSIZE = WSAEMSGSIZE
+ ECONNREFUSED = WSAECONNREFUSED
+ EAGAIN = WSAEWOULDBLOCK
+ EINTR = WSAEINTR
+else:
+ from errno import EWOULDBLOCK, EINTR, EMSGSIZE, ECONNREFUSED, EAGAIN
+ _sockErrReadIgnore = [EAGAIN, EINTR, EWOULDBLOCK]
+ _sockErrReadRefuse = [ECONNREFUSED]
+
+# Twisted Imports
+from twisted.internet import base, defer, address
+from twisted.python import log, failure
+from twisted.internet import abstract, error, interfaces
+
+
+class Port(base.BasePort):
+ """
+ UDP port, listening for packets.
+ """
+ implements(
+ interfaces.IListeningPort, interfaces.IUDPTransport,
+ interfaces.ISystemHandle)
+
+ addressFamily = socket.AF_INET
+ socketType = socket.SOCK_DGRAM
+ maxThroughput = 256 * 1024 # max bytes we read in one eventloop iteration
+
+ # Actual port number being listened on, only set to a non-None
+ # value when we are actually listening.
+ _realPortNumber = None
+
+ def __init__(self, port, proto, interface='', maxPacketSize=8192, reactor=None):
+ """
+ Initialize with a numeric port to listen on.
+ """
+ base.BasePort.__init__(self, reactor)
+ self.port = port
+ self.protocol = proto
+ self.maxPacketSize = maxPacketSize
+ self.interface = interface
+ self.setLogStr()
+ self._connectedAddr = None
+
+ def __repr__(self):
+ if self._realPortNumber is not None:
+ return "<%s on %s>" % (self.protocol.__class__, self._realPortNumber)
+ else:
+ return "<%s not connected>" % (self.protocol.__class__,)
+
+ def getHandle(self):
+ """
+ Return a socket object.
+ """
+ return self.socket
+
+ def startListening(self):
+ """
+ Create and bind my socket, and begin listening on it.
+
+ This is called on unserialization, and must be called after creating a
+ server to begin listening on the specified port.
+ """
+ self._bindSocket()
+ self._connectToProtocol()
+
+ def _bindSocket(self):
+ try:
+ skt = self.createInternetSocket()
+ skt.bind((self.interface, self.port))
+ except socket.error, le:
+ raise error.CannotListenError, (self.interface, self.port, le)
+
+ # Make sure that if we listened on port 0, we update that to
+ # reflect what the OS actually assigned us.
+ self._realPortNumber = skt.getsockname()[1]
+
+ log.msg("%s starting on %s" % (
+ self._getLogPrefix(self.protocol), self._realPortNumber))
+
+ self.connected = 1
+ self.socket = skt
+ self.fileno = self.socket.fileno
+
+ def _connectToProtocol(self):
+ self.protocol.makeConnection(self)
+ self.startReading()
+
+
+ def doRead(self):
+ """
+ Called when my socket is ready for reading.
+ """
+ read = 0
+ while read < self.maxThroughput:
+ try:
+ data, addr = self.socket.recvfrom(self.maxPacketSize)
+ except socket.error, se:
+ no = se.args[0]
+ if no in _sockErrReadIgnore:
+ return
+ if no in _sockErrReadRefuse:
+ if self._connectedAddr:
+ self.protocol.connectionRefused()
+ return
+ raise
+ else:
+ read += len(data)
+ try:
+ self.protocol.datagramReceived(data, addr)
+ except:
+ log.err()
+
+
+ def write(self, datagram, addr=None):
+ """
+ Write a datagram.
+
+ @type datagram: C{str}
+ @param datagram: The datagram to be sent.
+
+ @type addr: C{tuple} containing C{str} as first element and C{int} as
+ second element, or C{None}
+ @param addr: A tuple of (I{stringified dotted-quad IP address},
+ I{integer port number}); can be C{None} in connected mode.
+ """
+ if self._connectedAddr:
+ assert addr in (None, self._connectedAddr)
+ try:
+ return self.socket.send(datagram)
+ except socket.error, se:
+ no = se.args[0]
+ if no == EINTR:
+ return self.write(datagram)
+ elif no == EMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no == ECONNREFUSED:
+ self.protocol.connectionRefused()
+ else:
+ raise
+ else:
+ assert addr != None
+ if not addr[0].replace(".", "").isdigit() and addr[0] != "<broadcast>":
+ warnings.warn("Please only pass IPs to write(), not hostnames",
+ DeprecationWarning, stacklevel=2)
+ try:
+ return self.socket.sendto(datagram, addr)
+ except socket.error, se:
+ no = se.args[0]
+ if no == EINTR:
+ return self.write(datagram, addr)
+ elif no == EMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no == ECONNREFUSED:
+ # in non-connected UDP ECONNREFUSED is platform dependent, I
+ # think and the info is not necessarily useful. Nevertheless
+ # maybe we should call connectionRefused? XXX
+ return
+ else:
+ raise
+
+ def writeSequence(self, seq, addr):
+ self.write("".join(seq), addr)
+
+ def connect(self, host, port):
+ """
+ 'Connect' to remote server.
+ """
+ if self._connectedAddr:
+ raise RuntimeError, "already connected, reconnecting is not currently supported (talk to itamar if you want this)"
+ if not abstract.isIPAddress(host):
+ raise ValueError, "please pass only IP addresses, not domain names"
+ self._connectedAddr = (host, port)
+ self.socket.connect((host, port))
+
+ def _loseConnection(self):
+ self.stopReading()
+ if self.connected: # actually means if we are *listening*
+ self.reactor.callLater(0, self.connectionLost)
+
+ def stopListening(self):
+ if self.connected:
+ result = self.d = defer.Deferred()
+ else:
+ result = None
+ self._loseConnection()
+ return result
+
+ def loseConnection(self):
+ warnings.warn("Please use stopListening() to disconnect port", DeprecationWarning, stacklevel=2)
+ self.stopListening()
+
+ def connectionLost(self, reason=None):
+ """
+ Cleans up my socket.
+ """
+ log.msg('(UDP Port %s Closed)' % self._realPortNumber)
+ self._realPortNumber = None
+ base.BasePort.connectionLost(self, reason)
+ self.protocol.doStop()
+ self.socket.close()
+ del self.socket
+ del self.fileno
+ if hasattr(self, "d"):
+ self.d.callback(None)
+ del self.d
+
+
+ def setLogStr(self):
+ """
+ Initialize the C{logstr} attribute to be used by C{logPrefix}.
+ """
+ logPrefix = self._getLogPrefix(self.protocol)
+ self.logstr = "%s (UDP)" % logPrefix
+
+
+ def logPrefix(self):
+ """
+ Return the prefix to log with.
+ """
+ return self.logstr
+
+
+ def getHost(self):
+ """
+ Returns an IPv4Address.
+
+ This indicates the address from which I am connecting.
+ """
+ return address.IPv4Address('UDP', *self.socket.getsockname())
+
+
+
+class MulticastMixin:
+ """
+ Implement multicast functionality.
+ """
+
+ def getOutgoingInterface(self):
+ i = self.socket.getsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF)
+ return socket.inet_ntoa(struct.pack("@i", i))
+
+ def setOutgoingInterface(self, addr):
+ """Returns Deferred of success."""
+ return self.reactor.resolve(addr).addCallback(self._setInterface)
+
+ def _setInterface(self, addr):
+ i = socket.inet_aton(addr)
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, i)
+ return 1
+
+ def getLoopbackMode(self):
+ return self.socket.getsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP)
+
+ def setLoopbackMode(self, mode):
+ mode = struct.pack("b", operator.truth(mode))
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, mode)
+
+ def getTTL(self):
+ return self.socket.getsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL)
+
+ def setTTL(self, ttl):
+ ttl = struct.pack("B", ttl)
+ self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
+
+ def joinGroup(self, addr, interface=""):
+ """Join a multicast group. Returns Deferred of success."""
+ return self.reactor.resolve(addr).addCallback(self._joinAddr1, interface, 1)
+
+ def _joinAddr1(self, addr, interface, join):
+ return self.reactor.resolve(interface).addCallback(self._joinAddr2, addr, join)
+
+ def _joinAddr2(self, interface, addr, join):
+ addr = socket.inet_aton(addr)
+ interface = socket.inet_aton(interface)
+ if join:
+ cmd = socket.IP_ADD_MEMBERSHIP
+ else:
+ cmd = socket.IP_DROP_MEMBERSHIP
+ try:
+ self.socket.setsockopt(socket.IPPROTO_IP, cmd, addr + interface)
+ except socket.error, e:
+ return failure.Failure(error.MulticastJoinError(addr, interface, *e.args))
+
+ def leaveGroup(self, addr, interface=""):
+ """Leave multicast group, return Deferred of success."""
+ return self.reactor.resolve(addr).addCallback(self._joinAddr1, interface, 0)
+
+
+class MulticastPort(MulticastMixin, Port):
+ """
+ UDP Port that supports multicasting.
+ """
+
+ implements(interfaces.IMulticastTransport)
+
+ def __init__(self, port, proto, interface='', maxPacketSize=8192, reactor=None, listenMultiple=False):
+ """
+ @see: L{twisted.internet.interfaces.IReactorMulticast.listenMulticast}
+ """
+ Port.__init__(self, port, proto, interface, maxPacketSize, reactor)
+ self.listenMultiple = listenMultiple
+
+ def createInternetSocket(self):
+ skt = Port.createInternetSocket(self)
+ if self.listenMultiple:
+ skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if hasattr(socket, "SO_REUSEPORT"):
+ skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+ return skt
diff --git a/twisted/internet/unix.py b/twisted/internet/unix.py
new file mode 100644
index 0000000..77b87cd
--- /dev/null
+++ b/twisted/internet/unix.py
@@ -0,0 +1,518 @@
+# -*- test-case-name: twisted.test.test_unix,twisted.internet.test.test_unix,twisted.internet.test.test_posixbase -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Various asynchronous TCP/IP classes.
+
+End users shouldn't use this module directly - use the reactor APIs instead.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+# System imports
+import os, sys, stat, socket, struct
+from errno import EINTR, EMSGSIZE, EAGAIN, EWOULDBLOCK, ECONNREFUSED, ENOBUFS
+
+from zope.interface import implements, implementsOnly, implementedBy
+
+if not hasattr(socket, 'AF_UNIX'):
+ raise ImportError("UNIX sockets not supported on this platform")
+
+# Twisted imports
+from twisted.internet import main, base, tcp, udp, error, interfaces, protocol, address
+from twisted.internet.error import CannotListenError
+from twisted.python.util import untilConcludes
+from twisted.python import lockfile, log, reflect, failure
+
+try:
+ from twisted.python import sendmsg
+except ImportError:
+ sendmsg = None
+
+
+def _ancillaryDescriptor(fd):
+ """
+ Pack an integer into an ancillary data structure suitable for use with
+ L{sendmsg.send1msg}.
+ """
+ packed = struct.pack("i", fd)
+ return [(socket.SOL_SOCKET, sendmsg.SCM_RIGHTS, packed)]
+
+
+
+class _SendmsgMixin(object):
+ """
+ Mixin for stream-oriented UNIX transports which uses sendmsg and recvmsg to
+ offer additional functionality, such as copying file descriptors into other
+ processes.
+
+ @ivar _writeSomeDataBase: The class which provides the basic implementation
+ of C{writeSomeData}. Ultimately this should be a subclass of
+ L{twisted.internet.abstract.FileDescriptor}. Subclasses which mix in
+ L{_SendmsgMixin} must define this.
+
+ @ivar _sendmsgQueue: A C{list} of C{int} holding file descriptors which are
+ currently buffered before being sent.
+
+ @ivar _fileDescriptorBufferSize: An C{int} giving the maximum number of file
+ descriptors to accept and queue for sending before pausing the
+ registered producer, if there is one.
+ """
+ implements(interfaces.IUNIXTransport)
+
+ _writeSomeDataBase = None
+ _fileDescriptorBufferSize = 64
+
+ def __init__(self):
+ self._sendmsgQueue = []
+
+
+ def _isSendBufferFull(self):
+ """
+ Determine whether the user-space send buffer for this transport is full
+ or not.
+
+ This extends the base determination by adding consideration of how many
+ file descriptors need to be sent using L{sendmsg.send1msg}. When there
+ are more than C{self._fileDescriptorBufferSize}, the buffer is
+ considered full.
+
+ @return: C{True} if it is full, C{False} otherwise.
+ """
+ # There must be some bytes in the normal send buffer, checked by
+ # _writeSomeDataBase._isSendBufferFull, in order to send file
+ # descriptors from _sendmsgQueue. That means that the buffer will
+ # eventually be considered full even without this additional logic.
+ # However, since we send only one byte per file descriptor, having lots
+ # of elements in _sendmsgQueue incurs more overhead and perhaps slows
+ # things down. Anyway, try this for now, maybe rethink it later.
+ return (
+ len(self._sendmsgQueue) > self._fileDescriptorBufferSize
+ or self._writeSomeDataBase._isSendBufferFull(self))
+
+
+ def sendFileDescriptor(self, fileno):
+ """
+ Queue the given file descriptor to be sent and start trying to send it.
+ """
+ self._sendmsgQueue.append(fileno)
+ self._maybePauseProducer()
+ self.startWriting()
+
+
+ def writeSomeData(self, data):
+ """
+ Send as much of C{data} as possible. Also send any pending file
+ descriptors.
+ """
+ # Make it a programming error to send more file descriptors than you
+ # send regular bytes. Otherwise, due to the limitation mentioned below,
+ # we could end up with file descriptors left, but no bytes to send with
+ # them, therefore no way to send those file descriptors.
+ if len(self._sendmsgQueue) > len(data):
+ return error.FileDescriptorOverrun()
+
+ # If there are file descriptors to send, try sending them first, using a
+ # little bit of data from the stream-oriented write buffer too. It is
+ # not possible to send a file descriptor without sending some regular
+ # data.
+ index = 0
+ try:
+ while index < len(self._sendmsgQueue):
+ fd = self._sendmsgQueue[index]
+ try:
+ untilConcludes(
+ sendmsg.send1msg, self.socket.fileno(), data[index], 0,
+ _ancillaryDescriptor(fd))
+ except socket.error, se:
+ if se.args[0] in (EWOULDBLOCK, ENOBUFS):
+ return index
+ else:
+ return main.CONNECTION_LOST
+ else:
+ index += 1
+ finally:
+ del self._sendmsgQueue[:index]
+
+ # Hand the remaining data to the base implementation. Avoid slicing in
+ # favor of a buffer, in case that happens to be any faster.
+ limitedData = buffer(data, index)
+ result = self._writeSomeDataBase.writeSomeData(self, limitedData)
+ try:
+ return index + result
+ except TypeError:
+ return result
+
+
+ def doRead(self):
+ """
+ Calls L{IFileDescriptorReceiver.fileDescriptorReceived} and
+ L{IProtocol.dataReceived} with all available data.
+
+ This reads up to C{self.bufferSize} bytes of data from its socket, then
+ dispatches the data to protocol callbacks to be handled. If the
+ connection is not lost through an error in the underlying recvmsg(),
+ this function will return the result of the dataReceived call.
+ """
+ try:
+ data, flags, ancillary = untilConcludes(
+ sendmsg.recv1msg, self.socket.fileno(), 0, self.bufferSize)
+ except socket.error, se:
+ if se.args[0] == EWOULDBLOCK:
+ return
+ else:
+ return main.CONNECTION_LOST
+
+ if ancillary:
+ fd = struct.unpack('i', ancillary[0][2])[0]
+ if interfaces.IFileDescriptorReceiver.providedBy(self.protocol):
+ self.protocol.fileDescriptorReceived(fd)
+ else:
+ log.msg(
+ format=(
+ "%(protocolName)s (on %(hostAddress)r) does not "
+ "provide IFileDescriptorReceiver; closing file "
+ "descriptor received (from %(peerAddress)r)."),
+ hostAddress=self.getHost(), peerAddress=self.getPeer(),
+ protocolName=self._getLogPrefix(self.protocol),
+ )
+ os.close(fd)
+
+ return self._dataReceived(data)
+
+if sendmsg is None:
+ class _SendmsgMixin(object):
+ """
+ Behaviorless placeholder used when L{twisted.python.sendmsg} is not
+ available, preventing L{IUNIXTransport} from being supported.
+ """
+
+
+
+class Server(_SendmsgMixin, tcp.Server):
+
+ _writeSomeDataBase = tcp.Server
+
+ def __init__(self, sock, protocol, client, server, sessionno, reactor):
+ _SendmsgMixin.__init__(self)
+ tcp.Server.__init__(self, sock, protocol, (client, None), server, sessionno, reactor)
+
+
+ def getHost(self):
+ return address.UNIXAddress(self.socket.getsockname())
+
+ def getPeer(self):
+ return address.UNIXAddress(self.hostname or None)
+
+
+
+def _inFilesystemNamespace(path):
+ """
+ Determine whether the given unix socket path is in a filesystem namespace.
+
+ While most PF_UNIX sockets are entries in the filesystem, Linux 2.2 and
+ above support PF_UNIX sockets in an "abstract namespace" that does not
+ correspond to any path. This function returns C{True} if the given socket
+ path is stored in the filesystem and C{False} if the path is in this
+ abstract namespace.
+ """
+ return path[:1] != "\0"
+
+
+class _UNIXPort(object):
+ def getHost(self):
+ """Returns a UNIXAddress.
+
+ This indicates the server's address.
+ """
+ if sys.version_info > (2, 5) or _inFilesystemNamespace(self.port):
+ path = self.socket.getsockname()
+ else:
+ # Abstract namespace sockets aren't well supported on Python 2.4.
+ # getsockname() always returns ''.
+ path = self.port
+ return address.UNIXAddress(path)
+
+
+
+class Port(_UNIXPort, tcp.Port):
+ addressFamily = socket.AF_UNIX
+ socketType = socket.SOCK_STREAM
+
+ transport = Server
+ lockFile = None
+
+ def __init__(self, fileName, factory, backlog=50, mode=0666, reactor=None, wantPID = 0):
+ tcp.Port.__init__(self, fileName, factory, backlog, reactor=reactor)
+ self.mode = mode
+ self.wantPID = wantPID
+
+ def __repr__(self):
+ factoryName = reflect.qual(self.factory.__class__)
+ if hasattr(self, 'socket'):
+ return '<%s on %r>' % (factoryName, self.port)
+ else:
+ return '<%s (not listening)>' % (factoryName,)
+
+ def _buildAddr(self, name):
+ return address.UNIXAddress(name)
+
+ def startListening(self):
+ """
+ Create and bind my socket, and begin listening on it.
+
+ This is called on unserialization, and must be called after creating a
+ server to begin listening on the specified port.
+ """
+ log.msg("%s starting on %r" % (
+ self._getLogPrefix(self.factory), self.port))
+ if self.wantPID:
+ self.lockFile = lockfile.FilesystemLock(self.port + ".lock")
+ if not self.lockFile.lock():
+ raise CannotListenError, (None, self.port, "Cannot acquire lock")
+ else:
+ if not self.lockFile.clean:
+ try:
+ # This is a best-attempt at cleaning up
+ # left-over unix sockets on the filesystem.
+ # If it fails, there's not much else we can
+ # do. The bind() below will fail with an
+ # exception that actually propagates.
+ if stat.S_ISSOCK(os.stat(self.port).st_mode):
+ os.remove(self.port)
+ except:
+ pass
+
+ self.factory.doStart()
+ try:
+ skt = self.createInternetSocket()
+ skt.bind(self.port)
+ except socket.error, le:
+ raise CannotListenError, (None, self.port, le)
+ else:
+ if _inFilesystemNamespace(self.port):
+ # Make the socket readable and writable to the world.
+ os.chmod(self.port, self.mode)
+ skt.listen(self.backlog)
+ self.connected = True
+ self.socket = skt
+ self.fileno = self.socket.fileno
+ self.numberAccepts = 100
+ self.startReading()
+
+
+ def _logConnectionLostMsg(self):
+ """
+ Log message for closing socket
+ """
+ log.msg('(UNIX Port %s Closed)' % (repr(self.port),))
+
+
+ def connectionLost(self, reason):
+ if _inFilesystemNamespace(self.port):
+ os.unlink(self.port)
+ if self.lockFile is not None:
+ self.lockFile.unlock()
+ tcp.Port.connectionLost(self, reason)
+
+
+
+class Client(_SendmsgMixin, tcp.BaseClient):
+ """A client for Unix sockets."""
+ addressFamily = socket.AF_UNIX
+ socketType = socket.SOCK_STREAM
+
+ _writeSomeDataBase = tcp.BaseClient
+
+ def __init__(self, filename, connector, reactor=None, checkPID = 0):
+ _SendmsgMixin.__init__(self)
+ self.connector = connector
+ self.realAddress = self.addr = filename
+ if checkPID and not lockfile.isLocked(filename + ".lock"):
+ self._finishInit(None, None, error.BadFileError(filename), reactor)
+ self._finishInit(self.doConnect, self.createInternetSocket(),
+ None, reactor)
+
+ def getPeer(self):
+ return address.UNIXAddress(self.addr)
+
+ def getHost(self):
+ return address.UNIXAddress(None)
+
+
+class Connector(base.BaseConnector):
+ def __init__(self, address, factory, timeout, reactor, checkPID):
+ base.BaseConnector.__init__(self, factory, timeout, reactor)
+ self.address = address
+ self.checkPID = checkPID
+
+ def _makeTransport(self):
+ return Client(self.address, self, self.reactor, self.checkPID)
+
+ def getDestination(self):
+ return address.UNIXAddress(self.address)
+
+
+class DatagramPort(_UNIXPort, udp.Port):
+ """Datagram UNIX port, listening for packets."""
+
+ implements(interfaces.IUNIXDatagramTransport)
+
+ addressFamily = socket.AF_UNIX
+
+ def __init__(self, addr, proto, maxPacketSize=8192, mode=0666, reactor=None):
+ """Initialize with address to listen on.
+ """
+ udp.Port.__init__(self, addr, proto, maxPacketSize=maxPacketSize, reactor=reactor)
+ self.mode = mode
+
+
+ def __repr__(self):
+ protocolName = reflect.qual(self.protocol.__class__,)
+ if hasattr(self, 'socket'):
+ return '<%s on %r>' % (protocolName, self.port)
+ else:
+ return '<%s (not listening)>' % (protocolName,)
+
+
+ def _bindSocket(self):
+ log.msg("%s starting on %s"%(self.protocol.__class__, repr(self.port)))
+ try:
+ skt = self.createInternetSocket() # XXX: haha misnamed method
+ if self.port:
+ skt.bind(self.port)
+ except socket.error, le:
+ raise error.CannotListenError, (None, self.port, le)
+ if self.port and _inFilesystemNamespace(self.port):
+ # Make the socket readable and writable to the world.
+ os.chmod(self.port, self.mode)
+ self.connected = 1
+ self.socket = skt
+ self.fileno = self.socket.fileno
+
+ def write(self, datagram, address):
+ """Write a datagram."""
+ try:
+ return self.socket.sendto(datagram, address)
+ except socket.error, se:
+ no = se.args[0]
+ if no == EINTR:
+ return self.write(datagram, address)
+ elif no == EMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no == EAGAIN:
+ # oh, well, drop the data. The only difference from UDP
+ # is that UDP won't ever notice.
+ # TODO: add TCP-like buffering
+ pass
+ else:
+ raise
+
+ def connectionLost(self, reason=None):
+ """Cleans up my socket.
+ """
+ log.msg('(Port %s Closed)' % repr(self.port))
+ base.BasePort.connectionLost(self, reason)
+ if hasattr(self, "protocol"):
+ # we won't have attribute in ConnectedPort, in cases
+ # where there was an error in connection process
+ self.protocol.doStop()
+ self.connected = 0
+ self.socket.close()
+ del self.socket
+ del self.fileno
+ if hasattr(self, "d"):
+ self.d.callback(None)
+ del self.d
+
+ def setLogStr(self):
+ self.logstr = reflect.qual(self.protocol.__class__) + " (UDP)"
+
+
+
+class ConnectedDatagramPort(DatagramPort):
+ """
+ A connected datagram UNIX socket.
+ """
+
+ implementsOnly(interfaces.IUNIXDatagramConnectedTransport,
+ *(implementedBy(base.BasePort)))
+
+ def __init__(self, addr, proto, maxPacketSize=8192, mode=0666,
+ bindAddress=None, reactor=None):
+ assert isinstance(proto, protocol.ConnectedDatagramProtocol)
+ DatagramPort.__init__(self, bindAddress, proto, maxPacketSize, mode,
+ reactor)
+ self.remoteaddr = addr
+
+
+ def startListening(self):
+ try:
+ self._bindSocket()
+ self.socket.connect(self.remoteaddr)
+ self._connectToProtocol()
+ except:
+ self.connectionFailed(failure.Failure())
+
+
+ def connectionFailed(self, reason):
+ """
+ Called when a connection fails. Stop listening on the socket.
+
+ @type reason: L{Failure}
+ @param reason: Why the connection failed.
+ """
+ self.stopListening()
+ self.protocol.connectionFailed(reason)
+ del self.protocol
+
+
+ def doRead(self):
+ """
+ Called when my socket is ready for reading.
+ """
+ read = 0
+ while read < self.maxThroughput:
+ try:
+ data, addr = self.socket.recvfrom(self.maxPacketSize)
+ read += len(data)
+ self.protocol.datagramReceived(data)
+ except socket.error, se:
+ no = se.args[0]
+ if no in (EAGAIN, EINTR, EWOULDBLOCK):
+ return
+ if no == ECONNREFUSED:
+ self.protocol.connectionRefused()
+ else:
+ raise
+ except:
+ log.deferr()
+
+
+ def write(self, data):
+ """
+ Write a datagram.
+ """
+ try:
+ return self.socket.send(data)
+ except socket.error, se:
+ no = se.args[0]
+ if no == EINTR:
+ return self.write(data)
+ elif no == EMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif no == ECONNREFUSED:
+ self.protocol.connectionRefused()
+ elif no == EAGAIN:
+ # oh, well, drop the data. The only difference from UDP
+ # is that UDP won't ever notice.
+ # TODO: add TCP-like buffering
+ pass
+ else:
+ raise
+
+
+ def getPeer(self):
+ return address.UNIXAddress(self.remoteaddr)
diff --git a/twisted/internet/utils.py b/twisted/internet/utils.py
new file mode 100644
index 0000000..8250833
--- /dev/null
+++ b/twisted/internet/utils.py
@@ -0,0 +1,219 @@
+# -*- test-case-name: twisted.test.test_iutils -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Utility methods.
+"""
+
+import sys, warnings
+
+from twisted.internet import protocol, defer
+from twisted.python import failure, util as tputil
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+def _callProtocolWithDeferred(protocol, executable, args, env, path, reactor=None):
+ if reactor is None:
+ from twisted.internet import reactor
+
+ d = defer.Deferred()
+ p = protocol(d)
+ reactor.spawnProcess(p, executable, (executable,)+tuple(args), env, path)
+ return d
+
+
+
+class _UnexpectedErrorOutput(IOError):
+ """
+ Standard error data was received where it was not expected. This is a
+ subclass of L{IOError} to preserve backward compatibility with the previous
+ error behavior of L{getProcessOutput}.
+
+ @ivar processEnded: A L{Deferred} which will fire when the process which
+ produced the data on stderr has ended (exited and all file descriptors
+ closed).
+ """
+ def __init__(self, text, processEnded):
+ IOError.__init__(self, "got stderr: %r" % (text,))
+ self.processEnded = processEnded
+
+
+
+class _BackRelay(protocol.ProcessProtocol):
+ """
+ Trivial protocol for communicating with a process and turning its output
+ into the result of a L{Deferred}.
+
+ @ivar deferred: A L{Deferred} which will be called back with all of stdout
+ and, if C{errortoo} is true, all of stderr as well (mixed together in
+ one string). If C{errortoo} is false and any bytes are received over
+ stderr, this will fire with an L{_UnexpectedErrorOutput} instance and
+ the attribute will be set to C{None}.
+
+ @ivar onProcessEnded: If C{errortoo} is false and bytes are received over
+ stderr, this attribute will refer to a L{Deferred} which will be called
+ back when the process ends. This C{Deferred} is also associated with
+ the L{_UnexpectedErrorOutput} which C{deferred} fires with earlier in
+ this case so that users can determine when the process has actually
+ ended, in addition to knowing when bytes have been received via stderr.
+ """
+
+ def __init__(self, deferred, errortoo=0):
+ self.deferred = deferred
+ self.s = StringIO.StringIO()
+ if errortoo:
+ self.errReceived = self.errReceivedIsGood
+ else:
+ self.errReceived = self.errReceivedIsBad
+
+ def errReceivedIsBad(self, text):
+ if self.deferred is not None:
+ self.onProcessEnded = defer.Deferred()
+ err = _UnexpectedErrorOutput(text, self.onProcessEnded)
+ self.deferred.errback(failure.Failure(err))
+ self.deferred = None
+ self.transport.loseConnection()
+
+ def errReceivedIsGood(self, text):
+ self.s.write(text)
+
+ def outReceived(self, text):
+ self.s.write(text)
+
+ def processEnded(self, reason):
+ if self.deferred is not None:
+ self.deferred.callback(self.s.getvalue())
+ elif self.onProcessEnded is not None:
+ self.onProcessEnded.errback(reason)
+
+
+
+def getProcessOutput(executable, args=(), env={}, path=None, reactor=None,
+ errortoo=0):
+ """
+ Spawn a process and return its output as a deferred returning a string.
+
+ @param executable: The file name to run and get the output of - the
+ full path should be used.
+
+ @param args: the command line arguments to pass to the process; a
+ sequence of strings. The first string should *NOT* be the
+ executable's name.
+
+ @param env: the environment variables to pass to the processs; a
+ dictionary of strings.
+
+ @param path: the path to run the subprocess in - defaults to the
+ current directory.
+
+ @param reactor: the reactor to use - defaults to the default reactor
+
+ @param errortoo: If true, include stderr in the result. If false, if
+ stderr is received the returned L{Deferred} will errback with an
+ L{IOError} instance with a C{processEnded} attribute. The
+ C{processEnded} attribute refers to a L{Deferred} which fires when the
+ executed process ends.
+ """
+ return _callProtocolWithDeferred(lambda d:
+ _BackRelay(d, errortoo=errortoo),
+ executable, args, env, path,
+ reactor)
+
+
+class _ValueGetter(protocol.ProcessProtocol):
+
+ def __init__(self, deferred):
+ self.deferred = deferred
+
+ def processEnded(self, reason):
+ self.deferred.callback(reason.value.exitCode)
+
+
+def getProcessValue(executable, args=(), env={}, path=None, reactor=None):
+ """Spawn a process and return its exit code as a Deferred."""
+ return _callProtocolWithDeferred(_ValueGetter, executable, args, env, path,
+ reactor)
+
+
+class _EverythingGetter(protocol.ProcessProtocol):
+
+ def __init__(self, deferred):
+ self.deferred = deferred
+ self.outBuf = StringIO.StringIO()
+ self.errBuf = StringIO.StringIO()
+ self.outReceived = self.outBuf.write
+ self.errReceived = self.errBuf.write
+
+ def processEnded(self, reason):
+ out = self.outBuf.getvalue()
+ err = self.errBuf.getvalue()
+ e = reason.value
+ code = e.exitCode
+ if e.signal:
+ self.deferred.errback((out, err, e.signal))
+ else:
+ self.deferred.callback((out, err, code))
+
+def getProcessOutputAndValue(executable, args=(), env={}, path=None,
+ reactor=None):
+ """Spawn a process and returns a Deferred that will be called back with
+ its output (from stdout and stderr) and it's exit code as (out, err, code)
+ If a signal is raised, the Deferred will errback with the stdout and
+ stderr up to that point, along with the signal, as (out, err, signalNum)
+ """
+ return _callProtocolWithDeferred(_EverythingGetter, executable, args, env, path,
+ reactor)
+
+def _resetWarningFilters(passthrough, addedFilters):
+ for f in addedFilters:
+ try:
+ warnings.filters.remove(f)
+ except ValueError:
+ pass
+ return passthrough
+
+
+def runWithWarningsSuppressed(suppressedWarnings, f, *a, **kw):
+ """Run the function C{f}, but with some warnings suppressed.
+
+ @param suppressedWarnings: A list of arguments to pass to filterwarnings.
+ Must be a sequence of 2-tuples (args, kwargs).
+ @param f: A callable, followed by its arguments and keyword arguments
+ """
+ for args, kwargs in suppressedWarnings:
+ warnings.filterwarnings(*args, **kwargs)
+ addedFilters = warnings.filters[:len(suppressedWarnings)]
+ try:
+ result = f(*a, **kw)
+ except:
+ exc_info = sys.exc_info()
+ _resetWarningFilters(None, addedFilters)
+ raise exc_info[0], exc_info[1], exc_info[2]
+ else:
+ if isinstance(result, defer.Deferred):
+ result.addBoth(_resetWarningFilters, addedFilters)
+ else:
+ _resetWarningFilters(None, addedFilters)
+ return result
+
+
+def suppressWarnings(f, *suppressedWarnings):
+ """
+ Wrap C{f} in a callable which suppresses the indicated warnings before
+ invoking C{f} and unsuppresses them afterwards. If f returns a Deferred,
+ warnings will remain suppressed until the Deferred fires.
+ """
+ def warningSuppressingWrapper(*a, **kw):
+ return runWithWarningsSuppressed(suppressedWarnings, f, *a, **kw)
+ return tputil.mergeFunctionMetadata(f, warningSuppressingWrapper)
+
+
+__all__ = [
+ "runWithWarningsSuppressed", "suppressWarnings",
+
+ "getProcessOutput", "getProcessValue", "getProcessOutputAndValue",
+ ]
diff --git a/twisted/internet/win32eventreactor.py b/twisted/internet/win32eventreactor.py
new file mode 100644
index 0000000..3c0e09c
--- /dev/null
+++ b/twisted/internet/win32eventreactor.py
@@ -0,0 +1,430 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+A win32event based implementation of the Twisted main loop.
+
+This requires pywin32 (formerly win32all) or ActivePython to be installed.
+
+To install the event loop (and you should do this before any connections,
+listeners or connectors are added)::
+
+ from twisted.internet import win32eventreactor
+ win32eventreactor.install()
+
+LIMITATIONS:
+ 1. WaitForMultipleObjects and thus the event loop can only handle 64 objects.
+ 2. Process running has some problems (see L{Process} docstring).
+
+
+TODO:
+ 1. Event loop handling of writes is *very* problematic (this is causing failed tests).
+ Switch to doing it the correct way, whatever that means (see below).
+ 2. Replace icky socket loopback waker with event based waker (use dummyEvent object)
+ 3. Switch everyone to using Free Software so we don't have to deal with proprietary APIs.
+
+
+ALTERNATIVE SOLUTIONS:
+ - IIRC, sockets can only be registered once. So we switch to a structure
+ like the poll() reactor, thus allowing us to deal with write events in
+ a decent fashion. This should allow us to pass tests, but we're still
+ limited to 64 events.
+
+Or:
+
+ - Instead of doing a reactor, we make this an addon to the select reactor.
+ The WFMO event loop runs in a separate thread. This means no need to maintain
+ separate code for networking, 64 event limit doesn't apply to sockets,
+ we can run processes and other win32 stuff in default event loop. The
+ only problem is that we're stuck with the icky socket based waker.
+ Another benefit is that this could be extended to support >64 events
+ in a simpler manner than the previous solution.
+
+The 2nd solution is probably what will get implemented.
+"""
+
+# System imports
+import time
+import sys
+from threading import Thread
+from weakref import WeakKeyDictionary
+
+from zope.interface import implements
+
+# Win32 imports
+from win32file import FD_READ, FD_CLOSE, FD_ACCEPT, FD_CONNECT, WSAEventSelect
+try:
+ # WSAEnumNetworkEvents was added in pywin32 215
+ from win32file import WSAEnumNetworkEvents
+except ImportError:
+ import warnings
+ warnings.warn(
+ 'Reliable disconnection notification requires pywin32 215 or later',
+ category=UserWarning)
+ def WSAEnumNetworkEvents(fd, event):
+ return set([FD_READ])
+
+from win32event import CreateEvent, MsgWaitForMultipleObjects
+from win32event import WAIT_OBJECT_0, WAIT_TIMEOUT, QS_ALLINPUT, QS_ALLEVENTS
+
+import win32gui
+
+# Twisted imports
+from twisted.internet import posixbase
+from twisted.python import log, threadable, failure
+from twisted.internet.interfaces import IReactorFDSet
+from twisted.internet.interfaces import IReactorWin32Events
+from twisted.internet.threads import blockingCallFromThread
+
+
+class Win32Reactor(posixbase.PosixReactorBase):
+ """
+ Reactor that uses Win32 event APIs.
+
+ @ivar _reads: A dictionary mapping L{FileDescriptor} instances to a
+ win32 event object used to check for read events for that descriptor.
+
+ @ivar _writes: A dictionary mapping L{FileDescriptor} instances to a
+ arbitrary value. Keys in this dictionary will be given a chance to
+ write out their data.
+
+ @ivar _events: A dictionary mapping win32 event object to tuples of
+ L{FileDescriptor} instances and event masks.
+
+ @ivar _closedAndReading: Along with C{_closedAndNotReading}, keeps track of
+ descriptors which have had close notification delivered from the OS but
+ which we have not finished reading data from. MsgWaitForMultipleObjects
+ will only deliver close notification to us once, so we remember it in
+ these two dictionaries until we're ready to act on it. The OS has
+ delivered close notification for each descriptor in this dictionary, and
+ the descriptors are marked as allowed to handle read events in the
+ reactor, so they can be processed. When a descriptor is marked as not
+ allowed to handle read events in the reactor (ie, it is passed to
+ L{IReactorFDSet.removeReader}), it is moved out of this dictionary and
+ into C{_closedAndNotReading}. The descriptors are keys in this
+ dictionary. The values are arbitrary.
+ @type _closedAndReading: C{dict}
+
+ @ivar _closedAndNotReading: These descriptors have had close notification
+ delivered from the OS, but are not marked as allowed to handle read
+ events in the reactor. They are saved here to record their closed
+ state, but not processed at all. When one of these descriptors is
+ passed to L{IReactorFDSet.addReader}, it is moved out of this dictionary
+ and into C{_closedAndReading}. The descriptors are keys in this
+ dictionary. The values are arbitrary. This is a weak key dictionary so
+ that if an application tells the reactor to stop reading from a
+ descriptor and then forgets about that descriptor itself, the reactor
+ will also forget about it.
+ @type _closedAndNotReading: C{WeakKeyDictionary}
+ """
+ implements(IReactorFDSet, IReactorWin32Events)
+
+ dummyEvent = CreateEvent(None, 0, 0, None)
+
+ def __init__(self):
+ self._reads = {}
+ self._writes = {}
+ self._events = {}
+ self._closedAndReading = {}
+ self._closedAndNotReading = WeakKeyDictionary()
+ posixbase.PosixReactorBase.__init__(self)
+
+
+ def _makeSocketEvent(self, fd, action, why):
+ """
+ Make a win32 event object for a socket.
+ """
+ event = CreateEvent(None, 0, 0, None)
+ WSAEventSelect(fd, event, why)
+ self._events[event] = (fd, action)
+ return event
+
+
+ def addEvent(self, event, fd, action):
+ """
+ Add a new win32 event to the event loop.
+ """
+ self._events[event] = (fd, action)
+
+
+ def removeEvent(self, event):
+ """
+ Remove an event.
+ """
+ del self._events[event]
+
+
+ def addReader(self, reader):
+ """
+ Add a socket FileDescriptor for notification of data available to read.
+ """
+ if reader not in self._reads:
+ self._reads[reader] = self._makeSocketEvent(
+ reader, 'doRead', FD_READ | FD_ACCEPT | FD_CONNECT | FD_CLOSE)
+ # If the reader is closed, move it over to the dictionary of reading
+ # descriptors.
+ if reader in self._closedAndNotReading:
+ self._closedAndReading[reader] = True
+ del self._closedAndNotReading[reader]
+
+
+ def addWriter(self, writer):
+ """
+ Add a socket FileDescriptor for notification of data available to write.
+ """
+ if writer not in self._writes:
+ self._writes[writer] = 1
+
+
+ def removeReader(self, reader):
+ """Remove a Selectable for notification of data available to read.
+ """
+ if reader in self._reads:
+ del self._events[self._reads[reader]]
+ del self._reads[reader]
+
+ # If the descriptor is closed, move it out of the dictionary of
+ # reading descriptors into the dictionary of waiting descriptors.
+ if reader in self._closedAndReading:
+ self._closedAndNotReading[reader] = True
+ del self._closedAndReading[reader]
+
+
+ def removeWriter(self, writer):
+ """Remove a Selectable for notification of data available to write.
+ """
+ if writer in self._writes:
+ del self._writes[writer]
+
+
+ def removeAll(self):
+ """
+ Remove all selectables, and return a list of them.
+ """
+ return self._removeAll(self._reads, self._writes)
+
+
+ def getReaders(self):
+ return self._reads.keys()
+
+
+ def getWriters(self):
+ return self._writes.keys()
+
+
+ def doWaitForMultipleEvents(self, timeout):
+ log.msg(channel='system', event='iteration', reactor=self)
+ if timeout is None:
+ timeout = 100
+
+ # Keep track of whether we run any application code before we get to the
+ # MsgWaitForMultipleObjects. If so, there's a chance it will schedule a
+ # new timed call or stop the reactor or do something else that means we
+ # shouldn't block in MsgWaitForMultipleObjects for the full timeout.
+ ranUserCode = False
+
+ # If any descriptors are trying to close, try to get them out of the way
+ # first.
+ for reader in self._closedAndReading.keys():
+ ranUserCode = True
+ self._runAction('doRead', reader)
+
+ for fd in self._writes.keys():
+ ranUserCode = True
+ log.callWithLogger(fd, self._runWrite, fd)
+
+ if ranUserCode:
+ # If application code *might* have scheduled an event, assume it
+ # did. If we're wrong, we'll get back here shortly anyway. If
+ # we're right, we'll be sure to handle the event (including reactor
+ # shutdown) in a timely manner.
+ timeout = 0
+
+ if not (self._events or self._writes):
+ # sleep so we don't suck up CPU time
+ time.sleep(timeout)
+ return
+
+ handles = self._events.keys() or [self.dummyEvent]
+ timeout = int(timeout * 1000)
+ val = MsgWaitForMultipleObjects(handles, 0, timeout, QS_ALLINPUT)
+ if val == WAIT_TIMEOUT:
+ return
+ elif val == WAIT_OBJECT_0 + len(handles):
+ exit = win32gui.PumpWaitingMessages()
+ if exit:
+ self.callLater(0, self.stop)
+ return
+ elif val >= WAIT_OBJECT_0 and val < WAIT_OBJECT_0 + len(handles):
+ event = handles[val - WAIT_OBJECT_0]
+ fd, action = self._events[event]
+
+ if fd in self._reads:
+ # Before anything, make sure it's still a valid file descriptor.
+ fileno = fd.fileno()
+ if fileno == -1:
+ self._disconnectSelectable(fd, posixbase._NO_FILEDESC, False)
+ return
+
+ # Since it's a socket (not another arbitrary event added via
+ # addEvent) and we asked for FD_READ | FD_CLOSE, check to see if
+ # we actually got FD_CLOSE. This needs a special check because
+ # it only gets delivered once. If we miss it, it's gone forever
+ # and we'll never know that the connection is closed.
+ events = WSAEnumNetworkEvents(fileno, event)
+ if FD_CLOSE in events:
+ self._closedAndReading[fd] = True
+ log.callWithLogger(fd, self._runAction, action, fd)
+
+
+ def _runWrite(self, fd):
+ closed = 0
+ try:
+ closed = fd.doWrite()
+ except:
+ closed = sys.exc_info()[1]
+ log.deferr()
+
+ if closed:
+ self.removeReader(fd)
+ self.removeWriter(fd)
+ try:
+ fd.connectionLost(failure.Failure(closed))
+ except:
+ log.deferr()
+ elif closed is None:
+ return 1
+
+ def _runAction(self, action, fd):
+ try:
+ closed = getattr(fd, action)()
+ except:
+ closed = sys.exc_info()[1]
+ log.deferr()
+ if closed:
+ self._disconnectSelectable(fd, closed, action == 'doRead')
+
+ doIteration = doWaitForMultipleEvents
+
+
+
+class _ThreadFDWrapper(object):
+ """
+ This wraps an event handler and translates notification in the helper
+ L{Win32Reactor} thread into a notification in the primary reactor thread.
+
+ @ivar _reactor: The primary reactor, the one to which event notification
+ will be sent.
+
+ @ivar _fd: The L{FileDescriptor} to which the event will be dispatched.
+
+ @ivar _action: A C{str} giving the method of C{_fd} which handles the event.
+
+ @ivar _logPrefix: The pre-fetched log prefix string for C{_fd}, so that
+ C{_fd.logPrefix} does not need to be called in a non-main thread.
+ """
+ def __init__(self, reactor, fd, action, logPrefix):
+ self._reactor = reactor
+ self._fd = fd
+ self._action = action
+ self._logPrefix = logPrefix
+
+
+ def logPrefix(self):
+ """
+ Return the original handler's log prefix, as it was given to
+ C{__init__}.
+ """
+ return self._logPrefix
+
+
+ def _execute(self):
+ """
+ Callback fired when the associated event is set. Run the C{action}
+ callback on the wrapped descriptor in the main reactor thread and raise
+ or return whatever it raises or returns to cause this event handler to
+ be removed from C{self._reactor} if appropriate.
+ """
+ return blockingCallFromThread(
+ self._reactor, lambda: getattr(self._fd, self._action)())
+
+
+ def connectionLost(self, reason):
+ """
+ Pass through to the wrapped descriptor, but in the main reactor thread
+ instead of the helper C{Win32Reactor} thread.
+ """
+ self._reactor.callFromThread(self._fd.connectionLost, reason)
+
+
+
+class _ThreadedWin32EventsMixin(object):
+ """
+ This mixin implements L{IReactorWin32Events} for another reactor by running
+ a L{Win32Reactor} in a separate thread and dispatching work to it.
+
+ @ivar _reactor: The L{Win32Reactor} running in the other thread. This is
+ C{None} until it is actually needed.
+
+ @ivar _reactorThread: The L{threading.Thread} which is running the
+ L{Win32Reactor}. This is C{None} until it is actually needed.
+ """
+ implements(IReactorWin32Events)
+
+ _reactor = None
+ _reactorThread = None
+
+
+ def _unmakeHelperReactor(self):
+ """
+ Stop and discard the reactor started by C{_makeHelperReactor}.
+ """
+ self._reactor.callFromThread(self._reactor.stop)
+ self._reactor = None
+
+
+ def _makeHelperReactor(self):
+ """
+ Create and (in a new thread) start a L{Win32Reactor} instance to use for
+ the implementation of L{IReactorWin32Events}.
+ """
+ self._reactor = Win32Reactor()
+ # This is a helper reactor, it is not the global reactor and its thread
+ # is not "the" I/O thread. Prevent it from registering it as such.
+ self._reactor._registerAsIOThread = False
+ self._reactorThread = Thread(
+ target=self._reactor.run, args=(False,))
+ self.addSystemEventTrigger(
+ 'after', 'shutdown', self._unmakeHelperReactor)
+ self._reactorThread.start()
+
+
+ def addEvent(self, event, fd, action):
+ """
+ @see: L{IReactorWin32Events}
+ """
+ if self._reactor is None:
+ self._makeHelperReactor()
+ self._reactor.callFromThread(
+ self._reactor.addEvent,
+ event, _ThreadFDWrapper(self, fd, action, fd.logPrefix()),
+ "_execute")
+
+
+ def removeEvent(self, event):
+ """
+ @see: L{IReactorWin32Events}
+ """
+ self._reactor.callFromThread(self._reactor.removeEvent, event)
+
+
+
+def install():
+ threadable.init(1)
+ r = Win32Reactor()
+ import main
+ main.installReactor(r)
+
+
+__all__ = ["Win32Reactor", "install"]
diff --git a/twisted/internet/wxreactor.py b/twisted/internet/wxreactor.py
new file mode 100644
index 0000000..71e861a
--- /dev/null
+++ b/twisted/internet/wxreactor.py
@@ -0,0 +1,184 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module provides wxPython event loop support for Twisted.
+
+In order to use this support, simply do the following::
+
+ | from twisted.internet import wxreactor
+ | wxreactor.install()
+
+Then, when your root wxApp has been created::
+
+ | from twisted.internet import reactor
+ | reactor.registerWxApp(yourApp)
+ | reactor.run()
+
+Then use twisted.internet APIs as usual. Stop the event loop using
+reactor.stop(), not yourApp.ExitMainLoop().
+
+IMPORTANT: tests will fail when run under this reactor. This is
+expected and probably does not reflect on the reactor's ability to run
+real applications.
+"""
+
+import Queue
+try:
+ from wx import PySimpleApp as wxPySimpleApp, CallAfter as wxCallAfter, \
+ Timer as wxTimer
+except ImportError:
+ # older version of wxPython:
+ from wxPython.wx import wxPySimpleApp, wxCallAfter, wxTimer
+
+from twisted.python import log, runtime
+from twisted.internet import _threadedselect
+
+
+class ProcessEventsTimer(wxTimer):
+ """
+ Timer that tells wx to process pending events.
+
+ This is necessary on OS X, probably due to a bug in wx, if we want
+ wxCallAfters to be handled when modal dialogs, menus, etc. are open.
+ """
+ def __init__(self, wxapp):
+ wxTimer.__init__(self)
+ self.wxapp = wxapp
+
+
+ def Notify(self):
+ """
+ Called repeatedly by wx event loop.
+ """
+ self.wxapp.ProcessPendingEvents()
+
+
+
+class WxReactor(_threadedselect.ThreadedSelectReactor):
+ """
+ wxPython reactor.
+
+ wxPython drives the event loop, select() runs in a thread.
+ """
+
+ _stopping = False
+
+ def registerWxApp(self, wxapp):
+ """
+ Register wxApp instance with the reactor.
+ """
+ self.wxapp = wxapp
+
+
+ def _installSignalHandlersAgain(self):
+ """
+ wx sometimes removes our own signal handlers, so re-add them.
+ """
+ try:
+ # make _handleSignals happy:
+ import signal
+ signal.signal(signal.SIGINT, signal.default_int_handler)
+ except ImportError:
+ return
+ self._handleSignals()
+
+
+ def stop(self):
+ """
+ Stop the reactor.
+ """
+ if self._stopping:
+ return
+ self._stopping = True
+ _threadedselect.ThreadedSelectReactor.stop(self)
+
+
+ def _runInMainThread(self, f):
+ """
+ Schedule function to run in main wx/Twisted thread.
+
+ Called by the select() thread.
+ """
+ if hasattr(self, "wxapp"):
+ wxCallAfter(f)
+ else:
+ # wx shutdown but twisted hasn't
+ self._postQueue.put(f)
+
+
+ def _stopWx(self):
+ """
+ Stop the wx event loop if it hasn't already been stopped.
+
+ Called during Twisted event loop shutdown.
+ """
+ if hasattr(self, "wxapp"):
+ self.wxapp.ExitMainLoop()
+
+
+ def run(self, installSignalHandlers=True):
+ """
+ Start the reactor.
+ """
+ self._postQueue = Queue.Queue()
+ if not hasattr(self, "wxapp"):
+ log.msg("registerWxApp() was not called on reactor, "
+ "registering my own wxApp instance.")
+ self.registerWxApp(wxPySimpleApp())
+
+ # start select() thread:
+ self.interleave(self._runInMainThread,
+ installSignalHandlers=installSignalHandlers)
+ if installSignalHandlers:
+ self.callLater(0, self._installSignalHandlersAgain)
+
+ # add cleanup events:
+ self.addSystemEventTrigger("after", "shutdown", self._stopWx)
+ self.addSystemEventTrigger("after", "shutdown",
+ lambda: self._postQueue.put(None))
+
+ # On Mac OS X, work around wx bug by starting timer to ensure
+ # wxCallAfter calls are always processed. We don't wake up as
+ # often as we could since that uses too much CPU.
+ if runtime.platform.isMacOSX():
+ t = ProcessEventsTimer(self.wxapp)
+ t.Start(2) # wake up every 2ms
+
+ self.wxapp.MainLoop()
+ wxapp = self.wxapp
+ del self.wxapp
+
+ if not self._stopping:
+ # wx event loop exited without reactor.stop() being
+ # called. At this point events from select() thread will
+ # be added to _postQueue, but some may still be waiting
+ # unprocessed in wx, thus the ProcessPendingEvents()
+ # below.
+ self.stop()
+ wxapp.ProcessPendingEvents() # deal with any queued wxCallAfters
+ while 1:
+ try:
+ f = self._postQueue.get(timeout=0.01)
+ except Queue.Empty:
+ continue
+ else:
+ if f is None:
+ break
+ try:
+ f()
+ except:
+ log.err()
+
+
+def install():
+ """
+ Configure the twisted mainloop to be run inside the wxPython mainloop.
+ """
+ reactor = WxReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
+
+
+__all__ = ['install']
diff --git a/twisted/internet/wxsupport.py b/twisted/internet/wxsupport.py
new file mode 100644
index 0000000..d17c666
--- /dev/null
+++ b/twisted/internet/wxsupport.py
@@ -0,0 +1,61 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""Old method of wxPython support for Twisted.
+
+twisted.internet.wxreactor is probably a better choice.
+
+To use::
+
+ | # given a wxApp instance called myWxAppInstance:
+ | from twisted.internet import wxsupport
+ | wxsupport.install(myWxAppInstance)
+
+Use Twisted's APIs for running and stopping the event loop, don't use
+wxPython's methods.
+
+On Windows the Twisted event loop might block when dialogs are open
+or menus are selected.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+import warnings
+warnings.warn("wxsupport is not fully functional on Windows, wxreactor is better.")
+
+# wxPython imports
+from wxPython.wx import wxApp
+
+# twisted imports
+from twisted.internet import reactor
+from twisted.python.runtime import platformType
+
+
+class wxRunner:
+ """Make sure GUI events are handled."""
+
+ def __init__(self, app):
+ self.app = app
+
+ def run(self):
+ """
+ Execute pending WX events followed by WX idle events and
+ reschedule.
+ """
+ # run wx events
+ while self.app.Pending():
+ self.app.Dispatch()
+
+ # run wx idle events
+ self.app.ProcessIdle()
+ reactor.callLater(0.02, self.run)
+
+
+def install(app):
+ """Install the wxPython support, given a wxApp instance"""
+ runner = wxRunner(app)
+ reactor.callLater(0.02, runner.run)
+
+
+__all__ = ["install"]
diff --git a/twisted/lore/__init__.py b/twisted/lore/__init__.py
new file mode 100644
index 0000000..89ab207
--- /dev/null
+++ b/twisted/lore/__init__.py
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+'''
+The Twisted Documentation Generation System
+
+Maintainer: Andrew Bennetts
+'''
+
+# TODO
+# Abstract
+# Bibliography
+# Index
+# Allow non-web image formats (EPS, specifically)
+# Allow pickle output and input to minimize parses
+# Numbered headers
+# Navigational aides
+
+from twisted.lore._version import version
+__version__ = version.short()
diff --git a/twisted/lore/_version.py b/twisted/lore/_version.py
new file mode 100644
index 0000000..23c6a07
--- /dev/null
+++ b/twisted/lore/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.lore', 12, 1, 0)
diff --git a/twisted/lore/default.py b/twisted/lore/default.py
new file mode 100644
index 0000000..5b542ad
--- /dev/null
+++ b/twisted/lore/default.py
@@ -0,0 +1,56 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Default processing factory plugin.
+"""
+
+from xml.dom import minidom as dom
+
+from twisted.lore import tree, latex, lint, process
+from twisted.web import sux
+
+htmlDefault = {'template': 'template.tpl', 'baseurl': '%s', 'ext': '.html'}
+
+class ProcessingFunctionFactory:
+
+ def getDoFile(self):
+ return tree.doFile
+
+ def generate_html(self, options, filenameGenerator=tree.getOutputFileName):
+ n = htmlDefault.copy()
+ n.update(options)
+ options = n
+ try:
+ fp = open(options['template'])
+ templ = dom.parse(fp)
+ except IOError, e:
+ raise process.NoProcessorError(e.filename+": "+e.strerror)
+ except sux.ParseError, e:
+ raise process.NoProcessorError(str(e))
+ df = lambda file, linkrel: self.getDoFile()(file, linkrel, options['ext'],
+ options['baseurl'], templ, options, filenameGenerator)
+ return df
+
+ latexSpitters = {None: latex.LatexSpitter,
+ 'section': latex.SectionLatexSpitter,
+ 'chapter': latex.ChapterLatexSpitter,
+ 'book': latex.BookLatexSpitter,
+ }
+
+ def generate_latex(self, options, filenameGenerator=None):
+ spitter = self.latexSpitters[None]
+ for (key, value) in self.latexSpitters.items():
+ if key and options.get(key):
+ spitter = value
+ df = lambda file, linkrel: latex.convertFile(file, spitter)
+ return df
+
+ def getLintChecker(self):
+ return lint.getDefaultChecker()
+
+ def generate_lint(self, options, filenameGenerator=None):
+ checker = self.getLintChecker()
+ return lambda file, linkrel: lint.doFile(file, checker)
+
+factory = ProcessingFunctionFactory()
diff --git a/twisted/lore/docbook.py b/twisted/lore/docbook.py
new file mode 100644
index 0000000..62c8fc6
--- /dev/null
+++ b/twisted/lore/docbook.py
@@ -0,0 +1,68 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+DocBook output support for Lore.
+"""
+
+import os, cgi
+from xml.dom import minidom as dom
+
+from twisted.lore import latex
+
+
+class DocbookSpitter(latex.BaseLatexSpitter):
+
+ currentLevel = 1
+
+ def writeNodeData(self, node):
+ self.writer(node.data)
+
+ def visitNode_body(self, node):
+ self.visitNodeDefault(node)
+ self.writer('</section>'*self.currentLevel)
+
+ def visitNodeHeader(self, node):
+ level = int(node.tagName[1])
+ difference, self.currentLevel = level-self.currentLevel, level
+ self.writer('<section>'*difference+'</section>'*-difference)
+ if difference<=0:
+ self.writer('</section>\n<section>')
+ self.writer('<title>')
+ self.visitNodeDefault(node)
+
+ def visitNode_a_listing(self, node):
+ fileName = os.path.join(self.currDir, node.getAttribute('href'))
+ self.writer('<programlisting>\n')
+ self.writer(cgi.escape(open(fileName).read()))
+ self.writer('</programlisting>\n')
+
+ def visitNode_a_href(self, node):
+ self.visitNodeDefault(node)
+
+ def visitNode_a_name(self, node):
+ self.visitNodeDefault(node)
+
+ def visitNode_li(self, node):
+ for child in node.childNodes:
+ if getattr(child, 'tagName', None) != 'p':
+ new = dom.Element('p')
+ new.childNodes = [child]
+ node.replaceChild(new, child)
+ self.visitNodeDefault(node)
+
+ visitNode_h2 = visitNode_h3 = visitNode_h4 = visitNodeHeader
+ end_h2 = end_h3 = end_h4 = '</title><para />'
+ start_title, end_title = '<section><title>', '</title><para />'
+ start_p, end_p = '<para>', '</para>'
+ start_strong, end_strong = start_em, end_em = '<emphasis>', '</emphasis>'
+ start_span_footnote, end_span_footnote = '<footnote><para>', '</para></footnote>'
+ start_q = end_q = '"'
+ start_pre, end_pre = '<programlisting>', '</programlisting>'
+ start_div_note, end_div_note = '<note>', '</note>'
+ start_li, end_li = '<listitem>', '</listitem>'
+ start_ul, end_ul = '<itemizedlist>', '</itemizedlist>'
+ start_ol, end_ol = '<orderedlist>', '</orderedlist>'
+ start_dl, end_dl = '<variablelist>', '</variablelist>'
+ start_dt, end_dt = '<varlistentry><term>', '</term>'
+ start_dd, end_dd = '<listitem><para>', '</para></listitem></varlistentry>'
diff --git a/twisted/lore/htmlbook.py b/twisted/lore/htmlbook.py
new file mode 100644
index 0000000..1290e08
--- /dev/null
+++ b/twisted/lore/htmlbook.py
@@ -0,0 +1,47 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def getNumber(filename):
+ return None
+
+def getReference(filename):
+ return None
+
+class Book:
+
+ def __init__(self, filename):
+ self.chapters = []
+ self.indexFilename = None
+
+ global Chapter
+ Chapter = self.Chapter
+ global getNumber
+ getNumber = self.getNumber
+ global getReference
+ getReference = self.getNumber
+ global Index
+ Index = self.Index
+
+ if filename:
+ execfile(filename)
+
+ def getFiles(self):
+ return [c[0] for c in self.chapters]
+
+ def getNumber(self, filename):
+ for c in self.chapters:
+ if c[0] == filename:
+ return c[1]
+ return None
+
+ def getIndexFilename(self):
+ return self.indexFilename
+
+ def Chapter(self, filename, number):
+ self.chapters.append((filename, number))
+
+ def Index(self, filename):
+ self.indexFilename = filename
+
+#_book = Book(None)
diff --git a/twisted/lore/indexer.py b/twisted/lore/indexer.py
new file mode 100644
index 0000000..3fa2209
--- /dev/null
+++ b/twisted/lore/indexer.py
@@ -0,0 +1,50 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def setIndexFilename(filename='index.xhtml'):
+ global indexFilename
+ indexFilename = filename
+
+def getIndexFilename():
+ global indexFilename
+ return indexFilename
+
+def addEntry(filename, anchor, text, reference):
+ global entries
+ if not entries.has_key(text):
+ entries[text] = []
+ entries[text].append((filename, anchor, reference))
+
+def clearEntries():
+ global entries
+ entries = {}
+
+def generateIndex():
+ global entries
+ global indexFilename
+
+ if not indexFilename:
+ return
+
+ f = open(indexFilename, 'w')
+ sortedEntries = [(e.lower(), e) for e in entries]
+ sortedEntries.sort()
+ sortedEntries = [e[1] for e in sortedEntries]
+ for text in sortedEntries:
+ refs = []
+ f.write(text.replace('!', ', ') + ': ')
+ for (file, anchor, reference) in entries[text]:
+ refs.append('<a href="%s#%s">%s</a>' % (file, anchor, reference))
+ if text == 'infinite recursion':
+ refs.append('<em>See Also:</em> recursion, infinite\n')
+ if text == 'recursion!infinite':
+ refs.append('<em>See Also:</em> infinite recursion\n')
+ f.write('%s<br />\n' % ", ".join(refs))
+ f.close()
+
+def reset():
+ clearEntries()
+ setIndexFilename()
+
+reset()
diff --git a/twisted/lore/latex.py b/twisted/lore/latex.py
new file mode 100644
index 0000000..ed843ed
--- /dev/null
+++ b/twisted/lore/latex.py
@@ -0,0 +1,463 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+LaTeX output support for Lore.
+"""
+
+from xml.dom import minidom as dom
+import os.path, re
+from cStringIO import StringIO
+import urlparse
+
+from twisted.web import domhelpers
+from twisted.python import text, procutils
+
+import tree
+
+escapingRE = re.compile(r'([\[\]#$%&_{}^~\\])')
+lowerUpperRE = re.compile(r'([a-z])([A-Z])')
+
+def _escapeMatch(match):
+ c = match.group()
+ if c == '\\':
+ return '$\\backslash$'
+ elif c == '~':
+ return '\\~{}'
+ elif c == '^':
+ return '\\^{}'
+ elif c in '[]':
+ return '{'+c+'}'
+ else:
+ return '\\' + c
+
+def latexEscape(txt):
+ txt = escapingRE.sub(_escapeMatch, txt)
+ return txt.replace('\n', ' ')
+
+entities = {'amp': '\&', 'gt': '>', 'lt': '<', 'quot': '"',
+ 'copy': '\\copyright', 'mdash': '---', 'rdquo': '``',
+ 'ldquo': "''"}
+
+
+def realpath(path):
+ # Normalise path
+ cwd = os.getcwd()
+ path = os.path.normpath(os.path.join(cwd, path))
+ return path.replace('\\', '/') # windows slashes make LaTeX blow up
+
+
+def getLatexText(node, writer, filter=lambda x:x, entities=entities):
+ if hasattr(node, 'eref'):
+ return writer(entities.get(node.eref, ''))
+ if hasattr(node, 'data'):
+ if isinstance(node.data, unicode):
+ data = node.data.encode('utf-8')
+ else:
+ data = node.data
+ return writer(filter(data))
+ for child in node.childNodes:
+ getLatexText(child, writer, filter, entities)
+
+class BaseLatexSpitter:
+
+ def __init__(self, writer, currDir='.', filename=''):
+ self.writer = writer
+ self.currDir = currDir
+ self.filename = filename
+
+ def visitNode(self, node):
+ if isinstance(node, dom.Comment):
+ return
+ if not hasattr(node, 'tagName'):
+ self.writeNodeData(node)
+ return
+ getattr(self, 'visitNode_'+node.tagName, self.visitNodeDefault)(node)
+
+ def visitNodeDefault(self, node):
+ self.writer(getattr(self, 'start_'+node.tagName, ''))
+ for child in node.childNodes:
+ self.visitNode(child)
+ self.writer(getattr(self, 'end_'+node.tagName, ''))
+
+ def visitNode_a(self, node):
+ if node.hasAttribute('class'):
+ if node.getAttribute('class').endswith('listing'):
+ return self.visitNode_a_listing(node)
+ if node.hasAttribute('href'):
+ return self.visitNode_a_href(node)
+ if node.hasAttribute('name'):
+ return self.visitNode_a_name(node)
+ self.visitNodeDefault(node)
+
+ def visitNode_span(self, node):
+ if not node.hasAttribute('class'):
+ return self.visitNodeDefault(node)
+ node.tagName += '_'+node.getAttribute('class')
+ self.visitNode(node)
+
+ visitNode_div = visitNode_span
+
+ def visitNode_h1(self, node):
+ pass
+
+ def visitNode_style(self, node):
+ pass
+
+
+class LatexSpitter(BaseLatexSpitter):
+
+ baseLevel = 0
+ diaHack = bool(procutils.which("dia"))
+
+ def writeNodeData(self, node):
+ buf = StringIO()
+ getLatexText(node, buf.write, latexEscape)
+ self.writer(buf.getvalue().replace('<', '$<$').replace('>', '$>$'))
+
+ def visitNode_head(self, node):
+ authorNodes = domhelpers.findElementsWithAttribute(node, 'rel', 'author')
+ authorNodes = [n for n in authorNodes if n.tagName == 'link']
+
+ if authorNodes:
+ self.writer('\\author{')
+ authors = []
+ for aNode in authorNodes:
+ name = aNode.getAttribute('title')
+ href = aNode.getAttribute('href')
+ if href.startswith('mailto:'):
+ href = href[7:]
+ if href:
+ if name:
+ name += ' '
+ name += '$<$' + href + '$>$'
+ if name:
+ authors.append(name)
+
+ self.writer(' \\and '.join(authors))
+ self.writer('}')
+
+ self.visitNodeDefault(node)
+
+ def visitNode_pre(self, node):
+ self.writer('\\begin{verbatim}\n')
+ buf = StringIO()
+ getLatexText(node, buf.write)
+ self.writer(text.removeLeadingTrailingBlanks(buf.getvalue()))
+ self.writer('\\end{verbatim}\n')
+
+ def visitNode_code(self, node):
+ fout = StringIO()
+ getLatexText(node, fout.write, latexEscape)
+ data = lowerUpperRE.sub(r'\1\\linebreak[1]\2', fout.getvalue())
+ data = data[:1] + data[1:].replace('.', '.\\linebreak[1]')
+ self.writer('\\texttt{'+data+'}')
+
+ def visitNode_img(self, node):
+ fileName = os.path.join(self.currDir, node.getAttribute('src'))
+ target, ext = os.path.splitext(fileName)
+ if self.diaHack and os.access(target + '.dia', os.R_OK):
+ ext = '.dia'
+ fileName = target + ext
+ f = getattr(self, 'convert_'+ext[1:], None)
+ if not f:
+ return
+ target = os.path.join(self.currDir, os.path.basename(target)+'.eps')
+ f(fileName, target)
+ target = os.path.basename(target)
+ self._write_img(target)
+
+ def _write_img(self, target):
+ """Write LaTeX for image."""
+ self.writer('\\begin{center}\\includegraphics[%%\n'
+ 'width=1.0\n'
+ '\\textwidth,height=1.0\\textheight,\nkeepaspectratio]'
+ '{%s}\\end{center}\n' % target)
+
+ def convert_png(self, src, target):
+ # XXX there's a *reason* Python comes with the pipes module -
+ # someone fix this to use it.
+ r = os.system('pngtopnm "%s" | pnmtops -noturn > "%s"' % (src, target))
+ if r != 0:
+ raise OSError(r)
+
+ def convert_dia(self, src, target):
+ # EVIL DISGUSTING HACK
+ data = os.popen("gunzip -dc %s" % (src)).read()
+ pre = '<dia:attribute name="scaling">\n <dia:real val="1"/>'
+ post = '<dia:attribute name="scaling">\n <dia:real val="0.5"/>'
+ f = open('%s_hacked.dia' % (src), 'wb')
+ f.write(data.replace(pre, post))
+ f.close()
+ os.system('gzip %s_hacked.dia' % (src,))
+ os.system('mv %s_hacked.dia.gz %s_hacked.dia' % (src,src))
+ # Let's pretend we never saw that.
+
+ # Silly dia needs an X server, even though it doesn't display anything.
+ # If this is a problem for you, try using Xvfb.
+ os.system("dia %s_hacked.dia -n -e %s" % (src, target))
+
+ def visitNodeHeader(self, node):
+ level = (int(node.tagName[1])-2)+self.baseLevel
+ self.writer('\n\n\\'+level*'sub'+'section{')
+ spitter = HeadingLatexSpitter(self.writer, self.currDir, self.filename)
+ spitter.visitNodeDefault(node)
+ self.writer('}\n')
+
+ def visitNode_a_listing(self, node):
+ fileName = os.path.join(self.currDir, node.getAttribute('href'))
+ self.writer('\\begin{verbatim}\n')
+ lines = map(str.rstrip, open(fileName).readlines())
+ skipLines = int(node.getAttribute('skipLines') or 0)
+ lines = lines[skipLines:]
+ self.writer(text.removeLeadingTrailingBlanks('\n'.join(lines)))
+ self.writer('\\end{verbatim}')
+
+ # Write a caption for this source listing
+ fileName = os.path.basename(fileName)
+ caption = domhelpers.getNodeText(node)
+ if caption == fileName:
+ caption = 'Source listing'
+ self.writer('\parbox[b]{\linewidth}{\\begin{center}%s --- '
+ '\\begin{em}%s\\end{em}\\end{center}}'
+ % (latexEscape(caption), latexEscape(fileName)))
+
+ def visitNode_a_href(self, node):
+ supported_schemes=['http', 'https', 'ftp', 'mailto']
+ href = node.getAttribute('href')
+ if urlparse.urlparse(href)[0] in supported_schemes:
+ text = domhelpers.getNodeText(node)
+ self.visitNodeDefault(node)
+ if text != href:
+ self.writer('\\footnote{%s}' % latexEscape(href))
+ else:
+ path, fragid = (href.split('#', 1) + [None])[:2]
+ if path == '':
+ path = self.filename
+ else:
+ path = os.path.join(os.path.dirname(self.filename), path)
+ #if path == '':
+ #path = os.path.basename(self.filename)
+ #else:
+ # # Hack for linking to man pages from howtos, i.e.
+ # # ../doc/foo-man.html -> foo-man.html
+ # path = os.path.basename(path)
+
+ path = realpath(path)
+
+ if fragid:
+ ref = path + 'HASH' + fragid
+ else:
+ ref = path
+ self.writer('\\textit{')
+ self.visitNodeDefault(node)
+ self.writer('}')
+ self.writer('\\loreref{%s}' % ref)
+
+ def visitNode_a_name(self, node):
+ self.writer('\\label{%sHASH%s}' % (
+ realpath(self.filename), node.getAttribute('name')))
+ self.visitNodeDefault(node)
+
+ def visitNode_table(self, node):
+ rows = [[col for col in row.childNodes
+ if getattr(col, 'tagName', None) in ('th', 'td')]
+ for row in node.childNodes if getattr(row, 'tagName', None)=='tr']
+ numCols = 1+max([len(row) for row in rows])
+ self.writer('\\begin{table}[ht]\\begin{center}')
+ self.writer('\\begin{tabular}{@{}'+'l'*numCols+'@{}}')
+ for row in rows:
+ th = 0
+ for col in row:
+ self.visitNode(col)
+ self.writer('&')
+ if col.tagName == 'th':
+ th = 1
+ self.writer('\\\\\n') #\\ ends lines
+ if th:
+ self.writer('\\hline\n')
+ self.writer('\\end{tabular}\n')
+ if node.hasAttribute('title'):
+ self.writer('\\caption{%s}'
+ % latexEscape(node.getAttribute('title')))
+ self.writer('\\end{center}\\end{table}\n')
+
+ def visitNode_span_footnote(self, node):
+ self.writer('\\footnote{')
+ spitter = FootnoteLatexSpitter(self.writer, self.currDir, self.filename)
+ spitter.visitNodeDefault(node)
+ self.writer('}')
+
+ def visitNode_span_index(self, node):
+ self.writer('\\index{%s}\n' % node.getAttribute('value'))
+ self.visitNodeDefault(node)
+
+ visitNode_h2 = visitNode_h3 = visitNode_h4 = visitNodeHeader
+
+ start_title = '\\title{'
+ end_title = '}\n'
+
+ start_sub = '$_{'
+ end_sub = '}$'
+
+ start_sup = '$^{'
+ end_sup = '}$'
+
+ start_html = '''\\documentclass{article}
+ \\newcommand{\\loreref}[1]{%
+ \\ifthenelse{\\value{page}=\\pageref{#1}}%
+ { (this page)}%
+ { (page \\pageref{#1})}%
+ }'''
+
+ start_body = '\\begin{document}\n\\maketitle\n'
+ end_body = '\\end{document}'
+
+ start_dl = '\\begin{description}\n'
+ end_dl = '\\end{description}\n'
+ start_ul = '\\begin{itemize}\n'
+ end_ul = '\\end{itemize}\n'
+
+ start_ol = '\\begin{enumerate}\n'
+ end_ol = '\\end{enumerate}\n'
+
+ start_li = '\\item '
+ end_li = '\n'
+
+ start_dt = '\\item['
+ end_dt = ']'
+ end_dd = '\n'
+
+ start_p = '\n\n'
+
+ start_strong = start_em = '\\begin{em}'
+ end_strong = end_em = '\\end{em}'
+
+ start_q = "``"
+ end_q = "''"
+
+ start_div_note = '\\begin{quotation}\\textbf{Note:}'
+ end_div_note = '\\end{quotation}'
+
+ start_th = '\\textbf{'
+ end_th = '}'
+
+
+class SectionLatexSpitter(LatexSpitter):
+
+ baseLevel = 1
+
+ start_title = '\\section{'
+
+ def visitNode_title(self, node):
+ self.visitNodeDefault(node)
+ #self.writer('\\label{%s}}\n' % os.path.basename(self.filename))
+ self.writer('\\label{%s}}\n' % realpath(self.filename))
+
+ end_title = end_body = start_body = start_html = ''
+
+
+class ChapterLatexSpitter(SectionLatexSpitter):
+ baseLevel = 0
+ start_title = '\\chapter{'
+
+
+class HeadingLatexSpitter(BaseLatexSpitter):
+ start_q = "``"
+ end_q = "''"
+
+ writeNodeData = LatexSpitter.writeNodeData.im_func
+
+
+class FootnoteLatexSpitter(LatexSpitter):
+ """For multi-paragraph footnotes, this avoids having an empty leading
+ paragraph."""
+
+ start_p = ''
+
+ def visitNode_span_footnote(self, node):
+ self.visitNodeDefault(node)
+
+ def visitNode_p(self, node):
+ self.visitNodeDefault(node)
+ self.start_p = LatexSpitter.start_p
+
+class BookLatexSpitter(LatexSpitter):
+ def visitNode_body(self, node):
+ tocs=domhelpers.locateNodes([node], 'class', 'toc')
+ domhelpers.clearNode(node)
+ if len(tocs):
+ toc=tocs[0]
+ node.appendChild(toc)
+ self.visitNodeDefault(node)
+
+ def visitNode_link(self, node):
+ if not node.hasAttribute('rel'):
+ return self.visitNodeDefault(node)
+ node.tagName += '_'+node.getAttribute('rel')
+ self.visitNode(node)
+
+ def visitNode_link_author(self, node):
+ self.writer('\\author{%s}\n' % node.getAttribute('text'))
+
+ def visitNode_link_stylesheet(self, node):
+ if node.hasAttribute('type') and node.hasAttribute('href'):
+ if node.getAttribute('type')=='application/x-latex':
+ packagename=node.getAttribute('href')
+ packagebase,ext=os.path.splitext(packagename)
+ self.writer('\\usepackage{%s}\n' % packagebase)
+
+ start_html = r'''\documentclass[oneside]{book}
+\usepackage{graphicx}
+\usepackage{times,mathptmx}
+'''
+
+ start_body = r'''\begin{document}
+\maketitle
+\tableofcontents
+'''
+
+ start_li=''
+ end_li=''
+ start_ul=''
+ end_ul=''
+
+
+ def visitNode_a(self, node):
+ if node.hasAttribute('class'):
+ a_class=node.getAttribute('class')
+ if a_class.endswith('listing'):
+ return self.visitNode_a_listing(node)
+ else:
+ return getattr(self, 'visitNode_a_%s' % a_class)(node)
+ if node.hasAttribute('href'):
+ return self.visitNode_a_href(node)
+ if node.hasAttribute('name'):
+ return self.visitNode_a_name(node)
+ self.visitNodeDefault(node)
+
+ def visitNode_a_chapter(self, node):
+ self.writer('\\chapter{')
+ self.visitNodeDefault(node)
+ self.writer('}\n')
+
+ def visitNode_a_sect(self, node):
+ base,ext=os.path.splitext(node.getAttribute('href'))
+ self.writer('\\input{%s}\n' % base)
+
+
+
+def processFile(spitter, fin):
+ # XXX Use Inversion Of Control Pattern to orthogonalize the parsing API
+ # from the Visitor Pattern application. (EnterPrise)
+ dom = tree.parseFileAndReport(fin.name, lambda x: fin).documentElement
+ spitter.visitNode(dom)
+
+
+def convertFile(filename, spitterClass):
+ fout = open(os.path.splitext(filename)[0]+".tex", 'w')
+ spitter = spitterClass(fout.write, os.path.dirname(filename), filename)
+ fin = open(filename)
+ processFile(spitter, fin)
+ fin.close()
+ fout.close()
diff --git a/twisted/lore/lint.py b/twisted/lore/lint.py
new file mode 100644
index 0000000..a8c85c3
--- /dev/null
+++ b/twisted/lore/lint.py
@@ -0,0 +1,204 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Checker for common errors in Lore documents.
+"""
+
+from xml.dom import minidom as dom
+import parser, urlparse, os.path
+
+from twisted.lore import tree, process
+from twisted.web import domhelpers
+from twisted.python import reflect
+
+
+# parser.suite in Python 2.3 raises SyntaxError, <2.3 raises parser.ParserError
+parserErrors = (SyntaxError, parser.ParserError)
+
+class TagChecker:
+
+ def check(self, dom, filename):
+ self.hadErrors = 0
+ for method in reflect.prefixedMethods(self, 'check_'):
+ method(dom, filename)
+ if self.hadErrors:
+ raise process.ProcessingFailure("invalid format")
+
+ def _reportError(self, filename, element, error):
+ hlint = element.hasAttribute('hlint') and element.getAttribute('hlint')
+ if hlint != 'off':
+ self.hadErrors = 1
+ pos = getattr(element, '_markpos', None) or (0, 0)
+ print "%s:%s:%s: %s" % ((filename,)+pos+(error,))
+
+
+class DefaultTagChecker(TagChecker):
+
+ def __init__(self, allowedTags, allowedClasses):
+ self.allowedTags = allowedTags
+ self.allowedClasses = allowedClasses
+
+ def check_disallowedElements(self, dom, filename):
+ def m(node, self=self):
+ return not self.allowedTags(node.tagName)
+ for element in domhelpers.findElements(dom, m):
+ self._reportError(filename, element,
+ 'unrecommended tag %s' % element.tagName)
+
+ def check_disallowedClasses(self, dom, filename):
+ def matcher(element, self=self):
+ if not element.hasAttribute('class'):
+ return 0
+ checker = self.allowedClasses.get(element.tagName, lambda x:0)
+ return not checker(element.getAttribute('class'))
+ for element in domhelpers.findElements(dom, matcher):
+ self._reportError(filename, element,
+ 'unknown class %s' %element.getAttribute('class'))
+
+ def check_quote(self, doc, filename):
+ def matcher(node):
+ return ('"' in getattr(node, 'data', '') and
+ not isinstance(node, dom.Comment) and
+ not [1 for n in domhelpers.getParents(node)[1:-1]
+ if n.tagName in ('pre', 'code')])
+ for node in domhelpers.findNodes(doc, matcher):
+ self._reportError(filename, node.parentNode, 'contains quote')
+
+ def check_styleattr(self, dom, filename):
+ for node in domhelpers.findElementsWithAttribute(dom, 'style'):
+ self._reportError(filename, node, 'explicit style')
+
+ def check_align(self, dom, filename):
+ for node in domhelpers.findElementsWithAttribute(dom, 'align'):
+ self._reportError(filename, node, 'explicit alignment')
+
+ def check_style(self, dom, filename):
+ for node in domhelpers.findNodesNamed(dom, 'style'):
+ if domhelpers.getNodeText(node) != '':
+ self._reportError(filename, node, 'hand hacked style')
+
+ def check_title(self, dom, filename):
+ doc = dom.documentElement
+ title = domhelpers.findNodesNamed(dom, 'title')
+ if len(title)!=1:
+ return self._reportError(filename, doc, 'not exactly one title')
+ h1 = domhelpers.findNodesNamed(dom, 'h1')
+ if len(h1)!=1:
+ return self._reportError(filename, doc, 'not exactly one h1')
+ if domhelpers.getNodeText(h1[0]) != domhelpers.getNodeText(title[0]):
+ self._reportError(filename, h1[0], 'title and h1 text differ')
+
+ def check_80_columns(self, dom, filename):
+ for node in domhelpers.findNodesNamed(dom, 'pre'):
+ # the ps/pdf output is in a font that cuts off at 80 characters,
+ # so this is enforced to make sure the interesting parts (which
+ # are likely to be on the right-hand edge) stay on the printed
+ # page.
+ for line in domhelpers.gatherTextNodes(node, 1).split('\n'):
+ if len(line.rstrip()) > 80:
+ self._reportError(filename, node,
+ 'text wider than 80 columns in pre')
+ for node in domhelpers.findNodesNamed(dom, 'a'):
+ if node.getAttribute('class').endswith('listing'):
+ try:
+ fn = os.path.dirname(filename)
+ fn = os.path.join(fn, node.getAttribute('href'))
+ lines = open(fn,'r').readlines()
+ except:
+ self._reportError(filename, node,
+ 'bad listing href: %r' %
+ node.getAttribute('href'))
+ continue
+
+ for line in lines:
+ if len(line.rstrip()) > 80:
+ self._reportError(filename, node,
+ 'listing wider than 80 columns')
+
+ def check_pre_py_listing(self, dom, filename):
+ for node in domhelpers.findNodesNamed(dom, 'pre'):
+ if node.getAttribute('class') == 'python':
+ try:
+ text = domhelpers.getNodeText(node)
+ # Fix < and >
+ text = text.replace('&gt;', '>').replace('&lt;', '<')
+ # Strip blank lines
+ lines = filter(None,[l.rstrip() for l in text.split('\n')])
+ # Strip leading space
+ while not [1 for line in lines if line[:1] not in ('',' ')]:
+ lines = [line[1:] for line in lines]
+ text = '\n'.join(lines) + '\n'
+ try:
+ parser.suite(text)
+ except parserErrors, e:
+ # Pretend the "..." idiom is syntactically valid
+ text = text.replace("...","'...'")
+ parser.suite(text)
+ except parserErrors, e:
+ self._reportError(filename, node,
+ 'invalid python code:' + str(e))
+
+ def check_anchor_in_heading(self, dom, filename):
+ headingNames = ['h%d' % n for n in range(1,7)]
+ for hname in headingNames:
+ for node in domhelpers.findNodesNamed(dom, hname):
+ if domhelpers.findNodesNamed(node, 'a'):
+ self._reportError(filename, node, 'anchor in heading')
+
+ def check_texturl_matches_href(self, dom, filename):
+ for node in domhelpers.findNodesNamed(dom, 'a'):
+ if not node.hasAttribute('href'):
+ continue
+ text = domhelpers.getNodeText(node)
+ proto = urlparse.urlparse(text)[0]
+ if proto and ' ' not in text:
+ if text != node.getAttribute('href'):
+ self._reportError(filename, node,
+ 'link text does not match href')
+
+ def check_lists(self, dom, filename):
+ for node in (domhelpers.findNodesNamed(dom, 'ul')+
+ domhelpers.findNodesNamed(dom, 'ol')):
+ if not node.childNodes:
+ self._reportError(filename, node, 'empty list')
+ for child in node.childNodes:
+ if child.nodeName != 'li':
+ self._reportError(filename, node,
+ 'only list items allowed in lists')
+
+
+def list2dict(l):
+ d = {}
+ for el in l:
+ d[el] = None
+ return d
+
+classes = list2dict(['shell', 'API', 'python', 'py-prototype', 'py-filename',
+ 'py-src-string', 'py-signature', 'py-src-parameter',
+ 'py-src-identifier', 'py-src-keyword'])
+
+tags = list2dict(["html", "title", "head", "body", "h1", "h2", "h3", "ol", "ul",
+ "dl", "li", "dt", "dd", "p", "code", "img", "blockquote", "a",
+ "cite", "div", "span", "strong", "em", "pre", "q", "table",
+ "tr", "td", "th", "style", "sub", "sup", "link"])
+
+span = list2dict(['footnote', 'manhole-output', 'index'])
+
+div = list2dict(['note', 'boxed', 'doit'])
+
+a = list2dict(['listing', 'py-listing', 'html-listing', 'absolute'])
+
+pre = list2dict(['python', 'shell', 'python-interpreter', 'elisp'])
+
+allowed = {'code': classes.has_key, 'span': span.has_key, 'div': div.has_key,
+ 'a': a.has_key, 'pre': pre.has_key, 'ul': lambda x: x=='toc',
+ 'ol': lambda x: x=='toc', 'li': lambda x: x=='ignoretoc'}
+
+def getDefaultChecker():
+ return DefaultTagChecker(tags.has_key, allowed)
+
+def doFile(file, checker):
+ doc = tree.parseFileAndReport(file)
+ if doc:
+ checker.check(doc, file)
diff --git a/twisted/lore/lmath.py b/twisted/lore/lmath.py
new file mode 100644
index 0000000..fbd7e20
--- /dev/null
+++ b/twisted/lore/lmath.py
@@ -0,0 +1,85 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+LaTeX-defined image support for Lore documents.
+"""
+
+import os, tempfile
+from xml.dom import minidom as dom
+
+from twisted.web import domhelpers
+import latex, tree, lint, default
+
+
+class MathLatexSpitter(latex.LatexSpitter):
+
+ start_html = '\\documentclass{amsart}\n'
+
+ def visitNode_div_latexmacros(self, node):
+ self.writer(domhelpers.getNodeText(node))
+
+ def visitNode_span_latexformula(self, node):
+ self.writer('\[')
+ self.writer(domhelpers.getNodeText(node))
+ self.writer('\]')
+
+def formulaeToImages(document, dir, _system=os.system):
+ # gather all macros
+ macros = ''
+ for node in domhelpers.findElementsWithAttribute(document, 'class',
+ 'latexmacros'):
+ macros += domhelpers.getNodeText(node)
+ node.parentNode.removeChild(node)
+ i = 0
+ for node in domhelpers.findElementsWithAttribute(document, 'class',
+ 'latexformula'):
+ latexText='''\\documentclass[12pt]{amsart}%s
+ \\begin{document}\[%s\]
+ \\end{document}''' % (macros, domhelpers.getNodeText(node))
+ # This file really should be cleaned up by this function, or placed
+ # somewhere such that the calling code can find it and clean it up.
+ file = tempfile.mktemp()
+ f = open(file+'.tex', 'w')
+ f.write(latexText)
+ f.close()
+ _system('latex %s.tex' % file)
+ _system('dvips %s.dvi -o %s.ps' % (os.path.basename(file), file))
+ baseimgname = 'latexformula%d.png' % i
+ imgname = os.path.join(dir, baseimgname)
+ i += 1
+ _system('pstoimg -type png -crop a -trans -interlace -out '
+ '%s %s.ps' % (imgname, file))
+ newNode = dom.parseString(
+ '<span><br /><img src="%s" /><br /></span>' % (
+ baseimgname,)).documentElement
+ node.parentNode.replaceChild(newNode, node)
+
+
+def doFile(fn, docsdir, ext, url, templ, linkrel='', d=None):
+ d = d or {}
+ doc = tree.parseFileAndReport(fn)
+ formulaeToImages(doc, os.path.dirname(fn))
+ cn = templ.cloneNode(1)
+ tree.munge(doc, cn, linkrel, docsdir, fn, ext, url, d)
+ cn.writexml(open(os.path.splitext(fn)[0]+ext, 'wb'))
+
+
+class ProcessingFunctionFactory(default.ProcessingFunctionFactory):
+
+ latexSpitters = {None: MathLatexSpitter}
+
+ def getDoFile(self):
+ return doFile
+
+ def getLintChecker(self):
+ checker = lint.getDefaultChecker()
+ checker.allowedClasses = checker.allowedClasses.copy()
+ oldDiv = checker.allowedClasses['div']
+ oldSpan = checker.allowedClasses['span']
+ checker.allowedClasses['div'] = lambda x:oldDiv(x) or x=='latexmacros'
+ checker.allowedClasses['span'] = (lambda x:oldSpan(x) or
+ x=='latexformula')
+ return checker
+
+factory = ProcessingFunctionFactory()
diff --git a/twisted/lore/man2lore.py b/twisted/lore/man2lore.py
new file mode 100644
index 0000000..fbcba1c
--- /dev/null
+++ b/twisted/lore/man2lore.py
@@ -0,0 +1,295 @@
+# -*- test-case-name: twisted.lore.test.test_man2lore -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+man2lore: Converts man page source (i.e. groff) into lore-compatible html.
+
+This is nasty and hackish (and doesn't support lots of real groff), but is good
+enough for converting fairly simple man pages.
+"""
+
+import re, os
+
+quoteRE = re.compile('"(.*?)"')
+
+
+
+def escape(text):
+ text = text.replace('<', '&lt;').replace('>', '&gt;')
+ text = quoteRE.sub('<q>\\1</q>', text)
+ return text
+
+
+
+def stripQuotes(s):
+ if s[0] == s[-1] == '"':
+ s = s[1:-1]
+ return s
+
+
+
+class ManConverter(object):
+ """
+ Convert a man page to the Lore format.
+
+ @ivar tp: State variable for handling text inside a C{TP} token. It can
+ take values from 0 to 3:
+ - 0: when outside of a C{TP} token.
+ - 1: once a C{TP} token has been encountered. If the previous value
+ was 0, a definition list is started. Then, at the first line of
+ text, a definition term is started.
+ - 2: when the first line after the C{TP} token has been handled.
+ The definition term is closed, and a definition is started with
+ the next line of text.
+ - 3: when the first line as definition data has been handled.
+ @type tp: C{int}
+ """
+ state = 'regular'
+ name = None
+ tp = 0
+ dl = 0
+ para = 0
+
+ def convert(self, inf, outf):
+ self.write = outf.write
+ longline = ''
+ for line in inf.readlines():
+ if line.rstrip() and line.rstrip()[-1] == '\\':
+ longline += line.rstrip()[:-1] + ' '
+ continue
+ if longline:
+ line = longline + line
+ longline = ''
+ self.lineReceived(line)
+ self.closeTags()
+ self.write('</body>\n</html>\n')
+ outf.flush()
+
+
+ def lineReceived(self, line):
+ if line[0] == '.':
+ f = getattr(self, 'macro_' + line[1:3].rstrip().upper(), None)
+ if f:
+ f(line[3:].strip())
+ else:
+ self.text(line)
+
+
+ def continueReceived(self, cont):
+ if not cont:
+ return
+ if cont[0].isupper():
+ f = getattr(self, 'macro_' + cont[:2].rstrip().upper(), None)
+ if f:
+ f(cont[2:].strip())
+ else:
+ self.text(cont)
+
+
+ def closeTags(self):
+ if self.state != 'regular':
+ self.write('</%s>' % self.state)
+ if self.tp == 3:
+ self.write('</dd>\n\n')
+ self.tp = 0
+ if self.dl:
+ self.write('</dl>\n\n')
+ self.dl = 0
+ if self.para:
+ self.write('</p>\n\n')
+ self.para = 0
+
+
+ def paraCheck(self):
+ if not self.tp and not self.para:
+ self.write('<p>')
+ self.para = 1
+
+
+ def macro_TH(self, line):
+ self.write(
+ '<?xml version="1.0"?>\n'
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n'
+ ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
+ self.write('<html><head>\n')
+ parts = [stripQuotes(x) for x in line.split(' ', 2)] + ['', '']
+ title, manSection = parts[:2]
+ self.write('<title>%s.%s</title>' % (title, manSection))
+ self.write('</head>\n<body>\n\n')
+ self.write('<h1>%s.%s</h1>\n\n' % (title, manSection))
+
+ macro_DT = macro_TH
+
+
+ def macro_SH(self, line):
+ self.closeTags()
+ self.write('<h2>')
+ self.para = 1
+ self.text(stripQuotes(line))
+ self.para = 0
+ self.closeTags()
+ self.write('</h2>\n\n')
+
+
+ def macro_B(self, line):
+ words = line.split()
+ words[0] = '\\fB' + words[0] + '\\fR '
+ self.text(' '.join(words))
+
+
+ def macro_NM(self, line):
+ if not self.name:
+ self.name = line
+ self.text(self.name + ' ')
+
+
+ def macro_NS(self, line):
+ parts = line.split(' Ns ')
+ i = 0
+ for l in parts:
+ i = not i
+ if i:
+ self.text(l)
+ else:
+ self.continueReceived(l)
+
+
+ def macro_OO(self, line):
+ self.text('[')
+ self.continueReceived(line)
+
+
+ def macro_OC(self, line):
+ self.text(']')
+ self.continueReceived(line)
+
+
+ def macro_OP(self, line):
+ self.text('[')
+ self.continueReceived(line)
+ self.text(']')
+
+
+ def macro_FL(self, line):
+ parts = line.split()
+ self.text('\\fB-%s\\fR' % parts[0])
+ self.continueReceived(' '.join(parts[1:]))
+
+
+ def macro_AR(self, line):
+ parts = line.split()
+ self.text('\\fI %s\\fR' % parts[0])
+ self.continueReceived(' '.join(parts[1:]))
+
+
+ def macro_PP(self, line):
+ self.closeTags()
+
+
+ def macro_IC(self, line):
+ cmd = line.split(' ', 1)[0]
+ args = line[line.index(cmd) + len(cmd):]
+ args = args.split(' ')
+ text = cmd
+ while args:
+ arg = args.pop(0)
+ if arg.lower() == "ar":
+ text += " \\fU%s\\fR" % (args.pop(0),)
+ elif arg.lower() == "op":
+ ign = args.pop(0)
+ text += " [\\fU%s\\fR]" % (args.pop(0),)
+
+ self.text(text)
+
+
+ def macro_TP(self, line):
+ """
+ Handle C{TP} token: start a definition list if it's first token, or
+ close previous definition data.
+ """
+ if self.tp == 3:
+ self.write('</dd>\n\n')
+ self.tp = 1
+ else:
+ self.tp = 1
+ self.write('<dl>')
+ self.dl = 1
+
+
+ def macro_BL(self, line):
+ self.write('<dl>')
+ self.tp = 1
+
+
+ def macro_EL(self, line):
+ if self.tp == 3:
+ self.write('</dd>')
+ self.tp = 1
+ self.write('</dl>\n\n')
+ self.tp = 0
+
+
+ def macro_IT(self, line):
+ if self.tp == 3:
+ self.write('</dd>')
+ self.tp = 1
+ self.continueReceived(line)
+
+
+ def text(self, line):
+ """
+ Handle a line of text without detected token.
+ """
+ if self.tp == 1:
+ self.write('<dt>')
+ if self.tp == 2:
+ self.write('<dd>')
+ self.paraCheck()
+
+ bits = line.split('\\')
+ self.write(escape(bits[0]))
+ for bit in bits[1:]:
+ if bit[:2] == 'fI':
+ self.write('<em>' + escape(bit[2:]))
+ self.state = 'em'
+ elif bit[:2] == 'fB':
+ self.write('<strong>' + escape(bit[2:]))
+ self.state = 'strong'
+ elif bit[:2] == 'fR':
+ self.write('</%s>' % self.state)
+ self.write(escape(bit[2:]))
+ self.state = 'regular'
+ elif bit[:2] == 'fU':
+ # fU doesn't really exist, but it helps us to manage underlined
+ # text.
+ self.write('<u>' + escape(bit[2:]))
+ self.state = 'u'
+ elif bit[:3] == '(co':
+ self.write('&copy;' + escape(bit[3:]))
+ else:
+ self.write(escape(bit))
+
+ if self.tp == 1:
+ self.write('</dt>')
+ self.tp = 2
+ elif self.tp == 2:
+ self.tp = 3
+
+
+
+class ProcessingFunctionFactory:
+
+ def generate_lore(self, d, filenameGenerator=None):
+ ext = d.get('ext', '.html')
+ return lambda file,_: ManConverter().convert(open(file),
+ open(os.path.splitext(file)[0]+ext, 'w'))
+
+
+
+factory = ProcessingFunctionFactory()
+
+
+if __name__ == '__main__':
+ import sys
+ mc = ManConverter().convert(open(sys.argv[1]), sys.stdout)
diff --git a/twisted/lore/numberer.py b/twisted/lore/numberer.py
new file mode 100644
index 0000000..f91cc28
--- /dev/null
+++ b/twisted/lore/numberer.py
@@ -0,0 +1,33 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+def reset():
+ resetFilenum()
+ setNumberSections(False)
+
+def resetFilenum():
+ setFilenum(0)
+
+def setFilenum(arg):
+ global filenum
+ filenum = arg
+
+def getFilenum():
+ global filenum
+ return filenum
+
+def getNextFilenum():
+ global filenum
+ filenum += 1
+ return filenum
+
+def setNumberSections(arg):
+ global numberSections
+ numberSections = arg
+
+def getNumberSections():
+ global numberSections
+ return numberSections
+
+reset()
diff --git a/twisted/lore/process.py b/twisted/lore/process.py
new file mode 100644
index 0000000..ec5d036
--- /dev/null
+++ b/twisted/lore/process.py
@@ -0,0 +1,120 @@
+# -*- test-case-name: twisted.lore.test.test_lore -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+import sys, os
+import tree #todo: get rid of this later
+import indexer
+
+class NoProcessorError(Exception):
+ pass
+
+class ProcessingFailure(Exception):
+ pass
+
+cols = 79
+
+def dircount(d):
+ return len([1 for el in d.split("/") if el != '.'])
+
+
+class Walker:
+
+ def __init__(self, df, fext, linkrel):
+ self.df = df
+ self.linkrel = linkrel
+ self.fext = fext
+ self.walked = []
+ self.failures = []
+
+ def walkdir(self, topdir, prefix=''):
+ self.basecount = dircount(topdir)
+ os.path.walk(topdir, self.walk, prefix)
+
+ def walk(self, prefix, d, names):
+ linkrel = prefix + '../' * (dircount(d) - self.basecount)
+ for name in names:
+ fullpath = os.path.join(d, name)
+ fext = os.path.splitext(name)[1]
+ if fext == self.fext:
+ self.walked.append((linkrel, fullpath))
+
+ def generate(self):
+ i = 0
+ indexer.clearEntries()
+ tree.filenum = 0
+ for linkrel, fullpath in self.walked:
+ linkrel = self.linkrel + linkrel
+ i += 1
+ fname = os.path.splitext(fullpath)[0]
+ self.percentdone((float(i) / len(self.walked)), fname)
+ try:
+ self.df(fullpath, linkrel)
+ except ProcessingFailure, e:
+ self.failures.append((fullpath, e))
+ indexer.generateIndex()
+ self.percentdone(1., None)
+
+ def percentdone(self, percent, fname):
+ # override for neater progress bars
+ proglen = 40
+ hashes = int(percent * proglen)
+ spaces = proglen - hashes
+ progstat = "[%s%s] (%s)" %('#' * hashes, ' ' * spaces,fname or "*Done*")
+ progstat += (cols - len(progstat)) * ' '
+ progstat += '\r'
+ sys.stdout.write(progstat)
+ sys.stdout.flush()
+ if fname is None:
+ print
+
+class PlainReportingWalker(Walker):
+
+ def percentdone(self, percent, fname):
+ if fname:
+ print fname
+
+class NullReportingWalker(Walker):
+
+ def percentdone(self, percent, fname):
+ pass
+
+def parallelGenerator(originalFileName, outputExtension):
+ return os.path.splitext(originalFileName)[0]+outputExtension
+
+def fooAddingGenerator(originalFileName, outputExtension):
+ return os.path.splitext(originalFileName)[0]+"foo"+outputExtension
+
+def outputdirGenerator(originalFileName, outputExtension, inputdir, outputdir):
+ originalFileName = os.path.abspath(originalFileName)
+ abs_inputdir = os.path.abspath(inputdir)
+ if os.path.commonprefix((originalFileName, abs_inputdir)) != abs_inputdir:
+ raise ValueError("Original file name '" + originalFileName +
+ "' not under input directory '" + abs_inputdir + "'")
+
+ adjustedPath = os.path.join(outputdir, os.path.basename(originalFileName))
+ return tree.getOutputFileName(adjustedPath, outputExtension)
+
+def getFilenameGenerator(config, outputExt):
+ if config.get('outputdir'):
+ return (lambda originalFileName, outputExtension:
+ outputdirGenerator(originalFileName, outputExtension,
+ os.path.abspath(config.get('inputdir')),
+ os.path.abspath(config.get('outputdir'))))
+ else:
+ return tree.getOutputFileName
+
+def getProcessor(module, output, config):
+ try:
+ m = getattr(module.factory, 'generate_'+output)
+ except AttributeError:
+ raise NoProcessorError("cannot generate "+output+" output")
+
+ if config.get('ext'):
+ ext = config['ext']
+ else:
+ from default import htmlDefault
+ ext = htmlDefault['ext']
+
+ return m(config, getFilenameGenerator(config, ext))
diff --git a/twisted/lore/scripts/__init__.py b/twisted/lore/scripts/__init__.py
new file mode 100644
index 0000000..265270e
--- /dev/null
+++ b/twisted/lore/scripts/__init__.py
@@ -0,0 +1 @@
+"lore scripts"
diff --git a/twisted/lore/scripts/lore.py b/twisted/lore/scripts/lore.py
new file mode 100755
index 0000000..c82b2c6
--- /dev/null
+++ b/twisted/lore/scripts/lore.py
@@ -0,0 +1,155 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+from zope.interface import Interface, Attribute
+
+from twisted.lore import process, indexer, numberer, htmlbook
+
+from twisted.python import usage, reflect
+from twisted import plugin as plugin
+
+class IProcessor(Interface):
+ """
+ """
+
+ name = Attribute("The user-facing name of this processor")
+
+ moduleName = Attribute(
+ "The fully qualified Python name of the object defining "
+ "this processor. This object (typically a module) should "
+ "have a C{factory} attribute with C{generate_<output>} methods.")
+
+
+class Options(usage.Options):
+
+ longdesc = "lore converts documentation formats."
+
+ optFlags = [["plain", 'p', "Report filenames without progress bar"],
+ ["null", 'n', "Do not report filenames"],
+ ["number", 'N', "Add chapter/section numbers to section headings"],
+]
+
+ optParameters = [
+ ["input", "i", 'lore'],
+ ["inputext", "e", ".xhtml", "The extension that your Lore input files have"],
+ ["docsdir", "d", None],
+ ["linkrel", "l", ''],
+ ["output", "o", 'html'],
+ ["index", "x", None, "The base filename you want to give your index file"],
+ ["book", "b", None, "The book file to generate a book from"],
+ ["prefixurl", None, "", "The prefix to stick on to relative links; only useful when processing directories"],
+ ]
+
+ compData = usage.Completions(
+ extraActions=[usage.CompleteFiles(descr="files", repeat=True)])
+
+ def __init__(self, *args, **kw):
+ usage.Options.__init__(self, *args, **kw)
+ self.config = {}
+
+ def opt_config(self, s):
+ if '=' in s:
+ k, v = s.split('=', 1)
+ self.config[k] = v
+ else:
+ self.config[s] = 1
+
+ def parseArgs(self, *files):
+ self['files'] = files
+
+
+def getProcessor(input, output, config):
+ plugins = plugin.getPlugins(IProcessor)
+ for plug in plugins:
+ if plug.name == input:
+ module = reflect.namedModule(plug.moduleName)
+ break
+ else:
+ # try treating it as a module name
+ try:
+ module = reflect.namedModule(input)
+ except ImportError:
+ print '%s: no such input: %s' % (sys.argv[0], input)
+ return
+ try:
+ return process.getProcessor(module, output, config)
+ except process.NoProcessorError, e:
+ print "%s: %s" % (sys.argv[0], e)
+
+
+def getWalker(df, opt):
+ klass = process.Walker
+ if opt['plain']:
+ klass = process.PlainReportingWalker
+ if opt['null']:
+ klass = process.NullReportingWalker
+ return klass(df, opt['inputext'], opt['linkrel'])
+
+
+def runGivenOptions(opt):
+ """Do everything but parse the options; useful for testing.
+ Returns a descriptive string if there's an error."""
+
+ book = None
+ if opt['book']:
+ book = htmlbook.Book(opt['book'])
+
+ df = getProcessor(opt['input'], opt['output'], opt.config)
+ if not df:
+ return 'getProcessor() failed'
+
+ walker = getWalker(df, opt)
+
+ if opt['files']:
+ for filename in opt['files']:
+ walker.walked.append(('', filename))
+ elif book:
+ for filename in book.getFiles():
+ walker.walked.append(('', filename))
+ else:
+ walker.walkdir(opt['docsdir'] or '.', opt['prefixurl'])
+
+ if opt['index']:
+ indexFilename = opt['index']
+ elif book:
+ indexFilename = book.getIndexFilename()
+ else:
+ indexFilename = None
+
+ if indexFilename:
+ indexer.setIndexFilename("%s.%s" % (indexFilename, opt['output']))
+ else:
+ indexer.setIndexFilename(None)
+
+ ## TODO: get numberSections from book, if any
+ numberer.setNumberSections(opt['number'])
+
+ walker.generate()
+
+ if walker.failures:
+ for (file, errors) in walker.failures:
+ for error in errors:
+ print "%s:%s" % (file, error)
+ return 'Walker failures'
+
+
+def run():
+ opt = Options()
+ try:
+ opt.parseOptions()
+ except usage.UsageError, errortext:
+ print '%s: %s' % (sys.argv[0], errortext)
+ print '%s: Try --help for usage details.' % sys.argv[0]
+ sys.exit(1)
+
+ result = runGivenOptions(opt)
+ if result:
+ print result
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ run()
+
diff --git a/twisted/lore/slides.py b/twisted/lore/slides.py
new file mode 100644
index 0000000..fcddbc5
--- /dev/null
+++ b/twisted/lore/slides.py
@@ -0,0 +1,359 @@
+# -*- test-case-name: twisted.lore.test.test_slides -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Rudimentary slide support for Lore.
+
+TODO:
+ - Complete mgp output target
+ - syntax highlighting
+ - saner font handling
+ - probably lots more
+ - Add HTML output targets
+ - one slides per page (with navigation links)
+ - all in one page
+
+Example input file::
+ <html>
+
+ <head><title>Title of talk</title></head>
+
+ <body>
+ <h1>Title of talk</h1>
+
+ <h2>First Slide</h2>
+
+ <ul>
+ <li>Bullet point</li>
+ <li>Look ma, I'm <strong>bold</strong>!</li>
+ <li>... etc ...</li>
+ </ul>
+
+
+ <h2>Second Slide</h2>
+
+ <pre class="python">
+ # Sample code sample.
+ print "Hello, World!"
+ </pre>
+
+ </body>
+
+ </html>
+"""
+
+from xml.dom import minidom as dom
+import os.path, re
+from cStringIO import StringIO
+
+from twisted.lore import default
+from twisted.web import domhelpers
+from twisted.python import text
+# These should be factored out
+from twisted.lore.latex import BaseLatexSpitter, LatexSpitter, processFile
+from twisted.lore.latex import getLatexText, HeadingLatexSpitter
+from twisted.lore.tree import getHeaders
+from twisted.lore.tree import removeH1, fixAPI, fontifyPython
+from twisted.lore.tree import addPyListings, addHTMLListings, setTitle
+
+hacked_entities = { 'amp': ' &', 'gt': ' >', 'lt': ' <', 'quot': ' "',
+ 'copy': ' (c)'}
+
+entities = { 'amp': '&', 'gt': '>', 'lt': '<', 'quot': '"',
+ 'copy': '(c)'}
+
+class MagicpointOutput(BaseLatexSpitter):
+ bulletDepth = 0
+
+ def writeNodeData(self, node):
+ buf = StringIO()
+ getLatexText(node, buf.write, entities=hacked_entities)
+ data = buf.getvalue().rstrip().replace('\n', ' ')
+ self.writer(re.sub(' +', ' ', data))
+
+ def visitNode_title(self, node):
+ self.title = domhelpers.getNodeText(node)
+
+ def visitNode_body(self, node):
+ # Adapted from tree.generateToC
+ self.fontStack = [('standard', None)]
+
+ # Title slide
+ self.writer(self.start_h2)
+ self.writer(self.title)
+ self.writer(self.end_h2)
+
+ self.writer('%center\n\n\n\n\n')
+ for authorNode in domhelpers.findElementsWithAttribute(node, 'class', 'author'):
+ getLatexText(authorNode, self.writer, entities=entities)
+ self.writer('\n')
+
+ # Table of contents
+ self.writer(self.start_h2)
+ self.writer(self.title)
+ self.writer(self.end_h2)
+
+ for element in getHeaders(node):
+ level = int(element.tagName[1])-1
+ self.writer(level * '\t')
+ self.writer(domhelpers.getNodeText(element))
+ self.writer('\n')
+
+ self.visitNodeDefault(node)
+
+ def visitNode_div_author(self, node):
+ # Skip this node; it's already been used by visitNode_body
+ pass
+
+ def visitNode_div_pause(self, node):
+ self.writer('%pause\n')
+
+ def visitNode_pre(self, node):
+ # TODO: Syntax highlighting
+ buf = StringIO()
+ getLatexText(node, buf.write, entities=entities)
+ data = buf.getvalue()
+ data = text.removeLeadingTrailingBlanks(data)
+ lines = data.split('\n')
+ self.fontStack.append(('typewriter', 4))
+ self.writer('%' + self.fontName() + '\n')
+ for line in lines:
+ self.writer(' ' + line + '\n')
+ del self.fontStack[-1]
+ self.writer('%' + self.fontName() + '\n')
+
+ def visitNode_ul(self, node):
+ if self.bulletDepth > 0:
+ self.writer(self._start_ul)
+ self.bulletDepth += 1
+ self.start_li = self._start_li * self.bulletDepth
+ self.visitNodeDefault(node)
+ self.bulletDepth -= 1
+ self.start_li = self._start_li * self.bulletDepth
+
+ def visitNode_strong(self, node):
+ self.doFont(node, 'bold')
+
+ def visitNode_em(self, node):
+ self.doFont(node, 'italic')
+
+ def visitNode_code(self, node):
+ self.doFont(node, 'typewriter')
+
+ def doFont(self, node, style):
+ self.fontStack.append((style, None))
+ self.writer(' \n%cont, ' + self.fontName() + '\n')
+ self.visitNodeDefault(node)
+ del self.fontStack[-1]
+ self.writer('\n%cont, ' + self.fontName() + '\n')
+
+ def fontName(self):
+ names = [x[0] for x in self.fontStack]
+ if 'typewriter' in names:
+ name = 'typewriter'
+ else:
+ name = ''
+
+ if 'bold' in names:
+ name += 'bold'
+ if 'italic' in names:
+ name += 'italic'
+
+ if name == '':
+ name = 'standard'
+
+ sizes = [x[1] for x in self.fontStack]
+ sizes.reverse()
+ for size in sizes:
+ if size:
+ return 'font "%s", size %d' % (name, size)
+
+ return 'font "%s"' % name
+
+ start_h2 = "%page\n\n"
+ end_h2 = '\n\n\n'
+
+ _start_ul = '\n'
+
+ _start_li = "\t"
+ end_li = "\n"
+
+
+def convertFile(filename, outputter, template, ext=".mgp"):
+ fout = open(os.path.splitext(filename)[0]+ext, 'w')
+ fout.write(open(template).read())
+ spitter = outputter(fout.write, os.path.dirname(filename), filename)
+ fin = open(filename)
+ processFile(spitter, fin)
+ fin.close()
+ fout.close()
+
+
+# HTML DOM tree stuff
+
+def splitIntoSlides(document):
+ body = domhelpers.findNodesNamed(document, 'body')[0]
+ slides = []
+ slide = []
+ title = '(unset)'
+ for child in body.childNodes:
+ if isinstance(child, dom.Element) and child.tagName == 'h2':
+ if slide:
+ slides.append((title, slide))
+ slide = []
+ title = domhelpers.getNodeText(child)
+ else:
+ slide.append(child)
+ slides.append((title, slide))
+ return slides
+
+def insertPrevNextLinks(slides, filename, ext):
+ for slide in slides:
+ for name, offset in (("previous", -1), ("next", +1)):
+ if (slide.pos > 0 and name == "previous") or \
+ (slide.pos < len(slides)-1 and name == "next"):
+ for node in domhelpers.findElementsWithAttribute(slide.dom, "class", name):
+ if node.tagName == 'a':
+ node.setAttribute('href', '%s-%d%s'
+ % (filename[0], slide.pos+offset, ext))
+ else:
+ text = dom.Text()
+ text.data = slides[slide.pos+offset].title
+ node.appendChild(text)
+ else:
+ for node in domhelpers.findElementsWithAttribute(slide.dom, "class", name):
+ pos = 0
+ for child in node.parentNode.childNodes:
+ if child is node:
+ del node.parentNode.childNodes[pos]
+ break
+ pos += 1
+
+
+class HTMLSlide:
+ def __init__(self, dom, title, pos):
+ self.dom = dom
+ self.title = title
+ self.pos = pos
+
+
+def munge(document, template, linkrel, d, fullpath, ext, url, config):
+ # FIXME: This has *way* to much duplicated crap in common with tree.munge
+ #fixRelativeLinks(template, linkrel)
+ removeH1(document)
+ fixAPI(document, url)
+ fontifyPython(document)
+ addPyListings(document, d)
+ addHTMLListings(document, d)
+ #fixLinks(document, ext)
+ #putInToC(template, generateToC(document))
+ template = template.cloneNode(1)
+
+ # Insert the slides into the template
+ slides = []
+ pos = 0
+ for title, slide in splitIntoSlides(document):
+ t = template.cloneNode(1)
+ text = dom.Text()
+ text.data = title
+ setTitle(t, [text])
+ tmplbody = domhelpers.findElementsWithAttribute(t, "class", "body")[0]
+ tmplbody.childNodes = slide
+ tmplbody.setAttribute("class", "content")
+ # FIXME: Next/Prev links
+ # FIXME: Perhaps there should be a "Template" class? (setTitle/setBody
+ # could be methods...)
+ slides.append(HTMLSlide(t, title, pos))
+ pos += 1
+
+ insertPrevNextLinks(slides, os.path.splitext(os.path.basename(fullpath)), ext)
+
+ return slides
+
+from tree import makeSureDirectoryExists
+
+def getOutputFileName(originalFileName, outputExtension, index):
+ return os.path.splitext(originalFileName)[0]+'-'+str(index) + outputExtension
+
+def doFile(filename, linkrel, ext, url, templ, options={}, outfileGenerator=getOutputFileName):
+ from tree import parseFileAndReport
+ doc = parseFileAndReport(filename)
+ slides = munge(doc, templ, linkrel, os.path.dirname(filename), filename, ext, url, options)
+ for slide, index in zip(slides, range(len(slides))):
+ newFilename = outfileGenerator(filename, ext, index)
+ makeSureDirectoryExists(newFilename)
+ f = open(newFilename, 'wb')
+ slide.dom.writexml(f)
+ f.close()
+
+# Prosper output
+
+class ProsperSlides(LatexSpitter):
+ firstSlide = 1
+ start_html = '\\documentclass[ps]{prosper}\n'
+ start_body = '\\begin{document}\n'
+ start_div_author = '\\author{'
+ end_div_author = '}'
+
+ def visitNode_h2(self, node):
+ if self.firstSlide:
+ self.firstSlide = 0
+ self.end_body = '\\end{slide}\n\n' + self.end_body
+ else:
+ self.writer('\\end{slide}\n\n')
+ self.writer('\\begin{slide}{')
+ spitter = HeadingLatexSpitter(self.writer, self.currDir, self.filename)
+ spitter.visitNodeDefault(node)
+ self.writer('}')
+
+ def _write_img(self, target):
+ self.writer('\\begin{center}\\includegraphics[%%\nwidth=1.0\n\\textwidth,'
+ 'height=1.0\\textheight,\nkeepaspectratio]{%s}\\end{center}\n' % target)
+
+
+class PagebreakLatex(LatexSpitter):
+
+ everyN = 1
+ currentN = 0
+ seenH2 = 0
+
+ start_html = LatexSpitter.start_html+"\\date{}\n"
+ start_body = '\\begin{document}\n\n'
+
+ def visitNode_h2(self, node):
+ if not self.seenH2:
+ self.currentN = 0
+ self.seenH2 = 1
+ else:
+ self.currentN += 1
+ self.currentN %= self.everyN
+ if not self.currentN:
+ self.writer('\\clearpage\n')
+ level = (int(node.tagName[1])-2)+self.baseLevel
+ self.writer('\n\n\\'+level*'sub'+'section*{')
+ spitter = HeadingLatexSpitter(self.writer, self.currDir, self.filename)
+ spitter.visitNodeDefault(node)
+ self.writer('}\n')
+
+class TwoPagebreakLatex(PagebreakLatex):
+
+ everyN = 2
+
+
+class SlidesProcessingFunctionFactory(default.ProcessingFunctionFactory):
+
+ latexSpitters = default.ProcessingFunctionFactory.latexSpitters.copy()
+ latexSpitters['prosper'] = ProsperSlides
+ latexSpitters['page'] = PagebreakLatex
+ latexSpitters['twopage'] = TwoPagebreakLatex
+
+ def getDoFile(self):
+ return doFile
+
+ def generate_mgp(self, d, fileNameGenerator=None):
+ template = d.get('template', 'template.mgp')
+ df = lambda file, linkrel: convertFile(file, MagicpointOutput, template, ext=".mgp")
+ return df
+
+factory=SlidesProcessingFunctionFactory()
diff --git a/twisted/lore/template.mgp b/twisted/lore/template.mgp
new file mode 100644
index 0000000..79fc4d1
--- /dev/null
+++ b/twisted/lore/template.mgp
@@ -0,0 +1,24 @@
+%%deffont "standard" tfont "Arial.ttf"
+%%deffont "bold" tfont "Arial_Bold.ttf"
+%%deffont "italic" tfont "Arial_Italic.ttf"
+%%deffont "bolditalic" tfont "Arial_Bold_Italic.ttf"
+%%deffont "typewriter" tfont "Courier_New.ttf"
+%deffont "standard" xfont "Arial-medium-r"
+%deffont "bold" xfont "Arial-bold-r"
+%deffont "italic" xfont "Arial-medium-i"
+%deffont "bolditalic" xfont "Arial-bold-i"
+%deffont "typewriter" xfont "andale mono"
+%%deffont "standard" tfont "tahoma.ttf"
+%%deffont "thick" tfont "tahomabd.ttf"
+#%deffont "typewriter" tfont "Andale_Mono.ttf"
+%default 1 area 90 90, leftfill, size 2, fore "white", back "black", font "bold"
+%default 2 size 7, vgap 10, prefix " ", center
+%default 3 size 2, bar "gray70", vgap 10, leftfill
+%default 4 size 1, fore "white", vgap 30, prefix " ", font "standard"
+%default 5 size 5
+%%tab 1 size 5, vgap 40, prefix " ", icon box "green" 50
+%%tab 2 size 4, vgap 40, prefix " ", icon arc "yellow" 50
+%%tab 3 size 3, vgap 40, prefix " ", icon delta3 "white" 40
+%tab 1 size 5, vgap 50, prefix " ", icon box "green" 50
+%tab 2 size 5, vgap 50, prefix " ", icon arc "yellow" 50
+%tab 3 size 4, vgap 50, prefix " ", icon delta3 "white" 40
diff --git a/twisted/lore/test/__init__.py b/twisted/lore/test/__init__.py
new file mode 100644
index 0000000..1641a43
--- /dev/null
+++ b/twisted/lore/test/__init__.py
@@ -0,0 +1 @@
+"lore tests"
diff --git a/twisted/lore/test/lore_index_file_out.html b/twisted/lore/test/lore_index_file_out.html
new file mode 100644
index 0000000..0490f0c
--- /dev/null
+++ b/twisted/lore/test/lore_index_file_out.html
@@ -0,0 +1,2 @@
+language of programming: <a href="lore_index_test.html#index02">1.3</a><br />
+programming language: <a href="lore_index_test.html#index01">1.2</a><br />
diff --git a/twisted/lore/test/lore_index_file_out_multiple.html b/twisted/lore/test/lore_index_file_out_multiple.html
new file mode 100644
index 0000000..fa0235e
--- /dev/null
+++ b/twisted/lore/test/lore_index_file_out_multiple.html
@@ -0,0 +1,5 @@
+aahz: <a href="lore_index_test2.html#index03">link</a>
+aahz2: <a href="lore_index_test2.html#index02">link</a>
+language of programming: <a href="lore_index_test.html#index02">link</a>
+<a href="lore_index_test2.html#index01">link</a>
+programming language: <a href="lore_index_test.html#index01">link</a>
diff --git a/twisted/lore/test/lore_index_file_unnumbered_out.html b/twisted/lore/test/lore_index_file_unnumbered_out.html
new file mode 100644
index 0000000..fa724d7
--- /dev/null
+++ b/twisted/lore/test/lore_index_file_unnumbered_out.html
@@ -0,0 +1,2 @@
+language of programming: <a href="lore_index_test.html#index02">link</a><br />
+programming language: <a href="lore_index_test.html#index01">link</a><br />
diff --git a/twisted/lore/test/lore_index_test.xhtml b/twisted/lore/test/lore_index_test.xhtml
new file mode 100644
index 0000000..570b411
--- /dev/null
+++ b/twisted/lore/test/lore_index_test.xhtml
@@ -0,0 +1,21 @@
+<html>
+<head>
+ <title>The way of the program</title>
+</head>
+
+<body>
+
+<h1>The way of the program</h1>
+
+<p>The first paragraph.</p>
+
+
+<h2>The Python programming language</h2>
+<span class="index" value="programming language" />
+<span class="index" value="language of programming" />
+
+<p>The second paragraph.</p>
+
+
+</body>
+</html>
diff --git a/twisted/lore/test/lore_index_test2.xhtml b/twisted/lore/test/lore_index_test2.xhtml
new file mode 100644
index 0000000..4214e48
--- /dev/null
+++ b/twisted/lore/test/lore_index_test2.xhtml
@@ -0,0 +1,22 @@
+<html>
+<head>
+ <title>The second page to index</title>
+</head>
+
+<body>
+
+<h1>The second page to index</h1>
+
+<p>The first paragraph of the second page.</p>
+
+
+<h2>The Jython programming language</h2>
+<span class="index" value="language of programming" />
+<span class="index" value="aahz2" />
+<span class="index" value="aahz" />
+
+<p>The second paragraph of the second page.</p>
+
+
+</body>
+</html>
diff --git a/twisted/lore/test/lore_numbering_test_out.html b/twisted/lore/test/lore_numbering_test_out.html
new file mode 100644
index 0000000..15bb2b7
--- /dev/null
+++ b/twisted/lore/test/lore_numbering_test_out.html
@@ -0,0 +1,2 @@
+<?xml version="1.0"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><title>Twisted Documentation: 1. The way of the program</title><link href="resources/stylesheet.css" type="text/css" rel="stylesheet" /></head><body bgcolor="white"><h1 class="title">1. The way of the program</h1><div class="toc"><ol><li><a href="#auto0">The Python programming language</a></li><li><a href="#auto1">Section The Second</a></li><li><a href="#auto2">Section The Third</a></li></ol></div><div class="content"><span></span><p>The first paragraph.</p><h2>1.1 The Python programming language<a name="auto0"></a></h2><a name="index01"></a><a name="index02"></a><p>The second paragraph.</p><h2>1.2 Section The Second<a name="auto1"></a></h2><p>The second section.</p><h2>1.3 Section The Third<a name="auto2"></a></h2><p>The Third section.</p></div><p><a href="theIndexFile.html">Index</a></p></body></html> \ No newline at end of file
diff --git a/twisted/lore/test/lore_numbering_test_out2.html b/twisted/lore/test/lore_numbering_test_out2.html
new file mode 100644
index 0000000..33aff77
--- /dev/null
+++ b/twisted/lore/test/lore_numbering_test_out2.html
@@ -0,0 +1,2 @@
+<?xml version="1.0"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><title>Twisted Documentation: 2. The second page to index</title><link href="resources/stylesheet.css" type="text/css" rel="stylesheet" /></head><body bgcolor="white"><h1 class="title">2. The second page to index</h1><div class="toc"><ol><li><a href="#auto0">The Jython programming language</a></li><li><a href="#auto1">Second Section</a></li><li><a href="#auto2">Third Section of Second Page</a></li></ol></div><div class="content"><span></span><p>The first paragraph of the second page.</p><h2>2.1 The Jython programming language<a name="auto0"></a></h2><a name="index01"></a><a name="index02"></a><a name="index03"></a><p>The second paragraph of the second page.</p><h2>2.2 Second Section<a name="auto1"></a></h2><p>The second section of the second page.</p><h2>2.3 Third Section of Second Page<a name="auto2"></a></h2><p>The Third section.</p></div><p><a href="theIndexFile.html">Index</a></p></body></html> \ No newline at end of file
diff --git a/twisted/lore/test/simple.html b/twisted/lore/test/simple.html
new file mode 100644
index 0000000..8d77609
--- /dev/null
+++ b/twisted/lore/test/simple.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>My Test Lore Input</title>
+</head>
+<body>
+<h1>My Test Lore Input</h1>
+<p>A Body.</p>
+</body>
+</html> \ No newline at end of file
diff --git a/twisted/lore/test/simple3.html b/twisted/lore/test/simple3.html
new file mode 100644
index 0000000..8d77609
--- /dev/null
+++ b/twisted/lore/test/simple3.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>My Test Lore Input</title>
+</head>
+<body>
+<h1>My Test Lore Input</h1>
+<p>A Body.</p>
+</body>
+</html> \ No newline at end of file
diff --git a/twisted/lore/test/simple4.html b/twisted/lore/test/simple4.html
new file mode 100644
index 0000000..8d77609
--- /dev/null
+++ b/twisted/lore/test/simple4.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>My Test Lore Input</title>
+</head>
+<body>
+<h1>My Test Lore Input</h1>
+<p>A Body.</p>
+</body>
+</html> \ No newline at end of file
diff --git a/twisted/lore/test/template.tpl b/twisted/lore/test/template.tpl
new file mode 100644
index 0000000..195f6ca
--- /dev/null
+++ b/twisted/lore/test/template.tpl
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head><title>Twisted Documentation: </title></head>
+ <body bgcolor="white">
+ <h1 class="title" />
+ <div class="body" />
+ <span class="index-link">Index</span>
+ </body>
+</html>
+
diff --git a/twisted/lore/test/test_docbook.py b/twisted/lore/test/test_docbook.py
new file mode 100644
index 0000000..4bec127
--- /dev/null
+++ b/twisted/lore/test/test_docbook.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.lore.docbook}.
+"""
+
+from xml.dom.minidom import Element, Text
+
+from twisted.trial.unittest import TestCase
+from twisted.lore.docbook import DocbookSpitter
+
+
+class DocbookSpitterTests(TestCase):
+ """
+ Tests for L{twisted.lore.docbook.DocbookSpitter}.
+ """
+ def test_li(self):
+ """
+ L{DocbookSpitter} wraps any non-I{p} elements found intside any I{li}
+ elements with I{p} elements.
+ """
+ output = []
+ spitter = DocbookSpitter(output.append)
+
+ li = Element('li')
+ li.appendChild(Element('p'))
+ text = Text()
+ text.data = 'foo bar'
+ li.appendChild(text)
+
+ spitter.visitNode(li)
+ self.assertEqual(
+ ''.join(output),
+ '<listitem><para></para><para>foo bar</para></listitem>')
diff --git a/twisted/lore/test/test_latex.py b/twisted/lore/test/test_latex.py
new file mode 100644
index 0000000..21d5029
--- /dev/null
+++ b/twisted/lore/test/test_latex.py
@@ -0,0 +1,146 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.lore.latex}.
+"""
+
+import os.path
+from xml.dom.minidom import Comment, Element, Text
+
+from twisted.python.filepath import FilePath
+from twisted.trial.unittest import TestCase
+from twisted.lore.latex import LatexSpitter, getLatexText
+
+
+class LatexHelperTests(TestCase):
+ """
+ Tests for free functions in L{twisted.lore.latex}.
+ """
+ def test_getLatexText(self):
+ """
+ L{getLatexText} calls the writer function with all of the text at or
+ beneath the given node. Non-ASCII characters are encoded using
+ UTF-8.
+ """
+ node = Element('foo')
+ text = Text()
+ text.data = u"foo \N{SNOWMAN}"
+ node.appendChild(text)
+ result = []
+ getLatexText(node, result.append)
+ self.assertEqual(result, [u"foo \N{SNOWMAN}".encode('utf-8')])
+
+
+
+class LatexSpitterTests(TestCase):
+ """
+ Tests for L{LatexSpitter}.
+ """
+ def setUp(self):
+ self.filename = self.mktemp()
+ self.output = []
+ self.spitter = LatexSpitter(self.output.append, filename=self.filename)
+
+
+ def test_head(self):
+ """
+ L{LatexSpitter.visitNode} writes out author information for each
+ I{link} element with a I{rel} attribute set to I{author}.
+ """
+ head = Element('head')
+ first = Element('link')
+ first.setAttribute('rel', 'author')
+ first.setAttribute('title', 'alice')
+ second = Element('link')
+ second.setAttribute('rel', 'author')
+ second.setAttribute('href', 'http://example.com/bob')
+ third = Element('link')
+ third.setAttribute('rel', 'author')
+ third.setAttribute('href', 'mailto:carol@example.com')
+ head.appendChild(first)
+ head.appendChild(second)
+ head.appendChild(third)
+
+ self.spitter.visitNode(head)
+
+ self.assertEqual(
+ ''.join(self.output),
+ '\\author{alice \\and $<$http://example.com/bob$>$ \\and $<$carol@example.com$>$}')
+
+
+ def test_skipComments(self):
+ """
+ L{LatexSpitter.visitNode} writes nothing to its output stream for
+ comments.
+ """
+ self.spitter.visitNode(Comment('foo'))
+ self.assertNotIn('foo', ''.join(self.output))
+
+
+ def test_anchorListing(self):
+ """
+ L{LatexSpitter.visitNode} emits a verbatim block when it encounters a
+ code listing (represented by an I{a} element with a I{listing} class).
+ """
+ path = FilePath(self.mktemp())
+ path.setContent('foo\nbar\n')
+ listing = Element('a')
+ listing.setAttribute('class', 'listing')
+ listing.setAttribute('href', path.path)
+ self.spitter.visitNode(listing)
+ self.assertEqual(
+ ''.join(self.output),
+ "\\begin{verbatim}\n"
+ "foo\n"
+ "bar\n"
+ "\\end{verbatim}\\parbox[b]{\\linewidth}{\\begin{center} --- "
+ "\\begin{em}temp\\end{em}\\end{center}}")
+
+
+ def test_anchorListingSkipLines(self):
+ """
+ When passed an I{a} element with a I{listing} class and an I{skipLines}
+ attribute, L{LatexSpitter.visitNode} emits a verbatim block which skips
+ the indicated number of lines from the beginning of the source listing.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent('foo\nbar\n')
+ listing = Element('a')
+ listing.setAttribute('class', 'listing')
+ listing.setAttribute('skipLines', '1')
+ listing.setAttribute('href', path.path)
+ self.spitter.visitNode(listing)
+ self.assertEqual(
+ ''.join(self.output),
+ "\\begin{verbatim}\n"
+ "bar\n"
+ "\\end{verbatim}\\parbox[b]{\\linewidth}{\\begin{center} --- "
+ "\\begin{em}temp\\end{em}\\end{center}}")
+
+
+ def test_anchorRef(self):
+ """
+ L{LatexSpitter.visitNode} emits a footnote when it encounters an I{a}
+ element with an I{href} attribute with a network scheme.
+ """
+ listing = Element('a')
+ listing.setAttribute('href', 'http://example.com/foo')
+ self.spitter.visitNode(listing)
+ self.assertEqual(
+ ''.join(self.output),
+ "\\footnote{http://example.com/foo}")
+
+
+ def test_anchorName(self):
+ """
+ When passed an I{a} element with a I{name} attribute,
+ L{LatexSpitter.visitNode} emits a label.
+ """
+ listing = Element('a')
+ listing.setAttribute('name', 'foo')
+ self.spitter.visitNode(listing)
+ self.assertEqual(
+ ''.join(self.output),
+ "\\label{%sHASHfoo}" % (
+ os.path.abspath(self.filename).replace('\\', '/'),))
diff --git a/twisted/lore/test/test_lint.py b/twisted/lore/test/test_lint.py
new file mode 100644
index 0000000..ac94df2
--- /dev/null
+++ b/twisted/lore/test/test_lint.py
@@ -0,0 +1,132 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.lore.lint}.
+"""
+
+import sys
+from xml.dom import minidom
+from cStringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+from twisted.lore.lint import getDefaultChecker
+from twisted.lore.process import ProcessingFailure
+
+
+
+class DefaultTagCheckerTests(TestCase):
+ """
+ Tests for L{twisted.lore.lint.DefaultTagChecker}.
+ """
+ def test_quote(self):
+ """
+ If a non-comment node contains a quote (C{'"'}), the checker returned
+ by L{getDefaultChecker} reports an error and raises
+ L{ProcessingFailure}.
+ """
+ documentSource = (
+ '<html>'
+ '<head><title>foo</title></head>'
+ '<body><h1>foo</h1><div>"</div></body>'
+ '</html>')
+ document = minidom.parseString(documentSource)
+ filename = self.mktemp()
+ checker = getDefaultChecker()
+
+ output = StringIO()
+ patch = self.patch(sys, 'stdout', output)
+ self.assertRaises(ProcessingFailure, checker.check, document, filename)
+ patch.restore()
+
+ self.assertIn("contains quote", output.getvalue())
+
+
+ def test_quoteComment(self):
+ """
+ If a comment node contains a quote (C{'"'}), the checker returned by
+ L{getDefaultChecker} does not report an error.
+ """
+ documentSource = (
+ '<html>'
+ '<head><title>foo</title></head>'
+ '<body><h1>foo</h1><!-- " --></body>'
+ '</html>')
+ document = minidom.parseString(documentSource)
+ filename = self.mktemp()
+ checker = getDefaultChecker()
+
+ output = StringIO()
+ patch = self.patch(sys, 'stdout', output)
+ checker.check(document, filename)
+ patch.restore()
+
+ self.assertEqual(output.getvalue(), "")
+
+
+ def test_aNode(self):
+ """
+ If there is an <a> tag in the document, the checker returned by
+ L{getDefaultChecker} does not report an error.
+ """
+ documentSource = (
+ '<html>'
+ '<head><title>foo</title></head>'
+ '<body><h1>foo</h1><a>A link.</a></body>'
+ '</html>')
+
+ self.assertEqual(self._lintCheck(True, documentSource), "")
+
+
+ def test_textMatchesRef(self):
+ """
+ If an I{a} node has a link with a scheme as its contained text, a
+ warning is emitted if that link does not match the value of the
+ I{href} attribute.
+ """
+ documentSource = (
+ '<html>'
+ '<head><title>foo</title></head>'
+ '<body><h1>foo</h1>'
+ '<a href="http://bar/baz">%s</a>'
+ '</body>'
+ '</html>')
+ self.assertEqual(
+ self._lintCheck(True, documentSource % ("http://bar/baz",)), "")
+ self.assertIn(
+ "link text does not match href",
+ self._lintCheck(False, documentSource % ("http://bar/quux",)))
+
+
+ def _lintCheck(self, expectSuccess, source):
+ """
+ Lint the given document source and return the output.
+
+ @param expectSuccess: A flag indicating whether linting is expected
+ to succeed or not.
+
+ @param source: The document source to lint.
+
+ @return: A C{str} of the output of linting.
+ """
+ document = minidom.parseString(source)
+ filename = self.mktemp()
+ checker = getDefaultChecker()
+
+ output = StringIO()
+ patch = self.patch(sys, 'stdout', output)
+ try:
+ try:
+ checker.check(document, filename)
+ finally:
+ patch.restore()
+ except ProcessingFailure, e:
+ if expectSuccess:
+ raise
+ else:
+ if not expectSuccess:
+ self.fail(
+ "Expected checker to fail, but it did not. "
+ "Output was: %r" % (output.getvalue(),))
+
+ return output.getvalue()
diff --git a/twisted/lore/test/test_lmath.py b/twisted/lore/test/test_lmath.py
new file mode 100644
index 0000000..a1e4c09
--- /dev/null
+++ b/twisted/lore/test/test_lmath.py
@@ -0,0 +1,72 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.lore.lmath}.
+"""
+
+from xml.dom.minidom import Element, Text
+
+from twisted.trial.unittest import TestCase
+from twisted.python.filepath import FilePath
+from twisted.lore.scripts.lore import IProcessor
+
+from twisted.plugin import getPlugins
+
+from twisted.lore.lmath import formulaeToImages
+
+
+class PluginTests(TestCase):
+ """
+ Tests for the plugin which lets L{twisted.lore.lmath} be used from the lore
+ command line tool.
+ """
+ def test_discoverable(self):
+ """
+ The plugin for L{twisted.lore.lmath} can be discovered by querying for
+ L{IProcessor} plugins.
+ """
+ plugins = getPlugins(IProcessor)
+ lmath = [p for p in plugins if p.name == "mlore"]
+ self.assertEqual(len(lmath), 1, "Did not find math lore plugin: %r" % (lmath,))
+
+
+
+class FormulaeTests(TestCase):
+ """
+ Tests for L{formulaeToImages}.
+ """
+ def test_insertImages(self):
+ """
+ L{formulaeToImages} replaces any elements with the I{latexformula}
+ class with I{img} elements which refer to external images generated
+ based on the latex in the original elements.
+ """
+ parent = Element('div')
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ macros = Element('span')
+ macros.setAttribute('class', 'latexmacros')
+ text = Text()
+ text.data = 'foo'
+ macros.appendChild(text)
+ parent.appendChild(macros)
+
+ formula = Element('span')
+ formula.setAttribute('class', 'latexformula')
+ text = Text()
+ text.data = 'bar'
+ formula.appendChild(text)
+ parent.appendChild(formula)
+
+ # Avoid actually executing the commands to generate images from the
+ # latex. It might be nice to have some assertions about what commands
+ # are executed, or perhaps even execute them and make sure an image
+ # file is created, but that is a task for another day.
+ commands = []
+ formulaeToImages(parent, base.path, _system=commands.append)
+
+ self.assertEqual(
+ parent.toxml(),
+ '<div><span><br/><img src="latexformula0.png"/><br/></span></div>')
diff --git a/twisted/lore/test/test_lore.py b/twisted/lore/test/test_lore.py
new file mode 100644
index 0000000..3f399d9
--- /dev/null
+++ b/twisted/lore/test/test_lore.py
@@ -0,0 +1,1198 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# ++ single anchor added to individual output file
+# ++ two anchors added to individual output file
+# ++ anchors added to individual output files
+# ++ entry added to index
+# ++ index entry pointing to correct file and anchor
+# ++ multiple entries added to index
+# ++ multiple index entries pointing to correct files and anchors
+# __ all of above for files in deep directory structure
+#
+# ++ group index entries by indexed term
+# ++ sort index entries by indexed term
+# __ hierarchical index entries (e.g. language!programming)
+#
+# ++ add parameter for what the index filename should be
+# ++ add (default) ability to NOT index (if index not specified)
+#
+# ++ put actual index filename into INDEX link (if any) in the template
+# __ make index links RELATIVE!
+# __ make index pay attention to the outputdir!
+#
+# __ make index look nice
+#
+# ++ add section numbers to headers in lore output
+# ++ make text of index entry links be chapter numbers
+# ++ make text of index entry links be section numbers
+#
+# __ put all of our test files someplace neat and tidy
+#
+
+import os, shutil, errno, time
+from StringIO import StringIO
+from xml.dom import minidom as dom
+
+from twisted.trial import unittest
+from twisted.python.filepath import FilePath
+
+from twisted.lore import tree, process, indexer, numberer, htmlbook, default
+from twisted.lore.default import factory
+from twisted.lore.latex import LatexSpitter
+
+from twisted.python.util import sibpath
+
+from twisted.lore.scripts import lore
+
+from twisted.web import domhelpers
+
+def sp(originalFileName):
+ return sibpath(__file__, originalFileName)
+
+options = {"template" : sp("template.tpl"), 'baseurl': '%s', 'ext': '.xhtml' }
+d = options
+
+
+class _XMLAssertionMixin:
+ """
+ Test mixin defining a method for comparing serialized XML documents.
+ """
+ def assertXMLEqual(self, first, second):
+ """
+ Verify that two strings represent the same XML document.
+ """
+ self.assertEqual(
+ dom.parseString(first).toxml(),
+ dom.parseString(second).toxml())
+
+
+class TestFactory(unittest.TestCase, _XMLAssertionMixin):
+
+ file = sp('simple.html')
+ linkrel = ""
+
+ def assertEqualFiles1(self, exp, act):
+ if (exp == act): return True
+ fact = open(act)
+ self.assertEqualsFile(exp, fact.read())
+
+ def assertEqualFiles(self, exp, act):
+ if (exp == act): return True
+ fact = open(sp(act))
+ self.assertEqualsFile(exp, fact.read())
+
+ def assertEqualsFile(self, exp, act):
+ expected = open(sp(exp)).read()
+ self.assertEqual(expected, act)
+
+ def makeTemp(self, *filenames):
+ tmp = self.mktemp()
+ os.mkdir(tmp)
+ for filename in filenames:
+ tmpFile = os.path.join(tmp, filename)
+ shutil.copyfile(sp(filename), tmpFile)
+ return tmp
+
+########################################
+
+ def setUp(self):
+ indexer.reset()
+ numberer.reset()
+
+ def testProcessingFunctionFactory(self):
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ simple = base.child('simple.html')
+ FilePath(__file__).sibling('simple.html').copyTo(simple)
+
+ htmlGenerator = factory.generate_html(options)
+ htmlGenerator(simple.path, self.linkrel)
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="index.xhtml">Index</a>
+ </body>
+</html>""",
+ simple.sibling('simple.xhtml').getContent())
+
+
+ def testProcessingFunctionFactoryWithFilenameGenerator(self):
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ def filenameGenerator(originalFileName, outputExtension):
+ name = os.path.splitext(FilePath(originalFileName).basename())[0]
+ return base.child(name + outputExtension).path
+
+ htmlGenerator = factory.generate_html(options, filenameGenerator)
+ htmlGenerator(self.file, self.linkrel)
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="index.xhtml">Index</a>
+ </body>
+</html>""",
+ base.child("simple.xhtml").getContent())
+
+
+ def test_doFile(self):
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ simple = base.child('simple.html')
+ FilePath(__file__).sibling('simple.html').copyTo(simple)
+
+ templ = dom.parse(open(d['template']))
+
+ tree.doFile(simple.path, self.linkrel, d['ext'], d['baseurl'], templ, d)
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="index.xhtml">Index</a>
+ </body>
+</html>""",
+ base.child("simple.xhtml").getContent())
+
+
+ def test_doFile_withFilenameGenerator(self):
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ def filenameGenerator(originalFileName, outputExtension):
+ name = os.path.splitext(FilePath(originalFileName).basename())[0]
+ return base.child(name + outputExtension).path
+
+ templ = dom.parse(open(d['template']))
+ tree.doFile(self.file, self.linkrel, d['ext'], d['baseurl'], templ, d, filenameGenerator)
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="index.xhtml">Index</a>
+ </body>
+</html>""",
+ base.child("simple.xhtml").getContent())
+
+
+ def test_munge(self):
+ indexer.setIndexFilename("lore_index_file.html")
+ doc = dom.parse(open(self.file))
+ node = dom.parse(open(d['template']))
+ tree.munge(doc, node, self.linkrel,
+ os.path.dirname(self.file),
+ self.file,
+ d['ext'], d['baseurl'], d)
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="lore_index_file.html">Index</a>
+ </body>
+</html>""",
+ node.toxml())
+
+
+ def test_mungeAuthors(self):
+ """
+ If there is a node with a I{class} attribute set to C{"authors"},
+ L{tree.munge} adds anchors as children to it, takeing the necessary
+ information from any I{link} nodes in the I{head} with their I{rel}
+ attribute set to C{"author"}.
+ """
+ document = dom.parseString(
+ """\
+<html>
+ <head>
+ <title>munge authors</title>
+ <link rel="author" title="foo" href="bar"/>
+ <link rel="author" title="baz" href="quux"/>
+ <link rel="author" title="foobar" href="barbaz"/>
+ </head>
+ <body>
+ <h1>munge authors</h1>
+ </body>
+</html>""")
+ template = dom.parseString(
+ """\
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
+ <head>
+ <title />
+ </head>
+
+ <body>
+ <div class="body" />
+ <div class="authors" />
+ </body>
+</html>
+""")
+ tree.munge(
+ document, template, self.linkrel, os.path.dirname(self.file),
+ self.file, d['ext'], d['baseurl'], d)
+
+ self.assertXMLEqual(
+ template.toxml(),
+ """\
+<?xml version="1.0" ?><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>munge authors</title>
+ <link href="bar" rel="author" title="foo"/><link href="quux" rel="author" title="baz"/><link href="barbaz" rel="author" title="foobar"/></head>
+
+ <body>
+ <div class="content">
+ <span/>
+ </div>
+ <div class="authors"><span><a href="bar">foo</a>, <a href="quux">baz</a>, and <a href="barbaz">foobar</a></span></div>
+ </body>
+</html>""")
+
+
+ def test_getProcessor(self):
+
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ input = base.child("simple3.html")
+ FilePath(__file__).sibling("simple3.html").copyTo(input)
+
+ options = { 'template': sp('template.tpl'), 'ext': '.xhtml', 'baseurl': 'burl',
+ 'filenameMapping': None }
+ p = process.getProcessor(default, "html", options)
+ p(input.path, self.linkrel)
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: My Test Lore Input</title></head>
+ <body bgcolor="white">
+ <h1 class="title">My Test Lore Input</h1>
+ <div class="content">
+<span/>
+<p>A Body.</p>
+</div>
+ <a href="index.xhtml">Index</a>
+ </body>
+</html>""",
+ base.child("simple3.xhtml").getContent())
+
+ def test_outputdirGenerator(self):
+ normp = os.path.normpath; join = os.path.join
+ inputdir = normp(join("/", 'home', 'joe'))
+ outputdir = normp(join("/", 'away', 'joseph'))
+ actual = process.outputdirGenerator(join("/", 'home', 'joe', "myfile.html"),
+ '.xhtml', inputdir, outputdir)
+ expected = normp(join("/", 'away', 'joseph', 'myfile.xhtml'))
+ self.assertEqual(expected, actual)
+
+ def test_outputdirGeneratorBadInput(self):
+ options = {'outputdir': '/away/joseph/', 'inputdir': '/home/joe/' }
+ self.assertRaises(ValueError, process.outputdirGenerator, '.html', '.xhtml', **options)
+
+ def test_makeSureDirectoryExists(self):
+ dirname = os.path.join("tmp", 'nonexistentdir')
+ if os.path.exists(dirname):
+ os.rmdir(dirname)
+ self.failIf(os.path.exists(dirname), "Hey: someone already created the dir")
+ filename = os.path.join(dirname, 'newfile')
+ tree.makeSureDirectoryExists(filename)
+ self.failUnless(os.path.exists(dirname), 'should have created dir')
+ os.rmdir(dirname)
+
+
+ def test_indexAnchorsAdded(self):
+ indexer.setIndexFilename('theIndexFile.html')
+ # generate the output file
+ templ = dom.parse(open(d['template']))
+ tmp = self.makeTemp('lore_index_test.xhtml')
+
+ tree.doFile(os.path.join(tmp, 'lore_index_test.xhtml'),
+ self.linkrel, '.html', d['baseurl'], templ, d)
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: The way of the program</title></head>
+ <body bgcolor="white">
+ <h1 class="title">The way of the program</h1>
+ <div class="content">
+
+<span/>
+
+<p>The first paragraph.</p>
+
+
+<h2>The Python programming language<a name="auto0"/></h2>
+<a name="index01"/>
+<a name="index02"/>
+
+<p>The second paragraph.</p>
+
+
+</div>
+ <a href="theIndexFile.html">Index</a>
+ </body>
+</html>""",
+ FilePath(tmp).child("lore_index_test.html").getContent())
+
+
+ def test_indexEntriesAdded(self):
+ indexer.addEntry('lore_index_test.html', 'index02', 'language of programming', '1.3')
+ indexer.addEntry('lore_index_test.html', 'index01', 'programming language', '1.2')
+ indexer.setIndexFilename("lore_index_file.html")
+ indexer.generateIndex()
+ self.assertEqualFiles1("lore_index_file_out.html", "lore_index_file.html")
+
+ def test_book(self):
+ tmp = self.makeTemp()
+ inputFilename = sp('lore_index_test.xhtml')
+
+ bookFilename = os.path.join(tmp, 'lore_test_book.book')
+ bf = open(bookFilename, 'w')
+ bf.write('Chapter(r"%s", None)\r\n' % inputFilename)
+ bf.close()
+
+ book = htmlbook.Book(bookFilename)
+ expected = {'indexFilename': None,
+ 'chapters': [(inputFilename, None)],
+ }
+ dct = book.__dict__
+ for k in dct:
+ self.assertEqual(dct[k], expected[k])
+
+ def test_runningLore(self):
+ options = lore.Options()
+ tmp = self.makeTemp('lore_index_test.xhtml')
+
+ templateFilename = sp('template.tpl')
+ inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
+ indexFilename = 'theIndexFile'
+
+ bookFilename = os.path.join(tmp, 'lore_test_book.book')
+ bf = open(bookFilename, 'w')
+ bf.write('Chapter(r"%s", None)\n' % inputFilename)
+ bf.close()
+
+ options.parseOptions(['--null', '--book=%s' % bookFilename,
+ '--config', 'template=%s' % templateFilename,
+ '--index=%s' % indexFilename
+ ])
+ result = lore.runGivenOptions(options)
+ self.assertEqual(None, result)
+ self.assertEqualFiles1("lore_index_file_unnumbered_out.html", indexFilename + ".html")
+
+
+ def test_runningLoreMultipleFiles(self):
+ tmp = self.makeTemp('lore_index_test.xhtml', 'lore_index_test2.xhtml')
+ templateFilename = sp('template.tpl')
+ inputFilename = os.path.join(tmp, 'lore_index_test.xhtml')
+ inputFilename2 = os.path.join(tmp, 'lore_index_test2.xhtml')
+ indexFilename = 'theIndexFile'
+
+ bookFilename = os.path.join(tmp, 'lore_test_book.book')
+ bf = open(bookFilename, 'w')
+ bf.write('Chapter(r"%s", None)\n' % inputFilename)
+ bf.write('Chapter(r"%s", None)\n' % inputFilename2)
+ bf.close()
+
+ options = lore.Options()
+ options.parseOptions(['--null', '--book=%s' % bookFilename,
+ '--config', 'template=%s' % templateFilename,
+ '--index=%s' % indexFilename
+ ])
+ result = lore.runGivenOptions(options)
+ self.assertEqual(None, result)
+
+ self.assertEqual(
+ # XXX This doesn't seem like a very good index file.
+ """\
+aahz: <a href="lore_index_test2.html#index03">link</a><br />
+aahz2: <a href="lore_index_test2.html#index02">link</a><br />
+language of programming: <a href="lore_index_test.html#index02">link</a>, <a href="lore_index_test2.html#index01">link</a><br />
+programming language: <a href="lore_index_test.html#index01">link</a><br />
+""",
+ file(FilePath(indexFilename + ".html").path).read())
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: The way of the program</title></head>
+ <body bgcolor="white">
+ <h1 class="title">The way of the program</h1>
+ <div class="content">
+
+<span/>
+
+<p>The first paragraph.</p>
+
+
+<h2>The Python programming language<a name="auto0"/></h2>
+<a name="index01"/>
+<a name="index02"/>
+
+<p>The second paragraph.</p>
+
+
+</div>
+ <a href="theIndexFile.html">Index</a>
+ </body>
+</html>""",
+ FilePath(tmp).child("lore_index_test.html").getContent())
+
+ self.assertXMLEqual(
+ """\
+<?xml version="1.0" ?><!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'><html lang="en" xmlns="http://www.w3.org/1999/xhtml">
+ <head><title>Twisted Documentation: The second page to index</title></head>
+ <body bgcolor="white">
+ <h1 class="title">The second page to index</h1>
+ <div class="content">
+
+<span/>
+
+<p>The first paragraph of the second page.</p>
+
+
+<h2>The Jython programming language<a name="auto0"/></h2>
+<a name="index01"/>
+<a name="index02"/>
+<a name="index03"/>
+
+<p>The second paragraph of the second page.</p>
+
+
+</div>
+ <a href="theIndexFile.html">Index</a>
+ </body>
+</html>""",
+ FilePath(tmp).child("lore_index_test2.html").getContent())
+
+
+
+ def XXXtest_NumberedSections(self):
+ # run two files through lore, with numbering turned on
+ # every h2 should be numbered:
+ # first file's h2s should be 1.1, 1.2
+ # second file's h2s should be 2.1, 2.2
+ templateFilename = sp('template.tpl')
+ inputFilename = sp('lore_numbering_test.xhtml')
+ inputFilename2 = sp('lore_numbering_test2.xhtml')
+ indexFilename = 'theIndexFile'
+
+ # you can number without a book:
+ options = lore.Options()
+ options.parseOptions(['--null',
+ '--index=%s' % indexFilename,
+ '--config', 'template=%s' % templateFilename,
+ '--config', 'ext=%s' % ".tns",
+ '--number',
+ inputFilename, inputFilename2])
+ result = lore.runGivenOptions(options)
+
+ self.assertEqual(None, result)
+ #self.assertEqualFiles1("lore_index_file_out_multiple.html", indexFilename + ".tns")
+ # VVV change to new, numbered files
+ self.assertEqualFiles("lore_numbering_test_out.html", "lore_numbering_test.tns")
+ self.assertEqualFiles("lore_numbering_test_out2.html", "lore_numbering_test2.tns")
+
+
+ def test_setTitle(self):
+ """
+ L{tree.setTitle} inserts the given title into the first I{title}
+ element and the first element with the I{title} class in the given
+ template.
+ """
+ parent = dom.Element('div')
+ firstTitle = dom.Element('title')
+ parent.appendChild(firstTitle)
+ secondTitle = dom.Element('span')
+ secondTitle.setAttribute('class', 'title')
+ parent.appendChild(secondTitle)
+
+ titleNodes = [dom.Text()]
+ # minidom has issues with cloning documentless-nodes. See Python issue
+ # 4851.
+ titleNodes[0].ownerDocument = dom.Document()
+ titleNodes[0].data = 'foo bar'
+
+ tree.setTitle(parent, titleNodes, None)
+ self.assertEqual(firstTitle.toxml(), '<title>foo bar</title>')
+ self.assertEqual(
+ secondTitle.toxml(), '<span class="title">foo bar</span>')
+
+
+ def test_setTitleWithChapter(self):
+ """
+ L{tree.setTitle} includes a chapter number if it is passed one.
+ """
+ document = dom.Document()
+
+ parent = dom.Element('div')
+ parent.ownerDocument = document
+
+ title = dom.Element('title')
+ parent.appendChild(title)
+
+ titleNodes = [dom.Text()]
+ titleNodes[0].ownerDocument = document
+ titleNodes[0].data = 'foo bar'
+
+ # Oh yea. The numberer has to agree to put the chapter number in, too.
+ numberer.setNumberSections(True)
+
+ tree.setTitle(parent, titleNodes, '13')
+ self.assertEqual(title.toxml(), '<title>13. foo bar</title>')
+
+
+ def test_setIndexLink(self):
+ """
+ Tests to make sure that index links are processed when an index page
+ exists and removed when there is not.
+ """
+ templ = dom.parse(open(d['template']))
+ indexFilename = 'theIndexFile'
+ numLinks = len(domhelpers.findElementsWithAttribute(templ,
+ "class",
+ "index-link"))
+
+ # if our testing template has no index-link nodes, complain about it
+ self.assertNotEquals(
+ [],
+ domhelpers.findElementsWithAttribute(templ,
+ "class",
+ "index-link"))
+
+ tree.setIndexLink(templ, indexFilename)
+
+ self.assertEqual(
+ [],
+ domhelpers.findElementsWithAttribute(templ,
+ "class",
+ "index-link"))
+
+ indexLinks = domhelpers.findElementsWithAttribute(templ,
+ "href",
+ indexFilename)
+ self.assertTrue(len(indexLinks) >= numLinks)
+
+ templ = dom.parse(open(d['template']))
+ self.assertNotEquals(
+ [],
+ domhelpers.findElementsWithAttribute(templ,
+ "class",
+ "index-link"))
+ indexFilename = None
+
+ tree.setIndexLink(templ, indexFilename)
+
+ self.assertEqual(
+ [],
+ domhelpers.findElementsWithAttribute(templ,
+ "class",
+ "index-link"))
+
+
+ def test_addMtime(self):
+ """
+ L{tree.addMtime} inserts a text node giving the last modification time
+ of the specified file wherever it encounters an element with the
+ I{mtime} class.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent('')
+ when = time.ctime(path.getModificationTime())
+
+ parent = dom.Element('div')
+ mtime = dom.Element('span')
+ mtime.setAttribute('class', 'mtime')
+ parent.appendChild(mtime)
+
+ tree.addMtime(parent, path.path)
+ self.assertEqual(
+ mtime.toxml(), '<span class="mtime">' + when + '</span>')
+
+
+ def test_makeLineNumbers(self):
+ """
+ L{tree._makeLineNumbers} takes an integer and returns a I{p} tag with
+ that number of line numbers in it.
+ """
+ numbers = tree._makeLineNumbers(1)
+ self.assertEqual(numbers.tagName, 'p')
+ self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
+ self.assertIsInstance(numbers.firstChild, dom.Text)
+ self.assertEqual(numbers.firstChild.nodeValue, '1\n')
+
+ numbers = tree._makeLineNumbers(10)
+ self.assertEqual(numbers.tagName, 'p')
+ self.assertEqual(numbers.getAttribute('class'), 'py-linenumber')
+ self.assertIsInstance(numbers.firstChild, dom.Text)
+ self.assertEqual(
+ numbers.firstChild.nodeValue,
+ ' 1\n 2\n 3\n 4\n 5\n'
+ ' 6\n 7\n 8\n 9\n10\n')
+
+
+ def test_fontifyPythonNode(self):
+ """
+ L{tree.fontifyPythonNode} accepts a text node and replaces it in its
+ parent with a syntax colored and line numbered version of the Python
+ source it contains.
+ """
+ parent = dom.Element('div')
+ source = dom.Text()
+ source.data = 'def foo():\n pass\n'
+ parent.appendChild(source)
+
+ tree.fontifyPythonNode(source)
+
+ expected = """\
+<div><pre class="python"><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
+ <span class="py-src-keyword">pass</span>
+</pre></div>"""
+
+ self.assertEqual(parent.toxml(), expected)
+
+
+ def test_addPyListings(self):
+ """
+ L{tree.addPyListings} accepts a document with nodes with their I{class}
+ attribute set to I{py-listing} and replaces those nodes with Python
+ source listings from the file given by the node's I{href} attribute.
+ """
+ listingPath = FilePath(self.mktemp())
+ listingPath.setContent('def foo():\n pass\n')
+
+ parent = dom.Element('div')
+ listing = dom.Element('a')
+ listing.setAttribute('href', listingPath.basename())
+ listing.setAttribute('class', 'py-listing')
+ parent.appendChild(listing)
+
+ tree.addPyListings(parent, listingPath.dirname())
+
+ expected = """\
+<div><div class="py-listing"><pre><p class="py-linenumber">1
+2
+</p><span class="py-src-keyword">def</span> <span class="py-src-identifier">foo</span>():
+ <span class="py-src-keyword">pass</span>
+</pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
+
+ self.assertEqual(parent.toxml(), expected)
+
+
+ def test_addPyListingsSkipLines(self):
+ """
+ If a node with the I{py-listing} class also has a I{skipLines}
+ attribute, that number of lines from the beginning of the source
+ listing are omitted.
+ """
+ listingPath = FilePath(self.mktemp())
+ listingPath.setContent('def foo():\n pass\n')
+
+ parent = dom.Element('div')
+ listing = dom.Element('a')
+ listing.setAttribute('href', listingPath.basename())
+ listing.setAttribute('class', 'py-listing')
+ listing.setAttribute('skipLines', 1)
+ parent.appendChild(listing)
+
+ tree.addPyListings(parent, listingPath.dirname())
+
+ expected = """\
+<div><div class="py-listing"><pre><p class="py-linenumber">1
+</p> <span class="py-src-keyword">pass</span>
+</pre><div class="caption"> - <a href="temp"><span class="filename">temp</span></a></div></div></div>"""
+
+ self.assertEqual(parent.toxml(), expected)
+
+
+ def test_fixAPI(self):
+ """
+ The element passed to L{tree.fixAPI} has all of its children with the
+ I{API} class rewritten to contain links to the API which is referred to
+ by the text they contain.
+ """
+ parent = dom.Element('div')
+ link = dom.Element('span')
+ link.setAttribute('class', 'API')
+ text = dom.Text()
+ text.data = 'foo'
+ link.appendChild(text)
+ parent.appendChild(link)
+
+ tree.fixAPI(parent, 'http://example.com/%s')
+ self.assertEqual(
+ parent.toxml(),
+ '<div><span class="API">'
+ '<a href="http://example.com/foo" title="foo">foo</a>'
+ '</span></div>')
+
+
+ def test_fixAPIBase(self):
+ """
+ If a node with the I{API} class and a value for the I{base} attribute
+ is included in the DOM passed to L{tree.fixAPI}, the link added to that
+ node refers to the API formed by joining the value of the I{base}
+ attribute to the text contents of the node.
+ """
+ parent = dom.Element('div')
+ link = dom.Element('span')
+ link.setAttribute('class', 'API')
+ link.setAttribute('base', 'bar')
+ text = dom.Text()
+ text.data = 'baz'
+ link.appendChild(text)
+ parent.appendChild(link)
+
+ tree.fixAPI(parent, 'http://example.com/%s')
+
+ self.assertEqual(
+ parent.toxml(),
+ '<div><span class="API">'
+ '<a href="http://example.com/bar.baz" title="bar.baz">baz</a>'
+ '</span></div>')
+
+
+ def test_fixLinks(self):
+ """
+ Links in the nodes of the DOM passed to L{tree.fixLinks} have their
+ extensions rewritten to the given extension.
+ """
+ parent = dom.Element('div')
+ link = dom.Element('a')
+ link.setAttribute('href', 'foo.html')
+ parent.appendChild(link)
+
+ tree.fixLinks(parent, '.xhtml')
+
+ self.assertEqual(parent.toxml(), '<div><a href="foo.xhtml"/></div>')
+
+
+ def test_setVersion(self):
+ """
+ Nodes of the DOM passed to L{tree.setVersion} which have the I{version}
+ class have the given version added to them a child.
+ """
+ parent = dom.Element('div')
+ version = dom.Element('span')
+ version.setAttribute('class', 'version')
+ parent.appendChild(version)
+
+ tree.setVersion(parent, '1.2.3')
+
+ self.assertEqual(
+ parent.toxml(), '<div><span class="version">1.2.3</span></div>')
+
+
+ def test_footnotes(self):
+ """
+ L{tree.footnotes} finds all of the nodes with the I{footnote} class in
+ the DOM passed to it and adds a footnotes section to the end of the
+ I{body} element which includes them. It also inserts links to those
+ footnotes from the original definition location.
+ """
+ parent = dom.Element('div')
+ body = dom.Element('body')
+ footnote = dom.Element('span')
+ footnote.setAttribute('class', 'footnote')
+ text = dom.Text()
+ text.data = 'this is the footnote'
+ footnote.appendChild(text)
+ body.appendChild(footnote)
+ body.appendChild(dom.Element('p'))
+ parent.appendChild(body)
+
+ tree.footnotes(parent)
+
+ self.assertEqual(
+ parent.toxml(),
+ '<div><body>'
+ '<a href="#footnote-1" title="this is the footnote">'
+ '<super>1</super>'
+ '</a>'
+ '<p/>'
+ '<h2>Footnotes</h2>'
+ '<ol><li><a name="footnote-1">'
+ '<span class="footnote">this is the footnote</span>'
+ '</a></li></ol>'
+ '</body></div>')
+
+
+ def test_generateTableOfContents(self):
+ """
+ L{tree.generateToC} returns an element which contains a table of
+ contents generated from the headers in the document passed to it.
+ """
+ parent = dom.Element('body')
+ header = dom.Element('h2')
+ text = dom.Text()
+ text.data = u'header & special character'
+ header.appendChild(text)
+ parent.appendChild(header)
+ subheader = dom.Element('h3')
+ text = dom.Text()
+ text.data = 'subheader'
+ subheader.appendChild(text)
+ parent.appendChild(subheader)
+
+ tableOfContents = tree.generateToC(parent)
+ self.assertEqual(
+ tableOfContents.toxml(),
+ '<ol><li><a href="#auto0">header &amp; special character</a></li><ul><li><a href="#auto1">subheader</a></li></ul></ol>')
+
+ self.assertEqual(
+ header.toxml(),
+ '<h2>header &amp; special character<a name="auto0"/></h2>')
+
+ self.assertEqual(
+ subheader.toxml(),
+ '<h3>subheader<a name="auto1"/></h3>')
+
+
+ def test_putInToC(self):
+ """
+ L{tree.putInToC} replaces all of the children of the first node with
+ the I{toc} class with the given node representing a table of contents.
+ """
+ parent = dom.Element('div')
+ toc = dom.Element('span')
+ toc.setAttribute('class', 'toc')
+ toc.appendChild(dom.Element('foo'))
+ parent.appendChild(toc)
+
+ tree.putInToC(parent, dom.Element('toc'))
+
+ self.assertEqual(toc.toxml(), '<span class="toc"><toc/></span>')
+
+
+ def test_invalidTableOfContents(self):
+ """
+ If passed a document with I{h3} elements before any I{h2} element,
+ L{tree.generateToC} raises L{ValueError} explaining that this is not a
+ valid document.
+ """
+ parent = dom.Element('body')
+ parent.appendChild(dom.Element('h3'))
+ err = self.assertRaises(ValueError, tree.generateToC, parent)
+ self.assertEqual(
+ str(err), "No H3 element is allowed until after an H2 element")
+
+
+ def test_notes(self):
+ """
+ L{tree.notes} inserts some additional markup before the first child of
+ any node with the I{note} class.
+ """
+ parent = dom.Element('div')
+ noteworthy = dom.Element('span')
+ noteworthy.setAttribute('class', 'note')
+ noteworthy.appendChild(dom.Element('foo'))
+ parent.appendChild(noteworthy)
+
+ tree.notes(parent)
+
+ self.assertEqual(
+ noteworthy.toxml(),
+ '<span class="note"><strong>Note: </strong><foo/></span>')
+
+
+ def test_findNodeJustBefore(self):
+ """
+ L{tree.findNodeJustBefore} returns the previous sibling of the node it
+ is passed. The list of nodes passed in is ignored.
+ """
+ parent = dom.Element('div')
+ result = dom.Element('foo')
+ target = dom.Element('bar')
+ parent.appendChild(result)
+ parent.appendChild(target)
+
+ self.assertIdentical(
+ tree.findNodeJustBefore(target, [parent, result]),
+ result)
+
+ # Also, support other configurations. This is a really not nice API.
+ newTarget = dom.Element('baz')
+ target.appendChild(newTarget)
+ self.assertIdentical(
+ tree.findNodeJustBefore(newTarget, [parent, result]),
+ result)
+
+
+ def test_getSectionNumber(self):
+ """
+ L{tree.getSectionNumber} accepts an I{H2} element and returns its text
+ content.
+ """
+ header = dom.Element('foo')
+ text = dom.Text()
+ text.data = 'foobar'
+ header.appendChild(text)
+ self.assertEqual(tree.getSectionNumber(header), 'foobar')
+
+
+ def test_numberDocument(self):
+ """
+ L{tree.numberDocument} inserts section numbers into the text of each
+ header.
+ """
+ parent = dom.Element('foo')
+ section = dom.Element('h2')
+ text = dom.Text()
+ text.data = 'foo'
+ section.appendChild(text)
+ parent.appendChild(section)
+
+ tree.numberDocument(parent, '7')
+
+ self.assertEqual(section.toxml(), '<h2>7.1 foo</h2>')
+
+
+ def test_parseFileAndReport(self):
+ """
+ L{tree.parseFileAndReport} parses the contents of the filename passed
+ to it and returns the corresponding DOM.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent('<foo bar="baz">hello</foo>\n')
+
+ document = tree.parseFileAndReport(path.path)
+ self.assertXMLEqual(
+ document.toxml(),
+ '<?xml version="1.0" ?><foo bar="baz">hello</foo>')
+
+
+ def test_parseFileAndReportMismatchedTags(self):
+ """
+ If the contents of the file passed to L{tree.parseFileAndReport}
+ contain a mismatched tag, L{process.ProcessingFailure} is raised
+ indicating the location of the open and close tags which were
+ mismatched.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent(' <foo>\n\n </bar>')
+
+ err = self.assertRaises(
+ process.ProcessingFailure, tree.parseFileAndReport, path.path)
+ self.assertEqual(
+ str(err),
+ "mismatched close tag at line 3, column 4; expected </foo> "
+ "(from line 1, column 2)")
+
+ # Test a case which requires involves proper close tag handling.
+ path.setContent('<foo><bar></bar>\n </baz>')
+
+ err = self.assertRaises(
+ process.ProcessingFailure, tree.parseFileAndReport, path.path)
+ self.assertEqual(
+ str(err),
+ "mismatched close tag at line 2, column 4; expected </foo> "
+ "(from line 1, column 0)")
+
+
+ def test_parseFileAndReportParseError(self):
+ """
+ If the contents of the file passed to L{tree.parseFileAndReport} cannot
+ be parsed for a reason other than mismatched tags,
+ L{process.ProcessingFailure} is raised with a string describing the
+ parse error.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent('\n foo')
+
+ err = self.assertRaises(
+ process.ProcessingFailure, tree.parseFileAndReport, path.path)
+ self.assertEqual(str(err), 'syntax error at line 2, column 3')
+
+
+ def test_parseFileAndReportIOError(self):
+ """
+ If an L{IOError} is raised while reading from the file specified to
+ L{tree.parseFileAndReport}, a L{process.ProcessingFailure} is raised
+ indicating what the error was. The file should be closed by the
+ time the exception is raised to the caller.
+ """
+ class FakeFile:
+ _open = True
+ def read(self, bytes=None):
+ raise IOError(errno.ENOTCONN, 'socket not connected')
+
+ def close(self):
+ self._open = False
+
+ theFile = FakeFile()
+ def fakeOpen(filename):
+ return theFile
+
+ err = self.assertRaises(
+ process.ProcessingFailure, tree.parseFileAndReport, "foo", fakeOpen)
+ self.assertEqual(str(err), "socket not connected, filename was 'foo'")
+ self.assertFalse(theFile._open)
+
+
+
+class XMLParsingTests(unittest.TestCase):
+ """
+ Tests for various aspects of parsing a Lore XML input document using
+ L{tree.parseFileAndReport}.
+ """
+ def _parseTest(self, xml):
+ path = FilePath(self.mktemp())
+ path.setContent(xml)
+ return tree.parseFileAndReport(path.path)
+
+
+ def test_withoutDocType(self):
+ """
+ A Lore XML input document may omit a I{DOCTYPE} declaration. If it
+ does so, the XHTML1 Strict DTD is used.
+ """
+ # Parsing should succeed.
+ document = self._parseTest("<foo>uses an xhtml entity: &copy;</foo>")
+ # But even more than that, the &copy; entity should be turned into the
+ # appropriate unicode codepoint.
+ self.assertEqual(
+ domhelpers.gatherTextNodes(document.documentElement),
+ u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
+
+
+ def test_withTransitionalDocType(self):
+ """
+ A Lore XML input document may include a I{DOCTYPE} declaration
+ referring to the XHTML1 Transitional DTD.
+ """
+ # Parsing should succeed.
+ document = self._parseTest("""\
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<foo>uses an xhtml entity: &copy;</foo>
+""")
+ # But even more than that, the &copy; entity should be turned into the
+ # appropriate unicode codepoint.
+ self.assertEqual(
+ domhelpers.gatherTextNodes(document.documentElement),
+ u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
+
+
+ def test_withStrictDocType(self):
+ """
+ A Lore XML input document may include a I{DOCTYPE} declaration
+ referring to the XHTML1 Strict DTD.
+ """
+ # Parsing should succeed.
+ document = self._parseTest("""\
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<foo>uses an xhtml entity: &copy;</foo>
+""")
+ # But even more than that, the &copy; entity should be turned into the
+ # appropriate unicode codepoint.
+ self.assertEqual(
+ domhelpers.gatherTextNodes(document.documentElement),
+ u"uses an xhtml entity: \N{COPYRIGHT SIGN}")
+
+
+ def test_withDisallowedDocType(self):
+ """
+ A Lore XML input document may not include a I{DOCTYPE} declaration
+ referring to any DTD other than XHTML1 Transitional or XHTML1 Strict.
+ """
+ self.assertRaises(
+ process.ProcessingFailure,
+ self._parseTest,
+ """\
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+<foo>uses an xhtml entity: &copy;</foo>
+""")
+
+
+
+class XMLSerializationTests(unittest.TestCase, _XMLAssertionMixin):
+ """
+ Tests for L{tree._writeDocument}.
+ """
+ def test_nonASCIIData(self):
+ """
+ A document which contains non-ascii characters is serialized to a
+ file using UTF-8.
+ """
+ document = dom.Document()
+ parent = dom.Element('foo')
+ text = dom.Text()
+ text.data = u'\N{SNOWMAN}'
+ parent.appendChild(text)
+ document.appendChild(parent)
+ outFile = self.mktemp()
+ tree._writeDocument(outFile, document)
+ self.assertXMLEqual(
+ FilePath(outFile).getContent(),
+ u'<foo>\N{SNOWMAN}</foo>'.encode('utf-8'))
+
+
+
+class LatexSpitterTestCase(unittest.TestCase):
+ """
+ Tests for the Latex output plugin.
+ """
+ def test_indexedSpan(self):
+ """
+ Test processing of a span tag with an index class results in a latex
+ \\index directive the correct value.
+ """
+ doc = dom.parseString('<span class="index" value="name" />').documentElement
+ out = StringIO()
+ spitter = LatexSpitter(out.write)
+ spitter.visitNode(doc)
+ self.assertEqual(out.getvalue(), u'\\index{name}\n')
+
+
+
+class ScriptTests(unittest.TestCase):
+ """
+ Tests for L{twisted.lore.scripts.lore}, the I{lore} command's
+ implementation,
+ """
+ def test_getProcessor(self):
+ """
+ L{lore.getProcessor} loads the specified output plugin from the
+ specified input plugin.
+ """
+ processor = lore.getProcessor("lore", "html", options)
+ self.assertNotIdentical(processor, None)
diff --git a/twisted/lore/test/test_man2lore.py b/twisted/lore/test/test_man2lore.py
new file mode 100644
index 0000000..06ada30
--- /dev/null
+++ b/twisted/lore/test/test_man2lore.py
@@ -0,0 +1,169 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Tests for L{twisted.lore.man2lore}.
+"""
+
+from StringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+
+from twisted.lore.man2lore import ManConverter
+
+
+_TRANSITIONAL_XHTML_DTD = ("""\
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+""")
+
+
+class ManConverterTestCase(TestCase):
+ """
+ Tests for L{ManConverter}.
+ """
+
+ def setUp(self):
+ """
+ Build instance variables useful for tests.
+
+ @ivar converter: a L{ManConverter} to be used during tests.
+ """
+ self.converter = ManConverter()
+
+
+ def assertConvert(self, inputLines, expectedOutput):
+ """
+ Helper method to check conversion from a man page to a Lore output.
+
+ @param inputLines: lines of the manpages.
+ @type inputLines: C{list}
+
+ @param expectedOutput: expected Lore content.
+ @type expectedOutput: C{str}
+ """
+ inputFile = StringIO()
+ for line in inputLines:
+ inputFile.write(line + '\n')
+ inputFile.seek(0)
+ outputFile = StringIO()
+ self.converter.convert(inputFile, outputFile)
+ self.assertEqual(
+ outputFile.getvalue(), _TRANSITIONAL_XHTML_DTD + expectedOutput)
+
+
+ def test_convert(self):
+ """
+ Test convert on a minimal example.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""', "Foo\n"]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<p>Foo\n\n</p>\n\n</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
+
+
+ def test_TP(self):
+ """
+ Test C{TP} parsing.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""',
+ ".SH HEADER",
+ ".TP",
+ "\\fB-o\\fR, \\fB--option\\fR",
+ "An option"]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<h2>HEADER</h2>\n\n<dl><dt>"
+ "<strong>-o</strong>, <strong>--option</strong>\n</dt>"
+ "<dd>An option\n</dd>\n\n</dl>\n\n</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
+
+
+ def test_TPMultipleOptions(self):
+ """
+ Try to parse multiple C{TP} fields.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""',
+ ".SH HEADER",
+ ".TP",
+ "\\fB-o\\fR, \\fB--option\\fR",
+ "An option",
+ ".TP",
+ "\\fB-n\\fR, \\fB--another\\fR",
+ "Another option",
+ ]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<h2>HEADER</h2>\n\n<dl><dt>"
+ "<strong>-o</strong>, <strong>--option</strong>\n</dt>"
+ "<dd>An option\n</dd>\n\n<dt>"
+ "<strong>-n</strong>, <strong>--another</strong>\n</dt>"
+ "<dd>Another option\n</dd>\n\n</dl>\n\n</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
+
+
+ def test_TPMultiLineOptions(self):
+ """
+ Try to parse multiple C{TP} fields, with options text on several lines.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""',
+ ".SH HEADER",
+ ".TP",
+ "\\fB-o\\fR, \\fB--option\\fR",
+ "An option",
+ "on two lines",
+ ".TP",
+ "\\fB-n\\fR, \\fB--another\\fR",
+ "Another option",
+ "on two lines",
+ ]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<h2>HEADER</h2>\n\n<dl><dt>"
+ "<strong>-o</strong>, <strong>--option</strong>\n</dt>"
+ "<dd>An option\non two lines\n</dd>\n\n"
+ "<dt><strong>-n</strong>, <strong>--another</strong>\n</dt>"
+ "<dd>Another option\non two lines\n</dd>\n\n</dl>\n\n"
+ "</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
+
+
+ def test_ITLegacyManagement(self):
+ """
+ Test management of BL/IT/EL used in some man pages.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""',
+ ".SH HEADER",
+ ".BL",
+ ".IT An option",
+ "on two lines",
+ ".IT",
+ "Another option",
+ "on two lines",
+ ".EL"
+ ]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<h2>HEADER</h2>\n\n<dl>"
+ "<dt>on two lines\n</dt><dd>Another option\non two lines\n"
+ "</dd></dl>\n\n</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
+
+
+ def test_interactiveCommand(self):
+ """
+ Test management of interactive command tag.
+ """
+ inputLines = ['.TH BAR "1" "Oct 2007" "" ""',
+ ".SH HEADER",
+ ".BL",
+ ".IT IC foo AR bar",
+ "option 1",
+ ".IT IC egg AR spam OP AR stuff",
+ "option 2",
+ ".EL"
+ ]
+ output = ("<html><head>\n<title>BAR.1</title></head>\n<body>\n\n"
+ "<h1>BAR.1</h1>\n\n<h2>HEADER</h2>\n\n<dl>"
+ "<dt>foo <u>bar</u></dt><dd>option 1\n</dd><dt>egg "
+ "<u>spam</u> [<u>stuff</u>]</dt><dd>option 2\n</dd></dl>"
+ "\n\n</body>\n</html>\n")
+ self.assertConvert(inputLines, output)
diff --git a/twisted/lore/test/test_scripts.py b/twisted/lore/test/test_scripts.py
new file mode 100644
index 0000000..0a8328b
--- /dev/null
+++ b/twisted/lore/test/test_scripts.py
@@ -0,0 +1,27 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the command-line interface to lore.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.scripts.test.test_scripts import ScriptTestsMixin
+from twisted.python.test.test_shellcomp import ZshScriptTestMixin
+
+
+
+class ScriptTests(TestCase, ScriptTestsMixin):
+ """
+ Tests for all one of lore's scripts.
+ """
+ def test_lore(self):
+ self.scriptTest("lore/lore")
+
+
+
+class ZshIntegrationTestCase(TestCase, ZshScriptTestMixin):
+ """
+ Test that zsh completion functions are generated without error
+ """
+ generateFor = [('lore', 'twisted.lore.scripts.lore.Options')]
diff --git a/twisted/lore/test/test_slides.py b/twisted/lore/test/test_slides.py
new file mode 100644
index 0000000..78d2cbe
--- /dev/null
+++ b/twisted/lore/test/test_slides.py
@@ -0,0 +1,85 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.lore.slides}.
+"""
+
+from xml.dom.minidom import Element, Text
+
+from twisted.trial.unittest import TestCase
+from twisted.lore.slides import HTMLSlide, splitIntoSlides, insertPrevNextLinks
+
+
+class SlidesTests(TestCase):
+ """
+ Tests for functions in L{twisted.lore.slides}.
+ """
+ def test_splitIntoSlides(self):
+ """
+ L{splitIntoSlides} accepts a document and returns a list of two-tuples,
+ each element of which contains the title of a slide taken from an I{h2}
+ element and the body of that slide.
+ """
+ parent = Element('html')
+ body = Element('body')
+ parent.appendChild(body)
+
+ first = Element('h2')
+ text = Text()
+ text.data = 'first slide'
+ first.appendChild(text)
+ body.appendChild(first)
+ body.appendChild(Element('div'))
+ body.appendChild(Element('span'))
+
+ second = Element('h2')
+ text = Text()
+ text.data = 'second slide'
+ second.appendChild(text)
+ body.appendChild(second)
+ body.appendChild(Element('p'))
+ body.appendChild(Element('br'))
+
+ slides = splitIntoSlides(parent)
+
+ self.assertEqual(slides[0][0], 'first slide')
+ firstContent = slides[0][1]
+ self.assertEqual(firstContent[0].tagName, 'div')
+ self.assertEqual(firstContent[1].tagName, 'span')
+ self.assertEqual(len(firstContent), 2)
+
+ self.assertEqual(slides[1][0], 'second slide')
+ secondContent = slides[1][1]
+ self.assertEqual(secondContent[0].tagName, 'p')
+ self.assertEqual(secondContent[1].tagName, 'br')
+ self.assertEqual(len(secondContent), 2)
+
+ self.assertEqual(len(slides), 2)
+
+
+ def test_insertPrevNextText(self):
+ """
+ L{insertPrevNextLinks} appends a text node with the title of the
+ previous slide to each node with a I{previous} class and the title of
+ the next slide to each node with a I{next} class.
+ """
+ next = Element('span')
+ next.setAttribute('class', 'next')
+ container = Element('div')
+ container.appendChild(next)
+ slideWithNext = HTMLSlide(container, 'first', 0)
+
+ previous = Element('span')
+ previous.setAttribute('class', 'previous')
+ container = Element('div')
+ container.appendChild(previous)
+ slideWithPrevious = HTMLSlide(container, 'second', 1)
+
+ insertPrevNextLinks(
+ [slideWithNext, slideWithPrevious], None, None)
+
+ self.assertEqual(
+ next.toxml(), '<span class="next">second</span>')
+ self.assertEqual(
+ previous.toxml(), '<span class="previous">first</span>')
diff --git a/twisted/lore/texi.py b/twisted/lore/texi.py
new file mode 100644
index 0000000..03f7347
--- /dev/null
+++ b/twisted/lore/texi.py
@@ -0,0 +1,109 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+from cStringIO import StringIO
+import os, re
+from twisted.python import text
+from twisted.web import domhelpers
+import latex, tree
+
+spaceRe = re.compile('\s+')
+
+def texiEscape(text):
+ return spaceRe.sub(text, ' ')
+
+entities = latex.entities.copy()
+entities['copy'] = '@copyright{}'
+
+class TexiSpitter(latex.BaseLatexSpitter):
+
+ baseLevel = 1
+
+ def writeNodeData(self, node):
+ buf = StringIO()
+ latex.getLatexText(node, self.writer, texiEscape, entities)
+
+ def visitNode_title(self, node):
+ self.writer('@node ')
+ self.visitNodeDefault(node)
+ self.writer('\n')
+ self.writer('@section ')
+ self.visitNodeDefault(node)
+ self.writer('\n')
+ headers = tree.getHeaders(domhelpers.getParents(node)[-1])
+ if not headers:
+ return
+ self.writer('@menu\n')
+ for header in headers:
+ self.writer('* %s::\n' % domhelpers.getNodeText(header))
+ self.writer('@end menu\n')
+
+ def visitNode_pre(self, node):
+ self.writer('@verbatim\n')
+ buf = StringIO()
+ latex.getLatexText(node, buf.write, entities=entities)
+ self.writer(text.removeLeadingTrailingBlanks(buf.getvalue()))
+ self.writer('@end verbatim\n')
+
+ def visitNode_code(self, node):
+ fout = StringIO()
+ latex.getLatexText(node, fout.write, texiEscape, entities)
+ self.writer('@code{'+fout.getvalue()+'}')
+
+ def visitNodeHeader(self, node):
+ self.writer('\n\n@node ')
+ self.visitNodeDefault(node)
+ self.writer('\n')
+ level = (int(node.tagName[1])-2)+self.baseLevel
+ self.writer('\n\n@'+level*'sub'+'section ')
+ self.visitNodeDefault(node)
+ self.writer('\n')
+
+ def visitNode_a_listing(self, node):
+ fileName = os.path.join(self.currDir, node.getAttribute('href'))
+ self.writer('@verbatim\n')
+ self.writer(open(fileName).read())
+ self.writer('@end verbatim')
+ # Write a caption for this source listing
+
+ def visitNode_a_href(self, node):
+ self.visitNodeDefault(node)
+
+ def visitNode_a_name(self, node):
+ self.visitNodeDefault(node)
+
+ visitNode_h2 = visitNode_h3 = visitNode_h4 = visitNodeHeader
+
+ start_dl = '@itemize\n'
+ end_dl = '@end itemize\n'
+ start_ul = '@itemize\n'
+ end_ul = '@end itemize\n'
+
+ start_ol = '@enumerate\n'
+ end_ol = '@end enumerate\n'
+
+ start_li = '@item\n'
+ end_li = '\n'
+
+ start_dt = '@item\n'
+ end_dt = ': '
+ end_dd = '\n'
+
+ start_p = '\n\n'
+
+ start_strong = start_em = '@emph{'
+ end_strong = end_em = '}'
+
+ start_q = "``"
+ end_q = "''"
+
+ start_span_footnote = '@footnote{'
+ end_span_footnote = '}'
+
+ start_div_note = '@quotation\n@strong{Note:}'
+ end_div_note = '@end quotation\n'
+
+ start_th = '@strong{'
+ end_th = '}'
diff --git a/twisted/lore/topfiles/NEWS b/twisted/lore/topfiles/NEWS
new file mode 100644
index 0000000..9bc921b
--- /dev/null
+++ b/twisted/lore/topfiles/NEWS
@@ -0,0 +1,155 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Lore 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twisted.plugins.twisted_lore's MathProcessor plugin is now
+ associated with the correct implementation module. (#5326)
+
+
+Twisted Lore 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Lore 11.1.0 (2011-11-15)
+================================
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout,
+ bin/lore/lore will now use the version of Twisted it is part of.
+ (#3526)
+
+Deprecations and Removals
+-------------------------
+ - Removed compareMarkPos and comparePosition from lore.tree,
+ deprecated in Twisted 9.0. (#5127)
+
+
+Twisted Lore 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Lore 10.2.0 (2010-11-29)
+================================
+
+No significant changes have been made for this release.
+
+Other
+-----
+ - #4571
+
+
+Twisted Lore 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Lore 10.0.0 (2010-03-01)
+================================
+
+Other
+-----
+ - #4241
+
+
+Twisted Lore 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - Python source listings now include line numbers (#3486)
+
+Fixes
+-----
+ - Lore now uses minidom instead of Twisted's microdom, which incidentally
+ fixes some Lore bugs such as throwing away certain whitespace
+ (#3560, #414, #3619)
+ - Lore's "lint" command should no longer break on documents with links in them
+ (#4051, #4115)
+
+Deprecations and Removals
+-------------------------
+ - Lore no longer uses the ancient "tml" Twisted plugin system (#1911)
+
+Other
+-----
+ - #3565, #3246, #3540, #3750, #4050
+
+
+Lore 8.2.0 (2008-12-16)
+=======================
+
+Other
+-----
+ - #2207, #2514
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Fixes
+-----
+ - Change twisted.lore.tree.setIndexLin so that it removes node with index-link
+ class when the specified index filename is None. (#812)
+ - Fix the conversion of the list of options in man pages to Lore format.
+ (#3017)
+ - Fix conch man pages generation. (#3075)
+ - Fix management of the interactive command tag in man2lore. (#3076)
+
+Misc
+----
+ - #2847
+
+
+0.3.0 (2007-01-06)
+==================
+
+Features
+--------
+ - Many docstrings were added to twisted.lore.tree (#2301)
+
+Fixes
+-----
+ - Emitting a span with an index class to latex now works (#2134)
+
+
+0.2.0 (2006-05-24)
+==================
+
+Features
+--------
+ - Docstring improvements.
+
+Fixes
+-----
+ - Embedded Dia support for Latex no longer requires the 'which'
+ command line tool.
+ - Misc: #1142.
+
+Deprecations
+------------
+ - The unused, undocumented, untested and severely crashy 'bookify'
+ functionality was removed.
+
+
+0.1.0
+=====
+ - Use htmlizer mode that doesn't insert extra span tags, thus making
+ it not mess up in Safari.
diff --git a/twisted/lore/topfiles/README b/twisted/lore/topfiles/README
new file mode 100644
index 0000000..4a4aff4
--- /dev/null
+++ b/twisted/lore/topfiles/README
@@ -0,0 +1,3 @@
+Twisted Lore 12.1.0
+
+Twisted Lore depends on Twisted and Twisted Web.
diff --git a/twisted/lore/topfiles/setup.py b/twisted/lore/topfiles/setup.py
new file mode 100644
index 0000000..a04f563
--- /dev/null
+++ b/twisted/lore/topfiles/setup.py
@@ -0,0 +1,29 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ dist.setup(
+ twisted_subproject="lore",
+ scripts=dist.getScripts("lore"),
+ # metadata
+ name="Twisted Lore",
+ description="Twisted documentation system",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Andrew Bennetts",
+ url="http://twistedmatrix.com/trac/wiki/TwistedLore",
+ license="MIT",
+ long_description="""\
+Twisted Lore is a documentation generator with HTML and LaTeX support,
+used in the Twisted project.
+""",
+ )
diff --git a/twisted/lore/tree.py b/twisted/lore/tree.py
new file mode 100755
index 0000000..5cc71aa
--- /dev/null
+++ b/twisted/lore/tree.py
@@ -0,0 +1,1122 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from itertools import count
+import re, os, cStringIO, time, cgi, urlparse
+from xml.dom import minidom as dom
+from xml.sax.handler import ErrorHandler, feature_validation
+from xml.dom.pulldom import SAX2DOM
+from xml.sax import make_parser
+from xml.sax.xmlreader import InputSource
+
+from twisted.python import htmlizer, text
+from twisted.python.filepath import FilePath
+from twisted.web import domhelpers
+import process, latex, indexer, numberer, htmlbook
+
+# relative links to html files
+def fixLinks(document, ext):
+ """
+ Rewrite links to XHTML lore input documents so they point to lore XHTML
+ output documents.
+
+ Any node with an C{href} attribute which does not contain a value starting
+ with C{http}, C{https}, C{ftp}, or C{mailto} and which does not have a
+ C{class} attribute of C{absolute} or which contains C{listing} and which
+ does point to an URL ending with C{html} will have that attribute value
+ rewritten so that the filename extension is C{ext} instead of C{html}.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @type ext: C{str}
+ @param ext: The extension to use when selecting an output file name. This
+ replaces the extension of the input file name.
+
+ @return: C{None}
+ """
+ supported_schemes=['http', 'https', 'ftp', 'mailto']
+ for node in domhelpers.findElementsWithAttribute(document, 'href'):
+ href = node.getAttribute("href")
+ if urlparse.urlparse(href)[0] in supported_schemes:
+ continue
+ if node.getAttribute("class") == "absolute":
+ continue
+ if node.getAttribute("class").find('listing') != -1:
+ continue
+
+ # This is a relative link, so it should be munged.
+ if href.endswith('html') or href[:href.rfind('#')].endswith('html'):
+ fname, fext = os.path.splitext(href)
+ if '#' in fext:
+ fext = ext+'#'+fext.split('#', 1)[1]
+ else:
+ fext = ext
+ node.setAttribute("href", fname + fext)
+
+
+
+def addMtime(document, fullpath):
+ """
+ Set the last modified time of the given document.
+
+ @type document: A DOM Node or Document
+ @param document: The output template which defines the presentation of the
+ last modified time.
+
+ @type fullpath: C{str}
+ @param fullpath: The file name from which to take the last modified time.
+
+ @return: C{None}
+ """
+ for node in domhelpers.findElementsWithAttribute(document, "class","mtime"):
+ txt = dom.Text()
+ txt.data = time.ctime(os.path.getmtime(fullpath))
+ node.appendChild(txt)
+
+
+
+def _getAPI(node):
+ """
+ Retrieve the fully qualified Python name represented by the given node.
+
+ The name is represented by one or two aspects of the node: the value of the
+ node's first child forms the end of the name. If the node has a C{base}
+ attribute, that attribute's value is prepended to the node's value, with
+ C{.} separating the two parts.
+
+ @rtype: C{str}
+ @return: The fully qualified Python name.
+ """
+ base = ""
+ if node.hasAttribute("base"):
+ base = node.getAttribute("base") + "."
+ return base+node.childNodes[0].nodeValue
+
+
+
+def fixAPI(document, url):
+ """
+ Replace API references with links to API documentation.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @type url: C{str}
+ @param url: A string which will be interpolated with the fully qualified
+ Python name of any API reference encountered in the input document, the
+ result of which will be used as a link to API documentation for that name
+ in the output document.
+
+ @return: C{None}
+ """
+ # API references
+ for node in domhelpers.findElementsWithAttribute(document, "class", "API"):
+ fullname = _getAPI(node)
+ anchor = dom.Element('a')
+ anchor.setAttribute('href', url % (fullname,))
+ anchor.setAttribute('title', fullname)
+ while node.childNodes:
+ child = node.childNodes[0]
+ node.removeChild(child)
+ anchor.appendChild(child)
+ node.appendChild(anchor)
+ if node.hasAttribute('base'):
+ node.removeAttribute('base')
+
+
+
+def fontifyPython(document):
+ """
+ Syntax color any node in the given document which contains a Python source
+ listing.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @return: C{None}
+ """
+ def matcher(node):
+ return (node.nodeName == 'pre' and node.hasAttribute('class') and
+ node.getAttribute('class') == 'python')
+ for node in domhelpers.findElements(document, matcher):
+ fontifyPythonNode(node)
+
+
+
+def fontifyPythonNode(node):
+ """
+ Syntax color the given node containing Python source code.
+
+ The node must have a parent.
+
+ @return: C{None}
+ """
+ oldio = cStringIO.StringIO()
+ latex.getLatexText(node, oldio.write,
+ entities={'lt': '<', 'gt': '>', 'amp': '&'})
+ oldio = cStringIO.StringIO(oldio.getvalue().strip()+'\n')
+ howManyLines = len(oldio.getvalue().splitlines())
+ newio = cStringIO.StringIO()
+ htmlizer.filter(oldio, newio, writer=htmlizer.SmallerHTMLWriter)
+ lineLabels = _makeLineNumbers(howManyLines)
+ newel = dom.parseString(newio.getvalue()).documentElement
+ newel.setAttribute("class", "python")
+ node.parentNode.replaceChild(newel, node)
+ newel.insertBefore(lineLabels, newel.firstChild)
+
+
+
+def addPyListings(document, dir):
+ """
+ Insert Python source listings into the given document from files in the
+ given directory based on C{py-listing} nodes.
+
+ Any node in C{document} with a C{class} attribute set to C{py-listing} will
+ have source lines taken from the file named in that node's C{href}
+ attribute (searched for in C{dir}) inserted in place of that node.
+
+ If a node has a C{skipLines} attribute, its value will be parsed as an
+ integer and that many lines will be skipped at the beginning of the source
+ file.
+
+ @type document: A DOM Node or Document
+ @param document: The document within which to make listing replacements.
+
+ @type dir: C{str}
+ @param dir: The directory in which to find source files containing the
+ referenced Python listings.
+
+ @return: C{None}
+ """
+ for node in domhelpers.findElementsWithAttribute(document, "class",
+ "py-listing"):
+ filename = node.getAttribute("href")
+ outfile = cStringIO.StringIO()
+ lines = map(str.rstrip, open(os.path.join(dir, filename)).readlines())
+
+ skip = node.getAttribute('skipLines') or 0
+ lines = lines[int(skip):]
+ howManyLines = len(lines)
+ data = '\n'.join(lines)
+
+ data = cStringIO.StringIO(text.removeLeadingTrailingBlanks(data))
+ htmlizer.filter(data, outfile, writer=htmlizer.SmallerHTMLWriter)
+ sourceNode = dom.parseString(outfile.getvalue()).documentElement
+ sourceNode.insertBefore(_makeLineNumbers(howManyLines), sourceNode.firstChild)
+ _replaceWithListing(node, sourceNode.toxml(), filename, "py-listing")
+
+
+
+def _makeLineNumbers(howMany):
+ """
+ Return an element which will render line numbers for a source listing.
+
+ @param howMany: The number of lines in the source listing.
+ @type howMany: C{int}
+
+ @return: An L{dom.Element} which can be added to the document before
+ the source listing to add line numbers to it.
+ """
+ # Figure out how many digits wide the widest line number label will be.
+ width = len(str(howMany))
+
+ # Render all the line labels with appropriate padding
+ labels = ['%*d' % (width, i) for i in range(1, howMany + 1)]
+
+ # Create a p element with the right style containing the labels
+ p = dom.Element('p')
+ p.setAttribute('class', 'py-linenumber')
+ t = dom.Text()
+ t.data = '\n'.join(labels) + '\n'
+ p.appendChild(t)
+ return p
+
+
+def _replaceWithListing(node, val, filename, class_):
+ captionTitle = domhelpers.getNodeText(node)
+ if captionTitle == os.path.basename(filename):
+ captionTitle = 'Source listing'
+ text = ('<div class="%s">%s<div class="caption">%s - '
+ '<a href="%s"><span class="filename">%s</span></a></div></div>' %
+ (class_, val, captionTitle, filename, filename))
+ newnode = dom.parseString(text).documentElement
+ node.parentNode.replaceChild(newnode, node)
+
+
+
+def addHTMLListings(document, dir):
+ """
+ Insert HTML source listings into the given document from files in the given
+ directory based on C{html-listing} nodes.
+
+ Any node in C{document} with a C{class} attribute set to C{html-listing}
+ will have source lines taken from the file named in that node's C{href}
+ attribute (searched for in C{dir}) inserted in place of that node.
+
+ @type document: A DOM Node or Document
+ @param document: The document within which to make listing replacements.
+
+ @type dir: C{str}
+ @param dir: The directory in which to find source files containing the
+ referenced HTML listings.
+
+ @return: C{None}
+ """
+ for node in domhelpers.findElementsWithAttribute(document, "class",
+ "html-listing"):
+ filename = node.getAttribute("href")
+ val = ('<pre class="htmlsource">\n%s</pre>' %
+ cgi.escape(open(os.path.join(dir, filename)).read()))
+ _replaceWithListing(node, val, filename, "html-listing")
+
+
+
+def addPlainListings(document, dir):
+ """
+ Insert text listings into the given document from files in the given
+ directory based on C{listing} nodes.
+
+ Any node in C{document} with a C{class} attribute set to C{listing} will
+ have source lines taken from the file named in that node's C{href}
+ attribute (searched for in C{dir}) inserted in place of that node.
+
+ @type document: A DOM Node or Document
+ @param document: The document within which to make listing replacements.
+
+ @type dir: C{str}
+ @param dir: The directory in which to find source files containing the
+ referenced text listings.
+
+ @return: C{None}
+ """
+ for node in domhelpers.findElementsWithAttribute(document, "class",
+ "listing"):
+ filename = node.getAttribute("href")
+ val = ('<pre>\n%s</pre>' %
+ cgi.escape(open(os.path.join(dir, filename)).read()))
+ _replaceWithListing(node, val, filename, "listing")
+
+
+
+def getHeaders(document):
+ """
+ Return all H2 and H3 nodes in the given document.
+
+ @type document: A DOM Node or Document
+
+ @rtype: C{list}
+ """
+ return domhelpers.findElements(
+ document,
+ lambda n, m=re.compile('h[23]$').match: m(n.nodeName))
+
+
+
+def generateToC(document):
+ """
+ Create a table of contents for the given document.
+
+ @type document: A DOM Node or Document
+
+ @rtype: A DOM Node
+ @return: a Node containing a table of contents based on the headers of the
+ given document.
+ """
+ subHeaders = None
+ headers = []
+ for element in getHeaders(document):
+ if element.tagName == 'h2':
+ subHeaders = []
+ headers.append((element, subHeaders))
+ elif subHeaders is None:
+ raise ValueError(
+ "No H3 element is allowed until after an H2 element")
+ else:
+ subHeaders.append(element)
+
+ auto = count().next
+
+ def addItem(headerElement, parent):
+ anchor = dom.Element('a')
+ name = 'auto%d' % (auto(),)
+ anchor.setAttribute('href', '#' + name)
+ text = dom.Text()
+ text.data = domhelpers.getNodeText(headerElement)
+ anchor.appendChild(text)
+ headerNameItem = dom.Element('li')
+ headerNameItem.appendChild(anchor)
+ parent.appendChild(headerNameItem)
+ anchor = dom.Element('a')
+ anchor.setAttribute('name', name)
+ headerElement.appendChild(anchor)
+
+ toc = dom.Element('ol')
+ for headerElement, subHeaders in headers:
+ addItem(headerElement, toc)
+ if subHeaders:
+ subtoc = dom.Element('ul')
+ toc.appendChild(subtoc)
+ for subHeaderElement in subHeaders:
+ addItem(subHeaderElement, subtoc)
+
+ return toc
+
+
+
+def putInToC(document, toc):
+ """
+ Insert the given table of contents into the given document.
+
+ The node with C{class} attribute set to C{toc} has its children replaced
+ with C{toc}.
+
+ @type document: A DOM Node or Document
+ @type toc: A DOM Node
+ """
+ tocOrig = domhelpers.findElementsWithAttribute(document, 'class', 'toc')
+ if tocOrig:
+ tocOrig= tocOrig[0]
+ tocOrig.childNodes = [toc]
+
+
+
+def removeH1(document):
+ """
+ Replace all C{h1} nodes in the given document with empty C{span} nodes.
+
+ C{h1} nodes mark up document sections and the output template is given an
+ opportunity to present this information in a different way.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @return: C{None}
+ """
+ h1 = domhelpers.findNodesNamed(document, 'h1')
+ empty = dom.Element('span')
+ for node in h1:
+ node.parentNode.replaceChild(empty, node)
+
+
+
+def footnotes(document):
+ """
+ Find footnotes in the given document, move them to the end of the body, and
+ generate links to them.
+
+ A footnote is any node with a C{class} attribute set to C{footnote}.
+ Footnote links are generated as superscript. Footnotes are collected in a
+ C{ol} node at the end of the document.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @return: C{None}
+ """
+ footnotes = domhelpers.findElementsWithAttribute(document, "class",
+ "footnote")
+ if not footnotes:
+ return
+ footnoteElement = dom.Element('ol')
+ id = 1
+ for footnote in footnotes:
+ href = dom.parseString('<a href="#footnote-%(id)d">'
+ '<super>%(id)d</super></a>'
+ % vars()).documentElement
+ text = ' '.join(domhelpers.getNodeText(footnote).split())
+ href.setAttribute('title', text)
+ target = dom.Element('a')
+ target.setAttribute('name', 'footnote-%d' % (id,))
+ target.childNodes = [footnote]
+ footnoteContent = dom.Element('li')
+ footnoteContent.childNodes = [target]
+ footnoteElement.childNodes.append(footnoteContent)
+ footnote.parentNode.replaceChild(href, footnote)
+ id += 1
+ body = domhelpers.findNodesNamed(document, "body")[0]
+ header = dom.parseString('<h2>Footnotes</h2>').documentElement
+ body.childNodes.append(header)
+ body.childNodes.append(footnoteElement)
+
+
+
+def notes(document):
+ """
+ Find notes in the given document and mark them up as such.
+
+ A note is any node with a C{class} attribute set to C{note}.
+
+ (I think this is a very stupid feature. When I found it I actually
+ exclaimed out loud. -exarkun)
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @return: C{None}
+ """
+ notes = domhelpers.findElementsWithAttribute(document, "class", "note")
+ notePrefix = dom.parseString('<strong>Note: </strong>').documentElement
+ for note in notes:
+ note.childNodes.insert(0, notePrefix)
+
+
+
+def findNodeJustBefore(target, nodes):
+ """
+ Find the last Element which is a sibling of C{target} and is in C{nodes}.
+
+ @param target: A node the previous sibling of which to return.
+ @param nodes: A list of nodes which might be the right node.
+
+ @return: The previous sibling of C{target}.
+ """
+ while target is not None:
+ node = target.previousSibling
+ while node is not None:
+ if node in nodes:
+ return node
+ node = node.previousSibling
+ target = target.parentNode
+ raise RuntimeError("Oops")
+
+
+
+def getFirstAncestorWithSectionHeader(entry):
+ """
+ Visit the ancestors of C{entry} until one with at least one C{h2} child
+ node is found, then return all of that node's C{h2} child nodes.
+
+ @type entry: A DOM Node
+ @param entry: The node from which to begin traversal. This node itself is
+ excluded from consideration.
+
+ @rtype: C{list} of DOM Nodes
+ @return: All C{h2} nodes of the ultimately selected parent node.
+ """
+ for a in domhelpers.getParents(entry)[1:]:
+ headers = domhelpers.findNodesNamed(a, "h2")
+ if len(headers) > 0:
+ return headers
+ return []
+
+
+
+def getSectionNumber(header):
+ """
+ Retrieve the section number of the given node.
+
+ This is probably intended to interact in a rather specific way with
+ L{numberDocument}.
+
+ @type header: A DOM Node or L{None}
+ @param header: The section from which to extract a number. The section
+ number is the value of this node's first child.
+
+ @return: C{None} or a C{str} giving the section number.
+ """
+ if not header:
+ return None
+ return domhelpers.gatherTextNodes(header.childNodes[0])
+
+
+
+def getSectionReference(entry):
+ """
+ Find the section number which contains the given node.
+
+ This function looks at the given node's ancestry until it finds a node
+ which defines a section, then returns that section's number.
+
+ @type entry: A DOM Node
+ @param entry: The node for which to determine the section.
+
+ @rtype: C{str}
+ @return: The section number, as returned by C{getSectionNumber} of the
+ first ancestor of C{entry} which defines a section, as determined by
+ L{getFirstAncestorWithSectionHeader}.
+ """
+ headers = getFirstAncestorWithSectionHeader(entry)
+ myHeader = findNodeJustBefore(entry, headers)
+ return getSectionNumber(myHeader)
+
+
+
+def index(document, filename, chapterReference):
+ """
+ Extract index entries from the given document and store them for later use
+ and insert named anchors so that the index can link back to those entries.
+
+ Any node with a C{class} attribute set to C{index} is considered an index
+ entry.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @type filename: C{str}
+ @param filename: A link to the output for the given document which will be
+ included in the index to link to any index entry found here.
+
+ @type chapterReference: ???
+ @param chapterReference: ???
+
+ @return: C{None}
+ """
+ entries = domhelpers.findElementsWithAttribute(document, "class", "index")
+ if not entries:
+ return
+ i = 0;
+ for entry in entries:
+ i += 1
+ anchor = 'index%02d' % i
+ if chapterReference:
+ ref = getSectionReference(entry) or chapterReference
+ else:
+ ref = 'link'
+ indexer.addEntry(filename, anchor, entry.getAttribute('value'), ref)
+ # does nodeName even affect anything?
+ entry.nodeName = entry.tagName = entry.endTagName = 'a'
+ for attrName in entry.attributes.keys():
+ entry.removeAttribute(attrName)
+ entry.setAttribute('name', anchor)
+
+
+
+def setIndexLink(template, indexFilename):
+ """
+ Insert a link to an index document.
+
+ Any node with a C{class} attribute set to C{index-link} will have its tag
+ name changed to C{a} and its C{href} attribute set to C{indexFilename}.
+
+ @type template: A DOM Node or Document
+ @param template: The output template which defines the presentation of the
+ version information.
+
+ @type indexFilename: C{str}
+ @param indexFilename: The address of the index document to which to link.
+ If any C{False} value, this function will remove all index-link nodes.
+
+ @return: C{None}
+ """
+ indexLinks = domhelpers.findElementsWithAttribute(template,
+ "class",
+ "index-link")
+ for link in indexLinks:
+ if indexFilename is None:
+ link.parentNode.removeChild(link)
+ else:
+ link.nodeName = link.tagName = link.endTagName = 'a'
+ for attrName in link.attributes.keys():
+ link.removeAttribute(attrName)
+ link.setAttribute('href', indexFilename)
+
+
+
+def numberDocument(document, chapterNumber):
+ """
+ Number the sections of the given document.
+
+ A dot-separated chapter, section number is added to the beginning of each
+ section, as defined by C{h2} nodes.
+
+ This is probably intended to interact in a rather specific way with
+ L{getSectionNumber}.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @type chapterNumber: C{int}
+ @param chapterNumber: The chapter number of this content in an overall
+ document.
+
+ @return: C{None}
+ """
+ i = 1
+ for node in domhelpers.findNodesNamed(document, "h2"):
+ label = dom.Text()
+ label.data = "%s.%d " % (chapterNumber, i)
+ node.insertBefore(label, node.firstChild)
+ i += 1
+
+
+
+def fixRelativeLinks(document, linkrel):
+ """
+ Replace relative links in C{str} and C{href} attributes with links relative
+ to C{linkrel}.
+
+ @type document: A DOM Node or Document
+ @param document: The output template.
+
+ @type linkrel: C{str}
+ @param linkrel: An prefix to apply to all relative links in C{src} or
+ C{href} attributes in the input document when generating the output
+ document.
+ """
+ for attr in 'src', 'href':
+ for node in domhelpers.findElementsWithAttribute(document, attr):
+ href = node.getAttribute(attr)
+ if not href.startswith('http') and not href.startswith('/'):
+ node.setAttribute(attr, linkrel+node.getAttribute(attr))
+
+
+
+def setTitle(template, title, chapterNumber):
+ """
+ Add title and chapter number information to the template document.
+
+ The title is added to the end of the first C{title} tag and the end of the
+ first tag with a C{class} attribute set to C{title}. If specified, the
+ chapter is inserted before the title.
+
+ @type template: A DOM Node or Document
+ @param template: The output template which defines the presentation of the
+ version information.
+
+ @type title: C{list} of DOM Nodes
+ @param title: Nodes from the input document defining its title.
+
+ @type chapterNumber: C{int}
+ @param chapterNumber: The chapter number of this content in an overall
+ document. If not applicable, any C{False} value will result in this
+ information being omitted.
+
+ @return: C{None}
+ """
+ if numberer.getNumberSections() and chapterNumber:
+ titleNode = dom.Text()
+ # This is necessary in order for cloning below to work. See Python
+ # isuse 4851.
+ titleNode.ownerDocument = template.ownerDocument
+ titleNode.data = '%s. ' % (chapterNumber,)
+ title.insert(0, titleNode)
+
+ for nodeList in (domhelpers.findNodesNamed(template, "title"),
+ domhelpers.findElementsWithAttribute(template, "class",
+ 'title')):
+ if nodeList:
+ for titleNode in title:
+ nodeList[0].appendChild(titleNode.cloneNode(True))
+
+
+
+def setAuthors(template, authors):
+ """
+ Add author information to the template document.
+
+ Names and contact information for authors are added to each node with a
+ C{class} attribute set to C{authors} and to the template head as C{link}
+ nodes.
+
+ @type template: A DOM Node or Document
+ @param template: The output template which defines the presentation of the
+ version information.
+
+ @type authors: C{list} of two-tuples of C{str}
+ @param authors: List of names and contact information for the authors of
+ the input document.
+
+ @return: C{None}
+ """
+
+ for node in domhelpers.findElementsWithAttribute(template,
+ "class", 'authors'):
+
+ # First, similarly to setTitle, insert text into an <div
+ # class="authors">
+ container = dom.Element('span')
+ for name, href in authors:
+ anchor = dom.Element('a')
+ anchor.setAttribute('href', href)
+ anchorText = dom.Text()
+ anchorText.data = name
+ anchor.appendChild(anchorText)
+ if (name, href) == authors[-1]:
+ if len(authors) == 1:
+ container.appendChild(anchor)
+ else:
+ andText = dom.Text()
+ andText.data = 'and '
+ container.appendChild(andText)
+ container.appendChild(anchor)
+ else:
+ container.appendChild(anchor)
+ commaText = dom.Text()
+ commaText.data = ', '
+ container.appendChild(commaText)
+
+ node.appendChild(container)
+
+ # Second, add appropriate <link rel="author" ...> tags to the <head>.
+ head = domhelpers.findNodesNamed(template, 'head')[0]
+ authors = [dom.parseString('<link rel="author" href="%s" title="%s"/>'
+ % (href, name)).childNodes[0]
+ for name, href in authors]
+ head.childNodes.extend(authors)
+
+
+
+def setVersion(template, version):
+ """
+ Add a version indicator to the given template.
+
+ @type template: A DOM Node or Document
+ @param template: The output template which defines the presentation of the
+ version information.
+
+ @type version: C{str}
+ @param version: The version string to add to the template.
+
+ @return: C{None}
+ """
+ for node in domhelpers.findElementsWithAttribute(template, "class",
+ "version"):
+ text = dom.Text()
+ text.data = version
+ node.appendChild(text)
+
+
+
+def getOutputFileName(originalFileName, outputExtension, index=None):
+ """
+ Return a filename which is the same as C{originalFileName} except for the
+ extension, which is replaced with C{outputExtension}.
+
+ For example, if C{originalFileName} is C{'/foo/bar.baz'} and
+ C{outputExtension} is C{'quux'}, the return value will be
+ C{'/foo/bar.quux'}.
+
+ @type originalFileName: C{str}
+ @type outputExtension: C{stR}
+ @param index: ignored, never passed.
+ @rtype: C{str}
+ """
+ return os.path.splitext(originalFileName)[0]+outputExtension
+
+
+
+def munge(document, template, linkrel, dir, fullpath, ext, url, config, outfileGenerator=getOutputFileName):
+ """
+ Mutate C{template} until it resembles C{document}.
+
+ @type document: A DOM Node or Document
+ @param document: The input document which contains all of the content to be
+ presented.
+
+ @type template: A DOM Node or Document
+ @param template: The template document which defines the desired
+ presentation format of the content.
+
+ @type linkrel: C{str}
+ @param linkrel: An prefix to apply to all relative links in C{src} or
+ C{href} attributes in the input document when generating the output
+ document.
+
+ @type dir: C{str}
+ @param dir: The directory in which to search for source listing files.
+
+ @type fullpath: C{str}
+ @param fullpath: The file name which contained the input document.
+
+ @type ext: C{str}
+ @param ext: The extension to use when selecting an output file name. This
+ replaces the extension of the input file name.
+
+ @type url: C{str}
+ @param url: A string which will be interpolated with the fully qualified
+ Python name of any API reference encountered in the input document, the
+ result of which will be used as a link to API documentation for that name
+ in the output document.
+
+ @type config: C{dict}
+ @param config: Further specification of the desired form of the output.
+ Valid keys in this dictionary::
+
+ noapi: If present and set to a True value, links to API documentation
+ will not be generated.
+
+ version: A string which will be included in the output to indicate the
+ version of this documentation.
+
+ @type outfileGenerator: Callable of C{str}, C{str} returning C{str}
+ @param outfileGenerator: Output filename factory. This is invoked with the
+ intput filename and C{ext} and the output document is serialized to the
+ file with the name returned.
+
+ @return: C{None}
+ """
+ fixRelativeLinks(template, linkrel)
+ addMtime(template, fullpath)
+ removeH1(document)
+ if not config.get('noapi', False):
+ fixAPI(document, url)
+ fontifyPython(document)
+ fixLinks(document, ext)
+ addPyListings(document, dir)
+ addHTMLListings(document, dir)
+ addPlainListings(document, dir)
+ putInToC(template, generateToC(document))
+ footnotes(document)
+ notes(document)
+
+ setIndexLink(template, indexer.getIndexFilename())
+ setVersion(template, config.get('version', ''))
+
+ # Insert the document into the template
+ chapterNumber = htmlbook.getNumber(fullpath)
+ title = domhelpers.findNodesNamed(document, 'title')[0].childNodes
+ setTitle(template, title, chapterNumber)
+ if numberer.getNumberSections() and chapterNumber:
+ numberDocument(document, chapterNumber)
+ index(document, outfileGenerator(os.path.split(fullpath)[1], ext),
+ htmlbook.getReference(fullpath))
+
+ authors = domhelpers.findNodesNamed(document, 'link')
+ authors = [(node.getAttribute('title') or '',
+ node.getAttribute('href') or '')
+ for node in authors
+ if node.getAttribute('rel') == 'author']
+ setAuthors(template, authors)
+
+ body = domhelpers.findNodesNamed(document, "body")[0]
+ tmplbody = domhelpers.findElementsWithAttribute(template, "class",
+ "body")[0]
+ tmplbody.childNodes = body.childNodes
+ tmplbody.setAttribute("class", "content")
+
+
+class _LocationReportingErrorHandler(ErrorHandler):
+ """
+ Define a SAX error handler which can report the location of fatal
+ errors.
+
+ Unlike the errors reported during parsing by other APIs in the xml
+ package, this one tries to mismatched tag errors by including the
+ location of both the relevant opening and closing tags.
+ """
+ def __init__(self, contentHandler):
+ self.contentHandler = contentHandler
+
+ def fatalError(self, err):
+ # Unfortunately, the underlying expat error code is only exposed as
+ # a string. I surely do hope no one ever goes and localizes expat.
+ if err.getMessage() == 'mismatched tag':
+ expect, begLine, begCol = self.contentHandler._locationStack[-1]
+ endLine, endCol = err.getLineNumber(), err.getColumnNumber()
+ raise process.ProcessingFailure(
+ "mismatched close tag at line %d, column %d; expected </%s> "
+ "(from line %d, column %d)" % (
+ endLine, endCol, expect, begLine, begCol))
+ raise process.ProcessingFailure(
+ '%s at line %d, column %d' % (err.getMessage(),
+ err.getLineNumber(),
+ err.getColumnNumber()))
+
+
+class _TagTrackingContentHandler(SAX2DOM):
+ """
+ Define a SAX content handler which keeps track of the start location of
+ all open tags. This information is used by the above defined error
+ handler to report useful locations when a fatal error is encountered.
+ """
+ def __init__(self):
+ SAX2DOM.__init__(self)
+ self._locationStack = []
+
+ def setDocumentLocator(self, locator):
+ self._docLocator = locator
+ SAX2DOM.setDocumentLocator(self, locator)
+
+ def startElement(self, name, attrs):
+ self._locationStack.append((name, self._docLocator.getLineNumber(), self._docLocator.getColumnNumber()))
+ SAX2DOM.startElement(self, name, attrs)
+
+ def endElement(self, name):
+ self._locationStack.pop()
+ SAX2DOM.endElement(self, name)
+
+
+class _LocalEntityResolver(object):
+ """
+ Implement DTD loading (from a local source) for the limited number of
+ DTDs which are allowed for Lore input documents.
+
+ @ivar filename: The name of the file containing the lore input
+ document.
+
+ @ivar knownDTDs: A mapping from DTD system identifiers to L{FilePath}
+ instances pointing to the corresponding DTD.
+ """
+ s = FilePath(__file__).sibling
+
+ knownDTDs = {
+ None: s("xhtml1-strict.dtd"),
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd": s("xhtml1-strict.dtd"),
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd": s("xhtml1-transitional.dtd"),
+ "xhtml-lat1.ent": s("xhtml-lat1.ent"),
+ "xhtml-symbol.ent": s("xhtml-symbol.ent"),
+ "xhtml-special.ent": s("xhtml-special.ent"),
+ }
+ del s
+
+ def __init__(self, filename):
+ self.filename = filename
+
+
+ def resolveEntity(self, publicId, systemId):
+ source = InputSource()
+ source.setSystemId(systemId)
+ try:
+ dtdPath = self.knownDTDs[systemId]
+ except KeyError:
+ raise process.ProcessingFailure(
+ "Invalid DTD system identifier (%r) in %s. Only "
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd "
+ "is allowed." % (systemId, self.filename))
+ source.setByteStream(dtdPath.open())
+ return source
+
+
+
+def parseFileAndReport(filename, _open=file):
+ """
+ Parse and return the contents of the given lore XHTML document.
+
+ @type filename: C{str}
+ @param filename: The name of a file containing a lore XHTML document to
+ load.
+
+ @raise process.ProcessingFailure: When the contents of the specified file
+ cannot be parsed.
+
+ @rtype: A DOM Document
+ @return: The document contained in C{filename}.
+ """
+ content = _TagTrackingContentHandler()
+ error = _LocationReportingErrorHandler(content)
+ parser = make_parser()
+ parser.setContentHandler(content)
+ parser.setErrorHandler(error)
+
+ # In order to call a method on the expat parser which will be used by this
+ # parser, we need the expat parser to be created. This doesn't happen
+ # until reset is called, normally by the parser's parse method. That's too
+ # late for us, since it will then go on to parse the document without
+ # letting us do any extra set up. So, force the expat parser to be created
+ # here, and then disable reset so that the parser created is the one
+ # actually used to parse our document. Resetting is only needed if more
+ # than one document is going to be parsed, and that isn't the case here.
+ parser.reset()
+ parser.reset = lambda: None
+
+ # This is necessary to make the xhtml1 transitional declaration optional.
+ # It causes LocalEntityResolver.resolveEntity(None, None) to be called.
+ # LocalEntityResolver handles that case by giving out the xhtml1
+ # transitional dtd. Unfortunately, there is no public API for manipulating
+ # the expat parser when using xml.sax. Using the private _parser attribute
+ # may break. It's also possible that make_parser will return a parser
+ # which doesn't use expat, but uses some other parser. Oh well. :(
+ # -exarkun
+ parser._parser.UseForeignDTD(True)
+ parser.setEntityResolver(_LocalEntityResolver(filename))
+
+ # This is probably no-op because expat is not a validating parser. Who
+ # knows though, maybe you figured out a way to not use expat.
+ parser.setFeature(feature_validation, False)
+
+ fObj = _open(filename)
+ try:
+ try:
+ parser.parse(fObj)
+ except IOError, e:
+ raise process.ProcessingFailure(
+ e.strerror + ", filename was '" + filename + "'")
+ finally:
+ fObj.close()
+ return content.document
+
+
+def makeSureDirectoryExists(filename):
+ filename = os.path.abspath(filename)
+ dirname = os.path.dirname(filename)
+ if (not os.path.exists(dirname)):
+ os.makedirs(dirname)
+
+def doFile(filename, linkrel, ext, url, templ, options={}, outfileGenerator=getOutputFileName):
+ """
+ Process the input document at C{filename} and write an output document.
+
+ @type filename: C{str}
+ @param filename: The path to the input file which will be processed.
+
+ @type linkrel: C{str}
+ @param linkrel: An prefix to apply to all relative links in C{src} or
+ C{href} attributes in the input document when generating the output
+ document.
+
+ @type ext: C{str}
+ @param ext: The extension to use when selecting an output file name. This
+ replaces the extension of the input file name.
+
+ @type url: C{str}
+ @param url: A string which will be interpolated with the fully qualified
+ Python name of any API reference encountered in the input document, the
+ result of which will be used as a link to API documentation for that name
+ in the output document.
+
+ @type templ: A DOM Node or Document
+ @param templ: The template on which the output document will be based.
+ This is mutated and then serialized to the output file.
+
+ @type options: C{dict}
+ @param options: Further specification of the desired form of the output.
+ Valid keys in this dictionary::
+
+ noapi: If present and set to a True value, links to API documentation
+ will not be generated.
+
+ version: A string which will be included in the output to indicate the
+ version of this documentation.
+
+ @type outfileGenerator: Callable of C{str}, C{str} returning C{str}
+ @param outfileGenerator: Output filename factory. This is invoked with the
+ intput filename and C{ext} and the output document is serialized to the
+ file with the name returned.
+
+ @return: C{None}
+ """
+ doc = parseFileAndReport(filename)
+ clonedNode = templ.cloneNode(1)
+ munge(doc, clonedNode, linkrel, os.path.dirname(filename), filename, ext,
+ url, options, outfileGenerator)
+ newFilename = outfileGenerator(filename, ext)
+ _writeDocument(newFilename, clonedNode)
+
+
+
+def _writeDocument(newFilename, clonedNode):
+ """
+ Serialize the given node to XML into the named file.
+
+ @param newFilename: The name of the file to which the XML will be
+ written. If this is in a directory which does not exist, the
+ directory will be created.
+
+ @param clonedNode: The root DOM node which will be serialized.
+
+ @return: C{None}
+ """
+ makeSureDirectoryExists(newFilename)
+ f = open(newFilename, 'w')
+ f.write(clonedNode.toxml('utf-8'))
+ f.close()
diff --git a/twisted/lore/xhtml-lat1.ent b/twisted/lore/xhtml-lat1.ent
new file mode 100644
index 0000000..ffee223
--- /dev/null
+++ b/twisted/lore/xhtml-lat1.ent
@@ -0,0 +1,196 @@
+<!-- Portions (C) International Organization for Standardization 1986
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLlat1 PUBLIC
+ "-//W3C//ENTITIES Latin 1 for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent">
+ %HTMLlat1;
+-->
+
+<!ENTITY nbsp "&#160;"> <!-- no-break space = non-breaking space,
+ U+00A0 ISOnum -->
+<!ENTITY iexcl "&#161;"> <!-- inverted exclamation mark, U+00A1 ISOnum -->
+<!ENTITY cent "&#162;"> <!-- cent sign, U+00A2 ISOnum -->
+<!ENTITY pound "&#163;"> <!-- pound sign, U+00A3 ISOnum -->
+<!ENTITY curren "&#164;"> <!-- currency sign, U+00A4 ISOnum -->
+<!ENTITY yen "&#165;"> <!-- yen sign = yuan sign, U+00A5 ISOnum -->
+<!ENTITY brvbar "&#166;"> <!-- broken bar = broken vertical bar,
+ U+00A6 ISOnum -->
+<!ENTITY sect "&#167;"> <!-- section sign, U+00A7 ISOnum -->
+<!ENTITY uml "&#168;"> <!-- diaeresis = spacing diaeresis,
+ U+00A8 ISOdia -->
+<!ENTITY copy "&#169;"> <!-- copyright sign, U+00A9 ISOnum -->
+<!ENTITY ordf "&#170;"> <!-- feminine ordinal indicator, U+00AA ISOnum -->
+<!ENTITY laquo "&#171;"> <!-- left-pointing double angle quotation mark
+ = left pointing guillemet, U+00AB ISOnum -->
+<!ENTITY not "&#172;"> <!-- not sign = angled dash,
+ U+00AC ISOnum -->
+<!ENTITY shy "&#173;"> <!-- soft hyphen = discretionary hyphen,
+ U+00AD ISOnum -->
+<!ENTITY reg "&#174;"> <!-- registered sign = registered trade mark sign,
+ U+00AE ISOnum -->
+<!ENTITY macr "&#175;"> <!-- macron = spacing macron = overline
+ = APL overbar, U+00AF ISOdia -->
+<!ENTITY deg "&#176;"> <!-- degree sign, U+00B0 ISOnum -->
+<!ENTITY plusmn "&#177;"> <!-- plus-minus sign = plus-or-minus sign,
+ U+00B1 ISOnum -->
+<!ENTITY sup2 "&#178;"> <!-- superscript two = superscript digit two
+ = squared, U+00B2 ISOnum -->
+<!ENTITY sup3 "&#179;"> <!-- superscript three = superscript digit three
+ = cubed, U+00B3 ISOnum -->
+<!ENTITY acute "&#180;"> <!-- acute accent = spacing acute,
+ U+00B4 ISOdia -->
+<!ENTITY micro "&#181;"> <!-- micro sign, U+00B5 ISOnum -->
+<!ENTITY para "&#182;"> <!-- pilcrow sign = paragraph sign,
+ U+00B6 ISOnum -->
+<!ENTITY middot "&#183;"> <!-- middle dot = Georgian comma
+ = Greek middle dot, U+00B7 ISOnum -->
+<!ENTITY cedil "&#184;"> <!-- cedilla = spacing cedilla, U+00B8 ISOdia -->
+<!ENTITY sup1 "&#185;"> <!-- superscript one = superscript digit one,
+ U+00B9 ISOnum -->
+<!ENTITY ordm "&#186;"> <!-- masculine ordinal indicator,
+ U+00BA ISOnum -->
+<!ENTITY raquo "&#187;"> <!-- right-pointing double angle quotation mark
+ = right pointing guillemet, U+00BB ISOnum -->
+<!ENTITY frac14 "&#188;"> <!-- vulgar fraction one quarter
+ = fraction one quarter, U+00BC ISOnum -->
+<!ENTITY frac12 "&#189;"> <!-- vulgar fraction one half
+ = fraction one half, U+00BD ISOnum -->
+<!ENTITY frac34 "&#190;"> <!-- vulgar fraction three quarters
+ = fraction three quarters, U+00BE ISOnum -->
+<!ENTITY iquest "&#191;"> <!-- inverted question mark
+ = turned question mark, U+00BF ISOnum -->
+<!ENTITY Agrave "&#192;"> <!-- latin capital letter A with grave
+ = latin capital letter A grave,
+ U+00C0 ISOlat1 -->
+<!ENTITY Aacute "&#193;"> <!-- latin capital letter A with acute,
+ U+00C1 ISOlat1 -->
+<!ENTITY Acirc "&#194;"> <!-- latin capital letter A with circumflex,
+ U+00C2 ISOlat1 -->
+<!ENTITY Atilde "&#195;"> <!-- latin capital letter A with tilde,
+ U+00C3 ISOlat1 -->
+<!ENTITY Auml "&#196;"> <!-- latin capital letter A with diaeresis,
+ U+00C4 ISOlat1 -->
+<!ENTITY Aring "&#197;"> <!-- latin capital letter A with ring above
+ = latin capital letter A ring,
+ U+00C5 ISOlat1 -->
+<!ENTITY AElig "&#198;"> <!-- latin capital letter AE
+ = latin capital ligature AE,
+ U+00C6 ISOlat1 -->
+<!ENTITY Ccedil "&#199;"> <!-- latin capital letter C with cedilla,
+ U+00C7 ISOlat1 -->
+<!ENTITY Egrave "&#200;"> <!-- latin capital letter E with grave,
+ U+00C8 ISOlat1 -->
+<!ENTITY Eacute "&#201;"> <!-- latin capital letter E with acute,
+ U+00C9 ISOlat1 -->
+<!ENTITY Ecirc "&#202;"> <!-- latin capital letter E with circumflex,
+ U+00CA ISOlat1 -->
+<!ENTITY Euml "&#203;"> <!-- latin capital letter E with diaeresis,
+ U+00CB ISOlat1 -->
+<!ENTITY Igrave "&#204;"> <!-- latin capital letter I with grave,
+ U+00CC ISOlat1 -->
+<!ENTITY Iacute "&#205;"> <!-- latin capital letter I with acute,
+ U+00CD ISOlat1 -->
+<!ENTITY Icirc "&#206;"> <!-- latin capital letter I with circumflex,
+ U+00CE ISOlat1 -->
+<!ENTITY Iuml "&#207;"> <!-- latin capital letter I with diaeresis,
+ U+00CF ISOlat1 -->
+<!ENTITY ETH "&#208;"> <!-- latin capital letter ETH, U+00D0 ISOlat1 -->
+<!ENTITY Ntilde "&#209;"> <!-- latin capital letter N with tilde,
+ U+00D1 ISOlat1 -->
+<!ENTITY Ograve "&#210;"> <!-- latin capital letter O with grave,
+ U+00D2 ISOlat1 -->
+<!ENTITY Oacute "&#211;"> <!-- latin capital letter O with acute,
+ U+00D3 ISOlat1 -->
+<!ENTITY Ocirc "&#212;"> <!-- latin capital letter O with circumflex,
+ U+00D4 ISOlat1 -->
+<!ENTITY Otilde "&#213;"> <!-- latin capital letter O with tilde,
+ U+00D5 ISOlat1 -->
+<!ENTITY Ouml "&#214;"> <!-- latin capital letter O with diaeresis,
+ U+00D6 ISOlat1 -->
+<!ENTITY times "&#215;"> <!-- multiplication sign, U+00D7 ISOnum -->
+<!ENTITY Oslash "&#216;"> <!-- latin capital letter O with stroke
+ = latin capital letter O slash,
+ U+00D8 ISOlat1 -->
+<!ENTITY Ugrave "&#217;"> <!-- latin capital letter U with grave,
+ U+00D9 ISOlat1 -->
+<!ENTITY Uacute "&#218;"> <!-- latin capital letter U with acute,
+ U+00DA ISOlat1 -->
+<!ENTITY Ucirc "&#219;"> <!-- latin capital letter U with circumflex,
+ U+00DB ISOlat1 -->
+<!ENTITY Uuml "&#220;"> <!-- latin capital letter U with diaeresis,
+ U+00DC ISOlat1 -->
+<!ENTITY Yacute "&#221;"> <!-- latin capital letter Y with acute,
+ U+00DD ISOlat1 -->
+<!ENTITY THORN "&#222;"> <!-- latin capital letter THORN,
+ U+00DE ISOlat1 -->
+<!ENTITY szlig "&#223;"> <!-- latin small letter sharp s = ess-zed,
+ U+00DF ISOlat1 -->
+<!ENTITY agrave "&#224;"> <!-- latin small letter a with grave
+ = latin small letter a grave,
+ U+00E0 ISOlat1 -->
+<!ENTITY aacute "&#225;"> <!-- latin small letter a with acute,
+ U+00E1 ISOlat1 -->
+<!ENTITY acirc "&#226;"> <!-- latin small letter a with circumflex,
+ U+00E2 ISOlat1 -->
+<!ENTITY atilde "&#227;"> <!-- latin small letter a with tilde,
+ U+00E3 ISOlat1 -->
+<!ENTITY auml "&#228;"> <!-- latin small letter a with diaeresis,
+ U+00E4 ISOlat1 -->
+<!ENTITY aring "&#229;"> <!-- latin small letter a with ring above
+ = latin small letter a ring,
+ U+00E5 ISOlat1 -->
+<!ENTITY aelig "&#230;"> <!-- latin small letter ae
+ = latin small ligature ae, U+00E6 ISOlat1 -->
+<!ENTITY ccedil "&#231;"> <!-- latin small letter c with cedilla,
+ U+00E7 ISOlat1 -->
+<!ENTITY egrave "&#232;"> <!-- latin small letter e with grave,
+ U+00E8 ISOlat1 -->
+<!ENTITY eacute "&#233;"> <!-- latin small letter e with acute,
+ U+00E9 ISOlat1 -->
+<!ENTITY ecirc "&#234;"> <!-- latin small letter e with circumflex,
+ U+00EA ISOlat1 -->
+<!ENTITY euml "&#235;"> <!-- latin small letter e with diaeresis,
+ U+00EB ISOlat1 -->
+<!ENTITY igrave "&#236;"> <!-- latin small letter i with grave,
+ U+00EC ISOlat1 -->
+<!ENTITY iacute "&#237;"> <!-- latin small letter i with acute,
+ U+00ED ISOlat1 -->
+<!ENTITY icirc "&#238;"> <!-- latin small letter i with circumflex,
+ U+00EE ISOlat1 -->
+<!ENTITY iuml "&#239;"> <!-- latin small letter i with diaeresis,
+ U+00EF ISOlat1 -->
+<!ENTITY eth "&#240;"> <!-- latin small letter eth, U+00F0 ISOlat1 -->
+<!ENTITY ntilde "&#241;"> <!-- latin small letter n with tilde,
+ U+00F1 ISOlat1 -->
+<!ENTITY ograve "&#242;"> <!-- latin small letter o with grave,
+ U+00F2 ISOlat1 -->
+<!ENTITY oacute "&#243;"> <!-- latin small letter o with acute,
+ U+00F3 ISOlat1 -->
+<!ENTITY ocirc "&#244;"> <!-- latin small letter o with circumflex,
+ U+00F4 ISOlat1 -->
+<!ENTITY otilde "&#245;"> <!-- latin small letter o with tilde,
+ U+00F5 ISOlat1 -->
+<!ENTITY ouml "&#246;"> <!-- latin small letter o with diaeresis,
+ U+00F6 ISOlat1 -->
+<!ENTITY divide "&#247;"> <!-- division sign, U+00F7 ISOnum -->
+<!ENTITY oslash "&#248;"> <!-- latin small letter o with stroke,
+ = latin small letter o slash,
+ U+00F8 ISOlat1 -->
+<!ENTITY ugrave "&#249;"> <!-- latin small letter u with grave,
+ U+00F9 ISOlat1 -->
+<!ENTITY uacute "&#250;"> <!-- latin small letter u with acute,
+ U+00FA ISOlat1 -->
+<!ENTITY ucirc "&#251;"> <!-- latin small letter u with circumflex,
+ U+00FB ISOlat1 -->
+<!ENTITY uuml "&#252;"> <!-- latin small letter u with diaeresis,
+ U+00FC ISOlat1 -->
+<!ENTITY yacute "&#253;"> <!-- latin small letter y with acute,
+ U+00FD ISOlat1 -->
+<!ENTITY thorn "&#254;"> <!-- latin small letter thorn,
+ U+00FE ISOlat1 -->
+<!ENTITY yuml "&#255;"> <!-- latin small letter y with diaeresis,
+ U+00FF ISOlat1 -->
diff --git a/twisted/lore/xhtml-special.ent b/twisted/lore/xhtml-special.ent
new file mode 100644
index 0000000..ca358b2
--- /dev/null
+++ b/twisted/lore/xhtml-special.ent
@@ -0,0 +1,80 @@
+<!-- Special characters for XHTML -->
+
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLspecial PUBLIC
+ "-//W3C//ENTITIES Special for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent">
+ %HTMLspecial;
+-->
+
+<!-- Portions (C) International Organization for Standardization 1986:
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+
+<!-- Relevant ISO entity set is given unless names are newly introduced.
+ New names (i.e., not in ISO 8879 list) do not clash with any
+ existing ISO 8879 entity names. ISO 10646 character numbers
+ are given for each character, in hex. values are decimal
+ conversions of the ISO 10646 values and refer to the document
+ character set. Names are Unicode names.
+-->
+
+<!-- C0 Controls and Basic Latin -->
+<!ENTITY quot "&#34;"> <!-- quotation mark, U+0022 ISOnum -->
+<!ENTITY amp "&#38;#38;"> <!-- ampersand, U+0026 ISOnum -->
+<!ENTITY lt "&#38;#60;"> <!-- less-than sign, U+003C ISOnum -->
+<!ENTITY gt "&#62;"> <!-- greater-than sign, U+003E ISOnum -->
+<!ENTITY apos "&#39;"> <!-- apostrophe = APL quote, U+0027 ISOnum -->
+
+<!-- Latin Extended-A -->
+<!ENTITY OElig "&#338;"> <!-- latin capital ligature OE,
+ U+0152 ISOlat2 -->
+<!ENTITY oelig "&#339;"> <!-- latin small ligature oe, U+0153 ISOlat2 -->
+<!-- ligature is a misnomer, this is a separate character in some languages -->
+<!ENTITY Scaron "&#352;"> <!-- latin capital letter S with caron,
+ U+0160 ISOlat2 -->
+<!ENTITY scaron "&#353;"> <!-- latin small letter s with caron,
+ U+0161 ISOlat2 -->
+<!ENTITY Yuml "&#376;"> <!-- latin capital letter Y with diaeresis,
+ U+0178 ISOlat2 -->
+
+<!-- Spacing Modifier Letters -->
+<!ENTITY circ "&#710;"> <!-- modifier letter circumflex accent,
+ U+02C6 ISOpub -->
+<!ENTITY tilde "&#732;"> <!-- small tilde, U+02DC ISOdia -->
+
+<!-- General Punctuation -->
+<!ENTITY ensp "&#8194;"> <!-- en space, U+2002 ISOpub -->
+<!ENTITY emsp "&#8195;"> <!-- em space, U+2003 ISOpub -->
+<!ENTITY thinsp "&#8201;"> <!-- thin space, U+2009 ISOpub -->
+<!ENTITY zwnj "&#8204;"> <!-- zero width non-joiner,
+ U+200C NEW RFC 2070 -->
+<!ENTITY zwj "&#8205;"> <!-- zero width joiner, U+200D NEW RFC 2070 -->
+<!ENTITY lrm "&#8206;"> <!-- left-to-right mark, U+200E NEW RFC 2070 -->
+<!ENTITY rlm "&#8207;"> <!-- right-to-left mark, U+200F NEW RFC 2070 -->
+<!ENTITY ndash "&#8211;"> <!-- en dash, U+2013 ISOpub -->
+<!ENTITY mdash "&#8212;"> <!-- em dash, U+2014 ISOpub -->
+<!ENTITY lsquo "&#8216;"> <!-- left single quotation mark,
+ U+2018 ISOnum -->
+<!ENTITY rsquo "&#8217;"> <!-- right single quotation mark,
+ U+2019 ISOnum -->
+<!ENTITY sbquo "&#8218;"> <!-- single low-9 quotation mark, U+201A NEW -->
+<!ENTITY ldquo "&#8220;"> <!-- left double quotation mark,
+ U+201C ISOnum -->
+<!ENTITY rdquo "&#8221;"> <!-- right double quotation mark,
+ U+201D ISOnum -->
+<!ENTITY bdquo "&#8222;"> <!-- double low-9 quotation mark, U+201E NEW -->
+<!ENTITY dagger "&#8224;"> <!-- dagger, U+2020 ISOpub -->
+<!ENTITY Dagger "&#8225;"> <!-- double dagger, U+2021 ISOpub -->
+<!ENTITY permil "&#8240;"> <!-- per mille sign, U+2030 ISOtech -->
+<!ENTITY lsaquo "&#8249;"> <!-- single left-pointing angle quotation mark,
+ U+2039 ISO proposed -->
+<!-- lsaquo is proposed but not yet ISO standardized -->
+<!ENTITY rsaquo "&#8250;"> <!-- single right-pointing angle quotation mark,
+ U+203A ISO proposed -->
+<!-- rsaquo is proposed but not yet ISO standardized -->
+
+<!-- Currency Symbols -->
+<!ENTITY euro "&#8364;"> <!-- euro sign, U+20AC NEW -->
diff --git a/twisted/lore/xhtml-symbol.ent b/twisted/lore/xhtml-symbol.ent
new file mode 100644
index 0000000..63c2abf
--- /dev/null
+++ b/twisted/lore/xhtml-symbol.ent
@@ -0,0 +1,237 @@
+<!-- Mathematical, Greek and Symbolic characters for XHTML -->
+
+<!-- Character entity set. Typical invocation:
+ <!ENTITY % HTMLsymbol PUBLIC
+ "-//W3C//ENTITIES Symbols for XHTML//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent">
+ %HTMLsymbol;
+-->
+
+<!-- Portions (C) International Organization for Standardization 1986:
+ Permission to copy in any form is granted for use with
+ conforming SGML systems and applications as defined in
+ ISO 8879, provided this notice is included in all copies.
+-->
+
+<!-- Relevant ISO entity set is given unless names are newly introduced.
+ New names (i.e., not in ISO 8879 list) do not clash with any
+ existing ISO 8879 entity names. ISO 10646 character numbers
+ are given for each character, in hex. values are decimal
+ conversions of the ISO 10646 values and refer to the document
+ character set. Names are Unicode names.
+-->
+
+<!-- Latin Extended-B -->
+<!ENTITY fnof "&#402;"> <!-- latin small letter f with hook = function
+ = florin, U+0192 ISOtech -->
+
+<!-- Greek -->
+<!ENTITY Alpha "&#913;"> <!-- greek capital letter alpha, U+0391 -->
+<!ENTITY Beta "&#914;"> <!-- greek capital letter beta, U+0392 -->
+<!ENTITY Gamma "&#915;"> <!-- greek capital letter gamma,
+ U+0393 ISOgrk3 -->
+<!ENTITY Delta "&#916;"> <!-- greek capital letter delta,
+ U+0394 ISOgrk3 -->
+<!ENTITY Epsilon "&#917;"> <!-- greek capital letter epsilon, U+0395 -->
+<!ENTITY Zeta "&#918;"> <!-- greek capital letter zeta, U+0396 -->
+<!ENTITY Eta "&#919;"> <!-- greek capital letter eta, U+0397 -->
+<!ENTITY Theta "&#920;"> <!-- greek capital letter theta,
+ U+0398 ISOgrk3 -->
+<!ENTITY Iota "&#921;"> <!-- greek capital letter iota, U+0399 -->
+<!ENTITY Kappa "&#922;"> <!-- greek capital letter kappa, U+039A -->
+<!ENTITY Lambda "&#923;"> <!-- greek capital letter lamda,
+ U+039B ISOgrk3 -->
+<!ENTITY Mu "&#924;"> <!-- greek capital letter mu, U+039C -->
+<!ENTITY Nu "&#925;"> <!-- greek capital letter nu, U+039D -->
+<!ENTITY Xi "&#926;"> <!-- greek capital letter xi, U+039E ISOgrk3 -->
+<!ENTITY Omicron "&#927;"> <!-- greek capital letter omicron, U+039F -->
+<!ENTITY Pi "&#928;"> <!-- greek capital letter pi, U+03A0 ISOgrk3 -->
+<!ENTITY Rho "&#929;"> <!-- greek capital letter rho, U+03A1 -->
+<!-- there is no Sigmaf, and no U+03A2 character either -->
+<!ENTITY Sigma "&#931;"> <!-- greek capital letter sigma,
+ U+03A3 ISOgrk3 -->
+<!ENTITY Tau "&#932;"> <!-- greek capital letter tau, U+03A4 -->
+<!ENTITY Upsilon "&#933;"> <!-- greek capital letter upsilon,
+ U+03A5 ISOgrk3 -->
+<!ENTITY Phi "&#934;"> <!-- greek capital letter phi,
+ U+03A6 ISOgrk3 -->
+<!ENTITY Chi "&#935;"> <!-- greek capital letter chi, U+03A7 -->
+<!ENTITY Psi "&#936;"> <!-- greek capital letter psi,
+ U+03A8 ISOgrk3 -->
+<!ENTITY Omega "&#937;"> <!-- greek capital letter omega,
+ U+03A9 ISOgrk3 -->
+
+<!ENTITY alpha "&#945;"> <!-- greek small letter alpha,
+ U+03B1 ISOgrk3 -->
+<!ENTITY beta "&#946;"> <!-- greek small letter beta, U+03B2 ISOgrk3 -->
+<!ENTITY gamma "&#947;"> <!-- greek small letter gamma,
+ U+03B3 ISOgrk3 -->
+<!ENTITY delta "&#948;"> <!-- greek small letter delta,
+ U+03B4 ISOgrk3 -->
+<!ENTITY epsilon "&#949;"> <!-- greek small letter epsilon,
+ U+03B5 ISOgrk3 -->
+<!ENTITY zeta "&#950;"> <!-- greek small letter zeta, U+03B6 ISOgrk3 -->
+<!ENTITY eta "&#951;"> <!-- greek small letter eta, U+03B7 ISOgrk3 -->
+<!ENTITY theta "&#952;"> <!-- greek small letter theta,
+ U+03B8 ISOgrk3 -->
+<!ENTITY iota "&#953;"> <!-- greek small letter iota, U+03B9 ISOgrk3 -->
+<!ENTITY kappa "&#954;"> <!-- greek small letter kappa,
+ U+03BA ISOgrk3 -->
+<!ENTITY lambda "&#955;"> <!-- greek small letter lamda,
+ U+03BB ISOgrk3 -->
+<!ENTITY mu "&#956;"> <!-- greek small letter mu, U+03BC ISOgrk3 -->
+<!ENTITY nu "&#957;"> <!-- greek small letter nu, U+03BD ISOgrk3 -->
+<!ENTITY xi "&#958;"> <!-- greek small letter xi, U+03BE ISOgrk3 -->
+<!ENTITY omicron "&#959;"> <!-- greek small letter omicron, U+03BF NEW -->
+<!ENTITY pi "&#960;"> <!-- greek small letter pi, U+03C0 ISOgrk3 -->
+<!ENTITY rho "&#961;"> <!-- greek small letter rho, U+03C1 ISOgrk3 -->
+<!ENTITY sigmaf "&#962;"> <!-- greek small letter final sigma,
+ U+03C2 ISOgrk3 -->
+<!ENTITY sigma "&#963;"> <!-- greek small letter sigma,
+ U+03C3 ISOgrk3 -->
+<!ENTITY tau "&#964;"> <!-- greek small letter tau, U+03C4 ISOgrk3 -->
+<!ENTITY upsilon "&#965;"> <!-- greek small letter upsilon,
+ U+03C5 ISOgrk3 -->
+<!ENTITY phi "&#966;"> <!-- greek small letter phi, U+03C6 ISOgrk3 -->
+<!ENTITY chi "&#967;"> <!-- greek small letter chi, U+03C7 ISOgrk3 -->
+<!ENTITY psi "&#968;"> <!-- greek small letter psi, U+03C8 ISOgrk3 -->
+<!ENTITY omega "&#969;"> <!-- greek small letter omega,
+ U+03C9 ISOgrk3 -->
+<!ENTITY thetasym "&#977;"> <!-- greek theta symbol,
+ U+03D1 NEW -->
+<!ENTITY upsih "&#978;"> <!-- greek upsilon with hook symbol,
+ U+03D2 NEW -->
+<!ENTITY piv "&#982;"> <!-- greek pi symbol, U+03D6 ISOgrk3 -->
+
+<!-- General Punctuation -->
+<!ENTITY bull "&#8226;"> <!-- bullet = black small circle,
+ U+2022 ISOpub -->
+<!-- bullet is NOT the same as bullet operator, U+2219 -->
+<!ENTITY hellip "&#8230;"> <!-- horizontal ellipsis = three dot leader,
+ U+2026 ISOpub -->
+<!ENTITY prime "&#8242;"> <!-- prime = minutes = feet, U+2032 ISOtech -->
+<!ENTITY Prime "&#8243;"> <!-- double prime = seconds = inches,
+ U+2033 ISOtech -->
+<!ENTITY oline "&#8254;"> <!-- overline = spacing overscore,
+ U+203E NEW -->
+<!ENTITY frasl "&#8260;"> <!-- fraction slash, U+2044 NEW -->
+
+<!-- Letterlike Symbols -->
+<!ENTITY weierp "&#8472;"> <!-- script capital P = power set
+ = Weierstrass p, U+2118 ISOamso -->
+<!ENTITY image "&#8465;"> <!-- black-letter capital I = imaginary part,
+ U+2111 ISOamso -->
+<!ENTITY real "&#8476;"> <!-- black-letter capital R = real part symbol,
+ U+211C ISOamso -->
+<!ENTITY trade "&#8482;"> <!-- trade mark sign, U+2122 ISOnum -->
+<!ENTITY alefsym "&#8501;"> <!-- alef symbol = first transfinite cardinal,
+ U+2135 NEW -->
+<!-- alef symbol is NOT the same as hebrew letter alef,
+ U+05D0 although the same glyph could be used to depict both characters -->
+
+<!-- Arrows -->
+<!ENTITY larr "&#8592;"> <!-- leftwards arrow, U+2190 ISOnum -->
+<!ENTITY uarr "&#8593;"> <!-- upwards arrow, U+2191 ISOnum-->
+<!ENTITY rarr "&#8594;"> <!-- rightwards arrow, U+2192 ISOnum -->
+<!ENTITY darr "&#8595;"> <!-- downwards arrow, U+2193 ISOnum -->
+<!ENTITY harr "&#8596;"> <!-- left right arrow, U+2194 ISOamsa -->
+<!ENTITY crarr "&#8629;"> <!-- downwards arrow with corner leftwards
+ = carriage return, U+21B5 NEW -->
+<!ENTITY lArr "&#8656;"> <!-- leftwards double arrow, U+21D0 ISOtech -->
+<!-- Unicode does not say that lArr is the same as the 'is implied by' arrow
+ but also does not have any other character for that function. So lArr can
+ be used for 'is implied by' as ISOtech suggests -->
+<!ENTITY uArr "&#8657;"> <!-- upwards double arrow, U+21D1 ISOamsa -->
+<!ENTITY rArr "&#8658;"> <!-- rightwards double arrow,
+ U+21D2 ISOtech -->
+<!-- Unicode does not say this is the 'implies' character but does not have
+ another character with this function so rArr can be used for 'implies'
+ as ISOtech suggests -->
+<!ENTITY dArr "&#8659;"> <!-- downwards double arrow, U+21D3 ISOamsa -->
+<!ENTITY hArr "&#8660;"> <!-- left right double arrow,
+ U+21D4 ISOamsa -->
+
+<!-- Mathematical Operators -->
+<!ENTITY forall "&#8704;"> <!-- for all, U+2200 ISOtech -->
+<!ENTITY part "&#8706;"> <!-- partial differential, U+2202 ISOtech -->
+<!ENTITY exist "&#8707;"> <!-- there exists, U+2203 ISOtech -->
+<!ENTITY empty "&#8709;"> <!-- empty set = null set, U+2205 ISOamso -->
+<!ENTITY nabla "&#8711;"> <!-- nabla = backward difference,
+ U+2207 ISOtech -->
+<!ENTITY isin "&#8712;"> <!-- element of, U+2208 ISOtech -->
+<!ENTITY notin "&#8713;"> <!-- not an element of, U+2209 ISOtech -->
+<!ENTITY ni "&#8715;"> <!-- contains as member, U+220B ISOtech -->
+<!ENTITY prod "&#8719;"> <!-- n-ary product = product sign,
+ U+220F ISOamsb -->
+<!-- prod is NOT the same character as U+03A0 'greek capital letter pi' though
+ the same glyph might be used for both -->
+<!ENTITY sum "&#8721;"> <!-- n-ary summation, U+2211 ISOamsb -->
+<!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'
+ though the same glyph might be used for both -->
+<!ENTITY minus "&#8722;"> <!-- minus sign, U+2212 ISOtech -->
+<!ENTITY lowast "&#8727;"> <!-- asterisk operator, U+2217 ISOtech -->
+<!ENTITY radic "&#8730;"> <!-- square root = radical sign,
+ U+221A ISOtech -->
+<!ENTITY prop "&#8733;"> <!-- proportional to, U+221D ISOtech -->
+<!ENTITY infin "&#8734;"> <!-- infinity, U+221E ISOtech -->
+<!ENTITY ang "&#8736;"> <!-- angle, U+2220 ISOamso -->
+<!ENTITY and "&#8743;"> <!-- logical and = wedge, U+2227 ISOtech -->
+<!ENTITY or "&#8744;"> <!-- logical or = vee, U+2228 ISOtech -->
+<!ENTITY cap "&#8745;"> <!-- intersection = cap, U+2229 ISOtech -->
+<!ENTITY cup "&#8746;"> <!-- union = cup, U+222A ISOtech -->
+<!ENTITY int "&#8747;"> <!-- integral, U+222B ISOtech -->
+<!ENTITY there4 "&#8756;"> <!-- therefore, U+2234 ISOtech -->
+<!ENTITY sim "&#8764;"> <!-- tilde operator = varies with = similar to,
+ U+223C ISOtech -->
+<!-- tilde operator is NOT the same character as the tilde, U+007E,
+ although the same glyph might be used to represent both -->
+<!ENTITY cong "&#8773;"> <!-- approximately equal to, U+2245 ISOtech -->
+<!ENTITY asymp "&#8776;"> <!-- almost equal to = asymptotic to,
+ U+2248 ISOamsr -->
+<!ENTITY ne "&#8800;"> <!-- not equal to, U+2260 ISOtech -->
+<!ENTITY equiv "&#8801;"> <!-- identical to, U+2261 ISOtech -->
+<!ENTITY le "&#8804;"> <!-- less-than or equal to, U+2264 ISOtech -->
+<!ENTITY ge "&#8805;"> <!-- greater-than or equal to,
+ U+2265 ISOtech -->
+<!ENTITY sub "&#8834;"> <!-- subset of, U+2282 ISOtech -->
+<!ENTITY sup "&#8835;"> <!-- superset of, U+2283 ISOtech -->
+<!ENTITY nsub "&#8836;"> <!-- not a subset of, U+2284 ISOamsn -->
+<!ENTITY sube "&#8838;"> <!-- subset of or equal to, U+2286 ISOtech -->
+<!ENTITY supe "&#8839;"> <!-- superset of or equal to,
+ U+2287 ISOtech -->
+<!ENTITY oplus "&#8853;"> <!-- circled plus = direct sum,
+ U+2295 ISOamsb -->
+<!ENTITY otimes "&#8855;"> <!-- circled times = vector product,
+ U+2297 ISOamsb -->
+<!ENTITY perp "&#8869;"> <!-- up tack = orthogonal to = perpendicular,
+ U+22A5 ISOtech -->
+<!ENTITY sdot "&#8901;"> <!-- dot operator, U+22C5 ISOamsb -->
+<!-- dot operator is NOT the same character as U+00B7 middle dot -->
+
+<!-- Miscellaneous Technical -->
+<!ENTITY lceil "&#8968;"> <!-- left ceiling = APL upstile,
+ U+2308 ISOamsc -->
+<!ENTITY rceil "&#8969;"> <!-- right ceiling, U+2309 ISOamsc -->
+<!ENTITY lfloor "&#8970;"> <!-- left floor = APL downstile,
+ U+230A ISOamsc -->
+<!ENTITY rfloor "&#8971;"> <!-- right floor, U+230B ISOamsc -->
+<!ENTITY lang "&#9001;"> <!-- left-pointing angle bracket = bra,
+ U+2329 ISOtech -->
+<!-- lang is NOT the same character as U+003C 'less than sign'
+ or U+2039 'single left-pointing angle quotation mark' -->
+<!ENTITY rang "&#9002;"> <!-- right-pointing angle bracket = ket,
+ U+232A ISOtech -->
+<!-- rang is NOT the same character as U+003E 'greater than sign'
+ or U+203A 'single right-pointing angle quotation mark' -->
+
+<!-- Geometric Shapes -->
+<!ENTITY loz "&#9674;"> <!-- lozenge, U+25CA ISOpub -->
+
+<!-- Miscellaneous Symbols -->
+<!ENTITY spades "&#9824;"> <!-- black spade suit, U+2660 ISOpub -->
+<!-- black here seems to mean filled as opposed to hollow -->
+<!ENTITY clubs "&#9827;"> <!-- black club suit = shamrock,
+ U+2663 ISOpub -->
+<!ENTITY hearts "&#9829;"> <!-- black heart suit = valentine,
+ U+2665 ISOpub -->
+<!ENTITY diams "&#9830;"> <!-- black diamond suit, U+2666 ISOpub -->
diff --git a/twisted/lore/xhtml1-strict.dtd b/twisted/lore/xhtml1-strict.dtd
new file mode 100644
index 0000000..2927b9e
--- /dev/null
+++ b/twisted/lore/xhtml1-strict.dtd
@@ -0,0 +1,978 @@
+<!--
+ Extensible HTML version 1.0 Strict DTD
+
+ This is the same as HTML 4 Strict except for
+ changes due to the differences between XML and SGML.
+
+ Namespace = http://www.w3.org/1999/xhtml
+
+ For further information, see: http://www.w3.org/TR/xhtml1
+
+ Copyright (c) 1998-2002 W3C (MIT, INRIA, Keio),
+ All Rights Reserved.
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
+
+ $Revision: 1.1 $
+ $Date: 2002/08/01 13:56:03 $
+
+-->
+
+<!--================ Character mnemonic entities =========================-->
+
+<!ENTITY % HTMLlat1 PUBLIC
+ "-//W3C//ENTITIES Latin 1 for XHTML//EN"
+ "xhtml-lat1.ent">
+%HTMLlat1;
+
+<!ENTITY % HTMLsymbol PUBLIC
+ "-//W3C//ENTITIES Symbols for XHTML//EN"
+ "xhtml-symbol.ent">
+%HTMLsymbol;
+
+<!ENTITY % HTMLspecial PUBLIC
+ "-//W3C//ENTITIES Special for XHTML//EN"
+ "xhtml-special.ent">
+%HTMLspecial;
+
+<!--================== Imported Names ====================================-->
+
+<!ENTITY % ContentType "CDATA">
+ <!-- media type, as per [RFC2045] -->
+
+<!ENTITY % ContentTypes "CDATA">
+ <!-- comma-separated list of media types, as per [RFC2045] -->
+
+<!ENTITY % Charset "CDATA">
+ <!-- a character encoding, as per [RFC2045] -->
+
+<!ENTITY % Charsets "CDATA">
+ <!-- a space separated list of character encodings, as per [RFC2045] -->
+
+<!ENTITY % LanguageCode "NMTOKEN">
+ <!-- a language code, as per [RFC3066] -->
+
+<!ENTITY % Character "CDATA">
+ <!-- a single character, as per section 2.2 of [XML] -->
+
+<!ENTITY % Number "CDATA">
+ <!-- one or more digits -->
+
+<!ENTITY % LinkTypes "CDATA">
+ <!-- space-separated list of link types -->
+
+<!ENTITY % MediaDesc "CDATA">
+ <!-- single or comma-separated list of media descriptors -->
+
+<!ENTITY % URI "CDATA">
+ <!-- a Uniform Resource Identifier, see [RFC2396] -->
+
+<!ENTITY % UriList "CDATA">
+ <!-- a space separated list of Uniform Resource Identifiers -->
+
+<!ENTITY % Datetime "CDATA">
+ <!-- date and time information. ISO date format -->
+
+<!ENTITY % Script "CDATA">
+ <!-- script expression -->
+
+<!ENTITY % StyleSheet "CDATA">
+ <!-- style sheet data -->
+
+<!ENTITY % Text "CDATA">
+ <!-- used for titles etc. -->
+
+<!ENTITY % Length "CDATA">
+ <!-- nn for pixels or nn% for percentage length -->
+
+<!ENTITY % MultiLength "CDATA">
+ <!-- pixel, percentage, or relative -->
+
+<!ENTITY % Pixels "CDATA">
+ <!-- integer representing length in pixels -->
+
+<!-- these are used for image maps -->
+
+<!ENTITY % Shape "(rect|circle|poly|default)">
+
+<!ENTITY % Coords "CDATA">
+ <!-- comma separated list of lengths -->
+
+<!--=================== Generic Attributes ===============================-->
+
+<!-- core attributes common to most elements
+ id document-wide unique id
+ class space separated list of classes
+ style associated style info
+ title advisory title/amplification
+-->
+<!ENTITY % coreattrs
+ "id ID #IMPLIED
+ class CDATA #IMPLIED
+ style %StyleSheet; #IMPLIED
+ title %Text; #IMPLIED"
+ >
+
+<!-- internationalization attributes
+ lang language code (backwards compatible)
+ xml:lang language code (as per XML 1.0 spec)
+ dir direction for weak/neutral text
+-->
+<!ENTITY % i18n
+ "lang %LanguageCode; #IMPLIED
+ xml:lang %LanguageCode; #IMPLIED
+ dir (ltr|rtl) #IMPLIED"
+ >
+
+<!-- attributes for common UI events
+ onclick a pointer button was clicked
+ ondblclick a pointer button was double clicked
+ onmousedown a pointer button was pressed down
+ onmouseup a pointer button was released
+ onmousemove a pointer was moved onto the element
+ onmouseout a pointer was moved away from the element
+ onkeypress a key was pressed and released
+ onkeydown a key was pressed down
+ onkeyup a key was released
+-->
+<!ENTITY % events
+ "onclick %Script; #IMPLIED
+ ondblclick %Script; #IMPLIED
+ onmousedown %Script; #IMPLIED
+ onmouseup %Script; #IMPLIED
+ onmouseover %Script; #IMPLIED
+ onmousemove %Script; #IMPLIED
+ onmouseout %Script; #IMPLIED
+ onkeypress %Script; #IMPLIED
+ onkeydown %Script; #IMPLIED
+ onkeyup %Script; #IMPLIED"
+ >
+
+<!-- attributes for elements that can get the focus
+ accesskey accessibility key character
+ tabindex position in tabbing order
+ onfocus the element got the focus
+ onblur the element lost the focus
+-->
+<!ENTITY % focus
+ "accesskey %Character; #IMPLIED
+ tabindex %Number; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED"
+ >
+
+<!ENTITY % attrs "%coreattrs; %i18n; %events;">
+
+<!--=================== Text Elements ====================================-->
+
+<!ENTITY % special.pre
+ "br | span | bdo | map">
+
+
+<!ENTITY % special
+ "%special.pre; | object | img ">
+
+<!ENTITY % fontstyle "tt | i | b | big | small ">
+
+<!ENTITY % phrase "em | strong | dfn | code | q |
+ samp | kbd | var | cite | abbr | acronym | sub | sup ">
+
+<!ENTITY % inline.forms "input | select | textarea | label | button">
+
+<!-- these can occur at block or inline level -->
+<!ENTITY % misc.inline "ins | del | script">
+
+<!-- these can only occur at block level -->
+<!ENTITY % misc "noscript | %misc.inline;">
+
+<!ENTITY % inline "a | %special; | %fontstyle; | %phrase; | %inline.forms;">
+
+<!-- %Inline; covers inline or "text-level" elements -->
+<!ENTITY % Inline "(#PCDATA | %inline; | %misc.inline;)*">
+
+<!--================== Block level elements ==============================-->
+
+<!ENTITY % heading "h1|h2|h3|h4|h5|h6">
+<!ENTITY % lists "ul | ol | dl">
+<!ENTITY % blocktext "pre | hr | blockquote | address">
+
+<!ENTITY % block
+ "p | %heading; | div | %lists; | %blocktext; | fieldset | table">
+
+<!ENTITY % Block "(%block; | form | %misc;)*">
+
+<!-- %Flow; mixes block and inline and is used for list items etc. -->
+<!ENTITY % Flow "(#PCDATA | %block; | form | %inline; | %misc;)*">
+
+<!--================== Content models for exclusions =====================-->
+
+<!-- a elements use %Inline; excluding a -->
+
+<!ENTITY % a.content
+ "(#PCDATA | %special; | %fontstyle; | %phrase; | %inline.forms; | %misc.inline;)*">
+
+<!-- pre uses %Inline excluding big, small, sup or sup -->
+
+<!ENTITY % pre.content
+ "(#PCDATA | a | %fontstyle; | %phrase; | %special.pre; | %misc.inline;
+ | %inline.forms;)*">
+
+<!-- form uses %Block; excluding form -->
+
+<!ENTITY % form.content "(%block; | %misc;)*">
+
+<!-- button uses %Flow; but excludes a, form and form controls -->
+
+<!ENTITY % button.content
+ "(#PCDATA | p | %heading; | div | %lists; | %blocktext; |
+ table | %special; | %fontstyle; | %phrase; | %misc;)*">
+
+<!--================ Document Structure ==================================-->
+
+<!-- the namespace URI designates the document profile -->
+
+<!ELEMENT html (head, body)>
+<!ATTLIST html
+ %i18n;
+ id ID #IMPLIED
+ xmlns %URI; #FIXED 'http://www.w3.org/1999/xhtml'
+ >
+
+<!--================ Document Head =======================================-->
+
+<!ENTITY % head.misc "(script|style|meta|link|object)*">
+
+<!-- content model is %head.misc; combined with a single
+ title and an optional base element in any order -->
+
+<!ELEMENT head (%head.misc;,
+ ((title, %head.misc;, (base, %head.misc;)?) |
+ (base, %head.misc;, (title, %head.misc;))))>
+
+<!ATTLIST head
+ %i18n;
+ id ID #IMPLIED
+ profile %URI; #IMPLIED
+ >
+
+<!-- The title element is not considered part of the flow of text.
+ It should be displayed, for example as the page header or
+ window title. Exactly one title is required per document.
+ -->
+<!ELEMENT title (#PCDATA)>
+<!ATTLIST title
+ %i18n;
+ id ID #IMPLIED
+ >
+
+<!-- document base URI -->
+
+<!ELEMENT base EMPTY>
+<!ATTLIST base
+ href %URI; #REQUIRED
+ id ID #IMPLIED
+ >
+
+<!-- generic metainformation -->
+<!ELEMENT meta EMPTY>
+<!ATTLIST meta
+ %i18n;
+ id ID #IMPLIED
+ http-equiv CDATA #IMPLIED
+ name CDATA #IMPLIED
+ content CDATA #REQUIRED
+ scheme CDATA #IMPLIED
+ >
+
+<!--
+ Relationship values can be used in principle:
+
+ a) for document specific toolbars/menus when used
+ with the link element in document head e.g.
+ start, contents, previous, next, index, end, help
+ b) to link to a separate style sheet (rel="stylesheet")
+ c) to make a link to a script (rel="script")
+ d) by stylesheets to control how collections of
+ html nodes are rendered into printed documents
+ e) to make a link to a printable version of this document
+ e.g. a PostScript or PDF version (rel="alternate" media="print")
+-->
+
+<!ELEMENT link EMPTY>
+<!ATTLIST link
+ %attrs;
+ charset %Charset; #IMPLIED
+ href %URI; #IMPLIED
+ hreflang %LanguageCode; #IMPLIED
+ type %ContentType; #IMPLIED
+ rel %LinkTypes; #IMPLIED
+ rev %LinkTypes; #IMPLIED
+ media %MediaDesc; #IMPLIED
+ >
+
+<!-- style info, which may include CDATA sections -->
+<!ELEMENT style (#PCDATA)>
+<!ATTLIST style
+ %i18n;
+ id ID #IMPLIED
+ type %ContentType; #REQUIRED
+ media %MediaDesc; #IMPLIED
+ title %Text; #IMPLIED
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!-- script statements, which may include CDATA sections -->
+<!ELEMENT script (#PCDATA)>
+<!ATTLIST script
+ id ID #IMPLIED
+ charset %Charset; #IMPLIED
+ type %ContentType; #REQUIRED
+ src %URI; #IMPLIED
+ defer (defer) #IMPLIED
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!-- alternate content container for non script-based rendering -->
+
+<!ELEMENT noscript %Block;>
+<!ATTLIST noscript
+ %attrs;
+ >
+
+<!--=================== Document Body ====================================-->
+
+<!ELEMENT body %Block;>
+<!ATTLIST body
+ %attrs;
+ onload %Script; #IMPLIED
+ onunload %Script; #IMPLIED
+ >
+
+<!ELEMENT div %Flow;> <!-- generic language/style container -->
+<!ATTLIST div
+ %attrs;
+ >
+
+<!--=================== Paragraphs =======================================-->
+
+<!ELEMENT p %Inline;>
+<!ATTLIST p
+ %attrs;
+ >
+
+<!--=================== Headings =========================================-->
+
+<!--
+ There are six levels of headings from h1 (the most important)
+ to h6 (the least important).
+-->
+
+<!ELEMENT h1 %Inline;>
+<!ATTLIST h1
+ %attrs;
+ >
+
+<!ELEMENT h2 %Inline;>
+<!ATTLIST h2
+ %attrs;
+ >
+
+<!ELEMENT h3 %Inline;>
+<!ATTLIST h3
+ %attrs;
+ >
+
+<!ELEMENT h4 %Inline;>
+<!ATTLIST h4
+ %attrs;
+ >
+
+<!ELEMENT h5 %Inline;>
+<!ATTLIST h5
+ %attrs;
+ >
+
+<!ELEMENT h6 %Inline;>
+<!ATTLIST h6
+ %attrs;
+ >
+
+<!--=================== Lists ============================================-->
+
+<!-- Unordered list -->
+
+<!ELEMENT ul (li)+>
+<!ATTLIST ul
+ %attrs;
+ >
+
+<!-- Ordered (numbered) list -->
+
+<!ELEMENT ol (li)+>
+<!ATTLIST ol
+ %attrs;
+ >
+
+<!-- list item -->
+
+<!ELEMENT li %Flow;>
+<!ATTLIST li
+ %attrs;
+ >
+
+<!-- definition lists - dt for term, dd for its definition -->
+
+<!ELEMENT dl (dt|dd)+>
+<!ATTLIST dl
+ %attrs;
+ >
+
+<!ELEMENT dt %Inline;>
+<!ATTLIST dt
+ %attrs;
+ >
+
+<!ELEMENT dd %Flow;>
+<!ATTLIST dd
+ %attrs;
+ >
+
+<!--=================== Address ==========================================-->
+
+<!-- information on author -->
+
+<!ELEMENT address %Inline;>
+<!ATTLIST address
+ %attrs;
+ >
+
+<!--=================== Horizontal Rule ==================================-->
+
+<!ELEMENT hr EMPTY>
+<!ATTLIST hr
+ %attrs;
+ >
+
+<!--=================== Preformatted Text ================================-->
+
+<!-- content is %Inline; excluding "img|object|big|small|sub|sup" -->
+
+<!ELEMENT pre %pre.content;>
+<!ATTLIST pre
+ %attrs;
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!--=================== Block-like Quotes ================================-->
+
+<!ELEMENT blockquote %Block;>
+<!ATTLIST blockquote
+ %attrs;
+ cite %URI; #IMPLIED
+ >
+
+<!--=================== Inserted/Deleted Text ============================-->
+
+<!--
+ ins/del are allowed in block and inline content, but its
+ inappropriate to include block content within an ins element
+ occurring in inline content.
+-->
+<!ELEMENT ins %Flow;>
+<!ATTLIST ins
+ %attrs;
+ cite %URI; #IMPLIED
+ datetime %Datetime; #IMPLIED
+ >
+
+<!ELEMENT del %Flow;>
+<!ATTLIST del
+ %attrs;
+ cite %URI; #IMPLIED
+ datetime %Datetime; #IMPLIED
+ >
+
+<!--================== The Anchor Element ================================-->
+
+<!-- content is %Inline; except that anchors shouldn't be nested -->
+
+<!ELEMENT a %a.content;>
+<!ATTLIST a
+ %attrs;
+ %focus;
+ charset %Charset; #IMPLIED
+ type %ContentType; #IMPLIED
+ name NMTOKEN #IMPLIED
+ href %URI; #IMPLIED
+ hreflang %LanguageCode; #IMPLIED
+ rel %LinkTypes; #IMPLIED
+ rev %LinkTypes; #IMPLIED
+ shape %Shape; "rect"
+ coords %Coords; #IMPLIED
+ >
+
+<!--===================== Inline Elements ================================-->
+
+<!ELEMENT span %Inline;> <!-- generic language/style container -->
+<!ATTLIST span
+ %attrs;
+ >
+
+<!ELEMENT bdo %Inline;> <!-- I18N BiDi over-ride -->
+<!ATTLIST bdo
+ %coreattrs;
+ %events;
+ lang %LanguageCode; #IMPLIED
+ xml:lang %LanguageCode; #IMPLIED
+ dir (ltr|rtl) #REQUIRED
+ >
+
+<!ELEMENT br EMPTY> <!-- forced line break -->
+<!ATTLIST br
+ %coreattrs;
+ >
+
+<!ELEMENT em %Inline;> <!-- emphasis -->
+<!ATTLIST em %attrs;>
+
+<!ELEMENT strong %Inline;> <!-- strong emphasis -->
+<!ATTLIST strong %attrs;>
+
+<!ELEMENT dfn %Inline;> <!-- definitional -->
+<!ATTLIST dfn %attrs;>
+
+<!ELEMENT code %Inline;> <!-- program code -->
+<!ATTLIST code %attrs;>
+
+<!ELEMENT samp %Inline;> <!-- sample -->
+<!ATTLIST samp %attrs;>
+
+<!ELEMENT kbd %Inline;> <!-- something user would type -->
+<!ATTLIST kbd %attrs;>
+
+<!ELEMENT var %Inline;> <!-- variable -->
+<!ATTLIST var %attrs;>
+
+<!ELEMENT cite %Inline;> <!-- citation -->
+<!ATTLIST cite %attrs;>
+
+<!ELEMENT abbr %Inline;> <!-- abbreviation -->
+<!ATTLIST abbr %attrs;>
+
+<!ELEMENT acronym %Inline;> <!-- acronym -->
+<!ATTLIST acronym %attrs;>
+
+<!ELEMENT q %Inline;> <!-- inlined quote -->
+<!ATTLIST q
+ %attrs;
+ cite %URI; #IMPLIED
+ >
+
+<!ELEMENT sub %Inline;> <!-- subscript -->
+<!ATTLIST sub %attrs;>
+
+<!ELEMENT sup %Inline;> <!-- superscript -->
+<!ATTLIST sup %attrs;>
+
+<!ELEMENT tt %Inline;> <!-- fixed pitch font -->
+<!ATTLIST tt %attrs;>
+
+<!ELEMENT i %Inline;> <!-- italic font -->
+<!ATTLIST i %attrs;>
+
+<!ELEMENT b %Inline;> <!-- bold font -->
+<!ATTLIST b %attrs;>
+
+<!ELEMENT big %Inline;> <!-- bigger font -->
+<!ATTLIST big %attrs;>
+
+<!ELEMENT small %Inline;> <!-- smaller font -->
+<!ATTLIST small %attrs;>
+
+<!--==================== Object ======================================-->
+<!--
+ object is used to embed objects as part of HTML pages.
+ param elements should precede other content. Parameters
+ can also be expressed as attribute/value pairs on the
+ object element itself when brevity is desired.
+-->
+
+<!ELEMENT object (#PCDATA | param | %block; | form | %inline; | %misc;)*>
+<!ATTLIST object
+ %attrs;
+ declare (declare) #IMPLIED
+ classid %URI; #IMPLIED
+ codebase %URI; #IMPLIED
+ data %URI; #IMPLIED
+ type %ContentType; #IMPLIED
+ codetype %ContentType; #IMPLIED
+ archive %UriList; #IMPLIED
+ standby %Text; #IMPLIED
+ height %Length; #IMPLIED
+ width %Length; #IMPLIED
+ usemap %URI; #IMPLIED
+ name NMTOKEN #IMPLIED
+ tabindex %Number; #IMPLIED
+ >
+
+<!--
+ param is used to supply a named property value.
+ In XML it would seem natural to follow RDF and support an
+ abbreviated syntax where the param elements are replaced
+ by attribute value pairs on the object start tag.
+-->
+<!ELEMENT param EMPTY>
+<!ATTLIST param
+ id ID #IMPLIED
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ valuetype (data|ref|object) "data"
+ type %ContentType; #IMPLIED
+ >
+
+<!--=================== Images ===========================================-->
+
+<!--
+ To avoid accessibility problems for people who aren't
+ able to see the image, you should provide a text
+ description using the alt and longdesc attributes.
+ In addition, avoid the use of server-side image maps.
+ Note that in this DTD there is no name attribute. That
+ is only available in the transitional and frameset DTD.
+-->
+
+<!ELEMENT img EMPTY>
+<!ATTLIST img
+ %attrs;
+ src %URI; #REQUIRED
+ alt %Text; #REQUIRED
+ longdesc %URI; #IMPLIED
+ height %Length; #IMPLIED
+ width %Length; #IMPLIED
+ usemap %URI; #IMPLIED
+ ismap (ismap) #IMPLIED
+ >
+
+<!-- usemap points to a map element which may be in this document
+ or an external document, although the latter is not widely supported -->
+
+<!--================== Client-side image maps ============================-->
+
+<!-- These can be placed in the same document or grouped in a
+ separate document although this isn't yet widely supported -->
+
+<!ELEMENT map ((%block; | form | %misc;)+ | area+)>
+<!ATTLIST map
+ %i18n;
+ %events;
+ id ID #REQUIRED
+ class CDATA #IMPLIED
+ style %StyleSheet; #IMPLIED
+ title %Text; #IMPLIED
+ name NMTOKEN #IMPLIED
+ >
+
+<!ELEMENT area EMPTY>
+<!ATTLIST area
+ %attrs;
+ %focus;
+ shape %Shape; "rect"
+ coords %Coords; #IMPLIED
+ href %URI; #IMPLIED
+ nohref (nohref) #IMPLIED
+ alt %Text; #REQUIRED
+ >
+
+<!--================ Forms ===============================================-->
+<!ELEMENT form %form.content;> <!-- forms shouldn't be nested -->
+
+<!ATTLIST form
+ %attrs;
+ action %URI; #REQUIRED
+ method (get|post) "get"
+ enctype %ContentType; "application/x-www-form-urlencoded"
+ onsubmit %Script; #IMPLIED
+ onreset %Script; #IMPLIED
+ accept %ContentTypes; #IMPLIED
+ accept-charset %Charsets; #IMPLIED
+ >
+
+<!--
+ Each label must not contain more than ONE field
+ Label elements shouldn't be nested.
+-->
+<!ELEMENT label %Inline;>
+<!ATTLIST label
+ %attrs;
+ for IDREF #IMPLIED
+ accesskey %Character; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED
+ >
+
+<!ENTITY % InputType
+ "(text | password | checkbox |
+ radio | submit | reset |
+ file | hidden | image | button)"
+ >
+
+<!-- the name attribute is required for all but submit & reset -->
+
+<!ELEMENT input EMPTY> <!-- form control -->
+<!ATTLIST input
+ %attrs;
+ %focus;
+ type %InputType; "text"
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ checked (checked) #IMPLIED
+ disabled (disabled) #IMPLIED
+ readonly (readonly) #IMPLIED
+ size CDATA #IMPLIED
+ maxlength %Number; #IMPLIED
+ src %URI; #IMPLIED
+ alt CDATA #IMPLIED
+ usemap %URI; #IMPLIED
+ onselect %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ accept %ContentTypes; #IMPLIED
+ >
+
+<!ELEMENT select (optgroup|option)+> <!-- option selector -->
+<!ATTLIST select
+ %attrs;
+ name CDATA #IMPLIED
+ size %Number; #IMPLIED
+ multiple (multiple) #IMPLIED
+ disabled (disabled) #IMPLIED
+ tabindex %Number; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ >
+
+<!ELEMENT optgroup (option)+> <!-- option group -->
+<!ATTLIST optgroup
+ %attrs;
+ disabled (disabled) #IMPLIED
+ label %Text; #REQUIRED
+ >
+
+<!ELEMENT option (#PCDATA)> <!-- selectable choice -->
+<!ATTLIST option
+ %attrs;
+ selected (selected) #IMPLIED
+ disabled (disabled) #IMPLIED
+ label %Text; #IMPLIED
+ value CDATA #IMPLIED
+ >
+
+<!ELEMENT textarea (#PCDATA)> <!-- multi-line text field -->
+<!ATTLIST textarea
+ %attrs;
+ %focus;
+ name CDATA #IMPLIED
+ rows %Number; #REQUIRED
+ cols %Number; #REQUIRED
+ disabled (disabled) #IMPLIED
+ readonly (readonly) #IMPLIED
+ onselect %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ >
+
+<!--
+ The fieldset element is used to group form fields.
+ Only one legend element should occur in the content
+ and if present should only be preceded by whitespace.
+-->
+<!ELEMENT fieldset (#PCDATA | legend | %block; | form | %inline; | %misc;)*>
+<!ATTLIST fieldset
+ %attrs;
+ >
+
+<!ELEMENT legend %Inline;> <!-- fieldset label -->
+<!ATTLIST legend
+ %attrs;
+ accesskey %Character; #IMPLIED
+ >
+
+<!--
+ Content is %Flow; excluding a, form and form controls
+-->
+<!ELEMENT button %button.content;> <!-- push button -->
+<!ATTLIST button
+ %attrs;
+ %focus;
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ type (button|submit|reset) "submit"
+ disabled (disabled) #IMPLIED
+ >
+
+<!--======================= Tables =======================================-->
+
+<!-- Derived from IETF HTML table standard, see [RFC1942] -->
+
+<!--
+ The border attribute sets the thickness of the frame around the
+ table. The default units are screen pixels.
+
+ The frame attribute specifies which parts of the frame around
+ the table should be rendered. The values are not the same as
+ CALS to avoid a name clash with the valign attribute.
+-->
+<!ENTITY % TFrame "(void|above|below|hsides|lhs|rhs|vsides|box|border)">
+
+<!--
+ The rules attribute defines which rules to draw between cells:
+
+ If rules is absent then assume:
+ "none" if border is absent or border="0" otherwise "all"
+-->
+
+<!ENTITY % TRules "(none | groups | rows | cols | all)">
+
+<!-- horizontal alignment attributes for cell contents
+
+ char alignment char, e.g. char=':'
+ charoff offset for alignment char
+-->
+<!ENTITY % cellhalign
+ "align (left|center|right|justify|char) #IMPLIED
+ char %Character; #IMPLIED
+ charoff %Length; #IMPLIED"
+ >
+
+<!-- vertical alignment attributes for cell contents -->
+<!ENTITY % cellvalign
+ "valign (top|middle|bottom|baseline) #IMPLIED"
+ >
+
+<!ELEMENT table
+ (caption?, (col*|colgroup*), thead?, tfoot?, (tbody+|tr+))>
+<!ELEMENT caption %Inline;>
+<!ELEMENT thead (tr)+>
+<!ELEMENT tfoot (tr)+>
+<!ELEMENT tbody (tr)+>
+<!ELEMENT colgroup (col)*>
+<!ELEMENT col EMPTY>
+<!ELEMENT tr (th|td)+>
+<!ELEMENT th %Flow;>
+<!ELEMENT td %Flow;>
+
+<!ATTLIST table
+ %attrs;
+ summary %Text; #IMPLIED
+ width %Length; #IMPLIED
+ border %Pixels; #IMPLIED
+ frame %TFrame; #IMPLIED
+ rules %TRules; #IMPLIED
+ cellspacing %Length; #IMPLIED
+ cellpadding %Length; #IMPLIED
+ >
+
+<!ATTLIST caption
+ %attrs;
+ >
+
+<!--
+colgroup groups a set of col elements. It allows you to group
+several semantically related columns together.
+-->
+<!ATTLIST colgroup
+ %attrs;
+ span %Number; "1"
+ width %MultiLength; #IMPLIED
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!--
+ col elements define the alignment properties for cells in
+ one or more columns.
+
+ The width attribute specifies the width of the columns, e.g.
+
+ width=64 width in screen pixels
+ width=0.5* relative width of 0.5
+
+ The span attribute causes the attributes of one
+ col element to apply to more than one column.
+-->
+<!ATTLIST col
+ %attrs;
+ span %Number; "1"
+ width %MultiLength; #IMPLIED
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!--
+ Use thead to duplicate headers when breaking table
+ across page boundaries, or for static headers when
+ tbody sections are rendered in scrolling panel.
+
+ Use tfoot to duplicate footers when breaking table
+ across page boundaries, or for static footers when
+ tbody sections are rendered in scrolling panel.
+
+ Use multiple tbody sections when rules are needed
+ between groups of table rows.
+-->
+<!ATTLIST thead
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tfoot
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tbody
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tr
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+
+<!-- Scope is simpler than headers attribute for common tables -->
+<!ENTITY % Scope "(row|col|rowgroup|colgroup)">
+
+<!-- th is for headers, td for data and for cells acting as both -->
+
+<!ATTLIST th
+ %attrs;
+ abbr %Text; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ scope %Scope; #IMPLIED
+ rowspan %Number; "1"
+ colspan %Number; "1"
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST td
+ %attrs;
+ abbr %Text; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ scope %Scope; #IMPLIED
+ rowspan %Number; "1"
+ colspan %Number; "1"
+ %cellhalign;
+ %cellvalign;
+ >
+
diff --git a/twisted/lore/xhtml1-transitional.dtd b/twisted/lore/xhtml1-transitional.dtd
new file mode 100644
index 0000000..628f27a
--- /dev/null
+++ b/twisted/lore/xhtml1-transitional.dtd
@@ -0,0 +1,1201 @@
+<!--
+ Extensible HTML version 1.0 Transitional DTD
+
+ This is the same as HTML 4 Transitional except for
+ changes due to the differences between XML and SGML.
+
+ Namespace = http://www.w3.org/1999/xhtml
+
+ For further information, see: http://www.w3.org/TR/xhtml1
+
+ Copyright (c) 1998-2002 W3C (MIT, INRIA, Keio),
+ All Rights Reserved.
+
+ This DTD module is identified by the PUBLIC and SYSTEM identifiers:
+
+ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ SYSTEM "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
+
+ $Revision: 1.2 $
+ $Date: 2002/08/01 18:37:55 $
+
+-->
+
+<!--================ Character mnemonic entities =========================-->
+
+<!ENTITY % HTMLlat1 PUBLIC
+ "-//W3C//ENTITIES Latin 1 for XHTML//EN"
+ "xhtml-lat1.ent">
+%HTMLlat1;
+
+<!ENTITY % HTMLsymbol PUBLIC
+ "-//W3C//ENTITIES Symbols for XHTML//EN"
+ "xhtml-symbol.ent">
+%HTMLsymbol;
+
+<!ENTITY % HTMLspecial PUBLIC
+ "-//W3C//ENTITIES Special for XHTML//EN"
+ "xhtml-special.ent">
+%HTMLspecial;
+
+<!--================== Imported Names ====================================-->
+
+<!ENTITY % ContentType "CDATA">
+ <!-- media type, as per [RFC2045] -->
+
+<!ENTITY % ContentTypes "CDATA">
+ <!-- comma-separated list of media types, as per [RFC2045] -->
+
+<!ENTITY % Charset "CDATA">
+ <!-- a character encoding, as per [RFC2045] -->
+
+<!ENTITY % Charsets "CDATA">
+ <!-- a space separated list of character encodings, as per [RFC2045] -->
+
+<!ENTITY % LanguageCode "NMTOKEN">
+ <!-- a language code, as per [RFC3066] -->
+
+<!ENTITY % Character "CDATA">
+ <!-- a single character, as per section 2.2 of [XML] -->
+
+<!ENTITY % Number "CDATA">
+ <!-- one or more digits -->
+
+<!ENTITY % LinkTypes "CDATA">
+ <!-- space-separated list of link types -->
+
+<!ENTITY % MediaDesc "CDATA">
+ <!-- single or comma-separated list of media descriptors -->
+
+<!ENTITY % URI "CDATA">
+ <!-- a Uniform Resource Identifier, see [RFC2396] -->
+
+<!ENTITY % UriList "CDATA">
+ <!-- a space separated list of Uniform Resource Identifiers -->
+
+<!ENTITY % Datetime "CDATA">
+ <!-- date and time information. ISO date format -->
+
+<!ENTITY % Script "CDATA">
+ <!-- script expression -->
+
+<!ENTITY % StyleSheet "CDATA">
+ <!-- style sheet data -->
+
+<!ENTITY % Text "CDATA">
+ <!-- used for titles etc. -->
+
+<!ENTITY % FrameTarget "NMTOKEN">
+ <!-- render in this frame -->
+
+<!ENTITY % Length "CDATA">
+ <!-- nn for pixels or nn% for percentage length -->
+
+<!ENTITY % MultiLength "CDATA">
+ <!-- pixel, percentage, or relative -->
+
+<!ENTITY % Pixels "CDATA">
+ <!-- integer representing length in pixels -->
+
+<!-- these are used for image maps -->
+
+<!ENTITY % Shape "(rect|circle|poly|default)">
+
+<!ENTITY % Coords "CDATA">
+ <!-- comma separated list of lengths -->
+
+<!-- used for object, applet, img, input and iframe -->
+<!ENTITY % ImgAlign "(top|middle|bottom|left|right)">
+
+<!-- a color using sRGB: #RRGGBB as Hex values -->
+<!ENTITY % Color "CDATA">
+
+<!-- There are also 16 widely known color names with their sRGB values:
+
+ Black = #000000 Green = #008000
+ Silver = #C0C0C0 Lime = #00FF00
+ Gray = #808080 Olive = #808000
+ White = #FFFFFF Yellow = #FFFF00
+ Maroon = #800000 Navy = #000080
+ Red = #FF0000 Blue = #0000FF
+ Purple = #800080 Teal = #008080
+ Fuchsia= #FF00FF Aqua = #00FFFF
+-->
+
+<!--=================== Generic Attributes ===============================-->
+
+<!-- core attributes common to most elements
+ id document-wide unique id
+ class space separated list of classes
+ style associated style info
+ title advisory title/amplification
+-->
+<!ENTITY % coreattrs
+ "id ID #IMPLIED
+ class CDATA #IMPLIED
+ style %StyleSheet; #IMPLIED
+ title %Text; #IMPLIED"
+ >
+
+<!-- internationalization attributes
+ lang language code (backwards compatible)
+ xml:lang language code (as per XML 1.0 spec)
+ dir direction for weak/neutral text
+-->
+<!ENTITY % i18n
+ "lang %LanguageCode; #IMPLIED
+ xml:lang %LanguageCode; #IMPLIED
+ dir (ltr|rtl) #IMPLIED"
+ >
+
+<!-- attributes for common UI events
+ onclick a pointer button was clicked
+ ondblclick a pointer button was double clicked
+ onmousedown a pointer button was pressed down
+ onmouseup a pointer button was released
+ onmousemove a pointer was moved onto the element
+ onmouseout a pointer was moved away from the element
+ onkeypress a key was pressed and released
+ onkeydown a key was pressed down
+ onkeyup a key was released
+-->
+<!ENTITY % events
+ "onclick %Script; #IMPLIED
+ ondblclick %Script; #IMPLIED
+ onmousedown %Script; #IMPLIED
+ onmouseup %Script; #IMPLIED
+ onmouseover %Script; #IMPLIED
+ onmousemove %Script; #IMPLIED
+ onmouseout %Script; #IMPLIED
+ onkeypress %Script; #IMPLIED
+ onkeydown %Script; #IMPLIED
+ onkeyup %Script; #IMPLIED"
+ >
+
+<!-- attributes for elements that can get the focus
+ accesskey accessibility key character
+ tabindex position in tabbing order
+ onfocus the element got the focus
+ onblur the element lost the focus
+-->
+<!ENTITY % focus
+ "accesskey %Character; #IMPLIED
+ tabindex %Number; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED"
+ >
+
+<!ENTITY % attrs "%coreattrs; %i18n; %events;">
+
+<!-- text alignment for p, div, h1-h6. The default is
+ align="left" for ltr headings, "right" for rtl -->
+
+<!ENTITY % TextAlign "align (left|center|right|justify) #IMPLIED">
+
+<!--=================== Text Elements ====================================-->
+
+<!ENTITY % special.extra
+ "object | applet | img | map | iframe">
+
+<!ENTITY % special.basic
+ "br | span | bdo">
+
+<!ENTITY % special
+ "%special.basic; | %special.extra;">
+
+<!ENTITY % fontstyle.extra "big | small | font | basefont">
+
+<!ENTITY % fontstyle.basic "tt | i | b | u
+ | s | strike ">
+
+<!ENTITY % fontstyle "%fontstyle.basic; | %fontstyle.extra;">
+
+<!ENTITY % phrase.extra "sub | sup">
+<!ENTITY % phrase.basic "em | strong | dfn | code | q |
+ samp | kbd | var | cite | abbr | acronym">
+
+<!ENTITY % phrase "%phrase.basic; | %phrase.extra;">
+
+<!ENTITY % inline.forms "input | select | textarea | label | button">
+
+<!-- these can occur at block or inline level -->
+<!ENTITY % misc.inline "ins | del | script">
+
+<!-- these can only occur at block level -->
+<!ENTITY % misc "noscript | %misc.inline;">
+
+<!ENTITY % inline "a | %special; | %fontstyle; | %phrase; | %inline.forms;">
+
+<!-- %Inline; covers inline or "text-level" elements -->
+<!ENTITY % Inline "(#PCDATA | %inline; | %misc.inline;)*">
+
+<!--================== Block level elements ==============================-->
+
+<!ENTITY % heading "h1|h2|h3|h4|h5|h6">
+<!ENTITY % lists "ul | ol | dl | menu | dir">
+<!ENTITY % blocktext "pre | hr | blockquote | address | center | noframes">
+
+<!ENTITY % block
+ "p | %heading; | div | %lists; | %blocktext; | isindex |fieldset | table">
+
+<!-- %Flow; mixes block and inline and is used for list items etc. -->
+<!ENTITY % Flow "(#PCDATA | %block; | form | %inline; | %misc;)*">
+
+<!--================== Content models for exclusions =====================-->
+
+<!-- a elements use %Inline; excluding a -->
+
+<!ENTITY % a.content
+ "(#PCDATA | %special; | %fontstyle; | %phrase; | %inline.forms; | %misc.inline;)*">
+
+<!-- pre uses %Inline excluding img, object, applet, big, small,
+ font, or basefont -->
+
+<!ENTITY % pre.content
+ "(#PCDATA | a | %special.basic; | %fontstyle.basic; | %phrase.basic; |
+ %inline.forms; | %misc.inline;)*">
+
+<!-- form uses %Flow; excluding form -->
+
+<!ENTITY % form.content "(#PCDATA | %block; | %inline; | %misc;)*">
+
+<!-- button uses %Flow; but excludes a, form, form controls, iframe -->
+
+<!ENTITY % button.content
+ "(#PCDATA | p | %heading; | div | %lists; | %blocktext; |
+ table | br | span | bdo | object | applet | img | map |
+ %fontstyle; | %phrase; | %misc;)*">
+
+<!--================ Document Structure ==================================-->
+
+<!-- the namespace URI designates the document profile -->
+
+<!ELEMENT html (head, body)>
+<!ATTLIST html
+ %i18n;
+ id ID #IMPLIED
+ xmlns %URI; #FIXED 'http://www.w3.org/1999/xhtml'
+ >
+
+<!--================ Document Head =======================================-->
+
+<!ENTITY % head.misc "(script|style|meta|link|object|isindex)*">
+
+<!-- content model is %head.misc; combined with a single
+ title and an optional base element in any order -->
+
+<!ELEMENT head (%head.misc;,
+ ((title, %head.misc;, (base, %head.misc;)?) |
+ (base, %head.misc;, (title, %head.misc;))))>
+
+<!ATTLIST head
+ %i18n;
+ id ID #IMPLIED
+ profile %URI; #IMPLIED
+ >
+
+<!-- The title element is not considered part of the flow of text.
+ It should be displayed, for example as the page header or
+ window title. Exactly one title is required per document.
+ -->
+<!ELEMENT title (#PCDATA)>
+<!ATTLIST title
+ %i18n;
+ id ID #IMPLIED
+ >
+
+<!-- document base URI -->
+
+<!ELEMENT base EMPTY>
+<!ATTLIST base
+ id ID #IMPLIED
+ href %URI; #IMPLIED
+ target %FrameTarget; #IMPLIED
+ >
+
+<!-- generic metainformation -->
+<!ELEMENT meta EMPTY>
+<!ATTLIST meta
+ %i18n;
+ id ID #IMPLIED
+ http-equiv CDATA #IMPLIED
+ name CDATA #IMPLIED
+ content CDATA #REQUIRED
+ scheme CDATA #IMPLIED
+ >
+
+<!--
+ Relationship values can be used in principle:
+
+ a) for document specific toolbars/menus when used
+ with the link element in document head e.g.
+ start, contents, previous, next, index, end, help
+ b) to link to a separate style sheet (rel="stylesheet")
+ c) to make a link to a script (rel="script")
+ d) by stylesheets to control how collections of
+ html nodes are rendered into printed documents
+ e) to make a link to a printable version of this document
+ e.g. a PostScript or PDF version (rel="alternate" media="print")
+-->
+
+<!ELEMENT link EMPTY>
+<!ATTLIST link
+ %attrs;
+ charset %Charset; #IMPLIED
+ href %URI; #IMPLIED
+ hreflang %LanguageCode; #IMPLIED
+ type %ContentType; #IMPLIED
+ rel %LinkTypes; #IMPLIED
+ rev %LinkTypes; #IMPLIED
+ media %MediaDesc; #IMPLIED
+ target %FrameTarget; #IMPLIED
+ >
+
+<!-- style info, which may include CDATA sections -->
+<!ELEMENT style (#PCDATA)>
+<!ATTLIST style
+ %i18n;
+ id ID #IMPLIED
+ type %ContentType; #REQUIRED
+ media %MediaDesc; #IMPLIED
+ title %Text; #IMPLIED
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!-- script statements, which may include CDATA sections -->
+<!ELEMENT script (#PCDATA)>
+<!ATTLIST script
+ id ID #IMPLIED
+ charset %Charset; #IMPLIED
+ type %ContentType; #REQUIRED
+ language CDATA #IMPLIED
+ src %URI; #IMPLIED
+ defer (defer) #IMPLIED
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!-- alternate content container for non script-based rendering -->
+
+<!ELEMENT noscript %Flow;>
+<!ATTLIST noscript
+ %attrs;
+ >
+
+<!--======================= Frames =======================================-->
+
+<!-- inline subwindow -->
+
+<!ELEMENT iframe %Flow;>
+<!ATTLIST iframe
+ %coreattrs;
+ longdesc %URI; #IMPLIED
+ name NMTOKEN #IMPLIED
+ src %URI; #IMPLIED
+ frameborder (1|0) "1"
+ marginwidth %Pixels; #IMPLIED
+ marginheight %Pixels; #IMPLIED
+ scrolling (yes|no|auto) "auto"
+ align %ImgAlign; #IMPLIED
+ height %Length; #IMPLIED
+ width %Length; #IMPLIED
+ >
+
+<!-- alternate content container for non frame-based rendering -->
+
+<!ELEMENT noframes %Flow;>
+<!ATTLIST noframes
+ %attrs;
+ >
+
+<!--=================== Document Body ====================================-->
+
+<!ELEMENT body %Flow;>
+<!ATTLIST body
+ %attrs;
+ onload %Script; #IMPLIED
+ onunload %Script; #IMPLIED
+ background %URI; #IMPLIED
+ bgcolor %Color; #IMPLIED
+ text %Color; #IMPLIED
+ link %Color; #IMPLIED
+ vlink %Color; #IMPLIED
+ alink %Color; #IMPLIED
+ >
+
+<!ELEMENT div %Flow;> <!-- generic language/style container -->
+<!ATTLIST div
+ %attrs;
+ %TextAlign;
+ >
+
+<!--=================== Paragraphs =======================================-->
+
+<!ELEMENT p %Inline;>
+<!ATTLIST p
+ %attrs;
+ %TextAlign;
+ >
+
+<!--=================== Headings =========================================-->
+
+<!--
+ There are six levels of headings from h1 (the most important)
+ to h6 (the least important).
+-->
+
+<!ELEMENT h1 %Inline;>
+<!ATTLIST h1
+ %attrs;
+ %TextAlign;
+ >
+
+<!ELEMENT h2 %Inline;>
+<!ATTLIST h2
+ %attrs;
+ %TextAlign;
+ >
+
+<!ELEMENT h3 %Inline;>
+<!ATTLIST h3
+ %attrs;
+ %TextAlign;
+ >
+
+<!ELEMENT h4 %Inline;>
+<!ATTLIST h4
+ %attrs;
+ %TextAlign;
+ >
+
+<!ELEMENT h5 %Inline;>
+<!ATTLIST h5
+ %attrs;
+ %TextAlign;
+ >
+
+<!ELEMENT h6 %Inline;>
+<!ATTLIST h6
+ %attrs;
+ %TextAlign;
+ >
+
+<!--=================== Lists ============================================-->
+
+<!-- Unordered list bullet styles -->
+
+<!ENTITY % ULStyle "(disc|square|circle)">
+
+<!-- Unordered list -->
+
+<!ELEMENT ul (li)+>
+<!ATTLIST ul
+ %attrs;
+ type %ULStyle; #IMPLIED
+ compact (compact) #IMPLIED
+ >
+
+<!-- Ordered list numbering style
+
+ 1 arabic numbers 1, 2, 3, ...
+ a lower alpha a, b, c, ...
+ A upper alpha A, B, C, ...
+ i lower roman i, ii, iii, ...
+ I upper roman I, II, III, ...
+
+ The style is applied to the sequence number which by default
+ is reset to 1 for the first list item in an ordered list.
+-->
+<!ENTITY % OLStyle "CDATA">
+
+<!-- Ordered (numbered) list -->
+
+<!ELEMENT ol (li)+>
+<!ATTLIST ol
+ %attrs;
+ type %OLStyle; #IMPLIED
+ compact (compact) #IMPLIED
+ start %Number; #IMPLIED
+ >
+
+<!-- single column list (DEPRECATED) -->
+<!ELEMENT menu (li)+>
+<!ATTLIST menu
+ %attrs;
+ compact (compact) #IMPLIED
+ >
+
+<!-- multiple column list (DEPRECATED) -->
+<!ELEMENT dir (li)+>
+<!ATTLIST dir
+ %attrs;
+ compact (compact) #IMPLIED
+ >
+
+<!-- LIStyle is constrained to: "(%ULStyle;|%OLStyle;)" -->
+<!ENTITY % LIStyle "CDATA">
+
+<!-- list item -->
+
+<!ELEMENT li %Flow;>
+<!ATTLIST li
+ %attrs;
+ type %LIStyle; #IMPLIED
+ value %Number; #IMPLIED
+ >
+
+<!-- definition lists - dt for term, dd for its definition -->
+
+<!ELEMENT dl (dt|dd)+>
+<!ATTLIST dl
+ %attrs;
+ compact (compact) #IMPLIED
+ >
+
+<!ELEMENT dt %Inline;>
+<!ATTLIST dt
+ %attrs;
+ >
+
+<!ELEMENT dd %Flow;>
+<!ATTLIST dd
+ %attrs;
+ >
+
+<!--=================== Address ==========================================-->
+
+<!-- information on author -->
+
+<!ELEMENT address (#PCDATA | %inline; | %misc.inline; | p)*>
+<!ATTLIST address
+ %attrs;
+ >
+
+<!--=================== Horizontal Rule ==================================-->
+
+<!ELEMENT hr EMPTY>
+<!ATTLIST hr
+ %attrs;
+ align (left|center|right) #IMPLIED
+ noshade (noshade) #IMPLIED
+ size %Pixels; #IMPLIED
+ width %Length; #IMPLIED
+ >
+
+<!--=================== Preformatted Text ================================-->
+
+<!-- content is %Inline; excluding
+ "img|object|applet|big|small|sub|sup|font|basefont" -->
+
+<!ELEMENT pre %pre.content;>
+<!ATTLIST pre
+ %attrs;
+ width %Number; #IMPLIED
+ xml:space (preserve) #FIXED 'preserve'
+ >
+
+<!--=================== Block-like Quotes ================================-->
+
+<!ELEMENT blockquote %Flow;>
+<!ATTLIST blockquote
+ %attrs;
+ cite %URI; #IMPLIED
+ >
+
+<!--=================== Text alignment ===================================-->
+
+<!-- center content -->
+<!ELEMENT center %Flow;>
+<!ATTLIST center
+ %attrs;
+ >
+
+<!--=================== Inserted/Deleted Text ============================-->
+
+<!--
+ ins/del are allowed in block and inline content, but its
+ inappropriate to include block content within an ins element
+ occurring in inline content.
+-->
+<!ELEMENT ins %Flow;>
+<!ATTLIST ins
+ %attrs;
+ cite %URI; #IMPLIED
+ datetime %Datetime; #IMPLIED
+ >
+
+<!ELEMENT del %Flow;>
+<!ATTLIST del
+ %attrs;
+ cite %URI; #IMPLIED
+ datetime %Datetime; #IMPLIED
+ >
+
+<!--================== The Anchor Element ================================-->
+
+<!-- content is %Inline; except that anchors shouldn't be nested -->
+
+<!ELEMENT a %a.content;>
+<!ATTLIST a
+ %attrs;
+ %focus;
+ charset %Charset; #IMPLIED
+ type %ContentType; #IMPLIED
+ name NMTOKEN #IMPLIED
+ href %URI; #IMPLIED
+ hreflang %LanguageCode; #IMPLIED
+ rel %LinkTypes; #IMPLIED
+ rev %LinkTypes; #IMPLIED
+ shape %Shape; "rect"
+ coords %Coords; #IMPLIED
+ target %FrameTarget; #IMPLIED
+ >
+
+<!--===================== Inline Elements ================================-->
+
+<!ELEMENT span %Inline;> <!-- generic language/style container -->
+<!ATTLIST span
+ %attrs;
+ >
+
+<!ELEMENT bdo %Inline;> <!-- I18N BiDi over-ride -->
+<!ATTLIST bdo
+ %coreattrs;
+ %events;
+ lang %LanguageCode; #IMPLIED
+ xml:lang %LanguageCode; #IMPLIED
+ dir (ltr|rtl) #REQUIRED
+ >
+
+<!ELEMENT br EMPTY> <!-- forced line break -->
+<!ATTLIST br
+ %coreattrs;
+ clear (left|all|right|none) "none"
+ >
+
+<!ELEMENT em %Inline;> <!-- emphasis -->
+<!ATTLIST em %attrs;>
+
+<!ELEMENT strong %Inline;> <!-- strong emphasis -->
+<!ATTLIST strong %attrs;>
+
+<!ELEMENT dfn %Inline;> <!-- definitional -->
+<!ATTLIST dfn %attrs;>
+
+<!ELEMENT code %Inline;> <!-- program code -->
+<!ATTLIST code %attrs;>
+
+<!ELEMENT samp %Inline;> <!-- sample -->
+<!ATTLIST samp %attrs;>
+
+<!ELEMENT kbd %Inline;> <!-- something user would type -->
+<!ATTLIST kbd %attrs;>
+
+<!ELEMENT var %Inline;> <!-- variable -->
+<!ATTLIST var %attrs;>
+
+<!ELEMENT cite %Inline;> <!-- citation -->
+<!ATTLIST cite %attrs;>
+
+<!ELEMENT abbr %Inline;> <!-- abbreviation -->
+<!ATTLIST abbr %attrs;>
+
+<!ELEMENT acronym %Inline;> <!-- acronym -->
+<!ATTLIST acronym %attrs;>
+
+<!ELEMENT q %Inline;> <!-- inlined quote -->
+<!ATTLIST q
+ %attrs;
+ cite %URI; #IMPLIED
+ >
+
+<!ELEMENT sub %Inline;> <!-- subscript -->
+<!ATTLIST sub %attrs;>
+
+<!ELEMENT sup %Inline;> <!-- superscript -->
+<!ATTLIST sup %attrs;>
+
+<!ELEMENT tt %Inline;> <!-- fixed pitch font -->
+<!ATTLIST tt %attrs;>
+
+<!ELEMENT i %Inline;> <!-- italic font -->
+<!ATTLIST i %attrs;>
+
+<!ELEMENT b %Inline;> <!-- bold font -->
+<!ATTLIST b %attrs;>
+
+<!ELEMENT big %Inline;> <!-- bigger font -->
+<!ATTLIST big %attrs;>
+
+<!ELEMENT small %Inline;> <!-- smaller font -->
+<!ATTLIST small %attrs;>
+
+<!ELEMENT u %Inline;> <!-- underline -->
+<!ATTLIST u %attrs;>
+
+<!ELEMENT s %Inline;> <!-- strike-through -->
+<!ATTLIST s %attrs;>
+
+<!ELEMENT strike %Inline;> <!-- strike-through -->
+<!ATTLIST strike %attrs;>
+
+<!ELEMENT basefont EMPTY> <!-- base font size -->
+<!ATTLIST basefont
+ id ID #IMPLIED
+ size CDATA #REQUIRED
+ color %Color; #IMPLIED
+ face CDATA #IMPLIED
+ >
+
+<!ELEMENT font %Inline;> <!-- local change to font -->
+<!ATTLIST font
+ %coreattrs;
+ %i18n;
+ size CDATA #IMPLIED
+ color %Color; #IMPLIED
+ face CDATA #IMPLIED
+ >
+
+<!--==================== Object ======================================-->
+<!--
+ object is used to embed objects as part of HTML pages.
+ param elements should precede other content. Parameters
+ can also be expressed as attribute/value pairs on the
+ object element itself when brevity is desired.
+-->
+
+<!ELEMENT object (#PCDATA | param | %block; | form | %inline; | %misc;)*>
+<!ATTLIST object
+ %attrs;
+ declare (declare) #IMPLIED
+ classid %URI; #IMPLIED
+ codebase %URI; #IMPLIED
+ data %URI; #IMPLIED
+ type %ContentType; #IMPLIED
+ codetype %ContentType; #IMPLIED
+ archive %UriList; #IMPLIED
+ standby %Text; #IMPLIED
+ height %Length; #IMPLIED
+ width %Length; #IMPLIED
+ usemap %URI; #IMPLIED
+ name NMTOKEN #IMPLIED
+ tabindex %Number; #IMPLIED
+ align %ImgAlign; #IMPLIED
+ border %Pixels; #IMPLIED
+ hspace %Pixels; #IMPLIED
+ vspace %Pixels; #IMPLIED
+ >
+
+<!--
+ param is used to supply a named property value.
+ In XML it would seem natural to follow RDF and support an
+ abbreviated syntax where the param elements are replaced
+ by attribute value pairs on the object start tag.
+-->
+<!ELEMENT param EMPTY>
+<!ATTLIST param
+ id ID #IMPLIED
+ name CDATA #REQUIRED
+ value CDATA #IMPLIED
+ valuetype (data|ref|object) "data"
+ type %ContentType; #IMPLIED
+ >
+
+<!--=================== Java applet ==================================-->
+<!--
+ One of code or object attributes must be present.
+ Place param elements before other content.
+-->
+<!ELEMENT applet (#PCDATA | param | %block; | form | %inline; | %misc;)*>
+<!ATTLIST applet
+ %coreattrs;
+ codebase %URI; #IMPLIED
+ archive CDATA #IMPLIED
+ code CDATA #IMPLIED
+ object CDATA #IMPLIED
+ alt %Text; #IMPLIED
+ name NMTOKEN #IMPLIED
+ width %Length; #REQUIRED
+ height %Length; #REQUIRED
+ align %ImgAlign; #IMPLIED
+ hspace %Pixels; #IMPLIED
+ vspace %Pixels; #IMPLIED
+ >
+
+<!--=================== Images ===========================================-->
+
+<!--
+ To avoid accessibility problems for people who aren't
+ able to see the image, you should provide a text
+ description using the alt and longdesc attributes.
+ In addition, avoid the use of server-side image maps.
+-->
+
+<!ELEMENT img EMPTY>
+<!ATTLIST img
+ %attrs;
+ src %URI; #REQUIRED
+ alt %Text; #REQUIRED
+ name NMTOKEN #IMPLIED
+ longdesc %URI; #IMPLIED
+ height %Length; #IMPLIED
+ width %Length; #IMPLIED
+ usemap %URI; #IMPLIED
+ ismap (ismap) #IMPLIED
+ align %ImgAlign; #IMPLIED
+ border %Length; #IMPLIED
+ hspace %Pixels; #IMPLIED
+ vspace %Pixels; #IMPLIED
+ >
+
+<!-- usemap points to a map element which may be in this document
+ or an external document, although the latter is not widely supported -->
+
+<!--================== Client-side image maps ============================-->
+
+<!-- These can be placed in the same document or grouped in a
+ separate document although this isn't yet widely supported -->
+
+<!ELEMENT map ((%block; | form | %misc;)+ | area+)>
+<!ATTLIST map
+ %i18n;
+ %events;
+ id ID #REQUIRED
+ class CDATA #IMPLIED
+ style %StyleSheet; #IMPLIED
+ title %Text; #IMPLIED
+ name CDATA #IMPLIED
+ >
+
+<!ELEMENT area EMPTY>
+<!ATTLIST area
+ %attrs;
+ %focus;
+ shape %Shape; "rect"
+ coords %Coords; #IMPLIED
+ href %URI; #IMPLIED
+ nohref (nohref) #IMPLIED
+ alt %Text; #REQUIRED
+ target %FrameTarget; #IMPLIED
+ >
+
+<!--================ Forms ===============================================-->
+
+<!ELEMENT form %form.content;> <!-- forms shouldn't be nested -->
+
+<!ATTLIST form
+ %attrs;
+ action %URI; #REQUIRED
+ method (get|post) "get"
+ name NMTOKEN #IMPLIED
+ enctype %ContentType; "application/x-www-form-urlencoded"
+ onsubmit %Script; #IMPLIED
+ onreset %Script; #IMPLIED
+ accept %ContentTypes; #IMPLIED
+ accept-charset %Charsets; #IMPLIED
+ target %FrameTarget; #IMPLIED
+ >
+
+<!--
+ Each label must not contain more than ONE field
+ Label elements shouldn't be nested.
+-->
+<!ELEMENT label %Inline;>
+<!ATTLIST label
+ %attrs;
+ for IDREF #IMPLIED
+ accesskey %Character; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED
+ >
+
+<!ENTITY % InputType
+ "(text | password | checkbox |
+ radio | submit | reset |
+ file | hidden | image | button)"
+ >
+
+<!-- the name attribute is required for all but submit & reset -->
+
+<!ELEMENT input EMPTY> <!-- form control -->
+<!ATTLIST input
+ %attrs;
+ %focus;
+ type %InputType; "text"
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ checked (checked) #IMPLIED
+ disabled (disabled) #IMPLIED
+ readonly (readonly) #IMPLIED
+ size CDATA #IMPLIED
+ maxlength %Number; #IMPLIED
+ src %URI; #IMPLIED
+ alt CDATA #IMPLIED
+ usemap %URI; #IMPLIED
+ onselect %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ accept %ContentTypes; #IMPLIED
+ align %ImgAlign; #IMPLIED
+ >
+
+<!ELEMENT select (optgroup|option)+> <!-- option selector -->
+<!ATTLIST select
+ %attrs;
+ name CDATA #IMPLIED
+ size %Number; #IMPLIED
+ multiple (multiple) #IMPLIED
+ disabled (disabled) #IMPLIED
+ tabindex %Number; #IMPLIED
+ onfocus %Script; #IMPLIED
+ onblur %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ >
+
+<!ELEMENT optgroup (option)+> <!-- option group -->
+<!ATTLIST optgroup
+ %attrs;
+ disabled (disabled) #IMPLIED
+ label %Text; #REQUIRED
+ >
+
+<!ELEMENT option (#PCDATA)> <!-- selectable choice -->
+<!ATTLIST option
+ %attrs;
+ selected (selected) #IMPLIED
+ disabled (disabled) #IMPLIED
+ label %Text; #IMPLIED
+ value CDATA #IMPLIED
+ >
+
+<!ELEMENT textarea (#PCDATA)> <!-- multi-line text field -->
+<!ATTLIST textarea
+ %attrs;
+ %focus;
+ name CDATA #IMPLIED
+ rows %Number; #REQUIRED
+ cols %Number; #REQUIRED
+ disabled (disabled) #IMPLIED
+ readonly (readonly) #IMPLIED
+ onselect %Script; #IMPLIED
+ onchange %Script; #IMPLIED
+ >
+
+<!--
+ The fieldset element is used to group form fields.
+ Only one legend element should occur in the content
+ and if present should only be preceded by whitespace.
+-->
+<!ELEMENT fieldset (#PCDATA | legend | %block; | form | %inline; | %misc;)*>
+<!ATTLIST fieldset
+ %attrs;
+ >
+
+<!ENTITY % LAlign "(top|bottom|left|right)">
+
+<!ELEMENT legend %Inline;> <!-- fieldset label -->
+<!ATTLIST legend
+ %attrs;
+ accesskey %Character; #IMPLIED
+ align %LAlign; #IMPLIED
+ >
+
+<!--
+ Content is %Flow; excluding a, form, form controls, iframe
+-->
+<!ELEMENT button %button.content;> <!-- push button -->
+<!ATTLIST button
+ %attrs;
+ %focus;
+ name CDATA #IMPLIED
+ value CDATA #IMPLIED
+ type (button|submit|reset) "submit"
+ disabled (disabled) #IMPLIED
+ >
+
+<!-- single-line text input control (DEPRECATED) -->
+<!ELEMENT isindex EMPTY>
+<!ATTLIST isindex
+ %coreattrs;
+ %i18n;
+ prompt %Text; #IMPLIED
+ >
+
+<!--======================= Tables =======================================-->
+
+<!-- Derived from IETF HTML table standard, see [RFC1942] -->
+
+<!--
+ The border attribute sets the thickness of the frame around the
+ table. The default units are screen pixels.
+
+ The frame attribute specifies which parts of the frame around
+ the table should be rendered. The values are not the same as
+ CALS to avoid a name clash with the valign attribute.
+-->
+<!ENTITY % TFrame "(void|above|below|hsides|lhs|rhs|vsides|box|border)">
+
+<!--
+ The rules attribute defines which rules to draw between cells:
+
+ If rules is absent then assume:
+ "none" if border is absent or border="0" otherwise "all"
+-->
+
+<!ENTITY % TRules "(none | groups | rows | cols | all)">
+
+<!-- horizontal placement of table relative to document -->
+<!ENTITY % TAlign "(left|center|right)">
+
+<!-- horizontal alignment attributes for cell contents
+
+ char alignment char, e.g. char=':'
+ charoff offset for alignment char
+-->
+<!ENTITY % cellhalign
+ "align (left|center|right|justify|char) #IMPLIED
+ char %Character; #IMPLIED
+ charoff %Length; #IMPLIED"
+ >
+
+<!-- vertical alignment attributes for cell contents -->
+<!ENTITY % cellvalign
+ "valign (top|middle|bottom|baseline) #IMPLIED"
+ >
+
+<!ELEMENT table
+ (caption?, (col*|colgroup*), thead?, tfoot?, (tbody+|tr+))>
+<!ELEMENT caption %Inline;>
+<!ELEMENT thead (tr)+>
+<!ELEMENT tfoot (tr)+>
+<!ELEMENT tbody (tr)+>
+<!ELEMENT colgroup (col)*>
+<!ELEMENT col EMPTY>
+<!ELEMENT tr (th|td)+>
+<!ELEMENT th %Flow;>
+<!ELEMENT td %Flow;>
+
+<!ATTLIST table
+ %attrs;
+ summary %Text; #IMPLIED
+ width %Length; #IMPLIED
+ border %Pixels; #IMPLIED
+ frame %TFrame; #IMPLIED
+ rules %TRules; #IMPLIED
+ cellspacing %Length; #IMPLIED
+ cellpadding %Length; #IMPLIED
+ align %TAlign; #IMPLIED
+ bgcolor %Color; #IMPLIED
+ >
+
+<!ENTITY % CAlign "(top|bottom|left|right)">
+
+<!ATTLIST caption
+ %attrs;
+ align %CAlign; #IMPLIED
+ >
+
+<!--
+colgroup groups a set of col elements. It allows you to group
+several semantically related columns together.
+-->
+<!ATTLIST colgroup
+ %attrs;
+ span %Number; "1"
+ width %MultiLength; #IMPLIED
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!--
+ col elements define the alignment properties for cells in
+ one or more columns.
+
+ The width attribute specifies the width of the columns, e.g.
+
+ width=64 width in screen pixels
+ width=0.5* relative width of 0.5
+
+ The span attribute causes the attributes of one
+ col element to apply to more than one column.
+-->
+<!ATTLIST col
+ %attrs;
+ span %Number; "1"
+ width %MultiLength; #IMPLIED
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!--
+ Use thead to duplicate headers when breaking table
+ across page boundaries, or for static headers when
+ tbody sections are rendered in scrolling panel.
+
+ Use tfoot to duplicate footers when breaking table
+ across page boundaries, or for static footers when
+ tbody sections are rendered in scrolling panel.
+
+ Use multiple tbody sections when rules are needed
+ between groups of table rows.
+-->
+<!ATTLIST thead
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tfoot
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tbody
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ >
+
+<!ATTLIST tr
+ %attrs;
+ %cellhalign;
+ %cellvalign;
+ bgcolor %Color; #IMPLIED
+ >
+
+<!-- Scope is simpler than headers attribute for common tables -->
+<!ENTITY % Scope "(row|col|rowgroup|colgroup)">
+
+<!-- th is for headers, td for data and for cells acting as both -->
+
+<!ATTLIST th
+ %attrs;
+ abbr %Text; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ scope %Scope; #IMPLIED
+ rowspan %Number; "1"
+ colspan %Number; "1"
+ %cellhalign;
+ %cellvalign;
+ nowrap (nowrap) #IMPLIED
+ bgcolor %Color; #IMPLIED
+ width %Length; #IMPLIED
+ height %Length; #IMPLIED
+ >
+
+<!ATTLIST td
+ %attrs;
+ abbr %Text; #IMPLIED
+ axis CDATA #IMPLIED
+ headers IDREFS #IMPLIED
+ scope %Scope; #IMPLIED
+ rowspan %Number; "1"
+ colspan %Number; "1"
+ %cellhalign;
+ %cellvalign;
+ nowrap (nowrap) #IMPLIED
+ bgcolor %Color; #IMPLIED
+ width %Length; #IMPLIED
+ height %Length; #IMPLIED
+ >
+
diff --git a/twisted/mail/__init__.py b/twisted/mail/__init__.py
new file mode 100644
index 0000000..b434715
--- /dev/null
+++ b/twisted/mail/__init__.py
@@ -0,0 +1,15 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted Mail: a Twisted E-Mail Server.
+
+Maintainer: Jp Calderone
+
+"""
+
+from twisted.mail._version import version
+__version__ = version.short()
diff --git a/twisted/mail/_version.py b/twisted/mail/_version.py
new file mode 100644
index 0000000..a3284c2
--- /dev/null
+++ b/twisted/mail/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.mail', 12, 1, 0)
diff --git a/twisted/mail/alias.py b/twisted/mail/alias.py
new file mode 100644
index 0000000..8eccea6
--- /dev/null
+++ b/twisted/mail/alias.py
@@ -0,0 +1,435 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Support for aliases(5) configuration files
+
+@author: Jp Calderone
+
+TODO::
+ Monitor files for reparsing
+ Handle non-local alias targets
+ Handle maildir alias targets
+"""
+
+import os
+import tempfile
+
+from twisted.mail import smtp
+from twisted.internet import reactor
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.python import failure
+from twisted.python import log
+from zope.interface import implements, Interface
+
+
+def handle(result, line, filename, lineNo):
+ parts = [p.strip() for p in line.split(':', 1)]
+ if len(parts) != 2:
+ fmt = "Invalid format on line %d of alias file %s."
+ arg = (lineNo, filename)
+ log.err(fmt % arg)
+ else:
+ user, alias = parts
+ result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(',')))
+
+def loadAliasFile(domains, filename=None, fp=None):
+ """Load a file containing email aliases.
+
+ Lines in the file should be formatted like so::
+
+ username: alias1,alias2,...,aliasN
+
+ Aliases beginning with a | will be treated as programs, will be run, and
+ the message will be written to their stdin.
+
+ Aliases without a host part will be assumed to be addresses on localhost.
+
+ If a username is specified multiple times, the aliases for each are joined
+ together as if they had all been on one line.
+
+ @type domains: C{dict} of implementor of C{IDomain}
+ @param domains: The domains to which these aliases will belong.
+
+ @type filename: C{str}
+ @param filename: The filename from which to load aliases.
+
+ @type fp: Any file-like object.
+ @param fp: If specified, overrides C{filename}, and aliases are read from
+ it.
+
+ @rtype: C{dict}
+ @return: A dictionary mapping usernames to C{AliasGroup} objects.
+ """
+ result = {}
+ if fp is None:
+ fp = file(filename)
+ else:
+ filename = getattr(fp, 'name', '<unknown>')
+ i = 0
+ prev = ''
+ for line in fp:
+ i += 1
+ line = line.rstrip()
+ if line.lstrip().startswith('#'):
+ continue
+ elif line.startswith(' ') or line.startswith('\t'):
+ prev = prev + line
+ else:
+ if prev:
+ handle(result, prev, filename, i)
+ prev = line
+ if prev:
+ handle(result, prev, filename, i)
+ for (u, a) in result.items():
+ addr = smtp.Address(u)
+ result[u] = AliasGroup(a, domains, u)
+ return result
+
+class IAlias(Interface):
+ def createMessageReceiver():
+ pass
+
+class AliasBase:
+ def __init__(self, domains, original):
+ self.domains = domains
+ self.original = smtp.Address(original)
+
+ def domain(self):
+ return self.domains[self.original.domain]
+
+ def resolve(self, aliasmap, memo=None):
+ if memo is None:
+ memo = {}
+ if str(self) in memo:
+ return None
+ memo[str(self)] = None
+ return self.createMessageReceiver()
+
+class AddressAlias(AliasBase):
+ """The simplest alias, translating one email address into another."""
+
+ implements(IAlias)
+
+ def __init__(self, alias, *args):
+ AliasBase.__init__(self, *args)
+ self.alias = smtp.Address(alias)
+
+ def __str__(self):
+ return '<Address %s>' % (self.alias,)
+
+ def createMessageReceiver(self):
+ return self.domain().startMessage(str(self.alias))
+
+ def resolve(self, aliasmap, memo=None):
+ if memo is None:
+ memo = {}
+ if str(self) in memo:
+ return None
+ memo[str(self)] = None
+ try:
+ return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
+ except smtp.SMTPBadRcpt:
+ pass
+ if self.alias.local in aliasmap:
+ return aliasmap[self.alias.local].resolve(aliasmap, memo)
+ return None
+
+class FileWrapper:
+ implements(smtp.IMessage)
+
+ def __init__(self, filename):
+ self.fp = tempfile.TemporaryFile()
+ self.finalname = filename
+
+ def lineReceived(self, line):
+ self.fp.write(line + '\n')
+
+ def eomReceived(self):
+ self.fp.seek(0, 0)
+ try:
+ f = file(self.finalname, 'a')
+ except:
+ return defer.fail(failure.Failure())
+
+ f.write(self.fp.read())
+ self.fp.close()
+ f.close()
+
+ return defer.succeed(self.finalname)
+
+ def connectionLost(self):
+ self.fp.close()
+ self.fp = None
+
+ def __str__(self):
+ return '<FileWrapper %s>' % (self.finalname,)
+
+
+class FileAlias(AliasBase):
+
+ implements(IAlias)
+
+ def __init__(self, filename, *args):
+ AliasBase.__init__(self, *args)
+ self.filename = filename
+
+ def __str__(self):
+ return '<File %s>' % (self.filename,)
+
+ def createMessageReceiver(self):
+ return FileWrapper(self.filename)
+
+
+
+class ProcessAliasTimeout(Exception):
+ """
+ A timeout occurred while processing aliases.
+ """
+
+
+
+class MessageWrapper:
+ """
+ A message receiver which delivers content to a child process.
+
+ @type completionTimeout: C{int} or C{float}
+ @ivar completionTimeout: The number of seconds to wait for the child
+ process to exit before reporting the delivery as a failure.
+
+ @type _timeoutCallID: C{NoneType} or L{IDelayedCall}
+ @ivar _timeoutCallID: The call used to time out delivery, started when the
+ connection to the child process is closed.
+
+ @type done: C{bool}
+ @ivar done: Flag indicating whether the child process has exited or not.
+
+ @ivar reactor: An L{IReactorTime} provider which will be used to schedule
+ timeouts.
+ """
+ implements(smtp.IMessage)
+
+ done = False
+
+ completionTimeout = 60
+ _timeoutCallID = None
+
+ reactor = reactor
+
+ def __init__(self, protocol, process=None, reactor=None):
+ self.processName = process
+ self.protocol = protocol
+ self.completion = defer.Deferred()
+ self.protocol.onEnd = self.completion
+ self.completion.addBoth(self._processEnded)
+
+ if reactor is not None:
+ self.reactor = reactor
+
+
+ def _processEnded(self, result):
+ """
+ Record process termination and cancel the timeout call if it is active.
+ """
+ self.done = True
+ if self._timeoutCallID is not None:
+ # eomReceived was called, we're actually waiting for the process to
+ # exit.
+ self._timeoutCallID.cancel()
+ self._timeoutCallID = None
+ else:
+ # eomReceived was not called, this is unexpected, propagate the
+ # error.
+ return result
+
+
+ def lineReceived(self, line):
+ if self.done:
+ return
+ self.protocol.transport.write(line + '\n')
+
+
+ def eomReceived(self):
+ """
+ Disconnect from the child process, set up a timeout to wait for it to
+ exit, and return a Deferred which will be called back when the child
+ process exits.
+ """
+ if not self.done:
+ self.protocol.transport.loseConnection()
+ self._timeoutCallID = self.reactor.callLater(
+ self.completionTimeout, self._completionCancel)
+ return self.completion
+
+
+ def _completionCancel(self):
+ """
+ Handle the expiration of the timeout for the child process to exit by
+ terminating the child process forcefully and issuing a failure to the
+ completion deferred returned by L{eomReceived}.
+ """
+ self._timeoutCallID = None
+ self.protocol.transport.signalProcess('KILL')
+ exc = ProcessAliasTimeout(
+ "No answer after %s seconds" % (self.completionTimeout,))
+ self.protocol.onEnd = None
+ self.completion.errback(failure.Failure(exc))
+
+
+ def connectionLost(self):
+ # Heh heh
+ pass
+
+
+ def __str__(self):
+ return '<ProcessWrapper %s>' % (self.processName,)
+
+
+
+class ProcessAliasProtocol(protocol.ProcessProtocol):
+ """
+ Trivial process protocol which will callback a Deferred when the associated
+ process ends.
+
+ @ivar onEnd: If not C{None}, a L{Deferred} which will be called back with
+ the failure passed to C{processEnded}, when C{processEnded} is called.
+ """
+
+ onEnd = None
+
+ def processEnded(self, reason):
+ """
+ Call back C{onEnd} if it is set.
+ """
+ if self.onEnd is not None:
+ self.onEnd.errback(reason)
+
+
+
+class ProcessAlias(AliasBase):
+ """
+ An alias which is handled by the execution of a particular program.
+
+ @ivar reactor: An L{IReactorProcess} and L{IReactorTime} provider which
+ will be used to create and timeout the alias child process.
+ """
+ implements(IAlias)
+
+ reactor = reactor
+
+ def __init__(self, path, *args):
+ AliasBase.__init__(self, *args)
+ self.path = path.split()
+ self.program = self.path[0]
+
+
+ def __str__(self):
+ """
+ Build a string representation containing the path.
+ """
+ return '<Process %s>' % (self.path,)
+
+
+ def spawnProcess(self, proto, program, path):
+ """
+ Wrapper around C{reactor.spawnProcess}, to be customized for tests
+ purpose.
+ """
+ return self.reactor.spawnProcess(proto, program, path)
+
+
+ def createMessageReceiver(self):
+ """
+ Create a message receiver by launching a process.
+ """
+ p = ProcessAliasProtocol()
+ m = MessageWrapper(p, self.program, self.reactor)
+ fd = self.spawnProcess(p, self.program, self.path)
+ return m
+
+
+
+class MultiWrapper:
+ """
+ Wrapper to deliver a single message to multiple recipients.
+ """
+
+ implements(smtp.IMessage)
+
+ def __init__(self, objs):
+ self.objs = objs
+
+ def lineReceived(self, line):
+ for o in self.objs:
+ o.lineReceived(line)
+
+ def eomReceived(self):
+ return defer.DeferredList([
+ o.eomReceived() for o in self.objs
+ ])
+
+ def connectionLost(self):
+ for o in self.objs:
+ o.connectionLost()
+
+ def __str__(self):
+ return '<GroupWrapper %r>' % (map(str, self.objs),)
+
+
+
+class AliasGroup(AliasBase):
+ """
+ An alias which points to more than one recipient.
+
+ @ivar processAliasFactory: a factory for resolving process aliases.
+ @type processAliasFactory: C{class}
+ """
+
+ implements(IAlias)
+
+ processAliasFactory = ProcessAlias
+
+ def __init__(self, items, *args):
+ AliasBase.__init__(self, *args)
+ self.aliases = []
+ while items:
+ addr = items.pop().strip()
+ if addr.startswith(':'):
+ try:
+ f = file(addr[1:])
+ except:
+ log.err("Invalid filename in alias file %r" % (addr[1:],))
+ else:
+ addr = ' '.join([l.strip() for l in f])
+ items.extend(addr.split(','))
+ elif addr.startswith('|'):
+ self.aliases.append(self.processAliasFactory(addr[1:], *args))
+ elif addr.startswith('/'):
+ if os.path.isdir(addr):
+ log.err("Directory delivery not supported")
+ else:
+ self.aliases.append(FileAlias(addr, *args))
+ else:
+ self.aliases.append(AddressAlias(addr, *args))
+
+ def __len__(self):
+ return len(self.aliases)
+
+ def __str__(self):
+ return '<AliasGroup [%s]>' % (', '.join(map(str, self.aliases)))
+
+ def createMessageReceiver(self):
+ return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
+
+ def resolve(self, aliasmap, memo=None):
+ if memo is None:
+ memo = {}
+ r = []
+ for a in self.aliases:
+ r.append(a.resolve(aliasmap, memo))
+ return MultiWrapper(filter(None, r))
+
diff --git a/twisted/mail/bounce.py b/twisted/mail/bounce.py
new file mode 100644
index 0000000..5d5bde9
--- /dev/null
+++ b/twisted/mail/bounce.py
@@ -0,0 +1,60 @@
+# -*- test-case-name: twisted.mail.test.test_bounce -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import StringIO
+import rfc822
+import time
+import os
+
+
+from twisted.mail import smtp
+
+BOUNCE_FORMAT = """\
+From: postmaster@%(failedDomain)s
+To: %(failedFrom)s
+Subject: Returned Mail: see transcript for details
+Message-ID: %(messageID)s
+Content-Type: multipart/report; report-type=delivery-status;
+ boundary="%(boundary)s"
+
+--%(boundary)s
+
+%(transcript)s
+
+--%(boundary)s
+Content-Type: message/delivery-status
+Arrival-Date: %(ctime)s
+Final-Recipient: RFC822; %(failedTo)s
+"""
+
+def generateBounce(message, failedFrom, failedTo, transcript=''):
+ if not transcript:
+ transcript = '''\
+I'm sorry, the following address has permanent errors: %(failedTo)s.
+I've given up, and I will not retry the message again.
+''' % vars()
+
+ boundary = "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX')
+ failedAddress = rfc822.AddressList(failedTo)[0][1]
+ failedDomain = failedAddress.split('@', 1)[1]
+ messageID = smtp.messageid(uniq='bounce')
+ ctime = time.ctime(time.time())
+
+ fp = StringIO.StringIO()
+ fp.write(BOUNCE_FORMAT % vars())
+ orig = message.tell()
+ message.seek(2, 0)
+ sz = message.tell()
+ message.seek(0, orig)
+ if sz > 10000:
+ while 1:
+ line = message.readline()
+ if len(line)<=1:
+ break
+ fp.write(line)
+ else:
+ fp.write(message.read())
+ return '', failedFrom, fp.getvalue()
diff --git a/twisted/mail/imap4.py b/twisted/mail/imap4.py
new file mode 100644
index 0000000..6ca8384
--- /dev/null
+++ b/twisted/mail/imap4.py
@@ -0,0 +1,5854 @@
+# -*- test-case-name: twisted.mail.test.test_imap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An IMAP4 protocol implementation
+
+@author: Jp Calderone
+
+To do::
+ Suspend idle timeout while server is processing
+ Use an async message parser instead of buffering in memory
+ Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
+ Clarify some API docs (Query, etc)
+ Make APPEND recognize (again) non-existent mailboxes before accepting the literal
+"""
+
+import rfc822
+import base64
+import binascii
+import hmac
+import re
+import copy
+import tempfile
+import string
+import time
+import random
+import types
+
+import email.Utils
+
+try:
+ import cStringIO as StringIO
+except:
+ import StringIO
+
+from zope.interface import implements, Interface
+
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import defer
+from twisted.internet import error
+from twisted.internet.defer import maybeDeferred
+from twisted.python import log, text
+from twisted.internet import interfaces
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.credentials
+
+
+# locale-independent month names to use instead of strftime's
+_MONTH_NAMES = dict(zip(
+ range(1, 13),
+ "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()))
+
+
+class MessageSet(object):
+ """
+ Essentially an infinite bitfield, with some extra features.
+
+ @type getnext: Function taking C{int} returning C{int}
+ @ivar getnext: A function that returns the next message number,
+ used when iterating through the MessageSet. By default, a function
+ returning the next integer is supplied, but as this can be rather
+ inefficient for sparse UID iterations, it is recommended to supply
+ one when messages are requested by UID. The argument is provided
+ as a hint to the implementation and may be ignored if it makes sense
+ to do so (eg, if an iterator is being used that maintains its own
+ state, it is guaranteed that it will not be called out-of-order).
+ """
+ _empty = []
+
+ def __init__(self, start=_empty, end=_empty):
+ """
+ Create a new MessageSet()
+
+ @type start: Optional C{int}
+ @param start: Start of range, or only message number
+
+ @type end: Optional C{int}
+ @param end: End of range.
+ """
+ self._last = self._empty # Last message/UID in use
+ self.ranges = [] # List of ranges included
+ self.getnext = lambda x: x+1 # A function which will return the next
+ # message id. Handy for UID requests.
+
+ if start is self._empty:
+ return
+
+ if isinstance(start, types.ListType):
+ self.ranges = start[:]
+ self.clean()
+ else:
+ self.add(start,end)
+
+ # Ooo. A property.
+ def last():
+ def _setLast(self, value):
+ if self._last is not self._empty:
+ raise ValueError("last already set")
+
+ self._last = value
+ for i, (l, h) in enumerate(self.ranges):
+ if l is not None:
+ break # There are no more Nones after this
+ l = value
+ if h is None:
+ h = value
+ if l > h:
+ l, h = h, l
+ self.ranges[i] = (l, h)
+
+ self.clean()
+
+ def _getLast(self):
+ return self._last
+
+ doc = '''
+ "Highest" message number, refered to by "*".
+ Must be set before attempting to use the MessageSet.
+ '''
+ return _getLast, _setLast, None, doc
+ last = property(*last())
+
+ def add(self, start, end=_empty):
+ """
+ Add another range
+
+ @type start: C{int}
+ @param start: Start of range, or only message number
+
+ @type end: Optional C{int}
+ @param end: End of range.
+ """
+ if end is self._empty:
+ end = start
+
+ if self._last is not self._empty:
+ if start is None:
+ start = self.last
+ if end is None:
+ end = self.last
+
+ if start > end:
+ # Try to keep in low, high order if possible
+ # (But we don't know what None means, this will keep
+ # None at the start of the ranges list)
+ start, end = end, start
+
+ self.ranges.append((start, end))
+ self.clean()
+
+ def __add__(self, other):
+ if isinstance(other, MessageSet):
+ ranges = self.ranges + other.ranges
+ return MessageSet(ranges)
+ else:
+ res = MessageSet(self.ranges)
+ try:
+ res.add(*other)
+ except TypeError:
+ res.add(other)
+ return res
+
+
+ def extend(self, other):
+ if isinstance(other, MessageSet):
+ self.ranges.extend(other.ranges)
+ self.clean()
+ else:
+ try:
+ self.add(*other)
+ except TypeError:
+ self.add(other)
+
+ return self
+
+
+ def clean(self):
+ """
+ Clean ranges list, combining adjacent ranges
+ """
+
+ self.ranges.sort()
+
+ oldl, oldh = None, None
+ for i,(l, h) in enumerate(self.ranges):
+ if l is None:
+ continue
+ # l is >= oldl and h is >= oldh due to sort()
+ if oldl is not None and l <= oldh + 1:
+ l = oldl
+ h = max(oldh, h)
+ self.ranges[i - 1] = None
+ self.ranges[i] = (l, h)
+
+ oldl, oldh = l, h
+
+ self.ranges = filter(None, self.ranges)
+
+
+ def __contains__(self, value):
+ """
+ May raise TypeError if we encounter an open-ended range
+ """
+ for l, h in self.ranges:
+ if l is None:
+ raise TypeError(
+ "Can't determine membership; last value not set")
+ if l <= value <= h:
+ return True
+
+ return False
+
+
+ def _iterator(self):
+ for l, h in self.ranges:
+ l = self.getnext(l-1)
+ while l <= h:
+ yield l
+ l = self.getnext(l)
+ if l is None:
+ break
+
+ def __iter__(self):
+ if self.ranges and self.ranges[0][0] is None:
+ raise TypeError("Can't iterate; last value not set")
+
+ return self._iterator()
+
+ def __len__(self):
+ res = 0
+ for l, h in self.ranges:
+ if l is None:
+ if h is None:
+ res += 1
+ else:
+ raise TypeError("Can't size object; last value not set")
+ else:
+ res += (h - l) + 1
+
+ return res
+
+ def __str__(self):
+ p = []
+ for low, high in self.ranges:
+ if low == high:
+ if low is None:
+ p.append('*')
+ else:
+ p.append(str(low))
+ elif low is None:
+ p.append('%d:*' % (high,))
+ else:
+ p.append('%d:%d' % (low, high))
+ return ','.join(p)
+
+ def __repr__(self):
+ return '<MessageSet %s>' % (str(self),)
+
+ def __eq__(self, other):
+ if isinstance(other, MessageSet):
+ return self.ranges == other.ranges
+ return False
+
+
+class LiteralString:
+ def __init__(self, size, defered):
+ self.size = size
+ self.data = []
+ self.defer = defered
+
+ def write(self, data):
+ self.size -= len(data)
+ passon = None
+ if self.size > 0:
+ self.data.append(data)
+ else:
+ if self.size:
+ data, passon = data[:self.size], data[self.size:]
+ else:
+ passon = ''
+ if data:
+ self.data.append(data)
+ return passon
+
+ def callback(self, line):
+ """
+ Call defered with data and rest of line
+ """
+ self.defer.callback((''.join(self.data), line))
+
+class LiteralFile:
+ _memoryFileLimit = 1024 * 1024 * 10
+
+ def __init__(self, size, defered):
+ self.size = size
+ self.defer = defered
+ if size > self._memoryFileLimit:
+ self.data = tempfile.TemporaryFile()
+ else:
+ self.data = StringIO.StringIO()
+
+ def write(self, data):
+ self.size -= len(data)
+ passon = None
+ if self.size > 0:
+ self.data.write(data)
+ else:
+ if self.size:
+ data, passon = data[:self.size], data[self.size:]
+ else:
+ passon = ''
+ if data:
+ self.data.write(data)
+ return passon
+
+ def callback(self, line):
+ """
+ Call defered with data and rest of line
+ """
+ self.data.seek(0,0)
+ self.defer.callback((self.data, line))
+
+
+class WriteBuffer:
+ """Buffer up a bunch of writes before sending them all to a transport at once.
+ """
+ def __init__(self, transport, size=8192):
+ self.bufferSize = size
+ self.transport = transport
+ self._length = 0
+ self._writes = []
+
+ def write(self, s):
+ self._length += len(s)
+ self._writes.append(s)
+ if self._length > self.bufferSize:
+ self.flush()
+
+ def flush(self):
+ if self._writes:
+ self.transport.writeSequence(self._writes)
+ self._writes = []
+ self._length = 0
+
+
+class Command:
+ _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
+ _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
+ _OK_RESPONSES = ('UIDVALIDITY', 'UNSEEN', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
+ defer = None
+
+ def __init__(self, command, args=None, wantResponse=(),
+ continuation=None, *contArgs, **contKw):
+ self.command = command
+ self.args = args
+ self.wantResponse = wantResponse
+ self.continuation = lambda x: continuation(x, *contArgs, **contKw)
+ self.lines = []
+
+ def format(self, tag):
+ if self.args is None:
+ return ' '.join((tag, self.command))
+ return ' '.join((tag, self.command, self.args))
+
+ def finish(self, lastLine, unusedCallback):
+ send = []
+ unuse = []
+ for L in self.lines:
+ names = parseNestedParens(L)
+ N = len(names)
+ if (N >= 1 and names[0] in self._1_RESPONSES or
+ N >= 2 and names[1] in self._2_RESPONSES or
+ N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
+ send.append(names)
+ else:
+ unuse.append(names)
+ d, self.defer = self.defer, None
+ d.callback((send, lastLine))
+ if unuse:
+ unusedCallback(unuse)
+
+class LOGINCredentials(cred.credentials.UsernamePassword):
+ def __init__(self):
+ self.challenges = ['Password\0', 'User Name\0']
+ self.responses = ['password', 'username']
+ cred.credentials.UsernamePassword.__init__(self, None, None)
+
+ def getChallenge(self):
+ return self.challenges.pop()
+
+ def setResponse(self, response):
+ setattr(self, self.responses.pop(), response)
+
+ def moreChallenges(self):
+ return bool(self.challenges)
+
+class PLAINCredentials(cred.credentials.UsernamePassword):
+ def __init__(self):
+ cred.credentials.UsernamePassword.__init__(self, None, None)
+
+ def getChallenge(self):
+ return ''
+
+ def setResponse(self, response):
+ parts = response.split('\0')
+ if len(parts) != 3:
+ raise IllegalClientResponse("Malformed Response - wrong number of parts")
+ useless, self.username, self.password = parts
+
+ def moreChallenges(self):
+ return False
+
+class IMAP4Exception(Exception):
+ def __init__(self, *args):
+ Exception.__init__(self, *args)
+
+class IllegalClientResponse(IMAP4Exception): pass
+
+class IllegalOperation(IMAP4Exception): pass
+
+class IllegalMailboxEncoding(IMAP4Exception): pass
+
+class IMailboxListener(Interface):
+ """Interface for objects interested in mailbox events"""
+
+ def modeChanged(writeable):
+ """Indicates that the write status of a mailbox has changed.
+
+ @type writeable: C{bool}
+ @param writeable: A true value if write is now allowed, false
+ otherwise.
+ """
+
+ def flagsChanged(newFlags):
+ """Indicates that the flags of one or more messages have changed.
+
+ @type newFlags: C{dict}
+ @param newFlags: A mapping of message identifiers to tuples of flags
+ now set on that message.
+ """
+
+ def newMessages(exists, recent):
+ """Indicates that the number of messages in a mailbox has changed.
+
+ @type exists: C{int} or C{None}
+ @param exists: The total number of messages now in this mailbox.
+ If the total number of messages has not changed, this should be
+ C{None}.
+
+ @type recent: C{int}
+ @param recent: The number of messages now flagged \\Recent.
+ If the number of recent messages has not changed, this should be
+ C{None}.
+ """
+
+class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
+ """
+ Protocol implementation for an IMAP4rev1 server.
+
+ The server can be in any of four states:
+ - Non-authenticated
+ - Authenticated
+ - Selected
+ - Logout
+ """
+ implements(IMailboxListener)
+
+ # Identifier for this server software
+ IDENT = 'Twisted IMAP4rev1 Ready'
+
+ # Number of seconds before idle timeout
+ # Initially 1 minute. Raised to 30 minutes after login.
+ timeOut = 60
+
+ POSTAUTH_TIMEOUT = 60 * 30
+
+ # Whether STARTTLS has been issued successfully yet or not.
+ startedTLS = False
+
+ # Whether our transport supports TLS
+ canStartTLS = False
+
+ # Mapping of tags to commands we have received
+ tags = None
+
+ # The object which will handle logins for us
+ portal = None
+
+ # The account object for this connection
+ account = None
+
+ # Logout callback
+ _onLogout = None
+
+ # The currently selected mailbox
+ mbox = None
+
+ # Command data to be processed when literal data is received
+ _pendingLiteral = None
+
+ # Maximum length to accept for a "short" string literal
+ _literalStringLimit = 4096
+
+ # IChallengeResponse factories for AUTHENTICATE command
+ challengers = None
+
+ # Search terms the implementation of which needs to be passed both the last
+ # message identifier (UID) and the last sequence id.
+ _requiresLastMessageInfo = set(["OR", "NOT", "UID"])
+
+ state = 'unauth'
+
+ parseState = 'command'
+
+ def __init__(self, chal = None, contextFactory = None, scheduler = None):
+ if chal is None:
+ chal = {}
+ self.challengers = chal
+ self.ctx = contextFactory
+ if scheduler is None:
+ scheduler = iterateInReactor
+ self._scheduler = scheduler
+ self._queuedAsync = []
+
+ def capabilities(self):
+ cap = {'AUTH': self.challengers.keys()}
+ if self.ctx and self.canStartTLS:
+ if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
+ cap['LOGINDISABLED'] = None
+ cap['STARTTLS'] = None
+ cap['NAMESPACE'] = None
+ cap['IDLE'] = None
+ return cap
+
+ def connectionMade(self):
+ self.tags = {}
+ self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
+ self.setTimeout(self.timeOut)
+ self.sendServerGreeting()
+
+ def connectionLost(self, reason):
+ self.setTimeout(None)
+ if self._onLogout:
+ self._onLogout()
+ self._onLogout = None
+
+ def timeoutConnection(self):
+ self.sendLine('* BYE Autologout; connection idle too long')
+ self.transport.loseConnection()
+ if self.mbox:
+ self.mbox.removeListener(self)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ maybeDeferred(cmbx.close).addErrback(log.err)
+ self.mbox = None
+ self.state = 'timeout'
+
+ def rawDataReceived(self, data):
+ self.resetTimeout()
+ passon = self._pendingLiteral.write(data)
+ if passon is not None:
+ self.setLineMode(passon)
+
+ # Avoid processing commands while buffers are being dumped to
+ # our transport
+ blocked = None
+
+ def _unblock(self):
+ commands = self.blocked
+ self.blocked = None
+ while commands and self.blocked is None:
+ self.lineReceived(commands.pop(0))
+ if self.blocked is not None:
+ self.blocked.extend(commands)
+
+ def lineReceived(self, line):
+ if self.blocked is not None:
+ self.blocked.append(line)
+ return
+
+ self.resetTimeout()
+
+ f = getattr(self, 'parse_' + self.parseState)
+ try:
+ f(line)
+ except Exception, e:
+ self.sendUntaggedResponse('BAD Server error: ' + str(e))
+ log.err()
+
+ def parse_command(self, line):
+ args = line.split(None, 2)
+ rest = None
+ if len(args) == 3:
+ tag, cmd, rest = args
+ elif len(args) == 2:
+ tag, cmd = args
+ elif len(args) == 1:
+ tag = args[0]
+ self.sendBadResponse(tag, 'Missing command')
+ return None
+ else:
+ self.sendBadResponse(None, 'Null command')
+ return None
+
+ cmd = cmd.upper()
+ try:
+ return self.dispatchCommand(tag, cmd, rest)
+ except IllegalClientResponse, e:
+ self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
+ except IllegalOperation, e:
+ self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
+ except IllegalMailboxEncoding, e:
+ self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
+
+ def parse_pending(self, line):
+ d = self._pendingLiteral
+ self._pendingLiteral = None
+ self.parseState = 'command'
+ d.callback(line)
+
+ def dispatchCommand(self, tag, cmd, rest, uid=None):
+ f = self.lookupCommand(cmd)
+ if f:
+ fn = f[0]
+ parseargs = f[1:]
+ self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
+ else:
+ self.sendBadResponse(tag, 'Unsupported command')
+
+ def lookupCommand(self, cmd):
+ return getattr(self, '_'.join((self.state, cmd.upper())), None)
+
+ def __doCommand(self, tag, handler, args, parseargs, line, uid):
+ for (i, arg) in enumerate(parseargs):
+ if callable(arg):
+ parseargs = parseargs[i+1:]
+ maybeDeferred(arg, self, line).addCallback(
+ self.__cbDispatch, tag, handler, args,
+ parseargs, uid).addErrback(self.__ebDispatch, tag)
+ return
+ else:
+ args.append(arg)
+
+ if line:
+ # Too many arguments
+ raise IllegalClientResponse("Too many arguments for command: " + repr(line))
+
+ if uid is not None:
+ handler(uid=uid, *args)
+ else:
+ handler(*args)
+
+ def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
+ args.append(arg)
+ self.__doCommand(tag, fn, args, parseargs, rest, uid)
+
+ def __ebDispatch(self, failure, tag):
+ if failure.check(IllegalClientResponse):
+ self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
+ elif failure.check(IllegalOperation):
+ self.sendNegativeResponse(tag, 'Illegal operation: ' +
+ str(failure.value))
+ elif failure.check(IllegalMailboxEncoding):
+ self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
+ str(failure.value))
+ else:
+ self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+ log.err(failure)
+
+ def _stringLiteral(self, size):
+ if size > self._literalStringLimit:
+ raise IllegalClientResponse(
+ "Literal too long! I accept at most %d octets" %
+ (self._literalStringLimit,))
+ d = defer.Deferred()
+ self.parseState = 'pending'
+ self._pendingLiteral = LiteralString(size, d)
+ self.sendContinuationRequest('Ready for %d octets of text' % size)
+ self.setRawMode()
+ return d
+
+ def _fileLiteral(self, size):
+ d = defer.Deferred()
+ self.parseState = 'pending'
+ self._pendingLiteral = LiteralFile(size, d)
+ self.sendContinuationRequest('Ready for %d octets of data' % size)
+ self.setRawMode()
+ return d
+
+ def arg_astring(self, line):
+ """
+ Parse an astring from the line, return (arg, rest), possibly
+ via a deferred (to handle literals)
+ """
+ line = line.strip()
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+ d = None
+ arg, rest = None, None
+ if line[0] == '"':
+ try:
+ spam, arg, rest = line.split('"',2)
+ rest = rest[1:] # Strip space
+ except ValueError:
+ raise IllegalClientResponse("Unmatched quotes")
+ elif line[0] == '{':
+ # literal
+ if line[-1] != '}':
+ raise IllegalClientResponse("Malformed literal")
+ try:
+ size = int(line[1:-1])
+ except ValueError:
+ raise IllegalClientResponse("Bad literal size: " + line[1:-1])
+ d = self._stringLiteral(size)
+ else:
+ arg = line.split(' ',1)
+ if len(arg) == 1:
+ arg.append('')
+ arg, rest = arg
+ return d or (arg, rest)
+
+ # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
+ atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
+
+ def arg_atom(self, line):
+ """
+ Parse an atom from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+ m = self.atomre.match(line)
+ if m:
+ return m.group('atom'), m.group('rest')
+ else:
+ raise IllegalClientResponse("Malformed ATOM")
+
+ def arg_plist(self, line):
+ """
+ Parse a (non-nested) parenthesised list from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+
+ if line[0] != "(":
+ raise IllegalClientResponse("Missing parenthesis")
+
+ i = line.find(")")
+
+ if i == -1:
+ raise IllegalClientResponse("Mismatched parenthesis")
+
+ return (parseNestedParens(line[1:i],0), line[i+2:])
+
+ def arg_literal(self, line):
+ """
+ Parse a literal from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+
+ if line[0] != '{':
+ raise IllegalClientResponse("Missing literal")
+
+ if line[-1] != '}':
+ raise IllegalClientResponse("Malformed literal")
+
+ try:
+ size = int(line[1:-1])
+ except ValueError:
+ raise IllegalClientResponse("Bad literal size: " + line[1:-1])
+
+ return self._fileLiteral(size)
+
+ def arg_searchkeys(self, line):
+ """
+ searchkeys
+ """
+ query = parseNestedParens(line)
+ # XXX Should really use list of search terms and parse into
+ # a proper tree
+
+ return (query, '')
+
+ def arg_seqset(self, line):
+ """
+ sequence-set
+ """
+ rest = ''
+ arg = line.split(' ',1)
+ if len(arg) == 2:
+ rest = arg[1]
+ arg = arg[0]
+
+ try:
+ return (parseIdList(arg), rest)
+ except IllegalIdentifierError, e:
+ raise IllegalClientResponse("Bad message number " + str(e))
+
+ def arg_fetchatt(self, line):
+ """
+ fetch-att
+ """
+ p = _FetchParser()
+ p.parseString(line)
+ return (p.result, '')
+
+ def arg_flaglist(self, line):
+ """
+ Flag part of store-att-flag
+ """
+ flags = []
+ if line[0] == '(':
+ if line[-1] != ')':
+ raise IllegalClientResponse("Mismatched parenthesis")
+ line = line[1:-1]
+
+ while line:
+ m = self.atomre.search(line)
+ if not m:
+ raise IllegalClientResponse("Malformed flag")
+ if line[0] == '\\' and m.start() == 1:
+ flags.append('\\' + m.group('atom'))
+ elif m.start() == 0:
+ flags.append(m.group('atom'))
+ else:
+ raise IllegalClientResponse("Malformed flag")
+ line = m.group('rest')
+
+ return (flags, '')
+
+ def arg_line(self, line):
+ """
+ Command line of UID command
+ """
+ return (line, '')
+
+ def opt_plist(self, line):
+ """
+ Optional parenthesised list
+ """
+ if line.startswith('('):
+ return self.arg_plist(line)
+ else:
+ return (None, line)
+
+ def opt_datetime(self, line):
+ """
+ Optional date-time string
+ """
+ if line.startswith('"'):
+ try:
+ spam, date, rest = line.split('"',2)
+ except IndexError:
+ raise IllegalClientResponse("Malformed date-time")
+ return (date, rest[1:])
+ else:
+ return (None, line)
+
+ def opt_charset(self, line):
+ """
+ Optional charset of SEARCH command
+ """
+ if line[:7].upper() == 'CHARSET':
+ arg = line.split(' ',2)
+ if len(arg) == 1:
+ raise IllegalClientResponse("Missing charset identifier")
+ if len(arg) == 2:
+ arg.append('')
+ spam, arg, rest = arg
+ return (arg, rest)
+ else:
+ return (None, line)
+
+ def sendServerGreeting(self):
+ msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
+ self.sendPositiveResponse(message=msg)
+
+ def sendBadResponse(self, tag = None, message = ''):
+ self._respond('BAD', tag, message)
+
+ def sendPositiveResponse(self, tag = None, message = ''):
+ self._respond('OK', tag, message)
+
+ def sendNegativeResponse(self, tag = None, message = ''):
+ self._respond('NO', tag, message)
+
+ def sendUntaggedResponse(self, message, async=False):
+ if not async or (self.blocked is None):
+ self._respond(message, None, None)
+ else:
+ self._queuedAsync.append(message)
+
+ def sendContinuationRequest(self, msg = 'Ready for additional command text'):
+ if msg:
+ self.sendLine('+ ' + msg)
+ else:
+ self.sendLine('+')
+
+ def _respond(self, state, tag, message):
+ if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
+ lines = self._queuedAsync
+ self._queuedAsync = []
+ for msg in lines:
+ self._respond(msg, None, None)
+ if not tag:
+ tag = '*'
+ if message:
+ self.sendLine(' '.join((tag, state, message)))
+ else:
+ self.sendLine(' '.join((tag, state)))
+
+ def listCapabilities(self):
+ caps = ['IMAP4rev1']
+ for c, v in self.capabilities().iteritems():
+ if v is None:
+ caps.append(c)
+ elif len(v):
+ caps.extend([('%s=%s' % (c, cap)) for cap in v])
+ return caps
+
+ def do_CAPABILITY(self, tag):
+ self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
+ self.sendPositiveResponse(tag, 'CAPABILITY completed')
+
+ unauth_CAPABILITY = (do_CAPABILITY,)
+ auth_CAPABILITY = unauth_CAPABILITY
+ select_CAPABILITY = unauth_CAPABILITY
+ logout_CAPABILITY = unauth_CAPABILITY
+
+ def do_LOGOUT(self, tag):
+ self.sendUntaggedResponse('BYE Nice talking to you')
+ self.sendPositiveResponse(tag, 'LOGOUT successful')
+ self.transport.loseConnection()
+
+ unauth_LOGOUT = (do_LOGOUT,)
+ auth_LOGOUT = unauth_LOGOUT
+ select_LOGOUT = unauth_LOGOUT
+ logout_LOGOUT = unauth_LOGOUT
+
+ def do_NOOP(self, tag):
+ self.sendPositiveResponse(tag, 'NOOP No operation performed')
+
+ unauth_NOOP = (do_NOOP,)
+ auth_NOOP = unauth_NOOP
+ select_NOOP = unauth_NOOP
+ logout_NOOP = unauth_NOOP
+
+ def do_AUTHENTICATE(self, tag, args):
+ args = args.upper().strip()
+ if args not in self.challengers:
+ self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
+ else:
+ self.authenticate(self.challengers[args](), tag)
+
+ unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
+
+ def authenticate(self, chal, tag):
+ if self.portal is None:
+ self.sendNegativeResponse(tag, 'Temporary authentication failure')
+ return
+
+ self._setupChallenge(chal, tag)
+
+ def _setupChallenge(self, chal, tag):
+ try:
+ challenge = chal.getChallenge()
+ except Exception, e:
+ self.sendBadResponse(tag, 'Server error: ' + str(e))
+ else:
+ coded = base64.encodestring(challenge)[:-1]
+ self.parseState = 'pending'
+ self._pendingLiteral = defer.Deferred()
+ self.sendContinuationRequest(coded)
+ self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
+ self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
+
+ def __cbAuthChunk(self, result, chal, tag):
+ try:
+ uncoded = base64.decodestring(result)
+ except binascii.Error:
+ raise IllegalClientResponse("Malformed Response - not base64")
+
+ chal.setResponse(uncoded)
+ if chal.moreChallenges():
+ self._setupChallenge(chal, tag)
+ else:
+ self.portal.login(chal, None, IAccount).addCallbacks(
+ self.__cbAuthResp,
+ self.__ebAuthResp,
+ (tag,), None, (tag,), None
+ )
+
+ def __cbAuthResp(self, (iface, avatar, logout), tag):
+ assert iface is IAccount, "IAccount is the only supported interface"
+ self.account = avatar
+ self.state = 'auth'
+ self._onLogout = logout
+ self.sendPositiveResponse(tag, 'Authentication successful')
+ self.setTimeout(self.POSTAUTH_TIMEOUT)
+
+ def __ebAuthResp(self, failure, tag):
+ if failure.check(cred.error.UnauthorizedLogin):
+ self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
+ elif failure.check(cred.error.UnhandledCredentials):
+ self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
+ else:
+ self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
+ log.err(failure)
+
+ def __ebAuthChunk(self, failure, tag):
+ self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
+
+ def do_STARTTLS(self, tag):
+ if self.startedTLS:
+ self.sendNegativeResponse(tag, 'TLS already negotiated')
+ elif self.ctx and self.canStartTLS:
+ self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
+ self.transport.startTLS(self.ctx)
+ self.startedTLS = True
+ self.challengers = self.challengers.copy()
+ if 'LOGIN' not in self.challengers:
+ self.challengers['LOGIN'] = LOGINCredentials
+ if 'PLAIN' not in self.challengers:
+ self.challengers['PLAIN'] = PLAINCredentials
+ else:
+ self.sendNegativeResponse(tag, 'TLS not available')
+
+ unauth_STARTTLS = (do_STARTTLS,)
+
+ def do_LOGIN(self, tag, user, passwd):
+ if 'LOGINDISABLED' in self.capabilities():
+ self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
+ return
+
+ maybeDeferred(self.authenticateLogin, user, passwd
+ ).addCallback(self.__cbLogin, tag
+ ).addErrback(self.__ebLogin, tag
+ )
+
+ unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
+
+ def authenticateLogin(self, user, passwd):
+ """Lookup the account associated with the given parameters
+
+ Override this method to define the desired authentication behavior.
+
+ The default behavior is to defer authentication to C{self.portal}
+ if it is not None, or to deny the login otherwise.
+
+ @type user: C{str}
+ @param user: The username to lookup
+
+ @type passwd: C{str}
+ @param passwd: The password to login with
+ """
+ if self.portal:
+ return self.portal.login(
+ cred.credentials.UsernamePassword(user, passwd),
+ None, IAccount
+ )
+ raise cred.error.UnauthorizedLogin()
+
+ def __cbLogin(self, (iface, avatar, logout), tag):
+ if iface is not IAccount:
+ self.sendBadResponse(tag, 'Server error: login returned unexpected value')
+ log.err("__cbLogin called with %r, IAccount expected" % (iface,))
+ else:
+ self.account = avatar
+ self._onLogout = logout
+ self.sendPositiveResponse(tag, 'LOGIN succeeded')
+ self.state = 'auth'
+ self.setTimeout(self.POSTAUTH_TIMEOUT)
+
+ def __ebLogin(self, failure, tag):
+ if failure.check(cred.error.UnauthorizedLogin):
+ self.sendNegativeResponse(tag, 'LOGIN failed')
+ else:
+ self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+ log.err(failure)
+
+ def do_NAMESPACE(self, tag):
+ personal = public = shared = None
+ np = INamespacePresenter(self.account, None)
+ if np is not None:
+ personal = np.getPersonalNamespaces()
+ public = np.getSharedNamespaces()
+ shared = np.getSharedNamespaces()
+ self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
+ self.sendPositiveResponse(tag, "NAMESPACE command completed")
+
+ auth_NAMESPACE = (do_NAMESPACE,)
+ select_NAMESPACE = auth_NAMESPACE
+
+ def _parseMbox(self, name):
+ if isinstance(name, unicode):
+ return name
+ try:
+ return name.decode('imap4-utf-7')
+ except:
+ log.err()
+ raise IllegalMailboxEncoding(name)
+
+ def _selectWork(self, tag, name, rw, cmdName):
+ if self.mbox:
+ self.mbox.removeListener(self)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ maybeDeferred(cmbx.close).addErrback(log.err)
+ self.mbox = None
+ self.state = 'auth'
+
+ name = self._parseMbox(name)
+ maybeDeferred(self.account.select, self._parseMbox(name), rw
+ ).addCallback(self._cbSelectWork, cmdName, tag
+ ).addErrback(self._ebSelectWork, cmdName, tag
+ )
+
+ def _ebSelectWork(self, failure, cmdName, tag):
+ self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
+ log.err(failure)
+
+ def _cbSelectWork(self, mbox, cmdName, tag):
+ if mbox is None:
+ self.sendNegativeResponse(tag, 'No such mailbox')
+ return
+ if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
+ self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
+ return
+
+ flags = mbox.getFlags()
+ self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
+ self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
+ self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
+ self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
+
+ s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
+ mbox.addListener(self)
+ self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
+ self.state = 'select'
+ self.mbox = mbox
+
+ auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
+ select_SELECT = auth_SELECT
+
+ auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
+ select_EXAMINE = auth_EXAMINE
+
+
+ def do_IDLE(self, tag):
+ self.sendContinuationRequest(None)
+ self.parseTag = tag
+ self.lastState = self.parseState
+ self.parseState = 'idle'
+
+ def parse_idle(self, *args):
+ self.parseState = self.lastState
+ del self.lastState
+ self.sendPositiveResponse(self.parseTag, "IDLE terminated")
+ del self.parseTag
+
+ select_IDLE = ( do_IDLE, )
+ auth_IDLE = select_IDLE
+
+
+ def do_CREATE(self, tag, name):
+ name = self._parseMbox(name)
+ try:
+ result = self.account.create(name)
+ except MailboxException, c:
+ self.sendNegativeResponse(tag, str(c))
+ except:
+ self.sendBadResponse(tag, "Server error encountered while creating mailbox")
+ log.err()
+ else:
+ if result:
+ self.sendPositiveResponse(tag, 'Mailbox created')
+ else:
+ self.sendNegativeResponse(tag, 'Mailbox not created')
+
+ auth_CREATE = (do_CREATE, arg_astring)
+ select_CREATE = auth_CREATE
+
+ def do_DELETE(self, tag, name):
+ name = self._parseMbox(name)
+ if name.lower() == 'inbox':
+ self.sendNegativeResponse(tag, 'You cannot delete the inbox')
+ return
+ try:
+ self.account.delete(name)
+ except MailboxException, m:
+ self.sendNegativeResponse(tag, str(m))
+ except:
+ self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, 'Mailbox deleted')
+
+ auth_DELETE = (do_DELETE, arg_astring)
+ select_DELETE = auth_DELETE
+
+ def do_RENAME(self, tag, oldname, newname):
+ oldname, newname = [self._parseMbox(n) for n in oldname, newname]
+ if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
+ self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
+ return
+ try:
+ self.account.rename(oldname, newname)
+ except TypeError:
+ self.sendBadResponse(tag, 'Invalid command syntax')
+ except MailboxException, m:
+ self.sendNegativeResponse(tag, str(m))
+ except:
+ self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, 'Mailbox renamed')
+
+ auth_RENAME = (do_RENAME, arg_astring, arg_astring)
+ select_RENAME = auth_RENAME
+
+ def do_SUBSCRIBE(self, tag, name):
+ name = self._parseMbox(name)
+ try:
+ self.account.subscribe(name)
+ except MailboxException, m:
+ self.sendNegativeResponse(tag, str(m))
+ except:
+ self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, 'Subscribed')
+
+ auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
+ select_SUBSCRIBE = auth_SUBSCRIBE
+
+ def do_UNSUBSCRIBE(self, tag, name):
+ name = self._parseMbox(name)
+ try:
+ self.account.unsubscribe(name)
+ except MailboxException, m:
+ self.sendNegativeResponse(tag, str(m))
+ except:
+ self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, 'Unsubscribed')
+
+ auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
+ select_UNSUBSCRIBE = auth_UNSUBSCRIBE
+
+ def _listWork(self, tag, ref, mbox, sub, cmdName):
+ mbox = self._parseMbox(mbox)
+ maybeDeferred(self.account.listMailboxes, ref, mbox
+ ).addCallback(self._cbListWork, tag, sub, cmdName
+ ).addErrback(self._ebListWork, tag
+ )
+
+ def _cbListWork(self, mailboxes, tag, sub, cmdName):
+ for (name, box) in mailboxes:
+ if not sub or self.account.isSubscribed(name):
+ flags = box.getFlags()
+ delim = box.getHierarchicalDelimiter()
+ resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
+ self.sendUntaggedResponse(collapseNestedLists(resp))
+ self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
+
+ def _ebListWork(self, failure, tag):
+ self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
+ log.err(failure)
+
+ auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
+ select_LIST = auth_LIST
+
+ auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
+ select_LSUB = auth_LSUB
+
+ def do_STATUS(self, tag, mailbox, names):
+ mailbox = self._parseMbox(mailbox)
+ maybeDeferred(self.account.select, mailbox, 0
+ ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
+ ).addErrback(self._ebStatusGotMailbox, tag
+ )
+
+ def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
+ if mbox:
+ maybeDeferred(mbox.requestStatus, names).addCallbacks(
+ self.__cbStatus, self.__ebStatus,
+ (tag, mailbox), None, (tag, mailbox), None
+ )
+ else:
+ self.sendNegativeResponse(tag, "Could not open mailbox")
+
+ def _ebStatusGotMailbox(self, failure, tag):
+ self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
+ log.err(failure)
+
+ auth_STATUS = (do_STATUS, arg_astring, arg_plist)
+ select_STATUS = auth_STATUS
+
+ def __cbStatus(self, status, tag, box):
+ line = ' '.join(['%s %s' % x for x in status.iteritems()])
+ self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
+ self.sendPositiveResponse(tag, 'STATUS complete')
+
+ def __ebStatus(self, failure, tag, box):
+ self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
+
+ def do_APPEND(self, tag, mailbox, flags, date, message):
+ mailbox = self._parseMbox(mailbox)
+ maybeDeferred(self.account.select, mailbox
+ ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
+ ).addErrback(self._ebAppendGotMailbox, tag
+ )
+
+ def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
+ if not mbox:
+ self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
+ return
+
+ d = mbox.addMessage(message, flags, date)
+ d.addCallback(self.__cbAppend, tag, mbox)
+ d.addErrback(self.__ebAppend, tag)
+
+ def _ebAppendGotMailbox(self, failure, tag):
+ self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
+ log.err(failure)
+
+ auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
+ arg_literal)
+ select_APPEND = auth_APPEND
+
+ def __cbAppend(self, result, tag, mbox):
+ self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
+ self.sendPositiveResponse(tag, 'APPEND complete')
+
+ def __ebAppend(self, failure, tag):
+ self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
+
+ def do_CHECK(self, tag):
+ d = self.checkpoint()
+ if d is None:
+ self.__cbCheck(None, tag)
+ else:
+ d.addCallbacks(
+ self.__cbCheck,
+ self.__ebCheck,
+ callbackArgs=(tag,),
+ errbackArgs=(tag,)
+ )
+ select_CHECK = (do_CHECK,)
+
+ def __cbCheck(self, result, tag):
+ self.sendPositiveResponse(tag, 'CHECK completed')
+
+ def __ebCheck(self, failure, tag):
+ self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
+
+ def checkpoint(self):
+ """Called when the client issues a CHECK command.
+
+ This should perform any checkpoint operations required by the server.
+ It may be a long running operation, but may not block. If it returns
+ a deferred, the client will only be informed of success (or failure)
+ when the deferred's callback (or errback) is invoked.
+ """
+ return None
+
+ def do_CLOSE(self, tag):
+ d = None
+ if self.mbox.isWriteable():
+ d = maybeDeferred(self.mbox.expunge)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ if d is not None:
+ d.addCallback(lambda result: cmbx.close())
+ else:
+ d = maybeDeferred(cmbx.close)
+ if d is not None:
+ d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
+ else:
+ self.__cbClose(None, tag)
+
+ select_CLOSE = (do_CLOSE,)
+
+ def __cbClose(self, result, tag):
+ self.sendPositiveResponse(tag, 'CLOSE completed')
+ self.mbox.removeListener(self)
+ self.mbox = None
+ self.state = 'auth'
+
+ def __ebClose(self, failure, tag):
+ self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
+
+ def do_EXPUNGE(self, tag):
+ if self.mbox.isWriteable():
+ maybeDeferred(self.mbox.expunge).addCallbacks(
+ self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
+ )
+ else:
+ self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
+
+ select_EXPUNGE = (do_EXPUNGE,)
+
+ def __cbExpunge(self, result, tag):
+ for e in result:
+ self.sendUntaggedResponse('%d EXPUNGE' % e)
+ self.sendPositiveResponse(tag, 'EXPUNGE completed')
+
+ def __ebExpunge(self, failure, tag):
+ self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
+ log.err(failure)
+
+ def do_SEARCH(self, tag, charset, query, uid=0):
+ sm = ISearchableMailbox(self.mbox, None)
+ if sm is not None:
+ maybeDeferred(sm.search, query, uid=uid).addCallbacks(
+ self.__cbSearch, self.__ebSearch,
+ (tag, self.mbox, uid), None, (tag,), None
+ )
+ else:
+ # that's not the ideal way to get all messages, there should be a
+ # method on mailboxes that gives you all of them
+ s = parseIdList('1:*')
+ maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
+ self.__cbManualSearch, self.__ebSearch,
+ (tag, self.mbox, query, uid), None, (tag,), None
+ )
+
+ select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
+
+ def __cbSearch(self, result, tag, mbox, uid):
+ if uid:
+ result = map(mbox.getUID, result)
+ ids = ' '.join([str(i) for i in result])
+ self.sendUntaggedResponse('SEARCH ' + ids)
+ self.sendPositiveResponse(tag, 'SEARCH completed')
+
+
+ def __cbManualSearch(self, result, tag, mbox, query, uid,
+ searchResults=None):
+ """
+ Apply the search filter to a set of messages. Send the response to the
+ client.
+
+ @type result: C{list} of C{tuple} of (C{int}, provider of
+ L{imap4.IMessage})
+ @param result: A list two tuples of messages with their sequence ids,
+ sorted by the ids in descending order.
+
+ @type tag: C{str}
+ @param tag: A command tag.
+
+ @type mbox: Provider of L{imap4.IMailbox}
+ @param mbox: The searched mailbox.
+
+ @type query: C{list}
+ @param query: A list representing the parsed form of the search query.
+
+ @param uid: A flag indicating whether the search is over message
+ sequence numbers or UIDs.
+
+ @type searchResults: C{list}
+ @param searchResults: The search results so far or C{None} if no
+ results yet.
+ """
+ if searchResults is None:
+ searchResults = []
+ i = 0
+
+ # result is a list of tuples (sequenceId, Message)
+ lastSequenceId = result and result[-1][0]
+ lastMessageId = result and result[-1][1].getUID()
+
+ for (i, (id, msg)) in zip(range(5), result):
+ # searchFilter and singleSearchStep will mutate the query. Dang.
+ # Copy it here or else things will go poorly for subsequent
+ # messages.
+ if self._searchFilter(copy.deepcopy(query), id, msg,
+ lastSequenceId, lastMessageId):
+ if uid:
+ searchResults.append(str(msg.getUID()))
+ else:
+ searchResults.append(str(id))
+ if i == 4:
+ from twisted.internet import reactor
+ reactor.callLater(
+ 0, self.__cbManualSearch, result[5:], tag, mbox, query, uid,
+ searchResults)
+ else:
+ if searchResults:
+ self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
+ self.sendPositiveResponse(tag, 'SEARCH completed')
+
+
+ def _searchFilter(self, query, id, msg, lastSequenceId, lastMessageId):
+ """
+ Pop search terms from the beginning of C{query} until there are none
+ left and apply them to the given message.
+
+ @param query: A list representing the parsed form of the search query.
+
+ @param id: The sequence number of the message being checked.
+
+ @param msg: The message being checked.
+
+ @type lastSequenceId: C{int}
+ @param lastSequenceId: The highest sequence number of any message in
+ the mailbox being searched.
+
+ @type lastMessageId: C{int}
+ @param lastMessageId: The highest UID of any message in the mailbox
+ being searched.
+
+ @return: Boolean indicating whether all of the query terms match the
+ message.
+ """
+ while query:
+ if not self._singleSearchStep(query, id, msg,
+ lastSequenceId, lastMessageId):
+ return False
+ return True
+
+
+ def _singleSearchStep(self, query, id, msg, lastSequenceId, lastMessageId):
+ """
+ Pop one search term from the beginning of C{query} (possibly more than
+ one element) and return whether it matches the given message.
+
+ @param query: A list representing the parsed form of the search query.
+
+ @param id: The sequence number of the message being checked.
+
+ @param msg: The message being checked.
+
+ @param lastSequenceId: The highest sequence number of any message in
+ the mailbox being searched.
+
+ @param lastMessageId: The highest UID of any message in the mailbox
+ being searched.
+
+ @return: Boolean indicating whether the query term matched the message.
+ """
+
+ q = query.pop(0)
+ if isinstance(q, list):
+ if not self._searchFilter(q, id, msg,
+ lastSequenceId, lastMessageId):
+ return False
+ else:
+ c = q.upper()
+ if not c[:1].isalpha():
+ # A search term may be a word like ALL, ANSWERED, BCC, etc (see
+ # below) or it may be a message sequence set. Here we
+ # recognize a message sequence set "N:M".
+ messageSet = parseIdList(c, lastSequenceId)
+ return id in messageSet
+ else:
+ f = getattr(self, 'search_' + c)
+ if f is not None:
+ if c in self._requiresLastMessageInfo:
+ result = f(query, id, msg, (lastSequenceId,
+ lastMessageId))
+ else:
+ result = f(query, id, msg)
+ if not result:
+ return False
+ return True
+
+ def search_ALL(self, query, id, msg):
+ """
+ Returns C{True} if the message matches the ALL search key (always).
+
+ @type query: A C{list} of C{str}
+ @param query: A list representing the parsed query string.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ return True
+
+ def search_ANSWERED(self, query, id, msg):
+ """
+ Returns C{True} if the message has been answered.
+
+ @type query: A C{list} of C{str}
+ @param query: A list representing the parsed query string.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ return '\\Answered' in msg.getFlags()
+
+ def search_BCC(self, query, id, msg):
+ """
+ Returns C{True} if the message has a BCC address matching the query.
+
+ @type query: A C{list} of C{str}
+ @param query: A list whose first element is a BCC C{str}
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
+ return bcc.lower().find(query.pop(0).lower()) != -1
+
+ def search_BEFORE(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return rfc822.parsedate(msg.getInternalDate()) < date
+
+ def search_BODY(self, query, id, msg):
+ body = query.pop(0).lower()
+ return text.strFile(body, msg.getBodyFile(), False)
+
+ def search_CC(self, query, id, msg):
+ cc = msg.getHeaders(False, 'cc').get('cc', '')
+ return cc.lower().find(query.pop(0).lower()) != -1
+
+ def search_DELETED(self, query, id, msg):
+ return '\\Deleted' in msg.getFlags()
+
+ def search_DRAFT(self, query, id, msg):
+ return '\\Draft' in msg.getFlags()
+
+ def search_FLAGGED(self, query, id, msg):
+ return '\\Flagged' in msg.getFlags()
+
+ def search_FROM(self, query, id, msg):
+ fm = msg.getHeaders(False, 'from').get('from', '')
+ return fm.lower().find(query.pop(0).lower()) != -1
+
+ def search_HEADER(self, query, id, msg):
+ hdr = query.pop(0).lower()
+ hdr = msg.getHeaders(False, hdr).get(hdr, '')
+ return hdr.lower().find(query.pop(0).lower()) != -1
+
+ def search_KEYWORD(self, query, id, msg):
+ query.pop(0)
+ return False
+
+ def search_LARGER(self, query, id, msg):
+ return int(query.pop(0)) < msg.getSize()
+
+ def search_NEW(self, query, id, msg):
+ return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
+
+ def search_NOT(self, query, id, msg, (lastSequenceId, lastMessageId)):
+ """
+ Returns C{True} if the message does not match the query.
+
+ @type query: A C{list} of C{str}
+ @param query: A list representing the parsed form of the search query.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastSequenceId: C{int}
+ @param lastSequenceId: The highest sequence number of a message in the
+ mailbox.
+
+ @type lastMessageId: C{int}
+ @param lastMessageId: The highest UID of a message in the mailbox.
+ """
+ return not self._singleSearchStep(query, id, msg,
+ lastSequenceId, lastMessageId)
+
+ def search_OLD(self, query, id, msg):
+ return '\\Recent' not in msg.getFlags()
+
+ def search_ON(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return rfc822.parsedate(msg.getInternalDate()) == date
+
+ def search_OR(self, query, id, msg, (lastSequenceId, lastMessageId)):
+ """
+ Returns C{True} if the message matches any of the first two query
+ items.
+
+ @type query: A C{list} of C{str}
+ @param query: A list representing the parsed form of the search query.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastSequenceId: C{int}
+ @param lastSequenceId: The highest sequence number of a message in the
+ mailbox.
+
+ @type lastMessageId: C{int}
+ @param lastMessageId: The highest UID of a message in the mailbox.
+ """
+ a = self._singleSearchStep(query, id, msg,
+ lastSequenceId, lastMessageId)
+ b = self._singleSearchStep(query, id, msg,
+ lastSequenceId, lastMessageId)
+ return a or b
+
+ def search_RECENT(self, query, id, msg):
+ return '\\Recent' in msg.getFlags()
+
+ def search_SEEN(self, query, id, msg):
+ return '\\Seen' in msg.getFlags()
+
+ def search_SENTBEFORE(self, query, id, msg):
+ """
+ Returns C{True} if the message date is earlier than the query date.
+
+ @type query: A C{list} of C{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, 'date').get('date', '')
+ date = rfc822.parsedate(date)
+ return date < parseTime(query.pop(0))
+
+ def search_SENTON(self, query, id, msg):
+ """
+ Returns C{True} if the message date is the same as the query date.
+
+ @type query: A C{list} of C{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, 'date').get('date', '')
+ date = rfc822.parsedate(date)
+ return date[:3] == parseTime(query.pop(0))[:3]
+
+ def search_SENTSINCE(self, query, id, msg):
+ """
+ Returns C{True} if the message date is later than the query date.
+
+ @type query: A C{list} of C{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, 'date').get('date', '')
+ date = rfc822.parsedate(date)
+ return date > parseTime(query.pop(0))
+
+ def search_SINCE(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return rfc822.parsedate(msg.getInternalDate()) > date
+
+ def search_SMALLER(self, query, id, msg):
+ return int(query.pop(0)) > msg.getSize()
+
+ def search_SUBJECT(self, query, id, msg):
+ subj = msg.getHeaders(False, 'subject').get('subject', '')
+ return subj.lower().find(query.pop(0).lower()) != -1
+
+ def search_TEXT(self, query, id, msg):
+ # XXX - This must search headers too
+ body = query.pop(0).lower()
+ return text.strFile(body, msg.getBodyFile(), False)
+
+ def search_TO(self, query, id, msg):
+ to = msg.getHeaders(False, 'to').get('to', '')
+ return to.lower().find(query.pop(0).lower()) != -1
+
+ def search_UID(self, query, id, msg, (lastSequenceId, lastMessageId)):
+ """
+ Returns C{True} if the message UID is in the range defined by the
+ search query.
+
+ @type query: A C{list} of C{str}
+ @param query: A list representing the parsed form of the search
+ query. Its first element should be a C{str} that can be interpreted
+ as a sequence range, for example '2:4,5:*'.
+
+ @type id: C{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastSequenceId: C{int}
+ @param lastSequenceId: The highest sequence number of a message in the
+ mailbox.
+
+ @type lastMessageId: C{int}
+ @param lastMessageId: The highest UID of a message in the mailbox.
+ """
+ c = query.pop(0)
+ m = parseIdList(c, lastMessageId)
+ return msg.getUID() in m
+
+ def search_UNANSWERED(self, query, id, msg):
+ return '\\Answered' not in msg.getFlags()
+
+ def search_UNDELETED(self, query, id, msg):
+ return '\\Deleted' not in msg.getFlags()
+
+ def search_UNDRAFT(self, query, id, msg):
+ return '\\Draft' not in msg.getFlags()
+
+ def search_UNFLAGGED(self, query, id, msg):
+ return '\\Flagged' not in msg.getFlags()
+
+ def search_UNKEYWORD(self, query, id, msg):
+ query.pop(0)
+ return False
+
+ def search_UNSEEN(self, query, id, msg):
+ return '\\Seen' not in msg.getFlags()
+
+ def __ebSearch(self, failure, tag):
+ self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
+ log.err(failure)
+
+ def do_FETCH(self, tag, messages, query, uid=0):
+ if query:
+ self._oldTimeout = self.setTimeout(None)
+ maybeDeferred(self.mbox.fetch, messages, uid=uid
+ ).addCallback(iter
+ ).addCallback(self.__cbFetch, tag, query, uid
+ ).addErrback(self.__ebFetch, tag
+ )
+ else:
+ self.sendPositiveResponse(tag, 'FETCH complete')
+
+ select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
+
+ def __cbFetch(self, results, tag, query, uid):
+ if self.blocked is None:
+ self.blocked = []
+ try:
+ id, msg = results.next()
+ except StopIteration:
+ # The idle timeout was suspended while we delivered results,
+ # restore it now.
+ self.setTimeout(self._oldTimeout)
+ del self._oldTimeout
+
+ # All results have been processed, deliver completion notification.
+
+ # It's important to run this *after* resetting the timeout to "rig
+ # a race" in some test code. writing to the transport will
+ # synchronously call test code, which synchronously loses the
+ # connection, calling our connectionLost method, which cancels the
+ # timeout. We want to make sure that timeout is cancelled *after*
+ # we reset it above, so that the final state is no timed
+ # calls. This avoids reactor uncleanliness errors in the test
+ # suite.
+ # XXX: Perhaps loopback should be fixed to not call the user code
+ # synchronously in transport.write?
+ self.sendPositiveResponse(tag, 'FETCH completed')
+
+ # Instance state is now consistent again (ie, it is as though
+ # the fetch command never ran), so allow any pending blocked
+ # commands to execute.
+ self._unblock()
+ else:
+ self.spewMessage(id, msg, query, uid
+ ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
+ ).addErrback(self.__ebSpewMessage
+ )
+
+ def __ebSpewMessage(self, failure):
+ # This indicates a programming error.
+ # There's no reliable way to indicate anything to the client, since we
+ # may have already written an arbitrary amount of data in response to
+ # the command.
+ log.err(failure)
+ self.transport.loseConnection()
+
+ def spew_envelope(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
+
+ def spew_flags(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
+
+ def spew_internaldate(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ idate = msg.getInternalDate()
+ ttup = rfc822.parsedate_tz(idate)
+ if ttup is None:
+ log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
+ raise IMAP4Exception("Internal failure generating INTERNALDATE")
+
+ # need to specify the month manually, as strftime depends on locale
+ strdate = time.strftime("%d-%%s-%Y %H:%M:%S ", ttup[:9])
+ odate = strdate % (_MONTH_NAMES[ttup[1]],)
+ if ttup[9] is None:
+ odate = odate + "+0000"
+ else:
+ if ttup[9] >= 0:
+ sign = "+"
+ else:
+ sign = "-"
+ odate = odate + sign + str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)).zfill(4)
+ _w('INTERNALDATE ' + _quote(odate))
+
+ def spew_rfc822header(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ hdrs = _formatHeaders(msg.getHeaders(True))
+ _w('RFC822.HEADER ' + _literal(hdrs))
+
+ def spew_rfc822text(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('RFC822.TEXT ')
+ _f()
+ return FileProducer(msg.getBodyFile()
+ ).beginProducing(self.transport
+ )
+
+ def spew_rfc822size(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('RFC822.SIZE ' + str(msg.getSize()))
+
+ def spew_rfc822(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('RFC822 ')
+ _f()
+ mf = IMessageFile(msg, None)
+ if mf is not None:
+ return FileProducer(mf.open()
+ ).beginProducing(self.transport
+ )
+ return MessageProducer(msg, None, self._scheduler
+ ).beginProducing(self.transport
+ )
+
+ def spew_uid(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w('UID ' + str(msg.getUID()))
+
+ def spew_bodystructure(self, id, msg, _w=None, _f=None):
+ _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
+
+ def spew_body(self, part, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ for p in part.part:
+ if msg.isMultipart():
+ msg = msg.getSubPart(p)
+ elif p > 0:
+ # Non-multipart messages have an implicit first part but no
+ # other parts - reject any request for any other part.
+ raise TypeError("Requested subpart of non-multipart message")
+
+ if part.header:
+ hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
+ hdrs = _formatHeaders(hdrs)
+ _w(str(part) + ' ' + _literal(hdrs))
+ elif part.text:
+ _w(str(part) + ' ')
+ _f()
+ return FileProducer(msg.getBodyFile()
+ ).beginProducing(self.transport
+ )
+ elif part.mime:
+ hdrs = _formatHeaders(msg.getHeaders(True))
+ _w(str(part) + ' ' + _literal(hdrs))
+ elif part.empty:
+ _w(str(part) + ' ')
+ _f()
+ if part.part:
+ return FileProducer(msg.getBodyFile()
+ ).beginProducing(self.transport
+ )
+ else:
+ mf = IMessageFile(msg, None)
+ if mf is not None:
+ return FileProducer(mf.open()).beginProducing(self.transport)
+ return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
+
+ else:
+ _w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
+
+ def spewMessage(self, id, msg, query, uid):
+ wbuf = WriteBuffer(self.transport)
+ write = wbuf.write
+ flush = wbuf.flush
+ def start():
+ write('* %d FETCH (' % (id,))
+ def finish():
+ write(')\r\n')
+ def space():
+ write(' ')
+
+ def spew():
+ seenUID = False
+ start()
+ for part in query:
+ if part.type == 'uid':
+ seenUID = True
+ if part.type == 'body':
+ yield self.spew_body(part, id, msg, write, flush)
+ else:
+ f = getattr(self, 'spew_' + part.type)
+ yield f(id, msg, write, flush)
+ if part is not query[-1]:
+ space()
+ if uid and not seenUID:
+ space()
+ yield self.spew_uid(id, msg, write, flush)
+ finish()
+ flush()
+ return self._scheduler(spew())
+
+ def __ebFetch(self, failure, tag):
+ self.setTimeout(self._oldTimeout)
+ del self._oldTimeout
+ log.err(failure)
+ self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
+
+ def do_STORE(self, tag, messages, mode, flags, uid=0):
+ mode = mode.upper()
+ silent = mode.endswith('SILENT')
+ if mode.startswith('+'):
+ mode = 1
+ elif mode.startswith('-'):
+ mode = -1
+ else:
+ mode = 0
+
+ maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
+ self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
+ )
+
+ select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
+
+ def __cbStore(self, result, tag, mbox, uid, silent):
+ if result and not silent:
+ for (k, v) in result.iteritems():
+ if uid:
+ uidstr = ' UID %d' % mbox.getUID(k)
+ else:
+ uidstr = ''
+ self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
+ (k, ' '.join(v), uidstr))
+ self.sendPositiveResponse(tag, 'STORE completed')
+
+ def __ebStore(self, failure, tag):
+ self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+
+ def do_COPY(self, tag, messages, mailbox, uid=0):
+ mailbox = self._parseMbox(mailbox)
+ maybeDeferred(self.account.select, mailbox
+ ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
+ ).addErrback(self._ebCopySelectedMailbox, tag
+ )
+ select_COPY = (do_COPY, arg_seqset, arg_astring)
+
+ def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
+ if not mbox:
+ self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
+ else:
+ maybeDeferred(self.mbox.fetch, messages, uid
+ ).addCallback(self.__cbCopy, tag, mbox
+ ).addCallback(self.__cbCopied, tag, mbox
+ ).addErrback(self.__ebCopy, tag
+ )
+
+ def _ebCopySelectedMailbox(self, failure, tag):
+ self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
+
+ def __cbCopy(self, messages, tag, mbox):
+ # XXX - This should handle failures with a rollback or something
+ addedDeferreds = []
+ addedIDs = []
+ failures = []
+
+ fastCopyMbox = IMessageCopier(mbox, None)
+ for (id, msg) in messages:
+ if fastCopyMbox is not None:
+ d = maybeDeferred(fastCopyMbox.copy, msg)
+ addedDeferreds.append(d)
+ continue
+
+ # XXX - The following should be an implementation of IMessageCopier.copy
+ # on an IMailbox->IMessageCopier adapter.
+
+ flags = msg.getFlags()
+ date = msg.getInternalDate()
+
+ body = IMessageFile(msg, None)
+ if body is not None:
+ bodyFile = body.open()
+ d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
+ else:
+ def rewind(f):
+ f.seek(0)
+ return f
+ buffer = tempfile.TemporaryFile()
+ d = MessageProducer(msg, buffer, self._scheduler
+ ).beginProducing(None
+ ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
+ )
+ addedDeferreds.append(d)
+ return defer.DeferredList(addedDeferreds)
+
+ def __cbCopied(self, deferredIds, tag, mbox):
+ ids = []
+ failures = []
+ for (status, result) in deferredIds:
+ if status:
+ ids.append(result)
+ else:
+ failures.append(result.value)
+ if failures:
+ self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
+ else:
+ self.sendPositiveResponse(tag, 'COPY completed')
+
+ def __ebCopy(self, failure, tag):
+ self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
+ log.err(failure)
+
+ def do_UID(self, tag, command, line):
+ command = command.upper()
+
+ if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
+ raise IllegalClientResponse(command)
+
+ self.dispatchCommand(tag, command, line, uid=1)
+
+ select_UID = (do_UID, arg_atom, arg_line)
+ #
+ # IMailboxListener implementation
+ #
+ def modeChanged(self, writeable):
+ if writeable:
+ self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
+ else:
+ self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
+
+ def flagsChanged(self, newFlags):
+ for (mId, flags) in newFlags.iteritems():
+ msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
+ self.sendUntaggedResponse(msg, async=True)
+
+ def newMessages(self, exists, recent):
+ if exists is not None:
+ self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
+ if recent is not None:
+ self.sendUntaggedResponse('%d RECENT' % recent, async=True)
+
+
+class UnhandledResponse(IMAP4Exception): pass
+
+class NegativeResponse(IMAP4Exception): pass
+
+class NoSupportedAuthentication(IMAP4Exception):
+ def __init__(self, serverSupports, clientSupports):
+ IMAP4Exception.__init__(self, 'No supported authentication schemes available')
+ self.serverSupports = serverSupports
+ self.clientSupports = clientSupports
+
+ def __str__(self):
+ return (IMAP4Exception.__str__(self)
+ + ': Server supports %r, client supports %r'
+ % (self.serverSupports, self.clientSupports))
+
+class IllegalServerResponse(IMAP4Exception): pass
+
+TIMEOUT_ERROR = error.TimeoutError()
+
+class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
+ """IMAP4 client protocol implementation
+
+ @ivar state: A string representing the state the connection is currently
+ in.
+ """
+ implements(IMailboxListener)
+
+ tags = None
+ waiting = None
+ queued = None
+ tagID = 1
+ state = None
+
+ startedTLS = False
+
+ # Number of seconds to wait before timing out a connection.
+ # If the number is <= 0 no timeout checking will be performed.
+ timeout = 0
+
+ # Capabilities are not allowed to change during the session
+ # So cache the first response and use that for all later
+ # lookups
+ _capCache = None
+
+ _memoryFileLimit = 1024 * 1024 * 10
+
+ # Authentication is pluggable. This maps names to IClientAuthentication
+ # objects.
+ authenticators = None
+
+ STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
+
+ STATUS_TRANSFORMATIONS = {
+ 'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
+ }
+
+ context = None
+
+ def __init__(self, contextFactory = None):
+ self.tags = {}
+ self.queued = []
+ self.authenticators = {}
+ self.context = contextFactory
+
+ self._tag = None
+ self._parts = None
+ self._lastCmd = None
+
+ def registerAuthenticator(self, auth):
+ """Register a new form of authentication
+
+ When invoking the authenticate() method of IMAP4Client, the first
+ matching authentication scheme found will be used. The ordering is
+ that in which the server lists support authentication schemes.
+
+ @type auth: Implementor of C{IClientAuthentication}
+ @param auth: The object to use to perform the client
+ side of this authentication scheme.
+ """
+ self.authenticators[auth.getName().upper()] = auth
+
+ def rawDataReceived(self, data):
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ self._pendingSize -= len(data)
+ if self._pendingSize > 0:
+ self._pendingBuffer.write(data)
+ else:
+ passon = ''
+ if self._pendingSize < 0:
+ data, passon = data[:self._pendingSize], data[self._pendingSize:]
+ self._pendingBuffer.write(data)
+ rest = self._pendingBuffer
+ self._pendingBuffer = None
+ self._pendingSize = None
+ rest.seek(0, 0)
+ self._parts.append(rest.read())
+ self.setLineMode(passon.lstrip('\r\n'))
+
+# def sendLine(self, line):
+# print 'S:', repr(line)
+# return basic.LineReceiver.sendLine(self, line)
+
+ def _setupForLiteral(self, rest, octets):
+ self._pendingBuffer = self.messageFile(octets)
+ self._pendingSize = octets
+ if self._parts is None:
+ self._parts = [rest, '\r\n']
+ else:
+ self._parts.extend([rest, '\r\n'])
+ self.setRawMode()
+
+ def connectionMade(self):
+ if self.timeout > 0:
+ self.setTimeout(self.timeout)
+
+ def connectionLost(self, reason):
+ """We are no longer connected"""
+ if self.timeout > 0:
+ self.setTimeout(None)
+ if self.queued is not None:
+ queued = self.queued
+ self.queued = None
+ for cmd in queued:
+ cmd.defer.errback(reason)
+ if self.tags is not None:
+ tags = self.tags
+ self.tags = None
+ for cmd in tags.itervalues():
+ if cmd is not None and cmd.defer is not None:
+ cmd.defer.errback(reason)
+
+
+ def lineReceived(self, line):
+ """
+ Attempt to parse a single line from the server.
+
+ @type line: C{str}
+ @param line: The line from the server, without the line delimiter.
+
+ @raise IllegalServerResponse: If the line or some part of the line
+ does not represent an allowed message from the server at this time.
+ """
+# print 'C: ' + repr(line)
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ lastPart = line.rfind('{')
+ if lastPart != -1:
+ lastPart = line[lastPart + 1:]
+ if lastPart.endswith('}'):
+ # It's a literal a-comin' in
+ try:
+ octets = int(lastPart[:-1])
+ except ValueError:
+ raise IllegalServerResponse(line)
+ if self._parts is None:
+ self._tag, parts = line.split(None, 1)
+ else:
+ parts = line
+ self._setupForLiteral(parts, octets)
+ return
+
+ if self._parts is None:
+ # It isn't a literal at all
+ self._regularDispatch(line)
+ else:
+ # If an expression is in progress, no tag is required here
+ # Since we didn't find a literal indicator, this expression
+ # is done.
+ self._parts.append(line)
+ tag, rest = self._tag, ''.join(self._parts)
+ self._tag = self._parts = None
+ self.dispatchCommand(tag, rest)
+
+ def timeoutConnection(self):
+ if self._lastCmd and self._lastCmd.defer is not None:
+ d, self._lastCmd.defer = self._lastCmd.defer, None
+ d.errback(TIMEOUT_ERROR)
+
+ if self.queued:
+ for cmd in self.queued:
+ if cmd.defer is not None:
+ d, cmd.defer = cmd.defer, d
+ d.errback(TIMEOUT_ERROR)
+
+ self.transport.loseConnection()
+
+ def _regularDispatch(self, line):
+ parts = line.split(None, 1)
+ if len(parts) != 2:
+ parts.append('')
+ tag, rest = parts
+ self.dispatchCommand(tag, rest)
+
+ def messageFile(self, octets):
+ """Create a file to which an incoming message may be written.
+
+ @type octets: C{int}
+ @param octets: The number of octets which will be written to the file
+
+ @rtype: Any object which implements C{write(string)} and
+ C{seek(int, int)}
+ @return: A file-like object
+ """
+ if octets > self._memoryFileLimit:
+ return tempfile.TemporaryFile()
+ else:
+ return StringIO.StringIO()
+
+ def makeTag(self):
+ tag = '%0.4X' % self.tagID
+ self.tagID += 1
+ return tag
+
+ def dispatchCommand(self, tag, rest):
+ if self.state is None:
+ f = self.response_UNAUTH
+ else:
+ f = getattr(self, 'response_' + self.state.upper(), None)
+ if f:
+ try:
+ f(tag, rest)
+ except:
+ log.err()
+ self.transport.loseConnection()
+ else:
+ log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
+ self.transport.loseConnection()
+
+ def response_UNAUTH(self, tag, rest):
+ if self.state is None:
+ # Server greeting, this is
+ status, rest = rest.split(None, 1)
+ if status.upper() == 'OK':
+ self.state = 'unauth'
+ elif status.upper() == 'PREAUTH':
+ self.state = 'auth'
+ else:
+ # XXX - This is rude.
+ self.transport.loseConnection()
+ raise IllegalServerResponse(tag + ' ' + rest)
+
+ b, e = rest.find('['), rest.find(']')
+ if b != -1 and e != -1:
+ self.serverGreeting(
+ self.__cbCapabilities(
+ ([parseNestedParens(rest[b + 1:e])], None)))
+ else:
+ self.serverGreeting(None)
+ else:
+ self._defaultHandler(tag, rest)
+
+ def response_AUTH(self, tag, rest):
+ self._defaultHandler(tag, rest)
+
+ def _defaultHandler(self, tag, rest):
+ if tag == '*' or tag == '+':
+ if not self.waiting:
+ self._extraInfo([parseNestedParens(rest)])
+ else:
+ cmd = self.tags[self.waiting]
+ if tag == '+':
+ cmd.continuation(rest)
+ else:
+ cmd.lines.append(rest)
+ else:
+ try:
+ cmd = self.tags[tag]
+ except KeyError:
+ # XXX - This is rude.
+ self.transport.loseConnection()
+ raise IllegalServerResponse(tag + ' ' + rest)
+ else:
+ status, line = rest.split(None, 1)
+ if status == 'OK':
+ # Give them this last line, too
+ cmd.finish(rest, self._extraInfo)
+ else:
+ cmd.defer.errback(IMAP4Exception(line))
+ del self.tags[tag]
+ self.waiting = None
+ self._flushQueue()
+
+ def _flushQueue(self):
+ if self.queued:
+ cmd = self.queued.pop(0)
+ t = self.makeTag()
+ self.tags[t] = cmd
+ self.sendLine(cmd.format(t))
+ self.waiting = t
+
+ def _extraInfo(self, lines):
+ # XXX - This is terrible.
+ # XXX - Also, this should collapse temporally proximate calls into single
+ # invocations of IMailboxListener methods, where possible.
+ flags = {}
+ recent = exists = None
+ for response in lines:
+ elements = len(response)
+ if elements == 1 and response[0] == ['READ-ONLY']:
+ self.modeChanged(False)
+ elif elements == 1 and response[0] == ['READ-WRITE']:
+ self.modeChanged(True)
+ elif elements == 2 and response[1] == 'EXISTS':
+ exists = int(response[0])
+ elif elements == 2 and response[1] == 'RECENT':
+ recent = int(response[0])
+ elif elements == 3 and response[1] == 'FETCH':
+ mId = int(response[0])
+ values = self._parseFetchPairs(response[2])
+ flags.setdefault(mId, []).extend(values.get('FLAGS', ()))
+ else:
+ log.msg('Unhandled unsolicited response: %s' % (response,))
+
+ if flags:
+ self.flagsChanged(flags)
+ if recent is not None or exists is not None:
+ self.newMessages(exists, recent)
+
+ def sendCommand(self, cmd):
+ cmd.defer = defer.Deferred()
+ if self.waiting:
+ self.queued.append(cmd)
+ return cmd.defer
+ t = self.makeTag()
+ self.tags[t] = cmd
+ self.sendLine(cmd.format(t))
+ self.waiting = t
+ self._lastCmd = cmd
+ return cmd.defer
+
+ def getCapabilities(self, useCache=1):
+ """Request the capabilities available on this server.
+
+ This command is allowed in any state of connection.
+
+ @type useCache: C{bool}
+ @param useCache: Specify whether to use the capability-cache or to
+ re-retrieve the capabilities from the server. Server capabilities
+ should never change, so for normal use, this flag should never be
+ false.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback will be invoked with a
+ dictionary mapping capability types to lists of supported
+ mechanisms, or to None if a support list is not applicable.
+ """
+ if useCache and self._capCache is not None:
+ return defer.succeed(self._capCache)
+ cmd = 'CAPABILITY'
+ resp = ('CAPABILITY',)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbCapabilities)
+ return d
+
+ def __cbCapabilities(self, (lines, tagline)):
+ caps = {}
+ for rest in lines:
+ for cap in rest[1:]:
+ parts = cap.split('=', 1)
+ if len(parts) == 1:
+ category, value = parts[0], None
+ else:
+ category, value = parts
+ caps.setdefault(category, []).append(value)
+
+ # Preserve a non-ideal API for backwards compatibility. It would
+ # probably be entirely sensible to have an object with a wider API than
+ # dict here so this could be presented less insanely.
+ for category in caps:
+ if caps[category] == [None]:
+ caps[category] = None
+ self._capCache = caps
+ return caps
+
+ def logout(self):
+ """Inform the server that we are done with the connection.
+
+ This command is allowed in any state of connection.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback will be invoked with None
+ when the proper server acknowledgement has been received.
+ """
+ d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
+ d.addCallback(self.__cbLogout)
+ return d
+
+ def __cbLogout(self, (lines, tagline)):
+ self.transport.loseConnection()
+ # We don't particularly care what the server said
+ return None
+
+
+ def noop(self):
+ """Perform no operation.
+
+ This command is allowed in any state of connection.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback will be invoked with a list
+ of untagged status updates the server responds with.
+ """
+ d = self.sendCommand(Command('NOOP'))
+ d.addCallback(self.__cbNoop)
+ return d
+
+ def __cbNoop(self, (lines, tagline)):
+ # Conceivable, this is elidable.
+ # It is, afterall, a no-op.
+ return lines
+
+ def startTLS(self, contextFactory=None):
+ """
+ Initiates a 'STARTTLS' request and negotiates the TLS / SSL
+ Handshake.
+
+ @param contextFactory: The TLS / SSL Context Factory to
+ leverage. If the contextFactory is None the IMAP4Client will
+ either use the current TLS / SSL Context Factory or attempt to
+ create a new one.
+
+ @type contextFactory: C{ssl.ClientContextFactory}
+
+ @return: A Deferred which fires when the transport has been
+ secured according to the given contextFactory, or which fails
+ if the transport cannot be secured.
+ """
+ assert not self.startedTLS, "Client and Server are currently communicating via TLS"
+
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ if contextFactory is None:
+ return defer.fail(IMAP4Exception(
+ "IMAP4Client requires a TLS context to "
+ "initiate the STARTTLS handshake"))
+
+ if 'STARTTLS' not in self._capCache:
+ return defer.fail(IMAP4Exception(
+ "Server does not support secure communication "
+ "via TLS / SSL"))
+
+ tls = interfaces.ITLSTransport(self.transport, None)
+ if tls is None:
+ return defer.fail(IMAP4Exception(
+ "IMAP4Client transport does not implement "
+ "interfaces.ITLSTransport"))
+
+ d = self.sendCommand(Command('STARTTLS'))
+ d.addCallback(self._startedTLS, contextFactory)
+ d.addCallback(lambda _: self.getCapabilities())
+ return d
+
+
+ def authenticate(self, secret):
+ """Attempt to enter the authenticated state with the server
+
+ This command is allowed in the Non-Authenticated state.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the authentication
+ succeeds and whose errback will be invoked otherwise.
+ """
+ if self._capCache is None:
+ d = self.getCapabilities()
+ else:
+ d = defer.succeed(self._capCache)
+ d.addCallback(self.__cbAuthenticate, secret)
+ return d
+
+ def __cbAuthenticate(self, caps, secret):
+ auths = caps.get('AUTH', ())
+ for scheme in auths:
+ if scheme.upper() in self.authenticators:
+ cmd = Command('AUTHENTICATE', scheme, (),
+ self.__cbContinueAuth, scheme,
+ secret)
+ return self.sendCommand(cmd)
+
+ if self.startedTLS:
+ return defer.fail(NoSupportedAuthentication(
+ auths, self.authenticators.keys()))
+ else:
+ def ebStartTLS(err):
+ err.trap(IMAP4Exception)
+ # We couldn't negotiate TLS for some reason
+ return defer.fail(NoSupportedAuthentication(
+ auths, self.authenticators.keys()))
+
+ d = self.startTLS()
+ d.addErrback(ebStartTLS)
+ d.addCallback(lambda _: self.getCapabilities())
+ d.addCallback(self.__cbAuthTLS, secret)
+ return d
+
+
+ def __cbContinueAuth(self, rest, scheme, secret):
+ try:
+ chal = base64.decodestring(rest + '\n')
+ except binascii.Error:
+ self.sendLine('*')
+ raise IllegalServerResponse(rest)
+ self.transport.loseConnection()
+ else:
+ auth = self.authenticators[scheme]
+ chal = auth.challengeResponse(secret, chal)
+ self.sendLine(base64.encodestring(chal).strip())
+
+ def __cbAuthTLS(self, caps, secret):
+ auths = caps.get('AUTH', ())
+ for scheme in auths:
+ if scheme.upper() in self.authenticators:
+ cmd = Command('AUTHENTICATE', scheme, (),
+ self.__cbContinueAuth, scheme,
+ secret)
+ return self.sendCommand(cmd)
+ raise NoSupportedAuthentication(auths, self.authenticators.keys())
+
+
+ def login(self, username, password):
+ """Authenticate with the server using a username and password
+
+ This command is allowed in the Non-Authenticated state. If the
+ server supports the STARTTLS capability and our transport supports
+ TLS, TLS is negotiated before the login command is issued.
+
+ A more secure way to log in is to use C{startTLS} or
+ C{authenticate} or both.
+
+ @type username: C{str}
+ @param username: The username to log in with
+
+ @type password: C{str}
+ @param password: The password to log in with
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if login is successful
+ and whose errback is invoked otherwise.
+ """
+ d = maybeDeferred(self.getCapabilities)
+ d.addCallback(self.__cbLoginCaps, username, password)
+ return d
+
+ def serverGreeting(self, caps):
+ """Called when the server has sent us a greeting.
+
+ @type caps: C{dict}
+ @param caps: Capabilities the server advertised in its greeting.
+ """
+
+ def _getContextFactory(self):
+ if self.context is not None:
+ return self.context
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ return None
+ else:
+ context = ssl.ClientContextFactory()
+ context.method = ssl.SSL.TLSv1_METHOD
+ return context
+
+ def __cbLoginCaps(self, capabilities, username, password):
+ # If the server advertises STARTTLS, we might want to try to switch to TLS
+ tryTLS = 'STARTTLS' in capabilities
+
+ # If our transport supports switching to TLS, we might want to try to switch to TLS.
+ tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
+
+ # If our transport is not already using TLS, we might want to try to switch to TLS.
+ nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
+
+ if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
+ d = self.startTLS()
+
+ d.addCallbacks(
+ self.__cbLoginTLS,
+ self.__ebLoginTLS,
+ callbackArgs=(username, password),
+ )
+ return d
+ else:
+ if nontlsTransport:
+ log.msg("Server has no TLS support. logging in over cleartext!")
+ args = ' '.join((_quote(username), _quote(password)))
+ return self.sendCommand(Command('LOGIN', args))
+
+ def _startedTLS(self, result, context):
+ self.transport.startTLS(context)
+ self._capCache = None
+ self.startedTLS = True
+ return result
+
+ def __cbLoginTLS(self, result, username, password):
+ args = ' '.join((_quote(username), _quote(password)))
+ return self.sendCommand(Command('LOGIN', args))
+
+ def __ebLoginTLS(self, failure):
+ log.err(failure)
+ return failure
+
+ def namespace(self):
+ """Retrieve information about the namespaces available to this account
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with namespace
+ information. An example of this information is::
+
+ [[['', '/']], [], []]
+
+ which indicates a single personal namespace called '' with '/'
+ as its hierarchical delimiter, and no shared or user namespaces.
+ """
+ cmd = 'NAMESPACE'
+ resp = ('NAMESPACE',)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbNamespace)
+ return d
+
+ def __cbNamespace(self, (lines, last)):
+ for parts in lines:
+ if len(parts) == 4 and parts[0] == 'NAMESPACE':
+ return [e or [] for e in parts[1:]]
+ log.err("No NAMESPACE response to NAMESPACE command")
+ return [[], [], []]
+
+
+ def select(self, mailbox):
+ """
+ Select a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: C{str}
+ @param mailbox: The name of the mailbox to select
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with mailbox
+ information if the select is successful and whose errback is
+ invoked otherwise. Mailbox information consists of a dictionary
+ with the following keys and values::
+
+ FLAGS: A list of strings containing the flags settable on
+ messages in this mailbox.
+
+ EXISTS: An integer indicating the number of messages in this
+ mailbox.
+
+ RECENT: An integer indicating the number of "recent"
+ messages in this mailbox.
+
+ UNSEEN: The message sequence number (an integer) of the
+ first unseen message in the mailbox.
+
+ PERMANENTFLAGS: A list of strings containing the flags that
+ can be permanently set on messages in this mailbox.
+
+ UIDVALIDITY: An integer uniquely identifying this mailbox.
+ """
+ cmd = 'SELECT'
+ args = _prepareMailboxName(mailbox)
+ resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbSelect, 1)
+ return d
+
+
+ def examine(self, mailbox):
+ """Select a mailbox in read-only mode
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: C{str}
+ @param mailbox: The name of the mailbox to examine
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with mailbox
+ information if the examine is successful and whose errback
+ is invoked otherwise. Mailbox information consists of a dictionary
+ with the following keys and values::
+
+ 'FLAGS': A list of strings containing the flags settable on
+ messages in this mailbox.
+
+ 'EXISTS': An integer indicating the number of messages in this
+ mailbox.
+
+ 'RECENT': An integer indicating the number of \"recent\"
+ messages in this mailbox.
+
+ 'UNSEEN': An integer indicating the number of messages not
+ flagged \\Seen in this mailbox.
+
+ 'PERMANENTFLAGS': A list of strings containing the flags that
+ can be permanently set on messages in this mailbox.
+
+ 'UIDVALIDITY': An integer uniquely identifying this mailbox.
+ """
+ cmd = 'EXAMINE'
+ args = _prepareMailboxName(mailbox)
+ resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbSelect, 0)
+ return d
+
+
+ def _intOrRaise(self, value, phrase):
+ """
+ Parse C{value} as an integer and return the result or raise
+ L{IllegalServerResponse} with C{phrase} as an argument if C{value}
+ cannot be parsed as an integer.
+ """
+ try:
+ return int(value)
+ except ValueError:
+ raise IllegalServerResponse(phrase)
+
+
+ def __cbSelect(self, (lines, tagline), rw):
+ """
+ Handle lines received in response to a SELECT or EXAMINE command.
+
+ See RFC 3501, section 6.3.1.
+ """
+ # In the absense of specification, we are free to assume:
+ # READ-WRITE access
+ datum = {'READ-WRITE': rw}
+ lines.append(parseNestedParens(tagline))
+ for split in lines:
+ if len(split) > 0 and split[0].upper() == 'OK':
+ # Handle all the kinds of OK response.
+ content = split[1]
+ key = content[0].upper()
+ if key == 'READ-ONLY':
+ datum['READ-WRITE'] = False
+ elif key == 'READ-WRITE':
+ datum['READ-WRITE'] = True
+ elif key == 'UIDVALIDITY':
+ datum['UIDVALIDITY'] = self._intOrRaise(
+ content[1], split)
+ elif key == 'UNSEEN':
+ datum['UNSEEN'] = self._intOrRaise(content[1], split)
+ elif key == 'UIDNEXT':
+ datum['UIDNEXT'] = self._intOrRaise(content[1], split)
+ elif key == 'PERMANENTFLAGS':
+ datum['PERMANENTFLAGS'] = tuple(content[1])
+ else:
+ log.err('Unhandled SELECT response (2): %s' % (split,))
+ elif len(split) == 2:
+ # Handle FLAGS, EXISTS, and RECENT
+ if split[0].upper() == 'FLAGS':
+ datum['FLAGS'] = tuple(split[1])
+ elif isinstance(split[1], str):
+ # Must make sure things are strings before treating them as
+ # strings since some other forms of response have nesting in
+ # places which results in lists instead.
+ if split[1].upper() == 'EXISTS':
+ datum['EXISTS'] = self._intOrRaise(split[0], split)
+ elif split[1].upper() == 'RECENT':
+ datum['RECENT'] = self._intOrRaise(split[0], split)
+ else:
+ log.err('Unhandled SELECT response (0): %s' % (split,))
+ else:
+ log.err('Unhandled SELECT response (1): %s' % (split,))
+ else:
+ log.err('Unhandled SELECT response (4): %s' % (split,))
+ return datum
+
+
+ def create(self, name):
+ """Create a new mailbox on the server
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: C{str}
+ @param name: The name of the mailbox to create.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the mailbox creation
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
+
+ def delete(self, name):
+ """Delete a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: C{str}
+ @param name: The name of the mailbox to delete.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose calblack is invoked if the mailbox is
+ deleted successfully and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
+
+ def rename(self, oldname, newname):
+ """Rename a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type oldname: C{str}
+ @param oldname: The current name of the mailbox to rename.
+
+ @type newname: C{str}
+ @param newname: The new name to give the mailbox.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the rename is
+ successful and whose errback is invoked otherwise.
+ """
+ oldname = _prepareMailboxName(oldname)
+ newname = _prepareMailboxName(newname)
+ return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
+
+ def subscribe(self, name):
+ """Add a mailbox to the subscription list
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: C{str}
+ @param name: The mailbox to mark as 'active' or 'subscribed'
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the subscription
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
+
+ def unsubscribe(self, name):
+ """Remove a mailbox from the subscription list
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: C{str}
+ @param name: The mailbox to unsubscribe
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the unsubscription
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
+
+ def list(self, reference, wildcard):
+ """List a subset of the available mailboxes
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type reference: C{str}
+ @param reference: The context in which to interpret C{wildcard}
+
+ @type wildcard: C{str}
+ @param wildcard: The pattern of mailbox names to match, optionally
+ including either or both of the '*' and '%' wildcards. '*' will
+ match zero or more characters and cross hierarchical boundaries.
+ '%' will also match zero or more characters, but is limited to a
+ single hierarchical level.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a list of C{tuple}s,
+ the first element of which is a C{tuple} of mailbox flags, the second
+ element of which is the hierarchy delimiter for this mailbox, and the
+ third of which is the mailbox name; if the command is unsuccessful,
+ the deferred's errback is invoked instead.
+ """
+ cmd = 'LIST'
+ args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
+ resp = ('LIST',)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbList, 'LIST')
+ return d
+
+ def lsub(self, reference, wildcard):
+ """List a subset of the subscribed available mailboxes
+
+ This command is allowed in the Authenticated and Selected states.
+
+ The parameters and returned object are the same as for the C{list}
+ method, with one slight difference: Only mailboxes which have been
+ subscribed can be included in the resulting list.
+ """
+ cmd = 'LSUB'
+ args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
+ resp = ('LSUB',)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbList, 'LSUB')
+ return d
+
+ def __cbList(self, (lines, last), command):
+ results = []
+ for parts in lines:
+ if len(parts) == 4 and parts[0] == command:
+ parts[1] = tuple(parts[1])
+ results.append(tuple(parts[1:]))
+ return results
+
+ def status(self, mailbox, *names):
+ """
+ Retrieve the status of the given mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: C{str}
+ @param mailbox: The name of the mailbox to query
+
+ @type *names: C{str}
+ @param *names: The status names to query. These may be any number of:
+ C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and
+ C{'UNSEEN'}.
+
+ @rtype: C{Deferred}
+ @return: A deferred which fires with with the status information if the
+ command is successful and whose errback is invoked otherwise. The
+ status information is in the form of a C{dict}. Each element of
+ C{names} is a key in the dictionary. The value for each key is the
+ corresponding response from the server.
+ """
+ cmd = 'STATUS'
+ args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
+ resp = ('STATUS',)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbStatus)
+ return d
+
+ def __cbStatus(self, (lines, last)):
+ status = {}
+ for parts in lines:
+ if parts[0] == 'STATUS':
+ items = parts[2]
+ items = [items[i:i+2] for i in range(0, len(items), 2)]
+ status.update(dict(items))
+ for k in status.keys():
+ t = self.STATUS_TRANSFORMATIONS.get(k)
+ if t:
+ try:
+ status[k] = t(status[k])
+ except Exception, e:
+ raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
+ return status
+
+ def append(self, mailbox, message, flags = (), date = None):
+ """Add the given message to the given mailbox.
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: C{str}
+ @param mailbox: The mailbox to which to add this message.
+
+ @type message: Any file-like object
+ @param message: The message to add, in RFC822 format. Newlines
+ in this file should be \\r\\n-style.
+
+ @type flags: Any iterable of C{str}
+ @param flags: The flags to associated with this message.
+
+ @type date: C{str}
+ @param date: The date to associate with this message. This should
+ be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in
+ Eastern Standard Time, on July 1st 2004 at half past 1 PM,
+ \"01-07-2004 13:30:00 -0500\".
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked when this command
+ succeeds or whose errback is invoked if it fails.
+ """
+ message.seek(0, 2)
+ L = message.tell()
+ message.seek(0, 0)
+ fmt = '%s (%s)%s {%d}'
+ if date:
+ date = ' "%s"' % date
+ else:
+ date = ''
+ cmd = fmt % (
+ _prepareMailboxName(mailbox), ' '.join(flags),
+ date, L
+ )
+ d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
+ return d
+
+ def __cbContinueAppend(self, lines, message):
+ s = basic.FileSender()
+ return s.beginFileTransfer(message, self.transport, None
+ ).addCallback(self.__cbFinishAppend)
+
+ def __cbFinishAppend(self, foo):
+ self.sendLine('')
+
+ def check(self):
+ """Tell the server to perform a checkpoint
+
+ This command is allowed in the Selected state.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked when this command
+ succeeds or whose errback is invoked if it fails.
+ """
+ return self.sendCommand(Command('CHECK'))
+
+ def close(self):
+ """Return the connection to the Authenticated state.
+
+ This command is allowed in the Selected state.
+
+ Issuing this command will also remove all messages flagged \\Deleted
+ from the selected mailbox if it is opened in read-write mode,
+ otherwise it indicates success by no messages are removed.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked when the command
+ completes successfully or whose errback is invoked if it fails.
+ """
+ return self.sendCommand(Command('CLOSE'))
+
+
+ def expunge(self):
+ """Return the connection to the Authenticate state.
+
+ This command is allowed in the Selected state.
+
+ Issuing this command will perform the same actions as issuing the
+ close command, but will also generate an 'expunge' response for
+ every message deleted.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ 'expunge' responses when this command is successful or whose errback
+ is invoked otherwise.
+ """
+ cmd = 'EXPUNGE'
+ resp = ('EXPUNGE',)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbExpunge)
+ return d
+
+
+ def __cbExpunge(self, (lines, last)):
+ ids = []
+ for parts in lines:
+ if len(parts) == 2 and parts[1] == 'EXPUNGE':
+ ids.append(self._intOrRaise(parts[0], parts))
+ return ids
+
+
+ def search(self, *queries, **kwarg):
+ """Search messages in the currently selected mailbox
+
+ This command is allowed in the Selected state.
+
+ Any non-zero number of queries are accepted by this method, as
+ returned by the C{Query}, C{Or}, and C{Not} functions.
+
+ One keyword argument is accepted: if uid is passed in with a non-zero
+ value, the server is asked to return message UIDs instead of message
+ sequence numbers.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback will be invoked with a list of all
+ the message sequence numbers return by the search, or whose errback
+ will be invoked if there is an error.
+ """
+ if kwarg.get('uid'):
+ cmd = 'UID SEARCH'
+ else:
+ cmd = 'SEARCH'
+ args = ' '.join(queries)
+ d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
+ d.addCallback(self.__cbSearch)
+ return d
+
+
+ def __cbSearch(self, (lines, end)):
+ ids = []
+ for parts in lines:
+ if len(parts) > 0 and parts[0] == 'SEARCH':
+ ids.extend([self._intOrRaise(p, parts) for p in parts[1:]])
+ return ids
+
+
+ def fetchUID(self, messages, uid=0):
+ """Retrieve the unique identifier for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message sequence numbers to unique message identifiers, or whose
+ errback is invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, uid=1)
+
+
+ def fetchFlags(self, messages, uid=0):
+ """Retrieve the flags for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: The messages for which to retrieve flags.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to lists of flags, or whose errback is invoked if
+ there is an error.
+ """
+ return self._fetch(str(messages), useUID=uid, flags=1)
+
+
+ def fetchInternalDate(self, messages, uid=0):
+ """Retrieve the internal date associated with one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: The messages for which to retrieve the internal date.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to date strings, or whose errback is invoked
+ if there is an error. Date strings take the format of
+ \"day-month-year time timezone\".
+ """
+ return self._fetch(str(messages), useUID=uid, internaldate=1)
+
+
+ def fetchEnvelope(self, messages, uid=0):
+ """Retrieve the envelope data for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: The messages for which to retrieve envelope data.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to envelope data, or whose errback is invoked
+ if there is an error. Envelope data consists of a sequence of the
+ date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
+ and message-id header fields. The date, subject, in-reply-to, and
+ message-id fields are strings, while the from, sender, reply-to,
+ to, cc, and bcc fields contain address data. Address data consists
+ of a sequence of name, source route, mailbox name, and hostname.
+ Fields which are not present for a particular address may be C{None}.
+ """
+ return self._fetch(str(messages), useUID=uid, envelope=1)
+
+
+ def fetchBodyStructure(self, messages, uid=0):
+ """Retrieve the structure of the body of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: The messages for which to retrieve body structure
+ data.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to body structure data, or whose errback is invoked
+ if there is an error. Body structure data describes the MIME-IMB
+ format of a message and consists of a sequence of mime type, mime
+ subtype, parameters, content id, description, encoding, and size.
+ The fields following the size field are variable: if the mime
+ type/subtype is message/rfc822, the contained message's envelope
+ information, body structure data, and number of lines of text; if
+ the mime type is text, the number of lines of text. Extension fields
+ may also be included; if present, they are: the MD5 hash of the body,
+ body disposition, body language.
+ """
+ return self._fetch(messages, useUID=uid, bodystructure=1)
+
+
+ def fetchSimplifiedBody(self, messages, uid=0):
+ """Retrieve the simplified body structure of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to body data, or whose errback is invoked
+ if there is an error. The simplified body structure is the same
+ as the body structure, except that extension fields will never be
+ present.
+ """
+ return self._fetch(messages, useUID=uid, body=1)
+
+
+ def fetchMessage(self, messages, uid=0):
+ """Retrieve one or more entire messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+
+ @return: A L{Deferred} which will fire with a C{dict} mapping message
+ sequence numbers to C{dict}s giving message data for the
+ corresponding message. If C{uid} is true, the inner dictionaries
+ have a C{'UID'} key mapped to a C{str} giving the UID for the
+ message. The text of the message is a C{str} associated with the
+ C{'RFC822'} key in each dictionary.
+ """
+ return self._fetch(messages, useUID=uid, rfc822=1)
+
+
+ def fetchHeaders(self, messages, uid=0):
+ """Retrieve headers of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dicts of message headers, or whose errback is
+ invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822header=1)
+
+
+ def fetchBody(self, messages, uid=0):
+ """Retrieve body text of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to file-like objects containing body text, or whose
+ errback is invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822text=1)
+
+
+ def fetchSize(self, messages, uid=0):
+ """Retrieve the size, in octets, of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to sizes, or whose errback is invoked if there is
+ an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822size=1)
+
+
+ def fetchFull(self, messages, uid=0):
+ """Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+ C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
+ functions.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys
+ are "flags", "date", "size", "envelope", and "body".
+ """
+ return self._fetch(
+ messages, useUID=uid, flags=1, internaldate=1,
+ rfc822size=1, envelope=1, body=1)
+
+
+ def fetchAll(self, messages, uid=0):
+ """Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+ C{fetchSize}, and C{fetchEnvelope} functions.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys
+ are "flags", "date", "size", and "envelope".
+ """
+ return self._fetch(
+ messages, useUID=uid, flags=1, internaldate=1,
+ rfc822size=1, envelope=1)
+
+
+ def fetchFast(self, messages, uid=0):
+ """Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
+ C{fetchSize} functions.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys are
+ "flags", "date", and "size".
+ """
+ return self._fetch(
+ messages, useUID=uid, flags=1, internaldate=1, rfc822size=1)
+
+
+ def _parseFetchPairs(self, fetchResponseList):
+ """
+ Given the result of parsing a single I{FETCH} response, construct a
+ C{dict} mapping response keys to response values.
+
+ @param fetchResponseList: The result of parsing a I{FETCH} response
+ with L{parseNestedParens} and extracting just the response data
+ (that is, just the part that comes after C{"FETCH"}). The form
+ of this input (and therefore the output of this method) is very
+ disagreable. A valuable improvement would be to enumerate the
+ possible keys (representing them as structured objects of some
+ sort) rather than using strings and tuples of tuples of strings
+ and so forth. This would allow the keys to be documented more
+ easily and would allow for a much simpler application-facing API
+ (one not based on looking up somewhat hard to predict keys in a
+ dict). Since C{fetchResponseList} notionally represents a
+ flattened sequence of pairs (identifying keys followed by their
+ associated values), collapsing such complex elements of this
+ list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a
+ single object would also greatly simplify the implementation of
+ this method.
+
+ @return: A C{dict} of the response data represented by C{pairs}. Keys
+ in this dictionary are things like C{"RFC822.TEXT"}, C{"FLAGS"}, or
+ C{("BODY", ("HEADER.FIELDS", ("SUBJECT",)))}. Values are entirely
+ dependent on the key with which they are associated, but retain the
+ same structured as produced by L{parseNestedParens}.
+ """
+ values = {}
+ responseParts = iter(fetchResponseList)
+ while True:
+ try:
+ key = responseParts.next()
+ except StopIteration:
+ break
+
+ try:
+ value = responseParts.next()
+ except StopIteration:
+ raise IllegalServerResponse(
+ "Not enough arguments", fetchResponseList)
+
+ # The parsed forms of responses like:
+ #
+ # BODY[] VALUE
+ # BODY[TEXT] VALUE
+ # BODY[HEADER.FIELDS (SUBJECT)] VALUE
+ # BODY[HEADER.FIELDS (SUBJECT)]<N.M> VALUE
+ #
+ # are:
+ #
+ # ["BODY", [], VALUE]
+ # ["BODY", ["TEXT"], VALUE]
+ # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], VALUE]
+ # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], "<N.M>", VALUE]
+ #
+ # Here, check for these cases and grab as many extra elements as
+ # necessary to retrieve the body information.
+ if key in ("BODY", "BODY.PEEK") and isinstance(value, list) and len(value) < 3:
+ if len(value) < 2:
+ key = (key, tuple(value))
+ else:
+ key = (key, (value[0], tuple(value[1])))
+ try:
+ value = responseParts.next()
+ except StopIteration:
+ raise IllegalServerResponse(
+ "Not enough arguments", fetchResponseList)
+
+ # Handle partial ranges
+ if value.startswith('<') and value.endswith('>'):
+ try:
+ int(value[1:-1])
+ except ValueError:
+ # This isn't really a range, it's some content.
+ pass
+ else:
+ key = key + (value,)
+ try:
+ value = responseParts.next()
+ except StopIteration:
+ raise IllegalServerResponse(
+ "Not enough arguments", fetchResponseList)
+
+ values[key] = value
+ return values
+
+
+ def _cbFetch(self, (lines, last), requestedParts, structured):
+ info = {}
+ for parts in lines:
+ if len(parts) == 3 and parts[1] == 'FETCH':
+ id = self._intOrRaise(parts[0], parts)
+ if id not in info:
+ info[id] = [parts[2]]
+ else:
+ info[id][0].extend(parts[2])
+
+ results = {}
+ for (messageId, values) in info.iteritems():
+ mapping = self._parseFetchPairs(values[0])
+ results.setdefault(messageId, {}).update(mapping)
+
+ flagChanges = {}
+ for messageId in results.keys():
+ values = results[messageId]
+ for part in values.keys():
+ if part not in requestedParts and part == 'FLAGS':
+ flagChanges[messageId] = values['FLAGS']
+ # Find flags in the result and get rid of them.
+ for i in range(len(info[messageId][0])):
+ if info[messageId][0][i] == 'FLAGS':
+ del info[messageId][0][i:i+2]
+ break
+ del values['FLAGS']
+ if not values:
+ del results[messageId]
+
+ if flagChanges:
+ self.flagsChanged(flagChanges)
+
+ if structured:
+ return results
+ else:
+ return info
+
+
+ def fetchSpecific(self, messages, uid=0, headerType=None,
+ headerNumber=None, headerArgs=None, peek=None,
+ offset=None, length=None):
+ """Retrieve a specific section of one or more messages
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @type headerType: C{str}
+ @param headerType: If specified, must be one of HEADER,
+ HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
+ which part of the message is retrieved. For HEADER.FIELDS and
+ HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
+ For MIME, C{headerNumber} must be specified.
+
+ @type headerNumber: C{int} or C{int} sequence
+ @param headerNumber: The nested rfc822 index specifying the
+ entity to retrieve. For example, C{1} retrieves the first
+ entity of the message, and C{(2, 1, 3}) retrieves the 3rd
+ entity inside the first entity inside the second entity of
+ the message.
+
+ @type headerArgs: A sequence of C{str}
+ @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
+ headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
+ headers to exclude from retrieval.
+
+ @type peek: C{bool}
+ @param peek: If true, cause the server to not set the \\Seen
+ flag on this message as a result of this command.
+
+ @type offset: C{int}
+ @param offset: The number of octets at the beginning of the result
+ to skip.
+
+ @type length: C{int}
+ @param length: The number of octets to retrieve.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a mapping of
+ message numbers to retrieved data, or whose errback is invoked
+ if there is an error.
+ """
+ fmt = '%s BODY%s[%s%s%s]%s'
+ if headerNumber is None:
+ number = ''
+ elif isinstance(headerNumber, int):
+ number = str(headerNumber)
+ else:
+ number = '.'.join(map(str, headerNumber))
+ if headerType is None:
+ header = ''
+ elif number:
+ header = '.' + headerType
+ else:
+ header = headerType
+ if header and headerType not in ('TEXT', 'MIME'):
+ if headerArgs is not None:
+ payload = ' (%s)' % ' '.join(headerArgs)
+ else:
+ payload = ' ()'
+ else:
+ payload = ''
+ if offset is None:
+ extra = ''
+ else:
+ extra = '<%d.%d>' % (offset, length)
+ fetch = uid and 'UID FETCH' or 'FETCH'
+ cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
+ d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
+ d.addCallback(self._cbFetch, (), False)
+ return d
+
+
+ def _fetch(self, messages, useUID=0, **terms):
+ fetch = useUID and 'UID FETCH' or 'FETCH'
+
+ if 'rfc822text' in terms:
+ del terms['rfc822text']
+ terms['rfc822.text'] = True
+ if 'rfc822size' in terms:
+ del terms['rfc822size']
+ terms['rfc822.size'] = True
+ if 'rfc822header' in terms:
+ del terms['rfc822header']
+ terms['rfc822.header'] = True
+
+ cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
+ d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
+ d.addCallback(self._cbFetch, map(str.upper, terms.keys()), True)
+ return d
+
+ def setFlags(self, messages, flags, silent=1, uid=0):
+ """Set the flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of C{str}
+ @param flags: The flags to set
+
+ @type silent: C{bool}
+ @param silent: If true, cause the server to supress its verbose
+ response.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ the server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(str(messages), 'FLAGS', silent, flags, uid)
+
+ def addFlags(self, messages, flags, silent=1, uid=0):
+ """Add to the set flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of C{str}
+ @param flags: The flags to set
+
+ @type silent: C{bool}
+ @param silent: If true, cause the server to supress its verbose
+ response.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ the server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(str(messages),'+FLAGS', silent, flags, uid)
+
+ def removeFlags(self, messages, flags, silent=1, uid=0):
+ """Remove from the set flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or C{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of C{str}
+ @param flags: The flags to set
+
+ @type silent: C{bool}
+ @param silent: If true, cause the server to supress its verbose
+ response.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ the server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(str(messages), '-FLAGS', silent, flags, uid)
+
+
+ def _store(self, messages, cmd, silent, flags, uid):
+ if silent:
+ cmd = cmd + '.SILENT'
+ store = uid and 'UID STORE' or 'STORE'
+ args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
+ d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
+ expected = ()
+ if not silent:
+ expected = ('FLAGS',)
+ d.addCallback(self._cbFetch, expected, True)
+ return d
+
+
+ def copy(self, messages, mailbox, uid):
+ """Copy the specified messages to the specified mailbox.
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{str}
+ @param messages: A message sequence set
+
+ @type mailbox: C{str}
+ @param mailbox: The mailbox to which to copy the messages
+
+ @type uid: C{bool}
+ @param uid: If true, the C{messages} refers to message UIDs, rather
+ than message sequence numbers.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with a true value
+ when the copy is successful, or whose errback is invoked if there
+ is an error.
+ """
+ if uid:
+ cmd = 'UID COPY'
+ else:
+ cmd = 'COPY'
+ args = '%s %s' % (messages, _prepareMailboxName(mailbox))
+ return self.sendCommand(Command(cmd, args))
+
+ #
+ # IMailboxListener methods
+ #
+ def modeChanged(self, writeable):
+ """Override me"""
+
+ def flagsChanged(self, newFlags):
+ """Override me"""
+
+ def newMessages(self, exists, recent):
+ """Override me"""
+
+
+class IllegalIdentifierError(IMAP4Exception): pass
+
+def parseIdList(s, lastMessageId=None):
+ """
+ Parse a message set search key into a C{MessageSet}.
+
+ @type s: C{str}
+ @param s: A string description of a id list, for example "1:3, 4:*"
+
+ @type lastMessageId: C{int}
+ @param lastMessageId: The last message sequence id or UID, depending on
+ whether we are parsing the list in UID or sequence id context. The
+ caller should pass in the correct value.
+
+ @rtype: C{MessageSet}
+ @return: A C{MessageSet} that contains the ids defined in the list
+ """
+ res = MessageSet()
+ parts = s.split(',')
+ for p in parts:
+ if ':' in p:
+ low, high = p.split(':', 1)
+ try:
+ if low == '*':
+ low = None
+ else:
+ low = long(low)
+ if high == '*':
+ high = None
+ else:
+ high = long(high)
+ if low is high is None:
+ # *:* does not make sense
+ raise IllegalIdentifierError(p)
+ # non-positive values are illegal according to RFC 3501
+ if ((low is not None and low <= 0) or
+ (high is not None and high <= 0)):
+ raise IllegalIdentifierError(p)
+ # star means "highest value of an id in the mailbox"
+ high = high or lastMessageId
+ low = low or lastMessageId
+
+ # RFC says that 2:4 and 4:2 are equivalent
+ if low > high:
+ low, high = high, low
+ res.extend((low, high))
+ except ValueError:
+ raise IllegalIdentifierError(p)
+ else:
+ try:
+ if p == '*':
+ p = None
+ else:
+ p = long(p)
+ if p is not None and p <= 0:
+ raise IllegalIdentifierError(p)
+ except ValueError:
+ raise IllegalIdentifierError(p)
+ else:
+ res.extend(p or lastMessageId)
+ return res
+
+class IllegalQueryError(IMAP4Exception): pass
+
+_SIMPLE_BOOL = (
+ 'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
+ 'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
+)
+
+_NO_QUOTES = (
+ 'LARGER', 'SMALLER', 'UID'
+)
+
+def Query(sorted=0, **kwarg):
+ """Create a query string
+
+ Among the accepted keywords are::
+
+ all : If set to a true value, search all messages in the
+ current mailbox
+
+ answered : If set to a true value, search messages flagged with
+ \\Answered
+
+ bcc : A substring to search the BCC header field for
+
+ before : Search messages with an internal date before this
+ value. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ body : A substring to search the body of the messages for
+
+ cc : A substring to search the CC header field for
+
+ deleted : If set to a true value, search messages flagged with
+ \\Deleted
+
+ draft : If set to a true value, search messages flagged with
+ \\Draft
+
+ flagged : If set to a true value, search messages flagged with
+ \\Flagged
+
+ from : A substring to search the From header field for
+
+ header : A two-tuple of a header name and substring to search
+ for in that header
+
+ keyword : Search for messages with the given keyword set
+
+ larger : Search for messages larger than this number of octets
+
+ messages : Search only the given message sequence set.
+
+ new : If set to a true value, search messages flagged with
+ \\Recent but not \\Seen
+
+ old : If set to a true value, search messages not flagged with
+ \\Recent
+
+ on : Search messages with an internal date which is on this
+ date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ recent : If set to a true value, search for messages flagged with
+ \\Recent
+
+ seen : If set to a true value, search for messages flagged with
+ \\Seen
+
+ sentbefore : Search for messages with an RFC822 'Date' header before
+ this date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ senton : Search for messages with an RFC822 'Date' header which is
+ on this date The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ sentsince : Search for messages with an RFC822 'Date' header which is
+ after this date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ since : Search for messages with an internal date that is after
+ this date.. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ smaller : Search for messages smaller than this number of octets
+
+ subject : A substring to search the 'subject' header for
+
+ text : A substring to search the entire message for
+
+ to : A substring to search the 'to' header for
+
+ uid : Search only the messages in the given message set
+
+ unanswered : If set to a true value, search for messages not
+ flagged with \\Answered
+
+ undeleted : If set to a true value, search for messages not
+ flagged with \\Deleted
+
+ undraft : If set to a true value, search for messages not
+ flagged with \\Draft
+
+ unflagged : If set to a true value, search for messages not
+ flagged with \\Flagged
+
+ unkeyword : Search for messages without the given keyword set
+
+ unseen : If set to a true value, search for messages not
+ flagged with \\Seen
+
+ @type sorted: C{bool}
+ @param sorted: If true, the output will be sorted, alphabetically.
+ The standard does not require it, but it makes testing this function
+ easier. The default is zero, and this should be acceptable for any
+ application.
+
+ @rtype: C{str}
+ @return: The formatted query string
+ """
+ cmd = []
+ keys = kwarg.keys()
+ if sorted:
+ keys.sort()
+ for k in keys:
+ v = kwarg[k]
+ k = k.upper()
+ if k in _SIMPLE_BOOL and v:
+ cmd.append(k)
+ elif k == 'HEADER':
+ cmd.extend([k, v[0], '"%s"' % (v[1],)])
+ elif k not in _NO_QUOTES:
+ cmd.extend([k, '"%s"' % (v,)])
+ else:
+ cmd.extend([k, '%s' % (v,)])
+ if len(cmd) > 1:
+ return '(%s)' % ' '.join(cmd)
+ else:
+ return ' '.join(cmd)
+
+def Or(*args):
+ """The disjunction of two or more queries"""
+ if len(args) < 2:
+ raise IllegalQueryError, args
+ elif len(args) == 2:
+ return '(OR %s %s)' % args
+ else:
+ return '(OR %s %s)' % (args[0], Or(*args[1:]))
+
+def Not(query):
+ """The negation of a query"""
+ return '(NOT %s)' % (query,)
+
+class MismatchedNesting(IMAP4Exception):
+ pass
+
+class MismatchedQuoting(IMAP4Exception):
+ pass
+
+def wildcardToRegexp(wildcard, delim=None):
+ wildcard = wildcard.replace('*', '(?:.*?)')
+ if delim is None:
+ wildcard = wildcard.replace('%', '(?:.*?)')
+ else:
+ wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
+ return re.compile(wildcard, re.I)
+
+def splitQuoted(s):
+ """Split a string into whitespace delimited tokens
+
+ Tokens that would otherwise be separated but are surrounded by \"
+ remain as a single token. Any token that is not quoted and is
+ equal to \"NIL\" is tokenized as C{None}.
+
+ @type s: C{str}
+ @param s: The string to be split
+
+ @rtype: C{list} of C{str}
+ @return: A list of the resulting tokens
+
+ @raise MismatchedQuoting: Raised if an odd number of quotes are present
+ """
+ s = s.strip()
+ result = []
+ word = []
+ inQuote = inWord = False
+ for i, c in enumerate(s):
+ if c == '"':
+ if i and s[i-1] == '\\':
+ word.pop()
+ word.append('"')
+ elif not inQuote:
+ inQuote = True
+ else:
+ inQuote = False
+ result.append(''.join(word))
+ word = []
+ elif not inWord and not inQuote and c not in ('"' + string.whitespace):
+ inWord = True
+ word.append(c)
+ elif inWord and not inQuote and c in string.whitespace:
+ w = ''.join(word)
+ if w == 'NIL':
+ result.append(None)
+ else:
+ result.append(w)
+ word = []
+ inWord = False
+ elif inWord or inQuote:
+ word.append(c)
+
+ if inQuote:
+ raise MismatchedQuoting(s)
+ if inWord:
+ w = ''.join(word)
+ if w == 'NIL':
+ result.append(None)
+ else:
+ result.append(w)
+
+ return result
+
+
+
+def splitOn(sequence, predicate, transformers):
+ result = []
+ mode = predicate(sequence[0])
+ tmp = [sequence[0]]
+ for e in sequence[1:]:
+ p = predicate(e)
+ if p != mode:
+ result.extend(transformers[mode](tmp))
+ tmp = [e]
+ mode = p
+ else:
+ tmp.append(e)
+ result.extend(transformers[mode](tmp))
+ return result
+
+def collapseStrings(results):
+ """
+ Turns a list of length-one strings and lists into a list of longer
+ strings and lists. For example,
+
+ ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
+
+ @type results: C{list} of C{str} and C{list}
+ @param results: The list to be collapsed
+
+ @rtype: C{list} of C{str} and C{list}
+ @return: A new list which is the collapsed form of C{results}
+ """
+ copy = []
+ begun = None
+ listsList = [isinstance(s, types.ListType) for s in results]
+
+ pred = lambda e: isinstance(e, types.TupleType)
+ tran = {
+ 0: lambda e: splitQuoted(''.join(e)),
+ 1: lambda e: [''.join([i[0] for i in e])]
+ }
+ for (i, c, isList) in zip(range(len(results)), results, listsList):
+ if isList:
+ if begun is not None:
+ copy.extend(splitOn(results[begun:i], pred, tran))
+ begun = None
+ copy.append(collapseStrings(c))
+ elif begun is None:
+ begun = i
+ if begun is not None:
+ copy.extend(splitOn(results[begun:], pred, tran))
+ return copy
+
+
+def parseNestedParens(s, handleLiteral = 1):
+ """Parse an s-exp-like string into a more useful data structure.
+
+ @type s: C{str}
+ @param s: The s-exp-like string to parse
+
+ @rtype: C{list} of C{str} and C{list}
+ @return: A list containing the tokens present in the input.
+
+ @raise MismatchedNesting: Raised if the number or placement
+ of opening or closing parenthesis is invalid.
+ """
+ s = s.strip()
+ inQuote = 0
+ contentStack = [[]]
+ try:
+ i = 0
+ L = len(s)
+ while i < L:
+ c = s[i]
+ if inQuote:
+ if c == '\\':
+ contentStack[-1].append(s[i:i+2])
+ i += 2
+ continue
+ elif c == '"':
+ inQuote = not inQuote
+ contentStack[-1].append(c)
+ i += 1
+ else:
+ if c == '"':
+ contentStack[-1].append(c)
+ inQuote = not inQuote
+ i += 1
+ elif handleLiteral and c == '{':
+ end = s.find('}', i)
+ if end == -1:
+ raise ValueError, "Malformed literal"
+ literalSize = int(s[i+1:end])
+ contentStack[-1].append((s[end+3:end+3+literalSize],))
+ i = end + 3 + literalSize
+ elif c == '(' or c == '[':
+ contentStack.append([])
+ i += 1
+ elif c == ')' or c == ']':
+ contentStack[-2].append(contentStack.pop())
+ i += 1
+ else:
+ contentStack[-1].append(c)
+ i += 1
+ except IndexError:
+ raise MismatchedNesting(s)
+ if len(contentStack) != 1:
+ raise MismatchedNesting(s)
+ return collapseStrings(contentStack[0])
+
+def _quote(s):
+ return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
+
+def _literal(s):
+ return '{%d}\r\n%s' % (len(s), s)
+
+class DontQuoteMe:
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return str(self.value)
+
+_ATOM_SPECIALS = '(){ %*"'
+def _needsQuote(s):
+ if s == '':
+ return 1
+ for c in s:
+ if c < '\x20' or c > '\x7f':
+ return 1
+ if c in _ATOM_SPECIALS:
+ return 1
+ return 0
+
+def _prepareMailboxName(name):
+ name = name.encode('imap4-utf-7')
+ if _needsQuote(name):
+ return _quote(name)
+ return name
+
+def _needsLiteral(s):
+ # Change this to "return 1" to wig out stupid clients
+ return '\n' in s or '\r' in s or len(s) > 1000
+
+def collapseNestedLists(items):
+ """Turn a nested list structure into an s-exp-like string.
+
+ Strings in C{items} will be sent as literals if they contain CR or LF,
+ otherwise they will be quoted. References to None in C{items} will be
+ translated to the atom NIL. Objects with a 'read' attribute will have
+ it called on them with no arguments and the returned string will be
+ inserted into the output as a literal. Integers will be converted to
+ strings and inserted into the output unquoted. Instances of
+ C{DontQuoteMe} will be converted to strings and inserted into the output
+ unquoted.
+
+ This function used to be much nicer, and only quote things that really
+ needed to be quoted (and C{DontQuoteMe} did not exist), however, many
+ broken IMAP4 clients were unable to deal with this level of sophistication,
+ forcing the current behavior to be adopted for practical reasons.
+
+ @type items: Any iterable
+
+ @rtype: C{str}
+ """
+ pieces = []
+ for i in items:
+ if i is None:
+ pieces.extend([' ', 'NIL'])
+ elif isinstance(i, (DontQuoteMe, int, long)):
+ pieces.extend([' ', str(i)])
+ elif isinstance(i, types.StringTypes):
+ if _needsLiteral(i):
+ pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
+ else:
+ pieces.extend([' ', _quote(i)])
+ elif hasattr(i, 'read'):
+ d = i.read()
+ pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
+ else:
+ pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
+ return ''.join(pieces[1:])
+
+
+class IClientAuthentication(Interface):
+ def getName():
+ """Return an identifier associated with this authentication scheme.
+
+ @rtype: C{str}
+ """
+
+ def challengeResponse(secret, challenge):
+ """Generate a challenge response string"""
+
+
+
+class CramMD5ClientAuthenticator:
+ implements(IClientAuthentication)
+
+ def __init__(self, user):
+ self.user = user
+
+ def getName(self):
+ return "CRAM-MD5"
+
+ def challengeResponse(self, secret, chal):
+ response = hmac.HMAC(secret, chal).hexdigest()
+ return '%s %s' % (self.user, response)
+
+
+
+class LOGINAuthenticator:
+ implements(IClientAuthentication)
+
+ def __init__(self, user):
+ self.user = user
+ self.challengeResponse = self.challengeUsername
+
+ def getName(self):
+ return "LOGIN"
+
+ def challengeUsername(self, secret, chal):
+ # Respond to something like "Username:"
+ self.challengeResponse = self.challengeSecret
+ return self.user
+
+ def challengeSecret(self, secret, chal):
+ # Respond to something like "Password:"
+ return secret
+
+class PLAINAuthenticator:
+ implements(IClientAuthentication)
+
+ def __init__(self, user):
+ self.user = user
+
+ def getName(self):
+ return "PLAIN"
+
+ def challengeResponse(self, secret, chal):
+ return '\0%s\0%s' % (self.user, secret)
+
+
+class MailboxException(IMAP4Exception): pass
+
+class MailboxCollision(MailboxException):
+ def __str__(self):
+ return 'Mailbox named %s already exists' % self.args
+
+class NoSuchMailbox(MailboxException):
+ def __str__(self):
+ return 'No mailbox named %s exists' % self.args
+
+class ReadOnlyMailbox(MailboxException):
+ def __str__(self):
+ return 'Mailbox open in read-only state'
+
+
+class IAccount(Interface):
+ """Interface for Account classes
+
+ Implementors of this interface should consider implementing
+ C{INamespacePresenter}.
+ """
+
+ def addMailbox(name, mbox = None):
+ """Add a new mailbox to this account
+
+ @type name: C{str}
+ @param name: The name associated with this mailbox. It may not
+ contain multiple hierarchical parts.
+
+ @type mbox: An object implementing C{IMailbox}
+ @param mbox: The mailbox to associate with this name. If C{None},
+ a suitable default is created and used.
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the creation succeeds, or a deferred whose
+ callback will be invoked when the creation succeeds.
+
+ @raise MailboxException: Raised if this mailbox cannot be added for
+ some reason. This may also be raised asynchronously, if a C{Deferred}
+ is returned.
+ """
+
+ def create(pathspec):
+ """Create a new mailbox from the given hierarchical name.
+
+ @type pathspec: C{str}
+ @param pathspec: The full hierarchical name of a new mailbox to create.
+ If any of the inferior hierarchical names to this one do not exist,
+ they are created as well.
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the creation succeeds, or a deferred whose
+ callback will be invoked when the creation succeeds.
+
+ @raise MailboxException: Raised if this mailbox cannot be added.
+ This may also be raised asynchronously, if a C{Deferred} is
+ returned.
+ """
+
+ def select(name, rw=True):
+ """Acquire a mailbox, given its name.
+
+ @type name: C{str}
+ @param name: The mailbox to acquire
+
+ @type rw: C{bool}
+ @param rw: If a true value, request a read-write version of this
+ mailbox. If a false value, request a read-only version.
+
+ @rtype: Any object implementing C{IMailbox} or C{Deferred}
+ @return: The mailbox object, or a C{Deferred} whose callback will
+ be invoked with the mailbox object. None may be returned if the
+ specified mailbox may not be selected for any reason.
+ """
+
+ def delete(name):
+ """Delete the mailbox with the specified name.
+
+ @type name: C{str}
+ @param name: The mailbox to delete.
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the mailbox is successfully deleted, or a
+ C{Deferred} whose callback will be invoked when the deletion
+ completes.
+
+ @raise MailboxException: Raised if this mailbox cannot be deleted.
+ This may also be raised asynchronously, if a C{Deferred} is returned.
+ """
+
+ def rename(oldname, newname):
+ """Rename a mailbox
+
+ @type oldname: C{str}
+ @param oldname: The current name of the mailbox to rename.
+
+ @type newname: C{str}
+ @param newname: The new name to associate with the mailbox.
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the mailbox is successfully renamed, or a
+ C{Deferred} whose callback will be invoked when the rename operation
+ is completed.
+
+ @raise MailboxException: Raised if this mailbox cannot be
+ renamed. This may also be raised asynchronously, if a C{Deferred}
+ is returned.
+ """
+
+ def isSubscribed(name):
+ """Check the subscription status of a mailbox
+
+ @type name: C{str}
+ @param name: The name of the mailbox to check
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the given mailbox is currently subscribed
+ to, a false value otherwise. A C{Deferred} may also be returned
+ whose callback will be invoked with one of these values.
+ """
+
+ def subscribe(name):
+ """Subscribe to a mailbox
+
+ @type name: C{str}
+ @param name: The name of the mailbox to subscribe to
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the mailbox is subscribed to successfully,
+ or a Deferred whose callback will be invoked with this value when
+ the subscription is successful.
+
+ @raise MailboxException: Raised if this mailbox cannot be
+ subscribed to. This may also be raised asynchronously, if a
+ C{Deferred} is returned.
+ """
+
+ def unsubscribe(name):
+ """Unsubscribe from a mailbox
+
+ @type name: C{str}
+ @param name: The name of the mailbox to unsubscribe from
+
+ @rtype: C{Deferred} or C{bool}
+ @return: A true value if the mailbox is unsubscribed from successfully,
+ or a Deferred whose callback will be invoked with this value when
+ the unsubscription is successful.
+
+ @raise MailboxException: Raised if this mailbox cannot be
+ unsubscribed from. This may also be raised asynchronously, if a
+ C{Deferred} is returned.
+ """
+
+ def listMailboxes(ref, wildcard):
+ """List all the mailboxes that meet a certain criteria
+
+ @type ref: C{str}
+ @param ref: The context in which to apply the wildcard
+
+ @type wildcard: C{str}
+ @param wildcard: An expression against which to match mailbox names.
+ '*' matches any number of characters in a mailbox name, and '%'
+ matches similarly, but will not match across hierarchical boundaries.
+
+ @rtype: C{list} of C{tuple}
+ @return: A list of C{(mailboxName, mailboxObject)} which meet the
+ given criteria. C{mailboxObject} should implement either
+ C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned.
+ """
+
+class INamespacePresenter(Interface):
+ def getPersonalNamespaces():
+ """Report the available personal namespaces.
+
+ Typically there should be only one personal namespace. A common
+ name for it is \"\", and its hierarchical delimiter is usually
+ \"/\".
+
+ @rtype: iterable of two-tuples of strings
+ @return: The personal namespaces and their hierarchical delimiters.
+ If no namespaces of this type exist, None should be returned.
+ """
+
+ def getSharedNamespaces():
+ """Report the available shared namespaces.
+
+ Shared namespaces do not belong to any individual user but are
+ usually to one or more of them. Examples of shared namespaces
+ might be \"#news\" for a usenet gateway.
+
+ @rtype: iterable of two-tuples of strings
+ @return: The shared namespaces and their hierarchical delimiters.
+ If no namespaces of this type exist, None should be returned.
+ """
+
+ def getUserNamespaces():
+ """Report the available user namespaces.
+
+ These are namespaces that contain folders belonging to other users
+ access to which this account has been granted.
+
+ @rtype: iterable of two-tuples of strings
+ @return: The user namespaces and their hierarchical delimiters.
+ If no namespaces of this type exist, None should be returned.
+ """
+
+
+class MemoryAccount(object):
+ implements(IAccount, INamespacePresenter)
+
+ mailboxes = None
+ subscriptions = None
+ top_id = 0
+
+ def __init__(self, name):
+ self.name = name
+ self.mailboxes = {}
+ self.subscriptions = []
+
+ def allocateID(self):
+ id = self.top_id
+ self.top_id += 1
+ return id
+
+ ##
+ ## IAccount
+ ##
+ def addMailbox(self, name, mbox = None):
+ name = name.upper()
+ if self.mailboxes.has_key(name):
+ raise MailboxCollision, name
+ if mbox is None:
+ mbox = self._emptyMailbox(name, self.allocateID())
+ self.mailboxes[name] = mbox
+ return 1
+
+ def create(self, pathspec):
+ paths = filter(None, pathspec.split('/'))
+ for accum in range(1, len(paths)):
+ try:
+ self.addMailbox('/'.join(paths[:accum]))
+ except MailboxCollision:
+ pass
+ try:
+ self.addMailbox('/'.join(paths))
+ except MailboxCollision:
+ if not pathspec.endswith('/'):
+ return False
+ return True
+
+ def _emptyMailbox(self, name, id):
+ raise NotImplementedError
+
+ def select(self, name, readwrite=1):
+ return self.mailboxes.get(name.upper())
+
+ def delete(self, name):
+ name = name.upper()
+ # See if this mailbox exists at all
+ mbox = self.mailboxes.get(name)
+ if not mbox:
+ raise MailboxException("No such mailbox")
+ # See if this box is flagged \Noselect
+ if r'\Noselect' in mbox.getFlags():
+ # Check for hierarchically inferior mailboxes with this one
+ # as part of their root.
+ for others in self.mailboxes.keys():
+ if others != name and others.startswith(name):
+ raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
+ mbox.destroy()
+
+ # iff there are no hierarchically inferior names, we will
+ # delete it from our ken.
+ if self._inferiorNames(name) > 1:
+ del self.mailboxes[name]
+
+ def rename(self, oldname, newname):
+ oldname = oldname.upper()
+ newname = newname.upper()
+ if not self.mailboxes.has_key(oldname):
+ raise NoSuchMailbox, oldname
+
+ inferiors = self._inferiorNames(oldname)
+ inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
+
+ for (old, new) in inferiors:
+ if self.mailboxes.has_key(new):
+ raise MailboxCollision, new
+
+ for (old, new) in inferiors:
+ self.mailboxes[new] = self.mailboxes[old]
+ del self.mailboxes[old]
+
+ def _inferiorNames(self, name):
+ inferiors = []
+ for infname in self.mailboxes.keys():
+ if infname.startswith(name):
+ inferiors.append(infname)
+ return inferiors
+
+ def isSubscribed(self, name):
+ return name.upper() in self.subscriptions
+
+ def subscribe(self, name):
+ name = name.upper()
+ if name not in self.subscriptions:
+ self.subscriptions.append(name)
+
+ def unsubscribe(self, name):
+ name = name.upper()
+ if name not in self.subscriptions:
+ raise MailboxException, "Not currently subscribed to " + name
+ self.subscriptions.remove(name)
+
+ def listMailboxes(self, ref, wildcard):
+ ref = self._inferiorNames(ref.upper())
+ wildcard = wildcardToRegexp(wildcard, '/')
+ return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
+
+ ##
+ ## INamespacePresenter
+ ##
+ def getPersonalNamespaces(self):
+ return [["", "/"]]
+
+ def getSharedNamespaces(self):
+ return None
+
+ def getOtherNamespaces(self):
+ return None
+
+
+
+_statusRequestDict = {
+ 'MESSAGES': 'getMessageCount',
+ 'RECENT': 'getRecentCount',
+ 'UIDNEXT': 'getUIDNext',
+ 'UIDVALIDITY': 'getUIDValidity',
+ 'UNSEEN': 'getUnseenCount'
+}
+def statusRequestHelper(mbox, names):
+ r = {}
+ for n in names:
+ r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
+ return r
+
+def parseAddr(addr):
+ if addr is None:
+ return [(None, None, None),]
+ addrs = email.Utils.getaddresses([addr])
+ return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
+
+def getEnvelope(msg):
+ headers = msg.getHeaders(True)
+ date = headers.get('date')
+ subject = headers.get('subject')
+ from_ = headers.get('from')
+ sender = headers.get('sender', from_)
+ reply_to = headers.get('reply-to', from_)
+ to = headers.get('to')
+ cc = headers.get('cc')
+ bcc = headers.get('bcc')
+ in_reply_to = headers.get('in-reply-to')
+ mid = headers.get('message-id')
+ return (date, subject, parseAddr(from_), parseAddr(sender),
+ reply_to and parseAddr(reply_to), to and parseAddr(to),
+ cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
+
+def getLineCount(msg):
+ # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
+ # XXX - This must be the number of lines in the ENCODED version
+ lines = 0
+ for _ in msg.getBodyFile():
+ lines += 1
+ return lines
+
+def unquote(s):
+ if s[0] == s[-1] == '"':
+ return s[1:-1]
+ return s
+
+def getBodyStructure(msg, extended=False):
+ # XXX - This does not properly handle multipart messages
+ # BODYSTRUCTURE is obscenely complex and criminally under-documented.
+
+ attrs = {}
+ headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
+ headers = msg.getHeaders(False, *headers)
+ mm = headers.get('content-type')
+ if mm:
+ mm = ''.join(mm.splitlines())
+ mimetype = mm.split(';')
+ if mimetype:
+ type = mimetype[0].split('/', 1)
+ if len(type) == 1:
+ major = type[0]
+ minor = None
+ elif len(type) == 2:
+ major, minor = type
+ else:
+ major = minor = None
+ attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
+ else:
+ major = minor = None
+ else:
+ major = minor = None
+
+
+ size = str(msg.getSize())
+ unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()]
+ result = [
+ major, minor, # Main and Sub MIME types
+ unquotedAttrs, # content-type parameter list
+ headers.get('content-id'),
+ headers.get('content-description'),
+ headers.get('content-transfer-encoding'),
+ size, # Number of octets total
+ ]
+
+ if major is not None:
+ if major.lower() == 'text':
+ result.append(str(getLineCount(msg)))
+ elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
+ contained = msg.getSubPart(0)
+ result.append(getEnvelope(contained))
+ result.append(getBodyStructure(contained, False))
+ result.append(str(getLineCount(contained)))
+
+ if not extended or major is None:
+ return result
+
+ if major.lower() != 'multipart':
+ headers = 'content-md5', 'content-disposition', 'content-language'
+ headers = msg.getHeaders(False, *headers)
+ disp = headers.get('content-disposition')
+
+ # XXX - I dunno if this is really right
+ if disp:
+ disp = disp.split('; ')
+ if len(disp) == 1:
+ disp = (disp[0].lower(), None)
+ elif len(disp) > 1:
+ disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
+
+ result.append(headers.get('content-md5'))
+ result.append(disp)
+ result.append(headers.get('content-language'))
+ else:
+ result = [result]
+ try:
+ i = 0
+ while True:
+ submsg = msg.getSubPart(i)
+ result.append(getBodyStructure(submsg))
+ i += 1
+ except IndexError:
+ result.append(minor)
+ result.append(attrs.items())
+
+ # XXX - I dunno if this is really right
+ headers = msg.getHeaders(False, 'content-disposition', 'content-language')
+ disp = headers.get('content-disposition')
+ if disp:
+ disp = disp.split('; ')
+ if len(disp) == 1:
+ disp = (disp[0].lower(), None)
+ elif len(disp) > 1:
+ disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
+
+ result.append(disp)
+ result.append(headers.get('content-language'))
+
+ return result
+
+class IMessagePart(Interface):
+ def getHeaders(negate, *names):
+ """Retrieve a group of message headers.
+
+ @type names: C{tuple} of C{str}
+ @param names: The names of the headers to retrieve or omit.
+
+ @type negate: C{bool}
+ @param negate: If True, indicates that the headers listed in C{names}
+ should be omitted from the return value, rather than included.
+
+ @rtype: C{dict}
+ @return: A mapping of header field names to header field values
+ """
+
+ def getBodyFile():
+ """Retrieve a file object containing only the body of this message.
+ """
+
+ def getSize():
+ """Retrieve the total size, in octets, of this message.
+
+ @rtype: C{int}
+ """
+
+ def isMultipart():
+ """Indicate whether this message has subparts.
+
+ @rtype: C{bool}
+ """
+
+ def getSubPart(part):
+ """Retrieve a MIME sub-message
+
+ @type part: C{int}
+ @param part: The number of the part to retrieve, indexed from 0.
+
+ @raise IndexError: Raised if the specified part does not exist.
+ @raise TypeError: Raised if this message is not multipart.
+
+ @rtype: Any object implementing C{IMessagePart}.
+ @return: The specified sub-part.
+ """
+
+class IMessage(IMessagePart):
+ def getUID():
+ """Retrieve the unique identifier associated with this message.
+ """
+
+ def getFlags():
+ """Retrieve the flags associated with this message.
+
+ @rtype: C{iterable}
+ @return: The flags, represented as strings.
+ """
+
+ def getInternalDate():
+ """Retrieve the date internally associated with this message.
+
+ @rtype: C{str}
+ @return: An RFC822-formatted date string.
+ """
+
+class IMessageFile(Interface):
+ """Optional message interface for representing messages as files.
+
+ If provided by message objects, this interface will be used instead
+ the more complex MIME-based interface.
+ """
+ def open():
+ """Return an file-like object opened for reading.
+
+ Reading from the returned file will return all the bytes
+ of which this message consists.
+ """
+
+class ISearchableMailbox(Interface):
+ def search(query, uid):
+ """Search for messages that meet the given query criteria.
+
+ If this interface is not implemented by the mailbox, L{IMailbox.fetch}
+ and various methods of L{IMessage} will be used instead.
+
+ Implementations which wish to offer better performance than the
+ default implementation should implement this interface.
+
+ @type query: C{list}
+ @param query: The search criteria
+
+ @type uid: C{bool}
+ @param uid: If true, the IDs specified in the query are UIDs;
+ otherwise they are message sequence IDs.
+
+ @rtype: C{list} or C{Deferred}
+ @return: A list of message sequence numbers or message UIDs which
+ match the search criteria or a C{Deferred} whose callback will be
+ invoked with such a list.
+ """
+
+class IMessageCopier(Interface):
+ def copy(messageObject):
+ """Copy the given message object into this mailbox.
+
+ The message object will be one which was previously returned by
+ L{IMailbox.fetch}.
+
+ Implementations which wish to offer better performance than the
+ default implementation should implement this interface.
+
+ If this interface is not implemented by the mailbox, IMailbox.addMessage
+ will be used instead.
+
+ @rtype: C{Deferred} or C{int}
+ @return: Either the UID of the message or a Deferred which fires
+ with the UID when the copy finishes.
+ """
+
+class IMailboxInfo(Interface):
+ """Interface specifying only the methods required for C{listMailboxes}.
+
+ Implementations can return objects implementing only these methods for
+ return to C{listMailboxes} if it can allow them to operate more
+ efficiently.
+ """
+
+ def getFlags():
+ """Return the flags defined in this mailbox
+
+ Flags with the \\ prefix are reserved for use as system flags.
+
+ @rtype: C{list} of C{str}
+ @return: A list of the flags that can be set on messages in this mailbox.
+ """
+
+ def getHierarchicalDelimiter():
+ """Get the character which delimits namespaces for in this mailbox.
+
+ @rtype: C{str}
+ """
+
+class IMailbox(IMailboxInfo):
+ def getUIDValidity():
+ """Return the unique validity identifier for this mailbox.
+
+ @rtype: C{int}
+ """
+
+ def getUIDNext():
+ """Return the likely UID for the next message added to this mailbox.
+
+ @rtype: C{int}
+ """
+
+ def getUID(message):
+ """Return the UID of a message in the mailbox
+
+ @type message: C{int}
+ @param message: The message sequence number
+
+ @rtype: C{int}
+ @return: The UID of the message.
+ """
+
+ def getMessageCount():
+ """Return the number of messages in this mailbox.
+
+ @rtype: C{int}
+ """
+
+ def getRecentCount():
+ """Return the number of messages with the 'Recent' flag.
+
+ @rtype: C{int}
+ """
+
+ def getUnseenCount():
+ """Return the number of messages with the 'Unseen' flag.
+
+ @rtype: C{int}
+ """
+
+ def isWriteable():
+ """Get the read/write status of the mailbox.
+
+ @rtype: C{int}
+ @return: A true value if write permission is allowed, a false value otherwise.
+ """
+
+ def destroy():
+ """Called before this mailbox is deleted, permanently.
+
+ If necessary, all resources held by this mailbox should be cleaned
+ up here. This function _must_ set the \\Noselect flag on this
+ mailbox.
+ """
+
+ def requestStatus(names):
+ """Return status information about this mailbox.
+
+ Mailboxes which do not intend to do any special processing to
+ generate the return value, C{statusRequestHelper} can be used
+ to build the dictionary by calling the other interface methods
+ which return the data for each name.
+
+ @type names: Any iterable
+ @param names: The status names to return information regarding.
+ The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
+ UIDVALIDITY, UNSEEN.
+
+ @rtype: C{dict} or C{Deferred}
+ @return: A dictionary containing status information about the
+ requested names is returned. If the process of looking this
+ information up would be costly, a deferred whose callback will
+ eventually be passed this dictionary is returned instead.
+ """
+
+ def addListener(listener):
+ """Add a mailbox change listener
+
+ @type listener: Any object which implements C{IMailboxListener}
+ @param listener: An object to add to the set of those which will
+ be notified when the contents of this mailbox change.
+ """
+
+ def removeListener(listener):
+ """Remove a mailbox change listener
+
+ @type listener: Any object previously added to and not removed from
+ this mailbox as a listener.
+ @param listener: The object to remove from the set of listeners.
+
+ @raise ValueError: Raised when the given object is not a listener for
+ this mailbox.
+ """
+
+ def addMessage(message, flags = (), date = None):
+ """Add the given message to this mailbox.
+
+ @type message: A file-like object
+ @param message: The RFC822 formatted message
+
+ @type flags: Any iterable of C{str}
+ @param flags: The flags to associate with this message
+
+ @type date: C{str}
+ @param date: If specified, the date to associate with this
+ message.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked with the message
+ id if the message is added successfully and whose errback is
+ invoked otherwise.
+
+ @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+ read-write.
+ """
+
+ def expunge():
+ """Remove all messages flagged \\Deleted.
+
+ @rtype: C{list} or C{Deferred}
+ @return: The list of message sequence numbers which were deleted,
+ or a C{Deferred} whose callback will be invoked with such a list.
+
+ @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+ read-write.
+ """
+
+ def fetch(messages, uid):
+ """Retrieve one or more messages.
+
+ @type messages: C{MessageSet}
+ @param messages: The identifiers of messages to retrieve information
+ about
+
+ @type uid: C{bool}
+ @param uid: If true, the IDs specified in the query are UIDs;
+ otherwise they are message sequence IDs.
+
+ @rtype: Any iterable of two-tuples of message sequence numbers and
+ implementors of C{IMessage}.
+ """
+
+ def store(messages, flags, mode, uid):
+ """Set the flags of one or more messages.
+
+ @type messages: A MessageSet object with the list of messages requested
+ @param messages: The identifiers of the messages to set the flags of.
+
+ @type flags: sequence of C{str}
+ @param flags: The flags to set, unset, or add.
+
+ @type mode: -1, 0, or 1
+ @param mode: If mode is -1, these flags should be removed from the
+ specified messages. If mode is 1, these flags should be added to
+ the specified messages. If mode is 0, all existing flags should be
+ cleared and these flags should be added.
+
+ @type uid: C{bool}
+ @param uid: If true, the IDs specified in the query are UIDs;
+ otherwise they are message sequence IDs.
+
+ @rtype: C{dict} or C{Deferred}
+ @return: A C{dict} mapping message sequence numbers to sequences of C{str}
+ representing the flags set on the message after this operation has
+ been performed, or a C{Deferred} whose callback will be invoked with
+ such a C{dict}.
+
+ @raise ReadOnlyMailbox: Raised if this mailbox is not open for
+ read-write.
+ """
+
+class ICloseableMailbox(Interface):
+ """A supplementary interface for mailboxes which require cleanup on close.
+
+ Implementing this interface is optional. If it is implemented, the protocol
+ code will call the close method defined whenever a mailbox is closed.
+ """
+ def close():
+ """Close this mailbox.
+
+ @return: A C{Deferred} which fires when this mailbox
+ has been closed, or None if the mailbox can be closed
+ immediately.
+ """
+
+def _formatHeaders(headers):
+ hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
+ in headers.iteritems()]
+ hdrs = '\r\n'.join(hdrs) + '\r\n'
+ return hdrs
+
+def subparts(m):
+ i = 0
+ try:
+ while True:
+ yield m.getSubPart(i)
+ i += 1
+ except IndexError:
+ pass
+
+def iterateInReactor(i):
+ """Consume an interator at most a single iteration per reactor iteration.
+
+ If the iterator produces a Deferred, the next iteration will not occur
+ until the Deferred fires, otherwise the next iteration will be taken
+ in the next reactor iteration.
+
+ @rtype: C{Deferred}
+ @return: A deferred which fires (with None) when the iterator is
+ exhausted or whose errback is called if there is an exception.
+ """
+ from twisted.internet import reactor
+ d = defer.Deferred()
+ def go(last):
+ try:
+ r = i.next()
+ except StopIteration:
+ d.callback(last)
+ except:
+ d.errback()
+ else:
+ if isinstance(r, defer.Deferred):
+ r.addCallback(go)
+ else:
+ reactor.callLater(0, go, r)
+ go(None)
+ return d
+
+class MessageProducer:
+ CHUNK_SIZE = 2 ** 2 ** 2 ** 2
+
+ def __init__(self, msg, buffer = None, scheduler = None):
+ """Produce this message.
+
+ @param msg: The message I am to produce.
+ @type msg: L{IMessage}
+
+ @param buffer: A buffer to hold the message in. If None, I will
+ use a L{tempfile.TemporaryFile}.
+ @type buffer: file-like
+ """
+ self.msg = msg
+ if buffer is None:
+ buffer = tempfile.TemporaryFile()
+ self.buffer = buffer
+ if scheduler is None:
+ scheduler = iterateInReactor
+ self.scheduler = scheduler
+ self.write = self.buffer.write
+
+ def beginProducing(self, consumer):
+ self.consumer = consumer
+ return self.scheduler(self._produce())
+
+ def _produce(self):
+ headers = self.msg.getHeaders(True)
+ boundary = None
+ if self.msg.isMultipart():
+ content = headers.get('content-type')
+ parts = [x.split('=', 1) for x in content.split(';')[1:]]
+ parts = dict([(k.lower().strip(), v) for (k, v) in parts])
+ boundary = parts.get('boundary')
+ if boundary is None:
+ # Bastards
+ boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
+ headers['content-type'] += '; boundary="%s"' % (boundary,)
+ else:
+ if boundary.startswith('"') and boundary.endswith('"'):
+ boundary = boundary[1:-1]
+
+ self.write(_formatHeaders(headers))
+ self.write('\r\n')
+ if self.msg.isMultipart():
+ for p in subparts(self.msg):
+ self.write('\r\n--%s\r\n' % (boundary,))
+ yield MessageProducer(p, self.buffer, self.scheduler
+ ).beginProducing(None
+ )
+ self.write('\r\n--%s--\r\n' % (boundary,))
+ else:
+ f = self.msg.getBodyFile()
+ while True:
+ b = f.read(self.CHUNK_SIZE)
+ if b:
+ self.buffer.write(b)
+ yield None
+ else:
+ break
+ if self.consumer:
+ self.buffer.seek(0, 0)
+ yield FileProducer(self.buffer
+ ).beginProducing(self.consumer
+ ).addCallback(lambda _: self
+ )
+
+class _FetchParser:
+ class Envelope:
+ # Response should be a list of fields from the message:
+ # date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
+ # and message-id.
+ #
+ # from, sender, reply-to, to, cc, and bcc are themselves lists of
+ # address information:
+ # personal name, source route, mailbox name, host name
+ #
+ # reply-to and sender must not be None. If not present in a message
+ # they should be defaulted to the value of the from field.
+ type = 'envelope'
+ __str__ = lambda self: 'envelope'
+
+ class Flags:
+ type = 'flags'
+ __str__ = lambda self: 'flags'
+
+ class InternalDate:
+ type = 'internaldate'
+ __str__ = lambda self: 'internaldate'
+
+ class RFC822Header:
+ type = 'rfc822header'
+ __str__ = lambda self: 'rfc822.header'
+
+ class RFC822Text:
+ type = 'rfc822text'
+ __str__ = lambda self: 'rfc822.text'
+
+ class RFC822Size:
+ type = 'rfc822size'
+ __str__ = lambda self: 'rfc822.size'
+
+ class RFC822:
+ type = 'rfc822'
+ __str__ = lambda self: 'rfc822'
+
+ class UID:
+ type = 'uid'
+ __str__ = lambda self: 'uid'
+
+ class Body:
+ type = 'body'
+ peek = False
+ header = None
+ mime = None
+ text = None
+ part = ()
+ empty = False
+ partialBegin = None
+ partialLength = None
+ def __str__(self):
+ base = 'BODY'
+ part = ''
+ separator = ''
+ if self.part:
+ part = '.'.join([str(x + 1) for x in self.part])
+ separator = '.'
+# if self.peek:
+# base += '.PEEK'
+ if self.header:
+ base += '[%s%s%s]' % (part, separator, self.header,)
+ elif self.text:
+ base += '[%s%sTEXT]' % (part, separator)
+ elif self.mime:
+ base += '[%s%sMIME]' % (part, separator)
+ elif self.empty:
+ base += '[%s]' % (part,)
+ if self.partialBegin is not None:
+ base += '<%d.%d>' % (self.partialBegin, self.partialLength)
+ return base
+
+ class BodyStructure:
+ type = 'bodystructure'
+ __str__ = lambda self: 'bodystructure'
+
+ # These three aren't top-level, they don't need type indicators
+ class Header:
+ negate = False
+ fields = None
+ part = None
+ def __str__(self):
+ base = 'HEADER'
+ if self.fields:
+ base += '.FIELDS'
+ if self.negate:
+ base += '.NOT'
+ fields = []
+ for f in self.fields:
+ f = f.title()
+ if _needsQuote(f):
+ f = _quote(f)
+ fields.append(f)
+ base += ' (%s)' % ' '.join(fields)
+ if self.part:
+ base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
+ return base
+
+ class Text:
+ pass
+
+ class MIME:
+ pass
+
+ parts = None
+
+ _simple_fetch_att = [
+ ('envelope', Envelope),
+ ('flags', Flags),
+ ('internaldate', InternalDate),
+ ('rfc822.header', RFC822Header),
+ ('rfc822.text', RFC822Text),
+ ('rfc822.size', RFC822Size),
+ ('rfc822', RFC822),
+ ('uid', UID),
+ ('bodystructure', BodyStructure),
+ ]
+
+ def __init__(self):
+ self.state = ['initial']
+ self.result = []
+ self.remaining = ''
+
+ def parseString(self, s):
+ s = self.remaining + s
+ try:
+ while s or self.state:
+ # print 'Entering state_' + self.state[-1] + ' with', repr(s)
+ state = self.state.pop()
+ try:
+ used = getattr(self, 'state_' + state)(s)
+ except:
+ self.state.append(state)
+ raise
+ else:
+ # print state, 'consumed', repr(s[:used])
+ s = s[used:]
+ finally:
+ self.remaining = s
+
+ def state_initial(self, s):
+ # In the initial state, the literals "ALL", "FULL", and "FAST"
+ # are accepted, as is a ( indicating the beginning of a fetch_att
+ # token, as is the beginning of a fetch_att token.
+ if s == '':
+ return 0
+
+ l = s.lower()
+ if l.startswith('all'):
+ self.result.extend((
+ self.Flags(), self.InternalDate(),
+ self.RFC822Size(), self.Envelope()
+ ))
+ return 3
+ if l.startswith('full'):
+ self.result.extend((
+ self.Flags(), self.InternalDate(),
+ self.RFC822Size(), self.Envelope(),
+ self.Body()
+ ))
+ return 4
+ if l.startswith('fast'):
+ self.result.extend((
+ self.Flags(), self.InternalDate(), self.RFC822Size(),
+ ))
+ return 4
+
+ if l.startswith('('):
+ self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
+ return 1
+
+ self.state.append('fetch_att')
+ return 0
+
+ def state_close_paren(self, s):
+ if s.startswith(')'):
+ return 1
+ raise Exception("Missing )")
+
+ def state_whitespace(self, s):
+ # Eat up all the leading whitespace
+ if not s or not s[0].isspace():
+ raise Exception("Whitespace expected, none found")
+ i = 0
+ for i in range(len(s)):
+ if not s[i].isspace():
+ break
+ return i
+
+ def state_maybe_fetch_att(self, s):
+ if not s.startswith(')'):
+ self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
+ return 0
+
+ def state_fetch_att(self, s):
+ # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
+ # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
+ # "BODYSTRUCTURE", "UID",
+ # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
+
+ l = s.lower()
+ for (name, cls) in self._simple_fetch_att:
+ if l.startswith(name):
+ self.result.append(cls())
+ return len(name)
+
+ b = self.Body()
+ if l.startswith('body.peek'):
+ b.peek = True
+ used = 9
+ elif l.startswith('body'):
+ used = 4
+ else:
+ raise Exception("Nothing recognized in fetch_att: %s" % (l,))
+
+ self.pending_body = b
+ self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
+ return used
+
+ def state_got_body(self, s):
+ self.result.append(self.pending_body)
+ del self.pending_body
+ return 0
+
+ def state_maybe_section(self, s):
+ if not s.startswith("["):
+ return 0
+
+ self.state.extend(('section', 'part_number'))
+ return 1
+
+ _partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?')
+ def state_part_number(self, s):
+ m = self._partExpr.match(s)
+ if m is not None:
+ self.parts = [int(p) - 1 for p in m.groups()[0].split('.')]
+ return m.end()
+ else:
+ self.parts = []
+ return 0
+
+ def state_section(self, s):
+ # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
+ # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
+ # just "]".
+
+ l = s.lower()
+ used = 0
+ if l.startswith(']'):
+ self.pending_body.empty = True
+ used += 1
+ elif l.startswith('header]'):
+ h = self.pending_body.header = self.Header()
+ h.negate = True
+ h.fields = ()
+ used += 7
+ elif l.startswith('text]'):
+ self.pending_body.text = self.Text()
+ used += 5
+ elif l.startswith('mime]'):
+ self.pending_body.mime = self.MIME()
+ used += 5
+ else:
+ h = self.Header()
+ if l.startswith('header.fields.not'):
+ h.negate = True
+ used += 17
+ elif l.startswith('header.fields'):
+ used += 13
+ else:
+ raise Exception("Unhandled section contents: %r" % (l,))
+
+ self.pending_body.header = h
+ self.state.extend(('finish_section', 'header_list', 'whitespace'))
+ self.pending_body.part = tuple(self.parts)
+ self.parts = None
+ return used
+
+ def state_finish_section(self, s):
+ if not s.startswith(']'):
+ raise Exception("section must end with ]")
+ return 1
+
+ def state_header_list(self, s):
+ if not s.startswith('('):
+ raise Exception("Header list must begin with (")
+ end = s.find(')')
+ if end == -1:
+ raise Exception("Header list must end with )")
+
+ headers = s[1:end].split()
+ self.pending_body.header.fields = map(str.upper, headers)
+ return end + 1
+
+ def state_maybe_partial(self, s):
+ # Grab <number.number> or nothing at all
+ if not s.startswith('<'):
+ return 0
+ end = s.find('>')
+ if end == -1:
+ raise Exception("Found < but not >")
+
+ partial = s[1:end]
+ parts = partial.split('.', 1)
+ if len(parts) != 2:
+ raise Exception("Partial specification did not include two .-delimited integers")
+ begin, length = map(int, parts)
+ self.pending_body.partialBegin = begin
+ self.pending_body.partialLength = length
+
+ return end + 1
+
+class FileProducer:
+ CHUNK_SIZE = 2 ** 2 ** 2 ** 2
+
+ firstWrite = True
+
+ def __init__(self, f):
+ self.f = f
+
+ def beginProducing(self, consumer):
+ self.consumer = consumer
+ self.produce = consumer.write
+ d = self._onDone = defer.Deferred()
+ self.consumer.registerProducer(self, False)
+ return d
+
+ def resumeProducing(self):
+ b = ''
+ if self.firstWrite:
+ b = '{%d}\r\n' % self._size()
+ self.firstWrite = False
+ if not self.f:
+ return
+ b = b + self.f.read(self.CHUNK_SIZE)
+ if not b:
+ self.consumer.unregisterProducer()
+ self._onDone.callback(self)
+ self._onDone = self.f = self.consumer = None
+ else:
+ self.produce(b)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+ def _size(self):
+ b = self.f.tell()
+ self.f.seek(0, 2)
+ e = self.f.tell()
+ self.f.seek(b, 0)
+ return e - b
+
+def parseTime(s):
+ # XXX - This may require localization :(
+ months = [
+ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
+ 'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
+ 'july', 'august', 'september', 'october', 'november', 'december'
+ ]
+ expr = {
+ 'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
+ 'mon': r"(?P<mon>\w+)",
+ 'year': r"(?P<year>\d\d\d\d)"
+ }
+ m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
+ if not m:
+ raise ValueError, "Cannot parse time string %r" % (s,)
+ d = m.groupdict()
+ try:
+ d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
+ d['year'] = int(d['year'])
+ d['day'] = int(d['day'])
+ except ValueError:
+ raise ValueError, "Cannot parse time string %r" % (s,)
+ else:
+ return time.struct_time(
+ (d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
+ )
+
+import codecs
+def modified_base64(s):
+ s_utf7 = s.encode('utf-7')
+ return s_utf7[1:-1].replace('/', ',')
+
+def modified_unbase64(s):
+ s_utf7 = '+' + s.replace(',', '/') + '-'
+ return s_utf7.decode('utf-7')
+
+def encoder(s, errors=None):
+ """
+ Encode the given C{unicode} string using the IMAP4 specific variation of
+ UTF-7.
+
+ @type s: C{unicode}
+ @param s: The text to encode.
+
+ @param errors: Policy for handling encoding errors. Currently ignored.
+
+ @return: C{tuple} of a C{str} giving the encoded bytes and an C{int}
+ giving the number of code units consumed from the input.
+ """
+ r = []
+ _in = []
+ for c in s:
+ if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
+ if _in:
+ r.extend(['&', modified_base64(''.join(_in)), '-'])
+ del _in[:]
+ r.append(str(c))
+ elif c == '&':
+ if _in:
+ r.extend(['&', modified_base64(''.join(_in)), '-'])
+ del _in[:]
+ r.append('&-')
+ else:
+ _in.append(c)
+ if _in:
+ r.extend(['&', modified_base64(''.join(_in)), '-'])
+ return (''.join(r), len(s))
+
+def decoder(s, errors=None):
+ """
+ Decode the given C{str} using the IMAP4 specific variation of UTF-7.
+
+ @type s: C{str}
+ @param s: The bytes to decode.
+
+ @param errors: Policy for handling decoding errors. Currently ignored.
+
+ @return: a C{tuple} of a C{unicode} string giving the text which was
+ decoded and an C{int} giving the number of bytes consumed from the
+ input.
+ """
+ r = []
+ decode = []
+ for c in s:
+ if c == '&' and not decode:
+ decode.append('&')
+ elif c == '-' and decode:
+ if len(decode) == 1:
+ r.append('&')
+ else:
+ r.append(modified_unbase64(''.join(decode[1:])))
+ decode = []
+ elif decode:
+ decode.append(c)
+ else:
+ r.append(c)
+ if decode:
+ r.append(modified_unbase64(''.join(decode[1:])))
+ return (''.join(r), len(s))
+
+class StreamReader(codecs.StreamReader):
+ def decode(self, s, errors='strict'):
+ return decoder(s)
+
+class StreamWriter(codecs.StreamWriter):
+ def encode(self, s, errors='strict'):
+ return encoder(s)
+
+_codecInfo = (encoder, decoder, StreamReader, StreamWriter)
+try:
+ _codecInfoClass = codecs.CodecInfo
+except AttributeError:
+ pass
+else:
+ _codecInfo = _codecInfoClass(*_codecInfo)
+
+def imap4_utf_7(name):
+ if name == 'imap4-utf-7':
+ return _codecInfo
+codecs.register(imap4_utf_7)
+
+__all__ = [
+ # Protocol classes
+ 'IMAP4Server', 'IMAP4Client',
+
+ # Interfaces
+ 'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox',
+ 'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo',
+ 'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox',
+
+ # Exceptions
+ 'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation',
+ 'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse',
+ 'NoSupportedAuthentication', 'IllegalServerResponse',
+ 'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting',
+ 'MismatchedQuoting', 'MailboxException', 'MailboxCollision',
+ 'NoSuchMailbox', 'ReadOnlyMailbox',
+
+ # Auth objects
+ 'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator',
+ 'PLAINCredentials', 'LOGINCredentials',
+
+ # Simple query interface
+ 'Query', 'Not', 'Or',
+
+ # Miscellaneous
+ 'MemoryAccount',
+ 'statusRequestHelper',
+]
diff --git a/twisted/mail/mail.py b/twisted/mail/mail.py
new file mode 100644
index 0000000..07789aa
--- /dev/null
+++ b/twisted/mail/mail.py
@@ -0,0 +1,333 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Mail support for twisted python.
+"""
+
+# Twisted imports
+from twisted.internet import defer
+from twisted.application import service, internet
+from twisted.python import util
+from twisted.python import log
+
+from twisted import cred
+import twisted.cred.portal
+
+# Sibling imports
+from twisted.mail import protocols, smtp
+
+# System imports
+import os
+from zope.interface import implements, Interface
+
+
+class DomainWithDefaultDict:
+ '''Simulate a dictionary with a default value for non-existing keys.
+ '''
+ def __init__(self, domains, default):
+ self.domains = domains
+ self.default = default
+
+ def setDefaultDomain(self, domain):
+ self.default = domain
+
+ def has_key(self, name):
+ return 1
+
+ def fromkeys(klass, keys, value=None):
+ d = klass()
+ for k in keys:
+ d[k] = value
+ return d
+ fromkeys = classmethod(fromkeys)
+
+ def __contains__(self, name):
+ return 1
+
+ def __getitem__(self, name):
+ return self.domains.get(name, self.default)
+
+ def __setitem__(self, name, value):
+ self.domains[name] = value
+
+ def __delitem__(self, name):
+ del self.domains[name]
+
+ def __iter__(self):
+ return iter(self.domains)
+
+ def __len__(self):
+ return len(self.domains)
+
+
+ def __str__(self):
+ """
+ Return a string describing the underlying domain mapping of this
+ object.
+ """
+ return '<DomainWithDefaultDict %s>' % (self.domains,)
+
+
+ def __repr__(self):
+ """
+ Return a pseudo-executable string describing the underlying domain
+ mapping of this object.
+ """
+ return 'DomainWithDefaultDict(%s)' % (self.domains,)
+
+
+ def get(self, key, default=None):
+ return self.domains.get(key, default)
+
+ def copy(self):
+ return DomainWithDefaultDict(self.domains.copy(), self.default)
+
+ def iteritems(self):
+ return self.domains.iteritems()
+
+ def iterkeys(self):
+ return self.domains.iterkeys()
+
+ def itervalues(self):
+ return self.domains.itervalues()
+
+ def keys(self):
+ return self.domains.keys()
+
+ def values(self):
+ return self.domains.values()
+
+ def items(self):
+ return self.domains.items()
+
+ def popitem(self):
+ return self.domains.popitem()
+
+ def update(self, other):
+ return self.domains.update(other)
+
+ def clear(self):
+ return self.domains.clear()
+
+ def setdefault(self, key, default):
+ return self.domains.setdefault(key, default)
+
+class IDomain(Interface):
+ """An email domain."""
+
+ def exists(user):
+ """
+ Check whether or not the specified user exists in this domain.
+
+ @type user: C{twisted.protocols.smtp.User}
+ @param user: The user to check
+
+ @rtype: No-argument callable
+ @return: A C{Deferred} which becomes, or a callable which
+ takes no arguments and returns an object implementing C{IMessage}.
+ This will be called and the returned object used to deliver the
+ message when it arrives.
+
+ @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
+ user does not exist in this domain.
+ """
+
+ def addUser(user, password):
+ """Add a username/password to this domain."""
+
+ def startMessage(user):
+ """Create and return a new message to be delivered to the given user.
+
+ DEPRECATED. Implement validateTo() correctly instead.
+ """
+
+ def getCredentialsCheckers():
+ """Return a list of ICredentialsChecker implementors for this domain.
+ """
+
+class IAliasableDomain(IDomain):
+ def setAliasGroup(aliases):
+ """Set the group of defined aliases for this domain
+
+ @type aliases: C{dict}
+ @param aliases: Mapping of domain names to objects implementing
+ C{IAlias}
+ """
+
+ def exists(user, memo=None):
+ """
+ Check whether or not the specified user exists in this domain.
+
+ @type user: C{twisted.protocols.smtp.User}
+ @param user: The user to check
+
+ @type memo: C{dict}
+ @param memo: A record of the addresses already considered while
+ resolving aliases. The default value should be used by all
+ external code.
+
+ @rtype: No-argument callable
+ @return: A C{Deferred} which becomes, or a callable which
+ takes no arguments and returns an object implementing C{IMessage}.
+ This will be called and the returned object used to deliver the
+ message when it arrives.
+
+ @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
+ user does not exist in this domain.
+ """
+
+class BounceDomain:
+ """A domain in which no user exists.
+
+ This can be used to block off certain domains.
+ """
+
+ implements(IDomain)
+
+ def exists(self, user):
+ raise smtp.SMTPBadRcpt(user)
+
+ def willRelay(self, user, protocol):
+ return False
+
+ def addUser(self, user, password):
+ pass
+
+ def startMessage(self, user):
+ """
+ No code should ever call this function.
+ """
+ raise NotImplementedError(
+ "No code should ever call this method for any reason")
+
+ def getCredentialsCheckers(self):
+ return []
+
+
+class FileMessage:
+ """A file we can write an email too."""
+
+ implements(smtp.IMessage)
+
+ def __init__(self, fp, name, finalName):
+ self.fp = fp
+ self.name = name
+ self.finalName = finalName
+
+ def lineReceived(self, line):
+ self.fp.write(line+'\n')
+
+ def eomReceived(self):
+ self.fp.close()
+ os.rename(self.name, self.finalName)
+ return defer.succeed(self.finalName)
+
+ def connectionLost(self):
+ self.fp.close()
+ os.remove(self.name)
+
+
+class MailService(service.MultiService):
+ """An email service."""
+
+ queue = None
+ domains = None
+ portals = None
+ aliases = None
+ smtpPortal = None
+
+ def __init__(self):
+ service.MultiService.__init__(self)
+ # Domains and portals for "client" protocols - POP3, IMAP4, etc
+ self.domains = DomainWithDefaultDict({}, BounceDomain())
+ self.portals = {}
+
+ self.monitor = FileMonitoringService()
+ self.monitor.setServiceParent(self)
+ self.smtpPortal = cred.portal.Portal(self)
+
+ def getPOP3Factory(self):
+ return protocols.POP3Factory(self)
+
+ def getSMTPFactory(self):
+ return protocols.SMTPFactory(self, self.smtpPortal)
+
+ def getESMTPFactory(self):
+ return protocols.ESMTPFactory(self, self.smtpPortal)
+
+ def addDomain(self, name, domain):
+ portal = cred.portal.Portal(domain)
+ map(portal.registerChecker, domain.getCredentialsCheckers())
+ self.domains[name] = domain
+ self.portals[name] = portal
+ if self.aliases and IAliasableDomain.providedBy(domain):
+ domain.setAliasGroup(self.aliases)
+
+ def setQueue(self, queue):
+ """Set the queue for outgoing emails."""
+ self.queue = queue
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if smtp.IMessageDelivery in interfaces:
+ a = protocols.ESMTPDomainDelivery(self, avatarId)
+ return smtp.IMessageDelivery, a, lambda: None
+ raise NotImplementedError()
+
+ def lookupPortal(self, name):
+ return self.portals[name]
+
+ def defaultPortal(self):
+ return self.portals['']
+
+
+class FileMonitoringService(internet.TimerService):
+
+ def __init__(self):
+ self.files = []
+ self.intervals = iter(util.IntervalDifferential([], 60))
+
+ def startService(self):
+ service.Service.startService(self)
+ self._setupMonitor()
+
+ def _setupMonitor(self):
+ from twisted.internet import reactor
+ t, self.index = self.intervals.next()
+ self._call = reactor.callLater(t, self._monitor)
+
+ def stopService(self):
+ service.Service.stopService(self)
+ if self._call:
+ self._call.cancel()
+ self._call = None
+
+ def monitorFile(self, name, callback, interval=10):
+ try:
+ mtime = os.path.getmtime(name)
+ except:
+ mtime = 0
+ self.files.append([interval, name, callback, mtime])
+ self.intervals.addInterval(interval)
+
+ def unmonitorFile(self, name):
+ for i in range(len(self.files)):
+ if name == self.files[i][1]:
+ self.intervals.removeInterval(self.files[i][0])
+ del self.files[i]
+ break
+
+ def _monitor(self):
+ self._call = None
+ if self.index is not None:
+ name, callback, mtime = self.files[self.index][1:]
+ try:
+ now = os.path.getmtime(name)
+ except:
+ now = 0
+ if now > mtime:
+ log.msg("%s changed, notifying listener" % (name,))
+ self.files[self.index][3] = now
+ callback(name)
+ self._setupMonitor()
diff --git a/twisted/mail/maildir.py b/twisted/mail/maildir.py
new file mode 100644
index 0000000..7927b32
--- /dev/null
+++ b/twisted/mail/maildir.py
@@ -0,0 +1,518 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Maildir-style mailbox support
+"""
+
+import os
+import stat
+import socket
+
+from zope.interface import implements
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+from twisted.python.compat import set
+from twisted.mail import pop3
+from twisted.mail import smtp
+from twisted.protocols import basic
+from twisted.persisted import dirdbm
+from twisted.python import log, failure
+from twisted.python.hashlib import md5
+from twisted.mail import mail
+from twisted.internet import interfaces, defer, reactor
+from twisted.cred import portal, credentials, checkers
+from twisted.cred.error import UnauthorizedLogin
+
+INTERNAL_ERROR = '''\
+From: Twisted.mail Internals
+Subject: An Error Occurred
+
+ An internal server error has occurred. Please contact the
+ server administrator.
+'''
+
+class _MaildirNameGenerator:
+ """
+ Utility class to generate a unique maildir name
+
+ @ivar _clock: An L{IReactorTime} provider which will be used to learn
+ the current time to include in names returned by L{generate} so that
+ they sort properly.
+ """
+ n = 0
+ p = os.getpid()
+ s = socket.gethostname().replace('/', r'\057').replace(':', r'\072')
+
+ def __init__(self, clock):
+ self._clock = clock
+
+ def generate(self):
+ """
+ Return a string which is intended to unique across all calls to this
+ function (across all processes, reboots, etc).
+
+ Strings returned by earlier calls to this method will compare less
+ than strings returned by later calls as long as the clock provided
+ doesn't go backwards.
+ """
+ self.n = self.n + 1
+ t = self._clock.seconds()
+ seconds = str(int(t))
+ microseconds = '%07d' % (int((t - int(t)) * 10e6),)
+ return '%s.M%sP%sQ%s.%s' % (seconds, microseconds,
+ self.p, self.n, self.s)
+
+_generateMaildirName = _MaildirNameGenerator(reactor).generate
+
+def initializeMaildir(dir):
+ if not os.path.isdir(dir):
+ os.mkdir(dir, 0700)
+ for subdir in ['new', 'cur', 'tmp', '.Trash']:
+ os.mkdir(os.path.join(dir, subdir), 0700)
+ for subdir in ['new', 'cur', 'tmp']:
+ os.mkdir(os.path.join(dir, '.Trash', subdir), 0700)
+ # touch
+ open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
+
+
+class MaildirMessage(mail.FileMessage):
+ size = None
+
+ def __init__(self, address, fp, *a, **kw):
+ header = "Delivered-To: %s\n" % address
+ fp.write(header)
+ self.size = len(header)
+ mail.FileMessage.__init__(self, fp, *a, **kw)
+
+ def lineReceived(self, line):
+ mail.FileMessage.lineReceived(self, line)
+ self.size += len(line)+1
+
+ def eomReceived(self):
+ self.finalName = self.finalName+',S=%d' % self.size
+ return mail.FileMessage.eomReceived(self)
+
+class AbstractMaildirDomain:
+ """Abstract maildir-backed domain.
+ """
+ alias = None
+ root = None
+
+ def __init__(self, service, root):
+ """Initialize.
+ """
+ self.root = root
+
+ def userDirectory(self, user):
+ """Get the maildir directory for a given user
+
+ Override to specify where to save mails for users.
+ Return None for non-existing users.
+ """
+ return None
+
+ ##
+ ## IAliasableDomain
+ ##
+
+ def setAliasGroup(self, alias):
+ self.alias = alias
+
+ ##
+ ## IDomain
+ ##
+ def exists(self, user, memo=None):
+ """Check for existence of user in the domain
+ """
+ if self.userDirectory(user.dest.local) is not None:
+ return lambda: self.startMessage(user)
+ try:
+ a = self.alias[user.dest.local]
+ except:
+ raise smtp.SMTPBadRcpt(user)
+ else:
+ aliases = a.resolve(self.alias, memo)
+ if aliases:
+ return lambda: aliases
+ log.err("Bad alias configuration: " + str(user))
+ raise smtp.SMTPBadRcpt(user)
+
+ def startMessage(self, user):
+ """Save a message for a given user
+ """
+ if isinstance(user, str):
+ name, domain = user.split('@', 1)
+ else:
+ name, domain = user.dest.local, user.dest.domain
+ dir = self.userDirectory(name)
+ fname = _generateMaildirName()
+ filename = os.path.join(dir, 'tmp', fname)
+ fp = open(filename, 'w')
+ return MaildirMessage('%s@%s' % (name, domain), fp, filename,
+ os.path.join(dir, 'new', fname))
+
+ def willRelay(self, user, protocol):
+ return False
+
+ def addUser(self, user, password):
+ raise NotImplementedError
+
+ def getCredentialsCheckers(self):
+ raise NotImplementedError
+ ##
+ ## end of IDomain
+ ##
+
+class _MaildirMailboxAppendMessageTask:
+ implements(interfaces.IConsumer)
+
+ osopen = staticmethod(os.open)
+ oswrite = staticmethod(os.write)
+ osclose = staticmethod(os.close)
+ osrename = staticmethod(os.rename)
+
+ def __init__(self, mbox, msg):
+ self.mbox = mbox
+ self.defer = defer.Deferred()
+ self.openCall = None
+ if not hasattr(msg, "read"):
+ msg = StringIO.StringIO(msg)
+ self.msg = msg
+
+ def startUp(self):
+ self.createTempFile()
+ if self.fh != -1:
+ self.filesender = basic.FileSender()
+ self.filesender.beginFileTransfer(self.msg, self)
+
+ def registerProducer(self, producer, streaming):
+ self.myproducer = producer
+ self.streaming = streaming
+ if not streaming:
+ self.prodProducer()
+
+ def prodProducer(self):
+ self.openCall = None
+ if self.myproducer is not None:
+ self.openCall = reactor.callLater(0, self.prodProducer)
+ self.myproducer.resumeProducing()
+
+ def unregisterProducer(self):
+ self.myproducer = None
+ self.streaming = None
+ self.osclose(self.fh)
+ self.moveFileToNew()
+
+ def write(self, data):
+ try:
+ self.oswrite(self.fh, data)
+ except:
+ self.fail()
+
+ def fail(self, err=None):
+ if err is None:
+ err = failure.Failure()
+ if self.openCall is not None:
+ self.openCall.cancel()
+ self.defer.errback(err)
+ self.defer = None
+
+ def moveFileToNew(self):
+ while True:
+ newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
+ try:
+ self.osrename(self.tmpname, newname)
+ break
+ except OSError, (err, estr):
+ import errno
+ # if the newname exists, retry with a new newname.
+ if err != errno.EEXIST:
+ self.fail()
+ newname = None
+ break
+ if newname is not None:
+ self.mbox.list.append(newname)
+ self.defer.callback(None)
+ self.defer = None
+
+ def createTempFile(self):
+ attr = (os.O_RDWR | os.O_CREAT | os.O_EXCL
+ | getattr(os, "O_NOINHERIT", 0)
+ | getattr(os, "O_NOFOLLOW", 0))
+ tries = 0
+ self.fh = -1
+ while True:
+ self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
+ try:
+ self.fh = self.osopen(self.tmpname, attr, 0600)
+ return None
+ except OSError:
+ tries += 1
+ if tries > 500:
+ self.defer.errback(RuntimeError("Could not create tmp file for %s" % self.mbox.path))
+ self.defer = None
+ return None
+
+class MaildirMailbox(pop3.Mailbox):
+ """Implement the POP3 mailbox semantics for a Maildir mailbox
+ """
+ AppendFactory = _MaildirMailboxAppendMessageTask
+
+ def __init__(self, path):
+ """Initialize with name of the Maildir mailbox
+ """
+ self.path = path
+ self.list = []
+ self.deleted = {}
+ initializeMaildir(path)
+ for name in ('cur', 'new'):
+ for file in os.listdir(os.path.join(path, name)):
+ self.list.append((file, os.path.join(path, name, file)))
+ self.list.sort()
+ self.list = [e[1] for e in self.list]
+
+ def listMessages(self, i=None):
+ """Return a list of lengths of all files in new/ and cur/
+ """
+ if i is None:
+ ret = []
+ for mess in self.list:
+ if mess:
+ ret.append(os.stat(mess)[stat.ST_SIZE])
+ else:
+ ret.append(0)
+ return ret
+ return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
+
+ def getMessage(self, i):
+ """Return an open file-pointer to a message
+ """
+ return open(self.list[i])
+
+ def getUidl(self, i):
+ """Return a unique identifier for a message
+
+ This is done using the basename of the filename.
+ It is globally unique because this is how Maildirs are designed.
+ """
+ # Returning the actual filename is a mistake. Hash it.
+ base = os.path.basename(self.list[i])
+ return md5(base).hexdigest()
+
+ def deleteMessage(self, i):
+ """Delete a message
+
+ This only moves a message to the .Trash/ subfolder,
+ so it can be undeleted by an administrator.
+ """
+ trashFile = os.path.join(
+ self.path, '.Trash', 'cur', os.path.basename(self.list[i])
+ )
+ os.rename(self.list[i], trashFile)
+ self.deleted[self.list[i]] = trashFile
+ self.list[i] = 0
+
+ def undeleteMessages(self):
+ """Undelete any deleted messages it is possible to undelete
+
+ This moves any messages from .Trash/ subfolder back to their
+ original position, and empties out the deleted dictionary.
+ """
+ for (real, trash) in self.deleted.items():
+ try:
+ os.rename(trash, real)
+ except OSError, (err, estr):
+ import errno
+ # If the file has been deleted from disk, oh well!
+ if err != errno.ENOENT:
+ raise
+ # This is a pass
+ else:
+ try:
+ self.list[self.list.index(0)] = real
+ except ValueError:
+ self.list.append(real)
+ self.deleted.clear()
+
+ def appendMessage(self, txt):
+ """
+ Appends a message into the mailbox.
+
+ @param txt: A C{str} or file-like object giving the message to append.
+
+ @return: A L{Deferred} which fires when the message has been appended to
+ the mailbox.
+ """
+ task = self.AppendFactory(self, txt)
+ result = task.defer
+ task.startUp()
+ return result
+
+class StringListMailbox:
+ """
+ L{StringListMailbox} is an in-memory mailbox.
+
+ @ivar msgs: A C{list} of C{str} giving the contents of each message in the
+ mailbox.
+
+ @ivar _delete: A C{set} of the indexes of messages which have been deleted
+ since the last C{sync} call.
+ """
+ implements(pop3.IMailbox)
+
+ def __init__(self, msgs):
+ self.msgs = msgs
+ self._delete = set()
+
+
+ def listMessages(self, i=None):
+ """
+ Return the length of the message at the given offset, or a list of all
+ message lengths.
+ """
+ if i is None:
+ return [self.listMessages(i) for i in range(len(self.msgs))]
+ if i in self._delete:
+ return 0
+ return len(self.msgs[i])
+
+
+ def getMessage(self, i):
+ """
+ Return an in-memory file-like object for the message content at the
+ given offset.
+ """
+ return StringIO.StringIO(self.msgs[i])
+
+
+ def getUidl(self, i):
+ """
+ Return a hash of the contents of the message at the given offset.
+ """
+ return md5(self.msgs[i]).hexdigest()
+
+
+ def deleteMessage(self, i):
+ """
+ Mark the given message for deletion.
+ """
+ self._delete.add(i)
+
+
+ def undeleteMessages(self):
+ """
+ Reset deletion tracking, undeleting any messages which have been
+ deleted since the last call to C{sync}.
+ """
+ self._delete = set()
+
+
+ def sync(self):
+ """
+ Discard the contents of any message marked for deletion and reset
+ deletion tracking.
+ """
+ for index in self._delete:
+ self.msgs[index] = ""
+ self._delete = set()
+
+
+
+class MaildirDirdbmDomain(AbstractMaildirDomain):
+ """A Maildir Domain where membership is checked by a dirdbm file
+ """
+
+ implements(portal.IRealm, mail.IAliasableDomain)
+
+ portal = None
+ _credcheckers = None
+
+ def __init__(self, service, root, postmaster=0):
+ """Initialize
+
+ The first argument is where the Domain directory is rooted.
+ The second is whether non-existing addresses are simply
+ forwarded to postmaster instead of outright bounce
+
+ The directory structure of a MailddirDirdbmDomain is:
+
+ /passwd <-- a dirdbm file
+ /USER/{cur,new,del} <-- each user has these three directories
+ """
+ AbstractMaildirDomain.__init__(self, service, root)
+ dbm = os.path.join(root, 'passwd')
+ if not os.path.exists(dbm):
+ os.makedirs(dbm)
+ self.dbm = dirdbm.open(dbm)
+ self.postmaster = postmaster
+
+ def userDirectory(self, name):
+ """Get the directory for a user
+
+ If the user exists in the dirdbm file, return the directory
+ os.path.join(root, name), creating it if necessary.
+ Otherwise, returns postmaster's mailbox instead if bounces
+ go to postmaster, otherwise return None
+ """
+ if not self.dbm.has_key(name):
+ if not self.postmaster:
+ return None
+ name = 'postmaster'
+ dir = os.path.join(self.root, name)
+ if not os.path.exists(dir):
+ initializeMaildir(dir)
+ return dir
+
+ ##
+ ## IDomain
+ ##
+ def addUser(self, user, password):
+ self.dbm[user] = password
+ # Ensure it is initialized
+ self.userDirectory(user)
+
+ def getCredentialsCheckers(self):
+ if self._credcheckers is None:
+ self._credcheckers = [DirdbmDatabase(self.dbm)]
+ return self._credcheckers
+
+ ##
+ ## IRealm
+ ##
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pop3.IMailbox not in interfaces:
+ raise NotImplementedError("No interface")
+ if avatarId == checkers.ANONYMOUS:
+ mbox = StringListMailbox([INTERNAL_ERROR])
+ else:
+ mbox = MaildirMailbox(os.path.join(self.root, avatarId))
+
+ return (
+ pop3.IMailbox,
+ mbox,
+ lambda: None
+ )
+
+class DirdbmDatabase:
+ implements(checkers.ICredentialsChecker)
+
+ credentialInterfaces = (
+ credentials.IUsernamePassword,
+ credentials.IUsernameHashedPassword
+ )
+
+ def __init__(self, dbm):
+ self.dirdbm = dbm
+
+ def requestAvatarId(self, c):
+ if c.username in self.dirdbm:
+ if c.checkPassword(self.dirdbm[c.username]):
+ return c.username
+ raise UnauthorizedLogin()
diff --git a/twisted/mail/pb.py b/twisted/mail/pb.py
new file mode 100644
index 0000000..8a9417f
--- /dev/null
+++ b/twisted/mail/pb.py
@@ -0,0 +1,115 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.spread import pb
+from twisted.spread import banana
+
+import os
+import types
+
+class Maildir(pb.Referenceable):
+
+ def __init__(self, directory, rootDirectory):
+ self.virtualDirectory = directory
+ self.rootDirectory = rootDirectory
+ self.directory = os.path.join(rootDirectory, directory)
+
+ def getFolderMessage(self, folder, name):
+ if '/' in name:
+ raise IOError("can only open files in '%s' directory'" % folder)
+ fp = open(os.path.join(self.directory, 'new', name))
+ try:
+ return fp.read()
+ finally:
+ fp.close()
+
+ def deleteFolderMessage(self, folder, name):
+ if '/' in name:
+ raise IOError("can only delete files in '%s' directory'" % folder)
+ os.rename(os.path.join(self.directory, folder, name),
+ os.path.join(self.rootDirectory, '.Trash', folder, name))
+
+ def deleteNewMessage(self, name):
+ return self.deleteFolderMessage('new', name)
+ remote_deleteNewMessage = deleteNewMessage
+
+ def deleteCurMessage(self, name):
+ return self.deleteFolderMessage('cur', name)
+ remote_deleteCurMessage = deleteCurMessage
+
+ def getNewMessages(self):
+ return os.listdir(os.path.join(self.directory, 'new'))
+ remote_getNewMessages = getNewMessages
+
+ def getCurMessages(self):
+ return os.listdir(os.path.join(self.directory, 'cur'))
+ remote_getCurMessages = getCurMessages
+
+ def getNewMessage(self, name):
+ return self.getFolderMessage('new', name)
+ remote_getNewMessage = getNewMessage
+
+ def getCurMessage(self, name):
+ return self.getFolderMessage('cur', name)
+ remote_getCurMessage = getCurMessage
+
+ def getSubFolder(self, name):
+ if name[0] == '.':
+ raise IOError("subfolder name cannot begin with a '.'")
+ name = name.replace('/', ':')
+ if self.virtualDirectoy == '.':
+ name = '.'+name
+ else:
+ name = self.virtualDirectory+':'+name
+ if not self._isSubFolder(name):
+ raise IOError("not a subfolder")
+ return Maildir(name, self.rootDirectory)
+ remote_getSubFolder = getSubFolder
+
+ def _isSubFolder(self, name):
+ return (not os.path.isdir(os.path.join(self.rootDirectory, name)) or
+ not os.path.isfile(os.path.join(self.rootDirectory, name,
+ 'maildirfolder')))
+
+
+class MaildirCollection(pb.Referenceable):
+
+ def __init__(self, root):
+ self.root = root
+
+ def getSubFolders(self):
+ return os.listdir(self.getRoot())
+ remote_getSubFolders = getSubFolders
+
+ def getSubFolder(self, name):
+ if '/' in name or name[0] == '.':
+ raise IOError("invalid name")
+ return Maildir('.', os.path.join(self.getRoot(), name))
+ remote_getSubFolder = getSubFolder
+
+
+class MaildirBroker(pb.Broker):
+
+ def proto_getCollection(self, requestID, name, domain, password):
+ collection = self._getCollection()
+ if collection is None:
+ self.sendError(requestID, "permission denied")
+ else:
+ self.sendAnswer(requestID, collection)
+
+ def getCollection(self, name, domain, password):
+ if not self.domains.has_key(domain):
+ return
+ domain = self.domains[domain]
+ if (domain.dbm.has_key(name) and
+ domain.dbm[name] == password):
+ return MaildirCollection(domain.userDirectory(name))
+
+
+class MaildirClient(pb.Broker):
+
+ def getCollection(self, name, domain, password, callback, errback):
+ requestID = self.newRequestID()
+ self.waitingForAnswers[requestID] = callback, errback
+ self.sendCall("getCollection", requestID, name, domain, password)
diff --git a/twisted/mail/pop3.py b/twisted/mail/pop3.py
new file mode 100644
index 0000000..3b65242
--- /dev/null
+++ b/twisted/mail/pop3.py
@@ -0,0 +1,1071 @@
+# -*- test-case-name: twisted.mail.test.test_pop3 -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Post-office Protocol version 3
+
+@author: Glyph Lefkowitz
+@author: Jp Calderone
+"""
+
+import base64
+import binascii
+import warnings
+
+from zope.interface import implements, Interface
+
+from twisted.mail import smtp
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import task
+from twisted.internet import defer
+from twisted.internet import interfaces
+from twisted.python import log
+from twisted.python.hashlib import md5
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.credentials
+
+##
+## Authentication
+##
+class APOPCredentials:
+ implements(cred.credentials.IUsernamePassword)
+
+ def __init__(self, magic, username, digest):
+ self.magic = magic
+ self.username = username
+ self.digest = digest
+
+ def checkPassword(self, password):
+ seed = self.magic + password
+ myDigest = md5(seed).hexdigest()
+ return myDigest == self.digest
+
+
+class _HeadersPlusNLines:
+ def __init__(self, f, n):
+ self.f = f
+ self.n = n
+ self.linecount = 0
+ self.headers = 1
+ self.done = 0
+ self.buf = ''
+
+ def read(self, bytes):
+ if self.done:
+ return ''
+ data = self.f.read(bytes)
+ if not data:
+ return data
+ if self.headers:
+ df, sz = data.find('\r\n\r\n'), 4
+ if df == -1:
+ df, sz = data.find('\n\n'), 2
+ if df != -1:
+ df += sz
+ val = data[:df]
+ data = data[df:]
+ self.linecount = 1
+ self.headers = 0
+ else:
+ val = ''
+ if self.linecount > 0:
+ dsplit = (self.buf+data).split('\n')
+ self.buf = dsplit[-1]
+ for ln in dsplit[:-1]:
+ if self.linecount > self.n:
+ self.done = 1
+ return val
+ val += (ln + '\n')
+ self.linecount += 1
+ return val
+ else:
+ return data
+
+
+
+class _POP3MessageDeleted(Exception):
+ """
+ Internal control-flow exception. Indicates the file of a deleted message
+ was requested.
+ """
+
+
+class POP3Error(Exception):
+ pass
+
+
+
+class _IteratorBuffer(object):
+ bufSize = 0
+
+ def __init__(self, write, iterable, memoryBufferSize=None):
+ """
+ Create a _IteratorBuffer.
+
+ @param write: A one-argument callable which will be invoked with a list
+ of strings which have been buffered.
+
+ @param iterable: The source of input strings as any iterable.
+
+ @param memoryBufferSize: The upper limit on buffered string length,
+ beyond which the buffer will be flushed to the writer.
+ """
+ self.lines = []
+ self.write = write
+ self.iterator = iter(iterable)
+ if memoryBufferSize is None:
+ memoryBufferSize = 2 ** 16
+ self.memoryBufferSize = memoryBufferSize
+
+
+ def __iter__(self):
+ return self
+
+
+ def next(self):
+ try:
+ v = self.iterator.next()
+ except StopIteration:
+ if self.lines:
+ self.write(self.lines)
+ # Drop some references, in case they're edges in a cycle.
+ del self.iterator, self.lines, self.write
+ raise
+ else:
+ if v is not None:
+ self.lines.append(v)
+ self.bufSize += len(v)
+ if self.bufSize > self.memoryBufferSize:
+ self.write(self.lines)
+ self.lines = []
+ self.bufSize = 0
+
+
+
+def iterateLineGenerator(proto, gen):
+ """
+ Hook the given protocol instance up to the given iterator with an
+ _IteratorBuffer and schedule the result to be exhausted via the protocol.
+
+ @type proto: L{POP3}
+ @type gen: iterator
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+ coll = _IteratorBuffer(proto.transport.writeSequence, gen)
+ return proto.schedule(coll)
+
+
+
+def successResponse(response):
+ """
+ Format the given object as a positive response.
+ """
+ response = str(response)
+ return '+OK %s\r\n' % (response,)
+
+
+
+def formatStatResponse(msgs):
+ """
+ Format the list of message sizes appropriately for a STAT response.
+
+ Yields None until it finishes computing a result, then yields a str
+ instance that is suitable for use as a response to the STAT command.
+ Intended to be used with a L{twisted.internet.task.Cooperator}.
+ """
+ i = 0
+ bytes = 0
+ for size in msgs:
+ i += 1
+ bytes += size
+ yield None
+ yield successResponse('%d %d' % (i, bytes))
+
+
+
+def formatListLines(msgs):
+ """
+ Format a list of message sizes appropriately for the lines of a LIST
+ response.
+
+ Yields str instances formatted appropriately for use as lines in the
+ response to the LIST command. Does not include the trailing '.'.
+ """
+ i = 0
+ for size in msgs:
+ i += 1
+ yield '%d %d\r\n' % (i, size)
+
+
+
+def formatListResponse(msgs):
+ """
+ Format a list of message sizes appropriately for a complete LIST response.
+
+ Yields str instances formatted appropriately for use as a LIST command
+ response.
+ """
+ yield successResponse(len(msgs))
+ for ele in formatListLines(msgs):
+ yield ele
+ yield '.\r\n'
+
+
+
+def formatUIDListLines(msgs, getUidl):
+ """
+ Format the list of message sizes appropriately for the lines of a UIDL
+ response.
+
+ Yields str instances formatted appropriately for use as lines in the
+ response to the UIDL command. Does not include the trailing '.'.
+ """
+ for i, m in enumerate(msgs):
+ if m is not None:
+ uid = getUidl(i)
+ yield '%d %s\r\n' % (i + 1, uid)
+
+
+
+def formatUIDListResponse(msgs, getUidl):
+ """
+ Format a list of message sizes appropriately for a complete UIDL response.
+
+ Yields str instances formatted appropriately for use as a UIDL command
+ response.
+ """
+ yield successResponse('')
+ for ele in formatUIDListLines(msgs, getUidl):
+ yield ele
+ yield '.\r\n'
+
+
+
+class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """
+ POP3 server protocol implementation.
+
+ @ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we
+ will authenticate through.
+
+ @ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to
+ determine some extended behavior of the server.
+
+ @ivar timeOut: An integer which defines the minimum amount of time which
+ may elapse without receiving any traffic after which the client will be
+ disconnected.
+
+ @ivar schedule: A one-argument callable which should behave like
+ L{twisted.internet.task.coiterate}.
+ """
+ implements(interfaces.IProducer)
+
+ magic = None
+ _userIs = None
+ _onLogout = None
+
+ AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
+
+ portal = None
+ factory = None
+
+ # The mailbox we're serving
+ mbox = None
+
+ # Set this pretty low -- POP3 clients are expected to log in, download
+ # everything, and log out.
+ timeOut = 300
+
+ # Current protocol state
+ state = "COMMAND"
+
+ # PIPELINE
+ blocked = None
+
+ # Cooperate and suchlike.
+ schedule = staticmethod(task.coiterate)
+
+ # Message index of the highest retrieved message.
+ _highest = 0
+
+ def connectionMade(self):
+ if self.magic is None:
+ self.magic = self.generateMagic()
+ self.successResponse(self.magic)
+ self.setTimeout(self.timeOut)
+ if getattr(self.factory, 'noisy', True):
+ log.msg("New connection from " + str(self.transport.getPeer()))
+
+
+ def connectionLost(self, reason):
+ if self._onLogout is not None:
+ self._onLogout()
+ self._onLogout = None
+ self.setTimeout(None)
+
+
+ def generateMagic(self):
+ return smtp.messageid()
+
+
+ def successResponse(self, message=''):
+ self.transport.write(successResponse(message))
+
+ def failResponse(self, message=''):
+ self.sendLine('-ERR ' + str(message))
+
+# def sendLine(self, line):
+# print 'S:', repr(line)
+# basic.LineOnlyReceiver.sendLine(self, line)
+
+ def lineReceived(self, line):
+# print 'C:', repr(line)
+ self.resetTimeout()
+ getattr(self, 'state_' + self.state)(line)
+
+ def _unblock(self, _):
+ commands = self.blocked
+ self.blocked = None
+ while commands and self.blocked is None:
+ cmd, args = commands.pop(0)
+ self.processCommand(cmd, *args)
+ if self.blocked is not None:
+ self.blocked.extend(commands)
+
+ def state_COMMAND(self, line):
+ try:
+ return self.processCommand(*line.split(' '))
+ except (ValueError, AttributeError, POP3Error, TypeError), e:
+ log.err()
+ self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e))
+
+ def processCommand(self, command, *args):
+ if self.blocked is not None:
+ self.blocked.append((command, args))
+ return
+
+ command = command.upper()
+ authCmd = command in self.AUTH_CMDS
+ if not self.mbox and not authCmd:
+ raise POP3Error("not authenticated yet: cannot do " + command)
+ f = getattr(self, 'do_' + command, None)
+ if f:
+ return f(*args)
+ raise POP3Error("Unknown protocol command: " + command)
+
+
+ def listCapabilities(self):
+ baseCaps = [
+ "TOP",
+ "USER",
+ "UIDL",
+ "PIPELINE",
+ "CELERITY",
+ "AUSPEX",
+ "POTENCE",
+ ]
+
+ if IServerFactory.providedBy(self.factory):
+ # Oh my god. We can't just loop over a list of these because
+ # each has spectacularly different return value semantics!
+ try:
+ v = self.factory.cap_IMPLEMENTATION()
+ except NotImplementedError:
+ pass
+ except:
+ log.err()
+ else:
+ baseCaps.append("IMPLEMENTATION " + str(v))
+
+ try:
+ v = self.factory.cap_EXPIRE()
+ except NotImplementedError:
+ pass
+ except:
+ log.err()
+ else:
+ if v is None:
+ v = "NEVER"
+ if self.factory.perUserExpiration():
+ if self.mbox:
+ v = str(self.mbox.messageExpiration)
+ else:
+ v = str(v) + " USER"
+ v = str(v)
+ baseCaps.append("EXPIRE " + v)
+
+ try:
+ v = self.factory.cap_LOGIN_DELAY()
+ except NotImplementedError:
+ pass
+ except:
+ log.err()
+ else:
+ if self.factory.perUserLoginDelay():
+ if self.mbox:
+ v = str(self.mbox.loginDelay)
+ else:
+ v = str(v) + " USER"
+ v = str(v)
+ baseCaps.append("LOGIN-DELAY " + v)
+
+ try:
+ v = self.factory.challengers
+ except AttributeError:
+ pass
+ except:
+ log.err()
+ else:
+ baseCaps.append("SASL " + ' '.join(v.keys()))
+ return baseCaps
+
+ def do_CAPA(self):
+ self.successResponse("I can do the following:")
+ for cap in self.listCapabilities():
+ self.sendLine(cap)
+ self.sendLine(".")
+
+ def do_AUTH(self, args=None):
+ if not getattr(self.factory, 'challengers', None):
+ self.failResponse("AUTH extension unsupported")
+ return
+
+ if args is None:
+ self.successResponse("Supported authentication methods:")
+ for a in self.factory.challengers:
+ self.sendLine(a.upper())
+ self.sendLine(".")
+ return
+
+ auth = self.factory.challengers.get(args.strip().upper())
+ if not self.portal or not auth:
+ self.failResponse("Unsupported SASL selected")
+ return
+
+ self._auth = auth()
+ chal = self._auth.getChallenge()
+
+ self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n'))
+ self.state = 'AUTH'
+
+ def state_AUTH(self, line):
+ self.state = "COMMAND"
+ try:
+ parts = base64.decodestring(line).split(None, 1)
+ except binascii.Error:
+ self.failResponse("Invalid BASE64 encoding")
+ else:
+ if len(parts) != 2:
+ self.failResponse("Invalid AUTH response")
+ return
+ self._auth.username = parts[0]
+ self._auth.response = parts[1]
+ d = self.portal.login(self._auth, None, IMailbox)
+ d.addCallback(self._cbMailbox, parts[0])
+ d.addErrback(self._ebMailbox)
+ d.addErrback(self._ebUnexpected)
+
+ def do_APOP(self, user, digest):
+ d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest)
+ d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
+ ).addErrback(self._ebUnexpected)
+
+ def _cbMailbox(self, (interface, avatar, logout), user):
+ if interface is not IMailbox:
+ self.failResponse('Authentication failed')
+ log.err("_cbMailbox() called with an interface other than IMailbox")
+ return
+
+ self.mbox = avatar
+ self._onLogout = logout
+ self.successResponse('Authentication succeeded')
+ if getattr(self.factory, 'noisy', True):
+ log.msg("Authenticated login for " + user)
+
+ def _ebMailbox(self, failure):
+ failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed)
+ if issubclass(failure, cred.error.LoginDenied):
+ self.failResponse("Access denied: " + str(failure))
+ elif issubclass(failure, cred.error.LoginFailed):
+ self.failResponse('Authentication failed')
+ if getattr(self.factory, 'noisy', True):
+ log.msg("Denied login attempt from " + str(self.transport.getPeer()))
+
+ def _ebUnexpected(self, failure):
+ self.failResponse('Server error: ' + failure.getErrorMessage())
+ log.err(failure)
+
+ def do_USER(self, user):
+ self._userIs = user
+ self.successResponse('USER accepted, send PASS')
+
+ def do_PASS(self, password):
+ if self._userIs is None:
+ self.failResponse("USER required before PASS")
+ return
+ user = self._userIs
+ self._userIs = None
+ d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
+ d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
+ ).addErrback(self._ebUnexpected)
+
+
+ def _longOperation(self, d):
+ # Turn off timeouts and block further processing until the Deferred
+ # fires, then reverse those changes.
+ timeOut = self.timeOut
+ self.setTimeout(None)
+ self.blocked = []
+ d.addCallback(self._unblock)
+ d.addCallback(lambda ign: self.setTimeout(timeOut))
+ return d
+
+
+ def _coiterate(self, gen):
+ return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen))
+
+
+ def do_STAT(self):
+ d = defer.maybeDeferred(self.mbox.listMessages)
+ def cbMessages(msgs):
+ return self._coiterate(formatStatResponse(msgs))
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_STAT failure:")
+ log.err(err)
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+
+
+ def do_LIST(self, i=None):
+ if i is None:
+ d = defer.maybeDeferred(self.mbox.listMessages)
+ def cbMessages(msgs):
+ return self._coiterate(formatListResponse(msgs))
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_LIST failure:")
+ log.err(err)
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+ else:
+ try:
+ i = int(i)
+ if i < 1:
+ raise ValueError()
+ except ValueError:
+ self.failResponse("Invalid message-number: %r" % (i,))
+ else:
+ d = defer.maybeDeferred(self.mbox.listMessages, i - 1)
+ def cbMessage(msg):
+ self.successResponse('%d %d' % (i, msg))
+ def ebMessage(err):
+ errcls = err.check(ValueError, IndexError)
+ if errcls is not None:
+ if errcls is IndexError:
+ # IndexError was supported for a while, but really
+ # shouldn't be. One error condition, one exception
+ # type.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.listMessages may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.",
+ PendingDeprecationWarning)
+ self.failResponse("Invalid message-number: %r" % (i,))
+ else:
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_LIST failure:")
+ log.err(err)
+ return self._longOperation(d.addCallbacks(cbMessage, ebMessage))
+
+
+ def do_UIDL(self, i=None):
+ if i is None:
+ d = defer.maybeDeferred(self.mbox.listMessages)
+ def cbMessages(msgs):
+ return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl))
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_UIDL failure:")
+ log.err(err)
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+ else:
+ try:
+ i = int(i)
+ if i < 1:
+ raise ValueError()
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ else:
+ try:
+ msg = self.mbox.getUidl(i - 1)
+ except IndexError:
+ # XXX TODO See above comment regarding IndexError.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.getUidl may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.",
+ PendingDeprecationWarning)
+ self.failResponse("Bad message number argument")
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ else:
+ self.successResponse(str(msg))
+
+
+ def _getMessageFile(self, i):
+ """
+ Retrieve the size and contents of a given message, as a two-tuple.
+
+ @param i: The number of the message to operate on. This is a base-ten
+ string representation starting at 1.
+
+ @return: A Deferred which fires with a two-tuple of an integer and a
+ file-like object.
+ """
+ try:
+ msg = int(i) - 1
+ if msg < 0:
+ raise ValueError()
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ return defer.succeed(None)
+
+ sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg)
+ def cbMessageSize(size):
+ if not size:
+ return defer.fail(_POP3MessageDeleted())
+ fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg)
+ fileDeferred.addCallback(lambda fObj: (size, fObj))
+ return fileDeferred
+
+ def ebMessageSomething(err):
+ errcls = err.check(_POP3MessageDeleted, ValueError, IndexError)
+ if errcls is _POP3MessageDeleted:
+ self.failResponse("message deleted")
+ elif errcls in (ValueError, IndexError):
+ if errcls is IndexError:
+ # XXX TODO See above comment regarding IndexError.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.listMessages may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.",
+ PendingDeprecationWarning)
+ self.failResponse("Bad message number argument")
+ else:
+ log.msg("Unexpected _getMessageFile failure:")
+ log.err(err)
+ return None
+
+ sizeDeferred.addCallback(cbMessageSize)
+ sizeDeferred.addErrback(ebMessageSomething)
+ return sizeDeferred
+
+
+ def _sendMessageContent(self, i, fpWrapper, successResponse):
+ d = self._getMessageFile(i)
+ def cbMessageFile(info):
+ if info is None:
+ # Some error occurred - a failure response has been sent
+ # already, just give up.
+ return
+
+ self._highest = max(self._highest, int(i))
+ resp, fp = info
+ fp = fpWrapper(fp)
+ self.successResponse(successResponse(resp))
+ s = basic.FileSender()
+ d = s.beginFileTransfer(fp, self.transport, self.transformChunk)
+
+ def cbFileTransfer(lastsent):
+ if lastsent != '\n':
+ line = '\r\n.'
+ else:
+ line = '.'
+ self.sendLine(line)
+
+ def ebFileTransfer(err):
+ self.transport.loseConnection()
+ log.msg("Unexpected error in _sendMessageContent:")
+ log.err(err)
+
+ d.addCallback(cbFileTransfer)
+ d.addErrback(ebFileTransfer)
+ return d
+ return self._longOperation(d.addCallback(cbMessageFile))
+
+
+ def do_TOP(self, i, size):
+ try:
+ size = int(size)
+ if size < 0:
+ raise ValueError
+ except ValueError:
+ self.failResponse("Bad line count argument")
+ else:
+ return self._sendMessageContent(
+ i,
+ lambda fp: _HeadersPlusNLines(fp, size),
+ lambda size: "Top of message follows")
+
+
+ def do_RETR(self, i):
+ return self._sendMessageContent(
+ i,
+ lambda fp: fp,
+ lambda size: "%d" % (size,))
+
+
+ def transformChunk(self, chunk):
+ return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
+
+
+ def finishedFileTransfer(self, lastsent):
+ if lastsent != '\n':
+ line = '\r\n.'
+ else:
+ line = '.'
+ self.sendLine(line)
+
+
+ def do_DELE(self, i):
+ i = int(i)-1
+ self.mbox.deleteMessage(i)
+ self.successResponse()
+
+
+ def do_NOOP(self):
+ """Perform no operation. Return a success code"""
+ self.successResponse()
+
+
+ def do_RSET(self):
+ """Unset all deleted message flags"""
+ try:
+ self.mbox.undeleteMessages()
+ except:
+ log.err()
+ self.failResponse()
+ else:
+ self._highest = 0
+ self.successResponse()
+
+
+ def do_LAST(self):
+ """
+ Return the index of the highest message yet downloaded.
+ """
+ self.successResponse(self._highest)
+
+
+ def do_RPOP(self, user):
+ self.failResponse('permission denied, sucker')
+
+
+ def do_QUIT(self):
+ if self.mbox:
+ self.mbox.sync()
+ self.successResponse()
+ self.transport.loseConnection()
+
+
+ def authenticateUserAPOP(self, user, digest):
+ """Perform authentication of an APOP login.
+
+ @type user: C{str}
+ @param user: The name of the user attempting to log in.
+
+ @type digest: C{str}
+ @param digest: The response string with which the user replied.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the login is
+ successful, and whose errback will be invoked otherwise. The
+ callback will be passed a 3-tuple consisting of IMailbox,
+ an object implementing IMailbox, and a zero-argument callable
+ to be invoked when this session is terminated.
+ """
+ if self.portal is not None:
+ return self.portal.login(
+ APOPCredentials(self.magic, user, digest),
+ None,
+ IMailbox
+ )
+ raise cred.error.UnauthorizedLogin()
+
+ def authenticateUserPASS(self, user, password):
+ """Perform authentication of a username/password login.
+
+ @type user: C{str}
+ @param user: The name of the user attempting to log in.
+
+ @type password: C{str}
+ @param password: The password to attempt to authenticate with.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback is invoked if the login is
+ successful, and whose errback will be invoked otherwise. The
+ callback will be passed a 3-tuple consisting of IMailbox,
+ an object implementing IMailbox, and a zero-argument callable
+ to be invoked when this session is terminated.
+ """
+ if self.portal is not None:
+ return self.portal.login(
+ cred.credentials.UsernamePassword(user, password),
+ None,
+ IMailbox
+ )
+ raise cred.error.UnauthorizedLogin()
+
+
+class IServerFactory(Interface):
+ """Interface for querying additional parameters of this POP3 server.
+
+ Any cap_* method may raise NotImplementedError if the particular
+ capability is not supported. If cap_EXPIRE() does not raise
+ NotImplementedError, perUserExpiration() must be implemented, otherwise
+ they are optional. If cap_LOGIN_DELAY() is implemented,
+ perUserLoginDelay() must be implemented, otherwise they are optional.
+
+ @ivar challengers: A dictionary mapping challenger names to classes
+ implementing C{IUsernameHashedPassword}.
+ """
+
+ def cap_IMPLEMENTATION():
+ """Return a string describing this POP3 server implementation."""
+
+ def cap_EXPIRE():
+ """Return the minimum number of days messages are retained."""
+
+ def perUserExpiration():
+ """Indicate whether message expiration is per-user.
+
+ @return: True if it is, false otherwise.
+ """
+
+ def cap_LOGIN_DELAY():
+ """Return the minimum number of seconds between client logins."""
+
+ def perUserLoginDelay():
+ """Indicate whether the login delay period is per-user.
+
+ @return: True if it is, false otherwise.
+ """
+
+class IMailbox(Interface):
+ """
+ @type loginDelay: C{int}
+ @ivar loginDelay: The number of seconds between allowed logins for the
+ user associated with this mailbox. None
+
+ @type messageExpiration: C{int}
+ @ivar messageExpiration: The number of days messages in this mailbox will
+ remain on the server before being deleted.
+ """
+
+ def listMessages(index=None):
+ """Retrieve the size of one or more messages.
+
+ @type index: C{int} or C{None}
+ @param index: The number of the message for which to retrieve the
+ size (starting at 0), or None to retrieve the size of all messages.
+
+ @rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
+ with one of these.
+
+ @return: The number of octets in the specified message, or an iterable
+ of integers representing the number of octets in all the messages. Any
+ value which would have referred to a deleted message should be set to 0.
+
+ @raise ValueError: if C{index} is greater than the index of any message
+ in the mailbox.
+ """
+
+ def getMessage(index):
+ """Retrieve a file-like object for a particular message.
+
+ @type index: C{int}
+ @param index: The number of the message to retrieve
+
+ @rtype: A file-like object
+ @return: A file containing the message data with lines delimited by
+ C{\\n}.
+ """
+
+ def getUidl(index):
+ """Get a unique identifier for a particular message.
+
+ @type index: C{int}
+ @param index: The number of the message for which to retrieve a UIDL
+
+ @rtype: C{str}
+ @return: A string of printable characters uniquely identifying for all
+ time the specified message.
+
+ @raise ValueError: if C{index} is greater than the index of any message
+ in the mailbox.
+ """
+
+ def deleteMessage(index):
+ """Delete a particular message.
+
+ This must not change the number of messages in this mailbox. Further
+ requests for the size of deleted messages should return 0. Further
+ requests for the message itself may raise an exception.
+
+ @type index: C{int}
+ @param index: The number of the message to delete.
+ """
+
+ def undeleteMessages():
+ """
+ Undelete any messages which have been marked for deletion since the
+ most recent L{sync} call.
+
+ Any message which can be undeleted should be returned to its
+ original position in the message sequence and retain its original
+ UID.
+ """
+
+ def sync():
+ """Perform checkpointing.
+
+ This method will be called to indicate the mailbox should attempt to
+ clean up any remaining deleted messages.
+ """
+
+
+
+class Mailbox:
+ implements(IMailbox)
+
+ def listMessages(self, i=None):
+ return []
+ def getMessage(self, i):
+ raise ValueError
+ def getUidl(self, i):
+ raise ValueError
+ def deleteMessage(self, i):
+ raise ValueError
+ def undeleteMessages(self):
+ pass
+ def sync(self):
+ pass
+
+
+NONE, SHORT, FIRST_LONG, LONG = range(4)
+
+NEXT = {}
+NEXT[NONE] = NONE
+NEXT[SHORT] = NONE
+NEXT[FIRST_LONG] = LONG
+NEXT[LONG] = NONE
+
+class POP3Client(basic.LineOnlyReceiver):
+
+ mode = SHORT
+ command = 'WELCOME'
+ import re
+ welcomeRe = re.compile('<(.*)>')
+
+ def __init__(self):
+ import warnings
+ warnings.warn("twisted.mail.pop3.POP3Client is deprecated, "
+ "please use twisted.mail.pop3.AdvancedPOP3Client "
+ "instead.", DeprecationWarning,
+ stacklevel=3)
+
+ def sendShort(self, command, params=None):
+ if params is not None:
+ self.sendLine('%s %s' % (command, params))
+ else:
+ self.sendLine(command)
+ self.command = command
+ self.mode = SHORT
+
+ def sendLong(self, command, params):
+ if params:
+ self.sendLine('%s %s' % (command, params))
+ else:
+ self.sendLine(command)
+ self.command = command
+ self.mode = FIRST_LONG
+
+ def handle_default(self, line):
+ if line[:-4] == '-ERR':
+ self.mode = NONE
+
+ def handle_WELCOME(self, line):
+ code, data = line.split(' ', 1)
+ if code != '+OK':
+ self.transport.loseConnection()
+ else:
+ m = self.welcomeRe.match(line)
+ if m:
+ self.welcomeCode = m.group(1)
+
+ def _dispatch(self, command, default, *args):
+ try:
+ method = getattr(self, 'handle_'+command, default)
+ if method is not None:
+ method(*args)
+ except:
+ log.err()
+
+ def lineReceived(self, line):
+ if self.mode == SHORT or self.mode == FIRST_LONG:
+ self.mode = NEXT[self.mode]
+ self._dispatch(self.command, self.handle_default, line)
+ elif self.mode == LONG:
+ if line == '.':
+ self.mode = NEXT[self.mode]
+ self._dispatch(self.command+'_end', None)
+ return
+ if line[:1] == '.':
+ line = line[1:]
+ self._dispatch(self.command+"_continue", None, line)
+
+ def apopAuthenticate(self, user, password, magic):
+ digest = md5(magic + password).hexdigest()
+ self.apop(user, digest)
+
+ def apop(self, user, digest):
+ self.sendLong('APOP', ' '.join((user, digest)))
+ def retr(self, i):
+ self.sendLong('RETR', i)
+ def dele(self, i):
+ self.sendShort('DELE', i)
+ def list(self, i=''):
+ self.sendLong('LIST', i)
+ def uidl(self, i=''):
+ self.sendLong('UIDL', i)
+ def user(self, name):
+ self.sendShort('USER', name)
+ def pass_(self, pass_):
+ self.sendShort('PASS', pass_)
+ def quit(self):
+ self.sendShort('QUIT')
+
+from twisted.mail.pop3client import POP3Client as AdvancedPOP3Client
+from twisted.mail.pop3client import POP3ClientError
+from twisted.mail.pop3client import InsecureAuthenticationDisallowed
+from twisted.mail.pop3client import ServerErrorResponse
+from twisted.mail.pop3client import LineTooLong
+
+__all__ = [
+ # Interfaces
+ 'IMailbox', 'IServerFactory',
+
+ # Exceptions
+ 'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed',
+ 'ServerErrorResponse', 'LineTooLong',
+
+ # Protocol classes
+ 'POP3', 'POP3Client', 'AdvancedPOP3Client',
+
+ # Misc
+ 'APOPCredentials', 'Mailbox']
diff --git a/twisted/mail/pop3client.py b/twisted/mail/pop3client.py
new file mode 100644
index 0000000..fe8f497
--- /dev/null
+++ b/twisted/mail/pop3client.py
@@ -0,0 +1,706 @@
+# -*- test-case-name: twisted.mail.test.test_pop3client -*-
+# Copyright (c) 2001-2004 Divmod Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+POP3 client protocol implementation
+
+Don't use this module directly. Use twisted.mail.pop3 instead.
+
+@author: Jp Calderone
+"""
+
+import re
+
+from twisted.python import log
+from twisted.python.hashlib import md5
+from twisted.internet import defer
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import error
+from twisted.internet import interfaces
+
+OK = '+OK'
+ERR = '-ERR'
+
+class POP3ClientError(Exception):
+ """Base class for all exceptions raised by POP3Client.
+ """
+
+class InsecureAuthenticationDisallowed(POP3ClientError):
+ """Secure authentication was required but no mechanism could be found.
+ """
+
+class TLSError(POP3ClientError):
+ """
+ Secure authentication was required but either the transport does
+ not support TLS or no TLS context factory was supplied.
+ """
+
+class TLSNotSupportedError(POP3ClientError):
+ """
+ Secure authentication was required but the server does not support
+ TLS.
+ """
+
+class ServerErrorResponse(POP3ClientError):
+ """The server returned an error response to a request.
+ """
+ def __init__(self, reason, consumer=None):
+ POP3ClientError.__init__(self, reason)
+ self.consumer = consumer
+
+class LineTooLong(POP3ClientError):
+ """The server sent an extremely long line.
+ """
+
+class _ListSetter:
+ # Internal helper. POP3 responses sometimes occur in the
+ # form of a list of lines containing two pieces of data,
+ # a message index and a value of some sort. When a message
+ # is deleted, it is omitted from these responses. The
+ # setitem method of this class is meant to be called with
+ # these two values. In the cases where indexes are skipped,
+ # it takes care of padding out the missing values with None.
+ def __init__(self, L):
+ self.L = L
+ def setitem(self, (item, value)):
+ diff = item - len(self.L) + 1
+ if diff > 0:
+ self.L.extend([None] * diff)
+ self.L[item] = value
+
+
+def _statXform(line):
+ # Parse a STAT response
+ numMsgs, totalSize = line.split(None, 1)
+ return int(numMsgs), int(totalSize)
+
+
+def _listXform(line):
+ # Parse a LIST response
+ index, size = line.split(None, 1)
+ return int(index) - 1, int(size)
+
+
+def _uidXform(line):
+ # Parse a UIDL response
+ index, uid = line.split(None, 1)
+ return int(index) - 1, uid
+
+def _codeStatusSplit(line):
+ # Parse an +OK or -ERR response
+ parts = line.split(' ', 1)
+ if len(parts) == 1:
+ return parts[0], ''
+ return parts
+
+def _dotUnquoter(line):
+ """
+ C{'.'} characters which begin a line of a message are doubled to avoid
+ confusing with the terminating C{'.\\r\\n'} sequence. This function
+ unquotes them.
+ """
+ if line.startswith('..'):
+ return line[1:]
+ return line
+
+class POP3Client(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """POP3 client protocol implementation class
+
+ Instances of this class provide a convenient, efficient API for
+ retrieving and deleting messages from a POP3 server.
+
+ @type startedTLS: C{bool}
+ @ivar startedTLS: Whether TLS has been negotiated successfully.
+
+
+ @type allowInsecureLogin: C{bool}
+ @ivar allowInsecureLogin: Indicate whether login() should be
+ allowed if the server offers no authentication challenge and if
+ our transport does not offer any protection via encryption.
+
+ @type serverChallenge: C{str} or C{None}
+ @ivar serverChallenge: Challenge received from the server
+
+ @type timeout: C{int}
+ @ivar timeout: Number of seconds to wait before timing out a
+ connection. If the number is <= 0, no timeout checking will be
+ performed.
+ """
+
+ startedTLS = False
+ allowInsecureLogin = False
+ timeout = 0
+ serverChallenge = None
+
+ # Capabilities are not allowed to change during the session
+ # (except when TLS is negotiated), so cache the first response and
+ # use that for all later lookups
+ _capCache = None
+
+ # Regular expression to search for in the challenge string in the server
+ # greeting line.
+ _challengeMagicRe = re.compile('(<[^>]+>)')
+
+ # List of pending calls.
+ # We are a pipelining API but don't actually
+ # support pipelining on the network yet.
+ _blockedQueue = None
+
+ # The Deferred to which the very next result will go.
+ _waiting = None
+
+ # Whether we dropped the connection because of a timeout
+ _timedOut = False
+
+ # If the server sends an initial -ERR, this is the message it sent
+ # with it.
+ _greetingError = None
+
+ def _blocked(self, f, *a):
+ # Internal helper. If commands are being blocked, append
+ # the given command and arguments to a list and return a Deferred
+ # that will be chained with the return value of the function
+ # when it eventually runs. Otherwise, set up for commands to be
+
+ # blocked and return None.
+ if self._blockedQueue is not None:
+ d = defer.Deferred()
+ self._blockedQueue.append((d, f, a))
+ return d
+ self._blockedQueue = []
+ return None
+
+ def _unblock(self):
+ # Internal helper. Indicate that a function has completed.
+ # If there are blocked commands, run the next one. If there
+ # are not, set up for the next command to not be blocked.
+ if self._blockedQueue == []:
+ self._blockedQueue = None
+ elif self._blockedQueue is not None:
+ _blockedQueue = self._blockedQueue
+ self._blockedQueue = None
+
+ d, f, a = _blockedQueue.pop(0)
+ d2 = f(*a)
+ d2.chainDeferred(d)
+ # f is a function which uses _blocked (otherwise it wouldn't
+ # have gotten into the blocked queue), which means it will have
+ # re-set _blockedQueue to an empty list, so we can put the rest
+ # of the blocked queue back into it now.
+ self._blockedQueue.extend(_blockedQueue)
+
+
+ def sendShort(self, cmd, args):
+ # Internal helper. Send a command to which a short response
+ # is expected. Return a Deferred that fires when the response
+ # is received. Block all further commands from being sent until
+ # the response is received. Transition the state to SHORT.
+ d = self._blocked(self.sendShort, cmd, args)
+ if d is not None:
+ return d
+
+ if args:
+ self.sendLine(cmd + ' ' + args)
+ else:
+ self.sendLine(cmd)
+ self.state = 'SHORT'
+ self._waiting = defer.Deferred()
+ return self._waiting
+
+ def sendLong(self, cmd, args, consumer, xform):
+ # Internal helper. Send a command to which a multiline
+ # response is expected. Return a Deferred that fires when
+ # the entire response is received. Block all further commands
+ # from being sent until the entire response is received.
+ # Transition the state to LONG_INITIAL.
+ d = self._blocked(self.sendLong, cmd, args, consumer, xform)
+ if d is not None:
+ return d
+
+ if args:
+ self.sendLine(cmd + ' ' + args)
+ else:
+ self.sendLine(cmd)
+ self.state = 'LONG_INITIAL'
+ self._xform = xform
+ self._consumer = consumer
+ self._waiting = defer.Deferred()
+ return self._waiting
+
+ # Twisted protocol callback
+ def connectionMade(self):
+ if self.timeout > 0:
+ self.setTimeout(self.timeout)
+
+ self.state = 'WELCOME'
+ self._blockedQueue = []
+
+ def timeoutConnection(self):
+ self._timedOut = True
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ if self.timeout > 0:
+ self.setTimeout(None)
+
+ if self._timedOut:
+ reason = error.TimeoutError()
+ elif self._greetingError:
+ reason = ServerErrorResponse(self._greetingError)
+
+ d = []
+ if self._waiting is not None:
+ d.append(self._waiting)
+ self._waiting = None
+ if self._blockedQueue is not None:
+ d.extend([deferred for (deferred, f, a) in self._blockedQueue])
+ self._blockedQueue = None
+ for w in d:
+ w.errback(reason)
+
+ def lineReceived(self, line):
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ state = self.state
+ self.state = None
+ state = getattr(self, 'state_' + state)(line) or state
+ if self.state is None:
+ self.state = state
+
+ def lineLengthExceeded(self, buffer):
+ # XXX - We need to be smarter about this
+ if self._waiting is not None:
+ waiting, self._waiting = self._waiting, None
+ waiting.errback(LineTooLong())
+ self.transport.loseConnection()
+
+ # POP3 Client state logic - don't touch this.
+ def state_WELCOME(self, line):
+ # WELCOME is the first state. The server sends one line of text
+ # greeting us, possibly with an APOP challenge. Transition the
+ # state to WAITING.
+ code, status = _codeStatusSplit(line)
+ if code != OK:
+ self._greetingError = status
+ self.transport.loseConnection()
+ else:
+ m = self._challengeMagicRe.search(status)
+
+ if m is not None:
+ self.serverChallenge = m.group(1)
+
+ self.serverGreeting(status)
+
+ self._unblock()
+ return 'WAITING'
+
+ def state_WAITING(self, line):
+ # The server isn't supposed to send us anything in this state.
+ log.msg("Illegal line from server: " + repr(line))
+
+ def state_SHORT(self, line):
+ # This is the state we are in when waiting for a single
+ # line response. Parse it and fire the appropriate callback
+ # or errback. Transition the state back to WAITING.
+ deferred, self._waiting = self._waiting, None
+ self._unblock()
+ code, status = _codeStatusSplit(line)
+ if code == OK:
+ deferred.callback(status)
+ else:
+ deferred.errback(ServerErrorResponse(status))
+ return 'WAITING'
+
+ def state_LONG_INITIAL(self, line):
+ # This is the state we are in when waiting for the first
+ # line of a long response. Parse it and transition the
+ # state to LONG if it is an okay response; if it is an
+ # error response, fire an errback, clean up the things
+ # waiting for a long response, and transition the state
+ # to WAITING.
+ code, status = _codeStatusSplit(line)
+ if code == OK:
+ return 'LONG'
+ consumer = self._consumer
+ deferred = self._waiting
+ self._consumer = self._waiting = self._xform = None
+ self._unblock()
+ deferred.errback(ServerErrorResponse(status, consumer))
+ return 'WAITING'
+
+ def state_LONG(self, line):
+ # This is the state for each line of a long response.
+ # If it is the last line, finish things, fire the
+ # Deferred, and transition the state to WAITING.
+ # Otherwise, pass the line to the consumer.
+ if line == '.':
+ consumer = self._consumer
+ deferred = self._waiting
+ self._consumer = self._waiting = self._xform = None
+ self._unblock()
+ deferred.callback(consumer)
+ return 'WAITING'
+ else:
+ if self._xform is not None:
+ self._consumer(self._xform(line))
+ else:
+ self._consumer(line)
+ return 'LONG'
+
+
+ # Callbacks - override these
+ def serverGreeting(self, greeting):
+ """Called when the server has sent us a greeting.
+
+ @type greeting: C{str} or C{None}
+ @param greeting: The status message sent with the server
+ greeting. For servers implementing APOP authentication, this
+ will be a challenge string. .
+ """
+
+
+ # External API - call these (most of 'em anyway)
+ def startTLS(self, contextFactory=None):
+ """
+ Initiates a 'STLS' request and negotiates the TLS / SSL
+ Handshake.
+
+ @type contextFactory: C{ssl.ClientContextFactory} @param
+ contextFactory: The context factory with which to negotiate
+ TLS. If C{None}, try to create a new one.
+
+ @return: A Deferred which fires when the transport has been
+ secured according to the given contextFactory, or which fails
+ if the transport cannot be secured.
+ """
+ tls = interfaces.ITLSTransport(self.transport, None)
+ if tls is None:
+ return defer.fail(TLSError(
+ "POP3Client transport does not implement "
+ "interfaces.ITLSTransport"))
+
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ if contextFactory is None:
+ return defer.fail(TLSError(
+ "POP3Client requires a TLS context to "
+ "initiate the STLS handshake"))
+
+ d = self.capabilities()
+ d.addCallback(self._startTLS, contextFactory, tls)
+ return d
+
+
+ def _startTLS(self, caps, contextFactory, tls):
+ assert not self.startedTLS, "Client and Server are currently communicating via TLS"
+
+ if 'STLS' not in caps:
+ return defer.fail(TLSNotSupportedError(
+ "Server does not support secure communication "
+ "via TLS / SSL"))
+
+ d = self.sendShort('STLS', None)
+ d.addCallback(self._startedTLS, contextFactory, tls)
+ d.addCallback(lambda _: self.capabilities())
+ return d
+
+
+ def _startedTLS(self, result, context, tls):
+ self.transport = tls
+ self.transport.startTLS(context)
+ self._capCache = None
+ self.startedTLS = True
+ return result
+
+
+ def _getContextFactory(self):
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ return None
+ else:
+ context = ssl.ClientContextFactory()
+ context.method = ssl.SSL.TLSv1_METHOD
+ return context
+
+
+ def login(self, username, password):
+ """Log into the server.
+
+ If APOP is available it will be used. Otherwise, if TLS is
+ available an 'STLS' session will be started and plaintext
+ login will proceed. Otherwise, if the instance attribute
+ allowInsecureLogin is set to True, insecure plaintext login
+ will proceed. Otherwise, InsecureAuthenticationDisallowed
+ will be raised (asynchronously).
+
+ @param username: The username with which to log in.
+ @param password: The password with which to log in.
+
+ @rtype: C{Deferred}
+ @return: A deferred which fires when login has
+ completed.
+ """
+ d = self.capabilities()
+ d.addCallback(self._login, username, password)
+ return d
+
+
+ def _login(self, caps, username, password):
+ if self.serverChallenge is not None:
+ return self._apop(username, password, self.serverChallenge)
+
+ tryTLS = 'STLS' in caps
+
+ #If our transport supports switching to TLS, we might want to try to switch to TLS.
+ tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
+
+ # If our transport is not already using TLS, we might want to try to switch to TLS.
+ nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
+
+ if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
+ d = self.startTLS()
+
+ d.addCallback(self._loginTLS, username, password)
+ return d
+
+ elif self.startedTLS or not nontlsTransport or self.allowInsecureLogin:
+ return self._plaintext(username, password)
+ else:
+ return defer.fail(InsecureAuthenticationDisallowed())
+
+
+ def _loginTLS(self, res, username, password):
+ return self._plaintext(username, password)
+
+ def _plaintext(self, username, password):
+ # Internal helper. Send a username/password pair, returning a Deferred
+ # that fires when both have succeeded or fails when the server rejects
+ # either.
+ return self.user(username).addCallback(lambda r: self.password(password))
+
+ def _apop(self, username, password, challenge):
+ # Internal helper. Computes and sends an APOP response. Returns
+ # a Deferred that fires when the server responds to the response.
+ digest = md5(challenge + password).hexdigest()
+ return self.apop(username, digest)
+
+ def apop(self, username, digest):
+ """Perform APOP login.
+
+ This should be used in special circumstances only, when it is
+ known that the server supports APOP authentication, and APOP
+ authentication is absolutely required. For the common case,
+ use L{login} instead.
+
+ @param username: The username with which to log in.
+ @param digest: The challenge response to authenticate with.
+ """
+ return self.sendShort('APOP', username + ' ' + digest)
+
+ def user(self, username):
+ """Send the user command.
+
+ This performs the first half of plaintext login. Unless this
+ is absolutely required, use the L{login} method instead.
+
+ @param username: The username with which to log in.
+ """
+ return self.sendShort('USER', username)
+
+ def password(self, password):
+ """Send the password command.
+
+ This performs the second half of plaintext login. Unless this
+ is absolutely required, use the L{login} method instead.
+
+ @param password: The plaintext password with which to authenticate.
+ """
+ return self.sendShort('PASS', password)
+
+ def delete(self, index):
+ """Delete a message from the server.
+
+ @type index: C{int}
+ @param index: The index of the message to delete.
+ This is 0-based.
+
+ @rtype: C{Deferred}
+ @return: A deferred which fires when the delete command
+ is successful, or fails if the server returns an error.
+ """
+ return self.sendShort('DELE', str(index + 1))
+
+ def _consumeOrSetItem(self, cmd, args, consumer, xform):
+ # Internal helper. Send a long command. If no consumer is
+ # provided, create a consumer that puts results into a list
+ # and return a Deferred that fires with that list when it
+ # is complete.
+ if consumer is None:
+ L = []
+ consumer = _ListSetter(L).setitem
+ return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L)
+ return self.sendLong(cmd, args, consumer, xform)
+
+ def _consumeOrAppend(self, cmd, args, consumer, xform):
+ # Internal helper. Send a long command. If no consumer is
+ # provided, create a consumer that appends results to a list
+ # and return a Deferred that fires with that list when it is
+ # complete.
+ if consumer is None:
+ L = []
+ consumer = L.append
+ return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L)
+ return self.sendLong(cmd, args, consumer, xform)
+
+ def capabilities(self, useCache=True):
+ """Retrieve the capabilities supported by this server.
+
+ Not all servers support this command. If the server does not
+ support this, it is treated as though it returned a successful
+ response listing no capabilities. At some future time, this may be
+ changed to instead seek out information about a server's
+ capabilities in some other fashion (only if it proves useful to do
+ so, and only if there are servers still in use which do not support
+ CAPA but which do support POP3 extensions that are useful).
+
+ @type useCache: C{bool}
+ @param useCache: If set, and if capabilities have been
+ retrieved previously, just return the previously retrieved
+ results.
+
+ @return: A Deferred which fires with a C{dict} mapping C{str}
+ to C{None} or C{list}s of C{str}. For example::
+
+ C: CAPA
+ S: +OK Capability list follows
+ S: TOP
+ S: USER
+ S: SASL CRAM-MD5 KERBEROS_V4
+ S: RESP-CODES
+ S: LOGIN-DELAY 900
+ S: PIPELINING
+ S: EXPIRE 60
+ S: UIDL
+ S: IMPLEMENTATION Shlemazle-Plotz-v302
+ S: .
+
+ will be lead to a result of::
+
+ | {'TOP': None,
+ | 'USER': None,
+ | 'SASL': ['CRAM-MD5', 'KERBEROS_V4'],
+ | 'RESP-CODES': None,
+ | 'LOGIN-DELAY': ['900'],
+ | 'PIPELINING': None,
+ | 'EXPIRE': ['60'],
+ | 'UIDL': None,
+ | 'IMPLEMENTATION': ['Shlemazle-Plotz-v302']}
+ """
+ if useCache and self._capCache is not None:
+ return defer.succeed(self._capCache)
+
+ cache = {}
+ def consume(line):
+ tmp = line.split()
+ if len(tmp) == 1:
+ cache[tmp[0]] = None
+ elif len(tmp) > 1:
+ cache[tmp[0]] = tmp[1:]
+
+ def capaNotSupported(err):
+ err.trap(ServerErrorResponse)
+ return None
+
+ def gotCapabilities(result):
+ self._capCache = cache
+ return cache
+
+ d = self._consumeOrAppend('CAPA', None, consume, None)
+ d.addErrback(capaNotSupported).addCallback(gotCapabilities)
+ return d
+
+
+ def noop(self):
+ """Do nothing, with the help of the server.
+
+ No operation is performed. The returned Deferred fires when
+ the server responds.
+ """
+ return self.sendShort("NOOP", None)
+
+
+ def reset(self):
+ """Remove the deleted flag from any messages which have it.
+
+ The returned Deferred fires when the server responds.
+ """
+ return self.sendShort("RSET", None)
+
+
+ def retrieve(self, index, consumer=None, lines=None):
+ """Retrieve a message from the server.
+
+ If L{consumer} is not None, it will be called with
+ each line of the message as it is received. Otherwise,
+ the returned Deferred will be fired with a list of all
+ the lines when the message has been completely received.
+ """
+ idx = str(index + 1)
+ if lines is None:
+ return self._consumeOrAppend('RETR', idx, consumer, _dotUnquoter)
+
+ return self._consumeOrAppend('TOP', '%s %d' % (idx, lines), consumer, _dotUnquoter)
+
+
+ def stat(self):
+ """Get information about the size of this mailbox.
+
+ The returned Deferred will be fired with a tuple containing
+ the number or messages in the mailbox and the size (in bytes)
+ of the mailbox.
+ """
+ return self.sendShort('STAT', None).addCallback(_statXform)
+
+
+ def listSize(self, consumer=None):
+ """Retrieve a list of the size of all messages on the server.
+
+ If L{consumer} is not None, it will be called with two-tuples
+ of message index number and message size as they are received.
+ Otherwise, a Deferred which will fire with a list of B{only}
+ message sizes will be returned. For messages which have been
+ deleted, None will be used in place of the message size.
+ """
+ return self._consumeOrSetItem('LIST', None, consumer, _listXform)
+
+
+ def listUID(self, consumer=None):
+ """Retrieve a list of the UIDs of all messages on the server.
+
+ If L{consumer} is not None, it will be called with two-tuples
+ of message index number and message UID as they are received.
+ Otherwise, a Deferred which will fire with of list of B{only}
+ message UIDs will be returned. For messages which have been
+ deleted, None will be used in place of the message UID.
+ """
+ return self._consumeOrSetItem('UIDL', None, consumer, _uidXform)
+
+
+ def quit(self):
+ """Disconnect from the server.
+ """
+ return self.sendShort('QUIT', None)
+
+__all__ = [
+ # Exceptions
+ 'InsecureAuthenticationDisallowed', 'LineTooLong', 'POP3ClientError',
+ 'ServerErrorResponse', 'TLSError', 'TLSNotSupportedError',
+
+ # Protocol classes
+ 'POP3Client']
diff --git a/twisted/mail/protocols.py b/twisted/mail/protocols.py
new file mode 100644
index 0000000..c910be1
--- /dev/null
+++ b/twisted/mail/protocols.py
@@ -0,0 +1,225 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Protocol support for twisted.mail."""
+
+# twisted imports
+from twisted.mail import pop3
+from twisted.mail import smtp
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.copyright import longversion
+from twisted.python import log
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.credentials
+
+from twisted.mail import relay
+
+from zope.interface import implements
+
+
+class DomainDeliveryBase:
+ """A server that uses twisted.mail service's domains."""
+
+ implements(smtp.IMessageDelivery)
+
+ service = None
+ protocolName = None
+
+ def __init__(self, service, user, host=smtp.DNSNAME):
+ self.service = service
+ self.user = user
+ self.host = host
+
+ def receivedHeader(self, helo, origin, recipients):
+ authStr = heloStr = ""
+ if self.user:
+ authStr = " auth=%s" % (self.user.encode('xtext'),)
+ if helo[0]:
+ heloStr = " helo=%s" % (helo[0],)
+ from_ = "from %s ([%s]%s%s)" % (helo[0], helo[1], heloStr, authStr)
+ by = "by %s with %s (%s)" % (
+ self.host, self.protocolName, longversion
+ )
+ for_ = "for <%s>; %s" % (' '.join(map(str, recipients)), smtp.rfc822date())
+ return "Received: %s\n\t%s\n\t%s" % (from_, by, for_)
+
+ def validateTo(self, user):
+ # XXX - Yick. This needs cleaning up.
+ if self.user and self.service.queue:
+ d = self.service.domains.get(user.dest.domain, None)
+ if d is None:
+ d = relay.DomainQueuer(self.service, True)
+ else:
+ d = self.service.domains[user.dest.domain]
+ return defer.maybeDeferred(d.exists, user)
+
+ def validateFrom(self, helo, origin):
+ if not helo:
+ raise smtp.SMTPBadSender(origin, 503, "Who are you? Say HELO first.")
+ if origin.local != '' and origin.domain == '':
+ raise smtp.SMTPBadSender(origin, 501, "Sender address must contain domain.")
+ return origin
+
+ def startMessage(self, users):
+ ret = []
+ for user in users:
+ ret.append(self.service.domains[user.dest.domain].startMessage(user))
+ return ret
+
+
+class SMTPDomainDelivery(DomainDeliveryBase):
+ protocolName = 'smtp'
+
+class ESMTPDomainDelivery(DomainDeliveryBase):
+ protocolName = 'esmtp'
+
+class DomainSMTP(SMTPDomainDelivery, smtp.SMTP):
+ service = user = None
+
+ def __init__(self, *args, **kw):
+ import warnings
+ warnings.warn(
+ "DomainSMTP is deprecated. Use IMessageDelivery objects instead.",
+ DeprecationWarning, stacklevel=2,
+ )
+ smtp.SMTP.__init__(self, *args, **kw)
+ if self.delivery is None:
+ self.delivery = self
+
+class DomainESMTP(ESMTPDomainDelivery, smtp.ESMTP):
+ service = user = None
+
+ def __init__(self, *args, **kw):
+ import warnings
+ warnings.warn(
+ "DomainESMTP is deprecated. Use IMessageDelivery objects instead.",
+ DeprecationWarning, stacklevel=2,
+ )
+ smtp.ESMTP.__init__(self, *args, **kw)
+ if self.delivery is None:
+ self.delivery = self
+
+class SMTPFactory(smtp.SMTPFactory):
+ """A protocol factory for SMTP."""
+
+ protocol = smtp.SMTP
+ portal = None
+
+ def __init__(self, service, portal = None):
+ smtp.SMTPFactory.__init__(self)
+ self.service = service
+ self.portal = portal
+
+ def buildProtocol(self, addr):
+ log.msg('Connection from %s' % (addr,))
+ p = smtp.SMTPFactory.buildProtocol(self, addr)
+ p.service = self.service
+ p.portal = self.portal
+ return p
+
+class ESMTPFactory(SMTPFactory):
+ protocol = smtp.ESMTP
+ context = None
+
+ def __init__(self, *args):
+ SMTPFactory.__init__(self, *args)
+ self.challengers = {
+ 'CRAM-MD5': cred.credentials.CramMD5Credentials
+ }
+
+ def buildProtocol(self, addr):
+ p = SMTPFactory.buildProtocol(self, addr)
+ p.challengers = self.challengers
+ p.ctx = self.context
+ return p
+
+class VirtualPOP3(pop3.POP3):
+ """Virtual hosting POP3."""
+
+ service = None
+
+ domainSpecifier = '@' # Gaagh! I hate POP3. No standardized way
+ # to indicate user@host. '@' doesn't work
+ # with NS, e.g.
+
+ def authenticateUserAPOP(self, user, digest):
+ # Override the default lookup scheme to allow virtual domains
+ user, domain = self.lookupDomain(user)
+ try:
+ portal = self.service.lookupPortal(domain)
+ except KeyError:
+ return defer.fail(cred.error.UnauthorizedLogin())
+ else:
+ return portal.login(
+ pop3.APOPCredentials(self.magic, user, digest),
+ None,
+ pop3.IMailbox
+ )
+
+ def authenticateUserPASS(self, user, password):
+ user, domain = self.lookupDomain(user)
+ try:
+ portal = self.service.lookupPortal(domain)
+ except KeyError:
+ return defer.fail(cred.error.UnauthorizedLogin())
+ else:
+ return portal.login(
+ cred.credentials.UsernamePassword(user, password),
+ None,
+ pop3.IMailbox
+ )
+
+ def lookupDomain(self, user):
+ try:
+ user, domain = user.split(self.domainSpecifier, 1)
+ except ValueError:
+ domain = ''
+ if domain not in self.service.domains:
+ raise pop3.POP3Error("no such domain %s" % domain)
+ return user, domain
+
+
+class POP3Factory(protocol.ServerFactory):
+ """POP3 protocol factory."""
+
+ protocol = VirtualPOP3
+ service = None
+
+ def __init__(self, service):
+ self.service = service
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ p.service = self.service
+ return p
+
+#
+# It is useful to know, perhaps, that the required file for this to work can
+# be created thusly:
+#
+# openssl req -x509 -newkey rsa:2048 -keyout file.key -out file.crt \
+# -days 365 -nodes
+#
+# And then cat file.key and file.crt together. The number of days and bits
+# can be changed, of course.
+#
+class SSLContextFactory:
+ """An SSL Context Factory
+
+ This loads a certificate and private key from a specified file.
+ """
+ def __init__(self, filename):
+ self.filename = filename
+
+ def getContext(self):
+ """Create an SSL context."""
+ from OpenSSL import SSL
+ ctx = SSL.Context(SSL.SSLv23_METHOD)
+ ctx.use_certificate_file(self.filename)
+ ctx.use_privatekey_file(self.filename)
+ return ctx
diff --git a/twisted/mail/relay.py b/twisted/mail/relay.py
new file mode 100644
index 0000000..ac68095
--- /dev/null
+++ b/twisted/mail/relay.py
@@ -0,0 +1,114 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Support for relaying mail for twisted.mail"""
+
+from twisted.mail import smtp
+from twisted.python import log
+from twisted.internet.address import UNIXAddress
+
+import os
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+class DomainQueuer:
+ """An SMTP domain which add messages to a queue intended for relaying."""
+
+ def __init__(self, service, authenticated=False):
+ self.service = service
+ self.authed = authenticated
+
+ def exists(self, user):
+ """Check whether we will relay
+
+ Call overridable willRelay method
+ """
+ if self.willRelay(user.dest, user.protocol):
+ # The most cursor form of verification of the addresses
+ orig = filter(None, str(user.orig).split('@', 1))
+ dest = filter(None, str(user.dest).split('@', 1))
+ if len(orig) == 2 and len(dest) == 2:
+ return lambda: self.startMessage(user)
+ raise smtp.SMTPBadRcpt(user)
+
+ def willRelay(self, address, protocol):
+ """Check whether we agree to relay
+
+ The default is to relay for all connections over UNIX
+ sockets and all connections from localhost.
+ """
+ peer = protocol.transport.getPeer()
+ return self.authed or isinstance(peer, UNIXAddress) or peer.host == '127.0.0.1'
+
+ def startMessage(self, user):
+ """Add envelope to queue and returns ISMTPMessage."""
+ queue = self.service.queue
+ envelopeFile, smtpMessage = queue.createNewMessage()
+ try:
+ log.msg('Queueing mail %r -> %r' % (str(user.orig), str(user.dest)))
+ pickle.dump([str(user.orig), str(user.dest)], envelopeFile)
+ finally:
+ envelopeFile.close()
+ return smtpMessage
+
+class RelayerMixin:
+
+ # XXX - This is -totally- bogus
+ # It opens about a -hundred- -billion- files
+ # and -leaves- them open!
+
+ def loadMessages(self, messagePaths):
+ self.messages = []
+ self.names = []
+ for message in messagePaths:
+ fp = open(message+'-H')
+ try:
+ messageContents = pickle.load(fp)
+ finally:
+ fp.close()
+ fp = open(message+'-D')
+ messageContents.append(fp)
+ self.messages.append(messageContents)
+ self.names.append(message)
+
+ def getMailFrom(self):
+ if not self.messages:
+ return None
+ return self.messages[0][0]
+
+ def getMailTo(self):
+ if not self.messages:
+ return None
+ return [self.messages[0][1]]
+
+ def getMailData(self):
+ if not self.messages:
+ return None
+ return self.messages[0][2]
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ """Since we only use one recipient per envelope, this
+ will be called with 0 or 1 addresses. We probably want
+ to do something with the error message if we failed.
+ """
+ if code in smtp.SUCCESS:
+ # At least one, i.e. all, recipients successfully delivered
+ os.remove(self.names[0]+'-D')
+ os.remove(self.names[0]+'-H')
+ del self.messages[0]
+ del self.names[0]
+
+class SMTPRelayer(RelayerMixin, smtp.SMTPClient):
+ def __init__(self, messagePaths, *args, **kw):
+ smtp.SMTPClient.__init__(self, *args, **kw)
+ self.loadMessages(messagePaths)
+
+class ESMTPRelayer(RelayerMixin, smtp.ESMTPClient):
+ def __init__(self, messagePaths, *args, **kw):
+ smtp.ESMTPClient.__init__(self, *args, **kw)
+ self.loadMessages(messagePaths)
diff --git a/twisted/mail/relaymanager.py b/twisted/mail/relaymanager.py
new file mode 100644
index 0000000..66c777a
--- /dev/null
+++ b/twisted/mail/relaymanager.py
@@ -0,0 +1,631 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Infrastructure for relaying mail through smart host
+
+Today, internet e-mail has stopped being Peer-to-peer for many problems,
+spam (unsolicited bulk mail) among them. Instead, most nodes on the
+internet send all e-mail to a single computer, usually the ISP's though
+sometimes other schemes, such as SMTP-after-POP, are used. This computer
+is supposedly permanently up and traceable, and will do the work of
+figuring out MXs and connecting to them. This kind of configuration
+is usually termed "smart host", since the host we are connecting to
+is "smart" (and will find MXs and connect to them) rather then just
+accepting mail for a small set of domains.
+
+The classes here are meant to facilitate support for such a configuration
+for the twisted.mail SMTP server
+"""
+
+import rfc822
+import os
+import time
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.python.compat import set
+from twisted.mail import relay
+from twisted.mail import bounce
+from twisted.internet import protocol
+from twisted.internet.defer import Deferred, DeferredList
+from twisted.internet.error import DNSLookupError
+from twisted.mail import smtp
+from twisted.application import internet
+
+class ManagedRelayerMixin:
+ """SMTP Relayer which notifies a manager
+
+ Notify the manager about successful mail, failed mail
+ and broken connections
+ """
+
+ def __init__(self, manager):
+ self.manager = manager
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ """called when e-mail has been sent
+
+ we will always get 0 or 1 addresses.
+ """
+ message = self.names[0]
+ if code in smtp.SUCCESS:
+ self.manager.notifySuccess(self.factory, message)
+ else:
+ self.manager.notifyFailure(self.factory, message)
+ del self.messages[0]
+ del self.names[0]
+
+ def connectionLost(self, reason):
+ """called when connection is broken
+
+ notify manager we will try to send no more e-mail
+ """
+ self.manager.notifyDone(self.factory)
+
+class SMTPManagedRelayer(ManagedRelayerMixin, relay.SMTPRelayer):
+ def __init__(self, messages, manager, *args, **kw):
+ """
+ @type messages: C{list} of C{str}
+ @param messages: Filenames of messages to relay
+
+ manager should support .notifySuccess, .notifyFailure
+ and .notifyDone
+ """
+ ManagedRelayerMixin.__init__(self, manager)
+ relay.SMTPRelayer.__init__(self, messages, *args, **kw)
+
+class ESMTPManagedRelayer(ManagedRelayerMixin, relay.ESMTPRelayer):
+ def __init__(self, messages, manager, *args, **kw):
+ """
+ @type messages: C{list} of C{str}
+ @param messages: Filenames of messages to relay
+
+ manager should support .notifySuccess, .notifyFailure
+ and .notifyDone
+ """
+ ManagedRelayerMixin.__init__(self, manager)
+ relay.ESMTPRelayer.__init__(self, messages, *args, **kw)
+
+class SMTPManagedRelayerFactory(protocol.ClientFactory):
+ protocol = SMTPManagedRelayer
+
+ def __init__(self, messages, manager, *args, **kw):
+ self.messages = messages
+ self.manager = manager
+ self.pArgs = args
+ self.pKwArgs = kw
+
+ def buildProtocol(self, addr):
+ protocol = self.protocol(self.messages, self.manager, *self.pArgs,
+ **self.pKwArgs)
+ protocol.factory = self
+ return protocol
+
+ def clientConnectionFailed(self, connector, reason):
+ """called when connection could not be made
+
+ our manager should be notified that this happened,
+ it might prefer some other host in that case"""
+ self.manager.notifyNoConnection(self)
+ self.manager.notifyDone(self)
+
+class ESMTPManagedRelayerFactory(SMTPManagedRelayerFactory):
+ protocol = ESMTPManagedRelayer
+
+ def __init__(self, messages, manager, secret, contextFactory, *args, **kw):
+ self.secret = secret
+ self.contextFactory = contextFactory
+ SMTPManagedRelayerFactory.__init__(self, messages, manager, *args, **kw)
+
+ def buildProtocol(self, addr):
+ s = self.secret and self.secret(addr)
+ protocol = self.protocol(self.messages, self.manager, s,
+ self.contextFactory, *self.pArgs, **self.pKwArgs)
+ protocol.factory = self
+ return protocol
+
+class Queue:
+ """A queue of ougoing emails."""
+
+ noisy = True
+
+ def __init__(self, directory):
+ self.directory = directory
+ self._init()
+
+ def _init(self):
+ self.n = 0
+ self.waiting = {}
+ self.relayed = {}
+ self.readDirectory()
+
+ def __getstate__(self):
+ """(internal) delete volatile state"""
+ return {'directory' : self.directory}
+
+ def __setstate__(self, state):
+ """(internal) restore volatile state"""
+ self.__dict__.update(state)
+ self._init()
+
+ def readDirectory(self):
+ """Read the messages directory.
+
+ look for new messages.
+ """
+ for message in os.listdir(self.directory):
+ # Skip non data files
+ if message[-2:]!='-D':
+ continue
+ self.addMessage(message[:-2])
+
+ def getWaiting(self):
+ return self.waiting.keys()
+
+ def hasWaiting(self):
+ return len(self.waiting) > 0
+
+ def getRelayed(self):
+ return self.relayed.keys()
+
+ def setRelaying(self, message):
+ del self.waiting[message]
+ self.relayed[message] = 1
+
+ def setWaiting(self, message):
+ del self.relayed[message]
+ self.waiting[message] = 1
+
+ def addMessage(self, message):
+ if message not in self.relayed:
+ self.waiting[message] = 1
+ if self.noisy:
+ log.msg('Set ' + message + ' waiting')
+
+ def done(self, message):
+ """Remove message to from queue."""
+ message = os.path.basename(message)
+ os.remove(self.getPath(message) + '-D')
+ os.remove(self.getPath(message) + '-H')
+ del self.relayed[message]
+
+ def getPath(self, message):
+ """Get the path in the filesystem of a message."""
+ return os.path.join(self.directory, message)
+
+ def getEnvelope(self, message):
+ return pickle.load(self.getEnvelopeFile(message))
+
+ def getEnvelopeFile(self, message):
+ return open(os.path.join(self.directory, message+'-H'), 'rb')
+
+ def createNewMessage(self):
+ """Create a new message in the queue.
+
+ Return a tuple - file-like object for headers, and ISMTPMessage.
+ """
+ fname = "%s_%s_%s_%s" % (os.getpid(), time.time(), self.n, id(self))
+ self.n = self.n + 1
+ headerFile = open(os.path.join(self.directory, fname+'-H'), 'wb')
+ tempFilename = os.path.join(self.directory, fname+'-C')
+ finalFilename = os.path.join(self.directory, fname+'-D')
+ messageFile = open(tempFilename, 'wb')
+
+ from twisted.mail.mail import FileMessage
+ return headerFile,FileMessage(messageFile, tempFilename, finalFilename)
+
+
+class _AttemptManager(object):
+ """
+ Manage the state of a single attempt to flush the relay queue.
+ """
+ def __init__(self, manager):
+ self.manager = manager
+ self._completionDeferreds = []
+
+
+ def getCompletionDeferred(self):
+ self._completionDeferreds.append(Deferred())
+ return self._completionDeferreds[-1]
+
+
+ def _finish(self, relay, message):
+ self.manager.managed[relay].remove(os.path.basename(message))
+ self.manager.queue.done(message)
+
+
+ def notifySuccess(self, relay, message):
+ """a relay sent a message successfully
+
+ Mark it as sent in our lists
+ """
+ if self.manager.queue.noisy:
+ log.msg("success sending %s, removing from queue" % message)
+ self._finish(relay, message)
+
+
+ def notifyFailure(self, relay, message):
+ """Relaying the message has failed."""
+ if self.manager.queue.noisy:
+ log.msg("could not relay "+message)
+ # Moshe - Bounce E-mail here
+ # Be careful: if it's a bounced bounce, silently
+ # discard it
+ message = os.path.basename(message)
+ fp = self.manager.queue.getEnvelopeFile(message)
+ from_, to = pickle.load(fp)
+ fp.close()
+ from_, to, bounceMessage = bounce.generateBounce(open(self.manager.queue.getPath(message)+'-D'), from_, to)
+ fp, outgoingMessage = self.manager.queue.createNewMessage()
+ pickle.dump([from_, to], fp)
+ fp.close()
+ for line in bounceMessage.splitlines():
+ outgoingMessage.lineReceived(line)
+ outgoingMessage.eomReceived()
+ self._finish(relay, self.manager.queue.getPath(message))
+
+
+ def notifyDone(self, relay):
+ """A relaying SMTP client is disconnected.
+
+ unmark all pending messages under this relay's responsibility
+ as being relayed, and remove the relay.
+ """
+ for message in self.manager.managed.get(relay, ()):
+ if self.manager.queue.noisy:
+ log.msg("Setting " + message + " waiting")
+ self.manager.queue.setWaiting(message)
+ try:
+ del self.manager.managed[relay]
+ except KeyError:
+ pass
+ notifications = self._completionDeferreds
+ self._completionDeferreds = None
+ for d in notifications:
+ d.callback(None)
+
+
+ def notifyNoConnection(self, relay):
+ """Relaying SMTP client couldn't connect.
+
+ Useful because it tells us our upstream server is unavailable.
+ """
+ # Back off a bit
+ try:
+ msgs = self.manager.managed[relay]
+ except KeyError:
+ log.msg("notifyNoConnection passed unknown relay!")
+ return
+
+ if self.manager.queue.noisy:
+ log.msg("Backing off on delivery of " + str(msgs))
+ def setWaiting(queue, messages):
+ map(queue.setWaiting, messages)
+ from twisted.internet import reactor
+ reactor.callLater(30, setWaiting, self.manager.queue, msgs)
+ del self.manager.managed[relay]
+
+
+
+class SmartHostSMTPRelayingManager:
+ """Manage SMTP Relayers
+
+ Manage SMTP relayers, keeping track of the existing connections,
+ each connection's responsibility in term of messages. Create
+ more relayers if the need arises.
+
+ Someone should press .checkState periodically
+
+ @ivar fArgs: Additional positional arguments used to instantiate
+ C{factory}.
+
+ @ivar fKwArgs: Additional keyword arguments used to instantiate
+ C{factory}.
+
+ @ivar factory: A callable which returns a ClientFactory suitable for
+ making SMTP connections.
+ """
+
+ factory = SMTPManagedRelayerFactory
+
+ PORT = 25
+
+ mxcalc = None
+
+ def __init__(self, queue, maxConnections=2, maxMessagesPerConnection=10):
+ """
+ @type queue: Any implementor of C{IQueue}
+ @param queue: The object used to queue messages on their way to
+ delivery.
+
+ @type maxConnections: C{int}
+ @param maxConnections: The maximum number of SMTP connections to
+ allow to be opened at any given time.
+
+ @type maxMessagesPerConnection: C{int}
+ @param maxMessagesPerConnection: The maximum number of messages a
+ relayer will be given responsibility for.
+
+ Default values are meant for a small box with 1-5 users.
+ """
+ self.maxConnections = maxConnections
+ self.maxMessagesPerConnection = maxMessagesPerConnection
+ self.managed = {} # SMTP clients we're managing
+ self.queue = queue
+ self.fArgs = ()
+ self.fKwArgs = {}
+
+ def __getstate__(self):
+ """(internal) delete volatile state"""
+ dct = self.__dict__.copy()
+ del dct['managed']
+ return dct
+
+ def __setstate__(self, state):
+ """(internal) restore volatile state"""
+ self.__dict__.update(state)
+ self.managed = {}
+
+ def checkState(self):
+ """
+ Synchronize with the state of the world, and maybe launch a new
+ relay.
+
+ Call me periodically to check I am still up to date.
+
+ @return: None or a Deferred which fires when all of the SMTP clients
+ started by this call have disconnected.
+ """
+ self.queue.readDirectory()
+ if (len(self.managed) >= self.maxConnections):
+ return
+ if not self.queue.hasWaiting():
+ return
+
+ return self._checkStateMX()
+
+ def _checkStateMX(self):
+ nextMessages = self.queue.getWaiting()
+ nextMessages.reverse()
+
+ exchanges = {}
+ for msg in nextMessages:
+ from_, to = self.queue.getEnvelope(msg)
+ name, addr = rfc822.parseaddr(to)
+ parts = addr.split('@', 1)
+ if len(parts) != 2:
+ log.err("Illegal message destination: " + to)
+ continue
+ domain = parts[1]
+
+ self.queue.setRelaying(msg)
+ exchanges.setdefault(domain, []).append(self.queue.getPath(msg))
+ if len(exchanges) >= (self.maxConnections - len(self.managed)):
+ break
+
+ if self.mxcalc is None:
+ self.mxcalc = MXCalculator()
+
+ relays = []
+ for (domain, msgs) in exchanges.iteritems():
+ manager = _AttemptManager(self)
+ factory = self.factory(msgs, manager, *self.fArgs, **self.fKwArgs)
+ self.managed[factory] = map(os.path.basename, msgs)
+ relayAttemptDeferred = manager.getCompletionDeferred()
+ connectSetupDeferred = self.mxcalc.getMX(domain)
+ connectSetupDeferred.addCallback(lambda mx: str(mx.name))
+ connectSetupDeferred.addCallback(self._cbExchange, self.PORT, factory)
+ connectSetupDeferred.addErrback(lambda err: (relayAttemptDeferred.errback(err), err)[1])
+ connectSetupDeferred.addErrback(self._ebExchange, factory, domain)
+ relays.append(relayAttemptDeferred)
+ return DeferredList(relays)
+
+
+ def _cbExchange(self, address, port, factory):
+ from twisted.internet import reactor
+ reactor.connectTCP(address, port, factory)
+
+ def _ebExchange(self, failure, factory, domain):
+ log.err('Error setting up managed relay factory for ' + domain)
+ log.err(failure)
+ def setWaiting(queue, messages):
+ map(queue.setWaiting, messages)
+ from twisted.internet import reactor
+ reactor.callLater(30, setWaiting, self.queue, self.managed[factory])
+ del self.managed[factory]
+
+class SmartHostESMTPRelayingManager(SmartHostSMTPRelayingManager):
+ factory = ESMTPManagedRelayerFactory
+
+def _checkState(manager):
+ manager.checkState()
+
+def RelayStateHelper(manager, delay):
+ return internet.TimerService(delay, _checkState, manager)
+
+
+
+class CanonicalNameLoop(Exception):
+ """
+ When trying to look up the MX record for a host, a set of CNAME records was
+ found which form a cycle and resolution was abandoned.
+ """
+
+
+class CanonicalNameChainTooLong(Exception):
+ """
+ When trying to look up the MX record for a host, too many CNAME records
+ which point to other CNAME records were encountered and resolution was
+ abandoned.
+ """
+
+
+class MXCalculator:
+ """
+ A utility for looking up mail exchange hosts and tracking whether they are
+ working or not.
+
+ @ivar clock: L{IReactorTime} provider which will be used to decide when to
+ retry mail exchanges which have not been working.
+ """
+ timeOutBadMX = 60 * 60 # One hour
+ fallbackToDomain = True
+
+ def __init__(self, resolver=None, clock=None):
+ self.badMXs = {}
+ if resolver is None:
+ from twisted.names.client import createResolver
+ resolver = createResolver()
+ self.resolver = resolver
+ if clock is None:
+ from twisted.internet import reactor as clock
+ self.clock = clock
+
+
+ def markBad(self, mx):
+ """Indicate a given mx host is not currently functioning.
+
+ @type mx: C{str}
+ @param mx: The hostname of the host which is down.
+ """
+ self.badMXs[str(mx)] = self.clock.seconds() + self.timeOutBadMX
+
+ def markGood(self, mx):
+ """Indicate a given mx host is back online.
+
+ @type mx: C{str}
+ @param mx: The hostname of the host which is up.
+ """
+ try:
+ del self.badMXs[mx]
+ except KeyError:
+ pass
+
+ def getMX(self, domain, maximumCanonicalChainLength=3):
+ """
+ Find an MX record for the given domain.
+
+ @type domain: C{str}
+ @param domain: The domain name for which to look up an MX record.
+
+ @type maximumCanonicalChainLength: C{int}
+ @param maximumCanonicalChainLength: The maximum number of unique CNAME
+ records to follow while looking up the MX record.
+
+ @return: A L{Deferred} which is called back with a string giving the
+ name in the found MX record or which is errbacked if no MX record
+ can be found.
+ """
+ mailExchangeDeferred = self.resolver.lookupMailExchange(domain)
+ mailExchangeDeferred.addCallback(self._filterRecords)
+ mailExchangeDeferred.addCallback(
+ self._cbMX, domain, maximumCanonicalChainLength)
+ mailExchangeDeferred.addErrback(self._ebMX, domain)
+ return mailExchangeDeferred
+
+
+ def _filterRecords(self, records):
+ """
+ Convert a DNS response (a three-tuple of lists of RRHeaders) into a
+ mapping from record names to lists of corresponding record payloads.
+ """
+ recordBag = {}
+ for answer in records[0]:
+ recordBag.setdefault(str(answer.name), []).append(answer.payload)
+ return recordBag
+
+
+ def _cbMX(self, answers, domain, cnamesLeft):
+ """
+ Try to find the MX host from the given DNS information.
+
+ This will attempt to resolve CNAME results. It can recognize loops
+ and will give up on non-cyclic chains after a specified number of
+ lookups.
+ """
+ # Do this import here so that relaymanager.py doesn't depend on
+ # twisted.names, only MXCalculator will.
+ from twisted.names import dns, error
+
+ seenAliases = set()
+ exchanges = []
+ # Examine the answers for the domain we asked about
+ pertinentRecords = answers.get(domain, [])
+ while pertinentRecords:
+ record = pertinentRecords.pop()
+
+ # If it's a CNAME, we'll need to do some more processing
+ if record.TYPE == dns.CNAME:
+
+ # Remember that this name was an alias.
+ seenAliases.add(domain)
+
+ canonicalName = str(record.name)
+ # See if we have some local records which might be relevant.
+ if canonicalName in answers:
+
+ # Make sure it isn't a loop contained entirely within the
+ # results we have here.
+ if canonicalName in seenAliases:
+ return Failure(CanonicalNameLoop(record))
+
+ pertinentRecords = answers[canonicalName]
+ exchanges = []
+ else:
+ if cnamesLeft:
+ # Request more information from the server.
+ return self.getMX(canonicalName, cnamesLeft - 1)
+ else:
+ # Give up.
+ return Failure(CanonicalNameChainTooLong(record))
+
+ # If it's an MX, collect it.
+ if record.TYPE == dns.MX:
+ exchanges.append((record.preference, record))
+
+ if exchanges:
+ exchanges.sort()
+ for (preference, record) in exchanges:
+ host = str(record.name)
+ if host not in self.badMXs:
+ return record
+ t = self.clock.seconds() - self.badMXs[host]
+ if t >= 0:
+ del self.badMXs[host]
+ return record
+ return exchanges[0][1]
+ else:
+ # Treat no answers the same as an error - jump to the errback to try
+ # to look up an A record. This provides behavior described as a
+ # special case in RFC 974 in the section headed I{Interpreting the
+ # List of MX RRs}.
+ return Failure(
+ error.DNSNameError("No MX records for %r" % (domain,)))
+
+
+ def _ebMX(self, failure, domain):
+ from twisted.names import error, dns
+
+ if self.fallbackToDomain:
+ failure.trap(error.DNSNameError)
+ log.msg("MX lookup failed; attempting to use hostname (%s) directly" % (domain,))
+
+ # Alright, I admit, this is a bit icky.
+ d = self.resolver.getHostByName(domain)
+ def cbResolved(addr):
+ return dns.Record_MX(name=addr)
+ def ebResolved(err):
+ err.trap(error.DNSNameError)
+ raise DNSLookupError()
+ d.addCallbacks(cbResolved, ebResolved)
+ return d
+ elif failure.check(error.DNSNameError):
+ raise IOError("No MX found for %r" % (domain,))
+ return failure
diff --git a/twisted/mail/scripts/__init__.py b/twisted/mail/scripts/__init__.py
new file mode 100644
index 0000000..f653cc7
--- /dev/null
+++ b/twisted/mail/scripts/__init__.py
@@ -0,0 +1 @@
+"mail scripts"
diff --git a/twisted/mail/scripts/mailmail.py b/twisted/mail/scripts/mailmail.py
new file mode 100644
index 0000000..a045e82
--- /dev/null
+++ b/twisted/mail/scripts/mailmail.py
@@ -0,0 +1,366 @@
+# -*- test-case-name: twisted.mail.test.test_mailmail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation module for the I{mailmail} command.
+"""
+
+import os
+import sys
+import rfc822
+import getpass
+from ConfigParser import ConfigParser
+
+try:
+ import cStringIO as StringIO
+except:
+ import StringIO
+
+from twisted.copyright import version
+from twisted.internet import reactor
+from twisted.mail import smtp
+
+GLOBAL_CFG = "/etc/mailmail"
+LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail")
+SMARTHOST = '127.0.0.1'
+
+ERROR_FMT = """\
+Subject: Failed Message Delivery
+
+ Message delivery failed. The following occurred:
+
+ %s
+--
+The Twisted sendmail application.
+"""
+
+def log(message, *args):
+ sys.stderr.write(str(message) % args + '\n')
+
+class Options:
+ """
+ @type to: C{list} of C{str}
+ @ivar to: The addresses to which to deliver this message.
+
+ @type sender: C{str}
+ @ivar sender: The address from which this message is being sent.
+
+ @type body: C{file}
+ @ivar body: The object from which the message is to be read.
+ """
+
+def getlogin():
+ try:
+ return os.getlogin()
+ except:
+ return getpass.getuser()
+
+
+_unsupportedOption = SystemExit("Unsupported option.")
+
+def parseOptions(argv):
+ o = Options()
+ o.to = [e for e in argv if not e.startswith('-')]
+ o.sender = getlogin()
+
+ # Just be very stupid
+
+ # Skip -bm -- it is the default
+
+ # Add a non-standard option for querying the version of this tool.
+ if '--version' in argv:
+ print 'mailmail version:', version
+ raise SystemExit()
+
+ # -bp lists queue information. Screw that.
+ if '-bp' in argv:
+ raise _unsupportedOption
+
+ # -bs makes sendmail use stdin/stdout as its transport. Screw that.
+ if '-bs' in argv:
+ raise _unsupportedOption
+
+ # -F sets who the mail is from, but is overridable by the From header
+ if '-F' in argv:
+ o.sender = argv[argv.index('-F') + 1]
+ o.to.remove(o.sender)
+
+ # -i and -oi makes us ignore lone "."
+ if ('-i' in argv) or ('-oi' in argv):
+ raise _unsupportedOption
+
+ # -odb is background delivery
+ if '-odb' in argv:
+ o.background = True
+ else:
+ o.background = False
+
+ # -odf is foreground delivery
+ if '-odf' in argv:
+ o.background = False
+ else:
+ o.background = True
+
+ # -oem and -em cause errors to be mailed back to the sender.
+ # It is also the default.
+
+ # -oep and -ep cause errors to be printed to stderr
+ if ('-oep' in argv) or ('-ep' in argv):
+ o.printErrors = True
+ else:
+ o.printErrors = False
+
+ # -om causes a copy of the message to be sent to the sender if the sender
+ # appears in an alias expansion. We do not support aliases.
+ if '-om' in argv:
+ raise _unsupportedOption
+
+ # -t causes us to pick the recipients of the message from the To, Cc, and Bcc
+ # headers, and to remove the Bcc header if present.
+ if '-t' in argv:
+ o.recipientsFromHeaders = True
+ o.excludeAddresses = o.to
+ o.to = []
+ else:
+ o.recipientsFromHeaders = False
+ o.exludeAddresses = []
+
+ requiredHeaders = {
+ 'from': [],
+ 'to': [],
+ 'cc': [],
+ 'bcc': [],
+ 'date': [],
+ }
+
+ headers = []
+ buffer = StringIO.StringIO()
+ while 1:
+ write = 1
+ line = sys.stdin.readline()
+ if not line.strip():
+ break
+
+ hdrs = line.split(': ', 1)
+
+ hdr = hdrs[0].lower()
+ if o.recipientsFromHeaders and hdr in ('to', 'cc', 'bcc'):
+ o.to.extend([
+ a[1] for a in rfc822.AddressList(hdrs[1]).addresslist
+ ])
+ if hdr == 'bcc':
+ write = 0
+ elif hdr == 'from':
+ o.sender = rfc822.parseaddr(hdrs[1])[1]
+
+ if hdr in requiredHeaders:
+ requiredHeaders[hdr].append(hdrs[1])
+
+ if write:
+ buffer.write(line)
+
+ if not requiredHeaders['from']:
+ buffer.write('From: %s\r\n' % (o.sender,))
+ if not requiredHeaders['to']:
+ if not o.to:
+ raise SystemExit("No recipients specified.")
+ buffer.write('To: %s\r\n' % (', '.join(o.to),))
+ if not requiredHeaders['date']:
+ buffer.write('Date: %s\r\n' % (smtp.rfc822date(),))
+
+ buffer.write(line)
+
+ if o.recipientsFromHeaders:
+ for a in o.excludeAddresses:
+ try:
+ o.to.remove(a)
+ except:
+ pass
+
+ buffer.seek(0, 0)
+ o.body = StringIO.StringIO(buffer.getvalue() + sys.stdin.read())
+ return o
+
+class Configuration:
+ """
+ @ivar allowUIDs: A list of UIDs which are allowed to send mail.
+ @ivar allowGIDs: A list of GIDs which are allowed to send mail.
+ @ivar denyUIDs: A list of UIDs which are not allowed to send mail.
+ @ivar denyGIDs: A list of GIDs which are not allowed to send mail.
+
+ @type defaultAccess: C{bool}
+ @ivar defaultAccess: C{True} if access will be allowed when no other access
+ control rule matches or C{False} if it will be denied in that case.
+
+ @ivar useraccess: Either C{'allow'} to check C{allowUID} first
+ or C{'deny'} to check C{denyUID} first.
+
+ @ivar groupaccess: Either C{'allow'} to check C{allowGID} first or
+ C{'deny'} to check C{denyGID} first.
+
+ @ivar identities: A C{dict} mapping hostnames to credentials to use when
+ sending mail to that host.
+
+ @ivar smarthost: C{None} or a hostname through which all outgoing mail will
+ be sent.
+
+ @ivar domain: C{None} or the hostname with which to identify ourselves when
+ connecting to an MTA.
+ """
+ def __init__(self):
+ self.allowUIDs = []
+ self.denyUIDs = []
+ self.allowGIDs = []
+ self.denyGIDs = []
+ self.useraccess = 'deny'
+ self.groupaccess= 'deny'
+
+ self.identities = {}
+ self.smarthost = None
+ self.domain = None
+
+ self.defaultAccess = True
+
+
+def loadConfig(path):
+ # [useraccess]
+ # allow=uid1,uid2,...
+ # deny=uid1,uid2,...
+ # order=allow,deny
+ # [groupaccess]
+ # allow=gid1,gid2,...
+ # deny=gid1,gid2,...
+ # order=deny,allow
+ # [identity]
+ # host1=username:password
+ # host2=username:password
+ # [addresses]
+ # smarthost=a.b.c.d
+ # default_domain=x.y.z
+
+ c = Configuration()
+
+ if not os.access(path, os.R_OK):
+ return c
+
+ p = ConfigParser()
+ p.read(path)
+
+ au = c.allowUIDs
+ du = c.denyUIDs
+ ag = c.allowGIDs
+ dg = c.denyGIDs
+ for (section, a, d) in (('useraccess', au, du), ('groupaccess', ag, dg)):
+ if p.has_section(section):
+ for (mode, L) in (('allow', a), ('deny', d)):
+ if p.has_option(section, mode) and p.get(section, mode):
+ for id in p.get(section, mode).split(','):
+ try:
+ id = int(id)
+ except ValueError:
+ log("Illegal %sID in [%s] section: %s", section[0].upper(), section, id)
+ else:
+ L.append(id)
+ order = p.get(section, 'order')
+ order = map(str.split, map(str.lower, order.split(',')))
+ if order[0] == 'allow':
+ setattr(c, section, 'allow')
+ else:
+ setattr(c, section, 'deny')
+
+ if p.has_section('identity'):
+ for (host, up) in p.items('identity'):
+ parts = up.split(':', 1)
+ if len(parts) != 2:
+ log("Illegal entry in [identity] section: %s", up)
+ continue
+ p.identities[host] = parts
+
+ if p.has_section('addresses'):
+ if p.has_option('addresses', 'smarthost'):
+ c.smarthost = p.get('addresses', 'smarthost')
+ if p.has_option('addresses', 'default_domain'):
+ c.domain = p.get('addresses', 'default_domain')
+
+ return c
+
+def success(result):
+ reactor.stop()
+
+failed = None
+def failure(f):
+ global failed
+ reactor.stop()
+ failed = f
+
+def sendmail(host, options, ident):
+ d = smtp.sendmail(host, options.sender, options.to, options.body)
+ d.addCallbacks(success, failure)
+ reactor.run()
+
+def senderror(failure, options):
+ recipient = [options.sender]
+ sender = '"Internally Generated Message (%s)"<postmaster@%s>' % (sys.argv[0], smtp.DNSNAME)
+ error = StringIO.StringIO()
+ failure.printTraceback(file=error)
+ body = StringIO.StringIO(ERROR_FMT % error.getvalue())
+
+ d = smtp.sendmail('localhost', sender, recipient, body)
+ d.addBoth(lambda _: reactor.stop())
+
+def deny(conf):
+ uid = os.getuid()
+ gid = os.getgid()
+
+ if conf.useraccess == 'deny':
+ if uid in conf.denyUIDs:
+ return True
+ if uid in conf.allowUIDs:
+ return False
+ else:
+ if uid in conf.allowUIDs:
+ return False
+ if uid in conf.denyUIDs:
+ return True
+
+ if conf.groupaccess == 'deny':
+ if gid in conf.denyGIDs:
+ return True
+ if gid in conf.allowGIDs:
+ return False
+ else:
+ if gid in conf.allowGIDs:
+ return False
+ if gid in conf.denyGIDs:
+ return True
+
+ return not conf.defaultAccess
+
+def run():
+ o = parseOptions(sys.argv[1:])
+ gConf = loadConfig(GLOBAL_CFG)
+ lConf = loadConfig(LOCAL_CFG)
+
+ if deny(gConf) or deny(lConf):
+ log("Permission denied")
+ return
+
+ host = lConf.smarthost or gConf.smarthost or SMARTHOST
+
+ ident = gConf.identities.copy()
+ ident.update(lConf.identities)
+
+ if lConf.domain:
+ smtp.DNSNAME = lConf.domain
+ elif gConf.domain:
+ smtp.DNSNAME = gConf.domain
+
+ sendmail(host, o, ident)
+
+ if failed:
+ if o.printErrors:
+ failed.printTraceback(file=sys.stderr)
+ raise SystemExit(1)
+ else:
+ senderror(failed, o)
diff --git a/twisted/mail/smtp.py b/twisted/mail/smtp.py
new file mode 100644
index 0000000..d872111
--- /dev/null
+++ b/twisted/mail/smtp.py
@@ -0,0 +1,1934 @@
+# -*- test-case-name: twisted.mail.test.test_smtp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Simple Mail Transfer Protocol implementation.
+"""
+
+import time, re, base64, types, socket, os, random, rfc822
+import binascii
+from email.base64MIME import encode as encode_base64
+
+from zope.interface import implements, Interface
+
+from twisted.copyright import longversion
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.internet import error
+from twisted.internet import reactor
+from twisted.internet.interfaces import ITLSTransport
+from twisted.python import log
+from twisted.python import util
+
+from twisted import cred
+from twisted.python.runtime import platform
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+# Cache the hostname (XXX Yes - this is broken)
+if platform.isMacOSX():
+ # On OS X, getfqdn() is ridiculously slow - use the
+ # probably-identical-but-sometimes-not gethostname() there.
+ DNSNAME = socket.gethostname()
+else:
+ DNSNAME = socket.getfqdn()
+
+# Used for fast success code lookup
+SUCCESS = dict.fromkeys(xrange(200,300))
+
+class IMessageDelivery(Interface):
+ def receivedHeader(helo, origin, recipients):
+ """
+ Generate the Received header for a message
+
+ @type helo: C{(str, str)}
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: C{Address}
+ @param origin: The address the message is from
+
+ @type recipients: C{list} of L{User}
+ @param recipients: A list of the addresses for which this message
+ is bound.
+
+ @rtype: C{str}
+ @return: The full \"Received\" header string.
+ """
+
+ def validateTo(user):
+ """
+ Validate the address for which the message is destined.
+
+ @type user: C{User}
+ @param user: The address to validate.
+
+ @rtype: no-argument callable
+ @return: A C{Deferred} which becomes, or a callable which
+ takes no arguments and returns an object implementing C{IMessage}.
+ This will be called and the returned object used to deliver the
+ message when it arrives.
+
+ @raise SMTPBadRcpt: Raised if messages to the address are
+ not to be accepted.
+ """
+
+ def validateFrom(helo, origin):
+ """
+ Validate the address from which the message originates.
+
+ @type helo: C{(str, str)}
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: C{Address}
+ @param origin: The address the message is from
+
+ @rtype: C{Deferred} or C{Address}
+ @return: C{origin} or a C{Deferred} whose callback will be
+ passed C{origin}.
+
+ @raise SMTPBadSender: Raised of messages from this address are
+ not to be accepted.
+ """
+
+class IMessageDeliveryFactory(Interface):
+ """An alternate interface to implement for handling message delivery.
+
+ It is useful to implement this interface instead of L{IMessageDelivery}
+ directly because it allows the implementor to distinguish between
+ different messages delivery over the same connection. This can be
+ used to optimize delivery of a single message to multiple recipients,
+ something which cannot be done by L{IMessageDelivery} implementors
+ due to their lack of information.
+ """
+ def getMessageDelivery():
+ """Return an L{IMessageDelivery} object.
+
+ This will be called once per message.
+ """
+
+class SMTPError(Exception):
+ pass
+
+
+
+class SMTPClientError(SMTPError):
+ """Base class for SMTP client errors.
+ """
+ def __init__(self, code, resp, log=None, addresses=None, isFatal=False, retry=False):
+ """
+ @param code: The SMTP response code associated with this error.
+ @param resp: The string response associated with this error.
+
+ @param log: A string log of the exchange leading up to and including
+ the error.
+ @type log: L{str}
+
+ @param isFatal: A boolean indicating whether this connection can
+ proceed or not. If True, the connection will be dropped.
+
+ @param retry: A boolean indicating whether the delivery should be
+ retried. If True and the factory indicates further retries are
+ desirable, they will be attempted, otherwise the delivery will
+ be failed.
+ """
+ self.code = code
+ self.resp = resp
+ self.log = log
+ self.addresses = addresses
+ self.isFatal = isFatal
+ self.retry = retry
+
+
+ def __str__(self):
+ if self.code > 0:
+ res = ["%.3d %s" % (self.code, self.resp)]
+ else:
+ res = [self.resp]
+ if self.log:
+ res.append(self.log)
+ res.append('')
+ return '\n'.join(res)
+
+
+class ESMTPClientError(SMTPClientError):
+ """Base class for ESMTP client errors.
+ """
+
+class EHLORequiredError(ESMTPClientError):
+ """The server does not support EHLO.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class AUTHRequiredError(ESMTPClientError):
+ """Authentication was required but the server does not support it.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class TLSRequiredError(ESMTPClientError):
+ """Transport security was required but the server does not support it.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class AUTHDeclinedError(ESMTPClientError):
+ """The server rejected our credentials.
+
+ Either the username, password, or challenge response
+ given to the server was rejected.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class AuthenticationError(ESMTPClientError):
+ """An error ocurred while authenticating.
+
+ Either the server rejected our request for authentication or the
+ challenge received was malformed.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class TLSError(ESMTPClientError):
+ """An error occurred while negiotiating for transport security.
+
+ This is considered a non-fatal error (the connection will not be
+ dropped).
+ """
+
+class SMTPConnectError(SMTPClientError):
+ """Failed to connect to the mail exchange host.
+
+ This is considered a fatal error. A retry will be made.
+ """
+ def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
+ SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
+
+class SMTPTimeoutError(SMTPClientError):
+ """Failed to receive a response from the server in the expected time period.
+
+ This is considered a fatal error. A retry will be made.
+ """
+ def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
+ SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
+
+class SMTPProtocolError(SMTPClientError):
+ """The server sent a mangled response.
+
+ This is considered a fatal error. A retry will not be made.
+ """
+ def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False):
+ SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
+
+class SMTPDeliveryError(SMTPClientError):
+ """Indicates that a delivery attempt has had an error.
+ """
+
+class SMTPServerError(SMTPError):
+ def __init__(self, code, resp):
+ self.code = code
+ self.resp = resp
+
+ def __str__(self):
+ return "%.3d %s" % (self.code, self.resp)
+
+class SMTPAddressError(SMTPServerError):
+ def __init__(self, addr, code, resp):
+ SMTPServerError.__init__(self, code, resp)
+ self.addr = Address(addr)
+
+ def __str__(self):
+ return "%.3d <%s>... %s" % (self.code, self.addr, self.resp)
+
+class SMTPBadRcpt(SMTPAddressError):
+ def __init__(self, addr, code=550,
+ resp='Cannot receive for specified address'):
+ SMTPAddressError.__init__(self, addr, code, resp)
+
+class SMTPBadSender(SMTPAddressError):
+ def __init__(self, addr, code=550, resp='Sender not acceptable'):
+ SMTPAddressError.__init__(self, addr, code, resp)
+
+def rfc822date(timeinfo=None,local=1):
+ """
+ Format an RFC-2822 compliant date string.
+
+ @param timeinfo: (optional) A sequence as returned by C{time.localtime()}
+ or C{time.gmtime()}. Default is now.
+ @param local: (optional) Indicates if the supplied time is local or
+ universal time, or if no time is given, whether now should be local or
+ universal time. Default is local, as suggested (SHOULD) by rfc-2822.
+
+ @returns: A string representing the time and date in RFC-2822 format.
+ """
+ if not timeinfo:
+ if local:
+ timeinfo = time.localtime()
+ else:
+ timeinfo = time.gmtime()
+ if local:
+ if timeinfo[8]:
+ # DST
+ tz = -time.altzone
+ else:
+ tz = -time.timezone
+
+ (tzhr, tzmin) = divmod(abs(tz), 3600)
+ if tz:
+ tzhr *= int(abs(tz)/tz)
+ (tzmin, tzsec) = divmod(tzmin, 60)
+ else:
+ (tzhr, tzmin) = (0,0)
+
+ return "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d" % (
+ ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timeinfo[6]],
+ timeinfo[2],
+ ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timeinfo[1] - 1],
+ timeinfo[0], timeinfo[3], timeinfo[4], timeinfo[5],
+ tzhr, tzmin)
+
+def idGenerator():
+ i = 0
+ while True:
+ yield i
+ i += 1
+
+def messageid(uniq=None, N=idGenerator().next):
+ """Return a globally unique random string in RFC 2822 Message-ID format
+
+ <datetime.pid.random@host.dom.ain>
+
+ Optional uniq string will be added to strenghten uniqueness if given.
+ """
+ datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
+ pid = os.getpid()
+ rand = random.randrange(2**31L-1)
+ if uniq is None:
+ uniq = ''
+ else:
+ uniq = '.' + uniq
+
+ return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME)
+
+def quoteaddr(addr):
+ """Turn an email address, possibly with realname part etc, into
+ a form suitable for and SMTP envelope.
+ """
+
+ if isinstance(addr, Address):
+ return '<%s>' % str(addr)
+
+ res = rfc822.parseaddr(addr)
+
+ if res == (None, None):
+ # It didn't parse, use it as-is
+ return '<%s>' % str(addr)
+ else:
+ return '<%s>' % str(res[1])
+
+COMMAND, DATA, AUTH = 'COMMAND', 'DATA', 'AUTH'
+
+class AddressError(SMTPError):
+ "Parse error in address"
+
+# Character classes for parsing addresses
+atom = r"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]"
+
+class Address:
+ """Parse and hold an RFC 2821 address.
+
+ Source routes are stipped and ignored, UUCP-style bang-paths
+ and %-style routing are not parsed.
+
+ @type domain: C{str}
+ @ivar domain: The domain within which this address resides.
+
+ @type local: C{str}
+ @ivar local: The local (\"user\") portion of this address.
+ """
+
+ tstring = re.compile(r'''( # A string of
+ (?:"[^"]*" # quoted string
+ |\\. # backslash-escaped characted
+ |''' + atom + r''' # atom character
+ )+|.) # or any single character''',re.X)
+ atomre = re.compile(atom) # match any one atom character
+
+ def __init__(self, addr, defaultDomain=None):
+ if isinstance(addr, User):
+ addr = addr.dest
+ if isinstance(addr, Address):
+ self.__dict__ = addr.__dict__.copy()
+ return
+ elif not isinstance(addr, types.StringTypes):
+ addr = str(addr)
+ self.addrstr = addr
+
+ # Tokenize
+ atl = filter(None,self.tstring.split(addr))
+
+ local = []
+ domain = []
+
+ while atl:
+ if atl[0] == '<':
+ if atl[-1] != '>':
+ raise AddressError, "Unbalanced <>"
+ atl = atl[1:-1]
+ elif atl[0] == '@':
+ atl = atl[1:]
+ if not local:
+ # Source route
+ while atl and atl[0] != ':':
+ # remove it
+ atl = atl[1:]
+ if not atl:
+ raise AddressError, "Malformed source route"
+ atl = atl[1:] # remove :
+ elif domain:
+ raise AddressError, "Too many @"
+ else:
+ # Now in domain
+ domain = ['']
+ elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] != '.':
+ raise AddressError, "Parse error at %r of %r" % (atl[0], (addr, atl))
+ else:
+ if not domain:
+ local.append(atl[0])
+ else:
+ domain.append(atl[0])
+ atl = atl[1:]
+
+ self.local = ''.join(local)
+ self.domain = ''.join(domain)
+ if self.local != '' and self.domain == '':
+ if defaultDomain is None:
+ defaultDomain = DNSNAME
+ self.domain = defaultDomain
+
+ dequotebs = re.compile(r'\\(.)')
+
+ def dequote(self,addr):
+ """Remove RFC-2821 quotes from address."""
+ res = []
+
+ atl = filter(None,self.tstring.split(str(addr)))
+
+ for t in atl:
+ if t[0] == '"' and t[-1] == '"':
+ res.append(t[1:-1])
+ elif '\\' in t:
+ res.append(self.dequotebs.sub(r'\1',t))
+ else:
+ res.append(t)
+
+ return ''.join(res)
+
+ def __str__(self):
+ if self.local or self.domain:
+ return '@'.join((self.local, self.domain))
+ else:
+ return ''
+
+ def __repr__(self):
+ return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
+ repr(str(self)))
+
+class User:
+ """Hold information about and SMTP message recipient,
+ including information on where the message came from
+ """
+
+ def __init__(self, destination, helo, protocol, orig):
+ host = getattr(protocol, 'host', None)
+ self.dest = Address(destination, host)
+ self.helo = helo
+ self.protocol = protocol
+ if isinstance(orig, Address):
+ self.orig = orig
+ else:
+ self.orig = Address(orig, host)
+
+ def __getstate__(self):
+ """Helper for pickle.
+
+ protocol isn't picklabe, but we want User to be, so skip it in
+ the pickle.
+ """
+ return { 'dest' : self.dest,
+ 'helo' : self.helo,
+ 'protocol' : None,
+ 'orig' : self.orig }
+
+ def __str__(self):
+ return str(self.dest)
+
+class IMessage(Interface):
+ """Interface definition for messages that can be sent via SMTP."""
+
+ def lineReceived(line):
+ """handle another line"""
+
+ def eomReceived():
+ """handle end of message
+
+ return a deferred. The deferred should be called with either:
+ callback(string) or errback(error)
+ """
+
+ def connectionLost():
+ """handle message truncated
+
+ semantics should be to discard the message
+ """
+
+class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """SMTP server-side protocol."""
+
+ timeout = 600
+ host = DNSNAME
+ portal = None
+
+ # Control whether we log SMTP events
+ noisy = True
+
+ # A factory for IMessageDelivery objects. If an
+ # avatar implementing IMessageDeliveryFactory can
+ # be acquired from the portal, it will be used to
+ # create a new IMessageDelivery object for each
+ # message which is received.
+ deliveryFactory = None
+
+ # An IMessageDelivery object. A new instance is
+ # used for each message received if we can get an
+ # IMessageDeliveryFactory from the portal. Otherwise,
+ # a single instance is used throughout the lifetime
+ # of the connection.
+ delivery = None
+
+ # Cred cleanup function.
+ _onLogout = None
+
+ def __init__(self, delivery=None, deliveryFactory=None):
+ self.mode = COMMAND
+ self._from = None
+ self._helo = None
+ self._to = []
+ self.delivery = delivery
+ self.deliveryFactory = deliveryFactory
+
+ def timeoutConnection(self):
+ msg = '%s Timeout. Try talking faster next time!' % (self.host,)
+ self.sendCode(421, msg)
+ self.transport.loseConnection()
+
+ def greeting(self):
+ return '%s NO UCE NO UBE NO RELAY PROBES' % (self.host,)
+
+ def connectionMade(self):
+ # Ensure user-code always gets something sane for _helo
+ peer = self.transport.getPeer()
+ try:
+ host = peer.host
+ except AttributeError: # not an IPv4Address
+ host = str(peer)
+ self._helo = (None, host)
+ self.sendCode(220, self.greeting())
+ self.setTimeout(self.timeout)
+
+ def sendCode(self, code, message=''):
+ "Send an SMTP code with a message."
+ lines = message.splitlines()
+ lastline = lines[-1:]
+ for line in lines[:-1]:
+ self.sendLine('%3.3d-%s' % (code, line))
+ self.sendLine('%3.3d %s' % (code,
+ lastline and lastline[0] or ''))
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+ return getattr(self, 'state_' + self.mode)(line)
+
+ def state_COMMAND(self, line):
+ # Ignore leading and trailing whitespace, as well as an arbitrary
+ # amount of whitespace between the command and its argument, though
+ # it is not required by the protocol, for it is a nice thing to do.
+ line = line.strip()
+
+ parts = line.split(None, 1)
+ if parts:
+ method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
+ if len(parts) == 2:
+ method(parts[1])
+ else:
+ method('')
+ else:
+ self.sendSyntaxError()
+
+ def sendSyntaxError(self):
+ self.sendCode(500, 'Error: bad syntax')
+
+ def lookupMethod(self, command):
+ return getattr(self, 'do_' + command.upper(), None)
+
+ def lineLengthExceeded(self, line):
+ if self.mode is DATA:
+ for message in self.__messages:
+ message.connectionLost()
+ self.mode = COMMAND
+ del self.__messages
+ self.sendCode(500, 'Line too long')
+
+ def do_UNKNOWN(self, rest):
+ self.sendCode(500, 'Command not implemented')
+
+ def do_HELO(self, rest):
+ peer = self.transport.getPeer()
+ try:
+ host = peer.host
+ except AttributeError:
+ host = str(peer)
+ self._helo = (rest, host)
+ self._from = None
+ self._to = []
+ self.sendCode(250, '%s Hello %s, nice to meet you' % (self.host, host))
+
+ def do_QUIT(self, rest):
+ self.sendCode(221, 'See you later')
+ self.transport.loseConnection()
+
+ # A string of quoted strings, backslash-escaped character or
+ # atom characters + '@.,:'
+ qstring = r'("[^"]*"|\\.|' + atom + r'|[@.,:])+'
+
+ mail_re = re.compile(r'''\s*FROM:\s*(?P<path><> # Empty <>
+ |<''' + qstring + r'''> # <addr>
+ |''' + qstring + r''' # addr
+ )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
+ $''',re.I|re.X)
+ rcpt_re = re.compile(r'\s*TO:\s*(?P<path><' + qstring + r'''> # <addr>
+ |''' + qstring + r''' # addr
+ )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
+ $''',re.I|re.X)
+
+ def do_MAIL(self, rest):
+ if self._from:
+ self.sendCode(503,"Only one sender per message, please")
+ return
+ # Clear old recipient list
+ self._to = []
+ m = self.mail_re.match(rest)
+ if not m:
+ self.sendCode(501, "Syntax error")
+ return
+
+ try:
+ addr = Address(m.group('path'), self.host)
+ except AddressError, e:
+ self.sendCode(553, str(e))
+ return
+
+ validated = defer.maybeDeferred(self.validateFrom, self._helo, addr)
+ validated.addCallbacks(self._cbFromValidate, self._ebFromValidate)
+
+
+ def _cbFromValidate(self, from_, code=250, msg='Sender address accepted'):
+ self._from = from_
+ self.sendCode(code, msg)
+
+
+ def _ebFromValidate(self, failure):
+ if failure.check(SMTPBadSender):
+ self.sendCode(failure.value.code,
+ 'Cannot receive from specified address %s: %s'
+ % (quoteaddr(failure.value.addr), failure.value.resp))
+ elif failure.check(SMTPServerError):
+ self.sendCode(failure.value.code, failure.value.resp)
+ else:
+ log.err(failure, "SMTP sender validation failure")
+ self.sendCode(
+ 451,
+ 'Requested action aborted: local error in processing')
+
+
+ def do_RCPT(self, rest):
+ if not self._from:
+ self.sendCode(503, "Must have sender before recipient")
+ return
+ m = self.rcpt_re.match(rest)
+ if not m:
+ self.sendCode(501, "Syntax error")
+ return
+
+ try:
+ user = User(m.group('path'), self._helo, self, self._from)
+ except AddressError, e:
+ self.sendCode(553, str(e))
+ return
+
+ d = defer.maybeDeferred(self.validateTo, user)
+ d.addCallbacks(
+ self._cbToValidate,
+ self._ebToValidate,
+ callbackArgs=(user,)
+ )
+
+ def _cbToValidate(self, to, user=None, code=250, msg='Recipient address accepted'):
+ if user is None:
+ user = to
+ self._to.append((user, to))
+ self.sendCode(code, msg)
+
+ def _ebToValidate(self, failure):
+ if failure.check(SMTPBadRcpt, SMTPServerError):
+ self.sendCode(failure.value.code, failure.value.resp)
+ else:
+ log.err(failure)
+ self.sendCode(
+ 451,
+ 'Requested action aborted: local error in processing'
+ )
+
+ def _disconnect(self, msgs):
+ for msg in msgs:
+ try:
+ msg.connectionLost()
+ except:
+ log.msg("msg raised exception from connectionLost")
+ log.err()
+
+ def do_DATA(self, rest):
+ if self._from is None or (not self._to):
+ self.sendCode(503, 'Must have valid receiver and originator')
+ return
+ self.mode = DATA
+ helo, origin = self._helo, self._from
+ recipients = self._to
+
+ self._from = None
+ self._to = []
+ self.datafailed = None
+
+ msgs = []
+ for (user, msgFunc) in recipients:
+ try:
+ msg = msgFunc()
+ rcvdhdr = self.receivedHeader(helo, origin, [user])
+ if rcvdhdr:
+ msg.lineReceived(rcvdhdr)
+ msgs.append(msg)
+ except SMTPServerError, e:
+ self.sendCode(e.code, e.resp)
+ self.mode = COMMAND
+ self._disconnect(msgs)
+ return
+ except:
+ log.err()
+ self.sendCode(550, "Internal server error")
+ self.mode = COMMAND
+ self._disconnect(msgs)
+ return
+ self.__messages = msgs
+
+ self.__inheader = self.__inbody = 0
+ self.sendCode(354, 'Continue')
+
+ if self.noisy:
+ fmt = 'Receiving message for delivery: from=%s to=%s'
+ log.msg(fmt % (origin, [str(u) for (u, f) in recipients]))
+
+ def connectionLost(self, reason):
+ # self.sendCode(421, 'Dropping connection.') # This does nothing...
+ # Ideally, if we (rather than the other side) lose the connection,
+ # we should be able to tell the other side that we are going away.
+ # RFC-2821 requires that we try.
+ if self.mode is DATA:
+ try:
+ for message in self.__messages:
+ try:
+ message.connectionLost()
+ except:
+ log.err()
+ del self.__messages
+ except AttributeError:
+ pass
+ if self._onLogout:
+ self._onLogout()
+ self._onLogout = None
+ self.setTimeout(None)
+
+ def do_RSET(self, rest):
+ self._from = None
+ self._to = []
+ self.sendCode(250, 'I remember nothing.')
+
+ def dataLineReceived(self, line):
+ if line[:1] == '.':
+ if line == '.':
+ self.mode = COMMAND
+ if self.datafailed:
+ self.sendCode(self.datafailed.code,
+ self.datafailed.resp)
+ return
+ if not self.__messages:
+ self._messageHandled("thrown away")
+ return
+ defer.DeferredList([
+ m.eomReceived() for m in self.__messages
+ ], consumeErrors=True).addCallback(self._messageHandled
+ )
+ del self.__messages
+ return
+ line = line[1:]
+
+ if self.datafailed:
+ return
+
+ try:
+ # Add a blank line between the generated Received:-header
+ # and the message body if the message comes in without any
+ # headers
+ if not self.__inheader and not self.__inbody:
+ if ':' in line:
+ self.__inheader = 1
+ elif line:
+ for message in self.__messages:
+ message.lineReceived('')
+ self.__inbody = 1
+
+ if not line:
+ self.__inbody = 1
+
+ for message in self.__messages:
+ message.lineReceived(line)
+ except SMTPServerError, e:
+ self.datafailed = e
+ for message in self.__messages:
+ message.connectionLost()
+ state_DATA = dataLineReceived
+
+ def _messageHandled(self, resultList):
+ failures = 0
+ for (success, result) in resultList:
+ if not success:
+ failures += 1
+ log.err(result)
+ if failures:
+ msg = 'Could not send e-mail'
+ L = len(resultList)
+ if L > 1:
+ msg += ' (%d failures out of %d recipients)' % (failures, L)
+ self.sendCode(550, msg)
+ else:
+ self.sendCode(250, 'Delivery in progress')
+
+
+ def _cbAnonymousAuthentication(self, (iface, avatar, logout)):
+ """
+ Save the state resulting from a successful anonymous cred login.
+ """
+ if issubclass(iface, IMessageDeliveryFactory):
+ self.deliveryFactory = avatar
+ self.delivery = None
+ elif issubclass(iface, IMessageDelivery):
+ self.deliveryFactory = None
+ self.delivery = avatar
+ else:
+ raise RuntimeError("%s is not a supported interface" % (iface.__name__,))
+ self._onLogout = logout
+ self.challenger = None
+
+
+ # overridable methods:
+ def validateFrom(self, helo, origin):
+ """
+ Validate the address from which the message originates.
+
+ @type helo: C{(str, str)}
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: C{Address}
+ @param origin: The address the message is from
+
+ @rtype: C{Deferred} or C{Address}
+ @return: C{origin} or a C{Deferred} whose callback will be
+ passed C{origin}.
+
+ @raise SMTPBadSender: Raised of messages from this address are
+ not to be accepted.
+ """
+ if self.deliveryFactory is not None:
+ self.delivery = self.deliveryFactory.getMessageDelivery()
+
+ if self.delivery is not None:
+ return defer.maybeDeferred(self.delivery.validateFrom,
+ helo, origin)
+
+ # No login has been performed, no default delivery object has been
+ # provided: try to perform an anonymous login and then invoke this
+ # method again.
+ if self.portal:
+
+ result = self.portal.login(
+ cred.credentials.Anonymous(),
+ None,
+ IMessageDeliveryFactory, IMessageDelivery)
+
+ def ebAuthentication(err):
+ """
+ Translate cred exceptions into SMTP exceptions so that the
+ protocol code which invokes C{validateFrom} can properly report
+ the failure.
+ """
+ if err.check(cred.error.UnauthorizedLogin):
+ exc = SMTPBadSender(origin)
+ elif err.check(cred.error.UnhandledCredentials):
+ exc = SMTPBadSender(
+ origin, resp="Unauthenticated senders not allowed")
+ else:
+ return err
+ return defer.fail(exc)
+
+ result.addCallbacks(
+ self._cbAnonymousAuthentication, ebAuthentication)
+
+ def continueValidation(ignored):
+ """
+ Re-attempt from address validation.
+ """
+ return self.validateFrom(helo, origin)
+
+ result.addCallback(continueValidation)
+ return result
+
+ raise SMTPBadSender(origin)
+
+
+ def validateTo(self, user):
+ """
+ Validate the address for which the message is destined.
+
+ @type user: C{User}
+ @param user: The address to validate.
+
+ @rtype: no-argument callable
+ @return: A C{Deferred} which becomes, or a callable which
+ takes no arguments and returns an object implementing C{IMessage}.
+ This will be called and the returned object used to deliver the
+ message when it arrives.
+
+ @raise SMTPBadRcpt: Raised if messages to the address are
+ not to be accepted.
+ """
+ if self.delivery is not None:
+ return self.delivery.validateTo(user)
+ raise SMTPBadRcpt(user)
+
+ def receivedHeader(self, helo, origin, recipients):
+ if self.delivery is not None:
+ return self.delivery.receivedHeader(helo, origin, recipients)
+
+ heloStr = ""
+ if helo[0]:
+ heloStr = " helo=%s" % (helo[0],)
+ domain = self.transport.getHost().host
+ from_ = "from %s ([%s]%s)" % (helo[0], helo[1], heloStr)
+ by = "by %s with %s (%s)" % (domain,
+ self.__class__.__name__,
+ longversion)
+ for_ = "for %s; %s" % (' '.join(map(str, recipients)),
+ rfc822date())
+ return "Received: %s\n\t%s\n\t%s" % (from_, by, for_)
+
+ def startMessage(self, recipients):
+ if self.delivery:
+ return self.delivery.startMessage(recipients)
+ return []
+
+
+class SMTPFactory(protocol.ServerFactory):
+ """Factory for SMTP."""
+
+ # override in instances or subclasses
+ domain = DNSNAME
+ timeout = 600
+ protocol = SMTP
+
+ portal = None
+
+ def __init__(self, portal = None):
+ self.portal = portal
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ p.portal = self.portal
+ p.host = self.domain
+ return p
+
+class SMTPClient(basic.LineReceiver, policies.TimeoutMixin):
+ """
+ SMTP client for sending emails.
+
+ After the client has connected to the SMTP server, it repeatedly calls
+ L{SMTPClient.getMailFrom}, L{SMTPClient.getMailTo} and
+ L{SMTPClient.getMailData} and uses this information to send an email.
+ It then calls L{SMTPClient.getMailFrom} again; if it returns C{None}, the
+ client will disconnect, otherwise it will continue as normal i.e. call
+ L{SMTPClient.getMailTo} and L{SMTPClient.getMailData} and send a new email.
+ """
+
+ # If enabled then log SMTP client server communication
+ debug = True
+
+ # Number of seconds to wait before timing out a connection. If
+ # None, perform no timeout checking.
+ timeout = None
+
+ def __init__(self, identity, logsize=10):
+ self.identity = identity or ''
+ self.toAddressesResult = []
+ self.successAddresses = []
+ self._from = None
+ self.resp = []
+ self.code = -1
+ self.log = util.LineLog(logsize)
+
+ def sendLine(self, line):
+ # Log sendLine only if you are in debug mode for performance
+ if self.debug:
+ self.log.append('>>> ' + line)
+
+ basic.LineReceiver.sendLine(self,line)
+
+ def connectionMade(self):
+ self.setTimeout(self.timeout)
+
+ self._expected = [ 220 ]
+ self._okresponse = self.smtpState_helo
+ self._failresponse = self.smtpConnectionFailed
+
+ def connectionLost(self, reason=protocol.connectionDone):
+ """We are no longer connected"""
+ self.setTimeout(None)
+ self.mailFile = None
+
+ def timeoutConnection(self):
+ self.sendError(
+ SMTPTimeoutError(
+ -1, "Timeout waiting for SMTP server response",
+ self.log.str()))
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+
+ # Log lineReceived only if you are in debug mode for performance
+ if self.debug:
+ self.log.append('<<< ' + line)
+
+ why = None
+
+ try:
+ self.code = int(line[:3])
+ except ValueError:
+ # This is a fatal error and will disconnect the transport lineReceived will not be called again
+ self.sendError(SMTPProtocolError(-1, "Invalid response from SMTP server: %s" % line, self.log.str()))
+ return
+
+ if line[0] == '0':
+ # Verbose informational message, ignore it
+ return
+
+ self.resp.append(line[4:])
+
+ if line[3:4] == '-':
+ # continuation
+ return
+
+ if self.code in self._expected:
+ why = self._okresponse(self.code,'\n'.join(self.resp))
+ else:
+ why = self._failresponse(self.code,'\n'.join(self.resp))
+
+ self.code = -1
+ self.resp = []
+ return why
+
+ def smtpConnectionFailed(self, code, resp):
+ self.sendError(SMTPConnectError(code, resp, self.log.str()))
+
+ def smtpTransferFailed(self, code, resp):
+ if code < 0:
+ self.sendError(SMTPProtocolError(code, resp, self.log.str()))
+ else:
+ self.smtpState_msgSent(code, resp)
+
+ def smtpState_helo(self, code, resp):
+ self.sendLine('HELO ' + self.identity)
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_from
+
+ def smtpState_from(self, code, resp):
+ self._from = self.getMailFrom()
+ self._failresponse = self.smtpTransferFailed
+ if self._from is not None:
+ self.sendLine('MAIL FROM:%s' % quoteaddr(self._from))
+ self._expected = [250]
+ self._okresponse = self.smtpState_to
+ else:
+ # All messages have been sent, disconnect
+ self._disconnectFromServer()
+
+ def smtpState_disconnect(self, code, resp):
+ self.transport.loseConnection()
+
+ def smtpState_to(self, code, resp):
+ self.toAddresses = iter(self.getMailTo())
+ self.toAddressesResult = []
+ self.successAddresses = []
+ self._okresponse = self.smtpState_toOrData
+ self._expected = xrange(0,1000)
+ self.lastAddress = None
+ return self.smtpState_toOrData(0, '')
+
+ def smtpState_toOrData(self, code, resp):
+ if self.lastAddress is not None:
+ self.toAddressesResult.append((self.lastAddress, code, resp))
+ if code in SUCCESS:
+ self.successAddresses.append(self.lastAddress)
+ try:
+ self.lastAddress = self.toAddresses.next()
+ except StopIteration:
+ if self.successAddresses:
+ self.sendLine('DATA')
+ self._expected = [ 354 ]
+ self._okresponse = self.smtpState_data
+ else:
+ return self.smtpState_msgSent(code,'No recipients accepted')
+ else:
+ self.sendLine('RCPT TO:%s' % quoteaddr(self.lastAddress))
+
+ def smtpState_data(self, code, resp):
+ s = basic.FileSender()
+ d = s.beginFileTransfer(
+ self.getMailData(), self.transport, self.transformChunk)
+ def ebTransfer(err):
+ self.sendError(err.value)
+ d.addCallbacks(self.finishedFileTransfer, ebTransfer)
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_msgSent
+
+
+ def smtpState_msgSent(self, code, resp):
+ if self._from is not None:
+ self.sentMail(code, resp, len(self.successAddresses),
+ self.toAddressesResult, self.log)
+
+ self.toAddressesResult = []
+ self._from = None
+ self.sendLine('RSET')
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_from
+
+ ##
+ ## Helpers for FileSender
+ ##
+ def transformChunk(self, chunk):
+ """
+ Perform the necessary local to network newline conversion and escape
+ leading periods.
+
+ This method also resets the idle timeout so that as long as process is
+ being made sending the message body, the client will not time out.
+ """
+ self.resetTimeout()
+ return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
+
+ def finishedFileTransfer(self, lastsent):
+ if lastsent != '\n':
+ line = '\r\n.'
+ else:
+ line = '.'
+ self.sendLine(line)
+
+ ##
+ # these methods should be overriden in subclasses
+ def getMailFrom(self):
+ """Return the email address the mail is from."""
+ raise NotImplementedError
+
+ def getMailTo(self):
+ """Return a list of emails to send to."""
+ raise NotImplementedError
+
+ def getMailData(self):
+ """Return file-like object containing data of message to be sent.
+
+ Lines in the file should be delimited by '\\n'.
+ """
+ raise NotImplementedError
+
+ def sendError(self, exc):
+ """
+ If an error occurs before a mail message is sent sendError will be
+ called. This base class method sends a QUIT if the error is
+ non-fatal and disconnects the connection.
+
+ @param exc: The SMTPClientError (or child class) raised
+ @type exc: C{SMTPClientError}
+ """
+ if isinstance(exc, SMTPClientError) and not exc.isFatal:
+ self._disconnectFromServer()
+ else:
+ # If the error was fatal then the communication channel with the
+ # SMTP Server is broken so just close the transport connection
+ self.smtpState_disconnect(-1, None)
+
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ """Called when an attempt to send an email is completed.
+
+ If some addresses were accepted, code and resp are the response
+ to the DATA command. If no addresses were accepted, code is -1
+ and resp is an informative message.
+
+ @param code: the code returned by the SMTP Server
+ @param resp: The string response returned from the SMTP Server
+ @param numOK: the number of addresses accepted by the remote host.
+ @param addresses: is a list of tuples (address, code, resp) listing
+ the response to each RCPT command.
+ @param log: is the SMTP session log
+ """
+ raise NotImplementedError
+
+ def _disconnectFromServer(self):
+ self._expected = xrange(0, 1000)
+ self._okresponse = self.smtpState_disconnect
+ self.sendLine('QUIT')
+
+
+
+class ESMTPClient(SMTPClient):
+ # Fall back to HELO if the server does not support EHLO
+ heloFallback = True
+
+ # Refuse to proceed if authentication cannot be performed
+ requireAuthentication = False
+
+ # Refuse to proceed if TLS is not available
+ requireTransportSecurity = False
+
+ # Indicate whether or not our transport can be considered secure.
+ tlsMode = False
+
+ # ClientContextFactory to use for STARTTLS
+ context = None
+
+ def __init__(self, secret, contextFactory=None, *args, **kw):
+ SMTPClient.__init__(self, *args, **kw)
+ self.authenticators = []
+ self.secret = secret
+ self.context = contextFactory
+ self.tlsMode = False
+
+
+ def esmtpEHLORequired(self, code=-1, resp=None):
+ self.sendError(EHLORequiredError(502, "Server does not support ESMTP Authentication", self.log.str()))
+
+
+ def esmtpAUTHRequired(self, code=-1, resp=None):
+ tmp = []
+
+ for a in self.authenticators:
+ tmp.append(a.getName().upper())
+
+ auth = "[%s]" % ', '.join(tmp)
+
+ self.sendError(AUTHRequiredError(502, "Server does not support Client Authentication schemes %s" % auth,
+ self.log.str()))
+
+
+ def esmtpTLSRequired(self, code=-1, resp=None):
+ self.sendError(TLSRequiredError(502, "Server does not support secure communication via TLS / SSL",
+ self.log.str()))
+
+ def esmtpTLSFailed(self, code=-1, resp=None):
+ self.sendError(TLSError(code, "Could not complete the SSL/TLS handshake", self.log.str()))
+
+ def esmtpAUTHDeclined(self, code=-1, resp=None):
+ self.sendError(AUTHDeclinedError(code, resp, self.log.str()))
+
+ def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
+ str = "Login failed because the SMTP Server returned a malformed Authentication Challenge"
+ self.sendError(AuthenticationError(501, str, self.log.str()))
+
+ def esmtpAUTHServerError(self, code=-1, resp=None):
+ self.sendError(AuthenticationError(code, resp, self.log.str()))
+
+ def registerAuthenticator(self, auth):
+ """Registers an Authenticator with the ESMTPClient. The ESMTPClient
+ will attempt to login to the SMTP Server in the order the
+ Authenticators are registered. The most secure Authentication
+ mechanism should be registered first.
+
+ @param auth: The Authentication mechanism to register
+ @type auth: class implementing C{IClientAuthentication}
+ """
+
+ self.authenticators.append(auth)
+
+ def connectionMade(self):
+ SMTPClient.connectionMade(self)
+ self._okresponse = self.esmtpState_ehlo
+
+ def esmtpState_ehlo(self, code, resp):
+ self._expected = SUCCESS
+
+ self._okresponse = self.esmtpState_serverConfig
+ self._failresponse = self.esmtpEHLORequired
+
+ if self.heloFallback:
+ self._failresponse = self.smtpState_helo
+
+ self.sendLine('EHLO ' + self.identity)
+
+ def esmtpState_serverConfig(self, code, resp):
+ items = {}
+ for line in resp.splitlines():
+ e = line.split(None, 1)
+ if len(e) > 1:
+ items[e[0]] = e[1]
+ else:
+ items[e[0]] = None
+
+ if self.tlsMode:
+ self.authenticate(code, resp, items)
+ else:
+ self.tryTLS(code, resp, items)
+
+ def tryTLS(self, code, resp, items):
+ if self.context and 'STARTTLS' in items:
+ self._expected = [220]
+ self._okresponse = self.esmtpState_starttls
+ self._failresponse = self.esmtpTLSFailed
+ self.sendLine('STARTTLS')
+ elif self.requireTransportSecurity:
+ self.tlsMode = False
+ self.esmtpTLSRequired()
+ else:
+ self.tlsMode = False
+ self.authenticate(code, resp, items)
+
+ def esmtpState_starttls(self, code, resp):
+ try:
+ self.transport.startTLS(self.context)
+ self.tlsMode = True
+ except:
+ log.err()
+ self.esmtpTLSFailed(451)
+
+ # Send another EHLO once TLS has been started to
+ # get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode.
+ self.esmtpState_ehlo(code, resp)
+
+ def authenticate(self, code, resp, items):
+ if self.secret and items.get('AUTH'):
+ schemes = items['AUTH'].split()
+ tmpSchemes = {}
+
+ #XXX: May want to come up with a more efficient way to do this
+ for s in schemes:
+ tmpSchemes[s.upper()] = 1
+
+ for a in self.authenticators:
+ auth = a.getName().upper()
+
+ if auth in tmpSchemes:
+ self._authinfo = a
+
+ # Special condition handled
+ if auth == "PLAIN":
+ self._okresponse = self.smtpState_from
+ self._failresponse = self._esmtpState_plainAuth
+ self._expected = [235]
+ challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 1), eol="")
+ self.sendLine('AUTH ' + auth + ' ' + challenge)
+ else:
+ self._expected = [334]
+ self._okresponse = self.esmtpState_challenge
+ # If some error occurs here, the server declined the AUTH
+ # before the user / password phase. This would be
+ # a very rare case
+ self._failresponse = self.esmtpAUTHServerError
+ self.sendLine('AUTH ' + auth)
+ return
+
+ if self.requireAuthentication:
+ self.esmtpAUTHRequired()
+ else:
+ self.smtpState_from(code, resp)
+
+ def _esmtpState_plainAuth(self, code, resp):
+ self._okresponse = self.smtpState_from
+ self._failresponse = self.esmtpAUTHDeclined
+ self._expected = [235]
+ challenge = encode_base64(self._authinfo.challengeResponse(self.secret, 2), eol="")
+ self.sendLine('AUTH PLAIN ' + challenge)
+
+ def esmtpState_challenge(self, code, resp):
+ self._authResponse(self._authinfo, resp)
+
+ def _authResponse(self, auth, challenge):
+ self._failresponse = self.esmtpAUTHDeclined
+ try:
+ challenge = base64.decodestring(challenge)
+ except binascii.Error:
+ # Illegal challenge, give up, then quit
+ self.sendLine('*')
+ self._okresponse = self.esmtpAUTHMalformedChallenge
+ self._failresponse = self.esmtpAUTHMalformedChallenge
+ else:
+ resp = auth.challengeResponse(self.secret, challenge)
+ self._expected = [235, 334]
+ self._okresponse = self.smtpState_maybeAuthenticated
+ self.sendLine(encode_base64(resp, eol=""))
+
+
+ def smtpState_maybeAuthenticated(self, code, resp):
+ """
+ Called to handle the next message from the server after sending a
+ response to a SASL challenge. The server response might be another
+ challenge or it might indicate authentication has succeeded.
+ """
+ if code == 235:
+ # Yes, authenticated!
+ del self._authinfo
+ self.smtpState_from(code, resp)
+ else:
+ # No, not authenticated yet. Keep trying.
+ self._authResponse(self._authinfo, resp)
+
+
+
+class ESMTP(SMTP):
+
+ ctx = None
+ canStartTLS = False
+ startedTLS = False
+
+ authenticated = False
+
+ def __init__(self, chal = None, contextFactory = None):
+ SMTP.__init__(self)
+ if chal is None:
+ chal = {}
+ self.challengers = chal
+ self.authenticated = False
+ self.ctx = contextFactory
+
+ def connectionMade(self):
+ SMTP.connectionMade(self)
+ self.canStartTLS = ITLSTransport.providedBy(self.transport)
+ self.canStartTLS = self.canStartTLS and (self.ctx is not None)
+
+
+ def greeting(self):
+ return SMTP.greeting(self) + ' ESMTP'
+
+
+ def extensions(self):
+ ext = {'AUTH': self.challengers.keys()}
+ if self.canStartTLS and not self.startedTLS:
+ ext['STARTTLS'] = None
+ return ext
+
+ def lookupMethod(self, command):
+ m = SMTP.lookupMethod(self, command)
+ if m is None:
+ m = getattr(self, 'ext_' + command.upper(), None)
+ return m
+
+ def listExtensions(self):
+ r = []
+ for (c, v) in self.extensions().iteritems():
+ if v is not None:
+ if v:
+ # Intentionally omit extensions with empty argument lists
+ r.append('%s %s' % (c, ' '.join(v)))
+ else:
+ r.append(c)
+ return '\n'.join(r)
+
+ def do_EHLO(self, rest):
+ peer = self.transport.getPeer().host
+ self._helo = (rest, peer)
+ self._from = None
+ self._to = []
+ self.sendCode(
+ 250,
+ '%s Hello %s, nice to meet you\n%s' % (
+ self.host, peer,
+ self.listExtensions(),
+ )
+ )
+
+ def ext_STARTTLS(self, rest):
+ if self.startedTLS:
+ self.sendCode(503, 'TLS already negotiated')
+ elif self.ctx and self.canStartTLS:
+ self.sendCode(220, 'Begin TLS negotiation now')
+ self.transport.startTLS(self.ctx)
+ self.startedTLS = True
+ else:
+ self.sendCode(454, 'TLS not available')
+
+ def ext_AUTH(self, rest):
+ if self.authenticated:
+ self.sendCode(503, 'Already authenticated')
+ return
+ parts = rest.split(None, 1)
+ chal = self.challengers.get(parts[0].upper(), lambda: None)()
+ if not chal:
+ self.sendCode(504, 'Unrecognized authentication type')
+ return
+
+ self.mode = AUTH
+ self.challenger = chal
+
+ if len(parts) > 1:
+ chal.getChallenge() # Discard it, apparently the client does not
+ # care about it.
+ rest = parts[1]
+ else:
+ rest = None
+ self.state_AUTH(rest)
+
+
+ def _cbAuthenticated(self, loginInfo):
+ """
+ Save the state resulting from a successful cred login and mark this
+ connection as authenticated.
+ """
+ result = SMTP._cbAnonymousAuthentication(self, loginInfo)
+ self.authenticated = True
+ return result
+
+
+ def _ebAuthenticated(self, reason):
+ """
+ Handle cred login errors by translating them to the SMTP authenticate
+ failed. Translate all other errors into a generic SMTP error code and
+ log the failure for inspection. Stop all errors from propagating.
+ """
+ self.challenge = None
+ if reason.check(cred.error.UnauthorizedLogin):
+ self.sendCode(535, 'Authentication failed')
+ else:
+ log.err(reason, "SMTP authentication failure")
+ self.sendCode(
+ 451,
+ 'Requested action aborted: local error in processing')
+
+
+ def state_AUTH(self, response):
+ """
+ Handle one step of challenge/response authentication.
+
+ @param response: The text of a response. If None, this
+ function has been called as a result of an AUTH command with
+ no initial response. A response of '*' aborts authentication,
+ as per RFC 2554.
+ """
+ if self.portal is None:
+ self.sendCode(454, 'Temporary authentication failure')
+ self.mode = COMMAND
+ return
+
+ if response is None:
+ challenge = self.challenger.getChallenge()
+ encoded = challenge.encode('base64')
+ self.sendCode(334, encoded)
+ return
+
+ if response == '*':
+ self.sendCode(501, 'Authentication aborted')
+ self.challenger = None
+ self.mode = COMMAND
+ return
+
+ try:
+ uncoded = response.decode('base64')
+ except binascii.Error:
+ self.sendCode(501, 'Syntax error in parameters or arguments')
+ self.challenger = None
+ self.mode = COMMAND
+ return
+
+ self.challenger.setResponse(uncoded)
+ if self.challenger.moreChallenges():
+ challenge = self.challenger.getChallenge()
+ coded = challenge.encode('base64')[:-1]
+ self.sendCode(334, coded)
+ return
+
+ self.mode = COMMAND
+ result = self.portal.login(
+ self.challenger, None,
+ IMessageDeliveryFactory, IMessageDelivery)
+ result.addCallback(self._cbAuthenticated)
+ result.addCallback(lambda ign: self.sendCode(235, 'Authentication successful.'))
+ result.addErrback(self._ebAuthenticated)
+
+
+
+class SenderMixin:
+ """Utility class for sending emails easily.
+
+ Use with SMTPSenderFactory or ESMTPSenderFactory.
+ """
+ done = 0
+
+ def getMailFrom(self):
+ if not self.done:
+ self.done = 1
+ return str(self.factory.fromEmail)
+ else:
+ return None
+
+ def getMailTo(self):
+ return self.factory.toEmail
+
+ def getMailData(self):
+ return self.factory.file
+
+ def sendError(self, exc):
+ # Call the base class to close the connection with the SMTP server
+ SMTPClient.sendError(self, exc)
+
+ # Do not retry to connect to SMTP Server if:
+ # 1. No more retries left (This allows the correct error to be returned to the errorback)
+ # 2. retry is false
+ # 3. The error code is not in the 4xx range (Communication Errors)
+
+ if (self.factory.retries >= 0 or
+ (not exc.retry and not (exc.code >= 400 and exc.code < 500))):
+ self.factory.sendFinished = 1
+ self.factory.result.errback(exc)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ # Do not retry, the SMTP server acknowledged the request
+ self.factory.sendFinished = 1
+ if code not in SUCCESS:
+ errlog = []
+ for addr, acode, aresp in addresses:
+ if acode not in SUCCESS:
+ errlog.append("%s: %03d %s" % (addr, acode, aresp))
+
+ errlog.append(log.str())
+
+ exc = SMTPDeliveryError(code, resp, '\n'.join(errlog), addresses)
+ self.factory.result.errback(exc)
+ else:
+ self.factory.result.callback((numOk, addresses))
+
+
+class SMTPSender(SenderMixin, SMTPClient):
+ """
+ SMTP protocol that sends a single email based on information it
+ gets from its factory, a L{SMTPSenderFactory}.
+ """
+
+
+class SMTPSenderFactory(protocol.ClientFactory):
+ """
+ Utility factory for sending emails easily.
+ """
+
+ domain = DNSNAME
+ protocol = SMTPSender
+
+ def __init__(self, fromEmail, toEmail, file, deferred, retries=5,
+ timeout=None):
+ """
+ @param fromEmail: The RFC 2821 address from which to send this
+ message.
+
+ @param toEmail: A sequence of RFC 2821 addresses to which to
+ send this message.
+
+ @param file: A file-like object containing the message to send.
+
+ @param deferred: A Deferred to callback or errback when sending
+ of this message completes.
+
+ @param retries: The number of times to retry delivery of this
+ message.
+
+ @param timeout: Period, in seconds, for which to wait for
+ server responses, or None to wait forever.
+ """
+ assert isinstance(retries, (int, long))
+
+ if isinstance(toEmail, types.StringTypes):
+ toEmail = [toEmail]
+ self.fromEmail = Address(fromEmail)
+ self.nEmails = len(toEmail)
+ self.toEmail = toEmail
+ self.file = file
+ self.result = deferred
+ self.result.addBoth(self._removeDeferred)
+ self.sendFinished = 0
+
+ self.retries = -retries
+ self.timeout = timeout
+
+ def _removeDeferred(self, argh):
+ del self.result
+ return argh
+
+ def clientConnectionFailed(self, connector, err):
+ self._processConnectionError(connector, err)
+
+ def clientConnectionLost(self, connector, err):
+ self._processConnectionError(connector, err)
+
+ def _processConnectionError(self, connector, err):
+ if self.retries < self.sendFinished <= 0:
+ log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)
+
+ # Rewind the file in case part of it was read while attempting to
+ # send the message.
+ self.file.seek(0, 0)
+ connector.connect()
+ self.retries += 1
+ elif self.sendFinished <= 0:
+ # If we were unable to communicate with the SMTP server a ConnectionDone will be
+ # returned. We want a more clear error message for debugging
+ if err.check(error.ConnectionDone):
+ err.value = SMTPConnectError(-1, "Unable to connect to server.")
+ self.result.errback(err.value)
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self.domain, self.nEmails*2+2)
+ p.factory = self
+ p.timeout = self.timeout
+ return p
+
+
+
+from twisted.mail.imap4 import IClientAuthentication
+from twisted.mail.imap4 import CramMD5ClientAuthenticator, LOGINAuthenticator
+from twisted.mail.imap4 import LOGINCredentials as _lcredentials
+
+class LOGINCredentials(_lcredentials):
+ """
+ L{LOGINCredentials} generates challenges for I{LOGIN} authentication.
+
+ For interoperability with Outlook, the challenge generated does not exactly
+ match the one defined in the
+ U{draft specification<http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt>}.
+ """
+
+ def __init__(self):
+ _lcredentials.__init__(self)
+ self.challenges = ['Password:', 'Username:']
+
+
+
+class PLAINAuthenticator:
+ implements(IClientAuthentication)
+
+ def __init__(self, user):
+ self.user = user
+
+ def getName(self):
+ return "PLAIN"
+
+ def challengeResponse(self, secret, chal=1):
+ if chal == 1:
+ return "%s\0%s\0%s" % (self.user, self.user, secret)
+ else:
+ return "%s\0%s" % (self.user, secret)
+
+
+
+class ESMTPSender(SenderMixin, ESMTPClient):
+
+ requireAuthentication = True
+ requireTransportSecurity = True
+
+ def __init__(self, username, secret, contextFactory=None, *args, **kw):
+ self.heloFallback = 0
+ self.username = username
+
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ ESMTPClient.__init__(self, secret, contextFactory, *args, **kw)
+
+ self._registerAuthenticators()
+
+ def _registerAuthenticators(self):
+ # Register Authenticator in order from most secure to least secure
+ self.registerAuthenticator(CramMD5ClientAuthenticator(self.username))
+ self.registerAuthenticator(LOGINAuthenticator(self.username))
+ self.registerAuthenticator(PLAINAuthenticator(self.username))
+
+ def _getContextFactory(self):
+ if self.context is not None:
+ return self.context
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ return None
+ else:
+ try:
+ context = ssl.ClientContextFactory()
+ context.method = ssl.SSL.TLSv1_METHOD
+ return context
+ except AttributeError:
+ return None
+
+
+class ESMTPSenderFactory(SMTPSenderFactory):
+ """
+ Utility factory for sending emails easily.
+ """
+
+ protocol = ESMTPSender
+
+ def __init__(self, username, password, fromEmail, toEmail, file,
+ deferred, retries=5, timeout=None,
+ contextFactory=None, heloFallback=False,
+ requireAuthentication=True,
+ requireTransportSecurity=True):
+
+ SMTPSenderFactory.__init__(self, fromEmail, toEmail, file, deferred, retries, timeout)
+ self.username = username
+ self.password = password
+ self._contextFactory = contextFactory
+ self._heloFallback = heloFallback
+ self._requireAuthentication = requireAuthentication
+ self._requireTransportSecurity = requireTransportSecurity
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self.username, self.password, self._contextFactory, self.domain, self.nEmails*2+2)
+ p.heloFallback = self._heloFallback
+ p.requireAuthentication = self._requireAuthentication
+ p.requireTransportSecurity = self._requireTransportSecurity
+ p.factory = self
+ p.timeout = self.timeout
+ return p
+
+def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25):
+ """Send an email
+
+ This interface is intended to be a direct replacement for
+ smtplib.SMTP.sendmail() (with the obvious change that
+ you specify the smtphost as well). Also, ESMTP options
+ are not accepted, as we don't do ESMTP yet. I reserve the
+ right to implement the ESMTP options differently.
+
+ @param smtphost: The host the message should be sent to
+ @param from_addr: The (envelope) address sending this mail.
+ @param to_addrs: A list of addresses to send this mail to. A string will
+ be treated as a list of one address
+ @param msg: The message, including headers, either as a file or a string.
+ File-like objects need to support read() and close(). Lines must be
+ delimited by '\\n'. If you pass something that doesn't look like a
+ file, we try to convert it to a string (so you should be able to
+ pass an email.Message directly, but doing the conversion with
+ email.Generator manually will give you more control over the
+ process).
+
+ @param senderDomainName: Name by which to identify. If None, try
+ to pick something sane (but this depends on external configuration
+ and may not succeed).
+
+ @param port: Remote port to which to connect.
+
+ @rtype: L{Deferred}
+ @returns: A L{Deferred}, its callback will be called if a message is sent
+ to ANY address, the errback if no message is sent.
+
+ The callback will be called with a tuple (numOk, addresses) where numOk
+ is the number of successful recipient addresses and addresses is a list
+ of tuples (address, code, resp) giving the response to the RCPT command
+ for each address.
+ """
+ if not hasattr(msg,'read'):
+ # It's not a file
+ msg = StringIO(str(msg))
+
+ d = defer.Deferred()
+ factory = SMTPSenderFactory(from_addr, to_addrs, msg, d)
+
+ if senderDomainName is not None:
+ factory.domain = senderDomainName
+
+ reactor.connectTCP(smtphost, port, factory)
+
+ return d
+
+
+
+##
+## Yerg. Codecs!
+##
+import codecs
+def xtext_encode(s, errors=None):
+ r = []
+ for ch in s:
+ o = ord(ch)
+ if ch == '+' or ch == '=' or o < 33 or o > 126:
+ r.append('+%02X' % o)
+ else:
+ r.append(chr(o))
+ return (''.join(r), len(s))
+
+
+def xtext_decode(s, errors=None):
+ """
+ Decode the xtext-encoded string C{s}.
+ """
+ r = []
+ i = 0
+ while i < len(s):
+ if s[i] == '+':
+ try:
+ r.append(chr(int(s[i + 1:i + 3], 16)))
+ except ValueError:
+ r.append(s[i:i + 3])
+ i += 3
+ else:
+ r.append(s[i])
+ i += 1
+ return (''.join(r), len(s))
+
+class xtextStreamReader(codecs.StreamReader):
+ def decode(self, s, errors='strict'):
+ return xtext_decode(s)
+
+class xtextStreamWriter(codecs.StreamWriter):
+ def decode(self, s, errors='strict'):
+ return xtext_encode(s)
+
+def xtext_codec(name):
+ if name == 'xtext':
+ return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter)
+codecs.register(xtext_codec)
diff --git a/twisted/mail/tap.py b/twisted/mail/tap.py
new file mode 100644
index 0000000..b4b37f4
--- /dev/null
+++ b/twisted/mail/tap.py
@@ -0,0 +1,349 @@
+# -*- test-case-name: twisted.mail.test.test_options -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+I am the support module for creating mail servers with twistd
+"""
+
+import os
+import warnings
+
+from twisted.mail import mail
+from twisted.mail import maildir
+from twisted.mail import relay
+from twisted.mail import relaymanager
+from twisted.mail import alias
+
+from twisted.mail.protocols import SSLContextFactory
+
+from twisted.internet import endpoints
+
+from twisted.python import usage
+from twisted.python import deprecate
+from twisted.python import versions
+
+from twisted.cred import checkers
+from twisted.cred import strcred
+
+from twisted.application import internet
+
+
+class Options(usage.Options, strcred.AuthOptionMixin):
+ synopsis = "[options]"
+
+ optParameters = [
+ ["pop3s", "S", 0,
+ "Port to start the POP3-over-SSL server on (0 to disable). "
+ "DEPRECATED: use "
+ "'--pop3 ssl:port:privateKey=pkey.pem:certKey=cert.pem'"],
+
+ ["certificate", "c", None,
+ "Certificate file to use for SSL connections. "
+ "DEPRECATED: use "
+ "'--pop3 ssl:port:privateKey=pkey.pem:certKey=cert.pem'"],
+
+ ["relay", "R", None,
+ "Relay messages according to their envelope 'To', using "
+ "the given path as a queue directory."],
+
+ ["hostname", "H", None,
+ "The hostname by which to identify this server."],
+ ]
+
+ optFlags = [
+ ["esmtp", "E", "Use RFC 1425/1869 SMTP extensions"],
+ ["disable-anonymous", None,
+ "Disallow non-authenticated SMTP connections"],
+ ["no-pop3", None, "Disable the default POP3 server."],
+ ["no-smtp", None, "Disable the default SMTP server."],
+ ]
+
+ _protoDefaults = {
+ "pop3": 8110,
+ "smtp": 8025,
+ }
+
+ compData = usage.Completions(
+ optActions={"hostname" : usage.CompleteHostnames(),
+ "certificate" : usage.CompleteFiles("*.pem")}
+ )
+
+ longdesc = "This creates a mail.tap file that can be used by twistd."
+
+ def __init__(self):
+ usage.Options.__init__(self)
+ self.service = mail.MailService()
+ self.last_domain = None
+ for service in self._protoDefaults:
+ self[service] = []
+
+
+ def addEndpoint(self, service, description, certificate=None):
+ """
+ Given a 'service' (pop3 or smtp), add an endpoint.
+ """
+ self[service].append(
+ _toEndpoint(description, certificate=certificate))
+
+
+ def opt_pop3(self, description):
+ """
+ Add a pop3 port listener on the specified endpoint. You can listen on
+ multiple ports by specifying multiple --pop3 options. For backwards
+ compatibility, a bare TCP port number can be specified, but this is
+ deprecated. [SSL Example: ssl:8995:privateKey=mycert.pem] [default:
+ tcp:8110]
+ """
+ self.addEndpoint('pop3', description)
+ opt_p = opt_pop3
+
+
+ def opt_smtp(self, description):
+ """
+ Add an smtp port listener on the specified endpoint. You can listen on
+ multiple ports by specifying multiple --smtp options For backwards
+ compatibility, a bare TCP port number can be specified, but this is
+ deprecated. [SSL Example: ssl:8465:privateKey=mycert.pem] [default:
+ tcp:8025]
+ """
+ self.addEndpoint('smtp', description)
+ opt_s = opt_smtp
+
+
+ def opt_passwordfile(self, filename):
+ """
+ Specify a file containing username:password login info for authenticated
+ ESMTP connections. (DEPRECATED; see --help-auth instead)
+ """
+ ch = checkers.OnDiskUsernamePasswordDatabase(filename)
+ self.service.smtpPortal.registerChecker(ch)
+ msg = deprecate.getDeprecationWarningString(
+ self.opt_passwordfile, versions.Version('twisted.mail', 11, 0, 0))
+ warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
+ opt_P = opt_passwordfile
+
+
+ def opt_default(self):
+ """Make the most recently specified domain the default domain."""
+ if self.last_domain:
+ self.service.addDomain('', self.last_domain)
+ else:
+ raise usage.UsageError("Specify a domain before specifying using --default")
+ opt_D = opt_default
+
+
+ def opt_maildirdbmdomain(self, domain):
+ """generate an SMTP/POP3 virtual domain which saves to \"path\"
+ """
+ try:
+ name, path = domain.split('=')
+ except ValueError:
+ raise usage.UsageError("Argument to --maildirdbmdomain must be of the form 'name=path'")
+
+ self.last_domain = maildir.MaildirDirdbmDomain(self.service, os.path.abspath(path))
+ self.service.addDomain(name, self.last_domain)
+ opt_d = opt_maildirdbmdomain
+
+ def opt_user(self, user_pass):
+ """add a user/password to the last specified domains
+ """
+ try:
+ user, password = user_pass.split('=', 1)
+ except ValueError:
+ raise usage.UsageError("Argument to --user must be of the form 'user=password'")
+ if self.last_domain:
+ self.last_domain.addUser(user, password)
+ else:
+ raise usage.UsageError("Specify a domain before specifying users")
+ opt_u = opt_user
+
+ def opt_bounce_to_postmaster(self):
+ """undelivered mails are sent to the postmaster
+ """
+ self.last_domain.postmaster = 1
+ opt_b = opt_bounce_to_postmaster
+
+ def opt_aliases(self, filename):
+ """Specify an aliases(5) file to use for this domain"""
+ if self.last_domain is not None:
+ if mail.IAliasableDomain.providedBy(self.last_domain):
+ aliases = alias.loadAliasFile(self.service.domains, filename)
+ self.last_domain.setAliasGroup(aliases)
+ self.service.monitor.monitorFile(
+ filename,
+ AliasUpdater(self.service.domains, self.last_domain)
+ )
+ else:
+ raise usage.UsageError(
+ "%s does not support alias files" % (
+ self.last_domain.__class__.__name__,
+ )
+ )
+ else:
+ raise usage.UsageError("Specify a domain before specifying aliases")
+ opt_A = opt_aliases
+
+ def _getEndpoints(self, reactor, service):
+ """
+ Return a list of endpoints for the specified service, constructing
+ defaults if necessary.
+
+ @param reactor: If any endpoints are created, this is the reactor with
+ which they are created.
+
+ @param service: A key into self indicating the type of service to
+ retrieve endpoints for. This is either C{"pop3"} or C{"smtp"}.
+
+ @return: A C{list} of C{IServerStreamEndpoint} providers corresponding
+ to the command line parameters that were specified for C{service}.
+ If none were and the protocol was not explicitly disabled with a
+ I{--no-*} option, a default endpoint for the service is created
+ using C{self._protoDefaults}.
+ """
+ if service == 'pop3' and self['pop3s'] and len(self[service]) == 1:
+ # The single endpoint here is the POP3S service we added in
+ # postOptions. Include the default endpoint alongside it.
+ return self[service] + [
+ endpoints.TCP4ServerEndpoint(
+ reactor, self._protoDefaults[service])]
+ elif self[service]:
+ # For any non-POP3S case, if there are any services set up, just
+ # return those.
+ return self[service]
+ elif self['no-' + service]:
+ # If there are no services, but the service was explicitly disabled,
+ # return nothing.
+ return []
+ else:
+ # Otherwise, return the old default service.
+ return [
+ endpoints.TCP4ServerEndpoint(
+ reactor, self._protoDefaults[service])]
+
+
+ def postOptions(self):
+ from twisted.internet import reactor
+
+ if self['pop3s']:
+ if not self['certificate']:
+ raise usage.UsageError("Cannot specify --pop3s without "
+ "--certificate")
+ elif not os.path.exists(self['certificate']):
+ raise usage.UsageError("Certificate file %r does not exist."
+ % self['certificate'])
+ else:
+ self.addEndpoint(
+ 'pop3', self['pop3s'], certificate=self['certificate'])
+
+ if self['esmtp'] and self['hostname'] is None:
+ raise usage.UsageError("--esmtp requires --hostname")
+
+ # If the --auth option was passed, this will be present -- otherwise,
+ # it won't be, which is also a perfectly valid state.
+ if 'credCheckers' in self:
+ for ch in self['credCheckers']:
+ self.service.smtpPortal.registerChecker(ch)
+
+ if not self['disable-anonymous']:
+ self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess())
+
+ anything = False
+ for service in self._protoDefaults:
+ self[service] = self._getEndpoints(reactor, service)
+ if self[service]:
+ anything = True
+
+ if not anything:
+ raise usage.UsageError("You cannot disable all protocols")
+
+
+
+class AliasUpdater:
+ def __init__(self, domains, domain):
+ self.domains = domains
+ self.domain = domain
+ def __call__(self, new):
+ self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new))
+
+
+def _toEndpoint(description, certificate=None):
+ """
+ Tries to guess whether a description is a bare TCP port or a endpoint. If a
+ bare port is specified and a certificate file is present, returns an
+ SSL4ServerEndpoint and otherwise returns a TCP4ServerEndpoint.
+ """
+ from twisted.internet import reactor
+ try:
+ port = int(description)
+ except ValueError:
+ return endpoints.serverFromString(reactor, description)
+
+ warnings.warn(
+ "Specifying plain ports and/or a certificate is deprecated since "
+ "Twisted 11.0; use endpoint descriptions instead.",
+ category=DeprecationWarning, stacklevel=3)
+
+ if certificate:
+ ctx = SSLContextFactory(certificate)
+ return endpoints.SSL4ServerEndpoint(reactor, port, ctx)
+ return endpoints.TCP4ServerEndpoint(reactor, port)
+
+
+def makeService(config):
+ """
+ Construct a service for operating a mail server.
+
+ The returned service may include POP3 servers or SMTP servers (or both),
+ depending on the configuration passed in. If there are multiple servers,
+ they will share all of their non-network state (eg, the same user accounts
+ are available on all of them).
+
+ @param config: An L{Options} instance specifying what servers to include in
+ the returned service and where they should keep mail data.
+
+ @return: An L{IService} provider which contains the requested mail servers.
+ """
+ if config['esmtp']:
+ rmType = relaymanager.SmartHostESMTPRelayingManager
+ smtpFactory = config.service.getESMTPFactory
+ else:
+ rmType = relaymanager.SmartHostSMTPRelayingManager
+ smtpFactory = config.service.getSMTPFactory
+
+ if config['relay']:
+ dir = config['relay']
+ if not os.path.isdir(dir):
+ os.mkdir(dir)
+
+ config.service.setQueue(relaymanager.Queue(dir))
+ default = relay.DomainQueuer(config.service)
+
+ manager = rmType(config.service.queue)
+ if config['esmtp']:
+ manager.fArgs += (None, None)
+ manager.fArgs += (config['hostname'],)
+
+ helper = relaymanager.RelayStateHelper(manager, 1)
+ helper.setServiceParent(config.service)
+ config.service.domains.setDefaultDomain(default)
+
+ if config['pop3']:
+ f = config.service.getPOP3Factory()
+ for endpoint in config['pop3']:
+ svc = internet.StreamServerEndpointService(endpoint, f)
+ svc.setServiceParent(config.service)
+
+ if config['smtp']:
+ f = smtpFactory()
+ if config['hostname']:
+ f.domain = config['hostname']
+ f.fArgs = (f.domain,)
+ if config['esmtp']:
+ f.fArgs = (None, None) + f.fArgs
+ for endpoint in config['smtp']:
+ svc = internet.StreamServerEndpointService(endpoint, f)
+ svc.setServiceParent(config.service)
+
+ return config.service
diff --git a/twisted/mail/test/__init__.py b/twisted/mail/test/__init__.py
new file mode 100644
index 0000000..f8ec705
--- /dev/null
+++ b/twisted/mail/test/__init__.py
@@ -0,0 +1 @@
+"Tests for twistd.mail"
diff --git a/twisted/mail/test/pop3testserver.py b/twisted/mail/test/pop3testserver.py
new file mode 100644
index 0000000..c87892c
--- /dev/null
+++ b/twisted/mail/test/pop3testserver.py
@@ -0,0 +1,314 @@
+#!/usr/bin/env python
+# -*- test-case-name: twisted.mail.test.test_pop3client -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet.protocol import Factory
+from twisted.protocols import basic
+from twisted.internet import reactor
+import sys, time
+
+USER = "test"
+PASS = "twisted"
+
+PORT = 1100
+
+SSL_SUPPORT = True
+UIDL_SUPPORT = True
+INVALID_SERVER_RESPONSE = False
+INVALID_CAPABILITY_RESPONSE = False
+INVALID_LOGIN_RESPONSE = False
+DENY_CONNECTION = False
+DROP_CONNECTION = False
+BAD_TLS_RESPONSE = False
+TIMEOUT_RESPONSE = False
+TIMEOUT_DEFERRED = False
+SLOW_GREETING = False
+
+"""Commands"""
+CONNECTION_MADE = "+OK POP3 localhost v2003.83 server ready"
+
+CAPABILITIES = [
+"TOP",
+"LOGIN-DELAY 180",
+"USER",
+"SASL LOGIN"
+]
+
+CAPABILITIES_SSL = "STLS"
+CAPABILITIES_UIDL = "UIDL"
+
+
+INVALID_RESPONSE = "-ERR Unknown request"
+VALID_RESPONSE = "+OK Command Completed"
+AUTH_DECLINED = "-ERR LOGIN failed"
+AUTH_ACCEPTED = "+OK Mailbox open, 0 messages"
+TLS_ERROR = "-ERR server side error start TLS handshake"
+LOGOUT_COMPLETE = "+OK quit completed"
+NOT_LOGGED_IN = "-ERR Unknown AUHORIZATION state command"
+STAT = "+OK 0 0"
+UIDL = "+OK Unique-ID listing follows\r\n."
+LIST = "+OK Mailbox scan listing follows\r\n."
+CAP_START = "+OK Capability list follows:"
+
+
+class POP3TestServer(basic.LineReceiver):
+ def __init__(self, contextFactory = None):
+ self.loggedIn = False
+ self.caps = None
+ self.tmpUser = None
+ self.ctx = contextFactory
+
+ def sendSTATResp(self, req):
+ self.sendLine(STAT)
+
+ def sendUIDLResp(self, req):
+ self.sendLine(UIDL)
+
+ def sendLISTResp(self, req):
+ self.sendLine(LIST)
+
+ def sendCapabilities(self):
+ if self.caps is None:
+ self.caps = [CAP_START]
+
+ if UIDL_SUPPORT:
+ self.caps.append(CAPABILITIES_UIDL)
+
+ if SSL_SUPPORT:
+ self.caps.append(CAPABILITIES_SSL)
+
+ for cap in CAPABILITIES:
+ self.caps.append(cap)
+ resp = '\r\n'.join(self.caps)
+ resp += "\r\n."
+
+ self.sendLine(resp)
+
+
+ def connectionMade(self):
+ if DENY_CONNECTION:
+ self.disconnect()
+ return
+
+ if SLOW_GREETING:
+ reactor.callLater(20, self.sendGreeting)
+
+ else:
+ self.sendGreeting()
+
+ def sendGreeting(self):
+ self.sendLine(CONNECTION_MADE)
+
+ def lineReceived(self, line):
+ """Error Conditions"""
+
+ uline = line.upper()
+ find = lambda s: uline.find(s) != -1
+
+ if TIMEOUT_RESPONSE:
+ # Do not respond to clients request
+ return
+
+ if DROP_CONNECTION:
+ self.disconnect()
+ return
+
+ elif find("CAPA"):
+ if INVALID_CAPABILITY_RESPONSE:
+ self.sendLine(INVALID_RESPONSE)
+ else:
+ self.sendCapabilities()
+
+ elif find("STLS") and SSL_SUPPORT:
+ self.startTLS()
+
+ elif find("USER"):
+ if INVALID_LOGIN_RESPONSE:
+ self.sendLine(INVALID_RESPONSE)
+ return
+
+ resp = None
+ try:
+ self.tmpUser = line.split(" ")[1]
+ resp = VALID_RESPONSE
+ except:
+ resp = AUTH_DECLINED
+
+ self.sendLine(resp)
+
+ elif find("PASS"):
+ resp = None
+ try:
+ pwd = line.split(" ")[1]
+
+ if self.tmpUser is None or pwd is None:
+ resp = AUTH_DECLINED
+ elif self.tmpUser == USER and pwd == PASS:
+ resp = AUTH_ACCEPTED
+ self.loggedIn = True
+ else:
+ resp = AUTH_DECLINED
+ except:
+ resp = AUTH_DECLINED
+
+ self.sendLine(resp)
+
+ elif find("QUIT"):
+ self.loggedIn = False
+ self.sendLine(LOGOUT_COMPLETE)
+ self.disconnect()
+
+ elif INVALID_SERVER_RESPONSE:
+ self.sendLine(INVALID_RESPONSE)
+
+ elif not self.loggedIn:
+ self.sendLine(NOT_LOGGED_IN)
+
+ elif find("NOOP"):
+ self.sendLine(VALID_RESPONSE)
+
+ elif find("STAT"):
+ if TIMEOUT_DEFERRED:
+ return
+ self.sendLine(STAT)
+
+ elif find("LIST"):
+ if TIMEOUT_DEFERRED:
+ return
+ self.sendLine(LIST)
+
+ elif find("UIDL"):
+ if TIMEOUT_DEFERRED:
+ return
+ elif not UIDL_SUPPORT:
+ self.sendLine(INVALID_RESPONSE)
+ return
+
+ self.sendLine(UIDL)
+
+ def startTLS(self):
+ if self.ctx is None:
+ self.getContext()
+
+ if SSL_SUPPORT and self.ctx is not None:
+ self.sendLine('+OK Begin TLS negotiation now')
+ self.transport.startTLS(self.ctx)
+ else:
+ self.sendLine('-ERR TLS not available')
+
+ def disconnect(self):
+ self.transport.loseConnection()
+
+ def getContext(self):
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ self.ctx = None
+ else:
+ self.ctx = ssl.ClientContextFactory()
+ self.ctx.method = ssl.SSL.TLSv1_METHOD
+
+
+usage = """popServer.py [arg] (default is Standard POP Server with no messages)
+no_ssl - Start with no SSL support
+no_uidl - Start with no UIDL support
+bad_resp - Send a non-RFC compliant response to the Client
+bad_cap_resp - send a non-RFC compliant response when the Client sends a 'CAPABILITY' request
+bad_login_resp - send a non-RFC compliant response when the Client sends a 'LOGIN' request
+deny - Deny the connection
+drop - Drop the connection after sending the greeting
+bad_tls - Send a bad response to a STARTTLS
+timeout - Do not return a response to a Client request
+to_deferred - Do not return a response on a 'Select' request. This
+ will test Deferred callback handling
+slow - Wait 20 seconds after the connection is made to return a Server Greeting
+"""
+
+def printMessage(msg):
+ print "Server Starting in %s mode" % msg
+
+def processArg(arg):
+
+ if arg.lower() == 'no_ssl':
+ global SSL_SUPPORT
+ SSL_SUPPORT = False
+ printMessage("NON-SSL")
+
+ elif arg.lower() == 'no_uidl':
+ global UIDL_SUPPORT
+ UIDL_SUPPORT = False
+ printMessage("NON-UIDL")
+
+ elif arg.lower() == 'bad_resp':
+ global INVALID_SERVER_RESPONSE
+ INVALID_SERVER_RESPONSE = True
+ printMessage("Invalid Server Response")
+
+ elif arg.lower() == 'bad_cap_resp':
+ global INVALID_CAPABILITY_RESPONSE
+ INVALID_CAPABILITY_RESPONSE = True
+ printMessage("Invalid Capability Response")
+
+ elif arg.lower() == 'bad_login_resp':
+ global INVALID_LOGIN_RESPONSE
+ INVALID_LOGIN_RESPONSE = True
+ printMessage("Invalid Capability Response")
+
+ elif arg.lower() == 'deny':
+ global DENY_CONNECTION
+ DENY_CONNECTION = True
+ printMessage("Deny Connection")
+
+ elif arg.lower() == 'drop':
+ global DROP_CONNECTION
+ DROP_CONNECTION = True
+ printMessage("Drop Connection")
+
+
+ elif arg.lower() == 'bad_tls':
+ global BAD_TLS_RESPONSE
+ BAD_TLS_RESPONSE = True
+ printMessage("Bad TLS Response")
+
+ elif arg.lower() == 'timeout':
+ global TIMEOUT_RESPONSE
+ TIMEOUT_RESPONSE = True
+ printMessage("Timeout Response")
+
+ elif arg.lower() == 'to_deferred':
+ global TIMEOUT_DEFERRED
+ TIMEOUT_DEFERRED = True
+ printMessage("Timeout Deferred Response")
+
+ elif arg.lower() == 'slow':
+ global SLOW_GREETING
+ SLOW_GREETING = True
+ printMessage("Slow Greeting")
+
+ elif arg.lower() == '--help':
+ print usage
+ sys.exit()
+
+ else:
+ print usage
+ sys.exit()
+
+def main():
+
+ if len(sys.argv) < 2:
+ printMessage("POP3 with no messages")
+ else:
+ args = sys.argv[1:]
+
+ for arg in args:
+ processArg(arg)
+
+ f = Factory()
+ f.protocol = POP3TestServer
+ reactor.listenTCP(PORT, f)
+ reactor.run()
+
+if __name__ == '__main__':
+ main()
diff --git a/twisted/mail/test/rfc822.message b/twisted/mail/test/rfc822.message
new file mode 100644
index 0000000..ee97ab9
--- /dev/null
+++ b/twisted/mail/test/rfc822.message
@@ -0,0 +1,86 @@
+Return-Path: <twisted-commits-admin@twistedmatrix.com>
+Delivered-To: exarkun@meson.dyndns.org
+Received: from localhost [127.0.0.1]
+ by localhost with POP3 (fetchmail-6.2.1)
+ for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST)
+Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105])
+ by intarweb.us (Postfix) with ESMTP id 4A4A513EA4
+ for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST)
+Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
+ by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
+ id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600
+Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
+ id 18w63j-0007VK-00
+ for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600
+To: twisted-commits@twistedmatrix.com
+From: etrepum CVS <etrepum@twistedmatrix.com>
+Reply-To: twisted-python@twistedmatrix.com
+X-Mailer: CVSToys
+Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com>
+Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up.
+Sender: twisted-commits-admin@twistedmatrix.com
+Errors-To: twisted-commits-admin@twistedmatrix.com
+X-BeenThere: twisted-commits@twistedmatrix.com
+X-Mailman-Version: 2.0.11
+Precedence: bulk
+List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help>
+List-Post: <mailto:twisted-commits@twistedmatrix.com>
+List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>,
+ <mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe>
+List-Id: <twisted-commits.twistedmatrix.com>
+List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>,
+ <mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe>
+List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/>
+Date: Thu, 20 Mar 2003 13:50:39 -0600
+
+Modified files:
+Twisted/twisted/python/rebuild.py 1.19 1.20
+
+Log message:
+rebuild now works on python versions from 2.2.0 and up.
+
+
+ViewCVS links:
+http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted
+
+Index: Twisted/twisted/python/rebuild.py
+diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20
+--- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003
++++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003
+@@ -206,15 +206,27 @@
+ clazz.__dict__.clear()
+ clazz.__getattr__ = __getattr__
+ clazz.__module__ = module.__name__
++ if newclasses:
++ import gc
++ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2):
++ hasBrokenRebuild = 1
++ gc_objects = gc.get_objects()
++ else:
++ hasBrokenRebuild = 0
+ for nclass in newclasses:
+ ga = getattr(module, nclass.__name__)
+ if ga is nclass:
+ log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass))
+ else:
+- import gc
+- for r in gc.get_referrers(nclass):
+- if isinstance(r, nclass):
++ if hasBrokenRebuild:
++ for r in gc_objects:
++ if not getattr(r, '__class__', None) is nclass:
++ continue
+ r.__class__ = ga
++ else:
++ for r in gc.get_referrers(nclass):
++ if getattr(r, '__class__', None) is nclass:
++ r.__class__ = ga
+ if doLog:
+ log.msg('')
+ log.msg(' (fixing %s): ' % str(module.__name__))
+
+
+_______________________________________________
+Twisted-commits mailing list
+Twisted-commits@twistedmatrix.com
+http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
diff --git a/twisted/mail/test/test_bounce.py b/twisted/mail/test/test_bounce.py
new file mode 100644
index 0000000..963d21d
--- /dev/null
+++ b/twisted/mail/test/test_bounce.py
@@ -0,0 +1,32 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Test cases for bounce message generation
+"""
+
+from twisted.trial import unittest
+from twisted.mail import bounce
+import rfc822, cStringIO
+
+class BounceTestCase(unittest.TestCase):
+ """
+ testcases for bounce message generation
+ """
+
+ def testBounceFormat(self):
+ from_, to, s = bounce.generateBounce(cStringIO.StringIO('''\
+From: Moshe Zadka <moshez@example.com>
+To: nonexistant@example.org
+Subject: test
+
+'''), 'moshez@example.com', 'nonexistant@example.org')
+ self.assertEqual(from_, '')
+ self.assertEqual(to, 'moshez@example.com')
+ mess = rfc822.Message(cStringIO.StringIO(s))
+ self.assertEqual(mess['To'], 'moshez@example.com')
+ self.assertEqual(mess['From'], 'postmaster@example.org')
+ self.assertEqual(mess['subject'], 'Returned Mail: see transcript for details')
+
+ def testBounceMIME(self):
+ pass
diff --git a/twisted/mail/test/test_imap.py b/twisted/mail/test/test_imap.py
new file mode 100644
index 0000000..55c47e9
--- /dev/null
+++ b/twisted/mail/test/test_imap.py
@@ -0,0 +1,4489 @@
+# -*- test-case-name: twisted.mail.test.test_imap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test case for twisted.mail.imap4
+"""
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+import codecs
+import locale
+import os
+import types
+
+from zope.interface import implements
+
+from twisted.mail.imap4 import MessageSet
+from twisted.mail import imap4
+from twisted.protocols import loopback
+from twisted.internet import defer
+from twisted.internet import error
+from twisted.internet import reactor
+from twisted.internet import interfaces
+from twisted.internet.task import Clock
+from twisted.trial import unittest
+from twisted.python import util
+from twisted.python import failure
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.checkers
+import twisted.cred.credentials
+import twisted.cred.portal
+
+from twisted.test.proto_helpers import StringTransport, StringTransportWithDisconnection
+
+try:
+ from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
+except ImportError:
+ ClientTLSContext = ServerTLSContext = None
+
+def strip(f):
+ return lambda result, f=f: f()
+
+def sortNest(l):
+ l = l[:]
+ l.sort()
+ for i in range(len(l)):
+ if isinstance(l[i], types.ListType):
+ l[i] = sortNest(l[i])
+ elif isinstance(l[i], types.TupleType):
+ l[i] = tuple(sortNest(list(l[i])))
+ return l
+
+class IMAP4UTF7TestCase(unittest.TestCase):
+ tests = [
+ [u'Hello world', 'Hello world'],
+ [u'Hello & world', 'Hello &- world'],
+ [u'Hello\xffworld', 'Hello&AP8-world'],
+ [u'\xff\xfe\xfd\xfc', '&AP8A,gD9APw-'],
+ [u'~peter/mail/\u65e5\u672c\u8a9e/\u53f0\u5317',
+ '~peter/mail/&ZeVnLIqe-/&U,BTFw-'], # example from RFC 2060
+ ]
+
+ def test_encodeWithErrors(self):
+ """
+ Specifying an error policy to C{unicode.encode} with the
+ I{imap4-utf-7} codec should produce the same result as not
+ specifying the error policy.
+ """
+ text = u'Hello world'
+ self.assertEqual(
+ text.encode('imap4-utf-7', 'strict'),
+ text.encode('imap4-utf-7'))
+
+
+ def test_decodeWithErrors(self):
+ """
+ Similar to L{test_encodeWithErrors}, but for C{str.decode}.
+ """
+ bytes = 'Hello world'
+ self.assertEqual(
+ bytes.decode('imap4-utf-7', 'strict'),
+ bytes.decode('imap4-utf-7'))
+
+
+ def test_getreader(self):
+ """
+ C{codecs.getreader('imap4-utf-7')} returns the I{imap4-utf-7} stream
+ reader class.
+ """
+ reader = codecs.getreader('imap4-utf-7')(StringIO('Hello&AP8-world'))
+ self.assertEqual(reader.read(), u'Hello\xffworld')
+
+
+ def test_getwriter(self):
+ """
+ C{codecs.getwriter('imap4-utf-7')} returns the I{imap4-utf-7} stream
+ writer class.
+ """
+ output = StringIO()
+ writer = codecs.getwriter('imap4-utf-7')(output)
+ writer.write(u'Hello\xffworld')
+ self.assertEqual(output.getvalue(), 'Hello&AP8-world')
+
+
+ def test_encode(self):
+ """
+ The I{imap4-utf-7} can be used to encode a unicode string into a byte
+ string according to the IMAP4 modified UTF-7 encoding rules.
+ """
+ for (input, output) in self.tests:
+ self.assertEqual(input.encode('imap4-utf-7'), output)
+
+
+ def test_decode(self):
+ """
+ The I{imap4-utf-7} can be used to decode a byte string into a unicode
+ string according to the IMAP4 modified UTF-7 encoding rules.
+ """
+ for (input, output) in self.tests:
+ self.assertEqual(input, output.decode('imap4-utf-7'))
+
+
+ def test_printableSingletons(self):
+ """
+ The IMAP4 modified UTF-7 implementation encodes all printable
+ characters which are in ASCII using the corresponding ASCII byte.
+ """
+ # All printables represent themselves
+ for o in range(0x20, 0x26) + range(0x27, 0x7f):
+ self.assertEqual(chr(o), chr(o).encode('imap4-utf-7'))
+ self.assertEqual(chr(o), chr(o).decode('imap4-utf-7'))
+ self.assertEqual('&'.encode('imap4-utf-7'), '&-')
+ self.assertEqual('&-'.decode('imap4-utf-7'), '&')
+
+
+
+class BufferingConsumer:
+ def __init__(self):
+ self.buffer = []
+
+ def write(self, bytes):
+ self.buffer.append(bytes)
+ if self.consumer:
+ self.consumer.resumeProducing()
+
+ def registerProducer(self, consumer, streaming):
+ self.consumer = consumer
+ self.consumer.resumeProducing()
+
+ def unregisterProducer(self):
+ self.consumer = None
+
+class MessageProducerTestCase(unittest.TestCase):
+ def testSinglePart(self):
+ body = 'This is body text. Rar.'
+ headers = util.OrderedDict()
+ headers['from'] = 'sender@host'
+ headers['to'] = 'recipient@domain'
+ headers['subject'] = 'booga booga boo'
+ headers['content-type'] = 'text/plain'
+
+ msg = FakeyMessage(headers, (), None, body, 123, None )
+
+ c = BufferingConsumer()
+ p = imap4.MessageProducer(msg)
+ d = p.beginProducing(c)
+
+ def cbProduced(result):
+ self.assertIdentical(result, p)
+ self.assertEqual(
+ ''.join(c.buffer),
+
+ '{119}\r\n'
+ 'From: sender@host\r\n'
+ 'To: recipient@domain\r\n'
+ 'Subject: booga booga boo\r\n'
+ 'Content-Type: text/plain\r\n'
+ '\r\n'
+ + body)
+ return d.addCallback(cbProduced)
+
+
+ def testSingleMultiPart(self):
+ outerBody = ''
+ innerBody = 'Contained body message text. Squarge.'
+ headers = util.OrderedDict()
+ headers['from'] = 'sender@host'
+ headers['to'] = 'recipient@domain'
+ headers['subject'] = 'booga booga boo'
+ headers['content-type'] = 'multipart/alternative; boundary="xyz"'
+
+ innerHeaders = util.OrderedDict()
+ innerHeaders['subject'] = 'this is subject text'
+ innerHeaders['content-type'] = 'text/plain'
+ msg = FakeyMessage(headers, (), None, outerBody, 123,
+ [FakeyMessage(innerHeaders, (), None, innerBody,
+ None, None)],
+ )
+
+ c = BufferingConsumer()
+ p = imap4.MessageProducer(msg)
+ d = p.beginProducing(c)
+
+ def cbProduced(result):
+ self.failUnlessIdentical(result, p)
+
+ self.assertEqual(
+ ''.join(c.buffer),
+
+ '{239}\r\n'
+ 'From: sender@host\r\n'
+ 'To: recipient@domain\r\n'
+ 'Subject: booga booga boo\r\n'
+ 'Content-Type: multipart/alternative; boundary="xyz"\r\n'
+ '\r\n'
+ '\r\n'
+ '--xyz\r\n'
+ 'Subject: this is subject text\r\n'
+ 'Content-Type: text/plain\r\n'
+ '\r\n'
+ + innerBody
+ + '\r\n--xyz--\r\n')
+
+ return d.addCallback(cbProduced)
+
+
+ def testMultipleMultiPart(self):
+ outerBody = ''
+ innerBody1 = 'Contained body message text. Squarge.'
+ innerBody2 = 'Secondary <i>message</i> text of squarge body.'
+ headers = util.OrderedDict()
+ headers['from'] = 'sender@host'
+ headers['to'] = 'recipient@domain'
+ headers['subject'] = 'booga booga boo'
+ headers['content-type'] = 'multipart/alternative; boundary="xyz"'
+ innerHeaders = util.OrderedDict()
+ innerHeaders['subject'] = 'this is subject text'
+ innerHeaders['content-type'] = 'text/plain'
+ innerHeaders2 = util.OrderedDict()
+ innerHeaders2['subject'] = '<b>this is subject</b>'
+ innerHeaders2['content-type'] = 'text/html'
+ msg = FakeyMessage(headers, (), None, outerBody, 123, [
+ FakeyMessage(innerHeaders, (), None, innerBody1, None, None),
+ FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)
+ ],
+ )
+
+ c = BufferingConsumer()
+ p = imap4.MessageProducer(msg)
+ d = p.beginProducing(c)
+
+ def cbProduced(result):
+ self.failUnlessIdentical(result, p)
+
+ self.assertEqual(
+ ''.join(c.buffer),
+
+ '{354}\r\n'
+ 'From: sender@host\r\n'
+ 'To: recipient@domain\r\n'
+ 'Subject: booga booga boo\r\n'
+ 'Content-Type: multipart/alternative; boundary="xyz"\r\n'
+ '\r\n'
+ '\r\n'
+ '--xyz\r\n'
+ 'Subject: this is subject text\r\n'
+ 'Content-Type: text/plain\r\n'
+ '\r\n'
+ + innerBody1
+ + '\r\n--xyz\r\n'
+ 'Subject: <b>this is subject</b>\r\n'
+ 'Content-Type: text/html\r\n'
+ '\r\n'
+ + innerBody2
+ + '\r\n--xyz--\r\n')
+ return d.addCallback(cbProduced)
+
+
+
+class IMAP4HelperTestCase(unittest.TestCase):
+ """
+ Tests for various helper utilities in the IMAP4 module.
+ """
+
+ def test_fileProducer(self):
+ b = (('x' * 1) + ('y' * 1) + ('z' * 1)) * 10
+ c = BufferingConsumer()
+ f = StringIO(b)
+ p = imap4.FileProducer(f)
+ d = p.beginProducing(c)
+
+ def cbProduced(result):
+ self.failUnlessIdentical(result, p)
+ self.assertEqual(
+ ('{%d}\r\n' % len(b))+ b,
+ ''.join(c.buffer))
+ return d.addCallback(cbProduced)
+
+
+ def test_wildcard(self):
+ cases = [
+ ['foo/%gum/bar',
+ ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'],
+ ['foo/xgum/bar', 'foo/gum/bar'],
+ ], ['foo/x%x/bar',
+ ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'],
+ ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar'],
+ ], ['foo/xyz*abc/bar',
+ ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'],
+ ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'],
+ ]
+ ]
+
+ for (wildcard, fail, succeed) in cases:
+ wildcard = imap4.wildcardToRegexp(wildcard, '/')
+ for x in fail:
+ self.failIf(wildcard.match(x))
+ for x in succeed:
+ self.failUnless(wildcard.match(x))
+
+
+ def test_wildcardNoDelim(self):
+ cases = [
+ ['foo/%gum/bar',
+ ['foo/bar', 'oo/lalagum/bar', 'foo/gumx/bar', 'foo/gum/baz'],
+ ['foo/xgum/bar', 'foo/gum/bar', 'foo/x/gum/bar'],
+ ], ['foo/x%x/bar',
+ ['foo', 'bar', 'fuz fuz fuz', 'foo/*/bar', 'foo/xyz/bar', 'foo/xx/baz'],
+ ['foo/xyx/bar', 'foo/xx/bar', 'foo/xxxxxxxxxxxxxx/bar', 'foo/x/x/bar'],
+ ], ['foo/xyz*abc/bar',
+ ['foo/xyz/bar', 'foo/abc/bar', 'foo/xyzab/cbar', 'foo/xyza/bcbar'],
+ ['foo/xyzabc/bar', 'foo/xyz/abc/bar', 'foo/xyz/123/abc/bar'],
+ ]
+ ]
+
+ for (wildcard, fail, succeed) in cases:
+ wildcard = imap4.wildcardToRegexp(wildcard, None)
+ for x in fail:
+ self.failIf(wildcard.match(x), x)
+ for x in succeed:
+ self.failUnless(wildcard.match(x), x)
+
+
+ def test_headerFormatter(self):
+ cases = [
+ ({'Header1': 'Value1', 'Header2': 'Value2'}, 'Header2: Value2\r\nHeader1: Value1\r\n'),
+ ]
+
+ for (input, output) in cases:
+ self.assertEqual(imap4._formatHeaders(input), output)
+
+
+ def test_messageSet(self):
+ m1 = MessageSet()
+ m2 = MessageSet()
+
+ self.assertEqual(m1, m2)
+
+ m1 = m1 + (1, 3)
+ self.assertEqual(len(m1), 3)
+ self.assertEqual(list(m1), [1, 2, 3])
+
+ m2 = m2 + (1, 3)
+ self.assertEqual(m1, m2)
+ self.assertEqual(list(m1 + m2), [1, 2, 3])
+
+
+ def test_messageSetStringRepresentationWithWildcards(self):
+ """
+ In a L{MessageSet}, in the presence of wildcards, if the highest message
+ id is known, the wildcard should get replaced by that high value.
+ """
+ inputs = [
+ MessageSet(imap4.parseIdList('*')),
+ MessageSet(imap4.parseIdList('3:*', 6)),
+ MessageSet(imap4.parseIdList('*:2', 6)),
+ ]
+
+ outputs = [
+ "*",
+ "3:6",
+ "2:6",
+ ]
+
+ for i, o in zip(inputs, outputs):
+ self.assertEqual(str(i), o)
+
+
+ def test_messageSetStringRepresentationWithInversion(self):
+ """
+ In a L{MessageSet}, inverting the high and low numbers in a range
+ doesn't affect the meaning of the range. For example, 3:2 displays just
+ like 2:3, because according to the RFC they have the same meaning.
+ """
+ inputs = [
+ MessageSet(imap4.parseIdList('2:3')),
+ MessageSet(imap4.parseIdList('3:2')),
+ ]
+
+ outputs = [
+ "2:3",
+ "2:3",
+ ]
+
+ for i, o in zip(inputs, outputs):
+ self.assertEqual(str(i), o)
+
+
+ def test_quotedSplitter(self):
+ cases = [
+ '''Hello World''',
+ '''Hello "World!"''',
+ '''World "Hello" "How are you?"''',
+ '''"Hello world" How "are you?"''',
+ '''foo bar "baz buz" NIL''',
+ '''foo bar "baz buz" "NIL"''',
+ '''foo NIL "baz buz" bar''',
+ '''foo "NIL" "baz buz" bar''',
+ '''"NIL" bar "baz buz" foo''',
+ 'oo \\"oo\\" oo',
+ '"oo \\"oo\\" oo"',
+ 'oo \t oo',
+ '"oo \t oo"',
+ 'oo \\t oo',
+ '"oo \\t oo"',
+ 'oo \o oo',
+ '"oo \o oo"',
+ 'oo \\o oo',
+ '"oo \\o oo"',
+ ]
+
+ answers = [
+ ['Hello', 'World'],
+ ['Hello', 'World!'],
+ ['World', 'Hello', 'How are you?'],
+ ['Hello world', 'How', 'are you?'],
+ ['foo', 'bar', 'baz buz', None],
+ ['foo', 'bar', 'baz buz', 'NIL'],
+ ['foo', None, 'baz buz', 'bar'],
+ ['foo', 'NIL', 'baz buz', 'bar'],
+ ['NIL', 'bar', 'baz buz', 'foo'],
+ ['oo', '"oo"', 'oo'],
+ ['oo "oo" oo'],
+ ['oo', 'oo'],
+ ['oo \t oo'],
+ ['oo', '\\t', 'oo'],
+ ['oo \\t oo'],
+ ['oo', '\o', 'oo'],
+ ['oo \o oo'],
+ ['oo', '\\o', 'oo'],
+ ['oo \\o oo'],
+
+ ]
+
+ errors = [
+ '"mismatched quote',
+ 'mismatched quote"',
+ 'mismatched"quote',
+ '"oops here is" another"',
+ ]
+
+ for s in errors:
+ self.assertRaises(imap4.MismatchedQuoting, imap4.splitQuoted, s)
+
+ for (case, expected) in zip(cases, answers):
+ self.assertEqual(imap4.splitQuoted(case), expected)
+
+
+ def test_stringCollapser(self):
+ cases = [
+ ['a', 'b', 'c', 'd', 'e'],
+ ['a', ' ', '"', 'b', 'c', ' ', '"', ' ', 'd', 'e'],
+ [['a', 'b', 'c'], 'd', 'e'],
+ ['a', ['b', 'c', 'd'], 'e'],
+ ['a', 'b', ['c', 'd', 'e']],
+ ['"', 'a', ' ', '"', ['b', 'c', 'd'], '"', ' ', 'e', '"'],
+ ['a', ['"', ' ', 'b', 'c', ' ', ' ', '"'], 'd', 'e'],
+ ]
+
+ answers = [
+ ['abcde'],
+ ['a', 'bc ', 'de'],
+ [['abc'], 'de'],
+ ['a', ['bcd'], 'e'],
+ ['ab', ['cde']],
+ ['a ', ['bcd'], ' e'],
+ ['a', [' bc '], 'de'],
+ ]
+
+ for (case, expected) in zip(cases, answers):
+ self.assertEqual(imap4.collapseStrings(case), expected)
+
+
+ def test_parenParser(self):
+ s = '\r\n'.join(['xx'] * 4)
+ cases = [
+ '(BODY.PEEK[HEADER.FIELDS.NOT (subject bcc cc)] {%d}\r\n%s)' % (len(s), s,),
+
+# '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" '
+# 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" '
+# '"IMAP4rev1 WG mtg summary and minutes" '
+# '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
+# '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
+# '(("Terry Gray" NIL "gray" "cac.washington.edu")) '
+# '((NIL NIL "imap" "cac.washington.edu")) '
+# '((NIL NIL "minutes" "CNRI.Reston.VA.US") '
+# '("John Klensin" NIL "KLENSIN" "INFOODS.MIT.EDU")) NIL NIL '
+# '"<B27397-0100000@cac.washington.edu>") '
+# 'BODY ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 3028 92))',
+
+ '(FLAGS (\Seen) INTERNALDATE "17-Jul-1996 02:44:25 -0700" '
+ 'RFC822.SIZE 4286 ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" '
+ '"IMAP4rev1 WG mtg summary and minutes" '
+ '(("Terry Gray" NIL gray cac.washington.edu)) '
+ '(("Terry Gray" NIL gray cac.washington.edu)) '
+ '(("Terry Gray" NIL gray cac.washington.edu)) '
+ '((NIL NIL imap cac.washington.edu)) '
+ '((NIL NIL minutes CNRI.Reston.VA.US) '
+ '("John Klensin" NIL KLENSIN INFOODS.MIT.EDU)) NIL NIL '
+ '<B27397-0100000@cac.washington.edu>) '
+ 'BODY (TEXT PLAIN (CHARSET US-ASCII) NIL NIL 7BIT 3028 92))',
+ '("oo \\"oo\\" oo")',
+ '("oo \\\\ oo")',
+ '("oo \\ oo")',
+ '("oo \\o")',
+ '("oo \o")',
+ '(oo \o)',
+ '(oo \\o)',
+
+ ]
+
+ answers = [
+ ['BODY.PEEK', ['HEADER.FIELDS.NOT', ['subject', 'bcc', 'cc']], s],
+
+ ['FLAGS', [r'\Seen'], 'INTERNALDATE',
+ '17-Jul-1996 02:44:25 -0700', 'RFC822.SIZE', '4286', 'ENVELOPE',
+ ['Wed, 17 Jul 1996 02:23:25 -0700 (PDT)',
+ 'IMAP4rev1 WG mtg summary and minutes', [["Terry Gray", None,
+ "gray", "cac.washington.edu"]], [["Terry Gray", None,
+ "gray", "cac.washington.edu"]], [["Terry Gray", None,
+ "gray", "cac.washington.edu"]], [[None, None, "imap",
+ "cac.washington.edu"]], [[None, None, "minutes",
+ "CNRI.Reston.VA.US"], ["John Klensin", None, "KLENSIN",
+ "INFOODS.MIT.EDU"]], None, None,
+ "<B27397-0100000@cac.washington.edu>"], "BODY", ["TEXT", "PLAIN",
+ ["CHARSET", "US-ASCII"], None, None, "7BIT", "3028", "92"]],
+ ['oo "oo" oo'],
+ ['oo \\\\ oo'],
+ ['oo \\ oo'],
+ ['oo \\o'],
+ ['oo \o'],
+ ['oo', '\o'],
+ ['oo', '\\o'],
+ ]
+
+ for (case, expected) in zip(cases, answers):
+ self.assertEqual(imap4.parseNestedParens(case), [expected])
+
+ # XXX This code used to work, but changes occurred within the
+ # imap4.py module which made it no longer necessary for *all* of it
+ # to work. In particular, only the part that makes
+ # 'BODY.PEEK[HEADER.FIELDS.NOT (Subject Bcc Cc)]' come out correctly
+ # no longer needs to work. So, I am loathe to delete the entire
+ # section of the test. --exarkun
+ #
+
+# for (case, expected) in zip(answers, cases):
+# self.assertEqual('(' + imap4.collapseNestedLists(case) + ')', expected)
+
+
+ def test_fetchParserSimple(self):
+ cases = [
+ ['ENVELOPE', 'Envelope'],
+ ['FLAGS', 'Flags'],
+ ['INTERNALDATE', 'InternalDate'],
+ ['RFC822.HEADER', 'RFC822Header'],
+ ['RFC822.SIZE', 'RFC822Size'],
+ ['RFC822.TEXT', 'RFC822Text'],
+ ['RFC822', 'RFC822'],
+ ['UID', 'UID'],
+ ['BODYSTRUCTURE', 'BodyStructure'],
+ ]
+
+ for (inp, outp) in cases:
+ p = imap4._FetchParser()
+ p.parseString(inp)
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], getattr(p, outp)))
+
+
+ def test_fetchParserMacros(self):
+ cases = [
+ ['ALL', (4, ['flags', 'internaldate', 'rfc822.size', 'envelope'])],
+ ['FULL', (5, ['flags', 'internaldate', 'rfc822.size', 'envelope', 'body'])],
+ ['FAST', (3, ['flags', 'internaldate', 'rfc822.size'])],
+ ]
+
+ for (inp, outp) in cases:
+ p = imap4._FetchParser()
+ p.parseString(inp)
+ self.assertEqual(len(p.result), outp[0])
+ p = [str(p).lower() for p in p.result]
+ p.sort()
+ outp[1].sort()
+ self.assertEqual(p, outp[1])
+
+
+ def test_fetchParserBody(self):
+ P = imap4._FetchParser
+
+ p = P()
+ p.parseString('BODY')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, False)
+ self.assertEqual(p.result[0].header, None)
+ self.assertEqual(str(p.result[0]), 'BODY')
+
+ p = P()
+ p.parseString('BODY.PEEK')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, True)
+ self.assertEqual(str(p.result[0]), 'BODY')
+
+ p = P()
+ p.parseString('BODY[]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].empty, True)
+ self.assertEqual(str(p.result[0]), 'BODY[]')
+
+ p = P()
+ p.parseString('BODY[HEADER]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, False)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].header.negate, True)
+ self.assertEqual(p.result[0].header.fields, ())
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[HEADER]')
+
+ p = P()
+ p.parseString('BODY.PEEK[HEADER]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, True)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].header.negate, True)
+ self.assertEqual(p.result[0].header.fields, ())
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[HEADER]')
+
+ p = P()
+ p.parseString('BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, False)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].header.negate, False)
+ self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
+
+ p = P()
+ p.parseString('BODY.PEEK[HEADER.FIELDS (Subject Cc Message-Id)]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, True)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].header.negate, False)
+ self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS (Subject Cc Message-Id)]')
+
+ p = P()
+ p.parseString('BODY.PEEK[HEADER.FIELDS.NOT (Subject Cc Message-Id)]')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, True)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].header.negate, True)
+ self.assertEqual(p.result[0].header.fields, ['SUBJECT', 'CC', 'MESSAGE-ID'])
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[HEADER.FIELDS.NOT (Subject Cc Message-Id)]')
+
+ p = P()
+ p.parseString('BODY[1.MIME]<10.50>')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, False)
+ self.failUnless(isinstance(p.result[0].mime, p.MIME))
+ self.assertEqual(p.result[0].part, (0,))
+ self.assertEqual(p.result[0].partialBegin, 10)
+ self.assertEqual(p.result[0].partialLength, 50)
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[1.MIME]<10.50>')
+
+ p = P()
+ p.parseString('BODY.PEEK[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>')
+ self.assertEqual(len(p.result), 1)
+ self.failUnless(isinstance(p.result[0], p.Body))
+ self.assertEqual(p.result[0].peek, True)
+ self.failUnless(isinstance(p.result[0].header, p.Header))
+ self.assertEqual(p.result[0].part, (0, 2, 8, 10))
+ self.assertEqual(p.result[0].header.fields, ['MESSAGE-ID', 'DATE'])
+ self.assertEqual(p.result[0].partialBegin, 103)
+ self.assertEqual(p.result[0].partialLength, 69)
+ self.assertEqual(p.result[0].empty, False)
+ self.assertEqual(str(p.result[0]), 'BODY[1.3.9.11.HEADER.FIELDS.NOT (Message-Id Date)]<103.69>')
+
+
+ def test_files(self):
+ inputStructure = [
+ 'foo', 'bar', 'baz', StringIO('this is a file\r\n'), 'buz'
+ ]
+
+ output = '"foo" "bar" "baz" {16}\r\nthis is a file\r\n "buz"'
+
+ self.assertEqual(imap4.collapseNestedLists(inputStructure), output)
+
+
+ def test_quoteAvoider(self):
+ input = [
+ 'foo', imap4.DontQuoteMe('bar'), "baz", StringIO('this is a file\r\n'),
+ imap4.DontQuoteMe('buz'), ""
+ ]
+
+ output = '"foo" bar "baz" {16}\r\nthis is a file\r\n buz ""'
+
+ self.assertEqual(imap4.collapseNestedLists(input), output)
+
+
+ def test_literals(self):
+ cases = [
+ ('({10}\r\n0123456789)', [['0123456789']]),
+ ]
+
+ for (case, expected) in cases:
+ self.assertEqual(imap4.parseNestedParens(case), expected)
+
+
+ def test_queryBuilder(self):
+ inputs = [
+ imap4.Query(flagged=1),
+ imap4.Query(sorted=1, unflagged=1, deleted=1),
+ imap4.Or(imap4.Query(flagged=1), imap4.Query(deleted=1)),
+ imap4.Query(before='today'),
+ imap4.Or(
+ imap4.Query(deleted=1),
+ imap4.Query(unseen=1),
+ imap4.Query(new=1)
+ ),
+ imap4.Or(
+ imap4.Not(
+ imap4.Or(
+ imap4.Query(sorted=1, since='yesterday', smaller=1000),
+ imap4.Query(sorted=1, before='tuesday', larger=10000),
+ imap4.Query(sorted=1, unseen=1, deleted=1, before='today'),
+ imap4.Not(
+ imap4.Query(subject='spam')
+ ),
+ ),
+ ),
+ imap4.Not(
+ imap4.Query(uid='1:5')
+ ),
+ )
+ ]
+
+ outputs = [
+ 'FLAGGED',
+ '(DELETED UNFLAGGED)',
+ '(OR FLAGGED DELETED)',
+ '(BEFORE "today")',
+ '(OR DELETED (OR UNSEEN NEW))',
+ '(OR (NOT (OR (SINCE "yesterday" SMALLER 1000) ' # Continuing
+ '(OR (BEFORE "tuesday" LARGER 10000) (OR (BEFORE ' # Some more
+ '"today" DELETED UNSEEN) (NOT (SUBJECT "spam")))))) ' # And more
+ '(NOT (UID 1:5)))',
+ ]
+
+ for (query, expected) in zip(inputs, outputs):
+ self.assertEqual(query, expected)
+
+
+ def test_invalidIdListParser(self):
+ """
+ Trying to parse an invalid representation of a sequence range raises an
+ L{IllegalIdentifierError}.
+ """
+ inputs = [
+ '*:*',
+ 'foo',
+ '4:',
+ 'bar:5'
+ ]
+
+ for input in inputs:
+ self.assertRaises(imap4.IllegalIdentifierError,
+ imap4.parseIdList, input, 12345)
+
+
+ def test_invalidIdListParserNonPositive(self):
+ """
+ Zeroes and negative values are not accepted in id range expressions. RFC
+ 3501 states that sequence numbers and sequence ranges consist of
+ non-negative numbers (RFC 3501 section 9, the seq-number grammar item).
+ """
+ inputs = [
+ '0:5',
+ '0:0',
+ '*:0',
+ '0',
+ '-3:5',
+ '1:-2',
+ '-1'
+ ]
+
+ for input in inputs:
+ self.assertRaises(imap4.IllegalIdentifierError,
+ imap4.parseIdList, input, 12345)
+
+
+ def test_parseIdList(self):
+ """
+ The function to parse sequence ranges yields appropriate L{MessageSet}
+ objects.
+ """
+ inputs = [
+ '1:*',
+ '5:*',
+ '1:2,5:*',
+ '*',
+ '1',
+ '1,2',
+ '1,3,5',
+ '1:10',
+ '1:10,11',
+ '1:5,10:20',
+ '1,5:10',
+ '1,5:10,15:20',
+ '1:10,15,20:25',
+ '4:2'
+ ]
+
+ outputs = [
+ MessageSet(1, None),
+ MessageSet(5, None),
+ MessageSet(5, None) + MessageSet(1, 2),
+ MessageSet(None, None),
+ MessageSet(1),
+ MessageSet(1, 2),
+ MessageSet(1) + MessageSet(3) + MessageSet(5),
+ MessageSet(1, 10),
+ MessageSet(1, 11),
+ MessageSet(1, 5) + MessageSet(10, 20),
+ MessageSet(1) + MessageSet(5, 10),
+ MessageSet(1) + MessageSet(5, 10) + MessageSet(15, 20),
+ MessageSet(1, 10) + MessageSet(15) + MessageSet(20, 25),
+ MessageSet(2, 4),
+ ]
+
+ lengths = [
+ None, None, None,
+ 1, 1, 2, 3, 10, 11, 16, 7, 13, 17, 3
+ ]
+
+ for (input, expected) in zip(inputs, outputs):
+ self.assertEqual(imap4.parseIdList(input), expected)
+
+ for (input, expected) in zip(inputs, lengths):
+ if expected is None:
+ self.assertRaises(TypeError, len, imap4.parseIdList(input))
+ else:
+ L = len(imap4.parseIdList(input))
+ self.assertEqual(L, expected,
+ "len(%r) = %r != %r" % (input, L, expected))
+
+class SimpleMailbox:
+ implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
+
+ flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag')
+ messages = []
+ mUID = 0
+ rw = 1
+ closed = False
+
+ def __init__(self):
+ self.listeners = []
+ self.addListener = self.listeners.append
+ self.removeListener = self.listeners.remove
+
+ def getFlags(self):
+ return self.flags
+
+ def getUIDValidity(self):
+ return 42
+
+ def getUIDNext(self):
+ return len(self.messages) + 1
+
+ def getMessageCount(self):
+ return 9
+
+ def getRecentCount(self):
+ return 3
+
+ def getUnseenCount(self):
+ return 4
+
+ def isWriteable(self):
+ return self.rw
+
+ def destroy(self):
+ pass
+
+ def getHierarchicalDelimiter(self):
+ return '/'
+
+ def requestStatus(self, names):
+ r = {}
+ if 'MESSAGES' in names:
+ r['MESSAGES'] = self.getMessageCount()
+ if 'RECENT' in names:
+ r['RECENT'] = self.getRecentCount()
+ if 'UIDNEXT' in names:
+ r['UIDNEXT'] = self.getMessageCount() + 1
+ if 'UIDVALIDITY' in names:
+ r['UIDVALIDITY'] = self.getUID()
+ if 'UNSEEN' in names:
+ r['UNSEEN'] = self.getUnseenCount()
+ return defer.succeed(r)
+
+ def addMessage(self, message, flags, date = None):
+ self.messages.append((message, flags, date, self.mUID))
+ self.mUID += 1
+ return defer.succeed(None)
+
+ def expunge(self):
+ delete = []
+ for i in self.messages:
+ if '\\Deleted' in i[1]:
+ delete.append(i)
+ for i in delete:
+ self.messages.remove(i)
+ return [i[3] for i in delete]
+
+ def close(self):
+ self.closed = True
+
+class Account(imap4.MemoryAccount):
+ mailboxFactory = SimpleMailbox
+ def _emptyMailbox(self, name, id):
+ return self.mailboxFactory()
+
+ def select(self, name, rw=1):
+ mbox = imap4.MemoryAccount.select(self, name)
+ if mbox is not None:
+ mbox.rw = rw
+ return mbox
+
+class SimpleServer(imap4.IMAP4Server):
+ def __init__(self, *args, **kw):
+ imap4.IMAP4Server.__init__(self, *args, **kw)
+ realm = TestRealm()
+ realm.theAccount = Account('testuser')
+ portal = cred.portal.Portal(realm)
+ c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ self.checker = c
+ self.portal = portal
+ portal.registerChecker(c)
+ self.timeoutTest = False
+
+ def lineReceived(self, line):
+ if self.timeoutTest:
+ #Do not send a respones
+ return
+
+ imap4.IMAP4Server.lineReceived(self, line)
+
+ _username = 'testuser'
+ _password = 'password-test'
+ def authenticateLogin(self, username, password):
+ if username == self._username and password == self._password:
+ return imap4.IAccount, self.theAccount, lambda: None
+ raise cred.error.UnauthorizedLogin()
+
+
+class SimpleClient(imap4.IMAP4Client):
+ def __init__(self, deferred, contextFactory = None):
+ imap4.IMAP4Client.__init__(self, contextFactory)
+ self.deferred = deferred
+ self.events = []
+
+ def serverGreeting(self, caps):
+ self.deferred.callback(None)
+
+ def modeChanged(self, writeable):
+ self.events.append(['modeChanged', writeable])
+ self.transport.loseConnection()
+
+ def flagsChanged(self, newFlags):
+ self.events.append(['flagsChanged', newFlags])
+ self.transport.loseConnection()
+
+ def newMessages(self, exists, recent):
+ self.events.append(['newMessages', exists, recent])
+ self.transport.loseConnection()
+
+
+
+class IMAP4HelperMixin:
+
+ serverCTX = None
+ clientCTX = None
+
+ def setUp(self):
+ d = defer.Deferred()
+ self.server = SimpleServer(contextFactory=self.serverCTX)
+ self.client = SimpleClient(d, contextFactory=self.clientCTX)
+ self.connected = d
+
+ SimpleMailbox.messages = []
+ theAccount = Account('testuser')
+ theAccount.mboxType = SimpleMailbox
+ SimpleServer.theAccount = theAccount
+
+ def tearDown(self):
+ del self.server
+ del self.client
+ del self.connected
+
+ def _cbStopClient(self, ignore):
+ self.client.transport.loseConnection()
+
+ def _ebGeneral(self, failure):
+ self.client.transport.loseConnection()
+ self.server.transport.loseConnection()
+ failure.raiseException()
+
+ def loopback(self):
+ return loopback.loopbackAsync(self.server, self.client)
+
+class IMAP4ServerTestCase(IMAP4HelperMixin, unittest.TestCase):
+ def testCapability(self):
+ caps = {}
+ def getCaps():
+ def gotCaps(c):
+ caps.update(c)
+ self.server.transport.loseConnection()
+ return self.client.getCapabilities().addCallback(gotCaps)
+ d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ expected = {'IMAP4rev1': None, 'NAMESPACE': None, 'IDLE': None}
+ return d.addCallback(lambda _: self.assertEqual(expected, caps))
+
+ def testCapabilityWithAuth(self):
+ caps = {}
+ self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials
+ def getCaps():
+ def gotCaps(c):
+ caps.update(c)
+ self.server.transport.loseConnection()
+ return self.client.getCapabilities().addCallback(gotCaps)
+ d1 = self.connected.addCallback(strip(getCaps)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+
+ expCap = {'IMAP4rev1': None, 'NAMESPACE': None,
+ 'IDLE': None, 'AUTH': ['CRAM-MD5']}
+
+ return d.addCallback(lambda _: self.assertEqual(expCap, caps))
+
+ def testLogout(self):
+ self.loggedOut = 0
+ def logout():
+ def setLoggedOut():
+ self.loggedOut = 1
+ self.client.logout().addCallback(strip(setLoggedOut))
+ self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral)
+ d = self.loopback()
+ return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1))
+
+ def testNoop(self):
+ self.responses = None
+ def noop():
+ def setResponses(responses):
+ self.responses = responses
+ self.server.transport.loseConnection()
+ self.client.noop().addCallback(setResponses)
+ self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral)
+ d = self.loopback()
+ return d.addCallback(lambda _: self.assertEqual(self.responses, []))
+
+ def testLogin(self):
+ def login():
+ d = self.client.login('testuser', 'password-test')
+ d.addCallback(self._cbStopClient)
+ d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([d1, self.loopback()])
+ return d.addCallback(self._cbTestLogin)
+
+ def _cbTestLogin(self, ignored):
+ self.assertEqual(self.server.account, SimpleServer.theAccount)
+ self.assertEqual(self.server.state, 'auth')
+
+ def testFailedLogin(self):
+ def login():
+ d = self.client.login('testuser', 'wrong-password')
+ d.addBoth(self._cbStopClient)
+
+ d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestFailedLogin)
+
+ def _cbTestFailedLogin(self, ignored):
+ self.assertEqual(self.server.account, None)
+ self.assertEqual(self.server.state, 'unauth')
+
+
+ def testLoginRequiringQuoting(self):
+ self.server._username = '{test}user'
+ self.server._password = '{test}password'
+
+ def login():
+ d = self.client.login('{test}user', '{test}password')
+ d.addBoth(self._cbStopClient)
+
+ d1 = self.connected.addCallback(strip(login)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestLoginRequiringQuoting)
+
+ def _cbTestLoginRequiringQuoting(self, ignored):
+ self.assertEqual(self.server.account, SimpleServer.theAccount)
+ self.assertEqual(self.server.state, 'auth')
+
+
+ def testNamespace(self):
+ self.namespaceArgs = None
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def namespace():
+ def gotNamespace(args):
+ self.namespaceArgs = args
+ self._cbStopClient(None)
+ return self.client.namespace().addCallback(gotNamespace)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(namespace))
+ d1.addErrback(self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _: self.assertEqual(self.namespaceArgs,
+ [[['', '/']], [], []]))
+ return d
+
+ def testSelect(self):
+ SimpleServer.theAccount.addMailbox('test-mailbox')
+ self.selectedArgs = None
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def select():
+ def selected(args):
+ self.selectedArgs = args
+ self._cbStopClient(None)
+ d = self.client.select('test-mailbox')
+ d.addCallback(selected)
+ return d
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(select))
+ d1.addErrback(self._ebGeneral)
+ d2 = self.loopback()
+ return defer.gatherResults([d1, d2]).addCallback(self._cbTestSelect)
+
+ def _cbTestSelect(self, ignored):
+ mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX']
+ self.assertEqual(self.server.mbox, mbox)
+ self.assertEqual(self.selectedArgs, {
+ 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42,
+ 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'),
+ 'READ-WRITE': 1
+ })
+
+
+ def test_examine(self):
+ """
+ L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and
+ returns a L{Deferred} which fires with a C{dict} with as many of the
+ following keys as the server includes in its response: C{'FLAGS'},
+ C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'},
+ C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}.
+
+ Unfortunately the server doesn't generate all of these so it's hard to
+ test the client's handling of them here. See
+ L{IMAP4ClientExamineTests} below.
+
+ See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2,
+ for details.
+ """
+ SimpleServer.theAccount.addMailbox('test-mailbox')
+ self.examinedArgs = None
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def examine():
+ def examined(args):
+ self.examinedArgs = args
+ self._cbStopClient(None)
+ d = self.client.examine('test-mailbox')
+ d.addCallback(examined)
+ return d
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(examine))
+ d1.addErrback(self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestExamine)
+
+
+ def _cbTestExamine(self, ignored):
+ mbox = SimpleServer.theAccount.mailboxes['TEST-MAILBOX']
+ self.assertEqual(self.server.mbox, mbox)
+ self.assertEqual(self.examinedArgs, {
+ 'EXISTS': 9, 'RECENT': 3, 'UIDVALIDITY': 42,
+ 'FLAGS': ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag'),
+ 'READ-WRITE': False})
+
+
+ def testCreate(self):
+ succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'INBOX')
+ fail = ('testbox', 'test/box')
+
+ def cb(): self.result.append(1)
+ def eb(failure): self.result.append(0)
+
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def create():
+ for name in succeed + fail:
+ d = self.client.create(name)
+ d.addCallback(strip(cb)).addErrback(eb)
+ d.addCallbacks(self._cbStopClient, self._ebGeneral)
+
+ self.result = []
+ d1 = self.connected.addCallback(strip(login)).addCallback(strip(create))
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestCreate, succeed, fail)
+
+ def _cbTestCreate(self, ignored, succeed, fail):
+ self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail))
+ mbox = SimpleServer.theAccount.mailboxes.keys()
+ answers = ['inbox', 'testbox', 'test/box', 'test', 'test/box/box']
+ mbox.sort()
+ answers.sort()
+ self.assertEqual(mbox, [a.upper() for a in answers])
+
+ def testDelete(self):
+ SimpleServer.theAccount.addMailbox('delete/me')
+
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def delete():
+ return self.client.delete('delete/me')
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(delete), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _:
+ self.assertEqual(SimpleServer.theAccount.mailboxes.keys(), []))
+ return d
+
+ def testIllegalInboxDelete(self):
+ self.stashed = None
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def delete():
+ return self.client.delete('inbox')
+ def stash(result):
+ self.stashed = result
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(delete), self._ebGeneral)
+ d1.addBoth(stash)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _: self.failUnless(isinstance(self.stashed,
+ failure.Failure)))
+ return d
+
+
+ def testNonExistentDelete(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def delete():
+ return self.client.delete('delete/me')
+ def deleteFailed(failure):
+ self.failure = failure
+
+ self.failure = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(delete)).addErrback(deleteFailed)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _: self.assertEqual(str(self.failure.value),
+ 'No such mailbox'))
+ return d
+
+
+ def testIllegalDelete(self):
+ m = SimpleMailbox()
+ m.flags = (r'\Noselect',)
+ SimpleServer.theAccount.addMailbox('delete', m)
+ SimpleServer.theAccount.addMailbox('delete/me')
+
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def delete():
+ return self.client.delete('delete')
+ def deleteFailed(failure):
+ self.failure = failure
+
+ self.failure = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(delete)).addErrback(deleteFailed)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ expected = "Hierarchically inferior mailboxes exist and \\Noselect is set"
+ d.addCallback(lambda _:
+ self.assertEqual(str(self.failure.value), expected))
+ return d
+
+ def testRename(self):
+ SimpleServer.theAccount.addMailbox('oldmbox')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def rename():
+ return self.client.rename('oldmbox', 'newname')
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(rename), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _:
+ self.assertEqual(SimpleServer.theAccount.mailboxes.keys(),
+ ['NEWNAME']))
+ return d
+
+ def testIllegalInboxRename(self):
+ self.stashed = None
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def rename():
+ return self.client.rename('inbox', 'frotz')
+ def stash(stuff):
+ self.stashed = stuff
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(rename), self._ebGeneral)
+ d1.addBoth(stash)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _:
+ self.failUnless(isinstance(self.stashed, failure.Failure)))
+ return d
+
+ def testHierarchicalRename(self):
+ SimpleServer.theAccount.create('oldmbox/m1')
+ SimpleServer.theAccount.create('oldmbox/m2')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def rename():
+ return self.client.rename('oldmbox', 'newname')
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(rename), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestHierarchicalRename)
+
+ def _cbTestHierarchicalRename(self, ignored):
+ mboxes = SimpleServer.theAccount.mailboxes.keys()
+ expected = ['newname', 'newname/m1', 'newname/m2']
+ mboxes.sort()
+ self.assertEqual(mboxes, [s.upper() for s in expected])
+
+ def testSubscribe(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def subscribe():
+ return self.client.subscribe('this/mbox')
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(subscribe), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _:
+ self.assertEqual(SimpleServer.theAccount.subscriptions,
+ ['THIS/MBOX']))
+ return d
+
+ def testUnsubscribe(self):
+ SimpleServer.theAccount.subscriptions = ['THIS/MBOX', 'THAT/MBOX']
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def unsubscribe():
+ return self.client.unsubscribe('this/mbox')
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(unsubscribe), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _:
+ self.assertEqual(SimpleServer.theAccount.subscriptions,
+ ['THAT/MBOX']))
+ return d
+
+ def _listSetup(self, f):
+ SimpleServer.theAccount.addMailbox('root/subthing')
+ SimpleServer.theAccount.addMailbox('root/another-thing')
+ SimpleServer.theAccount.addMailbox('non-root/subthing')
+
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def listed(answers):
+ self.listed = answers
+
+ self.listed = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(f), self._ebGeneral)
+ d1.addCallbacks(listed, self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed)
+
+ def testList(self):
+ def list():
+ return self.client.list('root', '%')
+ d = self._listSetup(list)
+ d.addCallback(lambda listed: self.assertEqual(
+ sortNest(listed),
+ sortNest([
+ (SimpleMailbox.flags, "/", "ROOT/SUBTHING"),
+ (SimpleMailbox.flags, "/", "ROOT/ANOTHER-THING")
+ ])
+ ))
+ return d
+
+ def testLSub(self):
+ SimpleServer.theAccount.subscribe('ROOT/SUBTHING')
+ def lsub():
+ return self.client.lsub('root', '%')
+ d = self._listSetup(lsub)
+ d.addCallback(self.assertEqual,
+ [(SimpleMailbox.flags, "/", "ROOT/SUBTHING")])
+ return d
+
+ def testStatus(self):
+ SimpleServer.theAccount.addMailbox('root/subthing')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def status():
+ return self.client.status('root/subthing', 'MESSAGES', 'UIDNEXT', 'UNSEEN')
+ def statused(result):
+ self.statused = result
+
+ self.statused = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(status), self._ebGeneral)
+ d1.addCallbacks(statused, self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(lambda _: self.assertEqual(
+ self.statused,
+ {'MESSAGES': 9, 'UIDNEXT': '10', 'UNSEEN': 4}
+ ))
+ return d
+
+ def testFailedStatus(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def status():
+ return self.client.status('root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN')
+ def statused(result):
+ self.statused = result
+ def failed(failure):
+ self.failure = failure
+
+ self.statused = self.failure = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(status), self._ebGeneral)
+ d1.addCallbacks(statused, failed)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ return defer.gatherResults([d1, d2]).addCallback(self._cbTestFailedStatus)
+
+ def _cbTestFailedStatus(self, ignored):
+ self.assertEqual(
+ self.statused, None
+ )
+ self.assertEqual(
+ self.failure.value.args,
+ ('Could not open mailbox',)
+ )
+
+ def testFullAppend(self):
+ infile = util.sibpath(__file__, 'rfc822.message')
+ message = open(infile)
+ SimpleServer.theAccount.addMailbox('root/subthing')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def append():
+ return self.client.append(
+ 'root/subthing',
+ message,
+ ('\\SEEN', '\\DELETED'),
+ 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)',
+ )
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(append), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestFullAppend, infile)
+
+ def _cbTestFullAppend(self, ignored, infile):
+ mb = SimpleServer.theAccount.mailboxes['ROOT/SUBTHING']
+ self.assertEqual(1, len(mb.messages))
+ self.assertEqual(
+ (['\\SEEN', '\\DELETED'], 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', 0),
+ mb.messages[0][1:]
+ )
+ self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue())
+
+ def testPartialAppend(self):
+ infile = util.sibpath(__file__, 'rfc822.message')
+ message = open(infile)
+ SimpleServer.theAccount.addMailbox('PARTIAL/SUBTHING')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def append():
+ message = file(infile)
+ return self.client.sendCommand(
+ imap4.Command(
+ 'APPEND',
+ 'PARTIAL/SUBTHING (\\SEEN) "Right now" {%d}' % os.path.getsize(infile),
+ (), self.client._IMAP4Client__cbContinueAppend, message
+ )
+ )
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(append), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestPartialAppend, infile)
+
+ def _cbTestPartialAppend(self, ignored, infile):
+ mb = SimpleServer.theAccount.mailboxes['PARTIAL/SUBTHING']
+ self.assertEqual(1, len(mb.messages))
+ self.assertEqual(
+ (['\\SEEN'], 'Right now', 0),
+ mb.messages[0][1:]
+ )
+ self.assertEqual(open(infile).read(), mb.messages[0][0].getvalue())
+
+ def testCheck(self):
+ SimpleServer.theAccount.addMailbox('root/subthing')
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def select():
+ return self.client.select('root/subthing')
+ def check():
+ return self.client.check()
+
+ d = self.connected.addCallback(strip(login))
+ d.addCallbacks(strip(select), self._ebGeneral)
+ d.addCallbacks(strip(check), self._ebGeneral)
+ d.addCallbacks(self._cbStopClient, self._ebGeneral)
+ return self.loopback()
+
+ # Okay, that was fun
+
+ def testClose(self):
+ m = SimpleMailbox()
+ m.messages = [
+ ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0),
+ ('Message 2', ('AnotherFlag',), None, 1),
+ ('Message 3', ('\\Deleted',), None, 2),
+ ]
+ SimpleServer.theAccount.addMailbox('mailbox', m)
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def select():
+ return self.client.select('mailbox')
+ def close():
+ return self.client.close()
+
+ d = self.connected.addCallback(strip(login))
+ d.addCallbacks(strip(select), self._ebGeneral)
+ d.addCallbacks(strip(close), self._ebGeneral)
+ d.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ return defer.gatherResults([d, d2]).addCallback(self._cbTestClose, m)
+
+ def _cbTestClose(self, ignored, m):
+ self.assertEqual(len(m.messages), 1)
+ self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1))
+ self.failUnless(m.closed)
+
+ def testExpunge(self):
+ m = SimpleMailbox()
+ m.messages = [
+ ('Message 1', ('\\Deleted', 'AnotherFlag'), None, 0),
+ ('Message 2', ('AnotherFlag',), None, 1),
+ ('Message 3', ('\\Deleted',), None, 2),
+ ]
+ SimpleServer.theAccount.addMailbox('mailbox', m)
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def select():
+ return self.client.select('mailbox')
+ def expunge():
+ return self.client.expunge()
+ def expunged(results):
+ self.failIf(self.server.mbox is None)
+ self.results = results
+
+ self.results = None
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallbacks(strip(select), self._ebGeneral)
+ d1.addCallbacks(strip(expunge), self._ebGeneral)
+ d1.addCallbacks(expunged, self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestExpunge, m)
+
+ def _cbTestExpunge(self, ignored, m):
+ self.assertEqual(len(m.messages), 1)
+ self.assertEqual(m.messages[0], ('Message 2', ('AnotherFlag',), None, 1))
+
+ self.assertEqual(self.results, [0, 2])
+
+
+
+class IMAP4ServerSearchTestCase(IMAP4HelperMixin, unittest.TestCase):
+ """
+ Tests for the behavior of the search_* functions in L{imap4.IMAP4Server}.
+ """
+ def setUp(self):
+ IMAP4HelperMixin.setUp(self)
+ self.earlierQuery = ["10-Dec-2009"]
+ self.sameDateQuery = ["13-Dec-2009"]
+ self.laterQuery = ["16-Dec-2009"]
+ self.seq = 0
+ self.msg = FakeyMessage({"date" : "Mon, 13 Dec 2009 21:25:10 GMT"}, [],
+ '', '', 1234, None)
+
+
+ def test_searchSentBefore(self):
+ """
+ L{imap4.IMAP4Server.search_SENTBEFORE} returns True if the message date
+ is earlier than the query date.
+ """
+ self.assertFalse(
+ self.server.search_SENTBEFORE(self.earlierQuery, self.seq, self.msg))
+ self.assertTrue(
+ self.server.search_SENTBEFORE(self.laterQuery, self.seq, self.msg))
+
+ def test_searchWildcard(self):
+ """
+ L{imap4.IMAP4Server.search_UID} returns True if the message UID is in
+ the search range.
+ """
+ self.assertFalse(
+ self.server.search_UID(['2:3'], self.seq, self.msg, (1, 1234)))
+ # 2:* should get translated to 2:<max UID> and then to 1:2
+ self.assertTrue(
+ self.server.search_UID(['2:*'], self.seq, self.msg, (1, 1234)))
+ self.assertTrue(
+ self.server.search_UID(['*'], self.seq, self.msg, (1, 1234)))
+
+ def test_searchWildcardHigh(self):
+ """
+ L{imap4.IMAP4Server.search_UID} should return True if there is a
+ wildcard, because a wildcard means "highest UID in the mailbox".
+ """
+ self.assertTrue(
+ self.server.search_UID(['1235:*'], self.seq, self.msg, (1234, 1)))
+
+ def test_reversedSearchTerms(self):
+ """
+ L{imap4.IMAP4Server.search_SENTON} returns True if the message date is
+ the same as the query date.
+ """
+ msgset = imap4.parseIdList('4:2')
+ self.assertEqual(list(msgset), [2, 3, 4])
+
+ def test_searchSentOn(self):
+ """
+ L{imap4.IMAP4Server.search_SENTON} returns True if the message date is
+ the same as the query date.
+ """
+ self.assertFalse(
+ self.server.search_SENTON(self.earlierQuery, self.seq, self.msg))
+ self.assertTrue(
+ self.server.search_SENTON(self.sameDateQuery, self.seq, self.msg))
+ self.assertFalse(
+ self.server.search_SENTON(self.laterQuery, self.seq, self.msg))
+
+
+ def test_searchSentSince(self):
+ """
+ L{imap4.IMAP4Server.search_SENTSINCE} returns True if the message date
+ is later than the query date.
+ """
+ self.assertTrue(
+ self.server.search_SENTSINCE(self.earlierQuery, self.seq, self.msg))
+ self.assertFalse(
+ self.server.search_SENTSINCE(self.laterQuery, self.seq, self.msg))
+
+
+ def test_searchOr(self):
+ """
+ L{imap4.IMAP4Server.search_OR} returns true if either of the two
+ expressions supplied to it returns true and returns false if neither
+ does.
+ """
+ self.assertTrue(
+ self.server.search_OR(
+ ["SENTSINCE"] + self.earlierQuery +
+ ["SENTSINCE"] + self.laterQuery,
+ self.seq, self.msg, (None, None)))
+ self.assertTrue(
+ self.server.search_OR(
+ ["SENTSINCE"] + self.laterQuery +
+ ["SENTSINCE"] + self.earlierQuery,
+ self.seq, self.msg, (None, None)))
+ self.assertFalse(
+ self.server.search_OR(
+ ["SENTON"] + self.laterQuery +
+ ["SENTSINCE"] + self.laterQuery,
+ self.seq, self.msg, (None, None)))
+
+
+ def test_searchNot(self):
+ """
+ L{imap4.IMAP4Server.search_NOT} returns the negation of the result
+ of the expression supplied to it.
+ """
+ self.assertFalse(self.server.search_NOT(
+ ["SENTSINCE"] + self.earlierQuery, self.seq, self.msg,
+ (None, None)))
+ self.assertTrue(self.server.search_NOT(
+ ["SENTON"] + self.laterQuery, self.seq, self.msg,
+ (None, None)))
+
+
+
+class TestRealm:
+ theAccount = None
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return imap4.IAccount, self.theAccount, lambda: None
+
+class TestChecker:
+ credentialInterfaces = (cred.credentials.IUsernameHashedPassword, cred.credentials.IUsernamePassword)
+
+ users = {
+ 'testuser': 'secret'
+ }
+
+ def requestAvatarId(self, credentials):
+ if credentials.username in self.users:
+ return defer.maybeDeferred(
+ credentials.checkPassword, self.users[credentials.username]
+ ).addCallback(self._cbCheck, credentials.username)
+
+ def _cbCheck(self, result, username):
+ if result:
+ return username
+ raise cred.error.UnauthorizedLogin()
+
+class AuthenticatorTestCase(IMAP4HelperMixin, unittest.TestCase):
+ def setUp(self):
+ IMAP4HelperMixin.setUp(self)
+
+ realm = TestRealm()
+ realm.theAccount = Account('testuser')
+ portal = cred.portal.Portal(realm)
+ portal.registerChecker(TestChecker())
+ self.server.portal = portal
+
+ self.authenticated = 0
+ self.account = realm.theAccount
+
+ def testCramMD5(self):
+ self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials
+ cAuth = imap4.CramMD5ClientAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def auth():
+ return self.client.authenticate('secret')
+ def authed():
+ self.authenticated = 1
+
+ d1 = self.connected.addCallback(strip(auth))
+ d1.addCallbacks(strip(authed), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d2 = self.loopback()
+ d = defer.gatherResults([d1, d2])
+ return d.addCallback(self._cbTestCramMD5)
+
+ def _cbTestCramMD5(self, ignored):
+ self.assertEqual(self.authenticated, 1)
+ self.assertEqual(self.server.account, self.account)
+
+ def testFailedCramMD5(self):
+ self.server.challengers['CRAM-MD5'] = cred.credentials.CramMD5Credentials
+ cAuth = imap4.CramMD5ClientAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def misauth():
+ return self.client.authenticate('not the secret')
+ def authed():
+ self.authenticated = 1
+ def misauthed():
+ self.authenticated = -1
+
+ d1 = self.connected.addCallback(strip(misauth))
+ d1.addCallbacks(strip(authed), strip(misauthed))
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestFailedCramMD5)
+
+ def _cbTestFailedCramMD5(self, ignored):
+ self.assertEqual(self.authenticated, -1)
+ self.assertEqual(self.server.account, None)
+
+ def testLOGIN(self):
+ self.server.challengers['LOGIN'] = imap4.LOGINCredentials
+ cAuth = imap4.LOGINAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def auth():
+ return self.client.authenticate('secret')
+ def authed():
+ self.authenticated = 1
+
+ d1 = self.connected.addCallback(strip(auth))
+ d1.addCallbacks(strip(authed), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestLOGIN)
+
+ def _cbTestLOGIN(self, ignored):
+ self.assertEqual(self.authenticated, 1)
+ self.assertEqual(self.server.account, self.account)
+
+ def testFailedLOGIN(self):
+ self.server.challengers['LOGIN'] = imap4.LOGINCredentials
+ cAuth = imap4.LOGINAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def misauth():
+ return self.client.authenticate('not the secret')
+ def authed():
+ self.authenticated = 1
+ def misauthed():
+ self.authenticated = -1
+
+ d1 = self.connected.addCallback(strip(misauth))
+ d1.addCallbacks(strip(authed), strip(misauthed))
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestFailedLOGIN)
+
+ def _cbTestFailedLOGIN(self, ignored):
+ self.assertEqual(self.authenticated, -1)
+ self.assertEqual(self.server.account, None)
+
+ def testPLAIN(self):
+ self.server.challengers['PLAIN'] = imap4.PLAINCredentials
+ cAuth = imap4.PLAINAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def auth():
+ return self.client.authenticate('secret')
+ def authed():
+ self.authenticated = 1
+
+ d1 = self.connected.addCallback(strip(auth))
+ d1.addCallbacks(strip(authed), self._ebGeneral)
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestPLAIN)
+
+ def _cbTestPLAIN(self, ignored):
+ self.assertEqual(self.authenticated, 1)
+ self.assertEqual(self.server.account, self.account)
+
+ def testFailedPLAIN(self):
+ self.server.challengers['PLAIN'] = imap4.PLAINCredentials
+ cAuth = imap4.PLAINAuthenticator('testuser')
+ self.client.registerAuthenticator(cAuth)
+
+ def misauth():
+ return self.client.authenticate('not the secret')
+ def authed():
+ self.authenticated = 1
+ def misauthed():
+ self.authenticated = -1
+
+ d1 = self.connected.addCallback(strip(misauth))
+ d1.addCallbacks(strip(authed), strip(misauthed))
+ d1.addCallbacks(self._cbStopClient, self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestFailedPLAIN)
+
+ def _cbTestFailedPLAIN(self, ignored):
+ self.assertEqual(self.authenticated, -1)
+ self.assertEqual(self.server.account, None)
+
+
+
+class SASLPLAINTestCase(unittest.TestCase):
+ """
+ Tests for I{SASL PLAIN} authentication, as implemented by
+ L{imap4.PLAINAuthenticator} and L{imap4.PLAINCredentials}.
+
+ @see: U{http://www.faqs.org/rfcs/rfc2595.html}
+ @see: U{http://www.faqs.org/rfcs/rfc4616.html}
+ """
+ def test_authenticatorChallengeResponse(self):
+ """
+ L{PLAINAuthenticator.challengeResponse} returns challenge strings of
+ the form::
+
+ NUL<authn-id>NUL<secret>
+ """
+ username = 'testuser'
+ secret = 'secret'
+ chal = 'challenge'
+ cAuth = imap4.PLAINAuthenticator(username)
+ response = cAuth.challengeResponse(secret, chal)
+ self.assertEqual(response, '\0%s\0%s' % (username, secret))
+
+
+ def test_credentialsSetResponse(self):
+ """
+ L{PLAINCredentials.setResponse} parses challenge strings of the
+ form::
+
+ NUL<authn-id>NUL<secret>
+ """
+ cred = imap4.PLAINCredentials()
+ cred.setResponse('\0testuser\0secret')
+ self.assertEqual(cred.username, 'testuser')
+ self.assertEqual(cred.password, 'secret')
+
+
+ def test_credentialsInvalidResponse(self):
+ """
+ L{PLAINCredentials.setResponse} raises L{imap4.IllegalClientResponse}
+ when passed a string not of the expected form.
+ """
+ cred = imap4.PLAINCredentials()
+ self.assertRaises(
+ imap4.IllegalClientResponse, cred.setResponse, 'hello')
+ self.assertRaises(
+ imap4.IllegalClientResponse, cred.setResponse, 'hello\0world')
+ self.assertRaises(
+ imap4.IllegalClientResponse, cred.setResponse,
+ 'hello\0world\0Zoom!\0')
+
+
+
+class UnsolicitedResponseTestCase(IMAP4HelperMixin, unittest.TestCase):
+ def testReadWrite(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.modeChanged(1)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestReadWrite)
+
+ def _cbTestReadWrite(self, ignored):
+ E = self.client.events
+ self.assertEqual(E, [['modeChanged', 1]])
+
+ def testReadOnly(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.modeChanged(0)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestReadOnly)
+
+ def _cbTestReadOnly(self, ignored):
+ E = self.client.events
+ self.assertEqual(E, [['modeChanged', 0]])
+
+ def testFlagChange(self):
+ flags = {
+ 1: ['\\Answered', '\\Deleted'],
+ 5: [],
+ 10: ['\\Recent']
+ }
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.flagsChanged(flags)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestFlagChange, flags)
+
+ def _cbTestFlagChange(self, ignored, flags):
+ E = self.client.events
+ expect = [['flagsChanged', {x[0]: x[1]}] for x in flags.items()]
+ E.sort()
+ expect.sort()
+ self.assertEqual(E, expect)
+
+ def testNewMessages(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.newMessages(10, None)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestNewMessages)
+
+ def _cbTestNewMessages(self, ignored):
+ E = self.client.events
+ self.assertEqual(E, [['newMessages', 10, None]])
+
+ def testNewRecentMessages(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.newMessages(None, 10)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestNewRecentMessages)
+
+ def _cbTestNewRecentMessages(self, ignored):
+ E = self.client.events
+ self.assertEqual(E, [['newMessages', None, 10]])
+
+ def testNewMessagesAndRecent(self):
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def loggedIn():
+ self.server.newMessages(20, 10)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(loggedIn)).addErrback(self._ebGeneral)
+ d = defer.gatherResults([self.loopback(), d1])
+ return d.addCallback(self._cbTestNewMessagesAndRecent)
+
+ def _cbTestNewMessagesAndRecent(self, ignored):
+ E = self.client.events
+ self.assertEqual(E, [['newMessages', 20, None], ['newMessages', None, 10]])
+
+
+class ClientCapabilityTests(unittest.TestCase):
+ """
+ Tests for issuance of the CAPABILITY command and handling of its response.
+ """
+ def setUp(self):
+ """
+ Create an L{imap4.IMAP4Client} connected to a L{StringTransport}.
+ """
+ self.transport = StringTransport()
+ self.protocol = imap4.IMAP4Client()
+ self.protocol.makeConnection(self.transport)
+ self.protocol.dataReceived('* OK [IMAP4rev1]\r\n')
+
+
+ def test_simpleAtoms(self):
+ """
+ A capability response consisting only of atoms without C{'='} in them
+ should result in a dict mapping those atoms to C{None}.
+ """
+ capabilitiesResult = self.protocol.getCapabilities(useCache=False)
+ self.protocol.dataReceived('* CAPABILITY IMAP4rev1 LOGINDISABLED\r\n')
+ self.protocol.dataReceived('0001 OK Capability completed.\r\n')
+ def gotCapabilities(capabilities):
+ self.assertEqual(
+ capabilities, {'IMAP4rev1': None, 'LOGINDISABLED': None})
+ capabilitiesResult.addCallback(gotCapabilities)
+ return capabilitiesResult
+
+
+ def test_categoryAtoms(self):
+ """
+ A capability response consisting of atoms including C{'='} should have
+ those atoms split on that byte and have capabilities in the same
+ category aggregated into lists in the resulting dictionary.
+
+ (n.b. - I made up the word "category atom"; the protocol has no notion
+ of structure here, but rather allows each capability to define the
+ semantics of its entry in the capability response in a freeform manner.
+ If I had realized this earlier, the API for capabilities would look
+ different. As it is, we can hope that no one defines any crazy
+ semantics which are incompatible with this API, or try to figure out a
+ better API when someone does. -exarkun)
+ """
+ capabilitiesResult = self.protocol.getCapabilities(useCache=False)
+ self.protocol.dataReceived('* CAPABILITY IMAP4rev1 AUTH=LOGIN AUTH=PLAIN\r\n')
+ self.protocol.dataReceived('0001 OK Capability completed.\r\n')
+ def gotCapabilities(capabilities):
+ self.assertEqual(
+ capabilities, {'IMAP4rev1': None, 'AUTH': ['LOGIN', 'PLAIN']})
+ capabilitiesResult.addCallback(gotCapabilities)
+ return capabilitiesResult
+
+
+ def test_mixedAtoms(self):
+ """
+ A capability response consisting of both simple and category atoms of
+ the same type should result in a list containing C{None} as well as the
+ values for the category.
+ """
+ capabilitiesResult = self.protocol.getCapabilities(useCache=False)
+ # Exercise codepath for both orderings of =-having and =-missing
+ # capabilities.
+ self.protocol.dataReceived(
+ '* CAPABILITY IMAP4rev1 FOO FOO=BAR BAR=FOO BAR\r\n')
+ self.protocol.dataReceived('0001 OK Capability completed.\r\n')
+ def gotCapabilities(capabilities):
+ self.assertEqual(capabilities, {'IMAP4rev1': None,
+ 'FOO': [None, 'BAR'],
+ 'BAR': ['FOO', None]})
+ capabilitiesResult.addCallback(gotCapabilities)
+ return capabilitiesResult
+
+
+
+class StillSimplerClient(imap4.IMAP4Client):
+ """
+ An IMAP4 client which keeps track of unsolicited flag changes.
+ """
+ def __init__(self):
+ imap4.IMAP4Client.__init__(self)
+ self.flags = {}
+
+
+ def flagsChanged(self, newFlags):
+ self.flags.update(newFlags)
+
+
+
+class HandCraftedTestCase(IMAP4HelperMixin, unittest.TestCase):
+ def testTrailingLiteral(self):
+ transport = StringTransport()
+ c = imap4.IMAP4Client()
+ c.makeConnection(transport)
+ c.lineReceived('* OK [IMAP4rev1]')
+
+ def cbSelect(ignored):
+ d = c.fetchMessage('1')
+ c.dataReceived('* 1 FETCH (RFC822 {10}\r\n0123456789\r\n RFC822.SIZE 10)\r\n')
+ c.dataReceived('0003 OK FETCH\r\n')
+ return d
+
+ def cbLogin(ignored):
+ d = c.select('inbox')
+ c.lineReceived('0002 OK SELECT')
+ d.addCallback(cbSelect)
+ return d
+
+ d = c.login('blah', 'blah')
+ c.dataReceived('0001 OK LOGIN\r\n')
+ d.addCallback(cbLogin)
+ return d
+
+
+ def testPathelogicalScatteringOfLiterals(self):
+ self.server.checker.addUser('testuser', 'password-test')
+ transport = StringTransport()
+ self.server.makeConnection(transport)
+
+ transport.clear()
+ self.server.dataReceived("01 LOGIN {8}\r\n")
+ self.assertEqual(transport.value(), "+ Ready for 8 octets of text\r\n")
+
+ transport.clear()
+ self.server.dataReceived("testuser {13}\r\n")
+ self.assertEqual(transport.value(), "+ Ready for 13 octets of text\r\n")
+
+ transport.clear()
+ self.server.dataReceived("password-test\r\n")
+ self.assertEqual(transport.value(), "01 OK LOGIN succeeded\r\n")
+ self.assertEqual(self.server.state, 'auth')
+
+ self.server.connectionLost(error.ConnectionDone("Connection done."))
+
+
+ def test_unsolicitedResponseMixedWithSolicitedResponse(self):
+ """
+ If unsolicited data is received along with solicited data in the
+ response to a I{FETCH} command issued by L{IMAP4Client.fetchSpecific},
+ the unsolicited data is passed to the appropriate callback and not
+ included in the result with wihch the L{Deferred} returned by
+ L{IMAP4Client.fetchSpecific} fires.
+ """
+ transport = StringTransport()
+ c = StillSimplerClient()
+ c.makeConnection(transport)
+ c.lineReceived('* OK [IMAP4rev1]')
+
+ def login():
+ d = c.login('blah', 'blah')
+ c.dataReceived('0001 OK LOGIN\r\n')
+ return d
+ def select():
+ d = c.select('inbox')
+ c.lineReceived('0002 OK SELECT')
+ return d
+ def fetch():
+ d = c.fetchSpecific('1:*',
+ headerType='HEADER.FIELDS',
+ headerArgs=['SUBJECT'])
+ c.dataReceived('* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {38}\r\n')
+ c.dataReceived('Subject: Suprise for your woman...\r\n')
+ c.dataReceived('\r\n')
+ c.dataReceived(')\r\n')
+ c.dataReceived('* 1 FETCH (FLAGS (\Seen))\r\n')
+ c.dataReceived('* 2 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {75}\r\n')
+ c.dataReceived('Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n')
+ c.dataReceived('\r\n')
+ c.dataReceived(')\r\n')
+ c.dataReceived('0003 OK FETCH completed\r\n')
+ return d
+ def test(res):
+ self.assertEqual(res, {
+ 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
+ 'Subject: Suprise for your woman...\r\n\r\n']],
+ 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
+ 'Subject: What you been doing. Order your meds here . ,. handcuff madsen\r\n\r\n']]
+ })
+
+ self.assertEqual(c.flags, {1: ['\\Seen']})
+
+ return login(
+ ).addCallback(strip(select)
+ ).addCallback(strip(fetch)
+ ).addCallback(test)
+
+
+ def test_literalWithoutPrecedingWhitespace(self):
+ """
+ Literals should be recognized even when they are not preceded by
+ whitespace.
+ """
+ transport = StringTransport()
+ protocol = imap4.IMAP4Client()
+
+ protocol.makeConnection(transport)
+ protocol.lineReceived('* OK [IMAP4rev1]')
+
+ def login():
+ d = protocol.login('blah', 'blah')
+ protocol.dataReceived('0001 OK LOGIN\r\n')
+ return d
+ def select():
+ d = protocol.select('inbox')
+ protocol.lineReceived('0002 OK SELECT')
+ return d
+ def fetch():
+ d = protocol.fetchSpecific('1:*',
+ headerType='HEADER.FIELDS',
+ headerArgs=['SUBJECT'])
+ protocol.dataReceived(
+ '* 1 FETCH (BODY[HEADER.FIELDS ({7}\r\nSUBJECT)] "Hello")\r\n')
+ protocol.dataReceived('0003 OK FETCH completed\r\n')
+ return d
+ def test(result):
+ self.assertEqual(
+ result, {1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']], 'Hello']]})
+
+ d = login()
+ d.addCallback(strip(select))
+ d.addCallback(strip(fetch))
+ d.addCallback(test)
+ return d
+
+
+ def test_nonIntegerLiteralLength(self):
+ """
+ If the server sends a literal length which cannot be parsed as an
+ integer, L{IMAP4Client.lineReceived} should cause the protocol to be
+ disconnected by raising L{imap4.IllegalServerResponse}.
+ """
+ transport = StringTransport()
+ protocol = imap4.IMAP4Client()
+
+ protocol.makeConnection(transport)
+ protocol.lineReceived('* OK [IMAP4rev1]')
+
+ def login():
+ d = protocol.login('blah', 'blah')
+ protocol.dataReceived('0001 OK LOGIN\r\n')
+ return d
+ def select():
+ d = protocol.select('inbox')
+ protocol.lineReceived('0002 OK SELECT')
+ return d
+ def fetch():
+ d = protocol.fetchSpecific('1:*',
+ headerType='HEADER.FIELDS',
+ headerArgs=['SUBJECT'])
+ self.assertRaises(
+ imap4.IllegalServerResponse,
+ protocol.dataReceived,
+ '* 1 FETCH {xyz}\r\n...')
+ d = login()
+ d.addCallback(strip(select))
+ d.addCallback(strip(fetch))
+ return d
+
+
+ def test_flagsChangedInsideFetchSpecificResponse(self):
+ """
+ Any unrequested flag information received along with other requested
+ information in an untagged I{FETCH} received in response to a request
+ issued with L{IMAP4Client.fetchSpecific} is passed to the
+ C{flagsChanged} callback.
+ """
+ transport = StringTransport()
+ c = StillSimplerClient()
+ c.makeConnection(transport)
+ c.lineReceived('* OK [IMAP4rev1]')
+
+ def login():
+ d = c.login('blah', 'blah')
+ c.dataReceived('0001 OK LOGIN\r\n')
+ return d
+ def select():
+ d = c.select('inbox')
+ c.lineReceived('0002 OK SELECT')
+ return d
+ def fetch():
+ d = c.fetchSpecific('1:*',
+ headerType='HEADER.FIELDS',
+ headerArgs=['SUBJECT'])
+ # This response includes FLAGS after the requested data.
+ c.dataReceived('* 1 FETCH (BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n')
+ c.dataReceived('Subject: subject one\r\n')
+ c.dataReceived(' FLAGS (\\Recent))\r\n')
+ # And this one includes it before! Either is possible.
+ c.dataReceived('* 2 FETCH (FLAGS (\\Seen) BODY[HEADER.FIELDS ("SUBJECT")] {22}\r\n')
+ c.dataReceived('Subject: subject two\r\n')
+ c.dataReceived(')\r\n')
+ c.dataReceived('0003 OK FETCH completed\r\n')
+ return d
+
+ def test(res):
+ self.assertEqual(res, {
+ 1: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
+ 'Subject: subject one\r\n']],
+ 2: [['BODY', ['HEADER.FIELDS', ['SUBJECT']],
+ 'Subject: subject two\r\n']]
+ })
+
+ self.assertEqual(c.flags, {1: ['\\Recent'], 2: ['\\Seen']})
+
+ return login(
+ ).addCallback(strip(select)
+ ).addCallback(strip(fetch)
+ ).addCallback(test)
+
+
+ def test_flagsChangedInsideFetchMessageResponse(self):
+ """
+ Any unrequested flag information received along with other requested
+ information in an untagged I{FETCH} received in response to a request
+ issued with L{IMAP4Client.fetchMessage} is passed to the
+ C{flagsChanged} callback.
+ """
+ transport = StringTransport()
+ c = StillSimplerClient()
+ c.makeConnection(transport)
+ c.lineReceived('* OK [IMAP4rev1]')
+
+ def login():
+ d = c.login('blah', 'blah')
+ c.dataReceived('0001 OK LOGIN\r\n')
+ return d
+ def select():
+ d = c.select('inbox')
+ c.lineReceived('0002 OK SELECT')
+ return d
+ def fetch():
+ d = c.fetchMessage('1:*')
+ c.dataReceived('* 1 FETCH (RFC822 {24}\r\n')
+ c.dataReceived('Subject: first subject\r\n')
+ c.dataReceived(' FLAGS (\Seen))\r\n')
+ c.dataReceived('* 2 FETCH (FLAGS (\Recent \Seen) RFC822 {25}\r\n')
+ c.dataReceived('Subject: second subject\r\n')
+ c.dataReceived(')\r\n')
+ c.dataReceived('0003 OK FETCH completed\r\n')
+ return d
+
+ def test(res):
+ self.assertEqual(res, {
+ 1: {'RFC822': 'Subject: first subject\r\n'},
+ 2: {'RFC822': 'Subject: second subject\r\n'}})
+
+ self.assertEqual(
+ c.flags, {1: ['\\Seen'], 2: ['\\Recent', '\\Seen']})
+
+ return login(
+ ).addCallback(strip(select)
+ ).addCallback(strip(fetch)
+ ).addCallback(test)
+
+
+
+class PreauthIMAP4ClientMixin:
+ """
+ Mixin for L{unittest.TestCase} subclasses which provides a C{setUp} method
+ which creates an L{IMAP4Client} connected to a L{StringTransport} and puts
+ it into the I{authenticated} state.
+
+ @ivar transport: A L{StringTransport} to which C{client} is connected.
+ @ivar client: An L{IMAP4Client} which is connected to C{transport}.
+ """
+ clientProtocol = imap4.IMAP4Client
+
+ def setUp(self):
+ """
+ Create an IMAP4Client connected to a fake transport and in the
+ authenticated state.
+ """
+ self.transport = StringTransport()
+ self.client = self.clientProtocol()
+ self.client.makeConnection(self.transport)
+ self.client.dataReceived('* PREAUTH Hello unittest\r\n')
+
+
+ def _extractDeferredResult(self, d):
+ """
+ Synchronously extract the result of the given L{Deferred}. Fail the
+ test if that is not possible.
+ """
+ result = []
+ error = []
+ d.addCallbacks(result.append, error.append)
+ if result:
+ return result[0]
+ elif error:
+ error[0].raiseException()
+ else:
+ self.fail("Expected result not available")
+
+
+
+class SelectionTestsMixin(PreauthIMAP4ClientMixin):
+ """
+ Mixin for test cases which defines tests which apply to both I{EXAMINE} and
+ I{SELECT} support.
+ """
+ def _examineOrSelect(self):
+ """
+ Issue either an I{EXAMINE} or I{SELECT} command (depending on
+ C{self.method}), assert that the correct bytes are written to the
+ transport, and return the L{Deferred} returned by whichever method was
+ called.
+ """
+ d = getattr(self.client, self.method)('foobox')
+ self.assertEqual(
+ self.transport.value(), '0001 %s foobox\r\n' % (self.command,))
+ return d
+
+
+ def _response(self, *lines):
+ """
+ Deliver the given (unterminated) response lines to C{self.client} and
+ then deliver a tagged SELECT or EXAMINE completion line to finish the
+ SELECT or EXAMINE response.
+ """
+ for line in lines:
+ self.client.dataReceived(line + '\r\n')
+ self.client.dataReceived(
+ '0001 OK [READ-ONLY] %s completed\r\n' % (self.command,))
+
+
+ def test_exists(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{EXISTS} response, the L{Deferred} return by L{IMAP4Client.select} or
+ L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'EXISTS'} key.
+ """
+ d = self._examineOrSelect()
+ self._response('* 3 EXISTS')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {'READ-WRITE': False, 'EXISTS': 3})
+
+
+ def test_nonIntegerExists(self):
+ """
+ If the server returns a non-integer EXISTS value in its response to a
+ I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self._examineOrSelect()
+ self._response('* foo EXISTS')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_recent(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{RECENT} response, the L{Deferred} return by L{IMAP4Client.select} or
+ L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'RECENT'} key.
+ """
+ d = self._examineOrSelect()
+ self._response('* 5 RECENT')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {'READ-WRITE': False, 'RECENT': 5})
+
+
+ def test_nonIntegerRecent(self):
+ """
+ If the server returns a non-integer RECENT value in its response to a
+ I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self._examineOrSelect()
+ self._response('* foo RECENT')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_unseen(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{UNSEEN} response, the L{Deferred} returned by L{IMAP4Client.select} or
+ L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'UNSEEN'} key.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UNSEEN 8] Message 8 is first unseen')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {'READ-WRITE': False, 'UNSEEN': 8})
+
+
+ def test_nonIntegerUnseen(self):
+ """
+ If the server returns a non-integer UNSEEN value in its response to a
+ I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UNSEEN foo] Message foo is first unseen')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_uidvalidity(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{UIDVALIDITY} response, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fires with a C{dict}
+ including the value associated with the C{'UIDVALIDITY'} key.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UIDVALIDITY 12345] UIDs valid')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {'READ-WRITE': False, 'UIDVALIDITY': 12345})
+
+
+ def test_nonIntegerUIDVALIDITY(self):
+ """
+ If the server returns a non-integer UIDVALIDITY value in its response to
+ a I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UIDVALIDITY foo] UIDs valid')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_uidnext(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{UIDNEXT} response, the L{Deferred} returned by L{IMAP4Client.select}
+ or L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'UIDNEXT'} key.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UIDNEXT 4392] Predicted next UID')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {'READ-WRITE': False, 'UIDNEXT': 4392})
+
+
+ def test_nonIntegerUIDNEXT(self):
+ """
+ If the server returns a non-integer UIDNEXT value in its response to a
+ I{SELECT} or I{EXAMINE} command, the L{Deferred} returned by
+ L{IMAP4Client.select} or L{IMAP4Client.examine} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK [UIDNEXT foo] Predicted next UID')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_flags(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or
+ L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'FLAGS'} key.
+ """
+ d = self._examineOrSelect()
+ self._response(
+ '* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)')
+ self.assertEqual(
+ self._extractDeferredResult(d), {
+ 'READ-WRITE': False,
+ 'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', '\\Seen',
+ '\\Draft')})
+
+
+ def test_permanentflags(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{FLAGS} response, the L{Deferred} returned by L{IMAP4Client.select} or
+ L{IMAP4Client.examine} fires with a C{dict} including the value
+ associated with the C{'FLAGS'} key.
+ """
+ d = self._examineOrSelect()
+ self._response(
+ '* OK [PERMANENTFLAGS (\\Starred)] Just one permanent flag in '
+ 'that list up there')
+ self.assertEqual(
+ self._extractDeferredResult(d), {
+ 'READ-WRITE': False,
+ 'PERMANENTFLAGS': ('\\Starred',)})
+
+
+ def test_unrecognizedOk(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{OK} with unrecognized response code text, parsing does not fail.
+ """
+ d = self._examineOrSelect()
+ self._response(
+ '* OK [X-MADE-UP] I just made this response text up.')
+ # The value won't show up in the result. It would be okay if it did
+ # someday, perhaps. This shouldn't ever happen, though.
+ self.assertEqual(
+ self._extractDeferredResult(d), {'READ-WRITE': False})
+
+
+ def test_bareOk(self):
+ """
+ If the server response to a I{SELECT} or I{EXAMINE} command includes an
+ I{OK} with no response code text, parsing does not fail.
+ """
+ d = self._examineOrSelect()
+ self._response('* OK')
+ self.assertEqual(
+ self._extractDeferredResult(d), {'READ-WRITE': False})
+
+
+
+class IMAP4ClientExamineTests(SelectionTestsMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.examine} method.
+
+ An example of usage of the EXAMINE command from RFC 3501, section 6.3.2::
+
+ S: * 17 EXISTS
+ S: * 2 RECENT
+ S: * OK [UNSEEN 8] Message 8 is first unseen
+ S: * OK [UIDVALIDITY 3857529045] UIDs valid
+ S: * OK [UIDNEXT 4392] Predicted next UID
+ S: * FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)
+ S: * OK [PERMANENTFLAGS ()] No permanent flags permitted
+ S: A932 OK [READ-ONLY] EXAMINE completed
+ """
+ method = 'examine'
+ command = 'EXAMINE'
+
+
+
+
+class IMAP4ClientSelectTests(SelectionTestsMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.select} method.
+
+ An example of usage of the SELECT command from RFC 3501, section 6.3.1::
+
+ C: A142 SELECT INBOX
+ S: * 172 EXISTS
+ S: * 1 RECENT
+ S: * OK [UNSEEN 12] Message 12 is first unseen
+ S: * OK [UIDVALIDITY 3857529045] UIDs valid
+ S: * OK [UIDNEXT 4392] Predicted next UID
+ S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
+ S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited
+ S: A142 OK [READ-WRITE] SELECT completed
+ """
+ method = 'select'
+ command = 'SELECT'
+
+
+
+class IMAP4ClientExpungeTests(PreauthIMAP4ClientMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.expunge} method.
+
+ An example of usage of the EXPUNGE command from RFC 3501, section 6.4.3::
+
+ C: A202 EXPUNGE
+ S: * 3 EXPUNGE
+ S: * 3 EXPUNGE
+ S: * 5 EXPUNGE
+ S: * 8 EXPUNGE
+ S: A202 OK EXPUNGE completed
+ """
+ def _expunge(self):
+ d = self.client.expunge()
+ self.assertEqual(self.transport.value(), '0001 EXPUNGE\r\n')
+ self.transport.clear()
+ return d
+
+
+ def _response(self, sequenceNumbers):
+ for number in sequenceNumbers:
+ self.client.lineReceived('* %s EXPUNGE' % (number,))
+ self.client.lineReceived('0001 OK EXPUNGE COMPLETED')
+
+
+ def test_expunge(self):
+ """
+ L{IMAP4Client.expunge} sends the I{EXPUNGE} command and returns a
+ L{Deferred} which fires with a C{list} of message sequence numbers
+ given by the server's response.
+ """
+ d = self._expunge()
+ self._response([3, 3, 5, 8])
+ self.assertEqual(self._extractDeferredResult(d), [3, 3, 5, 8])
+
+
+ def test_nonIntegerExpunged(self):
+ """
+ If the server responds with a non-integer where a message sequence
+ number is expected, the L{Deferred} returned by L{IMAP4Client.expunge}
+ fails with L{IllegalServerResponse}.
+ """
+ d = self._expunge()
+ self._response([3, 3, 'foo', 8])
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+
+class IMAP4ClientSearchTests(PreauthIMAP4ClientMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.search} method.
+
+ An example of usage of the SEARCH command from RFC 3501, section 6.4.4::
+
+ C: A282 SEARCH FLAGGED SINCE 1-Feb-1994 NOT FROM "Smith"
+ S: * SEARCH 2 84 882
+ S: A282 OK SEARCH completed
+ C: A283 SEARCH TEXT "string not in mailbox"
+ S: * SEARCH
+ S: A283 OK SEARCH completed
+ C: A284 SEARCH CHARSET UTF-8 TEXT {6}
+ C: XXXXXX
+ S: * SEARCH 43
+ S: A284 OK SEARCH completed
+ """
+ def _search(self):
+ d = self.client.search(imap4.Query(text="ABCDEF"))
+ self.assertEqual(
+ self.transport.value(), '0001 SEARCH (TEXT "ABCDEF")\r\n')
+ return d
+
+
+ def _response(self, messageNumbers):
+ self.client.lineReceived(
+ "* SEARCH " + " ".join(map(str, messageNumbers)))
+ self.client.lineReceived("0001 OK SEARCH completed")
+
+
+ def test_search(self):
+ """
+ L{IMAP4Client.search} sends the I{SEARCH} command and returns a
+ L{Deferred} which fires with a C{list} of message sequence numbers
+ given by the server's response.
+ """
+ d = self._search()
+ self._response([2, 5, 10])
+ self.assertEqual(self._extractDeferredResult(d), [2, 5, 10])
+
+
+ def test_nonIntegerFound(self):
+ """
+ If the server responds with a non-integer where a message sequence
+ number is expected, the L{Deferred} returned by L{IMAP4Client.search}
+ fails with L{IllegalServerResponse}.
+ """
+ d = self._search()
+ self._response([2, "foo", 10])
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+
+class IMAP4ClientFetchTests(PreauthIMAP4ClientMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.fetch} method.
+
+ See RFC 3501, section 6.4.5.
+ """
+ def test_fetchUID(self):
+ """
+ L{IMAP4Client.fetchUID} sends the I{FETCH UID} command and returns a
+ L{Deferred} which fires with a C{dict} mapping message sequence numbers
+ to C{dict}s mapping C{'UID'} to that message's I{UID} in the server's
+ response.
+ """
+ d = self.client.fetchUID('1:7')
+ self.assertEqual(self.transport.value(), '0001 FETCH 1:7 (UID)\r\n')
+ self.client.lineReceived('* 2 FETCH (UID 22)')
+ self.client.lineReceived('* 3 FETCH (UID 23)')
+ self.client.lineReceived('* 4 FETCH (UID 24)')
+ self.client.lineReceived('* 5 FETCH (UID 25)')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d), {
+ 2: {'UID': '22'},
+ 3: {'UID': '23'},
+ 4: {'UID': '24'},
+ 5: {'UID': '25'}})
+
+
+ def test_fetchUIDNonIntegerFound(self):
+ """
+ If the server responds with a non-integer where a message sequence
+ number is expected, the L{Deferred} returned by L{IMAP4Client.fetchUID}
+ fails with L{IllegalServerResponse}.
+ """
+ d = self.client.fetchUID('1')
+ self.assertEqual(self.transport.value(), '0001 FETCH 1 (UID)\r\n')
+ self.client.lineReceived('* foo FETCH (UID 22)')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_incompleteFetchUIDResponse(self):
+ """
+ If the server responds with an incomplete I{FETCH} response line, the
+ L{Deferred} returned by L{IMAP4Client.fetchUID} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self.client.fetchUID('1:7')
+ self.assertEqual(self.transport.value(), '0001 FETCH 1:7 (UID)\r\n')
+ self.client.lineReceived('* 2 FETCH (UID 22)')
+ self.client.lineReceived('* 3 FETCH (UID)')
+ self.client.lineReceived('* 4 FETCH (UID 24)')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_fetchBody(self):
+ """
+ L{IMAP4Client.fetchBody} sends the I{FETCH BODY} command and returns a
+ L{Deferred} which fires with a C{dict} mapping message sequence numbers
+ to C{dict}s mapping C{'RFC822.TEXT'} to that message's body as given in
+ the server's response.
+ """
+ d = self.client.fetchBody('3')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 3 (RFC822.TEXT)\r\n')
+ self.client.lineReceived('* 3 FETCH (RFC822.TEXT "Message text")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {3: {'RFC822.TEXT': 'Message text'}})
+
+
+ def test_fetchSpecific(self):
+ """
+ L{IMAP4Client.fetchSpecific} sends the I{BODY[]} command if no
+ parameters beyond the message set to retrieve are given. It returns a
+ L{Deferred} which fires with a C{dict} mapping message sequence numbers
+ to C{list}s of corresponding message data given by the server's
+ response.
+ """
+ d = self.client.fetchSpecific('7')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 7 BODY[]\r\n')
+ self.client.lineReceived('* 7 FETCH (BODY[] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d), {7: [['BODY', [], "Some body"]]})
+
+
+ def test_fetchSpecificPeek(self):
+ """
+ L{IMAP4Client.fetchSpecific} issues a I{BODY.PEEK[]} command if passed
+ C{True} for the C{peek} parameter.
+ """
+ d = self.client.fetchSpecific('6', peek=True)
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 6 BODY.PEEK[]\r\n')
+ # BODY.PEEK responses are just BODY
+ self.client.lineReceived('* 6 FETCH (BODY[] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d), {6: [['BODY', [], "Some body"]]})
+
+
+ def test_fetchSpecificNumbered(self):
+ """
+ L{IMAP4Client.fetchSpecific}, when passed a sequence for for
+ C{headerNumber}, sends the I{BODY[N.M]} command. It returns a
+ L{Deferred} which fires with a C{dict} mapping message sequence numbers
+ to C{list}s of corresponding message data given by the server's
+ response.
+ """
+ d = self.client.fetchSpecific('7', headerNumber=(1, 2, 3))
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 7 BODY[1.2.3]\r\n')
+ self.client.lineReceived('* 7 FETCH (BODY[1.2.3] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {7: [['BODY', ['1.2.3'], "Some body"]]})
+
+
+ def test_fetchSpecificText(self):
+ """
+ L{IMAP4Client.fetchSpecific}, when passed C{'TEXT'} for C{headerType},
+ sends the I{BODY[TEXT]} command. It returns a L{Deferred} which fires
+ with a C{dict} mapping message sequence numbers to C{list}s of
+ corresponding message data given by the server's response.
+ """
+ d = self.client.fetchSpecific('8', headerType='TEXT')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n')
+ self.client.lineReceived('* 8 FETCH (BODY[TEXT] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {8: [['BODY', ['TEXT'], "Some body"]]})
+
+
+ def test_fetchSpecificNumberedText(self):
+ """
+ If passed a value for the C{headerNumber} parameter and C{'TEXT'} for
+ the C{headerType} parameter, L{IMAP4Client.fetchSpecific} sends a
+ I{BODY[number.TEXT]} request and returns a L{Deferred} which fires with
+ a C{dict} mapping message sequence numbers to C{list}s of message data
+ given by the server's response.
+ """
+ d = self.client.fetchSpecific('4', headerType='TEXT', headerNumber=7)
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 4 BODY[7.TEXT]\r\n')
+ self.client.lineReceived('* 4 FETCH (BODY[7.TEXT] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {4: [['BODY', ['7.TEXT'], "Some body"]]})
+
+
+ def test_incompleteFetchSpecificTextResponse(self):
+ """
+ If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line
+ which is truncated after the I{BODY[TEXT]} tokens, the L{Deferred}
+ returned by L{IMAP4Client.fetchUID} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self.client.fetchSpecific('8', headerType='TEXT')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n')
+ self.client.lineReceived('* 8 FETCH (BODY[TEXT])')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_fetchSpecificMIME(self):
+ """
+ L{IMAP4Client.fetchSpecific}, when passed C{'MIME'} for C{headerType},
+ sends the I{BODY[MIME]} command. It returns a L{Deferred} which fires
+ with a C{dict} mapping message sequence numbers to C{list}s of
+ corresponding message data given by the server's response.
+ """
+ d = self.client.fetchSpecific('8', headerType='MIME')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 8 BODY[MIME]\r\n')
+ self.client.lineReceived('* 8 FETCH (BODY[MIME] "Some body")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {8: [['BODY', ['MIME'], "Some body"]]})
+
+
+ def test_fetchSpecificPartial(self):
+ """
+ L{IMAP4Client.fetchSpecific}, when passed C{offset} and C{length},
+ sends a partial content request (like I{BODY[TEXT]<offset.length>}).
+ It returns a L{Deferred} which fires with a C{dict} mapping message
+ sequence numbers to C{list}s of corresponding message data given by the
+ server's response.
+ """
+ d = self.client.fetchSpecific(
+ '9', headerType='TEXT', offset=17, length=3)
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 9 BODY[TEXT]<17.3>\r\n')
+ self.client.lineReceived('* 9 FETCH (BODY[TEXT]<17> "foo")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {9: [['BODY', ['TEXT'], '<17>', 'foo']]})
+
+
+ def test_incompleteFetchSpecificPartialResponse(self):
+ """
+ If the server responds to a I{BODY[TEXT]} request with a I{FETCH} line
+ which is truncated after the I{BODY[TEXT]<offset>} tokens, the
+ L{Deferred} returned by L{IMAP4Client.fetchUID} fails with
+ L{IllegalServerResponse}.
+ """
+ d = self.client.fetchSpecific('8', headerType='TEXT')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 8 BODY[TEXT]\r\n')
+ self.client.lineReceived('* 8 FETCH (BODY[TEXT]<17>)')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertRaises(
+ imap4.IllegalServerResponse, self._extractDeferredResult, d)
+
+
+ def test_fetchSpecificHTML(self):
+ """
+ If the body of a message begins with I{<} and ends with I{>} (as,
+ for example, HTML bodies typically will), this is still interpreted
+ as the body by L{IMAP4Client.fetchSpecific} (and particularly, not
+ as a length indicator for a response to a request for a partial
+ body).
+ """
+ d = self.client.fetchSpecific('7')
+ self.assertEqual(
+ self.transport.value(), '0001 FETCH 7 BODY[]\r\n')
+ self.client.lineReceived('* 7 FETCH (BODY[] "<html>test</html>")')
+ self.client.lineReceived('0001 OK FETCH completed')
+ self.assertEqual(
+ self._extractDeferredResult(d), {7: [['BODY', [], "<html>test</html>"]]})
+
+
+
+class IMAP4ClientStoreTests(PreauthIMAP4ClientMixin, unittest.TestCase):
+ """
+ Tests for the L{IMAP4Client.setFlags}, L{IMAP4Client.addFlags}, and
+ L{IMAP4Client.removeFlags} methods.
+
+ An example of usage of the STORE command, in terms of which these three
+ methods are implemented, from RFC 3501, section 6.4.6::
+
+ C: A003 STORE 2:4 +FLAGS (\Deleted)
+ S: * 2 FETCH (FLAGS (\Deleted \Seen))
+ S: * 3 FETCH (FLAGS (\Deleted))
+ S: * 4 FETCH (FLAGS (\Deleted \Flagged \Seen))
+ S: A003 OK STORE completed
+ """
+ clientProtocol = StillSimplerClient
+
+ def _flagsTest(self, method, item):
+ """
+ Test a non-silent flag modifying method. Call the method, assert that
+ the correct bytes are sent, deliver a I{FETCH} response, and assert
+ that the result of the Deferred returned by the method is correct.
+
+ @param method: The name of the method to test.
+ @param item: The data item which is expected to be specified.
+ """
+ d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), False)
+ self.assertEqual(
+ self.transport.value(),
+ '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n')
+ self.client.lineReceived('* 3 FETCH (FLAGS (\\Read \\Seen))')
+ self.client.lineReceived('0001 OK STORE completed')
+ self.assertEqual(
+ self._extractDeferredResult(d),
+ {3: {'FLAGS': ['\\Read', '\\Seen']}})
+
+
+ def _flagsSilentlyTest(self, method, item):
+ """
+ Test a silent flag modifying method. Call the method, assert that the
+ correct bytes are sent, deliver an I{OK} response, and assert that the
+ result of the Deferred returned by the method is correct.
+
+ @param method: The name of the method to test.
+ @param item: The data item which is expected to be specified.
+ """
+ d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), True)
+ self.assertEqual(
+ self.transport.value(),
+ '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n')
+ self.client.lineReceived('0001 OK STORE completed')
+ self.assertEqual(self._extractDeferredResult(d), {})
+
+
+ def _flagsSilentlyWithUnsolicitedDataTest(self, method, item):
+ """
+ Test unsolicited data received in response to a silent flag modifying
+ method. Call the method, assert that the correct bytes are sent,
+ deliver the unsolicited I{FETCH} response, and assert that the result
+ of the Deferred returned by the method is correct.
+
+ @param method: The name of the method to test.
+ @param item: The data item which is expected to be specified.
+ """
+ d = getattr(self.client, method)('3', ('\\Read', '\\Seen'), True)
+ self.assertEqual(
+ self.transport.value(),
+ '0001 STORE 3 ' + item + ' (\\Read \\Seen)\r\n')
+ self.client.lineReceived('* 2 FETCH (FLAGS (\\Read \\Seen))')
+ self.client.lineReceived('0001 OK STORE completed')
+ self.assertEqual(self._extractDeferredResult(d), {})
+ self.assertEqual(self.client.flags, {2: ['\\Read', '\\Seen']})
+
+
+ def test_setFlags(self):
+ """
+ When passed a C{False} value for the C{silent} parameter,
+ L{IMAP4Client.setFlags} sends the I{STORE} command with a I{FLAGS} data
+ item and returns a L{Deferred} which fires with a C{dict} mapping
+ message sequence numbers to C{dict}s mapping C{'FLAGS'} to the new
+ flags of those messages.
+ """
+ self._flagsTest('setFlags', 'FLAGS')
+
+
+ def test_setFlagsSilently(self):
+ """
+ When passed a C{True} value for the C{silent} parameter,
+ L{IMAP4Client.setFlags} sends the I{STORE} command with a
+ I{FLAGS.SILENT} data item and returns a L{Deferred} which fires with an
+ empty dictionary.
+ """
+ self._flagsSilentlyTest('setFlags', 'FLAGS.SILENT')
+
+
+ def test_setFlagsSilentlyWithUnsolicitedData(self):
+ """
+ If unsolicited flag data is received in response to a I{STORE}
+ I{FLAGS.SILENT} request, that data is passed to the C{flagsChanged}
+ callback.
+ """
+ self._flagsSilentlyWithUnsolicitedDataTest('setFlags', 'FLAGS.SILENT')
+
+
+ def test_addFlags(self):
+ """
+ L{IMAP4Client.addFlags} is like L{IMAP4Client.setFlags}, but sends
+ I{+FLAGS} instead of I{FLAGS}.
+ """
+ self._flagsTest('addFlags', '+FLAGS')
+
+
+ def test_addFlagsSilently(self):
+ """
+ L{IMAP4Client.addFlags} with a C{True} value for C{silent} behaves like
+ L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it
+ sends I{+FLAGS.SILENT} instead of I{FLAGS.SILENT}.
+ """
+ self._flagsSilentlyTest('addFlags', '+FLAGS.SILENT')
+
+
+ def test_addFlagsSilentlyWithUnsolicitedData(self):
+ """
+ L{IMAP4Client.addFlags} behaves like L{IMAP4Client.setFlags} when used
+ in silent mode and unsolicited data is received.
+ """
+ self._flagsSilentlyWithUnsolicitedDataTest('addFlags', '+FLAGS.SILENT')
+
+
+ def test_removeFlags(self):
+ """
+ L{IMAP4Client.removeFlags} is like L{IMAP4Client.setFlags}, but sends
+ I{-FLAGS} instead of I{FLAGS}.
+ """
+ self._flagsTest('removeFlags', '-FLAGS')
+
+
+ def test_removeFlagsSilently(self):
+ """
+ L{IMAP4Client.removeFlags} with a C{True} value for C{silent} behaves
+ like L{IMAP4Client.setFlags} with a C{True} value for C{silent}, but it
+ sends I{-FLAGS.SILENT} instead of I{FLAGS.SILENT}.
+ """
+ self._flagsSilentlyTest('removeFlags', '-FLAGS.SILENT')
+
+
+ def test_removeFlagsSilentlyWithUnsolicitedData(self):
+ """
+ L{IMAP4Client.removeFlags} behaves like L{IMAP4Client.setFlags} when
+ used in silent mode and unsolicited data is received.
+ """
+ self._flagsSilentlyWithUnsolicitedDataTest('removeFlags', '-FLAGS.SILENT')
+
+
+
+class FakeyServer(imap4.IMAP4Server):
+ state = 'select'
+ timeout = None
+
+ def sendServerGreeting(self):
+ pass
+
+class FakeyMessage(util.FancyStrMixin):
+ implements(imap4.IMessage)
+
+ showAttributes = ('headers', 'flags', 'date', 'body', 'uid')
+
+ def __init__(self, headers, flags, date, body, uid, subpart):
+ self.headers = headers
+ self.flags = flags
+ self.body = StringIO(body)
+ self.size = len(body)
+ self.date = date
+ self.uid = uid
+ self.subpart = subpart
+
+ def getHeaders(self, negate, *names):
+ self.got_headers = negate, names
+ return self.headers
+
+ def getFlags(self):
+ return self.flags
+
+ def getInternalDate(self):
+ return self.date
+
+ def getBodyFile(self):
+ return self.body
+
+ def getSize(self):
+ return self.size
+
+ def getUID(self):
+ return self.uid
+
+ def isMultipart(self):
+ return self.subpart is not None
+
+ def getSubPart(self, part):
+ self.got_subpart = part
+ return self.subpart[part]
+
+class NewStoreTestCase(unittest.TestCase, IMAP4HelperMixin):
+ result = None
+ storeArgs = None
+
+ def setUp(self):
+ self.received_messages = self.received_uid = None
+
+ self.server = imap4.IMAP4Server()
+ self.server.state = 'select'
+ self.server.mbox = self
+ self.connected = defer.Deferred()
+ self.client = SimpleClient(self.connected)
+
+ def addListener(self, x):
+ pass
+ def removeListener(self, x):
+ pass
+
+ def store(self, *args, **kw):
+ self.storeArgs = args, kw
+ return self.response
+
+ def _storeWork(self):
+ def connected():
+ return self.function(self.messages, self.flags, self.silent, self.uid)
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(strip(connected)
+ ).addCallback(result
+ ).addCallback(self._cbStopClient
+ ).addErrback(self._ebGeneral)
+
+ def check(ignored):
+ self.assertEqual(self.result, self.expected)
+ self.assertEqual(self.storeArgs, self.expectedArgs)
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(check)
+ return d
+
+ def testSetFlags(self, uid=0):
+ self.function = self.client.setFlags
+ self.messages = '1,5,9'
+ self.flags = ['\\A', '\\B', 'C']
+ self.silent = False
+ self.uid = uid
+ self.response = {
+ 1: ['\\A', '\\B', 'C'],
+ 5: ['\\A', '\\B', 'C'],
+ 9: ['\\A', '\\B', 'C'],
+ }
+ self.expected = {
+ 1: {'FLAGS': ['\\A', '\\B', 'C']},
+ 5: {'FLAGS': ['\\A', '\\B', 'C']},
+ 9: {'FLAGS': ['\\A', '\\B', 'C']},
+ }
+ msg = imap4.MessageSet()
+ msg.add(1)
+ msg.add(5)
+ msg.add(9)
+ self.expectedArgs = ((msg, ['\\A', '\\B', 'C'], 0), {'uid': 0})
+ return self._storeWork()
+
+
+class NewFetchTestCase(unittest.TestCase, IMAP4HelperMixin):
+ def setUp(self):
+ self.received_messages = self.received_uid = None
+ self.result = None
+
+ self.server = imap4.IMAP4Server()
+ self.server.state = 'select'
+ self.server.mbox = self
+ self.connected = defer.Deferred()
+ self.client = SimpleClient(self.connected)
+
+ def addListener(self, x):
+ pass
+ def removeListener(self, x):
+ pass
+
+ def fetch(self, messages, uid):
+ self.received_messages = messages
+ self.received_uid = uid
+ return iter(zip(range(len(self.msgObjs)), self.msgObjs))
+
+ def _fetchWork(self, uid):
+ if uid:
+ for (i, msg) in zip(range(len(self.msgObjs)), self.msgObjs):
+ self.expected[i]['UID'] = str(msg.getUID())
+
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(lambda _: self.function(self.messages, uid)
+ ).addCallback(result
+ ).addCallback(self._cbStopClient
+ ).addErrback(self._ebGeneral)
+
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(lambda x : self.assertEqual(self.result, self.expected))
+ return d
+
+ def testFetchUID(self):
+ self.function = lambda m, u: self.client.fetchUID(m)
+
+ self.messages = '7'
+ self.msgObjs = [
+ FakeyMessage({}, (), '', '', 12345, None),
+ FakeyMessage({}, (), '', '', 999, None),
+ FakeyMessage({}, (), '', '', 10101, None),
+ ]
+ self.expected = {
+ 0: {'UID': '12345'},
+ 1: {'UID': '999'},
+ 2: {'UID': '10101'},
+ }
+ return self._fetchWork(0)
+
+ def testFetchFlags(self, uid=0):
+ self.function = self.client.fetchFlags
+ self.messages = '9'
+ self.msgObjs = [
+ FakeyMessage({}, ['FlagA', 'FlagB', '\\FlagC'], '', '', 54321, None),
+ FakeyMessage({}, ['\\FlagC', 'FlagA', 'FlagB'], '', '', 12345, None),
+ ]
+ self.expected = {
+ 0: {'FLAGS': ['FlagA', 'FlagB', '\\FlagC']},
+ 1: {'FLAGS': ['\\FlagC', 'FlagA', 'FlagB']},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchFlagsUID(self):
+ return self.testFetchFlags(1)
+
+ def testFetchInternalDate(self, uid=0):
+ self.function = self.client.fetchInternalDate
+ self.messages = '13'
+ self.msgObjs = [
+ FakeyMessage({}, (), 'Fri, 02 Nov 2003 21:25:10 GMT', '', 23232, None),
+ FakeyMessage({}, (), 'Thu, 29 Dec 2013 11:31:52 EST', '', 101, None),
+ FakeyMessage({}, (), 'Mon, 10 Mar 1992 02:44:30 CST', '', 202, None),
+ FakeyMessage({}, (), 'Sat, 11 Jan 2000 14:40:24 PST', '', 303, None),
+ ]
+ self.expected = {
+ 0: {'INTERNALDATE': '02-Nov-2003 21:25:10 +0000'},
+ 1: {'INTERNALDATE': '29-Dec-2013 11:31:52 -0500'},
+ 2: {'INTERNALDATE': '10-Mar-1992 02:44:30 -0600'},
+ 3: {'INTERNALDATE': '11-Jan-2000 14:40:24 -0800'},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchInternalDateUID(self):
+ return self.testFetchInternalDate(1)
+
+
+ def test_fetchInternalDateLocaleIndependent(self):
+ """
+ The month name in the date is locale independent.
+ """
+ # Fake that we're in a language where December is not Dec
+ currentLocale = locale.setlocale(locale.LC_ALL, None)
+ locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
+ self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale)
+ return self.testFetchInternalDate(1)
+
+ # if alternate locale is not available, the previous test will be skipped,
+ # please install this locale for it to run. Avoid using locale.getlocale to
+ # learn the current locale; its values don't round-trip well on all
+ # platforms. Fortunately setlocale returns a value which does round-trip
+ # well.
+ currentLocale = locale.setlocale(locale.LC_ALL, None)
+ try:
+ locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
+ except locale.Error:
+ test_fetchInternalDateLocaleIndependent.skip = (
+ "The es_AR.UTF8 locale is not installed.")
+ else:
+ locale.setlocale(locale.LC_ALL, currentLocale)
+
+
+ def testFetchEnvelope(self, uid=0):
+ self.function = self.client.fetchEnvelope
+ self.messages = '15'
+ self.msgObjs = [
+ FakeyMessage({
+ 'from': 'user@domain', 'to': 'resu@domain',
+ 'date': 'thursday', 'subject': 'it is a message',
+ 'message-id': 'id-id-id-yayaya'}, (), '', '', 65656,
+ None),
+ ]
+ self.expected = {
+ 0: {'ENVELOPE':
+ ['thursday', 'it is a message',
+ [[None, None, 'user', 'domain']],
+ [[None, None, 'user', 'domain']],
+ [[None, None, 'user', 'domain']],
+ [[None, None, 'resu', 'domain']],
+ None, None, None, 'id-id-id-yayaya']
+ }
+ }
+ return self._fetchWork(uid)
+
+ def testFetchEnvelopeUID(self):
+ return self.testFetchEnvelope(1)
+
+ def testFetchBodyStructure(self, uid=0):
+ self.function = self.client.fetchBodyStructure
+ self.messages = '3:9,10:*'
+ self.msgObjs = [FakeyMessage({
+ 'content-type': 'text/plain; name=thing; key="value"',
+ 'content-id': 'this-is-the-content-id',
+ 'content-description': 'describing-the-content-goes-here!',
+ 'content-transfer-encoding': '8BIT',
+ }, (), '', 'Body\nText\nGoes\nHere\n', 919293, None)]
+ self.expected = {0: {'BODYSTRUCTURE': [
+ 'text', 'plain', [['name', 'thing'], ['key', 'value']],
+ 'this-is-the-content-id', 'describing-the-content-goes-here!',
+ '8BIT', '20', '4', None, None, None]}}
+ return self._fetchWork(uid)
+
+ def testFetchBodyStructureUID(self):
+ return self.testFetchBodyStructure(1)
+
+ def testFetchSimplifiedBody(self, uid=0):
+ self.function = self.client.fetchSimplifiedBody
+ self.messages = '21'
+ self.msgObjs = [FakeyMessage({}, (), '', 'Yea whatever', 91825,
+ [FakeyMessage({'content-type': 'image/jpg'}, (), '',
+ 'Body Body Body', None, None
+ )]
+ )]
+ self.expected = {0:
+ {'BODY':
+ [None, None, [], None, None, None,
+ '12'
+ ]
+ }
+ }
+
+ return self._fetchWork(uid)
+
+ def testFetchSimplifiedBodyUID(self):
+ return self.testFetchSimplifiedBody(1)
+
+ def testFetchSimplifiedBodyText(self, uid=0):
+ self.function = self.client.fetchSimplifiedBody
+ self.messages = '21'
+ self.msgObjs = [FakeyMessage({'content-type': 'text/plain'},
+ (), '', 'Yea whatever', 91825, None)]
+ self.expected = {0:
+ {'BODY':
+ ['text', 'plain', [], None, None, None,
+ '12', '1'
+ ]
+ }
+ }
+
+ return self._fetchWork(uid)
+
+ def testFetchSimplifiedBodyTextUID(self):
+ return self.testFetchSimplifiedBodyText(1)
+
+ def testFetchSimplifiedBodyRFC822(self, uid=0):
+ self.function = self.client.fetchSimplifiedBody
+ self.messages = '21'
+ self.msgObjs = [FakeyMessage({'content-type': 'message/rfc822'},
+ (), '', 'Yea whatever', 91825,
+ [FakeyMessage({'content-type': 'image/jpg'}, (), '',
+ 'Body Body Body', None, None
+ )]
+ )]
+ self.expected = {0:
+ {'BODY':
+ ['message', 'rfc822', [], None, None, None,
+ '12', [None, None, [[None, None, None]],
+ [[None, None, None]], None, None, None,
+ None, None, None], ['image', 'jpg', [],
+ None, None, None, '14'], '1'
+ ]
+ }
+ }
+
+ return self._fetchWork(uid)
+
+ def testFetchSimplifiedBodyRFC822UID(self):
+ return self.testFetchSimplifiedBodyRFC822(1)
+
+ def testFetchMessage(self, uid=0):
+ self.function = self.client.fetchMessage
+ self.messages = '1,3,7,10101'
+ self.msgObjs = [
+ FakeyMessage({'Header': 'Value'}, (), '', 'BODY TEXT\r\n', 91, None),
+ ]
+ self.expected = {
+ 0: {'RFC822': 'Header: Value\r\n\r\nBODY TEXT\r\n'}
+ }
+ return self._fetchWork(uid)
+
+ def testFetchMessageUID(self):
+ return self.testFetchMessage(1)
+
+ def testFetchHeaders(self, uid=0):
+ self.function = self.client.fetchHeaders
+ self.messages = '9,6,2'
+ self.msgObjs = [
+ FakeyMessage({'H1': 'V1', 'H2': 'V2'}, (), '', '', 99, None),
+ ]
+ self.expected = {
+ 0: {'RFC822.HEADER': imap4._formatHeaders({'H1': 'V1', 'H2': 'V2'})},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchHeadersUID(self):
+ return self.testFetchHeaders(1)
+
+ def testFetchBody(self, uid=0):
+ self.function = self.client.fetchBody
+ self.messages = '1,2,3,4,5,6,7'
+ self.msgObjs = [
+ FakeyMessage({'Header': 'Value'}, (), '', 'Body goes here\r\n', 171, None),
+ ]
+ self.expected = {
+ 0: {'RFC822.TEXT': 'Body goes here\r\n'},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchBodyUID(self):
+ return self.testFetchBody(1)
+
+ def testFetchBodyParts(self):
+ """
+ Test the server's handling of requests for specific body sections.
+ """
+ self.function = self.client.fetchSpecific
+ self.messages = '1'
+ outerBody = ''
+ innerBody1 = 'Contained body message text. Squarge.'
+ innerBody2 = 'Secondary <i>message</i> text of squarge body.'
+ headers = util.OrderedDict()
+ headers['from'] = 'sender@host'
+ headers['to'] = 'recipient@domain'
+ headers['subject'] = 'booga booga boo'
+ headers['content-type'] = 'multipart/alternative; boundary="xyz"'
+ innerHeaders = util.OrderedDict()
+ innerHeaders['subject'] = 'this is subject text'
+ innerHeaders['content-type'] = 'text/plain'
+ innerHeaders2 = util.OrderedDict()
+ innerHeaders2['subject'] = '<b>this is subject</b>'
+ innerHeaders2['content-type'] = 'text/html'
+ self.msgObjs = [FakeyMessage(
+ headers, (), None, outerBody, 123,
+ [FakeyMessage(innerHeaders, (), None, innerBody1, None, None),
+ FakeyMessage(innerHeaders2, (), None, innerBody2, None, None)])]
+ self.expected = {
+ 0: [['BODY', ['1'], 'Contained body message text. Squarge.']]}
+
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(
+ lambda _: self.function(self.messages, headerNumber=1))
+ self.connected.addCallback(result)
+ self.connected.addCallback(self._cbStopClient)
+ self.connected.addErrback(self._ebGeneral)
+
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(lambda ign: self.assertEqual(self.result, self.expected))
+ return d
+
+
+ def test_fetchBodyPartOfNonMultipart(self):
+ """
+ Single-part messages have an implicit first part which clients
+ should be able to retrieve explicitly. Test that a client
+ requesting part 1 of a text/plain message receives the body of the
+ text/plain part.
+ """
+ self.function = self.client.fetchSpecific
+ self.messages = '1'
+ parts = [1]
+ outerBody = 'DA body'
+ headers = util.OrderedDict()
+ headers['from'] = 'sender@host'
+ headers['to'] = 'recipient@domain'
+ headers['subject'] = 'booga booga boo'
+ headers['content-type'] = 'text/plain'
+ self.msgObjs = [FakeyMessage(
+ headers, (), None, outerBody, 123, None)]
+
+ self.expected = {0: [['BODY', ['1'], 'DA body']]}
+
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(
+ lambda _: self.function(self.messages, headerNumber=parts))
+ self.connected.addCallback(result)
+ self.connected.addCallback(self._cbStopClient)
+ self.connected.addErrback(self._ebGeneral)
+
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(lambda ign: self.assertEqual(self.result, self.expected))
+ return d
+
+
+ def testFetchSize(self, uid=0):
+ self.function = self.client.fetchSize
+ self.messages = '1:100,2:*'
+ self.msgObjs = [
+ FakeyMessage({}, (), '', 'x' * 20, 123, None),
+ ]
+ self.expected = {
+ 0: {'RFC822.SIZE': '20'},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchSizeUID(self):
+ return self.testFetchSize(1)
+
+ def testFetchFull(self, uid=0):
+ self.function = self.client.fetchFull
+ self.messages = '1,3'
+ self.msgObjs = [
+ FakeyMessage({}, ('\\XYZ', '\\YZX', 'Abc'),
+ 'Sun, 25 Jul 2010 06:20:30 -0400 (EDT)',
+ 'xyz' * 2, 654, None),
+ FakeyMessage({}, ('\\One', '\\Two', 'Three'),
+ 'Mon, 14 Apr 2003 19:43:44 -0400',
+ 'abc' * 4, 555, None),
+ ]
+ self.expected = {
+ 0: {'FLAGS': ['\\XYZ', '\\YZX', 'Abc'],
+ 'INTERNALDATE': '25-Jul-2010 06:20:30 -0400',
+ 'RFC822.SIZE': '6',
+ 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
+ 'BODY': [None, None, [], None, None, None, '6']},
+ 1: {'FLAGS': ['\\One', '\\Two', 'Three'],
+ 'INTERNALDATE': '14-Apr-2003 19:43:44 -0400',
+ 'RFC822.SIZE': '12',
+ 'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
+ 'BODY': [None, None, [], None, None, None, '12']},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchFullUID(self):
+ return self.testFetchFull(1)
+
+ def testFetchAll(self, uid=0):
+ self.function = self.client.fetchAll
+ self.messages = '1,2:3'
+ self.msgObjs = [
+ FakeyMessage({}, (), 'Mon, 14 Apr 2003 19:43:44 +0400',
+ 'Lalala', 10101, None),
+ FakeyMessage({}, (), 'Tue, 15 Apr 2003 19:43:44 +0200',
+ 'Alalal', 20202, None),
+ ]
+ self.expected = {
+ 0: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
+ 'RFC822.SIZE': '6',
+ 'INTERNALDATE': '14-Apr-2003 19:43:44 +0400',
+ 'FLAGS': []},
+ 1: {'ENVELOPE': [None, None, [[None, None, None]], [[None, None, None]], None, None, None, None, None, None],
+ 'RFC822.SIZE': '6',
+ 'INTERNALDATE': '15-Apr-2003 19:43:44 +0200',
+ 'FLAGS': []},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchAllUID(self):
+ return self.testFetchAll(1)
+
+ def testFetchFast(self, uid=0):
+ self.function = self.client.fetchFast
+ self.messages = '1'
+ self.msgObjs = [
+ FakeyMessage({}, ('\\X',), '19 Mar 2003 19:22:21 -0500', '', 9, None),
+ ]
+ self.expected = {
+ 0: {'FLAGS': ['\\X'],
+ 'INTERNALDATE': '19-Mar-2003 19:22:21 -0500',
+ 'RFC822.SIZE': '0'},
+ }
+ return self._fetchWork(uid)
+
+ def testFetchFastUID(self):
+ return self.testFetchFast(1)
+
+
+
+class DefaultSearchTestCase(IMAP4HelperMixin, unittest.TestCase):
+ """
+ Test the behavior of the server's SEARCH implementation, particularly in
+ the face of unhandled search terms.
+ """
+ def setUp(self):
+ self.server = imap4.IMAP4Server()
+ self.server.state = 'select'
+ self.server.mbox = self
+ self.connected = defer.Deferred()
+ self.client = SimpleClient(self.connected)
+ self.msgObjs = [
+ FakeyMessage({}, (), '', '', 999, None),
+ FakeyMessage({}, (), '', '', 10101, None),
+ FakeyMessage({}, (), '', '', 12345, None),
+ FakeyMessage({}, (), '', '', 20001, None),
+ FakeyMessage({}, (), '', '', 20002, None),
+ ]
+
+
+ def fetch(self, messages, uid):
+ """
+ Pretend to be a mailbox and let C{self.server} lookup messages on me.
+ """
+ return zip(range(1, len(self.msgObjs) + 1), self.msgObjs)
+
+
+ def _messageSetSearchTest(self, queryTerms, expectedMessages):
+ """
+ Issue a search with given query and verify that the returned messages
+ match the given expected messages.
+
+ @param queryTerms: A string giving the search query.
+ @param expectedMessages: A list of the message sequence numbers
+ expected as the result of the search.
+ @return: A L{Deferred} which fires when the test is complete.
+ """
+ def search():
+ return self.client.search(queryTerms)
+
+ d = self.connected.addCallback(strip(search))
+ def searched(results):
+ self.assertEqual(results, expectedMessages)
+ d.addCallback(searched)
+ d.addCallback(self._cbStopClient)
+ d.addErrback(self._ebGeneral)
+ self.loopback()
+ return d
+
+
+ def test_searchMessageSet(self):
+ """
+ Test that a search which starts with a message set properly limits
+ the search results to messages in that set.
+ """
+ return self._messageSetSearchTest('1', [1])
+
+
+ def test_searchMessageSetWithStar(self):
+ """
+ If the search filter ends with a star, all the message from the
+ starting point are returned.
+ """
+ return self._messageSetSearchTest('2:*', [2, 3, 4, 5])
+
+
+ def test_searchMessageSetWithStarFirst(self):
+ """
+ If the search filter starts with a star, the result should be identical
+ with if the filter would end with a star.
+ """
+ return self._messageSetSearchTest('*:2', [2, 3, 4, 5])
+
+
+ def test_searchMessageSetUIDWithStar(self):
+ """
+ If the search filter ends with a star, all the message from the
+ starting point are returned (also for the SEARCH UID case).
+ """
+ return self._messageSetSearchTest('UID 10000:*', [2, 3, 4, 5])
+
+
+ def test_searchMessageSetUIDWithStarFirst(self):
+ """
+ If the search filter starts with a star, the result should be identical
+ with if the filter would end with a star (also for the SEARCH UID case).
+ """
+ return self._messageSetSearchTest('UID *:10000', [2, 3, 4, 5])
+
+
+ def test_searchMessageSetUIDWithStarAndHighStart(self):
+ """
+ A search filter of 1234:* should include the UID of the last message in
+ the mailbox, even if its UID is less than 1234.
+ """
+ # in our fake mbox the highest message UID is 20002
+ return self._messageSetSearchTest('UID 30000:*', [5])
+
+
+ def test_searchMessageSetWithList(self):
+ """
+ If the search filter contains nesting terms, one of which includes a
+ message sequence set with a wildcard, IT ALL WORKS GOOD.
+ """
+ # 6 is bigger than the biggest message sequence number, but that's
+ # okay, because N:* includes the biggest message sequence number even
+ # if N is bigger than that (read the rfc nub).
+ return self._messageSetSearchTest('(6:*)', [5])
+
+
+ def test_searchOr(self):
+ """
+ If the search filter contains an I{OR} term, all messages
+ which match either subexpression are returned.
+ """
+ return self._messageSetSearchTest('OR 1 2', [1, 2])
+
+
+ def test_searchOrMessageSet(self):
+ """
+ If the search filter contains an I{OR} term with a
+ subexpression which includes a message sequence set wildcard,
+ all messages in that set are considered for inclusion in the
+ results.
+ """
+ return self._messageSetSearchTest('OR 2:* 2:*', [2, 3, 4, 5])
+
+
+ def test_searchNot(self):
+ """
+ If the search filter contains a I{NOT} term, all messages
+ which do not match the subexpression are returned.
+ """
+ return self._messageSetSearchTest('NOT 3', [1, 2, 4, 5])
+
+
+ def test_searchNotMessageSet(self):
+ """
+ If the search filter contains a I{NOT} term with a
+ subexpression which includes a message sequence set wildcard,
+ no messages in that set are considered for inclusion in the
+ result.
+ """
+ return self._messageSetSearchTest('NOT 2:*', [1])
+
+
+ def test_searchAndMessageSet(self):
+ """
+ If the search filter contains multiple terms implicitly
+ conjoined with a message sequence set wildcard, only the
+ intersection of the results of each term are returned.
+ """
+ return self._messageSetSearchTest('2:* 3', [3])
+
+
+
+class FetchSearchStoreTestCase(unittest.TestCase, IMAP4HelperMixin):
+ implements(imap4.ISearchableMailbox)
+
+ def setUp(self):
+ self.expected = self.result = None
+ self.server_received_query = None
+ self.server_received_uid = None
+ self.server_received_parts = None
+ self.server_received_messages = None
+
+ self.server = imap4.IMAP4Server()
+ self.server.state = 'select'
+ self.server.mbox = self
+ self.connected = defer.Deferred()
+ self.client = SimpleClient(self.connected)
+
+ def search(self, query, uid):
+ self.server_received_query = query
+ self.server_received_uid = uid
+ return self.expected
+
+ def addListener(self, *a, **kw):
+ pass
+ removeListener = addListener
+
+ def _searchWork(self, uid):
+ def search():
+ return self.client.search(self.query, uid=uid)
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(strip(search)
+ ).addCallback(result
+ ).addCallback(self._cbStopClient
+ ).addErrback(self._ebGeneral)
+
+ def check(ignored):
+ # Ensure no short-circuiting wierdness is going on
+ self.failIf(self.result is self.expected)
+
+ self.assertEqual(self.result, self.expected)
+ self.assertEqual(self.uid, self.server_received_uid)
+ self.assertEqual(
+ imap4.parseNestedParens(self.query),
+ self.server_received_query
+ )
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(check)
+ return d
+
+ def testSearch(self):
+ self.query = imap4.Or(
+ imap4.Query(header=('subject', 'substring')),
+ imap4.Query(larger=1024, smaller=4096),
+ )
+ self.expected = [1, 4, 5, 7]
+ self.uid = 0
+ return self._searchWork(0)
+
+ def testUIDSearch(self):
+ self.query = imap4.Or(
+ imap4.Query(header=('subject', 'substring')),
+ imap4.Query(larger=1024, smaller=4096),
+ )
+ self.uid = 1
+ self.expected = [1, 2, 3]
+ return self._searchWork(1)
+
+ def getUID(self, msg):
+ try:
+ return self.expected[msg]['UID']
+ except (TypeError, IndexError):
+ return self.expected[msg-1]
+ except KeyError:
+ return 42
+
+ def fetch(self, messages, uid):
+ self.server_received_uid = uid
+ self.server_received_messages = str(messages)
+ return self.expected
+
+ def _fetchWork(self, fetch):
+ def result(R):
+ self.result = R
+
+ self.connected.addCallback(strip(fetch)
+ ).addCallback(result
+ ).addCallback(self._cbStopClient
+ ).addErrback(self._ebGeneral)
+
+ def check(ignored):
+ # Ensure no short-circuiting wierdness is going on
+ self.failIf(self.result is self.expected)
+
+ self.parts and self.parts.sort()
+ self.server_received_parts and self.server_received_parts.sort()
+
+ if self.uid:
+ for (k, v) in self.expected.items():
+ v['UID'] = str(k)
+
+ self.assertEqual(self.result, self.expected)
+ self.assertEqual(self.uid, self.server_received_uid)
+ self.assertEqual(self.parts, self.server_received_parts)
+ self.assertEqual(imap4.parseIdList(self.messages),
+ imap4.parseIdList(self.server_received_messages))
+
+ d = loopback.loopbackTCP(self.server, self.client, noisy=False)
+ d.addCallback(check)
+ return d
+
+
+
+class FakeMailbox:
+ def __init__(self):
+ self.args = []
+ def addMessage(self, body, flags, date):
+ self.args.append((body, flags, date))
+ return defer.succeed(None)
+
+class FeaturefulMessage:
+ implements(imap4.IMessageFile)
+
+ def getFlags(self):
+ return 'flags'
+
+ def getInternalDate(self):
+ return 'internaldate'
+
+ def open(self):
+ return StringIO("open")
+
+class MessageCopierMailbox:
+ implements(imap4.IMessageCopier)
+
+ def __init__(self):
+ self.msgs = []
+
+ def copy(self, msg):
+ self.msgs.append(msg)
+ return len(self.msgs)
+
+class CopyWorkerTestCase(unittest.TestCase):
+ def testFeaturefulMessage(self):
+ s = imap4.IMAP4Server()
+
+ # Yes. I am grabbing this uber-non-public method to test it.
+ # It is complex. It needs to be tested directly!
+ # Perhaps it should be refactored, simplified, or split up into
+ # not-so-private components, but that is a task for another day.
+
+ # Ha ha! Addendum! Soon it will be split up, and this test will
+ # be re-written to just use the default adapter for IMailbox to
+ # IMessageCopier and call .copy on that adapter.
+ f = s._IMAP4Server__cbCopy
+
+ m = FakeMailbox()
+ d = f([(i, FeaturefulMessage()) for i in range(1, 11)], 'tag', m)
+
+ def cbCopy(results):
+ for a in m.args:
+ self.assertEqual(a[0].read(), "open")
+ self.assertEqual(a[1], "flags")
+ self.assertEqual(a[2], "internaldate")
+
+ for (status, result) in results:
+ self.failUnless(status)
+ self.assertEqual(result, None)
+
+ return d.addCallback(cbCopy)
+
+
+ def testUnfeaturefulMessage(self):
+ s = imap4.IMAP4Server()
+
+ # See above comment
+ f = s._IMAP4Server__cbCopy
+
+ m = FakeMailbox()
+ msgs = [FakeyMessage({'Header-Counter': str(i)}, (), 'Date', 'Body %d' % (i,), i + 10, None) for i in range(1, 11)]
+ d = f([im for im in zip(range(1, 11), msgs)], 'tag', m)
+
+ def cbCopy(results):
+ seen = []
+ for a in m.args:
+ seen.append(a[0].read())
+ self.assertEqual(a[1], ())
+ self.assertEqual(a[2], "Date")
+
+ seen.sort()
+ exp = ["Header-Counter: %d\r\n\r\nBody %d" % (i, i) for i in range(1, 11)]
+ exp.sort()
+ self.assertEqual(seen, exp)
+
+ for (status, result) in results:
+ self.failUnless(status)
+ self.assertEqual(result, None)
+
+ return d.addCallback(cbCopy)
+
+ def testMessageCopier(self):
+ s = imap4.IMAP4Server()
+
+ # See above comment
+ f = s._IMAP4Server__cbCopy
+
+ m = MessageCopierMailbox()
+ msgs = [object() for i in range(1, 11)]
+ d = f([im for im in zip(range(1, 11), msgs)], 'tag', m)
+
+ def cbCopy(results):
+ self.assertEqual(results, zip([1] * 10, range(1, 11)))
+ for (orig, new) in zip(msgs, m.msgs):
+ self.assertIdentical(orig, new)
+
+ return d.addCallback(cbCopy)
+
+
+class TLSTestCase(IMAP4HelperMixin, unittest.TestCase):
+ serverCTX = ServerTLSContext and ServerTLSContext()
+ clientCTX = ClientTLSContext and ClientTLSContext()
+
+ def loopback(self):
+ return loopback.loopbackTCP(self.server, self.client, noisy=False)
+
+ def testAPileOfThings(self):
+ SimpleServer.theAccount.addMailbox('inbox')
+ called = []
+ def login():
+ called.append(None)
+ return self.client.login('testuser', 'password-test')
+ def list():
+ called.append(None)
+ return self.client.list('inbox', '%')
+ def status():
+ called.append(None)
+ return self.client.status('inbox', 'UIDNEXT')
+ def examine():
+ called.append(None)
+ return self.client.examine('inbox')
+ def logout():
+ called.append(None)
+ return self.client.logout()
+
+ self.client.requireTransportSecurity = True
+
+ methods = [login, list, status, examine, logout]
+ map(self.connected.addCallback, map(strip, methods))
+ self.connected.addCallbacks(self._cbStopClient, self._ebGeneral)
+ def check(ignored):
+ self.assertEqual(self.server.startedTLS, True)
+ self.assertEqual(self.client.startedTLS, True)
+ self.assertEqual(len(called), len(methods))
+ d = self.loopback()
+ d.addCallback(check)
+ return d
+
+ def testLoginLogin(self):
+ self.server.checker.addUser('testuser', 'password-test')
+ success = []
+ self.client.registerAuthenticator(imap4.LOGINAuthenticator('testuser'))
+ self.connected.addCallback(
+ lambda _: self.client.authenticate('password-test')
+ ).addCallback(
+ lambda _: self.client.logout()
+ ).addCallback(success.append
+ ).addCallback(self._cbStopClient
+ ).addErrback(self._ebGeneral)
+
+ d = self.loopback()
+ d.addCallback(lambda x : self.assertEqual(len(success), 1))
+ return d
+
+
+ def test_startTLS(self):
+ """
+ L{IMAP4Client.startTLS} triggers TLS negotiation and returns a
+ L{Deferred} which fires after the client's transport is using
+ encryption.
+ """
+ success = []
+ self.connected.addCallback(lambda _: self.client.startTLS())
+ def checkSecure(ignored):
+ self.assertTrue(
+ interfaces.ISSLTransport.providedBy(self.client.transport))
+ self.connected.addCallback(checkSecure)
+ self.connected.addCallback(self._cbStopClient)
+ self.connected.addCallback(success.append)
+ self.connected.addErrback(self._ebGeneral)
+
+ d = self.loopback()
+ d.addCallback(lambda x : self.failUnless(success))
+ return defer.gatherResults([d, self.connected])
+
+
+ def testFailedStartTLS(self):
+ failure = []
+ def breakServerTLS(ign):
+ self.server.canStartTLS = False
+
+ self.connected.addCallback(breakServerTLS)
+ self.connected.addCallback(lambda ign: self.client.startTLS())
+ self.connected.addErrback(lambda err: failure.append(err.trap(imap4.IMAP4Exception)))
+ self.connected.addCallback(self._cbStopClient)
+ self.connected.addErrback(self._ebGeneral)
+
+ def check(ignored):
+ self.failUnless(failure)
+ self.assertIdentical(failure[0], imap4.IMAP4Exception)
+ return self.loopback().addCallback(check)
+
+
+
+class SlowMailbox(SimpleMailbox):
+ howSlow = 2
+ callLater = None
+ fetchDeferred = None
+
+ # Not a very nice implementation of fetch(), but it'll
+ # do for the purposes of testing.
+ def fetch(self, messages, uid):
+ d = defer.Deferred()
+ self.callLater(self.howSlow, d.callback, ())
+ self.fetchDeferred.callback(None)
+ return d
+
+class Timeout(IMAP4HelperMixin, unittest.TestCase):
+
+ def test_serverTimeout(self):
+ """
+ The *client* has a timeout mechanism which will close connections that
+ are inactive for a period.
+ """
+ c = Clock()
+ self.server.timeoutTest = True
+ self.client.timeout = 5 #seconds
+ self.client.callLater = c.callLater
+ self.selectedArgs = None
+
+ def login():
+ d = self.client.login('testuser', 'password-test')
+ c.advance(5)
+ d.addErrback(timedOut)
+ return d
+
+ def timedOut(failure):
+ self._cbStopClient(None)
+ failure.trap(error.TimeoutError)
+
+ d = self.connected.addCallback(strip(login))
+ d.addErrback(self._ebGeneral)
+ return defer.gatherResults([d, self.loopback()])
+
+
+ def test_longFetchDoesntTimeout(self):
+ """
+ The connection timeout does not take effect during fetches.
+ """
+ c = Clock()
+ SlowMailbox.callLater = c.callLater
+ SlowMailbox.fetchDeferred = defer.Deferred()
+ self.server.callLater = c.callLater
+ SimpleServer.theAccount.mailboxFactory = SlowMailbox
+ SimpleServer.theAccount.addMailbox('mailbox-test')
+
+ self.server.setTimeout(1)
+
+ def login():
+ return self.client.login('testuser', 'password-test')
+ def select():
+ self.server.setTimeout(1)
+ return self.client.select('mailbox-test')
+ def fetch():
+ return self.client.fetchUID('1:*')
+ def stillConnected():
+ self.assertNotEquals(self.server.state, 'timeout')
+
+ def cbAdvance(ignored):
+ for i in xrange(4):
+ c.advance(.5)
+
+ SlowMailbox.fetchDeferred.addCallback(cbAdvance)
+
+ d1 = self.connected.addCallback(strip(login))
+ d1.addCallback(strip(select))
+ d1.addCallback(strip(fetch))
+ d1.addCallback(strip(stillConnected))
+ d1.addCallback(self._cbStopClient)
+ d1.addErrback(self._ebGeneral)
+ d = defer.gatherResults([d1, self.loopback()])
+ return d
+
+
+ def test_idleClientDoesDisconnect(self):
+ """
+ The *server* has a timeout mechanism which will close connections that
+ are inactive for a period.
+ """
+ c = Clock()
+ # Hook up our server protocol
+ transport = StringTransportWithDisconnection()
+ transport.protocol = self.server
+ self.server.callLater = c.callLater
+ self.server.makeConnection(transport)
+
+ # Make sure we can notice when the connection goes away
+ lost = []
+ connLost = self.server.connectionLost
+ self.server.connectionLost = lambda reason: (lost.append(None), connLost(reason))[1]
+
+ # 2/3rds of the idle timeout elapses...
+ c.pump([0.0] + [self.server.timeOut / 3.0] * 2)
+ self.failIf(lost, lost)
+
+ # Now some more
+ c.pump([0.0, self.server.timeOut / 2.0])
+ self.failUnless(lost)
+
+
+
+class Disconnection(unittest.TestCase):
+ def testClientDisconnectFailsDeferreds(self):
+ c = imap4.IMAP4Client()
+ t = StringTransportWithDisconnection()
+ c.makeConnection(t)
+ d = self.assertFailure(c.login('testuser', 'example.com'), error.ConnectionDone)
+ c.connectionLost(error.ConnectionDone("Connection closed"))
+ return d
+
+
+
+class SynchronousMailbox(object):
+ """
+ Trivial, in-memory mailbox implementation which can produce a message
+ synchronously.
+ """
+ def __init__(self, messages):
+ self.messages = messages
+
+
+ def fetch(self, msgset, uid):
+ assert not uid, "Cannot handle uid requests."
+ for msg in msgset:
+ yield msg, self.messages[msg - 1]
+
+
+
+class StringTransportConsumer(StringTransport):
+ producer = None
+ streaming = None
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+ self.streaming = streaming
+
+
+
+class Pipelining(unittest.TestCase):
+ """
+ Tests for various aspects of the IMAP4 server's pipelining support.
+ """
+ messages = [
+ FakeyMessage({}, [], '', '0', None, None),
+ FakeyMessage({}, [], '', '1', None, None),
+ FakeyMessage({}, [], '', '2', None, None),
+ ]
+
+ def setUp(self):
+ self.iterators = []
+
+ self.transport = StringTransportConsumer()
+ self.server = imap4.IMAP4Server(None, None, self.iterateInReactor)
+ self.server.makeConnection(self.transport)
+
+
+ def iterateInReactor(self, iterator):
+ d = defer.Deferred()
+ self.iterators.append((iterator, d))
+ return d
+
+
+ def tearDown(self):
+ self.server.connectionLost(failure.Failure(error.ConnectionDone()))
+
+
+ def test_synchronousFetch(self):
+ """
+ Test that pipelined FETCH commands which can be responded to
+ synchronously are responded to correctly.
+ """
+ mailbox = SynchronousMailbox(self.messages)
+
+ # Skip over authentication and folder selection
+ self.server.state = 'select'
+ self.server.mbox = mailbox
+
+ # Get rid of any greeting junk
+ self.transport.clear()
+
+ # Here's some pipelined stuff
+ self.server.dataReceived(
+ '01 FETCH 1 BODY[]\r\n'
+ '02 FETCH 2 BODY[]\r\n'
+ '03 FETCH 3 BODY[]\r\n')
+
+ # Flush anything the server has scheduled to run
+ while self.iterators:
+ for e in self.iterators[0][0]:
+ break
+ else:
+ self.iterators.pop(0)[1].callback(None)
+
+ # The bodies are empty because we aren't simulating a transport
+ # exactly correctly (we have StringTransportConsumer but we never
+ # call resumeProducing on its producer). It doesn't matter: just
+ # make sure the surrounding structure is okay, and that no
+ # exceptions occurred.
+ self.assertEqual(
+ self.transport.value(),
+ '* 1 FETCH (BODY[] )\r\n'
+ '01 OK FETCH completed\r\n'
+ '* 2 FETCH (BODY[] )\r\n'
+ '02 OK FETCH completed\r\n'
+ '* 3 FETCH (BODY[] )\r\n'
+ '03 OK FETCH completed\r\n')
+
+
+
+if ClientTLSContext is None:
+ for case in (TLSTestCase,):
+ case.skip = "OpenSSL not present"
+elif interfaces.IReactorSSL(reactor, None) is None:
+ for case in (TLSTestCase,):
+ case.skip = "Reactor doesn't support SSL"
diff --git a/twisted/mail/test/test_mail.py b/twisted/mail/test/test_mail.py
new file mode 100644
index 0000000..a197426
--- /dev/null
+++ b/twisted/mail/test/test_mail.py
@@ -0,0 +1,2039 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for large portions of L{twisted.mail}.
+"""
+
+import os
+import errno
+import shutil
+import pickle
+import StringIO
+import rfc822
+import tempfile
+import signal
+
+from zope.interface import Interface, implements
+
+from twisted.trial import unittest
+from twisted.mail import smtp
+from twisted.mail import pop3
+from twisted.names import dns
+from twisted.internet import protocol
+from twisted.internet import defer
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+from twisted.internet import interfaces
+from twisted.internet import task
+from twisted.internet.error import DNSLookupError, CannotListenError
+from twisted.internet.error import ProcessDone, ProcessTerminated
+from twisted.internet import address
+from twisted.python import failure
+from twisted.python.filepath import FilePath
+from twisted.python.hashlib import md5
+
+from twisted import mail
+import twisted.mail.mail
+import twisted.mail.maildir
+import twisted.mail.relay
+import twisted.mail.relaymanager
+import twisted.mail.protocols
+import twisted.mail.alias
+
+from twisted.names.error import DNSNameError
+from twisted.names.dns import RRHeader, Record_CNAME, Record_MX
+
+from twisted import cred
+import twisted.cred.credentials
+import twisted.cred.checkers
+import twisted.cred.portal
+
+from twisted.test.proto_helpers import LineSendingProtocol
+
+class DomainWithDefaultsTestCase(unittest.TestCase):
+ def testMethods(self):
+ d = dict([(x, x + 10) for x in range(10)])
+ d = mail.mail.DomainWithDefaultDict(d, 'Default')
+
+ self.assertEqual(len(d), 10)
+ self.assertEqual(list(iter(d)), range(10))
+ self.assertEqual(list(d.iterkeys()), list(iter(d)))
+
+ items = list(d.iteritems())
+ items.sort()
+ self.assertEqual(items, [(x, x + 10) for x in range(10)])
+
+ values = list(d.itervalues())
+ values.sort()
+ self.assertEqual(values, range(10, 20))
+
+ items = d.items()
+ items.sort()
+ self.assertEqual(items, [(x, x + 10) for x in range(10)])
+
+ values = d.values()
+ values.sort()
+ self.assertEqual(values, range(10, 20))
+
+ for x in range(10):
+ self.assertEqual(d[x], x + 10)
+ self.assertEqual(d.get(x), x + 10)
+ self.failUnless(x in d)
+ self.failUnless(d.has_key(x))
+
+ del d[2], d[4], d[6]
+
+ self.assertEqual(len(d), 7)
+ self.assertEqual(d[2], 'Default')
+ self.assertEqual(d[4], 'Default')
+ self.assertEqual(d[6], 'Default')
+
+ d.update({'a': None, 'b': (), 'c': '*'})
+ self.assertEqual(len(d), 10)
+ self.assertEqual(d['a'], None)
+ self.assertEqual(d['b'], ())
+ self.assertEqual(d['c'], '*')
+
+ d.clear()
+ self.assertEqual(len(d), 0)
+
+ self.assertEqual(d.setdefault('key', 'value'), 'value')
+ self.assertEqual(d['key'], 'value')
+
+ self.assertEqual(d.popitem(), ('key', 'value'))
+ self.assertEqual(len(d), 0)
+
+ dcopy = d.copy()
+ self.assertEqual(d.domains, dcopy.domains)
+ self.assertEqual(d.default, dcopy.default)
+
+
+ def _stringificationTest(self, stringifier):
+ """
+ Assert that the class name of a L{mail.mail.DomainWithDefaultDict}
+ instance and the string-formatted underlying domain dictionary both
+ appear in the string produced by the given string-returning function.
+
+ @type stringifier: one-argument callable
+ @param stringifier: either C{str} or C{repr}, to be used to get a
+ string to make assertions against.
+ """
+ domain = mail.mail.DomainWithDefaultDict({}, 'Default')
+ self.assertIn(domain.__class__.__name__, stringifier(domain))
+ domain['key'] = 'value'
+ self.assertIn(str({'key': 'value'}), stringifier(domain))
+
+
+ def test_str(self):
+ """
+ L{DomainWithDefaultDict.__str__} should return a string including
+ the class name and the domain mapping held by the instance.
+ """
+ self._stringificationTest(str)
+
+
+ def test_repr(self):
+ """
+ L{DomainWithDefaultDict.__repr__} should return a string including
+ the class name and the domain mapping held by the instance.
+ """
+ self._stringificationTest(repr)
+
+
+
+class BounceTestCase(unittest.TestCase):
+ def setUp(self):
+ self.domain = mail.mail.BounceDomain()
+
+ def testExists(self):
+ self.assertRaises(smtp.AddressError, self.domain.exists, "any user")
+
+ def testRelay(self):
+ self.assertEqual(
+ self.domain.willRelay("random q emailer", "protocol"),
+ False
+ )
+
+ def testMessage(self):
+ self.assertRaises(NotImplementedError, self.domain.startMessage, "whomever")
+
+ def testAddUser(self):
+ self.domain.addUser("bob", "password")
+ self.assertRaises(smtp.SMTPBadRcpt, self.domain.exists, "bob")
+
+class FileMessageTestCase(unittest.TestCase):
+ def setUp(self):
+ self.name = "fileMessage.testFile"
+ self.final = "final.fileMessage.testFile"
+ self.f = file(self.name, 'w')
+ self.fp = mail.mail.FileMessage(self.f, self.name, self.final)
+
+ def tearDown(self):
+ try:
+ self.f.close()
+ except:
+ pass
+ try:
+ os.remove(self.name)
+ except:
+ pass
+ try:
+ os.remove(self.final)
+ except:
+ pass
+
+ def testFinalName(self):
+ return self.fp.eomReceived().addCallback(self._cbFinalName)
+
+ def _cbFinalName(self, result):
+ self.assertEqual(result, self.final)
+ self.failUnless(self.f.closed)
+ self.failIf(os.path.exists(self.name))
+
+ def testContents(self):
+ contents = "first line\nsecond line\nthird line\n"
+ for line in contents.splitlines():
+ self.fp.lineReceived(line)
+ self.fp.eomReceived()
+ self.assertEqual(file(self.final).read(), contents)
+
+ def testInterrupted(self):
+ contents = "first line\nsecond line\n"
+ for line in contents.splitlines():
+ self.fp.lineReceived(line)
+ self.fp.connectionLost()
+ self.failIf(os.path.exists(self.name))
+ self.failIf(os.path.exists(self.final))
+
+class MailServiceTestCase(unittest.TestCase):
+ def setUp(self):
+ self.service = mail.mail.MailService()
+
+ def testFactories(self):
+ f = self.service.getPOP3Factory()
+ self.failUnless(isinstance(f, protocol.ServerFactory))
+ self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), pop3.POP3)
+
+ f = self.service.getSMTPFactory()
+ self.failUnless(isinstance(f, protocol.ServerFactory))
+ self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.SMTP)
+
+ f = self.service.getESMTPFactory()
+ self.failUnless(isinstance(f, protocol.ServerFactory))
+ self.failUnless(f.buildProtocol(('127.0.0.1', 12345)), smtp.ESMTP)
+
+ def testPortals(self):
+ o1 = object()
+ o2 = object()
+ self.service.portals['domain'] = o1
+ self.service.portals[''] = o2
+
+ self.failUnless(self.service.lookupPortal('domain') is o1)
+ self.failUnless(self.service.defaultPortal() is o2)
+
+
+class StringListMailboxTests(unittest.TestCase):
+ """
+ Tests for L{StringListMailbox}, an in-memory only implementation of
+ L{pop3.IMailbox}.
+ """
+ def test_listOneMessage(self):
+ """
+ L{StringListMailbox.listMessages} returns the length of the message at
+ the offset into the mailbox passed to it.
+ """
+ mailbox = mail.maildir.StringListMailbox(["abc", "ab", "a"])
+ self.assertEqual(mailbox.listMessages(0), 3)
+ self.assertEqual(mailbox.listMessages(1), 2)
+ self.assertEqual(mailbox.listMessages(2), 1)
+
+
+ def test_listAllMessages(self):
+ """
+ L{StringListMailbox.listMessages} returns a list of the lengths of all
+ messages if not passed an index.
+ """
+ mailbox = mail.maildir.StringListMailbox(["a", "abc", "ab"])
+ self.assertEqual(mailbox.listMessages(), [1, 3, 2])
+
+
+ def test_getMessage(self):
+ """
+ L{StringListMailbox.getMessage} returns a file-like object from which
+ the contents of the message at the given offset into the mailbox can be
+ read.
+ """
+ mailbox = mail.maildir.StringListMailbox(["foo", "real contents"])
+ self.assertEqual(mailbox.getMessage(1).read(), "real contents")
+
+
+ def test_getUidl(self):
+ """
+ L{StringListMailbox.getUidl} returns a unique identifier for the
+ message at the given offset into the mailbox.
+ """
+ mailbox = mail.maildir.StringListMailbox(["foo", "bar"])
+ self.assertNotEqual(mailbox.getUidl(0), mailbox.getUidl(1))
+
+
+ def test_deleteMessage(self):
+ """
+ L{StringListMailbox.deleteMessage} marks a message for deletion causing
+ further requests for its length to return 0.
+ """
+ mailbox = mail.maildir.StringListMailbox(["foo"])
+ mailbox.deleteMessage(0)
+ self.assertEqual(mailbox.listMessages(0), 0)
+ self.assertEqual(mailbox.listMessages(), [0])
+
+
+ def test_undeleteMessages(self):
+ """
+ L{StringListMailbox.undeleteMessages} causes any messages marked for
+ deletion to be returned to their original state.
+ """
+ mailbox = mail.maildir.StringListMailbox(["foo"])
+ mailbox.deleteMessage(0)
+ mailbox.undeleteMessages()
+ self.assertEqual(mailbox.listMessages(0), 3)
+ self.assertEqual(mailbox.listMessages(), [3])
+
+
+ def test_sync(self):
+ """
+ L{StringListMailbox.sync} causes any messages as marked for deletion to
+ be permanently deleted.
+ """
+ mailbox = mail.maildir.StringListMailbox(["foo"])
+ mailbox.deleteMessage(0)
+ mailbox.sync()
+ mailbox.undeleteMessages()
+ self.assertEqual(mailbox.listMessages(0), 0)
+ self.assertEqual(mailbox.listMessages(), [0])
+
+
+
+class FailingMaildirMailboxAppendMessageTask(mail.maildir._MaildirMailboxAppendMessageTask):
+ _openstate = True
+ _writestate = True
+ _renamestate = True
+ def osopen(self, fn, attr, mode):
+ if self._openstate:
+ return os.open(fn, attr, mode)
+ else:
+ raise OSError(errno.EPERM, "Faked Permission Problem")
+ def oswrite(self, fh, data):
+ if self._writestate:
+ return os.write(fh, data)
+ else:
+ raise OSError(errno.ENOSPC, "Faked Space problem")
+ def osrename(self, oldname, newname):
+ if self._renamestate:
+ return os.rename(oldname, newname)
+ else:
+ raise OSError(errno.EPERM, "Faked Permission Problem")
+
+
+class _AppendTestMixin(object):
+ """
+ Mixin for L{MaildirMailbox.appendMessage} test cases which defines a helper
+ for serially appending multiple messages to a mailbox.
+ """
+ def _appendMessages(self, mbox, messages):
+ """
+ Deliver the given messages one at a time. Delivery is serialized to
+ guarantee a predictable order in the mailbox (overlapped message deliver
+ makes no guarantees about which message which appear first).
+ """
+ results = []
+ def append():
+ for m in messages:
+ d = mbox.appendMessage(m)
+ d.addCallback(results.append)
+ yield d
+ d = task.cooperate(append()).whenDone()
+ d.addCallback(lambda ignored: results)
+ return d
+
+
+
+class MaildirAppendStringTestCase(unittest.TestCase, _AppendTestMixin):
+ """
+ Tests for L{MaildirMailbox.appendMessage} when invoked with a C{str}.
+ """
+ def setUp(self):
+ self.d = self.mktemp()
+ mail.maildir.initializeMaildir(self.d)
+
+
+ def _append(self, ignored, mbox):
+ d = mbox.appendMessage('TEST')
+ return self.assertFailure(d, Exception)
+
+
+ def _setState(self, ignored, mbox, rename=None, write=None, open=None):
+ """
+ Change the behavior of future C{rename}, C{write}, or C{open} calls made
+ by the mailbox C{mbox}.
+
+ @param rename: If not C{None}, a new value for the C{_renamestate}
+ attribute of the mailbox's append factory. The original value will
+ be restored at the end of the test.
+
+ @param write: Like C{rename}, but for the C{_writestate} attribute.
+
+ @param open: Like C{rename}, but for the C{_openstate} attribute.
+ """
+ if rename is not None:
+ self.addCleanup(
+ setattr, mbox.AppendFactory, '_renamestate',
+ mbox.AppendFactory._renamestate)
+ mbox.AppendFactory._renamestate = rename
+ if write is not None:
+ self.addCleanup(
+ setattr, mbox.AppendFactory, '_writestate',
+ mbox.AppendFactory._writestate)
+ mbox.AppendFactory._writestate = write
+ if open is not None:
+ self.addCleanup(
+ setattr, mbox.AppendFactory, '_openstate',
+ mbox.AppendFactory._openstate)
+ mbox.AppendFactory._openstate = open
+
+
+ def test_append(self):
+ """
+ L{MaildirMailbox.appendMessage} returns a L{Deferred} which fires when
+ the message has been added to the end of the mailbox.
+ """
+ mbox = mail.maildir.MaildirMailbox(self.d)
+ mbox.AppendFactory = FailingMaildirMailboxAppendMessageTask
+
+ d = self._appendMessages(mbox, ["X" * i for i in range(1, 11)])
+ d.addCallback(self.assertEqual, [None] * 10)
+ d.addCallback(self._cbTestAppend, mbox)
+ return d
+
+
+ def _cbTestAppend(self, ignored, mbox):
+ """
+ Check that the mailbox has the expected number (ten) of messages in it,
+ and that each has the expected contents, and that they are in the same
+ order as that in which they were appended.
+ """
+ self.assertEqual(len(mbox.listMessages()), 10)
+ self.assertEqual(
+ [len(mbox.getMessage(i).read()) for i in range(10)],
+ range(1, 11))
+ # test in the right order: last to first error location.
+ self._setState(None, mbox, rename=False)
+ d = self._append(None, mbox)
+ d.addCallback(self._setState, mbox, rename=True, write=False)
+ d.addCallback(self._append, mbox)
+ d.addCallback(self._setState, mbox, write=True, open=False)
+ d.addCallback(self._append, mbox)
+ d.addCallback(self._setState, mbox, open=True)
+ return d
+
+
+
+class MaildirAppendFileTestCase(unittest.TestCase, _AppendTestMixin):
+ """
+ Tests for L{MaildirMailbox.appendMessage} when invoked with a C{str}.
+ """
+ def setUp(self):
+ self.d = self.mktemp()
+ mail.maildir.initializeMaildir(self.d)
+
+
+ def test_append(self):
+ """
+ L{MaildirMailbox.appendMessage} returns a L{Deferred} which fires when
+ the message has been added to the end of the mailbox.
+ """
+ mbox = mail.maildir.MaildirMailbox(self.d)
+ messages = []
+ for i in xrange(1, 11):
+ temp = tempfile.TemporaryFile()
+ temp.write("X" * i)
+ temp.seek(0, 0)
+ messages.append(temp)
+ self.addCleanup(temp.close)
+
+ d = self._appendMessages(mbox, messages)
+ d.addCallback(self._cbTestAppend, mbox)
+ return d
+
+
+ def _cbTestAppend(self, result, mbox):
+ """
+ Check that the mailbox has the expected number (ten) of messages in it,
+ and that each has the expected contents, and that they are in the same
+ order as that in which they were appended.
+ """
+ self.assertEqual(len(mbox.listMessages()), 10)
+ self.assertEqual(
+ [len(mbox.getMessage(i).read()) for i in range(10)],
+ range(1, 11))
+
+
+
+class MaildirTestCase(unittest.TestCase):
+ def setUp(self):
+ self.d = self.mktemp()
+ mail.maildir.initializeMaildir(self.d)
+
+ def tearDown(self):
+ shutil.rmtree(self.d)
+
+ def testInitializer(self):
+ d = self.d
+ trash = os.path.join(d, '.Trash')
+
+ self.failUnless(os.path.exists(d) and os.path.isdir(d))
+ self.failUnless(os.path.exists(os.path.join(d, 'new')))
+ self.failUnless(os.path.exists(os.path.join(d, 'cur')))
+ self.failUnless(os.path.exists(os.path.join(d, 'tmp')))
+ self.failUnless(os.path.isdir(os.path.join(d, 'new')))
+ self.failUnless(os.path.isdir(os.path.join(d, 'cur')))
+ self.failUnless(os.path.isdir(os.path.join(d, 'tmp')))
+
+ self.failUnless(os.path.exists(os.path.join(trash, 'new')))
+ self.failUnless(os.path.exists(os.path.join(trash, 'cur')))
+ self.failUnless(os.path.exists(os.path.join(trash, 'tmp')))
+ self.failUnless(os.path.isdir(os.path.join(trash, 'new')))
+ self.failUnless(os.path.isdir(os.path.join(trash, 'cur')))
+ self.failUnless(os.path.isdir(os.path.join(trash, 'tmp')))
+
+
+ def test_nameGenerator(self):
+ """
+ Each call to L{_MaildirNameGenerator.generate} returns a unique
+ string suitable for use as the basename of a new message file. The
+ names are ordered such that those generated earlier sort less than
+ those generated later.
+ """
+ clock = task.Clock()
+ clock.advance(0.05)
+ generator = mail.maildir._MaildirNameGenerator(clock)
+
+ firstName = generator.generate()
+ clock.advance(0.05)
+ secondName = generator.generate()
+
+ self.assertTrue(firstName < secondName)
+
+
+ def test_mailbox(self):
+ """
+ Exercise the methods of L{IMailbox} as implemented by
+ L{MaildirMailbox}.
+ """
+ j = os.path.join
+ n = mail.maildir._generateMaildirName
+ msgs = [j(b, n()) for b in ('cur', 'new') for x in range(5)]
+
+ # Toss a few files into the mailbox
+ i = 1
+ for f in msgs:
+ fObj = file(j(self.d, f), 'w')
+ fObj.write('x' * i)
+ fObj.close()
+ i = i + 1
+
+ mb = mail.maildir.MaildirMailbox(self.d)
+ self.assertEqual(mb.listMessages(), range(1, 11))
+ self.assertEqual(mb.listMessages(1), 2)
+ self.assertEqual(mb.listMessages(5), 6)
+
+ self.assertEqual(mb.getMessage(6).read(), 'x' * 7)
+ self.assertEqual(mb.getMessage(1).read(), 'x' * 2)
+
+ d = {}
+ for i in range(10):
+ u = mb.getUidl(i)
+ self.failIf(u in d)
+ d[u] = None
+
+ p, f = os.path.split(msgs[5])
+
+ mb.deleteMessage(5)
+ self.assertEqual(mb.listMessages(5), 0)
+ self.failUnless(os.path.exists(j(self.d, '.Trash', 'cur', f)))
+ self.failIf(os.path.exists(j(self.d, msgs[5])))
+
+ mb.undeleteMessages()
+ self.assertEqual(mb.listMessages(5), 6)
+ self.failIf(os.path.exists(j(self.d, '.Trash', 'cur', f)))
+ self.failUnless(os.path.exists(j(self.d, msgs[5])))
+
+class MaildirDirdbmDomainTestCase(unittest.TestCase):
+ def setUp(self):
+ self.P = self.mktemp()
+ self.S = mail.mail.MailService()
+ self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.P)
+
+ def tearDown(self):
+ shutil.rmtree(self.P)
+
+ def testAddUser(self):
+ toAdd = (('user1', 'pwd1'), ('user2', 'pwd2'), ('user3', 'pwd3'))
+ for (u, p) in toAdd:
+ self.D.addUser(u, p)
+
+ for (u, p) in toAdd:
+ self.failUnless(u in self.D.dbm)
+ self.assertEqual(self.D.dbm[u], p)
+ self.failUnless(os.path.exists(os.path.join(self.P, u)))
+
+ def testCredentials(self):
+ creds = self.D.getCredentialsCheckers()
+
+ self.assertEqual(len(creds), 1)
+ self.failUnless(cred.checkers.ICredentialsChecker.providedBy(creds[0]))
+ self.failUnless(cred.credentials.IUsernamePassword in creds[0].credentialInterfaces)
+
+ def testRequestAvatar(self):
+ class ISomething(Interface):
+ pass
+
+ self.D.addUser('user', 'password')
+ self.assertRaises(
+ NotImplementedError,
+ self.D.requestAvatar, 'user', None, ISomething
+ )
+
+ t = self.D.requestAvatar('user', None, pop3.IMailbox)
+ self.assertEqual(len(t), 3)
+ self.failUnless(t[0] is pop3.IMailbox)
+ self.failUnless(pop3.IMailbox.providedBy(t[1]))
+
+ t[2]()
+
+ def testRequestAvatarId(self):
+ self.D.addUser('user', 'password')
+ database = self.D.getCredentialsCheckers()[0]
+
+ creds = cred.credentials.UsernamePassword('user', 'wrong password')
+ self.assertRaises(
+ cred.error.UnauthorizedLogin,
+ database.requestAvatarId, creds
+ )
+
+ creds = cred.credentials.UsernamePassword('user', 'password')
+ self.assertEqual(database.requestAvatarId(creds), 'user')
+
+
+class StubAliasableDomain(object):
+ """
+ Minimal testable implementation of IAliasableDomain.
+ """
+ implements(mail.mail.IAliasableDomain)
+
+ def exists(self, user):
+ """
+ No test coverage for invocations of this method on domain objects,
+ so we just won't implement it.
+ """
+ raise NotImplementedError()
+
+
+ def addUser(self, user, password):
+ """
+ No test coverage for invocations of this method on domain objects,
+ so we just won't implement it.
+ """
+ raise NotImplementedError()
+
+
+ def getCredentialsCheckers(self):
+ """
+ This needs to succeed in order for other tests to complete
+ successfully, but we don't actually assert anything about its
+ behavior. Return an empty list. Sometime later we should return
+ something else and assert that a portal got set up properly.
+ """
+ return []
+
+
+ def setAliasGroup(self, aliases):
+ """
+ Just record the value so the test can check it later.
+ """
+ self.aliasGroup = aliases
+
+
+class ServiceDomainTestCase(unittest.TestCase):
+ def setUp(self):
+ self.S = mail.mail.MailService()
+ self.D = mail.protocols.DomainDeliveryBase(self.S, None)
+ self.D.service = self.S
+ self.D.protocolName = 'TEST'
+ self.D.host = 'hostname'
+
+ self.tmpdir = self.mktemp()
+ domain = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir)
+ domain.addUser('user', 'password')
+ self.S.addDomain('test.domain', domain)
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+
+ def testAddAliasableDomain(self):
+ """
+ Test that adding an IAliasableDomain to a mail service properly sets
+ up alias group references and such.
+ """
+ aliases = object()
+ domain = StubAliasableDomain()
+ self.S.aliases = aliases
+ self.S.addDomain('example.com', domain)
+ self.assertIdentical(domain.aliasGroup, aliases)
+
+
+ def testReceivedHeader(self):
+ hdr = self.D.receivedHeader(
+ ('remotehost', '123.232.101.234'),
+ smtp.Address('<someguy@somplace>'),
+ ['user@host.name']
+ )
+ fp = StringIO.StringIO(hdr)
+ m = rfc822.Message(fp)
+ self.assertEqual(len(m.items()), 1)
+ self.failUnless(m.has_key('Received'))
+
+ def testValidateTo(self):
+ user = smtp.User('user@test.domain', 'helo', None, 'wherever@whatever')
+ return defer.maybeDeferred(self.D.validateTo, user
+ ).addCallback(self._cbValidateTo
+ )
+
+ def _cbValidateTo(self, result):
+ self.failUnless(callable(result))
+
+ def testValidateToBadUsername(self):
+ user = smtp.User('resu@test.domain', 'helo', None, 'wherever@whatever')
+ return self.assertFailure(
+ defer.maybeDeferred(self.D.validateTo, user),
+ smtp.SMTPBadRcpt)
+
+ def testValidateToBadDomain(self):
+ user = smtp.User('user@domain.test', 'helo', None, 'wherever@whatever')
+ return self.assertFailure(
+ defer.maybeDeferred(self.D.validateTo, user),
+ smtp.SMTPBadRcpt)
+
+ def testValidateFrom(self):
+ helo = ('hostname', '127.0.0.1')
+ origin = smtp.Address('<user@hostname>')
+ self.failUnless(self.D.validateFrom(helo, origin) is origin)
+
+ helo = ('hostname', '1.2.3.4')
+ origin = smtp.Address('<user@hostname>')
+ self.failUnless(self.D.validateFrom(helo, origin) is origin)
+
+ helo = ('hostname', '1.2.3.4')
+ origin = smtp.Address('<>')
+ self.failUnless(self.D.validateFrom(helo, origin) is origin)
+
+ self.assertRaises(
+ smtp.SMTPBadSender,
+ self.D.validateFrom, None, origin
+ )
+
+class VirtualPOP3TestCase(unittest.TestCase):
+ def setUp(self):
+ self.tmpdir = self.mktemp()
+ self.S = mail.mail.MailService()
+ self.D = mail.maildir.MaildirDirdbmDomain(self.S, self.tmpdir)
+ self.D.addUser('user', 'password')
+ self.S.addDomain('test.domain', self.D)
+
+ portal = cred.portal.Portal(self.D)
+ map(portal.registerChecker, self.D.getCredentialsCheckers())
+ self.S.portals[''] = self.S.portals['test.domain'] = portal
+
+ self.P = mail.protocols.VirtualPOP3()
+ self.P.service = self.S
+ self.P.magic = '<unit test magic>'
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+ def testAuthenticateAPOP(self):
+ resp = md5(self.P.magic + 'password').hexdigest()
+ return self.P.authenticateUserAPOP('user', resp
+ ).addCallback(self._cbAuthenticateAPOP
+ )
+
+ def _cbAuthenticateAPOP(self, result):
+ self.assertEqual(len(result), 3)
+ self.assertEqual(result[0], pop3.IMailbox)
+ self.failUnless(pop3.IMailbox.providedBy(result[1]))
+ result[2]()
+
+ def testAuthenticateIncorrectUserAPOP(self):
+ resp = md5(self.P.magic + 'password').hexdigest()
+ return self.assertFailure(
+ self.P.authenticateUserAPOP('resu', resp),
+ cred.error.UnauthorizedLogin)
+
+ def testAuthenticateIncorrectResponseAPOP(self):
+ resp = md5('wrong digest').hexdigest()
+ return self.assertFailure(
+ self.P.authenticateUserAPOP('user', resp),
+ cred.error.UnauthorizedLogin)
+
+ def testAuthenticatePASS(self):
+ return self.P.authenticateUserPASS('user', 'password'
+ ).addCallback(self._cbAuthenticatePASS
+ )
+
+ def _cbAuthenticatePASS(self, result):
+ self.assertEqual(len(result), 3)
+ self.assertEqual(result[0], pop3.IMailbox)
+ self.failUnless(pop3.IMailbox.providedBy(result[1]))
+ result[2]()
+
+ def testAuthenticateBadUserPASS(self):
+ return self.assertFailure(
+ self.P.authenticateUserPASS('resu', 'password'),
+ cred.error.UnauthorizedLogin)
+
+ def testAuthenticateBadPasswordPASS(self):
+ return self.assertFailure(
+ self.P.authenticateUserPASS('user', 'wrong password'),
+ cred.error.UnauthorizedLogin)
+
+class empty(smtp.User):
+ def __init__(self):
+ pass
+
+class RelayTestCase(unittest.TestCase):
+ def testExists(self):
+ service = mail.mail.MailService()
+ domain = mail.relay.DomainQueuer(service)
+
+ doRelay = [
+ address.UNIXAddress('/var/run/mail-relay'),
+ address.IPv4Address('TCP', '127.0.0.1', 12345),
+ ]
+
+ dontRelay = [
+ address.IPv4Address('TCP', '192.168.2.1', 62),
+ address.IPv4Address('TCP', '1.2.3.4', 1943),
+ ]
+
+ for peer in doRelay:
+ user = empty()
+ user.orig = 'user@host'
+ user.dest = 'tsoh@resu'
+ user.protocol = empty()
+ user.protocol.transport = empty()
+ user.protocol.transport.getPeer = lambda: peer
+
+ self.failUnless(callable(domain.exists(user)))
+
+ for peer in dontRelay:
+ user = empty()
+ user.orig = 'some@place'
+ user.protocol = empty()
+ user.protocol.transport = empty()
+ user.protocol.transport.getPeer = lambda: peer
+ user.dest = 'who@cares'
+
+ self.assertRaises(smtp.SMTPBadRcpt, domain.exists, user)
+
+class RelayerTestCase(unittest.TestCase):
+ def setUp(self):
+ self.tmpdir = self.mktemp()
+ os.mkdir(self.tmpdir)
+ self.messageFiles = []
+ for i in range(10):
+ name = os.path.join(self.tmpdir, 'body-%d' % (i,))
+ f = file(name + '-H', 'w')
+ pickle.dump(['from-%d' % (i,), 'to-%d' % (i,)], f)
+ f.close()
+
+ f = file(name + '-D', 'w')
+ f.write(name)
+ f.seek(0, 0)
+ self.messageFiles.append(name)
+
+ self.R = mail.relay.RelayerMixin()
+ self.R.loadMessages(self.messageFiles)
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+ def testMailFrom(self):
+ for i in range(10):
+ self.assertEqual(self.R.getMailFrom(), 'from-%d' % (i,))
+ self.R.sentMail(250, None, None, None, None)
+ self.assertEqual(self.R.getMailFrom(), None)
+
+ def testMailTo(self):
+ for i in range(10):
+ self.assertEqual(self.R.getMailTo(), ['to-%d' % (i,)])
+ self.R.sentMail(250, None, None, None, None)
+ self.assertEqual(self.R.getMailTo(), None)
+
+ def testMailData(self):
+ for i in range(10):
+ name = os.path.join(self.tmpdir, 'body-%d' % (i,))
+ self.assertEqual(self.R.getMailData().read(), name)
+ self.R.sentMail(250, None, None, None, None)
+ self.assertEqual(self.R.getMailData(), None)
+
+class Manager:
+ def __init__(self):
+ self.success = []
+ self.failure = []
+ self.done = []
+
+ def notifySuccess(self, factory, message):
+ self.success.append((factory, message))
+
+ def notifyFailure(self, factory, message):
+ self.failure.append((factory, message))
+
+ def notifyDone(self, factory):
+ self.done.append(factory)
+
+class ManagedRelayerTestCase(unittest.TestCase):
+ def setUp(self):
+ self.manager = Manager()
+ self.messages = range(0, 20, 2)
+ self.factory = object()
+ self.relay = mail.relaymanager.ManagedRelayerMixin(self.manager)
+ self.relay.messages = self.messages[:]
+ self.relay.names = self.messages[:]
+ self.relay.factory = self.factory
+
+ def testSuccessfulSentMail(self):
+ for i in self.messages:
+ self.relay.sentMail(250, None, None, None, None)
+
+ self.assertEqual(
+ self.manager.success,
+ [(self.factory, m) for m in self.messages]
+ )
+
+ def testFailedSentMail(self):
+ for i in self.messages:
+ self.relay.sentMail(550, None, None, None, None)
+
+ self.assertEqual(
+ self.manager.failure,
+ [(self.factory, m) for m in self.messages]
+ )
+
+ def testConnectionLost(self):
+ self.relay.connectionLost(failure.Failure(Exception()))
+ self.assertEqual(self.manager.done, [self.factory])
+
+class DirectoryQueueTestCase(unittest.TestCase):
+ def setUp(self):
+ # This is almost a test case itself.
+ self.tmpdir = self.mktemp()
+ os.mkdir(self.tmpdir)
+ self.queue = mail.relaymanager.Queue(self.tmpdir)
+ self.queue.noisy = False
+ for m in range(25):
+ hdrF, msgF = self.queue.createNewMessage()
+ pickle.dump(['header', m], hdrF)
+ hdrF.close()
+ msgF.lineReceived('body: %d' % (m,))
+ msgF.eomReceived()
+ self.queue.readDirectory()
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+ def testWaiting(self):
+ self.failUnless(self.queue.hasWaiting())
+ self.assertEqual(len(self.queue.getWaiting()), 25)
+
+ waiting = self.queue.getWaiting()
+ self.queue.setRelaying(waiting[0])
+ self.assertEqual(len(self.queue.getWaiting()), 24)
+
+ self.queue.setWaiting(waiting[0])
+ self.assertEqual(len(self.queue.getWaiting()), 25)
+
+ def testRelaying(self):
+ for m in self.queue.getWaiting():
+ self.queue.setRelaying(m)
+ self.assertEqual(
+ len(self.queue.getRelayed()),
+ 25 - len(self.queue.getWaiting())
+ )
+
+ self.failIf(self.queue.hasWaiting())
+
+ relayed = self.queue.getRelayed()
+ self.queue.setWaiting(relayed[0])
+ self.assertEqual(len(self.queue.getWaiting()), 1)
+ self.assertEqual(len(self.queue.getRelayed()), 24)
+
+ def testDone(self):
+ msg = self.queue.getWaiting()[0]
+ self.queue.setRelaying(msg)
+ self.queue.done(msg)
+
+ self.assertEqual(len(self.queue.getWaiting()), 24)
+ self.assertEqual(len(self.queue.getRelayed()), 0)
+
+ self.failIf(msg in self.queue.getWaiting())
+ self.failIf(msg in self.queue.getRelayed())
+
+ def testEnvelope(self):
+ envelopes = []
+
+ for msg in self.queue.getWaiting():
+ envelopes.append(self.queue.getEnvelope(msg))
+
+ envelopes.sort()
+ for i in range(25):
+ self.assertEqual(
+ envelopes.pop(0),
+ ['header', i]
+ )
+
+from twisted.names import server
+from twisted.names import client
+from twisted.names import common
+
+class TestAuthority(common.ResolverBase):
+ def __init__(self):
+ common.ResolverBase.__init__(self)
+ self.addresses = {}
+
+ def _lookup(self, name, cls, type, timeout = None):
+ if name in self.addresses and type == dns.MX:
+ results = []
+ for a in self.addresses[name]:
+ hdr = dns.RRHeader(
+ name, dns.MX, dns.IN, 60, dns.Record_MX(0, a)
+ )
+ results.append(hdr)
+ return defer.succeed((results, [], []))
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+def setUpDNS(self):
+ self.auth = TestAuthority()
+ factory = server.DNSServerFactory([self.auth])
+ protocol = dns.DNSDatagramProtocol(factory)
+ while 1:
+ self.port = reactor.listenTCP(0, factory, interface='127.0.0.1')
+ portNumber = self.port.getHost().port
+
+ try:
+ self.udpPort = reactor.listenUDP(portNumber, protocol, interface='127.0.0.1')
+ except CannotListenError:
+ self.port.stopListening()
+ else:
+ break
+ self.resolver = client.Resolver(servers=[('127.0.0.1', portNumber)])
+
+
+def tearDownDNS(self):
+ dl = []
+ dl.append(defer.maybeDeferred(self.port.stopListening))
+ dl.append(defer.maybeDeferred(self.udpPort.stopListening))
+ if self.resolver.protocol.transport is not None:
+ dl.append(defer.maybeDeferred(self.resolver.protocol.transport.stopListening))
+ try:
+ self.resolver._parseCall.cancel()
+ except:
+ pass
+ return defer.DeferredList(dl)
+
+class MXTestCase(unittest.TestCase):
+ """
+ Tests for L{mail.relaymanager.MXCalculator}.
+ """
+ def setUp(self):
+ setUpDNS(self)
+ self.clock = task.Clock()
+ self.mx = mail.relaymanager.MXCalculator(self.resolver, self.clock)
+
+ def tearDown(self):
+ return tearDownDNS(self)
+
+
+ def test_defaultClock(self):
+ """
+ L{MXCalculator}'s default clock is C{twisted.internet.reactor}.
+ """
+ self.assertIdentical(
+ mail.relaymanager.MXCalculator(self.resolver).clock,
+ reactor)
+
+
+ def testSimpleSuccess(self):
+ self.auth.addresses['test.domain'] = ['the.email.test.domain']
+ return self.mx.getMX('test.domain').addCallback(self._cbSimpleSuccess)
+
+ def _cbSimpleSuccess(self, mx):
+ self.assertEqual(mx.preference, 0)
+ self.assertEqual(str(mx.name), 'the.email.test.domain')
+
+ def testSimpleFailure(self):
+ self.mx.fallbackToDomain = False
+ return self.assertFailure(self.mx.getMX('test.domain'), IOError)
+
+ def testSimpleFailureWithFallback(self):
+ return self.assertFailure(self.mx.getMX('test.domain'), DNSLookupError)
+
+
+ def _exchangeTest(self, domain, records, correctMailExchange):
+ """
+ Issue an MX request for the given domain and arrange for it to be
+ responded to with the given records. Verify that the resulting mail
+ exchange is the indicated host.
+
+ @type domain: C{str}
+ @type records: C{list} of L{RRHeader}
+ @type correctMailExchange: C{str}
+ @rtype: L{Deferred}
+ """
+ class DummyResolver(object):
+ def lookupMailExchange(self, name):
+ if name == domain:
+ return defer.succeed((
+ records,
+ [],
+ []))
+ return defer.fail(DNSNameError(domain))
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX(domain)
+ def gotMailExchange(record):
+ self.assertEqual(str(record.name), correctMailExchange)
+ d.addCallback(gotMailExchange)
+ return d
+
+
+ def test_mailExchangePreference(self):
+ """
+ The MX record with the lowest preference is returned by
+ L{MXCalculator.getMX}.
+ """
+ domain = "example.com"
+ good = "good.example.com"
+ bad = "bad.example.com"
+
+ records = [
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(1, bad)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, good)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(2, bad))]
+ return self._exchangeTest(domain, records, good)
+
+
+ def test_badExchangeExcluded(self):
+ """
+ L{MXCalculator.getMX} returns the MX record with the lowest preference
+ which is not also marked as bad.
+ """
+ domain = "example.com"
+ good = "good.example.com"
+ bad = "bad.example.com"
+
+ records = [
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, bad)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(1, good))]
+ self.mx.markBad(bad)
+ return self._exchangeTest(domain, records, good)
+
+
+ def test_fallbackForAllBadExchanges(self):
+ """
+ L{MXCalculator.getMX} returns the MX record with the lowest preference
+ if all the MX records in the response have been marked bad.
+ """
+ domain = "example.com"
+ bad = "bad.example.com"
+ worse = "worse.example.com"
+
+ records = [
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, bad)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(1, worse))]
+ self.mx.markBad(bad)
+ self.mx.markBad(worse)
+ return self._exchangeTest(domain, records, bad)
+
+
+ def test_badExchangeExpires(self):
+ """
+ L{MXCalculator.getMX} returns the MX record with the lowest preference
+ if it was last marked bad longer than L{MXCalculator.timeOutBadMX}
+ seconds ago.
+ """
+ domain = "example.com"
+ good = "good.example.com"
+ previouslyBad = "bad.example.com"
+
+ records = [
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, previouslyBad)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(1, good))]
+ self.mx.markBad(previouslyBad)
+ self.clock.advance(self.mx.timeOutBadMX)
+ return self._exchangeTest(domain, records, previouslyBad)
+
+
+ def test_goodExchangeUsed(self):
+ """
+ L{MXCalculator.getMX} returns the MX record with the lowest preference
+ if it was marked good after it was marked bad.
+ """
+ domain = "example.com"
+ good = "good.example.com"
+ previouslyBad = "bad.example.com"
+
+ records = [
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, previouslyBad)),
+ RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(1, good))]
+ self.mx.markBad(previouslyBad)
+ self.mx.markGood(previouslyBad)
+ self.clock.advance(self.mx.timeOutBadMX)
+ return self._exchangeTest(domain, records, previouslyBad)
+
+
+ def test_successWithoutResults(self):
+ """
+ If an MX lookup succeeds but the result set is empty,
+ L{MXCalculator.getMX} should try to look up an I{A} record for the
+ requested name and call back its returned Deferred with that
+ address.
+ """
+ ip = '1.2.3.4'
+ domain = 'example.org'
+
+ class DummyResolver(object):
+ """
+ Fake resolver which will respond to an MX lookup with an empty
+ result set.
+
+ @ivar mx: A dictionary mapping hostnames to three-tuples of
+ results to be returned from I{MX} lookups.
+
+ @ivar a: A dictionary mapping hostnames to addresses to be
+ returned from I{A} lookups.
+ """
+ mx = {domain: ([], [], [])}
+ a = {domain: ip}
+
+ def lookupMailExchange(self, domain):
+ return defer.succeed(self.mx[domain])
+
+ def getHostByName(self, domain):
+ return defer.succeed(self.a[domain])
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX(domain)
+ d.addCallback(self.assertEqual, Record_MX(name=ip))
+ return d
+
+
+ def test_failureWithSuccessfulFallback(self):
+ """
+ Test that if the MX record lookup fails, fallback is enabled, and an A
+ record is available for the name, then the Deferred returned by
+ L{MXCalculator.getMX} ultimately fires with a Record_MX instance which
+ gives the address in the A record for the name.
+ """
+ class DummyResolver(object):
+ """
+ Fake resolver which will fail an MX lookup but then succeed a
+ getHostByName call.
+ """
+ def lookupMailExchange(self, domain):
+ return defer.fail(DNSNameError())
+
+ def getHostByName(self, domain):
+ return defer.succeed("1.2.3.4")
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX("domain")
+ d.addCallback(self.assertEqual, Record_MX(name="1.2.3.4"))
+ return d
+
+
+ def test_cnameWithoutGlueRecords(self):
+ """
+ If an MX lookup returns a single CNAME record as a result, MXCalculator
+ will perform an MX lookup for the canonical name indicated and return
+ the MX record which results.
+ """
+ alias = "alias.example.com"
+ canonical = "canonical.example.com"
+ exchange = "mail.example.com"
+
+ class DummyResolver(object):
+ """
+ Fake resolver which will return a CNAME for an MX lookup of a name
+ which is an alias and an MX for an MX lookup of the canonical name.
+ """
+ def lookupMailExchange(self, domain):
+ if domain == alias:
+ return defer.succeed((
+ [RRHeader(name=domain,
+ type=Record_CNAME.TYPE,
+ payload=Record_CNAME(canonical))],
+ [], []))
+ elif domain == canonical:
+ return defer.succeed((
+ [RRHeader(name=domain,
+ type=Record_MX.TYPE,
+ payload=Record_MX(0, exchange))],
+ [], []))
+ else:
+ return defer.fail(DNSNameError(domain))
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX(alias)
+ d.addCallback(self.assertEqual, Record_MX(name=exchange))
+ return d
+
+
+ def test_cnameChain(self):
+ """
+ If L{MXCalculator.getMX} encounters a CNAME chain which is longer than
+ the length specified, the returned L{Deferred} should errback with
+ L{CanonicalNameChainTooLong}.
+ """
+ class DummyResolver(object):
+ """
+ Fake resolver which generates a CNAME chain of infinite length in
+ response to MX lookups.
+ """
+ chainCounter = 0
+
+ def lookupMailExchange(self, domain):
+ self.chainCounter += 1
+ name = 'x-%d.example.com' % (self.chainCounter,)
+ return defer.succeed((
+ [RRHeader(name=domain,
+ type=Record_CNAME.TYPE,
+ payload=Record_CNAME(name))],
+ [], []))
+
+ cnameLimit = 3
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX("mail.example.com", cnameLimit)
+ self.assertFailure(
+ d, twisted.mail.relaymanager.CanonicalNameChainTooLong)
+ def cbChainTooLong(error):
+ self.assertEqual(error.args[0], Record_CNAME("x-%d.example.com" % (cnameLimit + 1,)))
+ self.assertEqual(self.mx.resolver.chainCounter, cnameLimit + 1)
+ d.addCallback(cbChainTooLong)
+ return d
+
+
+ def test_cnameWithGlueRecords(self):
+ """
+ If an MX lookup returns a CNAME and the MX record for the CNAME, the
+ L{Deferred} returned by L{MXCalculator.getMX} should be called back
+ with the name from the MX record without further lookups being
+ attempted.
+ """
+ lookedUp = []
+ alias = "alias.example.com"
+ canonical = "canonical.example.com"
+ exchange = "mail.example.com"
+
+ class DummyResolver(object):
+ def lookupMailExchange(self, domain):
+ if domain != alias or lookedUp:
+ # Don't give back any results for anything except the alias
+ # or on any request after the first.
+ return ([], [], [])
+ return defer.succeed((
+ [RRHeader(name=alias,
+ type=Record_CNAME.TYPE,
+ payload=Record_CNAME(canonical)),
+ RRHeader(name=canonical,
+ type=Record_MX.TYPE,
+ payload=Record_MX(name=exchange))],
+ [], []))
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX(alias)
+ d.addCallback(self.assertEqual, Record_MX(name=exchange))
+ return d
+
+
+ def test_cnameLoopWithGlueRecords(self):
+ """
+ If an MX lookup returns two CNAME records which point to each other,
+ the loop should be detected and the L{Deferred} returned by
+ L{MXCalculator.getMX} should be errbacked with L{CanonicalNameLoop}.
+ """
+ firstAlias = "cname1.example.com"
+ secondAlias = "cname2.example.com"
+
+ class DummyResolver(object):
+ def lookupMailExchange(self, domain):
+ return defer.succeed((
+ [RRHeader(name=firstAlias,
+ type=Record_CNAME.TYPE,
+ payload=Record_CNAME(secondAlias)),
+ RRHeader(name=secondAlias,
+ type=Record_CNAME.TYPE,
+ payload=Record_CNAME(firstAlias))],
+ [], []))
+
+ self.mx.resolver = DummyResolver()
+ d = self.mx.getMX(firstAlias)
+ self.assertFailure(d, twisted.mail.relaymanager.CanonicalNameLoop)
+ return d
+
+
+ def testManyRecords(self):
+ self.auth.addresses['test.domain'] = [
+ 'mx1.test.domain', 'mx2.test.domain', 'mx3.test.domain'
+ ]
+ return self.mx.getMX('test.domain'
+ ).addCallback(self._cbManyRecordsSuccessfulLookup
+ )
+
+ def _cbManyRecordsSuccessfulLookup(self, mx):
+ self.failUnless(str(mx.name).split('.', 1)[0] in ('mx1', 'mx2', 'mx3'))
+ self.mx.markBad(str(mx.name))
+ return self.mx.getMX('test.domain'
+ ).addCallback(self._cbManyRecordsDifferentResult, mx
+ )
+
+ def _cbManyRecordsDifferentResult(self, nextMX, mx):
+ self.assertNotEqual(str(mx.name), str(nextMX.name))
+ self.mx.markBad(str(nextMX.name))
+
+ return self.mx.getMX('test.domain'
+ ).addCallback(self._cbManyRecordsLastResult, mx, nextMX
+ )
+
+ def _cbManyRecordsLastResult(self, lastMX, mx, nextMX):
+ self.assertNotEqual(str(mx.name), str(lastMX.name))
+ self.assertNotEqual(str(nextMX.name), str(lastMX.name))
+
+ self.mx.markBad(str(lastMX.name))
+ self.mx.markGood(str(nextMX.name))
+
+ return self.mx.getMX('test.domain'
+ ).addCallback(self._cbManyRecordsRepeatSpecificResult, nextMX
+ )
+
+ def _cbManyRecordsRepeatSpecificResult(self, againMX, nextMX):
+ self.assertEqual(str(againMX.name), str(nextMX.name))
+
+class LiveFireExercise(unittest.TestCase):
+ if interfaces.IReactorUDP(reactor, None) is None:
+ skip = "UDP support is required to determining MX records"
+
+ def setUp(self):
+ setUpDNS(self)
+ self.tmpdirs = [
+ 'domainDir', 'insertionDomain', 'insertionQueue',
+ 'destinationDomain', 'destinationQueue'
+ ]
+
+ def tearDown(self):
+ for d in self.tmpdirs:
+ if os.path.exists(d):
+ shutil.rmtree(d)
+ return tearDownDNS(self)
+
+ def testLocalDelivery(self):
+ service = mail.mail.MailService()
+ service.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess())
+ domain = mail.maildir.MaildirDirdbmDomain(service, 'domainDir')
+ domain.addUser('user', 'password')
+ service.addDomain('test.domain', domain)
+ service.portals[''] = service.portals['test.domain']
+ map(service.portals[''].registerChecker, domain.getCredentialsCheckers())
+
+ service.setQueue(mail.relay.DomainQueuer(service))
+ manager = mail.relaymanager.SmartHostSMTPRelayingManager(service.queue, None)
+ helper = mail.relaymanager.RelayStateHelper(manager, 1)
+
+ f = service.getSMTPFactory()
+
+ self.smtpServer = reactor.listenTCP(0, f, interface='127.0.0.1')
+
+ client = LineSendingProtocol([
+ 'HELO meson',
+ 'MAIL FROM: <user@hostname>',
+ 'RCPT TO: <user@test.domain>',
+ 'DATA',
+ 'This is the message',
+ '.',
+ 'QUIT'
+ ])
+
+ done = Deferred()
+ f = protocol.ClientFactory()
+ f.protocol = lambda: client
+ f.clientConnectionLost = lambda *args: done.callback(None)
+ reactor.connectTCP('127.0.0.1', self.smtpServer.getHost().port, f)
+
+ def finished(ign):
+ mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1]
+ msg = mbox.getMessage(0).read()
+ self.failIfEqual(msg.find('This is the message'), -1)
+
+ return self.smtpServer.stopListening()
+ done.addCallback(finished)
+ return done
+
+
+ def testRelayDelivery(self):
+ # Here is the service we will connect to and send mail from
+ insServ = mail.mail.MailService()
+ insServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess())
+ domain = mail.maildir.MaildirDirdbmDomain(insServ, 'insertionDomain')
+ insServ.addDomain('insertion.domain', domain)
+ os.mkdir('insertionQueue')
+ insServ.setQueue(mail.relaymanager.Queue('insertionQueue'))
+ insServ.domains.setDefaultDomain(mail.relay.DomainQueuer(insServ))
+ manager = mail.relaymanager.SmartHostSMTPRelayingManager(insServ.queue)
+ manager.fArgs += ('test.identity.hostname',)
+ helper = mail.relaymanager.RelayStateHelper(manager, 1)
+ # Yoink! Now the internet obeys OUR every whim!
+ manager.mxcalc = mail.relaymanager.MXCalculator(self.resolver)
+ # And this is our whim.
+ self.auth.addresses['destination.domain'] = ['127.0.0.1']
+
+ f = insServ.getSMTPFactory()
+ self.insServer = reactor.listenTCP(0, f, interface='127.0.0.1')
+
+ # Here is the service the previous one will connect to for final
+ # delivery
+ destServ = mail.mail.MailService()
+ destServ.smtpPortal.registerChecker(cred.checkers.AllowAnonymousAccess())
+ domain = mail.maildir.MaildirDirdbmDomain(destServ, 'destinationDomain')
+ domain.addUser('user', 'password')
+ destServ.addDomain('destination.domain', domain)
+ os.mkdir('destinationQueue')
+ destServ.setQueue(mail.relaymanager.Queue('destinationQueue'))
+ manager2 = mail.relaymanager.SmartHostSMTPRelayingManager(destServ.queue)
+ helper = mail.relaymanager.RelayStateHelper(manager, 1)
+ helper.startService()
+
+ f = destServ.getSMTPFactory()
+ self.destServer = reactor.listenTCP(0, f, interface='127.0.0.1')
+
+ # Update the port number the *first* relay will connect to, because we can't use
+ # port 25
+ manager.PORT = self.destServer.getHost().port
+
+ client = LineSendingProtocol([
+ 'HELO meson',
+ 'MAIL FROM: <user@wherever>',
+ 'RCPT TO: <user@destination.domain>',
+ 'DATA',
+ 'This is the message',
+ '.',
+ 'QUIT'
+ ])
+
+ done = Deferred()
+ f = protocol.ClientFactory()
+ f.protocol = lambda: client
+ f.clientConnectionLost = lambda *args: done.callback(None)
+ reactor.connectTCP('127.0.0.1', self.insServer.getHost().port, f)
+
+ def finished(ign):
+ # First part of the delivery is done. Poke the queue manually now
+ # so we don't have to wait for the queue to be flushed.
+ delivery = manager.checkState()
+ def delivered(ign):
+ mbox = domain.requestAvatar('user', None, pop3.IMailbox)[1]
+ msg = mbox.getMessage(0).read()
+ self.failIfEqual(msg.find('This is the message'), -1)
+
+ self.insServer.stopListening()
+ self.destServer.stopListening()
+ helper.stopService()
+ delivery.addCallback(delivered)
+ return delivery
+ done.addCallback(finished)
+ return done
+
+
+aliasFile = StringIO.StringIO("""\
+# Here's a comment
+ # woop another one
+testuser: address1,address2, address3,
+ continuation@address, |/bin/process/this
+
+usertwo:thisaddress,thataddress, lastaddress
+lastuser: :/includable, /filename, |/program, address
+""")
+
+class LineBufferMessage:
+ def __init__(self):
+ self.lines = []
+ self.eom = False
+ self.lost = False
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def eomReceived(self):
+ self.eom = True
+ return defer.succeed('<Whatever>')
+
+ def connectionLost(self):
+ self.lost = True
+
+class AliasTestCase(unittest.TestCase):
+ lines = [
+ 'First line',
+ 'Next line',
+ '',
+ 'After a blank line',
+ 'Last line'
+ ]
+
+ def setUp(self):
+ aliasFile.seek(0)
+
+ def testHandle(self):
+ result = {}
+ lines = [
+ 'user: another@host\n',
+ 'nextuser: |/bin/program\n',
+ 'user: me@again\n',
+ 'moreusers: :/etc/include/filename\n',
+ 'multiuser: first@host, second@host,last@anotherhost',
+ ]
+
+ for l in lines:
+ mail.alias.handle(result, l, 'TestCase', None)
+
+ self.assertEqual(result['user'], ['another@host', 'me@again'])
+ self.assertEqual(result['nextuser'], ['|/bin/program'])
+ self.assertEqual(result['moreusers'], [':/etc/include/filename'])
+ self.assertEqual(result['multiuser'], ['first@host', 'second@host', 'last@anotherhost'])
+
+ def testFileLoader(self):
+ domains = {'': object()}
+ result = mail.alias.loadAliasFile(domains, fp=aliasFile)
+
+ self.assertEqual(len(result), 3)
+
+ group = result['testuser']
+ s = str(group)
+ for a in ('address1', 'address2', 'address3', 'continuation@address', '/bin/process/this'):
+ self.failIfEqual(s.find(a), -1)
+ self.assertEqual(len(group), 5)
+
+ group = result['usertwo']
+ s = str(group)
+ for a in ('thisaddress', 'thataddress', 'lastaddress'):
+ self.failIfEqual(s.find(a), -1)
+ self.assertEqual(len(group), 3)
+
+ group = result['lastuser']
+ s = str(group)
+ self.assertEqual(s.find('/includable'), -1)
+ for a in ('/filename', 'program', 'address'):
+ self.failIfEqual(s.find(a), -1, '%s not found' % a)
+ self.assertEqual(len(group), 3)
+
+ def testMultiWrapper(self):
+ msgs = LineBufferMessage(), LineBufferMessage(), LineBufferMessage()
+ msg = mail.alias.MultiWrapper(msgs)
+
+ for L in self.lines:
+ msg.lineReceived(L)
+ return msg.eomReceived().addCallback(self._cbMultiWrapper, msgs)
+
+ def _cbMultiWrapper(self, ignored, msgs):
+ for m in msgs:
+ self.failUnless(m.eom)
+ self.failIf(m.lost)
+ self.assertEqual(self.lines, m.lines)
+
+ def testFileAlias(self):
+ tmpfile = self.mktemp()
+ a = mail.alias.FileAlias(tmpfile, None, None)
+ m = a.createMessageReceiver()
+
+ for l in self.lines:
+ m.lineReceived(l)
+ return m.eomReceived().addCallback(self._cbTestFileAlias, tmpfile)
+
+ def _cbTestFileAlias(self, ignored, tmpfile):
+ lines = file(tmpfile).readlines()
+ self.assertEqual([L[:-1] for L in lines], self.lines)
+
+
+
+class DummyProcess(object):
+ __slots__ = ['onEnd']
+
+
+
+class MockProcessAlias(mail.alias.ProcessAlias):
+ """
+ A alias processor that doesn't actually launch processes.
+ """
+
+ def spawnProcess(self, proto, program, path):
+ """
+ Don't spawn a process.
+ """
+
+
+
+class MockAliasGroup(mail.alias.AliasGroup):
+ """
+ An alias group using C{MockProcessAlias}.
+ """
+ processAliasFactory = MockProcessAlias
+
+
+
+class StubProcess(object):
+ """
+ Fake implementation of L{IProcessTransport}.
+
+ @ivar signals: A list of all the signals which have been sent to this fake
+ process.
+ """
+ def __init__(self):
+ self.signals = []
+
+
+ def loseConnection(self):
+ """
+ No-op implementation of disconnection.
+ """
+
+
+ def signalProcess(self, signal):
+ """
+ Record a signal sent to this process for later inspection.
+ """
+ self.signals.append(signal)
+
+
+
+class ProcessAliasTestCase(unittest.TestCase):
+ """
+ Tests for alias resolution.
+ """
+ if interfaces.IReactorProcess(reactor, None) is None:
+ skip = "IReactorProcess not supported"
+
+ lines = [
+ 'First line',
+ 'Next line',
+ '',
+ 'After a blank line',
+ 'Last line'
+ ]
+
+ def exitStatus(self, code):
+ """
+ Construct a status from the given exit code.
+
+ @type code: L{int} between 0 and 255 inclusive.
+ @param code: The exit status which the code will represent.
+
+ @rtype: L{int}
+ @return: A status integer for the given exit code.
+ """
+ # /* Macros for constructing status values. */
+ # #define __W_EXITCODE(ret, sig) ((ret) << 8 | (sig))
+ status = (code << 8) | 0
+
+ # Sanity check
+ self.assertTrue(os.WIFEXITED(status))
+ self.assertEqual(os.WEXITSTATUS(status), code)
+ self.assertFalse(os.WIFSIGNALED(status))
+
+ return status
+
+
+ def signalStatus(self, signal):
+ """
+ Construct a status from the given signal.
+
+ @type signal: L{int} between 0 and 255 inclusive.
+ @param signal: The signal number which the status will represent.
+
+ @rtype: L{int}
+ @return: A status integer for the given signal.
+ """
+ # /* If WIFSIGNALED(STATUS), the terminating signal. */
+ # #define __WTERMSIG(status) ((status) & 0x7f)
+ # /* Nonzero if STATUS indicates termination by a signal. */
+ # #define __WIFSIGNALED(status) \
+ # (((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
+ status = signal
+
+ # Sanity check
+ self.assertTrue(os.WIFSIGNALED(status))
+ self.assertEqual(os.WTERMSIG(status), signal)
+ self.assertFalse(os.WIFEXITED(status))
+
+ return status
+
+
+ def setUp(self):
+ """
+ Replace L{smtp.DNSNAME} with a well-known value.
+ """
+ self.DNSNAME = smtp.DNSNAME
+ smtp.DNSNAME = ''
+
+
+ def tearDown(self):
+ """
+ Restore the original value of L{smtp.DNSNAME}.
+ """
+ smtp.DNSNAME = self.DNSNAME
+
+
+ def test_processAlias(self):
+ """
+ Standard call to C{mail.alias.ProcessAlias}: check that the specified
+ script is called, and that the input is correctly transferred to it.
+ """
+ sh = FilePath(self.mktemp())
+ sh.setContent("""\
+#!/bin/sh
+rm -f process.alias.out
+while read i; do
+ echo $i >> process.alias.out
+done""")
+ os.chmod(sh.path, 0700)
+ a = mail.alias.ProcessAlias(sh.path, None, None)
+ m = a.createMessageReceiver()
+
+ for l in self.lines:
+ m.lineReceived(l)
+
+ def _cbProcessAlias(ignored):
+ lines = file('process.alias.out').readlines()
+ self.assertEqual([L[:-1] for L in lines], self.lines)
+
+ return m.eomReceived().addCallback(_cbProcessAlias)
+
+
+ def test_processAliasTimeout(self):
+ """
+ If the alias child process does not exit within a particular period of
+ time, the L{Deferred} returned by L{MessageWrapper.eomReceived} should
+ fail with L{ProcessAliasTimeout} and send the I{KILL} signal to the
+ child process..
+ """
+ reactor = task.Clock()
+ transport = StubProcess()
+ proto = mail.alias.ProcessAliasProtocol()
+ proto.makeConnection(transport)
+
+ receiver = mail.alias.MessageWrapper(proto, None, reactor)
+ d = receiver.eomReceived()
+ reactor.advance(receiver.completionTimeout)
+ def timedOut(ignored):
+ self.assertEqual(transport.signals, ['KILL'])
+ # Now that it has been killed, disconnect the protocol associated
+ # with it.
+ proto.processEnded(
+ ProcessTerminated(self.signalStatus(signal.SIGKILL)))
+ self.assertFailure(d, mail.alias.ProcessAliasTimeout)
+ d.addCallback(timedOut)
+ return d
+
+
+ def test_earlyProcessTermination(self):
+ """
+ If the process associated with an L{mail.alias.MessageWrapper} exits
+ before I{eomReceived} is called, the L{Deferred} returned by
+ I{eomReceived} should fail.
+ """
+ transport = StubProcess()
+ protocol = mail.alias.ProcessAliasProtocol()
+ protocol.makeConnection(transport)
+ receiver = mail.alias.MessageWrapper(protocol, None, None)
+ protocol.processEnded(failure.Failure(ProcessDone(0)))
+ return self.assertFailure(receiver.eomReceived(), ProcessDone)
+
+
+ def _terminationTest(self, status):
+ """
+ Verify that if the process associated with an
+ L{mail.alias.MessageWrapper} exits with the given status, the
+ L{Deferred} returned by I{eomReceived} fails with L{ProcessTerminated}.
+ """
+ transport = StubProcess()
+ protocol = mail.alias.ProcessAliasProtocol()
+ protocol.makeConnection(transport)
+ receiver = mail.alias.MessageWrapper(protocol, None, None)
+ protocol.processEnded(
+ failure.Failure(ProcessTerminated(status)))
+ return self.assertFailure(receiver.eomReceived(), ProcessTerminated)
+
+
+ def test_errorProcessTermination(self):
+ """
+ If the process associated with an L{mail.alias.MessageWrapper} exits
+ with a non-zero exit code, the L{Deferred} returned by I{eomReceived}
+ should fail.
+ """
+ return self._terminationTest(self.exitStatus(1))
+
+
+ def test_signalProcessTermination(self):
+ """
+ If the process associated with an L{mail.alias.MessageWrapper} exits
+ because it received a signal, the L{Deferred} returned by
+ I{eomReceived} should fail.
+ """
+ return self._terminationTest(self.signalStatus(signal.SIGHUP))
+
+
+ def test_aliasResolution(self):
+ """
+ Check that the C{resolve} method of alias processors produce the correct
+ set of objects:
+ - direct alias with L{mail.alias.AddressAlias} if a simple input is passed
+ - aliases in a file with L{mail.alias.FileWrapper} if an input in the format
+ '/file' is given
+ - aliases resulting of a process call wrapped by L{mail.alias.MessageWrapper}
+ if the format is '|process'
+ """
+ aliases = {}
+ domain = {'': TestDomain(aliases, ['user1', 'user2', 'user3'])}
+ A1 = MockAliasGroup(['user1', '|echo', '/file'], domain, 'alias1')
+ A2 = MockAliasGroup(['user2', 'user3'], domain, 'alias2')
+ A3 = mail.alias.AddressAlias('alias1', domain, 'alias3')
+ aliases.update({
+ 'alias1': A1,
+ 'alias2': A2,
+ 'alias3': A3,
+ })
+
+ res1 = A1.resolve(aliases)
+ r1 = map(str, res1.objs)
+ r1.sort()
+ expected = map(str, [
+ mail.alias.AddressAlias('user1', None, None),
+ mail.alias.MessageWrapper(DummyProcess(), 'echo'),
+ mail.alias.FileWrapper('/file'),
+ ])
+ expected.sort()
+ self.assertEqual(r1, expected)
+
+ res2 = A2.resolve(aliases)
+ r2 = map(str, res2.objs)
+ r2.sort()
+ expected = map(str, [
+ mail.alias.AddressAlias('user2', None, None),
+ mail.alias.AddressAlias('user3', None, None)
+ ])
+ expected.sort()
+ self.assertEqual(r2, expected)
+
+ res3 = A3.resolve(aliases)
+ r3 = map(str, res3.objs)
+ r3.sort()
+ expected = map(str, [
+ mail.alias.AddressAlias('user1', None, None),
+ mail.alias.MessageWrapper(DummyProcess(), 'echo'),
+ mail.alias.FileWrapper('/file'),
+ ])
+ expected.sort()
+ self.assertEqual(r3, expected)
+
+
+ def test_cyclicAlias(self):
+ """
+ Check that a cycle in alias resolution is correctly handled.
+ """
+ aliases = {}
+ domain = {'': TestDomain(aliases, [])}
+ A1 = mail.alias.AddressAlias('alias2', domain, 'alias1')
+ A2 = mail.alias.AddressAlias('alias3', domain, 'alias2')
+ A3 = mail.alias.AddressAlias('alias1', domain, 'alias3')
+ aliases.update({
+ 'alias1': A1,
+ 'alias2': A2,
+ 'alias3': A3
+ })
+
+ self.assertEqual(aliases['alias1'].resolve(aliases), None)
+ self.assertEqual(aliases['alias2'].resolve(aliases), None)
+ self.assertEqual(aliases['alias3'].resolve(aliases), None)
+
+ A4 = MockAliasGroup(['|echo', 'alias1'], domain, 'alias4')
+ aliases['alias4'] = A4
+
+ res = A4.resolve(aliases)
+ r = map(str, res.objs)
+ r.sort()
+ expected = map(str, [
+ mail.alias.MessageWrapper(DummyProcess(), 'echo')
+ ])
+ expected.sort()
+ self.assertEqual(r, expected)
+
+
+
+
+
+
+class TestDomain:
+ def __init__(self, aliases, users):
+ self.aliases = aliases
+ self.users = users
+
+ def exists(self, user, memo=None):
+ user = user.dest.local
+ if user in self.users:
+ return lambda: mail.alias.AddressAlias(user, None, None)
+ try:
+ a = self.aliases[user]
+ except:
+ raise smtp.SMTPBadRcpt(user)
+ else:
+ aliases = a.resolve(self.aliases, memo)
+ if aliases:
+ return lambda: aliases
+ raise smtp.SMTPBadRcpt(user)
+
+
+from twisted.python.runtime import platformType
+import types
+if platformType != "posix":
+ for o in locals().values():
+ if isinstance(o, (types.ClassType, type)) and issubclass(o, unittest.TestCase):
+ o.skip = "twisted.mail only works on posix"
diff --git a/twisted/mail/test/test_mailmail.py b/twisted/mail/test/test_mailmail.py
new file mode 100644
index 0000000..8b9e4d8
--- /dev/null
+++ b/twisted/mail/test/test_mailmail.py
@@ -0,0 +1,75 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.mail.scripts.mailmail}, the implementation of the
+command line program I{mailmail}.
+"""
+
+import sys
+from StringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+from twisted.mail.scripts.mailmail import parseOptions
+
+
+class OptionsTests(TestCase):
+ """
+ Tests for L{parseOptions} which parses command line arguments and reads
+ message text from stdin to produce an L{Options} instance which can be
+ used to send a message.
+ """
+ def test_unspecifiedRecipients(self):
+ """
+ If no recipients are given in the argument list and there is no
+ recipient header in the message text, L{parseOptions} raises
+ L{SystemExit} with a string describing the problem.
+ """
+ self.addCleanup(setattr, sys, 'stdin', sys.stdin)
+ sys.stdin = StringIO(
+ 'Subject: foo\n'
+ '\n'
+ 'Hello, goodbye.\n')
+ exc = self.assertRaises(SystemExit, parseOptions, [])
+ self.assertEqual(exc.args, ('No recipients specified.',))
+
+
+ def test_listQueueInformation(self):
+ """
+ The I{-bp} option for listing queue information is unsupported and
+ if it is passed to L{parseOptions}, L{SystemExit} is raised.
+ """
+ exc = self.assertRaises(SystemExit, parseOptions, ['-bp'])
+ self.assertEqual(exc.args, ("Unsupported option.",))
+
+
+ def test_stdioTransport(self):
+ """
+ The I{-bs} option for using stdin and stdout as the SMTP transport
+ is unsupported and if it is passed to L{parseOptions}, L{SystemExit}
+ is raised.
+ """
+ exc = self.assertRaises(SystemExit, parseOptions, ['-bs'])
+ self.assertEqual(exc.args, ("Unsupported option.",))
+
+
+ def test_ignoreFullStop(self):
+ """
+ The I{-i} and I{-oi} options for ignoring C{"."} by itself on a line
+ are unsupported and if either is passed to L{parseOptions},
+ L{SystemExit} is raised.
+ """
+ exc = self.assertRaises(SystemExit, parseOptions, ['-i'])
+ self.assertEqual(exc.args, ("Unsupported option.",))
+ exc = self.assertRaises(SystemExit, parseOptions, ['-oi'])
+ self.assertEqual(exc.args, ("Unsupported option.",))
+
+
+ def test_copyAliasedSender(self):
+ """
+ The I{-om} option for copying the sender if they appear in an alias
+ expansion is unsupported and if it is passed to L{parseOptions},
+ L{SystemExit} is raised.
+ """
+ exc = self.assertRaises(SystemExit, parseOptions, ['-om'])
+ self.assertEqual(exc.args, ("Unsupported option.",))
diff --git a/twisted/mail/test/test_options.py b/twisted/mail/test/test_options.py
new file mode 100644
index 0000000..a74c67c
--- /dev/null
+++ b/twisted/mail/test/test_options.py
@@ -0,0 +1,255 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.mail.tap}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.usage import UsageError
+from twisted.mail import protocols
+from twisted.mail.tap import Options, makeService
+from twisted.python import deprecate, versions
+from twisted.python.filepath import FilePath
+from twisted.internet import endpoints, defer
+
+
+class OptionsTestCase(TestCase):
+ """
+ Tests for the command line option parser used for I{twistd mail}.
+ """
+ def setUp(self):
+ self.aliasFilename = self.mktemp()
+ aliasFile = file(self.aliasFilename, 'w')
+ aliasFile.write('someuser:\tdifferentuser\n')
+ aliasFile.close()
+
+
+ def testAliasesWithoutDomain(self):
+ """
+ Test that adding an aliases(5) file before adding a domain raises a
+ UsageError.
+ """
+ self.assertRaises(
+ UsageError,
+ Options().parseOptions,
+ ['--aliases', self.aliasFilename])
+
+
+ def testAliases(self):
+ """
+ Test that adding an aliases(5) file to an IAliasableDomain at least
+ doesn't raise an unhandled exception.
+ """
+ Options().parseOptions([
+ '--maildirdbmdomain', 'example.com=example.com',
+ '--aliases', self.aliasFilename])
+
+
+ def testPasswordfileDeprecation(self):
+ """
+ Test that the --passwordfile option will emit a correct warning.
+ """
+ passwd = FilePath(self.mktemp())
+ passwd.setContent("")
+ options = Options()
+ options.opt_passwordfile(passwd.path)
+ warnings = self.flushWarnings([self.testPasswordfileDeprecation])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(len(warnings), 1)
+ msg = deprecate.getDeprecationWarningString(options.opt_passwordfile,
+ versions.Version('twisted.mail', 11, 0, 0))
+ self.assertEqual(warnings[0]['message'], msg)
+
+
+ def test_barePort(self):
+ """
+ A bare port passed to I{--pop3} results in deprecation warning in
+ addition to a TCP4ServerEndpoint.
+ """
+ options = Options()
+ options.parseOptions(['--pop3', '8110'])
+ self.assertEqual(len(options['pop3']), 1)
+ self.assertIsInstance(
+ options['pop3'][0], endpoints.TCP4ServerEndpoint)
+ warnings = self.flushWarnings([options.opt_pop3])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Specifying plain ports and/or a certificate is deprecated since "
+ "Twisted 11.0; use endpoint descriptions instead.")
+
+
+ def _endpointTest(self, service):
+ """
+ Use L{Options} to parse a single service configuration parameter and
+ verify that an endpoint of the correct type is added to the list for
+ that service.
+ """
+ options = Options()
+ options.parseOptions(['--' + service, 'tcp:1234'])
+ self.assertEqual(len(options[service]), 1)
+ self.assertIsInstance(
+ options[service][0], endpoints.TCP4ServerEndpoint)
+
+
+ def test_endpointSMTP(self):
+ """
+ When I{--smtp} is given a TCP endpoint description as an argument, a
+ TCPServerEndpoint is added to the list of SMTP endpoints.
+ """
+ self._endpointTest('smtp')
+
+
+ def test_endpointPOP3(self):
+ """
+ When I{--pop3} is given a TCP endpoint description as an argument, a
+ TCPServerEndpoint is added to the list of POP3 endpoints.
+ """
+ self._endpointTest('pop3')
+
+
+ def test_protoDefaults(self):
+ """
+ POP3 and SMTP each listen on a TCP4ServerEndpoint by default.
+ """
+ options = Options()
+ options.parseOptions([])
+
+ self.assertEqual(len(options['pop3']), 1)
+ self.assertIsInstance(
+ options['pop3'][0], endpoints.TCP4ServerEndpoint)
+
+ self.assertEqual(len(options['smtp']), 1)
+ self.assertIsInstance(
+ options['smtp'][0], endpoints.TCP4ServerEndpoint)
+
+
+ def test_protoDisable(self):
+ """
+ The I{--no-pop3} and I{--no-smtp} options disable POP3 and SMTP
+ respectively.
+ """
+ options = Options()
+ options.parseOptions(['--no-pop3'])
+ self.assertEqual(options._getEndpoints(None, 'pop3'), [])
+ self.assertNotEquals(options._getEndpoints(None, 'smtp'), [])
+
+ options = Options()
+ options.parseOptions(['--no-smtp'])
+ self.assertNotEquals(options._getEndpoints(None, 'pop3'), [])
+ self.assertEqual(options._getEndpoints(None, 'smtp'), [])
+
+
+ def test_allProtosDisabledError(self):
+ """
+ If all protocols are disabled, L{UsageError} is raised.
+ """
+ options = Options()
+ self.assertRaises(
+ UsageError, options.parseOptions, (['--no-pop3', '--no-smtp']))
+
+
+ def test_pop3sBackwardCompatibility(self):
+ """
+ The deprecated I{--pop3s} and I{--certificate} options set up a POP3 SSL
+ server.
+ """
+ cert = FilePath(self.mktemp())
+ cert.setContent("")
+ options = Options()
+ options.parseOptions(['--pop3s', '8995',
+ '--certificate', cert.path])
+ self.assertEqual(len(options['pop3']), 2)
+ self.assertIsInstance(
+ options['pop3'][0], endpoints.SSL4ServerEndpoint)
+ self.assertIsInstance(
+ options['pop3'][1], endpoints.TCP4ServerEndpoint)
+
+ warnings = self.flushWarnings([options.postOptions])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Specifying plain ports and/or a certificate is deprecated since "
+ "Twisted 11.0; use endpoint descriptions instead.")
+
+
+ def test_esmtpWithoutHostname(self):
+ """
+ If I{--esmtp} is given without I{--hostname}, L{Options.parseOptions}
+ raises L{UsageError}.
+ """
+ options = Options()
+ exc = self.assertRaises(UsageError, options.parseOptions, ['--esmtp'])
+ self.assertEqual("--esmtp requires --hostname", str(exc))
+
+
+ def test_auth(self):
+ """
+ Tests that the --auth option registers a checker.
+ """
+ options = Options()
+ options.parseOptions(['--auth', 'memory:admin:admin:bob:password'])
+ self.assertEqual(len(options['credCheckers']), 1)
+ checker = options['credCheckers'][0]
+ interfaces = checker.credentialInterfaces
+ registered_checkers = options.service.smtpPortal.checkers
+ for iface in interfaces:
+ self.assertEqual(checker, registered_checkers[iface])
+
+
+
+class SpyEndpoint(object):
+ """
+ SpyEndpoint remembers what factory it is told to listen with.
+ """
+ listeningWith = None
+ def listen(self, factory):
+ self.listeningWith = factory
+ return defer.succeed(None)
+
+
+
+class MakeServiceTests(TestCase):
+ """
+ Tests for L{twisted.mail.tap.makeService}
+ """
+ def _endpointServerTest(self, key, factoryClass):
+ """
+ Configure a service with two endpoints for the protocol associated with
+ C{key} and verify that when the service is started a factory of type
+ C{factoryClass} is used to listen on each of them.
+ """
+ cleartext = SpyEndpoint()
+ secure = SpyEndpoint()
+ config = Options()
+ config[key] = [cleartext, secure]
+ service = makeService(config)
+ service.privilegedStartService()
+ service.startService()
+ self.addCleanup(service.stopService)
+ self.assertIsInstance(cleartext.listeningWith, factoryClass)
+ self.assertIsInstance(secure.listeningWith, factoryClass)
+
+
+ def test_pop3(self):
+ """
+ If one or more endpoints is included in the configuration passed to
+ L{makeService} for the C{"pop3"} key, a service for starting a POP3
+ server is constructed for each of them and attached to the returned
+ service.
+ """
+ self._endpointServerTest("pop3", protocols.POP3Factory)
+
+
+ def test_smtp(self):
+ """
+ If one or more endpoints is included in the configuration passed to
+ L{makeService} for the C{"smtp"} key, a service for starting an SMTP
+ server is constructed for each of them and attached to the returned
+ service.
+ """
+ self._endpointServerTest("smtp", protocols.SMTPFactory)
diff --git a/twisted/mail/test/test_pop3.py b/twisted/mail/test/test_pop3.py
new file mode 100644
index 0000000..65bb23f
--- /dev/null
+++ b/twisted/mail/test/test_pop3.py
@@ -0,0 +1,1069 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for Ltwisted.mail.pop3} module.
+"""
+
+import StringIO
+import hmac
+import base64
+import itertools
+
+from zope.interface import implements
+
+from twisted.internet import defer
+
+from twisted.trial import unittest, util
+from twisted import mail
+import twisted.mail.protocols
+import twisted.mail.pop3
+import twisted.internet.protocol
+from twisted import internet
+from twisted.mail import pop3
+from twisted.protocols import loopback
+from twisted.python import failure
+
+from twisted import cred
+import twisted.cred.portal
+import twisted.cred.checkers
+import twisted.cred.credentials
+
+from twisted.test.proto_helpers import LineSendingProtocol
+
+
+class UtilityTestCase(unittest.TestCase):
+ """
+ Test the various helper functions and classes used by the POP3 server
+ protocol implementation.
+ """
+
+ def testLineBuffering(self):
+ """
+ Test creating a LineBuffer and feeding it some lines. The lines should
+ build up in its internal buffer for a while and then get spat out to
+ the writer.
+ """
+ output = []
+ input = iter(itertools.cycle(['012', '345', '6', '7', '8', '9']))
+ c = pop3._IteratorBuffer(output.extend, input, 6)
+ i = iter(c)
+ self.assertEqual(output, []) # nothing is buffer
+ i.next()
+ self.assertEqual(output, []) # '012' is buffered
+ i.next()
+ self.assertEqual(output, []) # '012345' is buffered
+ i.next()
+ self.assertEqual(output, ['012', '345', '6']) # nothing is buffered
+ for n in range(5):
+ i.next()
+ self.assertEqual(output, ['012', '345', '6', '7', '8', '9', '012', '345'])
+
+
+ def testFinishLineBuffering(self):
+ """
+ Test that a LineBuffer flushes everything when its iterator is
+ exhausted, and itself raises StopIteration.
+ """
+ output = []
+ input = iter(['a', 'b', 'c'])
+ c = pop3._IteratorBuffer(output.extend, input, 5)
+ for i in c:
+ pass
+ self.assertEqual(output, ['a', 'b', 'c'])
+
+
+ def testSuccessResponseFormatter(self):
+ """
+ Test that the thing that spits out POP3 'success responses' works
+ right.
+ """
+ self.assertEqual(
+ pop3.successResponse('Great.'),
+ '+OK Great.\r\n')
+
+
+ def testStatLineFormatter(self):
+ """
+ Test that the function which formats stat lines does so appropriately.
+ """
+ statLine = list(pop3.formatStatResponse([]))[-1]
+ self.assertEqual(statLine, '+OK 0 0\r\n')
+
+ statLine = list(pop3.formatStatResponse([10, 31, 0, 10101]))[-1]
+ self.assertEqual(statLine, '+OK 4 10142\r\n')
+
+
+ def testListLineFormatter(self):
+ """
+ Test that the function which formats the lines in response to a LIST
+ command does so appropriately.
+ """
+ listLines = list(pop3.formatListResponse([]))
+ self.assertEqual(
+ listLines,
+ ['+OK 0\r\n', '.\r\n'])
+
+ listLines = list(pop3.formatListResponse([1, 2, 3, 100]))
+ self.assertEqual(
+ listLines,
+ ['+OK 4\r\n', '1 1\r\n', '2 2\r\n', '3 3\r\n', '4 100\r\n', '.\r\n'])
+
+
+
+ def testUIDListLineFormatter(self):
+ """
+ Test that the function which formats lines in response to a UIDL
+ command does so appropriately.
+ """
+ UIDs = ['abc', 'def', 'ghi']
+ listLines = list(pop3.formatUIDListResponse([], UIDs.__getitem__))
+ self.assertEqual(
+ listLines,
+ ['+OK \r\n', '.\r\n'])
+
+ listLines = list(pop3.formatUIDListResponse([123, 431, 591], UIDs.__getitem__))
+ self.assertEqual(
+ listLines,
+ ['+OK \r\n', '1 abc\r\n', '2 def\r\n', '3 ghi\r\n', '.\r\n'])
+
+ listLines = list(pop3.formatUIDListResponse([0, None, 591], UIDs.__getitem__))
+ self.assertEqual(
+ listLines,
+ ['+OK \r\n', '1 abc\r\n', '3 ghi\r\n', '.\r\n'])
+
+
+
+class MyVirtualPOP3(mail.protocols.VirtualPOP3):
+
+ magic = '<moshez>'
+
+ def authenticateUserAPOP(self, user, digest):
+ user, domain = self.lookupDomain(user)
+ return self.service.domains['baz.com'].authenticateUserAPOP(user, digest, self.magic, domain)
+
+class DummyDomain:
+
+ def __init__(self):
+ self.users = {}
+
+ def addUser(self, name):
+ self.users[name] = []
+
+ def addMessage(self, name, message):
+ self.users[name].append(message)
+
+ def authenticateUserAPOP(self, name, digest, magic, domain):
+ return pop3.IMailbox, ListMailbox(self.users[name]), lambda: None
+
+
+class ListMailbox:
+
+ def __init__(self, list):
+ self.list = list
+
+ def listMessages(self, i=None):
+ if i is None:
+ return map(len, self.list)
+ return len(self.list[i])
+
+ def getMessage(self, i):
+ return StringIO.StringIO(self.list[i])
+
+ def getUidl(self, i):
+ return i
+
+ def deleteMessage(self, i):
+ self.list[i] = ''
+
+ def sync(self):
+ pass
+
+class MyPOP3Downloader(pop3.POP3Client):
+
+ def handle_WELCOME(self, line):
+ pop3.POP3Client.handle_WELCOME(self, line)
+ self.apop('hello@baz.com', 'world')
+
+ def handle_APOP(self, line):
+ parts = line.split()
+ code = parts[0]
+ data = (parts[1:] or ['NONE'])[0]
+ if code != '+OK':
+ print parts
+ raise AssertionError, 'code is ' + code
+ self.lines = []
+ self.retr(1)
+
+ def handle_RETR_continue(self, line):
+ self.lines.append(line)
+
+ def handle_RETR_end(self):
+ self.message = '\n'.join(self.lines) + '\n'
+ self.quit()
+
+ def handle_QUIT(self, line):
+ if line[:3] != '+OK':
+ raise AssertionError, 'code is ' + line
+
+
+class POP3TestCase(unittest.TestCase):
+
+ message = '''\
+Subject: urgent
+
+Someone set up us the bomb!
+'''
+
+ expectedOutput = '''\
++OK <moshez>\015
++OK Authentication succeeded\015
++OK \015
+1 0\015
+.\015
++OK %d\015
+Subject: urgent\015
+\015
+Someone set up us the bomb!\015
+.\015
++OK \015
+''' % len(message)
+
+ def setUp(self):
+ self.factory = internet.protocol.Factory()
+ self.factory.domains = {}
+ self.factory.domains['baz.com'] = DummyDomain()
+ self.factory.domains['baz.com'].addUser('hello')
+ self.factory.domains['baz.com'].addMessage('hello', self.message)
+
+ def testMessages(self):
+ client = LineSendingProtocol([
+ 'APOP hello@baz.com world',
+ 'UIDL',
+ 'RETR 1',
+ 'QUIT',
+ ])
+ server = MyVirtualPOP3()
+ server.service = self.factory
+ def check(ignored):
+ output = '\r\n'.join(client.response) + '\r\n'
+ self.assertEqual(output, self.expectedOutput)
+ return loopback.loopbackTCP(server, client).addCallback(check)
+
+ def testLoopback(self):
+ protocol = MyVirtualPOP3()
+ protocol.service = self.factory
+ clientProtocol = MyPOP3Downloader()
+ def check(ignored):
+ self.assertEqual(clientProtocol.message, self.message)
+ protocol.connectionLost(
+ failure.Failure(Exception("Test harness disconnect")))
+ d = loopback.loopbackAsync(protocol, clientProtocol)
+ return d.addCallback(check)
+ testLoopback.suppress = [util.suppress(message="twisted.mail.pop3.POP3Client is deprecated")]
+
+
+
+class DummyPOP3(pop3.POP3):
+
+ magic = '<moshez>'
+
+ def authenticateUserAPOP(self, user, password):
+ return pop3.IMailbox, DummyMailbox(ValueError), lambda: None
+
+
+
+class DummyMailbox(pop3.Mailbox):
+
+ messages = ['From: moshe\nTo: moshe\n\nHow are you, friend?\n']
+
+ def __init__(self, exceptionType):
+ self.messages = DummyMailbox.messages[:]
+ self.exceptionType = exceptionType
+
+ def listMessages(self, i=None):
+ if i is None:
+ return map(len, self.messages)
+ if i >= len(self.messages):
+ raise self.exceptionType()
+ return len(self.messages[i])
+
+ def getMessage(self, i):
+ return StringIO.StringIO(self.messages[i])
+
+ def getUidl(self, i):
+ if i >= len(self.messages):
+ raise self.exceptionType()
+ return str(i)
+
+ def deleteMessage(self, i):
+ self.messages[i] = ''
+
+
+class AnotherPOP3TestCase(unittest.TestCase):
+
+ def runTest(self, lines, expectedOutput):
+ dummy = DummyPOP3()
+ client = LineSendingProtocol(lines)
+ d = loopback.loopbackAsync(dummy, client)
+ return d.addCallback(self._cbRunTest, client, dummy, expectedOutput)
+
+
+ def _cbRunTest(self, ignored, client, dummy, expectedOutput):
+ self.assertEqual('\r\n'.join(expectedOutput),
+ '\r\n'.join(client.response))
+ dummy.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+ return ignored
+
+
+ def test_buffer(self):
+ """
+ Test a lot of different POP3 commands in an extremely pipelined
+ scenario.
+
+ This test may cover legitimate behavior, but the intent and
+ granularity are not very good. It would likely be an improvement to
+ split it into a number of smaller, more focused tests.
+ """
+ return self.runTest(
+ ["APOP moshez dummy",
+ "LIST",
+ "UIDL",
+ "RETR 1",
+ "RETR 2",
+ "DELE 1",
+ "RETR 1",
+ "QUIT"],
+ ['+OK <moshez>',
+ '+OK Authentication succeeded',
+ '+OK 1',
+ '1 44',
+ '.',
+ '+OK ',
+ '1 0',
+ '.',
+ '+OK 44',
+ 'From: moshe',
+ 'To: moshe',
+ '',
+ 'How are you, friend?',
+ '.',
+ '-ERR Bad message number argument',
+ '+OK ',
+ '-ERR message deleted',
+ '+OK '])
+
+
+ def test_noop(self):
+ """
+ Test the no-op command.
+ """
+ return self.runTest(
+ ['APOP spiv dummy',
+ 'NOOP',
+ 'QUIT'],
+ ['+OK <moshez>',
+ '+OK Authentication succeeded',
+ '+OK ',
+ '+OK '])
+
+
+ def testAuthListing(self):
+ p = DummyPOP3()
+ p.factory = internet.protocol.Factory()
+ p.factory.challengers = {'Auth1': None, 'secondAuth': None, 'authLast': None}
+ client = LineSendingProtocol([
+ "AUTH",
+ "QUIT",
+ ])
+
+ d = loopback.loopbackAsync(p, client)
+ return d.addCallback(self._cbTestAuthListing, client)
+
+ def _cbTestAuthListing(self, ignored, client):
+ self.failUnless(client.response[1].startswith('+OK'))
+ self.assertEqual(client.response[2:6],
+ ["AUTH1", "SECONDAUTH", "AUTHLAST", "."])
+
+ def testIllegalPASS(self):
+ dummy = DummyPOP3()
+ client = LineSendingProtocol([
+ "PASS fooz",
+ "QUIT"
+ ])
+ d = loopback.loopbackAsync(dummy, client)
+ return d.addCallback(self._cbTestIllegalPASS, client, dummy)
+
+ def _cbTestIllegalPASS(self, ignored, client, dummy):
+ expected_output = '+OK <moshez>\r\n-ERR USER required before PASS\r\n+OK \r\n'
+ self.assertEqual(expected_output, '\r\n'.join(client.response) + '\r\n')
+ dummy.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+ def testEmptyPASS(self):
+ dummy = DummyPOP3()
+ client = LineSendingProtocol([
+ "PASS ",
+ "QUIT"
+ ])
+ d = loopback.loopbackAsync(dummy, client)
+ return d.addCallback(self._cbTestEmptyPASS, client, dummy)
+
+ def _cbTestEmptyPASS(self, ignored, client, dummy):
+ expected_output = '+OK <moshez>\r\n-ERR USER required before PASS\r\n+OK \r\n'
+ self.assertEqual(expected_output, '\r\n'.join(client.response) + '\r\n')
+ dummy.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+
+class TestServerFactory:
+ implements(pop3.IServerFactory)
+
+ def cap_IMPLEMENTATION(self):
+ return "Test Implementation String"
+
+ def cap_EXPIRE(self):
+ return 60
+
+ challengers = {"SCHEME_1": None, "SCHEME_2": None}
+
+ def cap_LOGIN_DELAY(self):
+ return 120
+
+ pue = True
+ def perUserExpiration(self):
+ return self.pue
+
+ puld = True
+ def perUserLoginDelay(self):
+ return self.puld
+
+
+class TestMailbox:
+ loginDelay = 100
+ messageExpiration = 25
+
+
+class CapabilityTestCase(unittest.TestCase):
+ def setUp(self):
+ s = StringIO.StringIO()
+ p = pop3.POP3()
+ p.factory = TestServerFactory()
+ p.transport = internet.protocol.FileWrapper(s)
+ p.connectionMade()
+ p.do_CAPA()
+
+ self.caps = p.listCapabilities()
+ self.pcaps = s.getvalue().splitlines()
+
+ s = StringIO.StringIO()
+ p.mbox = TestMailbox()
+ p.transport = internet.protocol.FileWrapper(s)
+ p.do_CAPA()
+
+ self.lpcaps = s.getvalue().splitlines()
+ p.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+ def contained(self, s, *caps):
+ for c in caps:
+ self.assertIn(s, c)
+
+ def testUIDL(self):
+ self.contained("UIDL", self.caps, self.pcaps, self.lpcaps)
+
+ def testTOP(self):
+ self.contained("TOP", self.caps, self.pcaps, self.lpcaps)
+
+ def testUSER(self):
+ self.contained("USER", self.caps, self.pcaps, self.lpcaps)
+
+ def testEXPIRE(self):
+ self.contained("EXPIRE 60 USER", self.caps, self.pcaps)
+ self.contained("EXPIRE 25", self.lpcaps)
+
+ def testIMPLEMENTATION(self):
+ self.contained(
+ "IMPLEMENTATION Test Implementation String",
+ self.caps, self.pcaps, self.lpcaps
+ )
+
+ def testSASL(self):
+ self.contained(
+ "SASL SCHEME_1 SCHEME_2",
+ self.caps, self.pcaps, self.lpcaps
+ )
+
+ def testLOGIN_DELAY(self):
+ self.contained("LOGIN-DELAY 120 USER", self.caps, self.pcaps)
+ self.assertIn("LOGIN-DELAY 100", self.lpcaps)
+
+
+
+class GlobalCapabilitiesTestCase(unittest.TestCase):
+ def setUp(self):
+ s = StringIO.StringIO()
+ p = pop3.POP3()
+ p.factory = TestServerFactory()
+ p.factory.pue = p.factory.puld = False
+ p.transport = internet.protocol.FileWrapper(s)
+ p.connectionMade()
+ p.do_CAPA()
+
+ self.caps = p.listCapabilities()
+ self.pcaps = s.getvalue().splitlines()
+
+ s = StringIO.StringIO()
+ p.mbox = TestMailbox()
+ p.transport = internet.protocol.FileWrapper(s)
+ p.do_CAPA()
+
+ self.lpcaps = s.getvalue().splitlines()
+ p.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+ def contained(self, s, *caps):
+ for c in caps:
+ self.assertIn(s, c)
+
+ def testEXPIRE(self):
+ self.contained("EXPIRE 60", self.caps, self.pcaps, self.lpcaps)
+
+ def testLOGIN_DELAY(self):
+ self.contained("LOGIN-DELAY 120", self.caps, self.pcaps, self.lpcaps)
+
+
+
+class TestRealm:
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if avatarId == 'testuser':
+ return pop3.IMailbox, DummyMailbox(ValueError), lambda: None
+ assert False
+
+
+
+class SASLTestCase(unittest.TestCase):
+ def testValidLogin(self):
+ p = pop3.POP3()
+ p.factory = TestServerFactory()
+ p.factory.challengers = {'CRAM-MD5': cred.credentials.CramMD5Credentials}
+ p.portal = cred.portal.Portal(TestRealm())
+ ch = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ ch.addUser('testuser', 'testpassword')
+ p.portal.registerChecker(ch)
+
+ s = StringIO.StringIO()
+ p.transport = internet.protocol.FileWrapper(s)
+ p.connectionMade()
+
+ p.lineReceived("CAPA")
+ self.failUnless(s.getvalue().find("SASL CRAM-MD5") >= 0)
+
+ p.lineReceived("AUTH CRAM-MD5")
+ chal = s.getvalue().splitlines()[-1][2:]
+ chal = base64.decodestring(chal)
+ response = hmac.HMAC('testpassword', chal).hexdigest()
+
+ p.lineReceived(base64.encodestring('testuser ' + response).rstrip('\n'))
+ self.failUnless(p.mbox)
+ self.failUnless(s.getvalue().splitlines()[-1].find("+OK") >= 0)
+ p.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+
+
+class CommandMixin:
+ """
+ Tests for all the commands a POP3 server is allowed to receive.
+ """
+
+ extraMessage = '''\
+From: guy
+To: fellow
+
+More message text for you.
+'''
+
+
+ def setUp(self):
+ """
+ Make a POP3 server protocol instance hooked up to a simple mailbox and
+ a transport that buffers output to a StringIO.
+ """
+ p = pop3.POP3()
+ p.mbox = self.mailboxType(self.exceptionType)
+ p.schedule = list
+ self.pop3Server = p
+
+ s = StringIO.StringIO()
+ p.transport = internet.protocol.FileWrapper(s)
+ p.connectionMade()
+ s.truncate(0)
+ self.pop3Transport = s
+
+
+ def tearDown(self):
+ """
+ Disconnect the server protocol so it can clean up anything it might
+ need to clean up.
+ """
+ self.pop3Server.connectionLost(failure.Failure(Exception("Test harness disconnect")))
+
+
+ def _flush(self):
+ """
+ Do some of the things that the reactor would take care of, if the
+ reactor were actually running.
+ """
+ # Oh man FileWrapper is pooh.
+ self.pop3Server.transport._checkProducer()
+
+
+ def testLIST(self):
+ """
+ Test the two forms of list: with a message index number, which should
+ return a short-form response, and without a message index number, which
+ should return a long-form response, one line per message.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("LIST 1")
+ self._flush()
+ self.assertEqual(s.getvalue(), "+OK 1 44\r\n")
+ s.truncate(0)
+
+ p.lineReceived("LIST")
+ self._flush()
+ self.assertEqual(s.getvalue(), "+OK 1\r\n1 44\r\n.\r\n")
+
+
+ def testLISTWithBadArgument(self):
+ """
+ Test that non-integers and out-of-bound integers produce appropriate
+ error responses.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("LIST a")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Invalid message-number: 'a'\r\n")
+ s.truncate(0)
+
+ p.lineReceived("LIST 0")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Invalid message-number: 0\r\n")
+ s.truncate(0)
+
+ p.lineReceived("LIST 2")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Invalid message-number: 2\r\n")
+ s.truncate(0)
+
+
+ def testUIDL(self):
+ """
+ Test the two forms of the UIDL command. These are just like the two
+ forms of the LIST command.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("UIDL 1")
+ self.assertEqual(s.getvalue(), "+OK 0\r\n")
+ s.truncate(0)
+
+ p.lineReceived("UIDL")
+ self._flush()
+ self.assertEqual(s.getvalue(), "+OK \r\n1 0\r\n.\r\n")
+
+
+ def testUIDLWithBadArgument(self):
+ """
+ Test that UIDL with a non-integer or an out-of-bounds integer produces
+ the appropriate error response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("UIDL a")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("UIDL 0")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("UIDL 2")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+
+ def testSTAT(self):
+ """
+ Test the single form of the STAT command, which returns a short-form
+ response of the number of messages in the mailbox and their total size.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("STAT")
+ self._flush()
+ self.assertEqual(s.getvalue(), "+OK 1 44\r\n")
+
+
+ def testRETR(self):
+ """
+ Test downloading a message.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("RETR 1")
+ self._flush()
+ self.assertEqual(
+ s.getvalue(),
+ "+OK 44\r\n"
+ "From: moshe\r\n"
+ "To: moshe\r\n"
+ "\r\n"
+ "How are you, friend?\r\n"
+ ".\r\n")
+ s.truncate(0)
+
+
+ def testRETRWithBadArgument(self):
+ """
+ Test that trying to download a message with a bad argument, either not
+ an integer or an out-of-bounds integer, fails with the appropriate
+ error response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+
+ p.lineReceived("RETR a")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("RETR 0")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("RETR 2")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+
+ def testTOP(self):
+ """
+ Test downloading the headers and part of the body of a message.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived("TOP 1 0")
+ self._flush()
+ self.assertEqual(
+ s.getvalue(),
+ "+OK Top of message follows\r\n"
+ "From: moshe\r\n"
+ "To: moshe\r\n"
+ "\r\n"
+ ".\r\n")
+
+
+ def testTOPWithBadArgument(self):
+ """
+ Test that trying to download a message with a bad argument, either a
+ message number which isn't an integer or is an out-of-bounds integer or
+ a number of lines which isn't an integer or is a negative integer,
+ fails with the appropriate error response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived("TOP 1 a")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad line count argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("TOP 1 -1")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad line count argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("TOP a 1")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("TOP 0 1")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+ p.lineReceived("TOP 3 1")
+ self.assertEqual(
+ s.getvalue(),
+ "-ERR Bad message number argument\r\n")
+ s.truncate(0)
+
+
+ def testLAST(self):
+ """
+ Test the exceedingly pointless LAST command, which tells you the
+ highest message index which you have already downloaded.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived('LAST')
+ self.assertEqual(
+ s.getvalue(),
+ "+OK 0\r\n")
+ s.truncate(0)
+
+
+ def testRetrieveUpdatesHighest(self):
+ """
+ Test that issuing a RETR command updates the LAST response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived('RETR 2')
+ self._flush()
+ s.truncate(0)
+ p.lineReceived('LAST')
+ self.assertEqual(
+ s.getvalue(),
+ '+OK 2\r\n')
+ s.truncate(0)
+
+
+ def testTopUpdatesHighest(self):
+ """
+ Test that issuing a TOP command updates the LAST response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived('TOP 2 10')
+ self._flush()
+ s.truncate(0)
+ p.lineReceived('LAST')
+ self.assertEqual(
+ s.getvalue(),
+ '+OK 2\r\n')
+
+
+ def testHighestOnlyProgresses(self):
+ """
+ Test that downloading a message with a smaller index than the current
+ LAST response doesn't change the LAST response.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived('RETR 2')
+ self._flush()
+ p.lineReceived('TOP 1 10')
+ self._flush()
+ s.truncate(0)
+ p.lineReceived('LAST')
+ self.assertEqual(
+ s.getvalue(),
+ '+OK 2\r\n')
+
+
+ def testResetClearsHighest(self):
+ """
+ Test that issuing RSET changes the LAST response to 0.
+ """
+ p = self.pop3Server
+ s = self.pop3Transport
+ p.mbox.messages.append(self.extraMessage)
+
+ p.lineReceived('RETR 2')
+ self._flush()
+ p.lineReceived('RSET')
+ s.truncate(0)
+ p.lineReceived('LAST')
+ self.assertEqual(
+ s.getvalue(),
+ '+OK 0\r\n')
+
+
+
+_listMessageDeprecation = (
+ "twisted.mail.pop3.IMailbox.listMessages may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.")
+_listMessageSuppression = util.suppress(
+ message=_listMessageDeprecation,
+ category=PendingDeprecationWarning)
+
+_getUidlDeprecation = (
+ "twisted.mail.pop3.IMailbox.getUidl may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.")
+_getUidlSuppression = util.suppress(
+ message=_getUidlDeprecation,
+ category=PendingDeprecationWarning)
+
+class IndexErrorCommandTestCase(CommandMixin, unittest.TestCase):
+ """
+ Run all of the command tests against a mailbox which raises IndexError
+ when an out of bounds request is made. This behavior will be deprecated
+ shortly and then removed.
+ """
+ exceptionType = IndexError
+ mailboxType = DummyMailbox
+
+ def testLISTWithBadArgument(self):
+ return CommandMixin.testLISTWithBadArgument(self)
+ testLISTWithBadArgument.suppress = [_listMessageSuppression]
+
+
+ def testUIDLWithBadArgument(self):
+ return CommandMixin.testUIDLWithBadArgument(self)
+ testUIDLWithBadArgument.suppress = [_getUidlSuppression]
+
+
+ def testTOPWithBadArgument(self):
+ return CommandMixin.testTOPWithBadArgument(self)
+ testTOPWithBadArgument.suppress = [_listMessageSuppression]
+
+
+ def testRETRWithBadArgument(self):
+ return CommandMixin.testRETRWithBadArgument(self)
+ testRETRWithBadArgument.suppress = [_listMessageSuppression]
+
+
+
+class ValueErrorCommandTestCase(CommandMixin, unittest.TestCase):
+ """
+ Run all of the command tests against a mailbox which raises ValueError
+ when an out of bounds request is made. This is the correct behavior and
+ after support for mailboxes which raise IndexError is removed, this will
+ become just C{CommandTestCase}.
+ """
+ exceptionType = ValueError
+ mailboxType = DummyMailbox
+
+
+
+class SyncDeferredMailbox(DummyMailbox):
+ """
+ Mailbox which has a listMessages implementation which returns a Deferred
+ which has already fired.
+ """
+ def listMessages(self, n=None):
+ return defer.succeed(DummyMailbox.listMessages(self, n))
+
+
+
+class IndexErrorSyncDeferredCommandTestCase(IndexErrorCommandTestCase):
+ """
+ Run all of the L{IndexErrorCommandTestCase} tests with a
+ synchronous-Deferred returning IMailbox implementation.
+ """
+ mailboxType = SyncDeferredMailbox
+
+
+
+class ValueErrorSyncDeferredCommandTestCase(ValueErrorCommandTestCase):
+ """
+ Run all of the L{ValueErrorCommandTestCase} tests with a
+ synchronous-Deferred returning IMailbox implementation.
+ """
+ mailboxType = SyncDeferredMailbox
+
+
+
+class AsyncDeferredMailbox(DummyMailbox):
+ """
+ Mailbox which has a listMessages implementation which returns a Deferred
+ which has not yet fired.
+ """
+ def __init__(self, *a, **kw):
+ self.waiting = []
+ DummyMailbox.__init__(self, *a, **kw)
+
+
+ def listMessages(self, n=None):
+ d = defer.Deferred()
+ # See AsyncDeferredMailbox._flush
+ self.waiting.append((d, DummyMailbox.listMessages(self, n)))
+ return d
+
+
+
+class IndexErrorAsyncDeferredCommandTestCase(IndexErrorCommandTestCase):
+ """
+ Run all of the L{IndexErrorCommandTestCase} tests with an asynchronous-Deferred
+ returning IMailbox implementation.
+ """
+ mailboxType = AsyncDeferredMailbox
+
+ def _flush(self):
+ """
+ Fire whatever Deferreds we've built up in our mailbox.
+ """
+ while self.pop3Server.mbox.waiting:
+ d, a = self.pop3Server.mbox.waiting.pop()
+ d.callback(a)
+ IndexErrorCommandTestCase._flush(self)
+
+
+
+class ValueErrorAsyncDeferredCommandTestCase(ValueErrorCommandTestCase):
+ """
+ Run all of the L{IndexErrorCommandTestCase} tests with an asynchronous-Deferred
+ returning IMailbox implementation.
+ """
+ mailboxType = AsyncDeferredMailbox
+
+ def _flush(self):
+ """
+ Fire whatever Deferreds we've built up in our mailbox.
+ """
+ while self.pop3Server.mbox.waiting:
+ d, a = self.pop3Server.mbox.waiting.pop()
+ d.callback(a)
+ ValueErrorCommandTestCase._flush(self)
+
+class POP3MiscTestCase(unittest.TestCase):
+ """
+ Miscellaneous tests more to do with module/package structure than
+ anything to do with the Post Office Protocol.
+ """
+ def test_all(self):
+ """
+ This test checks that all names listed in
+ twisted.mail.pop3.__all__ are actually present in the module.
+ """
+ mod = twisted.mail.pop3
+ for attr in mod.__all__:
+ self.failUnless(hasattr(mod, attr))
diff --git a/twisted/mail/test/test_pop3client.py b/twisted/mail/test/test_pop3client.py
new file mode 100644
index 0000000..502aae8
--- /dev/null
+++ b/twisted/mail/test/test_pop3client.py
@@ -0,0 +1,582 @@
+# -*- test-case-name: twisted.mail.test.test_pop3client -*-
+# Copyright (c) 2001-2004 Divmod Inc.
+# See LICENSE for details.
+
+from zope.interface import directlyProvides
+
+from twisted.mail.pop3 import AdvancedPOP3Client as POP3Client
+from twisted.mail.pop3 import InsecureAuthenticationDisallowed
+from twisted.mail.pop3 import ServerErrorResponse
+from twisted.protocols import loopback
+from twisted.internet import reactor, defer, error, protocol, interfaces
+from twisted.python import log
+
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+from twisted.protocols import basic
+
+from twisted.mail.test import pop3testserver
+
+try:
+ from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
+except ImportError:
+ ClientTLSContext = ServerTLSContext = None
+
+
+class StringTransportWithConnectionLosing(StringTransport):
+ def loseConnection(self):
+ self.protocol.connectionLost(error.ConnectionDone())
+
+
+capCache = {"TOP": None, "LOGIN-DELAY": "180", "UIDL": None, \
+ "STLS": None, "USER": None, "SASL": "LOGIN"}
+def setUp(greet=True):
+ p = POP3Client()
+
+ # Skip the CAPA login will issue if it doesn't already have a
+ # capability cache
+ p._capCache = capCache
+
+ t = StringTransportWithConnectionLosing()
+ t.protocol = p
+ p.makeConnection(t)
+
+ if greet:
+ p.dataReceived('+OK Hello!\r\n')
+
+ return p, t
+
+def strip(f):
+ return lambda result, f=f: f()
+
+class POP3ClientLoginTestCase(unittest.TestCase):
+ def testNegativeGreeting(self):
+ p, t = setUp(greet=False)
+ p.allowInsecureLogin = True
+ d = p.login("username", "password")
+ p.dataReceived('-ERR Offline for maintenance\r\n')
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "Offline for maintenance"))
+
+
+ def testOkUser(self):
+ p, t = setUp()
+ d = p.user("username")
+ self.assertEqual(t.value(), "USER username\r\n")
+ p.dataReceived("+OK send password\r\n")
+ return d.addCallback(self.assertEqual, "send password")
+
+ def testBadUser(self):
+ p, t = setUp()
+ d = p.user("username")
+ self.assertEqual(t.value(), "USER username\r\n")
+ p.dataReceived("-ERR account suspended\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "account suspended"))
+
+ def testOkPass(self):
+ p, t = setUp()
+ d = p.password("password")
+ self.assertEqual(t.value(), "PASS password\r\n")
+ p.dataReceived("+OK you're in!\r\n")
+ return d.addCallback(self.assertEqual, "you're in!")
+
+ def testBadPass(self):
+ p, t = setUp()
+ d = p.password("password")
+ self.assertEqual(t.value(), "PASS password\r\n")
+ p.dataReceived("-ERR go away\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "go away"))
+
+ def testOkLogin(self):
+ p, t = setUp()
+ p.allowInsecureLogin = True
+ d = p.login("username", "password")
+ self.assertEqual(t.value(), "USER username\r\n")
+ p.dataReceived("+OK go ahead\r\n")
+ self.assertEqual(t.value(), "USER username\r\nPASS password\r\n")
+ p.dataReceived("+OK password accepted\r\n")
+ return d.addCallback(self.assertEqual, "password accepted")
+
+ def testBadPasswordLogin(self):
+ p, t = setUp()
+ p.allowInsecureLogin = True
+ d = p.login("username", "password")
+ self.assertEqual(t.value(), "USER username\r\n")
+ p.dataReceived("+OK waiting on you\r\n")
+ self.assertEqual(t.value(), "USER username\r\nPASS password\r\n")
+ p.dataReceived("-ERR bogus login\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "bogus login"))
+
+ def testBadUsernameLogin(self):
+ p, t = setUp()
+ p.allowInsecureLogin = True
+ d = p.login("username", "password")
+ self.assertEqual(t.value(), "USER username\r\n")
+ p.dataReceived("-ERR bogus login\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "bogus login"))
+
+ def testServerGreeting(self):
+ p, t = setUp(greet=False)
+ p.dataReceived("+OK lalala this has no challenge\r\n")
+ self.assertEqual(p.serverChallenge, None)
+
+ def testServerGreetingWithChallenge(self):
+ p, t = setUp(greet=False)
+ p.dataReceived("+OK <here is the challenge>\r\n")
+ self.assertEqual(p.serverChallenge, "<here is the challenge>")
+
+ def testAPOP(self):
+ p, t = setUp(greet=False)
+ p.dataReceived("+OK <challenge string goes here>\r\n")
+ d = p.login("username", "password")
+ self.assertEqual(t.value(), "APOP username f34f1e464d0d7927607753129cabe39a\r\n")
+ p.dataReceived("+OK Welcome!\r\n")
+ return d.addCallback(self.assertEqual, "Welcome!")
+
+ def testInsecureLoginRaisesException(self):
+ p, t = setUp(greet=False)
+ p.dataReceived("+OK Howdy\r\n")
+ d = p.login("username", "password")
+ self.failIf(t.value())
+ return self.assertFailure(
+ d, InsecureAuthenticationDisallowed)
+
+
+ def testSSLTransportConsideredSecure(self):
+ """
+ If a server doesn't offer APOP but the transport is secured using
+ SSL or TLS, a plaintext login should be allowed, not rejected with
+ an InsecureAuthenticationDisallowed exception.
+ """
+ p, t = setUp(greet=False)
+ directlyProvides(t, interfaces.ISSLTransport)
+ p.dataReceived("+OK Howdy\r\n")
+ d = p.login("username", "password")
+ self.assertEqual(t.value(), "USER username\r\n")
+ t.clear()
+ p.dataReceived("+OK\r\n")
+ self.assertEqual(t.value(), "PASS password\r\n")
+ p.dataReceived("+OK\r\n")
+ return d
+
+
+
+class ListConsumer:
+ def __init__(self):
+ self.data = {}
+
+ def consume(self, (item, value)):
+ self.data.setdefault(item, []).append(value)
+
+class MessageConsumer:
+ def __init__(self):
+ self.data = []
+
+ def consume(self, line):
+ self.data.append(line)
+
+class POP3ClientListTestCase(unittest.TestCase):
+ def testListSize(self):
+ p, t = setUp()
+ d = p.listSize()
+ self.assertEqual(t.value(), "LIST\r\n")
+ p.dataReceived("+OK Here it comes\r\n")
+ p.dataReceived("1 3\r\n2 2\r\n3 1\r\n.\r\n")
+ return d.addCallback(self.assertEqual, [3, 2, 1])
+
+ def testListSizeWithConsumer(self):
+ p, t = setUp()
+ c = ListConsumer()
+ f = c.consume
+ d = p.listSize(f)
+ self.assertEqual(t.value(), "LIST\r\n")
+ p.dataReceived("+OK Here it comes\r\n")
+ p.dataReceived("1 3\r\n2 2\r\n3 1\r\n")
+ self.assertEqual(c.data, {0: [3], 1: [2], 2: [1]})
+ p.dataReceived("5 3\r\n6 2\r\n7 1\r\n")
+ self.assertEqual(c.data, {0: [3], 1: [2], 2: [1], 4: [3], 5: [2], 6: [1]})
+ p.dataReceived(".\r\n")
+ return d.addCallback(self.assertIdentical, f)
+
+ def testFailedListSize(self):
+ p, t = setUp()
+ d = p.listSize()
+ self.assertEqual(t.value(), "LIST\r\n")
+ p.dataReceived("-ERR Fatal doom server exploded\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded"))
+
+ def testListUID(self):
+ p, t = setUp()
+ d = p.listUID()
+ self.assertEqual(t.value(), "UIDL\r\n")
+ p.dataReceived("+OK Here it comes\r\n")
+ p.dataReceived("1 abc\r\n2 def\r\n3 ghi\r\n.\r\n")
+ return d.addCallback(self.assertEqual, ["abc", "def", "ghi"])
+
+ def testListUIDWithConsumer(self):
+ p, t = setUp()
+ c = ListConsumer()
+ f = c.consume
+ d = p.listUID(f)
+ self.assertEqual(t.value(), "UIDL\r\n")
+ p.dataReceived("+OK Here it comes\r\n")
+ p.dataReceived("1 xyz\r\n2 abc\r\n5 mno\r\n")
+ self.assertEqual(c.data, {0: ["xyz"], 1: ["abc"], 4: ["mno"]})
+ p.dataReceived(".\r\n")
+ return d.addCallback(self.assertIdentical, f)
+
+ def testFailedListUID(self):
+ p, t = setUp()
+ d = p.listUID()
+ self.assertEqual(t.value(), "UIDL\r\n")
+ p.dataReceived("-ERR Fatal doom server exploded\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded"))
+
+class POP3ClientMessageTestCase(unittest.TestCase):
+ def testRetrieve(self):
+ p, t = setUp()
+ d = p.retrieve(7)
+ self.assertEqual(t.value(), "RETR 8\r\n")
+ p.dataReceived("+OK Message incoming\r\n")
+ p.dataReceived("La la la here is message text\r\n")
+ p.dataReceived("..Further message text tra la la\r\n")
+ p.dataReceived(".\r\n")
+ return d.addCallback(
+ self.assertEqual,
+ ["La la la here is message text",
+ ".Further message text tra la la"])
+
+ def testRetrieveWithConsumer(self):
+ p, t = setUp()
+ c = MessageConsumer()
+ f = c.consume
+ d = p.retrieve(7, f)
+ self.assertEqual(t.value(), "RETR 8\r\n")
+ p.dataReceived("+OK Message incoming\r\n")
+ p.dataReceived("La la la here is message text\r\n")
+ p.dataReceived("..Further message text\r\n.\r\n")
+ return d.addCallback(self._cbTestRetrieveWithConsumer, f, c)
+
+ def _cbTestRetrieveWithConsumer(self, result, f, c):
+ self.assertIdentical(result, f)
+ self.assertEqual(c.data, ["La la la here is message text",
+ ".Further message text"])
+
+ def testPartialRetrieve(self):
+ p, t = setUp()
+ d = p.retrieve(7, lines=2)
+ self.assertEqual(t.value(), "TOP 8 2\r\n")
+ p.dataReceived("+OK 2 lines on the way\r\n")
+ p.dataReceived("Line the first! Woop\r\n")
+ p.dataReceived("Line the last! Bye\r\n")
+ p.dataReceived(".\r\n")
+ return d.addCallback(
+ self.assertEqual,
+ ["Line the first! Woop",
+ "Line the last! Bye"])
+
+ def testPartialRetrieveWithConsumer(self):
+ p, t = setUp()
+ c = MessageConsumer()
+ f = c.consume
+ d = p.retrieve(7, f, lines=2)
+ self.assertEqual(t.value(), "TOP 8 2\r\n")
+ p.dataReceived("+OK 2 lines on the way\r\n")
+ p.dataReceived("Line the first! Woop\r\n")
+ p.dataReceived("Line the last! Bye\r\n")
+ p.dataReceived(".\r\n")
+ return d.addCallback(self._cbTestPartialRetrieveWithConsumer, f, c)
+
+ def _cbTestPartialRetrieveWithConsumer(self, result, f, c):
+ self.assertIdentical(result, f)
+ self.assertEqual(c.data, ["Line the first! Woop",
+ "Line the last! Bye"])
+
+ def testFailedRetrieve(self):
+ p, t = setUp()
+ d = p.retrieve(0)
+ self.assertEqual(t.value(), "RETR 1\r\n")
+ p.dataReceived("-ERR Fatal doom server exploded\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "Fatal doom server exploded"))
+
+
+ def test_concurrentRetrieves(self):
+ """
+ Issue three retrieve calls immediately without waiting for any to
+ succeed and make sure they all do succeed eventually.
+ """
+ p, t = setUp()
+ messages = [
+ p.retrieve(i).addCallback(
+ self.assertEqual,
+ ["First line of %d." % (i + 1,),
+ "Second line of %d." % (i + 1,)])
+ for i
+ in range(3)]
+
+ for i in range(1, 4):
+ self.assertEqual(t.value(), "RETR %d\r\n" % (i,))
+ t.clear()
+ p.dataReceived("+OK 2 lines on the way\r\n")
+ p.dataReceived("First line of %d.\r\n" % (i,))
+ p.dataReceived("Second line of %d.\r\n" % (i,))
+ self.assertEqual(t.value(), "")
+ p.dataReceived(".\r\n")
+
+ return defer.DeferredList(messages, fireOnOneErrback=True)
+
+
+
+class POP3ClientMiscTestCase(unittest.TestCase):
+ def testCapability(self):
+ p, t = setUp()
+ d = p.capabilities(useCache=0)
+ self.assertEqual(t.value(), "CAPA\r\n")
+ p.dataReceived("+OK Capabilities on the way\r\n")
+ p.dataReceived("X\r\nY\r\nZ\r\nA 1 2 3\r\nB 1 2\r\nC 1\r\n.\r\n")
+ return d.addCallback(
+ self.assertEqual,
+ {"X": None, "Y": None, "Z": None,
+ "A": ["1", "2", "3"],
+ "B": ["1", "2"],
+ "C": ["1"]})
+
+ def testCapabilityError(self):
+ p, t = setUp()
+ d = p.capabilities(useCache=0)
+ self.assertEqual(t.value(), "CAPA\r\n")
+ p.dataReceived("-ERR This server is lame!\r\n")
+ return d.addCallback(self.assertEqual, {})
+
+ def testStat(self):
+ p, t = setUp()
+ d = p.stat()
+ self.assertEqual(t.value(), "STAT\r\n")
+ p.dataReceived("+OK 1 1212\r\n")
+ return d.addCallback(self.assertEqual, (1, 1212))
+
+ def testStatError(self):
+ p, t = setUp()
+ d = p.stat()
+ self.assertEqual(t.value(), "STAT\r\n")
+ p.dataReceived("-ERR This server is lame!\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
+
+ def testNoop(self):
+ p, t = setUp()
+ d = p.noop()
+ self.assertEqual(t.value(), "NOOP\r\n")
+ p.dataReceived("+OK No-op to you too!\r\n")
+ return d.addCallback(self.assertEqual, "No-op to you too!")
+
+ def testNoopError(self):
+ p, t = setUp()
+ d = p.noop()
+ self.assertEqual(t.value(), "NOOP\r\n")
+ p.dataReceived("-ERR This server is lame!\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
+
+ def testRset(self):
+ p, t = setUp()
+ d = p.reset()
+ self.assertEqual(t.value(), "RSET\r\n")
+ p.dataReceived("+OK Reset state\r\n")
+ return d.addCallback(self.assertEqual, "Reset state")
+
+ def testRsetError(self):
+ p, t = setUp()
+ d = p.reset()
+ self.assertEqual(t.value(), "RSET\r\n")
+ p.dataReceived("-ERR This server is lame!\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "This server is lame!"))
+
+ def testDelete(self):
+ p, t = setUp()
+ d = p.delete(3)
+ self.assertEqual(t.value(), "DELE 4\r\n")
+ p.dataReceived("+OK Hasta la vista\r\n")
+ return d.addCallback(self.assertEqual, "Hasta la vista")
+
+ def testDeleteError(self):
+ p, t = setUp()
+ d = p.delete(3)
+ self.assertEqual(t.value(), "DELE 4\r\n")
+ p.dataReceived("-ERR Winner is not you.\r\n")
+ return self.assertFailure(
+ d, ServerErrorResponse).addCallback(
+ lambda exc: self.assertEqual(exc.args[0], "Winner is not you."))
+
+
+class SimpleClient(POP3Client):
+ def __init__(self, deferred, contextFactory = None):
+ self.deferred = deferred
+ self.allowInsecureLogin = True
+
+ def serverGreeting(self, challenge):
+ self.deferred.callback(None)
+
+class POP3HelperMixin:
+ serverCTX = None
+ clientCTX = None
+
+ def setUp(self):
+ d = defer.Deferred()
+ self.server = pop3testserver.POP3TestServer(contextFactory=self.serverCTX)
+ self.client = SimpleClient(d, contextFactory=self.clientCTX)
+ self.client.timeout = 30
+ self.connected = d
+
+ def tearDown(self):
+ del self.server
+ del self.client
+ del self.connected
+
+ def _cbStopClient(self, ignore):
+ self.client.transport.loseConnection()
+
+ def _ebGeneral(self, failure):
+ self.client.transport.loseConnection()
+ self.server.transport.loseConnection()
+ return failure
+
+ def loopback(self):
+ return loopback.loopbackTCP(self.server, self.client, noisy=False)
+
+
+class TLSServerFactory(protocol.ServerFactory):
+ class protocol(basic.LineReceiver):
+ context = None
+ output = []
+ def connectionMade(self):
+ self.factory.input = []
+ self.output = self.output[:]
+ map(self.sendLine, self.output.pop(0))
+ def lineReceived(self, line):
+ self.factory.input.append(line)
+ map(self.sendLine, self.output.pop(0))
+ if line == 'STLS':
+ self.transport.startTLS(self.context)
+
+
+class POP3TLSTestCase(unittest.TestCase):
+ """
+ Tests for POP3Client's support for TLS connections.
+ """
+
+ def test_startTLS(self):
+ """
+ POP3Client.startTLS starts a TLS session over its existing TCP
+ connection.
+ """
+ sf = TLSServerFactory()
+ sf.protocol.output = [
+ ['+OK'], # Server greeting
+ ['+OK', 'STLS', '.'], # CAPA response
+ ['+OK'], # STLS response
+ ['+OK', '.'], # Second CAPA response
+ ['+OK'] # QUIT response
+ ]
+ sf.protocol.context = ServerTLSContext()
+ port = reactor.listenTCP(0, sf, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+ H = port.getHost().host
+ P = port.getHost().port
+
+ connLostDeferred = defer.Deferred()
+ cp = SimpleClient(defer.Deferred(), ClientTLSContext())
+ def connectionLost(reason):
+ SimpleClient.connectionLost(cp, reason)
+ connLostDeferred.callback(None)
+ cp.connectionLost = connectionLost
+ cf = protocol.ClientFactory()
+ cf.protocol = lambda: cp
+
+ conn = reactor.connectTCP(H, P, cf)
+
+ def cbConnected(ignored):
+ log.msg("Connected to server; starting TLS")
+ return cp.startTLS()
+
+ def cbStartedTLS(ignored):
+ log.msg("Started TLS; disconnecting")
+ return cp.quit()
+
+ def cbDisconnected(ign):
+ log.msg("Disconnected; asserting correct input received")
+ self.assertEqual(
+ sf.input,
+ ['CAPA', 'STLS', 'CAPA', 'QUIT'])
+
+ def cleanup(result):
+ log.msg("Asserted correct input; disconnecting client and shutting down server")
+ conn.disconnect()
+ return connLostDeferred
+
+ cp.deferred.addCallback(cbConnected)
+ cp.deferred.addCallback(cbStartedTLS)
+ cp.deferred.addCallback(cbDisconnected)
+ cp.deferred.addBoth(cleanup)
+
+ return cp.deferred
+
+
+class POP3TimeoutTestCase(POP3HelperMixin, unittest.TestCase):
+ def testTimeout(self):
+ def login():
+ d = self.client.login('test', 'twisted')
+ d.addCallback(loggedIn)
+ d.addErrback(timedOut)
+ return d
+
+ def loggedIn(result):
+ self.fail("Successfully logged in!? Impossible!")
+
+
+ def timedOut(failure):
+ failure.trap(error.TimeoutError)
+ self._cbStopClient(None)
+
+ def quit():
+ return self.client.quit()
+
+ self.client.timeout = 0.01
+
+ # Tell the server to not return a response to client. This
+ # will trigger a timeout.
+ pop3testserver.TIMEOUT_RESPONSE = True
+
+ methods = [login, quit]
+ map(self.connected.addCallback, map(strip, methods))
+ self.connected.addCallback(self._cbStopClient)
+ self.connected.addErrback(self._ebGeneral)
+ return self.loopback()
+
+
+if ClientTLSContext is None:
+ for case in (POP3TLSTestCase,):
+ case.skip = "OpenSSL not present"
+elif interfaces.IReactorSSL(reactor, None) is None:
+ for case in (POP3TLSTestCase,):
+ case.skip = "Reactor doesn't support SSL"
+
diff --git a/twisted/mail/test/test_scripts.py b/twisted/mail/test/test_scripts.py
new file mode 100644
index 0000000..cc14061
--- /dev/null
+++ b/twisted/mail/test/test_scripts.py
@@ -0,0 +1,18 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the command-line mailer tool provided by Twisted Mail.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.scripts.test.test_scripts import ScriptTestsMixin
+
+
+
+class ScriptTests(TestCase, ScriptTestsMixin):
+ """
+ Tests for all one of mail's scripts.
+ """
+ def test_mailmail(self):
+ self.scriptTest("mail/mailmail")
diff --git a/twisted/mail/test/test_smtp.py b/twisted/mail/test/test_smtp.py
new file mode 100644
index 0000000..058bb8e
--- /dev/null
+++ b/twisted/mail/test/test_smtp.py
@@ -0,0 +1,1520 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.mail.smtp module.
+"""
+
+from zope.interface import implements
+
+from twisted.python.util import LineLog
+from twisted.trial import unittest, util
+from twisted.protocols import basic, loopback
+from twisted.mail import smtp
+from twisted.internet import defer, protocol, reactor, interfaces
+from twisted.internet import address, error, task
+from twisted.test.proto_helpers import StringTransport
+
+from twisted import cred
+import twisted.cred.error
+import twisted.cred.portal
+import twisted.cred.checkers
+import twisted.cred.credentials
+
+from twisted.cred.portal import IRealm, Portal
+from twisted.cred.checkers import ICredentialsChecker, AllowAnonymousAccess
+from twisted.cred.credentials import IAnonymous
+from twisted.cred.error import UnauthorizedLogin
+
+from twisted.mail import imap4
+
+
+try:
+ from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
+except ImportError:
+ ClientTLSContext = ServerTLSContext = None
+
+import re
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+
+def spameater(*spam, **eggs):
+ return None
+
+
+
+class BrokenMessage(object):
+ """
+ L{BrokenMessage} is an L{IMessage} which raises an unexpected exception
+ from its C{eomReceived} method. This is useful for creating a server which
+ can be used to test client retry behavior.
+ """
+ implements(smtp.IMessage)
+
+ def __init__(self, user):
+ pass
+
+
+ def lineReceived(self, line):
+ pass
+
+
+ def eomReceived(self):
+ raise RuntimeError("Some problem, delivery is failing.")
+
+
+ def connectionLost(self):
+ pass
+
+
+
+class DummyMessage(object):
+ """
+ L{BrokenMessage} is an L{IMessage} which saves the message delivered to it
+ to its domain object.
+
+ @ivar domain: A L{DummyDomain} which will be used to store the message once
+ it is received.
+ """
+ def __init__(self, domain, user):
+ self.domain = domain
+ self.user = user
+ self.buffer = []
+
+
+ def lineReceived(self, line):
+ # Throw away the generated Received: header
+ if not re.match('Received: From yyy.com \(\[.*\]\) by localhost;', line):
+ self.buffer.append(line)
+
+
+ def eomReceived(self):
+ message = '\n'.join(self.buffer) + '\n'
+ self.domain.messages[self.user.dest.local].append(message)
+ deferred = defer.Deferred()
+ deferred.callback("saved")
+ return deferred
+
+
+
+class DummyDomain(object):
+ """
+ L{DummyDomain} is an L{IDomain} which keeps track of messages delivered to
+ it in memory.
+ """
+ def __init__(self, names):
+ self.messages = {}
+ for name in names:
+ self.messages[name] = []
+
+
+ def exists(self, user):
+ if user.dest.local in self.messages:
+ return defer.succeed(lambda: self.startMessage(user))
+ return defer.fail(smtp.SMTPBadRcpt(user))
+
+
+ def startMessage(self, user):
+ return DummyMessage(self, user)
+
+
+
+class SMTPTestCase(unittest.TestCase):
+
+ messages = [('foo@bar.com', ['foo@baz.com', 'qux@baz.com'], '''\
+Subject: urgent\015
+\015
+Someone set up us the bomb!\015
+''')]
+
+ mbox = {'foo': ['Subject: urgent\n\nSomeone set up us the bomb!\n']}
+
+ def setUp(self):
+ """
+ Create an in-memory mail domain to which messages may be delivered by
+ tests and create a factory and transport to do the delivering.
+ """
+ self.factory = smtp.SMTPFactory()
+ self.factory.domains = {}
+ self.factory.domains['baz.com'] = DummyDomain(['foo'])
+ self.transport = StringTransport()
+
+
+ def testMessages(self):
+ from twisted.mail import protocols
+ protocol = protocols.DomainSMTP()
+ protocol.service = self.factory
+ protocol.factory = self.factory
+ protocol.receivedHeader = spameater
+ protocol.makeConnection(self.transport)
+ protocol.lineReceived('HELO yyy.com')
+ for message in self.messages:
+ protocol.lineReceived('MAIL FROM:<%s>' % message[0])
+ for target in message[1]:
+ protocol.lineReceived('RCPT TO:<%s>' % target)
+ protocol.lineReceived('DATA')
+ protocol.dataReceived(message[2])
+ protocol.lineReceived('.')
+ protocol.lineReceived('QUIT')
+ if self.mbox != self.factory.domains['baz.com'].messages:
+ raise AssertionError(self.factory.domains['baz.com'].messages)
+ protocol.setTimeout(None)
+
+ testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)]
+
+mail = '''\
+Subject: hello
+
+Goodbye
+'''
+
+class MyClient:
+ def __init__(self, messageInfo=None):
+ if messageInfo is None:
+ messageInfo = (
+ 'moshez@foo.bar', ['moshez@foo.bar'], StringIO(mail))
+ self._sender = messageInfo[0]
+ self._recipient = messageInfo[1]
+ self._data = messageInfo[2]
+
+
+ def getMailFrom(self):
+ return self._sender
+
+
+ def getMailTo(self):
+ return self._recipient
+
+
+ def getMailData(self):
+ return self._data
+
+
+ def sendError(self, exc):
+ self._error = exc
+
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ # Prevent another mail from being sent.
+ self._sender = None
+ self._recipient = None
+ self._data = None
+
+
+
+class MySMTPClient(MyClient, smtp.SMTPClient):
+ def __init__(self, messageInfo=None):
+ smtp.SMTPClient.__init__(self, 'foo.baz')
+ MyClient.__init__(self, messageInfo)
+
+class MyESMTPClient(MyClient, smtp.ESMTPClient):
+ def __init__(self, secret = '', contextFactory = None):
+ smtp.ESMTPClient.__init__(self, secret, contextFactory, 'foo.baz')
+ MyClient.__init__(self)
+
+class LoopbackMixin:
+ def loopback(self, server, client):
+ return loopback.loopbackTCP(server, client)
+
+class LoopbackTestCase(LoopbackMixin):
+ def testMessages(self):
+ factory = smtp.SMTPFactory()
+ factory.domains = {}
+ factory.domains['foo.bar'] = DummyDomain(['moshez'])
+ from twisted.mail.protocols import DomainSMTP
+ protocol = DomainSMTP()
+ protocol.service = factory
+ protocol.factory = factory
+ clientProtocol = self.clientClass()
+ return self.loopback(protocol, clientProtocol)
+ testMessages.suppress = [util.suppress(message='DomainSMTP', category=DeprecationWarning)]
+
+class LoopbackSMTPTestCase(LoopbackTestCase, unittest.TestCase):
+ clientClass = MySMTPClient
+
+class LoopbackESMTPTestCase(LoopbackTestCase, unittest.TestCase):
+ clientClass = MyESMTPClient
+
+
+class FakeSMTPServer(basic.LineReceiver):
+
+ clientData = [
+ '220 hello', '250 nice to meet you',
+ '250 great', '250 great', '354 go on, lad'
+ ]
+
+ def connectionMade(self):
+ self.buffer = []
+ self.clientData = self.clientData[:]
+ self.clientData.reverse()
+ self.sendLine(self.clientData.pop())
+
+ def lineReceived(self, line):
+ self.buffer.append(line)
+ if line == "QUIT":
+ self.transport.write("221 see ya around\r\n")
+ self.transport.loseConnection()
+ elif line == ".":
+ self.transport.write("250 gotcha\r\n")
+ elif line == "RSET":
+ self.transport.loseConnection()
+
+ if self.clientData:
+ self.sendLine(self.clientData.pop())
+
+
+class SMTPClientTestCase(unittest.TestCase, LoopbackMixin):
+ """
+ Tests for L{smtp.SMTPClient}.
+ """
+
+ def test_timeoutConnection(self):
+ """
+ L{smtp.SMTPClient.timeoutConnection} calls the C{sendError} hook with a
+ fatal L{SMTPTimeoutError} with the current line log.
+ """
+ error = []
+ client = MySMTPClient()
+ client.sendError = error.append
+ client.makeConnection(StringTransport())
+ client.lineReceived("220 hello")
+ client.timeoutConnection()
+ self.assertIsInstance(error[0], smtp.SMTPTimeoutError)
+ self.assertTrue(error[0].isFatal)
+ self.assertEqual(
+ str(error[0]),
+ "Timeout waiting for SMTP server response\n"
+ "<<< 220 hello\n"
+ ">>> HELO foo.baz\n")
+
+
+ expected_output = [
+ 'HELO foo.baz', 'MAIL FROM:<moshez@foo.bar>',
+ 'RCPT TO:<moshez@foo.bar>', 'DATA',
+ 'Subject: hello', '', 'Goodbye', '.', 'RSET'
+ ]
+
+ def test_messages(self):
+ """
+ L{smtp.SMTPClient} sends I{HELO}, I{MAIL FROM}, I{RCPT TO}, and I{DATA}
+ commands based on the return values of its C{getMailFrom},
+ C{getMailTo}, and C{getMailData} methods.
+ """
+ client = MySMTPClient()
+ server = FakeSMTPServer()
+ d = self.loopback(server, client)
+ d.addCallback(lambda x :
+ self.assertEqual(server.buffer, self.expected_output))
+ return d
+
+
+ def test_transferError(self):
+ """
+ If there is an error while producing the message body to the
+ connection, the C{sendError} callback is invoked.
+ """
+ client = MySMTPClient(
+ ('alice@example.com', ['bob@example.com'], StringIO("foo")))
+ transport = StringTransport()
+ client.makeConnection(transport)
+ client.dataReceived(
+ '220 Ok\r\n' # Greeting
+ '250 Ok\r\n' # EHLO response
+ '250 Ok\r\n' # MAIL FROM response
+ '250 Ok\r\n' # RCPT TO response
+ '354 Ok\r\n' # DATA response
+ )
+
+ # Sanity check - a pull producer should be registered now.
+ self.assertNotIdentical(transport.producer, None)
+ self.assertFalse(transport.streaming)
+
+ # Now stop the producer prematurely, meaning the message was not sent.
+ transport.producer.stopProducing()
+
+ # The sendError hook should have been invoked as a result.
+ self.assertIsInstance(client._error, Exception)
+
+
+ def test_sendFatalError(self):
+ """
+ If L{smtp.SMTPClient.sendError} is called with an L{SMTPClientError}
+ which is fatal, it disconnects its transport without writing anything
+ more to it.
+ """
+ client = smtp.SMTPClient(None)
+ transport = StringTransport()
+ client.makeConnection(transport)
+ client.sendError(smtp.SMTPClientError(123, "foo", isFatal=True))
+ self.assertEqual(transport.value(), "")
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_sendNonFatalError(self):
+ """
+ If L{smtp.SMTPClient.sendError} is called with an L{SMTPClientError}
+ which is not fatal, it sends C{"QUIT"} and waits for the server to
+ close the connection.
+ """
+ client = smtp.SMTPClient(None)
+ transport = StringTransport()
+ client.makeConnection(transport)
+ client.sendError(smtp.SMTPClientError(123, "foo", isFatal=False))
+ self.assertEqual(transport.value(), "QUIT\r\n")
+ self.assertFalse(transport.disconnecting)
+
+
+ def test_sendOtherError(self):
+ """
+ If L{smtp.SMTPClient.sendError} is called with an exception which is
+ not an L{SMTPClientError}, it disconnects its transport without
+ writing anything more to it.
+ """
+ client = smtp.SMTPClient(None)
+ transport = StringTransport()
+ client.makeConnection(transport)
+ client.sendError(Exception("foo"))
+ self.assertEqual(transport.value(), "")
+ self.assertTrue(transport.disconnecting)
+
+
+
+class DummySMTPMessage:
+
+ def __init__(self, protocol, users):
+ self.protocol = protocol
+ self.users = users
+ self.buffer = []
+
+ def lineReceived(self, line):
+ self.buffer.append(line)
+
+ def eomReceived(self):
+ message = '\n'.join(self.buffer) + '\n'
+ helo, origin = self.users[0].helo[0], str(self.users[0].orig)
+ recipients = []
+ for user in self.users:
+ recipients.append(str(user))
+ self.protocol.message[tuple(recipients)] = (helo, origin, recipients, message)
+ return defer.succeed("saved")
+
+
+
+class DummyProto:
+ def connectionMade(self):
+ self.dummyMixinBase.connectionMade(self)
+ self.message = {}
+
+ def startMessage(self, users):
+ return DummySMTPMessage(self, users)
+
+ def receivedHeader(*spam):
+ return None
+
+ def validateTo(self, user):
+ self.delivery = SimpleDelivery(None)
+ return lambda: self.startMessage([user])
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+
+
+class DummySMTP(DummyProto, smtp.SMTP):
+ dummyMixinBase = smtp.SMTP
+
+class DummyESMTP(DummyProto, smtp.ESMTP):
+ dummyMixinBase = smtp.ESMTP
+
+class AnotherTestCase:
+ serverClass = None
+ clientClass = None
+
+ messages = [ ('foo.com', 'moshez@foo.com', ['moshez@bar.com'],
+ 'moshez@foo.com', ['moshez@bar.com'], '''\
+From: Moshe
+To: Moshe
+
+Hi,
+how are you?
+'''),
+ ('foo.com', 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'],
+ 'tttt@rrr.com', ['uuu@ooo', 'yyy@eee'], '''\
+Subject: pass
+
+..rrrr..
+'''),
+ ('foo.com', '@this,@is,@ignored:foo@bar.com',
+ ['@ignore,@this,@too:bar@foo.com'],
+ 'foo@bar.com', ['bar@foo.com'], '''\
+Subject: apa
+To: foo
+
+123
+.
+456
+'''),
+ ]
+
+ data = [
+ ('', '220.*\r\n$', None, None),
+ ('HELO foo.com\r\n', '250.*\r\n$', None, None),
+ ('RSET\r\n', '250.*\r\n$', None, None),
+ ]
+ for helo_, from_, to_, realfrom, realto, msg in messages:
+ data.append(('MAIL FROM:<%s>\r\n' % from_, '250.*\r\n',
+ None, None))
+ for rcpt in to_:
+ data.append(('RCPT TO:<%s>\r\n' % rcpt, '250.*\r\n',
+ None, None))
+
+ data.append(('DATA\r\n','354.*\r\n',
+ msg, ('250.*\r\n',
+ (helo_, realfrom, realto, msg))))
+
+
+ def test_buffer(self):
+ """
+ Exercise a lot of the SMTP client code. This is a "shotgun" style unit
+ test. It does a lot of things and hopes that something will go really
+ wrong if it is going to go wrong. This test should be replaced with a
+ suite of nicer tests.
+ """
+ transport = StringTransport()
+ a = self.serverClass()
+ class fooFactory:
+ domain = 'foo.com'
+
+ a.factory = fooFactory()
+ a.makeConnection(transport)
+ for (send, expect, msg, msgexpect) in self.data:
+ if send:
+ a.dataReceived(send)
+ data = transport.value()
+ transport.clear()
+ if not re.match(expect, data):
+ raise AssertionError, (send, expect, data)
+ if data[:3] == '354':
+ for line in msg.splitlines():
+ if line and line[0] == '.':
+ line = '.' + line
+ a.dataReceived(line + '\r\n')
+ a.dataReceived('.\r\n')
+ # Special case for DATA. Now we want a 250, and then
+ # we compare the messages
+ data = transport.value()
+ transport.clear()
+ resp, msgdata = msgexpect
+ if not re.match(resp, data):
+ raise AssertionError, (resp, data)
+ for recip in msgdata[2]:
+ expected = list(msgdata[:])
+ expected[2] = [recip]
+ self.assertEqual(
+ a.message[(recip,)],
+ tuple(expected)
+ )
+ a.setTimeout(None)
+
+
+class AnotherESMTPTestCase(AnotherTestCase, unittest.TestCase):
+ serverClass = DummyESMTP
+ clientClass = MyESMTPClient
+
+class AnotherSMTPTestCase(AnotherTestCase, unittest.TestCase):
+ serverClass = DummySMTP
+ clientClass = MySMTPClient
+
+
+
+class DummyChecker:
+ implements(cred.checkers.ICredentialsChecker)
+
+ users = {
+ 'testuser': 'testpassword'
+ }
+
+ credentialInterfaces = (cred.credentials.IUsernamePassword,
+ cred.credentials.IUsernameHashedPassword)
+
+ def requestAvatarId(self, credentials):
+ return defer.maybeDeferred(
+ credentials.checkPassword, self.users[credentials.username]
+ ).addCallback(self._cbCheck, credentials.username)
+
+ def _cbCheck(self, result, username):
+ if result:
+ return username
+ raise cred.error.UnauthorizedLogin()
+
+
+
+class SimpleDelivery(object):
+ """
+ L{SimpleDelivery} is a message delivery factory with no interesting
+ behavior.
+ """
+ implements(smtp.IMessageDelivery)
+
+ def __init__(self, messageFactory):
+ self._messageFactory = messageFactory
+
+
+ def receivedHeader(self, helo, origin, recipients):
+ return None
+
+
+ def validateFrom(self, helo, origin):
+ return origin
+
+
+ def validateTo(self, user):
+ return lambda: self._messageFactory(user)
+
+
+
+class DummyRealm:
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return smtp.IMessageDelivery, SimpleDelivery(None), lambda: None
+
+
+
+class AuthTestCase(unittest.TestCase, LoopbackMixin):
+ def test_crammd5Auth(self):
+ """
+ L{ESMTPClient} can authenticate using the I{CRAM-MD5} SASL mechanism.
+
+ @see: U{http://tools.ietf.org/html/rfc2195}
+ """
+ realm = DummyRealm()
+ p = cred.portal.Portal(realm)
+ p.registerChecker(DummyChecker())
+
+ server = DummyESMTP({'CRAM-MD5': cred.credentials.CramMD5Credentials})
+ server.portal = p
+ client = MyESMTPClient('testpassword')
+
+ cAuth = smtp.CramMD5ClientAuthenticator('testuser')
+ client.registerAuthenticator(cAuth)
+
+ d = self.loopback(server, client)
+ d.addCallback(lambda x : self.assertEqual(server.authenticated, 1))
+ return d
+
+
+ def test_loginAuth(self):
+ """
+ L{ESMTPClient} can authenticate using the I{LOGIN} SASL mechanism.
+
+ @see: U{http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt}
+ """
+ realm = DummyRealm()
+ p = cred.portal.Portal(realm)
+ p.registerChecker(DummyChecker())
+
+ server = DummyESMTP({'LOGIN': imap4.LOGINCredentials})
+ server.portal = p
+ client = MyESMTPClient('testpassword')
+
+ cAuth = smtp.LOGINAuthenticator('testuser')
+ client.registerAuthenticator(cAuth)
+
+ d = self.loopback(server, client)
+ d.addCallback(lambda x: self.assertTrue(server.authenticated))
+ return d
+
+
+ def test_loginAgainstWeirdServer(self):
+ """
+ When communicating with a server which implements the I{LOGIN} SASL
+ mechanism using C{"Username:"} as the challenge (rather than C{"User
+ Name\\0"}), L{ESMTPClient} can still authenticate successfully using
+ the I{LOGIN} mechanism.
+ """
+ realm = DummyRealm()
+ p = cred.portal.Portal(realm)
+ p.registerChecker(DummyChecker())
+
+ server = DummyESMTP({'LOGIN': smtp.LOGINCredentials})
+ server.portal = p
+
+ client = MyESMTPClient('testpassword')
+ cAuth = smtp.LOGINAuthenticator('testuser')
+ client.registerAuthenticator(cAuth)
+
+ d = self.loopback(server, client)
+ d.addCallback(lambda x: self.assertTrue(server.authenticated))
+ return d
+
+
+
+class SMTPHelperTestCase(unittest.TestCase):
+ def testMessageID(self):
+ d = {}
+ for i in range(1000):
+ m = smtp.messageid('testcase')
+ self.failIf(m in d)
+ d[m] = None
+
+ def testQuoteAddr(self):
+ cases = [
+ ['user@host.name', '<user@host.name>'],
+ ['"User Name" <user@host.name>', '<user@host.name>'],
+ [smtp.Address('someguy@someplace'), '<someguy@someplace>'],
+ ['', '<>'],
+ [smtp.Address(''), '<>'],
+ ]
+
+ for (c, e) in cases:
+ self.assertEqual(smtp.quoteaddr(c), e)
+
+ def testUser(self):
+ u = smtp.User('user@host', 'helo.host.name', None, None)
+ self.assertEqual(str(u), 'user@host')
+
+ def testXtextEncoding(self):
+ cases = [
+ ('Hello world', 'Hello+20world'),
+ ('Hello+world', 'Hello+2Bworld'),
+ ('\0\1\2\3\4\5', '+00+01+02+03+04+05'),
+ ('e=mc2@example.com', 'e+3Dmc2@example.com')
+ ]
+
+ for (case, expected) in cases:
+ self.assertEqual(smtp.xtext_encode(case), (expected, len(case)))
+ self.assertEqual(case.encode('xtext'), expected)
+ self.assertEqual(
+ smtp.xtext_decode(expected), (case, len(expected)))
+ self.assertEqual(expected.decode('xtext'), case)
+
+
+ def test_encodeWithErrors(self):
+ """
+ Specifying an error policy to C{unicode.encode} with the
+ I{xtext} codec should produce the same result as not
+ specifying the error policy.
+ """
+ text = u'Hello world'
+ self.assertEqual(
+ smtp.xtext_encode(text, 'strict'),
+ (text.encode('xtext'), len(text)))
+ self.assertEqual(
+ text.encode('xtext', 'strict'),
+ text.encode('xtext'))
+
+
+ def test_decodeWithErrors(self):
+ """
+ Similar to L{test_encodeWithErrors}, but for C{str.decode}.
+ """
+ bytes = 'Hello world'
+ self.assertEqual(
+ smtp.xtext_decode(bytes, 'strict'),
+ (bytes.decode('xtext'), len(bytes)))
+ self.assertEqual(
+ bytes.decode('xtext', 'strict'),
+ bytes.decode('xtext'))
+
+
+
+class NoticeTLSClient(MyESMTPClient):
+ tls = False
+
+ def esmtpState_starttls(self, code, resp):
+ MyESMTPClient.esmtpState_starttls(self, code, resp)
+ self.tls = True
+
+class TLSTestCase(unittest.TestCase, LoopbackMixin):
+ def testTLS(self):
+ clientCTX = ClientTLSContext()
+ serverCTX = ServerTLSContext()
+
+ client = NoticeTLSClient(contextFactory=clientCTX)
+ server = DummyESMTP(contextFactory=serverCTX)
+
+ def check(ignored):
+ self.assertEqual(client.tls, True)
+ self.assertEqual(server.startedTLS, True)
+
+ return self.loopback(server, client).addCallback(check)
+
+if ClientTLSContext is None:
+ for case in (TLSTestCase,):
+ case.skip = "OpenSSL not present"
+
+if not interfaces.IReactorSSL.providedBy(reactor):
+ for case in (TLSTestCase,):
+ case.skip = "Reactor doesn't support SSL"
+
+class EmptyLineTestCase(unittest.TestCase):
+ def test_emptyLineSyntaxError(self):
+ """
+ If L{smtp.SMTP} receives an empty line, it responds with a 500 error
+ response code and a message about a syntax error.
+ """
+ proto = smtp.SMTP()
+ transport = StringTransport()
+ proto.makeConnection(transport)
+ proto.lineReceived('')
+ proto.setTimeout(None)
+
+ out = transport.value().splitlines()
+ self.assertEqual(len(out), 2)
+ self.failUnless(out[0].startswith('220'))
+ self.assertEqual(out[1], "500 Error: bad syntax")
+
+
+
+class TimeoutTestCase(unittest.TestCase, LoopbackMixin):
+ """
+ Check that SMTP client factories correctly use the timeout.
+ """
+
+ def _timeoutTest(self, onDone, clientFactory):
+ """
+ Connect the clientFactory, and check the timeout on the request.
+ """
+ clock = task.Clock()
+ client = clientFactory.buildProtocol(
+ address.IPv4Address('TCP', 'example.net', 25))
+ client.callLater = clock.callLater
+ t = StringTransport()
+ client.makeConnection(t)
+ t.protocol = client
+ def check(ign):
+ self.assertEqual(clock.seconds(), 0.5)
+ d = self.assertFailure(onDone, smtp.SMTPTimeoutError
+ ).addCallback(check)
+ # The first call should not trigger the timeout
+ clock.advance(0.1)
+ # But this one should
+ clock.advance(0.4)
+ return d
+
+
+ def test_SMTPClient(self):
+ """
+ Test timeout for L{smtp.SMTPSenderFactory}: the response L{Deferred}
+ should be errback with a L{smtp.SMTPTimeoutError}.
+ """
+ onDone = defer.Deferred()
+ clientFactory = smtp.SMTPSenderFactory(
+ 'source@address', 'recipient@address',
+ StringIO("Message body"), onDone,
+ retries=0, timeout=0.5)
+ return self._timeoutTest(onDone, clientFactory)
+
+
+ def test_ESMTPClient(self):
+ """
+ Test timeout for L{smtp.ESMTPSenderFactory}: the response L{Deferred}
+ should be errback with a L{smtp.SMTPTimeoutError}.
+ """
+ onDone = defer.Deferred()
+ clientFactory = smtp.ESMTPSenderFactory(
+ 'username', 'password',
+ 'source@address', 'recipient@address',
+ StringIO("Message body"), onDone,
+ retries=0, timeout=0.5)
+ return self._timeoutTest(onDone, clientFactory)
+
+
+ def test_resetTimeoutWhileSending(self):
+ """
+ The timeout is not allowed to expire after the server has accepted a
+ DATA command and the client is actively sending data to it.
+ """
+ class SlowFile:
+ """
+ A file-like which returns one byte from each read call until the
+ specified number of bytes have been returned.
+ """
+ def __init__(self, size):
+ self._size = size
+
+ def read(self, max=None):
+ if self._size:
+ self._size -= 1
+ return 'x'
+ return ''
+
+ failed = []
+ onDone = defer.Deferred()
+ onDone.addErrback(failed.append)
+ clientFactory = smtp.SMTPSenderFactory(
+ 'source@address', 'recipient@address',
+ SlowFile(1), onDone, retries=0, timeout=3)
+ clientFactory.domain = "example.org"
+ clock = task.Clock()
+ client = clientFactory.buildProtocol(
+ address.IPv4Address('TCP', 'example.net', 25))
+ client.callLater = clock.callLater
+ transport = StringTransport()
+ client.makeConnection(transport)
+
+ client.dataReceived(
+ "220 Ok\r\n" # Greet the client
+ "250 Ok\r\n" # Respond to HELO
+ "250 Ok\r\n" # Respond to MAIL FROM
+ "250 Ok\r\n" # Respond to RCPT TO
+ "354 Ok\r\n" # Respond to DATA
+ )
+
+ # Now the client is producing data to the server. Any time
+ # resumeProducing is called on the producer, the timeout should be
+ # extended. First, a sanity check. This test is only written to
+ # handle pull producers.
+ self.assertNotIdentical(transport.producer, None)
+ self.assertFalse(transport.streaming)
+
+ # Now, allow 2 seconds (1 less than the timeout of 3 seconds) to
+ # elapse.
+ clock.advance(2)
+
+ # The timeout has not expired, so the failure should not have happened.
+ self.assertEqual(failed, [])
+
+ # Let some bytes be produced, extending the timeout. Then advance the
+ # clock some more and verify that the timeout still hasn't happened.
+ transport.producer.resumeProducing()
+ clock.advance(2)
+ self.assertEqual(failed, [])
+
+ # The file has been completely produced - the next resume producing
+ # finishes the upload, successfully.
+ transport.producer.resumeProducing()
+ client.dataReceived("250 Ok\r\n")
+ self.assertEqual(failed, [])
+
+ # Verify that the client actually did send the things expected.
+ self.assertEqual(
+ transport.value(),
+ "HELO example.org\r\n"
+ "MAIL FROM:<source@address>\r\n"
+ "RCPT TO:<recipient@address>\r\n"
+ "DATA\r\n"
+ "x\r\n"
+ ".\r\n"
+ # This RSET is just an implementation detail. It's nice, but this
+ # test doesn't really care about it.
+ "RSET\r\n")
+
+
+
+class MultipleDeliveryFactorySMTPServerFactory(protocol.ServerFactory):
+ """
+ L{MultipleDeliveryFactorySMTPServerFactory} creates SMTP server protocol
+ instances with message delivery factory objects supplied to it. Each
+ factory is used for one connection and then discarded. Factories are used
+ in the order they are supplied.
+ """
+ def __init__(self, messageFactories):
+ self._messageFactories = messageFactories
+
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ p.delivery = SimpleDelivery(self._messageFactories.pop(0))
+ return p
+
+
+
+class SMTPSenderFactoryRetryTestCase(unittest.TestCase):
+ """
+ Tests for the retry behavior of L{smtp.SMTPSenderFactory}.
+ """
+ def test_retryAfterDisconnect(self):
+ """
+ If the protocol created by L{SMTPSenderFactory} loses its connection
+ before receiving confirmation of message delivery, it reconnects and
+ tries to deliver the message again.
+ """
+ recipient = 'alice'
+ message = "some message text"
+ domain = DummyDomain([recipient])
+
+ class CleanSMTP(smtp.SMTP):
+ """
+ An SMTP subclass which ensures that its transport will be
+ disconnected before the test ends.
+ """
+ def makeConnection(innerSelf, transport):
+ self.addCleanup(transport.loseConnection)
+ smtp.SMTP.makeConnection(innerSelf, transport)
+
+ # Create a server which will fail the first message deliver attempt to
+ # it with a 500 and a disconnect, but which will accept a message
+ # delivered over the 2nd connection to it.
+ serverFactory = MultipleDeliveryFactorySMTPServerFactory([
+ BrokenMessage,
+ lambda user: DummyMessage(domain, user)])
+ serverFactory.protocol = CleanSMTP
+ serverPort = reactor.listenTCP(0, serverFactory, interface='127.0.0.1')
+ serverHost = serverPort.getHost()
+ self.addCleanup(serverPort.stopListening)
+
+ # Set up a client to try to deliver a message to the above created
+ # server.
+ sentDeferred = defer.Deferred()
+ clientFactory = smtp.SMTPSenderFactory(
+ "bob@example.org", recipient + "@example.com",
+ StringIO(message), sentDeferred)
+ clientFactory.domain = "example.org"
+ clientConnector = reactor.connectTCP(
+ serverHost.host, serverHost.port, clientFactory)
+ self.addCleanup(clientConnector.disconnect)
+
+ def cbSent(ignored):
+ """
+ Verify that the message was successfully delivered and flush the
+ error which caused the first attempt to fail.
+ """
+ self.assertEqual(
+ domain.messages,
+ {recipient: ["\n%s\n" % (message,)]})
+ # Flush the RuntimeError that BrokenMessage caused to be logged.
+ self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
+ sentDeferred.addCallback(cbSent)
+ return sentDeferred
+
+
+
+class SingletonRealm(object):
+ """
+ Trivial realm implementation which is constructed with an interface and an
+ avatar and returns that avatar when asked for that interface.
+ """
+ implements(IRealm)
+
+ def __init__(self, interface, avatar):
+ self.interface = interface
+ self.avatar = avatar
+
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ for iface in interfaces:
+ if iface is self.interface:
+ return iface, self.avatar, lambda: None
+
+
+
+class NotImplementedDelivery(object):
+ """
+ Non-implementation of L{smtp.IMessageDelivery} which only has methods which
+ raise L{NotImplementedError}. Subclassed by various tests to provide the
+ particular behavior being tested.
+ """
+ def validateFrom(self, helo, origin):
+ raise NotImplementedError("This oughtn't be called in the course of this test.")
+
+
+ def validateTo(self, user):
+ raise NotImplementedError("This oughtn't be called in the course of this test.")
+
+
+ def receivedHeader(self, helo, origin, recipients):
+ raise NotImplementedError("This oughtn't be called in the course of this test.")
+
+
+
+class SMTPServerTestCase(unittest.TestCase):
+ """
+ Test various behaviors of L{twisted.mail.smtp.SMTP} and
+ L{twisted.mail.smtp.ESMTP}.
+ """
+ def testSMTPGreetingHost(self, serverClass=smtp.SMTP):
+ """
+ Test that the specified hostname shows up in the SMTP server's
+ greeting.
+ """
+ s = serverClass()
+ s.host = "example.com"
+ t = StringTransport()
+ s.makeConnection(t)
+ s.connectionLost(error.ConnectionDone())
+ self.assertIn("example.com", t.value())
+
+
+ def testSMTPGreetingNotExtended(self):
+ """
+ Test that the string "ESMTP" does not appear in the SMTP server's
+ greeting since that string strongly suggests the presence of support
+ for various SMTP extensions which are not supported by L{smtp.SMTP}.
+ """
+ s = smtp.SMTP()
+ t = StringTransport()
+ s.makeConnection(t)
+ s.connectionLost(error.ConnectionDone())
+ self.assertNotIn("ESMTP", t.value())
+
+
+ def testESMTPGreetingHost(self):
+ """
+ Similar to testSMTPGreetingHost, but for the L{smtp.ESMTP} class.
+ """
+ self.testSMTPGreetingHost(smtp.ESMTP)
+
+
+ def testESMTPGreetingExtended(self):
+ """
+ Test that the string "ESMTP" does appear in the ESMTP server's
+ greeting since L{smtp.ESMTP} does support the SMTP extensions which
+ that advertises to the client.
+ """
+ s = smtp.ESMTP()
+ t = StringTransport()
+ s.makeConnection(t)
+ s.connectionLost(error.ConnectionDone())
+ self.assertIn("ESMTP", t.value())
+
+
+ def test_acceptSenderAddress(self):
+ """
+ Test that a C{MAIL FROM} command with an acceptable address is
+ responded to with the correct success code.
+ """
+ class AcceptanceDelivery(NotImplementedDelivery):
+ """
+ Delivery object which accepts all senders as valid.
+ """
+ def validateFrom(self, helo, origin):
+ return origin
+
+ realm = SingletonRealm(smtp.IMessageDelivery, AcceptanceDelivery())
+ portal = Portal(realm, [AllowAnonymousAccess()])
+ proto = smtp.SMTP()
+ proto.portal = portal
+ trans = StringTransport()
+ proto.makeConnection(trans)
+
+ # Deal with the necessary preliminaries
+ proto.dataReceived('HELO example.com\r\n')
+ trans.clear()
+
+ # Try to specify our sender address
+ proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
+
+ # Clean up the protocol before doing anything that might raise an
+ # exception.
+ proto.connectionLost(error.ConnectionLost())
+
+ # Make sure that we received exactly the correct response
+ self.assertEqual(
+ trans.value(),
+ '250 Sender address accepted\r\n')
+
+
+ def test_deliveryRejectedSenderAddress(self):
+ """
+ Test that a C{MAIL FROM} command with an address rejected by a
+ L{smtp.IMessageDelivery} instance is responded to with the correct
+ error code.
+ """
+ class RejectionDelivery(NotImplementedDelivery):
+ """
+ Delivery object which rejects all senders as invalid.
+ """
+ def validateFrom(self, helo, origin):
+ raise smtp.SMTPBadSender(origin)
+
+ realm = SingletonRealm(smtp.IMessageDelivery, RejectionDelivery())
+ portal = Portal(realm, [AllowAnonymousAccess()])
+ proto = smtp.SMTP()
+ proto.portal = portal
+ trans = StringTransport()
+ proto.makeConnection(trans)
+
+ # Deal with the necessary preliminaries
+ proto.dataReceived('HELO example.com\r\n')
+ trans.clear()
+
+ # Try to specify our sender address
+ proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
+
+ # Clean up the protocol before doing anything that might raise an
+ # exception.
+ proto.connectionLost(error.ConnectionLost())
+
+ # Make sure that we received exactly the correct response
+ self.assertEqual(
+ trans.value(),
+ '550 Cannot receive from specified address '
+ '<alice@example.com>: Sender not acceptable\r\n')
+
+
+ def test_portalRejectedSenderAddress(self):
+ """
+ Test that a C{MAIL FROM} command with an address rejected by an
+ L{smtp.SMTP} instance's portal is responded to with the correct error
+ code.
+ """
+ class DisallowAnonymousAccess(object):
+ """
+ Checker for L{IAnonymous} which rejects authentication attempts.
+ """
+ implements(ICredentialsChecker)
+
+ credentialInterfaces = (IAnonymous,)
+
+ def requestAvatarId(self, credentials):
+ return defer.fail(UnauthorizedLogin())
+
+ realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery())
+ portal = Portal(realm, [DisallowAnonymousAccess()])
+ proto = smtp.SMTP()
+ proto.portal = portal
+ trans = StringTransport()
+ proto.makeConnection(trans)
+
+ # Deal with the necessary preliminaries
+ proto.dataReceived('HELO example.com\r\n')
+ trans.clear()
+
+ # Try to specify our sender address
+ proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
+
+ # Clean up the protocol before doing anything that might raise an
+ # exception.
+ proto.connectionLost(error.ConnectionLost())
+
+ # Make sure that we received exactly the correct response
+ self.assertEqual(
+ trans.value(),
+ '550 Cannot receive from specified address '
+ '<alice@example.com>: Sender not acceptable\r\n')
+
+
+ def test_portalRejectedAnonymousSender(self):
+ """
+ Test that a C{MAIL FROM} command issued without first authenticating
+ when a portal has been configured to disallow anonymous logins is
+ responded to with the correct error code.
+ """
+ realm = SingletonRealm(smtp.IMessageDelivery, NotImplementedDelivery())
+ portal = Portal(realm, [])
+ proto = smtp.SMTP()
+ proto.portal = portal
+ trans = StringTransport()
+ proto.makeConnection(trans)
+
+ # Deal with the necessary preliminaries
+ proto.dataReceived('HELO example.com\r\n')
+ trans.clear()
+
+ # Try to specify our sender address
+ proto.dataReceived('MAIL FROM:<alice@example.com>\r\n')
+
+ # Clean up the protocol before doing anything that might raise an
+ # exception.
+ proto.connectionLost(error.ConnectionLost())
+
+ # Make sure that we received exactly the correct response
+ self.assertEqual(
+ trans.value(),
+ '550 Cannot receive from specified address '
+ '<alice@example.com>: Unauthenticated senders not allowed\r\n')
+
+
+
+class ESMTPAuthenticationTestCase(unittest.TestCase):
+ def assertServerResponse(self, bytes, response):
+ """
+ Assert that when the given bytes are delivered to the ESMTP server
+ instance, it responds with the indicated lines.
+
+ @type bytes: str
+ @type response: list of str
+ """
+ self.transport.clear()
+ self.server.dataReceived(bytes)
+ self.assertEqual(
+ response,
+ self.transport.value().splitlines())
+
+
+ def assertServerAuthenticated(self, loginArgs, username="username", password="password"):
+ """
+ Assert that a login attempt has been made, that the credentials and
+ interfaces passed to it are correct, and that when the login request
+ is satisfied, a successful response is sent by the ESMTP server
+ instance.
+
+ @param loginArgs: A C{list} previously passed to L{portalFactory}.
+ """
+ d, credentials, mind, interfaces = loginArgs.pop()
+ self.assertEqual(loginArgs, [])
+ self.failUnless(twisted.cred.credentials.IUsernamePassword.providedBy(credentials))
+ self.assertEqual(credentials.username, username)
+ self.failUnless(credentials.checkPassword(password))
+ self.assertIn(smtp.IMessageDeliveryFactory, interfaces)
+ self.assertIn(smtp.IMessageDelivery, interfaces)
+ d.callback((smtp.IMessageDeliveryFactory, None, lambda: None))
+
+ self.assertEqual(
+ ["235 Authentication successful."],
+ self.transport.value().splitlines())
+
+
+ def setUp(self):
+ """
+ Create an ESMTP instance attached to a StringTransport.
+ """
+ self.server = smtp.ESMTP({
+ 'LOGIN': imap4.LOGINCredentials})
+ self.server.host = 'localhost'
+ self.transport = StringTransport(
+ peerAddress=address.IPv4Address('TCP', '127.0.0.1', 12345))
+ self.server.makeConnection(self.transport)
+
+
+ def tearDown(self):
+ """
+ Disconnect the ESMTP instance to clean up its timeout DelayedCall.
+ """
+ self.server.connectionLost(error.ConnectionDone())
+
+
+ def portalFactory(self, loginList):
+ class DummyPortal:
+ def login(self, credentials, mind, *interfaces):
+ d = defer.Deferred()
+ loginList.append((d, credentials, mind, interfaces))
+ return d
+ return DummyPortal()
+
+
+ def test_authenticationCapabilityAdvertised(self):
+ """
+ Test that AUTH is advertised to clients which issue an EHLO command.
+ """
+ self.transport.clear()
+ self.server.dataReceived('EHLO\r\n')
+ responseLines = self.transport.value().splitlines()
+ self.assertEqual(
+ responseLines[0],
+ "250-localhost Hello 127.0.0.1, nice to meet you")
+ self.assertEqual(
+ responseLines[1],
+ "250 AUTH LOGIN")
+ self.assertEqual(len(responseLines), 2)
+
+
+ def test_plainAuthentication(self):
+ """
+ Test that the LOGIN authentication mechanism can be used
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.transport.clear()
+
+ self.assertServerResponse(
+ 'AUTH LOGIN\r\n',
+ ["334 " + "User Name\0".encode('base64').strip()])
+
+ self.assertServerResponse(
+ 'username'.encode('base64') + '\r\n',
+ ["334 " + "Password\0".encode('base64').strip()])
+
+ self.assertServerResponse(
+ 'password'.encode('base64').strip() + '\r\n',
+ [])
+
+ self.assertServerAuthenticated(loginArgs)
+
+
+ def test_plainAuthenticationEmptyPassword(self):
+ """
+ Test that giving an empty password for plain auth succeeds.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.transport.clear()
+
+ self.assertServerResponse(
+ 'AUTH LOGIN\r\n',
+ ["334 " + "User Name\0".encode('base64').strip()])
+
+ self.assertServerResponse(
+ 'username'.encode('base64') + '\r\n',
+ ["334 " + "Password\0".encode('base64').strip()])
+
+ self.assertServerResponse('\r\n', [])
+ self.assertServerAuthenticated(loginArgs, password='')
+
+
+ def test_plainAuthenticationInitialResponse(self):
+ """
+ The response to the first challenge may be included on the AUTH command
+ line. Test that this is also supported.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.transport.clear()
+
+ self.assertServerResponse(
+ 'AUTH LOGIN ' + "username".encode('base64').strip() + '\r\n',
+ ["334 " + "Password\0".encode('base64').strip()])
+
+ self.assertServerResponse(
+ 'password'.encode('base64').strip() + '\r\n',
+ [])
+
+ self.assertServerAuthenticated(loginArgs)
+
+
+ def test_abortAuthentication(self):
+ """
+ Test that a challenge/response sequence can be aborted by the client.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.server.dataReceived('AUTH LOGIN\r\n')
+
+ self.assertServerResponse(
+ '*\r\n',
+ ['501 Authentication aborted'])
+
+
+ def test_invalidBase64EncodedResponse(self):
+ """
+ Test that a response which is not properly Base64 encoded results in
+ the appropriate error code.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.server.dataReceived('AUTH LOGIN\r\n')
+
+ self.assertServerResponse(
+ 'x\r\n',
+ ['501 Syntax error in parameters or arguments'])
+
+ self.assertEqual(loginArgs, [])
+
+
+ def test_invalidBase64EncodedInitialResponse(self):
+ """
+ Like L{test_invalidBase64EncodedResponse} but for the case of an
+ initial response included with the C{AUTH} command.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.assertServerResponse(
+ 'AUTH LOGIN x\r\n',
+ ['501 Syntax error in parameters or arguments'])
+
+ self.assertEqual(loginArgs, [])
+
+
+ def test_unexpectedLoginFailure(self):
+ """
+ If the L{Deferred} returned by L{Portal.login} fires with an
+ exception of any type other than L{UnauthorizedLogin}, the exception
+ is logged and the client is informed that the authentication attempt
+ has failed.
+ """
+ loginArgs = []
+ self.server.portal = self.portalFactory(loginArgs)
+
+ self.server.dataReceived('EHLO\r\n')
+ self.transport.clear()
+
+ self.assertServerResponse(
+ 'AUTH LOGIN ' + 'username'.encode('base64').strip() + '\r\n',
+ ['334 ' + 'Password\0'.encode('base64').strip()])
+ self.assertServerResponse(
+ 'password'.encode('base64').strip() + '\r\n',
+ [])
+
+ d, credentials, mind, interfaces = loginArgs.pop()
+ d.errback(RuntimeError("Something wrong with the server"))
+
+ self.assertEqual(
+ '451 Requested action aborted: local error in processing\r\n',
+ self.transport.value())
+
+ self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
+
+
+
+class SMTPClientErrorTestCase(unittest.TestCase):
+ """
+ Tests for L{smtp.SMTPClientError}.
+ """
+ def test_str(self):
+ """
+ The string representation of a L{SMTPClientError} instance includes
+ the response code and response string.
+ """
+ err = smtp.SMTPClientError(123, "some text")
+ self.assertEqual(str(err), "123 some text")
+
+
+ def test_strWithNegativeCode(self):
+ """
+ If the response code supplied to L{SMTPClientError} is negative, it
+ is excluded from the string representation.
+ """
+ err = smtp.SMTPClientError(-1, "foo bar")
+ self.assertEqual(str(err), "foo bar")
+
+
+ def test_strWithLog(self):
+ """
+ If a line log is supplied to L{SMTPClientError}, its contents are
+ included in the string representation of the exception instance.
+ """
+ log = LineLog(10)
+ log.append("testlog")
+ log.append("secondline")
+ err = smtp.SMTPClientError(100, "test error", log=log.str())
+ self.assertEqual(
+ str(err),
+ "100 test error\n"
+ "testlog\n"
+ "secondline\n")
+
+
+
+class SenderMixinSentMailTests(unittest.TestCase):
+ """
+ Tests for L{smtp.SenderMixin.sentMail}, used in particular by
+ L{smtp.SMTPSenderFactory} and L{smtp.ESMTPSenderFactory}.
+ """
+ def test_onlyLogFailedAddresses(self):
+ """
+ L{smtp.SenderMixin.sentMail} adds only the addresses with failing
+ SMTP response codes to the log passed to the factory's errback.
+ """
+ onDone = self.assertFailure(defer.Deferred(), smtp.SMTPDeliveryError)
+ onDone.addCallback(lambda e: self.assertEqual(
+ e.log, "bob@example.com: 199 Error in sending.\n"))
+
+ clientFactory = smtp.SMTPSenderFactory(
+ 'source@address', 'recipient@address',
+ StringIO("Message body"), onDone,
+ retries=0, timeout=0.5)
+
+ client = clientFactory.buildProtocol(
+ address.IPv4Address('TCP', 'example.net', 25))
+
+ addresses = [("alice@example.com", 200, "No errors here!"),
+ ("bob@example.com", 199, "Error in sending.")]
+ client.sentMail(199, "Test response", 1, addresses, client.log)
+
+ return onDone
diff --git a/twisted/mail/topfiles/NEWS b/twisted/mail/topfiles/NEWS
new file mode 100644
index 0000000..ee24d44
--- /dev/null
+++ b/twisted/mail/topfiles/NEWS
@@ -0,0 +1,289 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Mail 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twistd mail --auth, broken in 11.0, now correctly connects
+ authentication to the portal being used (#5219)
+
+Other
+-----
+ - #5686
+
+
+Twisted Mail 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Mail 11.1.0 (2011-11-15)
+================================
+
+Features
+--------
+ - twisted.mail.smtp.LOGINCredentials now generates challenges with
+ ":" instead of "\0" for interoperability with Microsoft Outlook.
+ (#4692)
+
+Bugfixes
+--------
+ - When run from an unpacked source tarball or a VCS checkout,
+ bin/mail/mailmail will now use the version of Twisted it is part
+ of. (#3526)
+
+Other
+-----
+ - #4796, #5006
+
+
+Twisted Mail 11.0.0 (2011-04-01)
+================================
+
+Features
+--------
+ - The `twistd mail` command line now accepts endpoint descriptions
+ for POP3 and SMTP servers. (#4739)
+ - The twistd mail plugin now accepts new authentication options via
+ strcred.AuthOptionMixin. These include --auth, --auth-help, and
+ authentication type-specific help options. (#4740)
+
+Bugfixes
+--------
+ - twisted.mail.imap4.IMAP4Server now generates INTERNALDATE strings
+ which do not consider the locale. (#4937)
+
+Improved Documentation
+----------------------
+ - Added a simple SMTP example, showing how to use sendmail. (#4042)
+
+Other
+-----
+
+ - #4162
+
+
+Twisted Mail 10.2.0 (2010-11-29)
+================================
+
+Improved Documentation
+----------------------
+ - The email server example now demonstrates how to set up
+ authentication and authorization using twisted.cred. (#4609)
+
+Deprecations and Removals
+-------------------------
+ - twisted.mail.smtp.sendEmail, deprecated since mid 2003 (before
+ Twisted 2.0), has been removed. (#4529)
+
+Other
+-----
+ - #4038, #4572
+
+
+Twisted Mail 10.1.0 (2010-06-27)
+================================
+
+Bugfixes
+--------
+ - twisted.mail.imap4.IMAP4Server no longer fails on search queries
+ that contain wildcards. (#2278)
+ - A case which would cause twisted.mail.imap4.IMAP4Server to loop
+ indefinitely when handling a search command has been fixed. (#4385)
+
+Other
+-----
+ - #4069, #4271, #4467
+
+
+Twisted Mail 10.0.0 (2010-03-01)
+================================
+
+Bugfixes
+--------
+ - twisted.mail.smtp.ESMTPClient and
+ twisted.mail.smtp.LOGINAuthenticator now implement the (obsolete)
+ LOGIN SASL mechanism according to the draft specification. (#4031)
+
+ - twisted.mail.imap4.IMAP4Client will no longer misparse all html-
+ formatted message bodies received in response to a fetch command.
+ (#4049)
+
+ - The regression in IMAP4 search handling of "OR" and "NOT" terms has
+ been fixed. (#4178)
+
+Other
+-----
+ - #4028, #4170, #4200
+
+
+Twisted Mail 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - maildir.StringListMailbox, an in-memory maildir mailbox, now supports
+ deletion, undeletion, and syncing (#3547)
+ - SMTPClient's callbacks are now more completely documented (#684)
+
+Fixes
+-----
+ - Parse UNSEEN response data and include it in the result of
+ IMAP4Client.examine (#3550)
+ - The IMAP4 client now delivers more unsolicited server responses to callbacks
+ rather than ignoring them, and also won't ignore solicited responses that
+ arrive on the same line as an unsolicited one (#1105)
+ - Several bugs in the SMTP client's idle timeout support were fixed (#3641,
+ #1219)
+ - A case where the SMTP client could skip some recipients when retrying
+ delivery has been fixed (#3638)
+ - Errors during certain data transfers will no longer be swallowed. They will
+ now bubble up to the higher-level API (such as the sendmail function) (#3642)
+ - Escape sequences inside quoted strings in IMAP4 should now be parsed
+ correctly by the IMAP4 server protocol (#3659)
+ - The "imap4-utf-7" codec that is registered by twisted.mail.imap4 had a number
+ of fixes that allow it to work better with the Python codecs system, and to
+ actually work (#3663)
+ - The Maildir implementation now ensures time-based ordering of filenames so
+ that the lexical sorting of messages matches the order in which they were
+ received (#3812)
+ - SASL PLAIN credentials generated by the IMAP4 protocol implementations
+ (client and server) should now be RFC-compliant (#3939)
+ - Searching for a set of sequences using the IMAP4 "SEARCH" command should
+ now work on the IMAP4 server protocol implementation. This at least improves
+ support for the Pine mail client (#1977)
+
+Other
+-----
+ - #2763, #3647, #3750, #3819, #3540, #3846, #2023, #4050
+
+
+Mail 8.2.0 (2008-12-16)
+=======================
+
+Fixes
+-----
+ - The mailmail tool now provides better error messages for usage errors (#3339)
+ - The SMTP protocol implementation now works on PyPy (#2976)
+
+Other
+-----
+ - #3475
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Features
+--------
+ - Support CAPABILITY responses that include atoms of the form "FOO" and
+ "FOO=BAR" in IMAP4 (#2695)
+ - Parameterize error handling behavior of imap4.encoder and imap4.decoder.
+ (#2929)
+
+Fixes
+-----
+ - Handle empty passwords in SMTP auth. (#2521)
+ - Fix IMAP4Client's parsing of literals which are not preceeded by whitespace.
+ (#2700)
+ - Handle MX lookup suceeding without answers. (#2807)
+ - Fix issues with aliases(5) process support. (#2729)
+
+Misc
+----
+ - #2371, #2123, #2378, #739, #2640, #2746, #1917, #2266, #2864, #2832, #2063,
+ #2865, #2847
+
+
+0.4.0 (2007-01-06)
+==================
+
+Features
+--------
+ - Plaintext POP3 logins are now possible over SSL or TLS (#1809)
+
+Fixes
+-----
+ - ESMTP servers now greet with an "ESMTP" string (#1891)
+ - The POP3 client can now correctly deal with concurrent POP3
+ retrievals (#1988, #1691)
+ - In the IMAP4 server, a bug involving retrieving the first part
+ of a single-part message was fixed. This improves compatibility
+ with Pine (#1978)
+ - A bug in the IMAP4 server which caused corruption under heavy
+ pipelining was fixed (#1992)
+ - More strict support for the AUTH command was added to the SMTP
+ server, to support the AUTH <mechanism>
+ <initial-authentication-data> form of the command (#1552)
+ - An SMTP bug involving the interaction with validateFrom, which
+ caused multiple conflicting SMTP messages to be sent over the wire,
+ was fixed (#2158)
+
+Misc
+----
+ - #1648, #1801, #1636, #2003, #1936, #1202, #2051, #2072, #2248, #2250
+
+0.3.0 (2006-05-21)
+==================
+
+Features
+--------
+ - Support Deferred results from POP3's IMailbox.listMessages (#1701).
+
+Fixes
+-----
+ - Quote usernames and passwords automatically in the IMAP client (#1411).
+ - Improved parsing of literals in IMAP4 client and server (#1417).
+ - Recognize unsolicted FLAGS response in IMAP4 client (#1105).
+ - Parse and respond to requests with multiple BODY arguments in IMAP4
+ server (#1307).
+ - Misc: #1356, #1290, #1602
+
+0.2.0:
+ - SMTP server:
+ - Now gives application-level code opportunity to set a different
+ Received header for each recipient of a multi-recipient message.
+ - IMAP client:
+ - New `startTLS' method to allow explicit negotiation of transport
+ security.
+- POP client:
+ - Support for per-command timeouts
+ - New `startTLS' method, similar to the one added to the IMAP
+ client.
+ - NOOP, RSET, and STAT support added
+- POP server:
+ - Bug handling passwords of "" fixed
+
+
+0.1.0:
+ - Tons of bugfixes in IMAP4, POP3, and SMTP protocols
+ - Maildir append support
+ - Brand new, improved POP3 client (twisted.mail.pop3.AdvancedPOP3Client)
+ - Deprecated the old POP3 client (twisted.mail.pop3.POP3Client)
+ - SMTP client:
+ - Support SMTP AUTH
+ - Allow user to supply SSL context
+ - Improved error handling, via new exception classes and an overridable
+ hook to customize handling.
+ - Order to try the authenication schemes is user-definable.
+ - Timeout support.
+ - SMTP server:
+ - Properly understand <> sender.
+ - Parameterize remote port
+ - IMAP4:
+ - LOGIN authentication compatibility improved
+ - Improved unicode mailbox support
+ - Fix parsing/handling of "FETCH BODY[HEADER]"
+ - Many many quoting fixes
+ - Timeout support on client
diff --git a/twisted/mail/topfiles/README b/twisted/mail/topfiles/README
new file mode 100644
index 0000000..7c647a2
--- /dev/null
+++ b/twisted/mail/topfiles/README
@@ -0,0 +1,6 @@
+Twisted Mail 12.1.0
+
+Twisted Mail depends on Twisted Core and (sometimes) Twisted Names. For TLS
+support, pyOpenSSL (<http://launchpad.net/pyopenssl>) is also required. Aside
+from protocol implementations, much of Twisted Mail also only runs on POSIX
+platforms.
diff --git a/twisted/mail/topfiles/setup.py b/twisted/mail/topfiles/setup.py
new file mode 100644
index 0000000..d14fb6b
--- /dev/null
+++ b/twisted/mail/topfiles/setup.py
@@ -0,0 +1,50 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ if sys.version_info[:2] >= (2, 4):
+ extraMeta = dict(
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: No Input/Output (Daemon)",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Communications :: Email :: Post-Office :: IMAP",
+ "Topic :: Communications :: Email :: Post-Office :: POP3",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ])
+ else:
+ extraMeta = {}
+
+ dist.setup(
+ twisted_subproject="mail",
+ scripts=dist.getScripts("mail"),
+ # metadata
+ name="Twisted Mail",
+ description="A Twisted Mail library, server and client.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Jp Calderone",
+ url="http://twistedmatrix.com/trac/wiki/TwistedMail",
+ license="MIT",
+ long_description="""\
+An SMTP, IMAP and POP protocol implementation together with clients
+and servers.
+
+Twisted Mail contains high-level, efficient protocol implementations
+for both clients and servers of SMTP, POP3, and IMAP4. Additionally,
+it contains an "out of the box" combination SMTP/POP3 virtual-hosting
+mail server. Also included is a read/write Maildir implementation and
+a basic Mail Exchange calculator.
+""",
+ **extraMeta)
diff --git a/twisted/manhole/__init__.py b/twisted/manhole/__init__.py
new file mode 100644
index 0000000..64e2bbc
--- /dev/null
+++ b/twisted/manhole/__init__.py
@@ -0,0 +1,8 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Manhole: interactive interpreter and direct manipulation support for Twisted.
+"""
diff --git a/twisted/manhole/_inspectro.py b/twisted/manhole/_inspectro.py
new file mode 100644
index 0000000..430ae7b
--- /dev/null
+++ b/twisted/manhole/_inspectro.py
@@ -0,0 +1,369 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""An input/output window for the glade reactor inspector.
+"""
+
+import time
+import gtk
+import gobject
+import gtk.glade
+from twisted.python.util import sibpath
+from twisted.python import reflect
+
+from twisted.manhole.ui import gtk2manhole
+from twisted.python.components import Adapter, registerAdapter
+from twisted.python import log
+from twisted.protocols import policies
+from zope.interface import implements, Interface
+
+# the glade file uses stock icons, which requires gnome to be installed
+import gnome
+version = "$Revision: 1.1 $"[11:-2]
+gnome.init("gladereactor Inspector", version)
+
+class ConsoleOutput(gtk2manhole.ConsoleOutput):
+ def _captureLocalLog(self):
+ self.fobs = log.FileLogObserver(gtk2manhole._Notafile(self, "log"))
+ self.fobs.start()
+
+ def stop(self):
+ self.fobs.stop()
+ del self.fobs
+
+class ConsoleInput(gtk2manhole.ConsoleInput):
+ def sendMessage(self):
+ buffer = self.textView.get_buffer()
+ iter1, iter2 = buffer.get_bounds()
+ text = buffer.get_text(iter1, iter2, False)
+ self.do(text)
+
+ def do(self, text):
+ self.toplevel.do(text)
+
+class INode(Interface):
+ """A node in the inspector tree model.
+ """
+
+ def __adapt__(adaptable, default):
+ if hasattr(adaptable, "__dict__"):
+ return InstanceNode(adaptable)
+ return AttributesNode(adaptable)
+
+class InspectorNode(Adapter):
+ implements(INode)
+
+ def postInit(self, offset, parent, slot):
+ self.offset = offset
+ self.parent = parent
+ self.slot = slot
+
+ def getPath(self):
+ L = []
+ x = self
+ while x.parent is not None:
+ L.append(x.offset)
+ x = x.parent
+ L.reverse()
+ return L
+
+ def __getitem__(self, index):
+ slot, o = self.get(index)
+ n = INode(o, persist=False)
+ n.postInit(index, self, slot)
+ return n
+
+ def origstr(self):
+ return str(self.original)
+
+ def format(self):
+ return (self.slot, self.origstr())
+
+
+class ConstantNode(InspectorNode):
+ def __len__(self):
+ return 0
+
+class DictionaryNode(InspectorNode):
+ def get(self, index):
+ L = self.original.items()
+ L.sort()
+ return L[index]
+
+ def __len__(self):
+ return len(self.original)
+
+ def origstr(self):
+ return "Dictionary"
+
+class ListNode(InspectorNode):
+ def get(self, index):
+ return index, self.original[index]
+
+ def origstr(self):
+ return "List"
+
+ def __len__(self):
+ return len(self.original)
+
+class AttributesNode(InspectorNode):
+ def __len__(self):
+ return len(dir(self.original))
+
+ def get(self, index):
+ L = dir(self.original)
+ L.sort()
+ return L[index], getattr(self.original, L[index])
+
+class InstanceNode(InspectorNode):
+ def __len__(self):
+ return len(self.original.__dict__) + 1
+
+ def get(self, index):
+ if index == 0:
+ if hasattr(self.original, "__class__"):
+ v = self.original.__class__
+ else:
+ v = type(self.original)
+ return "__class__", v
+ else:
+ index -= 1
+ L = self.original.__dict__.items()
+ L.sort()
+ return L[index]
+
+import types
+
+for x in dict, types.DictProxyType:
+ registerAdapter(DictionaryNode, x, INode)
+for x in list, tuple:
+ registerAdapter(ListNode, x, INode)
+for x in int, str:
+ registerAdapter(ConstantNode, x, INode)
+
+
+class InspectorTreeModel(gtk.GenericTreeModel):
+ def __init__(self, root):
+ gtk.GenericTreeModel.__init__(self)
+ self.root = INode(root, persist=False)
+ self.root.postInit(0, None, 'root')
+
+ def on_get_flags(self):
+ return 0
+
+ def on_get_n_columns(self):
+ return 1
+
+ def on_get_column_type(self, index):
+ return gobject.TYPE_STRING
+
+ def on_get_path(self, node):
+ return node.getPath()
+
+ def on_get_iter(self, path):
+ x = self.root
+ for elem in path:
+ x = x[elem]
+ return x
+
+ def on_get_value(self, node, column):
+ return node.format()[column]
+
+ def on_iter_next(self, node):
+ try:
+ return node.parent[node.offset + 1]
+ except IndexError:
+ return None
+
+ def on_iter_children(self, node):
+ return node[0]
+
+ def on_iter_has_child(self, node):
+ return len(node)
+
+ def on_iter_n_children(self, node):
+ return len(node)
+
+ def on_iter_nth_child(self, node, n):
+ if node is None:
+ return None
+ return node[n]
+
+ def on_iter_parent(self, node):
+ return node.parent
+
+
+class Inspectro:
+ selected = None
+ def __init__(self, o=None):
+ self.xml = x = gtk.glade.XML(sibpath(__file__, "inspectro.glade"))
+ self.tree_view = x.get_widget("treeview")
+ colnames = ["Name", "Value"]
+ for i in range(len(colnames)):
+ self.tree_view.append_column(
+ gtk.TreeViewColumn(
+ colnames[i], gtk.CellRendererText(), text=i))
+ d = {}
+ for m in reflect.prefixedMethods(self, "on_"):
+ d[m.im_func.__name__] = m
+ self.xml.signal_autoconnect(d)
+ if o is not None:
+ self.inspect(o)
+ self.ns = {'inspect': self.inspect}
+ iwidget = x.get_widget('input')
+ self.input = ConsoleInput(iwidget)
+ self.input.toplevel = self
+ iwidget.connect("key_press_event", self.input._on_key_press_event)
+ self.output = ConsoleOutput(x.get_widget('output'))
+
+ def select(self, o):
+ self.selected = o
+ self.ns['it'] = o
+ self.xml.get_widget("itname").set_text(repr(o))
+ self.xml.get_widget("itpath").set_text("???")
+
+ def inspect(self, o):
+ self.model = InspectorTreeModel(o)
+ self.tree_view.set_model(self.model)
+ self.inspected = o
+
+ def do(self, command):
+ filename = '<inspector>'
+ try:
+ print repr(command)
+ try:
+ code = compile(command, filename, 'eval')
+ except:
+ code = compile(command, filename, 'single')
+ val = eval(code, self.ns, self.ns)
+ if val is not None:
+ print repr(val)
+ self.ns['_'] = val
+ except:
+ log.err()
+
+ def on_inspect(self, *a):
+ self.inspect(self.selected)
+
+ def on_inspect_new(self, *a):
+ Inspectro(self.selected)
+
+ def on_row_activated(self, tv, path, column):
+ self.select(self.model.on_get_iter(path).original)
+
+
+class LoggingProtocol(policies.ProtocolWrapper):
+ """Log network traffic."""
+
+ logging = True
+ logViewer = None
+
+ def __init__(self, *args):
+ policies.ProtocolWrapper.__init__(self, *args)
+ self.inLog = []
+ self.outLog = []
+
+ def write(self, data):
+ if self.logging:
+ self.outLog.append((time.time(), data))
+ if self.logViewer:
+ self.logViewer.updateOut(self.outLog[-1])
+ policies.ProtocolWrapper.write(self, data)
+
+ def dataReceived(self, data):
+ if self.logging:
+ self.inLog.append((time.time(), data))
+ if self.logViewer:
+ self.logViewer.updateIn(self.inLog[-1])
+ policies.ProtocolWrapper.dataReceived(self, data)
+
+ def __repr__(self):
+ r = "wrapped " + repr(self.wrappedProtocol)
+ if self.logging:
+ r += " (logging)"
+ return r
+
+
+class LoggingFactory(policies.WrappingFactory):
+ """Wrap protocols with logging wrappers."""
+
+ protocol = LoggingProtocol
+ logging = True
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self, self.wrappedFactory.buildProtocol(addr))
+ p.logging = self.logging
+ return p
+
+ def __repr__(self):
+ r = "wrapped " + repr(self.wrappedFactory)
+ if self.logging:
+ r += " (logging)"
+ return r
+
+
+class LogViewer:
+ """Display log of network traffic."""
+
+ def __init__(self, p):
+ self.p = p
+ vals = [time.time()]
+ if p.inLog:
+ vals.append(p.inLog[0][0])
+ if p.outLog:
+ vals.append(p.outLog[0][0])
+ self.startTime = min(vals)
+ p.logViewer = self
+ self.xml = x = gtk.glade.XML(sibpath(__file__, "logview.glade"))
+ self.xml.signal_autoconnect(self)
+ self.loglist = self.xml.get_widget("loglist")
+ # setup model, connect it to my treeview
+ self.model = gtk.ListStore(str, str, str)
+ self.loglist.set_model(self.model)
+ self.loglist.set_reorderable(1)
+ self.loglist.set_headers_clickable(1)
+ # self.servers.set_headers_draggable(1)
+ # add a column
+ for col in [
+ gtk.TreeViewColumn('Time',
+ gtk.CellRendererText(),
+ text=0),
+ gtk.TreeViewColumn('D',
+ gtk.CellRendererText(),
+ text=1),
+ gtk.TreeViewColumn('Data',
+ gtk.CellRendererText(),
+ text=2)]:
+ self.loglist.append_column(col)
+ col.set_resizable(1)
+ r = []
+ for t, data in p.inLog:
+ r.append(((str(t - self.startTime), "R", repr(data)[1:-1])))
+ for t, data in p.outLog:
+ r.append(((str(t - self.startTime), "S", repr(data)[1:-1])))
+ r.sort()
+ for i in r:
+ self.model.append(i)
+
+ def updateIn(self, (time, data)):
+ self.model.append((str(time - self.startTime), "R", repr(data)[1:-1]))
+
+ def updateOut(self, (time, data)):
+ self.model.append((str(time - self.startTime), "S", repr(data)[1:-1]))
+
+ def on_logview_destroy(self, w):
+ self.p.logViewer = None
+ del self.p
+
+
+def main():
+ x = Inspectro()
+ x.inspect(x)
+ gtk.main()
+
+if __name__ == '__main__':
+ import sys
+ log.startLogging(sys.stdout)
+ main()
+
diff --git a/twisted/manhole/explorer.py b/twisted/manhole/explorer.py
new file mode 100644
index 0000000..428b3e2
--- /dev/null
+++ b/twisted/manhole/explorer.py
@@ -0,0 +1,654 @@
+# -*- test-case-name: twisted.test.test_explorer -*-
+# $Id: explorer.py,v 1.6 2003/02/18 21:15:30 acapnotic Exp $
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Support for python object introspection and exploration.
+
+Note that Explorers, what with their list of attributes, are much like
+manhole.coil.Configurables. Someone should investigate this further. (TODO)
+
+Also TODO: Determine how much code in here (particularly the function
+signature stuff) can be replaced with functions available in the
+L{inspect} module available in Python 2.1.
+"""
+
+# System Imports
+import inspect, string, sys, types
+import UserDict
+
+# Twisted Imports
+from twisted.spread import pb
+from twisted.python import reflect
+
+
+True=(1==1)
+False=not True
+
+class Pool(UserDict.UserDict):
+ def getExplorer(self, object, identifier):
+ oid = id(object)
+ if self.data.has_key(oid):
+ # XXX: This potentially returns something with
+ # 'identifier' set to a different value.
+ return self.data[oid]
+ else:
+ klass = typeTable.get(type(object), ExplorerGeneric)
+ e = types.InstanceType(klass, {})
+ self.data[oid] = e
+ klass.__init__(e, object, identifier)
+ return e
+
+explorerPool = Pool()
+
+class Explorer(pb.Cacheable):
+ properties = ["id", "identifier"]
+ attributeGroups = []
+ accessors = ["get_refcount"]
+
+ id = None
+ identifier = None
+
+ def __init__(self, object, identifier):
+ self.object = object
+ self.identifier = identifier
+ self.id = id(object)
+
+ self.properties = []
+ reflect.accumulateClassList(self.__class__, 'properties',
+ self.properties)
+
+ self.attributeGroups = []
+ reflect.accumulateClassList(self.__class__, 'attributeGroups',
+ self.attributeGroups)
+
+ self.accessors = []
+ reflect.accumulateClassList(self.__class__, 'accessors',
+ self.accessors)
+
+ def getStateToCopyFor(self, perspective):
+ all = ["properties", "attributeGroups", "accessors"]
+ all.extend(self.properties)
+ all.extend(self.attributeGroups)
+
+ state = {}
+ for key in all:
+ state[key] = getattr(self, key)
+
+ state['view'] = pb.ViewPoint(perspective, self)
+ state['explorerClass'] = self.__class__.__name__
+ return state
+
+ def view_get_refcount(self, perspective):
+ return sys.getrefcount(self)
+
+class ExplorerGeneric(Explorer):
+ properties = ["str", "repr", "typename"]
+
+ def __init__(self, object, identifier):
+ Explorer.__init__(self, object, identifier)
+ self.str = str(object)
+ self.repr = repr(object)
+ self.typename = type(object).__name__
+
+
+class ExplorerImmutable(Explorer):
+ properties = ["value"]
+
+ def __init__(self, object, identifier):
+ Explorer.__init__(self, object, identifier)
+ self.value = object
+
+
+class ExplorerSequence(Explorer):
+ properties = ["len"]
+ attributeGroups = ["elements"]
+ accessors = ["get_elements"]
+
+ def __init__(self, seq, identifier):
+ Explorer.__init__(self, seq, identifier)
+ self.seq = seq
+ self.len = len(seq)
+
+ # Use accessor method to fill me in.
+ self.elements = []
+
+ def get_elements(self):
+ self.len = len(self.seq)
+ l = []
+ for i in xrange(self.len):
+ identifier = "%s[%s]" % (self.identifier, i)
+
+ # GLOBAL: using global explorerPool
+ l.append(explorerPool.getExplorer(self.seq[i], identifier))
+
+ return l
+
+ def view_get_elements(self, perspective):
+ # XXX: set the .elements member of all my remoteCaches
+ return self.get_elements()
+
+
+class ExplorerMapping(Explorer):
+ properties = ["len"]
+ attributeGroups = ["keys"]
+ accessors = ["get_keys", "get_item"]
+
+ def __init__(self, dct, identifier):
+ Explorer.__init__(self, dct, identifier)
+
+ self.dct = dct
+ self.len = len(dct)
+
+ # Use accessor method to fill me in.
+ self.keys = []
+
+ def get_keys(self):
+ keys = self.dct.keys()
+ self.len = len(keys)
+ l = []
+ for i in xrange(self.len):
+ identifier = "%s.keys()[%s]" % (self.identifier, i)
+
+ # GLOBAL: using global explorerPool
+ l.append(explorerPool.getExplorer(keys[i], identifier))
+
+ return l
+
+ def view_get_keys(self, perspective):
+ # XXX: set the .keys member of all my remoteCaches
+ return self.get_keys()
+
+ def view_get_item(self, perspective, key):
+ if type(key) is types.InstanceType:
+ key = key.object
+
+ item = self.dct[key]
+
+ identifier = "%s[%s]" % (self.identifier, repr(key))
+ # GLOBAL: using global explorerPool
+ item = explorerPool.getExplorer(item, identifier)
+ return item
+
+
+class ExplorerBuiltin(Explorer):
+ """
+ @ivar name: the name the function was defined as
+ @ivar doc: function's docstring, or C{None} if unavailable
+ @ivar self: if not C{None}, the function is a method of this object.
+ """
+ properties = ["doc", "name", "self"]
+ def __init__(self, function, identifier):
+ Explorer.__init__(self, function, identifier)
+ self.doc = function.__doc__
+ self.name = function.__name__
+ self.self = function.__self__
+
+
+class ExplorerInstance(Explorer):
+ """
+ Attribute groups:
+ - B{methods} -- dictionary of methods
+ - B{data} -- dictionary of data members
+
+ Note these are only the *instance* methods and members --
+ if you want the class methods, you'll have to look up the class.
+
+ TODO: Detail levels (me, me & class, me & class ancestory)
+
+ @ivar klass: the class this is an instance of.
+ """
+ properties = ["klass"]
+ attributeGroups = ["methods", "data"]
+
+ def __init__(self, instance, identifier):
+ Explorer.__init__(self, instance, identifier)
+ members = {}
+ methods = {}
+ for i in dir(instance):
+ # TODO: Make screening of private attributes configurable.
+ if i[0] == '_':
+ continue
+ mIdentifier = string.join([identifier, i], ".")
+ member = getattr(instance, i)
+ mType = type(member)
+
+ if mType is types.MethodType:
+ methods[i] = explorerPool.getExplorer(member, mIdentifier)
+ else:
+ members[i] = explorerPool.getExplorer(member, mIdentifier)
+
+ self.klass = explorerPool.getExplorer(instance.__class__,
+ self.identifier +
+ '.__class__')
+ self.data = members
+ self.methods = methods
+
+
+class ExplorerClass(Explorer):
+ """
+ @ivar name: the name the class was defined with
+ @ivar doc: the class's docstring
+ @ivar bases: a list of this class's base classes.
+ @ivar module: the module the class is defined in
+
+ Attribute groups:
+ - B{methods} -- class methods
+ - B{data} -- other members of the class
+ """
+ properties = ["name", "doc", "bases", "module"]
+ attributeGroups = ["methods", "data"]
+ def __init__(self, theClass, identifier):
+ Explorer.__init__(self, theClass, identifier)
+ if not identifier:
+ identifier = theClass.__name__
+ members = {}
+ methods = {}
+ for i in dir(theClass):
+ if (i[0] == '_') and (i != '__init__'):
+ continue
+
+ mIdentifier = string.join([identifier, i], ".")
+ member = getattr(theClass, i)
+ mType = type(member)
+
+ if mType is types.MethodType:
+ methods[i] = explorerPool.getExplorer(member, mIdentifier)
+ else:
+ members[i] = explorerPool.getExplorer(member, mIdentifier)
+
+ self.name = theClass.__name__
+ self.doc = inspect.getdoc(theClass)
+ self.data = members
+ self.methods = methods
+ self.bases = explorerPool.getExplorer(theClass.__bases__,
+ identifier + ".__bases__")
+ self.module = getattr(theClass, '__module__', None)
+
+
+class ExplorerFunction(Explorer):
+ properties = ["name", "doc", "file", "line","signature"]
+ """
+ name -- the name the function was defined as
+ signature -- the function's calling signature (Signature instance)
+ doc -- the function's docstring
+ file -- the file the function is defined in
+ line -- the line in the file the function begins on
+ """
+ def __init__(self, function, identifier):
+ Explorer.__init__(self, function, identifier)
+ code = function.func_code
+ argcount = code.co_argcount
+ takesList = (code.co_flags & 0x04) and 1
+ takesKeywords = (code.co_flags & 0x08) and 1
+
+ n = (argcount + takesList + takesKeywords)
+ signature = Signature(code.co_varnames[:n])
+
+ if function.func_defaults:
+ i_d = 0
+ for i in xrange(argcount - len(function.func_defaults),
+ argcount):
+ default = function.func_defaults[i_d]
+ default = explorerPool.getExplorer(
+ default, '%s.func_defaults[%d]' % (identifier, i_d))
+ signature.set_default(i, default)
+
+ i_d = i_d + 1
+
+ if takesKeywords:
+ signature.set_keyword(n - 1)
+
+ if takesList:
+ signature.set_varlist(n - 1 - takesKeywords)
+
+ # maybe also: function.func_globals,
+ # or at least func_globals.__name__?
+ # maybe the bytecode, for disassembly-view?
+
+ self.name = function.__name__
+ self.signature = signature
+ self.doc = inspect.getdoc(function)
+ self.file = code.co_filename
+ self.line = code.co_firstlineno
+
+
+class ExplorerMethod(ExplorerFunction):
+ properties = ["self", "klass"]
+ """
+ In addition to ExplorerFunction properties:
+ self -- the object I am bound to, or None if unbound
+ klass -- the class I am a method of
+ """
+ def __init__(self, method, identifier):
+
+ function = method.im_func
+ if type(function) is types.InstanceType:
+ function = function.__call__.im_func
+
+ ExplorerFunction.__init__(self, function, identifier)
+ self.id = id(method)
+ self.klass = explorerPool.getExplorer(method.im_class,
+ identifier + '.im_class')
+ self.self = explorerPool.getExplorer(method.im_self,
+ identifier + '.im_self')
+
+ if method.im_self:
+ # I'm a bound method -- eat the 'self' arg.
+ self.signature.discardSelf()
+
+
+class ExplorerModule(Explorer):
+ """
+ @ivar name: the name the module was defined as
+ @ivar doc: documentation string for the module
+ @ivar file: the file the module is defined in
+
+ Attribute groups:
+ - B{classes} -- the public classes provided by the module
+ - B{functions} -- the public functions provided by the module
+ - B{data} -- the public data members provided by the module
+
+ (\"Public\" is taken to be \"anything that doesn't start with _\")
+ """
+ properties = ["name","doc","file"]
+ attributeGroups = ["classes", "functions", "data"]
+
+ def __init__(self, module, identifier):
+ Explorer.__init__(self, module, identifier)
+ functions = {}
+ classes = {}
+ data = {}
+ for key, value in module.__dict__.items():
+ if key[0] == '_':
+ continue
+
+ mIdentifier = "%s.%s" % (identifier, key)
+
+ if type(value) is types.ClassType:
+ classes[key] = explorerPool.getExplorer(value,
+ mIdentifier)
+ elif type(value) is types.FunctionType:
+ functions[key] = explorerPool.getExplorer(value,
+ mIdentifier)
+ elif type(value) is types.ModuleType:
+ pass # pass on imported modules
+ else:
+ data[key] = explorerPool.getExplorer(value, mIdentifier)
+
+ self.name = module.__name__
+ self.doc = inspect.getdoc(module)
+ self.file = getattr(module, '__file__', None)
+ self.classes = classes
+ self.functions = functions
+ self.data = data
+
+typeTable = {types.InstanceType: ExplorerInstance,
+ types.ClassType: ExplorerClass,
+ types.MethodType: ExplorerMethod,
+ types.FunctionType: ExplorerFunction,
+ types.ModuleType: ExplorerModule,
+ types.BuiltinFunctionType: ExplorerBuiltin,
+ types.ListType: ExplorerSequence,
+ types.TupleType: ExplorerSequence,
+ types.DictType: ExplorerMapping,
+ types.StringType: ExplorerImmutable,
+ types.NoneType: ExplorerImmutable,
+ types.IntType: ExplorerImmutable,
+ types.FloatType: ExplorerImmutable,
+ types.LongType: ExplorerImmutable,
+ types.ComplexType: ExplorerImmutable,
+ }
+
+class Signature(pb.Copyable):
+ """I represent the signature of a callable.
+
+ Signatures are immutable, so don't expect my contents to change once
+ they've been set.
+ """
+ _FLAVOURLESS = None
+ _HAS_DEFAULT = 2
+ _VAR_LIST = 4
+ _KEYWORD_DICT = 8
+
+ def __init__(self, argNames):
+ self.name = argNames
+ self.default = [None] * len(argNames)
+ self.flavour = [None] * len(argNames)
+
+ def get_name(self, arg):
+ return self.name[arg]
+
+ def get_default(self, arg):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ # Wouldn't it be nice if we just returned "None" when there
+ # wasn't a default? Well, yes, but often times "None" *is*
+ # the default, so return a tuple instead.
+ if self.flavour[arg] == self._HAS_DEFAULT:
+ return (True, self.default[arg])
+ else:
+ return (False, None)
+
+ def set_default(self, arg, value):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ self.flavour[arg] = self._HAS_DEFAULT
+ self.default[arg] = value
+
+ def set_varlist(self, arg):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ self.flavour[arg] = self._VAR_LIST
+
+ def set_keyword(self, arg):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ self.flavour[arg] = self._KEYWORD_DICT
+
+ def is_varlist(self, arg):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ return (self.flavour[arg] == self._VAR_LIST)
+
+ def is_keyword(self, arg):
+ if arg is types.StringType:
+ arg = self.name.index(arg)
+
+ return (self.flavour[arg] == self._KEYWORD_DICT)
+
+ def discardSelf(self):
+ """Invoke me to discard the first argument if this is a bound method.
+ """
+ ## if self.name[0] != 'self':
+ ## log.msg("Warning: Told to discard self, but name is %s" %
+ ## self.name[0])
+ self.name = self.name[1:]
+ self.default.pop(0)
+ self.flavour.pop(0)
+
+ def getStateToCopy(self):
+ return {'name': tuple(self.name),
+ 'flavour': tuple(self.flavour),
+ 'default': tuple(self.default)}
+
+ def __len__(self):
+ return len(self.name)
+
+ def __str__(self):
+ arglist = []
+ for arg in xrange(len(self)):
+ name = self.get_name(arg)
+ hasDefault, default = self.get_default(arg)
+ if hasDefault:
+ a = "%s=%s" % (name, default)
+ elif self.is_varlist(arg):
+ a = "*%s" % (name,)
+ elif self.is_keyword(arg):
+ a = "**%s" % (name,)
+ else:
+ a = name
+ arglist.append(a)
+
+ return string.join(arglist,", ")
+
+
+
+
+
+class CRUFT_WatchyThingie:
+ # TODO:
+ #
+ # * an exclude mechanism for the watcher's browser, to avoid
+ # sending back large and uninteresting data structures.
+ #
+ # * an exclude mechanism for the watcher's trigger, to avoid
+ # triggering on some frequently-called-method-that-doesn't-
+ # actually-change-anything.
+ #
+ # * XXX! need removeWatch()
+
+ def watchIdentifier(self, identifier, callback):
+ """Watch the object returned by evaluating the identifier.
+
+ Whenever I think the object might have changed, I'll send an
+ ObjectLink of it to the callback.
+
+ WARNING: This calls eval() on its argument!
+ """
+ object = eval(identifier,
+ self.globalNamespace,
+ self.localNamespace)
+ return self.watchObject(object, identifier, callback)
+
+ def watchObject(self, object, identifier, callback):
+ """Watch the given object.
+
+ Whenever I think the object might have changed, I'll send an
+ ObjectLink of it to the callback.
+
+ The identifier argument is used to generate identifiers for
+ objects which are members of this one.
+ """
+ if type(object) is not types.InstanceType:
+ raise TypeError, "Sorry, can only place a watch on Instances."
+
+ # uninstallers = []
+
+ dct = {}
+ reflect.addMethodNamesToDict(object.__class__, dct, '')
+ for k in object.__dict__.keys():
+ dct[k] = 1
+
+ members = dct.keys()
+
+ clazzNS = {}
+ clazz = types.ClassType('Watching%s%X' %
+ (object.__class__.__name__, id(object)),
+ (_MonkeysSetattrMixin, object.__class__,),
+ clazzNS)
+
+ clazzNS['_watchEmitChanged'] = types.MethodType(
+ lambda slf, i=identifier, b=self, cb=callback:
+ cb(b.browseObject(slf, i)),
+ None, clazz)
+
+ # orig_class = object.__class__
+ object.__class__ = clazz
+
+ for name in members:
+ m = getattr(object, name)
+ # Only hook bound methods.
+ if ((type(m) is types.MethodType)
+ and (m.im_self is not None)):
+ # What's the use of putting watch monkeys on methods
+ # in addition to __setattr__? Well, um, uh, if the
+ # methods modify their attributes (i.e. add a key to
+ # a dictionary) instead of [re]setting them, then
+ # we wouldn't know about it unless we did this.
+ # (Is that convincing?)
+
+ monkey = _WatchMonkey(object)
+ monkey.install(name)
+ # uninstallers.append(monkey.uninstall)
+
+ # XXX: This probably prevents these objects from ever having a
+ # zero refcount. Leak, Leak!
+ ## self.watchUninstallers[object] = uninstallers
+
+
+class _WatchMonkey:
+ """I hang on a method and tell you what I see.
+
+ TODO: Aya! Now I just do browseObject all the time, but I could
+ tell you what got called with what when and returning what.
+ """
+ oldMethod = None
+
+ def __init__(self, instance):
+ """Make a monkey to hang on this instance object.
+ """
+ self.instance = instance
+
+ def install(self, methodIdentifier):
+ """Install myself on my instance in place of this method.
+ """
+ oldMethod = getattr(self.instance, methodIdentifier, None)
+
+ # XXX: this conditional probably isn't effective.
+ if oldMethod is not self:
+ # avoid triggering __setattr__
+ self.instance.__dict__[methodIdentifier] = types.MethodType(
+ self, self.instance, self.instance.__class__)
+ self.oldMethod = (methodIdentifier, oldMethod)
+
+ def uninstall(self):
+ """Remove myself from this instance and restore the original method.
+
+ (I hope.)
+ """
+ if self.oldMethod is None:
+ return
+
+ # XXX: This probably doesn't work if multiple monkies are hanging
+ # on a method and they're not removed in order.
+ if self.oldMethod[1] is None:
+ delattr(self.instance, self.oldMethod[0])
+ else:
+ setattr(self.instance, self.oldMethod[0], self.oldMethod[1])
+
+ def __call__(self, instance, *a, **kw):
+ """Pretend to be the method I replaced, and ring the bell.
+ """
+ if self.oldMethod[1]:
+ rval = apply(self.oldMethod[1], a, kw)
+ else:
+ rval = None
+
+ instance._watchEmitChanged()
+ return rval
+
+
+class _MonkeysSetattrMixin:
+ """A mix-in class providing __setattr__ for objects being watched.
+ """
+ def __setattr__(self, k, v):
+ """Set the attribute and ring the bell.
+ """
+ if hasattr(self.__class__.__bases__[1], '__setattr__'):
+ # Hack! Using __bases__[1] is Bad, but since we created
+ # this class, we can be reasonably sure it'll work.
+ self.__class__.__bases__[1].__setattr__(self, k, v)
+ else:
+ self.__dict__[k] = v
+
+ # XXX: Hey, waitasec, did someone just hang a new method on me?
+ # Do I need to put a monkey on it?
+
+ self._watchEmitChanged()
diff --git a/twisted/manhole/gladereactor.glade b/twisted/manhole/gladereactor.glade
new file mode 100644
index 0000000..c78dd5a
--- /dev/null
+++ b/twisted/manhole/gladereactor.glade
@@ -0,0 +1,342 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkWindow" id="window1">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Twisted Daemon</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="default_width">256</property>
+ <property name="default_height">300</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTreeView" id="servertree">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">True</property>
+ <property name="rules_hint">False</property>
+ <property name="reorderable">True</property>
+ <property name="enable_search">True</property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkHButtonBox" id="hbuttonbox1">
+ <property name="visible">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_DEFAULT_STYLE</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkButton" id="suspend">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_suspend_clicked" last_modification_time="Sun, 22 Jun 2003 05:09:20 GMT"/>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="stock">gtk-undo</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label11">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Suspend</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="disconnect">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_disconnect_clicked" last_modification_time="Sun, 22 Jun 2003 05:09:27 GMT"/>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="stock">gtk-dialog-warning</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label10">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Disconnect</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="inspect">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_inspect_clicked" last_modification_time="Wed, 17 Dec 2003 06:14:18 GMT"/>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment3">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox4">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="stock">gtk-open</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspect</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="viewlog">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_viewlog_clicked" last_modification_time="Sun, 04 Jan 2004 22:28:19 GMT"/>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox5">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="stock">gtk-dialog-info</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">View Log</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="quit">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="label">gtk-quit</property>
+ <property name="use_stock">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_quit_clicked" last_modification_time="Sun, 04 Jan 2004 22:26:43 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/twisted/manhole/gladereactor.py b/twisted/manhole/gladereactor.py
new file mode 100644
index 0000000..148fc5e
--- /dev/null
+++ b/twisted/manhole/gladereactor.py
@@ -0,0 +1,219 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+A modified gtk2 reactor with a Glade dialog in-process that allows you to stop,
+suspend, resume and inspect transports interactively.
+"""
+
+__all__ = ['install']
+
+# Twisted Imports
+from twisted.python import log, threadable, runtime, failure, util, reflect
+from twisted.internet.gtk2reactor import Gtk2Reactor as sup
+
+import gtk
+import gobject
+import gtk.glade
+
+COLUMN_DESCRIPTION = 0
+COLUMN_TRANSPORT = 1
+COLUMN_READING = 2
+COLUMN_WRITING = 3
+
+
+class GladeReactor(sup):
+ """GTK+-2 event loop reactor with GUI.
+ """
+
+ def listenTCP(self, port, factory, backlog=50, interface=''):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.listenTCP(self, port, factory, backlog, interface)
+
+ def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.connectTCP(self, host, port, factory, timeout, bindAddress)
+
+ def listenSSL(self, port, factory, contextFactory, backlog=50, interface=''):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.listenSSL(self, port, factory, contextFactory, backlog, interface)
+
+ def connectSSL(self, host, port, factory, contextFactory, timeout=30, bindAddress=None):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.connectSSL(self, host, port, factory, contextFactory, timeout, bindAddress)
+
+ def connectUNIX(self, address, factory, timeout=30):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.connectUNIX(self, address, factory, timeout)
+
+ def listenUNIX(self, address, factory, backlog=50, mode=0666):
+ from _inspectro import LoggingFactory
+ factory = LoggingFactory(factory)
+ return sup.listenUNIX(self, address, factory, backlog, mode)
+
+ def on_disconnect_clicked(self, w):
+ store, iter = self.servers.get_selection().get_selected()
+ store[iter][COLUMN_TRANSPORT].loseConnection()
+
+ def on_viewlog_clicked(self, w):
+ store, iter = self.servers.get_selection().get_selected()
+ data = store[iter][1]
+ from _inspectro import LogViewer
+ if hasattr(data, "protocol") and not data.protocol.logViewer:
+ LogViewer(data.protocol)
+
+ def on_inspect_clicked(self, w):
+ store, iter = self.servers.get_selection().get_selected()
+ data = store[iter]
+ from _inspectro import Inspectro
+ Inspectro(data[1])
+
+ def on_suspend_clicked(self, w):
+ store, iter = self.servers.get_selection().get_selected()
+ data = store[iter]
+ sup.removeReader(self, data[1])
+ sup.removeWriter(self, data[1])
+ if data[COLUMN_DESCRIPTION].endswith('(suspended)'):
+ if data[COLUMN_READING]:
+ sup.addReader(self, data[COLUMN_TRANSPORT])
+ if data[COLUMN_WRITING]:
+ sup.addWriter(self, data[COLUMN_TRANSPORT])
+ data[COLUMN_DESCRIPTION] = str(data[COLUMN_TRANSPORT])
+ self.toggle_suspend(1)
+ else:
+ data[0] += ' (suspended)'
+ self.toggle_suspend(0)
+
+ def toggle_suspend(self, suspending=0):
+ stock, nonstock = [('gtk-redo', 'Resume'),
+ ('gtk-undo', 'Suspend')][suspending]
+ b = self.xml.get_widget("suspend")
+ b.set_use_stock(1)
+ b.set_label(stock)
+ b.get_child().get_child().get_children()[1].set_label(nonstock)
+
+ def servers_selection_changed(self, w):
+ store, iter = w.get_selected()
+ if iter is None:
+ self.xml.get_widget("suspend").set_sensitive(0)
+ self.xml.get_widget('disconnect').set_sensitive(0)
+ else:
+ data = store[iter]
+ self.toggle_suspend(not
+ data[COLUMN_DESCRIPTION].endswith('(suspended)'))
+ self.xml.get_widget("suspend").set_sensitive(1)
+ self.xml.get_widget('disconnect').set_sensitive(1)
+
+ def on_quit_clicked(self, w):
+ self.stop()
+
+ def __init__(self):
+ self.xml = gtk.glade.XML(util.sibpath(__file__,"gladereactor.glade"))
+ d = {}
+ for m in reflect.prefixedMethods(self, "on_"):
+ d[m.im_func.__name__] = m
+ self.xml.signal_autoconnect(d)
+ self.xml.get_widget('window1').connect('destroy',
+ lambda w: self.stop())
+ self.servers = self.xml.get_widget("servertree")
+ sel = self.servers.get_selection()
+ sel.set_mode(gtk.SELECTION_SINGLE)
+ sel.connect("changed",
+ self.servers_selection_changed)
+ ## argh coredump: self.servers_selection_changed(sel)
+ self.xml.get_widget('suspend').set_sensitive(0)
+ self.xml.get_widget('disconnect').set_sensitive(0)
+ # setup model, connect it to my treeview
+ self.model = gtk.ListStore(str, object, gobject.TYPE_BOOLEAN,
+ gobject.TYPE_BOOLEAN)
+ self.servers.set_model(self.model)
+ self.servers.set_reorderable(1)
+ self.servers.set_headers_clickable(1)
+ # self.servers.set_headers_draggable(1)
+ # add a column
+ for col in [
+ gtk.TreeViewColumn('Server',
+ gtk.CellRendererText(),
+ text=0),
+ gtk.TreeViewColumn('Reading',
+ gtk.CellRendererToggle(),
+ active=2),
+ gtk.TreeViewColumn('Writing',
+ gtk.CellRendererToggle(),
+ active=3)]:
+
+ self.servers.append_column(col)
+ col.set_resizable(1)
+ sup.__init__(self)
+
+ def addReader(self, reader):
+ sup.addReader(self, reader)
+## gtk docs suggest this - but it's stupid
+## self.model.set(self.model.append(),
+## 0, str(reader),
+## 1, reader)
+ self._maybeAddServer(reader, read=1)
+
+ def _goAway(self,reader):
+ for p in range(len(self.model)):
+ if self.model[p][1] == reader:
+ self.model.remove(self.model.get_iter_from_string(str(p)))
+ return
+
+
+ def _maybeAddServer(self, reader, read=0, write=0):
+ p = 0
+ for x in self.model:
+ if x[1] == reader:
+ if reader == 0:
+ reader += 1
+ x[2] += read
+ x[3] += write
+ x[2] = max(x[2],0)
+ x[3] = max(x[3],0)
+
+ if not (x[2] or x[3]):
+ x[0] = x[0] + '(disconnected)'
+ self.callLater(5, self._goAway, reader)
+ return
+ p += 1
+ else:
+ read = max(read,0)
+ write = max(write, 0)
+ if read or write:
+ self.model.append((reader,reader,read,write))
+
+ def addWriter(self, writer):
+ sup.addWriter(self, writer)
+ self._maybeAddServer(writer, write=1)
+
+ def removeReader(self, reader):
+ sup.removeReader(self, reader)
+ self._maybeAddServer(reader, read=-1)
+
+ def removeWriter(self, writer):
+ sup.removeWriter(self, writer)
+ self._maybeAddServer(writer, write=-1)
+
+ def crash(self):
+ gtk.main_quit()
+
+ def run(self, installSignalHandlers=1):
+ self.startRunning(installSignalHandlers=installSignalHandlers)
+ self.simulate()
+ gtk.main()
+
+
+def install():
+ """Configure the twisted mainloop to be run inside the gtk mainloop.
+ """
+ reactor = GladeReactor()
+ from twisted.internet.main import installReactor
+ installReactor(reactor)
+ return reactor
diff --git a/twisted/manhole/inspectro.glade b/twisted/manhole/inspectro.glade
new file mode 100644
index 0000000..94b8717
--- /dev/null
+++ b/twisted/manhole/inspectro.glade
@@ -0,0 +1,510 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+<requires lib="gnome"/>
+<requires lib="bonobo"/>
+
+<widget class="GnomeApp" id="app1">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Inspectro</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="default_width">640</property>
+ <property name="default_height">480</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+ <property name="enable_layout_config">True</property>
+
+ <child internal-child="dock">
+ <widget class="BonoboDock" id="bonobodock1">
+ <property name="visible">True</property>
+ <property name="allow_floating">True</property>
+
+ <child>
+ <widget class="BonoboDockItem" id="bonobodockitem1">
+ <property name="visible">True</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+
+ <child>
+ <widget class="GtkMenuBar" id="menubar1">
+ <property name="visible">True</property>
+
+ <child>
+ <widget class="GtkMenuItem" id="inspector1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspector</property>
+ <property name="use_underline">True</property>
+
+ <child>
+ <widget class="GtkMenu" id="inspector1_menu">
+
+ <child>
+ <widget class="GtkMenuItem" id="select1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Select</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_select" last_modification_time="Wed, 17 Dec 2003 05:05:34 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="inspect1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspect</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_inspect" last_modification_time="Wed, 17 Dec 2003 05:05:34 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="inspect_new1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspect New</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_inspect_new" last_modification_time="Wed, 17 Dec 2003 05:05:34 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="help1">
+ <property name="visible">True</property>
+ <property name="stock_item">GNOMEUIINFO_MENU_HELP_TREE</property>
+
+ <child>
+ <widget class="GtkMenu" id="help1_menu">
+
+ <child>
+ <widget class="GtkImageMenuItem" id="about1">
+ <property name="visible">True</property>
+ <property name="stock_item">GNOMEUIINFO_MENU_ABOUT_ITEM</property>
+ <signal name="activate" handler="on_about1_activate" last_modification_time="Wed, 17 Dec 2003 04:48:59 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="placement">BONOBO_DOCK_TOP</property>
+ <property name="band">0</property>
+ <property name="position">0</property>
+ <property name="offset">0</property>
+ <property name="behavior">BONOBO_DOCK_ITEM_BEH_EXCLUSIVE|BONOBO_DOCK_ITEM_BEH_NEVER_VERTICAL|BONOBO_DOCK_ITEM_BEH_LOCKED</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="BonoboDockItem" id="bonobodockitem2">
+ <property name="visible">True</property>
+ <property name="shadow_type">GTK_SHADOW_OUT</property>
+
+ <child>
+ <widget class="GtkToolbar" id="toolbar2">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+ <property name="toolbar_style">GTK_TOOLBAR_BOTH</property>
+ <property name="tooltips">True</property>
+
+ <child>
+ <widget class="button" id="button13">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Select</property>
+ <property name="use_underline">True</property>
+ <property name="stock_pixmap">gtk-convert</property>
+ <signal name="clicked" handler="on_select" last_modification_time="Wed, 17 Dec 2003 05:05:14 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="button" id="button14">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspect</property>
+ <property name="use_underline">True</property>
+ <property name="stock_pixmap">gtk-jump-to</property>
+ <signal name="clicked" handler="on_inspect" last_modification_time="Wed, 17 Dec 2003 05:05:02 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="button" id="button15">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Inspect New</property>
+ <property name="use_underline">True</property>
+ <property name="stock_pixmap">gtk-redo</property>
+ <signal name="clicked" handler="on_inspect_new" last_modification_time="Wed, 17 Dec 2003 05:04:50 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="placement">BONOBO_DOCK_TOP</property>
+ <property name="band">1</property>
+ <property name="position">0</property>
+ <property name="offset">0</property>
+ <property name="behavior">BONOBO_DOCK_ITEM_BEH_EXCLUSIVE</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkHPaned" id="hpaned1">
+ <property name="width_request">350</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="position">250</property>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow4">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTreeView" id="treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">True</property>
+ <property name="rules_hint">False</property>
+ <property name="reorderable">False</property>
+ <property name="enable_search">True</property>
+ <signal name="row_activated" handler="on_row_activated" last_modification_time="Wed, 17 Dec 2003 05:07:55 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">2</property>
+ <property name="n_columns">2</property>
+ <property name="homogeneous">False</property>
+ <property name="row_spacing">0</property>
+ <property name="column_spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="itname">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">None</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="itpath">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">[]</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">It: </property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_RIGHT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="x_padding">3</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Path: </property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_RIGHT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_padding">3</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">3</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">True</property>
+ <property name="resize">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkVPaned" id="vpaned1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="position">303</property>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow3">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTextView" id="output">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="justification">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap_mode">GTK_WRAP_NONE</property>
+ <property name="cursor_visible">True</property>
+ <property name="pixels_above_lines">0</property>
+ <property name="pixels_below_lines">0</property>
+ <property name="pixels_inside_wrap">0</property>
+ <property name="left_margin">0</property>
+ <property name="right_margin">0</property>
+ <property name="indent">0</property>
+ <property name="text" translatable="yes"></property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">False</property>
+ <property name="resize">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkViewport" id="viewport1">
+ <property name="visible">True</property>
+ <property name="shadow_type">GTK_SHADOW_IN</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkButton" id="button16">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <signal name="clicked" handler="on_execute" last_modification_time="Wed, 17 Dec 2003 05:06:44 GMT"/>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox3">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="stock">gtk-execute</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">&gt;&gt;&gt;</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkTextView" id="input">
+ <property name="height_request">25</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="editable">True</property>
+ <property name="justification">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap_mode">GTK_WRAP_NONE</property>
+ <property name="cursor_visible">True</property>
+ <property name="pixels_above_lines">0</property>
+ <property name="pixels_below_lines">0</property>
+ <property name="pixels_inside_wrap">0</property>
+ <property name="left_margin">0</property>
+ <property name="right_margin">0</property>
+ <property name="indent">0</property>
+ <property name="text" translatable="yes"></property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">False</property>
+ <property name="resize">True</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">True</property>
+ <property name="resize">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child internal-child="appbar">
+ <widget class="GnomeAppBar" id="appbar1">
+ <property name="visible">True</property>
+ <property name="has_progress">False</property>
+ <property name="has_status">True</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/twisted/manhole/logview.glade b/twisted/manhole/logview.glade
new file mode 100644
index 0000000..1ec0b1f
--- /dev/null
+++ b/twisted/manhole/logview.glade
@@ -0,0 +1,39 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkWindow" id="logview">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Log</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+ <signal name="destroy" handler="on_logview_destroy" last_modification_time="Sun, 04 Jan 2004 22:16:59 GMT"/>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_ALWAYS</property>
+ <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTreeView" id="loglist">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">True</property>
+ <property name="rules_hint">False</property>
+ <property name="reorderable">False</property>
+ <property name="enable_search">True</property>
+ </widget>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/twisted/manhole/service.py b/twisted/manhole/service.py
new file mode 100644
index 0000000..332bc81
--- /dev/null
+++ b/twisted/manhole/service.py
@@ -0,0 +1,399 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""L{twisted.manhole} L{PB<twisted.spread.pb>} service implementation.
+"""
+
+# twisted imports
+from twisted import copyright
+from twisted.spread import pb
+from twisted.python import log, failure
+from twisted.cred import portal
+from twisted.application import service
+from zope.interface import implements, Interface
+
+# sibling imports
+import explorer
+
+# system imports
+from cStringIO import StringIO
+
+import string
+import sys
+import traceback
+import types
+
+
+class FakeStdIO:
+ def __init__(self, type_, list):
+ self.type = type_
+ self.list = list
+
+ def write(self, text):
+ log.msg("%s: %s" % (self.type, string.strip(str(text))))
+ self.list.append((self.type, text))
+
+ def flush(self):
+ pass
+
+ def consolidate(self):
+ """Concatenate adjacent messages of same type into one.
+
+ Greatly cuts down on the number of elements, increasing
+ network transport friendliness considerably.
+ """
+ if not self.list:
+ return
+
+ inlist = self.list
+ outlist = []
+ last_type = inlist[0]
+ block_begin = 0
+ for i in xrange(1, len(self.list)):
+ (mtype, message) = inlist[i]
+ if mtype == last_type:
+ continue
+ else:
+ if (i - block_begin) == 1:
+ outlist.append(inlist[block_begin])
+ else:
+ messages = map(lambda l: l[1],
+ inlist[block_begin:i])
+ message = string.join(messages, '')
+ outlist.append((last_type, message))
+ last_type = mtype
+ block_begin = i
+
+
+class IManholeClient(Interface):
+ def console(list_of_messages):
+ """Takes a list of (type, message) pairs to display.
+
+ Types include:
+ - \"stdout\" -- string sent to sys.stdout
+
+ - \"stderr\" -- string sent to sys.stderr
+
+ - \"result\" -- string repr of the resulting value
+ of the expression
+
+ - \"exception\" -- a L{failure.Failure}
+ """
+
+ def receiveExplorer(xplorer):
+ """Receives an explorer.Explorer
+ """
+
+ def listCapabilities():
+ """List what manholey things I am capable of doing.
+
+ i.e. C{\"Explorer\"}, C{\"Failure\"}
+ """
+
+def runInConsole(command, console, globalNS=None, localNS=None,
+ filename=None, args=None, kw=None, unsafeTracebacks=False):
+ """Run this, directing all output to the specified console.
+
+ If command is callable, it will be called with the args and keywords
+ provided. Otherwise, command will be compiled and eval'd.
+ (Wouldn't you like a macro?)
+
+ Returns the command's return value.
+
+ The console is called with a list of (type, message) pairs for
+ display, see L{IManholeClient.console}.
+ """
+ output = []
+ fakeout = FakeStdIO("stdout", output)
+ fakeerr = FakeStdIO("stderr", output)
+ errfile = FakeStdIO("exception", output)
+ code = None
+ val = None
+ if filename is None:
+ filename = str(console)
+ if args is None:
+ args = ()
+ if kw is None:
+ kw = {}
+ if localNS is None:
+ localNS = globalNS
+ if (globalNS is None) and (not callable(command)):
+ raise ValueError("Need a namespace to evaluate the command in.")
+
+ try:
+ out = sys.stdout
+ err = sys.stderr
+ sys.stdout = fakeout
+ sys.stderr = fakeerr
+ try:
+ if callable(command):
+ val = apply(command, args, kw)
+ else:
+ try:
+ code = compile(command, filename, 'eval')
+ except:
+ code = compile(command, filename, 'single')
+
+ if code:
+ val = eval(code, globalNS, localNS)
+ finally:
+ sys.stdout = out
+ sys.stderr = err
+ except:
+ (eType, eVal, tb) = sys.exc_info()
+ fail = failure.Failure(eVal, eType, tb)
+ del tb
+ # In CVS reversion 1.35, there was some code here to fill in the
+ # source lines in the traceback for frames in the local command
+ # buffer. But I can't figure out when that's triggered, so it's
+ # going away in the conversion to Failure, until you bring it back.
+ errfile.write(pb.failure2Copyable(fail, unsafeTracebacks))
+
+ if console:
+ fakeout.consolidate()
+ console(output)
+
+ return val
+
+def _failureOldStyle(fail):
+ """Pre-Failure manhole representation of exceptions.
+
+ For compatibility with manhole clients without the \"Failure\"
+ capability.
+
+ A dictionary with two members:
+ - \'traceback\' -- traceback.extract_tb output; a list of tuples
+ (filename, line number, function name, text) suitable for
+ feeding to traceback.format_list.
+
+ - \'exception\' -- a list of one or more strings, each
+ ending in a newline. (traceback.format_exception_only output)
+ """
+ import linecache
+ tb = []
+ for f in fail.frames:
+ # (filename, line number, function name, text)
+ tb.append((f[1], f[2], f[0], linecache.getline(f[1], f[2])))
+
+ return {
+ 'traceback': tb,
+ 'exception': traceback.format_exception_only(fail.type, fail.value)
+ }
+
+# Capabilities clients are likely to have before they knew how to answer a
+# "listCapabilities" query.
+_defaultCapabilities = {
+ "Explorer": 'Set'
+ }
+
+class Perspective(pb.Avatar):
+ lastDeferred = 0
+ def __init__(self, service):
+ self.localNamespace = {
+ "service": service,
+ "avatar": self,
+ "_": None,
+ }
+ self.clients = {}
+ self.service = service
+
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ state['clients'] = {}
+ if state['localNamespace'].has_key("__builtins__"):
+ del state['localNamespace']['__builtins__']
+ return state
+
+ def attached(self, client, identity):
+ """A client has attached -- welcome them and add them to the list.
+ """
+ self.clients[client] = identity
+
+ host = ':'.join(map(str, client.broker.transport.getHost()[1:]))
+
+ msg = self.service.welcomeMessage % {
+ 'you': getattr(identity, 'name', str(identity)),
+ 'host': host,
+ 'longversion': copyright.longversion,
+ }
+
+ client.callRemote('console', [("stdout", msg)])
+
+ client.capabilities = _defaultCapabilities
+ client.callRemote('listCapabilities').addCallbacks(
+ self._cbClientCapable, self._ebClientCapable,
+ callbackArgs=(client,),errbackArgs=(client,))
+
+ def detached(self, client, identity):
+ try:
+ del self.clients[client]
+ except KeyError:
+ pass
+
+ def runInConsole(self, command, *args, **kw):
+ """Convience method to \"runInConsole with my stuff\".
+ """
+ return runInConsole(command,
+ self.console,
+ self.service.namespace,
+ self.localNamespace,
+ str(self.service),
+ args=args,
+ kw=kw,
+ unsafeTracebacks=self.service.unsafeTracebacks)
+
+
+ ### Methods for communicating to my clients.
+
+ def console(self, message):
+ """Pass a message to my clients' console.
+ """
+ clients = self.clients.keys()
+ origMessage = message
+ compatMessage = None
+ for client in clients:
+ try:
+ if not client.capabilities.has_key("Failure"):
+ if compatMessage is None:
+ compatMessage = origMessage[:]
+ for i in xrange(len(message)):
+ if ((message[i][0] == "exception") and
+ isinstance(message[i][1], failure.Failure)):
+ compatMessage[i] = (
+ message[i][0],
+ _failureOldStyle(message[i][1]))
+ client.callRemote('console', compatMessage)
+ else:
+ client.callRemote('console', message)
+ except pb.ProtocolError:
+ # Stale broker.
+ self.detached(client, None)
+
+ def receiveExplorer(self, objectLink):
+ """Pass an Explorer on to my clients.
+ """
+ clients = self.clients.keys()
+ for client in clients:
+ try:
+ client.callRemote('receiveExplorer', objectLink)
+ except pb.ProtocolError:
+ # Stale broker.
+ self.detached(client, None)
+
+
+ def _cbResult(self, val, dnum):
+ self.console([('result', "Deferred #%s Result: %r\n" %(dnum, val))])
+ return val
+
+ def _cbClientCapable(self, capabilities, client):
+ log.msg("client %x has %s" % (id(client), capabilities))
+ client.capabilities = capabilities
+
+ def _ebClientCapable(self, reason, client):
+ reason.trap(AttributeError)
+ log.msg("Couldn't get capabilities from %s, assuming defaults." %
+ (client,))
+
+ ### perspective_ methods, commands used by the client.
+
+ def perspective_do(self, expr):
+ """Evaluate the given expression, with output to the console.
+
+ The result is stored in the local variable '_', and its repr()
+ string is sent to the console as a \"result\" message.
+ """
+ log.msg(">>> %s" % expr)
+ val = self.runInConsole(expr)
+ if val is not None:
+ self.localNamespace["_"] = val
+ from twisted.internet.defer import Deferred
+ # TODO: client support for Deferred.
+ if isinstance(val, Deferred):
+ self.lastDeferred += 1
+ self.console([('result', "Waiting for Deferred #%s...\n" % self.lastDeferred)])
+ val.addBoth(self._cbResult, self.lastDeferred)
+ else:
+ self.console([("result", repr(val) + '\n')])
+ log.msg("<<<")
+
+ def perspective_explore(self, identifier):
+ """Browse the object obtained by evaluating the identifier.
+
+ The resulting ObjectLink is passed back through the client's
+ receiveBrowserObject method.
+ """
+ object = self.runInConsole(identifier)
+ if object:
+ expl = explorer.explorerPool.getExplorer(object, identifier)
+ self.receiveExplorer(expl)
+
+ def perspective_watch(self, identifier):
+ """Watch the object obtained by evaluating the identifier.
+
+ Whenever I think this object might have changed, I will pass
+ an ObjectLink of it back to the client's receiveBrowserObject
+ method.
+ """
+ raise NotImplementedError
+ object = self.runInConsole(identifier)
+ if object:
+ # Return an ObjectLink of this right away, before the watch.
+ oLink = self.runInConsole(self.browser.browseObject,
+ object, identifier)
+ self.receiveExplorer(oLink)
+
+ self.runInConsole(self.browser.watchObject,
+ object, identifier,
+ self.receiveExplorer)
+
+
+class Realm:
+
+ implements(portal.IRealm)
+
+ def __init__(self, service):
+ self.service = service
+ self._cache = {}
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if pb.IPerspective not in interfaces:
+ raise NotImplementedError("no interface")
+ if avatarId in self._cache:
+ p = self._cache[avatarId]
+ else:
+ p = Perspective(self.service)
+ p.attached(mind, avatarId)
+ def detached():
+ p.detached(mind, avatarId)
+ return (pb.IPerspective, p, detached)
+
+
+class Service(service.Service):
+
+ welcomeMessage = (
+ "\nHello %(you)s, welcome to Manhole "
+ "on %(host)s.\n"
+ "%(longversion)s.\n\n")
+
+ def __init__(self, unsafeTracebacks=False, namespace=None):
+ self.unsafeTracebacks = unsafeTracebacks
+ self.namespace = {
+ '__name__': '__manhole%x__' % (id(self),),
+ 'sys': sys
+ }
+ if namespace:
+ self.namespace.update(namespace)
+
+ def __getstate__(self):
+ """This returns the persistent state of this shell factory.
+ """
+ # TODO -- refactor this and twisted.reality.author.Author to
+ # use common functionality (perhaps the 'code' module?)
+ dict = self.__dict__.copy()
+ ns = dict['namespace'].copy()
+ dict['namespace'] = ns
+ if ns.has_key('__builtins__'):
+ del ns['__builtins__']
+ return dict
diff --git a/twisted/manhole/telnet.py b/twisted/manhole/telnet.py
new file mode 100644
index 0000000..d63b3a6
--- /dev/null
+++ b/twisted/manhole/telnet.py
@@ -0,0 +1,117 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Telnet-based shell."""
+
+# twisted imports
+from twisted.protocols import telnet
+from twisted.internet import protocol
+from twisted.python import log, failure
+
+# system imports
+import string, copy, sys
+from cStringIO import StringIO
+
+
+class Shell(telnet.Telnet):
+ """A Python command-line shell."""
+
+ def connectionMade(self):
+ telnet.Telnet.connectionMade(self)
+ self.lineBuffer = []
+
+ def loggedIn(self):
+ self.transport.write(">>> ")
+
+ def checkUserAndPass(self, username, password):
+ return ((self.factory.username == username) and (password == self.factory.password))
+
+ def write(self, data):
+ """Write some data to the transport.
+ """
+ self.transport.write(data)
+
+ def telnet_Command(self, cmd):
+ if self.lineBuffer:
+ if not cmd:
+ cmd = string.join(self.lineBuffer, '\n') + '\n\n\n'
+ self.doCommand(cmd)
+ self.lineBuffer = []
+ return "Command"
+ else:
+ self.lineBuffer.append(cmd)
+ self.transport.write("... ")
+ return "Command"
+ else:
+ self.doCommand(cmd)
+ return "Command"
+
+ def doCommand(self, cmd):
+
+ # TODO -- refactor this, Reality.author.Author, and the manhole shell
+ #to use common functionality (perhaps a twisted.python.code module?)
+ fn = '$telnet$'
+ result = None
+ try:
+ out = sys.stdout
+ sys.stdout = self
+ try:
+ code = compile(cmd,fn,'eval')
+ result = eval(code, self.factory.namespace)
+ except:
+ try:
+ code = compile(cmd, fn, 'exec')
+ exec code in self.factory.namespace
+ except SyntaxError, e:
+ if not self.lineBuffer and str(e)[:14] == "unexpected EOF":
+ self.lineBuffer.append(cmd)
+ self.transport.write("... ")
+ return
+ else:
+ failure.Failure().printTraceback(file=self)
+ log.deferr()
+ self.write('\r\n>>> ')
+ return
+ except:
+ io = StringIO()
+ failure.Failure().printTraceback(file=self)
+ log.deferr()
+ self.write('\r\n>>> ')
+ return
+ finally:
+ sys.stdout = out
+
+ self.factory.namespace['_'] = result
+ if result is not None:
+ self.transport.write(repr(result))
+ self.transport.write('\r\n')
+ self.transport.write(">>> ")
+
+
+
+class ShellFactory(protocol.Factory):
+ username = "admin"
+ password = "admin"
+ protocol = Shell
+ service = None
+
+ def __init__(self):
+ self.namespace = {
+ 'factory': self,
+ 'service': None,
+ '_': None
+ }
+
+ def setService(self, service):
+ self.namespace['service'] = self.service = service
+
+ def __getstate__(self):
+ """This returns the persistent state of this shell factory.
+ """
+ dict = self.__dict__
+ ns = copy.copy(dict['namespace'])
+ dict['namespace'] = ns
+ if ns.has_key('__builtins__'):
+ del ns['__builtins__']
+ return dict
diff --git a/twisted/manhole/test/__init__.py b/twisted/manhole/test/__init__.py
new file mode 100644
index 0000000..83c9ea1
--- /dev/null
+++ b/twisted/manhole/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.manhole}.
+"""
diff --git a/twisted/manhole/test/test_explorer.py b/twisted/manhole/test/test_explorer.py
new file mode 100644
index 0000000..a52d3c1
--- /dev/null
+++ b/twisted/manhole/test/test_explorer.py
@@ -0,0 +1,102 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.manhole.explorer}.
+"""
+
+from twisted.trial import unittest
+from twisted.manhole.explorer import (
+ CRUFT_WatchyThingie,
+ ExplorerImmutable,
+ Pool,
+ _WatchMonkey,
+ )
+
+
+class Foo:
+ """
+ Test helper.
+ """
+
+
+class PoolTestCase(unittest.TestCase):
+ """
+ Tests for the Pool class.
+ """
+
+ def test_instanceBuilding(self):
+ """
+ If the object is not in the pool a new instance is created and
+ returned.
+ """
+ p = Pool()
+ e = p.getExplorer(123, 'id')
+ self.assertIsInstance(e, ExplorerImmutable)
+ self.assertEqual(e.value, 123)
+ self.assertEqual(e.identifier, 'id')
+
+
+
+class CRUFTWatchyThingieTestCase(unittest.TestCase):
+ """
+ Tests for the CRUFT_WatchyThingie class.
+ """
+ def test_watchObjectConstructedClass(self):
+ """
+ L{CRUFT_WatchyThingie.watchObject} changes the class of its
+ first argument to a custom watching class.
+ """
+ foo = Foo()
+ cwt = CRUFT_WatchyThingie()
+ cwt.watchObject(foo, 'id', 'cback')
+
+ # check new constructed class
+ newClassName = foo.__class__.__name__
+ self.assertEqual(newClassName, "WatchingFoo%X" % (id(foo),))
+
+
+ def test_watchObjectConstructedInstanceMethod(self):
+ """
+ L{CRUFT_WatchyThingie.watchingfoo} adds a C{_watchEmitChanged}
+ attribute which refers to a bound method on the instance
+ passed to it.
+ """
+ foo = Foo()
+ cwt = CRUFT_WatchyThingie()
+ cwt.watchObject(foo, 'id', 'cback')
+
+ # check new constructed instance method
+ self.assertIdentical(foo._watchEmitChanged.im_self, foo)
+
+
+
+class WatchMonkeyTestCase(unittest.TestCase):
+ """
+ Tests for the _WatchMonkey class.
+ """
+ def test_install(self):
+ """
+ When _WatchMonkey is installed on a method, calling that
+ method calls the _WatchMonkey.
+ """
+ class Foo:
+ """
+ Helper.
+ """
+ def someMethod(self):
+ """
+ Just a method.
+ """
+
+ foo = Foo()
+ wm = _WatchMonkey(foo)
+ wm.install('someMethod')
+
+ # patch wm's method to check that the method was exchanged
+ called = []
+ wm.__call__ = lambda s: called.append(True)
+
+ # call and check
+ foo.someMethod()
+ self.assertTrue(called)
diff --git a/twisted/manhole/ui/__init__.py b/twisted/manhole/ui/__init__.py
new file mode 100644
index 0000000..14af615
--- /dev/null
+++ b/twisted/manhole/ui/__init__.py
@@ -0,0 +1,7 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Twisted Manhole UI: User interface for direct manipulation in Twisted.
+"""
diff --git a/twisted/manhole/ui/gtk2manhole.glade b/twisted/manhole/ui/gtk2manhole.glade
new file mode 100644
index 0000000..423b3fb
--- /dev/null
+++ b/twisted/manhole/ui/gtk2manhole.glade
@@ -0,0 +1,268 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkWindow" id="manholeWindow">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Manhole</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="default_width">620</property>
+ <property name="default_height">320</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+ <property name="decorated">True</property>
+ <property name="skip_taskbar_hint">False</property>
+ <property name="skip_pager_hint">False</property>
+ <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
+ <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+ <signal name="delete_event" handler="_on_manholeWindow_delete_event" last_modification_time="Mon, 27 Jan 2003 05:14:26 GMT"/>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkMenuBar" id="menubar1">
+ <property name="visible">True</property>
+
+ <child>
+ <widget class="GtkMenuItem" id="menuitem4">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_File</property>
+ <property name="use_underline">True</property>
+
+ <child>
+ <widget class="GtkMenu" id="menuitem4_menu">
+
+ <child>
+ <widget class="GtkImageMenuItem" id="openMenuItem">
+ <property name="visible">True</property>
+ <property name="label">gtk-open</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="_on_openMenuItem_activate" last_modification_time="Sun, 02 Feb 2003 18:44:51 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkImageMenuItem" id="reload_self">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">Reload the manhole client code. (Only useful for client development.)</property>
+ <property name="label" translatable="yes">_Reload self</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="on_reload_self_activate" last_modification_time="Mon, 24 Feb 2003 00:15:10 GMT"/>
+
+ <child internal-child="image">
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="stock">gtk-revert-to-saved</property>
+ <property name="icon_size">1</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="separatormenuitem1">
+ <property name="visible">True</property>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkImageMenuItem" id="quitMenuItem">
+ <property name="visible">True</property>
+ <property name="label">gtk-quit</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="_on_quitMenuItem_activate" last_modification_time="Sun, 02 Feb 2003 18:48:12 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="menuitem5">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Edit</property>
+ <property name="use_underline">True</property>
+
+ <child>
+ <widget class="GtkMenu" id="menuitem5_menu">
+
+ <child>
+ <widget class="GtkImageMenuItem" id="cut1">
+ <property name="visible">True</property>
+ <property name="label">gtk-cut</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_cut1_activate" last_modification_time="Mon, 27 Jan 2003 04:50:50 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkImageMenuItem" id="copy1">
+ <property name="visible">True</property>
+ <property name="label">gtk-copy</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_copy1_activate" last_modification_time="Mon, 27 Jan 2003 04:50:50 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkImageMenuItem" id="paste1">
+ <property name="visible">True</property>
+ <property name="label">gtk-paste</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_paste1_activate" last_modification_time="Mon, 27 Jan 2003 04:50:50 GMT"/>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkImageMenuItem" id="delete1">
+ <property name="visible">True</property>
+ <property name="label">gtk-delete</property>
+ <property name="use_stock">True</property>
+ <signal name="activate" handler="on_delete1_activate" last_modification_time="Mon, 27 Jan 2003 04:50:50 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkMenuItem" id="menuitem7">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Help</property>
+ <property name="use_underline">True</property>
+
+ <child>
+ <widget class="GtkMenu" id="menuitem7_menu">
+
+ <child>
+ <widget class="GtkMenuItem" id="aboutMenuItem">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_About</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="_on_aboutMenuItem_activate" last_modification_time="Thu, 06 Feb 2003 19:49:53 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkVPaned" id="vpaned1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTextView" id="output">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="editable">False</property>
+ <property name="overwrite">False</property>
+ <property name="accepts_tab">True</property>
+ <property name="justification">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap_mode">GTK_WRAP_WORD</property>
+ <property name="cursor_visible">True</property>
+ <property name="pixels_above_lines">0</property>
+ <property name="pixels_below_lines">0</property>
+ <property name="pixels_inside_wrap">0</property>
+ <property name="left_margin">0</property>
+ <property name="right_margin">0</property>
+ <property name="indent">0</property>
+ <property name="text" translatable="yes"></property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">True</property>
+ <property name="resize">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+ <property name="vscrollbar_policy">GTK_POLICY_ALWAYS</property>
+ <property name="shadow_type">GTK_SHADOW_NONE</property>
+ <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
+
+ <child>
+ <widget class="GtkTextView" id="input">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="editable">True</property>
+ <property name="overwrite">False</property>
+ <property name="accepts_tab">True</property>
+ <property name="justification">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap_mode">GTK_WRAP_NONE</property>
+ <property name="cursor_visible">True</property>
+ <property name="pixels_above_lines">0</property>
+ <property name="pixels_below_lines">0</property>
+ <property name="pixels_inside_wrap">0</property>
+ <property name="left_margin">0</property>
+ <property name="right_margin">0</property>
+ <property name="indent">0</property>
+ <property name="text" translatable="yes"></property>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="shrink">True</property>
+ <property name="resize">False</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkStatusbar" id="statusbar1">
+ <property name="visible">True</property>
+ <property name="has_resize_grip">True</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/twisted/manhole/ui/gtk2manhole.py b/twisted/manhole/ui/gtk2manhole.py
new file mode 100644
index 0000000..2c6a532
--- /dev/null
+++ b/twisted/manhole/ui/gtk2manhole.py
@@ -0,0 +1,375 @@
+# -*- test-case-name: twisted.manhole.ui.test.test_gtk2manhole -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Manhole client with a GTK v2.x front-end.
+"""
+
+__version__ = '$Revision: 1.9 $'[11:-2]
+
+from twisted import copyright
+from twisted.internet import reactor
+from twisted.python import components, failure, log, util
+from twisted.python.reflect import prefixedMethodNames
+from twisted.spread import pb
+from twisted.spread.ui import gtk2util
+
+from twisted.manhole.service import IManholeClient
+from zope.interface import implements
+
+# The pygtk.require for version 2.0 has already been done by the reactor.
+import gtk
+
+import code, types, inspect
+
+# TODO:
+# Make wrap-mode a run-time option.
+# Explorer.
+# Code doesn't cleanly handle opening a second connection. Fix that.
+# Make some acknowledgement of when a command has completed, even if
+# it has no return value so it doesn't print anything to the console.
+
+class OfflineError(Exception):
+ pass
+
+class ManholeWindow(components.Componentized, gtk2util.GladeKeeper):
+ gladefile = util.sibpath(__file__, "gtk2manhole.glade")
+
+ _widgets = ('input','output','manholeWindow')
+
+ def __init__(self):
+ self.defaults = {}
+ gtk2util.GladeKeeper.__init__(self)
+ components.Componentized.__init__(self)
+
+ self.input = ConsoleInput(self._input)
+ self.input.toplevel = self
+ self.output = ConsoleOutput(self._output)
+
+ # Ugh. GladeKeeper actually isn't so good for composite objects.
+ # I want this connected to the ConsoleInput's handler, not something
+ # on this class.
+ self._input.connect("key_press_event", self.input._on_key_press_event)
+
+ def setDefaults(self, defaults):
+ self.defaults = defaults
+
+ def login(self):
+ client = self.getComponent(IManholeClient)
+ d = gtk2util.login(client, **self.defaults)
+ d.addCallback(self._cbLogin)
+ d.addCallback(client._cbLogin)
+ d.addErrback(self._ebLogin)
+
+ def _cbDisconnected(self, perspective):
+ self.output.append("%s went away. :(\n" % (perspective,), "local")
+ self._manholeWindow.set_title("Manhole")
+
+ def _cbLogin(self, perspective):
+ peer = perspective.broker.transport.getPeer()
+ self.output.append("Connected to %s\n" % (peer,), "local")
+ perspective.notifyOnDisconnect(self._cbDisconnected)
+ self._manholeWindow.set_title("Manhole - %s" % (peer))
+ return perspective
+
+ def _ebLogin(self, reason):
+ self.output.append("Login FAILED %s\n" % (reason.value,), "exception")
+
+ def _on_aboutMenuItem_activate(self, widget, *unused):
+ import sys
+ from os import path
+ self.output.append("""\
+a Twisted Manhole client
+ Versions:
+ %(twistedVer)s
+ Python %(pythonVer)s on %(platform)s
+ GTK %(gtkVer)s / PyGTK %(pygtkVer)s
+ %(module)s %(modVer)s
+http://twistedmatrix.com/
+""" % {'twistedVer': copyright.longversion,
+ 'pythonVer': sys.version.replace('\n', '\n '),
+ 'platform': sys.platform,
+ 'gtkVer': ".".join(map(str, gtk.gtk_version)),
+ 'pygtkVer': ".".join(map(str, gtk.pygtk_version)),
+ 'module': path.basename(__file__),
+ 'modVer': __version__,
+ }, "local")
+
+ def _on_openMenuItem_activate(self, widget, userdata=None):
+ self.login()
+
+ def _on_manholeWindow_delete_event(self, widget, *unused):
+ reactor.stop()
+
+ def _on_quitMenuItem_activate(self, widget, *unused):
+ reactor.stop()
+
+ def on_reload_self_activate(self, *unused):
+ from twisted.python import rebuild
+ rebuild.rebuild(inspect.getmodule(self.__class__))
+
+
+tagdefs = {
+ 'default': {"family": "monospace"},
+ # These are message types we get from the server.
+ 'stdout': {"foreground": "black"},
+ 'stderr': {"foreground": "#AA8000"},
+ 'result': {"foreground": "blue"},
+ 'exception': {"foreground": "red"},
+ # Messages generate locally.
+ 'local': {"foreground": "#008000"},
+ 'log': {"foreground": "#000080"},
+ 'command': {"foreground": "#666666"},
+ }
+
+# TODO: Factor Python console stuff back out to pywidgets.
+
+class ConsoleOutput:
+ _willScroll = None
+ def __init__(self, textView):
+ self.textView = textView
+ self.buffer = textView.get_buffer()
+
+ # TODO: Make this a singleton tag table.
+ for name, props in tagdefs.iteritems():
+ tag = self.buffer.create_tag(name)
+ # This can be done in the constructor in newer pygtk (post 1.99.14)
+ for k, v in props.iteritems():
+ tag.set_property(k, v)
+
+ self.buffer.tag_table.lookup("default").set_priority(0)
+
+ self._captureLocalLog()
+
+ def _captureLocalLog(self):
+ return log.startLogging(_Notafile(self, "log"), setStdout=False)
+
+ def append(self, text, kind=None):
+ # XXX: It seems weird to have to do this thing with always applying
+ # a 'default' tag. Can't we change the fundamental look instead?
+ tags = ["default"]
+ if kind is not None:
+ tags.append(kind)
+
+ self.buffer.insert_with_tags_by_name(self.buffer.get_end_iter(),
+ text, *tags)
+ # Silly things, the TextView needs to update itself before it knows
+ # where the bottom is.
+ if self._willScroll is None:
+ self._willScroll = gtk.idle_add(self._scrollDown)
+
+ def _scrollDown(self, *unused):
+ self.textView.scroll_to_iter(self.buffer.get_end_iter(), 0,
+ True, 1.0, 1.0)
+ self._willScroll = None
+ return False
+
+class History:
+ def __init__(self, maxhist=10000):
+ self.ringbuffer = ['']
+ self.maxhist = maxhist
+ self.histCursor = 0
+
+ def append(self, htext):
+ self.ringbuffer.insert(-1, htext)
+ if len(self.ringbuffer) > self.maxhist:
+ self.ringbuffer.pop(0)
+ self.histCursor = len(self.ringbuffer) - 1
+ self.ringbuffer[-1] = ''
+
+ def move(self, prevnext=1):
+ '''
+ Return next/previous item in the history, stopping at top/bottom.
+ '''
+ hcpn = self.histCursor + prevnext
+ if hcpn >= 0 and hcpn < len(self.ringbuffer):
+ self.histCursor = hcpn
+ return self.ringbuffer[hcpn]
+ else:
+ return None
+
+ def histup(self, textbuffer):
+ if self.histCursor == len(self.ringbuffer) - 1:
+ si, ei = textbuffer.get_start_iter(), textbuffer.get_end_iter()
+ self.ringbuffer[-1] = textbuffer.get_text(si,ei)
+ newtext = self.move(-1)
+ if newtext is None:
+ return
+ textbuffer.set_text(newtext)
+
+ def histdown(self, textbuffer):
+ newtext = self.move(1)
+ if newtext is None:
+ return
+ textbuffer.set_text(newtext)
+
+
+class ConsoleInput:
+ toplevel, rkeymap = None, None
+ __debug = False
+
+ def __init__(self, textView):
+ self.textView=textView
+ self.rkeymap = {}
+ self.history = History()
+ for name in prefixedMethodNames(self.__class__, "key_"):
+ keysymName = name.split("_")[-1]
+ self.rkeymap[getattr(gtk.keysyms, keysymName)] = keysymName
+
+ def _on_key_press_event(self, entry, event):
+ stopSignal = False
+ ksym = self.rkeymap.get(event.keyval, None)
+
+ mods = []
+ for prefix, mask in [('ctrl', gtk.gdk.CONTROL_MASK), ('shift', gtk.gdk.SHIFT_MASK)]:
+ if event.state & mask:
+ mods.append(prefix)
+
+ if mods:
+ ksym = '_'.join(mods + [ksym])
+
+ if ksym:
+ rvalue = getattr(
+ self, 'key_%s' % ksym, lambda *a, **kw: None)(entry, event)
+
+ if self.__debug:
+ print ksym
+ return rvalue
+
+ def getText(self):
+ buffer = self.textView.get_buffer()
+ iter1, iter2 = buffer.get_bounds()
+ text = buffer.get_text(iter1, iter2, False)
+ return text
+
+ def setText(self, text):
+ self.textView.get_buffer().set_text(text)
+
+ def key_Return(self, entry, event):
+ text = self.getText()
+ # Figure out if that Return meant "next line" or "execute."
+ try:
+ c = code.compile_command(text)
+ except SyntaxError, e:
+ # This could conceivably piss you off if the client's python
+ # doesn't accept keywords that are known to the manhole's
+ # python.
+ point = buffer.get_iter_at_line_offset(e.lineno, e.offset)
+ buffer.place(point)
+ # TODO: Componentize!
+ self.toplevel.output.append(str(e), "exception")
+ except (OverflowError, ValueError), e:
+ self.toplevel.output.append(str(e), "exception")
+ else:
+ if c is not None:
+ self.sendMessage()
+ # Don't insert Return as a newline in the buffer.
+ self.history.append(text)
+ self.clear()
+ # entry.emit_stop_by_name("key_press_event")
+ return True
+ else:
+ # not a complete code block
+ return False
+
+ return False
+
+ def key_Up(self, entry, event):
+ # if I'm at the top, previous history item.
+ textbuffer = self.textView.get_buffer()
+ if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == 0:
+ self.history.histup(textbuffer)
+ return True
+ return False
+
+ def key_Down(self, entry, event):
+ textbuffer = self.textView.get_buffer()
+ if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == (
+ textbuffer.get_line_count() - 1):
+ self.history.histdown(textbuffer)
+ return True
+ return False
+
+ key_ctrl_p = key_Up
+ key_ctrl_n = key_Down
+
+ def key_ctrl_shift_F9(self, entry, event):
+ if self.__debug:
+ import pdb; pdb.set_trace()
+
+ def clear(self):
+ buffer = self.textView.get_buffer()
+ buffer.delete(*buffer.get_bounds())
+
+ def sendMessage(self):
+ buffer = self.textView.get_buffer()
+ iter1, iter2 = buffer.get_bounds()
+ text = buffer.get_text(iter1, iter2, False)
+ self.toplevel.output.append(pythonify(text), 'command')
+ # TODO: Componentize better!
+ try:
+ return self.toplevel.getComponent(IManholeClient).do(text)
+ except OfflineError:
+ self.toplevel.output.append("Not connected, command not sent.\n",
+ "exception")
+
+
+def pythonify(text):
+ '''
+ Make some text appear as though it was typed in at a Python prompt.
+ '''
+ lines = text.split('\n')
+ lines[0] = '>>> ' + lines[0]
+ return '\n... '.join(lines) + '\n'
+
+class _Notafile:
+ """Curry to make failure.printTraceback work with the output widget."""
+ def __init__(self, output, kind):
+ self.output = output
+ self.kind = kind
+
+ def write(self, txt):
+ self.output.append(txt, self.kind)
+
+ def flush(self):
+ pass
+
+class ManholeClient(components.Adapter, pb.Referenceable):
+ implements(IManholeClient)
+
+ capabilities = {
+# "Explorer": 'Set',
+ "Failure": 'Set'
+ }
+
+ def _cbLogin(self, perspective):
+ self.perspective = perspective
+ perspective.notifyOnDisconnect(self._cbDisconnected)
+ return perspective
+
+ def remote_console(self, messages):
+ for kind, content in messages:
+ if isinstance(content, types.StringTypes):
+ self.original.output.append(content, kind)
+ elif (kind == "exception") and isinstance(content, failure.Failure):
+ content.printTraceback(_Notafile(self.original.output,
+ "exception"))
+ else:
+ self.original.output.append(str(content), kind)
+
+ def remote_receiveExplorer(self, xplorer):
+ pass
+
+ def remote_listCapabilities(self):
+ return self.capabilities
+
+ def _cbDisconnected(self, perspective):
+ self.perspective = None
+
+ def do(self, text):
+ if self.perspective is None:
+ raise OfflineError
+ return self.perspective.callRemote("do", text)
+
+components.registerAdapter(ManholeClient, ManholeWindow, IManholeClient)
diff --git a/twisted/manhole/ui/test/__init__.py b/twisted/manhole/ui/test/__init__.py
new file mode 100644
index 0000000..36214ba
--- /dev/null
+++ b/twisted/manhole/ui/test/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (c) 2009 Twisted Matrix Laboratories.
+"""
+Tests for the L{twisted.manhole.ui} package.
+"""
diff --git a/twisted/manhole/ui/test/test_gtk2manhole.py b/twisted/manhole/ui/test/test_gtk2manhole.py
new file mode 100644
index 0000000..b59f937
--- /dev/null
+++ b/twisted/manhole/ui/test/test_gtk2manhole.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2009 Twisted Matrix Laboratories.
+"""
+Tests for GTK2 GUI manhole.
+"""
+
+skip = False
+
+try:
+ import pygtk
+ pygtk.require("2.0")
+except:
+ skip = "GTK 2.0 not available"
+else:
+ try:
+ import gtk
+ except ImportError:
+ skip = "GTK 2.0 not available"
+ except RuntimeError:
+ skip = "Old version of GTK 2.0 requires DISPLAY, and we don't have one."
+ else:
+ if gtk.gtk_version[0] == 1:
+ skip = "Requested GTK 2.0, but 1.0 was already imported."
+ else:
+ from twisted.manhole.ui.gtk2manhole import ConsoleInput
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.reflect import prefixedMethodNames
+
+class ConsoleInputTests(TestCase):
+ """
+ Tests for L{ConsoleInput}.
+ """
+
+ def test_reverseKeymap(self):
+ """
+ Verify that a L{ConsoleInput} has a reverse mapping of the keysym names
+ it needs for event handling to their corresponding keysym.
+ """
+ ci = ConsoleInput(None)
+ for eventName in prefixedMethodNames(ConsoleInput, 'key_'):
+ keysymName = eventName.split("_")[-1]
+ keysymValue = getattr(gtk.keysyms, keysymName)
+ self.assertEqual(ci.rkeymap[keysymValue], keysymName)
+
+
+ skip = skip
+
diff --git a/twisted/names/__init__.py b/twisted/names/__init__.py
new file mode 100644
index 0000000..4c1d7c9
--- /dev/null
+++ b/twisted/names/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Resolving Internet Names"""
+
+from twisted.names._version import version
+__version__ = version.short()
diff --git a/twisted/names/_version.py b/twisted/names/_version.py
new file mode 100644
index 0000000..0a1d5e9
--- /dev/null
+++ b/twisted/names/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.names', 12, 1, 0)
diff --git a/twisted/names/authority.py b/twisted/names/authority.py
new file mode 100644
index 0000000..9d22d5f
--- /dev/null
+++ b/twisted/names/authority.py
@@ -0,0 +1,333 @@
+# -*- test-case-name: twisted.names.test.test_names -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Authoritative resolvers.
+"""
+
+import os
+import time
+
+from twisted.names import dns
+from twisted.internet import defer
+from twisted.python import failure
+
+import common
+
+def getSerial(filename = '/tmp/twisted-names.serial'):
+ """Return a monotonically increasing (across program runs) integer.
+
+ State is stored in the given file. If it does not exist, it is
+ created with rw-/---/--- permissions.
+ """
+ serial = time.strftime('%Y%m%d')
+
+ o = os.umask(0177)
+ try:
+ if not os.path.exists(filename):
+ f = file(filename, 'w')
+ f.write(serial + ' 0')
+ f.close()
+ finally:
+ os.umask(o)
+
+ serialFile = file(filename, 'r')
+ lastSerial, ID = serialFile.readline().split()
+ ID = (lastSerial == serial) and (int(ID) + 1) or 0
+ serialFile.close()
+ serialFile = file(filename, 'w')
+ serialFile.write('%s %d' % (serial, ID))
+ serialFile.close()
+ serial = serial + ('%02d' % (ID,))
+ return serial
+
+
+#class LookupCacherMixin(object):
+# _cache = None
+#
+# def _lookup(self, name, cls, type, timeout = 10):
+# if not self._cache:
+# self._cache = {}
+# self._meth = super(LookupCacherMixin, self)._lookup
+#
+# if self._cache.has_key((name, cls, type)):
+# return self._cache[(name, cls, type)]
+# else:
+# r = self._meth(name, cls, type, timeout)
+# self._cache[(name, cls, type)] = r
+# return r
+
+
+class FileAuthority(common.ResolverBase):
+ """An Authority that is loaded from a file."""
+
+ soa = None
+ records = None
+
+ def __init__(self, filename):
+ common.ResolverBase.__init__(self)
+ self.loadFile(filename)
+ self._cache = {}
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+# print 'setstate ', self.soa
+
+ def _lookup(self, name, cls, type, timeout = None):
+ cnames = []
+ results = []
+ authority = []
+ additional = []
+ default_ttl = max(self.soa[1].minimum, self.soa[1].expire)
+
+ domain_records = self.records.get(name.lower())
+
+ if domain_records:
+ for record in domain_records:
+ if record.ttl is not None:
+ ttl = record.ttl
+ else:
+ ttl = default_ttl
+
+ if record.TYPE == dns.NS and name.lower() != self.soa[0].lower():
+ # NS record belong to a child zone: this is a referral. As
+ # NS records are authoritative in the child zone, ours here
+ # are not. RFC 2181, section 6.1.
+ authority.append(
+ dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=False)
+ )
+ elif record.TYPE == type or type == dns.ALL_RECORDS:
+ results.append(
+ dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True)
+ )
+ if record.TYPE == dns.CNAME:
+ cnames.append(
+ dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True)
+ )
+ if not results:
+ results = cnames
+
+ for record in results + authority:
+ section = {dns.NS: additional, dns.CNAME: results, dns.MX: additional}.get(record.type)
+ if section is not None:
+ n = str(record.payload.name)
+ for rec in self.records.get(n.lower(), ()):
+ if rec.TYPE == dns.A:
+ section.append(
+ dns.RRHeader(n, dns.A, dns.IN, rec.ttl or default_ttl, rec, auth=True)
+ )
+
+ if not results and not authority:
+ # Empty response. Include SOA record to allow clients to cache
+ # this response. RFC 1034, sections 3.7 and 4.3.4, and RFC 2181
+ # section 7.1.
+ authority.append(
+ dns.RRHeader(self.soa[0], dns.SOA, dns.IN, ttl, self.soa[1], auth=True)
+ )
+ return defer.succeed((results, authority, additional))
+ else:
+ if name.lower().endswith(self.soa[0].lower()):
+ # We are the authority and we didn't find it. Goodbye.
+ return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+
+ def lookupZone(self, name, timeout = 10):
+ if self.soa[0].lower() == name.lower():
+ # Wee hee hee hooo yea
+ default_ttl = max(self.soa[1].minimum, self.soa[1].expire)
+ if self.soa[1].ttl is not None:
+ soa_ttl = self.soa[1].ttl
+ else:
+ soa_ttl = default_ttl
+ results = [dns.RRHeader(self.soa[0], dns.SOA, dns.IN, soa_ttl, self.soa[1], auth=True)]
+ for (k, r) in self.records.items():
+ for rec in r:
+ if rec.ttl is not None:
+ ttl = rec.ttl
+ else:
+ ttl = default_ttl
+ if rec.TYPE != dns.SOA:
+ results.append(dns.RRHeader(k, rec.TYPE, dns.IN, ttl, rec, auth=True))
+ results.append(results[0])
+ return defer.succeed((results, (), ()))
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+ def _cbAllRecords(self, results):
+ ans, auth, add = [], [], []
+ for res in results:
+ if res[0]:
+ ans.extend(res[1][0])
+ auth.extend(res[1][1])
+ add.extend(res[1][2])
+ return ans, auth, add
+
+
+class PySourceAuthority(FileAuthority):
+ """A FileAuthority that is built up from Python source code."""
+
+ def loadFile(self, filename):
+ g, l = self.setupConfigNamespace(), {}
+ execfile(filename, g, l)
+ if not l.has_key('zone'):
+ raise ValueError, "No zone defined in " + filename
+
+ self.records = {}
+ for rr in l['zone']:
+ if isinstance(rr[1], dns.Record_SOA):
+ self.soa = rr
+ self.records.setdefault(rr[0].lower(), []).append(rr[1])
+
+
+ def wrapRecord(self, type):
+ return lambda name, *arg, **kw: (name, type(*arg, **kw))
+
+
+ def setupConfigNamespace(self):
+ r = {}
+ items = dns.__dict__.iterkeys()
+ for record in [x for x in items if x.startswith('Record_')]:
+ type = getattr(dns, record)
+ f = self.wrapRecord(type)
+ r[record[len('Record_'):]] = f
+ return r
+
+
+class BindAuthority(FileAuthority):
+ """An Authority that loads BIND configuration files"""
+
+ def loadFile(self, filename):
+ self.origin = os.path.basename(filename) + '.' # XXX - this might suck
+ lines = open(filename).readlines()
+ lines = self.stripComments(lines)
+ lines = self.collapseContinuations(lines)
+ self.parseLines(lines)
+
+
+ def stripComments(self, lines):
+ return [
+ a.find(';') == -1 and a or a[:a.find(';')] for a in [
+ b.strip() for b in lines
+ ]
+ ]
+
+
+ def collapseContinuations(self, lines):
+ L = []
+ state = 0
+ for line in lines:
+ if state == 0:
+ if line.find('(') == -1:
+ L.append(line)
+ else:
+ L.append(line[:line.find('(')])
+ state = 1
+ else:
+ if line.find(')') != -1:
+ L[-1] += ' ' + line[:line.find(')')]
+ state = 0
+ else:
+ L[-1] += ' ' + line
+ lines = L
+ L = []
+ for line in lines:
+ L.append(line.split())
+ return filter(None, L)
+
+
+ def parseLines(self, lines):
+ TTL = 60 * 60 * 3
+ ORIGIN = self.origin
+
+ self.records = {}
+
+ for (line, index) in zip(lines, range(len(lines))):
+ if line[0] == '$TTL':
+ TTL = dns.str2time(line[1])
+ elif line[0] == '$ORIGIN':
+ ORIGIN = line[1]
+ elif line[0] == '$INCLUDE': # XXX - oh, fuck me
+ raise NotImplementedError('$INCLUDE directive not implemented')
+ elif line[0] == '$GENERATE':
+ raise NotImplementedError('$GENERATE directive not implemented')
+ else:
+ self.parseRecordLine(ORIGIN, TTL, line)
+
+
+ def addRecord(self, owner, ttl, type, domain, cls, rdata):
+ if not domain.endswith('.'):
+ domain = domain + '.' + owner
+ else:
+ domain = domain[:-1]
+ f = getattr(self, 'class_%s' % cls, None)
+ if f:
+ f(ttl, type, domain, rdata)
+ else:
+ raise NotImplementedError, "Record class %r not supported" % cls
+
+
+ def class_IN(self, ttl, type, domain, rdata):
+ record = getattr(dns, 'Record_%s' % type, None)
+ if record:
+ r = record(*rdata)
+ r.ttl = ttl
+ self.records.setdefault(domain.lower(), []).append(r)
+
+ print 'Adding IN Record', domain, ttl, r
+ if type == 'SOA':
+ self.soa = (domain, r)
+ else:
+ raise NotImplementedError, "Record type %r not supported" % type
+
+
+ #
+ # This file ends here. Read no further.
+ #
+ def parseRecordLine(self, origin, ttl, line):
+ MARKERS = dns.QUERY_CLASSES.values() + dns.QUERY_TYPES.values()
+ cls = 'IN'
+ owner = origin
+
+ if line[0] == '@':
+ line = line[1:]
+ owner = origin
+# print 'default owner'
+ elif not line[0].isdigit() and line[0] not in MARKERS:
+ owner = line[0]
+ line = line[1:]
+# print 'owner is ', owner
+
+ if line[0].isdigit() or line[0] in MARKERS:
+ domain = owner
+ owner = origin
+# print 'woops, owner is ', owner, ' domain is ', domain
+ else:
+ domain = line[0]
+ line = line[1:]
+# print 'domain is ', domain
+
+ if line[0] in dns.QUERY_CLASSES.values():
+ cls = line[0]
+ line = line[1:]
+# print 'cls is ', cls
+ if line[0].isdigit():
+ ttl = int(line[0])
+ line = line[1:]
+# print 'ttl is ', ttl
+ elif line[0].isdigit():
+ ttl = int(line[0])
+ line = line[1:]
+# print 'ttl is ', ttl
+ if line[0] in dns.QUERY_CLASSES.values():
+ cls = line[0]
+ line = line[1:]
+# print 'cls is ', cls
+
+ type = line[0]
+# print 'type is ', type
+ rdata = line[1:]
+# print 'rdata is ', rdata
+
+ self.addRecord(owner, ttl, type, domain, cls, rdata)
diff --git a/twisted/names/cache.py b/twisted/names/cache.py
new file mode 100644
index 0000000..973a3d9
--- /dev/null
+++ b/twisted/names/cache.py
@@ -0,0 +1,116 @@
+# -*- test-case-name: twisted.names.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+
+from twisted.names import dns, common
+from twisted.python import failure, log
+from twisted.internet import interfaces, defer
+
+
+
+class CacheResolver(common.ResolverBase):
+ """
+ A resolver that serves records from a local, memory cache.
+
+ @ivar _reactor: A provider of L{interfaces.IReactorTime}.
+ """
+
+ implements(interfaces.IResolver)
+
+ cache = None
+
+ def __init__(self, cache=None, verbose=0, reactor=None):
+ common.ResolverBase.__init__(self)
+
+ self.cache = {}
+ self.verbose = verbose
+ self.cancel = {}
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+ if cache:
+ for query, (seconds, payload) in cache.items():
+ self.cacheResult(query, payload, seconds)
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+
+ now = self._reactor.seconds()
+ for (k, (when, (ans, add, ns))) in self.cache.items():
+ diff = now - when
+ for rec in ans + add + ns:
+ if rec.ttl < diff:
+ del self.cache[k]
+ break
+
+
+ def __getstate__(self):
+ for c in self.cancel.values():
+ c.cancel()
+ self.cancel.clear()
+ return self.__dict__
+
+
+ def _lookup(self, name, cls, type, timeout):
+ now = self._reactor.seconds()
+ q = dns.Query(name, type, cls)
+ try:
+ when, (ans, auth, add) = self.cache[q]
+ except KeyError:
+ if self.verbose > 1:
+ log.msg('Cache miss for ' + repr(name))
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+ else:
+ if self.verbose:
+ log.msg('Cache hit for ' + repr(name))
+ diff = now - when
+ return defer.succeed((
+ [dns.RRHeader(str(r.name), r.type, r.cls, max(0, r.ttl - diff), r.payload) for r in ans],
+ [dns.RRHeader(str(r.name), r.type, r.cls, max(0, r.ttl - diff), r.payload) for r in auth],
+ [dns.RRHeader(str(r.name), r.type, r.cls, max(0, r.ttl - diff), r.payload) for r in add]
+ ))
+
+
+ def lookupAllRecords(self, name, timeout = None):
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+
+ def cacheResult(self, query, payload, cacheTime=None):
+ """
+ Cache a DNS entry.
+
+ @param query: a L{dns.Query} instance.
+
+ @param payload: a 3-tuple of lists of L{dns.RRHeader} records, the
+ matching result of the query (answers, authority and additional).
+
+ @param cacheTime: The time (seconds since epoch) at which the entry is
+ considered to have been added to the cache. If C{None} is given,
+ the current time is used.
+ """
+ if self.verbose > 1:
+ log.msg('Adding %r to cache' % query)
+
+ self.cache[query] = (cacheTime or self._reactor.seconds(), payload)
+
+ if self.cancel.has_key(query):
+ self.cancel[query].cancel()
+
+ s = list(payload[0]) + list(payload[1]) + list(payload[2])
+ if s:
+ m = s[0].ttl
+ for r in s:
+ m = min(m, r.ttl)
+ else:
+ m = 0
+
+ self.cancel[query] = self._reactor.callLater(m, self.clearEntry, query)
+
+
+ def clearEntry(self, query):
+ del self.cache[query]
+ del self.cancel[query]
diff --git a/twisted/names/client.py b/twisted/names/client.py
new file mode 100644
index 0000000..a8dd0c5
--- /dev/null
+++ b/twisted/names/client.py
@@ -0,0 +1,955 @@
+# -*- test-case-name: twisted.names.test.test_names -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Asynchronous client DNS
+
+The functions exposed in this module can be used for asynchronous name
+resolution and dns queries.
+
+If you need to create a resolver with specific requirements, such as needing to
+do queries against a particular host, the L{createResolver} function will
+return an C{IResolver}.
+
+Future plans: Proper nameserver acquisition on Windows/MacOS,
+better caching, respect timeouts
+
+@author: Jp Calderone
+"""
+
+import os
+import errno
+import warnings
+
+from zope.interface import implements
+
+# Twisted imports
+from twisted.python.runtime import platform
+from twisted.internet import error, defer, protocol, interfaces
+from twisted.python import log, failure
+from twisted.python.deprecate import getWarningMethod
+from twisted.names import dns, common
+
+
+class Resolver(common.ResolverBase):
+ """
+ @ivar _waiting: A C{dict} mapping tuple keys of query name/type/class to
+ Deferreds which will be called back with the result of those queries.
+ This is used to avoid issuing the same query more than once in
+ parallel. This is more efficient on the network and helps avoid a
+ "birthday paradox" attack by keeping the number of outstanding requests
+ for a particular query fixed at one instead of allowing the attacker to
+ raise it to an arbitrary number.
+
+ @ivar _reactor: A provider of L{IReactorTCP}, L{IReactorUDP}, and
+ L{IReactorTime} which will be used to set up network resources and
+ track timeouts.
+ """
+ implements(interfaces.IResolver)
+
+ index = 0
+ timeout = None
+
+ factory = None
+ servers = None
+ dynServers = ()
+ pending = None
+ connections = None
+
+ resolv = None
+ _lastResolvTime = None
+ _resolvReadInterval = 60
+
+ def _getProtocol(self):
+ getWarningMethod()(
+ "Resolver.protocol is deprecated; use Resolver.queryUDP instead.",
+ PendingDeprecationWarning,
+ stacklevel=0)
+ self.protocol = dns.DNSDatagramProtocol(self)
+ return self.protocol
+ protocol = property(_getProtocol)
+
+
+ def __init__(self, resolv=None, servers=None, timeout=(1, 3, 11, 45), reactor=None):
+ """
+ Construct a resolver which will query domain name servers listed in
+ the C{resolv.conf(5)}-format file given by C{resolv} as well as
+ those in the given C{servers} list. Servers are queried in a
+ round-robin fashion. If given, C{resolv} is periodically checked
+ for modification and re-parsed if it is noticed to have changed.
+
+ @type servers: C{list} of C{(str, int)} or C{None}
+ @param servers: If not None, interpreted as a list of (host, port)
+ pairs specifying addresses of domain name servers to attempt to use
+ for this lookup. Host addresses should be in IPv4 dotted-quad
+ form. If specified, overrides C{resolv}.
+
+ @type resolv: C{str}
+ @param resolv: Filename to read and parse as a resolver(5)
+ configuration file.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Default number of seconds after which to reissue the
+ query. When the last timeout expires, the query is considered
+ failed.
+
+ @param reactor: A provider of L{IReactorTime}, L{IReactorUDP}, and
+ L{IReactorTCP} which will be used to establish connections, listen
+ for DNS datagrams, and enforce timeouts. If not provided, the
+ global reactor will be used.
+
+ @raise ValueError: Raised if no nameserver addresses can be found.
+ """
+ common.ResolverBase.__init__(self)
+
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+ self.timeout = timeout
+
+ if servers is None:
+ self.servers = []
+ else:
+ self.servers = servers
+
+ self.resolv = resolv
+
+ if not len(self.servers) and not resolv:
+ raise ValueError, "No nameservers specified"
+
+ self.factory = DNSClientFactory(self, timeout)
+ self.factory.noisy = 0 # Be quiet by default
+
+ self.connections = []
+ self.pending = []
+
+ self._waiting = {}
+
+ self.maybeParseConfig()
+
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ d['connections'] = []
+ d['_parseCall'] = None
+ return d
+
+
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+ self.maybeParseConfig()
+
+
+ def maybeParseConfig(self):
+ if self.resolv is None:
+ # Don't try to parse it, don't set up a call loop
+ return
+
+ try:
+ resolvConf = file(self.resolv)
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ # Missing resolv.conf is treated the same as an empty resolv.conf
+ self.parseConfig(())
+ else:
+ raise
+ else:
+ mtime = os.fstat(resolvConf.fileno()).st_mtime
+ if mtime != self._lastResolvTime:
+ log.msg('%s changed, reparsing' % (self.resolv,))
+ self._lastResolvTime = mtime
+ self.parseConfig(resolvConf)
+
+ # Check again in a little while
+ self._parseCall = self._reactor.callLater(
+ self._resolvReadInterval, self.maybeParseConfig)
+
+
+ def parseConfig(self, resolvConf):
+ servers = []
+ for L in resolvConf:
+ L = L.strip()
+ if L.startswith('nameserver'):
+ resolver = (L.split()[1], dns.PORT)
+ servers.append(resolver)
+ log.msg("Resolver added %r to server list" % (resolver,))
+ elif L.startswith('domain'):
+ try:
+ self.domain = L.split()[1]
+ except IndexError:
+ self.domain = ''
+ self.search = None
+ elif L.startswith('search'):
+ try:
+ self.search = L.split()[1:]
+ except IndexError:
+ self.search = ''
+ self.domain = None
+ if not servers:
+ servers.append(('127.0.0.1', dns.PORT))
+ self.dynServers = servers
+
+
+ def pickServer(self):
+ """
+ Return the address of a nameserver.
+
+ TODO: Weight servers for response time so faster ones can be
+ preferred.
+ """
+ if not self.servers and not self.dynServers:
+ return None
+ serverL = len(self.servers)
+ dynL = len(self.dynServers)
+
+ self.index += 1
+ self.index %= (serverL + dynL)
+ if self.index < serverL:
+ return self.servers[self.index]
+ else:
+ return self.dynServers[self.index - serverL]
+
+
+ def _connectedProtocol(self):
+ """
+ Return a new L{DNSDatagramProtocol} bound to a randomly selected port
+ number.
+ """
+ if 'protocol' in self.__dict__:
+ # Some code previously asked for or set the deprecated `protocol`
+ # attribute, so it probably expects that object to be used for
+ # queries. Give it back and skip the super awesome source port
+ # randomization logic. This is actually a really good reason to
+ # remove this deprecated backward compatibility as soon as
+ # possible. -exarkun
+ return self.protocol
+ proto = dns.DNSDatagramProtocol(self)
+ while True:
+ try:
+ self._reactor.listenUDP(dns.randomSource(), proto)
+ except error.CannotListenError:
+ pass
+ else:
+ return proto
+
+
+ def connectionMade(self, protocol):
+ """
+ Called by associated L{dns.DNSProtocol} instances when they connect.
+ """
+ self.connections.append(protocol)
+ for (d, q, t) in self.pending:
+ self.queryTCP(q, t).chainDeferred(d)
+ del self.pending[:]
+
+
+ def connectionLost(self, protocol):
+ """
+ Called by associated L{dns.DNSProtocol} instances when they disconnect.
+ """
+ if protocol in self.connections:
+ self.connections.remove(protocol)
+
+
+ def messageReceived(self, message, protocol, address = None):
+ log.msg("Unexpected message (%d) received from %r" % (message.id, address))
+
+
+ def _query(self, *args):
+ """
+ Get a new L{DNSDatagramProtocol} instance from L{_connectedProtocol},
+ issue a query to it using C{*args}, and arrange for it to be
+ disconnected from its transport after the query completes.
+
+ @param *args: Positional arguments to be passed to
+ L{DNSDatagramProtocol.query}.
+
+ @return: A L{Deferred} which will be called back with the result of the
+ query.
+ """
+ protocol = self._connectedProtocol()
+ d = protocol.query(*args)
+ def cbQueried(result):
+ protocol.transport.stopListening()
+ return result
+ d.addBoth(cbQueried)
+ return d
+
+
+ def queryUDP(self, queries, timeout = None):
+ """
+ Make a number of DNS queries via UDP.
+
+ @type queries: A C{list} of C{dns.Query} instances
+ @param queries: The queries to make.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ @raise C{twisted.internet.defer.TimeoutError}: When the query times
+ out.
+ """
+ if timeout is None:
+ timeout = self.timeout
+
+ addresses = self.servers + list(self.dynServers)
+ if not addresses:
+ return defer.fail(IOError("No domain name servers available"))
+
+ # Make sure we go through servers in the list in the order they were
+ # specified.
+ addresses.reverse()
+
+ used = addresses.pop()
+ d = self._query(used, queries, timeout[0])
+ d.addErrback(self._reissue, addresses, [used], queries, timeout)
+ return d
+
+
+ def _reissue(self, reason, addressesLeft, addressesUsed, query, timeout):
+ reason.trap(dns.DNSQueryTimeoutError)
+
+ # If there are no servers left to be tried, adjust the timeout
+ # to the next longest timeout period and move all the
+ # "used" addresses back to the list of addresses to try.
+ if not addressesLeft:
+ addressesLeft = addressesUsed
+ addressesLeft.reverse()
+ addressesUsed = []
+ timeout = timeout[1:]
+
+ # If all timeout values have been used this query has failed. Tell the
+ # protocol we're giving up on it and return a terminal timeout failure
+ # to our caller.
+ if not timeout:
+ return failure.Failure(defer.TimeoutError(query))
+
+ # Get an address to try. Take it out of the list of addresses
+ # to try and put it ino the list of already tried addresses.
+ address = addressesLeft.pop()
+ addressesUsed.append(address)
+
+ # Issue a query to a server. Use the current timeout. Add this
+ # function as a timeout errback in case another retry is required.
+ d = self._query(address, query, timeout[0], reason.value.id)
+ d.addErrback(self._reissue, addressesLeft, addressesUsed, query, timeout)
+ return d
+
+
+ def queryTCP(self, queries, timeout = 10):
+ """
+ Make a number of DNS queries via TCP.
+
+ @type queries: Any non-zero number of C{dns.Query} instances
+ @param queries: The queries to make.
+
+ @type timeout: C{int}
+ @param timeout: The number of seconds after which to fail.
+
+ @rtype: C{Deferred}
+ """
+ if not len(self.connections):
+ address = self.pickServer()
+ if address is None:
+ return defer.fail(IOError("No domain name servers available"))
+ host, port = address
+ self._reactor.connectTCP(host, port, self.factory)
+ self.pending.append((defer.Deferred(), queries, timeout))
+ return self.pending[-1][0]
+ else:
+ return self.connections[0].query(queries, timeout)
+
+
+ def filterAnswers(self, message):
+ """
+ Extract results from the given message.
+
+ If the message was truncated, re-attempt the query over TCP and return
+ a Deferred which will fire with the results of that query.
+
+ If the message's result code is not L{dns.OK}, return a Failure
+ indicating the type of error which occurred.
+
+ Otherwise, return a three-tuple of lists containing the results from
+ the answers section, the authority section, and the additional section.
+ """
+ if message.trunc:
+ return self.queryTCP(message.queries).addCallback(self.filterAnswers)
+ if message.rCode != dns.OK:
+ return failure.Failure(self.exceptionForCode(message.rCode)(message))
+ return (message.answers, message.authority, message.additional)
+
+
+ def _lookup(self, name, cls, type, timeout):
+ """
+ Build a L{dns.Query} for the given parameters and dispatch it via UDP.
+
+ If this query is already outstanding, it will not be re-issued.
+ Instead, when the outstanding query receives a response, that response
+ will be re-used for this query as well.
+
+ @type name: C{str}
+ @type type: C{int}
+ @type cls: C{int}
+
+ @return: A L{Deferred} which fires with a three-tuple giving the
+ answer, authority, and additional sections of the response or with
+ a L{Failure} if the response code is anything other than C{dns.OK}.
+ """
+ key = (name, type, cls)
+ waiting = self._waiting.get(key)
+ if waiting is None:
+ self._waiting[key] = []
+ d = self.queryUDP([dns.Query(name, type, cls)], timeout)
+ def cbResult(result):
+ for d in self._waiting.pop(key):
+ d.callback(result)
+ return result
+ d.addCallback(self.filterAnswers)
+ d.addBoth(cbResult)
+ else:
+ d = defer.Deferred()
+ waiting.append(d)
+ return d
+
+
+ # This one doesn't ever belong on UDP
+ def lookupZone(self, name, timeout = 10):
+ """
+ Perform an AXFR request. This is quite different from usual
+ DNS requests. See http://cr.yp.to/djbdns/axfr-notes.html for
+ more information.
+ """
+ address = self.pickServer()
+ if address is None:
+ return defer.fail(IOError('No domain name servers available'))
+ host, port = address
+ d = defer.Deferred()
+ controller = AXFRController(name, d)
+ factory = DNSClientFactory(controller, timeout)
+ factory.noisy = False #stfu
+
+ connector = self._reactor.connectTCP(host, port, factory)
+ controller.timeoutCall = self._reactor.callLater(
+ timeout or 10, self._timeoutZone, d, controller,
+ connector, timeout or 10)
+ return d.addCallback(self._cbLookupZone, connector)
+
+ def _timeoutZone(self, d, controller, connector, seconds):
+ connector.disconnect()
+ controller.timeoutCall = None
+ controller.deferred = None
+ d.errback(error.TimeoutError("Zone lookup timed out after %d seconds" % (seconds,)))
+
+ def _cbLookupZone(self, result, connector):
+ connector.disconnect()
+ return (result, [], [])
+
+
+class AXFRController:
+ timeoutCall = None
+
+ def __init__(self, name, deferred):
+ self.name = name
+ self.deferred = deferred
+ self.soa = None
+ self.records = []
+
+ def connectionMade(self, protocol):
+ # dig saids recursion-desired to 0, so I will too
+ message = dns.Message(protocol.pickID(), recDes=0)
+ message.queries = [dns.Query(self.name, dns.AXFR, dns.IN)]
+ protocol.writeMessage(message)
+
+
+ def connectionLost(self, protocol):
+ # XXX Do something here - see #3428
+ pass
+
+
+ def messageReceived(self, message, protocol):
+ # Caveat: We have to handle two cases: All records are in 1
+ # message, or all records are in N messages.
+
+ # According to http://cr.yp.to/djbdns/axfr-notes.html,
+ # 'authority' and 'additional' are always empty, and only
+ # 'answers' is present.
+ self.records.extend(message.answers)
+ if not self.records:
+ return
+ if not self.soa:
+ if self.records[0].type == dns.SOA:
+ #print "first SOA!"
+ self.soa = self.records[0]
+ if len(self.records) > 1 and self.records[-1].type == dns.SOA:
+ #print "It's the second SOA! We're done."
+ if self.timeoutCall is not None:
+ self.timeoutCall.cancel()
+ self.timeoutCall = None
+ if self.deferred is not None:
+ self.deferred.callback(self.records)
+ self.deferred = None
+
+
+
+from twisted.internet.base import ThreadedResolver as _ThreadedResolverImpl
+
+class ThreadedResolver(_ThreadedResolverImpl):
+ def __init__(self, reactor=None):
+ if reactor is None:
+ from twisted.internet import reactor
+ _ThreadedResolverImpl.__init__(self, reactor)
+ warnings.warn(
+ "twisted.names.client.ThreadedResolver is deprecated since "
+ "Twisted 9.0, use twisted.internet.base.ThreadedResolver "
+ "instead.",
+ category=DeprecationWarning, stacklevel=2)
+
+class DNSClientFactory(protocol.ClientFactory):
+ def __init__(self, controller, timeout = 10):
+ self.controller = controller
+ self.timeout = timeout
+
+
+ def clientConnectionLost(self, connector, reason):
+ pass
+
+
+ def buildProtocol(self, addr):
+ p = dns.DNSProtocol(self.controller)
+ p.factory = self
+ return p
+
+
+
+def createResolver(servers=None, resolvconf=None, hosts=None):
+ """
+ Create and return a Resolver.
+
+ @type servers: C{list} of C{(str, int)} or C{None}
+
+ @param servers: If not C{None}, interpreted as a list of domain name servers
+ to attempt to use. Each server is a tuple of address in C{str} dotted-quad
+ form and C{int} port number.
+
+ @type resolvconf: C{str} or C{None}
+ @param resolvconf: If not C{None}, on posix systems will be interpreted as
+ an alternate resolv.conf to use. Will do nothing on windows systems. If
+ C{None}, /etc/resolv.conf will be used.
+
+ @type hosts: C{str} or C{None}
+ @param hosts: If not C{None}, an alternate hosts file to use. If C{None}
+ on posix systems, /etc/hosts will be used. On windows, C:\windows\hosts
+ will be used.
+
+ @rtype: C{IResolver}
+ """
+ from twisted.names import resolve, cache, root, hosts as hostsModule
+ if platform.getType() == 'posix':
+ if resolvconf is None:
+ resolvconf = '/etc/resolv.conf'
+ if hosts is None:
+ hosts = '/etc/hosts'
+ theResolver = Resolver(resolvconf, servers)
+ hostResolver = hostsModule.Resolver(hosts)
+ else:
+ if hosts is None:
+ hosts = r'c:\windows\hosts'
+ from twisted.internet import reactor
+ bootstrap = _ThreadedResolverImpl(reactor)
+ hostResolver = hostsModule.Resolver(hosts)
+ theResolver = root.bootstrap(bootstrap)
+
+ L = [hostResolver, cache.CacheResolver(), theResolver]
+ return resolve.ResolverChain(L)
+
+theResolver = None
+def getResolver():
+ """
+ Get a Resolver instance.
+
+ Create twisted.names.client.theResolver if it is C{None}, and then return
+ that value.
+
+ @rtype: C{IResolver}
+ """
+ global theResolver
+ if theResolver is None:
+ try:
+ theResolver = createResolver()
+ except ValueError:
+ theResolver = createResolver(servers=[('127.0.0.1', 53)])
+ return theResolver
+
+def getHostByName(name, timeout=None, effort=10):
+ """
+ Resolve a name to a valid ipv4 or ipv6 address.
+
+ Will errback with C{DNSQueryTimeoutError} on a timeout, C{DomainError} or
+ C{AuthoritativeDomainError} (or subclasses) on other errors.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @type effort: C{int}
+ @param effort: How many times CNAME and NS records to follow while
+ resolving this name.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().getHostByName(name, timeout, effort)
+
+def lookupAddress(name, timeout=None):
+ """
+ Perform an A record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupAddress(name, timeout)
+
+def lookupIPV6Address(name, timeout=None):
+ """
+ Perform an AAAA record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupIPV6Address(name, timeout)
+
+def lookupAddress6(name, timeout=None):
+ """
+ Perform an A6 record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupAddress6(name, timeout)
+
+def lookupMailExchange(name, timeout=None):
+ """
+ Perform an MX record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupMailExchange(name, timeout)
+
+def lookupNameservers(name, timeout=None):
+ """
+ Perform an NS record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupNameservers(name, timeout)
+
+def lookupCanonicalName(name, timeout=None):
+ """
+ Perform a CNAME record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupCanonicalName(name, timeout)
+
+def lookupMailBox(name, timeout=None):
+ """
+ Perform an MB record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupMailBox(name, timeout)
+
+def lookupMailGroup(name, timeout=None):
+ """
+ Perform an MG record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupMailGroup(name, timeout)
+
+def lookupMailRename(name, timeout=None):
+ """
+ Perform an MR record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupMailRename(name, timeout)
+
+def lookupPointer(name, timeout=None):
+ """
+ Perform a PTR record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupPointer(name, timeout)
+
+def lookupAuthority(name, timeout=None):
+ """
+ Perform an SOA record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupAuthority(name, timeout)
+
+def lookupNull(name, timeout=None):
+ """
+ Perform a NULL record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupNull(name, timeout)
+
+def lookupWellKnownServices(name, timeout=None):
+ """
+ Perform a WKS record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupWellKnownServices(name, timeout)
+
+def lookupService(name, timeout=None):
+ """
+ Perform an SRV record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupService(name, timeout)
+
+def lookupHostInfo(name, timeout=None):
+ """
+ Perform a HINFO record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupHostInfo(name, timeout)
+
+def lookupMailboxInfo(name, timeout=None):
+ """
+ Perform an MINFO record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupMailboxInfo(name, timeout)
+
+def lookupText(name, timeout=None):
+ """
+ Perform a TXT record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupText(name, timeout)
+
+def lookupSenderPolicy(name, timeout=None):
+ """
+ Perform a SPF record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupSenderPolicy(name, timeout)
+
+def lookupResponsibility(name, timeout=None):
+ """
+ Perform an RP record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupResponsibility(name, timeout)
+
+def lookupAFSDatabase(name, timeout=None):
+ """
+ Perform an AFSDB record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupAFSDatabase(name, timeout)
+
+def lookupZone(name, timeout=None):
+ """
+ Perform an AXFR record lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: C{int}
+ @param timeout: When this timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ # XXX: timeout here is not a list of ints, it is a single int.
+ return getResolver().lookupZone(name, timeout)
+
+def lookupAllRecords(name, timeout=None):
+ """
+ ALL_RECORD lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupAllRecords(name, timeout)
+
+
+
+def lookupNamingAuthorityPointer(name, timeout=None):
+ """
+ NAPTR lookup.
+
+ @type name: C{str}
+ @param name: DNS name to resolve.
+
+ @type timeout: Sequence of C{int}
+ @param timeout: Number of seconds after which to reissue the query.
+ When the last timeout expires, the query is considered failed.
+
+ @rtype: C{Deferred}
+ """
+ return getResolver().lookupNamingAuthorityPointer(name, timeout)
diff --git a/twisted/names/common.py b/twisted/names/common.py
new file mode 100644
index 0000000..440913e
--- /dev/null
+++ b/twisted/names/common.py
@@ -0,0 +1,278 @@
+# -*- test-case-name: twisted.names.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Base functionality useful to various parts of Twisted Names.
+"""
+
+import socket
+
+from twisted.names import dns
+from twisted.names.error import DNSFormatError, DNSServerError, DNSNameError
+from twisted.names.error import DNSNotImplementedError, DNSQueryRefusedError
+from twisted.names.error import DNSUnknownError
+
+from twisted.internet import defer, error
+from twisted.python import failure
+
+EMPTY_RESULT = (), (), ()
+
+class ResolverBase:
+ """
+ L{ResolverBase} is a base class for L{IResolver} implementations which
+ deals with a lot of the boilerplate of implementing all of the lookup
+ methods.
+
+ @cvar _errormap: A C{dict} mapping DNS protocol failure response codes
+ to exception classes which will be used to represent those failures.
+ """
+ _errormap = {
+ dns.EFORMAT: DNSFormatError,
+ dns.ESERVER: DNSServerError,
+ dns.ENAME: DNSNameError,
+ dns.ENOTIMP: DNSNotImplementedError,
+ dns.EREFUSED: DNSQueryRefusedError}
+
+ typeToMethod = None
+
+ def __init__(self):
+ self.typeToMethod = {}
+ for (k, v) in typeToMethod.items():
+ self.typeToMethod[k] = getattr(self, v)
+
+
+ def exceptionForCode(self, responseCode):
+ """
+ Convert a response code (one of the possible values of
+ L{dns.Message.rCode} to an exception instance representing it.
+
+ @since: 10.0
+ """
+ return self._errormap.get(responseCode, DNSUnknownError)
+
+
+ def query(self, query, timeout = None):
+ try:
+ return self.typeToMethod[query.type](str(query.name), timeout)
+ except KeyError, e:
+ return defer.fail(failure.Failure(NotImplementedError(str(self.__class__) + " " + str(query.type))))
+
+ def _lookup(self, name, cls, type, timeout):
+ return defer.fail(NotImplementedError("ResolverBase._lookup"))
+
+ def lookupAddress(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupAddress
+ """
+ return self._lookup(name, dns.IN, dns.A, timeout)
+
+ def lookupIPV6Address(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupIPV6Address
+ """
+ return self._lookup(name, dns.IN, dns.AAAA, timeout)
+
+ def lookupAddress6(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupAddress6
+ """
+ return self._lookup(name, dns.IN, dns.A6, timeout)
+
+ def lookupMailExchange(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupMailExchange
+ """
+ return self._lookup(name, dns.IN, dns.MX, timeout)
+
+ def lookupNameservers(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupNameservers
+ """
+ return self._lookup(name, dns.IN, dns.NS, timeout)
+
+ def lookupCanonicalName(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupCanonicalName
+ """
+ return self._lookup(name, dns.IN, dns.CNAME, timeout)
+
+ def lookupMailBox(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupMailBox
+ """
+ return self._lookup(name, dns.IN, dns.MB, timeout)
+
+ def lookupMailGroup(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupMailGroup
+ """
+ return self._lookup(name, dns.IN, dns.MG, timeout)
+
+ def lookupMailRename(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupMailRename
+ """
+ return self._lookup(name, dns.IN, dns.MR, timeout)
+
+ def lookupPointer(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupPointer
+ """
+ return self._lookup(name, dns.IN, dns.PTR, timeout)
+
+ def lookupAuthority(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupAuthority
+ """
+ return self._lookup(name, dns.IN, dns.SOA, timeout)
+
+ def lookupNull(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupNull
+ """
+ return self._lookup(name, dns.IN, dns.NULL, timeout)
+
+ def lookupWellKnownServices(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupWellKnownServices
+ """
+ return self._lookup(name, dns.IN, dns.WKS, timeout)
+
+ def lookupService(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupService
+ """
+ return self._lookup(name, dns.IN, dns.SRV, timeout)
+
+ def lookupHostInfo(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupHostInfo
+ """
+ return self._lookup(name, dns.IN, dns.HINFO, timeout)
+
+ def lookupMailboxInfo(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupMailboxInfo
+ """
+ return self._lookup(name, dns.IN, dns.MINFO, timeout)
+
+ def lookupText(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupText
+ """
+ return self._lookup(name, dns.IN, dns.TXT, timeout)
+
+ def lookupSenderPolicy(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupSenderPolicy
+ """
+ return self._lookup(name, dns.IN, dns.SPF, timeout)
+
+ def lookupResponsibility(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupResponsibility
+ """
+ return self._lookup(name, dns.IN, dns.RP, timeout)
+
+ def lookupAFSDatabase(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupAFSDatabase
+ """
+ return self._lookup(name, dns.IN, dns.AFSDB, timeout)
+
+ def lookupZone(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupZone
+ """
+ return self._lookup(name, dns.IN, dns.AXFR, timeout)
+
+
+ def lookupNamingAuthorityPointer(self, name, timeout=None):
+ """
+ @see: twisted.names.client.lookupNamingAuthorityPointer
+ """
+ return self._lookup(name, dns.IN, dns.NAPTR, timeout)
+
+
+ def lookupAllRecords(self, name, timeout = None):
+ """
+ @see: twisted.names.client.lookupAllRecords
+ """
+ return self._lookup(name, dns.IN, dns.ALL_RECORDS, timeout)
+
+ def getHostByName(self, name, timeout = None, effort = 10):
+ """
+ @see: twisted.names.client.getHostByName
+ """
+ # XXX - respect timeout
+ return self.lookupAllRecords(name, timeout
+ ).addCallback(self._cbRecords, name, effort
+ )
+
+ def _cbRecords(self, (ans, auth, add), name, effort):
+ result = extractRecord(self, dns.Name(name), ans + auth + add, effort)
+ if not result:
+ raise error.DNSLookupError(name)
+ return result
+
+
+def extractRecord(resolver, name, answers, level=10):
+ if not level:
+ return None
+ if hasattr(socket, 'inet_ntop'):
+ for r in answers:
+ if r.name == name and r.type == dns.A6:
+ return socket.inet_ntop(socket.AF_INET6, r.payload.address)
+ for r in answers:
+ if r.name == name and r.type == dns.AAAA:
+ return socket.inet_ntop(socket.AF_INET6, r.payload.address)
+ for r in answers:
+ if r.name == name and r.type == dns.A:
+ return socket.inet_ntop(socket.AF_INET, r.payload.address)
+ for r in answers:
+ if r.name == name and r.type == dns.CNAME:
+ result = extractRecord(
+ resolver, r.payload.name, answers, level - 1)
+ if not result:
+ return resolver.getHostByName(
+ str(r.payload.name), effort=level - 1)
+ return result
+ # No answers, but maybe there's a hint at who we should be asking about
+ # this
+ for r in answers:
+ if r.type == dns.NS:
+ from twisted.names import client
+ r = client.Resolver(servers=[(str(r.payload.name), dns.PORT)])
+ return r.lookupAddress(str(name)
+ ).addCallback(
+ lambda (ans, auth, add):
+ extractRecord(r, name, ans + auth + add, level - 1))
+
+
+typeToMethod = {
+ dns.A: 'lookupAddress',
+ dns.AAAA: 'lookupIPV6Address',
+ dns.A6: 'lookupAddress6',
+ dns.NS: 'lookupNameservers',
+ dns.CNAME: 'lookupCanonicalName',
+ dns.SOA: 'lookupAuthority',
+ dns.MB: 'lookupMailBox',
+ dns.MG: 'lookupMailGroup',
+ dns.MR: 'lookupMailRename',
+ dns.NULL: 'lookupNull',
+ dns.WKS: 'lookupWellKnownServices',
+ dns.PTR: 'lookupPointer',
+ dns.HINFO: 'lookupHostInfo',
+ dns.MINFO: 'lookupMailboxInfo',
+ dns.MX: 'lookupMailExchange',
+ dns.TXT: 'lookupText',
+ dns.SPF: 'lookupSenderPolicy',
+
+ dns.RP: 'lookupResponsibility',
+ dns.AFSDB: 'lookupAFSDatabase',
+ dns.SRV: 'lookupService',
+ dns.NAPTR: 'lookupNamingAuthorityPointer',
+ dns.AXFR: 'lookupZone',
+ dns.ALL_RECORDS: 'lookupAllRecords',
+}
diff --git a/twisted/names/dns.py b/twisted/names/dns.py
new file mode 100644
index 0000000..0ba118a
--- /dev/null
+++ b/twisted/names/dns.py
@@ -0,0 +1,1949 @@
+# -*- test-case-name: twisted.names.test.test_dns -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+DNS protocol implementation.
+
+Future Plans:
+ - Get rid of some toplevels, maybe.
+
+@author: Moshe Zadka
+@author: Jean-Paul Calderone
+"""
+
+__all__ = [
+ 'IEncodable', 'IRecord',
+
+ 'A', 'A6', 'AAAA', 'AFSDB', 'CNAME', 'DNAME', 'HINFO',
+ 'MAILA', 'MAILB', 'MB', 'MD', 'MF', 'MG', 'MINFO', 'MR', 'MX',
+ 'NAPTR', 'NS', 'NULL', 'PTR', 'RP', 'SOA', 'SPF', 'SRV', 'TXT', 'WKS',
+
+ 'ANY', 'CH', 'CS', 'HS', 'IN',
+
+ 'ALL_RECORDS', 'AXFR', 'IXFR',
+
+ 'EFORMAT', 'ENAME', 'ENOTIMP', 'EREFUSED', 'ESERVER',
+
+ 'Record_A', 'Record_A6', 'Record_AAAA', 'Record_AFSDB', 'Record_CNAME',
+ 'Record_DNAME', 'Record_HINFO', 'Record_MB', 'Record_MD', 'Record_MF',
+ 'Record_MG', 'Record_MINFO', 'Record_MR', 'Record_MX', 'Record_NAPTR',
+ 'Record_NS', 'Record_NULL', 'Record_PTR', 'Record_RP', 'Record_SOA',
+ 'Record_SPF', 'Record_SRV', 'Record_TXT', 'Record_WKS', 'UnknownRecord',
+
+ 'QUERY_CLASSES', 'QUERY_TYPES', 'REV_CLASSES', 'REV_TYPES', 'EXT_QUERIES',
+
+ 'Charstr', 'Message', 'Name', 'Query', 'RRHeader', 'SimpleRecord',
+ 'DNSDatagramProtocol', 'DNSMixin', 'DNSProtocol',
+
+ 'OK', 'OP_INVERSE', 'OP_NOTIFY', 'OP_QUERY', 'OP_STATUS', 'OP_UPDATE',
+ 'PORT',
+
+ 'AuthoritativeDomainError', 'DNSQueryTimeoutError', 'DomainError',
+ ]
+
+
+# System imports
+import warnings
+
+import struct, random, types, socket
+
+import cStringIO as StringIO
+
+AF_INET6 = socket.AF_INET6
+
+from zope.interface import implements, Interface, Attribute
+
+
+# Twisted imports
+from twisted.internet import protocol, defer
+from twisted.internet.error import CannotListenError
+from twisted.python import log, failure
+from twisted.python import util as tputil
+from twisted.python import randbytes
+
+
+def randomSource():
+ """
+ Wrapper around L{randbytes.secureRandom} to return 2 random chars.
+ """
+ return struct.unpack('H', randbytes.secureRandom(2, fallback=True))[0]
+
+
+PORT = 53
+
+(A, NS, MD, MF, CNAME, SOA, MB, MG, MR, NULL, WKS, PTR, HINFO, MINFO, MX, TXT,
+ RP, AFSDB) = range(1, 19)
+AAAA = 28
+SRV = 33
+NAPTR = 35
+A6 = 38
+DNAME = 39
+SPF = 99
+
+QUERY_TYPES = {
+ A: 'A',
+ NS: 'NS',
+ MD: 'MD',
+ MF: 'MF',
+ CNAME: 'CNAME',
+ SOA: 'SOA',
+ MB: 'MB',
+ MG: 'MG',
+ MR: 'MR',
+ NULL: 'NULL',
+ WKS: 'WKS',
+ PTR: 'PTR',
+ HINFO: 'HINFO',
+ MINFO: 'MINFO',
+ MX: 'MX',
+ TXT: 'TXT',
+ RP: 'RP',
+ AFSDB: 'AFSDB',
+
+ # 19 through 27? Eh, I'll get to 'em.
+
+ AAAA: 'AAAA',
+ SRV: 'SRV',
+ NAPTR: 'NAPTR',
+ A6: 'A6',
+ DNAME: 'DNAME',
+ SPF: 'SPF'
+}
+
+IXFR, AXFR, MAILB, MAILA, ALL_RECORDS = range(251, 256)
+
+# "Extended" queries (Hey, half of these are deprecated, good job)
+EXT_QUERIES = {
+ IXFR: 'IXFR',
+ AXFR: 'AXFR',
+ MAILB: 'MAILB',
+ MAILA: 'MAILA',
+ ALL_RECORDS: 'ALL_RECORDS'
+}
+
+REV_TYPES = dict([
+ (v, k) for (k, v) in QUERY_TYPES.items() + EXT_QUERIES.items()
+])
+
+IN, CS, CH, HS = range(1, 5)
+ANY = 255
+
+QUERY_CLASSES = {
+ IN: 'IN',
+ CS: 'CS',
+ CH: 'CH',
+ HS: 'HS',
+ ANY: 'ANY'
+}
+REV_CLASSES = dict([
+ (v, k) for (k, v) in QUERY_CLASSES.items()
+])
+
+
+# Opcodes
+OP_QUERY, OP_INVERSE, OP_STATUS = range(3)
+OP_NOTIFY = 4 # RFC 1996
+OP_UPDATE = 5 # RFC 2136
+
+
+# Response Codes
+OK, EFORMAT, ESERVER, ENAME, ENOTIMP, EREFUSED = range(6)
+
+class IRecord(Interface):
+ """
+ An single entry in a zone of authority.
+ """
+
+ TYPE = Attribute("An indicator of what kind of record this is.")
+
+
+# Backwards compatibility aliases - these should be deprecated or something I
+# suppose. -exarkun
+from twisted.names.error import DomainError, AuthoritativeDomainError
+from twisted.names.error import DNSQueryTimeoutError
+
+
+def str2time(s):
+ suffixes = (
+ ('S', 1), ('M', 60), ('H', 60 * 60), ('D', 60 * 60 * 24),
+ ('W', 60 * 60 * 24 * 7), ('Y', 60 * 60 * 24 * 365)
+ )
+ if isinstance(s, types.StringType):
+ s = s.upper().strip()
+ for (suff, mult) in suffixes:
+ if s.endswith(suff):
+ return int(float(s[:-1]) * mult)
+ try:
+ s = int(s)
+ except ValueError:
+ raise ValueError, "Invalid time interval specifier: " + s
+ return s
+
+
+def readPrecisely(file, l):
+ buff = file.read(l)
+ if len(buff) < l:
+ raise EOFError
+ return buff
+
+
+class IEncodable(Interface):
+ """
+ Interface for something which can be encoded to and decoded
+ from a file object.
+ """
+
+ def encode(strio, compDict = None):
+ """
+ Write a representation of this object to the given
+ file object.
+
+ @type strio: File-like object
+ @param strio: The stream to which to write bytes
+
+ @type compDict: C{dict} or C{None}
+ @param compDict: A dictionary of backreference addresses that have
+ have already been written to this stream and that may be used for
+ compression.
+ """
+
+ def decode(strio, length = None):
+ """
+ Reconstruct an object from data read from the given
+ file object.
+
+ @type strio: File-like object
+ @param strio: The stream from which bytes may be read
+
+ @type length: C{int} or C{None}
+ @param length: The number of bytes in this RDATA field. Most
+ implementations can ignore this value. Only in the case of
+ records similar to TXT where the total length is in no way
+ encoded in the data is it necessary.
+ """
+
+
+
+class Charstr(object):
+ implements(IEncodable)
+
+ def __init__(self, string=''):
+ if not isinstance(string, str):
+ raise ValueError("%r is not a string" % (string,))
+ self.string = string
+
+
+ def encode(self, strio, compDict=None):
+ """
+ Encode this Character string into the appropriate byte format.
+
+ @type strio: file
+ @param strio: The byte representation of this Charstr will be written
+ to this file.
+ """
+ string = self.string
+ ind = len(string)
+ strio.write(chr(ind))
+ strio.write(string)
+
+
+ def decode(self, strio, length=None):
+ """
+ Decode a byte string into this Name.
+
+ @type strio: file
+ @param strio: Bytes will be read from this file until the full string
+ is decoded.
+
+ @raise EOFError: Raised when there are not enough bytes available from
+ C{strio}.
+ """
+ self.string = ''
+ l = ord(readPrecisely(strio, 1))
+ self.string = readPrecisely(strio, l)
+
+
+ def __eq__(self, other):
+ if isinstance(other, Charstr):
+ return self.string == other.string
+ return False
+
+
+ def __hash__(self):
+ return hash(self.string)
+
+
+ def __str__(self):
+ return self.string
+
+
+
+class Name:
+ implements(IEncodable)
+
+ def __init__(self, name=''):
+ assert isinstance(name, types.StringTypes), "%r is not a string" % (name,)
+ self.name = name
+
+ def encode(self, strio, compDict=None):
+ """
+ Encode this Name into the appropriate byte format.
+
+ @type strio: file
+ @param strio: The byte representation of this Name will be written to
+ this file.
+
+ @type compDict: dict
+ @param compDict: dictionary of Names that have already been encoded
+ and whose addresses may be backreferenced by this Name (for the purpose
+ of reducing the message size).
+ """
+ name = self.name
+ while name:
+ if compDict is not None:
+ if name in compDict:
+ strio.write(
+ struct.pack("!H", 0xc000 | compDict[name]))
+ return
+ else:
+ compDict[name] = strio.tell() + Message.headerSize
+ ind = name.find('.')
+ if ind > 0:
+ label, name = name[:ind], name[ind + 1:]
+ else:
+ label, name = name, ''
+ ind = len(label)
+ strio.write(chr(ind))
+ strio.write(label)
+ strio.write(chr(0))
+
+
+ def decode(self, strio, length=None):
+ """
+ Decode a byte string into this Name.
+
+ @type strio: file
+ @param strio: Bytes will be read from this file until the full Name
+ is decoded.
+
+ @raise EOFError: Raised when there are not enough bytes available
+ from C{strio}.
+
+ @raise ValueError: Raised when the name cannot be decoded (for example,
+ because it contains a loop).
+ """
+ visited = set()
+ self.name = ''
+ off = 0
+ while 1:
+ l = ord(readPrecisely(strio, 1))
+ if l == 0:
+ if off > 0:
+ strio.seek(off)
+ return
+ if (l >> 6) == 3:
+ new_off = ((l&63) << 8
+ | ord(readPrecisely(strio, 1)))
+ if new_off in visited:
+ raise ValueError("Compression loop in encoded name")
+ visited.add(new_off)
+ if off == 0:
+ off = strio.tell()
+ strio.seek(new_off)
+ continue
+ label = readPrecisely(strio, l)
+ if self.name == '':
+ self.name = label
+ else:
+ self.name = self.name + '.' + label
+
+ def __eq__(self, other):
+ if isinstance(other, Name):
+ return str(self) == str(other)
+ return 0
+
+
+ def __hash__(self):
+ return hash(str(self))
+
+
+ def __str__(self):
+ return self.name
+
+class Query:
+ """
+ Represent a single DNS query.
+
+ @ivar name: The name about which this query is requesting information.
+ @ivar type: The query type.
+ @ivar cls: The query class.
+ """
+
+ implements(IEncodable)
+
+ name = None
+ type = None
+ cls = None
+
+ def __init__(self, name='', type=A, cls=IN):
+ """
+ @type name: C{str}
+ @param name: The name about which to request information.
+
+ @type type: C{int}
+ @param type: The query type.
+
+ @type cls: C{int}
+ @param cls: The query class.
+ """
+ self.name = Name(name)
+ self.type = type
+ self.cls = cls
+
+
+ def encode(self, strio, compDict=None):
+ self.name.encode(strio, compDict)
+ strio.write(struct.pack("!HH", self.type, self.cls))
+
+
+ def decode(self, strio, length = None):
+ self.name.decode(strio)
+ buff = readPrecisely(strio, 4)
+ self.type, self.cls = struct.unpack("!HH", buff)
+
+
+ def __hash__(self):
+ return hash((str(self.name).lower(), self.type, self.cls))
+
+
+ def __cmp__(self, other):
+ return isinstance(other, Query) and cmp(
+ (str(self.name).lower(), self.type, self.cls),
+ (str(other.name).lower(), other.type, other.cls)
+ ) or cmp(self.__class__, other.__class__)
+
+
+ def __str__(self):
+ t = QUERY_TYPES.get(self.type, EXT_QUERIES.get(self.type, 'UNKNOWN (%d)' % self.type))
+ c = QUERY_CLASSES.get(self.cls, 'UNKNOWN (%d)' % self.cls)
+ return '<Query %s %s %s>' % (self.name, t, c)
+
+
+ def __repr__(self):
+ return 'Query(%r, %r, %r)' % (str(self.name), self.type, self.cls)
+
+
+class RRHeader(tputil.FancyEqMixin):
+ """
+ A resource record header.
+
+ @cvar fmt: C{str} specifying the byte format of an RR.
+
+ @ivar name: The name about which this reply contains information.
+ @ivar type: The query type of the original request.
+ @ivar cls: The query class of the original request.
+ @ivar ttl: The time-to-live for this record.
+ @ivar payload: An object that implements the IEncodable interface
+
+ @ivar auth: A C{bool} indicating whether this C{RRHeader} was parsed from an
+ authoritative message.
+ """
+
+ implements(IEncodable)
+
+ compareAttributes = ('name', 'type', 'cls', 'ttl', 'payload', 'auth')
+
+ fmt = "!HHIH"
+
+ name = None
+ type = None
+ cls = None
+ ttl = None
+ payload = None
+ rdlength = None
+
+ cachedResponse = None
+
+ def __init__(self, name='', type=A, cls=IN, ttl=0, payload=None, auth=False):
+ """
+ @type name: C{str}
+ @param name: The name about which this reply contains information.
+
+ @type type: C{int}
+ @param type: The query type.
+
+ @type cls: C{int}
+ @param cls: The query class.
+
+ @type ttl: C{int}
+ @param ttl: Time to live for this record.
+
+ @type payload: An object implementing C{IEncodable}
+ @param payload: A Query Type specific data object.
+ """
+ assert (payload is None) or isinstance(payload, UnknownRecord) or (payload.TYPE == type)
+
+ self.name = Name(name)
+ self.type = type
+ self.cls = cls
+ self.ttl = ttl
+ self.payload = payload
+ self.auth = auth
+
+
+ def encode(self, strio, compDict=None):
+ self.name.encode(strio, compDict)
+ strio.write(struct.pack(self.fmt, self.type, self.cls, self.ttl, 0))
+ if self.payload:
+ prefix = strio.tell()
+ self.payload.encode(strio, compDict)
+ aft = strio.tell()
+ strio.seek(prefix - 2, 0)
+ strio.write(struct.pack('!H', aft - prefix))
+ strio.seek(aft, 0)
+
+
+ def decode(self, strio, length = None):
+ self.name.decode(strio)
+ l = struct.calcsize(self.fmt)
+ buff = readPrecisely(strio, l)
+ r = struct.unpack(self.fmt, buff)
+ self.type, self.cls, self.ttl, self.rdlength = r
+
+
+ def isAuthoritative(self):
+ return self.auth
+
+
+ def __str__(self):
+ t = QUERY_TYPES.get(self.type, EXT_QUERIES.get(self.type, 'UNKNOWN (%d)' % self.type))
+ c = QUERY_CLASSES.get(self.cls, 'UNKNOWN (%d)' % self.cls)
+ return '<RR name=%s type=%s class=%s ttl=%ds auth=%s>' % (self.name, t, c, self.ttl, self.auth and 'True' or 'False')
+
+
+ __repr__ = __str__
+
+
+
+class SimpleRecord(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ A Resource Record which consists of a single RFC 1035 domain-name.
+
+ @type name: L{Name}
+ @ivar name: The name associated with this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+
+ showAttributes = (('name', 'name', '%s'), 'ttl')
+ compareAttributes = ('name', 'ttl')
+
+ TYPE = None
+ name = None
+
+ def __init__(self, name='', ttl=None):
+ self.name = Name(name)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ self.name.encode(strio, compDict)
+
+
+ def decode(self, strio, length = None):
+ self.name = Name()
+ self.name.decode(strio)
+
+
+ def __hash__(self):
+ return hash(self.name)
+
+
+# Kinds of RRs - oh my!
+class Record_NS(SimpleRecord):
+ """
+ An authoritative nameserver.
+ """
+ TYPE = NS
+ fancybasename = 'NS'
+
+
+
+class Record_MD(SimpleRecord):
+ """
+ A mail destination.
+
+ This record type is obsolete.
+
+ @see: L{Record_MX}
+ """
+ TYPE = MD
+ fancybasename = 'MD'
+
+
+
+class Record_MF(SimpleRecord):
+ """
+ A mail forwarder.
+
+ This record type is obsolete.
+
+ @see: L{Record_MX}
+ """
+ TYPE = MF
+ fancybasename = 'MF'
+
+
+
+class Record_CNAME(SimpleRecord):
+ """
+ The canonical name for an alias.
+ """
+ TYPE = CNAME
+ fancybasename = 'CNAME'
+
+
+
+class Record_MB(SimpleRecord):
+ """
+ A mailbox domain name.
+
+ This is an experimental record type.
+ """
+ TYPE = MB
+ fancybasename = 'MB'
+
+
+
+class Record_MG(SimpleRecord):
+ """
+ A mail group member.
+
+ This is an experimental record type.
+ """
+ TYPE = MG
+ fancybasename = 'MG'
+
+
+
+class Record_MR(SimpleRecord):
+ """
+ A mail rename domain name.
+
+ This is an experimental record type.
+ """
+ TYPE = MR
+ fancybasename = 'MR'
+
+
+
+class Record_PTR(SimpleRecord):
+ """
+ A domain name pointer.
+ """
+ TYPE = PTR
+ fancybasename = 'PTR'
+
+
+
+class Record_DNAME(SimpleRecord):
+ """
+ A non-terminal DNS name redirection.
+
+ This record type provides the capability to map an entire subtree of the
+ DNS name space to another domain. It differs from the CNAME record which
+ maps a single node of the name space.
+
+ @see: U{http://www.faqs.org/rfcs/rfc2672.html}
+ @see: U{http://www.faqs.org/rfcs/rfc3363.html}
+ """
+ TYPE = DNAME
+ fancybasename = 'DNAME'
+
+
+
+class Record_A(tputil.FancyEqMixin):
+ """
+ An IPv4 host address.
+
+ @type address: C{str}
+ @ivar address: The packed network-order representation of the IPv4 address
+ associated with this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+
+ compareAttributes = ('address', 'ttl')
+
+ TYPE = A
+ address = None
+
+ def __init__(self, address='0.0.0.0', ttl=None):
+ address = socket.inet_aton(address)
+ self.address = address
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(self.address)
+
+
+ def decode(self, strio, length = None):
+ self.address = readPrecisely(strio, 4)
+
+
+ def __hash__(self):
+ return hash(self.address)
+
+
+ def __str__(self):
+ return '<A address=%s ttl=%s>' % (self.dottedQuad(), self.ttl)
+ __repr__ = __str__
+
+
+ def dottedQuad(self):
+ return socket.inet_ntoa(self.address)
+
+
+
+class Record_SOA(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ Marks the start of a zone of authority.
+
+ This record describes parameters which are shared by all records within a
+ particular zone.
+
+ @type mname: L{Name}
+ @ivar mname: The domain-name of the name server that was the original or
+ primary source of data for this zone.
+
+ @type rname: L{Name}
+ @ivar rname: A domain-name which specifies the mailbox of the person
+ responsible for this zone.
+
+ @type serial: C{int}
+ @ivar serial: The unsigned 32 bit version number of the original copy of
+ the zone. Zone transfers preserve this value. This value wraps and
+ should be compared using sequence space arithmetic.
+
+ @type refresh: C{int}
+ @ivar refresh: A 32 bit time interval before the zone should be refreshed.
+
+ @type minimum: C{int}
+ @ivar minimum: The unsigned 32 bit minimum TTL field that should be
+ exported with any RR from this zone.
+
+ @type expire: C{int}
+ @ivar expire: A 32 bit time value that specifies the upper limit on the
+ time interval that can elapse before the zone is no longer
+ authoritative.
+
+ @type retry: C{int}
+ @ivar retry: A 32 bit time interval that should elapse before a failed
+ refresh should be retried.
+
+ @type ttl: C{int}
+ @ivar ttl: The default TTL to use for records served from this zone.
+ """
+ implements(IEncodable, IRecord)
+
+ fancybasename = 'SOA'
+ compareAttributes = ('serial', 'mname', 'rname', 'refresh', 'expire', 'retry', 'minimum', 'ttl')
+ showAttributes = (('mname', 'mname', '%s'), ('rname', 'rname', '%s'), 'serial', 'refresh', 'retry', 'expire', 'minimum', 'ttl')
+
+ TYPE = SOA
+
+ def __init__(self, mname='', rname='', serial=0, refresh=0, retry=0, expire=0, minimum=0, ttl=None):
+ self.mname, self.rname = Name(mname), Name(rname)
+ self.serial, self.refresh = str2time(serial), str2time(refresh)
+ self.minimum, self.expire = str2time(minimum), str2time(expire)
+ self.retry = str2time(retry)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ self.mname.encode(strio, compDict)
+ self.rname.encode(strio, compDict)
+ strio.write(
+ struct.pack(
+ '!LlllL',
+ self.serial, self.refresh, self.retry, self.expire,
+ self.minimum
+ )
+ )
+
+
+ def decode(self, strio, length = None):
+ self.mname, self.rname = Name(), Name()
+ self.mname.decode(strio)
+ self.rname.decode(strio)
+ r = struct.unpack('!LlllL', readPrecisely(strio, 20))
+ self.serial, self.refresh, self.retry, self.expire, self.minimum = r
+
+
+ def __hash__(self):
+ return hash((
+ self.serial, self.mname, self.rname,
+ self.refresh, self.expire, self.retry
+ ))
+
+
+
+class Record_NULL(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ A null record.
+
+ This is an experimental record type.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+
+ fancybasename = 'NULL'
+ showAttributes = compareAttributes = ('payload', 'ttl')
+
+ TYPE = NULL
+
+ def __init__(self, payload=None, ttl=None):
+ self.payload = payload
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(self.payload)
+
+
+ def decode(self, strio, length = None):
+ self.payload = readPrecisely(strio, length)
+
+
+ def __hash__(self):
+ return hash(self.payload)
+
+
+
+class Record_WKS(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ A well known service description.
+
+ This record type is obsolete. See L{Record_SRV}.
+
+ @type address: C{str}
+ @ivar address: The packed network-order representation of the IPv4 address
+ associated with this record.
+
+ @type protocol: C{int}
+ @ivar protocol: The 8 bit IP protocol number for which this service map is
+ relevant.
+
+ @type map: C{str}
+ @ivar map: A bitvector indicating the services available at the specified
+ address.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+
+ fancybasename = "WKS"
+ compareAttributes = ('address', 'protocol', 'map', 'ttl')
+ showAttributes = [('_address', 'address', '%s'), 'protocol', 'ttl']
+
+ TYPE = WKS
+
+ _address = property(lambda self: socket.inet_ntoa(self.address))
+
+ def __init__(self, address='0.0.0.0', protocol=0, map='', ttl=None):
+ self.address = socket.inet_aton(address)
+ self.protocol, self.map = protocol, map
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(self.address)
+ strio.write(struct.pack('!B', self.protocol))
+ strio.write(self.map)
+
+
+ def decode(self, strio, length = None):
+ self.address = readPrecisely(strio, 4)
+ self.protocol = struct.unpack('!B', readPrecisely(strio, 1))[0]
+ self.map = readPrecisely(strio, length - 5)
+
+
+ def __hash__(self):
+ return hash((self.address, self.protocol, self.map))
+
+
+
+class Record_AAAA(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ An IPv6 host address.
+
+ @type address: C{str}
+ @ivar address: The packed network-order representation of the IPv6 address
+ associated with this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc1886.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = AAAA
+
+ fancybasename = 'AAAA'
+ showAttributes = (('_address', 'address', '%s'), 'ttl')
+ compareAttributes = ('address', 'ttl')
+
+ _address = property(lambda self: socket.inet_ntop(AF_INET6, self.address))
+
+ def __init__(self, address = '::', ttl=None):
+ self.address = socket.inet_pton(AF_INET6, address)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(self.address)
+
+
+ def decode(self, strio, length = None):
+ self.address = readPrecisely(strio, 16)
+
+
+ def __hash__(self):
+ return hash(self.address)
+
+
+
+class Record_A6(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ An IPv6 address.
+
+ This is an experimental record type.
+
+ @type prefixLen: C{int}
+ @ivar prefixLen: The length of the suffix.
+
+ @type suffix: C{str}
+ @ivar suffix: An IPv6 address suffix in network order.
+
+ @type prefix: L{Name}
+ @ivar prefix: If specified, a name which will be used as a prefix for other
+ A6 records.
+
+ @type bytes: C{int}
+ @ivar bytes: The length of the prefix.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc2874.html}
+ @see: U{http://www.faqs.org/rfcs/rfc3363.html}
+ @see: U{http://www.faqs.org/rfcs/rfc3364.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = A6
+
+ fancybasename = 'A6'
+ showAttributes = (('_suffix', 'suffix', '%s'), ('prefix', 'prefix', '%s'), 'ttl')
+ compareAttributes = ('prefixLen', 'prefix', 'suffix', 'ttl')
+
+ _suffix = property(lambda self: socket.inet_ntop(AF_INET6, self.suffix))
+
+ def __init__(self, prefixLen=0, suffix='::', prefix='', ttl=None):
+ self.prefixLen = prefixLen
+ self.suffix = socket.inet_pton(AF_INET6, suffix)
+ self.prefix = Name(prefix)
+ self.bytes = int((128 - self.prefixLen) / 8.0)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(struct.pack('!B', self.prefixLen))
+ if self.bytes:
+ strio.write(self.suffix[-self.bytes:])
+ if self.prefixLen:
+ # This may not be compressed
+ self.prefix.encode(strio, None)
+
+
+ def decode(self, strio, length = None):
+ self.prefixLen = struct.unpack('!B', readPrecisely(strio, 1))[0]
+ self.bytes = int((128 - self.prefixLen) / 8.0)
+ if self.bytes:
+ self.suffix = '\x00' * (16 - self.bytes) + readPrecisely(strio, self.bytes)
+ if self.prefixLen:
+ self.prefix.decode(strio)
+
+
+ def __eq__(self, other):
+ if isinstance(other, Record_A6):
+ return (self.prefixLen == other.prefixLen and
+ self.suffix[-self.bytes:] == other.suffix[-self.bytes:] and
+ self.prefix == other.prefix and
+ self.ttl == other.ttl)
+ return NotImplemented
+
+
+ def __hash__(self):
+ return hash((self.prefixLen, self.suffix[-self.bytes:], self.prefix))
+
+
+ def __str__(self):
+ return '<A6 %s %s (%d) ttl=%s>' % (
+ self.prefix,
+ socket.inet_ntop(AF_INET6, self.suffix),
+ self.prefixLen, self.ttl
+ )
+
+
+
+class Record_SRV(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ The location of the server(s) for a specific protocol and domain.
+
+ This is an experimental record type.
+
+ @type priority: C{int}
+ @ivar priority: The priority of this target host. A client MUST attempt to
+ contact the target host with the lowest-numbered priority it can reach;
+ target hosts with the same priority SHOULD be tried in an order defined
+ by the weight field.
+
+ @type weight: C{int}
+ @ivar weight: Specifies a relative weight for entries with the same
+ priority. Larger weights SHOULD be given a proportionately higher
+ probability of being selected.
+
+ @type port: C{int}
+ @ivar port: The port on this target host of this service.
+
+ @type target: L{Name}
+ @ivar target: The domain name of the target host. There MUST be one or
+ more address records for this name, the name MUST NOT be an alias (in
+ the sense of RFC 1034 or RFC 2181). Implementors are urged, but not
+ required, to return the address record(s) in the Additional Data
+ section. Unless and until permitted by future standards action, name
+ compression is not to be used for this field.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc2782.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = SRV
+
+ fancybasename = 'SRV'
+ compareAttributes = ('priority', 'weight', 'target', 'port', 'ttl')
+ showAttributes = ('priority', 'weight', ('target', 'target', '%s'), 'port', 'ttl')
+
+ def __init__(self, priority=0, weight=0, port=0, target='', ttl=None):
+ self.priority = int(priority)
+ self.weight = int(weight)
+ self.port = int(port)
+ self.target = Name(target)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(struct.pack('!HHH', self.priority, self.weight, self.port))
+ # This can't be compressed
+ self.target.encode(strio, None)
+
+
+ def decode(self, strio, length = None):
+ r = struct.unpack('!HHH', readPrecisely(strio, struct.calcsize('!HHH')))
+ self.priority, self.weight, self.port = r
+ self.target = Name()
+ self.target.decode(strio)
+
+
+ def __hash__(self):
+ return hash((self.priority, self.weight, self.port, self.target))
+
+
+
+class Record_NAPTR(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ The location of the server(s) for a specific protocol and domain.
+
+ @type order: C{int}
+ @ivar order: An integer specifying the order in which the NAPTR records
+ MUST be processed to ensure the correct ordering of rules. Low numbers
+ are processed before high numbers.
+
+ @type preference: C{int}
+ @ivar preference: An integer that specifies the order in which NAPTR
+ records with equal "order" values SHOULD be processed, low numbers
+ being processed before high numbers.
+
+ @type flag: L{Charstr}
+ @ivar flag: A <character-string> containing flags to control aspects of the
+ rewriting and interpretation of the fields in the record. Flags
+ aresingle characters from the set [A-Z0-9]. The case of the alphabetic
+ characters is not significant.
+
+ At this time only four flags, "S", "A", "U", and "P", are defined.
+
+ @type service: L{Charstr}
+ @ivar service: Specifies the service(s) available down this rewrite path.
+ It may also specify the particular protocol that is used to talk with a
+ service. A protocol MUST be specified if the flags field states that
+ the NAPTR is terminal.
+
+ @type regexp: L{Charstr}
+ @ivar regexp: A STRING containing a substitution expression that is applied
+ to the original string held by the client in order to construct the
+ next domain name to lookup.
+
+ @type replacement: L{Name}
+ @ivar replacement: The next NAME to query for NAPTR, SRV, or address
+ records depending on the value of the flags field. This MUST be a
+ fully qualified domain-name.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc2915.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = NAPTR
+
+ compareAttributes = ('order', 'preference', 'flags', 'service', 'regexp',
+ 'replacement')
+ fancybasename = 'NAPTR'
+ showAttributes = ('order', 'preference', ('flags', 'flags', '%s'),
+ ('service', 'service', '%s'), ('regexp', 'regexp', '%s'),
+ ('replacement', 'replacement', '%s'), 'ttl')
+
+ def __init__(self, order=0, preference=0, flags='', service='', regexp='',
+ replacement='', ttl=None):
+ self.order = int(order)
+ self.preference = int(preference)
+ self.flags = Charstr(flags)
+ self.service = Charstr(service)
+ self.regexp = Charstr(regexp)
+ self.replacement = Name(replacement)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict=None):
+ strio.write(struct.pack('!HH', self.order, self.preference))
+ # This can't be compressed
+ self.flags.encode(strio, None)
+ self.service.encode(strio, None)
+ self.regexp.encode(strio, None)
+ self.replacement.encode(strio, None)
+
+
+ def decode(self, strio, length=None):
+ r = struct.unpack('!HH', readPrecisely(strio, struct.calcsize('!HH')))
+ self.order, self.preference = r
+ self.flags = Charstr()
+ self.service = Charstr()
+ self.regexp = Charstr()
+ self.replacement = Name()
+ self.flags.decode(strio)
+ self.service.decode(strio)
+ self.regexp.decode(strio)
+ self.replacement.decode(strio)
+
+
+ def __hash__(self):
+ return hash((
+ self.order, self.preference, self.flags,
+ self.service, self.regexp, self.replacement))
+
+
+
+class Record_AFSDB(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ Map from a domain name to the name of an AFS cell database server.
+
+ @type subtype: C{int}
+ @ivar subtype: In the case of subtype 1, the host has an AFS version 3.0
+ Volume Location Server for the named AFS cell. In the case of subtype
+ 2, the host has an authenticated name server holding the cell-root
+ directory node for the named DCE/NCA cell.
+
+ @type hostname: L{Name}
+ @ivar hostname: The domain name of a host that has a server for the cell
+ named by this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc1183.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = AFSDB
+
+ fancybasename = 'AFSDB'
+ compareAttributes = ('subtype', 'hostname', 'ttl')
+ showAttributes = ('subtype', ('hostname', 'hostname', '%s'), 'ttl')
+
+ def __init__(self, subtype=0, hostname='', ttl=None):
+ self.subtype = int(subtype)
+ self.hostname = Name(hostname)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(struct.pack('!H', self.subtype))
+ self.hostname.encode(strio, compDict)
+
+
+ def decode(self, strio, length = None):
+ r = struct.unpack('!H', readPrecisely(strio, struct.calcsize('!H')))
+ self.subtype, = r
+ self.hostname.decode(strio)
+
+
+ def __hash__(self):
+ return hash((self.subtype, self.hostname))
+
+
+
+class Record_RP(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ The responsible person for a domain.
+
+ @type mbox: L{Name}
+ @ivar mbox: A domain name that specifies the mailbox for the responsible
+ person.
+
+ @type txt: L{Name}
+ @ivar txt: A domain name for which TXT RR's exist (indirection through
+ which allows information sharing about the contents of this RP record).
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+
+ @see: U{http://www.faqs.org/rfcs/rfc1183.html}
+ """
+ implements(IEncodable, IRecord)
+ TYPE = RP
+
+ fancybasename = 'RP'
+ compareAttributes = ('mbox', 'txt', 'ttl')
+ showAttributes = (('mbox', 'mbox', '%s'), ('txt', 'txt', '%s'), 'ttl')
+
+ def __init__(self, mbox='', txt='', ttl=None):
+ self.mbox = Name(mbox)
+ self.txt = Name(txt)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ self.mbox.encode(strio, compDict)
+ self.txt.encode(strio, compDict)
+
+
+ def decode(self, strio, length = None):
+ self.mbox = Name()
+ self.txt = Name()
+ self.mbox.decode(strio)
+ self.txt.decode(strio)
+
+
+ def __hash__(self):
+ return hash((self.mbox, self.txt))
+
+
+
+class Record_HINFO(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ Host information.
+
+ @type cpu: C{str}
+ @ivar cpu: Specifies the CPU type.
+
+ @type os: C{str}
+ @ivar os: Specifies the OS.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+ TYPE = HINFO
+
+ fancybasename = 'HINFO'
+ showAttributes = compareAttributes = ('cpu', 'os', 'ttl')
+
+ def __init__(self, cpu='', os='', ttl=None):
+ self.cpu, self.os = cpu, os
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ strio.write(struct.pack('!B', len(self.cpu)) + self.cpu)
+ strio.write(struct.pack('!B', len(self.os)) + self.os)
+
+
+ def decode(self, strio, length = None):
+ cpu = struct.unpack('!B', readPrecisely(strio, 1))[0]
+ self.cpu = readPrecisely(strio, cpu)
+ os = struct.unpack('!B', readPrecisely(strio, 1))[0]
+ self.os = readPrecisely(strio, os)
+
+
+ def __eq__(self, other):
+ if isinstance(other, Record_HINFO):
+ return (self.os.lower() == other.os.lower() and
+ self.cpu.lower() == other.cpu.lower() and
+ self.ttl == other.ttl)
+ return NotImplemented
+
+
+ def __hash__(self):
+ return hash((self.os.lower(), self.cpu.lower()))
+
+
+
+class Record_MINFO(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ Mailbox or mail list information.
+
+ This is an experimental record type.
+
+ @type rmailbx: L{Name}
+ @ivar rmailbx: A domain-name which specifies a mailbox which is responsible
+ for the mailing list or mailbox. If this domain name names the root,
+ the owner of the MINFO RR is responsible for itself.
+
+ @type emailbx: L{Name}
+ @ivar emailbx: A domain-name which specifies a mailbox which is to receive
+ error messages related to the mailing list or mailbox specified by the
+ owner of the MINFO record. If this domain name names the root, errors
+ should be returned to the sender of the message.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+ TYPE = MINFO
+
+ rmailbx = None
+ emailbx = None
+
+ fancybasename = 'MINFO'
+ compareAttributes = ('rmailbx', 'emailbx', 'ttl')
+ showAttributes = (('rmailbx', 'responsibility', '%s'),
+ ('emailbx', 'errors', '%s'),
+ 'ttl')
+
+ def __init__(self, rmailbx='', emailbx='', ttl=None):
+ self.rmailbx, self.emailbx = Name(rmailbx), Name(emailbx)
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict = None):
+ self.rmailbx.encode(strio, compDict)
+ self.emailbx.encode(strio, compDict)
+
+
+ def decode(self, strio, length = None):
+ self.rmailbx, self.emailbx = Name(), Name()
+ self.rmailbx.decode(strio)
+ self.emailbx.decode(strio)
+
+
+ def __hash__(self):
+ return hash((self.rmailbx, self.emailbx))
+
+
+
+class Record_MX(tputil.FancyStrMixin, tputil.FancyEqMixin):
+ """
+ Mail exchange.
+
+ @type preference: C{int}
+ @ivar preference: Specifies the preference given to this RR among others at
+ the same owner. Lower values are preferred.
+
+ @type name: L{Name}
+ @ivar name: A domain-name which specifies a host willing to act as a mail
+ exchange.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be
+ cached.
+ """
+ implements(IEncodable, IRecord)
+ TYPE = MX
+
+ fancybasename = 'MX'
+ compareAttributes = ('preference', 'name', 'ttl')
+ showAttributes = ('preference', ('name', 'name', '%s'), 'ttl')
+
+ def __init__(self, preference=0, name='', ttl=None, **kwargs):
+ self.preference, self.name = int(preference), Name(kwargs.get('exchange', name))
+ self.ttl = str2time(ttl)
+
+ def encode(self, strio, compDict = None):
+ strio.write(struct.pack('!H', self.preference))
+ self.name.encode(strio, compDict)
+
+
+ def decode(self, strio, length = None):
+ self.preference = struct.unpack('!H', readPrecisely(strio, 2))[0]
+ self.name = Name()
+ self.name.decode(strio)
+
+ def __hash__(self):
+ return hash((self.preference, self.name))
+
+
+
+# Oh god, Record_TXT how I hate thee.
+class Record_TXT(tputil.FancyEqMixin, tputil.FancyStrMixin):
+ """
+ Freeform text.
+
+ @type data: C{list} of C{str}
+ @ivar data: Freeform text which makes up this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be cached.
+ """
+ implements(IEncodable, IRecord)
+
+ TYPE = TXT
+
+ fancybasename = 'TXT'
+ showAttributes = compareAttributes = ('data', 'ttl')
+
+ def __init__(self, *data, **kw):
+ self.data = list(data)
+ # arg man python sucks so bad
+ self.ttl = str2time(kw.get('ttl', None))
+
+
+ def encode(self, strio, compDict = None):
+ for d in self.data:
+ strio.write(struct.pack('!B', len(d)) + d)
+
+
+ def decode(self, strio, length = None):
+ soFar = 0
+ self.data = []
+ while soFar < length:
+ L = struct.unpack('!B', readPrecisely(strio, 1))[0]
+ self.data.append(readPrecisely(strio, L))
+ soFar += L + 1
+ if soFar != length:
+ log.msg(
+ "Decoded %d bytes in %s record, but rdlength is %d" % (
+ soFar, self.fancybasename, length
+ )
+ )
+
+
+ def __hash__(self):
+ return hash(tuple(self.data))
+
+
+
+# This is a fallback record
+class UnknownRecord(tputil.FancyEqMixin, tputil.FancyStrMixin, object):
+ """
+ Encapsulate the wire data for unkown record types so that they can
+ pass through the system unchanged.
+
+ @type data: C{str}
+ @ivar data: Wire data which makes up this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be cached.
+
+ @since: 11.1
+ """
+ implements(IEncodable, IRecord)
+
+ fancybasename = 'UNKNOWN'
+ compareAttributes = ('data', 'ttl')
+ showAttributes = ('data', 'ttl')
+
+ def __init__(self, data='', ttl=None):
+ self.data = data
+ self.ttl = str2time(ttl)
+
+
+ def encode(self, strio, compDict=None):
+ """
+ Write the raw bytes corresponding to this record's payload to the
+ stream.
+ """
+ strio.write(self.data)
+
+
+ def decode(self, strio, length=None):
+ """
+ Load the bytes which are part of this record from the stream and store
+ them unparsed and unmodified.
+ """
+ if length is None:
+ raise Exception('must know length for unknown record types')
+ self.data = readPrecisely(strio, length)
+
+
+ def __hash__(self):
+ return hash((self.data, self.ttl))
+
+
+
+class Record_SPF(Record_TXT):
+ """
+ Structurally, freeform text. Semantically, a policy definition, formatted
+ as defined in U{rfc 4408<http://www.faqs.org/rfcs/rfc4408.html>}.
+
+ @type data: C{list} of C{str}
+ @ivar data: Freeform text which makes up this record.
+
+ @type ttl: C{int}
+ @ivar ttl: The maximum number of seconds which this record should be cached.
+ """
+ TYPE = SPF
+ fancybasename = 'SPF'
+
+
+
+class Message:
+ """
+ L{Message} contains all the information represented by a single
+ DNS request or response.
+ """
+ headerFmt = "!H2B4H"
+ headerSize = struct.calcsize(headerFmt)
+
+ # Question, answer, additional, and nameserver lists
+ queries = answers = add = ns = None
+
+ def __init__(self, id=0, answer=0, opCode=0, recDes=0, recAv=0,
+ auth=0, rCode=OK, trunc=0, maxSize=512):
+ self.maxSize = maxSize
+ self.id = id
+ self.answer = answer
+ self.opCode = opCode
+ self.auth = auth
+ self.trunc = trunc
+ self.recDes = recDes
+ self.recAv = recAv
+ self.rCode = rCode
+ self.queries = []
+ self.answers = []
+ self.authority = []
+ self.additional = []
+
+
+ def addQuery(self, name, type=ALL_RECORDS, cls=IN):
+ """
+ Add another query to this Message.
+
+ @type name: C{str}
+ @param name: The name to query.
+
+ @type type: C{int}
+ @param type: Query type
+
+ @type cls: C{int}
+ @param cls: Query class
+ """
+ self.queries.append(Query(name, type, cls))
+
+
+ def encode(self, strio):
+ compDict = {}
+ body_tmp = StringIO.StringIO()
+ for q in self.queries:
+ q.encode(body_tmp, compDict)
+ for q in self.answers:
+ q.encode(body_tmp, compDict)
+ for q in self.authority:
+ q.encode(body_tmp, compDict)
+ for q in self.additional:
+ q.encode(body_tmp, compDict)
+ body = body_tmp.getvalue()
+ size = len(body) + self.headerSize
+ if self.maxSize and size > self.maxSize:
+ self.trunc = 1
+ body = body[:self.maxSize - self.headerSize]
+ byte3 = (( ( self.answer & 1 ) << 7 )
+ | ((self.opCode & 0xf ) << 3 )
+ | ((self.auth & 1 ) << 2 )
+ | ((self.trunc & 1 ) << 1 )
+ | ( self.recDes & 1 ) )
+ byte4 = ( ( (self.recAv & 1 ) << 7 )
+ | (self.rCode & 0xf ) )
+
+ strio.write(struct.pack(self.headerFmt, self.id, byte3, byte4,
+ len(self.queries), len(self.answers),
+ len(self.authority), len(self.additional)))
+ strio.write(body)
+
+
+ def decode(self, strio, length=None):
+ self.maxSize = 0
+ header = readPrecisely(strio, self.headerSize)
+ r = struct.unpack(self.headerFmt, header)
+ self.id, byte3, byte4, nqueries, nans, nns, nadd = r
+ self.answer = ( byte3 >> 7 ) & 1
+ self.opCode = ( byte3 >> 3 ) & 0xf
+ self.auth = ( byte3 >> 2 ) & 1
+ self.trunc = ( byte3 >> 1 ) & 1
+ self.recDes = byte3 & 1
+ self.recAv = ( byte4 >> 7 ) & 1
+ self.rCode = byte4 & 0xf
+
+ self.queries = []
+ for i in range(nqueries):
+ q = Query()
+ try:
+ q.decode(strio)
+ except EOFError:
+ return
+ self.queries.append(q)
+
+ items = ((self.answers, nans), (self.authority, nns), (self.additional, nadd))
+ for (l, n) in items:
+ self.parseRecords(l, n, strio)
+
+
+ def parseRecords(self, list, num, strio):
+ for i in range(num):
+ header = RRHeader(auth=self.auth)
+ try:
+ header.decode(strio)
+ except EOFError:
+ return
+ t = self.lookupRecordType(header.type)
+ if not t:
+ continue
+ header.payload = t(ttl=header.ttl)
+ try:
+ header.payload.decode(strio, header.rdlength)
+ except EOFError:
+ return
+ list.append(header)
+
+
+ # Create a mapping from record types to their corresponding Record_*
+ # classes. This relies on the global state which has been created so
+ # far in initializing this module (so don't define Record classes after
+ # this).
+ _recordTypes = {}
+ for name in globals():
+ if name.startswith('Record_'):
+ _recordTypes[globals()[name].TYPE] = globals()[name]
+
+ # Clear the iteration variable out of the class namespace so it
+ # doesn't become an attribute.
+ del name
+
+
+ def lookupRecordType(self, type):
+ """
+ Retrieve the L{IRecord} implementation for the given record type.
+
+ @param type: A record type, such as L{A} or L{NS}.
+ @type type: C{int}
+
+ @return: An object which implements L{IRecord} or C{None} if none
+ can be found for the given type.
+ @rtype: L{types.ClassType}
+ """
+ return self._recordTypes.get(type, UnknownRecord)
+
+
+ def toStr(self):
+ strio = StringIO.StringIO()
+ self.encode(strio)
+ return strio.getvalue()
+
+
+ def fromStr(self, str):
+ strio = StringIO.StringIO(str)
+ self.decode(strio)
+
+
+
+class DNSMixin(object):
+ """
+ DNS protocol mixin shared by UDP and TCP implementations.
+
+ @ivar _reactor: A L{IReactorTime} and L{IReactorUDP} provider which will
+ be used to issue DNS queries and manage request timeouts.
+ """
+ id = None
+ liveMessages = None
+
+ def __init__(self, controller, reactor=None):
+ self.controller = controller
+ self.id = random.randrange(2 ** 10, 2 ** 15)
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+
+ def pickID(self):
+ """
+ Return a unique ID for queries.
+ """
+ while True:
+ id = randomSource()
+ if id not in self.liveMessages:
+ return id
+
+
+ def callLater(self, period, func, *args):
+ """
+ Wrapper around reactor.callLater, mainly for test purpose.
+ """
+ return self._reactor.callLater(period, func, *args)
+
+
+ def _query(self, queries, timeout, id, writeMessage):
+ """
+ Send out a message with the given queries.
+
+ @type queries: C{list} of C{Query} instances
+ @param queries: The queries to transmit
+
+ @type timeout: C{int} or C{float}
+ @param timeout: How long to wait before giving up
+
+ @type id: C{int}
+ @param id: Unique key for this request
+
+ @type writeMessage: C{callable}
+ @param writeMessage: One-parameter callback which writes the message
+
+ @rtype: C{Deferred}
+ @return: a C{Deferred} which will be fired with the result of the
+ query, or errbacked with any errors that could happen (exceptions
+ during writing of the query, timeout errors, ...).
+ """
+ m = Message(id, recDes=1)
+ m.queries = queries
+
+ try:
+ writeMessage(m)
+ except:
+ return defer.fail()
+
+ resultDeferred = defer.Deferred()
+ cancelCall = self.callLater(timeout, self._clearFailed, resultDeferred, id)
+ self.liveMessages[id] = (resultDeferred, cancelCall)
+
+ return resultDeferred
+
+ def _clearFailed(self, deferred, id):
+ """
+ Clean the Deferred after a timeout.
+ """
+ try:
+ del self.liveMessages[id]
+ except KeyError:
+ pass
+ deferred.errback(failure.Failure(DNSQueryTimeoutError(id)))
+
+
+class DNSDatagramProtocol(DNSMixin, protocol.DatagramProtocol):
+ """
+ DNS protocol over UDP.
+ """
+ resends = None
+
+ def stopProtocol(self):
+ """
+ Stop protocol: reset state variables.
+ """
+ self.liveMessages = {}
+ self.resends = {}
+ self.transport = None
+
+ def startProtocol(self):
+ """
+ Upon start, reset internal state.
+ """
+ self.liveMessages = {}
+ self.resends = {}
+
+ def writeMessage(self, message, address):
+ """
+ Send a message holding DNS queries.
+
+ @type message: L{Message}
+ """
+ self.transport.write(message.toStr(), address)
+
+ def startListening(self):
+ self._reactor.listenUDP(0, self, maxPacketSize=512)
+
+ def datagramReceived(self, data, addr):
+ """
+ Read a datagram, extract the message in it and trigger the associated
+ Deferred.
+ """
+ m = Message()
+ try:
+ m.fromStr(data)
+ except EOFError:
+ log.msg("Truncated packet (%d bytes) from %s" % (len(data), addr))
+ return
+ except:
+ # Nothing should trigger this, but since we're potentially
+ # invoking a lot of different decoding methods, we might as well
+ # be extra cautious. Anything that triggers this is itself
+ # buggy.
+ log.err(failure.Failure(), "Unexpected decoding error")
+ return
+
+ if m.id in self.liveMessages:
+ d, canceller = self.liveMessages[m.id]
+ del self.liveMessages[m.id]
+ canceller.cancel()
+ # XXX we shouldn't need this hack of catching exception on callback()
+ try:
+ d.callback(m)
+ except:
+ log.err()
+ else:
+ if m.id not in self.resends:
+ self.controller.messageReceived(m, self, addr)
+
+
+ def removeResend(self, id):
+ """
+ Mark message ID as no longer having duplication suppression.
+ """
+ try:
+ del self.resends[id]
+ except KeyError:
+ pass
+
+ def query(self, address, queries, timeout=10, id=None):
+ """
+ Send out a message with the given queries.
+
+ @type address: C{tuple} of C{str} and C{int}
+ @param address: The address to which to send the query
+
+ @type queries: C{list} of C{Query} instances
+ @param queries: The queries to transmit
+
+ @rtype: C{Deferred}
+ """
+ if not self.transport:
+ # XXX transport might not get created automatically, use callLater?
+ try:
+ self.startListening()
+ except CannotListenError:
+ return defer.fail()
+
+ if id is None:
+ id = self.pickID()
+ else:
+ self.resends[id] = 1
+
+ def writeMessage(m):
+ self.writeMessage(m, address)
+
+ return self._query(queries, timeout, id, writeMessage)
+
+
+class DNSProtocol(DNSMixin, protocol.Protocol):
+ """
+ DNS protocol over TCP.
+ """
+ length = None
+ buffer = ''
+
+ def writeMessage(self, message):
+ """
+ Send a message holding DNS queries.
+
+ @type message: L{Message}
+ """
+ s = message.toStr()
+ self.transport.write(struct.pack('!H', len(s)) + s)
+
+ def connectionMade(self):
+ """
+ Connection is made: reset internal state, and notify the controller.
+ """
+ self.liveMessages = {}
+ self.controller.connectionMade(self)
+
+
+ def connectionLost(self, reason):
+ """
+ Notify the controller that this protocol is no longer
+ connected.
+ """
+ self.controller.connectionLost(self)
+
+
+ def dataReceived(self, data):
+ self.buffer += data
+
+ while self.buffer:
+ if self.length is None and len(self.buffer) >= 2:
+ self.length = struct.unpack('!H', self.buffer[:2])[0]
+ self.buffer = self.buffer[2:]
+
+ if len(self.buffer) >= self.length:
+ myChunk = self.buffer[:self.length]
+ m = Message()
+ m.fromStr(myChunk)
+
+ try:
+ d, canceller = self.liveMessages[m.id]
+ except KeyError:
+ self.controller.messageReceived(m, self)
+ else:
+ del self.liveMessages[m.id]
+ canceller.cancel()
+ # XXX we shouldn't need this hack
+ try:
+ d.callback(m)
+ except:
+ log.err()
+
+ self.buffer = self.buffer[self.length:]
+ self.length = None
+ else:
+ break
+
+
+ def query(self, queries, timeout=60):
+ """
+ Send out a message with the given queries.
+
+ @type queries: C{list} of C{Query} instances
+ @param queries: The queries to transmit
+
+ @rtype: C{Deferred}
+ """
+ id = self.pickID()
+ return self._query(queries, timeout, id, self.writeMessage)
diff --git a/twisted/names/error.py b/twisted/names/error.py
new file mode 100644
index 0000000..3163cfe
--- /dev/null
+++ b/twisted/names/error.py
@@ -0,0 +1,95 @@
+# -*- test-case-name: twisted.names.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Exception class definitions for Twisted Names.
+"""
+
+from twisted.internet.defer import TimeoutError
+
+
+class DomainError(ValueError):
+ """
+ Indicates a lookup failed because there were no records matching the given
+ C{name, class, type} triple.
+ """
+
+
+
+class AuthoritativeDomainError(ValueError):
+ """
+ Indicates a lookup failed for a name for which this server is authoritative
+ because there were no records matching the given C{name, class, type}
+ triple.
+ """
+
+
+
+class DNSQueryTimeoutError(TimeoutError):
+ """
+ Indicates a lookup failed due to a timeout.
+
+ @ivar id: The id of the message which timed out.
+ """
+ def __init__(self, id):
+ TimeoutError.__init__(self)
+ self.id = id
+
+
+
+class DNSFormatError(DomainError):
+ """
+ Indicates a query failed with a result of L{twisted.names.dns.EFORMAT}.
+ """
+
+
+
+class DNSServerError(DomainError):
+ """
+ Indicates a query failed with a result of L{twisted.names.dns.ESERVER}.
+ """
+
+
+
+class DNSNameError(DomainError):
+ """
+ Indicates a query failed with a result of L{twisted.names.dns.ENAME}.
+ """
+
+
+
+class DNSNotImplementedError(DomainError):
+ """
+ Indicates a query failed with a result of L{twisted.names.dns.ENOTIMP}.
+ """
+
+
+
+class DNSQueryRefusedError(DomainError):
+ """
+ Indicates a query failed with a result of L{twisted.names.dns.EREFUSED}.
+ """
+
+
+
+class DNSUnknownError(DomainError):
+ """
+ Indicates a query failed with an unknown result.
+ """
+
+
+
+class ResolverError(Exception):
+ """
+ Indicates a query failed because of a decision made by the local
+ resolver object.
+ """
+
+
+__all__ = [
+ 'DomainError', 'AuthoritativeDomainError', 'DNSQueryTimeoutError',
+
+ 'DNSFormatError', 'DNSServerError', 'DNSNameError',
+ 'DNSNotImplementedError', 'DNSQueryRefusedError',
+ 'DNSUnknownError', 'ResolverError']
diff --git a/twisted/names/hosts.py b/twisted/names/hosts.py
new file mode 100644
index 0000000..fd2cd5e
--- /dev/null
+++ b/twisted/names/hosts.py
@@ -0,0 +1,157 @@
+# -*- test-case-name: twisted.names.test.test_hosts -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+hosts(5) support.
+"""
+
+from twisted.names import dns
+from twisted.persisted import styles
+from twisted.python import failure
+from twisted.python.filepath import FilePath
+from twisted.internet import defer
+from twisted.internet.abstract import isIPAddress
+
+from twisted.names import common
+
+def searchFileForAll(hostsFile, name):
+ """
+ Search the given file, which is in hosts(5) standard format, for an address
+ entry with a given name.
+
+ @param hostsFile: The name of the hosts(5)-format file to search.
+ @type hostsFile: L{FilePath}
+
+ @param name: The name to search for.
+ @type name: C{str}
+
+ @return: C{None} if the name is not found in the file, otherwise a
+ C{str} giving the address in the file associated with the name.
+ """
+ results = []
+ try:
+ lines = hostsFile.getContent().splitlines()
+ except:
+ return results
+
+ name = name.lower()
+ for line in lines:
+ idx = line.find('#')
+ if idx != -1:
+ line = line[:idx]
+ if not line:
+ continue
+ parts = line.split()
+
+ if name.lower() in [s.lower() for s in parts[1:]]:
+ results.append(parts[0])
+ return results
+
+
+
+def searchFileFor(file, name):
+ """
+ Grep given file, which is in hosts(5) standard format, for an address
+ entry with a given name.
+
+ @param file: The name of the hosts(5)-format file to search.
+
+ @param name: The name to search for.
+ @type name: C{str}
+
+ @return: C{None} if the name is not found in the file, otherwise a
+ C{str} giving the address in the file associated with the name.
+ """
+ addresses = searchFileForAll(FilePath(file), name)
+ if addresses:
+ return addresses[0]
+ return None
+
+
+
+class Resolver(common.ResolverBase, styles.Versioned):
+ """
+ A resolver that services hosts(5) format files.
+ """
+
+ persistenceVersion = 1
+
+ def upgradeToVersion1(self):
+ # <3 exarkun
+ self.typeToMethod = {}
+ for (k, v) in common.typeToMethod.items():
+ self.typeToMethod[k] = getattr(self, v)
+
+
+ def __init__(self, file='/etc/hosts', ttl = 60 * 60):
+ common.ResolverBase.__init__(self)
+ self.file = file
+ self.ttl = ttl
+
+
+ def _aRecords(self, name):
+ """
+ Return a tuple of L{dns.RRHeader} instances for all of the IPv4
+ addresses in the hosts file.
+ """
+ return tuple([
+ dns.RRHeader(name, dns.A, dns.IN, self.ttl,
+ dns.Record_A(addr, self.ttl))
+ for addr
+ in searchFileForAll(FilePath(self.file), name)
+ if isIPAddress(addr)])
+
+
+ def _aaaaRecords(self, name):
+ """
+ Return a tuple of L{dns.RRHeader} instances for all of the IPv6
+ addresses in the hosts file.
+ """
+ return tuple([
+ dns.RRHeader(name, dns.AAAA, dns.IN, self.ttl,
+ dns.Record_AAAA(addr, self.ttl))
+ for addr
+ in searchFileForAll(FilePath(self.file), name)
+ if not isIPAddress(addr)])
+
+
+ def _respond(self, name, records):
+ """
+ Generate a response for the given name containing the given result
+ records, or a failure if there are no result records.
+
+ @param name: The DNS name the response is for.
+ @type name: C{str}
+
+ @param records: A tuple of L{dns.RRHeader} instances giving the results
+ that will go into the response.
+
+ @return: A L{Deferred} which will fire with a three-tuple of result
+ records, authority records, and additional records, or which will
+ fail with L{dns.DomainError} if there are no result records.
+ """
+ if records:
+ return defer.succeed((records, (), ()))
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+
+ def lookupAddress(self, name, timeout=None):
+ """
+ Read any IPv4 addresses from C{self.file} and return them as L{Record_A}
+ instances.
+ """
+ return self._respond(name, self._aRecords(name))
+
+
+ def lookupIPV6Address(self, name, timeout=None):
+ """
+ Read any IPv4 addresses from C{self.file} and return them as L{Record_A}
+ instances.
+ """
+ return self._respond(name, self._aaaaRecords(name))
+
+ # Someday this should include IPv6 addresses too, but that will cause
+ # problems if users of the API (mainly via getHostByName) aren't updated to
+ # know about IPv6 first.
+ lookupAllRecords = lookupAddress
diff --git a/twisted/names/resolve.py b/twisted/names/resolve.py
new file mode 100644
index 0000000..19996e9
--- /dev/null
+++ b/twisted/names/resolve.py
@@ -0,0 +1,59 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Lookup a name using multiple resolvers.
+
+Future Plans: This needs someway to specify which resolver answered
+the query, or someway to specify (authority|ttl|cache behavior|more?)
+
+@author: Jp Calderone
+"""
+
+from twisted.internet import defer, interfaces
+from twisted.names import dns
+from zope.interface import implements
+import common
+
+class FailureHandler:
+ def __init__(self, resolver, query, timeout):
+ self.resolver = resolver
+ self.query = query
+ self.timeout = timeout
+
+
+ def __call__(self, failure):
+ # AuthoritativeDomainErrors should halt resolution attempts
+ failure.trap(dns.DomainError, defer.TimeoutError, NotImplementedError)
+ return self.resolver(self.query, self.timeout)
+
+
+class ResolverChain(common.ResolverBase):
+ """Lookup an address using multiple C{IResolver}s"""
+
+ implements(interfaces.IResolver)
+
+
+ def __init__(self, resolvers):
+ common.ResolverBase.__init__(self)
+ self.resolvers = resolvers
+
+
+ def _lookup(self, name, cls, type, timeout):
+ q = dns.Query(name, type, cls)
+ d = self.resolvers[0].query(q, timeout)
+ for r in self.resolvers[1:]:
+ d = d.addErrback(
+ FailureHandler(r.query, q, timeout)
+ )
+ return d
+
+
+ def lookupAllRecords(self, name, timeout = None):
+ d = self.resolvers[0].lookupAllRecords(name, timeout)
+ for r in self.resolvers[1:]:
+ d = d.addErrback(
+ FailureHandler(r.lookupAllRecords, name, timeout)
+ )
+ return d
diff --git a/twisted/names/root.py b/twisted/names/root.py
new file mode 100644
index 0000000..18908e7
--- /dev/null
+++ b/twisted/names/root.py
@@ -0,0 +1,446 @@
+# -*- test-case-name: twisted.names.test.test_rootresolve -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Resolver implementation for querying successive authoritative servers to
+lookup a record, starting from the root nameservers.
+
+@author: Jp Calderone
+
+todo::
+ robustify it
+ documentation
+"""
+
+import warnings
+
+from twisted.python.failure import Failure
+from twisted.internet import defer
+from twisted.names import dns, common, error
+
+
+def retry(t, p, *args):
+ """
+ Issue a query one or more times.
+
+ This function is deprecated. Use one of the resolver classes for retry
+ logic, or implement it yourself.
+ """
+ warnings.warn(
+ "twisted.names.root.retry is deprecated since Twisted 10.0. Use a "
+ "Resolver object for retry logic.", category=DeprecationWarning,
+ stacklevel=2)
+
+ assert t, "Timeout is required"
+ t = list(t)
+ def errback(failure):
+ failure.trap(defer.TimeoutError)
+ if not t:
+ return failure
+ return p.query(timeout=t.pop(0), *args
+ ).addErrback(errback
+ )
+ return p.query(timeout=t.pop(0), *args
+ ).addErrback(errback
+ )
+
+
+
+class _DummyController:
+ """
+ A do-nothing DNS controller. This is useful when all messages received
+ will be responses to previously issued queries. Anything else received
+ will be ignored.
+ """
+ def messageReceived(self, *args):
+ pass
+
+
+
+class Resolver(common.ResolverBase):
+ """
+ L{Resolver} implements recursive lookup starting from a specified list of
+ root servers.
+
+ @ivar hints: A C{list} of C{str} giving the dotted quad representation
+ of IP addresses of root servers at which to begin resolving names.
+
+ @ivar _maximumQueries: A C{int} giving the maximum number of queries
+ which will be attempted to resolve a single name.
+
+ @ivar _reactor: A L{IReactorTime} and L{IReactorUDP} provider to use to
+ bind UDP ports and manage timeouts.
+ """
+ def __init__(self, hints, maximumQueries=10, reactor=None):
+ common.ResolverBase.__init__(self)
+ self.hints = hints
+ self._maximumQueries = maximumQueries
+ self._reactor = reactor
+
+
+ def _roots(self):
+ """
+ Return a list of two-tuples representing the addresses of the root
+ servers, as defined by C{self.hints}.
+ """
+ return [(ip, dns.PORT) for ip in self.hints]
+
+
+ def _query(self, query, servers, timeout, filter):
+ """
+ Issue one query and return a L{Deferred} which fires with its response.
+
+ @param query: The query to issue.
+ @type query: L{dns.Query}
+
+ @param servers: The servers which might have an answer for this
+ query.
+ @type servers: L{list} of L{tuple} of L{str} and L{int}
+
+ @param timeout: A timeout on how long to wait for the response.
+ @type timeout: L{tuple} of L{int}
+
+ @param filter: A flag indicating whether to filter the results. If
+ C{True}, the returned L{Deferred} will fire with a three-tuple of
+ lists of L{RRHeaders} (like the return value of the I{lookup*}
+ methods of L{IResolver}. IF C{False}, the result will be a
+ L{Message} instance.
+ @type filter: L{bool}
+
+ @return: A L{Deferred} which fires with the response or a timeout
+ error.
+ @rtype: L{Deferred}
+ """
+ from twisted.names import client
+ r = client.Resolver(servers=servers, reactor=self._reactor)
+ d = r.queryUDP([query], timeout)
+ if filter:
+ d.addCallback(r.filterAnswers)
+ return d
+
+
+ def _lookup(self, name, cls, type, timeout):
+ """
+ Implement name lookup by recursively discovering the authoritative
+ server for the name and then asking it, starting at one of the servers
+ in C{self.hints}.
+ """
+ if timeout is None:
+ # A series of timeouts for semi-exponential backoff, summing to an
+ # arbitrary total of 60 seconds.
+ timeout = (1, 3, 11, 45)
+ return self._discoverAuthority(
+ dns.Query(name, type, cls), self._roots(), timeout,
+ self._maximumQueries)
+
+
+ def _discoverAuthority(self, query, servers, timeout, queriesLeft):
+ """
+ Issue a query to a server and follow a delegation if necessary.
+
+ @param query: The query to issue.
+ @type query: L{dns.Query}
+
+ @param servers: The servers which might have an answer for this
+ query.
+ @type servers: L{list} of L{tuple} of L{str} and L{int}
+
+ @param timeout: A C{tuple} of C{int} giving the timeout to use for this
+ query.
+
+ @param queriesLeft: A C{int} giving the number of queries which may
+ yet be attempted to answer this query before the attempt will be
+ abandoned.
+
+ @return: A L{Deferred} which fires with a three-tuple of lists of
+ L{RRHeaders} giving the response, or with a L{Failure} if there is
+ a timeout or response error.
+ """
+ # Stop now if we've hit the query limit.
+ if queriesLeft <= 0:
+ return Failure(
+ error.ResolverError("Query limit reached without result"))
+
+ d = self._query(query, servers, timeout, False)
+ d.addCallback(
+ self._discoveredAuthority, query, timeout, queriesLeft - 1)
+ return d
+
+
+ def _discoveredAuthority(self, response, query, timeout, queriesLeft):
+ """
+ Interpret the response to a query, checking for error codes and
+ following delegations if necessary.
+
+ @param response: The L{Message} received in response to issuing C{query}.
+ @type response: L{Message}
+
+ @param query: The L{dns.Query} which was issued.
+ @type query: L{dns.Query}.
+
+ @param timeout: The timeout to use if another query is indicated by
+ this response.
+ @type timeout: L{tuple} of L{int}
+
+ @param queriesLeft: A C{int} giving the number of queries which may
+ yet be attempted to answer this query before the attempt will be
+ abandoned.
+
+ @return: A L{Failure} indicating a response error, a three-tuple of
+ lists of L{RRHeaders} giving the response to C{query} or a
+ L{Deferred} which will fire with one of those.
+ """
+ if response.rCode != dns.OK:
+ return Failure(self.exceptionForCode(response.rCode)(response))
+
+ # Turn the answers into a structure that's a little easier to work with.
+ records = {}
+ for answer in response.answers:
+ records.setdefault(answer.name, []).append(answer)
+
+ def findAnswerOrCName(name, type, cls):
+ cname = None
+ for record in records.get(name, []):
+ if record.cls == cls:
+ if record.type == type:
+ return record
+ elif record.type == dns.CNAME:
+ cname = record
+ # If there were any CNAME records, return the last one. There's
+ # only supposed to be zero or one, though.
+ return cname
+
+ seen = set()
+ name = query.name
+ record = None
+ while True:
+ seen.add(name)
+ previous = record
+ record = findAnswerOrCName(name, query.type, query.cls)
+ if record is None:
+ if name == query.name:
+ # If there's no answer for the original name, then this may
+ # be a delegation. Code below handles it.
+ break
+ else:
+ # Try to resolve the CNAME with another query.
+ d = self._discoverAuthority(
+ dns.Query(str(name), query.type, query.cls),
+ self._roots(), timeout, queriesLeft)
+ # We also want to include the CNAME in the ultimate result,
+ # otherwise this will be pretty confusing.
+ def cbResolved((answers, authority, additional)):
+ answers.insert(0, previous)
+ return (answers, authority, additional)
+ d.addCallback(cbResolved)
+ return d
+ elif record.type == query.type:
+ return (
+ response.answers,
+ response.authority,
+ response.additional)
+ else:
+ # It's a CNAME record. Try to resolve it from the records
+ # in this response with another iteration around the loop.
+ if record.payload.name in seen:
+ raise error.ResolverError("Cycle in CNAME processing")
+ name = record.payload.name
+
+
+ # Build a map to use to convert NS names into IP addresses.
+ addresses = {}
+ for rr in response.additional:
+ if rr.type == dns.A:
+ addresses[str(rr.name)] = rr.payload.dottedQuad()
+
+ hints = []
+ traps = []
+ for rr in response.authority:
+ if rr.type == dns.NS:
+ ns = str(rr.payload.name)
+ if ns in addresses:
+ hints.append((addresses[ns], dns.PORT))
+ else:
+ traps.append(ns)
+ if hints:
+ return self._discoverAuthority(
+ query, hints, timeout, queriesLeft)
+ elif traps:
+ d = self.lookupAddress(traps[0], timeout)
+ d.addCallback(
+ lambda (answers, authority, additional):
+ answers[0].payload.dottedQuad())
+ d.addCallback(
+ lambda hint: self._discoverAuthority(
+ query, [(hint, dns.PORT)], timeout, queriesLeft - 1))
+ return d
+ else:
+ return Failure(error.ResolverError(
+ "Stuck at response without answers or delegation"))
+
+
+ def discoveredAuthority(self, auth, name, cls, type, timeout):
+ warnings.warn(
+ 'twisted.names.root.Resolver.discoveredAuthority is deprecated since '
+ 'Twisted 10.0. Use twisted.names.client.Resolver directly, instead.',
+ category=DeprecationWarning, stacklevel=2)
+ from twisted.names import client
+ q = dns.Query(name, type, cls)
+ r = client.Resolver(servers=[(auth, dns.PORT)])
+ d = r.queryUDP([q], timeout)
+ d.addCallback(r.filterAnswers)
+ return d
+
+
+
+def lookupNameservers(host, atServer, p=None):
+ warnings.warn(
+ 'twisted.names.root.lookupNameservers is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupNameservers instead.',
+ category=DeprecationWarning, stacklevel=2)
+ # print 'Nameserver lookup for', host, 'at', atServer, 'with', p
+ if p is None:
+ p = dns.DNSDatagramProtocol(_DummyController())
+ p.noisy = False
+ return retry(
+ (1, 3, 11, 45), # Timeouts
+ p, # Protocol instance
+ (atServer, dns.PORT), # Server to query
+ [dns.Query(host, dns.NS, dns.IN)] # Question to ask
+ )
+
+def lookupAddress(host, atServer, p=None):
+ warnings.warn(
+ 'twisted.names.root.lookupAddress is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupAddress instead.',
+ category=DeprecationWarning, stacklevel=2)
+ # print 'Address lookup for', host, 'at', atServer, 'with', p
+ if p is None:
+ p = dns.DNSDatagramProtocol(_DummyController())
+ p.noisy = False
+ return retry(
+ (1, 3, 11, 45), # Timeouts
+ p, # Protocol instance
+ (atServer, dns.PORT), # Server to query
+ [dns.Query(host, dns.A, dns.IN)] # Question to ask
+ )
+
+def extractAuthority(msg, cache):
+ warnings.warn(
+ 'twisted.names.root.extractAuthority is deprecated since Twisted '
+ '10.0. Please inspect the Message object directly.',
+ category=DeprecationWarning, stacklevel=2)
+ records = msg.answers + msg.authority + msg.additional
+ nameservers = [r for r in records if r.type == dns.NS]
+
+ # print 'Records for', soFar, ':', records
+ # print 'NS for', soFar, ':', nameservers
+
+ if not nameservers:
+ return None, nameservers
+ if not records:
+ raise IOError("No records")
+ for r in records:
+ if r.type == dns.A:
+ cache[str(r.name)] = r.payload.dottedQuad()
+ for r in records:
+ if r.type == dns.NS:
+ if str(r.payload.name) in cache:
+ return cache[str(r.payload.name)], nameservers
+ for addr in records:
+ if addr.type == dns.A and addr.name == r.name:
+ return addr.payload.dottedQuad(), nameservers
+ return None, nameservers
+
+def discoverAuthority(host, roots, cache=None, p=None):
+ warnings.warn(
+ 'twisted.names.root.discoverAuthority is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupNameservers instead.',
+ category=DeprecationWarning, stacklevel=4)
+
+ if cache is None:
+ cache = {}
+
+ rootAuths = list(roots)
+
+ parts = host.rstrip('.').split('.')
+ parts.reverse()
+
+ authority = rootAuths.pop()
+
+ soFar = ''
+ for part in parts:
+ soFar = part + '.' + soFar
+ # print '///////', soFar, authority, p
+ msg = defer.waitForDeferred(lookupNameservers(soFar, authority, p))
+ yield msg
+ msg = msg.getResult()
+
+ newAuth, nameservers = extractAuthority(msg, cache)
+
+ if newAuth is not None:
+ # print "newAuth is not None"
+ authority = newAuth
+ else:
+ if nameservers:
+ r = str(nameservers[0].payload.name)
+ # print 'Recursively discovering authority for', r
+ authority = defer.waitForDeferred(discoverAuthority(r, roots, cache, p))
+ yield authority
+ authority = authority.getResult()
+ # print 'Discovered to be', authority, 'for', r
+## else:
+## # print 'Doing address lookup for', soFar, 'at', authority
+## msg = defer.waitForDeferred(lookupAddress(soFar, authority, p))
+## yield msg
+## msg = msg.getResult()
+## records = msg.answers + msg.authority + msg.additional
+## addresses = [r for r in records if r.type == dns.A]
+## if addresses:
+## authority = addresses[0].payload.dottedQuad()
+## else:
+## raise IOError("Resolution error")
+ # print "Yielding authority", authority
+ yield authority
+
+discoverAuthority = defer.deferredGenerator(discoverAuthority)
+
+def makePlaceholder(deferred, name):
+ def placeholder(*args, **kw):
+ deferred.addCallback(lambda r: getattr(r, name)(*args, **kw))
+ return deferred
+ return placeholder
+
+class DeferredResolver:
+ def __init__(self, resolverDeferred):
+ self.waiting = []
+ resolverDeferred.addCallback(self.gotRealResolver)
+
+ def gotRealResolver(self, resolver):
+ w = self.waiting
+ self.__dict__ = resolver.__dict__
+ self.__class__ = resolver.__class__
+ for d in w:
+ d.callback(resolver)
+
+ def __getattr__(self, name):
+ if name.startswith('lookup') or name in ('getHostByName', 'query'):
+ self.waiting.append(defer.Deferred())
+ return makePlaceholder(self.waiting[-1], name)
+ raise AttributeError(name)
+
+def bootstrap(resolver):
+ """Lookup the root nameserver addresses using the given resolver
+
+ Return a Resolver which will eventually become a C{root.Resolver}
+ instance that has references to all the root servers that we were able
+ to look up.
+ """
+ domains = [chr(ord('a') + i) for i in range(13)]
+ # f = lambda r: (log.msg('Root server address: ' + str(r)), r)[1]
+ f = lambda r: r
+ L = [resolver.getHostByName('%s.root-servers.net' % d).addCallback(f) for d in domains]
+ d = defer.DeferredList(L)
+ d.addCallback(lambda r: Resolver([e[1] for e in r if e[0]]))
+ return DeferredResolver(d)
diff --git a/twisted/names/secondary.py b/twisted/names/secondary.py
new file mode 100644
index 0000000..c7c098c
--- /dev/null
+++ b/twisted/names/secondary.py
@@ -0,0 +1,179 @@
+# -*- test-case-name: twisted.names.test.test_names -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+__all__ = ['SecondaryAuthority', 'SecondaryAuthorityService']
+
+from twisted.internet import task, defer
+from twisted.names import dns
+from twisted.names import common
+from twisted.names import client
+from twisted.names import resolve
+from twisted.names.authority import FileAuthority
+
+from twisted.python import log, failure
+from twisted.application import service
+
+class SecondaryAuthorityService(service.Service):
+ calls = None
+
+ _port = 53
+
+ def __init__(self, primary, domains):
+ """
+ @param primary: The IP address of the server from which to perform
+ zone transfers.
+
+ @param domains: A sequence of domain names for which to perform
+ zone transfers.
+ """
+ self.primary = primary
+ self.domains = [SecondaryAuthority(primary, d) for d in domains]
+
+
+ @classmethod
+ def fromServerAddressAndDomains(cls, serverAddress, domains):
+ """
+ Construct a new L{SecondaryAuthorityService} from a tuple giving a
+ server address and a C{str} giving the name of a domain for which this
+ is an authority.
+
+ @param serverAddress: A two-tuple, the first element of which is a
+ C{str} giving an IP address and the second element of which is a
+ C{int} giving a port number. Together, these define where zone
+ transfers will be attempted from.
+
+ @param domain: A C{str} giving the domain to transfer.
+
+ @return: A new instance of L{SecondaryAuthorityService}.
+ """
+ service = cls(None, [])
+ service.primary = serverAddress[0]
+ service._port = serverAddress[1]
+ service.domains = [
+ SecondaryAuthority.fromServerAddressAndDomain(serverAddress, d)
+ for d in domains]
+ return service
+
+
+ def getAuthority(self):
+ return resolve.ResolverChain(self.domains)
+
+ def startService(self):
+ service.Service.startService(self)
+ self.calls = [task.LoopingCall(d.transfer) for d in self.domains]
+ i = 0
+ from twisted.internet import reactor
+ for c in self.calls:
+ # XXX Add errbacks, respect proper timeouts
+ reactor.callLater(i, c.start, 60 * 60)
+ i += 1
+
+ def stopService(self):
+ service.Service.stopService(self)
+ for c in self.calls:
+ c.stop()
+
+
+
+class SecondaryAuthority(common.ResolverBase):
+ """
+ An Authority that keeps itself updated by performing zone transfers.
+
+ @ivar primary: The IP address of the server from which zone transfers will
+ be attempted.
+ @type primary: C{str}
+
+ @ivar _port: The port number of the server from which zone transfers will be
+ attempted.
+ @type: C{int}
+
+ @ivar _reactor: The reactor to use to perform the zone transfers, or C{None}
+ to use the global reactor.
+ """
+
+ transferring = False
+ soa = records = None
+ _port = 53
+ _reactor = None
+
+ def __init__(self, primaryIP, domain):
+ common.ResolverBase.__init__(self)
+ self.primary = primaryIP
+ self.domain = domain
+
+
+ @classmethod
+ def fromServerAddressAndDomain(cls, serverAddress, domain):
+ """
+ Construct a new L{SecondaryAuthority} from a tuple giving a server
+ address and a C{str} giving the name of a domain for which this is an
+ authority.
+
+ @param serverAddress: A two-tuple, the first element of which is a
+ C{str} giving an IP address and the second element of which is a
+ C{int} giving a port number. Together, these define where zone
+ transfers will be attempted from.
+
+ @param domain: A C{str} giving the domain to transfer.
+
+ @return: A new instance of L{SecondaryAuthority}.
+ """
+ secondary = cls(None, None)
+ secondary.primary = serverAddress[0]
+ secondary._port = serverAddress[1]
+ secondary.domain = domain
+ return secondary
+
+
+ def transfer(self):
+ if self.transferring:
+ return
+ self.transfering = True
+
+ reactor = self._reactor
+ if reactor is None:
+ from twisted.internet import reactor
+
+ resolver = client.Resolver(
+ servers=[(self.primary, self._port)], reactor=reactor)
+ return resolver.lookupZone(self.domain
+ ).addCallback(self._cbZone
+ ).addErrback(self._ebZone
+ )
+
+
+ def _lookup(self, name, cls, type, timeout=None):
+ if not self.soa or not self.records:
+ return defer.fail(failure.Failure(dns.DomainError(name)))
+
+
+ return FileAuthority.__dict__['_lookup'](self, name, cls, type, timeout)
+
+ #shouldn't we just subclass? :P
+
+ lookupZone = FileAuthority.__dict__['lookupZone']
+
+ def _cbZone(self, zone):
+ ans, _, _ = zone
+ self.records = r = {}
+ for rec in ans:
+ if not self.soa and rec.type == dns.SOA:
+ self.soa = (str(rec.name).lower(), rec.payload)
+ else:
+ r.setdefault(str(rec.name).lower(), []).append(rec.payload)
+
+ def _ebZone(self, failure):
+ log.msg("Updating %s from %s failed during zone transfer" % (self.domain, self.primary))
+ log.err(failure)
+
+ def update(self):
+ self.transfer().addCallbacks(self._cbTransferred, self._ebTransferred)
+
+ def _cbTransferred(self, result):
+ self.transferring = False
+
+ def _ebTransferred(self, failure):
+ self.transferred = False
+ log.msg("Transferring %s from %s failed after zone transfer" % (self.domain, self.primary))
+ log.err(failure)
diff --git a/twisted/names/server.py b/twisted/names/server.py
new file mode 100644
index 0000000..0da6acd
--- /dev/null
+++ b/twisted/names/server.py
@@ -0,0 +1,205 @@
+# -*- test-case-name: twisted.names.test.test_names -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Async DNS server
+
+Future plans:
+ - Better config file format maybe
+ - Make sure to differentiate between different classes
+ - notice truncation bit
+
+Important: No additional processing is done on some of the record types.
+This violates the most basic RFC and is just plain annoying
+for resolvers to deal with. Fix it.
+
+@author: Jp Calderone
+"""
+
+import time
+
+from twisted.internet import protocol
+from twisted.names import dns, resolve
+from twisted.python import log
+
+
+class DNSServerFactory(protocol.ServerFactory):
+ """
+ Server factory and tracker for L{DNSProtocol} connections. This
+ class also provides records for responses to DNS queries.
+
+ @ivar connections: A list of all the connected L{DNSProtocol}
+ instances using this object as their controller.
+ @type connections: C{list} of L{DNSProtocol}
+ """
+
+ protocol = dns.DNSProtocol
+ cache = None
+
+ def __init__(self, authorities = None, caches = None, clients = None, verbose = 0):
+ resolvers = []
+ if authorities is not None:
+ resolvers.extend(authorities)
+ if caches is not None:
+ resolvers.extend(caches)
+ if clients is not None:
+ resolvers.extend(clients)
+
+ self.canRecurse = not not clients
+ self.resolver = resolve.ResolverChain(resolvers)
+ self.verbose = verbose
+ if caches:
+ self.cache = caches[-1]
+ self.connections = []
+
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self)
+ p.factory = self
+ return p
+
+
+ def connectionMade(self, protocol):
+ """
+ Track a newly connected L{DNSProtocol}.
+ """
+ self.connections.append(protocol)
+
+
+ def connectionLost(self, protocol):
+ """
+ Stop tracking a no-longer connected L{DNSProtocol}.
+ """
+ self.connections.remove(protocol)
+
+
+ def sendReply(self, protocol, message, address):
+ if self.verbose > 1:
+ s = ' '.join([str(a.payload) for a in message.answers])
+ auth = ' '.join([str(a.payload) for a in message.authority])
+ add = ' '.join([str(a.payload) for a in message.additional])
+ if not s:
+ log.msg("Replying with no answers")
+ else:
+ log.msg("Answers are " + s)
+ log.msg("Authority is " + auth)
+ log.msg("Additional is " + add)
+
+ if address is None:
+ protocol.writeMessage(message)
+ else:
+ protocol.writeMessage(message, address)
+
+ if self.verbose > 1:
+ log.msg("Processed query in %0.3f seconds" % (time.time() - message.timeReceived))
+
+
+ def gotResolverResponse(self, (ans, auth, add), protocol, message, address):
+ message.rCode = dns.OK
+ message.answers = ans
+ for x in ans:
+ if x.isAuthoritative():
+ message.auth = 1
+ break
+ message.authority = auth
+ message.additional = add
+ self.sendReply(protocol, message, address)
+
+ l = len(ans) + len(auth) + len(add)
+ if self.verbose:
+ log.msg("Lookup found %d record%s" % (l, l != 1 and "s" or ""))
+
+ if self.cache and l:
+ self.cache.cacheResult(
+ message.queries[0], (ans, auth, add)
+ )
+
+
+ def gotResolverError(self, failure, protocol, message, address):
+ if failure.check(dns.DomainError, dns.AuthoritativeDomainError):
+ message.rCode = dns.ENAME
+ else:
+ message.rCode = dns.ESERVER
+ log.err(failure)
+
+ self.sendReply(protocol, message, address)
+ if self.verbose:
+ log.msg("Lookup failed")
+
+
+ def handleQuery(self, message, protocol, address):
+ # Discard all but the first query! HOO-AAH HOOOOO-AAAAH
+ # (no other servers implement multi-query messages, so we won't either)
+ query = message.queries[0]
+
+ return self.resolver.query(query).addCallback(
+ self.gotResolverResponse, protocol, message, address
+ ).addErrback(
+ self.gotResolverError, protocol, message, address
+ )
+
+
+ def handleInverseQuery(self, message, protocol, address):
+ message.rCode = dns.ENOTIMP
+ self.sendReply(protocol, message, address)
+ if self.verbose:
+ log.msg("Inverse query from %r" % (address,))
+
+
+ def handleStatus(self, message, protocol, address):
+ message.rCode = dns.ENOTIMP
+ self.sendReply(protocol, message, address)
+ if self.verbose:
+ log.msg("Status request from %r" % (address,))
+
+
+ def handleNotify(self, message, protocol, address):
+ message.rCode = dns.ENOTIMP
+ self.sendReply(protocol, message, address)
+ if self.verbose:
+ log.msg("Notify message from %r" % (address,))
+
+
+ def handleOther(self, message, protocol, address):
+ message.rCode = dns.ENOTIMP
+ self.sendReply(protocol, message, address)
+ if self.verbose:
+ log.msg("Unknown op code (%d) from %r" % (message.opCode, address))
+
+
+ def messageReceived(self, message, proto, address = None):
+ message.timeReceived = time.time()
+
+ if self.verbose:
+ if self.verbose > 1:
+ s = ' '.join([str(q) for q in message.queries])
+ elif self.verbose > 0:
+ s = ' '.join([dns.QUERY_TYPES.get(q.type, 'UNKNOWN') for q in message.queries])
+
+ if not len(s):
+ log.msg("Empty query from %r" % ((address or proto.transport.getPeer()),))
+ else:
+ log.msg("%s query from %r" % (s, address or proto.transport.getPeer()))
+
+ message.recAv = self.canRecurse
+ message.answer = 1
+
+ if not self.allowQuery(message, proto, address):
+ message.rCode = dns.EREFUSED
+ self.sendReply(proto, message, address)
+ elif message.opCode == dns.OP_QUERY:
+ self.handleQuery(message, proto, address)
+ elif message.opCode == dns.OP_INVERSE:
+ self.handleInverseQuery(message, proto, address)
+ elif message.opCode == dns.OP_STATUS:
+ self.handleStatus(message, proto, address)
+ elif message.opCode == dns.OP_NOTIFY:
+ self.handleNotify(message, proto, address)
+ else:
+ self.handleOther(message, proto, address)
+
+
+ def allowQuery(self, message, protocol, address):
+ # Allow anything but empty queries
+ return len(message.queries)
diff --git a/twisted/names/srvconnect.py b/twisted/names/srvconnect.py
new file mode 100644
index 0000000..9bf3a82
--- /dev/null
+++ b/twisted/names/srvconnect.py
@@ -0,0 +1,186 @@
+# -*- test-case-name: twisted.names.test.test_srvconnect -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import random
+
+from zope.interface import implements
+
+from twisted.internet import error, interfaces
+
+from twisted.names import client, dns
+from twisted.names.error import DNSNameError
+from twisted.python.compat import reduce
+
+class _SRVConnector_ClientFactoryWrapper:
+ def __init__(self, connector, wrappedFactory):
+ self.__connector = connector
+ self.__wrappedFactory = wrappedFactory
+
+ def startedConnecting(self, connector):
+ self.__wrappedFactory.startedConnecting(self.__connector)
+
+ def clientConnectionFailed(self, connector, reason):
+ self.__connector.connectionFailed(reason)
+
+ def clientConnectionLost(self, connector, reason):
+ self.__connector.connectionLost(reason)
+
+ def __getattr__(self, key):
+ return getattr(self.__wrappedFactory, key)
+
+class SRVConnector:
+ """A connector that looks up DNS SRV records. See RFC2782."""
+
+ implements(interfaces.IConnector)
+
+ stopAfterDNS=0
+
+ def __init__(self, reactor, service, domain, factory,
+ protocol='tcp', connectFuncName='connectTCP',
+ connectFuncArgs=(),
+ connectFuncKwArgs={},
+ ):
+ self.reactor = reactor
+ self.service = service
+ self.domain = domain
+ self.factory = factory
+
+ self.protocol = protocol
+ self.connectFuncName = connectFuncName
+ self.connectFuncArgs = connectFuncArgs
+ self.connectFuncKwArgs = connectFuncKwArgs
+
+ self.connector = None
+ self.servers = None
+ self.orderedServers = None # list of servers already used in this round
+
+ def connect(self):
+ """Start connection to remote server."""
+ self.factory.doStart()
+ self.factory.startedConnecting(self)
+
+ if not self.servers:
+ if self.domain is None:
+ self.connectionFailed(error.DNSLookupError("Domain is not defined."))
+ return
+ d = client.lookupService('_%s._%s.%s' % (self.service,
+ self.protocol,
+ self.domain))
+ d.addCallbacks(self._cbGotServers, self._ebGotServers)
+ d.addCallback(lambda x, self=self: self._reallyConnect())
+ d.addErrback(self.connectionFailed)
+ elif self.connector is None:
+ self._reallyConnect()
+ else:
+ self.connector.connect()
+
+ def _ebGotServers(self, failure):
+ failure.trap(DNSNameError)
+
+ # Some DNS servers reply with NXDOMAIN when in fact there are
+ # just no SRV records for that domain. Act as if we just got an
+ # empty response and use fallback.
+
+ self.servers = []
+ self.orderedServers = []
+
+ def _cbGotServers(self, (answers, auth, add)):
+ if len(answers) == 1 and answers[0].type == dns.SRV \
+ and answers[0].payload \
+ and answers[0].payload.target == dns.Name('.'):
+ # decidedly not available
+ raise error.DNSLookupError("Service %s not available for domain %s."
+ % (repr(self.service), repr(self.domain)))
+
+ self.servers = []
+ self.orderedServers = []
+ for a in answers:
+ if a.type != dns.SRV or not a.payload:
+ continue
+
+ self.orderedServers.append((a.payload.priority, a.payload.weight,
+ str(a.payload.target), a.payload.port))
+
+ def _serverCmp(self, a, b):
+ if a[0]!=b[0]:
+ return cmp(a[0], b[0])
+ else:
+ return cmp(a[1], b[1])
+
+ def pickServer(self):
+ assert self.servers is not None
+ assert self.orderedServers is not None
+
+ if not self.servers and not self.orderedServers:
+ # no SRV record, fall back..
+ return self.domain, self.service
+
+ if not self.servers and self.orderedServers:
+ # start new round
+ self.servers = self.orderedServers
+ self.orderedServers = []
+
+ assert self.servers
+
+ self.servers.sort(self._serverCmp)
+ minPriority=self.servers[0][0]
+
+ weightIndex = zip(xrange(len(self.servers)), [x[1] for x in self.servers
+ if x[0]==minPriority])
+ weightSum = reduce(lambda x, y: (None, x[1]+y[1]), weightIndex, (None, 0))[1]
+ rand = random.randint(0, weightSum)
+
+ for index, weight in weightIndex:
+ weightSum -= weight
+ if weightSum <= 0:
+ chosen = self.servers[index]
+ del self.servers[index]
+ self.orderedServers.append(chosen)
+
+ p, w, host, port = chosen
+ return host, port
+
+ raise RuntimeError, 'Impossible %s pickServer result.' % self.__class__.__name__
+
+ def _reallyConnect(self):
+ if self.stopAfterDNS:
+ self.stopAfterDNS=0
+ return
+
+ self.host, self.port = self.pickServer()
+ assert self.host is not None, 'Must have a host to connect to.'
+ assert self.port is not None, 'Must have a port to connect to.'
+
+ connectFunc = getattr(self.reactor, self.connectFuncName)
+ self.connector=connectFunc(
+ self.host, self.port,
+ _SRVConnector_ClientFactoryWrapper(self, self.factory),
+ *self.connectFuncArgs, **self.connectFuncKwArgs)
+
+ def stopConnecting(self):
+ """Stop attempting to connect."""
+ if self.connector:
+ self.connector.stopConnecting()
+ else:
+ self.stopAfterDNS=1
+
+ def disconnect(self):
+ """Disconnect whatever our are state is."""
+ if self.connector is not None:
+ self.connector.disconnect()
+ else:
+ self.stopConnecting()
+
+ def getDestination(self):
+ assert self.connector
+ return self.connector.getDestination()
+
+ def connectionFailed(self, reason):
+ self.factory.clientConnectionFailed(self, reason)
+ self.factory.doStop()
+
+ def connectionLost(self, reason):
+ self.factory.clientConnectionLost(self, reason)
+ self.factory.doStop()
+
diff --git a/twisted/names/tap.py b/twisted/names/tap.py
new file mode 100644
index 0000000..d0e3b1d
--- /dev/null
+++ b/twisted/names/tap.py
@@ -0,0 +1,150 @@
+# -*- test-case-name: twisted.names.test.test_tap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Domain Name Server
+"""
+
+import os, traceback
+
+from twisted.python import usage
+from twisted.names import dns
+from twisted.application import internet, service
+
+from twisted.names import server
+from twisted.names import authority
+from twisted.names import secondary
+
+class Options(usage.Options):
+ optParameters = [
+ ["interface", "i", "", "The interface to which to bind"],
+ ["port", "p", "53", "The port on which to listen"],
+ ["resolv-conf", None, None,
+ "Override location of resolv.conf (implies --recursive)"],
+ ["hosts-file", None, None, "Perform lookups with a hosts file"],
+ ]
+
+ optFlags = [
+ ["cache", "c", "Enable record caching"],
+ ["recursive", "r", "Perform recursive lookups"],
+ ["verbose", "v", "Log verbosely"],
+ ]
+
+ compData = usage.Completions(
+ optActions={"interface" : usage.CompleteNetInterfaces()}
+ )
+
+ zones = None
+ zonefiles = None
+
+ def __init__(self):
+ usage.Options.__init__(self)
+ self['verbose'] = 0
+ self.bindfiles = []
+ self.zonefiles = []
+ self.secondaries = []
+
+
+ def opt_pyzone(self, filename):
+ """Specify the filename of a Python syntax zone definition"""
+ if not os.path.exists(filename):
+ raise usage.UsageError(filename + ": No such file")
+ self.zonefiles.append(filename)
+
+ def opt_bindzone(self, filename):
+ """Specify the filename of a BIND9 syntax zone definition"""
+ if not os.path.exists(filename):
+ raise usage.UsageError(filename + ": No such file")
+ self.bindfiles.append(filename)
+
+
+ def opt_secondary(self, ip_domain):
+ """Act as secondary for the specified domain, performing
+ zone transfers from the specified IP (IP/domain)
+ """
+ args = ip_domain.split('/', 1)
+ if len(args) != 2:
+ raise usage.UsageError("Argument must be of the form IP[:port]/domain")
+ address = args[0].split(':')
+ if len(address) == 1:
+ address = (address[0], dns.PORT)
+ else:
+ try:
+ port = int(address[1])
+ except ValueError:
+ raise usage.UsageError(
+ "Specify an integer port number, not %r" % (address[1],))
+ address = (address[0], port)
+ self.secondaries.append((address, [args[1]]))
+
+
+ def opt_verbose(self):
+ """Increment verbosity level"""
+ self['verbose'] += 1
+
+
+ def postOptions(self):
+ if self['resolv-conf']:
+ self['recursive'] = True
+
+ self.svcs = []
+ self.zones = []
+ for f in self.zonefiles:
+ try:
+ self.zones.append(authority.PySourceAuthority(f))
+ except Exception:
+ traceback.print_exc()
+ raise usage.UsageError("Invalid syntax in " + f)
+ for f in self.bindfiles:
+ try:
+ self.zones.append(authority.BindAuthority(f))
+ except Exception:
+ traceback.print_exc()
+ raise usage.UsageError("Invalid syntax in " + f)
+ for f in self.secondaries:
+ svc = secondary.SecondaryAuthorityService.fromServerAddressAndDomains(*f)
+ self.svcs.append(svc)
+ self.zones.append(self.svcs[-1].getAuthority())
+ try:
+ self['port'] = int(self['port'])
+ except ValueError:
+ raise usage.UsageError("Invalid port: %r" % (self['port'],))
+
+
+def _buildResolvers(config):
+ """
+ Build DNS resolver instances in an order which leaves recursive
+ resolving as a last resort.
+
+ @type config: L{Options} instance
+ @param config: Parsed command-line configuration
+
+ @return: Two-item tuple of a list of cache resovers and a list of client
+ resolvers
+ """
+ from twisted.names import client, cache, hosts
+
+ ca, cl = [], []
+ if config['cache']:
+ ca.append(cache.CacheResolver(verbose=config['verbose']))
+ if config['hosts-file']:
+ cl.append(hosts.Resolver(file=config['hosts-file']))
+ if config['recursive']:
+ cl.append(client.createResolver(resolvconf=config['resolv-conf']))
+ return ca, cl
+
+
+def makeService(config):
+ ca, cl = _buildResolvers(config)
+
+ f = server.DNSServerFactory(config.zones, ca, cl, config['verbose'])
+ p = dns.DNSDatagramProtocol(f)
+ f.noisy = 0
+ ret = service.MultiService()
+ for (klass, arg) in [(internet.TCPServer, f), (internet.UDPServer, p)]:
+ s = klass(config['port'], arg, interface=config['interface'])
+ s.setServiceParent(ret)
+ for svc in config.svcs:
+ svc.setServiceParent(ret)
+ return ret
diff --git a/twisted/names/test/__init__.py b/twisted/names/test/__init__.py
new file mode 100644
index 0000000..f6b7e3a
--- /dev/null
+++ b/twisted/names/test/__init__.py
@@ -0,0 +1 @@
+"Tests for twisted.names"
diff --git a/twisted/names/test/test_cache.py b/twisted/names/test/test_cache.py
new file mode 100644
index 0000000..9ab867e
--- /dev/null
+++ b/twisted/names/test/test_cache.py
@@ -0,0 +1,109 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import time
+
+from twisted.trial import unittest
+
+from twisted.names import dns, cache
+from twisted.internet import task
+
+class Caching(unittest.TestCase):
+ """
+ Tests for L{cache.CacheResolver}.
+ """
+
+ def test_lookup(self):
+ c = cache.CacheResolver({
+ dns.Query(name='example.com', type=dns.MX, cls=dns.IN):
+ (time.time(), ([], [], []))})
+ return c.lookupMailExchange('example.com').addCallback(
+ self.assertEqual, ([], [], []))
+
+
+ def test_constructorExpires(self):
+ """
+ Cache entries passed into L{cache.CacheResolver.__init__} get
+ cancelled just like entries added with cacheResult
+ """
+
+ r = ([dns.RRHeader("example.com", dns.A, dns.IN, 60,
+ dns.Record_A("127.0.0.1", 60))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 50,
+ dns.Record_A("127.0.0.1", 50))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 40,
+ dns.Record_A("127.0.0.1", 40))])
+
+ clock = task.Clock()
+ query = dns.Query(name="example.com", type=dns.A, cls=dns.IN)
+
+ c = cache.CacheResolver({ query : (clock.seconds(), r)}, reactor=clock)
+
+ # 40 seconds is enough to expire the entry because expiration is based
+ # on the minimum TTL.
+ clock.advance(40)
+
+ self.assertNotIn(query, c.cache)
+
+ return self.assertFailure(
+ c.lookupAddress("example.com"), dns.DomainError)
+
+
+ def test_normalLookup(self):
+ """
+ When a cache lookup finds a cached entry from 1 second ago, it is
+ returned with a TTL of original TTL minus the elapsed 1 second.
+ """
+ r = ([dns.RRHeader("example.com", dns.A, dns.IN, 60,
+ dns.Record_A("127.0.0.1", 60))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 50,
+ dns.Record_A("127.0.0.1", 50))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 40,
+ dns.Record_A("127.0.0.1", 40))])
+
+ clock = task.Clock()
+
+ c = cache.CacheResolver(reactor=clock)
+ c.cacheResult(dns.Query(name="example.com", type=dns.A, cls=dns.IN), r)
+
+ clock.advance(1)
+
+ def cbLookup(result):
+ self.assertEquals(result[0][0].ttl, 59)
+ self.assertEquals(result[1][0].ttl, 49)
+ self.assertEquals(result[2][0].ttl, 39)
+ self.assertEquals(result[0][0].name.name, "example.com")
+
+ return c.lookupAddress("example.com").addCallback(cbLookup)
+
+
+ def test_negativeTTLLookup(self):
+ """
+ When the cache is queried exactly as the cached entry should expire
+ but before it has actually been cleared, the TTL will be 0, not
+ negative.
+ """
+ r = ([dns.RRHeader("example.com", dns.A, dns.IN, 60,
+ dns.Record_A("127.0.0.1", 60))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 50,
+ dns.Record_A("127.0.0.1", 50))],
+ [dns.RRHeader("example.com", dns.A, dns.IN, 40,
+ dns.Record_A("127.0.0.1", 40))])
+
+ clock = task.Clock()
+ # Make sure timeouts never happen, so entries won't get cleared:
+ clock.callLater = lambda *args, **kwargs: None
+
+ c = cache.CacheResolver({
+ dns.Query(name="example.com", type=dns.A, cls=dns.IN) :
+ (clock.seconds(), r)}, reactor=clock)
+
+ clock.advance(60.1)
+
+ def cbLookup(result):
+ self.assertEquals(result[0][0].ttl, 0)
+ self.assertEquals(result[0][0].ttl, 0)
+ self.assertEquals(result[0][0].ttl, 0)
+ self.assertEquals(result[0][0].name.name, "example.com")
+
+ return c.lookupAddress("example.com").addCallback(cbLookup)
diff --git a/twisted/names/test/test_client.py b/twisted/names/test/test_client.py
new file mode 100644
index 0000000..d2ed09c
--- /dev/null
+++ b/twisted/names/test/test_client.py
@@ -0,0 +1,678 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for L{twisted.names.client}.
+"""
+
+from twisted.names import client, dns
+from twisted.names.error import DNSQueryTimeoutError
+from twisted.trial import unittest
+from twisted.names.common import ResolverBase
+from twisted.internet import defer, error
+from twisted.python import failure
+from twisted.python.deprecate import getWarningMethod, setWarningMethod
+from twisted.python.compat import set
+
+
+class FakeResolver(ResolverBase):
+
+ def _lookup(self, name, cls, qtype, timeout):
+ """
+ The getHostByNameTest does a different type of query that requires it
+ return an A record from an ALL_RECORDS lookup, so we accomodate that
+ here.
+ """
+ if name == 'getHostByNameTest':
+ rr = dns.RRHeader(name=name, type=dns.A, cls=cls, ttl=60,
+ payload=dns.Record_A(address='127.0.0.1', ttl=60))
+ else:
+ rr = dns.RRHeader(name=name, type=qtype, cls=cls, ttl=60)
+
+ results = [rr]
+ authority = []
+ addtional = []
+ return defer.succeed((results, authority, addtional))
+
+
+
+class StubPort(object):
+ """
+ A partial implementation of L{IListeningPort} which only keeps track of
+ whether it has been stopped.
+
+ @ivar disconnected: A C{bool} which is C{False} until C{stopListening} is
+ called, C{True} afterwards.
+ """
+ disconnected = False
+
+ def stopListening(self):
+ self.disconnected = True
+
+
+
+class StubDNSDatagramProtocol(object):
+ """
+ L{dns.DNSDatagramProtocol}-alike.
+
+ @ivar queries: A C{list} of tuples giving the arguments passed to
+ C{query} along with the L{defer.Deferred} which was returned from
+ the call.
+ """
+ def __init__(self):
+ self.queries = []
+ self.transport = StubPort()
+
+
+ def query(self, address, queries, timeout=10, id=None):
+ """
+ Record the given arguments and return a Deferred which will not be
+ called back by this code.
+ """
+ result = defer.Deferred()
+ self.queries.append((address, queries, timeout, id, result))
+ return result
+
+
+
+class ResolverTests(unittest.TestCase):
+ """
+ Tests for L{client.Resolver}.
+ """
+ def test_resolverProtocol(self):
+ """
+ Reading L{client.Resolver.protocol} causes a deprecation warning to be
+ emitted and evaluates to an instance of L{DNSDatagramProtocol}.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ self.addCleanup(setWarningMethod, getWarningMethod())
+ warnings = []
+ setWarningMethod(
+ lambda message, category, stacklevel:
+ warnings.append((message, category, stacklevel)))
+ protocol = resolver.protocol
+ self.assertIsInstance(protocol, dns.DNSDatagramProtocol)
+ self.assertEqual(
+ warnings, [("Resolver.protocol is deprecated; use "
+ "Resolver.queryUDP instead.",
+ PendingDeprecationWarning, 0)])
+ self.assertIdentical(protocol, resolver.protocol)
+
+
+ def test_datagramQueryServerOrder(self):
+ """
+ L{client.Resolver.queryUDP} should issue queries to its
+ L{dns.DNSDatagramProtocol} with server addresses taken from its own
+ C{servers} and C{dynServers} lists, proceeding through them in order
+ as L{DNSQueryTimeoutError}s occur.
+ """
+ protocol = StubDNSDatagramProtocol()
+
+ servers = [object(), object()]
+ dynServers = [object(), object()]
+ resolver = client.Resolver(servers=servers)
+ resolver.dynServers = dynServers
+ resolver.protocol = protocol
+
+ expectedResult = object()
+ queryResult = resolver.queryUDP(None)
+ queryResult.addCallback(self.assertEqual, expectedResult)
+
+ self.assertEqual(len(protocol.queries), 1)
+ self.assertIdentical(protocol.queries[0][0], servers[0])
+ protocol.queries[0][-1].errback(DNSQueryTimeoutError(0))
+ self.assertEqual(len(protocol.queries), 2)
+ self.assertIdentical(protocol.queries[1][0], servers[1])
+ protocol.queries[1][-1].errback(DNSQueryTimeoutError(1))
+ self.assertEqual(len(protocol.queries), 3)
+ self.assertIdentical(protocol.queries[2][0], dynServers[0])
+ protocol.queries[2][-1].errback(DNSQueryTimeoutError(2))
+ self.assertEqual(len(protocol.queries), 4)
+ self.assertIdentical(protocol.queries[3][0], dynServers[1])
+ protocol.queries[3][-1].callback(expectedResult)
+
+ return queryResult
+
+
+ def test_singleConcurrentRequest(self):
+ """
+ L{client.Resolver.query} only issues one request at a time per query.
+ Subsequent requests made before responses to prior ones are received
+ are queued and given the same response as is given to the first one.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ resolver.protocol = StubDNSDatagramProtocol()
+ queries = resolver.protocol.queries
+
+ query = dns.Query('foo.example.com', dns.A, dns.IN)
+ # The first query should be passed to the underlying protocol.
+ firstResult = resolver.query(query)
+ self.assertEqual(len(queries), 1)
+
+ # The same query again should not be passed to the underlying protocol.
+ secondResult = resolver.query(query)
+ self.assertEqual(len(queries), 1)
+
+ # The response to the first query should be sent in response to both
+ # queries.
+ answer = object()
+ response = dns.Message()
+ response.answers.append(answer)
+ queries.pop()[-1].callback(response)
+
+ d = defer.gatherResults([firstResult, secondResult])
+ def cbFinished((firstResponse, secondResponse)):
+ self.assertEqual(firstResponse, ([answer], [], []))
+ self.assertEqual(secondResponse, ([answer], [], []))
+ d.addCallback(cbFinished)
+ return d
+
+
+ def test_multipleConcurrentRequests(self):
+ """
+ L{client.Resolver.query} issues a request for each different concurrent
+ query.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ resolver.protocol = StubDNSDatagramProtocol()
+ queries = resolver.protocol.queries
+
+ # The first query should be passed to the underlying protocol.
+ firstQuery = dns.Query('foo.example.com', dns.A)
+ resolver.query(firstQuery)
+ self.assertEqual(len(queries), 1)
+
+ # A query for a different name is also passed to the underlying
+ # protocol.
+ secondQuery = dns.Query('bar.example.com', dns.A)
+ resolver.query(secondQuery)
+ self.assertEqual(len(queries), 2)
+
+ # A query for a different type is also passed to the underlying
+ # protocol.
+ thirdQuery = dns.Query('foo.example.com', dns.A6)
+ resolver.query(thirdQuery)
+ self.assertEqual(len(queries), 3)
+
+
+ def test_multipleSequentialRequests(self):
+ """
+ After a response is received to a query issued with
+ L{client.Resolver.query}, another query with the same parameters
+ results in a new network request.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ resolver.protocol = StubDNSDatagramProtocol()
+ queries = resolver.protocol.queries
+
+ query = dns.Query('foo.example.com', dns.A)
+
+ # The first query should be passed to the underlying protocol.
+ resolver.query(query)
+ self.assertEqual(len(queries), 1)
+
+ # Deliver the response.
+ queries.pop()[-1].callback(dns.Message())
+
+ # Repeating the first query should touch the protocol again.
+ resolver.query(query)
+ self.assertEqual(len(queries), 1)
+
+
+ def test_multipleConcurrentFailure(self):
+ """
+ If the result of a request is an error response, the Deferreds for all
+ concurrently issued requests associated with that result fire with the
+ L{Failure}.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ resolver.protocol = StubDNSDatagramProtocol()
+ queries = resolver.protocol.queries
+
+ query = dns.Query('foo.example.com', dns.A)
+ firstResult = resolver.query(query)
+ secondResult = resolver.query(query)
+
+ class ExpectedException(Exception):
+ pass
+
+ queries.pop()[-1].errback(failure.Failure(ExpectedException()))
+
+ return defer.gatherResults([
+ self.assertFailure(firstResult, ExpectedException),
+ self.assertFailure(secondResult, ExpectedException)])
+
+
+ def test_connectedProtocol(self):
+ """
+ L{client.Resolver._connectedProtocol} returns a new
+ L{DNSDatagramProtocol} connected to a new address with a
+ cryptographically secure random port number.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ firstProto = resolver._connectedProtocol()
+ secondProto = resolver._connectedProtocol()
+
+ self.assertNotIdentical(firstProto.transport, None)
+ self.assertNotIdentical(secondProto.transport, None)
+ self.assertNotEqual(
+ firstProto.transport.getHost().port,
+ secondProto.transport.getHost().port)
+
+ return defer.gatherResults([
+ defer.maybeDeferred(firstProto.transport.stopListening),
+ defer.maybeDeferred(secondProto.transport.stopListening)])
+
+
+ def test_differentProtocol(self):
+ """
+ L{client.Resolver._connectedProtocol} is called once each time a UDP
+ request needs to be issued and the resulting protocol instance is used
+ for that request.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocols = []
+
+ class FakeProtocol(object):
+ def __init__(self):
+ self.transport = StubPort()
+
+ def query(self, address, query, timeout=10, id=None):
+ protocols.append(self)
+ return defer.succeed(dns.Message())
+
+ resolver._connectedProtocol = FakeProtocol
+ resolver.query(dns.Query('foo.example.com'))
+ resolver.query(dns.Query('bar.example.com'))
+ self.assertEqual(len(set(protocols)), 2)
+
+
+ def test_disallowedPort(self):
+ """
+ If a port number is initially selected which cannot be bound, the
+ L{CannotListenError} is handled and another port number is attempted.
+ """
+ ports = []
+
+ class FakeReactor(object):
+ def listenUDP(self, port, *args):
+ ports.append(port)
+ if len(ports) == 1:
+ raise error.CannotListenError(None, port, None)
+
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ resolver._reactor = FakeReactor()
+
+ proto = resolver._connectedProtocol()
+ self.assertEqual(len(set(ports)), 2)
+
+
+ def test_differentProtocolAfterTimeout(self):
+ """
+ When a query issued by L{client.Resolver.query} times out, the retry
+ uses a new protocol instance.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocols = []
+ results = [defer.fail(failure.Failure(DNSQueryTimeoutError(None))),
+ defer.succeed(dns.Message())]
+
+ class FakeProtocol(object):
+ def __init__(self):
+ self.transport = StubPort()
+
+ def query(self, address, query, timeout=10, id=None):
+ protocols.append(self)
+ return results.pop(0)
+
+ resolver._connectedProtocol = FakeProtocol
+ resolver.query(dns.Query('foo.example.com'))
+ self.assertEqual(len(set(protocols)), 2)
+
+
+ def test_protocolShutDown(self):
+ """
+ After the L{Deferred} returned by L{DNSDatagramProtocol.query} is
+ called back, the L{DNSDatagramProtocol} is disconnected from its
+ transport.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocols = []
+ result = defer.Deferred()
+
+ class FakeProtocol(object):
+ def __init__(self):
+ self.transport = StubPort()
+
+ def query(self, address, query, timeout=10, id=None):
+ protocols.append(self)
+ return result
+
+ resolver._connectedProtocol = FakeProtocol
+ resolver.query(dns.Query('foo.example.com'))
+
+ self.assertFalse(protocols[0].transport.disconnected)
+ result.callback(dns.Message())
+ self.assertTrue(protocols[0].transport.disconnected)
+
+
+ def test_protocolShutDownAfterTimeout(self):
+ """
+ The L{DNSDatagramProtocol} created when an interim timeout occurs is
+ also disconnected from its transport after the Deferred returned by its
+ query method completes.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocols = []
+ result = defer.Deferred()
+ results = [defer.fail(failure.Failure(DNSQueryTimeoutError(None))),
+ result]
+
+ class FakeProtocol(object):
+ def __init__(self):
+ self.transport = StubPort()
+
+ def query(self, address, query, timeout=10, id=None):
+ protocols.append(self)
+ return results.pop(0)
+
+ resolver._connectedProtocol = FakeProtocol
+ resolver.query(dns.Query('foo.example.com'))
+
+ self.assertFalse(protocols[1].transport.disconnected)
+ result.callback(dns.Message())
+ self.assertTrue(protocols[1].transport.disconnected)
+
+
+ def test_protocolShutDownAfterFailure(self):
+ """
+ If the L{Deferred} returned by L{DNSDatagramProtocol.query} fires with
+ a failure, the L{DNSDatagramProtocol} is still disconnected from its
+ transport.
+ """
+ class ExpectedException(Exception):
+ pass
+
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocols = []
+ result = defer.Deferred()
+
+ class FakeProtocol(object):
+ def __init__(self):
+ self.transport = StubPort()
+
+ def query(self, address, query, timeout=10, id=None):
+ protocols.append(self)
+ return result
+
+ resolver._connectedProtocol = FakeProtocol
+ queryResult = resolver.query(dns.Query('foo.example.com'))
+
+ self.assertFalse(protocols[0].transport.disconnected)
+ result.errback(failure.Failure(ExpectedException()))
+ self.assertTrue(protocols[0].transport.disconnected)
+
+ return self.assertFailure(queryResult, ExpectedException)
+
+
+ def test_tcpDisconnectRemovesFromConnections(self):
+ """
+ When a TCP DNS protocol associated with a Resolver disconnects, it is
+ removed from the Resolver's connection list.
+ """
+ resolver = client.Resolver(servers=[('example.com', 53)])
+ protocol = resolver.factory.buildProtocol(None)
+ protocol.makeConnection(None)
+ self.assertIn(protocol, resolver.connections)
+
+ # Disconnecting should remove the protocol from the connection list:
+ protocol.connectionLost(None)
+ self.assertNotIn(protocol, resolver.connections)
+
+
+
+class ClientTestCase(unittest.TestCase):
+
+ def setUp(self):
+ """
+ Replace the resolver with a FakeResolver
+ """
+ client.theResolver = FakeResolver()
+ self.hostname = 'example.com'
+ self.ghbntest = 'getHostByNameTest'
+
+ def tearDown(self):
+ """
+ By setting the resolver to None, it will be recreated next time a name
+ lookup is done.
+ """
+ client.theResolver = None
+
+ def checkResult(self, (results, authority, additional), qtype):
+ """
+ Verify that the result is the same query type as what is expected.
+ """
+ result = results[0]
+ self.assertEqual(str(result.name), self.hostname)
+ self.assertEqual(result.type, qtype)
+
+ def checkGetHostByName(self, result):
+ """
+ Test that the getHostByName query returns the 127.0.0.1 address.
+ """
+ self.assertEqual(result, '127.0.0.1')
+
+ def test_getHostByName(self):
+ """
+ do a getHostByName of a value that should return 127.0.0.1.
+ """
+ d = client.getHostByName(self.ghbntest)
+ d.addCallback(self.checkGetHostByName)
+ return d
+
+ def test_lookupAddress(self):
+ """
+ Do a lookup and test that the resolver will issue the correct type of
+ query type. We do this by checking that FakeResolver returns a result
+ record with the same query type as what we issued.
+ """
+ d = client.lookupAddress(self.hostname)
+ d.addCallback(self.checkResult, dns.A)
+ return d
+
+ def test_lookupIPV6Address(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupIPV6Address(self.hostname)
+ d.addCallback(self.checkResult, dns.AAAA)
+ return d
+
+ def test_lookupAddress6(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupAddress6(self.hostname)
+ d.addCallback(self.checkResult, dns.A6)
+ return d
+
+ def test_lookupNameservers(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupNameservers(self.hostname)
+ d.addCallback(self.checkResult, dns.NS)
+ return d
+
+ def test_lookupCanonicalName(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupCanonicalName(self.hostname)
+ d.addCallback(self.checkResult, dns.CNAME)
+ return d
+
+ def test_lookupAuthority(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupAuthority(self.hostname)
+ d.addCallback(self.checkResult, dns.SOA)
+ return d
+
+ def test_lookupMailBox(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupMailBox(self.hostname)
+ d.addCallback(self.checkResult, dns.MB)
+ return d
+
+ def test_lookupMailGroup(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupMailGroup(self.hostname)
+ d.addCallback(self.checkResult, dns.MG)
+ return d
+
+ def test_lookupMailRename(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupMailRename(self.hostname)
+ d.addCallback(self.checkResult, dns.MR)
+ return d
+
+ def test_lookupNull(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupNull(self.hostname)
+ d.addCallback(self.checkResult, dns.NULL)
+ return d
+
+ def test_lookupWellKnownServices(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupWellKnownServices(self.hostname)
+ d.addCallback(self.checkResult, dns.WKS)
+ return d
+
+ def test_lookupPointer(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupPointer(self.hostname)
+ d.addCallback(self.checkResult, dns.PTR)
+ return d
+
+ def test_lookupHostInfo(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupHostInfo(self.hostname)
+ d.addCallback(self.checkResult, dns.HINFO)
+ return d
+
+ def test_lookupMailboxInfo(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupMailboxInfo(self.hostname)
+ d.addCallback(self.checkResult, dns.MINFO)
+ return d
+
+ def test_lookupMailExchange(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupMailExchange(self.hostname)
+ d.addCallback(self.checkResult, dns.MX)
+ return d
+
+ def test_lookupText(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupText(self.hostname)
+ d.addCallback(self.checkResult, dns.TXT)
+ return d
+
+ def test_lookupSenderPolicy(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupSenderPolicy(self.hostname)
+ d.addCallback(self.checkResult, dns.SPF)
+ return d
+
+ def test_lookupResponsibility(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupResponsibility(self.hostname)
+ d.addCallback(self.checkResult, dns.RP)
+ return d
+
+ def test_lookupAFSDatabase(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupAFSDatabase(self.hostname)
+ d.addCallback(self.checkResult, dns.AFSDB)
+ return d
+
+ def test_lookupService(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupService(self.hostname)
+ d.addCallback(self.checkResult, dns.SRV)
+ return d
+
+ def test_lookupZone(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupZone(self.hostname)
+ d.addCallback(self.checkResult, dns.AXFR)
+ return d
+
+ def test_lookupAllRecords(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupAllRecords(self.hostname)
+ d.addCallback(self.checkResult, dns.ALL_RECORDS)
+ return d
+
+
+ def test_lookupNamingAuthorityPointer(self):
+ """
+ See L{test_lookupAddress}
+ """
+ d = client.lookupNamingAuthorityPointer(self.hostname)
+ d.addCallback(self.checkResult, dns.NAPTR)
+ return d
+
+
+class ThreadedResolverTests(unittest.TestCase):
+ """
+ Tests for L{client.ThreadedResolver}.
+ """
+ def test_deprecated(self):
+ """
+ L{client.ThreadedResolver} is deprecated. Instantiating it emits a
+ deprecation warning pointing at the code that does the instantiation.
+ """
+ client.ThreadedResolver()
+ warnings = self.flushWarnings(offendingFunctions=[self.test_deprecated])
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.names.client.ThreadedResolver is deprecated since "
+ "Twisted 9.0, use twisted.internet.base.ThreadedResolver "
+ "instead.")
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(len(warnings), 1)
diff --git a/twisted/names/test/test_common.py b/twisted/names/test/test_common.py
new file mode 100644
index 0000000..b44e206
--- /dev/null
+++ b/twisted/names/test/test_common.py
@@ -0,0 +1,71 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.names.common}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.names.common import ResolverBase
+from twisted.names.dns import EFORMAT, ESERVER, ENAME, ENOTIMP, EREFUSED
+from twisted.names.error import DNSFormatError, DNSServerError, DNSNameError
+from twisted.names.error import DNSNotImplementedError, DNSQueryRefusedError
+from twisted.names.error import DNSUnknownError
+
+
+class ExceptionForCodeTests(TestCase):
+ """
+ Tests for L{ResolverBase.exceptionForCode}.
+ """
+ def setUp(self):
+ self.exceptionForCode = ResolverBase().exceptionForCode
+
+
+ def test_eformat(self):
+ """
+ L{ResolverBase.exceptionForCode} converts L{EFORMAT} to
+ L{DNSFormatError}.
+ """
+ self.assertIdentical(self.exceptionForCode(EFORMAT), DNSFormatError)
+
+
+ def test_eserver(self):
+ """
+ L{ResolverBase.exceptionForCode} converts L{ESERVER} to
+ L{DNSServerError}.
+ """
+ self.assertIdentical(self.exceptionForCode(ESERVER), DNSServerError)
+
+
+ def test_ename(self):
+ """
+ L{ResolverBase.exceptionForCode} converts L{ENAME} to L{DNSNameError}.
+ """
+ self.assertIdentical(self.exceptionForCode(ENAME), DNSNameError)
+
+
+ def test_enotimp(self):
+ """
+ L{ResolverBase.exceptionForCode} converts L{ENOTIMP} to
+ L{DNSNotImplementedError}.
+ """
+ self.assertIdentical(
+ self.exceptionForCode(ENOTIMP), DNSNotImplementedError)
+
+
+ def test_erefused(self):
+ """
+ L{ResolverBase.exceptionForCode} converts L{EREFUSED} to
+ L{DNSQueryRefusedError}.
+ """
+ self.assertIdentical(
+ self.exceptionForCode(EREFUSED), DNSQueryRefusedError)
+
+
+ def test_other(self):
+ """
+ L{ResolverBase.exceptionForCode} converts any other response code to
+ L{DNSUnknownError}.
+ """
+ self.assertIdentical(
+ self.exceptionForCode(object()), DNSUnknownError)
diff --git a/twisted/names/test/test_dns.py b/twisted/names/test/test_dns.py
new file mode 100644
index 0000000..92b13e3
--- /dev/null
+++ b/twisted/names/test/test_dns.py
@@ -0,0 +1,1485 @@
+# test-case-name: twisted.names.test.test_dns
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for twisted.names.dns.
+"""
+
+from cStringIO import StringIO
+
+import struct
+
+from twisted.python.failure import Failure
+from twisted.internet import address, task
+from twisted.internet.error import CannotListenError, ConnectionDone
+from twisted.trial import unittest
+from twisted.names import dns
+
+from twisted.test import proto_helpers
+
+RECORD_TYPES = [
+ dns.Record_NS, dns.Record_MD, dns.Record_MF, dns.Record_CNAME,
+ dns.Record_MB, dns.Record_MG, dns.Record_MR, dns.Record_PTR,
+ dns.Record_DNAME, dns.Record_A, dns.Record_SOA, dns.Record_NULL,
+ dns.Record_WKS, dns.Record_SRV, dns.Record_AFSDB, dns.Record_RP,
+ dns.Record_HINFO, dns.Record_MINFO, dns.Record_MX, dns.Record_TXT,
+ dns.Record_AAAA, dns.Record_A6, dns.Record_NAPTR, dns.UnknownRecord,
+ ]
+
+class NameTests(unittest.TestCase):
+ """
+ Tests for L{Name}, the representation of a single domain name with support
+ for encoding into and decoding from DNS message format.
+ """
+ def test_decode(self):
+ """
+ L{Name.decode} populates the L{Name} instance with name information read
+ from the file-like object passed to it.
+ """
+ n = dns.Name()
+ n.decode(StringIO("\x07example\x03com\x00"))
+ self.assertEqual(n.name, "example.com")
+
+
+ def test_encode(self):
+ """
+ L{Name.encode} encodes its name information and writes it to the
+ file-like object passed to it.
+ """
+ name = dns.Name("foo.example.com")
+ stream = StringIO()
+ name.encode(stream)
+ self.assertEqual(stream.getvalue(), "\x03foo\x07example\x03com\x00")
+
+
+ def test_encodeWithCompression(self):
+ """
+ If a compression dictionary is passed to it, L{Name.encode} uses offset
+ information from it to encode its name with references to existing
+ labels in the stream instead of including another copy of them in the
+ output. It also updates the compression dictionary with the location of
+ the name it writes to the stream.
+ """
+ name = dns.Name("foo.example.com")
+ compression = {"example.com": 0x17}
+
+ # Some bytes already encoded into the stream for this message
+ previous = "some prefix to change .tell()"
+ stream = StringIO()
+ stream.write(previous)
+
+ # The position at which the encoded form of this new name will appear in
+ # the stream.
+ expected = len(previous) + dns.Message.headerSize
+ name.encode(stream, compression)
+ self.assertEqual(
+ "\x03foo\xc0\x17",
+ stream.getvalue()[len(previous):])
+ self.assertEqual(
+ {"example.com": 0x17, "foo.example.com": expected},
+ compression)
+
+
+ def test_unknown(self):
+ """
+ A resource record of unknown type and class is parsed into an
+ L{UnknownRecord} instance with its data preserved, and an
+ L{UnknownRecord} instance is serialized to a string equal to the one it
+ was parsed from.
+ """
+ wire = (
+ '\x01\x00' # Message ID
+ '\x00' # answer bit, opCode nibble, auth bit, trunc bit, recursive
+ # bit
+ '\x00' # recursion bit, empty bit, empty bit, empty bit, response
+ # code nibble
+ '\x00\x01' # number of queries
+ '\x00\x01' # number of answers
+ '\x00\x00' # number of authorities
+ '\x00\x01' # number of additionals
+
+ # query
+ '\x03foo\x03bar\x00' # foo.bar
+ '\xde\xad' # type=0xdead
+ '\xbe\xef' # cls=0xbeef
+
+ # 1st answer
+ '\xc0\x0c' # foo.bar - compressed
+ '\xde\xad' # type=0xdead
+ '\xbe\xef' # cls=0xbeef
+ '\x00\x00\x01\x01' # ttl=257
+ '\x00\x08somedata' # some payload data
+
+ # 1st additional
+ '\x03baz\x03ban\x00' # baz.ban
+ '\x00\x01' # type=A
+ '\x00\x01' # cls=IN
+ '\x00\x00\x01\x01' # ttl=257
+ '\x00\x04' # len=4
+ '\x01\x02\x03\x04' # 1.2.3.4
+
+ )
+
+ msg = dns.Message()
+ msg.fromStr(wire)
+
+ self.assertEqual(msg.queries, [
+ dns.Query('foo.bar', type=0xdead, cls=0xbeef),
+ ])
+ self.assertEqual(msg.answers, [
+ dns.RRHeader('foo.bar', type=0xdead, cls=0xbeef, ttl=257,
+ payload=dns.UnknownRecord('somedata', ttl=257)),
+ ])
+ self.assertEqual(msg.additional, [
+ dns.RRHeader('baz.ban', type=dns.A, cls=dns.IN, ttl=257,
+ payload=dns.Record_A('1.2.3.4', ttl=257)),
+ ])
+
+ enc = msg.toStr()
+
+ self.assertEqual(enc, wire)
+
+
+ def test_decodeWithCompression(self):
+ """
+ If the leading byte of an encoded label (in bytes read from a stream
+ passed to L{Name.decode}) has its two high bits set, the next byte is
+ treated as a pointer to another label in the stream and that label is
+ included in the name being decoded.
+ """
+ # Slightly modified version of the example from RFC 1035, section 4.1.4.
+ stream = StringIO(
+ "x" * 20 +
+ "\x01f\x03isi\x04arpa\x00"
+ "\x03foo\xc0\x14"
+ "\x03bar\xc0\x20")
+ stream.seek(20)
+ name = dns.Name()
+ name.decode(stream)
+ # Verify we found the first name in the stream and that the stream
+ # position is left at the first byte after the decoded name.
+ self.assertEqual("f.isi.arpa", name.name)
+ self.assertEqual(32, stream.tell())
+
+ # Get the second name from the stream and make the same assertions.
+ name.decode(stream)
+ self.assertEqual(name.name, "foo.f.isi.arpa")
+ self.assertEqual(38, stream.tell())
+
+ # Get the third and final name
+ name.decode(stream)
+ self.assertEqual(name.name, "bar.foo.f.isi.arpa")
+ self.assertEqual(44, stream.tell())
+
+
+ def test_rejectCompressionLoop(self):
+ """
+ L{Name.decode} raises L{ValueError} if the stream passed to it includes
+ a compression pointer which forms a loop, causing the name to be
+ undecodable.
+ """
+ name = dns.Name()
+ stream = StringIO("\xc0\x00")
+ self.assertRaises(ValueError, name.decode, stream)
+
+
+
+class RoundtripDNSTestCase(unittest.TestCase):
+ """Encoding and then decoding various objects."""
+
+ names = ["example.org", "go-away.fish.tv", "23strikesback.net"]
+
+ def testName(self):
+ for n in self.names:
+ # encode the name
+ f = StringIO()
+ dns.Name(n).encode(f)
+
+ # decode the name
+ f.seek(0, 0)
+ result = dns.Name()
+ result.decode(f)
+ self.assertEqual(result.name, n)
+
+ def testQuery(self):
+ for n in self.names:
+ for dnstype in range(1, 17):
+ for dnscls in range(1, 5):
+ # encode the query
+ f = StringIO()
+ dns.Query(n, dnstype, dnscls).encode(f)
+
+ # decode the result
+ f.seek(0, 0)
+ result = dns.Query()
+ result.decode(f)
+ self.assertEqual(result.name.name, n)
+ self.assertEqual(result.type, dnstype)
+ self.assertEqual(result.cls, dnscls)
+
+ def testRR(self):
+ # encode the RR
+ f = StringIO()
+ dns.RRHeader("test.org", 3, 4, 17).encode(f)
+
+ # decode the result
+ f.seek(0, 0)
+ result = dns.RRHeader()
+ result.decode(f)
+ self.assertEqual(str(result.name), "test.org")
+ self.assertEqual(result.type, 3)
+ self.assertEqual(result.cls, 4)
+ self.assertEqual(result.ttl, 17)
+
+
+ def testResources(self):
+ names = (
+ "this.are.test.name",
+ "will.compress.will.this.will.name.will.hopefully",
+ "test.CASE.preSErVatIOn.YeAH",
+ "a.s.h.o.r.t.c.a.s.e.t.o.t.e.s.t",
+ "singleton"
+ )
+ for s in names:
+ f = StringIO()
+ dns.SimpleRecord(s).encode(f)
+ f.seek(0, 0)
+ result = dns.SimpleRecord()
+ result.decode(f)
+ self.assertEqual(str(result.name), s)
+
+ def test_hashable(self):
+ """
+ Instances of all record types are hashable.
+ """
+ for k in RECORD_TYPES:
+ k1, k2 = k(), k()
+ hk1 = hash(k1)
+ hk2 = hash(k2)
+ self.assertEqual(hk1, hk2, "%s != %s (for %s)" % (hk1,hk2,k))
+
+
+ def test_Charstr(self):
+ """
+ Test L{dns.Charstr} encode and decode.
+ """
+ for n in self.names:
+ # encode the name
+ f = StringIO()
+ dns.Charstr(n).encode(f)
+
+ # decode the name
+ f.seek(0, 0)
+ result = dns.Charstr()
+ result.decode(f)
+ self.assertEqual(result.string, n)
+
+
+ def test_NAPTR(self):
+ """
+ Test L{dns.Record_NAPTR} encode and decode.
+ """
+ naptrs = [(100, 10, "u", "sip+E2U",
+ "!^.*$!sip:information@domain.tld!", ""),
+ (100, 50, "s", "http+I2L+I2C+I2R", "",
+ "_http._tcp.gatech.edu")]
+
+ for (order, preference, flags, service, regexp, replacement) in naptrs:
+ rin = dns.Record_NAPTR(order, preference, flags, service, regexp,
+ replacement)
+ e = StringIO()
+ rin.encode(e)
+ e.seek(0,0)
+ rout = dns.Record_NAPTR()
+ rout.decode(e)
+ self.assertEqual(rin.order, rout.order)
+ self.assertEqual(rin.preference, rout.preference)
+ self.assertEqual(rin.flags, rout.flags)
+ self.assertEqual(rin.service, rout.service)
+ self.assertEqual(rin.regexp, rout.regexp)
+ self.assertEqual(rin.replacement.name, rout.replacement.name)
+ self.assertEqual(rin.ttl, rout.ttl)
+
+
+
+class MessageTestCase(unittest.TestCase):
+ """
+ Tests for L{twisted.names.dns.Message}.
+ """
+
+ def testEmptyMessage(self):
+ """
+ Test that a message which has been truncated causes an EOFError to
+ be raised when it is parsed.
+ """
+ msg = dns.Message()
+ self.assertRaises(EOFError, msg.fromStr, '')
+
+
+ def testEmptyQuery(self):
+ """
+ Test that bytes representing an empty query message can be decoded
+ as such.
+ """
+ msg = dns.Message()
+ msg.fromStr(
+ '\x01\x00' # Message ID
+ '\x00' # answer bit, opCode nibble, auth bit, trunc bit, recursive bit
+ '\x00' # recursion bit, empty bit, empty bit, empty bit, response code nibble
+ '\x00\x00' # number of queries
+ '\x00\x00' # number of answers
+ '\x00\x00' # number of authorities
+ '\x00\x00' # number of additionals
+ )
+ self.assertEqual(msg.id, 256)
+ self.failIf(msg.answer, "Message was not supposed to be an answer.")
+ self.assertEqual(msg.opCode, dns.OP_QUERY)
+ self.failIf(msg.auth, "Message was not supposed to be authoritative.")
+ self.failIf(msg.trunc, "Message was not supposed to be truncated.")
+ self.assertEqual(msg.queries, [])
+ self.assertEqual(msg.answers, [])
+ self.assertEqual(msg.authority, [])
+ self.assertEqual(msg.additional, [])
+
+
+ def testNULL(self):
+ bytes = ''.join([chr(i) for i in range(256)])
+ rec = dns.Record_NULL(bytes)
+ rr = dns.RRHeader('testname', dns.NULL, payload=rec)
+ msg1 = dns.Message()
+ msg1.answers.append(rr)
+ s = StringIO()
+ msg1.encode(s)
+ s.seek(0, 0)
+ msg2 = dns.Message()
+ msg2.decode(s)
+
+ self.failUnless(isinstance(msg2.answers[0].payload, dns.Record_NULL))
+ self.assertEqual(msg2.answers[0].payload.payload, bytes)
+
+
+ def test_lookupRecordTypeDefault(self):
+ """
+ L{Message.lookupRecordType} returns C{dns.UnknownRecord} if it is
+ called with an integer which doesn't correspond to any known record
+ type.
+ """
+ # 65280 is the first value in the range reserved for private
+ # use, so it shouldn't ever conflict with an officially
+ # allocated value.
+ self.assertIdentical(
+ dns.Message().lookupRecordType(65280), dns.UnknownRecord)
+
+
+ def test_nonAuthoritativeMessage(self):
+ """
+ The L{RRHeader} instances created by L{Message} from a non-authoritative
+ message are marked as not authoritative.
+ """
+ buf = StringIO()
+ answer = dns.RRHeader(payload=dns.Record_A('1.2.3.4', ttl=0))
+ answer.encode(buf)
+ message = dns.Message()
+ message.fromStr(
+ '\x01\x00' # Message ID
+ # answer bit, opCode nibble, auth bit, trunc bit, recursive bit
+ '\x00'
+ # recursion bit, empty bit, empty bit, empty bit, response code
+ # nibble
+ '\x00'
+ '\x00\x00' # number of queries
+ '\x00\x01' # number of answers
+ '\x00\x00' # number of authorities
+ '\x00\x00' # number of additionals
+ + buf.getvalue()
+ )
+ self.assertEqual(message.answers, [answer])
+ self.assertFalse(message.answers[0].auth)
+
+
+ def test_authoritativeMessage(self):
+ """
+ The L{RRHeader} instances created by L{Message} from an authoritative
+ message are marked as authoritative.
+ """
+ buf = StringIO()
+ answer = dns.RRHeader(payload=dns.Record_A('1.2.3.4', ttl=0))
+ answer.encode(buf)
+ message = dns.Message()
+ message.fromStr(
+ '\x01\x00' # Message ID
+ # answer bit, opCode nibble, auth bit, trunc bit, recursive bit
+ '\x04'
+ # recursion bit, empty bit, empty bit, empty bit, response code
+ # nibble
+ '\x00'
+ '\x00\x00' # number of queries
+ '\x00\x01' # number of answers
+ '\x00\x00' # number of authorities
+ '\x00\x00' # number of additionals
+ + buf.getvalue()
+ )
+ answer.auth = True
+ self.assertEqual(message.answers, [answer])
+ self.assertTrue(message.answers[0].auth)
+
+
+
+class TestController(object):
+ """
+ Pretend to be a DNS query processor for a DNSDatagramProtocol.
+
+ @ivar messages: the list of received messages.
+ @type messages: C{list} of (msg, protocol, address)
+ """
+
+ def __init__(self):
+ """
+ Initialize the controller: create a list of messages.
+ """
+ self.messages = []
+
+
+ def messageReceived(self, msg, proto, addr):
+ """
+ Save the message so that it can be checked during the tests.
+ """
+ self.messages.append((msg, proto, addr))
+
+
+
+class DatagramProtocolTestCase(unittest.TestCase):
+ """
+ Test various aspects of L{dns.DNSDatagramProtocol}.
+ """
+
+ def setUp(self):
+ """
+ Create a L{dns.DNSDatagramProtocol} with a deterministic clock.
+ """
+ self.clock = task.Clock()
+ self.controller = TestController()
+ self.proto = dns.DNSDatagramProtocol(self.controller)
+ transport = proto_helpers.FakeDatagramTransport()
+ self.proto.makeConnection(transport)
+ self.proto.callLater = self.clock.callLater
+
+
+ def test_truncatedPacket(self):
+ """
+ Test that when a short datagram is received, datagramReceived does
+ not raise an exception while processing it.
+ """
+ self.proto.datagramReceived('',
+ address.IPv4Address('UDP', '127.0.0.1', 12345))
+ self.assertEqual(self.controller.messages, [])
+
+
+ def test_simpleQuery(self):
+ """
+ Test content received after a query.
+ """
+ d = self.proto.query(('127.0.0.1', 21345), [dns.Query('foo')])
+ self.assertEqual(len(self.proto.liveMessages.keys()), 1)
+ m = dns.Message()
+ m.id = self.proto.liveMessages.items()[0][0]
+ m.answers = [dns.RRHeader(payload=dns.Record_A(address='1.2.3.4'))]
+ def cb(result):
+ self.assertEqual(result.answers[0].payload.dottedQuad(), '1.2.3.4')
+ d.addCallback(cb)
+ self.proto.datagramReceived(m.toStr(), ('127.0.0.1', 21345))
+ return d
+
+
+ def test_queryTimeout(self):
+ """
+ Test that query timeouts after some seconds.
+ """
+ d = self.proto.query(('127.0.0.1', 21345), [dns.Query('foo')])
+ self.assertEqual(len(self.proto.liveMessages), 1)
+ self.clock.advance(10)
+ self.assertFailure(d, dns.DNSQueryTimeoutError)
+ self.assertEqual(len(self.proto.liveMessages), 0)
+ return d
+
+
+ def test_writeError(self):
+ """
+ Exceptions raised by the transport's write method should be turned into
+ C{Failure}s passed to errbacks of the C{Deferred} returned by
+ L{DNSDatagramProtocol.query}.
+ """
+ def writeError(message, addr):
+ raise RuntimeError("bar")
+ self.proto.transport.write = writeError
+
+ d = self.proto.query(('127.0.0.1', 21345), [dns.Query('foo')])
+ return self.assertFailure(d, RuntimeError)
+
+
+ def test_listenError(self):
+ """
+ Exception L{CannotListenError} raised by C{listenUDP} should be turned
+ into a C{Failure} passed to errback of the C{Deferred} returned by
+ L{DNSDatagramProtocol.query}.
+ """
+ def startListeningError():
+ raise CannotListenError(None, None, None)
+ self.proto.startListening = startListeningError
+ # Clean up transport so that the protocol calls startListening again
+ self.proto.transport = None
+
+ d = self.proto.query(('127.0.0.1', 21345), [dns.Query('foo')])
+ return self.assertFailure(d, CannotListenError)
+
+
+
+class TestTCPController(TestController):
+ """
+ Pretend to be a DNS query processor for a DNSProtocol.
+
+ @ivar connections: A list of L{DNSProtocol} instances which have
+ notified this controller that they are connected and have not
+ yet notified it that their connection has been lost.
+ """
+ def __init__(self):
+ TestController.__init__(self)
+ self.connections = []
+
+
+ def connectionMade(self, proto):
+ self.connections.append(proto)
+
+
+ def connectionLost(self, proto):
+ self.connections.remove(proto)
+
+
+
+class DNSProtocolTestCase(unittest.TestCase):
+ """
+ Test various aspects of L{dns.DNSProtocol}.
+ """
+
+ def setUp(self):
+ """
+ Create a L{dns.DNSProtocol} with a deterministic clock.
+ """
+ self.clock = task.Clock()
+ self.controller = TestTCPController()
+ self.proto = dns.DNSProtocol(self.controller)
+ self.proto.makeConnection(proto_helpers.StringTransport())
+ self.proto.callLater = self.clock.callLater
+
+
+ def test_connectionTracking(self):
+ """
+ L{dns.DNSProtocol} calls its controller's C{connectionMade}
+ method with itself when it is connected to a transport and its
+ controller's C{connectionLost} method when it is disconnected.
+ """
+ self.assertEqual(self.controller.connections, [self.proto])
+ self.proto.connectionLost(
+ Failure(ConnectionDone("Fake Connection Done")))
+ self.assertEqual(self.controller.connections, [])
+
+
+ def test_queryTimeout(self):
+ """
+ Test that query timeouts after some seconds.
+ """
+ d = self.proto.query([dns.Query('foo')])
+ self.assertEqual(len(self.proto.liveMessages), 1)
+ self.clock.advance(60)
+ self.assertFailure(d, dns.DNSQueryTimeoutError)
+ self.assertEqual(len(self.proto.liveMessages), 0)
+ return d
+
+
+ def test_simpleQuery(self):
+ """
+ Test content received after a query.
+ """
+ d = self.proto.query([dns.Query('foo')])
+ self.assertEqual(len(self.proto.liveMessages.keys()), 1)
+ m = dns.Message()
+ m.id = self.proto.liveMessages.items()[0][0]
+ m.answers = [dns.RRHeader(payload=dns.Record_A(address='1.2.3.4'))]
+ def cb(result):
+ self.assertEqual(result.answers[0].payload.dottedQuad(), '1.2.3.4')
+ d.addCallback(cb)
+ s = m.toStr()
+ s = struct.pack('!H', len(s)) + s
+ self.proto.dataReceived(s)
+ return d
+
+
+ def test_writeError(self):
+ """
+ Exceptions raised by the transport's write method should be turned into
+ C{Failure}s passed to errbacks of the C{Deferred} returned by
+ L{DNSProtocol.query}.
+ """
+ def writeError(message):
+ raise RuntimeError("bar")
+ self.proto.transport.write = writeError
+
+ d = self.proto.query([dns.Query('foo')])
+ return self.assertFailure(d, RuntimeError)
+
+
+
+class ReprTests(unittest.TestCase):
+ """
+ Tests for the C{__repr__} implementation of record classes.
+ """
+ def test_ns(self):
+ """
+ The repr of a L{dns.Record_NS} instance includes the name of the
+ nameserver and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_NS('example.com', 4321)),
+ "<NS name=example.com ttl=4321>")
+
+
+ def test_md(self):
+ """
+ The repr of a L{dns.Record_MD} instance includes the name of the
+ mail destination and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MD('example.com', 4321)),
+ "<MD name=example.com ttl=4321>")
+
+
+ def test_mf(self):
+ """
+ The repr of a L{dns.Record_MF} instance includes the name of the
+ mail forwarder and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MF('example.com', 4321)),
+ "<MF name=example.com ttl=4321>")
+
+
+ def test_cname(self):
+ """
+ The repr of a L{dns.Record_CNAME} instance includes the name of the
+ mail forwarder and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_CNAME('example.com', 4321)),
+ "<CNAME name=example.com ttl=4321>")
+
+
+ def test_mb(self):
+ """
+ The repr of a L{dns.Record_MB} instance includes the name of the
+ mailbox and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MB('example.com', 4321)),
+ "<MB name=example.com ttl=4321>")
+
+
+ def test_mg(self):
+ """
+ The repr of a L{dns.Record_MG} instance includes the name of the
+ mail group memeber and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MG('example.com', 4321)),
+ "<MG name=example.com ttl=4321>")
+
+
+ def test_mr(self):
+ """
+ The repr of a L{dns.Record_MR} instance includes the name of the
+ mail rename domain and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MR('example.com', 4321)),
+ "<MR name=example.com ttl=4321>")
+
+
+ def test_ptr(self):
+ """
+ The repr of a L{dns.Record_PTR} instance includes the name of the
+ pointer and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_PTR('example.com', 4321)),
+ "<PTR name=example.com ttl=4321>")
+
+
+ def test_dname(self):
+ """
+ The repr of a L{dns.Record_DNAME} instance includes the name of the
+ non-terminal DNS name redirection and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_DNAME('example.com', 4321)),
+ "<DNAME name=example.com ttl=4321>")
+
+
+ def test_a(self):
+ """
+ The repr of a L{dns.Record_A} instance includes the dotted-quad
+ string representation of the address it is for and the TTL of the
+ record.
+ """
+ self.assertEqual(
+ repr(dns.Record_A('1.2.3.4', 567)),
+ '<A address=1.2.3.4 ttl=567>')
+
+
+ def test_soa(self):
+ """
+ The repr of a L{dns.Record_SOA} instance includes all of the
+ authority fields.
+ """
+ self.assertEqual(
+ repr(dns.Record_SOA(mname='mName', rname='rName', serial=123,
+ refresh=456, retry=789, expire=10,
+ minimum=11, ttl=12)),
+ "<SOA mname=mName rname=rName serial=123 refresh=456 "
+ "retry=789 expire=10 minimum=11 ttl=12>")
+
+
+ def test_null(self):
+ """
+ The repr of a L{dns.Record_NULL} instance includes the repr of its
+ payload and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_NULL('abcd', 123)),
+ "<NULL payload='abcd' ttl=123>")
+
+
+ def test_wks(self):
+ """
+ The repr of a L{dns.Record_WKS} instance includes the dotted-quad
+ string representation of the address it is for, the IP protocol
+ number it is for, and the TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_WKS('2.3.4.5', 7, ttl=8)),
+ "<WKS address=2.3.4.5 protocol=7 ttl=8>")
+
+
+ def test_aaaa(self):
+ """
+ The repr of a L{dns.Record_AAAA} instance includes the colon-separated
+ hex string representation of the address it is for and the TTL of the
+ record.
+ """
+ self.assertEqual(
+ repr(dns.Record_AAAA('8765::1234', ttl=10)),
+ "<AAAA address=8765::1234 ttl=10>")
+
+
+ def test_a6(self):
+ """
+ The repr of a L{dns.Record_A6} instance includes the colon-separated
+ hex string representation of the address it is for and the TTL of the
+ record.
+ """
+ self.assertEqual(
+ repr(dns.Record_A6(0, '1234::5678', 'foo.bar', ttl=10)),
+ "<A6 suffix=1234::5678 prefix=foo.bar ttl=10>")
+
+
+ def test_srv(self):
+ """
+ The repr of a L{dns.Record_SRV} instance includes the name and port of
+ the target and the priority, weight, and TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_SRV(1, 2, 3, 'example.org', 4)),
+ "<SRV priority=1 weight=2 target=example.org port=3 ttl=4>")
+
+
+ def test_naptr(self):
+ """
+ The repr of a L{dns.Record_NAPTR} instance includes the order,
+ preference, flags, service, regular expression, replacement, and TTL of
+ the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_NAPTR(5, 9, "S", "http", "/foo/bar/i", "baz", 3)),
+ "<NAPTR order=5 preference=9 flags=S service=http "
+ "regexp=/foo/bar/i replacement=baz ttl=3>")
+
+
+ def test_afsdb(self):
+ """
+ The repr of a L{dns.Record_AFSDB} instance includes the subtype,
+ hostname, and TTL of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_AFSDB(3, 'example.org', 5)),
+ "<AFSDB subtype=3 hostname=example.org ttl=5>")
+
+
+ def test_rp(self):
+ """
+ The repr of a L{dns.Record_RP} instance includes the mbox, txt, and TTL
+ fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_RP('alice.example.com', 'admin.example.com', 3)),
+ "<RP mbox=alice.example.com txt=admin.example.com ttl=3>")
+
+
+ def test_hinfo(self):
+ """
+ The repr of a L{dns.Record_HINFO} instance includes the cpu, os, and
+ TTL fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_HINFO('sparc', 'minix', 12)),
+ "<HINFO cpu='sparc' os='minix' ttl=12>")
+
+
+ def test_minfo(self):
+ """
+ The repr of a L{dns.Record_MINFO} instance includes the rmailbx,
+ emailbx, and TTL fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MINFO('alice.example.com', 'bob.example.com', 15)),
+ "<MINFO responsibility=alice.example.com "
+ "errors=bob.example.com ttl=15>")
+
+
+ def test_mx(self):
+ """
+ The repr of a L{dns.Record_MX} instance includes the preference, name,
+ and TTL fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_MX(13, 'mx.example.com', 2)),
+ "<MX preference=13 name=mx.example.com ttl=2>")
+
+
+ def test_txt(self):
+ """
+ The repr of a L{dns.Record_TXT} instance includes the data and ttl
+ fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_TXT("foo", "bar", ttl=15)),
+ "<TXT data=['foo', 'bar'] ttl=15>")
+
+
+ def test_spf(self):
+ """
+ The repr of a L{dns.Record_SPF} instance includes the data and ttl
+ fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.Record_SPF("foo", "bar", ttl=15)),
+ "<SPF data=['foo', 'bar'] ttl=15>")
+
+
+ def test_unknown(self):
+ """
+ The repr of a L{dns.UnknownRecord} instance includes the data and ttl
+ fields of the record.
+ """
+ self.assertEqual(
+ repr(dns.UnknownRecord("foo\x1fbar", 12)),
+ "<UNKNOWN data='foo\\x1fbar' ttl=12>")
+
+
+
+class _Equal(object):
+ """
+ A class the instances of which are equal to anything and everything.
+ """
+ def __eq__(self, other):
+ return True
+
+
+ def __ne__(self, other):
+ return False
+
+
+
+class _NotEqual(object):
+ """
+ A class the instances of which are equal to nothing.
+ """
+ def __eq__(self, other):
+ return False
+
+
+ def __ne__(self, other):
+ return True
+
+
+
+class EqualityTests(unittest.TestCase):
+ """
+ Tests for the equality and non-equality behavior of record classes.
+ """
+ def _equalityTest(self, firstValueOne, secondValueOne, valueTwo):
+ """
+ Assert that C{firstValueOne} is equal to C{secondValueOne} but not
+ equal to C{valueOne} and that it defines equality cooperatively with
+ other types it doesn't know about.
+ """
+ # This doesn't use assertEqual and assertNotEqual because the exact
+ # operator those functions use is not very well defined. The point
+ # of these assertions is to check the results of the use of specific
+ # operators (precisely to ensure that using different permutations
+ # (eg "x == y" or "not (x != y)") which should yield the same results
+ # actually does yield the same result). -exarkun
+ self.assertTrue(firstValueOne == firstValueOne)
+ self.assertTrue(firstValueOne == secondValueOne)
+ self.assertFalse(firstValueOne == valueTwo)
+ self.assertFalse(firstValueOne != firstValueOne)
+ self.assertFalse(firstValueOne != secondValueOne)
+ self.assertTrue(firstValueOne != valueTwo)
+ self.assertTrue(firstValueOne == _Equal())
+ self.assertFalse(firstValueOne != _Equal())
+ self.assertFalse(firstValueOne == _NotEqual())
+ self.assertTrue(firstValueOne != _NotEqual())
+
+
+ def _simpleEqualityTest(self, cls):
+ # Vary the TTL
+ self._equalityTest(
+ cls('example.com', 123),
+ cls('example.com', 123),
+ cls('example.com', 321))
+ # Vary the name
+ self._equalityTest(
+ cls('example.com', 123),
+ cls('example.com', 123),
+ cls('example.org', 123))
+
+
+ def test_rrheader(self):
+ """
+ Two L{dns.RRHeader} instances compare equal if and only if they have
+ the same name, type, class, time to live, payload, and authoritative
+ bit.
+ """
+ # Vary the name
+ self._equalityTest(
+ dns.RRHeader('example.com', payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.org', payload=dns.Record_A('1.2.3.4')))
+
+ # Vary the payload
+ self._equalityTest(
+ dns.RRHeader('example.com', payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', payload=dns.Record_A('1.2.3.5')))
+
+ # Vary the type. Leave the payload as None so that we don't have to
+ # provide non-equal values.
+ self._equalityTest(
+ dns.RRHeader('example.com', dns.A),
+ dns.RRHeader('example.com', dns.A),
+ dns.RRHeader('example.com', dns.MX))
+
+ # Probably not likely to come up. Most people use the internet.
+ self._equalityTest(
+ dns.RRHeader('example.com', cls=dns.IN, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', cls=dns.IN, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', cls=dns.CS, payload=dns.Record_A('1.2.3.4')))
+
+ # Vary the ttl
+ self._equalityTest(
+ dns.RRHeader('example.com', ttl=60, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', ttl=60, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', ttl=120, payload=dns.Record_A('1.2.3.4')))
+
+ # Vary the auth bit
+ self._equalityTest(
+ dns.RRHeader('example.com', auth=1, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', auth=1, payload=dns.Record_A('1.2.3.4')),
+ dns.RRHeader('example.com', auth=0, payload=dns.Record_A('1.2.3.4')))
+
+
+ def test_ns(self):
+ """
+ Two L{dns.Record_NS} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_NS)
+
+
+ def test_md(self):
+ """
+ Two L{dns.Record_MD} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_MD)
+
+
+ def test_mf(self):
+ """
+ Two L{dns.Record_MF} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_MF)
+
+
+ def test_cname(self):
+ """
+ Two L{dns.Record_CNAME} instances compare equal if and only if they
+ have the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_CNAME)
+
+
+ def test_mb(self):
+ """
+ Two L{dns.Record_MB} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_MB)
+
+
+ def test_mg(self):
+ """
+ Two L{dns.Record_MG} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_MG)
+
+
+ def test_mr(self):
+ """
+ Two L{dns.Record_MR} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_MR)
+
+
+ def test_ptr(self):
+ """
+ Two L{dns.Record_PTR} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_PTR)
+
+
+ def test_dname(self):
+ """
+ Two L{dns.Record_MD} instances compare equal if and only if they have
+ the same name and TTL.
+ """
+ self._simpleEqualityTest(dns.Record_DNAME)
+
+
+ def test_a(self):
+ """
+ Two L{dns.Record_A} instances compare equal if and only if they have
+ the same address and TTL.
+ """
+ # Vary the TTL
+ self._equalityTest(
+ dns.Record_A('1.2.3.4', 5),
+ dns.Record_A('1.2.3.4', 5),
+ dns.Record_A('1.2.3.4', 6))
+ # Vary the address
+ self._equalityTest(
+ dns.Record_A('1.2.3.4', 5),
+ dns.Record_A('1.2.3.4', 5),
+ dns.Record_A('1.2.3.5', 5))
+
+
+ def test_soa(self):
+ """
+ Two L{dns.Record_SOA} instances compare equal if and only if they have
+ the same mname, rname, serial, refresh, minimum, expire, retry, and
+ ttl.
+ """
+ # Vary the mname
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('xname', 'rname', 123, 456, 789, 10, 20, 30))
+ # Vary the rname
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'xname', 123, 456, 789, 10, 20, 30))
+ # Vary the serial
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 1, 456, 789, 10, 20, 30))
+ # Vary the refresh
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 1, 789, 10, 20, 30))
+ # Vary the minimum
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 1, 10, 20, 30))
+ # Vary the expire
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 1, 20, 30))
+ # Vary the retry
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 1, 30))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'rname', 123, 456, 789, 10, 20, 30),
+ dns.Record_SOA('mname', 'xname', 123, 456, 789, 10, 20, 1))
+
+
+ def test_null(self):
+ """
+ Two L{dns.Record_NULL} instances compare equal if and only if they have
+ the same payload and ttl.
+ """
+ # Vary the payload
+ self._equalityTest(
+ dns.Record_NULL('foo bar', 10),
+ dns.Record_NULL('foo bar', 10),
+ dns.Record_NULL('bar foo', 10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_NULL('foo bar', 10),
+ dns.Record_NULL('foo bar', 10),
+ dns.Record_NULL('foo bar', 100))
+
+
+ def test_wks(self):
+ """
+ Two L{dns.Record_WKS} instances compare equal if and only if they have
+ the same address, protocol, map, and ttl.
+ """
+ # Vary the address
+ self._equalityTest(
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('4.3.2.1', 1, 'foo', 2))
+ # Vary the protocol
+ self._equalityTest(
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 100, 'foo', 2))
+ # Vary the map
+ self._equalityTest(
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'bar', 2))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 2),
+ dns.Record_WKS('1.2.3.4', 1, 'foo', 200))
+
+
+ def test_aaaa(self):
+ """
+ Two L{dns.Record_AAAA} instances compare equal if and only if they have
+ the same address and ttl.
+ """
+ # Vary the address
+ self._equalityTest(
+ dns.Record_AAAA('1::2', 1),
+ dns.Record_AAAA('1::2', 1),
+ dns.Record_AAAA('2::1', 1))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_AAAA('1::2', 1),
+ dns.Record_AAAA('1::2', 1),
+ dns.Record_AAAA('1::2', 10))
+
+
+ def test_a6(self):
+ """
+ Two L{dns.Record_A6} instances compare equal if and only if they have
+ the same prefix, prefix length, suffix, and ttl.
+ """
+ # Note, A6 is crazy, I'm not sure these values are actually legal.
+ # Hopefully that doesn't matter for this test. -exarkun
+
+ # Vary the prefix length
+ self._equalityTest(
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(32, '::abcd', 'example.com', 10))
+ # Vary the suffix
+ self._equalityTest(
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd:0', 'example.com', 10))
+ # Vary the prefix
+ self._equalityTest(
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.org', 10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.com', 10),
+ dns.Record_A6(16, '::abcd', 'example.com', 100))
+
+
+ def test_srv(self):
+ """
+ Two L{dns.Record_SRV} instances compare equal if and only if they have
+ the same priority, weight, port, target, and ttl.
+ """
+ # Vary the priority
+ self._equalityTest(
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(100, 20, 30, 'example.com', 40))
+ # Vary the weight
+ self._equalityTest(
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 200, 30, 'example.com', 40))
+ # Vary the port
+ self._equalityTest(
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 300, 'example.com', 40))
+ # Vary the target
+ self._equalityTest(
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.org', 40))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 40),
+ dns.Record_SRV(10, 20, 30, 'example.com', 400))
+
+
+ def test_naptr(self):
+ """
+ Two L{dns.Record_NAPTR} instances compare equal if and only if they
+ have the same order, preference, flags, service, regexp, replacement,
+ and ttl.
+ """
+ # Vary the order
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(2, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12))
+ # Vary the preference
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 3, "u", "sip+E2U", "/foo/bar/", "baz", 12))
+ # Vary the flags
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "p", "sip+E2U", "/foo/bar/", "baz", 12))
+ # Vary the service
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "http", "/foo/bar/", "baz", 12))
+ # Vary the regexp
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/bar/foo/", "baz", 12))
+ # Vary the replacement
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/bar/foo/", "quux", 12))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/foo/bar/", "baz", 12),
+ dns.Record_NAPTR(1, 2, "u", "sip+E2U", "/bar/foo/", "baz", 5))
+
+
+ def test_afsdb(self):
+ """
+ Two L{dns.Record_AFSDB} instances compare equal if and only if they
+ have the same subtype, hostname, and ttl.
+ """
+ # Vary the subtype
+ self._equalityTest(
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(2, 'example.com', 2))
+ # Vary the hostname
+ self._equalityTest(
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(1, 'example.org', 2))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(1, 'example.com', 2),
+ dns.Record_AFSDB(1, 'example.com', 3))
+
+
+ def test_rp(self):
+ """
+ Two L{Record_RP} instances compare equal if and only if they have the
+ same mbox, txt, and ttl.
+ """
+ # Vary the mbox
+ self._equalityTest(
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('bob.example.com', 'alice is nice', 10))
+ # Vary the txt
+ self._equalityTest(
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('alice.example.com', 'alice is not nice', 10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('alice.example.com', 'alice is nice', 10),
+ dns.Record_RP('alice.example.com', 'alice is nice', 100))
+
+
+ def test_hinfo(self):
+ """
+ Two L{dns.Record_HINFO} instances compare equal if and only if they
+ have the same cpu, os, and ttl.
+ """
+ # Vary the cpu
+ self._equalityTest(
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('i386', 'plan9', 10))
+ # Vary the os
+ self._equalityTest(
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('x86-64', 'plan11', 10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('x86-64', 'plan9', 10),
+ dns.Record_HINFO('x86-64', 'plan9', 100))
+
+
+ def test_minfo(self):
+ """
+ Two L{dns.Record_MINFO} instances compare equal if and only if they
+ have the same rmailbx, emailbx, and ttl.
+ """
+ # Vary the rmailbx
+ self._equalityTest(
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('someplace', 'emailbox', 10))
+ # Vary the emailbx
+ self._equalityTest(
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('rmailbox', 'something', 10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('rmailbox', 'emailbox', 10),
+ dns.Record_MINFO('rmailbox', 'emailbox', 100))
+
+
+ def test_mx(self):
+ """
+ Two L{dns.Record_MX} instances compare equal if and only if they have
+ the same preference, name, and ttl.
+ """
+ # Vary the preference
+ self._equalityTest(
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(100, 'example.org', 20))
+ # Vary the name
+ self._equalityTest(
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(10, 'example.net', 20))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(10, 'example.org', 20),
+ dns.Record_MX(10, 'example.org', 200))
+
+
+ def test_txt(self):
+ """
+ Two L{dns.Record_TXT} instances compare equal if and only if they have
+ the same data and ttl.
+ """
+ # Vary the length of the data
+ self._equalityTest(
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('foo', 'bar', 'baz', ttl=10))
+ # Vary the value of the data
+ self._equalityTest(
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('bar', 'foo', ttl=10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('foo', 'bar', ttl=10),
+ dns.Record_TXT('foo', 'bar', ttl=100))
+
+
+ def test_spf(self):
+ """
+ L{dns.Record_SPF} instances compare equal if and only if they have the
+ same data and ttl.
+ """
+ # Vary the length of the data
+ self._equalityTest(
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('foo', 'bar', 'baz', ttl=10))
+ # Vary the value of the data
+ self._equalityTest(
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('bar', 'foo', ttl=10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('foo', 'bar', ttl=10),
+ dns.Record_SPF('foo', 'bar', ttl=100))
+
+
+ def test_unknown(self):
+ """
+ L{dns.UnknownRecord} instances compare equal if and only if they have
+ the same data and ttl.
+ """
+ # Vary the length of the data
+ self._equalityTest(
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('foobar', ttl=10))
+ # Vary the value of the data
+ self._equalityTest(
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('bar', ttl=10))
+ # Vary the ttl
+ self._equalityTest(
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('foo', ttl=10),
+ dns.UnknownRecord('foo', ttl=100))
diff --git a/twisted/names/test/test_hosts.py b/twisted/names/test/test_hosts.py
new file mode 100644
index 0000000..d4cdb69
--- /dev/null
+++ b/twisted/names/test/test_hosts.py
@@ -0,0 +1,232 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the I{hosts(5)}-based resolver, L{twisted.names.hosts}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.python.filepath import FilePath
+from twisted.internet.defer import gatherResults
+
+from twisted.names.dns import (
+ A, AAAA, IN, DomainError, RRHeader, Query, Record_A, Record_AAAA)
+from twisted.names.hosts import Resolver, searchFileFor, searchFileForAll
+
+
+class SearchHostsFileTests(TestCase):
+ """
+ Tests for L{searchFileFor}, a helper which finds the first address for a
+ particular hostname in a I{hosts(5)}-style file.
+ """
+ def test_findAddress(self):
+ """
+ If there is an IPv4 address for the hostname passed to
+ L{searchFileFor}, it is returned.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent(
+ "10.2.3.4 foo.example.com\n")
+ self.assertEqual(
+ "10.2.3.4", searchFileFor(hosts.path, "foo.example.com"))
+
+
+ def test_notFoundAddress(self):
+ """
+ If there is no address information for the hostname passed to
+ L{searchFileFor}, C{None} is returned.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent(
+ "10.2.3.4 foo.example.com\n")
+ self.assertIdentical(
+ None, searchFileFor(hosts.path, "bar.example.com"))
+
+
+ def test_firstAddress(self):
+ """
+ The first address associated with the given hostname is returned.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent(
+ "::1 foo.example.com\n"
+ "10.1.2.3 foo.example.com\n"
+ "fe80::21b:fcff:feee:5a1d foo.example.com\n")
+ self.assertEqual(
+ "::1", searchFileFor(hosts.path, "foo.example.com"))
+
+
+ def test_searchFileForAliases(self):
+ """
+ For a host with a canonical name and one or more aliases,
+ L{searchFileFor} can find an address given any of the names.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent(
+ "127.0.1.1 helmut.example.org helmut\n"
+ "# a comment\n"
+ "::1 localhost ip6-localhost ip6-loopback\n")
+ self.assertEqual(searchFileFor(hosts.path, 'helmut'), '127.0.1.1')
+ self.assertEqual(
+ searchFileFor(hosts.path, 'helmut.example.org'), '127.0.1.1')
+ self.assertEqual(searchFileFor(hosts.path, 'ip6-localhost'), '::1')
+ self.assertEqual(searchFileFor(hosts.path, 'ip6-loopback'), '::1')
+ self.assertEqual(searchFileFor(hosts.path, 'localhost'), '::1')
+
+
+
+class SearchHostsFileForAllTests(TestCase):
+ """
+ Tests for L{searchFileForAll}, a helper which finds all addresses for a
+ particular hostname in a I{hosts(5)}-style file.
+ """
+ def test_allAddresses(self):
+ """
+ L{searchFileForAll} returns a list of all addresses associated with the
+ name passed to it.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent(
+ "127.0.0.1 foobar.example.com\n"
+ "127.0.0.2 foobar.example.com\n"
+ "::1 foobar.example.com\n")
+ self.assertEqual(
+ ["127.0.0.1", "127.0.0.2", "::1"],
+ searchFileForAll(hosts, "foobar.example.com"))
+
+
+ def test_caseInsensitively(self):
+ """
+ L{searchFileForAll} searches for names case-insensitively.
+ """
+ hosts = FilePath(self.mktemp())
+ hosts.setContent("127.0.0.1 foobar.EXAMPLE.com\n")
+ self.assertEqual(
+ ["127.0.0.1"], searchFileForAll(hosts, "FOOBAR.example.com"))
+
+
+ def test_readError(self):
+ """
+ If there is an error reading the contents of the hosts file,
+ L{searchFileForAll} returns an empty list.
+ """
+ self.assertEqual(
+ [], searchFileForAll(FilePath(self.mktemp()), "example.com"))
+
+
+
+class HostsTestCase(TestCase):
+ """
+ Tests for the I{hosts(5)}-based L{twisted.names.hosts.Resolver}.
+ """
+ def setUp(self):
+ f = open('EtcHosts', 'w')
+ f.write('''
+1.1.1.1 EXAMPLE EXAMPLE.EXAMPLETHING
+::2 mixed
+1.1.1.2 MIXED
+::1 ip6thingy
+1.1.1.3 multiple
+1.1.1.4 multiple
+::3 ip6-multiple
+::4 ip6-multiple
+''')
+ f.close()
+ self.ttl = 4200
+ self.resolver = Resolver('EtcHosts', self.ttl)
+
+ def testGetHostByName(self):
+ data = [('EXAMPLE', '1.1.1.1'),
+ ('EXAMPLE.EXAMPLETHING', '1.1.1.1'),
+ ('MIXED', '1.1.1.2'),
+ ]
+ ds = [self.resolver.getHostByName(n).addCallback(self.assertEqual, ip)
+ for n, ip in data]
+ return gatherResults(ds)
+
+
+ def test_lookupAddress(self):
+ """
+ L{hosts.Resolver.lookupAddress} returns a L{Deferred} which fires with A
+ records from the hosts file.
+ """
+ d = self.resolver.lookupAddress('multiple')
+ def resolved((results, authority, additional)):
+ self.assertEqual(
+ (RRHeader("multiple", A, IN, self.ttl,
+ Record_A("1.1.1.3", self.ttl)),
+ RRHeader("multiple", A, IN, self.ttl,
+ Record_A("1.1.1.4", self.ttl))),
+ results)
+ d.addCallback(resolved)
+ return d
+
+
+ def test_lookupIPV6Address(self):
+ """
+ L{hosts.Resolver.lookupIPV6Address} returns a L{Deferred} which fires
+ with AAAA records from the hosts file.
+ """
+ d = self.resolver.lookupIPV6Address('ip6-multiple')
+ def resolved((results, authority, additional)):
+ self.assertEqual(
+ (RRHeader("ip6-multiple", AAAA, IN, self.ttl,
+ Record_AAAA("::3", self.ttl)),
+ RRHeader("ip6-multiple", AAAA, IN, self.ttl,
+ Record_AAAA("::4", self.ttl))),
+ results)
+ d.addCallback(resolved)
+ return d
+
+
+ def test_lookupAllRecords(self):
+ """
+ L{hosts.Resolver.lookupAllRecords} returns a L{Deferred} which fires
+ with A records from the hosts file.
+ """
+ d = self.resolver.lookupAllRecords('mixed')
+ def resolved((results, authority, additional)):
+ self.assertEqual(
+ (RRHeader("mixed", A, IN, self.ttl,
+ Record_A("1.1.1.2", self.ttl)),),
+ results)
+ d.addCallback(resolved)
+ return d
+
+
+ def testNotImplemented(self):
+ return self.assertFailure(self.resolver.lookupMailExchange('EXAMPLE'),
+ NotImplementedError)
+
+ def testQuery(self):
+ d = self.resolver.query(Query('EXAMPLE'))
+ d.addCallback(lambda x: self.assertEqual(x[0][0].payload.dottedQuad(),
+ '1.1.1.1'))
+ return d
+
+ def test_lookupAddressNotFound(self):
+ """
+ L{hosts.Resolver.lookupAddress} returns a L{Deferred} which fires with
+ L{dns.DomainError} if the name passed in has no addresses in the hosts
+ file.
+ """
+ return self.assertFailure(self.resolver.lookupAddress('foueoa'),
+ DomainError)
+
+ def test_lookupIPV6AddressNotFound(self):
+ """
+ Like L{test_lookupAddressNotFound}, but for
+ L{hosts.Resolver.lookupIPV6Address}.
+ """
+ return self.assertFailure(self.resolver.lookupIPV6Address('foueoa'),
+ DomainError)
+
+ def test_lookupAllRecordsNotFound(self):
+ """
+ Like L{test_lookupAddressNotFound}, but for
+ L{hosts.Resolver.lookupAllRecords}.
+ """
+ return self.assertFailure(self.resolver.lookupAllRecords('foueoa'),
+ DomainError)
+
+
diff --git a/twisted/names/test/test_names.py b/twisted/names/test/test_names.py
new file mode 100644
index 0000000..a5b20c1
--- /dev/null
+++ b/twisted/names/test/test_names.py
@@ -0,0 +1,956 @@
+# -*- test-case-name: twisted.names.test.test_names -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.names.
+"""
+
+import socket, operator, copy
+from StringIO import StringIO
+
+from twisted.trial import unittest
+
+from twisted.internet import reactor, defer, error
+from twisted.internet.task import Clock
+from twisted.internet.defer import succeed
+from twisted.names import client, server, common, authority, dns
+from twisted.python import failure
+from twisted.names.error import DNSFormatError, DNSServerError, DNSNameError
+from twisted.names.error import DNSNotImplementedError, DNSQueryRefusedError
+from twisted.names.error import DNSUnknownError
+from twisted.names.dns import EFORMAT, ESERVER, ENAME, ENOTIMP, EREFUSED
+from twisted.names.dns import Message
+from twisted.names.client import Resolver
+from twisted.names.secondary import (
+ SecondaryAuthorityService, SecondaryAuthority)
+from twisted.names.test.test_client import StubPort
+
+from twisted.python.compat import reduce
+from twisted.test.proto_helpers import StringTransport, MemoryReactor
+
+def justPayload(results):
+ return [r.payload for r in results[0]]
+
+class NoFileAuthority(authority.FileAuthority):
+ def __init__(self, soa, records):
+ # Yes, skip FileAuthority
+ common.ResolverBase.__init__(self)
+ self.soa, self.records = soa, records
+
+
+soa_record = dns.Record_SOA(
+ mname = 'test-domain.com',
+ rname = 'root.test-domain.com',
+ serial = 100,
+ refresh = 1234,
+ minimum = 7654,
+ expire = 19283784,
+ retry = 15,
+ ttl=1
+ )
+
+reverse_soa = dns.Record_SOA(
+ mname = '93.84.28.in-addr.arpa',
+ rname = '93.84.28.in-addr.arpa',
+ serial = 120,
+ refresh = 54321,
+ minimum = 382,
+ expire = 11193983,
+ retry = 30,
+ ttl=3
+ )
+
+my_soa = dns.Record_SOA(
+ mname = 'my-domain.com',
+ rname = 'postmaster.test-domain.com',
+ serial = 130,
+ refresh = 12345,
+ minimum = 1,
+ expire = 999999,
+ retry = 100,
+ )
+
+test_domain_com = NoFileAuthority(
+ soa = ('test-domain.com', soa_record),
+ records = {
+ 'test-domain.com': [
+ soa_record,
+ dns.Record_A('127.0.0.1'),
+ dns.Record_NS('39.28.189.39'),
+ dns.Record_SPF('v=spf1 mx/30 mx:example.org/30 -all'),
+ dns.Record_SPF('v=spf1 +mx a:\0colo', '.example.com/28 -all not valid'),
+ dns.Record_MX(10, 'host.test-domain.com'),
+ dns.Record_HINFO(os='Linux', cpu='A Fast One, Dontcha know'),
+ dns.Record_CNAME('canonical.name.com'),
+ dns.Record_MB('mailbox.test-domain.com'),
+ dns.Record_MG('mail.group.someplace'),
+ dns.Record_TXT('A First piece of Text', 'a SecoNd piece'),
+ dns.Record_A6(0, 'ABCD::4321', ''),
+ dns.Record_A6(12, '0:0069::0', 'some.network.tld'),
+ dns.Record_A6(8, '0:5634:1294:AFCB:56AC:48EF:34C3:01FF', 'tra.la.la.net'),
+ dns.Record_TXT('Some more text, haha! Yes. \0 Still here?'),
+ dns.Record_MR('mail.redirect.or.whatever'),
+ dns.Record_MINFO(rmailbx='r mail box', emailbx='e mail box'),
+ dns.Record_AFSDB(subtype=1, hostname='afsdb.test-domain.com'),
+ dns.Record_RP(mbox='whatever.i.dunno', txt='some.more.text'),
+ dns.Record_WKS('12.54.78.12', socket.IPPROTO_TCP,
+ '\x12\x01\x16\xfe\xc1\x00\x01'),
+ dns.Record_NAPTR(100, 10, "u", "sip+E2U",
+ "!^.*$!sip:information@domain.tld!"),
+ dns.Record_AAAA('AF43:5634:1294:AFCB:56AC:48EF:34C3:01FF')],
+ 'http.tcp.test-domain.com': [
+ dns.Record_SRV(257, 16383, 43690, 'some.other.place.fool')
+ ],
+ 'host.test-domain.com': [
+ dns.Record_A('123.242.1.5'),
+ dns.Record_A('0.255.0.255'),
+ ],
+ 'host-two.test-domain.com': [
+#
+# Python bug
+# dns.Record_A('255.255.255.255'),
+#
+ dns.Record_A('255.255.255.254'),
+ dns.Record_A('0.0.0.0')
+ ],
+ 'cname.test-domain.com': [
+ dns.Record_CNAME('test-domain.com')
+ ],
+ 'anothertest-domain.com': [
+ dns.Record_A('1.2.3.4')],
+ }
+)
+
+reverse_domain = NoFileAuthority(
+ soa = ('93.84.28.in-addr.arpa', reverse_soa),
+ records = {
+ '123.93.84.28.in-addr.arpa': [
+ dns.Record_PTR('test.host-reverse.lookup.com'),
+ reverse_soa
+ ]
+ }
+)
+
+
+my_domain_com = NoFileAuthority(
+ soa = ('my-domain.com', my_soa),
+ records = {
+ 'my-domain.com': [
+ my_soa,
+ dns.Record_A('1.2.3.4', ttl='1S'),
+ dns.Record_NS('ns1.domain', ttl='2M'),
+ dns.Record_NS('ns2.domain', ttl='3H'),
+ dns.Record_SRV(257, 16383, 43690, 'some.other.place.fool', ttl='4D')
+ ]
+ }
+ )
+
+
+class ServerDNSTestCase(unittest.TestCase):
+ """
+ Test cases for DNS server and client.
+ """
+
+ def setUp(self):
+ self.factory = server.DNSServerFactory([
+ test_domain_com, reverse_domain, my_domain_com
+ ], verbose=2)
+
+ p = dns.DNSDatagramProtocol(self.factory)
+
+ while 1:
+ listenerTCP = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
+ # It's simpler to do the stop listening with addCleanup,
+ # even though we might not end up using this TCP port in
+ # the test (if the listenUDP below fails). Cleaning up
+ # this TCP port sooner than "cleanup time" would mean
+ # adding more code to keep track of the Deferred returned
+ # by stopListening.
+ self.addCleanup(listenerTCP.stopListening)
+ port = listenerTCP.getHost().port
+
+ try:
+ listenerUDP = reactor.listenUDP(port, p, interface="127.0.0.1")
+ except error.CannotListenError:
+ pass
+ else:
+ self.addCleanup(listenerUDP.stopListening)
+ break
+
+ self.listenerTCP = listenerTCP
+ self.listenerUDP = listenerUDP
+ self.resolver = client.Resolver(servers=[('127.0.0.1', port)])
+
+
+ def tearDown(self):
+ """
+ Clean up any server connections associated with the
+ L{DNSServerFactory} created in L{setUp}
+ """
+ # It'd be great if DNSServerFactory had a method that
+ # encapsulated this task. At least the necessary data is
+ # available, though.
+ for conn in self.factory.connections[:]:
+ conn.transport.loseConnection()
+
+
+ def namesTest(self, d, r):
+ self.response = None
+ def setDone(response):
+ self.response = response
+
+ def checkResults(ignored):
+ if isinstance(self.response, failure.Failure):
+ raise self.response
+ results = justPayload(self.response)
+ assert len(results) == len(r), "%s != %s" % (map(str, results), map(str, r))
+ for rec in results:
+ assert rec in r, "%s not in %s" % (rec, map(str, r))
+
+ d.addBoth(setDone)
+ d.addCallback(checkResults)
+ return d
+
+ def testAddressRecord1(self):
+ """Test simple DNS 'A' record queries"""
+ return self.namesTest(
+ self.resolver.lookupAddress('test-domain.com'),
+ [dns.Record_A('127.0.0.1', ttl=19283784)]
+ )
+
+
+ def testAddressRecord2(self):
+ """Test DNS 'A' record queries with multiple answers"""
+ return self.namesTest(
+ self.resolver.lookupAddress('host.test-domain.com'),
+ [dns.Record_A('123.242.1.5', ttl=19283784), dns.Record_A('0.255.0.255', ttl=19283784)]
+ )
+
+
+ def testAddressRecord3(self):
+ """Test DNS 'A' record queries with edge cases"""
+ return self.namesTest(
+ self.resolver.lookupAddress('host-two.test-domain.com'),
+ [dns.Record_A('255.255.255.254', ttl=19283784), dns.Record_A('0.0.0.0', ttl=19283784)]
+ )
+
+
+ def testAuthority(self):
+ """Test DNS 'SOA' record queries"""
+ return self.namesTest(
+ self.resolver.lookupAuthority('test-domain.com'),
+ [soa_record]
+ )
+
+
+ def testMailExchangeRecord(self):
+ """Test DNS 'MX' record queries"""
+ return self.namesTest(
+ self.resolver.lookupMailExchange('test-domain.com'),
+ [dns.Record_MX(10, 'host.test-domain.com', ttl=19283784)]
+ )
+
+
+ def testNameserver(self):
+ """Test DNS 'NS' record queries"""
+ return self.namesTest(
+ self.resolver.lookupNameservers('test-domain.com'),
+ [dns.Record_NS('39.28.189.39', ttl=19283784)]
+ )
+
+
+ def testHINFO(self):
+ """Test DNS 'HINFO' record queries"""
+ return self.namesTest(
+ self.resolver.lookupHostInfo('test-domain.com'),
+ [dns.Record_HINFO(os='Linux', cpu='A Fast One, Dontcha know', ttl=19283784)]
+ )
+
+ def testPTR(self):
+ """Test DNS 'PTR' record queries"""
+ return self.namesTest(
+ self.resolver.lookupPointer('123.93.84.28.in-addr.arpa'),
+ [dns.Record_PTR('test.host-reverse.lookup.com', ttl=11193983)]
+ )
+
+
+ def testCNAME(self):
+ """Test DNS 'CNAME' record queries"""
+ return self.namesTest(
+ self.resolver.lookupCanonicalName('test-domain.com'),
+ [dns.Record_CNAME('canonical.name.com', ttl=19283784)]
+ )
+
+ def testCNAMEAdditional(self):
+ """Test additional processing for CNAME records"""
+ return self.namesTest(
+ self.resolver.lookupAddress('cname.test-domain.com'),
+ [dns.Record_CNAME('test-domain.com', ttl=19283784), dns.Record_A('127.0.0.1', ttl=19283784)]
+ )
+
+ def testMB(self):
+ """Test DNS 'MB' record queries"""
+ return self.namesTest(
+ self.resolver.lookupMailBox('test-domain.com'),
+ [dns.Record_MB('mailbox.test-domain.com', ttl=19283784)]
+ )
+
+
+ def testMG(self):
+ """Test DNS 'MG' record queries"""
+ return self.namesTest(
+ self.resolver.lookupMailGroup('test-domain.com'),
+ [dns.Record_MG('mail.group.someplace', ttl=19283784)]
+ )
+
+
+ def testMR(self):
+ """Test DNS 'MR' record queries"""
+ return self.namesTest(
+ self.resolver.lookupMailRename('test-domain.com'),
+ [dns.Record_MR('mail.redirect.or.whatever', ttl=19283784)]
+ )
+
+
+ def testMINFO(self):
+ """Test DNS 'MINFO' record queries"""
+ return self.namesTest(
+ self.resolver.lookupMailboxInfo('test-domain.com'),
+ [dns.Record_MINFO(rmailbx='r mail box', emailbx='e mail box', ttl=19283784)]
+ )
+
+
+ def testSRV(self):
+ """Test DNS 'SRV' record queries"""
+ return self.namesTest(
+ self.resolver.lookupService('http.tcp.test-domain.com'),
+ [dns.Record_SRV(257, 16383, 43690, 'some.other.place.fool', ttl=19283784)]
+ )
+
+ def testAFSDB(self):
+ """Test DNS 'AFSDB' record queries"""
+ return self.namesTest(
+ self.resolver.lookupAFSDatabase('test-domain.com'),
+ [dns.Record_AFSDB(subtype=1, hostname='afsdb.test-domain.com', ttl=19283784)]
+ )
+
+
+ def testRP(self):
+ """Test DNS 'RP' record queries"""
+ return self.namesTest(
+ self.resolver.lookupResponsibility('test-domain.com'),
+ [dns.Record_RP(mbox='whatever.i.dunno', txt='some.more.text', ttl=19283784)]
+ )
+
+
+ def testTXT(self):
+ """Test DNS 'TXT' record queries"""
+ return self.namesTest(
+ self.resolver.lookupText('test-domain.com'),
+ [dns.Record_TXT('A First piece of Text', 'a SecoNd piece', ttl=19283784),
+ dns.Record_TXT('Some more text, haha! Yes. \0 Still here?', ttl=19283784)]
+ )
+
+
+ def test_spf(self):
+ """
+ L{DNSServerFactory} can serve I{SPF} resource records.
+ """
+ return self.namesTest(
+ self.resolver.lookupSenderPolicy('test-domain.com'),
+ [dns.Record_SPF('v=spf1 mx/30 mx:example.org/30 -all', ttl=19283784),
+ dns.Record_SPF('v=spf1 +mx a:\0colo', '.example.com/28 -all not valid', ttl=19283784)]
+ )
+
+
+ def testWKS(self):
+ """Test DNS 'WKS' record queries"""
+ return self.namesTest(
+ self.resolver.lookupWellKnownServices('test-domain.com'),
+ [dns.Record_WKS('12.54.78.12', socket.IPPROTO_TCP, '\x12\x01\x16\xfe\xc1\x00\x01', ttl=19283784)]
+ )
+
+
+ def testSomeRecordsWithTTLs(self):
+ result_soa = copy.copy(my_soa)
+ result_soa.ttl = my_soa.expire
+ return self.namesTest(
+ self.resolver.lookupAllRecords('my-domain.com'),
+ [result_soa,
+ dns.Record_A('1.2.3.4', ttl='1S'),
+ dns.Record_NS('ns1.domain', ttl='2M'),
+ dns.Record_NS('ns2.domain', ttl='3H'),
+ dns.Record_SRV(257, 16383, 43690, 'some.other.place.fool', ttl='4D')]
+ )
+
+
+ def testAAAA(self):
+ """Test DNS 'AAAA' record queries (IPv6)"""
+ return self.namesTest(
+ self.resolver.lookupIPV6Address('test-domain.com'),
+ [dns.Record_AAAA('AF43:5634:1294:AFCB:56AC:48EF:34C3:01FF', ttl=19283784)]
+ )
+
+ def testA6(self):
+ """Test DNS 'A6' record queries (IPv6)"""
+ return self.namesTest(
+ self.resolver.lookupAddress6('test-domain.com'),
+ [dns.Record_A6(0, 'ABCD::4321', '', ttl=19283784),
+ dns.Record_A6(12, '0:0069::0', 'some.network.tld', ttl=19283784),
+ dns.Record_A6(8, '0:5634:1294:AFCB:56AC:48EF:34C3:01FF', 'tra.la.la.net', ttl=19283784)]
+ )
+
+
+ def test_zoneTransfer(self):
+ """
+ Test DNS 'AXFR' queries (Zone transfer)
+ """
+ default_ttl = soa_record.expire
+ results = [copy.copy(r) for r in reduce(operator.add, test_domain_com.records.values())]
+ for r in results:
+ if r.ttl is None:
+ r.ttl = default_ttl
+ return self.namesTest(
+ self.resolver.lookupZone('test-domain.com').addCallback(lambda r: (r[0][:-1],)),
+ results
+ )
+
+
+ def testSimilarZonesDontInterfere(self):
+ """Tests that unrelated zones don't mess with each other."""
+ return self.namesTest(
+ self.resolver.lookupAddress("anothertest-domain.com"),
+ [dns.Record_A('1.2.3.4', ttl=19283784)]
+ )
+
+
+ def test_NAPTR(self):
+ """
+ Test DNS 'NAPTR' record queries.
+ """
+ return self.namesTest(
+ self.resolver.lookupNamingAuthorityPointer('test-domain.com'),
+ [dns.Record_NAPTR(100, 10, "u", "sip+E2U",
+ "!^.*$!sip:information@domain.tld!",
+ ttl=19283784)])
+
+
+
+class DNSServerFactoryTests(unittest.TestCase):
+ """
+ Tests for L{server.DNSServerFactory}.
+ """
+ def _messageReceivedTest(self, methodName, message):
+ """
+ Assert that the named method is called with the given message when
+ it is passed to L{DNSServerFactory.messageReceived}.
+ """
+ # Make it appear to have some queries so that
+ # DNSServerFactory.allowQuery allows it.
+ message.queries = [None]
+
+ receivedMessages = []
+ def fakeHandler(message, protocol, address):
+ receivedMessages.append((message, protocol, address))
+
+ class FakeProtocol(object):
+ def writeMessage(self, message):
+ pass
+
+ protocol = FakeProtocol()
+ factory = server.DNSServerFactory(None)
+ setattr(factory, methodName, fakeHandler)
+ factory.messageReceived(message, protocol)
+ self.assertEqual(receivedMessages, [(message, protocol, None)])
+
+
+ def test_notifyMessageReceived(self):
+ """
+ L{DNSServerFactory.messageReceived} passes messages with an opcode
+ of C{OP_NOTIFY} on to L{DNSServerFactory.handleNotify}.
+ """
+ # RFC 1996, section 4.5
+ opCode = 4
+ self._messageReceivedTest('handleNotify', Message(opCode=opCode))
+
+
+ def test_updateMessageReceived(self):
+ """
+ L{DNSServerFactory.messageReceived} passes messages with an opcode
+ of C{OP_UPDATE} on to L{DNSServerFactory.handleOther}.
+
+ This may change if the implementation ever covers update messages.
+ """
+ # RFC 2136, section 1.3
+ opCode = 5
+ self._messageReceivedTest('handleOther', Message(opCode=opCode))
+
+
+ def test_connectionTracking(self):
+ """
+ The C{connectionMade} and C{connectionLost} methods of
+ L{DNSServerFactory} cooperate to keep track of all
+ L{DNSProtocol} objects created by a factory which are
+ connected.
+ """
+ protoA, protoB = object(), object()
+ factory = server.DNSServerFactory()
+ factory.connectionMade(protoA)
+ self.assertEqual(factory.connections, [protoA])
+ factory.connectionMade(protoB)
+ self.assertEqual(factory.connections, [protoA, protoB])
+ factory.connectionLost(protoA)
+ self.assertEqual(factory.connections, [protoB])
+ factory.connectionLost(protoB)
+ self.assertEqual(factory.connections, [])
+
+
+class HelperTestCase(unittest.TestCase):
+ def testSerialGenerator(self):
+ f = self.mktemp()
+ a = authority.getSerial(f)
+ for i in range(20):
+ b = authority.getSerial(f)
+ self.failUnless(a < b)
+ a = b
+
+
+class AXFRTest(unittest.TestCase):
+ def setUp(self):
+ self.results = None
+ self.d = defer.Deferred()
+ self.d.addCallback(self._gotResults)
+ self.controller = client.AXFRController('fooby.com', self.d)
+
+ self.soa = dns.RRHeader(name='fooby.com', type=dns.SOA, cls=dns.IN, ttl=86400, auth=False,
+ payload=dns.Record_SOA(mname='fooby.com',
+ rname='hooj.fooby.com',
+ serial=100,
+ refresh=200,
+ retry=300,
+ expire=400,
+ minimum=500,
+ ttl=600))
+
+ self.records = [
+ self.soa,
+ dns.RRHeader(name='fooby.com', type=dns.NS, cls=dns.IN, ttl=700, auth=False,
+ payload=dns.Record_NS(name='ns.twistedmatrix.com', ttl=700)),
+
+ dns.RRHeader(name='fooby.com', type=dns.MX, cls=dns.IN, ttl=700, auth=False,
+ payload=dns.Record_MX(preference=10, exchange='mail.mv3d.com', ttl=700)),
+
+ dns.RRHeader(name='fooby.com', type=dns.A, cls=dns.IN, ttl=700, auth=False,
+ payload=dns.Record_A(address='64.123.27.105', ttl=700)),
+ self.soa
+ ]
+
+ def _makeMessage(self):
+ # hooray they all have the same message format
+ return dns.Message(id=999, answer=1, opCode=0, recDes=0, recAv=1, auth=1, rCode=0, trunc=0, maxSize=0)
+
+ def testBindAndTNamesStyle(self):
+ # Bind style = One big single message
+ m = self._makeMessage()
+ m.queries = [dns.Query('fooby.com', dns.AXFR, dns.IN)]
+ m.answers = self.records
+ self.controller.messageReceived(m, None)
+ self.assertEqual(self.results, self.records)
+
+ def _gotResults(self, result):
+ self.results = result
+
+ def testDJBStyle(self):
+ # DJB style = message per record
+ records = self.records[:]
+ while records:
+ m = self._makeMessage()
+ m.queries = [] # DJB *doesn't* specify any queries.. hmm..
+ m.answers = [records.pop(0)]
+ self.controller.messageReceived(m, None)
+ self.assertEqual(self.results, self.records)
+
+class FakeDNSDatagramProtocol(object):
+ def __init__(self):
+ self.queries = []
+ self.transport = StubPort()
+
+ def query(self, address, queries, timeout=10, id=None):
+ self.queries.append((address, queries, timeout, id))
+ return defer.fail(dns.DNSQueryTimeoutError(queries))
+
+ def removeResend(self, id):
+ # Ignore this for the time being.
+ pass
+
+class RetryLogic(unittest.TestCase):
+ testServers = [
+ '1.2.3.4',
+ '4.3.2.1',
+ 'a.b.c.d',
+ 'z.y.x.w']
+
+ def testRoundRobinBackoff(self):
+ addrs = [(x, 53) for x in self.testServers]
+ r = client.Resolver(resolv=None, servers=addrs)
+ r.protocol = proto = FakeDNSDatagramProtocol()
+ return r.lookupAddress("foo.example.com"
+ ).addCallback(self._cbRoundRobinBackoff
+ ).addErrback(self._ebRoundRobinBackoff, proto
+ )
+
+ def _cbRoundRobinBackoff(self, result):
+ raise unittest.FailTest("Lookup address succeeded, should have timed out")
+
+ def _ebRoundRobinBackoff(self, failure, fakeProto):
+ failure.trap(defer.TimeoutError)
+
+ # Assert that each server is tried with a particular timeout
+ # before the timeout is increased and the attempts are repeated.
+
+ for t in (1, 3, 11, 45):
+ tries = fakeProto.queries[:len(self.testServers)]
+ del fakeProto.queries[:len(self.testServers)]
+
+ tries.sort()
+ expected = list(self.testServers)
+ expected.sort()
+
+ for ((addr, query, timeout, id), expectedAddr) in zip(tries, expected):
+ self.assertEqual(addr, (expectedAddr, 53))
+ self.assertEqual(timeout, t)
+
+ self.failIf(fakeProto.queries)
+
+class ResolvConfHandling(unittest.TestCase):
+ def testMissing(self):
+ resolvConf = self.mktemp()
+ r = client.Resolver(resolv=resolvConf)
+ self.assertEqual(r.dynServers, [('127.0.0.1', 53)])
+ r._parseCall.cancel()
+
+ def testEmpty(self):
+ resolvConf = self.mktemp()
+ fObj = file(resolvConf, 'w')
+ fObj.close()
+ r = client.Resolver(resolv=resolvConf)
+ self.assertEqual(r.dynServers, [('127.0.0.1', 53)])
+ r._parseCall.cancel()
+
+
+
+class FilterAnswersTests(unittest.TestCase):
+ """
+ Test L{twisted.names.client.Resolver.filterAnswers}'s handling of various
+ error conditions it might encounter.
+ """
+ def setUp(self):
+ # Create a resolver pointed at an invalid server - we won't be hitting
+ # the network in any of these tests.
+ self.resolver = Resolver(servers=[('0.0.0.0', 0)])
+
+
+ def test_truncatedMessage(self):
+ """
+ Test that a truncated message results in an equivalent request made via
+ TCP.
+ """
+ m = Message(trunc=True)
+ m.addQuery('example.com')
+
+ def queryTCP(queries):
+ self.assertEqual(queries, m.queries)
+ response = Message()
+ response.answers = ['answer']
+ response.authority = ['authority']
+ response.additional = ['additional']
+ return succeed(response)
+ self.resolver.queryTCP = queryTCP
+ d = self.resolver.filterAnswers(m)
+ d.addCallback(
+ self.assertEqual, (['answer'], ['authority'], ['additional']))
+ return d
+
+
+ def _rcodeTest(self, rcode, exc):
+ m = Message(rCode=rcode)
+ err = self.resolver.filterAnswers(m)
+ err.trap(exc)
+
+
+ def test_formatError(self):
+ """
+ Test that a message with a result code of C{EFORMAT} results in a
+ failure wrapped around L{DNSFormatError}.
+ """
+ return self._rcodeTest(EFORMAT, DNSFormatError)
+
+
+ def test_serverError(self):
+ """
+ Like L{test_formatError} but for C{ESERVER}/L{DNSServerError}.
+ """
+ return self._rcodeTest(ESERVER, DNSServerError)
+
+
+ def test_nameError(self):
+ """
+ Like L{test_formatError} but for C{ENAME}/L{DNSNameError}.
+ """
+ return self._rcodeTest(ENAME, DNSNameError)
+
+
+ def test_notImplementedError(self):
+ """
+ Like L{test_formatError} but for C{ENOTIMP}/L{DNSNotImplementedError}.
+ """
+ return self._rcodeTest(ENOTIMP, DNSNotImplementedError)
+
+
+ def test_refusedError(self):
+ """
+ Like L{test_formatError} but for C{EREFUSED}/L{DNSQueryRefusedError}.
+ """
+ return self._rcodeTest(EREFUSED, DNSQueryRefusedError)
+
+
+ def test_refusedErrorUnknown(self):
+ """
+ Like L{test_formatError} but for an unrecognized error code and
+ L{DNSUnknownError}.
+ """
+ return self._rcodeTest(EREFUSED + 1, DNSUnknownError)
+
+
+
+class AuthorityTests(unittest.TestCase):
+ """
+ Tests for the basic response record selection code in L{FileAuthority}
+ (independent of its fileness).
+ """
+ def test_recordMissing(self):
+ """
+ If a L{FileAuthority} has a zone which includes an I{NS} record for a
+ particular name and that authority is asked for another record for the
+ same name which does not exist, the I{NS} record is not included in the
+ authority section of the response.
+ """
+ authority = NoFileAuthority(
+ soa=(str(soa_record.mname), soa_record),
+ records={
+ str(soa_record.mname): [
+ soa_record,
+ dns.Record_NS('1.2.3.4'),
+ ]})
+ d = authority.lookupAddress(str(soa_record.mname))
+ result = []
+ d.addCallback(result.append)
+ answer, authority, additional = result[0]
+ self.assertEqual(answer, [])
+ self.assertEqual(
+ authority, [
+ dns.RRHeader(
+ str(soa_record.mname), soa_record.TYPE,
+ ttl=soa_record.expire, payload=soa_record,
+ auth=True)])
+ self.assertEqual(additional, [])
+
+
+ def _referralTest(self, method):
+ """
+ Create an authority and make a request against it. Then verify that the
+ result is a referral, including no records in the answers or additional
+ sections, but with an I{NS} record in the authority section.
+ """
+ subdomain = 'example.' + str(soa_record.mname)
+ nameserver = dns.Record_NS('1.2.3.4')
+ authority = NoFileAuthority(
+ soa=(str(soa_record.mname), soa_record),
+ records={
+ subdomain: [
+ nameserver,
+ ]})
+ d = getattr(authority, method)(subdomain)
+ result = []
+ d.addCallback(result.append)
+ answer, authority, additional = result[0]
+ self.assertEqual(answer, [])
+ self.assertEqual(
+ authority, [dns.RRHeader(
+ subdomain, dns.NS, ttl=soa_record.expire,
+ payload=nameserver, auth=False)])
+ self.assertEqual(additional, [])
+
+
+ def test_referral(self):
+ """
+ When an I{NS} record is found for a child zone, it is included in the
+ authority section of the response. It is marked as non-authoritative if
+ the authority is not also authoritative for the child zone (RFC 2181,
+ section 6.1).
+ """
+ self._referralTest('lookupAddress')
+
+
+ def test_allRecordsReferral(self):
+ """
+ A referral is also generated for a request of type C{ALL_RECORDS}.
+ """
+ self._referralTest('lookupAllRecords')
+
+
+
+class NoInitialResponseTestCase(unittest.TestCase):
+
+ def test_no_answer(self):
+ """
+ If a request returns a L{dns.NS} response, but we can't connect to the
+ given server, the request fails with the error returned at connection.
+ """
+
+ def query(self, *args):
+ # Pop from the message list, so that it blows up if more queries
+ # are run than expected.
+ return succeed(messages.pop(0))
+
+ def queryProtocol(self, *args, **kwargs):
+ return defer.fail(socket.gaierror("Couldn't connect"))
+
+ resolver = Resolver(servers=[('0.0.0.0', 0)])
+ resolver._query = query
+ messages = []
+ # Let's patch dns.DNSDatagramProtocol.query, as there is no easy way to
+ # customize it.
+ self.patch(dns.DNSDatagramProtocol, "query", queryProtocol)
+
+ records = [
+ dns.RRHeader(name='fooba.com', type=dns.NS, cls=dns.IN, ttl=700,
+ auth=False,
+ payload=dns.Record_NS(name='ns.twistedmatrix.com',
+ ttl=700))]
+ m = dns.Message(id=999, answer=1, opCode=0, recDes=0, recAv=1, auth=1,
+ rCode=0, trunc=0, maxSize=0)
+ m.answers = records
+ messages.append(m)
+ return self.assertFailure(
+ resolver.getHostByName("fooby.com"), socket.gaierror)
+
+
+
+class SecondaryAuthorityServiceTests(unittest.TestCase):
+ """
+ Tests for L{SecondaryAuthorityService}, a service which keeps one or more
+ authorities up to date by doing zone transfers from a master.
+ """
+
+ def test_constructAuthorityFromHost(self):
+ """
+ L{SecondaryAuthorityService} can be constructed with a C{str} giving a
+ master server address and several domains, causing the creation of a
+ secondary authority for each domain and that master server address and
+ the default DNS port.
+ """
+ primary = '192.168.1.2'
+ service = SecondaryAuthorityService(
+ primary, ['example.com', 'example.org'])
+ self.assertEqual(service.primary, primary)
+ self.assertEqual(service._port, 53)
+
+ self.assertEqual(service.domains[0].primary, primary)
+ self.assertEqual(service.domains[0]._port, 53)
+ self.assertEqual(service.domains[0].domain, 'example.com')
+
+ self.assertEqual(service.domains[1].primary, primary)
+ self.assertEqual(service.domains[1]._port, 53)
+ self.assertEqual(service.domains[1].domain, 'example.org')
+
+
+ def test_constructAuthorityFromHostAndPort(self):
+ """
+ L{SecondaryAuthorityService.fromServerAddressAndDomains} constructs a
+ new L{SecondaryAuthorityService} from a C{str} giving a master server
+ address and DNS port and several domains, causing the creation of a secondary
+ authority for each domain and that master server address and the given
+ DNS port.
+ """
+ primary = '192.168.1.3'
+ port = 5335
+ service = SecondaryAuthorityService.fromServerAddressAndDomains(
+ (primary, port), ['example.net', 'example.edu'])
+ self.assertEqual(service.primary, primary)
+ self.assertEqual(service._port, 5335)
+
+ self.assertEqual(service.domains[0].primary, primary)
+ self.assertEqual(service.domains[0]._port, port)
+ self.assertEqual(service.domains[0].domain, 'example.net')
+
+ self.assertEqual(service.domains[1].primary, primary)
+ self.assertEqual(service.domains[1]._port, port)
+ self.assertEqual(service.domains[1].domain, 'example.edu')
+
+
+
+class SecondaryAuthorityTests(unittest.TestCase):
+ """
+ L{twisted.names.secondary.SecondaryAuthority} correctly constructs objects
+ with a specified IP address and optionally specified DNS port.
+ """
+
+ def test_defaultPort(self):
+ """
+ When constructed using L{SecondaryAuthority.__init__}, the default port
+ of 53 is used.
+ """
+ secondary = SecondaryAuthority('192.168.1.1', 'inside.com')
+ self.assertEqual(secondary.primary, '192.168.1.1')
+ self.assertEqual(secondary._port, 53)
+ self.assertEqual(secondary.domain, 'inside.com')
+
+
+ def test_explicitPort(self):
+ """
+ When constructed using L{SecondaryAuthority.fromServerAddressAndDomain},
+ the specified port is used.
+ """
+ secondary = SecondaryAuthority.fromServerAddressAndDomain(
+ ('192.168.1.1', 5353), 'inside.com')
+ self.assertEqual(secondary.primary, '192.168.1.1')
+ self.assertEqual(secondary._port, 5353)
+ self.assertEqual(secondary.domain, 'inside.com')
+
+
+ def test_transfer(self):
+ """
+ An attempt is made to transfer the zone for the domain the
+ L{SecondaryAuthority} was constructed with from the server address it
+ was constructed with when L{SecondaryAuthority.transfer} is called.
+ """
+ class ClockMemoryReactor(Clock, MemoryReactor):
+ def __init__(self):
+ Clock.__init__(self)
+ MemoryReactor.__init__(self)
+
+ secondary = SecondaryAuthority.fromServerAddressAndDomain(
+ ('192.168.1.2', 1234), 'example.com')
+ secondary._reactor = reactor = ClockMemoryReactor()
+
+ secondary.transfer()
+
+ # Verify a connection attempt to the server address above
+ host, port, factory, timeout, bindAddress = reactor.tcpClients.pop(0)
+ self.assertEqual(host, '192.168.1.2')
+ self.assertEqual(port, 1234)
+
+ # See if a zone transfer query is issued.
+ proto = factory.buildProtocol((host, port))
+ transport = StringTransport()
+ proto.makeConnection(transport)
+
+ msg = Message()
+ # DNSProtocol.writeMessage length encodes the message by prepending a
+ # 2 byte message length to the buffered value.
+ msg.decode(StringIO(transport.value()[2:]))
+
+ self.assertEqual(
+ [dns.Query('example.com', dns.AXFR, dns.IN)], msg.queries)
diff --git a/twisted/names/test/test_rootresolve.py b/twisted/names/test/test_rootresolve.py
new file mode 100644
index 0000000..b3d34f3
--- /dev/null
+++ b/twisted/names/test/test_rootresolve.py
@@ -0,0 +1,705 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for Twisted.names' root resolver.
+"""
+
+from random import randrange
+
+from zope.interface import implements
+from zope.interface.verify import verifyClass
+
+from twisted.python.log import msg
+from twisted.trial import util
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import Deferred, succeed, gatherResults
+from twisted.internet.task import Clock
+from twisted.internet.address import IPv4Address
+from twisted.internet.interfaces import IReactorUDP, IUDPTransport
+from twisted.names.root import Resolver, lookupNameservers, lookupAddress
+from twisted.names.root import extractAuthority, discoverAuthority, retry
+from twisted.names.dns import IN, HS, A, NS, CNAME, OK, ENAME, Record_CNAME
+from twisted.names.dns import Query, Message, RRHeader, Record_A, Record_NS
+from twisted.names.error import DNSNameError, ResolverError
+
+
+class MemoryDatagramTransport(object):
+ """
+ This L{IUDPTransport} implementation enforces the usual connection rules
+ and captures sent traffic in a list for later inspection.
+
+ @ivar _host: The host address to which this transport is bound.
+ @ivar _protocol: The protocol connected to this transport.
+ @ivar _sentPackets: A C{list} of two-tuples of the datagrams passed to
+ C{write} and the addresses to which they are destined.
+
+ @ivar _connectedTo: C{None} if this transport is unconnected, otherwise an
+ address to which all traffic is supposedly sent.
+
+ @ivar _maxPacketSize: An C{int} giving the maximum length of a datagram
+ which will be successfully handled by C{write}.
+ """
+ implements(IUDPTransport)
+
+ def __init__(self, host, protocol, maxPacketSize):
+ self._host = host
+ self._protocol = protocol
+ self._sentPackets = []
+ self._connectedTo = None
+ self._maxPacketSize = maxPacketSize
+
+
+ def getHost(self):
+ """
+ Return the address which this transport is pretending to be bound
+ to.
+ """
+ return IPv4Address('UDP', *self._host)
+
+
+ def connect(self, host, port):
+ """
+ Connect this transport to the given address.
+ """
+ if self._connectedTo is not None:
+ raise ValueError("Already connected")
+ self._connectedTo = (host, port)
+
+
+ def write(self, datagram, addr=None):
+ """
+ Send the given datagram.
+ """
+ if addr is None:
+ addr = self._connectedTo
+ if addr is None:
+ raise ValueError("Need an address")
+ if len(datagram) > self._maxPacketSize:
+ raise ValueError("Packet too big")
+ self._sentPackets.append((datagram, addr))
+
+
+ def stopListening(self):
+ """
+ Shut down this transport.
+ """
+ self._protocol.stopProtocol()
+ return succeed(None)
+
+verifyClass(IUDPTransport, MemoryDatagramTransport)
+
+
+
+class MemoryReactor(Clock):
+ """
+ An L{IReactorTime} and L{IReactorUDP} provider.
+
+ Time is controlled deterministically via the base class, L{Clock}. UDP is
+ handled in-memory by connecting protocols to instances of
+ L{MemoryDatagramTransport}.
+
+ @ivar udpPorts: A C{dict} mapping port numbers to instances of
+ L{MemoryDatagramTransport}.
+ """
+ implements(IReactorUDP)
+
+ def __init__(self):
+ Clock.__init__(self)
+ self.udpPorts = {}
+
+
+ def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
+ """
+ Pretend to bind a UDP port and connect the given protocol to it.
+ """
+ if port == 0:
+ while True:
+ port = randrange(1, 2 ** 16)
+ if port not in self.udpPorts:
+ break
+ if port in self.udpPorts:
+ raise ValueError("Address in use")
+ transport = MemoryDatagramTransport(
+ (interface, port), protocol, maxPacketSize)
+ self.udpPorts[port] = transport
+ protocol.makeConnection(transport)
+ return transport
+
+verifyClass(IReactorUDP, MemoryReactor)
+
+
+
+class RootResolverTests(TestCase):
+ """
+ Tests for L{twisted.names.root.Resolver}.
+ """
+ def _queryTest(self, filter):
+ """
+ Invoke L{Resolver._query} and verify that it sends the correct DNS
+ query. Deliver a canned response to the query and return whatever the
+ L{Deferred} returned by L{Resolver._query} fires with.
+
+ @param filter: The value to pass for the C{filter} parameter to
+ L{Resolver._query}.
+ """
+ reactor = MemoryReactor()
+ resolver = Resolver([], reactor=reactor)
+ d = resolver._query(
+ Query('foo.example.com', A, IN), [('1.1.2.3', 1053)], (30,),
+ filter)
+
+ # A UDP port should have been started.
+ portNumber, transport = reactor.udpPorts.popitem()
+
+ # And a DNS packet sent.
+ [(packet, address)] = transport._sentPackets
+
+ msg = Message()
+ msg.fromStr(packet)
+
+ # It should be a query with the parameters used above.
+ self.assertEqual(msg.queries, [Query('foo.example.com', A, IN)])
+ self.assertEqual(msg.answers, [])
+ self.assertEqual(msg.authority, [])
+ self.assertEqual(msg.additional, [])
+
+ response = []
+ d.addCallback(response.append)
+ self.assertEqual(response, [])
+
+ # Once a reply is received, the Deferred should fire.
+ del msg.queries[:]
+ msg.answer = 1
+ msg.answers.append(RRHeader('foo.example.com', payload=Record_A('5.8.13.21')))
+ transport._protocol.datagramReceived(msg.toStr(), ('1.1.2.3', 1053))
+ return response[0]
+
+
+ def test_filteredQuery(self):
+ """
+ L{Resolver._query} accepts a L{Query} instance and an address, issues
+ the query, and returns a L{Deferred} which fires with the response to
+ the query. If a true value is passed for the C{filter} parameter, the
+ result is a three-tuple of lists of records.
+ """
+ answer, authority, additional = self._queryTest(True)
+ self.assertEqual(
+ answer,
+ [RRHeader('foo.example.com', payload=Record_A('5.8.13.21', ttl=0))])
+ self.assertEqual(authority, [])
+ self.assertEqual(additional, [])
+
+
+ def test_unfilteredQuery(self):
+ """
+ Similar to L{test_filteredQuery}, but for the case where a false value
+ is passed for the C{filter} parameter. In this case, the result is a
+ L{Message} instance.
+ """
+ message = self._queryTest(False)
+ self.assertIsInstance(message, Message)
+ self.assertEqual(message.queries, [])
+ self.assertEqual(
+ message.answers,
+ [RRHeader('foo.example.com', payload=Record_A('5.8.13.21', ttl=0))])
+ self.assertEqual(message.authority, [])
+ self.assertEqual(message.additional, [])
+
+
+ def _respond(self, answers=[], authority=[], additional=[], rCode=OK):
+ """
+ Create a L{Message} suitable for use as a response to a query.
+
+ @param answers: A C{list} of two-tuples giving data for the answers
+ section of the message. The first element of each tuple is a name
+ for the L{RRHeader}. The second element is the payload.
+ @param authority: A C{list} like C{answers}, but for the authority
+ section of the response.
+ @param additional: A C{list} like C{answers}, but for the
+ additional section of the response.
+ @param rCode: The response code the message will be created with.
+
+ @return: A new L{Message} initialized with the given values.
+ """
+ response = Message(rCode=rCode)
+ for (section, data) in [(response.answers, answers),
+ (response.authority, authority),
+ (response.additional, additional)]:
+ section.extend([
+ RRHeader(name, record.TYPE, getattr(record, 'CLASS', IN),
+ payload=record)
+ for (name, record) in data])
+ return response
+
+
+ def _getResolver(self, serverResponses, maximumQueries=10):
+ """
+ Create and return a new L{root.Resolver} modified to resolve queries
+ against the record data represented by C{servers}.
+
+ @param serverResponses: A mapping from dns server addresses to
+ mappings. The inner mappings are from query two-tuples (name,
+ type) to dictionaries suitable for use as **arguments to
+ L{_respond}. See that method for details.
+ """
+ roots = ['1.1.2.3']
+ resolver = Resolver(roots, maximumQueries)
+
+ def query(query, serverAddresses, timeout, filter):
+ msg("Query for QNAME %s at %r" % (query.name, serverAddresses))
+ for addr in serverAddresses:
+ try:
+ server = serverResponses[addr]
+ except KeyError:
+ continue
+ records = server[str(query.name), query.type]
+ return succeed(self._respond(**records))
+ resolver._query = query
+ return resolver
+
+
+ def test_lookupAddress(self):
+ """
+ L{root.Resolver.lookupAddress} looks up the I{A} records for the
+ specified hostname by first querying one of the root servers the
+ resolver was created with and then following the authority delegations
+ until a result is received.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('foo.example.com', A): {
+ 'authority': [('foo.example.com', Record_NS('ns1.example.com'))],
+ 'additional': [('ns1.example.com', Record_A('34.55.89.144'))],
+ },
+ },
+ ('34.55.89.144', 53): {
+ ('foo.example.com', A): {
+ 'answers': [('foo.example.com', Record_A('10.0.0.1'))],
+ }
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('foo.example.com')
+ d.addCallback(lambda (ans, auth, add): ans[0].payload.dottedQuad())
+ d.addCallback(self.assertEqual, '10.0.0.1')
+ return d
+
+
+ def test_lookupChecksClass(self):
+ """
+ If a response includes a record with a class different from the one
+ in the query, it is ignored and lookup continues until a record with
+ the right class is found.
+ """
+ badClass = Record_A('10.0.0.1')
+ badClass.CLASS = HS
+ servers = {
+ ('1.1.2.3', 53): {
+ ('foo.example.com', A): {
+ 'answers': [('foo.example.com', badClass)],
+ 'authority': [('foo.example.com', Record_NS('ns1.example.com'))],
+ 'additional': [('ns1.example.com', Record_A('10.0.0.2'))],
+ },
+ },
+ ('10.0.0.2', 53): {
+ ('foo.example.com', A): {
+ 'answers': [('foo.example.com', Record_A('10.0.0.3'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('foo.example.com')
+ d.addCallback(lambda (ans, auth, add): ans[0].payload)
+ d.addCallback(self.assertEqual, Record_A('10.0.0.3'))
+ return d
+
+
+ def test_missingGlue(self):
+ """
+ If an intermediate response includes no glue records for the
+ authorities, separate queries are made to find those addresses.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('foo.example.com', A): {
+ 'authority': [('foo.example.com', Record_NS('ns1.example.org'))],
+ # Conspicuous lack of an additional section naming ns1.example.com
+ },
+ ('ns1.example.org', A): {
+ 'answers': [('ns1.example.org', Record_A('10.0.0.1'))],
+ },
+ },
+ ('10.0.0.1', 53): {
+ ('foo.example.com', A): {
+ 'answers': [('foo.example.com', Record_A('10.0.0.2'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('foo.example.com')
+ d.addCallback(lambda (ans, auth, add): ans[0].payload.dottedQuad())
+ d.addCallback(self.assertEqual, '10.0.0.2')
+ return d
+
+
+ def test_missingName(self):
+ """
+ If a name is missing, L{Resolver.lookupAddress} returns a L{Deferred}
+ which fails with L{DNSNameError}.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('foo.example.com', A): {
+ 'rCode': ENAME,
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('foo.example.com')
+ return self.assertFailure(d, DNSNameError)
+
+
+ def test_answerless(self):
+ """
+ If a query is responded to with no answers or nameserver records, the
+ L{Deferred} returned by L{Resolver.lookupAddress} fires with
+ L{ResolverError}.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ return self.assertFailure(d, ResolverError)
+
+
+ def test_delegationLookupError(self):
+ """
+ If there is an error resolving the nameserver in a delegation response,
+ the L{Deferred} returned by L{Resolver.lookupAddress} fires with that
+ error.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'authority': [('example.com', Record_NS('ns1.example.com'))],
+ },
+ ('ns1.example.com', A): {
+ 'rCode': ENAME,
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ return self.assertFailure(d, DNSNameError)
+
+
+ def test_delegationLookupEmpty(self):
+ """
+ If there are no records in the response to a lookup of a delegation
+ nameserver, the L{Deferred} returned by L{Resolver.lookupAddress} fires
+ with L{ResolverError}.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'authority': [('example.com', Record_NS('ns1.example.com'))],
+ },
+ ('ns1.example.com', A): {
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ return self.assertFailure(d, ResolverError)
+
+
+ def test_lookupNameservers(self):
+ """
+ L{Resolver.lookupNameservers} is like L{Resolver.lookupAddress}, except
+ it queries for I{NS} records instead of I{A} records.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'rCode': ENAME,
+ },
+ ('example.com', NS): {
+ 'answers': [('example.com', Record_NS('ns1.example.com'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupNameservers('example.com')
+ d.addCallback(lambda (ans, auth, add): str(ans[0].payload.name))
+ d.addCallback(self.assertEqual, 'ns1.example.com')
+ return d
+
+
+ def test_returnCanonicalName(self):
+ """
+ If a I{CNAME} record is encountered as the answer to a query for
+ another record type, that record is returned as the answer.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'answers': [('example.com', Record_CNAME('example.net')),
+ ('example.net', Record_A('10.0.0.7'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ d.addCallback(lambda (ans, auth, add): ans)
+ d.addCallback(
+ self.assertEqual,
+ [RRHeader('example.com', CNAME, payload=Record_CNAME('example.net')),
+ RRHeader('example.net', A, payload=Record_A('10.0.0.7'))])
+ return d
+
+
+ def test_followCanonicalName(self):
+ """
+ If no record of the requested type is included in a response, but a
+ I{CNAME} record for the query name is included, queries are made to
+ resolve the value of the I{CNAME}.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'answers': [('example.com', Record_CNAME('example.net'))],
+ },
+ ('example.net', A): {
+ 'answers': [('example.net', Record_A('10.0.0.5'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ d.addCallback(lambda (ans, auth, add): ans)
+ d.addCallback(
+ self.assertEqual,
+ [RRHeader('example.com', CNAME, payload=Record_CNAME('example.net')),
+ RRHeader('example.net', A, payload=Record_A('10.0.0.5'))])
+ return d
+
+
+ def test_detectCanonicalNameLoop(self):
+ """
+ If there is a cycle between I{CNAME} records in a response, this is
+ detected and the L{Deferred} returned by the lookup method fails
+ with L{ResolverError}.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ ('example.com', A): {
+ 'answers': [('example.com', Record_CNAME('example.net')),
+ ('example.net', Record_CNAME('example.com'))],
+ },
+ },
+ }
+ resolver = self._getResolver(servers)
+ d = resolver.lookupAddress('example.com')
+ return self.assertFailure(d, ResolverError)
+
+
+ def test_boundedQueries(self):
+ """
+ L{Resolver.lookupAddress} won't issue more queries following
+ delegations than the limit passed to its initializer.
+ """
+ servers = {
+ ('1.1.2.3', 53): {
+ # First query - force it to start over with a name lookup of
+ # ns1.example.com
+ ('example.com', A): {
+ 'authority': [('example.com', Record_NS('ns1.example.com'))],
+ },
+ # Second query - let it resume the original lookup with the
+ # address of the nameserver handling the delegation.
+ ('ns1.example.com', A): {
+ 'answers': [('ns1.example.com', Record_A('10.0.0.2'))],
+ },
+ },
+ ('10.0.0.2', 53): {
+ # Third query - let it jump straight to asking the
+ # delegation server by including its address here (different
+ # case from the first query).
+ ('example.com', A): {
+ 'authority': [('example.com', Record_NS('ns2.example.com'))],
+ 'additional': [('ns2.example.com', Record_A('10.0.0.3'))],
+ },
+ },
+ ('10.0.0.3', 53): {
+ # Fourth query - give it the answer, we're done.
+ ('example.com', A): {
+ 'answers': [('example.com', Record_A('10.0.0.4'))],
+ },
+ },
+ }
+
+ # Make two resolvers. One which is allowed to make 3 queries
+ # maximum, and so will fail, and on which may make 4, and so should
+ # succeed.
+ failer = self._getResolver(servers, 3)
+ failD = self.assertFailure(
+ failer.lookupAddress('example.com'), ResolverError)
+
+ succeeder = self._getResolver(servers, 4)
+ succeedD = succeeder.lookupAddress('example.com')
+ succeedD.addCallback(lambda (ans, auth, add): ans[0].payload)
+ succeedD.addCallback(self.assertEqual, Record_A('10.0.0.4'))
+
+ return gatherResults([failD, succeedD])
+
+
+ def test_discoveredAuthorityDeprecated(self):
+ """
+ Calling L{Resolver.discoveredAuthority} produces a deprecation warning.
+ """
+ resolver = Resolver([])
+ d = resolver.discoveredAuthority('127.0.0.1', 'example.com', IN, A, (0,))
+
+ warnings = self.flushWarnings([
+ self.test_discoveredAuthorityDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.Resolver.discoveredAuthority is deprecated since '
+ 'Twisted 10.0. Use twisted.names.client.Resolver directly, instead.')
+ self.assertEqual(len(warnings), 1)
+
+ # This will time out quickly, but we need to wait for it because there
+ # are resources associated with.
+ d.addErrback(lambda ignored: None)
+ return d
+
+
+
+class StubDNSDatagramProtocol:
+ """
+ A do-nothing stand-in for L{DNSDatagramProtocol} which can be used to avoid
+ network traffic in tests where that kind of thing doesn't matter.
+ """
+ def query(self, *a, **kw):
+ return Deferred()
+
+
+
+_retrySuppression = util.suppress(
+ category=DeprecationWarning,
+ message=(
+ 'twisted.names.root.retry is deprecated since Twisted 10.0. Use a '
+ 'Resolver object for retry logic.'))
+
+
+class DiscoveryToolsTests(TestCase):
+ """
+ Tests for the free functions in L{twisted.names.root} which help out with
+ authority discovery. Since these are mostly deprecated, these are mostly
+ deprecation tests.
+ """
+ def test_lookupNameserversDeprecated(self):
+ """
+ Calling L{root.lookupNameservers} produces a deprecation warning.
+ """
+ # Don't care about the return value, since it will never have a result,
+ # since StubDNSDatagramProtocol doesn't actually work.
+ lookupNameservers('example.com', '127.0.0.1', StubDNSDatagramProtocol())
+
+ warnings = self.flushWarnings([
+ self.test_lookupNameserversDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.lookupNameservers is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupNameservers '
+ 'instead.')
+ self.assertEqual(len(warnings), 1)
+ test_lookupNameserversDeprecated.suppress = [_retrySuppression]
+
+
+ def test_lookupAddressDeprecated(self):
+ """
+ Calling L{root.lookupAddress} produces a deprecation warning.
+ """
+ # Don't care about the return value, since it will never have a result,
+ # since StubDNSDatagramProtocol doesn't actually work.
+ lookupAddress('example.com', '127.0.0.1', StubDNSDatagramProtocol())
+
+ warnings = self.flushWarnings([
+ self.test_lookupAddressDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.lookupAddress is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupAddress '
+ 'instead.')
+ self.assertEqual(len(warnings), 1)
+ test_lookupAddressDeprecated.suppress = [_retrySuppression]
+
+
+ def test_extractAuthorityDeprecated(self):
+ """
+ Calling L{root.extractAuthority} produces a deprecation warning.
+ """
+ extractAuthority(Message(), {})
+
+ warnings = self.flushWarnings([
+ self.test_extractAuthorityDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.extractAuthority is deprecated since Twisted '
+ '10.0. Please inspect the Message object directly.')
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_discoverAuthorityDeprecated(self):
+ """
+ Calling L{root.discoverAuthority} produces a deprecation warning.
+ """
+ discoverAuthority(
+ 'example.com', ['10.0.0.1'], p=StubDNSDatagramProtocol())
+
+ warnings = self.flushWarnings([
+ self.test_discoverAuthorityDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.discoverAuthority is deprecated since Twisted '
+ '10.0. Use twisted.names.root.Resolver.lookupNameservers '
+ 'instead.')
+ self.assertEqual(len(warnings), 1)
+
+ # discoverAuthority is implemented in terms of deprecated functions,
+ # too. Ignore those.
+ test_discoverAuthorityDeprecated.suppress = [
+ util.suppress(
+ category=DeprecationWarning,
+ message=(
+ 'twisted.names.root.lookupNameservers is deprecated since '
+ 'Twisted 10.0. Use '
+ 'twisted.names.root.Resolver.lookupNameservers instead.')),
+ _retrySuppression]
+
+
+ def test_retryDeprecated(self):
+ """
+ Calling L{root.retry} produces a deprecation warning.
+ """
+ retry([0], StubDNSDatagramProtocol())
+
+ warnings = self.flushWarnings([
+ self.test_retryDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.names.root.retry is deprecated since Twisted '
+ '10.0. Use a Resolver object for retry logic.')
+ self.assertEqual(len(warnings), 1)
diff --git a/twisted/names/test/test_srvconnect.py b/twisted/names/test/test_srvconnect.py
new file mode 100644
index 0000000..6135359
--- /dev/null
+++ b/twisted/names/test/test_srvconnect.py
@@ -0,0 +1,133 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for L{twisted.names.srvconnect}.
+"""
+
+from twisted.internet import defer, protocol
+from twisted.names import client, dns, srvconnect
+from twisted.names.common import ResolverBase
+from twisted.names.error import DNSNameError
+from twisted.internet.error import DNSLookupError
+from twisted.trial import unittest
+from twisted.test.proto_helpers import MemoryReactor
+
+
+class FakeResolver(ResolverBase):
+ """
+ Resolver that only gives out one given result.
+
+ Either L{results} or L{failure} must be set and will be used for
+ the return value of L{_lookup}
+
+ @ivar results: List of L{dns.RRHeader} for the desired result.
+ @type results: C{list}
+ @ivar failure: Failure with an exception from L{twisted.names.error}.
+ @type failure: L{Failure<twisted.python.failure.Failure>}
+ """
+
+ def __init__(self, results=None, failure=None):
+ self.results = results
+ self.failure = failure
+
+ def _lookup(self, name, cls, qtype, timeout):
+ """
+ Return the result or failure on lookup.
+ """
+ if self.results is not None:
+ return defer.succeed((self.results, [], []))
+ else:
+ return defer.fail(self.failure)
+
+
+
+class DummyFactory(protocol.ClientFactory):
+ """
+ Dummy client factory that stores the reason of connection failure.
+ """
+ def __init__(self):
+ self.reason = None
+
+ def clientConnectionFailed(self, connector, reason):
+ self.reason = reason
+
+class SRVConnectorTest(unittest.TestCase):
+
+ def setUp(self):
+ self.patch(client, 'theResolver', FakeResolver())
+ self.reactor = MemoryReactor()
+ self.factory = DummyFactory()
+ self.connector = srvconnect.SRVConnector(self.reactor, 'xmpp-server',
+ 'example.org', self.factory)
+
+
+ def test_SRVPresent(self):
+ """
+ Test connectTCP gets called with the address from the SRV record.
+ """
+ payload = dns.Record_SRV(port=6269, target='host.example.org', ttl=60)
+ client.theResolver.results = [dns.RRHeader(name='example.org',
+ type=dns.SRV,
+ cls=dns.IN, ttl=60,
+ payload=payload)]
+ self.connector.connect()
+
+ self.assertIdentical(None, self.factory.reason)
+ self.assertEqual(
+ self.reactor.tcpClients.pop()[:2], ('host.example.org', 6269))
+
+
+ def test_SRVNotPresent(self):
+ """
+ Test connectTCP gets called with fallback parameters on NXDOMAIN.
+ """
+ client.theResolver.failure = DNSNameError('example.org')
+ self.connector.connect()
+
+ self.assertIdentical(None, self.factory.reason)
+ self.assertEqual(
+ self.reactor.tcpClients.pop()[:2], ('example.org', 'xmpp-server'))
+
+
+ def test_SRVNoResult(self):
+ """
+ Test connectTCP gets called with fallback parameters on empty result.
+ """
+ client.theResolver.results = []
+ self.connector.connect()
+
+ self.assertIdentical(None, self.factory.reason)
+ self.assertEqual(
+ self.reactor.tcpClients.pop()[:2], ('example.org', 'xmpp-server'))
+
+
+ def test_SRVBadResult(self):
+ """
+ Test connectTCP gets called with fallback parameters on bad result.
+ """
+ client.theResolver.results = [dns.RRHeader(name='example.org',
+ type=dns.CNAME,
+ cls=dns.IN, ttl=60,
+ payload=None)]
+ self.connector.connect()
+
+ self.assertIdentical(None, self.factory.reason)
+ self.assertEqual(
+ self.reactor.tcpClients.pop()[:2], ('example.org', 'xmpp-server'))
+
+
+ def test_SRVNoService(self):
+ """
+ Test that connecting fails when no service is present.
+ """
+ payload = dns.Record_SRV(port=5269, target='.', ttl=60)
+ client.theResolver.results = [dns.RRHeader(name='example.org',
+ type=dns.SRV,
+ cls=dns.IN, ttl=60,
+ payload=payload)]
+ self.connector.connect()
+
+ self.assertNotIdentical(None, self.factory.reason)
+ self.factory.reason.trap(DNSLookupError)
+ self.assertEqual(self.reactor.tcpClients, [])
diff --git a/twisted/names/test/test_tap.py b/twisted/names/test/test_tap.py
new file mode 100644
index 0000000..0858d26
--- /dev/null
+++ b/twisted/names/test/test_tap.py
@@ -0,0 +1,99 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.names.tap}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.python.usage import UsageError
+from twisted.names.tap import Options, _buildResolvers
+from twisted.names.dns import PORT
+from twisted.names.secondary import SecondaryAuthorityService
+from twisted.names.resolve import ResolverChain
+from twisted.names.client import Resolver
+
+class OptionsTests(TestCase):
+ """
+ Tests for L{Options}, defining how command line arguments for the DNS server
+ are parsed.
+ """
+ def test_malformedSecondary(self):
+ """
+ If the value supplied for an I{--secondary} option does not provide a
+ server IP address, optional port number, and domain name,
+ L{Options.parseOptions} raises L{UsageError}.
+ """
+ options = Options()
+ self.assertRaises(
+ UsageError, options.parseOptions, ['--secondary', ''])
+ self.assertRaises(
+ UsageError, options.parseOptions, ['--secondary', '1.2.3.4'])
+ self.assertRaises(
+ UsageError, options.parseOptions, ['--secondary', '1.2.3.4:hello'])
+ self.assertRaises(
+ UsageError, options.parseOptions,
+ ['--secondary', '1.2.3.4:hello/example.com'])
+
+
+ def test_secondary(self):
+ """
+ An argument of the form C{"ip/domain"} is parsed by L{Options} for the
+ I{--secondary} option and added to its list of secondaries, using the
+ default DNS port number.
+ """
+ options = Options()
+ options.parseOptions(['--secondary', '1.2.3.4/example.com'])
+ self.assertEqual(
+ [(('1.2.3.4', PORT), ['example.com'])], options.secondaries)
+
+
+ def test_secondaryExplicitPort(self):
+ """
+ An argument of the form C{"ip:port/domain"} can be used to specify an
+ alternate port number for for which to act as a secondary.
+ """
+ options = Options()
+ options.parseOptions(['--secondary', '1.2.3.4:5353/example.com'])
+ self.assertEqual(
+ [(('1.2.3.4', 5353), ['example.com'])], options.secondaries)
+
+
+ def test_secondaryAuthorityServices(self):
+ """
+ After parsing I{--secondary} options, L{Options} constructs a
+ L{SecondaryAuthorityService} instance for each configured secondary.
+ """
+ options = Options()
+ options.parseOptions(['--secondary', '1.2.3.4:5353/example.com',
+ '--secondary', '1.2.3.5:5354/example.com'])
+ self.assertEqual(len(options.svcs), 2)
+ secondary = options.svcs[0]
+ self.assertIsInstance(options.svcs[0], SecondaryAuthorityService)
+ self.assertEqual(secondary.primary, '1.2.3.4')
+ self.assertEqual(secondary._port, 5353)
+ secondary = options.svcs[1]
+ self.assertIsInstance(options.svcs[1], SecondaryAuthorityService)
+ self.assertEqual(secondary.primary, '1.2.3.5')
+ self.assertEqual(secondary._port, 5354)
+
+
+ def test_recursiveConfiguration(self):
+ """
+ Recursive DNS lookups, if enabled, should be a last-resort option.
+ Any other lookup method (cache, local lookup, etc.) should take
+ precedence over recursive lookups
+ """
+ options = Options()
+ options.parseOptions(['--hosts-file', 'hosts.txt', '--recursive'])
+ ca, cl = _buildResolvers(options)
+
+ # Extra cleanup, necessary on POSIX because client.Resolver doesn't know
+ # when to stop parsing resolv.conf. See #NNN for improving this.
+ for x in cl:
+ if isinstance(x, ResolverChain):
+ recurser = x.resolvers[-1]
+ if isinstance(recurser, Resolver):
+ recurser._parseCall.cancel()
+
+ self.assertIsInstance(cl[-1], ResolverChain)
diff --git a/twisted/names/topfiles/NEWS b/twisted/names/topfiles/NEWS
new file mode 100644
index 0000000..909b6b8
--- /dev/null
+++ b/twisted/names/topfiles/NEWS
@@ -0,0 +1,230 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Names 12.1.0 (2012-06-02)
+=================================
+
+Features
+--------
+ - "twistd dns" secondary server functionality and
+ twisted.names.secondary now support retrieving zone information
+ from a master running on a non-standard DNS port. (#5468)
+
+Bugfixes
+--------
+ - twisted.names.dns.DNSProtocol instances no longer throw an
+ exception when disconnecting. (#5471)
+ - twisted.names.tap.makeService (thus also "twistd dns") now makes a
+ DNS server which gives precedence to the hosts file from its
+ configuration over the remote DNS servers from its configuration.
+ (#5524)
+ - twisted.name.cache.CacheResolver now makes sure TTLs on returned
+ results are never negative. (#5579)
+ - twisted.names.cache.CacheResolver entries added via the initializer
+ are now timed out correctly. (#5638)
+
+Improved Documentation
+----------------------
+ - The examples now contain instructions on how to run them and
+ descriptions in the examples index. (#5588)
+
+Deprecations and Removals
+-------------------------
+ - The deprecated twisted.names.dns.Record_mx.exchange attribute was
+ removed. (#4549)
+
+
+Twisted Names 12.0.0 (2012-02-10)
+=================================
+
+Bugfixes
+--------
+ - twisted.names.dns.Message now sets the `auth` flag on RRHeader
+ instances it creates to reflect the authority of the message
+ itself. (#5421)
+
+
+Twisted Names 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.names.dns.Message now parses records of unknown type into
+ instances of a new `UnknownType` class. (#4603)
+
+Bugfixes
+--------
+ - twisted.names.dns.Name now detects loops in names it is decoding
+ and raises an exception. Previously it would follow the loop
+ forever, allowing a remote denial of service attack against any
+ twisted.names client or server. (#5064)
+ - twisted.names.hosts.Resolver now supports IPv6 addresses; its
+ lookupAddress method now filters them out and its lookupIPV6Address
+ method is now implemented. (#5098)
+
+
+Twisted Names 11.0.0 (2011-04-01)
+=================================
+
+No significant changes have been made for this release.
+
+
+Twisted Names 10.2.0 (2010-11-29)
+=================================
+
+Features
+--------
+ - twisted.names.server can now serve SPF resource records using
+ twisted.names.dns.Record_SPF. twisted.names.client can query for
+ them using lookupSenderPolicy. (#3928)
+
+Bugfixes
+--------
+ - twisted.names.common.extractRecords doesn't try to close the
+ transport anymore in case of recursion, as it's done by the
+ Resolver itself now. (#3998)
+
+Improved Documentation
+----------------------
+ - Tidied up the Twisted Names documentation for easier conversion.
+ (#4573)
+
+
+Twisted Names 10.1.0 (2010-06-27)
+=================================
+
+Features
+--------
+ - twisted.names.dns.Message now uses a specially constructed
+ dictionary for looking up record types. This yields a significant
+ performance improvement on PyPy. (#4283)
+
+
+Twisted Names 10.0.0 (2010-03-01)
+=================================
+
+Bugfixes
+--------
+ - twisted.names.root.Resolver no longer leaks UDP sockets while
+ resolving names. (#970)
+
+Deprecations and Removals
+-------------------------
+ - Several top-level functions in twisted.names.root are now
+ deprecated. (#970)
+
+Other
+-----
+ - #4066
+
+
+Twisted Names 9.0.0 (2009-11-24)
+================================
+
+Deprecations and Removals
+-------------------------
+ - client.ThreadedResolver is deprecated in favor of
+ twisted.internet.base.ThreadedResolver (#3710)
+
+Other
+-----
+ - #3540, #3560, #3712, #3750, #3990
+
+
+Names 8.2.0 (2008-12-16)
+========================
+
+Features
+--------
+ - The NAPTR record type is now supported (#2276)
+
+Fixes
+-----
+ - Make client.Resolver less vulnerable to the Birthday Paradox attack by
+ avoiding sending duplicate queries when it's not necessary (#3347)
+ - client.Resolver now uses a random source port for each DNS request (#3342)
+ - client.Resolver now uses a full 16 bits of randomness for message IDs,
+ instead of 10 which it previously used (#3342)
+ - All record types now have value-based equality and a string representation
+ (#2935)
+
+Other
+-----
+ - #1622, #3424
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Fixes
+-----
+
+ - Refactor DNSDatagramProtocol and DNSProtocol to use same base class (#2414)
+ - Change Resolver to query specified nameservers in specified order, instead
+ of reverse order. (#2290)
+ - Make SRVConnector work with bad results and NXDOMAIN responses.
+ (#1908, #2777)
+ - Handle write errors happening in dns queries, to have correct deferred
+ failures. (#2492)
+ - Fix the value of OP_NOTIFY and add a definition for OP_UPDATE. (#2945)
+
+Misc
+----
+ - #2685, #2936, #2581, #2847
+
+
+0.4.0 (2007-01-06)
+==================
+
+Features
+--------
+
+ - In the twisted.names client, DNS responses which represent errors
+ are now translated to informative exception objects, rather than
+ empty lists. This means that client requests which fail will now
+ errback their Deferreds (#2248)
+
+Fixes
+-----
+ - A major DoS vulnerability in the UDP DNS server was fixed (#1708)
+
+Misc
+----
+ - #1799, #1636, #2149, #2181
+
+
+0.3.0 (2006-05-21)
+==================
+
+Features
+--------
+ - Some docstring improvements
+
+Fixes
+-----
+ - Fix a problem where the response for the first query with a
+ newly-created Resolver object would be dropped.(#1447)
+ - Misc: #1581, #1583
+
+
+0.2.0
+=====
+ - Fix occassional TCP connection leak in gethostbyname()
+ - Fix TCP connection leak in recursive lookups
+ - Remove deprecated use of Deferred.setTimeout
+ - Improved test coverage for zone transfers
+
+0.1.0
+=====
+ - Fix TCP connection leak in zone transfers
+ - Handle empty or missing resolv.conf as if 127.0.0.1 was specified
+ - Don't use blocking kernel entropy sources
+ - Retry logic now properly tries all specified servers.
diff --git a/twisted/names/topfiles/README b/twisted/names/topfiles/README
new file mode 100644
index 0000000..2a57286
--- /dev/null
+++ b/twisted/names/topfiles/README
@@ -0,0 +1,3 @@
+Twisted Names 12.1.0
+
+Twisted Names depends on Twisted Core.
diff --git a/twisted/names/topfiles/setup.py b/twisted/names/topfiles/setup.py
new file mode 100644
index 0000000..9a694c9
--- /dev/null
+++ b/twisted/names/topfiles/setup.py
@@ -0,0 +1,50 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ if sys.version_info[:2] >= (2, 4):
+ extraMeta = dict(
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: No Input/Output (Daemon)",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Internet :: Name Service (DNS)",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ])
+ else:
+ extraMeta = {}
+
+ dist.setup(
+ twisted_subproject="names",
+ # metadata
+ name="Twisted Names",
+ description="A Twisted DNS implementation.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Jp Calderone",
+ url="http://twistedmatrix.com/trac/wiki/TwistedNames",
+ license="MIT",
+ long_description="""\
+Twisted Names is both a domain name server as well as a client
+resolver library. Twisted Names comes with an "out of the box"
+nameserver which can read most BIND-syntax zone files as well as a
+simple Python-based configuration format. Twisted Names can act as an
+authoritative server, perform zone transfers from a master to act as a
+secondary, act as a caching nameserver, or any combination of
+these. Twisted Names' client resolver library provides functions to
+query for all commonly used record types as well as a replacement for
+the blocking gethostbyname() function provided by the Python stdlib
+socket module.
+""",
+ **extraMeta)
diff --git a/twisted/news/__init__.py b/twisted/news/__init__.py
new file mode 100644
index 0000000..d70440c
--- /dev/null
+++ b/twisted/news/__init__.py
@@ -0,0 +1,11 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+
+Twisted News: an NNTP-based news service.
+
+"""
+
+from twisted.news._version import version
+__version__ = version.short()
diff --git a/twisted/news/_version.py b/twisted/news/_version.py
new file mode 100644
index 0000000..16e59ef
--- /dev/null
+++ b/twisted/news/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.news', 12, 1, 0)
diff --git a/twisted/news/database.py b/twisted/news/database.py
new file mode 100644
index 0000000..1ba1694
--- /dev/null
+++ b/twisted/news/database.py
@@ -0,0 +1,1051 @@
+# -*- test-case-name: twisted.news.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+News server backend implementations.
+"""
+
+import getpass, pickle, time, socket
+import os
+import StringIO
+from email.Message import Message
+from email.Generator import Generator
+from zope.interface import implements, Interface
+
+from twisted.news.nntp import NNTPError
+from twisted.mail import smtp
+from twisted.internet import defer
+from twisted.enterprise import adbapi
+from twisted.persisted import dirdbm
+from twisted.python.hashlib import md5
+
+
+
+ERR_NOGROUP, ERR_NOARTICLE = range(2, 4) # XXX - put NNTP values here (I guess?)
+
+OVERVIEW_FMT = [
+ 'Subject', 'From', 'Date', 'Message-ID', 'References',
+ 'Bytes', 'Lines', 'Xref'
+]
+
+def hexdigest(md5): #XXX: argh. 1.5.2 doesn't have this.
+ return ''.join(map(lambda x: hex(ord(x))[2:], md5.digest()))
+
+class Article:
+ def __init__(self, head, body):
+ self.body = body
+ self.headers = {}
+ header = None
+ for line in head.split('\r\n'):
+ if line[0] in ' \t':
+ i = list(self.headers[header])
+ i[1] += '\r\n' + line
+ else:
+ i = line.split(': ', 1)
+ header = i[0].lower()
+ self.headers[header] = tuple(i)
+
+ if not self.getHeader('Message-ID'):
+ s = str(time.time()) + self.body
+ id = hexdigest(md5(s)) + '@' + socket.gethostname()
+ self.putHeader('Message-ID', '<%s>' % id)
+
+ if not self.getHeader('Bytes'):
+ self.putHeader('Bytes', str(len(self.body)))
+
+ if not self.getHeader('Lines'):
+ self.putHeader('Lines', str(self.body.count('\n')))
+
+ if not self.getHeader('Date'):
+ self.putHeader('Date', time.ctime(time.time()))
+
+
+ def getHeader(self, header):
+ h = header.lower()
+ if self.headers.has_key(h):
+ return self.headers[h][1]
+ else:
+ return ''
+
+
+ def putHeader(self, header, value):
+ self.headers[header.lower()] = (header, value)
+
+
+ def textHeaders(self):
+ headers = []
+ for i in self.headers.values():
+ headers.append('%s: %s' % i)
+ return '\r\n'.join(headers) + '\r\n'
+
+ def overview(self):
+ xover = []
+ for i in OVERVIEW_FMT:
+ xover.append(self.getHeader(i))
+ return xover
+
+
+class NewsServerError(Exception):
+ pass
+
+
+class INewsStorage(Interface):
+ """
+ An interface for storing and requesting news articles
+ """
+
+ def listRequest():
+ """
+ Returns a deferred whose callback will be passed a list of 4-tuples
+ containing (name, max index, min index, flags) for each news group
+ """
+
+
+ def subscriptionRequest():
+ """
+ Returns a deferred whose callback will be passed the list of
+ recommended subscription groups for new server users
+ """
+
+
+ def postRequest(message):
+ """
+ Returns a deferred whose callback will be invoked if 'message'
+ is successfully posted to one or more specified groups and
+ whose errback will be invoked otherwise.
+ """
+
+
+ def overviewRequest():
+ """
+ Returns a deferred whose callback will be passed the a list of
+ headers describing this server's overview format.
+ """
+
+
+ def xoverRequest(group, low, high):
+ """
+ Returns a deferred whose callback will be passed a list of xover
+ headers for the given group over the given range. If low is None,
+ the range starts at the first article. If high is None, the range
+ ends at the last article.
+ """
+
+
+ def xhdrRequest(group, low, high, header):
+ """
+ Returns a deferred whose callback will be passed a list of XHDR data
+ for the given group over the given range. If low is None,
+ the range starts at the first article. If high is None, the range
+ ends at the last article.
+ """
+
+
+ def listGroupRequest(group):
+ """
+ Returns a deferred whose callback will be passed a two-tuple of
+ (group name, [article indices])
+ """
+
+
+ def groupRequest(group):
+ """
+ Returns a deferred whose callback will be passed a five-tuple of
+ (group name, article count, highest index, lowest index, group flags)
+ """
+
+
+ def articleExistsRequest(id):
+ """
+ Returns a deferred whose callback will be passed with a true value
+ if a message with the specified Message-ID exists in the database
+ and with a false value otherwise.
+ """
+
+
+ def articleRequest(group, index, id = None):
+ """
+ Returns a deferred whose callback will be passed a file-like object
+ containing the full article text (headers and body) for the article
+ of the specified index in the specified group, and whose errback
+ will be invoked if the article or group does not exist. If id is
+ not None, index is ignored and the article with the given Message-ID
+ will be returned instead, along with its index in the specified
+ group.
+ """
+
+
+ def headRequest(group, index):
+ """
+ Returns a deferred whose callback will be passed the header for
+ the article of the specified index in the specified group, and
+ whose errback will be invoked if the article or group does not
+ exist.
+ """
+
+
+ def bodyRequest(group, index):
+ """
+ Returns a deferred whose callback will be passed the body for
+ the article of the specified index in the specified group, and
+ whose errback will be invoked if the article or group does not
+ exist.
+ """
+
+class NewsStorage:
+ """
+ Backwards compatibility class -- There is no reason to inherit from this,
+ just implement INewsStorage instead.
+ """
+ def listRequest(self):
+ raise NotImplementedError()
+ def subscriptionRequest(self):
+ raise NotImplementedError()
+ def postRequest(self, message):
+ raise NotImplementedError()
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+ def xoverRequest(self, group, low, high):
+ raise NotImplementedError()
+ def xhdrRequest(self, group, low, high, header):
+ raise NotImplementedError()
+ def listGroupRequest(self, group):
+ raise NotImplementedError()
+ def groupRequest(self, group):
+ raise NotImplementedError()
+ def articleExistsRequest(self, id):
+ raise NotImplementedError()
+ def articleRequest(self, group, index, id = None):
+ raise NotImplementedError()
+ def headRequest(self, group, index):
+ raise NotImplementedError()
+ def bodyRequest(self, group, index):
+ raise NotImplementedError()
+
+
+
+class _ModerationMixin:
+ """
+ Storage implementations can inherit from this class to get the easy-to-use
+ C{notifyModerators} method which will take care of sending messages which
+ require moderation to a list of moderators.
+ """
+ sendmail = staticmethod(smtp.sendmail)
+
+ def notifyModerators(self, moderators, article):
+ """
+ Send an article to a list of group moderators to be moderated.
+
+ @param moderators: A C{list} of C{str} giving RFC 2821 addresses of
+ group moderators to notify.
+
+ @param article: The article requiring moderation.
+ @type article: L{Article}
+
+ @return: A L{Deferred} which fires with the result of sending the email.
+ """
+ # Moderated postings go through as long as they have an Approved
+ # header, regardless of what the value is
+ group = article.getHeader('Newsgroups')
+ subject = article.getHeader('Subject')
+
+ if self._sender is None:
+ # This case should really go away. This isn't a good default.
+ sender = 'twisted-news@' + socket.gethostname()
+ else:
+ sender = self._sender
+
+ msg = Message()
+ msg['Message-ID'] = smtp.messageid()
+ msg['From'] = sender
+ msg['To'] = ', '.join(moderators)
+ msg['Subject'] = 'Moderate new %s message: %s' % (group, subject)
+ msg['Content-Type'] = 'message/rfc822'
+
+ payload = Message()
+ for header, value in article.headers.values():
+ payload.add_header(header, value)
+ payload.set_payload(article.body)
+
+ msg.attach(payload)
+
+ out = StringIO.StringIO()
+ gen = Generator(out, False)
+ gen.flatten(msg)
+ msg = out.getvalue()
+
+ return self.sendmail(self._mailhost, sender, moderators, msg)
+
+
+
+class PickleStorage(_ModerationMixin):
+ """
+ A trivial NewsStorage implementation using pickles
+
+ Contains numerous flaws and is generally unsuitable for any
+ real applications. Consider yourself warned!
+ """
+
+ implements(INewsStorage)
+
+ sharedDBs = {}
+
+ def __init__(self, filename, groups=None, moderators=(),
+ mailhost=None, sender=None):
+ """
+ @param mailhost: A C{str} giving the mail exchange host which will
+ accept moderation emails from this server. Must accept emails
+ destined for any address specified as a moderator.
+
+ @param sender: A C{str} giving the address which will be used as the
+ sender of any moderation email generated by this server.
+ """
+ self.datafile = filename
+ self.load(filename, groups, moderators)
+ self._mailhost = mailhost
+ self._sender = sender
+
+
+ def getModerators(self, groups):
+ # first see if any groups are moderated. if so, nothing gets posted,
+ # but the whole messages gets forwarded to the moderator address
+ moderators = []
+ for group in groups:
+ moderators.extend(self.db['moderators'].get(group, None))
+ return filter(None, moderators)
+
+
+ def listRequest(self):
+ "Returns a list of 4-tuples: (name, max index, min index, flags)"
+ l = self.db['groups']
+ r = []
+ for i in l:
+ if len(self.db[i].keys()):
+ low = min(self.db[i].keys())
+ high = max(self.db[i].keys()) + 1
+ else:
+ low = high = 0
+ if self.db['moderators'].has_key(i):
+ flags = 'm'
+ else:
+ flags = 'y'
+ r.append((i, high, low, flags))
+ return defer.succeed(r)
+
+ def subscriptionRequest(self):
+ return defer.succeed(['alt.test'])
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+
+ a = Article(headers, article)
+ groups = a.getHeader('Newsgroups').split()
+ xref = []
+
+ # Check moderated status
+ moderators = self.getModerators(groups)
+ if moderators and not a.getHeader('Approved'):
+ return self.notifyModerators(moderators, a)
+
+ for group in groups:
+ if self.db.has_key(group):
+ if len(self.db[group].keys()):
+ index = max(self.db[group].keys()) + 1
+ else:
+ index = 1
+ xref.append((group, str(index)))
+ self.db[group][index] = a
+
+ if len(xref) == 0:
+ return defer.fail(None)
+
+ a.putHeader('Xref', '%s %s' % (
+ socket.gethostname().split()[0],
+ ''.join(map(lambda x: ':'.join(x), xref))
+ ))
+
+ self.flush()
+ return defer.succeed(None)
+
+
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+
+
+ def xoverRequest(self, group, low, high):
+ if not self.db.has_key(group):
+ return defer.succeed([])
+ r = []
+ for i in self.db[group].keys():
+ if (low is None or i >= low) and (high is None or i <= high):
+ r.append([str(i)] + self.db[group][i].overview())
+ return defer.succeed(r)
+
+
+ def xhdrRequest(self, group, low, high, header):
+ if not self.db.has_key(group):
+ return defer.succeed([])
+ r = []
+ for i in self.db[group].keys():
+ if low is None or i >= low and high is None or i <= high:
+ r.append((i, self.db[group][i].getHeader(header)))
+ return defer.succeed(r)
+
+
+ def listGroupRequest(self, group):
+ if self.db.has_key(group):
+ return defer.succeed((group, self.db[group].keys()))
+ else:
+ return defer.fail(None)
+
+ def groupRequest(self, group):
+ if self.db.has_key(group):
+ if len(self.db[group].keys()):
+ num = len(self.db[group].keys())
+ low = min(self.db[group].keys())
+ high = max(self.db[group].keys())
+ else:
+ num = low = high = 0
+ flags = 'y'
+ return defer.succeed((group, num, high, low, flags))
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def articleExistsRequest(self, id):
+ for group in self.db['groups']:
+ for a in self.db[group].values():
+ if a.getHeader('Message-ID') == id:
+ return defer.succeed(1)
+ return defer.succeed(0)
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ raise NotImplementedError
+
+ if self.db.has_key(group):
+ if self.db[group].has_key(index):
+ a = self.db[group][index]
+ return defer.succeed((
+ index,
+ a.getHeader('Message-ID'),
+ StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
+ ))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def headRequest(self, group, index):
+ if self.db.has_key(group):
+ if self.db[group].has_key(index):
+ a = self.db[group][index]
+ return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def bodyRequest(self, group, index):
+ if self.db.has_key(group):
+ if self.db[group].has_key(index):
+ a = self.db[group][index]
+ return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def flush(self):
+ f = open(self.datafile, 'w')
+ pickle.dump(self.db, f)
+ f.close()
+
+
+ def load(self, filename, groups = None, moderators = ()):
+ if PickleStorage.sharedDBs.has_key(filename):
+ self.db = PickleStorage.sharedDBs[filename]
+ else:
+ try:
+ self.db = pickle.load(open(filename))
+ PickleStorage.sharedDBs[filename] = self.db
+ except IOError:
+ self.db = PickleStorage.sharedDBs[filename] = {}
+ self.db['groups'] = groups
+ if groups is not None:
+ for i in groups:
+ self.db[i] = {}
+ self.db['moderators'] = dict(moderators)
+ self.flush()
+
+
+class Group:
+ name = None
+ flags = ''
+ minArticle = 1
+ maxArticle = 0
+ articles = None
+
+ def __init__(self, name, flags = 'y'):
+ self.name = name
+ self.flags = flags
+ self.articles = {}
+
+
+class NewsShelf(_ModerationMixin):
+ """
+ A NewStorage implementation using Twisted's dirdbm persistence module.
+ """
+
+ implements(INewsStorage)
+
+ def __init__(self, mailhost, path, sender=None):
+ """
+ @param mailhost: A C{str} giving the mail exchange host which will
+ accept moderation emails from this server. Must accept emails
+ destined for any address specified as a moderator.
+
+ @param sender: A C{str} giving the address which will be used as the
+ sender of any moderation email generated by this server.
+ """
+ self.path = path
+ self._mailhost = self.mailhost = mailhost
+ self._sender = sender
+
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+ self.dbm = dirdbm.Shelf(os.path.join(path, "newsshelf"))
+ if not len(self.dbm.keys()):
+ self.initialize()
+
+
+ def initialize(self):
+ # A dictionary of group name/Group instance items
+ self.dbm['groups'] = dirdbm.Shelf(os.path.join(self.path, 'groups'))
+
+ # A dictionary of group name/email address
+ self.dbm['moderators'] = dirdbm.Shelf(os.path.join(self.path, 'moderators'))
+
+ # A list of group names
+ self.dbm['subscriptions'] = []
+
+ # A dictionary of MessageID strings/xref lists
+ self.dbm['Message-IDs'] = dirdbm.Shelf(os.path.join(self.path, 'Message-IDs'))
+
+
+ def addGroup(self, name, flags):
+ self.dbm['groups'][name] = Group(name, flags)
+
+
+ def addSubscription(self, name):
+ self.dbm['subscriptions'] = self.dbm['subscriptions'] + [name]
+
+
+ def addModerator(self, group, email):
+ self.dbm['moderators'][group] = email
+
+
+ def listRequest(self):
+ result = []
+ for g in self.dbm['groups'].values():
+ result.append((g.name, g.maxArticle, g.minArticle, g.flags))
+ return defer.succeed(result)
+
+
+ def subscriptionRequest(self):
+ return defer.succeed(self.dbm['subscriptions'])
+
+
+ def getModerator(self, groups):
+ # first see if any groups are moderated. if so, nothing gets posted,
+ # but the whole messages gets forwarded to the moderator address
+ for group in groups:
+ try:
+ return self.dbm['moderators'][group]
+ except KeyError:
+ pass
+ return None
+
+
+ def notifyModerator(self, moderator, article):
+ """
+ Notify a single moderator about an article requiring moderation.
+
+ C{notifyModerators} should be preferred.
+ """
+ return self.notifyModerators([moderator], article)
+
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+
+ article = Article(headers, article)
+ groups = article.getHeader('Newsgroups').split()
+ xref = []
+
+ # Check for moderated status
+ moderator = self.getModerator(groups)
+ if moderator and not article.getHeader('Approved'):
+ return self.notifyModerators([moderator], article)
+
+
+ for group in groups:
+ try:
+ g = self.dbm['groups'][group]
+ except KeyError:
+ pass
+ else:
+ index = g.maxArticle + 1
+ g.maxArticle += 1
+ g.articles[index] = article
+ xref.append((group, str(index)))
+ self.dbm['groups'][group] = g
+
+ if not xref:
+ return defer.fail(NewsServerError("No groups carried: " + ' '.join(groups)))
+
+ article.putHeader('Xref', '%s %s' % (socket.gethostname().split()[0], ' '.join(map(lambda x: ':'.join(x), xref))))
+ self.dbm['Message-IDs'][article.getHeader('Message-ID')] = xref
+ return defer.succeed(None)
+
+
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+
+
+ def xoverRequest(self, group, low, high):
+ if not self.dbm['groups'].has_key(group):
+ return defer.succeed([])
+
+ if low is None:
+ low = 0
+ if high is None:
+ high = self.dbm['groups'][group].maxArticle
+ r = []
+ for i in range(low, high + 1):
+ if self.dbm['groups'][group].articles.has_key(i):
+ r.append([str(i)] + self.dbm['groups'][group].articles[i].overview())
+ return defer.succeed(r)
+
+
+ def xhdrRequest(self, group, low, high, header):
+ if group not in self.dbm['groups']:
+ return defer.succeed([])
+
+ if low is None:
+ low = 0
+ if high is None:
+ high = self.dbm['groups'][group].maxArticle
+ r = []
+ for i in range(low, high + 1):
+ if self.dbm['groups'][group].articles.has_key(i):
+ r.append((i, self.dbm['groups'][group].articles[i].getHeader(header)))
+ return defer.succeed(r)
+
+
+ def listGroupRequest(self, group):
+ if self.dbm['groups'].has_key(group):
+ return defer.succeed((group, self.dbm['groups'][group].articles.keys()))
+ return defer.fail(NewsServerError("No such group: " + group))
+
+
+ def groupRequest(self, group):
+ try:
+ g = self.dbm['groups'][group]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ flags = g.flags
+ low = g.minArticle
+ high = g.maxArticle
+ num = high - low + 1
+ return defer.succeed((group, num, high, low, flags))
+
+
+ def articleExistsRequest(self, id):
+ return defer.succeed(id in self.dbm['Message-IDs'])
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((
+ index,
+ a.getHeader('Message-ID'),
+ StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
+ ))
+
+
+ def headRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
+
+
+ def bodyRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
+
+
+class NewsStorageAugmentation:
+ """
+ A NewsStorage implementation using Twisted's asynchronous DB-API
+ """
+
+ implements(INewsStorage)
+
+ schema = """
+
+ CREATE TABLE groups (
+ group_id SERIAL,
+ name VARCHAR(80) NOT NULL,
+
+ flags INTEGER DEFAULT 0 NOT NULL
+ );
+
+ CREATE UNIQUE INDEX group_id_index ON groups (group_id);
+ CREATE UNIQUE INDEX name_id_index ON groups (name);
+
+ CREATE TABLE articles (
+ article_id SERIAL,
+ message_id TEXT,
+
+ header TEXT,
+ body TEXT
+ );
+
+ CREATE UNIQUE INDEX article_id_index ON articles (article_id);
+ CREATE UNIQUE INDEX article_message_index ON articles (message_id);
+
+ CREATE TABLE postings (
+ group_id INTEGER,
+ article_id INTEGER,
+ article_index INTEGER NOT NULL
+ );
+
+ CREATE UNIQUE INDEX posting_article_index ON postings (article_id);
+
+ CREATE TABLE subscriptions (
+ group_id INTEGER
+ );
+
+ CREATE TABLE overview (
+ header TEXT
+ );
+ """
+
+ def __init__(self, info):
+ self.info = info
+ self.dbpool = adbapi.ConnectionPool(**self.info)
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self.info['password'] = getpass.getpass('Database password for %s: ' % (self.info['user'],))
+ self.dbpool = adbapi.ConnectionPool(**self.info)
+ del self.info['password']
+
+
+ def listRequest(self):
+ # COALESCE may not be totally portable
+ # it is shorthand for
+ # CASE WHEN (first parameter) IS NOT NULL then (first parameter) ELSE (second parameter) END
+ sql = """
+ SELECT groups.name,
+ COALESCE(MAX(postings.article_index), 0),
+ COALESCE(MIN(postings.article_index), 0),
+ groups.flags
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ GROUP BY groups.name, groups.flags
+ ORDER BY groups.name
+ """
+ return self.dbpool.runQuery(sql)
+
+
+ def subscriptionRequest(self):
+ sql = """
+ SELECT groups.name FROM groups,subscriptions WHERE groups.group_id = subscriptions.group_id
+ """
+ return self.dbpool.runQuery(sql)
+
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+ article = Article(headers, article)
+ return self.dbpool.runInteraction(self._doPost, article)
+
+
+ def _doPost(self, transaction, article):
+ # Get the group ids
+ groups = article.getHeader('Newsgroups').split()
+ if not len(groups):
+ raise NNTPError('Missing Newsgroups header')
+
+ sql = """
+ SELECT name, group_id FROM groups
+ WHERE name IN (%s)
+ """ % (', '.join([("'%s'" % (adbapi.safe(group),)) for group in groups]),)
+
+ transaction.execute(sql)
+ result = transaction.fetchall()
+
+ # No relevant groups, bye bye!
+ if not len(result):
+ raise NNTPError('None of groups in Newsgroup header carried')
+
+ # Got some groups, now find the indices this article will have in each
+ sql = """
+ SELECT groups.group_id, COALESCE(MAX(postings.article_index), 0) + 1
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ WHERE groups.group_id IN (%s)
+ GROUP BY groups.group_id
+ """ % (', '.join([("%d" % (id,)) for (group, id) in result]),)
+
+ transaction.execute(sql)
+ indices = transaction.fetchall()
+
+ if not len(indices):
+ raise NNTPError('Internal server error - no indices found')
+
+ # Associate indices with group names
+ gidToName = dict([(b, a) for (a, b) in result])
+ gidToIndex = dict(indices)
+
+ nameIndex = []
+ for i in gidToName:
+ nameIndex.append((gidToName[i], gidToIndex[i]))
+
+ # Build xrefs
+ xrefs = socket.gethostname().split()[0]
+ xrefs = xrefs + ' ' + ' '.join([('%s:%d' % (group, id)) for (group, id) in nameIndex])
+ article.putHeader('Xref', xrefs)
+
+ # Hey! The article is ready to be posted! God damn f'in finally.
+ sql = """
+ INSERT INTO articles (message_id, header, body)
+ VALUES ('%s', '%s', '%s')
+ """ % (
+ adbapi.safe(article.getHeader('Message-ID')),
+ adbapi.safe(article.textHeaders()),
+ adbapi.safe(article.body)
+ )
+
+ transaction.execute(sql)
+
+ # Now update the posting to reflect the groups to which this belongs
+ for gid in gidToName:
+ sql = """
+ INSERT INTO postings (group_id, article_id, article_index)
+ VALUES (%d, (SELECT last_value FROM articles_article_id_seq), %d)
+ """ % (gid, gidToIndex[gid])
+ transaction.execute(sql)
+
+ return len(nameIndex)
+
+
+ def overviewRequest(self):
+ sql = """
+ SELECT header FROM overview
+ """
+ return self.dbpool.runQuery(sql).addCallback(lambda result: [header[0] for header in result])
+
+
+ def xoverRequest(self, group, low, high):
+ sql = """
+ SELECT postings.article_index, articles.header
+ FROM articles,postings,groups
+ WHERE postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ AND postings.article_id = articles.article_id
+ %s
+ %s
+ """ % (
+ adbapi.safe(group),
+ low is not None and "AND postings.article_index >= %d" % (low,) or "",
+ high is not None and "AND postings.article_index <= %d" % (high,) or ""
+ )
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: [
+ [id] + Article(header, None).overview() for (id, header) in results
+ ]
+ )
+
+
+ def xhdrRequest(self, group, low, high, header):
+ sql = """
+ SELECT articles.header
+ FROM groups,postings,articles
+ WHERE groups.name = '%s' AND postings.group_id = groups.group_id
+ AND postings.article_index >= %d
+ AND postings.article_index <= %d
+ """ % (adbapi.safe(group), low, high)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: [
+ (i, Article(h, None).getHeader(h)) for (i, h) in results
+ ]
+ )
+
+
+ def listGroupRequest(self, group):
+ sql = """
+ SELECT postings.article_index FROM postings,groups
+ WHERE postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (adbapi.safe(group),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results, group = group: (group, [res[0] for res in results])
+ )
+
+
+ def groupRequest(self, group):
+ sql = """
+ SELECT groups.name,
+ COUNT(postings.article_index),
+ COALESCE(MAX(postings.article_index), 0),
+ COALESCE(MIN(postings.article_index), 0),
+ groups.flags
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ WHERE groups.name = '%s'
+ GROUP BY groups.name, groups.flags
+ """ % (adbapi.safe(group),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: tuple(results[0])
+ )
+
+
+ def articleExistsRequest(self, id):
+ sql = """
+ SELECT COUNT(message_id) FROM articles
+ WHERE message_id = '%s'
+ """ % (adbapi.safe(id),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: bool(result[0][0])
+ )
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header, articles.body
+ FROM groups,postings LEFT OUTER JOIN articles
+ ON articles.message_id = '%s'
+ WHERE groups.name = '%s'
+ AND groups.group_id = postings.group_id
+ """ % (adbapi.safe(id), adbapi.safe(group))
+ else:
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header, articles.body
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: (
+ result[0][0],
+ result[0][1],
+ StringIO.StringIO(result[0][2] + '\r\n' + result[0][3])
+ )
+ )
+
+
+ def headRequest(self, group, index):
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(lambda result: result[0])
+
+
+ def bodyRequest(self, group, index):
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.body
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: result[0]
+ ).addCallback(
+ lambda (index, id, body): (index, id, StringIO.StringIO(body))
+ )
+
+####
+#### XXX - make these static methods some day
+####
+def makeGroupSQL(groups):
+ res = ''
+ for g in groups:
+ res = res + """\n INSERT INTO groups (name) VALUES ('%s');\n""" % (adbapi.safe(g),)
+ return res
+
+
+def makeOverviewSQL():
+ res = ''
+ for o in OVERVIEW_FMT:
+ res = res + """\n INSERT INTO overview (header) VALUES ('%s');\n""" % (adbapi.safe(o),)
+ return res
diff --git a/twisted/news/news.py b/twisted/news/news.py
new file mode 100644
index 0000000..8165171
--- /dev/null
+++ b/twisted/news/news.py
@@ -0,0 +1,90 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Maintainer: Jp Calderone
+"""
+
+from twisted.news import nntp
+from twisted.internet import protocol, reactor
+
+import time
+
+class NNTPFactory(protocol.ServerFactory):
+ """A factory for NNTP server protocols."""
+
+ protocol = nntp.NNTPServer
+
+ def __init__(self, backend):
+ self.backend = backend
+
+ def buildProtocol(self, connection):
+ p = self.protocol()
+ p.factory = self
+ return p
+
+
+class UsenetClientFactory(protocol.ClientFactory):
+ def __init__(self, groups, storage):
+ self.lastChecks = {}
+ self.groups = groups
+ self.storage = storage
+
+
+ def clientConnectionLost(self, connector, reason):
+ pass
+
+
+ def clientConnectionFailed(self, connector, reason):
+ print 'Connection failed: ', reason
+
+
+ def updateChecks(self, addr):
+ self.lastChecks[addr] = time.mktime(time.gmtime())
+
+
+ def buildProtocol(self, addr):
+ last = self.lastChecks.setdefault(addr, time.mktime(time.gmtime()) - (60 * 60 * 24 * 7))
+ p = nntp.UsenetClientProtocol(self.groups, last, self.storage)
+ p.factory = self
+ return p
+
+
+# XXX - Maybe this inheritence doesn't make so much sense?
+class UsenetServerFactory(NNTPFactory):
+ """A factory for NNTP Usenet server protocols."""
+
+ protocol = nntp.NNTPServer
+
+ def __init__(self, backend, remoteHosts = None, updatePeriod = 60):
+ NNTPFactory.__init__(self, backend)
+ self.updatePeriod = updatePeriod
+ self.remoteHosts = remoteHosts or []
+ self.clientFactory = UsenetClientFactory(self.remoteHosts, self.backend)
+
+
+ def startFactory(self):
+ self._updateCall = reactor.callLater(0, self.syncWithRemotes)
+
+
+ def stopFactory(self):
+ if self._updateCall:
+ self._updateCall.cancel()
+ self._updateCall = None
+
+
+ def buildProtocol(self, connection):
+ p = self.protocol()
+ p.factory = self
+ return p
+
+
+ def syncWithRemotes(self):
+ for remote in self.remoteHosts:
+ reactor.connectTCP(remote, 119, self.clientFactory)
+ self._updateCall = reactor.callLater(self.updatePeriod, self.syncWithRemotes)
+
+
+# backwards compatability
+Factory = UsenetServerFactory
diff --git a/twisted/news/nntp.py b/twisted/news/nntp.py
new file mode 100644
index 0000000..864bd53
--- /dev/null
+++ b/twisted/news/nntp.py
@@ -0,0 +1,1036 @@
+# -*- test-case-name: twisted.news.test.test_nntp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+NNTP protocol support.
+
+The following protocol commands are currently understood::
+
+ LIST LISTGROUP XOVER XHDR
+ POST GROUP ARTICLE STAT HEAD
+ BODY NEXT MODE STREAM MODE READER SLAVE
+ LAST QUIT HELP IHAVE XPATH
+ XINDEX XROVER TAKETHIS CHECK
+
+The following protocol commands require implementation::
+
+ NEWNEWS
+ XGTITLE XPAT
+ XTHREAD AUTHINFO NEWGROUPS
+
+
+Other desired features:
+
+ - A real backend
+ - More robust client input handling
+ - A control protocol
+"""
+
+import time
+
+from twisted.protocols import basic
+from twisted.python import log
+
+def parseRange(text):
+ articles = text.split('-')
+ if len(articles) == 1:
+ try:
+ a = int(articles[0])
+ return a, a
+ except ValueError:
+ return None, None
+ elif len(articles) == 2:
+ try:
+ if len(articles[0]):
+ l = int(articles[0])
+ else:
+ l = None
+ if len(articles[1]):
+ h = int(articles[1])
+ else:
+ h = None
+ except ValueError:
+ return None, None
+ return l, h
+
+
+def extractCode(line):
+ line = line.split(' ', 1)
+ if len(line) != 2:
+ return None
+ try:
+ return int(line[0]), line[1]
+ except ValueError:
+ return None
+
+
+class NNTPError(Exception):
+ def __init__(self, string):
+ self.string = string
+
+ def __str__(self):
+ return 'NNTPError: %s' % self.string
+
+
+class NNTPClient(basic.LineReceiver):
+ MAX_COMMAND_LENGTH = 510
+
+ def __init__(self):
+ self.currentGroup = None
+
+ self._state = []
+ self._error = []
+ self._inputBuffers = []
+ self._responseCodes = []
+ self._responseHandlers = []
+
+ self._postText = []
+
+ self._newState(self._statePassive, None, self._headerInitial)
+
+
+ def gotAllGroups(self, groups):
+ "Override for notification when fetchGroups() action is completed"
+
+
+ def getAllGroupsFailed(self, error):
+ "Override for notification when fetchGroups() action fails"
+
+
+ def gotOverview(self, overview):
+ "Override for notification when fetchOverview() action is completed"
+
+
+ def getOverviewFailed(self, error):
+ "Override for notification when fetchOverview() action fails"
+
+
+ def gotSubscriptions(self, subscriptions):
+ "Override for notification when fetchSubscriptions() action is completed"
+
+
+ def getSubscriptionsFailed(self, error):
+ "Override for notification when fetchSubscriptions() action fails"
+
+
+ def gotGroup(self, group):
+ "Override for notification when fetchGroup() action is completed"
+
+
+ def getGroupFailed(self, error):
+ "Override for notification when fetchGroup() action fails"
+
+
+ def gotArticle(self, article):
+ "Override for notification when fetchArticle() action is completed"
+
+
+ def getArticleFailed(self, error):
+ "Override for notification when fetchArticle() action fails"
+
+
+ def gotHead(self, head):
+ "Override for notification when fetchHead() action is completed"
+
+
+ def getHeadFailed(self, error):
+ "Override for notification when fetchHead() action fails"
+
+
+ def gotBody(self, info):
+ "Override for notification when fetchBody() action is completed"
+
+
+ def getBodyFailed(self, body):
+ "Override for notification when fetchBody() action fails"
+
+
+ def postedOk(self):
+ "Override for notification when postArticle() action is successful"
+
+
+ def postFailed(self, error):
+ "Override for notification when postArticle() action fails"
+
+
+ def gotXHeader(self, headers):
+ "Override for notification when getXHeader() action is successful"
+
+
+ def getXHeaderFailed(self, error):
+ "Override for notification when getXHeader() action fails"
+
+
+ def gotNewNews(self, news):
+ "Override for notification when getNewNews() action is successful"
+
+
+ def getNewNewsFailed(self, error):
+ "Override for notification when getNewNews() action fails"
+
+
+ def gotNewGroups(self, groups):
+ "Override for notification when getNewGroups() action is successful"
+
+
+ def getNewGroupsFailed(self, error):
+ "Override for notification when getNewGroups() action fails"
+
+
+ def setStreamSuccess(self):
+ "Override for notification when setStream() action is successful"
+
+
+ def setStreamFailed(self, error):
+ "Override for notification when setStream() action fails"
+
+
+ def fetchGroups(self):
+ """
+ Request a list of all news groups from the server. gotAllGroups()
+ is called on success, getGroupsFailed() on failure
+ """
+ self.sendLine('LIST')
+ self._newState(self._stateList, self.getAllGroupsFailed)
+
+
+ def fetchOverview(self):
+ """
+ Request the overview format from the server. gotOverview() is called
+ on success, getOverviewFailed() on failure
+ """
+ self.sendLine('LIST OVERVIEW.FMT')
+ self._newState(self._stateOverview, self.getOverviewFailed)
+
+
+ def fetchSubscriptions(self):
+ """
+ Request a list of the groups it is recommended a new user subscribe to.
+ gotSubscriptions() is called on success, getSubscriptionsFailed() on
+ failure
+ """
+ self.sendLine('LIST SUBSCRIPTIONS')
+ self._newState(self._stateSubscriptions, self.getSubscriptionsFailed)
+
+
+ def fetchGroup(self, group):
+ """
+ Get group information for the specified group from the server. gotGroup()
+ is called on success, getGroupFailed() on failure.
+ """
+ self.sendLine('GROUP %s' % (group,))
+ self._newState(None, self.getGroupFailed, self._headerGroup)
+
+
+ def fetchHead(self, index = ''):
+ """
+ Get the header for the specified article (or the currently selected
+ article if index is '') from the server. gotHead() is called on
+ success, getHeadFailed() on failure
+ """
+ self.sendLine('HEAD %s' % (index,))
+ self._newState(self._stateHead, self.getHeadFailed)
+
+
+ def fetchBody(self, index = ''):
+ """
+ Get the body for the specified article (or the currently selected
+ article if index is '') from the server. gotBody() is called on
+ success, getBodyFailed() on failure
+ """
+ self.sendLine('BODY %s' % (index,))
+ self._newState(self._stateBody, self.getBodyFailed)
+
+
+ def fetchArticle(self, index = ''):
+ """
+ Get the complete article with the specified index (or the currently
+ selected article if index is '') or Message-ID from the server.
+ gotArticle() is called on success, getArticleFailed() on failure.
+ """
+ self.sendLine('ARTICLE %s' % (index,))
+ self._newState(self._stateArticle, self.getArticleFailed)
+
+
+ def postArticle(self, text):
+ """
+ Attempt to post an article with the specified text to the server. 'text'
+ must consist of both head and body data, as specified by RFC 850. If the
+ article is posted successfully, postedOk() is called, otherwise postFailed()
+ is called.
+ """
+ self.sendLine('POST')
+ self._newState(None, self.postFailed, self._headerPost)
+ self._postText.append(text)
+
+
+ def fetchNewNews(self, groups, date, distributions = ''):
+ """
+ Get the Message-IDs for all new news posted to any of the given
+ groups since the specified date - in seconds since the epoch, GMT -
+ optionally restricted to the given distributions. gotNewNews() is
+ called on success, getNewNewsFailed() on failure.
+
+ One invocation of this function may result in multiple invocations
+ of gotNewNews()/getNewNewsFailed().
+ """
+ date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
+ line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions)
+ groupPart = ''
+ while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 < NNTPClient.MAX_COMMAND_LENGTH:
+ group = groups.pop()
+ groupPart = groupPart + ',' + group
+
+ self.sendLine(line % (groupPart,))
+ self._newState(self._stateNewNews, self.getNewNewsFailed)
+
+ if len(groups):
+ self.fetchNewNews(groups, date, distributions)
+
+
+ def fetchNewGroups(self, date, distributions):
+ """
+ Get the names of all new groups created/added to the server since
+ the specified date - in seconds since the ecpoh, GMT - optionally
+ restricted to the given distributions. gotNewGroups() is called
+ on success, getNewGroupsFailed() on failure.
+ """
+ date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
+ self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions))
+ self._newState(self._stateNewGroups, self.getNewGroupsFailed)
+
+
+ def fetchXHeader(self, header, low = None, high = None, id = None):
+ """
+ Request a specific header from the server for an article or range
+ of articles. If 'id' is not None, a header for only the article
+ with that Message-ID will be requested. If both low and high are
+ None, a header for the currently selected article will be selected;
+ If both low and high are zero-length strings, headers for all articles
+ in the currently selected group will be requested; Otherwise, high
+ and low will be used as bounds - if one is None the first or last
+ article index will be substituted, as appropriate.
+ """
+ if id is not None:
+ r = header + ' <%s>' % (id,)
+ elif low is high is None:
+ r = header
+ elif high is None:
+ r = header + ' %d-' % (low,)
+ elif low is None:
+ r = header + ' -%d' % (high,)
+ else:
+ r = header + ' %d-%d' % (low, high)
+ self.sendLine('XHDR ' + r)
+ self._newState(self._stateXHDR, self.getXHeaderFailed)
+
+
+ def setStream(self):
+ """
+ Set the mode to STREAM, suspending the normal "lock-step" mode of
+ communications. setStreamSuccess() is called on success,
+ setStreamFailed() on failure.
+ """
+ self.sendLine('MODE STREAM')
+ self._newState(None, self.setStreamFailed, self._headerMode)
+
+
+ def quit(self):
+ self.sendLine('QUIT')
+ self.transport.loseConnection()
+
+
+ def _newState(self, method, error, responseHandler = None):
+ self._inputBuffers.append([])
+ self._responseCodes.append(None)
+ self._state.append(method)
+ self._error.append(error)
+ self._responseHandlers.append(responseHandler)
+
+
+ def _endState(self):
+ buf = self._inputBuffers[0]
+ del self._responseCodes[0]
+ del self._inputBuffers[0]
+ del self._state[0]
+ del self._error[0]
+ del self._responseHandlers[0]
+ return buf
+
+
+ def _newLine(self, line, check = 1):
+ if check and line and line[0] == '.':
+ line = line[1:]
+ self._inputBuffers[0].append(line)
+
+
+ def _setResponseCode(self, code):
+ self._responseCodes[0] = code
+
+
+ def _getResponseCode(self):
+ return self._responseCodes[0]
+
+
+ def lineReceived(self, line):
+ if not len(self._state):
+ self._statePassive(line)
+ elif self._getResponseCode() is None:
+ code = extractCode(line)
+ if code is None or not (200 <= code[0] < 400): # An error!
+ self._error[0](line)
+ self._endState()
+ else:
+ self._setResponseCode(code)
+ if self._responseHandlers[0]:
+ self._responseHandlers[0](code)
+ else:
+ self._state[0](line)
+
+
+ def _statePassive(self, line):
+ log.msg('Server said: %s' % line)
+
+
+ def _passiveError(self, error):
+ log.err('Passive Error: %s' % (error,))
+
+
+ def _headerInitial(self, (code, message)):
+ if code == 200:
+ self.canPost = 1
+ else:
+ self.canPost = 0
+ self._endState()
+
+
+ def _stateList(self, line):
+ if line != '.':
+ data = filter(None, line.strip().split())
+ self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0)
+ else:
+ self.gotAllGroups(self._endState())
+
+
+ def _stateOverview(self, line):
+ if line != '.':
+ self._newLine(filter(None, line.strip().split()), 0)
+ else:
+ self.gotOverview(self._endState())
+
+
+ def _stateSubscriptions(self, line):
+ if line != '.':
+ self._newLine(line.strip(), 0)
+ else:
+ self.gotSubscriptions(self._endState())
+
+
+ def _headerGroup(self, (code, line)):
+ self.gotGroup(tuple(line.split()))
+ self._endState()
+
+
+ def _stateArticle(self, line):
+ if line != '.':
+ if line.startswith('.'):
+ line = line[1:]
+ self._newLine(line, 0)
+ else:
+ self.gotArticle('\n'.join(self._endState())+'\n')
+
+
+ def _stateHead(self, line):
+ if line != '.':
+ self._newLine(line, 0)
+ else:
+ self.gotHead('\n'.join(self._endState()))
+
+
+ def _stateBody(self, line):
+ if line != '.':
+ if line.startswith('.'):
+ line = line[1:]
+ self._newLine(line, 0)
+ else:
+ self.gotBody('\n'.join(self._endState())+'\n')
+
+
+ def _headerPost(self, (code, message)):
+ if code == 340:
+ self.transport.write(self._postText[0].replace('\n', '\r\n').replace('\r\n.', '\r\n..'))
+ if self._postText[0][-1:] != '\n':
+ self.sendLine('')
+ self.sendLine('.')
+ del self._postText[0]
+ self._newState(None, self.postFailed, self._headerPosted)
+ else:
+ self.postFailed('%d %s' % (code, message))
+ self._endState()
+
+
+ def _headerPosted(self, (code, message)):
+ if code == 240:
+ self.postedOk()
+ else:
+ self.postFailed('%d %s' % (code, message))
+ self._endState()
+
+
+ def _stateXHDR(self, line):
+ if line != '.':
+ self._newLine(line.split(), 0)
+ else:
+ self._gotXHeader(self._endState())
+
+
+ def _stateNewNews(self, line):
+ if line != '.':
+ self._newLine(line, 0)
+ else:
+ self.gotNewNews(self._endState())
+
+
+ def _stateNewGroups(self, line):
+ if line != '.':
+ self._newLine(line, 0)
+ else:
+ self.gotNewGroups(self._endState())
+
+
+ def _headerMode(self, (code, message)):
+ if code == 203:
+ self.setStreamSuccess()
+ else:
+ self.setStreamFailed((code, message))
+ self._endState()
+
+
+class NNTPServer(basic.LineReceiver):
+ COMMANDS = [
+ 'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER',
+ 'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE',
+ 'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK'
+ ]
+
+ def __init__(self):
+ self.servingSlave = 0
+
+
+ def connectionMade(self):
+ self.inputHandler = None
+ self.currentGroup = None
+ self.currentIndex = None
+ self.sendLine('200 server ready - posting allowed')
+
+ def lineReceived(self, line):
+ if self.inputHandler is not None:
+ self.inputHandler(line)
+ else:
+ parts = line.strip().split()
+ if len(parts):
+ cmd, parts = parts[0].upper(), parts[1:]
+ if cmd in NNTPServer.COMMANDS:
+ func = getattr(self, 'do_%s' % cmd)
+ try:
+ func(*parts)
+ except TypeError:
+ self.sendLine('501 command syntax error')
+ log.msg("501 command syntax error")
+ log.msg("command was", line)
+ log.deferr()
+ except:
+ self.sendLine('503 program fault - command not performed')
+ log.msg("503 program fault")
+ log.msg("command was", line)
+ log.deferr()
+ else:
+ self.sendLine('500 command not recognized')
+
+
+ def do_LIST(self, subcmd = '', *dummy):
+ subcmd = subcmd.strip().lower()
+ if subcmd == 'newsgroups':
+ # XXX - this could use a real implementation, eh?
+ self.sendLine('215 Descriptions in form "group description"')
+ self.sendLine('.')
+ elif subcmd == 'overview.fmt':
+ defer = self.factory.backend.overviewRequest()
+ defer.addCallbacks(self._gotOverview, self._errOverview)
+ log.msg('overview')
+ elif subcmd == 'subscriptions':
+ defer = self.factory.backend.subscriptionRequest()
+ defer.addCallbacks(self._gotSubscription, self._errSubscription)
+ log.msg('subscriptions')
+ elif subcmd == '':
+ defer = self.factory.backend.listRequest()
+ defer.addCallbacks(self._gotList, self._errList)
+ else:
+ self.sendLine('500 command not recognized')
+
+
+ def _gotList(self, list):
+ self.sendLine('215 newsgroups in form "group high low flags"')
+ for i in list:
+ self.sendLine('%s %d %d %s' % tuple(i))
+ self.sendLine('.')
+
+
+ def _errList(self, failure):
+ print 'LIST failed: ', failure
+ self.sendLine('503 program fault - command not performed')
+
+
+ def _gotSubscription(self, parts):
+ self.sendLine('215 information follows')
+ for i in parts:
+ self.sendLine(i)
+ self.sendLine('.')
+
+
+ def _errSubscription(self, failure):
+ print 'SUBSCRIPTIONS failed: ', failure
+ self.sendLine('503 program fault - comand not performed')
+
+
+ def _gotOverview(self, parts):
+ self.sendLine('215 Order of fields in overview database.')
+ for i in parts:
+ self.sendLine(i + ':')
+ self.sendLine('.')
+
+
+ def _errOverview(self, failure):
+ print 'LIST OVERVIEW.FMT failed: ', failure
+ self.sendLine('503 program fault - command not performed')
+
+
+ def do_LISTGROUP(self, group = None):
+ group = group or self.currentGroup
+ if group is None:
+ self.sendLine('412 Not currently in newsgroup')
+ else:
+ defer = self.factory.backend.listGroupRequest(group)
+ defer.addCallbacks(self._gotListGroup, self._errListGroup)
+
+
+ def _gotListGroup(self, (group, articles)):
+ self.currentGroup = group
+ if len(articles):
+ self.currentIndex = int(articles[0])
+ else:
+ self.currentIndex = None
+
+ self.sendLine('211 list of article numbers follow')
+ for i in articles:
+ self.sendLine(str(i))
+ self.sendLine('.')
+
+
+ def _errListGroup(self, failure):
+ print 'LISTGROUP failed: ', failure
+ self.sendLine('502 no permission')
+
+
+ def do_XOVER(self, range):
+ if self.currentGroup is None:
+ self.sendLine('412 No news group currently selected')
+ else:
+ l, h = parseRange(range)
+ defer = self.factory.backend.xoverRequest(self.currentGroup, l, h)
+ defer.addCallbacks(self._gotXOver, self._errXOver)
+
+
+ def _gotXOver(self, parts):
+ self.sendLine('224 Overview information follows')
+ for i in parts:
+ self.sendLine('\t'.join(map(str, i)))
+ self.sendLine('.')
+
+
+ def _errXOver(self, failure):
+ print 'XOVER failed: ', failure
+ self.sendLine('420 No article(s) selected')
+
+
+ def xhdrWork(self, header, range):
+ if self.currentGroup is None:
+ self.sendLine('412 No news group currently selected')
+ else:
+ if range is None:
+ if self.currentIndex is None:
+ self.sendLine('420 No current article selected')
+ return
+ else:
+ l = h = self.currentIndex
+ else:
+ # FIXME: articles may be a message-id
+ l, h = parseRange(range)
+
+ if l is h is None:
+ self.sendLine('430 no such article')
+ else:
+ return self.factory.backend.xhdrRequest(self.currentGroup, l, h, header)
+
+
+ def do_XHDR(self, header, range = None):
+ d = self.xhdrWork(header, range)
+ if d:
+ d.addCallbacks(self._gotXHDR, self._errXHDR)
+
+
+ def _gotXHDR(self, parts):
+ self.sendLine('221 Header follows')
+ for i in parts:
+ self.sendLine('%d %s' % i)
+ self.sendLine('.')
+
+ def _errXHDR(self, failure):
+ print 'XHDR failed: ', failure
+ self.sendLine('502 no permission')
+
+
+ def do_POST(self):
+ self.inputHandler = self._doingPost
+ self.message = ''
+ self.sendLine('340 send article to be posted. End with <CR-LF>.<CR-LF>')
+
+
+ def _doingPost(self, line):
+ if line == '.':
+ self.inputHandler = None
+ group, article = self.currentGroup, self.message
+ self.message = ''
+
+ defer = self.factory.backend.postRequest(article)
+ defer.addCallbacks(self._gotPost, self._errPost)
+ else:
+ self.message = self.message + line + '\r\n'
+
+
+ def _gotPost(self, parts):
+ self.sendLine('240 article posted ok')
+
+
+ def _errPost(self, failure):
+ print 'POST failed: ', failure
+ self.sendLine('441 posting failed')
+
+
+ def do_CHECK(self, id):
+ d = self.factory.backend.articleExistsRequest(id)
+ d.addCallbacks(self._gotCheck, self._errCheck)
+
+
+ def _gotCheck(self, result):
+ if result:
+ self.sendLine("438 already have it, please don't send it to me")
+ else:
+ self.sendLine('238 no such article found, please send it to me')
+
+
+ def _errCheck(self, failure):
+ print 'CHECK failed: ', failure
+ self.sendLine('431 try sending it again later')
+
+
+ def do_TAKETHIS(self, id):
+ self.inputHandler = self._doingTakeThis
+ self.message = ''
+
+
+ def _doingTakeThis(self, line):
+ if line == '.':
+ self.inputHandler = None
+ article = self.message
+ self.message = ''
+ d = self.factory.backend.postRequest(article)
+ d.addCallbacks(self._didTakeThis, self._errTakeThis)
+ else:
+ self.message = self.message + line + '\r\n'
+
+
+ def _didTakeThis(self, result):
+ self.sendLine('239 article transferred ok')
+
+
+ def _errTakeThis(self, failure):
+ print 'TAKETHIS failed: ', failure
+ self.sendLine('439 article transfer failed')
+
+
+ def do_GROUP(self, group):
+ defer = self.factory.backend.groupRequest(group)
+ defer.addCallbacks(self._gotGroup, self._errGroup)
+
+
+ def _gotGroup(self, (name, num, high, low, flags)):
+ self.currentGroup = name
+ self.currentIndex = low
+ self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name))
+
+
+ def _errGroup(self, failure):
+ print 'GROUP failed: ', failure
+ self.sendLine('411 no such group')
+
+
+ def articleWork(self, article, cmd, func):
+ if self.currentGroup is None:
+ self.sendLine('412 no newsgroup has been selected')
+ else:
+ if not article:
+ if self.currentIndex is None:
+ self.sendLine('420 no current article has been selected')
+ else:
+ article = self.currentIndex
+ else:
+ if article[0] == '<':
+ return func(self.currentGroup, index = None, id = article)
+ else:
+ try:
+ article = int(article)
+ return func(self.currentGroup, article)
+ except ValueError:
+ self.sendLine('501 command syntax error')
+
+
+ def do_ARTICLE(self, article = None):
+ defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articleRequest)
+ if defer:
+ defer.addCallbacks(self._gotArticle, self._errArticle)
+
+
+ def _gotArticle(self, (index, id, article)):
+ self.currentIndex = index
+ self.sendLine('220 %d %s article' % (index, id))
+ s = basic.FileSender()
+ d = s.beginFileTransfer(article, self.transport)
+ d.addCallback(self.finishedFileTransfer)
+
+ ##
+ ## Helper for FileSender
+ ##
+ def finishedFileTransfer(self, lastsent):
+ if lastsent != '\n':
+ line = '\r\n.'
+ else:
+ line = '.'
+ self.sendLine(line)
+ ##
+
+ def _errArticle(self, failure):
+ print 'ARTICLE failed: ', failure
+ self.sendLine('423 bad article number')
+
+
+ def do_STAT(self, article = None):
+ defer = self.articleWork(article, 'STAT', self.factory.backend.articleRequest)
+ if defer:
+ defer.addCallbacks(self._gotStat, self._errStat)
+
+
+ def _gotStat(self, (index, id, article)):
+ self.currentIndex = index
+ self.sendLine('223 %d %s article retreived - request text separately' % (index, id))
+
+
+ def _errStat(self, failure):
+ print 'STAT failed: ', failure
+ self.sendLine('423 bad article number')
+
+
+ def do_HEAD(self, article = None):
+ defer = self.articleWork(article, 'HEAD', self.factory.backend.headRequest)
+ if defer:
+ defer.addCallbacks(self._gotHead, self._errHead)
+
+
+ def _gotHead(self, (index, id, head)):
+ self.currentIndex = index
+ self.sendLine('221 %d %s article retrieved' % (index, id))
+ self.transport.write(head + '\r\n')
+ self.sendLine('.')
+
+
+ def _errHead(self, failure):
+ print 'HEAD failed: ', failure
+ self.sendLine('423 no such article number in this group')
+
+
+ def do_BODY(self, article):
+ defer = self.articleWork(article, 'BODY', self.factory.backend.bodyRequest)
+ if defer:
+ defer.addCallbacks(self._gotBody, self._errBody)
+
+
+ def _gotBody(self, (index, id, body)):
+ self.currentIndex = index
+ self.sendLine('221 %d %s article retrieved' % (index, id))
+ self.lastsent = ''
+ s = basic.FileSender()
+ d = s.beginFileTransfer(body, self.transport)
+ d.addCallback(self.finishedFileTransfer)
+
+ def _errBody(self, failure):
+ print 'BODY failed: ', failure
+ self.sendLine('423 no such article number in this group')
+
+
+ # NEXT and LAST are just STATs that increment currentIndex first.
+ # Accordingly, use the STAT callbacks.
+ def do_NEXT(self):
+ i = self.currentIndex + 1
+ defer = self.factory.backend.articleRequest(self.currentGroup, i)
+ defer.addCallbacks(self._gotStat, self._errStat)
+
+
+ def do_LAST(self):
+ i = self.currentIndex - 1
+ defer = self.factory.backend.articleRequest(self.currentGroup, i)
+ defer.addCallbacks(self._gotStat, self._errStat)
+
+
+ def do_MODE(self, cmd):
+ cmd = cmd.strip().upper()
+ if cmd == 'READER':
+ self.servingSlave = 0
+ self.sendLine('200 Hello, you can post')
+ elif cmd == 'STREAM':
+ self.sendLine('500 Command not understood')
+ else:
+ # This is not a mistake
+ self.sendLine('500 Command not understood')
+
+
+ def do_QUIT(self):
+ self.sendLine('205 goodbye')
+ self.transport.loseConnection()
+
+
+ def do_HELP(self):
+ self.sendLine('100 help text follows')
+ self.sendLine('Read the RFC.')
+ self.sendLine('.')
+
+
+ def do_SLAVE(self):
+ self.sendLine('202 slave status noted')
+ self.servingeSlave = 1
+
+
+ def do_XPATH(self, article):
+ # XPATH is a silly thing to have. No client has the right to ask
+ # for this piece of information from me, and so that is what I'll
+ # tell them.
+ self.sendLine('502 access restriction or permission denied')
+
+
+ def do_XINDEX(self, article):
+ # XINDEX is another silly command. The RFC suggests it be relegated
+ # to the history books, and who am I to disagree?
+ self.sendLine('502 access restriction or permission denied')
+
+
+ def do_XROVER(self, range=None):
+ """
+ Handle a request for references of all messages in the currently
+ selected group.
+
+ This generates the same response a I{XHDR References} request would
+ generate.
+ """
+ self.do_XHDR('References', range)
+
+
+ def do_IHAVE(self, id):
+ self.factory.backend.articleExistsRequest(id).addCallback(self._foundArticle)
+
+
+ def _foundArticle(self, result):
+ if result:
+ self.sendLine('437 article rejected - do not try again')
+ else:
+ self.sendLine('335 send article to be transferred. End with <CR-LF>.<CR-LF>')
+ self.inputHandler = self._handleIHAVE
+ self.message = ''
+
+
+ def _handleIHAVE(self, line):
+ if line == '.':
+ self.inputHandler = None
+ self.factory.backend.postRequest(
+ self.message
+ ).addCallbacks(self._gotIHAVE, self._errIHAVE)
+
+ self.message = ''
+ else:
+ self.message = self.message + line + '\r\n'
+
+
+ def _gotIHAVE(self, result):
+ self.sendLine('235 article transferred ok')
+
+
+ def _errIHAVE(self, failure):
+ print 'IHAVE failed: ', failure
+ self.sendLine('436 transfer failed - try again later')
+
+
+class UsenetClientProtocol(NNTPClient):
+ """
+ A client that connects to an NNTP server and asks for articles new
+ since a certain time.
+ """
+
+ def __init__(self, groups, date, storage):
+ """
+ Fetch all new articles from the given groups since the
+ given date and dump them into the given storage. groups
+ is a list of group names. date is an integer or floating
+ point representing seconds since the epoch (GMT). storage is
+ any object that implements the NewsStorage interface.
+ """
+ NNTPClient.__init__(self)
+ self.groups, self.date, self.storage = groups, date, storage
+
+
+ def connectionMade(self):
+ NNTPClient.connectionMade(self)
+ log.msg("Initiating update with remote host: " + str(self.transport.getPeer()))
+ self.setStream()
+ self.fetchNewNews(self.groups, self.date, '')
+
+
+ def articleExists(self, exists, article):
+ if exists:
+ self.fetchArticle(article)
+ else:
+ self.count = self.count - 1
+ self.disregard = self.disregard + 1
+
+
+ def gotNewNews(self, news):
+ self.disregard = 0
+ self.count = len(news)
+ log.msg("Transfering " + str(self.count) + " articles from remote host: " + str(self.transport.getPeer()))
+ for i in news:
+ self.storage.articleExistsRequest(i).addCallback(self.articleExists, i)
+
+
+ def getNewNewsFailed(self, reason):
+ log.msg("Updated failed (" + reason + ") with remote host: " + str(self.transport.getPeer()))
+ self.quit()
+
+
+ def gotArticle(self, article):
+ self.storage.postRequest(article)
+ self.count = self.count - 1
+ if not self.count:
+ log.msg("Completed update with remote host: " + str(self.transport.getPeer()))
+ if self.disregard:
+ log.msg("Disregarded %d articles." % (self.disregard,))
+ self.factory.updateChecks(self.transport.getPeer())
+ self.quit()
diff --git a/twisted/news/tap.py b/twisted/news/tap.py
new file mode 100644
index 0000000..a4cf542
--- /dev/null
+++ b/twisted/news/tap.py
@@ -0,0 +1,138 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.news import news, database
+from twisted.application import strports
+from twisted.python import usage, log
+
+class DBOptions(usage.Options):
+ optParameters = [
+ ['module', None, 'pyPgSQL.PgSQL', "DB-API 2.0 module to use"],
+ ['dbhost', None, 'localhost', "Host where database manager is listening"],
+ ['dbuser', None, 'news', "Username with which to connect to database"],
+ ['database', None, 'news', "Database name to use"],
+ ['schema', None, 'schema.sql', "File to which to write SQL schema initialisation"],
+
+ # XXX - Hrm.
+ ["groups", "g", "groups.list", "File containing group list"],
+ ["servers", "s", "servers.list", "File containing server list"]
+ ]
+
+ def postOptions(self):
+ # XXX - Hmmm.
+ self['groups'] = [g.strip() for g in open(self['groups']).readlines() if not g.startswith('#')]
+ self['servers'] = [s.strip() for s in open(self['servers']).readlines() if not s.startswith('#')]
+
+ try:
+ __import__(self['module'])
+ except ImportError:
+ log.msg("Warning: Cannot import %s" % (self['module'],))
+
+ f = open(self['schema'], 'w')
+ f.write(
+ database.NewsStorageAugmentation.schema + '\n' +
+ database.makeGroupSQL(self['groups']) + '\n' +
+ database.makeOverviewSQL()
+ )
+ f.close()
+
+ info = {
+ 'host': self['dbhost'], 'user': self['dbuser'],
+ 'database': self['database'], 'dbapiName': self['module']
+ }
+ self.db = database.NewsStorageAugmentation(info)
+
+
+class PickleOptions(usage.Options):
+ optParameters = [
+ ['file', None, 'news.pickle', "File to which to save pickle"],
+
+ # XXX - Hrm.
+ ["groups", "g", "groups.list", "File containing group list"],
+ ["servers", "s", "servers.list", "File containing server list"],
+ ["moderators", "m", "moderators.list",
+ "File containing moderators list"],
+ ]
+
+ subCommands = None
+
+ def postOptions(self):
+ # XXX - Hmmm.
+ filename = self['file']
+ self['groups'] = [g.strip() for g in open(self['groups']).readlines()
+ if not g.startswith('#')]
+ self['servers'] = [s.strip() for s in open(self['servers']).readlines()
+ if not s.startswith('#')]
+ self['moderators'] = [s.split()
+ for s in open(self['moderators']).readlines()
+ if not s.startswith('#')]
+ self.db = database.PickleStorage(filename, self['groups'],
+ self['moderators'])
+
+
+class Options(usage.Options):
+ synopsis = "[options]"
+
+ groups = None
+ servers = None
+ subscriptions = None
+
+ optParameters = [
+ ["port", "p", "119", "Listen port"],
+ ["interface", "i", "", "Interface to which to bind"],
+ ["datadir", "d", "news.db", "Root data storage path"],
+ ["mailhost", "m", "localhost", "Host of SMTP server to use"]
+ ]
+ compData = usage.Completions(
+ optActions={"datadir" : usage.CompleteDirs(),
+ "mailhost" : usage.CompleteHostnames(),
+ "interface" : usage.CompleteNetInterfaces()}
+ )
+
+ def __init__(self):
+ usage.Options.__init__(self)
+ self.groups = []
+ self.servers = []
+ self.subscriptions = []
+
+
+ def opt_group(self, group):
+ """The name of a newsgroup to carry."""
+ self.groups.append([group, None])
+
+
+ def opt_moderator(self, moderator):
+ """The email of the moderator for the most recently passed group."""
+ self.groups[-1][1] = moderator
+
+
+ def opt_subscription(self, group):
+ """A newsgroup to list as a recommended subscription."""
+ self.subscriptions.append(group)
+
+
+ def opt_server(self, server):
+ """The address of a Usenet server to pass messages to and receive messages from."""
+ self.servers.append(server)
+
+
+def makeService(config):
+ if not len(config.groups):
+ raise usage.UsageError("No newsgroups specified")
+
+ db = database.NewsShelf(config['mailhost'], config['datadir'])
+ for (g, m) in config.groups:
+ if m:
+ db.addGroup(g, 'm')
+ db.addModerator(g, m)
+ else:
+ db.addGroup(g, 'y')
+ for s in config.subscriptions:
+ print s
+ db.addSubscription(s)
+ s = config['port']
+ if config['interface']:
+ # Add a warning here
+ s += ':interface='+config['interface']
+ return strports.service(s, news.UsenetServerFactory(db, config.servers))
diff --git a/twisted/news/test/__init__.py b/twisted/news/test/__init__.py
new file mode 100644
index 0000000..677518d
--- /dev/null
+++ b/twisted/news/test/__init__.py
@@ -0,0 +1 @@
+"""News Tests"""
diff --git a/twisted/news/test/test_database.py b/twisted/news/test/test_database.py
new file mode 100644
index 0000000..42900a2
--- /dev/null
+++ b/twisted/news/test/test_database.py
@@ -0,0 +1,224 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.news.database}.
+"""
+
+__metaclass__ = type
+
+from email.Parser import Parser
+from socket import gethostname
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import succeed
+from twisted.mail.smtp import messageid
+from twisted.news.database import Article, PickleStorage, NewsShelf
+
+
+
+class ModerationTestsMixin:
+ """
+ Tests for the moderation features of L{INewsStorage} implementations.
+ """
+ def setUp(self):
+ self._email = []
+
+
+ def sendmail(self, smtphost, from_addr, to_addrs, msg,
+ senderDomainName=None, port=25):
+ """
+ Fake of L{twisted.mail.smtp.sendmail} which records attempts to send
+ email and immediately pretends success.
+
+ Subclasses should arrange for their storage implementation to call this
+ instead of the real C{sendmail} function.
+ """
+ self._email.append((
+ smtphost, from_addr, to_addrs, msg, senderDomainName, port))
+ return succeed(None)
+
+
+ _messageTemplate = """\
+From: some dude
+To: another person
+Subject: activities etc
+Message-ID: %(articleID)s
+Newsgroups: %(newsgroup)s
+%(approved)s
+Body of the message is such.
+""".replace('\n', '\r\n')
+
+
+ def getApprovedMessage(self, articleID, group):
+ """
+ Return a C{str} containing an RFC 2822 formatted message including an
+ I{Approved} header indicating it has passed through moderation.
+ """
+ return self._messageTemplate % {
+ 'articleID': articleID,
+ 'newsgroup': group,
+ 'approved': 'Approved: yup\r\n'}
+
+
+ def getUnapprovedMessage(self, articleID, group):
+ """
+ Return a C{str} containing an RFC 2822 formatted message with no
+ I{Approved} header indicating it may require moderation.
+ """
+ return self._messageTemplate % {
+ 'articleID': articleID,
+ 'newsgroup': group,
+ 'approved': '\r\n'}
+
+
+ def getStorage(self, groups, moderators, mailhost, sender):
+ """
+ Override in a subclass to return a L{INewsStorage} provider to test for
+ correct moderation behavior.
+
+ @param groups: A C{list} of C{str} naming the groups which should exist
+ in the resulting storage object.
+
+ @param moderators: A C{dict} mapping C{str} each group name to a C{list}
+ of C{str} giving moderator email (RFC 2821) addresses.
+ """
+ raise NotImplementedError()
+
+
+ def test_postApproved(self):
+ """
+ L{INewsStorage.postRequest} posts the message if it includes an
+ I{Approved} header.
+ """
+ group = "example.group"
+ moderator = "alice@example.com"
+ mailhost = "127.0.0.1"
+ sender = "bob@example.org"
+ articleID = messageid()
+ storage = self.getStorage(
+ [group], {group: [moderator]}, mailhost, sender)
+ message = self.getApprovedMessage(articleID, group)
+ result = storage.postRequest(message)
+
+ def cbPosted(ignored):
+ self.assertEqual(self._email, [])
+ exists = storage.articleExistsRequest(articleID)
+ exists.addCallback(self.assertTrue)
+ return exists
+ result.addCallback(cbPosted)
+ return result
+
+
+ def test_postModerated(self):
+ """
+ L{INewsStorage.postRequest} forwards a message to the moderator if it
+ does not include an I{Approved} header.
+ """
+ group = "example.group"
+ moderator = "alice@example.com"
+ mailhost = "127.0.0.1"
+ sender = "bob@example.org"
+ articleID = messageid()
+ storage = self.getStorage(
+ [group], {group: [moderator]}, mailhost, sender)
+ message = self.getUnapprovedMessage(articleID, group)
+ result = storage.postRequest(message)
+
+ def cbModerated(ignored):
+ self.assertEqual(len(self._email), 1)
+ self.assertEqual(self._email[0][0], mailhost)
+ self.assertEqual(self._email[0][1], sender)
+ self.assertEqual(self._email[0][2], [moderator])
+ self._checkModeratorMessage(
+ self._email[0][3], sender, moderator, group, message)
+ self.assertEqual(self._email[0][4], None)
+ self.assertEqual(self._email[0][5], 25)
+ exists = storage.articleExistsRequest(articleID)
+ exists.addCallback(self.assertFalse)
+ return exists
+ result.addCallback(cbModerated)
+ return result
+
+
+ def _checkModeratorMessage(self, messageText, sender, moderator, group, postingText):
+ p = Parser()
+ msg = p.parsestr(messageText)
+ headers = dict(msg.items())
+ del headers['Message-ID']
+ self.assertEqual(
+ headers,
+ {'From': sender,
+ 'To': moderator,
+ 'Subject': 'Moderate new %s message: activities etc' % (group,),
+ 'Content-Type': 'message/rfc822'})
+
+ posting = p.parsestr(postingText)
+ attachment = msg.get_payload()[0]
+
+ for header in ['from', 'to', 'subject', 'message-id', 'newsgroups']:
+ self.assertEqual(posting[header], attachment[header])
+
+ self.assertEqual(posting.get_payload(), attachment.get_payload())
+
+
+
+class PickleStorageTests(ModerationTestsMixin, TestCase):
+ """
+ Tests for L{PickleStorage}.
+ """
+ def getStorage(self, groups, moderators, mailhost, sender):
+ """
+ Create and return a L{PickleStorage} instance configured to require
+ moderation.
+ """
+ storageFilename = self.mktemp()
+ storage = PickleStorage(
+ storageFilename, groups, moderators, mailhost, sender)
+ storage.sendmail = self.sendmail
+ self.addCleanup(PickleStorage.sharedDBs.pop, storageFilename)
+ return storage
+
+
+
+class NewsShelfTests(ModerationTestsMixin, TestCase):
+ """
+ Tests for L{NewsShelf}.
+ """
+ def getStorage(self, groups, moderators, mailhost, sender):
+ """
+ Create and return a L{NewsShelf} instance configured to require
+ moderation.
+ """
+ storageFilename = self.mktemp()
+ shelf = NewsShelf(mailhost, storageFilename, sender)
+ for name in groups:
+ shelf.addGroup(name, 'm') # Dial 'm' for moderator
+ for address in moderators.get(name, []):
+ shelf.addModerator(name, address)
+ shelf.sendmail = self.sendmail
+ return shelf
+
+
+ def test_notifyModerator(self):
+ """
+ L{NewsShelf.notifyModerator} sends a moderation email to a single
+ moderator.
+ """
+ shelf = NewsShelf('example.com', self.mktemp(), 'alice@example.com')
+ shelf.sendmail = self.sendmail
+ shelf.notifyModerator('bob@example.org', Article('Foo: bar', 'Some text'))
+ self.assertEqual(len(self._email), 1)
+
+
+ def test_defaultSender(self):
+ """
+ If no sender is specified to L{NewsShelf.notifyModerators}, a default
+ address based on the system hostname is used for both the envelope and
+ RFC 2822 sender addresses.
+ """
+ shelf = NewsShelf('example.com', self.mktemp())
+ shelf.sendmail = self.sendmail
+ shelf.notifyModerators(['bob@example.org'], Article('Foo: bar', 'Some text'))
+ self.assertEqual(self._email[0][1], 'twisted-news@' + gethostname())
+ self.assertIn('From: twisted-news@' + gethostname(), self._email[0][3])
diff --git a/twisted/news/test/test_news.py b/twisted/news/test/test_news.py
new file mode 100644
index 0000000..35ac7d7
--- /dev/null
+++ b/twisted/news/test/test_news.py
@@ -0,0 +1,107 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, types
+from pprint import pformat
+
+from twisted.trial import unittest
+from twisted.news import database
+from twisted.internet import reactor
+
+MESSAGE_ID = "f83ba57450ed0fd8ac9a472b847e830e"
+
+POST_STRING = """Path: not-for-mail
+From: <exarkun@somehost.domain.com>
+Subject: a test
+Newsgroups: alt.test.nntp
+Organization:
+Summary:
+Keywords:
+Message-Id: %s
+User-Agent: tin/1.4.5-20010409 ("One More Nightmare") (UNIX) (Linux/2.4.17 (i686))
+
+this is a test
+...
+lala
+moo
+--
+"One World, one Web, one Program." - Microsoft(R) promotional ad
+"Ein Volk, ein Reich, ein Fuhrer." - Adolf Hitler
+--
+ 10:56pm up 4 days, 4:42, 1 user, load average: 0.08, 0.08, 0.12
+""" % (MESSAGE_ID)
+
+class NewsTestCase(unittest.TestCase):
+ def setUp(self):
+ self.backend = database.NewsShelf(None, 'news2.db')
+ self.backend.addGroup('alt.test.nntp', 'y')
+ self.backend.postRequest(POST_STRING.replace('\n', '\r\n'))
+
+
+ def testArticleExists(self):
+ d = self.backend.articleExistsRequest(MESSAGE_ID)
+ d.addCallback(self.failUnless)
+ return d
+
+
+ def testArticleRequest(self):
+ d = self.backend.articleRequest(None, None, MESSAGE_ID)
+
+ def cbArticle(result):
+ self.failUnless(isinstance(result, tuple),
+ 'callback result is wrong type: ' + str(result))
+ self.assertEqual(len(result), 3,
+ 'callback result list should have three entries: ' +
+ str(result))
+ self.assertEqual(result[1], MESSAGE_ID,
+ "callback result Message-Id doesn't match: %s vs %s" %
+ (MESSAGE_ID, result[1]))
+ body = result[2].read()
+ self.failIfEqual(body.find('\r\n\r\n'), -1,
+ "Can't find \\r\\n\\r\\n between header and body")
+ return result
+
+ d.addCallback(cbArticle)
+ return d
+
+
+ def testHeadRequest(self):
+ d = self.testArticleRequest()
+
+ def cbArticle(result):
+ index = result[0]
+
+ d = self.backend.headRequest("alt.test.nntp", index)
+ d.addCallback(cbHead)
+ return d
+
+ def cbHead(result):
+ self.assertEqual(result[1], MESSAGE_ID,
+ "callback result Message-Id doesn't match: %s vs %s" %
+ (MESSAGE_ID, result[1]))
+
+ self.assertEqual(result[2][-2:], '\r\n',
+ "headers must be \\r\\n terminated.")
+
+ d.addCallback(cbArticle)
+ return d
+
+
+ def testBodyRequest(self):
+ d = self.testArticleRequest()
+
+ def cbArticle(result):
+ index = result[0]
+
+ d = self.backend.bodyRequest("alt.test.nntp", index)
+ d.addCallback(cbBody)
+ return d
+
+ def cbBody(result):
+ body = result[2].read()
+ self.assertEqual(body[0:4], 'this',
+ "message body has been altered: " +
+ pformat(body[0:4]))
+
+ d.addCallback(cbArticle)
+ return d
diff --git a/twisted/news/test/test_nntp.py b/twisted/news/test/test_nntp.py
new file mode 100644
index 0000000..987546b
--- /dev/null
+++ b/twisted/news/test/test_nntp.py
@@ -0,0 +1,197 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.news import database
+from twisted.news import nntp
+from twisted.protocols import loopback
+from twisted.test import proto_helpers
+
+ALL_GROUPS = ('alt.test.nntp', 0, 1, 'y'),
+GROUP = ('0', '1', '0', 'alt.test.nntp', 'group', 'selected')
+SUBSCRIPTIONS = ['alt.test.nntp', 'news.testgroup']
+
+POST_STRING = """Path: not-for-mail
+From: <exarkun@somehost.domain.com>
+Subject: a test
+Newsgroups: alt.test.nntp
+Organization:
+Summary:
+Keywords:
+User-Agent: tin/1.4.5-20010409 ("One More Nightmare") (UNIX) (Linux/2.4.17 (i686))
+
+this is a test
+.
+..
+...
+lala
+moo
+--
+"One World, one Web, one Program." - Microsoft(R) promotional ad
+"Ein Volk, ein Reich, ein Fuhrer." - Adolf Hitler
+--
+ 10:56pm up 4 days, 4:42, 1 user, load average: 0.08, 0.08, 0.12
+"""
+
+class TestNNTPClient(nntp.NNTPClient):
+ def __init__(self):
+ nntp.NNTPClient.__init__(self)
+
+ def assertEqual(self, foo, bar):
+ if foo != bar: raise AssertionError("%r != %r!" % (foo, bar))
+
+ def connectionMade(self):
+ nntp.NNTPClient.connectionMade(self)
+ self.fetchSubscriptions()
+
+
+ def gotSubscriptions(self, subscriptions):
+ self.assertEqual(len(subscriptions), len(SUBSCRIPTIONS))
+ for s in subscriptions:
+ assert s in SUBSCRIPTIONS
+
+ self.fetchGroups()
+
+ def gotAllGroups(self, info):
+ self.assertEqual(len(info), len(ALL_GROUPS))
+ self.assertEqual(info[0], ALL_GROUPS[0])
+
+ self.fetchGroup('alt.test.nntp')
+
+
+ def getAllGroupsFailed(self, error):
+ raise AssertionError("fetchGroups() failed: %s" % (error,))
+
+
+ def gotGroup(self, info):
+ self.assertEqual(len(info), 6)
+ self.assertEqual(info, GROUP)
+
+ self.postArticle(POST_STRING)
+
+
+ def getSubscriptionsFailed(self, error):
+ raise AssertionError("fetchSubscriptions() failed: %s" % (error,))
+
+
+ def getGroupFailed(self, error):
+ raise AssertionError("fetchGroup() failed: %s" % (error,))
+
+
+ def postFailed(self, error):
+ raise AssertionError("postArticle() failed: %s" % (error,))
+
+
+ def postedOk(self):
+ self.fetchArticle(1)
+
+
+ def gotArticle(self, info):
+ origBody = POST_STRING.split('\n\n')[1]
+ newBody = info.split('\n\n', 1)[1]
+
+ self.assertEqual(origBody, newBody)
+
+ # We're done
+ self.transport.loseConnection()
+
+
+ def getArticleFailed(self, error):
+ raise AssertionError("fetchArticle() failed: %s" % (error,))
+
+
+class NNTPTestCase(unittest.TestCase):
+ def setUp(self):
+ self.server = nntp.NNTPServer()
+ self.server.factory = self
+ self.backend = database.NewsShelf(None, 'news.db')
+ self.backend.addGroup('alt.test.nntp', 'y')
+
+ for s in SUBSCRIPTIONS:
+ self.backend.addSubscription(s)
+
+ self.transport = proto_helpers.StringTransport()
+ self.server.makeConnection(self.transport)
+ self.client = TestNNTPClient()
+
+ def testLoopback(self):
+ return loopback.loopbackAsync(self.server, self.client)
+
+ # XXX This test is woefully incomplete. It tests the single
+ # most common code path and nothing else. Expand it and the
+ # test fairy will leave you a surprise.
+
+ # reactor.iterate(1) # fetchGroups()
+ # reactor.iterate(1) # fetchGroup()
+ # reactor.iterate(1) # postArticle()
+
+
+ def test_connectionMade(self):
+ """
+ When L{NNTPServer} is connected, it sends a server greeting to the
+ client.
+ """
+ self.assertEqual(
+ self.transport.value().split('\r\n'), [
+ '200 server ready - posting allowed',
+ ''])
+
+
+ def test_LIST(self):
+ """
+ When L{NTTPServer} receives a I{LIST} command, it sends a list of news
+ groups to the client (RFC 3977, section 7.6.1.1).
+ """
+ self.transport.clear()
+ self.server.do_LIST()
+ self.assertEqual(
+ self.transport.value().split('\r\n'), [
+ '215 newsgroups in form "group high low flags"',
+ 'alt.test.nntp 0 1 y',
+ '.',
+ ''])
+
+
+ def test_GROUP(self):
+ """
+ When L{NNTPServer} receives a I{GROUP} command, it sends a line of
+ information about that group to the client (RFC 3977, section 6.1.1.1).
+ """
+ self.transport.clear()
+ self.server.do_GROUP('alt.test.nntp')
+ self.assertEqual(
+ self.transport.value().split('\r\n'), [
+ '211 0 1 0 alt.test.nntp group selected',
+ ''])
+
+
+ def test_LISTGROUP(self):
+ """
+ When L{NNTPServer} receives a I{LISTGROUP} command, it sends a list of
+ message numbers for the messages in a particular group (RFC 3977,
+ section 6.1.2.1).
+ """
+ self.transport.clear()
+ self.server.do_LISTGROUP('alt.test.nntp')
+ self.assertEqual(
+ self.transport.value().split('\r\n'), [
+ '211 list of article numbers follow',
+ '.',
+ ''])
+
+
+ def test_XROVER(self):
+ """
+ When L{NTTPServer} receives a I{XROVER} command, it sends a list of
+ I{References} header values for the messages in a particular group (RFC
+ 2980, section 2.11).
+ """
+ self.server.do_GROUP('alt.test.nntp')
+ self.transport.clear()
+
+ self.server.do_XROVER()
+ self.assertEqual(
+ self.transport.value().split('\r\n'), [
+ '221 Header follows',
+ '.',
+ ''])
diff --git a/twisted/news/topfiles/NEWS b/twisted/news/topfiles/NEWS
new file mode 100644
index 0000000..3410a14
--- /dev/null
+++ b/twisted/news/topfiles/NEWS
@@ -0,0 +1,106 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted News 12.1.0 (2012-06-02)
+================================
+
+Bugfixes
+--------
+ - twisted.news.nntp.NNTPServer now has additional test coverage and
+ less redundant implementation code. (#5537)
+
+Deprecations and Removals
+-------------------------
+ - The ability to pass a string article to NNTPServer._gotBody and
+ NNTPServer._gotArticle in t.news.nntp has been deprecated for years
+ and is now removed. (#4548)
+
+
+Twisted News 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted News 11.1.0 (2011-11-15)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted News 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+Other
+-----
+ - #4580
+
+
+Twisted News 10.2.0 (2010-11-29)
+================================
+
+Bugfixes
+--------
+ - twisted.news.database.PickleStorage now invokes the email APIs
+ correctly, allowing it to actually send moderation emails. (#4528)
+
+
+Twisted News 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted News 10.0.0 (2010-03-01)
+================================
+
+No interesting changes since Twisted 9.0.
+
+
+Twisted News 9.0.0 (2009-11-24)
+===============================
+
+Other
+-----
+ - #2763, #3540
+
+
+News 8.2.0 (2008-12-16)
+=======================
+
+No interesting changes since Twisted 8.0.
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Misc
+----
+ - Remove all "API Stability" markers (#2847)
+
+
+0.3.0 (2007-01-06)
+==================
+Fixes
+-----
+ - News was updated to work with the latest twisted.components changes
+ to Twisted (#1636)
+ - The 'ip' attribute is no longer available on NNTP protocols (#1936)
+
+
+0.2.0 (2006-05-24)
+==================
+
+Fixes:
+ - Fixed a critical bug in moderation support.
+
diff --git a/twisted/news/topfiles/README b/twisted/news/topfiles/README
new file mode 100644
index 0000000..5146386
--- /dev/null
+++ b/twisted/news/topfiles/README
@@ -0,0 +1,4 @@
+Twisted News 12.1.0
+
+News depends on Twisted, and, if you want to use the moderation
+features, Twisted Mail.
diff --git a/twisted/news/topfiles/setup.py b/twisted/news/topfiles/setup.py
new file mode 100644
index 0000000..d776f30
--- /dev/null
+++ b/twisted/news/topfiles/setup.py
@@ -0,0 +1,28 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ dist.setup(
+ twisted_subproject="news",
+ # metadata
+ name="Twisted News",
+ description="Twisted News is an NNTP server and programming library.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Jp Calderone",
+ url="http://twistedmatrix.com/trac/wiki/TwistedNews",
+ license="MIT",
+ long_description="""\
+Twisted News is an NNTP protocol (Usenet) programming library. The
+library contains server and client protocol implementations. A simple
+NNTP server is also provided.
+""",
+ )
+
diff --git a/twisted/pair/__init__.py b/twisted/pair/__init__.py
new file mode 100644
index 0000000..6d3f5aa
--- /dev/null
+++ b/twisted/pair/__init__.py
@@ -0,0 +1,20 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted Pair: The framework of your ethernet.
+
+Low-level networking transports and utilities.
+
+See also twisted.protocols.ethernet, twisted.protocols.ip,
+twisted.protocols.raw and twisted.protocols.rawudp.
+
+Maintainer: Tommi Virtanen
+
+"""
+
+from twisted.pair._version import version
+__version__ = version.short()
diff --git a/twisted/pair/_version.py b/twisted/pair/_version.py
new file mode 100644
index 0000000..bad1509
--- /dev/null
+++ b/twisted/pair/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.pair', 12, 1, 0)
diff --git a/twisted/pair/ethernet.py b/twisted/pair/ethernet.py
new file mode 100644
index 0000000..b432c6f
--- /dev/null
+++ b/twisted/pair/ethernet.py
@@ -0,0 +1,56 @@
+# -*- test-case-name: twisted.pair.test.test_ethernet -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+
+"""Support for working directly with ethernet frames"""
+
+import struct
+
+
+from twisted.internet import protocol
+from twisted.pair import raw
+from zope.interface import implements, Interface
+
+
+class IEthernetProtocol(Interface):
+ """An interface for protocols that handle Ethernet frames"""
+ def addProto():
+ """Add an IRawPacketProtocol protocol"""
+
+ def datagramReceived():
+ """An Ethernet frame has been received"""
+
+class EthernetHeader:
+ def __init__(self, data):
+
+ (self.dest, self.source, self.proto) \
+ = struct.unpack("!6s6sH", data[:6+6+2])
+
+class EthernetProtocol(protocol.AbstractDatagramProtocol):
+
+ implements(IEthernetProtocol)
+
+ def __init__(self):
+ self.etherProtos = {}
+
+ def addProto(self, num, proto):
+ proto = raw.IRawPacketProtocol(proto)
+ if num < 0:
+ raise TypeError, 'Added protocol must be positive or zero'
+ if num >= 2**16:
+ raise TypeError, 'Added protocol must fit in 16 bits'
+ if num not in self.etherProtos:
+ self.etherProtos[num] = []
+ self.etherProtos[num].append(proto)
+
+ def datagramReceived(self, data, partial=0):
+ header = EthernetHeader(data[:14])
+ for proto in self.etherProtos.get(header.proto, ()):
+ proto.datagramReceived(data=data[14:],
+ partial=partial,
+ dest=header.dest,
+ source=header.source,
+ protocol=header.proto)
diff --git a/twisted/pair/ip.py b/twisted/pair/ip.py
new file mode 100644
index 0000000..de03bd4
--- /dev/null
+++ b/twisted/pair/ip.py
@@ -0,0 +1,72 @@
+# -*- test-case-name: twisted.pair.test.test_ip -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+
+"""Support for working directly with IP packets"""
+
+import struct
+import socket
+
+from twisted.internet import protocol
+from twisted.pair import raw
+from zope.interface import implements
+
+
+class IPHeader:
+ def __init__(self, data):
+
+ (ihlversion, self.tos, self.tot_len, self.fragment_id, frag_off,
+ self.ttl, self.protocol, self.check, saddr, daddr) \
+ = struct.unpack("!BBHHHBBH4s4s", data[:20])
+ self.saddr = socket.inet_ntoa(saddr)
+ self.daddr = socket.inet_ntoa(daddr)
+ self.version = ihlversion & 0x0F
+ self.ihl = ((ihlversion & 0xF0) >> 4) << 2
+ self.fragment_offset = frag_off & 0x1FFF
+ self.dont_fragment = (frag_off & 0x4000 != 0)
+ self.more_fragments = (frag_off & 0x2000 != 0)
+
+MAX_SIZE = 2L**32
+
+class IPProtocol(protocol.AbstractDatagramProtocol):
+ implements(raw.IRawPacketProtocol)
+
+ def __init__(self):
+ self.ipProtos = {}
+
+ def addProto(self, num, proto):
+ proto = raw.IRawDatagramProtocol(proto)
+ if num < 0:
+ raise TypeError, 'Added protocol must be positive or zero'
+ if num >= MAX_SIZE:
+ raise TypeError, 'Added protocol must fit in 32 bits'
+ if num not in self.ipProtos:
+ self.ipProtos[num] = []
+ self.ipProtos[num].append(proto)
+
+ def datagramReceived(self,
+ data,
+ partial,
+ dest,
+ source,
+ protocol):
+ header = IPHeader(data)
+ for proto in self.ipProtos.get(header.protocol, ()):
+ proto.datagramReceived(data=data[20:],
+ partial=partial,
+ source=header.saddr,
+ dest=header.daddr,
+ protocol=header.protocol,
+ version=header.version,
+ ihl=header.ihl,
+ tos=header.tos,
+ tot_len=header.tot_len,
+ fragment_id=header.fragment_id,
+ fragment_offset=header.fragment_offset,
+ dont_fragment=header.dont_fragment,
+ more_fragments=header.more_fragments,
+ ttl=header.ttl,
+ )
diff --git a/twisted/pair/raw.py b/twisted/pair/raw.py
new file mode 100644
index 0000000..0d3875b
--- /dev/null
+++ b/twisted/pair/raw.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""Interface definitions for working with raw packets"""
+
+from twisted.internet import protocol
+from zope.interface import Interface
+
+class IRawDatagramProtocol(Interface):
+ """An interface for protocols such as UDP, ICMP and TCP."""
+
+ def addProto():
+ """
+ Add a protocol on top of this one.
+ """
+
+ def datagramReceived():
+ """
+ An IP datagram has been received. Parse and process it.
+ """
+
+class IRawPacketProtocol(Interface):
+ """An interface for low-level protocols such as IP and ARP."""
+
+ def addProto():
+ """
+ Add a protocol on top of this one.
+ """
+
+ def datagramReceived():
+ """
+ An IP datagram has been received. Parse and process it.
+ """
diff --git a/twisted/pair/rawudp.py b/twisted/pair/rawudp.py
new file mode 100644
index 0000000..1425e6b
--- /dev/null
+++ b/twisted/pair/rawudp.py
@@ -0,0 +1,55 @@
+# -*- test-case-name: twisted.pair.test.test_rawudp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""Implementation of raw packet interfaces for UDP"""
+
+import struct
+
+from twisted.internet import protocol
+from twisted.pair import raw
+from zope.interface import implements
+
+class UDPHeader:
+ def __init__(self, data):
+
+ (self.source, self.dest, self.len, self.check) \
+ = struct.unpack("!HHHH", data[:8])
+
+class RawUDPProtocol(protocol.AbstractDatagramProtocol):
+ implements(raw.IRawDatagramProtocol)
+ def __init__(self):
+ self.udpProtos = {}
+
+ def addProto(self, num, proto):
+ if not isinstance(proto, protocol.DatagramProtocol):
+ raise TypeError, 'Added protocol must be an instance of DatagramProtocol'
+ if num < 0:
+ raise TypeError, 'Added protocol must be positive or zero'
+ if num >= 2**16:
+ raise TypeError, 'Added protocol must fit in 16 bits'
+ if num not in self.udpProtos:
+ self.udpProtos[num] = []
+ self.udpProtos[num].append(proto)
+
+ def datagramReceived(self,
+ data,
+ partial,
+ source,
+ dest,
+ protocol,
+ version,
+ ihl,
+ tos,
+ tot_len,
+ fragment_id,
+ fragment_offset,
+ dont_fragment,
+ more_fragments,
+ ttl):
+ header = UDPHeader(data)
+ for proto in self.udpProtos.get(header.dest, ()):
+ proto.datagramReceived(data[8:],
+ (source, header.source))
diff --git a/twisted/pair/test/__init__.py b/twisted/pair/test/__init__.py
new file mode 100644
index 0000000..5aa286e
--- /dev/null
+++ b/twisted/pair/test/__init__.py
@@ -0,0 +1 @@
+'pair tests'
diff --git a/twisted/pair/test/test_ethernet.py b/twisted/pair/test/test_ethernet.py
new file mode 100644
index 0000000..2b675fe
--- /dev/null
+++ b/twisted/pair/test/test_ethernet.py
@@ -0,0 +1,226 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.trial import unittest
+
+from twisted.internet import protocol, reactor, error
+from twisted.python import failure, components
+from twisted.pair import ethernet, raw
+from zope.interface import implements
+
+class MyProtocol:
+ implements(raw.IRawPacketProtocol)
+
+ def __init__(self, expecting):
+ self.expecting = list(expecting)
+
+ def datagramReceived(self, data, **kw):
+ assert self.expecting, 'Got a packet when not expecting anymore.'
+ expect = self.expecting.pop(0)
+ assert expect == (data, kw), \
+ "Expected %r, got %r" % (
+ expect, (data, kw),
+ )
+
+class EthernetTestCase(unittest.TestCase):
+ def testPacketParsing(self):
+ proto = ethernet.EthernetProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0800,
+ }),
+
+ ])
+ proto.addProto(0x0800, p1)
+
+ proto.datagramReceived("123456987654\x08\x00foobar",
+ partial=0)
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+
+ def testMultiplePackets(self):
+ proto = ethernet.EthernetProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0800,
+ }),
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': "012345",
+ 'source': "abcdef",
+ 'protocol': 0x0800,
+ }),
+
+ ])
+ proto.addProto(0x0800, p1)
+
+ proto.datagramReceived("123456987654\x08\x00foobar",
+ partial=0)
+ proto.datagramReceived("012345abcdef\x08\x00quux",
+ partial=1)
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+
+ def testMultipleSameProtos(self):
+ proto = ethernet.EthernetProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0800,
+ }),
+
+ ])
+
+ p2 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0800,
+ }),
+
+ ])
+
+ proto.addProto(0x0800, p1)
+ proto.addProto(0x0800, p2)
+
+ proto.datagramReceived("123456987654\x08\x00foobar",
+ partial=0)
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testWrongProtoNotSeen(self):
+ proto = ethernet.EthernetProtocol()
+ p1 = MyProtocol([])
+ proto.addProto(0x0801, p1)
+
+ proto.datagramReceived("123456987654\x08\x00foobar",
+ partial=0)
+ proto.datagramReceived("012345abcdef\x08\x00quux",
+ partial=1)
+
+ def testDemuxing(self):
+ proto = ethernet.EthernetProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0800,
+ }),
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': "012345",
+ 'source': "abcdef",
+ 'protocol': 0x0800,
+ }),
+
+ ])
+ proto.addProto(0x0800, p1)
+
+ p2 = MyProtocol([
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': "012345",
+ 'source': "abcdef",
+ 'protocol': 0x0806,
+ }),
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': "123456",
+ 'source': "987654",
+ 'protocol': 0x0806,
+ }),
+
+ ])
+ proto.addProto(0x0806, p2)
+
+ proto.datagramReceived("123456987654\x08\x00foobar",
+ partial=0)
+ proto.datagramReceived("012345abcdef\x08\x06quux",
+ partial=1)
+ proto.datagramReceived("123456987654\x08\x06foobar",
+ partial=0)
+ proto.datagramReceived("012345abcdef\x08\x00quux",
+ partial=1)
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testAddingBadProtos_WrongLevel(self):
+ """Adding a wrong level protocol raises an exception."""
+ e = ethernet.EthernetProtocol()
+ try:
+ e.addProto(42, "silliness")
+ except components.CannotAdapt:
+ pass
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooSmall(self):
+ """Adding a protocol with a negative number raises an exception."""
+ e = ethernet.EthernetProtocol()
+ try:
+ e.addProto(-1, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must be positive or zero',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooBig(self):
+ """Adding a protocol with a number >=2**16 raises an exception."""
+ e = ethernet.EthernetProtocol()
+ try:
+ e.addProto(2**16, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 16 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+ def testAddingBadProtos_TooBig2(self):
+ """Adding a protocol with a number >=2**16 raises an exception."""
+ e = ethernet.EthernetProtocol()
+ try:
+ e.addProto(2**16+1, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 16 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
diff --git a/twisted/pair/test/test_ip.py b/twisted/pair/test/test_ip.py
new file mode 100644
index 0000000..ed1623b
--- /dev/null
+++ b/twisted/pair/test/test_ip.py
@@ -0,0 +1,417 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.trial import unittest
+
+from twisted.internet import protocol, reactor, error
+from twisted.python import failure, components
+from twisted.pair import ip, raw
+from zope import interface
+
+class MyProtocol:
+ interface.implements(raw.IRawDatagramProtocol)
+
+ def __init__(self, expecting):
+ self.expecting = list(expecting)
+
+ def datagramReceived(self, data, **kw):
+ assert self.expecting, 'Got a packet when not expecting anymore.'
+ expectData, expectKw = self.expecting.pop(0)
+
+ expectKwKeys = expectKw.keys(); expectKwKeys.sort()
+ kwKeys = kw.keys(); kwKeys.sort()
+ assert expectKwKeys == kwKeys, "Expected %r, got %r" % (expectKwKeys, kwKeys)
+
+ for k in expectKwKeys:
+ assert expectKw[k] == kw[k], "Expected %s=%r, got %r" % (k, expectKw[k], kw[k])
+ assert expectKw == kw, "Expected %r, got %r" % (expectKw, kw)
+ assert expectData == data, "Expected %r, got %r" % (expectData, data)
+
+class IPTestCase(unittest.TestCase):
+ def testPacketParsing(self):
+ proto = ip.IPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ])
+ proto.addProto(0x0F, p1)
+
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+ def testMultiplePackets(self):
+ proto = ip.IPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': '5.4.3.2',
+ 'source': '6.7.8.9',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ])
+ proto.addProto(0x0F, p1)
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x06\x07\x08\x09" + "\x05\x04\x03\x02" + "quux",
+ partial=1,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+
+ def testMultipleSameProtos(self):
+ proto = ip.IPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ])
+
+ p2 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ])
+
+ proto.addProto(0x0F, p1)
+ proto.addProto(0x0F, p2)
+
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testWrongProtoNotSeen(self):
+ proto = ip.IPProtocol()
+ p1 = MyProtocol([])
+ proto.addProto(1, p1)
+
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+
+ def testDemuxing(self):
+ proto = ip.IPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': '5.4.3.2',
+ 'source': '6.7.8.9',
+ 'protocol': 0x0F,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ])
+ proto.addProto(0x0F, p1)
+
+ p2 = MyProtocol([
+
+ ('quux', {
+ 'partial': 1,
+ 'dest': '5.4.3.2',
+ 'source': '6.7.8.9',
+ 'protocol': 0x0A,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+ ('foobar', {
+ 'partial': 0,
+ 'dest': '1.2.3.4',
+ 'source': '5.6.7.8',
+ 'protocol': 0x0A,
+ 'version': 4,
+ 'ihl': 20,
+ 'tos': 7,
+ 'tot_len': 20+6,
+ 'fragment_id': 0xDEAD,
+ 'fragment_offset': 0x1EEF,
+ 'dont_fragment': 0,
+ 'more_fragments': 1,
+ 'ttl': 0xC0,
+ }),
+
+
+ ])
+ proto.addProto(0x0A, p2)
+
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0A" #protocol
+ + "FE" #checksum
+ + "\x06\x07\x08\x09" + "\x05\x04\x03\x02" + "quux",
+ partial=1,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0F" #protocol
+ + "FE" #checksum
+ + "\x06\x07\x08\x09" + "\x05\x04\x03\x02" + "quux",
+ partial=1,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+ proto.datagramReceived("\x54" #ihl version
+ + "\x07" #tos
+ + "\x00\x1a" #tot_len
+ + "\xDE\xAD" #id
+ + "\xBE\xEF" #frag_off
+ + "\xC0" #ttl
+ + "\x0A" #protocol
+ + "FE" #checksum
+ + "\x05\x06\x07\x08" + "\x01\x02\x03\x04" + "foobar",
+ partial=0,
+ dest='dummy',
+ source='dummy',
+ protocol='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testAddingBadProtos_WrongLevel(self):
+ """Adding a wrong level protocol raises an exception."""
+ e = ip.IPProtocol()
+ try:
+ e.addProto(42, "silliness")
+ except components.CannotAdapt:
+ pass
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooSmall(self):
+ """Adding a protocol with a negative number raises an exception."""
+ e = ip.IPProtocol()
+ try:
+ e.addProto(-1, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must be positive or zero',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooBig(self):
+ """Adding a protocol with a number >=2**32 raises an exception."""
+ e = ip.IPProtocol()
+ try:
+ e.addProto(2L**32, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 32 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+ def testAddingBadProtos_TooBig2(self):
+ """Adding a protocol with a number >=2**32 raises an exception."""
+ e = ip.IPProtocol()
+ try:
+ e.addProto(2L**32+1, MyProtocol([]))
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 32 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
diff --git a/twisted/pair/test/test_rawudp.py b/twisted/pair/test/test_rawudp.py
new file mode 100644
index 0000000..f53f078
--- /dev/null
+++ b/twisted/pair/test/test_rawudp.py
@@ -0,0 +1,327 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.trial import unittest
+
+from twisted.internet import protocol, reactor, error
+from twisted.python import failure
+from twisted.pair import rawudp
+
+class MyProtocol(protocol.DatagramProtocol):
+ def __init__(self, expecting):
+ self.expecting = list(expecting)
+
+ def datagramReceived(self, data, (host, port)):
+ assert self.expecting, 'Got a packet when not expecting anymore.'
+ expectData, expectHost, expectPort = self.expecting.pop(0)
+
+ assert expectData == data, "Expected data %r, got %r" % (expectData, data)
+ assert expectHost == host, "Expected host %r, got %r" % (expectHost, host)
+ assert expectPort == port, "Expected port %d=0x%04x, got %d=0x%04x" % (expectPort, expectPort, port, port)
+
+class RawUDPTestCase(unittest.TestCase):
+ def testPacketParsing(self):
+ proto = rawudp.RawUDPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', 'testHost', 0x43A2),
+
+ ])
+ proto.addProto(0xF00F, p1)
+
+ proto.datagramReceived("\x43\xA2" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+ def testMultiplePackets(self):
+ proto = rawudp.RawUDPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', 'testHost', 0x43A2),
+ ('quux', 'otherHost', 0x33FE),
+
+ ])
+ proto.addProto(0xF00F, p1)
+ proto.datagramReceived("\x43\xA2" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+ proto.datagramReceived("\x33\xFE" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x05" #len
+ + "\xDE\xAD" #check
+ + "quux",
+ partial=0,
+ dest='dummy',
+ source='otherHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+
+
+ def testMultipleSameProtos(self):
+ proto = rawudp.RawUDPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', 'testHost', 0x43A2),
+
+ ])
+
+ p2 = MyProtocol([
+
+ ('foobar', 'testHost', 0x43A2),
+
+ ])
+
+ proto.addProto(0xF00F, p1)
+ proto.addProto(0xF00F, p2)
+
+ proto.datagramReceived("\x43\xA2" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testWrongProtoNotSeen(self):
+ proto = rawudp.RawUDPProtocol()
+ p1 = MyProtocol([])
+ proto.addProto(1, p1)
+
+ proto.datagramReceived("\x43\xA2" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+
+ def testDemuxing(self):
+ proto = rawudp.RawUDPProtocol()
+ p1 = MyProtocol([
+
+ ('foobar', 'testHost', 0x43A2),
+ ('quux', 'otherHost', 0x33FE),
+
+ ])
+ proto.addProto(0xF00F, p1)
+
+ p2 = MyProtocol([
+
+ ('quux', 'otherHost', 0xA401),
+ ('foobar', 'testHost', 0xA302),
+
+ ])
+ proto.addProto(0xB050, p2)
+
+ proto.datagramReceived("\xA4\x01" #source
+ + "\xB0\x50" #dest
+ + "\x00\x05" #len
+ + "\xDE\xAD" #check
+ + "quux",
+ partial=0,
+ dest='dummy',
+ source='otherHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+ proto.datagramReceived("\x43\xA2" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+ proto.datagramReceived("\x33\xFE" #source
+ + "\xf0\x0f" #dest
+ + "\x00\x05" #len
+ + "\xDE\xAD" #check
+ + "quux",
+ partial=0,
+ dest='dummy',
+ source='otherHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+ proto.datagramReceived("\xA3\x02" #source
+ + "\xB0\x50" #dest
+ + "\x00\x06" #len
+ + "\xDE\xAD" #check
+ + "foobar",
+ partial=0,
+ dest='dummy',
+ source='testHost',
+ protocol='dummy',
+ version='dummy',
+ ihl='dummy',
+ tos='dummy',
+ tot_len='dummy',
+ fragment_id='dummy',
+ fragment_offset='dummy',
+ dont_fragment='dummy',
+ more_fragments='dummy',
+ ttl='dummy',
+ )
+
+ assert not p1.expecting, \
+ 'Should not expect any more packets, but still want %r' % p1.expecting
+ assert not p2.expecting, \
+ 'Should not expect any more packets, but still want %r' % p2.expecting
+
+ def testAddingBadProtos_WrongLevel(self):
+ """Adding a wrong level protocol raises an exception."""
+ e = rawudp.RawUDPProtocol()
+ try:
+ e.addProto(42, "silliness")
+ except TypeError, e:
+ if e.args == ('Added protocol must be an instance of DatagramProtocol',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooSmall(self):
+ """Adding a protocol with a negative number raises an exception."""
+ e = rawudp.RawUDPProtocol()
+ try:
+ e.addProto(-1, protocol.DatagramProtocol())
+ except TypeError, e:
+ if e.args == ('Added protocol must be positive or zero',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+
+ def testAddingBadProtos_TooBig(self):
+ """Adding a protocol with a number >=2**16 raises an exception."""
+ e = rawudp.RawUDPProtocol()
+ try:
+ e.addProto(2**16, protocol.DatagramProtocol())
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 16 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
+
+ def testAddingBadProtos_TooBig2(self):
+ """Adding a protocol with a number >=2**16 raises an exception."""
+ e = rawudp.RawUDPProtocol()
+ try:
+ e.addProto(2**16+1, protocol.DatagramProtocol())
+ except TypeError, e:
+ if e.args == ('Added protocol must fit in 16 bits',):
+ pass
+ else:
+ raise
+ else:
+ raise AssertionError, 'addProto must raise an exception for bad protocols'
diff --git a/twisted/pair/topfiles/NEWS b/twisted/pair/topfiles/NEWS
new file mode 100644
index 0000000..509082c
--- /dev/null
+++ b/twisted/pair/topfiles/NEWS
@@ -0,0 +1,56 @@
+Twisted Pair 12.1.0 (2012-06-02)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 12.0.0 (2012-02-10)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 11.1.0 (2011-11-15)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 11.0.0 (2011-04-01)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 10.2.0 (2010-11-29)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 10.1.0 (2010-06-27)
+================================
+
+No significant changes have been made for this release.
+
+
+Twisted Pair 10.0.0 (2010-03-01)
+================================
+
+Other
+-----
+ - #4170
+
+
+Twisted Pair 9.0.0 (2009-11-24)
+===============================
+
+Other
+-----
+ - #3540, #4050
+
+
+Pair 8.2.0 (2008-12-16)
+=======================
+
+No interesting changes since Twisted 8.0.
diff --git a/twisted/pair/topfiles/README b/twisted/pair/topfiles/README
new file mode 100644
index 0000000..7135b5c
--- /dev/null
+++ b/twisted/pair/topfiles/README
@@ -0,0 +1,4 @@
+Twisted Pair 12.1.0
+
+Twisted Pair depends on Twisted Core. For TUN/TAP access, python-eunuchs
+(<http://pypi.python.org/pypi/python-eunuchs/0.0.0>) is also required.
diff --git a/twisted/pair/topfiles/setup.py b/twisted/pair/topfiles/setup.py
new file mode 100644
index 0000000..c42754f
--- /dev/null
+++ b/twisted/pair/topfiles/setup.py
@@ -0,0 +1,28 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ dist.setup(
+ twisted_subproject="pair",
+ # metadata
+ name="Twisted Pair",
+ description="Twisted Pair contains low-level networking support.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Tommi Virtanen",
+ url="http://twistedmatrix.com/trac/wiki/TwistedPair",
+ license="MIT",
+ long_description="""
+Raw network packet parsing routines, including ethernet, IP and UDP
+packets, and tuntap support.
+""",
+ )
diff --git a/twisted/pair/tuntap.py b/twisted/pair/tuntap.py
new file mode 100644
index 0000000..e3ece5e
--- /dev/null
+++ b/twisted/pair/tuntap.py
@@ -0,0 +1,170 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+import errno, os
+from twisted.python import log, reflect, components
+from twisted.internet import base, fdesc, error
+from twisted.pair import ethernet, ip
+
+"""
+You need Eunuchs for twisted.pair.tuntap to work.
+
+Eunuchs is a library containing the missing manly parts of
+UNIX API for Python.
+
+Eunuchs is a library of Python extension that complement the standard
+libraries in parts where full support for the UNIX API (or the Linux
+API) is missing.
+
+Most of the functions wrapped by Eunuchs are low-level, dirty, but
+absolutely necessary functions for real systems programming. The aim is
+to have the functions added to mainstream Python libraries.
+
+Current list of functions included:
+
+ - fchdir(2)
+ - recvmsg(2) and sendmsg(2), including use of cmsg(3)
+ - socketpair(2)
+ - support for TUN/TAP virtual network interfaces
+
+Eunuchs doesn't have a proper web home right now, but you can fetch
+the source from http://ftp.debian.org/debian/pool/main/e/eunuch
+-- debian users can just use 'apt-get install python-eunuchs'.
+
+"""
+from eunuchs.tuntap import opentuntap, TuntapPacketInfo, makePacketInfo
+
+class TuntapPort(base.BasePort):
+ """A Port that reads and writes packets from/to a TUN/TAP-device.
+
+ TODO: Share general start/stop etc implementation details with
+ twisted.internet.udp.Port.
+ """
+ maxThroughput = 256 * 1024 # max bytes we read in one eventloop iteration
+
+ def __init__(self, interface, proto, maxPacketSize=8192, reactor=None):
+ if components.implements(proto, ethernet.IEthernetProtocol):
+ self.ethernet = 1
+ else:
+ self.ethernet = 0
+ assert components.implements(proto, ip.IIPProtocol) # XXX: fix me
+ base.BasePort.__init__(self, reactor)
+ self.interface = interface
+ self.protocol = proto
+ self.maxPacketSize = maxPacketSize
+ self.setLogStr()
+
+ def __repr__(self):
+ return "<%s on %s>" % (self.protocol.__class__, self.interface)
+
+ def startListening(self):
+ """Create and bind my socket, and begin listening on it.
+
+ This is called on unserialization, and must be called after creating a
+ server to begin listening on the specified port.
+ """
+ self._bindSocket()
+ self._connectToProtocol()
+
+ def _bindSocket(self):
+ log.msg("%s starting on %s"%(self.protocol.__class__, self.interface))
+ try:
+ fd, name = opentuntap(name=self.interface,
+ ethernet=self.ethernet,
+ packetinfo=0)
+ except OSError, e:
+ raise error.CannotListenError, (None, self.interface, e)
+ fdesc.setNonBlocking(fd)
+ self.interface = name
+ self.connected = 1
+ self.fd = fd
+
+ def fileno(self):
+ return self.fd
+
+ def _connectToProtocol(self):
+ self.protocol.makeConnection(self)
+ self.startReading()
+
+ def doRead(self):
+ """Called when my socket is ready for reading."""
+ read = 0
+ while read < self.maxThroughput:
+ try:
+ data = os.read(self.fd, self.maxPacketSize)
+ read += len(data)
+# pkt = TuntapPacketInfo(data)
+ self.protocol.datagramReceived(data,
+ partial=0 # pkt.isPartial(),
+ )
+ except OSError, e:
+ if e.errno in (errno.EWOULDBLOCK,):
+ return
+ else:
+ raise
+ except IOError, e:
+ if e.errno in (errno.EAGAIN, errno.EINTR):
+ return
+ else:
+ raise
+ except:
+ log.deferr()
+
+ def write(self, datagram):
+ """Write a datagram."""
+# header = makePacketInfo(0, 0)
+ try:
+ return os.write(self.fd, datagram)
+ except IOError, e:
+ if e.errno == errno.EINTR:
+ return self.write(datagram)
+ elif e.errno == errno.EMSGSIZE:
+ raise error.MessageLengthError, "message too long"
+ elif e.errno == errno.ECONNREFUSED:
+ raise error.ConnectionRefusedError
+ else:
+ raise
+
+ def writeSequence(self, seq):
+ self.write("".join(seq))
+
+ def loseConnection(self):
+ """Stop accepting connections on this port.
+
+ This will shut down my socket and call self.connectionLost().
+ """
+ self.stopReading()
+ if self.connected:
+ from twisted.internet import reactor
+ reactor.callLater(0, self.connectionLost)
+
+ stopListening = loseConnection
+
+ def connectionLost(self, reason=None):
+ """Cleans up my socket.
+ """
+ log.msg('(Tuntap %s Closed)' % self.interface)
+ base.BasePort.connectionLost(self, reason)
+ if hasattr(self, "protocol"):
+ # we won't have attribute in ConnectedPort, in cases
+ # where there was an error in connection process
+ self.protocol.doStop()
+ self.connected = 0
+ os.close(self.fd)
+ del self.fd
+
+ def setLogStr(self):
+ self.logstr = reflect.qual(self.protocol.__class__) + " (TUNTAP)"
+
+ def logPrefix(self):
+ """Returns the name of my class, to prefix log entries with.
+ """
+ return self.logstr
+
+ def getHost(self):
+ """
+ Returns a tuple of ('TUNTAP', interface), indicating
+ the servers address
+ """
+ return ('TUNTAP',)+self.interface
diff --git a/twisted/persisted/__init__.py b/twisted/persisted/__init__.py
new file mode 100644
index 0000000..a8a918b
--- /dev/null
+++ b/twisted/persisted/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted Persisted: utilities for managing persistence.
+"""
diff --git a/twisted/persisted/aot.py b/twisted/persisted/aot.py
new file mode 100644
index 0000000..59fde57
--- /dev/null
+++ b/twisted/persisted/aot.py
@@ -0,0 +1,560 @@
+# -*- test-case-name: twisted.test.test_persisted -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+"""
+AOT: Abstract Object Trees
+The source-code-marshallin'est abstract-object-serializin'est persister
+this side of Marmalade!
+"""
+
+import types, copy_reg, tokenize, re
+
+from twisted.python import reflect, log
+from twisted.persisted import crefutil
+
+###########################
+# Abstract Object Classes #
+###########################
+
+#"\0" in a getSource means "insert variable-width indention here".
+#see `indentify'.
+
+class Named:
+ def __init__(self, name):
+ self.name = name
+
+class Class(Named):
+ def getSource(self):
+ return "Class(%r)" % self.name
+
+class Function(Named):
+ def getSource(self):
+ return "Function(%r)" % self.name
+
+class Module(Named):
+ def getSource(self):
+ return "Module(%r)" % self.name
+
+
+class InstanceMethod:
+ def __init__(self, name, klass, inst):
+ if not (isinstance(inst, Ref) or isinstance(inst, Instance) or isinstance(inst, Deref)):
+ raise TypeError("%s isn't an Instance, Ref, or Deref!" % inst)
+ self.name = name
+ self.klass = klass
+ self.instance = inst
+
+ def getSource(self):
+ return "InstanceMethod(%r, %r, \n\0%s)" % (self.name, self.klass, prettify(self.instance))
+
+
+class _NoStateObj:
+ pass
+NoStateObj = _NoStateObj()
+
+_SIMPLE_BUILTINS = [
+ types.StringType, types.UnicodeType, types.IntType, types.FloatType,
+ types.ComplexType, types.LongType, types.NoneType, types.SliceType,
+ types.EllipsisType]
+
+try:
+ _SIMPLE_BUILTINS.append(types.BooleanType)
+except AttributeError:
+ pass
+
+class Instance:
+ def __init__(self, className, __stateObj__=NoStateObj, **state):
+ if not isinstance(className, types.StringType):
+ raise TypeError("%s isn't a string!" % className)
+ self.klass = className
+ if __stateObj__ is not NoStateObj:
+ self.state = __stateObj__
+ self.stateIsDict = 0
+ else:
+ self.state = state
+ self.stateIsDict = 1
+
+ def getSource(self):
+ #XXX make state be foo=bar instead of a dict.
+ if self.stateIsDict:
+ stateDict = self.state
+ elif isinstance(self.state, Ref) and isinstance(self.state.obj, types.DictType):
+ stateDict = self.state.obj
+ else:
+ stateDict = None
+ if stateDict is not None:
+ try:
+ return "Instance(%r, %s)" % (self.klass, dictToKW(stateDict))
+ except NonFormattableDict:
+ return "Instance(%r, %s)" % (self.klass, prettify(stateDict))
+ return "Instance(%r, %s)" % (self.klass, prettify(self.state))
+
+class Ref:
+
+ def __init__(self, *args):
+ #blargh, lame.
+ if len(args) == 2:
+ self.refnum = args[0]
+ self.obj = args[1]
+ elif not args:
+ self.refnum = None
+ self.obj = None
+
+ def setRef(self, num):
+ if self.refnum:
+ raise ValueError("Error setting id %s, I already have %s" % (num, self.refnum))
+ self.refnum = num
+
+ def setObj(self, obj):
+ if self.obj:
+ raise ValueError("Error setting obj %s, I already have %s" % (obj, self.obj))
+ self.obj = obj
+
+ def getSource(self):
+ if self.obj is None:
+ raise RuntimeError("Don't try to display me before setting an object on me!")
+ if self.refnum:
+ return "Ref(%d, \n\0%s)" % (self.refnum, prettify(self.obj))
+ return prettify(self.obj)
+
+
+class Deref:
+ def __init__(self, num):
+ self.refnum = num
+
+ def getSource(self):
+ return "Deref(%d)" % self.refnum
+
+ __repr__ = getSource
+
+
+class Copyreg:
+ def __init__(self, loadfunc, state):
+ self.loadfunc = loadfunc
+ self.state = state
+
+ def getSource(self):
+ return "Copyreg(%r, %s)" % (self.loadfunc, prettify(self.state))
+
+
+
+###############
+# Marshalling #
+###############
+
+
+def getSource(ao):
+ """Pass me an AO, I'll return a nicely-formatted source representation."""
+ return indentify("app = " + prettify(ao))
+
+
+class NonFormattableDict(Exception):
+ """A dictionary was not formattable.
+ """
+
+r = re.compile('[a-zA-Z_][a-zA-Z0-9_]*$')
+
+def dictToKW(d):
+ out = []
+ items = d.items()
+ items.sort()
+ for k,v in items:
+ if not isinstance(k, types.StringType):
+ raise NonFormattableDict("%r ain't a string" % k)
+ if not r.match(k):
+ raise NonFormattableDict("%r ain't an identifier" % k)
+ out.append(
+ "\n\0%s=%s," % (k, prettify(v))
+ )
+ return ''.join(out)
+
+
+def prettify(obj):
+ if hasattr(obj, 'getSource'):
+ return obj.getSource()
+ else:
+ #basic type
+ t = type(obj)
+
+ if t in _SIMPLE_BUILTINS:
+ return repr(obj)
+
+ elif t is types.DictType:
+ out = ['{']
+ for k,v in obj.items():
+ out.append('\n\0%s: %s,' % (prettify(k), prettify(v)))
+ out.append(len(obj) and '\n\0}' or '}')
+ return ''.join(out)
+
+ elif t is types.ListType:
+ out = ["["]
+ for x in obj:
+ out.append('\n\0%s,' % prettify(x))
+ out.append(len(obj) and '\n\0]' or ']')
+ return ''.join(out)
+
+ elif t is types.TupleType:
+ out = ["("]
+ for x in obj:
+ out.append('\n\0%s,' % prettify(x))
+ out.append(len(obj) and '\n\0)' or ')')
+ return ''.join(out)
+ else:
+ raise TypeError("Unsupported type %s when trying to prettify %s." % (t, obj))
+
+def indentify(s):
+ out = []
+ stack = []
+ def eater(type, val, r, c, l, out=out, stack=stack):
+ #import sys
+ #sys.stdout.write(val)
+ if val in ['[', '(', '{']:
+ stack.append(val)
+ elif val in [']', ')', '}']:
+ stack.pop()
+ if val == '\0':
+ out.append(' '*len(stack))
+ else:
+ out.append(val)
+ l = ['', s]
+ tokenize.tokenize(l.pop, eater)
+ return ''.join(out)
+
+
+
+
+
+###########
+# Unjelly #
+###########
+
+def unjellyFromAOT(aot):
+ """
+ Pass me an Abstract Object Tree, and I'll unjelly it for you.
+ """
+ return AOTUnjellier().unjelly(aot)
+
+def unjellyFromSource(stringOrFile):
+ """
+ Pass me a string of code or a filename that defines an 'app' variable (in
+ terms of Abstract Objects!), and I'll execute it and unjelly the resulting
+ AOT for you, returning a newly unpersisted Application object!
+ """
+
+ ns = {"Instance": Instance,
+ "InstanceMethod": InstanceMethod,
+ "Class": Class,
+ "Function": Function,
+ "Module": Module,
+ "Ref": Ref,
+ "Deref": Deref,
+ "Copyreg": Copyreg,
+ }
+
+ if hasattr(stringOrFile, "read"):
+ exec stringOrFile.read() in ns
+ else:
+ exec stringOrFile in ns
+
+ if ns.has_key('app'):
+ return unjellyFromAOT(ns['app'])
+ else:
+ raise ValueError("%s needs to define an 'app', it didn't!" % stringOrFile)
+
+
+class AOTUnjellier:
+ """I handle the unjellying of an Abstract Object Tree.
+ See AOTUnjellier.unjellyAO
+ """
+ def __init__(self):
+ self.references = {}
+ self.stack = []
+ self.afterUnjelly = []
+
+ ##
+ # unjelly helpers (copied pretty much directly from (now deleted) marmalade)
+ ##
+ def unjellyLater(self, node):
+ """Unjelly a node, later.
+ """
+ d = crefutil._Defer()
+ self.unjellyInto(d, 0, node)
+ return d
+
+ def unjellyInto(self, obj, loc, ao):
+ """Utility method for unjellying one object into another.
+ This automates the handling of backreferences.
+ """
+ o = self.unjellyAO(ao)
+ obj[loc] = o
+ if isinstance(o, crefutil.NotKnown):
+ o.addDependant(obj, loc)
+ return o
+
+ def callAfter(self, callable, result):
+ if isinstance(result, crefutil.NotKnown):
+ l = [None]
+ result.addDependant(l, 1)
+ else:
+ l = [result]
+ self.afterUnjelly.append((callable, l))
+
+ def unjellyAttribute(self, instance, attrName, ao):
+ #XXX this is unused????
+ """Utility method for unjellying into instances of attributes.
+
+ Use this rather than unjellyAO unless you like surprising bugs!
+ Alternatively, you can use unjellyInto on your instance's __dict__.
+ """
+ self.unjellyInto(instance.__dict__, attrName, ao)
+
+ def unjellyAO(self, ao):
+ """Unjelly an Abstract Object and everything it contains.
+ I return the real object.
+ """
+ self.stack.append(ao)
+ t = type(ao)
+ if t is types.InstanceType:
+ #Abstract Objects
+ c = ao.__class__
+ if c is Module:
+ return reflect.namedModule(ao.name)
+
+ elif c in [Class, Function] or issubclass(c, type):
+ return reflect.namedObject(ao.name)
+
+ elif c is InstanceMethod:
+ im_name = ao.name
+ im_class = reflect.namedObject(ao.klass)
+ im_self = self.unjellyAO(ao.instance)
+ if im_name in im_class.__dict__:
+ if im_self is None:
+ return getattr(im_class, im_name)
+ elif isinstance(im_self, crefutil.NotKnown):
+ return crefutil._InstanceMethod(im_name, im_self, im_class)
+ else:
+ return types.MethodType(im_class.__dict__[im_name],
+ im_self,
+ im_class)
+ else:
+ raise TypeError("instance method changed")
+
+ elif c is Instance:
+ klass = reflect.namedObject(ao.klass)
+ state = self.unjellyAO(ao.state)
+ if hasattr(klass, "__setstate__"):
+ inst = types.InstanceType(klass, {})
+ self.callAfter(inst.__setstate__, state)
+ else:
+ inst = types.InstanceType(klass, state)
+ return inst
+
+ elif c is Ref:
+ o = self.unjellyAO(ao.obj) #THIS IS CHANGING THE REF OMG
+ refkey = ao.refnum
+ ref = self.references.get(refkey)
+ if ref is None:
+ self.references[refkey] = o
+ elif isinstance(ref, crefutil.NotKnown):
+ ref.resolveDependants(o)
+ self.references[refkey] = o
+ elif refkey is None:
+ # This happens when you're unjellying from an AOT not read from source
+ pass
+ else:
+ raise ValueError("Multiple references with the same ID: %s, %s, %s!" % (ref, refkey, ao))
+ return o
+
+ elif c is Deref:
+ num = ao.refnum
+ ref = self.references.get(num)
+ if ref is None:
+ der = crefutil._Dereference(num)
+ self.references[num] = der
+ return der
+ return ref
+
+ elif c is Copyreg:
+ loadfunc = reflect.namedObject(ao.loadfunc)
+ d = self.unjellyLater(ao.state).addCallback(
+ lambda result, _l: apply(_l, result), loadfunc)
+ return d
+
+ #Types
+
+ elif t in _SIMPLE_BUILTINS:
+ return ao
+
+ elif t is types.ListType:
+ l = []
+ for x in ao:
+ l.append(None)
+ self.unjellyInto(l, len(l)-1, x)
+ return l
+
+ elif t is types.TupleType:
+ l = []
+ tuple_ = tuple
+ for x in ao:
+ l.append(None)
+ if isinstance(self.unjellyInto(l, len(l)-1, x), crefutil.NotKnown):
+ tuple_ = crefutil._Tuple
+ return tuple_(l)
+
+ elif t is types.DictType:
+ d = {}
+ for k,v in ao.items():
+ kvd = crefutil._DictKeyAndValue(d)
+ self.unjellyInto(kvd, 0, k)
+ self.unjellyInto(kvd, 1, v)
+ return d
+
+ else:
+ raise TypeError("Unsupported AOT type: %s" % t)
+
+ del self.stack[-1]
+
+
+ def unjelly(self, ao):
+ try:
+ l = [None]
+ self.unjellyInto(l, 0, ao)
+ for func, v in self.afterUnjelly:
+ func(v[0])
+ return l[0]
+ except:
+ log.msg("Error jellying object! Stacktrace follows::")
+ log.msg("\n".join(map(repr, self.stack)))
+ raise
+#########
+# Jelly #
+#########
+
+
+def jellyToAOT(obj):
+ """Convert an object to an Abstract Object Tree."""
+ return AOTJellier().jelly(obj)
+
+def jellyToSource(obj, file=None):
+ """
+ Pass me an object and, optionally, a file object.
+ I'll convert the object to an AOT either return it (if no file was
+ specified) or write it to the file.
+ """
+
+ aot = jellyToAOT(obj)
+ if file:
+ file.write(getSource(aot))
+ else:
+ return getSource(aot)
+
+
+class AOTJellier:
+ def __init__(self):
+ # dict of {id(obj): (obj, node)}
+ self.prepared = {}
+ self._ref_id = 0
+ self.stack = []
+
+ def prepareForRef(self, aoref, object):
+ """I prepare an object for later referencing, by storing its id()
+ and its _AORef in a cache."""
+ self.prepared[id(object)] = aoref
+
+ def jellyToAO(self, obj):
+ """I turn an object into an AOT and return it."""
+ objType = type(obj)
+ self.stack.append(repr(obj))
+
+ #immutable: We don't care if these have multiple refs!
+ if objType in _SIMPLE_BUILTINS:
+ retval = obj
+
+ elif objType is types.MethodType:
+ # TODO: make methods 'prefer' not to jelly the object internally,
+ # so that the object will show up where it's referenced first NOT
+ # by a method.
+ retval = InstanceMethod(obj.im_func.__name__, reflect.qual(obj.im_class),
+ self.jellyToAO(obj.im_self))
+
+ elif objType is types.ModuleType:
+ retval = Module(obj.__name__)
+
+ elif objType is types.ClassType:
+ retval = Class(reflect.qual(obj))
+
+ elif issubclass(objType, type):
+ retval = Class(reflect.qual(obj))
+
+ elif objType is types.FunctionType:
+ retval = Function(reflect.fullFuncName(obj))
+
+ else: #mutable! gotta watch for refs.
+
+#Marmalade had the nicety of being able to just stick a 'reference' attribute
+#on any Node object that was referenced, but in AOT, the referenced object
+#is *inside* of a Ref call (Ref(num, obj) instead of
+#<objtype ... reference="1">). The problem is, especially for built-in types,
+#I can't just assign some attribute to them to give them a refnum. So, I have
+#to "wrap" a Ref(..) around them later -- that's why I put *everything* that's
+#mutable inside one. The Ref() class will only print the "Ref(..)" around an
+#object if it has a Reference explicitly attached.
+
+ if self.prepared.has_key(id(obj)):
+ oldRef = self.prepared[id(obj)]
+ if oldRef.refnum:
+ # it's been referenced already
+ key = oldRef.refnum
+ else:
+ # it hasn't been referenced yet
+ self._ref_id = self._ref_id + 1
+ key = self._ref_id
+ oldRef.setRef(key)
+ return Deref(key)
+
+ retval = Ref()
+ self.prepareForRef(retval, obj)
+
+ if objType is types.ListType:
+ retval.setObj(map(self.jellyToAO, obj)) #hah!
+
+ elif objType is types.TupleType:
+ retval.setObj(tuple(map(self.jellyToAO, obj)))
+
+ elif objType is types.DictionaryType:
+ d = {}
+ for k,v in obj.items():
+ d[self.jellyToAO(k)] = self.jellyToAO(v)
+ retval.setObj(d)
+
+ elif objType is types.InstanceType:
+ if hasattr(obj, "__getstate__"):
+ state = self.jellyToAO(obj.__getstate__())
+ else:
+ state = self.jellyToAO(obj.__dict__)
+ retval.setObj(Instance(reflect.qual(obj.__class__), state))
+
+ elif copy_reg.dispatch_table.has_key(objType):
+ unpickleFunc, state = copy_reg.dispatch_table[objType](obj)
+
+ retval.setObj(Copyreg( reflect.fullFuncName(unpickleFunc),
+ self.jellyToAO(state)))
+
+ else:
+ raise TypeError("Unsupported type: %s" % objType.__name__)
+
+ del self.stack[-1]
+ return retval
+
+ def jelly(self, obj):
+ try:
+ ao = self.jellyToAO(obj)
+ return ao
+ except:
+ log.msg("Error jellying object! Stacktrace follows::")
+ log.msg('\n'.join(self.stack))
+ raise
diff --git a/twisted/persisted/crefutil.py b/twisted/persisted/crefutil.py
new file mode 100644
index 0000000..39d7eb9
--- /dev/null
+++ b/twisted/persisted/crefutil.py
@@ -0,0 +1,163 @@
+# -*- test-case-name: twisted.test.test_persisted -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Utility classes for dealing with circular references.
+"""
+
+import types
+
+from twisted.python import log, reflect
+
+
+class NotKnown:
+ def __init__(self):
+ self.dependants = []
+ self.resolved = 0
+
+ def addDependant(self, mutableObject, key):
+ assert not self.resolved
+ self.dependants.append( (mutableObject, key) )
+
+ resolvedObject = None
+
+ def resolveDependants(self, newObject):
+ self.resolved = 1
+ self.resolvedObject = newObject
+ for mut, key in self.dependants:
+ mut[key] = newObject
+ if isinstance(newObject, NotKnown):
+ newObject.addDependant(mut, key)
+
+ def __hash__(self):
+ assert 0, "I am not to be used as a dictionary key."
+
+
+
+class _Container(NotKnown):
+ """
+ Helper class to resolve circular references on container objects.
+ """
+
+ def __init__(self, l, containerType):
+ """
+ @param l: The list of object which may contain some not yet referenced
+ objects.
+
+ @param containerType: A type of container objects (e.g., C{tuple} or
+ C{set}).
+ """
+ NotKnown.__init__(self)
+ self.containerType = containerType
+ self.l = l
+ self.locs = range(len(l))
+ for idx in xrange(len(l)):
+ if not isinstance(l[idx], NotKnown):
+ self.locs.remove(idx)
+ else:
+ l[idx].addDependant(self, idx)
+ if not self.locs:
+ self.resolveDependants(self.containerType(self.l))
+
+
+ def __setitem__(self, n, obj):
+ """
+ Change the value of one contained objects, and resolve references if
+ all objects have been referenced.
+ """
+ self.l[n] = obj
+ if not isinstance(obj, NotKnown):
+ self.locs.remove(n)
+ if not self.locs:
+ self.resolveDependants(self.containerType(self.l))
+
+
+
+class _Tuple(_Container):
+ """
+ Manage tuple containing circular references. Deprecated: use C{_Container}
+ instead.
+ """
+
+ def __init__(self, l):
+ """
+ @param l: The list of object which may contain some not yet referenced
+ objects.
+ """
+ _Container.__init__(self, l, tuple)
+
+
+
+class _InstanceMethod(NotKnown):
+ def __init__(self, im_name, im_self, im_class):
+ NotKnown.__init__(self)
+ self.my_class = im_class
+ self.name = im_name
+ # im_self _must_ be a
+ im_self.addDependant(self, 0)
+
+ def __call__(self, *args, **kw):
+ import traceback
+ log.msg('instance method %s.%s' % (reflect.qual(self.my_class), self.name))
+ log.msg('being called with %r %r' % (args, kw))
+ traceback.print_stack(file=log.logfile)
+ assert 0
+
+ def __setitem__(self, n, obj):
+ assert n == 0, "only zero index allowed"
+ if not isinstance(obj, NotKnown):
+ method = types.MethodType(self.my_class.__dict__[self.name],
+ obj, self.my_class)
+ self.resolveDependants(method)
+
+class _DictKeyAndValue:
+ def __init__(self, dict):
+ self.dict = dict
+ def __setitem__(self, n, obj):
+ if n not in (1, 0):
+ raise RuntimeError("DictKeyAndValue should only ever be called with 0 or 1")
+ if n: # value
+ self.value = obj
+ else:
+ self.key = obj
+ if hasattr(self, "key") and hasattr(self, "value"):
+ self.dict[self.key] = self.value
+
+
+class _Dereference(NotKnown):
+ def __init__(self, id):
+ NotKnown.__init__(self)
+ self.id = id
+
+
+from twisted.internet.defer import Deferred
+
+class _Catcher:
+ def catch(self, value):
+ self.value = value
+
+class _Defer(Deferred, NotKnown):
+ def __init__(self):
+ Deferred.__init__(self)
+ NotKnown.__init__(self)
+ self.pause()
+
+ wasset = 0
+
+ def __setitem__(self, n, obj):
+ if self.wasset:
+ raise RuntimeError('setitem should only be called once, setting %r to %r' % (n, obj))
+ else:
+ self.wasset = 1
+ self.callback(obj)
+
+ def addDependant(self, dep, key):
+ # by the time I'm adding a dependant, I'm *not* adding any more
+ # callbacks
+ NotKnown.addDependant(self, dep, key)
+ self.unpause()
+ resovd = self.result
+ self.resolveDependants(resovd)
diff --git a/twisted/persisted/dirdbm.py b/twisted/persisted/dirdbm.py
new file mode 100644
index 0000000..26bbc1b
--- /dev/null
+++ b/twisted/persisted/dirdbm.py
@@ -0,0 +1,358 @@
+# -*- test-case-name: twisted.test.test_dirdbm -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+"""
+DBM-style interface to a directory.
+
+Each key is stored as a single file. This is not expected to be very fast or
+efficient, but it's good for easy debugging.
+
+DirDBMs are *not* thread-safe, they should only be accessed by one thread at
+a time.
+
+No files should be placed in the working directory of a DirDBM save those
+created by the DirDBM itself!
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+
+import os
+import types
+import base64
+import glob
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+try:
+ _open
+except NameError:
+ _open = open
+
+
+class DirDBM:
+ """A directory with a DBM interface.
+
+ This class presents a hash-like interface to a directory of small,
+ flat files. It can only use strings as keys or values.
+ """
+
+ def __init__(self, name):
+ """
+ @type name: str
+ @param name: Base path to use for the directory storage.
+ """
+ self.dname = os.path.abspath(name)
+ if not os.path.isdir(self.dname):
+ os.mkdir(self.dname)
+ else:
+ # Run recovery, in case we crashed. we delete all files ending
+ # with ".new". Then we find all files who end with ".rpl". If a
+ # corresponding file exists without ".rpl", we assume the write
+ # failed and delete the ".rpl" file. If only a ".rpl" exist we
+ # assume the program crashed right after deleting the old entry
+ # but before renaming the replacement entry.
+ #
+ # NOTE: '.' is NOT in the base64 alphabet!
+ for f in glob.glob(os.path.join(self.dname, "*.new")):
+ os.remove(f)
+ replacements = glob.glob(os.path.join(self.dname, "*.rpl"))
+ for f in replacements:
+ old = f[:-4]
+ if os.path.exists(old):
+ os.remove(f)
+ else:
+ os.rename(f, old)
+
+ def _encode(self, k):
+ """Encode a key so it can be used as a filename.
+ """
+ # NOTE: '_' is NOT in the base64 alphabet!
+ return base64.encodestring(k).replace('\n', '_').replace("/", "-")
+
+ def _decode(self, k):
+ """Decode a filename to get the key.
+ """
+ return base64.decodestring(k.replace('_', '\n').replace("-", "/"))
+
+ def _readFile(self, path):
+ """Read in the contents of a file.
+
+ Override in subclasses to e.g. provide transparently encrypted dirdbm.
+ """
+ f = _open(path, "rb")
+ s = f.read()
+ f.close()
+ return s
+
+ def _writeFile(self, path, data):
+ """Write data to a file.
+
+ Override in subclasses to e.g. provide transparently encrypted dirdbm.
+ """
+ f = _open(path, "wb")
+ f.write(data)
+ f.flush()
+ f.close()
+
+ def __len__(self):
+ """
+ @return: The number of key/value pairs in this Shelf
+ """
+ return len(os.listdir(self.dname))
+
+ def __setitem__(self, k, v):
+ """
+ C{dirdbm[k] = v}
+ Create or modify a textfile in this directory
+
+ @type k: str
+ @param k: key to set
+
+ @type v: str
+ @param v: value to associate with C{k}
+ """
+ assert type(k) == types.StringType, "DirDBM key must be a string"
+ assert type(v) == types.StringType, "DirDBM value must be a string"
+ k = self._encode(k)
+
+ # we create a new file with extension .new, write the data to it, and
+ # if the write succeeds delete the old file and rename the new one.
+ old = os.path.join(self.dname, k)
+ if os.path.exists(old):
+ new = old + ".rpl" # replacement entry
+ else:
+ new = old + ".new" # new entry
+ try:
+ self._writeFile(new, v)
+ except:
+ os.remove(new)
+ raise
+ else:
+ if os.path.exists(old): os.remove(old)
+ os.rename(new, old)
+
+ def __getitem__(self, k):
+ """
+ C{dirdbm[k]}
+ Get the contents of a file in this directory as a string.
+
+ @type k: str
+ @param k: key to lookup
+
+ @return: The value associated with C{k}
+ @raise KeyError: Raised when there is no such key
+ """
+ assert type(k) == types.StringType, "DirDBM key must be a string"
+ path = os.path.join(self.dname, self._encode(k))
+ try:
+ return self._readFile(path)
+ except:
+ raise KeyError, k
+
+ def __delitem__(self, k):
+ """
+ C{del dirdbm[foo]}
+ Delete a file in this directory.
+
+ @type k: str
+ @param k: key to delete
+
+ @raise KeyError: Raised when there is no such key
+ """
+ assert type(k) == types.StringType, "DirDBM key must be a string"
+ k = self._encode(k)
+ try: os.remove(os.path.join(self.dname, k))
+ except (OSError, IOError): raise KeyError(self._decode(k))
+
+ def keys(self):
+ """
+ @return: a C{list} of filenames (keys).
+ """
+ return map(self._decode, os.listdir(self.dname))
+
+ def values(self):
+ """
+ @return: a C{list} of file-contents (values).
+ """
+ vals = []
+ keys = self.keys()
+ for key in keys:
+ vals.append(self[key])
+ return vals
+
+ def items(self):
+ """
+ @return: a C{list} of 2-tuples containing key/value pairs.
+ """
+ items = []
+ keys = self.keys()
+ for key in keys:
+ items.append((key, self[key]))
+ return items
+
+ def has_key(self, key):
+ """
+ @type key: str
+ @param key: The key to test
+
+ @return: A true value if this dirdbm has the specified key, a faluse
+ value otherwise.
+ """
+ assert type(key) == types.StringType, "DirDBM key must be a string"
+ key = self._encode(key)
+ return os.path.isfile(os.path.join(self.dname, key))
+
+ def setdefault(self, key, value):
+ """
+ @type key: str
+ @param key: The key to lookup
+
+ @param value: The value to associate with key if key is not already
+ associated with a value.
+ """
+ if not self.has_key(key):
+ self[key] = value
+ return value
+ return self[key]
+
+ def get(self, key, default = None):
+ """
+ @type key: str
+ @param key: The key to lookup
+
+ @param default: The value to return if the given key does not exist
+
+ @return: The value associated with C{key} or C{default} if not
+ C{self.has_key(key)}
+ """
+ if self.has_key(key):
+ return self[key]
+ else:
+ return default
+
+ def __contains__(self, key):
+ """
+ C{key in dirdbm}
+
+ @type key: str
+ @param key: The key to test
+
+ @return: A true value if C{self.has_key(key)}, a false value otherwise.
+ """
+ assert type(key) == types.StringType, "DirDBM key must be a string"
+ key = self._encode(key)
+ return os.path.isfile(os.path.join(self.dname, key))
+
+ def update(self, dict):
+ """
+ Add all the key/value pairs in C{dict} to this dirdbm. Any conflicting
+ keys will be overwritten with the values from C{dict}.
+
+ @type dict: mapping
+ @param dict: A mapping of key/value pairs to add to this dirdbm.
+ """
+ for key, val in dict.items():
+ self[key]=val
+
+ def copyTo(self, path):
+ """
+ Copy the contents of this dirdbm to the dirdbm at C{path}.
+
+ @type path: C{str}
+ @param path: The path of the dirdbm to copy to. If a dirdbm
+ exists at the destination path, it is cleared first.
+
+ @rtype: C{DirDBM}
+ @return: The dirdbm this dirdbm was copied to.
+ """
+ path = os.path.abspath(path)
+ assert path != self.dname
+
+ d = self.__class__(path)
+ d.clear()
+ for k in self.keys():
+ d[k] = self[k]
+ return d
+
+ def clear(self):
+ """
+ Delete all key/value pairs in this dirdbm.
+ """
+ for k in self.keys():
+ del self[k]
+
+ def close(self):
+ """
+ Close this dbm: no-op, for dbm-style interface compliance.
+ """
+
+ def getModificationTime(self, key):
+ """
+ Returns modification time of an entry.
+
+ @return: Last modification date (seconds since epoch) of entry C{key}
+ @raise KeyError: Raised when there is no such key
+ """
+ assert type(key) == types.StringType, "DirDBM key must be a string"
+ path = os.path.join(self.dname, self._encode(key))
+ if os.path.isfile(path):
+ return os.path.getmtime(path)
+ else:
+ raise KeyError, key
+
+
+class Shelf(DirDBM):
+ """A directory with a DBM shelf interface.
+
+ This class presents a hash-like interface to a directory of small,
+ flat files. Keys must be strings, but values can be any given object.
+ """
+
+ def __setitem__(self, k, v):
+ """
+ C{shelf[foo] = bar}
+ Create or modify a textfile in this directory.
+
+ @type k: str
+ @param k: The key to set
+
+ @param v: The value to associate with C{key}
+ """
+ v = pickle.dumps(v)
+ DirDBM.__setitem__(self, k, v)
+
+ def __getitem__(self, k):
+ """
+ C{dirdbm[foo]}
+ Get and unpickle the contents of a file in this directory.
+
+ @type k: str
+ @param k: The key to lookup
+
+ @return: The value associated with the given key
+ @raise KeyError: Raised if the given key does not exist
+ """
+ return pickle.loads(DirDBM.__getitem__(self, k))
+
+
+def open(file, flag = None, mode = None):
+ """
+ This is for 'anydbm' compatibility.
+
+ @param file: The parameter to pass to the DirDBM constructor.
+
+ @param flag: ignored
+ @param mode: ignored
+ """
+ return DirDBM(file)
+
+
+__all__ = ["open", "DirDBM", "Shelf"]
diff --git a/twisted/persisted/sob.py b/twisted/persisted/sob.py
new file mode 100644
index 0000000..2ba2e49
--- /dev/null
+++ b/twisted/persisted/sob.py
@@ -0,0 +1,227 @@
+# -*- test-case-name: twisted.test.test_sob -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""
+Save and load Small OBjects to and from files, using various formats.
+
+Maintainer: Moshe Zadka
+"""
+
+import os, sys
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+from twisted.python import log, runtime
+from twisted.python.hashlib import md5
+from twisted.persisted import styles
+from zope.interface import implements, Interface
+
+# Note:
+# These encrypt/decrypt functions only work for data formats
+# which are immune to having spaces tucked at the end.
+# All data formats which persist saves hold that condition.
+def _encrypt(passphrase, data):
+ from Crypto.Cipher import AES as cipher
+ leftover = len(data) % cipher.block_size
+ if leftover:
+ data += ' '*(cipher.block_size - leftover)
+ return cipher.new(md5(passphrase).digest()[:16]).encrypt(data)
+
+def _decrypt(passphrase, data):
+ from Crypto.Cipher import AES
+ return AES.new(md5(passphrase).digest()[:16]).decrypt(data)
+
+
+class IPersistable(Interface):
+
+ """An object which can be saved in several formats to a file"""
+
+ def setStyle(style):
+ """Set desired format.
+
+ @type style: string (one of 'pickle' or 'source')
+ """
+
+ def save(tag=None, filename=None, passphrase=None):
+ """Save object to file.
+
+ @type tag: string
+ @type filename: string
+ @type passphrase: string
+ """
+
+
+class Persistent:
+
+ implements(IPersistable)
+
+ style = "pickle"
+
+ def __init__(self, original, name):
+ self.original = original
+ self.name = name
+
+ def setStyle(self, style):
+ """Set desired format.
+
+ @type style: string (one of 'pickle' or 'source')
+ """
+ self.style = style
+
+ def _getFilename(self, filename, ext, tag):
+ if filename:
+ finalname = filename
+ filename = finalname + "-2"
+ elif tag:
+ filename = "%s-%s-2.%s" % (self.name, tag, ext)
+ finalname = "%s-%s.%s" % (self.name, tag, ext)
+ else:
+ filename = "%s-2.%s" % (self.name, ext)
+ finalname = "%s.%s" % (self.name, ext)
+ return finalname, filename
+
+ def _saveTemp(self, filename, passphrase, dumpFunc):
+ f = open(filename, 'wb')
+ if passphrase is None:
+ dumpFunc(self.original, f)
+ else:
+ s = StringIO.StringIO()
+ dumpFunc(self.original, s)
+ f.write(_encrypt(passphrase, s.getvalue()))
+ f.close()
+
+ def _getStyle(self):
+ if self.style == "source":
+ from twisted.persisted.aot import jellyToSource as dumpFunc
+ ext = "tas"
+ else:
+ def dumpFunc(obj, file):
+ pickle.dump(obj, file, 2)
+ ext = "tap"
+ return ext, dumpFunc
+
+ def save(self, tag=None, filename=None, passphrase=None):
+ """Save object to file.
+
+ @type tag: string
+ @type filename: string
+ @type passphrase: string
+ """
+ ext, dumpFunc = self._getStyle()
+ if passphrase:
+ ext = 'e' + ext
+ finalname, filename = self._getFilename(filename, ext, tag)
+ log.msg("Saving "+self.name+" application to "+finalname+"...")
+ self._saveTemp(filename, passphrase, dumpFunc)
+ if runtime.platformType == "win32" and os.path.isfile(finalname):
+ os.remove(finalname)
+ os.rename(filename, finalname)
+ log.msg("Saved.")
+
+# "Persistant" has been present since 1.0.7, so retain it for compatibility
+Persistant = Persistent
+
+class _EverythingEphemeral(styles.Ephemeral):
+
+ initRun = 0
+
+ def __init__(self, mainMod):
+ """
+ @param mainMod: The '__main__' module that this class will proxy.
+ """
+ self.mainMod = mainMod
+
+ def __getattr__(self, key):
+ try:
+ return getattr(self.mainMod, key)
+ except AttributeError:
+ if self.initRun:
+ raise
+ else:
+ log.msg("Warning! Loading from __main__: %s" % key)
+ return styles.Ephemeral()
+
+
+def load(filename, style, passphrase=None):
+ """Load an object from a file.
+
+ Deserialize an object from a file. The file can be encrypted.
+
+ @param filename: string
+ @param style: string (one of 'pickle' or 'source')
+ @param passphrase: string
+ """
+ mode = 'r'
+ if style=='source':
+ from twisted.persisted.aot import unjellyFromSource as _load
+ else:
+ _load, mode = pickle.load, 'rb'
+ if passphrase:
+ fp = StringIO.StringIO(_decrypt(passphrase,
+ open(filename, 'rb').read()))
+ else:
+ fp = open(filename, mode)
+ ee = _EverythingEphemeral(sys.modules['__main__'])
+ sys.modules['__main__'] = ee
+ ee.initRun = 1
+ try:
+ value = _load(fp)
+ finally:
+ # restore __main__ if an exception is raised.
+ sys.modules['__main__'] = ee.mainMod
+
+ styles.doUpgrade()
+ ee.initRun = 0
+ persistable = IPersistable(value, None)
+ if persistable is not None:
+ persistable.setStyle(style)
+ return value
+
+
+def loadValueFromFile(filename, variable, passphrase=None):
+ """Load the value of a variable in a Python file.
+
+ Run the contents of the file, after decrypting if C{passphrase} is
+ given, in a namespace and return the result of the variable
+ named C{variable}.
+
+ @param filename: string
+ @param variable: string
+ @param passphrase: string
+ """
+ if passphrase:
+ mode = 'rb'
+ else:
+ mode = 'r'
+ fileObj = open(filename, mode)
+ d = {'__file__': filename}
+ if passphrase:
+ data = fileObj.read()
+ data = _decrypt(passphrase, data)
+ exec data in d, d
+ else:
+ exec fileObj in d, d
+ value = d[variable]
+ return value
+
+def guessType(filename):
+ ext = os.path.splitext(filename)[1]
+ return {
+ '.tac': 'python',
+ '.etac': 'python',
+ '.py': 'python',
+ '.tap': 'pickle',
+ '.etap': 'pickle',
+ '.tas': 'source',
+ '.etas': 'source',
+ }[ext]
+
+__all__ = ['loadValueFromFile', 'load', 'Persistent', 'Persistant',
+ 'IPersistable', 'guessType']
diff --git a/twisted/persisted/styles.py b/twisted/persisted/styles.py
new file mode 100644
index 0000000..81c8c30
--- /dev/null
+++ b/twisted/persisted/styles.py
@@ -0,0 +1,262 @@
+# -*- test-case-name: twisted.test.test_persisted -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+"""
+Different styles of persisted objects.
+"""
+
+# System Imports
+import types
+import copy_reg
+import copy
+import inspect
+import sys
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+# Twisted Imports
+from twisted.python import log
+from twisted.python import reflect
+
+oldModules = {}
+
+## First, let's register support for some stuff that really ought to
+## be registerable...
+
+def pickleMethod(method):
+ 'support function for copy_reg to pickle method refs'
+ return unpickleMethod, (method.im_func.__name__,
+ method.im_self,
+ method.im_class)
+
+def unpickleMethod(im_name,
+ im_self,
+ im_class):
+ 'support function for copy_reg to unpickle method refs'
+ try:
+ unbound = getattr(im_class,im_name)
+ if im_self is None:
+ return unbound
+ bound = types.MethodType(unbound.im_func, im_self, im_class)
+ return bound
+ except AttributeError:
+ log.msg("Method",im_name,"not on class",im_class)
+ assert im_self is not None,"No recourse: no instance to guess from."
+ # Attempt a common fix before bailing -- if classes have
+ # changed around since we pickled this method, we may still be
+ # able to get it by looking on the instance's current class.
+ unbound = getattr(im_self.__class__,im_name)
+ log.msg("Attempting fixup with",unbound)
+ if im_self is None:
+ return unbound
+ bound = types.MethodType(unbound.im_func, im_self, im_self.__class__)
+ return bound
+
+copy_reg.pickle(types.MethodType,
+ pickleMethod,
+ unpickleMethod)
+
+def pickleModule(module):
+ 'support function for copy_reg to pickle module refs'
+ return unpickleModule, (module.__name__,)
+
+def unpickleModule(name):
+ 'support function for copy_reg to unpickle module refs'
+ if oldModules.has_key(name):
+ log.msg("Module has moved: %s" % name)
+ name = oldModules[name]
+ log.msg(name)
+ return __import__(name,{},{},'x')
+
+
+copy_reg.pickle(types.ModuleType,
+ pickleModule,
+ unpickleModule)
+
+def pickleStringO(stringo):
+ 'support function for copy_reg to pickle StringIO.OutputTypes'
+ return unpickleStringO, (stringo.getvalue(), stringo.tell())
+
+def unpickleStringO(val, sek):
+ x = StringIO.StringIO()
+ x.write(val)
+ x.seek(sek)
+ return x
+
+if hasattr(StringIO, 'OutputType'):
+ copy_reg.pickle(StringIO.OutputType,
+ pickleStringO,
+ unpickleStringO)
+
+def pickleStringI(stringi):
+ return unpickleStringI, (stringi.getvalue(), stringi.tell())
+
+def unpickleStringI(val, sek):
+ x = StringIO.StringIO(val)
+ x.seek(sek)
+ return x
+
+
+if hasattr(StringIO, 'InputType'):
+ copy_reg.pickle(StringIO.InputType,
+ pickleStringI,
+ unpickleStringI)
+
+class Ephemeral:
+ """
+ This type of object is never persisted; if possible, even references to it
+ are eliminated.
+ """
+
+ def __getstate__(self):
+ log.msg( "WARNING: serializing ephemeral %s" % self )
+ import gc
+ if '__pypy__' not in sys.builtin_module_names:
+ if getattr(gc, 'get_referrers', None):
+ for r in gc.get_referrers(self):
+ log.msg( " referred to by %s" % (r,))
+ return None
+
+ def __setstate__(self, state):
+ log.msg( "WARNING: unserializing ephemeral %s" % self.__class__ )
+ self.__class__ = Ephemeral
+
+
+versionedsToUpgrade = {}
+upgraded = {}
+
+def doUpgrade():
+ global versionedsToUpgrade, upgraded
+ for versioned in versionedsToUpgrade.values():
+ requireUpgrade(versioned)
+ versionedsToUpgrade = {}
+ upgraded = {}
+
+def requireUpgrade(obj):
+ """Require that a Versioned instance be upgraded completely first.
+ """
+ objID = id(obj)
+ if objID in versionedsToUpgrade and objID not in upgraded:
+ upgraded[objID] = 1
+ obj.versionUpgrade()
+ return obj
+
+def _aybabtu(c):
+ """
+ Get all of the parent classes of C{c}, not including C{c} itself, which are
+ strict subclasses of L{Versioned}.
+
+ The name comes from "all your base are belong to us", from the deprecated
+ L{twisted.python.reflect.allYourBase} function.
+
+ @param c: a class
+ @returns: list of classes
+ """
+ # begin with two classes that should *not* be included in the
+ # final result
+ l = [c, Versioned]
+ for b in inspect.getmro(c):
+ if b not in l and issubclass(b, Versioned):
+ l.append(b)
+ # return all except the unwanted classes
+ return l[2:]
+
+class Versioned:
+ """
+ This type of object is persisted with versioning information.
+
+ I have a single class attribute, the int persistenceVersion. After I am
+ unserialized (and styles.doUpgrade() is called), self.upgradeToVersionX()
+ will be called for each version upgrade I must undergo.
+
+ For example, if I serialize an instance of a Foo(Versioned) at version 4
+ and then unserialize it when the code is at version 9, the calls::
+
+ self.upgradeToVersion5()
+ self.upgradeToVersion6()
+ self.upgradeToVersion7()
+ self.upgradeToVersion8()
+ self.upgradeToVersion9()
+
+ will be made. If any of these methods are undefined, a warning message
+ will be printed.
+ """
+ persistenceVersion = 0
+ persistenceForgets = ()
+
+ def __setstate__(self, state):
+ versionedsToUpgrade[id(self)] = self
+ self.__dict__ = state
+
+ def __getstate__(self, dict=None):
+ """Get state, adding a version number to it on its way out.
+ """
+ dct = copy.copy(dict or self.__dict__)
+ bases = _aybabtu(self.__class__)
+ bases.reverse()
+ bases.append(self.__class__) # don't forget me!!
+ for base in bases:
+ if base.__dict__.has_key('persistenceForgets'):
+ for slot in base.persistenceForgets:
+ if dct.has_key(slot):
+ del dct[slot]
+ if base.__dict__.has_key('persistenceVersion'):
+ dct['%s.persistenceVersion' % reflect.qual(base)] = base.persistenceVersion
+ return dct
+
+ def versionUpgrade(self):
+ """(internal) Do a version upgrade.
+ """
+ bases = _aybabtu(self.__class__)
+ # put the bases in order so superclasses' persistenceVersion methods
+ # will be called first.
+ bases.reverse()
+ bases.append(self.__class__) # don't forget me!!
+ # first let's look for old-skool versioned's
+ if self.__dict__.has_key("persistenceVersion"):
+
+ # Hacky heuristic: if more than one class subclasses Versioned,
+ # we'll assume that the higher version number wins for the older
+ # class, so we'll consider the attribute the version of the older
+ # class. There are obviously possibly times when this will
+ # eventually be an incorrect assumption, but hopefully old-school
+ # persistenceVersion stuff won't make it that far into multiple
+ # classes inheriting from Versioned.
+
+ pver = self.__dict__['persistenceVersion']
+ del self.__dict__['persistenceVersion']
+ highestVersion = 0
+ highestBase = None
+ for base in bases:
+ if not base.__dict__.has_key('persistenceVersion'):
+ continue
+ if base.persistenceVersion > highestVersion:
+ highestBase = base
+ highestVersion = base.persistenceVersion
+ if highestBase:
+ self.__dict__['%s.persistenceVersion' % reflect.qual(highestBase)] = pver
+ for base in bases:
+ # ugly hack, but it's what the user expects, really
+ if (Versioned not in base.__bases__ and
+ not base.__dict__.has_key('persistenceVersion')):
+ continue
+ currentVers = base.persistenceVersion
+ pverName = '%s.persistenceVersion' % reflect.qual(base)
+ persistVers = (self.__dict__.get(pverName) or 0)
+ if persistVers:
+ del self.__dict__[pverName]
+ assert persistVers <= currentVers, "Sorry, can't go backwards in time."
+ while persistVers < currentVers:
+ persistVers = persistVers + 1
+ method = base.__dict__.get('upgradeToVersion%s' % persistVers, None)
+ if method:
+ log.msg( "Upgrading %s (of %s @ %s) to version %s" % (reflect.qual(base), reflect.qual(self.__class__), id(self), persistVers) )
+ method(self)
+ else:
+ log.msg( 'Warning: cannot upgrade %s to version %s' % (base, persistVers) )
diff --git a/twisted/persisted/test/__init__.py b/twisted/persisted/test/__init__.py
new file mode 100644
index 0000000..01ae065
--- /dev/null
+++ b/twisted/persisted/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.persisted}.
+"""
diff --git a/twisted/persisted/test/test_styles.py b/twisted/persisted/test/test_styles.py
new file mode 100644
index 0000000..29647a9
--- /dev/null
+++ b/twisted/persisted/test/test_styles.py
@@ -0,0 +1,55 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.persisted.styles}.
+"""
+
+from twisted.trial import unittest
+from twisted.persisted.styles import unpickleMethod
+
+
+class Foo:
+ """
+ Helper class.
+ """
+ def method(self):
+ """
+ Helper method.
+ """
+
+
+
+class Bar:
+ """
+ Helper class.
+ """
+
+
+
+class UnpickleMethodTestCase(unittest.TestCase):
+ """
+ Tests for the unpickleMethod function.
+ """
+
+ def test_instanceBuildingNamePresent(self):
+ """
+ L{unpickleMethod} returns an instance method bound to the
+ instance passed to it.
+ """
+ foo = Foo()
+ m = unpickleMethod('method', foo, Foo)
+ self.assertEqual(m, foo.method)
+ self.assertNotIdentical(m, foo.method)
+
+
+ def test_instanceBuildingNameNotPresent(self):
+ """
+ If the named method is not present in the class,
+ L{unpickleMethod} finds a method on the class of the instance
+ and returns a bound method from there.
+ """
+ foo = Foo()
+ m = unpickleMethod('method', foo, Bar)
+ self.assertEqual(m, foo.method)
+ self.assertNotIdentical(m, foo.method)
diff --git a/twisted/plugin.py b/twisted/plugin.py
new file mode 100644
index 0000000..a4f8334
--- /dev/null
+++ b/twisted/plugin.py
@@ -0,0 +1,255 @@
+# -*- test-case-name: twisted.test.test_plugin -*-
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Plugin system for Twisted.
+
+@author: Jp Calderone
+@author: Glyph Lefkowitz
+"""
+
+import os
+import sys
+
+from zope.interface import Interface, providedBy
+
+def _determinePickleModule():
+ """
+ Determine which 'pickle' API module to use.
+ """
+ try:
+ import cPickle
+ return cPickle
+ except ImportError:
+ import pickle
+ return pickle
+
+pickle = _determinePickleModule()
+
+from twisted.python.components import getAdapterFactory
+from twisted.python.reflect import namedAny
+from twisted.python import log
+from twisted.python.modules import getModule
+
+
+
+class IPlugin(Interface):
+ """
+ Interface that must be implemented by all plugins.
+
+ Only objects which implement this interface will be considered for return
+ by C{getPlugins}. To be useful, plugins should also implement some other
+ application-specific interface.
+ """
+
+
+
+class CachedPlugin(object):
+ def __init__(self, dropin, name, description, provided):
+ self.dropin = dropin
+ self.name = name
+ self.description = description
+ self.provided = provided
+ self.dropin.plugins.append(self)
+
+ def __repr__(self):
+ return '<CachedPlugin %r/%r (provides %r)>' % (
+ self.name, self.dropin.moduleName,
+ ', '.join([i.__name__ for i in self.provided]))
+
+ def load(self):
+ return namedAny(self.dropin.moduleName + '.' + self.name)
+
+ def __conform__(self, interface, registry=None, default=None):
+ for providedInterface in self.provided:
+ if providedInterface.isOrExtends(interface):
+ return self.load()
+ if getAdapterFactory(providedInterface, interface, None) is not None:
+ return interface(self.load(), default)
+ return default
+
+ # backwards compat HOORJ
+ getComponent = __conform__
+
+
+
+class CachedDropin(object):
+ """
+ A collection of L{CachedPlugin} instances from a particular module in a
+ plugin package.
+
+ @type moduleName: C{str}
+ @ivar moduleName: The fully qualified name of the plugin module this
+ represents.
+
+ @type description: C{str} or C{NoneType}
+ @ivar description: A brief explanation of this collection of plugins
+ (probably the plugin module's docstring).
+
+ @type plugins: C{list}
+ @ivar plugins: The L{CachedPlugin} instances which were loaded from this
+ dropin.
+ """
+ def __init__(self, moduleName, description):
+ self.moduleName = moduleName
+ self.description = description
+ self.plugins = []
+
+
+
+def _generateCacheEntry(provider):
+ dropin = CachedDropin(provider.__name__,
+ provider.__doc__)
+ for k, v in provider.__dict__.iteritems():
+ plugin = IPlugin(v, None)
+ if plugin is not None:
+ # Instantiated for its side-effects.
+ CachedPlugin(dropin, k, v.__doc__, list(providedBy(plugin)))
+ return dropin
+
+try:
+ fromkeys = dict.fromkeys
+except AttributeError:
+ def fromkeys(keys, value=None):
+ d = {}
+ for k in keys:
+ d[k] = value
+ return d
+
+
+
+def getCache(module):
+ """
+ Compute all the possible loadable plugins, while loading as few as
+ possible and hitting the filesystem as little as possible.
+
+ @param module: a Python module object. This represents a package to search
+ for plugins.
+
+ @return: a dictionary mapping module names to L{CachedDropin} instances.
+ """
+ allCachesCombined = {}
+ mod = getModule(module.__name__)
+ # don't want to walk deep, only immediate children.
+ buckets = {}
+ # Fill buckets with modules by related entry on the given package's
+ # __path__. There's an abstraction inversion going on here, because this
+ # information is already represented internally in twisted.python.modules,
+ # but it's simple enough that I'm willing to live with it. If anyone else
+ # wants to fix up this iteration so that it's one path segment at a time,
+ # be my guest. --glyph
+ for plugmod in mod.iterModules():
+ fpp = plugmod.filePath.parent()
+ if fpp not in buckets:
+ buckets[fpp] = []
+ bucket = buckets[fpp]
+ bucket.append(plugmod)
+ for pseudoPackagePath, bucket in buckets.iteritems():
+ dropinPath = pseudoPackagePath.child('dropin.cache')
+ try:
+ lastCached = dropinPath.getModificationTime()
+ dropinDotCache = pickle.load(dropinPath.open('r'))
+ except:
+ dropinDotCache = {}
+ lastCached = 0
+
+ needsWrite = False
+ existingKeys = {}
+ for pluginModule in bucket:
+ pluginKey = pluginModule.name.split('.')[-1]
+ existingKeys[pluginKey] = True
+ if ((pluginKey not in dropinDotCache) or
+ (pluginModule.filePath.getModificationTime() >= lastCached)):
+ needsWrite = True
+ try:
+ provider = pluginModule.load()
+ except:
+ # dropinDotCache.pop(pluginKey, None)
+ log.err()
+ else:
+ entry = _generateCacheEntry(provider)
+ dropinDotCache[pluginKey] = entry
+ # Make sure that the cache doesn't contain any stale plugins.
+ for pluginKey in dropinDotCache.keys():
+ if pluginKey not in existingKeys:
+ del dropinDotCache[pluginKey]
+ needsWrite = True
+ if needsWrite:
+ try:
+ dropinPath.setContent(pickle.dumps(dropinDotCache))
+ except OSError, e:
+ log.msg(
+ format=(
+ "Unable to write to plugin cache %(path)s: error "
+ "number %(errno)d"),
+ path=dropinPath.path, errno=e.errno)
+ except:
+ log.err(None, "Unexpected error while writing cache file")
+ allCachesCombined.update(dropinDotCache)
+ return allCachesCombined
+
+
+
+def getPlugins(interface, package=None):
+ """
+ Retrieve all plugins implementing the given interface beneath the given module.
+
+ @param interface: An interface class. Only plugins which implement this
+ interface will be returned.
+
+ @param package: A package beneath which plugins are installed. For
+ most uses, the default value is correct.
+
+ @return: An iterator of plugins.
+ """
+ if package is None:
+ import twisted.plugins as package
+ allDropins = getCache(package)
+ for dropin in allDropins.itervalues():
+ for plugin in dropin.plugins:
+ try:
+ adapted = interface(plugin, None)
+ except:
+ log.err()
+ else:
+ if adapted is not None:
+ yield adapted
+
+
+# Old, backwards compatible name. Don't use this.
+getPlugIns = getPlugins
+
+
+def pluginPackagePaths(name):
+ """
+ Return a list of additional directories which should be searched for
+ modules to be included as part of the named plugin package.
+
+ @type name: C{str}
+ @param name: The fully-qualified Python name of a plugin package, eg
+ C{'twisted.plugins'}.
+
+ @rtype: C{list} of C{str}
+ @return: The absolute paths to other directories which may contain plugin
+ modules for the named plugin package.
+ """
+ package = name.split('.')
+ # Note that this may include directories which do not exist. It may be
+ # preferable to remove such directories at this point, rather than allow
+ # them to be searched later on.
+ #
+ # Note as well that only '__init__.py' will be considered to make a
+ # directory a package (and thus exclude it from this list). This means
+ # that if you create a master plugin package which has some other kind of
+ # __init__ (eg, __init__.pyc) it will be incorrectly treated as a
+ # supplementary plugin directory.
+ return [
+ os.path.abspath(os.path.join(x, *package))
+ for x
+ in sys.path
+ if
+ not os.path.exists(os.path.join(x, *package + ['__init__.py']))]
+
+__all__ = ['getPlugins', 'pluginPackagePaths']
diff --git a/twisted/plugins/__init__.py b/twisted/plugins/__init__.py
new file mode 100644
index 0000000..0c11760
--- /dev/null
+++ b/twisted/plugins/__init__.py
@@ -0,0 +1,17 @@
+# -*- test-case-name: twisted.test.test_plugin -*-
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Plugins go in directories on your PYTHONPATH named twisted/plugins:
+this is the only place where an __init__.py is necessary, thanks to
+the __path__ variable.
+
+@author: Jp Calderone
+@author: Glyph Lefkowitz
+"""
+
+from twisted.plugin import pluginPackagePaths
+__path__.extend(pluginPackagePaths(__name__))
+__all__ = [] # nothing to see here, move along, move along
diff --git a/twisted/plugins/cred_anonymous.py b/twisted/plugins/cred_anonymous.py
new file mode 100644
index 0000000..ad0ea9e
--- /dev/null
+++ b/twisted/plugins/cred_anonymous.py
@@ -0,0 +1,40 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cred plugin for anonymous logins.
+"""
+
+from zope.interface import implements
+
+from twisted import plugin
+from twisted.cred.checkers import AllowAnonymousAccess
+from twisted.cred.strcred import ICheckerFactory
+from twisted.cred.credentials import IAnonymous
+
+
+anonymousCheckerFactoryHelp = """
+This allows anonymous authentication for servers that support it.
+"""
+
+
+class AnonymousCheckerFactory(object):
+ """
+ Generates checkers that will authenticate an anonymous request.
+ """
+ implements(ICheckerFactory, plugin.IPlugin)
+ authType = 'anonymous'
+ authHelp = anonymousCheckerFactoryHelp
+ argStringFormat = 'No argstring required.'
+ credentialInterfaces = (IAnonymous,)
+
+
+ def generateChecker(self, argstring=''):
+ return AllowAnonymousAccess()
+
+
+
+theAnonymousCheckerFactory = AnonymousCheckerFactory()
+
diff --git a/twisted/plugins/cred_file.py b/twisted/plugins/cred_file.py
new file mode 100644
index 0000000..3ff9b37
--- /dev/null
+++ b/twisted/plugins/cred_file.py
@@ -0,0 +1,60 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cred plugin for a file of the format 'username:password'.
+"""
+
+import sys
+
+from zope.interface import implements
+
+from twisted import plugin
+from twisted.cred.checkers import FilePasswordDB
+from twisted.cred.strcred import ICheckerFactory
+from twisted.cred.credentials import IUsernamePassword, IUsernameHashedPassword
+
+
+
+fileCheckerFactoryHelp = """
+This checker expects to receive the location of a file that
+conforms to the FilePasswordDB format. Each line in the file
+should be of the format 'username:password', in plain text.
+"""
+
+invalidFileWarning = 'Warning: not a valid file'
+
+
+
+class FileCheckerFactory(object):
+ """
+ A factory for instances of L{FilePasswordDB}.
+ """
+ implements(ICheckerFactory, plugin.IPlugin)
+ authType = 'file'
+ authHelp = fileCheckerFactoryHelp
+ argStringFormat = 'Location of a FilePasswordDB-formatted file.'
+ # Explicitly defined here because FilePasswordDB doesn't do it for us
+ credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword)
+
+ errorOutput = sys.stderr
+
+ def generateChecker(self, argstring):
+ """
+ This checker factory expects to get the location of a file.
+ The file should conform to the format required by
+ L{FilePasswordDB} (using defaults for all
+ initialization parameters).
+ """
+ from twisted.python.filepath import FilePath
+ if not argstring.strip():
+ raise ValueError, '%r requires a filename' % self.authType
+ elif not FilePath(argstring).isfile():
+ self.errorOutput.write('%s: %s\n' % (invalidFileWarning, argstring))
+ return FilePasswordDB(argstring)
+
+
+
+theFileCheckerFactory = FileCheckerFactory()
diff --git a/twisted/plugins/cred_memory.py b/twisted/plugins/cred_memory.py
new file mode 100644
index 0000000..0ed9083
--- /dev/null
+++ b/twisted/plugins/cred_memory.py
@@ -0,0 +1,68 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cred plugin for an in-memory user database.
+"""
+
+from zope.interface import implements
+
+from twisted import plugin
+from twisted.cred.strcred import ICheckerFactory
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.credentials import IUsernamePassword, IUsernameHashedPassword
+
+
+
+inMemoryCheckerFactoryHelp = """
+A checker that uses an in-memory user database.
+
+This is only of use in one-off test programs or examples which
+don't want to focus too much on how credentials are verified. You
+really don't want to use this for anything else. It is a toy.
+"""
+
+
+
+class InMemoryCheckerFactory(object):
+ """
+ A factory for in-memory credentials checkers.
+
+ This is only of use in one-off test programs or examples which don't
+ want to focus too much on how credentials are verified.
+
+ You really don't want to use this for anything else. It is, at best, a
+ toy. If you need a simple credentials checker for a real application,
+ see L{cred_passwd.PasswdCheckerFactory}.
+ """
+ implements(ICheckerFactory, plugin.IPlugin)
+ authType = 'memory'
+ authHelp = inMemoryCheckerFactoryHelp
+ argStringFormat = 'A colon-separated list (name:password:...)'
+ credentialInterfaces = (IUsernamePassword,
+ IUsernameHashedPassword)
+
+ def generateChecker(self, argstring):
+ """
+ This checker factory expects to get a list of
+ username:password pairs, with each pair also separated by a
+ colon. For example, the string 'alice:f:bob:g' would generate
+ two users, one named 'alice' and one named 'bob'.
+ """
+ checker = InMemoryUsernamePasswordDatabaseDontUse()
+ if argstring:
+ pieces = argstring.split(':')
+ if len(pieces) % 2:
+ from twisted.cred.strcred import InvalidAuthArgumentString
+ raise InvalidAuthArgumentString(
+ "argstring must be in format U:P:...")
+ for i in range(0, len(pieces), 2):
+ username, password = pieces[i], pieces[i+1]
+ checker.addUser(username, password)
+ return checker
+
+
+
+theInMemoryCheckerFactory = InMemoryCheckerFactory()
diff --git a/twisted/plugins/cred_sshkeys.py b/twisted/plugins/cred_sshkeys.py
new file mode 100644
index 0000000..226b34a
--- /dev/null
+++ b/twisted/plugins/cred_sshkeys.py
@@ -0,0 +1,51 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cred plugin for ssh key login
+"""
+
+from zope.interface import implements
+
+from twisted import plugin
+from twisted.cred.strcred import ICheckerFactory
+from twisted.cred.credentials import ISSHPrivateKey
+
+
+sshKeyCheckerFactoryHelp = """
+This allows SSH public key authentication, based on public keys listed in
+authorized_keys and authorized_keys2 files in user .ssh/ directories.
+"""
+
+
+try:
+ from twisted.conch.checkers import SSHPublicKeyDatabase
+
+ class SSHKeyCheckerFactory(object):
+ """
+ Generates checkers that will authenticate a SSH public key
+ """
+ implements(ICheckerFactory, plugin.IPlugin)
+ authType = 'sshkey'
+ authHelp = sshKeyCheckerFactoryHelp
+ argStringFormat = 'No argstring required.'
+ credentialInterfaces = SSHPublicKeyDatabase.credentialInterfaces
+
+
+ def generateChecker(self, argstring=''):
+ """
+ This checker factory ignores the argument string. Everything
+ needed to authenticate users is pulled out of the public keys
+ listed in user .ssh/ directories.
+ """
+ return SSHPublicKeyDatabase()
+
+
+
+ theSSHKeyCheckerFactory = SSHKeyCheckerFactory()
+
+except ImportError:
+ # if checkers can't be imported, then there should be no SSH cred plugin
+ pass
diff --git a/twisted/plugins/cred_unix.py b/twisted/plugins/cred_unix.py
new file mode 100644
index 0000000..a636497
--- /dev/null
+++ b/twisted/plugins/cred_unix.py
@@ -0,0 +1,138 @@
+# -*- test-case-name: twisted.test.test_strcred -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cred plugin for UNIX user accounts.
+"""
+
+from zope.interface import implements
+
+from twisted import plugin
+from twisted.cred.strcred import ICheckerFactory
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.credentials import IUsernamePassword
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer
+
+
+
+def verifyCryptedPassword(crypted, pw):
+ if crypted[0] == '$': # md5_crypt encrypted
+ salt = '$1$' + crypted.split('$')[2]
+ else:
+ salt = crypted[:2]
+ try:
+ import crypt
+ except ImportError:
+ crypt = None
+
+ if crypt is None:
+ raise NotImplementedError("cred_unix not supported on this platform")
+ return crypt.crypt(pw, salt) == crypted
+
+
+
+class UNIXChecker(object):
+ """
+ A credentials checker for a UNIX server. This will check that
+ an authenticating username/password is a valid user on the system.
+
+ Does not work on Windows.
+
+ Right now this supports Python's pwd and spwd modules, if they are
+ installed. It does not support PAM.
+ """
+ implements(ICredentialsChecker)
+ credentialInterfaces = (IUsernamePassword,)
+
+
+ def checkPwd(self, pwd, username, password):
+ try:
+ cryptedPass = pwd.getpwnam(username)[1]
+ except KeyError:
+ return defer.fail(UnauthorizedLogin())
+ else:
+ if cryptedPass in ('*', 'x'):
+ # Allow checkSpwd to take over
+ return None
+ elif verifyCryptedPassword(cryptedPass, password):
+ return defer.succeed(username)
+
+
+ def checkSpwd(self, spwd, username, password):
+ try:
+ cryptedPass = spwd.getspnam(username)[1]
+ except KeyError:
+ return defer.fail(UnauthorizedLogin())
+ else:
+ if verifyCryptedPassword(cryptedPass, password):
+ return defer.succeed(username)
+
+
+ def requestAvatarId(self, credentials):
+ username, password = credentials.username, credentials.password
+
+ try:
+ import pwd
+ except ImportError:
+ pwd = None
+
+ if pwd is not None:
+ checked = self.checkPwd(pwd, username, password)
+ if checked is not None:
+ return checked
+
+ try:
+ import spwd
+ except ImportError:
+ spwd = None
+
+ if spwd is not None:
+ checked = self.checkSpwd(spwd, username, password)
+ if checked is not None:
+ return checked
+ # TODO: check_pam?
+ # TODO: check_shadow?
+ return defer.fail(UnauthorizedLogin())
+
+
+
+unixCheckerFactoryHelp = """
+This checker will attempt to use every resource available to
+authenticate against the list of users on the local UNIX system.
+(This does not support Windows servers for very obvious reasons.)
+
+Right now, this includes support for:
+
+ * Python's pwd module (which checks /etc/passwd)
+ * Python's spwd module (which checks /etc/shadow)
+
+Future versions may include support for PAM authentication.
+"""
+
+
+
+class UNIXCheckerFactory(object):
+ """
+ A factory for L{UNIXChecker}.
+ """
+ implements(ICheckerFactory, plugin.IPlugin)
+ authType = 'unix'
+ authHelp = unixCheckerFactoryHelp
+ argStringFormat = 'No argstring required.'
+ credentialInterfaces = UNIXChecker.credentialInterfaces
+
+ def generateChecker(self, argstring):
+ """
+ This checker factory ignores the argument string. Everything
+ needed to generate a user database is pulled out of the local
+ UNIX environment.
+ """
+ return UNIXChecker()
+
+
+
+theUnixCheckerFactory = UNIXCheckerFactory()
+
diff --git a/twisted/plugins/twisted_conch.py b/twisted/plugins/twisted_conch.py
new file mode 100644
index 0000000..4b37e0b
--- /dev/null
+++ b/twisted/plugins/twisted_conch.py
@@ -0,0 +1,18 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedSSH = ServiceMaker(
+ "Twisted Conch Server",
+ "twisted.conch.tap",
+ "A Conch SSH service.",
+ "conch")
+
+TwistedManhole = ServiceMaker(
+ "Twisted Manhole (new)",
+ "twisted.conch.manhole_tap",
+ ("An interactive remote debugger service accessible via telnet "
+ "and ssh and providing syntax coloring and basic line editing "
+ "functionality."),
+ "manhole")
diff --git a/twisted/plugins/twisted_core.py b/twisted/plugins/twisted_core.py
new file mode 100644
index 0000000..7e86a51
--- /dev/null
+++ b/twisted/plugins/twisted_core.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.internet.endpoints import _SystemdParser
+
+systemdEndpointParser = _SystemdParser()
diff --git a/twisted/plugins/twisted_ftp.py b/twisted/plugins/twisted_ftp.py
new file mode 100644
index 0000000..474a9c7
--- /dev/null
+++ b/twisted/plugins/twisted_ftp.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedFTP = ServiceMaker(
+ "Twisted FTP",
+ "twisted.tap.ftp",
+ "An FTP server.",
+ "ftp")
diff --git a/twisted/plugins/twisted_inet.py b/twisted/plugins/twisted_inet.py
new file mode 100644
index 0000000..1196343
--- /dev/null
+++ b/twisted/plugins/twisted_inet.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedINETD = ServiceMaker(
+ "Twisted INETD Server",
+ "twisted.runner.inetdtap",
+ "An inetd(8) replacement.",
+ "inetd")
diff --git a/twisted/plugins/twisted_lore.py b/twisted/plugins/twisted_lore.py
new file mode 100644
index 0000000..1ab57a5
--- /dev/null
+++ b/twisted/plugins/twisted_lore.py
@@ -0,0 +1,38 @@
+
+from zope.interface import implements
+
+from twisted.lore.scripts.lore import IProcessor
+from twisted.plugin import IPlugin
+
+class _LorePlugin(object):
+ implements(IPlugin, IProcessor)
+
+ def __init__(self, name, moduleName, description):
+ self.name = name
+ self.moduleName = moduleName
+ self.description = description
+
+DefaultProcessor = _LorePlugin(
+ "lore",
+ "twisted.lore.default",
+ "Lore format")
+
+MathProcessor = _LorePlugin(
+ "mlore",
+ "twisted.lore.lmath",
+ "Lore format with LaTeX formula")
+
+SlideProcessor = _LorePlugin(
+ "lore-slides",
+ "twisted.lore.slides",
+ "Lore for slides")
+
+ManProcessor = _LorePlugin(
+ "man",
+ "twisted.lore.man2lore",
+ "UNIX Man pages")
+
+NevowProcessor = _LorePlugin(
+ "nevow",
+ "twisted.lore.nevowlore",
+ "Nevow for Lore")
diff --git a/twisted/plugins/twisted_mail.py b/twisted/plugins/twisted_mail.py
new file mode 100644
index 0000000..7e9a5bd
--- /dev/null
+++ b/twisted/plugins/twisted_mail.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedMail = ServiceMaker(
+ "Twisted Mail",
+ "twisted.mail.tap",
+ "An email service",
+ "mail")
diff --git a/twisted/plugins/twisted_manhole.py b/twisted/plugins/twisted_manhole.py
new file mode 100644
index 0000000..2481890
--- /dev/null
+++ b/twisted/plugins/twisted_manhole.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedManhole = ServiceMaker(
+ "Twisted Manhole (old)",
+ "twisted.tap.manhole",
+ "An interactive remote debugger service.",
+ "manhole-old")
diff --git a/twisted/plugins/twisted_names.py b/twisted/plugins/twisted_names.py
new file mode 100644
index 0000000..7123bf0
--- /dev/null
+++ b/twisted/plugins/twisted_names.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedNames = ServiceMaker(
+ "Twisted DNS Server",
+ "twisted.names.tap",
+ "A domain name server.",
+ "dns")
diff --git a/twisted/plugins/twisted_news.py b/twisted/plugins/twisted_news.py
new file mode 100644
index 0000000..0fc88d8
--- /dev/null
+++ b/twisted/plugins/twisted_news.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedNews = ServiceMaker(
+ "Twisted News",
+ "twisted.news.tap",
+ "A news server.",
+ "news")
diff --git a/twisted/plugins/twisted_portforward.py b/twisted/plugins/twisted_portforward.py
new file mode 100644
index 0000000..1969434
--- /dev/null
+++ b/twisted/plugins/twisted_portforward.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedPortForward = ServiceMaker(
+ "Twisted Port-Forwarding",
+ "twisted.tap.portforward",
+ "A simple port-forwarder.",
+ "portforward")
diff --git a/twisted/plugins/twisted_qtstub.py b/twisted/plugins/twisted_qtstub.py
new file mode 100644
index 0000000..ddf8843
--- /dev/null
+++ b/twisted/plugins/twisted_qtstub.py
@@ -0,0 +1,45 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Backwards-compatibility plugin for the Qt reactor.
+
+This provides a Qt reactor plugin named C{qt} which emits a deprecation
+warning and a pointer to the separately distributed Qt reactor plugins.
+"""
+
+import warnings
+
+from twisted.application.reactors import Reactor, NoSuchReactor
+
+wikiURL = 'http://twistedmatrix.com/trac/wiki/QTReactor'
+errorMessage = ('qtreactor is no longer a part of Twisted due to licensing '
+ 'issues. Please see %s for details.' % (wikiURL,))
+
+class QTStub(Reactor):
+ """
+ Reactor plugin which emits a deprecation warning on the successful
+ installation of its reactor or a pointer to further information if an
+ ImportError occurs while attempting to install it.
+ """
+ def __init__(self):
+ super(QTStub, self).__init__(
+ 'qt', 'qtreactor', 'QT integration reactor')
+
+
+ def install(self):
+ """
+ Install the Qt reactor with a deprecation warning or try to point
+ the user to further information if it cannot be installed.
+ """
+ try:
+ super(QTStub, self).install()
+ except (ValueError, ImportError):
+ raise NoSuchReactor(errorMessage)
+ else:
+ warnings.warn(
+ "Please use -r qt3 to import qtreactor",
+ category=DeprecationWarning)
+
+
+qt = QTStub()
diff --git a/twisted/plugins/twisted_reactors.py b/twisted/plugins/twisted_reactors.py
new file mode 100644
index 0000000..8562aa9
--- /dev/null
+++ b/twisted/plugins/twisted_reactors.py
@@ -0,0 +1,42 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.reactors import Reactor
+
+default = Reactor(
+ 'default', 'twisted.internet.default',
+ 'A reasonable default: poll(2) if available, otherwise select(2).')
+
+select = Reactor(
+ 'select', 'twisted.internet.selectreactor', 'select(2)-based reactor.')
+wx = Reactor(
+ 'wx', 'twisted.internet.wxreactor', 'wxPython integration reactor.')
+gi = Reactor(
+ 'gi', 'twisted.internet.gireactor', 'GObject Introspection integration reactor.')
+gtk3 = Reactor(
+ 'gtk3', 'twisted.internet.gtk3reactor', 'Gtk3 integration reactor.')
+gtk = Reactor(
+ 'gtk', 'twisted.internet.gtkreactor', 'Gtk1 integration reactor.')
+gtk2 = Reactor(
+ 'gtk2', 'twisted.internet.gtk2reactor', 'Gtk2 integration reactor.')
+glib2 = Reactor(
+ 'glib2', 'twisted.internet.glib2reactor',
+ 'GLib2 event-loop integration reactor.')
+glade = Reactor(
+ 'debug-gui', 'twisted.manhole.gladereactor',
+ 'Semi-functional debugging/introspection reactor.')
+win32er = Reactor(
+ 'win32', 'twisted.internet.win32eventreactor',
+ 'Win32 WaitForMultipleObjects-based reactor.')
+poll = Reactor(
+ 'poll', 'twisted.internet.pollreactor', 'poll(2)-based reactor.')
+epoll = Reactor(
+ 'epoll', 'twisted.internet.epollreactor', 'epoll(4)-based reactor.')
+cf = Reactor(
+ 'cf' , 'twisted.internet.cfreactor',
+ 'CoreFoundation integration reactor.')
+kqueue = Reactor(
+ 'kqueue', 'twisted.internet.kqreactor', 'kqueue(2)-based reactor.')
+iocp = Reactor(
+ 'iocp', 'twisted.internet.iocpreactor',
+ 'Win32 IO Completion Ports-based reactor.')
diff --git a/twisted/plugins/twisted_runner.py b/twisted/plugins/twisted_runner.py
new file mode 100644
index 0000000..dc63028
--- /dev/null
+++ b/twisted/plugins/twisted_runner.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedProcmon = ServiceMaker(
+ "Twisted Process Monitor",
+ "twisted.runner.procmontap",
+ ("A process watchdog / supervisor"),
+ "procmon")
diff --git a/twisted/plugins/twisted_socks.py b/twisted/plugins/twisted_socks.py
new file mode 100644
index 0000000..5a94f87
--- /dev/null
+++ b/twisted/plugins/twisted_socks.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedSOCKS = ServiceMaker(
+ "Twisted SOCKS",
+ "twisted.tap.socks",
+ "A SOCKSv4 proxy service.",
+ "socks")
diff --git a/twisted/plugins/twisted_telnet.py b/twisted/plugins/twisted_telnet.py
new file mode 100644
index 0000000..4cb1f98
--- /dev/null
+++ b/twisted/plugins/twisted_telnet.py
@@ -0,0 +1,10 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedTelnet = ServiceMaker(
+ "Twisted Telnet Shell Server",
+ "twisted.tap.telnet",
+ "A simple, telnet-based remote debugging service.",
+ "telnet")
diff --git a/twisted/plugins/twisted_trial.py b/twisted/plugins/twisted_trial.py
new file mode 100644
index 0000000..debc8af
--- /dev/null
+++ b/twisted/plugins/twisted_trial.py
@@ -0,0 +1,59 @@
+
+from zope.interface import implements
+
+from twisted.trial.itrial import IReporter
+from twisted.plugin import IPlugin
+
+class _Reporter(object):
+ implements(IPlugin, IReporter)
+
+ def __init__(self, name, module, description, longOpt, shortOpt, klass):
+ self.name = name
+ self.module = module
+ self.description = description
+ self.longOpt = longOpt
+ self.shortOpt = shortOpt
+ self.klass = klass
+
+
+Tree = _Reporter("Tree Reporter",
+ "twisted.trial.reporter",
+ description="verbose color output (default reporter)",
+ longOpt="verbose",
+ shortOpt="v",
+ klass="TreeReporter")
+
+BlackAndWhite = _Reporter("Black-And-White Reporter",
+ "twisted.trial.reporter",
+ description="Colorless verbose output",
+ longOpt="bwverbose",
+ shortOpt="o",
+ klass="VerboseTextReporter")
+
+Minimal = _Reporter("Minimal Reporter",
+ "twisted.trial.reporter",
+ description="minimal summary output",
+ longOpt="summary",
+ shortOpt="s",
+ klass="MinimalReporter")
+
+Classic = _Reporter("Classic Reporter",
+ "twisted.trial.reporter",
+ description="terse text output",
+ longOpt="text",
+ shortOpt="t",
+ klass="TextReporter")
+
+Timing = _Reporter("Timing Reporter",
+ "twisted.trial.reporter",
+ description="Timing output",
+ longOpt="timing",
+ shortOpt=None,
+ klass="TimingTextReporter")
+
+Subunit = _Reporter("Subunit Reporter",
+ "twisted.trial.reporter",
+ description="subunit output",
+ longOpt="subunit",
+ shortOpt=None,
+ klass="SubunitReporter")
diff --git a/twisted/plugins/twisted_web.py b/twisted/plugins/twisted_web.py
new file mode 100644
index 0000000..c7655a6
--- /dev/null
+++ b/twisted/plugins/twisted_web.py
@@ -0,0 +1,11 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application.service import ServiceMaker
+
+TwistedWeb = ServiceMaker(
+ "Twisted Web",
+ "twisted.web.tap",
+ ("A general-purpose web server which can serve from a "
+ "filesystem or application resource."),
+ "web")
diff --git a/twisted/plugins/twisted_words.py b/twisted/plugins/twisted_words.py
new file mode 100644
index 0000000..6f14aef
--- /dev/null
+++ b/twisted/plugins/twisted_words.py
@@ -0,0 +1,43 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import classProvides
+
+from twisted.plugin import IPlugin
+
+from twisted.application.service import ServiceMaker
+from twisted.words import iwords
+
+
+NewTwistedWords = ServiceMaker(
+ "New Twisted Words",
+ "twisted.words.tap",
+ "A modern words server",
+ "words")
+
+TwistedXMPPRouter = ServiceMaker(
+ "XMPP Router",
+ "twisted.words.xmpproutertap",
+ "An XMPP Router server",
+ "xmpp-router")
+
+class RelayChatInterface(object):
+ classProvides(IPlugin, iwords.IProtocolPlugin)
+
+ name = 'irc'
+
+ def getFactory(cls, realm, portal):
+ from twisted.words import service
+ return service.IRCFactory(realm, portal)
+ getFactory = classmethod(getFactory)
+
+class PBChatInterface(object):
+ classProvides(IPlugin, iwords.IProtocolPlugin)
+
+ name = 'pb'
+
+ def getFactory(cls, realm, portal):
+ from twisted.spread import pb
+ return pb.PBServerFactory(portal, True)
+ getFactory = classmethod(getFactory)
+
diff --git a/twisted/protocols/__init__.py b/twisted/protocols/__init__.py
new file mode 100644
index 0000000..a079651
--- /dev/null
+++ b/twisted/protocols/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Protocols: a collection of internet protocol implementations.
+"""
diff --git a/twisted/protocols/amp.py b/twisted/protocols/amp.py
new file mode 100644
index 0000000..72a3e7a
--- /dev/null
+++ b/twisted/protocols/amp.py
@@ -0,0 +1,2705 @@
+# -*- test-case-name: twisted.test.test_amp -*-
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module implements AMP, the Asynchronous Messaging Protocol.
+
+AMP is a protocol for sending multiple asynchronous request/response pairs over
+the same connection. Requests and responses are both collections of key/value
+pairs.
+
+AMP is a very simple protocol which is not an application. This module is a
+"protocol construction kit" of sorts; it attempts to be the simplest wire-level
+implementation of Deferreds. AMP provides the following base-level features:
+
+ - Asynchronous request/response handling (hence the name)
+
+ - Requests and responses are both key/value pairs
+
+ - Binary transfer of all data: all data is length-prefixed. Your
+ application will never need to worry about quoting.
+
+ - Command dispatching (like HTTP Verbs): the protocol is extensible, and
+ multiple AMP sub-protocols can be grouped together easily.
+
+The protocol implementation also provides a few additional features which are
+not part of the core wire protocol, but are nevertheless very useful:
+
+ - Tight TLS integration, with an included StartTLS command.
+
+ - Handshaking to other protocols: because AMP has well-defined message
+ boundaries and maintains all incoming and outgoing requests for you, you
+ can start a connection over AMP and then switch to another protocol.
+ This makes it ideal for firewall-traversal applications where you may
+ have only one forwarded port but multiple applications that want to use
+ it.
+
+Using AMP with Twisted is simple. Each message is a command, with a response.
+You begin by defining a command type. Commands specify their input and output
+in terms of the types that they expect to see in the request and response
+key-value pairs. Here's an example of a command that adds two integers, 'a'
+and 'b'::
+
+ class Sum(amp.Command):
+ arguments = [('a', amp.Integer()),
+ ('b', amp.Integer())]
+ response = [('total', amp.Integer())]
+
+Once you have specified a command, you need to make it part of a protocol, and
+define a responder for it. Here's a 'JustSum' protocol that includes a
+responder for our 'Sum' command::
+
+ class JustSum(amp.AMP):
+ def sum(self, a, b):
+ total = a + b
+ print 'Did a sum: %d + %d = %d' % (a, b, total)
+ return {'total': total}
+ Sum.responder(sum)
+
+Later, when you want to actually do a sum, the following expression will return
+a L{Deferred} which will fire with the result::
+
+ ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
+ lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
+ lambda result: result['total'])
+
+Command responders may also return Deferreds, causing the response to be
+sent only once the Deferred fires::
+
+ class DelayedSum(amp.AMP):
+ def slowSum(self, a, b):
+ total = a + b
+ result = defer.Deferred()
+ reactor.callLater(3, result.callback, {'total': total})
+ return result
+ Sum.responder(slowSum)
+
+This is transparent to the caller.
+
+You can also define the propagation of specific errors in AMP. For example,
+for the slightly more complicated case of division, we might have to deal with
+division by zero::
+
+ class Divide(amp.Command):
+ arguments = [('numerator', amp.Integer()),
+ ('denominator', amp.Integer())]
+ response = [('result', amp.Float())]
+ errors = {ZeroDivisionError: 'ZERO_DIVISION'}
+
+The 'errors' mapping here tells AMP that if a responder to Divide emits a
+L{ZeroDivisionError}, then the other side should be informed that an error of
+the type 'ZERO_DIVISION' has occurred. Writing a responder which takes
+advantage of this is very simple - just raise your exception normally::
+
+ class JustDivide(amp.AMP):
+ def divide(self, numerator, denominator):
+ result = numerator / denominator
+ print 'Divided: %d / %d = %d' % (numerator, denominator, total)
+ return {'result': result}
+ Divide.responder(divide)
+
+On the client side, the errors mapping will be used to determine what the
+'ZERO_DIVISION' error means, and translated into an asynchronous exception,
+which can be handled normally as any L{Deferred} would be::
+
+ def trapZero(result):
+ result.trap(ZeroDivisionError)
+ print "Divided by zero: returning INF"
+ return 1e1000
+ ClientCreator(reactor, amp.AMP).connectTCP(...).addCallback(
+ lambda p: p.callRemote(Divide, numerator=1234,
+ denominator=0)
+ ).addErrback(trapZero)
+
+For a complete, runnable example of both of these commands, see the files in
+the Twisted repository::
+
+ doc/core/examples/ampserver.py
+ doc/core/examples/ampclient.py
+
+On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and
+values, and empty keys to separate messages::
+
+ <2-byte length><key><2-byte length><value>
+ <2-byte length><key><2-byte length><value>
+ ...
+ <2-byte length><key><2-byte length><value>
+ <NUL><NUL> # Empty Key == End of Message
+
+And so on. Because it's tedious to refer to lengths and NULs constantly, the
+documentation will refer to packets as if they were newline delimited, like
+so::
+
+ C: _command: sum
+ C: _ask: ef639e5c892ccb54
+ C: a: 13
+ C: b: 81
+
+ S: _answer: ef639e5c892ccb54
+ S: total: 94
+
+Notes:
+
+In general, the order of keys is arbitrary. Specific uses of AMP may impose an
+ordering requirement, but unless this is specified explicitly, any ordering may
+be generated and any ordering must be accepted. This applies to the
+command-related keys I{_command} and I{_ask} as well as any other keys.
+
+Values are limited to the maximum encodable size in a 16-bit length, 65535
+bytes.
+
+Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes.
+Note that we still use 2-byte lengths to encode keys. This small redundancy
+has several features:
+
+ - If an implementation becomes confused and starts emitting corrupt data,
+ or gets keys confused with values, many common errors will be signalled
+ immediately instead of delivering obviously corrupt packets.
+
+ - A single NUL will separate every key, and a double NUL separates
+ messages. This provides some redundancy when debugging traffic dumps.
+
+ - NULs will be present at regular intervals along the protocol, providing
+ some padding for otherwise braindead C implementations of the protocol,
+ so that <stdio.h> string functions will see the NUL and stop.
+
+ - This makes it possible to run an AMP server on a port also used by a
+ plain-text protocol, and easily distinguish between non-AMP clients (like
+ web browsers) which issue non-NUL as the first byte, and AMP clients,
+ which always issue NUL as the first byte.
+"""
+
+__metaclass__ = type
+
+import types, warnings
+
+from cStringIO import StringIO
+from struct import pack
+import decimal, datetime
+from itertools import count
+
+from zope.interface import Interface, implements
+
+from twisted.python.compat import set
+from twisted.python.util import unsignedID
+from twisted.python.reflect import accumulateClassDict
+from twisted.python.failure import Failure
+from twisted.python import log, filepath
+
+from twisted.internet.interfaces import IFileDescriptorReceiver
+from twisted.internet.main import CONNECTION_LOST
+from twisted.internet.error import PeerVerifyError, ConnectionLost
+from twisted.internet.error import ConnectionClosed
+from twisted.internet.defer import Deferred, maybeDeferred, fail
+from twisted.protocols.basic import Int16StringReceiver, StatefulStringProtocol
+
+try:
+ from twisted.internet import ssl
+except ImportError:
+ ssl = None
+
+if ssl and not ssl.supported:
+ ssl = None
+
+if ssl is not None:
+ from twisted.internet.ssl import CertificateOptions, Certificate, DN, KeyPair
+
+ASK = '_ask'
+ANSWER = '_answer'
+COMMAND = '_command'
+ERROR = '_error'
+ERROR_CODE = '_error_code'
+ERROR_DESCRIPTION = '_error_description'
+UNKNOWN_ERROR_CODE = 'UNKNOWN'
+UNHANDLED_ERROR_CODE = 'UNHANDLED'
+
+MAX_KEY_LENGTH = 0xff
+MAX_VALUE_LENGTH = 0xffff
+
+
+class IArgumentType(Interface):
+ """
+ An L{IArgumentType} can serialize a Python object into an AMP box and
+ deserialize information from an AMP box back into a Python object.
+
+ @since: 9.0
+ """
+ def fromBox(name, strings, objects, proto):
+ """
+ Given an argument name and an AMP box containing serialized values,
+ extract one or more Python objects and add them to the C{objects}
+ dictionary.
+
+ @param name: The name associated with this argument. Most commonly,
+ this is the key which can be used to find a serialized value in
+ C{strings} and which should be used as the key in C{objects} to
+ associate with a structured Python object.
+ @type name: C{str}
+
+ @param strings: The AMP box from which to extract one or more
+ values.
+ @type strings: C{dict}
+
+ @param objects: The output dictionary to populate with the value for
+ this argument.
+ @type objects: C{dict}
+
+ @param proto: The protocol instance which received the AMP box being
+ interpreted. Most likely this is an instance of L{AMP}, but
+ this is not guaranteed.
+
+ @return: C{None}
+ """
+
+
+ def toBox(name, strings, objects, proto):
+ """
+ Given an argument name and a dictionary containing structured Python
+ objects, serialize values into one or more strings and add them to
+ the C{strings} dictionary.
+
+ @param name: The name associated with this argument. Most commonly,
+ this is the key which can be used to find an object in
+ C{objects} and which should be used as the key in C{strings} to
+ associate with a C{str} giving the serialized form of that
+ object.
+ @type name: C{str}
+
+ @param strings: The AMP box into which to insert one or more
+ strings.
+ @type strings: C{dict}
+
+ @param objects: The input dictionary from which to extract Python
+ objects to serialize.
+ @type objects: C{dict}
+
+ @param proto: The protocol instance which will send the AMP box once
+ it is fully populated. Most likely this is an instance of
+ L{AMP}, but this is not guaranteed.
+
+ @return: C{None}
+ """
+
+
+
+class IBoxSender(Interface):
+ """
+ A transport which can send L{AmpBox} objects.
+ """
+
+ def sendBox(box):
+ """
+ Send an L{AmpBox}.
+
+ @raise ProtocolSwitched: if the underlying protocol has been
+ switched.
+
+ @raise ConnectionLost: if the underlying connection has already been
+ lost.
+ """
+
+ def unhandledError(failure):
+ """
+ An unhandled error occurred in response to a box. Log it
+ appropriately.
+
+ @param failure: a L{Failure} describing the error that occurred.
+ """
+
+
+
+class IBoxReceiver(Interface):
+ """
+ An application object which can receive L{AmpBox} objects and dispatch them
+ appropriately.
+ """
+
+ def startReceivingBoxes(boxSender):
+ """
+ The L{ampBoxReceived} method will start being called; boxes may be
+ responded to by responding to the given L{IBoxSender}.
+
+ @param boxSender: an L{IBoxSender} provider.
+ """
+
+
+ def ampBoxReceived(box):
+ """
+ A box was received from the transport; dispatch it appropriately.
+ """
+
+
+ def stopReceivingBoxes(reason):
+ """
+ No further boxes will be received on this connection.
+
+ @type reason: L{Failure}
+ """
+
+
+
+class IResponderLocator(Interface):
+ """
+ An application object which can look up appropriate responder methods for
+ AMP commands.
+ """
+
+ def locateResponder(name):
+ """
+ Locate a responder method appropriate for the named command.
+
+ @param name: the wire-level name (commandName) of the AMP command to be
+ responded to.
+
+ @return: a 1-argument callable that takes an L{AmpBox} with argument
+ values for the given command, and returns an L{AmpBox} containing
+ argument values for the named command, or a L{Deferred} that fires the
+ same.
+ """
+
+
+
+class AmpError(Exception):
+ """
+ Base class of all Amp-related exceptions.
+ """
+
+
+
+class ProtocolSwitched(Exception):
+ """
+ Connections which have been switched to other protocols can no longer
+ accept traffic at the AMP level. This is raised when you try to send it.
+ """
+
+
+
+class OnlyOneTLS(AmpError):
+ """
+ This is an implementation limitation; TLS may only be started once per
+ connection.
+ """
+
+
+
+class NoEmptyBoxes(AmpError):
+ """
+ You can't have empty boxes on the connection. This is raised when you
+ receive or attempt to send one.
+ """
+
+
+
+class InvalidSignature(AmpError):
+ """
+ You didn't pass all the required arguments.
+ """
+
+
+
+class TooLong(AmpError):
+ """
+ One of the protocol's length limitations was violated.
+
+ @ivar isKey: true if the string being encoded in a key position, false if
+ it was in a value position.
+
+ @ivar isLocal: Was the string encoded locally, or received too long from
+ the network? (It's only physically possible to encode "too long" values on
+ the network for keys.)
+
+ @ivar value: The string that was too long.
+
+ @ivar keyName: If the string being encoded was in a value position, what
+ key was it being encoded for?
+ """
+
+ def __init__(self, isKey, isLocal, value, keyName=None):
+ AmpError.__init__(self)
+ self.isKey = isKey
+ self.isLocal = isLocal
+ self.value = value
+ self.keyName = keyName
+
+
+ def __repr__(self):
+ hdr = self.isKey and "key" or "value"
+ if not self.isKey:
+ hdr += ' ' + repr(self.keyName)
+ lcl = self.isLocal and "local" or "remote"
+ return "%s %s too long: %d" % (lcl, hdr, len(self.value))
+
+
+
+class BadLocalReturn(AmpError):
+ """
+ A bad value was returned from a local command; we were unable to coerce it.
+ """
+ def __init__(self, message, enclosed):
+ AmpError.__init__(self)
+ self.message = message
+ self.enclosed = enclosed
+
+
+ def __repr__(self):
+ return self.message + " " + self.enclosed.getBriefTraceback()
+
+ __str__ = __repr__
+
+
+
+class RemoteAmpError(AmpError):
+ """
+ This error indicates that something went wrong on the remote end of the
+ connection, and the error was serialized and transmitted to you.
+ """
+ def __init__(self, errorCode, description, fatal=False, local=None):
+ """Create a remote error with an error code and description.
+
+ @param errorCode: the AMP error code of this error.
+
+ @param description: some text to show to the user.
+
+ @param fatal: a boolean, true if this error should terminate the
+ connection.
+
+ @param local: a local Failure, if one exists.
+ """
+ if local:
+ localwhat = ' (local)'
+ othertb = local.getBriefTraceback()
+ else:
+ localwhat = ''
+ othertb = ''
+ Exception.__init__(self, "Code<%s>%s: %s%s" % (
+ errorCode, localwhat,
+ description, othertb))
+ self.local = local
+ self.errorCode = errorCode
+ self.description = description
+ self.fatal = fatal
+
+
+
+class UnknownRemoteError(RemoteAmpError):
+ """
+ This means that an error whose type we can't identify was raised from the
+ other side.
+ """
+ def __init__(self, description):
+ errorCode = UNKNOWN_ERROR_CODE
+ RemoteAmpError.__init__(self, errorCode, description)
+
+
+
+class MalformedAmpBox(AmpError):
+ """
+ This error indicates that the wire-level protocol was malformed.
+ """
+
+
+
+class UnhandledCommand(AmpError):
+ """
+ A command received via amp could not be dispatched.
+ """
+
+
+
+class IncompatibleVersions(AmpError):
+ """
+ It was impossible to negotiate a compatible version of the protocol with
+ the other end of the connection.
+ """
+
+
+PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}
+
+class AmpBox(dict):
+ """
+ I am a packet in the AMP protocol, much like a regular str:str dictionary.
+ """
+ __slots__ = [] # be like a regular dictionary, don't magically
+ # acquire a __dict__...
+
+
+ def copy(self):
+ """
+ Return another AmpBox just like me.
+ """
+ newBox = self.__class__()
+ newBox.update(self)
+ return newBox
+
+
+ def serialize(self):
+ """
+ Convert me into a wire-encoded string.
+
+ @return: a str encoded according to the rules described in the module
+ docstring.
+ """
+ i = self.items()
+ i.sort()
+ L = []
+ w = L.append
+ for k, v in i:
+ if type(k) == unicode:
+ raise TypeError("Unicode key not allowed: %r" % k)
+ if type(v) == unicode:
+ raise TypeError(
+ "Unicode value for key %r not allowed: %r" % (k, v))
+ if len(k) > MAX_KEY_LENGTH:
+ raise TooLong(True, True, k, None)
+ if len(v) > MAX_VALUE_LENGTH:
+ raise TooLong(False, True, v, k)
+ for kv in k, v:
+ w(pack("!H", len(kv)))
+ w(kv)
+ w(pack("!H", 0))
+ return ''.join(L)
+
+
+ def _sendTo(self, proto):
+ """
+ Serialize and send this box to a Amp instance. By the time it is being
+ sent, several keys are required. I must have exactly ONE of::
+
+ _ask
+ _answer
+ _error
+
+ If the '_ask' key is set, then the '_command' key must also be
+ set.
+
+ @param proto: an AMP instance.
+ """
+ proto.sendBox(self)
+
+ def __repr__(self):
+ return 'AmpBox(%s)' % (dict.__repr__(self),)
+
+# amp.Box => AmpBox
+
+Box = AmpBox
+
+class QuitBox(AmpBox):
+ """
+ I am an AmpBox that, upon being sent, terminates the connection.
+ """
+ __slots__ = []
+
+
+ def __repr__(self):
+ return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),)
+
+
+ def _sendTo(self, proto):
+ """
+ Immediately call loseConnection after sending.
+ """
+ super(QuitBox, self)._sendTo(proto)
+ proto.transport.loseConnection()
+
+
+
+class _SwitchBox(AmpBox):
+ """
+ Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets
+ up state for the protocol to switch.
+ """
+
+ # DON'T set __slots__ here; we do have an attribute.
+
+ def __init__(self, innerProto, **kw):
+ """
+ Create a _SwitchBox with the protocol to switch to after being sent.
+
+ @param innerProto: the protocol instance to switch to.
+ @type innerProto: an IProtocol provider.
+ """
+ super(_SwitchBox, self).__init__(**kw)
+ self.innerProto = innerProto
+
+
+ def __repr__(self):
+ return '_SwitchBox(%r, **%s)' % (self.innerProto,
+ dict.__repr__(self),)
+
+
+ def _sendTo(self, proto):
+ """
+ Send me; I am the last box on the connection. All further traffic will be
+ over the new protocol.
+ """
+ super(_SwitchBox, self)._sendTo(proto)
+ proto._lockForSwitch()
+ proto._switchTo(self.innerProto)
+
+
+
+class BoxDispatcher:
+ """
+ A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es,
+ both incoming and outgoing, to their appropriate destinations.
+
+ Outgoing commands are converted into L{Deferred}s and outgoing boxes, and
+ associated tracking state to fire those L{Deferred} when '_answer' boxes
+ come back. Incoming '_answer' and '_error' boxes are converted into
+ callbacks and errbacks on those L{Deferred}s, respectively.
+
+ Incoming '_ask' boxes are converted into method calls on a supplied method
+ locator.
+
+ @ivar _outstandingRequests: a dictionary mapping request IDs to
+ L{Deferred}s which were returned for those requests.
+
+ @ivar locator: an object with a L{locateResponder} method that locates a
+ responder function that takes a Box and returns a result (either a Box or a
+ Deferred which fires one).
+
+ @ivar boxSender: an object which can send boxes, via the L{_sendBox}
+ method, such as an L{AMP} instance.
+ @type boxSender: L{IBoxSender}
+ """
+
+ implements(IBoxReceiver)
+
+ _failAllReason = None
+ _outstandingRequests = None
+ _counter = 0L
+ boxSender = None
+
+ def __init__(self, locator):
+ self._outstandingRequests = {}
+ self.locator = locator
+
+
+ def startReceivingBoxes(self, boxSender):
+ """
+ The given boxSender is going to start calling boxReceived on this
+ L{BoxDispatcher}.
+
+ @param boxSender: The L{IBoxSender} to send command responses to.
+ """
+ self.boxSender = boxSender
+
+
+ def stopReceivingBoxes(self, reason):
+ """
+ No further boxes will be received here. Terminate all currently
+ oustanding command deferreds with the given reason.
+ """
+ self.failAllOutgoing(reason)
+
+
+ def failAllOutgoing(self, reason):
+ """
+ Call the errback on all outstanding requests awaiting responses.
+
+ @param reason: the Failure instance to pass to those errbacks.
+ """
+ self._failAllReason = reason
+ OR = self._outstandingRequests.items()
+ self._outstandingRequests = None # we can never send another request
+ for key, value in OR:
+ value.errback(reason)
+
+
+ def _nextTag(self):
+ """
+ Generate protocol-local serial numbers for _ask keys.
+
+ @return: a string that has not yet been used on this connection.
+ """
+ self._counter += 1
+ return '%x' % (self._counter,)
+
+
+ def _sendBoxCommand(self, command, box, requiresAnswer=True):
+ """
+ Send a command across the wire with the given C{amp.Box}.
+
+ Mutate the given box to give it any additional keys (_command, _ask)
+ required for the command and request/response machinery, then send it.
+
+ If requiresAnswer is True, returns a C{Deferred} which fires when a
+ response is received. The C{Deferred} is fired with an C{amp.Box} on
+ success, or with an C{amp.RemoteAmpError} if an error is received.
+
+ If the Deferred fails and the error is not handled by the caller of
+ this method, the failure will be logged and the connection dropped.
+
+ @param command: a str, the name of the command to issue.
+
+ @param box: an AmpBox with the arguments for the command.
+
+ @param requiresAnswer: a boolean. Defaults to True. If True, return a
+ Deferred which will fire when the other side responds to this command.
+ If False, return None and do not ask the other side for acknowledgement.
+
+ @return: a Deferred which fires the AmpBox that holds the response to
+ this command, or None, as specified by requiresAnswer.
+
+ @raise ProtocolSwitched: if the protocol has been switched.
+ """
+ if self._failAllReason is not None:
+ return fail(self._failAllReason)
+ box[COMMAND] = command
+ tag = self._nextTag()
+ if requiresAnswer:
+ box[ASK] = tag
+ box._sendTo(self.boxSender)
+ if requiresAnswer:
+ result = self._outstandingRequests[tag] = Deferred()
+ else:
+ result = None
+ return result
+
+
+ def callRemoteString(self, command, requiresAnswer=True, **kw):
+ """
+ This is a low-level API, designed only for optimizing simple messages
+ for which the overhead of parsing is too great.
+
+ @param command: a str naming the command.
+
+ @param kw: arguments to the amp box.
+
+ @param requiresAnswer: a boolean. Defaults to True. If True, return a
+ Deferred which will fire when the other side responds to this command.
+ If False, return None and do not ask the other side for acknowledgement.
+
+ @return: a Deferred which fires the AmpBox that holds the response to
+ this command, or None, as specified by requiresAnswer.
+ """
+ box = Box(kw)
+ return self._sendBoxCommand(command, box, requiresAnswer)
+
+
+ def callRemote(self, commandType, *a, **kw):
+ """
+ This is the primary high-level API for sending messages via AMP. Invoke it
+ with a command and appropriate arguments to send a message to this
+ connection's peer.
+
+ @param commandType: a subclass of Command.
+ @type commandType: L{type}
+
+ @param a: Positional (special) parameters taken by the command.
+ Positional parameters will typically not be sent over the wire. The
+ only command included with AMP which uses positional parameters is
+ L{ProtocolSwitchCommand}, which takes the protocol that will be
+ switched to as its first argument.
+
+ @param kw: Keyword arguments taken by the command. These are the
+ arguments declared in the command's 'arguments' attribute. They will
+ be encoded and sent to the peer as arguments for the L{commandType}.
+
+ @return: If L{commandType} has a C{requiresAnswer} attribute set to
+ L{False}, then return L{None}. Otherwise, return a L{Deferred} which
+ fires with a dictionary of objects representing the result of this
+ call. Additionally, this L{Deferred} may fail with an exception
+ representing a connection failure, with L{UnknownRemoteError} if the
+ other end of the connection fails for an unknown reason, or with any
+ error specified as a key in L{commandType}'s C{errors} dictionary.
+ """
+
+ # XXX this takes command subclasses and not command objects on purpose.
+ # There's really no reason to have all this back-and-forth between
+ # command objects and the protocol, and the extra object being created
+ # (the Command instance) is pointless. Command is kind of like
+ # Interface, and should be more like it.
+
+ # In other words, the fact that commandType is instantiated here is an
+ # implementation detail. Don't rely on it.
+
+ try:
+ co = commandType(*a, **kw)
+ except:
+ return fail()
+ return co._doCommand(self)
+
+
+ def unhandledError(self, failure):
+ """
+ This is a terminal callback called after application code has had a
+ chance to quash any errors.
+ """
+ return self.boxSender.unhandledError(failure)
+
+
+ def _answerReceived(self, box):
+ """
+ An AMP box was received that answered a command previously sent with
+ L{callRemote}.
+
+ @param box: an AmpBox with a value for its L{ANSWER} key.
+ """
+ question = self._outstandingRequests.pop(box[ANSWER])
+ question.addErrback(self.unhandledError)
+ question.callback(box)
+
+
+ def _errorReceived(self, box):
+ """
+ An AMP box was received that answered a command previously sent with
+ L{callRemote}, with an error.
+
+ @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE},
+ and L{ERROR_DESCRIPTION} keys.
+ """
+ question = self._outstandingRequests.pop(box[ERROR])
+ question.addErrback(self.unhandledError)
+ errorCode = box[ERROR_CODE]
+ description = box[ERROR_DESCRIPTION]
+ if errorCode in PROTOCOL_ERRORS:
+ exc = PROTOCOL_ERRORS[errorCode](errorCode, description)
+ else:
+ exc = RemoteAmpError(errorCode, description)
+ question.errback(Failure(exc))
+
+
+ def _commandReceived(self, box):
+ """
+ @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK}
+ keys.
+ """
+ def formatAnswer(answerBox):
+ answerBox[ANSWER] = box[ASK]
+ return answerBox
+ def formatError(error):
+ if error.check(RemoteAmpError):
+ code = error.value.errorCode
+ desc = error.value.description
+ if error.value.fatal:
+ errorBox = QuitBox()
+ else:
+ errorBox = AmpBox()
+ else:
+ errorBox = QuitBox()
+ log.err(error) # here is where server-side logging happens
+ # if the error isn't handled
+ code = UNKNOWN_ERROR_CODE
+ desc = "Unknown Error"
+ errorBox[ERROR] = box[ASK]
+ errorBox[ERROR_DESCRIPTION] = desc
+ errorBox[ERROR_CODE] = code
+ return errorBox
+ deferred = self.dispatchCommand(box)
+ if ASK in box:
+ deferred.addCallbacks(formatAnswer, formatError)
+ deferred.addCallback(self._safeEmit)
+ deferred.addErrback(self.unhandledError)
+
+
+ def ampBoxReceived(self, box):
+ """
+ An AmpBox was received, representing a command, or an answer to a
+ previously issued command (either successful or erroneous). Respond to
+ it according to its contents.
+
+ @param box: an AmpBox
+
+ @raise NoEmptyBoxes: when a box is received that does not contain an
+ '_answer', '_command' / '_ask', or '_error' key; i.e. one which does not
+ fit into the command / response protocol defined by AMP.
+ """
+ if ANSWER in box:
+ self._answerReceived(box)
+ elif ERROR in box:
+ self._errorReceived(box)
+ elif COMMAND in box:
+ self._commandReceived(box)
+ else:
+ raise NoEmptyBoxes(box)
+
+
+ def _safeEmit(self, aBox):
+ """
+ Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors
+ which cannot be usefully handled.
+ """
+ try:
+ aBox._sendTo(self.boxSender)
+ except (ProtocolSwitched, ConnectionLost):
+ pass
+
+
+ def dispatchCommand(self, box):
+ """
+ A box with a _command key was received.
+
+ Dispatch it to a local handler call it.
+
+ @param proto: an AMP instance.
+ @param box: an AmpBox to be dispatched.
+ """
+ cmd = box[COMMAND]
+ responder = self.locator.locateResponder(cmd)
+ if responder is None:
+ return fail(RemoteAmpError(
+ UNHANDLED_ERROR_CODE,
+ "Unhandled Command: %r" % (cmd,),
+ False,
+ local=Failure(UnhandledCommand())))
+ return maybeDeferred(responder, box)
+
+
+
+class CommandLocator:
+ """
+ A L{CommandLocator} is a collection of responders to AMP L{Command}s, with
+ the help of the L{Command.responder} decorator.
+ """
+
+ class __metaclass__(type):
+ """
+ This metaclass keeps track of all of the Command.responder-decorated
+ methods defined since the last CommandLocator subclass was defined. It
+ assumes (usually correctly, but unfortunately not necessarily so) that
+ those commands responders were all declared as methods of the class
+ being defined. Note that this list can be incorrect if users use the
+ Command.responder decorator outside the context of a CommandLocator
+ class declaration.
+
+ Command responders defined on subclasses are given precedence over
+ those inherited from a base class.
+
+ The Command.responder decorator explicitly cooperates with this
+ metaclass.
+ """
+
+ _currentClassCommands = []
+ def __new__(cls, name, bases, attrs):
+ commands = cls._currentClassCommands[:]
+ cls._currentClassCommands[:] = []
+ cd = attrs['_commandDispatch'] = {}
+ subcls = type.__new__(cls, name, bases, attrs)
+ ancestors = list(subcls.__mro__[1:])
+ ancestors.reverse()
+ for ancestor in ancestors:
+ cd.update(getattr(ancestor, '_commandDispatch', {}))
+ for commandClass, responderFunc in commands:
+ cd[commandClass.commandName] = (commandClass, responderFunc)
+ if (bases and (
+ subcls.lookupFunction != CommandLocator.lookupFunction)):
+ def locateResponder(self, name):
+ warnings.warn(
+ "Override locateResponder, not lookupFunction.",
+ category=PendingDeprecationWarning,
+ stacklevel=2)
+ return self.lookupFunction(name)
+ subcls.locateResponder = locateResponder
+ return subcls
+
+
+ implements(IResponderLocator)
+
+
+ def _wrapWithSerialization(self, aCallable, command):
+ """
+ Wrap aCallable with its command's argument de-serialization
+ and result serialization logic.
+
+ @param aCallable: a callable with a 'command' attribute, designed to be
+ called with keyword arguments.
+
+ @param command: the command class whose serialization to use.
+
+ @return: a 1-arg callable which, when invoked with an AmpBox, will
+ deserialize the argument list and invoke appropriate user code for the
+ callable's command, returning a Deferred which fires with the result or
+ fails with an error.
+ """
+ def doit(box):
+ kw = command.parseArguments(box, self)
+ def checkKnownErrors(error):
+ key = error.trap(*command.allErrors)
+ code = command.allErrors[key]
+ desc = str(error.value)
+ return Failure(RemoteAmpError(
+ code, desc, key in command.fatalErrors, local=error))
+ def makeResponseFor(objects):
+ try:
+ return command.makeResponse(objects, self)
+ except:
+ # let's helpfully log this.
+ originalFailure = Failure()
+ raise BadLocalReturn(
+ "%r returned %r and %r could not serialize it" % (
+ aCallable,
+ objects,
+ command),
+ originalFailure)
+ return maybeDeferred(aCallable, **kw).addCallback(
+ makeResponseFor).addErrback(
+ checkKnownErrors)
+ return doit
+
+
+ def lookupFunction(self, name):
+ """
+ Deprecated synonym for L{locateResponder}
+ """
+ if self.__class__.lookupFunction != CommandLocator.lookupFunction:
+ return CommandLocator.locateResponder(self, name)
+ else:
+ warnings.warn("Call locateResponder, not lookupFunction.",
+ category=PendingDeprecationWarning,
+ stacklevel=2)
+ return self.locateResponder(name)
+
+
+ def locateResponder(self, name):
+ """
+ Locate a callable to invoke when executing the named command.
+
+ @param name: the normalized name (from the wire) of the command.
+
+ @return: a 1-argument function that takes a Box and returns a box or a
+ Deferred which fires a Box, for handling the command identified by the
+ given name, or None, if no appropriate responder can be found.
+ """
+ # Try to find a high-level method to invoke, and if we can't find one,
+ # fall back to a low-level one.
+ cd = self._commandDispatch
+ if name in cd:
+ commandClass, responderFunc = cd[name]
+ responderMethod = types.MethodType(
+ responderFunc, self, self.__class__)
+ return self._wrapWithSerialization(responderMethod, commandClass)
+
+
+
+class SimpleStringLocator(object):
+ """
+ Implement the L{locateResponder} method to do simple, string-based
+ dispatch.
+ """
+
+ implements(IResponderLocator)
+
+ baseDispatchPrefix = 'amp_'
+
+ def locateResponder(self, name):
+ """
+ Locate a callable to invoke when executing the named command.
+
+ @return: a function with the name C{"amp_" + name} on L{self}, or None
+ if no such function exists. This function will then be called with the
+ L{AmpBox} itself as an argument.
+
+ @param name: the normalized name (from the wire) of the command.
+ """
+ fName = self.baseDispatchPrefix + (name.upper())
+ return getattr(self, fName, None)
+
+
+
+PYTHON_KEYWORDS = [
+ 'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda',
+ 'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except',
+ 'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield',
+ 'def', 'finally', 'in', 'print']
+
+
+
+def _wireNameToPythonIdentifier(key):
+ """
+ (Private) Normalize an argument name from the wire for use with Python
+ code. If the return value is going to be a python keyword it will be
+ capitalized. If it contains any dashes they will be replaced with
+ underscores.
+
+ The rationale behind this method is that AMP should be an inherently
+ multi-language protocol, so message keys may contain all manner of bizarre
+ bytes. This is not a complete solution; there are still forms of arguments
+ that this implementation will be unable to parse. However, Python
+ identifiers share a huge raft of properties with identifiers from many
+ other languages, so this is a 'good enough' effort for now. We deal
+ explicitly with dashes because that is the most likely departure: Lisps
+ commonly use dashes to separate method names, so protocols initially
+ implemented in a lisp amp dialect may use dashes in argument or command
+ names.
+
+ @param key: a str, looking something like 'foo-bar-baz' or 'from'
+
+ @return: a str which is a valid python identifier, looking something like
+ 'foo_bar_baz' or 'From'.
+ """
+ lkey = key.replace("-", "_")
+ if lkey in PYTHON_KEYWORDS:
+ return lkey.title()
+ return lkey
+
+
+
+class Argument:
+ """
+ Base-class of all objects that take values from Amp packets and convert
+ them into objects for Python functions.
+
+ This implementation of L{IArgumentType} provides several higher-level
+ hooks for subclasses to override. See L{toString} and L{fromString}
+ which will be used to define the behavior of L{IArgumentType.toBox} and
+ L{IArgumentType.fromBox}, respectively.
+ """
+ implements(IArgumentType)
+
+ optional = False
+
+
+ def __init__(self, optional=False):
+ """
+ Create an Argument.
+
+ @param optional: a boolean indicating whether this argument can be
+ omitted in the protocol.
+ """
+ self.optional = optional
+
+
+ def retrieve(self, d, name, proto):
+ """
+ Retrieve the given key from the given dictionary, removing it if found.
+
+ @param d: a dictionary.
+
+ @param name: a key in L{d}.
+
+ @param proto: an instance of an AMP.
+
+ @raise KeyError: if I am not optional and no value was found.
+
+ @return: d[name].
+ """
+ if self.optional:
+ value = d.get(name)
+ if value is not None:
+ del d[name]
+ else:
+ value = d.pop(name)
+ return value
+
+
+ def fromBox(self, name, strings, objects, proto):
+ """
+ Populate an 'out' dictionary with mapping names to Python values
+ decoded from an 'in' AmpBox mapping strings to string values.
+
+ @param name: the argument name to retrieve
+ @type name: str
+
+ @param strings: The AmpBox to read string(s) from, a mapping of
+ argument names to string values.
+ @type strings: AmpBox
+
+ @param objects: The dictionary to write object(s) to, a mapping of
+ names to Python objects.
+ @type objects: dict
+
+ @param proto: an AMP instance.
+ """
+ st = self.retrieve(strings, name, proto)
+ nk = _wireNameToPythonIdentifier(name)
+ if self.optional and st is None:
+ objects[nk] = None
+ else:
+ objects[nk] = self.fromStringProto(st, proto)
+
+
+ def toBox(self, name, strings, objects, proto):
+ """
+ Populate an 'out' AmpBox with strings encoded from an 'in' dictionary
+ mapping names to Python values.
+
+ @param name: the argument name to retrieve
+ @type name: str
+
+ @param strings: The AmpBox to write string(s) to, a mapping of
+ argument names to string values.
+ @type strings: AmpBox
+
+ @param objects: The dictionary to read object(s) from, a mapping of
+ names to Python objects.
+
+ @type objects: dict
+
+ @param proto: the protocol we are converting for.
+ @type proto: AMP
+ """
+ obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto)
+ if self.optional and obj is None:
+ # strings[name] = None
+ pass
+ else:
+ strings[name] = self.toStringProto(obj, proto)
+
+
+ def fromStringProto(self, inString, proto):
+ """
+ Convert a string to a Python value.
+
+ @param inString: the string to convert.
+
+ @param proto: the protocol we are converting for.
+ @type proto: AMP
+
+ @return: a Python object.
+ """
+ return self.fromString(inString)
+
+
+ def toStringProto(self, inObject, proto):
+ """
+ Convert a Python object to a string.
+
+ @param inObject: the object to convert.
+
+ @param proto: the protocol we are converting for.
+ @type proto: AMP
+ """
+ return self.toString(inObject)
+
+
+ def fromString(self, inString):
+ """
+ Convert a string to a Python object. Subclasses must implement this.
+
+ @param inString: the string to convert.
+ @type inString: str
+
+ @return: the decoded value from inString
+ """
+
+
+ def toString(self, inObject):
+ """
+ Convert a Python object into a string for passing over the network.
+
+ @param inObject: an object of the type that this Argument is intended
+ to deal with.
+
+ @return: the wire encoding of inObject
+ @rtype: str
+ """
+
+
+
+class Integer(Argument):
+ """
+ Encode any integer values of any size on the wire as the string
+ representation.
+
+ Example: C{123} becomes C{"123"}
+ """
+ fromString = int
+ def toString(self, inObject):
+ return str(int(inObject))
+
+
+
+class String(Argument):
+ """
+ Don't do any conversion at all; just pass through 'str'.
+ """
+ def toString(self, inObject):
+ return inObject
+
+
+ def fromString(self, inString):
+ return inString
+
+
+
+class Float(Argument):
+ """
+ Encode floating-point values on the wire as their repr.
+ """
+ fromString = float
+ toString = repr
+
+
+
+class Boolean(Argument):
+ """
+ Encode True or False as "True" or "False" on the wire.
+ """
+ def fromString(self, inString):
+ if inString == 'True':
+ return True
+ elif inString == 'False':
+ return False
+ else:
+ raise TypeError("Bad boolean value: %r" % (inString,))
+
+
+ def toString(self, inObject):
+ if inObject:
+ return 'True'
+ else:
+ return 'False'
+
+
+
+class Unicode(String):
+ """
+ Encode a unicode string on the wire as UTF-8.
+ """
+
+ def toString(self, inObject):
+ # assert isinstance(inObject, unicode)
+ return String.toString(self, inObject.encode('utf-8'))
+
+
+ def fromString(self, inString):
+ # assert isinstance(inString, str)
+ return String.fromString(self, inString).decode('utf-8')
+
+
+
+class Path(Unicode):
+ """
+ Encode and decode L{filepath.FilePath} instances as paths on the wire.
+
+ This is really intended for use with subprocess communication tools:
+ exchanging pathnames on different machines over a network is not generally
+ meaningful, but neither is it disallowed; you can use this to communicate
+ about NFS paths, for example.
+ """
+ def fromString(self, inString):
+ return filepath.FilePath(Unicode.fromString(self, inString))
+
+
+ def toString(self, inObject):
+ return Unicode.toString(self, inObject.path)
+
+
+
+class ListOf(Argument):
+ """
+ Encode and decode lists of instances of a single other argument type.
+
+ For example, if you want to pass::
+
+ [3, 7, 9, 15]
+
+ You can create an argument like this::
+
+ ListOf(Integer())
+
+ The serialized form of the entire list is subject to the limit imposed by
+ L{MAX_VALUE_LENGTH}. List elements are represented as 16-bit length
+ prefixed strings. The argument type passed to the L{ListOf} initializer is
+ responsible for producing the serialized form of each element.
+
+ @ivar elementType: The L{Argument} instance used to encode and decode list
+ elements (note, not an arbitrary L{IArgument} implementation:
+ arguments must be implemented using only the C{fromString} and
+ C{toString} methods, not the C{fromBox} and C{toBox} methods).
+
+ @param optional: a boolean indicating whether this argument can be
+ omitted in the protocol.
+
+ @since: 10.0
+ """
+ def __init__(self, elementType, optional=False):
+ self.elementType = elementType
+ Argument.__init__(self, optional)
+
+
+ def fromString(self, inString):
+ """
+ Convert the serialized form of a list of instances of some type back
+ into that list.
+ """
+ strings = []
+ parser = Int16StringReceiver()
+ parser.stringReceived = strings.append
+ parser.dataReceived(inString)
+ return map(self.elementType.fromString, strings)
+
+
+ def toString(self, inObject):
+ """
+ Serialize the given list of objects to a single string.
+ """
+ strings = []
+ for obj in inObject:
+ serialized = self.elementType.toString(obj)
+ strings.append(pack('!H', len(serialized)))
+ strings.append(serialized)
+ return ''.join(strings)
+
+
+
+class AmpList(Argument):
+ """
+ Convert a list of dictionaries into a list of AMP boxes on the wire.
+
+ For example, if you want to pass::
+
+ [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}]
+
+ You might use an AmpList like this in your arguments or response list::
+
+ AmpList([('a', Integer()),
+ ('b', Unicode())])
+ """
+ def __init__(self, subargs, optional=False):
+ """
+ Create an AmpList.
+
+ @param subargs: a list of 2-tuples of ('name', argument) describing the
+ schema of the dictionaries in the sequence of amp boxes.
+
+ @param optional: a boolean indicating whether this argument can be
+ omitted in the protocol.
+ """
+ self.subargs = subargs
+ Argument.__init__(self, optional)
+
+
+ def fromStringProto(self, inString, proto):
+ boxes = parseString(inString)
+ values = [_stringsToObjects(box, self.subargs, proto)
+ for box in boxes]
+ return values
+
+
+ def toStringProto(self, inObject, proto):
+ return ''.join([_objectsToStrings(
+ objects, self.subargs, Box(), proto
+ ).serialize() for objects in inObject])
+
+
+
+class Descriptor(Integer):
+ """
+ Encode and decode file descriptors for exchange over a UNIX domain socket.
+
+ This argument type requires an AMP connection set up over an
+ L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>} provider (for
+ example, the kind of connection created by
+ L{IReactorUNIX.connectUNIX<twisted.internet.interfaces.IReactorUNIX.connectUNIX>}
+ and L{UNIXClientEndpoint<twisted.internet.endpoints.UNIXClientEndpoint>}).
+
+ There is no correspondence between the integer value of the file descriptor
+ on the sending and receiving sides, therefore an alternate approach is taken
+ to matching up received descriptors with particular L{Descriptor}
+ parameters. The argument is encoded to an ordinal (unique per connection)
+ for inclusion in the AMP command or response box. The descriptor itself is
+ sent using
+ L{IUNIXTransport.sendFileDescriptor<twisted.internet.interfaces.IUNIXTransport.sendFileDescriptor>}.
+ The receiver uses the order in which file descriptors are received and the
+ ordinal value to come up with the received copy of the descriptor.
+ """
+ def fromStringProto(self, inString, proto):
+ """
+ Take a unique identifier associated with a file descriptor which must
+ have been received by now and use it to look up that descriptor in a
+ dictionary where they are kept.
+
+ @param inString: The base representation (as a byte string) of an
+ ordinal indicating which file descriptor corresponds to this usage
+ of this argument.
+ @type inString: C{str}
+
+ @param proto: The protocol used to receive this descriptor. This
+ protocol must be connected via a transport providing
+ L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
+ @type proto: L{BinaryBoxProtocol}
+
+ @return: The file descriptor represented by C{inString}.
+ @rtype: C{int}
+ """
+ return proto._getDescriptor(int(inString))
+
+
+ def toStringProto(self, inObject, proto):
+ """
+ Send C{inObject}, an integer file descriptor, over C{proto}'s connection
+ and return a unique identifier which will allow the receiver to
+ associate the file descriptor with this argument.
+
+ @param inObject: A file descriptor to duplicate over an AMP connection
+ as the value for this argument.
+ @type inObject: C{int}
+
+ @param proto: The protocol which will be used to send this descriptor.
+ This protocol must be connected via a transport providing
+ L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
+
+ @return: A byte string which can be used by the receiver to reconstruct
+ the file descriptor.
+ @type: C{str}
+ """
+ identifier = proto._sendFileDescriptor(inObject)
+ outString = Integer.toStringProto(self, identifier, proto)
+ return outString
+
+
+
+class Command:
+ """
+ Subclass me to specify an AMP Command.
+
+ @cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance),
+ specifying the names and values of the parameters which are required for
+ this command.
+
+ @cvar response: A list like L{arguments}, but instead used for the return
+ value.
+
+ @cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags
+ for errors represented as L{str}s. Responders which raise keys from this
+ dictionary will have the error translated to the corresponding tag on the
+ wire. Invokers which receive Deferreds from invoking this command with
+ L{AMP.callRemote} will potentially receive Failures with keys from this
+ mapping as their value. This mapping is inherited; if you declare a
+ command which handles C{FooError} as 'FOO_ERROR', then subclass it and
+ specify C{BarError} as 'BAR_ERROR', responders to the subclass may raise
+ either C{FooError} or C{BarError}, and invokers must be able to deal with
+ either of those exceptions.
+
+ @cvar fatalErrors: like 'errors', but errors in this list will always
+ terminate the connection, despite being of a recognizable error type.
+
+ @cvar commandType: The type of Box used to issue commands; useful only for
+ protocol-modifying behavior like startTLS or protocol switching. Defaults
+ to a plain vanilla L{Box}.
+
+ @cvar responseType: The type of Box used to respond to this command; only
+ useful for protocol-modifying behavior like startTLS or protocol switching.
+ Defaults to a plain vanilla L{Box}.
+
+ @ivar requiresAnswer: a boolean; defaults to True. Set it to False on your
+ subclass if you want callRemote to return None. Note: this is a hint only
+ to the client side of the protocol. The return-type of a command responder
+ method must always be a dictionary adhering to the contract specified by
+ L{response}, because clients are always free to request a response if they
+ want one.
+ """
+
+ class __metaclass__(type):
+ """
+ Metaclass hack to establish reverse-mappings for 'errors' and
+ 'fatalErrors' as class vars.
+ """
+ def __new__(cls, name, bases, attrs):
+ reverseErrors = attrs['reverseErrors'] = {}
+ er = attrs['allErrors'] = {}
+ if 'commandName' not in attrs:
+ attrs['commandName'] = name
+ newtype = type.__new__(cls, name, bases, attrs)
+ errors = {}
+ fatalErrors = {}
+ accumulateClassDict(newtype, 'errors', errors)
+ accumulateClassDict(newtype, 'fatalErrors', fatalErrors)
+ for v, k in errors.iteritems():
+ reverseErrors[k] = v
+ er[v] = k
+ for v, k in fatalErrors.iteritems():
+ reverseErrors[k] = v
+ er[v] = k
+ return newtype
+
+ arguments = []
+ response = []
+ extra = []
+ errors = {}
+ fatalErrors = {}
+
+ commandType = Box
+ responseType = Box
+
+ requiresAnswer = True
+
+
+ def __init__(self, **kw):
+ """
+ Create an instance of this command with specified values for its
+ parameters.
+
+ @param kw: a dict containing an appropriate value for each name
+ specified in the L{arguments} attribute of my class.
+
+ @raise InvalidSignature: if you forgot any required arguments.
+ """
+ self.structured = kw
+ givenArgs = kw.keys()
+ forgotten = []
+ for name, arg in self.arguments:
+ pythonName = _wireNameToPythonIdentifier(name)
+ if pythonName not in givenArgs and not arg.optional:
+ forgotten.append(pythonName)
+ if forgotten:
+ raise InvalidSignature("forgot %s for %s" % (
+ ', '.join(forgotten), self.commandName))
+ forgotten = []
+
+
+ def makeResponse(cls, objects, proto):
+ """
+ Serialize a mapping of arguments using this L{Command}'s
+ response schema.
+
+ @param objects: a dict with keys matching the names specified in
+ self.response, having values of the types that the Argument objects in
+ self.response can format.
+
+ @param proto: an L{AMP}.
+
+ @return: an L{AmpBox}.
+ """
+ try:
+ responseType = cls.responseType()
+ except:
+ return fail()
+ return _objectsToStrings(objects, cls.response, responseType, proto)
+ makeResponse = classmethod(makeResponse)
+
+
+ def makeArguments(cls, objects, proto):
+ """
+ Serialize a mapping of arguments using this L{Command}'s
+ argument schema.
+
+ @param objects: a dict with keys similar to the names specified in
+ self.arguments, having values of the types that the Argument objects in
+ self.arguments can parse.
+
+ @param proto: an L{AMP}.
+
+ @return: An instance of this L{Command}'s C{commandType}.
+ """
+ allowedNames = set()
+ for (argName, ignored) in cls.arguments:
+ allowedNames.add(_wireNameToPythonIdentifier(argName))
+
+ for intendedArg in objects:
+ if intendedArg not in allowedNames:
+ raise InvalidSignature(
+ "%s is not a valid argument" % (intendedArg,))
+ return _objectsToStrings(objects, cls.arguments, cls.commandType(),
+ proto)
+ makeArguments = classmethod(makeArguments)
+
+
+ def parseResponse(cls, box, protocol):
+ """
+ Parse a mapping of serialized arguments using this
+ L{Command}'s response schema.
+
+ @param box: A mapping of response-argument names to the
+ serialized forms of those arguments.
+ @param protocol: The L{AMP} protocol.
+
+ @return: A mapping of response-argument names to the parsed
+ forms.
+ """
+ return _stringsToObjects(box, cls.response, protocol)
+ parseResponse = classmethod(parseResponse)
+
+
+ def parseArguments(cls, box, protocol):
+ """
+ Parse a mapping of serialized arguments using this
+ L{Command}'s argument schema.
+
+ @param box: A mapping of argument names to the seralized forms
+ of those arguments.
+ @param protocol: The L{AMP} protocol.
+
+ @return: A mapping of argument names to the parsed forms.
+ """
+ return _stringsToObjects(box, cls.arguments, protocol)
+ parseArguments = classmethod(parseArguments)
+
+
+ def responder(cls, methodfunc):
+ """
+ Declare a method to be a responder for a particular command.
+
+ This is a decorator.
+
+ Use like so::
+
+ class MyCommand(Command):
+ arguments = [('a', ...), ('b', ...)]
+
+ class MyProto(AMP):
+ def myFunMethod(self, a, b):
+ ...
+ MyCommand.responder(myFunMethod)
+
+ Notes: Although decorator syntax is not used within Twisted, this
+ function returns its argument and is therefore safe to use with
+ decorator syntax.
+
+ This is not thread safe. Don't declare AMP subclasses in other
+ threads. Don't declare responders outside the scope of AMP subclasses;
+ the behavior is undefined.
+
+ @param methodfunc: A function which will later become a method, which
+ has a keyword signature compatible with this command's L{argument} list
+ and returns a dictionary with a set of keys compatible with this
+ command's L{response} list.
+
+ @return: the methodfunc parameter.
+ """
+ CommandLocator._currentClassCommands.append((cls, methodfunc))
+ return methodfunc
+ responder = classmethod(responder)
+
+
+ # Our only instance method
+ def _doCommand(self, proto):
+ """
+ Encode and send this Command to the given protocol.
+
+ @param proto: an AMP, representing the connection to send to.
+
+ @return: a Deferred which will fire or error appropriately when the
+ other side responds to the command (or error if the connection is lost
+ before it is responded to).
+ """
+
+ def _massageError(error):
+ error.trap(RemoteAmpError)
+ rje = error.value
+ errorType = self.reverseErrors.get(rje.errorCode,
+ UnknownRemoteError)
+ return Failure(errorType(rje.description))
+
+ d = proto._sendBoxCommand(self.commandName,
+ self.makeArguments(self.structured, proto),
+ self.requiresAnswer)
+
+ if self.requiresAnswer:
+ d.addCallback(self.parseResponse, proto)
+ d.addErrback(_massageError)
+
+ return d
+
+
+
+class _NoCertificate:
+ """
+ This is for peers which don't want to use a local certificate. Used by
+ AMP because AMP's internal language is all about certificates and this
+ duck-types in the appropriate place; this API isn't really stable though,
+ so it's not exposed anywhere public.
+
+ For clients, it will use ephemeral DH keys, or whatever the default is for
+ certificate-less clients in OpenSSL. For servers, it will generate a
+ temporary self-signed certificate with garbage values in the DN and use
+ that.
+ """
+
+ def __init__(self, client):
+ """
+ Create a _NoCertificate which either is or isn't for the client side of
+ the connection.
+
+ @param client: True if we are a client and should truly have no
+ certificate and be anonymous, False if we are a server and actually
+ have to generate a temporary certificate.
+
+ @type client: bool
+ """
+ self.client = client
+
+
+ def options(self, *authorities):
+ """
+ Behaves like L{twisted.internet.ssl.PrivateCertificate.options}().
+ """
+ if not self.client:
+ # do some crud with sslverify to generate a temporary self-signed
+ # certificate. This is SLOOOWWWWW so it is only in the absolute
+ # worst, most naive case.
+
+ # We have to do this because OpenSSL will not let both the server
+ # and client be anonymous.
+ sharedDN = DN(CN='TEMPORARY CERTIFICATE')
+ key = KeyPair.generate()
+ cr = key.certificateRequest(sharedDN)
+ sscrd = key.signCertificateRequest(sharedDN, cr, lambda dn: True, 1)
+ cert = key.newCertificate(sscrd)
+ return cert.options(*authorities)
+ options = dict()
+ if authorities:
+ options.update(dict(verify=True,
+ requireCertificate=True,
+ caCerts=[auth.original for auth in authorities]))
+ occo = CertificateOptions(**options)
+ return occo
+
+
+
+class _TLSBox(AmpBox):
+ """
+ I am an AmpBox that, upon being sent, initiates a TLS connection.
+ """
+ __slots__ = []
+
+ def __init__(self):
+ if ssl is None:
+ raise RemoteAmpError("TLS_ERROR", "TLS not available")
+ AmpBox.__init__(self)
+
+
+ def _keyprop(k, default):
+ return property(lambda self: self.get(k, default))
+
+
+ # These properties are described in startTLS
+ certificate = _keyprop('tls_localCertificate', _NoCertificate(False))
+ verify = _keyprop('tls_verifyAuthorities', None)
+
+ def _sendTo(self, proto):
+ """
+ Send my encoded value to the protocol, then initiate TLS.
+ """
+ ab = AmpBox(self)
+ for k in ['tls_localCertificate',
+ 'tls_verifyAuthorities']:
+ ab.pop(k, None)
+ ab._sendTo(proto)
+ proto._startTLS(self.certificate, self.verify)
+
+
+
+class _LocalArgument(String):
+ """
+ Local arguments are never actually relayed across the wire. This is just a
+ shim so that StartTLS can pretend to have some arguments: if arguments
+ acquire documentation properties, replace this with something nicer later.
+ """
+
+ def fromBox(self, name, strings, objects, proto):
+ pass
+
+
+
+class StartTLS(Command):
+ """
+ Use, or subclass, me to implement a command that starts TLS.
+
+ Callers of StartTLS may pass several special arguments, which affect the
+ TLS negotiation:
+
+ - tls_localCertificate: This is a
+ twisted.internet.ssl.PrivateCertificate which will be used to secure
+ the side of the connection it is returned on.
+
+ - tls_verifyAuthorities: This is a list of
+ twisted.internet.ssl.Certificate objects that will be used as the
+ certificate authorities to verify our peer's certificate.
+
+ Each of those special parameters may also be present as a key in the
+ response dictionary.
+ """
+
+ arguments = [("tls_localCertificate", _LocalArgument(optional=True)),
+ ("tls_verifyAuthorities", _LocalArgument(optional=True))]
+
+ response = [("tls_localCertificate", _LocalArgument(optional=True)),
+ ("tls_verifyAuthorities", _LocalArgument(optional=True))]
+
+ responseType = _TLSBox
+
+ def __init__(self, **kw):
+ """
+ Create a StartTLS command. (This is private. Use AMP.callRemote.)
+
+ @param tls_localCertificate: the PrivateCertificate object to use to
+ secure the connection. If it's None, or unspecified, an ephemeral DH
+ key is used instead.
+
+ @param tls_verifyAuthorities: a list of Certificate objects which
+ represent root certificates to verify our peer with.
+ """
+ if ssl is None:
+ raise RuntimeError("TLS not available.")
+ self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True))
+ self.authorities = kw.pop('tls_verifyAuthorities', None)
+ Command.__init__(self, **kw)
+
+
+ def _doCommand(self, proto):
+ """
+ When a StartTLS command is sent, prepare to start TLS, but don't actually
+ do it; wait for the acknowledgement, then initiate the TLS handshake.
+ """
+ d = Command._doCommand(self, proto)
+ proto._prepareTLS(self.certificate, self.authorities)
+ # XXX before we get back to user code we are going to start TLS...
+ def actuallystart(response):
+ proto._startTLS(self.certificate, self.authorities)
+ return response
+ d.addCallback(actuallystart)
+ return d
+
+
+
+class ProtocolSwitchCommand(Command):
+ """
+ Use this command to switch from something Amp-derived to a different
+ protocol mid-connection. This can be useful to use amp as the
+ connection-startup negotiation phase. Since TLS is a different layer
+ entirely, you can use Amp to negotiate the security parameters of your
+ connection, then switch to a different protocol, and the connection will
+ remain secured.
+ """
+
+ def __init__(self, _protoToSwitchToFactory, **kw):
+ """
+ Create a ProtocolSwitchCommand.
+
+ @param _protoToSwitchToFactory: a ProtocolFactory which will generate
+ the Protocol to switch to.
+
+ @param kw: Keyword arguments, encoded and handled normally as
+ L{Command} would.
+ """
+
+ self.protoToSwitchToFactory = _protoToSwitchToFactory
+ super(ProtocolSwitchCommand, self).__init__(**kw)
+
+
+ def makeResponse(cls, innerProto, proto):
+ return _SwitchBox(innerProto)
+ makeResponse = classmethod(makeResponse)
+
+
+ def _doCommand(self, proto):
+ """
+ When we emit a ProtocolSwitchCommand, lock the protocol, but don't actually
+ switch to the new protocol unless an acknowledgement is received. If
+ an error is received, switch back.
+ """
+ d = super(ProtocolSwitchCommand, self)._doCommand(proto)
+ proto._lockForSwitch()
+ def switchNow(ign):
+ innerProto = self.protoToSwitchToFactory.buildProtocol(
+ proto.transport.getPeer())
+ proto._switchTo(innerProto, self.protoToSwitchToFactory)
+ return ign
+ def handle(ign):
+ proto._unlockFromSwitch()
+ self.protoToSwitchToFactory.clientConnectionFailed(
+ None, Failure(CONNECTION_LOST))
+ return ign
+ return d.addCallbacks(switchNow, handle)
+
+
+
+class _DescriptorExchanger(object):
+ """
+ L{_DescriptorExchanger} is a mixin for L{BinaryBoxProtocol} which adds
+ support for receiving file descriptors, a feature offered by
+ L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
+
+ @ivar _descriptors: Temporary storage for all file descriptors received.
+ Values in this dictionary are the file descriptors (as integers). Keys
+ in this dictionary are ordinals giving the order in which each
+ descriptor was received. The ordering information is used to allow
+ L{Descriptor} to determine which is the correct descriptor for any
+ particular usage of that argument type.
+ @type _descriptors: C{dict}
+
+ @ivar _sendingDescriptorCounter: A no-argument callable which returns the
+ ordinals, starting from 0. This is used to construct values for
+ C{_sendFileDescriptor}.
+
+ @ivar _receivingDescriptorCounter: A no-argument callable which returns the
+ ordinals, starting from 0. This is used to construct values for
+ C{fileDescriptorReceived}.
+ """
+ implements(IFileDescriptorReceiver)
+
+ def __init__(self):
+ self._descriptors = {}
+ self._getDescriptor = self._descriptors.pop
+ self._sendingDescriptorCounter = count().next
+ self._receivingDescriptorCounter = count().next
+
+
+ def _sendFileDescriptor(self, descriptor):
+ """
+ Assign and return the next ordinal to the given descriptor after sending
+ the descriptor over this protocol's transport.
+ """
+ self.transport.sendFileDescriptor(descriptor)
+ return self._sendingDescriptorCounter()
+
+
+ def fileDescriptorReceived(self, descriptor):
+ """
+ Collect received file descriptors to be claimed later by L{Descriptor}.
+
+ @param descriptor: The received file descriptor.
+ @type descriptor: C{int}
+ """
+ self._descriptors[self._receivingDescriptorCounter()] = descriptor
+
+
+
+class BinaryBoxProtocol(StatefulStringProtocol, Int16StringReceiver,
+ _DescriptorExchanger):
+ """
+ A protocol for receiving L{AmpBox}es - key/value pairs - via length-prefixed
+ strings. A box is composed of:
+
+ - any number of key-value pairs, described by:
+ - a 2-byte network-endian packed key length (of which the first
+ byte must be null, and the second must be non-null: i.e. the
+ value of the length must be 1-255)
+ - a key, comprised of that many bytes
+ - a 2-byte network-endian unsigned value length (up to the maximum
+ of 65535)
+ - a value, comprised of that many bytes
+ - 2 null bytes
+
+ In other words, an even number of strings prefixed with packed unsigned
+ 16-bit integers, and then a 0-length string to indicate the end of the box.
+
+ This protocol also implements 2 extra private bits of functionality related
+ to the byte boundaries between messages; it can start TLS between two given
+ boxes or switch to an entirely different protocol. However, due to some
+ tricky elements of the implementation, the public interface to this
+ functionality is L{ProtocolSwitchCommand} and L{StartTLS}.
+
+ @ivar _keyLengthLimitExceeded: A flag which is only true when the
+ connection is being closed because a key length prefix which was longer
+ than allowed by the protocol was received.
+
+ @ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived}
+ method will be invoked for each L{AmpBox} that is received.
+ """
+
+ implements(IBoxSender)
+
+ _justStartedTLS = False
+ _startingTLSBuffer = None
+ _locked = False
+ _currentKey = None
+ _currentBox = None
+
+ _keyLengthLimitExceeded = False
+
+ hostCertificate = None
+ noPeerCertificate = False # for tests
+ innerProtocol = None
+ innerProtocolClientFactory = None
+
+ def __init__(self, boxReceiver):
+ _DescriptorExchanger.__init__(self)
+ self.boxReceiver = boxReceiver
+
+
+ def _switchTo(self, newProto, clientFactory=None):
+ """
+ Switch this BinaryBoxProtocol's transport to a new protocol. You need
+ to do this 'simultaneously' on both ends of a connection; the easiest
+ way to do this is to use a subclass of ProtocolSwitchCommand.
+
+ @param newProto: the new protocol instance to switch to.
+
+ @param clientFactory: the ClientFactory to send the
+ L{clientConnectionLost} notification to.
+ """
+ # All the data that Int16Receiver has not yet dealt with belongs to our
+ # new protocol: luckily it's keeping that in a handy (although
+ # ostensibly internal) variable for us:
+ newProtoData = self.recvd
+ # We're quite possibly in the middle of a 'dataReceived' loop in
+ # Int16StringReceiver: let's make sure that the next iteration, the
+ # loop will break and not attempt to look at something that isn't a
+ # length prefix.
+ self.recvd = ''
+ # Finally, do the actual work of setting up the protocol and delivering
+ # its first chunk of data, if one is available.
+ self.innerProtocol = newProto
+ self.innerProtocolClientFactory = clientFactory
+ newProto.makeConnection(self.transport)
+ if newProtoData:
+ newProto.dataReceived(newProtoData)
+
+
+ def sendBox(self, box):
+ """
+ Send a amp.Box to my peer.
+
+ Note: transport.write is never called outside of this method.
+
+ @param box: an AmpBox.
+
+ @raise ProtocolSwitched: if the protocol has previously been switched.
+
+ @raise ConnectionLost: if the connection has previously been lost.
+ """
+ if self._locked:
+ raise ProtocolSwitched(
+ "This connection has switched: no AMP traffic allowed.")
+ if self.transport is None:
+ raise ConnectionLost()
+ if self._startingTLSBuffer is not None:
+ self._startingTLSBuffer.append(box)
+ else:
+ self.transport.write(box.serialize())
+
+
+ def makeConnection(self, transport):
+ """
+ Notify L{boxReceiver} that it is about to receive boxes from this
+ protocol by invoking L{startReceivingBoxes}.
+ """
+ self.transport = transport
+ self.boxReceiver.startReceivingBoxes(self)
+ self.connectionMade()
+
+
+ def dataReceived(self, data):
+ """
+ Either parse incoming data as L{AmpBox}es or relay it to our nested
+ protocol.
+ """
+ if self._justStartedTLS:
+ self._justStartedTLS = False
+ # If we already have an inner protocol, then we don't deliver data to
+ # the protocol parser any more; we just hand it off.
+ if self.innerProtocol is not None:
+ self.innerProtocol.dataReceived(data)
+ return
+ return Int16StringReceiver.dataReceived(self, data)
+
+
+ def connectionLost(self, reason):
+ """
+ The connection was lost; notify any nested protocol.
+ """
+ if self.innerProtocol is not None:
+ self.innerProtocol.connectionLost(reason)
+ if self.innerProtocolClientFactory is not None:
+ self.innerProtocolClientFactory.clientConnectionLost(None, reason)
+ if self._keyLengthLimitExceeded:
+ failReason = Failure(TooLong(True, False, None, None))
+ elif reason.check(ConnectionClosed) and self._justStartedTLS:
+ # We just started TLS and haven't received any data. This means
+ # the other connection didn't like our cert (although they may not
+ # have told us why - later Twisted should make 'reason' into a TLS
+ # error.)
+ failReason = PeerVerifyError(
+ "Peer rejected our certificate for an unknown reason.")
+ else:
+ failReason = reason
+ self.boxReceiver.stopReceivingBoxes(failReason)
+
+
+ # The longest key allowed
+ _MAX_KEY_LENGTH = 255
+
+ # The longest value allowed (this is somewhat redundant, as longer values
+ # cannot be encoded - ah well).
+ _MAX_VALUE_LENGTH = 65535
+
+ # The first thing received is a key.
+ MAX_LENGTH = _MAX_KEY_LENGTH
+
+ def proto_init(self, string):
+ """
+ String received in the 'init' state.
+ """
+ self._currentBox = AmpBox()
+ return self.proto_key(string)
+
+
+ def proto_key(self, string):
+ """
+ String received in the 'key' state. If the key is empty, a complete
+ box has been received.
+ """
+ if string:
+ self._currentKey = string
+ self.MAX_LENGTH = self._MAX_VALUE_LENGTH
+ return 'value'
+ else:
+ self.boxReceiver.ampBoxReceived(self._currentBox)
+ self._currentBox = None
+ return 'init'
+
+
+ def proto_value(self, string):
+ """
+ String received in the 'value' state.
+ """
+ self._currentBox[self._currentKey] = string
+ self._currentKey = None
+ self.MAX_LENGTH = self._MAX_KEY_LENGTH
+ return 'key'
+
+
+ def lengthLimitExceeded(self, length):
+ """
+ The key length limit was exceeded. Disconnect the transport and make
+ sure a meaningful exception is reported.
+ """
+ self._keyLengthLimitExceeded = True
+ self.transport.loseConnection()
+
+
+ def _lockForSwitch(self):
+ """
+ Lock this binary protocol so that no further boxes may be sent. This
+ is used when sending a request to switch underlying protocols. You
+ probably want to subclass ProtocolSwitchCommand rather than calling
+ this directly.
+ """
+ self._locked = True
+
+
+ def _unlockFromSwitch(self):
+ """
+ Unlock this locked binary protocol so that further boxes may be sent
+ again. This is used after an attempt to switch protocols has failed
+ for some reason.
+ """
+ if self.innerProtocol is not None:
+ raise ProtocolSwitched("Protocol already switched. Cannot unlock.")
+ self._locked = False
+
+
+ def _prepareTLS(self, certificate, verifyAuthorities):
+ """
+ Used by StartTLSCommand to put us into the state where we don't
+ actually send things that get sent, instead we buffer them. see
+ L{_sendBox}.
+ """
+ self._startingTLSBuffer = []
+ if self.hostCertificate is not None:
+ raise OnlyOneTLS(
+ "Previously authenticated connection between %s and %s "
+ "is trying to re-establish as %s" % (
+ self.hostCertificate,
+ self.peerCertificate,
+ (certificate, verifyAuthorities)))
+
+
+ def _startTLS(self, certificate, verifyAuthorities):
+ """
+ Used by TLSBox to initiate the SSL handshake.
+
+ @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for
+ use locally.
+
+ @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances
+ representing certificate authorities which will verify our peer.
+ """
+ self.hostCertificate = certificate
+ self._justStartedTLS = True
+ if verifyAuthorities is None:
+ verifyAuthorities = ()
+ self.transport.startTLS(certificate.options(*verifyAuthorities))
+ stlsb = self._startingTLSBuffer
+ if stlsb is not None:
+ self._startingTLSBuffer = None
+ for box in stlsb:
+ self.sendBox(box)
+
+
+ def _getPeerCertificate(self):
+ if self.noPeerCertificate:
+ return None
+ return Certificate.peerFromTransport(self.transport)
+ peerCertificate = property(_getPeerCertificate)
+
+
+ def unhandledError(self, failure):
+ """
+ The buck stops here. This error was completely unhandled, time to
+ terminate the connection.
+ """
+ log.err(
+ failure,
+ "Amp server or network failure unhandled by client application. "
+ "Dropping connection! To avoid, add errbacks to ALL remote "
+ "commands!")
+ if self.transport is not None:
+ self.transport.loseConnection()
+
+
+ def _defaultStartTLSResponder(self):
+ """
+ The default TLS responder doesn't specify any certificate or anything.
+
+ From a security perspective, it's little better than a plain-text
+ connection - but it is still a *bit* better, so it's included for
+ convenience.
+
+ You probably want to override this by providing your own StartTLS.responder.
+ """
+ return {}
+ StartTLS.responder(_defaultStartTLSResponder)
+
+
+
+class AMP(BinaryBoxProtocol, BoxDispatcher,
+ CommandLocator, SimpleStringLocator):
+ """
+ This protocol is an AMP connection. See the module docstring for protocol
+ details.
+ """
+
+ _ampInitialized = False
+
+ def __init__(self, boxReceiver=None, locator=None):
+ # For backwards compatibility. When AMP did not separate parsing logic
+ # (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and
+ # command routing (L{CommandLocator}), it did not have a constructor.
+ # Now it does, so old subclasses might have defined their own that did
+ # not upcall. If this flag isn't set, we'll call the constructor in
+ # makeConnection before anything actually happens.
+ self._ampInitialized = True
+ if boxReceiver is None:
+ boxReceiver = self
+ if locator is None:
+ locator = self
+ BoxDispatcher.__init__(self, locator)
+ BinaryBoxProtocol.__init__(self, boxReceiver)
+
+
+ def locateResponder(self, name):
+ """
+ Unify the implementations of L{CommandLocator} and
+ L{SimpleStringLocator} to perform both kinds of dispatch, preferring
+ L{CommandLocator}.
+ """
+ firstResponder = CommandLocator.locateResponder(self, name)
+ if firstResponder is not None:
+ return firstResponder
+ secondResponder = SimpleStringLocator.locateResponder(self, name)
+ return secondResponder
+
+
+ def __repr__(self):
+ """
+ A verbose string representation which gives us information about this
+ AMP connection.
+ """
+ if self.innerProtocol is not None:
+ innerRepr = ' inner %r' % (self.innerProtocol,)
+ else:
+ innerRepr = ''
+ return '<%s%s at 0x%x>' % (
+ self.__class__.__name__, innerRepr, unsignedID(self))
+
+
+ def makeConnection(self, transport):
+ """
+ Emit a helpful log message when the connection is made.
+ """
+ if not self._ampInitialized:
+ # See comment in the constructor re: backward compatibility. I
+ # should probably emit a deprecation warning here.
+ AMP.__init__(self)
+ # Save these so we can emit a similar log message in L{connectionLost}.
+ self._transportPeer = transport.getPeer()
+ self._transportHost = transport.getHost()
+ log.msg("%s connection established (HOST:%s PEER:%s)" % (
+ self.__class__.__name__,
+ self._transportHost,
+ self._transportPeer))
+ BinaryBoxProtocol.makeConnection(self, transport)
+
+
+ def connectionLost(self, reason):
+ """
+ Emit a helpful log message when the connection is lost.
+ """
+ log.msg("%s connection lost (HOST:%s PEER:%s)" %
+ (self.__class__.__name__,
+ self._transportHost,
+ self._transportPeer))
+ BinaryBoxProtocol.connectionLost(self, reason)
+ self.transport = None
+
+
+
+class _ParserHelper:
+ """
+ A box receiver which records all boxes received.
+ """
+ def __init__(self):
+ self.boxes = []
+
+
+ def getPeer(self):
+ return 'string'
+
+
+ def getHost(self):
+ return 'string'
+
+ disconnecting = False
+
+
+ def startReceivingBoxes(self, sender):
+ """
+ No initialization is required.
+ """
+
+
+ def ampBoxReceived(self, box):
+ self.boxes.append(box)
+
+
+ # Synchronous helpers
+ def parse(cls, fileObj):
+ """
+ Parse some amp data stored in a file.
+
+ @param fileObj: a file-like object.
+
+ @return: a list of AmpBoxes encoded in the given file.
+ """
+ parserHelper = cls()
+ bbp = BinaryBoxProtocol(boxReceiver=parserHelper)
+ bbp.makeConnection(parserHelper)
+ bbp.dataReceived(fileObj.read())
+ return parserHelper.boxes
+ parse = classmethod(parse)
+
+
+ def parseString(cls, data):
+ """
+ Parse some amp data stored in a string.
+
+ @param data: a str holding some amp-encoded data.
+
+ @return: a list of AmpBoxes encoded in the given string.
+ """
+ return cls.parse(StringIO(data))
+ parseString = classmethod(parseString)
+
+
+
+parse = _ParserHelper.parse
+parseString = _ParserHelper.parseString
+
+def _stringsToObjects(strings, arglist, proto):
+ """
+ Convert an AmpBox to a dictionary of python objects, converting through a
+ given arglist.
+
+ @param strings: an AmpBox (or dict of strings)
+
+ @param arglist: a list of 2-tuples of strings and Argument objects, as
+ described in L{Command.arguments}.
+
+ @param proto: an L{AMP} instance.
+
+ @return: the converted dictionary mapping names to argument objects.
+ """
+ objects = {}
+ myStrings = strings.copy()
+ for argname, argparser in arglist:
+ argparser.fromBox(argname, myStrings, objects, proto)
+ return objects
+
+
+
+def _objectsToStrings(objects, arglist, strings, proto):
+ """
+ Convert a dictionary of python objects to an AmpBox, converting through a
+ given arglist.
+
+ @param objects: a dict mapping names to python objects
+
+ @param arglist: a list of 2-tuples of strings and Argument objects, as
+ described in L{Command.arguments}.
+
+ @param strings: [OUT PARAMETER] An object providing the L{dict}
+ interface which will be populated with serialized data.
+
+ @param proto: an L{AMP} instance.
+
+ @return: The converted dictionary mapping names to encoded argument
+ strings (identical to C{strings}).
+ """
+ myObjects = objects.copy()
+ for argname, argparser in arglist:
+ argparser.toBox(argname, strings, myObjects, proto)
+ return strings
+
+
+
+class _FixedOffsetTZInfo(datetime.tzinfo):
+ """
+ Represents a fixed timezone offset (without daylight saving time).
+
+ @ivar name: A C{str} giving the name of this timezone; the name just
+ includes how much time this offset represents.
+
+ @ivar offset: A C{datetime.timedelta} giving the amount of time this
+ timezone is offset.
+ """
+
+ def __init__(self, sign, hours, minutes):
+ self.name = '%s%02i:%02i' % (sign, hours, minutes)
+ if sign == '-':
+ hours = -hours
+ minutes = -minutes
+ elif sign != '+':
+ raise ValueError('invalid sign for timezone %r' % (sign,))
+ self.offset = datetime.timedelta(hours=hours, minutes=minutes)
+
+
+ def utcoffset(self, dt):
+ """
+ Return this timezone's offset from UTC.
+ """
+ return self.offset
+
+
+ def dst(self, dt):
+ """
+ Return a zero C{datetime.timedelta} for the daylight saving time offset,
+ since there is never one.
+ """
+ return datetime.timedelta(0)
+
+
+ def tzname(self, dt):
+ """
+ Return a string describing this timezone.
+ """
+ return self.name
+
+
+
+utc = _FixedOffsetTZInfo('+', 0, 0)
+
+
+
+class Decimal(Argument):
+ """
+ Encodes C{decimal.Decimal} instances.
+
+ There are several ways in which a decimal value might be encoded.
+
+ Special values are encoded as special strings::
+
+ - Positive infinity is encoded as C{"Infinity"}
+ - Negative infinity is encoded as C{"-Infinity"}
+ - Quiet not-a-number is encoded as either C{"NaN"} or C{"-NaN"}
+ - Signalling not-a-number is encoded as either C{"sNaN"} or C{"-sNaN"}
+
+ Normal values are encoded using the base ten string representation, using
+ engineering notation to indicate magnitude without precision, and "normal"
+ digits to indicate precision. For example::
+
+ - C{"1"} represents the value I{1} with precision to one place.
+ - C{"-1"} represents the value I{-1} with precision to one place.
+ - C{"1.0"} represents the value I{1} with precision to two places.
+ - C{"10"} represents the value I{10} with precision to two places.
+ - C{"1E+2"} represents the value I{10} with precision to one place.
+ - C{"1E-1"} represents the value I{0.1} with precision to one place.
+ - C{"1.5E+2"} represents the value I{15} with precision to two places.
+
+ U{http://speleotrove.com/decimal/} should be considered the authoritative
+ specification for the format.
+ """
+ fromString = decimal.Decimal
+
+ def toString(self, inObject):
+ """
+ Serialize a C{decimal.Decimal} instance to the specified wire format.
+ """
+ if isinstance(inObject, decimal.Decimal):
+ # Hopefully decimal.Decimal.__str__ actually does what we want.
+ return str(inObject)
+ raise ValueError(
+ "amp.Decimal can only encode instances of decimal.Decimal")
+
+
+
+class DateTime(Argument):
+ """
+ Encodes C{datetime.datetime} instances.
+
+ Wire format: '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i'. Fields in
+ order are: year, month, day, hour, minute, second, microsecond, timezone
+ direction (+ or -), timezone hour, timezone minute. Encoded string is
+ always exactly 32 characters long. This format is compatible with ISO 8601,
+ but that does not mean all ISO 8601 dates can be accepted.
+
+ Also, note that the datetime module's notion of a "timezone" can be
+ complex, but the wire format includes only a fixed offset, so the
+ conversion is not lossless. A lossless transmission of a C{datetime} instance
+ is not feasible since the receiving end would require a Python interpreter.
+
+ @ivar _positions: A sequence of slices giving the positions of various
+ interesting parts of the wire format.
+ """
+
+ _positions = [
+ slice(0, 4), slice(5, 7), slice(8, 10), # year, month, day
+ slice(11, 13), slice(14, 16), slice(17, 19), # hour, minute, second
+ slice(20, 26), # microsecond
+ # intentionally skip timezone direction, as it is not an integer
+ slice(27, 29), slice(30, 32) # timezone hour, timezone minute
+ ]
+
+ def fromString(self, s):
+ """
+ Parse a string containing a date and time in the wire format into a
+ C{datetime.datetime} instance.
+ """
+ if len(s) != 32:
+ raise ValueError('invalid date format %r' % (s,))
+
+ values = [int(s[p]) for p in self._positions]
+ sign = s[26]
+ timezone = _FixedOffsetTZInfo(sign, *values[7:])
+ values[7:] = [timezone]
+ return datetime.datetime(*values)
+
+
+ def toString(self, i):
+ """
+ Serialize a C{datetime.datetime} instance to a string in the specified
+ wire format.
+ """
+ offset = i.utcoffset()
+ if offset is None:
+ raise ValueError(
+ 'amp.DateTime cannot serialize naive datetime instances. '
+ 'You may find amp.utc useful.')
+
+ minutesOffset = (offset.days * 86400 + offset.seconds) // 60
+
+ if minutesOffset > 0:
+ sign = '+'
+ else:
+ sign = '-'
+
+ # strftime has no way to format the microseconds, or put a ':' in the
+ # timezone. Suprise!
+
+ return '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i' % (
+ i.year,
+ i.month,
+ i.day,
+ i.hour,
+ i.minute,
+ i.second,
+ i.microsecond,
+ sign,
+ abs(minutesOffset) // 60,
+ abs(minutesOffset) % 60)
diff --git a/twisted/protocols/basic.py b/twisted/protocols/basic.py
new file mode 100644
index 0000000..7c4c940
--- /dev/null
+++ b/twisted/protocols/basic.py
@@ -0,0 +1,939 @@
+# -*- test-case-name: twisted.test.test_protocols -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Basic protocols, such as line-oriented, netstring, and int prefixed strings.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+# System imports
+import re
+from struct import pack, unpack, calcsize
+import warnings
+import cStringIO
+import math
+
+from zope.interface import implements
+
+# Twisted imports
+from twisted.internet import protocol, defer, interfaces, error
+from twisted.python import log, deprecate, versions
+
+
+LENGTH, DATA, COMMA = range(3)
+NUMBER = re.compile('(\d*)(:?)')
+
+deprecatedSince = versions.Version("Twisted", 10, 2, 0)
+message = "NetstringReceiver parser state is private."
+for attr in ["LENGTH", "DATA", "COMMA", "NUMBER"]:
+ deprecate.deprecatedModuleAttribute(
+ deprecatedSince, message, __name__, attr)
+del deprecatedSince, message, attr
+
+DEBUG = 0
+
+class NetstringParseError(ValueError):
+ """
+ The incoming data is not in valid Netstring format.
+ """
+
+
+
+class IncompleteNetstring(Exception):
+ """
+ Not enough data to complete a netstring.
+ """
+
+
+
+class NetstringReceiver(protocol.Protocol):
+ """
+ A protocol that sends and receives netstrings.
+
+ See U{http://cr.yp.to/proto/netstrings.txt} for the specification of
+ netstrings. Every netstring starts with digits that specify the length
+ of the data. This length specification is separated from the data by
+ a colon. The data is terminated with a comma.
+
+ Override L{stringReceived} to handle received netstrings. This
+ method is called with the netstring payload as a single argument
+ whenever a complete netstring is received.
+
+ Security features:
+ 1. Messages are limited in size, useful if you don't want
+ someone sending you a 500MB netstring (change C{self.MAX_LENGTH}
+ to the maximum length you wish to accept).
+ 2. The connection is lost if an illegal message is received.
+
+ @ivar MAX_LENGTH: Defines the maximum length of netstrings that can be
+ received.
+ @type MAX_LENGTH: C{int}
+
+ @ivar _LENGTH: A pattern describing all strings that contain a netstring
+ length specification. Examples for length specifications are '0:',
+ '12:', and '179:'. '007:' is no valid length specification, since
+ leading zeros are not allowed.
+ @type _LENGTH: C{re.Match}
+
+ @ivar _LENGTH_PREFIX: A pattern describing all strings that contain
+ the first part of a netstring length specification (without the
+ trailing comma). Examples are '0', '12', and '179'. '007' does not
+ start a netstring length specification, since leading zeros are
+ not allowed.
+ @type _LENGTH_PREFIX: C{re.Match}
+
+ @ivar _PARSING_LENGTH: Indicates that the C{NetstringReceiver} is in
+ the state of parsing the length portion of a netstring.
+ @type _PARSING_LENGTH: C{int}
+
+ @ivar _PARSING_PAYLOAD: Indicates that the C{NetstringReceiver} is in
+ the state of parsing the payload portion (data and trailing comma)
+ of a netstring.
+ @type _PARSING_PAYLOAD: C{int}
+
+ @ivar brokenPeer: Indicates if the connection is still functional
+ @type brokenPeer: C{int}
+
+ @ivar _state: Indicates if the protocol is consuming the length portion
+ (C{PARSING_LENGTH}) or the payload (C{PARSING_PAYLOAD}) of a netstring
+ @type _state: C{int}
+
+ @ivar _remainingData: Holds the chunk of data that has not yet been consumed
+ @type _remainingData: C{string}
+
+ @ivar _payload: Holds the payload portion of a netstring including the
+ trailing comma
+ @type _payload: C{cStringIO.StringIO}
+
+ @ivar _expectedPayloadSize: Holds the payload size plus one for the trailing
+ comma.
+ @type _expectedPayloadSize: C{int}
+ """
+ MAX_LENGTH = 99999
+ _LENGTH = re.compile('(0|[1-9]\d*)(:)')
+
+ _LENGTH_PREFIX = re.compile('(0|[1-9]\d*)$')
+
+ # Some error information for NetstringParseError instances.
+ _MISSING_LENGTH = ("The received netstring does not start with a "
+ "length specification.")
+ _OVERFLOW = ("The length specification of the received netstring "
+ "cannot be represented in Python - it causes an "
+ "OverflowError!")
+ _TOO_LONG = ("The received netstring is longer than the maximum %s "
+ "specified by self.MAX_LENGTH")
+ _MISSING_COMMA = "The received netstring is not terminated by a comma."
+ _DATA_SUPPORT_DEPRECATED = ("Data passed to sendString() must be a string. "
+ "Non-string support is deprecated since "
+ "Twisted 10.0")
+
+ # The following constants are used for determining if the NetstringReceiver
+ # is parsing the length portion of a netstring, or the payload.
+ _PARSING_LENGTH, _PARSING_PAYLOAD = range(2)
+
+ def makeConnection(self, transport):
+ """
+ Initializes the protocol.
+ """
+ protocol.Protocol.makeConnection(self, transport)
+ self._remainingData = ""
+ self._currentPayloadSize = 0
+ self._payload = cStringIO.StringIO()
+ self._state = self._PARSING_LENGTH
+ self._expectedPayloadSize = 0
+ self.brokenPeer = 0
+
+
+ def sendString(self, string):
+ """
+ Sends a netstring.
+
+ Wraps up C{string} by adding length information and a
+ trailing comma; writes the result to the transport.
+
+ @param string: The string to send. The necessary framing (length
+ prefix, etc) will be added.
+ @type string: C{str}
+ """
+ if not isinstance(string, str):
+ warnings.warn(self._DATA_SUPPORT_DEPRECATED, DeprecationWarning, 2)
+ string = str(string)
+ self.transport.write('%d:%s,' % (len(string), string))
+
+
+ def dataReceived(self, data):
+ """
+ Receives some characters of a netstring.
+
+ Whenever a complete netstring is received, this method extracts
+ its payload and calls L{stringReceived} to process it.
+
+ @param data: A chunk of data representing a (possibly partial)
+ netstring
+ @type data: C{str}
+ """
+ self._remainingData += data
+ while self._remainingData:
+ try:
+ self._consumeData()
+ except IncompleteNetstring:
+ break
+ except NetstringParseError:
+ self._handleParseError()
+ break
+
+
+ def stringReceived(self, string):
+ """
+ Override this for notification when each complete string is received.
+
+ @param string: The complete string which was received with all
+ framing (length prefix, etc) removed.
+ @type string: C{str}
+
+ @raise NotImplementedError: because the method has to be implemented
+ by the child class.
+ """
+ raise NotImplementedError()
+
+
+ def _maxLengthSize(self):
+ """
+ Calculate and return the string size of C{self.MAX_LENGTH}.
+
+ @return: The size of the string representation for C{self.MAX_LENGTH}
+ @rtype: C{float}
+ """
+ return math.ceil(math.log10(self.MAX_LENGTH)) + 1
+
+
+ def _consumeData(self):
+ """
+ Consumes the content of C{self._remainingData}.
+
+ @raise IncompleteNetstring: if C{self._remainingData} does not
+ contain enough data to complete the current netstring.
+ @raise NetstringParseError: if the received data do not
+ form a valid netstring.
+ """
+ if self._state == self._PARSING_LENGTH:
+ self._consumeLength()
+ self._prepareForPayloadConsumption()
+ if self._state == self._PARSING_PAYLOAD:
+ self._consumePayload()
+
+
+ def _consumeLength(self):
+ """
+ Consumes the length portion of C{self._remainingData}.
+
+ @raise IncompleteNetstring: if C{self._remainingData} contains
+ a partial length specification (digits without trailing
+ comma).
+ @raise NetstringParseError: if the received data do not form a valid
+ netstring.
+ """
+ lengthMatch = self._LENGTH.match(self._remainingData)
+ if not lengthMatch:
+ self._checkPartialLengthSpecification()
+ raise IncompleteNetstring()
+ self._processLength(lengthMatch)
+
+
+ def _checkPartialLengthSpecification(self):
+ """
+ Makes sure that the received data represents a valid number.
+
+ Checks if C{self._remainingData} represents a number smaller or
+ equal to C{self.MAX_LENGTH}.
+
+ @raise NetstringParseError: if C{self._remainingData} is no
+ number or is too big (checked by L{extractLength}).
+ """
+ partialLengthMatch = self._LENGTH_PREFIX.match(self._remainingData)
+ if not partialLengthMatch:
+ raise NetstringParseError(self._MISSING_LENGTH)
+ lengthSpecification = (partialLengthMatch.group(1))
+ self._extractLength(lengthSpecification)
+
+
+ def _processLength(self, lengthMatch):
+ """
+ Processes the length definition of a netstring.
+
+ Extracts and stores in C{self._expectedPayloadSize} the number
+ representing the netstring size. Removes the prefix
+ representing the length specification from
+ C{self._remainingData}.
+
+ @raise NetstringParseError: if the received netstring does not
+ start with a number or the number is bigger than
+ C{self.MAX_LENGTH}.
+ @param lengthMatch: A regular expression match object matching
+ a netstring length specification
+ @type lengthMatch: C{re.Match}
+ """
+ endOfNumber = lengthMatch.end(1)
+ startOfData = lengthMatch.end(2)
+ lengthString = self._remainingData[:endOfNumber]
+ # Expect payload plus trailing comma:
+ self._expectedPayloadSize = self._extractLength(lengthString) + 1
+ self._remainingData = self._remainingData[startOfData:]
+
+
+ def _extractLength(self, lengthAsString):
+ """
+ Attempts to extract the length information of a netstring.
+
+ @raise NetstringParseError: if the number is bigger than
+ C{self.MAX_LENGTH}.
+ @param lengthAsString: A chunk of data starting with a length
+ specification
+ @type lengthAsString: C{str}
+ @return: The length of the netstring
+ @rtype: C{int}
+ """
+ self._checkStringSize(lengthAsString)
+ length = int(lengthAsString)
+ if length > self.MAX_LENGTH:
+ raise NetstringParseError(self._TOO_LONG % (self.MAX_LENGTH,))
+ return length
+
+
+ def _checkStringSize(self, lengthAsString):
+ """
+ Checks the sanity of lengthAsString.
+
+ Checks if the size of the length specification exceeds the
+ size of the string representing self.MAX_LENGTH. If this is
+ not the case, the number represented by lengthAsString is
+ certainly bigger than self.MAX_LENGTH, and a
+ NetstringParseError can be raised.
+
+ This method should make sure that netstrings with extremely
+ long length specifications are refused before even attempting
+ to convert them to an integer (which might trigger a
+ MemoryError).
+ """
+ if len(lengthAsString) > self._maxLengthSize():
+ raise NetstringParseError(self._TOO_LONG % (self.MAX_LENGTH,))
+
+
+ def _prepareForPayloadConsumption(self):
+ """
+ Sets up variables necessary for consuming the payload of a netstring.
+ """
+ self._state = self._PARSING_PAYLOAD
+ self._currentPayloadSize = 0
+ self._payload.seek(0)
+ self._payload.truncate()
+
+
+ def _consumePayload(self):
+ """
+ Consumes the payload portion of C{self._remainingData}.
+
+ If the payload is complete, checks for the trailing comma and
+ processes the payload. If not, raises an L{IncompleteNetstring}
+ exception.
+
+ @raise IncompleteNetstring: if the payload received so far
+ contains fewer characters than expected.
+ @raise NetstringParseError: if the payload does not end with a
+ comma.
+ """
+ self._extractPayload()
+ if self._currentPayloadSize < self._expectedPayloadSize:
+ raise IncompleteNetstring()
+ self._checkForTrailingComma()
+ self._state = self._PARSING_LENGTH
+ self._processPayload()
+
+
+ def _extractPayload(self):
+ """
+ Extracts payload information from C{self._remainingData}.
+
+ Splits C{self._remainingData} at the end of the netstring. The
+ first part becomes C{self._payload}, the second part is stored
+ in C{self._remainingData}.
+
+ If the netstring is not yet complete, the whole content of
+ C{self._remainingData} is moved to C{self._payload}.
+ """
+ if self._payloadComplete():
+ remainingPayloadSize = (self._expectedPayloadSize -
+ self._currentPayloadSize)
+ self._payload.write(self._remainingData[:remainingPayloadSize])
+ self._remainingData = self._remainingData[remainingPayloadSize:]
+ self._currentPayloadSize = self._expectedPayloadSize
+ else:
+ self._payload.write(self._remainingData)
+ self._currentPayloadSize += len(self._remainingData)
+ self._remainingData = ""
+
+
+ def _payloadComplete(self):
+ """
+ Checks if enough data have been received to complete the netstring.
+
+ @return: C{True} iff the received data contain at least as many
+ characters as specified in the length section of the
+ netstring
+ @rtype: C{bool}
+ """
+ return (len(self._remainingData) + self._currentPayloadSize >=
+ self._expectedPayloadSize)
+
+
+ def _processPayload(self):
+ """
+ Processes the actual payload with L{stringReceived}.
+
+ Strips C{self._payload} of the trailing comma and calls
+ L{stringReceived} with the result.
+ """
+ self.stringReceived(self._payload.getvalue()[:-1])
+
+
+ def _checkForTrailingComma(self):
+ """
+ Checks if the netstring has a trailing comma at the expected position.
+
+ @raise NetstringParseError: if the last payload character is
+ anything but a comma.
+ """
+ if self._payload.getvalue()[-1] != ",":
+ raise NetstringParseError(self._MISSING_COMMA)
+
+
+ def _handleParseError(self):
+ """
+ Terminates the connection and sets the flag C{self.brokenPeer}.
+ """
+ self.transport.loseConnection()
+ self.brokenPeer = 1
+
+
+
+class LineOnlyReceiver(protocol.Protocol):
+ """
+ A protocol that receives only lines.
+
+ This is purely a speed optimisation over LineReceiver, for the
+ cases that raw mode is known to be unnecessary.
+
+ @cvar delimiter: The line-ending delimiter to use. By default this is
+ '\\r\\n'.
+ @cvar MAX_LENGTH: The maximum length of a line to allow (If a
+ sent line is longer than this, the connection is dropped).
+ Default is 16384.
+ """
+ _buffer = ''
+ delimiter = '\r\n'
+ MAX_LENGTH = 16384
+
+ def dataReceived(self, data):
+ """
+ Translates bytes into lines, and calls lineReceived.
+ """
+ lines = (self._buffer+data).split(self.delimiter)
+ self._buffer = lines.pop(-1)
+ for line in lines:
+ if self.transport.disconnecting:
+ # this is necessary because the transport may be told to lose
+ # the connection by a line within a larger packet, and it is
+ # important to disregard all the lines in that packet following
+ # the one that told it to close.
+ return
+ if len(line) > self.MAX_LENGTH:
+ return self.lineLengthExceeded(line)
+ else:
+ self.lineReceived(line)
+ if len(self._buffer) > self.MAX_LENGTH:
+ return self.lineLengthExceeded(self._buffer)
+
+
+ def lineReceived(self, line):
+ """
+ Override this for when each line is received.
+
+ @param line: The line which was received with the delimiter removed.
+ @type line: C{str}
+ """
+ raise NotImplementedError
+
+
+ def sendLine(self, line):
+ """
+ Sends a line to the other end of the connection.
+
+ @param line: The line to send, not including the delimiter.
+ @type line: C{str}
+ """
+ return self.transport.writeSequence((line, self.delimiter))
+
+
+ def lineLengthExceeded(self, line):
+ """
+ Called when the maximum line length has been reached.
+ Override if it needs to be dealt with in some special way.
+ """
+ return error.ConnectionLost('Line length exceeded')
+
+
+
+class _PauseableMixin:
+ paused = False
+
+ def pauseProducing(self):
+ self.paused = True
+ self.transport.pauseProducing()
+
+
+ def resumeProducing(self):
+ self.paused = False
+ self.transport.resumeProducing()
+ self.dataReceived('')
+
+
+ def stopProducing(self):
+ self.paused = True
+ self.transport.stopProducing()
+
+
+
+class LineReceiver(protocol.Protocol, _PauseableMixin):
+ """
+ A protocol that receives lines and/or raw data, depending on mode.
+
+ In line mode, each line that's received becomes a callback to
+ L{lineReceived}. In raw data mode, each chunk of raw data becomes a
+ callback to L{rawDataReceived}. The L{setLineMode} and L{setRawMode}
+ methods switch between the two modes.
+
+ This is useful for line-oriented protocols such as IRC, HTTP, POP, etc.
+
+ @cvar delimiter: The line-ending delimiter to use. By default this is
+ '\\r\\n'.
+ @cvar MAX_LENGTH: The maximum length of a line to allow (If a
+ sent line is longer than this, the connection is dropped).
+ Default is 16384.
+ """
+ line_mode = 1
+ __buffer = ''
+ delimiter = '\r\n'
+ MAX_LENGTH = 16384
+
+ def clearLineBuffer(self):
+ """
+ Clear buffered data.
+
+ @return: All of the cleared buffered data.
+ @rtype: C{str}
+ """
+ b = self.__buffer
+ self.__buffer = ""
+ return b
+
+
+ def dataReceived(self, data):
+ """
+ Protocol.dataReceived.
+ Translates bytes into lines, and calls lineReceived (or
+ rawDataReceived, depending on mode.)
+ """
+ self.__buffer = self.__buffer+data
+ while self.line_mode and not self.paused:
+ try:
+ line, self.__buffer = self.__buffer.split(self.delimiter, 1)
+ except ValueError:
+ if len(self.__buffer) > self.MAX_LENGTH:
+ line, self.__buffer = self.__buffer, ''
+ return self.lineLengthExceeded(line)
+ break
+ else:
+ linelength = len(line)
+ if linelength > self.MAX_LENGTH:
+ exceeded = line + self.__buffer
+ self.__buffer = ''
+ return self.lineLengthExceeded(exceeded)
+ why = self.lineReceived(line)
+ if why or self.transport and self.transport.disconnecting:
+ return why
+ else:
+ if not self.paused:
+ data=self.__buffer
+ self.__buffer=''
+ if data:
+ return self.rawDataReceived(data)
+
+
+ def setLineMode(self, extra=''):
+ """
+ Sets the line-mode of this receiver.
+
+ If you are calling this from a rawDataReceived callback,
+ you can pass in extra unhandled data, and that data will
+ be parsed for lines. Further data received will be sent
+ to lineReceived rather than rawDataReceived.
+
+ Do not pass extra data if calling this function from
+ within a lineReceived callback.
+ """
+ self.line_mode = 1
+ if extra:
+ return self.dataReceived(extra)
+
+
+ def setRawMode(self):
+ """
+ Sets the raw mode of this receiver.
+ Further data received will be sent to rawDataReceived rather
+ than lineReceived.
+ """
+ self.line_mode = 0
+
+
+ def rawDataReceived(self, data):
+ """
+ Override this for when raw data is received.
+ """
+ raise NotImplementedError
+
+
+ def lineReceived(self, line):
+ """
+ Override this for when each line is received.
+
+ @param line: The line which was received with the delimiter removed.
+ @type line: C{str}
+ """
+ raise NotImplementedError
+
+
+ def sendLine(self, line):
+ """
+ Sends a line to the other end of the connection.
+
+ @param line: The line to send, not including the delimiter.
+ @type line: C{str}
+ """
+ return self.transport.write(line + self.delimiter)
+
+
+ def lineLengthExceeded(self, line):
+ """
+ Called when the maximum line length has been reached.
+ Override if it needs to be dealt with in some special way.
+
+ The argument 'line' contains the remainder of the buffer, starting
+ with (at least some part) of the line which is too long. This may
+ be more than one line, or may be only the initial portion of the
+ line.
+ """
+ return self.transport.loseConnection()
+
+
+
+class StringTooLongError(AssertionError):
+ """
+ Raised when trying to send a string too long for a length prefixed
+ protocol.
+ """
+
+
+
+class _RecvdCompatHack(object):
+ """
+ Emulates the to-be-deprecated C{IntNStringReceiver.recvd} attribute.
+
+ The C{recvd} attribute was where the working buffer for buffering and
+ parsing netstrings was kept. It was updated each time new data arrived and
+ each time some of that data was parsed and delivered to application code.
+ The piecemeal updates to its string value were expensive and have been
+ removed from C{IntNStringReceiver} in the normal case. However, for
+ applications directly reading this attribute, this descriptor restores that
+ behavior. It only copies the working buffer when necessary (ie, when
+ accessed). This avoids the cost for applications not using the data.
+
+ This is a custom descriptor rather than a property, because we still need
+ the default __set__ behavior in both new-style and old-style subclasses.
+ """
+ def __get__(self, oself, type=None):
+ return oself._unprocessed[oself._compatibilityOffset:]
+
+
+
+class IntNStringReceiver(protocol.Protocol, _PauseableMixin):
+ """
+ Generic class for length prefixed protocols.
+
+ @ivar _unprocessed: bytes received, but not yet broken up into messages /
+ sent to stringReceived. _compatibilityOffset must be updated when this
+ value is updated so that the C{recvd} attribute can be generated
+ correctly.
+ @type _unprocessed: C{bytes}
+
+ @ivar structFormat: format used for struct packing/unpacking. Define it in
+ subclass.
+ @type structFormat: C{str}
+
+ @ivar prefixLength: length of the prefix, in bytes. Define it in subclass,
+ using C{struct.calcsize(structFormat)}
+ @type prefixLength: C{int}
+
+ @ivar _compatibilityOffset: the offset within C{_unprocessed} to the next
+ message to be parsed. (used to generate the recvd attribute)
+ @type _compatibilityOffset: C{int}
+ """
+
+ MAX_LENGTH = 99999
+ _unprocessed = ""
+ _compatibilityOffset = 0
+
+ # Backwards compatibility support for applications which directly touch the
+ # "internal" parse buffer.
+ recvd = _RecvdCompatHack()
+
+ def stringReceived(self, string):
+ """
+ Override this for notification when each complete string is received.
+
+ @param string: The complete string which was received with all
+ framing (length prefix, etc) removed.
+ @type string: C{str}
+ """
+ raise NotImplementedError
+
+
+ def lengthLimitExceeded(self, length):
+ """
+ Callback invoked when a length prefix greater than C{MAX_LENGTH} is
+ received. The default implementation disconnects the transport.
+ Override this.
+
+ @param length: The length prefix which was received.
+ @type length: C{int}
+ """
+ self.transport.loseConnection()
+
+
+ def dataReceived(self, data):
+ """
+ Convert int prefixed strings into calls to stringReceived.
+ """
+ # Try to minimize string copying (via slices) by keeping one buffer
+ # containing all the data we have so far and a separate offset into that
+ # buffer.
+ alldata = self._unprocessed + data
+ currentOffset = 0
+ prefixLength = self.prefixLength
+ fmt = self.structFormat
+ self._unprocessed = alldata
+
+ while len(alldata) >= (currentOffset + prefixLength) and not self.paused:
+ messageStart = currentOffset + prefixLength
+ length, = unpack(fmt, alldata[currentOffset:messageStart])
+ if length > self.MAX_LENGTH:
+ self._unprocessed = alldata
+ self._compatibilityOffset = currentOffset
+ self.lengthLimitExceeded(length)
+ return
+ messageEnd = messageStart + length
+ if len(alldata) < messageEnd:
+ break
+
+ # Here we have to slice the working buffer so we can send just the
+ # netstring into the stringReceived callback.
+ packet = alldata[messageStart:messageEnd]
+ currentOffset = messageEnd
+ self._compatibilityOffset = currentOffset
+ self.stringReceived(packet)
+
+ # Check to see if the backwards compat "recvd" attribute got written
+ # to by application code. If so, drop the current data buffer and
+ # switch to the new buffer given by that attribute's value.
+ if 'recvd' in self.__dict__:
+ alldata = self.__dict__.pop('recvd')
+ self._unprocessed = alldata
+ self._compatibilityOffset = currentOffset = 0
+ if alldata:
+ continue
+ return
+
+ # Slice off all the data that has been processed, avoiding holding onto
+ # memory to store it, and update the compatibility attributes to reflect
+ # that change.
+ self._unprocessed = alldata[currentOffset:]
+ self._compatibilityOffset = 0
+
+
+ def sendString(self, string):
+ """
+ Send a prefixed string to the other end of the connection.
+
+ @param string: The string to send. The necessary framing (length
+ prefix, etc) will be added.
+ @type string: C{str}
+ """
+ if len(string) >= 2 ** (8 * self.prefixLength):
+ raise StringTooLongError(
+ "Try to send %s bytes whereas maximum is %s" % (
+ len(string), 2 ** (8 * self.prefixLength)))
+ self.transport.write(
+ pack(self.structFormat, len(string)) + string)
+
+
+
+class Int32StringReceiver(IntNStringReceiver):
+ """
+ A receiver for int32-prefixed strings.
+
+ An int32 string is a string prefixed by 4 bytes, the 32-bit length of
+ the string encoded in network byte order.
+
+ This class publishes the same interface as NetstringReceiver.
+ """
+ structFormat = "!I"
+ prefixLength = calcsize(structFormat)
+
+
+
+class Int16StringReceiver(IntNStringReceiver):
+ """
+ A receiver for int16-prefixed strings.
+
+ An int16 string is a string prefixed by 2 bytes, the 16-bit length of
+ the string encoded in network byte order.
+
+ This class publishes the same interface as NetstringReceiver.
+ """
+ structFormat = "!H"
+ prefixLength = calcsize(structFormat)
+
+
+
+class Int8StringReceiver(IntNStringReceiver):
+ """
+ A receiver for int8-prefixed strings.
+
+ An int8 string is a string prefixed by 1 byte, the 8-bit length of
+ the string.
+
+ This class publishes the same interface as NetstringReceiver.
+ """
+ structFormat = "!B"
+ prefixLength = calcsize(structFormat)
+
+
+
+class StatefulStringProtocol:
+ """
+ A stateful string protocol.
+
+ This is a mixin for string protocols (Int32StringReceiver,
+ NetstringReceiver) which translates stringReceived into a callback
+ (prefixed with 'proto_') depending on state.
+
+ The state 'done' is special; if a proto_* method returns it, the
+ connection will be closed immediately.
+ """
+
+ state = 'init'
+
+ def stringReceived(self, string):
+ """
+ Choose a protocol phase function and call it.
+
+ Call back to the appropriate protocol phase; this begins with
+ the function proto_init and moves on to proto_* depending on
+ what each proto_* function returns. (For example, if
+ self.proto_init returns 'foo', then self.proto_foo will be the
+ next function called when a protocol message is received.
+ """
+ try:
+ pto = 'proto_'+self.state
+ statehandler = getattr(self,pto)
+ except AttributeError:
+ log.msg('callback',self.state,'not found')
+ else:
+ self.state = statehandler(string)
+ if self.state == 'done':
+ self.transport.loseConnection()
+
+
+
+class FileSender:
+ """
+ A producer that sends the contents of a file to a consumer.
+
+ This is a helper for protocols that, at some point, will take a
+ file-like object, read its contents, and write them out to the network,
+ optionally performing some transformation on the bytes in between.
+ """
+ implements(interfaces.IProducer)
+
+ CHUNK_SIZE = 2 ** 14
+
+ lastSent = ''
+ deferred = None
+
+ def beginFileTransfer(self, file, consumer, transform = None):
+ """
+ Begin transferring a file
+
+ @type file: Any file-like object
+ @param file: The file object to read data from
+
+ @type consumer: Any implementor of IConsumer
+ @param consumer: The object to write data to
+
+ @param transform: A callable taking one string argument and returning
+ the same. All bytes read from the file are passed through this before
+ being written to the consumer.
+
+ @rtype: C{Deferred}
+ @return: A deferred whose callback will be invoked when the file has
+ been completely written to the consumer. The last byte written to the
+ consumer is passed to the callback.
+ """
+ self.file = file
+ self.consumer = consumer
+ self.transform = transform
+
+ self.deferred = deferred = defer.Deferred()
+ self.consumer.registerProducer(self, False)
+ return deferred
+
+
+ def resumeProducing(self):
+ chunk = ''
+ if self.file:
+ chunk = self.file.read(self.CHUNK_SIZE)
+ if not chunk:
+ self.file = None
+ self.consumer.unregisterProducer()
+ if self.deferred:
+ self.deferred.callback(self.lastSent)
+ self.deferred = None
+ return
+
+ if self.transform:
+ chunk = self.transform(chunk)
+ self.consumer.write(chunk)
+ self.lastSent = chunk[-1]
+
+
+ def pauseProducing(self):
+ pass
+
+
+ def stopProducing(self):
+ if self.deferred:
+ self.deferred.errback(
+ Exception("Consumer asked us to stop producing"))
+ self.deferred = None
diff --git a/twisted/protocols/dict.py b/twisted/protocols/dict.py
new file mode 100644
index 0000000..c3af402
--- /dev/null
+++ b/twisted/protocols/dict.py
@@ -0,0 +1,362 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Dict client protocol implementation.
+
+@author: Pavel Pergamenshchik
+"""
+
+from twisted.protocols import basic
+from twisted.internet import defer, protocol
+from twisted.python import log
+from StringIO import StringIO
+
+def parseParam(line):
+ """Chew one dqstring or atom from beginning of line and return (param, remaningline)"""
+ if line == '':
+ return (None, '')
+ elif line[0] != '"': # atom
+ mode = 1
+ else: # dqstring
+ mode = 2
+ res = ""
+ io = StringIO(line)
+ if mode == 2: # skip the opening quote
+ io.read(1)
+ while 1:
+ a = io.read(1)
+ if a == '"':
+ if mode == 2:
+ io.read(1) # skip the separating space
+ return (res, io.read())
+ elif a == '\\':
+ a = io.read(1)
+ if a == '':
+ return (None, line) # unexpected end of string
+ elif a == '':
+ if mode == 1:
+ return (res, io.read())
+ else:
+ return (None, line) # unexpected end of string
+ elif a == ' ':
+ if mode == 1:
+ return (res, io.read())
+ res += a
+
+def makeAtom(line):
+ """Munch a string into an 'atom'"""
+ # FIXME: proper quoting
+ return filter(lambda x: not (x in map(chr, range(33)+[34, 39, 92])), line)
+
+def makeWord(s):
+ mustquote = range(33)+[34, 39, 92]
+ result = []
+ for c in s:
+ if ord(c) in mustquote:
+ result.append("\\")
+ result.append(c)
+ s = "".join(result)
+ return s
+
+def parseText(line):
+ if len(line) == 1 and line == '.':
+ return None
+ else:
+ if len(line) > 1 and line[0:2] == '..':
+ line = line[1:]
+ return line
+
+class Definition:
+ """A word definition"""
+ def __init__(self, name, db, dbdesc, text):
+ self.name = name
+ self.db = db
+ self.dbdesc = dbdesc
+ self.text = text # list of strings not terminated by newline
+
+class DictClient(basic.LineReceiver):
+ """dict (RFC2229) client"""
+
+ data = None # multiline data
+ MAX_LENGTH = 1024
+ state = None
+ mode = None
+ result = None
+ factory = None
+
+ def __init__(self):
+ self.data = None
+ self.result = None
+
+ def connectionMade(self):
+ self.state = "conn"
+ self.mode = "command"
+
+ def sendLine(self, line):
+ """Throw up if the line is longer than 1022 characters"""
+ if len(line) > self.MAX_LENGTH - 2:
+ raise ValueError("DictClient tried to send a too long line")
+ basic.LineReceiver.sendLine(self, line)
+
+ def lineReceived(self, line):
+ try:
+ line = line.decode("UTF-8")
+ except UnicodeError: # garbage received, skip
+ return
+ if self.mode == "text": # we are receiving textual data
+ code = "text"
+ else:
+ if len(line) < 4:
+ log.msg("DictClient got invalid line from server -- %s" % line)
+ self.protocolError("Invalid line from server")
+ self.transport.LoseConnection()
+ return
+ code = int(line[:3])
+ line = line[4:]
+ method = getattr(self, 'dictCode_%s_%s' % (code, self.state), self.dictCode_default)
+ method(line)
+
+ def dictCode_default(self, line):
+ """Unkown message"""
+ log.msg("DictClient got unexpected message from server -- %s" % line)
+ self.protocolError("Unexpected server message")
+ self.transport.loseConnection()
+
+ def dictCode_221_ready(self, line):
+ """We are about to get kicked off, do nothing"""
+ pass
+
+ def dictCode_220_conn(self, line):
+ """Greeting message"""
+ self.state = "ready"
+ self.dictConnected()
+
+ def dictCode_530_conn(self):
+ self.protocolError("Access denied")
+ self.transport.loseConnection()
+
+ def dictCode_420_conn(self):
+ self.protocolError("Server temporarily unavailable")
+ self.transport.loseConnection()
+
+ def dictCode_421_conn(self):
+ self.protocolError("Server shutting down at operator request")
+ self.transport.loseConnection()
+
+ def sendDefine(self, database, word):
+ """Send a dict DEFINE command"""
+ assert self.state == "ready", "DictClient.sendDefine called when not in ready state"
+ self.result = None # these two are just in case. In "ready" state, result and data
+ self.data = None # should be None
+ self.state = "define"
+ command = "DEFINE %s %s" % (makeAtom(database.encode("UTF-8")), makeWord(word.encode("UTF-8")))
+ self.sendLine(command)
+
+ def sendMatch(self, database, strategy, word):
+ """Send a dict MATCH command"""
+ assert self.state == "ready", "DictClient.sendMatch called when not in ready state"
+ self.result = None
+ self.data = None
+ self.state = "match"
+ command = "MATCH %s %s %s" % (makeAtom(database), makeAtom(strategy), makeAtom(word))
+ self.sendLine(command.encode("UTF-8"))
+
+ def dictCode_550_define(self, line):
+ """Invalid database"""
+ self.mode = "ready"
+ self.defineFailed("Invalid database")
+
+ def dictCode_550_match(self, line):
+ """Invalid database"""
+ self.mode = "ready"
+ self.matchFailed("Invalid database")
+
+ def dictCode_551_match(self, line):
+ """Invalid strategy"""
+ self.mode = "ready"
+ self.matchFailed("Invalid strategy")
+
+ def dictCode_552_define(self, line):
+ """No match"""
+ self.mode = "ready"
+ self.defineFailed("No match")
+
+ def dictCode_552_match(self, line):
+ """No match"""
+ self.mode = "ready"
+ self.matchFailed("No match")
+
+ def dictCode_150_define(self, line):
+ """n definitions retrieved"""
+ self.result = []
+
+ def dictCode_151_define(self, line):
+ """Definition text follows"""
+ self.mode = "text"
+ (word, line) = parseParam(line)
+ (db, line) = parseParam(line)
+ (dbdesc, line) = parseParam(line)
+ if not (word and db and dbdesc):
+ self.protocolError("Invalid server response")
+ self.transport.loseConnection()
+ else:
+ self.result.append(Definition(word, db, dbdesc, []))
+ self.data = []
+
+ def dictCode_152_match(self, line):
+ """n matches found, text follows"""
+ self.mode = "text"
+ self.result = []
+ self.data = []
+
+ def dictCode_text_define(self, line):
+ """A line of definition text received"""
+ res = parseText(line)
+ if res == None:
+ self.mode = "command"
+ self.result[-1].text = self.data
+ self.data = None
+ else:
+ self.data.append(line)
+
+ def dictCode_text_match(self, line):
+ """One line of match text received"""
+ def l(s):
+ p1, t = parseParam(s)
+ p2, t = parseParam(t)
+ return (p1, p2)
+ res = parseText(line)
+ if res == None:
+ self.mode = "command"
+ self.result = map(l, self.data)
+ self.data = None
+ else:
+ self.data.append(line)
+
+ def dictCode_250_define(self, line):
+ """ok"""
+ t = self.result
+ self.result = None
+ self.state = "ready"
+ self.defineDone(t)
+
+ def dictCode_250_match(self, line):
+ """ok"""
+ t = self.result
+ self.result = None
+ self.state = "ready"
+ self.matchDone(t)
+
+ def protocolError(self, reason):
+ """override to catch unexpected dict protocol conditions"""
+ pass
+
+ def dictConnected(self):
+ """override to be notified when the server is ready to accept commands"""
+ pass
+
+ def defineFailed(self, reason):
+ """override to catch reasonable failure responses to DEFINE"""
+ pass
+
+ def defineDone(self, result):
+ """override to catch succesful DEFINE"""
+ pass
+
+ def matchFailed(self, reason):
+ """override to catch resonable failure responses to MATCH"""
+ pass
+
+ def matchDone(self, result):
+ """override to catch succesful MATCH"""
+ pass
+
+
+class InvalidResponse(Exception):
+ pass
+
+
+class DictLookup(DictClient):
+ """Utility class for a single dict transaction. To be used with DictLookupFactory"""
+
+ def protocolError(self, reason):
+ if not self.factory.done:
+ self.factory.d.errback(InvalidResponse(reason))
+ self.factory.clientDone()
+
+ def dictConnected(self):
+ if self.factory.queryType == "define":
+ apply(self.sendDefine, self.factory.param)
+ elif self.factory.queryType == "match":
+ apply(self.sendMatch, self.factory.param)
+
+ def defineFailed(self, reason):
+ self.factory.d.callback([])
+ self.factory.clientDone()
+ self.transport.loseConnection()
+
+ def defineDone(self, result):
+ self.factory.d.callback(result)
+ self.factory.clientDone()
+ self.transport.loseConnection()
+
+ def matchFailed(self, reason):
+ self.factory.d.callback([])
+ self.factory.clientDone()
+ self.transport.loseConnection()
+
+ def matchDone(self, result):
+ self.factory.d.callback(result)
+ self.factory.clientDone()
+ self.transport.loseConnection()
+
+
+class DictLookupFactory(protocol.ClientFactory):
+ """Utility factory for a single dict transaction"""
+ protocol = DictLookup
+ done = None
+
+ def __init__(self, queryType, param, d):
+ self.queryType = queryType
+ self.param = param
+ self.d = d
+ self.done = 0
+
+ def clientDone(self):
+ """Called by client when done."""
+ self.done = 1
+ del self.d
+
+ def clientConnectionFailed(self, connector, error):
+ self.d.errback(error)
+
+ def clientConnectionLost(self, connector, error):
+ if not self.done:
+ self.d.errback(error)
+
+ def buildProtocol(self, addr):
+ p = self.protocol()
+ p.factory = self
+ return p
+
+
+def define(host, port, database, word):
+ """Look up a word using a dict server"""
+ d = defer.Deferred()
+ factory = DictLookupFactory("define", (database, word), d)
+
+ from twisted.internet import reactor
+ reactor.connectTCP(host, port, factory)
+ return d
+
+def match(host, port, database, strategy, word):
+ """Match a word using a dict server"""
+ d = defer.Deferred()
+ factory = DictLookupFactory("match", (database, strategy, word), d)
+
+ from twisted.internet import reactor
+ reactor.connectTCP(host, port, factory)
+ return d
+
diff --git a/twisted/protocols/finger.py b/twisted/protocols/finger.py
new file mode 100644
index 0000000..fcb9396
--- /dev/null
+++ b/twisted/protocols/finger.py
@@ -0,0 +1,42 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""The Finger User Information Protocol (RFC 1288)"""
+
+from twisted.protocols import basic
+
+class Finger(basic.LineReceiver):
+
+ def lineReceived(self, line):
+ parts = line.split()
+ if not parts:
+ parts = ['']
+ if len(parts) == 1:
+ slash_w = 0
+ else:
+ slash_w = 1
+ user = parts[-1]
+ if '@' in user:
+ host_place = user.rfind('@')
+ user = user[:host_place]
+ host = user[host_place+1:]
+ return self.forwardQuery(slash_w, user, host)
+ if user:
+ return self.getUser(slash_w, user)
+ else:
+ return self.getDomain(slash_w)
+
+ def _refuseMessage(self, message):
+ self.transport.write(message+"\n")
+ self.transport.loseConnection()
+
+ def forwardQuery(self, slash_w, user, host):
+ self._refuseMessage('Finger forwarding service denied')
+
+ def getDomain(self, slash_w):
+ self._refuseMessage('Finger online list denied')
+
+ def getUser(self, slash_w, user):
+ self.transport.write('Login: '+user+'\n')
+ self._refuseMessage('No such user')
diff --git a/twisted/protocols/ftp.py b/twisted/protocols/ftp.py
new file mode 100644
index 0000000..5edc641
--- /dev/null
+++ b/twisted/protocols/ftp.py
@@ -0,0 +1,2953 @@
+# -*- test-case-name: twisted.test.test_ftp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An FTP protocol implementation
+"""
+
+# System Imports
+import os
+import time
+import re
+import operator
+import stat
+import errno
+import fnmatch
+import warnings
+
+try:
+ import pwd, grp
+except ImportError:
+ pwd = grp = None
+
+from zope.interface import Interface, implements
+
+# Twisted Imports
+from twisted import copyright
+from twisted.internet import reactor, interfaces, protocol, error, defer
+from twisted.protocols import basic, policies
+
+from twisted.python import log, failure, filepath
+from twisted.python.compat import reduce
+
+from twisted.cred import error as cred_error, portal, credentials, checkers
+
+# constants
+# response codes
+
+RESTART_MARKER_REPLY = "100"
+SERVICE_READY_IN_N_MINUTES = "120"
+DATA_CNX_ALREADY_OPEN_START_XFR = "125"
+FILE_STATUS_OK_OPEN_DATA_CNX = "150"
+
+CMD_OK = "200.1"
+TYPE_SET_OK = "200.2"
+ENTERING_PORT_MODE = "200.3"
+CMD_NOT_IMPLMNTD_SUPERFLUOUS = "202"
+SYS_STATUS_OR_HELP_REPLY = "211"
+DIR_STATUS = "212"
+FILE_STATUS = "213"
+HELP_MSG = "214"
+NAME_SYS_TYPE = "215"
+SVC_READY_FOR_NEW_USER = "220.1"
+WELCOME_MSG = "220.2"
+SVC_CLOSING_CTRL_CNX = "221.1"
+GOODBYE_MSG = "221.2"
+DATA_CNX_OPEN_NO_XFR_IN_PROGRESS = "225"
+CLOSING_DATA_CNX = "226.1"
+TXFR_COMPLETE_OK = "226.2"
+ENTERING_PASV_MODE = "227"
+ENTERING_EPSV_MODE = "229"
+USR_LOGGED_IN_PROCEED = "230.1" # v1 of code 230
+GUEST_LOGGED_IN_PROCEED = "230.2" # v2 of code 230
+REQ_FILE_ACTN_COMPLETED_OK = "250"
+PWD_REPLY = "257.1"
+MKD_REPLY = "257.2"
+
+USR_NAME_OK_NEED_PASS = "331.1" # v1 of Code 331
+GUEST_NAME_OK_NEED_EMAIL = "331.2" # v2 of code 331
+NEED_ACCT_FOR_LOGIN = "332"
+REQ_FILE_ACTN_PENDING_FURTHER_INFO = "350"
+
+SVC_NOT_AVAIL_CLOSING_CTRL_CNX = "421.1"
+TOO_MANY_CONNECTIONS = "421.2"
+CANT_OPEN_DATA_CNX = "425"
+CNX_CLOSED_TXFR_ABORTED = "426"
+REQ_ACTN_ABRTD_FILE_UNAVAIL = "450"
+REQ_ACTN_ABRTD_LOCAL_ERR = "451"
+REQ_ACTN_ABRTD_INSUFF_STORAGE = "452"
+
+SYNTAX_ERR = "500"
+SYNTAX_ERR_IN_ARGS = "501"
+CMD_NOT_IMPLMNTD = "502"
+BAD_CMD_SEQ = "503"
+CMD_NOT_IMPLMNTD_FOR_PARAM = "504"
+NOT_LOGGED_IN = "530.1" # v1 of code 530 - please log in
+AUTH_FAILURE = "530.2" # v2 of code 530 - authorization failure
+NEED_ACCT_FOR_STOR = "532"
+FILE_NOT_FOUND = "550.1" # no such file or directory
+PERMISSION_DENIED = "550.2" # permission denied
+ANON_USER_DENIED = "550.3" # anonymous users can't alter filesystem
+IS_NOT_A_DIR = "550.4" # rmd called on a path that is not a directory
+REQ_ACTN_NOT_TAKEN = "550.5"
+FILE_EXISTS = "550.6"
+IS_A_DIR = "550.7"
+PAGE_TYPE_UNK = "551"
+EXCEEDED_STORAGE_ALLOC = "552"
+FILENAME_NOT_ALLOWED = "553"
+
+
+RESPONSE = {
+ # -- 100's --
+ RESTART_MARKER_REPLY: '110 MARK yyyy-mmmm', # TODO: this must be fixed
+ SERVICE_READY_IN_N_MINUTES: '120 service ready in %s minutes',
+ DATA_CNX_ALREADY_OPEN_START_XFR: '125 Data connection already open, starting transfer',
+ FILE_STATUS_OK_OPEN_DATA_CNX: '150 File status okay; about to open data connection.',
+
+ # -- 200's --
+ CMD_OK: '200 Command OK',
+ TYPE_SET_OK: '200 Type set to %s.',
+ ENTERING_PORT_MODE: '200 PORT OK',
+ CMD_NOT_IMPLMNTD_SUPERFLUOUS: '202 Command not implemented, superfluous at this site',
+ SYS_STATUS_OR_HELP_REPLY: '211 System status reply',
+ DIR_STATUS: '212 %s',
+ FILE_STATUS: '213 %s',
+ HELP_MSG: '214 help: %s',
+ NAME_SYS_TYPE: '215 UNIX Type: L8',
+ WELCOME_MSG: "220 %s",
+ SVC_READY_FOR_NEW_USER: '220 Service ready',
+ SVC_CLOSING_CTRL_CNX: '221 Service closing control connection',
+ GOODBYE_MSG: '221 Goodbye.',
+ DATA_CNX_OPEN_NO_XFR_IN_PROGRESS: '225 data connection open, no transfer in progress',
+ CLOSING_DATA_CNX: '226 Abort successful',
+ TXFR_COMPLETE_OK: '226 Transfer Complete.',
+ ENTERING_PASV_MODE: '227 Entering Passive Mode (%s).',
+ ENTERING_EPSV_MODE: '229 Entering Extended Passive Mode (|||%s|).', # where is epsv defined in the rfc's?
+ USR_LOGGED_IN_PROCEED: '230 User logged in, proceed',
+ GUEST_LOGGED_IN_PROCEED: '230 Anonymous login ok, access restrictions apply.',
+ REQ_FILE_ACTN_COMPLETED_OK: '250 Requested File Action Completed OK', #i.e. CWD completed ok
+ PWD_REPLY: '257 "%s"',
+ MKD_REPLY: '257 "%s" created',
+
+ # -- 300's --
+ USR_NAME_OK_NEED_PASS: '331 Password required for %s.',
+ GUEST_NAME_OK_NEED_EMAIL: '331 Guest login ok, type your email address as password.',
+ NEED_ACCT_FOR_LOGIN: '332 Need account for login.',
+
+ REQ_FILE_ACTN_PENDING_FURTHER_INFO: '350 Requested file action pending further information.',
+
+# -- 400's --
+ SVC_NOT_AVAIL_CLOSING_CTRL_CNX: '421 Service not available, closing control connection.',
+ TOO_MANY_CONNECTIONS: '421 Too many users right now, try again in a few minutes.',
+ CANT_OPEN_DATA_CNX: "425 Can't open data connection.",
+ CNX_CLOSED_TXFR_ABORTED: '426 Transfer aborted. Data connection closed.',
+
+ REQ_ACTN_ABRTD_FILE_UNAVAIL: '450 Requested action aborted. File unavailable.',
+ REQ_ACTN_ABRTD_LOCAL_ERR: '451 Requested action aborted. Local error in processing.',
+ REQ_ACTN_ABRTD_INSUFF_STORAGE: '452 Requested action aborted. Insufficient storage.',
+
+ # -- 500's --
+ SYNTAX_ERR: "500 Syntax error: %s",
+ SYNTAX_ERR_IN_ARGS: '501 syntax error in argument(s) %s.',
+ CMD_NOT_IMPLMNTD: "502 Command '%s' not implemented",
+ BAD_CMD_SEQ: '503 Incorrect sequence of commands: %s',
+ CMD_NOT_IMPLMNTD_FOR_PARAM: "504 Not implemented for parameter '%s'.",
+ NOT_LOGGED_IN: '530 Please login with USER and PASS.',
+ AUTH_FAILURE: '530 Sorry, Authentication failed.',
+ NEED_ACCT_FOR_STOR: '532 Need an account for storing files',
+ FILE_NOT_FOUND: '550 %s: No such file or directory.',
+ PERMISSION_DENIED: '550 %s: Permission denied.',
+ ANON_USER_DENIED: '550 Anonymous users are forbidden to change the filesystem',
+ IS_NOT_A_DIR: '550 Cannot rmd, %s is not a directory',
+ FILE_EXISTS: '550 %s: File exists',
+ IS_A_DIR: '550 %s: is a directory',
+ REQ_ACTN_NOT_TAKEN: '550 Requested action not taken: %s',
+ PAGE_TYPE_UNK: '551 Page type unknown',
+ EXCEEDED_STORAGE_ALLOC: '552 Requested file action aborted, exceeded file storage allocation',
+ FILENAME_NOT_ALLOWED: '553 Requested action not taken, file name not allowed'
+}
+
+
+
+class InvalidPath(Exception):
+ """
+ Internal exception used to signify an error during parsing a path.
+ """
+
+
+
+def toSegments(cwd, path):
+ """
+ Normalize a path, as represented by a list of strings each
+ representing one segment of the path.
+ """
+ if path.startswith('/'):
+ segs = []
+ else:
+ segs = cwd[:]
+
+ for s in path.split('/'):
+ if s == '.' or s == '':
+ continue
+ elif s == '..':
+ if segs:
+ segs.pop()
+ else:
+ raise InvalidPath(cwd, path)
+ elif '\0' in s or '/' in s:
+ raise InvalidPath(cwd, path)
+ else:
+ segs.append(s)
+ return segs
+
+
+def errnoToFailure(e, path):
+ """
+ Map C{OSError} and C{IOError} to standard FTP errors.
+ """
+ if e == errno.ENOENT:
+ return defer.fail(FileNotFoundError(path))
+ elif e == errno.EACCES or e == errno.EPERM:
+ return defer.fail(PermissionDeniedError(path))
+ elif e == errno.ENOTDIR:
+ return defer.fail(IsNotADirectoryError(path))
+ elif e == errno.EEXIST:
+ return defer.fail(FileExistsError(path))
+ elif e == errno.EISDIR:
+ return defer.fail(IsADirectoryError(path))
+ else:
+ return defer.fail()
+
+
+
+class FTPCmdError(Exception):
+ """
+ Generic exception for FTP commands.
+ """
+ def __init__(self, *msg):
+ Exception.__init__(self, *msg)
+ self.errorMessage = msg
+
+
+ def response(self):
+ """
+ Generate a FTP response message for this error.
+ """
+ return RESPONSE[self.errorCode] % self.errorMessage
+
+
+
+class FileNotFoundError(FTPCmdError):
+ """
+ Raised when trying to access a non existent file or directory.
+ """
+ errorCode = FILE_NOT_FOUND
+
+
+
+class AnonUserDeniedError(FTPCmdError):
+ """
+ Raised when an anonymous user issues a command that will alter the
+ filesystem
+ """
+
+ errorCode = ANON_USER_DENIED
+
+
+
+class PermissionDeniedError(FTPCmdError):
+ """
+ Raised when access is attempted to a resource to which access is
+ not allowed.
+ """
+ errorCode = PERMISSION_DENIED
+
+
+
+class IsNotADirectoryError(FTPCmdError):
+ """
+ Raised when RMD is called on a path that isn't a directory.
+ """
+ errorCode = IS_NOT_A_DIR
+
+
+
+class FileExistsError(FTPCmdError):
+ """
+ Raised when attempted to override an existing resource.
+ """
+ errorCode = FILE_EXISTS
+
+
+
+class IsADirectoryError(FTPCmdError):
+ """
+ Raised when DELE is called on a path that is a directory.
+ """
+ errorCode = IS_A_DIR
+
+
+
+class CmdSyntaxError(FTPCmdError):
+ """
+ Raised when a command syntax is wrong.
+ """
+ errorCode = SYNTAX_ERR
+
+
+
+class CmdArgSyntaxError(FTPCmdError):
+ """
+ Raised when a command is called with wrong value or a wrong number of
+ arguments.
+ """
+ errorCode = SYNTAX_ERR_IN_ARGS
+
+
+
+class CmdNotImplementedError(FTPCmdError):
+ """
+ Raised when an unimplemented command is given to the server.
+ """
+ errorCode = CMD_NOT_IMPLMNTD
+
+
+
+class CmdNotImplementedForArgError(FTPCmdError):
+ """
+ Raised when the handling of a parameter for a command is not implemented by
+ the server.
+ """
+ errorCode = CMD_NOT_IMPLMNTD_FOR_PARAM
+
+
+
+class FTPError(Exception):
+ pass
+
+
+
+class PortConnectionError(Exception):
+ pass
+
+
+
+class BadCmdSequenceError(FTPCmdError):
+ """
+ Raised when a client sends a series of commands in an illogical sequence.
+ """
+ errorCode = BAD_CMD_SEQ
+
+
+
+class AuthorizationError(FTPCmdError):
+ """
+ Raised when client authentication fails.
+ """
+ errorCode = AUTH_FAILURE
+
+
+
+def debugDeferred(self, *_):
+ log.msg('debugDeferred(): %s' % str(_), debug=True)
+
+
+# -- DTP Protocol --
+
+
+_months = [
+ None,
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+
+class DTP(object, protocol.Protocol):
+ implements(interfaces.IConsumer)
+
+ isConnected = False
+
+ _cons = None
+ _onConnLost = None
+ _buffer = None
+
+ def connectionMade(self):
+ self.isConnected = True
+ self.factory.deferred.callback(None)
+ self._buffer = []
+
+ def connectionLost(self, reason):
+ self.isConnected = False
+ if self._onConnLost is not None:
+ self._onConnLost.callback(None)
+
+ def sendLine(self, line):
+ self.transport.write(line + '\r\n')
+
+
+ def _formatOneListResponse(self, name, size, directory, permissions, hardlinks, modified, owner, group):
+ def formatMode(mode):
+ return ''.join([mode & (256 >> n) and 'rwx'[n % 3] or '-' for n in range(9)])
+
+ def formatDate(mtime):
+ now = time.gmtime()
+ info = {
+ 'month': _months[mtime.tm_mon],
+ 'day': mtime.tm_mday,
+ 'year': mtime.tm_year,
+ 'hour': mtime.tm_hour,
+ 'minute': mtime.tm_min
+ }
+ if now.tm_year != mtime.tm_year:
+ return '%(month)s %(day)02d %(year)5d' % info
+ else:
+ return '%(month)s %(day)02d %(hour)02d:%(minute)02d' % info
+
+ format = ('%(directory)s%(permissions)s%(hardlinks)4d '
+ '%(owner)-9s %(group)-9s %(size)15d %(date)12s '
+ '%(name)s')
+
+ return format % {
+ 'directory': directory and 'd' or '-',
+ 'permissions': formatMode(permissions),
+ 'hardlinks': hardlinks,
+ 'owner': owner[:8],
+ 'group': group[:8],
+ 'size': size,
+ 'date': formatDate(time.gmtime(modified)),
+ 'name': name}
+
+ def sendListResponse(self, name, response):
+ self.sendLine(self._formatOneListResponse(name, *response))
+
+
+ # Proxy IConsumer to our transport
+ def registerProducer(self, producer, streaming):
+ return self.transport.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ self.transport.unregisterProducer()
+ self.transport.loseConnection()
+
+ def write(self, data):
+ if self.isConnected:
+ return self.transport.write(data)
+ raise Exception("Crap damn crap damn crap damn")
+
+
+ # Pretend to be a producer, too.
+ def _conswrite(self, bytes):
+ try:
+ self._cons.write(bytes)
+ except:
+ self._onConnLost.errback()
+
+ def dataReceived(self, bytes):
+ if self._cons is not None:
+ self._conswrite(bytes)
+ else:
+ self._buffer.append(bytes)
+
+ def _unregConsumer(self, ignored):
+ self._cons.unregisterProducer()
+ self._cons = None
+ del self._onConnLost
+ return ignored
+
+ def registerConsumer(self, cons):
+ assert self._cons is None
+ self._cons = cons
+ self._cons.registerProducer(self, True)
+ for chunk in self._buffer:
+ self._conswrite(chunk)
+ self._buffer = None
+ if self.isConnected:
+ self._onConnLost = d = defer.Deferred()
+ d.addBoth(self._unregConsumer)
+ return d
+ else:
+ self._cons.unregisterProducer()
+ self._cons = None
+ return defer.succeed(None)
+
+ def resumeProducing(self):
+ self.transport.resumeProducing()
+
+ def pauseProducing(self):
+ self.transport.pauseProducing()
+
+ def stopProducing(self):
+ self.transport.stopProducing()
+
+class DTPFactory(protocol.ClientFactory):
+ """
+ Client factory for I{data transfer process} protocols.
+
+ @ivar peerCheck: perform checks to make sure the ftp-pi's peer is the same
+ as the dtp's
+ @ivar pi: a reference to this factory's protocol interpreter
+
+ @ivar _state: Indicates the current state of the DTPFactory. Initially,
+ this is L{_IN_PROGRESS}. If the connection fails or times out, it is
+ L{_FAILED}. If the connection succeeds before the timeout, it is
+ L{_FINISHED}.
+ """
+
+ _IN_PROGRESS = object()
+ _FAILED = object()
+ _FINISHED = object()
+
+ _state = _IN_PROGRESS
+
+ # -- configuration variables --
+ peerCheck = False
+
+ # -- class variables --
+ def __init__(self, pi, peerHost=None, reactor=None):
+ """Constructor
+ @param pi: this factory's protocol interpreter
+ @param peerHost: if peerCheck is True, this is the tuple that the
+ generated instance will use to perform security checks
+ """
+ self.pi = pi # the protocol interpreter that is using this factory
+ self.peerHost = peerHost # the from FTP.transport.peerHost()
+ self.deferred = defer.Deferred() # deferred will fire when instance is connected
+ self.delayedCall = None
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+
+ def buildProtocol(self, addr):
+ log.msg('DTPFactory.buildProtocol', debug=True)
+
+ if self._state is not self._IN_PROGRESS:
+ return None
+ self._state = self._FINISHED
+
+ self.cancelTimeout()
+ p = DTP()
+ p.factory = self
+ p.pi = self.pi
+ self.pi.dtpInstance = p
+ return p
+
+
+ def stopFactory(self):
+ log.msg('dtpFactory.stopFactory', debug=True)
+ self.cancelTimeout()
+
+
+ def timeoutFactory(self):
+ log.msg('timed out waiting for DTP connection')
+ if self._state is not self._IN_PROGRESS:
+ return
+ self._state = self._FAILED
+
+ d = self.deferred
+ self.deferred = None
+ d.errback(
+ PortConnectionError(defer.TimeoutError("DTPFactory timeout")))
+
+
+ def cancelTimeout(self):
+ if self.delayedCall is not None and self.delayedCall.active():
+ log.msg('cancelling DTP timeout', debug=True)
+ self.delayedCall.cancel()
+
+
+ def setTimeout(self, seconds):
+ log.msg('DTPFactory.setTimeout set to %s seconds' % seconds)
+ self.delayedCall = self._reactor.callLater(seconds, self.timeoutFactory)
+
+
+ def clientConnectionFailed(self, connector, reason):
+ if self._state is not self._IN_PROGRESS:
+ return
+ self._state = self._FAILED
+ d = self.deferred
+ self.deferred = None
+ d.errback(PortConnectionError(reason))
+
+
+# -- FTP-PI (Protocol Interpreter) --
+
+class ASCIIConsumerWrapper(object):
+ def __init__(self, cons):
+ self.cons = cons
+ self.registerProducer = cons.registerProducer
+ self.unregisterProducer = cons.unregisterProducer
+
+ assert os.linesep == "\r\n" or len(os.linesep) == 1, "Unsupported platform (yea right like this even exists)"
+
+ if os.linesep == "\r\n":
+ self.write = cons.write
+
+ def write(self, bytes):
+ return self.cons.write(bytes.replace(os.linesep, "\r\n"))
+
+
+
+class FileConsumer(object):
+ """
+ A consumer for FTP input that writes data to a file.
+
+ @ivar fObj: a file object opened for writing, used to write data received.
+ @type fObj: C{file}
+ """
+
+ implements(interfaces.IConsumer)
+
+ def __init__(self, fObj):
+ self.fObj = fObj
+
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+ assert streaming
+
+
+ def unregisterProducer(self):
+ self.producer = None
+ self.fObj.close()
+
+
+ def write(self, bytes):
+ self.fObj.write(bytes)
+
+
+
+class FTPOverflowProtocol(basic.LineReceiver):
+ """FTP mini-protocol for when there are too many connections."""
+ def connectionMade(self):
+ self.sendLine(RESPONSE[TOO_MANY_CONNECTIONS])
+ self.transport.loseConnection()
+
+
+class FTP(object, basic.LineReceiver, policies.TimeoutMixin):
+ """
+ Protocol Interpreter for the File Transfer Protocol
+
+ @ivar state: The current server state. One of L{UNAUTH},
+ L{INAUTH}, L{AUTHED}, L{RENAMING}.
+
+ @ivar shell: The connected avatar
+ @ivar binary: The transfer mode. If false, ASCII.
+ @ivar dtpFactory: Generates a single DTP for this session
+ @ivar dtpPort: Port returned from listenTCP
+ @ivar listenFactory: A callable with the signature of
+ L{twisted.internet.interfaces.IReactorTCP.listenTCP} which will be used
+ to create Ports for passive connections (mainly for testing).
+
+ @ivar passivePortRange: iterator used as source of passive port numbers.
+ @type passivePortRange: C{iterator}
+ """
+
+ disconnected = False
+
+ # States an FTP can be in
+ UNAUTH, INAUTH, AUTHED, RENAMING = range(4)
+
+ # how long the DTP waits for a connection
+ dtpTimeout = 10
+
+ portal = None
+ shell = None
+ dtpFactory = None
+ dtpPort = None
+ dtpInstance = None
+ binary = True
+
+ passivePortRange = xrange(0, 1)
+
+ listenFactory = reactor.listenTCP
+
+ def reply(self, key, *args):
+ msg = RESPONSE[key] % args
+ self.sendLine(msg)
+
+
+ def connectionMade(self):
+ self.state = self.UNAUTH
+ self.setTimeout(self.timeOut)
+ self.reply(WELCOME_MSG, self.factory.welcomeMessage)
+
+ def connectionLost(self, reason):
+ # if we have a DTP protocol instance running and
+ # we lose connection to the client's PI, kill the
+ # DTP connection and close the port
+ if self.dtpFactory:
+ self.cleanupDTP()
+ self.setTimeout(None)
+ if hasattr(self.shell, 'logout') and self.shell.logout is not None:
+ self.shell.logout()
+ self.shell = None
+ self.transport = None
+
+ def timeoutConnection(self):
+ self.transport.loseConnection()
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+ self.pauseProducing()
+
+ def processFailed(err):
+ if err.check(FTPCmdError):
+ self.sendLine(err.value.response())
+ elif (err.check(TypeError) and
+ err.value.args[0].find('takes exactly') != -1):
+ self.reply(SYNTAX_ERR, "%s requires an argument." % (cmd,))
+ else:
+ log.msg("Unexpected FTP error")
+ log.err(err)
+ self.reply(REQ_ACTN_NOT_TAKEN, "internal server error")
+
+ def processSucceeded(result):
+ if isinstance(result, tuple):
+ self.reply(*result)
+ elif result is not None:
+ self.reply(result)
+
+ def allDone(ignored):
+ if not self.disconnected:
+ self.resumeProducing()
+
+ spaceIndex = line.find(' ')
+ if spaceIndex != -1:
+ cmd = line[:spaceIndex]
+ args = (line[spaceIndex + 1:],)
+ else:
+ cmd = line
+ args = ()
+ d = defer.maybeDeferred(self.processCommand, cmd, *args)
+ d.addCallbacks(processSucceeded, processFailed)
+ d.addErrback(log.err)
+
+ # XXX It burnsss
+ # LineReceiver doesn't let you resumeProducing inside
+ # lineReceived atm
+ from twisted.internet import reactor
+ reactor.callLater(0, d.addBoth, allDone)
+
+
+ def processCommand(self, cmd, *params):
+ cmd = cmd.upper()
+
+ if self.state == self.UNAUTH:
+ if cmd == 'USER':
+ return self.ftp_USER(*params)
+ elif cmd == 'PASS':
+ return BAD_CMD_SEQ, "USER required before PASS"
+ else:
+ return NOT_LOGGED_IN
+
+ elif self.state == self.INAUTH:
+ if cmd == 'PASS':
+ return self.ftp_PASS(*params)
+ else:
+ return BAD_CMD_SEQ, "PASS required after USER"
+
+ elif self.state == self.AUTHED:
+ method = getattr(self, "ftp_" + cmd, None)
+ if method is not None:
+ return method(*params)
+ return defer.fail(CmdNotImplementedError(cmd))
+
+ elif self.state == self.RENAMING:
+ if cmd == 'RNTO':
+ return self.ftp_RNTO(*params)
+ else:
+ return BAD_CMD_SEQ, "RNTO required after RNFR"
+
+
+ def getDTPPort(self, factory):
+ """
+ Return a port for passive access, using C{self.passivePortRange}
+ attribute.
+ """
+ for portn in self.passivePortRange:
+ try:
+ dtpPort = self.listenFactory(portn, factory)
+ except error.CannotListenError:
+ continue
+ else:
+ return dtpPort
+ raise error.CannotListenError('', portn,
+ "No port available in range %s" %
+ (self.passivePortRange,))
+
+
+ def ftp_USER(self, username):
+ """
+ First part of login. Get the username the peer wants to
+ authenticate as.
+ """
+ if not username:
+ return defer.fail(CmdSyntaxError('USER requires an argument'))
+
+ self._user = username
+ self.state = self.INAUTH
+ if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
+ return GUEST_NAME_OK_NEED_EMAIL
+ else:
+ return (USR_NAME_OK_NEED_PASS, username)
+
+ # TODO: add max auth try before timeout from ip...
+ # TODO: need to implement minimal ABOR command
+
+ def ftp_PASS(self, password):
+ """
+ Second part of login. Get the password the peer wants to
+ authenticate with.
+ """
+ if self.factory.allowAnonymous and self._user == self.factory.userAnonymous:
+ # anonymous login
+ creds = credentials.Anonymous()
+ reply = GUEST_LOGGED_IN_PROCEED
+ else:
+ # user login
+ creds = credentials.UsernamePassword(self._user, password)
+ reply = USR_LOGGED_IN_PROCEED
+ del self._user
+
+ def _cbLogin((interface, avatar, logout)):
+ assert interface is IFTPShell, "The realm is busted, jerk."
+ self.shell = avatar
+ self.logout = logout
+ self.workingDirectory = []
+ self.state = self.AUTHED
+ return reply
+
+ def _ebLogin(failure):
+ failure.trap(cred_error.UnauthorizedLogin, cred_error.UnhandledCredentials)
+ self.state = self.UNAUTH
+ raise AuthorizationError
+
+ d = self.portal.login(creds, None, IFTPShell)
+ d.addCallbacks(_cbLogin, _ebLogin)
+ return d
+
+
+ def ftp_PASV(self):
+ """Request for a passive connection
+
+ from the rfc::
+
+ This command requests the server-DTP to \"listen\" on a data port
+ (which is not its default data port) and to wait for a connection
+ rather than initiate one upon receipt of a transfer command. The
+ response to this command includes the host and port address this
+ server is listening on.
+ """
+ # if we have a DTP port set up, lose it.
+ if self.dtpFactory is not None:
+ # cleanupDTP sets dtpFactory to none. Later we'll do
+ # cleanup here or something.
+ self.cleanupDTP()
+ self.dtpFactory = DTPFactory(pi=self)
+ self.dtpFactory.setTimeout(self.dtpTimeout)
+ self.dtpPort = self.getDTPPort(self.dtpFactory)
+
+ host = self.transport.getHost().host
+ port = self.dtpPort.getHost().port
+ self.reply(ENTERING_PASV_MODE, encodeHostPort(host, port))
+ return self.dtpFactory.deferred.addCallback(lambda ign: None)
+
+
+ def ftp_PORT(self, address):
+ addr = map(int, address.split(','))
+ ip = '%d.%d.%d.%d' % tuple(addr[:4])
+ port = addr[4] << 8 | addr[5]
+
+ # if we have a DTP port set up, lose it.
+ if self.dtpFactory is not None:
+ self.cleanupDTP()
+
+ self.dtpFactory = DTPFactory(pi=self, peerHost=self.transport.getPeer().host)
+ self.dtpFactory.setTimeout(self.dtpTimeout)
+ self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
+
+ def connected(ignored):
+ return ENTERING_PORT_MODE
+ def connFailed(err):
+ err.trap(PortConnectionError)
+ return CANT_OPEN_DATA_CNX
+ return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
+
+
+ def ftp_LIST(self, path=''):
+ """ This command causes a list to be sent from the server to the
+ passive DTP. If the pathname specifies a directory or other
+ group of files, the server should transfer a list of files
+ in the specified directory. If the pathname specifies a
+ file then the server should send current information on the
+ file. A null argument implies the user's current working or
+ default directory.
+ """
+ # Uh, for now, do this retarded thing.
+ if self.dtpInstance is None or not self.dtpInstance.isConnected:
+ return defer.fail(BadCmdSequenceError('must send PORT or PASV before RETR'))
+
+ # bug in konqueror
+ if path == "-a":
+ path = ''
+ # bug in gFTP 2.0.15
+ if path == "-aL":
+ path = ''
+ # bug in Nautilus 2.10.0
+ if path == "-L":
+ path = ''
+ # bug in ange-ftp
+ if path == "-la":
+ path = ''
+
+ def gotListing(results):
+ self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
+ for (name, attrs) in results:
+ self.dtpInstance.sendListResponse(name, attrs)
+ self.dtpInstance.transport.loseConnection()
+ return (TXFR_COMPLETE_OK,)
+
+ try:
+ segments = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ d = self.shell.list(
+ segments,
+ ('size', 'directory', 'permissions', 'hardlinks',
+ 'modified', 'owner', 'group'))
+ d.addCallback(gotListing)
+ return d
+
+
+ def ftp_NLST(self, path):
+ """
+ This command causes a directory listing to be sent from the server to
+ the client. The pathname should specify a directory or other
+ system-specific file group descriptor. An empty path implies the current
+ working directory. If the path is non-existent, send nothing. If the
+ path is to a file, send only the file name.
+
+ @type path: C{str}
+ @param path: The path for which a directory listing should be returned.
+
+ @rtype: L{Deferred}
+ @return: a L{Deferred} which will be fired when the listing request
+ is finished.
+ """
+ # XXX: why is this check different from ftp_RETR/ftp_STOR? See #4180
+ if self.dtpInstance is None or not self.dtpInstance.isConnected:
+ return defer.fail(
+ BadCmdSequenceError('must send PORT or PASV before RETR'))
+
+ try:
+ segments = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ def cbList(results):
+ """
+ Send, line by line, each file in the directory listing, and then
+ close the connection.
+
+ @type results: A C{list} of C{tuple}. The first element of each
+ C{tuple} is a C{str} and the second element is a C{list}.
+ @param results: The names of the files in the directory.
+
+ @rtype: C{tuple}
+ @return: A C{tuple} containing the status code for a successful
+ transfer.
+ """
+ self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
+ for (name, ignored) in results:
+ self.dtpInstance.sendLine(name)
+ self.dtpInstance.transport.loseConnection()
+ return (TXFR_COMPLETE_OK,)
+
+ def cbGlob(results):
+ self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
+ for (name, ignored) in results:
+ if fnmatch.fnmatch(name, segments[-1]):
+ self.dtpInstance.sendLine(name)
+ self.dtpInstance.transport.loseConnection()
+ return (TXFR_COMPLETE_OK,)
+
+ def listErr(results):
+ """
+ RFC 959 specifies that an NLST request may only return directory
+ listings. Thus, send nothing and just close the connection.
+
+ @type results: L{Failure}
+ @param results: The L{Failure} wrapping a L{FileNotFoundError} that
+ occurred while trying to list the contents of a nonexistent
+ directory.
+
+ @rtype: C{tuple}
+ @returns: A C{tuple} containing the status code for a successful
+ transfer.
+ """
+ self.dtpInstance.transport.loseConnection()
+ return (TXFR_COMPLETE_OK,)
+
+ # XXX This globbing may be incomplete: see #4181
+ if segments and (
+ '*' in segments[-1] or '?' in segments[-1] or
+ ('[' in segments[-1] and ']' in segments[-1])):
+ d = self.shell.list(segments[:-1])
+ d.addCallback(cbGlob)
+ else:
+ d = self.shell.list(segments)
+ d.addCallback(cbList)
+ # self.shell.list will generate an error if the path is invalid
+ d.addErrback(listErr)
+ return d
+
+
+ def ftp_CWD(self, path):
+ try:
+ segments = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ # XXX Eh, what to fail with here?
+ return defer.fail(FileNotFoundError(path))
+
+ def accessGranted(result):
+ self.workingDirectory = segments
+ return (REQ_FILE_ACTN_COMPLETED_OK,)
+
+ return self.shell.access(segments).addCallback(accessGranted)
+
+
+ def ftp_CDUP(self):
+ return self.ftp_CWD('..')
+
+
+ def ftp_PWD(self):
+ return (PWD_REPLY, '/' + '/'.join(self.workingDirectory))
+
+
+ def ftp_RETR(self, path):
+ """
+ This command causes the content of a file to be sent over the data
+ transfer channel. If the path is to a folder, an error will be raised.
+
+ @type path: C{str}
+ @param path: The path to the file which should be transferred over the
+ data transfer channel.
+
+ @rtype: L{Deferred}
+ @return: a L{Deferred} which will be fired when the transfer is done.
+ """
+ if self.dtpInstance is None:
+ raise BadCmdSequenceError('PORT or PASV required before RETR')
+
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ # XXX For now, just disable the timeout. Later we'll want to
+ # leave it active and have the DTP connection reset it
+ # periodically.
+ self.setTimeout(None)
+
+ # Put it back later
+ def enableTimeout(result):
+ self.setTimeout(self.factory.timeOut)
+ return result
+
+ # And away she goes
+ if not self.binary:
+ cons = ASCIIConsumerWrapper(self.dtpInstance)
+ else:
+ cons = self.dtpInstance
+
+ def cbSent(result):
+ return (TXFR_COMPLETE_OK,)
+
+ def ebSent(err):
+ log.msg("Unexpected error attempting to transmit file to client:")
+ log.err(err)
+ return (CNX_CLOSED_TXFR_ABORTED,)
+
+ def cbOpened(file):
+ # Tell them what to doooo
+ if self.dtpInstance.isConnected:
+ self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
+ else:
+ self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
+
+ d = file.send(cons)
+ d.addCallbacks(cbSent, ebSent)
+ return d
+
+ def ebOpened(err):
+ if not err.check(PermissionDeniedError, FileNotFoundError, IsADirectoryError):
+ log.msg("Unexpected error attempting to open file for transmission:")
+ log.err(err)
+ if err.check(FTPCmdError):
+ return (err.value.errorCode, '/'.join(newsegs))
+ return (FILE_NOT_FOUND, '/'.join(newsegs))
+
+ d = self.shell.openForReading(newsegs)
+ d.addCallbacks(cbOpened, ebOpened)
+ d.addBoth(enableTimeout)
+
+ # Pass back Deferred that fires when the transfer is done
+ return d
+
+
+ def ftp_STOR(self, path):
+ if self.dtpInstance is None:
+ raise BadCmdSequenceError('PORT or PASV required before STOR')
+
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ # XXX For now, just disable the timeout. Later we'll want to
+ # leave it active and have the DTP connection reset it
+ # periodically.
+ self.setTimeout(None)
+
+ # Put it back later
+ def enableTimeout(result):
+ self.setTimeout(self.factory.timeOut)
+ return result
+
+ def cbSent(result):
+ return (TXFR_COMPLETE_OK,)
+
+ def ebSent(err):
+ log.msg("Unexpected error receiving file from client:")
+ log.err(err)
+ if err.check(FTPCmdError):
+ return err
+ return (CNX_CLOSED_TXFR_ABORTED,)
+
+ def cbConsumer(cons):
+ if not self.binary:
+ cons = ASCIIConsumerWrapper(cons)
+
+ d = self.dtpInstance.registerConsumer(cons)
+
+ # Tell them what to doooo
+ if self.dtpInstance.isConnected:
+ self.reply(DATA_CNX_ALREADY_OPEN_START_XFR)
+ else:
+ self.reply(FILE_STATUS_OK_OPEN_DATA_CNX)
+
+ return d
+
+ def cbOpened(file):
+ d = file.receive()
+ d.addCallback(cbConsumer)
+ d.addCallback(lambda ignored: file.close())
+ d.addCallbacks(cbSent, ebSent)
+ return d
+
+ def ebOpened(err):
+ if not err.check(PermissionDeniedError, FileNotFoundError, IsNotADirectoryError):
+ log.msg("Unexpected error attempting to open file for upload:")
+ log.err(err)
+ if isinstance(err.value, FTPCmdError):
+ return (err.value.errorCode, '/'.join(newsegs))
+ return (FILE_NOT_FOUND, '/'.join(newsegs))
+
+ d = self.shell.openForWriting(newsegs)
+ d.addCallbacks(cbOpened, ebOpened)
+ d.addBoth(enableTimeout)
+
+ # Pass back Deferred that fires when the transfer is done
+ return d
+
+
+ def ftp_SIZE(self, path):
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ def cbStat((size,)):
+ return (FILE_STATUS, str(size))
+
+ return self.shell.stat(newsegs, ('size',)).addCallback(cbStat)
+
+
+ def ftp_MDTM(self, path):
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+
+ def cbStat((modified,)):
+ return (FILE_STATUS, time.strftime('%Y%m%d%H%M%S', time.gmtime(modified)))
+
+ return self.shell.stat(newsegs, ('modified',)).addCallback(cbStat)
+
+
+ def ftp_TYPE(self, type):
+ p = type.upper()
+ if p:
+ f = getattr(self, 'type_' + p[0], None)
+ if f is not None:
+ return f(p[1:])
+ return self.type_UNKNOWN(p)
+ return (SYNTAX_ERR,)
+
+ def type_A(self, code):
+ if code == '' or code == 'N':
+ self.binary = False
+ return (TYPE_SET_OK, 'A' + code)
+ else:
+ return defer.fail(CmdArgSyntaxError(code))
+
+ def type_I(self, code):
+ if code == '':
+ self.binary = True
+ return (TYPE_SET_OK, 'I')
+ else:
+ return defer.fail(CmdArgSyntaxError(code))
+
+ def type_UNKNOWN(self, code):
+ return defer.fail(CmdNotImplementedForArgError(code))
+
+
+
+ def ftp_SYST(self):
+ return NAME_SYS_TYPE
+
+
+ def ftp_STRU(self, structure):
+ p = structure.upper()
+ if p == 'F':
+ return (CMD_OK,)
+ return defer.fail(CmdNotImplementedForArgError(structure))
+
+
+ def ftp_MODE(self, mode):
+ p = mode.upper()
+ if p == 'S':
+ return (CMD_OK,)
+ return defer.fail(CmdNotImplementedForArgError(mode))
+
+
+ def ftp_MKD(self, path):
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+ return self.shell.makeDirectory(newsegs).addCallback(lambda ign: (MKD_REPLY, path))
+
+
+ def ftp_RMD(self, path):
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+ return self.shell.removeDirectory(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
+
+
+ def ftp_DELE(self, path):
+ try:
+ newsegs = toSegments(self.workingDirectory, path)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(path))
+ return self.shell.removeFile(newsegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
+
+
+ def ftp_NOOP(self):
+ return (CMD_OK,)
+
+
+ def ftp_RNFR(self, fromName):
+ self._fromName = fromName
+ self.state = self.RENAMING
+ return (REQ_FILE_ACTN_PENDING_FURTHER_INFO,)
+
+
+ def ftp_RNTO(self, toName):
+ fromName = self._fromName
+ del self._fromName
+ self.state = self.AUTHED
+
+ try:
+ fromsegs = toSegments(self.workingDirectory, fromName)
+ tosegs = toSegments(self.workingDirectory, toName)
+ except InvalidPath:
+ return defer.fail(FileNotFoundError(fromName))
+ return self.shell.rename(fromsegs, tosegs).addCallback(lambda ign: (REQ_FILE_ACTN_COMPLETED_OK,))
+
+
+ def ftp_QUIT(self):
+ self.reply(GOODBYE_MSG)
+ self.transport.loseConnection()
+ self.disconnected = True
+
+
+ def cleanupDTP(self):
+ """call when DTP connection exits
+ """
+ log.msg('cleanupDTP', debug=True)
+
+ log.msg(self.dtpPort)
+ dtpPort, self.dtpPort = self.dtpPort, None
+ if interfaces.IListeningPort.providedBy(dtpPort):
+ dtpPort.stopListening()
+ elif interfaces.IConnector.providedBy(dtpPort):
+ dtpPort.disconnect()
+ else:
+ assert False, "dtpPort should be an IListeningPort or IConnector, instead is %r" % (dtpPort,)
+
+ self.dtpFactory.stopFactory()
+ self.dtpFactory = None
+
+ if self.dtpInstance is not None:
+ self.dtpInstance = None
+
+
+class FTPFactory(policies.LimitTotalConnectionsFactory):
+ """
+ A factory for producing ftp protocol instances
+
+ @ivar timeOut: the protocol interpreter's idle timeout time in seconds,
+ default is 600 seconds.
+
+ @ivar passivePortRange: value forwarded to C{protocol.passivePortRange}.
+ @type passivePortRange: C{iterator}
+ """
+ protocol = FTP
+ overflowProtocol = FTPOverflowProtocol
+ allowAnonymous = True
+ userAnonymous = 'anonymous'
+ timeOut = 600
+
+ welcomeMessage = "Twisted %s FTP Server" % (copyright.version,)
+
+ passivePortRange = xrange(0, 1)
+
+ def __init__(self, portal=None, userAnonymous='anonymous'):
+ self.portal = portal
+ self.userAnonymous = userAnonymous
+ self.instances = []
+
+ def buildProtocol(self, addr):
+ p = policies.LimitTotalConnectionsFactory.buildProtocol(self, addr)
+ if p is not None:
+ p.wrappedProtocol.portal = self.portal
+ p.wrappedProtocol.timeOut = self.timeOut
+ p.wrappedProtocol.passivePortRange = self.passivePortRange
+ return p
+
+ def stopFactory(self):
+ # make sure ftp instance's timeouts are set to None
+ # to avoid reactor complaints
+ [p.setTimeout(None) for p in self.instances if p.timeOut is not None]
+ policies.LimitTotalConnectionsFactory.stopFactory(self)
+
+# -- Cred Objects --
+
+
+class IFTPShell(Interface):
+ """
+ An abstraction of the shell commands used by the FTP protocol for
+ a given user account.
+
+ All path names must be absolute.
+ """
+
+ def makeDirectory(path):
+ """
+ Create a directory.
+
+ @param path: The path, as a list of segments, to create
+ @type path: C{list} of C{unicode}
+
+ @return: A Deferred which fires when the directory has been
+ created, or which fails if the directory cannot be created.
+ """
+
+
+ def removeDirectory(path):
+ """
+ Remove a directory.
+
+ @param path: The path, as a list of segments, to remove
+ @type path: C{list} of C{unicode}
+
+ @return: A Deferred which fires when the directory has been
+ removed, or which fails if the directory cannot be removed.
+ """
+
+
+ def removeFile(path):
+ """
+ Remove a file.
+
+ @param path: The path, as a list of segments, to remove
+ @type path: C{list} of C{unicode}
+
+ @return: A Deferred which fires when the file has been
+ removed, or which fails if the file cannot be removed.
+ """
+
+
+ def rename(fromPath, toPath):
+ """
+ Rename a file or directory.
+
+ @param fromPath: The current name of the path.
+ @type fromPath: C{list} of C{unicode}
+
+ @param toPath: The desired new name of the path.
+ @type toPath: C{list} of C{unicode}
+
+ @return: A Deferred which fires when the path has been
+ renamed, or which fails if the path cannot be renamed.
+ """
+
+
+ def access(path):
+ """
+ Determine whether access to the given path is allowed.
+
+ @param path: The path, as a list of segments
+
+ @return: A Deferred which fires with None if access is allowed
+ or which fails with a specific exception type if access is
+ denied.
+ """
+
+
+ def stat(path, keys=()):
+ """
+ Retrieve information about the given path.
+
+ This is like list, except it will never return results about
+ child paths.
+ """
+
+
+ def list(path, keys=()):
+ """
+ Retrieve information about the given path.
+
+ If the path represents a non-directory, the result list should
+ have only one entry with information about that non-directory.
+ Otherwise, the result list should have an element for each
+ child of the directory.
+
+ @param path: The path, as a list of segments, to list
+ @type path: C{list} of C{unicode}
+
+ @param keys: A tuple of keys desired in the resulting
+ dictionaries.
+
+ @return: A Deferred which fires with a list of (name, list),
+ where the name is the name of the entry as a unicode string
+ and each list contains values corresponding to the requested
+ keys. The following are possible elements of keys, and the
+ values which should be returned for them:
+
+ - C{'size'}: size in bytes, as an integer (this is kinda required)
+
+ - C{'directory'}: boolean indicating the type of this entry
+
+ - C{'permissions'}: a bitvector (see os.stat(foo).st_mode)
+
+ - C{'hardlinks'}: Number of hard links to this entry
+
+ - C{'modified'}: number of seconds since the epoch since entry was
+ modified
+
+ - C{'owner'}: string indicating the user owner of this entry
+
+ - C{'group'}: string indicating the group owner of this entry
+ """
+
+
+ def openForReading(path):
+ """
+ @param path: The path, as a list of segments, to open
+ @type path: C{list} of C{unicode}
+
+ @rtype: C{Deferred} which will fire with L{IReadFile}
+ """
+
+
+ def openForWriting(path):
+ """
+ @param path: The path, as a list of segments, to open
+ @type path: C{list} of C{unicode}
+
+ @rtype: C{Deferred} which will fire with L{IWriteFile}
+ """
+
+
+
+class IReadFile(Interface):
+ """
+ A file out of which bytes may be read.
+ """
+
+ def send(consumer):
+ """
+ Produce the contents of the given path to the given consumer. This
+ method may only be invoked once on each provider.
+
+ @type consumer: C{IConsumer}
+
+ @return: A Deferred which fires when the file has been
+ consumed completely.
+ """
+
+
+
+class IWriteFile(Interface):
+ """
+ A file into which bytes may be written.
+ """
+
+ def receive():
+ """
+ Create a consumer which will write to this file. This method may
+ only be invoked once on each provider.
+
+ @rtype: C{Deferred} of C{IConsumer}
+ """
+
+ def close():
+ """
+ Perform any post-write work that needs to be done. This method may
+ only be invoked once on each provider, and will always be invoked
+ after receive().
+
+ @rtype: C{Deferred} of anything: the value is ignored. The FTP client
+ will not see their upload request complete until this Deferred has
+ been fired.
+ """
+
+def _getgroups(uid):
+ """Return the primary and supplementary groups for the given UID.
+
+ @type uid: C{int}
+ """
+ result = []
+ pwent = pwd.getpwuid(uid)
+
+ result.append(pwent.pw_gid)
+
+ for grent in grp.getgrall():
+ if pwent.pw_name in grent.gr_mem:
+ result.append(grent.gr_gid)
+
+ return result
+
+
+def _testPermissions(uid, gid, spath, mode='r'):
+ """
+ checks to see if uid has proper permissions to access path with mode
+
+ @type uid: C{int}
+ @param uid: numeric user id
+
+ @type gid: C{int}
+ @param gid: numeric group id
+
+ @type spath: C{str}
+ @param spath: the path on the server to test
+
+ @type mode: C{str}
+ @param mode: 'r' or 'w' (read or write)
+
+ @rtype: C{bool}
+ @return: True if the given credentials have the specified form of
+ access to the given path
+ """
+ if mode == 'r':
+ usr = stat.S_IRUSR
+ grp = stat.S_IRGRP
+ oth = stat.S_IROTH
+ amode = os.R_OK
+ elif mode == 'w':
+ usr = stat.S_IWUSR
+ grp = stat.S_IWGRP
+ oth = stat.S_IWOTH
+ amode = os.W_OK
+ else:
+ raise ValueError("Invalid mode %r: must specify 'r' or 'w'" % (mode,))
+
+ access = False
+ if os.path.exists(spath):
+ if uid == 0:
+ access = True
+ else:
+ s = os.stat(spath)
+ if usr & s.st_mode and uid == s.st_uid:
+ access = True
+ elif grp & s.st_mode and gid in _getgroups(uid):
+ access = True
+ elif oth & s.st_mode:
+ access = True
+
+ if access:
+ if not os.access(spath, amode):
+ access = False
+ log.msg("Filesystem grants permission to UID %d but it is inaccessible to me running as UID %d" % (
+ uid, os.getuid()))
+ return access
+
+
+
+class FTPAnonymousShell(object):
+ """
+ An anonymous implementation of IFTPShell
+
+ @type filesystemRoot: L{twisted.python.filepath.FilePath}
+ @ivar filesystemRoot: The path which is considered the root of
+ this shell.
+ """
+ implements(IFTPShell)
+
+ def __init__(self, filesystemRoot):
+ self.filesystemRoot = filesystemRoot
+
+
+ def _path(self, path):
+ return reduce(filepath.FilePath.child, path, self.filesystemRoot)
+
+
+ def makeDirectory(self, path):
+ return defer.fail(AnonUserDeniedError())
+
+
+ def removeDirectory(self, path):
+ return defer.fail(AnonUserDeniedError())
+
+
+ def removeFile(self, path):
+ return defer.fail(AnonUserDeniedError())
+
+
+ def rename(self, fromPath, toPath):
+ return defer.fail(AnonUserDeniedError())
+
+
+ def receive(self, path):
+ path = self._path(path)
+ return defer.fail(AnonUserDeniedError())
+
+
+ def openForReading(self, path):
+ """
+ Open C{path} for reading.
+
+ @param path: The path, as a list of segments, to open.
+ @type path: C{list} of C{unicode}
+ @return: A L{Deferred} is returned that will fire with an object
+ implementing L{IReadFile} if the file is successfully opened. If
+ C{path} is a directory, or if an exception is raised while trying
+ to open the file, the L{Deferred} will fire with an error.
+ """
+ p = self._path(path)
+ if p.isdir():
+ # Normally, we would only check for EISDIR in open, but win32
+ # returns EACCES in this case, so we check before
+ return defer.fail(IsADirectoryError(path))
+ try:
+ f = p.open('r')
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(_FileReader(f))
+
+
+ def openForWriting(self, path):
+ """
+ Reject write attempts by anonymous users with
+ L{PermissionDeniedError}.
+ """
+ return defer.fail(PermissionDeniedError("STOR not allowed"))
+
+
+ def access(self, path):
+ p = self._path(path)
+ if not p.exists():
+ # Again, win32 doesn't report a sane error after, so let's fail
+ # early if we can
+ return defer.fail(FileNotFoundError(path))
+ # For now, just see if we can os.listdir() it
+ try:
+ p.listdir()
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(None)
+
+
+ def stat(self, path, keys=()):
+ p = self._path(path)
+ if p.isdir():
+ try:
+ statResult = self._statNode(p, keys)
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(statResult)
+ else:
+ return self.list(path, keys).addCallback(lambda res: res[0][1])
+
+
+ def list(self, path, keys=()):
+ """
+ Return the list of files at given C{path}, adding C{keys} stat
+ informations if specified.
+
+ @param path: the directory or file to check.
+ @type path: C{str}
+
+ @param keys: the list of desired metadata
+ @type keys: C{list} of C{str}
+ """
+ filePath = self._path(path)
+ if filePath.isdir():
+ entries = filePath.listdir()
+ fileEntries = [filePath.child(p) for p in entries]
+ elif filePath.isfile():
+ entries = [os.path.join(*filePath.segmentsFrom(self.filesystemRoot))]
+ fileEntries = [filePath]
+ else:
+ return defer.fail(FileNotFoundError(path))
+
+ results = []
+ for fileName, filePath in zip(entries, fileEntries):
+ ent = []
+ results.append((fileName, ent))
+ if keys:
+ try:
+ ent.extend(self._statNode(filePath, keys))
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, fileName)
+ except:
+ return defer.fail()
+
+ return defer.succeed(results)
+
+
+ def _statNode(self, filePath, keys):
+ """
+ Shortcut method to get stat info on a node.
+
+ @param filePath: the node to stat.
+ @type filePath: C{filepath.FilePath}
+
+ @param keys: the stat keys to get.
+ @type keys: C{iterable}
+ """
+ filePath.restat()
+ return [getattr(self, '_stat_' + k)(filePath.statinfo) for k in keys]
+
+ _stat_size = operator.attrgetter('st_size')
+ _stat_permissions = operator.attrgetter('st_mode')
+ _stat_hardlinks = operator.attrgetter('st_nlink')
+ _stat_modified = operator.attrgetter('st_mtime')
+
+
+ def _stat_owner(self, st):
+ if pwd is not None:
+ try:
+ return pwd.getpwuid(st.st_uid)[0]
+ except KeyError:
+ pass
+ return str(st.st_uid)
+
+
+ def _stat_group(self, st):
+ if grp is not None:
+ try:
+ return grp.getgrgid(st.st_gid)[0]
+ except KeyError:
+ pass
+ return str(st.st_gid)
+
+
+ def _stat_directory(self, st):
+ return bool(st.st_mode & stat.S_IFDIR)
+
+
+
+class _FileReader(object):
+ implements(IReadFile)
+
+ def __init__(self, fObj):
+ self.fObj = fObj
+ self._send = False
+
+ def _close(self, passthrough):
+ self._send = True
+ self.fObj.close()
+ return passthrough
+
+ def send(self, consumer):
+ assert not self._send, "Can only call IReadFile.send *once* per instance"
+ self._send = True
+ d = basic.FileSender().beginFileTransfer(self.fObj, consumer)
+ d.addBoth(self._close)
+ return d
+
+
+
+class FTPShell(FTPAnonymousShell):
+ """
+ An authenticated implementation of L{IFTPShell}.
+ """
+
+ def makeDirectory(self, path):
+ p = self._path(path)
+ try:
+ p.makedirs()
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(None)
+
+
+ def removeDirectory(self, path):
+ p = self._path(path)
+ if p.isfile():
+ # Win32 returns the wrong errno when rmdir is called on a file
+ # instead of a directory, so as we have the info here, let's fail
+ # early with a pertinent error
+ return defer.fail(IsNotADirectoryError(path))
+ try:
+ os.rmdir(p.path)
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(None)
+
+
+ def removeFile(self, path):
+ p = self._path(path)
+ if p.isdir():
+ # Win32 returns the wrong errno when remove is called on a
+ # directory instead of a file, so as we have the info here,
+ # let's fail early with a pertinent error
+ return defer.fail(IsADirectoryError(path))
+ try:
+ p.remove()
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(None)
+
+
+ def rename(self, fromPath, toPath):
+ fp = self._path(fromPath)
+ tp = self._path(toPath)
+ try:
+ os.rename(fp.path, tp.path)
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, fromPath)
+ except:
+ return defer.fail()
+ else:
+ return defer.succeed(None)
+
+
+ def openForWriting(self, path):
+ """
+ Open C{path} for writing.
+
+ @param path: The path, as a list of segments, to open.
+ @type path: C{list} of C{unicode}
+ @return: A L{Deferred} is returned that will fire with an object
+ implementing L{IWriteFile} if the file is successfully opened. If
+ C{path} is a directory, or if an exception is raised while trying
+ to open the file, the L{Deferred} will fire with an error.
+ """
+ p = self._path(path)
+ if p.isdir():
+ # Normally, we would only check for EISDIR in open, but win32
+ # returns EACCES in this case, so we check before
+ return defer.fail(IsADirectoryError(path))
+ try:
+ fObj = p.open('w')
+ except (IOError, OSError), e:
+ return errnoToFailure(e.errno, path)
+ except:
+ return defer.fail()
+ return defer.succeed(_FileWriter(fObj))
+
+
+
+class _FileWriter(object):
+ implements(IWriteFile)
+
+ def __init__(self, fObj):
+ self.fObj = fObj
+ self._receive = False
+
+ def receive(self):
+ assert not self._receive, "Can only call IWriteFile.receive *once* per instance"
+ self._receive = True
+ # FileConsumer will close the file object
+ return defer.succeed(FileConsumer(self.fObj))
+
+ def close(self):
+ return defer.succeed(None)
+
+
+
+class BaseFTPRealm:
+ """
+ Base class for simple FTP realms which provides an easy hook for specifying
+ the home directory for each user.
+ """
+ implements(portal.IRealm)
+
+ def __init__(self, anonymousRoot):
+ self.anonymousRoot = filepath.FilePath(anonymousRoot)
+
+
+ def getHomeDirectory(self, avatarId):
+ """
+ Return a L{FilePath} representing the home directory of the given
+ avatar. Override this in a subclass.
+
+ @param avatarId: A user identifier returned from a credentials checker.
+ @type avatarId: C{str}
+
+ @rtype: L{FilePath}
+ """
+ raise NotImplementedError(
+ "%r did not override getHomeDirectory" % (self.__class__,))
+
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ for iface in interfaces:
+ if iface is IFTPShell:
+ if avatarId is checkers.ANONYMOUS:
+ avatar = FTPAnonymousShell(self.anonymousRoot)
+ else:
+ avatar = FTPShell(self.getHomeDirectory(avatarId))
+ return (IFTPShell, avatar,
+ getattr(avatar, 'logout', lambda: None))
+ raise NotImplementedError(
+ "Only IFTPShell interface is supported by this realm")
+
+
+
+class FTPRealm(BaseFTPRealm):
+ """
+ @type anonymousRoot: L{twisted.python.filepath.FilePath}
+ @ivar anonymousRoot: Root of the filesystem to which anonymous
+ users will be granted access.
+
+ @type userHome: L{filepath.FilePath}
+ @ivar userHome: Root of the filesystem containing user home directories.
+ """
+ def __init__(self, anonymousRoot, userHome='/home'):
+ BaseFTPRealm.__init__(self, anonymousRoot)
+ self.userHome = filepath.FilePath(userHome)
+
+
+ def getHomeDirectory(self, avatarId):
+ """
+ Use C{avatarId} as a single path segment to construct a child of
+ C{self.userHome} and return that child.
+ """
+ return self.userHome.child(avatarId)
+
+
+
+class SystemFTPRealm(BaseFTPRealm):
+ """
+ L{SystemFTPRealm} uses system user account information to decide what the
+ home directory for a particular avatarId is.
+
+ This works on POSIX but probably is not reliable on Windows.
+ """
+ def getHomeDirectory(self, avatarId):
+ """
+ Return the system-defined home directory of the system user account with
+ the name C{avatarId}.
+ """
+ path = os.path.expanduser('~' + avatarId)
+ if path.startswith('~'):
+ raise cred_error.UnauthorizedLogin()
+ return filepath.FilePath(path)
+
+
+
+# --- FTP CLIENT -------------------------------------------------------------
+
+####
+# And now for the client...
+
+# Notes:
+# * Reference: http://cr.yp.to/ftp.html
+# * FIXME: Does not support pipelining (which is not supported by all
+# servers anyway). This isn't a functionality limitation, just a
+# small performance issue.
+# * Only has a rudimentary understanding of FTP response codes (although
+# the full response is passed to the caller if they so choose).
+# * Assumes that USER and PASS should always be sent
+# * Always sets TYPE I (binary mode)
+# * Doesn't understand any of the weird, obscure TELNET stuff (\377...)
+# * FIXME: Doesn't share any code with the FTPServer
+
+class ConnectionLost(FTPError):
+ pass
+
+class CommandFailed(FTPError):
+ pass
+
+class BadResponse(FTPError):
+ pass
+
+class UnexpectedResponse(FTPError):
+ pass
+
+class UnexpectedData(FTPError):
+ pass
+
+class FTPCommand:
+ def __init__(self, text=None, public=0):
+ self.text = text
+ self.deferred = defer.Deferred()
+ self.ready = 1
+ self.public = public
+ self.transferDeferred = None
+
+ def fail(self, failure):
+ if self.public:
+ self.deferred.errback(failure)
+
+
+class ProtocolWrapper(protocol.Protocol):
+ def __init__(self, original, deferred):
+ self.original = original
+ self.deferred = deferred
+ def makeConnection(self, transport):
+ self.original.makeConnection(transport)
+ def dataReceived(self, data):
+ self.original.dataReceived(data)
+ def connectionLost(self, reason):
+ self.original.connectionLost(reason)
+ # Signal that transfer has completed
+ self.deferred.callback(None)
+
+
+
+class IFinishableConsumer(interfaces.IConsumer):
+ """
+ A Consumer for producers that finish.
+
+ @since: 11.0
+ """
+
+ def finish():
+ """
+ The producer has finished producing.
+ """
+
+
+
+class SenderProtocol(protocol.Protocol):
+ implements(IFinishableConsumer)
+
+ def __init__(self):
+ # Fired upon connection
+ self.connectedDeferred = defer.Deferred()
+
+ # Fired upon disconnection
+ self.deferred = defer.Deferred()
+
+ #Protocol stuff
+ def dataReceived(self, data):
+ raise UnexpectedData(
+ "Received data from the server on a "
+ "send-only data-connection"
+ )
+
+ def makeConnection(self, transport):
+ protocol.Protocol.makeConnection(self, transport)
+ self.connectedDeferred.callback(self)
+
+ def connectionLost(self, reason):
+ if reason.check(error.ConnectionDone):
+ self.deferred.callback('connection done')
+ else:
+ self.deferred.errback(reason)
+
+ #IFinishableConsumer stuff
+ def write(self, data):
+ self.transport.write(data)
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register the given producer with our transport.
+ """
+ self.transport.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ """
+ Unregister the previously registered producer.
+ """
+ self.transport.unregisterProducer()
+
+ def finish(self):
+ self.transport.loseConnection()
+
+
+def decodeHostPort(line):
+ """Decode an FTP response specifying a host and port.
+
+ @return: a 2-tuple of (host, port).
+ """
+ abcdef = re.sub('[^0-9, ]', '', line)
+ parsed = [int(p.strip()) for p in abcdef.split(',')]
+ for x in parsed:
+ if x < 0 or x > 255:
+ raise ValueError("Out of range", line, x)
+ a, b, c, d, e, f = parsed
+ host = "%s.%s.%s.%s" % (a, b, c, d)
+ port = (int(e) << 8) + int(f)
+ return host, port
+
+def encodeHostPort(host, port):
+ numbers = host.split('.') + [str(port >> 8), str(port % 256)]
+ return ','.join(numbers)
+
+def _unwrapFirstError(failure):
+ failure.trap(defer.FirstError)
+ return failure.value.subFailure
+
+class FTPDataPortFactory(protocol.ServerFactory):
+ """Factory for data connections that use the PORT command
+
+ (i.e. "active" transfers)
+ """
+ noisy = 0
+ def buildProtocol(self, addr):
+ # This is a bit hackish -- we already have a Protocol instance,
+ # so just return it instead of making a new one
+ # FIXME: Reject connections from the wrong address/port
+ # (potential security problem)
+ self.protocol.factory = self
+ self.port.loseConnection()
+ return self.protocol
+
+
+class FTPClientBasic(basic.LineReceiver):
+ """
+ Foundations of an FTP client.
+ """
+ debug = False
+
+ def __init__(self):
+ self.actionQueue = []
+ self.greeting = None
+ self.nextDeferred = defer.Deferred().addCallback(self._cb_greeting)
+ self.nextDeferred.addErrback(self.fail)
+ self.response = []
+ self._failed = 0
+
+ def fail(self, error):
+ """
+ Give an error to any queued deferreds.
+ """
+ self._fail(error)
+
+ def _fail(self, error):
+ """
+ Errback all queued deferreds.
+ """
+ if self._failed:
+ # We're recursing; bail out here for simplicity
+ return error
+ self._failed = 1
+ if self.nextDeferred:
+ try:
+ self.nextDeferred.errback(failure.Failure(ConnectionLost('FTP connection lost', error)))
+ except defer.AlreadyCalledError:
+ pass
+ for ftpCommand in self.actionQueue:
+ ftpCommand.fail(failure.Failure(ConnectionLost('FTP connection lost', error)))
+ return error
+
+ def _cb_greeting(self, greeting):
+ self.greeting = greeting
+
+ def sendLine(self, line):
+ """
+ (Private) Sends a line, unless line is None.
+ """
+ if line is None:
+ return
+ basic.LineReceiver.sendLine(self, line)
+
+ def sendNextCommand(self):
+ """
+ (Private) Processes the next command in the queue.
+ """
+ ftpCommand = self.popCommandQueue()
+ if ftpCommand is None:
+ self.nextDeferred = None
+ return
+ if not ftpCommand.ready:
+ self.actionQueue.insert(0, ftpCommand)
+ reactor.callLater(1.0, self.sendNextCommand)
+ self.nextDeferred = None
+ return
+
+ # FIXME: this if block doesn't belong in FTPClientBasic, it belongs in
+ # FTPClient.
+ if ftpCommand.text == 'PORT':
+ self.generatePortCommand(ftpCommand)
+
+ if self.debug:
+ log.msg('<-- %s' % ftpCommand.text)
+ self.nextDeferred = ftpCommand.deferred
+ self.sendLine(ftpCommand.text)
+
+ def queueCommand(self, ftpCommand):
+ """
+ Add an FTPCommand object to the queue.
+
+ If it's the only thing in the queue, and we are connected and we aren't
+ waiting for a response of an earlier command, the command will be sent
+ immediately.
+
+ @param ftpCommand: an L{FTPCommand}
+ """
+ self.actionQueue.append(ftpCommand)
+ if (len(self.actionQueue) == 1 and self.transport is not None and
+ self.nextDeferred is None):
+ self.sendNextCommand()
+
+ def queueStringCommand(self, command, public=1):
+ """
+ Queues a string to be issued as an FTP command
+
+ @param command: string of an FTP command to queue
+ @param public: a flag intended for internal use by FTPClient. Don't
+ change it unless you know what you're doing.
+
+ @return: a L{Deferred} that will be called when the response to the
+ command has been received.
+ """
+ ftpCommand = FTPCommand(command, public)
+ self.queueCommand(ftpCommand)
+ return ftpCommand.deferred
+
+ def popCommandQueue(self):
+ """
+ Return the front element of the command queue, or None if empty.
+ """
+ if self.actionQueue:
+ return self.actionQueue.pop(0)
+ else:
+ return None
+
+ def queueLogin(self, username, password):
+ """
+ Login: send the username, send the password.
+
+ If the password is C{None}, the PASS command won't be sent. Also, if
+ the response to the USER command has a response code of 230 (User logged
+ in), then PASS won't be sent either.
+ """
+ # Prepare the USER command
+ deferreds = []
+ userDeferred = self.queueStringCommand('USER ' + username, public=0)
+ deferreds.append(userDeferred)
+
+ # Prepare the PASS command (if a password is given)
+ if password is not None:
+ passwordCmd = FTPCommand('PASS ' + password, public=0)
+ self.queueCommand(passwordCmd)
+ deferreds.append(passwordCmd.deferred)
+
+ # Avoid sending PASS if the response to USER is 230.
+ # (ref: http://cr.yp.to/ftp/user.html#user)
+ def cancelPasswordIfNotNeeded(response):
+ if response[0].startswith('230'):
+ # No password needed!
+ self.actionQueue.remove(passwordCmd)
+ return response
+ userDeferred.addCallback(cancelPasswordIfNotNeeded)
+
+ # Error handling.
+ for deferred in deferreds:
+ # If something goes wrong, call fail
+ deferred.addErrback(self.fail)
+ # But also swallow the error, so we don't cause spurious errors
+ deferred.addErrback(lambda x: None)
+
+ def lineReceived(self, line):
+ """
+ (Private) Parses the response messages from the FTP server.
+ """
+ # Add this line to the current response
+ if self.debug:
+ log.msg('--> %s' % line)
+ self.response.append(line)
+
+ # Bail out if this isn't the last line of a response
+ # The last line of response starts with 3 digits followed by a space
+ codeIsValid = re.match(r'\d{3} ', line)
+ if not codeIsValid:
+ return
+
+ code = line[0:3]
+
+ # Ignore marks
+ if code[0] == '1':
+ return
+
+ # Check that we were expecting a response
+ if self.nextDeferred is None:
+ self.fail(UnexpectedResponse(self.response))
+ return
+
+ # Reset the response
+ response = self.response
+ self.response = []
+
+ # Look for a success or error code, and call the appropriate callback
+ if code[0] in ('2', '3'):
+ # Success
+ self.nextDeferred.callback(response)
+ elif code[0] in ('4', '5'):
+ # Failure
+ self.nextDeferred.errback(failure.Failure(CommandFailed(response)))
+ else:
+ # This shouldn't happen unless something screwed up.
+ log.msg('Server sent invalid response code %s' % (code,))
+ self.nextDeferred.errback(failure.Failure(BadResponse(response)))
+
+ # Run the next command
+ self.sendNextCommand()
+
+ def connectionLost(self, reason):
+ self._fail(reason)
+
+
+
+class _PassiveConnectionFactory(protocol.ClientFactory):
+ noisy = False
+
+ def __init__(self, protoInstance):
+ self.protoInstance = protoInstance
+
+ def buildProtocol(self, ignored):
+ self.protoInstance.factory = self
+ return self.protoInstance
+
+ def clientConnectionFailed(self, connector, reason):
+ e = FTPError('Connection Failed', reason)
+ self.protoInstance.deferred.errback(e)
+
+
+
+class FTPClient(FTPClientBasic):
+ """
+ L{FTPClient} is a client implementation of the FTP protocol which
+ exposes FTP commands as methods which return L{Deferred}s.
+
+ Each command method returns a L{Deferred} which is called back when a
+ successful response code (2xx or 3xx) is received from the server or
+ which is error backed if an error response code (4xx or 5xx) is received
+ from the server or if a protocol violation occurs. If an error response
+ code is received, the L{Deferred} fires with a L{Failure} wrapping a
+ L{CommandFailed} instance. The L{CommandFailed} instance is created
+ with a list of the response lines received from the server.
+
+ See U{RFC 959<http://www.ietf.org/rfc/rfc959.txt>} for error code
+ definitions.
+
+ Both active and passive transfers are supported.
+
+ @ivar passive: See description in __init__.
+ """
+ connectFactory = reactor.connectTCP
+
+ def __init__(self, username='anonymous',
+ password='twisted@twistedmatrix.com',
+ passive=1):
+ """
+ Constructor.
+
+ I will login as soon as I receive the welcome message from the server.
+
+ @param username: FTP username
+ @param password: FTP password
+ @param passive: flag that controls if I use active or passive data
+ connections. You can also change this after construction by
+ assigning to C{self.passive}.
+ """
+ FTPClientBasic.__init__(self)
+ self.queueLogin(username, password)
+
+ self.passive = passive
+
+ def fail(self, error):
+ """
+ Disconnect, and also give an error to any queued deferreds.
+ """
+ self.transport.loseConnection()
+ self._fail(error)
+
+ def receiveFromConnection(self, commands, protocol):
+ """
+ Retrieves a file or listing generated by the given command,
+ feeding it to the given protocol.
+
+ @param commands: list of strings of FTP commands to execute then receive
+ the results of (e.g. C{LIST}, C{RETR})
+ @param protocol: A L{Protocol} B{instance} e.g. an
+ L{FTPFileListProtocol}, or something that can be adapted to one.
+ Typically this will be an L{IConsumer} implementation.
+
+ @return: L{Deferred}.
+ """
+ protocol = interfaces.IProtocol(protocol)
+ wrapper = ProtocolWrapper(protocol, defer.Deferred())
+ return self._openDataConnection(commands, wrapper)
+
+ def queueLogin(self, username, password):
+ """
+ Login: send the username, send the password, and
+ set retrieval mode to binary
+ """
+ FTPClientBasic.queueLogin(self, username, password)
+ d = self.queueStringCommand('TYPE I', public=0)
+ # If something goes wrong, call fail
+ d.addErrback(self.fail)
+ # But also swallow the error, so we don't cause spurious errors
+ d.addErrback(lambda x: None)
+
+ def sendToConnection(self, commands):
+ """
+ XXX
+
+ @return: A tuple of two L{Deferred}s:
+ - L{Deferred} L{IFinishableConsumer}. You must call
+ the C{finish} method on the IFinishableConsumer when the file
+ is completely transferred.
+ - L{Deferred} list of control-connection responses.
+ """
+ s = SenderProtocol()
+ r = self._openDataConnection(commands, s)
+ return (s.connectedDeferred, r)
+
+ def _openDataConnection(self, commands, protocol):
+ """
+ This method returns a DeferredList.
+ """
+ cmds = [FTPCommand(command, public=1) for command in commands]
+ cmdsDeferred = defer.DeferredList([cmd.deferred for cmd in cmds],
+ fireOnOneErrback=True, consumeErrors=True)
+ cmdsDeferred.addErrback(_unwrapFirstError)
+
+ if self.passive:
+ # Hack: use a mutable object to sneak a variable out of the
+ # scope of doPassive
+ _mutable = [None]
+ def doPassive(response):
+ """Connect to the port specified in the response to PASV"""
+ host, port = decodeHostPort(response[-1][4:])
+
+ f = _PassiveConnectionFactory(protocol)
+ _mutable[0] = self.connectFactory(host, port, f)
+
+ pasvCmd = FTPCommand('PASV')
+ self.queueCommand(pasvCmd)
+ pasvCmd.deferred.addCallback(doPassive).addErrback(self.fail)
+
+ results = [cmdsDeferred, pasvCmd.deferred, protocol.deferred]
+ d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
+ d.addErrback(_unwrapFirstError)
+
+ # Ensure the connection is always closed
+ def close(x, m=_mutable):
+ m[0] and m[0].disconnect()
+ return x
+ d.addBoth(close)
+
+ else:
+ # We just place a marker command in the queue, and will fill in
+ # the host and port numbers later (see generatePortCommand)
+ portCmd = FTPCommand('PORT')
+
+ # Ok, now we jump through a few hoops here.
+ # This is the problem: a transfer is not to be trusted as complete
+ # until we get both the "226 Transfer complete" message on the
+ # control connection, and the data socket is closed. Thus, we use
+ # a DeferredList to make sure we only fire the callback at the
+ # right time.
+
+ portCmd.transferDeferred = protocol.deferred
+ portCmd.protocol = protocol
+ portCmd.deferred.addErrback(portCmd.transferDeferred.errback)
+ self.queueCommand(portCmd)
+
+ # Create dummy functions for the next callback to call.
+ # These will also be replaced with real functions in
+ # generatePortCommand.
+ portCmd.loseConnection = lambda result: result
+ portCmd.fail = lambda error: error
+
+ # Ensure that the connection always gets closed
+ cmdsDeferred.addErrback(lambda e, pc=portCmd: pc.fail(e) or e)
+
+ results = [cmdsDeferred, portCmd.deferred, portCmd.transferDeferred]
+ d = defer.DeferredList(results, fireOnOneErrback=True, consumeErrors=True)
+ d.addErrback(_unwrapFirstError)
+
+ for cmd in cmds:
+ self.queueCommand(cmd)
+ return d
+
+ def generatePortCommand(self, portCmd):
+ """
+ (Private) Generates the text of a given PORT command.
+ """
+
+ # The problem is that we don't create the listening port until we need
+ # it for various reasons, and so we have to muck about to figure out
+ # what interface and port it's listening on, and then finally we can
+ # create the text of the PORT command to send to the FTP server.
+
+ # FIXME: This method is far too ugly.
+
+ # FIXME: The best solution is probably to only create the data port
+ # once per FTPClient, and just recycle it for each new download.
+ # This should be ok, because we don't pipeline commands.
+
+ # Start listening on a port
+ factory = FTPDataPortFactory()
+ factory.protocol = portCmd.protocol
+ listener = reactor.listenTCP(0, factory)
+ factory.port = listener
+
+ # Ensure we close the listening port if something goes wrong
+ def listenerFail(error, listener=listener):
+ if listener.connected:
+ listener.loseConnection()
+ return error
+ portCmd.fail = listenerFail
+
+ # Construct crufty FTP magic numbers that represent host & port
+ host = self.transport.getHost().host
+ port = listener.getHost().port
+ portCmd.text = 'PORT ' + encodeHostPort(host, port)
+
+ def escapePath(self, path):
+ """
+ Returns a FTP escaped path (replace newlines with nulls).
+ """
+ # Escape newline characters
+ return path.replace('\n', '\0')
+
+ def retrieveFile(self, path, protocol, offset=0):
+ """
+ Retrieve a file from the given path
+
+ This method issues the 'RETR' FTP command.
+
+ The file is fed into the given Protocol instance. The data connection
+ will be passive if self.passive is set.
+
+ @param path: path to file that you wish to receive.
+ @param protocol: a L{Protocol} instance.
+ @param offset: offset to start downloading from
+
+ @return: L{Deferred}
+ """
+ cmds = ['RETR ' + self.escapePath(path)]
+ if offset:
+ cmds.insert(0, ('REST ' + str(offset)))
+ return self.receiveFromConnection(cmds, protocol)
+
+ retr = retrieveFile
+
+ def storeFile(self, path, offset=0):
+ """
+ Store a file at the given path.
+
+ This method issues the 'STOR' FTP command.
+
+ @return: A tuple of two L{Deferred}s:
+ - L{Deferred} L{IFinishableConsumer}. You must call
+ the C{finish} method on the IFinishableConsumer when the file
+ is completely transferred.
+ - L{Deferred} list of control-connection responses.
+ """
+ cmds = ['STOR ' + self.escapePath(path)]
+ if offset:
+ cmds.insert(0, ('REST ' + str(offset)))
+ return self.sendToConnection(cmds)
+
+ stor = storeFile
+
+
+ def rename(self, pathFrom, pathTo):
+ """
+ Rename a file.
+
+ This method issues the I{RNFR}/I{RNTO} command sequence to rename
+ C{pathFrom} to C{pathTo}.
+
+ @param: pathFrom: the absolute path to the file to be renamed
+ @type pathFrom: C{str}
+
+ @param: pathTo: the absolute path to rename the file to.
+ @type pathTo: C{str}
+
+ @return: A L{Deferred} which fires when the rename operation has
+ succeeded or failed. If it succeeds, the L{Deferred} is called
+ back with a two-tuple of lists. The first list contains the
+ responses to the I{RNFR} command. The second list contains the
+ responses to the I{RNTO} command. If either I{RNFR} or I{RNTO}
+ fails, the L{Deferred} is errbacked with L{CommandFailed} or
+ L{BadResponse}.
+ @rtype: L{Deferred}
+
+ @since: 8.2
+ """
+ renameFrom = self.queueStringCommand('RNFR ' + self.escapePath(pathFrom))
+ renameTo = self.queueStringCommand('RNTO ' + self.escapePath(pathTo))
+
+ fromResponse = []
+
+ # Use a separate Deferred for the ultimate result so that Deferred
+ # chaining can't interfere with its result.
+ result = defer.Deferred()
+ # Bundle up all the responses
+ result.addCallback(lambda toResponse: (fromResponse, toResponse))
+
+ def ebFrom(failure):
+ # Make sure the RNTO doesn't run if the RNFR failed.
+ self.popCommandQueue()
+ result.errback(failure)
+
+ # Save the RNFR response to pass to the result Deferred later
+ renameFrom.addCallbacks(fromResponse.extend, ebFrom)
+
+ # Hook up the RNTO to the result Deferred as well
+ renameTo.chainDeferred(result)
+
+ return result
+
+
+ def list(self, path, protocol):
+ """
+ Retrieve a file listing into the given protocol instance.
+
+ This method issues the 'LIST' FTP command.
+
+ @param path: path to get a file listing for.
+ @param protocol: a L{Protocol} instance, probably a
+ L{FTPFileListProtocol} instance. It can cope with most common file
+ listing formats.
+
+ @return: L{Deferred}
+ """
+ if path is None:
+ path = ''
+ return self.receiveFromConnection(['LIST ' + self.escapePath(path)], protocol)
+
+
+ def nlst(self, path, protocol):
+ """
+ Retrieve a short file listing into the given protocol instance.
+
+ This method issues the 'NLST' FTP command.
+
+ NLST (should) return a list of filenames, one per line.
+
+ @param path: path to get short file listing for.
+ @param protocol: a L{Protocol} instance.
+ """
+ if path is None:
+ path = ''
+ return self.receiveFromConnection(['NLST ' + self.escapePath(path)], protocol)
+
+
+ def cwd(self, path):
+ """
+ Issues the CWD (Change Working Directory) command. It's also
+ available as changeDirectory, which parses the result.
+
+ @return: a L{Deferred} that will be called when done.
+ """
+ return self.queueStringCommand('CWD ' + self.escapePath(path))
+
+
+ def changeDirectory(self, path):
+ """
+ Change the directory on the server and parse the result to determine
+ if it was successful or not.
+
+ @type path: C{str}
+ @param path: The path to which to change.
+
+ @return: a L{Deferred} which will be called back when the directory
+ change has succeeded or errbacked if an error occurrs.
+ """
+ warnings.warn(
+ "FTPClient.changeDirectory is deprecated in Twisted 8.2 and "
+ "newer. Use FTPClient.cwd instead.",
+ category=DeprecationWarning,
+ stacklevel=2)
+
+ def cbResult(result):
+ if result[-1][:3] != '250':
+ return failure.Failure(CommandFailed(result))
+ return True
+ return self.cwd(path).addCallback(cbResult)
+
+
+ def makeDirectory(self, path):
+ """
+ Make a directory
+
+ This method issues the MKD command.
+
+ @param path: The path to the directory to create.
+ @type path: C{str}
+
+ @return: A L{Deferred} which fires when the server responds. If the
+ directory is created, the L{Deferred} is called back with the
+ server response. If the server response indicates the directory
+ was not created, the L{Deferred} is errbacked with a L{Failure}
+ wrapping L{CommandFailed} or L{BadResponse}.
+ @rtype: L{Deferred}
+
+ @since: 8.2
+ """
+ return self.queueStringCommand('MKD ' + self.escapePath(path))
+
+
+ def removeFile(self, path):
+ """
+ Delete a file on the server.
+
+ L{removeFile} issues a I{DELE} command to the server to remove the
+ indicated file. Note that this command cannot remove a directory.
+
+ @param path: The path to the file to delete. May be relative to the
+ current dir.
+ @type path: C{str}
+
+ @return: A L{Deferred} which fires when the server responds. On error,
+ it is errbacked with either L{CommandFailed} or L{BadResponse}. On
+ success, it is called back with a list of response lines.
+ @rtype: L{Deferred}
+
+ @since: 8.2
+ """
+ return self.queueStringCommand('DELE ' + self.escapePath(path))
+
+
+ def removeDirectory(self, path):
+ """
+ Delete a directory on the server.
+
+ L{removeDirectory} issues a I{RMD} command to the server to remove the
+ indicated directory. Described in RFC959.
+
+ @param path: The path to the directory to delete. May be relative to
+ the current working directory.
+ @type path: C{str}
+
+ @return: A L{Deferred} which fires when the server responds. On error,
+ it is errbacked with either L{CommandFailed} or L{BadResponse}. On
+ success, it is called back with a list of response lines.
+ @rtype: L{Deferred}
+
+ @since: 11.1
+ """
+ return self.queueStringCommand('RMD ' + self.escapePath(path))
+
+
+ def cdup(self):
+ """
+ Issues the CDUP (Change Directory UP) command.
+
+ @return: a L{Deferred} that will be called when done.
+ """
+ return self.queueStringCommand('CDUP')
+
+
+ def pwd(self):
+ """
+ Issues the PWD (Print Working Directory) command.
+
+ The L{getDirectory} does the same job but automatically parses the
+ result.
+
+ @return: a L{Deferred} that will be called when done. It is up to the
+ caller to interpret the response, but the L{parsePWDResponse} method
+ in this module should work.
+ """
+ return self.queueStringCommand('PWD')
+
+
+ def getDirectory(self):
+ """
+ Returns the current remote directory.
+
+ @return: a L{Deferred} that will be called back with a C{str} giving
+ the remote directory or which will errback with L{CommandFailed}
+ if an error response is returned.
+ """
+ def cbParse(result):
+ try:
+ # The only valid code is 257
+ if int(result[0].split(' ', 1)[0]) != 257:
+ raise ValueError
+ except (IndexError, ValueError):
+ return failure.Failure(CommandFailed(result))
+ path = parsePWDResponse(result[0])
+ if path is None:
+ return failure.Failure(CommandFailed(result))
+ return path
+ return self.pwd().addCallback(cbParse)
+
+
+ def quit(self):
+ """
+ Issues the I{QUIT} command.
+
+ @return: A L{Deferred} that fires when the server acknowledges the
+ I{QUIT} command. The transport should not be disconnected until
+ this L{Deferred} fires.
+ """
+ return self.queueStringCommand('QUIT')
+
+
+
+class FTPFileListProtocol(basic.LineReceiver):
+ """Parser for standard FTP file listings
+
+ This is the evil required to match::
+
+ -rw-r--r-- 1 root other 531 Jan 29 03:26 README
+
+ If you need different evil for a wacky FTP server, you can
+ override either C{fileLinePattern} or C{parseDirectoryLine()}.
+
+ It populates the instance attribute self.files, which is a list containing
+ dicts with the following keys (examples from the above line):
+ - filetype: e.g. 'd' for directories, or '-' for an ordinary file
+ - perms: e.g. 'rw-r--r--'
+ - nlinks: e.g. 1
+ - owner: e.g. 'root'
+ - group: e.g. 'other'
+ - size: e.g. 531
+ - date: e.g. 'Jan 29 03:26'
+ - filename: e.g. 'README'
+ - linktarget: e.g. 'some/file'
+
+ Note that the 'date' value will be formatted differently depending on the
+ date. Check U{http://cr.yp.to/ftp.html} if you really want to try to parse
+ it.
+
+ @ivar files: list of dicts describing the files in this listing
+ """
+ fileLinePattern = re.compile(
+ r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
+ r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
+ r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>([^ ]|\\ )*?)'
+ r'( -> (?P<linktarget>[^\r]*))?\r?$'
+ )
+ delimiter = '\n'
+
+ def __init__(self):
+ self.files = []
+
+ def lineReceived(self, line):
+ d = self.parseDirectoryLine(line)
+ if d is None:
+ self.unknownLine(line)
+ else:
+ self.addFile(d)
+
+ def parseDirectoryLine(self, line):
+ """Return a dictionary of fields, or None if line cannot be parsed.
+
+ @param line: line of text expected to contain a directory entry
+ @type line: str
+
+ @return: dict
+ """
+ match = self.fileLinePattern.match(line)
+ if match is None:
+ return None
+ else:
+ d = match.groupdict()
+ d['filename'] = d['filename'].replace(r'\ ', ' ')
+ d['nlinks'] = int(d['nlinks'])
+ d['size'] = int(d['size'])
+ if d['linktarget']:
+ d['linktarget'] = d['linktarget'].replace(r'\ ', ' ')
+ return d
+
+ def addFile(self, info):
+ """Append file information dictionary to the list of known files.
+
+ Subclasses can override or extend this method to handle file
+ information differently without affecting the parsing of data
+ from the server.
+
+ @param info: dictionary containing the parsed representation
+ of the file information
+ @type info: dict
+ """
+ self.files.append(info)
+
+ def unknownLine(self, line):
+ """Deal with received lines which could not be parsed as file
+ information.
+
+ Subclasses can override this to perform any special processing
+ needed.
+
+ @param line: unparsable line as received
+ @type line: str
+ """
+ pass
+
+def parsePWDResponse(response):
+ """Returns the path from a response to a PWD command.
+
+ Responses typically look like::
+
+ 257 "/home/andrew" is current directory.
+
+ For this example, I will return C{'/home/andrew'}.
+
+ If I can't find the path, I return C{None}.
+ """
+ match = re.search('"(.*)"', response)
+ if match:
+ return match.groups()[0]
+ else:
+ return None
diff --git a/twisted/protocols/gps/__init__.py b/twisted/protocols/gps/__init__.py
new file mode 100644
index 0000000..278648c
--- /dev/null
+++ b/twisted/protocols/gps/__init__.py
@@ -0,0 +1 @@
+"""Global Positioning System protocols."""
diff --git a/twisted/protocols/gps/nmea.py b/twisted/protocols/gps/nmea.py
new file mode 100644
index 0000000..71d37ea
--- /dev/null
+++ b/twisted/protocols/gps/nmea.py
@@ -0,0 +1,209 @@
+# -*- test-case-name: twisted.test.test_nmea -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""NMEA 0183 implementation
+
+Maintainer: Bob Ippolito
+
+The following NMEA 0183 sentences are currently understood::
+ GPGGA (fix)
+ GPGLL (position)
+ GPRMC (position and time)
+ GPGSA (active satellites)
+
+The following NMEA 0183 sentences require implementation::
+ None really, the others aren't generally useful or implemented in most devices anyhow
+
+Other desired features::
+ - A NMEA 0183 producer to emulate GPS devices (?)
+"""
+
+import operator
+from twisted.protocols import basic
+from twisted.python.compat import reduce
+
+POSFIX_INVALID, POSFIX_SPS, POSFIX_DGPS, POSFIX_PPS = 0, 1, 2, 3
+MODE_AUTO, MODE_FORCED = 'A', 'M'
+MODE_NOFIX, MODE_2D, MODE_3D = 1, 2, 3
+
+class InvalidSentence(Exception):
+ pass
+
+class InvalidChecksum(Exception):
+ pass
+
+class NMEAReceiver(basic.LineReceiver):
+ """This parses most common NMEA-0183 messages, presumably from a serial GPS device at 4800 bps
+ """
+ delimiter = '\r\n'
+ dispatch = {
+ 'GPGGA': 'fix',
+ 'GPGLL': 'position',
+ 'GPGSA': 'activesatellites',
+ 'GPRMC': 'positiontime',
+ 'GPGSV': 'viewsatellites', # not implemented
+ 'GPVTG': 'course', # not implemented
+ 'GPALM': 'almanac', # not implemented
+ 'GPGRS': 'range', # not implemented
+ 'GPGST': 'noise', # not implemented
+ 'GPMSS': 'beacon', # not implemented
+ 'GPZDA': 'time', # not implemented
+ }
+ # generally you may miss the beginning of the first message
+ ignore_invalid_sentence = 1
+ # checksums shouldn't be invalid
+ ignore_checksum_mismatch = 0
+ # ignore unknown sentence types
+ ignore_unknown_sentencetypes = 0
+ # do we want to even bother checking to see if it's from the 20th century?
+ convert_dates_before_y2k = 1
+
+ def lineReceived(self, line):
+ if not line.startswith('$'):
+ if self.ignore_invalid_sentence:
+ return
+ raise InvalidSentence("%r does not begin with $" % (line,))
+ # message is everything between $ and *, checksum is xor of all ASCII values of the message
+ strmessage, checksum = line[1:].strip().split('*')
+ message = strmessage.split(',')
+ sentencetype, message = message[0], message[1:]
+ dispatch = self.dispatch.get(sentencetype, None)
+ if (not dispatch) and (not self.ignore_unknown_sentencetypes):
+ raise InvalidSentence("sentencetype %r" % (sentencetype,))
+ if not self.ignore_checksum_mismatch:
+ checksum, calculated_checksum = int(checksum, 16), reduce(operator.xor, map(ord, strmessage))
+ if checksum != calculated_checksum:
+ raise InvalidChecksum("Given 0x%02X != 0x%02X" % (checksum, calculated_checksum))
+ handler = getattr(self, "handle_%s" % dispatch, None)
+ decoder = getattr(self, "decode_%s" % dispatch, None)
+ if not (dispatch and handler and decoder):
+ # missing dispatch, handler, or decoder
+ return
+ # return handler(*decoder(*message))
+ try:
+ decoded = decoder(*message)
+ except Exception, e:
+ raise InvalidSentence("%r is not a valid %s (%s) sentence" % (line, sentencetype, dispatch))
+ return handler(*decoded)
+
+ def decode_position(self, latitude, ns, longitude, ew, utc, status):
+ latitude, longitude = self._decode_latlon(latitude, ns, longitude, ew)
+ utc = self._decode_utc(utc)
+ if status == 'A':
+ status = 1
+ else:
+ status = 0
+ return (
+ latitude,
+ longitude,
+ utc,
+ status,
+ )
+
+ def decode_positiontime(self, utc, status, latitude, ns, longitude, ew, speed, course, utcdate, magvar, magdir):
+ utc = self._decode_utc(utc)
+ latitude, longitude = self._decode_latlon(latitude, ns, longitude, ew)
+ if speed != '':
+ speed = float(speed)
+ else:
+ speed = None
+ if course != '':
+ course = float(course)
+ else:
+ course = None
+ utcdate = 2000+int(utcdate[4:6]), int(utcdate[2:4]), int(utcdate[0:2])
+ if self.convert_dates_before_y2k and utcdate[0] > 2073:
+ # GPS was invented by the US DoD in 1973, but NMEA uses 2 digit year.
+ # Highly unlikely that we'll be using NMEA or this twisted module in 70 years,
+ # but remotely possible that you'll be using it to play back data from the 20th century.
+ utcdate = (utcdate[0] - 100, utcdate[1], utcdate[2])
+ if magvar != '':
+ magvar = float(magvar)
+ if magdir == 'W':
+ magvar = -magvar
+ else:
+ magvar = None
+ return (
+ latitude,
+ longitude,
+ speed,
+ course,
+ # UTC seconds past utcdate
+ utc,
+ # UTC (year, month, day)
+ utcdate,
+ # None or magnetic variation in degrees (west is negative)
+ magvar,
+ )
+
+ def _decode_utc(self, utc):
+ utc_hh, utc_mm, utc_ss = map(float, (utc[:2], utc[2:4], utc[4:]))
+ return utc_hh * 3600.0 + utc_mm * 60.0 + utc_ss
+
+ def _decode_latlon(self, latitude, ns, longitude, ew):
+ latitude = float(latitude[:2]) + float(latitude[2:])/60.0
+ if ns == 'S':
+ latitude = -latitude
+ longitude = float(longitude[:3]) + float(longitude[3:])/60.0
+ if ew == 'W':
+ longitude = -longitude
+ return (latitude, longitude)
+
+ def decode_activesatellites(self, mode1, mode2, *args):
+ satellites, (pdop, hdop, vdop) = args[:12], map(float, args[12:])
+ satlist = []
+ for n in satellites:
+ if n:
+ satlist.append(int(n))
+ else:
+ satlist.append(None)
+ mode = (mode1, int(mode2))
+ return (
+ # satellite list by channel
+ tuple(satlist),
+ # (MODE_AUTO/MODE_FORCED, MODE_NOFIX/MODE_2DFIX/MODE_3DFIX)
+ mode,
+ # position dilution of precision
+ pdop,
+ # horizontal dilution of precision
+ hdop,
+ # vertical dilution of precision
+ vdop,
+ )
+
+ def decode_fix(self, utc, latitude, ns, longitude, ew, posfix, satellites, hdop, altitude, altitude_units, geoid_separation, geoid_separation_units, dgps_age, dgps_station_id):
+ latitude, longitude = self._decode_latlon(latitude, ns, longitude, ew)
+ utc = self._decode_utc(utc)
+ posfix = int(posfix)
+ satellites = int(satellites)
+ hdop = float(hdop)
+ altitude = (float(altitude), altitude_units)
+ if geoid_separation != '':
+ geoid = (float(geoid_separation), geoid_separation_units)
+ else:
+ geoid = None
+ if dgps_age != '':
+ dgps = (float(dgps_age), dgps_station_id)
+ else:
+ dgps = None
+ return (
+ # seconds since 00:00 UTC
+ utc,
+ # latitude (degrees)
+ latitude,
+ # longitude (degrees)
+ longitude,
+ # position fix status (POSFIX_INVALID, POSFIX_SPS, POSFIX_DGPS, POSFIX_PPS)
+ posfix,
+ # number of satellites used for fix 0 <= satellites <= 12
+ satellites,
+ # horizontal dilution of precision
+ hdop,
+ # None or (altitude according to WGS-84 ellipsoid, units (typically 'M' for meters))
+ altitude,
+ # None or (geoid separation according to WGS-84 ellipsoid, units (typically 'M' for meters))
+ geoid,
+ # (age of dgps data in seconds, dgps station id)
+ dgps,
+ )
diff --git a/twisted/protocols/gps/rockwell.py b/twisted/protocols/gps/rockwell.py
new file mode 100644
index 0000000..7c1d2ad
--- /dev/null
+++ b/twisted/protocols/gps/rockwell.py
@@ -0,0 +1,268 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Rockwell Semiconductor Zodiac Serial Protocol
+Coded from official protocol specs (Order No. GPS-25, 09/24/1996, Revision 11)
+
+Maintainer: Bob Ippolito
+
+The following Rockwell Zodiac messages are currently understood::
+ EARTHA\\r\\n (a hack to "turn on" a DeLorme Earthmate)
+ 1000 (Geodesic Position Status Output)
+ 1002 (Channel Summary)
+ 1003 (Visible Satellites)
+ 1011 (Receiver ID)
+
+The following Rockwell Zodiac messages require implementation::
+ None really, the others aren't quite so useful and require bidirectional communication w/ the device
+
+Other desired features::
+ - Compatability with the DeLorme Tripmate and other devices with this chipset (?)
+"""
+
+import struct, operator, math
+from twisted.internet import protocol
+from twisted.python import log
+
+DEBUG = 1
+
+class ZodiacParseError(ValueError):
+ pass
+
+class Zodiac(protocol.Protocol):
+ dispatch = {
+ # Output Messages (* means they get sent by the receiver by default periodically)
+ 1000: 'fix', # *Geodesic Position Status Output
+ 1001: 'ecef', # ECEF Position Status Output
+ 1002: 'channels', # *Channel Summary
+ 1003: 'satellites', # *Visible Satellites
+ 1005: 'dgps', # Differential GPS Status
+ 1007: 'channelmeas', # Channel Measurement
+ 1011: 'id', # *Receiver ID
+ 1012: 'usersettings', # User-Settings Output
+ 1100: 'testresults', # Built-In Test Results
+ 1102: 'meastimemark', # Measurement Time Mark
+ 1108: 'utctimemark', # UTC Time Mark Pulse Output
+ 1130: 'serial', # Serial Port Communication Parameters In Use
+ 1135: 'eepromupdate', # EEPROM Update
+ 1136: 'eepromstatus', # EEPROM Status
+ }
+ # these aren't used for anything yet, just sitting here for reference
+ messages = {
+ # Input Messages
+ 'fix': 1200, # Geodesic Position and Velocity Initialization
+ 'udatum': 1210, # User-Defined Datum Definition
+ 'mdatum': 1211, # Map Datum Select
+ 'smask': 1212, # Satellite Elevation Mask Control
+ 'sselect': 1213, # Satellite Candidate Select
+ 'dgpsc': 1214, # Differential GPS Control
+ 'startc': 1216, # Cold Start Control
+ 'svalid': 1217, # Solution Validity Control
+ 'antenna': 1218, # Antenna Type Select
+ 'altinput': 1219, # User-Entered Altitude Input
+ 'appctl': 1220, # Application Platform Control
+ 'navcfg': 1221, # Nav Configuration
+ 'test': 1300, # Perform Built-In Test Command
+ 'restart': 1303, # Restart Command
+ 'serial': 1330, # Serial Port Communications Parameters
+ 'msgctl': 1331, # Message Protocol Control
+ 'dgpsd': 1351, # Raw DGPS RTCM SC-104 Data
+ }
+ MAX_LENGTH = 296
+ allow_earthmate_hack = 1
+ recvd = ""
+
+ def dataReceived(self, recd):
+ self.recvd = self.recvd + recd
+ while len(self.recvd) >= 10:
+
+ # hack for DeLorme EarthMate
+ if self.recvd[:8] == 'EARTHA\r\n':
+ if self.allow_earthmate_hack:
+ self.allow_earthmate_hack = 0
+ self.transport.write('EARTHA\r\n')
+ self.recvd = self.recvd[8:]
+ continue
+
+ if self.recvd[0:2] != '\xFF\x81':
+ if DEBUG:
+ raise ZodiacParseError('Invalid Sync %r' % self.recvd)
+ else:
+ raise ZodiacParseError
+ sync, msg_id, length, acknak, checksum = struct.unpack('<HHHHh', self.recvd[:10])
+
+ # verify checksum
+ cksum = -(reduce(operator.add, (sync, msg_id, length, acknak)) & 0xFFFF)
+ cksum, = struct.unpack('<h', struct.pack('<h', cksum))
+ if cksum != checksum:
+ if DEBUG:
+ raise ZodiacParseError('Invalid Header Checksum %r != %r %r' % (checksum, cksum, self.recvd[:8]))
+ else:
+ raise ZodiacParseError
+
+ # length was in words, now it's bytes
+ length = length * 2
+
+ # do we need more data ?
+ neededBytes = 10
+ if length:
+ neededBytes += length + 2
+ if len(self.recvd) < neededBytes:
+ break
+
+ if neededBytes > self.MAX_LENGTH:
+ raise ZodiacParseError("Invalid Header??")
+
+ # empty messages pass empty strings
+ message = ''
+
+ # does this message have data ?
+ if length:
+ message, checksum = self.recvd[10:10+length], struct.unpack('<h', self.recvd[10+length:neededBytes])[0]
+ cksum = 0x10000 - (reduce(operator.add, struct.unpack('<%dH' % (length/2), message)) & 0xFFFF)
+ cksum, = struct.unpack('<h', struct.pack('<h', cksum))
+ if cksum != checksum:
+ if DEBUG:
+ log.dmsg('msg_id = %r length = %r' % (msg_id, length), debug=True)
+ raise ZodiacParseError('Invalid Data Checksum %r != %r %r' % (checksum, cksum, message))
+ else:
+ raise ZodiacParseError
+
+ # discard used buffer, dispatch message
+ self.recvd = self.recvd[neededBytes:]
+ self.receivedMessage(msg_id, message, acknak)
+
+ def receivedMessage(self, msg_id, message, acknak):
+ dispatch = self.dispatch.get(msg_id, None)
+ if not dispatch:
+ raise ZodiacParseError('Unknown msg_id = %r' % msg_id)
+ handler = getattr(self, 'handle_%s' % dispatch, None)
+ decoder = getattr(self, 'decode_%s' % dispatch, None)
+ if not (handler and decoder):
+ # missing handler or decoder
+ #if DEBUG:
+ # log.msg('MISSING HANDLER/DECODER PAIR FOR: %r' % (dispatch,), debug=True)
+ return
+ decoded = decoder(message)
+ return handler(*decoded)
+
+ def decode_fix(self, message):
+ assert len(message) == 98, "Geodesic Position Status Output should be 55 words total (98 byte message)"
+ (ticks, msgseq, satseq, navstatus, navtype, nmeasure, polar, gpswk, gpses, gpsns, utcdy, utcmo, utcyr, utchr, utcmn, utcsc, utcns, latitude, longitude, height, geoidalsep, speed, course, magvar, climb, mapdatum, exhposerr, exvposerr, extimeerr, exphvelerr, clkbias, clkbiasdev, clkdrift, clkdriftdev) = struct.unpack('<LhhHHHHHLLHHHHHHLlllhLHhhHLLLHllll', message)
+
+ # there's a lot of shit in here..
+ # I'll just snag the important stuff and spit it out like my NMEA decoder
+ utc = (utchr * 3600.0) + (utcmn * 60.0) + utcsc + (float(utcns) * 0.000000001)
+
+ log.msg('utchr, utcmn, utcsc, utcns = ' + repr((utchr, utcmn, utcsc, utcns)), debug=True)
+
+ latitude = float(latitude) * 0.00000180 / math.pi
+ longitude = float(longitude) * 0.00000180 / math.pi
+ posfix = not (navstatus & 0x001c)
+ satellites = nmeasure
+ hdop = float(exhposerr) * 0.01
+ altitude = float(height) * 0.01, 'M'
+ geoid = float(geoidalsep) * 0.01, 'M'
+ dgps = None
+ return (
+ # seconds since 00:00 UTC
+ utc,
+ # latitude (degrees)
+ latitude,
+ # longitude (degrees)
+ longitude,
+ # position fix status (invalid = False, valid = True)
+ posfix,
+ # number of satellites [measurements] used for fix 0 <= satellites <= 12
+ satellites,
+ # horizontal dilution of precision
+ hdop,
+ # (altitude according to WGS-84 ellipsoid, units (always 'M' for meters))
+ altitude,
+ # (geoid separation according to WGS-84 ellipsoid, units (always 'M' for meters))
+ geoid,
+ # None, for compatability w/ NMEA code
+ dgps,
+ )
+
+ def decode_id(self, message):
+ assert len(message) == 106, "Receiver ID Message should be 59 words total (106 byte message)"
+ ticks, msgseq, channels, software_version, software_date, options_list, reserved = struct.unpack('<Lh20s20s20s20s20s', message)
+ channels, software_version, software_date, options_list = map(lambda s: s.split('\0')[0], (channels, software_version, software_date, options_list))
+ software_version = float(software_version)
+ channels = int(channels) # 0-12 .. but ALWAYS 12, so we ignore.
+ options_list = int(options_list[:4], 16) # only two bitflags, others are reserved
+ minimize_rom = (options_list & 0x01) > 0
+ minimize_ram = (options_list & 0x02) > 0
+ # (version info), (options info)
+ return ((software_version, software_date), (minimize_rom, minimize_ram))
+
+ def decode_channels(self, message):
+ assert len(message) == 90, "Channel Summary Message should be 51 words total (90 byte message)"
+ ticks, msgseq, satseq, gpswk, gpsws, gpsns = struct.unpack('<LhhHLL', message[:18])
+ channels = []
+ message = message[18:]
+ for i in range(12):
+ flags, prn, cno = struct.unpack('<HHH', message[6 * i:6 * (i + 1)])
+ # measurement used, ephemeris available, measurement valid, dgps corrections available
+ flags = (flags & 0x01, flags & 0x02, flags & 0x04, flags & 0x08)
+ channels.append((flags, prn, cno))
+ # ((flags, satellite PRN, C/No in dbHz)) for 12 channels
+ # satellite message sequence number
+ # gps week number, gps seconds in week (??), gps nanoseconds from Epoch
+ return (tuple(channels),) #, satseq, (gpswk, gpsws, gpsns))
+
+ def decode_satellites(self, message):
+ assert len(message) == 90, "Visible Satellites Message should be 51 words total (90 byte message)"
+ ticks, msgseq, gdop, pdop, hdop, vdop, tdop, numsatellites = struct.unpack('<LhhhhhhH', message[:18])
+ gdop, pdop, hdop, vdop, tdop = map(lambda n: float(n) * 0.01, (gdop, pdop, hdop, vdop, tdop))
+ satellites = []
+ message = message[18:]
+ for i in range(numsatellites):
+ prn, azi, elev = struct.unpack('<Hhh', message[6 * i:6 * (i + 1)])
+ azi, elev = map(lambda n: (float(n) * 0.0180 / math.pi), (azi, elev))
+ satellites.push((prn, azi, elev))
+ # ((PRN [0, 32], azimuth +=[0.0, 180.0] deg, elevation +-[0.0, 90.0] deg)) satellite info (0-12)
+ # (geometric, position, horizontal, vertical, time) dilution of precision
+ return (tuple(satellites), (gdop, pdop, hdop, vdop, tdop))
+
+ def decode_dgps(self, message):
+ assert len(message) == 38, "Differential GPS Status Message should be 25 words total (38 byte message)"
+ raise NotImplementedError
+
+ def decode_ecef(self, message):
+ assert len(message) == 96, "ECEF Position Status Output Message should be 54 words total (96 byte message)"
+ raise NotImplementedError
+
+ def decode_channelmeas(self, message):
+ assert len(message) == 296, "Channel Measurement Message should be 154 words total (296 byte message)"
+ raise NotImplementedError
+
+ def decode_usersettings(self, message):
+ assert len(message) == 32, "User-Settings Output Message should be 22 words total (32 byte message)"
+ raise NotImplementedError
+
+ def decode_testresults(self, message):
+ assert len(message) == 28, "Built-In Test Results Message should be 20 words total (28 byte message)"
+ raise NotImplementedError
+
+ def decode_meastimemark(self, message):
+ assert len(message) == 494, "Measurement Time Mark Message should be 253 words total (494 byte message)"
+ raise NotImplementedError
+
+ def decode_utctimemark(self, message):
+ assert len(message) == 28, "UTC Time Mark Pulse Output Message should be 20 words total (28 byte message)"
+ raise NotImplementedError
+
+ def decode_serial(self, message):
+ assert len(message) == 30, "Serial Port Communication Paramaters In Use Message should be 21 words total (30 byte message)"
+ raise NotImplementedError
+
+ def decode_eepromupdate(self, message):
+ assert len(message) == 8, "EEPROM Update Message should be 10 words total (8 byte message)"
+ raise NotImplementedError
+
+ def decode_eepromstatus(self, message):
+ assert len(message) == 24, "EEPROM Status Message should be 18 words total (24 byte message)"
+ raise NotImplementedError
diff --git a/twisted/protocols/htb.py b/twisted/protocols/htb.py
new file mode 100644
index 0000000..082b2c8
--- /dev/null
+++ b/twisted/protocols/htb.py
@@ -0,0 +1,269 @@
+# -*- test-case-name: twisted.test.test_htb -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Hierarchical Token Bucket traffic shaping.
+
+Patterned after U{Martin Devera's Hierarchical Token Bucket traffic
+shaper for the Linux kernel<http://luxik.cdi.cz/~devik/qos/htb/>}.
+
+@seealso: U{HTB Linux queuing discipline manual - user guide
+ <http://luxik.cdi.cz/~devik/qos/htb/manual/userg.htm>}
+@seealso: U{Token Bucket Filter in Linux Advanced Routing & Traffic Control
+ HOWTO<http://lartc.org/howto/lartc.qdisc.classless.html#AEN682>}
+@author: Kevin Turner
+"""
+
+from __future__ import nested_scopes
+
+__version__ = '$Revision: 1.5 $'[11:-2]
+
+
+# TODO: Investigate whether we should be using os.times()[-1] instead of
+# time.time. time.time, it has been pointed out, can go backwards. Is
+# the same true of os.times?
+from time import time
+from zope.interface import implements, Interface
+
+from twisted.protocols import pcp
+
+
+class Bucket:
+ """Token bucket, or something like it.
+
+ I can hold up to a certain number of tokens, and I drain over time.
+
+ @cvar maxburst: Size of the bucket, in bytes. If None, the bucket is
+ never full.
+ @type maxburst: int
+ @cvar rate: Rate the bucket drains, in bytes per second. If None,
+ the bucket drains instantaneously.
+ @type rate: int
+ """
+
+ maxburst = None
+ rate = None
+
+ _refcount = 0
+
+ def __init__(self, parentBucket=None):
+ self.content = 0
+ self.parentBucket=parentBucket
+ self.lastDrip = time()
+
+ def add(self, amount):
+ """Add tokens to me.
+
+ @param amount: A quanity of tokens to add.
+ @type amount: int
+
+ @returns: The number of tokens that fit.
+ @returntype: int
+ """
+ self.drip()
+ if self.maxburst is None:
+ allowable = amount
+ else:
+ allowable = min(amount, self.maxburst - self.content)
+
+ if self.parentBucket is not None:
+ allowable = self.parentBucket.add(allowable)
+ self.content += allowable
+ return allowable
+
+ def drip(self):
+ """
+ Let some of the bucket drain.
+
+ How much of the bucket drains depends on how long it has been
+ since I was last called.
+
+ @returns: C{True} if the bucket is empty after this drip.
+ @returntype: bool
+ """
+ if self.parentBucket is not None:
+ self.parentBucket.drip()
+
+ if self.rate is None:
+ self.content = 0
+ else:
+ now = time()
+ deltaT = now - self.lastDrip
+ self.content = long(max(0, self.content - deltaT * self.rate))
+ self.lastDrip = now
+ return self.content == 0
+
+
+class IBucketFilter(Interface):
+ def getBucketFor(*somethings, **some_kw):
+ """I'll give you a bucket for something.
+
+ @returntype: L{Bucket}
+ """
+
+class HierarchicalBucketFilter:
+ """I filter things into buckets, and I am nestable.
+
+ @cvar bucketFactory: Class of buckets to make.
+ @type bucketFactory: L{Bucket} class
+ @cvar sweepInterval: Seconds between sweeping out the bucket cache.
+ @type sweepInterval: int
+ """
+
+ implements(IBucketFilter)
+
+ bucketFactory = Bucket
+ sweepInterval = None
+
+ def __init__(self, parentFilter=None):
+ self.buckets = {}
+ self.parentFilter = parentFilter
+ self.lastSweep = time()
+
+ def getBucketFor(self, *a, **kw):
+ """You want a bucket for that? I'll give you a bucket.
+
+ Any parameters are passed on to L{getBucketKey}, from them it
+ decides which bucket you get.
+
+ @returntype: L{Bucket}
+ """
+ if ((self.sweepInterval is not None)
+ and ((time() - self.lastSweep) > self.sweepInterval)):
+ self.sweep()
+
+ if self.parentFilter:
+ parentBucket = self.parentFilter.getBucketFor(self, *a, **kw)
+ else:
+ parentBucket = None
+
+ key = self.getBucketKey(*a, **kw)
+ bucket = self.buckets.get(key)
+ if bucket is None:
+ bucket = self.bucketFactory(parentBucket)
+ self.buckets[key] = bucket
+ return bucket
+
+ def getBucketKey(self, *a, **kw):
+ """I determine who gets which bucket.
+
+ Unless I'm overridden, everything gets the same bucket.
+
+ @returns: something to be used as a key in the bucket cache.
+ """
+ return None
+
+ def sweep(self):
+ """I throw away references to empty buckets."""
+ for key, bucket in self.buckets.items():
+ if (bucket._refcount == 0) and bucket.drip():
+ del self.buckets[key]
+
+ self.lastSweep = time()
+
+
+class FilterByHost(HierarchicalBucketFilter):
+ """A bucket filter with a bucket for each host.
+ """
+ sweepInterval = 60 * 20
+
+ def getBucketKey(self, transport):
+ return transport.getPeer()[1]
+
+
+class FilterByServer(HierarchicalBucketFilter):
+ """A bucket filter with a bucket for each service.
+ """
+ sweepInterval = None
+
+ def getBucketKey(self, transport):
+ return transport.getHost()[2]
+
+
+class ShapedConsumer(pcp.ProducerConsumerProxy):
+ """I wrap a Consumer and shape the rate at which it receives data.
+ """
+ # Providing a Pull interface means I don't have to try to schedule
+ # traffic with callLaters.
+ iAmStreaming = False
+
+ def __init__(self, consumer, bucket):
+ pcp.ProducerConsumerProxy.__init__(self, consumer)
+ self.bucket = bucket
+ self.bucket._refcount += 1
+
+ def _writeSomeData(self, data):
+ # In practice, this actually results in obscene amounts of
+ # overhead, as a result of generating lots and lots of packets
+ # with twelve-byte payloads. We may need to do a version of
+ # this with scheduled writes after all.
+ amount = self.bucket.add(len(data))
+ return pcp.ProducerConsumerProxy._writeSomeData(self, data[:amount])
+
+ def stopProducing(self):
+ pcp.ProducerConsumerProxy.stopProducing(self)
+ self.bucket._refcount -= 1
+
+
+class ShapedTransport(ShapedConsumer):
+ """I wrap a Transport and shape the rate at which it receives data.
+
+ I am a L{ShapedConsumer} with a little bit of magic to provide for
+ the case where the consumer I wrap is also a Transport and people
+ will be attempting to access attributes I do not proxy as a
+ Consumer (e.g. loseConnection).
+ """
+ # Ugh. We only wanted to filter IConsumer, not ITransport.
+
+ iAmStreaming = False
+ def __getattr__(self, name):
+ # Because people will be doing things like .getPeer and
+ # .loseConnection on me.
+ return getattr(self.consumer, name)
+
+
+class ShapedProtocolFactory:
+ """I dispense Protocols with traffic shaping on their transports.
+
+ Usage::
+
+ myserver = SomeFactory()
+ myserver.protocol = ShapedProtocolFactory(myserver.protocol,
+ bucketFilter)
+
+ Where SomeServerFactory is a L{twisted.internet.protocol.Factory}, and
+ bucketFilter is an instance of L{HierarchicalBucketFilter}.
+ """
+ def __init__(self, protoClass, bucketFilter):
+ """Tell me what to wrap and where to get buckets.
+
+ @param protoClass: The class of Protocol I will generate
+ wrapped instances of.
+ @type protoClass: L{Protocol<twisted.internet.interfaces.IProtocol>}
+ class
+ @param bucketFilter: The filter which will determine how
+ traffic is shaped.
+ @type bucketFilter: L{HierarchicalBucketFilter}.
+ """
+ # More precisely, protoClass can be any callable that will return
+ # instances of something that implements IProtocol.
+ self.protocol = protoClass
+ self.bucketFilter = bucketFilter
+
+ def __call__(self, *a, **kw):
+ """Make a Protocol instance with a shaped transport.
+
+ Any parameters will be passed on to the protocol's initializer.
+
+ @returns: a Protocol instance with a L{ShapedTransport}.
+ """
+ proto = self.protocol(*a, **kw)
+ origMakeConnection = proto.makeConnection
+ def makeConnection(transport):
+ bucket = self.bucketFilter.getBucketFor(transport)
+ shapedTransport = ShapedTransport(transport, bucket)
+ return origMakeConnection(shapedTransport)
+ proto.makeConnection = makeConnection
+ return proto
diff --git a/twisted/protocols/ident.py b/twisted/protocols/ident.py
new file mode 100644
index 0000000..3dfaaf3
--- /dev/null
+++ b/twisted/protocols/ident.py
@@ -0,0 +1,235 @@
+# -*- test-case-name: twisted.test.test_ident -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Ident protocol implementation.
+
+@author: Jean-Paul Calderone
+"""
+
+from __future__ import generators
+
+import struct
+
+from twisted.internet import defer
+from twisted.protocols import basic
+from twisted.python import log, failure
+
+_MIN_PORT = 1
+_MAX_PORT = 2 ** 16 - 1
+
+class IdentError(Exception):
+ """
+ Can't determine connection owner; reason unknown.
+ """
+
+ identDescription = 'UNKNOWN-ERROR'
+
+ def __str__(self):
+ return self.identDescription
+
+
+class NoUser(IdentError):
+ """
+ The connection specified by the port pair is not currently in use or
+ currently not owned by an identifiable entity.
+ """
+ identDescription = 'NO-USER'
+
+
+class InvalidPort(IdentError):
+ """
+ Either the local or foreign port was improperly specified. This should
+ be returned if either or both of the port ids were out of range (TCP
+ port numbers are from 1-65535), negative integers, reals or in any
+ fashion not recognized as a non-negative integer.
+ """
+ identDescription = 'INVALID-PORT'
+
+
+class HiddenUser(IdentError):
+ """
+ The server was able to identify the user of this port, but the
+ information was not returned at the request of the user.
+ """
+ identDescription = 'HIDDEN-USER'
+
+
+class IdentServer(basic.LineOnlyReceiver):
+ """
+ The Identification Protocol (a.k.a., "ident", a.k.a., "the Ident
+ Protocol") provides a means to determine the identity of a user of a
+ particular TCP connection. Given a TCP port number pair, it returns a
+ character string which identifies the owner of that connection on the
+ server's system.
+
+ Server authors should subclass this class and override the lookup method.
+ The default implementation returns an UNKNOWN-ERROR response for every
+ query.
+ """
+
+ def lineReceived(self, line):
+ parts = line.split(',')
+ if len(parts) != 2:
+ self.invalidQuery()
+ else:
+ try:
+ portOnServer, portOnClient = map(int, parts)
+ except ValueError:
+ self.invalidQuery()
+ else:
+ if _MIN_PORT <= portOnServer <= _MAX_PORT and _MIN_PORT <= portOnClient <= _MAX_PORT:
+ self.validQuery(portOnServer, portOnClient)
+ else:
+ self._ebLookup(failure.Failure(InvalidPort()), portOnServer, portOnClient)
+
+ def invalidQuery(self):
+ self.transport.loseConnection()
+
+
+ def validQuery(self, portOnServer, portOnClient):
+ """
+ Called when a valid query is received to look up and deliver the
+ response.
+
+ @param portOnServer: The server port from the query.
+ @param portOnClient: The client port from the query.
+ """
+ serverAddr = self.transport.getHost().host, portOnServer
+ clientAddr = self.transport.getPeer().host, portOnClient
+ defer.maybeDeferred(self.lookup, serverAddr, clientAddr
+ ).addCallback(self._cbLookup, portOnServer, portOnClient
+ ).addErrback(self._ebLookup, portOnServer, portOnClient
+ )
+
+
+ def _cbLookup(self, (sysName, userId), sport, cport):
+ self.sendLine('%d, %d : USERID : %s : %s' % (sport, cport, sysName, userId))
+
+ def _ebLookup(self, failure, sport, cport):
+ if failure.check(IdentError):
+ self.sendLine('%d, %d : ERROR : %s' % (sport, cport, failure.value))
+ else:
+ log.err(failure)
+ self.sendLine('%d, %d : ERROR : %s' % (sport, cport, IdentError(failure.value)))
+
+ def lookup(self, serverAddress, clientAddress):
+ """Lookup user information about the specified address pair.
+
+ Return value should be a two-tuple of system name and username.
+ Acceptable values for the system name may be found online at::
+
+ U{http://www.iana.org/assignments/operating-system-names}
+
+ This method may also raise any IdentError subclass (or IdentError
+ itself) to indicate user information will not be provided for the
+ given query.
+
+ A Deferred may also be returned.
+
+ @param serverAddress: A two-tuple representing the server endpoint
+ of the address being queried. The first element is a string holding
+ a dotted-quad IP address. The second element is an integer
+ representing the port.
+
+ @param clientAddress: Like L{serverAddress}, but represents the
+ client endpoint of the address being queried.
+ """
+ raise IdentError()
+
+class ProcServerMixin:
+ """Implements lookup() to grab entries for responses from /proc/net/tcp
+ """
+
+ SYSTEM_NAME = 'LINUX'
+
+ try:
+ from pwd import getpwuid
+ def getUsername(self, uid, getpwuid=getpwuid):
+ return getpwuid(uid)[0]
+ del getpwuid
+ except ImportError:
+ def getUsername(self, uid):
+ raise IdentError()
+
+ def entries(self):
+ f = file('/proc/net/tcp')
+ f.readline()
+ for L in f:
+ yield L.strip()
+
+ def dottedQuadFromHexString(self, hexstr):
+ return '.'.join(map(str, struct.unpack('4B', struct.pack('=L', int(hexstr, 16)))))
+
+ def unpackAddress(self, packed):
+ addr, port = packed.split(':')
+ addr = self.dottedQuadFromHexString(addr)
+ port = int(port, 16)
+ return addr, port
+
+ def parseLine(self, line):
+ parts = line.strip().split()
+ localAddr, localPort = self.unpackAddress(parts[1])
+ remoteAddr, remotePort = self.unpackAddress(parts[2])
+ uid = int(parts[7])
+ return (localAddr, localPort), (remoteAddr, remotePort), uid
+
+ def lookup(self, serverAddress, clientAddress):
+ for ent in self.entries():
+ localAddr, remoteAddr, uid = self.parseLine(ent)
+ if remoteAddr == clientAddress and localAddr[1] == serverAddress[1]:
+ return (self.SYSTEM_NAME, self.getUsername(uid))
+
+ raise NoUser()
+
+
+class IdentClient(basic.LineOnlyReceiver):
+
+ errorTypes = (IdentError, NoUser, InvalidPort, HiddenUser)
+
+ def __init__(self):
+ self.queries = []
+
+ def lookup(self, portOnServer, portOnClient):
+ """Lookup user information about the specified address pair.
+ """
+ self.queries.append((defer.Deferred(), portOnServer, portOnClient))
+ if len(self.queries) > 1:
+ return self.queries[-1][0]
+
+ self.sendLine('%d, %d' % (portOnServer, portOnClient))
+ return self.queries[-1][0]
+
+ def lineReceived(self, line):
+ if not self.queries:
+ log.msg("Unexpected server response: %r" % (line,))
+ else:
+ d, _, _ = self.queries.pop(0)
+ self.parseResponse(d, line)
+ if self.queries:
+ self.sendLine('%d, %d' % (self.queries[0][1], self.queries[0][2]))
+
+ def connectionLost(self, reason):
+ for q in self.queries:
+ q[0].errback(IdentError(reason))
+ self.queries = []
+
+ def parseResponse(self, deferred, line):
+ parts = line.split(':', 2)
+ if len(parts) != 3:
+ deferred.errback(IdentError(line))
+ else:
+ ports, type, addInfo = map(str.strip, parts)
+ if type == 'ERROR':
+ for et in self.errorTypes:
+ if et.identDescription == addInfo:
+ deferred.errback(et(line))
+ return
+ deferred.errback(IdentError(line))
+ else:
+ deferred.callback((type, addInfo))
+
+__all__ = ['IdentError', 'NoUser', 'InvalidPort', 'HiddenUser',
+ 'IdentServer', 'IdentClient',
+ 'ProcServerMixin']
diff --git a/twisted/protocols/loopback.py b/twisted/protocols/loopback.py
new file mode 100644
index 0000000..e584827
--- /dev/null
+++ b/twisted/protocols/loopback.py
@@ -0,0 +1,372 @@
+# -*- test-case-name: twisted.test.test_loopback -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Testing support for protocols -- loopback between client and server.
+"""
+
+# system imports
+import tempfile
+from zope.interface import implements
+
+# Twisted Imports
+from twisted.protocols import policies
+from twisted.internet import interfaces, protocol, main, defer
+from twisted.internet.task import deferLater
+from twisted.python import failure
+from twisted.internet.interfaces import IAddress
+
+
+class _LoopbackQueue(object):
+ """
+ Trivial wrapper around a list to give it an interface like a queue, which
+ the addition of also sending notifications by way of a Deferred whenever
+ the list has something added to it.
+ """
+
+ _notificationDeferred = None
+ disconnect = False
+
+ def __init__(self):
+ self._queue = []
+
+
+ def put(self, v):
+ self._queue.append(v)
+ if self._notificationDeferred is not None:
+ d, self._notificationDeferred = self._notificationDeferred, None
+ d.callback(None)
+
+
+ def __nonzero__(self):
+ return bool(self._queue)
+
+
+ def get(self):
+ return self._queue.pop(0)
+
+
+
+class _LoopbackAddress(object):
+ implements(IAddress)
+
+
+class _LoopbackTransport(object):
+ implements(interfaces.ITransport, interfaces.IConsumer)
+
+ disconnecting = False
+ producer = None
+
+ # ITransport
+ def __init__(self, q):
+ self.q = q
+
+ def write(self, bytes):
+ self.q.put(bytes)
+
+ def writeSequence(self, iovec):
+ self.q.put(''.join(iovec))
+
+ def loseConnection(self):
+ self.q.disconnect = True
+ self.q.put(None)
+
+ def getPeer(self):
+ return _LoopbackAddress()
+
+ def getHost(self):
+ return _LoopbackAddress()
+
+ # IConsumer
+ def registerProducer(self, producer, streaming):
+ assert self.producer is None
+ self.producer = producer
+ self.streamingProducer = streaming
+ self._pollProducer()
+
+ def unregisterProducer(self):
+ assert self.producer is not None
+ self.producer = None
+
+ def _pollProducer(self):
+ if self.producer is not None and not self.streamingProducer:
+ self.producer.resumeProducing()
+
+
+
+def identityPumpPolicy(queue, target):
+ """
+ L{identityPumpPolicy} is a policy which delivers each chunk of data written
+ to the given queue as-is to the target.
+
+ This isn't a particularly realistic policy.
+
+ @see: L{loopbackAsync}
+ """
+ while queue:
+ bytes = queue.get()
+ if bytes is None:
+ break
+ target.dataReceived(bytes)
+
+
+
+def collapsingPumpPolicy(queue, target):
+ """
+ L{collapsingPumpPolicy} is a policy which collapses all outstanding chunks
+ into a single string and delivers it to the target.
+
+ @see: L{loopbackAsync}
+ """
+ bytes = []
+ while queue:
+ chunk = queue.get()
+ if chunk is None:
+ break
+ bytes.append(chunk)
+ if bytes:
+ target.dataReceived(''.join(bytes))
+
+
+
+def loopbackAsync(server, client, pumpPolicy=identityPumpPolicy):
+ """
+ Establish a connection between C{server} and C{client} then transfer data
+ between them until the connection is closed. This is often useful for
+ testing a protocol.
+
+ @param server: The protocol instance representing the server-side of this
+ connection.
+
+ @param client: The protocol instance representing the client-side of this
+ connection.
+
+ @param pumpPolicy: When either C{server} or C{client} writes to its
+ transport, the string passed in is added to a queue of data for the
+ other protocol. Eventually, C{pumpPolicy} will be called with one such
+ queue and the corresponding protocol object. The pump policy callable
+ is responsible for emptying the queue and passing the strings it
+ contains to the given protocol's C{dataReceived} method. The signature
+ of C{pumpPolicy} is C{(queue, protocol)}. C{queue} is an object with a
+ C{get} method which will return the next string written to the
+ transport, or C{None} if the transport has been disconnected, and which
+ evaluates to C{True} if and only if there are more items to be
+ retrieved via C{get}.
+
+ @return: A L{Deferred} which fires when the connection has been closed and
+ both sides have received notification of this.
+ """
+ serverToClient = _LoopbackQueue()
+ clientToServer = _LoopbackQueue()
+
+ server.makeConnection(_LoopbackTransport(serverToClient))
+ client.makeConnection(_LoopbackTransport(clientToServer))
+
+ return _loopbackAsyncBody(
+ server, serverToClient, client, clientToServer, pumpPolicy)
+
+
+
+def _loopbackAsyncBody(server, serverToClient, client, clientToServer,
+ pumpPolicy):
+ """
+ Transfer bytes from the output queue of each protocol to the input of the other.
+
+ @param server: The protocol instance representing the server-side of this
+ connection.
+
+ @param serverToClient: The L{_LoopbackQueue} holding the server's output.
+
+ @param client: The protocol instance representing the client-side of this
+ connection.
+
+ @param clientToServer: The L{_LoopbackQueue} holding the client's output.
+
+ @param pumpPolicy: See L{loopbackAsync}.
+
+ @return: A L{Deferred} which fires when the connection has been closed and
+ both sides have received notification of this.
+ """
+ def pump(source, q, target):
+ sent = False
+ if q:
+ pumpPolicy(q, target)
+ sent = True
+ if sent and not q:
+ # A write buffer has now been emptied. Give any producer on that
+ # side an opportunity to produce more data.
+ source.transport._pollProducer()
+
+ return sent
+
+ while 1:
+ disconnect = clientSent = serverSent = False
+
+ # Deliver the data which has been written.
+ serverSent = pump(server, serverToClient, client)
+ clientSent = pump(client, clientToServer, server)
+
+ if not clientSent and not serverSent:
+ # Neither side wrote any data. Wait for some new data to be added
+ # before trying to do anything further.
+ d = defer.Deferred()
+ clientToServer._notificationDeferred = d
+ serverToClient._notificationDeferred = d
+ d.addCallback(
+ _loopbackAsyncContinue,
+ server, serverToClient, client, clientToServer, pumpPolicy)
+ return d
+ if serverToClient.disconnect:
+ # The server wants to drop the connection. Flush any remaining
+ # data it has.
+ disconnect = True
+ pump(server, serverToClient, client)
+ elif clientToServer.disconnect:
+ # The client wants to drop the connection. Flush any remaining
+ # data it has.
+ disconnect = True
+ pump(client, clientToServer, server)
+ if disconnect:
+ # Someone wanted to disconnect, so okay, the connection is gone.
+ server.connectionLost(failure.Failure(main.CONNECTION_DONE))
+ client.connectionLost(failure.Failure(main.CONNECTION_DONE))
+ return defer.succeed(None)
+
+
+
+def _loopbackAsyncContinue(ignored, server, serverToClient, client,
+ clientToServer, pumpPolicy):
+ # Clear the Deferred from each message queue, since it has already fired
+ # and cannot be used again.
+ clientToServer._notificationDeferred = None
+ serverToClient._notificationDeferred = None
+
+ # Schedule some more byte-pushing to happen. This isn't done
+ # synchronously because no actual transport can re-enter dataReceived as
+ # a result of calling write, and doing this synchronously could result
+ # in that.
+ from twisted.internet import reactor
+ return deferLater(
+ reactor, 0,
+ _loopbackAsyncBody,
+ server, serverToClient, client, clientToServer, pumpPolicy)
+
+
+
+class LoopbackRelay:
+
+ implements(interfaces.ITransport, interfaces.IConsumer)
+
+ buffer = ''
+ shouldLose = 0
+ disconnecting = 0
+ producer = None
+
+ def __init__(self, target, logFile=None):
+ self.target = target
+ self.logFile = logFile
+
+ def write(self, data):
+ self.buffer = self.buffer + data
+ if self.logFile:
+ self.logFile.write("loopback writing %s\n" % repr(data))
+
+ def writeSequence(self, iovec):
+ self.write("".join(iovec))
+
+ def clearBuffer(self):
+ if self.shouldLose == -1:
+ return
+
+ if self.producer:
+ self.producer.resumeProducing()
+ if self.buffer:
+ if self.logFile:
+ self.logFile.write("loopback receiving %s\n" % repr(self.buffer))
+ buffer = self.buffer
+ self.buffer = ''
+ self.target.dataReceived(buffer)
+ if self.shouldLose == 1:
+ self.shouldLose = -1
+ self.target.connectionLost(failure.Failure(main.CONNECTION_DONE))
+
+ def loseConnection(self):
+ if self.shouldLose != -1:
+ self.shouldLose = 1
+
+ def getHost(self):
+ return 'loopback'
+
+ def getPeer(self):
+ return 'loopback'
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+
+ def unregisterProducer(self):
+ self.producer = None
+
+ def logPrefix(self):
+ return 'Loopback(%r)' % (self.target.__class__.__name__,)
+
+
+
+class LoopbackClientFactory(protocol.ClientFactory):
+
+ def __init__(self, protocol):
+ self.disconnected = 0
+ self.deferred = defer.Deferred()
+ self.protocol = protocol
+
+ def buildProtocol(self, addr):
+ return self.protocol
+
+ def clientConnectionLost(self, connector, reason):
+ self.disconnected = 1
+ self.deferred.callback(None)
+
+
+class _FireOnClose(policies.ProtocolWrapper):
+ def __init__(self, protocol, factory):
+ policies.ProtocolWrapper.__init__(self, protocol, factory)
+ self.deferred = defer.Deferred()
+
+ def connectionLost(self, reason):
+ policies.ProtocolWrapper.connectionLost(self, reason)
+ self.deferred.callback(None)
+
+
+def loopbackTCP(server, client, port=0, noisy=True):
+ """Run session between server and client protocol instances over TCP."""
+ from twisted.internet import reactor
+ f = policies.WrappingFactory(protocol.Factory())
+ serverWrapper = _FireOnClose(f, server)
+ f.noisy = noisy
+ f.buildProtocol = lambda addr: serverWrapper
+ serverPort = reactor.listenTCP(port, f, interface='127.0.0.1')
+ clientF = LoopbackClientFactory(client)
+ clientF.noisy = noisy
+ reactor.connectTCP('127.0.0.1', serverPort.getHost().port, clientF)
+ d = clientF.deferred
+ d.addCallback(lambda x: serverWrapper.deferred)
+ d.addCallback(lambda x: serverPort.stopListening())
+ return d
+
+
+def loopbackUNIX(server, client, noisy=True):
+ """Run session between server and client protocol instances over UNIX socket."""
+ path = tempfile.mktemp()
+ from twisted.internet import reactor
+ f = policies.WrappingFactory(protocol.Factory())
+ serverWrapper = _FireOnClose(f, server)
+ f.noisy = noisy
+ f.buildProtocol = lambda addr: serverWrapper
+ serverPort = reactor.listenUNIX(path, f)
+ clientF = LoopbackClientFactory(client)
+ clientF.noisy = noisy
+ reactor.connectUNIX(path, clientF)
+ d = clientF.deferred
+ d.addCallback(lambda x: serverWrapper.deferred)
+ d.addCallback(lambda x: serverPort.stopListening())
+ return d
diff --git a/twisted/protocols/memcache.py b/twisted/protocols/memcache.py
new file mode 100644
index 0000000..a5e987d
--- /dev/null
+++ b/twisted/protocols/memcache.py
@@ -0,0 +1,758 @@
+# -*- test-case-name: twisted.test.test_memcache -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Memcache client protocol. Memcached is a caching server, storing data in the
+form of pairs key/value, and memcache is the protocol to talk with it.
+
+To connect to a server, create a factory for L{MemCacheProtocol}::
+
+ from twisted.internet import reactor, protocol
+ from twisted.protocols.memcache import MemCacheProtocol, DEFAULT_PORT
+ d = protocol.ClientCreator(reactor, MemCacheProtocol
+ ).connectTCP("localhost", DEFAULT_PORT)
+ def doSomething(proto):
+ # Here you call the memcache operations
+ return proto.set("mykey", "a lot of data")
+ d.addCallback(doSomething)
+ reactor.run()
+
+All the operations of the memcache protocol are present, but
+L{MemCacheProtocol.set} and L{MemCacheProtocol.get} are the more important.
+
+See U{http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt} for
+more information about the protocol.
+"""
+
+try:
+ from collections import deque
+except ImportError:
+ class deque(list):
+ def popleft(self):
+ return self.pop(0)
+
+
+from twisted.protocols.basic import LineReceiver
+from twisted.protocols.policies import TimeoutMixin
+from twisted.internet.defer import Deferred, fail, TimeoutError
+from twisted.python import log
+
+
+
+DEFAULT_PORT = 11211
+
+
+
+class NoSuchCommand(Exception):
+ """
+ Exception raised when a non existent command is called.
+ """
+
+
+
+class ClientError(Exception):
+ """
+ Error caused by an invalid client call.
+ """
+
+
+
+class ServerError(Exception):
+ """
+ Problem happening on the server.
+ """
+
+
+
+class Command(object):
+ """
+ Wrap a client action into an object, that holds the values used in the
+ protocol.
+
+ @ivar _deferred: the L{Deferred} object that will be fired when the result
+ arrives.
+ @type _deferred: L{Deferred}
+
+ @ivar command: name of the command sent to the server.
+ @type command: C{str}
+ """
+
+ def __init__(self, command, **kwargs):
+ """
+ Create a command.
+
+ @param command: the name of the command.
+ @type command: C{str}
+
+ @param kwargs: this values will be stored as attributes of the object
+ for future use
+ """
+ self.command = command
+ self._deferred = Deferred()
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+
+ def success(self, value):
+ """
+ Shortcut method to fire the underlying deferred.
+ """
+ self._deferred.callback(value)
+
+
+ def fail(self, error):
+ """
+ Make the underlying deferred fails.
+ """
+ self._deferred.errback(error)
+
+
+
+class MemCacheProtocol(LineReceiver, TimeoutMixin):
+ """
+ MemCache protocol: connect to a memcached server to store/retrieve values.
+
+ @ivar persistentTimeOut: the timeout period used to wait for a response.
+ @type persistentTimeOut: C{int}
+
+ @ivar _current: current list of requests waiting for an answer from the
+ server.
+ @type _current: C{deque} of L{Command}
+
+ @ivar _lenExpected: amount of data expected in raw mode, when reading for
+ a value.
+ @type _lenExpected: C{int}
+
+ @ivar _getBuffer: current buffer of data, used to store temporary data
+ when reading in raw mode.
+ @type _getBuffer: C{list}
+
+ @ivar _bufferLength: the total amount of bytes in C{_getBuffer}.
+ @type _bufferLength: C{int}
+
+ @ivar _disconnected: indicate if the connectionLost has been called or not.
+ @type _disconnected: C{bool}
+ """
+ MAX_KEY_LENGTH = 250
+ _disconnected = False
+
+ def __init__(self, timeOut=60):
+ """
+ Create the protocol.
+
+ @param timeOut: the timeout to wait before detecting that the
+ connection is dead and close it. It's expressed in seconds.
+ @type timeOut: C{int}
+ """
+ self._current = deque()
+ self._lenExpected = None
+ self._getBuffer = None
+ self._bufferLength = None
+ self.persistentTimeOut = self.timeOut = timeOut
+
+
+ def _cancelCommands(self, reason):
+ """
+ Cancel all the outstanding commands, making them fail with C{reason}.
+ """
+ while self._current:
+ cmd = self._current.popleft()
+ cmd.fail(reason)
+
+
+ def timeoutConnection(self):
+ """
+ Close the connection in case of timeout.
+ """
+ self._cancelCommands(TimeoutError("Connection timeout"))
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ """
+ Cause any outstanding commands to fail.
+ """
+ self._disconnected = True
+ self._cancelCommands(reason)
+ LineReceiver.connectionLost(self, reason)
+
+
+ def sendLine(self, line):
+ """
+ Override sendLine to add a timeout to response.
+ """
+ if not self._current:
+ self.setTimeout(self.persistentTimeOut)
+ LineReceiver.sendLine(self, line)
+
+
+ def rawDataReceived(self, data):
+ """
+ Collect data for a get.
+ """
+ self.resetTimeout()
+ self._getBuffer.append(data)
+ self._bufferLength += len(data)
+ if self._bufferLength >= self._lenExpected + 2:
+ data = "".join(self._getBuffer)
+ buf = data[:self._lenExpected]
+ rem = data[self._lenExpected + 2:]
+ val = buf
+ self._lenExpected = None
+ self._getBuffer = None
+ self._bufferLength = None
+ cmd = self._current[0]
+ if cmd.multiple:
+ flags, cas = cmd.values[cmd.currentKey]
+ cmd.values[cmd.currentKey] = (flags, cas, val)
+ else:
+ cmd.value = val
+ self.setLineMode(rem)
+
+
+ def cmd_STORED(self):
+ """
+ Manage a success response to a set operation.
+ """
+ self._current.popleft().success(True)
+
+
+ def cmd_NOT_STORED(self):
+ """
+ Manage a specific 'not stored' response to a set operation: this is not
+ an error, but some condition wasn't met.
+ """
+ self._current.popleft().success(False)
+
+
+ def cmd_END(self):
+ """
+ This the end token to a get or a stat operation.
+ """
+ cmd = self._current.popleft()
+ if cmd.command == "get":
+ if cmd.multiple:
+ values = dict([(key, val[::2]) for key, val in
+ cmd.values.iteritems()])
+ cmd.success(values)
+ else:
+ cmd.success((cmd.flags, cmd.value))
+ elif cmd.command == "gets":
+ if cmd.multiple:
+ cmd.success(cmd.values)
+ else:
+ cmd.success((cmd.flags, cmd.cas, cmd.value))
+ elif cmd.command == "stats":
+ cmd.success(cmd.values)
+
+
+ def cmd_NOT_FOUND(self):
+ """
+ Manage error response for incr/decr/delete.
+ """
+ self._current.popleft().success(False)
+
+
+ def cmd_VALUE(self, line):
+ """
+ Prepare the reading a value after a get.
+ """
+ cmd = self._current[0]
+ if cmd.command == "get":
+ key, flags, length = line.split()
+ cas = ""
+ else:
+ key, flags, length, cas = line.split()
+ self._lenExpected = int(length)
+ self._getBuffer = []
+ self._bufferLength = 0
+ if cmd.multiple:
+ if key not in cmd.keys:
+ raise RuntimeError("Unexpected commands answer.")
+ cmd.currentKey = key
+ cmd.values[key] = [int(flags), cas]
+ else:
+ if cmd.key != key:
+ raise RuntimeError("Unexpected commands answer.")
+ cmd.flags = int(flags)
+ cmd.cas = cas
+ self.setRawMode()
+
+
+ def cmd_STAT(self, line):
+ """
+ Reception of one stat line.
+ """
+ cmd = self._current[0]
+ key, val = line.split(" ", 1)
+ cmd.values[key] = val
+
+
+ def cmd_VERSION(self, versionData):
+ """
+ Read version token.
+ """
+ self._current.popleft().success(versionData)
+
+
+ def cmd_ERROR(self):
+ """
+ An non-existent command has been sent.
+ """
+ log.err("Non-existent command sent.")
+ cmd = self._current.popleft()
+ cmd.fail(NoSuchCommand())
+
+
+ def cmd_CLIENT_ERROR(self, errText):
+ """
+ An invalid input as been sent.
+ """
+ log.err("Invalid input: %s" % (errText,))
+ cmd = self._current.popleft()
+ cmd.fail(ClientError(errText))
+
+
+ def cmd_SERVER_ERROR(self, errText):
+ """
+ An error has happened server-side.
+ """
+ log.err("Server error: %s" % (errText,))
+ cmd = self._current.popleft()
+ cmd.fail(ServerError(errText))
+
+
+ def cmd_DELETED(self):
+ """
+ A delete command has completed successfully.
+ """
+ self._current.popleft().success(True)
+
+
+ def cmd_OK(self):
+ """
+ The last command has been completed.
+ """
+ self._current.popleft().success(True)
+
+
+ def cmd_EXISTS(self):
+ """
+ A C{checkAndSet} update has failed.
+ """
+ self._current.popleft().success(False)
+
+
+ def lineReceived(self, line):
+ """
+ Receive line commands from the server.
+ """
+ self.resetTimeout()
+ token = line.split(" ", 1)[0]
+ # First manage standard commands without space
+ cmd = getattr(self, "cmd_%s" % (token,), None)
+ if cmd is not None:
+ args = line.split(" ", 1)[1:]
+ if args:
+ cmd(args[0])
+ else:
+ cmd()
+ else:
+ # Then manage commands with space in it
+ line = line.replace(" ", "_")
+ cmd = getattr(self, "cmd_%s" % (line,), None)
+ if cmd is not None:
+ cmd()
+ else:
+ # Increment/Decrement response
+ cmd = self._current.popleft()
+ val = int(line)
+ cmd.success(val)
+ if not self._current:
+ # No pending request, remove timeout
+ self.setTimeout(None)
+
+
+ def increment(self, key, val=1):
+ """
+ Increment the value of C{key} by given value (default to 1).
+ C{key} must be consistent with an int. Return the new value.
+
+ @param key: the key to modify.
+ @type key: C{str}
+
+ @param val: the value to increment.
+ @type val: C{int}
+
+ @return: a deferred with will be called back with the new value
+ associated with the key (after the increment).
+ @rtype: L{Deferred}
+ """
+ return self._incrdecr("incr", key, val)
+
+
+ def decrement(self, key, val=1):
+ """
+ Decrement the value of C{key} by given value (default to 1).
+ C{key} must be consistent with an int. Return the new value, coerced to
+ 0 if negative.
+
+ @param key: the key to modify.
+ @type key: C{str}
+
+ @param val: the value to decrement.
+ @type val: C{int}
+
+ @return: a deferred with will be called back with the new value
+ associated with the key (after the decrement).
+ @rtype: L{Deferred}
+ """
+ return self._incrdecr("decr", key, val)
+
+
+ def _incrdecr(self, cmd, key, val):
+ """
+ Internal wrapper for incr/decr.
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ if not isinstance(key, str):
+ return fail(ClientError(
+ "Invalid type for key: %s, expecting a string" % (type(key),)))
+ if len(key) > self.MAX_KEY_LENGTH:
+ return fail(ClientError("Key too long"))
+ fullcmd = "%s %s %d" % (cmd, key, int(val))
+ self.sendLine(fullcmd)
+ cmdObj = Command(cmd, key=key)
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+ def replace(self, key, val, flags=0, expireTime=0):
+ """
+ Replace the given C{key}. It must already exist in the server.
+
+ @param key: the key to replace.
+ @type key: C{str}
+
+ @param val: the new value associated with the key.
+ @type val: C{str}
+
+ @param flags: the flags to store with the key.
+ @type flags: C{int}
+
+ @param expireTime: if different from 0, the relative time in seconds
+ when the key will be deleted from the store.
+ @type expireTime: C{int}
+
+ @return: a deferred that will fire with C{True} if the operation has
+ succeeded, and C{False} with the key didn't previously exist.
+ @rtype: L{Deferred}
+ """
+ return self._set("replace", key, val, flags, expireTime, "")
+
+
+ def add(self, key, val, flags=0, expireTime=0):
+ """
+ Add the given C{key}. It must not exist in the server.
+
+ @param key: the key to add.
+ @type key: C{str}
+
+ @param val: the value associated with the key.
+ @type val: C{str}
+
+ @param flags: the flags to store with the key.
+ @type flags: C{int}
+
+ @param expireTime: if different from 0, the relative time in seconds
+ when the key will be deleted from the store.
+ @type expireTime: C{int}
+
+ @return: a deferred that will fire with C{True} if the operation has
+ succeeded, and C{False} with the key already exists.
+ @rtype: L{Deferred}
+ """
+ return self._set("add", key, val, flags, expireTime, "")
+
+
+ def set(self, key, val, flags=0, expireTime=0):
+ """
+ Set the given C{key}.
+
+ @param key: the key to set.
+ @type key: C{str}
+
+ @param val: the value associated with the key.
+ @type val: C{str}
+
+ @param flags: the flags to store with the key.
+ @type flags: C{int}
+
+ @param expireTime: if different from 0, the relative time in seconds
+ when the key will be deleted from the store.
+ @type expireTime: C{int}
+
+ @return: a deferred that will fire with C{True} if the operation has
+ succeeded.
+ @rtype: L{Deferred}
+ """
+ return self._set("set", key, val, flags, expireTime, "")
+
+
+ def checkAndSet(self, key, val, cas, flags=0, expireTime=0):
+ """
+ Change the content of C{key} only if the C{cas} value matches the
+ current one associated with the key. Use this to store a value which
+ hasn't been modified since last time you fetched it.
+
+ @param key: The key to set.
+ @type key: C{str}
+
+ @param val: The value associated with the key.
+ @type val: C{str}
+
+ @param cas: Unique 64-bit value returned by previous call of C{get}.
+ @type cas: C{str}
+
+ @param flags: The flags to store with the key.
+ @type flags: C{int}
+
+ @param expireTime: If different from 0, the relative time in seconds
+ when the key will be deleted from the store.
+ @type expireTime: C{int}
+
+ @return: A deferred that will fire with C{True} if the operation has
+ succeeded, C{False} otherwise.
+ @rtype: L{Deferred}
+ """
+ return self._set("cas", key, val, flags, expireTime, cas)
+
+
+ def _set(self, cmd, key, val, flags, expireTime, cas):
+ """
+ Internal wrapper for setting values.
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ if not isinstance(key, str):
+ return fail(ClientError(
+ "Invalid type for key: %s, expecting a string" % (type(key),)))
+ if len(key) > self.MAX_KEY_LENGTH:
+ return fail(ClientError("Key too long"))
+ if not isinstance(val, str):
+ return fail(ClientError(
+ "Invalid type for value: %s, expecting a string" %
+ (type(val),)))
+ if cas:
+ cas = " " + cas
+ length = len(val)
+ fullcmd = "%s %s %d %d %d%s" % (
+ cmd, key, flags, expireTime, length, cas)
+ self.sendLine(fullcmd)
+ self.sendLine(val)
+ cmdObj = Command(cmd, key=key, flags=flags, length=length)
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+ def append(self, key, val):
+ """
+ Append given data to the value of an existing key.
+
+ @param key: The key to modify.
+ @type key: C{str}
+
+ @param val: The value to append to the current value associated with
+ the key.
+ @type val: C{str}
+
+ @return: A deferred that will fire with C{True} if the operation has
+ succeeded, C{False} otherwise.
+ @rtype: L{Deferred}
+ """
+ # Even if flags and expTime values are ignored, we have to pass them
+ return self._set("append", key, val, 0, 0, "")
+
+
+ def prepend(self, key, val):
+ """
+ Prepend given data to the value of an existing key.
+
+ @param key: The key to modify.
+ @type key: C{str}
+
+ @param val: The value to prepend to the current value associated with
+ the key.
+ @type val: C{str}
+
+ @return: A deferred that will fire with C{True} if the operation has
+ succeeded, C{False} otherwise.
+ @rtype: L{Deferred}
+ """
+ # Even if flags and expTime values are ignored, we have to pass them
+ return self._set("prepend", key, val, 0, 0, "")
+
+
+ def get(self, key, withIdentifier=False):
+ """
+ Get the given C{key}. It doesn't support multiple keys. If
+ C{withIdentifier} is set to C{True}, the command issued is a C{gets},
+ that will return the current identifier associated with the value. This
+ identifier has to be used when issuing C{checkAndSet} update later,
+ using the corresponding method.
+
+ @param key: The key to retrieve.
+ @type key: C{str}
+
+ @param withIdentifier: If set to C{True}, retrieve the current
+ identifier along with the value and the flags.
+ @type withIdentifier: C{bool}
+
+ @return: A deferred that will fire with the tuple (flags, value) if
+ C{withIdentifier} is C{False}, or (flags, cas identifier, value)
+ if C{True}. If the server indicates there is no value
+ associated with C{key}, the returned value will be C{None} and
+ the returned flags will be C{0}.
+ @rtype: L{Deferred}
+ """
+ return self._get([key], withIdentifier, False)
+
+
+ def getMultiple(self, keys, withIdentifier=False):
+ """
+ Get the given list of C{keys}. If C{withIdentifier} is set to C{True},
+ the command issued is a C{gets}, that will return the identifiers
+ associated with each values. This identifier has to be used when
+ issuing C{checkAndSet} update later, using the corresponding method.
+
+ @param keys: The keys to retrieve.
+ @type keys: C{list} of C{str}
+
+ @param withIdentifier: If set to C{True}, retrieve the identifiers
+ along with the values and the flags.
+ @type withIdentifier: C{bool}
+
+ @return: A deferred that will fire with a dictionary with the elements
+ of C{keys} as keys and the tuples (flags, value) as values if
+ C{withIdentifier} is C{False}, or (flags, cas identifier, value) if
+ C{True}. If the server indicates there is no value associated with
+ C{key}, the returned values will be C{None} and the returned flags
+ will be C{0}.
+ @rtype: L{Deferred}
+
+ @since: 9.0
+ """
+ return self._get(keys, withIdentifier, True)
+
+ def _get(self, keys, withIdentifier, multiple):
+ """
+ Helper method for C{get} and C{getMultiple}.
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ for key in keys:
+ if not isinstance(key, str):
+ return fail(ClientError(
+ "Invalid type for key: %s, expecting a string" % (type(key),)))
+ if len(key) > self.MAX_KEY_LENGTH:
+ return fail(ClientError("Key too long"))
+ if withIdentifier:
+ cmd = "gets"
+ else:
+ cmd = "get"
+ fullcmd = "%s %s" % (cmd, " ".join(keys))
+ self.sendLine(fullcmd)
+ if multiple:
+ values = dict([(key, (0, "", None)) for key in keys])
+ cmdObj = Command(cmd, keys=keys, values=values, multiple=True)
+ else:
+ cmdObj = Command(cmd, key=keys[0], value=None, flags=0, cas="",
+ multiple=False)
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+ def stats(self, arg=None):
+ """
+ Get some stats from the server. It will be available as a dict.
+
+ @param arg: An optional additional string which will be sent along
+ with the I{stats} command. The interpretation of this value by
+ the server is left undefined by the memcache protocol
+ specification.
+ @type arg: L{NoneType} or L{str}
+
+ @return: a deferred that will fire with a C{dict} of the available
+ statistics.
+ @rtype: L{Deferred}
+ """
+ if arg:
+ cmd = "stats " + arg
+ else:
+ cmd = "stats"
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ self.sendLine(cmd)
+ cmdObj = Command("stats", values={})
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+ def version(self):
+ """
+ Get the version of the server.
+
+ @return: a deferred that will fire with the string value of the
+ version.
+ @rtype: L{Deferred}
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ self.sendLine("version")
+ cmdObj = Command("version")
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+ def delete(self, key):
+ """
+ Delete an existing C{key}.
+
+ @param key: the key to delete.
+ @type key: C{str}
+
+ @return: a deferred that will be called back with C{True} if the key
+ was successfully deleted, or C{False} if not.
+ @rtype: L{Deferred}
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ if not isinstance(key, str):
+ return fail(ClientError(
+ "Invalid type for key: %s, expecting a string" % (type(key),)))
+ self.sendLine("delete %s" % key)
+ cmdObj = Command("delete", key=key)
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+ def flushAll(self):
+ """
+ Flush all cached values.
+
+ @return: a deferred that will be called back with C{True} when the
+ operation has succeeded.
+ @rtype: L{Deferred}
+ """
+ if self._disconnected:
+ return fail(RuntimeError("not connected"))
+ self.sendLine("flush_all")
+ cmdObj = Command("flush_all")
+ self._current.append(cmdObj)
+ return cmdObj._deferred
+
+
+
+__all__ = ["MemCacheProtocol", "DEFAULT_PORT", "NoSuchCommand", "ClientError",
+ "ServerError"]
diff --git a/twisted/protocols/mice/__init__.py b/twisted/protocols/mice/__init__.py
new file mode 100644
index 0000000..fda89c5
--- /dev/null
+++ b/twisted/protocols/mice/__init__.py
@@ -0,0 +1 @@
+"""Mice Protocols."""
diff --git a/twisted/protocols/mice/mouseman.py b/twisted/protocols/mice/mouseman.py
new file mode 100644
index 0000000..4071b20
--- /dev/null
+++ b/twisted/protocols/mice/mouseman.py
@@ -0,0 +1,127 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""Logictech MouseMan serial protocol.
+
+http://www.softnco.demon.co.uk/SerialMouse.txt
+"""
+
+from twisted.internet import protocol
+
+class MouseMan(protocol.Protocol):
+ """
+
+ Parser for Logitech MouseMan serial mouse protocol (compatible
+ with Microsoft Serial Mouse).
+
+ """
+
+ state = 'initial'
+
+ leftbutton=None
+ rightbutton=None
+ middlebutton=None
+
+ leftold=None
+ rightold=None
+ middleold=None
+
+ horiz=None
+ vert=None
+ horizold=None
+ vertold=None
+
+ def down_left(self):
+ pass
+
+ def up_left(self):
+ pass
+
+ def down_middle(self):
+ pass
+
+ def up_middle(self):
+ pass
+
+ def down_right(self):
+ pass
+
+ def up_right(self):
+ pass
+
+ def move(self, x, y):
+ pass
+
+ horiz=None
+ vert=None
+
+ def state_initial(self, byte):
+ if byte & 1<<6:
+ self.word1=byte
+ self.leftbutton = byte & 1<<5
+ self.rightbutton = byte & 1<<4
+ return 'horiz'
+ else:
+ return 'initial'
+
+ def state_horiz(self, byte):
+ if byte & 1<<6:
+ return self.state_initial(byte)
+ else:
+ x=(self.word1 & 0x03)<<6 | (byte & 0x3f)
+ if x>=128:
+ x=-256+x
+ self.horiz = x
+ return 'vert'
+
+ def state_vert(self, byte):
+ if byte & 1<<6:
+ # short packet
+ return self.state_initial(byte)
+ else:
+ x = (self.word1 & 0x0c)<<4 | (byte & 0x3f)
+ if x>=128:
+ x=-256+x
+ self.vert = x
+ self.snapshot()
+ return 'maybemiddle'
+
+ def state_maybemiddle(self, byte):
+ if byte & 1<<6:
+ self.snapshot()
+ return self.state_initial(byte)
+ else:
+ self.middlebutton=byte & 1<<5
+ self.snapshot()
+ return 'initial'
+
+ def snapshot(self):
+ if self.leftbutton and not self.leftold:
+ self.down_left()
+ self.leftold=1
+ if not self.leftbutton and self.leftold:
+ self.up_left()
+ self.leftold=0
+
+ if self.middlebutton and not self.middleold:
+ self.down_middle()
+ self.middleold=1
+ if not self.middlebutton and self.middleold:
+ self.up_middle()
+ self.middleold=0
+
+ if self.rightbutton and not self.rightold:
+ self.down_right()
+ self.rightold=1
+ if not self.rightbutton and self.rightold:
+ self.up_right()
+ self.rightold=0
+
+ if self.horiz or self.vert:
+ self.move(self.horiz, self.vert)
+
+ def dataReceived(self, data):
+ for c in data:
+ byte = ord(c)
+ self.state = getattr(self, 'state_'+self.state)(byte)
diff --git a/twisted/protocols/pcp.py b/twisted/protocols/pcp.py
new file mode 100644
index 0000000..8970f90
--- /dev/null
+++ b/twisted/protocols/pcp.py
@@ -0,0 +1,204 @@
+# -*- test-case-name: twisted.test.test_pcp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Producer-Consumer Proxy.
+"""
+
+from zope.interface import implements
+
+from twisted.internet import interfaces
+
+
+class BasicProducerConsumerProxy:
+ """
+ I can act as a man in the middle between any Producer and Consumer.
+
+ @ivar producer: the Producer I subscribe to.
+ @type producer: L{IProducer<interfaces.IProducer>}
+ @ivar consumer: the Consumer I publish to.
+ @type consumer: L{IConsumer<interfaces.IConsumer>}
+ @ivar paused: As a Producer, am I paused?
+ @type paused: bool
+ """
+ implements(interfaces.IProducer, interfaces.IConsumer)
+
+ consumer = None
+ producer = None
+ producerIsStreaming = None
+ iAmStreaming = True
+ outstandingPull = False
+ paused = False
+ stopped = False
+
+ def __init__(self, consumer):
+ self._buffer = []
+ if consumer is not None:
+ self.consumer = consumer
+ consumer.registerProducer(self, self.iAmStreaming)
+
+ # Producer methods:
+
+ def pauseProducing(self):
+ self.paused = True
+ if self.producer:
+ self.producer.pauseProducing()
+
+ def resumeProducing(self):
+ self.paused = False
+ if self._buffer:
+ # TODO: Check to see if consumer supports writeSeq.
+ self.consumer.write(''.join(self._buffer))
+ self._buffer[:] = []
+ else:
+ if not self.iAmStreaming:
+ self.outstandingPull = True
+
+ if self.producer is not None:
+ self.producer.resumeProducing()
+
+ def stopProducing(self):
+ if self.producer is not None:
+ self.producer.stopProducing()
+ if self.consumer is not None:
+ del self.consumer
+
+ # Consumer methods:
+
+ def write(self, data):
+ if self.paused or (not self.iAmStreaming and not self.outstandingPull):
+ # We could use that fifo queue here.
+ self._buffer.append(data)
+
+ elif self.consumer is not None:
+ self.consumer.write(data)
+ self.outstandingPull = False
+
+ def finish(self):
+ if self.consumer is not None:
+ self.consumer.finish()
+ self.unregisterProducer()
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+ self.producerIsStreaming = streaming
+
+ def unregisterProducer(self):
+ if self.producer is not None:
+ del self.producer
+ del self.producerIsStreaming
+ if self.consumer:
+ self.consumer.unregisterProducer()
+
+ def __repr__(self):
+ return '<%s@%x around %s>' % (self.__class__, id(self), self.consumer)
+
+
+class ProducerConsumerProxy(BasicProducerConsumerProxy):
+ """ProducerConsumerProxy with a finite buffer.
+
+ When my buffer fills up, I have my parent Producer pause until my buffer
+ has room in it again.
+ """
+ # Copies much from abstract.FileDescriptor
+ bufferSize = 2**2**2**2
+
+ producerPaused = False
+ unregistered = False
+
+ def pauseProducing(self):
+ # Does *not* call up to ProducerConsumerProxy to relay the pause
+ # message through to my parent Producer.
+ self.paused = True
+
+ def resumeProducing(self):
+ self.paused = False
+ if self._buffer:
+ data = ''.join(self._buffer)
+ bytesSent = self._writeSomeData(data)
+ if bytesSent < len(data):
+ unsent = data[bytesSent:]
+ assert not self.iAmStreaming, (
+ "Streaming producer did not write all its data.")
+ self._buffer[:] = [unsent]
+ else:
+ self._buffer[:] = []
+ else:
+ bytesSent = 0
+
+ if (self.unregistered and bytesSent and not self._buffer and
+ self.consumer is not None):
+ self.consumer.unregisterProducer()
+
+ if not self.iAmStreaming:
+ self.outstandingPull = not bytesSent
+
+ if self.producer is not None:
+ bytesBuffered = sum([len(s) for s in self._buffer])
+ # TODO: You can see here the potential for high and low
+ # watermarks, where bufferSize would be the high mark when we
+ # ask the upstream producer to pause, and we wouldn't have
+ # it resume again until it hit the low mark. Or if producer
+ # is Pull, maybe we'd like to pull from it as much as necessary
+ # to keep our buffer full to the low mark, so we're never caught
+ # without something to send.
+ if self.producerPaused and (bytesBuffered < self.bufferSize):
+ # Now that our buffer is empty,
+ self.producerPaused = False
+ self.producer.resumeProducing()
+ elif self.outstandingPull:
+ # I did not have any data to write in response to a pull,
+ # so I'd better pull some myself.
+ self.producer.resumeProducing()
+
+ def write(self, data):
+ if self.paused or (not self.iAmStreaming and not self.outstandingPull):
+ # We could use that fifo queue here.
+ self._buffer.append(data)
+
+ elif self.consumer is not None:
+ assert not self._buffer, (
+ "Writing fresh data to consumer before my buffer is empty!")
+ # I'm going to use _writeSomeData here so that there is only one
+ # path to self.consumer.write. But it doesn't actually make sense,
+ # if I am streaming, for some data to not be all data. But maybe I
+ # am not streaming, but I am writing here anyway, because there was
+ # an earlier request for data which was not answered.
+ bytesSent = self._writeSomeData(data)
+ self.outstandingPull = False
+ if not bytesSent == len(data):
+ assert not self.iAmStreaming, (
+ "Streaming producer did not write all its data.")
+ self._buffer.append(data[bytesSent:])
+
+ if (self.producer is not None) and self.producerIsStreaming:
+ bytesBuffered = sum([len(s) for s in self._buffer])
+ if bytesBuffered >= self.bufferSize:
+
+ self.producer.pauseProducing()
+ self.producerPaused = True
+
+ def registerProducer(self, producer, streaming):
+ self.unregistered = False
+ BasicProducerConsumerProxy.registerProducer(self, producer, streaming)
+ if not streaming:
+ producer.resumeProducing()
+
+ def unregisterProducer(self):
+ if self.producer is not None:
+ del self.producer
+ del self.producerIsStreaming
+ self.unregistered = True
+ if self.consumer and not self._buffer:
+ self.consumer.unregisterProducer()
+
+ def _writeSomeData(self, data):
+ """Write as much of this data as possible.
+
+ @returns: The number of bytes written.
+ """
+ if self.consumer is None:
+ return 0
+ self.consumer.write(data)
+ return len(data)
diff --git a/twisted/protocols/policies.py b/twisted/protocols/policies.py
new file mode 100644
index 0000000..83a3ec7
--- /dev/null
+++ b/twisted/protocols/policies.py
@@ -0,0 +1,725 @@
+# -*- test-case-name: twisted.test.test_policies -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Resource limiting policies.
+
+@seealso: See also L{twisted.protocols.htb} for rate limiting.
+"""
+
+# system imports
+import sys, operator
+
+from zope.interface import directlyProvides, providedBy
+
+# twisted imports
+from twisted.internet.protocol import ServerFactory, Protocol, ClientFactory
+from twisted.internet import error
+from twisted.internet.interfaces import ILoggingContext
+from twisted.python import log
+
+
+def _wrappedLogPrefix(wrapper, wrapped):
+ """
+ Compute a log prefix for a wrapper and the object it wraps.
+
+ @rtype: C{str}
+ """
+ if ILoggingContext.providedBy(wrapped):
+ logPrefix = wrapped.logPrefix()
+ else:
+ logPrefix = wrapped.__class__.__name__
+ return "%s (%s)" % (logPrefix, wrapper.__class__.__name__)
+
+
+
+class ProtocolWrapper(Protocol):
+ """
+ Wraps protocol instances and acts as their transport as well.
+
+ @ivar wrappedProtocol: An L{IProtocol<twisted.internet.interfaces.IProtocol>}
+ provider to which L{IProtocol<twisted.internet.interfaces.IProtocol>}
+ method calls onto this L{ProtocolWrapper} will be proxied.
+
+ @ivar factory: The L{WrappingFactory} which created this
+ L{ProtocolWrapper}.
+ """
+
+ disconnecting = 0
+
+ def __init__(self, factory, wrappedProtocol):
+ self.wrappedProtocol = wrappedProtocol
+ self.factory = factory
+
+
+ def logPrefix(self):
+ """
+ Use a customized log prefix mentioning both the wrapped protocol and
+ the current one.
+ """
+ return _wrappedLogPrefix(self, self.wrappedProtocol)
+
+
+ def makeConnection(self, transport):
+ """
+ When a connection is made, register this wrapper with its factory,
+ save the real transport, and connect the wrapped protocol to this
+ L{ProtocolWrapper} to intercept any transport calls it makes.
+ """
+ directlyProvides(self, providedBy(transport))
+ Protocol.makeConnection(self, transport)
+ self.factory.registerProtocol(self)
+ self.wrappedProtocol.makeConnection(self)
+
+
+ # Transport relaying
+
+ def write(self, data):
+ self.transport.write(data)
+
+
+ def writeSequence(self, data):
+ self.transport.writeSequence(data)
+
+
+ def loseConnection(self):
+ self.disconnecting = 1
+ self.transport.loseConnection()
+
+
+ def getPeer(self):
+ return self.transport.getPeer()
+
+
+ def getHost(self):
+ return self.transport.getHost()
+
+
+ def registerProducer(self, producer, streaming):
+ self.transport.registerProducer(producer, streaming)
+
+
+ def unregisterProducer(self):
+ self.transport.unregisterProducer()
+
+
+ def stopConsuming(self):
+ self.transport.stopConsuming()
+
+
+ def __getattr__(self, name):
+ return getattr(self.transport, name)
+
+
+ # Protocol relaying
+
+ def dataReceived(self, data):
+ self.wrappedProtocol.dataReceived(data)
+
+
+ def connectionLost(self, reason):
+ self.factory.unregisterProtocol(self)
+ self.wrappedProtocol.connectionLost(reason)
+
+
+
+class WrappingFactory(ClientFactory):
+ """
+ Wraps a factory and its protocols, and keeps track of them.
+ """
+
+ protocol = ProtocolWrapper
+
+ def __init__(self, wrappedFactory):
+ self.wrappedFactory = wrappedFactory
+ self.protocols = {}
+
+
+ def logPrefix(self):
+ """
+ Generate a log prefix mentioning both the wrapped factory and this one.
+ """
+ return _wrappedLogPrefix(self, self.wrappedFactory)
+
+
+ def doStart(self):
+ self.wrappedFactory.doStart()
+ ClientFactory.doStart(self)
+
+
+ def doStop(self):
+ self.wrappedFactory.doStop()
+ ClientFactory.doStop(self)
+
+
+ def startedConnecting(self, connector):
+ self.wrappedFactory.startedConnecting(connector)
+
+
+ def clientConnectionFailed(self, connector, reason):
+ self.wrappedFactory.clientConnectionFailed(connector, reason)
+
+
+ def clientConnectionLost(self, connector, reason):
+ self.wrappedFactory.clientConnectionLost(connector, reason)
+
+
+ def buildProtocol(self, addr):
+ return self.protocol(self, self.wrappedFactory.buildProtocol(addr))
+
+
+ def registerProtocol(self, p):
+ """
+ Called by protocol to register itself.
+ """
+ self.protocols[p] = 1
+
+
+ def unregisterProtocol(self, p):
+ """
+ Called by protocols when they go away.
+ """
+ del self.protocols[p]
+
+
+
+class ThrottlingProtocol(ProtocolWrapper):
+ """Protocol for ThrottlingFactory."""
+
+ # wrap API for tracking bandwidth
+
+ def write(self, data):
+ self.factory.registerWritten(len(data))
+ ProtocolWrapper.write(self, data)
+
+ def writeSequence(self, seq):
+ self.factory.registerWritten(reduce(operator.add, map(len, seq)))
+ ProtocolWrapper.writeSequence(self, seq)
+
+ def dataReceived(self, data):
+ self.factory.registerRead(len(data))
+ ProtocolWrapper.dataReceived(self, data)
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+ ProtocolWrapper.registerProducer(self, producer, streaming)
+
+ def unregisterProducer(self):
+ del self.producer
+ ProtocolWrapper.unregisterProducer(self)
+
+
+ def throttleReads(self):
+ self.transport.pauseProducing()
+
+ def unthrottleReads(self):
+ self.transport.resumeProducing()
+
+ def throttleWrites(self):
+ if hasattr(self, "producer"):
+ self.producer.pauseProducing()
+
+ def unthrottleWrites(self):
+ if hasattr(self, "producer"):
+ self.producer.resumeProducing()
+
+
+class ThrottlingFactory(WrappingFactory):
+ """
+ Throttles bandwidth and number of connections.
+
+ Write bandwidth will only be throttled if there is a producer
+ registered.
+ """
+
+ protocol = ThrottlingProtocol
+
+ def __init__(self, wrappedFactory, maxConnectionCount=sys.maxint,
+ readLimit=None, writeLimit=None):
+ WrappingFactory.__init__(self, wrappedFactory)
+ self.connectionCount = 0
+ self.maxConnectionCount = maxConnectionCount
+ self.readLimit = readLimit # max bytes we should read per second
+ self.writeLimit = writeLimit # max bytes we should write per second
+ self.readThisSecond = 0
+ self.writtenThisSecond = 0
+ self.unthrottleReadsID = None
+ self.checkReadBandwidthID = None
+ self.unthrottleWritesID = None
+ self.checkWriteBandwidthID = None
+
+
+ def callLater(self, period, func):
+ """
+ Wrapper around L{reactor.callLater} for test purpose.
+ """
+ from twisted.internet import reactor
+ return reactor.callLater(period, func)
+
+
+ def registerWritten(self, length):
+ """
+ Called by protocol to tell us more bytes were written.
+ """
+ self.writtenThisSecond += length
+
+
+ def registerRead(self, length):
+ """
+ Called by protocol to tell us more bytes were read.
+ """
+ self.readThisSecond += length
+
+
+ def checkReadBandwidth(self):
+ """
+ Checks if we've passed bandwidth limits.
+ """
+ if self.readThisSecond > self.readLimit:
+ self.throttleReads()
+ throttleTime = (float(self.readThisSecond) / self.readLimit) - 1.0
+ self.unthrottleReadsID = self.callLater(throttleTime,
+ self.unthrottleReads)
+ self.readThisSecond = 0
+ self.checkReadBandwidthID = self.callLater(1, self.checkReadBandwidth)
+
+
+ def checkWriteBandwidth(self):
+ if self.writtenThisSecond > self.writeLimit:
+ self.throttleWrites()
+ throttleTime = (float(self.writtenThisSecond) / self.writeLimit) - 1.0
+ self.unthrottleWritesID = self.callLater(throttleTime,
+ self.unthrottleWrites)
+ # reset for next round
+ self.writtenThisSecond = 0
+ self.checkWriteBandwidthID = self.callLater(1, self.checkWriteBandwidth)
+
+
+ def throttleReads(self):
+ """
+ Throttle reads on all protocols.
+ """
+ log.msg("Throttling reads on %s" % self)
+ for p in self.protocols.keys():
+ p.throttleReads()
+
+
+ def unthrottleReads(self):
+ """
+ Stop throttling reads on all protocols.
+ """
+ self.unthrottleReadsID = None
+ log.msg("Stopped throttling reads on %s" % self)
+ for p in self.protocols.keys():
+ p.unthrottleReads()
+
+
+ def throttleWrites(self):
+ """
+ Throttle writes on all protocols.
+ """
+ log.msg("Throttling writes on %s" % self)
+ for p in self.protocols.keys():
+ p.throttleWrites()
+
+
+ def unthrottleWrites(self):
+ """
+ Stop throttling writes on all protocols.
+ """
+ self.unthrottleWritesID = None
+ log.msg("Stopped throttling writes on %s" % self)
+ for p in self.protocols.keys():
+ p.unthrottleWrites()
+
+
+ def buildProtocol(self, addr):
+ if self.connectionCount == 0:
+ if self.readLimit is not None:
+ self.checkReadBandwidth()
+ if self.writeLimit is not None:
+ self.checkWriteBandwidth()
+
+ if self.connectionCount < self.maxConnectionCount:
+ self.connectionCount += 1
+ return WrappingFactory.buildProtocol(self, addr)
+ else:
+ log.msg("Max connection count reached!")
+ return None
+
+
+ def unregisterProtocol(self, p):
+ WrappingFactory.unregisterProtocol(self, p)
+ self.connectionCount -= 1
+ if self.connectionCount == 0:
+ if self.unthrottleReadsID is not None:
+ self.unthrottleReadsID.cancel()
+ if self.checkReadBandwidthID is not None:
+ self.checkReadBandwidthID.cancel()
+ if self.unthrottleWritesID is not None:
+ self.unthrottleWritesID.cancel()
+ if self.checkWriteBandwidthID is not None:
+ self.checkWriteBandwidthID.cancel()
+
+
+
+class SpewingProtocol(ProtocolWrapper):
+ def dataReceived(self, data):
+ log.msg("Received: %r" % data)
+ ProtocolWrapper.dataReceived(self,data)
+
+ def write(self, data):
+ log.msg("Sending: %r" % data)
+ ProtocolWrapper.write(self,data)
+
+
+
+class SpewingFactory(WrappingFactory):
+ protocol = SpewingProtocol
+
+
+
+class LimitConnectionsByPeer(WrappingFactory):
+
+ maxConnectionsPerPeer = 5
+
+ def startFactory(self):
+ self.peerConnections = {}
+
+ def buildProtocol(self, addr):
+ peerHost = addr[0]
+ connectionCount = self.peerConnections.get(peerHost, 0)
+ if connectionCount >= self.maxConnectionsPerPeer:
+ return None
+ self.peerConnections[peerHost] = connectionCount + 1
+ return WrappingFactory.buildProtocol(self, addr)
+
+ def unregisterProtocol(self, p):
+ peerHost = p.getPeer()[1]
+ self.peerConnections[peerHost] -= 1
+ if self.peerConnections[peerHost] == 0:
+ del self.peerConnections[peerHost]
+
+
+class LimitTotalConnectionsFactory(ServerFactory):
+ """
+ Factory that limits the number of simultaneous connections.
+
+ @type connectionCount: C{int}
+ @ivar connectionCount: number of current connections.
+ @type connectionLimit: C{int} or C{None}
+ @cvar connectionLimit: maximum number of connections.
+ @type overflowProtocol: L{Protocol} or C{None}
+ @cvar overflowProtocol: Protocol to use for new connections when
+ connectionLimit is exceeded. If C{None} (the default value), excess
+ connections will be closed immediately.
+ """
+ connectionCount = 0
+ connectionLimit = None
+ overflowProtocol = None
+
+ def buildProtocol(self, addr):
+ if (self.connectionLimit is None or
+ self.connectionCount < self.connectionLimit):
+ # Build the normal protocol
+ wrappedProtocol = self.protocol()
+ elif self.overflowProtocol is None:
+ # Just drop the connection
+ return None
+ else:
+ # Too many connections, so build the overflow protocol
+ wrappedProtocol = self.overflowProtocol()
+
+ wrappedProtocol.factory = self
+ protocol = ProtocolWrapper(self, wrappedProtocol)
+ self.connectionCount += 1
+ return protocol
+
+ def registerProtocol(self, p):
+ pass
+
+ def unregisterProtocol(self, p):
+ self.connectionCount -= 1
+
+
+
+class TimeoutProtocol(ProtocolWrapper):
+ """
+ Protocol that automatically disconnects when the connection is idle.
+ """
+
+ def __init__(self, factory, wrappedProtocol, timeoutPeriod):
+ """
+ Constructor.
+
+ @param factory: An L{IFactory}.
+ @param wrappedProtocol: A L{Protocol} to wrapp.
+ @param timeoutPeriod: Number of seconds to wait for activity before
+ timing out.
+ """
+ ProtocolWrapper.__init__(self, factory, wrappedProtocol)
+ self.timeoutCall = None
+ self.setTimeout(timeoutPeriod)
+
+
+ def setTimeout(self, timeoutPeriod=None):
+ """
+ Set a timeout.
+
+ This will cancel any existing timeouts.
+
+ @param timeoutPeriod: If not C{None}, change the timeout period.
+ Otherwise, use the existing value.
+ """
+ self.cancelTimeout()
+ if timeoutPeriod is not None:
+ self.timeoutPeriod = timeoutPeriod
+ self.timeoutCall = self.factory.callLater(self.timeoutPeriod, self.timeoutFunc)
+
+
+ def cancelTimeout(self):
+ """
+ Cancel the timeout.
+
+ If the timeout was already cancelled, this does nothing.
+ """
+ if self.timeoutCall:
+ try:
+ self.timeoutCall.cancel()
+ except error.AlreadyCalled:
+ pass
+ self.timeoutCall = None
+
+
+ def resetTimeout(self):
+ """
+ Reset the timeout, usually because some activity just happened.
+ """
+ if self.timeoutCall:
+ self.timeoutCall.reset(self.timeoutPeriod)
+
+
+ def write(self, data):
+ self.resetTimeout()
+ ProtocolWrapper.write(self, data)
+
+
+ def writeSequence(self, seq):
+ self.resetTimeout()
+ ProtocolWrapper.writeSequence(self, seq)
+
+
+ def dataReceived(self, data):
+ self.resetTimeout()
+ ProtocolWrapper.dataReceived(self, data)
+
+
+ def connectionLost(self, reason):
+ self.cancelTimeout()
+ ProtocolWrapper.connectionLost(self, reason)
+
+
+ def timeoutFunc(self):
+ """
+ This method is called when the timeout is triggered.
+
+ By default it calls L{loseConnection}. Override this if you want
+ something else to happen.
+ """
+ self.loseConnection()
+
+
+
+class TimeoutFactory(WrappingFactory):
+ """
+ Factory for TimeoutWrapper.
+ """
+ protocol = TimeoutProtocol
+
+
+ def __init__(self, wrappedFactory, timeoutPeriod=30*60):
+ self.timeoutPeriod = timeoutPeriod
+ WrappingFactory.__init__(self, wrappedFactory)
+
+
+ def buildProtocol(self, addr):
+ return self.protocol(self, self.wrappedFactory.buildProtocol(addr),
+ timeoutPeriod=self.timeoutPeriod)
+
+
+ def callLater(self, period, func):
+ """
+ Wrapper around L{reactor.callLater} for test purpose.
+ """
+ from twisted.internet import reactor
+ return reactor.callLater(period, func)
+
+
+
+class TrafficLoggingProtocol(ProtocolWrapper):
+
+ def __init__(self, factory, wrappedProtocol, logfile, lengthLimit=None,
+ number=0):
+ """
+ @param factory: factory which created this protocol.
+ @type factory: C{protocol.Factory}.
+ @param wrappedProtocol: the underlying protocol.
+ @type wrappedProtocol: C{protocol.Protocol}.
+ @param logfile: file opened for writing used to write log messages.
+ @type logfile: C{file}
+ @param lengthLimit: maximum size of the datareceived logged.
+ @type lengthLimit: C{int}
+ @param number: identifier of the connection.
+ @type number: C{int}.
+ """
+ ProtocolWrapper.__init__(self, factory, wrappedProtocol)
+ self.logfile = logfile
+ self.lengthLimit = lengthLimit
+ self._number = number
+
+
+ def _log(self, line):
+ self.logfile.write(line + '\n')
+ self.logfile.flush()
+
+
+ def _mungeData(self, data):
+ if self.lengthLimit and len(data) > self.lengthLimit:
+ data = data[:self.lengthLimit - 12] + '<... elided>'
+ return data
+
+
+ # IProtocol
+ def connectionMade(self):
+ self._log('*')
+ return ProtocolWrapper.connectionMade(self)
+
+
+ def dataReceived(self, data):
+ self._log('C %d: %r' % (self._number, self._mungeData(data)))
+ return ProtocolWrapper.dataReceived(self, data)
+
+
+ def connectionLost(self, reason):
+ self._log('C %d: %r' % (self._number, reason))
+ return ProtocolWrapper.connectionLost(self, reason)
+
+
+ # ITransport
+ def write(self, data):
+ self._log('S %d: %r' % (self._number, self._mungeData(data)))
+ return ProtocolWrapper.write(self, data)
+
+
+ def writeSequence(self, iovec):
+ self._log('SV %d: %r' % (self._number, [self._mungeData(d) for d in iovec]))
+ return ProtocolWrapper.writeSequence(self, iovec)
+
+
+ def loseConnection(self):
+ self._log('S %d: *' % (self._number,))
+ return ProtocolWrapper.loseConnection(self)
+
+
+
+class TrafficLoggingFactory(WrappingFactory):
+ protocol = TrafficLoggingProtocol
+
+ _counter = 0
+
+ def __init__(self, wrappedFactory, logfilePrefix, lengthLimit=None):
+ self.logfilePrefix = logfilePrefix
+ self.lengthLimit = lengthLimit
+ WrappingFactory.__init__(self, wrappedFactory)
+
+
+ def open(self, name):
+ return file(name, 'w')
+
+
+ def buildProtocol(self, addr):
+ self._counter += 1
+ logfile = self.open(self.logfilePrefix + '-' + str(self._counter))
+ return self.protocol(self, self.wrappedFactory.buildProtocol(addr),
+ logfile, self.lengthLimit, self._counter)
+
+
+ def resetCounter(self):
+ """
+ Reset the value of the counter used to identify connections.
+ """
+ self._counter = 0
+
+
+
+class TimeoutMixin:
+ """
+ Mixin for protocols which wish to timeout connections.
+
+ Protocols that mix this in have a single timeout, set using L{setTimeout}.
+ When the timeout is hit, L{timeoutConnection} is called, which, by
+ default, closes the connection.
+
+ @cvar timeOut: The number of seconds after which to timeout the connection.
+ """
+ timeOut = None
+
+ __timeoutCall = None
+
+ def callLater(self, period, func):
+ """
+ Wrapper around L{reactor.callLater} for test purpose.
+ """
+ from twisted.internet import reactor
+ return reactor.callLater(period, func)
+
+
+ def resetTimeout(self):
+ """
+ Reset the timeout count down.
+
+ If the connection has already timed out, then do nothing. If the
+ timeout has been cancelled (probably using C{setTimeout(None)}), also
+ do nothing.
+
+ It's often a good idea to call this when the protocol has received
+ some meaningful input from the other end of the connection. "I've got
+ some data, they're still there, reset the timeout".
+ """
+ if self.__timeoutCall is not None and self.timeOut is not None:
+ self.__timeoutCall.reset(self.timeOut)
+
+ def setTimeout(self, period):
+ """
+ Change the timeout period
+
+ @type period: C{int} or C{NoneType}
+ @param period: The period, in seconds, to change the timeout to, or
+ C{None} to disable the timeout.
+ """
+ prev = self.timeOut
+ self.timeOut = period
+
+ if self.__timeoutCall is not None:
+ if period is None:
+ self.__timeoutCall.cancel()
+ self.__timeoutCall = None
+ else:
+ self.__timeoutCall.reset(period)
+ elif period is not None:
+ self.__timeoutCall = self.callLater(period, self.__timedOut)
+
+ return prev
+
+ def __timedOut(self):
+ self.__timeoutCall = None
+ self.timeoutConnection()
+
+ def timeoutConnection(self):
+ """
+ Called when the connection times out.
+
+ Override to define behavior other than dropping the connection.
+ """
+ self.transport.loseConnection()
diff --git a/twisted/protocols/portforward.py b/twisted/protocols/portforward.py
new file mode 100644
index 0000000..626d5aa
--- /dev/null
+++ b/twisted/protocols/portforward.py
@@ -0,0 +1,87 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A simple port forwarder.
+"""
+
+# Twisted imports
+from twisted.internet import protocol
+from twisted.python import log
+
+class Proxy(protocol.Protocol):
+ noisy = True
+
+ peer = None
+
+ def setPeer(self, peer):
+ self.peer = peer
+
+ def connectionLost(self, reason):
+ if self.peer is not None:
+ self.peer.transport.loseConnection()
+ self.peer = None
+ elif self.noisy:
+ log.msg("Unable to connect to peer: %s" % (reason,))
+
+ def dataReceived(self, data):
+ self.peer.transport.write(data)
+
+class ProxyClient(Proxy):
+ def connectionMade(self):
+ self.peer.setPeer(self)
+
+ # Wire this and the peer transport together to enable
+ # flow control (this stops connections from filling
+ # this proxy memory when one side produces data at a
+ # higher rate than the other can consume).
+ self.transport.registerProducer(self.peer.transport, True)
+ self.peer.transport.registerProducer(self.transport, True)
+
+ # We're connected, everybody can read to their hearts content.
+ self.peer.transport.resumeProducing()
+
+class ProxyClientFactory(protocol.ClientFactory):
+
+ protocol = ProxyClient
+
+ def setServer(self, server):
+ self.server = server
+
+ def buildProtocol(self, *args, **kw):
+ prot = protocol.ClientFactory.buildProtocol(self, *args, **kw)
+ prot.setPeer(self.server)
+ return prot
+
+ def clientConnectionFailed(self, connector, reason):
+ self.server.transport.loseConnection()
+
+
+class ProxyServer(Proxy):
+
+ clientProtocolFactory = ProxyClientFactory
+ reactor = None
+
+ def connectionMade(self):
+ # Don't read anything from the connecting client until we have
+ # somewhere to send it to.
+ self.transport.pauseProducing()
+
+ client = self.clientProtocolFactory()
+ client.setServer(self)
+
+ if self.reactor is None:
+ from twisted.internet import reactor
+ self.reactor = reactor
+ self.reactor.connectTCP(self.factory.host, self.factory.port, client)
+
+
+class ProxyFactory(protocol.Factory):
+ """Factory for port forwarder."""
+
+ protocol = ProxyServer
+
+ def __init__(self, host, port):
+ self.host = host
+ self.port = port
diff --git a/twisted/protocols/postfix.py b/twisted/protocols/postfix.py
new file mode 100644
index 0000000..7a2079d
--- /dev/null
+++ b/twisted/protocols/postfix.py
@@ -0,0 +1,112 @@
+# -*- test-case-name: twisted.test.test_postfix -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Postfix mail transport agent related protocols.
+"""
+
+import sys
+import UserDict
+import urllib
+
+from twisted.protocols import basic
+from twisted.protocols import policies
+from twisted.internet import protocol, defer
+from twisted.python import log
+
+# urllib's quote functions just happen to match
+# the postfix semantics.
+def quote(s):
+ return urllib.quote(s)
+
+def unquote(s):
+ return urllib.unquote(s)
+
+class PostfixTCPMapServer(basic.LineReceiver, policies.TimeoutMixin):
+ """Postfix mail transport agent TCP map protocol implementation.
+
+ Receive requests for data matching given key via lineReceived,
+ asks it's factory for the data with self.factory.get(key), and
+ returns the data to the requester. None means no entry found.
+
+ You can use postfix's postmap to test the map service::
+
+ /usr/sbin/postmap -q KEY tcp:localhost:4242
+
+ """
+
+ timeout = 600
+ delimiter = '\n'
+
+ def connectionMade(self):
+ self.setTimeout(self.timeout)
+
+ def sendCode(self, code, message=''):
+ "Send an SMTP-like code with a message."
+ self.sendLine('%3.3d %s' % (code, message or ''))
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+ try:
+ request, params = line.split(None, 1)
+ except ValueError:
+ request = line
+ params = None
+ try:
+ f = getattr(self, 'do_' + request)
+ except AttributeError:
+ self.sendCode(400, 'unknown command')
+ else:
+ try:
+ f(params)
+ except:
+ self.sendCode(400, 'Command %r failed: %s.' % (request, sys.exc_info()[1]))
+
+ def do_get(self, key):
+ if key is None:
+ self.sendCode(400, 'Command %r takes 1 parameters.' % 'get')
+ else:
+ d = defer.maybeDeferred(self.factory.get, key)
+ d.addCallbacks(self._cbGot, self._cbNot)
+ d.addErrback(log.err)
+
+ def _cbNot(self, fail):
+ self.sendCode(400, fail.getErrorMessage())
+
+ def _cbGot(self, value):
+ if value is None:
+ self.sendCode(500)
+ else:
+ self.sendCode(200, quote(value))
+
+ def do_put(self, keyAndValue):
+ if keyAndValue is None:
+ self.sendCode(400, 'Command %r takes 2 parameters.' % 'put')
+ else:
+ try:
+ key, value = keyAndValue.split(None, 1)
+ except ValueError:
+ self.sendCode(400, 'Command %r takes 2 parameters.' % 'put')
+ else:
+ self.sendCode(500, 'put is not implemented yet.')
+
+
+class PostfixTCPMapDictServerFactory(protocol.ServerFactory,
+ UserDict.UserDict):
+ """An in-memory dictionary factory for PostfixTCPMapServer."""
+
+ protocol = PostfixTCPMapServer
+
+class PostfixTCPMapDeferringDictServerFactory(protocol.ServerFactory):
+ """An in-memory dictionary factory for PostfixTCPMapServer."""
+
+ protocol = PostfixTCPMapServer
+
+ def __init__(self, data=None):
+ self.data = {}
+ if data is not None:
+ self.data.update(data)
+
+ def get(self, key):
+ return defer.succeed(self.data.get(key))
diff --git a/twisted/protocols/shoutcast.py b/twisted/protocols/shoutcast.py
new file mode 100644
index 0000000..317d5e8
--- /dev/null
+++ b/twisted/protocols/shoutcast.py
@@ -0,0 +1,111 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Chop up shoutcast stream into MP3s and metadata, if available.
+"""
+
+from twisted.web import http
+from twisted import copyright
+
+
+class ShoutcastClient(http.HTTPClient):
+ """
+ Shoutcast HTTP stream.
+
+ Modes can be 'length', 'meta' and 'mp3'.
+
+ See U{http://www.smackfu.com/stuff/programming/shoutcast.html}
+ for details on the protocol.
+ """
+
+ userAgent = "Twisted Shoutcast client " + copyright.version
+
+ def __init__(self, path="/"):
+ self.path = path
+ self.got_metadata = False
+ self.metaint = None
+ self.metamode = "mp3"
+ self.databuffer = ""
+
+ def connectionMade(self):
+ self.sendCommand("GET", self.path)
+ self.sendHeader("User-Agent", self.userAgent)
+ self.sendHeader("Icy-MetaData", "1")
+ self.endHeaders()
+
+ def lineReceived(self, line):
+ # fix shoutcast crappiness
+ if not self.firstLine and line:
+ if len(line.split(": ", 1)) == 1:
+ line = line.replace(":", ": ", 1)
+ http.HTTPClient.lineReceived(self, line)
+
+ def handleHeader(self, key, value):
+ if key.lower() == 'icy-metaint':
+ self.metaint = int(value)
+ self.got_metadata = True
+
+ def handleEndHeaders(self):
+ # Lets check if we got metadata, and set the
+ # appropriate handleResponsePart method.
+ if self.got_metadata:
+ # if we have metadata, then it has to be parsed out of the data stream
+ self.handleResponsePart = self.handleResponsePart_with_metadata
+ else:
+ # otherwise, all the data is MP3 data
+ self.handleResponsePart = self.gotMP3Data
+
+ def handleResponsePart_with_metadata(self, data):
+ self.databuffer += data
+ while self.databuffer:
+ stop = getattr(self, "handle_%s" % self.metamode)()
+ if stop:
+ return
+
+ def handle_length(self):
+ self.remaining = ord(self.databuffer[0]) * 16
+ self.databuffer = self.databuffer[1:]
+ self.metamode = "meta"
+
+ def handle_mp3(self):
+ if len(self.databuffer) > self.metaint:
+ self.gotMP3Data(self.databuffer[:self.metaint])
+ self.databuffer = self.databuffer[self.metaint:]
+ self.metamode = "length"
+ else:
+ return 1
+
+ def handle_meta(self):
+ if len(self.databuffer) >= self.remaining:
+ if self.remaining:
+ data = self.databuffer[:self.remaining]
+ self.gotMetaData(self.parseMetadata(data))
+ self.databuffer = self.databuffer[self.remaining:]
+ self.metamode = "mp3"
+ else:
+ return 1
+
+ def parseMetadata(self, data):
+ meta = []
+ for chunk in data.split(';'):
+ chunk = chunk.strip().replace("\x00", "")
+ if not chunk:
+ continue
+ key, value = chunk.split('=', 1)
+ if value.startswith("'") and value.endswith("'"):
+ value = value[1:-1]
+ meta.append((key, value))
+ return meta
+
+ def gotMetaData(self, metadata):
+ """Called with a list of (key, value) pairs of metadata,
+ if metadata is available on the server.
+
+ Will only be called on non-empty metadata.
+ """
+ raise NotImplementedError, "implement in subclass"
+
+ def gotMP3Data(self, data):
+ """Called with chunk of MP3 data."""
+ raise NotImplementedError, "implement in subclass"
diff --git a/twisted/protocols/sip.py b/twisted/protocols/sip.py
new file mode 100644
index 0000000..58f5e8b
--- /dev/null
+++ b/twisted/protocols/sip.py
@@ -0,0 +1,1334 @@
+# -*- test-case-name: twisted.test.test_sip -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Session Initialization Protocol.
+
+Documented in RFC 2543.
+[Superceded by 3261]
+
+
+This module contains a deprecated implementation of HTTP Digest authentication.
+See L{twisted.cred.credentials} and L{twisted.cred._digest} for its new home.
+"""
+
+# system imports
+import socket, time, sys, random, warnings
+from zope.interface import implements, Interface
+
+# twisted imports
+from twisted.python import log, util
+from twisted.python.deprecate import deprecated
+from twisted.python.versions import Version
+from twisted.python.hashlib import md5
+from twisted.internet import protocol, defer, reactor
+
+from twisted import cred
+import twisted.cred.error
+from twisted.cred.credentials import UsernameHashedPassword, UsernamePassword
+
+
+# sibling imports
+from twisted.protocols import basic
+
+PORT = 5060
+
+# SIP headers have short forms
+shortHeaders = {"call-id": "i",
+ "contact": "m",
+ "content-encoding": "e",
+ "content-length": "l",
+ "content-type": "c",
+ "from": "f",
+ "subject": "s",
+ "to": "t",
+ "via": "v",
+ }
+
+longHeaders = {}
+for k, v in shortHeaders.items():
+ longHeaders[v] = k
+del k, v
+
+statusCodes = {
+ 100: "Trying",
+ 180: "Ringing",
+ 181: "Call Is Being Forwarded",
+ 182: "Queued",
+ 183: "Session Progress",
+
+ 200: "OK",
+
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Moved Temporarily",
+ 303: "See Other",
+ 305: "Use Proxy",
+ 380: "Alternative Service",
+
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict", # Not in RFC3261
+ 410: "Gone",
+ 411: "Length Required", # Not in RFC3261
+ 413: "Request Entity Too Large",
+ 414: "Request-URI Too Large",
+ 415: "Unsupported Media Type",
+ 416: "Unsupported URI Scheme",
+ 420: "Bad Extension",
+ 421: "Extension Required",
+ 423: "Interval Too Brief",
+ 480: "Temporarily Unavailable",
+ 481: "Call/Transaction Does Not Exist",
+ 482: "Loop Detected",
+ 483: "Too Many Hops",
+ 484: "Address Incomplete",
+ 485: "Ambiguous",
+ 486: "Busy Here",
+ 487: "Request Terminated",
+ 488: "Not Acceptable Here",
+ 491: "Request Pending",
+ 493: "Undecipherable",
+
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway", # no donut
+ 503: "Service Unavailable",
+ 504: "Server Time-out",
+ 505: "SIP Version not supported",
+ 513: "Message Too Large",
+
+ 600: "Busy Everywhere",
+ 603: "Decline",
+ 604: "Does not exist anywhere",
+ 606: "Not Acceptable",
+}
+
+specialCases = {
+ 'cseq': 'CSeq',
+ 'call-id': 'Call-ID',
+ 'www-authenticate': 'WWW-Authenticate',
+}
+
+
+def dashCapitalize(s):
+ ''' Capitalize a string, making sure to treat - as a word seperator '''
+ return '-'.join([ x.capitalize() for x in s.split('-')])
+
+def unq(s):
+ if s[0] == s[-1] == '"':
+ return s[1:-1]
+ return s
+
+def DigestCalcHA1(
+ pszAlg,
+ pszUserName,
+ pszRealm,
+ pszPassword,
+ pszNonce,
+ pszCNonce,
+):
+ m = md5()
+ m.update(pszUserName)
+ m.update(":")
+ m.update(pszRealm)
+ m.update(":")
+ m.update(pszPassword)
+ HA1 = m.digest()
+ if pszAlg == "md5-sess":
+ m = md5()
+ m.update(HA1)
+ m.update(":")
+ m.update(pszNonce)
+ m.update(":")
+ m.update(pszCNonce)
+ HA1 = m.digest()
+ return HA1.encode('hex')
+
+
+DigestCalcHA1 = deprecated(Version("Twisted", 9, 0, 0))(DigestCalcHA1)
+
+def DigestCalcResponse(
+ HA1,
+ pszNonce,
+ pszNonceCount,
+ pszCNonce,
+ pszQop,
+ pszMethod,
+ pszDigestUri,
+ pszHEntity,
+):
+ m = md5()
+ m.update(pszMethod)
+ m.update(":")
+ m.update(pszDigestUri)
+ if pszQop == "auth-int":
+ m.update(":")
+ m.update(pszHEntity)
+ HA2 = m.digest().encode('hex')
+
+ m = md5()
+ m.update(HA1)
+ m.update(":")
+ m.update(pszNonce)
+ m.update(":")
+ if pszNonceCount and pszCNonce: # pszQop:
+ m.update(pszNonceCount)
+ m.update(":")
+ m.update(pszCNonce)
+ m.update(":")
+ m.update(pszQop)
+ m.update(":")
+ m.update(HA2)
+ hash = m.digest().encode('hex')
+ return hash
+
+
+DigestCalcResponse = deprecated(Version("Twisted", 9, 0, 0))(DigestCalcResponse)
+
+_absent = object()
+
+class Via(object):
+ """
+ A L{Via} is a SIP Via header, representing a segment of the path taken by
+ the request.
+
+ See RFC 3261, sections 8.1.1.7, 18.2.2, and 20.42.
+
+ @ivar transport: Network protocol used for this leg. (Probably either "TCP"
+ or "UDP".)
+ @type transport: C{str}
+ @ivar branch: Unique identifier for this request.
+ @type branch: C{str}
+ @ivar host: Hostname or IP for this leg.
+ @type host: C{str}
+ @ivar port: Port used for this leg.
+ @type port C{int}, or None.
+ @ivar rportRequested: Whether to request RFC 3581 client processing or not.
+ @type rportRequested: C{bool}
+ @ivar rportValue: Servers wishing to honor requests for RFC 3581 processing
+ should set this parameter to the source port the request was received
+ from.
+ @type rportValue: C{int}, or None.
+
+ @ivar ttl: Time-to-live for requests on multicast paths.
+ @type ttl: C{int}, or None.
+ @ivar maddr: The destination multicast address, if any.
+ @type maddr: C{str}, or None.
+ @ivar hidden: Obsolete in SIP 2.0.
+ @type hidden: C{bool}
+ @ivar otherParams: Any other parameters in the header.
+ @type otherParams: C{dict}
+ """
+
+ def __init__(self, host, port=PORT, transport="UDP", ttl=None,
+ hidden=False, received=None, rport=_absent, branch=None,
+ maddr=None, **kw):
+ """
+ Set parameters of this Via header. All arguments correspond to
+ attributes of the same name.
+
+ To maintain compatibility with old SIP
+ code, the 'rport' argument is used to determine the values of
+ C{rportRequested} and C{rportValue}. If None, C{rportRequested} is set
+ to True. (The deprecated method for doing this is to pass True.) If an
+ integer, C{rportValue} is set to the given value.
+
+ Any arguments not explicitly named here are collected into the
+ C{otherParams} dict.
+ """
+ self.transport = transport
+ self.host = host
+ self.port = port
+ self.ttl = ttl
+ self.hidden = hidden
+ self.received = received
+ if rport is True:
+ warnings.warn(
+ "rport=True is deprecated since Twisted 9.0.",
+ DeprecationWarning,
+ stacklevel=2)
+ self.rportValue = None
+ self.rportRequested = True
+ elif rport is None:
+ self.rportValue = None
+ self.rportRequested = True
+ elif rport is _absent:
+ self.rportValue = None
+ self.rportRequested = False
+ else:
+ self.rportValue = rport
+ self.rportRequested = False
+
+ self.branch = branch
+ self.maddr = maddr
+ self.otherParams = kw
+
+
+ def _getrport(self):
+ """
+ Returns the rport value expected by the old SIP code.
+ """
+ if self.rportRequested == True:
+ return True
+ elif self.rportValue is not None:
+ return self.rportValue
+ else:
+ return None
+
+
+ def _setrport(self, newRPort):
+ """
+ L{Base._fixupNAT} sets C{rport} directly, so this method sets
+ C{rportValue} based on that.
+
+ @param newRPort: The new rport value.
+ @type newRPort: C{int}
+ """
+ self.rportValue = newRPort
+ self.rportRequested = False
+
+
+ rport = property(_getrport, _setrport)
+
+ def toString(self):
+ """
+ Serialize this header for use in a request or response.
+ """
+ s = "SIP/2.0/%s %s:%s" % (self.transport, self.host, self.port)
+ if self.hidden:
+ s += ";hidden"
+ for n in "ttl", "branch", "maddr", "received":
+ value = getattr(self, n)
+ if value is not None:
+ s += ";%s=%s" % (n, value)
+ if self.rportRequested:
+ s += ";rport"
+ elif self.rportValue is not None:
+ s += ";rport=%s" % (self.rport,)
+
+ etc = self.otherParams.items()
+ etc.sort()
+ for k, v in etc:
+ if v is None:
+ s += ";" + k
+ else:
+ s += ";%s=%s" % (k, v)
+ return s
+
+
+def parseViaHeader(value):
+ """
+ Parse a Via header.
+
+ @return: The parsed version of this header.
+ @rtype: L{Via}
+ """
+ parts = value.split(";")
+ sent, params = parts[0], parts[1:]
+ protocolinfo, by = sent.split(" ", 1)
+ by = by.strip()
+ result = {}
+ pname, pversion, transport = protocolinfo.split("/")
+ if pname != "SIP" or pversion != "2.0":
+ raise ValueError, "wrong protocol or version: %r" % value
+ result["transport"] = transport
+ if ":" in by:
+ host, port = by.split(":")
+ result["port"] = int(port)
+ result["host"] = host
+ else:
+ result["host"] = by
+ for p in params:
+ # it's the comment-striping dance!
+ p = p.strip().split(" ", 1)
+ if len(p) == 1:
+ p, comment = p[0], ""
+ else:
+ p, comment = p
+ if p == "hidden":
+ result["hidden"] = True
+ continue
+ parts = p.split("=", 1)
+ if len(parts) == 1:
+ name, value = parts[0], None
+ else:
+ name, value = parts
+ if name in ("rport", "ttl"):
+ value = int(value)
+ result[name] = value
+ return Via(**result)
+
+
+class URL:
+ """A SIP URL."""
+
+ def __init__(self, host, username=None, password=None, port=None,
+ transport=None, usertype=None, method=None,
+ ttl=None, maddr=None, tag=None, other=None, headers=None):
+ self.username = username
+ self.host = host
+ self.password = password
+ self.port = port
+ self.transport = transport
+ self.usertype = usertype
+ self.method = method
+ self.tag = tag
+ self.ttl = ttl
+ self.maddr = maddr
+ if other == None:
+ self.other = []
+ else:
+ self.other = other
+ if headers == None:
+ self.headers = {}
+ else:
+ self.headers = headers
+
+ def toString(self):
+ l = []; w = l.append
+ w("sip:")
+ if self.username != None:
+ w(self.username)
+ if self.password != None:
+ w(":%s" % self.password)
+ w("@")
+ w(self.host)
+ if self.port != None:
+ w(":%d" % self.port)
+ if self.usertype != None:
+ w(";user=%s" % self.usertype)
+ for n in ("transport", "ttl", "maddr", "method", "tag"):
+ v = getattr(self, n)
+ if v != None:
+ w(";%s=%s" % (n, v))
+ for v in self.other:
+ w(";%s" % v)
+ if self.headers:
+ w("?")
+ w("&".join([("%s=%s" % (specialCases.get(h) or dashCapitalize(h), v)) for (h, v) in self.headers.items()]))
+ return "".join(l)
+
+ def __str__(self):
+ return self.toString()
+
+ def __repr__(self):
+ return '<URL %s:%s@%s:%r/%s>' % (self.username, self.password, self.host, self.port, self.transport)
+
+
+def parseURL(url, host=None, port=None):
+ """Return string into URL object.
+
+ URIs are of of form 'sip:user@example.com'.
+ """
+ d = {}
+ if not url.startswith("sip:"):
+ raise ValueError("unsupported scheme: " + url[:4])
+ parts = url[4:].split(";")
+ userdomain, params = parts[0], parts[1:]
+ udparts = userdomain.split("@", 1)
+ if len(udparts) == 2:
+ userpass, hostport = udparts
+ upparts = userpass.split(":", 1)
+ if len(upparts) == 1:
+ d["username"] = upparts[0]
+ else:
+ d["username"] = upparts[0]
+ d["password"] = upparts[1]
+ else:
+ hostport = udparts[0]
+ hpparts = hostport.split(":", 1)
+ if len(hpparts) == 1:
+ d["host"] = hpparts[0]
+ else:
+ d["host"] = hpparts[0]
+ d["port"] = int(hpparts[1])
+ if host != None:
+ d["host"] = host
+ if port != None:
+ d["port"] = port
+ for p in params:
+ if p == params[-1] and "?" in p:
+ d["headers"] = h = {}
+ p, headers = p.split("?", 1)
+ for header in headers.split("&"):
+ k, v = header.split("=")
+ h[k] = v
+ nv = p.split("=", 1)
+ if len(nv) == 1:
+ d.setdefault("other", []).append(p)
+ continue
+ name, value = nv
+ if name == "user":
+ d["usertype"] = value
+ elif name in ("transport", "ttl", "maddr", "method", "tag"):
+ if name == "ttl":
+ value = int(value)
+ d[name] = value
+ else:
+ d.setdefault("other", []).append(p)
+ return URL(**d)
+
+
+def cleanRequestURL(url):
+ """Clean a URL from a Request line."""
+ url.transport = None
+ url.maddr = None
+ url.ttl = None
+ url.headers = {}
+
+
+def parseAddress(address, host=None, port=None, clean=0):
+ """Return (name, uri, params) for From/To/Contact header.
+
+ @param clean: remove unnecessary info, usually for From and To headers.
+ """
+ address = address.strip()
+ # simple 'sip:foo' case
+ if address.startswith("sip:"):
+ return "", parseURL(address, host=host, port=port), {}
+ params = {}
+ name, url = address.split("<", 1)
+ name = name.strip()
+ if name.startswith('"'):
+ name = name[1:]
+ if name.endswith('"'):
+ name = name[:-1]
+ url, paramstring = url.split(">", 1)
+ url = parseURL(url, host=host, port=port)
+ paramstring = paramstring.strip()
+ if paramstring:
+ for l in paramstring.split(";"):
+ if not l:
+ continue
+ k, v = l.split("=")
+ params[k] = v
+ if clean:
+ # rfc 2543 6.21
+ url.ttl = None
+ url.headers = {}
+ url.transport = None
+ url.maddr = None
+ return name, url, params
+
+
+class SIPError(Exception):
+ def __init__(self, code, phrase=None):
+ if phrase is None:
+ phrase = statusCodes[code]
+ Exception.__init__(self, "SIP error (%d): %s" % (code, phrase))
+ self.code = code
+ self.phrase = phrase
+
+
+class RegistrationError(SIPError):
+ """Registration was not possible."""
+
+
+class Message:
+ """A SIP message."""
+
+ length = None
+
+ def __init__(self):
+ self.headers = util.OrderedDict() # map name to list of values
+ self.body = ""
+ self.finished = 0
+
+ def addHeader(self, name, value):
+ name = name.lower()
+ name = longHeaders.get(name, name)
+ if name == "content-length":
+ self.length = int(value)
+ self.headers.setdefault(name,[]).append(value)
+
+ def bodyDataReceived(self, data):
+ self.body += data
+
+ def creationFinished(self):
+ if (self.length != None) and (self.length != len(self.body)):
+ raise ValueError, "wrong body length"
+ self.finished = 1
+
+ def toString(self):
+ s = "%s\r\n" % self._getHeaderLine()
+ for n, vs in self.headers.items():
+ for v in vs:
+ s += "%s: %s\r\n" % (specialCases.get(n) or dashCapitalize(n), v)
+ s += "\r\n"
+ s += self.body
+ return s
+
+ def _getHeaderLine(self):
+ raise NotImplementedError
+
+
+class Request(Message):
+ """A Request for a URI"""
+
+
+ def __init__(self, method, uri, version="SIP/2.0"):
+ Message.__init__(self)
+ self.method = method
+ if isinstance(uri, URL):
+ self.uri = uri
+ else:
+ self.uri = parseURL(uri)
+ cleanRequestURL(self.uri)
+
+ def __repr__(self):
+ return "<SIP Request %d:%s %s>" % (id(self), self.method, self.uri.toString())
+
+ def _getHeaderLine(self):
+ return "%s %s SIP/2.0" % (self.method, self.uri.toString())
+
+
+class Response(Message):
+ """A Response to a URI Request"""
+
+ def __init__(self, code, phrase=None, version="SIP/2.0"):
+ Message.__init__(self)
+ self.code = code
+ if phrase == None:
+ phrase = statusCodes[code]
+ self.phrase = phrase
+
+ def __repr__(self):
+ return "<SIP Response %d:%s>" % (id(self), self.code)
+
+ def _getHeaderLine(self):
+ return "SIP/2.0 %s %s" % (self.code, self.phrase)
+
+
+class MessagesParser(basic.LineReceiver):
+ """A SIP messages parser.
+
+ Expects dataReceived, dataDone repeatedly,
+ in that order. Shouldn't be connected to actual transport.
+ """
+
+ version = "SIP/2.0"
+ acceptResponses = 1
+ acceptRequests = 1
+ state = "firstline" # or "headers", "body" or "invalid"
+
+ debug = 0
+
+ def __init__(self, messageReceivedCallback):
+ self.messageReceived = messageReceivedCallback
+ self.reset()
+
+ def reset(self, remainingData=""):
+ self.state = "firstline"
+ self.length = None # body length
+ self.bodyReceived = 0 # how much of the body we received
+ self.message = None
+ self.setLineMode(remainingData)
+
+ def invalidMessage(self):
+ self.state = "invalid"
+ self.setRawMode()
+
+ def dataDone(self):
+ # clear out any buffered data that may be hanging around
+ self.clearLineBuffer()
+ if self.state == "firstline":
+ return
+ if self.state != "body":
+ self.reset()
+ return
+ if self.length == None:
+ # no content-length header, so end of data signals message done
+ self.messageDone()
+ elif self.length < self.bodyReceived:
+ # aborted in the middle
+ self.reset()
+ else:
+ # we have enough data and message wasn't finished? something is wrong
+ raise RuntimeError, "this should never happen"
+
+ def dataReceived(self, data):
+ try:
+ basic.LineReceiver.dataReceived(self, data)
+ except:
+ log.err()
+ self.invalidMessage()
+
+ def handleFirstLine(self, line):
+ """Expected to create self.message."""
+ raise NotImplementedError
+
+ def lineLengthExceeded(self, line):
+ self.invalidMessage()
+
+ def lineReceived(self, line):
+ if self.state == "firstline":
+ while line.startswith("\n") or line.startswith("\r"):
+ line = line[1:]
+ if not line:
+ return
+ try:
+ a, b, c = line.split(" ", 2)
+ except ValueError:
+ self.invalidMessage()
+ return
+ if a == "SIP/2.0" and self.acceptResponses:
+ # response
+ try:
+ code = int(b)
+ except ValueError:
+ self.invalidMessage()
+ return
+ self.message = Response(code, c)
+ elif c == "SIP/2.0" and self.acceptRequests:
+ self.message = Request(a, b)
+ else:
+ self.invalidMessage()
+ return
+ self.state = "headers"
+ return
+ else:
+ assert self.state == "headers"
+ if line:
+ # XXX support multi-line headers
+ try:
+ name, value = line.split(":", 1)
+ except ValueError:
+ self.invalidMessage()
+ return
+ self.message.addHeader(name, value.lstrip())
+ if name.lower() == "content-length":
+ try:
+ self.length = int(value.lstrip())
+ except ValueError:
+ self.invalidMessage()
+ return
+ else:
+ # CRLF, we now have message body until self.length bytes,
+ # or if no length was given, until there is no more data
+ # from the connection sending us data.
+ self.state = "body"
+ if self.length == 0:
+ self.messageDone()
+ return
+ self.setRawMode()
+
+ def messageDone(self, remainingData=""):
+ assert self.state == "body"
+ self.message.creationFinished()
+ self.messageReceived(self.message)
+ self.reset(remainingData)
+
+ def rawDataReceived(self, data):
+ assert self.state in ("body", "invalid")
+ if self.state == "invalid":
+ return
+ if self.length == None:
+ self.message.bodyDataReceived(data)
+ else:
+ dataLen = len(data)
+ expectedLen = self.length - self.bodyReceived
+ if dataLen > expectedLen:
+ self.message.bodyDataReceived(data[:expectedLen])
+ self.messageDone(data[expectedLen:])
+ return
+ else:
+ self.bodyReceived += dataLen
+ self.message.bodyDataReceived(data)
+ if self.bodyReceived == self.length:
+ self.messageDone()
+
+
+class Base(protocol.DatagramProtocol):
+ """Base class for SIP clients and servers."""
+
+ PORT = PORT
+ debug = False
+
+ def __init__(self):
+ self.messages = []
+ self.parser = MessagesParser(self.addMessage)
+
+ def addMessage(self, msg):
+ self.messages.append(msg)
+
+ def datagramReceived(self, data, addr):
+ self.parser.dataReceived(data)
+ self.parser.dataDone()
+ for m in self.messages:
+ self._fixupNAT(m, addr)
+ if self.debug:
+ log.msg("Received %r from %r" % (m.toString(), addr))
+ if isinstance(m, Request):
+ self.handle_request(m, addr)
+ else:
+ self.handle_response(m, addr)
+ self.messages[:] = []
+
+ def _fixupNAT(self, message, (srcHost, srcPort)):
+ # RFC 2543 6.40.2,
+ senderVia = parseViaHeader(message.headers["via"][0])
+ if senderVia.host != srcHost:
+ senderVia.received = srcHost
+ if senderVia.port != srcPort:
+ senderVia.rport = srcPort
+ message.headers["via"][0] = senderVia.toString()
+ elif senderVia.rport == True:
+ senderVia.received = srcHost
+ senderVia.rport = srcPort
+ message.headers["via"][0] = senderVia.toString()
+
+ def deliverResponse(self, responseMessage):
+ """Deliver response.
+
+ Destination is based on topmost Via header."""
+ destVia = parseViaHeader(responseMessage.headers["via"][0])
+ # XXX we don't do multicast yet
+ host = destVia.received or destVia.host
+ port = destVia.rport or destVia.port or self.PORT
+ destAddr = URL(host=host, port=port)
+ self.sendMessage(destAddr, responseMessage)
+
+ def responseFromRequest(self, code, request):
+ """Create a response to a request message."""
+ response = Response(code)
+ for name in ("via", "to", "from", "call-id", "cseq"):
+ response.headers[name] = request.headers.get(name, [])[:]
+
+ return response
+
+ def sendMessage(self, destURL, message):
+ """Send a message.
+
+ @param destURL: C{URL}. This should be a *physical* URL, not a logical one.
+ @param message: The message to send.
+ """
+ if destURL.transport not in ("udp", None):
+ raise RuntimeError, "only UDP currently supported"
+ if self.debug:
+ log.msg("Sending %r to %r" % (message.toString(), destURL))
+ self.transport.write(message.toString(), (destURL.host, destURL.port or self.PORT))
+
+ def handle_request(self, message, addr):
+ """Override to define behavior for requests received
+
+ @type message: C{Message}
+ @type addr: C{tuple}
+ """
+ raise NotImplementedError
+
+ def handle_response(self, message, addr):
+ """Override to define behavior for responses received.
+
+ @type message: C{Message}
+ @type addr: C{tuple}
+ """
+ raise NotImplementedError
+
+
+class IContact(Interface):
+ """A user of a registrar or proxy"""
+
+
+class Registration:
+ def __init__(self, secondsToExpiry, contactURL):
+ self.secondsToExpiry = secondsToExpiry
+ self.contactURL = contactURL
+
+class IRegistry(Interface):
+ """Allows registration of logical->physical URL mapping."""
+
+ def registerAddress(domainURL, logicalURL, physicalURL):
+ """Register the physical address of a logical URL.
+
+ @return: Deferred of C{Registration} or failure with RegistrationError.
+ """
+
+ def unregisterAddress(domainURL, logicalURL, physicalURL):
+ """Unregister the physical address of a logical URL.
+
+ @return: Deferred of C{Registration} or failure with RegistrationError.
+ """
+
+ def getRegistrationInfo(logicalURL):
+ """Get registration info for logical URL.
+
+ @return: Deferred of C{Registration} object or failure of LookupError.
+ """
+
+
+class ILocator(Interface):
+ """Allow looking up physical address for logical URL."""
+
+ def getAddress(logicalURL):
+ """Return physical URL of server for logical URL of user.
+
+ @param logicalURL: a logical C{URL}.
+ @return: Deferred which becomes URL or fails with LookupError.
+ """
+
+
+class Proxy(Base):
+ """SIP proxy."""
+
+ PORT = PORT
+
+ locator = None # object implementing ILocator
+
+ def __init__(self, host=None, port=PORT):
+ """Create new instance.
+
+ @param host: our hostname/IP as set in Via headers.
+ @param port: our port as set in Via headers.
+ """
+ self.host = host or socket.getfqdn()
+ self.port = port
+ Base.__init__(self)
+
+ def getVia(self):
+ """Return value of Via header for this proxy."""
+ return Via(host=self.host, port=self.port)
+
+ def handle_request(self, message, addr):
+ # send immediate 100/trying message before processing
+ #self.deliverResponse(self.responseFromRequest(100, message))
+ f = getattr(self, "handle_%s_request" % message.method, None)
+ if f is None:
+ f = self.handle_request_default
+ try:
+ d = f(message, addr)
+ except SIPError, e:
+ self.deliverResponse(self.responseFromRequest(e.code, message))
+ except:
+ log.err()
+ self.deliverResponse(self.responseFromRequest(500, message))
+ else:
+ if d is not None:
+ d.addErrback(lambda e:
+ self.deliverResponse(self.responseFromRequest(e.code, message))
+ )
+
+ def handle_request_default(self, message, (srcHost, srcPort)):
+ """Default request handler.
+
+ Default behaviour for OPTIONS and unknown methods for proxies
+ is to forward message on to the client.
+
+ Since at the moment we are stateless proxy, thats basically
+ everything.
+ """
+ def _mungContactHeader(uri, message):
+ message.headers['contact'][0] = uri.toString()
+ return self.sendMessage(uri, message)
+
+ viaHeader = self.getVia()
+ if viaHeader.toString() in message.headers["via"]:
+ # must be a loop, so drop message
+ log.msg("Dropping looped message.")
+ return
+
+ message.headers["via"].insert(0, viaHeader.toString())
+ name, uri, tags = parseAddress(message.headers["to"][0], clean=1)
+
+ # this is broken and needs refactoring to use cred
+ d = self.locator.getAddress(uri)
+ d.addCallback(self.sendMessage, message)
+ d.addErrback(self._cantForwardRequest, message)
+
+ def _cantForwardRequest(self, error, message):
+ error.trap(LookupError)
+ del message.headers["via"][0] # this'll be us
+ self.deliverResponse(self.responseFromRequest(404, message))
+
+ def deliverResponse(self, responseMessage):
+ """Deliver response.
+
+ Destination is based on topmost Via header."""
+ destVia = parseViaHeader(responseMessage.headers["via"][0])
+ # XXX we don't do multicast yet
+ host = destVia.received or destVia.host
+ port = destVia.rport or destVia.port or self.PORT
+
+ destAddr = URL(host=host, port=port)
+ self.sendMessage(destAddr, responseMessage)
+
+ def responseFromRequest(self, code, request):
+ """Create a response to a request message."""
+ response = Response(code)
+ for name in ("via", "to", "from", "call-id", "cseq"):
+ response.headers[name] = request.headers.get(name, [])[:]
+ return response
+
+ def handle_response(self, message, addr):
+ """Default response handler."""
+ v = parseViaHeader(message.headers["via"][0])
+ if (v.host, v.port) != (self.host, self.port):
+ # we got a message not intended for us?
+ # XXX note this check breaks if we have multiple external IPs
+ # yay for suck protocols
+ log.msg("Dropping incorrectly addressed message")
+ return
+ del message.headers["via"][0]
+ if not message.headers["via"]:
+ # this message is addressed to us
+ self.gotResponse(message, addr)
+ return
+ self.deliverResponse(message)
+
+ def gotResponse(self, message, addr):
+ """Called with responses that are addressed at this server."""
+ pass
+
+class IAuthorizer(Interface):
+ def getChallenge(peer):
+ """Generate a challenge the client may respond to.
+
+ @type peer: C{tuple}
+ @param peer: The client's address
+
+ @rtype: C{str}
+ @return: The challenge string
+ """
+
+ def decode(response):
+ """Create a credentials object from the given response.
+
+ @type response: C{str}
+ """
+
+class BasicAuthorizer:
+ """Authorizer for insecure Basic (base64-encoded plaintext) authentication.
+
+ This form of authentication is broken and insecure. Do not use it.
+ """
+
+ implements(IAuthorizer)
+
+ def __init__(self):
+ """
+ This method exists solely to issue a deprecation warning.
+ """
+ warnings.warn(
+ "twisted.protocols.sip.BasicAuthorizer was deprecated "
+ "in Twisted 9.0.0",
+ category=DeprecationWarning,
+ stacklevel=2)
+
+
+ def getChallenge(self, peer):
+ return None
+
+ def decode(self, response):
+ # At least one SIP client improperly pads its Base64 encoded messages
+ for i in range(3):
+ try:
+ creds = (response + ('=' * i)).decode('base64')
+ except:
+ pass
+ else:
+ break
+ else:
+ # Totally bogus
+ raise SIPError(400)
+ p = creds.split(':', 1)
+ if len(p) == 2:
+ return UsernamePassword(*p)
+ raise SIPError(400)
+
+
+
+class DigestedCredentials(UsernameHashedPassword):
+ """Yet Another Simple Digest-MD5 authentication scheme"""
+
+ def __init__(self, username, fields, challenges):
+ warnings.warn(
+ "twisted.protocols.sip.DigestedCredentials was deprecated "
+ "in Twisted 9.0.0",
+ category=DeprecationWarning,
+ stacklevel=2)
+ self.username = username
+ self.fields = fields
+ self.challenges = challenges
+
+ def checkPassword(self, password):
+ method = 'REGISTER'
+ response = self.fields.get('response')
+ uri = self.fields.get('uri')
+ nonce = self.fields.get('nonce')
+ cnonce = self.fields.get('cnonce')
+ nc = self.fields.get('nc')
+ algo = self.fields.get('algorithm', 'MD5')
+ qop = self.fields.get('qop-options', 'auth')
+ opaque = self.fields.get('opaque')
+
+ if opaque not in self.challenges:
+ return False
+ del self.challenges[opaque]
+
+ user, domain = self.username.split('@', 1)
+ if uri is None:
+ uri = 'sip:' + domain
+
+ expected = DigestCalcResponse(
+ DigestCalcHA1(algo, user, domain, password, nonce, cnonce),
+ nonce, nc, cnonce, qop, method, uri, None,
+ )
+
+ return expected == response
+
+class DigestAuthorizer:
+ CHALLENGE_LIFETIME = 15
+
+ implements(IAuthorizer)
+
+ def __init__(self):
+ warnings.warn(
+ "twisted.protocols.sip.DigestAuthorizer was deprecated "
+ "in Twisted 9.0.0",
+ category=DeprecationWarning,
+ stacklevel=2)
+
+ self.outstanding = {}
+
+
+
+ def generateNonce(self):
+ c = tuple([random.randrange(sys.maxint) for _ in range(3)])
+ c = '%d%d%d' % c
+ return c
+
+ def generateOpaque(self):
+ return str(random.randrange(sys.maxint))
+
+ def getChallenge(self, peer):
+ c = self.generateNonce()
+ o = self.generateOpaque()
+ self.outstanding[o] = c
+ return ','.join((
+ 'nonce="%s"' % c,
+ 'opaque="%s"' % o,
+ 'qop-options="auth"',
+ 'algorithm="MD5"',
+ ))
+
+ def decode(self, response):
+ response = ' '.join(response.splitlines())
+ parts = response.split(',')
+ auth = dict([(k.strip(), unq(v.strip())) for (k, v) in [p.split('=', 1) for p in parts]])
+ try:
+ username = auth['username']
+ except KeyError:
+ raise SIPError(401)
+ try:
+ return DigestedCredentials(username, auth, self.outstanding)
+ except:
+ raise SIPError(400)
+
+
+class RegisterProxy(Proxy):
+ """A proxy that allows registration for a specific domain.
+
+ Unregistered users won't be handled.
+ """
+
+ portal = None
+
+ registry = None # should implement IRegistry
+
+ authorizers = {}
+
+ def __init__(self, *args, **kw):
+ Proxy.__init__(self, *args, **kw)
+ self.liveChallenges = {}
+ if "digest" not in self.authorizers:
+ self.authorizers["digest"] = DigestAuthorizer()
+
+ def handle_ACK_request(self, message, (host, port)):
+ # XXX
+ # ACKs are a client's way of indicating they got the last message
+ # Responding to them is not a good idea.
+ # However, we should keep track of terminal messages and re-transmit
+ # if no ACK is received.
+ pass
+
+ def handle_REGISTER_request(self, message, (host, port)):
+ """Handle a registration request.
+
+ Currently registration is not proxied.
+ """
+ if self.portal is None:
+ # There is no portal. Let anyone in.
+ self.register(message, host, port)
+ else:
+ # There is a portal. Check for credentials.
+ if not message.headers.has_key("authorization"):
+ return self.unauthorized(message, host, port)
+ else:
+ return self.login(message, host, port)
+
+ def unauthorized(self, message, host, port):
+ m = self.responseFromRequest(401, message)
+ for (scheme, auth) in self.authorizers.iteritems():
+ chal = auth.getChallenge((host, port))
+ if chal is None:
+ value = '%s realm="%s"' % (scheme.title(), self.host)
+ else:
+ value = '%s %s,realm="%s"' % (scheme.title(), chal, self.host)
+ m.headers.setdefault('www-authenticate', []).append(value)
+ self.deliverResponse(m)
+
+
+ def login(self, message, host, port):
+ parts = message.headers['authorization'][0].split(None, 1)
+ a = self.authorizers.get(parts[0].lower())
+ if a:
+ try:
+ c = a.decode(parts[1])
+ except SIPError:
+ raise
+ except:
+ log.err()
+ self.deliverResponse(self.responseFromRequest(500, message))
+ else:
+ c.username += '@' + self.host
+ self.portal.login(c, None, IContact
+ ).addCallback(self._cbLogin, message, host, port
+ ).addErrback(self._ebLogin, message, host, port
+ ).addErrback(log.err
+ )
+ else:
+ self.deliverResponse(self.responseFromRequest(501, message))
+
+ def _cbLogin(self, (i, a, l), message, host, port):
+ # It's stateless, matey. What a joke.
+ self.register(message, host, port)
+
+ def _ebLogin(self, failure, message, host, port):
+ failure.trap(cred.error.UnauthorizedLogin)
+ self.unauthorized(message, host, port)
+
+ def register(self, message, host, port):
+ """Allow all users to register"""
+ name, toURL, params = parseAddress(message.headers["to"][0], clean=1)
+ contact = None
+ if message.headers.has_key("contact"):
+ contact = message.headers["contact"][0]
+
+ if message.headers.get("expires", [None])[0] == "0":
+ self.unregister(message, toURL, contact)
+ else:
+ # XXX Check expires on appropriate URL, and pass it to registry
+ # instead of having registry hardcode it.
+ if contact is not None:
+ name, contactURL, params = parseAddress(contact, host=host, port=port)
+ d = self.registry.registerAddress(message.uri, toURL, contactURL)
+ else:
+ d = self.registry.getRegistrationInfo(toURL)
+ d.addCallbacks(self._cbRegister, self._ebRegister,
+ callbackArgs=(message,),
+ errbackArgs=(message,)
+ )
+
+ def _cbRegister(self, registration, message):
+ response = self.responseFromRequest(200, message)
+ if registration.contactURL != None:
+ response.addHeader("contact", registration.contactURL.toString())
+ response.addHeader("expires", "%d" % registration.secondsToExpiry)
+ response.addHeader("content-length", "0")
+ self.deliverResponse(response)
+
+ def _ebRegister(self, error, message):
+ error.trap(RegistrationError, LookupError)
+ # XXX return error message, and alter tests to deal with
+ # this, currently tests assume no message sent on failure
+
+ def unregister(self, message, toURL, contact):
+ try:
+ expires = int(message.headers["expires"][0])
+ except ValueError:
+ self.deliverResponse(self.responseFromRequest(400, message))
+ else:
+ if expires == 0:
+ if contact == "*":
+ contactURL = "*"
+ else:
+ name, contactURL, params = parseAddress(contact)
+ d = self.registry.unregisterAddress(message.uri, toURL, contactURL)
+ d.addCallback(self._cbUnregister, message
+ ).addErrback(self._ebUnregister, message
+ )
+
+ def _cbUnregister(self, registration, message):
+ msg = self.responseFromRequest(200, message)
+ msg.headers.setdefault('contact', []).append(registration.contactURL.toString())
+ msg.addHeader("expires", "0")
+ self.deliverResponse(msg)
+
+ def _ebUnregister(self, registration, message):
+ pass
+
+
+class InMemoryRegistry:
+ """A simplistic registry for a specific domain."""
+
+ implements(IRegistry, ILocator)
+
+ def __init__(self, domain):
+ self.domain = domain # the domain we handle registration for
+ self.users = {} # map username to (IDelayedCall for expiry, address URI)
+
+ def getAddress(self, userURI):
+ if userURI.host != self.domain:
+ return defer.fail(LookupError("unknown domain"))
+ if self.users.has_key(userURI.username):
+ dc, url = self.users[userURI.username]
+ return defer.succeed(url)
+ else:
+ return defer.fail(LookupError("no such user"))
+
+ def getRegistrationInfo(self, userURI):
+ if userURI.host != self.domain:
+ return defer.fail(LookupError("unknown domain"))
+ if self.users.has_key(userURI.username):
+ dc, url = self.users[userURI.username]
+ return defer.succeed(Registration(int(dc.getTime() - time.time()), url))
+ else:
+ return defer.fail(LookupError("no such user"))
+
+ def _expireRegistration(self, username):
+ try:
+ dc, url = self.users[username]
+ except KeyError:
+ return defer.fail(LookupError("no such user"))
+ else:
+ dc.cancel()
+ del self.users[username]
+ return defer.succeed(Registration(0, url))
+
+ def registerAddress(self, domainURL, logicalURL, physicalURL):
+ if domainURL.host != self.domain:
+ log.msg("Registration for domain we don't handle.")
+ return defer.fail(RegistrationError(404))
+ if logicalURL.host != self.domain:
+ log.msg("Registration for domain we don't handle.")
+ return defer.fail(RegistrationError(404))
+ if self.users.has_key(logicalURL.username):
+ dc, old = self.users[logicalURL.username]
+ dc.reset(3600)
+ else:
+ dc = reactor.callLater(3600, self._expireRegistration, logicalURL.username)
+ log.msg("Registered %s at %s" % (logicalURL.toString(), physicalURL.toString()))
+ self.users[logicalURL.username] = (dc, physicalURL)
+ return defer.succeed(Registration(int(dc.getTime() - time.time()), physicalURL))
+
+ def unregisterAddress(self, domainURL, logicalURL, physicalURL):
+ return self._expireRegistration(logicalURL.username)
diff --git a/twisted/protocols/socks.py b/twisted/protocols/socks.py
new file mode 100644
index 0000000..445b9f3
--- /dev/null
+++ b/twisted/protocols/socks.py
@@ -0,0 +1,240 @@
+# -*- test-case-name: twisted.test.test_socks -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation of the SOCKSv4 protocol.
+"""
+
+# python imports
+import struct
+import string
+import socket
+import time
+
+# twisted imports
+from twisted.internet import reactor, protocol, defer
+from twisted.python import log
+
+
+class SOCKSv4Outgoing(protocol.Protocol):
+
+ def __init__(self,socks):
+ self.socks=socks
+
+ def connectionMade(self):
+ peer = self.transport.getPeer()
+ self.socks.makeReply(90, 0, port=peer.port, ip=peer.host)
+ self.socks.otherConn=self
+
+ def connectionLost(self, reason):
+ self.socks.transport.loseConnection()
+
+ def dataReceived(self,data):
+ self.socks.write(data)
+
+ def write(self,data):
+ self.socks.log(self,data)
+ self.transport.write(data)
+
+
+
+class SOCKSv4Incoming(protocol.Protocol):
+
+ def __init__(self,socks):
+ self.socks=socks
+ self.socks.otherConn=self
+
+ def connectionLost(self, reason):
+ self.socks.transport.loseConnection()
+
+ def dataReceived(self,data):
+ self.socks.write(data)
+
+ def write(self,data):
+ self.socks.log(self,data)
+ self.transport.write(data)
+
+
+class SOCKSv4(protocol.Protocol):
+ """
+ An implementation of the SOCKSv4 protocol.
+
+ @type logging: C{str} or C{None}
+ @ivar logging: If not C{None}, the name of the logfile to which connection
+ information will be written.
+
+ @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
+ @ivar reactor: The reactor used to create connections.
+
+ @type buf: C{str}
+ @ivar buf: Part of a SOCKSv4 connection request.
+
+ @type otherConn: C{SOCKSv4Incoming}, C{SOCKSv4Outgoing} or C{None}
+ @ivar otherConn: Until the connection has been established, C{otherConn} is
+ C{None}. After that, it is the proxy-to-destination protocol instance
+ along which the client's connection is being forwarded.
+ """
+ def __init__(self, logging=None, reactor=reactor):
+ self.logging = logging
+ self.reactor = reactor
+
+ def connectionMade(self):
+ self.buf = ""
+ self.otherConn = None
+
+ def dataReceived(self, data):
+ """
+ Called whenever data is received.
+
+ @type data: C{str}
+ @param data: Part or all of a SOCKSv4 packet.
+ """
+ if self.otherConn:
+ self.otherConn.write(data)
+ return
+ self.buf = self.buf + data
+ completeBuffer = self.buf
+ if "\000" in self.buf[8:]:
+ head, self.buf = self.buf[:8], self.buf[8:]
+ version, code, port = struct.unpack("!BBH", head[:4])
+ user, self.buf = self.buf.split("\000", 1)
+ if head[4:7] == "\000\000\000" and head[7] != "\000":
+ # An IP address of the form 0.0.0.X, where X is non-zero,
+ # signifies that this is a SOCKSv4a packet.
+ # If the complete packet hasn't been received, restore the
+ # buffer and wait for it.
+ if "\000" not in self.buf:
+ self.buf = completeBuffer
+ return
+ server, self.buf = self.buf.split("\000", 1)
+ d = self.reactor.resolve(server)
+ d.addCallback(self._dataReceived2, user,
+ version, code, port)
+ d.addErrback(lambda result, self = self: self.makeReply(91))
+ return
+ else:
+ server = socket.inet_ntoa(head[4:8])
+
+ self._dataReceived2(server, user, version, code, port)
+
+ def _dataReceived2(self, server, user, version, code, port):
+ """
+ The second half of the SOCKS connection setup. For a SOCKSv4 packet this
+ is after the server address has been extracted from the header. For a
+ SOCKSv4a packet this is after the host name has been resolved.
+
+ @type server: C{str}
+ @param server: The IP address of the destination, represented as a
+ dotted quad.
+
+ @type user: C{str}
+ @param user: The username associated with the connection.
+
+ @type version: C{int}
+ @param version: The SOCKS protocol version number.
+
+ @type code: C{int}
+ @param code: The comand code. 1 means establish a TCP/IP stream
+ connection, and 2 means establish a TCP/IP port binding.
+
+ @type port: C{int}
+ @param port: The port number associated with the connection.
+ """
+ assert version == 4, "Bad version code: %s" % version
+ if not self.authorize(code, server, port, user):
+ self.makeReply(91)
+ return
+ if code == 1: # CONNECT
+ d = self.connectClass(server, port, SOCKSv4Outgoing, self)
+ d.addErrback(lambda result, self = self: self.makeReply(91))
+ elif code == 2: # BIND
+ d = self.listenClass(0, SOCKSv4IncomingFactory, self, server)
+ d.addCallback(lambda (h, p),
+ self = self: self.makeReply(90, 0, p, h))
+ else:
+ raise RuntimeError, "Bad Connect Code: %s" % code
+ assert self.buf == "", "hmm, still stuff in buffer... %s" % repr(
+ self.buf)
+
+ def connectionLost(self, reason):
+ if self.otherConn:
+ self.otherConn.transport.loseConnection()
+
+ def authorize(self,code,server,port,user):
+ log.msg("code %s connection to %s:%s (user %s) authorized" % (code,server,port,user))
+ return 1
+
+ def connectClass(self, host, port, klass, *args):
+ return protocol.ClientCreator(reactor, klass, *args).connectTCP(host,port)
+
+ def listenClass(self, port, klass, *args):
+ serv = reactor.listenTCP(port, klass(*args))
+ return defer.succeed(serv.getHost()[1:])
+
+ def makeReply(self,reply,version=0,port=0,ip="0.0.0.0"):
+ self.transport.write(struct.pack("!BBH",version,reply,port)+socket.inet_aton(ip))
+ if reply!=90: self.transport.loseConnection()
+
+ def write(self,data):
+ self.log(self,data)
+ self.transport.write(data)
+
+ def log(self,proto,data):
+ if not self.logging: return
+ peer = self.transport.getPeer()
+ their_peer = self.otherConn.transport.getPeer()
+ f=open(self.logging,"a")
+ f.write("%s\t%s:%d %s %s:%d\n"%(time.ctime(),
+ peer.host,peer.port,
+ ((proto==self and '<') or '>'),
+ their_peer.host,their_peer.port))
+ while data:
+ p,data=data[:16],data[16:]
+ f.write(string.join(map(lambda x:'%02X'%ord(x),p),' ')+' ')
+ f.write((16-len(p))*3*' ')
+ for c in p:
+ if len(repr(c))>3: f.write('.')
+ else: f.write(c)
+ f.write('\n')
+ f.write('\n')
+ f.close()
+
+
+
+class SOCKSv4Factory(protocol.Factory):
+ """
+ A factory for a SOCKSv4 proxy.
+
+ Constructor accepts one argument, a log file name.
+ """
+
+ def __init__(self, log):
+ self.logging = log
+
+ def buildProtocol(self, addr):
+ return SOCKSv4(self.logging, reactor)
+
+
+
+class SOCKSv4IncomingFactory(protocol.Factory):
+ """
+ A utility class for building protocols for incoming connections.
+ """
+
+ def __init__(self, socks, ip):
+ self.socks = socks
+ self.ip = ip
+
+
+ def buildProtocol(self, addr):
+ if addr[0] == self.ip:
+ self.ip = ""
+ self.socks.makeReply(90, 0)
+ return SOCKSv4Incoming(self.socks)
+ elif self.ip == "":
+ return None
+ else:
+ self.socks.makeReply(91, 0)
+ self.ip = ""
+ return None
diff --git a/twisted/protocols/stateful.py b/twisted/protocols/stateful.py
new file mode 100644
index 0000000..7b82ae3
--- /dev/null
+++ b/twisted/protocols/stateful.py
@@ -0,0 +1,52 @@
+# -*- test-case-name: twisted.test.test_stateful -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import protocol
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+class StatefulProtocol(protocol.Protocol):
+ """A Protocol that stores state for you.
+
+ state is a pair (function, num_bytes). When num_bytes bytes of data arrives
+ from the network, function is called. It is expected to return the next
+ state or None to keep same state. Initial state is returned by
+ getInitialState (override it).
+ """
+ _sful_data = None, None, 0
+
+ def makeConnection(self, transport):
+ protocol.Protocol.makeConnection(self, transport)
+ self._sful_data = self.getInitialState(), StringIO(), 0
+
+ def getInitialState(self):
+ raise NotImplementedError
+
+ def dataReceived(self, data):
+ state, buffer, offset = self._sful_data
+ buffer.seek(0, 2)
+ buffer.write(data)
+ blen = buffer.tell() # how many bytes total is in the buffer
+ buffer.seek(offset)
+ while blen - offset >= state[1]:
+ d = buffer.read(state[1])
+ offset += state[1]
+ next = state[0](d)
+ if self.transport.disconnecting: # XXX: argh stupid hack borrowed right from LineReceiver
+ return # dataReceived won't be called again, so who cares about consistent state
+ if next:
+ state = next
+ if offset != 0:
+ b = buffer.read()
+ buffer.seek(0)
+ buffer.truncate()
+ buffer.write(b)
+ offset = 0
+ self._sful_data = state, buffer, offset
+
diff --git a/twisted/protocols/telnet.py b/twisted/protocols/telnet.py
new file mode 100644
index 0000000..ba1c826
--- /dev/null
+++ b/twisted/protocols/telnet.py
@@ -0,0 +1,325 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""TELNET implementation, with line-oriented command handling.
+"""
+
+import warnings
+warnings.warn(
+ "As of Twisted 2.1, twisted.protocols.telnet is deprecated. "
+ "See twisted.conch.telnet for the current, supported API.",
+ DeprecationWarning,
+ stacklevel=2)
+
+
+# System Imports
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+# Twisted Imports
+from twisted import copyright
+from twisted.internet import protocol
+
+# Some utility chars.
+ESC = chr(27) # ESC for doing fanciness
+BOLD_MODE_ON = ESC+"[1m" # turn bold on
+BOLD_MODE_OFF= ESC+"[m" # no char attributes
+
+
+# Characters gleaned from the various (and conflicting) RFCs. Not all of these are correct.
+
+NULL = chr(0) # No operation.
+LF = chr(10) # Moves the printer to the
+ # next print line, keeping the
+ # same horizontal position.
+CR = chr(13) # Moves the printer to the left
+ # margin of the current line.
+BEL = chr(7) # Produces an audible or
+ # visible signal (which does
+ # NOT move the print head).
+BS = chr(8) # Moves the print head one
+ # character position towards
+ # the left margin.
+HT = chr(9) # Moves the printer to the
+ # next horizontal tab stop.
+ # It remains unspecified how
+ # either party determines or
+ # establishes where such tab
+ # stops are located.
+VT = chr(11) # Moves the printer to the
+ # next vertical tab stop. It
+ # remains unspecified how
+ # either party determines or
+ # establishes where such tab
+ # stops are located.
+FF = chr(12) # Moves the printer to the top
+ # of the next page, keeping
+ # the same horizontal position.
+SE = chr(240) # End of subnegotiation parameters.
+NOP= chr(241) # No operation.
+DM = chr(242) # "Data Mark": The data stream portion
+ # of a Synch. This should always be
+ # accompanied by a TCP Urgent
+ # notification.
+BRK= chr(243) # NVT character Break.
+IP = chr(244) # The function Interrupt Process.
+AO = chr(245) # The function Abort Output
+AYT= chr(246) # The function Are You There.
+EC = chr(247) # The function Erase Character.
+EL = chr(248) # The function Erase Line
+GA = chr(249) # The Go Ahead signal.
+SB = chr(250) # Indicates that what follows is
+ # subnegotiation of the indicated
+ # option.
+WILL = chr(251) # Indicates the desire to begin
+ # performing, or confirmation that
+ # you are now performing, the
+ # indicated option.
+WONT = chr(252) # Indicates the refusal to perform,
+ # or continue performing, the
+ # indicated option.
+DO = chr(253) # Indicates the request that the
+ # other party perform, or
+ # confirmation that you are expecting
+ # the other party to perform, the
+ # indicated option.
+DONT = chr(254) # Indicates the demand that the
+ # other party stop performing,
+ # or confirmation that you are no
+ # longer expecting the other party
+ # to perform, the indicated option.
+IAC = chr(255) # Data Byte 255.
+
+# features
+
+ECHO = chr(1) # User-to-Server: Asks the server to send
+ # Echos of the transmitted data.
+
+ # Server-to User: States that the server is
+ # sending echos of the transmitted data.
+ # Sent only as a reply to ECHO or NO ECHO.
+
+SUPGA = chr(3) # Supress Go Ahead...? "Modern" telnet servers
+ # are supposed to do this.
+
+LINEMODE = chr(34) # I don't care that Jon Postel is dead.
+
+HIDE = chr(133) # The intention is that a server will send
+ # this signal to a user system which is
+ # echoing locally (to the user) when the user
+ # is about to type something secret (e.g. a
+ # password). In this case, the user system
+ # is to suppress local echoing or overprint
+ # the input (or something) until the server
+ # sends a NOECHO signal. In situations where
+ # the user system is not echoing locally,
+ # this signal must not be sent by the server.
+
+
+NOECHO= chr(131) # User-to-Server: Asks the server not to
+ # return Echos of the transmitted data.
+ #
+ # Server-to-User: States that the server is
+ # not sending echos of the transmitted data.
+ # Sent only as a reply to ECHO or NO ECHO,
+ # or to end the hide your input.
+
+
+
+iacBytes = {
+ DO: 'DO',
+ DONT: 'DONT',
+ WILL: 'WILL',
+ WONT: 'WONT',
+ IP: 'IP'
+ }
+
+def multireplace(st, dct):
+ for k, v in dct.items():
+ st = st.replace(k, v)
+ return st
+
+class Telnet(protocol.Protocol):
+ """I am a Protocol for handling Telnet connections. I have two
+ sets of special methods, telnet_* and iac_*.
+
+ telnet_* methods get called on every line sent to me. The method
+ to call is decided by the current mode. The initial mode is 'User';
+ this means that telnet_User is the first telnet_* method to be called.
+ All telnet_* methods should return a string which specifies the mode
+ to go into next; thus dictating which telnet_* method to call next.
+ For example, the default telnet_User method returns 'Password' to go
+ into Password mode, and the default telnet_Password method returns
+ 'Command' to go into Command mode.
+
+ The iac_* methods are less-used; they are called when an IAC telnet
+ byte is received. You can define iac_DO, iac_DONT, iac_WILL, iac_WONT,
+ and iac_IP methods to do what you want when one of these bytes is
+ received."""
+
+
+ gotIAC = 0
+ iacByte = None
+ lastLine = None
+ buffer = ''
+ echo = 0
+ delimiters = ['\r\n', '\r\000']
+ mode = "User"
+
+ def write(self, data):
+ """Send the given data over my transport."""
+ self.transport.write(data)
+
+
+ def connectionMade(self):
+ """I will write a welcomeMessage and loginPrompt to the client."""
+ self.write(self.welcomeMessage() + self.loginPrompt())
+
+ def welcomeMessage(self):
+ """Override me to return a string which will be sent to the client
+ before login."""
+ x = self.factory.__class__
+ return ("\r\n" + x.__module__ + '.' + x.__name__ +
+ '\r\nTwisted %s\r\n' % copyright.version
+ )
+
+ def loginPrompt(self):
+ """Override me to return a 'login:'-type prompt."""
+ return "username: "
+
+ def iacSBchunk(self, chunk):
+ pass
+
+ def iac_DO(self, feature):
+ pass
+
+ def iac_DONT(self, feature):
+ pass
+
+ def iac_WILL(self, feature):
+ pass
+
+ def iac_WONT(self, feature):
+ pass
+
+ def iac_IP(self, feature):
+ pass
+
+ def processLine(self, line):
+ """I call a method that looks like 'telnet_*' where '*' is filled
+ in by the current mode. telnet_* methods should return a string which
+ will become the new mode. If None is returned, the mode will not change.
+ """
+ mode = getattr(self, "telnet_"+self.mode)(line)
+ if mode is not None:
+ self.mode = mode
+
+ def telnet_User(self, user):
+ """I take a username, set it to the 'self.username' attribute,
+ print out a password prompt, and switch to 'Password' mode. If
+ you want to do something else when the username is received (ie,
+ create a new user if the user doesn't exist), override me."""
+ self.username = user
+ self.write(IAC+WILL+ECHO+"password: ")
+ return "Password"
+
+ def telnet_Password(self, paswd):
+ """I accept a password as an argument, and check it with the
+ checkUserAndPass method. If the login is successful, I call
+ loggedIn()."""
+ self.write(IAC+WONT+ECHO+"*****\r\n")
+ try:
+ checked = self.checkUserAndPass(self.username, paswd)
+ except:
+ return "Done"
+ if not checked:
+ return "Done"
+ self.loggedIn()
+ return "Command"
+
+ def telnet_Command(self, cmd):
+ """The default 'command processing' mode. You probably want to
+ override me."""
+ return "Command"
+
+ def processChunk(self, chunk):
+ """I take a chunk of data and delegate out to telnet_* methods
+ by way of processLine. If the current mode is 'Done', I'll close
+ the connection. """
+ self.buffer = self.buffer + chunk
+
+ #yech.
+ for delim in self.delimiters:
+ idx = self.buffer.find(delim)
+ if idx != -1:
+ break
+
+ while idx != -1:
+ buf, self.buffer = self.buffer[:idx], self.buffer[idx+2:]
+ self.processLine(buf)
+ if self.mode == 'Done':
+ self.transport.loseConnection()
+
+ for delim in self.delimiters:
+ idx = self.buffer.find(delim)
+ if idx != -1:
+ break
+
+ def dataReceived(self, data):
+ chunk = StringIO()
+ # silly little IAC state-machine
+ for char in data:
+ if self.gotIAC:
+ # working on an IAC request state
+ if self.iacByte:
+ # we're in SB mode, getting a chunk
+ if self.iacByte == SB:
+ if char == SE:
+ self.iacSBchunk(chunk.getvalue())
+ chunk = StringIO()
+ del self.iacByte
+ del self.gotIAC
+ else:
+ chunk.write(char)
+ else:
+ # got all I need to know state
+ try:
+ getattr(self, 'iac_%s' % iacBytes[self.iacByte])(char)
+ except KeyError:
+ pass
+ del self.iacByte
+ del self.gotIAC
+ else:
+ # got IAC, this is my W/W/D/D (or perhaps sb)
+ self.iacByte = char
+ elif char == IAC:
+ # Process what I've got so far before going into
+ # the IAC state; don't want to process characters
+ # in an inconsistent state with what they were
+ # received in.
+ c = chunk.getvalue()
+ if c:
+ why = self.processChunk(c)
+ if why:
+ return why
+ chunk = StringIO()
+ self.gotIAC = 1
+ else:
+ chunk.write(char)
+ # chunks are of a relatively indeterminate size.
+ c = chunk.getvalue()
+ if c:
+ why = self.processChunk(c)
+ if why:
+ return why
+
+ def loggedIn(self):
+ """Called after the user succesfully logged in.
+
+ Override in subclasses.
+ """
+ pass
diff --git a/twisted/protocols/test/__init__.py b/twisted/protocols/test/__init__.py
new file mode 100644
index 0000000..fd1e058
--- /dev/null
+++ b/twisted/protocols/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Unit tests for L{twisted.protocols}.
+"""
diff --git a/twisted/protocols/test/test_tls.py b/twisted/protocols/test/test_tls.py
new file mode 100644
index 0000000..06c3d03
--- /dev/null
+++ b/twisted/protocols/test/test_tls.py
@@ -0,0 +1,1474 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.protocols.tls}.
+"""
+
+from zope.interface.verify import verifyObject
+
+try:
+ from twisted.protocols.tls import TLSMemoryBIOProtocol, TLSMemoryBIOFactory
+ from twisted.protocols.tls import _PullToPush, _ProducerMembrane
+except ImportError:
+ # Skip the whole test module if it can't be imported.
+ skip = "pyOpenSSL 0.10 or newer required for twisted.protocol.tls"
+else:
+ # Otherwise, the pyOpenSSL dependency must be satisfied, so all these
+ # imports will work.
+ from OpenSSL.crypto import X509Type
+ from OpenSSL.SSL import TLSv1_METHOD, Error, Context, ConnectionType, WantReadError
+ from twisted.internet.ssl import ClientContextFactory, PrivateCertificate
+ from twisted.internet.ssl import DefaultOpenSSLContextFactory
+
+from twisted.python.filepath import FilePath
+from twisted.python.failure import Failure
+from twisted.python import log
+from twisted.internet.interfaces import ISystemHandle, ISSLTransport
+from twisted.internet.interfaces import IPushProducer
+from twisted.internet.error import ConnectionDone, ConnectionLost
+from twisted.internet.defer import Deferred, gatherResults
+from twisted.internet.protocol import Protocol, ClientFactory, ServerFactory
+from twisted.internet.task import TaskStopped
+from twisted.protocols.loopback import loopbackAsync, collapsingPumpPolicy
+from twisted.trial.unittest import TestCase
+from twisted.test.test_tcp import ConnectionLostNotifyingProtocol
+from twisted.test.test_ssl import certPath
+from twisted.test.proto_helpers import StringTransport
+
+
+class HandshakeCallbackContextFactory:
+ """
+ L{HandshakeCallbackContextFactory} is a factory for SSL contexts which
+ allows applications to get notification when the SSL handshake completes.
+
+ @ivar _finished: A L{Deferred} which will be called back when the handshake
+ is done.
+ """
+ # pyOpenSSL needs to expose this.
+ # https://bugs.launchpad.net/pyopenssl/+bug/372832
+ SSL_CB_HANDSHAKE_DONE = 0x20
+
+ def __init__(self):
+ self._finished = Deferred()
+
+
+ def factoryAndDeferred(cls):
+ """
+ Create a new L{HandshakeCallbackContextFactory} and return a two-tuple
+ of it and a L{Deferred} which will fire when a connection created with
+ it completes a TLS handshake.
+ """
+ contextFactory = cls()
+ return contextFactory, contextFactory._finished
+ factoryAndDeferred = classmethod(factoryAndDeferred)
+
+
+ def _info(self, connection, where, ret):
+ """
+ This is the "info callback" on the context. It will be called
+ periodically by pyOpenSSL with information about the state of a
+ connection. When it indicates the handshake is complete, it will fire
+ C{self._finished}.
+ """
+ if where & self.SSL_CB_HANDSHAKE_DONE:
+ self._finished.callback(None)
+
+
+ def getContext(self):
+ """
+ Create and return an SSL context configured to use L{self._info} as the
+ info callback.
+ """
+ context = Context(TLSv1_METHOD)
+ context.set_info_callback(self._info)
+ return context
+
+
+
+class AccumulatingProtocol(Protocol):
+ """
+ A protocol which collects the bytes it receives and closes its connection
+ after receiving a certain minimum of data.
+
+ @ivar howMany: The number of bytes of data to wait for before closing the
+ connection.
+
+ @ivar receiving: A C{list} of C{str} of the bytes received so far.
+ """
+ def __init__(self, howMany):
+ self.howMany = howMany
+
+
+ def connectionMade(self):
+ self.received = []
+
+
+ def dataReceived(self, bytes):
+ self.received.append(bytes)
+ if sum(map(len, self.received)) >= self.howMany:
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ if not reason.check(ConnectionDone):
+ log.err(reason)
+
+
+
+def buildTLSProtocol(server=False, transport=None):
+ """
+ Create a protocol hooked up to a TLS transport hooked up to a
+ StringTransport.
+ """
+ # We want to accumulate bytes without disconnecting, so set high limit:
+ clientProtocol = AccumulatingProtocol(999999999999)
+ clientFactory = ClientFactory()
+ clientFactory.protocol = lambda: clientProtocol
+
+ if server:
+ contextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ else:
+ contextFactory = ClientContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ contextFactory, not server, clientFactory)
+ sslProtocol = wrapperFactory.buildProtocol(None)
+
+ if transport is None:
+ transport = StringTransport()
+ sslProtocol.makeConnection(transport)
+ return clientProtocol, sslProtocol
+
+
+
+class TLSMemoryBIOFactoryTests(TestCase):
+ """
+ Ensure TLSMemoryBIOFactory logging acts correctly.
+ """
+
+ def test_quiet(self):
+ """
+ L{TLSMemoryBIOFactory.doStart} and L{TLSMemoryBIOFactory.doStop} do
+ not log any messages.
+ """
+ contextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+
+ logs = []
+ logger = logs.append
+ log.addObserver(logger)
+ self.addCleanup(log.removeObserver, logger)
+ wrappedFactory = ServerFactory()
+ # Disable logging on the wrapped factory:
+ wrappedFactory.doStart = lambda: None
+ wrappedFactory.doStop = lambda: None
+ factory = TLSMemoryBIOFactory(contextFactory, False, wrappedFactory)
+ factory.doStart()
+ factory.doStop()
+ self.assertEqual(logs, [])
+
+
+ def test_logPrefix(self):
+ """
+ L{TLSMemoryBIOFactory.logPrefix} amends the wrapped factory's log prefix
+ with a short string (C{"TLS"}) indicating the wrapping, rather than its
+ full class name.
+ """
+ contextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ factory = TLSMemoryBIOFactory(contextFactory, False, ServerFactory())
+ self.assertEqual("ServerFactory (TLS)", factory.logPrefix())
+
+
+ def test_logPrefixFallback(self):
+ """
+ If the wrapped factory does not provide L{ILoggingContext},
+ L{TLSMemoryBIOFactory.logPrefix} uses the wrapped factory's class name.
+ """
+ class NoFactory(object):
+ pass
+
+ contextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ factory = TLSMemoryBIOFactory(contextFactory, False, NoFactory())
+ self.assertEqual("NoFactory (TLS)", factory.logPrefix())
+
+
+
+class TLSMemoryBIOTests(TestCase):
+ """
+ Tests for the implementation of L{ISSLTransport} which runs over another
+ L{ITransport}.
+ """
+
+ def test_interfaces(self):
+ """
+ L{TLSMemoryBIOProtocol} instances provide L{ISSLTransport} and
+ L{ISystemHandle}.
+ """
+ proto = TLSMemoryBIOProtocol(None, None)
+ self.assertTrue(ISSLTransport.providedBy(proto))
+ self.assertTrue(ISystemHandle.providedBy(proto))
+
+
+ def test_getHandle(self):
+ """
+ L{TLSMemoryBIOProtocol.getHandle} returns the L{OpenSSL.SSL.Connection}
+ instance it uses to actually implement TLS.
+
+ This may seem odd. In fact, it is. The L{OpenSSL.SSL.Connection} is
+ not actually the "system handle" here, nor even an object the reactor
+ knows about directly. However, L{twisted.internet.ssl.Certificate}'s
+ C{peerFromTransport} and C{hostFromTransport} methods depend on being
+ able to get an L{OpenSSL.SSL.Connection} object in order to work
+ properly. Implementing L{ISystemHandle.getHandle} like this is the
+ easiest way for those APIs to be made to work. If they are changed,
+ then it may make sense to get rid of this implementation of
+ L{ISystemHandle} and return the underlying socket instead.
+ """
+ factory = ClientFactory()
+ contextFactory = ClientContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(contextFactory, True, factory)
+ proto = TLSMemoryBIOProtocol(wrapperFactory, Protocol())
+ transport = StringTransport()
+ proto.makeConnection(transport)
+ self.assertIsInstance(proto.getHandle(), ConnectionType)
+
+
+ def test_makeConnection(self):
+ """
+ When L{TLSMemoryBIOProtocol} is connected to a transport, it connects
+ the protocol it wraps to a transport.
+ """
+ clientProtocol = Protocol()
+ clientFactory = ClientFactory()
+ clientFactory.protocol = lambda: clientProtocol
+
+ contextFactory = ClientContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ contextFactory, True, clientFactory)
+ sslProtocol = wrapperFactory.buildProtocol(None)
+
+ transport = StringTransport()
+ sslProtocol.makeConnection(transport)
+
+ self.assertNotIdentical(clientProtocol.transport, None)
+ self.assertNotIdentical(clientProtocol.transport, transport)
+ self.assertIdentical(clientProtocol.transport, sslProtocol)
+
+
+ def handshakeProtocols(self):
+ """
+ Start handshake between TLS client and server.
+ """
+ clientFactory = ClientFactory()
+ clientFactory.protocol = Protocol
+
+ clientContextFactory, handshakeDeferred = (
+ HandshakeCallbackContextFactory.factoryAndDeferred())
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverFactory = ServerFactory()
+ serverFactory.protocol = Protocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
+ return (sslClientProtocol, sslServerProtocol, handshakeDeferred,
+ connectionDeferred)
+
+
+ def test_handshake(self):
+ """
+ The TLS handshake is performed when L{TLSMemoryBIOProtocol} is
+ connected to a transport.
+ """
+ tlsClient, tlsServer, handshakeDeferred, _ = self.handshakeProtocols()
+
+ # Only wait for the handshake to complete. Anything after that isn't
+ # important here.
+ return handshakeDeferred
+
+
+ def test_handshakeFailure(self):
+ """
+ L{TLSMemoryBIOProtocol} reports errors in the handshake process to the
+ application-level protocol object using its C{connectionLost} method
+ and disconnects the underlying transport.
+ """
+ clientConnectionLost = Deferred()
+ clientFactory = ClientFactory()
+ clientFactory.protocol = (
+ lambda: ConnectionLostNotifyingProtocol(
+ clientConnectionLost))
+
+ clientContextFactory = HandshakeCallbackContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverConnectionLost = Deferred()
+ serverFactory = ServerFactory()
+ serverFactory.protocol = (
+ lambda: ConnectionLostNotifyingProtocol(
+ serverConnectionLost))
+
+ # This context factory rejects any clients which do not present a
+ # certificate.
+ certificateData = FilePath(certPath).getContent()
+ certificate = PrivateCertificate.loadPEM(certificateData)
+ serverContextFactory = certificate.options(certificate)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
+
+ def cbConnectionLost(protocol):
+ # The connection should close on its own in response to the error
+ # induced by the client not supplying the required certificate.
+ # After that, check to make sure the protocol's connectionLost was
+ # called with the right thing.
+ protocol.lostConnectionReason.trap(Error)
+ clientConnectionLost.addCallback(cbConnectionLost)
+ serverConnectionLost.addCallback(cbConnectionLost)
+
+ # Additionally, the underlying transport should have been told to
+ # go away.
+ return gatherResults([
+ clientConnectionLost, serverConnectionLost,
+ connectionDeferred])
+
+
+ def test_getPeerCertificate(self):
+ """
+ L{TLSMemoryBIOProtocol.getPeerCertificate} returns the
+ L{OpenSSL.crypto.X509Type} instance representing the peer's
+ certificate.
+ """
+ # Set up a client and server so there's a certificate to grab.
+ clientFactory = ClientFactory()
+ clientFactory.protocol = Protocol
+
+ clientContextFactory, handshakeDeferred = (
+ HandshakeCallbackContextFactory.factoryAndDeferred())
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverFactory = ServerFactory()
+ serverFactory.protocol = Protocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ loopbackAsync(sslServerProtocol, sslClientProtocol)
+
+ # Wait for the handshake
+ def cbHandshook(ignored):
+ # Grab the server's certificate and check it out
+ cert = sslClientProtocol.getPeerCertificate()
+ self.assertIsInstance(cert, X509Type)
+ self.assertEqual(
+ cert.digest('md5'),
+ '9B:A4:AB:43:10:BE:82:AE:94:3E:6B:91:F2:F3:40:E8')
+ handshakeDeferred.addCallback(cbHandshook)
+ return handshakeDeferred
+
+
+ def test_writeAfterHandshake(self):
+ """
+ Bytes written to L{TLSMemoryBIOProtocol} before the handshake is
+ complete are received by the protocol on the other side of the
+ connection once the handshake succeeds.
+ """
+ bytes = "some bytes"
+
+ clientProtocol = Protocol()
+ clientFactory = ClientFactory()
+ clientFactory.protocol = lambda: clientProtocol
+
+ clientContextFactory, handshakeDeferred = (
+ HandshakeCallbackContextFactory.factoryAndDeferred())
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverProtocol = AccumulatingProtocol(len(bytes))
+ serverFactory = ServerFactory()
+ serverFactory.protocol = lambda: serverProtocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
+
+ # Wait for the handshake to finish before writing anything.
+ def cbHandshook(ignored):
+ clientProtocol.transport.write(bytes)
+
+ # The server will drop the connection once it gets the bytes.
+ return connectionDeferred
+ handshakeDeferred.addCallback(cbHandshook)
+
+ # Once the connection is lost, make sure the server received the
+ # expected bytes.
+ def cbDisconnected(ignored):
+ self.assertEqual("".join(serverProtocol.received), bytes)
+ handshakeDeferred.addCallback(cbDisconnected)
+
+ return handshakeDeferred
+
+
+ def writeBeforeHandshakeTest(self, sendingProtocol, bytes):
+ """
+ Run test where client sends data before handshake, given the sending
+ protocol and expected bytes.
+ """
+ clientFactory = ClientFactory()
+ clientFactory.protocol = sendingProtocol
+
+ clientContextFactory, handshakeDeferred = (
+ HandshakeCallbackContextFactory.factoryAndDeferred())
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverProtocol = AccumulatingProtocol(len(bytes))
+ serverFactory = ServerFactory()
+ serverFactory.protocol = lambda: serverProtocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
+
+ # Wait for the connection to end, then make sure the server received
+ # the bytes sent by the client.
+ def cbConnectionDone(ignored):
+ self.assertEqual("".join(serverProtocol.received), bytes)
+ connectionDeferred.addCallback(cbConnectionDone)
+ return connectionDeferred
+
+
+ def test_writeBeforeHandshake(self):
+ """
+ Bytes written to L{TLSMemoryBIOProtocol} before the handshake is
+ complete are received by the protocol on the other side of the
+ connection once the handshake succeeds.
+ """
+ bytes = "some bytes"
+
+ class SimpleSendingProtocol(Protocol):
+ def connectionMade(self):
+ self.transport.write(bytes)
+
+ return self.writeBeforeHandshakeTest(SimpleSendingProtocol, bytes)
+
+
+ def test_writeSequence(self):
+ """
+ Bytes written to L{TLSMemoryBIOProtocol} with C{writeSequence} are
+ received by the protocol on the other side of the connection.
+ """
+ bytes = "some bytes"
+ class SimpleSendingProtocol(Protocol):
+ def connectionMade(self):
+ self.transport.writeSequence(list(bytes))
+
+ return self.writeBeforeHandshakeTest(SimpleSendingProtocol, bytes)
+
+
+ def test_writeAfterLoseConnection(self):
+ """
+ Bytes written to L{TLSMemoryBIOProtocol} after C{loseConnection} is
+ called are not transmitted (unless there is a registered producer,
+ which will be tested elsewhere).
+ """
+ bytes = "some bytes"
+ class SimpleSendingProtocol(Protocol):
+ def connectionMade(self):
+ self.transport.write(bytes)
+ self.transport.loseConnection()
+ self.transport.write("hello")
+ self.transport.writeSequence(["world"])
+ return self.writeBeforeHandshakeTest(SimpleSendingProtocol, bytes)
+
+
+ def test_multipleWrites(self):
+ """
+ If multiple separate TLS messages are received in a single chunk from
+ the underlying transport, all of the application bytes from each
+ message are delivered to the application-level protocol.
+ """
+ bytes = [str(i) for i in range(10)]
+ class SimpleSendingProtocol(Protocol):
+ def connectionMade(self):
+ for b in bytes:
+ self.transport.write(b)
+
+ clientFactory = ClientFactory()
+ clientFactory.protocol = SimpleSendingProtocol
+
+ clientContextFactory = HandshakeCallbackContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverProtocol = AccumulatingProtocol(sum(map(len, bytes)))
+ serverFactory = ServerFactory()
+ serverFactory.protocol = lambda: serverProtocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol, collapsingPumpPolicy)
+
+ # Wait for the connection to end, then make sure the server received
+ # the bytes sent by the client.
+ def cbConnectionDone(ignored):
+ self.assertEqual("".join(serverProtocol.received), ''.join(bytes))
+ connectionDeferred.addCallback(cbConnectionDone)
+ return connectionDeferred
+
+
+ def test_hugeWrite(self):
+ """
+ If a very long string is passed to L{TLSMemoryBIOProtocol.write}, any
+ trailing part of it which cannot be send immediately is buffered and
+ sent later.
+ """
+ bytes = "some bytes"
+ factor = 8192
+ class SimpleSendingProtocol(Protocol):
+ def connectionMade(self):
+ self.transport.write(bytes * factor)
+
+ clientFactory = ClientFactory()
+ clientFactory.protocol = SimpleSendingProtocol
+
+ clientContextFactory = HandshakeCallbackContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverProtocol = AccumulatingProtocol(len(bytes) * factor)
+ serverFactory = ServerFactory()
+ serverFactory.protocol = lambda: serverProtocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ connectionDeferred = loopbackAsync(sslServerProtocol, sslClientProtocol)
+
+ # Wait for the connection to end, then make sure the server received
+ # the bytes sent by the client.
+ def cbConnectionDone(ignored):
+ self.assertEqual("".join(serverProtocol.received), bytes * factor)
+ connectionDeferred.addCallback(cbConnectionDone)
+ return connectionDeferred
+
+
+ def test_disorderlyShutdown(self):
+ """
+ If a L{TLSMemoryBIOProtocol} loses its connection unexpectedly, this is
+ reported to the application.
+ """
+ clientConnectionLost = Deferred()
+ clientFactory = ClientFactory()
+ clientFactory.protocol = (
+ lambda: ConnectionLostNotifyingProtocol(
+ clientConnectionLost))
+
+ clientContextFactory = HandshakeCallbackContextFactory()
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ # Client speaks first, so the server can be dumb.
+ serverProtocol = Protocol()
+
+ loopbackAsync(serverProtocol, sslClientProtocol)
+
+ # Now destroy the connection.
+ serverProtocol.transport.loseConnection()
+
+ # And when the connection completely dies, check the reason.
+ def cbDisconnected(clientProtocol):
+ clientProtocol.lostConnectionReason.trap(Error)
+ clientConnectionLost.addCallback(cbDisconnected)
+ return clientConnectionLost
+
+
+ def test_loseConnectionAfterHandshake(self):
+ """
+ L{TLSMemoryBIOProtocol.loseConnection} sends a TLS close alert and
+ shuts down the underlying connection cleanly on both sides, after
+ transmitting all buffered data.
+ """
+ class NotifyingProtocol(ConnectionLostNotifyingProtocol):
+ def __init__(self, onConnectionLost):
+ ConnectionLostNotifyingProtocol.__init__(self,
+ onConnectionLost)
+ self.data = []
+
+ def dataReceived(self, bytes):
+ self.data.append(bytes)
+
+ clientConnectionLost = Deferred()
+ clientFactory = ClientFactory()
+ clientProtocol = NotifyingProtocol(clientConnectionLost)
+ clientFactory.protocol = lambda: clientProtocol
+
+ clientContextFactory, handshakeDeferred = (
+ HandshakeCallbackContextFactory.factoryAndDeferred())
+ wrapperFactory = TLSMemoryBIOFactory(
+ clientContextFactory, True, clientFactory)
+ sslClientProtocol = wrapperFactory.buildProtocol(None)
+
+ serverConnectionLost = Deferred()
+ serverProtocol = NotifyingProtocol(serverConnectionLost)
+ serverFactory = ServerFactory()
+ serverFactory.protocol = lambda: serverProtocol
+
+ serverContextFactory = DefaultOpenSSLContextFactory(certPath, certPath)
+ wrapperFactory = TLSMemoryBIOFactory(
+ serverContextFactory, False, serverFactory)
+ sslServerProtocol = wrapperFactory.buildProtocol(None)
+
+ loopbackAsync(sslServerProtocol, sslClientProtocol)
+ chunkOfBytes = "123456890" * 100000
+
+ # Wait for the handshake before dropping the connection.
+ def cbHandshake(ignored):
+ # Write more than a single bio_read, to ensure client will still
+ # have some data it needs to write when it receives the TLS close
+ # alert, and that simply doing a single bio_read won't be
+ # sufficient. Thus we will verify that any amount of buffered data
+ # will be written out before the connection is closed, rather than
+ # just small amounts that can be returned in a single bio_read:
+ clientProtocol.transport.write(chunkOfBytes)
+ serverProtocol.transport.loseConnection()
+
+ # Now wait for the client and server to notice.
+ return gatherResults([clientConnectionLost, serverConnectionLost])
+ handshakeDeferred.addCallback(cbHandshake)
+
+ # Wait for the connection to end, then make sure the client and server
+ # weren't notified of a handshake failure that would cause the test to
+ # fail.
+ def cbConnectionDone((clientProtocol, serverProtocol)):
+ clientProtocol.lostConnectionReason.trap(ConnectionDone)
+ serverProtocol.lostConnectionReason.trap(ConnectionDone)
+
+ # The server should have received all bytes sent by the client:
+ self.assertEqual("".join(serverProtocol.data), chunkOfBytes)
+
+ # The server should have closed its underlying transport, in
+ # addition to whatever it did to shut down the TLS layer.
+ self.assertTrue(serverProtocol.transport.q.disconnect)
+
+ # The client should also have closed its underlying transport once
+ # it saw the server shut down the TLS layer, so as to avoid relying
+ # on the server to close the underlying connection.
+ self.assertTrue(clientProtocol.transport.q.disconnect)
+ handshakeDeferred.addCallback(cbConnectionDone)
+ return handshakeDeferred
+
+
+ def test_connectionLostOnlyAfterUnderlyingCloses(self):
+ """
+ The user protocol's connectionLost is only called when transport
+ underlying TLS is disconnected.
+ """
+ class LostProtocol(Protocol):
+ disconnected = None
+ def connectionLost(self, reason):
+ self.disconnected = reason
+ wrapperFactory = TLSMemoryBIOFactory(ClientContextFactory(),
+ True, ClientFactory())
+ protocol = LostProtocol()
+ tlsProtocol = TLSMemoryBIOProtocol(wrapperFactory, protocol)
+ transport = StringTransport()
+ tlsProtocol.makeConnection(transport)
+
+ # Pretend TLS shutdown finished cleanly; the underlying transport
+ # should be told to close, but the user protocol should not yet be
+ # notified:
+ tlsProtocol._tlsShutdownFinished(None)
+ self.assertEqual(transport.disconnecting, True)
+ self.assertEqual(protocol.disconnected, None)
+
+ # Now close the underlying connection; the user protocol should be
+ # notified with the given reason (since TLS closed cleanly):
+ tlsProtocol.connectionLost(Failure(ConnectionLost("ono")))
+ self.assertTrue(protocol.disconnected.check(ConnectionLost))
+ self.assertEqual(protocol.disconnected.value.args, ("ono",))
+
+
+ def test_loseConnectionTwice(self):
+ """
+ If TLSMemoryBIOProtocol.loseConnection is called multiple times, all
+ but the first call have no effect.
+ """
+ wrapperFactory = TLSMemoryBIOFactory(ClientContextFactory(),
+ True, ClientFactory())
+ tlsProtocol = TLSMemoryBIOProtocol(wrapperFactory, Protocol())
+ transport = StringTransport()
+ tlsProtocol.makeConnection(transport)
+ self.assertEqual(tlsProtocol.disconnecting, False)
+
+ # Make sure loseConnection calls _shutdownTLS the first time (mostly
+ # to make sure we've overriding it correctly):
+ calls = []
+ def _shutdownTLS(shutdown=tlsProtocol._shutdownTLS):
+ calls.append(1)
+ return shutdown()
+ tlsProtocol._shutdownTLS = _shutdownTLS
+ tlsProtocol.loseConnection()
+ self.assertEqual(tlsProtocol.disconnecting, True)
+ self.assertEqual(calls, [1])
+
+ # Make sure _shutdownTLS isn't called a second time:
+ tlsProtocol.loseConnection()
+ self.assertEqual(calls, [1])
+
+
+ def test_unexpectedEOF(self):
+ """
+ Unexpected disconnects get converted to ConnectionLost errors.
+ """
+ tlsClient, tlsServer, handshakeDeferred, disconnectDeferred = (
+ self.handshakeProtocols())
+ serverProtocol = tlsServer.wrappedProtocol
+ data = []
+ reason = []
+ serverProtocol.dataReceived = data.append
+ serverProtocol.connectionLost = reason.append
+
+ # Write data, then disconnect *underlying* transport, resulting in an
+ # unexpected TLS disconnect:
+ def handshakeDone(ign):
+ tlsClient.write("hello")
+ tlsClient.transport.loseConnection()
+ handshakeDeferred.addCallback(handshakeDone)
+
+ # Receiver should be disconnected, with ConnectionLost notification
+ # (masking the Unexpected EOF SSL error):
+ def disconnected(ign):
+ self.assertTrue(reason[0].check(ConnectionLost), reason[0])
+ disconnectDeferred.addCallback(disconnected)
+ return disconnectDeferred
+
+
+ def test_errorWriting(self):
+ """
+ Errors while writing cause the protocols to be disconnected.
+ """
+ tlsClient, tlsServer, handshakeDeferred, disconnectDeferred = (
+ self.handshakeProtocols())
+ reason = []
+ tlsClient.wrappedProtocol.connectionLost = reason.append
+
+ # Pretend TLS connection is unhappy sending:
+ class Wrapper(object):
+ def __init__(self, wrapped):
+ self._wrapped = wrapped
+ def __getattr__(self, attr):
+ return getattr(self._wrapped, attr)
+ def send(self, *args):
+ raise Error("ONO!")
+ tlsClient._tlsConnection = Wrapper(tlsClient._tlsConnection)
+
+ # Write some data:
+ def handshakeDone(ign):
+ tlsClient.write("hello")
+ handshakeDeferred.addCallback(handshakeDone)
+
+ # Failed writer should be disconnected with SSL error:
+ def disconnected(ign):
+ self.assertTrue(reason[0].check(Error), reason[0])
+ disconnectDeferred.addCallback(disconnected)
+ return disconnectDeferred
+
+
+
+class TLSProducerTests(TestCase):
+ """
+ The TLS transport must support the IConsumer interface.
+ """
+
+ def setupStreamingProducer(self, transport=None):
+ class HistoryStringTransport(StringTransport):
+ def __init__(self):
+ StringTransport.__init__(self)
+ self.producerHistory = []
+
+ def pauseProducing(self):
+ self.producerHistory.append("pause")
+ StringTransport.pauseProducing(self)
+
+ def resumeProducing(self):
+ self.producerHistory.append("resume")
+ StringTransport.resumeProducing(self)
+
+ def stopProducing(self):
+ self.producerHistory.append("stop")
+ StringTransport.stopProducing(self)
+
+ clientProtocol, tlsProtocol = buildTLSProtocol(transport=transport)
+ producer = HistoryStringTransport()
+ clientProtocol.transport.registerProducer(producer, True)
+ self.assertEqual(tlsProtocol.transport.streaming, True)
+ return clientProtocol, tlsProtocol, producer
+
+
+ def flushTwoTLSProtocols(self, tlsProtocol, serverTLSProtocol):
+ """
+ Transfer bytes back and forth between two TLS protocols.
+ """
+ # We want to make sure all bytes are passed back and forth; JP
+ # estimated that 3 rounds should be enough:
+ for i in range(3):
+ clientData = tlsProtocol.transport.value()
+ if clientData:
+ serverTLSProtocol.dataReceived(clientData)
+ tlsProtocol.transport.clear()
+ serverData = serverTLSProtocol.transport.value()
+ if serverData:
+ tlsProtocol.dataReceived(serverData)
+ serverTLSProtocol.transport.clear()
+ if not serverData and not clientData:
+ break
+ self.assertEqual(tlsProtocol.transport.value(), "")
+ self.assertEqual(serverTLSProtocol.transport.value(), "")
+
+
+ def test_streamingProducerPausedInNormalMode(self):
+ """
+ When the TLS transport is not blocked on reads, it correctly calls
+ pauseProducing on the registered producer.
+ """
+ _, tlsProtocol, producer = self.setupStreamingProducer()
+
+ # The TLS protocol's transport pretends to be full, pausing its
+ # producer:
+ tlsProtocol.transport.producer.pauseProducing()
+ self.assertEqual(producer.producerState, 'paused')
+ self.assertEqual(producer.producerHistory, ['pause'])
+ self.assertEqual(tlsProtocol._producer._producerPaused, True)
+
+
+ def test_streamingProducerResumedInNormalMode(self):
+ """
+ When the TLS transport is not blocked on reads, it correctly calls
+ resumeProducing on the registered producer.
+ """
+ _, tlsProtocol, producer = self.setupStreamingProducer()
+ tlsProtocol.transport.producer.pauseProducing()
+ self.assertEqual(producer.producerHistory, ['pause'])
+
+ # The TLS protocol's transport pretends to have written everything
+ # out, so it resumes its producer:
+ tlsProtocol.transport.producer.resumeProducing()
+ self.assertEqual(producer.producerState, 'producing')
+ self.assertEqual(producer.producerHistory, ['pause', 'resume'])
+ self.assertEqual(tlsProtocol._producer._producerPaused, False)
+
+
+ def test_streamingProducerPausedInWriteBlockedOnReadMode(self):
+ """
+ When the TLS transport is blocked on reads, it correctly calls
+ pauseProducing on the registered producer.
+ """
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer()
+
+ # Write to TLS transport. Because we do this before the initial TLS
+ # handshake is finished, writing bytes triggers a WantReadError,
+ # indicating that until bytes are read for the handshake, more bytes
+ # cannot be written. Thus writing bytes before the handshake should
+ # cause the producer to be paused:
+ clientProtocol.transport.write("hello")
+ self.assertEqual(producer.producerState, 'paused')
+ self.assertEqual(producer.producerHistory, ['pause'])
+ self.assertEqual(tlsProtocol._producer._producerPaused, True)
+
+
+ def test_streamingProducerResumedInWriteBlockedOnReadMode(self):
+ """
+ When the TLS transport is blocked on reads, it correctly calls
+ resumeProducing on the registered producer.
+ """
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer()
+
+ # Write to TLS transport, triggering WantReadError; this should cause
+ # the producer to be paused. We use a large chunk of data to make sure
+ # large writes don't trigger multiple pauses:
+ clientProtocol.transport.write("hello world" * 320000)
+ self.assertEqual(producer.producerHistory, ['pause'])
+
+ # Now deliver bytes that will fix the WantRead condition; this should
+ # unpause the producer:
+ serverProtocol, serverTLSProtocol = buildTLSProtocol(server=True)
+ self.flushTwoTLSProtocols(tlsProtocol, serverTLSProtocol)
+ self.assertEqual(producer.producerHistory, ['pause', 'resume'])
+ self.assertEqual(tlsProtocol._producer._producerPaused, False)
+
+ # Make sure we haven't disconnected for some reason:
+ self.assertEqual(tlsProtocol.transport.disconnecting, False)
+ self.assertEqual(producer.producerState, 'producing')
+
+
+ def test_streamingProducerTwice(self):
+ """
+ Registering a streaming producer twice throws an exception.
+ """
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer()
+ originalProducer = tlsProtocol._producer
+ producer2 = object()
+ self.assertRaises(RuntimeError,
+ clientProtocol.transport.registerProducer, producer2, True)
+ self.assertIdentical(tlsProtocol._producer, originalProducer)
+
+
+ def test_streamingProducerUnregister(self):
+ """
+ Unregistering a streaming producer removes it, reverting to initial state.
+ """
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer()
+ clientProtocol.transport.unregisterProducer()
+ self.assertEqual(tlsProtocol._producer, None)
+ self.assertEqual(tlsProtocol.transport.producer, None)
+
+
+ def loseConnectionWithProducer(self, writeBlockedOnRead):
+ """
+ Common code for tests involving writes by producer after
+ loseConnection is called.
+ """
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer()
+ serverProtocol, serverTLSProtocol = buildTLSProtocol(server=True)
+
+ if not writeBlockedOnRead:
+ # Do the initial handshake before write:
+ self.flushTwoTLSProtocols(tlsProtocol, serverTLSProtocol)
+ else:
+ # In this case the write below will trigger write-blocked-on-read
+ # condition...
+ pass
+
+ # Now write, then lose connection:
+ clientProtocol.transport.write("x ")
+ clientProtocol.transport.loseConnection()
+ self.flushTwoTLSProtocols(tlsProtocol, serverTLSProtocol)
+
+ # Underlying transport should not have loseConnection called yet, nor
+ # should producer be stopped:
+ self.assertEqual(tlsProtocol.transport.disconnecting, False)
+ self.assertFalse("stop" in producer.producerHistory)
+
+ # Writes from client to server should continue to go through, since we
+ # haven't unregistered producer yet:
+ clientProtocol.transport.write("hello")
+ clientProtocol.transport.writeSequence([" ", "world"])
+
+ # Unregister producer; this should trigger TLS shutdown:
+ clientProtocol.transport.unregisterProducer()
+ self.assertNotEqual(tlsProtocol.transport.value(), "")
+ self.assertEqual(tlsProtocol.transport.disconnecting, False)
+
+ # Additional writes should not go through:
+ clientProtocol.transport.write("won't")
+ clientProtocol.transport.writeSequence(["won't!"])
+
+ # Finish TLS close handshake:
+ self.flushTwoTLSProtocols(tlsProtocol, serverTLSProtocol)
+ self.assertEqual(tlsProtocol.transport.disconnecting, True)
+
+ # Bytes made it through, as long as they were written before producer
+ # was unregistered:
+ self.assertEqual("".join(serverProtocol.received), "x hello world")
+
+
+ def test_streamingProducerLoseConnectionWithProducer(self):
+ """
+ loseConnection() waits for the producer to unregister itself, then
+ does a clean TLS close alert, then closes the underlying connection.
+ """
+ return self.loseConnectionWithProducer(False)
+
+
+ def test_streamingProducerLoseConnectionWithProducerWBOR(self):
+ """
+ Even when writes are blocked on reading, loseConnection() waits for
+ the producer to unregister itself, then does a clean TLS close alert,
+ then closes the underlying connection.
+ """
+ return self.loseConnectionWithProducer(True)
+
+
+ def test_streamingProducerBothTransportsDecideToPause(self):
+ """
+ pauseProducing() events can come from both the TLS transport layer and
+ the underlying transport. In this case, both decide to pause,
+ underlying first.
+ """
+ class PausingStringTransport(StringTransport):
+ _didPause = False
+
+ def write(self, data):
+ if not self._didPause and self.producer is not None:
+ self._didPause = True
+ self.producer.pauseProducing()
+ StringTransport.write(self, data)
+
+
+ class TLSConnection(object):
+ def __init__(self):
+ self.l = []
+
+ def send(self, bytes):
+ # on first write, don't send all bytes:
+ if not self.l:
+ bytes = bytes[:-1]
+ # pause on second write:
+ if len(self.l) == 1:
+ self.l.append("paused")
+ raise WantReadError()
+ # otherwise just take in data:
+ self.l.append(bytes)
+ return len(bytes)
+
+ def bio_write(self, data):
+ pass
+
+ def bio_read(self, size):
+ return chr(ord('A') + len(self.l))
+
+ def recv(self, size):
+ raise WantReadError()
+
+ transport = PausingStringTransport()
+ clientProtocol, tlsProtocol, producer = self.setupStreamingProducer(
+ transport)
+ self.assertEqual(producer.producerState, 'producing')
+
+ # Shove in fake TLSConnection that will raise WantReadError the second
+ # time send() is called. This will allow us to have bytes written to
+ # to the PausingStringTransport, so it will pause the producer. Then,
+ # WantReadError will be thrown, triggering the TLS transport's
+ # producer code path.
+ tlsProtocol._tlsConnection = TLSConnection()
+ clientProtocol.transport.write("hello")
+ self.assertEqual(producer.producerState, 'paused')
+ self.assertEqual(producer.producerHistory, ['pause'])
+
+ # Now, underlying transport resumes, and then we deliver some data to
+ # TLS transport so that it will resume:
+ tlsProtocol.transport.producer.resumeProducing()
+ self.assertEqual(producer.producerState, 'producing')
+ self.assertEqual(producer.producerHistory, ['pause', 'resume'])
+ tlsProtocol.dataReceived("hello")
+ self.assertEqual(producer.producerState, 'producing')
+ self.assertEqual(producer.producerHistory, ['pause', 'resume'])
+
+
+ def test_streamingProducerStopProducing(self):
+ """
+ If the underlying transport tells its producer to stopProducing(),
+ this is passed on to the high-level producer.
+ """
+ _, tlsProtocol, producer = self.setupStreamingProducer()
+ tlsProtocol.transport.producer.stopProducing()
+ self.assertEqual(producer.producerState, 'stopped')
+
+
+ def test_nonStreamingProducer(self):
+ """
+ Non-streaming producers get wrapped as streaming producers.
+ """
+ clientProtocol, tlsProtocol = buildTLSProtocol()
+ producer = NonStreamingProducer(clientProtocol.transport)
+
+ # Register non-streaming producer:
+ clientProtocol.transport.registerProducer(producer, False)
+ streamingProducer = tlsProtocol.transport.producer._producer
+
+ # Verify it was wrapped into streaming producer:
+ self.assertIsInstance(streamingProducer, _PullToPush)
+ self.assertEqual(streamingProducer._producer, producer)
+ self.assertEqual(streamingProducer._consumer, clientProtocol.transport)
+ self.assertEqual(tlsProtocol.transport.streaming, True)
+
+ # Verify the streaming producer was started, and ran until the end:
+ def done(ignore):
+ # Our own producer is done:
+ self.assertEqual(producer.consumer, None)
+ # The producer has been unregistered:
+ self.assertEqual(tlsProtocol.transport.producer, None)
+ # The streaming producer wrapper knows it's done:
+ self.assertEqual(streamingProducer._finished, True)
+ producer.result.addCallback(done)
+
+ serverProtocol, serverTLSProtocol = buildTLSProtocol(server=True)
+ self.flushTwoTLSProtocols(tlsProtocol, serverTLSProtocol)
+ return producer.result
+
+
+ def test_interface(self):
+ """
+ L{_ProducerMembrane} implements L{IPushProducer}.
+ """
+ producer = StringTransport()
+ membrane = _ProducerMembrane(producer)
+ self.assertTrue(verifyObject(IPushProducer, membrane))
+
+
+ def registerProducerAfterConnectionLost(self, streaming):
+ """
+ If a producer is registered after the transport has disconnected, the
+ producer is not used, and its stopProducing method is called.
+ """
+ clientProtocol, tlsProtocol = buildTLSProtocol()
+ clientProtocol.connectionLost = lambda reason: reason.trap(Error)
+
+ class Producer(object):
+ stopped = False
+
+ def resumeProducing(self):
+ return 1/0 # this should never be called
+
+ def stopProducing(self):
+ self.stopped = True
+
+ # Disconnect the transport:
+ tlsProtocol.connectionLost(Failure(ConnectionDone()))
+
+ # Register the producer; startProducing should not be called, but
+ # stopProducing will:
+ producer = Producer()
+ tlsProtocol.registerProducer(producer, False)
+ self.assertIdentical(tlsProtocol.transport.producer, None)
+ self.assertEqual(producer.stopped, True)
+
+
+ def test_streamingProducerAfterConnectionLost(self):
+ """
+ If a streaming producer is registered after the transport has
+ disconnected, the producer is not used, and its stopProducing method
+ is called.
+ """
+ self.registerProducerAfterConnectionLost(True)
+
+
+ def test_nonStreamingProducerAfterConnectionLost(self):
+ """
+ If a non-streaming producer is registered after the transport has
+ disconnected, the producer is not used, and its stopProducing method
+ is called.
+ """
+ self.registerProducerAfterConnectionLost(False)
+
+
+
+class NonStreamingProducer(object):
+ """
+ A pull producer which writes 10 times only.
+ """
+
+ counter = 0
+ stopped = False
+
+ def __init__(self, consumer):
+ self.consumer = consumer
+ self.result = Deferred()
+
+ def resumeProducing(self):
+ if self.counter < 10:
+ self.consumer.write(str(self.counter))
+ self.counter += 1
+ if self.counter == 10:
+ self.consumer.unregisterProducer()
+ self._done()
+ else:
+ if self.consumer is None:
+ raise RuntimeError("BUG: resume after unregister/stop.")
+
+
+ def pauseProducing(self):
+ raise RuntimeError("BUG: pause should never be called.")
+
+
+ def _done(self):
+ self.consumer = None
+ d = self.result
+ del self.result
+ d.callback(None)
+
+
+ def stopProducing(self):
+ self.stopped = True
+ self._done()
+
+
+
+class NonStreamingProducerTests(TestCase):
+ """
+ Non-streaming producers can be adapted into being streaming producers.
+ """
+
+ def streamUntilEnd(self, consumer):
+ """
+ Verify the consumer writes out all its data, but is not called after
+ that.
+ """
+ nsProducer = NonStreamingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ consumer.registerProducer(streamingProducer, True)
+
+ # The producer will call unregisterProducer(), and we need to hook
+ # that up so the streaming wrapper is notified; the
+ # TLSMemoryBIOProtocol will have to do this itself, which is tested
+ # elsewhere:
+ def unregister(orig=consumer.unregisterProducer):
+ orig()
+ streamingProducer.stopStreaming()
+ consumer.unregisterProducer = unregister
+
+ done = nsProducer.result
+ def doneStreaming(_):
+ # All data was streamed, and the producer unregistered itself:
+ self.assertEqual(consumer.value(), "0123456789")
+ self.assertEqual(consumer.producer, None)
+ # And the streaming wrapper stopped:
+ self.assertEqual(streamingProducer._finished, True)
+ done.addCallback(doneStreaming)
+
+ # Now, start streaming:
+ streamingProducer.startStreaming()
+ return done
+
+
+ def test_writeUntilDone(self):
+ """
+ When converted to a streaming producer, the non-streaming producer
+ writes out all its data, but is not called after that.
+ """
+ consumer = StringTransport()
+ return self.streamUntilEnd(consumer)
+
+
+ def test_pause(self):
+ """
+ When the streaming producer is paused, the underlying producer stops
+ getting resumeProducing calls.
+ """
+ class PausingStringTransport(StringTransport):
+ writes = 0
+
+ def __init__(self):
+ StringTransport.__init__(self)
+ self.paused = Deferred()
+
+ def write(self, data):
+ self.writes += 1
+ StringTransport.write(self, data)
+ if self.writes == 3:
+ self.producer.pauseProducing()
+ d = self.paused
+ del self.paused
+ d.callback(None)
+
+
+ consumer = PausingStringTransport()
+ nsProducer = NonStreamingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ consumer.registerProducer(streamingProducer, True)
+
+ # Make sure the consumer does not continue:
+ def shouldNotBeCalled(ignore):
+ self.fail("BUG: The producer should not finish!")
+ nsProducer.result.addCallback(shouldNotBeCalled)
+
+ done = consumer.paused
+ def paused(ignore):
+ # The CooperatorTask driving the producer was paused:
+ self.assertEqual(streamingProducer._coopTask._pauseCount, 1)
+ done.addCallback(paused)
+
+ # Now, start streaming:
+ streamingProducer.startStreaming()
+ return done
+
+
+ def test_resume(self):
+ """
+ When the streaming producer is paused and then resumed, the underlying
+ producer starts getting resumeProducing calls again after the resume.
+
+ The test will never finish (or rather, time out) if the resume
+ producing call is not working.
+ """
+ class PausingStringTransport(StringTransport):
+ writes = 0
+
+ def write(self, data):
+ self.writes += 1
+ StringTransport.write(self, data)
+ if self.writes == 3:
+ self.producer.pauseProducing()
+ self.producer.resumeProducing()
+
+ consumer = PausingStringTransport()
+ return self.streamUntilEnd(consumer)
+
+
+ def test_stopProducing(self):
+ """
+ When the streaming producer is stopped by the consumer, the underlying
+ producer is stopped, and streaming is stopped.
+ """
+ class StoppingStringTransport(StringTransport):
+ writes = 0
+
+ def write(self, data):
+ self.writes += 1
+ StringTransport.write(self, data)
+ if self.writes == 3:
+ self.producer.stopProducing()
+
+ consumer = StoppingStringTransport()
+ nsProducer = NonStreamingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ consumer.registerProducer(streamingProducer, True)
+
+ done = nsProducer.result
+ def doneStreaming(_):
+ # Not all data was streamed, and the producer was stopped:
+ self.assertEqual(consumer.value(), "012")
+ self.assertEqual(nsProducer.stopped, True)
+ # And the streaming wrapper stopped:
+ self.assertEqual(streamingProducer._finished, True)
+ done.addCallback(doneStreaming)
+
+ # Now, start streaming:
+ streamingProducer.startStreaming()
+ return done
+
+
+ def resumeProducingRaises(self, consumer, expectedExceptions):
+ """
+ Common implementation for tests where the underlying producer throws
+ an exception when its resumeProducing is called.
+ """
+ class ThrowingProducer(NonStreamingProducer):
+
+ def resumeProducing(self):
+ if self.counter == 2:
+ return 1/0
+ else:
+ NonStreamingProducer.resumeProducing(self)
+
+ nsProducer = ThrowingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ consumer.registerProducer(streamingProducer, True)
+
+ # Register log observer:
+ loggedMsgs = []
+ log.addObserver(loggedMsgs.append)
+ self.addCleanup(log.removeObserver, loggedMsgs.append)
+
+ # Make consumer unregister do what TLSMemoryBIOProtocol would do:
+ def unregister(orig=consumer.unregisterProducer):
+ orig()
+ streamingProducer.stopStreaming()
+ consumer.unregisterProducer = unregister
+
+ # Start streaming:
+ streamingProducer.startStreaming()
+
+ done = streamingProducer._coopTask.whenDone()
+ done.addErrback(lambda reason: reason.trap(TaskStopped))
+ def stopped(ign):
+ self.assertEqual(consumer.value(), "01")
+ # Any errors from resumeProducing were logged:
+ errors = self.flushLoggedErrors()
+ self.assertEqual(len(errors), len(expectedExceptions))
+ for f, (expected, msg), logMsg in zip(
+ errors, expectedExceptions, loggedMsgs):
+ self.assertTrue(f.check(expected))
+ self.assertIn(msg, logMsg['why'])
+ # And the streaming wrapper stopped:
+ self.assertEqual(streamingProducer._finished, True)
+ done.addCallback(stopped)
+ return done
+
+
+ def test_resumeProducingRaises(self):
+ """
+ If the underlying producer raises an exception when resumeProducing is
+ called, the streaming wrapper should log the error, unregister from
+ the consumer and stop streaming.
+ """
+ consumer = StringTransport()
+ done = self.resumeProducingRaises(
+ consumer,
+ [(ZeroDivisionError, "failed, producing will be stopped")])
+ def cleanShutdown(ignore):
+ # Producer was unregistered from consumer:
+ self.assertEqual(consumer.producer, None)
+ done.addCallback(cleanShutdown)
+ return done
+
+
+ def test_resumeProducingRaiseAndUnregisterProducerRaises(self):
+ """
+ If the underlying producer raises an exception when resumeProducing is
+ called, the streaming wrapper should log the error, unregister from
+ the consumer and stop streaming even if the unregisterProducer call
+ also raise.
+ """
+ consumer = StringTransport()
+ def raiser():
+ raise RuntimeError()
+ consumer.unregisterProducer = raiser
+ return self.resumeProducingRaises(
+ consumer,
+ [(ZeroDivisionError, "failed, producing will be stopped"),
+ (RuntimeError, "failed to unregister producer")])
+
+
+ def test_stopStreamingTwice(self):
+ """
+ stopStreaming() can be called more than once without blowing
+ up. This is useful for error-handling paths.
+ """
+ consumer = StringTransport()
+ nsProducer = NonStreamingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ streamingProducer.startStreaming()
+ streamingProducer.stopStreaming()
+ streamingProducer.stopStreaming()
+ self.assertEqual(streamingProducer._finished, True)
+
+
+ def test_interface(self):
+ """
+ L{_PullToPush} implements L{IPushProducer}.
+ """
+ consumer = StringTransport()
+ nsProducer = NonStreamingProducer(consumer)
+ streamingProducer = _PullToPush(nsProducer, consumer)
+ self.assertTrue(verifyObject(IPushProducer, streamingProducer))
diff --git a/twisted/protocols/tls.py b/twisted/protocols/tls.py
new file mode 100644
index 0000000..0094fda
--- /dev/null
+++ b/twisted/protocols/tls.py
@@ -0,0 +1,609 @@
+# -*- test-case-name: twisted.protocols.test.test_tls,twisted.internet.test.test_tls,twisted.test.test_sslverify -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation of a TLS transport (L{ISSLTransport}) as an
+L{IProtocol<twisted.internet.interfaces.IProtocol>} layered on top of any
+L{ITransport<twisted.internet.interfaces.ITransport>} implementation, based on
+U{OpenSSL<http://www.openssl.org>}'s memory BIO features.
+
+L{TLSMemoryBIOFactory} is a L{WrappingFactory} which wraps protocols created by
+the factory it wraps with L{TLSMemoryBIOProtocol}. L{TLSMemoryBIOProtocol}
+intercedes between the underlying transport and the wrapped protocol to
+implement SSL and TLS. Typical usage of this module looks like this::
+
+ from twisted.protocols.tls import TLSMemoryBIOFactory
+ from twisted.internet.protocol import ServerFactory
+ from twisted.internet.ssl import PrivateCertificate
+ from twisted.internet import reactor
+
+ from someapplication import ApplicationProtocol
+
+ serverFactory = ServerFactory()
+ serverFactory.protocol = ApplicationProtocol
+ certificate = PrivateCertificate.loadPEM(certPEMData)
+ contextFactory = certificate.options()
+ tlsFactory = TLSMemoryBIOFactory(contextFactory, False, serverFactory)
+ reactor.listenTCP(12345, tlsFactory)
+ reactor.run()
+
+This API offers somewhat more flexibility than
+L{twisted.internet.interfaces.IReactorSSL}; for example, a L{TLSMemoryBIOProtocol}
+instance can use another instance of L{TLSMemoryBIOProtocol} as its transport,
+yielding TLS over TLS - useful to implement onion routing. It can also be used
+to run TLS over unusual transports, such as UNIX sockets and stdio.
+"""
+
+
+from OpenSSL.SSL import Error, ZeroReturnError, WantReadError
+from OpenSSL.SSL import TLSv1_METHOD, Context, Connection
+
+try:
+ Connection(Context(TLSv1_METHOD), None)
+except TypeError, e:
+ if str(e) != "argument must be an int, or have a fileno() method.":
+ raise
+ raise ImportError("twisted.protocols.tls requires pyOpenSSL 0.10 or newer.")
+
+from zope.interface import implements
+
+from twisted.python.failure import Failure
+from twisted.python import log
+from twisted.python.reflect import safe_str
+from twisted.internet.interfaces import ISystemHandle, ISSLTransport
+from twisted.internet.interfaces import IPushProducer, ILoggingContext
+from twisted.internet.main import CONNECTION_LOST
+from twisted.internet.protocol import Protocol
+from twisted.internet.task import cooperate
+from twisted.protocols.policies import ProtocolWrapper, WrappingFactory
+
+
+class _PullToPush(object):
+ """
+ An adapter that converts a non-streaming to a streaming producer.
+
+ Because of limitations of the producer API, this adapter requires the
+ cooperation of the consumer. When the consumer's C{registerProducer} is
+ called with a non-streaming producer, it must wrap it with L{_PullToPush}
+ and then call C{startStreaming} on the resulting object. When the
+ consumer's C{unregisterProducer} is called, it must call
+ C{stopStreaming} on the L{_PullToPush} instance.
+
+ If the underlying producer throws an exception from C{resumeProducing},
+ the producer will be unregistered from the consumer.
+
+ @ivar _producer: the underling non-streaming producer.
+
+ @ivar _consumer: the consumer with which the underlying producer was
+ registered.
+
+ @ivar _finished: C{bool} indicating whether the producer has finished.
+
+ @ivar _coopTask: the result of calling L{cooperate}, the task driving the
+ streaming producer.
+ """
+ implements(IPushProducer)
+
+ _finished = False
+
+
+ def __init__(self, pullProducer, consumer):
+ self._producer = pullProducer
+ self._consumer = consumer
+
+
+ def _pull(self):
+ """
+ A generator that calls C{resumeProducing} on the underlying producer
+ forever.
+
+ If C{resumeProducing} throws an exception, the producer is
+ unregistered, which should result in streaming stopping.
+ """
+ while True:
+ try:
+ self._producer.resumeProducing()
+ except:
+ log.err(None, "%s failed, producing will be stopped:" %
+ (safe_str(self._producer),))
+ try:
+ self._consumer.unregisterProducer()
+ # The consumer should now call stopStreaming() on us,
+ # thus stopping the streaming.
+ except:
+ # Since the consumer blew up, we may not have had
+ # stopStreaming() called, so we just stop on our own:
+ log.err(None, "%s failed to unregister producer:" %
+ (safe_str(self._consumer),))
+ self._finished = True
+ return
+ yield None
+
+
+ def startStreaming(self):
+ """
+ This should be called by the consumer when the producer is registered.
+
+ Start streaming data to the consumer.
+ """
+ self._coopTask = cooperate(self._pull())
+
+
+ def stopStreaming(self):
+ """
+ This should be called by the consumer when the producer is unregistered.
+
+ Stop streaming data to the consumer.
+ """
+ if self._finished:
+ return
+ self._finished = True
+ self._coopTask.stop()
+
+
+ # IPushProducer implementation:
+ def pauseProducing(self):
+ self._coopTask.pause()
+
+
+ def resumeProducing(self):
+ self._coopTask.resume()
+
+
+ def stopProducing(self):
+ self.stopStreaming()
+ self._producer.stopProducing()
+
+
+
+class _ProducerMembrane(object):
+ """
+ Stand-in for producer registered with a L{TLSMemoryBIOProtocol} transport.
+
+ Ensures that producer pause/resume events from the undelying transport are
+ coordinated with pause/resume events from the TLS layer.
+
+ @ivar _producer: The application-layer producer.
+ """
+ implements(IPushProducer)
+
+ _producerPaused = False
+
+ def __init__(self, producer):
+ self._producer = producer
+
+
+ def pauseProducing(self):
+ """
+ C{pauseProducing} the underlying producer, if it's not paused.
+ """
+ if self._producerPaused:
+ return
+ self._producerPaused = True
+ self._producer.pauseProducing()
+
+
+ def resumeProducing(self):
+ """
+ C{resumeProducing} the underlying producer, if it's paused.
+ """
+ if not self._producerPaused:
+ return
+ self._producerPaused = False
+ self._producer.resumeProducing()
+
+
+ def stopProducing(self):
+ """
+ C{stopProducing} the underlying producer.
+
+ There is only a single source for this event, so it's simply passed
+ on.
+ """
+ self._producer.stopProducing()
+
+
+
+class TLSMemoryBIOProtocol(ProtocolWrapper):
+ """
+ L{TLSMemoryBIOProtocol} is a protocol wrapper which uses OpenSSL via a
+ memory BIO to encrypt bytes written to it before sending them on to the
+ underlying transport and decrypts bytes received from the underlying
+ transport before delivering them to the wrapped protocol.
+
+ In addition to producer events from the underlying transport, the need to
+ wait for reads before a write can proceed means the
+ L{TLSMemoryBIOProtocol} may also want to pause a producer. Pause/resume
+ events are therefore merged using the L{_ProducerMembrane}
+ wrapper. Non-streaming (pull) producers are supported by wrapping them
+ with L{_PullToPush}.
+
+ @ivar _tlsConnection: The L{OpenSSL.SSL.Connection} instance which is
+ encrypted and decrypting this connection.
+
+ @ivar _lostTLSConnection: A flag indicating whether connection loss has
+ already been dealt with (C{True}) or not (C{False}). TLS disconnection
+ is distinct from the underlying connection being lost.
+
+ @ivar _writeBlockedOnRead: A flag indicating whether further writing must
+ wait for data to be received (C{True}) or not (C{False}).
+
+ @ivar _appSendBuffer: A C{list} of C{str} of application-level (cleartext)
+ data which is waiting for C{_writeBlockedOnRead} to be reset to
+ C{False} so it can be passed to and perhaps accepted by
+ C{_tlsConnection.send}.
+
+ @ivar _connectWrapped: A flag indicating whether or not to call
+ C{makeConnection} on the wrapped protocol. This is for the reactor's
+ L{twisted.internet.interfaces.ITLSTransport.startTLS} implementation,
+ since it has a protocol which it has already called C{makeConnection}
+ on, and which has no interest in a new transport. See #3821.
+
+ @ivar _handshakeDone: A flag indicating whether or not the handshake is
+ known to have completed successfully (C{True}) or not (C{False}). This
+ is used to control error reporting behavior. If the handshake has not
+ completed, the underlying L{OpenSSL.SSL.Error} will be passed to the
+ application's C{connectionLost} method. If it has completed, any
+ unexpected L{OpenSSL.SSL.Error} will be turned into a
+ L{ConnectionLost}. This is weird; however, it is simply an attempt at
+ a faithful re-implementation of the behavior provided by
+ L{twisted.internet.ssl}.
+
+ @ivar _reason: If an unexpected L{OpenSSL.SSL.Error} occurs which causes
+ the connection to be lost, it is saved here. If appropriate, this may
+ be used as the reason passed to the application protocol's
+ C{connectionLost} method.
+
+ @ivar _producer: The current producer registered via C{registerProducer},
+ or C{None} if no producer has been registered or a previous one was
+ unregistered.
+ """
+ implements(ISystemHandle, ISSLTransport)
+
+ _reason = None
+ _handshakeDone = False
+ _lostTLSConnection = False
+ _writeBlockedOnRead = False
+ _producer = None
+
+ def __init__(self, factory, wrappedProtocol, _connectWrapped=True):
+ ProtocolWrapper.__init__(self, factory, wrappedProtocol)
+ self._connectWrapped = _connectWrapped
+
+
+ def getHandle(self):
+ """
+ Return the L{OpenSSL.SSL.Connection} object being used to encrypt and
+ decrypt this connection.
+
+ This is done for the benefit of L{twisted.internet.ssl.Certificate}'s
+ C{peerFromTransport} and C{hostFromTransport} methods only. A
+ different system handle may be returned by future versions of this
+ method.
+ """
+ return self._tlsConnection
+
+
+ def makeConnection(self, transport):
+ """
+ Connect this wrapper to the given transport and initialize the
+ necessary L{OpenSSL.SSL.Connection} with a memory BIO.
+ """
+ tlsContext = self.factory._contextFactory.getContext()
+ self._tlsConnection = Connection(tlsContext, None)
+ if self.factory._isClient:
+ self._tlsConnection.set_connect_state()
+ else:
+ self._tlsConnection.set_accept_state()
+ self._appSendBuffer = []
+
+ # Intentionally skip ProtocolWrapper.makeConnection - it might call
+ # wrappedProtocol.makeConnection, which we want to make conditional.
+ Protocol.makeConnection(self, transport)
+ self.factory.registerProtocol(self)
+ if self._connectWrapped:
+ # Now that the TLS layer is initialized, notify the application of
+ # the connection.
+ ProtocolWrapper.makeConnection(self, transport)
+
+ # Now that we ourselves have a transport (initialized by the
+ # ProtocolWrapper.makeConnection call above), kick off the TLS
+ # handshake.
+ try:
+ self._tlsConnection.do_handshake()
+ except WantReadError:
+ # This is the expected case - there's no data in the connection's
+ # input buffer yet, so it won't be able to complete the whole
+ # handshake now. If this is the speak-first side of the
+ # connection, then some bytes will be in the send buffer now; flush
+ # them.
+ self._flushSendBIO()
+
+
+ def _flushSendBIO(self):
+ """
+ Read any bytes out of the send BIO and write them to the underlying
+ transport.
+ """
+ try:
+ bytes = self._tlsConnection.bio_read(2 ** 15)
+ except WantReadError:
+ # There may be nothing in the send BIO right now.
+ pass
+ else:
+ self.transport.write(bytes)
+
+
+ def _flushReceiveBIO(self):
+ """
+ Try to receive any application-level bytes which are now available
+ because of a previous write into the receive BIO. This will take
+ care of delivering any application-level bytes which are received to
+ the protocol, as well as handling of the various exceptions which
+ can come from trying to get such bytes.
+ """
+ # Keep trying this until an error indicates we should stop or we
+ # close the connection. Looping is necessary to make sure we
+ # process all of the data which was put into the receive BIO, as
+ # there is no guarantee that a single recv call will do it all.
+ while not self._lostTLSConnection:
+ try:
+ bytes = self._tlsConnection.recv(2 ** 15)
+ except WantReadError:
+ # The newly received bytes might not have been enough to produce
+ # any application data.
+ break
+ except ZeroReturnError:
+ # TLS has shut down and no more TLS data will be received over
+ # this connection.
+ self._shutdownTLS()
+ # Passing in None means the user protocol's connnectionLost
+ # will get called with reason from underlying transport:
+ self._tlsShutdownFinished(None)
+ except Error, e:
+ # Something went pretty wrong. For example, this might be a
+ # handshake failure (because there were no shared ciphers, because
+ # a certificate failed to verify, etc). TLS can no longer proceed.
+
+ # Squash EOF in violation of protocol into ConnectionLost; we
+ # create Failure before calling _flushSendBio so that no new
+ # exception will get thrown in the interim.
+ if e.args[0] == -1 and e.args[1] == 'Unexpected EOF':
+ failure = Failure(CONNECTION_LOST)
+ else:
+ failure = Failure()
+
+ self._flushSendBIO()
+ self._tlsShutdownFinished(failure)
+ else:
+ # If we got application bytes, the handshake must be done by
+ # now. Keep track of this to control error reporting later.
+ self._handshakeDone = True
+ ProtocolWrapper.dataReceived(self, bytes)
+
+ # The received bytes might have generated a response which needs to be
+ # sent now. For example, the handshake involves several round-trip
+ # exchanges without ever producing application-bytes.
+ self._flushSendBIO()
+
+
+ def dataReceived(self, bytes):
+ """
+ Deliver any received bytes to the receive BIO and then read and deliver
+ to the application any application-level data which becomes available
+ as a result of this.
+ """
+ self._tlsConnection.bio_write(bytes)
+
+ if self._writeBlockedOnRead:
+ # A read just happened, so we might not be blocked anymore. Try to
+ # flush all the pending application bytes.
+ self._writeBlockedOnRead = False
+ appSendBuffer = self._appSendBuffer
+ self._appSendBuffer = []
+ for bytes in appSendBuffer:
+ self._write(bytes)
+ if (not self._writeBlockedOnRead and self.disconnecting and
+ self.producer is None):
+ self._shutdownTLS()
+ if self._producer is not None:
+ self._producer.resumeProducing()
+
+ self._flushReceiveBIO()
+
+
+ def _shutdownTLS(self):
+ """
+ Initiate, or reply to, the shutdown handshake of the TLS layer.
+ """
+ shutdownSuccess = self._tlsConnection.shutdown()
+ self._flushSendBIO()
+ if shutdownSuccess:
+ # Both sides have shutdown, so we can start closing lower-level
+ # transport. This will also happen if we haven't started
+ # negotiation at all yet, in which case shutdown succeeds
+ # immediately.
+ self.transport.loseConnection()
+
+
+ def _tlsShutdownFinished(self, reason):
+ """
+ Called when TLS connection has gone away; tell underlying transport to
+ disconnect.
+ """
+ self._reason = reason
+ self._lostTLSConnection = True
+ # Using loseConnection causes the application protocol's
+ # connectionLost method to be invoked non-reentrantly, which is always
+ # a nice feature. However, for error cases (reason != None) we might
+ # want to use abortConnection when it becomes available. The
+ # loseConnection call is basically tested by test_handshakeFailure.
+ # At least one side will need to do it or the test never finishes.
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ """
+ Handle the possible repetition of calls to this method (due to either
+ the underlying transport going away or due to an error at the TLS
+ layer) and make sure the base implementation only gets invoked once.
+ """
+ if not self._lostTLSConnection:
+ # Tell the TLS connection that it's not going to get any more data
+ # and give it a chance to finish reading.
+ self._tlsConnection.bio_shutdown()
+ self._flushReceiveBIO()
+ self._lostTLSConnection = True
+ reason = self._reason or reason
+ self._reason = None
+ ProtocolWrapper.connectionLost(self, reason)
+
+
+ def loseConnection(self):
+ """
+ Send a TLS close alert and close the underlying connection.
+ """
+ if self.disconnecting:
+ return
+ self.disconnecting = True
+ if not self._writeBlockedOnRead and self._producer is None:
+ self._shutdownTLS()
+
+
+ def write(self, bytes):
+ """
+ Process the given application bytes and send any resulting TLS traffic
+ which arrives in the send BIO.
+
+ If C{loseConnection} was called, subsequent calls to C{write} will
+ drop the bytes on the floor.
+ """
+ # Writes after loseConnection are not supported, unless a producer has
+ # been registered, in which case writes can happen until the producer
+ # is unregistered:
+ if self.disconnecting and self._producer is None:
+ return
+ self._write(bytes)
+
+
+ def _write(self, bytes):
+ """
+ Process the given application bytes and send any resulting TLS traffic
+ which arrives in the send BIO.
+
+ This may be called by C{dataReceived} with bytes that were buffered
+ before C{loseConnection} was called, which is why this function
+ doesn't check for disconnection but accepts the bytes regardless.
+ """
+ if self._lostTLSConnection:
+ return
+
+ leftToSend = bytes
+ while leftToSend:
+ try:
+ sent = self._tlsConnection.send(leftToSend)
+ except WantReadError:
+ self._writeBlockedOnRead = True
+ self._appSendBuffer.append(leftToSend)
+ if self._producer is not None:
+ self._producer.pauseProducing()
+ break
+ except Error:
+ # Pretend TLS connection disconnected, which will trigger
+ # disconnect of underlying transport. The error will be passed
+ # to the application protocol's connectionLost method. The
+ # other SSL implementation doesn't, but losing helpful
+ # debugging information is a bad idea.
+ self._tlsShutdownFinished(Failure())
+ break
+ else:
+ # If we sent some bytes, the handshake must be done. Keep
+ # track of this to control error reporting behavior.
+ self._handshakeDone = True
+ self._flushSendBIO()
+ leftToSend = leftToSend[sent:]
+
+
+ def writeSequence(self, iovec):
+ """
+ Write a sequence of application bytes by joining them into one string
+ and passing them to L{write}.
+ """
+ self.write("".join(iovec))
+
+
+ def getPeerCertificate(self):
+ return self._tlsConnection.get_peer_certificate()
+
+
+ def registerProducer(self, producer, streaming):
+ # If we've already disconnected, nothing to do here:
+ if self._lostTLSConnection:
+ producer.stopProducing()
+ return
+
+ # If we received a non-streaming producer, wrap it so it becomes a
+ # streaming producer:
+ if not streaming:
+ producer = streamingProducer = _PullToPush(producer, self)
+ producer = _ProducerMembrane(producer)
+ # This will raise an exception if a producer is already registered:
+ self.transport.registerProducer(producer, True)
+ self._producer = producer
+ # If we received a non-streaming producer, we need to start the
+ # streaming wrapper:
+ if not streaming:
+ streamingProducer.startStreaming()
+
+
+ def unregisterProducer(self):
+ # If we received a non-streaming producer, we need to stop the
+ # streaming wrapper:
+ if isinstance(self._producer._producer, _PullToPush):
+ self._producer._producer.stopStreaming()
+ self._producer = None
+ self._producerPaused = False
+ self.transport.unregisterProducer()
+ if self.disconnecting and not self._writeBlockedOnRead:
+ self._shutdownTLS()
+
+
+
+class TLSMemoryBIOFactory(WrappingFactory):
+ """
+ L{TLSMemoryBIOFactory} adds TLS to connections.
+
+ @ivar _contextFactory: The TLS context factory which will be used to define
+ certain TLS connection parameters.
+
+ @ivar _isClient: A flag which is C{True} if this is a client TLS
+ connection, C{False} if it is a server TLS connection.
+ """
+ protocol = TLSMemoryBIOProtocol
+
+ noisy = False # disable unnecessary logging.
+
+ def __init__(self, contextFactory, isClient, wrappedFactory):
+ WrappingFactory.__init__(self, wrappedFactory)
+ self._contextFactory = contextFactory
+ self._isClient = isClient
+
+ # Force some parameter checking in pyOpenSSL. It's better to fail now
+ # than after we've set up the transport.
+ contextFactory.getContext()
+
+
+ def logPrefix(self):
+ """
+ Annotate the wrapped factory's log prefix with some text indicating TLS
+ is in use.
+
+ @rtype: C{str}
+ """
+ if ILoggingContext.providedBy(self.wrappedFactory):
+ logPrefix = self.wrappedFactory.logPrefix()
+ else:
+ logPrefix = self.wrappedFactory.__class__.__name__
+ return "%s (TLS)" % (logPrefix,)
+
diff --git a/twisted/protocols/wire.py b/twisted/protocols/wire.py
new file mode 100644
index 0000000..dddf215
--- /dev/null
+++ b/twisted/protocols/wire.py
@@ -0,0 +1,90 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Implement standard (and unused) TCP protocols.
+
+These protocols are either provided by inetd, or are not provided at all.
+"""
+
+# system imports
+import time, struct
+from zope.interface import implements
+
+# twisted import
+from twisted.internet import protocol, interfaces
+
+
+class Echo(protocol.Protocol):
+ """As soon as any data is received, write it back (RFC 862)"""
+
+ def dataReceived(self, data):
+ self.transport.write(data)
+
+
+class Discard(protocol.Protocol):
+ """Discard any received data (RFC 863)"""
+
+ def dataReceived(self, data):
+ # I'm ignoring you, nyah-nyah
+ pass
+
+
+class Chargen(protocol.Protocol):
+ """Generate repeating noise (RFC 864)"""
+ noise = r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ !"#$%&?'
+
+ implements(interfaces.IProducer)
+
+ def connectionMade(self):
+ self.transport.registerProducer(self, 0)
+
+ def resumeProducing(self):
+ self.transport.write(self.noise)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+
+class QOTD(protocol.Protocol):
+ """Return a quote of the day (RFC 865)"""
+
+ def connectionMade(self):
+ self.transport.write(self.getQuote())
+ self.transport.loseConnection()
+
+ def getQuote(self):
+ """Return a quote. May be overrriden in subclasses."""
+ return "An apple a day keeps the doctor away.\r\n"
+
+class Who(protocol.Protocol):
+ """Return list of active users (RFC 866)"""
+
+ def connectionMade(self):
+ self.transport.write(self.getUsers())
+ self.transport.loseConnection()
+
+ def getUsers(self):
+ """Return active users. Override in subclasses."""
+ return "root\r\n"
+
+
+class Daytime(protocol.Protocol):
+ """Send back the daytime in ASCII form (RFC 867)"""
+
+ def connectionMade(self):
+ self.transport.write(time.asctime(time.gmtime(time.time())) + '\r\n')
+ self.transport.loseConnection()
+
+
+class Time(protocol.Protocol):
+ """Send back the time in machine readable form (RFC 868)"""
+
+ def connectionMade(self):
+ # is this correct only for 32-bit machines?
+ result = struct.pack("!i", int(time.time()))
+ self.transport.write(result)
+ self.transport.loseConnection()
+
diff --git a/twisted/python/__init__.py b/twisted/python/__init__.py
new file mode 100644
index 0000000..ae78c7b
--- /dev/null
+++ b/twisted/python/__init__.py
@@ -0,0 +1,13 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted Python: Utilities and Enhancements for Python.
+
+"""
+
+
+
diff --git a/twisted/python/_epoll.c b/twisted/python/_epoll.c
new file mode 100644
index 0000000..dffbe25
--- /dev/null
+++ b/twisted/python/_epoll.c
@@ -0,0 +1,3348 @@
+/* Generated by Cython 0.15.1 on Fri Feb 17 23:33:28 2012 */
+
+#define PY_SSIZE_T_CLEAN
+#include "Python.h"
+#ifndef Py_PYTHON_H
+ #error Python headers needed to compile C extensions, please install development version of Python.
+#else
+
+#include <stddef.h> /* For offsetof */
+#ifndef offsetof
+#define offsetof(type, member) ( (size_t) & ((type*)0) -> member )
+#endif
+
+#if !defined(WIN32) && !defined(MS_WINDOWS)
+ #ifndef __stdcall
+ #define __stdcall
+ #endif
+ #ifndef __cdecl
+ #define __cdecl
+ #endif
+ #ifndef __fastcall
+ #define __fastcall
+ #endif
+#endif
+
+#ifndef DL_IMPORT
+ #define DL_IMPORT(t) t
+#endif
+#ifndef DL_EXPORT
+ #define DL_EXPORT(t) t
+#endif
+
+#ifndef PY_LONG_LONG
+ #define PY_LONG_LONG LONG_LONG
+#endif
+
+#if PY_VERSION_HEX < 0x02040000
+ #define METH_COEXIST 0
+ #define PyDict_CheckExact(op) (Py_TYPE(op) == &PyDict_Type)
+ #define PyDict_Contains(d,o) PySequence_Contains(d,o)
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ typedef int Py_ssize_t;
+ #define PY_SSIZE_T_MAX INT_MAX
+ #define PY_SSIZE_T_MIN INT_MIN
+ #define PY_FORMAT_SIZE_T ""
+ #define PyInt_FromSsize_t(z) PyInt_FromLong(z)
+ #define PyInt_AsSsize_t(o) __Pyx_PyInt_AsInt(o)
+ #define PyNumber_Index(o) PyNumber_Int(o)
+ #define PyIndex_Check(o) PyNumber_Check(o)
+ #define PyErr_WarnEx(category, message, stacklevel) PyErr_Warn(category, message)
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt)
+ #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
+ #define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
+ #define PyVarObject_HEAD_INIT(type, size) \
+ PyObject_HEAD_INIT(type) size,
+ #define PyType_Modified(t)
+
+ typedef struct {
+ void *buf;
+ PyObject *obj;
+ Py_ssize_t len;
+ Py_ssize_t itemsize;
+ int readonly;
+ int ndim;
+ char *format;
+ Py_ssize_t *shape;
+ Py_ssize_t *strides;
+ Py_ssize_t *suboffsets;
+ void *internal;
+ } Py_buffer;
+
+ #define PyBUF_SIMPLE 0
+ #define PyBUF_WRITABLE 0x0001
+ #define PyBUF_FORMAT 0x0004
+ #define PyBUF_ND 0x0008
+ #define PyBUF_STRIDES (0x0010 | PyBUF_ND)
+ #define PyBUF_C_CONTIGUOUS (0x0020 | PyBUF_STRIDES)
+ #define PyBUF_F_CONTIGUOUS (0x0040 | PyBUF_STRIDES)
+ #define PyBUF_ANY_CONTIGUOUS (0x0080 | PyBUF_STRIDES)
+ #define PyBUF_INDIRECT (0x0100 | PyBUF_STRIDES)
+
+#endif
+
+#if PY_MAJOR_VERSION < 3
+ #define __Pyx_BUILTIN_MODULE_NAME "__builtin__"
+#else
+ #define __Pyx_BUILTIN_MODULE_NAME "builtins"
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define Py_TPFLAGS_CHECKTYPES 0
+ #define Py_TPFLAGS_HAVE_INDEX 0
+#endif
+
+#if (PY_VERSION_HEX < 0x02060000) || (PY_MAJOR_VERSION >= 3)
+ #define Py_TPFLAGS_HAVE_NEWBUFFER 0
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBaseString_Type PyUnicode_Type
+ #define PyStringObject PyUnicodeObject
+ #define PyString_Type PyUnicode_Type
+ #define PyString_Check PyUnicode_Check
+ #define PyString_CheckExact PyUnicode_CheckExact
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PyBytesObject PyStringObject
+ #define PyBytes_Type PyString_Type
+ #define PyBytes_Check PyString_Check
+ #define PyBytes_CheckExact PyString_CheckExact
+ #define PyBytes_FromString PyString_FromString
+ #define PyBytes_FromStringAndSize PyString_FromStringAndSize
+ #define PyBytes_FromFormat PyString_FromFormat
+ #define PyBytes_DecodeEscape PyString_DecodeEscape
+ #define PyBytes_AsString PyString_AsString
+ #define PyBytes_AsStringAndSize PyString_AsStringAndSize
+ #define PyBytes_Size PyString_Size
+ #define PyBytes_AS_STRING PyString_AS_STRING
+ #define PyBytes_GET_SIZE PyString_GET_SIZE
+ #define PyBytes_Repr PyString_Repr
+ #define PyBytes_Concat PyString_Concat
+ #define PyBytes_ConcatAndDel PyString_ConcatAndDel
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PySet_Check(obj) PyObject_TypeCheck(obj, &PySet_Type)
+ #define PyFrozenSet_Check(obj) PyObject_TypeCheck(obj, &PyFrozenSet_Type)
+#endif
+#ifndef PySet_CheckExact
+ #define PySet_CheckExact(obj) (Py_TYPE(obj) == &PySet_Type)
+#endif
+
+#define __Pyx_TypeCheck(obj, type) PyObject_TypeCheck(obj, (PyTypeObject *)type)
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyIntObject PyLongObject
+ #define PyInt_Type PyLong_Type
+ #define PyInt_Check(op) PyLong_Check(op)
+ #define PyInt_CheckExact(op) PyLong_CheckExact(op)
+ #define PyInt_FromString PyLong_FromString
+ #define PyInt_FromUnicode PyLong_FromUnicode
+ #define PyInt_FromLong PyLong_FromLong
+ #define PyInt_FromSize_t PyLong_FromSize_t
+ #define PyInt_FromSsize_t PyLong_FromSsize_t
+ #define PyInt_AsLong PyLong_AsLong
+ #define PyInt_AS_LONG PyLong_AS_LONG
+ #define PyInt_AsSsize_t PyLong_AsSsize_t
+ #define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLongMask
+ #define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBoolObject PyLongObject
+#endif
+
+#if PY_VERSION_HEX < 0x03020000
+ typedef long Py_hash_t;
+ #define __Pyx_PyInt_FromHash_t PyInt_FromLong
+ #define __Pyx_PyInt_AsHash_t PyInt_AsLong
+#else
+ #define __Pyx_PyInt_FromHash_t PyInt_FromSsize_t
+ #define __Pyx_PyInt_AsHash_t PyInt_AsSsize_t
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_TrueDivide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceTrueDivide(x,y)
+#else
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_Divide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceDivide(x,y)
+#endif
+
+#if (PY_MAJOR_VERSION < 3) || (PY_VERSION_HEX >= 0x03010300)
+ #define __Pyx_PySequence_GetSlice(obj, a, b) PySequence_GetSlice(obj, a, b)
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) PySequence_SetSlice(obj, a, b, value)
+ #define __Pyx_PySequence_DelSlice(obj, a, b) PySequence_DelSlice(obj, a, b)
+#else
+ #define __Pyx_PySequence_GetSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), (PyObject*)0) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_GetSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object is unsliceable", (obj)->ob_type->tp_name), (PyObject*)0)))
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_SetSlice(obj, a, b, value)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice assignment", (obj)->ob_type->tp_name), -1)))
+ #define __Pyx_PySequence_DelSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_DelSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice deletion", (obj)->ob_type->tp_name), -1)))
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyMethod_New(func, self, klass) ((self) ? PyMethod_New(func, self) : PyInstanceMethod_New(func))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),((char *)(n)))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),((char *)(n)),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),((char *)(n)))
+#else
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),(n))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),(n),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),(n))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_NAMESTR(n) ((char *)(n))
+ #define __Pyx_DOCSTR(n) ((char *)(n))
+#else
+ #define __Pyx_NAMESTR(n) (n)
+ #define __Pyx_DOCSTR(n) (n)
+#endif
+
+#ifndef __PYX_EXTERN_C
+ #ifdef __cplusplus
+ #define __PYX_EXTERN_C extern "C"
+ #else
+ #define __PYX_EXTERN_C extern
+ #endif
+#endif
+
+#if defined(WIN32) || defined(MS_WINDOWS)
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+#define __PYX_HAVE__twisted__python___epoll
+#define __PYX_HAVE_API__twisted__python___epoll
+#include "stdio.h"
+#include "errno.h"
+#include "string.h"
+#include "stdint.h"
+#include "sys/epoll.h"
+#ifdef _OPENMP
+#include <omp.h>
+#endif /* _OPENMP */
+
+#ifdef PYREX_WITHOUT_ASSERTIONS
+#define CYTHON_WITHOUT_ASSERTIONS
+#endif
+
+
+/* inline attribute */
+#ifndef CYTHON_INLINE
+ #if defined(__GNUC__)
+ #define CYTHON_INLINE __inline__
+ #elif defined(_MSC_VER)
+ #define CYTHON_INLINE __inline
+ #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+ #define CYTHON_INLINE inline
+ #else
+ #define CYTHON_INLINE
+ #endif
+#endif
+
+/* unused attribute */
+#ifndef CYTHON_UNUSED
+# if defined(__GNUC__)
+# if !(defined(__cplusplus)) || (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+# elif defined(__ICC) || defined(__INTEL_COMPILER)
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+#endif
+
+typedef struct {PyObject **p; char *s; const long n; const char* encoding; const char is_unicode; const char is_str; const char intern; } __Pyx_StringTabEntry; /*proto*/
+
+
+/* Type Conversion Predeclarations */
+
+#define __Pyx_PyBytes_FromUString(s) PyBytes_FromString((char*)s)
+#define __Pyx_PyBytes_AsUString(s) ((unsigned char*) PyBytes_AsString(s))
+
+#define __Pyx_Owned_Py_None(b) (Py_INCREF(Py_None), Py_None)
+#define __Pyx_PyBool_FromLong(b) ((b) ? (Py_INCREF(Py_True), Py_True) : (Py_INCREF(Py_False), Py_False))
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject*);
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x);
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject*);
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t);
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject*);
+
+#define __pyx_PyFloat_AsDouble(x) (PyFloat_CheckExact(x) ? PyFloat_AS_DOUBLE(x) : PyFloat_AsDouble(x))
+
+
+#ifdef __GNUC__
+ /* Test for GCC > 2.95 */
+ #if __GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95))
+ #define likely(x) __builtin_expect(!!(x), 1)
+ #define unlikely(x) __builtin_expect(!!(x), 0)
+ #else /* __GNUC__ > 2 ... */
+ #define likely(x) (x)
+ #define unlikely(x) (x)
+ #endif /* __GNUC__ > 2 ... */
+#else /* __GNUC__ */
+ #define likely(x) (x)
+ #define unlikely(x) (x)
+#endif /* __GNUC__ */
+
+static PyObject *__pyx_m;
+static PyObject *__pyx_b;
+static PyObject *__pyx_empty_tuple;
+static PyObject *__pyx_empty_bytes;
+static int __pyx_lineno;
+static int __pyx_clineno = 0;
+static const char * __pyx_cfilenm= __FILE__;
+static const char *__pyx_filename;
+
+
+static const char *__pyx_f[] = {
+ "_epoll.pyx",
+};
+
+/*--- Type declarations ---*/
+struct __pyx_obj_7twisted_6python_6_epoll_epoll;
+
+/* "twisted/python/_epoll.pyx":106
+ * free(events)
+ *
+ * cdef class epoll: # <<<<<<<<<<<<<<
+ * """
+ * Represent a set of file descriptors being monitored for events.
+ */
+struct __pyx_obj_7twisted_6python_6_epoll_epoll {
+ PyObject_HEAD
+ int fd;
+ int initialized;
+};
+
+
+#ifndef CYTHON_REFNANNY
+ #define CYTHON_REFNANNY 0
+#endif
+
+#if CYTHON_REFNANNY
+ typedef struct {
+ void (*INCREF)(void*, PyObject*, int);
+ void (*DECREF)(void*, PyObject*, int);
+ void (*GOTREF)(void*, PyObject*, int);
+ void (*GIVEREF)(void*, PyObject*, int);
+ void* (*SetupContext)(const char*, int, const char*);
+ void (*FinishContext)(void**);
+ } __Pyx_RefNannyAPIStruct;
+ static __Pyx_RefNannyAPIStruct *__Pyx_RefNanny = NULL;
+ static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname); /*proto*/
+ #define __Pyx_RefNannyDeclarations void *__pyx_refnanny = NULL;
+ #define __Pyx_RefNannySetupContext(name) __pyx_refnanny = __Pyx_RefNanny->SetupContext((name), __LINE__, __FILE__)
+ #define __Pyx_RefNannyFinishContext() __Pyx_RefNanny->FinishContext(&__pyx_refnanny)
+ #define __Pyx_INCREF(r) __Pyx_RefNanny->INCREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_DECREF(r) __Pyx_RefNanny->DECREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GOTREF(r) __Pyx_RefNanny->GOTREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GIVEREF(r) __Pyx_RefNanny->GIVEREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_XINCREF(r) do { if((r) != NULL) {__Pyx_INCREF(r); }} while(0)
+ #define __Pyx_XDECREF(r) do { if((r) != NULL) {__Pyx_DECREF(r); }} while(0)
+ #define __Pyx_XGOTREF(r) do { if((r) != NULL) {__Pyx_GOTREF(r); }} while(0)
+ #define __Pyx_XGIVEREF(r) do { if((r) != NULL) {__Pyx_GIVEREF(r);}} while(0)
+#else
+ #define __Pyx_RefNannyDeclarations
+ #define __Pyx_RefNannySetupContext(name)
+ #define __Pyx_RefNannyFinishContext()
+ #define __Pyx_INCREF(r) Py_INCREF(r)
+ #define __Pyx_DECREF(r) Py_DECREF(r)
+ #define __Pyx_GOTREF(r)
+ #define __Pyx_GIVEREF(r)
+ #define __Pyx_XINCREF(r) Py_XINCREF(r)
+ #define __Pyx_XDECREF(r) Py_XDECREF(r)
+ #define __Pyx_XGOTREF(r)
+ #define __Pyx_XGIVEREF(r)
+#endif /* CYTHON_REFNANNY */
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name); /*proto*/
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb); /*proto*/
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb); /*proto*/
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause); /*proto*/
+
+static void __Pyx_RaiseDoubleKeywordsError(
+ const char* func_name, PyObject* kw_name); /*proto*/
+
+static int __Pyx_ParseOptionalKeywords(PyObject *kwds, PyObject **argnames[], PyObject *kwds2, PyObject *values[], Py_ssize_t num_pos_args, const char* function_name); /*proto*/
+
+static void __Pyx_RaiseArgtupleInvalid(const char* func_name, int exact,
+ Py_ssize_t num_min, Py_ssize_t num_max, Py_ssize_t num_found); /*proto*/
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject *);
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject *);
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject *);
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject *);
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject *);
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject *);
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject *);
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject *);
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject *);
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject *);
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject *);
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject *);
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject *);
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject *);
+
+static int __Pyx_check_binary_version(void);
+
+static void __Pyx_AddTraceback(const char *funcname, int __pyx_clineno,
+ int __pyx_lineno, const char *__pyx_filename); /*proto*/
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t); /*proto*/
+
+/* Module declarations from 'twisted.python._epoll' */
+static PyTypeObject *__pyx_ptype_7twisted_6python_6_epoll_epoll = 0;
+static PyObject *__pyx_f_7twisted_6python_6_epoll_call_epoll_wait(int, unsigned int, int); /*proto*/
+#define __Pyx_MODULE_NAME "twisted.python._epoll"
+int __pyx_module_is_main_twisted__python___epoll = 0;
+
+/* Implementation of 'twisted.python._epoll' */
+static PyObject *__pyx_builtin_IOError;
+static char __pyx_k_1[] = "\nInterface to epoll I/O event notification facility.\n";
+static char __pyx_k__ET[] = "ET";
+static char __pyx_k__IN[] = "IN";
+static char __pyx_k__fd[] = "fd";
+static char __pyx_k__op[] = "op";
+static char __pyx_k__ERR[] = "ERR";
+static char __pyx_k__HUP[] = "HUP";
+static char __pyx_k__MSG[] = "MSG";
+static char __pyx_k__OUT[] = "OUT";
+static char __pyx_k__PRI[] = "PRI";
+static char __pyx_k__size[] = "size";
+static char __pyx_k__RDBAND[] = "RDBAND";
+static char __pyx_k__RDNORM[] = "RDNORM";
+static char __pyx_k__WRBAND[] = "WRBAND";
+static char __pyx_k__WRNORM[] = "WRNORM";
+static char __pyx_k__events[] = "events";
+static char __pyx_k__CTL_ADD[] = "CTL_ADD";
+static char __pyx_k__CTL_DEL[] = "CTL_DEL";
+static char __pyx_k__CTL_MOD[] = "CTL_MOD";
+static char __pyx_k__EPOLLET[] = "EPOLLET";
+static char __pyx_k__EPOLLIN[] = "EPOLLIN";
+static char __pyx_k__IOError[] = "IOError";
+static char __pyx_k__timeout[] = "timeout";
+static char __pyx_k__EPOLLERR[] = "EPOLLERR";
+static char __pyx_k__EPOLLHUP[] = "EPOLLHUP";
+static char __pyx_k__EPOLLMSG[] = "EPOLLMSG";
+static char __pyx_k__EPOLLOUT[] = "EPOLLOUT";
+static char __pyx_k__EPOLLPRI[] = "EPOLLPRI";
+static char __pyx_k____main__[] = "__main__";
+static char __pyx_k____test__[] = "__test__";
+static char __pyx_k__maxevents[] = "maxevents";
+static char __pyx_k__EPOLLRDBAND[] = "EPOLLRDBAND";
+static char __pyx_k__EPOLLRDNORM[] = "EPOLLRDNORM";
+static char __pyx_k__EPOLLWRBAND[] = "EPOLLWRBAND";
+static char __pyx_k__EPOLLWRNORM[] = "EPOLLWRNORM";
+static PyObject *__pyx_n_s__CTL_ADD;
+static PyObject *__pyx_n_s__CTL_DEL;
+static PyObject *__pyx_n_s__CTL_MOD;
+static PyObject *__pyx_n_s__EPOLLERR;
+static PyObject *__pyx_n_s__EPOLLET;
+static PyObject *__pyx_n_s__EPOLLHUP;
+static PyObject *__pyx_n_s__EPOLLIN;
+static PyObject *__pyx_n_s__EPOLLMSG;
+static PyObject *__pyx_n_s__EPOLLOUT;
+static PyObject *__pyx_n_s__EPOLLPRI;
+static PyObject *__pyx_n_s__EPOLLRDBAND;
+static PyObject *__pyx_n_s__EPOLLRDNORM;
+static PyObject *__pyx_n_s__EPOLLWRBAND;
+static PyObject *__pyx_n_s__EPOLLWRNORM;
+static PyObject *__pyx_n_s__ERR;
+static PyObject *__pyx_n_s__ET;
+static PyObject *__pyx_n_s__HUP;
+static PyObject *__pyx_n_s__IN;
+static PyObject *__pyx_n_s__IOError;
+static PyObject *__pyx_n_s__MSG;
+static PyObject *__pyx_n_s__OUT;
+static PyObject *__pyx_n_s__PRI;
+static PyObject *__pyx_n_s__RDBAND;
+static PyObject *__pyx_n_s__RDNORM;
+static PyObject *__pyx_n_s__WRBAND;
+static PyObject *__pyx_n_s__WRNORM;
+static PyObject *__pyx_n_s____main__;
+static PyObject *__pyx_n_s____test__;
+static PyObject *__pyx_n_s__events;
+static PyObject *__pyx_n_s__fd;
+static PyObject *__pyx_n_s__maxevents;
+static PyObject *__pyx_n_s__op;
+static PyObject *__pyx_n_s__size;
+static PyObject *__pyx_n_s__timeout;
+
+/* "twisted/python/_epoll.pyx":68
+ * cdef extern void PyEval_RestoreThread(PyThreadState*)
+ *
+ * cdef call_epoll_wait(int fd, unsigned int maxevents, int timeout_msec): # <<<<<<<<<<<<<<
+ * """
+ * Wait for an I/O event, wrap epoll_wait(2).
+ */
+
+static PyObject *__pyx_f_7twisted_6python_6_epoll_call_epoll_wait(int __pyx_v_fd, unsigned int __pyx_v_maxevents, int __pyx_v_timeout_msec) {
+ struct epoll_event *__pyx_v_events;
+ int __pyx_v_result;
+ int __pyx_v_nbytes;
+ PyThreadState *__pyx_v__save;
+ PyObject *__pyx_v_results = NULL;
+ long __pyx_v_i;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_t_5;
+ int __pyx_t_6;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("call_epoll_wait");
+
+ /* "twisted/python/_epoll.pyx":89
+ * cdef PyThreadState *_save
+ *
+ * nbytes = sizeof(epoll_event) * maxevents # <<<<<<<<<<<<<<
+ * events = <epoll_event*>malloc(nbytes)
+ * memset(events, 0, nbytes)
+ */
+ __pyx_v_nbytes = ((sizeof(struct epoll_event)) * __pyx_v_maxevents);
+
+ /* "twisted/python/_epoll.pyx":90
+ *
+ * nbytes = sizeof(epoll_event) * maxevents
+ * events = <epoll_event*>malloc(nbytes) # <<<<<<<<<<<<<<
+ * memset(events, 0, nbytes)
+ * try:
+ */
+ __pyx_v_events = ((struct epoll_event *)malloc(__pyx_v_nbytes));
+
+ /* "twisted/python/_epoll.pyx":91
+ * nbytes = sizeof(epoll_event) * maxevents
+ * events = <epoll_event*>malloc(nbytes)
+ * memset(events, 0, nbytes) # <<<<<<<<<<<<<<
+ * try:
+ * _save = PyEval_SaveThread()
+ */
+ memset(__pyx_v_events, 0, __pyx_v_nbytes);
+
+ /* "twisted/python/_epoll.pyx":92
+ * events = <epoll_event*>malloc(nbytes)
+ * memset(events, 0, nbytes)
+ * try: # <<<<<<<<<<<<<<
+ * _save = PyEval_SaveThread()
+ * result = epoll_wait(fd, events, maxevents, timeout_msec)
+ */
+ /*try:*/ {
+
+ /* "twisted/python/_epoll.pyx":93
+ * memset(events, 0, nbytes)
+ * try:
+ * _save = PyEval_SaveThread() # <<<<<<<<<<<<<<
+ * result = epoll_wait(fd, events, maxevents, timeout_msec)
+ * PyEval_RestoreThread(_save)
+ */
+ __pyx_v__save = PyEval_SaveThread();
+
+ /* "twisted/python/_epoll.pyx":94
+ * try:
+ * _save = PyEval_SaveThread()
+ * result = epoll_wait(fd, events, maxevents, timeout_msec) # <<<<<<<<<<<<<<
+ * PyEval_RestoreThread(_save)
+ *
+ */
+ __pyx_v_result = epoll_wait(__pyx_v_fd, __pyx_v_events, __pyx_v_maxevents, __pyx_v_timeout_msec);
+
+ /* "twisted/python/_epoll.pyx":95
+ * _save = PyEval_SaveThread()
+ * result = epoll_wait(fd, events, maxevents, timeout_msec)
+ * PyEval_RestoreThread(_save) # <<<<<<<<<<<<<<
+ *
+ * if result == -1:
+ */
+ PyEval_RestoreThread(__pyx_v__save);
+
+ /* "twisted/python/_epoll.pyx":97
+ * PyEval_RestoreThread(_save)
+ *
+ * if result == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ * results = []
+ */
+ __pyx_t_1 = (__pyx_v_result == -1);
+ if (__pyx_t_1) {
+
+ /* "twisted/python/_epoll.pyx":98
+ *
+ * if result == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ * results = []
+ * for i from 0 <= i < result:
+ */
+ __pyx_t_2 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, ((PyObject *)__pyx_t_3));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_3));
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_3 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_t_3, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "twisted/python/_epoll.pyx":99
+ * if result == -1:
+ * raise IOError(errno, strerror(errno))
+ * results = [] # <<<<<<<<<<<<<<
+ * for i from 0 <= i < result:
+ * results.append((events[i].data.fd, <int>events[i].events))
+ */
+ __pyx_t_3 = PyList_New(0); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 99; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __pyx_v_results = __pyx_t_3;
+ __pyx_t_3 = 0;
+
+ /* "twisted/python/_epoll.pyx":100
+ * raise IOError(errno, strerror(errno))
+ * results = []
+ * for i from 0 <= i < result: # <<<<<<<<<<<<<<
+ * results.append((events[i].data.fd, <int>events[i].events))
+ * return results
+ */
+ __pyx_t_5 = __pyx_v_result;
+ for (__pyx_v_i = 0; __pyx_v_i < __pyx_t_5; __pyx_v_i++) {
+
+ /* "twisted/python/_epoll.pyx":101
+ * results = []
+ * for i from 0 <= i < result:
+ * results.append((events[i].data.fd, <int>events[i].events)) # <<<<<<<<<<<<<<
+ * return results
+ * finally:
+ */
+ if (unlikely(((PyObject *)__pyx_v_results) == Py_None)) {
+ PyErr_Format(PyExc_AttributeError, "'NoneType' object has no attribute '%s'", "append"); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 101; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ }
+ __pyx_t_3 = PyInt_FromLong((__pyx_v_events[__pyx_v_i]).data.fd); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 101; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __pyx_t_4 = PyInt_FromLong(((int)(__pyx_v_events[__pyx_v_i]).events)); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 101; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __pyx_t_2 = PyTuple_New(2); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 101; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_2));
+ PyTuple_SET_ITEM(__pyx_t_2, 0, __pyx_t_3);
+ __Pyx_GIVEREF(__pyx_t_3);
+ PyTuple_SET_ITEM(__pyx_t_2, 1, __pyx_t_4);
+ __Pyx_GIVEREF(__pyx_t_4);
+ __pyx_t_3 = 0;
+ __pyx_t_4 = 0;
+ __pyx_t_6 = PyList_Append(__pyx_v_results, ((PyObject *)__pyx_t_2)); if (unlikely(__pyx_t_6 == -1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 101; __pyx_clineno = __LINE__; goto __pyx_L4;}
+ __Pyx_DECREF(((PyObject *)__pyx_t_2)); __pyx_t_2 = 0;
+ }
+
+ /* "twisted/python/_epoll.pyx":102
+ * for i from 0 <= i < result:
+ * results.append((events[i].data.fd, <int>events[i].events))
+ * return results # <<<<<<<<<<<<<<
+ * finally:
+ * free(events)
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __Pyx_INCREF(((PyObject *)__pyx_v_results));
+ __pyx_r = ((PyObject *)__pyx_v_results);
+ goto __pyx_L3;
+ }
+
+ /* "twisted/python/_epoll.pyx":104
+ * return results
+ * finally:
+ * free(events) # <<<<<<<<<<<<<<
+ *
+ * cdef class epoll:
+ */
+ /*finally:*/ {
+ int __pyx_why;
+ PyObject *__pyx_exc_type, *__pyx_exc_value, *__pyx_exc_tb;
+ int __pyx_exc_lineno;
+ __pyx_exc_type = 0; __pyx_exc_value = 0; __pyx_exc_tb = 0; __pyx_exc_lineno = 0;
+ __pyx_why = 0; goto __pyx_L5;
+ __pyx_L3: __pyx_exc_type = 0; __pyx_exc_value = 0; __pyx_exc_tb = 0; __pyx_exc_lineno = 0;
+ __pyx_why = 3; goto __pyx_L5;
+ __pyx_L4: {
+ __pyx_why = 4;
+ __Pyx_XDECREF(__pyx_t_3); __pyx_t_3 = 0;
+ __Pyx_XDECREF(__pyx_t_4); __pyx_t_4 = 0;
+ __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0;
+ __Pyx_ErrFetch(&__pyx_exc_type, &__pyx_exc_value, &__pyx_exc_tb);
+ __pyx_exc_lineno = __pyx_lineno;
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+ free(__pyx_v_events);
+ switch (__pyx_why) {
+ case 3: goto __pyx_L0;
+ case 4: {
+ __Pyx_ErrRestore(__pyx_exc_type, __pyx_exc_value, __pyx_exc_tb);
+ __pyx_lineno = __pyx_exc_lineno;
+ __pyx_exc_type = 0;
+ __pyx_exc_value = 0;
+ __pyx_exc_tb = 0;
+ goto __pyx_L1_error;
+ }
+ }
+ }
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("twisted.python._epoll.call_epoll_wait", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = 0;
+ __pyx_L0:;
+ __Pyx_XDECREF(__pyx_v_results);
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":114
+ * cdef int initialized
+ *
+ * def __init__(self, int size=1023): # <<<<<<<<<<<<<<
+ * """
+ * The constructor arguments are compatible with select.poll.__init__.
+ */
+
+static int __pyx_pf_7twisted_6python_6_epoll_5epoll___init__(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll___init__[] = "\n The constructor arguments are compatible with select.poll.__init__.\n ";
+struct wrapperbase __pyx_wrapperbase_7twisted_6python_6_epoll_5epoll___init__;
+static int __pyx_pf_7twisted_6python_6_epoll_5epoll___init__(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ int __pyx_v_size;
+ int __pyx_r;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__size,0};
+ __Pyx_RefNannySetupContext("__init__");
+ {
+ PyObject* values[1] = {0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__size);
+ if (value) { values[0] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "__init__") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 114; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ if (values[0]) {
+ __pyx_v_size = __Pyx_PyInt_AsInt(values[0]); if (unlikely((__pyx_v_size == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 114; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_size = ((int)1023);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("__init__", 0, 0, 1, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 114; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.__init__", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return -1;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":118
+ * The constructor arguments are compatible with select.poll.__init__.
+ * """
+ * self.fd = epoll_create(size) # <<<<<<<<<<<<<<
+ * if self.fd == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ ((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd = epoll_create(__pyx_v_size);
+
+ /* "twisted/python/_epoll.pyx":119
+ * """
+ * self.fd = epoll_create(size)
+ * if self.fd == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ * self.initialized = 1
+ */
+ __pyx_t_1 = (((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd == -1);
+ if (__pyx_t_1) {
+
+ /* "twisted/python/_epoll.pyx":120
+ * self.fd = epoll_create(size)
+ * if self.fd == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ * self.initialized = 1
+ *
+ */
+ __pyx_t_2 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 120; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 120; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 120; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, ((PyObject *)__pyx_t_3));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_3));
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_3 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 120; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_t_3, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 120; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "twisted/python/_epoll.pyx":121
+ * if self.fd == -1:
+ * raise IOError(errno, strerror(errno))
+ * self.initialized = 1 # <<<<<<<<<<<<<<
+ *
+ * def __dealloc__(self):
+ */
+ ((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->initialized = 1;
+
+ __pyx_r = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.__init__", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = -1;
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":123
+ * self.initialized = 1
+ *
+ * def __dealloc__(self): # <<<<<<<<<<<<<<
+ * if self.initialized:
+ * close(self.fd)
+ */
+
+static void __pyx_pf_7twisted_6python_6_epoll_5epoll_1__dealloc__(PyObject *__pyx_v_self); /*proto*/
+static void __pyx_pf_7twisted_6python_6_epoll_5epoll_1__dealloc__(PyObject *__pyx_v_self) {
+ __Pyx_RefNannyDeclarations
+ __Pyx_RefNannySetupContext("__dealloc__");
+
+ /* "twisted/python/_epoll.pyx":124
+ *
+ * def __dealloc__(self):
+ * if self.initialized: # <<<<<<<<<<<<<<
+ * close(self.fd)
+ * self.initialized = 0
+ */
+ if (((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->initialized) {
+
+ /* "twisted/python/_epoll.pyx":125
+ * def __dealloc__(self):
+ * if self.initialized:
+ * close(self.fd) # <<<<<<<<<<<<<<
+ * self.initialized = 0
+ *
+ */
+ close(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd);
+
+ /* "twisted/python/_epoll.pyx":126
+ * if self.initialized:
+ * close(self.fd)
+ * self.initialized = 0 # <<<<<<<<<<<<<<
+ *
+ * def close(self):
+ */
+ ((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->initialized = 0;
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ __Pyx_RefNannyFinishContext();
+}
+
+/* "twisted/python/_epoll.pyx":128
+ * self.initialized = 0
+ *
+ * def close(self): # <<<<<<<<<<<<<<
+ * """
+ * Close the epoll file descriptor.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_2close(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_2close[] = "\n Close the epoll file descriptor.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_2close(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused) {
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("close");
+
+ /* "twisted/python/_epoll.pyx":132
+ * Close the epoll file descriptor.
+ * """
+ * if self.initialized: # <<<<<<<<<<<<<<
+ * if close(self.fd) == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ if (((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->initialized) {
+
+ /* "twisted/python/_epoll.pyx":133
+ * """
+ * if self.initialized:
+ * if close(self.fd) == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ * self.initialized = 0
+ */
+ __pyx_t_1 = (close(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd) == -1);
+ if (__pyx_t_1) {
+
+ /* "twisted/python/_epoll.pyx":134
+ * if self.initialized:
+ * if close(self.fd) == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ * self.initialized = 0
+ *
+ */
+ __pyx_t_2 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 134; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 134; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 134; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, ((PyObject *)__pyx_t_3));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_3));
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_3 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 134; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_t_3, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 134; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ /* "twisted/python/_epoll.pyx":135
+ * if close(self.fd) == -1:
+ * raise IOError(errno, strerror(errno))
+ * self.initialized = 0 # <<<<<<<<<<<<<<
+ *
+ * def fileno(self):
+ */
+ ((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->initialized = 0;
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.close", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":137
+ * self.initialized = 0
+ *
+ * def fileno(self): # <<<<<<<<<<<<<<
+ * """
+ * Return the epoll file descriptor number.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_3fileno(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_3fileno[] = "\n Return the epoll file descriptor number.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_3fileno(PyObject *__pyx_v_self, CYTHON_UNUSED PyObject *unused) {
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("fileno");
+
+ /* "twisted/python/_epoll.pyx":141
+ * Return the epoll file descriptor number.
+ * """
+ * return self.fd # <<<<<<<<<<<<<<
+ *
+ * def register(self, int fd, int events):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = PyInt_FromLong(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 141; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_r = __pyx_t_1;
+ __pyx_t_1 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.fileno", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":143
+ * return self.fd
+ *
+ * def register(self, int fd, int events): # <<<<<<<<<<<<<<
+ * """
+ * Add (register) a file descriptor to be monitored by self.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_4register(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_4register[] = "\n Add (register) a file descriptor to be monitored by self.\n\n This method is compatible with select.epoll.register in Python 2.6.\n\n Wrap epoll_ctl(2).\n\n @type fd: C{int}\n @param fd: File descriptor to modify\n\n @type events: C{int}\n @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.\n\n @raise IOError: Raised if the underlying epoll_ctl() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_4register(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ int __pyx_v_fd;
+ int __pyx_v_events;
+ int __pyx_v_result;
+ struct epoll_event __pyx_v_evt;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_t_2;
+ int __pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__fd,&__pyx_n_s__events,0};
+ __Pyx_RefNannySetupContext("register");
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__fd);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__events);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("register", 1, 2, 2, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "register") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ }
+ __pyx_v_fd = __Pyx_PyInt_AsInt(values[0]); if (unlikely((__pyx_v_fd == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_events = __Pyx_PyInt_AsInt(values[1]); if (unlikely((__pyx_v_events == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("register", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 143; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.register", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":161
+ * cdef int result
+ * cdef epoll_event evt
+ * evt.events = events # <<<<<<<<<<<<<<
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_ADD, fd, &evt)
+ */
+ __pyx_v_evt.events = __pyx_v_events;
+
+ /* "twisted/python/_epoll.pyx":162
+ * cdef epoll_event evt
+ * evt.events = events
+ * evt.data.fd = fd # <<<<<<<<<<<<<<
+ * result = epoll_ctl(self.fd, CTL_ADD, fd, &evt)
+ * if result == -1:
+ */
+ __pyx_v_evt.data.fd = __pyx_v_fd;
+
+ /* "twisted/python/_epoll.pyx":163
+ * evt.events = events
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_ADD, fd, &evt) # <<<<<<<<<<<<<<
+ * if result == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ __pyx_t_1 = __Pyx_GetName(__pyx_m, __pyx_n_s__CTL_ADD); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 163; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_2 = __Pyx_PyInt_AsInt(__pyx_t_1); if (unlikely((__pyx_t_2 == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 163; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_v_result = epoll_ctl(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_t_2, __pyx_v_fd, (&__pyx_v_evt));
+
+ /* "twisted/python/_epoll.pyx":164
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_ADD, fd, &evt)
+ * if result == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ *
+ */
+ __pyx_t_3 = (__pyx_v_result == -1);
+ if (__pyx_t_3) {
+
+ /* "twisted/python/_epoll.pyx":165
+ * result = epoll_ctl(self.fd, CTL_ADD, fd, &evt)
+ * if result == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ *
+ * def unregister(self, int fd):
+ */
+ __pyx_t_1 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 165; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_4 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 165; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ __pyx_t_5 = PyTuple_New(2); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 165; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_1);
+ __Pyx_GIVEREF(__pyx_t_1);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, ((PyObject *)__pyx_t_4));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_4));
+ __pyx_t_1 = 0;
+ __pyx_t_4 = 0;
+ __pyx_t_4 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_5), NULL); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 165; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __Pyx_DECREF(((PyObject *)__pyx_t_5)); __pyx_t_5 = 0;
+ __Pyx_Raise(__pyx_t_4, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 165; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.register", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":167
+ * raise IOError(errno, strerror(errno))
+ *
+ * def unregister(self, int fd): # <<<<<<<<<<<<<<
+ * """
+ * Remove (unregister) a file descriptor monitored by self.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_5unregister(PyObject *__pyx_v_self, PyObject *__pyx_arg_fd); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_5unregister[] = "\n Remove (unregister) a file descriptor monitored by self.\n\n This method is compatible with select.epoll.unregister in Python 2.6.\n\n Wrap epoll_ctl(2).\n\n @type fd: C{int}\n @param fd: File descriptor to modify\n\n @raise IOError: Raised if the underlying epoll_ctl() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_5unregister(PyObject *__pyx_v_self, PyObject *__pyx_arg_fd) {
+ int __pyx_v_fd;
+ int __pyx_v_result;
+ struct epoll_event __pyx_v_evt;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_t_2;
+ int __pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ __Pyx_RefNannySetupContext("unregister");
+ assert(__pyx_arg_fd); {
+ __pyx_v_fd = __Pyx_PyInt_AsInt(__pyx_arg_fd); if (unlikely((__pyx_v_fd == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 167; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.unregister", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":183
+ * cdef epoll_event evt
+ * # We don't have to fill evt.events for CTL_DEL.
+ * evt.data.fd = fd # <<<<<<<<<<<<<<
+ * result = epoll_ctl(self.fd, CTL_DEL, fd, &evt)
+ * if result == -1:
+ */
+ __pyx_v_evt.data.fd = __pyx_v_fd;
+
+ /* "twisted/python/_epoll.pyx":184
+ * # We don't have to fill evt.events for CTL_DEL.
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_DEL, fd, &evt) # <<<<<<<<<<<<<<
+ * if result == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ __pyx_t_1 = __Pyx_GetName(__pyx_m, __pyx_n_s__CTL_DEL); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 184; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_2 = __Pyx_PyInt_AsInt(__pyx_t_1); if (unlikely((__pyx_t_2 == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 184; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_v_result = epoll_ctl(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_t_2, __pyx_v_fd, (&__pyx_v_evt));
+
+ /* "twisted/python/_epoll.pyx":185
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_DEL, fd, &evt)
+ * if result == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ *
+ */
+ __pyx_t_3 = (__pyx_v_result == -1);
+ if (__pyx_t_3) {
+
+ /* "twisted/python/_epoll.pyx":186
+ * result = epoll_ctl(self.fd, CTL_DEL, fd, &evt)
+ * if result == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ *
+ * def modify(self, int fd, int events):
+ */
+ __pyx_t_1 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 186; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_4 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 186; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ __pyx_t_5 = PyTuple_New(2); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 186; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_1);
+ __Pyx_GIVEREF(__pyx_t_1);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, ((PyObject *)__pyx_t_4));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_4));
+ __pyx_t_1 = 0;
+ __pyx_t_4 = 0;
+ __pyx_t_4 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_5), NULL); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 186; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __Pyx_DECREF(((PyObject *)__pyx_t_5)); __pyx_t_5 = 0;
+ __Pyx_Raise(__pyx_t_4, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 186; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L5;
+ }
+ __pyx_L5:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.unregister", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":188
+ * raise IOError(errno, strerror(errno))
+ *
+ * def modify(self, int fd, int events): # <<<<<<<<<<<<<<
+ * """
+ * Modify the modified state of a file descriptor monitored by self.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_6modify(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_6modify[] = "\n Modify the modified state of a file descriptor monitored by self.\n\n This method is compatible with select.epoll.modify in Python 2.6.\n\n Wrap epoll_ctl(2).\n\n @type fd: C{int}\n @param fd: File descriptor to modify\n\n @type events: C{int}\n @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.\n\n @raise IOError: Raised if the underlying epoll_ctl() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_6modify(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ int __pyx_v_fd;
+ int __pyx_v_events;
+ int __pyx_v_result;
+ struct epoll_event __pyx_v_evt;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_t_2;
+ int __pyx_t_3;
+ PyObject *__pyx_t_4 = NULL;
+ PyObject *__pyx_t_5 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__fd,&__pyx_n_s__events,0};
+ __Pyx_RefNannySetupContext("modify");
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__fd);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__events);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("modify", 1, 2, 2, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 188; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "modify") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 188; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ }
+ __pyx_v_fd = __Pyx_PyInt_AsInt(values[0]); if (unlikely((__pyx_v_fd == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 188; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_events = __Pyx_PyInt_AsInt(values[1]); if (unlikely((__pyx_v_events == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 188; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("modify", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 188; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.modify", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":206
+ * cdef int result
+ * cdef epoll_event evt
+ * evt.events = events # <<<<<<<<<<<<<<
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_MOD, fd, &evt)
+ */
+ __pyx_v_evt.events = __pyx_v_events;
+
+ /* "twisted/python/_epoll.pyx":207
+ * cdef epoll_event evt
+ * evt.events = events
+ * evt.data.fd = fd # <<<<<<<<<<<<<<
+ * result = epoll_ctl(self.fd, CTL_MOD, fd, &evt)
+ * if result == -1:
+ */
+ __pyx_v_evt.data.fd = __pyx_v_fd;
+
+ /* "twisted/python/_epoll.pyx":208
+ * evt.events = events
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_MOD, fd, &evt) # <<<<<<<<<<<<<<
+ * if result == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ __pyx_t_1 = __Pyx_GetName(__pyx_m, __pyx_n_s__CTL_MOD); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 208; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_2 = __Pyx_PyInt_AsInt(__pyx_t_1); if (unlikely((__pyx_t_2 == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 208; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_v_result = epoll_ctl(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_t_2, __pyx_v_fd, (&__pyx_v_evt));
+
+ /* "twisted/python/_epoll.pyx":209
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, CTL_MOD, fd, &evt)
+ * if result == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ *
+ */
+ __pyx_t_3 = (__pyx_v_result == -1);
+ if (__pyx_t_3) {
+
+ /* "twisted/python/_epoll.pyx":210
+ * result = epoll_ctl(self.fd, CTL_MOD, fd, &evt)
+ * if result == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ *
+ * def _control(self, int op, int fd, int events):
+ */
+ __pyx_t_1 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 210; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_4 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 210; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ __pyx_t_5 = PyTuple_New(2); if (unlikely(!__pyx_t_5)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 210; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_5));
+ PyTuple_SET_ITEM(__pyx_t_5, 0, __pyx_t_1);
+ __Pyx_GIVEREF(__pyx_t_1);
+ PyTuple_SET_ITEM(__pyx_t_5, 1, ((PyObject *)__pyx_t_4));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_4));
+ __pyx_t_1 = 0;
+ __pyx_t_4 = 0;
+ __pyx_t_4 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_5), NULL); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 210; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_4);
+ __Pyx_DECREF(((PyObject *)__pyx_t_5)); __pyx_t_5 = 0;
+ __Pyx_Raise(__pyx_t_4, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 210; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_XDECREF(__pyx_t_5);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.modify", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":212
+ * raise IOError(errno, strerror(errno))
+ *
+ * def _control(self, int op, int fd, int events): # <<<<<<<<<<<<<<
+ * """
+ * Modify the monitored state of a particular file descriptor.
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_7_control(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_7_control[] = "\n Modify the monitored state of a particular file descriptor.\n \n Wrap epoll_ctl(2).\n\n @type op: C{int}\n @param op: One of CTL_ADD, CTL_DEL, or CTL_MOD\n\n @type fd: C{int}\n @param fd: File descriptor to modify\n\n @type events: C{int}\n @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.\n\n @raise IOError: Raised if the underlying epoll_ctl() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_7_control(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ int __pyx_v_op;
+ int __pyx_v_fd;
+ int __pyx_v_events;
+ int __pyx_v_result;
+ struct epoll_event __pyx_v_evt;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ int __pyx_t_1;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ PyObject *__pyx_t_4 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__op,&__pyx_n_s__fd,&__pyx_n_s__events,0};
+ __Pyx_RefNannySetupContext("_control");
+ {
+ PyObject* values[3] = {0,0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 3: values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__op);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__fd);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("_control", 1, 3, 3, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ case 2:
+ values[2] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__events);
+ if (likely(values[2])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("_control", 1, 3, 3, 2); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "_control") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 3) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ values[2] = PyTuple_GET_ITEM(__pyx_args, 2);
+ }
+ __pyx_v_op = __Pyx_PyInt_AsInt(values[0]); if (unlikely((__pyx_v_op == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_fd = __Pyx_PyInt_AsInt(values[1]); if (unlikely((__pyx_v_fd == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_events = __Pyx_PyInt_AsInt(values[2]); if (unlikely((__pyx_v_events == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("_control", 1, 3, 3, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 212; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll._control", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":231
+ * cdef int result
+ * cdef epoll_event evt
+ * evt.events = events # <<<<<<<<<<<<<<
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, op, fd, &evt)
+ */
+ __pyx_v_evt.events = __pyx_v_events;
+
+ /* "twisted/python/_epoll.pyx":232
+ * cdef epoll_event evt
+ * evt.events = events
+ * evt.data.fd = fd # <<<<<<<<<<<<<<
+ * result = epoll_ctl(self.fd, op, fd, &evt)
+ * if result == -1:
+ */
+ __pyx_v_evt.data.fd = __pyx_v_fd;
+
+ /* "twisted/python/_epoll.pyx":233
+ * evt.events = events
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, op, fd, &evt) # <<<<<<<<<<<<<<
+ * if result == -1:
+ * raise IOError(errno, strerror(errno))
+ */
+ __pyx_v_result = epoll_ctl(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_v_op, __pyx_v_fd, (&__pyx_v_evt));
+
+ /* "twisted/python/_epoll.pyx":234
+ * evt.data.fd = fd
+ * result = epoll_ctl(self.fd, op, fd, &evt)
+ * if result == -1: # <<<<<<<<<<<<<<
+ * raise IOError(errno, strerror(errno))
+ *
+ */
+ __pyx_t_1 = (__pyx_v_result == -1);
+ if (__pyx_t_1) {
+
+ /* "twisted/python/_epoll.pyx":235
+ * result = epoll_ctl(self.fd, op, fd, &evt)
+ * if result == -1:
+ * raise IOError(errno, strerror(errno)) # <<<<<<<<<<<<<<
+ *
+ * def wait(self, unsigned int maxevents, int timeout):
+ */
+ __pyx_t_2 = PyInt_FromLong(errno); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __pyx_t_3 = PyBytes_FromString(strerror(errno)); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_3));
+ __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_4));
+ PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_2);
+ __Pyx_GIVEREF(__pyx_t_2);
+ PyTuple_SET_ITEM(__pyx_t_4, 1, ((PyObject *)__pyx_t_3));
+ __Pyx_GIVEREF(((PyObject *)__pyx_t_3));
+ __pyx_t_2 = 0;
+ __pyx_t_3 = 0;
+ __pyx_t_3 = PyObject_Call(__pyx_builtin_IOError, ((PyObject *)__pyx_t_4), NULL); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(((PyObject *)__pyx_t_4)); __pyx_t_4 = 0;
+ __Pyx_Raise(__pyx_t_3, 0, 0, 0);
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 235; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ goto __pyx_L6;
+ }
+ __pyx_L6:;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ __Pyx_XDECREF(__pyx_t_4);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll._control", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":237
+ * raise IOError(errno, strerror(errno))
+ *
+ * def wait(self, unsigned int maxevents, int timeout): # <<<<<<<<<<<<<<
+ * """
+ * Wait for an I/O event, wrap epoll_wait(2).
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_8wait(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_8wait[] = "\n Wait for an I/O event, wrap epoll_wait(2).\n\n @type maxevents: C{int}\n @param maxevents: Maximum number of events returned.\n\n @type timeout: C{int}\n @param timeout: Maximum time in milliseconds waiting for events. 0\n makes it return immediately whereas -1 makes it wait indefinitely.\n \n @raise IOError: Raised if the underlying epoll_wait() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_8wait(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ unsigned int __pyx_v_maxevents;
+ int __pyx_v_timeout;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__maxevents,&__pyx_n_s__timeout,0};
+ __Pyx_RefNannySetupContext("wait");
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ values[0] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__maxevents);
+ if (likely(values[0])) kw_args--;
+ else goto __pyx_L5_argtuple_error;
+ case 1:
+ values[1] = PyDict_GetItem(__pyx_kwds, __pyx_n_s__timeout);
+ if (likely(values[1])) kw_args--;
+ else {
+ __Pyx_RaiseArgtupleInvalid("wait", 1, 2, 2, 1); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "wait") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else if (PyTuple_GET_SIZE(__pyx_args) != 2) {
+ goto __pyx_L5_argtuple_error;
+ } else {
+ values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ }
+ __pyx_v_maxevents = __Pyx_PyInt_AsUnsignedInt(values[0]); if (unlikely((__pyx_v_maxevents == (unsigned int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_v_timeout = __Pyx_PyInt_AsInt(values[1]); if (unlikely((__pyx_v_timeout == (int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("wait", 1, 2, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 237; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.wait", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":250
+ * @raise IOError: Raised if the underlying epoll_wait() call fails.
+ * """
+ * return call_epoll_wait(self.fd, maxevents, timeout) # <<<<<<<<<<<<<<
+ *
+ * def poll(self, float timeout=-1, unsigned int maxevents=1024):
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = __pyx_f_7twisted_6python_6_epoll_call_epoll_wait(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_v_maxevents, __pyx_v_timeout); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 250; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_r = __pyx_t_1;
+ __pyx_t_1 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.wait", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+/* "twisted/python/_epoll.pyx":252
+ * return call_epoll_wait(self.fd, maxevents, timeout)
+ *
+ * def poll(self, float timeout=-1, unsigned int maxevents=1024): # <<<<<<<<<<<<<<
+ * """
+ * Wait for an I/O event, wrap epoll_wait(2).
+ */
+
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_9poll(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds); /*proto*/
+static char __pyx_doc_7twisted_6python_6_epoll_5epoll_9poll[] = "\n Wait for an I/O event, wrap epoll_wait(2).\n\n This method is compatible with select.epoll.poll in Python 2.6.\n\n @type maxevents: C{int}\n @param maxevents: Maximum number of events returned.\n\n @type timeout: C{int}\n @param timeout: Maximum time waiting for events. 0 makes it return\n immediately whereas -1 makes it wait indefinitely.\n \n @raise IOError: Raised if the underlying epoll_wait() call fails.\n ";
+static PyObject *__pyx_pf_7twisted_6python_6_epoll_5epoll_9poll(PyObject *__pyx_v_self, PyObject *__pyx_args, PyObject *__pyx_kwds) {
+ float __pyx_v_timeout;
+ unsigned int __pyx_v_maxevents;
+ PyObject *__pyx_r = NULL;
+ __Pyx_RefNannyDeclarations
+ PyObject *__pyx_t_1 = NULL;
+ int __pyx_lineno = 0;
+ const char *__pyx_filename = NULL;
+ int __pyx_clineno = 0;
+ static PyObject **__pyx_pyargnames[] = {&__pyx_n_s__timeout,&__pyx_n_s__maxevents,0};
+ __Pyx_RefNannySetupContext("poll");
+ {
+ PyObject* values[2] = {0,0};
+ if (unlikely(__pyx_kwds)) {
+ Py_ssize_t kw_args;
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ kw_args = PyDict_Size(__pyx_kwds);
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 0:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__timeout);
+ if (value) { values[0] = value; kw_args--; }
+ }
+ case 1:
+ if (kw_args > 0) {
+ PyObject* value = PyDict_GetItem(__pyx_kwds, __pyx_n_s__maxevents);
+ if (value) { values[1] = value; kw_args--; }
+ }
+ }
+ if (unlikely(kw_args > 0)) {
+ if (unlikely(__Pyx_ParseOptionalKeywords(__pyx_kwds, __pyx_pyargnames, 0, values, PyTuple_GET_SIZE(__pyx_args), "poll") < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 252; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ }
+ } else {
+ switch (PyTuple_GET_SIZE(__pyx_args)) {
+ case 2: values[1] = PyTuple_GET_ITEM(__pyx_args, 1);
+ case 1: values[0] = PyTuple_GET_ITEM(__pyx_args, 0);
+ case 0: break;
+ default: goto __pyx_L5_argtuple_error;
+ }
+ }
+ if (values[0]) {
+ __pyx_v_timeout = __pyx_PyFloat_AsDouble(values[0]); if (unlikely((__pyx_v_timeout == (float)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 252; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_timeout = ((float)-1.0);
+ }
+ if (values[1]) {
+ __pyx_v_maxevents = __Pyx_PyInt_AsUnsignedInt(values[1]); if (unlikely((__pyx_v_maxevents == (unsigned int)-1) && PyErr_Occurred())) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 252; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ } else {
+ __pyx_v_maxevents = ((unsigned int)1024);
+ }
+ }
+ goto __pyx_L4_argument_unpacking_done;
+ __pyx_L5_argtuple_error:;
+ __Pyx_RaiseArgtupleInvalid("poll", 0, 0, 2, PyTuple_GET_SIZE(__pyx_args)); {__pyx_filename = __pyx_f[0]; __pyx_lineno = 252; __pyx_clineno = __LINE__; goto __pyx_L3_error;}
+ __pyx_L3_error:;
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.poll", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __Pyx_RefNannyFinishContext();
+ return NULL;
+ __pyx_L4_argument_unpacking_done:;
+
+ /* "twisted/python/_epoll.pyx":267
+ * @raise IOError: Raised if the underlying epoll_wait() call fails.
+ * """
+ * return call_epoll_wait(self.fd, maxevents, <int>(timeout * 1000.0)) # <<<<<<<<<<<<<<
+ *
+ *
+ */
+ __Pyx_XDECREF(__pyx_r);
+ __pyx_t_1 = __pyx_f_7twisted_6python_6_epoll_call_epoll_wait(((struct __pyx_obj_7twisted_6python_6_epoll_epoll *)__pyx_v_self)->fd, __pyx_v_maxevents, ((int)(__pyx_v_timeout * 1000.0))); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 267; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_r = __pyx_t_1;
+ __pyx_t_1 = 0;
+ goto __pyx_L0;
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_AddTraceback("twisted.python._epoll.epoll.poll", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+static PyObject *__pyx_tp_new_7twisted_6python_6_epoll_epoll(PyTypeObject *t, PyObject *a, PyObject *k) {
+ PyObject *o = (*t->tp_alloc)(t, 0);
+ if (!o) return 0;
+ return o;
+}
+
+static void __pyx_tp_dealloc_7twisted_6python_6_epoll_epoll(PyObject *o) {
+ {
+ PyObject *etype, *eval, *etb;
+ PyErr_Fetch(&etype, &eval, &etb);
+ ++Py_REFCNT(o);
+ __pyx_pf_7twisted_6python_6_epoll_5epoll_1__dealloc__(o);
+ if (PyErr_Occurred()) PyErr_WriteUnraisable(o);
+ --Py_REFCNT(o);
+ PyErr_Restore(etype, eval, etb);
+ }
+ (*Py_TYPE(o)->tp_free)(o);
+}
+
+static PyMethodDef __pyx_methods_7twisted_6python_6_epoll_epoll[] = {
+ {__Pyx_NAMESTR("close"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_2close, METH_NOARGS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_2close)},
+ {__Pyx_NAMESTR("fileno"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_3fileno, METH_NOARGS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_3fileno)},
+ {__Pyx_NAMESTR("register"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_4register, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_4register)},
+ {__Pyx_NAMESTR("unregister"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_5unregister, METH_O, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_5unregister)},
+ {__Pyx_NAMESTR("modify"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_6modify, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_6modify)},
+ {__Pyx_NAMESTR("_control"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_7_control, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_7_control)},
+ {__Pyx_NAMESTR("wait"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_8wait, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_8wait)},
+ {__Pyx_NAMESTR("poll"), (PyCFunction)__pyx_pf_7twisted_6python_6_epoll_5epoll_9poll, METH_VARARGS|METH_KEYWORDS, __Pyx_DOCSTR(__pyx_doc_7twisted_6python_6_epoll_5epoll_9poll)},
+ {0, 0, 0, 0}
+};
+
+static PyNumberMethods __pyx_tp_as_number_epoll = {
+ 0, /*nb_add*/
+ 0, /*nb_subtract*/
+ 0, /*nb_multiply*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_divide*/
+ #endif
+ 0, /*nb_remainder*/
+ 0, /*nb_divmod*/
+ 0, /*nb_power*/
+ 0, /*nb_negative*/
+ 0, /*nb_positive*/
+ 0, /*nb_absolute*/
+ 0, /*nb_nonzero*/
+ 0, /*nb_invert*/
+ 0, /*nb_lshift*/
+ 0, /*nb_rshift*/
+ 0, /*nb_and*/
+ 0, /*nb_xor*/
+ 0, /*nb_or*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_coerce*/
+ #endif
+ 0, /*nb_int*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_long*/
+ #else
+ 0, /*reserved*/
+ #endif
+ 0, /*nb_float*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_oct*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_hex*/
+ #endif
+ 0, /*nb_inplace_add*/
+ 0, /*nb_inplace_subtract*/
+ 0, /*nb_inplace_multiply*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*nb_inplace_divide*/
+ #endif
+ 0, /*nb_inplace_remainder*/
+ 0, /*nb_inplace_power*/
+ 0, /*nb_inplace_lshift*/
+ 0, /*nb_inplace_rshift*/
+ 0, /*nb_inplace_and*/
+ 0, /*nb_inplace_xor*/
+ 0, /*nb_inplace_or*/
+ 0, /*nb_floor_divide*/
+ 0, /*nb_true_divide*/
+ 0, /*nb_inplace_floor_divide*/
+ 0, /*nb_inplace_true_divide*/
+ #if PY_VERSION_HEX >= 0x02050000
+ 0, /*nb_index*/
+ #endif
+};
+
+static PySequenceMethods __pyx_tp_as_sequence_epoll = {
+ 0, /*sq_length*/
+ 0, /*sq_concat*/
+ 0, /*sq_repeat*/
+ 0, /*sq_item*/
+ 0, /*sq_slice*/
+ 0, /*sq_ass_item*/
+ 0, /*sq_ass_slice*/
+ 0, /*sq_contains*/
+ 0, /*sq_inplace_concat*/
+ 0, /*sq_inplace_repeat*/
+};
+
+static PyMappingMethods __pyx_tp_as_mapping_epoll = {
+ 0, /*mp_length*/
+ 0, /*mp_subscript*/
+ 0, /*mp_ass_subscript*/
+};
+
+static PyBufferProcs __pyx_tp_as_buffer_epoll = {
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getreadbuffer*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getwritebuffer*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getsegcount*/
+ #endif
+ #if PY_MAJOR_VERSION < 3
+ 0, /*bf_getcharbuffer*/
+ #endif
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*bf_getbuffer*/
+ #endif
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*bf_releasebuffer*/
+ #endif
+};
+
+static PyTypeObject __pyx_type_7twisted_6python_6_epoll_epoll = {
+ PyVarObject_HEAD_INIT(0, 0)
+ __Pyx_NAMESTR("twisted.python._epoll.epoll"), /*tp_name*/
+ sizeof(struct __pyx_obj_7twisted_6python_6_epoll_epoll), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ __pyx_tp_dealloc_7twisted_6python_6_epoll_epoll, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ #if PY_MAJOR_VERSION < 3
+ 0, /*tp_compare*/
+ #else
+ 0, /*reserved*/
+ #endif
+ 0, /*tp_repr*/
+ &__pyx_tp_as_number_epoll, /*tp_as_number*/
+ &__pyx_tp_as_sequence_epoll, /*tp_as_sequence*/
+ &__pyx_tp_as_mapping_epoll, /*tp_as_mapping*/
+ 0, /*tp_hash*/
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro*/
+ &__pyx_tp_as_buffer_epoll, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_CHECKTYPES|Py_TPFLAGS_HAVE_NEWBUFFER|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ __Pyx_DOCSTR("\n Represent a set of file descriptors being monitored for events.\n "), /*tp_doc*/
+ 0, /*tp_traverse*/
+ 0, /*tp_clear*/
+ 0, /*tp_richcompare*/
+ 0, /*tp_weaklistoffset*/
+ 0, /*tp_iter*/
+ 0, /*tp_iternext*/
+ __pyx_methods_7twisted_6python_6_epoll_epoll, /*tp_methods*/
+ 0, /*tp_members*/
+ 0, /*tp_getset*/
+ 0, /*tp_base*/
+ 0, /*tp_dict*/
+ 0, /*tp_descr_get*/
+ 0, /*tp_descr_set*/
+ 0, /*tp_dictoffset*/
+ __pyx_pf_7twisted_6python_6_epoll_5epoll___init__, /*tp_init*/
+ 0, /*tp_alloc*/
+ __pyx_tp_new_7twisted_6python_6_epoll_epoll, /*tp_new*/
+ 0, /*tp_free*/
+ 0, /*tp_is_gc*/
+ 0, /*tp_bases*/
+ 0, /*tp_mro*/
+ 0, /*tp_cache*/
+ 0, /*tp_subclasses*/
+ 0, /*tp_weaklist*/
+ 0, /*tp_del*/
+ #if PY_VERSION_HEX >= 0x02060000
+ 0, /*tp_version_tag*/
+ #endif
+};
+
+static PyMethodDef __pyx_methods[] = {
+ {0, 0, 0, 0}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef __pyx_moduledef = {
+ PyModuleDef_HEAD_INIT,
+ __Pyx_NAMESTR("_epoll"),
+ __Pyx_DOCSTR(__pyx_k_1), /* m_doc */
+ -1, /* m_size */
+ __pyx_methods /* m_methods */,
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL /* m_free */
+};
+#endif
+
+static __Pyx_StringTabEntry __pyx_string_tab[] = {
+ {&__pyx_n_s__CTL_ADD, __pyx_k__CTL_ADD, sizeof(__pyx_k__CTL_ADD), 0, 0, 1, 1},
+ {&__pyx_n_s__CTL_DEL, __pyx_k__CTL_DEL, sizeof(__pyx_k__CTL_DEL), 0, 0, 1, 1},
+ {&__pyx_n_s__CTL_MOD, __pyx_k__CTL_MOD, sizeof(__pyx_k__CTL_MOD), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLERR, __pyx_k__EPOLLERR, sizeof(__pyx_k__EPOLLERR), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLET, __pyx_k__EPOLLET, sizeof(__pyx_k__EPOLLET), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLHUP, __pyx_k__EPOLLHUP, sizeof(__pyx_k__EPOLLHUP), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLIN, __pyx_k__EPOLLIN, sizeof(__pyx_k__EPOLLIN), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLMSG, __pyx_k__EPOLLMSG, sizeof(__pyx_k__EPOLLMSG), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLOUT, __pyx_k__EPOLLOUT, sizeof(__pyx_k__EPOLLOUT), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLPRI, __pyx_k__EPOLLPRI, sizeof(__pyx_k__EPOLLPRI), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLRDBAND, __pyx_k__EPOLLRDBAND, sizeof(__pyx_k__EPOLLRDBAND), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLRDNORM, __pyx_k__EPOLLRDNORM, sizeof(__pyx_k__EPOLLRDNORM), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLWRBAND, __pyx_k__EPOLLWRBAND, sizeof(__pyx_k__EPOLLWRBAND), 0, 0, 1, 1},
+ {&__pyx_n_s__EPOLLWRNORM, __pyx_k__EPOLLWRNORM, sizeof(__pyx_k__EPOLLWRNORM), 0, 0, 1, 1},
+ {&__pyx_n_s__ERR, __pyx_k__ERR, sizeof(__pyx_k__ERR), 0, 0, 1, 1},
+ {&__pyx_n_s__ET, __pyx_k__ET, sizeof(__pyx_k__ET), 0, 0, 1, 1},
+ {&__pyx_n_s__HUP, __pyx_k__HUP, sizeof(__pyx_k__HUP), 0, 0, 1, 1},
+ {&__pyx_n_s__IN, __pyx_k__IN, sizeof(__pyx_k__IN), 0, 0, 1, 1},
+ {&__pyx_n_s__IOError, __pyx_k__IOError, sizeof(__pyx_k__IOError), 0, 0, 1, 1},
+ {&__pyx_n_s__MSG, __pyx_k__MSG, sizeof(__pyx_k__MSG), 0, 0, 1, 1},
+ {&__pyx_n_s__OUT, __pyx_k__OUT, sizeof(__pyx_k__OUT), 0, 0, 1, 1},
+ {&__pyx_n_s__PRI, __pyx_k__PRI, sizeof(__pyx_k__PRI), 0, 0, 1, 1},
+ {&__pyx_n_s__RDBAND, __pyx_k__RDBAND, sizeof(__pyx_k__RDBAND), 0, 0, 1, 1},
+ {&__pyx_n_s__RDNORM, __pyx_k__RDNORM, sizeof(__pyx_k__RDNORM), 0, 0, 1, 1},
+ {&__pyx_n_s__WRBAND, __pyx_k__WRBAND, sizeof(__pyx_k__WRBAND), 0, 0, 1, 1},
+ {&__pyx_n_s__WRNORM, __pyx_k__WRNORM, sizeof(__pyx_k__WRNORM), 0, 0, 1, 1},
+ {&__pyx_n_s____main__, __pyx_k____main__, sizeof(__pyx_k____main__), 0, 0, 1, 1},
+ {&__pyx_n_s____test__, __pyx_k____test__, sizeof(__pyx_k____test__), 0, 0, 1, 1},
+ {&__pyx_n_s__events, __pyx_k__events, sizeof(__pyx_k__events), 0, 0, 1, 1},
+ {&__pyx_n_s__fd, __pyx_k__fd, sizeof(__pyx_k__fd), 0, 0, 1, 1},
+ {&__pyx_n_s__maxevents, __pyx_k__maxevents, sizeof(__pyx_k__maxevents), 0, 0, 1, 1},
+ {&__pyx_n_s__op, __pyx_k__op, sizeof(__pyx_k__op), 0, 0, 1, 1},
+ {&__pyx_n_s__size, __pyx_k__size, sizeof(__pyx_k__size), 0, 0, 1, 1},
+ {&__pyx_n_s__timeout, __pyx_k__timeout, sizeof(__pyx_k__timeout), 0, 0, 1, 1},
+ {0, 0, 0, 0, 0, 0, 0}
+};
+static int __Pyx_InitCachedBuiltins(void) {
+ __pyx_builtin_IOError = __Pyx_GetName(__pyx_b, __pyx_n_s__IOError); if (!__pyx_builtin_IOError) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 98; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+static int __Pyx_InitCachedConstants(void) {
+ __Pyx_RefNannyDeclarations
+ __Pyx_RefNannySetupContext("__Pyx_InitCachedConstants");
+ __Pyx_RefNannyFinishContext();
+ return 0;
+}
+
+static int __Pyx_InitGlobals(void) {
+ if (__Pyx_InitStrings(__pyx_string_tab) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+#if PY_MAJOR_VERSION < 3
+PyMODINIT_FUNC init_epoll(void); /*proto*/
+PyMODINIT_FUNC init_epoll(void)
+#else
+PyMODINIT_FUNC PyInit__epoll(void); /*proto*/
+PyMODINIT_FUNC PyInit__epoll(void)
+#endif
+{
+ PyObject *__pyx_t_1 = NULL;
+ __Pyx_RefNannyDeclarations
+ #if CYTHON_REFNANNY
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("refnanny");
+ if (!__Pyx_RefNanny) {
+ PyErr_Clear();
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("Cython.Runtime.refnanny");
+ if (!__Pyx_RefNanny)
+ Py_FatalError("failed to import 'refnanny' module");
+ }
+ #endif
+ __Pyx_RefNannySetupContext("PyMODINIT_FUNC PyInit__epoll(void)");
+ if ( __Pyx_check_binary_version() < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_empty_tuple = PyTuple_New(0); if (unlikely(!__pyx_empty_tuple)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_empty_bytes = PyBytes_FromStringAndSize("", 0); if (unlikely(!__pyx_empty_bytes)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #ifdef __pyx_binding_PyCFunctionType_USED
+ if (__pyx_binding_PyCFunctionType_init() < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #endif
+ /*--- Library function declarations ---*/
+ /*--- Threads initialization code ---*/
+ #if defined(__PYX_FORCE_INIT_THREADS) && __PYX_FORCE_INIT_THREADS
+ #ifdef WITH_THREAD /* Python build with threading support? */
+ PyEval_InitThreads();
+ #endif
+ #endif
+ /*--- Module creation code ---*/
+ #if PY_MAJOR_VERSION < 3
+ __pyx_m = Py_InitModule4(__Pyx_NAMESTR("_epoll"), __pyx_methods, __Pyx_DOCSTR(__pyx_k_1), 0, PYTHON_API_VERSION);
+ #else
+ __pyx_m = PyModule_Create(&__pyx_moduledef);
+ #endif
+ if (!__pyx_m) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ #if PY_MAJOR_VERSION < 3
+ Py_INCREF(__pyx_m);
+ #endif
+ __pyx_b = PyImport_AddModule(__Pyx_NAMESTR(__Pyx_BUILTIN_MODULE_NAME));
+ if (!__pyx_b) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ if (__Pyx_SetAttrString(__pyx_m, "__builtins__", __pyx_b) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ /*--- Initialize various global constants etc. ---*/
+ if (unlikely(__Pyx_InitGlobals() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ if (__pyx_module_is_main_twisted__python___epoll) {
+ if (__Pyx_SetAttrString(__pyx_m, "__name__", __pyx_n_s____main__) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ }
+ /*--- Builtin init code ---*/
+ if (unlikely(__Pyx_InitCachedBuiltins() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Constants init code ---*/
+ if (unlikely(__Pyx_InitCachedConstants() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Global init code ---*/
+ /*--- Variable export code ---*/
+ /*--- Function export code ---*/
+ /*--- Type init code ---*/
+ if (PyType_Ready(&__pyx_type_7twisted_6python_6_epoll_epoll) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 106; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ {
+ PyObject *wrapper = __Pyx_GetAttrString((PyObject *)&__pyx_type_7twisted_6python_6_epoll_epoll, "__init__"); if (unlikely(!wrapper)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 106; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ if (Py_TYPE(wrapper) == &PyWrapperDescr_Type) {
+ __pyx_wrapperbase_7twisted_6python_6_epoll_5epoll___init__ = *((PyWrapperDescrObject *)wrapper)->d_base;
+ __pyx_wrapperbase_7twisted_6python_6_epoll_5epoll___init__.doc = __pyx_doc_7twisted_6python_6_epoll_5epoll___init__;
+ ((PyWrapperDescrObject *)wrapper)->d_base = &__pyx_wrapperbase_7twisted_6python_6_epoll_5epoll___init__;
+ }
+ }
+ if (__Pyx_SetAttrString(__pyx_m, "epoll", (PyObject *)&__pyx_type_7twisted_6python_6_epoll_epoll) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 106; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_ptype_7twisted_6python_6_epoll_epoll = &__pyx_type_7twisted_6python_6_epoll_epoll;
+ /*--- Type import code ---*/
+ /*--- Variable import code ---*/
+ /*--- Function import code ---*/
+ /*--- Execution code ---*/
+
+ /* "twisted/python/_epoll.pyx":270
+ *
+ *
+ * CTL_ADD = EPOLL_CTL_ADD # <<<<<<<<<<<<<<
+ * CTL_DEL = EPOLL_CTL_DEL
+ * CTL_MOD = EPOLL_CTL_MOD
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLL_CTL_ADD); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 270; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__CTL_ADD, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 270; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":271
+ *
+ * CTL_ADD = EPOLL_CTL_ADD
+ * CTL_DEL = EPOLL_CTL_DEL # <<<<<<<<<<<<<<
+ * CTL_MOD = EPOLL_CTL_MOD
+ *
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLL_CTL_DEL); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 271; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__CTL_DEL, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 271; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":272
+ * CTL_ADD = EPOLL_CTL_ADD
+ * CTL_DEL = EPOLL_CTL_DEL
+ * CTL_MOD = EPOLL_CTL_MOD # <<<<<<<<<<<<<<
+ *
+ * IN = EPOLLIN = c_EPOLLIN
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLL_CTL_MOD); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 272; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__CTL_MOD, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 272; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":274
+ * CTL_MOD = EPOLL_CTL_MOD
+ *
+ * IN = EPOLLIN = c_EPOLLIN # <<<<<<<<<<<<<<
+ * OUT = EPOLLOUT = c_EPOLLOUT
+ * PRI = EPOLLPRI = c_EPOLLPRI
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLIN); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 274; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__IN, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 274; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLIN); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 274; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLIN, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 274; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":275
+ *
+ * IN = EPOLLIN = c_EPOLLIN
+ * OUT = EPOLLOUT = c_EPOLLOUT # <<<<<<<<<<<<<<
+ * PRI = EPOLLPRI = c_EPOLLPRI
+ * ERR = EPOLLERR = c_EPOLLERR
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLOUT); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 275; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__OUT, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 275; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLOUT); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 275; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLOUT, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 275; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":276
+ * IN = EPOLLIN = c_EPOLLIN
+ * OUT = EPOLLOUT = c_EPOLLOUT
+ * PRI = EPOLLPRI = c_EPOLLPRI # <<<<<<<<<<<<<<
+ * ERR = EPOLLERR = c_EPOLLERR
+ * HUP = EPOLLHUP = c_EPOLLHUP
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLPRI); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 276; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__PRI, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 276; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLPRI); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 276; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLPRI, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 276; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":277
+ * OUT = EPOLLOUT = c_EPOLLOUT
+ * PRI = EPOLLPRI = c_EPOLLPRI
+ * ERR = EPOLLERR = c_EPOLLERR # <<<<<<<<<<<<<<
+ * HUP = EPOLLHUP = c_EPOLLHUP
+ * ET = EPOLLET = c_EPOLLET
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLERR); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 277; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__ERR, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 277; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLERR); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 277; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLERR, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 277; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":278
+ * PRI = EPOLLPRI = c_EPOLLPRI
+ * ERR = EPOLLERR = c_EPOLLERR
+ * HUP = EPOLLHUP = c_EPOLLHUP # <<<<<<<<<<<<<<
+ * ET = EPOLLET = c_EPOLLET
+ *
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLHUP); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 278; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__HUP, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 278; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLHUP); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 278; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLHUP, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 278; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":279
+ * ERR = EPOLLERR = c_EPOLLERR
+ * HUP = EPOLLHUP = c_EPOLLHUP
+ * ET = EPOLLET = c_EPOLLET # <<<<<<<<<<<<<<
+ *
+ * RDNORM = EPOLLRDNORM = c_EPOLLRDNORM
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLET); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__ET, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLET); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLET, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 279; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":281
+ * ET = EPOLLET = c_EPOLLET
+ *
+ * RDNORM = EPOLLRDNORM = c_EPOLLRDNORM # <<<<<<<<<<<<<<
+ * RDBAND = EPOLLRDBAND = c_EPOLLRDBAND
+ * WRNORM = EPOLLWRNORM = c_EPOLLWRNORM
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLRDNORM); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 281; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__RDNORM, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 281; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLRDNORM); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 281; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLRDNORM, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 281; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":282
+ *
+ * RDNORM = EPOLLRDNORM = c_EPOLLRDNORM
+ * RDBAND = EPOLLRDBAND = c_EPOLLRDBAND # <<<<<<<<<<<<<<
+ * WRNORM = EPOLLWRNORM = c_EPOLLWRNORM
+ * WRBAND = EPOLLWRBAND = c_EPOLLWRBAND
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLRDBAND); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__RDBAND, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLRDBAND); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLRDBAND, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 282; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":283
+ * RDNORM = EPOLLRDNORM = c_EPOLLRDNORM
+ * RDBAND = EPOLLRDBAND = c_EPOLLRDBAND
+ * WRNORM = EPOLLWRNORM = c_EPOLLWRNORM # <<<<<<<<<<<<<<
+ * WRBAND = EPOLLWRBAND = c_EPOLLWRBAND
+ * MSG = EPOLLMSG = c_EPOLLMSG
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLWRNORM); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 283; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__WRNORM, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 283; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLWRNORM); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 283; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLWRNORM, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 283; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":284
+ * RDBAND = EPOLLRDBAND = c_EPOLLRDBAND
+ * WRNORM = EPOLLWRNORM = c_EPOLLWRNORM
+ * WRBAND = EPOLLWRBAND = c_EPOLLWRBAND # <<<<<<<<<<<<<<
+ * MSG = EPOLLMSG = c_EPOLLMSG
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLWRBAND); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 284; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__WRBAND, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 284; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLWRBAND); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 284; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLWRBAND, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 284; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":285
+ * WRNORM = EPOLLWRNORM = c_EPOLLWRNORM
+ * WRBAND = EPOLLWRBAND = c_EPOLLWRBAND
+ * MSG = EPOLLMSG = c_EPOLLMSG # <<<<<<<<<<<<<<
+ */
+ __pyx_t_1 = PyInt_FromLong(EPOLLMSG); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 285; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__MSG, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 285; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __pyx_t_1 = PyInt_FromLong(EPOLLMSG); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 285; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__EPOLLMSG, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 285; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/python/_epoll.pyx":1
+ * # Copyright (c) Twisted Matrix Laboratories. # <<<<<<<<<<<<<<
+ * # See LICENSE for details.
+ *
+ */
+ __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_1));
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s____test__, ((PyObject *)__pyx_t_1)) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(((PyObject *)__pyx_t_1)); __pyx_t_1 = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ if (__pyx_m) {
+ __Pyx_AddTraceback("init twisted.python._epoll", __pyx_clineno, __pyx_lineno, __pyx_filename);
+ Py_DECREF(__pyx_m); __pyx_m = 0;
+ } else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_ImportError, "init twisted.python._epoll");
+ }
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ #if PY_MAJOR_VERSION < 3
+ return;
+ #else
+ return __pyx_m;
+ #endif
+}
+
+/* Runtime support code */
+
+#if CYTHON_REFNANNY
+static __Pyx_RefNannyAPIStruct *__Pyx_RefNannyImportAPI(const char *modname) {
+ PyObject *m = NULL, *p = NULL;
+ void *r = NULL;
+ m = PyImport_ImportModule((char *)modname);
+ if (!m) goto end;
+ p = PyObject_GetAttrString(m, (char *)"RefNannyAPI");
+ if (!p) goto end;
+ r = PyLong_AsVoidPtr(p);
+end:
+ Py_XDECREF(p);
+ Py_XDECREF(m);
+ return (__Pyx_RefNannyAPIStruct *)r;
+}
+#endif /* CYTHON_REFNANNY */
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name) {
+ PyObject *result;
+ result = PyObject_GetAttr(dict, name);
+ if (!result) {
+ if (dict != __pyx_b) {
+ PyErr_Clear();
+ result = PyObject_GetAttr(__pyx_b, name);
+ }
+ if (!result) {
+ PyErr_SetObject(PyExc_NameError, name);
+ }
+ }
+ return result;
+}
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb) {
+ PyObject *tmp_type, *tmp_value, *tmp_tb;
+ PyThreadState *tstate = PyThreadState_GET();
+
+ tmp_type = tstate->curexc_type;
+ tmp_value = tstate->curexc_value;
+ tmp_tb = tstate->curexc_traceback;
+ tstate->curexc_type = type;
+ tstate->curexc_value = value;
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_type);
+ Py_XDECREF(tmp_value);
+ Py_XDECREF(tmp_tb);
+}
+
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ *type = tstate->curexc_type;
+ *value = tstate->curexc_value;
+ *tb = tstate->curexc_traceback;
+
+ tstate->curexc_type = 0;
+ tstate->curexc_value = 0;
+ tstate->curexc_traceback = 0;
+}
+
+
+#if PY_MAJOR_VERSION < 3
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause) {
+ /* cause is unused */
+ Py_XINCREF(type);
+ Py_XINCREF(value);
+ Py_XINCREF(tb);
+ /* First, check the traceback argument, replacing None with NULL. */
+ if (tb == Py_None) {
+ Py_DECREF(tb);
+ tb = 0;
+ }
+ else if (tb != NULL && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto raise_error;
+ }
+ /* Next, replace a missing value with None */
+ if (value == NULL) {
+ value = Py_None;
+ Py_INCREF(value);
+ }
+ #if PY_VERSION_HEX < 0x02050000
+ if (!PyClass_Check(type))
+ #else
+ if (!PyType_Check(type))
+ #endif
+ {
+ /* Raising an instance. The value should be a dummy. */
+ if (value != Py_None) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto raise_error;
+ }
+ /* Normalize to raise <class>, <instance> */
+ Py_DECREF(value);
+ value = type;
+ #if PY_VERSION_HEX < 0x02050000
+ if (PyInstance_Check(type)) {
+ type = (PyObject*) ((PyInstanceObject*)type)->in_class;
+ Py_INCREF(type);
+ }
+ else {
+ type = 0;
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception must be an old-style class or instance");
+ goto raise_error;
+ }
+ #else
+ type = (PyObject*) Py_TYPE(type);
+ Py_INCREF(type);
+ if (!PyType_IsSubtype((PyTypeObject *)type, (PyTypeObject *)PyExc_BaseException)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto raise_error;
+ }
+ #endif
+ }
+
+ __Pyx_ErrRestore(type, value, tb);
+ return;
+raise_error:
+ Py_XDECREF(value);
+ Py_XDECREF(type);
+ Py_XDECREF(tb);
+ return;
+}
+
+#else /* Python 3+ */
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb, PyObject *cause) {
+ if (tb == Py_None) {
+ tb = 0;
+ } else if (tb && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto bad;
+ }
+ if (value == Py_None)
+ value = 0;
+
+ if (PyExceptionInstance_Check(type)) {
+ if (value) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto bad;
+ }
+ value = type;
+ type = (PyObject*) Py_TYPE(value);
+ } else if (!PyExceptionClass_Check(type)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto bad;
+ }
+
+ if (cause) {
+ PyObject *fixed_cause;
+ if (PyExceptionClass_Check(cause)) {
+ fixed_cause = PyObject_CallObject(cause, NULL);
+ if (fixed_cause == NULL)
+ goto bad;
+ }
+ else if (PyExceptionInstance_Check(cause)) {
+ fixed_cause = cause;
+ Py_INCREF(fixed_cause);
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError,
+ "exception causes must derive from "
+ "BaseException");
+ goto bad;
+ }
+ if (!value) {
+ value = PyObject_CallObject(type, NULL);
+ }
+ PyException_SetCause(value, fixed_cause);
+ }
+
+ PyErr_SetObject(type, value);
+
+ if (tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ PyObject* tmp_tb = tstate->curexc_traceback;
+ if (tb != tmp_tb) {
+ Py_INCREF(tb);
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_tb);
+ }
+ }
+
+bad:
+ return;
+}
+#endif
+
+static void __Pyx_RaiseDoubleKeywordsError(
+ const char* func_name,
+ PyObject* kw_name)
+{
+ PyErr_Format(PyExc_TypeError,
+ #if PY_MAJOR_VERSION >= 3
+ "%s() got multiple values for keyword argument '%U'", func_name, kw_name);
+ #else
+ "%s() got multiple values for keyword argument '%s'", func_name,
+ PyString_AS_STRING(kw_name));
+ #endif
+}
+
+static int __Pyx_ParseOptionalKeywords(
+ PyObject *kwds,
+ PyObject **argnames[],
+ PyObject *kwds2,
+ PyObject *values[],
+ Py_ssize_t num_pos_args,
+ const char* function_name)
+{
+ PyObject *key = 0, *value = 0;
+ Py_ssize_t pos = 0;
+ PyObject*** name;
+ PyObject*** first_kw_arg = argnames + num_pos_args;
+
+ while (PyDict_Next(kwds, &pos, &key, &value)) {
+ name = first_kw_arg;
+ while (*name && (**name != key)) name++;
+ if (*name) {
+ values[name-argnames] = value;
+ } else {
+ #if PY_MAJOR_VERSION < 3
+ if (unlikely(!PyString_CheckExact(key)) && unlikely(!PyString_Check(key))) {
+ #else
+ if (unlikely(!PyUnicode_CheckExact(key)) && unlikely(!PyUnicode_Check(key))) {
+ #endif
+ goto invalid_keyword_type;
+ } else {
+ for (name = first_kw_arg; *name; name++) {
+ #if PY_MAJOR_VERSION >= 3
+ if (PyUnicode_GET_SIZE(**name) == PyUnicode_GET_SIZE(key) &&
+ PyUnicode_Compare(**name, key) == 0) break;
+ #else
+ if (PyString_GET_SIZE(**name) == PyString_GET_SIZE(key) &&
+ _PyString_Eq(**name, key)) break;
+ #endif
+ }
+ if (*name) {
+ values[name-argnames] = value;
+ } else {
+ /* unexpected keyword found */
+ for (name=argnames; name != first_kw_arg; name++) {
+ if (**name == key) goto arg_passed_twice;
+ #if PY_MAJOR_VERSION >= 3
+ if (PyUnicode_GET_SIZE(**name) == PyUnicode_GET_SIZE(key) &&
+ PyUnicode_Compare(**name, key) == 0) goto arg_passed_twice;
+ #else
+ if (PyString_GET_SIZE(**name) == PyString_GET_SIZE(key) &&
+ _PyString_Eq(**name, key)) goto arg_passed_twice;
+ #endif
+ }
+ if (kwds2) {
+ if (unlikely(PyDict_SetItem(kwds2, key, value))) goto bad;
+ } else {
+ goto invalid_keyword;
+ }
+ }
+ }
+ }
+ }
+ return 0;
+arg_passed_twice:
+ __Pyx_RaiseDoubleKeywordsError(function_name, **name);
+ goto bad;
+invalid_keyword_type:
+ PyErr_Format(PyExc_TypeError,
+ "%s() keywords must be strings", function_name);
+ goto bad;
+invalid_keyword:
+ PyErr_Format(PyExc_TypeError,
+ #if PY_MAJOR_VERSION < 3
+ "%s() got an unexpected keyword argument '%s'",
+ function_name, PyString_AsString(key));
+ #else
+ "%s() got an unexpected keyword argument '%U'",
+ function_name, key);
+ #endif
+bad:
+ return -1;
+}
+
+static void __Pyx_RaiseArgtupleInvalid(
+ const char* func_name,
+ int exact,
+ Py_ssize_t num_min,
+ Py_ssize_t num_max,
+ Py_ssize_t num_found)
+{
+ Py_ssize_t num_expected;
+ const char *more_or_less;
+
+ if (num_found < num_min) {
+ num_expected = num_min;
+ more_or_less = "at least";
+ } else {
+ num_expected = num_max;
+ more_or_less = "at most";
+ }
+ if (exact) {
+ more_or_less = "exactly";
+ }
+ PyErr_Format(PyExc_TypeError,
+ "%s() takes %s %"PY_FORMAT_SIZE_T"d positional argument%s (%"PY_FORMAT_SIZE_T"d given)",
+ func_name, more_or_less, num_expected,
+ (num_expected == 1) ? "" : "s", num_found);
+}
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject* x) {
+ const unsigned char neg_one = (unsigned char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned char" :
+ "value too large to convert to unsigned char");
+ }
+ return (unsigned char)-1;
+ }
+ return (unsigned char)val;
+ }
+ return (unsigned char)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject* x) {
+ const unsigned short neg_one = (unsigned short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned short" :
+ "value too large to convert to unsigned short");
+ }
+ return (unsigned short)-1;
+ }
+ return (unsigned short)val;
+ }
+ return (unsigned short)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject* x) {
+ const unsigned int neg_one = (unsigned int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned int" :
+ "value too large to convert to unsigned int");
+ }
+ return (unsigned int)-1;
+ }
+ return (unsigned int)val;
+ }
+ return (unsigned int)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject* x) {
+ const char neg_one = (char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to char" :
+ "value too large to convert to char");
+ }
+ return (char)-1;
+ }
+ return (char)val;
+ }
+ return (char)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject* x) {
+ const short neg_one = (short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to short" :
+ "value too large to convert to short");
+ }
+ return (short)-1;
+ }
+ return (short)val;
+ }
+ return (short)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject* x) {
+ const signed char neg_one = (signed char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed char" :
+ "value too large to convert to signed char");
+ }
+ return (signed char)-1;
+ }
+ return (signed char)val;
+ }
+ return (signed char)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject* x) {
+ const signed short neg_one = (signed short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed short" :
+ "value too large to convert to signed short");
+ }
+ return (signed short)-1;
+ }
+ return (signed short)val;
+ }
+ return (signed short)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject* x) {
+ const signed int neg_one = (signed int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed int" :
+ "value too large to convert to signed int");
+ }
+ return (signed int)-1;
+ }
+ return (signed int)val;
+ }
+ return (signed int)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject* x) {
+ const unsigned long neg_one = (unsigned long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return (unsigned long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return (unsigned long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (unsigned long)PyLong_AsLong(x);
+ }
+ } else {
+ unsigned long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned long)-1;
+ val = __Pyx_PyInt_AsUnsignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject* x) {
+ const unsigned PY_LONG_LONG neg_one = (unsigned PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return (unsigned PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return (unsigned PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (unsigned PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ unsigned PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsUnsignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject* x) {
+ const long neg_one = (long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return (long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return (long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (long)PyLong_AsLong(x);
+ }
+ } else {
+ long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (long)-1;
+ val = __Pyx_PyInt_AsLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject* x) {
+ const PY_LONG_LONG neg_one = (PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return (PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return (PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject* x) {
+ const signed long neg_one = (signed long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return (signed long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return (signed long)PyLong_AsUnsignedLong(x);
+ } else {
+ return (signed long)PyLong_AsLong(x);
+ }
+ } else {
+ signed long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed long)-1;
+ val = __Pyx_PyInt_AsSignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject* x) {
+ const signed PY_LONG_LONG neg_one = (signed PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return (signed PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return (signed PY_LONG_LONG)PyLong_AsUnsignedLongLong(x);
+ } else {
+ return (signed PY_LONG_LONG)PyLong_AsLongLong(x);
+ }
+ } else {
+ signed PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsSignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static int __Pyx_check_binary_version(void) {
+ char ctversion[4], rtversion[4];
+ PyOS_snprintf(ctversion, 4, "%d.%d", PY_MAJOR_VERSION, PY_MINOR_VERSION);
+ PyOS_snprintf(rtversion, 4, "%s", Py_GetVersion());
+ if (ctversion[0] != rtversion[0] || ctversion[2] != rtversion[2]) {
+ char message[200];
+ PyOS_snprintf(message, sizeof(message),
+ "compiletime version %s of module '%.100s' "
+ "does not match runtime version %s",
+ ctversion, __Pyx_MODULE_NAME, rtversion);
+ #if PY_VERSION_HEX < 0x02050000
+ return PyErr_Warn(NULL, message);
+ #else
+ return PyErr_WarnEx(NULL, message, 1);
+ #endif
+ }
+ return 0;
+}
+
+#include "compile.h"
+#include "frameobject.h"
+#include "traceback.h"
+
+static void __Pyx_AddTraceback(const char *funcname, int __pyx_clineno,
+ int __pyx_lineno, const char *__pyx_filename) {
+ PyObject *py_srcfile = 0;
+ PyObject *py_funcname = 0;
+ PyObject *py_globals = 0;
+ PyCodeObject *py_code = 0;
+ PyFrameObject *py_frame = 0;
+
+ #if PY_MAJOR_VERSION < 3
+ py_srcfile = PyString_FromString(__pyx_filename);
+ #else
+ py_srcfile = PyUnicode_FromString(__pyx_filename);
+ #endif
+ if (!py_srcfile) goto bad;
+ if (__pyx_clineno) {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #else
+ py_funcname = PyUnicode_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #endif
+ }
+ else {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromString(funcname);
+ #else
+ py_funcname = PyUnicode_FromString(funcname);
+ #endif
+ }
+ if (!py_funcname) goto bad;
+ py_globals = PyModule_GetDict(__pyx_m);
+ if (!py_globals) goto bad;
+ py_code = PyCode_New(
+ 0, /*int argcount,*/
+ #if PY_MAJOR_VERSION >= 3
+ 0, /*int kwonlyargcount,*/
+ #endif
+ 0, /*int nlocals,*/
+ 0, /*int stacksize,*/
+ 0, /*int flags,*/
+ __pyx_empty_bytes, /*PyObject *code,*/
+ __pyx_empty_tuple, /*PyObject *consts,*/
+ __pyx_empty_tuple, /*PyObject *names,*/
+ __pyx_empty_tuple, /*PyObject *varnames,*/
+ __pyx_empty_tuple, /*PyObject *freevars,*/
+ __pyx_empty_tuple, /*PyObject *cellvars,*/
+ py_srcfile, /*PyObject *filename,*/
+ py_funcname, /*PyObject *name,*/
+ __pyx_lineno, /*int firstlineno,*/
+ __pyx_empty_bytes /*PyObject *lnotab*/
+ );
+ if (!py_code) goto bad;
+ py_frame = PyFrame_New(
+ PyThreadState_GET(), /*PyThreadState *tstate,*/
+ py_code, /*PyCodeObject *code,*/
+ py_globals, /*PyObject *globals,*/
+ 0 /*PyObject *locals*/
+ );
+ if (!py_frame) goto bad;
+ py_frame->f_lineno = __pyx_lineno;
+ PyTraceBack_Here(py_frame);
+bad:
+ Py_XDECREF(py_srcfile);
+ Py_XDECREF(py_funcname);
+ Py_XDECREF(py_code);
+ Py_XDECREF(py_frame);
+}
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t) {
+ while (t->p) {
+ #if PY_MAJOR_VERSION < 3
+ if (t->is_unicode) {
+ *t->p = PyUnicode_DecodeUTF8(t->s, t->n - 1, NULL);
+ } else if (t->intern) {
+ *t->p = PyString_InternFromString(t->s);
+ } else {
+ *t->p = PyString_FromStringAndSize(t->s, t->n - 1);
+ }
+ #else /* Python 3+ has unicode identifiers */
+ if (t->is_unicode | t->is_str) {
+ if (t->intern) {
+ *t->p = PyUnicode_InternFromString(t->s);
+ } else if (t->encoding) {
+ *t->p = PyUnicode_Decode(t->s, t->n - 1, t->encoding, NULL);
+ } else {
+ *t->p = PyUnicode_FromStringAndSize(t->s, t->n - 1);
+ }
+ } else {
+ *t->p = PyBytes_FromStringAndSize(t->s, t->n - 1);
+ }
+ #endif
+ if (!*t->p)
+ return -1;
+ ++t;
+ }
+ return 0;
+}
+
+/* Type Conversion Functions */
+
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject* x) {
+ int is_true = x == Py_True;
+ if (is_true | (x == Py_False) | (x == Py_None)) return is_true;
+ else return PyObject_IsTrue(x);
+}
+
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x) {
+ PyNumberMethods *m;
+ const char *name = NULL;
+ PyObject *res = NULL;
+#if PY_VERSION_HEX < 0x03000000
+ if (PyInt_Check(x) || PyLong_Check(x))
+#else
+ if (PyLong_Check(x))
+#endif
+ return Py_INCREF(x), x;
+ m = Py_TYPE(x)->tp_as_number;
+#if PY_VERSION_HEX < 0x03000000
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Int(x);
+ }
+ else if (m && m->nb_long) {
+ name = "long";
+ res = PyNumber_Long(x);
+ }
+#else
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Long(x);
+ }
+#endif
+ if (res) {
+#if PY_VERSION_HEX < 0x03000000
+ if (!PyInt_Check(res) && !PyLong_Check(res)) {
+#else
+ if (!PyLong_Check(res)) {
+#endif
+ PyErr_Format(PyExc_TypeError,
+ "__%s__ returned non-%s (type %.200s)",
+ name, name, Py_TYPE(res)->tp_name);
+ Py_DECREF(res);
+ return NULL;
+ }
+ }
+ else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_TypeError,
+ "an integer is required");
+ }
+ return res;
+}
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject* b) {
+ Py_ssize_t ival;
+ PyObject* x = PyNumber_Index(b);
+ if (!x) return -1;
+ ival = PyInt_AsSsize_t(x);
+ Py_DECREF(x);
+ return ival;
+}
+
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t ival) {
+#if PY_VERSION_HEX < 0x02050000
+ if (ival <= LONG_MAX)
+ return PyInt_FromLong((long)ival);
+ else {
+ unsigned char *bytes = (unsigned char *) &ival;
+ int one = 1; int little = (int)*(unsigned char*)&one;
+ return _PyLong_FromByteArray(bytes, sizeof(size_t), little, 0);
+ }
+#else
+ return PyInt_FromSize_t(ival);
+#endif
+}
+
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject* x) {
+ unsigned PY_LONG_LONG val = __Pyx_PyInt_AsUnsignedLongLong(x);
+ if (unlikely(val == (unsigned PY_LONG_LONG)-1 && PyErr_Occurred())) {
+ return (size_t)-1;
+ } else if (unlikely(val != (unsigned PY_LONG_LONG)(size_t)val)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "value too large to convert to size_t");
+ return (size_t)-1;
+ }
+ return (size_t)val;
+}
+
+
+#endif /* Py_PYTHON_H */
diff --git a/twisted/python/_epoll.pyx b/twisted/python/_epoll.pyx
new file mode 100644
index 0000000..b8d6aa7
--- /dev/null
+++ b/twisted/python/_epoll.pyx
@@ -0,0 +1,285 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Interface to epoll I/O event notification facility.
+"""
+
+# NOTE: The version of Pyrex you are using probably _does not work_ with
+# Python 2.5. If you need to recompile this file, _make sure you are using
+# a version of Pyrex which works with Python 2.5_. I am using 0.9.4.1 from
+# <http://codespeak.net/svn/lxml/pyrex/>. -exarkun
+
+cdef extern from "stdio.h":
+ cdef extern void *malloc(int)
+ cdef extern void free(void *)
+ cdef extern int close(int)
+
+cdef extern from "errno.h":
+ cdef extern int errno
+ cdef extern char *strerror(int)
+
+cdef extern from "string.h":
+ cdef extern void *memset(void* s, int c, int n)
+
+cdef extern from "stdint.h":
+ ctypedef unsigned long uint32_t
+ ctypedef unsigned long long uint64_t
+
+cdef extern from "sys/epoll.h":
+
+ cdef enum:
+ EPOLL_CTL_ADD = 1
+ EPOLL_CTL_DEL = 2
+ EPOLL_CTL_MOD = 3
+
+ cdef enum EPOLL_EVENTS:
+ c_EPOLLIN "EPOLLIN" = 0x001
+ c_EPOLLPRI "EPOLLPRI" = 0x002
+ c_EPOLLOUT "EPOLLOUT" = 0x004
+ c_EPOLLRDNORM "EPOLLRDNORM" = 0x040
+ c_EPOLLRDBAND "EPOLLRDBAND" = 0x080
+ c_EPOLLWRNORM "EPOLLWRNORM" = 0x100
+ c_EPOLLWRBAND "EPOLLWRBAND" = 0x200
+ c_EPOLLMSG "EPOLLMSG" = 0x400
+ c_EPOLLERR "EPOLLERR" = 0x008
+ c_EPOLLHUP "EPOLLHUP" = 0x010
+ c_EPOLLET "EPOLLET" = (1 << 31)
+
+ ctypedef union epoll_data_t:
+ void *ptr
+ int fd
+ uint32_t u32
+ uint64_t u64
+
+ cdef struct epoll_event:
+ uint32_t events
+ epoll_data_t data
+
+ int epoll_create(int size)
+ int epoll_ctl(int epfd, int op, int fd, epoll_event *event)
+ int epoll_wait(int epfd, epoll_event *events, int maxevents, int timeout)
+
+cdef extern from "Python.h":
+ ctypedef struct PyThreadState
+ cdef extern PyThreadState *PyEval_SaveThread()
+ cdef extern void PyEval_RestoreThread(PyThreadState*)
+
+cdef call_epoll_wait(int fd, unsigned int maxevents, int timeout_msec):
+ """
+ Wait for an I/O event, wrap epoll_wait(2).
+
+ @type fd: C{int}
+ @param fd: The epoll file descriptor number.
+
+ @type maxevents: C{int}
+ @param maxevents: Maximum number of events returned.
+
+ @type timeout_msec: C{int}
+ @param timeout_msec: Maximum time in milliseconds waiting for events. 0
+ makes it return immediately whereas -1 makes it wait indefinitely.
+
+ @raise IOError: Raised if the underlying epoll_wait() call fails.
+ """
+ cdef epoll_event *events
+ cdef int result
+ cdef int nbytes
+ cdef PyThreadState *_save
+
+ nbytes = sizeof(epoll_event) * maxevents
+ events = <epoll_event*>malloc(nbytes)
+ memset(events, 0, nbytes)
+ try:
+ _save = PyEval_SaveThread()
+ result = epoll_wait(fd, events, maxevents, timeout_msec)
+ PyEval_RestoreThread(_save)
+
+ if result == -1:
+ raise IOError(errno, strerror(errno))
+ results = []
+ for i from 0 <= i < result:
+ results.append((events[i].data.fd, <int>events[i].events))
+ return results
+ finally:
+ free(events)
+
+cdef class epoll:
+ """
+ Represent a set of file descriptors being monitored for events.
+ """
+
+ cdef int fd
+ cdef int initialized
+
+ def __init__(self, int size=1023):
+ """
+ The constructor arguments are compatible with select.poll.__init__.
+ """
+ self.fd = epoll_create(size)
+ if self.fd == -1:
+ raise IOError(errno, strerror(errno))
+ self.initialized = 1
+
+ def __dealloc__(self):
+ if self.initialized:
+ close(self.fd)
+ self.initialized = 0
+
+ def close(self):
+ """
+ Close the epoll file descriptor.
+ """
+ if self.initialized:
+ if close(self.fd) == -1:
+ raise IOError(errno, strerror(errno))
+ self.initialized = 0
+
+ def fileno(self):
+ """
+ Return the epoll file descriptor number.
+ """
+ return self.fd
+
+ def register(self, int fd, int events):
+ """
+ Add (register) a file descriptor to be monitored by self.
+
+ This method is compatible with select.epoll.register in Python 2.6.
+
+ Wrap epoll_ctl(2).
+
+ @type fd: C{int}
+ @param fd: File descriptor to modify
+
+ @type events: C{int}
+ @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.
+
+ @raise IOError: Raised if the underlying epoll_ctl() call fails.
+ """
+ cdef int result
+ cdef epoll_event evt
+ evt.events = events
+ evt.data.fd = fd
+ result = epoll_ctl(self.fd, CTL_ADD, fd, &evt)
+ if result == -1:
+ raise IOError(errno, strerror(errno))
+
+ def unregister(self, int fd):
+ """
+ Remove (unregister) a file descriptor monitored by self.
+
+ This method is compatible with select.epoll.unregister in Python 2.6.
+
+ Wrap epoll_ctl(2).
+
+ @type fd: C{int}
+ @param fd: File descriptor to modify
+
+ @raise IOError: Raised if the underlying epoll_ctl() call fails.
+ """
+ cdef int result
+ cdef epoll_event evt
+ # We don't have to fill evt.events for CTL_DEL.
+ evt.data.fd = fd
+ result = epoll_ctl(self.fd, CTL_DEL, fd, &evt)
+ if result == -1:
+ raise IOError(errno, strerror(errno))
+
+ def modify(self, int fd, int events):
+ """
+ Modify the modified state of a file descriptor monitored by self.
+
+ This method is compatible with select.epoll.modify in Python 2.6.
+
+ Wrap epoll_ctl(2).
+
+ @type fd: C{int}
+ @param fd: File descriptor to modify
+
+ @type events: C{int}
+ @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.
+
+ @raise IOError: Raised if the underlying epoll_ctl() call fails.
+ """
+ cdef int result
+ cdef epoll_event evt
+ evt.events = events
+ evt.data.fd = fd
+ result = epoll_ctl(self.fd, CTL_MOD, fd, &evt)
+ if result == -1:
+ raise IOError(errno, strerror(errno))
+
+ def _control(self, int op, int fd, int events):
+ """
+ Modify the monitored state of a particular file descriptor.
+
+ Wrap epoll_ctl(2).
+
+ @type op: C{int}
+ @param op: One of CTL_ADD, CTL_DEL, or CTL_MOD
+
+ @type fd: C{int}
+ @param fd: File descriptor to modify
+
+ @type events: C{int}
+ @param events: A bit set of IN, OUT, PRI, ERR, HUP, and ET.
+
+ @raise IOError: Raised if the underlying epoll_ctl() call fails.
+ """
+ cdef int result
+ cdef epoll_event evt
+ evt.events = events
+ evt.data.fd = fd
+ result = epoll_ctl(self.fd, op, fd, &evt)
+ if result == -1:
+ raise IOError(errno, strerror(errno))
+
+ def wait(self, unsigned int maxevents, int timeout):
+ """
+ Wait for an I/O event, wrap epoll_wait(2).
+
+ @type maxevents: C{int}
+ @param maxevents: Maximum number of events returned.
+
+ @type timeout: C{int}
+ @param timeout: Maximum time in milliseconds waiting for events. 0
+ makes it return immediately whereas -1 makes it wait indefinitely.
+
+ @raise IOError: Raised if the underlying epoll_wait() call fails.
+ """
+ return call_epoll_wait(self.fd, maxevents, timeout)
+
+ def poll(self, float timeout=-1, unsigned int maxevents=1024):
+ """
+ Wait for an I/O event, wrap epoll_wait(2).
+
+ This method is compatible with select.epoll.poll in Python 2.6.
+
+ @type maxevents: C{int}
+ @param maxevents: Maximum number of events returned.
+
+ @type timeout: C{int}
+ @param timeout: Maximum time waiting for events. 0 makes it return
+ immediately whereas -1 makes it wait indefinitely.
+
+ @raise IOError: Raised if the underlying epoll_wait() call fails.
+ """
+ return call_epoll_wait(self.fd, maxevents, <int>(timeout * 1000.0))
+
+
+CTL_ADD = EPOLL_CTL_ADD
+CTL_DEL = EPOLL_CTL_DEL
+CTL_MOD = EPOLL_CTL_MOD
+
+IN = EPOLLIN = c_EPOLLIN
+OUT = EPOLLOUT = c_EPOLLOUT
+PRI = EPOLLPRI = c_EPOLLPRI
+ERR = EPOLLERR = c_EPOLLERR
+HUP = EPOLLHUP = c_EPOLLHUP
+ET = EPOLLET = c_EPOLLET
+
+RDNORM = EPOLLRDNORM = c_EPOLLRDNORM
+RDBAND = EPOLLRDBAND = c_EPOLLRDBAND
+WRNORM = EPOLLWRNORM = c_EPOLLWRNORM
+WRBAND = EPOLLWRBAND = c_EPOLLWRBAND
+MSG = EPOLLMSG = c_EPOLLMSG
diff --git a/twisted/python/_initgroups.c b/twisted/python/_initgroups.c
new file mode 100644
index 0000000..93500b5
--- /dev/null
+++ b/twisted/python/_initgroups.c
@@ -0,0 +1,66 @@
+/*****************************************************************************
+
+ Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
+
+ This software is subject to the provisions of the Zope Public License,
+ Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+ THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+ WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+ FOR A PARTICULAR PURPOSE
+
+ ****************************************************************************/
+
+/*
+ * This has been reported for inclusion in Python here: http://bugs.python.org/issue7333
+ * Hopefully we may be able to remove this file in some years.
+ */
+
+#include "Python.h"
+
+#if defined(__unix__) || defined(unix) || defined(__NetBSD__) || defined(__MACH__) /* Mac OS X */
+
+#include <grp.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+static PyObject *
+initgroups_initgroups(PyObject *self, PyObject *args)
+{
+ char *username;
+ unsigned int igid;
+ gid_t gid;
+
+ if (!PyArg_ParseTuple(args, "sI:initgroups", &username, &igid))
+ return NULL;
+
+ gid = igid;
+
+ if (initgroups(username, gid) == -1)
+ return PyErr_SetFromErrno(PyExc_OSError);
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyMethodDef InitgroupsMethods[] = {
+ {"initgroups", initgroups_initgroups, METH_VARARGS},
+ {NULL, NULL}
+};
+
+#else
+
+/* This module is empty on non-UNIX systems. */
+
+static PyMethodDef InitgroupsMethods[] = {
+ {NULL, NULL}
+};
+
+#endif /* defined(__unix__) || defined(unix) */
+
+void
+init_initgroups(void)
+{
+ Py_InitModule("_initgroups", InitgroupsMethods);
+}
+
diff --git a/twisted/python/_inotify.py b/twisted/python/_inotify.py
new file mode 100644
index 0000000..b4692ba
--- /dev/null
+++ b/twisted/python/_inotify.py
@@ -0,0 +1,101 @@
+# -*- test-case-name: twisted.internet.test.test_inotify -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Very low-level ctypes-based interface to Linux inotify(7).
+
+ctypes and a version of libc which supports inotify system calls are
+required.
+"""
+
+import ctypes
+import ctypes.util
+
+
+
+class INotifyError(Exception):
+ """
+ Unify all the possible exceptions that can be raised by the INotify API.
+ """
+
+
+
+def init():
+ """
+ Create an inotify instance and return the associated file descriptor.
+ """
+ fd = libc.inotify_init()
+ if fd < 0:
+ raise INotifyError("INotify initialization error.")
+ return fd
+
+
+
+def add(fd, path, mask):
+ """
+ Add a watch for the given path to the inotify file descriptor, and return
+ the watch descriptor.
+ """
+ wd = libc.inotify_add_watch(fd, path, mask)
+ if wd < 0:
+ raise INotifyError("Failed to add watch on '%r' - (%r)" % (path, wd))
+ return wd
+
+
+
+def remove(fd, wd):
+ """
+ Remove the given watch descriptor from the inotify file descriptor.
+ """
+ # When inotify_rm_watch returns -1 there's an error:
+ # The errno for this call can be either one of the following:
+ # EBADF: fd is not a valid file descriptor.
+ # EINVAL: The watch descriptor wd is not valid; or fd is
+ # not an inotify file descriptor.
+ #
+ # if we can't access the errno here we cannot even raise
+ # an exception and we need to ignore the problem, one of
+ # the most common cases is when you remove a directory from
+ # the filesystem and that directory is observed. When inotify
+ # tries to call inotify_rm_watch with a non existing directory
+ # either of the 2 errors might come up because the files inside
+ # it might have events generated way before they were handled.
+ # Unfortunately only ctypes in Python 2.6 supports accessing errno:
+ # http://bugs.python.org/issue1798 and in order to solve
+ # the problem for previous versions we need to introduce
+ # code that is quite complex:
+ # http://stackoverflow.com/questions/661017/access-to-errno-from-python
+ #
+ # See #4310 for future resolution of this issue.
+ libc.inotify_rm_watch(fd, wd)
+
+
+
+def initializeModule(libc):
+ """
+ Intialize the module, checking if the expected APIs exist and setting the
+ argtypes and restype for for C{inotify_init}, C{inotify_add_watch}, and
+ C{inotify_rm_watch}.
+ """
+ for function in ("inotify_add_watch", "inotify_init", "inotify_rm_watch"):
+ if getattr(libc, function, None) is None:
+ raise ImportError("libc6 2.4 or higher needed")
+ libc.inotify_init.argtypes = []
+ libc.inotify_init.restype = ctypes.c_int
+
+ libc.inotify_rm_watch.argtypes = [
+ ctypes.c_int, ctypes.c_int]
+ libc.inotify_rm_watch.restype = ctypes.c_int
+
+ libc.inotify_add_watch.argtypes = [
+ ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
+ libc.inotify_add_watch.restype = ctypes.c_int
+
+
+
+name = ctypes.util.find_library('c')
+if not name:
+ raise ImportError("Can't find C library.")
+libc = ctypes.cdll.LoadLibrary(name)
+initializeModule(libc)
diff --git a/twisted/python/_release.py b/twisted/python/_release.py
new file mode 100644
index 0000000..6df70fa
--- /dev/null
+++ b/twisted/python/_release.py
@@ -0,0 +1,1369 @@
+# -*- test-case-name: twisted.python.test.test_release -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted's automated release system.
+
+This module is only for use within Twisted's release system. If you are anyone
+else, do not use it. The interface and behaviour will change without notice.
+
+Only Linux is supported by this code. It should not be used by any tools
+which must run on multiple platforms (eg the setup.py script).
+"""
+
+import textwrap
+from datetime import date
+import re
+import sys
+import os
+from tempfile import mkdtemp
+import tarfile
+
+from subprocess import PIPE, STDOUT, Popen
+
+from twisted.python.versions import Version
+from twisted.python.filepath import FilePath
+from twisted.python.dist import twisted_subprojects
+
+# This import is an example of why you shouldn't use this module unless you're
+# radix
+try:
+ from twisted.lore.scripts import lore
+except ImportError:
+ pass
+
+# The offset between a year and the corresponding major version number.
+VERSION_OFFSET = 2000
+
+
+def runCommand(args):
+ """
+ Execute a vector of arguments.
+
+ @type args: C{list} of C{str}
+ @param args: A list of arguments, the first of which will be used as the
+ executable to run.
+
+ @rtype: C{str}
+ @return: All of the standard output.
+
+ @raise CommandFailed: when the program exited with a non-0 exit code.
+ """
+ process = Popen(args, stdout=PIPE, stderr=STDOUT)
+ stdout = process.stdout.read()
+ exitCode = process.wait()
+ if exitCode < 0:
+ raise CommandFailed(None, -exitCode, stdout)
+ elif exitCode > 0:
+ raise CommandFailed(exitCode, None, stdout)
+ return stdout
+
+
+class CommandFailed(Exception):
+ """
+ Raised when a child process exits unsuccessfully.
+
+ @type exitStatus: C{int}
+ @ivar exitStatus: The exit status for the child process.
+
+ @type exitSignal: C{int}
+ @ivar exitSignal: The exit signal for the child process.
+
+ @type output: C{str}
+ @ivar output: The bytes read from stdout and stderr of the child process.
+ """
+ def __init__(self, exitStatus, exitSignal, output):
+ Exception.__init__(self, exitStatus, exitSignal, output)
+ self.exitStatus = exitStatus
+ self.exitSignal = exitSignal
+ self.output = output
+
+
+
+def _changeVersionInFile(old, new, filename):
+ """
+ Replace the C{old} version number with the C{new} one in the given
+ C{filename}.
+ """
+ replaceInFile(filename, {old.base(): new.base()})
+
+
+
+def getNextVersion(version, now=None):
+ """
+ Calculate the version number for a new release of Twisted based on
+ the previous version number.
+
+ @param version: The previous version number.
+ @param now: (optional) The current date.
+ """
+ # XXX: This has no way of incrementing the patch number. Currently, we
+ # don't need it. See bug 2915. Jonathan Lange, 2007-11-20.
+ if now is None:
+ now = date.today()
+ major = now.year - VERSION_OFFSET
+ if major != version.major:
+ minor = 0
+ else:
+ minor = version.minor + 1
+ return Version(version.package, major, minor, 0)
+
+
+def changeAllProjectVersions(root, versionTemplate, today=None):
+ """
+ Change the version of all projects (including core and all subprojects).
+
+ If the current version of a project is pre-release, then also change the
+ versions in the current NEWS entries for that project.
+
+ @type root: L{FilePath}
+ @param root: The root of the Twisted source tree.
+ @type versionTemplate: L{Version}
+ @param versionTemplate: The version of all projects. The name will be
+ replaced for each respective project.
+ @type today: C{str}
+ @param today: A YYYY-MM-DD formatted string. If not provided, defaults to
+ the current day, according to the system clock.
+ """
+ if not today:
+ today = date.today().strftime('%Y-%m-%d')
+ for project in findTwistedProjects(root):
+ if project.directory.basename() == "twisted":
+ packageName = "twisted"
+ else:
+ packageName = "twisted." + project.directory.basename()
+ oldVersion = project.getVersion()
+ newVersion = Version(packageName, versionTemplate.major,
+ versionTemplate.minor, versionTemplate.micro,
+ prerelease=versionTemplate.prerelease)
+
+ if oldVersion.prerelease:
+ builder = NewsBuilder()
+ builder._changeNewsVersion(
+ root.child("NEWS"), builder._getNewsName(project),
+ oldVersion, newVersion, today)
+ builder._changeNewsVersion(
+ project.directory.child("topfiles").child("NEWS"),
+ builder._getNewsName(project), oldVersion, newVersion,
+ today)
+
+ # The placement of the top-level README with respect to other files (eg
+ # _version.py) is sufficiently different from the others that we just
+ # have to handle it specially.
+ if packageName == "twisted":
+ _changeVersionInFile(
+ oldVersion, newVersion, root.child('README').path)
+
+ project.updateVersion(newVersion)
+
+
+
+
+class Project(object):
+ """
+ A representation of a project that has a version.
+
+ @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base
+ directory of a Twisted-style Python package. The package should contain
+ a C{_version.py} file and a C{topfiles} directory that contains a
+ C{README} file.
+ """
+
+ def __init__(self, directory):
+ self.directory = directory
+
+
+ def __repr__(self):
+ return '%s(%r)' % (
+ self.__class__.__name__, self.directory)
+
+
+ def getVersion(self):
+ """
+ @return: A L{Version} specifying the version number of the project
+ based on live python modules.
+ """
+ namespace = {}
+ execfile(self.directory.child("_version.py").path, namespace)
+ return namespace["version"]
+
+
+ def updateVersion(self, version):
+ """
+ Replace the existing version numbers in _version.py and README files
+ with the specified version.
+ """
+ oldVersion = self.getVersion()
+ replaceProjectVersion(self.directory.child("_version.py").path,
+ version)
+ _changeVersionInFile(
+ oldVersion, version,
+ self.directory.child("topfiles").child("README").path)
+
+
+
+def findTwistedProjects(baseDirectory):
+ """
+ Find all Twisted-style projects beneath a base directory.
+
+ @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside.
+ @return: A list of L{Project}.
+ """
+ projects = []
+ for filePath in baseDirectory.walk():
+ if filePath.basename() == 'topfiles':
+ projectDirectory = filePath.parent()
+ projects.append(Project(projectDirectory))
+ return projects
+
+
+
+def updateTwistedVersionInformation(baseDirectory, now):
+ """
+ Update the version information for Twisted and all subprojects to the
+ date-based version number.
+
+ @param baseDirectory: Where to look for Twisted. If None, the function
+ infers the information from C{twisted.__file__}.
+ @param now: The current date (as L{datetime.date}). If None, it defaults
+ to today.
+ """
+ for project in findTwistedProjects(baseDirectory):
+ project.updateVersion(getNextVersion(project.getVersion(), now=now))
+
+
+def generateVersionFileData(version):
+ """
+ Generate the data to be placed into a _version.py file.
+
+ @param version: A version object.
+ """
+ if version.prerelease is not None:
+ prerelease = ", prerelease=%r" % (version.prerelease,)
+ else:
+ prerelease = ""
+ data = '''\
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version(%r, %s, %s, %s%s)
+''' % (version.package, version.major, version.minor, version.micro, prerelease)
+ return data
+
+
+def replaceProjectVersion(filename, newversion):
+ """
+ Write version specification code into the given filename, which
+ sets the version to the given version number.
+
+ @param filename: A filename which is most likely a "_version.py"
+ under some Twisted project.
+ @param newversion: A version object.
+ """
+ # XXX - this should be moved to Project and renamed to writeVersionFile.
+ # jml, 2007-11-15.
+ f = open(filename, 'w')
+ f.write(generateVersionFileData(newversion))
+ f.close()
+
+
+
+def replaceInFile(filename, oldToNew):
+ """
+ I replace the text `oldstr' with `newstr' in `filename' using science.
+ """
+ os.rename(filename, filename+'.bak')
+ f = open(filename+'.bak')
+ d = f.read()
+ f.close()
+ for k,v in oldToNew.items():
+ d = d.replace(k, v)
+ f = open(filename + '.new', 'w')
+ f.write(d)
+ f.close()
+ os.rename(filename+'.new', filename)
+ os.unlink(filename+'.bak')
+
+
+
+class NoDocumentsFound(Exception):
+ """
+ Raised when no input documents are found.
+ """
+
+
+
+class LoreBuilderMixin(object):
+ """
+ Base class for builders which invoke lore.
+ """
+ def lore(self, arguments):
+ """
+ Run lore with the given arguments.
+
+ @param arguments: A C{list} of C{str} giving command line arguments to
+ lore which should be used.
+ """
+ options = lore.Options()
+ options.parseOptions(["--null"] + arguments)
+ lore.runGivenOptions(options)
+
+
+
+class DocBuilder(LoreBuilderMixin):
+ """
+ Generate HTML documentation for projects.
+ """
+
+ def build(self, version, resourceDir, docDir, template, apiBaseURL=None,
+ deleteInput=False):
+ """
+ Build the documentation in C{docDir} with Lore.
+
+ Input files ending in .xhtml will be considered. Output will written as
+ .html files.
+
+ @param version: the version of the documentation to pass to lore.
+ @type version: C{str}
+
+ @param resourceDir: The directory which contains the toplevel index and
+ stylesheet file for this section of documentation.
+ @type resourceDir: L{twisted.python.filepath.FilePath}
+
+ @param docDir: The directory of the documentation.
+ @type docDir: L{twisted.python.filepath.FilePath}
+
+ @param template: The template used to generate the documentation.
+ @type template: L{twisted.python.filepath.FilePath}
+
+ @type apiBaseURL: C{str} or C{NoneType}
+ @param apiBaseURL: A format string which will be interpolated with the
+ fully-qualified Python name for each API link. For example, to
+ generate the Twisted 8.0.0 documentation, pass
+ C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
+
+ @param deleteInput: If True, the input documents will be deleted after
+ their output is generated.
+ @type deleteInput: C{bool}
+
+ @raise NoDocumentsFound: When there are no .xhtml files in the given
+ C{docDir}.
+ """
+ linkrel = self.getLinkrel(resourceDir, docDir)
+ inputFiles = docDir.globChildren("*.xhtml")
+ filenames = [x.path for x in inputFiles]
+ if not filenames:
+ raise NoDocumentsFound("No input documents found in %s" % (docDir,))
+ if apiBaseURL is not None:
+ arguments = ["--config", "baseurl=" + apiBaseURL]
+ else:
+ arguments = []
+ arguments.extend(["--config", "template=%s" % (template.path,),
+ "--config", "ext=.html",
+ "--config", "version=%s" % (version,),
+ "--linkrel", linkrel] + filenames)
+ self.lore(arguments)
+ if deleteInput:
+ for inputFile in inputFiles:
+ inputFile.remove()
+
+
+ def getLinkrel(self, resourceDir, docDir):
+ """
+ Calculate a value appropriate for Lore's --linkrel option.
+
+ Lore's --linkrel option defines how to 'find' documents that are
+ linked to from TEMPLATE files (NOT document bodies). That is, it's a
+ prefix for links ('a' and 'link') in the template.
+
+ @param resourceDir: The directory which contains the toplevel index and
+ stylesheet file for this section of documentation.
+ @type resourceDir: L{twisted.python.filepath.FilePath}
+
+ @param docDir: The directory containing documents that must link to
+ C{resourceDir}.
+ @type docDir: L{twisted.python.filepath.FilePath}
+ """
+ if resourceDir != docDir:
+ return '/'.join(filePathDelta(docDir, resourceDir)) + "/"
+ else:
+ return ""
+
+
+
+class ManBuilder(LoreBuilderMixin):
+ """
+ Generate man pages of the different existing scripts.
+ """
+
+ def build(self, manDir):
+ """
+ Generate Lore input files from the man pages in C{manDir}.
+
+ Input files ending in .1 will be considered. Output will written as
+ -man.xhtml files.
+
+ @param manDir: The directory of the man pages.
+ @type manDir: L{twisted.python.filepath.FilePath}
+
+ @raise NoDocumentsFound: When there are no .1 files in the given
+ C{manDir}.
+ """
+ inputFiles = manDir.globChildren("*.1")
+ filenames = [x.path for x in inputFiles]
+ if not filenames:
+ raise NoDocumentsFound("No manual pages found in %s" % (manDir,))
+ arguments = ["--input", "man",
+ "--output", "lore",
+ "--config", "ext=-man.xhtml"] + filenames
+ self.lore(arguments)
+
+
+
+class APIBuilder(object):
+ """
+ Generate API documentation from source files using
+ U{pydoctor<http://codespeak.net/~mwh/pydoctor/>}. This requires
+ pydoctor to be installed and usable (which means you won't be able to
+ use it with Python 2.3).
+ """
+ def build(self, projectName, projectURL, sourceURL, packagePath,
+ outputPath):
+ """
+ Call pydoctor's entry point with options which will generate HTML
+ documentation for the specified package's API.
+
+ @type projectName: C{str}
+ @param projectName: The name of the package for which to generate
+ documentation.
+
+ @type projectURL: C{str}
+ @param projectURL: The location (probably an HTTP URL) of the project
+ on the web.
+
+ @type sourceURL: C{str}
+ @param sourceURL: The location (probably an HTTP URL) of the root of
+ the source browser for the project.
+
+ @type packagePath: L{FilePath}
+ @param packagePath: The path to the top-level of the package named by
+ C{projectName}.
+
+ @type outputPath: L{FilePath}
+ @param outputPath: An existing directory to which the generated API
+ documentation will be written.
+ """
+ from pydoctor.driver import main
+ main(
+ ["--project-name", projectName,
+ "--project-url", projectURL,
+ "--system-class", "pydoctor.twistedmodel.TwistedSystem",
+ "--project-base-dir", packagePath.parent().path,
+ "--html-viewsource-base", sourceURL,
+ "--add-package", packagePath.path,
+ "--html-output", outputPath.path,
+ "--html-write-function-pages", "--quiet", "--make-html"])
+
+
+
+class BookBuilder(LoreBuilderMixin):
+ """
+ Generate the LaTeX and PDF documentation.
+
+ The book is built by assembling a number of LaTeX documents. Only the
+ overall document which describes how to assemble the documents is stored
+ in LaTeX in the source. The rest of the documentation is generated from
+ Lore input files. These are primarily XHTML files (of the particular
+ Lore subset), but man pages are stored in GROFF format. BookBuilder
+ expects all of its input to be Lore XHTML format, so L{ManBuilder}
+ should be invoked first if the man pages are to be included in the
+ result (this is determined by the book LaTeX definition file).
+ Therefore, a sample usage of BookBuilder may look something like this::
+
+ man = ManBuilder()
+ man.build(FilePath("doc/core/man"))
+ book = BookBuilder()
+ book.build(
+ FilePath('doc/core/howto'),
+ [FilePath('doc/core/howto'), FilePath('doc/core/howto/tutorial'),
+ FilePath('doc/core/man'), FilePath('doc/core/specifications')],
+ FilePath('doc/core/howto/book.tex'), FilePath('/tmp/book.pdf'))
+ """
+ def run(self, command):
+ """
+ Execute a command in a child process and return the output.
+
+ @type command: C{str}
+ @param command: The shell command to run.
+
+ @raise CommandFailed: If the child process exits with an error.
+ """
+ return runCommand(command)
+
+
+ def buildTeX(self, howtoDir):
+ """
+ Build LaTeX files for lore input files in the given directory.
+
+ Input files ending in .xhtml will be considered. Output will written as
+ .tex files.
+
+ @type howtoDir: L{FilePath}
+ @param howtoDir: A directory containing lore input files.
+
+ @raise ValueError: If C{howtoDir} does not exist.
+ """
+ if not howtoDir.exists():
+ raise ValueError("%r does not exist." % (howtoDir.path,))
+ self.lore(
+ ["--output", "latex",
+ "--config", "section"] +
+ [child.path for child in howtoDir.globChildren("*.xhtml")])
+
+
+ def buildPDF(self, bookPath, inputDirectory, outputPath):
+ """
+ Build a PDF from the given a LaTeX book document.
+
+ @type bookPath: L{FilePath}
+ @param bookPath: The location of a LaTeX document defining a book.
+
+ @type inputDirectory: L{FilePath}
+ @param inputDirectory: The directory which the inputs of the book are
+ relative to.
+
+ @type outputPath: L{FilePath}
+ @param outputPath: The location to which to write the resulting book.
+ """
+ if not bookPath.basename().endswith(".tex"):
+ raise ValueError("Book filename must end with .tex")
+
+ workPath = FilePath(mkdtemp())
+ try:
+ startDir = os.getcwd()
+ try:
+ os.chdir(inputDirectory.path)
+
+ texToDVI = [
+ "latex", "-interaction=nonstopmode",
+ "-output-directory=" + workPath.path,
+ bookPath.path]
+
+ # What I tell you three times is true!
+ # The first two invocations of latex on the book file allows it
+ # correctly create page numbers for in-text references. Why this is
+ # the case, I could not tell you. -exarkun
+ for i in range(3):
+ self.run(texToDVI)
+
+ bookBaseWithoutExtension = bookPath.basename()[:-4]
+ dviPath = workPath.child(bookBaseWithoutExtension + ".dvi")
+ psPath = workPath.child(bookBaseWithoutExtension + ".ps")
+ pdfPath = workPath.child(bookBaseWithoutExtension + ".pdf")
+ self.run([
+ "dvips", "-o", psPath.path, "-t", "letter", "-Ppdf",
+ dviPath.path])
+ self.run(["ps2pdf13", psPath.path, pdfPath.path])
+ pdfPath.moveTo(outputPath)
+ workPath.remove()
+ finally:
+ os.chdir(startDir)
+ except:
+ workPath.moveTo(bookPath.parent().child(workPath.basename()))
+ raise
+
+
+ def build(self, baseDirectory, inputDirectories, bookPath, outputPath):
+ """
+ Build a PDF book from the given TeX book definition and directories
+ containing lore inputs.
+
+ @type baseDirectory: L{FilePath}
+ @param baseDirectory: The directory which the inputs of the book are
+ relative to.
+
+ @type inputDirectories: C{list} of L{FilePath}
+ @param inputDirectories: The paths which contain lore inputs to be
+ converted to LaTeX.
+
+ @type bookPath: L{FilePath}
+ @param bookPath: The location of a LaTeX document defining a book.
+
+ @type outputPath: L{FilePath}
+ @param outputPath: The location to which to write the resulting book.
+ """
+ for inputDir in inputDirectories:
+ self.buildTeX(inputDir)
+ self.buildPDF(bookPath, baseDirectory, outputPath)
+ for inputDirectory in inputDirectories:
+ for child in inputDirectory.children():
+ if child.splitext()[1] == ".tex" and child != bookPath:
+ child.remove()
+
+
+
+class NewsBuilder(object):
+ """
+ Generate the new section of a NEWS file.
+
+ The C{_FEATURE}, C{_BUGFIX}, C{_DOC}, C{_REMOVAL}, and C{_MISC}
+ attributes of this class are symbolic names for the news entry types
+ which are supported. Conveniently, they each also take on the value of
+ the file name extension which indicates a news entry of that type.
+
+ @cvar _headings: A C{dict} mapping one of the news entry types to the
+ heading to write out for that type of news entry.
+
+ @cvar _NO_CHANGES: A C{str} giving the text which appears when there are
+ no significant changes in a release.
+
+ @cvar _TICKET_HINT: A C{str} giving the text which appears at the top of
+ each news file and which should be kept at the top, not shifted down
+ with all the other content. Put another way, this is the text after
+ which the new news text is inserted.
+ """
+
+ _FEATURE = ".feature"
+ _BUGFIX = ".bugfix"
+ _DOC = ".doc"
+ _REMOVAL = ".removal"
+ _MISC = ".misc"
+
+ _headings = {
+ _FEATURE: "Features",
+ _BUGFIX: "Bugfixes",
+ _DOC: "Improved Documentation",
+ _REMOVAL: "Deprecations and Removals",
+ _MISC: "Other",
+ }
+
+ _NO_CHANGES = "No significant changes have been made for this release.\n"
+
+ _TICKET_HINT = (
+ 'Ticket numbers in this file can be looked up by visiting\n'
+ 'http://twistedmatrix.com/trac/ticket/<number>\n'
+ '\n')
+
+ def _today(self):
+ """
+ Return today's date as a string in YYYY-MM-DD format.
+ """
+ return date.today().strftime('%Y-%m-%d')
+
+
+ def _findChanges(self, path, ticketType):
+ """
+ Load all the feature ticket summaries.
+
+ @param path: A L{FilePath} the direct children of which to search
+ for news entries.
+
+ @param ticketType: The type of news entries to search for. One of
+ L{NewsBuilder._FEATURE}, L{NewsBuilder._BUGFIX},
+ L{NewsBuilder._REMOVAL}, or L{NewsBuilder._MISC}.
+
+ @return: A C{list} of two-tuples. The first element is the ticket
+ number as an C{int}. The second element of each tuple is the
+ description of the feature.
+ """
+ results = []
+ for child in path.children():
+ base, ext = os.path.splitext(child.basename())
+ if ext == ticketType:
+ results.append((
+ int(base),
+ ' '.join(child.getContent().splitlines())))
+ results.sort()
+ return results
+
+
+ def _formatHeader(self, header):
+ """
+ Format a header for a NEWS file.
+
+ A header is a title with '=' signs underlining it.
+
+ @param header: The header string to format.
+ @type header: C{str}
+ @return: A C{str} containing C{header}.
+ """
+ return header + '\n' + '=' * len(header) + '\n\n'
+
+
+ def _writeHeader(self, fileObj, header):
+ """
+ Write a version header to the given file.
+
+ @param fileObj: A file-like object to which to write the header.
+ @param header: The header to write to the file.
+ @type header: C{str}
+ """
+ fileObj.write(self._formatHeader(header))
+
+
+ def _writeSection(self, fileObj, header, tickets):
+ """
+ Write out one section (features, bug fixes, etc) to the given file.
+
+ @param fileObj: A file-like object to which to write the news section.
+
+ @param header: The header for the section to write.
+ @type header: C{str}
+
+ @param tickets: A C{list} of ticket information of the sort returned
+ by L{NewsBuilder._findChanges}.
+ """
+ if not tickets:
+ return
+
+ reverse = {}
+ for (ticket, description) in tickets:
+ reverse.setdefault(description, []).append(ticket)
+ for description in reverse:
+ reverse[description].sort()
+ reverse = reverse.items()
+ reverse.sort(key=lambda (descr, tickets): tickets[0])
+
+ fileObj.write(header + '\n' + '-' * len(header) + '\n')
+ for (description, relatedTickets) in reverse:
+ ticketList = ', '.join([
+ '#' + str(ticket) for ticket in relatedTickets])
+ entry = ' - %s (%s)' % (description, ticketList)
+ entry = textwrap.fill(entry, subsequent_indent=' ')
+ fileObj.write(entry + '\n')
+ fileObj.write('\n')
+
+
+ def _writeMisc(self, fileObj, header, tickets):
+ """
+ Write out a miscellaneous-changes section to the given file.
+
+ @param fileObj: A file-like object to which to write the news section.
+
+ @param header: The header for the section to write.
+ @type header: C{str}
+
+ @param tickets: A C{list} of ticket information of the sort returned
+ by L{NewsBuilder._findChanges}.
+ """
+ if not tickets:
+ return
+
+ fileObj.write(header + '\n' + '-' * len(header) + '\n')
+ formattedTickets = []
+ for (ticket, ignored) in tickets:
+ formattedTickets.append('#' + str(ticket))
+ entry = ' - ' + ', '.join(formattedTickets)
+ entry = textwrap.fill(entry, subsequent_indent=' ')
+ fileObj.write(entry + '\n\n')
+
+
+ def build(self, path, output, header):
+ """
+ Load all of the change information from the given directory and write
+ it out to the given output file.
+
+ @param path: A directory (probably a I{topfiles} directory) containing
+ change information in the form of <ticket>.<change type> files.
+ @type path: L{FilePath}
+
+ @param output: The NEWS file to which the results will be prepended.
+ @type output: L{FilePath}
+
+ @param header: The top-level header to use when writing the news.
+ @type header: L{str}
+ """
+ changes = []
+ for part in (self._FEATURE, self._BUGFIX, self._DOC, self._REMOVAL):
+ tickets = self._findChanges(path, part)
+ if tickets:
+ changes.append((part, tickets))
+ misc = self._findChanges(path, self._MISC)
+
+ oldNews = output.getContent()
+ newNews = output.sibling('NEWS.new').open('w')
+ if oldNews.startswith(self._TICKET_HINT):
+ newNews.write(self._TICKET_HINT)
+ oldNews = oldNews[len(self._TICKET_HINT):]
+
+ self._writeHeader(newNews, header)
+ if changes:
+ for (part, tickets) in changes:
+ self._writeSection(newNews, self._headings.get(part), tickets)
+ else:
+ newNews.write(self._NO_CHANGES)
+ newNews.write('\n')
+ self._writeMisc(newNews, self._headings.get(self._MISC), misc)
+ newNews.write('\n')
+ newNews.write(oldNews)
+ newNews.close()
+ output.sibling('NEWS.new').moveTo(output)
+
+
+ def _getNewsName(self, project):
+ """
+ Return the name of C{project} that should appear in NEWS.
+
+ @param project: A L{Project}
+ @return: The name of C{project}.
+ """
+ name = project.directory.basename().title()
+ if name == 'Twisted':
+ name = 'Core'
+ return name
+
+
+ def _iterProjects(self, baseDirectory):
+ """
+ Iterate through the Twisted projects in C{baseDirectory}, yielding
+ everything we need to know to build news for them.
+
+ Yields C{topfiles}, C{news}, C{name}, C{version} for each sub-project
+ in reverse-alphabetical order. C{topfile} is the L{FilePath} for the
+ topfiles directory, C{news} is the L{FilePath} for the NEWS file,
+ C{name} is the nice name of the project (as should appear in the NEWS
+ file), C{version} is the current version string for that project.
+
+ @param baseDirectory: A L{FilePath} representing the root directory
+ beneath which to find Twisted projects for which to generate
+ news (see L{findTwistedProjects}).
+ @type baseDirectory: L{FilePath}
+ """
+ # Get all the subprojects to generate news for
+ projects = findTwistedProjects(baseDirectory)
+ # And order them alphabetically for ease of reading
+ projects.sort(key=lambda proj: proj.directory.path)
+ # And generate them backwards since we write news by prepending to
+ # files.
+ projects.reverse()
+
+ for aggregateNews in [False, True]:
+ for project in projects:
+ topfiles = project.directory.child("topfiles")
+ if aggregateNews:
+ news = baseDirectory.child("NEWS")
+ else:
+ news = topfiles.child("NEWS")
+ name = self._getNewsName(project)
+ version = project.getVersion()
+ yield topfiles, news, name, version
+
+
+ def buildAll(self, baseDirectory):
+ """
+ Find all of the Twisted subprojects beneath C{baseDirectory} and update
+ their news files from the ticket change description files in their
+ I{topfiles} directories and update the news file in C{baseDirectory}
+ with all of the news.
+
+ @param baseDirectory: A L{FilePath} representing the root directory
+ beneath which to find Twisted projects for which to generate
+ news (see L{findTwistedProjects}).
+ """
+ today = self._today()
+ for topfiles, news, name, version in self._iterProjects(baseDirectory):
+ self.build(
+ topfiles, news,
+ "Twisted %s %s (%s)" % (name, version.base(), today))
+
+
+ def _changeNewsVersion(self, news, name, oldVersion, newVersion, today):
+ """
+ Change all references to the current version number in a NEWS file to
+ refer to C{newVersion} instead.
+
+ @param news: The NEWS file to change.
+ @type news: L{FilePath}
+ @param name: The name of the project to change.
+ @type name: C{str}
+ @param oldVersion: The old version of the project.
+ @type oldVersion: L{Version}
+ @param newVersion: The new version of the project.
+ @type newVersion: L{Version}
+ @param today: A YYYY-MM-DD string representing today's date.
+ @type today: C{str}
+ """
+ newHeader = self._formatHeader(
+ "Twisted %s %s (%s)" % (name, newVersion.base(), today))
+ expectedHeaderRegex = re.compile(
+ r"Twisted %s %s \(\d{4}-\d\d-\d\d\)\n=+\n\n" % (
+ re.escape(name), re.escape(oldVersion.base())))
+ oldNews = news.getContent()
+ match = expectedHeaderRegex.search(oldNews)
+ if match:
+ oldHeader = match.group()
+ replaceInFile(news.path, {oldHeader: newHeader})
+
+
+ def main(self, args):
+ """
+ Build all news files.
+
+ @param args: The command line arguments to process. This must contain
+ one string, the path to the base of the Twisted checkout for which
+ to build the news.
+ @type args: C{list} of C{str}
+ """
+ if len(args) != 1:
+ sys.exit("Must specify one argument: the path to the Twisted checkout")
+ self.buildAll(FilePath(args[0]))
+
+
+
+def filePathDelta(origin, destination):
+ """
+ Return a list of strings that represent C{destination} as a path relative
+ to C{origin}.
+
+ It is assumed that both paths represent directories, not files. That is to
+ say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to
+ L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},
+ not C{baz}.
+
+ @type origin: L{twisted.python.filepath.FilePath}
+ @param origin: The origin of the relative path.
+
+ @type destination: L{twisted.python.filepath.FilePath}
+ @param destination: The destination of the relative path.
+ """
+ commonItems = 0
+ path1 = origin.path.split(os.sep)
+ path2 = destination.path.split(os.sep)
+ for elem1, elem2 in zip(path1, path2):
+ if elem1 == elem2:
+ commonItems += 1
+ else:
+ break
+ path = [".."] * (len(path1) - commonItems)
+ return path + path2[commonItems:]
+
+
+
+class DistributionBuilder(object):
+ """
+ A builder of Twisted distributions.
+
+ This knows how to build tarballs for Twisted and all of its subprojects.
+ """
+ from twisted.python.dist import twisted_subprojects as subprojects
+
+ def __init__(self, rootDirectory, outputDirectory, templatePath=None,
+ apiBaseURL=None):
+ """
+ Create a distribution builder.
+
+ @param rootDirectory: root of a Twisted export which will populate
+ subsequent tarballs.
+ @type rootDirectory: L{FilePath}.
+
+ @param outputDirectory: The directory in which to create the tarballs.
+ @type outputDirectory: L{FilePath}
+
+ @param templatePath: Path to the template file that is used for the
+ howto documentation.
+ @type templatePath: L{FilePath}
+
+ @type apiBaseURL: C{str} or C{NoneType}
+ @param apiBaseURL: A format string which will be interpolated with the
+ fully-qualified Python name for each API link. For example, to
+ generate the Twisted 8.0.0 documentation, pass
+ C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
+ """
+ self.rootDirectory = rootDirectory
+ self.outputDirectory = outputDirectory
+ self.templatePath = templatePath
+ self.apiBaseURL = apiBaseURL
+ self.manBuilder = ManBuilder()
+ self.docBuilder = DocBuilder()
+
+
+ def _buildDocInDir(self, path, version, howtoPath):
+ """
+ Generate documentation in the given path, building man pages first if
+ necessary and swallowing errors (so that directories without lore
+ documentation in them are ignored).
+
+ @param path: The path containing documentation to build.
+ @type path: L{FilePath}
+ @param version: The version of the project to include in all generated
+ pages.
+ @type version: C{str}
+ @param howtoPath: The "resource path" as L{DocBuilder} describes it.
+ @type howtoPath: L{FilePath}
+ """
+ if self.templatePath is None:
+ self.templatePath = self.rootDirectory.descendant(
+ ["doc", "core", "howto", "template.tpl"])
+ if path.basename() == "man":
+ self.manBuilder.build(path)
+ if path.isdir():
+ try:
+ self.docBuilder.build(version, howtoPath, path,
+ self.templatePath, self.apiBaseURL, True)
+ except NoDocumentsFound:
+ pass
+
+
+ def buildTwisted(self, version):
+ """
+ Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}.
+
+ bin/admin is excluded.
+
+ @type version: C{str}
+ @param version: The version of Twisted to build.
+
+ @return: The tarball file.
+ @rtype: L{FilePath}.
+ """
+ releaseName = "Twisted-%s" % (version,)
+ buildPath = lambda *args: '/'.join((releaseName,) + args)
+
+ outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
+ tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
+
+ docPath = self.rootDirectory.child("doc")
+
+ # Generate docs!
+ if docPath.isdir():
+ for subProjectDir in docPath.children():
+ if subProjectDir.isdir():
+ for child in subProjectDir.walk():
+ self._buildDocInDir(child, version,
+ subProjectDir.child("howto"))
+
+ for binthing in self.rootDirectory.child("bin").children():
+ # bin/admin should not be included.
+ if binthing.basename() != "admin":
+ tarball.add(binthing.path,
+ buildPath("bin", binthing.basename()))
+
+ for submodule in self.rootDirectory.child("twisted").children():
+ if submodule.basename() == "plugins":
+ for plugin in submodule.children():
+ tarball.add(plugin.path, buildPath("twisted", "plugins",
+ plugin.basename()))
+ else:
+ tarball.add(submodule.path, buildPath("twisted",
+ submodule.basename()))
+
+ for docDir in self.rootDirectory.child("doc").children():
+ tarball.add(docDir.path, buildPath("doc", docDir.basename()))
+
+ for toplevel in self.rootDirectory.children():
+ if not toplevel.isdir():
+ tarball.add(toplevel.path, buildPath(toplevel.basename()))
+
+ tarball.close()
+
+ return outputFile
+
+
+ def buildCore(self, version):
+ """
+ Build a core distribution in C{TwistedCore-<version>.tar.bz2}.
+
+ This is very similar to L{buildSubProject}, but core tarballs and the
+ input are laid out slightly differently.
+
+ - scripts are in the top level of the C{bin} directory.
+ - code is included directly from the C{twisted} directory, excluding
+ subprojects.
+ - all plugins except the subproject plugins are included.
+
+ @type version: C{str}
+ @param version: The version of Twisted to build.
+
+ @return: The tarball file.
+ @rtype: L{FilePath}.
+ """
+ releaseName = "TwistedCore-%s" % (version,)
+ outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
+ buildPath = lambda *args: '/'.join((releaseName,) + args)
+ tarball = self._createBasicSubprojectTarball(
+ "core", version, outputFile)
+
+ # Include the bin directory for the subproject.
+ for path in self.rootDirectory.child("bin").children():
+ if not path.isdir():
+ tarball.add(path.path, buildPath("bin", path.basename()))
+
+ # Include all files within twisted/ that aren't part of a subproject.
+ for path in self.rootDirectory.child("twisted").children():
+ if path.basename() == "plugins":
+ for plugin in path.children():
+ for subproject in self.subprojects:
+ if plugin.basename() == "twisted_%s.py" % (subproject,):
+ break
+ else:
+ tarball.add(plugin.path,
+ buildPath("twisted", "plugins",
+ plugin.basename()))
+ elif not path.basename() in self.subprojects + ["topfiles"]:
+ tarball.add(path.path, buildPath("twisted", path.basename()))
+
+ tarball.add(self.rootDirectory.child("twisted").child("topfiles").path,
+ releaseName)
+ tarball.close()
+
+ return outputFile
+
+
+ def buildSubProject(self, projectName, version):
+ """
+ Build a subproject distribution in
+ C{Twisted<Projectname>-<version>.tar.bz2}.
+
+ @type projectName: C{str}
+ @param projectName: The lowercase name of the subproject to build.
+ @type version: C{str}
+ @param version: The version of Twisted to build.
+
+ @return: The tarball file.
+ @rtype: L{FilePath}.
+ """
+ releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
+ outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
+ buildPath = lambda *args: '/'.join((releaseName,) + args)
+ subProjectDir = self.rootDirectory.child("twisted").child(projectName)
+
+ tarball = self._createBasicSubprojectTarball(projectName, version,
+ outputFile)
+
+ tarball.add(subProjectDir.child("topfiles").path, releaseName)
+
+ # Include all files in the subproject package except for topfiles.
+ for child in subProjectDir.children():
+ name = child.basename()
+ if name != "topfiles":
+ tarball.add(
+ child.path,
+ buildPath("twisted", projectName, name))
+
+ pluginsDir = self.rootDirectory.child("twisted").child("plugins")
+ # Include the plugin for the subproject.
+ pluginFileName = "twisted_%s.py" % (projectName,)
+ pluginFile = pluginsDir.child(pluginFileName)
+ if pluginFile.exists():
+ tarball.add(pluginFile.path,
+ buildPath("twisted", "plugins", pluginFileName))
+
+ # Include the bin directory for the subproject.
+ binPath = self.rootDirectory.child("bin").child(projectName)
+ if binPath.isdir():
+ tarball.add(binPath.path, buildPath("bin"))
+ tarball.close()
+
+ return outputFile
+
+
+ def _createBasicSubprojectTarball(self, projectName, version, outputFile):
+ """
+ Helper method to create and fill a tarball with things common between
+ subprojects and core.
+
+ @param projectName: The subproject's name.
+ @type projectName: C{str}
+ @param version: The version of the release.
+ @type version: C{str}
+ @param outputFile: The location of the tar file to create.
+ @type outputFile: L{FilePath}
+ """
+ releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
+ buildPath = lambda *args: '/'.join((releaseName,) + args)
+
+ tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
+
+ tarball.add(self.rootDirectory.child("LICENSE").path,
+ buildPath("LICENSE"))
+
+ docPath = self.rootDirectory.child("doc").child(projectName)
+
+ if docPath.isdir():
+ for child in docPath.walk():
+ self._buildDocInDir(child, version, docPath.child("howto"))
+ tarball.add(docPath.path, buildPath("doc"))
+
+ return tarball
+
+
+
+class UncleanWorkingDirectory(Exception):
+ """
+ Raised when the working directory of an SVN checkout is unclean.
+ """
+
+
+
+class NotWorkingDirectory(Exception):
+ """
+ Raised when a directory does not appear to be an SVN working directory.
+ """
+
+
+
+def buildAllTarballs(checkout, destination, templatePath=None):
+ """
+ Build complete tarballs (including documentation) for Twisted and all
+ subprojects.
+
+ This should be called after the version numbers have been updated and
+ NEWS files created.
+
+ @type checkout: L{FilePath}
+ @param checkout: The SVN working copy from which a pristine source tree
+ will be exported.
+ @type destination: L{FilePath}
+ @param destination: The directory in which tarballs will be placed.
+ @type templatePath: L{FilePath}
+ @param templatePath: Location of the template file that is used for the
+ howto documentation.
+
+ @raise UncleanWorkingDirectory: If there are modifications to the
+ working directory of C{checkout}.
+ @raise NotWorkingDirectory: If the C{checkout} path is not an SVN checkout.
+ """
+ if not checkout.child(".svn").exists():
+ raise NotWorkingDirectory(
+ "%s does not appear to be an SVN working directory."
+ % (checkout.path,))
+ if runCommand(["svn", "st", checkout.path]).strip():
+ raise UncleanWorkingDirectory(
+ "There are local modifications to the SVN checkout in %s."
+ % (checkout.path,))
+
+ workPath = FilePath(mkdtemp())
+ export = workPath.child("export")
+ runCommand(["svn", "export", checkout.path, export.path])
+ twistedPath = export.child("twisted")
+ version = Project(twistedPath).getVersion()
+ versionString = version.base()
+
+ apiBaseURL = "http://twistedmatrix.com/documents/%s/api/%%s.html" % (
+ versionString)
+ if not destination.exists():
+ destination.createDirectory()
+ db = DistributionBuilder(export, destination, templatePath=templatePath,
+ apiBaseURL=apiBaseURL)
+
+ db.buildCore(versionString)
+ for subproject in twisted_subprojects:
+ if twistedPath.child(subproject).exists():
+ db.buildSubProject(subproject, versionString)
+
+ db.buildTwisted(versionString)
+ workPath.remove()
+
+
+class ChangeVersionsScript(object):
+ """
+ A thing for changing version numbers. See L{main}.
+ """
+ changeAllProjectVersions = staticmethod(changeAllProjectVersions)
+
+ def main(self, args):
+ """
+ Given a list of command-line arguments, change all the Twisted versions
+ in the current directory.
+
+ @type args: list of str
+ @param args: List of command line arguments. This should only
+ contain the version number.
+ """
+ version_format = (
+ "Version should be in a form kind of like '1.2.3[pre4]'")
+ if len(args) != 1:
+ sys.exit("Must specify exactly one argument to change-versions")
+ version = args[0]
+ try:
+ major, minor, micro_and_pre = version.split(".")
+ except ValueError:
+ raise SystemExit(version_format)
+ if "pre" in micro_and_pre:
+ micro, pre = micro_and_pre.split("pre")
+ else:
+ micro = micro_and_pre
+ pre = None
+ try:
+ major = int(major)
+ minor = int(minor)
+ micro = int(micro)
+ if pre is not None:
+ pre = int(pre)
+ except ValueError:
+ raise SystemExit(version_format)
+ version_template = Version("Whatever",
+ major, minor, micro, prerelease=pre)
+ self.changeAllProjectVersions(FilePath("."), version_template)
+
+
+
+class BuildTarballsScript(object):
+ """
+ A thing for building release tarballs. See L{main}.
+ """
+ buildAllTarballs = staticmethod(buildAllTarballs)
+
+ def main(self, args):
+ """
+ Build all release tarballs.
+
+ @type args: list of C{str}
+ @param args: The command line arguments to process. This must contain
+ at least two strings: the checkout directory and the destination
+ directory. An optional third string can be specified for the website
+ template file, used for building the howto documentation. If this
+ string isn't specified, the default template included in twisted
+ will be used.
+ """
+ if len(args) < 2 or len(args) > 3:
+ sys.exit("Must specify at least two arguments: "
+ "Twisted checkout and destination path. The optional third "
+ "argument is the website template path.")
+ if len(args) == 2:
+ self.buildAllTarballs(FilePath(args[0]), FilePath(args[1]))
+ elif len(args) == 3:
+ self.buildAllTarballs(FilePath(args[0]), FilePath(args[1]),
+ FilePath(args[2]))
+
+
+
+class BuildAPIDocsScript(object):
+ """
+ A thing for building API documentation. See L{main}.
+ """
+
+ def buildAPIDocs(self, projectRoot, output):
+ """
+ Build the API documentation of Twisted, with our project policy.
+
+ @param projectRoot: A L{FilePath} representing the root of the Twisted
+ checkout.
+ @param output: A L{FilePath} pointing to the desired output directory.
+ """
+ version = Project(projectRoot.child("twisted")).getVersion()
+ versionString = version.base()
+ sourceURL = ("http://twistedmatrix.com/trac/browser/tags/releases/"
+ "twisted-%s" % (versionString,))
+ apiBuilder = APIBuilder()
+ apiBuilder.build(
+ "Twisted",
+ "http://twistedmatrix.com/",
+ sourceURL,
+ projectRoot.child("twisted"),
+ output)
+
+
+ def main(self, args):
+ """
+ Build API documentation.
+
+ @type args: list of str
+ @param args: The command line arguments to process. This must contain
+ two strings: the path to the root of the Twisted checkout, and a
+ path to an output directory.
+ """
+ if len(args) != 2:
+ sys.exit("Must specify two arguments: "
+ "Twisted checkout and destination path")
+ self.buildAPIDocs(FilePath(args[0]), FilePath(args[1]))
diff --git a/twisted/python/_shellcomp.py b/twisted/python/_shellcomp.py
new file mode 100644
index 0000000..b776802
--- /dev/null
+++ b/twisted/python/_shellcomp.py
@@ -0,0 +1,668 @@
+# -*- test-case-name: twisted.python.test.test_shellcomp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+No public APIs are provided by this module. Internal use only.
+
+This module implements dynamic tab-completion for any command that uses
+twisted.python.usage. Currently, only zsh is supported. Bash support may
+be added in the future.
+
+Maintainer: Eric P. Mangold - twisted AT teratorn DOT org
+
+In order for zsh completion to take place the shell must be able to find an
+appropriate "stub" file ("completion function") that invokes this code and
+displays the results to the user.
+
+The stub used for Twisted commands is in the file C{twisted-completion.zsh},
+which is also included in the official Zsh distribution at
+C{Completion/Unix/Command/_twisted}. Use this file as a basis for completion
+functions for your own commands. You should only need to change the first line
+to something like C{#compdef mycommand}.
+
+The main public documentation exists in the L{twisted.python.usage.Options}
+docstring, the L{twisted.python.usage.Completions} docstring, and the
+Options howto.
+"""
+import itertools, getopt, inspect
+
+from twisted.python import reflect, util, usage
+
+
+
+def shellComplete(config, cmdName, words, shellCompFile):
+ """
+ Perform shell completion.
+
+ A completion function (shell script) is generated for the requested
+ shell and written to C{shellCompFile}, typically C{stdout}. The result
+ is then eval'd by the shell to produce the desired completions.
+
+ @type config: L{twisted.python.usage.Options}
+ @param config: The L{twisted.python.usage.Options} instance to generate
+ completions for.
+
+ @type cmdName: C{str}
+ @param cmdName: The name of the command we're generating completions for.
+ In the case of zsh, this is used to print an appropriate
+ "#compdef $CMD" line at the top of the output. This is
+ not necessary for the functionality of the system, but it
+ helps in debugging, since the output we produce is properly
+ formed and may be saved in a file and used as a stand-alone
+ completion function.
+
+ @type words: C{list} of C{str}
+ @param words: The raw command-line words passed to use by the shell
+ stub function. argv[0] has already been stripped off.
+
+ @type shellCompFile: C{file}
+ @param shellCompFile: The file to write completion data to.
+ """
+ # shellName is provided for forward-compatibility. It is not used,
+ # since we currently only support zsh.
+ shellName, position = words[-1].split(":")
+ position = int(position)
+ # zsh gives the completion position ($CURRENT) as a 1-based index,
+ # and argv[0] has already been stripped off, so we subtract 2 to
+ # get the real 0-based index.
+ position -= 2
+ cWord = words[position]
+
+ # since the user may hit TAB at any time, we may have been called with an
+ # incomplete command-line that would generate getopt errors if parsed
+ # verbatim. However, we must do *some* parsing in order to determine if
+ # there is a specific subcommand that we need to provide completion for.
+ # So, to make the command-line more sane we work backwards from the
+ # current completion position and strip off all words until we find one
+ # that "looks" like a subcommand. It may in fact be the argument to a
+ # normal command-line option, but that won't matter for our purposes.
+ while position >= 1:
+ if words[position - 1].startswith("-"):
+ position -= 1
+ else:
+ break
+ words = words[:position]
+
+ subCommands = getattr(config, 'subCommands', None)
+ if subCommands:
+ # OK, this command supports sub-commands, so lets see if we have been
+ # given one.
+
+ # If the command-line arguments are not valid then we won't be able to
+ # sanely detect the sub-command, so just generate completions as if no
+ # sub-command was found.
+ args = None
+ try:
+ opts, args = getopt.getopt(words,
+ config.shortOpt, config.longOpt)
+ except getopt.error:
+ pass
+
+ if args:
+ # yes, we have a subcommand. Try to find it.
+ for (cmd, short, parser, doc) in config.subCommands:
+ if args[0] == cmd or args[0] == short:
+ subOptions = parser()
+ subOptions.parent = config
+
+ gen = ZshSubcommandBuilder(subOptions, config, cmdName,
+ shellCompFile)
+ gen.write()
+ return
+
+ # sub-command not given, or did not match any knowns sub-command names
+ genSubs = True
+ if cWord.startswith("-"):
+ # optimization: if the current word being completed starts
+ # with a hyphen then it can't be a sub-command, so skip
+ # the expensive generation of the sub-command list
+ genSubs = False
+ gen = ZshBuilder(config, cmdName, shellCompFile)
+ gen.write(genSubs=genSubs)
+ else:
+ gen = ZshBuilder(config, cmdName, shellCompFile)
+ gen.write()
+
+
+
+class SubcommandAction(usage.Completer):
+ def _shellCode(self, optName, shellType):
+ if shellType == usage._ZSH:
+ return '*::subcmd:->subcmd'
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class ZshBuilder(object):
+ """
+ Constructs zsh code that will complete options for a given usage.Options
+ instance, possibly including a list of subcommand names.
+
+ Completions for options to subcommands won't be generated because this
+ class will never be used if the user is completing options for a specific
+ subcommand. (See L{ZshSubcommandBuilder} below)
+
+ @type options: L{twisted.python.usage.Options}
+ @ivar options: The L{twisted.python.usage.Options} instance defined for this
+ command.
+
+ @type cmdName: C{str}
+ @ivar cmdName: The name of the command we're generating completions for.
+
+ @type file: C{file}
+ @ivar file: The C{file} to write the completion function to.
+ """
+ def __init__(self, options, cmdName, file):
+ self.options = options
+ self.cmdName = cmdName
+ self.file = file
+
+
+ def write(self, genSubs=True):
+ """
+ Generate the completion function and write it to the output file
+ @return: C{None}
+
+ @type genSubs: C{bool}
+ @param genSubs: Flag indicating whether or not completions for the list
+ of subcommand should be generated. Only has an effect
+ if the C{subCommands} attribute has been defined on the
+ L{twisted.python.usage.Options} instance.
+ """
+ if genSubs and getattr(self.options, 'subCommands', None) is not None:
+ gen = ZshArgumentsGenerator(self.options, self.cmdName, self.file)
+ gen.extraActions.insert(0, SubcommandAction())
+ gen.write()
+ self.file.write('local _zsh_subcmds_array\n_zsh_subcmds_array=(\n')
+ for (cmd, short, parser, desc) in self.options.subCommands:
+ self.file.write('"%s:%s"\n' % (cmd, desc))
+ self.file.write(")\n\n")
+ self.file.write('_describe "sub-command" _zsh_subcmds_array\n')
+ else:
+ gen = ZshArgumentsGenerator(self.options, self.cmdName, self.file)
+ gen.write()
+
+
+
+class ZshSubcommandBuilder(ZshBuilder):
+ """
+ Constructs zsh code that will complete options for a given usage.Options
+ instance, and also for a single sub-command. This will only be used in
+ the case where the user is completing options for a specific subcommand.
+
+ @type subOptions: L{twisted.python.usage.Options}
+ @ivar subOptions: The L{twisted.python.usage.Options} instance defined for
+ the sub command.
+ """
+ def __init__(self, subOptions, *args):
+ self.subOptions = subOptions
+ ZshBuilder.__init__(self, *args)
+
+
+ def write(self):
+ """
+ Generate the completion function and write it to the output file
+ @return: C{None}
+ """
+ gen = ZshArgumentsGenerator(self.options, self.cmdName, self.file)
+ gen.extraActions.insert(0, SubcommandAction())
+ gen.write()
+
+ gen = ZshArgumentsGenerator(self.subOptions, self.cmdName, self.file)
+ gen.write()
+
+
+
+class ZshArgumentsGenerator(object):
+ """
+ Generate a call to the zsh _arguments completion function
+ based on data in a usage.Options instance
+
+ @type options: L{twisted.python.usage.Options}
+ @ivar options: The L{twisted.python.usage.Options} instance to generate for
+
+ @type cmdName: C{str}
+ @ivar cmdName: The name of the command we're generating completions for.
+
+ @type file: C{file}
+ @ivar file: The C{file} to write the completion function to
+
+ The following non-constructor variables are populated by this class
+ with data gathered from the C{Options} instance passed in, and its
+ base classes.
+
+ @type descriptions: C{dict}
+ @ivar descriptions: A dict mapping long option names to alternate
+ descriptions. When this variable is defined, the descriptions
+ contained here will override those descriptions provided in the
+ optFlags and optParameters variables.
+
+ @type multiUse: C{list}
+ @ivar multiUse: An iterable containing those long option names which may
+ appear on the command line more than once. By default, options will
+ only be completed one time.
+
+ @type mutuallyExclusive: C{list} of C{tuple}
+ @ivar mutuallyExclusive: A sequence of sequences, with each sub-sequence
+ containing those long option names that are mutually exclusive. That is,
+ those options that cannot appear on the command line together.
+
+ @type optActions: C{dict}
+ @ivar optActions: A dict mapping long option names to shell "actions".
+ These actions define what may be completed as the argument to the
+ given option, and should be given as instances of
+ L{twisted.python.usage.Completer}.
+
+ Callables may instead be given for the values in this dict. The
+ callable should accept no arguments, and return a C{Completer}
+ instance used as the action.
+
+ @type extraActions: C{list} of C{twisted.python.usage.Completer}
+ @ivar extraActions: Extra arguments are those arguments typically
+ appearing at the end of the command-line, which are not associated
+ with any particular named option. That is, the arguments that are
+ given to the parseArgs() method of your usage.Options subclass.
+ """
+ def __init__(self, options, cmdName, file):
+ self.options = options
+ self.cmdName = cmdName
+ self.file = file
+
+ self.descriptions = {}
+ self.multiUse = set()
+ self.mutuallyExclusive = []
+ self.optActions = {}
+ self.extraActions = []
+
+ for cls in reversed(inspect.getmro(options.__class__)):
+ data = getattr(cls, 'compData', None)
+ if data:
+ self.descriptions.update(data.descriptions)
+ self.optActions.update(data.optActions)
+ self.multiUse.update(data.multiUse)
+
+ self.mutuallyExclusive.extend(data.mutuallyExclusive)
+
+ # I don't see any sane way to aggregate extraActions, so just
+ # take the one at the top of the MRO (nearest the `options'
+ # instance).
+ if data.extraActions:
+ self.extraActions = data.extraActions
+
+ aCL = reflect.accumulateClassList
+ aCD = reflect.accumulateClassDict
+
+ optFlags = []
+ optParams = []
+
+ aCL(options.__class__, 'optFlags', optFlags)
+ aCL(options.__class__, 'optParameters', optParams)
+
+ for i, optList in enumerate(optFlags):
+ if len(optList) != 3:
+ optFlags[i] = util.padTo(3, optList)
+
+ for i, optList in enumerate(optParams):
+ if len(optList) != 5:
+ optParams[i] = util.padTo(5, optList)
+
+
+ self.optFlags = optFlags
+ self.optParams = optParams
+
+ paramNameToDefinition = {}
+ for optList in optParams:
+ paramNameToDefinition[optList[0]] = optList[1:]
+ self.paramNameToDefinition = paramNameToDefinition
+
+ flagNameToDefinition = {}
+ for optList in optFlags:
+ flagNameToDefinition[optList[0]] = optList[1:]
+ self.flagNameToDefinition = flagNameToDefinition
+
+ allOptionsNameToDefinition = {}
+ allOptionsNameToDefinition.update(paramNameToDefinition)
+ allOptionsNameToDefinition.update(flagNameToDefinition)
+ self.allOptionsNameToDefinition = allOptionsNameToDefinition
+
+ self.addAdditionalOptions()
+
+ # makes sure none of the Completions metadata references
+ # option names that don't exist. (great for catching typos)
+ self.verifyZshNames()
+
+ self.excludes = self.makeExcludesDict()
+
+
+ def write(self):
+ """
+ Write the zsh completion code to the file given to __init__
+ @return: C{None}
+ """
+ self.writeHeader()
+ self.writeExtras()
+ self.writeOptions()
+ self.writeFooter()
+
+
+ def writeHeader(self):
+ """
+ This is the start of the code that calls _arguments
+ @return: C{None}
+ """
+ self.file.write('#compdef %s\n\n'
+ '_arguments -s -A "-*" \\\n' % (self.cmdName,))
+
+
+ def writeOptions(self):
+ """
+ Write out zsh code for each option in this command
+ @return: C{None}
+ """
+ optNames = self.allOptionsNameToDefinition.keys()
+ optNames.sort()
+ for longname in optNames:
+ self.writeOpt(longname)
+
+
+ def writeExtras(self):
+ """
+ Write out completion information for extra arguments appearing on the
+ command-line. These are extra positional arguments not associated
+ with a named option. That is, the stuff that gets passed to
+ Options.parseArgs().
+
+ @return: C{None}
+
+ @raises: ValueError: if C{Completer} with C{repeat=True} is found and
+ is not the last item in the C{extraActions} list.
+ """
+ for i, action in enumerate(self.extraActions):
+ descr = ""
+ if action._descr:
+ descr = action._descr
+ # a repeatable action must be the last action in the list
+ if action._repeat and i != len(self.extraActions) - 1:
+ raise ValueError("Completer with repeat=True must be "
+ "last item in Options.extraActions")
+ self.file.write(escape(action._shellCode('', usage._ZSH)))
+ self.file.write(' \\\n')
+
+
+ def writeFooter(self):
+ """
+ Write the last bit of code that finishes the call to _arguments
+ @return: C{None}
+ """
+ self.file.write('&& return 0\n')
+
+
+ def verifyZshNames(self):
+ """
+ Ensure that none of the option names given in the metadata are typoed
+ @return: C{None}
+ @raise ValueError: Raised if unknown option names have been found.
+ """
+ def err(name):
+ raise ValueError("Unknown option name \"%s\" found while\n"
+ "examining Completions instances on %s" % (
+ name, self.options))
+
+ for name in itertools.chain(self.descriptions, self.optActions,
+ self.multiUse):
+ if name not in self.allOptionsNameToDefinition:
+ err(name)
+
+ for seq in self.mutuallyExclusive:
+ for name in seq:
+ if name not in self.allOptionsNameToDefinition:
+ err(name)
+
+
+ def excludeStr(self, longname, buildShort=False):
+ """
+ Generate an "exclusion string" for the given option
+
+ @type longname: C{str}
+ @param longname: The long option name (e.g. "verbose" instead of "v")
+
+ @type buildShort: C{bool}
+ @param buildShort: May be True to indicate we're building an excludes
+ string for the short option that correspondes to the given long opt.
+
+ @return: The generated C{str}
+ """
+ if longname in self.excludes:
+ exclusions = self.excludes[longname].copy()
+ else:
+ exclusions = set()
+
+ # if longname isn't a multiUse option (can't appear on the cmd line more
+ # than once), then we have to exclude the short option if we're
+ # building for the long option, and vice versa.
+ if longname not in self.multiUse:
+ if buildShort is False:
+ short = self.getShortOption(longname)
+ if short is not None:
+ exclusions.add(short)
+ else:
+ exclusions.add(longname)
+
+ if not exclusions:
+ return ''
+
+ strings = []
+ for optName in exclusions:
+ if len(optName) == 1:
+ # short option
+ strings.append("-" + optName)
+ else:
+ strings.append("--" + optName)
+ strings.sort() # need deterministic order for reliable unit-tests
+ return "(%s)" % " ".join(strings)
+
+
+ def makeExcludesDict(self):
+ """
+ @return: A C{dict} that maps each option name appearing in
+ self.mutuallyExclusive to a list of those option names that is it
+ mutually exclusive with (can't appear on the cmd line with).
+ """
+
+ #create a mapping of long option name -> single character name
+ longToShort = {}
+ for optList in itertools.chain(self.optParams, self.optFlags):
+ if optList[1] != None:
+ longToShort[optList[0]] = optList[1]
+
+ excludes = {}
+ for lst in self.mutuallyExclusive:
+ for i, longname in enumerate(lst):
+ tmp = set(lst[:i] + lst[i+1:])
+ for name in tmp.copy():
+ if name in longToShort:
+ tmp.add(longToShort[name])
+
+ if longname in excludes:
+ excludes[longname] = excludes[longname].union(tmp)
+ else:
+ excludes[longname] = tmp
+ return excludes
+
+
+ def writeOpt(self, longname):
+ """
+ Write out the zsh code for the given argument. This is just part of the
+ one big call to _arguments
+
+ @type longname: C{str}
+ @param longname: The long option name (e.g. "verbose" instead of "v")
+
+ @return: C{None}
+ """
+ if longname in self.flagNameToDefinition:
+ # It's a flag option. Not one that takes a parameter.
+ longField = "--%s" % longname
+ else:
+ longField = "--%s=" % longname
+
+ short = self.getShortOption(longname)
+ if short != None:
+ shortField = "-" + short
+ else:
+ shortField = ''
+
+ descr = self.getDescription(longname)
+ descriptionField = descr.replace("[", "\[")
+ descriptionField = descriptionField.replace("]", "\]")
+ descriptionField = '[%s]' % descriptionField
+
+ actionField = self.getAction(longname)
+ if longname in self.multiUse:
+ multiField = '*'
+ else:
+ multiField = ''
+
+ longExclusionsField = self.excludeStr(longname)
+
+ if short:
+ #we have to write an extra line for the short option if we have one
+ shortExclusionsField = self.excludeStr(longname, buildShort=True)
+ self.file.write(escape('%s%s%s%s%s' % (shortExclusionsField,
+ multiField, shortField, descriptionField, actionField)))
+ self.file.write(' \\\n')
+
+ self.file.write(escape('%s%s%s%s%s' % (longExclusionsField,
+ multiField, longField, descriptionField, actionField)))
+ self.file.write(' \\\n')
+
+
+ def getAction(self, longname):
+ """
+ Return a zsh "action" string for the given argument
+ @return: C{str}
+ """
+ if longname in self.optActions:
+ if callable(self.optActions[longname]):
+ action = self.optActions[longname]()
+ else:
+ action = self.optActions[longname]
+ return action._shellCode(longname, usage._ZSH)
+
+ if longname in self.paramNameToDefinition:
+ return ':%s:_files' % (longname,)
+ return ''
+
+
+ def getDescription(self, longname):
+ """
+ Return the description to be used for this argument
+ @return: C{str}
+ """
+ #check if we have an alternate descr for this arg, and if so use it
+ if longname in self.descriptions:
+ return self.descriptions[longname]
+
+ #otherwise we have to get it from the optFlags or optParams
+ try:
+ descr = self.flagNameToDefinition[longname][1]
+ except KeyError:
+ try:
+ descr = self.paramNameToDefinition[longname][2]
+ except KeyError:
+ descr = None
+
+ if descr is not None:
+ return descr
+
+ # let's try to get it from the opt_foo method doc string if there is one
+ longMangled = longname.replace('-', '_') # this is what t.p.usage does
+ obj = getattr(self.options, 'opt_%s' % longMangled, None)
+ if obj is not None:
+ descr = descrFromDoc(obj)
+ if descr is not None:
+ return descr
+
+ return longname # we really ought to have a good description to use
+
+
+ def getShortOption(self, longname):
+ """
+ Return the short option letter or None
+ @return: C{str} or C{None}
+ """
+ optList = self.allOptionsNameToDefinition[longname]
+ return optList[0] or None
+
+
+ def addAdditionalOptions(self):
+ """
+ Add additional options to the optFlags and optParams lists.
+ These will be defined by 'opt_foo' methods of the Options subclass
+ @return: C{None}
+ """
+ methodsDict = {}
+ reflect.accumulateMethods(self.options, methodsDict, 'opt_')
+ methodToShort = {}
+ for name in methodsDict.copy():
+ if len(name) == 1:
+ methodToShort[methodsDict[name]] = name
+ del methodsDict[name]
+
+ for methodName, methodObj in methodsDict.items():
+ longname = methodName.replace('_', '-') # t.p.usage does this
+ # if this option is already defined by the optFlags or
+ # optParameters then we don't want to override that data
+ if longname in self.allOptionsNameToDefinition:
+ continue
+
+ descr = self.getDescription(longname)
+
+ short = None
+ if methodObj in methodToShort:
+ short = methodToShort[methodObj]
+
+ reqArgs = methodObj.im_func.func_code.co_argcount
+ if reqArgs == 2:
+ self.optParams.append([longname, short, None, descr])
+ self.paramNameToDefinition[longname] = [short, None, descr]
+ self.allOptionsNameToDefinition[longname] = [short, None, descr]
+ else:
+ # reqArgs must equal 1. self.options would have failed
+ # to instantiate if it had opt_ methods with bad signatures.
+ self.optFlags.append([longname, short, descr])
+ self.flagNameToDefinition[longname] = [short, descr]
+ self.allOptionsNameToDefinition[longname] = [short, None, descr]
+
+
+
+def descrFromDoc(obj):
+ """
+ Generate an appropriate description from docstring of the given object
+ """
+ if obj.__doc__ is None or obj.__doc__.isspace():
+ return None
+
+ lines = [x.strip() for x in obj.__doc__.split("\n")
+ if x and not x.isspace()]
+ return " ".join(lines)
+
+
+
+def escape(x):
+ """
+ Shell escape the given string
+
+ Implementation borrowed from now-deprecated commands.mkarg() in the stdlib
+ """
+ if '\'' not in x:
+ return '\'' + x + '\''
+ s = '"'
+ for c in x:
+ if c in '\\$"`':
+ s = s + '\\'
+ s = s + c
+ s = s + '"'
+ return s
+
diff --git a/twisted/python/compat.py b/twisted/python/compat.py
new file mode 100644
index 0000000..d939fff
--- /dev/null
+++ b/twisted/python/compat.py
@@ -0,0 +1,177 @@
+# -*- test-case-name: twisted.test.test_compat -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Compatibility module to provide backwards compatibility for useful Python
+features.
+
+This is mainly for use of internal Twisted code. We encourage you to use
+the latest version of Python directly from your code, if possible.
+"""
+
+import sys, string, socket, struct
+
+def inet_pton(af, addr):
+ if af == socket.AF_INET:
+ return socket.inet_aton(addr)
+ elif af == getattr(socket, 'AF_INET6', 'AF_INET6'):
+ if [x for x in addr if x not in string.hexdigits + ':.']:
+ raise ValueError("Illegal characters: %r" % (''.join(x),))
+
+ parts = addr.split(':')
+ elided = parts.count('')
+ ipv4Component = '.' in parts[-1]
+
+ if len(parts) > (8 - ipv4Component) or elided > 3:
+ raise ValueError("Syntactically invalid address")
+
+ if elided == 3:
+ return '\x00' * 16
+
+ if elided:
+ zeros = ['0'] * (8 - len(parts) - ipv4Component + elided)
+
+ if addr.startswith('::'):
+ parts[:2] = zeros
+ elif addr.endswith('::'):
+ parts[-2:] = zeros
+ else:
+ idx = parts.index('')
+ parts[idx:idx+1] = zeros
+
+ if len(parts) != 8 - ipv4Component:
+ raise ValueError("Syntactically invalid address")
+ else:
+ if len(parts) != (8 - ipv4Component):
+ raise ValueError("Syntactically invalid address")
+
+ if ipv4Component:
+ if parts[-1].count('.') != 3:
+ raise ValueError("Syntactically invalid address")
+ rawipv4 = socket.inet_aton(parts[-1])
+ unpackedipv4 = struct.unpack('!HH', rawipv4)
+ parts[-1:] = [hex(x)[2:] for x in unpackedipv4]
+
+ parts = [int(x, 16) for x in parts]
+ return struct.pack('!8H', *parts)
+ else:
+ raise socket.error(97, 'Address family not supported by protocol')
+
+def inet_ntop(af, addr):
+ if af == socket.AF_INET:
+ return socket.inet_ntoa(addr)
+ elif af == socket.AF_INET6:
+ if len(addr) != 16:
+ raise ValueError("address length incorrect")
+ parts = struct.unpack('!8H', addr)
+ curBase = bestBase = None
+ for i in range(8):
+ if not parts[i]:
+ if curBase is None:
+ curBase = i
+ curLen = 0
+ curLen += 1
+ else:
+ if curBase is not None:
+ if bestBase is None or curLen > bestLen:
+ bestBase = curBase
+ bestLen = curLen
+ curBase = None
+ if curBase is not None and (bestBase is None or curLen > bestLen):
+ bestBase = curBase
+ bestLen = curLen
+ parts = [hex(x)[2:] for x in parts]
+ if bestBase is not None:
+ parts[bestBase:bestBase + bestLen] = ['']
+ if parts[0] == '':
+ parts.insert(0, '')
+ if parts[-1] == '':
+ parts.insert(len(parts) - 1, '')
+ return ':'.join(parts)
+ else:
+ raise socket.error(97, 'Address family not supported by protocol')
+
+try:
+ socket.AF_INET6
+except AttributeError:
+ socket.AF_INET6 = 'AF_INET6'
+
+try:
+ socket.inet_pton(socket.AF_INET6, "::")
+except (AttributeError, NameError, socket.error):
+ socket.inet_pton = inet_pton
+ socket.inet_ntop = inet_ntop
+
+adict = dict
+
+# OpenSSL/__init__.py imports OpenSSL.tsafe. OpenSSL/tsafe.py imports
+# threading. threading imports thread. All to make this stupid threadsafe
+# version of its Connection class. We don't even care about threadsafe
+# Connections. In the interest of not screwing over some crazy person
+# calling into OpenSSL from another thread and trying to use Twisted's SSL
+# support, we don't totally destroy OpenSSL.tsafe, but we will replace it
+# with our own version which imports threading as late as possible.
+
+class tsafe(object):
+ class Connection:
+ """
+ OpenSSL.tsafe.Connection, defined in such a way as to not blow.
+ """
+ __module__ = 'OpenSSL.tsafe'
+
+ def __init__(self, *args):
+ from OpenSSL import SSL as _ssl
+ self._ssl_conn = apply(_ssl.Connection, args)
+ from threading import _RLock
+ self._lock = _RLock()
+
+ for f in ('get_context', 'pending', 'send', 'write', 'recv',
+ 'read', 'renegotiate', 'bind', 'listen', 'connect',
+ 'accept', 'setblocking', 'fileno', 'shutdown',
+ 'close', 'get_cipher_list', 'getpeername',
+ 'getsockname', 'getsockopt', 'setsockopt',
+ 'makefile', 'get_app_data', 'set_app_data',
+ 'state_string', 'sock_shutdown',
+ 'get_peer_certificate', 'want_read', 'want_write',
+ 'set_connect_state', 'set_accept_state',
+ 'connect_ex', 'sendall'):
+
+ exec """def %s(self, *args):
+ self._lock.acquire()
+ try:
+ return apply(self._ssl_conn.%s, args)
+ finally:
+ self._lock.release()\n""" % (f, f)
+sys.modules['OpenSSL.tsafe'] = tsafe
+
+import operator
+try:
+ operator.attrgetter
+except AttributeError:
+ class attrgetter(object):
+ def __init__(self, name):
+ self.name = name
+ def __call__(self, obj):
+ return getattr(obj, self.name)
+ operator.attrgetter = attrgetter
+
+
+try:
+ set = set
+except NameError:
+ from sets import Set as set
+
+
+try:
+ frozenset = frozenset
+except NameError:
+ from sets import ImmutableSet as frozenset
+
+
+try:
+ from functools import reduce
+except ImportError:
+ reduce = reduce
diff --git a/twisted/python/components.py b/twisted/python/components.py
new file mode 100644
index 0000000..72f15cd
--- /dev/null
+++ b/twisted/python/components.py
@@ -0,0 +1,438 @@
+# -*- test-case-name: twisted.python.test.test_components -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Component architecture for Twisted, based on Zope3 components.
+
+Using the Zope3 API directly is strongly recommended. Everything
+you need is in the top-level of the zope.interface package, e.g.::
+
+ from zope.interface import Interface, implements
+
+ class IFoo(Interface):
+ pass
+
+ class Foo:
+ implements(IFoo)
+
+ print IFoo.implementedBy(Foo) # True
+ print IFoo.providedBy(Foo()) # True
+
+L{twisted.python.components.registerAdapter} from this module may be used to
+add to Twisted's global adapter registry.
+
+L{twisted.python.components.proxyForInterface} is a factory for classes
+which allow access to only the parts of another class defined by a specified
+interface.
+"""
+
+# zope3 imports
+from zope.interface import interface, declarations
+from zope.interface.adapter import AdapterRegistry
+
+# twisted imports
+from twisted.python import reflect
+from twisted.persisted import styles
+
+
+
+# Twisted's global adapter registry
+globalRegistry = AdapterRegistry()
+
+# Attribute that registerAdapter looks at. Is this supposed to be public?
+ALLOW_DUPLICATES = 0
+
+# Define a function to find the registered adapter factory, using either a
+# version of Zope Interface which has the `registered' method or an older
+# version which does not.
+if getattr(AdapterRegistry, 'registered', None) is None:
+ def _registered(registry, required, provided):
+ """
+ Return the adapter factory for the given parameters in the given
+ registry, or None if there is not one.
+ """
+ return registry.get(required).selfImplied.get(provided, {}).get('')
+else:
+ def _registered(registry, required, provided):
+ """
+ Return the adapter factory for the given parameters in the given
+ registry, or None if there is not one.
+ """
+ return registry.registered([required], provided)
+
+
+def registerAdapter(adapterFactory, origInterface, *interfaceClasses):
+ """Register an adapter class.
+
+ An adapter class is expected to implement the given interface, by
+ adapting instances implementing 'origInterface'. An adapter class's
+ __init__ method should accept one parameter, an instance implementing
+ 'origInterface'.
+ """
+ self = globalRegistry
+ assert interfaceClasses, "You need to pass an Interface"
+ global ALLOW_DUPLICATES
+
+ # deal with class->interface adapters:
+ if not isinstance(origInterface, interface.InterfaceClass):
+ origInterface = declarations.implementedBy(origInterface)
+
+ for interfaceClass in interfaceClasses:
+ factory = _registered(self, origInterface, interfaceClass)
+ if factory is not None and not ALLOW_DUPLICATES:
+ raise ValueError("an adapter (%s) was already registered." % (factory, ))
+ for interfaceClass in interfaceClasses:
+ self.register([origInterface], interfaceClass, '', adapterFactory)
+
+
+def getAdapterFactory(fromInterface, toInterface, default):
+ """Return registered adapter for a given class and interface.
+
+ Note that is tied to the *Twisted* global registry, and will
+ thus not find adapters registered elsewhere.
+ """
+ self = globalRegistry
+ if not isinstance(fromInterface, interface.InterfaceClass):
+ fromInterface = declarations.implementedBy(fromInterface)
+ factory = self.lookup1(fromInterface, toInterface)
+ if factory is None:
+ factory = default
+ return factory
+
+
+def _addHook(registry):
+ """
+ Add an adapter hook which will attempt to look up adapters in the given
+ registry.
+
+ @type registry: L{zope.interface.adapter.AdapterRegistry}
+
+ @return: The hook which was added, for later use with L{_removeHook}.
+ """
+ lookup = registry.lookup1
+ def _hook(iface, ob):
+ factory = lookup(declarations.providedBy(ob), iface)
+ if factory is None:
+ return None
+ else:
+ return factory(ob)
+ interface.adapter_hooks.append(_hook)
+ return _hook
+
+
+def _removeHook(hook):
+ """
+ Remove a previously added adapter hook.
+
+ @param hook: An object previously returned by a call to L{_addHook}. This
+ will be removed from the list of adapter hooks.
+ """
+ interface.adapter_hooks.remove(hook)
+
+# add global adapter lookup hook for our newly created registry
+_addHook(globalRegistry)
+
+
+def getRegistry():
+ """Returns the Twisted global
+ C{zope.interface.adapter.AdapterRegistry} instance.
+ """
+ return globalRegistry
+
+# FIXME: deprecate attribute somehow?
+CannotAdapt = TypeError
+
+class Adapter:
+ """I am the default implementation of an Adapter for some interface.
+
+ This docstring contains a limerick, by popular demand::
+
+ Subclassing made Zope and TR
+ much harder to work with by far.
+ So before you inherit,
+ be sure to declare it
+ Adapter, not PyObject*
+
+ @cvar temporaryAdapter: If this is True, the adapter will not be
+ persisted on the Componentized.
+ @cvar multiComponent: If this adapter is persistent, should it be
+ automatically registered for all appropriate interfaces.
+ """
+
+ # These attributes are used with Componentized.
+
+ temporaryAdapter = 0
+ multiComponent = 1
+
+ def __init__(self, original):
+ """Set my 'original' attribute to be the object I am adapting.
+ """
+ self.original = original
+
+ def __conform__(self, interface):
+ """
+ I forward __conform__ to self.original if it has it, otherwise I
+ simply return None.
+ """
+ if hasattr(self.original, "__conform__"):
+ return self.original.__conform__(interface)
+ return None
+
+ def isuper(self, iface, adapter):
+ """
+ Forward isuper to self.original
+ """
+ return self.original.isuper(iface, adapter)
+
+
+class Componentized(styles.Versioned):
+ """I am a mixin to allow you to be adapted in various ways persistently.
+
+ I define a list of persistent adapters. This is to allow adapter classes
+ to store system-specific state, and initialized on demand. The
+ getComponent method implements this. You must also register adapters for
+ this class for the interfaces that you wish to pass to getComponent.
+
+ Many other classes and utilities listed here are present in Zope3; this one
+ is specific to Twisted.
+ """
+
+ persistenceVersion = 1
+
+ def __init__(self):
+ self._adapterCache = {}
+
+ def locateAdapterClass(self, klass, interfaceClass, default):
+ return getAdapterFactory(klass, interfaceClass, default)
+
+ def setAdapter(self, interfaceClass, adapterClass):
+ self.setComponent(interfaceClass, adapterClass(self))
+
+ def addAdapter(self, adapterClass, ignoreClass=0):
+ """Utility method that calls addComponent. I take an adapter class and
+ instantiate it with myself as the first argument.
+
+ @return: The adapter instantiated.
+ """
+ adapt = adapterClass(self)
+ self.addComponent(adapt, ignoreClass)
+ return adapt
+
+ def setComponent(self, interfaceClass, component):
+ """
+ """
+ self._adapterCache[reflect.qual(interfaceClass)] = component
+
+ def addComponent(self, component, ignoreClass=0):
+ """
+ Add a component to me, for all appropriate interfaces.
+
+ In order to determine which interfaces are appropriate, the component's
+ provided interfaces will be scanned.
+
+ If the argument 'ignoreClass' is True, then all interfaces are
+ considered appropriate.
+
+ Otherwise, an 'appropriate' interface is one for which its class has
+ been registered as an adapter for my class according to the rules of
+ getComponent.
+
+ @return: the list of appropriate interfaces
+ """
+ for iface in declarations.providedBy(component):
+ if (ignoreClass or
+ (self.locateAdapterClass(self.__class__, iface, None)
+ == component.__class__)):
+ self._adapterCache[reflect.qual(iface)] = component
+
+ def unsetComponent(self, interfaceClass):
+ """Remove my component specified by the given interface class."""
+ del self._adapterCache[reflect.qual(interfaceClass)]
+
+ def removeComponent(self, component):
+ """
+ Remove the given component from me entirely, for all interfaces for which
+ it has been registered.
+
+ @return: a list of the interfaces that were removed.
+ """
+ l = []
+ for k, v in self._adapterCache.items():
+ if v is component:
+ del self._adapterCache[k]
+ l.append(reflect.namedObject(k))
+ return l
+
+ def getComponent(self, interface, default=None):
+ """Create or retrieve an adapter for the given interface.
+
+ If such an adapter has already been created, retrieve it from the cache
+ that this instance keeps of all its adapters. Adapters created through
+ this mechanism may safely store system-specific state.
+
+ If you want to register an adapter that will be created through
+ getComponent, but you don't require (or don't want) your adapter to be
+ cached and kept alive for the lifetime of this Componentized object,
+ set the attribute 'temporaryAdapter' to True on your adapter class.
+
+ If you want to automatically register an adapter for all appropriate
+ interfaces (with addComponent), set the attribute 'multiComponent' to
+ True on your adapter class.
+ """
+ k = reflect.qual(interface)
+ if self._adapterCache.has_key(k):
+ return self._adapterCache[k]
+ else:
+ adapter = interface.__adapt__(self)
+ if adapter is not None and not (
+ hasattr(adapter, "temporaryAdapter") and
+ adapter.temporaryAdapter):
+ self._adapterCache[k] = adapter
+ if (hasattr(adapter, "multiComponent") and
+ adapter.multiComponent):
+ self.addComponent(adapter)
+ if adapter is None:
+ return default
+ return adapter
+
+
+ def __conform__(self, interface):
+ return self.getComponent(interface)
+
+
+class ReprableComponentized(Componentized):
+ def __init__(self):
+ Componentized.__init__(self)
+
+ def __repr__(self):
+ from cStringIO import StringIO
+ from pprint import pprint
+ sio = StringIO()
+ pprint(self._adapterCache, sio)
+ return sio.getvalue()
+
+
+
+def proxyForInterface(iface, originalAttribute='original'):
+ """
+ Create a class which proxies all method calls which adhere to an interface
+ to another provider of that interface.
+
+ This function is intended for creating specialized proxies. The typical way
+ to use it is by subclassing the result::
+
+ class MySpecializedProxy(proxyForInterface(IFoo)):
+ def someInterfaceMethod(self, arg):
+ if arg == 3:
+ return 3
+ return self.original.someInterfaceMethod(arg)
+
+ @param iface: The Interface to which the resulting object will conform, and
+ which the wrapped object must provide.
+
+ @param originalAttribute: name of the attribute used to save the original
+ object in the resulting class. Default to C{original}.
+ @type originalAttribute: C{str}
+
+ @return: A class whose constructor takes the original object as its only
+ argument. Constructing the class creates the proxy.
+ """
+ def __init__(self, original):
+ setattr(self, originalAttribute, original)
+ contents = {"__init__": __init__}
+ for name in iface:
+ contents[name] = _ProxyDescriptor(name, originalAttribute)
+ proxy = type("(Proxy for %s)"
+ % (reflect.qual(iface),), (object,), contents)
+ declarations.classImplements(proxy, iface)
+ return proxy
+
+
+
+class _ProxiedClassMethod(object):
+ """
+ A proxied class method.
+
+ @ivar methodName: the name of the method which this should invoke when
+ called.
+ @type methodName: C{str}
+
+ @ivar originalAttribute: name of the attribute of the proxy where the
+ original object is stored.
+ @type orginalAttribute: C{str}
+ """
+ def __init__(self, methodName, originalAttribute):
+ self.methodName = methodName
+ self.originalAttribute = originalAttribute
+
+
+ def __call__(self, oself, *args, **kw):
+ """
+ Invoke the specified L{methodName} method of the C{original} attribute
+ for proxyForInterface.
+
+ @param oself: an instance of a L{proxyForInterface} object.
+
+ @return: the result of the underlying method.
+ """
+ original = getattr(oself, self.originalAttribute)
+ actualMethod = getattr(original, self.methodName)
+ return actualMethod(*args, **kw)
+
+
+
+class _ProxyDescriptor(object):
+ """
+ A descriptor which will proxy attribute access, mutation, and
+ deletion to the L{original} attribute of the object it is being accessed
+ from.
+
+ @ivar attributeName: the name of the attribute which this descriptor will
+ retrieve from instances' C{original} attribute.
+ @type attributeName: C{str}
+
+ @ivar originalAttribute: name of the attribute of the proxy where the
+ original object is stored.
+ @type orginalAttribute: C{str}
+ """
+ def __init__(self, attributeName, originalAttribute):
+ self.attributeName = attributeName
+ self.originalAttribute = originalAttribute
+
+
+ def __get__(self, oself, type=None):
+ """
+ Retrieve the C{self.attributeName} property from L{oself}.
+ """
+ if oself is None:
+ return _ProxiedClassMethod(self.attributeName,
+ self.originalAttribute)
+ original = getattr(oself, self.originalAttribute)
+ return getattr(original, self.attributeName)
+
+
+ def __set__(self, oself, value):
+ """
+ Set the C{self.attributeName} property of L{oself}.
+ """
+ original = getattr(oself, self.originalAttribute)
+ setattr(original, self.attributeName, value)
+
+
+ def __delete__(self, oself):
+ """
+ Delete the C{self.attributeName} property of L{oself}.
+ """
+ original = getattr(oself, self.originalAttribute)
+ delattr(original, self.attributeName)
+
+
+
+__all__ = [
+ # Sticking around:
+ "registerAdapter", "getAdapterFactory",
+ "Adapter", "Componentized", "ReprableComponentized", "getRegistry",
+ "proxyForInterface",
+]
diff --git a/twisted/python/constants.py b/twisted/python/constants.py
new file mode 100644
index 0000000..db708d6
--- /dev/null
+++ b/twisted/python/constants.py
@@ -0,0 +1,377 @@
+# -*- test-case-name: twisted.python.test.test_constants -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Symbolic constant support, including collections and constants with text,
+numeric, and bit flag values.
+"""
+
+__all__ = [
+ 'NamedConstant', 'ValueConstant', 'FlagConstant',
+ 'Names', 'Values', 'Flags']
+
+from itertools import count
+from operator import and_, or_, xor
+
+_unspecified = object()
+_constantOrder = count().next
+
+
+class _Constant(object):
+ """
+ @ivar _index: A C{int} allocated from a shared counter in order to keep
+ track of the order in which L{_Constant}s are instantiated.
+
+ @ivar name: A C{str} giving the name of this constant; only set once the
+ constant is initialized by L{_ConstantsContainer}.
+
+ @ivar _container: The L{_ConstantsContainer} subclass this constant belongs
+ to; only set once the constant is initialized by that subclass.
+ """
+ def __init__(self):
+ self._index = _constantOrder()
+
+
+ def __get__(self, oself, cls):
+ """
+ Ensure this constant has been initialized before returning it.
+ """
+ cls._initializeEnumerants()
+ return self
+
+
+ def __repr__(self):
+ """
+ Return text identifying both which constant this is and which collection
+ it belongs to.
+ """
+ return "<%s=%s>" % (self._container.__name__, self.name)
+
+
+ def _realize(self, container, name, value):
+ """
+ Complete the initialization of this L{_Constant}.
+
+ @param container: The L{_ConstantsContainer} subclass this constant is
+ part of.
+
+ @param name: The name of this constant in its container.
+
+ @param value: The value of this constant; not used, as named constants
+ have no value apart from their identity.
+ """
+ self._container = container
+ self.name = name
+
+
+
+class _EnumerantsInitializer(object):
+ """
+ L{_EnumerantsInitializer} is a descriptor used to initialize a cache of
+ objects representing named constants for a particular L{_ConstantsContainer}
+ subclass.
+ """
+ def __get__(self, oself, cls):
+ """
+ Trigger the initialization of the enumerants cache on C{cls} and then
+ return it.
+ """
+ cls._initializeEnumerants()
+ return cls._enumerants
+
+
+
+class _ConstantsContainer(object):
+ """
+ L{_ConstantsContainer} is a class with attributes used as symbolic
+ constants. It is up to subclasses to specify what kind of constants are
+ allowed.
+
+ @cvar _constantType: Specified by a L{_ConstantsContainer} subclass to
+ specify the type of constants allowed by that subclass.
+
+ @cvar _enumerantsInitialized: A C{bool} tracking whether C{_enumerants} has
+ been initialized yet or not.
+
+ @cvar _enumerants: A C{dict} mapping the names of constants (eg
+ L{NamedConstant} instances) found in the class definition to those
+ instances. This is initialized via the L{_EnumerantsInitializer}
+ descriptor the first time it is accessed.
+ """
+ _constantType = None
+
+ _enumerantsInitialized = False
+ _enumerants = _EnumerantsInitializer()
+
+ def __new__(cls):
+ """
+ Classes representing constants containers are not intended to be
+ instantiated.
+
+ The class object itself is used directly.
+ """
+ raise TypeError("%s may not be instantiated." % (cls.__name__,))
+
+
+ def _initializeEnumerants(cls):
+ """
+ Find all of the L{NamedConstant} instances in the definition of C{cls},
+ initialize them with constant values, and build a mapping from their
+ names to them to attach to C{cls}.
+ """
+ if not cls._enumerantsInitialized:
+ constants = []
+ for (name, descriptor) in cls.__dict__.iteritems():
+ if isinstance(descriptor, cls._constantType):
+ constants.append((descriptor._index, name, descriptor))
+ enumerants = {}
+ constants.sort()
+ for (index, enumerant, descriptor) in constants:
+ value = cls._constantFactory(enumerant, descriptor)
+ descriptor._realize(cls, enumerant, value)
+ enumerants[enumerant] = descriptor
+ # Replace the _enumerants descriptor with the result so future
+ # access will go directly to the values. The _enumerantsInitialized
+ # flag is still necessary because NamedConstant.__get__ may also
+ # call this method.
+ cls._enumerants = enumerants
+ cls._enumerantsInitialized = True
+ _initializeEnumerants = classmethod(_initializeEnumerants)
+
+
+ def _constantFactory(cls, name, descriptor):
+ """
+ Construct the value for a new constant to add to this container.
+
+ @param name: The name of the constant to create.
+
+ @return: L{NamedConstant} instances have no value apart from identity,
+ so return a meaningless dummy value.
+ """
+ return _unspecified
+ _constantFactory = classmethod(_constantFactory)
+
+
+ def lookupByName(cls, name):
+ """
+ Retrieve a constant by its name or raise a C{ValueError} if there is no
+ constant associated with that name.
+
+ @param name: A C{str} giving the name of one of the constants defined by
+ C{cls}.
+
+ @raise ValueError: If C{name} is not the name of one of the constants
+ defined by C{cls}.
+
+ @return: The L{NamedConstant} associated with C{name}.
+ """
+ if name in cls._enumerants:
+ return getattr(cls, name)
+ raise ValueError(name)
+ lookupByName = classmethod(lookupByName)
+
+
+ def iterconstants(cls):
+ """
+ Iteration over a L{Names} subclass results in all of the constants it
+ contains.
+
+ @return: an iterator the elements of which are the L{NamedConstant}
+ instances defined in the body of this L{Names} subclass.
+ """
+ constants = cls._enumerants.values()
+ constants.sort(key=lambda descriptor: descriptor._index)
+ return iter(constants)
+ iterconstants = classmethod(iterconstants)
+
+
+
+class NamedConstant(_Constant):
+ """
+ L{NamedConstant} defines an attribute to be a named constant within a
+ collection defined by a L{Names} subclass.
+
+ L{NamedConstant} is only for use in the definition of L{Names}
+ subclasses. Do not instantiate L{NamedConstant} elsewhere and do not
+ subclass it.
+ """
+
+
+
+class Names(_ConstantsContainer):
+ """
+ A L{Names} subclass contains constants which differ only in their names and
+ identities.
+ """
+ _constantType = NamedConstant
+
+
+
+class ValueConstant(_Constant):
+ """
+ L{ValueConstant} defines an attribute to be a named constant within a
+ collection defined by a L{Values} subclass.
+
+ L{ValueConstant} is only for use in the definition of L{Values} subclasses.
+ Do not instantiate L{ValueConstant} elsewhere and do not subclass it.
+ """
+ def __init__(self, value):
+ _Constant.__init__(self)
+ self.value = value
+
+
+
+class Values(_ConstantsContainer):
+ """
+ A L{Values} subclass contains constants which are associated with arbitrary
+ values.
+ """
+ _constantType = ValueConstant
+
+ def lookupByValue(cls, value):
+ """
+ Retrieve a constant by its value or raise a C{ValueError} if there is no
+ constant associated with that value.
+
+ @param value: The value of one of the constants defined by C{cls}.
+
+ @raise ValueError: If C{value} is not the value of one of the constants
+ defined by C{cls}.
+
+ @return: The L{ValueConstant} associated with C{value}.
+ """
+ for constant in cls.iterconstants():
+ if constant.value == value:
+ return constant
+ raise ValueError(value)
+ lookupByValue = classmethod(lookupByValue)
+
+
+
+def _flagOp(op, left, right):
+ """
+ Implement a binary operator for a L{FlagConstant} instance.
+
+ @param op: A two-argument callable implementing the binary operation. For
+ example, C{operator.or_}.
+
+ @param left: The left-hand L{FlagConstant} instance.
+ @param right: The right-hand L{FlagConstant} instance.
+
+ @return: A new L{FlagConstant} instance representing the result of the
+ operation.
+ """
+ value = op(left.value, right.value)
+ names = op(left.names, right.names)
+ result = FlagConstant()
+ result._realize(left._container, names, value)
+ return result
+
+
+
+class FlagConstant(_Constant):
+ """
+ L{FlagConstant} defines an attribute to be a flag constant within a
+ collection defined by a L{Flags} subclass.
+
+ L{FlagConstant} is only for use in the definition of L{Flags} subclasses.
+ Do not instantiate L{FlagConstant} elsewhere and do not subclass it.
+ """
+ def __init__(self, value=_unspecified):
+ _Constant.__init__(self)
+ self.value = value
+
+
+ def _realize(self, container, names, value):
+ """
+ Complete the initialization of this L{FlagConstant}.
+
+ This implementation differs from other C{_realize} implementations in
+ that a L{FlagConstant} may have several names which apply to it, due to
+ flags being combined with various operators.
+
+ @param container: The L{Flags} subclass this constant is part of.
+
+ @param names: When a single-flag value is being initialized, a C{str}
+ giving the name of that flag. This is the case which happens when a
+ L{Flags} subclass is being initialized and L{FlagConstant} instances
+ from its body are being realized. Otherwise, a C{set} of C{str}
+ giving names of all the flags set on this L{FlagConstant} instance.
+ This is the case when two flags are combined using C{|}, for
+ example.
+ """
+ if isinstance(names, str):
+ name = names
+ names = set([names])
+ elif len(names) == 1:
+ (name,) = names
+ else:
+ name = "{" + ",".join(sorted(names)) + "}"
+ _Constant._realize(self, container, name, value)
+ self.value = value
+ self.names = names
+
+
+ def __or__(self, other):
+ """
+ Define C{|} on two L{FlagConstant} instances to create a new
+ L{FlagConstant} instance with all flags set in either instance set.
+ """
+ return _flagOp(or_, self, other)
+
+
+ def __and__(self, other):
+ """
+ Define C{&} on two L{FlagConstant} instances to create a new
+ L{FlagConstant} instance with only flags set in both instances set.
+ """
+ return _flagOp(and_, self, other)
+
+
+ def __xor__(self, other):
+ """
+ Define C{^} on two L{FlagConstant} instances to create a new
+ L{FlagConstant} instance with only flags set on exactly one instance
+ set.
+ """
+ return _flagOp(xor, self, other)
+
+
+ def __invert__(self):
+ """
+ Define C{~} on a L{FlagConstant} instance to create a new
+ L{FlagConstant} instance with all flags not set on this instance set.
+ """
+ result = FlagConstant()
+ result._realize(self._container, set(), 0)
+ for flag in self._container.iterconstants():
+ if flag.value & self.value == 0:
+ result |= flag
+ return result
+
+
+
+class Flags(Values):
+ """
+ A L{Flags} subclass contains constants which can be combined using the
+ common bitwise operators (C{|}, C{&}, etc) similar to a I{bitvector} from a
+ language like C.
+ """
+ _constantType = FlagConstant
+
+ _value = 1
+
+ def _constantFactory(cls, name, descriptor):
+ """
+ For L{FlagConstant} instances with no explicitly defined value, assign
+ the next power of two as its value.
+ """
+ if descriptor.value is _unspecified:
+ value = cls._value
+ cls._value <<= 1
+ else:
+ value = descriptor.value
+ cls._value = value << 1
+ return value
+ _constantFactory = classmethod(_constantFactory)
diff --git a/twisted/python/context.py b/twisted/python/context.py
new file mode 100644
index 0000000..2bda75f
--- /dev/null
+++ b/twisted/python/context.py
@@ -0,0 +1,133 @@
+# -*- test-case-name: twisted.test.test_context -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+Dynamic pseudo-scoping for Python.
+
+Call functions with context.call({key: value}, func); func and
+functions that it calls will be able to use 'context.get(key)' to
+retrieve 'value'.
+
+This is thread-safe.
+"""
+
+from threading import local
+
+defaultContextDict = {}
+
+setDefault = defaultContextDict.__setitem__
+
+class ContextTracker:
+ """
+ A L{ContextTracker} provides a way to pass arbitrary key/value data up and
+ down a call stack without passing them as parameters to the functions on
+ that call stack.
+
+ This can be useful when functions on the top and bottom of the call stack
+ need to cooperate but the functions in between them do not allow passing the
+ necessary state. For example::
+
+ from twisted.python.context import call, get
+
+ def handleRequest(request):
+ call({'request-id': request.id}, renderRequest, request.url)
+
+ def renderRequest(url):
+ renderHeader(url)
+ renderBody(url)
+
+ def renderHeader(url):
+ return "the header"
+
+ def renderBody(url):
+ return "the body (request id=%r)" % (get("request-id"),)
+
+ This should be used sparingly, since the lack of a clear connection between
+ the two halves can result in code which is difficult to understand and
+ maintain.
+
+ @ivar contexts: A C{list} of C{dict}s tracking the context state. Each new
+ L{ContextTracker.callWithContext} pushes a new C{dict} onto this stack
+ for the duration of the call, making the data available to the function
+ called and restoring the previous data once it is complete..
+ """
+ def __init__(self):
+ self.contexts = [defaultContextDict]
+
+
+ def callWithContext(self, newContext, func, *args, **kw):
+ """
+ Call C{func(*args, **kw)} such that the contents of C{newContext} will
+ be available for it to retrieve using L{getContext}.
+
+ @param newContext: A C{dict} of data to push onto the context for the
+ duration of the call to C{func}.
+
+ @param func: A callable which will be called.
+
+ @param *args: Any additional positional arguments to pass to C{func}.
+
+ @param **kw: Any additional keyword arguments to pass to C{func}.
+
+ @return: Whatever is returned by C{func}
+
+ @raise: Whatever is raised by C{func}.
+ """
+ self.contexts.append(newContext)
+ try:
+ return func(*args,**kw)
+ finally:
+ self.contexts.pop()
+
+
+ def getContext(self, key, default=None):
+ """
+ Retrieve the value for a key from the context.
+
+ @param key: The key to look up in the context.
+
+ @param default: The value to return if C{key} is not found in the
+ context.
+
+ @return: The value most recently remembered in the context for C{key}.
+ """
+ for ctx in reversed(self.contexts):
+ try:
+ return ctx[key]
+ except KeyError:
+ pass
+ return default
+
+
+
+class ThreadedContextTracker(object):
+ def __init__(self):
+ self.storage = local()
+
+ def currentContext(self):
+ try:
+ return self.storage.ct
+ except AttributeError:
+ ct = self.storage.ct = ContextTracker()
+ return ct
+
+ def callWithContext(self, ctx, func, *args, **kw):
+ return self.currentContext().callWithContext(ctx, func, *args, **kw)
+
+ def getContext(self, key, default=None):
+ return self.currentContext().getContext(key, default)
+
+
+def installContextTracker(ctr):
+ global theContextTracker
+ global call
+ global get
+
+ theContextTracker = ctr
+ call = theContextTracker.callWithContext
+ get = theContextTracker.getContext
+
+installContextTracker(ThreadedContextTracker())
diff --git a/twisted/python/deprecate.py b/twisted/python/deprecate.py
new file mode 100644
index 0000000..f4b31b4
--- /dev/null
+++ b/twisted/python/deprecate.py
@@ -0,0 +1,534 @@
+# -*- test-case-name: twisted.python.test.test_deprecate -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Deprecation framework for Twisted.
+
+To mark a method or function as being deprecated do this::
+
+ from twisted.python.versions import Version
+ from twisted.python.deprecate import deprecated
+
+ @deprecated(Version("Twisted", 8, 0, 0))
+ def badAPI(self, first, second):
+ '''
+ Docstring for badAPI.
+ '''
+ ...
+
+The newly-decorated badAPI will issue a warning when called. It will also have
+a deprecation notice appended to its docstring.
+
+To mark module-level attributes as being deprecated you can use::
+
+ badAttribute = "someValue"
+
+ ...
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 8, 0, 0),
+ "Use goodAttribute instead.",
+ "your.full.module.name",
+ "badAttribute")
+
+The deprecated attributes will issue a warning whenever they are accessed. If
+the attributes being deprecated are in the same module as the
+L{deprecatedModuleAttribute} call is being made from, the C{__name__} global
+can be used as the C{moduleName} parameter.
+
+See also L{Version}.
+
+@type DEPRECATION_WARNING_FORMAT: C{str}
+@var DEPRECATION_WARNING_FORMAT: The default deprecation warning string format
+ to use when one is not provided by the user.
+"""
+
+
+__all__ = [
+ 'deprecated',
+ 'getDeprecationWarningString',
+ 'getWarningMethod',
+ 'setWarningMethod',
+ 'deprecatedModuleAttribute',
+ ]
+
+
+import sys, inspect
+from warnings import warn, warn_explicit
+from dis import findlinestarts
+
+from twisted.python.versions import getVersionString
+from twisted.python.util import mergeFunctionMetadata
+
+
+
+DEPRECATION_WARNING_FORMAT = '%(fqpn)s was deprecated in %(version)s'
+
+
+# Notionally, part of twisted.python.reflect, but defining it there causes a
+# cyclic dependency between this module and that module. Define it here,
+# instead, and let reflect import it to re-expose to the public.
+def _fullyQualifiedName(obj):
+ """
+ Return the fully qualified name of a module, class, method or function.
+ Classes and functions need to be module level ones to be correctly
+ qualified.
+
+ @rtype: C{str}.
+ """
+ name = obj.__name__
+ if inspect.isclass(obj) or inspect.isfunction(obj):
+ moduleName = obj.__module__
+ return "%s.%s" % (moduleName, name)
+ elif inspect.ismethod(obj):
+ className = _fullyQualifiedName(obj.im_class)
+ return "%s.%s" % (className, name)
+ return name
+# Try to keep it looking like something in twisted.python.reflect.
+_fullyQualifiedName.__module__ = 'twisted.python.reflect'
+_fullyQualifiedName.__name__ = 'fullyQualifiedName'
+
+
+
+def getWarningMethod():
+ """
+ Return the warning method currently used to record deprecation warnings.
+ """
+ return warn
+
+
+
+def setWarningMethod(newMethod):
+ """
+ Set the warning method to use to record deprecation warnings.
+
+ The callable should take message, category and stacklevel. The return
+ value is ignored.
+ """
+ global warn
+ warn = newMethod
+
+
+
+def _getDeprecationDocstring(version, replacement=None):
+ """
+ Generate an addition to a deprecated object's docstring that explains its
+ deprecation.
+
+ @param version: the version it was deprecated.
+ @type version: L{Version}
+
+ @param replacement: The replacement, if specified.
+ @type replacement: C{str} or callable
+
+ @return: a string like "Deprecated in Twisted 27.2.0; please use
+ twisted.timestream.tachyon.flux instead."
+ """
+ doc = "Deprecated in %s" % (getVersionString(version),)
+ if replacement:
+ doc = "%s; %s" % (doc, _getReplacementString(replacement))
+ return doc + "."
+
+
+
+def _getReplacementString(replacement):
+ """
+ Surround a replacement for a deprecated API with some polite text exhorting
+ the user to consider it as an alternative.
+
+ @type replacement: C{str} or callable
+
+ @return: a string like "please use twisted.python.modules.getModule
+ instead".
+ """
+ if callable(replacement):
+ replacement = _fullyQualifiedName(replacement)
+ return "please use %s instead" % (replacement,)
+
+
+
+def _getDeprecationWarningString(fqpn, version, format=None, replacement=None):
+ """
+ Return a string indicating that the Python name was deprecated in the given
+ version.
+
+ @param fqpn: Fully qualified Python name of the thing being deprecated
+ @type fqpn: C{str}
+
+ @param version: Version that C{fqpn} was deprecated in.
+ @type version: L{twisted.python.versions.Version}
+
+ @param format: A user-provided format to interpolate warning values into, or
+ L{DEPRECATION_WARNING_FORMAT
+ <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if C{None} is
+ given.
+ @type format: C{str}
+
+ @param replacement: what should be used in place of C{fqpn}. Either pass in
+ a string, which will be inserted into the warning message, or a
+ callable, which will be expanded to its full import path.
+ @type replacement: C{str} or callable
+
+ @return: A textual description of the deprecation
+ @rtype: C{str}
+ """
+ if format is None:
+ format = DEPRECATION_WARNING_FORMAT
+ warningString = format % {
+ 'fqpn': fqpn,
+ 'version': getVersionString(version)}
+ if replacement:
+ warningString = "%s; %s" % (
+ warningString, _getReplacementString(replacement))
+ return warningString
+
+
+
+def getDeprecationWarningString(callableThing, version, format=None,
+ replacement=None):
+ """
+ Return a string indicating that the callable was deprecated in the given
+ version.
+
+ @type callableThing: C{callable}
+ @param callableThing: Callable object to be deprecated
+
+ @type version: L{twisted.python.versions.Version}
+ @param version: Version that C{callableThing} was deprecated in
+
+ @type format: C{str}
+ @param format: A user-provided format to interpolate warning values into,
+ or L{DEPRECATION_WARNING_FORMAT
+ <twisted.python.deprecate.DEPRECATION_WARNING_FORMAT>} if C{None} is
+ given
+
+ @param callableThing: A callable to be deprecated.
+
+ @param version: The L{twisted.python.versions.Version} that the callable
+ was deprecated in.
+
+ @param replacement: what should be used in place of the callable. Either
+ pass in a string, which will be inserted into the warning message,
+ or a callable, which will be expanded to its full import path.
+ @type replacement: C{str} or callable
+
+ @return: A string describing the deprecation.
+ @rtype: C{str}
+ """
+ return _getDeprecationWarningString(
+ _fullyQualifiedName(callableThing), version, format, replacement)
+
+
+
+def deprecated(version, replacement=None):
+ """
+ Return a decorator that marks callables as deprecated.
+
+ @type version: L{twisted.python.versions.Version}
+ @param version: The version in which the callable will be marked as
+ having been deprecated. The decorated function will be annotated
+ with this version, having it set as its C{deprecatedVersion}
+ attribute.
+
+ @param version: the version that the callable was deprecated in.
+ @type version: L{twisted.python.versions.Version}
+
+ @param replacement: what should be used in place of the callable. Either
+ pass in a string, which will be inserted into the warning message,
+ or a callable, which will be expanded to its full import path.
+ @type replacement: C{str} or callable
+ """
+ def deprecationDecorator(function):
+ """
+ Decorator that marks C{function} as deprecated.
+ """
+ warningString = getDeprecationWarningString(
+ function, version, None, replacement)
+
+ def deprecatedFunction(*args, **kwargs):
+ warn(
+ warningString,
+ DeprecationWarning,
+ stacklevel=2)
+ return function(*args, **kwargs)
+
+ deprecatedFunction = mergeFunctionMetadata(
+ function, deprecatedFunction)
+ _appendToDocstring(deprecatedFunction,
+ _getDeprecationDocstring(version, replacement))
+ deprecatedFunction.deprecatedVersion = version
+ return deprecatedFunction
+
+ return deprecationDecorator
+
+
+
+def _appendToDocstring(thingWithDoc, textToAppend):
+ """
+ Append the given text to the docstring of C{thingWithDoc}.
+
+ If C{thingWithDoc} has no docstring, then the text just replaces the
+ docstring. If it has a single-line docstring then it appends a blank line
+ and the message text. If it has a multi-line docstring, then in appends a
+ blank line a the message text, and also does the indentation correctly.
+ """
+ if thingWithDoc.__doc__:
+ docstringLines = thingWithDoc.__doc__.splitlines()
+ else:
+ docstringLines = []
+
+ if len(docstringLines) == 0:
+ docstringLines.append(textToAppend)
+ elif len(docstringLines) == 1:
+ docstringLines.extend(['', textToAppend, ''])
+ else:
+ spaces = docstringLines.pop()
+ docstringLines.extend(['',
+ spaces + textToAppend,
+ spaces])
+ thingWithDoc.__doc__ = '\n'.join(docstringLines)
+
+
+
+class _InternalState(object):
+ """
+ An L{_InternalState} is a helper object for a L{_ModuleProxy}, so that it
+ can easily access its own attributes, bypassing its logic for delegating to
+ another object that it's proxying for.
+
+ @ivar proxy: a L{ModuleProxy}
+ """
+ def __init__(self, proxy):
+ object.__setattr__(self, 'proxy', proxy)
+
+
+ def __getattribute__(self, name):
+ return object.__getattribute__(object.__getattribute__(self, 'proxy'),
+ name)
+
+
+ def __setattr__(self, name, value):
+ return object.__setattr__(object.__getattribute__(self, 'proxy'),
+ name, value)
+
+
+
+class _ModuleProxy(object):
+ """
+ Python module wrapper to hook module-level attribute access.
+
+ Access to deprecated attributes first checks
+ L{_ModuleProxy._deprecatedAttributes}, if the attribute does not appear
+ there then access falls through to L{_ModuleProxy._module}, the wrapped
+ module object.
+
+ @ivar _module: Module on which to hook attribute access.
+ @type _module: C{module}
+
+ @ivar _deprecatedAttributes: Mapping of attribute names to objects that
+ retrieve the module attribute's original value.
+ @type _deprecatedAttributes: C{dict} mapping C{str} to
+ L{_DeprecatedAttribute}
+
+ @ivar _lastWasPath: Heuristic guess as to whether warnings about this
+ package should be ignored for the next call. If the last attribute
+ access of this module was a C{getattr} of C{__path__}, we will assume
+ that it was the import system doing it and we won't emit a warning for
+ the next access, even if it is to a deprecated attribute. The CPython
+ import system always tries to access C{__path__}, then the attribute
+ itself, then the attribute itself again, in both successful and failed
+ cases.
+ @type _lastWasPath: C{bool}
+ """
+ def __init__(self, module):
+ state = _InternalState(self)
+ state._module = module
+ state._deprecatedAttributes = {}
+ state._lastWasPath = False
+
+
+ def __repr__(self):
+ """
+ Get a string containing the type of the module proxy and a
+ representation of the wrapped module object.
+ """
+ state = _InternalState(self)
+ return '<%s module=%r>' % (type(self).__name__, state._module)
+
+
+ def __setattr__(self, name, value):
+ """
+ Set an attribute on the wrapped module object.
+ """
+ state = _InternalState(self)
+ state._lastWasPath = False
+ setattr(state._module, name, value)
+
+
+ def __getattribute__(self, name):
+ """
+ Get an attribute from the module object, possibly emitting a warning.
+
+ If the specified name has been deprecated, then a warning is issued.
+ (Unless certain obscure conditions are met; see
+ L{_ModuleProxy._lastWasPath} for more information about what might quash
+ such a warning.)
+ """
+ state = _InternalState(self)
+ if state._lastWasPath:
+ deprecatedAttribute = None
+ else:
+ deprecatedAttribute = state._deprecatedAttributes.get(name)
+
+ if deprecatedAttribute is not None:
+ # If we have a _DeprecatedAttribute object from the earlier lookup,
+ # allow it to issue the warning.
+ value = deprecatedAttribute.get()
+ else:
+ # Otherwise, just retrieve the underlying value directly; it's not
+ # deprecated, there's no warning to issue.
+ value = getattr(state._module, name)
+ if name == '__path__':
+ state._lastWasPath = True
+ else:
+ state._lastWasPath = False
+ return value
+
+
+
+class _DeprecatedAttribute(object):
+ """
+ Wrapper for deprecated attributes.
+
+ This is intended to be used by L{_ModuleProxy}. Calling
+ L{_DeprecatedAttribute.get} will issue a warning and retrieve the
+ underlying attribute's value.
+
+ @type module: C{module}
+ @ivar module: The original module instance containing this attribute
+
+ @type fqpn: C{str}
+ @ivar fqpn: Fully qualified Python name for the deprecated attribute
+
+ @type version: L{twisted.python.versions.Version}
+ @ivar version: Version that the attribute was deprecated in
+
+ @type message: C{str}
+ @ivar message: Deprecation message
+ """
+ def __init__(self, module, name, version, message):
+ """
+ Initialise a deprecated name wrapper.
+ """
+ self.module = module
+ self.__name__ = name
+ self.fqpn = module.__name__ + '.' + name
+ self.version = version
+ self.message = message
+
+
+ def get(self):
+ """
+ Get the underlying attribute value and issue a deprecation warning.
+ """
+ # This might fail if the deprecated thing is a module inside a package.
+ # In that case, don't emit the warning this time. The import system
+ # will come back again when it's not an AttributeError and we can emit
+ # the warning then.
+ result = getattr(self.module, self.__name__)
+ message = _getDeprecationWarningString(self.fqpn, self.version,
+ DEPRECATION_WARNING_FORMAT + ': ' + self.message)
+ warn(message, DeprecationWarning, stacklevel=3)
+ return result
+
+
+
+def _deprecateAttribute(proxy, name, version, message):
+ """
+ Mark a module-level attribute as being deprecated.
+
+ @type proxy: L{_ModuleProxy}
+ @param proxy: The module proxy instance proxying the deprecated attributes
+
+ @type name: C{str}
+ @param name: Attribute name
+
+ @type version: L{twisted.python.versions.Version}
+ @param version: Version that the attribute was deprecated in
+
+ @type message: C{str}
+ @param message: Deprecation message
+ """
+ _module = object.__getattribute__(proxy, '_module')
+ attr = _DeprecatedAttribute(_module, name, version, message)
+ # Add a deprecated attribute marker for this module's attribute. When this
+ # attribute is accessed via _ModuleProxy a warning is emitted.
+ _deprecatedAttributes = object.__getattribute__(
+ proxy, '_deprecatedAttributes')
+ _deprecatedAttributes[name] = attr
+
+
+
+def deprecatedModuleAttribute(version, message, moduleName, name):
+ """
+ Declare a module-level attribute as being deprecated.
+
+ @type version: L{twisted.python.versions.Version}
+ @param version: Version that the attribute was deprecated in
+
+ @type message: C{str}
+ @param message: Deprecation message
+
+ @type moduleName: C{str}
+ @param moduleName: Fully-qualified Python name of the module containing
+ the deprecated attribute; if called from the same module as the
+ attributes are being deprecated in, using the C{__name__} global can
+ be helpful
+
+ @type name: C{str}
+ @param name: Attribute name to deprecate
+ """
+ module = sys.modules[moduleName]
+ if not isinstance(module, _ModuleProxy):
+ module = _ModuleProxy(module)
+ sys.modules[moduleName] = module
+
+ _deprecateAttribute(module, name, version, message)
+
+
+def warnAboutFunction(offender, warningString):
+ """
+ Issue a warning string, identifying C{offender} as the responsible code.
+
+ This function is used to deprecate some behavior of a function. It differs
+ from L{warnings.warn} in that it is not limited to deprecating the behavior
+ of a function currently on the call stack.
+
+ @param function: The function that is being deprecated.
+
+ @param warningString: The string that should be emitted by this warning.
+ @type warningString: C{str}
+
+ @since: 11.0
+ """
+ # inspect.getmodule() is attractive, but somewhat
+ # broken in Python < 2.6. See Python bug 4845.
+ offenderModule = sys.modules[offender.__module__]
+ filename = inspect.getabsfile(offenderModule)
+ lineStarts = list(findlinestarts(offender.func_code))
+ lastLineNo = lineStarts[-1][1]
+ globals = offender.func_globals
+
+ kwargs = dict(
+ category=DeprecationWarning,
+ filename=filename,
+ lineno=lastLineNo,
+ module=offenderModule.__name__,
+ registry=globals.setdefault("__warningregistry__", {}),
+ module_globals=None)
+
+ if sys.version_info[:2] < (2, 5):
+ kwargs.pop('module_globals')
+
+ warn_explicit(warningString, **kwargs)
diff --git a/twisted/python/dist.py b/twisted/python/dist.py
new file mode 100644
index 0000000..a4daac3
--- /dev/null
+++ b/twisted/python/dist.py
@@ -0,0 +1,401 @@
+"""
+Distutils convenience functionality.
+
+Don't use this outside of Twisted.
+
+Maintainer: Christopher Armstrong
+"""
+
+from distutils.command import build_scripts, install_data, build_ext
+from distutils.errors import CompileError
+from distutils import core
+from distutils.core import Extension
+import fnmatch
+import os
+import platform
+import sys
+
+
+twisted_subprojects = ["conch", "lore", "mail", "names",
+ "news", "pair", "runner", "web",
+ "words"]
+
+
+
+class ConditionalExtension(Extension):
+ """
+ An extension module that will only be compiled if certain conditions are
+ met.
+
+ @param condition: A callable of one argument which returns True or False to
+ indicate whether the extension should be built. The argument is an
+ instance of L{build_ext_twisted}, which has useful methods for checking
+ things about the platform.
+ """
+ def __init__(self, *args, **kwargs):
+ self.condition = kwargs.pop("condition", lambda builder: True)
+ Extension.__init__(self, *args, **kwargs)
+
+
+
+def setup(**kw):
+ """
+ An alternative to distutils' setup() which is specially designed
+ for Twisted subprojects.
+
+ Pass twisted_subproject=projname if you want package and data
+ files to automatically be found for you.
+
+ @param conditionalExtensions: Extensions to optionally build.
+ @type conditionalExtensions: C{list} of L{ConditionalExtension}
+ """
+ return core.setup(**get_setup_args(**kw))
+
+
+def get_setup_args(**kw):
+ if 'twisted_subproject' in kw:
+ if 'twisted' not in os.listdir('.'):
+ raise RuntimeError("Sorry, you need to run setup.py from the "
+ "toplevel source directory.")
+ projname = kw['twisted_subproject']
+ projdir = os.path.join('twisted', projname)
+
+ kw['packages'] = getPackages(projdir, parent='twisted')
+ kw['version'] = getVersion(projname)
+
+ plugin = "twisted/plugins/twisted_" + projname + ".py"
+ if os.path.exists(plugin):
+ kw.setdefault('py_modules', []).append(
+ plugin.replace("/", ".")[:-3])
+
+ kw['data_files'] = getDataFiles(projdir, parent='twisted')
+
+ del kw['twisted_subproject']
+ else:
+ if 'plugins' in kw:
+ py_modules = []
+ for plg in kw['plugins']:
+ py_modules.append("twisted.plugins." + plg)
+ kw.setdefault('py_modules', []).extend(py_modules)
+ del kw['plugins']
+
+ if 'cmdclass' not in kw:
+ kw['cmdclass'] = {
+ 'install_data': install_data_twisted,
+ 'build_scripts': build_scripts_twisted}
+
+ if "conditionalExtensions" in kw:
+ extensions = kw["conditionalExtensions"]
+ del kw["conditionalExtensions"]
+
+ if 'ext_modules' not in kw:
+ # This is a workaround for distutils behavior; ext_modules isn't
+ # actually used by our custom builder. distutils deep-down checks
+ # to see if there are any ext_modules defined before invoking
+ # the build_ext command. We need to trigger build_ext regardless
+ # because it is the thing that does the conditional checks to see
+ # if it should build any extensions. The reason we have to delay
+ # the conditional checks until then is that the compiler objects
+ # are not yet set up when this code is executed.
+ kw["ext_modules"] = extensions
+
+ class my_build_ext(build_ext_twisted):
+ conditionalExtensions = extensions
+ kw.setdefault('cmdclass', {})['build_ext'] = my_build_ext
+ return kw
+
+
+def getVersion(proj, base="twisted"):
+ """
+ Extract the version number for a given project.
+
+ @param proj: the name of the project. Examples are "core",
+ "conch", "words", "mail".
+
+ @rtype: str
+ @returns: The version number of the project, as a string like
+ "2.0.0".
+ """
+ if proj == 'core':
+ vfile = os.path.join(base, '_version.py')
+ else:
+ vfile = os.path.join(base, proj, '_version.py')
+ ns = {'__name__': 'Nothing to see here'}
+ execfile(vfile, ns)
+ return ns['version'].base()
+
+
+# Names that are exluded from globbing results:
+EXCLUDE_NAMES = ["{arch}", "CVS", ".cvsignore", "_darcs",
+ "RCS", "SCCS", ".svn"]
+EXCLUDE_PATTERNS = ["*.py[cdo]", "*.s[ol]", ".#*", "*~", "*.py"]
+
+
+def _filterNames(names):
+ """
+ Given a list of file names, return those names that should be copied.
+ """
+ names = [n for n in names
+ if n not in EXCLUDE_NAMES]
+ # This is needed when building a distro from a working
+ # copy (likely a checkout) rather than a pristine export:
+ for pattern in EXCLUDE_PATTERNS:
+ names = [n for n in names
+ if (not fnmatch.fnmatch(n, pattern))
+ and (not n.endswith('.py'))]
+ return names
+
+
+def relativeTo(base, relativee):
+ """
+ Gets 'relativee' relative to 'basepath'.
+
+ i.e.,
+
+ >>> relativeTo('/home/', '/home/radix/')
+ 'radix'
+ >>> relativeTo('.', '/home/radix/Projects/Twisted') # curdir is /home/radix
+ 'Projects/Twisted'
+
+ The 'relativee' must be a child of 'basepath'.
+ """
+ basepath = os.path.abspath(base)
+ relativee = os.path.abspath(relativee)
+ if relativee.startswith(basepath):
+ relative = relativee[len(basepath):]
+ if relative.startswith(os.sep):
+ relative = relative[1:]
+ return os.path.join(base, relative)
+ raise ValueError("%s is not a subpath of %s" % (relativee, basepath))
+
+
+def getDataFiles(dname, ignore=None, parent=None):
+ """
+ Get all the data files that should be included in this distutils Project.
+
+ 'dname' should be the path to the package that you're distributing.
+
+ 'ignore' is a list of sub-packages to ignore. This facilitates
+ disparate package hierarchies. That's a fancy way of saying that
+ the 'twisted' package doesn't want to include the 'twisted.conch'
+ package, so it will pass ['conch'] as the value.
+
+ 'parent' is necessary if you're distributing a subpackage like
+ twisted.conch. 'dname' should point to 'twisted/conch' and 'parent'
+ should point to 'twisted'. This ensures that your data_files are
+ generated correctly, only using relative paths for the first element
+ of the tuple ('twisted/conch/*').
+ The default 'parent' is the current working directory.
+ """
+ parent = parent or "."
+ ignore = ignore or []
+ result = []
+ for directory, subdirectories, filenames in os.walk(dname):
+ resultfiles = []
+ for exname in EXCLUDE_NAMES:
+ if exname in subdirectories:
+ subdirectories.remove(exname)
+ for ig in ignore:
+ if ig in subdirectories:
+ subdirectories.remove(ig)
+ for filename in _filterNames(filenames):
+ resultfiles.append(filename)
+ if resultfiles:
+ result.append((relativeTo(parent, directory),
+ [relativeTo(parent,
+ os.path.join(directory, filename))
+ for filename in resultfiles]))
+ return result
+
+
+def getPackages(dname, pkgname=None, results=None, ignore=None, parent=None):
+ """
+ Get all packages which are under dname. This is necessary for
+ Python 2.2's distutils. Pretty similar arguments to getDataFiles,
+ including 'parent'.
+ """
+ parent = parent or ""
+ prefix = []
+ if parent:
+ prefix = [parent]
+ bname = os.path.basename(dname)
+ ignore = ignore or []
+ if bname in ignore:
+ return []
+ if results is None:
+ results = []
+ if pkgname is None:
+ pkgname = []
+ subfiles = os.listdir(dname)
+ abssubfiles = [os.path.join(dname, x) for x in subfiles]
+ if '__init__.py' in subfiles:
+ results.append(prefix + pkgname + [bname])
+ for subdir in filter(os.path.isdir, abssubfiles):
+ getPackages(subdir, pkgname=pkgname + [bname],
+ results=results, ignore=ignore,
+ parent=parent)
+ res = ['.'.join(result) for result in results]
+ return res
+
+
+def getScripts(projname, basedir=''):
+ """
+ Returns a list of scripts for a Twisted subproject; this works in
+ any of an SVN checkout, a project-specific tarball.
+ """
+ scriptdir = os.path.join(basedir, 'bin', projname)
+ if not os.path.isdir(scriptdir):
+ # Probably a project-specific tarball, in which case only this
+ # project's bins are included in 'bin'
+ scriptdir = os.path.join(basedir, 'bin')
+ if not os.path.isdir(scriptdir):
+ return []
+ thingies = os.listdir(scriptdir)
+ for specialExclusion in ['.svn', '_preamble.py', '_preamble.pyc']:
+ if specialExclusion in thingies:
+ thingies.remove(specialExclusion)
+ return filter(os.path.isfile,
+ [os.path.join(scriptdir, x) for x in thingies])
+
+
+## Helpers and distutil tweaks
+
+class build_scripts_twisted(build_scripts.build_scripts):
+ """Renames scripts so they end with '.py' on Windows."""
+
+ def run(self):
+ build_scripts.build_scripts.run(self)
+ if not os.name == "nt":
+ return
+ for f in os.listdir(self.build_dir):
+ fpath=os.path.join(self.build_dir, f)
+ if not fpath.endswith(".py"):
+ try:
+ os.unlink(fpath + ".py")
+ except EnvironmentError, e:
+ if e.args[1]=='No such file or directory':
+ pass
+ os.rename(fpath, fpath + ".py")
+
+
+
+class install_data_twisted(install_data.install_data):
+ """I make sure data files are installed in the package directory."""
+ def finalize_options(self):
+ self.set_undefined_options('install',
+ ('install_lib', 'install_dir')
+ )
+ install_data.install_data.finalize_options(self)
+
+
+
+class build_ext_twisted(build_ext.build_ext):
+ """
+ Allow subclasses to easily detect and customize Extensions to
+ build at install-time.
+ """
+
+ def prepare_extensions(self):
+ """
+ Prepare the C{self.extensions} attribute (used by
+ L{build_ext.build_ext}) by checking which extensions in
+ L{conditionalExtensions} should be built. In addition, if we are
+ building on NT, define the WIN32 macro to 1.
+ """
+ # always define WIN32 under Windows
+ if os.name == 'nt':
+ self.define_macros = [("WIN32", 1)]
+ else:
+ self.define_macros = []
+ self.extensions = [x for x in self.conditionalExtensions
+ if x.condition(self)]
+ for ext in self.extensions:
+ ext.define_macros.extend(self.define_macros)
+
+
+ def build_extensions(self):
+ """
+ Check to see which extension modules to build and then build them.
+ """
+ self.prepare_extensions()
+ build_ext.build_ext.build_extensions(self)
+
+
+ def _remove_conftest(self):
+ for filename in ("conftest.c", "conftest.o", "conftest.obj"):
+ try:
+ os.unlink(filename)
+ except EnvironmentError:
+ pass
+
+
+ def _compile_helper(self, content):
+ conftest = open("conftest.c", "w")
+ try:
+ conftest.write(content)
+ conftest.close()
+
+ try:
+ self.compiler.compile(["conftest.c"], output_dir='')
+ except CompileError:
+ return False
+ return True
+ finally:
+ self._remove_conftest()
+
+
+ def _check_header(self, header_name):
+ """
+ Check if the given header can be included by trying to compile a file
+ that contains only an #include line.
+ """
+ self.compiler.announce("checking for %s ..." % header_name, 0)
+ return self._compile_helper("#include <%s>\n" % header_name)
+
+
+
+def _checkCPython(sys=sys, platform=platform):
+ """
+ Checks if this implementation is CPython.
+
+ On recent versions of Python, will use C{platform.python_implementation}.
+ On 2.5, it will try to extract the implementation from sys.subversion. On
+ older versions (currently the only supported older version is 2.4), checks
+ if C{__pypy__} is in C{sys.modules}, since PyPy is the implementation we
+ really care about. If it isn't, assumes CPython.
+
+ This takes C{sys} and C{platform} kwargs that by default use the real
+ modules. You shouldn't care about these -- they are for testing purposes
+ only.
+
+ @return: C{False} if the implementation is definitely not CPython, C{True}
+ otherwise.
+ """
+ try:
+ return platform.python_implementation() == "CPython"
+ except AttributeError:
+ # For 2.5:
+ try:
+ implementation, _, _ = sys.subversion
+ return implementation == "CPython"
+ except AttributeError:
+ pass
+
+ # Are we on Pypy?
+ if "__pypy__" in sys.modules:
+ return False
+
+ # No? Well, then we're *probably* on CPython.
+ return True
+
+
+_isCPython = _checkCPython()
+
+
+def _hasEpoll(builder):
+ """
+ Checks if the header for building epoll (C{sys/epoll.h}) is available.
+
+ @return: C{True} if the header is available, C{False} otherwise.
+ """
+ return builder._check_header("sys/epoll.h")
diff --git a/twisted/python/failure.py b/twisted/python/failure.py
new file mode 100644
index 0000000..ed03281
--- /dev/null
+++ b/twisted/python/failure.py
@@ -0,0 +1,650 @@
+# -*- test-case-name: twisted.test.test_failure -*-
+# See also test suite twisted.test.test_pbfailure
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Asynchronous-friendly error mechanism.
+
+See L{Failure}.
+"""
+
+# System Imports
+import sys
+import linecache
+import inspect
+import opcode
+from cStringIO import StringIO
+from inspect import getmro
+
+from twisted.python import reflect
+
+count = 0
+traceupLength = 4
+
+class DefaultException(Exception):
+ pass
+
+def format_frames(frames, write, detail="default"):
+ """Format and write frames.
+
+ @param frames: is a list of frames as used by Failure.frames, with
+ each frame being a list of
+ (funcName, fileName, lineNumber, locals.items(), globals.items())
+ @type frames: list
+ @param write: this will be called with formatted strings.
+ @type write: callable
+ @param detail: Four detail levels are available:
+ default, brief, verbose, and verbose-vars-not-captured.
+ C{Failure.printDetailedTraceback} uses the latter when the caller asks
+ for verbose, but no vars were captured, so that an explicit warning
+ about the missing data is shown.
+ @type detail: string
+ """
+ if detail not in ('default', 'brief', 'verbose',
+ 'verbose-vars-not-captured'):
+ raise ValueError(
+ "Detail must be default, brief, verbose, or "
+ "verbose-vars-not-captured. (not %r)" % (detail,))
+ w = write
+ if detail == "brief":
+ for method, filename, lineno, localVars, globalVars in frames:
+ w('%s:%s:%s\n' % (filename, lineno, method))
+ elif detail == "default":
+ for method, filename, lineno, localVars, globalVars in frames:
+ w( ' File "%s", line %s, in %s\n' % (filename, lineno, method))
+ w( ' %s\n' % linecache.getline(filename, lineno).strip())
+ elif detail == "verbose-vars-not-captured":
+ for method, filename, lineno, localVars, globalVars in frames:
+ w("%s:%d: %s(...)\n" % (filename, lineno, method))
+ w(' [Capture of Locals and Globals disabled (use captureVars=True)]\n')
+ elif detail == "verbose":
+ for method, filename, lineno, localVars, globalVars in frames:
+ w("%s:%d: %s(...)\n" % (filename, lineno, method))
+ w(' [ Locals ]\n')
+ # Note: the repr(val) was (self.pickled and val) or repr(val)))
+ for name, val in localVars:
+ w(" %s : %s\n" % (name, repr(val)))
+ w(' ( Globals )\n')
+ for name, val in globalVars:
+ w(" %s : %s\n" % (name, repr(val)))
+
+# slyphon: i have a need to check for this value in trial
+# so I made it a module-level constant
+EXCEPTION_CAUGHT_HERE = "--- <exception caught here> ---"
+
+
+
+class NoCurrentExceptionError(Exception):
+ """
+ Raised when trying to create a Failure from the current interpreter
+ exception state and there is no current exception state.
+ """
+
+
+class _Traceback(object):
+ """
+ Fake traceback object which can be passed to functions in the standard
+ library L{traceback} module.
+ """
+
+ def __init__(self, frames):
+ """
+ Construct a fake traceback object using a list of frames. Note that
+ although frames generally include locals and globals, this information
+ is not kept by this object, since locals and globals are not used in
+ standard tracebacks.
+
+ @param frames: [(methodname, filename, lineno, locals, globals), ...]
+ """
+ assert len(frames) > 0, "Must pass some frames"
+ head, frames = frames[0], frames[1:]
+ name, filename, lineno, localz, globalz = head
+ self.tb_frame = _Frame(name, filename)
+ self.tb_lineno = lineno
+ if len(frames) == 0:
+ self.tb_next = None
+ else:
+ self.tb_next = _Traceback(frames)
+
+
+class _Frame(object):
+ """
+ A fake frame object, used by L{_Traceback}.
+
+ @ivar f_code: fake L{code<types.CodeType>} object
+ @ivar f_globals: fake f_globals dictionary (usually empty)
+ @ivar f_locals: fake f_locals dictionary (usually empty)
+ """
+
+ def __init__(self, name, filename):
+ """
+ @param name: method/function name for this frame.
+ @type name: C{str}
+ @param filename: filename for this frame.
+ @type name: C{str}
+ """
+ self.f_code = _Code(name, filename)
+ self.f_globals = {}
+ self.f_locals = {}
+
+
+class _Code(object):
+ """
+ A fake code object, used by L{_Traceback} via L{_Frame}.
+ """
+ def __init__(self, name, filename):
+ self.co_name = name
+ self.co_filename = filename
+
+
+class Failure:
+ """
+ A basic abstraction for an error that has occurred.
+
+ This is necessary because Python's built-in error mechanisms are
+ inconvenient for asynchronous communication.
+
+ The C{stack} and C{frame} attributes contain frames. Each frame is a tuple
+ of (funcName, fileName, lineNumber, localsItems, globalsItems), where
+ localsItems and globalsItems are the contents of
+ C{locals().items()}/C{globals().items()} for that frame, or an empty tuple
+ if those details were not captured.
+
+ @ivar value: The exception instance responsible for this failure.
+ @ivar type: The exception's class.
+ @ivar stack: list of frames, innermost last, excluding C{Failure.__init__}.
+ @ivar frames: list of frames, innermost first.
+ """
+
+ pickled = 0
+ stack = None
+
+ # The opcode of "yield" in Python bytecode. We need this in _findFailure in
+ # order to identify whether an exception was thrown by a
+ # throwExceptionIntoGenerator.
+ _yieldOpcode = chr(opcode.opmap["YIELD_VALUE"])
+
+ def __init__(self, exc_value=None, exc_type=None, exc_tb=None,
+ captureVars=False):
+ """
+ Initialize me with an explanation of the error.
+
+ By default, this will use the current C{exception}
+ (L{sys.exc_info}()). However, if you want to specify a
+ particular kind of failure, you can pass an exception as an
+ argument.
+
+ If no C{exc_value} is passed, then an "original" C{Failure} will
+ be searched for. If the current exception handler that this
+ C{Failure} is being constructed in is handling an exception
+ raised by L{raiseException}, then this C{Failure} will act like
+ the original C{Failure}.
+
+ For C{exc_tb} only L{traceback} instances or C{None} are allowed.
+ If C{None} is supplied for C{exc_value}, the value of C{exc_tb} is
+ ignored, otherwise if C{exc_tb} is C{None}, it will be found from
+ execution context (ie, L{sys.exc_info}).
+
+ @param captureVars: if set, capture locals and globals of stack
+ frames. This is pretty slow, and makes no difference unless you
+ are going to use L{printDetailedTraceback}.
+ """
+ global count
+ count = count + 1
+ self.count = count
+ self.type = self.value = tb = None
+ self.captureVars = captureVars
+
+ #strings Exceptions/Failures are bad, mmkay?
+ if isinstance(exc_value, (str, unicode)) and exc_type is None:
+ import warnings
+ warnings.warn(
+ "Don't pass strings (like %r) to failure.Failure (replacing with a DefaultException)." %
+ exc_value, DeprecationWarning, stacklevel=2)
+ exc_value = DefaultException(exc_value)
+
+ stackOffset = 0
+
+ if exc_value is None:
+ exc_value = self._findFailure()
+
+ if exc_value is None:
+ self.type, self.value, tb = sys.exc_info()
+ if self.type is None:
+ raise NoCurrentExceptionError()
+ stackOffset = 1
+ elif exc_type is None:
+ if isinstance(exc_value, Exception):
+ self.type = exc_value.__class__
+ else: #allow arbitrary objects.
+ self.type = type(exc_value)
+ self.value = exc_value
+ else:
+ self.type = exc_type
+ self.value = exc_value
+ if isinstance(self.value, Failure):
+ self.__dict__ = self.value.__dict__
+ return
+ if tb is None:
+ if exc_tb:
+ tb = exc_tb
+# else:
+# log.msg("Erf, %r created with no traceback, %s %s." % (
+# repr(self), repr(exc_value), repr(exc_type)))
+# for s in traceback.format_stack():
+# log.msg(s)
+
+ frames = self.frames = []
+ stack = self.stack = []
+
+ # added 2003-06-23 by Chris Armstrong. Yes, I actually have a
+ # use case where I need this traceback object, and I've made
+ # sure that it'll be cleaned up.
+ self.tb = tb
+
+ if tb:
+ f = tb.tb_frame
+ elif not isinstance(self.value, Failure):
+ # we don't do frame introspection since it's expensive,
+ # and if we were passed a plain exception with no
+ # traceback, it's not useful anyway
+ f = stackOffset = None
+
+ while stackOffset and f:
+ # This excludes this Failure.__init__ frame from the
+ # stack, leaving it to start with our caller instead.
+ f = f.f_back
+ stackOffset -= 1
+
+ # Keeps the *full* stack. Formerly in spread.pb.print_excFullStack:
+ #
+ # The need for this function arises from the fact that several
+ # PB classes have the peculiar habit of discarding exceptions
+ # with bareword "except:"s. This premature exception
+ # catching means tracebacks generated here don't tend to show
+ # what called upon the PB object.
+
+ while f:
+ if captureVars:
+ localz = f.f_locals.copy()
+ if f.f_locals is f.f_globals:
+ globalz = {}
+ else:
+ globalz = f.f_globals.copy()
+ for d in globalz, localz:
+ if "__builtins__" in d:
+ del d["__builtins__"]
+ localz = localz.items()
+ globalz = globalz.items()
+ else:
+ localz = globalz = ()
+ stack.insert(0, (
+ f.f_code.co_name,
+ f.f_code.co_filename,
+ f.f_lineno,
+ localz,
+ globalz,
+ ))
+ f = f.f_back
+
+ while tb is not None:
+ f = tb.tb_frame
+ if captureVars:
+ localz = f.f_locals.copy()
+ if f.f_locals is f.f_globals:
+ globalz = {}
+ else:
+ globalz = f.f_globals.copy()
+ for d in globalz, localz:
+ if "__builtins__" in d:
+ del d["__builtins__"]
+ localz = localz.items()
+ globalz = globalz.items()
+ else:
+ localz = globalz = ()
+ frames.append((
+ f.f_code.co_name,
+ f.f_code.co_filename,
+ tb.tb_lineno,
+ localz,
+ globalz,
+ ))
+ tb = tb.tb_next
+ if inspect.isclass(self.type) and issubclass(self.type, Exception):
+ parentCs = getmro(self.type)
+ self.parents = map(reflect.qual, parentCs)
+ else:
+ self.parents = [self.type]
+
+ def trap(self, *errorTypes):
+ """Trap this failure if its type is in a predetermined list.
+
+ This allows you to trap a Failure in an error callback. It will be
+ automatically re-raised if it is not a type that you expect.
+
+ The reason for having this particular API is because it's very useful
+ in Deferred errback chains::
+
+ def _ebFoo(self, failure):
+ r = failure.trap(Spam, Eggs)
+ print 'The Failure is due to either Spam or Eggs!'
+ if r == Spam:
+ print 'Spam did it!'
+ elif r == Eggs:
+ print 'Eggs did it!'
+
+ If the failure is not a Spam or an Eggs, then the Failure
+ will be 'passed on' to the next errback.
+
+ @type errorTypes: L{Exception}
+ """
+ error = self.check(*errorTypes)
+ if not error:
+ raise self
+ return error
+
+ def check(self, *errorTypes):
+ """Check if this failure's type is in a predetermined list.
+
+ @type errorTypes: list of L{Exception} classes or
+ fully-qualified class names.
+ @returns: the matching L{Exception} type, or None if no match.
+ """
+ for error in errorTypes:
+ err = error
+ if inspect.isclass(error) and issubclass(error, Exception):
+ err = reflect.qual(error)
+ if err in self.parents:
+ return error
+ return None
+
+
+ def raiseException(self):
+ """
+ raise the original exception, preserving traceback
+ information if available.
+ """
+ raise self.type, self.value, self.tb
+
+
+ def throwExceptionIntoGenerator(self, g):
+ """
+ Throw the original exception into the given generator,
+ preserving traceback information if available.
+
+ @return: The next value yielded from the generator.
+ @raise StopIteration: If there are no more values in the generator.
+ @raise anything else: Anything that the generator raises.
+ """
+ return g.throw(self.type, self.value, self.tb)
+
+
+ def _findFailure(cls):
+ """
+ Find the failure that represents the exception currently in context.
+ """
+ tb = sys.exc_info()[-1]
+ if not tb:
+ return
+
+ secondLastTb = None
+ lastTb = tb
+ while lastTb.tb_next:
+ secondLastTb = lastTb
+ lastTb = lastTb.tb_next
+
+ lastFrame = lastTb.tb_frame
+
+ # NOTE: f_locals.get('self') is used rather than
+ # f_locals['self'] because psyco frames do not contain
+ # anything in their locals() dicts. psyco makes debugging
+ # difficult anyhow, so losing the Failure objects (and thus
+ # the tracebacks) here when it is used is not that big a deal.
+
+ # handle raiseException-originated exceptions
+ if lastFrame.f_code is cls.raiseException.func_code:
+ return lastFrame.f_locals.get('self')
+
+ # handle throwExceptionIntoGenerator-originated exceptions
+ # this is tricky, and differs if the exception was caught
+ # inside the generator, or above it:
+
+ # it is only really originating from
+ # throwExceptionIntoGenerator if the bottom of the traceback
+ # is a yield.
+ # Pyrex and Cython extensions create traceback frames
+ # with no co_code, but they can't yield so we know it's okay to just return here.
+ if ((not lastFrame.f_code.co_code) or
+ lastFrame.f_code.co_code[lastTb.tb_lasti] != cls._yieldOpcode):
+ return
+
+ # if the exception was caught above the generator.throw
+ # (outside the generator), it will appear in the tb (as the
+ # second last item):
+ if secondLastTb:
+ frame = secondLastTb.tb_frame
+ if frame.f_code is cls.throwExceptionIntoGenerator.func_code:
+ return frame.f_locals.get('self')
+
+ # if the exception was caught below the generator.throw
+ # (inside the generator), it will appear in the frames' linked
+ # list, above the top-level traceback item (which must be the
+ # generator frame itself, thus its caller is
+ # throwExceptionIntoGenerator).
+ frame = tb.tb_frame.f_back
+ if frame and frame.f_code is cls.throwExceptionIntoGenerator.func_code:
+ return frame.f_locals.get('self')
+
+ _findFailure = classmethod(_findFailure)
+
+ def __repr__(self):
+ return "<%s %s>" % (self.__class__, self.type)
+
+ def __str__(self):
+ return "[Failure instance: %s]" % self.getBriefTraceback()
+
+ def __getstate__(self):
+ """Avoid pickling objects in the traceback.
+ """
+ if self.pickled:
+ return self.__dict__
+ c = self.__dict__.copy()
+
+ c['frames'] = [
+ [
+ v[0], v[1], v[2],
+ _safeReprVars(v[3]),
+ _safeReprVars(v[4]),
+ ] for v in self.frames
+ ]
+
+ # added 2003-06-23. See comment above in __init__
+ c['tb'] = None
+
+ if self.stack is not None:
+ # XXX: This is a band-aid. I can't figure out where these
+ # (failure.stack is None) instances are coming from.
+ c['stack'] = [
+ [
+ v[0], v[1], v[2],
+ _safeReprVars(v[3]),
+ _safeReprVars(v[4]),
+ ] for v in self.stack
+ ]
+
+ c['pickled'] = 1
+ return c
+
+ def cleanFailure(self):
+ """Remove references to other objects, replacing them with strings.
+ """
+ self.__dict__ = self.__getstate__()
+
+ def getTracebackObject(self):
+ """
+ Get an object that represents this Failure's stack that can be passed
+ to traceback.extract_tb.
+
+ If the original traceback object is still present, return that. If this
+ traceback object has been lost but we still have the information,
+ return a fake traceback object (see L{_Traceback}). If there is no
+ traceback information at all, return None.
+ """
+ if self.tb is not None:
+ return self.tb
+ elif len(self.frames) > 0:
+ return _Traceback(self.frames)
+ else:
+ return None
+
+ def getErrorMessage(self):
+ """Get a string of the exception which caused this Failure."""
+ if isinstance(self.value, Failure):
+ return self.value.getErrorMessage()
+ return reflect.safe_str(self.value)
+
+ def getBriefTraceback(self):
+ io = StringIO()
+ self.printBriefTraceback(file=io)
+ return io.getvalue()
+
+ def getTraceback(self, elideFrameworkCode=0, detail='default'):
+ io = StringIO()
+ self.printTraceback(file=io, elideFrameworkCode=elideFrameworkCode, detail=detail)
+ return io.getvalue()
+
+
+ def printTraceback(self, file=None, elideFrameworkCode=False, detail='default'):
+ """
+ Emulate Python's standard error reporting mechanism.
+
+ @param file: If specified, a file-like object to which to write the
+ traceback.
+
+ @param elideFrameworkCode: A flag indicating whether to attempt to
+ remove uninteresting frames from within Twisted itself from the
+ output.
+
+ @param detail: A string indicating how much information to include
+ in the traceback. Must be one of C{'brief'}, C{'default'}, or
+ C{'verbose'}.
+ """
+ if file is None:
+ file = log.logerr
+ w = file.write
+
+ if detail == 'verbose' and not self.captureVars:
+ # We don't have any locals or globals, so rather than show them as
+ # empty make the output explicitly say that we don't have them at
+ # all.
+ formatDetail = 'verbose-vars-not-captured'
+ else:
+ formatDetail = detail
+
+ # Preamble
+ if detail == 'verbose':
+ w( '*--- Failure #%d%s---\n' %
+ (self.count,
+ (self.pickled and ' (pickled) ') or ' '))
+ elif detail == 'brief':
+ if self.frames:
+ hasFrames = 'Traceback'
+ else:
+ hasFrames = 'Traceback (failure with no frames)'
+ w("%s: %s: %s\n" % (
+ hasFrames,
+ reflect.safe_str(self.type),
+ reflect.safe_str(self.value)))
+ else:
+ w( 'Traceback (most recent call last):\n')
+
+ # Frames, formatted in appropriate style
+ if self.frames:
+ if not elideFrameworkCode:
+ format_frames(self.stack[-traceupLength:], w, formatDetail)
+ w("%s\n" % (EXCEPTION_CAUGHT_HERE,))
+ format_frames(self.frames, w, formatDetail)
+ elif not detail == 'brief':
+ # Yeah, it's not really a traceback, despite looking like one...
+ w("Failure: ")
+
+ # postamble, if any
+ if not detail == 'brief':
+ # Unfortunately, self.type will not be a class object if this
+ # Failure was created implicitly from a string exception.
+ # qual() doesn't make any sense on a string, so check for this
+ # case here and just write out the string if that's what we
+ # have.
+ if isinstance(self.type, (str, unicode)):
+ w(self.type + "\n")
+ else:
+ w("%s: %s\n" % (reflect.qual(self.type),
+ reflect.safe_str(self.value)))
+ # chaining
+ if isinstance(self.value, Failure):
+ # TODO: indentation for chained failures?
+ file.write(" (chained Failure)\n")
+ self.value.printTraceback(file, elideFrameworkCode, detail)
+ if detail == 'verbose':
+ w('*--- End of Failure #%d ---\n' % self.count)
+
+
+ def printBriefTraceback(self, file=None, elideFrameworkCode=0):
+ """Print a traceback as densely as possible.
+ """
+ self.printTraceback(file, elideFrameworkCode, detail='brief')
+
+ def printDetailedTraceback(self, file=None, elideFrameworkCode=0):
+ """Print a traceback with detailed locals and globals information.
+ """
+ self.printTraceback(file, elideFrameworkCode, detail='verbose')
+
+
+def _safeReprVars(varsDictItems):
+ """
+ Convert a list of (name, object) pairs into (name, repr) pairs.
+
+ L{twisted.python.reflect.safe_repr} is used to generate the repr, so no
+ exceptions will be raised by faulty C{__repr__} methods.
+
+ @param varsDictItems: a sequence of (name, value) pairs as returned by e.g.
+ C{locals().items()}.
+ @returns: a sequence of (name, repr) pairs.
+ """
+ return [(name, reflect.safe_repr(obj)) for (name, obj) in varsDictItems]
+
+
+# slyphon: make post-morteming exceptions tweakable
+
+DO_POST_MORTEM = True
+
+def _debuginit(self, exc_value=None, exc_type=None, exc_tb=None,
+ captureVars=False,
+ Failure__init__=Failure.__init__.im_func):
+ """
+ Initialize failure object, possibly spawning pdb.
+ """
+ if (exc_value, exc_type, exc_tb) == (None, None, None):
+ exc = sys.exc_info()
+ if not exc[0] == self.__class__ and DO_POST_MORTEM:
+ try:
+ strrepr = str(exc[1])
+ except:
+ strrepr = "broken str"
+ print "Jumping into debugger for post-mortem of exception '%s':" % (strrepr,)
+ import pdb
+ pdb.post_mortem(exc[2])
+ Failure__init__(self, exc_value, exc_type, exc_tb, captureVars)
+
+
+def startDebugMode():
+ """Enable debug hooks for Failures."""
+ Failure.__init__ = _debuginit
+
+
+# Sibling imports - at the bottom and unqualified to avoid unresolvable
+# circularity
+import log
diff --git a/twisted/python/fakepwd.py b/twisted/python/fakepwd.py
new file mode 100644
index 0000000..183b30c
--- /dev/null
+++ b/twisted/python/fakepwd.py
@@ -0,0 +1,219 @@
+# -*- test-case-name: twisted.python.test.test_fakepwd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+L{twisted.python.fakepwd} provides a fake implementation of the L{pwd} API.
+"""
+
+
+__all__ = ['UserDatabase', 'ShadowDatabase']
+
+
+class _UserRecord(object):
+ """
+ L{_UserRecord} holds the user data for a single user in L{UserDatabase}.
+ It corresponds to L{pwd.struct_passwd}. See that class for attribute
+ documentation.
+ """
+ def __init__(self, name, password, uid, gid, gecos, home, shell):
+ self.pw_name = name
+ self.pw_passwd = password
+ self.pw_uid = uid
+ self.pw_gid = gid
+ self.pw_gecos = gecos
+ self.pw_dir = home
+ self.pw_shell = shell
+
+
+ def __len__(self):
+ return 7
+
+
+ def __getitem__(self, index):
+ return (
+ self.pw_name, self.pw_passwd, self.pw_uid,
+ self.pw_gid, self.pw_gecos, self.pw_dir, self.pw_shell)[index]
+
+
+
+class UserDatabase(object):
+ """
+ L{UserDatabase} holds a traditional POSIX user data in memory and makes it
+ available via the same API as L{pwd}.
+
+ @ivar _users: A C{list} of L{_UserRecord} instances holding all user data
+ added to this database.
+ """
+ def __init__(self):
+ self._users = []
+
+
+ def addUser(self, username, password, uid, gid, gecos, home, shell):
+ """
+ Add a new user record to this database.
+
+ @param username: The value for the C{pw_name} field of the user
+ record to add.
+ @type username: C{str}
+
+ @param password: The value for the C{pw_passwd} field of the user
+ record to add.
+ @type password: C{str}
+
+ @param uid: The value for the C{pw_uid} field of the user record to
+ add.
+ @type uid: C{int}
+
+ @param gid: The value for the C{pw_gid} field of the user record to
+ add.
+ @type gid: C{int}
+
+ @param gecos: The value for the C{pw_gecos} field of the user record
+ to add.
+ @type gecos: C{str}
+
+ @param home: The value for the C{pw_dir} field of the user record to
+ add.
+ @type home: C{str}
+
+ @param shell: The value for the C{pw_shell} field of the user record to
+ add.
+ @type shell: C{str}
+ """
+ self._users.append(_UserRecord(
+ username, password, uid, gid, gecos, home, shell))
+
+
+ def getpwuid(self, uid):
+ """
+ Return the user record corresponding to the given uid.
+ """
+ for entry in self._users:
+ if entry.pw_uid == uid:
+ return entry
+ raise KeyError()
+
+
+ def getpwnam(self, name):
+ """
+ Return the user record corresponding to the given username.
+ """
+ for entry in self._users:
+ if entry.pw_name == name:
+ return entry
+ raise KeyError()
+
+
+ def getpwall(self):
+ """
+ Return a list of all user records.
+ """
+ return self._users
+
+
+
+class _ShadowRecord(object):
+ """
+ L{_ShadowRecord} holds the shadow user data for a single user in
+ L{ShadowDatabase}. It corresponds to C{spwd.struct_spwd}. See that class
+ for attribute documentation.
+ """
+ def __init__(self, username, password, lastChange, min, max, warn, inact,
+ expire, flag):
+ self.sp_nam = username
+ self.sp_pwd = password
+ self.sp_lstchg = lastChange
+ self.sp_min = min
+ self.sp_max = max
+ self.sp_warn = warn
+ self.sp_inact = inact
+ self.sp_expire = expire
+ self.sp_flag = flag
+
+
+ def __len__(self):
+ return 9
+
+
+ def __getitem__(self, index):
+ return (
+ self.sp_nam, self.sp_pwd, self.sp_lstchg, self.sp_min,
+ self.sp_max, self.sp_warn, self.sp_inact, self.sp_expire,
+ self.sp_flag)[index]
+
+
+
+class ShadowDatabase(object):
+ """
+ L{ShadowDatabase} holds a shadow user database in memory and makes it
+ available via the same API as C{spwd}.
+
+ @ivar _users: A C{list} of L{_ShadowRecord} instances holding all user data
+ added to this database.
+
+ @since: 12.0
+ """
+ def __init__(self):
+ self._users = []
+
+
+ def addUser(self, username, password, lastChange, min, max, warn, inact,
+ expire, flag):
+ """
+ Add a new user record to this database.
+
+ @param username: The value for the C{sp_nam} field of the user record to
+ add.
+ @type username: C{str}
+
+ @param password: The value for the C{sp_pwd} field of the user record to
+ add.
+ @type password: C{str}
+
+ @param lastChange: The value for the C{sp_lstchg} field of the user
+ record to add.
+ @type lastChange: C{int}
+
+ @param min: The value for the C{sp_min} field of the user record to add.
+ @type min: C{int}
+
+ @param max: The value for the C{sp_max} field of the user record to add.
+ @type max: C{int}
+
+ @param warn: The value for the C{sp_warn} field of the user record to
+ add.
+ @type warn: C{int}
+
+ @param inact: The value for the C{sp_inact} field of the user record to
+ add.
+ @type inact: C{int}
+
+ @param expire: The value for the C{sp_expire} field of the user record
+ to add.
+ @type expire: C{int}
+
+ @param flag: The value for the C{sp_flag} field of the user record to
+ add.
+ @type flag: C{int}
+ """
+ self._users.append(_ShadowRecord(
+ username, password, lastChange,
+ min, max, warn, inact, expire, flag))
+
+
+ def getspnam(self, username):
+ """
+ Return the shadow user record corresponding to the given username.
+ """
+ for entry in self._users:
+ if entry.sp_nam == username:
+ return entry
+ raise KeyError
+
+
+ def getspall(self):
+ """
+ Return a list of all shadow user records.
+ """
+ return self._users
diff --git a/twisted/python/filepath.py b/twisted/python/filepath.py
new file mode 100644
index 0000000..8cfabc2
--- /dev/null
+++ b/twisted/python/filepath.py
@@ -0,0 +1,1444 @@
+# -*- test-case-name: twisted.test.test_paths -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Object-oriented filesystem path representation.
+"""
+
+import os
+import errno
+import random
+import base64
+
+from os.path import isabs, exists, normpath, abspath, splitext
+from os.path import basename, dirname
+from os.path import join as joinpath
+from os import sep as slash
+from os import listdir, utime, stat
+
+from stat import S_ISREG, S_ISDIR, S_IMODE, S_ISBLK, S_ISSOCK
+from stat import S_IRUSR, S_IWUSR, S_IXUSR
+from stat import S_IRGRP, S_IWGRP, S_IXGRP
+from stat import S_IROTH, S_IWOTH, S_IXOTH
+
+
+# Please keep this as light as possible on other Twisted imports; many, many
+# things import this module, and it would be good if it could easily be
+# modified for inclusion in the standard library. --glyph
+
+from twisted.python.runtime import platform
+from twisted.python.hashlib import sha1
+
+from twisted.python.win32 import ERROR_FILE_NOT_FOUND, ERROR_PATH_NOT_FOUND
+from twisted.python.win32 import ERROR_INVALID_NAME, ERROR_DIRECTORY, O_BINARY
+from twisted.python.win32 import WindowsError
+
+from twisted.python.util import FancyEqMixin
+
+from zope.interface import Interface, Attribute, implements
+
+_CREATE_FLAGS = (os.O_EXCL |
+ os.O_CREAT |
+ os.O_RDWR |
+ O_BINARY)
+
+
+def _stub_islink(path):
+ """
+ Always return 'false' if the operating system does not support symlinks.
+
+ @param path: a path string.
+ @type path: L{str}
+ @return: false
+ """
+ return False
+
+
+def _stub_urandom(n):
+ """
+ Provide random data in versions of Python prior to 2.4. This is an
+ effectively compatible replacement for 'os.urandom'.
+
+ @type n: L{int}
+ @param n: the number of bytes of data to return
+ @return: C{n} bytes of random data.
+ @rtype: str
+ """
+ randomData = [random.randrange(256) for n in xrange(n)]
+ return ''.join(map(chr, randomData))
+
+
+def _stub_armor(s):
+ """
+ ASCII-armor for random data. This uses a hex encoding, although we will
+ prefer url-safe base64 encoding for features in this module if it is
+ available.
+ """
+ return s.encode('hex')
+
+islink = getattr(os.path, 'islink', _stub_islink)
+randomBytes = getattr(os, 'urandom', _stub_urandom)
+armor = getattr(base64, 'urlsafe_b64encode', _stub_armor)
+
+class IFilePath(Interface):
+ """
+ File path object.
+
+ A file path represents a location for a file-like-object and can be
+ organized into a hierarchy; a file path can can children which are
+ themselves file paths.
+
+ A file path has a name which unique identifies it in the context of its
+ parent (if it has one); a file path can not have two children with the same
+ name. This name is referred to as the file path's "base name".
+
+ A series of such names can be used to locate nested children of a file path;
+ such a series is referred to as the child's "path", relative to the parent.
+ In this case, each name in the path is referred to as a "path segment"; the
+ child's base name is the segment in the path.
+
+ When representing a file path as a string, a "path separator" is used to
+ delimit the path segments within the string. For a file system path, that
+ would be C{os.sep}.
+
+ Note that the values of child names may be restricted. For example, a file
+ system path will not allow the use of the path separator in a name, and
+ certain names (eg. C{"."} and C{".."}) may be reserved or have special
+ meanings.
+
+ @since: 12.1
+ """
+ sep = Attribute("The path separator to use in string representations")
+
+ def child(name):
+ """
+ Obtain a direct child of this file path. The child may or may not
+ exist.
+
+ @param name: the name of a child of this path. C{name} must be a direct
+ child of this path and may not contain a path separator.
+ @return: the child of this path with the given C{name}.
+ @raise InsecurePath: if C{name} describes a file path that is not a
+ direct child of this file path.
+ """
+
+ def open(mode="r"):
+ """
+ Opens this file path with the given mode.
+ @return: a file-like-object.
+ @raise Exception: if this file path cannot be opened.
+ """
+
+ def changed():
+ """
+ Clear any cached information about the state of this path on disk.
+ """
+
+ def getsize():
+ """
+ @return: the size of the file at this file path in bytes.
+ @raise Exception: if the size cannot be obtained.
+ """
+
+ def getModificationTime():
+ """
+ Retrieve the time of last access from this file.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+
+ def getStatusChangeTime():
+ """
+ Retrieve the time of the last status change for this file.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+
+ def getAccessTime():
+ """
+ Retrieve the time that this file was last accessed.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+
+ def exists():
+ """
+ @return: C{True} if the file at this file path exists, C{False}
+ otherwise.
+ """
+
+ def isdir():
+ """
+ @return: C{True} if the file at this file path is a directory, C{False}
+ otherwise.
+ """
+
+ def isfile():
+ """
+ @return: C{True} if the file at this file path is a regular file,
+ C{False} otherwise.
+ """
+
+ def children():
+ """
+ @return: a sequence of the children of the directory at this file path.
+ @raise Exception: if the file at this file path is not a directory.
+ """
+
+ def basename():
+ """
+ @return: the base name of this file path.
+ """
+
+ def parent():
+ """
+ A file path for the directory containing the file at this file path.
+ """
+
+ def sibling(name):
+ """
+ A file path for the directory containing the file at this file path.
+ @param name: the name of a sibling of this path. C{name} must be a direct
+ sibling of this path and may not contain a path separator.
+
+ @return: a sibling file path of this one.
+ """
+
+class InsecurePath(Exception):
+ """
+ Error that is raised when the path provided to FilePath is invalid.
+ """
+
+
+
+class LinkError(Exception):
+ """
+ An error with symlinks - either that there are cyclical symlinks or that
+ symlink are not supported on this platform.
+ """
+
+
+
+class UnlistableError(OSError):
+ """
+ An exception which is used to distinguish between errors which mean 'this
+ is not a directory you can list' and other, more catastrophic errors.
+
+ This error will try to look as much like the original error as possible,
+ while still being catchable as an independent type.
+
+ @ivar originalException: the actual original exception instance, either an
+ L{OSError} or a L{WindowsError}.
+ """
+ def __init__(self, originalException):
+ """
+ Create an UnlistableError exception.
+
+ @param originalException: an instance of OSError.
+ """
+ self.__dict__.update(originalException.__dict__)
+ self.originalException = originalException
+
+
+
+class _WindowsUnlistableError(UnlistableError, WindowsError):
+ """
+ This exception is raised on Windows, for compatibility with previous
+ releases of FilePath where unportable programs may have done "except
+ WindowsError:" around a call to children().
+
+ It is private because all application code may portably catch
+ L{UnlistableError} instead.
+ """
+
+
+
+def _secureEnoughString():
+ """
+ Create a pseudorandom, 16-character string for use in secure filenames.
+ """
+ return armor(sha1(randomBytes(64)).digest())[:16]
+
+
+
+class AbstractFilePath(object):
+ """
+ Abstract implementation of an IFilePath; must be completed by a subclass.
+
+ This class primarily exists to provide common implementations of certain
+ methods in IFilePath. It is *not* a required parent class for IFilePath
+ implementations, just a useful starting point.
+ """
+
+ def getContent(self):
+ fp = self.open()
+ try:
+ return fp.read()
+ finally:
+ fp.close()
+
+
+ def parents(self):
+ """
+ @return: an iterator of all the ancestors of this path, from the most
+ recent (its immediate parent) to the root of its filesystem.
+ """
+ path = self
+ parent = path.parent()
+ # root.parent() == root, so this means "are we the root"
+ while path != parent:
+ yield parent
+ path = parent
+ parent = parent.parent()
+
+
+ def children(self):
+ """
+ List the children of this path object.
+
+ @raise OSError: If an error occurs while listing the directory. If the
+ error is 'serious', meaning that the operation failed due to an access
+ violation, exhaustion of some kind of resource (file descriptors or
+ memory), OSError or a platform-specific variant will be raised.
+
+ @raise UnlistableError: If the inability to list the directory is due
+ to this path not existing or not being a directory, the more specific
+ OSError subclass L{UnlistableError} is raised instead.
+
+ @return: an iterable of all currently-existing children of this object
+ accessible with L{_PathHelper.child}.
+ """
+ try:
+ subnames = self.listdir()
+ except WindowsError, winErrObj:
+ # WindowsError is an OSError subclass, so if not for this clause
+ # the OSError clause below would be handling these. Windows error
+ # codes aren't the same as POSIX error codes, so we need to handle
+ # them differently.
+
+ # Under Python 2.5 on Windows, WindowsError has a winerror
+ # attribute and an errno attribute. The winerror attribute is
+ # bound to the Windows error code while the errno attribute is
+ # bound to a translation of that code to a perhaps equivalent POSIX
+ # error number.
+
+ # Under Python 2.4 on Windows, WindowsError only has an errno
+ # attribute. It is bound to the Windows error code.
+
+ # For simplicity of code and to keep the number of paths through
+ # this suite minimal, we grab the Windows error code under either
+ # version.
+
+ # Furthermore, attempting to use os.listdir on a non-existent path
+ # in Python 2.4 will result in a Windows error code of
+ # ERROR_PATH_NOT_FOUND. However, in Python 2.5,
+ # ERROR_FILE_NOT_FOUND results instead. -exarkun
+ winerror = getattr(winErrObj, 'winerror', winErrObj.errno)
+ if winerror not in (ERROR_PATH_NOT_FOUND,
+ ERROR_FILE_NOT_FOUND,
+ ERROR_INVALID_NAME,
+ ERROR_DIRECTORY):
+ raise
+ raise _WindowsUnlistableError(winErrObj)
+ except OSError, ose:
+ if ose.errno not in (errno.ENOENT, errno.ENOTDIR):
+ # Other possible errors here, according to linux manpages:
+ # EACCES, EMIFLE, ENFILE, ENOMEM. None of these seem like the
+ # sort of thing which should be handled normally. -glyph
+ raise
+ raise UnlistableError(ose)
+ return map(self.child, subnames)
+
+ def walk(self, descend=None):
+ """
+ Yield myself, then each of my children, and each of those children's
+ children in turn. The optional argument C{descend} is a predicate that
+ takes a FilePath, and determines whether or not that FilePath is
+ traversed/descended into. It will be called with each path for which
+ C{isdir} returns C{True}. If C{descend} is not specified, all
+ directories will be traversed (including symbolic links which refer to
+ directories).
+
+ @param descend: A one-argument callable that will return True for
+ FilePaths that should be traversed, False otherwise.
+
+ @return: a generator yielding FilePath-like objects.
+ """
+ yield self
+ if self.isdir():
+ for c in self.children():
+ # we should first see if it's what we want, then we
+ # can walk through the directory
+ if (descend is None or descend(c)):
+ for subc in c.walk(descend):
+ if os.path.realpath(self.path).startswith(
+ os.path.realpath(subc.path)):
+ raise LinkError("Cycle in file graph.")
+ yield subc
+ else:
+ yield c
+
+
+ def sibling(self, path):
+ """
+ Return a L{FilePath} with the same directory as this instance but with a
+ basename of C{path}.
+
+ @param path: The basename of the L{FilePath} to return.
+ @type path: C{str}
+
+ @rtype: L{FilePath}
+ """
+ return self.parent().child(path)
+
+
+ def descendant(self, segments):
+ """
+ Retrieve a child or child's child of this path.
+
+ @param segments: A sequence of path segments as C{str} instances.
+
+ @return: A L{FilePath} constructed by looking up the C{segments[0]}
+ child of this path, the C{segments[1]} child of that path, and so
+ on.
+
+ @since: 10.2
+ """
+ path = self
+ for name in segments:
+ path = path.child(name)
+ return path
+
+
+ def segmentsFrom(self, ancestor):
+ """
+ Return a list of segments between a child and its ancestor.
+
+ For example, in the case of a path X representing /a/b/c/d and a path Y
+ representing /a/b, C{Y.segmentsFrom(X)} will return C{['c',
+ 'd']}.
+
+ @param ancestor: an instance of the same class as self, ostensibly an
+ ancestor of self.
+
+ @raise: ValueError if the 'ancestor' parameter is not actually an
+ ancestor, i.e. a path for /x/y/z is passed as an ancestor for /a/b/c/d.
+
+ @return: a list of strs
+ """
+ # this might be an unnecessarily inefficient implementation but it will
+ # work on win32 and for zipfiles; later I will deterimine if the
+ # obvious fast implemenation does the right thing too
+ f = self
+ p = f.parent()
+ segments = []
+ while f != ancestor and p != f:
+ segments[0:0] = [f.basename()]
+ f = p
+ p = p.parent()
+ if f == ancestor and segments:
+ return segments
+ raise ValueError("%r not parent of %r" % (ancestor, self))
+
+
+ # new in 8.0
+ def __hash__(self):
+ """
+ Hash the same as another FilePath with the same path as mine.
+ """
+ return hash((self.__class__, self.path))
+
+
+ # pending deprecation in 8.0
+ def getmtime(self):
+ """
+ Deprecated. Use getModificationTime instead.
+ """
+ return int(self.getModificationTime())
+
+
+ def getatime(self):
+ """
+ Deprecated. Use getAccessTime instead.
+ """
+ return int(self.getAccessTime())
+
+
+ def getctime(self):
+ """
+ Deprecated. Use getStatusChangeTime instead.
+ """
+ return int(self.getStatusChangeTime())
+
+
+
+class RWX(FancyEqMixin, object):
+ """
+ A class representing read/write/execute permissions for a single user
+ category (i.e. user/owner, group, or other/world). Instantiate with
+ three boolean values: readable? writable? executable?.
+
+ @type read: C{bool}
+ @ivar read: Whether permission to read is given
+
+ @type write: C{bool}
+ @ivar write: Whether permission to write is given
+
+ @type execute: C{bool}
+ @ivar execute: Whether permission to execute is given
+
+ @since: 11.1
+ """
+ compareAttributes = ('read', 'write', 'execute')
+ def __init__(self, readable, writable, executable):
+ self.read = readable
+ self.write = writable
+ self.execute = executable
+
+
+ def __repr__(self):
+ return "RWX(read=%s, write=%s, execute=%s)" % (
+ self.read, self.write, self.execute)
+
+
+ def shorthand(self):
+ """
+ Returns a short string representing the permission bits. Looks like
+ part of what is printed by command line utilities such as 'ls -l'
+ (e.g. 'rwx')
+ """
+ returnval = ['r', 'w', 'x']
+ i = 0
+ for val in (self.read, self.write, self.execute):
+ if not val:
+ returnval[i] = '-'
+ i += 1
+ return ''.join(returnval)
+
+
+
+class Permissions(FancyEqMixin, object):
+ """
+ A class representing read/write/execute permissions. Instantiate with any
+ portion of the file's mode that includes the permission bits.
+
+ @type user: L{RWX}
+ @ivar user: User/Owner permissions
+
+ @type group: L{RWX}
+ @ivar group: Group permissions
+
+ @type other: L{RWX}
+ @ivar other: Other/World permissions
+
+ @since: 11.1
+ """
+
+ compareAttributes = ('user', 'group', 'other')
+
+ def __init__(self, statModeInt):
+ self.user, self.group, self.other = (
+ [RWX(*[statModeInt & bit > 0 for bit in bitGroup]) for bitGroup in
+ [[S_IRUSR, S_IWUSR, S_IXUSR],
+ [S_IRGRP, S_IWGRP, S_IXGRP],
+ [S_IROTH, S_IWOTH, S_IXOTH]]]
+ )
+
+
+ def __repr__(self):
+ return "[%s | %s | %s]" % (
+ str(self.user), str(self.group), str(self.other))
+
+
+ def shorthand(self):
+ """
+ Returns a short string representing the permission bits. Looks like
+ what is printed by command line utilities such as 'ls -l'
+ (e.g. 'rwx-wx--x')
+ """
+ return "".join(
+ [x.shorthand() for x in (self.user, self.group, self.other)])
+
+
+
+class FilePath(AbstractFilePath):
+ """
+ I am a path on the filesystem that only permits 'downwards' access.
+
+ Instantiate me with a pathname (for example,
+ FilePath('/home/myuser/public_html')) and I will attempt to only provide
+ access to files which reside inside that path. I may be a path to a file,
+ a directory, or a file which does not exist.
+
+ The correct way to use me is to instantiate me, and then do ALL filesystem
+ access through me. In other words, do not import the 'os' module; if you
+ need to open a file, call my 'open' method. If you need to list a
+ directory, call my 'path' method.
+
+ Even if you pass me a relative path, I will convert that to an absolute
+ path internally.
+
+ Note: although time-related methods do return floating-point results, they
+ may still be only second resolution depending on the platform and the last
+ value passed to L{os.stat_float_times}. If you want greater-than-second
+ precision, call C{os.stat_float_times(True)}, or use Python 2.5.
+ Greater-than-second precision is only available in Windows on Python2.5 and
+ later.
+
+ @type alwaysCreate: C{bool}
+ @ivar alwaysCreate: When opening this file, only succeed if the file does
+ not already exist.
+
+ @type path: C{str}
+ @ivar path: The path from which 'downward' traversal is permitted.
+
+ @ivar statinfo: The currently cached status information about the file on
+ the filesystem that this L{FilePath} points to. This attribute is
+ C{None} if the file is in an indeterminate state (either this
+ L{FilePath} has not yet had cause to call C{stat()} yet or
+ L{FilePath.changed} indicated that new information is required), 0 if
+ C{stat()} was called and returned an error (i.e. the path did not exist
+ when C{stat()} was called), or a C{stat_result} object that describes
+ the last known status of the underlying file (or directory, as the case
+ may be). Trust me when I tell you that you do not want to use this
+ attribute. Instead, use the methods on L{FilePath} which give you
+ information about it, like C{getsize()}, C{isdir()},
+ C{getModificationTime()}, and so on.
+ @type statinfo: C{int} or L{types.NoneType} or L{os.stat_result}
+ """
+
+ implements(IFilePath)
+
+ statinfo = None
+ path = None
+
+ sep = slash
+
+ def __init__(self, path, alwaysCreate=False):
+ """
+ Convert a path string to an absolute path if necessary and initialize
+ the L{FilePath} with the result.
+ """
+ self.path = abspath(path)
+ self.alwaysCreate = alwaysCreate
+
+ def __getstate__(self):
+ """
+ Support serialization by discarding cached L{os.stat} results and
+ returning everything else.
+ """
+ d = self.__dict__.copy()
+ if d.has_key('statinfo'):
+ del d['statinfo']
+ return d
+
+
+ def child(self, path):
+ """
+ Create and return a new L{FilePath} representing a path contained by
+ C{self}.
+
+ @param path: The base name of the new L{FilePath}. If this contains
+ directory separators or parent references it will be rejected.
+ @type path: C{str}
+
+ @raise InsecurePath: If the result of combining this path with C{path}
+ would result in a path which is not a direct child of this path.
+ """
+ if platform.isWindows() and path.count(":"):
+ # Catch paths like C:blah that don't have a slash
+ raise InsecurePath("%r contains a colon." % (path,))
+ norm = normpath(path)
+ if self.sep in norm:
+ raise InsecurePath("%r contains one or more directory separators" % (path,))
+ newpath = abspath(joinpath(self.path, norm))
+ if not newpath.startswith(self.path):
+ raise InsecurePath("%r is not a child of %s" % (newpath, self.path))
+ return self.clonePath(newpath)
+
+
+ def preauthChild(self, path):
+ """
+ Use me if `path' might have slashes in it, but you know they're safe.
+
+ (NOT slashes at the beginning. It still needs to be a _child_).
+ """
+ newpath = abspath(joinpath(self.path, normpath(path)))
+ if not newpath.startswith(self.path):
+ raise InsecurePath("%s is not a child of %s" % (newpath, self.path))
+ return self.clonePath(newpath)
+
+ def childSearchPreauth(self, *paths):
+ """Return my first existing child with a name in 'paths'.
+
+ paths is expected to be a list of *pre-secured* path fragments; in most
+ cases this will be specified by a system administrator and not an
+ arbitrary user.
+
+ If no appropriately-named children exist, this will return None.
+ """
+ p = self.path
+ for child in paths:
+ jp = joinpath(p, child)
+ if exists(jp):
+ return self.clonePath(jp)
+
+ def siblingExtensionSearch(self, *exts):
+ """Attempt to return a path with my name, given multiple possible
+ extensions.
+
+ Each extension in exts will be tested and the first path which exists
+ will be returned. If no path exists, None will be returned. If '' is
+ in exts, then if the file referred to by this path exists, 'self' will
+ be returned.
+
+ The extension '*' has a magic meaning, which means "any path that
+ begins with self.path+'.' is acceptable".
+ """
+ p = self.path
+ for ext in exts:
+ if not ext and self.exists():
+ return self
+ if ext == '*':
+ basedot = basename(p)+'.'
+ for fn in listdir(dirname(p)):
+ if fn.startswith(basedot):
+ return self.clonePath(joinpath(dirname(p), fn))
+ p2 = p + ext
+ if exists(p2):
+ return self.clonePath(p2)
+
+
+ def realpath(self):
+ """
+ Returns the absolute target as a FilePath if self is a link, self
+ otherwise. The absolute link is the ultimate file or directory the
+ link refers to (for instance, if the link refers to another link, and
+ another...). If the filesystem does not support symlinks, or
+ if the link is cyclical, raises a LinkError.
+
+ Behaves like L{os.path.realpath} in that it does not resolve link
+ names in the middle (ex. /x/y/z, y is a link to w - realpath on z
+ will return /x/y/z, not /x/w/z).
+
+ @return: FilePath of the target path
+ @raises LinkError: if links are not supported or links are cyclical.
+ """
+ if self.islink():
+ result = os.path.realpath(self.path)
+ if result == self.path:
+ raise LinkError("Cyclical link - will loop forever")
+ return self.clonePath(result)
+ return self
+
+
+ def siblingExtension(self, ext):
+ return self.clonePath(self.path+ext)
+
+
+ def linkTo(self, linkFilePath):
+ """
+ Creates a symlink to self to at the path in the L{FilePath}
+ C{linkFilePath}. Only works on posix systems due to its dependence on
+ C{os.symlink}. Propagates C{OSError}s up from C{os.symlink} if
+ C{linkFilePath.parent()} does not exist, or C{linkFilePath} already
+ exists.
+
+ @param linkFilePath: a FilePath representing the link to be created
+ @type linkFilePath: L{FilePath}
+ """
+ os.symlink(self.path, linkFilePath.path)
+
+
+ def open(self, mode='r'):
+ """
+ Open this file using C{mode} or for writing if C{alwaysCreate} is
+ C{True}.
+
+ In all cases the file is opened in binary mode, so it is not necessary
+ to include C{b} in C{mode}.
+
+ @param mode: The mode to open the file in. Default is C{r}.
+ @type mode: C{str}
+ @raises AssertionError: If C{a} is included in the mode and
+ C{alwaysCreate} is C{True}.
+ @rtype: C{file}
+ @return: An open C{file} object.
+ """
+ if self.alwaysCreate:
+ assert 'a' not in mode, ("Appending not supported when "
+ "alwaysCreate == True")
+ return self.create()
+ # This hack is necessary because of a bug in Python 2.7 on Windows:
+ # http://bugs.python.org/issue7686
+ mode = mode.replace('b', '')
+ return open(self.path, mode + 'b')
+
+ # stat methods below
+
+ def restat(self, reraise=True):
+ """
+ Re-calculate cached effects of 'stat'. To refresh information on this path
+ after you know the filesystem may have changed, call this method.
+
+ @param reraise: a boolean. If true, re-raise exceptions from
+ L{os.stat}; otherwise, mark this path as not existing, and remove any
+ cached stat information.
+
+ @raise Exception: is C{reraise} is C{True} and an exception occurs while
+ reloading metadata.
+ """
+ try:
+ self.statinfo = stat(self.path)
+ except OSError:
+ self.statinfo = 0
+ if reraise:
+ raise
+
+
+ def changed(self):
+ """
+ Clear any cached information about the state of this path on disk.
+
+ @since: 10.1.0
+ """
+ self.statinfo = None
+
+
+ def chmod(self, mode):
+ """
+ Changes the permissions on self, if possible. Propagates errors from
+ C{os.chmod} up.
+
+ @param mode: integer representing the new permissions desired (same as
+ the command line chmod)
+ @type mode: C{int}
+ """
+ os.chmod(self.path, mode)
+
+
+ def getsize(self):
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return st.st_size
+
+
+ def getModificationTime(self):
+ """
+ Retrieve the time of last access from this file.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return float(st.st_mtime)
+
+
+ def getStatusChangeTime(self):
+ """
+ Retrieve the time of the last status change for this file.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return float(st.st_ctime)
+
+
+ def getAccessTime(self):
+ """
+ Retrieve the time that this file was last accessed.
+
+ @return: a number of seconds from the epoch.
+ @rtype: float
+ """
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return float(st.st_atime)
+
+
+ def getInodeNumber(self):
+ """
+ Retrieve the file serial number, also called inode number, which
+ distinguishes this file from all other files on the same device.
+
+ @raise: NotImplementedError if the platform is Windows, since the
+ inode number would be a dummy value for all files in Windows
+ @return: a number representing the file serial number
+ @rtype: C{long}
+ @since: 11.0
+ """
+ if platform.isWindows():
+ raise NotImplementedError
+
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return long(st.st_ino)
+
+
+ def getDevice(self):
+ """
+ Retrieves the device containing the file. The inode number and device
+ number together uniquely identify the file, but the device number is
+ not necessarily consistent across reboots or system crashes.
+
+ @raise: NotImplementedError if the platform is Windows, since the
+ device number would be 0 for all partitions on a Windows
+ platform
+ @return: a number representing the device
+ @rtype: C{long}
+ @since: 11.0
+ """
+ if platform.isWindows():
+ raise NotImplementedError
+
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return long(st.st_dev)
+
+
+ def getNumberOfHardLinks(self):
+ """
+ Retrieves the number of hard links to the file. This count keeps
+ track of how many directories have entries for this file. If the
+ count is ever decremented to zero then the file itself is discarded
+ as soon as no process still holds it open. Symbolic links are not
+ counted in the total.
+
+ @raise: NotImplementedError if the platform is Windows, since Windows
+ doesn't maintain a link count for directories, and os.stat
+ does not set st_nlink on Windows anyway.
+ @return: the number of hard links to the file
+ @rtype: C{int}
+ @since: 11.0
+ """
+ if platform.isWindows():
+ raise NotImplementedError
+
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return int(st.st_nlink)
+
+
+ def getUserID(self):
+ """
+ Returns the user ID of the file's owner.
+
+ @raise: NotImplementedError if the platform is Windows, since the UID
+ is always 0 on Windows
+ @return: the user ID of the file's owner
+ @rtype: C{int}
+ @since: 11.0
+ """
+ if platform.isWindows():
+ raise NotImplementedError
+
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return int(st.st_uid)
+
+
+ def getGroupID(self):
+ """
+ Returns the group ID of the file.
+
+ @raise: NotImplementedError if the platform is Windows, since the GID
+ is always 0 on windows
+ @return: the group ID of the file
+ @rtype: C{int}
+ @since: 11.0
+ """
+ if platform.isWindows():
+ raise NotImplementedError
+
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return int(st.st_gid)
+
+
+ def getPermissions(self):
+ """
+ Returns the permissions of the file. Should also work on Windows,
+ however, those permissions may not what is expected in Windows.
+
+ @return: the permissions for the file
+ @rtype: L{Permissions}
+ @since: 11.1
+ """
+ st = self.statinfo
+ if not st:
+ self.restat()
+ st = self.statinfo
+ return Permissions(S_IMODE(st.st_mode))
+
+
+ def exists(self):
+ """
+ Check if this L{FilePath} exists.
+
+ @return: C{True} if the stats of C{path} can be retrieved successfully,
+ C{False} in the other cases.
+ @rtype: C{bool}
+ """
+ if self.statinfo:
+ return True
+ else:
+ self.restat(False)
+ if self.statinfo:
+ return True
+ else:
+ return False
+
+
+ def isdir(self):
+ """
+ @return: C{True} if this L{FilePath} refers to a directory, C{False}
+ otherwise.
+ """
+ st = self.statinfo
+ if not st:
+ self.restat(False)
+ st = self.statinfo
+ if not st:
+ return False
+ return S_ISDIR(st.st_mode)
+
+
+ def isfile(self):
+ """
+ @return: C{True} if this L{FilePath} points to a regular file (not a
+ directory, socket, named pipe, etc), C{False} otherwise.
+ """
+ st = self.statinfo
+ if not st:
+ self.restat(False)
+ st = self.statinfo
+ if not st:
+ return False
+ return S_ISREG(st.st_mode)
+
+
+ def isBlockDevice(self):
+ """
+ Returns whether the underlying path is a block device.
+
+ @return: C{True} if it is a block device, C{False} otherwise
+ @rtype: C{bool}
+ @since: 11.1
+ """
+ st = self.statinfo
+ if not st:
+ self.restat(False)
+ st = self.statinfo
+ if not st:
+ return False
+ return S_ISBLK(st.st_mode)
+
+
+ def isSocket(self):
+ """
+ Returns whether the underlying path is a socket.
+
+ @return: C{True} if it is a socket, C{False} otherwise
+ @rtype: C{bool}
+ @since: 11.1
+ """
+ st = self.statinfo
+ if not st:
+ self.restat(False)
+ st = self.statinfo
+ if not st:
+ return False
+ return S_ISSOCK(st.st_mode)
+
+
+ def islink(self):
+ """
+ @return: C{True} if this L{FilePath} points to a symbolic link.
+ """
+ # We can't use cached stat results here, because that is the stat of
+ # the destination - (see #1773) which in *every case* but this one is
+ # the right thing to use. We could call lstat here and use that, but
+ # it seems unlikely we'd actually save any work that way. -glyph
+ return islink(self.path)
+
+
+ def isabs(self):
+ """
+ @return: C{True}, always.
+ """
+ return isabs(self.path)
+
+
+ def listdir(self):
+ """
+ List the base names of the direct children of this L{FilePath}.
+
+ @return: a C{list} of C{str} giving the names of the contents of the
+ directory this L{FilePath} refers to. These names are relative to
+ this L{FilePath}.
+
+ @raise: Anything the platform C{os.listdir} implementation might raise
+ (typically OSError).
+ """
+ return listdir(self.path)
+
+
+ def splitext(self):
+ """
+ @return: tuple where the first item is the filename and second item is
+ the file extension. See Python docs for C{os.path.splitext}
+ """
+ return splitext(self.path)
+
+
+ def __repr__(self):
+ return 'FilePath(%r)' % (self.path,)
+
+
+ def touch(self):
+ """
+ Updates the access and last modification times of the file at this
+ file path to the current time. Also creates the file if it does not
+ already exist.
+
+ @raise Exception: if unable to create or modify the last modification
+ time of the file.
+ """
+ try:
+ self.open('a').close()
+ except IOError:
+ pass
+ utime(self.path, None)
+
+
+ def remove(self):
+ """
+ Removes the file or directory that is represented by self. If
+ C{self.path} is a directory, recursively remove all its children
+ before removing the directory. If it's a file or link, just delete it.
+ """
+ if self.isdir() and not self.islink():
+ for child in self.children():
+ child.remove()
+ os.rmdir(self.path)
+ else:
+ os.remove(self.path)
+ self.changed()
+
+
+ def makedirs(self):
+ """
+ Create all directories not yet existing in C{path} segments, using
+ C{os.makedirs}.
+ """
+ return os.makedirs(self.path)
+
+
+ def globChildren(self, pattern):
+ """
+ Assuming I am representing a directory, return a list of
+ FilePaths representing my children that match the given
+ pattern.
+ """
+ import glob
+ path = self.path[-1] == '/' and self.path + pattern or self.sep.join([self.path, pattern])
+ return map(self.clonePath, glob.glob(path))
+
+
+ def basename(self):
+ """
+ @return: The final component of the L{FilePath}'s path (Everything after
+ the final path separator).
+ @rtype: C{str}
+ """
+ return basename(self.path)
+
+
+ def dirname(self):
+ """
+ @return: All of the components of the L{FilePath}'s path except the last
+ one (everything up to the final path separator).
+ @rtype: C{str}
+ """
+ return dirname(self.path)
+
+
+ def parent(self):
+ """
+ @return: A L{FilePath} representing the path which directly contains
+ this L{FilePath}.
+ """
+ return self.clonePath(self.dirname())
+
+
+ def setContent(self, content, ext='.new'):
+ """
+ Replace the file at this path with a new file that contains the given
+ bytes, trying to avoid data-loss in the meanwhile.
+
+ On UNIX-like platforms, this method does its best to ensure that by the
+ time this method returns, either the old contents I{or} the new contents
+ of the file will be present at this path for subsequent readers
+ regardless of premature device removal, program crash, or power loss,
+ making the following assumptions:
+
+ - your filesystem is journaled (i.e. your filesystem will not
+ I{itself} lose data due to power loss)
+
+ - your filesystem's C{rename()} is atomic
+
+ - your filesystem will not discard new data while preserving new
+ metadata (see U{http://mjg59.livejournal.com/108257.html} for more
+ detail)
+
+ On most versions of Windows there is no atomic C{rename()} (see
+ U{http://bit.ly/win32-overwrite} for more information), so this method
+ is slightly less helpful. There is a small window where the file at
+ this path may be deleted before the new file is moved to replace it:
+ however, the new file will be fully written and flushed beforehand so in
+ the unlikely event that there is a crash at that point, it should be
+ possible for the user to manually recover the new version of their data.
+ In the future, Twisted will support atomic file moves on those versions
+ of Windows which I{do} support them: see U{Twisted ticket
+ 3004<http://twistedmatrix.com/trac/ticket/3004>}.
+
+ This method should be safe for use by multiple concurrent processes, but
+ note that it is not easy to predict which process's contents will
+ ultimately end up on disk if they invoke this method at close to the
+ same time.
+
+ @param content: The desired contents of the file at this path.
+
+ @type content: L{str}
+
+ @param ext: An extension to append to the temporary filename used to
+ store the bytes while they are being written. This can be used to
+ make sure that temporary files can be identified by their suffix,
+ for cleanup in case of crashes.
+
+ @type ext: C{str}
+ """
+ sib = self.temporarySibling(ext)
+ f = sib.open('w')
+ try:
+ f.write(content)
+ finally:
+ f.close()
+ if platform.isWindows() and exists(self.path):
+ os.unlink(self.path)
+ os.rename(sib.path, self.path)
+
+
+ # new in 2.2.0
+
+ def __cmp__(self, other):
+ if not isinstance(other, FilePath):
+ return NotImplemented
+ return cmp(self.path, other.path)
+
+
+ def createDirectory(self):
+ """
+ Create the directory the L{FilePath} refers to.
+
+ @see: L{makedirs}
+
+ @raise OSError: If the directory cannot be created.
+ """
+ os.mkdir(self.path)
+
+
+ def requireCreate(self, val=1):
+ self.alwaysCreate = val
+
+
+ def create(self):
+ """
+ Exclusively create a file, only if this file previously did not exist.
+ """
+ fdint = os.open(self.path, _CREATE_FLAGS)
+
+ # XXX TODO: 'name' attribute of returned files is not mutable or
+ # settable via fdopen, so this file is slighly less functional than the
+ # one returned from 'open' by default. send a patch to Python...
+
+ return os.fdopen(fdint, 'w+b')
+
+
+ def temporarySibling(self, extension=""):
+ """
+ Construct a path referring to a sibling of this path.
+
+ The resulting path will be unpredictable, so that other subprocesses
+ should neither accidentally attempt to refer to the same path before it
+ is created, nor they should other processes be able to guess its name in
+ advance.
+
+ @param extension: A suffix to append to the created filename. (Note
+ that if you want an extension with a '.' you must include the '.'
+ yourself.)
+
+ @type extension: C{str}
+
+ @return: a path object with the given extension suffix, C{alwaysCreate}
+ set to True.
+
+ @rtype: L{FilePath}
+ """
+ sib = self.sibling(_secureEnoughString() + self.basename() + extension)
+ sib.requireCreate()
+ return sib
+
+
+ _chunkSize = 2 ** 2 ** 2 ** 2
+
+ def copyTo(self, destination, followLinks=True):
+ """
+ Copies self to destination.
+
+ If self doesn't exist, an OSError is raised.
+
+ If self is a directory, this method copies its children (but not
+ itself) recursively to destination - if destination does not exist as a
+ directory, this method creates it. If destination is a file, an
+ IOError will be raised.
+
+ If self is a file, this method copies it to destination. If
+ destination is a file, this method overwrites it. If destination is a
+ directory, an IOError will be raised.
+
+ If self is a link (and followLinks is False), self will be copied
+ over as a new symlink with the same target as returned by os.readlink.
+ That means that if it is absolute, both the old and new symlink will
+ link to the same thing. If it's relative, then perhaps not (and
+ it's also possible that this relative link will be broken).
+
+ File/directory permissions and ownership will NOT be copied over.
+
+ If followLinks is True, symlinks are followed so that they're treated
+ as their targets. In other words, if self is a link, the link's target
+ will be copied. If destination is a link, self will be copied to the
+ destination's target (the actual destination will be destination's
+ target). Symlinks under self (if self is a directory) will be
+ followed and its target's children be copied recursively.
+
+ If followLinks is False, symlinks will be copied over as symlinks.
+
+ @param destination: the destination (a FilePath) to which self
+ should be copied
+ @param followLinks: whether symlinks in self should be treated as links
+ or as their targets
+ """
+ if self.islink() and not followLinks:
+ os.symlink(os.readlink(self.path), destination.path)
+ return
+ # XXX TODO: *thorough* audit and documentation of the exact desired
+ # semantics of this code. Right now the behavior of existent
+ # destination symlinks is convenient, and quite possibly correct, but
+ # its security properties need to be explained.
+ if self.isdir():
+ if not destination.exists():
+ destination.createDirectory()
+ for child in self.children():
+ destChild = destination.child(child.basename())
+ child.copyTo(destChild, followLinks)
+ elif self.isfile():
+ writefile = destination.open('w')
+ try:
+ readfile = self.open()
+ try:
+ while 1:
+ # XXX TODO: optionally use os.open, os.read and O_DIRECT
+ # and use os.fstatvfs to determine chunk sizes and make
+ # *****sure**** copy is page-atomic; the following is
+ # good enough for 99.9% of everybody and won't take a
+ # week to audit though.
+ chunk = readfile.read(self._chunkSize)
+ writefile.write(chunk)
+ if len(chunk) < self._chunkSize:
+ break
+ finally:
+ readfile.close()
+ finally:
+ writefile.close()
+ elif not self.exists():
+ raise OSError(errno.ENOENT, "No such file or directory")
+ else:
+ # If you see the following message because you want to copy
+ # symlinks, fifos, block devices, character devices, or unix
+ # sockets, please feel free to add support to do sensible things in
+ # reaction to those types!
+ raise NotImplementedError(
+ "Only copying of files and directories supported")
+
+
+ def moveTo(self, destination, followLinks=True):
+ """
+ Move self to destination - basically renaming self to whatever
+ destination is named. If destination is an already-existing directory,
+ moves all children to destination if destination is empty. If
+ destination is a non-empty directory, or destination is a file, an
+ OSError will be raised.
+
+ If moving between filesystems, self needs to be copied, and everything
+ that applies to copyTo applies to moveTo.
+
+ @param destination: the destination (a FilePath) to which self
+ should be copied
+ @param followLinks: whether symlinks in self should be treated as links
+ or as their targets (only applicable when moving between
+ filesystems)
+ """
+ try:
+ os.rename(self.path, destination.path)
+ except OSError, ose:
+ if ose.errno == errno.EXDEV:
+ # man 2 rename, ubuntu linux 5.10 "breezy":
+
+ # oldpath and newpath are not on the same mounted filesystem.
+ # (Linux permits a filesystem to be mounted at multiple
+ # points, but rename(2) does not work across different mount
+ # points, even if the same filesystem is mounted on both.)
+
+ # that means it's time to copy trees of directories!
+ secsib = destination.temporarySibling()
+ self.copyTo(secsib, followLinks) # slow
+ secsib.moveTo(destination, followLinks) # visible
+
+ # done creating new stuff. let's clean me up.
+ mysecsib = self.temporarySibling()
+ self.moveTo(mysecsib, followLinks) # visible
+ mysecsib.remove() # slow
+ else:
+ raise
+ else:
+ self.changed()
+ destination.changed()
+
+
+FilePath.clonePath = FilePath
diff --git a/twisted/python/finalize.py b/twisted/python/finalize.py
new file mode 100644
index 0000000..8b99bf6
--- /dev/null
+++ b/twisted/python/finalize.py
@@ -0,0 +1,46 @@
+
+"""
+A module for externalized finalizers.
+"""
+
+import weakref
+
+garbageKey = 0
+
+def callbackFactory(num, fins):
+ def _cb(w):
+ del refs[num]
+ for fx in fins:
+ fx()
+ return _cb
+
+refs = {}
+
+def register(inst):
+ global garbageKey
+ garbageKey += 1
+ r = weakref.ref(inst, callbackFactory(garbageKey, inst.__finalizers__()))
+ refs[garbageKey] = r
+
+if __name__ == '__main__':
+ def fin():
+ print 'I am _so_ dead.'
+
+ class Finalizeable:
+ """
+ An un-sucky __del__
+ """
+
+ def __finalizers__(self):
+ """
+ I'm going away.
+ """
+ return [fin]
+
+ f = Finalizeable()
+ f.f2 = f
+ register(f)
+ del f
+ import gc
+ gc.collect()
+ print 'deled'
diff --git a/twisted/python/formmethod.py b/twisted/python/formmethod.py
new file mode 100644
index 0000000..4fb1c5f
--- /dev/null
+++ b/twisted/python/formmethod.py
@@ -0,0 +1,363 @@
+# -*- test-case-name: twisted.test.test_formmethod -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Form-based method objects.
+
+This module contains support for descriptive method signatures that can be used
+to format methods.
+"""
+
+import calendar
+
+class FormException(Exception):
+ """An error occurred calling the form method.
+ """
+ def __init__(self, *args, **kwargs):
+ Exception.__init__(self, *args)
+ self.descriptions = kwargs
+
+
+class InputError(FormException):
+ """
+ An error occurred with some input.
+ """
+
+
+class Argument:
+ """Base class for form arguments."""
+
+ # default value for argument, if no other default is given
+ defaultDefault = None
+
+ def __init__(self, name, default=None, shortDesc=None,
+ longDesc=None, hints=None, allowNone=1):
+ self.name = name
+ self.allowNone = allowNone
+ if default is None:
+ default = self.defaultDefault
+ self.default = default
+ self.shortDesc = shortDesc
+ self.longDesc = longDesc
+ if not hints:
+ hints = {}
+ self.hints = hints
+
+ def addHints(self, **kwargs):
+ self.hints.update(kwargs)
+
+ def getHint(self, name, default=None):
+ return self.hints.get(name, default)
+
+ def getShortDescription(self):
+ return self.shortDesc or self.name.capitalize()
+
+ def getLongDescription(self):
+ return self.longDesc or '' #self.shortDesc or "The %s." % self.name
+
+ def coerce(self, val):
+ """Convert the value to the correct format."""
+ raise NotImplementedError, "implement in subclass"
+
+
+class String(Argument):
+ """A single string.
+ """
+ defaultDefault = ''
+ min = 0
+ max = None
+
+ def __init__(self, name, default=None, shortDesc=None,
+ longDesc=None, hints=None, allowNone=1, min=0, max=None):
+ Argument.__init__(self, name, default=default, shortDesc=shortDesc,
+ longDesc=longDesc, hints=hints, allowNone=allowNone)
+ self.min = min
+ self.max = max
+
+ def coerce(self, val):
+ s = str(val)
+ if len(s) < self.min:
+ raise InputError, "Value must be at least %s characters long" % self.min
+ if self.max != None and len(s) > self.max:
+ raise InputError, "Value must be at most %s characters long" % self.max
+ return str(val)
+
+
+class Text(String):
+ """A long string.
+ """
+
+
+class Password(String):
+ """A string which should be obscured when input.
+ """
+
+
+class VerifiedPassword(String):
+ """A string that should be obscured when input and needs verification."""
+
+ def coerce(self, vals):
+ if len(vals) != 2 or vals[0] != vals[1]:
+ raise InputError, "Please enter the same password twice."
+ s = str(vals[0])
+ if len(s) < self.min:
+ raise InputError, "Value must be at least %s characters long" % self.min
+ if self.max != None and len(s) > self.max:
+ raise InputError, "Value must be at most %s characters long" % self.max
+ return s
+
+
+class Hidden(String):
+ """A string which is not displayed.
+
+ The passed default is used as the value.
+ """
+
+
+class Integer(Argument):
+ """A single integer.
+ """
+ defaultDefault = None
+
+ def __init__(self, name, allowNone=1, default=None, shortDesc=None,
+ longDesc=None, hints=None):
+ #although Argument now has allowNone, that was recently added, and
+ #putting it at the end kept things which relied on argument order
+ #from breaking. However, allowNone originally was in here, so
+ #I have to keep the same order, to prevent breaking code that
+ #depends on argument order only
+ Argument.__init__(self, name, default, shortDesc, longDesc, hints,
+ allowNone)
+
+ def coerce(self, val):
+ if not val.strip() and self.allowNone:
+ return None
+ try:
+ return int(val)
+ except ValueError:
+ raise InputError, "%s is not valid, please enter a whole number, e.g. 10" % val
+
+
+class IntegerRange(Integer):
+
+ def __init__(self, name, min, max, allowNone=1, default=None, shortDesc=None,
+ longDesc=None, hints=None):
+ self.min = min
+ self.max = max
+ Integer.__init__(self, name, allowNone=allowNone, default=default, shortDesc=shortDesc,
+ longDesc=longDesc, hints=hints)
+
+ def coerce(self, val):
+ result = Integer.coerce(self, val)
+ if self.allowNone and result == None:
+ return result
+ if result < self.min:
+ raise InputError, "Value %s is too small, it should be at least %s" % (result, self.min)
+ if result > self.max:
+ raise InputError, "Value %s is too large, it should be at most %s" % (result, self.max)
+ return result
+
+
+class Float(Argument):
+
+ defaultDefault = None
+
+ def __init__(self, name, allowNone=1, default=None, shortDesc=None,
+ longDesc=None, hints=None):
+ #although Argument now has allowNone, that was recently added, and
+ #putting it at the end kept things which relied on argument order
+ #from breaking. However, allowNone originally was in here, so
+ #I have to keep the same order, to prevent breaking code that
+ #depends on argument order only
+ Argument.__init__(self, name, default, shortDesc, longDesc, hints,
+ allowNone)
+
+
+ def coerce(self, val):
+ if not val.strip() and self.allowNone:
+ return None
+ try:
+ return float(val)
+ except ValueError:
+ raise InputError, "Invalid float: %s" % val
+
+
+class Choice(Argument):
+ """
+ The result of a choice between enumerated types. The choices should
+ be a list of tuples of tag, value, and description. The tag will be
+ the value returned if the user hits "Submit", and the description
+ is the bale for the enumerated type. default is a list of all the
+ values (seconds element in choices). If no defaults are specified,
+ initially the first item will be selected. Only one item can (should)
+ be selected at once.
+ """
+ def __init__(self, name, choices=[], default=[], shortDesc=None,
+ longDesc=None, hints=None, allowNone=1):
+ self.choices = choices
+ if choices and not default:
+ default.append(choices[0][1])
+ Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone)
+
+ def coerce(self, inIdent):
+ for ident, val, desc in self.choices:
+ if ident == inIdent:
+ return val
+ else:
+ raise InputError("Invalid Choice: %s" % inIdent)
+
+
+class Flags(Argument):
+ """
+ The result of a checkbox group or multi-menu. The flags should be a
+ list of tuples of tag, value, and description. The tag will be
+ the value returned if the user hits "Submit", and the description
+ is the bale for the enumerated type. default is a list of all the
+ values (second elements in flags). If no defaults are specified,
+ initially nothing will be selected. Several items may be selected at
+ once.
+ """
+ def __init__(self, name, flags=(), default=(), shortDesc=None,
+ longDesc=None, hints=None, allowNone=1):
+ self.flags = flags
+ Argument.__init__(self, name, default, shortDesc, longDesc, hints, allowNone=allowNone)
+
+ def coerce(self, inFlagKeys):
+ if not inFlagKeys:
+ return []
+ outFlags = []
+ for inFlagKey in inFlagKeys:
+ for flagKey, flagVal, flagDesc in self.flags:
+ if inFlagKey == flagKey:
+ outFlags.append(flagVal)
+ break
+ else:
+ raise InputError("Invalid Flag: %s" % inFlagKey)
+ return outFlags
+
+
+class CheckGroup(Flags):
+ pass
+
+
+class RadioGroup(Choice):
+ pass
+
+
+class Boolean(Argument):
+ def coerce(self, inVal):
+ if not inVal:
+ return 0
+ lInVal = str(inVal).lower()
+ if lInVal in ('no', 'n', 'f', 'false', '0'):
+ return 0
+ return 1
+
+class File(Argument):
+ def __init__(self, name, allowNone=1, shortDesc=None, longDesc=None,
+ hints=None):
+ self.allowNone = allowNone
+ Argument.__init__(self, name, None, shortDesc, longDesc, hints)
+
+ def coerce(self, file):
+ if not file and self.allowNone:
+ return None
+ elif file:
+ return file
+ else:
+ raise InputError, "Invalid File"
+
+def positiveInt(x):
+ x = int(x)
+ if x <= 0: raise ValueError
+ return x
+
+class Date(Argument):
+ """A date -- (year, month, day) tuple."""
+
+ defaultDefault = None
+
+ def __init__(self, name, allowNone=1, default=None, shortDesc=None,
+ longDesc=None, hints=None):
+ Argument.__init__(self, name, default, shortDesc, longDesc, hints)
+ self.allowNone = allowNone
+ if not allowNone:
+ self.defaultDefault = (1970, 1, 1)
+
+ def coerce(self, args):
+ """Return tuple of ints (year, month, day)."""
+ if tuple(args) == ("", "", "") and self.allowNone:
+ return None
+
+ try:
+ year, month, day = map(positiveInt, args)
+ except ValueError:
+ raise InputError, "Invalid date"
+ if (month, day) == (2, 29):
+ if not calendar.isleap(year):
+ raise InputError, "%d was not a leap year" % year
+ else:
+ return year, month, day
+ try:
+ mdays = calendar.mdays[month]
+ except IndexError:
+ raise InputError, "Invalid date"
+ if day > mdays:
+ raise InputError, "Invalid date"
+ return year, month, day
+
+
+class Submit(Choice):
+ """Submit button or a reasonable facsimile thereof."""
+
+ def __init__(self, name, choices=[("Submit", "submit", "Submit form")],
+ reset=0, shortDesc=None, longDesc=None, allowNone=0, hints=None):
+ Choice.__init__(self, name, choices=choices, shortDesc=shortDesc,
+ longDesc=longDesc, hints=hints)
+ self.allowNone = allowNone
+ self.reset = reset
+
+ def coerce(self, value):
+ if self.allowNone and not value:
+ return None
+ else:
+ return Choice.coerce(self, value)
+
+
+class PresentationHint:
+ """
+ A hint to a particular system.
+ """
+
+
+class MethodSignature:
+
+ def __init__(self, *sigList):
+ """
+ """
+ self.methodSignature = sigList
+
+ def getArgument(self, name):
+ for a in self.methodSignature:
+ if a.name == name:
+ return a
+
+ def method(self, callable, takesRequest=False):
+ return FormMethod(self, callable, takesRequest)
+
+
+class FormMethod:
+ """A callable object with a signature."""
+
+ def __init__(self, signature, callable, takesRequest=False):
+ self.signature = signature
+ self.callable = callable
+ self.takesRequest = takesRequest
+
+ def getArgs(self):
+ return tuple(self.signature.methodSignature)
+
+ def call(self,*args,**kw):
+ return self.callable(*args,**kw)
diff --git a/twisted/python/hashlib.py b/twisted/python/hashlib.py
new file mode 100644
index 0000000..f3ee0fe
--- /dev/null
+++ b/twisted/python/hashlib.py
@@ -0,0 +1,24 @@
+# -*- test-case-name: twisted.python.test.test_hashlib -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+L{twisted.python.hashlib} presents a subset of the interface provided by
+U{hashlib<http://docs.python.org/library/hashlib.html>}. The subset is the
+interface required by various parts of Twisted. This allows application code
+to transparently use APIs which existed before C{hashlib} was introduced or to
+use C{hashlib} if it is available.
+"""
+
+
+try:
+ _hashlib = __import__("hashlib")
+except ImportError:
+ from md5 import md5
+ from sha import sha as sha1
+else:
+ md5 = _hashlib.md5
+ sha1 = _hashlib.sha1
+
+
+__all__ = ["md5", "sha1"]
diff --git a/twisted/python/hook.py b/twisted/python/hook.py
new file mode 100644
index 0000000..256054c
--- /dev/null
+++ b/twisted/python/hook.py
@@ -0,0 +1,177 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+"""
+I define support for hookable instance methods.
+
+These are methods which you can register pre-call and post-call external
+functions to augment their functionality. People familiar with more esoteric
+languages may think of these as \"method combinations\".
+
+This could be used to add optional preconditions, user-extensible callbacks
+(a-la emacs) or a thread-safety mechanism.
+
+The four exported calls are:
+
+ - L{addPre}
+ - L{addPost}
+ - L{removePre}
+ - L{removePost}
+
+All have the signature (class, methodName, callable), and the callable they
+take must always have the signature (instance, *args, **kw) unless the
+particular signature of the method they hook is known.
+
+Hooks should typically not throw exceptions, however, no effort will be made by
+this module to prevent them from doing so. Pre-hooks will always be called,
+but post-hooks will only be called if the pre-hooks do not raise any exceptions
+(they will still be called if the main method raises an exception). The return
+values and exception status of the main method will be propogated (assuming
+none of the hooks raise an exception). Hooks will be executed in the order in
+which they are added.
+
+"""
+
+# System Imports
+import string
+
+### Public Interface
+
+class HookError(Exception):
+ "An error which will fire when an invariant is violated."
+
+def addPre(klass, name, func):
+ """hook.addPre(klass, name, func) -> None
+
+ Add a function to be called before the method klass.name is invoked.
+ """
+
+ _addHook(klass, name, PRE, func)
+
+def addPost(klass, name, func):
+ """hook.addPost(klass, name, func) -> None
+
+ Add a function to be called after the method klass.name is invoked.
+ """
+ _addHook(klass, name, POST, func)
+
+def removePre(klass, name, func):
+ """hook.removePre(klass, name, func) -> None
+
+ Remove a function (previously registered with addPre) so that it
+ is no longer executed before klass.name.
+ """
+
+ _removeHook(klass, name, PRE, func)
+
+def removePost(klass, name, func):
+ """hook.removePre(klass, name, func) -> None
+
+ Remove a function (previously registered with addPost) so that it
+ is no longer executed after klass.name.
+ """
+ _removeHook(klass, name, POST, func)
+
+### "Helper" functions.
+
+hooked_func = """
+
+import %(module)s
+
+def %(name)s(*args, **kw):
+ klazz = %(module)s.%(klass)s
+ for preMethod in klazz.%(preName)s:
+ preMethod(*args, **kw)
+ try:
+ return klazz.%(originalName)s(*args, **kw)
+ finally:
+ for postMethod in klazz.%(postName)s:
+ postMethod(*args, **kw)
+"""
+
+_PRE = '__hook_pre_%s_%s_%s__'
+_POST = '__hook_post_%s_%s_%s__'
+_ORIG = '__hook_orig_%s_%s_%s__'
+
+
+def _XXX(k,n,s):
+ "string manipulation garbage"
+ x = s % (string.replace(k.__module__,'.','_'), k.__name__, n)
+ return x
+
+def PRE(k,n):
+ "(private) munging to turn a method name into a pre-hook-method-name"
+ return _XXX(k,n,_PRE)
+
+def POST(k,n):
+ "(private) munging to turn a method name into a post-hook-method-name"
+ return _XXX(k,n,_POST)
+
+def ORIG(k,n):
+ "(private) munging to turn a method name into an `original' identifier"
+ return _XXX(k,n,_ORIG)
+
+
+def _addHook(klass, name, phase, func):
+ "(private) adds a hook to a method on a class"
+ _enhook(klass, name)
+
+ if not hasattr(klass, phase(klass, name)):
+ setattr(klass, phase(klass, name), [])
+
+ phaselist = getattr(klass, phase(klass, name))
+ phaselist.append(func)
+
+
+def _removeHook(klass, name, phase, func):
+ "(private) removes a hook from a method on a class"
+ phaselistname = phase(klass, name)
+ if not hasattr(klass, ORIG(klass,name)):
+ raise HookError("no hooks present!")
+
+ phaselist = getattr(klass, phase(klass, name))
+ try: phaselist.remove(func)
+ except ValueError:
+ raise HookError("hook %s not found in removal list for %s"%
+ (name,klass))
+
+ if not getattr(klass, PRE(klass,name)) and not getattr(klass, POST(klass, name)):
+ _dehook(klass, name)
+
+def _enhook(klass, name):
+ "(private) causes a certain method name to be hooked on a class"
+ if hasattr(klass, ORIG(klass, name)):
+ return
+
+ def newfunc(*args, **kw):
+ for preMethod in getattr(klass, PRE(klass, name)):
+ preMethod(*args, **kw)
+ try:
+ return getattr(klass, ORIG(klass, name))(*args, **kw)
+ finally:
+ for postMethod in getattr(klass, POST(klass, name)):
+ postMethod(*args, **kw)
+ try:
+ newfunc.func_name = name
+ except TypeError:
+ # Older python's don't let you do this
+ pass
+
+ oldfunc = getattr(klass, name).im_func
+ setattr(klass, ORIG(klass, name), oldfunc)
+ setattr(klass, PRE(klass, name), [])
+ setattr(klass, POST(klass, name), [])
+ setattr(klass, name, newfunc)
+
+def _dehook(klass, name):
+ "(private) causes a certain method name no longer to be hooked on a class"
+
+ if not hasattr(klass, ORIG(klass, name)):
+ raise HookError("Cannot unhook!")
+ setattr(klass, name, getattr(klass, ORIG(klass,name)))
+ delattr(klass, PRE(klass,name))
+ delattr(klass, POST(klass,name))
+ delattr(klass, ORIG(klass,name))
diff --git a/twisted/python/htmlizer.py b/twisted/python/htmlizer.py
new file mode 100644
index 0000000..c95fb00
--- /dev/null
+++ b/twisted/python/htmlizer.py
@@ -0,0 +1,91 @@
+# -*- test-case-name: twisted.python.test.test_htmlizer -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HTML rendering of Python source.
+"""
+
+import tokenize, cgi, keyword
+import reflect
+
+class TokenPrinter:
+
+ currentCol, currentLine = 0, 1
+ lastIdentifier = parameters = 0
+
+ def __init__(self, writer):
+ self.writer = writer
+
+ def printtoken(self, type, token, (srow, scol), (erow, ecol), line):
+ #print "printtoken(%r,%r,%r,(%r,%r),(%r,%r),%r), row=%r,col=%r" % (
+ # self, type, token, srow,scol, erow,ecol, line,
+ # self.currentLine, self.currentCol)
+ if self.currentLine < srow:
+ self.writer('\n'*(srow-self.currentLine))
+ self.currentLine, self.currentCol = srow, 0
+ self.writer(' '*(scol-self.currentCol))
+ if self.lastIdentifier:
+ type = "identifier"
+ self.parameters = 1
+ elif type == tokenize.NAME:
+ if keyword.iskeyword(token):
+ type = 'keyword'
+ else:
+ if self.parameters:
+ type = 'parameter'
+ else:
+ type = 'variable'
+ else:
+ type = tokenize.tok_name.get(type).lower()
+ self.writer(token, type)
+ self.currentCol = ecol
+ self.currentLine += token.count('\n')
+ if self.currentLine != erow:
+ self.currentCol = 0
+ self.lastIdentifier = token in ('def', 'class')
+ if token == ':':
+ self.parameters = 0
+
+
+class HTMLWriter:
+
+ noSpan = []
+
+ def __init__(self, writer):
+ self.writer = writer
+ noSpan = []
+ reflect.accumulateClassList(self.__class__, "noSpan", noSpan)
+ self.noSpan = noSpan
+
+ def write(self, token, type=None):
+ token = cgi.escape(token)
+ if (type is None) or (type in self.noSpan):
+ self.writer(token)
+ else:
+ self.writer('<span class="py-src-%s">%s</span>' %
+ (type, token))
+
+
+class SmallerHTMLWriter(HTMLWriter):
+ """HTMLWriter that doesn't generate spans for some junk.
+
+ Results in much smaller HTML output.
+ """
+ noSpan = ["endmarker", "indent", "dedent", "op", "newline", "nl"]
+
+def filter(inp, out, writer=HTMLWriter):
+ out.write('<pre>')
+ printer = TokenPrinter(writer(out.write).write).printtoken
+ try:
+ tokenize.tokenize(inp.readline, printer)
+ except tokenize.TokenError:
+ pass
+ out.write('</pre>\n')
+
+def main():
+ import sys
+ filter(open(sys.argv[1]), sys.stdout)
+
+if __name__ == '__main__':
+ main()
diff --git a/twisted/python/lockfile.py b/twisted/python/lockfile.py
new file mode 100644
index 0000000..a044957
--- /dev/null
+++ b/twisted/python/lockfile.py
@@ -0,0 +1,214 @@
+# -*- test-case-name: twisted.test.test_lockfile -*-
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Filesystem-based interprocess mutex.
+"""
+
+__metaclass__ = type
+
+import errno, os
+
+from time import time as _uniquefloat
+
+from twisted.python.runtime import platform
+
+def unique():
+ return str(long(_uniquefloat() * 1000))
+
+from os import rename
+if not platform.isWindows():
+ from os import kill
+ from os import symlink
+ from os import readlink
+ from os import remove as rmlink
+ _windows = False
+else:
+ _windows = True
+
+ try:
+ from win32api import OpenProcess
+ import pywintypes
+ except ImportError:
+ kill = None
+ else:
+ ERROR_ACCESS_DENIED = 5
+ ERROR_INVALID_PARAMETER = 87
+
+ def kill(pid, signal):
+ try:
+ OpenProcess(0, 0, pid)
+ except pywintypes.error, e:
+ if e.args[0] == ERROR_ACCESS_DENIED:
+ return
+ elif e.args[0] == ERROR_INVALID_PARAMETER:
+ raise OSError(errno.ESRCH, None)
+ raise
+ else:
+ raise RuntimeError("OpenProcess is required to fail.")
+
+ _open = file
+
+ # XXX Implement an atomic thingamajig for win32
+ def symlink(value, filename):
+ newlinkname = filename+"."+unique()+'.newlink'
+ newvalname = os.path.join(newlinkname,"symlink")
+ os.mkdir(newlinkname)
+ f = _open(newvalname,'wcb')
+ f.write(value)
+ f.flush()
+ f.close()
+ try:
+ rename(newlinkname, filename)
+ except:
+ os.remove(newvalname)
+ os.rmdir(newlinkname)
+ raise
+
+ def readlink(filename):
+ try:
+ fObj = _open(os.path.join(filename,'symlink'), 'rb')
+ except IOError, e:
+ if e.errno == errno.ENOENT or e.errno == errno.EIO:
+ raise OSError(e.errno, None)
+ raise
+ else:
+ result = fObj.read()
+ fObj.close()
+ return result
+
+ def rmlink(filename):
+ os.remove(os.path.join(filename, 'symlink'))
+ os.rmdir(filename)
+
+
+
+class FilesystemLock:
+ """
+ A mutex.
+
+ This relies on the filesystem property that creating
+ a symlink is an atomic operation and that it will
+ fail if the symlink already exists. Deleting the
+ symlink will release the lock.
+
+ @ivar name: The name of the file associated with this lock.
+
+ @ivar clean: Indicates whether this lock was released cleanly by its
+ last owner. Only meaningful after C{lock} has been called and
+ returns True.
+
+ @ivar locked: Indicates whether the lock is currently held by this
+ object.
+ """
+
+ clean = None
+ locked = False
+
+ def __init__(self, name):
+ self.name = name
+
+
+ def lock(self):
+ """
+ Acquire this lock.
+
+ @rtype: C{bool}
+ @return: True if the lock is acquired, false otherwise.
+
+ @raise: Any exception os.symlink() may raise, other than
+ EEXIST.
+ """
+ clean = True
+ while True:
+ try:
+ symlink(str(os.getpid()), self.name)
+ except OSError, e:
+ if _windows and e.errno in (errno.EACCES, errno.EIO):
+ # The lock is in the middle of being deleted because we're
+ # on Windows where lock removal isn't atomic. Give up, we
+ # don't know how long this is going to take.
+ return False
+ if e.errno == errno.EEXIST:
+ try:
+ pid = readlink(self.name)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ # The lock has vanished, try to claim it in the
+ # next iteration through the loop.
+ continue
+ raise
+ except IOError, e:
+ if _windows and e.errno == errno.EACCES:
+ # The lock is in the middle of being
+ # deleted because we're on Windows where
+ # lock removal isn't atomic. Give up, we
+ # don't know how long this is going to
+ # take.
+ return False
+ raise
+ try:
+ if kill is not None:
+ kill(int(pid), 0)
+ except OSError, e:
+ if e.errno == errno.ESRCH:
+ # The owner has vanished, try to claim it in the next
+ # iteration through the loop.
+ try:
+ rmlink(self.name)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ # Another process cleaned up the lock.
+ # Race them to acquire it in the next
+ # iteration through the loop.
+ continue
+ raise
+ clean = False
+ continue
+ raise
+ return False
+ raise
+ self.locked = True
+ self.clean = clean
+ return True
+
+
+ def unlock(self):
+ """
+ Release this lock.
+
+ This deletes the directory with the given name.
+
+ @raise: Any exception os.readlink() may raise, or
+ ValueError if the lock is not owned by this process.
+ """
+ pid = readlink(self.name)
+ if int(pid) != os.getpid():
+ raise ValueError("Lock %r not owned by this process" % (self.name,))
+ rmlink(self.name)
+ self.locked = False
+
+
+def isLocked(name):
+ """Determine if the lock of the given name is held or not.
+
+ @type name: C{str}
+ @param name: The filesystem path to the lock to test
+
+ @rtype: C{bool}
+ @return: True if the lock is held, False otherwise.
+ """
+ l = FilesystemLock(name)
+ result = None
+ try:
+ result = l.lock()
+ finally:
+ if result:
+ l.unlock()
+ return not result
+
+
+__all__ = ['FilesystemLock', 'isLocked']
+
diff --git a/twisted/python/log.py b/twisted/python/log.py
new file mode 100644
index 0000000..6a7fa49
--- /dev/null
+++ b/twisted/python/log.py
@@ -0,0 +1,706 @@
+# -*- test-case-name: twisted.test.test_log -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Logging and metrics infrastructure.
+"""
+
+from __future__ import division
+
+import sys
+import time
+import warnings
+from datetime import datetime
+import logging
+
+from zope.interface import Interface
+
+from twisted.python import util, context, reflect
+
+
+
+class ILogContext:
+ """
+ Actually, this interface is just a synoym for the dictionary interface,
+ but it serves as a key for the default information in a log.
+
+ I do not inherit from Interface because the world is a cruel place.
+ """
+
+
+
+class ILogObserver(Interface):
+ """
+ An observer which can do something with log events.
+
+ Given that most log observers are actually bound methods, it's okay to not
+ explicitly declare provision of this interface.
+ """
+ def __call__(eventDict):
+ """
+ Log an event.
+
+ @type eventDict: C{dict} with C{str} keys.
+ @param eventDict: A dictionary with arbitrary keys. However, these
+ keys are often available:
+ - C{message}: A C{tuple} of C{str} containing messages to be
+ logged.
+ - C{system}: A C{str} which indicates the "system" which is
+ generating this event.
+ - C{isError}: A C{bool} indicating whether this event represents
+ an error.
+ - C{failure}: A L{failure.Failure} instance
+ - C{why}: Used as header of the traceback in case of errors.
+ - C{format}: A string format used in place of C{message} to
+ customize the event. The intent is for the observer to format
+ a message by doing something like C{format % eventDict}.
+ """
+
+
+
+context.setDefault(ILogContext,
+ {"isError": 0,
+ "system": "-"})
+
+def callWithContext(ctx, func, *args, **kw):
+ newCtx = context.get(ILogContext).copy()
+ newCtx.update(ctx)
+ return context.call({ILogContext: newCtx}, func, *args, **kw)
+
+def callWithLogger(logger, func, *args, **kw):
+ """
+ Utility method which wraps a function in a try:/except:, logs a failure if
+ one occurrs, and uses the system's logPrefix.
+ """
+ try:
+ lp = logger.logPrefix()
+ except KeyboardInterrupt:
+ raise
+ except:
+ lp = '(buggy logPrefix method)'
+ err(system=lp)
+ try:
+ return callWithContext({"system": lp}, func, *args, **kw)
+ except KeyboardInterrupt:
+ raise
+ except:
+ err(system=lp)
+
+
+
+_keepErrors = 0
+_keptErrors = []
+_ignoreErrors = []
+
+def startKeepingErrors():
+ """
+ DEPRECATED in Twisted 2.5.
+
+ Support function for testing frameworks.
+
+ Start keeping errors in a buffer which can be retrieved (and emptied) with
+ flushErrors.
+ """
+ warnings.warn("log.startKeepingErrors is deprecated since Twisted 2.5",
+ category=DeprecationWarning, stacklevel=2)
+ global _keepErrors
+ _keepErrors = 1
+
+
+def flushErrors(*errorTypes):
+ """
+ DEPRECATED in Twisted 2.5. See L{TestCase.flushLoggedErrors}.
+
+ Support function for testing frameworks.
+
+ Return a list of errors that occurred since the last call to flushErrors().
+ (This will return None unless startKeepingErrors has been called.)
+ """
+
+ warnings.warn("log.flushErrors is deprecated since Twisted 2.5. "
+ "If you need to flush errors from within a unittest, "
+ "use TestCase.flushLoggedErrors instead.",
+ category=DeprecationWarning, stacklevel=2)
+ return _flushErrors(*errorTypes)
+
+
+def _flushErrors(*errorTypes):
+ """
+ PRIVATE. DEPRECATED. DON'T USE.
+ """
+ global _keptErrors
+ k = _keptErrors
+ _keptErrors = []
+ if errorTypes:
+ for erk in k:
+ shouldReLog = 1
+ for errT in errorTypes:
+ if erk.check(errT):
+ shouldReLog = 0
+ if shouldReLog:
+ err(erk)
+ return k
+
+def ignoreErrors(*types):
+ """
+ DEPRECATED
+ """
+ warnings.warn("log.ignoreErrors is deprecated since Twisted 2.5",
+ category=DeprecationWarning, stacklevel=2)
+ _ignore(*types)
+
+def _ignore(*types):
+ """
+ PRIVATE. DEPRECATED. DON'T USE.
+ """
+ for type in types:
+ _ignoreErrors.append(type)
+
+def clearIgnores():
+ """
+ DEPRECATED
+ """
+ warnings.warn("log.clearIgnores is deprecated since Twisted 2.5",
+ category=DeprecationWarning, stacklevel=2)
+ _clearIgnores()
+
+def _clearIgnores():
+ """
+ PRIVATE. DEPRECATED. DON'T USE.
+ """
+ global _ignoreErrors
+ _ignoreErrors = []
+
+
+def err(_stuff=None, _why=None, **kw):
+ """
+ Write a failure to the log.
+
+ The C{_stuff} and C{_why} parameters use an underscore prefix to lessen
+ the chance of colliding with a keyword argument the application wishes
+ to pass. It is intended that they be supplied with arguments passed
+ positionally, not by keyword.
+
+ @param _stuff: The failure to log. If C{_stuff} is C{None} a new
+ L{Failure} will be created from the current exception state. If
+ C{_stuff} is an C{Exception} instance it will be wrapped in a
+ L{Failure}.
+ @type _stuff: C{NoneType}, C{Exception}, or L{Failure}.
+
+ @param _why: The source of this failure. This will be logged along with
+ C{_stuff} and should describe the context in which the failure
+ occurred.
+ @type _why: C{str}
+ """
+ if _stuff is None:
+ _stuff = failure.Failure()
+ if isinstance(_stuff, failure.Failure):
+ if _keepErrors:
+ if _ignoreErrors:
+ keep = 0
+ for err in _ignoreErrors:
+ r = _stuff.check(err)
+ if r:
+ keep = 0
+ break
+ else:
+ keep = 1
+ if keep:
+ _keptErrors.append(_stuff)
+ else:
+ _keptErrors.append(_stuff)
+ msg(failure=_stuff, why=_why, isError=1, **kw)
+ elif isinstance(_stuff, Exception):
+ msg(failure=failure.Failure(_stuff), why=_why, isError=1, **kw)
+ else:
+ msg(repr(_stuff), why=_why, isError=1, **kw)
+
+deferr = err
+
+
+class Logger:
+ """
+ This represents a class which may 'own' a log. Used by subclassing.
+ """
+ def logPrefix(self):
+ """
+ Override this method to insert custom logging behavior. Its
+ return value will be inserted in front of every line. It may
+ be called more times than the number of output lines.
+ """
+ return '-'
+
+
+class LogPublisher:
+ """
+ Class for singleton log message publishing.
+ """
+
+ synchronized = ['msg']
+
+ def __init__(self):
+ self.observers = []
+
+ def addObserver(self, other):
+ """
+ Add a new observer.
+
+ @type other: Provider of L{ILogObserver}
+ @param other: A callable object that will be called with each new log
+ message (a dict).
+ """
+ assert callable(other)
+ self.observers.append(other)
+
+ def removeObserver(self, other):
+ """
+ Remove an observer.
+ """
+ self.observers.remove(other)
+
+ def msg(self, *message, **kw):
+ """
+ Log a new message.
+
+ For example::
+
+ >>> log.msg('Hello, world.')
+
+ In particular, you MUST avoid the forms::
+
+ >>> log.msg(u'Hello, world.')
+ >>> log.msg('Hello ', 'world.')
+
+ These forms work (sometimes) by accident and will be disabled
+ entirely in the future.
+ """
+ actualEventDict = (context.get(ILogContext) or {}).copy()
+ actualEventDict.update(kw)
+ actualEventDict['message'] = message
+ actualEventDict['time'] = time.time()
+ for i in xrange(len(self.observers) - 1, -1, -1):
+ try:
+ self.observers[i](actualEventDict)
+ except KeyboardInterrupt:
+ # Don't swallow keyboard interrupt!
+ raise
+ except UnicodeEncodeError:
+ raise
+ except:
+ observer = self.observers[i]
+ self.observers[i] = lambda event: None
+ try:
+ self._err(failure.Failure(),
+ "Log observer %s failed." % (observer,))
+ except:
+ # Sometimes err() will throw an exception,
+ # e.g. RuntimeError due to blowing the stack; if that
+ # happens, there's not much we can do...
+ pass
+ self.observers[i] = observer
+
+
+ def _err(self, failure, why):
+ """
+ Log a failure.
+
+ Similar in functionality to the global {err} function, but the failure
+ gets published only to observers attached to this publisher.
+
+ @param failure: The failure to log.
+ @type failure: L{Failure}.
+
+ @param why: The source of this failure. This will be logged along with
+ the C{failure} and should describe the context in which the failure
+ occurred.
+ @type why: C{str}
+ """
+ self.msg(failure=failure, why=why, isError=1)
+
+
+ def showwarning(self, message, category, filename, lineno, file=None,
+ line=None):
+ """
+ Twisted-enabled wrapper around L{warnings.showwarning}.
+
+ If C{file} is C{None}, the default behaviour is to emit the warning to
+ the log system, otherwise the original L{warnings.showwarning} Python
+ function is called.
+ """
+ if file is None:
+ self.msg(warning=message, category=reflect.qual(category),
+ filename=filename, lineno=lineno,
+ format="%(filename)s:%(lineno)s: %(category)s: %(warning)s")
+ else:
+ if sys.version_info < (2, 6):
+ _oldshowwarning(message, category, filename, lineno, file)
+ else:
+ _oldshowwarning(message, category, filename, lineno, file, line)
+
+
+
+
+try:
+ theLogPublisher
+except NameError:
+ theLogPublisher = LogPublisher()
+ addObserver = theLogPublisher.addObserver
+ removeObserver = theLogPublisher.removeObserver
+ msg = theLogPublisher.msg
+ showwarning = theLogPublisher.showwarning
+
+
+def _safeFormat(fmtString, fmtDict):
+ """
+ Try to format the string C{fmtString} using C{fmtDict} arguments,
+ swallowing all errors to always return a string.
+ """
+ # There's a way we could make this if not safer at least more
+ # informative: perhaps some sort of str/repr wrapper objects
+ # could be wrapped around the things inside of C{fmtDict}. That way
+ # if the event dict contains an object with a bad __repr__, we
+ # can only cry about that individual object instead of the
+ # entire event dict.
+ try:
+ text = fmtString % fmtDict
+ except KeyboardInterrupt:
+ raise
+ except:
+ try:
+ text = ('Invalid format string or unformattable object in log message: %r, %s' % (fmtString, fmtDict))
+ except:
+ try:
+ text = 'UNFORMATTABLE OBJECT WRITTEN TO LOG with fmt %r, MESSAGE LOST' % (fmtString,)
+ except:
+ text = 'PATHOLOGICAL ERROR IN BOTH FORMAT STRING AND MESSAGE DETAILS, MESSAGE LOST'
+ return text
+
+
+def textFromEventDict(eventDict):
+ """
+ Extract text from an event dict passed to a log observer. If it cannot
+ handle the dict, it returns None.
+
+ The possible keys of eventDict are:
+ - C{message}: by default, it holds the final text. It's required, but can
+ be empty if either C{isError} or C{format} is provided (the first
+ having the priority).
+ - C{isError}: boolean indicating the nature of the event.
+ - C{failure}: L{failure.Failure} instance, required if the event is an
+ error.
+ - C{why}: if defined, used as header of the traceback in case of errors.
+ - C{format}: string format used in place of C{message} to customize
+ the event. It uses all keys present in C{eventDict} to format
+ the text.
+ Other keys will be used when applying the C{format}, or ignored.
+ """
+ edm = eventDict['message']
+ if not edm:
+ if eventDict['isError'] and 'failure' in eventDict:
+ text = ((eventDict.get('why') or 'Unhandled Error')
+ + '\n' + eventDict['failure'].getTraceback())
+ elif 'format' in eventDict:
+ text = _safeFormat(eventDict['format'], eventDict)
+ else:
+ # we don't know how to log this
+ return
+ else:
+ text = ' '.join(map(reflect.safe_str, edm))
+ return text
+
+
+class FileLogObserver:
+ """
+ Log observer that writes to a file-like object.
+
+ @type timeFormat: C{str} or C{NoneType}
+ @ivar timeFormat: If not C{None}, the format string passed to strftime().
+ """
+ timeFormat = None
+
+ def __init__(self, f):
+ self.write = f.write
+ self.flush = f.flush
+
+ def getTimezoneOffset(self, when):
+ """
+ Return the current local timezone offset from UTC.
+
+ @type when: C{int}
+ @param when: POSIX (ie, UTC) timestamp for which to find the offset.
+
+ @rtype: C{int}
+ @return: The number of seconds offset from UTC. West is positive,
+ east is negative.
+ """
+ offset = datetime.utcfromtimestamp(when) - datetime.fromtimestamp(when)
+ return offset.days * (60 * 60 * 24) + offset.seconds
+
+ def formatTime(self, when):
+ """
+ Format the given UTC value as a string representing that time in the
+ local timezone.
+
+ By default it's formatted as a ISO8601-like string (ISO8601 date and
+ ISO8601 time separated by a space). It can be customized using the
+ C{timeFormat} attribute, which will be used as input for the underlying
+ C{time.strftime} call.
+
+ @type when: C{int}
+ @param when: POSIX (ie, UTC) timestamp for which to find the offset.
+
+ @rtype: C{str}
+ """
+ if self.timeFormat is not None:
+ return time.strftime(self.timeFormat, time.localtime(when))
+
+ tzOffset = -self.getTimezoneOffset(when)
+ when = datetime.utcfromtimestamp(when + tzOffset)
+ tzHour = abs(int(tzOffset / 60 / 60))
+ tzMin = abs(int(tzOffset / 60 % 60))
+ if tzOffset < 0:
+ tzSign = '-'
+ else:
+ tzSign = '+'
+ return '%d-%02d-%02d %02d:%02d:%02d%s%02d%02d' % (
+ when.year, when.month, when.day,
+ when.hour, when.minute, when.second,
+ tzSign, tzHour, tzMin)
+
+ def emit(self, eventDict):
+ text = textFromEventDict(eventDict)
+ if text is None:
+ return
+
+ timeStr = self.formatTime(eventDict['time'])
+ fmtDict = {'system': eventDict['system'], 'text': text.replace("\n", "\n\t")}
+ msgStr = _safeFormat("[%(system)s] %(text)s\n", fmtDict)
+
+ util.untilConcludes(self.write, timeStr + " " + msgStr)
+ util.untilConcludes(self.flush) # Hoorj!
+
+ def start(self):
+ """
+ Start observing log events.
+ """
+ addObserver(self.emit)
+
+ def stop(self):
+ """
+ Stop observing log events.
+ """
+ removeObserver(self.emit)
+
+
+class PythonLoggingObserver(object):
+ """
+ Output twisted messages to Python standard library L{logging} module.
+
+ WARNING: specific logging configurations (example: network) can lead to
+ a blocking system. Nothing is done here to prevent that, so be sure to not
+ use this: code within Twisted, such as twisted.web, assumes that logging
+ does not block.
+ """
+
+ def __init__(self, loggerName="twisted"):
+ """
+ @param loggerName: identifier used for getting logger.
+ @type loggerName: C{str}
+ """
+ self.logger = logging.getLogger(loggerName)
+
+ def emit(self, eventDict):
+ """
+ Receive a twisted log entry, format it and bridge it to python.
+
+ By default the logging level used is info; log.err produces error
+ level, and you can customize the level by using the C{logLevel} key::
+
+ >>> log.msg('debugging', logLevel=logging.DEBUG)
+
+ """
+ if 'logLevel' in eventDict:
+ level = eventDict['logLevel']
+ elif eventDict['isError']:
+ level = logging.ERROR
+ else:
+ level = logging.INFO
+ text = textFromEventDict(eventDict)
+ if text is None:
+ return
+ self.logger.log(level, text)
+
+ def start(self):
+ """
+ Start observing log events.
+ """
+ addObserver(self.emit)
+
+ def stop(self):
+ """
+ Stop observing log events.
+ """
+ removeObserver(self.emit)
+
+
+class StdioOnnaStick:
+ """
+ Class that pretends to be stdout/err, and turns writes into log messages.
+
+ @ivar isError: boolean indicating whether this is stderr, in which cases
+ log messages will be logged as errors.
+
+ @ivar encoding: unicode encoding used to encode any unicode strings
+ written to this object.
+ """
+
+ closed = 0
+ softspace = 0
+ mode = 'wb'
+ name = '<stdio (log)>'
+
+ def __init__(self, isError=0, encoding=None):
+ self.isError = isError
+ if encoding is None:
+ encoding = sys.getdefaultencoding()
+ self.encoding = encoding
+ self.buf = ''
+
+ def close(self):
+ pass
+
+ def fileno(self):
+ return -1
+
+ def flush(self):
+ pass
+
+ def read(self):
+ raise IOError("can't read from the log!")
+
+ readline = read
+ readlines = read
+ seek = read
+ tell = read
+
+ def write(self, data):
+ if isinstance(data, unicode):
+ data = data.encode(self.encoding)
+ d = (self.buf + data).split('\n')
+ self.buf = d[-1]
+ messages = d[0:-1]
+ for message in messages:
+ msg(message, printed=1, isError=self.isError)
+
+ def writelines(self, lines):
+ for line in lines:
+ if isinstance(line, unicode):
+ line = line.encode(self.encoding)
+ msg(line, printed=1, isError=self.isError)
+
+
+try:
+ _oldshowwarning
+except NameError:
+ _oldshowwarning = None
+
+
+def startLogging(file, *a, **kw):
+ """
+ Initialize logging to a specified file.
+
+ @return: A L{FileLogObserver} if a new observer is added, None otherwise.
+ """
+ if isinstance(file, StdioOnnaStick):
+ return
+ flo = FileLogObserver(file)
+ startLoggingWithObserver(flo.emit, *a, **kw)
+ return flo
+
+
+
+def startLoggingWithObserver(observer, setStdout=1):
+ """
+ Initialize logging to a specified observer. If setStdout is true
+ (defaults to yes), also redirect sys.stdout and sys.stderr
+ to the specified file.
+ """
+ global defaultObserver, _oldshowwarning
+ if not _oldshowwarning:
+ _oldshowwarning = warnings.showwarning
+ warnings.showwarning = showwarning
+ if defaultObserver:
+ defaultObserver.stop()
+ defaultObserver = None
+ addObserver(observer)
+ msg("Log opened.")
+ if setStdout:
+ sys.stdout = logfile
+ sys.stderr = logerr
+
+
+class NullFile:
+ softspace = 0
+ def read(self): pass
+ def write(self, bytes): pass
+ def flush(self): pass
+ def close(self): pass
+
+
+def discardLogs():
+ """
+ Throw away all logs.
+ """
+ global logfile
+ logfile = NullFile()
+
+
+# Prevent logfile from being erased on reload. This only works in cpython.
+try:
+ logfile
+except NameError:
+ logfile = StdioOnnaStick(0, getattr(sys.stdout, "encoding", None))
+ logerr = StdioOnnaStick(1, getattr(sys.stderr, "encoding", None))
+
+
+
+class DefaultObserver:
+ """
+ Default observer.
+
+ Will ignore all non-error messages and send error messages to sys.stderr.
+ Will be removed when startLogging() is called for the first time.
+ """
+ stderr = sys.stderr
+
+ def _emit(self, eventDict):
+ if eventDict["isError"]:
+ if 'failure' in eventDict:
+ text = ((eventDict.get('why') or 'Unhandled Error')
+ + '\n' + eventDict['failure'].getTraceback())
+ else:
+ text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+ self.stderr.write(text)
+ self.stderr.flush()
+
+ def start(self):
+ addObserver(self._emit)
+
+ def stop(self):
+ removeObserver(self._emit)
+
+
+# Some more sibling imports, at the bottom and unqualified to avoid
+# unresolvable circularity
+import threadable, failure
+threadable.synchronize(LogPublisher)
+
+
+try:
+ defaultObserver
+except NameError:
+ defaultObserver = DefaultObserver()
+ defaultObserver.start()
+
diff --git a/twisted/python/logfile.py b/twisted/python/logfile.py
new file mode 100644
index 0000000..f652271
--- /dev/null
+++ b/twisted/python/logfile.py
@@ -0,0 +1,323 @@
+# -*- test-case-name: twisted.test.test_logfile -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A rotating, browsable log file.
+"""
+
+# System Imports
+import os, glob, time, stat
+
+from twisted.python import threadable
+
+
+
+class BaseLogFile:
+ """
+ The base class for a log file that can be rotated.
+ """
+
+ synchronized = ["write", "rotate"]
+
+ def __init__(self, name, directory, defaultMode=None):
+ """
+ Create a log file.
+
+ @param name: name of the file
+ @param directory: directory holding the file
+ @param defaultMode: permissions used to create the file. Default to
+ current permissions of the file if the file exists.
+ """
+ self.directory = directory
+ self.name = name
+ self.path = os.path.join(directory, name)
+ if defaultMode is None and os.path.exists(self.path):
+ self.defaultMode = stat.S_IMODE(os.stat(self.path)[stat.ST_MODE])
+ else:
+ self.defaultMode = defaultMode
+ self._openFile()
+
+ def fromFullPath(cls, filename, *args, **kwargs):
+ """
+ Construct a log file from a full file path.
+ """
+ logPath = os.path.abspath(filename)
+ return cls(os.path.basename(logPath),
+ os.path.dirname(logPath), *args, **kwargs)
+ fromFullPath = classmethod(fromFullPath)
+
+ def shouldRotate(self):
+ """
+ Override with a method to that returns true if the log
+ should be rotated.
+ """
+ raise NotImplementedError
+
+ def _openFile(self):
+ """
+ Open the log file.
+ """
+ self.closed = False
+ if os.path.exists(self.path):
+ self._file = file(self.path, "r+", 1)
+ self._file.seek(0, 2)
+ else:
+ if self.defaultMode is not None:
+ # Set the lowest permissions
+ oldUmask = os.umask(0777)
+ try:
+ self._file = file(self.path, "w+", 1)
+ finally:
+ os.umask(oldUmask)
+ else:
+ self._file = file(self.path, "w+", 1)
+ if self.defaultMode is not None:
+ try:
+ os.chmod(self.path, self.defaultMode)
+ except OSError:
+ # Probably /dev/null or something?
+ pass
+
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ del state["_file"]
+ return state
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self._openFile()
+
+ def write(self, data):
+ """
+ Write some data to the file.
+ """
+ if self.shouldRotate():
+ self.flush()
+ self.rotate()
+ self._file.write(data)
+
+ def flush(self):
+ """
+ Flush the file.
+ """
+ self._file.flush()
+
+ def close(self):
+ """
+ Close the file.
+
+ The file cannot be used once it has been closed.
+ """
+ self.closed = True
+ self._file.close()
+ self._file = None
+
+
+ def reopen(self):
+ """
+ Reopen the log file. This is mainly useful if you use an external log
+ rotation tool, which moves under your feet.
+
+ Note that on Windows you probably need a specific API to rename the
+ file, as it's not supported to simply use os.rename, for example.
+ """
+ self.close()
+ self._openFile()
+
+
+ def getCurrentLog(self):
+ """
+ Return a LogReader for the current log file.
+ """
+ return LogReader(self.path)
+
+
+class LogFile(BaseLogFile):
+ """
+ A log file that can be rotated.
+
+ A rotateLength of None disables automatic log rotation.
+ """
+ def __init__(self, name, directory, rotateLength=1000000, defaultMode=None,
+ maxRotatedFiles=None):
+ """
+ Create a log file rotating on length.
+
+ @param name: file name.
+ @type name: C{str}
+ @param directory: path of the log file.
+ @type directory: C{str}
+ @param rotateLength: size of the log file where it rotates. Default to
+ 1M.
+ @type rotateLength: C{int}
+ @param defaultMode: mode used to create the file.
+ @type defaultMode: C{int}
+ @param maxRotatedFiles: if not None, max number of log files the class
+ creates. Warning: it removes all log files above this number.
+ @type maxRotatedFiles: C{int}
+ """
+ BaseLogFile.__init__(self, name, directory, defaultMode)
+ self.rotateLength = rotateLength
+ self.maxRotatedFiles = maxRotatedFiles
+
+ def _openFile(self):
+ BaseLogFile._openFile(self)
+ self.size = self._file.tell()
+
+ def shouldRotate(self):
+ """
+ Rotate when the log file size is larger than rotateLength.
+ """
+ return self.rotateLength and self.size >= self.rotateLength
+
+ def getLog(self, identifier):
+ """
+ Given an integer, return a LogReader for an old log file.
+ """
+ filename = "%s.%d" % (self.path, identifier)
+ if not os.path.exists(filename):
+ raise ValueError, "no such logfile exists"
+ return LogReader(filename)
+
+ def write(self, data):
+ """
+ Write some data to the file.
+ """
+ BaseLogFile.write(self, data)
+ self.size += len(data)
+
+ def rotate(self):
+ """
+ Rotate the file and create a new one.
+
+ If it's not possible to open new logfile, this will fail silently,
+ and continue logging to old logfile.
+ """
+ if not (os.access(self.directory, os.W_OK) and os.access(self.path, os.W_OK)):
+ return
+ logs = self.listLogs()
+ logs.reverse()
+ for i in logs:
+ if self.maxRotatedFiles is not None and i >= self.maxRotatedFiles:
+ os.remove("%s.%d" % (self.path, i))
+ else:
+ os.rename("%s.%d" % (self.path, i), "%s.%d" % (self.path, i + 1))
+ self._file.close()
+ os.rename(self.path, "%s.1" % self.path)
+ self._openFile()
+
+ def listLogs(self):
+ """
+ Return sorted list of integers - the old logs' identifiers.
+ """
+ result = []
+ for name in glob.glob("%s.*" % self.path):
+ try:
+ counter = int(name.split('.')[-1])
+ if counter:
+ result.append(counter)
+ except ValueError:
+ pass
+ result.sort()
+ return result
+
+ def __getstate__(self):
+ state = BaseLogFile.__getstate__(self)
+ del state["size"]
+ return state
+
+threadable.synchronize(LogFile)
+
+
+class DailyLogFile(BaseLogFile):
+ """A log file that is rotated daily (at or after midnight localtime)
+ """
+ def _openFile(self):
+ BaseLogFile._openFile(self)
+ self.lastDate = self.toDate(os.stat(self.path)[8])
+
+ def shouldRotate(self):
+ """Rotate when the date has changed since last write"""
+ return self.toDate() > self.lastDate
+
+ def toDate(self, *args):
+ """Convert a unixtime to (year, month, day) localtime tuple,
+ or return the current (year, month, day) localtime tuple.
+
+ This function primarily exists so you may overload it with
+ gmtime, or some cruft to make unit testing possible.
+ """
+ # primarily so this can be unit tested easily
+ return time.localtime(*args)[:3]
+
+ def suffix(self, tupledate):
+ """Return the suffix given a (year, month, day) tuple or unixtime"""
+ try:
+ return '_'.join(map(str, tupledate))
+ except:
+ # try taking a float unixtime
+ return '_'.join(map(str, self.toDate(tupledate)))
+
+ def getLog(self, identifier):
+ """Given a unix time, return a LogReader for an old log file."""
+ if self.toDate(identifier) == self.lastDate:
+ return self.getCurrentLog()
+ filename = "%s.%s" % (self.path, self.suffix(identifier))
+ if not os.path.exists(filename):
+ raise ValueError, "no such logfile exists"
+ return LogReader(filename)
+
+ def write(self, data):
+ """Write some data to the log file"""
+ BaseLogFile.write(self, data)
+ # Guard against a corner case where time.time()
+ # could potentially run backwards to yesterday.
+ # Primarily due to network time.
+ self.lastDate = max(self.lastDate, self.toDate())
+
+ def rotate(self):
+ """Rotate the file and create a new one.
+
+ If it's not possible to open new logfile, this will fail silently,
+ and continue logging to old logfile.
+ """
+ if not (os.access(self.directory, os.W_OK) and os.access(self.path, os.W_OK)):
+ return
+ newpath = "%s.%s" % (self.path, self.suffix(self.lastDate))
+ if os.path.exists(newpath):
+ return
+ self._file.close()
+ os.rename(self.path, newpath)
+ self._openFile()
+
+ def __getstate__(self):
+ state = BaseLogFile.__getstate__(self)
+ del state["lastDate"]
+ return state
+
+threadable.synchronize(DailyLogFile)
+
+
+class LogReader:
+ """Read from a log file."""
+
+ def __init__(self, name):
+ self._file = file(name, "r")
+
+ def readLines(self, lines=10):
+ """Read a list of lines from the log file.
+
+ This doesn't returns all of the files lines - call it multiple times.
+ """
+ result = []
+ for i in range(lines):
+ line = self._file.readline()
+ if not line:
+ break
+ result.append(line)
+ return result
+
+ def close(self):
+ self._file.close()
diff --git a/twisted/python/modules.py b/twisted/python/modules.py
new file mode 100644
index 0000000..307970c
--- /dev/null
+++ b/twisted/python/modules.py
@@ -0,0 +1,758 @@
+# -*- test-case-name: twisted.test.test_modules -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module aims to provide a unified, object-oriented view of Python's
+runtime hierarchy.
+
+Python is a very dynamic language with wide variety of introspection utilities.
+However, these utilities can be hard to use, because there is no consistent
+API. The introspection API in python is made up of attributes (__name__,
+__module__, func_name, etc) on instances, modules, classes and functions which
+vary between those four types, utility modules such as 'inspect' which provide
+some functionality, the 'imp' module, the "compiler" module, the semantics of
+PEP 302 support, and setuptools, among other things.
+
+At the top, you have "PythonPath", an abstract representation of sys.path which
+includes methods to locate top-level modules, with or without loading them.
+The top-level exposed functions in this module for accessing the system path
+are "walkModules", "iterModules", and "getModule".
+
+From most to least specific, here are the objects provided::
+
+ PythonPath # sys.path
+ |
+ v
+ PathEntry # one entry on sys.path: an importer
+ |
+ v
+ PythonModule # a module or package that can be loaded
+ |
+ v
+ PythonAttribute # an attribute of a module (function or class)
+ |
+ v
+ PythonAttribute # an attribute of a function or class
+ |
+ v
+ ...
+
+Here's an example of idiomatic usage: this is what you would do to list all of
+the modules outside the standard library's python-files directory::
+
+ import os
+ stdlibdir = os.path.dirname(os.__file__)
+
+ from twisted.python.modules import iterModules
+
+ for modinfo in iterModules():
+ if (modinfo.pathEntry.filePath.path != stdlibdir
+ and not modinfo.isPackage()):
+ print 'unpackaged: %s: %s' % (
+ modinfo.name, modinfo.filePath.path)
+"""
+
+__metaclass__ = type
+
+# let's try to keep path imports to a minimum...
+from os.path import dirname, split as splitpath
+
+import sys
+import zipimport
+import inspect
+import warnings
+from zope.interface import Interface, implements
+
+from twisted.python.components import registerAdapter
+from twisted.python.filepath import FilePath, UnlistableError
+from twisted.python.zippath import ZipArchive
+from twisted.python.reflect import namedAny
+
+_nothing = object()
+
+PYTHON_EXTENSIONS = ['.py']
+OPTIMIZED_MODE = __doc__ is None
+if OPTIMIZED_MODE:
+ PYTHON_EXTENSIONS.append('.pyo')
+else:
+ PYTHON_EXTENSIONS.append('.pyc')
+
+def _isPythonIdentifier(string):
+ """
+ cheezy fake test for proper identifier-ness.
+
+ @param string: a str which might or might not be a valid python identifier.
+
+ @return: True or False
+ """
+ return (' ' not in string and
+ '.' not in string and
+ '-' not in string)
+
+
+
+def _isPackagePath(fpath):
+ # Determine if a FilePath-like object is a Python package. TODO: deal with
+ # __init__module.(so|dll|pyd)?
+ extless = fpath.splitext()[0]
+ basend = splitpath(extless)[1]
+ return basend == "__init__"
+
+
+
+class _ModuleIteratorHelper:
+ """
+ This mixin provides common behavior between python module and path entries,
+ since the mechanism for searching sys.path and __path__ attributes is
+ remarkably similar.
+ """
+
+ def iterModules(self):
+ """
+ Loop over the modules present below this entry or package on PYTHONPATH.
+
+ For modules which are not packages, this will yield nothing.
+
+ For packages and path entries, this will only yield modules one level
+ down; i.e. if there is a package a.b.c, iterModules on a will only
+ return a.b. If you want to descend deeply, use walkModules.
+
+ @return: a generator which yields PythonModule instances that describe
+ modules which can be, or have been, imported.
+ """
+ yielded = {}
+ if not self.filePath.exists():
+ return
+
+ for placeToLook in self._packagePaths():
+ try:
+ children = placeToLook.children()
+ except UnlistableError:
+ continue
+
+ children.sort()
+ for potentialTopLevel in children:
+ ext = potentialTopLevel.splitext()[1]
+ potentialBasename = potentialTopLevel.basename()[:-len(ext)]
+ if ext in PYTHON_EXTENSIONS:
+ # TODO: this should be a little choosier about which path entry
+ # it selects first, and it should do all the .so checking and
+ # crud
+ if not _isPythonIdentifier(potentialBasename):
+ continue
+ modname = self._subModuleName(potentialBasename)
+ if modname.split(".")[-1] == '__init__':
+ # This marks the directory as a package so it can't be
+ # a module.
+ continue
+ if modname not in yielded:
+ yielded[modname] = True
+ pm = PythonModule(modname, potentialTopLevel, self._getEntry())
+ assert pm != self
+ yield pm
+ else:
+ if (ext or not _isPythonIdentifier(potentialBasename)
+ or not potentialTopLevel.isdir()):
+ continue
+ modname = self._subModuleName(potentialTopLevel.basename())
+ for ext in PYTHON_EXTENSIONS:
+ initpy = potentialTopLevel.child("__init__"+ext)
+ if initpy.exists() and modname not in yielded:
+ yielded[modname] = True
+ pm = PythonModule(modname, initpy, self._getEntry())
+ assert pm != self
+ yield pm
+ break
+
+ def walkModules(self, importPackages=False):
+ """
+ Similar to L{iterModules}, this yields self, and then every module in my
+ package or entry, and every submodule in each package or entry.
+
+ In other words, this is deep, and L{iterModules} is shallow.
+ """
+ yield self
+ for package in self.iterModules():
+ for module in package.walkModules(importPackages=importPackages):
+ yield module
+
+ def _subModuleName(self, mn):
+ """
+ This is a hook to provide packages with the ability to specify their names
+ as a prefix to submodules here.
+ """
+ return mn
+
+ def _packagePaths(self):
+ """
+ Implement in subclasses to specify where to look for modules.
+
+ @return: iterable of FilePath-like objects.
+ """
+ raise NotImplementedError()
+
+ def _getEntry(self):
+ """
+ Implement in subclasses to specify what path entry submodules will come
+ from.
+
+ @return: a PathEntry instance.
+ """
+ raise NotImplementedError()
+
+
+ def __getitem__(self, modname):
+ """
+ Retrieve a module from below this path or package.
+
+ @param modname: a str naming a module to be loaded. For entries, this
+ is a top-level, undotted package name, and for packages it is the name
+ of the module without the package prefix. For example, if you have a
+ PythonModule representing the 'twisted' package, you could use::
+
+ twistedPackageObj['python']['modules']
+
+ to retrieve this module.
+
+ @raise: KeyError if the module is not found.
+
+ @return: a PythonModule.
+ """
+ for module in self.iterModules():
+ if module.name == self._subModuleName(modname):
+ return module
+ raise KeyError(modname)
+
+ def __iter__(self):
+ """
+ Implemented to raise NotImplementedError for clarity, so that attempting to
+ loop over this object won't call __getitem__.
+
+ Note: in the future there might be some sensible default for iteration,
+ like 'walkEverything', so this is deliberately untested and undefined
+ behavior.
+ """
+ raise NotImplementedError()
+
+class PythonAttribute:
+ """
+ I represent a function, class, or other object that is present.
+
+ @ivar name: the fully-qualified python name of this attribute.
+
+ @ivar onObject: a reference to a PythonModule or other PythonAttribute that
+ is this attribute's logical parent.
+
+ @ivar name: the fully qualified python name of the attribute represented by
+ this class.
+ """
+ def __init__(self, name, onObject, loaded, pythonValue):
+ """
+ Create a PythonAttribute. This is a private constructor. Do not construct
+ me directly, use PythonModule.iterAttributes.
+
+ @param name: the FQPN
+ @param onObject: see ivar
+ @param loaded: always True, for now
+ @param pythonValue: the value of the attribute we're pointing to.
+ """
+ self.name = name
+ self.onObject = onObject
+ self._loaded = loaded
+ self.pythonValue = pythonValue
+
+ def __repr__(self):
+ return 'PythonAttribute<%r>'%(self.name,)
+
+ def isLoaded(self):
+ """
+ Return a boolean describing whether the attribute this describes has
+ actually been loaded into memory by importing its module.
+
+ Note: this currently always returns true; there is no Python parser
+ support in this module yet.
+ """
+ return self._loaded
+
+ def load(self, default=_nothing):
+ """
+ Load the value associated with this attribute.
+
+ @return: an arbitrary Python object, or 'default' if there is an error
+ loading it.
+ """
+ return self.pythonValue
+
+ def iterAttributes(self):
+ for name, val in inspect.getmembers(self.load()):
+ yield PythonAttribute(self.name+'.'+name, self, True, val)
+
+class PythonModule(_ModuleIteratorHelper):
+ """
+ Representation of a module which could be imported from sys.path.
+
+ @ivar name: the fully qualified python name of this module.
+
+ @ivar filePath: a FilePath-like object which points to the location of this
+ module.
+
+ @ivar pathEntry: a L{PathEntry} instance which this module was located
+ from.
+ """
+
+ def __init__(self, name, filePath, pathEntry):
+ """
+ Create a PythonModule. Do not construct this directly, instead inspect a
+ PythonPath or other PythonModule instances.
+
+ @param name: see ivar
+ @param filePath: see ivar
+ @param pathEntry: see ivar
+ """
+ assert not name.endswith(".__init__")
+ self.name = name
+ self.filePath = filePath
+ self.parentPath = filePath.parent()
+ self.pathEntry = pathEntry
+
+ def _getEntry(self):
+ return self.pathEntry
+
+ def __repr__(self):
+ """
+ Return a string representation including the module name.
+ """
+ return 'PythonModule<%r>' % (self.name,)
+
+
+ def isLoaded(self):
+ """
+ Determine if the module is loaded into sys.modules.
+
+ @return: a boolean: true if loaded, false if not.
+ """
+ return self.pathEntry.pythonPath.moduleDict.get(self.name) is not None
+
+
+ def iterAttributes(self):
+ """
+ List all the attributes defined in this module.
+
+ Note: Future work is planned here to make it possible to list python
+ attributes on a module without loading the module by inspecting ASTs or
+ bytecode, but currently any iteration of PythonModule objects insists
+ they must be loaded, and will use inspect.getmodule.
+
+ @raise NotImplementedError: if this module is not loaded.
+
+ @return: a generator yielding PythonAttribute instances describing the
+ attributes of this module.
+ """
+ if not self.isLoaded():
+ raise NotImplementedError(
+ "You can't load attributes from non-loaded modules yet.")
+ for name, val in inspect.getmembers(self.load()):
+ yield PythonAttribute(self.name+'.'+name, self, True, val)
+
+ def isPackage(self):
+ """
+ Returns true if this module is also a package, and might yield something
+ from iterModules.
+ """
+ return _isPackagePath(self.filePath)
+
+ def load(self, default=_nothing):
+ """
+ Load this module.
+
+ @param default: if specified, the value to return in case of an error.
+
+ @return: a genuine python module.
+
+ @raise: any type of exception. Importing modules is a risky business;
+ the erorrs of any code run at module scope may be raised from here, as
+ well as ImportError if something bizarre happened to the system path
+ between the discovery of this PythonModule object and the attempt to
+ import it. If you specify a default, the error will be swallowed
+ entirely, and not logged.
+
+ @rtype: types.ModuleType.
+ """
+ try:
+ return self.pathEntry.pythonPath.moduleLoader(self.name)
+ except: # this needs more thought...
+ if default is not _nothing:
+ return default
+ raise
+
+ def __eq__(self, other):
+ """
+ PythonModules with the same name are equal.
+ """
+ if not isinstance(other, PythonModule):
+ return False
+ return other.name == self.name
+
+ def __ne__(self, other):
+ """
+ PythonModules with different names are not equal.
+ """
+ if not isinstance(other, PythonModule):
+ return True
+ return other.name != self.name
+
+ def walkModules(self, importPackages=False):
+ if importPackages and self.isPackage():
+ self.load()
+ return super(PythonModule, self).walkModules(importPackages=importPackages)
+
+ def _subModuleName(self, mn):
+ """
+ submodules of this module are prefixed with our name.
+ """
+ return self.name + '.' + mn
+
+ def _packagePaths(self):
+ """
+ Yield a sequence of FilePath-like objects which represent path segments.
+ """
+ if not self.isPackage():
+ return
+ if self.isLoaded():
+ load = self.load()
+ if hasattr(load, '__path__'):
+ for fn in load.__path__:
+ if fn == self.parentPath.path:
+ # this should _really_ exist.
+ assert self.parentPath.exists()
+ yield self.parentPath
+ else:
+ smp = self.pathEntry.pythonPath._smartPath(fn)
+ if smp.exists():
+ yield smp
+ else:
+ yield self.parentPath
+
+
+class PathEntry(_ModuleIteratorHelper):
+ """
+ I am a proxy for a single entry on sys.path.
+
+ @ivar filePath: a FilePath-like object pointing at the filesystem location
+ or archive file where this path entry is stored.
+
+ @ivar pythonPath: a PythonPath instance.
+ """
+ def __init__(self, filePath, pythonPath):
+ """
+ Create a PathEntry. This is a private constructor.
+ """
+ self.filePath = filePath
+ self.pythonPath = pythonPath
+
+ def _getEntry(self):
+ return self
+
+ def __repr__(self):
+ return 'PathEntry<%r>' % (self.filePath,)
+
+ def _packagePaths(self):
+ yield self.filePath
+
+class IPathImportMapper(Interface):
+ """
+ This is an internal interface, used to map importers to factories for
+ FilePath-like objects.
+ """
+ def mapPath(self, pathLikeString):
+ """
+ Return a FilePath-like object.
+
+ @param pathLikeString: a path-like string, like one that might be
+ passed to an import hook.
+
+ @return: a L{FilePath}, or something like it (currently only a
+ L{ZipPath}, but more might be added later).
+ """
+
+class _DefaultMapImpl:
+ """ Wrapper for the default importer, i.e. None. """
+ implements(IPathImportMapper)
+ def mapPath(self, fsPathString):
+ return FilePath(fsPathString)
+_theDefaultMapper = _DefaultMapImpl()
+
+class _ZipMapImpl:
+ """ IPathImportMapper implementation for zipimport.ZipImporter. """
+ implements(IPathImportMapper)
+ def __init__(self, importer):
+ self.importer = importer
+
+ def mapPath(self, fsPathString):
+ """
+ Map the given FS path to a ZipPath, by looking at the ZipImporter's
+ "archive" attribute and using it as our ZipArchive root, then walking
+ down into the archive from there.
+
+ @return: a L{zippath.ZipPath} or L{zippath.ZipArchive} instance.
+ """
+ za = ZipArchive(self.importer.archive)
+ myPath = FilePath(self.importer.archive)
+ itsPath = FilePath(fsPathString)
+ if myPath == itsPath:
+ return za
+ # This is NOT a general-purpose rule for sys.path or __file__:
+ # zipimport specifically uses regular OS path syntax in its pathnames,
+ # even though zip files specify that slashes are always the separator,
+ # regardless of platform.
+ segs = itsPath.segmentsFrom(myPath)
+ zp = za
+ for seg in segs:
+ zp = zp.child(seg)
+ return zp
+
+registerAdapter(_ZipMapImpl, zipimport.zipimporter, IPathImportMapper)
+
+def _defaultSysPathFactory():
+ """
+ Provide the default behavior of PythonPath's sys.path factory, which is to
+ return the current value of sys.path.
+
+ @return: L{sys.path}
+ """
+ return sys.path
+
+
+class PythonPath:
+ """
+ I represent the very top of the Python object-space, the module list in
+ sys.path and the modules list in sys.modules.
+
+ @ivar _sysPath: a sequence of strings like sys.path. This attribute is
+ read-only.
+
+ @ivar moduleDict: a dictionary mapping string module names to module
+ objects, like sys.modules.
+
+ @ivar sysPathHooks: a list of PEP-302 path hooks, like sys.path_hooks.
+
+ @ivar moduleLoader: a function that takes a fully-qualified python name and
+ returns a module, like twisted.python.reflect.namedAny.
+ """
+
+ def __init__(self,
+ sysPath=None,
+ moduleDict=sys.modules,
+ sysPathHooks=sys.path_hooks,
+ importerCache=sys.path_importer_cache,
+ moduleLoader=namedAny,
+ sysPathFactory=None):
+ """
+ Create a PythonPath. You almost certainly want to use
+ modules.theSystemPath, or its aliased methods, rather than creating a
+ new instance yourself, though.
+
+ All parameters are optional, and if unspecified, will use 'system'
+ equivalents that makes this PythonPath like the global L{theSystemPath}
+ instance.
+
+ @param sysPath: a sys.path-like list to use for this PythonPath, to
+ specify where to load modules from.
+
+ @param moduleDict: a sys.modules-like dictionary to use for keeping
+ track of what modules this PythonPath has loaded.
+
+ @param sysPathHooks: sys.path_hooks-like list of PEP-302 path hooks to
+ be used for this PythonPath, to determie which importers should be
+ used.
+
+ @param importerCache: a sys.path_importer_cache-like list of PEP-302
+ importers. This will be used in conjunction with the given
+ sysPathHooks.
+
+ @param moduleLoader: a module loader function which takes a string and
+ returns a module. That is to say, it is like L{namedAny} - *not* like
+ L{__import__}.
+
+ @param sysPathFactory: a 0-argument callable which returns the current
+ value of a sys.path-like list of strings. Specify either this, or
+ sysPath, not both. This alternative interface is provided because the
+ way the Python import mechanism works, you can re-bind the 'sys.path'
+ name and that is what is used for current imports, so it must be a
+ factory rather than a value to deal with modification by rebinding
+ rather than modification by mutation. Note: it is not recommended to
+ rebind sys.path. Although this mechanism can deal with that, it is a
+ subtle point which some tools that it is easy for tools which interact
+ with sys.path to miss.
+ """
+ if sysPath is not None:
+ sysPathFactory = lambda : sysPath
+ elif sysPathFactory is None:
+ sysPathFactory = _defaultSysPathFactory
+ self._sysPathFactory = sysPathFactory
+ self._sysPath = sysPath
+ self.moduleDict = moduleDict
+ self.sysPathHooks = sysPathHooks
+ self.importerCache = importerCache
+ self.moduleLoader = moduleLoader
+
+
+ def _getSysPath(self):
+ """
+ Retrieve the current value of the module search path list.
+ """
+ return self._sysPathFactory()
+
+ sysPath = property(_getSysPath)
+
+ def _findEntryPathString(self, modobj):
+ """
+ Determine where a given Python module object came from by looking at path
+ entries.
+ """
+ topPackageObj = modobj
+ while '.' in topPackageObj.__name__:
+ topPackageObj = self.moduleDict['.'.join(
+ topPackageObj.__name__.split('.')[:-1])]
+ if _isPackagePath(FilePath(topPackageObj.__file__)):
+ # if package 'foo' is on sys.path at /a/b/foo, package 'foo's
+ # __file__ will be /a/b/foo/__init__.py, and we are looking for
+ # /a/b here, the path-entry; so go up two steps.
+ rval = dirname(dirname(topPackageObj.__file__))
+ else:
+ # the module is completely top-level, not within any packages. The
+ # path entry it's on is just its dirname.
+ rval = dirname(topPackageObj.__file__)
+
+ # There are probably some awful tricks that an importer could pull
+ # which would break this, so let's just make sure... it's a loaded
+ # module after all, which means that its path MUST be in
+ # path_importer_cache according to PEP 302 -glyph
+ if rval not in self.importerCache:
+ warnings.warn(
+ "%s (for module %s) not in path importer cache "
+ "(PEP 302 violation - check your local configuration)." % (
+ rval, modobj.__name__),
+ stacklevel=3)
+
+ return rval
+
+ def _smartPath(self, pathName):
+ """
+ Given a path entry from sys.path which may refer to an importer,
+ return the appropriate FilePath-like instance.
+
+ @param pathName: a str describing the path.
+
+ @return: a FilePath-like object.
+ """
+ importr = self.importerCache.get(pathName, _nothing)
+ if importr is _nothing:
+ for hook in self.sysPathHooks:
+ try:
+ importr = hook(pathName)
+ except ImportError:
+ pass
+ if importr is _nothing: # still
+ importr = None
+ return IPathImportMapper(importr, _theDefaultMapper).mapPath(pathName)
+
+ def iterEntries(self):
+ """
+ Iterate the entries on my sysPath.
+
+ @return: a generator yielding PathEntry objects
+ """
+ for pathName in self.sysPath:
+ fp = self._smartPath(pathName)
+ yield PathEntry(fp, self)
+
+
+ def __getitem__(self, modname):
+ """
+ Get a python module by its given fully-qualified name.
+
+ @param modname: The fully-qualified Python module name to load.
+
+ @type modname: C{str}
+
+ @return: an object representing the module identified by C{modname}
+
+ @rtype: L{PythonModule}
+
+ @raise KeyError: if the module name is not a valid module name, or no
+ such module can be identified as loadable.
+ """
+ # See if the module is already somewhere in Python-land.
+ moduleObject = self.moduleDict.get(modname)
+ if moduleObject is not None:
+ # we need 2 paths; one of the path entry and one for the module.
+ pe = PathEntry(
+ self._smartPath(
+ self._findEntryPathString(moduleObject)),
+ self)
+ mp = self._smartPath(moduleObject.__file__)
+ return PythonModule(modname, mp, pe)
+
+ # Recurse if we're trying to get a submodule.
+ if '.' in modname:
+ pkg = self
+ for name in modname.split('.'):
+ pkg = pkg[name]
+ return pkg
+
+ # Finally do the slowest possible thing and iterate
+ for module in self.iterModules():
+ if module.name == modname:
+ return module
+ raise KeyError(modname)
+
+
+ def __repr__(self):
+ """
+ Display my sysPath and moduleDict in a string representation.
+ """
+ return "PythonPath(%r,%r)" % (self.sysPath, self.moduleDict)
+
+ def iterModules(self):
+ """
+ Yield all top-level modules on my sysPath.
+ """
+ for entry in self.iterEntries():
+ for module in entry.iterModules():
+ yield module
+
+ def walkModules(self, importPackages=False):
+ """
+ Similar to L{iterModules}, this yields every module on the path, then every
+ submodule in each package or entry.
+ """
+ for package in self.iterModules():
+ for module in package.walkModules(importPackages=False):
+ yield module
+
+theSystemPath = PythonPath()
+
+def walkModules(importPackages=False):
+ """
+ Deeply iterate all modules on the global python path.
+
+ @param importPackages: Import packages as they are seen.
+ """
+ return theSystemPath.walkModules(importPackages=importPackages)
+
+def iterModules():
+ """
+ Iterate all modules and top-level packages on the global Python path, but
+ do not descend into packages.
+
+ @param importPackages: Import packages as they are seen.
+ """
+ return theSystemPath.iterModules()
+
+def getModule(moduleName):
+ """
+ Retrieve a module from the system path.
+ """
+ return theSystemPath[moduleName]
diff --git a/twisted/python/monkey.py b/twisted/python/monkey.py
new file mode 100644
index 0000000..9be285d
--- /dev/null
+++ b/twisted/python/monkey.py
@@ -0,0 +1,73 @@
+# -*- test-case-name: twisted.test.test_monkey -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+class MonkeyPatcher(object):
+ """
+ Cover up attributes with new objects. Neat for monkey-patching things for
+ unit-testing purposes.
+ """
+
+ def __init__(self, *patches):
+ # List of patches to apply in (obj, name, value).
+ self._patchesToApply = []
+ # List of the original values for things that have been patched.
+ # (obj, name, value) format.
+ self._originals = []
+ for patch in patches:
+ self.addPatch(*patch)
+
+
+ def addPatch(self, obj, name, value):
+ """
+ Add a patch so that the attribute C{name} on C{obj} will be assigned to
+ C{value} when C{patch} is called or during C{runWithPatches}.
+
+ You can restore the original values with a call to restore().
+ """
+ self._patchesToApply.append((obj, name, value))
+
+
+ def _alreadyPatched(self, obj, name):
+ """
+ Has the C{name} attribute of C{obj} already been patched by this
+ patcher?
+ """
+ for o, n, v in self._originals:
+ if (o, n) == (obj, name):
+ return True
+ return False
+
+
+ def patch(self):
+ """
+ Apply all of the patches that have been specified with L{addPatch}.
+ Reverse this operation using L{restore}.
+ """
+ for obj, name, value in self._patchesToApply:
+ if not self._alreadyPatched(obj, name):
+ self._originals.append((obj, name, getattr(obj, name)))
+ setattr(obj, name, value)
+
+
+ def restore(self):
+ """
+ Restore all original values to any patched objects.
+ """
+ while self._originals:
+ obj, name, value = self._originals.pop()
+ setattr(obj, name, value)
+
+
+ def runWithPatches(self, f, *args, **kw):
+ """
+ Apply each patch already specified. Then run the function f with the
+ given args and kwargs. Restore everything when done.
+ """
+ self.patch()
+ try:
+ return f(*args, **kw)
+ finally:
+ self.restore()
diff --git a/twisted/python/procutils.py b/twisted/python/procutils.py
new file mode 100644
index 0000000..26ff95d
--- /dev/null
+++ b/twisted/python/procutils.py
@@ -0,0 +1,45 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Utilities for dealing with processes.
+"""
+
+import os
+
+def which(name, flags=os.X_OK):
+ """Search PATH for executable files with the given name.
+
+ On newer versions of MS-Windows, the PATHEXT environment variable will be
+ set to the list of file extensions for files considered executable. This
+ will normally include things like ".EXE". This fuction will also find files
+ with the given name ending with any of these extensions.
+
+ On MS-Windows the only flag that has any meaning is os.F_OK. Any other
+ flags will be ignored.
+
+ @type name: C{str}
+ @param name: The name for which to search.
+
+ @type flags: C{int}
+ @param flags: Arguments to L{os.access}.
+
+ @rtype: C{list}
+ @param: A list of the full paths to files found, in the
+ order in which they were found.
+ """
+ result = []
+ exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep))
+ path = os.environ.get('PATH', None)
+ if path is None:
+ return []
+ for p in os.environ.get('PATH', '').split(os.pathsep):
+ p = os.path.join(p, name)
+ if os.access(p, flags):
+ result.append(p)
+ for e in exts:
+ pext = p + e
+ if os.access(pext, flags):
+ result.append(pext)
+ return result
+
diff --git a/twisted/python/randbytes.py b/twisted/python/randbytes.py
new file mode 100644
index 0000000..63ae2dc
--- /dev/null
+++ b/twisted/python/randbytes.py
@@ -0,0 +1,131 @@
+# -*- test-case-name: twisted.test.test_randbytes -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Cryptographically secure random implementation, with fallback on normal random.
+"""
+
+# System imports
+import warnings, os, random
+
+getrandbits = getattr(random, 'getrandbits', None)
+
+
+class SecureRandomNotAvailable(RuntimeError):
+ """
+ Exception raised when no secure random algorithm is found.
+ """
+
+
+
+class SourceNotAvailable(RuntimeError):
+ """
+ Internal exception used when a specific random source is not available.
+ """
+
+
+
+class RandomFactory(object):
+ """
+ Factory providing L{secureRandom} and L{insecureRandom} methods.
+
+ You shouldn't have to instantiate this class, use the module level
+ functions instead: it is an implementation detail and could be removed or
+ changed arbitrarily.
+ """
+
+ # This variable is no longer used, and will eventually be removed.
+ randomSources = ()
+
+ getrandbits = getrandbits
+
+
+ def _osUrandom(self, nbytes):
+ """
+ Wrapper around C{os.urandom} that cleanly manage its absence.
+ """
+ try:
+ return os.urandom(nbytes)
+ except (AttributeError, NotImplementedError), e:
+ raise SourceNotAvailable(e)
+
+
+ def secureRandom(self, nbytes, fallback=False):
+ """
+ Return a number of secure random bytes.
+
+ @param nbytes: number of bytes to generate.
+ @type nbytes: C{int}
+ @param fallback: Whether the function should fallback on non-secure
+ random or not. Default to C{False}.
+ @type fallback: C{bool}
+
+ @return: a string of random bytes.
+ @rtype: C{str}
+ """
+ try:
+ return self._osUrandom(nbytes)
+ except SourceNotAvailable:
+ pass
+
+ if fallback:
+ warnings.warn(
+ "urandom unavailable - "
+ "proceeding with non-cryptographically secure random source",
+ category=RuntimeWarning,
+ stacklevel=2)
+ return self.insecureRandom(nbytes)
+ else:
+ raise SecureRandomNotAvailable("No secure random source available")
+
+
+ def _randBits(self, nbytes):
+ """
+ Wrapper around C{os.getrandbits}.
+ """
+ if self.getrandbits is not None:
+ n = self.getrandbits(nbytes * 8)
+ hexBytes = ("%%0%dx" % (nbytes * 2)) % n
+ return hexBytes.decode('hex')
+ raise SourceNotAvailable("random.getrandbits is not available")
+
+
+ def _randRange(self, nbytes):
+ """
+ Wrapper around C{random.randrange}.
+ """
+ bytes = ""
+ for i in xrange(nbytes):
+ bytes += chr(random.randrange(0, 255))
+ return bytes
+
+
+ def insecureRandom(self, nbytes):
+ """
+ Return a number of non secure random bytes.
+
+ @param nbytes: number of bytes to generate.
+ @type nbytes: C{int}
+
+ @return: a string of random bytes.
+ @rtype: C{str}
+ """
+ for src in ("_randBits", "_randRange"):
+ try:
+ return getattr(self, src)(nbytes)
+ except SourceNotAvailable:
+ pass
+
+
+
+factory = RandomFactory()
+
+secureRandom = factory.secureRandom
+
+insecureRandom = factory.insecureRandom
+
+del factory
+
+
+__all__ = ["secureRandom", "insecureRandom", "SecureRandomNotAvailable"]
diff --git a/twisted/python/rebuild.py b/twisted/python/rebuild.py
new file mode 100644
index 0000000..10a0b67
--- /dev/null
+++ b/twisted/python/rebuild.py
@@ -0,0 +1,271 @@
+# -*- test-case-name: twisted.test.test_rebuild -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+*Real* reloading support for Python.
+"""
+
+# System Imports
+import sys
+import types
+import time
+import linecache
+
+# Sibling Imports
+from twisted.python import log, reflect
+
+lastRebuild = time.time()
+
+
+class Sensitive:
+ """
+ A utility mixin that's sensitive to rebuilds.
+
+ This is a mixin for classes (usually those which represent collections of
+ callbacks) to make sure that their code is up-to-date before running.
+ """
+
+ lastRebuild = lastRebuild
+
+ def needRebuildUpdate(self):
+ yn = (self.lastRebuild < lastRebuild)
+ return yn
+
+ def rebuildUpToDate(self):
+ self.lastRebuild = time.time()
+
+ def latestVersionOf(self, anObject):
+ """
+ Get the latest version of an object.
+
+ This can handle just about anything callable; instances, functions,
+ methods, and classes.
+ """
+ t = type(anObject)
+ if t == types.FunctionType:
+ return latestFunction(anObject)
+ elif t == types.MethodType:
+ if anObject.im_self is None:
+ return getattr(anObject.im_class, anObject.__name__)
+ else:
+ return getattr(anObject.im_self, anObject.__name__)
+ elif t == types.InstanceType:
+ # Kick it, if it's out of date.
+ getattr(anObject, 'nothing', None)
+ return anObject
+ elif t == types.ClassType:
+ return latestClass(anObject)
+ else:
+ log.msg('warning returning anObject!')
+ return anObject
+
+_modDictIDMap = {}
+
+def latestFunction(oldFunc):
+ """
+ Get the latest version of a function.
+ """
+ # This may be CPython specific, since I believe jython instantiates a new
+ # module upon reload.
+ dictID = id(oldFunc.func_globals)
+ module = _modDictIDMap.get(dictID)
+ if module is None:
+ return oldFunc
+ return getattr(module, oldFunc.__name__)
+
+
+def latestClass(oldClass):
+ """
+ Get the latest version of a class.
+ """
+ module = reflect.namedModule(oldClass.__module__)
+ newClass = getattr(module, oldClass.__name__)
+ newBases = [latestClass(base) for base in newClass.__bases__]
+
+ try:
+ # This makes old-style stuff work
+ newClass.__bases__ = tuple(newBases)
+ return newClass
+ except TypeError:
+ if newClass.__module__ == "__builtin__":
+ # __builtin__ members can't be reloaded sanely
+ return newClass
+ ctor = getattr(newClass, '__metaclass__', type)
+ return ctor(newClass.__name__, tuple(newBases), dict(newClass.__dict__))
+
+
+class RebuildError(Exception):
+ """
+ Exception raised when trying to rebuild a class whereas it's not possible.
+ """
+
+
+def updateInstance(self):
+ """
+ Updates an instance to be current.
+ """
+ try:
+ self.__class__ = latestClass(self.__class__)
+ except TypeError:
+ if hasattr(self.__class__, '__slots__'):
+ raise RebuildError("Can't rebuild class with __slots__ on Python < 2.6")
+ else:
+ raise
+
+
+def __getattr__(self, name):
+ """
+ A getattr method to cause a class to be refreshed.
+ """
+ if name == '__del__':
+ raise AttributeError("Without this, Python segfaults.")
+ updateInstance(self)
+ log.msg("(rebuilding stale %s instance (%s))" % (reflect.qual(self.__class__), name))
+ result = getattr(self, name)
+ return result
+
+
+def rebuild(module, doLog=1):
+ """
+ Reload a module and do as much as possible to replace its references.
+ """
+ global lastRebuild
+ lastRebuild = time.time()
+ if hasattr(module, 'ALLOW_TWISTED_REBUILD'):
+ # Is this module allowed to be rebuilt?
+ if not module.ALLOW_TWISTED_REBUILD:
+ raise RuntimeError("I am not allowed to be rebuilt.")
+ if doLog:
+ log.msg('Rebuilding %s...' % str(module.__name__))
+
+ ## Safely handle adapter re-registration
+ from twisted.python import components
+ components.ALLOW_DUPLICATES = True
+
+ d = module.__dict__
+ _modDictIDMap[id(d)] = module
+ newclasses = {}
+ classes = {}
+ functions = {}
+ values = {}
+ if doLog:
+ log.msg(' (scanning %s): ' % str(module.__name__))
+ for k, v in d.items():
+ if type(v) == types.ClassType:
+ # Failure condition -- instances of classes with buggy
+ # __hash__/__cmp__ methods referenced at the module level...
+ if v.__module__ == module.__name__:
+ classes[v] = 1
+ if doLog:
+ log.logfile.write("c")
+ log.logfile.flush()
+ elif type(v) == types.FunctionType:
+ if v.func_globals is module.__dict__:
+ functions[v] = 1
+ if doLog:
+ log.logfile.write("f")
+ log.logfile.flush()
+ elif isinstance(v, type):
+ if v.__module__ == module.__name__:
+ newclasses[v] = 1
+ if doLog:
+ log.logfile.write("o")
+ log.logfile.flush()
+
+ values.update(classes)
+ values.update(functions)
+ fromOldModule = values.has_key
+ newclasses = newclasses.keys()
+ classes = classes.keys()
+ functions = functions.keys()
+
+ if doLog:
+ log.msg('')
+ log.msg(' (reload %s)' % str(module.__name__))
+
+ # Boom.
+ reload(module)
+ # Make sure that my traceback printing will at least be recent...
+ linecache.clearcache()
+
+ if doLog:
+ log.msg(' (cleaning %s): ' % str(module.__name__))
+
+ for clazz in classes:
+ if getattr(module, clazz.__name__) is clazz:
+ log.msg("WARNING: class %s not replaced by reload!" % reflect.qual(clazz))
+ else:
+ if doLog:
+ log.logfile.write("x")
+ log.logfile.flush()
+ clazz.__bases__ = ()
+ clazz.__dict__.clear()
+ clazz.__getattr__ = __getattr__
+ clazz.__module__ = module.__name__
+ if newclasses:
+ import gc
+ for nclass in newclasses:
+ ga = getattr(module, nclass.__name__)
+ if ga is nclass:
+ log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass))
+ else:
+ for r in gc.get_referrers(nclass):
+ if getattr(r, '__class__', None) is nclass:
+ r.__class__ = ga
+ if doLog:
+ log.msg('')
+ log.msg(' (fixing %s): ' % str(module.__name__))
+ modcount = 0
+ for mk, mod in sys.modules.items():
+ modcount = modcount + 1
+ if mod == module or mod is None:
+ continue
+
+ if not hasattr(mod, '__file__'):
+ # It's a builtin module; nothing to replace here.
+ continue
+
+ if hasattr(mod, '__bundle__'):
+ # PyObjC has a few buggy objects which segfault if you hash() them.
+ # It doesn't make sense to try rebuilding extension modules like
+ # this anyway, so don't try.
+ continue
+
+ changed = 0
+
+ for k, v in mod.__dict__.items():
+ try:
+ hash(v)
+ except Exception:
+ continue
+ if fromOldModule(v):
+ if type(v) == types.ClassType:
+ if doLog:
+ log.logfile.write("c")
+ log.logfile.flush()
+ nv = latestClass(v)
+ else:
+ if doLog:
+ log.logfile.write("f")
+ log.logfile.flush()
+ nv = latestFunction(v)
+ changed = 1
+ setattr(mod, k, nv)
+ else:
+ # Replace bases of non-module classes just to be sure.
+ if type(v) == types.ClassType:
+ for base in v.__bases__:
+ if fromOldModule(base):
+ latestClass(v)
+ if doLog and not changed and ((modcount % 10) ==0) :
+ log.logfile.write(".")
+ log.logfile.flush()
+
+ components.ALLOW_DUPLICATES = False
+ if doLog:
+ log.msg('')
+ log.msg(' Rebuilt %s.' % str(module.__name__))
+ return module
+
diff --git a/twisted/python/reflect.py b/twisted/python/reflect.py
new file mode 100644
index 0000000..f529754
--- /dev/null
+++ b/twisted/python/reflect.py
@@ -0,0 +1,827 @@
+# -*- test-case-name: twisted.test.test_reflect -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Standardized versions of various cool and/or strange things that you can do
+with Python's reflection capabilities.
+"""
+
+import sys
+import os
+import types
+import pickle
+import traceback
+import weakref
+import re
+import warnings
+
+try:
+ from collections import deque
+except ImportError:
+ deque = list
+
+RegexType = type(re.compile(""))
+
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+from twisted.python.util import unsignedID
+from twisted.python.deprecate import deprecated, deprecatedModuleAttribute
+from twisted.python.deprecate import _fullyQualifiedName as fullyQualifiedName
+from twisted.python.versions import Version
+
+
+
+class Settable:
+ """
+ A mixin class for syntactic sugar. Lets you assign attributes by
+ calling with keyword arguments; for example, C{x(a=b,c=d,y=z)} is the
+ same as C{x.a=b;x.c=d;x.y=z}. The most useful place for this is
+ where you don't want to name a variable, but you do want to set
+ some attributes; for example, C{X()(y=z,a=b)}.
+ """
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "Settable is old and untested. Please write your own version of this "
+ "functionality if you need it.", "twisted.python.reflect", "Settable")
+
+ def __init__(self, **kw):
+ self(**kw)
+
+ def __call__(self,**kw):
+ for key,val in kw.items():
+ setattr(self,key,val)
+ return self
+
+
+class AccessorType(type):
+ """
+ Metaclass that generates properties automatically.
+
+ This is for Python 2.2 and up.
+
+ Using this metaclass for your class will give you explicit accessor
+ methods; a method called set_foo, will automatically create a property
+ 'foo' that uses set_foo as a setter method. Same for get_foo and del_foo.
+
+ Note that this will only work on methods that are present on class
+ creation. If you add methods after the class is defined they will not
+ automatically become properties. Likewise, class attributes will only
+ be used if they are present upon class creation, and no getter function
+ was set - if a getter is present, the class attribute will be ignored.
+
+ This is a 2.2-only alternative to the Accessor mixin - just set in your
+ class definition::
+
+ __metaclass__ = AccessorType
+
+ """
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "AccessorType is old and untested. Please write your own version of "
+ "this functionality if you need it.", "twisted.python.reflect",
+ "AccessorType")
+
+ def __init__(self, name, bases, d):
+ type.__init__(self, name, bases, d)
+ accessors = {}
+ prefixs = ["get_", "set_", "del_"]
+ for k in d.keys():
+ v = getattr(self, k)
+ for i in range(3):
+ if k.startswith(prefixs[i]):
+ accessors.setdefault(k[4:], [None, None, None])[i] = v
+ for name, (getter, setter, deler) in accessors.items():
+ # create default behaviours for the property - if we leave
+ # the getter as None we won't be able to getattr, etc..
+ if getter is None:
+ if hasattr(self, name):
+ value = getattr(self, name)
+ def getter(this, value=value, name=name):
+ if name in this.__dict__:
+ return this.__dict__[name]
+ else:
+ return value
+ else:
+ def getter(this, name=name):
+ if name in this.__dict__:
+ return this.__dict__[name]
+ else:
+ raise AttributeError("no such attribute %r" % name)
+ if setter is None:
+ def setter(this, value, name=name):
+ this.__dict__[name] = value
+ if deler is None:
+ def deler(this, name=name):
+ del this.__dict__[name]
+ setattr(self, name, property(getter, setter, deler, ""))
+
+
+class PropertyAccessor(object):
+ """
+ A mixin class for Python 2.2 that uses AccessorType.
+
+ This provides compatability with the pre-2.2 Accessor mixin, up
+ to a point.
+
+ Extending this class will give you explicit accessor methods; a
+ method called set_foo, for example, is the same as an if statement
+ in __setattr__ looking for 'foo'. Same for get_foo and del_foo.
+
+ There are also reallyDel and reallySet methods, so you can
+ override specifics in subclasses without clobbering __setattr__
+ and __getattr__, or using non-2.1 compatible code.
+
+ There is are incompatibilities with Accessor - accessor
+ methods added after class creation will *not* be detected. OTOH,
+ this method is probably way faster.
+
+ In addition, class attributes will only be used if no getter
+ was defined, and instance attributes will not override getter methods
+ whereas in original Accessor the class attribute or instance attribute
+ would override the getter method.
+ """
+ # addendum to above:
+ # The behaviour of Accessor is wrong IMHO, and I've found bugs
+ # caused by it.
+ # -- itamar
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "PropertyAccessor is old and untested. Please write your own version "
+ "of this functionality if you need it.", "twisted.python.reflect",
+ "PropertyAccessor")
+ __metaclass__ = AccessorType
+
+ def reallySet(self, k, v):
+ self.__dict__[k] = v
+
+ def reallyDel(self, k):
+ del self.__dict__[k]
+
+
+class Accessor:
+ """
+ Extending this class will give you explicit accessor methods; a
+ method called C{set_foo}, for example, is the same as an if statement
+ in L{__setattr__} looking for C{'foo'}. Same for C{get_foo} and
+ C{del_foo}. There are also L{reallyDel} and L{reallySet} methods,
+ so you can override specifics in subclasses without clobbering
+ L{__setattr__} and L{__getattr__}.
+
+ This implementation is for Python 2.1.
+ """
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "Accessor is an implementation for Python 2.1 which is no longer "
+ "supported by Twisted.", "twisted.python.reflect", "Accessor")
+
+ def __setattr__(self, k,v):
+ kstring='set_%s'%k
+ if hasattr(self.__class__,kstring):
+ return getattr(self,kstring)(v)
+ else:
+ self.reallySet(k,v)
+
+ def __getattr__(self, k):
+ kstring='get_%s'%k
+ if hasattr(self.__class__,kstring):
+ return getattr(self,kstring)()
+ raise AttributeError("%s instance has no accessor for: %s" % (qual(self.__class__),k))
+
+ def __delattr__(self, k):
+ kstring='del_%s'%k
+ if hasattr(self.__class__,kstring):
+ getattr(self,kstring)()
+ return
+ self.reallyDel(k)
+
+ def reallySet(self, k,v):
+ """
+ *actually* set self.k to v without incurring side-effects.
+ This is a hook to be overridden by subclasses.
+ """
+ if k == "__dict__":
+ self.__dict__.clear()
+ self.__dict__.update(v)
+ else:
+ self.__dict__[k]=v
+
+ def reallyDel(self, k):
+ """
+ *actually* del self.k without incurring side-effects. This is a
+ hook to be overridden by subclasses.
+ """
+ del self.__dict__[k]
+
+# just in case
+OriginalAccessor = Accessor
+deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "OriginalAccessor is a reference to class twisted.python.reflect.Accessor "
+ "which is deprecated.", "twisted.python.reflect", "OriginalAccessor")
+
+
+class Summer(Accessor):
+ """
+ Extend from this class to get the capability to maintain 'related
+ sums'. Have a tuple in your class like the following::
+
+ sums=(('amount','credit','credit_total'),
+ ('amount','debit','debit_total'))
+
+ and the 'credit_total' member of the 'credit' member of self will
+ always be incremented when the 'amount' member of self is
+ incremented, similiarly for the debit versions.
+ """
+
+ deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "Summer is a child class of twisted.python.reflect.Accessor which is "
+ "deprecated.", "twisted.python.reflect", "Summer")
+
+ def reallySet(self, k,v):
+ "This method does the work."
+ for sum in self.sums:
+ attr=sum[0]
+ obj=sum[1]
+ objattr=sum[2]
+ if k == attr:
+ try:
+ oldval=getattr(self, attr)
+ except:
+ oldval=0
+ diff=v-oldval
+ if hasattr(self, obj):
+ ob=getattr(self,obj)
+ if ob is not None:
+ try:oldobjval=getattr(ob, objattr)
+ except:oldobjval=0.0
+ setattr(ob,objattr,oldobjval+diff)
+
+ elif k == obj:
+ if hasattr(self, attr):
+ x=getattr(self,attr)
+ setattr(self,attr,0)
+ y=getattr(self,k)
+ Accessor.reallySet(self,k,v)
+ setattr(self,attr,x)
+ Accessor.reallySet(self,y,v)
+ Accessor.reallySet(self,k,v)
+
+
+class QueueMethod:
+ """
+ I represent a method that doesn't exist yet.
+ """
+ def __init__(self, name, calls):
+ self.name = name
+ self.calls = calls
+ def __call__(self, *args):
+ self.calls.append((self.name, args))
+
+
+def funcinfo(function):
+ """
+ this is more documentation for myself than useful code.
+ """
+ warnings.warn(
+ "[v2.5] Use inspect.getargspec instead of twisted.python.reflect.funcinfo",
+ DeprecationWarning,
+ stacklevel=2)
+ code=function.func_code
+ name=function.func_name
+ argc=code.co_argcount
+ argv=code.co_varnames[:argc]
+ defaults=function.func_defaults
+
+ out = []
+
+ out.append('The function %s accepts %s arguments' % (name ,argc))
+ if defaults:
+ required=argc-len(defaults)
+ out.append('It requires %s arguments' % required)
+ out.append('The arguments required are: %s' % argv[:required])
+ out.append('additional arguments are:')
+ for i in range(argc-required):
+ j=i+required
+ out.append('%s which has a default of' % (argv[j], defaults[i]))
+ return out
+
+
+ISNT=0
+WAS=1
+IS=2
+
+
+def fullFuncName(func):
+ qualName = (str(pickle.whichmodule(func, func.__name__)) + '.' + func.__name__)
+ if namedObject(qualName) is not func:
+ raise Exception("Couldn't find %s as %s." % (func, qualName))
+ return qualName
+
+
+def qual(clazz):
+ """
+ Return full import path of a class.
+ """
+ return clazz.__module__ + '.' + clazz.__name__
+
+
+def getcurrent(clazz):
+ assert type(clazz) == types.ClassType, 'must be a class...'
+ module = namedModule(clazz.__module__)
+ currclass = getattr(module, clazz.__name__, None)
+ if currclass is None:
+ return clazz
+ return currclass
+
+
+def getClass(obj):
+ """
+ Return the class or type of object 'obj'.
+ Returns sensible result for oldstyle and newstyle instances and types.
+ """
+ if hasattr(obj, '__class__'):
+ return obj.__class__
+ else:
+ return type(obj)
+
+# class graph nonsense
+
+# I should really have a better name for this...
+def isinst(inst,clazz):
+ if type(inst) != types.InstanceType or type(clazz)!= types.ClassType:
+ return isinstance(inst,clazz)
+ cl = inst.__class__
+ cl2 = getcurrent(cl)
+ clazz = getcurrent(clazz)
+ if issubclass(cl2,clazz):
+ if cl == cl2:
+ return WAS
+ else:
+ inst.__class__ = cl2
+ return IS
+ else:
+ return ISNT
+
+
+def namedModule(name):
+ """
+ Return a module given its name.
+ """
+ topLevel = __import__(name)
+ packages = name.split(".")[1:]
+ m = topLevel
+ for p in packages:
+ m = getattr(m, p)
+ return m
+
+
+def namedObject(name):
+ """
+ Get a fully named module-global object.
+ """
+ classSplit = name.split('.')
+ module = namedModule('.'.join(classSplit[:-1]))
+ return getattr(module, classSplit[-1])
+
+namedClass = namedObject # backwards compat
+
+
+
+class _NoModuleFound(Exception):
+ """
+ No module was found because none exists.
+ """
+
+
+class InvalidName(ValueError):
+ """
+ The given name is not a dot-separated list of Python objects.
+ """
+
+
+class ModuleNotFound(InvalidName):
+ """
+ The module associated with the given name doesn't exist and it can't be
+ imported.
+ """
+
+
+class ObjectNotFound(InvalidName):
+ """
+ The object associated with the given name doesn't exist and it can't be
+ imported.
+ """
+
+
+def _importAndCheckStack(importName):
+ """
+ Import the given name as a module, then walk the stack to determine whether
+ the failure was the module not existing, or some code in the module (for
+ example a dependent import) failing. This can be helpful to determine
+ whether any actual application code was run. For example, to distiguish
+ administrative error (entering the wrong module name), from programmer
+ error (writing buggy code in a module that fails to import).
+
+ @raise Exception: if something bad happens. This can be any type of
+ exception, since nobody knows what loading some arbitrary code might do.
+
+ @raise _NoModuleFound: if no module was found.
+ """
+ try:
+ try:
+ return __import__(importName)
+ except ImportError:
+ excType, excValue, excTraceback = sys.exc_info()
+ while excTraceback:
+ execName = excTraceback.tb_frame.f_globals["__name__"]
+ if (execName is None or # python 2.4+, post-cleanup
+ execName == importName): # python 2.3, no cleanup
+ raise excType, excValue, excTraceback
+ excTraceback = excTraceback.tb_next
+ raise _NoModuleFound()
+ except:
+ # Necessary for cleaning up modules in 2.3.
+ sys.modules.pop(importName, None)
+ raise
+
+
+
+def namedAny(name):
+ """
+ Retrieve a Python object by its fully qualified name from the global Python
+ module namespace. The first part of the name, that describes a module,
+ will be discovered and imported. Each subsequent part of the name is
+ treated as the name of an attribute of the object specified by all of the
+ name which came before it. For example, the fully-qualified name of this
+ object is 'twisted.python.reflect.namedAny'.
+
+ @type name: L{str}
+ @param name: The name of the object to return.
+
+ @raise InvalidName: If the name is an empty string, starts or ends with
+ a '.', or is otherwise syntactically incorrect.
+
+ @raise ModuleNotFound: If the name is syntactically correct but the
+ module it specifies cannot be imported because it does not appear to
+ exist.
+
+ @raise ObjectNotFound: If the name is syntactically correct, includes at
+ least one '.', but the module it specifies cannot be imported because
+ it does not appear to exist.
+
+ @raise AttributeError: If an attribute of an object along the way cannot be
+ accessed, or a module along the way is not found.
+
+ @return: the Python object identified by 'name'.
+ """
+ if not name:
+ raise InvalidName('Empty module name')
+
+ names = name.split('.')
+
+ # if the name starts or ends with a '.' or contains '..', the __import__
+ # will raise an 'Empty module name' error. This will provide a better error
+ # message.
+ if '' in names:
+ raise InvalidName(
+ "name must be a string giving a '.'-separated list of Python "
+ "identifiers, not %r" % (name,))
+
+ topLevelPackage = None
+ moduleNames = names[:]
+ while not topLevelPackage:
+ if moduleNames:
+ trialname = '.'.join(moduleNames)
+ try:
+ topLevelPackage = _importAndCheckStack(trialname)
+ except _NoModuleFound:
+ moduleNames.pop()
+ else:
+ if len(names) == 1:
+ raise ModuleNotFound("No module named %r" % (name,))
+ else:
+ raise ObjectNotFound('%r does not name an object' % (name,))
+
+ obj = topLevelPackage
+ for n in names[1:]:
+ obj = getattr(obj, n)
+
+ return obj
+
+
+
+def _determineClass(x):
+ try:
+ return x.__class__
+ except:
+ return type(x)
+
+
+
+def _determineClassName(x):
+ c = _determineClass(x)
+ try:
+ return c.__name__
+ except:
+ try:
+ return str(c)
+ except:
+ return '<BROKEN CLASS AT 0x%x>' % unsignedID(c)
+
+
+
+def _safeFormat(formatter, o):
+ """
+ Helper function for L{safe_repr} and L{safe_str}.
+ """
+ try:
+ return formatter(o)
+ except:
+ io = StringIO()
+ traceback.print_exc(file=io)
+ className = _determineClassName(o)
+ tbValue = io.getvalue()
+ return "<%s instance at 0x%x with %s error:\n %s>" % (
+ className, unsignedID(o), formatter.__name__, tbValue)
+
+
+
+def safe_repr(o):
+ """
+ safe_repr(anything) -> string
+
+ Returns a string representation of an object, or a string containing a
+ traceback, if that object's __repr__ raised an exception.
+ """
+ return _safeFormat(repr, o)
+
+
+
+def safe_str(o):
+ """
+ safe_str(anything) -> string
+
+ Returns a string representation of an object, or a string containing a
+ traceback, if that object's __str__ raised an exception.
+ """
+ return _safeFormat(str, o)
+
+
+
+## the following were factored out of usage
+
+@deprecated(Version("Twisted", 11, 0, 0), "inspect.getmro")
+def allYourBase(classObj, baseClass=None):
+ """
+ allYourBase(classObj, baseClass=None) -> list of all base
+ classes that are subclasses of baseClass, unless it is None,
+ in which case all bases will be added.
+ """
+ l = []
+ _accumulateBases(classObj, l, baseClass)
+ return l
+
+
+@deprecated(Version("Twisted", 11, 0, 0), "inspect.getmro")
+def accumulateBases(classObj, l, baseClass=None):
+ _accumulateBases(classObj, l, baseClass)
+
+
+def _accumulateBases(classObj, l, baseClass=None):
+ for base in classObj.__bases__:
+ if baseClass is None or issubclass(base, baseClass):
+ l.append(base)
+ _accumulateBases(base, l, baseClass)
+
+
+def prefixedMethodNames(classObj, prefix):
+ """
+ A list of method names with a given prefix in a given class.
+ """
+ dct = {}
+ addMethodNamesToDict(classObj, dct, prefix)
+ return dct.keys()
+
+
+def addMethodNamesToDict(classObj, dict, prefix, baseClass=None):
+ """
+ addMethodNamesToDict(classObj, dict, prefix, baseClass=None) -> dict
+ this goes through 'classObj' (and its bases) and puts method names
+ starting with 'prefix' in 'dict' with a value of 1. if baseClass isn't
+ None, methods will only be added if classObj is-a baseClass
+
+ If the class in question has the methods 'prefix_methodname' and
+ 'prefix_methodname2', the resulting dict should look something like:
+ {"methodname": 1, "methodname2": 1}.
+ """
+ for base in classObj.__bases__:
+ addMethodNamesToDict(base, dict, prefix, baseClass)
+
+ if baseClass is None or baseClass in classObj.__bases__:
+ for name, method in classObj.__dict__.items():
+ optName = name[len(prefix):]
+ if ((type(method) is types.FunctionType)
+ and (name[:len(prefix)] == prefix)
+ and (len(optName))):
+ dict[optName] = 1
+
+
+def prefixedMethods(obj, prefix=''):
+ """
+ A list of methods with a given prefix on a given instance.
+ """
+ dct = {}
+ accumulateMethods(obj, dct, prefix)
+ return dct.values()
+
+
+def accumulateMethods(obj, dict, prefix='', curClass=None):
+ """
+ accumulateMethods(instance, dict, prefix)
+ I recurse through the bases of instance.__class__, and add methods
+ beginning with 'prefix' to 'dict', in the form of
+ {'methodname':*instance*method_object}.
+ """
+ if not curClass:
+ curClass = obj.__class__
+ for base in curClass.__bases__:
+ accumulateMethods(obj, dict, prefix, base)
+
+ for name, method in curClass.__dict__.items():
+ optName = name[len(prefix):]
+ if ((type(method) is types.FunctionType)
+ and (name[:len(prefix)] == prefix)
+ and (len(optName))):
+ dict[optName] = getattr(obj, name)
+
+
+def accumulateClassDict(classObj, attr, adict, baseClass=None):
+ """
+ Accumulate all attributes of a given name in a class hierarchy into a single dictionary.
+
+ Assuming all class attributes of this name are dictionaries.
+ If any of the dictionaries being accumulated have the same key, the
+ one highest in the class heirarchy wins.
+ (XXX: If \"higest\" means \"closest to the starting class\".)
+
+ Ex::
+
+ class Soy:
+ properties = {\"taste\": \"bland\"}
+
+ class Plant:
+ properties = {\"colour\": \"green\"}
+
+ class Seaweed(Plant):
+ pass
+
+ class Lunch(Soy, Seaweed):
+ properties = {\"vegan\": 1 }
+
+ dct = {}
+
+ accumulateClassDict(Lunch, \"properties\", dct)
+
+ print dct
+
+ {\"taste\": \"bland\", \"colour\": \"green\", \"vegan\": 1}
+ """
+ for base in classObj.__bases__:
+ accumulateClassDict(base, attr, adict)
+ if baseClass is None or baseClass in classObj.__bases__:
+ adict.update(classObj.__dict__.get(attr, {}))
+
+
+def accumulateClassList(classObj, attr, listObj, baseClass=None):
+ """
+ Accumulate all attributes of a given name in a class heirarchy into a single list.
+
+ Assuming all class attributes of this name are lists.
+ """
+ for base in classObj.__bases__:
+ accumulateClassList(base, attr, listObj)
+ if baseClass is None or baseClass in classObj.__bases__:
+ listObj.extend(classObj.__dict__.get(attr, []))
+
+
+def isSame(a, b):
+ return (a is b)
+
+
+def isLike(a, b):
+ return (a == b)
+
+
+def modgrep(goal):
+ return objgrep(sys.modules, goal, isLike, 'sys.modules')
+
+
+def isOfType(start, goal):
+ return ((type(start) is goal) or
+ (isinstance(start, types.InstanceType) and
+ start.__class__ is goal))
+
+
+def findInstances(start, t):
+ return objgrep(start, t, isOfType)
+
+
+def objgrep(start, goal, eq=isLike, path='', paths=None, seen=None, showUnknowns=0, maxDepth=None):
+ """
+ An insanely CPU-intensive process for finding stuff.
+ """
+ if paths is None:
+ paths = []
+ if seen is None:
+ seen = {}
+ if eq(start, goal):
+ paths.append(path)
+ if id(start) in seen:
+ if seen[id(start)] is start:
+ return
+ if maxDepth is not None:
+ if maxDepth == 0:
+ return
+ maxDepth -= 1
+ seen[id(start)] = start
+ if isinstance(start, types.DictionaryType):
+ for k, v in start.items():
+ objgrep(k, goal, eq, path+'{'+repr(v)+'}', paths, seen, showUnknowns, maxDepth)
+ objgrep(v, goal, eq, path+'['+repr(k)+']', paths, seen, showUnknowns, maxDepth)
+ elif isinstance(start, (list, tuple, deque)):
+ for idx in xrange(len(start)):
+ objgrep(start[idx], goal, eq, path+'['+str(idx)+']', paths, seen, showUnknowns, maxDepth)
+ elif isinstance(start, types.MethodType):
+ objgrep(start.im_self, goal, eq, path+'.im_self', paths, seen, showUnknowns, maxDepth)
+ objgrep(start.im_func, goal, eq, path+'.im_func', paths, seen, showUnknowns, maxDepth)
+ objgrep(start.im_class, goal, eq, path+'.im_class', paths, seen, showUnknowns, maxDepth)
+ elif hasattr(start, '__dict__'):
+ for k, v in start.__dict__.items():
+ objgrep(v, goal, eq, path+'.'+k, paths, seen, showUnknowns, maxDepth)
+ if isinstance(start, types.InstanceType):
+ objgrep(start.__class__, goal, eq, path+'.__class__', paths, seen, showUnknowns, maxDepth)
+ elif isinstance(start, weakref.ReferenceType):
+ objgrep(start(), goal, eq, path+'()', paths, seen, showUnknowns, maxDepth)
+ elif (isinstance(start, types.StringTypes+
+ (types.IntType, types.FunctionType,
+ types.BuiltinMethodType, RegexType, types.FloatType,
+ types.NoneType, types.FileType)) or
+ type(start).__name__ in ('wrapper_descriptor', 'method_descriptor',
+ 'member_descriptor', 'getset_descriptor')):
+ pass
+ elif showUnknowns:
+ print 'unknown type', type(start), start
+ return paths
+
+
+def filenameToModuleName(fn):
+ """
+ Convert a name in the filesystem to the name of the Python module it is.
+
+ This is agressive about getting a module name back from a file; it will
+ always return a string. Agressive means 'sometimes wrong'; it won't look
+ at the Python path or try to do any error checking: don't use this method
+ unless you already know that the filename you're talking about is a Python
+ module.
+ """
+ fullName = os.path.abspath(fn)
+ base = os.path.basename(fn)
+ if not base:
+ # this happens when fn ends with a path separator, just skit it
+ base = os.path.basename(fn[:-1])
+ modName = os.path.splitext(base)[0]
+ while 1:
+ fullName = os.path.dirname(fullName)
+ if os.path.exists(os.path.join(fullName, "__init__.py")):
+ modName = "%s.%s" % (os.path.basename(fullName), modName)
+ else:
+ break
+ return modName
+
+
+
+__all__ = [
+ 'InvalidName', 'ModuleNotFound', 'ObjectNotFound',
+
+ 'ISNT', 'WAS', 'IS',
+
+ 'Settable', 'AccessorType', 'PropertyAccessor', 'Accessor', 'Summer',
+ 'QueueMethod', 'OriginalAccessor',
+
+ 'funcinfo', 'fullFuncName', 'qual', 'getcurrent', 'getClass', 'isinst',
+ 'namedModule', 'namedObject', 'namedClass', 'namedAny',
+ 'safe_repr', 'safe_str', 'allYourBase', 'accumulateBases',
+ 'prefixedMethodNames', 'addMethodNamesToDict', 'prefixedMethods',
+ 'accumulateClassDict', 'accumulateClassList', 'isSame', 'isLike',
+ 'modgrep', 'isOfType', 'findInstances', 'objgrep', 'filenameToModuleName',
+ 'fullyQualifiedName']
diff --git a/twisted/python/release.py b/twisted/python/release.py
new file mode 100644
index 0000000..2454792
--- /dev/null
+++ b/twisted/python/release.py
@@ -0,0 +1,63 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A release-automation toolkit.
+
+Don't use this outside of Twisted.
+
+Maintainer: Christopher Armstrong
+"""
+
+import os
+
+
+# errors
+
+class DirectoryExists(OSError):
+ """
+ Some directory exists when it shouldn't.
+ """
+ pass
+
+
+
+class DirectoryDoesntExist(OSError):
+ """
+ Some directory doesn't exist when it should.
+ """
+ pass
+
+
+
+class CommandFailed(OSError):
+ pass
+
+
+
+# utilities
+
+def sh(command, null=True, prompt=False):
+ """
+ I'll try to execute C{command}, and if C{prompt} is true, I'll
+ ask before running it. If the command returns something other
+ than 0, I'll raise C{CommandFailed(command)}.
+ """
+ print "--$", command
+
+ if prompt:
+ if raw_input("run ?? ").startswith('n'):
+ return
+ if null:
+ command = "%s > /dev/null" % command
+ if os.system(command) != 0:
+ raise CommandFailed(command)
+
+
+
+def runChdirSafe(f, *args, **kw):
+ origdir = os.path.abspath('.')
+ try:
+ return f(*args, **kw)
+ finally:
+ os.chdir(origdir)
diff --git a/twisted/python/roots.py b/twisted/python/roots.py
new file mode 100644
index 0000000..ee3c8a3
--- /dev/null
+++ b/twisted/python/roots.py
@@ -0,0 +1,248 @@
+# -*- test-case-name: twisted.test.test_roots -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted Python Roots: an abstract hierarchy representation for Twisted.
+
+Maintainer: Glyph Lefkowitz
+"""
+
+# System imports
+import types
+from twisted.python import reflect
+
+class NotSupportedError(NotImplementedError):
+ """
+ An exception meaning that the tree-manipulation operation
+ you're attempting to perform is not supported.
+ """
+
+
+class Request:
+ """I am an abstract representation of a request for an entity.
+
+ I also function as the response. The request is responded to by calling
+ self.write(data) until there is no data left and then calling
+ self.finish().
+ """
+ # This attribute should be set to the string name of the protocol being
+ # responded to (e.g. HTTP or FTP)
+ wireProtocol = None
+ def write(self, data):
+ """Add some data to the response to this request.
+ """
+ raise NotImplementedError("%s.write" % reflect.qual(self.__class__))
+
+ def finish(self):
+ """The response to this request is finished; flush all data to the network stream.
+ """
+ raise NotImplementedError("%s.finish" % reflect.qual(self.__class__))
+
+
+class Entity:
+ """I am a terminal object in a hierarchy, with no children.
+
+ I represent a null interface; certain non-instance objects (strings and
+ integers, notably) are Entities.
+
+ Methods on this class are suggested to be implemented, but are not
+ required, and will be emulated on a per-protocol basis for types which do
+ not handle them.
+ """
+ def render(self, request):
+ """
+ I produce a stream of bytes for the request, by calling request.write()
+ and request.finish().
+ """
+ raise NotImplementedError("%s.render" % reflect.qual(self.__class__))
+
+
+class Collection:
+ """I represent a static collection of entities.
+
+ I contain methods designed to represent collections that can be dynamically
+ created.
+ """
+
+ def __init__(self, entities=None):
+ """Initialize me.
+ """
+ if entities is not None:
+ self.entities = entities
+ else:
+ self.entities = {}
+
+ def getStaticEntity(self, name):
+ """Get an entity that was added to me using putEntity.
+
+ This method will return 'None' if it fails.
+ """
+ return self.entities.get(name)
+
+ def getDynamicEntity(self, name, request):
+ """Subclass this to generate an entity on demand.
+
+ This method should return 'None' if it fails.
+ """
+
+ def getEntity(self, name, request):
+ """Retrieve an entity from me.
+
+ I will first attempt to retrieve an entity statically; static entities
+ will obscure dynamic ones. If that fails, I will retrieve the entity
+ dynamically.
+
+ If I cannot retrieve an entity, I will return 'None'.
+ """
+ ent = self.getStaticEntity(name)
+ if ent is not None:
+ return ent
+ ent = self.getDynamicEntity(name, request)
+ if ent is not None:
+ return ent
+ return None
+
+ def putEntity(self, name, entity):
+ """Store a static reference on 'name' for 'entity'.
+
+ Raises a KeyError if the operation fails.
+ """
+ self.entities[name] = entity
+
+ def delEntity(self, name):
+ """Remove a static reference for 'name'.
+
+ Raises a KeyError if the operation fails.
+ """
+ del self.entities[name]
+
+ def storeEntity(self, name, request):
+ """Store an entity for 'name', based on the content of 'request'.
+ """
+ raise NotSupportedError("%s.storeEntity" % reflect.qual(self.__class__))
+
+ def removeEntity(self, name, request):
+ """Remove an entity for 'name', based on the content of 'request'.
+ """
+ raise NotSupportedError("%s.removeEntity" % reflect.qual(self.__class__))
+
+ def listStaticEntities(self):
+ """Retrieve a list of all name, entity pairs that I store references to.
+
+ See getStaticEntity.
+ """
+ return self.entities.items()
+
+ def listDynamicEntities(self, request):
+ """A list of all name, entity that I can generate on demand.
+
+ See getDynamicEntity.
+ """
+ return []
+
+ def listEntities(self, request):
+ """Retrieve a list of all name, entity pairs I contain.
+
+ See getEntity.
+ """
+ return self.listStaticEntities() + self.listDynamicEntities(request)
+
+ def listStaticNames(self):
+ """Retrieve a list of the names of entities that I store references to.
+
+ See getStaticEntity.
+ """
+ return self.entities.keys()
+
+
+ def listDynamicNames(self):
+ """Retrieve a list of the names of entities that I store references to.
+
+ See getDynamicEntity.
+ """
+ return []
+
+
+ def listNames(self, request):
+ """Retrieve a list of all names for entities that I contain.
+
+ See getEntity.
+ """
+ return self.listStaticNames()
+
+
+class ConstraintViolation(Exception):
+ """An exception raised when a constraint is violated.
+ """
+
+
+class Constrained(Collection):
+ """A collection that has constraints on its names and/or entities."""
+
+ def nameConstraint(self, name):
+ """A method that determines whether an entity may be added to me with a given name.
+
+ If the constraint is satisfied, return 1; if the constraint is not
+ satisfied, either return 0 or raise a descriptive ConstraintViolation.
+ """
+ return 1
+
+ def entityConstraint(self, entity):
+ """A method that determines whether an entity may be added to me.
+
+ If the constraint is satisfied, return 1; if the constraint is not
+ satisfied, either return 0 or raise a descriptive ConstraintViolation.
+ """
+ return 1
+
+ def reallyPutEntity(self, name, entity):
+ Collection.putEntity(self, name, entity)
+
+ def putEntity(self, name, entity):
+ """Store an entity if it meets both constraints.
+
+ Otherwise raise a ConstraintViolation.
+ """
+ if self.nameConstraint(name):
+ if self.entityConstraint(entity):
+ self.reallyPutEntity(name, entity)
+ else:
+ raise ConstraintViolation("Entity constraint violated.")
+ else:
+ raise ConstraintViolation("Name constraint violated.")
+
+
+class Locked(Constrained):
+ """A collection that can be locked from adding entities."""
+
+ locked = 0
+
+ def lock(self):
+ self.locked = 1
+
+ def entityConstraint(self, entity):
+ return not self.locked
+
+
+class Homogenous(Constrained):
+ """A homogenous collection of entities.
+
+ I will only contain entities that are an instance of the class or type
+ specified by my 'entityType' attribute.
+ """
+
+ entityType = types.InstanceType
+
+ def entityConstraint(self, entity):
+ if isinstance(entity, self.entityType):
+ return 1
+ else:
+ raise ConstraintViolation("%s of incorrect type (%s)" %
+ (entity, self.entityType))
+
+ def getNameType(self):
+ return "Name"
+
+ def getEntityType(self):
+ return self.entityType.__name__
diff --git a/twisted/python/runtime.py b/twisted/python/runtime.py
new file mode 100644
index 0000000..159d5dc
--- /dev/null
+++ b/twisted/python/runtime.py
@@ -0,0 +1,137 @@
+# -*- test-case-name: twisted.python.test.test_runtime -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+# System imports
+import os
+import sys
+import time
+import imp
+
+
+def shortPythonVersion():
+ hv = sys.hexversion
+ major = (hv & 0xff000000L) >> 24
+ minor = (hv & 0x00ff0000L) >> 16
+ teeny = (hv & 0x0000ff00L) >> 8
+ return "%s.%s.%s" % (major,minor,teeny)
+
+knownPlatforms = {
+ 'nt': 'win32',
+ 'ce': 'win32',
+ 'posix': 'posix',
+ 'java': 'java',
+ 'org.python.modules.os': 'java',
+ }
+
+_timeFunctions = {
+ #'win32': time.clock,
+ 'win32': time.time,
+ }
+
+class Platform:
+ """Gives us information about the platform we're running on"""
+
+ type = knownPlatforms.get(os.name)
+ seconds = staticmethod(_timeFunctions.get(type, time.time))
+ _platform = sys.platform
+
+ def __init__(self, name=None, platform=None):
+ if name is not None:
+ self.type = knownPlatforms.get(name)
+ self.seconds = _timeFunctions.get(self.type, time.time)
+ if platform is not None:
+ self._platform = platform
+
+
+ def isKnown(self):
+ """Do we know about this platform?"""
+ return self.type != None
+
+
+ def getType(self):
+ """Return 'posix', 'win32' or 'java'"""
+ return self.type
+
+
+ def isMacOSX(self):
+ """Check if current platform is Mac OS X.
+
+ @return: C{True} if the current platform has been detected as OS X
+ @rtype: C{bool}
+ """
+ return self._platform == "darwin"
+
+
+ def isWinNT(self):
+ """Are we running in Windows NT?"""
+ if self.getType() == 'win32':
+ import _winreg
+ try:
+ k=_winreg.OpenKeyEx(_winreg.HKEY_LOCAL_MACHINE,
+ r'Software\Microsoft\Windows NT\CurrentVersion')
+ _winreg.QueryValueEx(k, 'SystemRoot')
+ return 1
+ except WindowsError:
+ return 0
+ # not windows NT
+ return 0
+
+
+ def isWindows(self):
+ return self.getType() == 'win32'
+
+
+ def isVista(self):
+ """
+ Check if current platform is Windows Vista or Windows Server 2008.
+
+ @return: C{True} if the current platform has been detected as Vista
+ @rtype: C{bool}
+ """
+ if getattr(sys, "getwindowsversion", None) is not None:
+ return sys.getwindowsversion()[0] == 6
+ else:
+ return False
+
+
+ def isLinux(self):
+ """
+ Check if current platform is Linux.
+
+ @return: C{True} if the current platform has been detected as Linux.
+ @rtype: C{bool}
+ """
+ return self._platform.startswith("linux")
+
+
+ def supportsThreads(self):
+ """Can threads be created?
+ """
+ try:
+ return imp.find_module('thread')[0] is None
+ except ImportError:
+ return False
+
+
+ def supportsINotify(self):
+ """
+ Return C{True} if we can use the inotify API on this platform.
+
+ @since: 10.1
+ """
+ try:
+ from twisted.python._inotify import INotifyError, init
+ except ImportError:
+ return False
+ try:
+ os.close(init())
+ except INotifyError:
+ return False
+ return True
+
+
+platform = Platform()
+platformType = platform.getType()
+seconds = platform.seconds
diff --git a/twisted/python/sendmsg.c b/twisted/python/sendmsg.c
new file mode 100644
index 0000000..4da6aab
--- /dev/null
+++ b/twisted/python/sendmsg.c
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) Twisted Matrix Laboratories.
+ * See LICENSE for details.
+ */
+
+#define PY_SSIZE_T_CLEAN 1
+#include <Python.h>
+
+#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
+/* This may cause some warnings, but if you want to get rid of them, upgrade
+ * your Python version. */
+typedef int Py_ssize_t;
+#endif
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <signal.h>
+
+/*
+ * As per
+ * <http://pubs.opengroup.org/onlinepubs/007904875/basedefs/sys/socket.h.html
+ * #tag_13_61_05>:
+ *
+ * "To forestall portability problems, it is recommended that applications
+ * not use values larger than (2**31)-1 for the socklen_t type."
+ */
+
+#define SOCKLEN_MAX 0x7FFFFFFF
+
+PyObject *sendmsg_socket_error;
+
+static PyObject *sendmsg_sendmsg(PyObject *self, PyObject *args, PyObject *keywds);
+static PyObject *sendmsg_recvmsg(PyObject *self, PyObject *args, PyObject *keywds);
+static PyObject *sendmsg_getsockfam(PyObject *self, PyObject *args, PyObject *keywds);
+
+static char sendmsg_doc[] = "\
+Bindings for sendmsg(2), recvmsg(2), and a minimal helper for inspecting\n\
+address family of a socket.\n\
+";
+
+static char sendmsg_sendmsg_doc[] = "\
+Wrap the C sendmsg(2) function for sending \"messages\" on a socket.\n\
+\n\
+@param fd: The file descriptor of the socket over which to send a message.\n\
+@type fd: C{int}\n\
+\n\
+@param data: Bytes to write to the socket.\n\
+@type data: C{str}\n\
+\n\
+@param flags: Flags to affect how the message is sent. See the C{MSG_}\n\
+ constants in the sendmsg(2) manual page. By default no flags are set.\n\
+@type flags: C{int}\n\
+\n\
+@param ancillary: Extra data to send over the socket outside of the normal\n\
+ datagram or stream mechanism. By default no ancillary data is sent.\n\
+@type ancillary: C{list} of C{tuple} of C{int}, C{int}, and C{str}.\n\
+\n\
+@raise OverflowError: Raised if too much ancillary data is given.\n\
+@raise socket.error: Raised if the underlying syscall indicates an error.\n\
+\n\
+@return: The return value of the underlying syscall, if it succeeds.\n\
+";
+
+static char sendmsg_recvmsg_doc[] = "\
+Wrap the C recvmsg(2) function for receiving \"messages\" on a socket.\n\
+\n\
+@param fd: The file descriptor of the socket over which to receve a message.\n\
+@type fd: C{int}\n\
+\n\
+@param flags: Flags to affect how the message is sent. See the C{MSG_}\n\
+ constants in the sendmsg(2) manual page. By default no flags are set.\n\
+@type flags: C{int}\n\
+\n\
+@param maxsize: The maximum number of bytes to receive from the socket\n\
+ using the datagram or stream mechanism. The default maximum is 8192.\n\
+@type maxsize: C{int}\n\
+\n\
+@param cmsg_size: The maximum number of bytes to receive from the socket\n\
+ outside of the normal datagram or stream mechanism. The default maximum is 4096.\n\
+\n\
+@raise OverflowError: Raised if too much ancillary data is given.\n\
+@raise socket.error: Raised if the underlying syscall indicates an error.\n\
+\n\
+@return: A C{tuple} of three elements: the bytes received using the\n\
+ datagram/stream mechanism, flags as an C{int} describing the data\n\
+ received, and a C{list} of C{tuples} giving ancillary received data.\n\
+";
+
+static char sendmsg_getsockfam_doc[] = "\
+Retrieve the address family of a given socket.\n\
+\n\
+@param fd: The file descriptor of the socket the address family of which\n\
+ to retrieve.\n\
+@type fd: C{int}\n\
+\n\
+@raise socket.error: Raised if the underlying getsockname call indicates\n\
+ an error.\n\
+\n\
+@return: A C{int} representing the address family of the socket. For\n\
+ example, L{socket.AF_INET}, L{socket.AF_INET6}, or L{socket.AF_UNIX}.\n\
+";
+
+static PyMethodDef sendmsg_methods[] = {
+ {"send1msg", (PyCFunction) sendmsg_sendmsg, METH_VARARGS | METH_KEYWORDS,
+ sendmsg_sendmsg_doc},
+ {"recv1msg", (PyCFunction) sendmsg_recvmsg, METH_VARARGS | METH_KEYWORDS,
+ sendmsg_recvmsg_doc},
+ {"getsockfam", (PyCFunction) sendmsg_getsockfam,
+ METH_VARARGS | METH_KEYWORDS, sendmsg_getsockfam_doc},
+ {NULL, NULL, 0, NULL}
+};
+
+
+PyMODINIT_FUNC initsendmsg(void) {
+ PyObject *module;
+
+ sendmsg_socket_error = NULL; /* Make sure that this has a known value
+ before doing anything that might exit. */
+
+ module = Py_InitModule3("sendmsg", sendmsg_methods, sendmsg_doc);
+
+ if (!module) {
+ return;
+ }
+
+ /*
+ The following is the only value mentioned by POSIX:
+ http://www.opengroup.org/onlinepubs/9699919799/basedefs/sys_socket.h.html
+ */
+
+ if (-1 == PyModule_AddIntConstant(module, "SCM_RIGHTS", SCM_RIGHTS)) {
+ return;
+ }
+
+
+ /* BSD, Darwin, Hurd */
+#if defined(SCM_CREDS)
+ if (-1 == PyModule_AddIntConstant(module, "SCM_CREDS", SCM_CREDS)) {
+ return;
+ }
+#endif
+
+ /* Linux */
+#if defined(SCM_CREDENTIALS)
+ if (-1 == PyModule_AddIntConstant(module, "SCM_CREDENTIALS", SCM_CREDENTIALS)) {
+ return;
+ }
+#endif
+
+ /* Apparently everywhere, but not standardized. */
+#if defined(SCM_TIMESTAMP)
+ if (-1 == PyModule_AddIntConstant(module, "SCM_TIMESTAMP", SCM_TIMESTAMP)) {
+ return;
+ }
+#endif
+
+ module = PyImport_ImportModule("socket");
+ if (!module) {
+ return;
+ }
+
+ sendmsg_socket_error = PyObject_GetAttrString(module, "error");
+ if (!sendmsg_socket_error) {
+ return;
+ }
+}
+
+static PyObject *sendmsg_sendmsg(PyObject *self, PyObject *args, PyObject *keywds) {
+
+ int fd;
+ int flags = 0;
+ Py_ssize_t sendmsg_result;
+ struct msghdr message_header;
+ struct iovec iov[1];
+ PyObject *ancillary = NULL;
+ PyObject *iterator = NULL;
+ PyObject *item = NULL;
+ PyObject *result_object = NULL;
+
+ static char *kwlist[] = {"fd", "data", "flags", "ancillary", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(
+ args, keywds, "it#|iO:sendmsg", kwlist,
+ &fd,
+ &iov[0].iov_base,
+ &iov[0].iov_len,
+ &flags,
+ &ancillary)) {
+ return NULL;
+ }
+
+ message_header.msg_name = NULL;
+ message_header.msg_namelen = 0;
+
+ message_header.msg_iov = iov;
+ message_header.msg_iovlen = 1;
+
+ message_header.msg_control = NULL;
+ message_header.msg_controllen = 0;
+
+ message_header.msg_flags = 0;
+
+ if (ancillary) {
+
+ if (!PyList_Check(ancillary)) {
+ PyErr_Format(PyExc_TypeError,
+ "send1msg argument 3 expected list, got %s",
+ ancillary->ob_type->tp_name);
+ goto finished;
+ }
+
+ iterator = PyObject_GetIter(ancillary);
+
+ if (iterator == NULL) {
+ goto finished;
+ }
+
+ size_t all_data_len = 0;
+
+ /* First we need to know how big the buffer needs to be in order to
+ have enough space for all of the messages. */
+ while ( (item = PyIter_Next(iterator)) ) {
+ int type, level;
+ Py_ssize_t data_len;
+ size_t prev_all_data_len;
+ char *data;
+
+ if (!PyTuple_Check(item)) {
+ PyErr_Format(PyExc_TypeError,
+ "send1msg argument 3 expected list of tuple, "
+ "got list containing %s",
+ item->ob_type->tp_name);
+ goto finished;
+ }
+
+ if (!PyArg_ParseTuple(
+ item, "iit#:sendmsg ancillary data (level, type, data)",
+ &level, &type, &data, &data_len)) {
+ goto finished;
+ }
+
+ prev_all_data_len = all_data_len;
+ all_data_len += CMSG_SPACE(data_len);
+
+ Py_DECREF(item);
+ item = NULL;
+
+ if (all_data_len < prev_all_data_len) {
+ PyErr_Format(PyExc_OverflowError,
+ "Too much msg_control to fit in a size_t: %zu",
+ prev_all_data_len);
+ goto finished;
+ }
+ }
+
+ Py_DECREF(iterator);
+ iterator = NULL;
+
+ /* Allocate the buffer for all of the ancillary elements, if we have
+ * any. */
+ if (all_data_len) {
+ if (all_data_len > SOCKLEN_MAX) {
+ PyErr_Format(PyExc_OverflowError,
+ "Too much msg_control to fit in a socklen_t: %zu",
+ all_data_len);
+ goto finished;
+ }
+ message_header.msg_control = PyMem_Malloc(all_data_len);
+ if (!message_header.msg_control) {
+ PyErr_NoMemory();
+ goto finished;
+ }
+ } else {
+ message_header.msg_control = NULL;
+ }
+ message_header.msg_controllen = (socklen_t) all_data_len;
+
+ iterator = PyObject_GetIter(ancillary); /* again */
+
+ if (!iterator) {
+ goto finished;
+ }
+
+ /* Unpack the tuples into the control message. */
+ struct cmsghdr *control_message = CMSG_FIRSTHDR(&message_header);
+ while ( (item = PyIter_Next(iterator)) ) {
+ int data_len, type, level;
+ size_t data_size;
+ unsigned char *data, *cmsg_data;
+
+ /* We explicitly allocated enough space for all ancillary data
+ above; if there isn't enough room, all bets are off. */
+ assert(control_message);
+
+ if (!PyArg_ParseTuple(item,
+ "iit#:sendmsg ancillary data (level, type, data)",
+ &level,
+ &type,
+ &data,
+ &data_len)) {
+ goto finished;
+ }
+
+ control_message->cmsg_level = level;
+ control_message->cmsg_type = type;
+ data_size = CMSG_LEN(data_len);
+
+ if (data_size > SOCKLEN_MAX) {
+ PyErr_Format(PyExc_OverflowError,
+ "CMSG_LEN(%d) > SOCKLEN_MAX", data_len);
+ goto finished;
+ }
+
+ control_message->cmsg_len = (socklen_t) data_size;
+
+ cmsg_data = CMSG_DATA(control_message);
+ memcpy(cmsg_data, data, data_len);
+
+ Py_DECREF(item);
+ item = NULL;
+
+ control_message = CMSG_NXTHDR(&message_header, control_message);
+ }
+ Py_DECREF(iterator);
+ iterator = NULL;
+
+ if (PyErr_Occurred()) {
+ goto finished;
+ }
+ }
+
+ sendmsg_result = sendmsg(fd, &message_header, flags);
+
+ if (sendmsg_result < 0) {
+ PyErr_SetFromErrno(sendmsg_socket_error);
+ goto finished;
+ }
+
+ result_object = Py_BuildValue("n", sendmsg_result);
+
+ finished:
+
+ if (item) {
+ Py_DECREF(item);
+ item = NULL;
+ }
+ if (iterator) {
+ Py_DECREF(iterator);
+ iterator = NULL;
+ }
+ if (message_header.msg_control) {
+ PyMem_Free(message_header.msg_control);
+ message_header.msg_control = NULL;
+ }
+ return result_object;
+}
+
+static PyObject *sendmsg_recvmsg(PyObject *self, PyObject *args, PyObject *keywds) {
+ int fd = -1;
+ int flags = 0;
+ int maxsize = 8192;
+ int cmsg_size = 4096;
+ size_t cmsg_space;
+ size_t cmsg_overhead;
+ Py_ssize_t recvmsg_result;
+
+ struct msghdr message_header;
+ struct cmsghdr *control_message;
+ struct iovec iov[1];
+ char *cmsgbuf;
+ PyObject *ancillary;
+ PyObject *final_result = NULL;
+
+ static char *kwlist[] = {"fd", "flags", "maxsize", "cmsg_size", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|iii:recvmsg", kwlist,
+ &fd, &flags, &maxsize, &cmsg_size)) {
+ return NULL;
+ }
+
+ cmsg_space = CMSG_SPACE(cmsg_size);
+
+ /* overflow check */
+ if (cmsg_space > SOCKLEN_MAX) {
+ PyErr_Format(PyExc_OverflowError,
+ "CMSG_SPACE(cmsg_size) greater than SOCKLEN_MAX: %d",
+ cmsg_size);
+ return NULL;
+ }
+
+ message_header.msg_name = NULL;
+ message_header.msg_namelen = 0;
+
+ iov[0].iov_len = maxsize;
+ iov[0].iov_base = PyMem_Malloc(maxsize);
+
+ if (!iov[0].iov_base) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ message_header.msg_iov = iov;
+ message_header.msg_iovlen = 1;
+
+ cmsgbuf = PyMem_Malloc(cmsg_space);
+
+ if (!cmsgbuf) {
+ PyMem_Free(iov[0].iov_base);
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ memset(cmsgbuf, 0, cmsg_space);
+ message_header.msg_control = cmsgbuf;
+ /* see above for overflow check */
+ message_header.msg_controllen = (socklen_t) cmsg_space;
+
+ recvmsg_result = recvmsg(fd, &message_header, flags);
+ if (recvmsg_result < 0) {
+ PyErr_SetFromErrno(sendmsg_socket_error);
+ goto finished;
+ }
+
+ ancillary = PyList_New(0);
+ if (!ancillary) {
+ goto finished;
+ }
+
+ for (control_message = CMSG_FIRSTHDR(&message_header);
+ control_message;
+ control_message = CMSG_NXTHDR(&message_header,
+ control_message)) {
+ PyObject *entry;
+
+ /* Some platforms apparently always fill out the ancillary data
+ structure with a single bogus value if none is provided; ignore it,
+ if that is the case. */
+
+ if ((!(control_message->cmsg_level)) &&
+ (!(control_message->cmsg_type))) {
+ continue;
+ }
+
+ /*
+ * Figure out how much of the cmsg size is cmsg structure overhead - in
+ * other words, how much is not part of the application data. This lets
+ * us compute the right application data size below. There should
+ * really be a CMSG_ macro for this.
+ */
+ cmsg_overhead = (char*)CMSG_DATA(control_message) - (char*)control_message;
+
+ entry = Py_BuildValue(
+ "(iis#)",
+ control_message->cmsg_level,
+ control_message->cmsg_type,
+ CMSG_DATA(control_message),
+ (Py_ssize_t) (control_message->cmsg_len - cmsg_overhead));
+
+ if (!entry) {
+ Py_DECREF(ancillary);
+ goto finished;
+ }
+
+ if (PyList_Append(ancillary, entry) < 0) {
+ Py_DECREF(ancillary);
+ Py_DECREF(entry);
+ goto finished;
+ } else {
+ Py_DECREF(entry);
+ }
+ }
+
+ final_result = Py_BuildValue(
+ "s#iO",
+ iov[0].iov_base,
+ recvmsg_result,
+ message_header.msg_flags,
+ ancillary);
+
+ Py_DECREF(ancillary);
+
+ finished:
+ PyMem_Free(iov[0].iov_base);
+ PyMem_Free(cmsgbuf);
+ return final_result;
+}
+
+static PyObject *sendmsg_getsockfam(PyObject *self, PyObject *args,
+ PyObject *keywds) {
+ int fd;
+ struct sockaddr sa;
+ static char *kwlist[] = {"fd", NULL};
+ if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", kwlist, &fd)) {
+ return NULL;
+ }
+ socklen_t sz = sizeof(sa);
+ if (getsockname(fd, &sa, &sz)) {
+ PyErr_SetFromErrno(sendmsg_socket_error);
+ return NULL;
+ }
+ return Py_BuildValue("i", sa.sa_family);
+}
diff --git a/twisted/python/shortcut.py b/twisted/python/shortcut.py
new file mode 100644
index 0000000..6d6546b
--- /dev/null
+++ b/twisted/python/shortcut.py
@@ -0,0 +1,76 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Creation of Windows shortcuts.
+
+Requires win32all.
+"""
+
+from win32com.shell import shell
+import pythoncom
+import os
+
+
+def open(filename):
+ """Open an existing shortcut for reading.
+
+ @return: The shortcut object
+ @rtype: Shortcut
+ """
+ sc=Shortcut()
+ sc.load(filename)
+ return sc
+
+
+class Shortcut:
+ """A shortcut on Win32.
+ >>> sc=Shortcut(path, arguments, description, workingdir, iconpath, iconidx)
+ @param path: Location of the target
+ @param arguments: If path points to an executable, optional arguments to
+ pass
+ @param description: Human-readable decription of target
+ @param workingdir: Directory from which target is launched
+ @param iconpath: Filename that contains an icon for the shortcut
+ @param iconidx: If iconpath is set, optional index of the icon desired
+ """
+
+ def __init__(self,
+ path=None,
+ arguments=None,
+ description=None,
+ workingdir=None,
+ iconpath=None,
+ iconidx=0):
+ self._base = pythoncom.CoCreateInstance(
+ shell.CLSID_ShellLink, None,
+ pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink
+ )
+ data = map(None,
+ ['"%s"' % os.path.abspath(path), arguments, description,
+ os.path.abspath(workingdir), os.path.abspath(iconpath)],
+ ("SetPath", "SetArguments", "SetDescription",
+ "SetWorkingDirectory") )
+ for value, function in data:
+ if value and function:
+ # call function on each non-null value
+ getattr(self, function)(value)
+ if iconpath:
+ self.SetIconLocation(iconpath, iconidx)
+
+ def load( self, filename ):
+ """Read a shortcut file from disk."""
+ self._base.QueryInterface(pythoncom.IID_IPersistFile).Load(filename)
+
+ def save( self, filename ):
+ """Write the shortcut to disk.
+
+ The file should be named something.lnk.
+ """
+ self._base.QueryInterface(pythoncom.IID_IPersistFile).Save(filename, 0)
+
+ def __getattr__( self, name ):
+ if name != "_base":
+ return getattr(self._base, name)
+ raise AttributeError, "%s instance has no attribute %s" % \
+ (self.__class__.__name__, name)
diff --git a/twisted/python/syslog.py b/twisted/python/syslog.py
new file mode 100644
index 0000000..88d8d02
--- /dev/null
+++ b/twisted/python/syslog.py
@@ -0,0 +1,107 @@
+# -*- test-case-name: twisted.python.test.test_syslog -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Classes and utility functions for integrating Twisted and syslog.
+
+You probably want to call L{startLogging}.
+"""
+
+syslog = __import__('syslog')
+
+from twisted.python import log
+
+# These defaults come from the Python 2.3 syslog docs.
+DEFAULT_OPTIONS = 0
+DEFAULT_FACILITY = syslog.LOG_USER
+
+
+
+class SyslogObserver:
+ """
+ A log observer for logging to syslog.
+
+ See L{twisted.python.log} for context.
+
+ This logObserver will automatically use LOG_ALERT priority for logged
+ failures (such as from C{log.err()}), but you can use any priority and
+ facility by setting the 'C{syslogPriority}' and 'C{syslogFacility}' keys in
+ the event dict.
+ """
+ openlog = syslog.openlog
+ syslog = syslog.syslog
+
+ def __init__(self, prefix, options=DEFAULT_OPTIONS,
+ facility=DEFAULT_FACILITY):
+ """
+ @type prefix: C{str}
+ @param prefix: The syslog prefix to use.
+
+ @type options: C{int}
+ @param options: A bitvector represented as an integer of the syslog
+ options to use.
+
+ @type facility: C{int}
+ @param facility: An indication to the syslog daemon of what sort of
+ program this is (essentially, an additional arbitrary metadata
+ classification for messages sent to syslog by this observer).
+ """
+ self.openlog(prefix, options, facility)
+
+
+ def emit(self, eventDict):
+ """
+ Send a message event to the I{syslog}.
+
+ @param eventDict: The event to send. If it has no C{'message'} key, it
+ will be ignored. Otherwise, if it has C{'syslogPriority'} and/or
+ C{'syslogFacility'} keys, these will be used as the syslog priority
+ and facility. If it has no C{'syslogPriority'} key but a true
+ value for the C{'isError'} key, the B{LOG_ALERT} priority will be
+ used; if it has a false value for C{'isError'}, B{LOG_INFO} will be
+ used. If the C{'message'} key is multiline, each line will be sent
+ to the syslog separately.
+ """
+ # Figure out what the message-text is.
+ text = log.textFromEventDict(eventDict)
+ if text is None:
+ return
+
+ # Figure out what syslog parameters we might need to use.
+ priority = syslog.LOG_INFO
+ facility = 0
+ if eventDict['isError']:
+ priority = syslog.LOG_ALERT
+ if 'syslogPriority' in eventDict:
+ priority = int(eventDict['syslogPriority'])
+ if 'syslogFacility' in eventDict:
+ facility = int(eventDict['syslogFacility'])
+
+ # Break the message up into lines and send them.
+ lines = text.split('\n')
+ while lines[-1:] == ['']:
+ lines.pop()
+
+ firstLine = True
+ for line in lines:
+ if firstLine:
+ firstLine = False
+ else:
+ line = '\t' + line
+ self.syslog(priority | facility,
+ '[%s] %s' % (eventDict['system'], line))
+
+
+
+def startLogging(prefix='Twisted', options=DEFAULT_OPTIONS,
+ facility=DEFAULT_FACILITY, setStdout=1):
+ """
+ Send all Twisted logging output to syslog from now on.
+
+ The prefix, options and facility arguments are passed to
+ C{syslog.openlog()}, see the Python syslog documentation for details. For
+ other parameters, see L{twisted.python.log.startLoggingWithObserver}.
+ """
+ obs = SyslogObserver(prefix, options, facility)
+ log.startLoggingWithObserver(obs.emit, setStdout=setStdout)
diff --git a/twisted/python/systemd.py b/twisted/python/systemd.py
new file mode 100644
index 0000000..d20fa04
--- /dev/null
+++ b/twisted/python/systemd.py
@@ -0,0 +1,87 @@
+# -*- test-case-name: twisted.python.test.test_systemd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Integration with systemd.
+
+Currently only the minimum APIs necessary for using systemd's socket activation
+feature are supported.
+"""
+
+__all__ = ['ListenFDs']
+
+from os import getpid
+
+
+class ListenFDs(object):
+ """
+ L{ListenFDs} provides access to file descriptors inherited from systemd.
+
+ Typically L{ListenFDs.fromEnvironment} should be used to construct a new
+ instance of L{ListenFDs}.
+
+ @cvar _START: File descriptors inherited from systemd are always
+ consecutively numbered, with a fixed lowest "starting" descriptor. This
+ gives the default starting descriptor. Since this must agree with the
+ value systemd is using, it typically should not be overridden.
+ @type _START: C{int}
+
+ @ivar _descriptors: A C{list} of C{int} giving the descriptors which were
+ inherited.
+ """
+ _START = 3
+
+ def __init__(self, descriptors):
+ """
+ @param descriptors: The descriptors which will be returned from calls to
+ C{inheritedDescriptors}.
+ """
+ self._descriptors = descriptors
+
+
+ @classmethod
+ def fromEnvironment(cls, environ=None, start=None):
+ """
+ @param environ: A dictionary-like object to inspect to discover
+ inherited descriptors. By default, C{None}, indicating that the
+ real process environment should be inspected. The default is
+ suitable for typical usage.
+
+ @param start: An integer giving the lowest value of an inherited
+ descriptor systemd will give us. By default, C{None}, indicating
+ the known correct (that is, in agreement with systemd) value will be
+ used. The default is suitable for typical usage.
+
+ @return: A new instance of C{cls} which can be used to look up the
+ descriptors which have been inherited.
+ """
+ if environ is None:
+ from os import environ
+ if start is None:
+ start = cls._START
+
+ descriptors = []
+
+ try:
+ pid = int(environ['LISTEN_PID'])
+ except (KeyError, ValueError):
+ pass
+ else:
+ if pid == getpid():
+ try:
+ count = int(environ['LISTEN_FDS'])
+ except (KeyError, ValueError):
+ pass
+ else:
+ descriptors = range(start, start + count)
+ del environ['LISTEN_PID'], environ['LISTEN_FDS']
+
+ return cls(descriptors)
+
+
+ def inheritedDescriptors(self):
+ """
+ @return: The configured list of descriptors.
+ """
+ return list(self._descriptors)
diff --git a/twisted/python/test/__init__.py b/twisted/python/test/__init__.py
new file mode 100644
index 0000000..cfdc40d
--- /dev/null
+++ b/twisted/python/test/__init__.py
@@ -0,0 +1,3 @@
+"""
+Unit tests for L{twisted.python}.
+"""
diff --git a/twisted/python/test/deprecatedattributes.py b/twisted/python/test/deprecatedattributes.py
new file mode 100644
index 0000000..b94c361
--- /dev/null
+++ b/twisted/python/test/deprecatedattributes.py
@@ -0,0 +1,21 @@
+# Import reflect first, so that circular imports (between deprecate and
+# reflect) don't cause headaches.
+import twisted.python.reflect
+from twisted.python.versions import Version
+from twisted.python.deprecate import deprecatedModuleAttribute
+
+
+# Known module-level attributes.
+DEPRECATED_ATTRIBUTE = 42
+ANOTHER_ATTRIBUTE = 'hello'
+
+
+version = Version('Twisted', 8, 0, 0)
+message = 'Oh noes!'
+
+
+deprecatedModuleAttribute(
+ version,
+ message,
+ __name__,
+ 'DEPRECATED_ATTRIBUTE')
diff --git a/twisted/python/test/modules_helpers.py b/twisted/python/test/modules_helpers.py
new file mode 100644
index 0000000..15ef6c1
--- /dev/null
+++ b/twisted/python/test/modules_helpers.py
@@ -0,0 +1,64 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Facilities for helping test code which interacts with L{twisted.python.modules},
+or uses Python's own module system to load code.
+"""
+
+import sys
+
+from twisted.trial.unittest import TestCase
+from twisted.python import modules
+from twisted.python.filepath import FilePath
+
+class TwistedModulesTestCase(TestCase):
+
+ def findByIteration(self, modname, where=modules, importPackages=False):
+ """
+ You don't ever actually want to do this, so it's not in the public API, but
+ sometimes we want to compare the result of an iterative call with a
+ lookup call and make sure they're the same for test purposes.
+ """
+ for modinfo in where.walkModules(importPackages=importPackages):
+ if modinfo.name == modname:
+ return modinfo
+ self.fail("Unable to find module %r through iteration." % (modname,))
+
+
+ def replaceSysPath(self, sysPath):
+ """
+ Replace sys.path, for the duration of the test, with the given value.
+ """
+ originalSysPath = sys.path[:]
+ def cleanUpSysPath():
+ sys.path[:] = originalSysPath
+ self.addCleanup(cleanUpSysPath)
+ sys.path[:] = sysPath
+
+
+ def replaceSysModules(self, sysModules):
+ """
+ Replace sys.modules, for the duration of the test, with the given value.
+ """
+ originalSysModules = sys.modules.copy()
+ def cleanUpSysModules():
+ sys.modules.clear()
+ sys.modules.update(originalSysModules)
+ self.addCleanup(cleanUpSysModules)
+ sys.modules.clear()
+ sys.modules.update(sysModules)
+
+
+ def pathEntryWithOnePackage(self, pkgname="test_package"):
+ """
+ Generate a L{FilePath} with one package, named C{pkgname}, on it, and
+ return the L{FilePath} of the path entry.
+ """
+ entry = FilePath(self.mktemp())
+ pkg = entry.child("test_package")
+ pkg.makedirs()
+ pkg.child("__init__.py").setContent("")
+ return entry
+
+
diff --git a/twisted/python/test/pullpipe.py b/twisted/python/test/pullpipe.py
new file mode 100644
index 0000000..e606662
--- /dev/null
+++ b/twisted/python/test/pullpipe.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+# -*- test-case-name: twisted.python.test.test_sendmsg -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os
+from struct import unpack
+
+# This makes me sad. Why aren't things nice?
+sys.path.insert(0, __file__.rsplit('/', 4)[0])
+
+from twisted.python.sendmsg import recv1msg
+
+def recvfd(socketfd):
+ """
+ Receive a file descriptor from a L{send1msg} message on the given C{AF_UNIX}
+ socket.
+
+ @param socketfd: An C{AF_UNIX} socket, attached to another process waiting
+ to send sockets via the ancillary data mechanism in L{send1msg}.
+
+ @param fd: C{int}
+
+ @return: a 2-tuple of (new file descriptor, description).
+
+ @rtype: 2-tuple of (C{int}, C{str})
+ """
+ data, flags, ancillary = recv1msg(socketfd)
+ [(cmsg_level, cmsg_type, packedFD)] = ancillary
+ # cmsg_level and cmsg_type really need to be SOL_SOCKET / SCM_RIGHTS, but
+ # since those are the *only* standard values, there's not much point in
+ # checking.
+ [unpackedFD] = unpack("i", packedFD)
+ return (unpackedFD, data)
+
+
+if __name__ == '__main__':
+ fd, description = recvfd(int(sys.argv[1]))
+ os.write(fd, "Test fixture data: %s.\n" % (description,))
+ os.close(fd)
diff --git a/twisted/python/test/test_components.py b/twisted/python/test/test_components.py
new file mode 100644
index 0000000..c4c1b45
--- /dev/null
+++ b/twisted/python/test/test_components.py
@@ -0,0 +1,770 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for Twisted component architecture.
+"""
+
+from zope.interface import Interface, implements, Attribute
+from zope.interface.adapter import AdapterRegistry
+
+from twisted.trial import unittest
+from twisted.python import components
+from twisted.python.components import _addHook, _removeHook, proxyForInterface
+
+
+class InterfacesTestCase(unittest.TestCase):
+ """Test interfaces."""
+
+class Compo(components.Componentized):
+ num = 0
+ def inc(self):
+ self.num = self.num + 1
+ return self.num
+
+class IAdept(Interface):
+ def adaptorFunc():
+ raise NotImplementedError()
+
+class IElapsed(Interface):
+ def elapsedFunc():
+ """
+ 1!
+ """
+
+class Adept(components.Adapter):
+ implements(IAdept)
+ def __init__(self, orig):
+ self.original = orig
+ self.num = 0
+ def adaptorFunc(self):
+ self.num = self.num + 1
+ return self.num, self.original.inc()
+
+class Elapsed(components.Adapter):
+ implements(IElapsed)
+ def elapsedFunc(self):
+ return 1
+
+class AComp(components.Componentized):
+ pass
+class BComp(AComp):
+ pass
+class CComp(BComp):
+ pass
+
+class ITest(Interface):
+ pass
+class ITest2(Interface):
+ pass
+class ITest3(Interface):
+ pass
+class ITest4(Interface):
+ pass
+class Test(components.Adapter):
+ implements(ITest, ITest3, ITest4)
+ def __init__(self, orig):
+ pass
+class Test2:
+ implements(ITest2)
+ temporaryAdapter = 1
+ def __init__(self, orig):
+ pass
+
+
+
+class RegistryUsingMixin(object):
+ """
+ Mixin for test cases which modify the global registry somehow.
+ """
+ def setUp(self):
+ """
+ Configure L{twisted.python.components.registerAdapter} to mutate an
+ alternate registry to improve test isolation.
+ """
+ # Create a brand new, empty registry and put it onto the components
+ # module where registerAdapter will use it. Also ensure that it goes
+ # away at the end of the test.
+ scratchRegistry = AdapterRegistry()
+ self.patch(components, 'globalRegistry', scratchRegistry)
+ # Hook the new registry up to the adapter lookup system and ensure that
+ # association is also discarded after the test.
+ hook = _addHook(scratchRegistry)
+ self.addCleanup(_removeHook, hook)
+
+
+
+class ComponentizedTestCase(unittest.TestCase, RegistryUsingMixin):
+ """
+ Simple test case for caching in Componentized.
+ """
+ def setUp(self):
+ RegistryUsingMixin.setUp(self)
+
+ components.registerAdapter(Test, AComp, ITest)
+ components.registerAdapter(Test, AComp, ITest3)
+ components.registerAdapter(Test2, AComp, ITest2)
+
+
+ def testComponentized(self):
+ components.registerAdapter(Adept, Compo, IAdept)
+ components.registerAdapter(Elapsed, Compo, IElapsed)
+
+ c = Compo()
+ assert c.getComponent(IAdept).adaptorFunc() == (1, 1)
+ assert c.getComponent(IAdept).adaptorFunc() == (2, 2)
+ assert IElapsed(IAdept(c)).elapsedFunc() == 1
+
+ def testInheritanceAdaptation(self):
+ c = CComp()
+ co1 = c.getComponent(ITest)
+ co2 = c.getComponent(ITest)
+ co3 = c.getComponent(ITest2)
+ co4 = c.getComponent(ITest2)
+ assert co1 is co2
+ assert co3 is not co4
+ c.removeComponent(co1)
+ co5 = c.getComponent(ITest)
+ co6 = c.getComponent(ITest)
+ assert co5 is co6
+ assert co1 is not co5
+
+ def testMultiAdapter(self):
+ c = CComp()
+ co1 = c.getComponent(ITest)
+ co2 = c.getComponent(ITest2)
+ co3 = c.getComponent(ITest3)
+ co4 = c.getComponent(ITest4)
+ self.assertIdentical(None, co4)
+ self.assertIdentical(co1, co3)
+
+
+ def test_getComponentDefaults(self):
+ """
+ Test that a default value specified to Componentized.getComponent if
+ there is no component for the requested interface.
+ """
+ componentized = components.Componentized()
+ default = object()
+ self.assertIdentical(
+ componentized.getComponent(ITest, default),
+ default)
+ self.assertIdentical(
+ componentized.getComponent(ITest, default=default),
+ default)
+ self.assertIdentical(
+ componentized.getComponent(ITest),
+ None)
+
+
+
+class AdapterTestCase(unittest.TestCase):
+ """Test adapters."""
+
+ def testAdapterGetComponent(self):
+ o = object()
+ a = Adept(o)
+ self.assertRaises(components.CannotAdapt, ITest, a)
+ self.assertEqual(ITest(a, None), None)
+
+
+
+class IMeta(Interface):
+ pass
+
+class MetaAdder(components.Adapter):
+ implements(IMeta)
+ def add(self, num):
+ return self.original.num + num
+
+class BackwardsAdder(components.Adapter):
+ implements(IMeta)
+ def add(self, num):
+ return self.original.num - num
+
+class MetaNumber:
+ def __init__(self, num):
+ self.num = num
+
+class FakeAdder:
+ def add(self, num):
+ return num + 5
+
+class FakeNumber:
+ num = 3
+
+class ComponentNumber(components.Componentized):
+ def __init__(self):
+ self.num = 0
+ components.Componentized.__init__(self)
+
+class ComponentMeta(components.Adapter):
+ implements(IMeta)
+ def __init__(self, original):
+ components.Adapter.__init__(self, original)
+ self.num = self.original.num
+
+class ComponentAdder(ComponentMeta):
+ def add(self, num):
+ self.num += num
+ return self.num
+
+class ComponentDoubler(ComponentMeta):
+ def add(self, num):
+ self.num += (num * 2)
+ return self.original.num
+
+class IAttrX(Interface):
+ def x():
+ pass
+
+class IAttrXX(Interface):
+ def xx():
+ pass
+
+class Xcellent:
+ implements(IAttrX)
+ def x(self):
+ return 'x!'
+
+class DoubleXAdapter:
+ num = 42
+ def __init__(self, original):
+ self.original = original
+ def xx(self):
+ return (self.original.x(), self.original.x())
+ def __cmp__(self, other):
+ return cmp(self.num, other.num)
+
+
+class TestMetaInterface(RegistryUsingMixin, unittest.TestCase):
+ def testBasic(self):
+ components.registerAdapter(MetaAdder, MetaNumber, IMeta)
+ n = MetaNumber(1)
+ self.assertEqual(IMeta(n).add(1), 2)
+
+ def testComponentizedInteraction(self):
+ components.registerAdapter(ComponentAdder, ComponentNumber, IMeta)
+ c = ComponentNumber()
+ IMeta(c).add(1)
+ IMeta(c).add(1)
+ self.assertEqual(IMeta(c).add(1), 3)
+
+ def testAdapterWithCmp(self):
+ # Make sure that a __cmp__ on an adapter doesn't break anything
+ components.registerAdapter(DoubleXAdapter, IAttrX, IAttrXX)
+ xx = IAttrXX(Xcellent())
+ self.assertEqual(('x!', 'x!'), xx.xx())
+
+
+class RegistrationTestCase(RegistryUsingMixin, unittest.TestCase):
+ """
+ Tests for adapter registration.
+ """
+ def _registerAdapterForClassOrInterface(self, original):
+ """
+ Register an adapter with L{components.registerAdapter} for the given
+ class or interface and verify that the adapter can be looked up with
+ L{components.getAdapterFactory}.
+ """
+ adapter = lambda o: None
+ components.registerAdapter(adapter, original, ITest)
+ self.assertIdentical(
+ components.getAdapterFactory(original, ITest, None),
+ adapter)
+
+
+ def test_registerAdapterForClass(self):
+ """
+ Test that an adapter from a class can be registered and then looked
+ up.
+ """
+ class TheOriginal(object):
+ pass
+ return self._registerAdapterForClassOrInterface(TheOriginal)
+
+
+ def test_registerAdapterForInterface(self):
+ """
+ Test that an adapter from an interface can be registered and then
+ looked up.
+ """
+ return self._registerAdapterForClassOrInterface(ITest2)
+
+
+ def _duplicateAdapterForClassOrInterface(self, original):
+ """
+ Verify that L{components.registerAdapter} raises L{ValueError} if the
+ from-type/interface and to-interface pair is not unique.
+ """
+ firstAdapter = lambda o: False
+ secondAdapter = lambda o: True
+ components.registerAdapter(firstAdapter, original, ITest)
+ self.assertRaises(
+ ValueError,
+ components.registerAdapter,
+ secondAdapter, original, ITest)
+ # Make sure that the original adapter is still around as well
+ self.assertIdentical(
+ components.getAdapterFactory(original, ITest, None),
+ firstAdapter)
+
+
+ def test_duplicateAdapterForClass(self):
+ """
+ Test that attempting to register a second adapter from a class
+ raises the appropriate exception.
+ """
+ class TheOriginal(object):
+ pass
+ return self._duplicateAdapterForClassOrInterface(TheOriginal)
+
+
+ def test_duplicateAdapterForInterface(self):
+ """
+ Test that attempting to register a second adapter from an interface
+ raises the appropriate exception.
+ """
+ return self._duplicateAdapterForClassOrInterface(ITest2)
+
+
+ def _duplicateAdapterForClassOrInterfaceAllowed(self, original):
+ """
+ Verify that when C{components.ALLOW_DUPLICATES} is set to C{True}, new
+ adapter registrations for a particular from-type/interface and
+ to-interface pair replace older registrations.
+ """
+ firstAdapter = lambda o: False
+ secondAdapter = lambda o: True
+ class TheInterface(Interface):
+ pass
+ components.registerAdapter(firstAdapter, original, TheInterface)
+ components.ALLOW_DUPLICATES = True
+ try:
+ components.registerAdapter(secondAdapter, original, TheInterface)
+ self.assertIdentical(
+ components.getAdapterFactory(original, TheInterface, None),
+ secondAdapter)
+ finally:
+ components.ALLOW_DUPLICATES = False
+
+ # It should be rejected again at this point
+ self.assertRaises(
+ ValueError,
+ components.registerAdapter,
+ firstAdapter, original, TheInterface)
+
+ self.assertIdentical(
+ components.getAdapterFactory(original, TheInterface, None),
+ secondAdapter)
+
+
+ def test_duplicateAdapterForClassAllowed(self):
+ """
+ Test that when L{components.ALLOW_DUPLICATES} is set to a true
+ value, duplicate registrations from classes are allowed to override
+ the original registration.
+ """
+ class TheOriginal(object):
+ pass
+ return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
+
+
+ def test_duplicateAdapterForInterfaceAllowed(self):
+ """
+ Test that when L{components.ALLOW_DUPLICATES} is set to a true
+ value, duplicate registrations from interfaces are allowed to
+ override the original registration.
+ """
+ class TheOriginal(Interface):
+ pass
+ return self._duplicateAdapterForClassOrInterfaceAllowed(TheOriginal)
+
+
+ def _multipleInterfacesForClassOrInterface(self, original):
+ """
+ Verify that an adapter can be registered for multiple to-interfaces at a
+ time.
+ """
+ adapter = lambda o: None
+ components.registerAdapter(adapter, original, ITest, ITest2)
+ self.assertIdentical(
+ components.getAdapterFactory(original, ITest, None), adapter)
+ self.assertIdentical(
+ components.getAdapterFactory(original, ITest2, None), adapter)
+
+
+ def test_multipleInterfacesForClass(self):
+ """
+ Test the registration of an adapter from a class to several
+ interfaces at once.
+ """
+ class TheOriginal(object):
+ pass
+ return self._multipleInterfacesForClassOrInterface(TheOriginal)
+
+
+ def test_multipleInterfacesForInterface(self):
+ """
+ Test the registration of an adapter from an interface to several
+ interfaces at once.
+ """
+ return self._multipleInterfacesForClassOrInterface(ITest3)
+
+
+ def _subclassAdapterRegistrationForClassOrInterface(self, original):
+ """
+ Verify that a new adapter can be registered for a particular
+ to-interface from a subclass of a type or interface which already has an
+ adapter registered to that interface and that the subclass adapter takes
+ precedence over the base class adapter.
+ """
+ firstAdapter = lambda o: True
+ secondAdapter = lambda o: False
+ class TheSubclass(original):
+ pass
+ components.registerAdapter(firstAdapter, original, ITest)
+ components.registerAdapter(secondAdapter, TheSubclass, ITest)
+ self.assertIdentical(
+ components.getAdapterFactory(original, ITest, None),
+ firstAdapter)
+ self.assertIdentical(
+ components.getAdapterFactory(TheSubclass, ITest, None),
+ secondAdapter)
+
+
+ def test_subclassAdapterRegistrationForClass(self):
+ """
+ Test that an adapter to a particular interface can be registered
+ from both a class and its subclass.
+ """
+ class TheOriginal(object):
+ pass
+ return self._subclassAdapterRegistrationForClassOrInterface(TheOriginal)
+
+
+ def test_subclassAdapterRegistrationForInterface(self):
+ """
+ Test that an adapter to a particular interface can be registered
+ from both an interface and its subclass.
+ """
+ return self._subclassAdapterRegistrationForClassOrInterface(ITest2)
+
+
+
+class IProxiedInterface(Interface):
+ """
+ An interface class for use by L{proxyForInterface}.
+ """
+
+ ifaceAttribute = Attribute("""
+ An example declared attribute, which should be proxied.""")
+
+ def yay(*a, **kw):
+ """
+ A sample method which should be proxied.
+ """
+
+class IProxiedSubInterface(IProxiedInterface):
+ """
+ An interface that derives from another for use with L{proxyForInterface}.
+ """
+
+ def boo(self):
+ """
+ A different sample method which should be proxied.
+ """
+
+
+
+class Yayable(object):
+ """
+ A provider of L{IProxiedInterface} which increments a counter for
+ every call to C{yay}.
+
+ @ivar yays: The number of times C{yay} has been called.
+ """
+ implements(IProxiedInterface)
+
+ def __init__(self):
+ self.yays = 0
+ self.yayArgs = []
+
+ def yay(self, *a, **kw):
+ """
+ Increment C{self.yays}.
+ """
+ self.yays += 1
+ self.yayArgs.append((a, kw))
+ return self.yays
+
+
+class Booable(object):
+ """
+ An implementation of IProxiedSubInterface
+ """
+ implements(IProxiedSubInterface)
+ yayed = False
+ booed = False
+ def yay(self):
+ """
+ Mark the fact that 'yay' has been called.
+ """
+ self.yayed = True
+
+
+ def boo(self):
+ """
+ Mark the fact that 'boo' has been called.1
+ """
+ self.booed = True
+
+
+
+class IMultipleMethods(Interface):
+ """
+ An interface with multiple methods.
+ """
+
+ def methodOne():
+ """
+ The first method. Should return 1.
+ """
+
+ def methodTwo():
+ """
+ The second method. Should return 2.
+ """
+
+
+
+class MultipleMethodImplementor(object):
+ """
+ A precise implementation of L{IMultipleMethods}.
+ """
+
+ def methodOne(self):
+ """
+ @return: 1
+ """
+ return 1
+
+
+ def methodTwo(self):
+ """
+ @return: 2
+ """
+ return 2
+
+
+
+class ProxyForInterfaceTests(unittest.TestCase):
+ """
+ Tests for L{proxyForInterface}.
+ """
+
+ def test_original(self):
+ """
+ Proxy objects should have an C{original} attribute which refers to the
+ original object passed to the constructor.
+ """
+ original = object()
+ proxy = proxyForInterface(IProxiedInterface)(original)
+ self.assertIdentical(proxy.original, original)
+
+
+ def test_proxyMethod(self):
+ """
+ The class created from L{proxyForInterface} passes methods on an
+ interface to the object which is passed to its constructor.
+ """
+ klass = proxyForInterface(IProxiedInterface)
+ yayable = Yayable()
+ proxy = klass(yayable)
+ proxy.yay()
+ self.assertEqual(proxy.yay(), 2)
+ self.assertEqual(yayable.yays, 2)
+
+
+ def test_proxyAttribute(self):
+ """
+ Proxy objects should proxy declared attributes, but not other
+ attributes.
+ """
+ yayable = Yayable()
+ yayable.ifaceAttribute = object()
+ proxy = proxyForInterface(IProxiedInterface)(yayable)
+ self.assertIdentical(proxy.ifaceAttribute, yayable.ifaceAttribute)
+ self.assertRaises(AttributeError, lambda: proxy.yays)
+
+
+ def test_proxySetAttribute(self):
+ """
+ The attributes that proxy objects proxy should be assignable and affect
+ the original object.
+ """
+ yayable = Yayable()
+ proxy = proxyForInterface(IProxiedInterface)(yayable)
+ thingy = object()
+ proxy.ifaceAttribute = thingy
+ self.assertIdentical(yayable.ifaceAttribute, thingy)
+
+
+ def test_proxyDeleteAttribute(self):
+ """
+ The attributes that proxy objects proxy should be deletable and affect
+ the original object.
+ """
+ yayable = Yayable()
+ yayable.ifaceAttribute = None
+ proxy = proxyForInterface(IProxiedInterface)(yayable)
+ del proxy.ifaceAttribute
+ self.assertFalse(hasattr(yayable, 'ifaceAttribute'))
+
+
+ def test_multipleMethods(self):
+ """
+ [Regression test] The proxy should send its method calls to the correct
+ method, not the incorrect one.
+ """
+ multi = MultipleMethodImplementor()
+ proxy = proxyForInterface(IMultipleMethods)(multi)
+ self.assertEqual(proxy.methodOne(), 1)
+ self.assertEqual(proxy.methodTwo(), 2)
+
+
+ def test_subclassing(self):
+ """
+ It is possible to subclass the result of L{proxyForInterface}.
+ """
+
+ class SpecializedProxy(proxyForInterface(IProxiedInterface)):
+ """
+ A specialized proxy which can decrement the number of yays.
+ """
+ def boo(self):
+ """
+ Decrement the number of yays.
+ """
+ self.original.yays -= 1
+
+ yayable = Yayable()
+ special = SpecializedProxy(yayable)
+ self.assertEqual(yayable.yays, 0)
+ special.boo()
+ self.assertEqual(yayable.yays, -1)
+
+
+ def test_proxyName(self):
+ """
+ The name of a proxy class indicates which interface it proxies.
+ """
+ proxy = proxyForInterface(IProxiedInterface)
+ self.assertEqual(
+ proxy.__name__,
+ "(Proxy for "
+ "twisted.python.test.test_components.IProxiedInterface)")
+
+
+ def test_implements(self):
+ """
+ The resulting proxy implements the interface that it proxies.
+ """
+ proxy = proxyForInterface(IProxiedInterface)
+ self.assertTrue(IProxiedInterface.implementedBy(proxy))
+
+
+ def test_proxyDescriptorGet(self):
+ """
+ _ProxyDescriptor's __get__ method should return the appropriate
+ attribute of its argument's 'original' attribute if it is invoked with
+ an object. If it is invoked with None, it should return a false
+ class-method emulator instead.
+
+ For some reason, Python's documentation recommends to define
+ descriptors' __get__ methods with the 'type' parameter as optional,
+ despite the fact that Python itself never actually calls the descriptor
+ that way. This is probably do to support 'foo.__get__(bar)' as an
+ idiom. Let's make sure that the behavior is correct. Since we don't
+ actually use the 'type' argument at all, this test calls it the
+ idiomatic way to ensure that signature works; test_proxyInheritance
+ verifies the how-Python-actually-calls-it signature.
+ """
+ class Sample:
+ called = False
+ def hello(self):
+ self.called = True
+ fakeProxy = Sample()
+ testObject = Sample()
+ fakeProxy.original = testObject
+ pd = components._ProxyDescriptor("hello", "original")
+ self.assertEqual(pd.__get__(fakeProxy), testObject.hello)
+ fakeClassMethod = pd.__get__(None)
+ fakeClassMethod(fakeProxy)
+ self.failUnless(testObject.called)
+
+
+ def test_proxyInheritance(self):
+ """
+ Subclasses of the class returned from L{proxyForInterface} should be
+ able to upcall methods by reference to their superclass, as any normal
+ Python class can.
+ """
+ class YayableWrapper(proxyForInterface(IProxiedInterface)):
+ """
+ This class does not override any functionality.
+ """
+
+ class EnhancedWrapper(YayableWrapper):
+ """
+ This class overrides the 'yay' method.
+ """
+ wrappedYays = 1
+ def yay(self, *a, **k):
+ self.wrappedYays += 1
+ return YayableWrapper.yay(self, *a, **k) + 7
+
+ yayable = Yayable()
+ wrapper = EnhancedWrapper(yayable)
+ self.assertEqual(wrapper.yay(3, 4, x=5, y=6), 8)
+ self.assertEqual(yayable.yayArgs,
+ [((3, 4), dict(x=5, y=6))])
+
+
+ def test_interfaceInheritance(self):
+ """
+ Proxies of subinterfaces generated with proxyForInterface should allow
+ access to attributes of both the child and the base interfaces.
+ """
+ proxyClass = proxyForInterface(IProxiedSubInterface)
+ booable = Booable()
+ proxy = proxyClass(booable)
+ proxy.yay()
+ proxy.boo()
+ self.failUnless(booable.yayed)
+ self.failUnless(booable.booed)
+
+
+ def test_attributeCustomization(self):
+ """
+ The original attribute name can be customized via the
+ C{originalAttribute} argument of L{proxyForInterface}: the attribute
+ should change, but the methods of the original object should still be
+ callable, and the attributes still accessible.
+ """
+ yayable = Yayable()
+ yayable.ifaceAttribute = object()
+ proxy = proxyForInterface(
+ IProxiedInterface, originalAttribute='foo')(yayable)
+ self.assertIdentical(proxy.foo, yayable)
+
+ # Check the behavior
+ self.assertEqual(proxy.yay(), 1)
+ self.assertIdentical(proxy.ifaceAttribute, yayable.ifaceAttribute)
+ thingy = object()
+ proxy.ifaceAttribute = thingy
+ self.assertIdentical(yayable.ifaceAttribute, thingy)
+ del proxy.ifaceAttribute
+ self.assertFalse(hasattr(yayable, 'ifaceAttribute'))
+
diff --git a/twisted/python/test/test_constants.py b/twisted/python/test/test_constants.py
new file mode 100644
index 0000000..870d342
--- /dev/null
+++ b/twisted/python/test/test_constants.py
@@ -0,0 +1,778 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Unit tests for L{twisted.python.constants}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.constants import (
+ NamedConstant, Names, ValueConstant, Values, FlagConstant, Flags)
+
+
+class NamedConstantTests(TestCase):
+ """
+ Tests for the L{twisted.python.constants.NamedConstant} class which is used
+ to represent individual values.
+ """
+ def setUp(self):
+ """
+ Create a dummy container into which constants can be placed.
+ """
+ class foo(Names):
+ pass
+ self.container = foo
+
+
+ def test_name(self):
+ """
+ The C{name} attribute of a L{NamedConstant} refers to the value passed
+ for the C{name} parameter to C{_realize}.
+ """
+ name = NamedConstant()
+ name._realize(self.container, "bar", None)
+ self.assertEqual("bar", name.name)
+
+
+ def test_representation(self):
+ """
+ The string representation of an instance of L{NamedConstant} includes
+ the container the instances belongs to as well as the instance's name.
+ """
+ name = NamedConstant()
+ name._realize(self.container, "bar", None)
+ self.assertEqual("<foo=bar>", repr(name))
+
+
+ def test_equality(self):
+ """
+ A L{NamedConstant} instance compares equal to itself.
+ """
+ name = NamedConstant()
+ name._realize(self.container, "bar", None)
+ self.assertTrue(name == name)
+ self.assertFalse(name != name)
+
+
+ def test_nonequality(self):
+ """
+ Two different L{NamedConstant} instances do not compare equal to each
+ other.
+ """
+ first = NamedConstant()
+ first._realize(self.container, "bar", None)
+ second = NamedConstant()
+ second._realize(self.container, "bar", None)
+ self.assertFalse(first == second)
+ self.assertTrue(first != second)
+
+
+ def test_hash(self):
+ """
+ Because two different L{NamedConstant} instances do not compare as equal
+ to each other, they also have different hashes to avoid collisions when
+ added to a C{dict} or C{set}.
+ """
+ first = NamedConstant()
+ first._realize(self.container, "bar", None)
+ second = NamedConstant()
+ second._realize(self.container, "bar", None)
+ self.assertNotEqual(hash(first), hash(second))
+
+
+
+class _ConstantsTestsMixin(object):
+ """
+ Mixin defining test helpers common to multiple types of constants
+ collections.
+ """
+ def _notInstantiableTest(self, name, cls):
+ """
+ Assert that an attempt to instantiate the constants class raises
+ C{TypeError}.
+
+ @param name: A C{str} giving the name of the constants collection.
+ @param cls: The constants class to test.
+ """
+ exc = self.assertRaises(TypeError, cls)
+ self.assertEqual(name + " may not be instantiated.", str(exc))
+
+
+
+class NamesTests(TestCase, _ConstantsTestsMixin):
+ """
+ Tests for L{twisted.python.constants.Names}, a base class for containers of
+ related constaints.
+ """
+ def setUp(self):
+ """
+ Create a fresh new L{Names} subclass for each unit test to use. Since
+ L{Names} is stateful, re-using the same subclass across test methods
+ makes exercising all of the implementation code paths difficult.
+ """
+ class METHOD(Names):
+ """
+ A container for some named constants to use in unit tests for
+ L{Names}.
+ """
+ GET = NamedConstant()
+ PUT = NamedConstant()
+ POST = NamedConstant()
+ DELETE = NamedConstant()
+
+ self.METHOD = METHOD
+
+
+ def test_notInstantiable(self):
+ """
+ A subclass of L{Names} raises C{TypeError} if an attempt is made to
+ instantiate it.
+ """
+ self._notInstantiableTest("METHOD", self.METHOD)
+
+
+ def test_symbolicAttributes(self):
+ """
+ Each name associated with a L{NamedConstant} instance in the definition
+ of a L{Names} subclass is available as an attribute on the resulting
+ class.
+ """
+ self.assertTrue(hasattr(self.METHOD, "GET"))
+ self.assertTrue(hasattr(self.METHOD, "PUT"))
+ self.assertTrue(hasattr(self.METHOD, "POST"))
+ self.assertTrue(hasattr(self.METHOD, "DELETE"))
+
+
+ def test_withoutOtherAttributes(self):
+ """
+ As usual, names not defined in the class scope of a L{Names}
+ subclass are not available as attributes on the resulting class.
+ """
+ self.assertFalse(hasattr(self.METHOD, "foo"))
+
+
+ def test_representation(self):
+ """
+ The string representation of a constant on a L{Names} subclass includes
+ the name of the L{Names} subclass and the name of the constant itself.
+ """
+ self.assertEqual("<METHOD=GET>", repr(self.METHOD.GET))
+
+
+ def test_lookupByName(self):
+ """
+ Constants can be looked up by name using L{Names.lookupByName}.
+ """
+ method = self.METHOD.lookupByName("GET")
+ self.assertIdentical(self.METHOD.GET, method)
+
+
+ def test_notLookupMissingByName(self):
+ """
+ Names not defined with a L{NamedConstant} instance cannot be looked up
+ using L{Names.lookupByName}.
+ """
+ self.assertRaises(ValueError, self.METHOD.lookupByName, "lookupByName")
+ self.assertRaises(ValueError, self.METHOD.lookupByName, "__init__")
+ self.assertRaises(ValueError, self.METHOD.lookupByName, "foo")
+
+
+ def test_name(self):
+ """
+ The C{name} attribute of one of the named constants gives that
+ constant's name.
+ """
+ self.assertEqual("GET", self.METHOD.GET.name)
+
+
+ def test_attributeIdentity(self):
+ """
+ Repeated access of an attribute associated with a L{NamedConstant} value
+ in a L{Names} subclass results in the same object.
+ """
+ self.assertIdentical(self.METHOD.GET, self.METHOD.GET)
+
+
+ def test_iterconstants(self):
+ """
+ L{Names.iterconstants} returns an iterator over all of the constants
+ defined in the class, in the order they were defined.
+ """
+ constants = list(self.METHOD.iterconstants())
+ self.assertEqual(
+ [self.METHOD.GET, self.METHOD.PUT,
+ self.METHOD.POST, self.METHOD.DELETE],
+ constants)
+
+
+ def test_attributeIterconstantsIdentity(self):
+ """
+ The constants returned from L{Names.iterconstants} are identical to the
+ constants accessible using attributes.
+ """
+ constants = list(self.METHOD.iterconstants())
+ self.assertIdentical(self.METHOD.GET, constants[0])
+ self.assertIdentical(self.METHOD.PUT, constants[1])
+ self.assertIdentical(self.METHOD.POST, constants[2])
+ self.assertIdentical(self.METHOD.DELETE, constants[3])
+
+
+ def test_iterconstantsIdentity(self):
+ """
+ The constants returned from L{Names.iterconstants} are identical on each
+ call to that method.
+ """
+ constants = list(self.METHOD.iterconstants())
+ again = list(self.METHOD.iterconstants())
+ self.assertIdentical(again[0], constants[0])
+ self.assertIdentical(again[1], constants[1])
+ self.assertIdentical(again[2], constants[2])
+ self.assertIdentical(again[3], constants[3])
+
+
+ def test_initializedOnce(self):
+ """
+ L{Names._enumerants} is initialized once and its value re-used on
+ subsequent access.
+ """
+ first = self.METHOD._enumerants
+ self.METHOD.GET # Side-effects!
+ second = self.METHOD._enumerants
+ self.assertIdentical(first, second)
+
+
+
+class ValuesTests(TestCase, _ConstantsTestsMixin):
+ """
+ Tests for L{twisted.python.constants.Names}, a base class for containers of
+ related constaints with arbitrary values.
+ """
+ def setUp(self):
+ """
+ Create a fresh new L{Values} subclass for each unit test to use. Since
+ L{Values} is stateful, re-using the same subclass across test methods
+ makes exercising all of the implementation code paths difficult.
+ """
+ class STATUS(Values):
+ OK = ValueConstant("200")
+ NOT_FOUND = ValueConstant("404")
+
+ self.STATUS = STATUS
+
+
+ def test_notInstantiable(self):
+ """
+ A subclass of L{Values} raises C{TypeError} if an attempt is made to
+ instantiate it.
+ """
+ self._notInstantiableTest("STATUS", self.STATUS)
+
+
+ def test_symbolicAttributes(self):
+ """
+ Each name associated with a L{ValueConstant} instance in the definition
+ of a L{Values} subclass is available as an attribute on the resulting
+ class.
+ """
+ self.assertTrue(hasattr(self.STATUS, "OK"))
+ self.assertTrue(hasattr(self.STATUS, "NOT_FOUND"))
+
+
+ def test_withoutOtherAttributes(self):
+ """
+ As usual, names not defined in the class scope of a L{Values}
+ subclass are not available as attributes on the resulting class.
+ """
+ self.assertFalse(hasattr(self.STATUS, "foo"))
+
+
+ def test_representation(self):
+ """
+ The string representation of a constant on a L{Values} subclass includes
+ the name of the L{Values} subclass and the name of the constant itself.
+ """
+ self.assertEqual("<STATUS=OK>", repr(self.STATUS.OK))
+
+
+ def test_lookupByName(self):
+ """
+ Constants can be looked up by name using L{Values.lookupByName}.
+ """
+ method = self.STATUS.lookupByName("OK")
+ self.assertIdentical(self.STATUS.OK, method)
+
+
+ def test_notLookupMissingByName(self):
+ """
+ Names not defined with a L{ValueConstant} instance cannot be looked up
+ using L{Values.lookupByName}.
+ """
+ self.assertRaises(ValueError, self.STATUS.lookupByName, "lookupByName")
+ self.assertRaises(ValueError, self.STATUS.lookupByName, "__init__")
+ self.assertRaises(ValueError, self.STATUS.lookupByName, "foo")
+
+
+ def test_lookupByValue(self):
+ """
+ Constants can be looked up by their associated value, defined by the
+ argument passed to L{ValueConstant}, using L{Values.lookupByValue}.
+ """
+ status = self.STATUS.lookupByValue("200")
+ self.assertIdentical(self.STATUS.OK, status)
+
+
+ def test_lookupDuplicateByValue(self):
+ """
+ If more than one constant is associated with a particular value,
+ L{Values.lookupByValue} returns whichever of them is defined first.
+ """
+ class TRANSPORT_MESSAGE(Values):
+ """
+ Message types supported by an SSH transport.
+ """
+ KEX_DH_GEX_REQUEST_OLD = ValueConstant(30)
+ KEXDH_INIT = ValueConstant(30)
+
+ self.assertIdentical(
+ TRANSPORT_MESSAGE.lookupByValue(30),
+ TRANSPORT_MESSAGE.KEX_DH_GEX_REQUEST_OLD)
+
+
+ def test_notLookupMissingByValue(self):
+ """
+ L{Values.lookupByValue} raises L{ValueError} when called with a value
+ with which no constant is associated.
+ """
+ self.assertRaises(ValueError, self.STATUS.lookupByValue, "OK")
+ self.assertRaises(ValueError, self.STATUS.lookupByValue, 200)
+ self.assertRaises(ValueError, self.STATUS.lookupByValue, "200.1")
+
+
+ def test_name(self):
+ """
+ The C{name} attribute of one of the constants gives that constant's
+ name.
+ """
+ self.assertEqual("OK", self.STATUS.OK.name)
+
+
+ def test_attributeIdentity(self):
+ """
+ Repeated access of an attribute associated with a L{ValueConstant} value
+ in a L{Values} subclass results in the same object.
+ """
+ self.assertIdentical(self.STATUS.OK, self.STATUS.OK)
+
+
+ def test_iterconstants(self):
+ """
+ L{Values.iterconstants} returns an iterator over all of the constants
+ defined in the class, in the order they were defined.
+ """
+ constants = list(self.STATUS.iterconstants())
+ self.assertEqual(
+ [self.STATUS.OK, self.STATUS.NOT_FOUND],
+ constants)
+
+
+ def test_attributeIterconstantsIdentity(self):
+ """
+ The constants returned from L{Values.iterconstants} are identical to the
+ constants accessible using attributes.
+ """
+ constants = list(self.STATUS.iterconstants())
+ self.assertIdentical(self.STATUS.OK, constants[0])
+ self.assertIdentical(self.STATUS.NOT_FOUND, constants[1])
+
+
+ def test_iterconstantsIdentity(self):
+ """
+ The constants returned from L{Values.iterconstants} are identical on
+ each call to that method.
+ """
+ constants = list(self.STATUS.iterconstants())
+ again = list(self.STATUS.iterconstants())
+ self.assertIdentical(again[0], constants[0])
+ self.assertIdentical(again[1], constants[1])
+
+
+ def test_initializedOnce(self):
+ """
+ L{Values._enumerants} is initialized once and its value re-used on
+ subsequent access.
+ """
+ first = self.STATUS._enumerants
+ self.STATUS.OK # Side-effects!
+ second = self.STATUS._enumerants
+ self.assertIdentical(first, second)
+
+
+class _FlagsTestsMixin(object):
+ """
+ Mixin defining setup code for any tests for L{Flags} subclasses.
+
+ @ivar FXF: A L{Flags} subclass created for each test method.
+ """
+ def setUp(self):
+ """
+ Create a fresh new L{Flags} subclass for each unit test to use. Since
+ L{Flags} is stateful, re-using the same subclass across test methods
+ makes exercising all of the implementation code paths difficult.
+ """
+ class FXF(Flags):
+ # Implicitly assign three flag values based on definition order
+ READ = FlagConstant()
+ WRITE = FlagConstant()
+ APPEND = FlagConstant()
+
+ # Explicitly assign one flag value by passing it in
+ EXCLUSIVE = FlagConstant(0x20)
+
+ # Implicitly assign another flag value, following the previously
+ # specified explicit value.
+ TEXT = FlagConstant()
+
+ self.FXF = FXF
+
+
+
+class FlagsTests(_FlagsTestsMixin, TestCase, _ConstantsTestsMixin):
+ """
+ Tests for L{twisted.python.constants.Flags}, a base class for containers of
+ related, combinable flag or bitvector-like constants.
+ """
+ def test_notInstantiable(self):
+ """
+ A subclass of L{Flags} raises L{TypeError} if an attempt is made to
+ instantiate it.
+ """
+ self._notInstantiableTest("FXF", self.FXF)
+
+
+ def test_symbolicAttributes(self):
+ """
+ Each name associated with a L{FlagConstant} instance in the definition
+ of a L{Flags} subclass is available as an attribute on the resulting
+ class.
+ """
+ self.assertTrue(hasattr(self.FXF, "READ"))
+ self.assertTrue(hasattr(self.FXF, "WRITE"))
+ self.assertTrue(hasattr(self.FXF, "APPEND"))
+ self.assertTrue(hasattr(self.FXF, "EXCLUSIVE"))
+ self.assertTrue(hasattr(self.FXF, "TEXT"))
+
+
+ def test_withoutOtherAttributes(self):
+ """
+ As usual, names not defined in the class scope of a L{Flags} subclass
+ are not available as attributes on the resulting class.
+ """
+ self.assertFalse(hasattr(self.FXF, "foo"))
+
+
+ def test_representation(self):
+ """
+ The string representation of a constant on a L{Flags} subclass includes
+ the name of the L{Flags} subclass and the name of the constant itself.
+ """
+ self.assertEqual("<FXF=READ>", repr(self.FXF.READ))
+
+
+ def test_lookupByName(self):
+ """
+ Constants can be looked up by name using L{Flags.lookupByName}.
+ """
+ flag = self.FXF.lookupByName("READ")
+ self.assertIdentical(self.FXF.READ, flag)
+
+
+ def test_notLookupMissingByName(self):
+ """
+ Names not defined with a L{FlagConstant} instance cannot be looked up
+ using L{Flags.lookupByName}.
+ """
+ self.assertRaises(ValueError, self.FXF.lookupByName, "lookupByName")
+ self.assertRaises(ValueError, self.FXF.lookupByName, "__init__")
+ self.assertRaises(ValueError, self.FXF.lookupByName, "foo")
+
+
+ def test_lookupByValue(self):
+ """
+ Constants can be looked up by their associated value, defined implicitly
+ by the position in which the constant appears in the class definition or
+ explicitly by the argument passed to L{FlagConstant}.
+ """
+ flag = self.FXF.lookupByValue(0x01)
+ self.assertIdentical(flag, self.FXF.READ)
+
+ flag = self.FXF.lookupByValue(0x02)
+ self.assertIdentical(flag, self.FXF.WRITE)
+
+ flag = self.FXF.lookupByValue(0x04)
+ self.assertIdentical(flag, self.FXF.APPEND)
+
+ flag = self.FXF.lookupByValue(0x20)
+ self.assertIdentical(flag, self.FXF.EXCLUSIVE)
+
+ flag = self.FXF.lookupByValue(0x40)
+ self.assertIdentical(flag, self.FXF.TEXT)
+
+
+ def test_lookupDuplicateByValue(self):
+ """
+ If more than one constant is associated with a particular value,
+ L{Flags.lookupByValue} returns whichever of them is defined first.
+ """
+ class TIMEX(Flags):
+ # (timex.mode)
+ ADJ_OFFSET = FlagConstant(0x0001) # time offset
+
+ # xntp 3.4 compatibility names
+ MOD_OFFSET = FlagConstant(0x0001)
+
+ self.assertIdentical(TIMEX.lookupByValue(0x0001), TIMEX.ADJ_OFFSET)
+
+
+ def test_notLookupMissingByValue(self):
+ """
+ L{Flags.lookupByValue} raises L{ValueError} when called with a value
+ with which no constant is associated.
+ """
+ self.assertRaises(ValueError, self.FXF.lookupByValue, 0x10)
+
+
+ def test_name(self):
+ """
+ The C{name} attribute of one of the constants gives that constant's
+ name.
+ """
+ self.assertEqual("READ", self.FXF.READ.name)
+
+
+ def test_attributeIdentity(self):
+ """
+ Repeated access of an attribute associated with a L{FlagConstant} value
+ in a L{Flags} subclass results in the same object.
+ """
+ self.assertIdentical(self.FXF.READ, self.FXF.READ)
+
+
+ def test_iterconstants(self):
+ """
+ L{Flags.iterconstants} returns an iterator over all of the constants
+ defined in the class, in the order they were defined.
+ """
+ constants = list(self.FXF.iterconstants())
+ self.assertEqual(
+ [self.FXF.READ, self.FXF.WRITE, self.FXF.APPEND,
+ self.FXF.EXCLUSIVE, self.FXF.TEXT],
+ constants)
+
+
+ def test_attributeIterconstantsIdentity(self):
+ """
+ The constants returned from L{Flags.iterconstants} are identical to the
+ constants accessible using attributes.
+ """
+ constants = list(self.FXF.iterconstants())
+ self.assertIdentical(self.FXF.READ, constants[0])
+ self.assertIdentical(self.FXF.WRITE, constants[1])
+ self.assertIdentical(self.FXF.APPEND, constants[2])
+ self.assertIdentical(self.FXF.EXCLUSIVE, constants[3])
+ self.assertIdentical(self.FXF.TEXT, constants[4])
+
+
+ def test_iterconstantsIdentity(self):
+ """
+ The constants returned from L{Flags.iterconstants} are identical on each
+ call to that method.
+ """
+ constants = list(self.FXF.iterconstants())
+ again = list(self.FXF.iterconstants())
+ self.assertIdentical(again[0], constants[0])
+ self.assertIdentical(again[1], constants[1])
+ self.assertIdentical(again[2], constants[2])
+ self.assertIdentical(again[3], constants[3])
+ self.assertIdentical(again[4], constants[4])
+
+
+ def test_initializedOnce(self):
+ """
+ L{Flags._enumerants} is initialized once and its value re-used on
+ subsequent access.
+ """
+ first = self.FXF._enumerants
+ self.FXF.READ # Side-effects!
+ second = self.FXF._enumerants
+ self.assertIdentical(first, second)
+
+
+
+class FlagConstantSimpleOrTests(_FlagsTestsMixin, TestCase):
+ """
+ Tests for the C{|} operator as defined for L{FlagConstant} instances, used
+ to create new L{FlagConstant} instances representing both of two existing
+ L{FlagConstant} instances from the same L{Flags} class.
+ """
+ def test_value(self):
+ """
+ The value of the L{FlagConstant} which results from C{|} has all of the
+ bits set which were set in either of the values of the two original
+ constants.
+ """
+ flag = self.FXF.READ | self.FXF.WRITE
+ self.assertEqual(self.FXF.READ.value | self.FXF.WRITE.value, flag.value)
+
+
+ def test_name(self):
+ """
+ The name of the L{FlagConstant} instance which results from C{|}
+ includes the names of both of the two original constants.
+ """
+ flag = self.FXF.READ | self.FXF.WRITE
+ self.assertEqual("{READ,WRITE}", flag.name)
+
+
+ def test_representation(self):
+ """
+ The string representation of a L{FlagConstant} instance which results
+ from C{|} includes the names of both of the two original constants.
+ """
+ flag = self.FXF.READ | self.FXF.WRITE
+ self.assertEqual("<FXF={READ,WRITE}>", repr(flag))
+
+
+
+class FlagConstantSimpleAndTests(_FlagsTestsMixin, TestCase):
+ """
+ Tests for the C{&} operator as defined for L{FlagConstant} instances, used
+ to create new L{FlagConstant} instances representing the common parts of two
+ existing L{FlagConstant} instances from the same L{Flags} class.
+ """
+ def test_value(self):
+ """
+ The value of the L{FlagConstant} which results from C{&} has all of the
+ bits set which were set in both of the values of the two original
+ constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite & writeAppend
+ self.assertEqual(self.FXF.WRITE.value, flag.value)
+
+
+ def test_name(self):
+ """
+ The name of the L{FlagConstant} instance which results from C{&}
+ includes the names of only the flags which were set in both of the two
+ original constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite & writeAppend
+ self.assertEqual("WRITE", flag.name)
+
+
+ def test_representation(self):
+ """
+ The string representation of a L{FlagConstant} instance which results
+ from C{&} includes the names of only the flags which were set in both
+ both of the two original constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite & writeAppend
+ self.assertEqual("<FXF=WRITE>", repr(flag))
+
+
+
+class FlagConstantSimpleExclusiveOrTests(_FlagsTestsMixin, TestCase):
+ """
+ Tests for the C{^} operator as defined for L{FlagConstant} instances, used
+ to create new L{FlagConstant} instances representing the uncommon parts of
+ two existing L{FlagConstant} instances from the same L{Flags} class.
+ """
+ def test_value(self):
+ """
+ The value of the L{FlagConstant} which results from C{^} has all of the
+ bits set which were set in exactly one of the values of the two original
+ constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite ^ writeAppend
+ self.assertEqual(self.FXF.READ.value | self.FXF.APPEND.value, flag.value)
+
+
+ def test_name(self):
+ """
+ The name of the L{FlagConstant} instance which results from C{^}
+ includes the names of only the flags which were set in exactly one of
+ the two original constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite ^ writeAppend
+ self.assertEqual("{APPEND,READ}", flag.name)
+
+
+ def test_representation(self):
+ """
+ The string representation of a L{FlagConstant} instance which results
+ from C{^} includes the names of only the flags which were set in exactly
+ one of the two original constants.
+ """
+ readWrite = (self.FXF.READ | self.FXF.WRITE)
+ writeAppend = (self.FXF.WRITE | self.FXF.APPEND)
+ flag = readWrite ^ writeAppend
+ self.assertEqual("<FXF={APPEND,READ}>", repr(flag))
+
+
+
+class FlagConstantNegationTests(_FlagsTestsMixin, TestCase):
+ """
+ Tests for the C{~} operator as defined for L{FlagConstant} instances, used
+ to create new L{FlagConstant} instances representing all the flags from a
+ L{Flags} class not set in a particular L{FlagConstant} instance.
+ """
+ def test_value(self):
+ """
+ The value of the L{FlagConstant} which results from C{~} has all of the
+ bits set which were not set in the original constant.
+ """
+ flag = ~self.FXF.READ
+ self.assertEqual(
+ self.FXF.WRITE.value |
+ self.FXF.APPEND.value |
+ self.FXF.EXCLUSIVE.value |
+ self.FXF.TEXT.value,
+ flag.value)
+
+ flag = ~self.FXF.WRITE
+ self.assertEqual(
+ self.FXF.READ.value |
+ self.FXF.APPEND.value |
+ self.FXF.EXCLUSIVE.value |
+ self.FXF.TEXT.value,
+ flag.value)
+
+
+ def test_name(self):
+ """
+ The name of the L{FlagConstant} instance which results from C{~}
+ includes the names of all the flags which were not set in the original
+ constant.
+ """
+ flag = ~self.FXF.WRITE
+ self.assertEqual("{APPEND,EXCLUSIVE,READ,TEXT}", flag.name)
+
+
+ def test_representation(self):
+ """
+ The string representation of a L{FlagConstant} instance which results
+ from C{~} includes the names of all the flags which were not set in the
+ original constant.
+ """
+ flag = ~self.FXF.WRITE
+ self.assertEqual("<FXF={APPEND,EXCLUSIVE,READ,TEXT}>", repr(flag))
diff --git a/twisted/python/test/test_deprecate.py b/twisted/python/test/test_deprecate.py
new file mode 100644
index 0000000..03498e4
--- /dev/null
+++ b/twisted/python/test/test_deprecate.py
@@ -0,0 +1,767 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for Twisted's deprecation framework, L{twisted.python.deprecate}.
+"""
+
+import sys, types
+import warnings
+from os.path import normcase
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python import deprecate
+from twisted.python.deprecate import _appendToDocstring
+from twisted.python.deprecate import _getDeprecationDocstring
+from twisted.python.deprecate import deprecated, getDeprecationWarningString
+from twisted.python.deprecate import _getDeprecationWarningString
+from twisted.python.deprecate import DEPRECATION_WARNING_FORMAT
+from twisted.python.reflect import fullyQualifiedName
+from twisted.python.versions import Version
+from twisted.python.filepath import FilePath
+
+from twisted.python.test import deprecatedattributes
+from twisted.python.test.modules_helpers import TwistedModulesTestCase
+
+
+
+def dummyCallable():
+ """
+ Do nothing.
+
+ This is used to test the deprecation decorators.
+ """
+
+
+def dummyReplacementMethod():
+ """
+ Do nothing.
+
+ This is used to test the replacement parameter to L{deprecated}.
+ """
+
+
+
+class TestDeprecationWarnings(TestCase):
+ def test_getDeprecationWarningString(self):
+ """
+ L{getDeprecationWarningString} returns a string that tells us that a
+ callable was deprecated at a certain released version of Twisted.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ self.assertEqual(
+ getDeprecationWarningString(self.test_getDeprecationWarningString,
+ version),
+ "twisted.python.test.test_deprecate.TestDeprecationWarnings."
+ "test_getDeprecationWarningString was deprecated in "
+ "Twisted 8.0.0")
+
+
+ def test_getDeprecationWarningStringWithFormat(self):
+ """
+ L{getDeprecationWarningString} returns a string that tells us that a
+ callable was deprecated at a certain released version of Twisted, with
+ a message containing additional information about the deprecation.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ format = deprecate.DEPRECATION_WARNING_FORMAT + ': This is a message'
+ self.assertEqual(
+ getDeprecationWarningString(self.test_getDeprecationWarningString,
+ version, format),
+ 'twisted.python.test.test_deprecate.TestDeprecationWarnings.'
+ 'test_getDeprecationWarningString was deprecated in '
+ 'Twisted 8.0.0: This is a message')
+
+
+ def test_deprecateEmitsWarning(self):
+ """
+ Decorating a callable with L{deprecated} emits a warning.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ dummy = deprecated(version)(dummyCallable)
+ def addStackLevel():
+ dummy()
+ self.assertWarns(
+ DeprecationWarning,
+ getDeprecationWarningString(dummyCallable, version),
+ __file__,
+ addStackLevel)
+
+
+ def test_deprecatedPreservesName(self):
+ """
+ The decorated function has the same name as the original.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ dummy = deprecated(version)(dummyCallable)
+ self.assertEqual(dummyCallable.__name__, dummy.__name__)
+ self.assertEqual(fullyQualifiedName(dummyCallable),
+ fullyQualifiedName(dummy))
+
+
+ def test_getDeprecationDocstring(self):
+ """
+ L{_getDeprecationDocstring} returns a note about the deprecation to go
+ into a docstring.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ self.assertEqual(
+ "Deprecated in Twisted 8.0.0.",
+ _getDeprecationDocstring(version, ''))
+
+
+ def test_deprecatedUpdatesDocstring(self):
+ """
+ The docstring of the deprecated function is appended with information
+ about the deprecation.
+ """
+
+ version = Version('Twisted', 8, 0, 0)
+ dummy = deprecated(version)(dummyCallable)
+
+ _appendToDocstring(
+ dummyCallable,
+ _getDeprecationDocstring(version, ''))
+
+ self.assertEqual(dummyCallable.__doc__, dummy.__doc__)
+
+
+ def test_versionMetadata(self):
+ """
+ Deprecating a function adds version information to the decorated
+ version of that function.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ dummy = deprecated(version)(dummyCallable)
+ self.assertEqual(version, dummy.deprecatedVersion)
+
+
+ def test_getDeprecationWarningStringReplacement(self):
+ """
+ L{getDeprecationWarningString} takes an additional replacement parameter
+ that can be used to add information to the deprecation. If the
+ replacement parameter is a string, it will be interpolated directly into
+ the result.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ warningString = getDeprecationWarningString(
+ self.test_getDeprecationWarningString, version,
+ replacement="something.foobar")
+ self.assertEqual(
+ warningString,
+ "%s was deprecated in Twisted 8.0.0; please use something.foobar "
+ "instead" % (
+ fullyQualifiedName(self.test_getDeprecationWarningString),))
+
+
+ def test_getDeprecationWarningStringReplacementWithCallable(self):
+ """
+ L{getDeprecationWarningString} takes an additional replacement parameter
+ that can be used to add information to the deprecation. If the
+ replacement parameter is a callable, its fully qualified name will be
+ interpolated into the result.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ warningString = getDeprecationWarningString(
+ self.test_getDeprecationWarningString, version,
+ replacement=dummyReplacementMethod)
+ self.assertEqual(
+ warningString,
+ "%s was deprecated in Twisted 8.0.0; please use "
+ "twisted.python.test.test_deprecate.dummyReplacementMethod "
+ "instead" % (
+ fullyQualifiedName(self.test_getDeprecationWarningString),))
+
+
+ def test_deprecatedReplacement(self):
+ """
+ L{deprecated} takes an additional replacement parameter that can be used
+ to indicate the new, non-deprecated method developers should use. If
+ the replacement parameter is a string, it will be interpolated directly
+ into the warning message.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ dummy = deprecated(version, "something.foobar")(dummyCallable)
+ self.assertEqual(dummy.__doc__,
+ "\n"
+ " Do nothing.\n\n"
+ " This is used to test the deprecation decorators.\n\n"
+ " Deprecated in Twisted 8.0.0; please use "
+ "something.foobar"
+ " instead.\n"
+ " ")
+
+
+ def test_deprecatedReplacementWithCallable(self):
+ """
+ L{deprecated} takes an additional replacement parameter that can be used
+ to indicate the new, non-deprecated method developers should use. If
+ the replacement parameter is a callable, its fully qualified name will
+ be interpolated into the warning message.
+ """
+ version = Version('Twisted', 8, 0, 0)
+ decorator = deprecated(version, replacement=dummyReplacementMethod)
+ dummy = decorator(dummyCallable)
+ self.assertEqual(dummy.__doc__,
+ "\n"
+ " Do nothing.\n\n"
+ " This is used to test the deprecation decorators.\n\n"
+ " Deprecated in Twisted 8.0.0; please use "
+ "twisted.python.test.test_deprecate.dummyReplacementMethod"
+ " instead.\n"
+ " ")
+
+
+
+class TestAppendToDocstring(TestCase):
+ """
+ Test the _appendToDocstring function.
+
+ _appendToDocstring is used to add text to a docstring.
+ """
+
+ def test_appendToEmptyDocstring(self):
+ """
+ Appending to an empty docstring simply replaces the docstring.
+ """
+
+ def noDocstring():
+ pass
+
+ _appendToDocstring(noDocstring, "Appended text.")
+ self.assertEqual("Appended text.", noDocstring.__doc__)
+
+
+ def test_appendToSingleLineDocstring(self):
+ """
+ Appending to a single line docstring places the message on a new line,
+ with a blank line separating it from the rest of the docstring.
+
+ The docstring ends with a newline, conforming to Twisted and PEP 8
+ standards. Unfortunately, the indentation is incorrect, since the
+ existing docstring doesn't have enough info to help us indent
+ properly.
+ """
+
+ def singleLineDocstring():
+ """This doesn't comply with standards, but is here for a test."""
+
+ _appendToDocstring(singleLineDocstring, "Appended text.")
+ self.assertEqual(
+ ["This doesn't comply with standards, but is here for a test.",
+ "",
+ "Appended text."],
+ singleLineDocstring.__doc__.splitlines())
+ self.assertTrue(singleLineDocstring.__doc__.endswith('\n'))
+
+
+ def test_appendToMultilineDocstring(self):
+ """
+ Appending to a multi-line docstring places the messade on a new line,
+ with a blank line separating it from the rest of the docstring.
+
+ Because we have multiple lines, we have enough information to do
+ indentation.
+ """
+
+ def multiLineDocstring():
+ """
+ This is a multi-line docstring.
+ """
+
+ def expectedDocstring():
+ """
+ This is a multi-line docstring.
+
+ Appended text.
+ """
+
+ _appendToDocstring(multiLineDocstring, "Appended text.")
+ self.assertEqual(
+ expectedDocstring.__doc__, multiLineDocstring.__doc__)
+
+
+
+class _MockDeprecatedAttribute(object):
+ """
+ Mock of L{twisted.python.deprecate._DeprecatedAttribute}.
+
+ @ivar value: The value of the attribute.
+ """
+ def __init__(self, value):
+ self.value = value
+
+
+ def get(self):
+ """
+ Get a known value.
+ """
+ return self.value
+
+
+
+class ModuleProxyTests(TestCase):
+ """
+ Tests for L{twisted.python.deprecate._ModuleProxy}, which proxies
+ access to module-level attributes, intercepting access to deprecated
+ attributes and passing through access to normal attributes.
+ """
+ def _makeProxy(self, **attrs):
+ """
+ Create a temporary module proxy object.
+
+ @param **kw: Attributes to initialise on the temporary module object
+
+ @rtype: L{twistd.python.deprecate._ModuleProxy}
+ """
+ mod = types.ModuleType('foo')
+ for key, value in attrs.iteritems():
+ setattr(mod, key, value)
+ return deprecate._ModuleProxy(mod)
+
+
+ def test_getattrPassthrough(self):
+ """
+ Getting a normal attribute on a L{twisted.python.deprecate._ModuleProxy}
+ retrieves the underlying attribute's value, and raises C{AttributeError}
+ if a non-existant attribute is accessed.
+ """
+ proxy = self._makeProxy(SOME_ATTRIBUTE='hello')
+ self.assertIdentical(proxy.SOME_ATTRIBUTE, 'hello')
+ self.assertRaises(AttributeError, getattr, proxy, 'DOES_NOT_EXIST')
+
+
+ def test_getattrIntercept(self):
+ """
+ Getting an attribute marked as being deprecated on
+ L{twisted.python.deprecate._ModuleProxy} results in calling the
+ deprecated wrapper's C{get} method.
+ """
+ proxy = self._makeProxy()
+ _deprecatedAttributes = object.__getattribute__(
+ proxy, '_deprecatedAttributes')
+ _deprecatedAttributes['foo'] = _MockDeprecatedAttribute(42)
+ self.assertEqual(proxy.foo, 42)
+
+
+ def test_privateAttributes(self):
+ """
+ Private attributes of L{twisted.python.deprecate._ModuleProxy} are
+ inaccessible when regular attribute access is used.
+ """
+ proxy = self._makeProxy()
+ self.assertRaises(AttributeError, getattr, proxy, '_module')
+ self.assertRaises(
+ AttributeError, getattr, proxy, '_deprecatedAttributes')
+
+
+ def test_setattr(self):
+ """
+ Setting attributes on L{twisted.python.deprecate._ModuleProxy} proxies
+ them through to the wrapped module.
+ """
+ proxy = self._makeProxy()
+ proxy._module = 1
+ self.assertNotEquals(object.__getattribute__(proxy, '_module'), 1)
+ self.assertEqual(proxy._module, 1)
+
+
+ def test_repr(self):
+ """
+ L{twisted.python.deprecated._ModuleProxy.__repr__} produces a string
+ containing the proxy type and a representation of the wrapped module
+ object.
+ """
+ proxy = self._makeProxy()
+ realModule = object.__getattribute__(proxy, '_module')
+ self.assertEqual(
+ repr(proxy), '<%s module=%r>' % (type(proxy).__name__, realModule))
+
+
+
+class DeprecatedAttributeTests(TestCase):
+ """
+ Tests for L{twisted.python.deprecate._DeprecatedAttribute} and
+ L{twisted.python.deprecate.deprecatedModuleAttribute}, which issue
+ warnings for deprecated module-level attributes.
+ """
+ def setUp(self):
+ self.version = deprecatedattributes.version
+ self.message = deprecatedattributes.message
+ self._testModuleName = __name__ + '.foo'
+
+
+ def _getWarningString(self, attr):
+ """
+ Create the warning string used by deprecated attributes.
+ """
+ return _getDeprecationWarningString(
+ deprecatedattributes.__name__ + '.' + attr,
+ deprecatedattributes.version,
+ DEPRECATION_WARNING_FORMAT + ': ' + deprecatedattributes.message)
+
+
+ def test_deprecatedAttributeHelper(self):
+ """
+ L{twisted.python.deprecate._DeprecatedAttribute} correctly sets its
+ __name__ to match that of the deprecated attribute and emits a warning
+ when the original attribute value is accessed.
+ """
+ name = 'ANOTHER_DEPRECATED_ATTRIBUTE'
+ setattr(deprecatedattributes, name, 42)
+ attr = deprecate._DeprecatedAttribute(
+ deprecatedattributes, name, self.version, self.message)
+
+ self.assertEqual(attr.__name__, name)
+
+ # Since we're accessing the value getter directly, as opposed to via
+ # the module proxy, we need to match the warning's stack level.
+ def addStackLevel():
+ attr.get()
+
+ # Access the deprecated attribute.
+ addStackLevel()
+ warningsShown = self.flushWarnings([
+ self.test_deprecatedAttributeHelper])
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warningsShown[0]['message'],
+ self._getWarningString(name))
+ self.assertEqual(len(warningsShown), 1)
+
+
+ def test_deprecatedAttribute(self):
+ """
+ L{twisted.python.deprecate.deprecatedModuleAttribute} wraps a
+ module-level attribute in an object that emits a deprecation warning
+ when it is accessed the first time only, while leaving other unrelated
+ attributes alone.
+ """
+ # Accessing non-deprecated attributes does not issue a warning.
+ deprecatedattributes.ANOTHER_ATTRIBUTE
+ warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
+ self.assertEqual(len(warningsShown), 0)
+
+ name = 'DEPRECATED_ATTRIBUTE'
+
+ # Access the deprecated attribute. This uses getattr to avoid repeating
+ # the attribute name.
+ getattr(deprecatedattributes, name)
+
+ warningsShown = self.flushWarnings([self.test_deprecatedAttribute])
+ self.assertEqual(len(warningsShown), 1)
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warningsShown[0]['message'],
+ self._getWarningString(name))
+
+
+ def test_wrappedModule(self):
+ """
+ Deprecating an attribute in a module replaces and wraps that module
+ instance, in C{sys.modules}, with a
+ L{twisted.python.deprecate._ModuleProxy} instance but only if it hasn't
+ already been wrapped.
+ """
+ sys.modules[self._testModuleName] = mod = types.ModuleType('foo')
+ self.addCleanup(sys.modules.pop, self._testModuleName)
+
+ setattr(mod, 'first', 1)
+ setattr(mod, 'second', 2)
+
+ deprecate.deprecatedModuleAttribute(
+ Version('Twisted', 8, 0, 0),
+ 'message',
+ self._testModuleName,
+ 'first')
+
+ proxy = sys.modules[self._testModuleName]
+ self.assertNotEqual(proxy, mod)
+
+ deprecate.deprecatedModuleAttribute(
+ Version('Twisted', 8, 0, 0),
+ 'message',
+ self._testModuleName,
+ 'second')
+
+ self.assertIdentical(proxy, sys.modules[self._testModuleName])
+
+
+
+class ImportedModuleAttributeTests(TwistedModulesTestCase):
+ """
+ Tests for L{deprecatedModuleAttribute} which involve loading a module via
+ 'import'.
+ """
+
+ _packageInit = """\
+from twisted.python.deprecate import deprecatedModuleAttribute
+from twisted.python.versions import Version
+
+deprecatedModuleAttribute(
+ Version('Package', 1, 2, 3), 'message', __name__, 'module')
+"""
+
+
+ def pathEntryTree(self, tree):
+ """
+ Create some files in a hierarchy, based on a dictionary describing those
+ files. The resulting hierarchy will be placed onto sys.path for the
+ duration of the test.
+
+ @param tree: A dictionary representing a directory structure. Keys are
+ strings, representing filenames, dictionary values represent
+ directories, string values represent file contents.
+
+ @return: another dictionary similar to the input, with file content
+ strings replaced with L{FilePath} objects pointing at where those
+ contents are now stored.
+ """
+ def makeSomeFiles(pathobj, dirdict):
+ pathdict = {}
+ for (key, value) in dirdict.items():
+ child = pathobj.child(key)
+ if isinstance(value, str):
+ pathdict[key] = child
+ child.setContent(value)
+ elif isinstance(value, dict):
+ child.createDirectory()
+ pathdict[key] = makeSomeFiles(child, value)
+ else:
+ raise ValueError("only strings and dicts allowed as values")
+ return pathdict
+ base = FilePath(self.mktemp())
+ base.makedirs()
+
+ result = makeSomeFiles(base, tree)
+ self.replaceSysPath([base.path] + sys.path)
+ self.replaceSysModules(sys.modules.copy())
+ return result
+
+
+ def simpleModuleEntry(self):
+ """
+ Add a sample module and package to the path, returning a L{FilePath}
+ pointing at the module which will be loadable as C{package.module}.
+ """
+ paths = self.pathEntryTree(
+ {"package": {"__init__.py": self._packageInit,
+ "module.py": ""}})
+ return paths['package']['module.py']
+
+
+ def checkOneWarning(self, modulePath):
+ """
+ Verification logic for L{test_deprecatedModule}.
+ """
+ # import package.module
+ from package import module
+ self.assertEqual(module.__file__, modulePath.path)
+ emitted = self.flushWarnings([self.checkOneWarning])
+ self.assertEqual(len(emitted), 1)
+ self.assertEqual(emitted[0]['message'],
+ 'package.module was deprecated in Package 1.2.3: '
+ 'message')
+ self.assertEqual(emitted[0]['category'], DeprecationWarning)
+
+
+ def test_deprecatedModule(self):
+ """
+ If L{deprecatedModuleAttribute} is used to deprecate a module attribute
+ of a package, only one deprecation warning is emitted when the
+ deprecated module is imported.
+ """
+ self.checkOneWarning(self.simpleModuleEntry())
+
+
+ def test_deprecatedModuleMultipleTimes(self):
+ """
+ If L{deprecatedModuleAttribute} is used to deprecate a module attribute
+ of a package, only one deprecation warning is emitted when the
+ deprecated module is subsequently imported.
+ """
+ mp = self.simpleModuleEntry()
+ # The first time, the code needs to be loaded.
+ self.checkOneWarning(mp)
+ # The second time, things are slightly different; the object's already
+ # in the namespace.
+ self.checkOneWarning(mp)
+ # The third and fourth times, things things should all be exactly the
+ # same, but this is a sanity check to make sure the implementation isn't
+ # special casing the second time. Also, putting these cases into a loop
+ # means that the stack will be identical, to make sure that the
+ # implementation doesn't rely too much on stack-crawling.
+ for x in range(2):
+ self.checkOneWarning(mp)
+
+
+
+class WarnAboutFunctionTests(TestCase):
+ """
+ Tests for L{twisted.python.deprecate.warnAboutFunction} which allows the
+ callers of a function to issue a C{DeprecationWarning} about that function.
+ """
+ def setUp(self):
+ """
+ Create a file that will have known line numbers when emitting warnings.
+ """
+ self.package = FilePath(self.mktemp()).child('twisted_private_helper')
+ self.package.makedirs()
+ self.package.child('__init__.py').setContent('')
+ self.package.child('module.py').setContent('''
+"A module string"
+
+from twisted.python import deprecate
+
+def testFunction():
+ "A doc string"
+ a = 1 + 2
+ return a
+
+def callTestFunction():
+ b = testFunction()
+ if b == 3:
+ deprecate.warnAboutFunction(testFunction, "A Warning String")
+''')
+ sys.path.insert(0, self.package.parent().path)
+ self.addCleanup(sys.path.remove, self.package.parent().path)
+
+ modules = sys.modules.copy()
+ self.addCleanup(
+ lambda: (sys.modules.clear(), sys.modules.update(modules)))
+
+
+ def test_warning(self):
+ """
+ L{deprecate.warnAboutFunction} emits a warning the file and line number
+ of which point to the beginning of the implementation of the function
+ passed to it.
+ """
+ def aFunc():
+ pass
+ deprecate.warnAboutFunction(aFunc, 'A Warning Message')
+ warningsShown = self.flushWarnings()
+ filename = __file__
+ if filename.lower().endswith('.pyc'):
+ filename = filename[:-1]
+ self.assertSamePath(
+ FilePath(warningsShown[0]["filename"]), FilePath(filename))
+ self.assertEqual(warningsShown[0]["message"], "A Warning Message")
+
+
+ def test_warningLineNumber(self):
+ """
+ L{deprecate.warnAboutFunction} emits a C{DeprecationWarning} with the
+ number of a line within the implementation of the function passed to it.
+ """
+ from twisted_private_helper import module
+ module.callTestFunction()
+ warningsShown = self.flushWarnings()
+ self.assertSamePath(
+ FilePath(warningsShown[0]["filename"]),
+ self.package.sibling('twisted_private_helper').child('module.py'))
+ # Line number 9 is the last line in the testFunction in the helper
+ # module.
+ self.assertEqual(warningsShown[0]["lineno"], 9)
+ self.assertEqual(warningsShown[0]["message"], "A Warning String")
+ self.assertEqual(len(warningsShown), 1)
+
+
+ def assertSamePath(self, first, second):
+ """
+ Assert that the two paths are the same, considering case normalization
+ appropriate for the current platform.
+
+ @type first: L{FilePath}
+ @type second: L{FilePath}
+
+ @raise C{self.failureType}: If the paths are not the same.
+ """
+ self.assertTrue(
+ normcase(first.path) == normcase(second.path),
+ "%r != %r" % (first, second))
+
+
+ def test_renamedFile(self):
+ """
+ Even if the implementation of a deprecated function is moved around on
+ the filesystem, the line number in the warning emitted by
+ L{deprecate.warnAboutFunction} points to a line in the implementation of
+ the deprecated function.
+ """
+ from twisted_private_helper import module
+ # Clean up the state resulting from that import; we're not going to use
+ # this module, so it should go away.
+ del sys.modules['twisted_private_helper']
+ del sys.modules[module.__name__]
+
+ # Rename the source directory
+ self.package.moveTo(self.package.sibling('twisted_renamed_helper'))
+
+ # Import the newly renamed version
+ from twisted_renamed_helper import module
+ self.addCleanup(sys.modules.pop, 'twisted_renamed_helper')
+ self.addCleanup(sys.modules.pop, module.__name__)
+
+ module.callTestFunction()
+ warningsShown = self.flushWarnings()
+ warnedPath = FilePath(warningsShown[0]["filename"])
+ expectedPath = self.package.sibling(
+ 'twisted_renamed_helper').child('module.py')
+ self.assertSamePath(warnedPath, expectedPath)
+ self.assertEqual(warningsShown[0]["lineno"], 9)
+ self.assertEqual(warningsShown[0]["message"], "A Warning String")
+ self.assertEqual(len(warningsShown), 1)
+
+
+ def test_filteredWarning(self):
+ """
+ L{deprecate.warnAboutFunction} emits a warning that will be filtered if
+ L{warnings.filterwarning} is called with the module name of the
+ deprecated function.
+ """
+ # Clean up anything *else* that might spuriously filter out the warning,
+ # such as the "always" simplefilter set up by unittest._collectWarnings.
+ # We'll also rely on trial to restore the original filters afterwards.
+ del warnings.filters[:]
+
+ warnings.filterwarnings(
+ action="ignore", module="twisted_private_helper")
+
+ from twisted_private_helper import module
+ module.callTestFunction()
+
+ warningsShown = self.flushWarnings()
+ self.assertEqual(len(warningsShown), 0)
+
+
+ def test_filteredOnceWarning(self):
+ """
+ L{deprecate.warnAboutFunction} emits a warning that will be filtered
+ once if L{warnings.filterwarning} is called with the module name of the
+ deprecated function and an action of once.
+ """
+ # Clean up anything *else* that might spuriously filter out the warning,
+ # such as the "always" simplefilter set up by unittest._collectWarnings.
+ # We'll also rely on trial to restore the original filters afterwards.
+ del warnings.filters[:]
+
+ warnings.filterwarnings(
+ action="module", module="twisted_private_helper")
+
+ from twisted_private_helper import module
+ module.callTestFunction()
+ module.callTestFunction()
+
+ warningsShown = self.flushWarnings()
+ self.assertEqual(len(warningsShown), 1)
+ message = warningsShown[0]['message']
+ category = warningsShown[0]['category']
+ filename = warningsShown[0]['filename']
+ lineno = warningsShown[0]['lineno']
+ msg = warnings.formatwarning(message, category, filename, lineno)
+ self.assertTrue(
+ msg.endswith("module.py:9: DeprecationWarning: A Warning String\n"
+ " return a\n"),
+ "Unexpected warning string: %r" % (msg,))
diff --git a/twisted/python/test/test_dist.py b/twisted/python/test/test_dist.py
new file mode 100644
index 0000000..69b902d
--- /dev/null
+++ b/twisted/python/test/test_dist.py
@@ -0,0 +1,316 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for parts of our release automation system.
+"""
+
+
+import os
+
+from distutils.core import Distribution
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python import dist
+from twisted.python.dist import get_setup_args, ConditionalExtension
+from twisted.python.filepath import FilePath
+
+
+class SetupTest(TestCase):
+ """
+ Tests for L{get_setup_args}.
+ """
+ def test_conditionalExtensions(self):
+ """
+ Passing C{conditionalExtensions} as a list of L{ConditionalExtension}
+ objects to get_setup_args inserts a custom build_ext into the result
+ which knows how to check whether they should be built.
+ """
+ good_ext = ConditionalExtension("whatever", ["whatever.c"],
+ condition=lambda b: True)
+ bad_ext = ConditionalExtension("whatever", ["whatever.c"],
+ condition=lambda b: False)
+ args = get_setup_args(conditionalExtensions=[good_ext, bad_ext])
+ # ext_modules should be set even though it's not used. See comment
+ # in get_setup_args
+ self.assertEqual(args["ext_modules"], [good_ext, bad_ext])
+ cmdclass = args["cmdclass"]
+ build_ext = cmdclass["build_ext"]
+ builder = build_ext(Distribution())
+ builder.prepare_extensions()
+ self.assertEqual(builder.extensions, [good_ext])
+
+
+ def test_win32Definition(self):
+ """
+ When building on Windows NT, the WIN32 macro will be defined as 1.
+ """
+ ext = ConditionalExtension("whatever", ["whatever.c"],
+ define_macros=[("whatever", 2)])
+ args = get_setup_args(conditionalExtensions=[ext])
+ builder = args["cmdclass"]["build_ext"](Distribution())
+ self.patch(os, "name", "nt")
+ builder.prepare_extensions()
+ self.assertEqual(ext.define_macros, [("whatever", 2), ("WIN32", 1)])
+
+
+
+class GetVersionTest(TestCase):
+ """
+ Tests for L{dist.getVersion}.
+ """
+
+ def setUp(self):
+ self.dirname = self.mktemp()
+ os.mkdir(self.dirname)
+
+ def test_getVersionCore(self):
+ """
+ Test that getting the version of core reads from the
+ [base]/_version.py file.
+ """
+ f = open(os.path.join(self.dirname, "_version.py"), "w")
+ f.write("""
+from twisted.python import versions
+version = versions.Version("twisted", 0, 1, 2)
+""")
+ f.close()
+ self.assertEqual(dist.getVersion("core", base=self.dirname), "0.1.2")
+
+ def test_getVersionOther(self):
+ """
+ Test that getting the version of a non-core project reads from
+ the [base]/[projname]/_version.py file.
+ """
+ os.mkdir(os.path.join(self.dirname, "blat"))
+ f = open(os.path.join(self.dirname, "blat", "_version.py"), "w")
+ f.write("""
+from twisted.python import versions
+version = versions.Version("twisted.blat", 9, 8, 10)
+""")
+ f.close()
+ self.assertEqual(dist.getVersion("blat", base=self.dirname), "9.8.10")
+
+
+class GetScriptsTest(TestCase):
+ """
+ Tests for L{dist.getScripts} which returns the scripts which should be
+ included in the distribution of a project.
+ """
+
+ def test_scriptsInSVN(self):
+ """
+ getScripts should return the scripts associated with a project
+ in the context of Twisted SVN.
+ """
+ basedir = self.mktemp()
+ os.mkdir(basedir)
+ os.mkdir(os.path.join(basedir, 'bin'))
+ os.mkdir(os.path.join(basedir, 'bin', 'proj'))
+ f = open(os.path.join(basedir, 'bin', 'proj', 'exy'), 'w')
+ f.write('yay')
+ f.close()
+ scripts = dist.getScripts('proj', basedir=basedir)
+ self.assertEqual(len(scripts), 1)
+ self.assertEqual(os.path.basename(scripts[0]), 'exy')
+
+
+ def test_excludedPreamble(self):
+ """
+ L{dist.getScripts} includes neither C{"_preamble.py"} nor
+ C{"_preamble.pyc"}.
+ """
+ basedir = FilePath(self.mktemp())
+ bin = basedir.child('bin')
+ bin.makedirs()
+ bin.child('_preamble.py').setContent('some preamble code\n')
+ bin.child('_preamble.pyc').setContent('some preamble byte code\n')
+ bin.child('program').setContent('good program code\n')
+ scripts = dist.getScripts("", basedir=basedir.path)
+ self.assertEqual(scripts, [bin.child('program').path])
+
+
+ def test_scriptsInRelease(self):
+ """
+ getScripts should return the scripts associated with a project
+ in the context of a released subproject tarball.
+ """
+ basedir = self.mktemp()
+ os.mkdir(basedir)
+ os.mkdir(os.path.join(basedir, 'bin'))
+ f = open(os.path.join(basedir, 'bin', 'exy'), 'w')
+ f.write('yay')
+ f.close()
+ scripts = dist.getScripts('proj', basedir=basedir)
+ self.assertEqual(len(scripts), 1)
+ self.assertEqual(os.path.basename(scripts[0]), 'exy')
+
+
+ def test_noScriptsInSVN(self):
+ """
+ When calling getScripts for a project which doesn't actually
+ have any scripts, in the context of an SVN checkout, an
+ empty list should be returned.
+ """
+ basedir = self.mktemp()
+ os.mkdir(basedir)
+ os.mkdir(os.path.join(basedir, 'bin'))
+ os.mkdir(os.path.join(basedir, 'bin', 'otherproj'))
+ scripts = dist.getScripts('noscripts', basedir=basedir)
+ self.assertEqual(scripts, [])
+
+
+ def test_getScriptsTopLevel(self):
+ """
+ Passing the empty string to getScripts returns scripts that are (only)
+ in the top level bin directory.
+ """
+ basedir = FilePath(self.mktemp())
+ basedir.createDirectory()
+ bindir = basedir.child("bin")
+ bindir.createDirectory()
+ included = bindir.child("included")
+ included.setContent("yay included")
+ subdir = bindir.child("subdir")
+ subdir.createDirectory()
+ subdir.child("not-included").setContent("not included")
+
+ scripts = dist.getScripts("", basedir=basedir.path)
+ self.assertEqual(scripts, [included.path])
+
+
+ def test_noScriptsInSubproject(self):
+ """
+ When calling getScripts for a project which doesn't actually
+ have any scripts in the context of that project's individual
+ project structure, an empty list should be returned.
+ """
+ basedir = self.mktemp()
+ os.mkdir(basedir)
+ scripts = dist.getScripts('noscripts', basedir=basedir)
+ self.assertEqual(scripts, [])
+
+
+
+class FakeModule(object):
+ """
+ A fake module, suitable for dependency injection in testing.
+ """
+ def __init__(self, attrs):
+ """
+ Initializes a fake module.
+
+ @param attrs: The attrs that will be accessible on the module.
+ @type attrs: C{dict} of C{str} (Python names) to objects
+ """
+ self._attrs = attrs
+
+ def __getattr__(self, name):
+ """
+ Gets an attribute of this fake module from its attrs.
+
+ @raise AttributeError: When the requested attribute is missing.
+ """
+ try:
+ return self._attrs[name]
+ except KeyError:
+ raise AttributeError()
+
+
+
+fakeCPythonPlatform = FakeModule({"python_implementation": lambda: "CPython"})
+fakeOtherPlatform = FakeModule({"python_implementation": lambda: "lvhpy"})
+emptyPlatform = FakeModule({})
+
+
+
+class WithPlatformTests(TestCase):
+ """
+ Tests for L{_checkCPython} when used with a (fake) recent C{platform}
+ module.
+ """
+ def test_cpython(self):
+ """
+ L{_checkCPython} returns C{True} when C{platform.python_implementation}
+ says we're running on CPython.
+ """
+ self.assertTrue(dist._checkCPython(platform=fakeCPythonPlatform))
+
+
+ def test_other(self):
+ """
+ L{_checkCPython} returns C{False} when C{platform.python_implementation}
+ says we're not running on CPython.
+ """
+ self.assertFalse(dist._checkCPython(platform=fakeOtherPlatform))
+
+
+
+fakeCPythonSys = FakeModule({"subversion": ("CPython", None, None)})
+fakeOtherSys = FakeModule({"subversion": ("lvhpy", None, None)})
+
+
+def _checkCPythonWithEmptyPlatform(sys):
+ """
+ A partially applied L{_checkCPython} that uses an empty C{platform}
+ module (otherwise the code this test case is supposed to test won't
+ even be called).
+ """
+ return dist._checkCPython(platform=emptyPlatform, sys=sys)
+
+
+
+class WithSubversionTest(TestCase):
+ """
+ Tests for L{_checkCPython} when used with a (fake) recent (2.5+)
+ C{sys.subversion}. This is effectively only relevant for 2.5, since 2.6 and
+ beyond have L{platform.python_implementation}, which is tried first.
+ """
+ def test_cpython(self):
+ """
+ L{_checkCPython} returns C{True} when C{platform.python_implementation}
+ is unavailable and C{sys.subversion} says we're running on CPython.
+ """
+ isCPython = _checkCPythonWithEmptyPlatform(fakeCPythonSys)
+ self.assertTrue(isCPython)
+
+
+ def test_other(self):
+ """
+ L{_checkCPython} returns C{False} when C{platform.python_implementation}
+ is unavailable and C{sys.subversion} says we're not running on CPython.
+ """
+ isCPython = _checkCPythonWithEmptyPlatform(fakeOtherSys)
+ self.assertFalse(isCPython)
+
+
+
+oldCPythonSys = FakeModule({"modules": {}})
+oldPypySys = FakeModule({"modules": {"__pypy__": None}})
+
+
+class OldPythonsFallbackTest(TestCase):
+ """
+ Tests for L{_checkCPython} when used on a Python 2.4-like platform, when
+ neither C{platform.python_implementation} nor C{sys.subversion} is
+ available.
+ """
+ def test_cpython(self):
+ """
+ L{_checkCPython} returns C{True} when both
+ C{platform.python_implementation} and C{sys.subversion} are unavailable
+ and there is no C{__pypy__} module in C{sys.modules}.
+ """
+ isCPython = _checkCPythonWithEmptyPlatform(oldCPythonSys)
+ self.assertTrue(isCPython)
+
+
+ def test_pypy(self):
+ """
+ L{_checkCPython} returns C{False} when both
+ C{platform.python_implementation} and C{sys.subversion} are unavailable
+ and there is a C{__pypy__} module in C{sys.modules}.
+ """
+ isCPython = _checkCPythonWithEmptyPlatform(oldPypySys)
+ self.assertFalse(isCPython)
diff --git a/twisted/python/test/test_fakepwd.py b/twisted/python/test/test_fakepwd.py
new file mode 100644
index 0000000..47dc470
--- /dev/null
+++ b/twisted/python/test/test_fakepwd.py
@@ -0,0 +1,386 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.fakepwd}.
+"""
+
+try:
+ import pwd
+except ImportError:
+ pwd = None
+
+try:
+ import spwd
+except ImportError:
+ spwd = None
+
+import os
+from operator import getitem
+
+from twisted.trial.unittest import TestCase
+from twisted.python.fakepwd import UserDatabase, ShadowDatabase
+from twisted.python.compat import set
+
+
+class UserDatabaseTestsMixin:
+ """
+ L{UserDatabaseTestsMixin} defines tests which apply to any user database
+ implementation. Subclasses should mix it in, implement C{setUp} to create
+ C{self.database} bound to a user database instance, and implement
+ C{getExistingUserInfo} to return information about a user (such information
+ should be unique per test method).
+ """
+ def test_getpwuid(self):
+ """
+ I{getpwuid} accepts a uid and returns the user record associated with
+ it.
+ """
+ for i in range(2):
+ # Get some user which exists in the database.
+ username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
+
+ # Now try to look it up and make sure the result is correct.
+ entry = self.database.getpwuid(uid)
+ self.assertEqual(entry.pw_name, username)
+ self.assertEqual(entry.pw_passwd, password)
+ self.assertEqual(entry.pw_uid, uid)
+ self.assertEqual(entry.pw_gid, gid)
+ self.assertEqual(entry.pw_gecos, gecos)
+ self.assertEqual(entry.pw_dir, dir)
+ self.assertEqual(entry.pw_shell, shell)
+
+
+ def test_noSuchUID(self):
+ """
+ I{getpwuid} raises L{KeyError} when passed a uid which does not exist
+ in the user database.
+ """
+ self.assertRaises(KeyError, self.database.getpwuid, -13)
+
+
+ def test_getpwnam(self):
+ """
+ I{getpwnam} accepts a username and returns the user record associated
+ with it.
+ """
+ for i in range(2):
+ # Get some user which exists in the database.
+ username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
+
+ # Now try to look it up and make sure the result is correct.
+ entry = self.database.getpwnam(username)
+ self.assertEqual(entry.pw_name, username)
+ self.assertEqual(entry.pw_passwd, password)
+ self.assertEqual(entry.pw_uid, uid)
+ self.assertEqual(entry.pw_gid, gid)
+ self.assertEqual(entry.pw_gecos, gecos)
+ self.assertEqual(entry.pw_dir, dir)
+ self.assertEqual(entry.pw_shell, shell)
+
+
+ def test_noSuchName(self):
+ """
+ I{getpwnam} raises L{KeyError} when passed a username which does not
+ exist in the user database.
+ """
+ self.assertRaises(
+ KeyError, self.database.getpwnam,
+ 'no' 'such' 'user' 'exists' 'the' 'name' 'is' 'too' 'long' 'and' 'has'
+ '\1' 'in' 'it' 'too')
+
+
+ def test_recordLength(self):
+ """
+ The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
+ has a length.
+ """
+ db = self.database
+ username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
+ for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
+ self.assertIsInstance(len(entry), int)
+ self.assertEquals(len(entry), 7)
+
+
+ def test_recordIndexable(self):
+ """
+ The user record returned by I{getpwuid}, I{getpwnam}, and I{getpwall}
+ is indexable, with successive indexes starting from 0 corresponding to
+ the values of the C{pw_name}, C{pw_passwd}, C{pw_uid}, C{pw_gid},
+ C{pw_gecos}, C{pw_dir}, and C{pw_shell} attributes, respectively.
+ """
+ db = self.database
+ username, password, uid, gid, gecos, dir, shell = self.getExistingUserInfo()
+ for entry in [db.getpwuid(uid), db.getpwnam(username), db.getpwall()[0]]:
+ self.assertEqual(entry[0], username)
+ self.assertEqual(entry[1], password)
+ self.assertEqual(entry[2], uid)
+ self.assertEqual(entry[3], gid)
+ self.assertEqual(entry[4], gecos)
+ self.assertEqual(entry[5], dir)
+ self.assertEqual(entry[6], shell)
+
+ self.assertEqual(len(entry), len(list(entry)))
+ self.assertRaises(IndexError, getitem, entry, 7)
+
+
+
+class UserDatabaseTests(TestCase, UserDatabaseTestsMixin):
+ """
+ Tests for L{UserDatabase}.
+ """
+ def setUp(self):
+ """
+ Create a L{UserDatabase} with no user data in it.
+ """
+ self.database = UserDatabase()
+ self._counter = 0
+
+
+ def getExistingUserInfo(self):
+ """
+ Add a new user to C{self.database} and return its information.
+ """
+ self._counter += 1
+ suffix = '_' + str(self._counter)
+ username = 'username' + suffix
+ password = 'password' + suffix
+ uid = self._counter
+ gid = self._counter + 1000
+ gecos = 'gecos' + suffix
+ dir = 'dir' + suffix
+ shell = 'shell' + suffix
+
+ self.database.addUser(username, password, uid, gid, gecos, dir, shell)
+ return (username, password, uid, gid, gecos, dir, shell)
+
+
+ def test_addUser(self):
+ """
+ L{UserDatabase.addUser} accepts seven arguments, one for each field of
+ a L{pwd.struct_passwd}, and makes the new record available via
+ L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
+ L{UserDatabase.getpwall}.
+ """
+ username = 'alice'
+ password = 'secr3t'
+ uid = 123
+ gid = 456
+ gecos = 'Alice,,,'
+ home = '/users/alice'
+ shell = '/usr/bin/foosh'
+
+ db = self.database
+ db.addUser(username, password, uid, gid, gecos, home, shell)
+
+ for [entry] in [[db.getpwuid(uid)], [db.getpwnam(username)],
+ db.getpwall()]:
+ self.assertEqual(entry.pw_name, username)
+ self.assertEqual(entry.pw_passwd, password)
+ self.assertEqual(entry.pw_uid, uid)
+ self.assertEqual(entry.pw_gid, gid)
+ self.assertEqual(entry.pw_gecos, gecos)
+ self.assertEqual(entry.pw_dir, home)
+ self.assertEqual(entry.pw_shell, shell)
+
+
+
+class PwdModuleTests(TestCase, UserDatabaseTestsMixin):
+ """
+ L{PwdModuleTests} runs the tests defined by L{UserDatabaseTestsMixin}
+ against the built-in C{pwd} module. This serves to verify that
+ L{UserDatabase} is really a fake of that API.
+ """
+ if pwd is None:
+ skip = "Cannot verify UserDatabase against pwd without pwd"
+ else:
+ database = pwd
+
+ def setUp(self):
+ self._users = iter(self.database.getpwall())
+ self._uids = set()
+
+
+ def getExistingUserInfo(self):
+ """
+ Read and return the next record from C{self._users}, filtering out
+ any records with previously seen uid values (as these cannot be
+ found with C{getpwuid} and only cause trouble).
+ """
+ while True:
+ entry = self._users.next()
+ uid = entry.pw_uid
+ if uid not in self._uids:
+ self._uids.add(uid)
+ return entry
+
+
+
+class ShadowDatabaseTestsMixin:
+ """
+ L{ShadowDatabaseTestsMixin} defines tests which apply to any shadow user
+ database implementation. Subclasses should mix it in, implement C{setUp} to
+ create C{self.database} bound to a shadow user database instance, and
+ implement C{getExistingUserInfo} to return information about a user (such
+ information should be unique per test method).
+ """
+ def test_getspnam(self):
+ """
+ L{getspnam} accepts a username and returns the user record associated
+ with it.
+ """
+ for i in range(2):
+ # Get some user which exists in the database.
+ (username, password, lastChange, min, max, warn, inact, expire,
+ flag) = self.getExistingUserInfo()
+
+ entry = self.database.getspnam(username)
+ self.assertEquals(entry.sp_nam, username)
+ self.assertEquals(entry.sp_pwd, password)
+ self.assertEquals(entry.sp_lstchg, lastChange)
+ self.assertEquals(entry.sp_min, min)
+ self.assertEquals(entry.sp_max, max)
+ self.assertEquals(entry.sp_warn, warn)
+ self.assertEquals(entry.sp_inact, inact)
+ self.assertEquals(entry.sp_expire, expire)
+ self.assertEquals(entry.sp_flag, flag)
+
+
+ def test_noSuchName(self):
+ """
+ I{getspnam} raises L{KeyError} when passed a username which does not
+ exist in the user database.
+ """
+ self.assertRaises(KeyError, self.database.getspnam, "alice")
+
+
+ def test_recordLength(self):
+ """
+ The shadow user record returned by I{getspnam} and I{getspall} has a
+ length.
+ """
+ db = self.database
+ username = self.getExistingUserInfo()[0]
+ for entry in [db.getspnam(username), db.getspall()[0]]:
+ self.assertIsInstance(len(entry), int)
+ self.assertEquals(len(entry), 9)
+
+
+ def test_recordIndexable(self):
+ """
+ The shadow user record returned by I{getpwnam} and I{getspall} is
+ indexable, with successive indexes starting from 0 corresponding to the
+ values of the C{sp_nam}, C{sp_pwd}, C{sp_lstchg}, C{sp_min}, C{sp_max},
+ C{sp_warn}, C{sp_inact}, C{sp_expire}, and C{sp_flag} attributes,
+ respectively.
+ """
+ db = self.database
+ (username, password, lastChange, min, max, warn, inact, expire,
+ flag) = self.getExistingUserInfo()
+ for entry in [db.getspnam(username), db.getspall()[0]]:
+ self.assertEquals(entry[0], username)
+ self.assertEquals(entry[1], password)
+ self.assertEquals(entry[2], lastChange)
+ self.assertEquals(entry[3], min)
+ self.assertEquals(entry[4], max)
+ self.assertEquals(entry[5], warn)
+ self.assertEquals(entry[6], inact)
+ self.assertEquals(entry[7], expire)
+ self.assertEquals(entry[8], flag)
+
+ self.assertEquals(len(entry), len(list(entry)))
+ self.assertRaises(IndexError, getitem, entry, 9)
+
+
+
+class ShadowDatabaseTests(TestCase, ShadowDatabaseTestsMixin):
+ """
+ Tests for L{ShadowDatabase}.
+ """
+ def setUp(self):
+ """
+ Create a L{ShadowDatabase} with no user data in it.
+ """
+ self.database = ShadowDatabase()
+ self._counter = 0
+
+
+ def getExistingUserInfo(self):
+ """
+ Add a new user to C{self.database} and return its information.
+ """
+ self._counter += 1
+ suffix = '_' + str(self._counter)
+ username = 'username' + suffix
+ password = 'password' + suffix
+ lastChange = self._counter + 1
+ min = self._counter + 2
+ max = self._counter + 3
+ warn = self._counter + 4
+ inact = self._counter + 5
+ expire = self._counter + 6
+ flag = self._counter + 7
+
+ self.database.addUser(username, password, lastChange, min, max, warn,
+ inact, expire, flag)
+ return (username, password, lastChange, min, max, warn, inact,
+ expire, flag)
+
+
+ def test_addUser(self):
+ """
+ L{UserDatabase.addUser} accepts seven arguments, one for each field of
+ a L{pwd.struct_passwd}, and makes the new record available via
+ L{UserDatabase.getpwuid}, L{UserDatabase.getpwnam}, and
+ L{UserDatabase.getpwall}.
+ """
+ username = 'alice'
+ password = 'secr3t'
+ lastChange = 17
+ min = 42
+ max = 105
+ warn = 12
+ inact = 3
+ expire = 400
+ flag = 3
+
+ db = self.database
+ db.addUser(username, password, lastChange, min, max, warn, inact,
+ expire, flag)
+
+ for [entry] in [[db.getspnam(username)], db.getspall()]:
+ self.assertEquals(entry.sp_nam, username)
+ self.assertEquals(entry.sp_pwd, password)
+ self.assertEquals(entry.sp_lstchg, lastChange)
+ self.assertEquals(entry.sp_min, min)
+ self.assertEquals(entry.sp_max, max)
+ self.assertEquals(entry.sp_warn, warn)
+ self.assertEquals(entry.sp_inact, inact)
+ self.assertEquals(entry.sp_expire, expire)
+ self.assertEquals(entry.sp_flag, flag)
+
+
+
+class SPwdModuleTests(TestCase, ShadowDatabaseTestsMixin):
+ """
+ L{SPwdModuleTests} runs the tests defined by L{ShadowDatabaseTestsMixin}
+ against the built-in C{spwd} module. This serves to verify that
+ L{ShadowDatabase} is really a fake of that API.
+ """
+ if spwd is None:
+ skip = "Cannot verify ShadowDatabase against spwd without spwd"
+ elif os.getuid() != 0:
+ skip = "Cannot access shadow user database except as root"
+ else:
+ database = spwd
+
+ def setUp(self):
+ self._users = iter(self.database.getspall())
+
+
+ def getExistingUserInfo(self):
+ """
+ Read and return the next record from C{self._users}.
+ """
+ return self._users.next()
+
diff --git a/twisted/python/test/test_hashlib.py b/twisted/python/test/test_hashlib.py
new file mode 100644
index 0000000..b50997c
--- /dev/null
+++ b/twisted/python/test/test_hashlib.py
@@ -0,0 +1,90 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.hashlib}
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.hashlib import md5, sha1
+
+
+class HashObjectTests(TestCase):
+ """
+ Tests for the hash object APIs presented by L{hashlib}, C{md5} and C{sha1}.
+ """
+ def test_md5(self):
+ """
+ L{hashlib.md5} returns an object which can be used to compute an MD5
+ hash as defined by U{RFC 1321<http://www.ietf.org/rfc/rfc1321.txt>}.
+ """
+ # Test the result using values from section A.5 of the RFC.
+ self.assertEqual(
+ md5().hexdigest(), "d41d8cd98f00b204e9800998ecf8427e")
+ self.assertEqual(
+ md5("a").hexdigest(), "0cc175b9c0f1b6a831c399e269772661")
+ self.assertEqual(
+ md5("abc").hexdigest(), "900150983cd24fb0d6963f7d28e17f72")
+ self.assertEqual(
+ md5("message digest").hexdigest(),
+ "f96b697d7cb7938d525a2f31aaf161d0")
+ self.assertEqual(
+ md5("abcdefghijklmnopqrstuvwxyz").hexdigest(),
+ "c3fcd3d76192e4007dfb496cca67e13b")
+ self.assertEqual(
+ md5("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ "0123456789").hexdigest(),
+ "d174ab98d277d9f5a5611c2c9f419d9f")
+ self.assertEqual(
+ md5("1234567890123456789012345678901234567890123456789012345678901"
+ "2345678901234567890").hexdigest(),
+ "57edf4a22be3c955ac49da2e2107b67a")
+
+ # It should have digest and update methods, too.
+ self.assertEqual(
+ md5().digest().encode('hex'),
+ "d41d8cd98f00b204e9800998ecf8427e")
+ hash = md5()
+ hash.update("a")
+ self.assertEqual(
+ hash.digest().encode('hex'),
+ "0cc175b9c0f1b6a831c399e269772661")
+
+ # Instances of it should have a digest_size attribute
+ self.assertEqual(md5().digest_size, 16)
+
+
+ def test_sha1(self):
+ """
+ L{hashlib.sha1} returns an object which can be used to compute a SHA1
+ hash as defined by U{RFC 3174<http://tools.ietf.org/rfc/rfc3174.txt>}.
+ """
+ def format(s):
+ return ''.join(s.split()).lower()
+ # Test the result using values from section 7.3 of the RFC.
+ self.assertEqual(
+ sha1("abc").hexdigest(),
+ format(
+ "A9 99 3E 36 47 06 81 6A BA 3E 25 71 78 50 C2 6C 9C D0 D8 9D"))
+ self.assertEqual(
+ sha1("abcdbcdecdefdefgefghfghighijhi"
+ "jkijkljklmklmnlmnomnopnopq").hexdigest(),
+ format(
+ "84 98 3E 44 1C 3B D2 6E BA AE 4A A1 F9 51 29 E5 E5 46 70 F1"))
+
+ # It should have digest and update methods, too.
+ self.assertEqual(
+ sha1("abc").digest().encode('hex'),
+ format(
+ "A9 99 3E 36 47 06 81 6A BA 3E 25 71 78 50 C2 6C 9C D0 D8 9D"))
+ hash = sha1()
+ hash.update("abc")
+ self.assertEqual(
+ hash.digest().encode('hex'),
+ format(
+ "A9 99 3E 36 47 06 81 6A BA 3E 25 71 78 50 C2 6C 9C D0 D8 9D"))
+
+ # Instances of it should have a digest_size attribute.
+ self.assertEqual(
+ sha1().digest_size, 20)
diff --git a/twisted/python/test/test_htmlizer.py b/twisted/python/test/test_htmlizer.py
new file mode 100644
index 0000000..38e607a
--- /dev/null
+++ b/twisted/python/test/test_htmlizer.py
@@ -0,0 +1,41 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.htmlizer}.
+"""
+
+from StringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+from twisted.python.htmlizer import filter
+
+
+class FilterTests(TestCase):
+ """
+ Tests for L{twisted.python.htmlizer.filter}.
+ """
+ def test_empty(self):
+ """
+ If passed an empty input file, L{filter} writes a I{pre} tag containing
+ only an end marker to the output file.
+ """
+ input = StringIO("")
+ output = StringIO()
+ filter(input, output)
+ self.assertEqual(output.getvalue(), '<pre><span class="py-src-endmarker"></span></pre>\n')
+
+
+ def test_variable(self):
+ """
+ If passed an input file containing a variable access, L{filter} writes
+ a I{pre} tag containing a I{py-src-variable} span containing the
+ variable.
+ """
+ input = StringIO("foo\n")
+ output = StringIO()
+ filter(input, output)
+ self.assertEqual(
+ output.getvalue(),
+ '<pre><span class="py-src-variable">foo</span><span class="py-src-newline">\n'
+ '</span><span class="py-src-endmarker"></span></pre>\n')
diff --git a/twisted/python/test/test_inotify.py b/twisted/python/test/test_inotify.py
new file mode 100644
index 0000000..a6cea65
--- /dev/null
+++ b/twisted/python/test/test_inotify.py
@@ -0,0 +1,120 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python._inotify}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.python.runtime import platform
+
+if platform.supportsINotify():
+ from ctypes import c_int, c_uint32, c_char_p
+ from twisted.python import _inotify
+ from twisted.python._inotify import (
+ INotifyError, initializeModule, init, add)
+else:
+ _inotify = None
+
+
+
+class INotifyTests(TestCase):
+ """
+ Tests for L{twisted.python._inotify}.
+ """
+ if _inotify is None:
+ skip = "This platform doesn't support INotify."
+
+ def test_missingInit(self):
+ """
+ If the I{libc} object passed to L{initializeModule} has no
+ C{inotify_init} attribute, L{ImportError} is raised.
+ """
+ class libc:
+ def inotify_add_watch(self):
+ pass
+ def inotify_rm_watch(self):
+ pass
+ self.assertRaises(ImportError, initializeModule, libc())
+
+
+ def test_missingAdd(self):
+ """
+ If the I{libc} object passed to L{initializeModule} has no
+ C{inotify_add_watch} attribute, L{ImportError} is raised.
+ """
+ class libc:
+ def inotify_init(self):
+ pass
+ def inotify_rm_watch(self):
+ pass
+ self.assertRaises(ImportError, initializeModule, libc())
+
+
+ def test_missingRemove(self):
+ """
+ If the I{libc} object passed to L{initializeModule} has no
+ C{inotify_rm_watch} attribute, L{ImportError} is raised.
+ """
+ class libc:
+ def inotify_init(self):
+ pass
+ def inotify_add_watch(self):
+ pass
+ self.assertRaises(ImportError, initializeModule, libc())
+
+
+ def test_setTypes(self):
+ """
+ If the I{libc} object passed to L{initializeModule} has all of the
+ necessary attributes, it sets the C{argtypes} and C{restype} attributes
+ of the three ctypes methods used from libc.
+ """
+ class libc:
+ def inotify_init(self):
+ pass
+ inotify_init = staticmethod(inotify_init)
+
+ def inotify_rm_watch(self):
+ pass
+ inotify_rm_watch = staticmethod(inotify_rm_watch)
+
+ def inotify_add_watch(self):
+ pass
+ inotify_add_watch = staticmethod(inotify_add_watch)
+
+ c = libc()
+ initializeModule(c)
+ self.assertEqual(c.inotify_init.argtypes, [])
+ self.assertEqual(c.inotify_init.restype, c_int)
+
+ self.assertEqual(c.inotify_rm_watch.argtypes, [c_int, c_int])
+ self.assertEqual(c.inotify_rm_watch.restype, c_int)
+
+ self.assertEqual(
+ c.inotify_add_watch.argtypes, [c_int, c_char_p, c_uint32])
+ self.assertEqual(c.inotify_add_watch.restype, c_int)
+
+
+ def test_failedInit(self):
+ """
+ If C{inotify_init} returns a negative number, L{init} raises
+ L{INotifyError}.
+ """
+ class libc:
+ def inotify_init(self):
+ return -1
+ self.patch(_inotify, 'libc', libc())
+ self.assertRaises(INotifyError, init)
+
+
+ def test_failedAddWatch(self):
+ """
+ If C{inotify_add_watch} returns a negative number, L{add}
+ raises L{INotifyError}.
+ """
+ class libc:
+ def inotify_add_watch(self, fd, path, mask):
+ return -1
+ self.patch(_inotify, 'libc', libc())
+ self.assertRaises(INotifyError, add, 3, '/foo', 0)
diff --git a/twisted/python/test/test_release.py b/twisted/python/test/test_release.py
new file mode 100644
index 0000000..55f360e
--- /dev/null
+++ b/twisted/python/test/test_release.py
@@ -0,0 +1,2564 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.release} and L{twisted.python._release}.
+
+All of these tests are skipped on platforms other than Linux, as the release is
+only ever performed on Linux.
+"""
+
+
+import warnings
+import operator
+import os, sys, signal
+from StringIO import StringIO
+import tarfile
+from xml.dom import minidom as dom
+
+from datetime import date
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.compat import set
+from twisted.python.procutils import which
+from twisted.python import release
+from twisted.python.filepath import FilePath
+from twisted.python.versions import Version
+from twisted.python._release import _changeVersionInFile, getNextVersion
+from twisted.python._release import findTwistedProjects, replaceInFile
+from twisted.python._release import replaceProjectVersion
+from twisted.python._release import updateTwistedVersionInformation, Project
+from twisted.python._release import generateVersionFileData
+from twisted.python._release import changeAllProjectVersions
+from twisted.python._release import VERSION_OFFSET, DocBuilder, ManBuilder
+from twisted.python._release import NoDocumentsFound, filePathDelta
+from twisted.python._release import CommandFailed, BookBuilder
+from twisted.python._release import DistributionBuilder, APIBuilder
+from twisted.python._release import BuildAPIDocsScript
+from twisted.python._release import buildAllTarballs, runCommand
+from twisted.python._release import UncleanWorkingDirectory, NotWorkingDirectory
+from twisted.python._release import ChangeVersionsScript, BuildTarballsScript
+from twisted.python._release import NewsBuilder
+
+if os.name != 'posix':
+ skip = "Release toolchain only supported on POSIX."
+else:
+ skip = None
+
+
+# Check a bunch of dependencies to skip tests if necessary.
+try:
+ from twisted.lore.scripts import lore
+except ImportError:
+ loreSkip = "Lore is not present."
+else:
+ loreSkip = skip
+
+
+try:
+ import pydoctor.driver
+ # it might not be installed, or it might use syntax not available in
+ # this version of Python.
+except (ImportError, SyntaxError):
+ pydoctorSkip = "Pydoctor is not present."
+else:
+ if getattr(pydoctor, "version_info", (0,)) < (0, 1):
+ pydoctorSkip = "Pydoctor is too old."
+ else:
+ pydoctorSkip = skip
+
+
+if which("latex") and which("dvips") and which("ps2pdf13"):
+ latexSkip = skip
+else:
+ latexSkip = "LaTeX is not available."
+
+
+if which("svn") and which("svnadmin"):
+ svnSkip = skip
+else:
+ svnSkip = "svn or svnadmin is not present."
+
+
+def genVersion(*args, **kwargs):
+ """
+ A convenience for generating _version.py data.
+
+ @param args: Arguments to pass to L{Version}.
+ @param kwargs: Keyword arguments to pass to L{Version}.
+ """
+ return generateVersionFileData(Version(*args, **kwargs))
+
+
+
+class StructureAssertingMixin(object):
+ """
+ A mixin for L{TestCase} subclasses which provides some methods for asserting
+ the structure and contents of directories and files on the filesystem.
+ """
+ def createStructure(self, root, dirDict):
+ """
+ Create a set of directories and files given a dict defining their
+ structure.
+
+ @param root: The directory in which to create the structure. It must
+ already exist.
+ @type root: L{FilePath}
+
+ @param dirDict: The dict defining the structure. Keys should be strings
+ naming files, values should be strings describing file contents OR
+ dicts describing subdirectories. All files are written in binary
+ mode. Any string values are assumed to describe text files and
+ will have their newlines replaced with the platform-native newline
+ convention. For example::
+
+ {"foofile": "foocontents",
+ "bardir": {"barfile": "bar\ncontents"}}
+ @type dirDict: C{dict}
+ """
+ for x in dirDict:
+ child = root.child(x)
+ if isinstance(dirDict[x], dict):
+ child.createDirectory()
+ self.createStructure(child, dirDict[x])
+ else:
+ child.setContent(dirDict[x].replace('\n', os.linesep))
+
+ def assertStructure(self, root, dirDict):
+ """
+ Assert that a directory is equivalent to one described by a dict.
+
+ @param root: The filesystem directory to compare.
+ @type root: L{FilePath}
+ @param dirDict: The dict that should describe the contents of the
+ directory. It should be the same structure as the C{dirDict}
+ parameter to L{createStructure}.
+ @type dirDict: C{dict}
+ """
+ children = [x.basename() for x in root.children()]
+ for x in dirDict:
+ child = root.child(x)
+ if isinstance(dirDict[x], dict):
+ self.assertTrue(child.isdir(), "%s is not a dir!"
+ % (child.path,))
+ self.assertStructure(child, dirDict[x])
+ else:
+ a = child.getContent().replace(os.linesep, '\n')
+ self.assertEqual(a, dirDict[x], child.path)
+ children.remove(x)
+ if children:
+ self.fail("There were extra children in %s: %s"
+ % (root.path, children))
+
+
+ def assertExtractedStructure(self, outputFile, dirDict):
+ """
+ Assert that a tarfile content is equivalent to one described by a dict.
+
+ @param outputFile: The tar file built by L{DistributionBuilder}.
+ @type outputFile: L{FilePath}.
+ @param dirDict: The dict that should describe the contents of the
+ directory. It should be the same structure as the C{dirDict}
+ parameter to L{createStructure}.
+ @type dirDict: C{dict}
+ """
+ tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2")
+ extracted = FilePath(self.mktemp())
+ extracted.createDirectory()
+ for info in tarFile:
+ tarFile.extract(info, path=extracted.path)
+ self.assertStructure(extracted.children()[0], dirDict)
+
+
+
+class ChangeVersionTest(TestCase, StructureAssertingMixin):
+ """
+ Twisted has the ability to change versions.
+ """
+
+ def makeFile(self, relativePath, content):
+ """
+ Create a file with the given content relative to a temporary directory.
+
+ @param relativePath: The basename of the file to create.
+ @param content: The content that the file will have.
+ @return: The filename.
+ """
+ baseDirectory = FilePath(self.mktemp())
+ directory, filename = os.path.split(relativePath)
+ directory = baseDirectory.preauthChild(directory)
+ directory.makedirs()
+ file = directory.child(filename)
+ directory.child(filename).setContent(content)
+ return file
+
+
+ def test_getNextVersion(self):
+ """
+ When calculating the next version to release when a release is
+ happening in the same year as the last release, the minor version
+ number is incremented.
+ """
+ now = date.today()
+ major = now.year - VERSION_OFFSET
+ version = Version("twisted", major, 9, 0)
+ self.assertEqual(getNextVersion(version, now=now),
+ Version("twisted", major, 10, 0))
+
+
+ def test_getNextVersionAfterYearChange(self):
+ """
+ When calculating the next version to release when a release is
+ happening in a later year, the minor version number is reset to 0.
+ """
+ now = date.today()
+ major = now.year - VERSION_OFFSET
+ version = Version("twisted", major - 1, 9, 0)
+ self.assertEqual(getNextVersion(version, now=now),
+ Version("twisted", major, 0, 0))
+
+
+ def test_changeVersionInFile(self):
+ """
+ _changeVersionInFile replaces the old version information in a file
+ with the given new version information.
+ """
+ # The version numbers are arbitrary, the name is only kind of
+ # arbitrary.
+ packageName = 'foo'
+ oldVersion = Version(packageName, 2, 5, 0)
+ file = self.makeFile('README',
+ "Hello and welcome to %s." % oldVersion.base())
+
+ newVersion = Version(packageName, 7, 6, 0)
+ _changeVersionInFile(oldVersion, newVersion, file.path)
+
+ self.assertEqual(file.getContent(),
+ "Hello and welcome to %s." % newVersion.base())
+
+
+ def test_changeAllProjectVersions(self):
+ """
+ L{changeAllProjectVersions} changes all version numbers in _version.py
+ and README files for all projects as well as in the the top-level
+ README file.
+ """
+ root = FilePath(self.mktemp())
+ root.createDirectory()
+ structure = {
+ "README": "Hi this is 1.0.0.",
+ "twisted": {
+ "topfiles": {
+ "README": "Hi this is 1.0.0"},
+ "_version.py":
+ genVersion("twisted", 1, 0, 0),
+ "web": {
+ "topfiles": {
+ "README": "Hi this is 1.0.0"},
+ "_version.py": genVersion("twisted.web", 1, 0, 0)
+ }}}
+ self.createStructure(root, structure)
+ changeAllProjectVersions(root, Version("lol", 1, 0, 2))
+ outStructure = {
+ "README": "Hi this is 1.0.2.",
+ "twisted": {
+ "topfiles": {
+ "README": "Hi this is 1.0.2"},
+ "_version.py":
+ genVersion("twisted", 1, 0, 2),
+ "web": {
+ "topfiles": {
+ "README": "Hi this is 1.0.2"},
+ "_version.py": genVersion("twisted.web", 1, 0, 2),
+ }}}
+ self.assertStructure(root, outStructure)
+
+
+ def test_changeAllProjectVersionsPreRelease(self):
+ """
+ L{changeAllProjectVersions} changes all version numbers in _version.py
+ and README files for all projects as well as in the the top-level
+ README file. If the old version was a pre-release, it will change the
+ version in NEWS files as well.
+ """
+ root = FilePath(self.mktemp())
+ root.createDirectory()
+ coreNews = ("Twisted Core 1.0.0 (2009-12-25)\n"
+ "===============================\n"
+ "\n")
+ webNews = ("Twisted Web 1.0.0pre1 (2009-12-25)\n"
+ "==================================\n"
+ "\n")
+ structure = {
+ "README": "Hi this is 1.0.0.",
+ "NEWS": coreNews + webNews,
+ "twisted": {
+ "topfiles": {
+ "README": "Hi this is 1.0.0",
+ "NEWS": coreNews},
+ "_version.py":
+ genVersion("twisted", 1, 0, 0),
+ "web": {
+ "topfiles": {
+ "README": "Hi this is 1.0.0pre1",
+ "NEWS": webNews},
+ "_version.py": genVersion("twisted.web", 1, 0, 0, 1)
+ }}}
+ self.createStructure(root, structure)
+ changeAllProjectVersions(root, Version("lol", 1, 0, 2), '2010-01-01')
+ coreNews = (
+ "Twisted Core 1.0.0 (2009-12-25)\n"
+ "===============================\n"
+ "\n")
+ webNews = ("Twisted Web 1.0.2 (2010-01-01)\n"
+ "==============================\n"
+ "\n")
+ outStructure = {
+ "README": "Hi this is 1.0.2.",
+ "NEWS": coreNews + webNews,
+ "twisted": {
+ "topfiles": {
+ "README": "Hi this is 1.0.2",
+ "NEWS": coreNews},
+ "_version.py":
+ genVersion("twisted", 1, 0, 2),
+ "web": {
+ "topfiles": {
+ "README": "Hi this is 1.0.2",
+ "NEWS": webNews},
+ "_version.py": genVersion("twisted.web", 1, 0, 2),
+ }}}
+ self.assertStructure(root, outStructure)
+
+
+
+class ProjectTest(TestCase):
+ """
+ There is a first-class representation of a project.
+ """
+
+ def assertProjectsEqual(self, observedProjects, expectedProjects):
+ """
+ Assert that two lists of L{Project}s are equal.
+ """
+ self.assertEqual(len(observedProjects), len(expectedProjects))
+ observedProjects = sorted(observedProjects,
+ key=operator.attrgetter('directory'))
+ expectedProjects = sorted(expectedProjects,
+ key=operator.attrgetter('directory'))
+ for observed, expected in zip(observedProjects, expectedProjects):
+ self.assertEqual(observed.directory, expected.directory)
+
+
+ def makeProject(self, version, baseDirectory=None):
+ """
+ Make a Twisted-style project in the given base directory.
+
+ @param baseDirectory: The directory to create files in
+ (as a L{FilePath).
+ @param version: The version information for the project.
+ @return: L{Project} pointing to the created project.
+ """
+ if baseDirectory is None:
+ baseDirectory = FilePath(self.mktemp())
+ baseDirectory.createDirectory()
+ segments = version.package.split('.')
+ directory = baseDirectory
+ for segment in segments:
+ directory = directory.child(segment)
+ if not directory.exists():
+ directory.createDirectory()
+ directory.child('__init__.py').setContent('')
+ directory.child('topfiles').createDirectory()
+ directory.child('topfiles').child('README').setContent(version.base())
+ replaceProjectVersion(
+ directory.child('_version.py').path, version)
+ return Project(directory)
+
+
+ def makeProjects(self, *versions):
+ """
+ Create a series of projects underneath a temporary base directory.
+
+ @return: A L{FilePath} for the base directory.
+ """
+ baseDirectory = FilePath(self.mktemp())
+ baseDirectory.createDirectory()
+ for version in versions:
+ self.makeProject(version, baseDirectory)
+ return baseDirectory
+
+
+ def test_getVersion(self):
+ """
+ Project objects know their version.
+ """
+ version = Version('foo', 2, 1, 0)
+ project = self.makeProject(version)
+ self.assertEqual(project.getVersion(), version)
+
+
+ def test_updateVersion(self):
+ """
+ Project objects know how to update the version numbers in those
+ projects.
+ """
+ project = self.makeProject(Version("bar", 2, 1, 0))
+ newVersion = Version("bar", 3, 2, 9)
+ project.updateVersion(newVersion)
+ self.assertEqual(project.getVersion(), newVersion)
+ self.assertEqual(
+ project.directory.child("topfiles").child("README").getContent(),
+ "3.2.9")
+
+
+ def test_repr(self):
+ """
+ The representation of a Project is Project(directory).
+ """
+ foo = Project(FilePath('bar'))
+ self.assertEqual(
+ repr(foo), 'Project(%r)' % (foo.directory))
+
+
+ def test_findTwistedStyleProjects(self):
+ """
+ findTwistedStyleProjects finds all projects underneath a particular
+ directory. A 'project' is defined by the existence of a 'topfiles'
+ directory and is returned as a Project object.
+ """
+ baseDirectory = self.makeProjects(
+ Version('foo', 2, 3, 0), Version('foo.bar', 0, 7, 4))
+ projects = findTwistedProjects(baseDirectory)
+ self.assertProjectsEqual(
+ projects,
+ [Project(baseDirectory.child('foo')),
+ Project(baseDirectory.child('foo').child('bar'))])
+
+
+ def test_updateTwistedVersionInformation(self):
+ """
+ Update Twisted version information in the top-level project and all of
+ the subprojects.
+ """
+ baseDirectory = FilePath(self.mktemp())
+ baseDirectory.createDirectory()
+ now = date.today()
+
+ projectName = 'foo'
+ oldVersion = Version(projectName, 2, 5, 0)
+ newVersion = getNextVersion(oldVersion, now=now)
+
+ project = self.makeProject(oldVersion, baseDirectory)
+
+ updateTwistedVersionInformation(baseDirectory, now=now)
+
+ self.assertEqual(project.getVersion(), newVersion)
+ self.assertEqual(
+ project.directory.child('topfiles').child('README').getContent(),
+ newVersion.base())
+
+
+
+class UtilityTest(TestCase):
+ """
+ Tests for various utility functions for releasing.
+ """
+
+ def test_chdir(self):
+ """
+ Test that the runChdirSafe is actually safe, i.e., it still
+ changes back to the original directory even if an error is
+ raised.
+ """
+ cwd = os.getcwd()
+ def chAndBreak():
+ os.mkdir('releaseCh')
+ os.chdir('releaseCh')
+ 1/0
+ self.assertRaises(ZeroDivisionError,
+ release.runChdirSafe, chAndBreak)
+ self.assertEqual(cwd, os.getcwd())
+
+
+
+ def test_replaceInFile(self):
+ """
+ L{replaceInFile} replaces data in a file based on a dict. A key from
+ the dict that is found in the file is replaced with the corresponding
+ value.
+ """
+ in_ = 'foo\nhey hey $VER\nbar\n'
+ outf = open('release.replace', 'w')
+ outf.write(in_)
+ outf.close()
+
+ expected = in_.replace('$VER', '2.0.0')
+ replaceInFile('release.replace', {'$VER': '2.0.0'})
+ self.assertEqual(open('release.replace').read(), expected)
+
+
+ expected = expected.replace('2.0.0', '3.0.0')
+ replaceInFile('release.replace', {'2.0.0': '3.0.0'})
+ self.assertEqual(open('release.replace').read(), expected)
+
+
+
+class VersionWritingTest(TestCase):
+ """
+ Tests for L{replaceProjectVersion}.
+ """
+
+ def test_replaceProjectVersion(self):
+ """
+ L{replaceProjectVersion} writes a Python file that defines a
+ C{version} variable that corresponds to the given name and version
+ number.
+ """
+ replaceProjectVersion("test_project",
+ Version("twisted.test_project", 0, 82, 7))
+ ns = {'__name___': 'twisted.test_project'}
+ execfile("test_project", ns)
+ self.assertEqual(ns["version"].base(), "0.82.7")
+
+
+ def test_replaceProjectVersionWithPrerelease(self):
+ """
+ L{replaceProjectVersion} will write a Version instantiation that
+ includes a prerelease parameter if necessary.
+ """
+ replaceProjectVersion("test_project",
+ Version("twisted.test_project", 0, 82, 7,
+ prerelease=8))
+ ns = {'__name___': 'twisted.test_project'}
+ execfile("test_project", ns)
+ self.assertEqual(ns["version"].base(), "0.82.7pre8")
+
+
+
+class BuilderTestsMixin(object):
+ """
+ A mixin class which provides various methods for creating sample Lore input
+ and output.
+
+ @cvar template: The lore template that will be used to prepare sample
+ output.
+ @type template: C{str}
+
+ @ivar docCounter: A counter which is incremented every time input is
+ generated and which is included in the documents.
+ @type docCounter: C{int}
+ """
+ template = '''
+ <html>
+ <head><title>Yo:</title></head>
+ <body>
+ <div class="body" />
+ <a href="index.html">Index</a>
+ <span class="version">Version: </span>
+ </body>
+ </html>
+ '''
+
+ def setUp(self):
+ """
+ Initialize the doc counter which ensures documents are unique.
+ """
+ self.docCounter = 0
+
+
+ def assertXMLEqual(self, first, second):
+ """
+ Verify that two strings represent the same XML document.
+ """
+ self.assertEqual(
+ dom.parseString(first).toxml(),
+ dom.parseString(second).toxml())
+
+
+ def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):
+ """
+ Get the correct HTML output for the arbitrary input returned by
+ L{getArbitraryLoreInput} for the given parameters.
+
+ @param version: The version string to include in the output.
+ @type version: C{str}
+ @param counter: A counter to include in the output.
+ @type counter: C{int}
+ """
+ document = """\
+<?xml version="1.0"?><html>
+ <head><title>Yo:Hi! Title: %(count)d</title></head>
+ <body>
+ <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div>
+ <a href="%(prefix)sindex.html">Index</a>
+ <span class="version">Version: %(version)s</span>
+ </body>
+ </html>"""
+ # Try to normalize irrelevant whitespace.
+ return dom.parseString(
+ document % {"count": counter, "prefix": prefix,
+ "version": version,
+ "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')
+
+
+ def getArbitraryLoreInput(self, counter):
+ """
+ Get an arbitrary, unique (for this test case) string of lore input.
+
+ @param counter: A counter to include in the input.
+ @type counter: C{int}
+ """
+ template = (
+ '<html>'
+ '<head><title>Hi! Title: %(count)s</title></head>'
+ '<body>'
+ 'Hi! %(count)s'
+ '<div class="API">foobar</div>'
+ '</body>'
+ '</html>')
+ return template % {"count": counter}
+
+
+ def getArbitraryLoreInputAndOutput(self, version, prefix="",
+ apiBaseURL="%s"):
+ """
+ Get an input document along with expected output for lore run on that
+ output document, assuming an appropriately-specified C{self.template}.
+
+ @param version: A version string to include in the input and output.
+ @type version: C{str}
+ @param prefix: The prefix to include in the link to the index.
+ @type prefix: C{str}
+
+ @return: A two-tuple of input and expected output.
+ @rtype: C{(str, str)}.
+ """
+ self.docCounter += 1
+ return (self.getArbitraryLoreInput(self.docCounter),
+ self.getArbitraryOutput(version, self.docCounter,
+ prefix=prefix, apiBaseURL=apiBaseURL))
+
+
+ def getArbitraryManInput(self):
+ """
+ Get an arbitrary man page content.
+ """
+ return """.TH MANHOLE "1" "August 2001" "" ""
+.SH NAME
+manhole \- Connect to a Twisted Manhole service
+.SH SYNOPSIS
+.B manhole
+.SH DESCRIPTION
+manhole is a GTK interface to Twisted Manhole services. You can execute python
+code as if at an interactive Python console inside a running Twisted process
+with this."""
+
+
+ def getArbitraryManLoreOutput(self):
+ """
+ Get an arbitrary lore input document which represents man-to-lore
+ output based on the man page returned from L{getArbitraryManInput}
+ """
+ return """\
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html><head>
+<title>MANHOLE.1</title></head>
+<body>
+
+<h1>MANHOLE.1</h1>
+
+<h2>NAME</h2>
+
+<p>manhole - Connect to a Twisted Manhole service
+</p>
+
+<h2>SYNOPSIS</h2>
+
+<p><strong>manhole</strong> </p>
+
+<h2>DESCRIPTION</h2>
+
+<p>manhole is a GTK interface to Twisted Manhole services. You can execute python
+code as if at an interactive Python console inside a running Twisted process
+with this.</p>
+
+</body>
+</html>
+"""
+
+ def getArbitraryManHTMLOutput(self, version, prefix=""):
+ """
+ Get an arbitrary lore output document which represents the lore HTML
+ output based on the input document returned from
+ L{getArbitraryManLoreOutput}.
+
+ @param version: A version string to include in the document.
+ @type version: C{str}
+ @param prefix: The prefix to include in the link to the index.
+ @type prefix: C{str}
+ """
+ # Try to normalize the XML a little bit.
+ return dom.parseString("""\
+<?xml version="1.0" ?><html>
+ <head><title>Yo:MANHOLE.1</title></head>
+ <body>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>manhole - Connect to a Twisted Manhole service
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>manhole</strong> </p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>manhole is a GTK interface to Twisted Manhole services. You can execute python
+code as if at an interactive Python console inside a running Twisted process
+with this.</p>
+
+</div>
+ <a href="%(prefix)sindex.html">Index</a>
+ <span class="version">Version: %(version)s</span>
+ </body>
+ </html>""" % {
+ 'prefix': prefix, 'version': version}).toxml("utf-8")
+
+
+
+class DocBuilderTestCase(TestCase, BuilderTestsMixin):
+ """
+ Tests for L{DocBuilder}.
+
+ Note for future maintainers: The exact byte equality assertions throughout
+ this suite may need to be updated due to minor differences in lore. They
+ should not be taken to mean that Lore must maintain the same byte format
+ forever. Feel free to update the tests when Lore changes, but please be
+ careful.
+ """
+ skip = loreSkip
+
+ def setUp(self):
+ """
+ Set up a few instance variables that will be useful.
+
+ @ivar builder: A plain L{DocBuilder}.
+ @ivar docCounter: An integer to be used as a counter by the
+ C{getArbitrary...} methods.
+ @ivar howtoDir: A L{FilePath} representing a directory to be used for
+ containing Lore documents.
+ @ivar templateFile: A L{FilePath} representing a file with
+ C{self.template} as its content.
+ """
+ BuilderTestsMixin.setUp(self)
+ self.builder = DocBuilder()
+ self.howtoDir = FilePath(self.mktemp())
+ self.howtoDir.createDirectory()
+ self.templateFile = self.howtoDir.child("template.tpl")
+ self.templateFile.setContent(self.template)
+
+
+ def test_build(self):
+ """
+ The L{DocBuilder} runs lore on all .xhtml files within a directory.
+ """
+ version = "1.2.3"
+ input1, output1 = self.getArbitraryLoreInputAndOutput(version)
+ input2, output2 = self.getArbitraryLoreInputAndOutput(version)
+
+ self.howtoDir.child("one.xhtml").setContent(input1)
+ self.howtoDir.child("two.xhtml").setContent(input2)
+
+ self.builder.build(version, self.howtoDir, self.howtoDir,
+ self.templateFile)
+ out1 = self.howtoDir.child('one.html')
+ out2 = self.howtoDir.child('two.html')
+ self.assertXMLEqual(out1.getContent(), output1)
+ self.assertXMLEqual(out2.getContent(), output2)
+
+
+ def test_noDocumentsFound(self):
+ """
+ The C{build} method raises L{NoDocumentsFound} if there are no
+ .xhtml files in the given directory.
+ """
+ self.assertRaises(
+ NoDocumentsFound,
+ self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,
+ self.templateFile)
+
+
+ def test_parentDocumentLinking(self):
+ """
+ The L{DocBuilder} generates correct links from documents to
+ template-generated links like stylesheets and index backreferences.
+ """
+ input = self.getArbitraryLoreInput(0)
+ tutoDir = self.howtoDir.child("tutorial")
+ tutoDir.createDirectory()
+ tutoDir.child("child.xhtml").setContent(input)
+ self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile)
+ outFile = tutoDir.child('child.html')
+ self.assertIn('<a href="../index.html">Index</a>',
+ outFile.getContent())
+
+
+ def test_siblingDirectoryDocumentLinking(self):
+ """
+ It is necessary to generate documentation in a directory foo/bar where
+ stylesheet and indexes are located in foo/baz. Such resources should be
+ appropriately linked to.
+ """
+ input = self.getArbitraryLoreInput(0)
+ resourceDir = self.howtoDir.child("resources")
+ docDir = self.howtoDir.child("docs")
+ docDir.createDirectory()
+ docDir.child("child.xhtml").setContent(input)
+ self.builder.build("1.2.3", resourceDir, docDir, self.templateFile)
+ outFile = docDir.child('child.html')
+ self.assertIn('<a href="../resources/index.html">Index</a>',
+ outFile.getContent())
+
+
+ def test_apiLinking(self):
+ """
+ The L{DocBuilder} generates correct links from documents to API
+ documentation.
+ """
+ version = "1.2.3"
+ input, output = self.getArbitraryLoreInputAndOutput(version)
+ self.howtoDir.child("one.xhtml").setContent(input)
+
+ self.builder.build(version, self.howtoDir, self.howtoDir,
+ self.templateFile, "scheme:apilinks/%s.ext")
+ out = self.howtoDir.child('one.html')
+ self.assertIn(
+ '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',
+ out.getContent())
+
+
+ def test_deleteInput(self):
+ """
+ L{DocBuilder.build} can be instructed to delete the input files after
+ generating the output based on them.
+ """
+ input1 = self.getArbitraryLoreInput(0)
+ self.howtoDir.child("one.xhtml").setContent(input1)
+ self.builder.build("whatever", self.howtoDir, self.howtoDir,
+ self.templateFile, deleteInput=True)
+ self.assertTrue(self.howtoDir.child('one.html').exists())
+ self.assertFalse(self.howtoDir.child('one.xhtml').exists())
+
+
+ def test_doNotDeleteInput(self):
+ """
+ Input will not be deleted by default.
+ """
+ input1 = self.getArbitraryLoreInput(0)
+ self.howtoDir.child("one.xhtml").setContent(input1)
+ self.builder.build("whatever", self.howtoDir, self.howtoDir,
+ self.templateFile)
+ self.assertTrue(self.howtoDir.child('one.html').exists())
+ self.assertTrue(self.howtoDir.child('one.xhtml').exists())
+
+
+ def test_getLinkrelToSameDirectory(self):
+ """
+ If the doc and resource directories are the same, the linkrel should be
+ an empty string.
+ """
+ linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),
+ FilePath("/foo/bar"))
+ self.assertEqual(linkrel, "")
+
+
+ def test_getLinkrelToParentDirectory(self):
+ """
+ If the doc directory is a child of the resource directory, the linkrel
+ should make use of '..'.
+ """
+ linkrel = self.builder.getLinkrel(FilePath("/foo"),
+ FilePath("/foo/bar"))
+ self.assertEqual(linkrel, "../")
+
+
+ def test_getLinkrelToSibling(self):
+ """
+ If the doc directory is a sibling of the resource directory, the
+ linkrel should make use of '..' and a named segment.
+ """
+ linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
+ FilePath("/foo/examples"))
+ self.assertEqual(linkrel, "../howto/")
+
+
+ def test_getLinkrelToUncle(self):
+ """
+ If the doc directory is a sibling of the parent of the resource
+ directory, the linkrel should make use of multiple '..'s and a named
+ segment.
+ """
+ linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
+ FilePath("/foo/examples/quotes"))
+ self.assertEqual(linkrel, "../../howto/")
+
+
+
+class APIBuilderTestCase(TestCase):
+ """
+ Tests for L{APIBuilder}.
+ """
+ skip = pydoctorSkip
+
+ def test_build(self):
+ """
+ L{APIBuilder.build} writes an index file which includes the name of the
+ project specified.
+ """
+ stdout = StringIO()
+ self.patch(sys, 'stdout', stdout)
+
+ projectName = "Foobar"
+ packageName = "quux"
+ projectURL = "scheme:project"
+ sourceURL = "scheme:source"
+ docstring = "text in docstring"
+ privateDocstring = "should also appear in output"
+
+ inputPath = FilePath(self.mktemp()).child(packageName)
+ inputPath.makedirs()
+ inputPath.child("__init__.py").setContent(
+ "def foo():\n"
+ " '%s'\n"
+ "def _bar():\n"
+ " '%s'" % (docstring, privateDocstring))
+
+ outputPath = FilePath(self.mktemp())
+ outputPath.makedirs()
+
+ builder = APIBuilder()
+ builder.build(projectName, projectURL, sourceURL, inputPath, outputPath)
+
+ indexPath = outputPath.child("index.html")
+ self.assertTrue(
+ indexPath.exists(),
+ "API index %r did not exist." % (outputPath.path,))
+ self.assertIn(
+ '<a href="%s">%s</a>' % (projectURL, projectName),
+ indexPath.getContent(),
+ "Project name/location not in file contents.")
+
+ quuxPath = outputPath.child("quux.html")
+ self.assertTrue(
+ quuxPath.exists(),
+ "Package documentation file %r did not exist." % (quuxPath.path,))
+ self.assertIn(
+ docstring, quuxPath.getContent(),
+ "Docstring not in package documentation file.")
+ self.assertIn(
+ '<a href="%s/%s">View Source</a>' % (sourceURL, packageName),
+ quuxPath.getContent())
+ self.assertIn(
+ '<a href="%s/%s/__init__.py#L1" class="functionSourceLink">' % (
+ sourceURL, packageName),
+ quuxPath.getContent())
+ self.assertIn(privateDocstring, quuxPath.getContent())
+
+ # There should also be a page for the foo function in quux.
+ self.assertTrue(quuxPath.sibling('quux.foo.html').exists())
+
+ self.assertEqual(stdout.getvalue(), '')
+
+
+ def test_buildWithPolicy(self):
+ """
+ L{BuildAPIDocsScript.buildAPIDocs} builds the API docs with values
+ appropriate for the Twisted project.
+ """
+ stdout = StringIO()
+ self.patch(sys, 'stdout', stdout)
+ docstring = "text in docstring"
+
+ projectRoot = FilePath(self.mktemp())
+ packagePath = projectRoot.child("twisted")
+ packagePath.makedirs()
+ packagePath.child("__init__.py").setContent(
+ "def foo():\n"
+ " '%s'\n" % (docstring,))
+ packagePath.child("_version.py").setContent(
+ genVersion("twisted", 1, 0, 0))
+ outputPath = FilePath(self.mktemp())
+
+ script = BuildAPIDocsScript()
+ script.buildAPIDocs(projectRoot, outputPath)
+
+ indexPath = outputPath.child("index.html")
+ self.assertTrue(
+ indexPath.exists(),
+ "API index %r did not exist." % (outputPath.path,))
+ self.assertIn(
+ '<a href="http://twistedmatrix.com/">Twisted</a>',
+ indexPath.getContent(),
+ "Project name/location not in file contents.")
+
+ twistedPath = outputPath.child("twisted.html")
+ self.assertTrue(
+ twistedPath.exists(),
+ "Package documentation file %r did not exist."
+ % (twistedPath.path,))
+ self.assertIn(
+ docstring, twistedPath.getContent(),
+ "Docstring not in package documentation file.")
+ #Here we check that it figured out the correct version based on the
+ #source code.
+ self.assertIn(
+ '<a href="http://twistedmatrix.com/trac/browser/tags/releases/'
+ 'twisted-1.0.0/twisted">View Source</a>',
+ twistedPath.getContent())
+
+ self.assertEqual(stdout.getvalue(), '')
+
+
+ def test_apiBuilderScriptMainRequiresTwoArguments(self):
+ """
+ SystemExit is raised when the incorrect number of command line
+ arguments are passed to the API building script.
+ """
+ script = BuildAPIDocsScript()
+ self.assertRaises(SystemExit, script.main, [])
+ self.assertRaises(SystemExit, script.main, ["foo"])
+ self.assertRaises(SystemExit, script.main, ["foo", "bar", "baz"])
+
+
+ def test_apiBuilderScriptMain(self):
+ """
+ The API building script invokes the same code that
+ L{test_buildWithPolicy} tests.
+ """
+ script = BuildAPIDocsScript()
+ calls = []
+ script.buildAPIDocs = lambda a, b: calls.append((a, b))
+ script.main(["hello", "there"])
+ self.assertEqual(calls, [(FilePath("hello"), FilePath("there"))])
+
+
+
+class ManBuilderTestCase(TestCase, BuilderTestsMixin):
+ """
+ Tests for L{ManBuilder}.
+ """
+ skip = loreSkip
+
+ def setUp(self):
+ """
+ Set up a few instance variables that will be useful.
+
+ @ivar builder: A plain L{ManBuilder}.
+ @ivar manDir: A L{FilePath} representing a directory to be used for
+ containing man pages.
+ """
+ BuilderTestsMixin.setUp(self)
+ self.builder = ManBuilder()
+ self.manDir = FilePath(self.mktemp())
+ self.manDir.createDirectory()
+
+
+ def test_noDocumentsFound(self):
+ """
+ L{ManBuilder.build} raises L{NoDocumentsFound} if there are no
+ .1 files in the given directory.
+ """
+ self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)
+
+
+ def test_build(self):
+ """
+ Check that L{ManBuilder.build} find the man page in the directory, and
+ successfully produce a Lore content.
+ """
+ manContent = self.getArbitraryManInput()
+ self.manDir.child('test1.1').setContent(manContent)
+ self.builder.build(self.manDir)
+ output = self.manDir.child('test1-man.xhtml').getContent()
+ expected = self.getArbitraryManLoreOutput()
+ # No-op on *nix, fix for windows
+ expected = expected.replace('\n', os.linesep)
+ self.assertEqual(output, expected)
+
+
+ def test_toHTML(self):
+ """
+ Check that the content output by C{build} is compatible as input of
+ L{DocBuilder.build}.
+ """
+ manContent = self.getArbitraryManInput()
+ self.manDir.child('test1.1').setContent(manContent)
+ self.builder.build(self.manDir)
+
+ templateFile = self.manDir.child("template.tpl")
+ templateFile.setContent(DocBuilderTestCase.template)
+ docBuilder = DocBuilder()
+ docBuilder.build("1.2.3", self.manDir, self.manDir,
+ templateFile)
+ output = self.manDir.child('test1-man.html').getContent()
+
+ self.assertXMLEqual(
+ output,
+ """\
+<?xml version="1.0" ?><html>
+ <head><title>Yo:MANHOLE.1</title></head>
+ <body>
+ <div class="content">
+
+<span/>
+
+<h2>NAME<a name="auto0"/></h2>
+
+<p>manhole - Connect to a Twisted Manhole service
+</p>
+
+<h2>SYNOPSIS<a name="auto1"/></h2>
+
+<p><strong>manhole</strong> </p>
+
+<h2>DESCRIPTION<a name="auto2"/></h2>
+
+<p>manhole is a GTK interface to Twisted Manhole services. You can execute python
+code as if at an interactive Python console inside a running Twisted process
+with this.</p>
+
+</div>
+ <a href="index.html">Index</a>
+ <span class="version">Version: 1.2.3</span>
+ </body>
+ </html>""")
+
+
+
+class BookBuilderTests(TestCase, BuilderTestsMixin):
+ """
+ Tests for L{BookBuilder}.
+ """
+ skip = latexSkip or loreSkip
+
+ def setUp(self):
+ """
+ Make a directory into which to place temporary files.
+ """
+ self.docCounter = 0
+ self.howtoDir = FilePath(self.mktemp())
+ self.howtoDir.makedirs()
+ self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+
+
+ def tearDown(self):
+ signal.signal(signal.SIGCHLD, self.oldHandler)
+
+
+ def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL=None):
+ """
+ Create and return a C{str} containing the LaTeX document which is
+ expected as the output for processing the result of the document
+ returned by C{self.getArbitraryLoreInput(counter)}.
+ """
+ path = self.howtoDir.child("%d.xhtml" % (counter,)).path
+ return (
+ r'\section{Hi! Title: %(count)s\label{%(path)s}}'
+ '\n'
+ r'Hi! %(count)sfoobar') % {'count': counter, 'path': path}
+
+
+ def test_runSuccess(self):
+ """
+ L{BookBuilder.run} executes the command it is passed and returns a
+ string giving the stdout and stderr of the command if it completes
+ successfully.
+ """
+ builder = BookBuilder()
+ self.assertEqual(
+ builder.run([
+ sys.executable, '-c',
+ 'import sys; '
+ 'sys.stdout.write("hi\\n"); '
+ 'sys.stdout.flush(); '
+ 'sys.stderr.write("bye\\n"); '
+ 'sys.stderr.flush()']),
+ "hi\nbye\n")
+
+
+ def test_runFailed(self):
+ """
+ L{BookBuilder.run} executes the command it is passed and raises
+ L{CommandFailed} if it completes unsuccessfully.
+ """
+ builder = BookBuilder()
+ exc = self.assertRaises(
+ CommandFailed, builder.run,
+ [sys.executable, '-c', 'print "hi"; raise SystemExit(1)'])
+ self.assertEqual(exc.exitStatus, 1)
+ self.assertEqual(exc.exitSignal, None)
+ self.assertEqual(exc.output, "hi\n")
+
+
+ def test_runSignaled(self):
+ """
+ L{BookBuilder.run} executes the command it is passed and raises
+ L{CommandFailed} if it exits due to a signal.
+ """
+ builder = BookBuilder()
+ exc = self.assertRaises(
+ CommandFailed, builder.run,
+ [sys.executable, '-c',
+ 'import sys; print "hi"; sys.stdout.flush(); '
+ 'import os; os.kill(os.getpid(), 9)'])
+ self.assertEqual(exc.exitSignal, 9)
+ self.assertEqual(exc.exitStatus, None)
+ self.assertEqual(exc.output, "hi\n")
+
+
+ def test_buildTeX(self):
+ """
+ L{BookBuilder.buildTeX} writes intermediate TeX files for all lore
+ input files in a directory.
+ """
+ version = "3.2.1"
+ input1, output1 = self.getArbitraryLoreInputAndOutput(version)
+ input2, output2 = self.getArbitraryLoreInputAndOutput(version)
+
+ # Filenames are chosen by getArbitraryOutput to match the counter used
+ # by getArbitraryLoreInputAndOutput.
+ self.howtoDir.child("1.xhtml").setContent(input1)
+ self.howtoDir.child("2.xhtml").setContent(input2)
+
+ builder = BookBuilder()
+ builder.buildTeX(self.howtoDir)
+ self.assertEqual(self.howtoDir.child("1.tex").getContent(), output1)
+ self.assertEqual(self.howtoDir.child("2.tex").getContent(), output2)
+
+
+ def test_buildTeXRejectsInvalidDirectory(self):
+ """
+ L{BookBuilder.buildTeX} raises L{ValueError} if passed a directory
+ which does not exist.
+ """
+ builder = BookBuilder()
+ self.assertRaises(
+ ValueError, builder.buildTeX, self.howtoDir.temporarySibling())
+
+
+ def test_buildTeXOnlyBuildsXHTML(self):
+ """
+ L{BookBuilder.buildTeX} ignores files which which don't end with
+ ".xhtml".
+ """
+ # Hopefully ">" is always a parse error from microdom!
+ self.howtoDir.child("not-input.dat").setContent(">")
+ self.test_buildTeX()
+
+
+ def test_stdout(self):
+ """
+ L{BookBuilder.buildTeX} does not write to stdout.
+ """
+ stdout = StringIO()
+ self.patch(sys, 'stdout', stdout)
+
+ # Suppress warnings so that if there are any old-style plugins that
+ # lore queries for don't confuse the assertion below. See #3070.
+ self.patch(warnings, 'warn', lambda *a, **kw: None)
+ self.test_buildTeX()
+ self.assertEqual(stdout.getvalue(), '')
+
+
+ def test_buildPDFRejectsInvalidBookFilename(self):
+ """
+ L{BookBuilder.buildPDF} raises L{ValueError} if the book filename does
+ not end with ".tex".
+ """
+ builder = BookBuilder()
+ self.assertRaises(
+ ValueError,
+ builder.buildPDF,
+ FilePath(self.mktemp()).child("foo"),
+ None,
+ None)
+
+
+ def _setupTeXFiles(self):
+ sections = range(3)
+ self._setupTeXSections(sections)
+ return self._setupTeXBook(sections)
+
+
+ def _setupTeXSections(self, sections):
+ for texSectionNumber in sections:
+ texPath = self.howtoDir.child("%d.tex" % (texSectionNumber,))
+ texPath.setContent(self.getArbitraryOutput(
+ "1.2.3", texSectionNumber))
+
+
+ def _setupTeXBook(self, sections):
+ bookTeX = self.howtoDir.child("book.tex")
+ bookTeX.setContent(
+ r"\documentclass{book}" "\n"
+ r"\begin{document}" "\n" +
+ "\n".join([r"\input{%d.tex}" % (n,) for n in sections]) +
+ r"\end{document}" "\n")
+ return bookTeX
+
+
+ def test_buildPDF(self):
+ """
+ L{BookBuilder.buildPDF} creates a PDF given an index tex file and a
+ directory containing .tex files.
+ """
+ bookPath = self._setupTeXFiles()
+ outputPath = FilePath(self.mktemp())
+
+ builder = BookBuilder()
+ builder.buildPDF(bookPath, self.howtoDir, outputPath)
+
+ self.assertTrue(outputPath.exists())
+
+
+ def test_buildPDFLongPath(self):
+ """
+ L{BookBuilder.buildPDF} succeeds even if the paths it is operating on
+ are very long.
+
+ C{ps2pdf13} seems to have problems when path names are long. This test
+ verifies that even if inputs have long paths, generation still
+ succeeds.
+ """
+ # Make it long.
+ self.howtoDir = self.howtoDir.child("x" * 128).child("x" * 128).child("x" * 128)
+ self.howtoDir.makedirs()
+
+ # This will use the above long path.
+ bookPath = self._setupTeXFiles()
+ outputPath = FilePath(self.mktemp())
+
+ builder = BookBuilder()
+ builder.buildPDF(bookPath, self.howtoDir, outputPath)
+
+ self.assertTrue(outputPath.exists())
+
+
+ def test_buildPDFRunsLaTeXThreeTimes(self):
+ """
+ L{BookBuilder.buildPDF} runs C{latex} three times.
+ """
+ class InspectableBookBuilder(BookBuilder):
+ def __init__(self):
+ BookBuilder.__init__(self)
+ self.commands = []
+
+ def run(self, command):
+ """
+ Record the command and then execute it.
+ """
+ self.commands.append(command)
+ return BookBuilder.run(self, command)
+
+ bookPath = self._setupTeXFiles()
+ outputPath = FilePath(self.mktemp())
+
+ builder = InspectableBookBuilder()
+ builder.buildPDF(bookPath, self.howtoDir, outputPath)
+
+ # These string comparisons are very fragile. It would be better to
+ # have a test which asserted the correctness of the contents of the
+ # output files. I don't know how one could do that, though. -exarkun
+ latex1, latex2, latex3, dvips, ps2pdf13 = builder.commands
+ self.assertEqual(latex1, latex2)
+ self.assertEqual(latex2, latex3)
+ self.assertEqual(
+ latex1[:1], ["latex"],
+ "LaTeX command %r does not seem right." % (latex1,))
+ self.assertEqual(
+ latex1[-1:], [bookPath.path],
+ "LaTeX command %r does not end with the book path (%r)." % (
+ latex1, bookPath.path))
+
+ self.assertEqual(
+ dvips[:1], ["dvips"],
+ "dvips command %r does not seem right." % (dvips,))
+ self.assertEqual(
+ ps2pdf13[:1], ["ps2pdf13"],
+ "ps2pdf13 command %r does not seem right." % (ps2pdf13,))
+
+
+ def test_noSideEffects(self):
+ """
+ The working directory is the same before and after a call to
+ L{BookBuilder.buildPDF}. Also the contents of the directory containing
+ the input book are the same before and after the call.
+ """
+ startDir = os.getcwd()
+ bookTeX = self._setupTeXFiles()
+ startTeXSiblings = bookTeX.parent().children()
+ startHowtoChildren = self.howtoDir.children()
+
+ builder = BookBuilder()
+ builder.buildPDF(bookTeX, self.howtoDir, FilePath(self.mktemp()))
+
+ self.assertEqual(startDir, os.getcwd())
+ self.assertEqual(startTeXSiblings, bookTeX.parent().children())
+ self.assertEqual(startHowtoChildren, self.howtoDir.children())
+
+
+ def test_failedCommandProvidesOutput(self):
+ """
+ If a subprocess fails, L{BookBuilder.buildPDF} raises L{CommandFailed}
+ with the subprocess's output and leaves the temporary directory as a
+ sibling of the book path.
+ """
+ bookTeX = FilePath(self.mktemp() + ".tex")
+ builder = BookBuilder()
+ inputState = bookTeX.parent().children()
+ exc = self.assertRaises(
+ CommandFailed,
+ builder.buildPDF,
+ bookTeX, self.howtoDir, FilePath(self.mktemp()))
+ self.assertTrue(exc.output)
+ newOutputState = set(bookTeX.parent().children()) - set(inputState)
+ self.assertEqual(len(newOutputState), 1)
+ workPath = newOutputState.pop()
+ self.assertTrue(
+ workPath.isdir(),
+ "Expected work path %r was not a directory." % (workPath.path,))
+
+
+ def test_build(self):
+ """
+ L{BookBuilder.build} generates a pdf book file from some lore input
+ files.
+ """
+ sections = range(1, 4)
+ for sectionNumber in sections:
+ self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent(
+ self.getArbitraryLoreInput(sectionNumber))
+ bookTeX = self._setupTeXBook(sections)
+ bookPDF = FilePath(self.mktemp())
+
+ builder = BookBuilder()
+ builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
+
+ self.assertTrue(bookPDF.exists())
+
+
+ def test_buildRemovesTemporaryLaTeXFiles(self):
+ """
+ L{BookBuilder.build} removes the intermediate LaTeX files it creates.
+ """
+ sections = range(1, 4)
+ for sectionNumber in sections:
+ self.howtoDir.child("%d.xhtml" % (sectionNumber,)).setContent(
+ self.getArbitraryLoreInput(sectionNumber))
+ bookTeX = self._setupTeXBook(sections)
+ bookPDF = FilePath(self.mktemp())
+
+ builder = BookBuilder()
+ builder.build(self.howtoDir, [self.howtoDir], bookTeX, bookPDF)
+
+ self.assertEqual(
+ set(self.howtoDir.listdir()),
+ set([bookTeX.basename()] + ["%d.xhtml" % (n,) for n in sections]))
+
+
+
+class FilePathDeltaTest(TestCase):
+ """
+ Tests for L{filePathDelta}.
+ """
+
+ def test_filePathDeltaSubdir(self):
+ """
+ L{filePathDelta} can create a simple relative path to a child path.
+ """
+ self.assertEqual(filePathDelta(FilePath("/foo/bar"),
+ FilePath("/foo/bar/baz")),
+ ["baz"])
+
+
+ def test_filePathDeltaSiblingDir(self):
+ """
+ L{filePathDelta} can traverse upwards to create relative paths to
+ siblings.
+ """
+ self.assertEqual(filePathDelta(FilePath("/foo/bar"),
+ FilePath("/foo/baz")),
+ ["..", "baz"])
+
+
+ def test_filePathNoCommonElements(self):
+ """
+ L{filePathDelta} can create relative paths to totally unrelated paths
+ for maximum portability.
+ """
+ self.assertEqual(filePathDelta(FilePath("/foo/bar"),
+ FilePath("/baz/quux")),
+ ["..", "..", "baz", "quux"])
+
+
+ def test_filePathDeltaSimilarEndElements(self):
+ """
+ L{filePathDelta} doesn't take into account final elements when
+ comparing 2 paths, but stops at the first difference.
+ """
+ self.assertEqual(filePathDelta(FilePath("/foo/bar/bar/spam"),
+ FilePath("/foo/bar/baz/spam")),
+ ["..", "..", "baz", "spam"])
+
+
+
+class NewsBuilderTests(TestCase, StructureAssertingMixin):
+ """
+ Tests for L{NewsBuilder}.
+ """
+ def setUp(self):
+ """
+ Create a fake project and stuff some basic structure and content into
+ it.
+ """
+ self.builder = NewsBuilder()
+ self.project = FilePath(self.mktemp())
+ self.project.createDirectory()
+ self.existingText = 'Here is stuff which was present previously.\n'
+ self.createStructure(self.project, {
+ 'NEWS': self.existingText,
+ '5.feature': 'We now support the web.\n',
+ '12.feature': 'The widget is more robust.\n',
+ '15.feature': (
+ 'A very long feature which takes many words to '
+ 'describe with any accuracy was introduced so that '
+ 'the line wrapping behavior of the news generating '
+ 'code could be verified.\n'),
+ '16.feature': (
+ 'A simpler feature\ndescribed on multiple lines\n'
+ 'was added.\n'),
+ '23.bugfix': 'Broken stuff was fixed.\n',
+ '25.removal': 'Stupid stuff was deprecated.\n',
+ '30.misc': '',
+ '35.misc': '',
+ '40.doc': 'foo.bar.Baz.quux',
+ '41.doc': 'writing Foo servers'})
+
+
+ def test_today(self):
+ """
+ L{NewsBuilder._today} returns today's date in YYYY-MM-DD form.
+ """
+ self.assertEqual(
+ self.builder._today(), date.today().strftime('%Y-%m-%d'))
+
+
+ def test_findFeatures(self):
+ """
+ When called with L{NewsBuilder._FEATURE}, L{NewsBuilder._findChanges}
+ returns a list of bugfix ticket numbers and descriptions as a list of
+ two-tuples.
+ """
+ features = self.builder._findChanges(
+ self.project, self.builder._FEATURE)
+ self.assertEqual(
+ features,
+ [(5, "We now support the web."),
+ (12, "The widget is more robust."),
+ (15,
+ "A very long feature which takes many words to describe with "
+ "any accuracy was introduced so that the line wrapping behavior "
+ "of the news generating code could be verified."),
+ (16, "A simpler feature described on multiple lines was added.")])
+
+
+ def test_findBugfixes(self):
+ """
+ When called with L{NewsBuilder._BUGFIX}, L{NewsBuilder._findChanges}
+ returns a list of bugfix ticket numbers and descriptions as a list of
+ two-tuples.
+ """
+ bugfixes = self.builder._findChanges(
+ self.project, self.builder._BUGFIX)
+ self.assertEqual(
+ bugfixes,
+ [(23, 'Broken stuff was fixed.')])
+
+
+ def test_findRemovals(self):
+ """
+ When called with L{NewsBuilder._REMOVAL}, L{NewsBuilder._findChanges}
+ returns a list of removal/deprecation ticket numbers and descriptions
+ as a list of two-tuples.
+ """
+ removals = self.builder._findChanges(
+ self.project, self.builder._REMOVAL)
+ self.assertEqual(
+ removals,
+ [(25, 'Stupid stuff was deprecated.')])
+
+
+ def test_findDocumentation(self):
+ """
+ When called with L{NewsBuilder._DOC}, L{NewsBuilder._findChanges}
+ returns a list of documentation ticket numbers and descriptions as a
+ list of two-tuples.
+ """
+ doc = self.builder._findChanges(
+ self.project, self.builder._DOC)
+ self.assertEqual(
+ doc,
+ [(40, 'foo.bar.Baz.quux'),
+ (41, 'writing Foo servers')])
+
+
+ def test_findMiscellaneous(self):
+ """
+ When called with L{NewsBuilder._MISC}, L{NewsBuilder._findChanges}
+ returns a list of removal/deprecation ticket numbers and descriptions
+ as a list of two-tuples.
+ """
+ misc = self.builder._findChanges(
+ self.project, self.builder._MISC)
+ self.assertEqual(
+ misc,
+ [(30, ''),
+ (35, '')])
+
+
+ def test_writeHeader(self):
+ """
+ L{NewsBuilder._writeHeader} accepts a file-like object opened for
+ writing and a header string and writes out a news file header to it.
+ """
+ output = StringIO()
+ self.builder._writeHeader(output, "Super Awesometastic 32.16")
+ self.assertEqual(
+ output.getvalue(),
+ "Super Awesometastic 32.16\n"
+ "=========================\n"
+ "\n")
+
+
+ def test_writeSection(self):
+ """
+ L{NewsBuilder._writeSection} accepts a file-like object opened for
+ writing, a section name, and a list of ticket information (as returned
+ by L{NewsBuilder._findChanges}) and writes out a section header and all
+ of the given ticket information.
+ """
+ output = StringIO()
+ self.builder._writeSection(
+ output, "Features",
+ [(3, "Great stuff."),
+ (17, "Very long line which goes on and on and on, seemingly "
+ "without end until suddenly without warning it does end.")])
+ self.assertEqual(
+ output.getvalue(),
+ "Features\n"
+ "--------\n"
+ " - Great stuff. (#3)\n"
+ " - Very long line which goes on and on and on, seemingly without end\n"
+ " until suddenly without warning it does end. (#17)\n"
+ "\n")
+
+
+ def test_writeMisc(self):
+ """
+ L{NewsBuilder._writeMisc} accepts a file-like object opened for
+ writing, a section name, and a list of ticket information (as returned
+ by L{NewsBuilder._findChanges} and writes out a section header and all
+ of the ticket numbers, but excludes any descriptions.
+ """
+ output = StringIO()
+ self.builder._writeMisc(
+ output, "Other",
+ [(x, "") for x in range(2, 50, 3)])
+ self.assertEqual(
+ output.getvalue(),
+ "Other\n"
+ "-----\n"
+ " - #2, #5, #8, #11, #14, #17, #20, #23, #26, #29, #32, #35, #38, #41,\n"
+ " #44, #47\n"
+ "\n")
+
+
+ def test_build(self):
+ """
+ L{NewsBuilder.build} updates a NEWS file with new features based on the
+ I{<ticket>.feature} files found in the directory specified.
+ """
+ self.builder.build(
+ self.project, self.project.child('NEWS'),
+ "Super Awesometastic 32.16")
+
+ results = self.project.child('NEWS').getContent()
+ self.assertEqual(
+ results,
+ 'Super Awesometastic 32.16\n'
+ '=========================\n'
+ '\n'
+ 'Features\n'
+ '--------\n'
+ ' - We now support the web. (#5)\n'
+ ' - The widget is more robust. (#12)\n'
+ ' - A very long feature which takes many words to describe with any\n'
+ ' accuracy was introduced so that the line wrapping behavior of the\n'
+ ' news generating code could be verified. (#15)\n'
+ ' - A simpler feature described on multiple lines was added. (#16)\n'
+ '\n'
+ 'Bugfixes\n'
+ '--------\n'
+ ' - Broken stuff was fixed. (#23)\n'
+ '\n'
+ 'Improved Documentation\n'
+ '----------------------\n'
+ ' - foo.bar.Baz.quux (#40)\n'
+ ' - writing Foo servers (#41)\n'
+ '\n'
+ 'Deprecations and Removals\n'
+ '-------------------------\n'
+ ' - Stupid stuff was deprecated. (#25)\n'
+ '\n'
+ 'Other\n'
+ '-----\n'
+ ' - #30, #35\n'
+ '\n\n' + self.existingText)
+
+
+ def test_emptyProjectCalledOut(self):
+ """
+ If no changes exist for a project, I{NEWS} gains a new section for
+ that project that includes some helpful text about how there were no
+ interesting changes.
+ """
+ project = FilePath(self.mktemp()).child("twisted")
+ project.makedirs()
+ self.createStructure(project, {
+ 'NEWS': self.existingText })
+
+ self.builder.build(
+ project, project.child('NEWS'),
+ "Super Awesometastic 32.16")
+ results = project.child('NEWS').getContent()
+ self.assertEqual(
+ results,
+ 'Super Awesometastic 32.16\n'
+ '=========================\n'
+ '\n' +
+ self.builder._NO_CHANGES +
+ '\n\n' + self.existingText)
+
+
+ def test_preserveTicketHint(self):
+ """
+ If a I{NEWS} file begins with the two magic lines which point readers
+ at the issue tracker, those lines are kept at the top of the new file.
+ """
+ news = self.project.child('NEWS')
+ news.setContent(
+ 'Ticket numbers in this file can be looked up by visiting\n'
+ 'http://twistedmatrix.com/trac/ticket/<number>\n'
+ '\n'
+ 'Blah blah other stuff.\n')
+
+ self.builder.build(self.project, news, "Super Awesometastic 32.16")
+
+ self.assertEqual(
+ news.getContent(),
+ 'Ticket numbers in this file can be looked up by visiting\n'
+ 'http://twistedmatrix.com/trac/ticket/<number>\n'
+ '\n'
+ 'Super Awesometastic 32.16\n'
+ '=========================\n'
+ '\n'
+ 'Features\n'
+ '--------\n'
+ ' - We now support the web. (#5)\n'
+ ' - The widget is more robust. (#12)\n'
+ ' - A very long feature which takes many words to describe with any\n'
+ ' accuracy was introduced so that the line wrapping behavior of the\n'
+ ' news generating code could be verified. (#15)\n'
+ ' - A simpler feature described on multiple lines was added. (#16)\n'
+ '\n'
+ 'Bugfixes\n'
+ '--------\n'
+ ' - Broken stuff was fixed. (#23)\n'
+ '\n'
+ 'Improved Documentation\n'
+ '----------------------\n'
+ ' - foo.bar.Baz.quux (#40)\n'
+ ' - writing Foo servers (#41)\n'
+ '\n'
+ 'Deprecations and Removals\n'
+ '-------------------------\n'
+ ' - Stupid stuff was deprecated. (#25)\n'
+ '\n'
+ 'Other\n'
+ '-----\n'
+ ' - #30, #35\n'
+ '\n\n'
+ 'Blah blah other stuff.\n')
+
+
+ def test_emptySectionsOmitted(self):
+ """
+ If there are no changes of a particular type (feature, bugfix, etc), no
+ section for that type is written by L{NewsBuilder.build}.
+ """
+ for ticket in self.project.children():
+ if ticket.splitext()[1] in ('.feature', '.misc', '.doc'):
+ ticket.remove()
+
+ self.builder.build(
+ self.project, self.project.child('NEWS'),
+ 'Some Thing 1.2')
+
+ self.assertEqual(
+ self.project.child('NEWS').getContent(),
+ 'Some Thing 1.2\n'
+ '==============\n'
+ '\n'
+ 'Bugfixes\n'
+ '--------\n'
+ ' - Broken stuff was fixed. (#23)\n'
+ '\n'
+ 'Deprecations and Removals\n'
+ '-------------------------\n'
+ ' - Stupid stuff was deprecated. (#25)\n'
+ '\n\n'
+ 'Here is stuff which was present previously.\n')
+
+
+ def test_duplicatesMerged(self):
+ """
+ If two change files have the same contents, they are merged in the
+ generated news entry.
+ """
+ def feature(s):
+ return self.project.child(s + '.feature')
+ feature('5').copyTo(feature('15'))
+ feature('5').copyTo(feature('16'))
+
+ self.builder.build(
+ self.project, self.project.child('NEWS'),
+ 'Project Name 5.0')
+
+ self.assertEqual(
+ self.project.child('NEWS').getContent(),
+ 'Project Name 5.0\n'
+ '================\n'
+ '\n'
+ 'Features\n'
+ '--------\n'
+ ' - We now support the web. (#5, #15, #16)\n'
+ ' - The widget is more robust. (#12)\n'
+ '\n'
+ 'Bugfixes\n'
+ '--------\n'
+ ' - Broken stuff was fixed. (#23)\n'
+ '\n'
+ 'Improved Documentation\n'
+ '----------------------\n'
+ ' - foo.bar.Baz.quux (#40)\n'
+ ' - writing Foo servers (#41)\n'
+ '\n'
+ 'Deprecations and Removals\n'
+ '-------------------------\n'
+ ' - Stupid stuff was deprecated. (#25)\n'
+ '\n'
+ 'Other\n'
+ '-----\n'
+ ' - #30, #35\n'
+ '\n\n'
+ 'Here is stuff which was present previously.\n')
+
+
+ def createFakeTwistedProject(self):
+ """
+ Create a fake-looking Twisted project to build from.
+ """
+ project = FilePath(self.mktemp()).child("twisted")
+ project.makedirs()
+ self.createStructure(project, {
+ 'NEWS': 'Old boring stuff from the past.\n',
+ '_version.py': genVersion("twisted", 1, 2, 3),
+ 'topfiles': {
+ 'NEWS': 'Old core news.\n',
+ '3.feature': 'Third feature addition.\n',
+ '5.misc': ''},
+ 'conch': {
+ '_version.py': genVersion("twisted.conch", 3, 4, 5),
+ 'topfiles': {
+ 'NEWS': 'Old conch news.\n',
+ '7.bugfix': 'Fixed that bug.\n'}},
+ })
+ return project
+
+
+ def test_buildAll(self):
+ """
+ L{NewsBuilder.buildAll} calls L{NewsBuilder.build} once for each
+ subproject, passing that subproject's I{topfiles} directory as C{path},
+ the I{NEWS} file in that directory as C{output}, and the subproject's
+ name as C{header}, and then again for each subproject with the
+ top-level I{NEWS} file for C{output}. Blacklisted subprojects are
+ skipped.
+ """
+ builds = []
+ builder = NewsBuilder()
+ builder.build = lambda path, output, header: builds.append((
+ path, output, header))
+ builder._today = lambda: '2009-12-01'
+
+ project = self.createFakeTwistedProject()
+ builder.buildAll(project)
+
+ coreTopfiles = project.child("topfiles")
+ coreNews = coreTopfiles.child("NEWS")
+ coreHeader = "Twisted Core 1.2.3 (2009-12-01)"
+
+ conchTopfiles = project.child("conch").child("topfiles")
+ conchNews = conchTopfiles.child("NEWS")
+ conchHeader = "Twisted Conch 3.4.5 (2009-12-01)"
+
+ aggregateNews = project.child("NEWS")
+
+ self.assertEqual(
+ builds,
+ [(conchTopfiles, conchNews, conchHeader),
+ (coreTopfiles, coreNews, coreHeader),
+ (conchTopfiles, aggregateNews, conchHeader),
+ (coreTopfiles, aggregateNews, coreHeader)])
+
+
+ def test_changeVersionInNews(self):
+ """
+ L{NewsBuilder._changeVersions} gets the release date for a given
+ version of a project as a string.
+ """
+ builder = NewsBuilder()
+ builder._today = lambda: '2009-12-01'
+ project = self.createFakeTwistedProject()
+ builder.buildAll(project)
+ newVersion = Version('TEMPLATE', 7, 7, 14)
+ coreNews = project.child('topfiles').child('NEWS')
+ # twisted 1.2.3 is the old version.
+ builder._changeNewsVersion(
+ coreNews, "Core", Version("twisted", 1, 2, 3),
+ newVersion, '2010-01-01')
+ expectedCore = (
+ 'Twisted Core 7.7.14 (2010-01-01)\n'
+ '================================\n'
+ '\n'
+ 'Features\n'
+ '--------\n'
+ ' - Third feature addition. (#3)\n'
+ '\n'
+ 'Other\n'
+ '-----\n'
+ ' - #5\n\n\n')
+ self.assertEqual(
+ expectedCore + 'Old core news.\n', coreNews.getContent())
+
+
+
+class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,
+ TestCase):
+ """
+ Base for tests of L{DistributionBuilder}.
+ """
+ skip = loreSkip
+
+ def setUp(self):
+ BuilderTestsMixin.setUp(self)
+
+ self.rootDir = FilePath(self.mktemp())
+ self.rootDir.createDirectory()
+
+ self.outputDir = FilePath(self.mktemp())
+ self.outputDir.createDirectory()
+ self.builder = DistributionBuilder(self.rootDir, self.outputDir)
+
+
+
+class DistributionBuilderTest(DistributionBuilderTestBase):
+
+ def test_twistedDistribution(self):
+ """
+ The Twisted tarball contains everything in the source checkout, with
+ built documentation.
+ """
+ loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")
+ manInput1 = self.getArbitraryManInput()
+ manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
+ manInput2 = self.getArbitraryManInput()
+ manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
+ coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
+ "10.0.0", prefix="howto/")
+
+ structure = {
+ "README": "Twisted",
+ "unrelated": "x",
+ "LICENSE": "copyright!",
+ "setup.py": "import toplevel",
+ "bin": {"web": {"websetroot": "SET ROOT"},
+ "twistd": "TWISTD"},
+ "twisted":
+ {"web":
+ {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINSTALL",
+ "README": "WEB!"}},
+ "words": {"__init__.py": "import WORDS"},
+ "plugins": {"twisted_web.py": "import WEBPLUG",
+ "twisted_words.py": "import WORDPLUG"}},
+ "doc": {"web": {"howto": {"index.xhtml": loreInput},
+ "man": {"websetroot.1": manInput2}},
+ "core": {"howto": {"template.tpl": self.template},
+ "man": {"twistd.1": manInput1},
+ "index.xhtml": coreIndexInput}}}
+
+ outStructure = {
+ "README": "Twisted",
+ "unrelated": "x",
+ "LICENSE": "copyright!",
+ "setup.py": "import toplevel",
+ "bin": {"web": {"websetroot": "SET ROOT"},
+ "twistd": "TWISTD"},
+ "twisted":
+ {"web": {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINSTALL",
+ "README": "WEB!"}},
+ "words": {"__init__.py": "import WORDS"},
+ "plugins": {"twisted_web.py": "import WEBPLUG",
+ "twisted_words.py": "import WORDPLUG"}},
+ "doc": {"web": {"howto": {"index.html": loreOutput},
+ "man": {"websetroot.1": manInput2,
+ "websetroot-man.html": manOutput2}},
+ "core": {"howto": {"template.tpl": self.template},
+ "man": {"twistd.1": manInput1,
+ "twistd-man.html": manOutput1},
+ "index.html": coreIndexOutput}}}
+
+ self.createStructure(self.rootDir, structure)
+
+ outputFile = self.builder.buildTwisted("10.0.0")
+
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+ def test_subProjectLayout(self):
+ """
+ The subproject tarball includes files like so:
+
+ 1. twisted/<subproject>/topfiles defines the files that will be in the
+ top level in the tarball, except LICENSE, which comes from the real
+ top-level directory.
+ 2. twisted/<subproject> is included, but without the topfiles entry
+ in that directory. No other twisted subpackages are included.
+ 3. twisted/plugins/twisted_<subproject>.py is included, but nothing
+ else in plugins is.
+ """
+ structure = {
+ "README": "HI!@",
+ "unrelated": "x",
+ "LICENSE": "copyright!",
+ "setup.py": "import toplevel",
+ "bin": {"web": {"websetroot": "SET ROOT"},
+ "words": {"im": "#!im"}},
+ "twisted":
+ {"web":
+ {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINSTALL",
+ "README": "WEB!"}},
+ "words": {"__init__.py": "import WORDS"},
+ "plugins": {"twisted_web.py": "import WEBPLUG",
+ "twisted_words.py": "import WORDPLUG"}}}
+
+ outStructure = {
+ "README": "WEB!",
+ "LICENSE": "copyright!",
+ "setup.py": "import WEBINSTALL",
+ "bin": {"websetroot": "SET ROOT"},
+ "twisted": {"web": {"__init__.py": "import WEB"},
+ "plugins": {"twisted_web.py": "import WEBPLUG"}}}
+
+ self.createStructure(self.rootDir, structure)
+
+ outputFile = self.builder.buildSubProject("web", "0.3.0")
+
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+ def test_minimalSubProjectLayout(self):
+ """
+ buildSubProject should work with minimal subprojects.
+ """
+ structure = {
+ "LICENSE": "copyright!",
+ "bin": {},
+ "twisted":
+ {"web": {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINSTALL"}},
+ "plugins": {}}}
+
+ outStructure = {
+ "setup.py": "import WEBINSTALL",
+ "LICENSE": "copyright!",
+ "twisted": {"web": {"__init__.py": "import WEB"}}}
+
+ self.createStructure(self.rootDir, structure)
+
+ outputFile = self.builder.buildSubProject("web", "0.3.0")
+
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+ def test_subProjectDocBuilding(self):
+ """
+ When building a subproject release, documentation should be built with
+ lore.
+ """
+ loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")
+ manInput = self.getArbitraryManInput()
+ manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")
+ structure = {
+ "LICENSE": "copyright!",
+ "twisted": {"web": {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINST"}}},
+ "doc": {"web": {"howto": {"index.xhtml": loreInput},
+ "man": {"twistd.1": manInput}},
+ "core": {"howto": {"template.tpl": self.template}}
+ }
+ }
+
+ outStructure = {
+ "LICENSE": "copyright!",
+ "setup.py": "import WEBINST",
+ "twisted": {"web": {"__init__.py": "import WEB"}},
+ "doc": {"howto": {"index.html": loreOutput},
+ "man": {"twistd.1": manInput,
+ "twistd-man.html": manOutput}}}
+
+ self.createStructure(self.rootDir, structure)
+
+ outputFile = self.builder.buildSubProject("web", "0.3.0")
+
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+ def test_coreProjectLayout(self):
+ """
+ The core tarball looks a lot like a subproject tarball, except it
+ doesn't include:
+
+ - Python packages from other subprojects
+ - plugins from other subprojects
+ - scripts from other subprojects
+ """
+ indexInput, indexOutput = self.getArbitraryLoreInputAndOutput(
+ "8.0.0", prefix="howto/")
+ howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0")
+ specInput, specOutput = self.getArbitraryLoreInputAndOutput(
+ "8.0.0", prefix="../howto/")
+ tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput(
+ "8.0.0", prefix="../")
+
+ structure = {
+ "LICENSE": "copyright!",
+ "twisted": {"__init__.py": "twisted",
+ "python": {"__init__.py": "python",
+ "roots.py": "roots!"},
+ "conch": {"__init__.py": "conch",
+ "unrelated.py": "import conch"},
+ "plugin.py": "plugin",
+ "plugins": {"twisted_web.py": "webplug",
+ "twisted_whatever.py": "include!",
+ "cred.py": "include!"},
+ "topfiles": {"setup.py": "import CORE",
+ "README": "core readme"}},
+ "doc": {"core": {"howto": {"template.tpl": self.template,
+ "index.xhtml": howtoInput,
+ "tutorial":
+ {"index.xhtml": tutorialInput}},
+ "specifications": {"index.xhtml": specInput},
+ "examples": {"foo.py": "foo.py"},
+ "index.xhtml": indexInput},
+ "web": {"howto": {"index.xhtml": "webindex"}}},
+ "bin": {"twistd": "TWISTD",
+ "web": {"websetroot": "websetroot"}}
+ }
+
+ outStructure = {
+ "LICENSE": "copyright!",
+ "setup.py": "import CORE",
+ "README": "core readme",
+ "twisted": {"__init__.py": "twisted",
+ "python": {"__init__.py": "python",
+ "roots.py": "roots!"},
+ "plugin.py": "plugin",
+ "plugins": {"twisted_whatever.py": "include!",
+ "cred.py": "include!"}},
+ "doc": {"howto": {"template.tpl": self.template,
+ "index.html": howtoOutput,
+ "tutorial": {"index.html": tutorialOutput}},
+ "specifications": {"index.html": specOutput},
+ "examples": {"foo.py": "foo.py"},
+ "index.html": indexOutput},
+ "bin": {"twistd": "TWISTD"},
+ }
+
+ self.createStructure(self.rootDir, structure)
+ outputFile = self.builder.buildCore("8.0.0")
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+ def test_apiBaseURL(self):
+ """
+ DistributionBuilder builds documentation with the specified
+ API base URL.
+ """
+ apiBaseURL = "http://%s"
+ builder = DistributionBuilder(self.rootDir, self.outputDir,
+ apiBaseURL=apiBaseURL)
+ loreInput, loreOutput = self.getArbitraryLoreInputAndOutput(
+ "0.3.0", apiBaseURL=apiBaseURL)
+ structure = {
+ "LICENSE": "copyright!",
+ "twisted": {"web": {"__init__.py": "import WEB",
+ "topfiles": {"setup.py": "import WEBINST"}}},
+ "doc": {"web": {"howto": {"index.xhtml": loreInput}},
+ "core": {"howto": {"template.tpl": self.template}}
+ }
+ }
+
+ outStructure = {
+ "LICENSE": "copyright!",
+ "setup.py": "import WEBINST",
+ "twisted": {"web": {"__init__.py": "import WEB"}},
+ "doc": {"howto": {"index.html": loreOutput}}}
+
+ self.createStructure(self.rootDir, structure)
+ outputFile = builder.buildSubProject("web", "0.3.0")
+ self.assertExtractedStructure(outputFile, outStructure)
+
+
+
+class BuildAllTarballsTest(DistributionBuilderTestBase):
+ """
+ Tests for L{DistributionBuilder.buildAllTarballs}.
+ """
+ skip = svnSkip
+
+ def setUp(self):
+ self.oldHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+ DistributionBuilderTestBase.setUp(self)
+
+
+ def tearDown(self):
+ signal.signal(signal.SIGCHLD, self.oldHandler)
+ DistributionBuilderTestBase.tearDown(self)
+
+
+ def test_buildAllTarballs(self):
+ """
+ L{buildAllTarballs} builds tarballs for Twisted and all of its
+ subprojects based on an SVN checkout; the resulting tarballs contain
+ no SVN metadata. This involves building documentation, which it will
+ build with the correct API documentation reference base URL.
+ """
+ repositoryPath = self.mktemp()
+ repository = FilePath(repositoryPath)
+ checkoutPath = self.mktemp()
+ checkout = FilePath(checkoutPath)
+ self.outputDir.remove()
+
+ runCommand(["svnadmin", "create", repositoryPath])
+ runCommand(["svn", "checkout", "file://" + repository.path,
+ checkout.path])
+ coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
+ "1.2.0", prefix="howto/",
+ apiBaseURL="http://twistedmatrix.com/documents/1.2.0/api/%s.html")
+
+ structure = {
+ "README": "Twisted",
+ "unrelated": "x",
+ "LICENSE": "copyright!",
+ "setup.py": "import toplevel",
+ "bin": {"words": {"im": "import im"},
+ "twistd": "TWISTD"},
+ "twisted":
+ {
+ "topfiles": {"setup.py": "import TOPINSTALL",
+ "README": "CORE!"},
+ "_version.py": genVersion("twisted", 1, 2, 0),
+ "words": {"__init__.py": "import WORDS",
+ "_version.py":
+ genVersion("twisted.words", 1, 2, 0),
+ "topfiles": {"setup.py": "import WORDSINSTALL",
+ "README": "WORDS!"},
+ },
+ "plugins": {"twisted_web.py": "import WEBPLUG",
+ "twisted_words.py": "import WORDPLUG",
+ "twisted_yay.py": "import YAY"}},
+ "doc": {"core": {"howto": {"template.tpl": self.template},
+ "index.xhtml": coreIndexInput}}}
+
+ twistedStructure = {
+ "README": "Twisted",
+ "unrelated": "x",
+ "LICENSE": "copyright!",
+ "setup.py": "import toplevel",
+ "bin": {"twistd": "TWISTD",
+ "words": {"im": "import im"}},
+ "twisted":
+ {
+ "topfiles": {"setup.py": "import TOPINSTALL",
+ "README": "CORE!"},
+ "_version.py": genVersion("twisted", 1, 2, 0),
+ "words": {"__init__.py": "import WORDS",
+ "_version.py":
+ genVersion("twisted.words", 1, 2, 0),
+ "topfiles": {"setup.py": "import WORDSINSTALL",
+ "README": "WORDS!"},
+ },
+ "plugins": {"twisted_web.py": "import WEBPLUG",
+ "twisted_words.py": "import WORDPLUG",
+ "twisted_yay.py": "import YAY"}},
+ "doc": {"core": {"howto": {"template.tpl": self.template},
+ "index.html": coreIndexOutput}}}
+
+ coreStructure = {
+ "setup.py": "import TOPINSTALL",
+ "README": "CORE!",
+ "LICENSE": "copyright!",
+ "bin": {"twistd": "TWISTD"},
+ "twisted": {
+ "_version.py": genVersion("twisted", 1, 2, 0),
+ "plugins": {"twisted_yay.py": "import YAY"}},
+ "doc": {"howto": {"template.tpl": self.template},
+ "index.html": coreIndexOutput}}
+
+ wordsStructure = {
+ "README": "WORDS!",
+ "LICENSE": "copyright!",
+ "setup.py": "import WORDSINSTALL",
+ "bin": {"im": "import im"},
+ "twisted":
+ {
+ "words": {"__init__.py": "import WORDS",
+ "_version.py":
+ genVersion("twisted.words", 1, 2, 0),
+ },
+ "plugins": {"twisted_words.py": "import WORDPLUG"}}}
+
+ self.createStructure(checkout, structure)
+ childs = [x.path for x in checkout.children()]
+ runCommand(["svn", "add"] + childs)
+ runCommand(["svn", "commit", checkout.path, "-m", "yay"])
+
+ buildAllTarballs(checkout, self.outputDir)
+ self.assertEqual(
+ set(self.outputDir.children()),
+ set([self.outputDir.child("Twisted-1.2.0.tar.bz2"),
+ self.outputDir.child("TwistedCore-1.2.0.tar.bz2"),
+ self.outputDir.child("TwistedWords-1.2.0.tar.bz2")]))
+
+ self.assertExtractedStructure(
+ self.outputDir.child("Twisted-1.2.0.tar.bz2"),
+ twistedStructure)
+ self.assertExtractedStructure(
+ self.outputDir.child("TwistedCore-1.2.0.tar.bz2"),
+ coreStructure)
+ self.assertExtractedStructure(
+ self.outputDir.child("TwistedWords-1.2.0.tar.bz2"),
+ wordsStructure)
+
+
+ def test_buildAllTarballsEnsuresCleanCheckout(self):
+ """
+ L{UncleanWorkingDirectory} is raised by L{buildAllTarballs} when the
+ SVN checkout provided has uncommitted changes.
+ """
+ repositoryPath = self.mktemp()
+ repository = FilePath(repositoryPath)
+ checkoutPath = self.mktemp()
+ checkout = FilePath(checkoutPath)
+
+ runCommand(["svnadmin", "create", repositoryPath])
+ runCommand(["svn", "checkout", "file://" + repository.path,
+ checkout.path])
+
+ checkout.child("foo").setContent("whatever")
+ self.assertRaises(UncleanWorkingDirectory,
+ buildAllTarballs, checkout, FilePath(self.mktemp()))
+
+
+ def test_buildAllTarballsEnsuresExistingCheckout(self):
+ """
+ L{NotWorkingDirectory} is raised by L{buildAllTarballs} when the
+ checkout passed does not exist or is not an SVN checkout.
+ """
+ checkout = FilePath(self.mktemp())
+ self.assertRaises(NotWorkingDirectory,
+ buildAllTarballs,
+ checkout, FilePath(self.mktemp()))
+ checkout.createDirectory()
+ self.assertRaises(NotWorkingDirectory,
+ buildAllTarballs,
+ checkout, FilePath(self.mktemp()))
+
+
+
+class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase):
+ """
+ Tests for the release script functionality.
+ """
+
+ def _testVersionChanging(self, major, minor, micro, prerelease=None):
+ """
+ Check that L{ChangeVersionsScript.main} calls the version-changing
+ function with the appropriate version data and filesystem path.
+ """
+ versionUpdates = []
+ def myVersionChanger(sourceTree, versionTemplate):
+ versionUpdates.append((sourceTree, versionTemplate))
+ versionChanger = ChangeVersionsScript()
+ versionChanger.changeAllProjectVersions = myVersionChanger
+ version = "%d.%d.%d" % (major, minor, micro)
+ if prerelease is not None:
+ version += "pre%d" % (prerelease,)
+ versionChanger.main([version])
+ self.assertEqual(len(versionUpdates), 1)
+ self.assertEqual(versionUpdates[0][0], FilePath("."))
+ self.assertEqual(versionUpdates[0][1].major, major)
+ self.assertEqual(versionUpdates[0][1].minor, minor)
+ self.assertEqual(versionUpdates[0][1].micro, micro)
+ self.assertEqual(versionUpdates[0][1].prerelease, prerelease)
+
+
+ def test_changeVersions(self):
+ """
+ L{ChangeVersionsScript.main} changes version numbers for all Twisted
+ projects.
+ """
+ self._testVersionChanging(8, 2, 3)
+
+
+ def test_changeVersionsWithPrerelease(self):
+ """
+ A prerelease can be specified to L{changeVersionsScript}.
+ """
+ self._testVersionChanging(9, 2, 7, 38)
+
+
+ def test_defaultChangeVersionsVersionChanger(self):
+ """
+ The default implementation of C{changeAllProjectVersions} is
+ L{changeAllProjectVersions}.
+ """
+ versionChanger = ChangeVersionsScript()
+ self.assertEqual(versionChanger.changeAllProjectVersions,
+ changeAllProjectVersions)
+
+
+ def test_badNumberOfArgumentsToChangeVersionsScript(self):
+ """
+ L{changeVersionsScript} raises SystemExit when the wrong number of
+ arguments are passed.
+ """
+ versionChanger = ChangeVersionsScript()
+ self.assertRaises(SystemExit, versionChanger.main, [])
+
+
+ def test_tooManyDotsToChangeVersionsScript(self):
+ """
+ L{changeVersionsScript} raises SystemExit when there are the wrong
+ number of segments in the version number passed.
+ """
+ versionChanger = ChangeVersionsScript()
+ self.assertRaises(SystemExit, versionChanger.main,
+ ["3.2.1.0"])
+
+
+ def test_nonIntPartsToChangeVersionsScript(self):
+ """
+ L{changeVersionsScript} raises SystemExit when the version number isn't
+ made out of numbers.
+ """
+ versionChanger = ChangeVersionsScript()
+ self.assertRaises(SystemExit, versionChanger.main,
+ ["my united.states.of prewhatever"])
+
+
+ def test_buildTarballsScript(self):
+ """
+ L{BuildTarballsScript.main} invokes L{buildAllTarballs} with
+ 2 or 3 L{FilePath} instances representing the paths passed to it.
+ """
+ builds = []
+ def myBuilder(checkout, destination, template=None):
+ builds.append((checkout, destination, template))
+ tarballBuilder = BuildTarballsScript()
+ tarballBuilder.buildAllTarballs = myBuilder
+
+ tarballBuilder.main(["checkoutDir", "destinationDir"])
+ self.assertEqual(
+ builds,
+ [(FilePath("checkoutDir"), FilePath("destinationDir"), None)])
+
+ builds = []
+ tarballBuilder.main(["checkoutDir", "destinationDir", "templatePath"])
+ self.assertEqual(
+ builds,
+ [(FilePath("checkoutDir"), FilePath("destinationDir"),
+ FilePath("templatePath"))])
+
+
+ def test_defaultBuildTarballsScriptBuilder(self):
+ """
+ The default implementation of L{BuildTarballsScript.buildAllTarballs}
+ is L{buildAllTarballs}.
+ """
+ tarballBuilder = BuildTarballsScript()
+ self.assertEqual(tarballBuilder.buildAllTarballs, buildAllTarballs)
+
+
+ def test_badNumberOfArgumentsToBuildTarballs(self):
+ """
+ L{BuildTarballsScript.main} raises SystemExit when the wrong number of
+ arguments are passed.
+ """
+ tarballBuilder = BuildTarballsScript()
+ self.assertRaises(SystemExit, tarballBuilder.main, [])
+ self.assertRaises(SystemExit, tarballBuilder.main, ["a", "b", "c", "d"])
+
+
+ def test_badNumberOfArgumentsToBuildNews(self):
+ """
+ L{NewsBuilder.main} raises L{SystemExit} when other than 1 argument is
+ passed to it.
+ """
+ newsBuilder = NewsBuilder()
+ self.assertRaises(SystemExit, newsBuilder.main, [])
+ self.assertRaises(SystemExit, newsBuilder.main, ["hello", "world"])
+
+
+ def test_buildNews(self):
+ """
+ L{NewsBuilder.main} calls L{NewsBuilder.buildAll} with a L{FilePath}
+ instance constructed from the path passed to it.
+ """
+ builds = []
+ newsBuilder = NewsBuilder()
+ newsBuilder.buildAll = builds.append
+ newsBuilder.main(["/foo/bar/baz"])
+ self.assertEqual(builds, [FilePath("/foo/bar/baz")])
diff --git a/twisted/python/test/test_runtime.py b/twisted/python/test/test_runtime.py
new file mode 100644
index 0000000..6d0d7cf
--- /dev/null
+++ b/twisted/python/test/test_runtime.py
@@ -0,0 +1,91 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for runtime checks.
+"""
+
+import sys
+
+from twisted.python.runtime import Platform
+from twisted.trial.unittest import TestCase
+
+
+
+class PlatformTests(TestCase):
+ """
+ Tests for the default L{Platform} initializer.
+ """
+
+ def test_isVistaConsistency(self):
+ """
+ Verify consistency of L{Platform.isVista}: it can only be C{True} if
+ L{Platform.isWinNT} and L{Platform.isWindows} are C{True}.
+ """
+ platform = Platform()
+ if platform.isVista():
+ self.assertTrue(platform.isWinNT())
+ self.assertTrue(platform.isWindows())
+ self.assertFalse(platform.isMacOSX())
+
+
+ def test_isMacOSXConsistency(self):
+ """
+ L{Platform.isMacOSX} can only return C{True} if L{Platform.getType}
+ returns C{'posix'}.
+ """
+ platform = Platform()
+ if platform.isMacOSX():
+ self.assertEqual(platform.getType(), 'posix')
+
+
+ def test_isLinuxConsistency(self):
+ """
+ L{Platform.isLinux} can only return C{True} if L{Platform.getType}
+ returns C{'posix'} and L{sys.platform} starts with C{"linux"}.
+ """
+ platform = Platform()
+ if platform.isLinux():
+ self.assertTrue(sys.platform.startswith("linux"))
+
+
+
+class ForeignPlatformTests(TestCase):
+ """
+ Tests for L{Platform} based overridden initializer values.
+ """
+
+ def test_getType(self):
+ """
+ If an operating system name is supplied to L{Platform}'s initializer,
+ L{Platform.getType} returns the platform type which corresponds to that
+ name.
+ """
+ self.assertEqual(Platform('nt').getType(), 'win32')
+ self.assertEqual(Platform('ce').getType(), 'win32')
+ self.assertEqual(Platform('posix').getType(), 'posix')
+ self.assertEqual(Platform('java').getType(), 'java')
+
+
+ def test_isMacOSX(self):
+ """
+ If a system platform name is supplied to L{Platform}'s initializer, it
+ is used to determine the result of L{Platform.isMacOSX}, which returns
+ C{True} for C{"darwin"}, C{False} otherwise.
+ """
+ self.assertTrue(Platform(None, 'darwin').isMacOSX())
+ self.assertFalse(Platform(None, 'linux2').isMacOSX())
+ self.assertFalse(Platform(None, 'win32').isMacOSX())
+
+
+ def test_isLinux(self):
+ """
+ If a system platform name is supplied to L{Platform}'s initializer, it
+ is used to determine the result of L{Platform.isLinux}, which returns
+ C{True} for values beginning with C{"linux"}, C{False} otherwise.
+ """
+ self.assertFalse(Platform(None, 'darwin').isLinux())
+ self.assertTrue(Platform(None, 'linux').isLinux())
+ self.assertTrue(Platform(None, 'linux2').isLinux())
+ self.assertTrue(Platform(None, 'linux3').isLinux())
+ self.assertFalse(Platform(None, 'win32').isLinux())
diff --git a/twisted/python/test/test_sendmsg.py b/twisted/python/test/test_sendmsg.py
new file mode 100644
index 0000000..48301f5
--- /dev/null
+++ b/twisted/python/test/test_sendmsg.py
@@ -0,0 +1,543 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.sendmsg}.
+"""
+
+import sys
+import errno
+
+from socket import SOL_SOCKET, AF_INET, AF_INET6, socket, error
+
+try:
+ from socket import AF_UNIX, socketpair
+except ImportError:
+ nonUNIXSkip = "Platform does not support AF_UNIX sockets"
+else:
+ nonUNIXSkip = None
+
+from struct import pack
+from os import devnull, pipe, read, close, environ
+
+from twisted.internet.defer import Deferred
+from twisted.internet.error import ProcessDone
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet import reactor
+from twisted.python.filepath import FilePath
+from twisted.python.runtime import platform
+
+from twisted.internet.protocol import ProcessProtocol
+
+if platform.isLinux():
+ from socket import MSG_DONTWAIT
+ dontWaitSkip = None
+else:
+ # It would be nice to be able to test flags on more platforms, but finding a
+ # flag that works *at all* is somewhat challenging.
+ dontWaitSkip = "MSG_DONTWAIT is only known to work as intended on Linux"
+
+try:
+ from twisted.python.sendmsg import SCM_RIGHTS, send1msg, recv1msg, getsockfam
+except ImportError:
+ importSkip = "Cannot import twisted.python.sendmsg"
+else:
+ importSkip = None
+
+
+class ExitedWithStderr(Exception):
+ """
+ A process exited with some stderr.
+ """
+
+ def __str__(self):
+ """
+ Dump the errors in a pretty way in the event of a subprocess traceback.
+ """
+ return '\n'.join([''] + list(self.args))
+
+
+class StartStopProcessProtocol(ProcessProtocol):
+ """
+ An L{IProcessProtocol} with a Deferred for events where the subprocess
+ starts and stops.
+
+ @ivar started: A L{Deferred} which fires with this protocol's
+ L{IProcessTransport} provider when it is connected to one.
+
+ @ivar stopped: A L{Deferred} which fires with the process output or a
+ failure if the process produces output on standard error.
+
+ @ivar output: A C{str} used to accumulate standard output.
+
+ @ivar errors: A C{str} used to accumulate standard error.
+ """
+ def __init__(self):
+ self.started = Deferred()
+ self.stopped = Deferred()
+ self.output = ''
+ self.errors = ''
+
+
+ def connectionMade(self):
+ self.started.callback(self.transport)
+
+
+ def outReceived(self, data):
+ self.output += data
+
+
+ def errReceived(self, data):
+ self.errors += data
+
+
+ def processEnded(self, reason):
+ if reason.check(ProcessDone):
+ self.stopped.callback(self.output)
+ else:
+ self.stopped.errback(ExitedWithStderr(
+ self.errors, self.output))
+
+
+
+class BadList(list):
+ """
+ A list which cannot be iterated sometimes.
+
+ This is a C{list} subclass to get past the type check in L{send1msg}, not as
+ an example of how real programs might want to interact with L{send1msg} (or
+ anything else). A custom C{list} subclass makes it easier to trigger
+ certain error cases in the implementation.
+
+ @ivar iterate: A flag which indicates whether an instance of L{BadList} will
+ allow iteration over itself or not. If C{False}, an attempt to iterate
+ over the instance will raise an exception.
+ """
+ iterate = True
+
+ def __iter__(self):
+ """
+ Allow normal list iteration, or raise an exception.
+
+ If C{self.iterate} is C{True}, it will be flipped to C{False} and then
+ normal iteration will proceed. If C{self.iterate} is C{False},
+ L{RuntimeError} is raised instead.
+ """
+ if self.iterate:
+ self.iterate = False
+ return super(BadList, self).__iter__()
+ raise RuntimeError("Something bad happened")
+
+
+
+class WorseList(list):
+ """
+ A list which at first gives the appearance of being iterable, but then
+ raises an exception.
+
+ See L{BadList} for a warning about not writing code like this.
+ """
+ def __iter__(self):
+ """
+ Return an iterator which will raise an exception as soon as C{next} is
+ called on it.
+ """
+ class BadIterator(object):
+ def next(self):
+ raise RuntimeError("This is a really bad case.")
+ return BadIterator()
+
+
+
+class SendmsgTestCase(TestCase):
+ """
+ Tests for sendmsg extension module and associated file-descriptor sending
+ functionality.
+ """
+ if nonUNIXSkip is not None:
+ skip = nonUNIXSkip
+ elif importSkip is not None:
+ skip = importSkip
+
+ def setUp(self):
+ """
+ Create a pair of UNIX sockets.
+ """
+ self.input, self.output = socketpair(AF_UNIX)
+
+
+ def tearDown(self):
+ """
+ Close the sockets opened by setUp.
+ """
+ self.input.close()
+ self.output.close()
+
+
+ def test_sendmsgBadArguments(self):
+ """
+ The argument types accepted by L{send1msg} are:
+
+ 1. C{int}
+ 2. read-only character buffer
+ 3. C{int}
+ 4. sequence
+
+ The 3rd and 4th arguments are optional. If fewer than two arguments or
+ more than four arguments are passed, or if any of the arguments passed
+ are not compatible with these types, L{TypeError} is raised.
+ """
+ # Exercise the wrong number of arguments cases
+ self.assertRaises(TypeError, send1msg)
+ self.assertRaises(TypeError, send1msg, 1)
+ self.assertRaises(TypeError, send1msg, 1, "hello world", 2, [], object())
+
+ # Exercise the wrong type of arguments cases
+ self.assertRaises(TypeError, send1msg, object(), "hello world", 2, [])
+ self.assertRaises(TypeError, send1msg, 1, object(), 2, [])
+ self.assertRaises(TypeError, send1msg, 1, "hello world", object(), [])
+ self.assertRaises(TypeError, send1msg, 1, "hello world", 2, object())
+
+
+ def test_badAncillaryIter(self):
+ """
+ If iteration over the ancillary data list fails (at the point of the
+ C{__iter__} call), the exception with which it fails is propagated to
+ the caller of L{send1msg}.
+ """
+ badList = BadList()
+ badList.append((1, 2, "hello world"))
+ badList.iterate = False
+
+ self.assertRaises(RuntimeError, send1msg, 1, "hello world", 2, badList)
+
+ # Hit the second iteration
+ badList.iterate = True
+ self.assertRaises(RuntimeError, send1msg, 1, "hello world", 2, badList)
+
+
+ def test_badAncillaryNext(self):
+ """
+ If iteration over the ancillary data list fails (at the point of a
+ C{next} call), the exception with which it fails is propagated to the
+ caller of L{send1msg}.
+ """
+ worseList = WorseList()
+ self.assertRaises(RuntimeError, send1msg, 1, "hello world", 2, worseList)
+
+
+ def test_sendmsgBadAncillaryItem(self):
+ """
+ The ancillary data list contains three-tuples with element types of:
+
+ 1. C{int}
+ 2. C{int}
+ 3. read-only character buffer
+
+ If a tuple in the ancillary data list does not elements of these types,
+ L{TypeError} is raised.
+ """
+ # Exercise the wrong number of arguments cases
+ self.assertRaises(TypeError, send1msg, 1, "hello world", 2, [()])
+ self.assertRaises(TypeError, send1msg, 1, "hello world", 2, [(1,)])
+ self.assertRaises(TypeError, send1msg, 1, "hello world", 2, [(1, 2)])
+ self.assertRaises(
+ TypeError,
+ send1msg, 1, "hello world", 2, [(1, 2, "goodbye", object())])
+
+ # Exercise the wrong type of arguments cases
+ exc = self.assertRaises(
+ TypeError, send1msg, 1, "hello world", 2, [object()])
+ self.assertEqual(
+ "send1msg argument 3 expected list of tuple, "
+ "got list containing object",
+ str(exc))
+ self.assertRaises(
+ TypeError,
+ send1msg, 1, "hello world", 2, [(object(), 1, "goodbye")])
+ self.assertRaises(
+ TypeError,
+ send1msg, 1, "hello world", 2, [(1, object(), "goodbye")])
+ self.assertRaises(
+ TypeError,
+ send1msg, 1, "hello world", 2, [(1, 1, object())])
+
+
+ def test_syscallError(self):
+ """
+ If the underlying C{sendmsg} call fails, L{send1msg} raises
+ L{socket.error} with its errno set to the underlying errno value.
+ """
+ probe = file(devnull)
+ fd = probe.fileno()
+ probe.close()
+ exc = self.assertRaises(error, send1msg, fd, "hello, world")
+ self.assertEqual(exc.args[0], errno.EBADF)
+
+
+ def test_syscallErrorWithControlMessage(self):
+ """
+ The behavior when the underlying C{sendmsg} call fails is the same
+ whether L{send1msg} is passed ancillary data or not.
+ """
+ probe = file(devnull)
+ fd = probe.fileno()
+ probe.close()
+ exc = self.assertRaises(
+ error, send1msg, fd, "hello, world", 0, [(0, 0, "0123")])
+ self.assertEqual(exc.args[0], errno.EBADF)
+
+
+ def test_roundtrip(self):
+ """
+ L{recv1msg} will retrieve a message sent via L{send1msg}.
+ """
+ message = "hello, world!"
+ self.assertEqual(
+ len(message),
+ send1msg(self.input.fileno(), message, 0))
+
+ result = recv1msg(fd=self.output.fileno())
+ self.assertEquals(result, (message, 0, []))
+
+
+ def test_shortsend(self):
+ """
+ L{send1msg} returns the number of bytes which it was able to send.
+ """
+ message = "x" * 1024 * 1024
+ self.input.setblocking(False)
+ sent = send1msg(self.input.fileno(), message)
+ # Sanity check - make sure we did fill the send buffer and then some
+ self.assertTrue(sent < len(message))
+ received = recv1msg(self.output.fileno(), 0, len(message))
+ self.assertEqual(len(received[0]), sent)
+
+
+ def test_roundtripEmptyAncillary(self):
+ """
+ L{send1msg} treats an empty ancillary data list the same way it treats
+ receiving no argument for the ancillary parameter at all.
+ """
+ send1msg(self.input.fileno(), "hello, world!", 0, [])
+
+ result = recv1msg(fd=self.output.fileno())
+ self.assertEquals(result, ("hello, world!", 0, []))
+
+
+ def test_flags(self):
+ """
+ The C{flags} argument to L{send1msg} is passed on to the underlying
+ C{sendmsg} call, to affect it in whatever way is defined by those flags.
+ """
+ # Just exercise one flag with simple, well-known behavior. MSG_DONTWAIT
+ # makes the send a non-blocking call, even if the socket is in blocking
+ # mode. See also test_flags in RecvmsgTestCase
+ for i in range(1024):
+ try:
+ send1msg(self.input.fileno(), "x" * 1024, MSG_DONTWAIT)
+ except error, e:
+ self.assertEqual(e.args[0], errno.EAGAIN)
+ break
+ else:
+ self.fail(
+ "Failed to fill up the send buffer, "
+ "or maybe send1msg blocked for a while")
+ if dontWaitSkip is not None:
+ test_flags.skip = dontWaitSkip
+
+
+ def test_wrongTypeAncillary(self):
+ """
+ L{send1msg} will show a helpful exception message when given the wrong
+ type of object for the 'ancillary' argument.
+ """
+ error = self.assertRaises(TypeError,
+ send1msg, self.input.fileno(),
+ "hello, world!", 0, 4321)
+ self.assertEquals(str(error),
+ "send1msg argument 3 expected list, got int")
+
+
+ def spawn(self, script):
+ """
+ Start a script that is a peer of this test as a subprocess.
+
+ @param script: the module name of the script in this directory (no
+ package prefix, no '.py')
+ @type script: C{str}
+
+ @rtype: L{StartStopProcessProtocol}
+ """
+ sspp = StartStopProcessProtocol()
+ reactor.spawnProcess(
+ sspp, sys.executable, [
+ sys.executable,
+ FilePath(__file__).sibling(script + ".py").path,
+ str(self.output.fileno()),
+ ],
+ environ,
+ childFDs={0: "w", 1: "r", 2: "r",
+ self.output.fileno(): self.output.fileno()}
+ )
+ return sspp
+
+
+ @inlineCallbacks
+ def test_sendSubProcessFD(self):
+ """
+ Calling L{sendsmsg} with SOL_SOCKET, SCM_RIGHTS, and a platform-endian
+ packed file descriptor number should send that file descriptor to a
+ different process, where it can be retrieved by using L{recv1msg}.
+ """
+ sspp = self.spawn("pullpipe")
+ yield sspp.started
+ pipeOut, pipeIn = pipe()
+ self.addCleanup(close, pipeOut)
+
+ send1msg(
+ self.input.fileno(), "blonk", 0,
+ [(SOL_SOCKET, SCM_RIGHTS, pack("i", pipeIn))])
+
+ close(pipeIn)
+ yield sspp.stopped
+ self.assertEquals(read(pipeOut, 1024), "Test fixture data: blonk.\n")
+ # Make sure that the pipe is actually closed now.
+ self.assertEquals(read(pipeOut, 1024), "")
+
+
+
+class RecvmsgTestCase(TestCase):
+ """
+ Tests for L{recv1msg} (primarily error handling cases).
+ """
+ if importSkip is not None:
+ skip = importSkip
+
+ def test_badArguments(self):
+ """
+ The argument types accepted by L{recv1msg} are:
+
+ 1. C{int}
+ 2. C{int}
+ 3. C{int}
+ 4. C{int}
+
+ The 2nd, 3rd, and 4th arguments are optional. If fewer than one
+ argument or more than four arguments are passed, or if any of the
+ arguments passed are not compatible with these types, L{TypeError} is
+ raised.
+ """
+ # Exercise the wrong number of arguments cases
+ self.assertRaises(TypeError, recv1msg)
+ self.assertRaises(TypeError, recv1msg, 1, 2, 3, 4, object())
+
+ # Exercise the wrong type of arguments cases
+ self.assertRaises(TypeError, recv1msg, object(), 2, 3, 4)
+ self.assertRaises(TypeError, recv1msg, 1, object(), 3, 4)
+ self.assertRaises(TypeError, recv1msg, 1, 2, object(), 4)
+ self.assertRaises(TypeError, recv1msg, 1, 2, 3, object())
+
+
+ def test_cmsgSpaceOverflow(self):
+ """
+ L{recv1msg} raises L{OverflowError} if passed a value for the
+ C{cmsg_size} argument which exceeds C{SOCKLEN_MAX}.
+ """
+ self.assertRaises(OverflowError, recv1msg, 0, 0, 0, 0x7FFFFFFF)
+
+
+ def test_syscallError(self):
+ """
+ If the underlying C{recvmsg} call fails, L{recv1msg} raises
+ L{socket.error} with its errno set to the underlying errno value.
+ """
+ probe = file(devnull)
+ fd = probe.fileno()
+ probe.close()
+ exc = self.assertRaises(error, recv1msg, fd)
+ self.assertEqual(exc.args[0], errno.EBADF)
+
+
+ def test_flags(self):
+ """
+ The C{flags} argument to L{recv1msg} is passed on to the underlying
+ C{recvmsg} call, to affect it in whatever way is defined by those flags.
+ """
+ # See test_flags in SendmsgTestCase
+ reader, writer = socketpair(AF_UNIX)
+ exc = self.assertRaises(
+ error, recv1msg, reader.fileno(), MSG_DONTWAIT)
+ self.assertEqual(exc.args[0], errno.EAGAIN)
+ if dontWaitSkip is not None:
+ test_flags.skip = dontWaitSkip
+
+
+
+class GetSocketFamilyTests(TestCase):
+ """
+ Tests for L{getsockfam}, a helper which reveals the address family of an
+ arbitrary socket.
+ """
+ if importSkip is not None:
+ skip = importSkip
+
+ def _socket(self, addressFamily):
+ """
+ Create a new socket using the given address family and return that
+ socket's file descriptor. The socket will automatically be closed when
+ the test is torn down.
+ """
+ s = socket(addressFamily)
+ self.addCleanup(s.close)
+ return s.fileno()
+
+
+ def test_badArguments(self):
+ """
+ L{getsockfam} accepts a single C{int} argument. If it is called in some
+ other way, L{TypeError} is raised.
+ """
+ self.assertRaises(TypeError, getsockfam)
+ self.assertRaises(TypeError, getsockfam, 1, 2)
+
+ self.assertRaises(TypeError, getsockfam, object())
+
+
+ def test_syscallError(self):
+ """
+ If the underlying C{getsockname} call fails, L{getsockfam} raises
+ L{socket.error} with its errno set to the underlying errno value.
+ """
+ probe = file(devnull)
+ fd = probe.fileno()
+ probe.close()
+ exc = self.assertRaises(error, getsockfam, fd)
+ self.assertEqual(errno.EBADF, exc.args[0])
+
+
+ def test_inet(self):
+ """
+ When passed the file descriptor of a socket created with the C{AF_INET}
+ address family, L{getsockfam} returns C{AF_INET}.
+ """
+ self.assertEqual(AF_INET, getsockfam(self._socket(AF_INET)))
+
+
+ def test_inet6(self):
+ """
+ When passed the file descriptor of a socket created with the C{AF_INET6}
+ address family, L{getsockfam} returns C{AF_INET6}.
+ """
+ self.assertEqual(AF_INET6, getsockfam(self._socket(AF_INET6)))
+
+
+ def test_unix(self):
+ """
+ When passed the file descriptor of a socket created with the C{AF_UNIX}
+ address family, L{getsockfam} returns C{AF_UNIX}.
+ """
+ self.assertEqual(AF_UNIX, getsockfam(self._socket(AF_UNIX)))
+ if nonUNIXSkip is not None:
+ test_unix.skip = nonUNIXSkip
diff --git a/twisted/python/test/test_shellcomp.py b/twisted/python/test/test_shellcomp.py
new file mode 100755
index 0000000..7f9cc83
--- /dev/null
+++ b/twisted/python/test/test_shellcomp.py
@@ -0,0 +1,623 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.python._shellcomp
+"""
+
+import sys
+from cStringIO import StringIO
+
+from twisted.trial import unittest
+from twisted.python import _shellcomp, usage, reflect
+from twisted.python.usage import Completions, Completer, CompleteFiles
+from twisted.python.usage import CompleteList
+
+
+
+class ZshScriptTestMeta(type):
+ """
+ Metaclass of ZshScriptTestMixin.
+ """
+ def __new__(cls, name, bases, attrs):
+ def makeTest(cmdName, optionsFQPN):
+ def runTest(self):
+ return test_genZshFunction(self, cmdName, optionsFQPN)
+ return runTest
+
+ # add test_ methods to the class for each script
+ # we are testing.
+ if 'generateFor' in attrs:
+ for cmdName, optionsFQPN in attrs['generateFor']:
+ test = makeTest(cmdName, optionsFQPN)
+ attrs['test_genZshFunction_' + cmdName] = test
+
+ return type.__new__(cls, name, bases, attrs)
+
+
+
+class ZshScriptTestMixin(object):
+ """
+ Integration test helper to show that C{usage.Options} classes can have zsh
+ completion functions generated for them without raising errors.
+
+ In your subclasses set a class variable like so:
+
+ # | cmd name | Fully Qualified Python Name of Options class |
+ #
+ generateFor = [('conch', 'twisted.conch.scripts.conch.ClientOptions'),
+ ('twistd', 'twisted.scripts.twistd.ServerOptions'),
+ ]
+
+ Each package that contains Twisted scripts should contain one TestCase
+ subclass which also inherits from this mixin, and contains a C{generateFor}
+ list appropriate for the scripts in that package.
+ """
+ __metaclass__ = ZshScriptTestMeta
+
+
+
+def test_genZshFunction(self, cmdName, optionsFQPN):
+ """
+ Generate completion functions for given twisted command - no errors
+ should be raised
+
+ @type cmdName: C{str}
+ @param cmdName: The name of the command-line utility e.g. 'twistd'
+
+ @type optionsFQPN: C{str}
+ @param optionsFQPN: The Fully Qualified Python Name of the C{Options}
+ class to be tested.
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+
+ # some scripts won't import or instantiate because of missing
+ # dependencies (PyCrypto, etc) so we have to skip them.
+ try:
+ o = reflect.namedAny(optionsFQPN)()
+ except Exception, e:
+ raise unittest.SkipTest("Couldn't import or instantiate "
+ "Options class: %s" % (e,))
+
+ try:
+ o.parseOptions(["", "--_shell-completion", "zsh:2"])
+ except ImportError, e:
+ # this can happen for commands which don't have all
+ # the necessary dependencies installed. skip test.
+ # skip
+ raise unittest.SkipTest("ImportError calling parseOptions(): %s", (e,))
+ except SystemExit:
+ pass # expected
+ else:
+ self.fail('SystemExit not raised')
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+ outputFile.seek(0)
+ outputFile.truncate()
+
+ # now, if it has sub commands, we have to test those too
+ if hasattr(o, 'subCommands'):
+ for (cmd, short, parser, doc) in o.subCommands:
+ try:
+ o.parseOptions([cmd, "", "--_shell-completion",
+ "zsh:3"])
+ except ImportError, e:
+ # this can happen for commands which don't have all
+ # the necessary dependencies installed. skip test.
+ raise unittest.SkipTest("ImportError calling parseOptions() "
+ "on subcommand: %s", (e,))
+ except SystemExit:
+ pass # expected
+ else:
+ self.fail('SystemExit not raised')
+
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+ outputFile.seek(0)
+ outputFile.truncate()
+
+ # flushed because we don't want DeprecationWarnings to be printed when
+ # running these test cases.
+ self.flushWarnings()
+
+
+
+class ZshTestCase(unittest.TestCase):
+ """
+ Tests for zsh completion code
+ """
+ def test_accumulateMetadata(self):
+ """
+ Are `compData' attributes you can place on Options classes
+ picked up correctly?
+ """
+ opts = FighterAceExtendedOptions()
+ ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
+
+ descriptions = FighterAceOptions.compData.descriptions.copy()
+ descriptions.update(FighterAceExtendedOptions.compData.descriptions)
+
+ self.assertEqual(ag.descriptions, descriptions)
+ self.assertEqual(ag.multiUse,
+ set(FighterAceOptions.compData.multiUse))
+ self.assertEqual(ag.mutuallyExclusive,
+ FighterAceOptions.compData.mutuallyExclusive)
+
+ optActions = FighterAceOptions.compData.optActions.copy()
+ optActions.update(FighterAceExtendedOptions.compData.optActions)
+ self.assertEqual(ag.optActions, optActions)
+
+ self.assertEqual(ag.extraActions,
+ FighterAceOptions.compData.extraActions)
+
+
+ def test_mutuallyExclusiveCornerCase(self):
+ """
+ Exercise a corner-case of ZshArgumentsGenerator.makeExcludesDict()
+ where the long option name already exists in the `excludes` dict being
+ built.
+ """
+ class OddFighterAceOptions(FighterAceExtendedOptions):
+ # since "fokker", etc, are already defined as mutually-
+ # exclusive on the super-class, defining them again here forces
+ # the corner-case to be exercised.
+ optFlags = [['anatra', None,
+ 'Select the Anatra DS as your dogfighter aircraft']]
+ compData = Completions(
+ mutuallyExclusive=[['anatra', 'fokker', 'albatros',
+ 'spad', 'bristol']])
+
+ opts = OddFighterAceOptions()
+ ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
+
+ expected = {
+ 'albatros': set(['anatra', 'b', 'bristol', 'f',
+ 'fokker', 's', 'spad']),
+ 'anatra': set(['a', 'albatros', 'b', 'bristol',
+ 'f', 'fokker', 's', 'spad']),
+ 'bristol': set(['a', 'albatros', 'anatra', 'f',
+ 'fokker', 's', 'spad']),
+ 'fokker': set(['a', 'albatros', 'anatra', 'b',
+ 'bristol', 's', 'spad']),
+ 'spad': set(['a', 'albatros', 'anatra', 'b',
+ 'bristol', 'f', 'fokker'])}
+
+ self.assertEqual(ag.excludes, expected)
+
+
+ def test_accumulateAdditionalOptions(self):
+ """
+ We pick up options that are only defined by having an
+ appropriately named method on your Options class,
+ e.g. def opt_foo(self, foo)
+ """
+ opts = FighterAceExtendedOptions()
+ ag = _shellcomp.ZshArgumentsGenerator(opts, 'ace', 'dummy_value')
+
+ self.assertIn('nocrash', ag.flagNameToDefinition)
+ self.assertIn('nocrash', ag.allOptionsNameToDefinition)
+
+ self.assertIn('difficulty', ag.paramNameToDefinition)
+ self.assertIn('difficulty', ag.allOptionsNameToDefinition)
+
+
+ def test_verifyZshNames(self):
+ """
+ Using a parameter/flag name that doesn't exist
+ will raise an error
+ """
+ class TmpOptions(FighterAceExtendedOptions):
+ # Note typo of detail
+ compData = Completions(optActions={'detaill' : None})
+
+ self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
+ TmpOptions(), 'ace', 'dummy_value')
+
+ class TmpOptions2(FighterAceExtendedOptions):
+ # Note that 'foo' and 'bar' are not real option
+ # names defined in this class
+ compData = Completions(
+ mutuallyExclusive=[("foo", "bar")])
+
+ self.assertRaises(ValueError, _shellcomp.ZshArgumentsGenerator,
+ TmpOptions2(), 'ace', 'dummy_value')
+
+
+ def test_zshCode(self):
+ """
+ Generate a completion function, and test the textual output
+ against a known correct output
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ self.patch(sys, 'argv', ["silly", "", "--_shell-completion", "zsh:2"])
+ opts = SimpleProgOptions()
+ self.assertRaises(SystemExit, opts.parseOptions)
+ self.assertEqual(testOutput1, outputFile.getvalue())
+
+
+ def test_zshCodeWithSubs(self):
+ """
+ Generate a completion function with subcommands,
+ and test the textual output against a known correct output
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ self.patch(sys, 'argv', ["silly2", "", "--_shell-completion", "zsh:2"])
+ opts = SimpleProgWithSubcommands()
+ self.assertRaises(SystemExit, opts.parseOptions)
+ self.assertEqual(testOutput2, outputFile.getvalue())
+
+
+ def test_incompleteCommandLine(self):
+ """
+ Completion still happens even if a command-line is given
+ that would normally throw UsageError.
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ opts = FighterAceOptions()
+
+ self.assertRaises(SystemExit, opts.parseOptions,
+ ["--fokker", "server", "--unknown-option",
+ "--unknown-option2",
+ "--_shell-completion", "zsh:5"])
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+
+
+ def test_incompleteCommandLine_case2(self):
+ """
+ Completion still happens even if a command-line is given
+ that would normally throw UsageError.
+
+ The existance of --unknown-option prior to the subcommand
+ will break subcommand detection... but we complete anyway
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ opts = FighterAceOptions()
+
+ self.assertRaises(SystemExit, opts.parseOptions,
+ ["--fokker", "--unknown-option", "server",
+ "--list-server", "--_shell-completion", "zsh:5"])
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+
+ outputFile.seek(0)
+ outputFile.truncate()
+
+
+ def test_incompleteCommandLine_case3(self):
+ """
+ Completion still happens even if a command-line is given
+ that would normally throw UsageError.
+
+ Break subcommand detection in a different way by providing
+ an invalid subcommand name.
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ opts = FighterAceOptions()
+
+ self.assertRaises(SystemExit, opts.parseOptions,
+ ["--fokker", "unknown-subcommand",
+ "--list-server", "--_shell-completion", "zsh:4"])
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+
+
+ def test_skipSubcommandList(self):
+ """
+ Ensure the optimization which skips building the subcommand list
+ under certain conditions isn't broken.
+ """
+ outputFile = StringIO()
+ self.patch(usage.Options, '_shellCompFile', outputFile)
+ opts = FighterAceOptions()
+
+ self.assertRaises(SystemExit, opts.parseOptions,
+ ["--alba", "--_shell-completion", "zsh:2"])
+ outputFile.seek(0)
+ # test that we got some output
+ self.assertEqual(1, len(outputFile.read(1)))
+
+
+ def test_poorlyDescribedOptMethod(self):
+ """
+ Test corner case fetching an option description from a method docstring
+ """
+ opts = FighterAceOptions()
+ argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
+
+ descr = argGen.getDescription('silly')
+
+ # docstring for opt_silly is useless so it should just use the
+ # option name as the description
+ self.assertEqual(descr, 'silly')
+
+
+ def test_brokenActions(self):
+ """
+ A C{Completer} with repeat=True may only be used as the
+ last item in the extraActions list.
+ """
+ class BrokenActions(usage.Options):
+ compData = usage.Completions(
+ extraActions=[usage.Completer(repeat=True),
+ usage.Completer()]
+ )
+
+ outputFile = StringIO()
+ opts = BrokenActions()
+ self.patch(opts, '_shellCompFile', outputFile)
+ self.assertRaises(ValueError, opts.parseOptions,
+ ["", "--_shell-completion", "zsh:2"])
+
+
+ def test_optMethodsDontOverride(self):
+ """
+ opt_* methods on Options classes should not override the
+ data provided in optFlags or optParameters.
+ """
+ class Options(usage.Options):
+ optFlags = [['flag', 'f', 'A flag']]
+ optParameters = [['param', 'p', None, 'A param']]
+
+ def opt_flag(self):
+ """ junk description """
+
+ def opt_param(self, param):
+ """ junk description """
+
+ opts = Options()
+ argGen = _shellcomp.ZshArgumentsGenerator(opts, 'ace', None)
+
+ self.assertEqual(argGen.getDescription('flag'), 'A flag')
+ self.assertEqual(argGen.getDescription('param'), 'A param')
+
+
+
+class EscapeTestCase(unittest.TestCase):
+ def test_escape(self):
+ """
+ Verify _shellcomp.escape() function
+ """
+ esc = _shellcomp.escape
+
+ test = "$"
+ self.assertEqual(esc(test), "'$'")
+
+ test = 'A--\'$"\\`--B'
+ self.assertEqual(esc(test), '"A--\'\\$\\"\\\\\\`--B"')
+
+
+
+class CompleterNotImplementedTestCase(unittest.TestCase):
+ """
+ Test that using an unknown shell constant with SubcommandAction
+ raises NotImplementedError
+
+ The other Completer() subclasses are tested in test_usage.py
+ """
+ def test_unknownShell(self):
+ """
+ Using an unknown shellType should raise NotImplementedError
+ """
+ action = _shellcomp.SubcommandAction()
+
+ self.assertRaises(NotImplementedError, action._shellCode,
+ None, "bad_shell_type")
+
+
+
+class FighterAceServerOptions(usage.Options):
+ """
+ Options for FighterAce 'server' subcommand
+ """
+ optFlags = [['list-server', None,
+ 'List this server with the online FighterAce network']]
+ optParameters = [['packets-per-second', None,
+ 'Number of update packets to send per second', '20']]
+
+
+
+class FighterAceOptions(usage.Options):
+ """
+ Command-line options for an imaginary `Fighter Ace` game
+ """
+ optFlags = [['fokker', 'f',
+ 'Select the Fokker Dr.I as your dogfighter aircraft'],
+ ['albatros', 'a',
+ 'Select the Albatros D-III as your dogfighter aircraft'],
+ ['spad', 's',
+ 'Select the SPAD S.VII as your dogfighter aircraft'],
+ ['bristol', 'b',
+ 'Select the Bristol Scout as your dogfighter aircraft'],
+ ['physics', 'p',
+ 'Enable secret Twisted physics engine'],
+ ['jam', 'j',
+ 'Enable a small chance that your machine guns will jam!'],
+ ['verbose', 'v',
+ 'Verbose logging (may be specified more than once)'],
+ ]
+
+ optParameters = [['pilot-name', None, "What's your name, Ace?",
+ 'Manfred von Richthofen'],
+ ['detail', 'd',
+ 'Select the level of rendering detail (1-5)', '3'],
+ ]
+
+ subCommands = [['server', None, FighterAceServerOptions,
+ 'Start FighterAce game-server.'],
+ ]
+
+ compData = Completions(
+ descriptions={'physics' : 'Twisted-Physics',
+ 'detail' : 'Rendering detail level'},
+ multiUse=['verbose'],
+ mutuallyExclusive=[['fokker', 'albatros', 'spad',
+ 'bristol']],
+ optActions={'detail' : CompleteList(['1' '2' '3'
+ '4' '5'])},
+ extraActions=[CompleteFiles(descr='saved game file to load')]
+ )
+
+ def opt_silly(self):
+ # A silly option which nobody can explain
+ """ """
+
+
+
+class FighterAceExtendedOptions(FighterAceOptions):
+ """
+ Extend the options and zsh metadata provided by FighterAceOptions.
+ _shellcomp must accumulate options and metadata from all classes in the
+ hiearchy so this is important to test.
+ """
+ optFlags = [['no-stalls', None,
+ 'Turn off the ability to stall your aircraft']]
+ optParameters = [['reality-level', None,
+ 'Select the level of physics reality (1-5)', '5']]
+
+ compData = Completions(
+ descriptions={'no-stalls' : 'Can\'t stall your plane'},
+ optActions={'reality-level' :
+ Completer(descr='Physics reality level')}
+ )
+
+ def opt_nocrash(self):
+ """
+ Select that you can't crash your plane
+ """
+
+
+ def opt_difficulty(self, difficulty):
+ """
+ How tough are you? (1-10)
+ """
+
+
+
+def _accuracyAction():
+ # add tick marks just to exercise quoting
+ return CompleteList(['1', '2', '3'], descr='Accuracy\'`?')
+
+
+
+class SimpleProgOptions(usage.Options):
+ """
+ Command-line options for a `Silly` imaginary program
+ """
+ optFlags = [['color', 'c', 'Turn on color output'],
+ ['gray', 'g', 'Turn on gray-scale output'],
+ ['verbose', 'v',
+ 'Verbose logging (may be specified more than once)'],
+ ]
+
+ optParameters = [['optimization', None, '5',
+ 'Select the level of optimization (1-5)'],
+ ['accuracy', 'a', '3',
+ 'Select the level of accuracy (1-3)'],
+ ]
+
+
+ compData = Completions(
+ descriptions={'color' : 'Color on',
+ 'optimization' : 'Optimization level'},
+ multiUse=['verbose'],
+ mutuallyExclusive=[['color', 'gray']],
+ optActions={'optimization' : CompleteList(['1', '2', '3', '4', '5'],
+ descr='Optimization?'),
+ 'accuracy' : _accuracyAction},
+ extraActions=[CompleteFiles(descr='output file')]
+ )
+
+ def opt_X(self):
+ """
+ usage.Options does not recognize single-letter opt_ methods
+ """
+
+
+
+class SimpleProgSub1(usage.Options):
+ optFlags = [['sub-opt', 's', 'Sub Opt One']]
+
+
+
+class SimpleProgSub2(usage.Options):
+ optFlags = [['sub-opt', 's', 'Sub Opt Two']]
+
+
+
+class SimpleProgWithSubcommands(SimpleProgOptions):
+ optFlags = [['some-option'],
+ ['other-option', 'o']]
+
+ optParameters = [['some-param'],
+ ['other-param', 'p'],
+ ['another-param', 'P', 'Yet Another Param']]
+
+ subCommands = [ ['sub1', None, SimpleProgSub1, 'Sub Command 1'],
+ ['sub2', None, SimpleProgSub2, 'Sub Command 2']]
+
+
+
+testOutput1 = """#compdef silly
+
+_arguments -s -A "-*" \\
+':output file (*):_files -g "*"' \\
+"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
+"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
+'(--color --gray -g)-c[Color on]' \\
+'(--gray -c -g)--color[Color on]' \\
+'(--color --gray -c)-g[Turn on gray-scale output]' \\
+'(--color -c -g)--gray[Turn on gray-scale output]' \\
+'--help[Display this help and exit.]' \\
+'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
+'*-v[Verbose logging (may be specified more than once)]' \\
+'*--verbose[Verbose logging (may be specified more than once)]' \\
+'--version[Display Twisted version and exit.]' \\
+&& return 0
+"""
+
+# with sub-commands
+testOutput2 = """#compdef silly2
+
+_arguments -s -A "-*" \\
+'*::subcmd:->subcmd' \\
+':output file (*):_files -g "*"' \\
+"(--accuracy)-a[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
+"(-a)--accuracy=[Select the level of accuracy (1-3)]:Accuracy'\`?:(1 2 3)" \\
+'(--another-param)-P[another-param]:another-param:_files' \\
+'(-P)--another-param=[another-param]:another-param:_files' \\
+'(--color --gray -g)-c[Color on]' \\
+'(--gray -c -g)--color[Color on]' \\
+'(--color --gray -c)-g[Turn on gray-scale output]' \\
+'(--color -c -g)--gray[Turn on gray-scale output]' \\
+'--help[Display this help and exit.]' \\
+'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
+'(--other-option)-o[other-option]' \\
+'(-o)--other-option[other-option]' \\
+'(--other-param)-p[other-param]:other-param:_files' \\
+'(-p)--other-param=[other-param]:other-param:_files' \\
+'--some-option[some-option]' \\
+'--some-param=[some-param]:some-param:_files' \\
+'*-v[Verbose logging (may be specified more than once)]' \\
+'*--verbose[Verbose logging (may be specified more than once)]' \\
+'--version[Display Twisted version and exit.]' \\
+&& return 0
+local _zsh_subcmds_array
+_zsh_subcmds_array=(
+"sub1:Sub Command 1"
+"sub2:Sub Command 2"
+)
+
+_describe "sub-command" _zsh_subcmds_array
+"""
diff --git a/twisted/python/test/test_syslog.py b/twisted/python/test/test_syslog.py
new file mode 100644
index 0000000..559c62f
--- /dev/null
+++ b/twisted/python/test/test_syslog.py
@@ -0,0 +1,151 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial.unittest import TestCase
+from twisted.python.failure import Failure
+
+try:
+ import syslog as stdsyslog
+except ImportError:
+ stdsyslog = None
+else:
+ from twisted.python import syslog
+
+
+
+class SyslogObserverTests(TestCase):
+ """
+ Tests for L{SyslogObserver} which sends Twisted log events to the syslog.
+ """
+ events = None
+
+ if stdsyslog is None:
+ skip = "syslog is not supported on this platform"
+
+ def setUp(self):
+ self.patch(syslog.SyslogObserver, 'openlog', self.openlog)
+ self.patch(syslog.SyslogObserver, 'syslog', self.syslog)
+ self.observer = syslog.SyslogObserver('SyslogObserverTests')
+
+
+ def openlog(self, prefix, options, facility):
+ self.logOpened = (prefix, options, facility)
+ self.events = []
+
+
+ def syslog(self, options, message):
+ self.events.append((options, message))
+
+
+ def test_emitWithoutMessage(self):
+ """
+ L{SyslogObserver.emit} ignores events with an empty value for the
+ C{'message'} key.
+ """
+ self.observer.emit({'message': (), 'isError': False, 'system': '-'})
+ self.assertEqual(self.events, [])
+
+
+ def test_emitCustomPriority(self):
+ """
+ L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
+ syslog priority, if that key is present in the event dictionary.
+ """
+ self.observer.emit({
+ 'message': ('hello, world',), 'isError': False, 'system': '-',
+ 'syslogPriority': stdsyslog.LOG_DEBUG})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_DEBUG, '[-] hello, world')])
+
+
+ def test_emitErrorPriority(self):
+ """
+ L{SyslogObserver.emit} uses C{LOG_ALERT} if the event represents an
+ error.
+ """
+ self.observer.emit({
+ 'message': ('hello, world',), 'isError': True, 'system': '-',
+ 'failure': Failure(Exception("foo"))})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_ALERT, '[-] hello, world')])
+
+
+ def test_emitCustomPriorityOverridesError(self):
+ """
+ L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} key if
+ it is specified even if the event dictionary represents an error.
+ """
+ self.observer.emit({
+ 'message': ('hello, world',), 'isError': True, 'system': '-',
+ 'syslogPriority': stdsyslog.LOG_NOTICE,
+ 'failure': Failure(Exception("bar"))})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_NOTICE, '[-] hello, world')])
+
+
+ def test_emitCustomFacility(self):
+ """
+ L{SyslogObserver.emit} uses the value of the C{'syslogPriority'} as the
+ syslog priority, if that key is present in the event dictionary.
+ """
+ self.observer.emit({
+ 'message': ('hello, world',), 'isError': False, 'system': '-',
+ 'syslogFacility': stdsyslog.LOG_CRON})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_INFO | stdsyslog.LOG_CRON, '[-] hello, world')])
+
+
+ def test_emitCustomSystem(self):
+ """
+ L{SyslogObserver.emit} uses the value of the C{'system'} key to prefix
+ the logged message.
+ """
+ self.observer.emit({'message': ('hello, world',), 'isError': False,
+ 'system': 'nonDefaultSystem'})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_INFO, "[nonDefaultSystem] hello, world")])
+
+
+ def test_emitMessage(self):
+ """
+ L{SyslogObserver.emit} logs the value of the C{'message'} key of the
+ event dictionary it is passed to the syslog.
+ """
+ self.observer.emit({
+ 'message': ('hello, world',), 'isError': False,
+ 'system': '-'})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_INFO, "[-] hello, world")])
+
+
+ def test_emitMultilineMessage(self):
+ """
+ Each line of a multiline message is emitted separately to the syslog.
+ """
+ self.observer.emit({
+ 'message': ('hello,\nworld',), 'isError': False,
+ 'system': '-'})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_INFO, '[-] hello,'),
+ (stdsyslog.LOG_INFO, '[-] \tworld')])
+
+
+ def test_emitStripsTrailingEmptyLines(self):
+ """
+ Trailing empty lines of a multiline message are omitted from the
+ messages sent to the syslog.
+ """
+ self.observer.emit({
+ 'message': ('hello,\nworld\n\n',), 'isError': False,
+ 'system': '-'})
+ self.assertEqual(
+ self.events,
+ [(stdsyslog.LOG_INFO, '[-] hello,'),
+ (stdsyslog.LOG_INFO, '[-] \tworld')])
diff --git a/twisted/python/test/test_systemd.py b/twisted/python/test/test_systemd.py
new file mode 100644
index 0000000..23d590f
--- /dev/null
+++ b/twisted/python/test/test_systemd.py
@@ -0,0 +1,173 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.systemd}.
+"""
+
+import os
+
+from twisted.trial.unittest import TestCase
+from twisted.python.systemd import ListenFDs
+
+
+class InheritedDescriptorsMixin(object):
+ """
+ Mixin for a L{TestCase} subclass which defines test methods for some kind of
+ systemd sd-daemon class. In particular, it defines tests for a
+ C{inheritedDescriptors} method.
+ """
+ def test_inheritedDescriptors(self):
+ """
+ C{inheritedDescriptors} returns a list of integers giving the file
+ descriptors which were inherited from systemd.
+ """
+ sddaemon = self.getDaemon(7, 3)
+ self.assertEqual([7, 8, 9], sddaemon.inheritedDescriptors())
+
+
+ def test_repeated(self):
+ """
+ Any subsequent calls to C{inheritedDescriptors} return the same list.
+ """
+ sddaemon = self.getDaemon(7, 3)
+ self.assertEqual(
+ sddaemon.inheritedDescriptors(),
+ sddaemon.inheritedDescriptors())
+
+
+
+class MemoryOnlyMixin(object):
+ """
+ Mixin for a L{TestCase} subclass which creates creating a fake, in-memory
+ implementation of C{inheritedDescriptors}. This provides verification that
+ the fake behaves in a compatible way with the real implementation.
+ """
+ def getDaemon(self, start, count):
+ """
+ Invent C{count} new I{file descriptors} (actually integers, attached to
+ no real file description), starting at C{start}. Construct and return a
+ new L{ListenFDs} which will claim those integers represent inherited
+ file descriptors.
+ """
+ return ListenFDs(range(start, start + count))
+
+
+
+class EnvironmentMixin(object):
+ """
+ Mixin for a L{TestCase} subclass which creates a real implementation of
+ C{inheritedDescriptors} which is based on the environment variables set by
+ systemd. To facilitate testing, this mixin will also create a fake
+ environment dictionary and add keys to it to make it look as if some
+ descriptors have been inherited.
+ """
+ def initializeEnvironment(self, count, pid):
+ """
+ Create a copy of the process environment and add I{LISTEN_FDS} and
+ I{LISTEN_PID} (the environment variables set by systemd) to it.
+ """
+ result = os.environ.copy()
+ result['LISTEN_FDS'] = str(count)
+ result['LISTEN_PID'] = str(pid)
+ return result
+
+
+ def getDaemon(self, start, count):
+ """
+ Create a new L{ListenFDs} instance, initialized with a fake environment
+ dictionary which will be set up as systemd would have set it up if
+ C{count} descriptors were being inherited. The descriptors will also
+ start at C{start}.
+ """
+ fakeEnvironment = self.initializeEnvironment(count, os.getpid())
+ return ListenFDs.fromEnvironment(environ=fakeEnvironment, start=start)
+
+
+
+class MemoryOnlyTests(MemoryOnlyMixin, InheritedDescriptorsMixin, TestCase):
+ """
+ Apply tests to L{ListenFDs}, explicitly constructed with some fake file
+ descriptors.
+ """
+
+
+
+class EnvironmentTests(EnvironmentMixin, InheritedDescriptorsMixin, TestCase):
+ """
+ Apply tests to L{ListenFDs}, constructed based on an environment dictionary.
+ """
+ def test_secondEnvironment(self):
+ """
+ Only a single L{Environment} can extract inherited file descriptors.
+ """
+ fakeEnvironment = self.initializeEnvironment(3, os.getpid())
+ first = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ second = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual(range(3, 6), first.inheritedDescriptors())
+ self.assertEqual([], second.inheritedDescriptors())
+
+
+ def test_mismatchedPID(self):
+ """
+ If the current process PID does not match the PID in the environment, no
+ inherited descriptors are reported.
+ """
+ fakeEnvironment = self.initializeEnvironment(3, os.getpid() + 1)
+ sddaemon = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual([], sddaemon.inheritedDescriptors())
+
+
+ def test_missingPIDVariable(self):
+ """
+ If the I{LISTEN_PID} environment variable is not present, no inherited
+ descriptors are reported.
+ """
+ fakeEnvironment = self.initializeEnvironment(3, os.getpid())
+ del fakeEnvironment['LISTEN_PID']
+ sddaemon = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual([], sddaemon.inheritedDescriptors())
+
+
+ def test_nonIntegerPIDVariable(self):
+ """
+ If the I{LISTEN_PID} environment variable is set to a string that cannot
+ be parsed as an integer, no inherited descriptors are reported.
+ """
+ fakeEnvironment = self.initializeEnvironment(3, "hello, world")
+ sddaemon = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual([], sddaemon.inheritedDescriptors())
+
+
+ def test_missingFDSVariable(self):
+ """
+ If the I{LISTEN_FDS} environment variable is not present, no inherited
+ descriptors are reported.
+ """
+ fakeEnvironment = self.initializeEnvironment(3, os.getpid())
+ del fakeEnvironment['LISTEN_FDS']
+ sddaemon = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual([], sddaemon.inheritedDescriptors())
+
+
+ def test_nonIntegerFDSVariable(self):
+ """
+ If the I{LISTEN_FDS} environment variable is set to a string that cannot
+ be parsed as an integer, no inherited descriptors are reported.
+ """
+ fakeEnvironment = self.initializeEnvironment("hello, world", os.getpid())
+ sddaemon = ListenFDs.fromEnvironment(environ=fakeEnvironment)
+ self.assertEqual([], sddaemon.inheritedDescriptors())
+
+
+ def test_defaultEnviron(self):
+ """
+ If the process environment is not explicitly passed to
+ L{Environment.__init__}, the real process environment dictionary is
+ used.
+ """
+ self.patch(os, 'environ', {
+ 'LISTEN_PID': str(os.getpid()),
+ 'LISTEN_FDS': '5'})
+ sddaemon = ListenFDs.fromEnvironment()
+ self.assertEqual(range(3, 3 + 5), sddaemon.inheritedDescriptors())
diff --git a/twisted/python/test/test_util.py b/twisted/python/test/test_util.py
new file mode 100644
index 0000000..2d49db1
--- /dev/null
+++ b/twisted/python/test/test_util.py
@@ -0,0 +1,892 @@
+# -*- test-case-name: twisted.python.test.test_util
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.util}.
+"""
+
+import os.path, sys
+import shutil, errno
+try:
+ import pwd, grp
+except ImportError:
+ pwd = grp = None
+
+from twisted.trial import unittest
+
+from twisted.python import util
+from twisted.internet import reactor
+from twisted.internet.interfaces import IReactorProcess
+from twisted.internet.protocol import ProcessProtocol
+from twisted.internet.defer import Deferred
+from twisted.internet.error import ProcessDone
+
+from twisted.test.test_process import MockOS
+
+
+
+class UtilTestCase(unittest.TestCase):
+
+ def testUniq(self):
+ l = ["a", 1, "ab", "a", 3, 4, 1, 2, 2, 4, 6]
+ self.assertEqual(util.uniquify(l), ["a", 1, "ab", 3, 4, 2, 6])
+
+ def testRaises(self):
+ self.failUnless(util.raises(ZeroDivisionError, divmod, 1, 0))
+ self.failIf(util.raises(ZeroDivisionError, divmod, 0, 1))
+
+ try:
+ util.raises(TypeError, divmod, 1, 0)
+ except ZeroDivisionError:
+ pass
+ else:
+ raise unittest.FailTest, "util.raises didn't raise when it should have"
+
+ def testUninterruptably(self):
+ def f(a, b):
+ self.calls += 1
+ exc = self.exceptions.pop()
+ if exc is not None:
+ raise exc(errno.EINTR, "Interrupted system call!")
+ return a + b
+
+ self.exceptions = [None]
+ self.calls = 0
+ self.assertEqual(util.untilConcludes(f, 1, 2), 3)
+ self.assertEqual(self.calls, 1)
+
+ self.exceptions = [None, OSError, IOError]
+ self.calls = 0
+ self.assertEqual(util.untilConcludes(f, 2, 3), 5)
+ self.assertEqual(self.calls, 3)
+
+ def testNameToLabel(self):
+ """
+ Test the various kinds of inputs L{nameToLabel} supports.
+ """
+ nameData = [
+ ('f', 'F'),
+ ('fo', 'Fo'),
+ ('foo', 'Foo'),
+ ('fooBar', 'Foo Bar'),
+ ('fooBarBaz', 'Foo Bar Baz'),
+ ]
+ for inp, out in nameData:
+ got = util.nameToLabel(inp)
+ self.assertEqual(
+ got, out,
+ "nameToLabel(%r) == %r != %r" % (inp, got, out))
+
+
+ def test_uidFromNumericString(self):
+ """
+ When L{uidFromString} is called with a base-ten string representation
+ of an integer, it returns the integer.
+ """
+ self.assertEqual(util.uidFromString("100"), 100)
+
+
+ def test_uidFromUsernameString(self):
+ """
+ When L{uidFromString} is called with a base-ten string representation
+ of an integer, it returns the integer.
+ """
+ pwent = pwd.getpwuid(os.getuid())
+ self.assertEqual(util.uidFromString(pwent.pw_name), pwent.pw_uid)
+ if pwd is None:
+ test_uidFromUsernameString.skip = (
+ "Username/UID conversion requires the pwd module.")
+
+
+ def test_gidFromNumericString(self):
+ """
+ When L{gidFromString} is called with a base-ten string representation
+ of an integer, it returns the integer.
+ """
+ self.assertEqual(util.gidFromString("100"), 100)
+
+
+ def test_gidFromGroupnameString(self):
+ """
+ When L{gidFromString} is called with a base-ten string representation
+ of an integer, it returns the integer.
+ """
+ grent = grp.getgrgid(os.getgid())
+ self.assertEqual(util.gidFromString(grent.gr_name), grent.gr_gid)
+ if grp is None:
+ test_gidFromGroupnameString.skip = (
+ "Group Name/GID conversion requires the grp module.")
+
+
+
+class SwitchUIDTest(unittest.TestCase):
+ """
+ Tests for L{util.switchUID}.
+ """
+
+ if getattr(os, "getuid", None) is None:
+ skip = "getuid/setuid not available"
+
+
+ def setUp(self):
+ self.mockos = MockOS()
+ self.patch(util, "os", self.mockos)
+ self.patch(util, "initgroups", self.initgroups)
+ self.initgroupsCalls = []
+
+
+ def initgroups(self, uid, gid):
+ """
+ Save L{util.initgroups} calls in C{self.initgroupsCalls}.
+ """
+ self.initgroupsCalls.append((uid, gid))
+
+
+ def test_uid(self):
+ """
+ L{util.switchUID} calls L{util.initgroups} and then C{os.setuid} with
+ the given uid.
+ """
+ util.switchUID(12000, None)
+ self.assertEqual(self.initgroupsCalls, [(12000, None)])
+ self.assertEqual(self.mockos.actions, [("setuid", 12000)])
+
+
+ def test_euid(self):
+ """
+ L{util.switchUID} calls L{util.initgroups} and then C{os.seteuid} with
+ the given uid if the C{euid} parameter is set to C{True}.
+ """
+ util.switchUID(12000, None, True)
+ self.assertEqual(self.initgroupsCalls, [(12000, None)])
+ self.assertEqual(self.mockos.seteuidCalls, [12000])
+
+
+ def test_currentUID(self):
+ """
+ If the current uid is the same as the uid passed to L{util.switchUID},
+ then initgroups does not get called, but a warning is issued.
+ """
+ uid = self.mockos.getuid()
+ util.switchUID(uid, None)
+ self.assertEqual(self.initgroupsCalls, [])
+ self.assertEqual(self.mockos.actions, [])
+ warnings = self.flushWarnings([util.switchUID])
+ self.assertEqual(len(warnings), 1)
+ self.assertIn('tried to drop privileges and setuid %i' % uid,
+ warnings[0]['message'])
+ self.assertIn('but uid is already %i' % uid, warnings[0]['message'])
+
+
+ def test_currentEUID(self):
+ """
+ If the current euid is the same as the euid passed to L{util.switchUID},
+ then initgroups does not get called, but a warning is issued.
+ """
+ euid = self.mockos.geteuid()
+ util.switchUID(euid, None, True)
+ self.assertEqual(self.initgroupsCalls, [])
+ self.assertEqual(self.mockos.seteuidCalls, [])
+ warnings = self.flushWarnings([util.switchUID])
+ self.assertEqual(len(warnings), 1)
+ self.assertIn('tried to drop privileges and seteuid %i' % euid,
+ warnings[0]['message'])
+ self.assertIn('but euid is already %i' % euid, warnings[0]['message'])
+
+
+
+class TestMergeFunctionMetadata(unittest.TestCase):
+ """
+ Tests for L{mergeFunctionMetadata}.
+ """
+
+ def test_mergedFunctionBehavesLikeMergeTarget(self):
+ """
+ After merging C{foo}'s data into C{bar}, the returned function behaves
+ as if it is C{bar}.
+ """
+ foo_object = object()
+ bar_object = object()
+
+ def foo():
+ return foo_object
+
+ def bar(x, y, (a, b), c=10, *d, **e):
+ return bar_object
+
+ baz = util.mergeFunctionMetadata(foo, bar)
+ self.assertIdentical(baz(1, 2, (3, 4), quux=10), bar_object)
+
+
+ def test_moduleIsMerged(self):
+ """
+ Merging C{foo} into C{bar} returns a function with C{foo}'s
+ C{__module__}.
+ """
+ def foo():
+ pass
+
+ def bar():
+ pass
+ bar.__module__ = 'somewhere.else'
+
+ baz = util.mergeFunctionMetadata(foo, bar)
+ self.assertEqual(baz.__module__, foo.__module__)
+
+
+ def test_docstringIsMerged(self):
+ """
+ Merging C{foo} into C{bar} returns a function with C{foo}'s docstring.
+ """
+
+ def foo():
+ """
+ This is foo.
+ """
+
+ def bar():
+ """
+ This is bar.
+ """
+
+ baz = util.mergeFunctionMetadata(foo, bar)
+ self.assertEqual(baz.__doc__, foo.__doc__)
+
+
+ def test_nameIsMerged(self):
+ """
+ Merging C{foo} into C{bar} returns a function with C{foo}'s name.
+ """
+
+ def foo():
+ pass
+
+ def bar():
+ pass
+
+ baz = util.mergeFunctionMetadata(foo, bar)
+ self.assertEqual(baz.__name__, foo.__name__)
+
+
+ def test_instanceDictionaryIsMerged(self):
+ """
+ Merging C{foo} into C{bar} returns a function with C{bar}'s
+ dictionary, updated by C{foo}'s.
+ """
+
+ def foo():
+ pass
+ foo.a = 1
+ foo.b = 2
+
+ def bar():
+ pass
+ bar.b = 3
+ bar.c = 4
+
+ baz = util.mergeFunctionMetadata(foo, bar)
+ self.assertEqual(foo.a, baz.a)
+ self.assertEqual(foo.b, baz.b)
+ self.assertEqual(bar.c, baz.c)
+
+
+
+class OrderedDictTest(unittest.TestCase):
+ def testOrderedDict(self):
+ d = util.OrderedDict()
+ d['a'] = 'b'
+ d['b'] = 'a'
+ d[3] = 12
+ d[1234] = 4321
+ self.assertEqual(repr(d), "{'a': 'b', 'b': 'a', 3: 12, 1234: 4321}")
+ self.assertEqual(d.values(), ['b', 'a', 12, 4321])
+ del d[3]
+ self.assertEqual(repr(d), "{'a': 'b', 'b': 'a', 1234: 4321}")
+ self.assertEqual(d, {'a': 'b', 'b': 'a', 1234:4321})
+ self.assertEqual(d.keys(), ['a', 'b', 1234])
+ self.assertEqual(list(d.iteritems()),
+ [('a', 'b'), ('b','a'), (1234, 4321)])
+ item = d.popitem()
+ self.assertEqual(item, (1234, 4321))
+
+ def testInitialization(self):
+ d = util.OrderedDict({'monkey': 'ook',
+ 'apple': 'red'})
+ self.failUnless(d._order)
+
+ d = util.OrderedDict(((1,1),(3,3),(2,2),(0,0)))
+ self.assertEqual(repr(d), "{1: 1, 3: 3, 2: 2, 0: 0}")
+
+class InsensitiveDictTest(unittest.TestCase):
+ def testPreserve(self):
+ InsensitiveDict=util.InsensitiveDict
+ dct=InsensitiveDict({'Foo':'bar', 1:2, 'fnz':{1:2}}, preserve=1)
+ self.assertEqual(dct['fnz'], {1:2})
+ self.assertEqual(dct['foo'], 'bar')
+ self.assertEqual(dct.copy(), dct)
+ self.assertEqual(dct['foo'], dct.get('Foo'))
+ assert 1 in dct and 'foo' in dct
+ self.assertEqual(eval(repr(dct)), dct)
+ keys=['Foo', 'fnz', 1]
+ for x in keys:
+ assert x in dct.keys()
+ assert (x, dct[x]) in dct.items()
+ self.assertEqual(len(keys), len(dct))
+ del dct[1]
+ del dct['foo']
+
+ def testNoPreserve(self):
+ InsensitiveDict=util.InsensitiveDict
+ dct=InsensitiveDict({'Foo':'bar', 1:2, 'fnz':{1:2}}, preserve=0)
+ keys=['foo', 'fnz', 1]
+ for x in keys:
+ assert x in dct.keys()
+ assert (x, dct[x]) in dct.items()
+ self.assertEqual(len(keys), len(dct))
+ del dct[1]
+ del dct['foo']
+
+
+
+
+class PasswordTestingProcessProtocol(ProcessProtocol):
+ """
+ Write the string C{"secret\n"} to a subprocess and then collect all of
+ its output and fire a Deferred with it when the process ends.
+ """
+ def connectionMade(self):
+ self.output = []
+ self.transport.write('secret\n')
+
+ def childDataReceived(self, fd, output):
+ self.output.append((fd, output))
+
+ def processEnded(self, reason):
+ self.finished.callback((reason, self.output))
+
+
+class GetPasswordTest(unittest.TestCase):
+ if not IReactorProcess.providedBy(reactor):
+ skip = "Process support required to test getPassword"
+
+ def test_stdin(self):
+ """
+ Making sure getPassword accepts a password from standard input by
+ running a child process which uses getPassword to read in a string
+ which it then writes it out again. Write a string to the child
+ process and then read one and make sure it is the right string.
+ """
+ p = PasswordTestingProcessProtocol()
+ p.finished = Deferred()
+ reactor.spawnProcess(
+ p,
+ sys.executable,
+ [sys.executable,
+ '-c',
+ ('import sys\n'
+ 'from twisted.python.util import getPassword\n'
+ 'sys.stdout.write(getPassword())\n'
+ 'sys.stdout.flush()\n')],
+ env={'PYTHONPATH': os.pathsep.join(sys.path)})
+
+ def processFinished((reason, output)):
+ reason.trap(ProcessDone)
+ self.assertIn((1, 'secret'), output)
+
+ return p.finished.addCallback(processFinished)
+
+
+
+class SearchUpwardsTest(unittest.TestCase):
+ def testSearchupwards(self):
+ os.makedirs('searchupwards/a/b/c')
+ file('searchupwards/foo.txt', 'w').close()
+ file('searchupwards/a/foo.txt', 'w').close()
+ file('searchupwards/a/b/c/foo.txt', 'w').close()
+ os.mkdir('searchupwards/bar')
+ os.mkdir('searchupwards/bam')
+ os.mkdir('searchupwards/a/bar')
+ os.mkdir('searchupwards/a/b/bam')
+ actual=util.searchupwards('searchupwards/a/b/c',
+ files=['foo.txt'],
+ dirs=['bar', 'bam'])
+ expected=os.path.abspath('searchupwards') + os.sep
+ self.assertEqual(actual, expected)
+ shutil.rmtree('searchupwards')
+ actual=util.searchupwards('searchupwards/a/b/c',
+ files=['foo.txt'],
+ dirs=['bar', 'bam'])
+ expected=None
+ self.assertEqual(actual, expected)
+
+
+
+class IntervalDifferentialTestCase(unittest.TestCase):
+ def testDefault(self):
+ d = iter(util.IntervalDifferential([], 10))
+ for i in range(100):
+ self.assertEqual(d.next(), (10, None))
+
+ def testSingle(self):
+ d = iter(util.IntervalDifferential([5], 10))
+ for i in range(100):
+ self.assertEqual(d.next(), (5, 0))
+
+ def testPair(self):
+ d = iter(util.IntervalDifferential([5, 7], 10))
+ for i in range(100):
+ self.assertEqual(d.next(), (5, 0))
+ self.assertEqual(d.next(), (2, 1))
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (4, 1))
+ self.assertEqual(d.next(), (1, 0))
+ self.assertEqual(d.next(), (5, 0))
+ self.assertEqual(d.next(), (1, 1))
+ self.assertEqual(d.next(), (4, 0))
+ self.assertEqual(d.next(), (3, 1))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (5, 0))
+ self.assertEqual(d.next(), (0, 1))
+
+ def testTriple(self):
+ d = iter(util.IntervalDifferential([2, 4, 5], 10))
+ for i in range(100):
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (1, 2))
+ self.assertEqual(d.next(), (1, 0))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (0, 2))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (1, 2))
+ self.assertEqual(d.next(), (1, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (2, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (0, 2))
+
+ def testInsert(self):
+ d = iter(util.IntervalDifferential([], 10))
+ self.assertEqual(d.next(), (10, None))
+ d.addInterval(3)
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (3, 0))
+ d.addInterval(6)
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (0, 1))
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (0, 1))
+
+ def testRemove(self):
+ d = iter(util.IntervalDifferential([3, 5], 10))
+ self.assertEqual(d.next(), (3, 0))
+ self.assertEqual(d.next(), (2, 1))
+ self.assertEqual(d.next(), (1, 0))
+ d.removeInterval(3)
+ self.assertEqual(d.next(), (4, 0))
+ self.assertEqual(d.next(), (5, 0))
+ d.removeInterval(5)
+ self.assertEqual(d.next(), (10, None))
+ self.assertRaises(ValueError, d.removeInterval, 10)
+
+
+
+class Record(util.FancyEqMixin):
+ """
+ Trivial user of L{FancyEqMixin} used by tests.
+ """
+ compareAttributes = ('a', 'b')
+
+ def __init__(self, a, b):
+ self.a = a
+ self.b = b
+
+
+
+class DifferentRecord(util.FancyEqMixin):
+ """
+ Trivial user of L{FancyEqMixin} which is not related to L{Record}.
+ """
+ compareAttributes = ('a', 'b')
+
+ def __init__(self, a, b):
+ self.a = a
+ self.b = b
+
+
+
+class DerivedRecord(Record):
+ """
+ A class with an inheritance relationship to L{Record}.
+ """
+
+
+
+class EqualToEverything(object):
+ """
+ A class the instances of which consider themselves equal to everything.
+ """
+ def __eq__(self, other):
+ return True
+
+
+ def __ne__(self, other):
+ return False
+
+
+
+class EqualToNothing(object):
+ """
+ A class the instances of which consider themselves equal to nothing.
+ """
+ def __eq__(self, other):
+ return False
+
+
+ def __ne__(self, other):
+ return True
+
+
+
+class EqualityTests(unittest.TestCase):
+ """
+ Tests for L{FancyEqMixin}.
+ """
+ def test_identity(self):
+ """
+ Instances of a class which mixes in L{FancyEqMixin} but which
+ defines no comparison attributes compare by identity.
+ """
+ class Empty(util.FancyEqMixin):
+ pass
+
+ self.assertFalse(Empty() == Empty())
+ self.assertTrue(Empty() != Empty())
+ empty = Empty()
+ self.assertTrue(empty == empty)
+ self.assertFalse(empty != empty)
+
+
+ def test_equality(self):
+ """
+ Instances of a class which mixes in L{FancyEqMixin} should compare
+ equal if all of their attributes compare equal. They should not
+ compare equal if any of their attributes do not compare equal.
+ """
+ self.assertTrue(Record(1, 2) == Record(1, 2))
+ self.assertFalse(Record(1, 2) == Record(1, 3))
+ self.assertFalse(Record(1, 2) == Record(2, 2))
+ self.assertFalse(Record(1, 2) == Record(3, 4))
+
+
+ def test_unequality(self):
+ """
+ Unequality between instances of a particular L{record} should be
+ defined as the negation of equality.
+ """
+ self.assertFalse(Record(1, 2) != Record(1, 2))
+ self.assertTrue(Record(1, 2) != Record(1, 3))
+ self.assertTrue(Record(1, 2) != Record(2, 2))
+ self.assertTrue(Record(1, 2) != Record(3, 4))
+
+
+ def test_differentClassesEquality(self):
+ """
+ Instances of different classes which mix in L{FancyEqMixin} should not
+ compare equal.
+ """
+ self.assertFalse(Record(1, 2) == DifferentRecord(1, 2))
+
+
+ def test_differentClassesInequality(self):
+ """
+ Instances of different classes which mix in L{FancyEqMixin} should
+ compare unequal.
+ """
+ self.assertTrue(Record(1, 2) != DifferentRecord(1, 2))
+
+
+ def test_inheritedClassesEquality(self):
+ """
+ An instance of a class which derives from a class which mixes in
+ L{FancyEqMixin} should compare equal to an instance of the base class
+ if and only if all of their attributes compare equal.
+ """
+ self.assertTrue(Record(1, 2) == DerivedRecord(1, 2))
+ self.assertFalse(Record(1, 2) == DerivedRecord(1, 3))
+ self.assertFalse(Record(1, 2) == DerivedRecord(2, 2))
+ self.assertFalse(Record(1, 2) == DerivedRecord(3, 4))
+
+
+ def test_inheritedClassesInequality(self):
+ """
+ An instance of a class which derives from a class which mixes in
+ L{FancyEqMixin} should compare unequal to an instance of the base
+ class if any of their attributes compare unequal.
+ """
+ self.assertFalse(Record(1, 2) != DerivedRecord(1, 2))
+ self.assertTrue(Record(1, 2) != DerivedRecord(1, 3))
+ self.assertTrue(Record(1, 2) != DerivedRecord(2, 2))
+ self.assertTrue(Record(1, 2) != DerivedRecord(3, 4))
+
+
+ def test_rightHandArgumentImplementsEquality(self):
+ """
+ The right-hand argument to the equality operator is given a chance
+ to determine the result of the operation if it is of a type
+ unrelated to the L{FancyEqMixin}-based instance on the left-hand
+ side.
+ """
+ self.assertTrue(Record(1, 2) == EqualToEverything())
+ self.assertFalse(Record(1, 2) == EqualToNothing())
+
+
+ def test_rightHandArgumentImplementsUnequality(self):
+ """
+ The right-hand argument to the non-equality operator is given a
+ chance to determine the result of the operation if it is of a type
+ unrelated to the L{FancyEqMixin}-based instance on the left-hand
+ side.
+ """
+ self.assertFalse(Record(1, 2) != EqualToEverything())
+ self.assertTrue(Record(1, 2) != EqualToNothing())
+
+
+
+class RunAsEffectiveUserTests(unittest.TestCase):
+ """
+ Test for the L{util.runAsEffectiveUser} function.
+ """
+
+ if getattr(os, "geteuid", None) is None:
+ skip = "geteuid/seteuid not available"
+
+ def setUp(self):
+ self.mockos = MockOS()
+ self.patch(os, "geteuid", self.mockos.geteuid)
+ self.patch(os, "getegid", self.mockos.getegid)
+ self.patch(os, "seteuid", self.mockos.seteuid)
+ self.patch(os, "setegid", self.mockos.setegid)
+
+
+ def _securedFunction(self, startUID, startGID, wantUID, wantGID):
+ """
+ Check if wanted UID/GID matched start or saved ones.
+ """
+ self.assertTrue(wantUID == startUID or
+ wantUID == self.mockos.seteuidCalls[-1])
+ self.assertTrue(wantGID == startGID or
+ wantGID == self.mockos.setegidCalls[-1])
+
+
+ def test_forwardResult(self):
+ """
+ L{util.runAsEffectiveUser} forwards the result obtained by calling the
+ given function
+ """
+ result = util.runAsEffectiveUser(0, 0, lambda: 1)
+ self.assertEqual(result, 1)
+
+
+ def test_takeParameters(self):
+ """
+ L{util.runAsEffectiveUser} pass the given parameters to the given
+ function.
+ """
+ result = util.runAsEffectiveUser(0, 0, lambda x: 2*x, 3)
+ self.assertEqual(result, 6)
+
+
+ def test_takesKeyworkArguments(self):
+ """
+ L{util.runAsEffectiveUser} pass the keyword parameters to the given
+ function.
+ """
+ result = util.runAsEffectiveUser(0, 0, lambda x, y=1, z=1: x*y*z, 2, z=3)
+ self.assertEqual(result, 6)
+
+
+ def _testUIDGIDSwitch(self, startUID, startGID, wantUID, wantGID,
+ expectedUIDSwitches, expectedGIDSwitches):
+ """
+ Helper method checking the calls to C{os.seteuid} and C{os.setegid}
+ made by L{util.runAsEffectiveUser}, when switching from startUID to
+ wantUID and from startGID to wantGID.
+ """
+ self.mockos.euid = startUID
+ self.mockos.egid = startGID
+ util.runAsEffectiveUser(
+ wantUID, wantGID,
+ self._securedFunction, startUID, startGID, wantUID, wantGID)
+ self.assertEqual(self.mockos.seteuidCalls, expectedUIDSwitches)
+ self.assertEqual(self.mockos.setegidCalls, expectedGIDSwitches)
+ self.mockos.seteuidCalls = []
+ self.mockos.setegidCalls = []
+
+
+ def test_root(self):
+ """
+ Check UID/GID switches when current effective UID is root.
+ """
+ self._testUIDGIDSwitch(0, 0, 0, 0, [], [])
+ self._testUIDGIDSwitch(0, 0, 1, 0, [1, 0], [])
+ self._testUIDGIDSwitch(0, 0, 0, 1, [], [1, 0])
+ self._testUIDGIDSwitch(0, 0, 1, 1, [1, 0], [1, 0])
+
+
+ def test_UID(self):
+ """
+ Check UID/GID switches when current effective UID is non-root.
+ """
+ self._testUIDGIDSwitch(1, 0, 0, 0, [0, 1], [])
+ self._testUIDGIDSwitch(1, 0, 1, 0, [], [])
+ self._testUIDGIDSwitch(1, 0, 1, 1, [0, 1, 0, 1], [1, 0])
+ self._testUIDGIDSwitch(1, 0, 2, 1, [0, 2, 0, 1], [1, 0])
+
+
+ def test_GID(self):
+ """
+ Check UID/GID switches when current effective GID is non-root.
+ """
+ self._testUIDGIDSwitch(0, 1, 0, 0, [], [0, 1])
+ self._testUIDGIDSwitch(0, 1, 0, 1, [], [])
+ self._testUIDGIDSwitch(0, 1, 1, 1, [1, 0], [])
+ self._testUIDGIDSwitch(0, 1, 1, 2, [1, 0], [2, 1])
+
+
+ def test_UIDGID(self):
+ """
+ Check UID/GID switches when current effective UID/GID is non-root.
+ """
+ self._testUIDGIDSwitch(1, 1, 0, 0, [0, 1], [0, 1])
+ self._testUIDGIDSwitch(1, 1, 0, 1, [0, 1], [])
+ self._testUIDGIDSwitch(1, 1, 1, 0, [0, 1, 0, 1], [0, 1])
+ self._testUIDGIDSwitch(1, 1, 1, 1, [], [])
+ self._testUIDGIDSwitch(1, 1, 2, 1, [0, 2, 0, 1], [])
+ self._testUIDGIDSwitch(1, 1, 1, 2, [0, 1, 0, 1], [2, 1])
+ self._testUIDGIDSwitch(1, 1, 2, 2, [0, 2, 0, 1], [2, 1])
+
+
+
+class UnsignedIDTests(unittest.TestCase):
+ """
+ Tests for L{util.unsignedID} and L{util.setIDFunction}.
+ """
+ def setUp(self):
+ """
+ Save the value of L{util._idFunction} and arrange for it to be restored
+ after the test runs.
+ """
+ self.addCleanup(setattr, util, '_idFunction', util._idFunction)
+
+
+ def test_setIDFunction(self):
+ """
+ L{util.setIDFunction} returns the last value passed to it.
+ """
+ value = object()
+ previous = util.setIDFunction(value)
+ result = util.setIDFunction(previous)
+ self.assertIdentical(value, result)
+
+
+ def test_unsignedID(self):
+ """
+ L{util.unsignedID} uses the function passed to L{util.setIDFunction} to
+ determine the unique integer id of an object and then adjusts it to be
+ positive if necessary.
+ """
+ foo = object()
+ bar = object()
+
+ # A fake object identity mapping
+ objects = {foo: 17, bar: -73}
+ def fakeId(obj):
+ return objects[obj]
+
+ util.setIDFunction(fakeId)
+
+ self.assertEqual(util.unsignedID(foo), 17)
+ self.assertEqual(util.unsignedID(bar), (sys.maxint + 1) * 2 - 73)
+
+
+ def test_defaultIDFunction(self):
+ """
+ L{util.unsignedID} uses the built in L{id} by default.
+ """
+ obj = object()
+ idValue = id(obj)
+ if idValue < 0:
+ idValue += (sys.maxint + 1) * 2
+
+ self.assertEqual(util.unsignedID(obj), idValue)
+
+
+
+class InitGroupsTests(unittest.TestCase):
+ """
+ Tests for L{util.initgroups}.
+ """
+
+ if pwd is None:
+ skip = "pwd not available"
+
+
+ def setUp(self):
+ self.addCleanup(setattr, util, "_c_initgroups", util._c_initgroups)
+ self.addCleanup(setattr, util, "setgroups", util.setgroups)
+
+
+ def test_initgroupsForceC(self):
+ """
+ If we fake the presence of the C extension, it's called instead of the
+ Python implementation.
+ """
+ calls = []
+ util._c_initgroups = lambda x, y: calls.append((x, y))
+ setgroupsCalls = []
+ util.setgroups = calls.append
+
+ util.initgroups(os.getuid(), 4)
+ self.assertEqual(calls, [(pwd.getpwuid(os.getuid())[0], 4)])
+ self.assertFalse(setgroupsCalls)
+
+
+ def test_initgroupsForcePython(self):
+ """
+ If we fake the absence of the C extension, the Python implementation is
+ called instead, calling C{os.setgroups}.
+ """
+ util._c_initgroups = None
+ calls = []
+ util.setgroups = calls.append
+ util.initgroups(os.getuid(), os.getgid())
+ # Something should be in the calls, we don't really care what
+ self.assertTrue(calls)
+
+
+ def test_initgroupsInC(self):
+ """
+ If the C extension is present, it's called instead of the Python
+ version. We check that by making sure C{os.setgroups} is not called.
+ """
+ calls = []
+ util.setgroups = calls.append
+ try:
+ util.initgroups(os.getuid(), os.getgid())
+ except OSError:
+ pass
+ self.assertFalse(calls)
+
+
+ if util._c_initgroups is None:
+ test_initgroupsInC.skip = "C initgroups not available"
diff --git a/twisted/python/test/test_versions.py b/twisted/python/test/test_versions.py
new file mode 100644
index 0000000..79388cf
--- /dev/null
+++ b/twisted/python/test/test_versions.py
@@ -0,0 +1,323 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+from cStringIO import StringIO
+
+from twisted.python.versions import getVersionString, IncomparableVersions
+from twisted.python.versions import Version, _inf
+from twisted.python.filepath import FilePath
+
+from twisted.trial import unittest
+
+
+
+VERSION_4_ENTRIES = """\
+<?xml version="1.0" encoding="utf-8"?>
+<wc-entries
+ xmlns="svn:">
+<entry
+ committed-rev="18210"
+ name=""
+ committed-date="2006-09-21T04:43:09.542953Z"
+ url="svn+ssh://svn.twistedmatrix.com/svn/Twisted/trunk/twisted"
+ last-author="exarkun"
+ kind="dir"
+ uuid="bbbe8e31-12d6-0310-92fd-ac37d47ddeeb"
+ repos="svn+ssh://svn.twistedmatrix.com/svn/Twisted"
+ revision="18211"/>
+</wc-entries>
+"""
+
+
+
+VERSION_8_ENTRIES = """\
+8
+
+dir
+22715
+svn+ssh://svn.twistedmatrix.com/svn/Twisted/trunk
+"""
+
+
+VERSION_9_ENTRIES = """\
+9
+
+dir
+22715
+svn+ssh://svn.twistedmatrix.com/svn/Twisted/trunk
+"""
+
+
+VERSION_10_ENTRIES = """\
+10
+
+dir
+22715
+svn+ssh://svn.twistedmatrix.com/svn/Twisted/trunk
+"""
+
+
+class VersionsTest(unittest.TestCase):
+
+ def test_versionComparison(self):
+ """
+ Versions can be compared for equality and order.
+ """
+ va = Version("dummy", 1, 0, 0)
+ vb = Version("dummy", 0, 1, 0)
+ self.failUnless(va > vb)
+ self.failUnless(vb < va)
+ self.failUnless(va >= vb)
+ self.failUnless(vb <= va)
+ self.failUnless(va != vb)
+ self.failUnless(vb == Version("dummy", 0, 1, 0))
+ self.failUnless(vb == vb)
+
+ # BREAK IT DOWN@!!
+ self.failIf(va < vb)
+ self.failIf(vb > va)
+ self.failIf(va <= vb)
+ self.failIf(vb >= va)
+ self.failIf(va == vb)
+ self.failIf(vb != Version("dummy", 0, 1, 0))
+ self.failIf(vb != vb)
+
+
+ def test_comparingPrereleasesWithReleases(self):
+ """
+ Prereleases are always less than versions without prereleases.
+ """
+ va = Version("whatever", 1, 0, 0, prerelease=1)
+ vb = Version("whatever", 1, 0, 0)
+ self.assertTrue(va < vb)
+ self.assertFalse(va > vb)
+ self.assertNotEquals(vb, va)
+
+
+ def test_comparingPrereleases(self):
+ """
+ The value specified as the prerelease is used in version comparisons.
+ """
+ va = Version("whatever", 1, 0, 0, prerelease=1)
+ vb = Version("whatever", 1, 0, 0, prerelease=2)
+ self.assertTrue(va < vb)
+ self.assertFalse(va > vb)
+ self.assertNotEqual(va, vb)
+
+
+ def test_infComparison(self):
+ """
+ L{_inf} is equal to L{_inf}.
+
+ This is a regression test.
+ """
+ self.assertEqual(_inf, _inf)
+
+
+ def testDontAllowBuggyComparisons(self):
+ self.assertRaises(IncomparableVersions,
+ cmp,
+ Version("dummy", 1, 0, 0),
+ Version("dumym", 1, 0, 0))
+
+
+ def test_repr(self):
+ """
+ Calling C{repr} on a version returns a human-readable string
+ representation of the version.
+ """
+ self.assertEqual(repr(Version("dummy", 1, 2, 3)),
+ "Version('dummy', 1, 2, 3)")
+
+
+ def test_reprWithPrerelease(self):
+ """
+ Calling C{repr} on a version with a prerelease returns a human-readable
+ string representation of the version including the prerelease.
+ """
+ self.assertEqual(repr(Version("dummy", 1, 2, 3, prerelease=4)),
+ "Version('dummy', 1, 2, 3, prerelease=4)")
+
+
+ def test_str(self):
+ """
+ Calling C{str} on a version returns a human-readable string
+ representation of the version.
+ """
+ self.assertEqual(str(Version("dummy", 1, 2, 3)),
+ "[dummy, version 1.2.3]")
+
+
+ def test_strWithPrerelease(self):
+ """
+ Calling C{str} on a version with a prerelease includes the prerelease.
+ """
+ self.assertEqual(str(Version("dummy", 1, 0, 0, prerelease=1)),
+ "[dummy, version 1.0.0pre1]")
+
+
+ def testShort(self):
+ self.assertEqual(Version('dummy', 1, 2, 3).short(), '1.2.3')
+
+
+ def test_goodSVNEntries_4(self):
+ """
+ Version should be able to parse an SVN format 4 entries file.
+ """
+ version = Version("dummy", 1, 0, 0)
+ self.assertEqual(
+ version._parseSVNEntries_4(StringIO(VERSION_4_ENTRIES)), '18211')
+
+
+ def test_goodSVNEntries_8(self):
+ """
+ Version should be able to parse an SVN format 8 entries file.
+ """
+ version = Version("dummy", 1, 0, 0)
+ self.assertEqual(
+ version._parseSVNEntries_8(StringIO(VERSION_8_ENTRIES)), '22715')
+
+
+ def test_goodSVNEntries_9(self):
+ """
+ Version should be able to parse an SVN format 9 entries file.
+ """
+ version = Version("dummy", 1, 0, 0)
+ self.assertEqual(
+ version._parseSVNEntries_9(StringIO(VERSION_9_ENTRIES)), '22715')
+
+
+ def test_goodSVNEntriesTenPlus(self):
+ """
+ Version should be able to parse an SVN format 10 entries file.
+ """
+ version = Version("dummy", 1, 0, 0)
+ self.assertEqual(
+ version._parseSVNEntriesTenPlus(StringIO(VERSION_10_ENTRIES)), '22715')
+
+
+ def test_getVersionString(self):
+ """
+ L{getVersionString} returns a string with the package name and the
+ short version number.
+ """
+ self.assertEqual(
+ 'Twisted 8.0.0', getVersionString(Version('Twisted', 8, 0, 0)))
+
+
+ def test_getVersionStringWithPrerelease(self):
+ """
+ L{getVersionString} includes the prerelease, if any.
+ """
+ self.assertEqual(
+ getVersionString(Version("whatever", 8, 0, 0, prerelease=1)),
+ "whatever 8.0.0pre1")
+
+
+ def test_base(self):
+ """
+ The L{base} method returns a very simple representation of the version.
+ """
+ self.assertEqual(Version("foo", 1, 0, 0).base(), "1.0.0")
+
+
+ def test_baseWithPrerelease(self):
+ """
+ The base version includes 'preX' for versions with prereleases.
+ """
+ self.assertEqual(Version("foo", 1, 0, 0, prerelease=8).base(),
+ "1.0.0pre8")
+
+
+
+class FormatDiscoveryTests(unittest.TestCase):
+ """
+ Tests which discover the parsing method based on the imported module name.
+ """
+
+ def setUp(self):
+ """
+ Create a temporary directory with a package structure in it.
+ """
+ self.entry = FilePath(self.mktemp())
+ self.preTestModules = sys.modules.copy()
+ sys.path.append(self.entry.path)
+ pkg = self.entry.child("twisted_python_versions_package")
+ pkg.makedirs()
+ pkg.child("__init__.py").setContent(
+ "from twisted.python.versions import Version\n"
+ "version = Version('twisted_python_versions_package', 1, 0, 0)\n")
+ self.svnEntries = pkg.child(".svn")
+ self.svnEntries.makedirs()
+
+
+ def tearDown(self):
+ """
+ Remove the imported modules and sys.path modifications.
+ """
+ sys.modules.clear()
+ sys.modules.update(self.preTestModules)
+ sys.path.remove(self.entry.path)
+
+
+ def checkSVNFormat(self, formatVersion, entriesText, expectedRevision):
+ """
+ Check for the given revision being detected after setting the SVN
+ entries text and format version of the test directory structure.
+ """
+ self.svnEntries.child("format").setContent(formatVersion+"\n")
+ self.svnEntries.child("entries").setContent(entriesText)
+ self.assertEqual(self.getVersion()._getSVNVersion(), expectedRevision)
+
+
+ def getVersion(self):
+ """
+ Import and retrieve the Version object from our dynamically created
+ package.
+ """
+ import twisted_python_versions_package
+ return twisted_python_versions_package.version
+
+
+ def test_detectVersion4(self):
+ """
+ Verify that version 4 format file will be properly detected and parsed.
+ """
+ self.checkSVNFormat("4", VERSION_4_ENTRIES, '18211')
+
+
+ def test_detectVersion8(self):
+ """
+ Verify that version 8 format files will be properly detected and
+ parsed.
+ """
+ self.checkSVNFormat("8", VERSION_8_ENTRIES, '22715')
+
+
+ def test_detectVersion9(self):
+ """
+ Verify that version 9 format files will be properly detected and
+ parsed.
+ """
+ self.checkSVNFormat("9", VERSION_9_ENTRIES, '22715')
+
+
+ def test_detectVersion10(self):
+ """
+ Verify that version 10 format files will be properly detected and
+ parsed.
+
+ Differing from previous formats, the version 10 format lacks a
+ I{format} file and B{only} has the version information on the first
+ line of the I{entries} file.
+ """
+ self.svnEntries.child("entries").setContent(VERSION_10_ENTRIES)
+ self.assertEqual(self.getVersion()._getSVNVersion(), '22715')
+
+
+ def test_detectUnknownVersion(self):
+ """
+ Verify that a new version of SVN will result in the revision 'Unknown'.
+ """
+ self.checkSVNFormat("some-random-new-version", "ooga booga!", 'Unknown')
diff --git a/twisted/python/test/test_win32.py b/twisted/python/test/test_win32.py
new file mode 100644
index 0000000..e262ea2
--- /dev/null
+++ b/twisted/python/test/test_win32.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.python.runtime import platform
+from twisted.python.win32 import cmdLineQuote
+
+
+class CommandLineQuotingTests(unittest.TestCase):
+ """
+ Tests for L{cmdLineQuote}.
+ """
+
+ def test_argWithoutSpaces(self):
+ """
+ Calling C{cmdLineQuote} with an argument with no spaces should
+ return the argument unchanged.
+ """
+ self.assertEqual(cmdLineQuote('an_argument'), 'an_argument')
+
+
+ def test_argWithSpaces(self):
+ """
+ Calling C{cmdLineQuote} with an argument containing spaces should
+ return the argument surrounded by quotes.
+ """
+ self.assertEqual(cmdLineQuote('An Argument'), '"An Argument"')
+
+
+ def test_emptyStringArg(self):
+ """
+ Calling C{cmdLineQuote} with an empty string should return a
+ quoted empty string.
+ """
+ self.assertEqual(cmdLineQuote(''), '""')
diff --git a/twisted/python/test/test_zipstream.py b/twisted/python/test/test_zipstream.py
new file mode 100644
index 0000000..1a0fcc4
--- /dev/null
+++ b/twisted/python/test/test_zipstream.py
@@ -0,0 +1,504 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.zipstream}
+"""
+import sys
+import random
+import zipfile
+
+from twisted.python.compat import set
+from twisted.python import zipstream, filepath
+from twisted.python.hashlib import md5
+from twisted.trial import unittest, util
+
+class FileEntryMixin:
+ """
+ File entry classes should behave as file-like objects
+ """
+ def getFileEntry(self, contents):
+ """
+ Return an appropriate zip file entry
+ """
+ filename = self.mktemp()
+ z = zipfile.ZipFile(filename, 'w', self.compression)
+ z.writestr('content', contents)
+ z.close()
+ z = zipstream.ChunkingZipFile(filename, 'r')
+ return z.readfile('content')
+
+
+ def test_isatty(self):
+ """
+ zip files should not be ttys, so isatty() should be false
+ """
+ self.assertEqual(self.getFileEntry('').isatty(), False)
+
+
+ def test_closed(self):
+ """
+ The C{closed} attribute should reflect whether C{close()} has been
+ called.
+ """
+ fileEntry = self.getFileEntry('')
+ self.assertEqual(fileEntry.closed, False)
+ fileEntry.close()
+ self.assertEqual(fileEntry.closed, True)
+
+
+ def test_readline(self):
+ """
+ C{readline()} should mirror L{file.readline} and return up to a single
+ deliminter.
+ """
+ fileEntry = self.getFileEntry('hoho\nho')
+ self.assertEqual(fileEntry.readline(), 'hoho\n')
+ self.assertEqual(fileEntry.readline(), 'ho')
+ self.assertEqual(fileEntry.readline(), '')
+
+
+ def test_next(self):
+ """
+ Zip file entries should implement the iterator protocol as files do.
+ """
+ fileEntry = self.getFileEntry('ho\nhoho')
+ self.assertEqual(fileEntry.next(), 'ho\n')
+ self.assertEqual(fileEntry.next(), 'hoho')
+ self.assertRaises(StopIteration, fileEntry.next)
+
+
+ def test_readlines(self):
+ """
+ C{readlines()} should return a list of all the lines.
+ """
+ fileEntry = self.getFileEntry('ho\nho\nho')
+ self.assertEqual(fileEntry.readlines(), ['ho\n', 'ho\n', 'ho'])
+
+
+ def test_iteration(self):
+ """
+ C{__iter__()} and C{xreadlines()} should return C{self}.
+ """
+ fileEntry = self.getFileEntry('')
+ self.assertIdentical(iter(fileEntry), fileEntry)
+ self.assertIdentical(fileEntry.xreadlines(), fileEntry)
+
+
+ def test_readWhole(self):
+ """
+ C{.read()} should read the entire file.
+ """
+ contents = "Hello, world!"
+ entry = self.getFileEntry(contents)
+ self.assertEqual(entry.read(), contents)
+
+
+ def test_readPartial(self):
+ """
+ C{.read(num)} should read num bytes from the file.
+ """
+ contents = "0123456789"
+ entry = self.getFileEntry(contents)
+ one = entry.read(4)
+ two = entry.read(200)
+ self.assertEqual(one, "0123")
+ self.assertEqual(two, "456789")
+
+
+ def test_tell(self):
+ """
+ C{.tell()} should return the number of bytes that have been read so
+ far.
+ """
+ contents = "x" * 100
+ entry = self.getFileEntry(contents)
+ entry.read(2)
+ self.assertEqual(entry.tell(), 2)
+ entry.read(4)
+ self.assertEqual(entry.tell(), 6)
+
+
+
+class DeflatedZipFileEntryTest(FileEntryMixin, unittest.TestCase):
+ """
+ DeflatedZipFileEntry should be file-like
+ """
+ compression = zipfile.ZIP_DEFLATED
+
+
+
+class ZipFileEntryTest(FileEntryMixin, unittest.TestCase):
+ """
+ ZipFileEntry should be file-like
+ """
+ compression = zipfile.ZIP_STORED
+
+
+
+class ZipstreamTest(unittest.TestCase):
+ """
+ Tests for twisted.python.zipstream
+ """
+ def setUp(self):
+ """
+ Creates junk data that can be compressed and a test directory for any
+ files that will be created
+ """
+ self.testdir = filepath.FilePath(self.mktemp())
+ self.testdir.makedirs()
+ self.unzipdir = self.testdir.child('unzipped')
+ self.unzipdir.makedirs()
+
+
+ def makeZipFile(self, contents, directory=''):
+ """
+ Makes a zip file archive containing len(contents) files. Contents
+ should be a list of strings, each string being the content of one file.
+ """
+ zpfilename = self.testdir.child('zipfile.zip').path
+ zpfile = zipfile.ZipFile(zpfilename, 'w')
+ for i, content in enumerate(contents):
+ filename = str(i)
+ if directory:
+ filename = directory + "/" + filename
+ zpfile.writestr(filename, content)
+ zpfile.close()
+ return zpfilename
+
+
+ def test_countEntries(self):
+ """
+ Make sure the deprecated L{countZipFileEntries} returns the correct
+ number of entries for a zip file.
+ """
+ name = self.makeZipFile(["one", "two", "three", "four", "five"])
+ result = self.assertWarns(DeprecationWarning,
+ "countZipFileEntries is deprecated.",
+ __file__, lambda :
+ zipstream.countZipFileEntries(name))
+ self.assertEqual(result, 5)
+
+
+ def test_invalidMode(self):
+ """
+ A ChunkingZipFile opened in write-mode should not allow .readfile(),
+ and raise a RuntimeError instead.
+ """
+ czf = zipstream.ChunkingZipFile(self.mktemp(), "w")
+ self.assertRaises(RuntimeError, czf.readfile, "something")
+
+
+ def test_closedArchive(self):
+ """
+ A closed ChunkingZipFile should raise a L{RuntimeError} when
+ .readfile() is invoked.
+ """
+ czf = zipstream.ChunkingZipFile(self.makeZipFile(["something"]), "r")
+ czf.close()
+ self.assertRaises(RuntimeError, czf.readfile, "something")
+
+
+ def test_invalidHeader(self):
+ """
+ A zipfile entry with the wrong magic number should raise BadZipfile for
+ readfile(), but that should not affect other files in the archive.
+ """
+ fn = self.makeZipFile(["test contents",
+ "more contents"])
+ zf = zipfile.ZipFile(fn, "r")
+ zeroOffset = zf.getinfo("0").header_offset
+ zf.close()
+ # Zero out just the one header.
+ scribble = file(fn, "r+b")
+ scribble.seek(zeroOffset, 0)
+ scribble.write(chr(0) * 4)
+ scribble.close()
+ czf = zipstream.ChunkingZipFile(fn)
+ self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
+ self.assertEqual(czf.readfile("1").read(), "more contents")
+
+
+ def test_filenameMismatch(self):
+ """
+ A zipfile entry with a different filename than is found in the central
+ directory should raise BadZipfile.
+ """
+ fn = self.makeZipFile(["test contents",
+ "more contents"])
+ zf = zipfile.ZipFile(fn, "r")
+ info = zf.getinfo("0")
+ info.filename = "not zero"
+ zf.close()
+ scribble = file(fn, "r+b")
+ scribble.seek(info.header_offset, 0)
+ scribble.write(info.FileHeader())
+ scribble.close()
+
+ czf = zipstream.ChunkingZipFile(fn)
+ self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
+ self.assertEqual(czf.readfile("1").read(), "more contents")
+
+
+ if sys.version_info < (2, 5):
+ # In python 2.4 and earlier, consistency between the directory and the
+ # file header are verified at archive-opening time. In python 2.5
+ # (and, presumably, later) it is readzipfile's responsibility.
+ message = "Consistency-checking only necessary in 2.5."
+ test_invalidHeader.skip = message
+ test_filenameMismatch.skip = message
+
+
+
+ def test_unsupportedCompression(self):
+ """
+ A zipfile which describes an unsupported compression mechanism should
+ raise BadZipfile.
+ """
+ fn = self.mktemp()
+ zf = zipfile.ZipFile(fn, "w")
+ zi = zipfile.ZipInfo("0")
+ zf.writestr(zi, "some data")
+ # Mangle its compression type in the central directory; can't do this
+ # before the writestr call or zipfile will (correctly) tell us not to
+ # pass bad compression types :)
+ zi.compress_type = 1234
+ zf.close()
+
+ czf = zipstream.ChunkingZipFile(fn)
+ self.assertRaises(zipfile.BadZipfile, czf.readfile, "0")
+
+
+ def test_extraData(self):
+ """
+ readfile() should skip over 'extra' data present in the zip metadata.
+ """
+ fn = self.mktemp()
+ zf = zipfile.ZipFile(fn, 'w')
+ zi = zipfile.ZipInfo("0")
+ zi.extra = "hello, extra"
+ zf.writestr(zi, "the real data")
+ zf.close()
+ czf = zipstream.ChunkingZipFile(fn)
+ self.assertEqual(czf.readfile("0").read(), "the real data")
+
+
+ def test_unzipIter(self):
+ """
+ L{twisted.python.zipstream.unzipIter} should unzip a file for each
+ iteration and yield the number of files left to unzip after that
+ iteration
+ """
+ numfiles = 10
+ contents = ['This is test file %d!' % i for i in range(numfiles)]
+ zpfilename = self.makeZipFile(contents)
+ uziter = zipstream.unzipIter(zpfilename, self.unzipdir.path)
+ for i in range(numfiles):
+ self.assertEqual(len(list(self.unzipdir.children())), i)
+ self.assertEqual(uziter.next(), numfiles - i - 1)
+ self.assertEqual(len(list(self.unzipdir.children())), numfiles)
+
+ for child in self.unzipdir.children():
+ num = int(child.basename())
+ self.assertEqual(child.open().read(), contents[num])
+ test_unzipIter.suppress = [
+ util.suppress(message="zipstream.unzipIter is deprecated")]
+
+
+ def test_unzipIterDeprecated(self):
+ """
+ Use of C{twisted.python.zipstream.unzipIter} will emit a
+ deprecated warning.
+ """
+ zpfilename = self.makeZipFile('foo')
+
+ self.assertEqual(len(self.flushWarnings()), 0)
+
+ for f in zipstream.unzipIter(zpfilename, self.unzipdir.path):
+ pass
+
+ warnings = self.flushWarnings()
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "zipstream.unzipIter is deprecated since Twisted 11.0.0 for "
+ "security reasons. Use Python's zipfile instead.")
+
+
+ def test_unzipIterChunky(self):
+ """
+ L{twisted.python.zipstream.unzipIterChunky} returns an iterator which
+ must be exhausted to completely unzip the input archive.
+ """
+ numfiles = 10
+ contents = ['This is test file %d!' % i for i in range(numfiles)]
+ zpfilename = self.makeZipFile(contents)
+ list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
+ self.assertEqual(
+ set(self.unzipdir.listdir()),
+ set(map(str, range(numfiles))))
+
+ for child in self.unzipdir.children():
+ num = int(child.basename())
+ self.assertEqual(child.getContent(), contents[num])
+
+
+ def test_unzipIterChunkyDirectory(self):
+ """
+ The path to which a file is extracted by L{zipstream.unzipIterChunky}
+ is determined by joining the C{directory} argument to C{unzip} with the
+ path within the archive of the file being extracted.
+ """
+ numfiles = 10
+ contents = ['This is test file %d!' % i for i in range(numfiles)]
+ zpfilename = self.makeZipFile(contents, 'foo')
+ list(zipstream.unzipIterChunky(zpfilename, self.unzipdir.path))
+ self.assertEqual(
+ set(self.unzipdir.child('foo').listdir()),
+ set(map(str, range(numfiles))))
+
+ for child in self.unzipdir.child('foo').children():
+ num = int(child.basename())
+ self.assertEqual(child.getContent(), contents[num])
+
+
+ def test_unzip(self):
+ """
+ L{twisted.python.zipstream.unzip} should extract all files from a zip
+ archive
+ """
+ numfiles = 3
+ zpfilename = self.makeZipFile([str(i) for i in range(numfiles)])
+ zipstream.unzip(zpfilename, self.unzipdir.path)
+ self.assertEqual(
+ set(self.unzipdir.listdir()),
+ set(map(str, range(numfiles))))
+ for i in range(numfiles):
+ self.assertEqual(self.unzipdir.child(str(i)).getContent(), str(i))
+ test_unzip.suppress = [
+ util.suppress(message="zipstream.unzip is deprecated")]
+
+
+ def test_unzipDeprecated(self):
+ """
+ Use of C{twisted.python.zipstream.unzip} will emit a deprecated warning.
+ """
+ zpfilename = self.makeZipFile('foo')
+
+ self.assertEqual(len(self.flushWarnings()), 0)
+
+ zipstream.unzip(zpfilename, self.unzipdir.path)
+
+ warnings = self.flushWarnings()
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "zipstream.unzip is deprecated since Twisted 11.0.0 for "
+ "security reasons. Use Python's zipfile instead.")
+
+
+ def test_unzipDirectory(self):
+ """
+ The path to which a file is extracted by L{zipstream.unzip} is
+ determined by joining the C{directory} argument to C{unzip} with the
+ path within the archive of the file being extracted.
+ """
+ numfiles = 3
+ zpfilename = self.makeZipFile([str(i) for i in range(numfiles)], 'foo')
+ zipstream.unzip(zpfilename, self.unzipdir.path)
+ self.assertEqual(
+ set(self.unzipdir.child('foo').listdir()),
+ set(map(str, range(numfiles))))
+ for i in range(numfiles):
+ self.assertEqual(
+ self.unzipdir.child('foo').child(str(i)).getContent(), str(i))
+ test_unzipDirectory.suppress = [
+ util.suppress(message="zipstream.unzip is deprecated")]
+
+
+ def test_overwrite(self):
+ """
+ L{twisted.python.zipstream.unzip} and
+ L{twisted.python.zipstream.unzipIter} shouldn't overwrite files unless
+ the 'overwrite' flag is passed
+ """
+ testfile = self.unzipdir.child('0')
+ zpfilename = self.makeZipFile(['OVERWRITTEN'])
+
+ testfile.setContent('NOT OVERWRITTEN')
+ zipstream.unzip(zpfilename, self.unzipdir.path)
+ self.assertEqual(testfile.open().read(), 'NOT OVERWRITTEN')
+ zipstream.unzip(zpfilename, self.unzipdir.path, overwrite=True)
+ self.assertEqual(testfile.open().read(), 'OVERWRITTEN')
+
+ testfile.setContent('NOT OVERWRITTEN')
+ uziter = zipstream.unzipIter(zpfilename, self.unzipdir.path)
+ uziter.next()
+ self.assertEqual(testfile.open().read(), 'NOT OVERWRITTEN')
+ uziter = zipstream.unzipIter(zpfilename, self.unzipdir.path,
+ overwrite=True)
+ uziter.next()
+ self.assertEqual(testfile.open().read(), 'OVERWRITTEN')
+ test_overwrite.suppress = [
+ util.suppress(message="zipstream.unzip is deprecated"),
+ util.suppress(message="zipstream.unzipIter is deprecated")]
+
+
+ # XXX these tests are kind of gross and old, but I think unzipIterChunky is
+ # kind of a gross function anyway. We should really write an abstract
+ # copyTo/moveTo that operates on FilePath and make sure ZipPath can support
+ # it, then just deprecate / remove this stuff.
+ def _unzipIterChunkyTest(self, compression, chunksize, lower, upper):
+ """
+ unzipIterChunky should unzip the given number of bytes per iteration.
+ """
+ junk = ' '.join([str(random.random()) for n in xrange(1000)])
+ junkmd5 = md5(junk).hexdigest()
+
+ tempdir = filepath.FilePath(self.mktemp())
+ tempdir.makedirs()
+ zfpath = tempdir.child('bigfile.zip').path
+ self._makebigfile(zfpath, compression, junk)
+ uziter = zipstream.unzipIterChunky(zfpath, tempdir.path,
+ chunksize=chunksize)
+ r = uziter.next()
+ # test that the number of chunks is in the right ballpark;
+ # this could theoretically be any number but statistically it
+ # should always be in this range
+ approx = lower < r < upper
+ self.failUnless(approx)
+ for r in uziter:
+ pass
+ self.assertEqual(r, 0)
+ newmd5 = md5(
+ tempdir.child("zipstreamjunk").open().read()).hexdigest()
+ self.assertEqual(newmd5, junkmd5)
+
+ def test_unzipIterChunkyStored(self):
+ """
+ unzipIterChunky should unzip the given number of bytes per iteration on
+ a stored archive.
+ """
+ self._unzipIterChunkyTest(zipfile.ZIP_STORED, 500, 35, 45)
+
+
+ def test_chunkyDeflated(self):
+ """
+ unzipIterChunky should unzip the given number of bytes per iteration on
+ a deflated archive.
+ """
+ self._unzipIterChunkyTest(zipfile.ZIP_DEFLATED, 972, 23, 27)
+
+
+ def _makebigfile(self, filename, compression, junk):
+ """
+ Create a zip file with the given file name and compression scheme.
+ """
+ zf = zipfile.ZipFile(filename, 'w', compression)
+ for i in range(10):
+ fn = 'zipstream%d' % i
+ zf.writestr(fn, "")
+ zf.writestr('zipstreamjunk', junk)
+ zf.close()
diff --git a/twisted/python/test/test_zshcomp.py b/twisted/python/test/test_zshcomp.py
new file mode 100644
index 0000000..13865b2
--- /dev/null
+++ b/twisted/python/test/test_zshcomp.py
@@ -0,0 +1,228 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.python.zshcomp
+"""
+import os
+import os.path
+from cStringIO import StringIO
+
+from twisted.trial import unittest
+from twisted.python import zshcomp, usage
+
+
+
+class ZshcompTestCase(unittest.TestCase):
+ """
+ Tests for the zsh completion function builder in twisted/python/zshcomp.py
+ """
+ def test_buildAll(self):
+ """
+ Build all the completion functions for twisted commands - no errors
+ should be raised
+ """
+ dirname = self.mktemp()
+ os.mkdir(dirname)
+ skippedCmds = [x[0] for x in zshcomp.makeCompFunctionFiles(dirname)]
+
+ # verify a zsh function was created for each twisted command
+ for info in zshcomp.generateFor:
+ if info[0] in skippedCmds:
+ continue
+ funcPath = os.path.join(dirname, '_' + info[0])
+ self.assertTrue(os.path.exists(funcPath))
+
+
+ def test_accumulateMetadata(self):
+ """
+ The zsh_* variables you can place on Options classes are
+ picked up correctly?
+ """
+ opts = FighterAceExtendedOptions()
+ ag = zshcomp.ArgumentsGenerator('dummy_cmd', opts, 'dummy_value')
+
+ altArgDescr = FighterAceOptions.zsh_altArgDescr.copy()
+ altArgDescr.update(FighterAceExtendedOptions.zsh_altArgDescr)
+
+ actionDescr = FighterAceOptions.zsh_actionDescr.copy()
+ actionDescr.update(FighterAceExtendedOptions.zsh_actionDescr)
+
+ self.assertEqual(ag.altArgDescr, altArgDescr)
+ self.assertEqual(ag.actionDescr, actionDescr)
+ self.assertEqual(ag.multiUse, FighterAceOptions.zsh_multiUse)
+ self.assertEqual(ag.mutuallyExclusive,
+ FighterAceOptions.zsh_mutuallyExclusive)
+ self.assertEqual(ag.actions, FighterAceOptions.zsh_actions)
+ self.assertEqual(ag.extras, FighterAceOptions.zsh_extras)
+
+
+ def test_accumulateAdditionalOptions(self):
+ """
+ We pick up options that are only defined by having an
+ appropriately named method on your Options class,
+ e.g. def opt_foo(self, foo)
+ """
+ opts = FighterAceExtendedOptions()
+ ag = zshcomp.ArgumentsGenerator('dummy_cmd', opts, 'dummy_value')
+
+ self.assertIn('nocrash', ag.optFlags_d)
+ self.assertIn('nocrash', ag.optAll_d)
+
+ self.assertIn('difficulty', ag.optParams_d)
+ self.assertIn('difficulty', ag.optAll_d)
+
+
+ def test_verifyZshNames(self):
+ """
+ Using a parameter/flag name that doesn't exist
+ will raise an error
+ """
+ class TmpOptions(FighterAceExtendedOptions):
+ zsh_actions = {'detaill' : 'foo'} # Note typo of detail
+
+ opts = TmpOptions()
+ self.assertRaises(ValueError, zshcomp.ArgumentsGenerator,
+ 'dummy_cmd', opts, 'dummy_value')
+
+
+ def test_zshCode(self):
+ """
+ Generate a completion function, and test the textual output
+ against a known correct output
+ """
+ cmd_name = 'testprog'
+ opts = SillyOptions()
+ f = StringIO()
+ b = zshcomp.Builder(cmd_name, opts, f)
+ b.write()
+ f.reset()
+ self.assertEqual(f.read(), testOutput1)
+
+
+ def test_skipBuild(self):
+ """
+ makeCompFunctionFiles skips building for commands whos
+ script module cannot be imported
+ """
+ generateFor = [('test_cmd', 'no.way.your.gonna.import.this', 'Foo')]
+ skips = zshcomp.makeCompFunctionFiles('out_dir', generateFor, {})
+ # no exceptions should be raised. hooray.
+ self.assertEqual(len(skips), 1)
+ self.assertEqual(len(skips[0]), 2)
+ self.assertEqual(skips[0][0], 'test_cmd')
+ self.assertTrue(isinstance(skips[0][1], ImportError))
+
+
+
+class FighterAceOptions(usage.Options):
+ """
+ Command-line options for an imaginary "Fighter Ace" game
+ """
+ optFlags = [['fokker', 'f',
+ 'Select the Fokker Dr.I as your dogfighter aircraft'],
+ ['albatros', 'a',
+ 'Select the Albatros D-III as your dogfighter aircraft'],
+ ['spad', 's',
+ 'Select the SPAD S.VII as your dogfighter aircraft'],
+ ['bristol', 'b',
+ 'Select the Bristol Scout as your dogfighter aircraft'],
+ ['physics', 'p',
+ 'Enable secret Twisted physics engine'],
+ ['jam', 'j',
+ 'Enable a small chance that your machine guns will jam!'],
+ ['verbose', 'v',
+ 'Verbose logging (may be specified more than once)'],
+ ]
+
+ optParameters = [['pilot-name', None, "What's your name, Ace?",
+ 'Manfred von Richthofen'],
+ ['detail', 'd',
+ 'Select the level of rendering detail (1-5)', '3'],
+ ]
+
+ zsh_altArgDescr = {'physics' : 'Twisted-Physics',
+ 'detail' : 'Rendering detail level'}
+ zsh_actionDescr = {'detail' : 'Pick your detail'}
+ zsh_multiUse = ['verbose']
+ zsh_mutuallyExclusive = [['fokker', 'albatros', 'spad', 'bristol']]
+ zsh_actions = {'detail' : '(1 2 3 4 5)'}
+ zsh_extras = [':saved game file to load:_files']
+
+
+
+class FighterAceExtendedOptions(FighterAceOptions):
+ """
+ Extend the options and zsh metadata provided by FighterAceOptions. zshcomp
+ must accumulate options and metadata from all classes in the hiearchy so
+ this is important for testing
+ """
+ optFlags = [['no-stalls', None,
+ 'Turn off the ability to stall your aircraft']]
+ optParameters = [['reality-level', None,
+ 'Select the level of physics reality (1-5)', '5']]
+
+ zsh_altArgDescr = {'no-stalls' : 'Can\'t stall your plane'}
+ zsh_actionDescr = {'reality-level' : 'Physics reality level'}
+
+
+ def opt_nocrash(self):
+ """Select that you can't crash your plane"""
+
+
+ def opt_difficulty(self, difficulty):
+ """How tough are you? (1-10)"""
+
+
+
+def _accuracyAction():
+ return '(1 2 3)'
+
+
+
+class SillyOptions(usage.Options):
+ """
+ Command-line options for a "silly" program
+ """
+ optFlags = [['color', 'c', 'Turn on color output'],
+ ['gray', 'g', 'Turn on gray-scale output'],
+ ['verbose', 'v',
+ 'Verbose logging (may be specified more than once)'],
+ ]
+
+ optParameters = [['optimization', None,
+ 'Select the level of optimization (1-5)', '5'],
+ ['accuracy', 'a',
+ 'Select the level of accuracy (1-3)', '3'],
+ ]
+
+
+ zsh_altArgDescr = {'color' : 'Color on',
+ 'optimization' : 'Optimization level'}
+ zsh_actionDescr = {'optimization' : 'Optimization?',
+ 'accuracy' : 'Accuracy?'}
+ zsh_multiUse = ['verbose']
+ zsh_mutuallyExclusive = [['color', 'gray']]
+ zsh_actions = {'optimization' : '(1 2 3 4 5)',
+ 'accuracy' : _accuracyAction}
+ zsh_extras = [':output file:_files']
+
+
+
+testOutput1 = """#compdef testprog
+_arguments -s -A "-*" \\
+':output file:_files' \\
+'(--accuracy)-a[3]:Accuracy?:(1 2 3)' \\
+'(-a)--accuracy=[3]:Accuracy?:(1 2 3)' \\
+'(--gray -g --color)-c[Color on]' \\
+'(--gray -g -c)--color[Color on]' \\
+'(--color -c --gray)-g[Turn on gray-scale output]' \\
+'(--color -c -g)--gray[Turn on gray-scale output]' \\
+'--help[Display this help and exit.]' \\
+'--optimization=[Optimization level]:Optimization?:(1 2 3 4 5)' \\
+'*-v[Verbose logging (may be specified more than once)]' \\
+'*--verbose[Verbose logging (may be specified more than once)]' \\
+'--version[Display Twisted version and exit.]' \\
+&& return 0
+"""
+
diff --git a/twisted/python/text.py b/twisted/python/text.py
new file mode 100644
index 0000000..def3c11
--- /dev/null
+++ b/twisted/python/text.py
@@ -0,0 +1,198 @@
+# -*- test-case-name: twisted.test.test_text -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Miscellany of text-munging functions.
+"""
+
+import string
+import types
+
+
+def stringyString(object, indentation=''):
+ """
+ Expansive string formatting for sequence types.
+
+ list.__str__ and dict.__str__ use repr() to display their
+ elements. This function also turns these sequence types
+ into strings, but uses str() on their elements instead.
+
+ Sequence elements are also displayed on seperate lines,
+ and nested sequences have nested indentation.
+ """
+ braces = ''
+ sl = []
+
+ if type(object) is types.DictType:
+ braces = '{}'
+ for key, value in object.items():
+ value = stringyString(value, indentation + ' ')
+ if isMultiline(value):
+ if endsInNewline(value):
+ value = value[:-len('\n')]
+ sl.append("%s %s:\n%s" % (indentation, key, value))
+ else:
+ # Oops. Will have to move that indentation.
+ sl.append("%s %s: %s" % (indentation, key,
+ value[len(indentation) + 3:]))
+
+ elif type(object) in (types.TupleType, types.ListType):
+ if type(object) is types.TupleType:
+ braces = '()'
+ else:
+ braces = '[]'
+
+ for element in object:
+ element = stringyString(element, indentation + ' ')
+ sl.append(string.rstrip(element) + ',')
+ else:
+ sl[:] = map(lambda s, i=indentation: i+s,
+ string.split(str(object),'\n'))
+
+ if not sl:
+ sl.append(indentation)
+
+ if braces:
+ sl[0] = indentation + braces[0] + sl[0][len(indentation) + 1:]
+ sl[-1] = sl[-1] + braces[-1]
+
+ s = string.join(sl, "\n")
+
+ if isMultiline(s) and not endsInNewline(s):
+ s = s + '\n'
+
+ return s
+
+def isMultiline(s):
+ """Returns True if this string has a newline in it."""
+ return (string.find(s, '\n') != -1)
+
+def endsInNewline(s):
+ """Returns True if this string ends in a newline."""
+ return (s[-len('\n'):] == '\n')
+
+
+def greedyWrap(inString, width=80):
+ """Given a string and a column width, return a list of lines.
+
+ Caveat: I'm use a stupid greedy word-wrapping
+ algorythm. I won't put two spaces at the end
+ of a sentence. I don't do full justification.
+ And no, I've never even *heard* of hypenation.
+ """
+
+ outLines = []
+
+ #eww, evil hacks to allow paragraphs delimited by two \ns :(
+ if inString.find('\n\n') >= 0:
+ paragraphs = string.split(inString, '\n\n')
+ for para in paragraphs:
+ outLines.extend(greedyWrap(para, width) + [''])
+ return outLines
+ inWords = string.split(inString)
+
+ column = 0
+ ptr_line = 0
+ while inWords:
+ column = column + len(inWords[ptr_line])
+ ptr_line = ptr_line + 1
+
+ if (column > width):
+ if ptr_line == 1:
+ # This single word is too long, it will be the whole line.
+ pass
+ else:
+ # We've gone too far, stop the line one word back.
+ ptr_line = ptr_line - 1
+ (l, inWords) = (inWords[0:ptr_line], inWords[ptr_line:])
+ outLines.append(string.join(l,' '))
+
+ ptr_line = 0
+ column = 0
+ elif not (len(inWords) > ptr_line):
+ # Clean up the last bit.
+ outLines.append(string.join(inWords, ' '))
+ del inWords[:]
+ else:
+ # Space
+ column = column + 1
+ # next word
+
+ return outLines
+
+
+wordWrap = greedyWrap
+
+def removeLeadingBlanks(lines):
+ ret = []
+ for line in lines:
+ if ret or line.strip():
+ ret.append(line)
+ return ret
+
+def removeLeadingTrailingBlanks(s):
+ lines = removeLeadingBlanks(s.split('\n'))
+ lines.reverse()
+ lines = removeLeadingBlanks(lines)
+ lines.reverse()
+ return '\n'.join(lines)+'\n'
+
+def splitQuoted(s):
+ """Like string.split, but don't break substrings inside quotes.
+
+ >>> splitQuoted('the \"hairy monkey\" likes pie')
+ ['the', 'hairy monkey', 'likes', 'pie']
+
+ Another one of those \"someone must have a better solution for
+ this\" things. This implementation is a VERY DUMB hack done too
+ quickly.
+ """
+ out = []
+ quot = None
+ phrase = None
+ for word in s.split():
+ if phrase is None:
+ if word and (word[0] in ("\"", "'")):
+ quot = word[0]
+ word = word[1:]
+ phrase = []
+
+ if phrase is None:
+ out.append(word)
+ else:
+ if word and (word[-1] == quot):
+ word = word[:-1]
+ phrase.append(word)
+ out.append(" ".join(phrase))
+ phrase = None
+ else:
+ phrase.append(word)
+
+ return out
+
+def strFile(p, f, caseSensitive=True):
+ """Find whether string p occurs in a read()able object f
+ @rtype: C{bool}
+ """
+ buf = ""
+ buf_len = max(len(p), 2**2**2**2)
+ if not caseSensitive:
+ p = p.lower()
+ while 1:
+ r = f.read(buf_len-len(p))
+ if not caseSensitive:
+ r = r.lower()
+ bytes_read = len(r)
+ if bytes_read == 0:
+ return False
+ l = len(buf)+bytes_read-buf_len
+ if l <= 0:
+ buf = buf + r
+ else:
+ buf = buf[l:] + r
+ if buf.find(p) != -1:
+ return True
+
diff --git a/twisted/python/threadable.py b/twisted/python/threadable.py
new file mode 100644
index 0000000..adb5b8b
--- /dev/null
+++ b/twisted/python/threadable.py
@@ -0,0 +1,118 @@
+# -*- test-case-name: twisted.python.test_threadable -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+A module that will allow your program to be multi-threaded,
+micro-threaded, and single-threaded. Currently microthreads are
+unimplemented. The idea is to abstract away some commonly used
+functionality so that I don't have to special-case it in all programs.
+"""
+
+
+
+from twisted.python import hook
+
+class DummyLock(object):
+ """
+ Hack to allow locks to be unpickled on an unthreaded system.
+ """
+
+ def __reduce__(self):
+ return (unpickle_lock, ())
+
+def unpickle_lock():
+ if threadingmodule is not None:
+ return XLock()
+ else:
+ return DummyLock()
+unpickle_lock.__safe_for_unpickling__ = True
+
+def _synchPre(self, *a, **b):
+ if '_threadable_lock' not in self.__dict__:
+ _synchLockCreator.acquire()
+ if '_threadable_lock' not in self.__dict__:
+ self.__dict__['_threadable_lock'] = XLock()
+ _synchLockCreator.release()
+ self._threadable_lock.acquire()
+
+def _synchPost(self, *a, **b):
+ self._threadable_lock.release()
+
+def synchronize(*klasses):
+ """Make all methods listed in each class' synchronized attribute synchronized.
+
+ The synchronized attribute should be a list of strings, consisting of the
+ names of methods that must be synchronized. If we are running in threaded
+ mode these methods will be wrapped with a lock.
+ """
+ if threadmodule is not None:
+ for klass in klasses:
+ for methodName in klass.synchronized:
+ hook.addPre(klass, methodName, _synchPre)
+ hook.addPost(klass, methodName, _synchPost)
+
+def init(with_threads=1):
+ """Initialize threading.
+
+ Don't bother calling this. If it needs to happen, it will happen.
+ """
+ global threaded, _synchLockCreator, XLock
+
+ if with_threads:
+ if not threaded:
+ if threadmodule is not None:
+ threaded = True
+
+ class XLock(threadingmodule._RLock, object):
+ def __reduce__(self):
+ return (unpickle_lock, ())
+
+ _synchLockCreator = XLock()
+ else:
+ raise RuntimeError("Cannot initialize threading, platform lacks thread support")
+ else:
+ if threaded:
+ raise RuntimeError("Cannot uninitialize threads")
+ else:
+ pass
+
+_dummyID = object()
+def getThreadID():
+ if threadmodule is None:
+ return _dummyID
+ return threadmodule.get_ident()
+
+
+def isInIOThread():
+ """Are we in the thread responsable for I/O requests (the event loop)?
+ """
+ return ioThread == getThreadID()
+
+
+
+def registerAsIOThread():
+ """Mark the current thread as responsable for I/O requests.
+ """
+ global ioThread
+ ioThread = getThreadID()
+
+
+ioThread = None
+threaded = False
+
+
+
+try:
+ import thread as threadmodule
+ import threading as threadingmodule
+except ImportError:
+ threadmodule = None
+ threadingmodule = None
+else:
+ init(True)
+
+
+
+__all__ = ['isInIOThread', 'registerAsIOThread', 'getThreadID', 'XLock']
diff --git a/twisted/python/threadpool.py b/twisted/python/threadpool.py
new file mode 100644
index 0000000..1fa2ed5
--- /dev/null
+++ b/twisted/python/threadpool.py
@@ -0,0 +1,240 @@
+# -*- test-case-name: twisted.test.test_threadpool -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+twisted.python.threadpool: a pool of threads to which we dispatch tasks.
+
+In most cases you can just use C{reactor.callInThread} and friends
+instead of creating a thread pool directly.
+"""
+
+import Queue
+import threading
+import copy
+
+from twisted.python import log, context, failure
+
+
+WorkerStop = object()
+
+
+class ThreadPool:
+ """
+ This class (hopefully) generalizes the functionality of a pool of
+ threads to which work can be dispatched.
+
+ L{callInThread} and L{stop} should only be called from
+ a single thread, unless you make a subclass where L{stop} and
+ L{_startSomeWorkers} are synchronized.
+ """
+ min = 5
+ max = 20
+ joined = False
+ started = False
+ workers = 0
+ name = None
+
+ threadFactory = threading.Thread
+ currentThread = staticmethod(threading.currentThread)
+
+ def __init__(self, minthreads=5, maxthreads=20, name=None):
+ """
+ Create a new threadpool.
+
+ @param minthreads: minimum number of threads in the pool
+ @param maxthreads: maximum number of threads in the pool
+ """
+ assert minthreads >= 0, 'minimum is negative'
+ assert minthreads <= maxthreads, 'minimum is greater than maximum'
+ self.q = Queue.Queue(0)
+ self.min = minthreads
+ self.max = maxthreads
+ self.name = name
+ self.waiters = []
+ self.threads = []
+ self.working = []
+
+
+ def start(self):
+ """
+ Start the threadpool.
+ """
+ self.joined = False
+ self.started = True
+ # Start some threads.
+ self.adjustPoolsize()
+
+
+ def startAWorker(self):
+ self.workers += 1
+ name = "PoolThread-%s-%s" % (self.name or id(self), self.workers)
+ newThread = self.threadFactory(target=self._worker, name=name)
+ self.threads.append(newThread)
+ newThread.start()
+
+
+ def stopAWorker(self):
+ self.q.put(WorkerStop)
+ self.workers -= 1
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ ThreadPool.__init__(self, self.min, self.max)
+
+
+ def __getstate__(self):
+ state = {}
+ state['min'] = self.min
+ state['max'] = self.max
+ return state
+
+
+ def _startSomeWorkers(self):
+ neededSize = self.q.qsize() + len(self.working)
+ # Create enough, but not too many
+ while self.workers < min(self.max, neededSize):
+ self.startAWorker()
+
+
+ def callInThread(self, func, *args, **kw):
+ """
+ Call a callable object in a separate thread.
+
+ @param func: callable object to be called in separate thread
+
+ @param *args: positional arguments to be passed to C{func}
+
+ @param **kw: keyword args to be passed to C{func}
+ """
+ self.callInThreadWithCallback(None, func, *args, **kw)
+
+
+ def callInThreadWithCallback(self, onResult, func, *args, **kw):
+ """
+ Call a callable object in a separate thread and call C{onResult}
+ with the return value, or a L{twisted.python.failure.Failure}
+ if the callable raises an exception.
+
+ The callable is allowed to block, but the C{onResult} function
+ must not block and should perform as little work as possible.
+
+ A typical action for C{onResult} for a threadpool used with a
+ Twisted reactor would be to schedule a
+ L{twisted.internet.defer.Deferred} to fire in the main
+ reactor thread using C{.callFromThread}. Note that C{onResult}
+ is called inside the separate thread, not inside the reactor thread.
+
+ @param onResult: a callable with the signature C{(success, result)}.
+ If the callable returns normally, C{onResult} is called with
+ C{(True, result)} where C{result} is the return value of the
+ callable. If the callable throws an exception, C{onResult} is
+ called with C{(False, failure)}.
+
+ Optionally, C{onResult} may be C{None}, in which case it is not
+ called at all.
+
+ @param func: callable object to be called in separate thread
+
+ @param *args: positional arguments to be passed to C{func}
+
+ @param **kwargs: keyword arguments to be passed to C{func}
+ """
+ if self.joined:
+ return
+ ctx = context.theContextTracker.currentContext().contexts[-1]
+ o = (ctx, func, args, kw, onResult)
+ self.q.put(o)
+ if self.started:
+ self._startSomeWorkers()
+
+
+ def _worker(self):
+ """
+ Method used as target of the created threads: retrieve a task to run
+ from the threadpool, run it, and proceed to the next task until
+ threadpool is stopped.
+ """
+ ct = self.currentThread()
+ o = self.q.get()
+ while o is not WorkerStop:
+ self.working.append(ct)
+ ctx, function, args, kwargs, onResult = o
+ del o
+
+ try:
+ result = context.call(ctx, function, *args, **kwargs)
+ success = True
+ except:
+ success = False
+ if onResult is None:
+ context.call(ctx, log.err)
+ result = None
+ else:
+ result = failure.Failure()
+
+ del function, args, kwargs
+
+ self.working.remove(ct)
+
+ if onResult is not None:
+ try:
+ context.call(ctx, onResult, success, result)
+ except:
+ context.call(ctx, log.err)
+
+ del ctx, onResult, result
+
+ self.waiters.append(ct)
+ o = self.q.get()
+ self.waiters.remove(ct)
+
+ self.threads.remove(ct)
+
+
+ def stop(self):
+ """
+ Shutdown the threads in the threadpool.
+ """
+ self.joined = True
+ threads = copy.copy(self.threads)
+ while self.workers:
+ self.q.put(WorkerStop)
+ self.workers -= 1
+
+ # and let's just make sure
+ # FIXME: threads that have died before calling stop() are not joined.
+ for thread in threads:
+ thread.join()
+
+
+ def adjustPoolsize(self, minthreads=None, maxthreads=None):
+ if minthreads is None:
+ minthreads = self.min
+ if maxthreads is None:
+ maxthreads = self.max
+
+ assert minthreads >= 0, 'minimum is negative'
+ assert minthreads <= maxthreads, 'minimum is greater than maximum'
+
+ self.min = minthreads
+ self.max = maxthreads
+ if not self.started:
+ return
+
+ # Kill of some threads if we have too many.
+ while self.workers > self.max:
+ self.stopAWorker()
+ # Start some threads if we have too few.
+ while self.workers < self.min:
+ self.startAWorker()
+ # Start some threads if there is a need.
+ self._startSomeWorkers()
+
+
+ def dumpStats(self):
+ log.msg('queue: %s' % self.q.queue)
+ log.msg('waiters: %s' % self.waiters)
+ log.msg('workers: %s' % self.working)
+ log.msg('total: %s' % self.threads)
diff --git a/twisted/python/twisted-completion.zsh b/twisted/python/twisted-completion.zsh
new file mode 100644
index 0000000..70cb89e
--- /dev/null
+++ b/twisted/python/twisted-completion.zsh
@@ -0,0 +1,33 @@
+#compdef twistd trial conch cftp tapconvert ckeygen lore pyhtmlizer tap2deb tkconch manhole tap2rpm
+#
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked all
+# over the user's terminal if completing options for a deprecated command.
+# Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/urlpath.py b/twisted/python/urlpath.py
new file mode 100644
index 0000000..1c15f09
--- /dev/null
+++ b/twisted/python/urlpath.py
@@ -0,0 +1,122 @@
+# -*- test-case-name: twisted.test.test_paths -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+import urlparse
+import urllib
+
+class URLPath:
+ def __init__(self, scheme='', netloc='localhost', path='',
+ query='', fragment=''):
+ self.scheme = scheme or 'http'
+ self.netloc = netloc
+ self.path = path or '/'
+ self.query = query
+ self.fragment = fragment
+
+ _qpathlist = None
+ _uqpathlist = None
+
+ def pathList(self, unquote=0, copy=1):
+ if self._qpathlist is None:
+ self._qpathlist = self.path.split('/')
+ self._uqpathlist = map(urllib.unquote, self._qpathlist)
+ if unquote:
+ result = self._uqpathlist
+ else:
+ result = self._qpathlist
+ if copy:
+ return result[:]
+ else:
+ return result
+
+ def fromString(klass, st):
+ t = urlparse.urlsplit(st)
+ u = klass(*t)
+ return u
+
+ fromString = classmethod(fromString)
+
+ def fromRequest(klass, request):
+ return klass.fromString(request.prePathURL())
+
+ fromRequest = classmethod(fromRequest)
+
+ def _pathMod(self, newpathsegs, keepQuery):
+ if keepQuery:
+ query = self.query
+ else:
+ query = ''
+ return URLPath(self.scheme,
+ self.netloc,
+ '/'.join(newpathsegs),
+ query)
+
+ def sibling(self, path, keepQuery=0):
+ l = self.pathList()
+ l[-1] = path
+ return self._pathMod(l, keepQuery)
+
+ def child(self, path, keepQuery=0):
+ l = self.pathList()
+ if l[-1] == '':
+ l[-1] = path
+ else:
+ l.append(path)
+ return self._pathMod(l, keepQuery)
+
+ def parent(self, keepQuery=0):
+ l = self.pathList()
+ if l[-1] == '':
+ del l[-2]
+ else:
+ # We are a file, such as http://example.com/foo/bar
+ # our parent directory is http://example.com/
+ l.pop()
+ l[-1] = ''
+ return self._pathMod(l, keepQuery)
+
+ def here(self, keepQuery=0):
+ l = self.pathList()
+ if l[-1] != '':
+ l[-1] = ''
+ return self._pathMod(l, keepQuery)
+
+ def click(self, st):
+ """Return a path which is the URL where a browser would presumably take
+ you if you clicked on a link with an HREF as given.
+ """
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(st)
+ if not scheme:
+ scheme = self.scheme
+ if not netloc:
+ netloc = self.netloc
+ if not path:
+ path = self.path
+ if not query:
+ query = self.query
+ elif path[0] != '/':
+ l = self.pathList()
+ l[-1] = path
+ path = '/'.join(l)
+
+ return URLPath(scheme,
+ netloc,
+ path,
+ query,
+ fragment)
+
+
+
+ def __str__(self):
+ x = urlparse.urlunsplit((
+ self.scheme, self.netloc, self.path,
+ self.query, self.fragment))
+ return x
+
+ def __repr__(self):
+ return ('URLPath(scheme=%r, netloc=%r, path=%r, query=%r, fragment=%r)'
+ % (self.scheme, self.netloc, self.path, self.query, self.fragment))
+
diff --git a/twisted/python/usage.py b/twisted/python/usage.py
new file mode 100644
index 0000000..9280ae2
--- /dev/null
+++ b/twisted/python/usage.py
@@ -0,0 +1,973 @@
+# -*- test-case-name: twisted.test.test_usage -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+twisted.python.usage is a module for parsing/handling the
+command line of your program.
+
+For information on how to use it, see
+U{http://twistedmatrix.com/projects/core/documentation/howto/options.html},
+or doc/core/howto/options.xhtml in your Twisted directory.
+"""
+
+# System Imports
+import os
+import sys
+import getopt
+from os import path
+
+# Sibling Imports
+from twisted.python import reflect, text, util
+
+
+class UsageError(Exception):
+ pass
+
+
+error = UsageError
+
+
+class CoerceParameter(object):
+ """
+ Utility class that can corce a parameter before storing it.
+ """
+ def __init__(self, options, coerce):
+ """
+ @param options: parent Options object
+ @param coerce: callable used to coerce the value.
+ """
+ self.options = options
+ self.coerce = coerce
+ self.doc = getattr(self.coerce, 'coerceDoc', '')
+
+ def dispatch(self, parameterName, value):
+ """
+ When called in dispatch, do the coerce for C{value} and save the
+ returned value.
+ """
+ if value is None:
+ raise UsageError("Parameter '%s' requires an argument."
+ % (parameterName,))
+ try:
+ value = self.coerce(value)
+ except ValueError, e:
+ raise UsageError("Parameter type enforcement failed: %s" % (e,))
+
+ self.options.opts[parameterName] = value
+
+
+class Options(dict):
+ """
+ An option list parser class
+
+ C{optFlags} and C{optParameters} are lists of available parameters
+ which your program can handle. The difference between the two
+ is the 'flags' have an on(1) or off(0) state (off by default)
+ whereas 'parameters' have an assigned value, with an optional
+ default. (Compare '--verbose' and '--verbosity=2')
+
+ optFlags is assigned a list of lists. Each list represents
+ a flag parameter, as so::
+
+ | optFlags = [['verbose', 'v', 'Makes it tell you what it doing.'],
+ | ['quiet', 'q', 'Be vewy vewy quiet.']]
+
+ As you can see, the first item is the long option name
+ (prefixed with '--' on the command line), followed by the
+ short option name (prefixed with '-'), and the description.
+ The description is used for the built-in handling of the
+ --help switch, which prints a usage summary.
+
+ C{optParameters} is much the same, except the list also contains
+ a default value::
+
+ | optParameters = [['outfile', 'O', 'outfile.log', 'Description...']]
+
+ A coerce function can also be specified as the last element: it will be
+ called with the argument and should return the value that will be stored
+ for the option. This function can have a C{coerceDoc} attribute which
+ will be appended to the documentation of the option.
+
+ subCommands is a list of 4-tuples of (command name, command shortcut,
+ parser class, documentation). If the first non-option argument found is
+ one of the given command names, an instance of the given parser class is
+ instantiated and given the remainder of the arguments to parse and
+ self.opts[command] is set to the command name. For example::
+
+ | subCommands = [
+ | ['inquisition', 'inquest', InquisitionOptions,
+ | 'Perform an inquisition'],
+ | ['holyquest', 'quest', HolyQuestOptions,
+ | 'Embark upon a holy quest']
+ | ]
+
+ In this case, C{"<program> holyquest --horseback --for-grail"} will cause
+ C{HolyQuestOptions} to be instantiated and asked to parse
+ C{['--horseback', '--for-grail']}. Currently, only the first sub-command
+ is parsed, and all options following it are passed to its parser. If a
+ subcommand is found, the subCommand attribute is set to its name and the
+ subOptions attribute is set to the Option instance that parses the
+ remaining options. If a subcommand is not given to parseOptions,
+ the subCommand attribute will be None. You can also mark one of
+ the subCommands to be the default.
+
+ | defaultSubCommand = 'holyquest'
+
+ In this case, the subCommand attribute will never be None, and
+ the subOptions attribute will always be set.
+
+ If you want to handle your own options, define a method named
+ C{opt_paramname} that takes C{(self, option)} as arguments. C{option}
+ will be whatever immediately follows the parameter on the
+ command line. Options fully supports the mapping interface, so you
+ can do things like C{'self["option"] = val'} in these methods.
+
+ Shell tab-completion is supported by this class, for zsh only at present.
+ Zsh ships with a stub file ("completion function") which, for Twisted
+ commands, performs tab-completion on-the-fly using the support provided
+ by this class. The stub file lives in our tree at
+ C{twisted/python/twisted-completion.zsh}, and in the Zsh tree at
+ C{Completion/Unix/Command/_twisted}.
+
+ Tab-completion is based upon the contents of the optFlags and optParameters
+ lists. And, optionally, additional metadata may be provided by assigning a
+ special attribute, C{compData}, which should be an instance of
+ C{Completions}. See that class for details of what can and should be
+ included - and see the howto for additional help using these features -
+ including how third-parties may take advantage of tab-completion for their
+ own commands.
+
+ Advanced functionality is covered in the howto documentation,
+ available at
+ U{http://twistedmatrix.com/projects/core/documentation/howto/options.html},
+ or doc/core/howto/options.xhtml in your Twisted directory.
+ """
+
+ subCommand = None
+ defaultSubCommand = None
+ parent = None
+ completionData = None
+ _shellCompFile = sys.stdout # file to use if shell completion is requested
+ def __init__(self):
+ super(Options, self).__init__()
+
+ self.opts = self
+ self.defaults = {}
+
+ # These are strings/lists we will pass to getopt
+ self.longOpt = []
+ self.shortOpt = ''
+ self.docs = {}
+ self.synonyms = {}
+ self._dispatch = {}
+
+
+ collectors = [
+ self._gather_flags,
+ self._gather_parameters,
+ self._gather_handlers,
+ ]
+
+ for c in collectors:
+ (longOpt, shortOpt, docs, settings, synonyms, dispatch) = c()
+ self.longOpt.extend(longOpt)
+ self.shortOpt = self.shortOpt + shortOpt
+ self.docs.update(docs)
+
+ self.opts.update(settings)
+ self.defaults.update(settings)
+
+ self.synonyms.update(synonyms)
+ self._dispatch.update(dispatch)
+
+ def __hash__(self):
+ """
+ Define a custom hash function so that Options instances can be used
+ as dictionary keys. This is an internal feature used to implement
+ the parser. Do not rely on it in application code.
+ """
+ return int(id(self) % sys.maxint)
+
+ def opt_help(self):
+ """
+ Display this help and exit.
+ """
+ print self.__str__()
+ sys.exit(0)
+
+ def opt_version(self):
+ """
+ Display Twisted version and exit.
+ """
+ from twisted import copyright
+ print "Twisted version:", copyright.version
+ sys.exit(0)
+
+ #opt_h = opt_help # this conflicted with existing 'host' options.
+
+ def parseOptions(self, options=None):
+ """
+ The guts of the command-line parser.
+ """
+
+ if options is None:
+ options = sys.argv[1:]
+
+ # we really do need to place the shell completion check here, because
+ # if we used an opt_shell_completion method then it would be possible
+ # for other opt_* methods to be run first, and they could possibly
+ # raise validation errors which would result in error output on the
+ # terminal of the user performing shell completion. Validation errors
+ # would occur quite frequently, in fact, because users often initiate
+ # tab-completion while they are editing an unfinished command-line.
+ if len(options) > 1 and options[-2] == "--_shell-completion":
+ from twisted.python import _shellcomp
+ cmdName = path.basename(sys.argv[0])
+ _shellcomp.shellComplete(self, cmdName, options,
+ self._shellCompFile)
+ sys.exit(0)
+
+ try:
+ opts, args = getopt.getopt(options,
+ self.shortOpt, self.longOpt)
+ except getopt.error, e:
+ raise UsageError(str(e))
+
+ for opt, arg in opts:
+ if opt[1] == '-':
+ opt = opt[2:]
+ else:
+ opt = opt[1:]
+
+ optMangled = opt
+ if optMangled not in self.synonyms:
+ optMangled = opt.replace("-", "_")
+ if optMangled not in self.synonyms:
+ raise UsageError("No such option '%s'" % (opt,))
+
+ optMangled = self.synonyms[optMangled]
+ if isinstance(self._dispatch[optMangled], CoerceParameter):
+ self._dispatch[optMangled].dispatch(optMangled, arg)
+ else:
+ self._dispatch[optMangled](optMangled, arg)
+
+ if (getattr(self, 'subCommands', None)
+ and (args or self.defaultSubCommand is not None)):
+ if not args:
+ args = [self.defaultSubCommand]
+ sub, rest = args[0], args[1:]
+ for (cmd, short, parser, doc) in self.subCommands:
+ if sub == cmd or sub == short:
+ self.subCommand = cmd
+ self.subOptions = parser()
+ self.subOptions.parent = self
+ self.subOptions.parseOptions(rest)
+ break
+ else:
+ raise UsageError("Unknown command: %s" % sub)
+ else:
+ try:
+ self.parseArgs(*args)
+ except TypeError:
+ raise UsageError("Wrong number of arguments.")
+
+ self.postOptions()
+
+ def postOptions(self):
+ """
+ I am called after the options are parsed.
+
+ Override this method in your subclass to do something after
+ the options have been parsed and assigned, like validate that
+ all options are sane.
+ """
+
+ def parseArgs(self):
+ """
+ I am called with any leftover arguments which were not options.
+
+ Override me to do something with the remaining arguments on
+ the command line, those which were not flags or options. e.g.
+ interpret them as a list of files to operate on.
+
+ Note that if there more arguments on the command line
+ than this method accepts, parseArgs will blow up with
+ a getopt.error. This means if you don't override me,
+ parseArgs will blow up if I am passed any arguments at
+ all!
+ """
+
+ def _generic_flag(self, flagName, value=None):
+ if value not in ('', None):
+ raise UsageError("Flag '%s' takes no argument."
+ " Not even \"%s\"." % (flagName, value))
+
+ self.opts[flagName] = 1
+
+ def _gather_flags(self):
+ """
+ Gather up boolean (flag) options.
+ """
+
+ longOpt, shortOpt = [], ''
+ docs, settings, synonyms, dispatch = {}, {}, {}, {}
+
+ flags = []
+ reflect.accumulateClassList(self.__class__, 'optFlags', flags)
+
+ for flag in flags:
+ long, short, doc = util.padTo(3, flag)
+ if not long:
+ raise ValueError("A flag cannot be without a name.")
+
+ docs[long] = doc
+ settings[long] = 0
+ if short:
+ shortOpt = shortOpt + short
+ synonyms[short] = long
+ longOpt.append(long)
+ synonyms[long] = long
+ dispatch[long] = self._generic_flag
+
+ return longOpt, shortOpt, docs, settings, synonyms, dispatch
+
+ def _gather_parameters(self):
+ """
+ Gather options which take a value.
+ """
+ longOpt, shortOpt = [], ''
+ docs, settings, synonyms, dispatch = {}, {}, {}, {}
+
+ parameters = []
+
+ reflect.accumulateClassList(self.__class__, 'optParameters',
+ parameters)
+
+ synonyms = {}
+
+ for parameter in parameters:
+ long, short, default, doc, paramType = util.padTo(5, parameter)
+ if not long:
+ raise ValueError("A parameter cannot be without a name.")
+
+ docs[long] = doc
+ settings[long] = default
+ if short:
+ shortOpt = shortOpt + short + ':'
+ synonyms[short] = long
+ longOpt.append(long + '=')
+ synonyms[long] = long
+ if paramType is not None:
+ dispatch[long] = CoerceParameter(self, paramType)
+ else:
+ dispatch[long] = CoerceParameter(self, str)
+
+ return longOpt, shortOpt, docs, settings, synonyms, dispatch
+
+
+ def _gather_handlers(self):
+ """
+ Gather up options with their own handler methods.
+
+ This returns a tuple of many values. Amongst those values is a
+ synonyms dictionary, mapping all of the possible aliases (C{str})
+ for an option to the longest spelling of that option's name
+ C({str}).
+
+ Another element is a dispatch dictionary, mapping each user-facing
+ option name (with - substituted for _) to a callable to handle that
+ option.
+ """
+
+ longOpt, shortOpt = [], ''
+ docs, settings, synonyms, dispatch = {}, {}, {}, {}
+
+ dct = {}
+ reflect.addMethodNamesToDict(self.__class__, dct, "opt_")
+
+ for name in dct.keys():
+ method = getattr(self, 'opt_'+name)
+
+ takesArg = not flagFunction(method, name)
+
+ prettyName = name.replace('_', '-')
+ doc = getattr(method, '__doc__', None)
+ if doc:
+ ## Only use the first line.
+ #docs[name] = doc.split('\n')[0]
+ docs[prettyName] = doc
+ else:
+ docs[prettyName] = self.docs.get(prettyName)
+
+ synonyms[prettyName] = prettyName
+
+ # A little slight-of-hand here makes dispatching much easier
+ # in parseOptions, as it makes all option-methods have the
+ # same signature.
+ if takesArg:
+ fn = lambda name, value, m=method: m(value)
+ else:
+ # XXX: This won't raise a TypeError if it's called
+ # with a value when it shouldn't be.
+ fn = lambda name, value=None, m=method: m()
+
+ dispatch[prettyName] = fn
+
+ if len(name) == 1:
+ shortOpt = shortOpt + name
+ if takesArg:
+ shortOpt = shortOpt + ':'
+ else:
+ if takesArg:
+ prettyName = prettyName + '='
+ longOpt.append(prettyName)
+
+ reverse_dct = {}
+ # Map synonyms
+ for name in dct.keys():
+ method = getattr(self, 'opt_' + name)
+ if method not in reverse_dct:
+ reverse_dct[method] = []
+ reverse_dct[method].append(name.replace('_', '-'))
+
+ cmpLength = lambda a, b: cmp(len(a), len(b))
+
+ for method, names in reverse_dct.items():
+ if len(names) < 2:
+ continue
+ names_ = names[:]
+ names_.sort(cmpLength)
+ longest = names_.pop()
+ for name in names_:
+ synonyms[name] = longest
+
+ return longOpt, shortOpt, docs, settings, synonyms, dispatch
+
+
+ def __str__(self):
+ return self.getSynopsis() + '\n' + self.getUsage(width=None)
+
+ def getSynopsis(self):
+ """
+ Returns a string containing a description of these options and how to
+ pass them to the executed file.
+ """
+
+ default = "%s%s" % (path.basename(sys.argv[0]),
+ (self.longOpt and " [options]") or '')
+ if self.parent is None:
+ default = "Usage: %s%s" % (path.basename(sys.argv[0]),
+ (self.longOpt and " [options]") or '')
+ else:
+ default = '%s' % ((self.longOpt and "[options]") or '')
+ synopsis = getattr(self, "synopsis", default)
+
+ synopsis = synopsis.rstrip()
+
+ if self.parent is not None:
+ synopsis = ' '.join((self.parent.getSynopsis(),
+ self.parent.subCommand, synopsis))
+
+ return synopsis
+
+ def getUsage(self, width=None):
+ # If subOptions exists by now, then there was probably an error while
+ # parsing its options.
+ if hasattr(self, 'subOptions'):
+ return self.subOptions.getUsage(width=width)
+
+ if not width:
+ width = int(os.environ.get('COLUMNS', '80'))
+
+ if hasattr(self, 'subCommands'):
+ cmdDicts = []
+ for (cmd, short, parser, desc) in self.subCommands:
+ cmdDicts.append(
+ {'long': cmd,
+ 'short': short,
+ 'doc': desc,
+ 'optType': 'command',
+ 'default': None
+ })
+ chunks = docMakeChunks(cmdDicts, width)
+ commands = 'Commands:\n' + ''.join(chunks)
+ else:
+ commands = ''
+
+ longToShort = {}
+ for key, value in self.synonyms.items():
+ longname = value
+ if (key != longname) and (len(key) == 1):
+ longToShort[longname] = key
+ else:
+ if longname not in longToShort:
+ longToShort[longname] = None
+ else:
+ pass
+
+ optDicts = []
+ for opt in self.longOpt:
+ if opt[-1] == '=':
+ optType = 'parameter'
+ opt = opt[:-1]
+ else:
+ optType = 'flag'
+
+ optDicts.append(
+ {'long': opt,
+ 'short': longToShort[opt],
+ 'doc': self.docs[opt],
+ 'optType': optType,
+ 'default': self.defaults.get(opt, None),
+ 'dispatch': self._dispatch.get(opt, None)
+ })
+
+ if not (getattr(self, "longdesc", None) is None):
+ longdesc = self.longdesc
+ else:
+ import __main__
+ if getattr(__main__, '__doc__', None):
+ longdesc = __main__.__doc__
+ else:
+ longdesc = ''
+
+ if longdesc:
+ longdesc = ('\n' +
+ '\n'.join(text.wordWrap(longdesc, width)).strip()
+ + '\n')
+
+ if optDicts:
+ chunks = docMakeChunks(optDicts, width)
+ s = "Options:\n%s" % (''.join(chunks))
+ else:
+ s = "Options: None\n"
+
+ return s + longdesc + commands
+
+ #def __repr__(self):
+ # XXX: It'd be cool if we could return a succinct representation
+ # of which flags and options are set here.
+
+
+_ZSH = 'zsh'
+_BASH = 'bash'
+
+class Completer(object):
+ """
+ A completion "action" - provides completion possibilities for a particular
+ command-line option. For example we might provide the user a fixed list of
+ choices, or files/dirs according to a glob.
+
+ This class produces no completion matches itself - see the various
+ subclasses for specific completion functionality.
+ """
+ _descr = None
+ def __init__(self, descr=None, repeat=False):
+ """
+ @type descr: C{str}
+ @param descr: An optional descriptive string displayed above matches.
+
+ @type repeat: C{bool}
+ @param repeat: A flag, defaulting to False, indicating whether this
+ C{Completer} should repeat - that is, be used to complete more
+ than one command-line word. This may ONLY be set to True for
+ actions in the C{extraActions} keyword argument to C{Completions}.
+ And ONLY if it is the LAST (or only) action in the C{extraActions}
+ list.
+ """
+ if descr is not None:
+ self._descr = descr
+ self._repeat = repeat
+
+
+ def _getRepeatFlag(self):
+ if self._repeat:
+ return "*"
+ else:
+ return ""
+ _repeatFlag = property(_getRepeatFlag)
+
+
+ def _description(self, optName):
+ if self._descr is not None:
+ return self._descr
+ else:
+ return optName
+
+
+ def _shellCode(self, optName, shellType):
+ """
+ Fetch a fragment of shell code representing this action which is
+ suitable for use by the completion system in _shellcomp.py
+
+ @type optName: C{str}
+ @param optName: The long name of the option this action is being
+ used for.
+
+ @type shellType: C{str}
+ @param shellType: One of the supported shell constants e.g.
+ C{twisted.python.usage._ZSH}
+ """
+ if shellType == _ZSH:
+ return "%s:%s:" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteFiles(Completer):
+ """
+ Completes file names based on a glob pattern
+ """
+ def __init__(self, globPattern='*', **kw):
+ Completer.__init__(self, **kw)
+ self._globPattern = globPattern
+
+
+ def _description(self, optName):
+ if self._descr is not None:
+ return "%s (%s)" % (self._descr, self._globPattern)
+ else:
+ return "%s (%s)" % (optName, self._globPattern)
+
+
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_files -g \"%s\"" % (self._repeatFlag,
+ self._description(optName),
+ self._globPattern,)
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteDirs(Completer):
+ """
+ Completes directory names
+ """
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_directories" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteList(Completer):
+ """
+ Completes based on a fixed list of words
+ """
+ def __init__(self, items, **kw):
+ Completer.__init__(self, **kw)
+ self._items = items
+
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:(%s)" % (self._repeatFlag,
+ self._description(optName),
+ " ".join(self._items,))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteMultiList(Completer):
+ """
+ Completes multiple comma-separated items based on a fixed list of words
+ """
+ def __init__(self, items, **kw):
+ Completer.__init__(self, **kw)
+ self._items = items
+
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_values -s , '%s' %s" % (self._repeatFlag,
+ self._description(optName),
+ self._description(optName),
+ " ".join(self._items))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteUsernames(Completer):
+ """
+ Complete usernames
+ """
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_users" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteGroups(Completer):
+ """
+ Complete system group names
+ """
+ _descr = 'group'
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_groups" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteHostnames(Completer):
+ """
+ Complete hostnames
+ """
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_hosts" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteUserAtHost(Completer):
+ """
+ A completion action which produces matches in any of these forms::
+ <username>
+ <hostname>
+ <username>@<hostname>
+ """
+ _descr = 'host | user@host'
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ # Yes this looks insane but it does work. For bonus points
+ # add code to grep 'Hostname' lines from ~/.ssh/config
+ return ('%s:%s:{_ssh;if compset -P "*@"; '
+ 'then _wanted hosts expl "remote host name" _ssh_hosts '
+ '&& ret=0 elif compset -S "@*"; then _wanted users '
+ 'expl "login name" _ssh_users -S "" && ret=0 '
+ 'else if (( $+opt_args[-l] )); then tmp=() '
+ 'else tmp=( "users:login name:_ssh_users -qS@" ) fi; '
+ '_alternative "hosts:remote host name:_ssh_hosts" "$tmp[@]"'
+ ' && ret=0 fi}' % (self._repeatFlag,
+ self._description(optName)))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class CompleteNetInterfaces(Completer):
+ """
+ Complete network interface names
+ """
+ def _shellCode(self, optName, shellType):
+ if shellType == _ZSH:
+ return "%s:%s:_net_interfaces" % (self._repeatFlag,
+ self._description(optName))
+ raise NotImplementedError("Unknown shellType %r" % (shellType,))
+
+
+
+class Completions(object):
+ """
+ Extra metadata for the shell tab-completion system.
+
+ @type descriptions: C{dict}
+ @ivar descriptions: ex. C{{"foo" : "use this description for foo instead"}}
+ A dict mapping long option names to alternate descriptions. When this
+ variable is defined, the descriptions contained here will override
+ those descriptions provided in the optFlags and optParameters
+ variables.
+
+ @type multiUse: C{list}
+ @ivar multiUse: ex. C{ ["foo", "bar"] }
+ An iterable containing those long option names which may appear on the
+ command line more than once. By default, options will only be completed
+ one time.
+
+ @type mutuallyExclusive: C{list} of C{tuple}
+ @ivar mutuallyExclusive: ex. C{ [("foo", "bar"), ("bar", "baz")] }
+ A sequence of sequences, with each sub-sequence containing those long
+ option names that are mutually exclusive. That is, those options that
+ cannot appear on the command line together.
+
+ @type optActions: C{dict}
+ @ivar optActions: A dict mapping long option names to shell "actions".
+ These actions define what may be completed as the argument to the
+ given option. By default, all files/dirs will be completed if no
+ action is given. For example::
+
+ {"foo" : CompleteFiles("*.py", descr="python files"),
+ "bar" : CompleteList(["one", "two", "three"]),
+ "colors" : CompleteMultiList(["red", "green", "blue"])}
+
+ Callables may instead be given for the values in this dict. The
+ callable should accept no arguments, and return a C{Completer}
+ instance used as the action in the same way as the literal actions in
+ the example above.
+
+ As you can see in the example above. The "foo" option will have files
+ that end in .py completed when the user presses Tab. The "bar"
+ option will have either of the strings "one", "two", or "three"
+ completed when the user presses Tab.
+
+ "colors" will allow multiple arguments to be completed, seperated by
+ commas. The possible arguments are red, green, and blue. Examples::
+
+ my_command --foo some-file.foo --colors=red,green
+ my_command --colors=green
+ my_command --colors=green,blue
+
+ Descriptions for the actions may be given with the optional C{descr}
+ keyword argument. This is separate from the description of the option
+ itself.
+
+ Normally Zsh does not show these descriptions unless you have
+ "verbose" completion turned on. Turn on verbosity with this in your
+ ~/.zshrc::
+
+ zstyle ':completion:*' verbose yes
+ zstyle ':completion:*:descriptions' format '%B%d%b'
+
+ @type extraActions: C{list}
+ @ivar extraActions: Extra arguments are those arguments typically
+ appearing at the end of the command-line, which are not associated
+ with any particular named option. That is, the arguments that are
+ given to the parseArgs() method of your usage.Options subclass. For
+ example::
+ [CompleteFiles(descr="file to read from"),
+ Completer(descr="book title")]
+
+ In the example above, the 1st non-option argument will be described as
+ "file to read from" and all file/dir names will be completed (*). The
+ 2nd non-option argument will be described as "book title", but no
+ actual completion matches will be produced.
+
+ See the various C{Completer} subclasses for other types of things which
+ may be tab-completed (users, groups, network interfaces, etc).
+
+ Also note the C{repeat=True} flag which may be passed to any of the
+ C{Completer} classes. This is set to allow the C{Completer} instance
+ to be re-used for subsequent command-line words. See the C{Completer}
+ docstring for details.
+ """
+ def __init__(self, descriptions={}, multiUse=[],
+ mutuallyExclusive=[], optActions={}, extraActions=[]):
+ self.descriptions = descriptions
+ self.multiUse = multiUse
+ self.mutuallyExclusive = mutuallyExclusive
+ self.optActions = optActions
+ self.extraActions = extraActions
+
+
+
+def docMakeChunks(optList, width=80):
+ """
+ Makes doc chunks for option declarations.
+
+ Takes a list of dictionaries, each of which may have one or more
+ of the keys 'long', 'short', 'doc', 'default', 'optType'.
+
+ Returns a list of strings.
+ The strings may be multiple lines,
+ all of them end with a newline.
+ """
+
+ # XXX: sanity check to make sure we have a sane combination of keys.
+
+ maxOptLen = 0
+ for opt in optList:
+ optLen = len(opt.get('long', ''))
+ if optLen:
+ if opt.get('optType', None) == "parameter":
+ # these take up an extra character
+ optLen = optLen + 1
+ maxOptLen = max(optLen, maxOptLen)
+
+ colWidth1 = maxOptLen + len(" -s, -- ")
+ colWidth2 = width - colWidth1
+ # XXX - impose some sane minimum limit.
+ # Then if we don't have enough room for the option and the doc
+ # to share one line, they can take turns on alternating lines.
+
+ colFiller1 = " " * colWidth1
+
+ optChunks = []
+ seen = {}
+ for opt in optList:
+ if opt.get('short', None) in seen or opt.get('long', None) in seen:
+ continue
+ for x in opt.get('short', None), opt.get('long', None):
+ if x is not None:
+ seen[x] = 1
+
+ optLines = []
+ comma = " "
+ if opt.get('short', None):
+ short = "-%c" % (opt['short'],)
+ else:
+ short = ''
+
+ if opt.get('long', None):
+ long = opt['long']
+ if opt.get("optType", None) == "parameter":
+ long = long + '='
+
+ long = "%-*s" % (maxOptLen, long)
+ if short:
+ comma = ","
+ else:
+ long = " " * (maxOptLen + len('--'))
+
+ if opt.get('optType', None) == 'command':
+ column1 = ' %s ' % long
+ else:
+ column1 = " %2s%c --%s " % (short, comma, long)
+
+ if opt.get('doc', ''):
+ doc = opt['doc'].strip()
+ else:
+ doc = ''
+
+ if (opt.get("optType", None) == "parameter") \
+ and not (opt.get('default', None) is None):
+ doc = "%s [default: %s]" % (doc, opt['default'])
+
+ if (opt.get("optType", None) == "parameter") \
+ and opt.get('dispatch', None) is not None:
+ d = opt['dispatch']
+ if isinstance(d, CoerceParameter) and d.doc:
+ doc = "%s. %s" % (doc, d.doc)
+
+ if doc:
+ column2_l = text.wordWrap(doc, colWidth2)
+ else:
+ column2_l = ['']
+
+ optLines.append("%s%s\n" % (column1, column2_l.pop(0)))
+
+ for line in column2_l:
+ optLines.append("%s%s\n" % (colFiller1, line))
+
+ optChunks.append(''.join(optLines))
+
+ return optChunks
+
+
+def flagFunction(method, name=None):
+ reqArgs = method.im_func.func_code.co_argcount
+ if reqArgs > 2:
+ raise UsageError('Invalid Option function for %s' %
+ (name or method.func_name))
+ if reqArgs == 2:
+ # argName = method.im_func.func_code.co_varnames[1]
+ return 0
+ return 1
+
+
+def portCoerce(value):
+ """
+ Coerce a string value to an int port number, and checks the validity.
+ """
+ value = int(value)
+ if value < 0 or value > 65535:
+ raise ValueError("Port number not in range: %s" % (value,))
+ return value
+portCoerce.coerceDoc = "Must be an int between 0 and 65535."
+
+
diff --git a/twisted/python/util.py b/twisted/python/util.py
new file mode 100644
index 0000000..ee4cc8a
--- /dev/null
+++ b/twisted/python/util.py
@@ -0,0 +1,983 @@
+# -*- test-case-name: twisted.python.test.test_util -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import os, sys, errno, inspect, warnings
+import types
+try:
+ import pwd, grp
+except ImportError:
+ pwd = grp = None
+try:
+ from os import setgroups, getgroups
+except ImportError:
+ setgroups = getgroups = None
+from UserDict import UserDict
+
+
+class InsensitiveDict:
+ """Dictionary, that has case-insensitive keys.
+
+ Normally keys are retained in their original form when queried with
+ .keys() or .items(). If initialized with preserveCase=0, keys are both
+ looked up in lowercase and returned in lowercase by .keys() and .items().
+ """
+ """
+ Modified recipe at
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66315 originally
+ contributed by Sami Hangaslammi.
+ """
+
+ def __init__(self, dict=None, preserve=1):
+ """Create an empty dictionary, or update from 'dict'."""
+ self.data = {}
+ self.preserve=preserve
+ if dict:
+ self.update(dict)
+
+ def __delitem__(self, key):
+ k=self._lowerOrReturn(key)
+ del self.data[k]
+
+ def _lowerOrReturn(self, key):
+ if isinstance(key, str) or isinstance(key, unicode):
+ return key.lower()
+ else:
+ return key
+
+ def __getitem__(self, key):
+ """Retrieve the value associated with 'key' (in any case)."""
+ k = self._lowerOrReturn(key)
+ return self.data[k][1]
+
+ def __setitem__(self, key, value):
+ """Associate 'value' with 'key'. If 'key' already exists, but
+ in different case, it will be replaced."""
+ k = self._lowerOrReturn(key)
+ self.data[k] = (key, value)
+
+ def has_key(self, key):
+ """Case insensitive test whether 'key' exists."""
+ k = self._lowerOrReturn(key)
+ return self.data.has_key(k)
+ __contains__=has_key
+
+ def _doPreserve(self, key):
+ if not self.preserve and (isinstance(key, str)
+ or isinstance(key, unicode)):
+ return key.lower()
+ else:
+ return key
+
+ def keys(self):
+ """List of keys in their original case."""
+ return list(self.iterkeys())
+
+ def values(self):
+ """List of values."""
+ return list(self.itervalues())
+
+ def items(self):
+ """List of (key,value) pairs."""
+ return list(self.iteritems())
+
+ def get(self, key, default=None):
+ """Retrieve value associated with 'key' or return default value
+ if 'key' doesn't exist."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def setdefault(self, key, default):
+ """If 'key' doesn't exists, associate it with the 'default' value.
+ Return value associated with 'key'."""
+ if not self.has_key(key):
+ self[key] = default
+ return self[key]
+
+ def update(self, dict):
+ """Copy (key,value) pairs from 'dict'."""
+ for k,v in dict.items():
+ self[k] = v
+
+ def __repr__(self):
+ """String representation of the dictionary."""
+ items = ", ".join([("%r: %r" % (k,v)) for k,v in self.items()])
+ return "InsensitiveDict({%s})" % items
+
+ def iterkeys(self):
+ for v in self.data.itervalues():
+ yield self._doPreserve(v[0])
+
+ def itervalues(self):
+ for v in self.data.itervalues():
+ yield v[1]
+
+ def iteritems(self):
+ for (k, v) in self.data.itervalues():
+ yield self._doPreserve(k), v
+
+ def popitem(self):
+ i=self.items()[0]
+ del self[i[0]]
+ return i
+
+ def clear(self):
+ for k in self.keys():
+ del self[k]
+
+ def copy(self):
+ return InsensitiveDict(self, self.preserve)
+
+ def __len__(self):
+ return len(self.data)
+
+ def __eq__(self, other):
+ for k,v in self.items():
+ if not (k in other) or not (other[k]==v):
+ return 0
+ return len(self)==len(other)
+
+class OrderedDict(UserDict):
+ """A UserDict that preserves insert order whenever possible."""
+ def __init__(self, dict=None, **kwargs):
+ self._order = []
+ self.data = {}
+ if dict is not None:
+ if hasattr(dict,'keys'):
+ self.update(dict)
+ else:
+ for k,v in dict: # sequence
+ self[k] = v
+ if len(kwargs):
+ self.update(kwargs)
+ def __repr__(self):
+ return '{'+', '.join([('%r: %r' % item) for item in self.items()])+'}'
+
+ def __setitem__(self, key, value):
+ if not self.has_key(key):
+ self._order.append(key)
+ UserDict.__setitem__(self, key, value)
+
+ def copy(self):
+ return self.__class__(self)
+
+ def __delitem__(self, key):
+ UserDict.__delitem__(self, key)
+ self._order.remove(key)
+
+ def iteritems(self):
+ for item in self._order:
+ yield (item, self[item])
+
+ def items(self):
+ return list(self.iteritems())
+
+ def itervalues(self):
+ for item in self._order:
+ yield self[item]
+
+ def values(self):
+ return list(self.itervalues())
+
+ def iterkeys(self):
+ return iter(self._order)
+
+ def keys(self):
+ return list(self._order)
+
+ def popitem(self):
+ key = self._order[-1]
+ value = self[key]
+ del self[key]
+ return (key, value)
+
+ def setdefault(self, item, default):
+ if self.has_key(item):
+ return self[item]
+ self[item] = default
+ return default
+
+ def update(self, d):
+ for k, v in d.items():
+ self[k] = v
+
+def uniquify(lst):
+ """Make the elements of a list unique by inserting them into a dictionary.
+ This must not change the order of the input lst.
+ """
+ dct = {}
+ result = []
+ for k in lst:
+ if not dct.has_key(k): result.append(k)
+ dct[k] = 1
+ return result
+
+def padTo(n, seq, default=None):
+ """Pads a sequence out to n elements,
+
+ filling in with a default value if it is not long enough.
+
+ If the input sequence is longer than n, raises ValueError.
+
+ Details, details:
+ This returns a new list; it does not extend the original sequence.
+ The new list contains the values of the original sequence, not copies.
+ """
+
+ if len(seq) > n:
+ raise ValueError, "%d elements is more than %d." % (len(seq), n)
+
+ blank = [default] * n
+
+ blank[:len(seq)] = list(seq)
+
+ return blank
+
+def getPluginDirs():
+ import twisted
+ systemPlugins = os.path.join(os.path.dirname(os.path.dirname(
+ os.path.abspath(twisted.__file__))), 'plugins')
+ userPlugins = os.path.expanduser("~/TwistedPlugins")
+ confPlugins = os.path.expanduser("~/.twisted")
+ allPlugins = filter(os.path.isdir, [systemPlugins, userPlugins, confPlugins])
+ return allPlugins
+
+def addPluginDir():
+ sys.path.extend(getPluginDirs())
+
+def sibpath(path, sibling):
+ """Return the path to a sibling of a file in the filesystem.
+
+ This is useful in conjunction with the special __file__ attribute
+ that Python provides for modules, so modules can load associated
+ resource files.
+ """
+ return os.path.join(os.path.dirname(os.path.abspath(path)), sibling)
+
+
+def _getpass(prompt):
+ """Helper to turn IOErrors into KeyboardInterrupts"""
+ import getpass
+ try:
+ return getpass.getpass(prompt)
+ except IOError, e:
+ if e.errno == errno.EINTR:
+ raise KeyboardInterrupt
+ raise
+ except EOFError:
+ raise KeyboardInterrupt
+
+def getPassword(prompt = 'Password: ', confirm = 0, forceTTY = 0,
+ confirmPrompt = 'Confirm password: ',
+ mismatchMessage = "Passwords don't match."):
+ """Obtain a password by prompting or from stdin.
+
+ If stdin is a terminal, prompt for a new password, and confirm (if
+ C{confirm} is true) by asking again to make sure the user typed the same
+ thing, as keystrokes will not be echoed.
+
+ If stdin is not a terminal, and C{forceTTY} is not true, read in a line
+ and use it as the password, less the trailing newline, if any. If
+ C{forceTTY} is true, attempt to open a tty and prompt for the password
+ using it. Raise a RuntimeError if this is not possible.
+
+ @returns: C{str}
+ """
+ isaTTY = hasattr(sys.stdin, 'isatty') and sys.stdin.isatty()
+
+ old = None
+ try:
+ if not isaTTY:
+ if forceTTY:
+ try:
+ old = sys.stdin, sys.stdout
+ sys.stdin = sys.stdout = open('/dev/tty', 'r+')
+ except:
+ raise RuntimeError("Cannot obtain a TTY")
+ else:
+ password = sys.stdin.readline()
+ if password[-1] == '\n':
+ password = password[:-1]
+ return password
+
+ while 1:
+ try1 = _getpass(prompt)
+ if not confirm:
+ return try1
+ try2 = _getpass(confirmPrompt)
+ if try1 == try2:
+ return try1
+ else:
+ sys.stderr.write(mismatchMessage + "\n")
+ finally:
+ if old:
+ sys.stdin.close()
+ sys.stdin, sys.stdout = old
+
+
+def println(*a):
+ sys.stdout.write(' '.join(map(str, a))+'\n')
+
+# XXX
+# This does not belong here
+# But where does it belong?
+
+def str_xor(s, b):
+ return ''.join([chr(ord(c) ^ b) for c in s])
+
+
+def makeStatBar(width, maxPosition, doneChar = '=', undoneChar = '-', currentChar = '>'):
+ """
+ Creates a function that will return a string representing a progress bar.
+ """
+ aValue = width / float(maxPosition)
+ def statBar(position, force = 0, last = ['']):
+ assert len(last) == 1, "Don't mess with the last parameter."
+ done = int(aValue * position)
+ toDo = width - done - 2
+ result = "[%s%s%s]" % (doneChar * done, currentChar, undoneChar * toDo)
+ if force:
+ last[0] = result
+ return result
+ if result == last[0]:
+ return ''
+ last[0] = result
+ return result
+
+ statBar.__doc__ = """statBar(position, force = 0) -> '[%s%s%s]'-style progress bar
+
+ returned string is %d characters long, and the range goes from 0..%d.
+ The 'position' argument is where the '%s' will be drawn. If force is false,
+ '' will be returned instead if the resulting progress bar is identical to the
+ previously returned progress bar.
+""" % (doneChar * 3, currentChar, undoneChar * 3, width, maxPosition, currentChar)
+ return statBar
+
+
+def spewer(frame, s, ignored):
+ """
+ A trace function for sys.settrace that prints every function or method call.
+ """
+ from twisted.python import reflect
+ if frame.f_locals.has_key('self'):
+ se = frame.f_locals['self']
+ if hasattr(se, '__class__'):
+ k = reflect.qual(se.__class__)
+ else:
+ k = reflect.qual(type(se))
+ print 'method %s of %s at %s' % (
+ frame.f_code.co_name, k, id(se)
+ )
+ else:
+ print 'function %s in %s, line %s' % (
+ frame.f_code.co_name,
+ frame.f_code.co_filename,
+ frame.f_lineno)
+
+
+def searchupwards(start, files=[], dirs=[]):
+ """
+ Walk upwards from start, looking for a directory containing
+ all files and directories given as arguments::
+ >>> searchupwards('.', ['foo.txt'], ['bar', 'bam'])
+
+ If not found, return None
+ """
+ start=os.path.abspath(start)
+ parents=start.split(os.sep)
+ exists=os.path.exists; join=os.sep.join; isdir=os.path.isdir
+ while len(parents):
+ candidate=join(parents)+os.sep
+ allpresent=1
+ for f in files:
+ if not exists("%s%s" % (candidate, f)):
+ allpresent=0
+ break
+ if allpresent:
+ for d in dirs:
+ if not isdir("%s%s" % (candidate, d)):
+ allpresent=0
+ break
+ if allpresent: return candidate
+ parents.pop(-1)
+ return None
+
+
+class LineLog:
+ """
+ A limited-size line-based log, useful for logging line-based
+ protocols such as SMTP.
+
+ When the log fills up, old entries drop off the end.
+ """
+ def __init__(self, size=10):
+ """
+ Create a new log, with size lines of storage (default 10).
+ A log size of 0 (or less) means an infinite log.
+ """
+ if size < 0:
+ size = 0
+ self.log = [None]*size
+ self.size = size
+
+ def append(self,line):
+ if self.size:
+ self.log[:-1] = self.log[1:]
+ self.log[-1] = line
+ else:
+ self.log.append(line)
+
+ def str(self):
+ return '\n'.join(filter(None,self.log))
+
+ def __getitem__(self, item):
+ return filter(None,self.log)[item]
+
+ def clear(self):
+ """Empty the log"""
+ self.log = [None]*self.size
+
+
+def raises(exception, f, *args, **kwargs):
+ """
+ Determine whether the given call raises the given exception.
+ """
+ try:
+ f(*args, **kwargs)
+ except exception:
+ return 1
+ return 0
+
+
+class IntervalDifferential:
+ """
+ Given a list of intervals, generate the amount of time to sleep between
+ "instants".
+
+ For example, given 7, 11 and 13, the three (infinite) sequences::
+
+ 7 14 21 28 35 ...
+ 11 22 33 44 ...
+ 13 26 39 52 ...
+
+ will be generated, merged, and used to produce::
+
+ (7, 0) (4, 1) (2, 2) (1, 0) (7, 0) (1, 1) (4, 2) (2, 0) (5, 1) (2, 0)
+
+ New intervals may be added or removed as iteration proceeds using the
+ proper methods.
+ """
+
+ def __init__(self, intervals, default=60):
+ """
+ @type intervals: C{list} of C{int}, C{long}, or C{float} param
+ @param intervals: The intervals between instants.
+
+ @type default: C{int}, C{long}, or C{float}
+ @param default: The duration to generate if the intervals list
+ becomes empty.
+ """
+ self.intervals = intervals[:]
+ self.default = default
+
+ def __iter__(self):
+ return _IntervalDifferentialIterator(self.intervals, self.default)
+
+
+class _IntervalDifferentialIterator:
+ def __init__(self, i, d):
+
+ self.intervals = [[e, e, n] for (e, n) in zip(i, range(len(i)))]
+ self.default = d
+ self.last = 0
+
+ def next(self):
+ if not self.intervals:
+ return (self.default, None)
+ last, index = self.intervals[0][0], self.intervals[0][2]
+ self.intervals[0][0] += self.intervals[0][1]
+ self.intervals.sort()
+ result = last - self.last
+ self.last = last
+ return result, index
+
+ def addInterval(self, i):
+ if self.intervals:
+ delay = self.intervals[0][0] - self.intervals[0][1]
+ self.intervals.append([delay + i, i, len(self.intervals)])
+ self.intervals.sort()
+ else:
+ self.intervals.append([i, i, 0])
+
+ def removeInterval(self, interval):
+ for i in range(len(self.intervals)):
+ if self.intervals[i][1] == interval:
+ index = self.intervals[i][2]
+ del self.intervals[i]
+ for i in self.intervals:
+ if i[2] > index:
+ i[2] -= 1
+ return
+ raise ValueError, "Specified interval not in IntervalDifferential"
+
+
+class FancyStrMixin:
+ """
+ Set showAttributes to a sequence of strings naming attributes, OR
+ sequences of C{(attributeName, displayName, formatCharacter)}.
+ """
+ showAttributes = ()
+ def __str__(self):
+ r = ['<', hasattr(self, 'fancybasename') and self.fancybasename or self.__class__.__name__]
+ for attr in self.showAttributes:
+ if isinstance(attr, str):
+ r.append(' %s=%r' % (attr, getattr(self, attr)))
+ else:
+ r.append((' %s=' + attr[2]) % (attr[1], getattr(self, attr[0])))
+ r.append('>')
+ return ''.join(r)
+ __repr__ = __str__
+
+
+
+class FancyEqMixin:
+ compareAttributes = ()
+ def __eq__(self, other):
+ if not self.compareAttributes:
+ return self is other
+ if isinstance(self, other.__class__):
+ return (
+ [getattr(self, name) for name in self.compareAttributes] ==
+ [getattr(other, name) for name in self.compareAttributes])
+ return NotImplemented
+
+
+ def __ne__(self, other):
+ result = self.__eq__(other)
+ if result is NotImplemented:
+ return result
+ return not result
+
+
+
+try:
+ from twisted.python._initgroups import initgroups as _c_initgroups
+except ImportError:
+ _c_initgroups = None
+
+
+
+if pwd is None or grp is None or setgroups is None or getgroups is None:
+ def initgroups(uid, primaryGid):
+ """
+ Do nothing.
+
+ Underlying platform support require to manipulate groups is missing.
+ """
+else:
+ # Fallback to the inefficient Python version
+ def _setgroups_until_success(l):
+ while(1):
+ # NASTY NASTY HACK (but glibc does it so it must be okay):
+ # In case sysconfig didn't give the right answer, find the limit
+ # on max groups by just looping, trying to set fewer and fewer
+ # groups each time until it succeeds.
+ try:
+ setgroups(l)
+ except ValueError:
+ # This exception comes from python itself restricting
+ # number of groups allowed.
+ if len(l) > 1:
+ del l[-1]
+ else:
+ raise
+ except OSError, e:
+ if e.errno == errno.EINVAL and len(l) > 1:
+ # This comes from the OS saying too many groups
+ del l[-1]
+ else:
+ raise
+ else:
+ # Success, yay!
+ return
+
+ def initgroups(uid, primaryGid):
+ """
+ Initializes the group access list.
+
+ If the C extension is present, we're calling it, which in turn calls
+ initgroups(3).
+
+ If not, this is done by reading the group database /etc/group and using
+ all groups of which C{uid} is a member. The additional group
+ C{primaryGid} is also added to the list.
+
+ If the given user is a member of more than C{NGROUPS}, arbitrary
+ groups will be silently discarded to bring the number below that
+ limit.
+
+ @type uid: C{int}
+ @param uid: The UID for which to look up group information.
+
+ @type primaryGid: C{int} or C{NoneType}
+ @param primaryGid: If provided, an additional GID to include when
+ setting the groups.
+ """
+ if _c_initgroups is not None:
+ return _c_initgroups(pwd.getpwuid(uid)[0], primaryGid)
+ try:
+ # Try to get the maximum number of groups
+ max_groups = os.sysconf("SC_NGROUPS_MAX")
+ except:
+ # No predefined limit
+ max_groups = 0
+
+ username = pwd.getpwuid(uid)[0]
+ l = []
+ if primaryGid is not None:
+ l.append(primaryGid)
+ for groupname, password, gid, userlist in grp.getgrall():
+ if username in userlist:
+ l.append(gid)
+ if len(l) == max_groups:
+ break # No more groups, ignore any more
+ try:
+ _setgroups_until_success(l)
+ except OSError, e:
+ # We might be able to remove this code now that we
+ # don't try to setgid/setuid even when not asked to.
+ if e.errno == errno.EPERM:
+ for g in getgroups():
+ if g not in l:
+ raise
+ else:
+ raise
+
+
+
+def switchUID(uid, gid, euid=False):
+ """
+ Attempts to switch the uid/euid and gid/egid for the current process.
+
+ If C{uid} is the same value as L{os.getuid} (or L{os.geteuid}),
+ this function will issue a L{UserWarning} and not raise an exception.
+
+ @type uid: C{int} or C{NoneType}
+ @param uid: the UID (or EUID) to switch the current process to. This
+ parameter will be ignored if the value is C{None}.
+
+ @type gid: C{int} or C{NoneType}
+ @param gid: the GID (or EGID) to switch the current process to. This
+ parameter will be ignored if the value is C{None}.
+
+ @type euid: C{bool}
+ @param euid: if True, set only effective user-id rather than real user-id.
+ (This option has no effect unless the process is running
+ as root, in which case it means not to shed all
+ privileges, retaining the option to regain privileges
+ in cases such as spawning processes. Use with caution.)
+ """
+ if euid:
+ setuid = os.seteuid
+ setgid = os.setegid
+ getuid = os.geteuid
+ else:
+ setuid = os.setuid
+ setgid = os.setgid
+ getuid = os.getuid
+ if gid is not None:
+ setgid(gid)
+ if uid is not None:
+ if uid == getuid():
+ uidText = (euid and "euid" or "uid")
+ actionText = "tried to drop privileges and set%s %s" % (uidText, uid)
+ problemText = "%s is already %s" % (uidText, getuid())
+ warnings.warn("%s but %s; should we be root? Continuing."
+ % (actionText, problemText))
+ else:
+ initgroups(uid, gid)
+ setuid(uid)
+
+
+class SubclassableCStringIO(object):
+ """
+ A wrapper around cStringIO to allow for subclassing.
+ """
+ __csio = None
+
+ def __init__(self, *a, **kw):
+ from cStringIO import StringIO
+ self.__csio = StringIO(*a, **kw)
+
+ def __iter__(self):
+ return self.__csio.__iter__()
+
+ def next(self):
+ return self.__csio.next()
+
+ def close(self):
+ return self.__csio.close()
+
+ def isatty(self):
+ return self.__csio.isatty()
+
+ def seek(self, pos, mode=0):
+ return self.__csio.seek(pos, mode)
+
+ def tell(self):
+ return self.__csio.tell()
+
+ def read(self, n=-1):
+ return self.__csio.read(n)
+
+ def readline(self, length=None):
+ return self.__csio.readline(length)
+
+ def readlines(self, sizehint=0):
+ return self.__csio.readlines(sizehint)
+
+ def truncate(self, size=None):
+ return self.__csio.truncate(size)
+
+ def write(self, s):
+ return self.__csio.write(s)
+
+ def writelines(self, list):
+ return self.__csio.writelines(list)
+
+ def flush(self):
+ return self.__csio.flush()
+
+ def getvalue(self):
+ return self.__csio.getvalue()
+
+
+
+def untilConcludes(f, *a, **kw):
+ while True:
+ try:
+ return f(*a, **kw)
+ except (IOError, OSError), e:
+ if e.args[0] == errno.EINTR:
+ continue
+ raise
+
+_idFunction = id
+
+def setIDFunction(idFunction):
+ """
+ Change the function used by L{unsignedID} to determine the integer id value
+ of an object. This is largely useful for testing to give L{unsignedID}
+ deterministic, easily-controlled behavior.
+
+ @param idFunction: A function with the signature of L{id}.
+ @return: The previous function being used by L{unsignedID}.
+ """
+ global _idFunction
+ oldIDFunction = _idFunction
+ _idFunction = idFunction
+ return oldIDFunction
+
+
+# A value about twice as large as any Python int, to which negative values
+# from id() will be added, moving them into a range which should begin just
+# above where positive values from id() leave off.
+_HUGEINT = (sys.maxint + 1L) * 2L
+def unsignedID(obj):
+ """
+ Return the id of an object as an unsigned number so that its hex
+ representation makes sense.
+
+ This is mostly necessary in Python 2.4 which implements L{id} to sometimes
+ return a negative value. Python 2.3 shares this behavior, but also
+ implements hex and the %x format specifier to represent negative values as
+ though they were positive ones, obscuring the behavior of L{id}. Python
+ 2.5's implementation of L{id} always returns positive values.
+ """
+ rval = _idFunction(obj)
+ if rval < 0:
+ rval += _HUGEINT
+ return rval
+
+
+def mergeFunctionMetadata(f, g):
+ """
+ Overwrite C{g}'s name and docstring with values from C{f}. Update
+ C{g}'s instance dictionary with C{f}'s.
+
+ To use this function safely you must use the return value. In Python 2.3,
+ L{mergeFunctionMetadata} will create a new function. In later versions of
+ Python, C{g} will be mutated and returned.
+
+ @return: A function that has C{g}'s behavior and metadata merged from
+ C{f}.
+ """
+ try:
+ g.__name__ = f.__name__
+ except TypeError:
+ try:
+ merged = types.FunctionType(
+ g.func_code, g.func_globals,
+ f.__name__, inspect.getargspec(g)[-1],
+ g.func_closure)
+ except TypeError:
+ pass
+ else:
+ merged = g
+ try:
+ merged.__doc__ = f.__doc__
+ except (TypeError, AttributeError):
+ pass
+ try:
+ merged.__dict__.update(g.__dict__)
+ merged.__dict__.update(f.__dict__)
+ except (TypeError, AttributeError):
+ pass
+ merged.__module__ = f.__module__
+ return merged
+
+
+def nameToLabel(mname):
+ """
+ Convert a string like a variable name into a slightly more human-friendly
+ string with spaces and capitalized letters.
+
+ @type mname: C{str}
+ @param mname: The name to convert to a label. This must be a string
+ which could be used as a Python identifier. Strings which do not take
+ this form will result in unpredictable behavior.
+
+ @rtype: C{str}
+ """
+ labelList = []
+ word = ''
+ lastWasUpper = False
+ for letter in mname:
+ if letter.isupper() == lastWasUpper:
+ # Continuing a word.
+ word += letter
+ else:
+ # breaking a word OR beginning a word
+ if lastWasUpper:
+ # could be either
+ if len(word) == 1:
+ # keep going
+ word += letter
+ else:
+ # acronym
+ # we're processing the lowercase letter after the acronym-then-capital
+ lastWord = word[:-1]
+ firstLetter = word[-1]
+ labelList.append(lastWord)
+ word = firstLetter + letter
+ else:
+ # definitely breaking: lower to upper
+ labelList.append(word)
+ word = letter
+ lastWasUpper = letter.isupper()
+ if labelList:
+ labelList[0] = labelList[0].capitalize()
+ else:
+ return mname.capitalize()
+ labelList.append(word)
+ return ' '.join(labelList)
+
+
+
+def uidFromString(uidString):
+ """
+ Convert a user identifier, as a string, into an integer UID.
+
+ @type uid: C{str}
+ @param uid: A string giving the base-ten representation of a UID or the
+ name of a user which can be converted to a UID via L{pwd.getpwnam}.
+
+ @rtype: C{int}
+ @return: The integer UID corresponding to the given string.
+
+ @raise ValueError: If the user name is supplied and L{pwd} is not
+ available.
+ """
+ try:
+ return int(uidString)
+ except ValueError:
+ if pwd is None:
+ raise
+ return pwd.getpwnam(uidString)[2]
+
+
+
+def gidFromString(gidString):
+ """
+ Convert a group identifier, as a string, into an integer GID.
+
+ @type uid: C{str}
+ @param uid: A string giving the base-ten representation of a GID or the
+ name of a group which can be converted to a GID via L{grp.getgrnam}.
+
+ @rtype: C{int}
+ @return: The integer GID corresponding to the given string.
+
+ @raise ValueError: If the group name is supplied and L{grp} is not
+ available.
+ """
+ try:
+ return int(gidString)
+ except ValueError:
+ if grp is None:
+ raise
+ return grp.getgrnam(gidString)[2]
+
+
+
+def runAsEffectiveUser(euid, egid, function, *args, **kwargs):
+ """
+ Run the given function wrapped with seteuid/setegid calls.
+
+ This will try to minimize the number of seteuid/setegid calls, comparing
+ current and wanted permissions
+
+ @param euid: effective UID used to call the function.
+ @type euid: C{int}
+
+ @type egid: effective GID used to call the function.
+ @param egid: C{int}
+
+ @param function: the function run with the specific permission.
+ @type function: any callable
+
+ @param *args: arguments passed to C{function}
+ @param **kwargs: keyword arguments passed to C{function}
+ """
+ uid, gid = os.geteuid(), os.getegid()
+ if uid == euid and gid == egid:
+ return function(*args, **kwargs)
+ else:
+ if uid != 0 and (uid != euid or gid != egid):
+ os.seteuid(0)
+ if gid != egid:
+ os.setegid(egid)
+ if euid != 0 and (euid != uid or gid != egid):
+ os.seteuid(euid)
+ try:
+ return function(*args, **kwargs)
+ finally:
+ if euid != 0 and (uid != euid or gid != egid):
+ os.seteuid(0)
+ if gid != egid:
+ os.setegid(gid)
+ if uid != 0 and (uid != euid or gid != egid):
+ os.seteuid(uid)
+
+
+
+__all__ = [
+ "uniquify", "padTo", "getPluginDirs", "addPluginDir", "sibpath",
+ "getPassword", "println", "makeStatBar", "OrderedDict",
+ "InsensitiveDict", "spewer", "searchupwards", "LineLog",
+ "raises", "IntervalDifferential", "FancyStrMixin", "FancyEqMixin",
+ "switchUID", "SubclassableCStringIO", "unsignedID", "mergeFunctionMetadata",
+ "nameToLabel", "uidFromString", "gidFromString", "runAsEffectiveUser",
+]
diff --git a/twisted/python/versions.py b/twisted/python/versions.py
new file mode 100644
index 0000000..d6f6715
--- /dev/null
+++ b/twisted/python/versions.py
@@ -0,0 +1,249 @@
+# -*- test-case-name: twisted.python.test.test_versions -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Versions for Python packages.
+
+See L{Version}.
+"""
+
+import sys, os
+
+
+class _inf(object):
+ """
+ An object that is bigger than all other objects.
+ """
+ def __cmp__(self, other):
+ """
+ @param other: Another object.
+ @type other: any
+
+ @return: 0 if other is inf, 1 otherwise.
+ @rtype: C{int}
+ """
+ if other is _inf:
+ return 0
+ return 1
+
+_inf = _inf()
+
+
+class IncomparableVersions(TypeError):
+ """
+ Two versions could not be compared.
+ """
+
+class Version(object):
+ """
+ An object that represents a three-part version number.
+
+ If running from an svn checkout, include the revision number in
+ the version string.
+ """
+ def __init__(self, package, major, minor, micro, prerelease=None):
+ """
+ @param package: Name of the package that this is a version of.
+ @type package: C{str}
+ @param major: The major version number.
+ @type major: C{int}
+ @param minor: The minor version number.
+ @type minor: C{int}
+ @param micro: The micro version number.
+ @type micro: C{int}
+ @param prerelease: The prerelease number.
+ @type prerelease: C{int}
+ """
+ self.package = package
+ self.major = major
+ self.minor = minor
+ self.micro = micro
+ self.prerelease = prerelease
+
+
+ def short(self):
+ """
+ Return a string in canonical short version format,
+ <major>.<minor>.<micro>[+rSVNVer].
+ """
+ s = self.base()
+ svnver = self._getSVNVersion()
+ if svnver:
+ s += '+r' + str(svnver)
+ return s
+
+
+ def base(self):
+ """
+ Like L{short}, but without the +rSVNVer.
+ """
+ if self.prerelease is None:
+ pre = ""
+ else:
+ pre = "pre%s" % (self.prerelease,)
+ return '%d.%d.%d%s' % (self.major,
+ self.minor,
+ self.micro,
+ pre)
+
+
+ def __repr__(self):
+ svnver = self._formatSVNVersion()
+ if svnver:
+ svnver = ' #' + svnver
+ if self.prerelease is None:
+ prerelease = ""
+ else:
+ prerelease = ", prerelease=%r" % (self.prerelease,)
+ return '%s(%r, %d, %d, %d%s)%s' % (
+ self.__class__.__name__,
+ self.package,
+ self.major,
+ self.minor,
+ self.micro,
+ prerelease,
+ svnver)
+
+
+ def __str__(self):
+ return '[%s, version %s]' % (
+ self.package,
+ self.short())
+
+
+ def __cmp__(self, other):
+ """
+ Compare two versions, considering major versions, minor versions, micro
+ versions, then prereleases.
+
+ A version with a prerelease is always less than a version without a
+ prerelease. If both versions have prereleases, they will be included in
+ the comparison.
+
+ @param other: Another version.
+ @type other: L{Version}
+
+ @return: NotImplemented when the other object is not a Version, or one
+ of -1, 0, or 1.
+
+ @raise IncomparableVersions: when the package names of the versions
+ differ.
+ """
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ if self.package != other.package:
+ raise IncomparableVersions("%r != %r"
+ % (self.package, other.package))
+
+ if self.prerelease is None:
+ prerelease = _inf
+ else:
+ prerelease = self.prerelease
+
+ if other.prerelease is None:
+ otherpre = _inf
+ else:
+ otherpre = other.prerelease
+
+ x = cmp((self.major,
+ self.minor,
+ self.micro,
+ prerelease),
+ (other.major,
+ other.minor,
+ other.micro,
+ otherpre))
+ return x
+
+
+ def _parseSVNEntries_4(self, entriesFile):
+ """
+ Given a readable file object which represents a .svn/entries file in
+ format version 4, return the revision as a string. We do this by
+ reading first XML element in the document that has a 'revision'
+ attribute.
+ """
+ from xml.dom.minidom import parse
+ doc = parse(entriesFile).documentElement
+ for node in doc.childNodes:
+ if hasattr(node, 'getAttribute'):
+ rev = node.getAttribute('revision')
+ if rev is not None:
+ return rev.encode('ascii')
+
+
+ def _parseSVNEntries_8(self, entriesFile):
+ """
+ Given a readable file object which represents a .svn/entries file in
+ format version 8, return the revision as a string.
+ """
+ entriesFile.readline()
+ entriesFile.readline()
+ entriesFile.readline()
+ return entriesFile.readline().strip()
+
+
+ # Add handlers for version 9 and 10 formats, which are the same as
+ # version 8 as far as revision information is concerned.
+ _parseSVNEntries_9 = _parseSVNEntries_8
+ _parseSVNEntriesTenPlus = _parseSVNEntries_8
+
+
+ def _getSVNVersion(self):
+ """
+ Figure out the SVN revision number based on the existance of
+ <package>/.svn/entries, and its contents. This requires discovering the
+ format version from the 'format' file and parsing the entries file
+ accordingly.
+
+ @return: None or string containing SVN Revision number.
+ """
+ mod = sys.modules.get(self.package)
+ if mod:
+ svn = os.path.join(os.path.dirname(mod.__file__), '.svn')
+ if not os.path.exists(svn):
+ # It's not an svn working copy
+ return None
+
+ formatFile = os.path.join(svn, 'format')
+ if os.path.exists(formatFile):
+ # It looks like a less-than-version-10 working copy.
+ format = file(formatFile).read().strip()
+ parser = getattr(self, '_parseSVNEntries_' + format, None)
+ else:
+ # It looks like a version-10-or-greater working copy, which
+ # has version information in the entries file.
+ parser = self._parseSVNEntriesTenPlus
+
+ if parser is None:
+ return 'Unknown'
+
+ entriesFile = os.path.join(svn, 'entries')
+ entries = file(entriesFile)
+ try:
+ try:
+ return parser(entries)
+ finally:
+ entries.close()
+ except:
+ return 'Unknown'
+
+
+ def _formatSVNVersion(self):
+ ver = self._getSVNVersion()
+ if ver is None:
+ return ''
+ return ' (SVN r%s)' % (ver,)
+
+
+
+def getVersionString(version):
+ """
+ Get a friendly string for the given version object.
+
+ @param version: A L{Version} object.
+ @return: A string containing the package and short version number.
+ """
+ result = '%s %s' % (version.package, version.short())
+ return result
diff --git a/twisted/python/win32.py b/twisted/python/win32.py
new file mode 100644
index 0000000..ca04fc0
--- /dev/null
+++ b/twisted/python/win32.py
@@ -0,0 +1,168 @@
+# -*- test-case-name: twisted.python.test.test_win32 -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Win32 utilities.
+
+See also twisted.python.shortcut.
+
+@var O_BINARY: the 'binary' mode flag on Windows, or 0 on other platforms, so it
+ may safely be OR'ed into a mask for os.open.
+"""
+
+import re
+import exceptions
+import os
+
+try:
+ import win32api
+ import win32con
+except ImportError:
+ pass
+
+from twisted.python.runtime import platform
+
+# http://msdn.microsoft.com/library/default.asp?url=/library/en-us/debug/base/system_error_codes.asp
+ERROR_FILE_NOT_FOUND = 2
+ERROR_PATH_NOT_FOUND = 3
+ERROR_INVALID_NAME = 123
+ERROR_DIRECTORY = 267
+
+O_BINARY = getattr(os, "O_BINARY", 0)
+
+def _determineWindowsError():
+ """
+ Determine which WindowsError name to export.
+ """
+ return getattr(exceptions, 'WindowsError', FakeWindowsError)
+
+class FakeWindowsError(OSError):
+ """
+ Stand-in for sometimes-builtin exception on platforms for which it
+ is missing.
+ """
+
+WindowsError = _determineWindowsError()
+
+# XXX fix this to use python's builtin _winreg?
+
+def getProgramsMenuPath():
+ """Get the path to the Programs menu.
+
+ Probably will break on non-US Windows.
+
+ @returns: the filesystem location of the common Start Menu->Programs.
+ """
+ if not platform.isWinNT():
+ return "C:\\Windows\\Start Menu\\Programs"
+ keyname = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders'
+ hShellFolders = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE,
+ keyname, 0, win32con.KEY_READ)
+ return win32api.RegQueryValueEx(hShellFolders, 'Common Programs')[0]
+
+
+def getProgramFilesPath():
+ """Get the path to the Program Files folder."""
+ keyname = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion'
+ currentV = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE,
+ keyname, 0, win32con.KEY_READ)
+ return win32api.RegQueryValueEx(currentV, 'ProgramFilesDir')[0]
+
+_cmdLineQuoteRe = re.compile(r'(\\*)"')
+_cmdLineQuoteRe2 = re.compile(r'(\\+)\Z')
+def cmdLineQuote(s):
+ """
+ Internal method for quoting a single command-line argument.
+
+ @param s: an unquoted string that you want to quote so that something that
+ does cmd.exe-style unquoting will interpret it as a single argument,
+ even if it contains spaces.
+ @type s: C{str}
+
+ @return: a quoted string.
+ @rtype: C{str}
+ """
+ quote = ((" " in s) or ("\t" in s) or ('"' in s) or s == '') and '"' or ''
+ return quote + _cmdLineQuoteRe2.sub(r"\1\1", _cmdLineQuoteRe.sub(r'\1\1\\"', s)) + quote
+
+def quoteArguments(arguments):
+ """
+ Quote an iterable of command-line arguments for passing to CreateProcess or
+ a similar API. This allows the list passed to C{reactor.spawnProcess} to
+ match the child process's C{sys.argv} properly.
+
+ @param arglist: an iterable of C{str}, each unquoted.
+
+ @return: a single string, with the given sequence quoted as necessary.
+ """
+ return ' '.join([cmdLineQuote(a) for a in arguments])
+
+
+class _ErrorFormatter(object):
+ """
+ Formatter for Windows error messages.
+
+ @ivar winError: A callable which takes one integer error number argument
+ and returns an L{exceptions.WindowsError} instance for that error (like
+ L{ctypes.WinError}).
+
+ @ivar formatMessage: A callable which takes one integer error number
+ argument and returns a C{str} giving the message for that error (like
+ L{win32api.FormatMessage}).
+
+ @ivar errorTab: A mapping from integer error numbers to C{str} messages
+ which correspond to those erorrs (like L{socket.errorTab}).
+ """
+ def __init__(self, WinError, FormatMessage, errorTab):
+ self.winError = WinError
+ self.formatMessage = FormatMessage
+ self.errorTab = errorTab
+
+ def fromEnvironment(cls):
+ """
+ Get as many of the platform-specific error translation objects as
+ possible and return an instance of C{cls} created with them.
+ """
+ try:
+ from ctypes import WinError
+ except ImportError:
+ WinError = None
+ try:
+ from win32api import FormatMessage
+ except ImportError:
+ FormatMessage = None
+ try:
+ from socket import errorTab
+ except ImportError:
+ errorTab = None
+ return cls(WinError, FormatMessage, errorTab)
+ fromEnvironment = classmethod(fromEnvironment)
+
+
+ def formatError(self, errorcode):
+ """
+ Returns the string associated with a Windows error message, such as the
+ ones found in socket.error.
+
+ Attempts direct lookup against the win32 API via ctypes and then
+ pywin32 if available), then in the error table in the socket module,
+ then finally defaulting to C{os.strerror}.
+
+ @param errorcode: the Windows error code
+ @type errorcode: C{int}
+
+ @return: The error message string
+ @rtype: C{str}
+ """
+ if self.winError is not None:
+ return self.winError(errorcode).strerror
+ if self.formatMessage is not None:
+ return self.formatMessage(errorcode)
+ if self.errorTab is not None:
+ result = self.errorTab.get(errorcode)
+ if result is not None:
+ return result
+ return os.strerror(errorcode)
+
+formatError = _ErrorFormatter.fromEnvironment().formatError
diff --git a/twisted/python/zippath.py b/twisted/python/zippath.py
new file mode 100644
index 0000000..a82f253
--- /dev/null
+++ b/twisted/python/zippath.py
@@ -0,0 +1,268 @@
+# -*- test-case-name: twisted.test.test_paths.ZipFilePathTestCase -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module contains implementations of IFilePath for zip files.
+
+See the constructor for ZipArchive for use.
+"""
+
+__metaclass__ = type
+
+import os
+import time
+import errno
+
+
+# Python 2.6 includes support for incremental unzipping of zipfiles, and
+# thus obviates the need for ChunkingZipFile.
+import sys
+if sys.version_info[:2] >= (2, 6):
+ _USE_ZIPFILE = True
+ from zipfile import ZipFile
+else:
+ _USE_ZIPFILE = False
+ from twisted.python.zipstream import ChunkingZipFile
+
+from twisted.python.filepath import IFilePath, FilePath, AbstractFilePath
+
+from zope.interface import implements
+
+# using FilePath here exclusively rather than os to make sure that we don't do
+# anything OS-path-specific here.
+
+ZIP_PATH_SEP = '/' # In zipfiles, "/" is universally used as the
+ # path separator, regardless of platform.
+
+
+class ZipPath(AbstractFilePath):
+ """
+ I represent a file or directory contained within a zip file.
+ """
+
+ implements(IFilePath)
+
+ sep = ZIP_PATH_SEP
+
+ def __init__(self, archive, pathInArchive):
+ """
+ Don't construct me directly. Use ZipArchive.child().
+
+ @param archive: a ZipArchive instance.
+
+ @param pathInArchive: a ZIP_PATH_SEP-separated string.
+ """
+ self.archive = archive
+ self.pathInArchive = pathInArchive
+ # self.path pretends to be os-specific because that's the way the
+ # 'zipimport' module does it.
+ self.path = os.path.join(archive.zipfile.filename,
+ *(self.pathInArchive.split(ZIP_PATH_SEP)))
+
+ def __cmp__(self, other):
+ if not isinstance(other, ZipPath):
+ return NotImplemented
+ return cmp((self.archive, self.pathInArchive),
+ (other.archive, other.pathInArchive))
+
+
+ def __repr__(self):
+ parts = [os.path.abspath(self.archive.path)]
+ parts.extend(self.pathInArchive.split(ZIP_PATH_SEP))
+ path = os.sep.join(parts)
+ return "ZipPath('%s')" % (path.encode('string-escape'),)
+
+
+ def parent(self):
+ splitup = self.pathInArchive.split(ZIP_PATH_SEP)
+ if len(splitup) == 1:
+ return self.archive
+ return ZipPath(self.archive, ZIP_PATH_SEP.join(splitup[:-1]))
+
+
+ def child(self, path):
+ """
+ Return a new ZipPath representing a path in C{self.archive} which is
+ a child of this path.
+
+ @note: Requesting the C{".."} (or other special name) child will not
+ cause L{InsecurePath} to be raised since these names do not have
+ any special meaning inside a zip archive. Be particularly
+ careful with the C{path} attribute (if you absolutely must use
+ it) as this means it may include special names with special
+ meaning outside of the context of a zip archive.
+ """
+ return ZipPath(self.archive, ZIP_PATH_SEP.join([self.pathInArchive, path]))
+
+
+ def sibling(self, path):
+ return self.parent().child(path)
+
+ # preauthChild = child
+
+ def exists(self):
+ return self.isdir() or self.isfile()
+
+ def isdir(self):
+ return self.pathInArchive in self.archive.childmap
+
+ def isfile(self):
+ return self.pathInArchive in self.archive.zipfile.NameToInfo
+
+ def islink(self):
+ return False
+
+ def listdir(self):
+ if self.exists():
+ if self.isdir():
+ return self.archive.childmap[self.pathInArchive].keys()
+ else:
+ raise OSError(errno.ENOTDIR, "Leaf zip entry listed")
+ else:
+ raise OSError(errno.ENOENT, "Non-existent zip entry listed")
+
+
+ def splitext(self):
+ """
+ Return a value similar to that returned by os.path.splitext.
+ """
+ # This happens to work out because of the fact that we use OS-specific
+ # path separators in the constructor to construct our fake 'path'
+ # attribute.
+ return os.path.splitext(self.path)
+
+
+ def basename(self):
+ return self.pathInArchive.split(ZIP_PATH_SEP)[-1]
+
+ def dirname(self):
+ # XXX NOTE: This API isn't a very good idea on filepath, but it's even
+ # less meaningful here.
+ return self.parent().path
+
+ def open(self, mode="r"):
+ if _USE_ZIPFILE:
+ return self.archive.zipfile.open(self.pathInArchive, mode=mode)
+ else:
+ # XXX oh man, is this too much hax?
+ self.archive.zipfile.mode = mode
+ return self.archive.zipfile.readfile(self.pathInArchive)
+
+ def changed(self):
+ pass
+
+ def getsize(self):
+ """
+ Retrieve this file's size.
+
+ @return: file size, in bytes
+ """
+
+ return self.archive.zipfile.NameToInfo[self.pathInArchive].file_size
+
+ def getAccessTime(self):
+ """
+ Retrieve this file's last access-time. This is the same as the last access
+ time for the archive.
+
+ @return: a number of seconds since the epoch
+ """
+ return self.archive.getAccessTime()
+
+
+ def getModificationTime(self):
+ """
+ Retrieve this file's last modification time. This is the time of
+ modification recorded in the zipfile.
+
+ @return: a number of seconds since the epoch.
+ """
+ return time.mktime(
+ self.archive.zipfile.NameToInfo[self.pathInArchive].date_time
+ + (0, 0, 0))
+
+
+ def getStatusChangeTime(self):
+ """
+ Retrieve this file's last modification time. This name is provided for
+ compatibility, and returns the same value as getmtime.
+
+ @return: a number of seconds since the epoch.
+ """
+ return self.getModificationTime()
+
+
+
+class ZipArchive(ZipPath):
+ """ I am a FilePath-like object which can wrap a zip archive as if it were a
+ directory.
+ """
+ archive = property(lambda self: self)
+ def __init__(self, archivePathname):
+ """Create a ZipArchive, treating the archive at archivePathname as a zip file.
+
+ @param archivePathname: a str, naming a path in the filesystem.
+ """
+ if _USE_ZIPFILE:
+ self.zipfile = ZipFile(archivePathname)
+ else:
+ self.zipfile = ChunkingZipFile(archivePathname)
+ self.path = archivePathname
+ self.pathInArchive = ''
+ # zipfile is already wasting O(N) memory on cached ZipInfo instances,
+ # so there's no sense in trying to do this lazily or intelligently
+ self.childmap = {} # map parent: list of children
+
+ for name in self.zipfile.namelist():
+ name = name.split(ZIP_PATH_SEP)
+ for x in range(len(name)):
+ child = name[-x]
+ parent = ZIP_PATH_SEP.join(name[:-x])
+ if parent not in self.childmap:
+ self.childmap[parent] = {}
+ self.childmap[parent][child] = 1
+ parent = ''
+
+ def child(self, path):
+ """
+ Create a ZipPath pointing at a path within the archive.
+
+ @param path: a str with no path separators in it, either '/' or the
+ system path separator, if it's different.
+ """
+ return ZipPath(self, path)
+
+ def exists(self):
+ """
+ Returns true if the underlying archive exists.
+ """
+ return FilePath(self.zipfile.filename).exists()
+
+
+ def getAccessTime(self):
+ """
+ Return the archive file's last access time.
+ """
+ return FilePath(self.zipfile.filename).getAccessTime()
+
+
+ def getModificationTime(self):
+ """
+ Return the archive file's modification time.
+ """
+ return FilePath(self.zipfile.filename).getModificationTime()
+
+
+ def getStatusChangeTime(self):
+ """
+ Return the archive file's status change time.
+ """
+ return FilePath(self.zipfile.filename).getStatusChangeTime()
+
+
+ def __repr__(self):
+ return 'ZipArchive(%r)' % (os.path.abspath(self.path),)
+
+
+__all__ = ['ZipArchive', 'ZipPath']
diff --git a/twisted/python/zipstream.py b/twisted/python/zipstream.py
new file mode 100644
index 0000000..97e2628
--- /dev/null
+++ b/twisted/python/zipstream.py
@@ -0,0 +1,387 @@
+# -*- test-case-name: twisted.python.test.test_zipstream -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An incremental approach to unzipping files. This allows you to unzip a little
+bit of a file at a time, which means you can report progress as a file unzips.
+"""
+
+import warnings
+import zipfile
+import os.path
+import zlib
+import struct
+
+_fileHeaderSize = struct.calcsize(zipfile.structFileHeader)
+
+class ChunkingZipFile(zipfile.ZipFile):
+ """
+ A ZipFile object which, with readfile(), also gives you access to a
+ filelike object for each entry.
+ """
+
+ def readfile(self, name):
+ """
+ Return file-like object for name.
+ """
+ if self.mode not in ("r", "a"):
+ raise RuntimeError('read() requires mode "r" or "a"')
+ if not self.fp:
+ raise RuntimeError(
+ "Attempt to read ZIP archive that was already closed")
+ zinfo = self.getinfo(name)
+
+ self.fp.seek(zinfo.header_offset, 0)
+
+ fheader = self.fp.read(_fileHeaderSize)
+ if fheader[0:4] != zipfile.stringFileHeader:
+ raise zipfile.BadZipfile("Bad magic number for file header")
+
+ fheader = struct.unpack(zipfile.structFileHeader, fheader)
+ fname = self.fp.read(fheader[zipfile._FH_FILENAME_LENGTH])
+
+ if fheader[zipfile._FH_EXTRA_FIELD_LENGTH]:
+ self.fp.read(fheader[zipfile._FH_EXTRA_FIELD_LENGTH])
+
+ if fname != zinfo.orig_filename:
+ raise zipfile.BadZipfile(
+ 'File name in directory "%s" and header "%s" differ.' % (
+ zinfo.orig_filename, fname))
+
+ if zinfo.compress_type == zipfile.ZIP_STORED:
+ return ZipFileEntry(self, zinfo.compress_size)
+ elif zinfo.compress_type == zipfile.ZIP_DEFLATED:
+ return DeflatedZipFileEntry(self, zinfo.compress_size)
+ else:
+ raise zipfile.BadZipfile(
+ "Unsupported compression method %d for file %s" %
+ (zinfo.compress_type, name))
+
+
+
+class _FileEntry(object):
+ """
+ Abstract superclass of both compressed and uncompressed variants of
+ file-like objects within a zip archive.
+
+ @ivar chunkingZipFile: a chunking zip file.
+ @type chunkingZipFile: L{ChunkingZipFile}
+
+ @ivar length: The number of bytes within the zip file that represent this
+ file. (This is the size on disk, not the number of decompressed bytes
+ which will result from reading it.)
+
+ @ivar fp: the underlying file object (that contains pkzip data). Do not
+ touch this, please. It will quite likely move or go away.
+
+ @ivar closed: File-like 'closed' attribute; True before this file has been
+ closed, False after.
+ @type closed: L{bool}
+
+ @ivar finished: An older, broken synonym for 'closed'. Do not touch this,
+ please.
+ @type finished: L{int}
+ """
+ def __init__(self, chunkingZipFile, length):
+ """
+ Create a L{_FileEntry} from a L{ChunkingZipFile}.
+ """
+ self.chunkingZipFile = chunkingZipFile
+ self.fp = self.chunkingZipFile.fp
+ self.length = length
+ self.finished = 0
+ self.closed = False
+
+
+ def isatty(self):
+ """
+ Returns false because zip files should not be ttys
+ """
+ return False
+
+
+ def close(self):
+ """
+ Close self (file-like object)
+ """
+ self.closed = True
+ self.finished = 1
+ del self.fp
+
+
+ def readline(self):
+ """
+ Read a line.
+ """
+ bytes = ""
+ for byte in iter(lambda : self.read(1), ""):
+ bytes += byte
+ if byte == "\n":
+ break
+ return bytes
+
+
+ def next(self):
+ """
+ Implement next as file does (like readline, except raises StopIteration
+ at EOF)
+ """
+ nextline = self.readline()
+ if nextline:
+ return nextline
+ raise StopIteration()
+
+
+ def readlines(self):
+ """
+ Returns a list of all the lines
+ """
+ return list(self)
+
+
+ def xreadlines(self):
+ """
+ Returns an iterator (so self)
+ """
+ return self
+
+
+ def __iter__(self):
+ """
+ Returns an iterator (so self)
+ """
+ return self
+
+
+
+class ZipFileEntry(_FileEntry):
+ """
+ File-like object used to read an uncompressed entry in a ZipFile
+ """
+
+ def __init__(self, chunkingZipFile, length):
+ _FileEntry.__init__(self, chunkingZipFile, length)
+ self.readBytes = 0
+
+
+ def tell(self):
+ return self.readBytes
+
+
+ def read(self, n=None):
+ if n is None:
+ n = self.length - self.readBytes
+ if n == 0 or self.finished:
+ return ''
+ data = self.chunkingZipFile.fp.read(
+ min(n, self.length - self.readBytes))
+ self.readBytes += len(data)
+ if self.readBytes == self.length or len(data) < n:
+ self.finished = 1
+ return data
+
+
+
+class DeflatedZipFileEntry(_FileEntry):
+ """
+ File-like object used to read a deflated entry in a ZipFile
+ """
+
+ def __init__(self, chunkingZipFile, length):
+ _FileEntry.__init__(self, chunkingZipFile, length)
+ self.returnedBytes = 0
+ self.readBytes = 0
+ self.decomp = zlib.decompressobj(-15)
+ self.buffer = ""
+
+
+ def tell(self):
+ return self.returnedBytes
+
+
+ def read(self, n=None):
+ if self.finished:
+ return ""
+ if n is None:
+ result = [self.buffer,]
+ result.append(
+ self.decomp.decompress(
+ self.chunkingZipFile.fp.read(
+ self.length - self.readBytes)))
+ result.append(self.decomp.decompress("Z"))
+ result.append(self.decomp.flush())
+ self.buffer = ""
+ self.finished = 1
+ result = "".join(result)
+ self.returnedBytes += len(result)
+ return result
+ else:
+ while len(self.buffer) < n:
+ data = self.chunkingZipFile.fp.read(
+ min(n, 1024, self.length - self.readBytes))
+ self.readBytes += len(data)
+ if not data:
+ result = (self.buffer
+ + self.decomp.decompress("Z")
+ + self.decomp.flush())
+ self.finished = 1
+ self.buffer = ""
+ self.returnedBytes += len(result)
+ return result
+ else:
+ self.buffer += self.decomp.decompress(data)
+ result = self.buffer[:n]
+ self.buffer = self.buffer[n:]
+ self.returnedBytes += len(result)
+ return result
+
+
+
+def unzip(filename, directory=".", overwrite=0):
+ """
+ Unzip the file
+
+ @param filename: the name of the zip file
+ @param directory: the directory into which the files will be
+ extracted
+ @param overwrite: if on, overwrite files when they exist. You can
+ still get an error if you try to create a directory over a file
+ with the same name or vice-versa.
+ """
+ warnings.warn("zipstream.unzip is deprecated since Twisted 11.0.0 for " +
+ "security reasons. Use Python's zipfile instead.",
+ category=DeprecationWarning, stacklevel=2)
+
+ for i in unzipIter(filename, directory, overwrite, suppressWarning=True):
+ pass
+
+DIR_BIT = 16
+
+def unzipIter(filename, directory='.', overwrite=0, suppressWarning=False):
+ """
+ Return a generator for the zipfile. This implementation will yield
+ after every file.
+
+ The value it yields is the number of files left to unzip.
+ """
+ if not suppressWarning:
+ warnings.warn("zipstream.unzipIter is deprecated since Twisted " +
+ "11.0.0 for security reasons. Use Python's " +
+ "zipfile instead.",
+ category=DeprecationWarning, stacklevel=2)
+
+ zf = zipfile.ZipFile(filename, 'r')
+ names = zf.namelist()
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ remaining = len(zf.namelist())
+ for entry in names:
+ remaining -= 1
+ isdir = zf.getinfo(entry).external_attr & DIR_BIT
+ f = os.path.join(directory, entry)
+ if isdir:
+ # overwrite flag only applies to files
+ if not os.path.exists(f):
+ os.makedirs(f)
+ else:
+ # create the directory the file will be in first,
+ # since we can't guarantee it exists
+ fdir = os.path.split(f)[0]
+ if not os.path.exists(fdir):
+ os.makedirs(fdir)
+ if overwrite or not os.path.exists(f):
+ outfile = file(f, 'wb')
+ outfile.write(zf.read(entry))
+ outfile.close()
+ yield remaining
+
+
+def countZipFileChunks(filename, chunksize):
+ """
+ Predict the number of chunks that will be extracted from the entire
+ zipfile, given chunksize blocks.
+ """
+ totalchunks = 0
+ zf = ChunkingZipFile(filename)
+ for info in zf.infolist():
+ totalchunks += countFileChunks(info, chunksize)
+ return totalchunks
+
+
+def countFileChunks(zipinfo, chunksize):
+ """
+ Count the number of chunks that will result from the given L{ZipInfo}.
+
+ @param zipinfo: a L{zipfile.ZipInfo} instance describing an entry in a zip
+ archive to be counted.
+
+ @return: the number of chunks present in the zip file. (Even an empty file
+ counts as one chunk.)
+ @rtype: L{int}
+ """
+ count, extra = divmod(zipinfo.file_size, chunksize)
+ if extra > 0:
+ count += 1
+ return count or 1
+
+
+def countZipFileEntries(filename):
+ """
+ Count the number of entries in a zip archive. (Don't use this function.)
+
+ @param filename: The filename of a zip archive.
+ @type filename: L{str}
+ """
+ warnings.warn("countZipFileEntries is deprecated.",
+ DeprecationWarning, 2)
+ zf = zipfile.ZipFile(filename)
+ return len(zf.namelist())
+
+
+def unzipIterChunky(filename, directory='.', overwrite=0,
+ chunksize=4096):
+ """
+ Return a generator for the zipfile. This implementation will yield after
+ every chunksize uncompressed bytes, or at the end of a file, whichever
+ comes first.
+
+ The value it yields is the number of chunks left to unzip.
+ """
+ czf = ChunkingZipFile(filename, 'r')
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ remaining = countZipFileChunks(filename, chunksize)
+ names = czf.namelist()
+ infos = czf.infolist()
+
+ for entry, info in zip(names, infos):
+ isdir = info.external_attr & DIR_BIT
+ f = os.path.join(directory, entry)
+ if isdir:
+ # overwrite flag only applies to files
+ if not os.path.exists(f):
+ os.makedirs(f)
+ remaining -= 1
+ yield remaining
+ else:
+ # create the directory the file will be in first,
+ # since we can't guarantee it exists
+ fdir = os.path.split(f)[0]
+ if not os.path.exists(fdir):
+ os.makedirs(fdir)
+ if overwrite or not os.path.exists(f):
+ outfile = file(f, 'wb')
+ fp = czf.readfile(entry)
+ if info.file_size == 0:
+ remaining -= 1
+ yield remaining
+ while fp.tell() < info.file_size:
+ hunk = fp.read(chunksize)
+ outfile.write(hunk)
+ remaining -= 1
+ yield remaining
+ outfile.close()
+ else:
+ remaining -= countFileChunks(info, chunksize)
+ yield remaining
diff --git a/twisted/python/zsh/README.txt b/twisted/python/zsh/README.txt
new file mode 100644
index 0000000..2a7b1c2
--- /dev/null
+++ b/twisted/python/zsh/README.txt
@@ -0,0 +1,9 @@
+THIS DIRECTORY AND ALL FILES INCLUDED ARE DEPRECATED.
+
+These are the old zsh completion functions for Twisted commands... they used
+to contain full completion functions, but now they've simply been replaced
+by the current "stub" code that delegates completion control to Twisted.
+
+This directory and included files need to remain for several years in order
+to provide backwards-compatibility with an old version of the Twisted
+stub function that was shipped with Zsh.
diff --git a/twisted/python/zsh/_cftp b/twisted/python/zsh/_cftp
new file mode 100644
index 0000000..e89fcdb
--- /dev/null
+++ b/twisted/python/zsh/_cftp
@@ -0,0 +1,34 @@
+#compdef cftp
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_ckeygen b/twisted/python/zsh/_ckeygen
new file mode 100644
index 0000000..38050a0
--- /dev/null
+++ b/twisted/python/zsh/_ckeygen
@@ -0,0 +1,34 @@
+#compdef ckeygen
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_conch b/twisted/python/zsh/_conch
new file mode 100644
index 0000000..e3ac3b6
--- /dev/null
+++ b/twisted/python/zsh/_conch
@@ -0,0 +1,34 @@
+#compdef conch
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_lore b/twisted/python/zsh/_lore
new file mode 100644
index 0000000..8b1c328
--- /dev/null
+++ b/twisted/python/zsh/_lore
@@ -0,0 +1,34 @@
+#compdef lore
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_manhole b/twisted/python/zsh/_manhole
new file mode 100644
index 0000000..54ec99f
--- /dev/null
+++ b/twisted/python/zsh/_manhole
@@ -0,0 +1,34 @@
+#compdef manhole
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_mktap b/twisted/python/zsh/_mktap
new file mode 100644
index 0000000..2a08ea4
--- /dev/null
+++ b/twisted/python/zsh/_mktap
@@ -0,0 +1,34 @@
+#compdef mktap
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_pyhtmlizer b/twisted/python/zsh/_pyhtmlizer
new file mode 100644
index 0000000..2fd2d6d
--- /dev/null
+++ b/twisted/python/zsh/_pyhtmlizer
@@ -0,0 +1,34 @@
+#compdef pyhtmlizer
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_tap2deb b/twisted/python/zsh/_tap2deb
new file mode 100644
index 0000000..b4e0836
--- /dev/null
+++ b/twisted/python/zsh/_tap2deb
@@ -0,0 +1,34 @@
+#compdef tap2deb
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_tap2rpm b/twisted/python/zsh/_tap2rpm
new file mode 100644
index 0000000..10a083f
--- /dev/null
+++ b/twisted/python/zsh/_tap2rpm
@@ -0,0 +1,34 @@
+#compdef tap2rpm
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_tapconvert b/twisted/python/zsh/_tapconvert
new file mode 100644
index 0000000..41a0e4d
--- /dev/null
+++ b/twisted/python/zsh/_tapconvert
@@ -0,0 +1,34 @@
+#compdef tapconvert
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_tkconch b/twisted/python/zsh/_tkconch
new file mode 100644
index 0000000..3af1f12
--- /dev/null
+++ b/twisted/python/zsh/_tkconch
@@ -0,0 +1,34 @@
+#compdef tkconch
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_tkmktap b/twisted/python/zsh/_tkmktap
new file mode 100644
index 0000000..0e3bdaa
--- /dev/null
+++ b/twisted/python/zsh/_tkmktap
@@ -0,0 +1,34 @@
+#compdef tkmktap
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_trial b/twisted/python/zsh/_trial
new file mode 100644
index 0000000..b692f44
--- /dev/null
+++ b/twisted/python/zsh/_trial
@@ -0,0 +1,34 @@
+#compdef trial
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_twistd b/twisted/python/zsh/_twistd
new file mode 100644
index 0000000..171224f
--- /dev/null
+++ b/twisted/python/zsh/_twistd
@@ -0,0 +1,34 @@
+#compdef twistd
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zsh/_websetroot b/twisted/python/zsh/_websetroot
new file mode 100644
index 0000000..58ae550
--- /dev/null
+++ b/twisted/python/zsh/_websetroot
@@ -0,0 +1,34 @@
+#compdef websetroot
+# This file is deprecated. See README.
+
+# This is the ZSH completion file for Twisted commands. It calls the current
+# command-line with the special "--_shell-completion" option which is handled
+# by twisted.python.usage. t.p.usage then generates zsh code on stdout to
+# handle the completions for this particular command-line.
+#
+# 3rd parties that wish to provide zsh completion for commands that
+# use t.p.usage may copy this file and change the first line to reference
+# the name(s) of their command(s).
+#
+# This file is included in the official Zsh distribution as
+# Completion/Unix/Command/_twisted
+
+# redirect stderr to /dev/null otherwise deprecation warnings may get puked
+# all over the user's terminal if completing options for mktap or other
+# deprecated commands. Redirect stderr to a file to debug errors.
+local cmd output
+cmd=("$words[@]" --_shell-completion zsh:$CURRENT)
+output=$("$cmd[@]" 2>/dev/null)
+
+if [[ $output == "#compdef "* ]]; then
+ # Looks like we got a valid completion function - so eval it to produce
+ # the completion matches.
+ eval $output
+else
+ echo "\nCompletion error running command:" ${(qqq)cmd}
+ echo -n "If output below is unhelpful you may need to edit this file and "
+ echo "redirect stderr to a file."
+ echo "Expected completion function, but instead got:"
+ echo $output
+ return 1
+fi
diff --git a/twisted/python/zshcomp.py b/twisted/python/zshcomp.py
new file mode 100644
index 0000000..89389fe
--- /dev/null
+++ b/twisted/python/zshcomp.py
@@ -0,0 +1,824 @@
+# -*- test-case-name: twisted.python.test.test_zshcomp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+"""
+Rebuild the completion functions for the currently active version of Twisted::
+ $ python zshcomp.py -i
+
+This module implements a zsh code generator which generates completion code for
+commands that use twisted.python.usage. This is the stuff that makes pressing
+Tab at the command line work.
+
+Maintainer: Eric Mangold
+
+To build completion functions for your own commands, and not Twisted commands,
+then just do something like this::
+
+ o = mymodule.MyOptions()
+ f = file('_mycommand', 'w')
+ Builder("mycommand", o, f).write()
+
+Then all you have to do is place the generated file somewhere in your
+C{$fpath}, and restart zsh. Note the "site-functions" directory in your
+C{$fpath} where you may install 3rd-party completion functions (like the one
+you're building). Call C{siteFunctionsPath} to locate this directory
+programmatically.
+
+SPECIAL CLASS VARIABLES. You may set these on your usage.Options subclass::
+
+ zsh_altArgDescr
+ zsh_multiUse
+ zsh_mutuallyExclusive
+ zsh_actions
+ zsh_actionDescr
+ zsh_extras
+
+Here is what they mean (with examples)::
+
+ zsh_altArgDescr = {"foo":"use this description for foo instead"}
+ A dict mapping long option names to alternate descriptions. When this
+ variable is present, the descriptions contained here will override
+ those descriptions provided in the optFlags and optParameters
+ variables.
+
+ zsh_multiUse = ["foo", "bar"]
+ A sequence containing those long option names which may appear on the
+ command line more than once. By default, options will only be completed
+ one time.
+
+ zsh_mutuallyExclusive = [("foo", "bar"), ("bar", "baz")]
+ A sequence of sequences, with each sub-sequence containing those long
+ option names that are mutually exclusive. That is, those options that
+ cannot appear on the command line together.
+
+ zsh_actions = {"foo":'_files -g "*.foo"', "bar":"(one two three)",
+ "colors":"_values -s , 'colors to use' red green blue"}
+ A dict mapping long option names to Zsh "actions". These actions
+ define what will be completed as the argument to the given option. By
+ default, all files/dirs will be completed if no action is given.
+
+ Callables may instead be given for the values in this dict. The
+ callable should accept no arguments, and return a string that will be
+ used as the zsh "action" in the same way as the literal strings in the
+ examples above.
+
+ As you can see in the example above. The "foo" option will have files
+ that end in .foo completed when the user presses Tab. The "bar"
+ option will have either of the strings "one", "two", or "three"
+ completed when the user presses Tab.
+
+ "colors" will allow multiple arguments to be completed, seperated by
+ commas. The possible arguments are red, green, and blue. Examples::
+
+ my_command --foo some-file.foo --colors=red,green
+ my_command --colors=green
+ my_command --colors=green,blue
+
+ Actions may take many forms, and it is beyond the scope of this
+ document to illustrate them all. Please refer to the documention for
+ the Zsh _arguments function. zshcomp is basically a front-end to Zsh's
+ _arguments completion function.
+
+ That documentation is available on the zsh web site at this URL:
+ U{http://zsh.sunsite.dk/Doc/Release/zsh_19.html#SEC124}
+
+ zsh_actionDescr = {"logfile":"log file name", "random":"random seed"}
+ A dict mapping long option names to a description for the corresponding
+ zsh "action". These descriptions are show above the generated matches
+ when the user is doing completions for this option.
+
+ Normally Zsh does not show these descriptions unless you have
+ "verbose" completion turned on. Turn on verbosity with this in your
+ ~/.zshrc::
+
+ zstyle ':completion:*' verbose yes
+ zstyle ':completion:*:descriptions' format '%B%d%b'
+
+ zsh_extras = [":file to read from:action", ":file to write to:action"]
+ A sequence of extra arguments that will be passed verbatim to Zsh's
+ _arguments completion function. The _arguments function does all the
+ hard work of doing command line completions. You can see how zshcomp
+ invokes the _arguments call by looking at the generated completion
+ files that this module creates.
+
+ *** NOTE ***
+
+ You will need to use this variable to describe completions for normal
+ command line arguments. That is, those arguments that are not
+ associated with an option. That is, the arguments that are given to the
+ parseArgs method of your usage.Options subclass.
+
+ In the example above, the 1st non-option argument will be described as
+ "file to read from" and completion options will be generated in
+ accordance with the "action". (See above about zsh "actions") The
+ 2nd non-option argument will be described as "file to write to" and
+ the action will be interpreted likewise.
+
+ Things you can put here are all documented under the _arguments
+ function here: U{http://zsh.sunsite.dk/Doc/Release/zsh_19.html#SEC124}
+
+Zsh Notes:
+
+To enable advanced completion add something like this to your ~/.zshrc::
+
+ autoload -U compinit
+ compinit
+
+For some extra verbosity, and general niceness add these lines too::
+
+ zstyle ':completion:*' verbose yes
+ zstyle ':completion:*:descriptions' format '%B%d%b'
+ zstyle ':completion:*:messages' format '%d'
+ zstyle ':completion:*:warnings' format 'No matches for: %d'
+
+Have fun!
+"""
+import warnings
+warnings.warn(
+ "zshcomp is deprecated as of Twisted 11.1. Shell tab-completion is now "
+ "handled by twisted.python.usage.", DeprecationWarning, stacklevel=2)
+
+import itertools, sys, commands, os.path
+
+from twisted.python import reflect, util, usage
+from twisted.application.service import IServiceMaker
+
+
+
+class MyOptions(usage.Options):
+ """
+ Options for this file
+ """
+ longdesc = ""
+ synopsis = "Usage: python zshcomp.py [--install | -i] | <output directory>"
+ optFlags = [["install", "i",
+ 'Output files to the "installation" directory ' \
+ '(twisted/python/zsh in the currently active ' \
+ 'Twisted package)']]
+ optParameters = [["directory", "d", None,
+ "Output files to this directory"]]
+
+
+ def postOptions(self):
+ if self['install'] and self['directory']:
+ raise usage.UsageError, "Can't have --install and " \
+ "--directory at the same time"
+ if not self['install'] and not self['directory']:
+ raise usage.UsageError, "Not enough arguments"
+ if self['directory'] and not os.path.isdir(self['directory']):
+ raise usage.UsageError, "%s is not a directory" % self['directory']
+
+
+
+class Builder:
+ def __init__(self, cmd_name, options, file):
+ """
+ @type cmd_name: C{str}
+ @param cmd_name: The name of the command
+
+ @type options: C{twisted.usage.Options}
+ @param options: The C{twisted.usage.Options} instance defined for
+ this command
+
+ @type file: C{file}
+ @param file: The C{file} to write the completion function to
+ """
+
+ self.cmd_name = cmd_name
+ self.options = options
+ self.file = file
+
+
+ def write(self):
+ """
+ Write the completion function to the file given to __init__
+ @return: C{None}
+ """
+ # by default, we just write out a single call to _arguments
+ self.file.write('#compdef %s\n' % (self.cmd_name,))
+ gen = ArgumentsGenerator(self.cmd_name, self.options, self.file)
+ gen.write()
+
+
+
+class SubcommandBuilder(Builder):
+ """
+ Use this builder for commands that have sub-commands. twisted.python.usage
+ has the notion of sub-commands that are defined using an entirely seperate
+ Options class.
+ """
+ interface = None
+ subcmdLabel = None
+
+
+ def write(self):
+ """
+ Write the completion function to the file given to __init__
+ @return: C{None}
+ """
+ self.file.write('#compdef %s\n' % (self.cmd_name,))
+ self.file.write('local _zsh_subcmds_array\n_zsh_subcmds_array=(\n')
+ from twisted import plugin as newplugin
+ plugins = newplugin.getPlugins(self.interface)
+
+ for p in plugins:
+ self.file.write('"%s:%s"\n' % (p.tapname, p.description))
+ self.file.write(")\n\n")
+
+ self.options.__class__.zsh_extras = ['*::subcmd:->subcmd']
+ gen = ArgumentsGenerator(self.cmd_name, self.options, self.file)
+ gen.write()
+
+ self.file.write("""if (( CURRENT == 1 )); then
+ _describe "%s" _zsh_subcmds_array && ret=0
+fi
+(( ret )) || return 0
+
+service="$words[1]"
+
+case $service in\n""" % (self.subcmdLabel,))
+
+ plugins = newplugin.getPlugins(self.interface)
+ for p in plugins:
+ self.file.write(p.tapname + ")\n")
+ gen = ArgumentsGenerator(p.tapname, p.options(), self.file)
+ gen.write()
+ self.file.write(";;\n")
+ self.file.write("*) _message \"don't know how to" \
+ " complete $service\";;\nesac")
+
+
+
+class MktapBuilder(SubcommandBuilder):
+ """
+ Builder for the mktap command
+ """
+ interface = IServiceMaker
+ subcmdLabel = 'tap to build'
+
+
+
+class TwistdBuilder(SubcommandBuilder):
+ """
+ Builder for the twistd command
+ """
+ interface = IServiceMaker
+ subcmdLabel = 'service to run'
+
+
+
+class ArgumentsGenerator:
+ """
+ Generate a call to the zsh _arguments completion function
+ based on data in a usage.Options subclass
+ """
+ def __init__(self, cmd_name, options, file):
+ """
+ @type cmd_name: C{str}
+ @param cmd_name: The name of the command
+
+ @type options: C{twisted.usage.Options}
+ @param options: The C{twisted.usage.Options} instance defined
+ for this command
+
+ @type file: C{file}
+ @param file: The C{file} to write the completion function to
+ """
+ self.cmd_name = cmd_name
+ self.options = options
+ self.file = file
+
+ self.altArgDescr = {}
+ self.actionDescr = {}
+ self.multiUse = []
+ self.mutuallyExclusive = []
+ self.actions = {}
+ self.extras = []
+
+ aCL = reflect.accumulateClassList
+ aCD = reflect.accumulateClassDict
+
+ aCD(options.__class__, 'zsh_altArgDescr', self.altArgDescr)
+ aCD(options.__class__, 'zsh_actionDescr', self.actionDescr)
+ aCL(options.__class__, 'zsh_multiUse', self.multiUse)
+ aCL(options.__class__, 'zsh_mutuallyExclusive',
+ self.mutuallyExclusive)
+ aCD(options.__class__, 'zsh_actions', self.actions)
+ aCL(options.__class__, 'zsh_extras', self.extras)
+
+ optFlags = []
+ optParams = []
+
+ aCL(options.__class__, 'optFlags', optFlags)
+ aCL(options.__class__, 'optParameters', optParams)
+
+ for i, optList in enumerate(optFlags):
+ if len(optList) != 3:
+ optFlags[i] = util.padTo(3, optList)
+
+ for i, optList in enumerate(optParams):
+ if len(optList) != 4:
+ optParams[i] = util.padTo(4, optList)
+
+
+ self.optFlags = optFlags
+ self.optParams = optParams
+
+ optParams_d = {}
+ for optList in optParams:
+ optParams_d[optList[0]] = optList[1:]
+ self.optParams_d = optParams_d
+
+ optFlags_d = {}
+ for optList in optFlags:
+ optFlags_d[optList[0]] = optList[1:]
+ self.optFlags_d = optFlags_d
+
+ optAll_d = {}
+ optAll_d.update(optParams_d)
+ optAll_d.update(optFlags_d)
+ self.optAll_d = optAll_d
+
+ self.addAdditionalOptions()
+
+ # makes sure none of the zsh_ data structures reference option
+ # names that don't exist. (great for catching typos)
+ self.verifyZshNames()
+
+ self.excludes = self.makeExcludesDict()
+
+
+ def write(self):
+ """
+ Write the zsh completion code to the file given to __init__
+ @return: C{None}
+ """
+ self.writeHeader()
+ self.writeExtras()
+ self.writeOptions()
+ self.writeFooter()
+
+
+ def writeHeader(self):
+ """
+ This is the start of the code that calls _arguments
+ @return: C{None}
+ """
+ self.file.write('_arguments -s -A "-*" \\\n')
+
+
+ def writeOptions(self):
+ """
+ Write out zsh code for each option in this command
+ @return: C{None}
+ """
+ optNames = self.optAll_d.keys()
+ optNames.sort()
+ for longname in optNames:
+ self.writeOpt(longname)
+
+
+ def writeExtras(self):
+ """
+ Write out the "extras" list. These are just passed verbatim to the
+ _arguments call
+ @return: C{None}
+ """
+ for s in self.extras:
+ self.file.write(escape(s))
+ self.file.write(' \\\n')
+
+
+ def writeFooter(self):
+ """
+ Write the last bit of code that finishes the call to _arguments
+ @return: C{None}
+ """
+ self.file.write('&& return 0\n')
+
+
+ def verifyZshNames(self):
+ """
+ Ensure that none of the names given in zsh_* variables are typoed
+ @return: C{None}
+ @raise ValueError: Raised if unknown option names have been given in
+ zsh_* variables
+ """
+ def err(name):
+ raise ValueError, "Unknown option name \"%s\" found while\n" \
+ "examining zsh_ attributes for the %s command" % (
+ name, self.cmd_name)
+
+ for name in itertools.chain(self.altArgDescr, self.actionDescr,
+ self.actions, self.multiUse):
+ if name not in self.optAll_d:
+ err(name)
+
+ for seq in self.mutuallyExclusive:
+ for name in seq:
+ if name not in self.optAll_d:
+ err(name)
+
+
+ def excludeStr(self, longname, buildShort=False):
+ """
+ Generate an "exclusion string" for the given option
+
+ @type longname: C{str}
+ @param longname: The long name of the option
+ (i.e. "verbose" instead of "v")
+
+ @type buildShort: C{bool}
+ @param buildShort: May be True to indicate we're building an excludes
+ string for the short option that correspondes to
+ the given long opt
+
+ @return: The generated C{str}
+ """
+ if longname in self.excludes:
+ exclusions = self.excludes[longname][:]
+ else:
+ exclusions = []
+
+ # if longname isn't a multiUse option (can't appear on the cmd line more
+ # than once), then we have to exclude the short option if we're
+ # building for the long option, and vice versa.
+ if longname not in self.multiUse:
+ if buildShort is False:
+ short = self.getShortOption(longname)
+ if short is not None:
+ exclusions.append(short)
+ else:
+ exclusions.append(longname)
+
+ if not exclusions:
+ return ''
+
+ strings = []
+ for optName in exclusions:
+ if len(optName) == 1:
+ # short option
+ strings.append("-" + optName)
+ else:
+ strings.append("--" + optName)
+ return "(%s)" % " ".join(strings)
+
+
+ def makeExcludesDict(self):
+ """
+ @return: A C{dict} that maps each option name appearing in
+ self.mutuallyExclusive to a list of those option names that
+ is it mutually exclusive with (can't appear on the cmd line with)
+ """
+
+ #create a mapping of long option name -> single character name
+ longToShort = {}
+ for optList in itertools.chain(self.optParams, self.optFlags):
+ try:
+ if optList[1] != None:
+ longToShort[optList[0]] = optList[1]
+ except IndexError:
+ pass
+
+ excludes = {}
+ for lst in self.mutuallyExclusive:
+ for i, longname in enumerate(lst):
+ tmp = []
+ tmp.extend(lst[:i])
+ tmp.extend(lst[i+1:])
+ for name in tmp[:]:
+ if name in longToShort:
+ tmp.append(longToShort[name])
+
+ if longname in excludes:
+ excludes[longname].extend(tmp)
+ else:
+ excludes[longname] = tmp
+ return excludes
+
+
+ def writeOpt(self, longname):
+ """
+ Write out the zsh code for the given argument. This is just part of the
+ one big call to _arguments
+
+ @type longname: C{str}
+ @param longname: The long name of the option
+ (i.e. "verbose" instead of "v")
+
+ @return: C{None}
+ """
+ if longname in self.optFlags_d:
+ # It's a flag option. Not one that takes a parameter.
+ long_field = "--%s" % longname
+ else:
+ long_field = "--%s=" % longname
+
+ short = self.getShortOption(longname)
+ if short != None:
+ short_field = "-" + short
+ else:
+ short_field = ''
+
+ descr = self.getDescription(longname)
+ descr_field = descr.replace("[", "\[")
+ descr_field = descr_field.replace("]", "\]")
+ descr_field = '[%s]' % descr_field
+
+ if longname in self.actionDescr:
+ actionDescr_field = self.actionDescr[longname]
+ else:
+ actionDescr_field = descr
+
+ action_field = self.getAction(longname)
+ if longname in self.multiUse:
+ multi_field = '*'
+ else:
+ multi_field = ''
+
+ longExclusions_field = self.excludeStr(longname)
+
+ if short:
+ #we have to write an extra line for the short option if we have one
+ shortExclusions_field = self.excludeStr(longname, buildShort=True)
+ self.file.write(escape('%s%s%s%s%s' % (shortExclusions_field,
+ multi_field, short_field, descr_field, action_field)))
+ self.file.write(' \\\n')
+
+ self.file.write(escape('%s%s%s%s%s' % (longExclusions_field,
+ multi_field, long_field, descr_field, action_field)))
+ self.file.write(' \\\n')
+
+
+ def getAction(self, longname):
+ """
+ Return a zsh "action" string for the given argument
+ @return: C{str}
+ """
+ if longname in self.actions:
+ if callable(self.actions[longname]):
+ action = self.actions[longname]()
+ else:
+ action = self.actions[longname]
+ return ":%s:%s" % (self.getActionDescr(longname), action)
+ if longname in self.optParams_d:
+ return ':%s:_files' % self.getActionDescr(longname)
+ return ''
+
+
+ def getActionDescr(self, longname):
+ """
+ Return the description to be used when this argument is completed
+ @return: C{str}
+ """
+ if longname in self.actionDescr:
+ return self.actionDescr[longname]
+ else:
+ return longname
+
+
+ def getDescription(self, longname):
+ """
+ Return the description to be used for this argument
+ @return: C{str}
+ """
+ #check if we have an alternate descr for this arg, and if so use it
+ if longname in self.altArgDescr:
+ return self.altArgDescr[longname]
+
+ #otherwise we have to get it from the optFlags or optParams
+ try:
+ descr = self.optFlags_d[longname][1]
+ except KeyError:
+ try:
+ descr = self.optParams_d[longname][2]
+ except KeyError:
+ descr = None
+
+ if descr is not None:
+ return descr
+
+ # lets try to get it from the opt_foo method doc string if there is one
+ longMangled = longname.replace('-', '_') # this is what t.p.usage does
+ obj = getattr(self.options, 'opt_%s' % longMangled, None)
+ if obj:
+ descr = descrFromDoc(obj)
+ if descr is not None:
+ return descr
+
+ return longname # we really ought to have a good description to use
+
+
+ def getShortOption(self, longname):
+ """
+ Return the short option letter or None
+ @return: C{str} or C{None}
+ """
+ optList = self.optAll_d[longname]
+ try:
+ return optList[0] or None
+ except IndexError:
+ pass
+
+
+ def addAdditionalOptions(self):
+ """
+ Add additional options to the optFlags and optParams lists.
+ These will be defined by 'opt_foo' methods of the Options subclass
+ @return: C{None}
+ """
+ methodsDict = {}
+ reflect.accumulateMethods(self.options, methodsDict, 'opt_')
+ methodToShort = {}
+ for name in methodsDict.copy():
+ if len(name) == 1:
+ methodToShort[methodsDict[name]] = name
+ del methodsDict[name]
+
+ for methodName, methodObj in methodsDict.items():
+ longname = methodName.replace('_', '-') # t.p.usage does this
+ # if this option is already defined by the optFlags or
+ # optParameters then we don't want to override that data
+ if longname in self.optAll_d:
+ continue
+
+ descr = self.getDescription(longname)
+
+ short = None
+ if methodObj in methodToShort:
+ short = methodToShort[methodObj]
+
+ reqArgs = methodObj.im_func.func_code.co_argcount
+ if reqArgs == 2:
+ self.optParams.append([longname, short, None, descr])
+ self.optParams_d[longname] = [short, None, descr]
+ self.optAll_d[longname] = [short, None, descr]
+ elif reqArgs == 1:
+ self.optFlags.append([longname, short, descr])
+ self.optFlags_d[longname] = [short, descr]
+ self.optAll_d[longname] = [short, None, descr]
+ else:
+ raise TypeError, '%r has wrong number ' \
+ 'of arguments' % (methodObj,)
+
+
+
+def descrFromDoc(obj):
+ """
+ Generate an appropriate description from docstring of the given object
+ """
+ if obj.__doc__ is None:
+ return None
+
+ lines = obj.__doc__.split("\n")
+ descr = None
+ try:
+ if lines[0] != "" and not lines[0].isspace():
+ descr = lines[0].lstrip()
+ # skip first line if it's blank
+ elif lines[1] != "" and not lines[1].isspace():
+ descr = lines[1].lstrip()
+ except IndexError:
+ pass
+ return descr
+
+
+
+def firstLine(s):
+ """
+ Return the first line of the given string
+ """
+ try:
+ i = s.index('\n')
+ return s[:i]
+ except ValueError:
+ return s
+
+
+
+def escape(str):
+ """
+ Shell escape the given string
+ """
+ return commands.mkarg(str)[1:]
+
+
+
+def siteFunctionsPath():
+ """
+ Return the path to the system-wide site-functions directory or
+ C{None} if it cannot be determined
+ """
+ try:
+ cmd = "zsh -f -c 'echo ${(M)fpath:#/*/site-functions}'"
+ output = commands.getoutput(cmd)
+ if os.path.isdir(output):
+ return output
+ except:
+ pass
+
+
+
+generateFor = [('conch', 'twisted.conch.scripts.conch', 'ClientOptions'),
+ ('mktap', 'twisted.scripts.mktap', 'FirstPassOptions'),
+ ('trial', 'twisted.scripts.trial', 'Options'),
+ ('cftp', 'twisted.conch.scripts.cftp', 'ClientOptions'),
+ ('tapconvert', 'twisted.scripts.tapconvert', 'ConvertOptions'),
+ ('twistd', 'twisted.scripts.twistd', 'ServerOptions'),
+ ('ckeygen', 'twisted.conch.scripts.ckeygen', 'GeneralOptions'),
+ ('lore', 'twisted.lore.scripts.lore', 'Options'),
+ ('pyhtmlizer', 'twisted.scripts.htmlizer', 'Options'),
+ ('tap2deb', 'twisted.scripts.tap2deb', 'MyOptions'),
+ ('tkconch', 'twisted.conch.scripts.tkconch', 'GeneralOptions'),
+ ('manhole', 'twisted.scripts.manhole', 'MyOptions'),
+ ('tap2rpm', 'twisted.scripts.tap2rpm', 'MyOptions'),
+ ]
+
+specialBuilders = {'mktap' : MktapBuilder,
+ 'twistd' : TwistdBuilder}
+
+
+
+def makeCompFunctionFiles(out_path, generateFor=generateFor,
+ specialBuilders=specialBuilders):
+ """
+ Generate completion function files in the given directory for all
+ twisted commands
+
+ @type out_path: C{str}
+ @param out_path: The path to the directory to generate completion function
+ fils in
+
+ @param generateFor: Sequence in the form of the 'generateFor' top-level
+ variable as defined in this module. Indicates what
+ commands to build completion files for.
+
+ @param specialBuilders: Sequence in the form of the 'specialBuilders'
+ top-level variable as defined in this module.
+ Indicates what commands require a special
+ Builder class.
+
+ @return: C{list} of 2-tuples of the form (cmd_name, error) indicating
+ commands that we skipped building completions for. cmd_name
+ is the name of the skipped command, and error is the Exception
+ that was raised when trying to import the script module.
+ Commands are usually skipped due to a missing dependency,
+ e.g. Tkinter.
+ """
+ skips = []
+ for cmd_name, module_name, class_name in generateFor:
+ if module_name is None:
+ # create empty file
+ f = _openCmdFile(out_path, cmd_name)
+ f.close()
+ continue
+ try:
+ m = __import__('%s' % (module_name,), None, None, (class_name))
+ f = _openCmdFile(out_path, cmd_name)
+ o = getattr(m, class_name)() # instantiate Options class
+
+ if cmd_name in specialBuilders:
+ b = specialBuilders[cmd_name](cmd_name, o, f)
+ b.write()
+ else:
+ b = Builder(cmd_name, o, f)
+ b.write()
+ except Exception, e:
+ skips.append( (cmd_name, e) )
+ continue
+ return skips
+
+
+
+def _openCmdFile(out_path, cmd_name):
+ return file(os.path.join(out_path, '_'+cmd_name), 'w')
+
+
+
+def run():
+ options = MyOptions()
+ try:
+ options.parseOptions(sys.argv[1:])
+ except usage.UsageError, e:
+ print e
+ print options.getUsage()
+ sys.exit(2)
+
+ if options['install']:
+ import twisted
+ dir = os.path.join(os.path.dirname(twisted.__file__), "python", "zsh")
+ skips = makeCompFunctionFiles(dir)
+ else:
+ skips = makeCompFunctionFiles(options['directory'])
+
+ for cmd_name, error in skips:
+ sys.stderr.write("zshcomp: Skipped building for %s. Script module " \
+ "could not be imported:\n" % (cmd_name,))
+ sys.stderr.write(str(error)+'\n')
+ if skips:
+ sys.exit(3)
+
+
+
+if __name__ == '__main__':
+ run()
diff --git a/twisted/runner/__init__.py b/twisted/runner/__init__.py
new file mode 100644
index 0000000..06b5d4b
--- /dev/null
+++ b/twisted/runner/__init__.py
@@ -0,0 +1,15 @@
+"""
+Twisted runer: run and monitor processes
+
+Maintainer: Andrew Bennetts
+
+classic inetd(8) support:
+Future Plans: The basic design should be final. There are some bugs that need
+fixing regarding UDP and Sun-RPC support. Perhaps some day xinetd
+compatibility will be added.
+
+procmon:monitor and restart processes
+"""
+
+from twisted.runner._version import version
+__version__ = version.short()
diff --git a/twisted/runner/_version.py b/twisted/runner/_version.py
new file mode 100644
index 0000000..e5da938
--- /dev/null
+++ b/twisted/runner/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.runner', 12, 1, 0)
diff --git a/twisted/runner/inetd.py b/twisted/runner/inetd.py
new file mode 100644
index 0000000..010b89e
--- /dev/null
+++ b/twisted/runner/inetd.py
@@ -0,0 +1,70 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+Twisted inetd.
+
+Maintainer: Andrew Bennetts
+
+Future Plans: Bugfixes. Specifically for UDP and Sun-RPC, which don't work
+correctly yet.
+"""
+
+import os
+
+from twisted.internet import process, reactor, fdesc
+from twisted.internet.protocol import Protocol, ServerFactory
+from twisted.protocols import wire
+
+# A dict of known 'internal' services (i.e. those that don't involve spawning
+# another process.
+internalProtocols = {
+ 'echo': wire.Echo,
+ 'chargen': wire.Chargen,
+ 'discard': wire.Discard,
+ 'daytime': wire.Daytime,
+ 'time': wire.Time,
+}
+
+
+class InetdProtocol(Protocol):
+ """Forks a child process on connectionMade, passing the socket as fd 0."""
+ def connectionMade(self):
+ sockFD = self.transport.fileno()
+ childFDs = {0: sockFD, 1: sockFD}
+ if self.factory.stderrFile:
+ childFDs[2] = self.factory.stderrFile.fileno()
+
+ # processes run by inetd expect blocking sockets
+ # FIXME: maybe this should be done in process.py? are other uses of
+ # Process possibly affected by this?
+ fdesc.setBlocking(sockFD)
+ if childFDs.has_key(2):
+ fdesc.setBlocking(childFDs[2])
+
+ service = self.factory.service
+ uid = service.user
+ gid = service.group
+
+ # don't tell Process to change our UID/GID if it's what we
+ # already are
+ if uid == os.getuid():
+ uid = None
+ if gid == os.getgid():
+ gid = None
+
+ process.Process(None, service.program, service.programArgs, os.environ,
+ None, None, uid, gid, childFDs)
+
+ reactor.removeReader(self.transport)
+ reactor.removeWriter(self.transport)
+
+
+class InetdFactory(ServerFactory):
+ protocol = InetdProtocol
+ stderrFile = None
+
+ def __init__(self, service):
+ self.service = service
diff --git a/twisted/runner/inetdconf.py b/twisted/runner/inetdconf.py
new file mode 100644
index 0000000..f06a2ab
--- /dev/null
+++ b/twisted/runner/inetdconf.py
@@ -0,0 +1,194 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""
+Parser for inetd.conf files
+
+Maintainer: Andrew Bennetts
+
+Future Plans: xinetd configuration file support?
+"""
+
+# Various exceptions
+class InvalidConfError(Exception):
+ """Invalid configuration file"""
+
+
+class InvalidInetdConfError(InvalidConfError):
+ """Invalid inetd.conf file"""
+
+
+class InvalidServicesConfError(InvalidConfError):
+ """Invalid services file"""
+
+
+class InvalidRPCServicesConfError(InvalidConfError):
+ """Invalid rpc services file"""
+
+
+class UnknownService(Exception):
+ """Unknown service name"""
+
+
+class SimpleConfFile:
+ """Simple configuration file parser superclass.
+
+ Filters out comments and empty lines (which includes lines that only
+ contain comments).
+
+ To use this class, override parseLine or parseFields.
+ """
+
+ commentChar = '#'
+ defaultFilename = None
+
+ def parseFile(self, file=None):
+ """Parse a configuration file
+
+ If file is None and self.defaultFilename is set, it will open
+ defaultFilename and use it.
+ """
+ if file is None and self.defaultFilename:
+ file = open(self.defaultFilename,'r')
+
+ for line in file.readlines():
+ # Strip out comments
+ comment = line.find(self.commentChar)
+ if comment != -1:
+ line = line[:comment]
+
+ # Strip whitespace
+ line = line.strip()
+
+ # Skip empty lines (and lines which only contain comments)
+ if not line:
+ continue
+
+ self.parseLine(line)
+
+ def parseLine(self, line):
+ """Override this.
+
+ By default, this will split the line on whitespace and call
+ self.parseFields (catching any errors).
+ """
+ try:
+ self.parseFields(*line.split())
+ except ValueError:
+ raise InvalidInetdConfError, 'Invalid line: ' + repr(line)
+
+ def parseFields(self, *fields):
+ """Override this."""
+
+
+class InetdService:
+ """A simple description of an inetd service."""
+ name = None
+ port = None
+ socketType = None
+ protocol = None
+ wait = None
+ user = None
+ group = None
+ program = None
+ programArgs = None
+
+ def __init__(self, name, port, socketType, protocol, wait, user, group,
+ program, programArgs):
+ self.name = name
+ self.port = port
+ self.socketType = socketType
+ self.protocol = protocol
+ self.wait = wait
+ self.user = user
+ self.group = group
+ self.program = program
+ self.programArgs = programArgs
+
+
+class InetdConf(SimpleConfFile):
+ """Configuration parser for a traditional UNIX inetd(8)"""
+
+ defaultFilename = '/etc/inetd.conf'
+
+ def __init__(self, knownServices=None):
+ self.services = []
+
+ if knownServices is None:
+ knownServices = ServicesConf()
+ knownServices.parseFile()
+ self.knownServices = knownServices
+
+ def parseFields(self, serviceName, socketType, protocol, wait, user,
+ program, *programArgs):
+ """Parse an inetd.conf file.
+
+ Implemented from the description in the Debian inetd.conf man page.
+ """
+ # Extract user (and optional group)
+ user, group = (user.split('.') + [None])[:2]
+
+ # Find the port for a service
+ port = self.knownServices.services.get((serviceName, protocol), None)
+ if not port and not protocol.startswith('rpc/'):
+ # FIXME: Should this be discarded/ignored, rather than throwing
+ # an exception?
+ try:
+ port = int(serviceName)
+ serviceName = 'unknown'
+ except:
+ raise UnknownService, "Unknown service: %s (%s)" \
+ % (serviceName, protocol)
+
+ self.services.append(InetdService(serviceName, port, socketType,
+ protocol, wait, user, group, program,
+ programArgs))
+
+
+class ServicesConf(SimpleConfFile):
+ """/etc/services parser
+
+ @ivar services: dict mapping service names to (port, protocol) tuples.
+ """
+
+ defaultFilename = '/etc/services'
+
+ def __init__(self):
+ self.services = {}
+
+ def parseFields(self, name, portAndProtocol, *aliases):
+ try:
+ port, protocol = portAndProtocol.split('/')
+ port = long(port)
+ except:
+ raise InvalidServicesConfError, 'Invalid port/protocol:' + \
+ repr(portAndProtocol)
+
+ self.services[(name, protocol)] = port
+ for alias in aliases:
+ self.services[(alias, protocol)] = port
+
+
+class RPCServicesConf(SimpleConfFile):
+ """/etc/rpc parser
+
+ @ivar self.services: dict mapping rpc service names to rpc ports.
+ """
+
+ defaultFilename = '/etc/rpc'
+
+ def __init__(self):
+ self.services = {}
+
+ def parseFields(self, name, port, *aliases):
+ try:
+ port = long(port)
+ except:
+ raise InvalidRPCServicesConfError, 'Invalid port:' + repr(port)
+
+ self.services[name] = port
+ for alias in aliases:
+ self.services[alias] = port
+
+
diff --git a/twisted/runner/inetdtap.py b/twisted/runner/inetdtap.py
new file mode 100644
index 0000000..3e62877
--- /dev/null
+++ b/twisted/runner/inetdtap.py
@@ -0,0 +1,163 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""
+Twisted inetd TAP support
+
+Maintainer: Andrew Bennetts
+
+Future Plans: more configurability.
+"""
+
+import os, pwd, grp, socket
+
+from twisted.runner import inetd, inetdconf
+from twisted.python import log, usage
+from twisted.internet.protocol import ServerFactory
+from twisted.application import internet, service as appservice
+
+try:
+ import portmap
+ rpcOk = 1
+except ImportError:
+ rpcOk = 0
+
+
+# Protocol map
+protocolDict = {'tcp': socket.IPPROTO_TCP, 'udp': socket.IPPROTO_UDP}
+
+
+class Options(usage.Options):
+
+ optParameters = [
+ ['rpc', 'r', '/etc/rpc', 'RPC procedure table file'],
+ ['file', 'f', '/etc/inetd.conf', 'Service configuration file']
+ ]
+
+ optFlags = [['nointernal', 'i', "Don't run internal services"]]
+
+ compData = usage.Completions(
+ optActions={"file": usage.CompleteFiles('*.conf')}
+ )
+
+class RPCServer(internet.TCPServer):
+
+ def __init__(self, rpcVersions, rpcConf, proto, service):
+ internet.TCPServer.__init__(0, ServerFactory())
+ self.rpcConf = rpcConf
+ self.proto = proto
+ self.service = service
+
+ def startService(self):
+ internet.TCPServer.startService(self)
+ import portmap
+ portNo = self._port.getHost()[2]
+ service = self.service
+ for version in rpcVersions:
+ portmap.set(self.rpcConf.services[name], version, self.proto,
+ portNo)
+ inetd.forkPassingFD(service.program, service.programArgs,
+ os.environ, service.user, service.group, p)
+
+def makeService(config):
+ s = appservice.MultiService()
+ conf = inetdconf.InetdConf()
+ conf.parseFile(open(config['file']))
+
+ rpcConf = inetdconf.RPCServicesConf()
+ try:
+ rpcConf.parseFile(open(config['rpc']))
+ except:
+ # We'll survive even if we can't read /etc/rpc
+ log.deferr()
+
+ for service in conf.services:
+ rpc = service.protocol.startswith('rpc/')
+ protocol = service.protocol
+
+ if rpc and not rpcOk:
+ log.msg('Skipping rpc service due to lack of rpc support')
+ continue
+
+ if rpc:
+ # RPC has extra options, so extract that
+ protocol = protocol[4:] # trim 'rpc/'
+ if not protocolDict.has_key(protocol):
+ log.msg('Bad protocol: ' + protocol)
+ continue
+
+ try:
+ name, rpcVersions = service.name.split('/')
+ except ValueError:
+ log.msg('Bad RPC service/version: ' + service.name)
+ continue
+
+ if not rpcConf.services.has_key(name):
+ log.msg('Unknown RPC service: ' + repr(service.name))
+ continue
+
+ try:
+ if '-' in rpcVersions:
+ start, end = map(int, rpcVersions.split('-'))
+ rpcVersions = range(start, end+1)
+ else:
+ rpcVersions = [int(rpcVersions)]
+ except ValueError:
+ log.msg('Bad RPC versions: ' + str(rpcVersions))
+ continue
+
+ if (protocol, service.socketType) not in [('tcp', 'stream'),
+ ('udp', 'dgram')]:
+ log.msg('Skipping unsupported type/protocol: %s/%s'
+ % (service.socketType, service.protocol))
+ continue
+
+ # Convert the username into a uid (if necessary)
+ try:
+ service.user = int(service.user)
+ except ValueError:
+ try:
+ service.user = pwd.getpwnam(service.user)[2]
+ except KeyError:
+ log.msg('Unknown user: ' + service.user)
+ continue
+
+ # Convert the group name into a gid (if necessary)
+ if service.group is None:
+ # If no group was specified, use the user's primary group
+ service.group = pwd.getpwuid(service.user)[3]
+ else:
+ try:
+ service.group = int(service.group)
+ except ValueError:
+ try:
+ service.group = grp.getgrnam(service.group)[2]
+ except KeyError:
+ log.msg('Unknown group: ' + service.group)
+ continue
+
+ if service.program == 'internal':
+ if config['nointernal']:
+ continue
+
+ # Internal services can use a standard ServerFactory
+ if not inetd.internalProtocols.has_key(service.name):
+ log.msg('Unknown internal service: ' + service.name)
+ continue
+ factory = ServerFactory()
+ factory.protocol = inetd.internalProtocols[service.name]
+ elif rpc:
+ i = RPCServer(rpcVersions, rpcConf, proto, service)
+ i.setServiceParent(s)
+ continue
+ else:
+ # Non-internal non-rpc services use InetdFactory
+ factory = inetd.InetdFactory(service)
+
+ if protocol == 'tcp':
+ internet.TCPServer(service.port, factory).setServiceParent(s)
+ elif protocol == 'udp':
+ raise RuntimeError("not supporting UDP")
+ return s
diff --git a/twisted/runner/portmap.c b/twisted/runner/portmap.c
new file mode 100644
index 0000000..ca0c1c9
--- /dev/null
+++ b/twisted/runner/portmap.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2001-2004 Twisted Matrix Laboratories.
+ * See LICENSE for details.
+
+ *
+ */
+
+/* portmap.c: A simple Python wrapper for pmap_set(3) and pmap_unset(3) */
+
+#include <Python.h>
+#include <rpc/rpc.h>
+#include <rpc/pmap_clnt.h>
+
+static PyObject * portmap_set(PyObject *self, PyObject *args)
+{
+ unsigned long program, version;
+ int protocol;
+ unsigned short port;
+
+ if (!PyArg_ParseTuple(args, "llih:set",
+ &program, &version, &protocol, &port))
+ return NULL;
+
+ pmap_unset(program, version);
+ pmap_set(program, version, protocol, port);
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyObject * portmap_unset(PyObject *self, PyObject *args)
+{
+ unsigned long program, version;
+
+ if (!PyArg_ParseTuple(args, "ll:unset",
+ &program, &version))
+ return NULL;
+
+ pmap_unset(program, version);
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+static PyMethodDef PortmapMethods[] = {
+ {"set", portmap_set, METH_VARARGS,
+ "Set an entry in the portmapper."},
+ {"unset", portmap_unset, METH_VARARGS,
+ "Unset an entry in the portmapper."},
+ {NULL, NULL, 0, NULL}
+};
+
+void initportmap(void)
+{
+ (void) Py_InitModule("portmap", PortmapMethods);
+}
+
diff --git a/twisted/runner/procmon.py b/twisted/runner/procmon.py
new file mode 100644
index 0000000..3515995
--- /dev/null
+++ b/twisted/runner/procmon.py
@@ -0,0 +1,310 @@
+# -*- test-case-name: twisted.runner.test.test_procmon -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support for starting, monitoring, and restarting child process.
+"""
+import warnings
+
+from twisted.python import log
+from twisted.internet import error, protocol, reactor as _reactor
+from twisted.application import service
+from twisted.protocols import basic
+
+class DummyTransport:
+
+ disconnecting = 0
+
+transport = DummyTransport()
+
+class LineLogger(basic.LineReceiver):
+
+ tag = None
+ delimiter = '\n'
+
+ def lineReceived(self, line):
+ log.msg('[%s] %s' % (self.tag, line))
+
+
+class LoggingProtocol(protocol.ProcessProtocol):
+
+ service = None
+ name = None
+ empty = 1
+
+ def connectionMade(self):
+ self.output = LineLogger()
+ self.output.tag = self.name
+ self.output.makeConnection(transport)
+
+
+ def outReceived(self, data):
+ self.output.dataReceived(data)
+ self.empty = data[-1] == '\n'
+
+ errReceived = outReceived
+
+
+ def processEnded(self, reason):
+ if not self.empty:
+ self.output.dataReceived('\n')
+ self.service.connectionLost(self.name)
+
+
+class ProcessMonitor(service.Service):
+ """
+ ProcessMonitor runs processes, monitors their progress, and restarts
+ them when they die.
+
+ The ProcessMonitor will not attempt to restart a process that appears to
+ die instantly -- with each "instant" death (less than 1 second, by
+ default), it will delay approximately twice as long before restarting
+ it. A successful run will reset the counter.
+
+ The primary interface is L{addProcess} and L{removeProcess}. When the
+ service is running (that is, when the application it is attached to is
+ running), adding a process automatically starts it.
+
+ Each process has a name. This name string must uniquely identify the
+ process. In particular, attempting to add two processes with the same
+ name will result in a C{KeyError}.
+
+ @type threshold: C{float}
+ @ivar threshold: How long a process has to live before the death is
+ considered instant, in seconds. The default value is 1 second.
+
+ @type killTime: C{float}
+ @ivar killTime: How long a process being killed has to get its affairs
+ in order before it gets killed with an unmaskable signal. The
+ default value is 5 seconds.
+
+ @type minRestartDelay: C{float}
+ @ivar minRestartDelay: The minimum time (in seconds) to wait before
+ attempting to restart a process. Default 1s.
+
+ @type maxRestartDelay: C{float}
+ @ivar maxRestartDelay: The maximum time (in seconds) to wait before
+ attempting to restart a process. Default 3600s (1h).
+
+ @type _reactor: L{IReactorProcess} provider
+ @ivar _reactor: A provider of L{IReactorProcess} and L{IReactorTime}
+ which will be used to spawn processes and register delayed calls.
+
+ """
+ threshold = 1
+ killTime = 5
+ minRestartDelay = 1
+ maxRestartDelay = 3600
+
+
+ def __init__(self, reactor=_reactor):
+ self._reactor = reactor
+
+ self.processes = {}
+ self.protocols = {}
+ self.delay = {}
+ self.timeStarted = {}
+ self.murder = {}
+ self.restart = {}
+
+
+ def __getstate__(self):
+ dct = service.Service.__getstate__(self)
+ del dct['_reactor']
+ dct['protocols'] = {}
+ dct['delay'] = {}
+ dct['timeStarted'] = {}
+ dct['murder'] = {}
+ dct['restart'] = {}
+ return dct
+
+
+ def addProcess(self, name, args, uid=None, gid=None, env={}):
+ """
+ Add a new monitored process and start it immediately if the
+ L{ProcessMonitor} service is running.
+
+ Note that args are passed to the system call, not to the shell. If
+ running the shell is desired, the common idiom is to use
+ C{ProcessMonitor.addProcess("name", ['/bin/sh', '-c', shell_script])}
+
+ @param name: A name for this process. This value must be
+ unique across all processes added to this monitor.
+ @type name: C{str}
+ @param args: The argv sequence for the process to launch.
+ @param uid: The user ID to use to run the process. If C{None},
+ the current UID is used.
+ @type uid: C{int}
+ @param gid: The group ID to use to run the process. If C{None},
+ the current GID is used.
+ @type uid: C{int}
+ @param env: The environment to give to the launched process. See
+ L{IReactorProcess.spawnProcess}'s C{env} parameter.
+ @type env: C{dict}
+ @raises: C{KeyError} if a process with the given name already
+ exists
+ """
+ if name in self.processes:
+ raise KeyError("remove %s first" % (name,))
+ self.processes[name] = args, uid, gid, env
+ self.delay[name] = self.minRestartDelay
+ if self.running:
+ self.startProcess(name)
+
+
+ def removeProcess(self, name):
+ """
+ Stop the named process and remove it from the list of monitored
+ processes.
+
+ @type name: C{str}
+ @param name: A string that uniquely identifies the process.
+ """
+ self.stopProcess(name)
+ del self.processes[name]
+
+
+ def startService(self):
+ """
+ Start all monitored processes.
+ """
+ service.Service.startService(self)
+ for name in self.processes:
+ self.startProcess(name)
+
+
+ def stopService(self):
+ """
+ Stop all monitored processes and cancel all scheduled process restarts.
+ """
+ service.Service.stopService(self)
+
+ # Cancel any outstanding restarts
+ for name, delayedCall in self.restart.items():
+ if delayedCall.active():
+ delayedCall.cancel()
+
+ for name in self.processes:
+ self.stopProcess(name)
+
+
+ def connectionLost(self, name):
+ """
+ Called when a monitored processes exits. If
+ L{ProcessMonitor.running} is C{True} (ie the service is started), the
+ process will be restarted.
+ If the process had been running for more than
+ L{ProcessMonitor.threshold} seconds it will be restarted immediately.
+ If the process had been running for less than
+ L{ProcessMonitor.threshold} seconds, the restart will be delayed and
+ each time the process dies before the configured threshold, the restart
+ delay will be doubled - up to a maximum delay of maxRestartDelay sec.
+
+ @type name: C{str}
+ @param name: A string that uniquely identifies the process
+ which exited.
+ """
+ # Cancel the scheduled _forceStopProcess function if the process
+ # dies naturally
+ if name in self.murder:
+ if self.murder[name].active():
+ self.murder[name].cancel()
+ del self.murder[name]
+
+ del self.protocols[name]
+
+ if self._reactor.seconds() - self.timeStarted[name] < self.threshold:
+ # The process died too fast - backoff
+ nextDelay = self.delay[name]
+ self.delay[name] = min(self.delay[name] * 2, self.maxRestartDelay)
+
+ else:
+ # Process had been running for a significant amount of time
+ # restart immediately
+ nextDelay = 0
+ self.delay[name] = self.minRestartDelay
+
+ # Schedule a process restart if the service is running
+ if self.running and name in self.processes:
+ self.restart[name] = self._reactor.callLater(nextDelay,
+ self.startProcess,
+ name)
+
+
+ def startProcess(self, name):
+ """
+ @param name: The name of the process to be started
+ """
+ # If a protocol instance already exists, it means the process is
+ # already running
+ if name in self.protocols:
+ return
+
+ args, uid, gid, env = self.processes[name]
+
+ proto = LoggingProtocol()
+ proto.service = self
+ proto.name = name
+ self.protocols[name] = proto
+ self.timeStarted[name] = self._reactor.seconds()
+ self._reactor.spawnProcess(proto, args[0], args, uid=uid,
+ gid=gid, env=env)
+
+
+ def _forceStopProcess(self, proc):
+ """
+ @param proc: An L{IProcessTransport} provider
+ """
+ try:
+ proc.signalProcess('KILL')
+ except error.ProcessExitedAlready:
+ pass
+
+
+ def stopProcess(self, name):
+ """
+ @param name: The name of the process to be stopped
+ """
+ if name not in self.processes:
+ raise KeyError('Unrecognized process name: %s' % (name,))
+
+ proto = self.protocols.get(name, None)
+ if proto is not None:
+ proc = proto.transport
+ try:
+ proc.signalProcess('TERM')
+ except error.ProcessExitedAlready:
+ pass
+ else:
+ self.murder[name] = self._reactor.callLater(
+ self.killTime,
+ self._forceStopProcess, proc)
+
+
+ def restartAll(self):
+ """
+ Restart all processes. This is useful for third party management
+ services to allow a user to restart servers because of an outside change
+ in circumstances -- for example, a new version of a library is
+ installed.
+ """
+ for name in self.processes:
+ self.stopProcess(name)
+
+
+ def __repr__(self):
+ l = []
+ for name, proc in self.processes.items():
+ uidgid = ''
+ if proc[1] is not None:
+ uidgid = str(proc[1])
+ if proc[2] is not None:
+ uidgid += ':'+str(proc[2])
+
+ if uidgid:
+ uidgid = '(' + uidgid + ')'
+ l.append('%r%s: %r' % (name, uidgid, proc[0]))
+ return ('<' + self.__class__.__name__ + ' '
+ + ' '.join(l)
+ + '>')
diff --git a/twisted/runner/procmontap.py b/twisted/runner/procmontap.py
new file mode 100644
index 0000000..c0e72a4
--- /dev/null
+++ b/twisted/runner/procmontap.py
@@ -0,0 +1,73 @@
+# -*- test-case-name: twisted.runner.test.test_procmontap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support for creating a service which runs a process monitor.
+"""
+
+from twisted.python import usage
+from twisted.runner.procmon import ProcessMonitor
+
+
+class Options(usage.Options):
+ """
+ Define the options accepted by the I{twistd procmon} plugin.
+ """
+
+ synopsis = "[procmon options] commandline"
+
+ optParameters = [["threshold", "t", 1, "How long a process has to live "
+ "before the death is considered instant, in seconds.",
+ float],
+ ["killtime", "k", 5, "How long a process being killed "
+ "has to get its affairs in order before it gets killed "
+ "with an unmaskable signal.",
+ float],
+ ["minrestartdelay", "m", 1, "The minimum time (in "
+ "seconds) to wait before attempting to restart a "
+ "process", float],
+ ["maxrestartdelay", "M", 3600, "The maximum time (in "
+ "seconds) to wait before attempting to restart a "
+ "process", float]]
+
+ optFlags = []
+
+
+ longdesc = """\
+procmon runs processes, monitors their progress, and restarts them when they
+die.
+
+procmon will not attempt to restart a process that appears to die instantly;
+with each "instant" death (less than 1 second, by default), it will delay
+approximately twice as long before restarting it. A successful run will reset
+the counter.
+
+Eg twistd procmon sleep 10"""
+
+ def parseArgs(self, *args):
+ """
+ Grab the command line that is going to be started and monitored
+ """
+ self['args'] = args
+
+
+ def postOptions(self):
+ """
+ Check for dependencies.
+ """
+ if len(self["args"]) < 1:
+ raise usage.UsageError("Please specify a process commandline")
+
+
+
+def makeService(config):
+ s = ProcessMonitor()
+
+ s.threshold = config["threshold"]
+ s.killTime = config["killtime"]
+ s.minRestartDelay = config["minrestartdelay"]
+ s.maxRestartDelay = config["maxrestartdelay"]
+
+ s.addProcess(" ".join(config["args"]), config["args"])
+ return s
diff --git a/twisted/runner/test/__init__.py b/twisted/runner/test/__init__.py
new file mode 100644
index 0000000..e6c22ba
--- /dev/null
+++ b/twisted/runner/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test package for Twisted Runner.
+"""
diff --git a/twisted/runner/test/test_procmon.py b/twisted/runner/test/test_procmon.py
new file mode 100644
index 0000000..d5217a0
--- /dev/null
+++ b/twisted/runner/test/test_procmon.py
@@ -0,0 +1,477 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.runner.procmon}.
+"""
+
+from twisted.trial import unittest
+from twisted.runner.procmon import LoggingProtocol, ProcessMonitor
+from twisted.internet.error import (ProcessDone, ProcessTerminated,
+ ProcessExitedAlready)
+from twisted.internet.task import Clock
+from twisted.python.failure import Failure
+from twisted.test.proto_helpers import MemoryReactor
+
+
+
+class DummyProcess(object):
+ """
+ An incomplete and fake L{IProcessTransport} implementation for testing how
+ L{ProcessMonitor} behaves when its monitored processes exit.
+
+ @ivar _terminationDelay: the delay in seconds after which the DummyProcess
+ will appear to exit when it receives a TERM signal
+ """
+
+ pid = 1
+ proto = None
+
+ _terminationDelay = 1
+
+ def __init__(self, reactor, executable, args, environment, path,
+ proto, uid=None, gid=None, usePTY=0, childFDs=None):
+
+ self.proto = proto
+
+ self._reactor = reactor
+ self._executable = executable
+ self._args = args
+ self._environment = environment
+ self._path = path
+ self._uid = uid
+ self._gid = gid
+ self._usePTY = usePTY
+ self._childFDs = childFDs
+
+
+ def signalProcess(self, signalID):
+ """
+ A partial implementation of signalProcess which can only handle TERM and
+ KILL signals.
+ - When a TERM signal is given, the dummy process will appear to exit
+ after L{DummyProcess._terminationDelay} seconds with exit code 0
+ - When a KILL signal is given, the dummy process will appear to exit
+ immediately with exit code 1.
+
+ @param signalID: The signal name or number to be issued to the process.
+ @type signalID: C{str}
+ """
+ params = {
+ "TERM": (self._terminationDelay, 0),
+ "KILL": (0, 1)
+ }
+
+ if self.pid is None:
+ raise ProcessExitedAlready()
+
+ if signalID in params:
+ delay, status = params[signalID]
+ self._signalHandler = self._reactor.callLater(
+ delay, self.processEnded, status)
+
+
+ def processEnded(self, status):
+ """
+ Deliver the process ended event to C{self.proto}.
+ """
+ self.pid = None
+ statusMap = {
+ 0: ProcessDone,
+ 1: ProcessTerminated,
+ }
+ self.proto.processEnded(Failure(statusMap[status](status)))
+
+
+
+class DummyProcessReactor(MemoryReactor, Clock):
+ """
+ @ivar spawnedProcesses: a list that keeps track of the fake process
+ instances built by C{spawnProcess}.
+ @type spawnedProcesses: C{list}
+ """
+ def __init__(self):
+ MemoryReactor.__init__(self)
+ Clock.__init__(self)
+
+ self.spawnedProcesses = []
+
+
+ def spawnProcess(self, processProtocol, executable, args=(), env={},
+ path=None, uid=None, gid=None, usePTY=0,
+ childFDs=None):
+ """
+ Fake L{reactor.spawnProcess}, that logs all the process
+ arguments and returns a L{DummyProcess}.
+ """
+
+ proc = DummyProcess(self, executable, args, env, path,
+ processProtocol, uid, gid, usePTY, childFDs)
+ processProtocol.makeConnection(proc)
+ self.spawnedProcesses.append(proc)
+ return proc
+
+
+
+class ProcmonTests(unittest.TestCase):
+ """
+ Tests for L{ProcessMonitor}.
+ """
+
+ def setUp(self):
+ """
+ Create an L{ProcessMonitor} wrapped around a fake reactor.
+ """
+ self.reactor = DummyProcessReactor()
+ self.pm = ProcessMonitor(reactor=self.reactor)
+ self.pm.minRestartDelay = 2
+ self.pm.maxRestartDelay = 10
+ self.pm.threshold = 10
+
+
+ def test_getStateIncludesProcesses(self):
+ """
+ The list of monitored processes must be included in the pickle state.
+ """
+ self.pm.addProcess("foo", ["arg1", "arg2"],
+ uid=1, gid=2, env={})
+ self.assertEqual(self.pm.__getstate__()['processes'],
+ {'foo': (['arg1', 'arg2'], 1, 2, {})})
+
+
+ def test_getStateExcludesReactor(self):
+ """
+ The private L{ProcessMonitor._reactor} instance variable should not be
+ included in the pickle state.
+ """
+ self.assertNotIn('_reactor', self.pm.__getstate__())
+
+
+ def test_addProcess(self):
+ """
+ L{ProcessMonitor.addProcess} only starts the named program if
+ L{ProcessMonitor.startService} has been called.
+ """
+ self.pm.addProcess("foo", ["arg1", "arg2"],
+ uid=1, gid=2, env={})
+ self.assertEqual(self.pm.protocols, {})
+ self.assertEqual(self.pm.processes,
+ {"foo": (["arg1", "arg2"], 1, 2, {})})
+ self.pm.startService()
+ self.reactor.advance(0)
+ self.assertEqual(self.pm.protocols.keys(), ["foo"])
+
+
+ def test_addProcessDuplicateKeyError(self):
+ """
+ L{ProcessMonitor.addProcess} raises a C{KeyError} if a process with the
+ given name already exists.
+ """
+ self.pm.addProcess("foo", ["arg1", "arg2"],
+ uid=1, gid=2, env={})
+ self.assertRaises(KeyError, self.pm.addProcess,
+ "foo", ["arg1", "arg2"], uid=1, gid=2, env={})
+
+
+ def test_addProcessEnv(self):
+ """
+ L{ProcessMonitor.addProcess} takes an C{env} parameter that is passed to
+ L{IReactorProcess.spawnProcess}.
+ """
+ fakeEnv = {"KEY": "value"}
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"], uid=1, gid=2, env=fakeEnv)
+ self.reactor.advance(0)
+ self.assertEqual(
+ self.reactor.spawnedProcesses[0]._environment, fakeEnv)
+
+
+ def test_removeProcess(self):
+ """
+ L{ProcessMonitor.removeProcess} removes the process from the public
+ processes list.
+ """
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ self.assertEqual(len(self.pm.processes), 1)
+ self.pm.removeProcess("foo")
+ self.assertEqual(len(self.pm.processes), 0)
+
+
+ def test_removeProcessUnknownKeyError(self):
+ """
+ L{ProcessMonitor.removeProcess} raises a C{KeyError} if the given
+ process name isn't recognised.
+ """
+ self.pm.startService()
+ self.assertRaises(KeyError, self.pm.removeProcess, "foo")
+
+
+ def test_startProcess(self):
+ """
+ When a process has been started, an instance of L{LoggingProtocol} will
+ be added to the L{ProcessMonitor.protocols} dict and the start time of
+ the process will be recorded in the L{ProcessMonitor.timeStarted}
+ dictionary.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ self.pm.startProcess("foo")
+ self.assertIsInstance(self.pm.protocols["foo"], LoggingProtocol)
+ self.assertIn("foo", self.pm.timeStarted.keys())
+
+
+ def test_startProcessAlreadyStarted(self):
+ """
+ L{ProcessMonitor.startProcess} silently returns if the named process is
+ already started.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ self.pm.startProcess("foo")
+ self.assertIdentical(None, self.pm.startProcess("foo"))
+
+
+ def test_startProcessUnknownKeyError(self):
+ """
+ L{ProcessMonitor.startProcess} raises a C{KeyError} if the given
+ process name isn't recognised.
+ """
+ self.assertRaises(KeyError, self.pm.startProcess, "foo")
+
+
+ def test_stopProcessNaturalTermination(self):
+ """
+ L{ProcessMonitor.stopProcess} immediately sends a TERM signal to the
+ named process.
+ """
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ self.assertIn("foo", self.pm.protocols)
+
+ # Configure fake process to die 1 second after receiving term signal
+ timeToDie = self.pm.protocols["foo"].transport._terminationDelay = 1
+
+ # Advance the reactor to just before the short lived process threshold
+ # and leave enough time for the process to die
+ self.reactor.advance(self.pm.threshold)
+ # Then signal the process to stop
+ self.pm.stopProcess("foo")
+
+ # Advance the reactor just enough to give the process time to die and
+ # verify that the process restarts
+ self.reactor.advance(timeToDie)
+
+ # We expect it to be restarted immediately
+ self.assertEqual(self.reactor.seconds(),
+ self.pm.timeStarted["foo"])
+
+
+ def test_stopProcessForcedKill(self):
+ """
+ L{ProcessMonitor.stopProcess} kills a process which fails to terminate
+ naturally within L{ProcessMonitor.killTime} seconds.
+ """
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ self.assertIn("foo", self.pm.protocols)
+ self.reactor.advance(self.pm.threshold)
+ proc = self.pm.protocols["foo"].transport
+ # Arrange for the fake process to live longer than the killTime
+ proc._terminationDelay = self.pm.killTime + 1
+ self.pm.stopProcess("foo")
+ # If process doesn't die before the killTime, procmon should
+ # terminate it
+ self.reactor.advance(self.pm.killTime - 1)
+ self.assertEqual(0.0, self.pm.timeStarted["foo"])
+
+ self.reactor.advance(1)
+ # We expect it to be immediately restarted
+ self.assertEqual(self.reactor.seconds(), self.pm.timeStarted["foo"])
+
+
+ def test_stopProcessUnknownKeyError(self):
+ """
+ L{ProcessMonitor.stopProcess} raises a C{KeyError} if the given process
+ name isn't recognised.
+ """
+ self.assertRaises(KeyError, self.pm.stopProcess, "foo")
+
+
+ def test_stopProcessAlreadyStopped(self):
+ """
+ L{ProcessMonitor.stopProcess} silently returns if the named process
+ is already stopped. eg Process has crashed and a restart has been
+ rescheduled, but in the meantime, the service is stopped.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ self.assertIdentical(None, self.pm.stopProcess("foo"))
+
+
+ def test_connectionLostLongLivedProcess(self):
+ """
+ L{ProcessMonitor.connectionLost} should immediately restart a process
+ if it has been running longer than L{ProcessMonitor.threshold} seconds.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ # Schedule the process to start
+ self.pm.startService()
+ # advance the reactor to start the process
+ self.reactor.advance(0)
+ self.assertIn("foo", self.pm.protocols)
+ # Long time passes
+ self.reactor.advance(self.pm.threshold)
+ # Process dies after threshold
+ self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
+ self.assertNotIn("foo", self.pm.protocols)
+ # Process should be restarted immediately
+ self.reactor.advance(0)
+ self.assertIn("foo", self.pm.protocols)
+
+
+ def test_connectionLostMurderCancel(self):
+ """
+ L{ProcessMonitor.connectionLost} cancels a scheduled process killer and
+ deletes the DelayedCall from the L{ProcessMonitor.murder} list.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ # Schedule the process to start
+ self.pm.startService()
+ # Advance 1s to start the process then ask ProcMon to stop it
+ self.reactor.advance(1)
+ self.pm.stopProcess("foo")
+ # A process killer has been scheduled, delayedCall is active
+ self.assertIn("foo", self.pm.murder)
+ delayedCall = self.pm.murder["foo"]
+ self.assertTrue(delayedCall.active())
+ # Advance to the point at which the dummy process exits
+ self.reactor.advance(
+ self.pm.protocols["foo"].transport._terminationDelay)
+ # Now the delayedCall has been cancelled and deleted
+ self.assertFalse(delayedCall.active())
+ self.assertNotIn("foo", self.pm.murder)
+
+
+ def test_connectionLostProtocolDeletion(self):
+ """
+ L{ProcessMonitor.connectionLost} removes the corresponding
+ ProcessProtocol instance from the L{ProcessMonitor.protocols} list.
+ """
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ self.assertIn("foo", self.pm.protocols)
+ self.pm.protocols["foo"].transport.signalProcess("KILL")
+ self.reactor.advance(
+ self.pm.protocols["foo"].transport._terminationDelay)
+ self.assertNotIn("foo", self.pm.protocols)
+
+
+ def test_connectionLostMinMaxRestartDelay(self):
+ """
+ L{ProcessMonitor.connectionLost} will wait at least minRestartDelay s
+ and at most maxRestartDelay s
+ """
+ self.pm.minRestartDelay = 2
+ self.pm.maxRestartDelay = 3
+
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+
+ self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
+ self.reactor.advance(self.pm.threshold - 1)
+ self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
+ self.assertEqual(self.pm.delay["foo"], self.pm.maxRestartDelay)
+
+
+ def test_connectionLostBackoffDelayDoubles(self):
+ """
+ L{ProcessMonitor.connectionLost} doubles the restart delay each time
+ the process dies too quickly.
+ """
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ self.reactor.advance(self.pm.threshold - 1) #9s
+ self.assertIn("foo", self.pm.protocols)
+ self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay)
+ # process dies within the threshold and should not restart immediately
+ self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
+ self.assertEqual(self.pm.delay["foo"], self.pm.minRestartDelay * 2)
+
+
+ def test_startService(self):
+ """
+ L{ProcessMonitor.startService} starts all monitored processes.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ # Schedule the process to start
+ self.pm.startService()
+ # advance the reactor to start the process
+ self.reactor.advance(0)
+ self.assertTrue("foo" in self.pm.protocols)
+
+
+ def test_stopService(self):
+ """
+ L{ProcessMonitor.stopService} should stop all monitored processes.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ self.pm.addProcess("bar", ["bar"])
+ # Schedule the process to start
+ self.pm.startService()
+ # advance the reactor to start the processes
+ self.reactor.advance(self.pm.threshold)
+ self.assertIn("foo", self.pm.protocols)
+ self.assertIn("bar", self.pm.protocols)
+
+ self.reactor.advance(1)
+
+ self.pm.stopService()
+ # Advance to beyond the killTime - all monitored processes
+ # should have exited
+ self.reactor.advance(self.pm.killTime + 1)
+ # The processes shouldn't be restarted
+ self.assertEqual({}, self.pm.protocols)
+
+
+ def test_stopServiceCancelRestarts(self):
+ """
+ L{ProcessMonitor.stopService} should cancel any scheduled process
+ restarts.
+ """
+ self.pm.addProcess("foo", ["foo"])
+ # Schedule the process to start
+ self.pm.startService()
+ # advance the reactor to start the processes
+ self.reactor.advance(self.pm.threshold)
+ self.assertIn("foo", self.pm.protocols)
+
+ self.reactor.advance(1)
+ # Kill the process early
+ self.pm.protocols["foo"].processEnded(Failure(ProcessDone(0)))
+ self.assertTrue(self.pm.restart['foo'].active())
+ self.pm.stopService()
+ # Scheduled restart should have been cancelled
+ self.assertFalse(self.pm.restart['foo'].active())
+
+
+ def test_stopServiceCleanupScheduledRestarts(self):
+ """
+ L{ProcessMonitor.stopService} should cancel all scheduled process
+ restarts.
+ """
+ self.pm.threshold = 5
+ self.pm.minRestartDelay = 5
+ # Start service and add a process (started immediately)
+ self.pm.startService()
+ self.pm.addProcess("foo", ["foo"])
+ # Stop the process after 1s
+ self.reactor.advance(1)
+ self.pm.stopProcess("foo")
+ # Wait 1s for it to exit it will be scheduled to restart 5s later
+ self.reactor.advance(1)
+ # Meanwhile stop the service
+ self.pm.stopService()
+ # Advance to beyond the process restart time
+ self.reactor.advance(6)
+ # The process shouldn't have restarted because stopService has cancelled
+ # all pending process restarts.
+ self.assertEqual(self.pm.protocols, {})
+
diff --git a/twisted/runner/test/test_procmontap.py b/twisted/runner/test/test_procmontap.py
new file mode 100644
index 0000000..de394f4
--- /dev/null
+++ b/twisted/runner/test/test_procmontap.py
@@ -0,0 +1,87 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.runner.procmontap}.
+"""
+
+from twisted.python.usage import UsageError
+from twisted.trial import unittest
+from twisted.runner.procmon import ProcessMonitor
+from twisted.runner import procmontap as tap
+
+
+class ProcessMonitorTapTest(unittest.TestCase):
+ """
+ Tests for L{twisted.runner.procmontap}'s option parsing and makeService
+ method.
+ """
+
+ def test_commandLineRequired(self):
+ """
+ The command line arguments must be provided.
+ """
+ opt = tap.Options()
+ self.assertRaises(UsageError, opt.parseOptions, [])
+
+
+ def test_threshold(self):
+ """
+ The threshold option is recognised as a parameter and coerced to
+ float.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--threshold', '7.5', 'foo'])
+ self.assertEqual(opt['threshold'], 7.5)
+
+
+ def test_killTime(self):
+ """
+ The killtime option is recognised as a parameter and coerced to float.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--killtime', '7.5', 'foo'])
+ self.assertEqual(opt['killtime'], 7.5)
+
+
+ def test_minRestartDelay(self):
+ """
+ The minrestartdelay option is recognised as a parameter and coerced to
+ float.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--minrestartdelay', '7.5', 'foo'])
+ self.assertEqual(opt['minrestartdelay'], 7.5)
+
+
+ def test_maxRestartDelay(self):
+ """
+ The maxrestartdelay option is recognised as a parameter and coerced to
+ float.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--maxrestartdelay', '7.5', 'foo'])
+ self.assertEqual(opt['maxrestartdelay'], 7.5)
+
+
+ def test_parameterDefaults(self):
+ """
+ The parameters all have default values
+ """
+ opt = tap.Options()
+ opt.parseOptions(['foo'])
+ self.assertEqual(opt['threshold'], 1)
+ self.assertEqual(opt['killtime'], 5)
+ self.assertEqual(opt['minrestartdelay'], 1)
+ self.assertEqual(opt['maxrestartdelay'], 3600)
+
+
+ def test_makeService(self):
+ """
+ The command line gets added as a process to the ProcessMontor.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['ping', '-c', '3', '8.8.8.8'])
+ s = tap.makeService(opt)
+ self.assertIsInstance(s, ProcessMonitor)
+ self.assertIn('ping -c 3 8.8.8.8', s.processes)
diff --git a/twisted/runner/topfiles/NEWS b/twisted/runner/topfiles/NEWS
new file mode 100644
index 0000000..0d71ae6
--- /dev/null
+++ b/twisted/runner/topfiles/NEWS
@@ -0,0 +1,101 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Runner 12.1.0 (2012-06-02)
+==================================
+
+Deprecations and Removals
+-------------------------
+ - ProcessMonitor.active, consistencyDelay, and consistency in
+ twisted.runner.procmon were deprecated since 10.1 have been
+ removed. (#5517)
+
+
+Twisted Runner 12.0.0 (2012-02-10)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 11.1.0 (2011-11-15)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 11.0.0 (2011-04-01)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 10.2.0 (2010-11-29)
+==================================
+
+No significant changes have been made for this release.
+
+
+Twisted Runner 10.1.0 (2010-06-27)
+==================================
+
+Features
+--------
+ - twistd now has a procmon subcommand plugin - a convenient way to
+ monitor and automatically restart another process. (#4356)
+
+Deprecations and Removals
+-------------------------
+ - twisted.runner.procmon.ProcessMonitor's active, consistency, and
+ consistencyDelay attributes are now deprecated. (#1763)
+
+Other
+-----
+ - #3775
+
+
+Twisted Runner 10.0.0 (2010-03-01)
+==================================
+
+Other
+-----
+ - #3961
+
+
+Twisted Runner 9.0.0 (2009-11-24)
+=================================
+
+Features
+--------
+ - procmon.ProcessMonitor.addProcess now accepts an 'env' parameter which allows
+ users to specify the environment in which a process will be run (#3691)
+
+Other
+-----
+ - #3540
+
+
+Runner 8.2.0 (2008-12-16)
+=========================
+
+No interesting changes since Twisted 8.0.
+
+8.0.0 (2008-03-17)
+==================
+
+Misc
+----
+ - Remove all "API Stability" markers (#2847)
+
+
+0.2.0 (2006-05-24)
+==================
+
+Fixes
+-----
+ - Fix a bug that broke inetdtap.RPCServer.
+ - Misc: #1142
+
+
+0.1.0
+=====
+ - Pass *blocking* sockets to subprocesses run by inetd
diff --git a/twisted/runner/topfiles/README b/twisted/runner/topfiles/README
new file mode 100644
index 0000000..f0f0571
--- /dev/null
+++ b/twisted/runner/topfiles/README
@@ -0,0 +1,3 @@
+Twisted Runner 12.1.0
+
+Twisted Runner depends on Twisted.
diff --git a/twisted/runner/topfiles/setup.py b/twisted/runner/topfiles/setup.py
new file mode 100644
index 0000000..27f65d3
--- /dev/null
+++ b/twisted/runner/topfiles/setup.py
@@ -0,0 +1,35 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+try:
+ from twisted.python.dist import setup, ConditionalExtension as Extension
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+extensions = [
+ Extension("twisted.runner.portmap",
+ ["twisted/runner/portmap.c"],
+ condition=lambda builder: builder._check_header("rpc/rpc.h")),
+]
+
+if __name__ == '__main__':
+ setup(
+ twisted_subproject="runner",
+ # metadata
+ name="Twisted Runner",
+ description="Twisted Runner is a process management library and inetd "
+ "replacement.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Andrew Bennetts",
+ url="http://twistedmatrix.com/trac/wiki/TwistedRunner",
+ license="MIT",
+ long_description="""\
+Twisted Runner contains code useful for persistent process management
+with Python and Twisted, and has an almost full replacement for inetd.
+""",
+ # build stuff
+ conditionalExtensions=extensions,
+ )
diff --git a/twisted/scripts/__init__.py b/twisted/scripts/__init__.py
new file mode 100644
index 0000000..fcde968
--- /dev/null
+++ b/twisted/scripts/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Subpackage containing the modules that implement the command line tools.
+
+Note that these are imported by top-level scripts which are intended to be
+invoked directly from a shell.
+"""
+
+from twisted.python.versions import Version
+from twisted.python.deprecate import deprecatedModuleAttribute
+
+
+deprecatedModuleAttribute(
+ Version("Twisted", 11, 1, 0),
+ "Seek unzipping software outside of Twisted.",
+ __name__,
+ "tkunzip")
+
+deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "tapconvert has been deprecated.",
+ __name__,
+ "tapconvert")
+
+del Version, deprecatedModuleAttribute
diff --git a/twisted/scripts/_twistd_unix.py b/twisted/scripts/_twistd_unix.py
new file mode 100644
index 0000000..786249b
--- /dev/null
+++ b/twisted/scripts/_twistd_unix.py
@@ -0,0 +1,349 @@
+# -*- test-case-name: twisted.test.test_twistd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import os, errno, sys
+
+from twisted.python import log, syslog, logfile, usage
+from twisted.python.util import switchUID, uidFromString, gidFromString
+from twisted.application import app, service
+from twisted.internet.interfaces import IReactorDaemonize
+from twisted import copyright
+
+
+def _umask(value):
+ return int(value, 8)
+
+
+class ServerOptions(app.ServerOptions):
+ synopsis = "Usage: twistd [options]"
+
+ optFlags = [['nodaemon','n', "don't daemonize, don't use default umask of 0077"],
+ ['originalname', None, "Don't try to change the process name"],
+ ['syslog', None, "Log to syslog, not to file"],
+ ['euid', '',
+ "Set only effective user-id rather than real user-id. "
+ "(This option has no effect unless the server is running as "
+ "root, in which case it means not to shed all privileges "
+ "after binding ports, retaining the option to regain "
+ "privileges in cases such as spawning processes. "
+ "Use with caution.)"],
+ ]
+
+ optParameters = [
+ ['prefix', None,'twisted',
+ "use the given prefix when syslogging"],
+ ['pidfile','','twistd.pid',
+ "Name of the pidfile"],
+ ['chroot', None, None,
+ 'Chroot to a supplied directory before running'],
+ ['uid', 'u', None, "The uid to run as.", uidFromString],
+ ['gid', 'g', None, "The gid to run as.", gidFromString],
+ ['umask', None, None,
+ "The (octal) file creation mask to apply.", _umask],
+ ]
+
+ compData = usage.Completions(
+ optActions={"pidfile": usage.CompleteFiles("*.pid"),
+ "chroot": usage.CompleteDirs(descr="chroot directory"),
+ "gid": usage.CompleteGroups(descr="gid to run as"),
+ "uid": usage.CompleteUsernames(descr="uid to run as"),
+ "prefix": usage.Completer(descr="syslog prefix"),
+ },
+ )
+
+ def opt_version(self):
+ """Print version information and exit.
+ """
+ print 'twistd (the Twisted daemon) %s' % copyright.version
+ print copyright.copyright
+ sys.exit()
+
+
+ def postOptions(self):
+ app.ServerOptions.postOptions(self)
+ if self['pidfile']:
+ self['pidfile'] = os.path.abspath(self['pidfile'])
+
+
+def checkPID(pidfile):
+ if not pidfile:
+ return
+ if os.path.exists(pidfile):
+ try:
+ pid = int(open(pidfile).read())
+ except ValueError:
+ sys.exit('Pidfile %s contains non-numeric value' % pidfile)
+ try:
+ os.kill(pid, 0)
+ except OSError, why:
+ if why[0] == errno.ESRCH:
+ # The pid doesnt exists.
+ log.msg('Removing stale pidfile %s' % pidfile, isError=True)
+ os.remove(pidfile)
+ else:
+ sys.exit("Can't check status of PID %s from pidfile %s: %s" %
+ (pid, pidfile, why[1]))
+ else:
+ sys.exit("""\
+Another twistd server is running, PID %s\n
+This could either be a previously started instance of your application or a
+different application entirely. To start a new one, either run it in some other
+directory, or use the --pidfile and --logfile parameters to avoid clashes.
+""" % pid)
+
+
+
+class UnixAppLogger(app.AppLogger):
+ """
+ A logger able to log to syslog, to files, and to stdout.
+
+ @ivar _syslog: A flag indicating whether to use syslog instead of file
+ logging.
+ @type _syslog: C{bool}
+
+ @ivar _syslogPrefix: If C{sysLog} is C{True}, the string prefix to use for
+ syslog messages.
+ @type _syslogPrefix: C{str}
+
+ @ivar _nodaemon: A flag indicating the process will not be daemonizing.
+ @type _nodaemon: C{bool}
+ """
+
+ def __init__(self, options):
+ app.AppLogger.__init__(self, options)
+ self._syslog = options.get("syslog", False)
+ self._syslogPrefix = options.get("prefix", "")
+ self._nodaemon = options.get("nodaemon", False)
+
+
+ def _getLogObserver(self):
+ """
+ Create and return a suitable log observer for the given configuration.
+
+ The observer will go to syslog using the prefix C{_syslogPrefix} if
+ C{_syslog} is true. Otherwise, it will go to the file named
+ C{_logfilename} or, if C{_nodaemon} is true and C{_logfilename} is
+ C{"-"}, to stdout.
+
+ @return: An object suitable to be passed to C{log.addObserver}.
+ """
+ if self._syslog:
+ return syslog.SyslogObserver(self._syslogPrefix).emit
+
+ if self._logfilename == '-':
+ if not self._nodaemon:
+ sys.exit('Daemons cannot log to stdout, exiting!')
+ logFile = sys.stdout
+ elif self._nodaemon and not self._logfilename:
+ logFile = sys.stdout
+ else:
+ if not self._logfilename:
+ self._logfilename = 'twistd.log'
+ logFile = logfile.LogFile.fromFullPath(self._logfilename)
+ try:
+ import signal
+ except ImportError:
+ pass
+ else:
+ # Override if signal is set to None or SIG_DFL (0)
+ if not signal.getsignal(signal.SIGUSR1):
+ def rotateLog(signal, frame):
+ from twisted.internet import reactor
+ reactor.callFromThread(logFile.rotate)
+ signal.signal(signal.SIGUSR1, rotateLog)
+ return log.FileLogObserver(logFile).emit
+
+
+
+def daemonize(reactor, os):
+ """
+ Daemonizes the application on Unix. This is done by the usual double
+ forking approach.
+
+ @see: U{http://code.activestate.com/recipes/278731/}
+ @see: W. Richard Stevens, "Advanced Programming in the Unix Environment",
+ 1992, Addison-Wesley, ISBN 0-201-56317-7
+
+ @param reactor: The reactor in use. If it provides L{IReactorDaemonize},
+ its daemonization-related callbacks will be invoked.
+
+ @param os: An object like the os module to use to perform the daemonization.
+ """
+
+ ## If the reactor requires hooks to be called for daemonization, call them.
+ ## Currently the only reactor which provides/needs that is KQueueReactor.
+ if IReactorDaemonize.providedBy(reactor):
+ reactor.beforeDaemonize()
+
+ if os.fork(): # launch child and...
+ os._exit(0) # kill off parent
+ os.setsid()
+ if os.fork(): # launch child and...
+ os._exit(0) # kill off parent again.
+ null = os.open('/dev/null', os.O_RDWR)
+ for i in range(3):
+ try:
+ os.dup2(null, i)
+ except OSError, e:
+ if e.errno != errno.EBADF:
+ raise
+ os.close(null)
+
+ if IReactorDaemonize.providedBy(reactor):
+ reactor.afterDaemonize()
+
+
+
+def launchWithName(name):
+ if name and name != sys.argv[0]:
+ exe = os.path.realpath(sys.executable)
+ log.msg('Changing process name to ' + name)
+ os.execv(exe, [name, sys.argv[0], '--originalname'] + sys.argv[1:])
+
+
+
+class UnixApplicationRunner(app.ApplicationRunner):
+ """
+ An ApplicationRunner which does Unix-specific things, like fork,
+ shed privileges, and maintain a PID file.
+ """
+ loggerFactory = UnixAppLogger
+
+ def preApplication(self):
+ """
+ Do pre-application-creation setup.
+ """
+ checkPID(self.config['pidfile'])
+ self.config['nodaemon'] = (self.config['nodaemon']
+ or self.config['debug'])
+ self.oldstdout = sys.stdout
+ self.oldstderr = sys.stderr
+
+
+ def postApplication(self):
+ """
+ To be called after the application is created: start the
+ application and run the reactor. After the reactor stops,
+ clean up PID files and such.
+ """
+ self.startApplication(self.application)
+ self.startReactor(None, self.oldstdout, self.oldstderr)
+ self.removePID(self.config['pidfile'])
+
+
+ def removePID(self, pidfile):
+ """
+ Remove the specified PID file, if possible. Errors are logged, not
+ raised.
+
+ @type pidfile: C{str}
+ @param pidfile: The path to the PID tracking file.
+ """
+ if not pidfile:
+ return
+ try:
+ os.unlink(pidfile)
+ except OSError, e:
+ if e.errno == errno.EACCES or e.errno == errno.EPERM:
+ log.msg("Warning: No permission to delete pid file")
+ else:
+ log.err(e, "Failed to unlink PID file")
+ except:
+ log.err(None, "Failed to unlink PID file")
+
+
+ def setupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
+ """
+ Set the filesystem root, the working directory, and daemonize.
+
+ @type chroot: C{str} or L{NoneType}
+ @param chroot: If not None, a path to use as the filesystem root (using
+ L{os.chroot}).
+
+ @type rundir: C{str}
+ @param rundir: The path to set as the working directory.
+
+ @type nodaemon: C{bool}
+ @param nodaemon: A flag which, if set, indicates that daemonization
+ should not be done.
+
+ @type umask: C{int} or L{NoneType}
+ @param umask: The value to which to change the process umask.
+
+ @type pidfile: C{str} or L{NoneType}
+ @param pidfile: If not C{None}, the path to a file into which to put
+ the PID of this process.
+ """
+ daemon = not nodaemon
+
+ if chroot is not None:
+ os.chroot(chroot)
+ if rundir == '.':
+ rundir = '/'
+ os.chdir(rundir)
+ if daemon and umask is None:
+ umask = 077
+ if umask is not None:
+ os.umask(umask)
+ if daemon:
+ from twisted.internet import reactor
+ daemonize(reactor, os)
+ if pidfile:
+ f = open(pidfile,'wb')
+ f.write(str(os.getpid()))
+ f.close()
+
+
+ def shedPrivileges(self, euid, uid, gid):
+ """
+ Change the UID and GID or the EUID and EGID of this process.
+
+ @type euid: C{bool}
+ @param euid: A flag which, if set, indicates that only the I{effective}
+ UID and GID should be set.
+
+ @type uid: C{int} or C{NoneType}
+ @param uid: If not C{None}, the UID to which to switch.
+
+ @type gid: C{int} or C{NoneType}
+ @param gid: If not C{None}, the GID to which to switch.
+ """
+ if uid is not None or gid is not None:
+ extra = euid and 'e' or ''
+ desc = '%suid/%sgid %s/%s' % (extra, extra, uid, gid)
+ try:
+ switchUID(uid, gid, euid)
+ except OSError:
+ log.msg('failed to set %s (are you root?) -- exiting.' % desc)
+ sys.exit(1)
+ else:
+ log.msg('set %s' % desc)
+
+
+ def startApplication(self, application):
+ """
+ Configure global process state based on the given application and run
+ the application.
+
+ @param application: An object which can be adapted to
+ L{service.IProcess} and L{service.IService}.
+ """
+ process = service.IProcess(application)
+ if not self.config['originalname']:
+ launchWithName(process.processName)
+ self.setupEnvironment(
+ self.config['chroot'], self.config['rundir'],
+ self.config['nodaemon'], self.config['umask'],
+ self.config['pidfile'])
+
+ service.IService(application).privilegedStartService()
+
+ uid, gid = self.config['uid'], self.config['gid']
+ if uid is None:
+ uid = process.uid
+ if gid is None:
+ gid = process.gid
+
+ self.shedPrivileges(self.config['euid'], uid, gid)
+ app.startApplication(application, not self.config['no_save'])
diff --git a/twisted/scripts/_twistw.py b/twisted/scripts/_twistw.py
new file mode 100644
index 0000000..153b58a
--- /dev/null
+++ b/twisted/scripts/_twistw.py
@@ -0,0 +1,50 @@
+# -*- test-case-name: twisted.test.test_twistd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.python import log
+from twisted.application import app, service, internet
+from twisted import copyright
+import sys, os
+
+
+
+class ServerOptions(app.ServerOptions):
+ synopsis = "Usage: twistd [options]"
+
+ optFlags = [['nodaemon','n', "(for backwards compatability)."],
+ ]
+
+ def opt_version(self):
+ """Print version information and exit.
+ """
+ print 'twistd (the Twisted Windows runner) %s' % copyright.version
+ print copyright.copyright
+ sys.exit()
+
+
+
+class WindowsApplicationRunner(app.ApplicationRunner):
+ """
+ An ApplicationRunner which avoids unix-specific things. No
+ forking, no PID files, no privileges.
+ """
+
+ def preApplication(self):
+ """
+ Do pre-application-creation setup.
+ """
+ self.oldstdout = sys.stdout
+ self.oldstderr = sys.stderr
+ os.chdir(self.config['rundir'])
+
+
+ def postApplication(self):
+ """
+ Start the application and run the reactor.
+ """
+ service.IService(self.application).privilegedStartService()
+ app.startApplication(self.application, not self.config['no_save'])
+ app.startApplication(internet.TimerService(0.1, lambda:None), 0)
+ self.startReactor(None, self.oldstdout, self.oldstderr)
+ log.msg("Server Shut Down.")
diff --git a/twisted/scripts/htmlizer.py b/twisted/scripts/htmlizer.py
new file mode 100644
index 0000000..4357809
--- /dev/null
+++ b/twisted/scripts/htmlizer.py
@@ -0,0 +1,69 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""HTML pretty-printing for Python source code."""
+
+__version__ = '$Revision: 1.8 $'[11:-2]
+
+from twisted.python import htmlizer, usage
+from twisted import copyright
+
+import os, sys
+
+header = '''<html><head>
+<title>%(title)s</title>
+<meta name=\"Generator\" content="%(generator)s" />
+%(alternate)s
+%(stylesheet)s
+</head>
+<body>
+'''
+footer = """</body>"""
+
+styleLink = '<link rel="stylesheet" href="%s" type="text/css" />'
+alternateLink = '<link rel="alternate" href="%(source)s" type="text/x-python" />'
+
+class Options(usage.Options):
+ synopsis = """%s [options] source.py
+ """ % (
+ os.path.basename(sys.argv[0]),)
+
+ optParameters = [
+ ('stylesheet', 's', None, "URL of stylesheet to link to."),
+ ]
+
+ compData = usage.Completions(
+ extraActions=[usage.CompleteFiles('*.py', descr='source python file')]
+ )
+
+ def parseArgs(self, filename):
+ self['filename'] = filename
+
+def run():
+ options = Options()
+ try:
+ options.parseOptions()
+ except usage.UsageError, e:
+ print str(e)
+ sys.exit(1)
+ filename = options['filename']
+ if options.get('stylesheet') is not None:
+ stylesheet = styleLink % (options['stylesheet'],)
+ else:
+ stylesheet = ''
+
+ output = open(filename + '.html', 'w')
+ try:
+ output.write(header % {
+ 'title': filename,
+ 'generator': 'htmlizer/%s' % (copyright.longversion,),
+ 'alternate': alternateLink % {'source': filename},
+ 'stylesheet': stylesheet
+ })
+ htmlizer.filter(open(filename), output,
+ htmlizer.SmallerHTMLWriter)
+ output.write(footer)
+ finally:
+ output.close()
diff --git a/twisted/scripts/manhole.py b/twisted/scripts/manhole.py
new file mode 100644
index 0000000..06adffb
--- /dev/null
+++ b/twisted/scripts/manhole.py
@@ -0,0 +1,69 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Start a L{twisted.manhole} client.
+"""
+
+import sys
+
+from twisted.python import usage
+
+def run():
+ config = MyOptions()
+ try:
+ config.parseOptions()
+ except usage.UsageError, e:
+ print str(e)
+ print str(config)
+ sys.exit(1)
+
+ run_gtk2(config)
+
+ from twisted.internet import reactor
+ reactor.run()
+
+
+def run_gtk2(config):
+ # Put these off until after we parse options, so we know what reactor
+ # to load.
+ from twisted.internet import gtk2reactor
+ gtk2reactor.install()
+
+ # Put this off until after we parse options, or else gnome eats them.
+ sys.argv[:] = ['manhole']
+ from twisted.manhole.ui import gtk2manhole
+
+ o = config.opts
+ defaults = {
+ 'host': o['host'],
+ 'port': o['port'],
+ 'identityName': o['user'],
+ 'password': o['password'],
+ 'serviceName': o['service'],
+ 'perspectiveName': o['perspective']
+ }
+ w = gtk2manhole.ManholeWindow()
+ w.setDefaults(defaults)
+ w.login()
+
+
+pbportno = 8787
+
+class MyOptions(usage.Options):
+ optParameters=[("user", "u", "guest", "username"),
+ ("password", "w", "guest"),
+ ("service", "s", "twisted.manhole", "PB Service"),
+ ("host", "h", "localhost"),
+ ("port", "p", str(pbportno)),
+ ("perspective", "P", "",
+ "PB Perspective to ask for "
+ "(if different than username)")]
+
+ compData = usage.Completions(
+ optActions={"host": usage.CompleteHostnames(),
+ "user": usage.CompleteUsernames()}
+ )
+
+if __name__ == '__main__':
+ run()
diff --git a/twisted/scripts/tap2deb.py b/twisted/scripts/tap2deb.py
new file mode 100644
index 0000000..3951adf
--- /dev/null
+++ b/twisted/scripts/tap2deb.py
@@ -0,0 +1,281 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+
+import sys, os, string, shutil
+
+from twisted.python import usage
+
+class MyOptions(usage.Options):
+ optFlags = [["unsigned", "u"]]
+ optParameters = [["tapfile", "t", "twistd.tap"],
+ ["maintainer", "m", "", "The maintainer's name and email in a specific format: "
+ "'John Doe <johndoe@example.com>'"],
+ ["protocol", "p", ""],
+ ["description", "e", ""],
+ ["long_description", "l", ""],
+ ["set-version", "V", "1.0"],
+ ["debfile", "d", None],
+ ["type", "y", "tap", "type of configuration: 'tap', 'xml, 'source' or 'python' for .tac files"]]
+
+ compData = usage.Completions(
+ optActions={
+ "type": usage.CompleteList(["tap", "xml", "source", "python"]),
+ "debfile": usage.CompleteFiles("*.deb")}
+ )
+
+ def postOptions(self):
+ if not self["maintainer"]:
+ raise usage.UsageError, "maintainer must be specified."
+
+
+type_dict = {
+'tap': 'file',
+'python': 'python',
+'source': 'source',
+'xml': 'xml',
+}
+
+def save_to_file(file, text):
+ f = open(file, 'w')
+ f.write(text)
+ f.close()
+
+
+def run():
+
+ try:
+ config = MyOptions()
+ config.parseOptions()
+ except usage.error, ue:
+ sys.exit("%s: %s" % (sys.argv[0], ue))
+
+ tap_file = config['tapfile']
+ base_tap_file = os.path.basename(config['tapfile'])
+ protocol = (config['protocol'] or os.path.splitext(base_tap_file)[0])
+ deb_file = config['debfile'] or 'twisted-'+protocol
+ version = config['set-version']
+ maintainer = config['maintainer']
+ description = config['description'] or ('A Twisted-based server for %(protocol)s' %
+ vars())
+ long_description = config['long_description'] or 'Automatically created by tap2deb'
+ twistd_option = type_dict[config['type']]
+ date = string.strip(os.popen('822-date').read())
+ directory = deb_file + '-' + version
+ python_version = '%s.%s' % sys.version_info[:2]
+
+ if os.path.exists(os.path.join('.build', directory)):
+ os.system('rm -rf %s' % os.path.join('.build', directory))
+ os.makedirs(os.path.join('.build', directory, 'debian'))
+
+ shutil.copy(tap_file, os.path.join('.build', directory))
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'README.Debian'),
+ '''This package was auto-generated by tap2deb\n''')
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'conffiles'),
+ '''\
+/etc/init.d/%(deb_file)s
+/etc/default/%(deb_file)s
+/etc/%(base_tap_file)s
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'default'),
+ '''\
+pidfile=/var/run/%(deb_file)s.pid
+rundir=/var/lib/%(deb_file)s/
+file=/etc/%(tap_file)s
+logfile=/var/log/%(deb_file)s.log
+ ''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'init.d'),
+ '''\
+#!/bin/sh
+
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+
+pidfile=/var/run/%(deb_file)s.pid \
+rundir=/var/lib/%(deb_file)s/ \
+file=/etc/%(tap_file)s \
+logfile=/var/log/%(deb_file)s.log
+
+[ -r /etc/default/%(deb_file)s ] && . /etc/default/%(deb_file)s
+
+test -x /usr/bin/twistd%(python_version)s || exit 0
+test -r $file || exit 0
+test -r /usr/share/%(deb_file)s/package-installed || exit 0
+
+
+case "$1" in
+ start)
+ echo -n "Starting %(deb_file)s: twistd"
+ start-stop-daemon --start --quiet --exec /usr/bin/twistd%(python_version)s -- \
+ --pidfile=$pidfile \
+ --rundir=$rundir \
+ --%(twistd_option)s=$file \
+ --logfile=$logfile
+ echo "."
+ ;;
+
+ stop)
+ echo -n "Stopping %(deb_file)s: twistd"
+ start-stop-daemon --stop --quiet \
+ --pidfile $pidfile
+ echo "."
+ ;;
+
+ restart)
+ $0 stop
+ $0 start
+ ;;
+
+ force-reload)
+ $0 restart
+ ;;
+
+ *)
+ echo "Usage: /etc/init.d/%(deb_file)s {start|stop|restart|force-reload}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
+''' % vars())
+
+ os.chmod(os.path.join('.build', directory, 'debian', 'init.d'), 0755)
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'postinst'),
+ '''\
+#!/bin/sh
+update-rc.d %(deb_file)s defaults >/dev/null
+invoke-rc.d %(deb_file)s start
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'prerm'),
+ '''\
+#!/bin/sh
+invoke-rc.d %(deb_file)s stop
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'postrm'),
+ '''\
+#!/bin/sh
+if [ "$1" = purge ]; then
+ update-rc.d %(deb_file)s remove >/dev/null
+fi
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'changelog'),
+ '''\
+%(deb_file)s (%(version)s) unstable; urgency=low
+
+ * Created by tap2deb
+
+ -- %(maintainer)s %(date)s
+
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'control'),
+ '''\
+Source: %(deb_file)s
+Section: net
+Priority: extra
+Maintainer: %(maintainer)s
+Build-Depends-Indep: debhelper
+Standards-Version: 3.5.6
+
+Package: %(deb_file)s
+Architecture: all
+Depends: python%(python_version)s-twisted
+Description: %(description)s
+ %(long_description)s
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'copyright'),
+ '''\
+This package was auto-debianized by %(maintainer)s on
+%(date)s
+
+It was auto-generated by tap2deb
+
+Upstream Author(s):
+Moshe Zadka <moshez@twistedmatrix.com> -- tap2deb author
+
+Copyright:
+
+Insert copyright here.
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'dirs'),
+ '''\
+etc/init.d
+etc/default
+var/lib/%(deb_file)s
+usr/share/doc/%(deb_file)s
+usr/share/%(deb_file)s
+''' % vars())
+
+ save_to_file(os.path.join('.build', directory, 'debian', 'rules'),
+ '''\
+#!/usr/bin/make -f
+
+export DH_COMPAT=1
+
+build: build-stamp
+build-stamp:
+ dh_testdir
+ touch build-stamp
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp install-stamp
+ dh_clean
+
+install: install-stamp
+install-stamp: build-stamp
+ dh_testdir
+ dh_testroot
+ dh_clean -k
+ dh_installdirs
+
+ # Add here commands to install the package into debian/tmp.
+ cp %(base_tap_file)s debian/tmp/etc/
+ cp debian/init.d debian/tmp/etc/init.d/%(deb_file)s
+ cp debian/default debian/tmp/etc/default/%(deb_file)s
+ cp debian/copyright debian/tmp/usr/share/doc/%(deb_file)s/
+ cp debian/README.Debian debian/tmp/usr/share/doc/%(deb_file)s/
+ touch debian/tmp/usr/share/%(deb_file)s/package-installed
+ touch install-stamp
+
+binary-arch: build install
+
+binary-indep: build install
+ dh_testdir
+ dh_testroot
+ dh_strip
+ dh_compress
+ dh_installchangelogs
+ dh_fixperms
+ dh_installdeb
+ dh_shlibdeps
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+source diff:
+ @echo >&2 'source and diff are obsolete - use dpkg-source -b'; false
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install
+''' % vars())
+
+ os.chmod(os.path.join('.build', directory, 'debian', 'rules'), 0755)
+
+ os.chdir('.build/%(directory)s' % vars())
+ os.system('dpkg-buildpackage -rfakeroot'+ ['', ' -uc -us'][config['unsigned']])
+
+if __name__ == '__main__':
+ run()
+
diff --git a/twisted/scripts/tap2rpm.py b/twisted/scripts/tap2rpm.py
new file mode 100755
index 0000000..30149b7
--- /dev/null
+++ b/twisted/scripts/tap2rpm.py
@@ -0,0 +1,331 @@
+# -*- test-case-name: twisted.scripts.test.test_tap2rpm -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, os, shutil, time, glob
+import subprocess
+import tempfile
+import tarfile
+from StringIO import StringIO
+import warnings
+
+from twisted.python import usage, log, versions, deprecate
+
+
+#################################
+# data that goes in /etc/inittab
+initFileData = '''\
+#!/bin/sh
+#
+# Startup script for a Twisted service.
+#
+# chkconfig: - 85 15
+# description: Start-up script for the Twisted service "%(tap_file)s".
+
+PATH=/usr/bin:/bin:/usr/sbin:/sbin
+
+pidfile=/var/run/%(rpm_file)s.pid
+rundir=/var/lib/twisted-taps/%(rpm_file)s/
+file=/etc/twisted-taps/%(tap_file)s
+logfile=/var/log/%(rpm_file)s.log
+
+# load init function library
+. /etc/init.d/functions
+
+[ -r /etc/default/%(rpm_file)s ] && . /etc/default/%(rpm_file)s
+
+# check for required files
+if [ ! -x /usr/bin/twistd ]
+then
+ echo "$0: Aborting, no /usr/bin/twistd found"
+ exit 0
+fi
+if [ ! -r "$file" ]
+then
+ echo "$0: Aborting, no file $file found."
+ exit 0
+fi
+
+# set up run directory if necessary
+if [ ! -d "${rundir}" ]
+then
+ mkdir -p "${rundir}"
+fi
+
+
+case "$1" in
+ start)
+ echo -n "Starting %(rpm_file)s: twistd"
+ daemon twistd \\
+ --pidfile=$pidfile \\
+ --rundir=$rundir \\
+ --%(twistd_option)s=$file \\
+ --logfile=$logfile
+ status %(rpm_file)s
+ ;;
+
+ stop)
+ echo -n "Stopping %(rpm_file)s: twistd"
+ kill `cat "${pidfile}"`
+ status %(rpm_file)s
+ ;;
+
+ restart)
+ "${0}" stop
+ "${0}" start
+ ;;
+
+ *)
+ echo "Usage: ${0} {start|stop|restart|}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
+'''
+
+#######################################
+# the data for creating the spec file
+specFileData = '''\
+Summary: %(description)s
+Name: %(rpm_file)s
+Version: %(version)s
+Release: 1
+License: Unknown
+Group: Networking/Daemons
+Source: %(tarfile_basename)s
+BuildRoot: %%{_tmppath}/%%{name}-%%{version}-root
+Requires: /usr/bin/twistd
+BuildArch: noarch
+
+%%description
+%(long_description)s
+
+%%prep
+%%setup
+%%build
+
+%%install
+[ ! -z "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != '/' ] \
+ && rm -rf "$RPM_BUILD_ROOT"
+mkdir -p "$RPM_BUILD_ROOT"/etc/twisted-taps
+mkdir -p "$RPM_BUILD_ROOT"/etc/init.d
+mkdir -p "$RPM_BUILD_ROOT"/var/lib/twisted-taps
+cp "%(tap_file)s" "$RPM_BUILD_ROOT"/etc/twisted-taps/
+cp "%(rpm_file)s.init" "$RPM_BUILD_ROOT"/etc/init.d/"%(rpm_file)s"
+
+%%clean
+[ ! -z "$RPM_BUILD_ROOT" -a "$RPM_BUILD_ROOT" != '/' ] \
+ && rm -rf "$RPM_BUILD_ROOT"
+
+%%post
+/sbin/chkconfig --add %(rpm_file)s
+/sbin/chkconfig --level 35 %(rpm_file)s
+/etc/init.d/%(rpm_file)s start
+
+%%preun
+/etc/init.d/%(rpm_file)s stop
+/sbin/chkconfig --del %(rpm_file)s
+
+%%files
+%%defattr(-,root,root)
+%%attr(0755,root,root) /etc/init.d/%(rpm_file)s
+%%attr(0660,root,root) /etc/twisted-taps/%(tap_file)s
+
+%%changelog
+* %(date)s %(maintainer)s
+- Created by tap2rpm: %(rpm_file)s (%(version)s)
+'''
+
+###############################
+class MyOptions(usage.Options):
+ optFlags = [['quiet', 'q']]
+ optParameters = [
+ ["tapfile", "t", "twistd.tap"],
+ ["maintainer", "m", "tap2rpm"],
+ ["protocol", "p", None],
+ ["description", "e", None],
+ ["long_description", "l",
+ "Automatically created by tap2rpm"],
+ ["set-version", "V", "1.0"],
+ ["rpmfile", "r", None],
+ ["type", "y", "tap", "type of configuration: 'tap', 'xml, "
+ "'source' or 'python'"],
+ ]
+
+ compData = usage.Completions(
+ optActions={"type": usage.CompleteList(["tap", "xml", "source",
+ "python"]),
+ "rpmfile": usage.CompleteFiles("*.rpm")}
+ )
+
+ def postOptions(self):
+ """
+ Calculate the default values for certain command-line options.
+ """
+ # Options whose defaults depend on other parameters.
+ if self['protocol'] is None:
+ base_tapfile = os.path.basename(self['tapfile'])
+ self['protocol'] = os.path.splitext(base_tapfile)[0]
+ if self['description'] is None:
+ self['description'] = "A TCP server for %s" % (self['protocol'],)
+ if self['rpmfile'] is None:
+ self['rpmfile'] = 'twisted-%s' % (self['protocol'],)
+
+ # Values that aren't options, but are calculated from options and are
+ # handy to have around.
+ self['twistd_option'] = type_dict[self['type']]
+ self['release-name'] = '%s-%s' % (self['rpmfile'], self['set-version'])
+
+
+ def opt_unsigned(self):
+ """
+ Generate an unsigned rather than a signed RPM. (DEPRECATED; unsigned
+ is the default)
+ """
+ msg = deprecate.getDeprecationWarningString(
+ self.opt_unsigned, versions.Version("Twisted", 12, 1, 0))
+ warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
+
+ # Maintain the -u short flag
+ opt_u = opt_unsigned
+
+
+type_dict = {
+ 'tap': 'file',
+ 'python': 'python',
+ 'source': 'source',
+ 'xml': 'xml',
+}
+
+
+
+##########################
+def makeBuildDir():
+ """
+ Set up the temporary directory for building RPMs.
+
+ Returns: buildDir, a randomly-named subdirectory of baseDir.
+ """
+ tmpDir = tempfile.mkdtemp()
+ # set up initial directory contents
+ os.makedirs(os.path.join(tmpDir, 'RPMS', 'noarch'))
+ os.makedirs(os.path.join(tmpDir, 'SPECS'))
+ os.makedirs(os.path.join(tmpDir, 'BUILD'))
+ os.makedirs(os.path.join(tmpDir, 'SOURCES'))
+ os.makedirs(os.path.join(tmpDir, 'SRPMS'))
+
+ log.msg(format="Created RPM build structure in %(path)r",
+ path=tmpDir)
+ return tmpDir
+
+
+
+def setupBuildFiles(buildDir, config):
+ """
+ Create files required to build an RPM in the build directory.
+ """
+ # Create the source tarball in the SOURCES directory.
+ tarballName = "%s.tar" % (config['release-name'],)
+ tarballPath = os.path.join(buildDir, "SOURCES", tarballName)
+ tarballHandle = tarfile.open(tarballPath, "w")
+
+ sourceDirInfo = tarfile.TarInfo(config['release-name'])
+ sourceDirInfo.type = tarfile.DIRTYPE
+ sourceDirInfo.mode = 0755
+ tarballHandle.addfile(sourceDirInfo)
+
+ tapFileBase = os.path.basename(config['tapfile'])
+
+ initFileInfo = tarfile.TarInfo(
+ os.path.join(
+ config['release-name'],
+ '%s.init' % config['rpmfile'],
+ )
+ )
+ initFileInfo.type = tarfile.REGTYPE
+ initFileInfo.mode = 0755
+ initFileRealData = initFileData % {
+ 'tap_file': tapFileBase,
+ 'rpm_file': config['release-name'],
+ 'twistd_option': config['twistd_option'],
+ }
+ initFileInfo.size = len(initFileRealData)
+ tarballHandle.addfile(initFileInfo, StringIO(initFileRealData))
+
+ tapFileHandle = open(config['tapfile'], 'rb')
+ tapFileInfo = tarballHandle.gettarinfo(
+ arcname=os.path.join(config['release-name'], tapFileBase),
+ fileobj=tapFileHandle,
+ )
+ tapFileInfo.mode = 0644
+ tarballHandle.addfile(tapFileInfo, tapFileHandle)
+
+ tarballHandle.close()
+
+ log.msg(format="Created dummy source tarball %(tarballPath)r",
+ tarballPath=tarballPath)
+
+ # Create the spec file in the SPECS directory.
+ specName = "%s.spec" % (config['release-name'],)
+ specPath = os.path.join(buildDir, "SPECS", specName)
+ specHandle = open(specPath, "w")
+ specFileRealData = specFileData % {
+ 'description': config['description'],
+ 'rpm_file': config['rpmfile'],
+ 'version': config['set-version'],
+ 'tarfile_basename': tarballName,
+ 'tap_file': tapFileBase,
+ 'date': time.strftime('%a %b %d %Y', time.localtime(time.time())),
+ 'maintainer': config['maintainer'],
+ 'long_description': config['long_description'],
+ }
+ specHandle.write(specFileRealData)
+ specHandle.close()
+
+ log.msg(format="Created RPM spec file %(specPath)r",
+ specPath=specPath)
+
+ return specPath
+
+
+
+def run(options=None):
+ # parse options
+ try:
+ config = MyOptions()
+ config.parseOptions(options)
+ except usage.error, ue:
+ sys.exit("%s: %s" % (sys.argv[0], ue))
+
+ # create RPM build environment
+ tmpDir = makeBuildDir()
+ specPath = setupBuildFiles(tmpDir, config)
+
+ # build rpm
+ job = subprocess.Popen([
+ "rpmbuild",
+ "-vv",
+ "--define", "_topdir %s" % (tmpDir,),
+ "-ba", specPath,
+ ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ stdout, _ = job.communicate()
+
+ # If there was a problem, show people what it was.
+ if job.returncode != 0:
+ print stdout
+
+ # copy the RPMs to the local directory
+ rpmPath = glob.glob(os.path.join(tmpDir, 'RPMS', 'noarch', '*'))[0]
+ srpmPath = glob.glob(os.path.join(tmpDir, 'SRPMS', '*'))[0]
+ if not config['quiet']:
+ print 'Writing "%s"...' % os.path.basename(rpmPath)
+ shutil.copy(rpmPath, '.')
+ if not config['quiet']:
+ print 'Writing "%s"...' % os.path.basename(srpmPath)
+ shutil.copy(srpmPath, '.')
+
+ # remove the build directory
+ shutil.rmtree(tmpDir)
+
+ return [os.path.basename(rpmPath), os.path.basename(srpmPath)]
diff --git a/twisted/scripts/tapconvert.py b/twisted/scripts/tapconvert.py
new file mode 100644
index 0000000..4c994a0
--- /dev/null
+++ b/twisted/scripts/tapconvert.py
@@ -0,0 +1,57 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, getpass
+
+from twisted.python import usage
+from twisted.application import app
+from twisted.persisted import sob
+
+
+class ConvertOptions(usage.Options):
+ synopsis = "Usage: tapconvert [options]"
+ optParameters = [
+ ['in', 'i', None, "The filename of the tap to read from"],
+ ['out', 'o', None, "A filename to write the tap to"],
+ ['typein', 'f', 'guess',
+ "The format to use; this can be 'guess', 'python', "
+ "'pickle', 'xml', or 'source'."],
+ ['typeout', 't', 'source',
+ "The output format to use; this can be 'pickle', 'xml', or 'source'."],
+ ]
+
+ optFlags = [
+ ['decrypt', 'd', "The specified tap/aos/xml file is encrypted."],
+ ['encrypt', 'e', "Encrypt file before writing"]
+ ]
+
+ compData = usage.Completions(
+ optActions={"typein": usage.CompleteList(["guess", "python", "pickle",
+ "xml", "source"]),
+ "typeout": usage.CompleteList(["pickle", "xml", "source"]),
+ "in": usage.CompleteFiles(descr="tap file to read from"),
+ "out": usage.CompleteFiles(descr="tap file to write to"),
+ }
+ )
+
+ def postOptions(self):
+ if self['in'] is None:
+ raise usage.UsageError("%s\nYou must specify the input filename."
+ % self)
+ if self["typein"] == "guess":
+ try:
+ self["typein"] = sob.guessType(self["in"])
+ except KeyError:
+ raise usage.UsageError("Could not guess type for '%s'" %
+ self["typein"])
+
+def run():
+ options = ConvertOptions()
+ try:
+ options.parseOptions(sys.argv[1:])
+ except usage.UsageError, e:
+ print e
+ else:
+ app.convertStyle(options["in"], options["typein"],
+ options.opts['decrypt'] or getpass.getpass('Passphrase: '),
+ options["out"], options['typeout'], options["encrypt"])
diff --git a/twisted/scripts/test/__init__.py b/twisted/scripts/test/__init__.py
new file mode 100644
index 0000000..c04ed1c
--- /dev/null
+++ b/twisted/scripts/test/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test package for L{twisted.scripts}.
+"""
diff --git a/twisted/scripts/test/test_scripts.py b/twisted/scripts/test/test_scripts.py
new file mode 100644
index 0000000..eeb1d1a
--- /dev/null
+++ b/twisted/scripts/test/test_scripts.py
@@ -0,0 +1,178 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the command-line scripts in the top-level I{bin/} directory.
+
+Tests for actual functionality belong elsewhere, written in a way that doesn't
+involve launching child processes.
+"""
+
+from os import devnull, getcwd, chdir
+from sys import executable
+from subprocess import PIPE, Popen
+
+from twisted.trial.unittest import SkipTest, TestCase
+from twisted.python.modules import getModule
+from twisted.python.filepath import FilePath
+from twisted.python.test.test_shellcomp import ZshScriptTestMixin
+
+
+class ScriptTestsMixin:
+ """
+ Mixin for L{TestCase} subclasses which defines a helper function for testing
+ a Twisted-using script.
+ """
+ bin = getModule("twisted").pathEntry.filePath.child("bin")
+
+ def scriptTest(self, name):
+ """
+ Verify that the given script runs and uses the version of Twisted
+ currently being tested.
+
+ This only works when running tests against a vcs checkout of Twisted,
+ since it relies on the scripts being in the place they are kept in
+ version control, and exercises their logic for finding the right version
+ of Twisted to use in that situation.
+
+ @param name: A path fragment, relative to the I{bin} directory of a
+ Twisted source checkout, identifying a script to test.
+ @type name: C{str}
+
+ @raise SkipTest: if the script is not where it is expected to be.
+ """
+ script = self.bin.preauthChild(name)
+ if not script.exists():
+ raise SkipTest(
+ "Script tests do not apply to installed configuration.")
+
+ from twisted.copyright import version
+ scriptVersion = Popen(
+ [executable, script.path, '--version'],
+ stdout=PIPE, stderr=file(devnull)).stdout.read()
+
+ self.assertIn(str(version), scriptVersion)
+
+
+
+class ScriptTests(TestCase, ScriptTestsMixin):
+ """
+ Tests for the core scripts.
+ """
+ def test_twistd(self):
+ self.scriptTest("twistd")
+
+
+ def test_twistdPathInsert(self):
+ """
+ The twistd script adds the current working directory to sys.path so
+ that it's able to import modules from it.
+ """
+ script = self.bin.child("twistd")
+ if not script.exists():
+ raise SkipTest(
+ "Script tests do not apply to installed configuration.")
+ cwd = getcwd()
+ self.addCleanup(chdir, cwd)
+ testDir = FilePath(self.mktemp())
+ testDir.makedirs()
+ chdir(testDir.path)
+ testDir.child("bar.tac").setContent(
+ "import sys\n"
+ "print sys.path\n")
+ output = Popen(
+ [executable, script.path, '-ny', 'bar.tac'],
+ stdout=PIPE, stderr=file(devnull)).stdout.read()
+ self.assertIn(repr(testDir.path), output)
+
+
+ def test_manhole(self):
+ self.scriptTest("manhole")
+
+
+ def test_trial(self):
+ self.scriptTest("trial")
+
+
+ def test_trialPathInsert(self):
+ """
+ The trial script adds the current working directory to sys.path so that
+ it's able to import modules from it.
+ """
+ script = self.bin.child("trial")
+ if not script.exists():
+ raise SkipTest(
+ "Script tests do not apply to installed configuration.")
+ cwd = getcwd()
+ self.addCleanup(chdir, cwd)
+ testDir = FilePath(self.mktemp())
+ testDir.makedirs()
+ chdir(testDir.path)
+ testDir.child("foo.py").setContent("")
+ output = Popen(
+ [executable, script.path, 'foo'],
+ stdout=PIPE, stderr=file(devnull)).stdout.read()
+ self.assertIn("PASSED", output)
+
+
+ def test_pyhtmlizer(self):
+ self.scriptTest("pyhtmlizer")
+
+
+ def test_tap2rpm(self):
+ self.scriptTest("tap2rpm")
+
+
+ def test_tap2deb(self):
+ self.scriptTest("tap2deb")
+
+
+ def test_tapconvert(self):
+ self.scriptTest("tapconvert")
+
+
+ def test_deprecatedTkunzip(self):
+ """
+ The entire L{twisted.scripts.tkunzip} module, part of the old Windows
+ installer tool chain, is deprecated.
+ """
+ from twisted.scripts import tkunzip
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedTkunzip])
+ self.assertEqual(DeprecationWarning, warnings[0]['category'])
+ self.assertEqual(
+ "twisted.scripts.tkunzip was deprecated in Twisted 11.1.0: "
+ "Seek unzipping software outside of Twisted.",
+ warnings[0]['message'])
+ self.assertEqual(1, len(warnings))
+
+
+ def test_deprecatedTapconvert(self):
+ """
+ The entire L{twisted.scripts.tapconvert} module is deprecated.
+ """
+ from twisted.scripts import tapconvert
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedTapconvert])
+ self.assertEqual(DeprecationWarning, warnings[0]['category'])
+ self.assertEqual(
+ "twisted.scripts.tapconvert was deprecated in Twisted 12.1.0: "
+ "tapconvert has been deprecated.",
+ warnings[0]['message'])
+ self.assertEqual(1, len(warnings))
+
+
+
+class ZshIntegrationTestCase(TestCase, ZshScriptTestMixin):
+ """
+ Test that zsh completion functions are generated without error
+ """
+ generateFor = [('twistd', 'twisted.scripts.twistd.ServerOptions'),
+ ('trial', 'twisted.scripts.trial.Options'),
+ ('pyhtmlizer', 'twisted.scripts.htmlizer.Options'),
+ ('tap2rpm', 'twisted.scripts.tap2rpm.MyOptions'),
+ ('tap2deb', 'twisted.scripts.tap2deb.MyOptions'),
+ ('tapconvert', 'twisted.scripts.tapconvert.ConvertOptions'),
+ ('manhole', 'twisted.scripts.manhole.MyOptions')
+ ]
+
diff --git a/twisted/scripts/test/test_tap2rpm.py b/twisted/scripts/test/test_tap2rpm.py
new file mode 100644
index 0000000..509e69c
--- /dev/null
+++ b/twisted/scripts/test/test_tap2rpm.py
@@ -0,0 +1,399 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.scripts.tap2rpm}.
+"""
+import os
+
+from twisted.trial.unittest import TestCase, SkipTest
+from twisted.python import procutils
+from twisted.python import versions
+from twisted.python import deprecate
+from twisted.python.failure import Failure
+from twisted.internet import utils
+from twisted.scripts import tap2rpm
+
+# When we query the RPM metadata, we get back a string we'll have to parse, so
+# we'll use suitably rare delimiter characters to split on. Luckily, ASCII
+# defines some for us!
+RECORD_SEPARATOR = "\x1E"
+UNIT_SEPARATOR = "\x1F"
+
+
+
+def _makeRPMs(tapfile=None, maintainer=None, protocol=None, description=None,
+ longDescription=None, setVersion=None, rpmfile=None, type_=None):
+ """
+ Helper function to invoke tap2rpm with the given parameters.
+ """
+ args = []
+
+ if not tapfile:
+ tapfile = "dummy-tap-file"
+ handle = open(tapfile, "w")
+ handle.write("# Dummy TAP file\n")
+ handle.close()
+
+ args.extend(["--quiet", "--tapfile", tapfile])
+
+ if maintainer:
+ args.extend(["--maintainer", maintainer])
+ if protocol:
+ args.extend(["--protocol", protocol])
+ if description:
+ args.extend(["--description", description])
+ if longDescription:
+ args.extend(["--long_description", longDescription])
+ if setVersion:
+ args.extend(["--set-version", setVersion])
+ if rpmfile:
+ args.extend(["--rpmfile", rpmfile])
+ if type_:
+ args.extend(["--type", type_])
+
+ return tap2rpm.run(args)
+
+
+
+def _queryRPMTags(rpmfile, taglist):
+ """
+ Helper function to read the given header tags from the given RPM file.
+
+ Returns a Deferred that fires with dictionary mapping a tag name to a list
+ of the associated values in the RPM header. If a tag has only a single
+ value in the header (like NAME or VERSION), it will be returned as a 1-item
+ list.
+
+ Run "rpm --querytags" to see what tags can be queried.
+ """
+
+ # Build a query format string that will return appropriately delimited
+ # results. Every field is treated as an array field, so single-value tags
+ # like VERSION will be returned as 1-item lists.
+ queryFormat = RECORD_SEPARATOR.join([
+ "[%%{%s}%s]" % (tag, UNIT_SEPARATOR) for tag in taglist
+ ])
+
+ def parseTagValues(output):
+ res = {}
+
+ for tag, values in zip(taglist, output.split(RECORD_SEPARATOR)):
+ values = values.strip(UNIT_SEPARATOR).split(UNIT_SEPARATOR)
+ res[tag] = values
+
+ return res
+
+ def checkErrorResult(failure):
+ # The current rpm packages on Debian and Ubuntu don't properly set up
+ # the RPM database, which causes rpm to print a harmless warning to
+ # stderr. Unfortunately, .getProcessOutput() assumes all warnings are
+ # catastrophic and panics whenever it sees one.
+ #
+ # See also:
+ # http://twistedmatrix.com/trac/ticket/3292#comment:42
+ # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=551669
+ # http://rpm.org/ticket/106
+
+ failure.trap(IOError)
+
+ # Depending on kernel scheduling, we might read the whole error
+ # message, or only the first few bytes.
+ if str(failure.value).startswith("got stderr: 'error: "):
+ newFailure = Failure(SkipTest("rpm is missing its package "
+ "database. Run 'sudo rpm -qa > /dev/null' to create one."))
+ else:
+ # Not the exception we were looking for; we should report the
+ # original failure.
+ newFailure = failure
+
+ # We don't want to raise the exception right away; we want to wait for
+ # the process to exit, otherwise we'll get extra useless errors
+ # reported.
+ d = failure.value.processEnded
+ d.addBoth(lambda _: newFailure)
+ return d
+
+ d = utils.getProcessOutput("rpm",
+ ("-q", "--queryformat", queryFormat, "-p", rpmfile))
+ d.addCallbacks(parseTagValues, checkErrorResult)
+ return d
+
+
+
+class TestTap2RPM(TestCase):
+
+
+ def setUp(self):
+ return self._checkForRpmbuild()
+
+
+ def _checkForRpmbuild(self):
+ """
+ tap2rpm requires rpmbuild; skip tests if rpmbuild is not present.
+ """
+ if not procutils.which("rpmbuild"):
+ raise SkipTest("rpmbuild must be present to test tap2rpm")
+
+
+ def _makeTapFile(self, basename="dummy"):
+ """
+ Make a temporary .tap file and returns the absolute path.
+ """
+ path = basename + ".tap"
+ handle = open(path, "w")
+ handle.write("# Dummy .tap file")
+ handle.close()
+ return path
+
+
+ def _verifyRPMTags(self, rpmfile, **tags):
+ """
+ Check the given file has the given tags set to the given values.
+ """
+
+ d = _queryRPMTags(rpmfile, tags.keys())
+ d.addCallback(self.assertEqual, tags)
+ return d
+
+
+ def test_optionDefaults(self):
+ """
+ Commandline options should default to sensible values.
+
+ "sensible" here is defined as "the same values that previous versions
+ defaulted to".
+ """
+ config = tap2rpm.MyOptions()
+ config.parseOptions([])
+
+ self.assertEqual(config['tapfile'], 'twistd.tap')
+ self.assertEqual(config['maintainer'], 'tap2rpm')
+ self.assertEqual(config['protocol'], 'twistd')
+ self.assertEqual(config['description'], 'A TCP server for twistd')
+ self.assertEqual(config['long_description'],
+ 'Automatically created by tap2rpm')
+ self.assertEqual(config['set-version'], '1.0')
+ self.assertEqual(config['rpmfile'], 'twisted-twistd')
+ self.assertEqual(config['type'], 'tap')
+ self.assertEqual(config['quiet'], False)
+ self.assertEqual(config['twistd_option'], 'file')
+ self.assertEqual(config['release-name'], 'twisted-twistd-1.0')
+
+
+ def test_protocolCalculatedFromTapFile(self):
+ """
+ The protocol name defaults to a value based on the tapfile value.
+ """
+ config = tap2rpm.MyOptions()
+ config.parseOptions(['--tapfile', 'pancakes.tap'])
+
+ self.assertEqual(config['tapfile'], 'pancakes.tap')
+ self.assertEqual(config['protocol'], 'pancakes')
+
+
+ def test_optionsDefaultToProtocolValue(self):
+ """
+ Many options default to a value calculated from the protocol name.
+ """
+ config = tap2rpm.MyOptions()
+ config.parseOptions([
+ '--tapfile', 'sausages.tap',
+ '--protocol', 'eggs',
+ ])
+
+ self.assertEqual(config['tapfile'], 'sausages.tap')
+ self.assertEqual(config['maintainer'], 'tap2rpm')
+ self.assertEqual(config['protocol'], 'eggs')
+ self.assertEqual(config['description'], 'A TCP server for eggs')
+ self.assertEqual(config['long_description'],
+ 'Automatically created by tap2rpm')
+ self.assertEqual(config['set-version'], '1.0')
+ self.assertEqual(config['rpmfile'], 'twisted-eggs')
+ self.assertEqual(config['type'], 'tap')
+ self.assertEqual(config['quiet'], False)
+ self.assertEqual(config['twistd_option'], 'file')
+ self.assertEqual(config['release-name'], 'twisted-eggs-1.0')
+
+
+ def test_releaseNameDefaultsToRpmfileValue(self):
+ """
+ The release-name option is calculated from rpmfile and set-version.
+ """
+ config = tap2rpm.MyOptions()
+ config.parseOptions([
+ "--rpmfile", "beans",
+ "--set-version", "1.2.3",
+ ])
+
+ self.assertEqual(config['release-name'], 'beans-1.2.3')
+
+
+ def test_basicOperation(self):
+ """
+ Calling tap2rpm should produce an RPM and SRPM with default metadata.
+ """
+ basename = "frenchtoast"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(basename))
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ NAME=["twisted-%s" % (basename,)],
+ VERSION=["1.0"],
+ RELEASE=["1"],
+ SUMMARY=["A TCP server for %s" % (basename,)],
+ DESCRIPTION=["Automatically created by tap2rpm"],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ NAME=["twisted-%s" % (basename,)],
+ VERSION=["1.0"],
+ RELEASE=["1"],
+ SUMMARY=["A TCP server for %s" % (basename,)],
+ DESCRIPTION=["Automatically created by tap2rpm"],
+ ))
+
+ return d
+
+
+ def test_protocolOverride(self):
+ """
+ Setting 'protocol' should change the name of the resulting package.
+ """
+ basename = "acorn"
+ protocol = "banana"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(basename),
+ protocol=protocol)
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ NAME=["twisted-%s" % (protocol,)],
+ SUMMARY=["A TCP server for %s" % (protocol,)],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ NAME=["twisted-%s" % (protocol,)],
+ SUMMARY=["A TCP server for %s" % (protocol,)],
+ ))
+
+ return d
+
+
+ def test_rpmfileOverride(self):
+ """
+ Setting 'rpmfile' should change the name of the resulting package.
+ """
+ basename = "cherry"
+ rpmfile = "donut"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(basename),
+ rpmfile=rpmfile)
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ NAME=[rpmfile],
+ SUMMARY=["A TCP server for %s" % (basename,)],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ NAME=[rpmfile],
+ SUMMARY=["A TCP server for %s" % (basename,)],
+ ))
+
+ return d
+
+
+ def test_descriptionOverride(self):
+ """
+ Setting 'description' should change the SUMMARY tag.
+ """
+ description = "eggplant"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(),
+ description=description)
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ SUMMARY=[description],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ SUMMARY=[description],
+ ))
+
+ return d
+
+
+ def test_longDescriptionOverride(self):
+ """
+ Setting 'longDescription' should change the DESCRIPTION tag.
+ """
+ longDescription = "fig"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(),
+ longDescription=longDescription)
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ DESCRIPTION=[longDescription],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ DESCRIPTION=[longDescription],
+ ))
+
+ return d
+
+
+ def test_setVersionOverride(self):
+ """
+ Setting 'setVersion' should change the RPM's version info.
+ """
+ version = "123.456"
+
+ # Create RPMs based on a TAP file with this name.
+ rpm, srpm = _makeRPMs(tapfile=self._makeTapFile(),
+ setVersion=version)
+
+ # Verify the resulting RPMs have the correct tags.
+ d = self._verifyRPMTags(rpm,
+ VERSION=["123.456"],
+ RELEASE=["1"],
+ )
+ d.addCallback(lambda _: self._verifyRPMTags(srpm,
+ VERSION=["123.456"],
+ RELEASE=["1"],
+ ))
+
+ return d
+
+
+ def test_tapInOtherDirectory(self):
+ """
+ tap2rpm handles tapfiles outside the current directory.
+ """
+ # Make a tapfile outside the current directory.
+ tempdir = self.mktemp()
+ os.mkdir(tempdir)
+ tapfile = self._makeTapFile(os.path.join(tempdir, "bacon"))
+
+ # Try and make an RPM from that tapfile.
+ _makeRPMs(tapfile=tapfile)
+
+
+ def test_unsignedFlagDeprecationWarning(self):
+ """
+ The 'unsigned' flag in tap2rpm should be deprecated, and its use
+ should raise a warning as such.
+ """
+ config = tap2rpm.MyOptions()
+ config.parseOptions(['--unsigned'])
+ warnings = self.flushWarnings()
+ self.assertEqual(DeprecationWarning, warnings[0]['category'])
+ self.assertEqual(
+ deprecate.getDeprecationWarningString(
+ config.opt_unsigned, versions.Version("Twisted", 12, 1, 0)),
+ warnings[0]['message'])
+ self.assertEqual(1, len(warnings))
diff --git a/twisted/scripts/tkunzip.py b/twisted/scripts/tkunzip.py
new file mode 100644
index 0000000..e17253b
--- /dev/null
+++ b/twisted/scripts/tkunzip.py
@@ -0,0 +1,292 @@
+# -*- test-case-name: twisted.scripts.test.test_scripts -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Post-install GUI to compile to pyc and unpack twisted doco
+"""
+
+from __future__ import generators
+
+import sys
+import zipfile
+import py_compile
+
+# we're going to ignore failures to import tkinter and fall back
+# to using the console if the required dll is not found
+
+# Scary kludge to work around tk84.dll bug:
+# https://sourceforge.net/tracker/index.php?func=detail&aid=814654&group_id=5470&atid=105470
+# Without which(): you get a windows missing-dll popup message
+from twisted.python.procutils import which
+tkdll='tk84.dll'
+if which(tkdll) or which('DLLs/%s' % tkdll):
+ try:
+ import Tkinter
+ from Tkinter import *
+ from twisted.internet import tksupport
+ except ImportError:
+ pass
+
+# twisted
+from twisted.internet import reactor, defer
+from twisted.python import failure, log, zipstream, util, usage, log
+# local
+import os.path
+
+class ProgressBar:
+ def __init__(self, master=None, orientation="horizontal",
+ min=0, max=100, width=100, height=18,
+ doLabel=1, appearance="sunken",
+ fillColor="blue", background="gray",
+ labelColor="yellow", labelFont="Arial",
+ labelText="", labelFormat="%d%%",
+ value=0, bd=2):
+ # preserve various values
+ self.master=master
+ self.orientation=orientation
+ self.min=min
+ self.max=max
+ self.width=width
+ self.height=height
+ self.doLabel=doLabel
+ self.fillColor=fillColor
+ self.labelFont= labelFont
+ self.labelColor=labelColor
+ self.background=background
+ self.labelText=labelText
+ self.labelFormat=labelFormat
+ self.value=value
+ self.frame=Frame(master, relief=appearance, bd=bd)
+ self.canvas=Canvas(self.frame, height=height, width=width, bd=0,
+ highlightthickness=0, background=background)
+ self.scale=self.canvas.create_rectangle(0, 0, width, height,
+ fill=fillColor)
+ self.label=self.canvas.create_text(self.canvas.winfo_reqwidth() / 2,
+ height / 2, text=labelText,
+ anchor="c", fill=labelColor,
+ font=self.labelFont)
+ self.update()
+ self.canvas.pack(side='top', fill='x', expand='no')
+
+ def pack(self, *args, **kwargs):
+ self.frame.pack(*args, **kwargs)
+
+ def updateProgress(self, newValue, newMax=None):
+ if newMax:
+ self.max = newMax
+ self.value = newValue
+ self.update()
+
+ def update(self):
+ # Trim the values to be between min and max
+ value=self.value
+ if value > self.max:
+ value = self.max
+ if value < self.min:
+ value = self.min
+ # Adjust the rectangle
+ if self.orientation == "horizontal":
+ self.canvas.coords(self.scale, 0, 0,
+ float(value) / self.max * self.width, self.height)
+ else:
+ self.canvas.coords(self.scale, 0,
+ self.height - (float(value) /
+ self.max*self.height),
+ self.width, self.height)
+ # Now update the colors
+ self.canvas.itemconfig(self.scale, fill=self.fillColor)
+ self.canvas.itemconfig(self.label, fill=self.labelColor)
+ # And update the label
+ if self.doLabel:
+ if value:
+ if value >= 0:
+ pvalue = int((float(value) / float(self.max)) *
+ 100.0)
+ else:
+ pvalue = 0
+ self.canvas.itemconfig(self.label, text=self.labelFormat
+ % pvalue)
+ else:
+ self.canvas.itemconfig(self.label, text='')
+ else:
+ self.canvas.itemconfig(self.label, text=self.labelFormat %
+ self.labelText)
+ self.canvas.update_idletasks()
+
+
+class Progressor:
+ """A base class to make it simple to hook a progress bar up to a process.
+ """
+ def __init__(self, title, *args, **kwargs):
+ self.title=title
+ self.stopping=0
+ self.bar=None
+ self.iterator=None
+ self.remaining=1000
+
+ def setBar(self, bar, max):
+ self.bar=bar
+ bar.updateProgress(0, max)
+ return self
+
+ def setIterator(self, iterator):
+ self.iterator=iterator
+ return self
+
+ def updateBar(self, deferred):
+ b=self.bar
+ try:
+ b.updateProgress(b.max - self.remaining)
+ except TclError:
+ self.stopping=1
+ except:
+ deferred.errback(failure.Failure())
+
+ def processAll(self, root):
+ assert self.bar and self.iterator, "must setBar and setIterator"
+ self.root=root
+ root.title(self.title)
+ d=defer.Deferred()
+ d.addErrback(log.err)
+ reactor.callLater(0.1, self.processOne, d)
+ return d
+
+ def processOne(self, deferred):
+ if self.stopping:
+ deferred.callback(self.root)
+ return
+
+ try:
+ self.remaining=self.iterator.next()
+ except StopIteration:
+ self.stopping=1
+ except:
+ deferred.errback(failure.Failure())
+
+ if self.remaining%10==0:
+ reactor.callLater(0, self.updateBar, deferred)
+ if self.remaining%100==0:
+ log.msg(self.remaining)
+ reactor.callLater(0, self.processOne, deferred)
+
+def compiler(path):
+ """A generator for compiling files to .pyc"""
+ def justlist(arg, directory, names):
+ pynames=[os.path.join(directory, n) for n in names
+ if n.endswith('.py')]
+ arg.extend(pynames)
+ all=[]
+ os.path.walk(path, justlist, all)
+
+ remaining=len(all)
+ i=zip(all, range(remaining-1, -1, -1))
+ for f, remaining in i:
+ py_compile.compile(f)
+ yield remaining
+
+class TkunzipOptions(usage.Options):
+ optParameters=[["zipfile", "z", "", "a zipfile"],
+ ["ziptargetdir", "t", ".", "where to extract zipfile"],
+ ["compiledir", "c", "", "a directory to compile"],
+ ]
+ optFlags=[["use-console", "C", "show in the console, not graphically"],
+ ["shell-exec", "x", """\
+spawn a new console to show output (implies -C)"""],
+ ]
+
+def countPys(countl, directory, names):
+ sofar=countl[0]
+ sofar=sofar+len([f for f in names if f.endswith('.py')])
+ countl[0]=sofar
+ return sofar
+
+def countPysRecursive(path):
+ countl=[0]
+ os.path.walk(path, countPys, countl)
+ return countl[0]
+
+def run(argv=sys.argv):
+ log.startLogging(file('tkunzip.log', 'w'))
+ opt=TkunzipOptions()
+ try:
+ opt.parseOptions(argv[1:])
+ except usage.UsageError, e:
+ print str(opt)
+ print str(e)
+ sys.exit(1)
+
+ if opt['use-console']:
+ # this should come before shell-exec to prevent infinite loop
+ return doItConsolicious(opt)
+ if opt['shell-exec'] or not 'Tkinter' in sys.modules:
+ from distutils import sysconfig
+ from twisted.scripts import tkunzip
+ myfile=tkunzip.__file__
+ exe=os.path.join(sysconfig.get_config_var('prefix'), 'python.exe')
+ return os.system('%s %s --use-console %s' % (exe, myfile,
+ ' '.join(argv[1:])))
+ return doItTkinterly(opt)
+
+def doItConsolicious(opt):
+ # reclaim stdout/stderr from log
+ sys.stdout = sys.__stdout__
+ sys.stderr = sys.__stderr__
+ if opt['zipfile']:
+ print 'Unpacking documentation...'
+ for n in zipstream.unzipIter(opt['zipfile'], opt['ziptargetdir']):
+ if n % 100 == 0:
+ print n,
+ if n % 1000 == 0:
+ print
+ print 'Done unpacking.'
+
+ if opt['compiledir']:
+ print 'Compiling to pyc...'
+ import compileall
+ compileall.compile_dir(opt["compiledir"])
+ print 'Done compiling.'
+
+def doItTkinterly(opt):
+ root=Tkinter.Tk()
+ root.withdraw()
+ root.title('One Moment.')
+ root.protocol('WM_DELETE_WINDOW', reactor.stop)
+ tksupport.install(root)
+
+ prog=ProgressBar(root, value=0, labelColor="black", width=200)
+ prog.pack()
+
+ # callback immediately
+ d=defer.succeed(root).addErrback(log.err)
+
+ def deiconify(root):
+ root.deiconify()
+ return root
+
+ d.addCallback(deiconify)
+
+ if opt['zipfile']:
+ uz=Progressor('Unpacking documentation...')
+ max=zipstream.countZipFileChunks(opt['zipfile'], 4096)
+ uz.setBar(prog, max)
+ uz.setIterator(zipstream.unzipIterChunky(opt['zipfile'],
+ opt['ziptargetdir']))
+ d.addCallback(uz.processAll)
+
+ if opt['compiledir']:
+ comp=Progressor('Compiling to pyc...')
+ comp.setBar(prog, countPysRecursive(opt['compiledir']))
+ comp.setIterator(compiler(opt['compiledir']))
+ d.addCallback(comp.processAll)
+
+ def stop(ignore):
+ reactor.stop()
+ root.destroy()
+ d.addCallback(stop)
+
+ reactor.run()
+
+
+if __name__=='__main__':
+ run()
diff --git a/twisted/scripts/trial.py b/twisted/scripts/trial.py
new file mode 100644
index 0000000..349b0c6
--- /dev/null
+++ b/twisted/scripts/trial.py
@@ -0,0 +1,389 @@
+# -*- test-case-name: twisted.trial.test.test_script -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import sys, os, random, gc, time, warnings
+
+from twisted.internet import defer
+from twisted.application import app
+from twisted.python import usage, reflect, failure, versions, deprecate
+from twisted.python.filepath import FilePath
+from twisted import plugin
+from twisted.python.util import spewer
+from twisted.python.compat import set
+from twisted.trial import runner, itrial, reporter
+
+
+# Yea, this is stupid. Leave it for for command-line compatibility for a
+# while, though.
+TBFORMAT_MAP = {
+ 'plain': 'default',
+ 'default': 'default',
+ 'emacs': 'brief',
+ 'brief': 'brief',
+ 'cgitb': 'verbose',
+ 'verbose': 'verbose'
+ }
+
+
+def _parseLocalVariables(line):
+ """
+ Accepts a single line in Emacs local variable declaration format and
+ returns a dict of all the variables {name: value}.
+ Raises ValueError if 'line' is in the wrong format.
+
+ See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
+ """
+ paren = '-*-'
+ start = line.find(paren) + len(paren)
+ end = line.rfind(paren)
+ if start == -1 or end == -1:
+ raise ValueError("%r not a valid local variable declaration" % (line,))
+ items = line[start:end].split(';')
+ localVars = {}
+ for item in items:
+ if len(item.strip()) == 0:
+ continue
+ split = item.split(':')
+ if len(split) != 2:
+ raise ValueError("%r contains invalid declaration %r"
+ % (line, item))
+ localVars[split[0].strip()] = split[1].strip()
+ return localVars
+
+
+def loadLocalVariables(filename):
+ """
+ Accepts a filename and attempts to load the Emacs variable declarations
+ from that file, simulating what Emacs does.
+
+ See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
+ """
+ f = file(filename, "r")
+ lines = [f.readline(), f.readline()]
+ f.close()
+ for line in lines:
+ try:
+ return _parseLocalVariables(line)
+ except ValueError:
+ pass
+ return {}
+
+
+def getTestModules(filename):
+ testCaseVar = loadLocalVariables(filename).get('test-case-name', None)
+ if testCaseVar is None:
+ return []
+ return testCaseVar.split(',')
+
+
+def isTestFile(filename):
+ """
+ Returns true if 'filename' looks like a file containing unit tests.
+ False otherwise. Doesn't care whether filename exists.
+ """
+ basename = os.path.basename(filename)
+ return (basename.startswith('test_')
+ and os.path.splitext(basename)[1] == ('.py'))
+
+
+def _reporterAction():
+ return usage.CompleteList([p.longOpt for p in
+ plugin.getPlugins(itrial.IReporter)])
+
+class Options(usage.Options, app.ReactorSelectionMixin):
+ synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...]
+ """ % (os.path.basename(sys.argv[0]),)
+
+ longdesc = ("trial loads and executes a suite of unit tests, obtained "
+ "from modules, packages and files listed on the command line.")
+
+ optFlags = [["help", "h"],
+ ["rterrors", "e", "realtime errors, print out tracebacks as "
+ "soon as they occur"],
+ ["debug", "b", "Run tests in the Python debugger. Will load "
+ "'.pdbrc' from current directory if it exists."],
+ ["debug-stacktraces", "B", "Report Deferred creation and "
+ "callback stack traces"],
+ ["nopm", None, "don't automatically jump into debugger for "
+ "postmorteming of exceptions"],
+ ["dry-run", 'n', "do everything but run the tests"],
+ ["force-gc", None, "Have Trial run gc.collect() before and "
+ "after each test case."],
+ ["profile", None, "Run tests under the Python profiler"],
+ ["unclean-warnings", None,
+ "Turn dirty reactor errors into warnings"],
+ ["until-failure", "u", "Repeat test until it fails"],
+ ["no-recurse", "N", "Don't recurse into packages"],
+ ['help-reporters', None,
+ "Help on available output plugins (reporters)"]
+ ]
+
+ optParameters = [
+ ["logfile", "l", "test.log", "log file name"],
+ ["random", "z", None,
+ "Run tests in random order using the specified seed"],
+ ['temp-directory', None, '_trial_temp',
+ 'Path to use as working directory for tests.'],
+ ['reporter', None, 'verbose',
+ 'The reporter to use for this test run. See --help-reporters for '
+ 'more info.']]
+
+ compData = usage.Completions(
+ optActions={"tbformat": usage.CompleteList(["plain", "emacs", "cgitb"]),
+ "reporter": _reporterAction,
+ "logfile": usage.CompleteFiles(descr="log file name"),
+ "random": usage.Completer(descr="random seed")},
+ extraActions=[usage.CompleteFiles(
+ "*.py", descr="file | module | package | TestCase | testMethod",
+ repeat=True)],
+ )
+
+ fallbackReporter = reporter.TreeReporter
+ extra = None
+ tracer = None
+
+ def __init__(self):
+ self['tests'] = set()
+ usage.Options.__init__(self)
+
+
+ def coverdir(self):
+ """
+ Return a L{FilePath} representing the directory into which coverage
+ results should be written.
+ """
+ coverdir = 'coverage'
+ result = FilePath(self['temp-directory']).child(coverdir)
+ print "Setting coverage directory to %s." % (result.path,)
+ return result
+
+
+ def opt_coverage(self):
+ """
+ Generate coverage information in the I{coverage} file in the
+ directory specified by the I{trial-temp} option.
+ """
+ import trace
+ self.tracer = trace.Trace(count=1, trace=0)
+ sys.settrace(self.tracer.globaltrace)
+
+
+ def opt_testmodule(self, filename):
+ """
+ Filename to grep for test cases (-*- test-case-name)
+ """
+ # If the filename passed to this parameter looks like a test module
+ # we just add that to the test suite.
+ #
+ # If not, we inspect it for an Emacs buffer local variable called
+ # 'test-case-name'. If that variable is declared, we try to add its
+ # value to the test suite as a module.
+ #
+ # This parameter allows automated processes (like Buildbot) to pass
+ # a list of files to Trial with the general expectation of "these files,
+ # whatever they are, will get tested"
+ if not os.path.isfile(filename):
+ sys.stderr.write("File %r doesn't exist\n" % (filename,))
+ return
+ filename = os.path.abspath(filename)
+ if isTestFile(filename):
+ self['tests'].add(filename)
+ else:
+ self['tests'].update(getTestModules(filename))
+
+
+ def opt_spew(self):
+ """
+ Print an insanely verbose log of everything that happens. Useful
+ when debugging freezes or locks in complex code.
+ """
+ sys.settrace(spewer)
+
+
+ def opt_help_reporters(self):
+ synopsis = ("Trial's output can be customized using plugins called "
+ "Reporters. You can\nselect any of the following "
+ "reporters using --reporter=<foo>\n")
+ print synopsis
+ for p in plugin.getPlugins(itrial.IReporter):
+ print ' ', p.longOpt, '\t', p.description
+ print
+ sys.exit(0)
+
+
+ def opt_disablegc(self):
+ """
+ Disable the garbage collector
+ """
+ gc.disable()
+
+
+ def opt_tbformat(self, opt):
+ """
+ Specify the format to display tracebacks with. Valid formats are
+ 'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib
+ cgitb.text function
+ """
+ try:
+ self['tbformat'] = TBFORMAT_MAP[opt]
+ except KeyError:
+ raise usage.UsageError(
+ "tbformat must be 'plain', 'emacs', or 'cgitb'.")
+
+
+ def opt_extra(self, arg):
+ """
+ Add an extra argument. (This is a hack necessary for interfacing with
+ emacs's `gud'.) NOTE: This option is deprecated as of Twisted 11.0
+ """
+ warnings.warn(deprecate.getDeprecationWarningString(Options.opt_extra,
+ versions.Version('Twisted', 11, 0, 0)),
+ category=DeprecationWarning, stacklevel=2)
+
+ if self.extra is None:
+ self.extra = []
+ self.extra.append(arg)
+ opt_x = opt_extra
+
+
+ def opt_recursionlimit(self, arg):
+ """
+ see sys.setrecursionlimit()
+ """
+ try:
+ sys.setrecursionlimit(int(arg))
+ except (TypeError, ValueError):
+ raise usage.UsageError(
+ "argument to recursionlimit must be an integer")
+
+
+ def opt_random(self, option):
+ try:
+ self['random'] = long(option)
+ except ValueError:
+ raise usage.UsageError(
+ "Argument to --random must be a positive integer")
+ else:
+ if self['random'] < 0:
+ raise usage.UsageError(
+ "Argument to --random must be a positive integer")
+ elif self['random'] == 0:
+ self['random'] = long(time.time() * 100)
+
+
+ def opt_without_module(self, option):
+ """
+ Fake the lack of the specified modules, separated with commas.
+ """
+ for module in option.split(","):
+ if module in sys.modules:
+ warnings.warn("Module '%s' already imported, "
+ "disabling anyway." % (module,),
+ category=RuntimeWarning)
+ sys.modules[module] = None
+
+
+ def parseArgs(self, *args):
+ self['tests'].update(args)
+ if self.extra is not None:
+ self['tests'].update(self.extra)
+
+
+ def _loadReporterByName(self, name):
+ for p in plugin.getPlugins(itrial.IReporter):
+ qual = "%s.%s" % (p.module, p.klass)
+ if p.longOpt == name:
+ return reflect.namedAny(qual)
+ raise usage.UsageError("Only pass names of Reporter plugins to "
+ "--reporter. See --help-reporters for "
+ "more info.")
+
+
+ def postOptions(self):
+ # Only load reporters now, as opposed to any earlier, to avoid letting
+ # application-defined plugins muck up reactor selecting by importing
+ # t.i.reactor and causing the default to be installed.
+ self['reporter'] = self._loadReporterByName(self['reporter'])
+
+ if 'tbformat' not in self:
+ self['tbformat'] = 'default'
+ if self['nopm']:
+ if not self['debug']:
+ raise usage.UsageError("you must specify --debug when using "
+ "--nopm ")
+ failure.DO_POST_MORTEM = False
+
+
+
+def _initialDebugSetup(config):
+ # do this part of debug setup first for easy debugging of import failures
+ if config['debug']:
+ failure.startDebugMode()
+ if config['debug'] or config['debug-stacktraces']:
+ defer.setDebugging(True)
+
+
+
+def _getSuite(config):
+ loader = _getLoader(config)
+ recurse = not config['no-recurse']
+ return loader.loadByNames(config['tests'], recurse)
+
+
+
+def _getLoader(config):
+ loader = runner.TestLoader()
+ if config['random']:
+ randomer = random.Random()
+ randomer.seed(config['random'])
+ loader.sorter = lambda x : randomer.random()
+ print 'Running tests shuffled with seed %d\n' % config['random']
+ if not config['until-failure']:
+ loader.suiteFactory = runner.DestructiveTestSuite
+ return loader
+
+
+
+def _makeRunner(config):
+ mode = None
+ if config['debug']:
+ mode = runner.TrialRunner.DEBUG
+ if config['dry-run']:
+ mode = runner.TrialRunner.DRY_RUN
+ return runner.TrialRunner(config['reporter'],
+ mode=mode,
+ profile=config['profile'],
+ logfile=config['logfile'],
+ tracebackFormat=config['tbformat'],
+ realTimeErrors=config['rterrors'],
+ uncleanWarnings=config['unclean-warnings'],
+ workingDirectory=config['temp-directory'],
+ forceGarbageCollection=config['force-gc'])
+
+
+
+def run():
+ if len(sys.argv) == 1:
+ sys.argv.append("--help")
+ config = Options()
+ try:
+ config.parseOptions()
+ except usage.error, ue:
+ raise SystemExit, "%s: %s" % (sys.argv[0], ue)
+ _initialDebugSetup(config)
+ trialRunner = _makeRunner(config)
+ suite = _getSuite(config)
+ if config['until-failure']:
+ test_result = trialRunner.runUntilFailure(suite)
+ else:
+ test_result = trialRunner.run(suite)
+ if config.tracer:
+ sys.settrace(None)
+ results = config.tracer.results()
+ results.write_results(show_missing=1, summary=False,
+ coverdir=config.coverdir().path)
+ sys.exit(not test_result.wasSuccessful())
+
diff --git a/twisted/scripts/twistd.py b/twisted/scripts/twistd.py
new file mode 100644
index 0000000..c2b53c7
--- /dev/null
+++ b/twisted/scripts/twistd.py
@@ -0,0 +1,30 @@
+# -*- test-case-name: twisted.test.test_twistd -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+The Twisted Daemon: platform-independent interface.
+
+@author: Christopher Armstrong
+"""
+
+from twisted.application import app
+
+from twisted.python.runtime import platformType
+if platformType == "win32":
+ from twisted.scripts._twistw import ServerOptions, \
+ WindowsApplicationRunner as _SomeApplicationRunner
+else:
+ from twisted.scripts._twistd_unix import ServerOptions, \
+ UnixApplicationRunner as _SomeApplicationRunner
+
+
+def runApp(config):
+ _SomeApplicationRunner(config).run()
+
+
+def run():
+ app.run(runApp, ServerOptions)
+
+
+__all__ = ['run', 'runApp']
diff --git a/twisted/spread/__init__.py b/twisted/spread/__init__.py
new file mode 100644
index 0000000..e38b149
--- /dev/null
+++ b/twisted/spread/__init__.py
@@ -0,0 +1,12 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Spread: Spreadable (Distributed) Computing.
+
+Future Plans: PB, Jelly and Banana need to be optimized.
+
+@author: Glyph Lefkowitz
+"""
diff --git a/twisted/spread/banana.py b/twisted/spread/banana.py
new file mode 100644
index 0000000..edae9c6
--- /dev/null
+++ b/twisted/spread/banana.py
@@ -0,0 +1,358 @@
+# -*- test-case-name: twisted.test.test_banana -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Banana -- s-exp based protocol.
+
+Future Plans: This module is almost entirely stable. The same caveat applies
+to it as applies to L{twisted.spread.jelly}, however. Read its future plans
+for more details.
+
+@author: Glyph Lefkowitz
+"""
+
+import copy, cStringIO, struct
+
+from twisted.internet import protocol
+from twisted.persisted import styles
+from twisted.python import log
+
+class BananaError(Exception):
+ pass
+
+def int2b128(integer, stream):
+ if integer == 0:
+ stream(chr(0))
+ return
+ assert integer > 0, "can only encode positive integers"
+ while integer:
+ stream(chr(integer & 0x7f))
+ integer = integer >> 7
+
+
+def b1282int(st):
+ """
+ Convert an integer represented as a base 128 string into an C{int} or
+ C{long}.
+
+ @param st: The integer encoded in a string.
+ @type st: C{str}
+
+ @return: The integer value extracted from the string.
+ @rtype: C{int} or C{long}
+ """
+ e = 1
+ i = 0
+ for char in st:
+ n = ord(char)
+ i += (n * e)
+ e <<= 7
+ return i
+
+
+# delimiter characters.
+LIST = chr(0x80)
+INT = chr(0x81)
+STRING = chr(0x82)
+NEG = chr(0x83)
+FLOAT = chr(0x84)
+# "optional" -- these might be refused by a low-level implementation.
+LONGINT = chr(0x85)
+LONGNEG = chr(0x86)
+# really optional; this is is part of the 'pb' vocabulary
+VOCAB = chr(0x87)
+
+HIGH_BIT_SET = chr(0x80)
+
+def setPrefixLimit(limit):
+ """
+ Set the limit on the prefix length for all Banana connections
+ established after this call.
+
+ The prefix length limit determines how many bytes of prefix a banana
+ decoder will allow before rejecting a potential object as too large.
+
+ @type limit: C{int}
+ @param limit: The number of bytes of prefix for banana to allow when
+ decoding.
+ """
+ global _PREFIX_LIMIT
+ _PREFIX_LIMIT = limit
+setPrefixLimit(64)
+
+SIZE_LIMIT = 640 * 1024 # 640k is all you'll ever need :-)
+
+class Banana(protocol.Protocol, styles.Ephemeral):
+ knownDialects = ["pb", "none"]
+
+ prefixLimit = None
+ sizeLimit = SIZE_LIMIT
+
+ def setPrefixLimit(self, limit):
+ """
+ Set the prefix limit for decoding done by this protocol instance.
+
+ @see: L{setPrefixLimit}
+ """
+ self.prefixLimit = limit
+ self._smallestLongInt = -2 ** (limit * 7) + 1
+ self._smallestInt = -2 ** 31
+ self._largestInt = 2 ** 31 - 1
+ self._largestLongInt = 2 ** (limit * 7) - 1
+
+
+ def connectionReady(self):
+ """Surrogate for connectionMade
+ Called after protocol negotiation.
+ """
+
+ def _selectDialect(self, dialect):
+ self.currentDialect = dialect
+ self.connectionReady()
+
+ def callExpressionReceived(self, obj):
+ if self.currentDialect:
+ self.expressionReceived(obj)
+ else:
+ # this is the first message we've received
+ if self.isClient:
+ # if I'm a client I have to respond
+ for serverVer in obj:
+ if serverVer in self.knownDialects:
+ self.sendEncoded(serverVer)
+ self._selectDialect(serverVer)
+ break
+ else:
+ # I can't speak any of those dialects.
+ log.msg("The client doesn't speak any of the protocols "
+ "offered by the server: disconnecting.")
+ self.transport.loseConnection()
+ else:
+ if obj in self.knownDialects:
+ self._selectDialect(obj)
+ else:
+ # the client just selected a protocol that I did not suggest.
+ log.msg("The client selected a protocol the server didn't "
+ "suggest and doesn't know: disconnecting.")
+ self.transport.loseConnection()
+
+
+ def connectionMade(self):
+ self.setPrefixLimit(_PREFIX_LIMIT)
+ self.currentDialect = None
+ if not self.isClient:
+ self.sendEncoded(self.knownDialects)
+
+
+ def gotItem(self, item):
+ l = self.listStack
+ if l:
+ l[-1][1].append(item)
+ else:
+ self.callExpressionReceived(item)
+
+ buffer = ''
+
+ def dataReceived(self, chunk):
+ buffer = self.buffer + chunk
+ listStack = self.listStack
+ gotItem = self.gotItem
+ while buffer:
+ assert self.buffer != buffer, "This ain't right: %s %s" % (repr(self.buffer), repr(buffer))
+ self.buffer = buffer
+ pos = 0
+ for ch in buffer:
+ if ch >= HIGH_BIT_SET:
+ break
+ pos = pos + 1
+ else:
+ if pos > self.prefixLimit:
+ raise BananaError("Security precaution: more than %d bytes of prefix" % (self.prefixLimit,))
+ return
+ num = buffer[:pos]
+ typebyte = buffer[pos]
+ rest = buffer[pos+1:]
+ if len(num) > self.prefixLimit:
+ raise BananaError("Security precaution: longer than %d bytes worth of prefix" % (self.prefixLimit,))
+ if typebyte == LIST:
+ num = b1282int(num)
+ if num > SIZE_LIMIT:
+ raise BananaError("Security precaution: List too long.")
+ listStack.append((num, []))
+ buffer = rest
+ elif typebyte == STRING:
+ num = b1282int(num)
+ if num > SIZE_LIMIT:
+ raise BananaError("Security precaution: String too long.")
+ if len(rest) >= num:
+ buffer = rest[num:]
+ gotItem(rest[:num])
+ else:
+ return
+ elif typebyte == INT:
+ buffer = rest
+ num = b1282int(num)
+ gotItem(num)
+ elif typebyte == LONGINT:
+ buffer = rest
+ num = b1282int(num)
+ gotItem(num)
+ elif typebyte == LONGNEG:
+ buffer = rest
+ num = b1282int(num)
+ gotItem(-num)
+ elif typebyte == NEG:
+ buffer = rest
+ num = -b1282int(num)
+ gotItem(num)
+ elif typebyte == VOCAB:
+ buffer = rest
+ num = b1282int(num)
+ gotItem(self.incomingVocabulary[num])
+ elif typebyte == FLOAT:
+ if len(rest) >= 8:
+ buffer = rest[8:]
+ gotItem(struct.unpack("!d", rest[:8])[0])
+ else:
+ return
+ else:
+ raise NotImplementedError(("Invalid Type Byte %r" % (typebyte,)))
+ while listStack and (len(listStack[-1][1]) == listStack[-1][0]):
+ item = listStack.pop()[1]
+ gotItem(item)
+ self.buffer = ''
+
+
+ def expressionReceived(self, lst):
+ """Called when an expression (list, string, or int) is received.
+ """
+ raise NotImplementedError()
+
+
+ outgoingVocabulary = {
+ # Jelly Data Types
+ 'None' : 1,
+ 'class' : 2,
+ 'dereference' : 3,
+ 'reference' : 4,
+ 'dictionary' : 5,
+ 'function' : 6,
+ 'instance' : 7,
+ 'list' : 8,
+ 'module' : 9,
+ 'persistent' : 10,
+ 'tuple' : 11,
+ 'unpersistable' : 12,
+
+ # PB Data Types
+ 'copy' : 13,
+ 'cache' : 14,
+ 'cached' : 15,
+ 'remote' : 16,
+ 'local' : 17,
+ 'lcache' : 18,
+
+ # PB Protocol Messages
+ 'version' : 19,
+ 'login' : 20,
+ 'password' : 21,
+ 'challenge' : 22,
+ 'logged_in' : 23,
+ 'not_logged_in' : 24,
+ 'cachemessage' : 25,
+ 'message' : 26,
+ 'answer' : 27,
+ 'error' : 28,
+ 'decref' : 29,
+ 'decache' : 30,
+ 'uncache' : 31,
+ }
+
+ incomingVocabulary = {}
+ for k, v in outgoingVocabulary.items():
+ incomingVocabulary[v] = k
+
+ def __init__(self, isClient=1):
+ self.listStack = []
+ self.outgoingSymbols = copy.copy(self.outgoingVocabulary)
+ self.outgoingSymbolCount = 0
+ self.isClient = isClient
+
+ def sendEncoded(self, obj):
+ io = cStringIO.StringIO()
+ self._encode(obj, io.write)
+ value = io.getvalue()
+ self.transport.write(value)
+
+ def _encode(self, obj, write):
+ if isinstance(obj, (list, tuple)):
+ if len(obj) > SIZE_LIMIT:
+ raise BananaError(
+ "list/tuple is too long to send (%d)" % (len(obj),))
+ int2b128(len(obj), write)
+ write(LIST)
+ for elem in obj:
+ self._encode(elem, write)
+ elif isinstance(obj, (int, long)):
+ if obj < self._smallestLongInt or obj > self._largestLongInt:
+ raise BananaError(
+ "int/long is too large to send (%d)" % (obj,))
+ if obj < self._smallestInt:
+ int2b128(-obj, write)
+ write(LONGNEG)
+ elif obj < 0:
+ int2b128(-obj, write)
+ write(NEG)
+ elif obj <= self._largestInt:
+ int2b128(obj, write)
+ write(INT)
+ else:
+ int2b128(obj, write)
+ write(LONGINT)
+ elif isinstance(obj, float):
+ write(FLOAT)
+ write(struct.pack("!d", obj))
+ elif isinstance(obj, str):
+ # TODO: an API for extending banana...
+ if self.currentDialect == "pb" and obj in self.outgoingSymbols:
+ symbolID = self.outgoingSymbols[obj]
+ int2b128(symbolID, write)
+ write(VOCAB)
+ else:
+ if len(obj) > SIZE_LIMIT:
+ raise BananaError(
+ "string is too long to send (%d)" % (len(obj),))
+ int2b128(len(obj), write)
+ write(STRING)
+ write(obj)
+ else:
+ raise BananaError("could not send object: %r" % (obj,))
+
+
+# For use from the interactive interpreter
+_i = Banana()
+_i.connectionMade()
+_i._selectDialect("none")
+
+
+def encode(lst):
+ """Encode a list s-expression."""
+ io = cStringIO.StringIO()
+ _i.transport = io
+ _i.sendEncoded(lst)
+ return io.getvalue()
+
+
+def decode(st):
+ """
+ Decode a banana-encoded string.
+ """
+ l = []
+ _i.expressionReceived = l.append
+ try:
+ _i.dataReceived(st)
+ finally:
+ _i.buffer = ''
+ del _i.expressionReceived
+ return l[0]
diff --git a/twisted/spread/flavors.py b/twisted/spread/flavors.py
new file mode 100644
index 0000000..f5a986c
--- /dev/null
+++ b/twisted/spread/flavors.py
@@ -0,0 +1,590 @@
+# -*- test-case-name: twisted.test.test_pb -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module represents flavors of remotely acessible objects.
+
+Currently this is only objects accessible through Perspective Broker, but will
+hopefully encompass all forms of remote access which can emulate subsets of PB
+(such as XMLRPC or SOAP).
+
+Future Plans: Optimization. Exploitation of new-style object model.
+Optimizations to this module should not affect external-use semantics at all,
+but may have a small impact on users who subclass and override methods.
+
+@author: Glyph Lefkowitz
+"""
+
+# NOTE: this module should NOT import pb; it is supposed to be a module which
+# abstractly defines remotely accessible types. Many of these types expect to
+# be serialized by Jelly, but they ought to be accessible through other
+# mechanisms (like XMLRPC)
+
+# system imports
+import sys
+from zope.interface import implements, Interface
+
+# twisted imports
+from twisted.python import log, reflect
+
+# sibling imports
+from jelly import setUnjellyableForClass, setUnjellyableForClassTree, setUnjellyableFactoryForClass, unjellyableRegistry
+from jelly import Jellyable, Unjellyable, _newDummyLike
+from jelly import setInstanceState, getInstanceState
+
+# compatibility
+setCopierForClass = setUnjellyableForClass
+setCopierForClassTree = setUnjellyableForClassTree
+setFactoryForClass = setUnjellyableFactoryForClass
+copyTags = unjellyableRegistry
+
+copy_atom = "copy"
+cache_atom = "cache"
+cached_atom = "cached"
+remote_atom = "remote"
+
+
+class NoSuchMethod(AttributeError):
+ """Raised if there is no such remote method"""
+
+
+class IPBRoot(Interface):
+ """Factory for root Referenceable objects for PB servers."""
+
+ def rootObject(broker):
+ """Return root Referenceable for broker."""
+
+
+class Serializable(Jellyable):
+ """An object that can be passed remotely.
+
+ I am a style of object which can be serialized by Perspective
+ Broker. Objects which wish to be referenceable or copied remotely
+ have to subclass Serializable. However, clients of Perspective
+ Broker will probably not want to directly subclass Serializable; the
+ Flavors of transferable objects are listed below.
+
+ What it means to be \"Serializable\" is that an object can be
+ passed to or returned from a remote method. Certain basic types
+ (dictionaries, lists, tuples, numbers, strings) are serializable by
+ default; however, classes need to choose a specific serialization
+ style: L{Referenceable}, L{Viewable}, L{Copyable} or L{Cacheable}.
+
+ You may also pass C{[lists, dictionaries, tuples]} of L{Serializable}
+ instances to or return them from remote methods, as many levels deep
+ as you like.
+ """
+
+ def processUniqueID(self):
+ """Return an ID which uniquely represents this object for this process.
+
+ By default, this uses the 'id' builtin, but can be overridden to
+ indicate that two values are identity-equivalent (such as proxies
+ for the same object).
+ """
+
+ return id(self)
+
+class Referenceable(Serializable):
+ perspective = None
+ """I am an object sent remotely as a direct reference.
+
+ When one of my subclasses is sent as an argument to or returned
+ from a remote method call, I will be serialized by default as a
+ direct reference.
+
+ This means that the peer will be able to call methods on me;
+ a method call xxx() from my peer will be resolved to methods
+ of the name remote_xxx.
+ """
+
+ def remoteMessageReceived(self, broker, message, args, kw):
+ """A remote message has been received. Dispatch it appropriately.
+
+ The default implementation is to dispatch to a method called
+ 'remote_messagename' and call it with the same arguments.
+ """
+ args = broker.unserialize(args)
+ kw = broker.unserialize(kw)
+ method = getattr(self, "remote_%s" % message, None)
+ if method is None:
+ raise NoSuchMethod("No such method: remote_%s" % (message,))
+ try:
+ state = method(*args, **kw)
+ except TypeError:
+ log.msg("%s didn't accept %s and %s" % (method, args, kw))
+ raise
+ return broker.serialize(state, self.perspective)
+
+ def jellyFor(self, jellier):
+ """(internal)
+
+ Return a tuple which will be used as the s-expression to
+ serialize this to a peer.
+ """
+
+ return ["remote", jellier.invoker.registerReference(self)]
+
+
+class Root(Referenceable):
+ """I provide a root object to L{pb.Broker}s for a L{pb.BrokerFactory}.
+
+ When a L{pb.BrokerFactory} produces a L{pb.Broker}, it supplies that
+ L{pb.Broker} with an object named \"root\". That object is obtained
+ by calling my rootObject method.
+ """
+
+ implements(IPBRoot)
+
+ def rootObject(self, broker):
+ """A L{pb.BrokerFactory} is requesting to publish me as a root object.
+
+ When a L{pb.BrokerFactory} is sending me as the root object, this
+ method will be invoked to allow per-broker versions of an
+ object. By default I return myself.
+ """
+ return self
+
+
+class ViewPoint(Referenceable):
+ """
+ I act as an indirect reference to an object accessed through a
+ L{pb.Perspective}.
+
+ Simply put, I combine an object with a perspective so that when a
+ peer calls methods on the object I refer to, the method will be
+ invoked with that perspective as a first argument, so that it can
+ know who is calling it.
+
+ While L{Viewable} objects will be converted to ViewPoints by default
+ when they are returned from or sent as arguments to a remote
+ method, any object may be manually proxied as well. (XXX: Now that
+ this class is no longer named C{Proxy}, this is the only occourance
+ of the term 'proxied' in this docstring, and may be unclear.)
+
+ This can be useful when dealing with L{pb.Perspective}s, L{Copyable}s,
+ and L{Cacheable}s. It is legal to implement a method as such on
+ a perspective::
+
+ | def perspective_getViewPointForOther(self, name):
+ | defr = self.service.getPerspectiveRequest(name)
+ | defr.addCallbacks(lambda x, self=self: ViewPoint(self, x), log.msg)
+ | return defr
+
+ This will allow you to have references to Perspective objects in two
+ different ways. One is through the initial 'attach' call -- each
+ peer will have a L{pb.RemoteReference} to their perspective directly. The
+ other is through this method; each peer can get a L{pb.RemoteReference} to
+ all other perspectives in the service; but that L{pb.RemoteReference} will
+ be to a L{ViewPoint}, not directly to the object.
+
+ The practical offshoot of this is that you can implement 2 varieties
+ of remotely callable methods on this Perspective; view_xxx and
+ C{perspective_xxx}. C{view_xxx} methods will follow the rules for
+ ViewPoint methods (see ViewPoint.L{remoteMessageReceived}), and
+ C{perspective_xxx} methods will follow the rules for Perspective
+ methods.
+ """
+
+ def __init__(self, perspective, object):
+ """Initialize me with a Perspective and an Object.
+ """
+ self.perspective = perspective
+ self.object = object
+
+ def processUniqueID(self):
+ """Return an ID unique to a proxy for this perspective+object combination.
+ """
+ return (id(self.perspective), id(self.object))
+
+ def remoteMessageReceived(self, broker, message, args, kw):
+ """A remote message has been received. Dispatch it appropriately.
+
+ The default implementation is to dispatch to a method called
+ 'C{view_messagename}' to my Object and call it on my object with
+ the same arguments, modified by inserting my Perspective as
+ the first argument.
+ """
+ args = broker.unserialize(args, self.perspective)
+ kw = broker.unserialize(kw, self.perspective)
+ method = getattr(self.object, "view_%s" % message)
+ try:
+ state = apply(method, (self.perspective,)+args, kw)
+ except TypeError:
+ log.msg("%s didn't accept %s and %s" % (method, args, kw))
+ raise
+ rv = broker.serialize(state, self.perspective, method, args, kw)
+ return rv
+
+
+class Viewable(Serializable):
+ """I will be converted to a L{ViewPoint} when passed to or returned from a remote method.
+
+ The beginning of a peer's interaction with a PB Service is always
+ through a perspective. However, if a C{perspective_xxx} method returns
+ a Viewable, it will be serialized to the peer as a response to that
+ method.
+ """
+
+ def jellyFor(self, jellier):
+ """Serialize a L{ViewPoint} for me and the perspective of the given broker.
+ """
+ return ViewPoint(jellier.invoker.serializingPerspective, self).jellyFor(jellier)
+
+
+
+class Copyable(Serializable):
+ """Subclass me to get copied each time you are returned from or passed to a remote method.
+
+ When I am returned from or passed to a remote method call, I will be
+ converted into data via a set of callbacks (see my methods for more
+ info). That data will then be serialized using Jelly, and sent to
+ the peer.
+
+ The peer will then look up the type to represent this with; see
+ L{RemoteCopy} for details.
+ """
+
+ def getStateToCopy(self):
+ """Gather state to send when I am serialized for a peer.
+
+ I will default to returning self.__dict__. Override this to
+ customize this behavior.
+ """
+
+ return self.__dict__
+
+ def getStateToCopyFor(self, perspective):
+ """
+ Gather state to send when I am serialized for a particular
+ perspective.
+
+ I will default to calling L{getStateToCopy}. Override this to
+ customize this behavior.
+ """
+
+ return self.getStateToCopy()
+
+ def getTypeToCopy(self):
+ """Determine what type tag to send for me.
+
+ By default, send the string representation of my class
+ (package.module.Class); normally this is adequate, but
+ you may override this to change it.
+ """
+
+ return reflect.qual(self.__class__)
+
+ def getTypeToCopyFor(self, perspective):
+ """Determine what type tag to send for me.
+
+ By default, defer to self.L{getTypeToCopy}() normally this is
+ adequate, but you may override this to change it.
+ """
+
+ return self.getTypeToCopy()
+
+ def jellyFor(self, jellier):
+ """Assemble type tag and state to copy for this broker.
+
+ This will call L{getTypeToCopyFor} and L{getStateToCopy}, and
+ return an appropriate s-expression to represent me.
+ """
+
+ if jellier.invoker is None:
+ return getInstanceState(self, jellier)
+ p = jellier.invoker.serializingPerspective
+ t = self.getTypeToCopyFor(p)
+ state = self.getStateToCopyFor(p)
+ sxp = jellier.prepare(self)
+ sxp.extend([t, jellier.jelly(state)])
+ return jellier.preserve(self, sxp)
+
+
+class Cacheable(Copyable):
+ """A cached instance.
+
+ This means that it's copied; but there is some logic to make sure
+ that it's only copied once. Additionally, when state is retrieved,
+ it is passed a "proto-reference" to the state as it will exist on
+ the client.
+
+ XXX: The documentation for this class needs work, but it's the most
+ complex part of PB and it is inherently difficult to explain.
+ """
+
+ def getStateToCacheAndObserveFor(self, perspective, observer):
+ """
+ Get state to cache on the client and client-cache reference
+ to observe locally.
+
+ This is similiar to getStateToCopyFor, but it additionally
+ passes in a reference to the client-side RemoteCache instance
+ that will be created when it is unserialized. This allows
+ Cacheable instances to keep their RemoteCaches up to date when
+ they change, such that no changes can occur between the point
+ at which the state is initially copied and the client receives
+ it that are not propogated.
+ """
+
+ return self.getStateToCopyFor(perspective)
+
+ def jellyFor(self, jellier):
+ """Return an appropriate tuple to serialize me.
+
+ Depending on whether this broker has cached me or not, this may
+ return either a full state or a reference to an existing cache.
+ """
+ if jellier.invoker is None:
+ return getInstanceState(self, jellier)
+ luid = jellier.invoker.cachedRemotelyAs(self, 1)
+ if luid is None:
+ luid = jellier.invoker.cacheRemotely(self)
+ p = jellier.invoker.serializingPerspective
+ type_ = self.getTypeToCopyFor(p)
+ observer = RemoteCacheObserver(jellier.invoker, self, p)
+ state = self.getStateToCacheAndObserveFor(p, observer)
+ l = jellier.prepare(self)
+ jstate = jellier.jelly(state)
+ l.extend([type_, luid, jstate])
+ return jellier.preserve(self, l)
+ else:
+ return cached_atom, luid
+
+ def stoppedObserving(self, perspective, observer):
+ """This method is called when a client has stopped observing me.
+
+ The 'observer' argument is the same as that passed in to
+ getStateToCacheAndObserveFor.
+ """
+
+
+
+class RemoteCopy(Unjellyable):
+ """I am a remote copy of a Copyable object.
+
+ When the state from a L{Copyable} object is received, an instance will
+ be created based on the copy tags table (see setUnjellyableForClass) and
+ sent the L{setCopyableState} message. I provide a reasonable default
+ implementation of that message; subclass me if you wish to serve as
+ a copier for remote data.
+
+ NOTE: copiers are invoked with no arguments. Do not implement a
+ constructor which requires args in a subclass of L{RemoteCopy}!
+ """
+
+ def setCopyableState(self, state):
+ """I will be invoked with the state to copy locally.
+
+ 'state' is the data returned from the remote object's
+ 'getStateToCopyFor' method, which will often be the remote
+ object's dictionary (or a filtered approximation of it depending
+ on my peer's perspective).
+ """
+
+ self.__dict__ = state
+
+ def unjellyFor(self, unjellier, jellyList):
+ if unjellier.invoker is None:
+ return setInstanceState(self, unjellier, jellyList)
+ self.setCopyableState(unjellier.unjelly(jellyList[1]))
+ return self
+
+
+
+class RemoteCache(RemoteCopy, Serializable):
+ """A cache is a local representation of a remote L{Cacheable} object.
+
+ This represents the last known state of this object. It may
+ also have methods invoked on it -- in order to update caches,
+ the cached class generates a L{pb.RemoteReference} to this object as
+ it is originally sent.
+
+ Much like copy, I will be invoked with no arguments. Do not
+ implement a constructor that requires arguments in one of my
+ subclasses.
+ """
+
+ def remoteMessageReceived(self, broker, message, args, kw):
+ """A remote message has been received. Dispatch it appropriately.
+
+ The default implementation is to dispatch to a method called
+ 'C{observe_messagename}' and call it on my with the same arguments.
+ """
+
+ args = broker.unserialize(args)
+ kw = broker.unserialize(kw)
+ method = getattr(self, "observe_%s" % message)
+ try:
+ state = apply(method, args, kw)
+ except TypeError:
+ log.msg("%s didn't accept %s and %s" % (method, args, kw))
+ raise
+ return broker.serialize(state, None, method, args, kw)
+
+ def jellyFor(self, jellier):
+ """serialize me (only for the broker I'm for) as the original cached reference
+ """
+ if jellier.invoker is None:
+ return getInstanceState(self, jellier)
+ assert jellier.invoker is self.broker, "You cannot exchange cached proxies between brokers."
+ return 'lcache', self.luid
+
+
+ def unjellyFor(self, unjellier, jellyList):
+ if unjellier.invoker is None:
+ return setInstanceState(self, unjellier, jellyList)
+ self.broker = unjellier.invoker
+ self.luid = jellyList[1]
+ cProxy = _newDummyLike(self)
+ # XXX questionable whether this was a good design idea...
+ init = getattr(cProxy, "__init__", None)
+ if init:
+ init()
+ unjellier.invoker.cacheLocally(jellyList[1], self)
+ cProxy.setCopyableState(unjellier.unjelly(jellyList[2]))
+ # Might have changed due to setCopyableState method; we'll assume that
+ # it's bad form to do so afterwards.
+ self.__dict__ = cProxy.__dict__
+ # chomp, chomp -- some existing code uses "self.__dict__ =", some uses
+ # "__dict__.update". This is here in order to handle both cases.
+ self.broker = unjellier.invoker
+ self.luid = jellyList[1]
+ return cProxy
+
+## def __really_del__(self):
+## """Final finalization call, made after all remote references have been lost.
+## """
+
+ def __cmp__(self, other):
+ """Compare me [to another RemoteCache.
+ """
+ if isinstance(other, self.__class__):
+ return cmp(id(self.__dict__), id(other.__dict__))
+ else:
+ return cmp(id(self.__dict__), other)
+
+ def __hash__(self):
+ """Hash me.
+ """
+ return int(id(self.__dict__) % sys.maxint)
+
+ broker = None
+ luid = None
+
+ def __del__(self):
+ """Do distributed reference counting on finalize.
+ """
+ try:
+ # log.msg( ' --- decache: %s %s' % (self, self.luid) )
+ if self.broker:
+ self.broker.decCacheRef(self.luid)
+ except:
+ log.deferr()
+
+def unjellyCached(unjellier, unjellyList):
+ luid = unjellyList[1]
+ cNotProxy = unjellier.invoker.cachedLocallyAs(luid)
+ cProxy = _newDummyLike(cNotProxy)
+ return cProxy
+
+setUnjellyableForClass("cached", unjellyCached)
+
+def unjellyLCache(unjellier, unjellyList):
+ luid = unjellyList[1]
+ obj = unjellier.invoker.remotelyCachedForLUID(luid)
+ return obj
+
+setUnjellyableForClass("lcache", unjellyLCache)
+
+def unjellyLocal(unjellier, unjellyList):
+ obj = unjellier.invoker.localObjectForID(unjellyList[1])
+ return obj
+
+setUnjellyableForClass("local", unjellyLocal)
+
+class RemoteCacheMethod:
+ """A method on a reference to a L{RemoteCache}.
+ """
+
+ def __init__(self, name, broker, cached, perspective):
+ """(internal) initialize.
+ """
+ self.name = name
+ self.broker = broker
+ self.perspective = perspective
+ self.cached = cached
+
+ def __cmp__(self, other):
+ return cmp((self.name, self.broker, self.perspective, self.cached), other)
+
+ def __hash__(self):
+ return hash((self.name, self.broker, self.perspective, self.cached))
+
+ def __call__(self, *args, **kw):
+ """(internal) action method.
+ """
+ cacheID = self.broker.cachedRemotelyAs(self.cached)
+ if cacheID is None:
+ from pb import ProtocolError
+ raise ProtocolError("You can't call a cached method when the object hasn't been given to the peer yet.")
+ return self.broker._sendMessage('cache', self.perspective, cacheID, self.name, args, kw)
+
+class RemoteCacheObserver:
+ """I am a reverse-reference to the peer's L{RemoteCache}.
+
+ I am generated automatically when a cache is serialized. I
+ represent a reference to the client's L{RemoteCache} object that
+ will represent a particular L{Cacheable}; I am the additional
+ object passed to getStateToCacheAndObserveFor.
+ """
+
+ def __init__(self, broker, cached, perspective):
+ """(internal) Initialize me.
+
+ @param broker: a L{pb.Broker} instance.
+
+ @param cached: a L{Cacheable} instance that this L{RemoteCacheObserver}
+ corresponds to.
+
+ @param perspective: a reference to the perspective who is observing this.
+ """
+
+ self.broker = broker
+ self.cached = cached
+ self.perspective = perspective
+
+ def __repr__(self):
+ return "<RemoteCacheObserver(%s, %s, %s) at %s>" % (
+ self.broker, self.cached, self.perspective, id(self))
+
+ def __hash__(self):
+ """Generate a hash unique to all L{RemoteCacheObserver}s for this broker/perspective/cached triplet
+ """
+
+ return ( (hash(self.broker) % 2**10)
+ + (hash(self.perspective) % 2**10)
+ + (hash(self.cached) % 2**10))
+
+ def __cmp__(self, other):
+ """Compare me to another L{RemoteCacheObserver}.
+ """
+
+ return cmp((self.broker, self.perspective, self.cached), other)
+
+ def callRemote(self, _name, *args, **kw):
+ """(internal) action method.
+ """
+ cacheID = self.broker.cachedRemotelyAs(self.cached)
+ if cacheID is None:
+ from pb import ProtocolError
+ raise ProtocolError("You can't call a cached method when the "
+ "object hasn't been given to the peer yet.")
+ return self.broker._sendMessage('cache', self.perspective, cacheID,
+ _name, args, kw)
+
+ def remoteMethod(self, key):
+ """Get a L{pb.RemoteMethod} for this key.
+ """
+ return RemoteCacheMethod(key, self.broker, self.cached, self.perspective)
diff --git a/twisted/spread/interfaces.py b/twisted/spread/interfaces.py
new file mode 100644
index 0000000..6d48d00
--- /dev/null
+++ b/twisted/spread/interfaces.py
@@ -0,0 +1,28 @@
+"""
+Twisted Spread Interfaces.
+
+This module is unused so far. It's also undecided whether this module
+will remain monolithic.
+"""
+
+from zope.interface import Interface
+
+class IJellyable(Interface):
+ def jellyFor(jellier):
+ """
+ Jelly myself for jellier.
+ """
+
+class IUnjellyable(Interface):
+ def unjellyFor(jellier, jellyList):
+ """
+ Unjelly myself for the jellier.
+
+ @param jellier: A stateful object which exists for the lifetime of a
+ single call to L{unjelly}.
+
+ @param jellyList: The C{list} which represents the jellied state of the
+ object to be unjellied.
+
+ @return: The object which results from unjellying.
+ """
diff --git a/twisted/spread/jelly.py b/twisted/spread/jelly.py
new file mode 100644
index 0000000..1879530
--- /dev/null
+++ b/twisted/spread/jelly.py
@@ -0,0 +1,1151 @@
+# -*- test-case-name: twisted.test.test_jelly -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+S-expression-based persistence of python objects.
+
+It does something very much like L{Pickle<pickle>}; however, pickle's main goal
+seems to be efficiency (both in space and time); jelly's main goals are
+security, human readability, and portability to other environments.
+
+This is how Jelly converts various objects to s-expressions.
+
+Boolean::
+ True --> ['boolean', 'true']
+
+Integer::
+ 1 --> 1
+
+List::
+ [1, 2] --> ['list', 1, 2]
+
+String::
+ \"hello\" --> \"hello\"
+
+Float::
+ 2.3 --> 2.3
+
+Dictionary::
+ {'a': 1, 'b': 'c'} --> ['dictionary', ['b', 'c'], ['a', 1]]
+
+Module::
+ UserString --> ['module', 'UserString']
+
+Class::
+ UserString.UserString --> ['class', ['module', 'UserString'], 'UserString']
+
+Function::
+ string.join --> ['function', 'join', ['module', 'string']]
+
+Instance: s is an instance of UserString.UserString, with a __dict__
+{'data': 'hello'}::
+ [\"UserString.UserString\", ['dictionary', ['data', 'hello']]]
+
+Class Method: UserString.UserString.center::
+ ['method', 'center', ['None'], ['class', ['module', 'UserString'],
+ 'UserString']]
+
+Instance Method: s.center, where s is an instance of UserString.UserString::
+ ['method', 'center', ['instance', ['reference', 1, ['class',
+ ['module', 'UserString'], 'UserString']], ['dictionary', ['data', 'd']]],
+ ['dereference', 1]]
+
+The C{set} builtin and the C{sets.Set} class are serialized to the same
+thing, and unserialized to C{set} if available, else to C{sets.Set}. It means
+that there's a possibility of type switching in the serialization process. The
+solution is to always use C{set} if possible, and only use C{sets.Set} under
+Python 2.3; this can be accomplished by using L{twisted.python.compat.set}.
+
+The same rule applies for C{frozenset} and C{sets.ImmutableSet}.
+
+@author: Glyph Lefkowitz
+"""
+
+# System Imports
+import pickle
+import types
+import warnings
+from types import StringType
+from types import UnicodeType
+from types import IntType
+from types import TupleType
+from types import ListType
+from types import LongType
+from types import FloatType
+from types import FunctionType
+from types import MethodType
+from types import ModuleType
+from types import DictionaryType
+from types import InstanceType
+from types import NoneType
+from types import ClassType
+import copy
+
+import datetime
+from types import BooleanType
+
+try:
+ import decimal
+except ImportError:
+ decimal = None
+
+try:
+ _set = set
+except NameError:
+ _set = None
+
+try:
+ # Filter out deprecation warning for Python >= 2.6
+ warnings.filterwarnings("ignore", category=DeprecationWarning,
+ message="the sets module is deprecated", append=True)
+ import sets as _sets
+finally:
+ warnings.filters.pop()
+
+
+from zope.interface import implements
+
+# Twisted Imports
+from twisted.python.reflect import namedObject, qual
+from twisted.persisted.crefutil import NotKnown, _Tuple, _InstanceMethod
+from twisted.persisted.crefutil import _DictKeyAndValue, _Dereference
+from twisted.persisted.crefutil import _Container
+from twisted.python.compat import reduce
+
+from twisted.spread.interfaces import IJellyable, IUnjellyable
+
+DictTypes = (DictionaryType,)
+
+None_atom = "None" # N
+# code
+class_atom = "class" # c
+module_atom = "module" # m
+function_atom = "function" # f
+
+# references
+dereference_atom = 'dereference' # D
+persistent_atom = 'persistent' # p
+reference_atom = 'reference' # r
+
+# mutable collections
+dictionary_atom = "dictionary" # d
+list_atom = 'list' # l
+set_atom = 'set'
+
+# immutable collections
+# (assignment to __dict__ and __class__ still might go away!)
+tuple_atom = "tuple" # t
+instance_atom = 'instance' # i
+frozenset_atom = 'frozenset'
+
+
+# errors
+unpersistable_atom = "unpersistable"# u
+unjellyableRegistry = {}
+unjellyableFactoryRegistry = {}
+
+_NO_STATE = object()
+
+def _newInstance(cls, state=_NO_STATE):
+ """
+ Make a new instance of a class without calling its __init__ method.
+ Supports both new- and old-style classes.
+
+ @param state: A C{dict} used to update C{inst.__dict__} or C{_NO_STATE}
+ to skip this part of initialization.
+
+ @return: A new instance of C{cls}.
+ """
+ if not isinstance(cls, types.ClassType):
+ # new-style
+ inst = cls.__new__(cls)
+
+ if state is not _NO_STATE:
+ inst.__dict__.update(state) # Copy 'instance' behaviour
+ else:
+ if state is not _NO_STATE:
+ inst = InstanceType(cls, state)
+ else:
+ inst = InstanceType(cls)
+ return inst
+
+
+
+def _maybeClass(classnamep):
+ try:
+ object
+ except NameError:
+ isObject = 0
+ else:
+ isObject = isinstance(classnamep, type)
+ if isinstance(classnamep, ClassType) or isObject:
+ return qual(classnamep)
+ return classnamep
+
+
+
+def setUnjellyableForClass(classname, unjellyable):
+ """
+ Set which local class will represent a remote type.
+
+ If you have written a Copyable class that you expect your client to be
+ receiving, write a local "copy" class to represent it, then call::
+
+ jellier.setUnjellyableForClass('module.package.Class', MyCopier).
+
+ Call this at the module level immediately after its class
+ definition. MyCopier should be a subclass of RemoteCopy.
+
+ The classname may be a special tag returned by
+ 'Copyable.getTypeToCopyFor' rather than an actual classname.
+
+ This call is also for cached classes, since there will be no
+ overlap. The rules are the same.
+ """
+
+ global unjellyableRegistry
+ classname = _maybeClass(classname)
+ unjellyableRegistry[classname] = unjellyable
+ globalSecurity.allowTypes(classname)
+
+
+
+def setUnjellyableFactoryForClass(classname, copyFactory):
+ """
+ Set the factory to construct a remote instance of a type::
+
+ jellier.setUnjellyableFactoryForClass('module.package.Class', MyFactory)
+
+ Call this at the module level immediately after its class definition.
+ C{copyFactory} should return an instance or subclass of
+ L{RemoteCopy<pb.RemoteCopy>}.
+
+ Similar to L{setUnjellyableForClass} except it uses a factory instead
+ of creating an instance.
+ """
+
+ global unjellyableFactoryRegistry
+ classname = _maybeClass(classname)
+ unjellyableFactoryRegistry[classname] = copyFactory
+ globalSecurity.allowTypes(classname)
+
+
+
+def setUnjellyableForClassTree(module, baseClass, prefix=None):
+ """
+ Set all classes in a module derived from C{baseClass} as copiers for
+ a corresponding remote class.
+
+ When you have a heirarchy of Copyable (or Cacheable) classes on one
+ side, and a mirror structure of Copied (or RemoteCache) classes on the
+ other, use this to setUnjellyableForClass all your Copieds for the
+ Copyables.
+
+ Each copyTag (the \"classname\" argument to getTypeToCopyFor, and
+ what the Copyable's getTypeToCopyFor returns) is formed from
+ adding a prefix to the Copied's class name. The prefix defaults
+ to module.__name__. If you wish the copy tag to consist of solely
+ the classname, pass the empty string \'\'.
+
+ @param module: a module object from which to pull the Copied classes.
+ (passing sys.modules[__name__] might be useful)
+
+ @param baseClass: the base class from which all your Copied classes derive.
+
+ @param prefix: the string prefixed to classnames to form the
+ unjellyableRegistry.
+ """
+ if prefix is None:
+ prefix = module.__name__
+
+ if prefix:
+ prefix = "%s." % prefix
+
+ for i in dir(module):
+ i_ = getattr(module, i)
+ if type(i_) == types.ClassType:
+ if issubclass(i_, baseClass):
+ setUnjellyableForClass('%s%s' % (prefix, i), i_)
+
+
+
+def getInstanceState(inst, jellier):
+ """
+ Utility method to default to 'normal' state rules in serialization.
+ """
+ if hasattr(inst, "__getstate__"):
+ state = inst.__getstate__()
+ else:
+ state = inst.__dict__
+ sxp = jellier.prepare(inst)
+ sxp.extend([qual(inst.__class__), jellier.jelly(state)])
+ return jellier.preserve(inst, sxp)
+
+
+
+def setInstanceState(inst, unjellier, jellyList):
+ """
+ Utility method to default to 'normal' state rules in unserialization.
+ """
+ state = unjellier.unjelly(jellyList[1])
+ if hasattr(inst, "__setstate__"):
+ inst.__setstate__(state)
+ else:
+ inst.__dict__ = state
+ return inst
+
+
+
+class Unpersistable:
+ """
+ This is an instance of a class that comes back when something couldn't be
+ unpersisted.
+ """
+
+ def __init__(self, reason):
+ """
+ Initialize an unpersistable object with a descriptive C{reason} string.
+ """
+ self.reason = reason
+
+
+ def __repr__(self):
+ return "Unpersistable(%s)" % repr(self.reason)
+
+
+
+class Jellyable:
+ """
+ Inherit from me to Jelly yourself directly with the `getStateFor'
+ convenience method.
+ """
+ implements(IJellyable)
+
+ def getStateFor(self, jellier):
+ return self.__dict__
+
+
+ def jellyFor(self, jellier):
+ """
+ @see: L{twisted.spread.interfaces.IJellyable.jellyFor}
+ """
+ sxp = jellier.prepare(self)
+ sxp.extend([
+ qual(self.__class__),
+ jellier.jelly(self.getStateFor(jellier))])
+ return jellier.preserve(self, sxp)
+
+
+
+class Unjellyable:
+ """
+ Inherit from me to Unjelly yourself directly with the
+ C{setStateFor} convenience method.
+ """
+ implements(IUnjellyable)
+
+ def setStateFor(self, unjellier, state):
+ self.__dict__ = state
+
+
+ def unjellyFor(self, unjellier, jellyList):
+ """
+ Perform the inverse operation of L{Jellyable.jellyFor}.
+
+ @see: L{twisted.spread.interfaces.IUnjellyable.unjellyFor}
+ """
+ state = unjellier.unjelly(jellyList[1])
+ self.setStateFor(unjellier, state)
+ return self
+
+
+
+class _Jellier:
+ """
+ (Internal) This class manages state for a call to jelly()
+ """
+
+ def __init__(self, taster, persistentStore, invoker):
+ """
+ Initialize.
+ """
+ self.taster = taster
+ # `preserved' is a dict of previously seen instances.
+ self.preserved = {}
+ # `cooked' is a dict of previously backreferenced instances to their
+ # `ref' lists.
+ self.cooked = {}
+ self.cooker = {}
+ self._ref_id = 1
+ self.persistentStore = persistentStore
+ self.invoker = invoker
+
+
+ def _cook(self, object):
+ """
+ (internal) Backreference an object.
+
+ Notes on this method for the hapless future maintainer: If I've already
+ gone through the prepare/preserve cycle on the specified object (it is
+ being referenced after the serializer is \"done with\" it, e.g. this
+ reference is NOT circular), the copy-in-place of aList is relevant,
+ since the list being modified is the actual, pre-existing jelly
+ expression that was returned for that object. If not, it's technically
+ superfluous, since the value in self.preserved didn't need to be set,
+ but the invariant that self.preserved[id(object)] is a list is
+ convenient because that means we don't have to test and create it or
+ not create it here, creating fewer code-paths. that's why
+ self.preserved is always set to a list.
+
+ Sorry that this code is so hard to follow, but Python objects are
+ tricky to persist correctly. -glyph
+ """
+ aList = self.preserved[id(object)]
+ newList = copy.copy(aList)
+ # make a new reference ID
+ refid = self._ref_id
+ self._ref_id = self._ref_id + 1
+ # replace the old list in-place, so that we don't have to track the
+ # previous reference to it.
+ aList[:] = [reference_atom, refid, newList]
+ self.cooked[id(object)] = [dereference_atom, refid]
+ return aList
+
+
+ def prepare(self, object):
+ """
+ (internal) Create a list for persisting an object to. This will allow
+ backreferences to be made internal to the object. (circular
+ references).
+
+ The reason this needs to happen is that we don't generate an ID for
+ every object, so we won't necessarily know which ID the object will
+ have in the future. When it is 'cooked' ( see _cook ), it will be
+ assigned an ID, and the temporary placeholder list created here will be
+ modified in-place to create an expression that gives this object an ID:
+ [reference id# [object-jelly]].
+ """
+
+ # create a placeholder list to be preserved
+ self.preserved[id(object)] = []
+ # keep a reference to this object around, so it doesn't disappear!
+ # (This isn't always necessary, but for cases where the objects are
+ # dynamically generated by __getstate__ or getStateToCopyFor calls, it
+ # is; id() will return the same value for a different object if it gets
+ # garbage collected. This may be optimized later.)
+ self.cooker[id(object)] = object
+ return []
+
+
+ def preserve(self, object, sexp):
+ """
+ (internal) Mark an object's persistent list for later referral.
+ """
+ # if I've been cooked in the meanwhile,
+ if id(object) in self.cooked:
+ # replace the placeholder empty list with the real one
+ self.preserved[id(object)][2] = sexp
+ # but give this one back.
+ sexp = self.preserved[id(object)]
+ else:
+ self.preserved[id(object)] = sexp
+ return sexp
+
+ constantTypes = {types.StringType : 1, types.IntType : 1,
+ types.FloatType : 1, types.LongType : 1}
+
+
+ def _checkMutable(self,obj):
+ objId = id(obj)
+ if objId in self.cooked:
+ return self.cooked[objId]
+ if objId in self.preserved:
+ self._cook(obj)
+ return self.cooked[objId]
+
+
+ def jelly(self, obj):
+ if isinstance(obj, Jellyable):
+ preRef = self._checkMutable(obj)
+ if preRef:
+ return preRef
+ return obj.jellyFor(self)
+ objType = type(obj)
+ if self.taster.isTypeAllowed(qual(objType)):
+ # "Immutable" Types
+ if ((objType is StringType) or
+ (objType is IntType) or
+ (objType is LongType) or
+ (objType is FloatType)):
+ return obj
+ elif objType is MethodType:
+ return ["method",
+ obj.im_func.__name__,
+ self.jelly(obj.im_self),
+ self.jelly(obj.im_class)]
+
+ elif UnicodeType and objType is UnicodeType:
+ return ['unicode', obj.encode('UTF-8')]
+ elif objType is NoneType:
+ return ['None']
+ elif objType is FunctionType:
+ name = obj.__name__
+ return ['function', str(pickle.whichmodule(obj, obj.__name__))
+ + '.' +
+ name]
+ elif objType is ModuleType:
+ return ['module', obj.__name__]
+ elif objType is BooleanType:
+ return ['boolean', obj and 'true' or 'false']
+ elif objType is datetime.datetime:
+ if obj.tzinfo:
+ raise NotImplementedError(
+ "Currently can't jelly datetime objects with tzinfo")
+ return ['datetime', '%s %s %s %s %s %s %s' % (
+ obj.year, obj.month, obj.day, obj.hour,
+ obj.minute, obj.second, obj.microsecond)]
+ elif objType is datetime.time:
+ if obj.tzinfo:
+ raise NotImplementedError(
+ "Currently can't jelly datetime objects with tzinfo")
+ return ['time', '%s %s %s %s' % (obj.hour, obj.minute,
+ obj.second, obj.microsecond)]
+ elif objType is datetime.date:
+ return ['date', '%s %s %s' % (obj.year, obj.month, obj.day)]
+ elif objType is datetime.timedelta:
+ return ['timedelta', '%s %s %s' % (obj.days, obj.seconds,
+ obj.microseconds)]
+ elif objType is ClassType or issubclass(objType, type):
+ return ['class', qual(obj)]
+ elif decimal is not None and objType is decimal.Decimal:
+ return self.jelly_decimal(obj)
+ else:
+ preRef = self._checkMutable(obj)
+ if preRef:
+ return preRef
+ # "Mutable" Types
+ sxp = self.prepare(obj)
+ if objType is ListType:
+ sxp.extend(self._jellyIterable(list_atom, obj))
+ elif objType is TupleType:
+ sxp.extend(self._jellyIterable(tuple_atom, obj))
+ elif objType in DictTypes:
+ sxp.append(dictionary_atom)
+ for key, val in obj.items():
+ sxp.append([self.jelly(key), self.jelly(val)])
+ elif (_set is not None and objType is set or
+ objType is _sets.Set):
+ sxp.extend(self._jellyIterable(set_atom, obj))
+ elif (_set is not None and objType is frozenset or
+ objType is _sets.ImmutableSet):
+ sxp.extend(self._jellyIterable(frozenset_atom, obj))
+ else:
+ className = qual(obj.__class__)
+ persistent = None
+ if self.persistentStore:
+ persistent = self.persistentStore(obj, self)
+ if persistent is not None:
+ sxp.append(persistent_atom)
+ sxp.append(persistent)
+ elif self.taster.isClassAllowed(obj.__class__):
+ sxp.append(className)
+ if hasattr(obj, "__getstate__"):
+ state = obj.__getstate__()
+ else:
+ state = obj.__dict__
+ sxp.append(self.jelly(state))
+ else:
+ self.unpersistable(
+ "instance of class %s deemed insecure" %
+ qual(obj.__class__), sxp)
+ return self.preserve(obj, sxp)
+ else:
+ if objType is InstanceType:
+ raise InsecureJelly("Class not allowed for instance: %s %s" %
+ (obj.__class__, obj))
+ raise InsecureJelly("Type not allowed for object: %s %s" %
+ (objType, obj))
+
+
+ def _jellyIterable(self, atom, obj):
+ """
+ Jelly an iterable object.
+
+ @param atom: the identifier atom of the object.
+ @type atom: C{str}
+
+ @param obj: any iterable object.
+ @type obj: C{iterable}
+
+ @return: a generator of jellied data.
+ @rtype: C{generator}
+ """
+ yield atom
+ for item in obj:
+ yield self.jelly(item)
+
+
+ def jelly_decimal(self, d):
+ """
+ Jelly a decimal object.
+
+ @param d: a decimal object to serialize.
+ @type d: C{decimal.Decimal}
+
+ @return: jelly for the decimal object.
+ @rtype: C{list}
+ """
+ sign, guts, exponent = d.as_tuple()
+ value = reduce(lambda left, right: left * 10 + right, guts)
+ if sign:
+ value = -value
+ return ['decimal', value, exponent]
+
+
+ def unpersistable(self, reason, sxp=None):
+ """
+ (internal) Returns an sexp: (unpersistable "reason"). Utility method
+ for making note that a particular object could not be serialized.
+ """
+ if sxp is None:
+ sxp = []
+ sxp.append(unpersistable_atom)
+ sxp.append(reason)
+ return sxp
+
+
+
+class _Unjellier:
+
+ def __init__(self, taster, persistentLoad, invoker):
+ self.taster = taster
+ self.persistentLoad = persistentLoad
+ self.references = {}
+ self.postCallbacks = []
+ self.invoker = invoker
+
+
+ def unjellyFull(self, obj):
+ o = self.unjelly(obj)
+ for m in self.postCallbacks:
+ m()
+ return o
+
+
+ def unjelly(self, obj):
+ if type(obj) is not types.ListType:
+ return obj
+ jelType = obj[0]
+ if not self.taster.isTypeAllowed(jelType):
+ raise InsecureJelly(jelType)
+ regClass = unjellyableRegistry.get(jelType)
+ if regClass is not None:
+ if isinstance(regClass, ClassType):
+ inst = _Dummy() # XXX chomp, chomp
+ inst.__class__ = regClass
+ method = inst.unjellyFor
+ elif isinstance(regClass, type):
+ # regClass.__new__ does not call regClass.__init__
+ inst = regClass.__new__(regClass)
+ method = inst.unjellyFor
+ else:
+ method = regClass # this is how it ought to be done
+ val = method(self, obj)
+ if hasattr(val, 'postUnjelly'):
+ self.postCallbacks.append(inst.postUnjelly)
+ return val
+ regFactory = unjellyableFactoryRegistry.get(jelType)
+ if regFactory is not None:
+ state = self.unjelly(obj[1])
+ inst = regFactory(state)
+ if hasattr(inst, 'postUnjelly'):
+ self.postCallbacks.append(inst.postUnjelly)
+ return inst
+ thunk = getattr(self, '_unjelly_%s'%jelType, None)
+ if thunk is not None:
+ ret = thunk(obj[1:])
+ else:
+ nameSplit = jelType.split('.')
+ modName = '.'.join(nameSplit[:-1])
+ if not self.taster.isModuleAllowed(modName):
+ raise InsecureJelly(
+ "Module %s not allowed (in type %s)." % (modName, jelType))
+ clz = namedObject(jelType)
+ if not self.taster.isClassAllowed(clz):
+ raise InsecureJelly("Class %s not allowed." % jelType)
+ if hasattr(clz, "__setstate__"):
+ ret = _newInstance(clz)
+ state = self.unjelly(obj[1])
+ ret.__setstate__(state)
+ else:
+ state = self.unjelly(obj[1])
+ ret = _newInstance(clz, state)
+ if hasattr(clz, 'postUnjelly'):
+ self.postCallbacks.append(ret.postUnjelly)
+ return ret
+
+
+ def _unjelly_None(self, exp):
+ return None
+
+
+ def _unjelly_unicode(self, exp):
+ if UnicodeType:
+ return unicode(exp[0], "UTF-8")
+ else:
+ return Unpersistable("Could not unpersist unicode: %s" % (exp[0],))
+
+
+ def _unjelly_decimal(self, exp):
+ """
+ Unjelly decimal objects, if decimal is available. If not, return a
+ L{Unpersistable} object instead.
+ """
+ if decimal is None:
+ return Unpersistable(
+ "Could not unpersist decimal: %s" % (exp[0] * (10**exp[1]),))
+ value = exp[0]
+ exponent = exp[1]
+ if value < 0:
+ sign = 1
+ else:
+ sign = 0
+ guts = decimal.Decimal(value).as_tuple()[1]
+ return decimal.Decimal((sign, guts, exponent))
+
+
+ def _unjelly_boolean(self, exp):
+ if BooleanType:
+ assert exp[0] in ('true', 'false')
+ return exp[0] == 'true'
+ else:
+ return Unpersistable("Could not unpersist boolean: %s" % (exp[0],))
+
+
+ def _unjelly_datetime(self, exp):
+ return datetime.datetime(*map(int, exp[0].split()))
+
+
+ def _unjelly_date(self, exp):
+ return datetime.date(*map(int, exp[0].split()))
+
+
+ def _unjelly_time(self, exp):
+ return datetime.time(*map(int, exp[0].split()))
+
+
+ def _unjelly_timedelta(self, exp):
+ days, seconds, microseconds = map(int, exp[0].split())
+ return datetime.timedelta(
+ days=days, seconds=seconds, microseconds=microseconds)
+
+
+ def unjellyInto(self, obj, loc, jel):
+ o = self.unjelly(jel)
+ if isinstance(o, NotKnown):
+ o.addDependant(obj, loc)
+ obj[loc] = o
+ return o
+
+
+ def _unjelly_dereference(self, lst):
+ refid = lst[0]
+ x = self.references.get(refid)
+ if x is not None:
+ return x
+ der = _Dereference(refid)
+ self.references[refid] = der
+ return der
+
+
+ def _unjelly_reference(self, lst):
+ refid = lst[0]
+ exp = lst[1]
+ o = self.unjelly(exp)
+ ref = self.references.get(refid)
+ if (ref is None):
+ self.references[refid] = o
+ elif isinstance(ref, NotKnown):
+ ref.resolveDependants(o)
+ self.references[refid] = o
+ else:
+ assert 0, "Multiple references with same ID!"
+ return o
+
+
+ def _unjelly_tuple(self, lst):
+ l = range(len(lst))
+ finished = 1
+ for elem in l:
+ if isinstance(self.unjellyInto(l, elem, lst[elem]), NotKnown):
+ finished = 0
+ if finished:
+ return tuple(l)
+ else:
+ return _Tuple(l)
+
+
+ def _unjelly_list(self, lst):
+ l = range(len(lst))
+ for elem in l:
+ self.unjellyInto(l, elem, lst[elem])
+ return l
+
+
+ def _unjellySetOrFrozenset(self, lst, containerType):
+ """
+ Helper method to unjelly set or frozenset.
+
+ @param lst: the content of the set.
+ @type lst: C{list}
+
+ @param containerType: the type of C{set} to use.
+ """
+ l = range(len(lst))
+ finished = True
+ for elem in l:
+ data = self.unjellyInto(l, elem, lst[elem])
+ if isinstance(data, NotKnown):
+ finished = False
+ if not finished:
+ return _Container(l, containerType)
+ else:
+ return containerType(l)
+
+
+ def _unjelly_set(self, lst):
+ """
+ Unjelly set using either the C{set} builtin if available, or
+ C{sets.Set} as fallback.
+ """
+ if _set is not None:
+ containerType = set
+ else:
+ containerType = _sets.Set
+ return self._unjellySetOrFrozenset(lst, containerType)
+
+
+ def _unjelly_frozenset(self, lst):
+ """
+ Unjelly frozenset using either the C{frozenset} builtin if available,
+ or C{sets.ImmutableSet} as fallback.
+ """
+ if _set is not None:
+ containerType = frozenset
+ else:
+ containerType = _sets.ImmutableSet
+ return self._unjellySetOrFrozenset(lst, containerType)
+
+
+ def _unjelly_dictionary(self, lst):
+ d = {}
+ for k, v in lst:
+ kvd = _DictKeyAndValue(d)
+ self.unjellyInto(kvd, 0, k)
+ self.unjellyInto(kvd, 1, v)
+ return d
+
+
+ def _unjelly_module(self, rest):
+ moduleName = rest[0]
+ if type(moduleName) != types.StringType:
+ raise InsecureJelly(
+ "Attempted to unjelly a module with a non-string name.")
+ if not self.taster.isModuleAllowed(moduleName):
+ raise InsecureJelly(
+ "Attempted to unjelly module named %r" % (moduleName,))
+ mod = __import__(moduleName, {}, {},"x")
+ return mod
+
+
+ def _unjelly_class(self, rest):
+ clist = rest[0].split('.')
+ modName = '.'.join(clist[:-1])
+ if not self.taster.isModuleAllowed(modName):
+ raise InsecureJelly("module %s not allowed" % modName)
+ klaus = namedObject(rest[0])
+ objType = type(klaus)
+ if objType not in (types.ClassType, types.TypeType):
+ raise InsecureJelly(
+ "class %r unjellied to something that isn't a class: %r" % (
+ rest[0], klaus))
+ if not self.taster.isClassAllowed(klaus):
+ raise InsecureJelly("class not allowed: %s" % qual(klaus))
+ return klaus
+
+
+ def _unjelly_function(self, rest):
+ modSplit = rest[0].split('.')
+ modName = '.'.join(modSplit[:-1])
+ if not self.taster.isModuleAllowed(modName):
+ raise InsecureJelly("Module not allowed: %s"% modName)
+ # XXX do I need an isFunctionAllowed?
+ function = namedObject(rest[0])
+ return function
+
+
+ def _unjelly_persistent(self, rest):
+ if self.persistentLoad:
+ pload = self.persistentLoad(rest[0], self)
+ return pload
+ else:
+ return Unpersistable("Persistent callback not found")
+
+
+ def _unjelly_instance(self, rest):
+ clz = self.unjelly(rest[0])
+ if type(clz) is not types.ClassType:
+ raise InsecureJelly("Instance found with non-class class.")
+ if hasattr(clz, "__setstate__"):
+ inst = _newInstance(clz, {})
+ state = self.unjelly(rest[1])
+ inst.__setstate__(state)
+ else:
+ state = self.unjelly(rest[1])
+ inst = _newInstance(clz, state)
+ if hasattr(clz, 'postUnjelly'):
+ self.postCallbacks.append(inst.postUnjelly)
+ return inst
+
+
+ def _unjelly_unpersistable(self, rest):
+ return Unpersistable("Unpersistable data: %s" % (rest[0],))
+
+
+ def _unjelly_method(self, rest):
+ """
+ (internal) Unjelly a method.
+ """
+ im_name = rest[0]
+ im_self = self.unjelly(rest[1])
+ im_class = self.unjelly(rest[2])
+ if type(im_class) is not types.ClassType:
+ raise InsecureJelly("Method found with non-class class.")
+ if im_name in im_class.__dict__:
+ if im_self is None:
+ im = getattr(im_class, im_name)
+ elif isinstance(im_self, NotKnown):
+ im = _InstanceMethod(im_name, im_self, im_class)
+ else:
+ im = MethodType(im_class.__dict__[im_name], im_self, im_class)
+ else:
+ raise TypeError('instance method changed')
+ return im
+
+
+
+class _Dummy:
+ """
+ (Internal) Dummy class, used for unserializing instances.
+ """
+
+
+
+class _DummyNewStyle(object):
+ """
+ (Internal) Dummy class, used for unserializing instances of new-style
+ classes.
+ """
+
+
+def _newDummyLike(instance):
+ """
+ Create a new instance like C{instance}.
+
+ The new instance has the same class and instance dictionary as the given
+ instance.
+
+ @return: The new instance.
+ """
+ if isinstance(instance.__class__, type):
+ # New-style class
+ dummy = _DummyNewStyle()
+ else:
+ # Classic class
+ dummy = _Dummy()
+ dummy.__class__ = instance.__class__
+ dummy.__dict__ = instance.__dict__
+ return dummy
+
+
+#### Published Interface.
+
+
+class InsecureJelly(Exception):
+ """
+ This exception will be raised when a jelly is deemed `insecure'; e.g. it
+ contains a type, class, or module disallowed by the specified `taster'
+ """
+
+
+
+class DummySecurityOptions:
+ """
+ DummySecurityOptions() -> insecure security options
+ Dummy security options -- this class will allow anything.
+ """
+
+ def isModuleAllowed(self, moduleName):
+ """
+ DummySecurityOptions.isModuleAllowed(moduleName) -> boolean
+ returns 1 if a module by that name is allowed, 0 otherwise
+ """
+ return 1
+
+
+ def isClassAllowed(self, klass):
+ """
+ DummySecurityOptions.isClassAllowed(class) -> boolean
+ Assumes the module has already been allowed. Returns 1 if the given
+ class is allowed, 0 otherwise.
+ """
+ return 1
+
+
+ def isTypeAllowed(self, typeName):
+ """
+ DummySecurityOptions.isTypeAllowed(typeName) -> boolean
+ Returns 1 if the given type is allowed, 0 otherwise.
+ """
+ return 1
+
+
+
+class SecurityOptions:
+ """
+ This will by default disallow everything, except for 'none'.
+ """
+
+ basicTypes = ["dictionary", "list", "tuple",
+ "reference", "dereference", "unpersistable",
+ "persistent", "long_int", "long", "dict"]
+
+ def __init__(self):
+ """
+ SecurityOptions() initialize.
+ """
+ # I don't believe any of these types can ever pose a security hazard,
+ # except perhaps "reference"...
+ self.allowedTypes = {"None": 1,
+ "bool": 1,
+ "boolean": 1,
+ "string": 1,
+ "str": 1,
+ "int": 1,
+ "float": 1,
+ "datetime": 1,
+ "time": 1,
+ "date": 1,
+ "timedelta": 1,
+ "NoneType": 1}
+ if hasattr(types, 'UnicodeType'):
+ self.allowedTypes['unicode'] = 1
+ if decimal is not None:
+ self.allowedTypes['decimal'] = 1
+ self.allowedTypes['set'] = 1
+ self.allowedTypes['frozenset'] = 1
+ self.allowedModules = {}
+ self.allowedClasses = {}
+
+
+ def allowBasicTypes(self):
+ """
+ Allow all `basic' types. (Dictionary and list. Int, string, and float
+ are implicitly allowed.)
+ """
+ self.allowTypes(*self.basicTypes)
+
+
+ def allowTypes(self, *types):
+ """
+ SecurityOptions.allowTypes(typeString): Allow a particular type, by its
+ name.
+ """
+ for typ in types:
+ if not isinstance(typ, str):
+ typ = qual(typ)
+ self.allowedTypes[typ] = 1
+
+
+ def allowInstancesOf(self, *classes):
+ """
+ SecurityOptions.allowInstances(klass, klass, ...): allow instances
+ of the specified classes
+
+ This will also allow the 'instance', 'class' (renamed 'classobj' in
+ Python 2.3), and 'module' types, as well as basic types.
+ """
+ self.allowBasicTypes()
+ self.allowTypes("instance", "class", "classobj", "module")
+ for klass in classes:
+ self.allowTypes(qual(klass))
+ self.allowModules(klass.__module__)
+ self.allowedClasses[klass] = 1
+
+
+ def allowModules(self, *modules):
+ """
+ SecurityOptions.allowModules(module, module, ...): allow modules by
+ name. This will also allow the 'module' type.
+ """
+ for module in modules:
+ if type(module) == types.ModuleType:
+ module = module.__name__
+ self.allowedModules[module] = 1
+
+
+ def isModuleAllowed(self, moduleName):
+ """
+ SecurityOptions.isModuleAllowed(moduleName) -> boolean
+ returns 1 if a module by that name is allowed, 0 otherwise
+ """
+ return moduleName in self.allowedModules
+
+
+ def isClassAllowed(self, klass):
+ """
+ SecurityOptions.isClassAllowed(class) -> boolean
+ Assumes the module has already been allowed. Returns 1 if the given
+ class is allowed, 0 otherwise.
+ """
+ return klass in self.allowedClasses
+
+
+ def isTypeAllowed(self, typeName):
+ """
+ SecurityOptions.isTypeAllowed(typeName) -> boolean
+ Returns 1 if the given type is allowed, 0 otherwise.
+ """
+ return (typeName in self.allowedTypes or '.' in typeName)
+
+
+globalSecurity = SecurityOptions()
+globalSecurity.allowBasicTypes()
+
+
+
+def jelly(object, taster=DummySecurityOptions(), persistentStore=None,
+ invoker=None):
+ """
+ Serialize to s-expression.
+
+ Returns a list which is the serialized representation of an object. An
+ optional 'taster' argument takes a SecurityOptions and will mark any
+ insecure objects as unpersistable rather than serializing them.
+ """
+ return _Jellier(taster, persistentStore, invoker).jelly(object)
+
+
+
+def unjelly(sexp, taster=DummySecurityOptions(), persistentLoad=None,
+ invoker=None):
+ """
+ Unserialize from s-expression.
+
+ Takes an list that was the result from a call to jelly() and unserializes
+ an arbitrary object from it. The optional 'taster' argument, an instance
+ of SecurityOptions, will cause an InsecureJelly exception to be raised if a
+ disallowed type, module, or class attempted to unserialize.
+ """
+ return _Unjellier(taster, persistentLoad, invoker).unjellyFull(sexp)
diff --git a/twisted/spread/pb.py b/twisted/spread/pb.py
new file mode 100644
index 0000000..76e803c
--- /dev/null
+++ b/twisted/spread/pb.py
@@ -0,0 +1,1434 @@
+# -*- test-case-name: twisted.test.test_pb -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Perspective Broker
+
+\"This isn\'t a professional opinion, but it's probably got enough
+internet to kill you.\" --glyph
+
+Introduction
+============
+
+This is a broker for proxies for and copies of objects. It provides a
+translucent interface layer to those proxies.
+
+The protocol is not opaque, because it provides objects which represent the
+remote proxies and require no context (server references, IDs) to operate on.
+
+It is not transparent because it does I{not} attempt to make remote objects
+behave identically, or even similiarly, to local objects. Method calls are
+invoked asynchronously, and specific rules are applied when serializing
+arguments.
+
+To get started, begin with L{PBClientFactory} and L{PBServerFactory}.
+
+@author: Glyph Lefkowitz
+"""
+
+import random
+import types
+
+from zope.interface import implements, Interface
+
+# Twisted Imports
+from twisted.python import log, failure, reflect
+from twisted.python.hashlib import md5
+from twisted.internet import defer, protocol
+from twisted.cred.portal import Portal
+from twisted.cred.credentials import IAnonymous, ICredentials
+from twisted.cred.credentials import IUsernameHashedPassword, Anonymous
+from twisted.persisted import styles
+from twisted.python.components import registerAdapter
+
+from twisted.spread.interfaces import IJellyable, IUnjellyable
+from twisted.spread.jelly import jelly, unjelly, globalSecurity
+from twisted.spread import banana
+
+from twisted.spread.flavors import Serializable
+from twisted.spread.flavors import Referenceable, NoSuchMethod
+from twisted.spread.flavors import Root, IPBRoot
+from twisted.spread.flavors import ViewPoint
+from twisted.spread.flavors import Viewable
+from twisted.spread.flavors import Copyable
+from twisted.spread.flavors import Jellyable
+from twisted.spread.flavors import Cacheable
+from twisted.spread.flavors import RemoteCopy
+from twisted.spread.flavors import RemoteCache
+from twisted.spread.flavors import RemoteCacheObserver
+from twisted.spread.flavors import copyTags
+
+from twisted.spread.flavors import setUnjellyableForClass
+from twisted.spread.flavors import setUnjellyableFactoryForClass
+from twisted.spread.flavors import setUnjellyableForClassTree
+# These three are backwards compatibility aliases for the previous three.
+# Ultimately they should be deprecated. -exarkun
+from twisted.spread.flavors import setCopierForClass
+from twisted.spread.flavors import setFactoryForClass
+from twisted.spread.flavors import setCopierForClassTree
+
+
+MAX_BROKER_REFS = 1024
+
+portno = 8787
+
+
+
+class ProtocolError(Exception):
+ """
+ This error is raised when an invalid protocol statement is received.
+ """
+
+
+
+class DeadReferenceError(ProtocolError):
+ """
+ This error is raised when a method is called on a dead reference (one whose
+ broker has been disconnected).
+ """
+
+
+
+class Error(Exception):
+ """
+ This error can be raised to generate known error conditions.
+
+ When a PB callable method (perspective_, remote_, view_) raises
+ this error, it indicates that a traceback should not be printed,
+ but instead, the string representation of the exception should be
+ sent.
+ """
+
+
+
+class RemoteError(Exception):
+ """
+ This class is used to wrap a string-ified exception from the remote side to
+ be able to reraise it. (Raising string exceptions is no longer possible in
+ Python 2.6+)
+
+ The value of this exception will be a str() representation of the remote
+ value.
+
+ @ivar remoteType: The full import path of the exception class which was
+ raised on the remote end.
+ @type remoteType: C{str}
+
+ @ivar remoteTraceback: The remote traceback.
+ @type remoteTraceback: C{str}
+
+ @note: It's not possible to include the remoteTraceback if this exception is
+ thrown into a generator. It must be accessed as an attribute.
+ """
+ def __init__(self, remoteType, value, remoteTraceback):
+ Exception.__init__(self, value)
+ self.remoteType = remoteType
+ self.remoteTraceback = remoteTraceback
+
+
+
+class RemoteMethod:
+ """
+ This is a translucent reference to a remote message.
+ """
+ def __init__(self, obj, name):
+ """
+ Initialize with a L{RemoteReference} and the name of this message.
+ """
+ self.obj = obj
+ self.name = name
+
+
+ def __cmp__(self, other):
+ return cmp((self.obj, self.name), other)
+
+
+ def __hash__(self):
+ return hash((self.obj, self.name))
+
+
+ def __call__(self, *args, **kw):
+ """
+ Asynchronously invoke a remote method.
+ """
+ return self.obj.broker._sendMessage('',self.obj.perspective,
+ self.obj.luid, self.name, args, kw)
+
+
+
+class PBConnectionLost(Exception):
+ pass
+
+
+
+class IPerspective(Interface):
+ """
+ per*spec*tive, n. : The relationship of aspects of a subject to each
+ other and to a whole: 'a perspective of history'; 'a need to view
+ the problem in the proper perspective'.
+
+ This is a Perspective Broker-specific wrapper for an avatar. That
+ is to say, a PB-published view on to the business logic for the
+ system's concept of a 'user'.
+
+ The concept of attached/detached is no longer implemented by the
+ framework. The realm is expected to implement such semantics if
+ needed.
+ """
+
+ def perspectiveMessageReceived(broker, message, args, kwargs):
+ """
+ This method is called when a network message is received.
+
+ @arg broker: The Perspective Broker.
+
+ @type message: str
+ @arg message: The name of the method called by the other end.
+
+ @type args: list in jelly format
+ @arg args: The arguments that were passed by the other end. It
+ is recommend that you use the `unserialize' method of the
+ broker to decode this.
+
+ @type kwargs: dict in jelly format
+ @arg kwargs: The keyword arguments that were passed by the
+ other end. It is recommended that you use the
+ `unserialize' method of the broker to decode this.
+
+ @rtype: A jelly list.
+ @return: It is recommended that you use the `serialize' method
+ of the broker on whatever object you need to return to
+ generate the return value.
+ """
+
+
+
+class Avatar:
+ """
+ A default IPerspective implementor.
+
+ This class is intended to be subclassed, and a realm should return
+ an instance of such a subclass when IPerspective is requested of
+ it.
+
+ A peer requesting a perspective will receive only a
+ L{RemoteReference} to a pb.Avatar. When a method is called on
+ that L{RemoteReference}, it will translate to a method on the
+ remote perspective named 'perspective_methodname'. (For more
+ information on invoking methods on other objects, see
+ L{flavors.ViewPoint}.)
+ """
+
+ implements(IPerspective)
+
+ def perspectiveMessageReceived(self, broker, message, args, kw):
+ """
+ This method is called when a network message is received.
+
+ This will call::
+
+ self.perspective_%(message)s(*broker.unserialize(args),
+ **broker.unserialize(kw))
+
+ to handle the method; subclasses of Avatar are expected to
+ implement methods using this naming convention.
+ """
+
+ args = broker.unserialize(args, self)
+ kw = broker.unserialize(kw, self)
+ method = getattr(self, "perspective_%s" % message)
+ try:
+ state = method(*args, **kw)
+ except TypeError:
+ log.msg("%s didn't accept %s and %s" % (method, args, kw))
+ raise
+ return broker.serialize(state, self, method, args, kw)
+
+
+
+class AsReferenceable(Referenceable):
+ """
+ A reference directed towards another object.
+ """
+
+ def __init__(self, object, messageType="remote"):
+ self.remoteMessageReceived = getattr(
+ object, messageType + "MessageReceived")
+
+
+
+class RemoteReference(Serializable, styles.Ephemeral):
+ """
+ A translucent reference to a remote object.
+
+ I may be a reference to a L{flavors.ViewPoint}, a
+ L{flavors.Referenceable}, or an L{IPerspective} implementor (e.g.,
+ pb.Avatar). From the client's perspective, it is not possible to
+ tell which except by convention.
+
+ I am a \"translucent\" reference because although no additional
+ bookkeeping overhead is given to the application programmer for
+ manipulating a reference, return values are asynchronous.
+
+ See also L{twisted.internet.defer}.
+
+ @ivar broker: The broker I am obtained through.
+ @type broker: L{Broker}
+ """
+
+ implements(IUnjellyable)
+
+ def __init__(self, perspective, broker, luid, doRefCount):
+ """(internal) Initialize me with a broker and a locally-unique ID.
+
+ The ID is unique only to the particular Perspective Broker
+ instance.
+ """
+ self.luid = luid
+ self.broker = broker
+ self.doRefCount = doRefCount
+ self.perspective = perspective
+ self.disconnectCallbacks = []
+
+ def notifyOnDisconnect(self, callback):
+ """Register a callback to be called if our broker gets disconnected.
+
+ This callback will be called with one argument, this instance.
+ """
+ assert callable(callback)
+ self.disconnectCallbacks.append(callback)
+ if len(self.disconnectCallbacks) == 1:
+ self.broker.notifyOnDisconnect(self._disconnected)
+
+ def dontNotifyOnDisconnect(self, callback):
+ """Remove a callback that was registered with notifyOnDisconnect."""
+ self.disconnectCallbacks.remove(callback)
+ if not self.disconnectCallbacks:
+ self.broker.dontNotifyOnDisconnect(self._disconnected)
+
+ def _disconnected(self):
+ """Called if we are disconnected and have callbacks registered."""
+ for callback in self.disconnectCallbacks:
+ callback(self)
+ self.disconnectCallbacks = None
+
+ def jellyFor(self, jellier):
+ """If I am being sent back to where I came from, serialize as a local backreference.
+ """
+ if jellier.invoker:
+ assert self.broker == jellier.invoker, "Can't send references to brokers other than their own."
+ return "local", self.luid
+ else:
+ return "unpersistable", "References cannot be serialized"
+
+ def unjellyFor(self, unjellier, unjellyList):
+ self.__init__(unjellier.invoker.unserializingPerspective, unjellier.invoker, unjellyList[1], 1)
+ return self
+
+ def callRemote(self, _name, *args, **kw):
+ """Asynchronously invoke a remote method.
+
+ @type _name: C{str}
+ @param _name: the name of the remote method to invoke
+ @param args: arguments to serialize for the remote function
+ @param kw: keyword arguments to serialize for the remote function.
+ @rtype: L{twisted.internet.defer.Deferred}
+ @returns: a Deferred which will be fired when the result of
+ this remote call is received.
+ """
+ # note that we use '_name' instead of 'name' so the user can call
+ # remote methods with 'name' as a keyword parameter, like this:
+ # ref.callRemote("getPeopleNamed", count=12, name="Bob")
+
+ return self.broker._sendMessage('',self.perspective, self.luid,
+ _name, args, kw)
+
+ def remoteMethod(self, key):
+ """Get a L{RemoteMethod} for this key.
+ """
+ return RemoteMethod(self, key)
+
+ def __cmp__(self,other):
+ """Compare me [to another L{RemoteReference}].
+ """
+ if isinstance(other, RemoteReference):
+ if other.broker == self.broker:
+ return cmp(self.luid, other.luid)
+ return cmp(self.broker, other)
+
+ def __hash__(self):
+ """Hash me.
+ """
+ return self.luid
+
+ def __del__(self):
+ """Do distributed reference counting on finalization.
+ """
+ if self.doRefCount:
+ self.broker.sendDecRef(self.luid)
+
+setUnjellyableForClass("remote", RemoteReference)
+
+class Local:
+ """(internal) A reference to a local object.
+ """
+
+ def __init__(self, object, perspective=None):
+ """Initialize.
+ """
+ self.object = object
+ self.perspective = perspective
+ self.refcount = 1
+
+ def __repr__(self):
+ return "<pb.Local %r ref:%s>" % (self.object, self.refcount)
+
+ def incref(self):
+ """Increment and return my reference count.
+ """
+ self.refcount = self.refcount + 1
+ return self.refcount
+
+ def decref(self):
+ """Decrement and return my reference count.
+ """
+ self.refcount = self.refcount - 1
+ return self.refcount
+
+
+##
+# Failure
+##
+
+class CopyableFailure(failure.Failure, Copyable):
+ """
+ A L{flavors.RemoteCopy} and L{flavors.Copyable} version of
+ L{twisted.python.failure.Failure} for serialization.
+ """
+
+ unsafeTracebacks = 0
+
+ def getStateToCopy(self):
+ """
+ Collect state related to the exception which occurred, discarding
+ state which cannot reasonably be serialized.
+ """
+ state = self.__dict__.copy()
+ state['tb'] = None
+ state['frames'] = []
+ state['stack'] = []
+ state['value'] = str(self.value) # Exception instance
+ if isinstance(self.type, str):
+ state['type'] = self.type
+ else:
+ state['type'] = reflect.qual(self.type) # Exception class
+ if self.unsafeTracebacks:
+ state['traceback'] = self.getTraceback()
+ else:
+ state['traceback'] = 'Traceback unavailable\n'
+ return state
+
+
+
+class CopiedFailure(RemoteCopy, failure.Failure):
+ """
+ A L{CopiedFailure} is a L{pb.RemoteCopy} of a L{failure.Failure}
+ transfered via PB.
+
+ @ivar type: The full import path of the exception class which was raised on
+ the remote end.
+ @type type: C{str}
+
+ @ivar value: A str() representation of the remote value.
+ @type value: L{CopiedFailure} or C{str}
+
+ @ivar traceback: The remote traceback.
+ @type traceback: C{str}
+ """
+
+ def printTraceback(self, file=None, elideFrameworkCode=0, detail='default'):
+ if file is None:
+ file = log.logfile
+ file.write("Traceback from remote host -- ")
+ file.write(self.traceback)
+ file.write(self.type + ": " + self.value)
+ file.write('\n')
+
+
+ def throwExceptionIntoGenerator(self, g):
+ """
+ Throw the original exception into the given generator, preserving
+ traceback information if available. In the case of a L{CopiedFailure}
+ where the exception type is a string, a L{pb.RemoteError} is thrown
+ instead.
+
+ @return: The next value yielded from the generator.
+ @raise StopIteration: If there are no more values in the generator.
+ @raise RemoteError: The wrapped remote exception.
+ """
+ return g.throw(RemoteError(self.type, self.value, self.traceback))
+
+ printBriefTraceback = printTraceback
+ printDetailedTraceback = printTraceback
+
+setUnjellyableForClass(CopyableFailure, CopiedFailure)
+
+
+
+def failure2Copyable(fail, unsafeTracebacks=0):
+ f = types.InstanceType(CopyableFailure, fail.__dict__)
+ f.unsafeTracebacks = unsafeTracebacks
+ return f
+
+
+
+class Broker(banana.Banana):
+ """I am a broker for objects.
+ """
+
+ version = 6
+ username = None
+ factory = None
+
+ def __init__(self, isClient=1, security=globalSecurity):
+ banana.Banana.__init__(self, isClient)
+ self.disconnected = 0
+ self.disconnects = []
+ self.failures = []
+ self.connects = []
+ self.localObjects = {}
+ self.security = security
+ self.pageProducers = []
+ self.currentRequestID = 0
+ self.currentLocalID = 0
+ self.unserializingPerspective = None
+ # Some terms:
+ # PUID: process unique ID; return value of id() function. type "int".
+ # LUID: locally unique ID; an ID unique to an object mapped over this
+ # connection. type "int"
+ # GUID: (not used yet) globally unique ID; an ID for an object which
+ # may be on a redirected or meta server. Type as yet undecided.
+ # Dictionary mapping LUIDs to local objects.
+ # set above to allow root object to be assigned before connection is made
+ # self.localObjects = {}
+ # Dictionary mapping PUIDs to LUIDs.
+ self.luids = {}
+ # Dictionary mapping LUIDs to local (remotely cached) objects. Remotely
+ # cached means that they're objects which originate here, and were
+ # copied remotely.
+ self.remotelyCachedObjects = {}
+ # Dictionary mapping PUIDs to (cached) LUIDs
+ self.remotelyCachedLUIDs = {}
+ # Dictionary mapping (remote) LUIDs to (locally cached) objects.
+ self.locallyCachedObjects = {}
+ self.waitingForAnswers = {}
+
+ # Mapping from LUIDs to weakref objects with callbacks for performing
+ # any local cleanup which may be necessary for the corresponding
+ # object once it no longer exists.
+ self._localCleanup = {}
+
+
+ def resumeProducing(self):
+ """Called when the consumer attached to me runs out of buffer.
+ """
+ # Go backwards over the list so we can remove indexes from it as we go
+ for pageridx in xrange(len(self.pageProducers)-1, -1, -1):
+ pager = self.pageProducers[pageridx]
+ pager.sendNextPage()
+ if not pager.stillPaging():
+ del self.pageProducers[pageridx]
+ if not self.pageProducers:
+ self.transport.unregisterProducer()
+
+ # Streaming producer methods; not necessary to implement.
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+ def registerPageProducer(self, pager):
+ self.pageProducers.append(pager)
+ if len(self.pageProducers) == 1:
+ self.transport.registerProducer(self, 0)
+
+ def expressionReceived(self, sexp):
+ """Evaluate an expression as it's received.
+ """
+ if isinstance(sexp, types.ListType):
+ command = sexp[0]
+ methodName = "proto_%s" % command
+ method = getattr(self, methodName, None)
+ if method:
+ method(*sexp[1:])
+ else:
+ self.sendCall("didNotUnderstand", command)
+ else:
+ raise ProtocolError("Non-list expression received.")
+
+
+ def proto_version(self, vnum):
+ """Protocol message: (version version-number)
+
+ Check to make sure that both ends of the protocol are speaking
+ the same version dialect.
+ """
+
+ if vnum != self.version:
+ raise ProtocolError("Version Incompatibility: %s %s" % (self.version, vnum))
+
+
+ def sendCall(self, *exp):
+ """Utility method to send an expression to the other side of the connection.
+ """
+ self.sendEncoded(exp)
+
+ def proto_didNotUnderstand(self, command):
+ """Respond to stock 'C{didNotUnderstand}' message.
+
+ Log the command that was not understood and continue. (Note:
+ this will probably be changed to close the connection or raise
+ an exception in the future.)
+ """
+ log.msg("Didn't understand command: %r" % command)
+
+ def connectionReady(self):
+ """Initialize. Called after Banana negotiation is done.
+ """
+ self.sendCall("version", self.version)
+ for notifier in self.connects:
+ try:
+ notifier()
+ except:
+ log.deferr()
+ self.connects = None
+ if self.factory: # in tests we won't have factory
+ self.factory.clientConnectionMade(self)
+
+ def connectionFailed(self):
+ # XXX should never get called anymore? check!
+ for notifier in self.failures:
+ try:
+ notifier()
+ except:
+ log.deferr()
+ self.failures = None
+
+ waitingForAnswers = None
+
+ def connectionLost(self, reason):
+ """The connection was lost.
+ """
+ self.disconnected = 1
+ # nuke potential circular references.
+ self.luids = None
+ if self.waitingForAnswers:
+ for d in self.waitingForAnswers.values():
+ try:
+ d.errback(failure.Failure(PBConnectionLost(reason)))
+ except:
+ log.deferr()
+ # Assure all Cacheable.stoppedObserving are called
+ for lobj in self.remotelyCachedObjects.values():
+ cacheable = lobj.object
+ perspective = lobj.perspective
+ try:
+ cacheable.stoppedObserving(perspective, RemoteCacheObserver(self, cacheable, perspective))
+ except:
+ log.deferr()
+ # Loop on a copy to prevent notifiers to mixup
+ # the list by calling dontNotifyOnDisconnect
+ for notifier in self.disconnects[:]:
+ try:
+ notifier()
+ except:
+ log.deferr()
+ self.disconnects = None
+ self.waitingForAnswers = None
+ self.localSecurity = None
+ self.remoteSecurity = None
+ self.remotelyCachedObjects = None
+ self.remotelyCachedLUIDs = None
+ self.locallyCachedObjects = None
+ self.localObjects = None
+
+ def notifyOnDisconnect(self, notifier):
+ """Call the given callback when the Broker disconnects."""
+ assert callable(notifier)
+ self.disconnects.append(notifier)
+
+ def notifyOnFail(self, notifier):
+ """Call the given callback if the Broker fails to connect."""
+ assert callable(notifier)
+ self.failures.append(notifier)
+
+ def notifyOnConnect(self, notifier):
+ """Call the given callback when the Broker connects."""
+ assert callable(notifier)
+ if self.connects is None:
+ try:
+ notifier()
+ except:
+ log.err()
+ else:
+ self.connects.append(notifier)
+
+ def dontNotifyOnDisconnect(self, notifier):
+ """Remove a callback from list of disconnect callbacks."""
+ try:
+ self.disconnects.remove(notifier)
+ except ValueError:
+ pass
+
+ def localObjectForID(self, luid):
+ """
+ Get a local object for a locally unique ID.
+
+ @return: An object previously stored with L{registerReference} or
+ C{None} if there is no object which corresponds to the given
+ identifier.
+ """
+ lob = self.localObjects.get(luid)
+ if lob is None:
+ return
+ return lob.object
+
+ maxBrokerRefsViolations = 0
+
+ def registerReference(self, object):
+ """Get an ID for a local object.
+
+ Store a persistent reference to a local object and map its id()
+ to a generated, session-unique ID and return that ID.
+ """
+
+ assert object is not None
+ puid = object.processUniqueID()
+ luid = self.luids.get(puid)
+ if luid is None:
+ if len(self.localObjects) > MAX_BROKER_REFS:
+ self.maxBrokerRefsViolations = self.maxBrokerRefsViolations + 1
+ if self.maxBrokerRefsViolations > 3:
+ self.transport.loseConnection()
+ raise Error("Maximum PB reference count exceeded. "
+ "Goodbye.")
+ raise Error("Maximum PB reference count exceeded.")
+
+ luid = self.newLocalID()
+ self.localObjects[luid] = Local(object)
+ self.luids[puid] = luid
+ else:
+ self.localObjects[luid].incref()
+ return luid
+
+ def setNameForLocal(self, name, object):
+ """Store a special (string) ID for this object.
+
+ This is how you specify a 'base' set of objects that the remote
+ protocol can connect to.
+ """
+ assert object is not None
+ self.localObjects[name] = Local(object)
+
+ def remoteForName(self, name):
+ """Returns an object from the remote name mapping.
+
+ Note that this does not check the validity of the name, only
+ creates a translucent reference for it.
+ """
+ return RemoteReference(None, self, name, 0)
+
+ def cachedRemotelyAs(self, instance, incref=0):
+ """Returns an ID that says what this instance is cached as remotely, or C{None} if it's not.
+ """
+
+ puid = instance.processUniqueID()
+ luid = self.remotelyCachedLUIDs.get(puid)
+ if (luid is not None) and (incref):
+ self.remotelyCachedObjects[luid].incref()
+ return luid
+
+ def remotelyCachedForLUID(self, luid):
+ """Returns an instance which is cached remotely, with this LUID.
+ """
+ return self.remotelyCachedObjects[luid].object
+
+ def cacheRemotely(self, instance):
+ """
+ XXX"""
+ puid = instance.processUniqueID()
+ luid = self.newLocalID()
+ if len(self.remotelyCachedObjects) > MAX_BROKER_REFS:
+ self.maxBrokerRefsViolations = self.maxBrokerRefsViolations + 1
+ if self.maxBrokerRefsViolations > 3:
+ self.transport.loseConnection()
+ raise Error("Maximum PB cache count exceeded. "
+ "Goodbye.")
+ raise Error("Maximum PB cache count exceeded.")
+
+ self.remotelyCachedLUIDs[puid] = luid
+ # This table may not be necessary -- for now, it's to make sure that no
+ # monkey business happens with id(instance)
+ self.remotelyCachedObjects[luid] = Local(instance, self.serializingPerspective)
+ return luid
+
+ def cacheLocally(self, cid, instance):
+ """(internal)
+
+ Store a non-filled-out cached instance locally.
+ """
+ self.locallyCachedObjects[cid] = instance
+
+ def cachedLocallyAs(self, cid):
+ instance = self.locallyCachedObjects[cid]
+ return instance
+
+ def serialize(self, object, perspective=None, method=None, args=None, kw=None):
+ """Jelly an object according to the remote security rules for this broker.
+ """
+
+ if isinstance(object, defer.Deferred):
+ object.addCallbacks(self.serialize, lambda x: x,
+ callbackKeywords={
+ 'perspective': perspective,
+ 'method': method,
+ 'args': args,
+ 'kw': kw
+ })
+ return object
+
+ # XXX This call is NOT REENTRANT and testing for reentrancy is just
+ # crazy, so it likely won't be. Don't ever write methods that call the
+ # broker's serialize() method recursively (e.g. sending a method call
+ # from within a getState (this causes concurrency problems anyway so
+ # you really, really shouldn't do it))
+
+ # self.jellier = _NetJellier(self)
+ self.serializingPerspective = perspective
+ self.jellyMethod = method
+ self.jellyArgs = args
+ self.jellyKw = kw
+ try:
+ return jelly(object, self.security, None, self)
+ finally:
+ self.serializingPerspective = None
+ self.jellyMethod = None
+ self.jellyArgs = None
+ self.jellyKw = None
+
+ def unserialize(self, sexp, perspective = None):
+ """Unjelly an sexp according to the local security rules for this broker.
+ """
+
+ self.unserializingPerspective = perspective
+ try:
+ return unjelly(sexp, self.security, None, self)
+ finally:
+ self.unserializingPerspective = None
+
+ def newLocalID(self):
+ """Generate a new LUID.
+ """
+ self.currentLocalID = self.currentLocalID + 1
+ return self.currentLocalID
+
+ def newRequestID(self):
+ """Generate a new request ID.
+ """
+ self.currentRequestID = self.currentRequestID + 1
+ return self.currentRequestID
+
+ def _sendMessage(self, prefix, perspective, objectID, message, args, kw):
+ pbc = None
+ pbe = None
+ answerRequired = 1
+ if kw.has_key('pbcallback'):
+ pbc = kw['pbcallback']
+ del kw['pbcallback']
+ if kw.has_key('pberrback'):
+ pbe = kw['pberrback']
+ del kw['pberrback']
+ if kw.has_key('pbanswer'):
+ assert (not pbe) and (not pbc), "You can't specify a no-answer requirement."
+ answerRequired = kw['pbanswer']
+ del kw['pbanswer']
+ if self.disconnected:
+ raise DeadReferenceError("Calling Stale Broker")
+ try:
+ netArgs = self.serialize(args, perspective=perspective, method=message)
+ netKw = self.serialize(kw, perspective=perspective, method=message)
+ except:
+ return defer.fail(failure.Failure())
+ requestID = self.newRequestID()
+ if answerRequired:
+ rval = defer.Deferred()
+ self.waitingForAnswers[requestID] = rval
+ if pbc or pbe:
+ log.msg('warning! using deprecated "pbcallback"')
+ rval.addCallbacks(pbc, pbe)
+ else:
+ rval = None
+ self.sendCall(prefix+"message", requestID, objectID, message, answerRequired, netArgs, netKw)
+ return rval
+
+ def proto_message(self, requestID, objectID, message, answerRequired, netArgs, netKw):
+ self._recvMessage(self.localObjectForID, requestID, objectID, message, answerRequired, netArgs, netKw)
+ def proto_cachemessage(self, requestID, objectID, message, answerRequired, netArgs, netKw):
+ self._recvMessage(self.cachedLocallyAs, requestID, objectID, message, answerRequired, netArgs, netKw)
+
+ def _recvMessage(self, findObjMethod, requestID, objectID, message, answerRequired, netArgs, netKw):
+ """Received a message-send.
+
+ Look up message based on object, unserialize the arguments, and
+ invoke it with args, and send an 'answer' or 'error' response.
+ """
+ try:
+ object = findObjMethod(objectID)
+ if object is None:
+ raise Error("Invalid Object ID")
+ netResult = object.remoteMessageReceived(self, message, netArgs, netKw)
+ except Error, e:
+ if answerRequired:
+ # If the error is Jellyable or explicitly allowed via our
+ # security options, send it back and let the code on the
+ # other end deal with unjellying. If it isn't Jellyable,
+ # wrap it in a CopyableFailure, which ensures it can be
+ # unjellied on the other end. We have to do this because
+ # all errors must be sent back.
+ if isinstance(e, Jellyable) or self.security.isClassAllowed(e.__class__):
+ self._sendError(e, requestID)
+ else:
+ self._sendError(CopyableFailure(e), requestID)
+ except:
+ if answerRequired:
+ log.msg("Peer will receive following PB traceback:", isError=True)
+ f = CopyableFailure()
+ self._sendError(f, requestID)
+ log.err()
+ else:
+ if answerRequired:
+ if isinstance(netResult, defer.Deferred):
+ args = (requestID,)
+ netResult.addCallbacks(self._sendAnswer, self._sendFailureOrError,
+ callbackArgs=args, errbackArgs=args)
+ # XXX Should this be done somewhere else?
+ else:
+ self._sendAnswer(netResult, requestID)
+ ##
+ # success
+ ##
+
+ def _sendAnswer(self, netResult, requestID):
+ """(internal) Send an answer to a previously sent message.
+ """
+ self.sendCall("answer", requestID, netResult)
+
+ def proto_answer(self, requestID, netResult):
+ """(internal) Got an answer to a previously sent message.
+
+ Look up the appropriate callback and call it.
+ """
+ d = self.waitingForAnswers[requestID]
+ del self.waitingForAnswers[requestID]
+ d.callback(self.unserialize(netResult))
+
+ ##
+ # failure
+ ##
+ def _sendFailureOrError(self, fail, requestID):
+ """
+ Call L{_sendError} or L{_sendFailure}, depending on whether C{fail}
+ represents an L{Error} subclass or not.
+ """
+ if fail.check(Error) is None:
+ self._sendFailure(fail, requestID)
+ else:
+ self._sendError(fail, requestID)
+
+
+ def _sendFailure(self, fail, requestID):
+ """Log error and then send it."""
+ log.msg("Peer will receive following PB traceback:")
+ log.err(fail)
+ self._sendError(fail, requestID)
+
+ def _sendError(self, fail, requestID):
+ """(internal) Send an error for a previously sent message.
+ """
+ if isinstance(fail, failure.Failure):
+ # If the failures value is jellyable or allowed through security,
+ # send the value
+ if (isinstance(fail.value, Jellyable) or
+ self.security.isClassAllowed(fail.value.__class__)):
+ fail = fail.value
+ elif not isinstance(fail, CopyableFailure):
+ fail = failure2Copyable(fail, self.factory.unsafeTracebacks)
+ if isinstance(fail, CopyableFailure):
+ fail.unsafeTracebacks = self.factory.unsafeTracebacks
+ self.sendCall("error", requestID, self.serialize(fail))
+
+ def proto_error(self, requestID, fail):
+ """(internal) Deal with an error.
+ """
+ d = self.waitingForAnswers[requestID]
+ del self.waitingForAnswers[requestID]
+ d.errback(self.unserialize(fail))
+
+ ##
+ # refcounts
+ ##
+
+ def sendDecRef(self, objectID):
+ """(internal) Send a DECREF directive.
+ """
+ self.sendCall("decref", objectID)
+
+ def proto_decref(self, objectID):
+ """(internal) Decrement the reference count of an object.
+
+ If the reference count is zero, it will free the reference to this
+ object.
+ """
+ refs = self.localObjects[objectID].decref()
+ if refs == 0:
+ puid = self.localObjects[objectID].object.processUniqueID()
+ del self.luids[puid]
+ del self.localObjects[objectID]
+ self._localCleanup.pop(puid, lambda: None)()
+
+ ##
+ # caching
+ ##
+
+ def decCacheRef(self, objectID):
+ """(internal) Send a DECACHE directive.
+ """
+ self.sendCall("decache", objectID)
+
+ def proto_decache(self, objectID):
+ """(internal) Decrement the reference count of a cached object.
+
+ If the reference count is zero, free the reference, then send an
+ 'uncached' directive.
+ """
+ refs = self.remotelyCachedObjects[objectID].decref()
+ # log.msg('decaching: %s #refs: %s' % (objectID, refs))
+ if refs == 0:
+ lobj = self.remotelyCachedObjects[objectID]
+ cacheable = lobj.object
+ perspective = lobj.perspective
+ # TODO: force_decache needs to be able to force-invalidate a
+ # cacheable reference.
+ try:
+ cacheable.stoppedObserving(perspective, RemoteCacheObserver(self, cacheable, perspective))
+ except:
+ log.deferr()
+ puid = cacheable.processUniqueID()
+ del self.remotelyCachedLUIDs[puid]
+ del self.remotelyCachedObjects[objectID]
+ self.sendCall("uncache", objectID)
+
+ def proto_uncache(self, objectID):
+ """(internal) Tell the client it is now OK to uncache an object.
+ """
+ # log.msg("uncaching locally %d" % objectID)
+ obj = self.locallyCachedObjects[objectID]
+ obj.broker = None
+## def reallyDel(obj=obj):
+## obj.__really_del__()
+## obj.__del__ = reallyDel
+ del self.locallyCachedObjects[objectID]
+
+
+
+def respond(challenge, password):
+ """Respond to a challenge.
+
+ This is useful for challenge/response authentication.
+ """
+ m = md5()
+ m.update(password)
+ hashedPassword = m.digest()
+ m = md5()
+ m.update(hashedPassword)
+ m.update(challenge)
+ doubleHashedPassword = m.digest()
+ return doubleHashedPassword
+
+def challenge():
+ """I return some random data."""
+ crap = ''
+ for x in range(random.randrange(15,25)):
+ crap = crap + chr(random.randint(65,90))
+ crap = md5(crap).digest()
+ return crap
+
+
+class PBClientFactory(protocol.ClientFactory):
+ """
+ Client factory for PB brokers.
+
+ As with all client factories, use with reactor.connectTCP/SSL/etc..
+ getPerspective and getRootObject can be called either before or
+ after the connect.
+ """
+
+ protocol = Broker
+ unsafeTracebacks = False
+
+ def __init__(self, unsafeTracebacks=False, security=globalSecurity):
+ """
+ @param unsafeTracebacks: if set, tracebacks for exceptions will be sent
+ over the wire.
+ @type unsafeTracebacks: C{bool}
+
+ @param security: security options used by the broker, default to
+ C{globalSecurity}.
+ @type security: L{twisted.spread.jelly.SecurityOptions}
+ """
+ self.unsafeTracebacks = unsafeTracebacks
+ self.security = security
+ self._reset()
+
+
+ def buildProtocol(self, addr):
+ """
+ Build the broker instance, passing the security options to it.
+ """
+ p = self.protocol(isClient=True, security=self.security)
+ p.factory = self
+ return p
+
+
+ def _reset(self):
+ self.rootObjectRequests = [] # list of deferred
+ self._broker = None
+ self._root = None
+
+ def _failAll(self, reason):
+ deferreds = self.rootObjectRequests
+ self._reset()
+ for d in deferreds:
+ d.errback(reason)
+
+ def clientConnectionFailed(self, connector, reason):
+ self._failAll(reason)
+
+ def clientConnectionLost(self, connector, reason, reconnecting=0):
+ """Reconnecting subclasses should call with reconnecting=1."""
+ if reconnecting:
+ # any pending requests will go to next connection attempt
+ # so we don't fail them.
+ self._broker = None
+ self._root = None
+ else:
+ self._failAll(reason)
+
+ def clientConnectionMade(self, broker):
+ self._broker = broker
+ self._root = broker.remoteForName("root")
+ ds = self.rootObjectRequests
+ self.rootObjectRequests = []
+ for d in ds:
+ d.callback(self._root)
+
+ def getRootObject(self):
+ """Get root object of remote PB server.
+
+ @return: Deferred of the root object.
+ """
+ if self._broker and not self._broker.disconnected:
+ return defer.succeed(self._root)
+ d = defer.Deferred()
+ self.rootObjectRequests.append(d)
+ return d
+
+ def disconnect(self):
+ """If the factory is connected, close the connection.
+
+ Note that if you set up the factory to reconnect, you will need to
+ implement extra logic to prevent automatic reconnection after this
+ is called.
+ """
+ if self._broker:
+ self._broker.transport.loseConnection()
+
+ def _cbSendUsername(self, root, username, password, client):
+ return root.callRemote("login", username).addCallback(
+ self._cbResponse, password, client)
+
+ def _cbResponse(self, (challenge, challenger), password, client):
+ return challenger.callRemote("respond", respond(challenge, password), client)
+
+
+ def _cbLoginAnonymous(self, root, client):
+ """
+ Attempt an anonymous login on the given remote root object.
+
+ @type root: L{RemoteReference}
+ @param root: The object on which to attempt the login, most likely
+ returned by a call to L{PBClientFactory.getRootObject}.
+
+ @param client: A jellyable object which will be used as the I{mind}
+ parameter for the login attempt.
+
+ @rtype: L{Deferred}
+ @return: A L{Deferred} which will be called back with a
+ L{RemoteReference} to an avatar when anonymous login succeeds, or
+ which will errback if anonymous login fails.
+ """
+ return root.callRemote("loginAnonymous", client)
+
+
+ def login(self, credentials, client=None):
+ """
+ Login and get perspective from remote PB server.
+
+ Currently the following credentials are supported::
+
+ L{twisted.cred.credentials.IUsernamePassword}
+ L{twisted.cred.credentials.IAnonymous}
+
+ @rtype: L{Deferred}
+ @return: A L{Deferred} which will be called back with a
+ L{RemoteReference} for the avatar logged in to, or which will
+ errback if login fails.
+ """
+ d = self.getRootObject()
+
+ if IAnonymous.providedBy(credentials):
+ d.addCallback(self._cbLoginAnonymous, client)
+ else:
+ d.addCallback(
+ self._cbSendUsername, credentials.username,
+ credentials.password, client)
+ return d
+
+
+
+class PBServerFactory(protocol.ServerFactory):
+ """
+ Server factory for perspective broker.
+
+ Login is done using a Portal object, whose realm is expected to return
+ avatars implementing IPerspective. The credential checkers in the portal
+ should accept IUsernameHashedPassword or IUsernameMD5Password.
+
+ Alternatively, any object providing or adaptable to L{IPBRoot} can be
+ used instead of a portal to provide the root object of the PB server.
+ """
+
+ unsafeTracebacks = False
+
+ # object broker factory
+ protocol = Broker
+
+ def __init__(self, root, unsafeTracebacks=False, security=globalSecurity):
+ """
+ @param root: factory providing the root Referenceable used by the broker.
+ @type root: object providing or adaptable to L{IPBRoot}.
+
+ @param unsafeTracebacks: if set, tracebacks for exceptions will be sent
+ over the wire.
+ @type unsafeTracebacks: C{bool}
+
+ @param security: security options used by the broker, default to
+ C{globalSecurity}.
+ @type security: L{twisted.spread.jelly.SecurityOptions}
+ """
+ self.root = IPBRoot(root)
+ self.unsafeTracebacks = unsafeTracebacks
+ self.security = security
+
+
+ def buildProtocol(self, addr):
+ """
+ Return a Broker attached to the factory (as the service provider).
+ """
+ proto = self.protocol(isClient=False, security=self.security)
+ proto.factory = self
+ proto.setNameForLocal("root", self.root.rootObject(proto))
+ return proto
+
+ def clientConnectionMade(self, protocol):
+ # XXX does this method make any sense?
+ pass
+
+
+class IUsernameMD5Password(ICredentials):
+ """
+ I encapsulate a username and a hashed password.
+
+ This credential is used for username/password over PB. CredentialCheckers
+ which check this kind of credential must store the passwords in plaintext
+ form or as a MD5 digest.
+
+ @type username: C{str} or C{Deferred}
+ @ivar username: The username associated with these credentials.
+ """
+
+ def checkPassword(password):
+ """
+ Validate these credentials against the correct password.
+
+ @type password: C{str}
+ @param password: The correct, plaintext password against which to
+ check.
+
+ @rtype: C{bool} or L{Deferred}
+ @return: C{True} if the credentials represented by this object match the
+ given password, C{False} if they do not, or a L{Deferred} which will
+ be called back with one of these values.
+ """
+
+ def checkMD5Password(password):
+ """
+ Validate these credentials against the correct MD5 digest of the
+ password.
+
+ @type password: C{str}
+ @param password: The correct MD5 digest of a password against which to
+ check.
+
+ @rtype: C{bool} or L{Deferred}
+ @return: C{True} if the credentials represented by this object match the
+ given digest, C{False} if they do not, or a L{Deferred} which will
+ be called back with one of these values.
+ """
+
+
+class _PortalRoot:
+ """Root object, used to login to portal."""
+
+ implements(IPBRoot)
+
+ def __init__(self, portal):
+ self.portal = portal
+
+ def rootObject(self, broker):
+ return _PortalWrapper(self.portal, broker)
+
+registerAdapter(_PortalRoot, Portal, IPBRoot)
+
+
+
+class _JellyableAvatarMixin:
+ """
+ Helper class for code which deals with avatars which PB must be capable of
+ sending to a peer.
+ """
+ def _cbLogin(self, (interface, avatar, logout)):
+ """
+ Ensure that the avatar to be returned to the client is jellyable and
+ set up disconnection notification to call the realm's logout object.
+ """
+ if not IJellyable.providedBy(avatar):
+ avatar = AsReferenceable(avatar, "perspective")
+
+ puid = avatar.processUniqueID()
+
+ # only call logout once, whether the connection is dropped (disconnect)
+ # or a logout occurs (cleanup), and be careful to drop the reference to
+ # it in either case
+ logout = [ logout ]
+ def maybeLogout():
+ if not logout:
+ return
+ fn = logout[0]
+ del logout[0]
+ fn()
+ self.broker._localCleanup[puid] = maybeLogout
+ self.broker.notifyOnDisconnect(maybeLogout)
+
+ return avatar
+
+
+
+class _PortalWrapper(Referenceable, _JellyableAvatarMixin):
+ """
+ Root Referenceable object, used to login to portal.
+ """
+
+ def __init__(self, portal, broker):
+ self.portal = portal
+ self.broker = broker
+
+
+ def remote_login(self, username):
+ """
+ Start of username/password login.
+ """
+ c = challenge()
+ return c, _PortalAuthChallenger(self.portal, self.broker, username, c)
+
+
+ def remote_loginAnonymous(self, mind):
+ """
+ Attempt an anonymous login.
+
+ @param mind: An object to use as the mind parameter to the portal login
+ call (possibly None).
+
+ @rtype: L{Deferred}
+ @return: A Deferred which will be called back with an avatar when login
+ succeeds or which will be errbacked if login fails somehow.
+ """
+ d = self.portal.login(Anonymous(), mind, IPerspective)
+ d.addCallback(self._cbLogin)
+ return d
+
+
+
+class _PortalAuthChallenger(Referenceable, _JellyableAvatarMixin):
+ """
+ Called with response to password challenge.
+ """
+ implements(IUsernameHashedPassword, IUsernameMD5Password)
+
+ def __init__(self, portal, broker, username, challenge):
+ self.portal = portal
+ self.broker = broker
+ self.username = username
+ self.challenge = challenge
+
+
+ def remote_respond(self, response, mind):
+ self.response = response
+ d = self.portal.login(self, mind, IPerspective)
+ d.addCallback(self._cbLogin)
+ return d
+
+
+ # IUsernameHashedPassword:
+ def checkPassword(self, password):
+ return self.checkMD5Password(md5(password).digest())
+
+
+ # IUsernameMD5Password
+ def checkMD5Password(self, md5Password):
+ md = md5()
+ md.update(md5Password)
+ md.update(self.challenge)
+ correct = md.digest()
+ return self.response == correct
+
+
+__all__ = [
+ # Everything from flavors is exposed publically here.
+ 'IPBRoot', 'Serializable', 'Referenceable', 'NoSuchMethod', 'Root',
+ 'ViewPoint', 'Viewable', 'Copyable', 'Jellyable', 'Cacheable',
+ 'RemoteCopy', 'RemoteCache', 'RemoteCacheObserver', 'copyTags',
+ 'setUnjellyableForClass', 'setUnjellyableFactoryForClass',
+ 'setUnjellyableForClassTree',
+ 'setCopierForClass', 'setFactoryForClass', 'setCopierForClassTree',
+
+ 'MAX_BROKER_REFS', 'portno',
+
+ 'ProtocolError', 'DeadReferenceError', 'Error', 'PBConnectionLost',
+ 'RemoteMethod', 'IPerspective', 'Avatar', 'AsReferenceable',
+ 'RemoteReference', 'CopyableFailure', 'CopiedFailure', 'failure2Copyable',
+ 'Broker', 'respond', 'challenge', 'PBClientFactory', 'PBServerFactory',
+ 'IUsernameMD5Password',
+ ]
diff --git a/twisted/spread/publish.py b/twisted/spread/publish.py
new file mode 100644
index 0000000..5bc1868
--- /dev/null
+++ b/twisted/spread/publish.py
@@ -0,0 +1,142 @@
+# -*- test-case-name: twisted.test.test_pb -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Persistently cached objects for PB.
+
+Maintainer: Glyph Lefkowitz
+
+Future Plans: None known.
+"""
+
+import time
+
+from twisted.internet import defer
+from twisted.spread import banana, jelly, flavors
+
+
+class Publishable(flavors.Cacheable):
+ """An object whose cached state persists across sessions.
+ """
+ def __init__(self, publishedID):
+ self.republish()
+ self.publishedID = publishedID
+
+ def republish(self):
+ """Set the timestamp to current and (TODO) update all observers.
+ """
+ self.timestamp = time.time()
+
+ def view_getStateToPublish(self, perspective):
+ '(internal)'
+ return self.getStateToPublishFor(perspective)
+
+ def getStateToPublishFor(self, perspective):
+ """Implement me to special-case your state for a perspective.
+ """
+ return self.getStateToPublish()
+
+ def getStateToPublish(self):
+ """Implement me to return state to copy as part of the publish phase.
+ """
+ raise NotImplementedError("%s.getStateToPublishFor" % self.__class__)
+
+ def getStateToCacheAndObserveFor(self, perspective, observer):
+ """Get all necessary metadata to keep a clientside cache.
+ """
+ if perspective:
+ pname = perspective.perspectiveName
+ sname = perspective.getService().serviceName
+ else:
+ pname = "None"
+ sname = "None"
+
+ return {"remote": flavors.ViewPoint(perspective, self),
+ "publishedID": self.publishedID,
+ "perspective": pname,
+ "service": sname,
+ "timestamp": self.timestamp}
+
+class RemotePublished(flavors.RemoteCache):
+ """The local representation of remote Publishable object.
+ """
+ isActivated = 0
+ _wasCleanWhenLoaded = 0
+ def getFileName(self, ext='pub'):
+ return ("%s-%s-%s.%s" %
+ (self.service, self.perspective, str(self.publishedID), ext))
+
+ def setCopyableState(self, state):
+ self.__dict__.update(state)
+ self._activationListeners = []
+ try:
+ dataFile = file(self.getFileName(), "rb")
+ data = dataFile.read()
+ dataFile.close()
+ except IOError:
+ recent = 0
+ else:
+ newself = jelly.unjelly(banana.decode(data))
+ recent = (newself.timestamp == self.timestamp)
+ if recent:
+ self._cbGotUpdate(newself.__dict__)
+ self._wasCleanWhenLoaded = 1
+ else:
+ self.remote.callRemote('getStateToPublish').addCallbacks(self._cbGotUpdate)
+
+ def __getstate__(self):
+ other = self.__dict__.copy()
+ # Remove PB-specific attributes
+ del other['broker']
+ del other['remote']
+ del other['luid']
+ # remove my own runtime-tracking stuff
+ del other['_activationListeners']
+ del other['isActivated']
+ return other
+
+ def _cbGotUpdate(self, newState):
+ self.__dict__.update(newState)
+ self.isActivated = 1
+ # send out notifications
+ for listener in self._activationListeners:
+ listener(self)
+ self._activationListeners = []
+ self.activated()
+ dataFile = file(self.getFileName(), "wb")
+ dataFile.write(banana.encode(jelly.jelly(self)))
+ dataFile.close()
+
+
+ def activated(self):
+ """Implement this method if you want to be notified when your
+ publishable subclass is activated.
+ """
+
+ def callWhenActivated(self, callback):
+ """Externally register for notification when this publishable has received all relevant data.
+ """
+ if self.isActivated:
+ callback(self)
+ else:
+ self._activationListeners.append(callback)
+
+def whenReady(d):
+ """
+ Wrap a deferred returned from a pb method in another deferred that
+ expects a RemotePublished as a result. This will allow you to wait until
+ the result is really available.
+
+ Idiomatic usage would look like::
+
+ publish.whenReady(serverObject.getMeAPublishable()).addCallback(lookAtThePublishable)
+ """
+ d2 = defer.Deferred()
+ d.addCallbacks(_pubReady, d2.errback,
+ callbackArgs=(d2,))
+ return d2
+
+def _pubReady(result, d2):
+ '(internal)'
+ result.callWhenActivated(d2.callback)
diff --git a/twisted/spread/ui/__init__.py b/twisted/spread/ui/__init__.py
new file mode 100644
index 0000000..56bf766
--- /dev/null
+++ b/twisted/spread/ui/__init__.py
@@ -0,0 +1,12 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Spread UI: UI utilities for various toolkits connecting to PB.
+"""
+
+# Undeprecating this until someone figures out a real plan for alternatives to spread.ui.
+##import warnings
+##warnings.warn("twisted.spread.ui is deprecated. Please do not use.", DeprecationWarning)
diff --git a/twisted/spread/ui/gtk2util.py b/twisted/spread/ui/gtk2util.py
new file mode 100644
index 0000000..6faaccb
--- /dev/null
+++ b/twisted/spread/ui/gtk2util.py
@@ -0,0 +1,222 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from __future__ import nested_scopes
+
+import gtk
+
+from twisted import copyright
+from twisted.internet import defer
+from twisted.python import failure, log, util
+from twisted.spread import pb
+from twisted.cred.credentials import UsernamePassword
+
+from twisted.internet import error as netError
+
+def login(client=None, **defaults):
+ """
+ @param host:
+ @param port:
+ @param identityName:
+ @param password:
+ @param serviceName:
+ @param perspectiveName:
+
+ @returntype: Deferred RemoteReference of Perspective
+ """
+ d = defer.Deferred()
+ LoginDialog(client, d, defaults)
+ return d
+
+class GladeKeeper:
+ """
+ @cvar gladefile: The file in which the glade GUI definition is kept.
+ @type gladefile: str
+
+ @cvar _widgets: Widgets that should be attached to me as attributes.
+ @type _widgets: list of strings
+ """
+
+ gladefile = None
+ _widgets = ()
+
+ def __init__(self):
+ from gtk import glade
+ self.glade = glade.XML(self.gladefile)
+
+ # mold can go away when we get a newer pygtk (post 1.99.14)
+ mold = {}
+ for k in dir(self):
+ mold[k] = getattr(self, k)
+ self.glade.signal_autoconnect(mold)
+ self._setWidgets()
+
+ def _setWidgets(self):
+ get_widget = self.glade.get_widget
+ for widgetName in self._widgets:
+ setattr(self, "_" + widgetName, get_widget(widgetName))
+
+
+class LoginDialog(GladeKeeper):
+ # IdentityConnector host port identityName password
+ # requestLogin -> identityWrapper or login failure
+ # requestService serviceName perspectiveName client
+
+ # window killed
+ # cancel button pressed
+ # login button activated
+
+ fields = ['host','port','identityName','password',
+ 'perspectiveName']
+
+ _widgets = ("hostEntry", "portEntry", "identityNameEntry", "passwordEntry",
+ "perspectiveNameEntry", "statusBar",
+ "loginDialog")
+
+ _advancedControls = ['perspectiveLabel', 'perspectiveNameEntry',
+ 'protocolLabel', 'versionLabel']
+
+ gladefile = util.sibpath(__file__, "login2.glade")
+
+ _timeoutID = None
+
+ def __init__(self, client, deferred, defaults):
+ self.client = client
+ self.deferredResult = deferred
+
+ GladeKeeper.__init__(self)
+
+ self.setDefaults(defaults)
+ self._loginDialog.show()
+
+
+ def setDefaults(self, defaults):
+ if not defaults.has_key('port'):
+ defaults['port'] = str(pb.portno)
+ elif isinstance(defaults['port'], (int, long)):
+ defaults['port'] = str(defaults['port'])
+
+ for k, v in defaults.iteritems():
+ if k in self.fields:
+ widget = getattr(self, "_%sEntry" % (k,))
+ widget.set_text(v)
+
+ def _setWidgets(self):
+ GladeKeeper._setWidgets(self)
+ self._statusContext = self._statusBar.get_context_id("Login dialog.")
+ get_widget = self.glade.get_widget
+ get_widget("versionLabel").set_text(copyright.longversion)
+ get_widget("protocolLabel").set_text("Protocol PB-%s" %
+ (pb.Broker.version,))
+
+ def _on_loginDialog_response(self, widget, response):
+ handlers = {gtk.RESPONSE_NONE: self._windowClosed,
+ gtk.RESPONSE_DELETE_EVENT: self._windowClosed,
+ gtk.RESPONSE_OK: self._doLogin,
+ gtk.RESPONSE_CANCEL: self._cancelled}
+ handler = handlers.get(response)
+ if handler is not None:
+ handler()
+ else:
+ log.msg("Unexpected dialog response %r from %s" % (response,
+ widget))
+
+ def _on_loginDialog_close(self, widget, userdata=None):
+ self._windowClosed()
+
+ def _on_loginDialog_destroy_event(self, widget, userdata=None):
+ self._windowClosed()
+
+ def _cancelled(self):
+ if not self.deferredResult.called:
+ self.deferredResult.errback(netError.UserError("User hit Cancel."))
+ self._loginDialog.destroy()
+
+ def _windowClosed(self, reason=None):
+ if not self.deferredResult.called:
+ self.deferredResult.errback(netError.UserError("Window closed."))
+
+ def _doLogin(self):
+ idParams = {}
+
+ idParams['host'] = self._hostEntry.get_text()
+ idParams['port'] = self._portEntry.get_text()
+ idParams['identityName'] = self._identityNameEntry.get_text()
+ idParams['password'] = self._passwordEntry.get_text()
+
+ try:
+ idParams['port'] = int(idParams['port'])
+ except ValueError:
+ pass
+
+ f = pb.PBClientFactory()
+ from twisted.internet import reactor
+ reactor.connectTCP(idParams['host'], idParams['port'], f)
+ creds = UsernamePassword(idParams['identityName'], idParams['password'])
+ d = f.login(creds, self.client)
+ def _timeoutLogin():
+ self._timeoutID = None
+ d.errback(failure.Failure(defer.TimeoutError("Login timed out.")))
+ self._timeoutID = reactor.callLater(30, _timeoutLogin)
+ d.addCallbacks(self._cbGotPerspective, self._ebFailedLogin)
+ self.statusMsg("Contacting server...")
+
+ # serviceName = self._serviceNameEntry.get_text()
+ # perspectiveName = self._perspectiveNameEntry.get_text()
+ # if not perspectiveName:
+ # perspectiveName = idParams['identityName']
+
+ # d = _identityConnector.requestService(serviceName, perspectiveName,
+ # self.client)
+ # d.addCallbacks(self._cbGotPerspective, self._ebFailedLogin)
+ # setCursor to waiting
+
+ def _cbGotPerspective(self, perspective):
+ self.statusMsg("Connected to server.")
+ if self._timeoutID is not None:
+ self._timeoutID.cancel()
+ self._timeoutID = None
+ self.deferredResult.callback(perspective)
+ # clear waiting cursor
+ self._loginDialog.destroy()
+
+ def _ebFailedLogin(self, reason):
+ if isinstance(reason, failure.Failure):
+ reason = reason.value
+ self.statusMsg(reason)
+ if isinstance(reason, (unicode, str)):
+ text = reason
+ else:
+ text = unicode(reason)
+ msg = gtk.MessageDialog(self._loginDialog,
+ gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_ERROR,
+ gtk.BUTTONS_CLOSE,
+ text)
+ msg.show_all()
+ msg.connect("response", lambda *a: msg.destroy())
+
+ # hostname not found
+ # host unreachable
+ # connection refused
+ # authentication failed
+ # no such service
+ # no such perspective
+ # internal server error
+
+ def _on_advancedButton_toggled(self, widget, userdata=None):
+ active = widget.get_active()
+ if active:
+ op = "show"
+ else:
+ op = "hide"
+ for widgetName in self._advancedControls:
+ widget = self.glade.get_widget(widgetName)
+ getattr(widget, op)()
+
+ def statusMsg(self, text):
+ if not isinstance(text, (unicode, str)):
+ text = unicode(text)
+ return self._statusBar.push(self._statusContext, text)
diff --git a/twisted/spread/ui/login2.glade b/twisted/spread/ui/login2.glade
new file mode 100644
index 0000000..af8c53d
--- /dev/null
+++ b/twisted/spread/ui/login2.glade
@@ -0,0 +1,461 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkDialog" id="loginDialog">
+ <property name="title" translatable="yes">Login</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_NONE</property>
+ <property name="modal">False</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="has_separator">True</property>
+ <signal name="response" handler="_on_loginDialog_response" last_modification_time="Sat, 25 Jan 2003 13:52:57 GMT"/>
+ <signal name="close" handler="_on_loginDialog_close" last_modification_time="Sat, 25 Jan 2003 13:53:04 GMT"/>
+
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+ <child>
+ <widget class="GtkButton" id="cancelbutton1">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="label">gtk-cancel</property>
+ <property name="use_stock">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="response_id">-6</property>
+ </widget>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="loginButton">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="response_id">-5</property>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">2</property>
+
+ <child>
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="stock">gtk-ok</property>
+ <property name="icon_size">4</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="label9">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Login</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkStatusbar" id="statusBar">
+ <property name="visible">True</property>
+ <property name="has_resize_grip">False</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">6</property>
+ <property name="n_columns">2</property>
+ <property name="homogeneous">False</property>
+ <property name="row_spacing">2</property>
+ <property name="column_spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="hostLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Host:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">hostEntry</property>
+ <accessibility>
+ <atkrelation target="hostEntry" type="label-for"/>
+ <atkrelation target="portEntry" type="label-for"/>
+ </accessibility>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkEntry" id="hostEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The name of a host to connect to.</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">localhost</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ <accessibility>
+ <atkrelation target="hostLabel" type="labelled-by"/>
+ </accessibility>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="portEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The number of a port to connect on.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes">8787</property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ <property name="width_chars">5</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="y_options">fill</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="nameLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Name:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">identityNameEntry</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="identityNameEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">An identity to log in as.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes"></property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="passwordEntry">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">The Identity's log-in password.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">False</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes"></property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="passwordLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Password:</property>
+ <property name="use_underline">True</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="mnemonic_widget">passwordEntry</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="perspectiveLabel">
+ <property name="label" translatable="yes">Perspective:</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.9</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">1</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkEntry" id="perspectiveNameEntry">
+ <property name="tooltip" translatable="yes">The name of a Perspective to request.</property>
+ <property name="can_focus">True</property>
+ <property name="editable">True</property>
+ <property name="visibility">True</property>
+ <property name="max_length">0</property>
+ <property name="text" translatable="yes"></property>
+ <property name="has_frame">True</property>
+ <property name="invisible_char" translatable="yes">*</property>
+ <property name="activates_default">False</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">5</property>
+ <property name="bottom_attach">6</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="protocolLabel">
+ <property name="label" translatable="yes">Insert Protocol Version Here</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="versionLabel">
+ <property name="label" translatable="yes">Insert Twisted Version Here</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">False</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">4</property>
+ <property name="bottom_attach">5</property>
+ <property name="x_options">fill</property>
+ <property name="y_options">fill</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">0</property>
+ <property name="yscale">1</property>
+
+ <child>
+ <widget class="GtkToggleButton" id="advancedButton">
+ <property name="visible">True</property>
+ <property name="tooltip" translatable="yes">Advanced options.</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Advanced &gt;&gt;</property>
+ <property name="use_underline">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="active">False</property>
+ <property name="inconsistent">False</property>
+ <signal name="toggled" handler="_on_advancedButton_toggled" object="Login" last_modification_time="Sat, 25 Jan 2003 13:47:17 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options"></property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
diff --git a/twisted/spread/ui/tktree.py b/twisted/spread/ui/tktree.py
new file mode 100644
index 0000000..8fbe462
--- /dev/null
+++ b/twisted/spread/ui/tktree.py
@@ -0,0 +1,204 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+What I want it to look like:
+
++- One
+| \- Two
+| |- Three
+| |- Four
+| +- Five
+| | \- Six
+| |- Seven
++- Eight
+| \- Nine
+"""
+
+import os
+from Tkinter import *
+
+class Node:
+ def __init__(self):
+ """
+ Do whatever you want here.
+ """
+ self.item=None
+ def getName(self):
+ """
+ Return the name of this node in the tree.
+ """
+ pass
+ def isExpandable(self):
+ """
+ Return true if this node is expandable.
+ """
+ return len(self.getSubNodes())>0
+ def getSubNodes(self):
+ """
+ Return the sub nodes of this node.
+ """
+ return []
+ def gotDoubleClick(self):
+ """
+ Called when we are double clicked.
+ """
+ pass
+ def updateMe(self):
+ """
+ Call me when something about me changes, so that my representation
+ changes.
+ """
+ if self.item:
+ self.item.update()
+
+class FileNode(Node):
+ def __init__(self,name):
+ Node.__init__(self)
+ self.name=name
+ def getName(self):
+ return os.path.basename(self.name)
+ def isExpandable(self):
+ return os.path.isdir(self.name)
+ def getSubNodes(self):
+ names=map(lambda x,n=self.name:os.path.join(n,x),os.listdir(self.name))
+ return map(FileNode,names)
+
+class TreeItem:
+ def __init__(self,widget,parent,node):
+ self.widget=widget
+ self.node=node
+ node.item=self
+ if self.node.isExpandable():
+ self.expand=0
+ else:
+ self.expand=None
+ self.parent=parent
+ if parent:
+ self.level=self.parent.level+1
+ else:
+ self.level=0
+ self.first=0 # gets set in Tree.expand()
+ self.subitems=[]
+ def __del__(self):
+ del self.node
+ del self.widget
+ def __repr__(self):
+ return "<Item for Node %s at level %s>"%(self.node.getName(),self.level)
+ def render(self):
+ """
+ Override in a subclass.
+ """
+ raise NotImplementedError
+ def update(self):
+ self.widget.update(self)
+
+class ListboxTreeItem(TreeItem):
+ def render(self):
+ start=self.level*"| "
+ if self.expand==None and not self.first:
+ start=start+"|"
+ elif self.expand==0:
+ start=start+"L"
+ elif self.expand==1:
+ start=start+"+"
+ else:
+ start=start+"\\"
+ r=[start+"- "+self.node.getName()]
+ if self.expand:
+ for i in self.subitems:
+ r.extend(i.render())
+ return r
+
+class ListboxTree:
+ def __init__(self,parent=None,**options):
+ self.box=apply(Listbox,[parent],options)
+ self.box.bind("<Double-1>",self.flip)
+ self.roots=[]
+ self.items=[]
+ def pack(self,*args,**kw):
+ """
+ for packing.
+ """
+ apply(self.box.pack,args,kw)
+ def grid(self,*args,**kw):
+ """
+ for gridding.
+ """
+ apply(self.box.grid,args,kw)
+ def yview(self,*args,**kw):
+ """
+ for scrolling.
+ """
+ apply(self.box.yview,args,kw)
+ def addRoot(self,node):
+ r=ListboxTreeItem(self,None,node)
+ self.roots.append(r)
+ self.items.append(r)
+ self.box.insert(END,r.render()[0])
+ return r
+ def curselection(self):
+ c=self.box.curselection()
+ if not c: return
+ return self.items[int(c[0])]
+ def flip(self,*foo):
+ if not self.box.curselection(): return
+ item=self.items[int(self.box.curselection()[0])]
+ if item.expand==None: return
+ if not item.expand:
+ self.expand(item)
+ else:
+ self.close(item)
+ item.node.gotDoubleClick()
+ def expand(self,item):
+ if item.expand or item.expand==None: return
+ item.expand=1
+ item.subitems=map(lambda x,i=item,s=self:ListboxTreeItem(s,i,x),item.node.getSubNodes())
+ if item.subitems:
+ item.subitems[0].first=1
+ i=self.items.index(item)
+ self.items,after=self.items[:i+1],self.items[i+1:]
+ self.items=self.items+item.subitems+after
+ c=self.items.index(item)
+ self.box.delete(c)
+ r=item.render()
+ for i in r:
+ self.box.insert(c,i)
+ c=c+1
+ def close(self,item):
+ if not item.expand: return
+ item.expand=0
+ length=len(item.subitems)
+ for i in item.subitems:
+ self.close(i)
+ c=self.items.index(item)
+ del self.items[c+1:c+1+length]
+ for i in range(length+1):
+ self.box.delete(c)
+ self.box.insert(c,item.render()[0])
+ def remove(self,item):
+ if item.expand:
+ self.close(item)
+ c=self.items.index(item)
+ del self.items[c]
+ if item.parent:
+ item.parent.subitems.remove(item)
+ self.box.delete(c)
+ def update(self,item):
+ if item.expand==None:
+ c=self.items.index(item)
+ self.box.delete(c)
+ self.box.insert(c,item.render()[0])
+ elif item.expand:
+ self.close(item)
+ self.expand(item)
+
+if __name__=="__main__":
+ tk=Tk()
+ s=Scrollbar()
+ t=ListboxTree(tk,yscrollcommand=s.set)
+ t.pack(side=LEFT,fill=BOTH)
+ s.config(command=t.yview)
+ s.pack(side=RIGHT,fill=Y)
+ t.addRoot(FileNode("C:/"))
+ #mainloop()
diff --git a/twisted/spread/ui/tkutil.py b/twisted/spread/ui/tkutil.py
new file mode 100644
index 0000000..2aee67d
--- /dev/null
+++ b/twisted/spread/ui/tkutil.py
@@ -0,0 +1,397 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Utilities for building L{PB<twisted.spread.pb>} clients with L{Tkinter}.
+"""
+from Tkinter import *
+from tkSimpleDialog import _QueryString
+from tkFileDialog import _Dialog
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted import copyright
+
+import string
+
+#normalFont = Font("-adobe-courier-medium-r-normal-*-*-120-*-*-m-*-iso8859-1")
+#boldFont = Font("-adobe-courier-bold-r-normal-*-*-120-*-*-m-*-iso8859-1")
+#errorFont = Font("-adobe-courier-medium-o-normal-*-*-120-*-*-m-*-iso8859-1")
+
+class _QueryPassword(_QueryString):
+ def body(self, master):
+
+ w = Label(master, text=self.prompt, justify=LEFT)
+ w.grid(row=0, padx=5, sticky=W)
+
+ self.entry = Entry(master, name="entry",show="*")
+ self.entry.grid(row=1, padx=5, sticky=W+E)
+
+ if self.initialvalue:
+ self.entry.insert(0, self.initialvalue)
+ self.entry.select_range(0, END)
+
+ return self.entry
+
+def askpassword(title, prompt, **kw):
+ '''get a password from the user
+
+ @param title: the dialog title
+ @param prompt: the label text
+ @param **kw: see L{SimpleDialog} class
+
+ @returns: a string
+ '''
+ d = apply(_QueryPassword, (title, prompt), kw)
+ return d.result
+
+def grid_setexpand(widget):
+ cols,rows=widget.grid_size()
+ for i in range(cols):
+ widget.columnconfigure(i,weight=1)
+ for i in range(rows):
+ widget.rowconfigure(i,weight=1)
+
+class CList(Frame):
+ def __init__(self,parent,labels,disablesorting=0,**kw):
+ Frame.__init__(self,parent)
+ self.labels=labels
+ self.lists=[]
+ self.disablesorting=disablesorting
+ kw["exportselection"]=0
+ for i in range(len(labels)):
+ b=Button(self,text=labels[i],anchor=W,height=1,pady=0)
+ b.config(command=lambda s=self,i=i:s.setSort(i))
+ b.grid(column=i,row=0,sticky=N+E+W)
+ box=apply(Listbox,(self,),kw)
+ box.grid(column=i,row=1,sticky=N+E+S+W)
+ self.lists.append(box)
+ grid_setexpand(self)
+ self.rowconfigure(0,weight=0)
+ self._callall("bind",'<Button-1>',self.Button1)
+ self._callall("bind",'<B1-Motion>',self.Button1)
+ self.bind('<Up>',self.UpKey)
+ self.bind('<Down>',self.DownKey)
+ self.sort=None
+
+ def _callall(self,funcname,*args,**kw):
+ rets=[]
+ for l in self.lists:
+ func=getattr(l,funcname)
+ ret=apply(func,args,kw)
+ if ret!=None: rets.append(ret)
+ if rets: return rets
+
+ def Button1(self,e):
+ index=self.nearest(e.y)
+ self.select_clear(0,END)
+ self.select_set(index)
+ self.activate(index)
+ return "break"
+
+ def UpKey(self,e):
+ index=self.index(ACTIVE)
+ if index:
+ self.select_clear(0,END)
+ self.select_set(index-1)
+ return "break"
+
+ def DownKey(self,e):
+ index=self.index(ACTIVE)
+ if index!=self.size()-1:
+ self.select_clear(0,END)
+ self.select_set(index+1)
+ return "break"
+
+ def setSort(self,index):
+ if self.sort==None:
+ self.sort=[index,1]
+ elif self.sort[0]==index:
+ self.sort[1]=-self.sort[1]
+ else:
+ self.sort=[index,1]
+ self._sort()
+
+ def _sort(self):
+ if self.disablesorting:
+ return
+ if self.sort==None:
+ return
+ ind,direc=self.sort
+ li=list(self.get(0,END))
+ li.sort(lambda x,y,i=ind,d=direc:d*cmp(x[i],y[i]))
+ self.delete(0,END)
+ for l in li:
+ self._insert(END,l)
+ def activate(self,index):
+ self._callall("activate",index)
+
+ # def bbox(self,index):
+ # return self._callall("bbox",index)
+
+ def curselection(self):
+ return self.lists[0].curselection()
+
+ def delete(self,*args):
+ apply(self._callall,("delete",)+args)
+
+ def get(self,*args):
+ bad=apply(self._callall,("get",)+args)
+ if len(args)==1:
+ return bad
+ ret=[]
+ for i in range(len(bad[0])):
+ r=[]
+ for j in range(len(bad)):
+ r.append(bad[j][i])
+ ret.append(r)
+ return ret
+
+ def index(self,index):
+ return self.lists[0].index(index)
+
+ def insert(self,index,items):
+ self._insert(index,items)
+ self._sort()
+
+ def _insert(self,index,items):
+ for i in range(len(items)):
+ self.lists[i].insert(index,items[i])
+
+ def nearest(self,y):
+ return self.lists[0].nearest(y)
+
+ def see(self,index):
+ self._callall("see",index)
+
+ def size(self):
+ return self.lists[0].size()
+
+ def selection_anchor(self,index):
+ self._callall("selection_anchor",index)
+
+ select_anchor=selection_anchor
+
+ def selection_clear(self,*args):
+ apply(self._callall,("selection_clear",)+args)
+
+ select_clear=selection_clear
+
+ def selection_includes(self,index):
+ return self.lists[0].select_includes(index)
+
+ select_includes=selection_includes
+
+ def selection_set(self,*args):
+ apply(self._callall,("selection_set",)+args)
+
+ select_set=selection_set
+
+ def xview(self,*args):
+ if not args: return self.lists[0].xview()
+ apply(self._callall,("xview",)+args)
+
+ def yview(self,*args):
+ if not args: return self.lists[0].yview()
+ apply(self._callall,("yview",)+args)
+
+class ProgressBar:
+ def __init__(self, master=None, orientation="horizontal",
+ min=0, max=100, width=100, height=18,
+ doLabel=1, appearance="sunken",
+ fillColor="blue", background="gray",
+ labelColor="yellow", labelFont="Verdana",
+ labelText="", labelFormat="%d%%",
+ value=0, bd=2):
+ # preserve various values
+ self.master=master
+ self.orientation=orientation
+ self.min=min
+ self.max=max
+ self.width=width
+ self.height=height
+ self.doLabel=doLabel
+ self.fillColor=fillColor
+ self.labelFont= labelFont
+ self.labelColor=labelColor
+ self.background=background
+ self.labelText=labelText
+ self.labelFormat=labelFormat
+ self.value=value
+ self.frame=Frame(master, relief=appearance, bd=bd)
+ self.canvas=Canvas(self.frame, height=height, width=width, bd=0,
+ highlightthickness=0, background=background)
+ self.scale=self.canvas.create_rectangle(0, 0, width, height,
+ fill=fillColor)
+ self.label=self.canvas.create_text(self.canvas.winfo_reqwidth() / 2,
+ height / 2, text=labelText,
+ anchor="c", fill=labelColor,
+ font=self.labelFont)
+ self.update()
+ self.canvas.pack(side='top', fill='x', expand='no')
+
+ def updateProgress(self, newValue, newMax=None):
+ if newMax:
+ self.max = newMax
+ self.value = newValue
+ self.update()
+
+ def update(self):
+ # Trim the values to be between min and max
+ value=self.value
+ if value > self.max:
+ value = self.max
+ if value < self.min:
+ value = self.min
+ # Adjust the rectangle
+ if self.orientation == "horizontal":
+ self.canvas.coords(self.scale, 0, 0,
+ float(value) / self.max * self.width, self.height)
+ else:
+ self.canvas.coords(self.scale, 0,
+ self.height - (float(value) /
+ self.max*self.height),
+ self.width, self.height)
+ # Now update the colors
+ self.canvas.itemconfig(self.scale, fill=self.fillColor)
+ self.canvas.itemconfig(self.label, fill=self.labelColor)
+ # And update the label
+ if self.doLabel:
+ if value:
+ if value >= 0:
+ pvalue = int((float(value) / float(self.max)) *
+ 100.0)
+ else:
+ pvalue = 0
+ self.canvas.itemconfig(self.label, text=self.labelFormat
+ % pvalue)
+ else:
+ self.canvas.itemconfig(self.label, text='')
+ else:
+ self.canvas.itemconfig(self.label, text=self.labelFormat %
+ self.labelText)
+ self.canvas.update_idletasks()
+
+class DirectoryBrowser(_Dialog):
+ command = "tk_chooseDirectory"
+
+def askdirectory(**options):
+ "Ask for a directory to save to."
+
+ return apply(DirectoryBrowser, (), options).show()
+
+class GenericLogin(Toplevel):
+ def __init__(self,callback,buttons):
+ Toplevel.__init__(self)
+ self.callback=callback
+ Label(self,text="Twisted v%s"%copyright.version).grid(column=0,row=0,columnspan=2)
+ self.entries={}
+ row=1
+ for stuff in buttons:
+ label,value=stuff[:2]
+ if len(stuff)==3:
+ dict=stuff[2]
+ else: dict={}
+ Label(self,text=label+": ").grid(column=0,row=row)
+ e=apply(Entry,(self,),dict)
+ e.grid(column=1,row=row)
+ e.insert(0,value)
+ self.entries[label]=e
+ row=row+1
+ Button(self,text="Login",command=self.doLogin).grid(column=0,row=row)
+ Button(self,text="Cancel",command=self.close).grid(column=1,row=row)
+ self.protocol('WM_DELETE_WINDOW',self.close)
+
+ def close(self):
+ self.tk.quit()
+ self.destroy()
+
+ def doLogin(self):
+ values={}
+ for k in self.entries.keys():
+ values[string.lower(k)]=self.entries[k].get()
+ self.callback(values)
+ self.destroy()
+
+class Login(Toplevel):
+ def __init__(self,
+ callback,
+ referenced = None,
+ initialUser = "guest",
+ initialPassword = "guest",
+ initialHostname = "localhost",
+ initialService = "",
+ initialPortno = pb.portno):
+ Toplevel.__init__(self)
+ version_label = Label(self,text="Twisted v%s" % copyright.version)
+ self.pbReferenceable = referenced
+ self.pbCallback = callback
+ # version_label.show()
+ self.username = Entry(self)
+ self.password = Entry(self,show='*')
+ self.hostname = Entry(self)
+ self.service = Entry(self)
+ self.port = Entry(self)
+
+ self.username.insert(0,initialUser)
+ self.password.insert(0,initialPassword)
+ self.service.insert(0,initialService)
+ self.hostname.insert(0,initialHostname)
+ self.port.insert(0,str(initialPortno))
+
+ userlbl=Label(self,text="Username:")
+ passlbl=Label(self,text="Password:")
+ servicelbl=Label(self,text="Service:")
+ hostlbl=Label(self,text="Hostname:")
+ portlbl=Label(self,text="Port #:")
+ self.logvar=StringVar()
+ self.logvar.set("Protocol PB-%s"%pb.Broker.version)
+ self.logstat = Label(self,textvariable=self.logvar)
+ self.okbutton = Button(self,text="Log In", command=self.login)
+
+ version_label.grid(column=0,row=0,columnspan=2)
+ z=0
+ for i in [[userlbl,self.username],
+ [passlbl,self.password],
+ [hostlbl,self.hostname],
+ [servicelbl,self.service],
+ [portlbl,self.port]]:
+ i[0].grid(column=0,row=z+1)
+ i[1].grid(column=1,row=z+1)
+ z = z+1
+
+ self.logstat.grid(column=0,row=6,columnspan=2)
+ self.okbutton.grid(column=0,row=7,columnspan=2)
+
+ self.protocol('WM_DELETE_WINDOW',self.tk.quit)
+
+ def loginReset(self):
+ self.logvar.set("Idle.")
+
+ def loginReport(self, txt):
+ self.logvar.set(txt)
+ self.after(30000, self.loginReset)
+
+ def login(self):
+ host = self.hostname.get()
+ port = self.port.get()
+ service = self.service.get()
+ try:
+ port = int(port)
+ except:
+ pass
+ user = self.username.get()
+ pswd = self.password.get()
+ pb.connect(host, port, user, pswd, service,
+ client=self.pbReferenceable).addCallback(self.pbCallback).addErrback(
+ self.couldNotConnect)
+
+ def couldNotConnect(self,f):
+ self.loginReport("could not connect:"+f.getErrorMessage())
+
+if __name__=="__main__":
+ root=Tk()
+ o=CList(root,["Username","Online","Auto-Logon","Gateway"])
+ o.pack()
+ for i in range(0,16,4):
+ o.insert(END,[i,i+1,i+2,i+3])
+ mainloop()
diff --git a/twisted/spread/util.py b/twisted/spread/util.py
new file mode 100644
index 0000000..3c529b4
--- /dev/null
+++ b/twisted/spread/util.py
@@ -0,0 +1,215 @@
+# -*- test-case-name: twisted.test.test_pb -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Utility classes for spread.
+"""
+
+from twisted.internet import defer
+from twisted.python.failure import Failure
+from twisted.spread import pb
+from twisted.protocols import basic
+from twisted.internet import interfaces
+
+from zope.interface import implements
+
+
+class LocalMethod:
+ def __init__(self, local, name):
+ self.local = local
+ self.name = name
+
+ def __call__(self, *args, **kw):
+ return self.local.callRemote(self.name, *args, **kw)
+
+
+class LocalAsRemote:
+ """
+ A class useful for emulating the effects of remote behavior locally.
+ """
+ reportAllTracebacks = 1
+
+ def callRemote(self, name, *args, **kw):
+ """
+ Call a specially-designated local method.
+
+ self.callRemote('x') will first try to invoke a method named
+ sync_x and return its result (which should probably be a
+ Deferred). Second, it will look for a method called async_x,
+ which will be called and then have its result (or Failure)
+ automatically wrapped in a Deferred.
+ """
+ if hasattr(self, 'sync_'+name):
+ return getattr(self, 'sync_'+name)(*args, **kw)
+ try:
+ method = getattr(self, "async_" + name)
+ return defer.succeed(method(*args, **kw))
+ except:
+ f = Failure()
+ if self.reportAllTracebacks:
+ f.printTraceback()
+ return defer.fail(f)
+
+ def remoteMethod(self, name):
+ return LocalMethod(self, name)
+
+
+class LocalAsyncForwarder:
+ """
+ A class useful for forwarding a locally-defined interface.
+ """
+
+ def __init__(self, forwarded, interfaceClass, failWhenNotImplemented=0):
+ assert interfaceClass.providedBy(forwarded)
+ self.forwarded = forwarded
+ self.interfaceClass = interfaceClass
+ self.failWhenNotImplemented = failWhenNotImplemented
+
+ def _callMethod(self, method, *args, **kw):
+ return getattr(self.forwarded, method)(*args, **kw)
+
+ def callRemote(self, method, *args, **kw):
+ if self.interfaceClass.queryDescriptionFor(method):
+ result = defer.maybeDeferred(self._callMethod, method, *args, **kw)
+ return result
+ elif self.failWhenNotImplemented:
+ return defer.fail(
+ Failure(NotImplementedError,
+ "No Such Method in Interface: %s" % method))
+ else:
+ return defer.succeed(None)
+
+
+class Pager:
+ """
+ I am an object which pages out information.
+ """
+ def __init__(self, collector, callback=None, *args, **kw):
+ """
+ Create a pager with a Reference to a remote collector and
+ an optional callable to invoke upon completion.
+ """
+ if callable(callback):
+ self.callback = callback
+ self.callbackArgs = args
+ self.callbackKeyword = kw
+ else:
+ self.callback = None
+ self._stillPaging = 1
+ self.collector = collector
+ collector.broker.registerPageProducer(self)
+
+ def stillPaging(self):
+ """
+ (internal) Method called by Broker.
+ """
+ if not self._stillPaging:
+ self.collector.callRemote("endedPaging")
+ if self.callback is not None:
+ self.callback(*self.callbackArgs, **self.callbackKeyword)
+ return self._stillPaging
+
+ def sendNextPage(self):
+ """
+ (internal) Method called by Broker.
+ """
+ self.collector.callRemote("gotPage", self.nextPage())
+
+ def nextPage(self):
+ """
+ Override this to return an object to be sent to my collector.
+ """
+ raise NotImplementedError()
+
+ def stopPaging(self):
+ """
+ Call this when you're done paging.
+ """
+ self._stillPaging = 0
+
+
+class StringPager(Pager):
+ """
+ A simple pager that splits a string into chunks.
+ """
+ def __init__(self, collector, st, chunkSize=8192, callback=None, *args, **kw):
+ self.string = st
+ self.pointer = 0
+ self.chunkSize = chunkSize
+ Pager.__init__(self, collector, callback, *args, **kw)
+
+ def nextPage(self):
+ val = self.string[self.pointer:self.pointer+self.chunkSize]
+ self.pointer += self.chunkSize
+ if self.pointer >= len(self.string):
+ self.stopPaging()
+ return val
+
+
+class FilePager(Pager):
+ """
+ Reads a file in chunks and sends the chunks as they come.
+ """
+ implements(interfaces.IConsumer)
+
+ def __init__(self, collector, fd, callback=None, *args, **kw):
+ self.chunks = []
+ Pager.__init__(self, collector, callback, *args, **kw)
+ self.startProducing(fd)
+
+ def startProducing(self, fd):
+ self.deferred = basic.FileSender().beginFileTransfer(fd, self)
+ self.deferred.addBoth(lambda x : self.stopPaging())
+
+ def registerProducer(self, producer, streaming):
+ self.producer = producer
+ if not streaming:
+ self.producer.resumeProducing()
+
+ def unregisterProducer(self):
+ self.producer = None
+
+ def write(self, chunk):
+ self.chunks.append(chunk)
+
+ def sendNextPage(self):
+ """
+ Get the first chunk read and send it to collector.
+ """
+ if not self.chunks:
+ return
+ val = self.chunks.pop(0)
+ self.producer.resumeProducing()
+ self.collector.callRemote("gotPage", val)
+
+
+# Utility paging stuff.
+class CallbackPageCollector(pb.Referenceable):
+ """
+ I receive pages from the peer. You may instantiate a Pager with a
+ remote reference to me. I will call the callback with a list of pages
+ once they are all received.
+ """
+ def __init__(self, callback):
+ self.pages = []
+ self.callback = callback
+
+ def remote_gotPage(self, page):
+ self.pages.append(page)
+
+ def remote_endedPaging(self):
+ self.callback(self.pages)
+
+
+def getAllPages(referenceable, methodName, *args, **kw):
+ """
+ A utility method that will call a remote method which expects a
+ PageCollector as the first argument.
+ """
+ d = defer.Deferred()
+ referenceable.callRemote(methodName, CallbackPageCollector(d.callback), *args, **kw)
+ return d
+
diff --git a/twisted/tap/__init__.py b/twisted/tap/__init__.py
new file mode 100644
index 0000000..3736107
--- /dev/null
+++ b/twisted/tap/__init__.py
@@ -0,0 +1,10 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted TAP: Twisted Application Persistence builders for other Twisted servers.
+
+"""
diff --git a/twisted/tap/ftp.py b/twisted/tap/ftp.py
new file mode 100644
index 0000000..735ab4b
--- /dev/null
+++ b/twisted/tap/ftp.py
@@ -0,0 +1,69 @@
+# -*- test-case-name: twisted.test.test_ftp_options -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+I am the support module for making a ftp server with twistd.
+"""
+
+from twisted.application import internet
+from twisted.cred import portal, checkers, strcred
+from twisted.protocols import ftp
+
+from twisted.python import usage, deprecate, versions
+
+import warnings
+
+
+
+class Options(usage.Options, strcred.AuthOptionMixin):
+ synopsis = """[options].
+ WARNING: This FTP server is probably INSECURE do not use it.
+ """
+ optParameters = [
+ ["port", "p", "2121", "set the port number"],
+ ["root", "r", "/usr/local/ftp", "define the root of the ftp-site."],
+ ["userAnonymous", "", "anonymous", "Name of the anonymous user."]
+ ]
+
+ compData = usage.Completions(
+ optActions={"root": usage.CompleteDirs(descr="root of the ftp site")}
+ )
+
+ longdesc = ''
+
+ def __init__(self, *a, **kw):
+ usage.Options.__init__(self, *a, **kw)
+ self.addChecker(checkers.AllowAnonymousAccess())
+
+
+ def opt_password_file(self, filename):
+ """
+ Specify a file containing username:password login info for
+ authenticated connections. (DEPRECATED; see --help-auth instead)
+ """
+ self['password-file'] = filename
+ msg = deprecate.getDeprecationWarningString(
+ self.opt_password_file, versions.Version('Twisted', 11, 1, 0))
+ warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
+ self.addChecker(checkers.FilePasswordDB(filename, cache=True))
+
+
+
+def makeService(config):
+ f = ftp.FTPFactory()
+
+ r = ftp.FTPRealm(config['root'])
+ p = portal.Portal(r, config.get('credCheckers', []))
+
+ f.tld = config['root']
+ f.userAnonymous = config['userAnonymous']
+ f.portal = p
+ f.protocol = ftp.FTP
+
+ try:
+ portno = int(config['port'])
+ except KeyError:
+ portno = 2121
+ return internet.TCPServer(portno, f)
diff --git a/twisted/tap/manhole.py b/twisted/tap/manhole.py
new file mode 100644
index 0000000..8d727fa
--- /dev/null
+++ b/twisted/tap/manhole.py
@@ -0,0 +1,54 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+I am the support module for making a manhole server with twistd.
+"""
+
+from twisted.manhole import service
+from twisted.spread import pb
+from twisted.python import usage, util
+from twisted.cred import portal, checkers
+from twisted.application import strports
+import os, sys
+
+class Options(usage.Options):
+ synopsis = "[options]"
+ optParameters = [
+ ["user", "u", "admin", "Name of user to allow to log in"],
+ ["port", "p", str(pb.portno), "Port to listen on"],
+ ]
+
+ optFlags = [
+ ["tracebacks", "T", "Allow tracebacks to be sent over the network"],
+ ]
+
+ compData = usage.Completions(
+ optActions={"user": usage.CompleteUsernames()}
+ )
+
+ def opt_password(self, password):
+ """Required. '-' will prompt or read a password from stdin.
+ """
+ # If standard input is a terminal, I prompt for a password and
+ # confirm it. Otherwise, I use the first line from standard
+ # input, stripping off a trailing newline if there is one.
+ if password in ('', '-'):
+ self['password'] = util.getPassword(confirm=1)
+ else:
+ self['password'] = password
+ opt_w = opt_password
+
+ def postOptions(self):
+ if not self.has_key('password'):
+ self.opt_password('-')
+
+def makeService(config):
+ port, user, password = config['port'], config['user'], config['password']
+ p = portal.Portal(
+ service.Realm(service.Service(config["tracebacks"], config.get('namespace'))),
+ [checkers.InMemoryUsernamePasswordDatabaseDontUse(**{user: password})]
+ )
+ return strports.service(port, pb.PBServerFactory(p, config["tracebacks"]))
diff --git a/twisted/tap/portforward.py b/twisted/tap/portforward.py
new file mode 100644
index 0000000..2ad3f36
--- /dev/null
+++ b/twisted/tap/portforward.py
@@ -0,0 +1,27 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support module for making a port forwarder with twistd.
+"""
+from twisted.protocols import portforward
+from twisted.python import usage
+from twisted.application import strports
+
+class Options(usage.Options):
+ synopsis = "[options]"
+ longdesc = 'Port Forwarder.'
+ optParameters = [
+ ["port", "p", "6666","Set the port number."],
+ ["host", "h", "localhost","Set the host."],
+ ["dest_port", "d", 6665,"Set the destination port."],
+ ]
+
+ compData = usage.Completions(
+ optActions={"host": usage.CompleteHostnames()}
+ )
+
+def makeService(config):
+ f = portforward.ProxyFactory(config['host'], int(config['dest_port']))
+ return strports.service(config['port'], f)
diff --git a/twisted/tap/socks.py b/twisted/tap/socks.py
new file mode 100644
index 0000000..e0780ad
--- /dev/null
+++ b/twisted/tap/socks.py
@@ -0,0 +1,38 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+I am a support module for making SOCKSv4 servers with twistd.
+"""
+
+from twisted.protocols import socks
+from twisted.python import usage
+from twisted.application import internet
+import sys
+
+class Options(usage.Options):
+ synopsis = "[-i <interface>] [-p <port>] [-l <file>]"
+ optParameters = [["interface", "i", "127.0.0.1", "local interface to which we listen"],
+ ["port", "p", 1080, "Port on which to listen"],
+ ["log", "l", None, "file to log connection data to"]]
+
+ compData = usage.Completions(
+ optActions={"log": usage.CompleteFiles("*.log"),
+ "interface": usage.CompleteNetInterfaces()}
+ )
+
+ longdesc = "Makes a SOCKSv4 server."
+
+def makeService(config):
+ if config["interface"] != "127.0.0.1":
+ print
+ print "WARNING:"
+ print " You have chosen to listen on a non-local interface."
+ print " This may allow intruders to access your local network"
+ print " if you run this on a firewall."
+ print
+ t = socks.SOCKSv4Factory(config['log'])
+ portno = int(config['port'])
+ return internet.TCPServer(portno, t, interface=config['interface'])
diff --git a/twisted/tap/telnet.py b/twisted/tap/telnet.py
new file mode 100644
index 0000000..bc0c802
--- /dev/null
+++ b/twisted/tap/telnet.py
@@ -0,0 +1,32 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Support module for making a telnet server with twistd.
+"""
+
+from twisted.manhole import telnet
+from twisted.python import usage
+from twisted.application import strports
+
+class Options(usage.Options):
+ synopsis = "[options]"
+ longdesc = "Makes a telnet server to a Python shell."
+ optParameters = [
+ ["username", "u", "admin","set the login username"],
+ ["password", "w", "changeme","set the password"],
+ ["port", "p", "4040", "port to listen on"],
+ ]
+
+ compData = usage.Completions(
+ optActions={"username": usage.CompleteUsernames()}
+ )
+
+def makeService(config):
+ t = telnet.ShellFactory()
+ t.username, t.password = config['username'], config['password']
+ s = strports.service(config['port'], t)
+ t.setService(s)
+ return s
diff --git a/twisted/test/__init__.py b/twisted/test/__init__.py
new file mode 100644
index 0000000..ff5a9d5
--- /dev/null
+++ b/twisted/test/__init__.py
@@ -0,0 +1,10 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted Test: Unit Tests for Twisted.
+
+"""
diff --git a/twisted/test/_preamble.py b/twisted/test/_preamble.py
new file mode 100644
index 0000000..e3e794e
--- /dev/null
+++ b/twisted/test/_preamble.py
@@ -0,0 +1,17 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# This makes sure Twisted-using child processes used in the test suite import
+# the correct version of Twisted (ie, the version of Twisted under test).
+
+# This is a copy of the bin/_preamble.py script because it's not clear how to
+# use the functionality for both things without having a copy.
+
+import sys, os
+
+path = os.path.abspath(sys.argv[0])
+while os.path.dirname(path) != path:
+ if os.path.exists(os.path.join(path, 'twisted', '__init__.py')):
+ sys.path.insert(0, path)
+ break
+ path = os.path.dirname(path)
diff --git a/twisted/test/crash_test_dummy.py b/twisted/test/crash_test_dummy.py
new file mode 100644
index 0000000..5a30bd4
--- /dev/null
+++ b/twisted/test/crash_test_dummy.py
@@ -0,0 +1,34 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.python import components
+from zope.interface import implements, Interface
+
+def foo():
+ return 2
+
+class X:
+ def __init__(self, x):
+ self.x = x
+
+ def do(self):
+ #print 'X',self.x,'doing!'
+ pass
+
+
+class XComponent(components.Componentized):
+ pass
+
+class IX(Interface):
+ pass
+
+class XA(components.Adapter):
+ implements(IX)
+
+ def method(self):
+ # Kick start :(
+ pass
+
+components.registerAdapter(XA, X, IX)
diff --git a/twisted/test/generator_failure_tests.py b/twisted/test/generator_failure_tests.py
new file mode 100644
index 0000000..dcc6d42
--- /dev/null
+++ b/twisted/test/generator_failure_tests.py
@@ -0,0 +1,177 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Python 2.5+ test cases for failures thrown into generators.
+"""
+
+import sys
+import traceback
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python.failure import Failure
+from twisted.internet import defer
+
+# Re-implement getDivisionFailure here instead of using the one in
+# test_failure.py in order to avoid creating a cyclic dependency.
+def getDivisionFailure():
+ try:
+ 1/0
+ except:
+ f = Failure()
+ return f
+
+
+
+class TwoPointFiveFailureTests(TestCase):
+
+ def test_inlineCallbacksTracebacks(self):
+ """
+ inlineCallbacks that re-raise tracebacks into their deferred
+ should not lose their tracebacsk.
+ """
+ f = getDivisionFailure()
+ d = defer.Deferred()
+ try:
+ f.raiseException()
+ except:
+ d.errback()
+
+ failures = []
+ def collect_error(result):
+ failures.append(result)
+
+ def ic(d):
+ yield d
+ ic = defer.inlineCallbacks(ic)
+ ic(d).addErrback(collect_error)
+
+ newFailure, = failures
+ self.assertEqual(
+ traceback.extract_tb(newFailure.getTracebackObject())[-1][-1],
+ "1/0"
+ )
+
+
+ def _throwIntoGenerator(self, f, g):
+ try:
+ f.throwExceptionIntoGenerator(g)
+ except StopIteration:
+ pass
+ else:
+ self.fail("throwExceptionIntoGenerator should have raised "
+ "StopIteration")
+
+ def test_throwExceptionIntoGenerator(self):
+ """
+ It should be possible to throw the exception that a Failure
+ represents into a generator.
+ """
+ stuff = []
+ def generator():
+ try:
+ yield
+ except:
+ stuff.append(sys.exc_info())
+ else:
+ self.fail("Yield should have yielded exception.")
+ g = generator()
+ f = getDivisionFailure()
+ g.next()
+ self._throwIntoGenerator(f, g)
+
+ self.assertEqual(stuff[0][0], ZeroDivisionError)
+ self.assertTrue(isinstance(stuff[0][1], ZeroDivisionError))
+
+ self.assertEqual(traceback.extract_tb(stuff[0][2])[-1][-1], "1/0")
+
+
+ def test_findFailureInGenerator(self):
+ """
+ Within an exception handler, it should be possible to find the
+ original Failure that caused the current exception (if it was
+ caused by throwExceptionIntoGenerator).
+ """
+ f = getDivisionFailure()
+ f.cleanFailure()
+
+ foundFailures = []
+ def generator():
+ try:
+ yield
+ except:
+ foundFailures.append(Failure._findFailure())
+ else:
+ self.fail("No exception sent to generator")
+
+ g = generator()
+ g.next()
+ self._throwIntoGenerator(f, g)
+
+ self.assertEqual(foundFailures, [f])
+
+
+ def test_failureConstructionFindsOriginalFailure(self):
+ """
+ When a Failure is constructed in the context of an exception
+ handler that is handling an exception raised by
+ throwExceptionIntoGenerator, the new Failure should be chained to that
+ original Failure.
+ """
+ f = getDivisionFailure()
+ f.cleanFailure()
+
+ newFailures = []
+
+ def generator():
+ try:
+ yield
+ except:
+ newFailures.append(Failure())
+ else:
+ self.fail("No exception sent to generator")
+ g = generator()
+ g.next()
+ self._throwIntoGenerator(f, g)
+
+ self.assertEqual(len(newFailures), 1)
+ self.assertEqual(newFailures[0].getTraceback(), f.getTraceback())
+
+ def test_ambiguousFailureInGenerator(self):
+ """
+ When a generator reraises a different exception,
+ L{Failure._findFailure} inside the generator should find the reraised
+ exception rather than original one.
+ """
+ def generator():
+ try:
+ try:
+ yield
+ except:
+ [][1]
+ except:
+ self.assertIsInstance(Failure().value, IndexError)
+ g = generator()
+ g.next()
+ f = getDivisionFailure()
+ self._throwIntoGenerator(f, g)
+
+ def test_ambiguousFailureFromGenerator(self):
+ """
+ When a generator reraises a different exception,
+ L{Failure._findFailure} above the generator should find the reraised
+ exception rather than original one.
+ """
+ def generator():
+ try:
+ yield
+ except:
+ [][1]
+ g = generator()
+ g.next()
+ f = getDivisionFailure()
+ try:
+ self._throwIntoGenerator(f, g)
+ except:
+ self.assertIsInstance(Failure().value, IndexError)
diff --git a/twisted/test/iosim.py b/twisted/test/iosim.py
new file mode 100644
index 0000000..afa80f9
--- /dev/null
+++ b/twisted/test/iosim.py
@@ -0,0 +1,270 @@
+# -*- test-case-name: twisted.test.test_amp.TLSTest -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Utilities and helpers for simulating a network
+"""
+
+import itertools
+
+try:
+ from OpenSSL.SSL import Error as NativeOpenSSLError
+except ImportError:
+ pass
+
+from zope.interface import implements, directlyProvides
+
+from twisted.python.failure import Failure
+from twisted.internet import error
+from twisted.internet import interfaces
+
+class TLSNegotiation:
+ def __init__(self, obj, connectState):
+ self.obj = obj
+ self.connectState = connectState
+ self.sent = False
+ self.readyToSend = connectState
+
+ def __repr__(self):
+ return 'TLSNegotiation(%r)' % (self.obj,)
+
+ def pretendToVerify(self, other, tpt):
+ # Set the transport problems list here? disconnections?
+ # hmmmmm... need some negative path tests.
+
+ if not self.obj.iosimVerify(other.obj):
+ tpt.disconnectReason = NativeOpenSSLError()
+ tpt.loseConnection()
+
+
+class FakeTransport:
+ """
+ A wrapper around a file-like object to make it behave as a Transport.
+
+ This doesn't actually stream the file to the attached protocol,
+ and is thus useful mainly as a utility for debugging protocols.
+ """
+ implements(interfaces.ITransport,
+ interfaces.ITLSTransport) # ha ha not really
+
+ _nextserial = itertools.count().next
+ closed = 0
+ disconnecting = 0
+ disconnected = 0
+ disconnectReason = error.ConnectionDone("Connection done")
+ producer = None
+ streamingProducer = 0
+ tls = None
+
+ def __init__(self):
+ self.stream = []
+ self.serial = self._nextserial()
+
+ def __repr__(self):
+ return 'FakeTransport<%s,%s,%s>' % (
+ self.isServer and 'S' or 'C', self.serial,
+ self.protocol.__class__.__name__)
+
+ def write(self, data):
+ if self.tls is not None:
+ self.tlsbuf.append(data)
+ else:
+ self.stream.append(data)
+
+ def _checkProducer(self):
+ # Cheating; this is called at "idle" times to allow producers to be
+ # found and dealt with
+ if self.producer:
+ self.producer.resumeProducing()
+
+ def registerProducer(self, producer, streaming):
+ """From abstract.FileDescriptor
+ """
+ self.producer = producer
+ self.streamingProducer = streaming
+ if not streaming:
+ producer.resumeProducing()
+
+ def unregisterProducer(self):
+ self.producer = None
+
+ def stopConsuming(self):
+ self.unregisterProducer()
+ self.loseConnection()
+
+ def writeSequence(self, iovec):
+ self.write("".join(iovec))
+
+ def loseConnection(self):
+ self.disconnecting = True
+
+ def reportDisconnect(self):
+ if self.tls is not None:
+ # We were in the middle of negotiating! Must have been a TLS problem.
+ err = NativeOpenSSLError()
+ else:
+ err = self.disconnectReason
+ self.protocol.connectionLost(Failure(err))
+
+ def getPeer(self):
+ # XXX: According to ITransport, this should return an IAddress!
+ return 'file', 'file'
+
+ def getHost(self):
+ # XXX: According to ITransport, this should return an IAddress!
+ return 'file'
+
+ def resumeProducing(self):
+ # Never sends data anyways
+ pass
+
+ def pauseProducing(self):
+ # Never sends data anyways
+ pass
+
+ def stopProducing(self):
+ self.loseConnection()
+
+ def startTLS(self, contextFactory, beNormal=True):
+ # Nothing's using this feature yet, but startTLS has an undocumented
+ # second argument which defaults to true; if set to False, servers will
+ # behave like clients and clients will behave like servers.
+ connectState = self.isServer ^ beNormal
+ self.tls = TLSNegotiation(contextFactory, connectState)
+ self.tlsbuf = []
+
+ def getOutBuffer(self):
+ S = self.stream
+ if S:
+ self.stream = []
+ return ''.join(S)
+ elif self.tls is not None:
+ if self.tls.readyToSend:
+ # Only _send_ the TLS negotiation "packet" if I'm ready to.
+ self.tls.sent = True
+ return self.tls
+ else:
+ return None
+ else:
+ return None
+
+ def bufferReceived(self, buf):
+ if isinstance(buf, TLSNegotiation):
+ assert self.tls is not None # By the time you're receiving a
+ # negotiation, you have to have called
+ # startTLS already.
+ if self.tls.sent:
+ self.tls.pretendToVerify(buf, self)
+ self.tls = None # we're done with the handshake if we've gotten
+ # this far... although maybe it failed...?
+ # TLS started! Unbuffer...
+ b, self.tlsbuf = self.tlsbuf, None
+ self.writeSequence(b)
+ directlyProvides(self, interfaces.ISSLTransport)
+ else:
+ # We haven't sent our own TLS negotiation: time to do that!
+ self.tls.readyToSend = True
+ else:
+ self.protocol.dataReceived(buf)
+
+
+
+def makeFakeClient(c):
+ ft = FakeTransport()
+ ft.isServer = False
+ ft.protocol = c
+ return ft
+
+def makeFakeServer(s):
+ ft = FakeTransport()
+ ft.isServer = True
+ ft.protocol = s
+ return ft
+
+class IOPump:
+ """Utility to pump data between clients and servers for protocol testing.
+
+ Perhaps this is a utility worthy of being in protocol.py?
+ """
+ def __init__(self, client, server, clientIO, serverIO, debug):
+ self.client = client
+ self.server = server
+ self.clientIO = clientIO
+ self.serverIO = serverIO
+ self.debug = debug
+
+ def flush(self, debug=False):
+ """Pump until there is no more input or output.
+
+ Returns whether any data was moved.
+ """
+ result = False
+ for x in range(1000):
+ if self.pump(debug):
+ result = True
+ else:
+ break
+ else:
+ assert 0, "Too long"
+ return result
+
+
+ def pump(self, debug=False):
+ """Move data back and forth.
+
+ Returns whether any data was moved.
+ """
+ if self.debug or debug:
+ print '-- GLUG --'
+ sData = self.serverIO.getOutBuffer()
+ cData = self.clientIO.getOutBuffer()
+ self.clientIO._checkProducer()
+ self.serverIO._checkProducer()
+ if self.debug or debug:
+ print '.'
+ # XXX slightly buggy in the face of incremental output
+ if cData:
+ print 'C: '+repr(cData)
+ if sData:
+ print 'S: '+repr(sData)
+ if cData:
+ self.serverIO.bufferReceived(cData)
+ if sData:
+ self.clientIO.bufferReceived(sData)
+ if cData or sData:
+ return True
+ if (self.serverIO.disconnecting and
+ not self.serverIO.disconnected):
+ if self.debug or debug:
+ print '* C'
+ self.serverIO.disconnected = True
+ self.clientIO.disconnecting = True
+ self.clientIO.reportDisconnect()
+ return True
+ if self.clientIO.disconnecting and not self.clientIO.disconnected:
+ if self.debug or debug:
+ print '* S'
+ self.clientIO.disconnected = True
+ self.serverIO.disconnecting = True
+ self.serverIO.reportDisconnect()
+ return True
+ return False
+
+
+def connectedServerAndClient(ServerClass, ClientClass,
+ clientTransportFactory=makeFakeClient,
+ serverTransportFactory=makeFakeServer,
+ debug=False):
+ """Returns a 3-tuple: (client, server, pump)
+ """
+ c = ClientClass()
+ s = ServerClass()
+ cio = clientTransportFactory(c)
+ sio = serverTransportFactory(s)
+ c.makeConnection(cio)
+ s.makeConnection(sio)
+ pump = IOPump(c, s, cio, sio, debug)
+ # kick off server greeting, etc
+ pump.flush()
+ return c, s, pump
diff --git a/twisted/test/mock_win32process.py b/twisted/test/mock_win32process.py
new file mode 100644
index 0000000..b70bdca
--- /dev/null
+++ b/twisted/test/mock_win32process.py
@@ -0,0 +1,48 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This is a mock win32process module.
+
+The purpose of this module is mock process creation for the PID test.
+
+CreateProcess(...) will spawn a process, and always return a PID of 42.
+"""
+
+import win32process
+GetExitCodeProcess = win32process.GetExitCodeProcess
+STARTUPINFO = win32process.STARTUPINFO
+
+STARTF_USESTDHANDLES = win32process.STARTF_USESTDHANDLES
+
+
+def CreateProcess(appName,
+ cmdline,
+ procSecurity,
+ threadSecurity,
+ inheritHandles,
+ newEnvironment,
+ env,
+ workingDir,
+ startupInfo):
+ """
+ This function mocks the generated pid aspect of the win32.CreateProcess
+ function.
+ - the true win32process.CreateProcess is called
+ - return values are harvested in a tuple.
+ - all return values from createProcess are passed back to the calling
+ function except for the pid, the returned pid is hardcoded to 42
+ """
+
+ hProcess, hThread, dwPid, dwTid = win32process.CreateProcess(
+ appName,
+ cmdline,
+ procSecurity,
+ threadSecurity,
+ inheritHandles,
+ newEnvironment,
+ env,
+ workingDir,
+ startupInfo)
+ dwPid = 42
+ return (hProcess, hThread, dwPid, dwTid)
diff --git a/twisted/test/myrebuilder1.py b/twisted/test/myrebuilder1.py
new file mode 100644
index 0000000..f53e8c7
--- /dev/null
+++ b/twisted/test/myrebuilder1.py
@@ -0,0 +1,15 @@
+
+class A:
+ def a(self):
+ return 'a'
+try:
+ object
+except NameError:
+ pass
+else:
+ class B(object, A):
+ def b(self):
+ return 'b'
+class Inherit(A):
+ def a(self):
+ return 'c'
diff --git a/twisted/test/myrebuilder2.py b/twisted/test/myrebuilder2.py
new file mode 100644
index 0000000..d2a0d10
--- /dev/null
+++ b/twisted/test/myrebuilder2.py
@@ -0,0 +1,16 @@
+
+class A:
+ def a(self):
+ return 'b'
+try:
+ object
+except NameError:
+ pass
+else:
+ class B(A, object):
+ def b(self):
+ return 'c'
+
+class Inherit(A):
+ def a(self):
+ return 'd'
diff --git a/twisted/test/plugin_basic.py b/twisted/test/plugin_basic.py
new file mode 100644
index 0000000..a4c297b
--- /dev/null
+++ b/twisted/test/plugin_basic.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# Don't change the docstring, it's part of the tests
+"""
+I'm a test drop-in. The plugin system's unit tests use me. No one
+else should.
+"""
+
+from zope.interface import classProvides
+
+from twisted.plugin import IPlugin
+from twisted.test.test_plugin import ITestPlugin, ITestPlugin2
+
+
+
+class TestPlugin:
+ """
+ A plugin used solely for testing purposes.
+ """
+
+ classProvides(ITestPlugin,
+ IPlugin)
+
+ def test1():
+ pass
+ test1 = staticmethod(test1)
+
+
+
+class AnotherTestPlugin:
+ """
+ Another plugin used solely for testing purposes.
+ """
+
+ classProvides(ITestPlugin2,
+ IPlugin)
+
+ def test():
+ pass
+ test = staticmethod(test)
+
+
+
+class ThirdTestPlugin:
+ """
+ Another plugin used solely for testing purposes.
+ """
+
+ classProvides(ITestPlugin2,
+ IPlugin)
+
+ def test():
+ pass
+ test = staticmethod(test)
+
diff --git a/twisted/test/plugin_extra1.py b/twisted/test/plugin_extra1.py
new file mode 100644
index 0000000..9e4c8d4
--- /dev/null
+++ b/twisted/test/plugin_extra1.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test plugin used in L{twisted.test.test_plugin}.
+"""
+
+from zope.interface import classProvides
+
+from twisted.plugin import IPlugin
+from twisted.test.test_plugin import ITestPlugin
+
+
+
+class FourthTestPlugin:
+ classProvides(ITestPlugin,
+ IPlugin)
+
+ def test1():
+ pass
+ test1 = staticmethod(test1)
+
diff --git a/twisted/test/plugin_extra2.py b/twisted/test/plugin_extra2.py
new file mode 100644
index 0000000..a6b3f09
--- /dev/null
+++ b/twisted/test/plugin_extra2.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test plugin used in L{twisted.test.test_plugin}.
+"""
+
+from zope.interface import classProvides
+
+from twisted.plugin import IPlugin
+from twisted.test.test_plugin import ITestPlugin
+
+
+
+class FourthTestPlugin:
+ classProvides(ITestPlugin,
+ IPlugin)
+
+ def test1():
+ pass
+ test1 = staticmethod(test1)
+
+
+
+class FifthTestPlugin:
+ """
+ More documentation: I hate you.
+ """
+ classProvides(ITestPlugin,
+ IPlugin)
+
+ def test1():
+ pass
+ test1 = staticmethod(test1)
diff --git a/twisted/test/process_cmdline.py b/twisted/test/process_cmdline.py
new file mode 100644
index 0000000..bd250de
--- /dev/null
+++ b/twisted/test/process_cmdline.py
@@ -0,0 +1,5 @@
+"""Write to stdout the command line args it received, one per line."""
+
+import sys
+for x in sys.argv[1:]:
+ print x
diff --git a/twisted/test/process_echoer.py b/twisted/test/process_echoer.py
new file mode 100644
index 0000000..8a7bf6d
--- /dev/null
+++ b/twisted/test/process_echoer.py
@@ -0,0 +1,11 @@
+"""Write back all data it receives."""
+
+import sys
+
+data = sys.stdin.read(1)
+while data:
+ sys.stdout.write(data)
+ sys.stdout.flush()
+ data = sys.stdin.read(1)
+sys.stderr.write("byebye")
+sys.stderr.flush()
diff --git a/twisted/test/process_fds.py b/twisted/test/process_fds.py
new file mode 100644
index 0000000..e2273c1
--- /dev/null
+++ b/twisted/test/process_fds.py
@@ -0,0 +1,40 @@
+
+"""Write to a handful of file descriptors, to test the childFDs= argument of
+reactor.spawnProcess()
+"""
+
+import os, sys
+
+debug = 0
+
+if debug: stderr = os.fdopen(2, "w")
+
+if debug: print >>stderr, "this is stderr"
+
+abcd = os.read(0, 4)
+if debug: print >>stderr, "read(0):", abcd
+if abcd != "abcd":
+ sys.exit(1)
+
+if debug: print >>stderr, "os.write(1, righto)"
+
+os.write(1, "righto")
+
+efgh = os.read(3, 4)
+if debug: print >>stderr, "read(3):", efgh
+if efgh != "efgh":
+ sys.exit(2)
+
+if debug: print >>stderr, "os.close(4)"
+os.close(4)
+
+eof = os.read(5, 4)
+if debug: print >>stderr, "read(5):", eof
+if eof != "":
+ sys.exit(3)
+
+if debug: print >>stderr, "os.write(1, closed)"
+os.write(1, "closed")
+
+if debug: print >>stderr, "sys.exit(0)"
+sys.exit(0)
diff --git a/twisted/test/process_linger.py b/twisted/test/process_linger.py
new file mode 100644
index 0000000..a95a8d2
--- /dev/null
+++ b/twisted/test/process_linger.py
@@ -0,0 +1,17 @@
+
+"""Write to a file descriptor and then close it, waiting a few seconds before
+quitting. This serves to make sure SIGCHLD is actually being noticed.
+"""
+
+import os, sys, time
+
+print "here is some text"
+time.sleep(1)
+print "goodbye"
+os.close(1)
+os.close(2)
+
+time.sleep(2)
+
+sys.exit(0)
+
diff --git a/twisted/test/process_reader.py b/twisted/test/process_reader.py
new file mode 100644
index 0000000..be37a7c
--- /dev/null
+++ b/twisted/test/process_reader.py
@@ -0,0 +1,12 @@
+"""Script used by test_process.TestTwoProcesses"""
+
+# run until stdin is closed, then quit
+
+import sys
+
+while 1:
+ d = sys.stdin.read()
+ if len(d) == 0:
+ sys.exit(0)
+
+
diff --git a/twisted/test/process_signal.py b/twisted/test/process_signal.py
new file mode 100644
index 0000000..f2ff108
--- /dev/null
+++ b/twisted/test/process_signal.py
@@ -0,0 +1,8 @@
+import sys, signal
+
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+if getattr(signal, "SIGHUP", None) is not None:
+ signal.signal(signal.SIGHUP, signal.SIG_DFL)
+print 'ok, signal us'
+sys.stdin.read()
+sys.exit(1)
diff --git a/twisted/test/process_stdinreader.py b/twisted/test/process_stdinreader.py
new file mode 100644
index 0000000..f060db4
--- /dev/null
+++ b/twisted/test/process_stdinreader.py
@@ -0,0 +1,23 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Script used by twisted.test.test_process on win32."""
+
+import sys, time, os, msvcrt
+msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
+
+
+sys.stdout.write("out\n")
+sys.stdout.flush()
+sys.stderr.write("err\n")
+sys.stderr.flush()
+
+data = sys.stdin.read()
+
+sys.stdout.write(data)
+sys.stdout.write("\nout\n")
+sys.stderr.write("err\n")
+
+sys.stdout.flush()
+sys.stderr.flush()
diff --git a/twisted/test/process_tester.py b/twisted/test/process_tester.py
new file mode 100644
index 0000000..d9779b1
--- /dev/null
+++ b/twisted/test/process_tester.py
@@ -0,0 +1,37 @@
+"""Test program for processes."""
+
+import sys, os
+
+test_file_match = "process_test.log.*"
+test_file = "process_test.log.%d" % os.getpid()
+
+def main():
+ f = open(test_file, 'wb')
+
+ # stage 1
+ bytes = sys.stdin.read(4)
+ f.write("one: %r\n" % bytes)
+ # stage 2
+ sys.stdout.write(bytes)
+ sys.stdout.flush()
+ os.close(sys.stdout.fileno())
+
+ # and a one, and a two, and a...
+ bytes = sys.stdin.read(4)
+ f.write("two: %r\n" % bytes)
+
+ # stage 3
+ sys.stderr.write(bytes)
+ sys.stderr.flush()
+ os.close(sys.stderr.fileno())
+
+ # stage 4
+ bytes = sys.stdin.read(4)
+ f.write("three: %r\n" % bytes)
+
+ # exit with status code 23
+ sys.exit(23)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/twisted/test/process_tty.py b/twisted/test/process_tty.py
new file mode 100644
index 0000000..9dab638
--- /dev/null
+++ b/twisted/test/process_tty.py
@@ -0,0 +1,6 @@
+"""Test to make sure we can open /dev/tty"""
+
+f = open("/dev/tty", "r+")
+a = f.readline()
+f.write(a)
+f.close()
diff --git a/twisted/test/process_twisted.py b/twisted/test/process_twisted.py
new file mode 100644
index 0000000..2071090
--- /dev/null
+++ b/twisted/test/process_twisted.py
@@ -0,0 +1,43 @@
+"""A process that reads from stdin and out using Twisted."""
+
+### Twisted Preamble
+# This makes sure that users don't have to set up their environment
+# specially in order to run these programs from bin/.
+import sys, os
+pos = os.path.abspath(sys.argv[0]).find(os.sep+'Twisted')
+if pos != -1:
+ sys.path.insert(0, os.path.abspath(sys.argv[0])[:pos+8])
+sys.path.insert(0, os.curdir)
+### end of preamble
+
+
+from twisted.python import log
+from zope.interface import implements
+from twisted.internet import interfaces
+
+log.startLogging(sys.stderr)
+
+from twisted.internet import protocol, reactor, stdio
+
+
+class Echo(protocol.Protocol):
+ implements(interfaces.IHalfCloseableProtocol)
+
+ def connectionMade(self):
+ print "connection made"
+
+ def dataReceived(self, data):
+ self.transport.write(data)
+
+ def readConnectionLost(self):
+ print "readConnectionLost"
+ self.transport.loseConnection()
+ def writeConnectionLost(self):
+ print "writeConnectionLost"
+
+ def connectionLost(self, reason):
+ print "connectionLost", reason
+ reactor.stop()
+
+stdio.StandardIO(Echo())
+reactor.run()
diff --git a/twisted/test/proto_helpers.py b/twisted/test/proto_helpers.py
new file mode 100644
index 0000000..42585b0
--- /dev/null
+++ b/twisted/test/proto_helpers.py
@@ -0,0 +1,558 @@
+# -*- test-case-name: twisted.test.test_stringtransport -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Assorted functionality which is commonly useful when writing unit tests.
+"""
+
+from socket import AF_INET, AF_INET6
+from StringIO import StringIO
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import (
+ ITransport, IConsumer, IPushProducer, IConnector)
+from twisted.internet.interfaces import (
+ IReactorTCP, IReactorSSL, IReactorUNIX, IReactorSocket)
+from twisted.internet.interfaces import IListeningPort
+from twisted.protocols import basic
+from twisted.internet import protocol, error, address
+
+from twisted.internet.address import IPv4Address, UNIXAddress
+
+
+class AccumulatingProtocol(protocol.Protocol):
+ """
+ L{AccumulatingProtocol} is an L{IProtocol} implementation which collects
+ the data delivered to it and can fire a Deferred when it is connected or
+ disconnected.
+
+ @ivar made: A flag indicating whether C{connectionMade} has been called.
+ @ivar data: A string giving all the data passed to C{dataReceived}.
+ @ivar closed: A flag indicated whether C{connectionLost} has been called.
+ @ivar closedReason: The value of the I{reason} parameter passed to
+ C{connectionLost}.
+ @ivar closedDeferred: If set to a L{Deferred}, this will be fired when
+ C{connectionLost} is called.
+ """
+ made = closed = 0
+ closedReason = None
+
+ closedDeferred = None
+
+ data = ""
+
+ factory = None
+
+ def connectionMade(self):
+ self.made = 1
+ if (self.factory is not None and
+ self.factory.protocolConnectionMade is not None):
+ d = self.factory.protocolConnectionMade
+ self.factory.protocolConnectionMade = None
+ d.callback(self)
+
+ def dataReceived(self, data):
+ self.data += data
+
+ def connectionLost(self, reason):
+ self.closed = 1
+ self.closedReason = reason
+ if self.closedDeferred is not None:
+ d, self.closedDeferred = self.closedDeferred, None
+ d.callback(None)
+
+
+class LineSendingProtocol(basic.LineReceiver):
+ lostConn = False
+
+ def __init__(self, lines, start = True):
+ self.lines = lines[:]
+ self.response = []
+ self.start = start
+
+ def connectionMade(self):
+ if self.start:
+ map(self.sendLine, self.lines)
+
+ def lineReceived(self, line):
+ if not self.start:
+ map(self.sendLine, self.lines)
+ self.lines = []
+ self.response.append(line)
+
+ def connectionLost(self, reason):
+ self.lostConn = True
+
+
+class FakeDatagramTransport:
+ noAddr = object()
+
+ def __init__(self):
+ self.written = []
+
+ def write(self, packet, addr=noAddr):
+ self.written.append((packet, addr))
+
+
+class StringTransport:
+ """
+ A transport implementation which buffers data in memory and keeps track of
+ its other state without providing any behavior.
+
+ L{StringTransport} has a number of attributes which are not part of any of
+ the interfaces it claims to implement. These attributes are provided for
+ testing purposes. Implementation code should not use any of these
+ attributes; they are not provided by other transports.
+
+ @ivar disconnecting: A C{bool} which is C{False} until L{loseConnection} is
+ called, then C{True}.
+
+ @ivar producer: If a producer is currently registered, C{producer} is a
+ reference to it. Otherwise, C{None}.
+
+ @ivar streaming: If a producer is currently registered, C{streaming} refers
+ to the value of the second parameter passed to C{registerProducer}.
+
+ @ivar hostAddr: C{None} or an object which will be returned as the host
+ address of this transport. If C{None}, a nasty tuple will be returned
+ instead.
+
+ @ivar peerAddr: C{None} or an object which will be returned as the peer
+ address of this transport. If C{None}, a nasty tuple will be returned
+ instead.
+
+ @ivar producerState: The state of this L{StringTransport} in its capacity
+ as an L{IPushProducer}. One of C{'producing'}, C{'paused'}, or
+ C{'stopped'}.
+
+ @ivar io: A L{StringIO} which holds the data which has been written to this
+ transport since the last call to L{clear}. Use L{value} instead of
+ accessing this directly.
+ """
+ implements(ITransport, IConsumer, IPushProducer)
+
+ disconnecting = False
+
+ producer = None
+ streaming = None
+
+ hostAddr = None
+ peerAddr = None
+
+ producerState = 'producing'
+
+ def __init__(self, hostAddress=None, peerAddress=None):
+ self.clear()
+ if hostAddress is not None:
+ self.hostAddr = hostAddress
+ if peerAddress is not None:
+ self.peerAddr = peerAddress
+ self.connected = True
+
+ def clear(self):
+ """
+ Discard all data written to this transport so far.
+
+ This is not a transport method. It is intended for tests. Do not use
+ it in implementation code.
+ """
+ self.io = StringIO()
+
+
+ def value(self):
+ """
+ Retrieve all data which has been buffered by this transport.
+
+ This is not a transport method. It is intended for tests. Do not use
+ it in implementation code.
+
+ @return: A C{str} giving all data written to this transport since the
+ last call to L{clear}.
+ @rtype: C{str}
+ """
+ return self.io.getvalue()
+
+
+ # ITransport
+ def write(self, data):
+ if isinstance(data, unicode): # no, really, I mean it
+ raise TypeError("Data must not be unicode")
+ self.io.write(data)
+
+
+ def writeSequence(self, data):
+ self.io.write(''.join(data))
+
+
+ def loseConnection(self):
+ """
+ Close the connection. Does nothing besides toggle the C{disconnecting}
+ instance variable to C{True}.
+ """
+ self.disconnecting = True
+
+
+ def getPeer(self):
+ if self.peerAddr is None:
+ return address.IPv4Address('TCP', '192.168.1.1', 54321)
+ return self.peerAddr
+
+
+ def getHost(self):
+ if self.hostAddr is None:
+ return address.IPv4Address('TCP', '10.0.0.1', 12345)
+ return self.hostAddr
+
+
+ # IConsumer
+ def registerProducer(self, producer, streaming):
+ if self.producer is not None:
+ raise RuntimeError("Cannot register two producers")
+ self.producer = producer
+ self.streaming = streaming
+
+
+ def unregisterProducer(self):
+ if self.producer is None:
+ raise RuntimeError(
+ "Cannot unregister a producer unless one is registered")
+ self.producer = None
+ self.streaming = None
+
+
+ # IPushProducer
+ def _checkState(self):
+ if self.disconnecting:
+ raise RuntimeError(
+ "Cannot resume producing after loseConnection")
+ if self.producerState == 'stopped':
+ raise RuntimeError("Cannot resume a stopped producer")
+
+
+ def pauseProducing(self):
+ self._checkState()
+ self.producerState = 'paused'
+
+
+ def stopProducing(self):
+ self.producerState = 'stopped'
+
+
+ def resumeProducing(self):
+ self._checkState()
+ self.producerState = 'producing'
+
+
+
+class StringTransportWithDisconnection(StringTransport):
+ def loseConnection(self):
+ if self.connected:
+ self.connected = False
+ self.protocol.connectionLost(error.ConnectionDone("Bye."))
+
+
+
+class StringIOWithoutClosing(StringIO):
+ """
+ A StringIO that can't be closed.
+ """
+ def close(self):
+ """
+ Do nothing.
+ """
+
+
+
+class _FakePort(object):
+ """
+ A fake L{IListeningPort} to be used in tests.
+
+ @ivar _hostAddress: The L{IAddress} this L{IListeningPort} is pretending
+ to be listening on.
+ """
+ implements(IListeningPort)
+
+ def __init__(self, hostAddress):
+ """
+ @param hostAddress: An L{IAddress} this L{IListeningPort} should
+ pretend to be listening on.
+ """
+ self._hostAddress = hostAddress
+
+
+ def startListening(self):
+ """
+ Fake L{IListeningPort.startListening} that doesn't do anything.
+ """
+
+
+ def stopListening(self):
+ """
+ Fake L{IListeningPort.stopListening} that doesn't do anything.
+ """
+
+
+ def getHost(self):
+ """
+ Fake L{IListeningPort.getHost} that returns our L{IAddress}.
+ """
+ return self._hostAddress
+
+
+
+class _FakeConnector(object):
+ """
+ A fake L{IConnector} that allows us to inspect if it has been told to stop
+ connecting.
+
+ @ivar stoppedConnecting: has this connector's
+ L{FakeConnector.stopConnecting} method been invoked yet?
+
+ @ivar _address: An L{IAddress} provider that represents our destination.
+ """
+ implements(IConnector)
+
+ stoppedConnecting = False
+
+ def __init__(self, address):
+ """
+ @param address: An L{IAddress} provider that represents this
+ connector's destination.
+ """
+ self._address = address
+
+
+ def stopConnecting(self):
+ """
+ Implement L{IConnector.stopConnecting} and set
+ L{FakeConnector.stoppedConnecting} to C{True}
+ """
+ self.stoppedConnecting = True
+
+
+ def disconnect(self):
+ """
+ Implement L{IConnector.disconnect} as a no-op.
+ """
+
+
+ def connect(self):
+ """
+ Implement L{IConnector.connect} as a no-op.
+ """
+
+
+ def getDestination(self):
+ """
+ Implement L{IConnector.getDestination} to return the C{address} passed
+ to C{__init__}.
+ """
+ return self._address
+
+
+
+class MemoryReactor(object):
+ """
+ A fake reactor to be used in tests. This reactor doesn't actually do
+ much that's useful yet. It accepts TCP connection setup attempts, but
+ they will never succeed.
+
+ @ivar tcpClients: a list that keeps track of connection attempts (ie, calls
+ to C{connectTCP}).
+ @type tcpClients: C{list}
+
+ @ivar tcpServers: a list that keeps track of server listen attempts (ie, calls
+ to C{listenTCP}).
+ @type tcpServers: C{list}
+
+ @ivar sslClients: a list that keeps track of connection attempts (ie,
+ calls to C{connectSSL}).
+ @type sslClients: C{list}
+
+ @ivar sslServers: a list that keeps track of server listen attempts (ie,
+ calls to C{listenSSL}).
+ @type sslServers: C{list}
+
+ @ivar unixClients: a list that keeps track of connection attempts (ie,
+ calls to C{connectUNIX}).
+ @type unixClients: C{list}
+
+ @ivar unixServers: a list that keeps track of server listen attempts (ie,
+ calls to C{listenUNIX}).
+ @type unixServers: C{list}
+
+ @ivar adoptedPorts: a list that keeps track of server listen attempts (ie,
+ calls to C{adoptStreamPort}).
+ """
+ implements(IReactorTCP, IReactorSSL, IReactorUNIX, IReactorSocket)
+
+ def __init__(self):
+ """
+ Initialize the tracking lists.
+ """
+ self.tcpClients = []
+ self.tcpServers = []
+ self.sslClients = []
+ self.sslServers = []
+ self.unixClients = []
+ self.unixServers = []
+ self.adoptedPorts = []
+
+
+ def adoptStreamPort(self, fileno, addressFamily, factory):
+ """
+ Fake L{IReactorSocket.adoptStreamPort}, that logs the call and returns
+ an L{IListeningPort}.
+ """
+ if addressFamily == AF_INET:
+ addr = IPv4Address('TCP', '0.0.0.0', 1234)
+ elif addressFamily == AF_INET6:
+ addr = IPv6Address('TCP', '::', 1234)
+ else:
+ raise UnsupportedAddressFamily()
+
+ self.adoptedPorts.append((fileno, addressFamily, factory))
+ return _FakePort(addr)
+
+
+ def listenTCP(self, port, factory, backlog=50, interface=''):
+ """
+ Fake L{reactor.listenTCP}, that logs the call and returns an
+ L{IListeningPort}.
+ """
+ self.tcpServers.append((port, factory, backlog, interface))
+ return _FakePort(IPv4Address('TCP', '0.0.0.0', port))
+
+
+ def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
+ """
+ Fake L{reactor.connectTCP}, that logs the call and returns an
+ L{IConnector}.
+ """
+ self.tcpClients.append((host, port, factory, timeout, bindAddress))
+ conn = _FakeConnector(IPv4Address('TCP', host, port))
+ factory.startedConnecting(conn)
+ return conn
+
+
+ def listenSSL(self, port, factory, contextFactory,
+ backlog=50, interface=''):
+ """
+ Fake L{reactor.listenSSL}, that logs the call and returns an
+ L{IListeningPort}.
+ """
+ self.sslServers.append((port, factory, contextFactory,
+ backlog, interface))
+ return _FakePort(IPv4Address('TCP', '0.0.0.0', port))
+
+
+ def connectSSL(self, host, port, factory, contextFactory,
+ timeout=30, bindAddress=None):
+ """
+ Fake L{reactor.connectSSL}, that logs the call and returns an
+ L{IConnector}.
+ """
+ self.sslClients.append((host, port, factory, contextFactory,
+ timeout, bindAddress))
+ conn = _FakeConnector(IPv4Address('TCP', host, port))
+ factory.startedConnecting(conn)
+ return conn
+
+
+ def listenUNIX(self, address, factory,
+ backlog=50, mode=0666, wantPID=0):
+ """
+ Fake L{reactor.listenUNIX}, that logs the call and returns an
+ L{IListeningPort}.
+ """
+ self.unixServers.append((address, factory, backlog, mode, wantPID))
+ return _FakePort(UNIXAddress(address))
+
+
+ def connectUNIX(self, address, factory, timeout=30, checkPID=0):
+ """
+ Fake L{reactor.connectUNIX}, that logs the call and returns an
+ L{IConnector}.
+ """
+ self.unixClients.append((address, factory, timeout, checkPID))
+ conn = _FakeConnector(UNIXAddress(address))
+ factory.startedConnecting(conn)
+ return conn
+
+
+
+class RaisingMemoryReactor(object):
+ """
+ A fake reactor to be used in tests. It accepts TCP connection setup
+ attempts, but they will fail.
+
+ @ivar _listenException: An instance of an L{Exception}
+ @ivar _connectException: An instance of an L{Exception}
+ """
+ implements(IReactorTCP, IReactorSSL, IReactorUNIX, IReactorSocket)
+
+ def __init__(self, listenException=None, connectException=None):
+ """
+ @param listenException: An instance of an L{Exception} to raise when any
+ C{listen} method is called.
+
+ @param connectException: An instance of an L{Exception} to raise when
+ any C{connect} method is called.
+ """
+ self._listenException = listenException
+ self._connectException = connectException
+
+
+ def adoptStreamPort(self, fileno, addressFamily, factory):
+ """
+ Fake L{IReactorSocket.adoptStreamPort}, that raises
+ L{self._listenException}.
+ """
+ raise self._listenException
+
+
+ def listenTCP(self, port, factory, backlog=50, interface=''):
+ """
+ Fake L{reactor.listenTCP}, that raises L{self._listenException}.
+ """
+ raise self._listenException
+
+
+ def connectTCP(self, host, port, factory, timeout=30, bindAddress=None):
+ """
+ Fake L{reactor.connectTCP}, that raises L{self._connectException}.
+ """
+ raise self._connectException
+
+
+ def listenSSL(self, port, factory, contextFactory,
+ backlog=50, interface=''):
+ """
+ Fake L{reactor.listenSSL}, that raises L{self._listenException}.
+ """
+ raise self._listenException
+
+
+ def connectSSL(self, host, port, factory, contextFactory,
+ timeout=30, bindAddress=None):
+ """
+ Fake L{reactor.connectSSL}, that raises L{self._connectException}.
+ """
+ raise self._connectException
+
+
+ def listenUNIX(self, address, factory,
+ backlog=50, mode=0666, wantPID=0):
+ """
+ Fake L{reactor.listenUNIX}, that raises L{self._listenException}.
+ """
+ raise self._listenException
+
+
+ def connectUNIX(self, address, factory, timeout=30, checkPID=0):
+ """
+ Fake L{reactor.connectUNIX}, that raises L{self._connectException}.
+ """
+ raise self._connectException
diff --git a/twisted/test/raiser.c b/twisted/test/raiser.c
new file mode 100644
index 0000000..b9ba176
--- /dev/null
+++ b/twisted/test/raiser.c
@@ -0,0 +1,1443 @@
+/* Generated by Cython 0.14.1 on Tue Mar 8 19:41:56 2011 */
+
+#define PY_SSIZE_T_CLEAN
+#include "Python.h"
+#ifndef Py_PYTHON_H
+ #error Python headers needed to compile C extensions, please install development version of Python.
+#else
+
+#include <stddef.h> /* For offsetof */
+#ifndef offsetof
+#define offsetof(type, member) ( (size_t) & ((type*)0) -> member )
+#endif
+
+#if !defined(WIN32) && !defined(MS_WINDOWS)
+ #ifndef __stdcall
+ #define __stdcall
+ #endif
+ #ifndef __cdecl
+ #define __cdecl
+ #endif
+ #ifndef __fastcall
+ #define __fastcall
+ #endif
+#endif
+
+#ifndef DL_IMPORT
+ #define DL_IMPORT(t) t
+#endif
+#ifndef DL_EXPORT
+ #define DL_EXPORT(t) t
+#endif
+
+#ifndef PY_LONG_LONG
+ #define PY_LONG_LONG LONG_LONG
+#endif
+
+#if PY_VERSION_HEX < 0x02040000
+ #define METH_COEXIST 0
+ #define PyDict_CheckExact(op) (Py_TYPE(op) == &PyDict_Type)
+ #define PyDict_Contains(d,o) PySequence_Contains(d,o)
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ typedef int Py_ssize_t;
+ #define PY_SSIZE_T_MAX INT_MAX
+ #define PY_SSIZE_T_MIN INT_MIN
+ #define PY_FORMAT_SIZE_T ""
+ #define PyInt_FromSsize_t(z) PyInt_FromLong(z)
+ #define PyInt_AsSsize_t(o) PyInt_AsLong(o)
+ #define PyNumber_Index(o) PyNumber_Int(o)
+ #define PyIndex_Check(o) PyNumber_Check(o)
+ #define PyErr_WarnEx(category, message, stacklevel) PyErr_Warn(category, message)
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt)
+ #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
+ #define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)
+ #define PyVarObject_HEAD_INIT(type, size) \
+ PyObject_HEAD_INIT(type) size,
+ #define PyType_Modified(t)
+
+ typedef struct {
+ void *buf;
+ PyObject *obj;
+ Py_ssize_t len;
+ Py_ssize_t itemsize;
+ int readonly;
+ int ndim;
+ char *format;
+ Py_ssize_t *shape;
+ Py_ssize_t *strides;
+ Py_ssize_t *suboffsets;
+ void *internal;
+ } Py_buffer;
+
+ #define PyBUF_SIMPLE 0
+ #define PyBUF_WRITABLE 0x0001
+ #define PyBUF_FORMAT 0x0004
+ #define PyBUF_ND 0x0008
+ #define PyBUF_STRIDES (0x0010 | PyBUF_ND)
+ #define PyBUF_C_CONTIGUOUS (0x0020 | PyBUF_STRIDES)
+ #define PyBUF_F_CONTIGUOUS (0x0040 | PyBUF_STRIDES)
+ #define PyBUF_ANY_CONTIGUOUS (0x0080 | PyBUF_STRIDES)
+ #define PyBUF_INDIRECT (0x0100 | PyBUF_STRIDES)
+
+#endif
+
+#if PY_MAJOR_VERSION < 3
+ #define __Pyx_BUILTIN_MODULE_NAME "__builtin__"
+#else
+ #define __Pyx_BUILTIN_MODULE_NAME "builtins"
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define Py_TPFLAGS_CHECKTYPES 0
+ #define Py_TPFLAGS_HAVE_INDEX 0
+#endif
+
+#if (PY_VERSION_HEX < 0x02060000) || (PY_MAJOR_VERSION >= 3)
+ #define Py_TPFLAGS_HAVE_NEWBUFFER 0
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBaseString_Type PyUnicode_Type
+ #define PyStringObject PyUnicodeObject
+ #define PyString_Type PyUnicode_Type
+ #define PyString_Check PyUnicode_Check
+ #define PyString_CheckExact PyUnicode_CheckExact
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PyBytesObject PyStringObject
+ #define PyBytes_Type PyString_Type
+ #define PyBytes_Check PyString_Check
+ #define PyBytes_CheckExact PyString_CheckExact
+ #define PyBytes_FromString PyString_FromString
+ #define PyBytes_FromStringAndSize PyString_FromStringAndSize
+ #define PyBytes_FromFormat PyString_FromFormat
+ #define PyBytes_DecodeEscape PyString_DecodeEscape
+ #define PyBytes_AsString PyString_AsString
+ #define PyBytes_AsStringAndSize PyString_AsStringAndSize
+ #define PyBytes_Size PyString_Size
+ #define PyBytes_AS_STRING PyString_AS_STRING
+ #define PyBytes_GET_SIZE PyString_GET_SIZE
+ #define PyBytes_Repr PyString_Repr
+ #define PyBytes_Concat PyString_Concat
+ #define PyBytes_ConcatAndDel PyString_ConcatAndDel
+#endif
+
+#if PY_VERSION_HEX < 0x02060000
+ #define PySet_Check(obj) PyObject_TypeCheck(obj, &PySet_Type)
+ #define PyFrozenSet_Check(obj) PyObject_TypeCheck(obj, &PyFrozenSet_Type)
+#endif
+#ifndef PySet_CheckExact
+ #define PySet_CheckExact(obj) (Py_TYPE(obj) == &PySet_Type)
+#endif
+
+#define __Pyx_TypeCheck(obj, type) PyObject_TypeCheck(obj, (PyTypeObject *)type)
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyIntObject PyLongObject
+ #define PyInt_Type PyLong_Type
+ #define PyInt_Check(op) PyLong_Check(op)
+ #define PyInt_CheckExact(op) PyLong_CheckExact(op)
+ #define PyInt_FromString PyLong_FromString
+ #define PyInt_FromUnicode PyLong_FromUnicode
+ #define PyInt_FromLong PyLong_FromLong
+ #define PyInt_FromSize_t PyLong_FromSize_t
+ #define PyInt_FromSsize_t PyLong_FromSsize_t
+ #define PyInt_AsLong PyLong_AsLong
+ #define PyInt_AS_LONG PyLong_AS_LONG
+ #define PyInt_AsSsize_t PyLong_AsSsize_t
+ #define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLongMask
+ #define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyBoolObject PyLongObject
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_TrueDivide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceTrueDivide(x,y)
+#else
+ #define __Pyx_PyNumber_Divide(x,y) PyNumber_Divide(x,y)
+ #define __Pyx_PyNumber_InPlaceDivide(x,y) PyNumber_InPlaceDivide(x,y)
+#endif
+
+#if (PY_MAJOR_VERSION < 3) || (PY_VERSION_HEX >= 0x03010300)
+ #define __Pyx_PySequence_GetSlice(obj, a, b) PySequence_GetSlice(obj, a, b)
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) PySequence_SetSlice(obj, a, b, value)
+ #define __Pyx_PySequence_DelSlice(obj, a, b) PySequence_DelSlice(obj, a, b)
+#else
+ #define __Pyx_PySequence_GetSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), (PyObject*)0) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_GetSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object is unsliceable", (obj)->ob_type->tp_name), (PyObject*)0)))
+ #define __Pyx_PySequence_SetSlice(obj, a, b, value) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_SetSlice(obj, a, b, value)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice assignment", (obj)->ob_type->tp_name), -1)))
+ #define __Pyx_PySequence_DelSlice(obj, a, b) (unlikely(!(obj)) ? \
+ (PyErr_SetString(PyExc_SystemError, "null argument to internal routine"), -1) : \
+ (likely((obj)->ob_type->tp_as_mapping) ? (PySequence_DelSlice(obj, a, b)) : \
+ (PyErr_Format(PyExc_TypeError, "'%.200s' object doesn't support slice deletion", (obj)->ob_type->tp_name), -1)))
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ #define PyMethod_New(func, self, klass) ((self) ? PyMethod_New(func, self) : PyInstanceMethod_New(func))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),((char *)(n)))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),((char *)(n)),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),((char *)(n)))
+#else
+ #define __Pyx_GetAttrString(o,n) PyObject_GetAttrString((o),(n))
+ #define __Pyx_SetAttrString(o,n,a) PyObject_SetAttrString((o),(n),(a))
+ #define __Pyx_DelAttrString(o,n) PyObject_DelAttrString((o),(n))
+#endif
+
+#if PY_VERSION_HEX < 0x02050000
+ #define __Pyx_NAMESTR(n) ((char *)(n))
+ #define __Pyx_DOCSTR(n) ((char *)(n))
+#else
+ #define __Pyx_NAMESTR(n) (n)
+ #define __Pyx_DOCSTR(n) (n)
+#endif
+
+#ifdef __cplusplus
+#define __PYX_EXTERN_C extern "C"
+#else
+#define __PYX_EXTERN_C extern
+#endif
+
+#if defined(WIN32) || defined(MS_WINDOWS)
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+#define __PYX_HAVE_API__twisted__test__raiser
+
+#ifdef PYREX_WITHOUT_ASSERTIONS
+#define CYTHON_WITHOUT_ASSERTIONS
+#endif
+
+
+/* inline attribute */
+#ifndef CYTHON_INLINE
+ #if defined(__GNUC__)
+ #define CYTHON_INLINE __inline__
+ #elif defined(_MSC_VER)
+ #define CYTHON_INLINE __inline
+ #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+ #define CYTHON_INLINE inline
+ #else
+ #define CYTHON_INLINE
+ #endif
+#endif
+
+/* unused attribute */
+#ifndef CYTHON_UNUSED
+# if defined(__GNUC__)
+# if !(defined(__cplusplus)) || (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4))
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+# elif defined(__ICC) || defined(__INTEL_COMPILER)
+# define CYTHON_UNUSED __attribute__ ((__unused__))
+# else
+# define CYTHON_UNUSED
+# endif
+#endif
+
+typedef struct {PyObject **p; char *s; const long n; const char* encoding; const char is_unicode; const char is_str; const char intern; } __Pyx_StringTabEntry; /*proto*/
+
+
+/* Type Conversion Predeclarations */
+
+#define __Pyx_PyBytes_FromUString(s) PyBytes_FromString((char*)s)
+#define __Pyx_PyBytes_AsUString(s) ((unsigned char*) PyBytes_AsString(s))
+
+#define __Pyx_PyBool_FromLong(b) ((b) ? (Py_INCREF(Py_True), Py_True) : (Py_INCREF(Py_False), Py_False))
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject*);
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x);
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject*);
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t);
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject*);
+
+#define __pyx_PyFloat_AsDouble(x) (PyFloat_CheckExact(x) ? PyFloat_AS_DOUBLE(x) : PyFloat_AsDouble(x))
+
+
+#ifdef __GNUC__
+/* Test for GCC > 2.95 */
+#if __GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95))
+#define likely(x) __builtin_expect(!!(x), 1)
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#else /* __GNUC__ > 2 ... */
+#define likely(x) (x)
+#define unlikely(x) (x)
+#endif /* __GNUC__ > 2 ... */
+#else /* __GNUC__ */
+#define likely(x) (x)
+#define unlikely(x) (x)
+#endif /* __GNUC__ */
+
+static PyObject *__pyx_m;
+static PyObject *__pyx_b;
+static PyObject *__pyx_empty_tuple;
+static PyObject *__pyx_empty_bytes;
+static int __pyx_lineno;
+static int __pyx_clineno = 0;
+static const char * __pyx_cfilenm= __FILE__;
+static const char *__pyx_filename;
+
+
+static const char *__pyx_f[] = {
+ "raiser.pyx",
+};
+
+/* Type declarations */
+
+#ifndef CYTHON_REFNANNY
+ #define CYTHON_REFNANNY 0
+#endif
+
+#if CYTHON_REFNANNY
+ typedef struct {
+ void (*INCREF)(void*, PyObject*, int);
+ void (*DECREF)(void*, PyObject*, int);
+ void (*GOTREF)(void*, PyObject*, int);
+ void (*GIVEREF)(void*, PyObject*, int);
+ void* (*SetupContext)(const char*, int, const char*);
+ void (*FinishContext)(void**);
+ } __Pyx_RefNannyAPIStruct;
+ static __Pyx_RefNannyAPIStruct *__Pyx_RefNanny = NULL;
+ static __Pyx_RefNannyAPIStruct * __Pyx_RefNannyImportAPI(const char *modname) {
+ PyObject *m = NULL, *p = NULL;
+ void *r = NULL;
+ m = PyImport_ImportModule((char *)modname);
+ if (!m) goto end;
+ p = PyObject_GetAttrString(m, (char *)"RefNannyAPI");
+ if (!p) goto end;
+ r = PyLong_AsVoidPtr(p);
+ end:
+ Py_XDECREF(p);
+ Py_XDECREF(m);
+ return (__Pyx_RefNannyAPIStruct *)r;
+ }
+ #define __Pyx_RefNannySetupContext(name) void *__pyx_refnanny = __Pyx_RefNanny->SetupContext((name), __LINE__, __FILE__)
+ #define __Pyx_RefNannyFinishContext() __Pyx_RefNanny->FinishContext(&__pyx_refnanny)
+ #define __Pyx_INCREF(r) __Pyx_RefNanny->INCREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_DECREF(r) __Pyx_RefNanny->DECREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GOTREF(r) __Pyx_RefNanny->GOTREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_GIVEREF(r) __Pyx_RefNanny->GIVEREF(__pyx_refnanny, (PyObject *)(r), __LINE__)
+ #define __Pyx_XDECREF(r) do { if((r) != NULL) {__Pyx_DECREF(r);} } while(0)
+#else
+ #define __Pyx_RefNannySetupContext(name)
+ #define __Pyx_RefNannyFinishContext()
+ #define __Pyx_INCREF(r) Py_INCREF(r)
+ #define __Pyx_DECREF(r) Py_DECREF(r)
+ #define __Pyx_GOTREF(r)
+ #define __Pyx_GIVEREF(r)
+ #define __Pyx_XDECREF(r) Py_XDECREF(r)
+#endif /* CYTHON_REFNANNY */
+#define __Pyx_XGIVEREF(r) do { if((r) != NULL) {__Pyx_GIVEREF(r);} } while(0)
+#define __Pyx_XGOTREF(r) do { if((r) != NULL) {__Pyx_GOTREF(r);} } while(0)
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name); /*proto*/
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb); /*proto*/
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb); /*proto*/
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb); /*proto*/
+
+static PyObject *__Pyx_FindPy2Metaclass(PyObject *bases); /*proto*/
+
+static PyObject *__Pyx_CreateClass(PyObject *bases, PyObject *dict, PyObject *name,
+ PyObject *modname); /*proto*/
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject *);
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject *);
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject *);
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject *);
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject *);
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject *);
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject *);
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject *);
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject *);
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject *);
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject *);
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject *);
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject *);
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject *);
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject *);
+
+static void __Pyx_AddTraceback(const char *funcname); /*proto*/
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t); /*proto*/
+/* Module declarations from twisted.test.raiser */
+
+#define __Pyx_MODULE_NAME "twisted.test.raiser"
+static int __pyx_module_is_main_twisted__test__raiser = 0;
+
+/* Implementation of twisted.test.raiser */
+static PyObject *__pyx_builtin_Exception;
+static char __pyx_k_1[] = "This function is intentionally broken";
+static char __pyx_k_3[] = "\nA trivial extension that just raises an exception.\nSee L{twisted.test.test_failure.test_failureConstructionWithMungedStackSucceeds}.\n";
+static char __pyx_k_4[] = "\n A speficic exception only used to be identified in tests.\n ";
+static char __pyx_k_5[] = "twisted.test.raiser";
+static char __pyx_k____main__[] = "__main__";
+static char __pyx_k____test__[] = "__test__";
+static char __pyx_k__Exception[] = "Exception";
+static char __pyx_k__raiseException[] = "raiseException";
+static char __pyx_k__RaiserException[] = "RaiserException";
+static PyObject *__pyx_kp_s_1;
+static PyObject *__pyx_kp_s_4;
+static PyObject *__pyx_n_s_5;
+static PyObject *__pyx_n_s__Exception;
+static PyObject *__pyx_n_s__RaiserException;
+static PyObject *__pyx_n_s____main__;
+static PyObject *__pyx_n_s____test__;
+static PyObject *__pyx_n_s__raiseException;
+static PyObject *__pyx_k_tuple_2;
+
+/* "twisted/test/raiser.pyx":17
+ *
+ *
+ * def raiseException(): # <<<<<<<<<<<<<<
+ * """
+ * Raise L{RaiserException}.
+ */
+
+static PyObject *__pyx_pf_7twisted_4test_6raiser_raiseException(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused); /*proto*/
+static char __pyx_doc_7twisted_4test_6raiser_raiseException[] = "\n Raise L{RaiserException}.\n ";
+static PyMethodDef __pyx_mdef_7twisted_4test_6raiser_raiseException = {__Pyx_NAMESTR("raiseException"), (PyCFunction)__pyx_pf_7twisted_4test_6raiser_raiseException, METH_NOARGS, __Pyx_DOCSTR(__pyx_doc_7twisted_4test_6raiser_raiseException)};
+static PyObject *__pyx_pf_7twisted_4test_6raiser_raiseException(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused) {
+ PyObject *__pyx_r = NULL;
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ __Pyx_RefNannySetupContext("raiseException");
+ __pyx_self = __pyx_self;
+
+ /* "twisted/test/raiser.pyx":21
+ * Raise L{RaiserException}.
+ * """
+ * raise RaiserException("This function is intentionally broken") # <<<<<<<<<<<<<<
+ */
+ __pyx_t_1 = __Pyx_GetName(__pyx_m, __pyx_n_s__RaiserException); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 21; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ __pyx_t_2 = PyObject_Call(__pyx_t_1, ((PyObject *)__pyx_k_tuple_2), NULL); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 21; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_2);
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+ __Pyx_Raise(__pyx_t_2, 0, 0);
+ __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
+ {__pyx_filename = __pyx_f[0]; __pyx_lineno = 21; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+
+ __pyx_r = Py_None; __Pyx_INCREF(Py_None);
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_AddTraceback("twisted.test.raiser.raiseException");
+ __pyx_r = NULL;
+ __pyx_L0:;
+ __Pyx_XGIVEREF(__pyx_r);
+ __Pyx_RefNannyFinishContext();
+ return __pyx_r;
+}
+
+static PyMethodDef __pyx_methods[] = {
+ {0, 0, 0, 0}
+};
+
+#if PY_MAJOR_VERSION >= 3
+static struct PyModuleDef __pyx_moduledef = {
+ PyModuleDef_HEAD_INIT,
+ __Pyx_NAMESTR("raiser"),
+ __Pyx_DOCSTR(__pyx_k_3), /* m_doc */
+ -1, /* m_size */
+ __pyx_methods /* m_methods */,
+ NULL, /* m_reload */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ NULL /* m_free */
+};
+#endif
+
+static __Pyx_StringTabEntry __pyx_string_tab[] = {
+ {&__pyx_kp_s_1, __pyx_k_1, sizeof(__pyx_k_1), 0, 0, 1, 0},
+ {&__pyx_kp_s_4, __pyx_k_4, sizeof(__pyx_k_4), 0, 0, 1, 0},
+ {&__pyx_n_s_5, __pyx_k_5, sizeof(__pyx_k_5), 0, 0, 1, 1},
+ {&__pyx_n_s__Exception, __pyx_k__Exception, sizeof(__pyx_k__Exception), 0, 0, 1, 1},
+ {&__pyx_n_s__RaiserException, __pyx_k__RaiserException, sizeof(__pyx_k__RaiserException), 0, 0, 1, 1},
+ {&__pyx_n_s____main__, __pyx_k____main__, sizeof(__pyx_k____main__), 0, 0, 1, 1},
+ {&__pyx_n_s____test__, __pyx_k____test__, sizeof(__pyx_k____test__), 0, 0, 1, 1},
+ {&__pyx_n_s__raiseException, __pyx_k__raiseException, sizeof(__pyx_k__raiseException), 0, 0, 1, 1},
+ {0, 0, 0, 0, 0, 0, 0}
+};
+static int __Pyx_InitCachedBuiltins(void) {
+ __pyx_builtin_Exception = __Pyx_GetName(__pyx_b, __pyx_n_s__Exception); if (!__pyx_builtin_Exception) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+static int __Pyx_InitCachedConstants(void) {
+ __Pyx_RefNannySetupContext("__Pyx_InitCachedConstants");
+
+ /* "twisted/test/raiser.pyx":21
+ * Raise L{RaiserException}.
+ * """
+ * raise RaiserException("This function is intentionally broken") # <<<<<<<<<<<<<<
+ */
+ __pyx_k_tuple_2 = PyTuple_New(1); if (unlikely(!__pyx_k_tuple_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 21; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_k_tuple_2));
+ __Pyx_INCREF(((PyObject *)__pyx_kp_s_1));
+ PyTuple_SET_ITEM(__pyx_k_tuple_2, 0, ((PyObject *)__pyx_kp_s_1));
+ __Pyx_GIVEREF(((PyObject *)__pyx_kp_s_1));
+ __Pyx_GIVEREF(((PyObject *)__pyx_k_tuple_2));
+ __Pyx_RefNannyFinishContext();
+ return 0;
+ __pyx_L1_error:;
+ __Pyx_RefNannyFinishContext();
+ return -1;
+}
+
+static int __Pyx_InitGlobals(void) {
+ if (__Pyx_InitStrings(__pyx_string_tab) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ return 0;
+ __pyx_L1_error:;
+ return -1;
+}
+
+#if PY_MAJOR_VERSION < 3
+PyMODINIT_FUNC initraiser(void); /*proto*/
+PyMODINIT_FUNC initraiser(void)
+#else
+PyMODINIT_FUNC PyInit_raiser(void); /*proto*/
+PyMODINIT_FUNC PyInit_raiser(void)
+#endif
+{
+ PyObject *__pyx_t_1 = NULL;
+ PyObject *__pyx_t_2 = NULL;
+ PyObject *__pyx_t_3 = NULL;
+ #if CYTHON_REFNANNY
+ void* __pyx_refnanny = NULL;
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("refnanny");
+ if (!__Pyx_RefNanny) {
+ PyErr_Clear();
+ __Pyx_RefNanny = __Pyx_RefNannyImportAPI("Cython.Runtime.refnanny");
+ if (!__Pyx_RefNanny)
+ Py_FatalError("failed to import 'refnanny' module");
+ }
+ __pyx_refnanny = __Pyx_RefNanny->SetupContext("PyMODINIT_FUNC PyInit_raiser(void)", __LINE__, __FILE__);
+ #endif
+ __pyx_empty_tuple = PyTuple_New(0); if (unlikely(!__pyx_empty_tuple)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_empty_bytes = PyBytes_FromStringAndSize("", 0); if (unlikely(!__pyx_empty_bytes)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #ifdef __pyx_binding_PyCFunctionType_USED
+ if (__pyx_binding_PyCFunctionType_init() < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ #endif
+ /*--- Library function declarations ---*/
+ /*--- Threads initialization code ---*/
+ #if defined(__PYX_FORCE_INIT_THREADS) && __PYX_FORCE_INIT_THREADS
+ #ifdef WITH_THREAD /* Python build with threading support? */
+ PyEval_InitThreads();
+ #endif
+ #endif
+ /*--- Module creation code ---*/
+ #if PY_MAJOR_VERSION < 3
+ __pyx_m = Py_InitModule4(__Pyx_NAMESTR("raiser"), __pyx_methods, __Pyx_DOCSTR(__pyx_k_3), 0, PYTHON_API_VERSION);
+ #else
+ __pyx_m = PyModule_Create(&__pyx_moduledef);
+ #endif
+ if (!__pyx_m) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ #if PY_MAJOR_VERSION < 3
+ Py_INCREF(__pyx_m);
+ #endif
+ __pyx_b = PyImport_AddModule(__Pyx_NAMESTR(__Pyx_BUILTIN_MODULE_NAME));
+ if (!__pyx_b) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ if (__Pyx_SetAttrString(__pyx_m, "__builtins__", __pyx_b) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ /*--- Initialize various global constants etc. ---*/
+ if (unlikely(__Pyx_InitGlobals() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ if (__pyx_module_is_main_twisted__test__raiser) {
+ if (__Pyx_SetAttrString(__pyx_m, "__name__", __pyx_n_s____main__) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;};
+ }
+ /*--- Builtin init code ---*/
+ if (unlikely(__Pyx_InitCachedBuiltins() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Constants init code ---*/
+ if (unlikely(__Pyx_InitCachedConstants() < 0)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ /*--- Global init code ---*/
+ /*--- Function export code ---*/
+ /*--- Type init code ---*/
+ /*--- Type import code ---*/
+ /*--- Function import code ---*/
+ /*--- Execution code ---*/
+
+ /* "twisted/test/raiser.pyx":11
+ *
+ *
+ * class RaiserException(Exception): # <<<<<<<<<<<<<<
+ * """
+ * A speficic exception only used to be identified in tests.
+ */
+ __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_1));
+ __pyx_t_2 = PyTuple_New(1); if (unlikely(!__pyx_t_2)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_2));
+ __Pyx_INCREF(__pyx_builtin_Exception);
+ PyTuple_SET_ITEM(__pyx_t_2, 0, __pyx_builtin_Exception);
+ __Pyx_GIVEREF(__pyx_builtin_Exception);
+ if (PyDict_SetItemString(((PyObject *)__pyx_t_1), "__doc__", ((PyObject *)__pyx_kp_s_4)) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __pyx_t_3 = __Pyx_CreateClass(((PyObject *)__pyx_t_2), ((PyObject *)__pyx_t_1), __pyx_n_s__RaiserException, __pyx_n_s_5); if (unlikely(!__pyx_t_3)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_3);
+ __Pyx_DECREF(((PyObject *)__pyx_t_2)); __pyx_t_2 = 0;
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__RaiserException, __pyx_t_3) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 11; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
+ __Pyx_DECREF(((PyObject *)__pyx_t_1)); __pyx_t_1 = 0;
+
+ /* "twisted/test/raiser.pyx":17
+ *
+ *
+ * def raiseException(): # <<<<<<<<<<<<<<
+ * """
+ * Raise L{RaiserException}.
+ */
+ __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_7twisted_4test_6raiser_raiseException, NULL, __pyx_n_s_5); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 17; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(__pyx_t_1);
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s__raiseException, __pyx_t_1) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 17; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
+
+ /* "twisted/test/raiser.pyx":1
+ * # Copyright (c) Twisted Matrix Laboratories. # <<<<<<<<<<<<<<
+ * # See LICENSE for details.
+ *
+ */
+ __pyx_t_1 = PyDict_New(); if (unlikely(!__pyx_t_1)) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_GOTREF(((PyObject *)__pyx_t_1));
+ if (PyObject_SetAttr(__pyx_m, __pyx_n_s____test__, ((PyObject *)__pyx_t_1)) < 0) {__pyx_filename = __pyx_f[0]; __pyx_lineno = 1; __pyx_clineno = __LINE__; goto __pyx_L1_error;}
+ __Pyx_DECREF(((PyObject *)__pyx_t_1)); __pyx_t_1 = 0;
+ goto __pyx_L0;
+ __pyx_L1_error:;
+ __Pyx_XDECREF(__pyx_t_1);
+ __Pyx_XDECREF(__pyx_t_2);
+ __Pyx_XDECREF(__pyx_t_3);
+ if (__pyx_m) {
+ __Pyx_AddTraceback("init twisted.test.raiser");
+ Py_DECREF(__pyx_m); __pyx_m = 0;
+ } else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_ImportError, "init twisted.test.raiser");
+ }
+ __pyx_L0:;
+ __Pyx_RefNannyFinishContext();
+ #if PY_MAJOR_VERSION < 3
+ return;
+ #else
+ return __pyx_m;
+ #endif
+}
+
+/* Runtime support code */
+
+static PyObject *__Pyx_GetName(PyObject *dict, PyObject *name) {
+ PyObject *result;
+ result = PyObject_GetAttr(dict, name);
+ if (!result)
+ PyErr_SetObject(PyExc_NameError, name);
+ return result;
+}
+
+static CYTHON_INLINE void __Pyx_ErrRestore(PyObject *type, PyObject *value, PyObject *tb) {
+ PyObject *tmp_type, *tmp_value, *tmp_tb;
+ PyThreadState *tstate = PyThreadState_GET();
+
+ tmp_type = tstate->curexc_type;
+ tmp_value = tstate->curexc_value;
+ tmp_tb = tstate->curexc_traceback;
+ tstate->curexc_type = type;
+ tstate->curexc_value = value;
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_type);
+ Py_XDECREF(tmp_value);
+ Py_XDECREF(tmp_tb);
+}
+
+static CYTHON_INLINE void __Pyx_ErrFetch(PyObject **type, PyObject **value, PyObject **tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ *type = tstate->curexc_type;
+ *value = tstate->curexc_value;
+ *tb = tstate->curexc_traceback;
+
+ tstate->curexc_type = 0;
+ tstate->curexc_value = 0;
+ tstate->curexc_traceback = 0;
+}
+
+
+#if PY_MAJOR_VERSION < 3
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb) {
+ Py_XINCREF(type);
+ Py_XINCREF(value);
+ Py_XINCREF(tb);
+ /* First, check the traceback argument, replacing None with NULL. */
+ if (tb == Py_None) {
+ Py_DECREF(tb);
+ tb = 0;
+ }
+ else if (tb != NULL && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto raise_error;
+ }
+ /* Next, replace a missing value with None */
+ if (value == NULL) {
+ value = Py_None;
+ Py_INCREF(value);
+ }
+ #if PY_VERSION_HEX < 0x02050000
+ if (!PyClass_Check(type))
+ #else
+ if (!PyType_Check(type))
+ #endif
+ {
+ /* Raising an instance. The value should be a dummy. */
+ if (value != Py_None) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto raise_error;
+ }
+ /* Normalize to raise <class>, <instance> */
+ Py_DECREF(value);
+ value = type;
+ #if PY_VERSION_HEX < 0x02050000
+ if (PyInstance_Check(type)) {
+ type = (PyObject*) ((PyInstanceObject*)type)->in_class;
+ Py_INCREF(type);
+ }
+ else {
+ type = 0;
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception must be an old-style class or instance");
+ goto raise_error;
+ }
+ #else
+ type = (PyObject*) Py_TYPE(type);
+ Py_INCREF(type);
+ if (!PyType_IsSubtype((PyTypeObject *)type, (PyTypeObject *)PyExc_BaseException)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto raise_error;
+ }
+ #endif
+ }
+
+ __Pyx_ErrRestore(type, value, tb);
+ return;
+raise_error:
+ Py_XDECREF(value);
+ Py_XDECREF(type);
+ Py_XDECREF(tb);
+ return;
+}
+
+#else /* Python 3+ */
+
+static void __Pyx_Raise(PyObject *type, PyObject *value, PyObject *tb) {
+ if (tb == Py_None) {
+ tb = 0;
+ } else if (tb && !PyTraceBack_Check(tb)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: arg 3 must be a traceback or None");
+ goto bad;
+ }
+ if (value == Py_None)
+ value = 0;
+
+ if (PyExceptionInstance_Check(type)) {
+ if (value) {
+ PyErr_SetString(PyExc_TypeError,
+ "instance exception may not have a separate value");
+ goto bad;
+ }
+ value = type;
+ type = (PyObject*) Py_TYPE(value);
+ } else if (!PyExceptionClass_Check(type)) {
+ PyErr_SetString(PyExc_TypeError,
+ "raise: exception class must be a subclass of BaseException");
+ goto bad;
+ }
+
+ PyErr_SetObject(type, value);
+
+ if (tb) {
+ PyThreadState *tstate = PyThreadState_GET();
+ PyObject* tmp_tb = tstate->curexc_traceback;
+ if (tb != tmp_tb) {
+ Py_INCREF(tb);
+ tstate->curexc_traceback = tb;
+ Py_XDECREF(tmp_tb);
+ }
+ }
+
+bad:
+ return;
+}
+#endif
+
+static PyObject *__Pyx_FindPy2Metaclass(PyObject *bases) {
+ PyObject *metaclass;
+ /* Default metaclass */
+#if PY_MAJOR_VERSION < 3
+ if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
+ PyObject *base = PyTuple_GET_ITEM(bases, 0);
+ metaclass = PyObject_GetAttrString(base, "__class__");
+ if (!metaclass) {
+ PyErr_Clear();
+ metaclass = (PyObject*) Py_TYPE(base);
+ }
+ } else {
+ metaclass = (PyObject *) &PyClass_Type;
+ }
+#else
+ if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
+ PyObject *base = PyTuple_GET_ITEM(bases, 0);
+ metaclass = (PyObject*) Py_TYPE(base);
+ } else {
+ metaclass = (PyObject *) &PyType_Type;
+ }
+#endif
+ Py_INCREF(metaclass);
+ return metaclass;
+}
+
+static PyObject *__Pyx_CreateClass(PyObject *bases, PyObject *dict, PyObject *name,
+ PyObject *modname) {
+ PyObject *result;
+ PyObject *metaclass;
+
+ if (PyDict_SetItemString(dict, "__module__", modname) < 0)
+ return NULL;
+
+ /* Python2 __metaclass__ */
+ metaclass = PyDict_GetItemString(dict, "__metaclass__");
+ if (metaclass) {
+ Py_INCREF(metaclass);
+ } else {
+ metaclass = __Pyx_FindPy2Metaclass(bases);
+ }
+ result = PyObject_CallFunctionObjArgs(metaclass, name, bases, dict, NULL);
+ Py_DECREF(metaclass);
+ return result;
+}
+
+static CYTHON_INLINE unsigned char __Pyx_PyInt_AsUnsignedChar(PyObject* x) {
+ const unsigned char neg_one = (unsigned char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned char" :
+ "value too large to convert to unsigned char");
+ }
+ return (unsigned char)-1;
+ }
+ return (unsigned char)val;
+ }
+ return (unsigned char)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned short __Pyx_PyInt_AsUnsignedShort(PyObject* x) {
+ const unsigned short neg_one = (unsigned short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned short" :
+ "value too large to convert to unsigned short");
+ }
+ return (unsigned short)-1;
+ }
+ return (unsigned short)val;
+ }
+ return (unsigned short)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE unsigned int __Pyx_PyInt_AsUnsignedInt(PyObject* x) {
+ const unsigned int neg_one = (unsigned int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(unsigned int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(unsigned int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to unsigned int" :
+ "value too large to convert to unsigned int");
+ }
+ return (unsigned int)-1;
+ }
+ return (unsigned int)val;
+ }
+ return (unsigned int)__Pyx_PyInt_AsUnsignedLong(x);
+}
+
+static CYTHON_INLINE char __Pyx_PyInt_AsChar(PyObject* x) {
+ const char neg_one = (char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to char" :
+ "value too large to convert to char");
+ }
+ return (char)-1;
+ }
+ return (char)val;
+ }
+ return (char)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE short __Pyx_PyInt_AsShort(PyObject* x) {
+ const short neg_one = (short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to short" :
+ "value too large to convert to short");
+ }
+ return (short)-1;
+ }
+ return (short)val;
+ }
+ return (short)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsInt(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE signed char __Pyx_PyInt_AsSignedChar(PyObject* x) {
+ const signed char neg_one = (signed char)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed char) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed char)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed char" :
+ "value too large to convert to signed char");
+ }
+ return (signed char)-1;
+ }
+ return (signed char)val;
+ }
+ return (signed char)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed short __Pyx_PyInt_AsSignedShort(PyObject* x) {
+ const signed short neg_one = (signed short)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed short) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed short)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed short" :
+ "value too large to convert to signed short");
+ }
+ return (signed short)-1;
+ }
+ return (signed short)val;
+ }
+ return (signed short)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE signed int __Pyx_PyInt_AsSignedInt(PyObject* x) {
+ const signed int neg_one = (signed int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(signed int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(signed int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to signed int" :
+ "value too large to convert to signed int");
+ }
+ return (signed int)-1;
+ }
+ return (signed int)val;
+ }
+ return (signed int)__Pyx_PyInt_AsSignedLong(x);
+}
+
+static CYTHON_INLINE int __Pyx_PyInt_AsLongDouble(PyObject* x) {
+ const int neg_one = (int)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+ if (sizeof(int) < sizeof(long)) {
+ long val = __Pyx_PyInt_AsLong(x);
+ if (unlikely(val != (long)(int)val)) {
+ if (!unlikely(val == -1 && PyErr_Occurred())) {
+ PyErr_SetString(PyExc_OverflowError,
+ (is_unsigned && unlikely(val < 0)) ?
+ "can't convert negative value to int" :
+ "value too large to convert to int");
+ }
+ return (int)-1;
+ }
+ return (int)val;
+ }
+ return (int)__Pyx_PyInt_AsLong(x);
+}
+
+static CYTHON_INLINE unsigned long __Pyx_PyInt_AsUnsignedLong(PyObject* x) {
+ const unsigned long neg_one = (unsigned long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return (unsigned long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned long");
+ return (unsigned long)-1;
+ }
+ return PyLong_AsUnsignedLong(x);
+ } else {
+ return PyLong_AsLong(x);
+ }
+ } else {
+ unsigned long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned long)-1;
+ val = __Pyx_PyInt_AsUnsignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE unsigned PY_LONG_LONG __Pyx_PyInt_AsUnsignedLongLong(PyObject* x) {
+ const unsigned PY_LONG_LONG neg_one = (unsigned PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return (unsigned PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to unsigned PY_LONG_LONG");
+ return (unsigned PY_LONG_LONG)-1;
+ }
+ return PyLong_AsUnsignedLongLong(x);
+ } else {
+ return PyLong_AsLongLong(x);
+ }
+ } else {
+ unsigned PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (unsigned PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsUnsignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE long __Pyx_PyInt_AsLong(PyObject* x) {
+ const long neg_one = (long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return (long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to long");
+ return (long)-1;
+ }
+ return PyLong_AsUnsignedLong(x);
+ } else {
+ return PyLong_AsLong(x);
+ }
+ } else {
+ long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (long)-1;
+ val = __Pyx_PyInt_AsLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE PY_LONG_LONG __Pyx_PyInt_AsLongLong(PyObject* x) {
+ const PY_LONG_LONG neg_one = (PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return (PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to PY_LONG_LONG");
+ return (PY_LONG_LONG)-1;
+ }
+ return PyLong_AsUnsignedLongLong(x);
+ } else {
+ return PyLong_AsLongLong(x);
+ }
+ } else {
+ PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed long __Pyx_PyInt_AsSignedLong(PyObject* x) {
+ const signed long neg_one = (signed long)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return (signed long)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed long");
+ return (signed long)-1;
+ }
+ return PyLong_AsUnsignedLong(x);
+ } else {
+ return PyLong_AsLong(x);
+ }
+ } else {
+ signed long val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed long)-1;
+ val = __Pyx_PyInt_AsSignedLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+static CYTHON_INLINE signed PY_LONG_LONG __Pyx_PyInt_AsSignedLongLong(PyObject* x) {
+ const signed PY_LONG_LONG neg_one = (signed PY_LONG_LONG)-1, const_zero = 0;
+ const int is_unsigned = neg_one > const_zero;
+#if PY_VERSION_HEX < 0x03000000
+ if (likely(PyInt_Check(x))) {
+ long val = PyInt_AS_LONG(x);
+ if (is_unsigned && unlikely(val < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return (signed PY_LONG_LONG)val;
+ } else
+#endif
+ if (likely(PyLong_Check(x))) {
+ if (is_unsigned) {
+ if (unlikely(Py_SIZE(x) < 0)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "can't convert negative value to signed PY_LONG_LONG");
+ return (signed PY_LONG_LONG)-1;
+ }
+ return PyLong_AsUnsignedLongLong(x);
+ } else {
+ return PyLong_AsLongLong(x);
+ }
+ } else {
+ signed PY_LONG_LONG val;
+ PyObject *tmp = __Pyx_PyNumber_Int(x);
+ if (!tmp) return (signed PY_LONG_LONG)-1;
+ val = __Pyx_PyInt_AsSignedLongLong(tmp);
+ Py_DECREF(tmp);
+ return val;
+ }
+}
+
+#include "compile.h"
+#include "frameobject.h"
+#include "traceback.h"
+
+static void __Pyx_AddTraceback(const char *funcname) {
+ PyObject *py_srcfile = 0;
+ PyObject *py_funcname = 0;
+ PyObject *py_globals = 0;
+ PyCodeObject *py_code = 0;
+ PyFrameObject *py_frame = 0;
+
+ #if PY_MAJOR_VERSION < 3
+ py_srcfile = PyString_FromString(__pyx_filename);
+ #else
+ py_srcfile = PyUnicode_FromString(__pyx_filename);
+ #endif
+ if (!py_srcfile) goto bad;
+ if (__pyx_clineno) {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #else
+ py_funcname = PyUnicode_FromFormat( "%s (%s:%d)", funcname, __pyx_cfilenm, __pyx_clineno);
+ #endif
+ }
+ else {
+ #if PY_MAJOR_VERSION < 3
+ py_funcname = PyString_FromString(funcname);
+ #else
+ py_funcname = PyUnicode_FromString(funcname);
+ #endif
+ }
+ if (!py_funcname) goto bad;
+ py_globals = PyModule_GetDict(__pyx_m);
+ if (!py_globals) goto bad;
+ py_code = PyCode_New(
+ 0, /*int argcount,*/
+ #if PY_MAJOR_VERSION >= 3
+ 0, /*int kwonlyargcount,*/
+ #endif
+ 0, /*int nlocals,*/
+ 0, /*int stacksize,*/
+ 0, /*int flags,*/
+ __pyx_empty_bytes, /*PyObject *code,*/
+ __pyx_empty_tuple, /*PyObject *consts,*/
+ __pyx_empty_tuple, /*PyObject *names,*/
+ __pyx_empty_tuple, /*PyObject *varnames,*/
+ __pyx_empty_tuple, /*PyObject *freevars,*/
+ __pyx_empty_tuple, /*PyObject *cellvars,*/
+ py_srcfile, /*PyObject *filename,*/
+ py_funcname, /*PyObject *name,*/
+ __pyx_lineno, /*int firstlineno,*/
+ __pyx_empty_bytes /*PyObject *lnotab*/
+ );
+ if (!py_code) goto bad;
+ py_frame = PyFrame_New(
+ PyThreadState_GET(), /*PyThreadState *tstate,*/
+ py_code, /*PyCodeObject *code,*/
+ py_globals, /*PyObject *globals,*/
+ 0 /*PyObject *locals*/
+ );
+ if (!py_frame) goto bad;
+ py_frame->f_lineno = __pyx_lineno;
+ PyTraceBack_Here(py_frame);
+bad:
+ Py_XDECREF(py_srcfile);
+ Py_XDECREF(py_funcname);
+ Py_XDECREF(py_code);
+ Py_XDECREF(py_frame);
+}
+
+static int __Pyx_InitStrings(__Pyx_StringTabEntry *t) {
+ while (t->p) {
+ #if PY_MAJOR_VERSION < 3
+ if (t->is_unicode) {
+ *t->p = PyUnicode_DecodeUTF8(t->s, t->n - 1, NULL);
+ } else if (t->intern) {
+ *t->p = PyString_InternFromString(t->s);
+ } else {
+ *t->p = PyString_FromStringAndSize(t->s, t->n - 1);
+ }
+ #else /* Python 3+ has unicode identifiers */
+ if (t->is_unicode | t->is_str) {
+ if (t->intern) {
+ *t->p = PyUnicode_InternFromString(t->s);
+ } else if (t->encoding) {
+ *t->p = PyUnicode_Decode(t->s, t->n - 1, t->encoding, NULL);
+ } else {
+ *t->p = PyUnicode_FromStringAndSize(t->s, t->n - 1);
+ }
+ } else {
+ *t->p = PyBytes_FromStringAndSize(t->s, t->n - 1);
+ }
+ #endif
+ if (!*t->p)
+ return -1;
+ ++t;
+ }
+ return 0;
+}
+
+/* Type Conversion Functions */
+
+static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject* x) {
+ int is_true = x == Py_True;
+ if (is_true | (x == Py_False) | (x == Py_None)) return is_true;
+ else return PyObject_IsTrue(x);
+}
+
+static CYTHON_INLINE PyObject* __Pyx_PyNumber_Int(PyObject* x) {
+ PyNumberMethods *m;
+ const char *name = NULL;
+ PyObject *res = NULL;
+#if PY_VERSION_HEX < 0x03000000
+ if (PyInt_Check(x) || PyLong_Check(x))
+#else
+ if (PyLong_Check(x))
+#endif
+ return Py_INCREF(x), x;
+ m = Py_TYPE(x)->tp_as_number;
+#if PY_VERSION_HEX < 0x03000000
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Int(x);
+ }
+ else if (m && m->nb_long) {
+ name = "long";
+ res = PyNumber_Long(x);
+ }
+#else
+ if (m && m->nb_int) {
+ name = "int";
+ res = PyNumber_Long(x);
+ }
+#endif
+ if (res) {
+#if PY_VERSION_HEX < 0x03000000
+ if (!PyInt_Check(res) && !PyLong_Check(res)) {
+#else
+ if (!PyLong_Check(res)) {
+#endif
+ PyErr_Format(PyExc_TypeError,
+ "__%s__ returned non-%s (type %.200s)",
+ name, name, Py_TYPE(res)->tp_name);
+ Py_DECREF(res);
+ return NULL;
+ }
+ }
+ else if (!PyErr_Occurred()) {
+ PyErr_SetString(PyExc_TypeError,
+ "an integer is required");
+ }
+ return res;
+}
+
+static CYTHON_INLINE Py_ssize_t __Pyx_PyIndex_AsSsize_t(PyObject* b) {
+ Py_ssize_t ival;
+ PyObject* x = PyNumber_Index(b);
+ if (!x) return -1;
+ ival = PyInt_AsSsize_t(x);
+ Py_DECREF(x);
+ return ival;
+}
+
+static CYTHON_INLINE PyObject * __Pyx_PyInt_FromSize_t(size_t ival) {
+#if PY_VERSION_HEX < 0x02050000
+ if (ival <= LONG_MAX)
+ return PyInt_FromLong((long)ival);
+ else {
+ unsigned char *bytes = (unsigned char *) &ival;
+ int one = 1; int little = (int)*(unsigned char*)&one;
+ return _PyLong_FromByteArray(bytes, sizeof(size_t), little, 0);
+ }
+#else
+ return PyInt_FromSize_t(ival);
+#endif
+}
+
+static CYTHON_INLINE size_t __Pyx_PyInt_AsSize_t(PyObject* x) {
+ unsigned PY_LONG_LONG val = __Pyx_PyInt_AsUnsignedLongLong(x);
+ if (unlikely(val == (unsigned PY_LONG_LONG)-1 && PyErr_Occurred())) {
+ return (size_t)-1;
+ } else if (unlikely(val != (unsigned PY_LONG_LONG)(size_t)val)) {
+ PyErr_SetString(PyExc_OverflowError,
+ "value too large to convert to size_t");
+ return (size_t)-1;
+ }
+ return (size_t)val;
+}
+
+
+#endif /* Py_PYTHON_H */
diff --git a/twisted/test/raiser.pyx b/twisted/test/raiser.pyx
new file mode 100644
index 0000000..820540e
--- /dev/null
+++ b/twisted/test/raiser.pyx
@@ -0,0 +1,21 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A trivial extension that just raises an exception.
+See L{twisted.test.test_failure.test_failureConstructionWithMungedStackSucceeds}.
+"""
+
+
+
+class RaiserException(Exception):
+ """
+ A speficic exception only used to be identified in tests.
+ """
+
+
+def raiseException():
+ """
+ Raise L{RaiserException}.
+ """
+ raise RaiserException("This function is intentionally broken")
diff --git a/twisted/test/reflect_helper_IE.py b/twisted/test/reflect_helper_IE.py
new file mode 100644
index 0000000..614d948
--- /dev/null
+++ b/twisted/test/reflect_helper_IE.py
@@ -0,0 +1,4 @@
+
+# Helper for a test_reflect test
+
+import idonotexist
diff --git a/twisted/test/reflect_helper_VE.py b/twisted/test/reflect_helper_VE.py
new file mode 100644
index 0000000..e19507f
--- /dev/null
+++ b/twisted/test/reflect_helper_VE.py
@@ -0,0 +1,4 @@
+
+# Helper for a test_reflect test
+
+raise ValueError("Stuff is broken and things")
diff --git a/twisted/test/reflect_helper_ZDE.py b/twisted/test/reflect_helper_ZDE.py
new file mode 100644
index 0000000..bd05fbc
--- /dev/null
+++ b/twisted/test/reflect_helper_ZDE.py
@@ -0,0 +1,4 @@
+
+# Helper module for a test_reflect test
+
+1/0
diff --git a/twisted/test/server.pem b/twisted/test/server.pem
new file mode 100644
index 0000000..80ef9dc
--- /dev/null
+++ b/twisted/test/server.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzER
+MA8GA1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQD
+ExtNMkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5n
+cHNAcG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzEL
+MAkGA1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhv
+c3QxHTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEB
+BQADSwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
+5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQC
+MAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl
+MB0GA1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7
+hyNp65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoT
+CE0yQ3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlw
+dG8gQ2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3Qx
+LmNvbYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6
+BoJuVwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++
+7QGG/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JE
+WUQ9Ho4EzbYCOQ==
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIBPAIBAAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh
+5kwIzOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAQJBAIqm/bz4NA1H++Vx5Ewx
+OcKp3w19QSaZAwlGRtsUxrP7436QjnREM3Bm8ygU11BjkPVmtrKm6AayQfCHqJoT
+ZIECIQDW0BoMoL0HOYM/mrTLhaykYAVqgIeJsPjvkEhTFXWBuQIhAM3deFAvWNu4
+nklUQ37XsCT2c9tmNt1LAT+slG2JOTTRAiAuXDtC/m3NYVwyHfFm+zKHRzHkClk2
+HjubeEgjpj32AQIhAJqMGTaZVOwevTXvvHwNEH+vRWsAYU/gbx+OQB+7VOcBAiEA
+oolb6NMg/R3enNPvS1O4UU1H8wpaF77L4yiSWlE0p4w=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBDTCBuAIBADBTMQswCQYDVQQGEwJTRzERMA8GA1UEChMITTJDcnlwdG8xEjAQ
+BgNVBAMTCWxvY2FsaG9zdDEdMBsGCSqGSIb3DQEJARYObmdwc0Bwb3N0MS5jb20w
+XDANBgkqhkiG9w0BAQEFAANLADBIAkEArL57d26W9fNXvOhNlZzlPOACmvwOZ5Ad
+NgLzJ1/MfsQQJ7hHVeHmTAjM664V+fXvwUGJLziCeBo1ysWLRnl8CQIDAQABoAAw
+DQYJKoZIhvcNAQEEBQADQQA7uqbrNTjVWpF6By5ZNPvhZ4YdFgkeXFVWi5ao/TaP
+Vq4BG021fJ9nlHRtr4rotpgHDX1rr+iWeHKsx4+5DRSy
+-----END CERTIFICATE REQUEST-----
diff --git a/twisted/test/ssl_helpers.py b/twisted/test/ssl_helpers.py
new file mode 100644
index 0000000..5612b25
--- /dev/null
+++ b/twisted/test/ssl_helpers.py
@@ -0,0 +1,26 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.internet import ssl
+from twisted.python.util import sibpath
+
+from OpenSSL import SSL
+
+class ClientTLSContext(ssl.ClientContextFactory):
+ isClient = 1
+ def getContext(self):
+ return SSL.Context(SSL.TLSv1_METHOD)
+
+class ServerTLSContext:
+ isClient = 0
+
+ def __init__(self, filename = sibpath(__file__, 'server.pem')):
+ self.filename = filename
+
+ def getContext(self):
+ ctx = SSL.Context(SSL.TLSv1_METHOD)
+ ctx.use_certificate_file(self.filename)
+ ctx.use_privatekey_file(self.filename)
+ return ctx
diff --git a/twisted/test/stdio_test_consumer.py b/twisted/test/stdio_test_consumer.py
new file mode 100644
index 0000000..8254387
--- /dev/null
+++ b/twisted/test/stdio_test_consumer.py
@@ -0,0 +1,39 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_consumer -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_consumer} to test
+that process transports implement IConsumer properly.
+"""
+
+import sys, _preamble
+
+from twisted.python import log, reflect
+from twisted.internet import stdio, protocol
+from twisted.protocols import basic
+
+def failed(err):
+ log.startLogging(sys.stderr)
+ log.err(err)
+
+class ConsumerChild(protocol.Protocol):
+ def __init__(self, junkPath):
+ self.junkPath = junkPath
+
+ def connectionMade(self):
+ d = basic.FileSender().beginFileTransfer(file(self.junkPath), self.transport)
+ d.addErrback(failed)
+ d.addCallback(lambda ign: self.transport.loseConnection())
+
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ stdio.StandardIO(ConsumerChild(sys.argv[2]))
+ reactor.run()
diff --git a/twisted/test/stdio_test_halfclose.py b/twisted/test/stdio_test_halfclose.py
new file mode 100644
index 0000000..b80a8f9
--- /dev/null
+++ b/twisted/test/stdio_test_halfclose.py
@@ -0,0 +1,66 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_readConnectionLost -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_readConnectionLost}
+to test that IHalfCloseableProtocol.readConnectionLost works for process
+transports.
+"""
+
+import sys, _preamble
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IHalfCloseableProtocol
+from twisted.internet import stdio, protocol
+from twisted.python import reflect, log
+
+
+class HalfCloseProtocol(protocol.Protocol):
+ """
+ A protocol to hook up to stdio and observe its transport being
+ half-closed. If all goes as expected, C{exitCode} will be set to C{0};
+ otherwise it will be set to C{1} to indicate failure.
+ """
+ implements(IHalfCloseableProtocol)
+
+ exitCode = None
+
+ def connectionMade(self):
+ """
+ Signal the parent process that we're ready.
+ """
+ self.transport.write("x")
+
+
+ def readConnectionLost(self):
+ """
+ This is the desired event. Once it has happened, stop the reactor so
+ the process will exit.
+ """
+ self.exitCode = 0
+ reactor.stop()
+
+
+ def connectionLost(self, reason):
+ """
+ This may only be invoked after C{readConnectionLost}. If it happens
+ otherwise, mark it as an error and shut down.
+ """
+ if self.exitCode is None:
+ self.exitCode = 1
+ log.err(reason, "Unexpected call to connectionLost")
+ reactor.stop()
+
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ log.startLogging(file(sys.argv[2], 'w'))
+ from twisted.internet import reactor
+ protocol = HalfCloseProtocol()
+ stdio.StandardIO(protocol)
+ reactor.run()
+ sys.exit(protocol.exitCode)
diff --git a/twisted/test/stdio_test_hostpeer.py b/twisted/test/stdio_test_hostpeer.py
new file mode 100644
index 0000000..1e6f014
--- /dev/null
+++ b/twisted/test/stdio_test_hostpeer.py
@@ -0,0 +1,32 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_hostAndPeer -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_hostAndPeer} to test
+that ITransport.getHost() and ITransport.getPeer() work for process transports.
+"""
+
+import sys, _preamble
+
+from twisted.internet import stdio, protocol
+from twisted.python import reflect
+
+class HostPeerChild(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.write('\n'.join([
+ str(self.transport.getHost()),
+ str(self.transport.getPeer())]))
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ stdio.StandardIO(HostPeerChild())
+ reactor.run()
diff --git a/twisted/test/stdio_test_lastwrite.py b/twisted/test/stdio_test_lastwrite.py
new file mode 100644
index 0000000..2b70514
--- /dev/null
+++ b/twisted/test/stdio_test_lastwrite.py
@@ -0,0 +1,45 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_lastWriteReceived -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_lastWriteReceived}
+to test that L{os.write} can be reliably used after
+L{twisted.internet.stdio.StandardIO} has finished.
+"""
+
+import sys, _preamble
+
+from twisted.internet.protocol import Protocol
+from twisted.internet.stdio import StandardIO
+from twisted.python.reflect import namedAny
+
+
+class LastWriteChild(Protocol):
+ def __init__(self, reactor, magicString):
+ self.reactor = reactor
+ self.magicString = magicString
+
+
+ def connectionMade(self):
+ self.transport.write(self.magicString)
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ self.reactor.stop()
+
+
+
+def main(reactor, magicString):
+ p = LastWriteChild(reactor, magicString)
+ StandardIO(p)
+ reactor.run()
+
+
+
+if __name__ == '__main__':
+ namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ main(reactor, sys.argv[2])
diff --git a/twisted/test/stdio_test_loseconn.py b/twisted/test/stdio_test_loseconn.py
new file mode 100644
index 0000000..7f95a01
--- /dev/null
+++ b/twisted/test/stdio_test_loseconn.py
@@ -0,0 +1,48 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_loseConnection -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_loseConnection} to
+test that ITransport.loseConnection() works for process transports.
+"""
+
+import sys, _preamble
+
+from twisted.internet.error import ConnectionDone
+from twisted.internet import stdio, protocol
+from twisted.python import reflect, log
+
+class LoseConnChild(protocol.Protocol):
+ exitCode = 0
+
+ def connectionMade(self):
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ """
+ Check that C{reason} is a L{Failure} wrapping a L{ConnectionDone}
+ instance and stop the reactor. If C{reason} is wrong for some reason,
+ log something about that in C{self.errorLogFile} and make sure the
+ process exits with a non-zero status.
+ """
+ try:
+ try:
+ reason.trap(ConnectionDone)
+ except:
+ log.err(None, "Problem with reason passed to connectionLost")
+ self.exitCode = 1
+ finally:
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ log.startLogging(file(sys.argv[2], 'w'))
+ from twisted.internet import reactor
+ protocol = LoseConnChild()
+ stdio.StandardIO(protocol)
+ reactor.run()
+ sys.exit(protocol.exitCode)
diff --git a/twisted/test/stdio_test_producer.py b/twisted/test/stdio_test_producer.py
new file mode 100644
index 0000000..5c0b501
--- /dev/null
+++ b/twisted/test/stdio_test_producer.py
@@ -0,0 +1,55 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_producer -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_producer} to test
+that process transports implement IProducer properly.
+"""
+
+import sys, _preamble
+
+from twisted.internet import stdio, protocol
+from twisted.python import log, reflect
+
+class ProducerChild(protocol.Protocol):
+ _paused = False
+ buf = ''
+
+ def connectionLost(self, reason):
+ log.msg("*****OVER*****")
+ reactor.callLater(1, reactor.stop)
+ # reactor.stop()
+
+
+ def dataReceived(self, bytes):
+ self.buf += bytes
+ if self._paused:
+ log.startLogging(sys.stderr)
+ log.msg("dataReceived while transport paused!")
+ self.transport.loseConnection()
+ else:
+ self.transport.write(bytes)
+ if self.buf.endswith('\n0\n'):
+ self.transport.loseConnection()
+ else:
+ self.pause()
+
+
+ def pause(self):
+ self._paused = True
+ self.transport.pauseProducing()
+ reactor.callLater(0.01, self.unpause)
+
+
+ def unpause(self):
+ self._paused = False
+ self.transport.resumeProducing()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ stdio.StandardIO(ProducerChild())
+ reactor.run()
diff --git a/twisted/test/stdio_test_write.py b/twisted/test/stdio_test_write.py
new file mode 100644
index 0000000..9f92c94
--- /dev/null
+++ b/twisted/test/stdio_test_write.py
@@ -0,0 +1,31 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_write -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_write} to test that
+ITransport.write() works for process transports.
+"""
+
+import sys, _preamble
+
+from twisted.internet import stdio, protocol
+from twisted.python import reflect
+
+class WriteChild(protocol.Protocol):
+ def connectionMade(self):
+ for ch in 'ok!':
+ self.transport.write(ch)
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ stdio.StandardIO(WriteChild())
+ reactor.run()
diff --git a/twisted/test/stdio_test_writeseq.py b/twisted/test/stdio_test_writeseq.py
new file mode 100644
index 0000000..aeab716
--- /dev/null
+++ b/twisted/test/stdio_test_writeseq.py
@@ -0,0 +1,30 @@
+# -*- test-case-name: twisted.test.test_stdio.StandardInputOutputTestCase.test_writeSequence -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Main program for the child process run by
+L{twisted.test.test_stdio.StandardInputOutputTestCase.test_writeSequence} to test that
+ITransport.writeSequence() works for process transports.
+"""
+
+import sys, _preamble
+
+from twisted.internet import stdio, protocol
+from twisted.python import reflect
+
+class WriteSequenceChild(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.writeSequence(list('ok!'))
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+
+if __name__ == '__main__':
+ reflect.namedAny(sys.argv[1]).install()
+ from twisted.internet import reactor
+ stdio.StandardIO(WriteSequenceChild())
+ reactor.run()
diff --git a/twisted/test/test_abstract.py b/twisted/test/test_abstract.py
new file mode 100644
index 0000000..347e388
--- /dev/null
+++ b/twisted/test/test_abstract.py
@@ -0,0 +1,83 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for generic file descriptor based reactor support code.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.internet.abstract import isIPAddress
+
+
+class AddressTests(TestCase):
+ """
+ Tests for address-related functionality.
+ """
+ def test_decimalDotted(self):
+ """
+ L{isIPAddress} should return C{True} for any decimal dotted
+ representation of an IPv4 address.
+ """
+ self.assertTrue(isIPAddress('0.1.2.3'))
+ self.assertTrue(isIPAddress('252.253.254.255'))
+
+
+ def test_shortDecimalDotted(self):
+ """
+ L{isIPAddress} should return C{False} for a dotted decimal
+ representation with fewer or more than four octets.
+ """
+ self.assertFalse(isIPAddress('0'))
+ self.assertFalse(isIPAddress('0.1'))
+ self.assertFalse(isIPAddress('0.1.2'))
+ self.assertFalse(isIPAddress('0.1.2.3.4'))
+
+
+ def test_invalidLetters(self):
+ """
+ L{isIPAddress} should return C{False} for any non-decimal dotted
+ representation including letters.
+ """
+ self.assertFalse(isIPAddress('a.2.3.4'))
+ self.assertFalse(isIPAddress('1.b.3.4'))
+
+
+ def test_invalidPunctuation(self):
+ """
+ L{isIPAddress} should return C{False} for a string containing
+ strange punctuation.
+ """
+ self.assertFalse(isIPAddress(','))
+ self.assertFalse(isIPAddress('1,2'))
+ self.assertFalse(isIPAddress('1,2,3'))
+ self.assertFalse(isIPAddress('1.,.3,4'))
+
+
+ def test_emptyString(self):
+ """
+ L{isIPAddress} should return C{False} for the empty string.
+ """
+ self.assertFalse(isIPAddress(''))
+
+
+ def test_invalidNegative(self):
+ """
+ L{isIPAddress} should return C{False} for negative decimal values.
+ """
+ self.assertFalse(isIPAddress('-1'))
+ self.assertFalse(isIPAddress('1.-2'))
+ self.assertFalse(isIPAddress('1.2.-3'))
+ self.assertFalse(isIPAddress('1.2.-3.4'))
+
+
+ def test_invalidPositive(self):
+ """
+ L{isIPAddress} should return C{False} for a string containing
+ positive decimal values greater than 255.
+ """
+ self.assertFalse(isIPAddress('256.0.0.0'))
+ self.assertFalse(isIPAddress('0.256.0.0'))
+ self.assertFalse(isIPAddress('0.0.256.0'))
+ self.assertFalse(isIPAddress('0.0.0.256'))
+ self.assertFalse(isIPAddress('256.256.256.256'))
diff --git a/twisted/test/test_adbapi.py b/twisted/test/test_adbapi.py
new file mode 100644
index 0000000..92ff601
--- /dev/null
+++ b/twisted/test/test_adbapi.py
@@ -0,0 +1,819 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Tests for twisted.enterprise.adbapi.
+"""
+
+from twisted.trial import unittest
+
+import os, stat
+import types
+
+from twisted.enterprise.adbapi import ConnectionPool, ConnectionLost
+from twisted.enterprise.adbapi import Connection, Transaction
+from twisted.internet import reactor, defer, interfaces
+from twisted.python.failure import Failure
+
+
+simple_table_schema = """
+CREATE TABLE simple (
+ x integer
+)
+"""
+
+
+class ADBAPITestBase:
+ """Test the asynchronous DB-API code."""
+
+ openfun_called = {}
+
+ if interfaces.IReactorThreads(reactor, None) is None:
+ skip = "ADB-API requires threads, no way to test without them"
+
+ def extraSetUp(self):
+ """
+ Set up the database and create a connection pool pointing at it.
+ """
+ self.startDB()
+ self.dbpool = self.makePool(cp_openfun=self.openfun)
+ self.dbpool.start()
+
+
+ def tearDown(self):
+ d = self.dbpool.runOperation('DROP TABLE simple')
+ d.addCallback(lambda res: self.dbpool.close())
+ d.addCallback(lambda res: self.stopDB())
+ return d
+
+ def openfun(self, conn):
+ self.openfun_called[conn] = True
+
+ def checkOpenfunCalled(self, conn=None):
+ if not conn:
+ self.failUnless(self.openfun_called)
+ else:
+ self.failUnless(self.openfun_called.has_key(conn))
+
+ def testPool(self):
+ d = self.dbpool.runOperation(simple_table_schema)
+ if self.test_failures:
+ d.addCallback(self._testPool_1_1)
+ d.addCallback(self._testPool_1_2)
+ d.addCallback(self._testPool_1_3)
+ d.addCallback(self._testPool_1_4)
+ d.addCallback(lambda res: self.flushLoggedErrors())
+ d.addCallback(self._testPool_2)
+ d.addCallback(self._testPool_3)
+ d.addCallback(self._testPool_4)
+ d.addCallback(self._testPool_5)
+ d.addCallback(self._testPool_6)
+ d.addCallback(self._testPool_7)
+ d.addCallback(self._testPool_8)
+ d.addCallback(self._testPool_9)
+ return d
+
+ def _testPool_1_1(self, res):
+ d = defer.maybeDeferred(self.dbpool.runQuery, "select * from NOTABLE")
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: None)
+ return d
+
+ def _testPool_1_2(self, res):
+ d = defer.maybeDeferred(self.dbpool.runOperation,
+ "deletexxx from NOTABLE")
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: None)
+ return d
+
+ def _testPool_1_3(self, res):
+ d = defer.maybeDeferred(self.dbpool.runInteraction,
+ self.bad_interaction)
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: None)
+ return d
+
+ def _testPool_1_4(self, res):
+ d = defer.maybeDeferred(self.dbpool.runWithConnection,
+ self.bad_withConnection)
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: None)
+ return d
+
+ def _testPool_2(self, res):
+ # verify simple table is empty
+ sql = "select count(1) from simple"
+ d = self.dbpool.runQuery(sql)
+ def _check(row):
+ self.failUnless(int(row[0][0]) == 0, "Interaction not rolled back")
+ self.checkOpenfunCalled()
+ d.addCallback(_check)
+ return d
+
+ def _testPool_3(self, res):
+ sql = "select count(1) from simple"
+ inserts = []
+ # add some rows to simple table (runOperation)
+ for i in range(self.num_iterations):
+ sql = "insert into simple(x) values(%d)" % i
+ inserts.append(self.dbpool.runOperation(sql))
+ d = defer.gatherResults(inserts)
+
+ def _select(res):
+ # make sure they were added (runQuery)
+ sql = "select x from simple order by x";
+ d = self.dbpool.runQuery(sql)
+ return d
+ d.addCallback(_select)
+
+ def _check(rows):
+ self.failUnless(len(rows) == self.num_iterations,
+ "Wrong number of rows")
+ for i in range(self.num_iterations):
+ self.failUnless(len(rows[i]) == 1, "Wrong size row")
+ self.failUnless(rows[i][0] == i, "Values not returned.")
+ d.addCallback(_check)
+
+ return d
+
+ def _testPool_4(self, res):
+ # runInteraction
+ d = self.dbpool.runInteraction(self.interaction)
+ d.addCallback(lambda res: self.assertEqual(res, "done"))
+ return d
+
+ def _testPool_5(self, res):
+ # withConnection
+ d = self.dbpool.runWithConnection(self.withConnection)
+ d.addCallback(lambda res: self.assertEqual(res, "done"))
+ return d
+
+ def _testPool_6(self, res):
+ # Test a withConnection cannot be closed
+ d = self.dbpool.runWithConnection(self.close_withConnection)
+ return d
+
+ def _testPool_7(self, res):
+ # give the pool a workout
+ ds = []
+ for i in range(self.num_iterations):
+ sql = "select x from simple where x = %d" % i
+ ds.append(self.dbpool.runQuery(sql))
+ dlist = defer.DeferredList(ds, fireOnOneErrback=True)
+ def _check(result):
+ for i in range(self.num_iterations):
+ self.failUnless(result[i][1][0][0] == i, "Value not returned")
+ dlist.addCallback(_check)
+ return dlist
+
+ def _testPool_8(self, res):
+ # now delete everything
+ ds = []
+ for i in range(self.num_iterations):
+ sql = "delete from simple where x = %d" % i
+ ds.append(self.dbpool.runOperation(sql))
+ dlist = defer.DeferredList(ds, fireOnOneErrback=True)
+ return dlist
+
+ def _testPool_9(self, res):
+ # verify simple table is empty
+ sql = "select count(1) from simple"
+ d = self.dbpool.runQuery(sql)
+ def _check(row):
+ self.failUnless(int(row[0][0]) == 0,
+ "Didn't successfully delete table contents")
+ self.checkConnect()
+ d.addCallback(_check)
+ return d
+
+ def checkConnect(self):
+ """Check the connect/disconnect synchronous calls."""
+ conn = self.dbpool.connect()
+ self.checkOpenfunCalled(conn)
+ curs = conn.cursor()
+ curs.execute("insert into simple(x) values(1)")
+ curs.execute("select x from simple")
+ res = curs.fetchall()
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 1)
+ self.assertEqual(res[0][0], 1)
+ curs.execute("delete from simple")
+ curs.execute("select x from simple")
+ self.assertEqual(len(curs.fetchall()), 0)
+ curs.close()
+ self.dbpool.disconnect(conn)
+
+ def interaction(self, transaction):
+ transaction.execute("select x from simple order by x")
+ for i in range(self.num_iterations):
+ row = transaction.fetchone()
+ self.failUnless(len(row) == 1, "Wrong size row")
+ self.failUnless(row[0] == i, "Value not returned.")
+ # should test this, but gadfly throws an exception instead
+ #self.failUnless(transaction.fetchone() is None, "Too many rows")
+ return "done"
+
+ def bad_interaction(self, transaction):
+ if self.can_rollback:
+ transaction.execute("insert into simple(x) values(0)")
+
+ transaction.execute("select * from NOTABLE")
+
+ def withConnection(self, conn):
+ curs = conn.cursor()
+ try:
+ curs.execute("select x from simple order by x")
+ for i in range(self.num_iterations):
+ row = curs.fetchone()
+ self.failUnless(len(row) == 1, "Wrong size row")
+ self.failUnless(row[0] == i, "Value not returned.")
+ # should test this, but gadfly throws an exception instead
+ #self.failUnless(transaction.fetchone() is None, "Too many rows")
+ finally:
+ curs.close()
+ return "done"
+
+ def close_withConnection(self, conn):
+ conn.close()
+
+ def bad_withConnection(self, conn):
+ curs = conn.cursor()
+ try:
+ curs.execute("select * from NOTABLE")
+ finally:
+ curs.close()
+
+
+class ReconnectTestBase:
+ """Test the asynchronous DB-API code with reconnect."""
+
+ if interfaces.IReactorThreads(reactor, None) is None:
+ skip = "ADB-API requires threads, no way to test without them"
+
+ def extraSetUp(self):
+ """
+ Skip the test if C{good_sql} is unavailable. Otherwise, set up the
+ database, create a connection pool pointed at it, and set up a simple
+ schema in it.
+ """
+ if self.good_sql is None:
+ raise unittest.SkipTest('no good sql for reconnect test')
+ self.startDB()
+ self.dbpool = self.makePool(cp_max=1, cp_reconnect=True,
+ cp_good_sql=self.good_sql)
+ self.dbpool.start()
+ return self.dbpool.runOperation(simple_table_schema)
+
+
+ def tearDown(self):
+ d = self.dbpool.runOperation('DROP TABLE simple')
+ d.addCallback(lambda res: self.dbpool.close())
+ d.addCallback(lambda res: self.stopDB())
+ return d
+
+ def testPool(self):
+ d = defer.succeed(None)
+ d.addCallback(self._testPool_1)
+ d.addCallback(self._testPool_2)
+ if not self.early_reconnect:
+ d.addCallback(self._testPool_3)
+ d.addCallback(self._testPool_4)
+ d.addCallback(self._testPool_5)
+ return d
+
+ def _testPool_1(self, res):
+ sql = "select count(1) from simple"
+ d = self.dbpool.runQuery(sql)
+ def _check(row):
+ self.failUnless(int(row[0][0]) == 0, "Table not empty")
+ d.addCallback(_check)
+ return d
+
+ def _testPool_2(self, res):
+ # reach in and close the connection manually
+ self.dbpool.connections.values()[0].close()
+
+ def _testPool_3(self, res):
+ sql = "select count(1) from simple"
+ d = defer.maybeDeferred(self.dbpool.runQuery, sql)
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: None)
+ return d
+
+ def _testPool_4(self, res):
+ sql = "select count(1) from simple"
+ d = self.dbpool.runQuery(sql)
+ def _check(row):
+ self.failUnless(int(row[0][0]) == 0, "Table not empty")
+ d.addCallback(_check)
+ return d
+
+ def _testPool_5(self, res):
+ self.flushLoggedErrors()
+ sql = "select * from NOTABLE" # bad sql
+ d = defer.maybeDeferred(self.dbpool.runQuery, sql)
+ d.addCallbacks(lambda res: self.fail('no exception'),
+ lambda f: self.failIf(f.check(ConnectionLost)))
+ return d
+
+
+class DBTestConnector:
+ """A class which knows how to test for the presence of
+ and establish a connection to a relational database.
+
+ To enable test cases which use a central, system database,
+ you must create a database named DB_NAME with a user DB_USER
+ and password DB_PASS with full access rights to database DB_NAME.
+ """
+
+ TEST_PREFIX = None # used for creating new test cases
+
+ DB_NAME = "twisted_test"
+ DB_USER = 'twisted_test'
+ DB_PASS = 'twisted_test'
+
+ DB_DIR = None # directory for database storage
+
+ nulls_ok = True # nulls supported
+ trailing_spaces_ok = True # trailing spaces in strings preserved
+ can_rollback = True # rollback supported
+ test_failures = True # test bad sql?
+ escape_slashes = True # escape \ in sql?
+ good_sql = ConnectionPool.good_sql
+ early_reconnect = True # cursor() will fail on closed connection
+ can_clear = True # can try to clear out tables when starting
+
+ num_iterations = 50 # number of iterations for test loops
+ # (lower this for slow db's)
+
+ def setUp(self):
+ self.DB_DIR = self.mktemp()
+ os.mkdir(self.DB_DIR)
+ if not self.can_connect():
+ raise unittest.SkipTest('%s: Cannot access db' % self.TEST_PREFIX)
+ return self.extraSetUp()
+
+ def can_connect(self):
+ """Return true if this database is present on the system
+ and can be used in a test."""
+ raise NotImplementedError()
+
+ def startDB(self):
+ """Take any steps needed to bring database up."""
+ pass
+
+ def stopDB(self):
+ """Bring database down, if needed."""
+ pass
+
+ def makePool(self, **newkw):
+ """Create a connection pool with additional keyword arguments."""
+ args, kw = self.getPoolArgs()
+ kw = kw.copy()
+ kw.update(newkw)
+ return ConnectionPool(*args, **kw)
+
+ def getPoolArgs(self):
+ """Return a tuple (args, kw) of list and keyword arguments
+ that need to be passed to ConnectionPool to create a connection
+ to this database."""
+ raise NotImplementedError()
+
+class GadflyConnector(DBTestConnector):
+ TEST_PREFIX = 'Gadfly'
+
+ nulls_ok = False
+ can_rollback = False
+ escape_slashes = False
+ good_sql = 'select * from simple where 1=0'
+
+ num_iterations = 1 # slow
+
+ def can_connect(self):
+ try: import gadfly
+ except: return False
+ if not getattr(gadfly, 'connect', None):
+ gadfly.connect = gadfly.gadfly
+ return True
+
+ def startDB(self):
+ import gadfly
+ conn = gadfly.gadfly()
+ conn.startup(self.DB_NAME, self.DB_DIR)
+
+ # gadfly seems to want us to create something to get the db going
+ cursor = conn.cursor()
+ cursor.execute("create table x (x integer)")
+ conn.commit()
+ conn.close()
+
+ def getPoolArgs(self):
+ args = ('gadfly', self.DB_NAME, self.DB_DIR)
+ kw = {'cp_max': 1}
+ return args, kw
+
+class SQLiteConnector(DBTestConnector):
+ TEST_PREFIX = 'SQLite'
+
+ escape_slashes = False
+
+ num_iterations = 1 # slow
+
+ def can_connect(self):
+ try: import sqlite
+ except: return False
+ return True
+
+ def startDB(self):
+ self.database = os.path.join(self.DB_DIR, self.DB_NAME)
+ if os.path.exists(self.database):
+ os.unlink(self.database)
+
+ def getPoolArgs(self):
+ args = ('sqlite',)
+ kw = {'database': self.database, 'cp_max': 1}
+ return args, kw
+
+class PyPgSQLConnector(DBTestConnector):
+ TEST_PREFIX = "PyPgSQL"
+
+ def can_connect(self):
+ try: from pyPgSQL import PgSQL
+ except: return False
+ try:
+ conn = PgSQL.connect(database=self.DB_NAME, user=self.DB_USER,
+ password=self.DB_PASS)
+ conn.close()
+ return True
+ except:
+ return False
+
+ def getPoolArgs(self):
+ args = ('pyPgSQL.PgSQL',)
+ kw = {'database': self.DB_NAME, 'user': self.DB_USER,
+ 'password': self.DB_PASS, 'cp_min': 0}
+ return args, kw
+
+class PsycopgConnector(DBTestConnector):
+ TEST_PREFIX = 'Psycopg'
+
+ def can_connect(self):
+ try: import psycopg
+ except: return False
+ try:
+ conn = psycopg.connect(database=self.DB_NAME, user=self.DB_USER,
+ password=self.DB_PASS)
+ conn.close()
+ return True
+ except:
+ return False
+
+ def getPoolArgs(self):
+ args = ('psycopg',)
+ kw = {'database': self.DB_NAME, 'user': self.DB_USER,
+ 'password': self.DB_PASS, 'cp_min': 0}
+ return args, kw
+
+class MySQLConnector(DBTestConnector):
+ TEST_PREFIX = 'MySQL'
+
+ trailing_spaces_ok = False
+ can_rollback = False
+ early_reconnect = False
+
+ def can_connect(self):
+ try: import MySQLdb
+ except: return False
+ try:
+ conn = MySQLdb.connect(db=self.DB_NAME, user=self.DB_USER,
+ passwd=self.DB_PASS)
+ conn.close()
+ return True
+ except:
+ return False
+
+ def getPoolArgs(self):
+ args = ('MySQLdb',)
+ kw = {'db': self.DB_NAME, 'user': self.DB_USER, 'passwd': self.DB_PASS}
+ return args, kw
+
+class FirebirdConnector(DBTestConnector):
+ TEST_PREFIX = 'Firebird'
+
+ test_failures = False # failure testing causes problems
+ escape_slashes = False
+ good_sql = None # firebird doesn't handle failed sql well
+ can_clear = False # firebird is not so good
+
+ num_iterations = 5 # slow
+
+ def can_connect(self):
+ try: import kinterbasdb
+ except: return False
+ try:
+ self.startDB()
+ self.stopDB()
+ return True
+ except:
+ return False
+
+
+ def startDB(self):
+ import kinterbasdb
+ self.DB_NAME = os.path.join(self.DB_DIR, DBTestConnector.DB_NAME)
+ os.chmod(self.DB_DIR, stat.S_IRWXU + stat.S_IRWXG + stat.S_IRWXO)
+ sql = 'create database "%s" user "%s" password "%s"'
+ sql %= (self.DB_NAME, self.DB_USER, self.DB_PASS);
+ conn = kinterbasdb.create_database(sql)
+ conn.close()
+
+
+ def getPoolArgs(self):
+ args = ('kinterbasdb',)
+ kw = {'database': self.DB_NAME, 'host': '127.0.0.1',
+ 'user': self.DB_USER, 'password': self.DB_PASS}
+ return args, kw
+
+ def stopDB(self):
+ import kinterbasdb
+ conn = kinterbasdb.connect(database=self.DB_NAME,
+ host='127.0.0.1', user=self.DB_USER,
+ password=self.DB_PASS)
+ conn.drop_database()
+
+def makeSQLTests(base, suffix, globals):
+ """
+ Make a test case for every db connector which can connect.
+
+ @param base: Base class for test case. Additional base classes
+ will be a DBConnector subclass and unittest.TestCase
+ @param suffix: A suffix used to create test case names. Prefixes
+ are defined in the DBConnector subclasses.
+ """
+ connectors = [GadflyConnector, SQLiteConnector, PyPgSQLConnector,
+ PsycopgConnector, MySQLConnector, FirebirdConnector]
+ for connclass in connectors:
+ name = connclass.TEST_PREFIX + suffix
+ klass = types.ClassType(name, (connclass, base, unittest.TestCase),
+ base.__dict__)
+ globals[name] = klass
+
+# GadflyADBAPITestCase SQLiteADBAPITestCase PyPgSQLADBAPITestCase
+# PsycopgADBAPITestCase MySQLADBAPITestCase FirebirdADBAPITestCase
+makeSQLTests(ADBAPITestBase, 'ADBAPITestCase', globals())
+
+# GadflyReconnectTestCase SQLiteReconnectTestCase PyPgSQLReconnectTestCase
+# PsycopgReconnectTestCase MySQLReconnectTestCase FirebirdReconnectTestCase
+makeSQLTests(ReconnectTestBase, 'ReconnectTestCase', globals())
+
+
+
+class FakePool(object):
+ """
+ A fake L{ConnectionPool} for tests.
+
+ @ivar connectionFactory: factory for making connections returned by the
+ C{connect} method.
+ @type connectionFactory: any callable
+ """
+ reconnect = True
+ noisy = True
+
+ def __init__(self, connectionFactory):
+ self.connectionFactory = connectionFactory
+
+
+ def connect(self):
+ """
+ Return an instance of C{self.connectionFactory}.
+ """
+ return self.connectionFactory()
+
+
+ def disconnect(self, connection):
+ """
+ Do nothing.
+ """
+
+
+
+class ConnectionTestCase(unittest.TestCase):
+ """
+ Tests for the L{Connection} class.
+ """
+
+ def test_rollbackErrorLogged(self):
+ """
+ If an error happens during rollback, L{ConnectionLost} is raised but
+ the original error is logged.
+ """
+ class ConnectionRollbackRaise(object):
+ def rollback(self):
+ raise RuntimeError("problem!")
+
+ pool = FakePool(ConnectionRollbackRaise)
+ connection = Connection(pool)
+ self.assertRaises(ConnectionLost, connection.rollback)
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.args[0], "problem!")
+
+
+
+class TransactionTestCase(unittest.TestCase):
+ """
+ Tests for the L{Transaction} class.
+ """
+
+ def test_reopenLogErrorIfReconnect(self):
+ """
+ If the cursor creation raises an error in L{Transaction.reopen}, it
+ reconnects but log the error occurred.
+ """
+ class ConnectionCursorRaise(object):
+ count = 0
+
+ def reconnect(self):
+ pass
+
+ def cursor(self):
+ if self.count == 0:
+ self.count += 1
+ raise RuntimeError("problem!")
+
+ pool = FakePool(None)
+ transaction = Transaction(pool, ConnectionCursorRaise())
+ transaction.reopen()
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.args[0], "problem!")
+
+
+
+class NonThreadPool(object):
+ def callInThreadWithCallback(self, onResult, f, *a, **kw):
+ success = True
+ try:
+ result = f(*a, **kw)
+ except Exception, e:
+ success = False
+ result = Failure()
+ onResult(success, result)
+
+
+
+class DummyConnectionPool(ConnectionPool):
+ """
+ A testable L{ConnectionPool};
+ """
+ threadpool = NonThreadPool()
+
+ def __init__(self):
+ """
+ Don't forward init call.
+ """
+ self.reactor = reactor
+
+
+
+class EventReactor(object):
+ """
+ Partial L{IReactorCore} implementation with simple event-related
+ methods.
+
+ @ivar _running: A C{bool} indicating whether the reactor is pretending
+ to have been started already or not.
+
+ @ivar triggers: A C{list} of pending system event triggers.
+ """
+ def __init__(self, running):
+ self._running = running
+ self.triggers = []
+
+
+ def callWhenRunning(self, function):
+ if self._running:
+ function()
+ else:
+ return self.addSystemEventTrigger('after', 'startup', function)
+
+
+ def addSystemEventTrigger(self, phase, event, trigger):
+ handle = (phase, event, trigger)
+ self.triggers.append(handle)
+ return handle
+
+
+ def removeSystemEventTrigger(self, handle):
+ self.triggers.remove(handle)
+
+
+
+class ConnectionPoolTestCase(unittest.TestCase):
+ """
+ Unit tests for L{ConnectionPool}.
+ """
+
+ def test_runWithConnectionRaiseOriginalError(self):
+ """
+ If rollback fails, L{ConnectionPool.runWithConnection} raises the
+ original exception and log the error of the rollback.
+ """
+ class ConnectionRollbackRaise(object):
+ def __init__(self, pool):
+ pass
+
+ def rollback(self):
+ raise RuntimeError("problem!")
+
+ def raisingFunction(connection):
+ raise ValueError("foo")
+
+ pool = DummyConnectionPool()
+ pool.connectionFactory = ConnectionRollbackRaise
+ d = pool.runWithConnection(raisingFunction)
+ d = self.assertFailure(d, ValueError)
+ def cbFailed(ignored):
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.args[0], "problem!")
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_closeLogError(self):
+ """
+ L{ConnectionPool._close} logs exceptions.
+ """
+ class ConnectionCloseRaise(object):
+ def close(self):
+ raise RuntimeError("problem!")
+
+ pool = DummyConnectionPool()
+ pool._close(ConnectionCloseRaise())
+
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.args[0], "problem!")
+
+
+ def test_runWithInteractionRaiseOriginalError(self):
+ """
+ If rollback fails, L{ConnectionPool.runInteraction} raises the
+ original exception and log the error of the rollback.
+ """
+ class ConnectionRollbackRaise(object):
+ def __init__(self, pool):
+ pass
+
+ def rollback(self):
+ raise RuntimeError("problem!")
+
+ class DummyTransaction(object):
+ def __init__(self, pool, connection):
+ pass
+
+ def raisingFunction(transaction):
+ raise ValueError("foo")
+
+ pool = DummyConnectionPool()
+ pool.connectionFactory = ConnectionRollbackRaise
+ pool.transactionFactory = DummyTransaction
+
+ d = pool.runInteraction(raisingFunction)
+ d = self.assertFailure(d, ValueError)
+ def cbFailed(ignored):
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.args[0], "problem!")
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_unstartedClose(self):
+ """
+ If L{ConnectionPool.close} is called without L{ConnectionPool.start}
+ having been called, the pool's startup event is cancelled.
+ """
+ reactor = EventReactor(False)
+ pool = ConnectionPool('twisted.test.test_adbapi', cp_reactor=reactor)
+ # There should be a startup trigger waiting.
+ self.assertEqual(reactor.triggers, [('after', 'startup', pool._start)])
+ pool.close()
+ # But not anymore.
+ self.assertFalse(reactor.triggers)
+
+
+ def test_startedClose(self):
+ """
+ If L{ConnectionPool.close} is called after it has been started, but
+ not by its shutdown trigger, the shutdown trigger is cancelled.
+ """
+ reactor = EventReactor(True)
+ pool = ConnectionPool('twisted.test.test_adbapi', cp_reactor=reactor)
+ # There should be a shutdown trigger waiting.
+ self.assertEqual(reactor.triggers, [('during', 'shutdown', pool.finalClose)])
+ pool.close()
+ # But not anymore.
+ self.assertFalse(reactor.triggers)
diff --git a/twisted/test/test_amp.py b/twisted/test/test_amp.py
new file mode 100644
index 0000000..1f4c369
--- /dev/null
+++ b/twisted/test/test_amp.py
@@ -0,0 +1,3178 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.protocols.amp}.
+"""
+
+import datetime
+import decimal
+
+from zope.interface import implements
+from zope.interface.verify import verifyClass, verifyObject
+
+from twisted.python.util import setIDFunction
+from twisted.python import filepath
+from twisted.python.failure import Failure
+from twisted.protocols import amp
+from twisted.trial import unittest
+from twisted.internet import protocol, defer, error, reactor, interfaces
+from twisted.test import iosim
+from twisted.test.proto_helpers import StringTransport
+
+ssl = None
+try:
+ from twisted.internet import ssl
+except ImportError:
+ pass
+
+if ssl and not ssl.supported:
+ ssl = None
+
+if ssl is None:
+ skipSSL = "SSL not available"
+else:
+ skipSSL = None
+
+
+class TestProto(protocol.Protocol):
+ """
+ A trivial protocol for use in testing where a L{Protocol} is expected.
+
+ @ivar instanceId: the id of this instance
+ @ivar onConnLost: deferred that will fired when the connection is lost
+ @ivar dataToSend: data to send on the protocol
+ """
+
+ instanceCount = 0
+
+ def __init__(self, onConnLost, dataToSend):
+ self.onConnLost = onConnLost
+ self.dataToSend = dataToSend
+ self.instanceId = TestProto.instanceCount
+ TestProto.instanceCount = TestProto.instanceCount + 1
+
+
+ def connectionMade(self):
+ self.data = []
+ self.transport.write(self.dataToSend)
+
+
+ def dataReceived(self, bytes):
+ self.data.append(bytes)
+
+
+ def connectionLost(self, reason):
+ self.onConnLost.callback(self.data)
+
+
+ def __repr__(self):
+ """
+ Custom repr for testing to avoid coupling amp tests with repr from
+ L{Protocol}
+
+ Returns a string which contains a unique identifier that can be looked
+ up using the instanceId property::
+
+ <TestProto #3>
+ """
+ return "<TestProto #%d>" % (self.instanceId,)
+
+
+
+class SimpleSymmetricProtocol(amp.AMP):
+
+ def sendHello(self, text):
+ return self.callRemoteString(
+ "hello",
+ hello=text)
+
+ def amp_HELLO(self, box):
+ return amp.Box(hello=box['hello'])
+
+ def amp_HOWDOYOUDO(self, box):
+ return amp.QuitBox(howdoyoudo='world')
+
+
+
+class UnfriendlyGreeting(Exception):
+ """Greeting was insufficiently kind.
+ """
+
+class DeathThreat(Exception):
+ """Greeting was insufficiently kind.
+ """
+
+class UnknownProtocol(Exception):
+ """Asked to switch to the wrong protocol.
+ """
+
+
+class TransportPeer(amp.Argument):
+ # this serves as some informal documentation for how to get variables from
+ # the protocol or your environment and pass them to methods as arguments.
+ def retrieve(self, d, name, proto):
+ return ''
+
+ def fromStringProto(self, notAString, proto):
+ return proto.transport.getPeer()
+
+ def toBox(self, name, strings, objects, proto):
+ return
+
+
+
+class Hello(amp.Command):
+
+ commandName = 'hello'
+
+ arguments = [('hello', amp.String()),
+ ('optional', amp.Boolean(optional=True)),
+ ('print', amp.Unicode(optional=True)),
+ ('from', TransportPeer(optional=True)),
+ ('mixedCase', amp.String(optional=True)),
+ ('dash-arg', amp.String(optional=True)),
+ ('underscore_arg', amp.String(optional=True))]
+
+ response = [('hello', amp.String()),
+ ('print', amp.Unicode(optional=True))]
+
+ errors = {UnfriendlyGreeting: 'UNFRIENDLY'}
+
+ fatalErrors = {DeathThreat: 'DEAD'}
+
+class NoAnswerHello(Hello):
+ commandName = Hello.commandName
+ requiresAnswer = False
+
+class FutureHello(amp.Command):
+ commandName = 'hello'
+
+ arguments = [('hello', amp.String()),
+ ('optional', amp.Boolean(optional=True)),
+ ('print', amp.Unicode(optional=True)),
+ ('from', TransportPeer(optional=True)),
+ ('bonus', amp.String(optional=True)), # addt'l arguments
+ # should generally be
+ # added at the end, and
+ # be optional...
+ ]
+
+ response = [('hello', amp.String()),
+ ('print', amp.Unicode(optional=True))]
+
+ errors = {UnfriendlyGreeting: 'UNFRIENDLY'}
+
+class WTF(amp.Command):
+ """
+ An example of an invalid command.
+ """
+
+
+class BrokenReturn(amp.Command):
+ """ An example of a perfectly good command, but the handler is going to return
+ None...
+ """
+
+ commandName = 'broken_return'
+
+class Goodbye(amp.Command):
+ # commandName left blank on purpose: this tests implicit command names.
+ response = [('goodbye', amp.String())]
+ responseType = amp.QuitBox
+
+class Howdoyoudo(amp.Command):
+ commandName = 'howdoyoudo'
+ # responseType = amp.QuitBox
+
+class WaitForever(amp.Command):
+ commandName = 'wait_forever'
+
+class GetList(amp.Command):
+ commandName = 'getlist'
+ arguments = [('length', amp.Integer())]
+ response = [('body', amp.AmpList([('x', amp.Integer())]))]
+
+class DontRejectMe(amp.Command):
+ commandName = 'dontrejectme'
+ arguments = [
+ ('magicWord', amp.Unicode()),
+ ('list', amp.AmpList([('name', amp.Unicode())], optional=True)),
+ ]
+ response = [('response', amp.Unicode())]
+
+class SecuredPing(amp.Command):
+ # XXX TODO: actually make this refuse to send over an insecure connection
+ response = [('pinged', amp.Boolean())]
+
+class TestSwitchProto(amp.ProtocolSwitchCommand):
+ commandName = 'Switch-Proto'
+
+ arguments = [
+ ('name', amp.String()),
+ ]
+ errors = {UnknownProtocol: 'UNKNOWN'}
+
+class SingleUseFactory(protocol.ClientFactory):
+ def __init__(self, proto):
+ self.proto = proto
+ self.proto.factory = self
+
+ def buildProtocol(self, addr):
+ p, self.proto = self.proto, None
+ return p
+
+ reasonFailed = None
+
+ def clientConnectionFailed(self, connector, reason):
+ self.reasonFailed = reason
+ return
+
+THING_I_DONT_UNDERSTAND = 'gwebol nargo'
+class ThingIDontUnderstandError(Exception):
+ pass
+
+class FactoryNotifier(amp.AMP):
+ factory = None
+ def connectionMade(self):
+ if self.factory is not None:
+ self.factory.theProto = self
+ if hasattr(self.factory, 'onMade'):
+ self.factory.onMade.callback(None)
+
+ def emitpong(self):
+ from twisted.internet.interfaces import ISSLTransport
+ if not ISSLTransport.providedBy(self.transport):
+ raise DeathThreat("only send secure pings over secure channels")
+ return {'pinged': True}
+ SecuredPing.responder(emitpong)
+
+
+class SimpleSymmetricCommandProtocol(FactoryNotifier):
+ maybeLater = None
+ def __init__(self, onConnLost=None):
+ amp.AMP.__init__(self)
+ self.onConnLost = onConnLost
+
+ def sendHello(self, text):
+ return self.callRemote(Hello, hello=text)
+
+ def sendUnicodeHello(self, text, translation):
+ return self.callRemote(Hello, hello=text, Print=translation)
+
+ greeted = False
+
+ def cmdHello(self, hello, From, optional=None, Print=None,
+ mixedCase=None, dash_arg=None, underscore_arg=None):
+ assert From == self.transport.getPeer()
+ if hello == THING_I_DONT_UNDERSTAND:
+ raise ThingIDontUnderstandError()
+ if hello.startswith('fuck'):
+ raise UnfriendlyGreeting("Don't be a dick.")
+ if hello == 'die':
+ raise DeathThreat("aieeeeeeeee")
+ result = dict(hello=hello)
+ if Print is not None:
+ result.update(dict(Print=Print))
+ self.greeted = True
+ return result
+ Hello.responder(cmdHello)
+
+ def cmdGetlist(self, length):
+ return {'body': [dict(x=1)] * length}
+ GetList.responder(cmdGetlist)
+
+ def okiwont(self, magicWord, list=None):
+ if list is None:
+ response = u'list omitted'
+ else:
+ response = u'%s accepted' % (list[0]['name'])
+ return dict(response=response)
+ DontRejectMe.responder(okiwont)
+
+ def waitforit(self):
+ self.waiting = defer.Deferred()
+ return self.waiting
+ WaitForever.responder(waitforit)
+
+ def howdo(self):
+ return dict(howdoyoudo='world')
+ Howdoyoudo.responder(howdo)
+
+ def saybye(self):
+ return dict(goodbye="everyone")
+ Goodbye.responder(saybye)
+
+ def switchToTestProtocol(self, fail=False):
+ if fail:
+ name = 'no-proto'
+ else:
+ name = 'test-proto'
+ p = TestProto(self.onConnLost, SWITCH_CLIENT_DATA)
+ return self.callRemote(
+ TestSwitchProto,
+ SingleUseFactory(p), name=name).addCallback(lambda ign: p)
+
+ def switchit(self, name):
+ if name == 'test-proto':
+ return TestProto(self.onConnLost, SWITCH_SERVER_DATA)
+ raise UnknownProtocol(name)
+ TestSwitchProto.responder(switchit)
+
+ def donothing(self):
+ return None
+ BrokenReturn.responder(donothing)
+
+
+class DeferredSymmetricCommandProtocol(SimpleSymmetricCommandProtocol):
+ def switchit(self, name):
+ if name == 'test-proto':
+ self.maybeLaterProto = TestProto(self.onConnLost, SWITCH_SERVER_DATA)
+ self.maybeLater = defer.Deferred()
+ return self.maybeLater
+ raise UnknownProtocol(name)
+ TestSwitchProto.responder(switchit)
+
+class BadNoAnswerCommandProtocol(SimpleSymmetricCommandProtocol):
+ def badResponder(self, hello, From, optional=None, Print=None,
+ mixedCase=None, dash_arg=None, underscore_arg=None):
+ """
+ This responder does nothing and forgets to return a dictionary.
+ """
+ NoAnswerHello.responder(badResponder)
+
+class NoAnswerCommandProtocol(SimpleSymmetricCommandProtocol):
+ def goodNoAnswerResponder(self, hello, From, optional=None, Print=None,
+ mixedCase=None, dash_arg=None, underscore_arg=None):
+ return dict(hello=hello+"-noanswer")
+ NoAnswerHello.responder(goodNoAnswerResponder)
+
+def connectedServerAndClient(ServerClass=SimpleSymmetricProtocol,
+ ClientClass=SimpleSymmetricProtocol,
+ *a, **kw):
+ """Returns a 3-tuple: (client, server, pump)
+ """
+ return iosim.connectedServerAndClient(
+ ServerClass, ClientClass,
+ *a, **kw)
+
+class TotallyDumbProtocol(protocol.Protocol):
+ buf = ''
+ def dataReceived(self, data):
+ self.buf += data
+
+class LiteralAmp(amp.AMP):
+ def __init__(self):
+ self.boxes = []
+
+ def ampBoxReceived(self, box):
+ self.boxes.append(box)
+ return
+
+
+
+class AmpBoxTests(unittest.TestCase):
+ """
+ Test a few essential properties of AMP boxes, mostly with respect to
+ serialization correctness.
+ """
+
+ def test_serializeStr(self):
+ """
+ Make sure that strs serialize to strs.
+ """
+ a = amp.AmpBox(key='value')
+ self.assertEqual(type(a.serialize()), str)
+
+ def test_serializeUnicodeKeyRaises(self):
+ """
+ Verify that TypeError is raised when trying to serialize Unicode keys.
+ """
+ a = amp.AmpBox(**{u'key': 'value'})
+ self.assertRaises(TypeError, a.serialize)
+
+ def test_serializeUnicodeValueRaises(self):
+ """
+ Verify that TypeError is raised when trying to serialize Unicode
+ values.
+ """
+ a = amp.AmpBox(key=u'value')
+ self.assertRaises(TypeError, a.serialize)
+
+
+
+class ParsingTest(unittest.TestCase):
+
+ def test_booleanValues(self):
+ """
+ Verify that the Boolean parser parses 'True' and 'False', but nothing
+ else.
+ """
+ b = amp.Boolean()
+ self.assertEqual(b.fromString("True"), True)
+ self.assertEqual(b.fromString("False"), False)
+ self.assertRaises(TypeError, b.fromString, "ninja")
+ self.assertRaises(TypeError, b.fromString, "true")
+ self.assertRaises(TypeError, b.fromString, "TRUE")
+ self.assertEqual(b.toString(True), 'True')
+ self.assertEqual(b.toString(False), 'False')
+
+ def test_pathValueRoundTrip(self):
+ """
+ Verify the 'Path' argument can parse and emit a file path.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ p = amp.Path()
+ s = p.toString(fp)
+ v = p.fromString(s)
+ self.assertNotIdentical(fp, v) # sanity check
+ self.assertEqual(fp, v)
+
+
+ def test_sillyEmptyThing(self):
+ """
+ Test that empty boxes raise an error; they aren't supposed to be sent
+ on purpose.
+ """
+ a = amp.AMP()
+ return self.assertRaises(amp.NoEmptyBoxes, a.ampBoxReceived, amp.Box())
+
+
+ def test_ParsingRoundTrip(self):
+ """
+ Verify that various kinds of data make it through the encode/parse
+ round-trip unharmed.
+ """
+ c, s, p = connectedServerAndClient(ClientClass=LiteralAmp,
+ ServerClass=LiteralAmp)
+
+ SIMPLE = ('simple', 'test')
+ CE = ('ceq', ': ')
+ CR = ('crtest', 'test\r')
+ LF = ('lftest', 'hello\n')
+ NEWLINE = ('newline', 'test\r\none\r\ntwo')
+ NEWLINE2 = ('newline2', 'test\r\none\r\n two')
+ BODYTEST = ('body', 'blah\r\n\r\ntesttest')
+
+ testData = [
+ [SIMPLE],
+ [SIMPLE, BODYTEST],
+ [SIMPLE, CE],
+ [SIMPLE, CR],
+ [SIMPLE, CE, CR, LF],
+ [CE, CR, LF],
+ [SIMPLE, NEWLINE, CE, NEWLINE2],
+ [BODYTEST, SIMPLE, NEWLINE]
+ ]
+
+ for test in testData:
+ jb = amp.Box()
+ jb.update(dict(test))
+ jb._sendTo(c)
+ p.flush()
+ self.assertEqual(s.boxes[-1], jb)
+
+
+
+class FakeLocator(object):
+ """
+ This is a fake implementation of the interface implied by
+ L{CommandLocator}.
+ """
+ def __init__(self):
+ """
+ Remember the given keyword arguments as a set of responders.
+ """
+ self.commands = {}
+
+
+ def locateResponder(self, commandName):
+ """
+ Look up and return a function passed as a keyword argument of the given
+ name to the constructor.
+ """
+ return self.commands[commandName]
+
+
+class FakeSender:
+ """
+ This is a fake implementation of the 'box sender' interface implied by
+ L{AMP}.
+ """
+ def __init__(self):
+ """
+ Create a fake sender and initialize the list of received boxes and
+ unhandled errors.
+ """
+ self.sentBoxes = []
+ self.unhandledErrors = []
+ self.expectedErrors = 0
+
+
+ def expectError(self):
+ """
+ Expect one error, so that the test doesn't fail.
+ """
+ self.expectedErrors += 1
+
+
+ def sendBox(self, box):
+ """
+ Accept a box, but don't do anything.
+ """
+ self.sentBoxes.append(box)
+
+
+ def unhandledError(self, failure):
+ """
+ Deal with failures by instantly re-raising them for easier debugging.
+ """
+ self.expectedErrors -= 1
+ if self.expectedErrors < 0:
+ failure.raiseException()
+ else:
+ self.unhandledErrors.append(failure)
+
+
+
+class CommandDispatchTests(unittest.TestCase):
+ """
+ The AMP CommandDispatcher class dispatches converts AMP boxes into commands
+ and responses using Command.responder decorator.
+
+ Note: Originally, AMP's factoring was such that many tests for this
+ functionality are now implemented as full round-trip tests in L{AMPTest}.
+ Future tests should be written at this level instead, to ensure API
+ compatibility and to provide more granular, readable units of test
+ coverage.
+ """
+
+ def setUp(self):
+ """
+ Create a dispatcher to use.
+ """
+ self.locator = FakeLocator()
+ self.sender = FakeSender()
+ self.dispatcher = amp.BoxDispatcher(self.locator)
+ self.dispatcher.startReceivingBoxes(self.sender)
+
+
+ def test_receivedAsk(self):
+ """
+ L{CommandDispatcher.ampBoxReceived} should locate the appropriate
+ command in its responder lookup, based on the '_ask' key.
+ """
+ received = []
+ def thunk(box):
+ received.append(box)
+ return amp.Box({"hello": "goodbye"})
+ input = amp.Box(_command="hello",
+ _ask="test-command-id",
+ hello="world")
+ self.locator.commands['hello'] = thunk
+ self.dispatcher.ampBoxReceived(input)
+ self.assertEqual(received, [input])
+
+
+ def test_sendUnhandledError(self):
+ """
+ L{CommandDispatcher} should relay its unhandled errors in responding to
+ boxes to its boxSender.
+ """
+ err = RuntimeError("something went wrong, oh no")
+ self.sender.expectError()
+ self.dispatcher.unhandledError(Failure(err))
+ self.assertEqual(len(self.sender.unhandledErrors), 1)
+ self.assertEqual(self.sender.unhandledErrors[0].value, err)
+
+
+ def test_unhandledSerializationError(self):
+ """
+ Errors during serialization ought to be relayed to the sender's
+ unhandledError method.
+ """
+ err = RuntimeError("something undefined went wrong")
+ def thunk(result):
+ class BrokenBox(amp.Box):
+ def _sendTo(self, proto):
+ raise err
+ return BrokenBox()
+ self.locator.commands['hello'] = thunk
+ input = amp.Box(_command="hello",
+ _ask="test-command-id",
+ hello="world")
+ self.sender.expectError()
+ self.dispatcher.ampBoxReceived(input)
+ self.assertEqual(len(self.sender.unhandledErrors), 1)
+ self.assertEqual(self.sender.unhandledErrors[0].value, err)
+
+
+ def test_callRemote(self):
+ """
+ L{CommandDispatcher.callRemote} should emit a properly formatted '_ask'
+ box to its boxSender and record an outstanding L{Deferred}. When a
+ corresponding '_answer' packet is received, the L{Deferred} should be
+ fired, and the results translated via the given L{Command}'s response
+ de-serialization.
+ """
+ D = self.dispatcher.callRemote(Hello, hello='world')
+ self.assertEqual(self.sender.sentBoxes,
+ [amp.AmpBox(_command="hello",
+ _ask="1",
+ hello="world")])
+ answers = []
+ D.addCallback(answers.append)
+ self.assertEqual(answers, [])
+ self.dispatcher.ampBoxReceived(amp.AmpBox({'hello': "yay",
+ 'print': "ignored",
+ '_answer': "1"}))
+ self.assertEqual(answers, [dict(hello="yay",
+ Print=u"ignored")])
+
+
+ def _localCallbackErrorLoggingTest(self, callResult):
+ """
+ Verify that C{callResult} completes with a C{None} result and that an
+ unhandled error has been logged.
+ """
+ finalResult = []
+ callResult.addBoth(finalResult.append)
+
+ self.assertEqual(1, len(self.sender.unhandledErrors))
+ self.assertIsInstance(
+ self.sender.unhandledErrors[0].value, ZeroDivisionError)
+
+ self.assertEqual([None], finalResult)
+
+
+ def test_callRemoteSuccessLocalCallbackErrorLogging(self):
+ """
+ If the last callback on the L{Deferred} returned by C{callRemote} (added
+ by application code calling C{callRemote}) fails, the failure is passed
+ to the sender's C{unhandledError} method.
+ """
+ self.sender.expectError()
+
+ callResult = self.dispatcher.callRemote(Hello, hello='world')
+ callResult.addCallback(lambda result: 1 / 0)
+
+ self.dispatcher.ampBoxReceived(amp.AmpBox({
+ 'hello': "yay", 'print': "ignored", '_answer': "1"}))
+
+ self._localCallbackErrorLoggingTest(callResult)
+
+
+ def test_callRemoteErrorLocalCallbackErrorLogging(self):
+ """
+ Like L{test_callRemoteSuccessLocalCallbackErrorLogging}, but for the
+ case where the L{Deferred} returned by C{callRemote} fails.
+ """
+ self.sender.expectError()
+
+ callResult = self.dispatcher.callRemote(Hello, hello='world')
+ callResult.addErrback(lambda result: 1 / 0)
+
+ self.dispatcher.ampBoxReceived(amp.AmpBox({
+ '_error': '1', '_error_code': 'bugs',
+ '_error_description': 'stuff'}))
+
+ self._localCallbackErrorLoggingTest(callResult)
+
+
+
+class SimpleGreeting(amp.Command):
+ """
+ A very simple greeting command that uses a few basic argument types.
+ """
+ commandName = 'simple'
+ arguments = [('greeting', amp.Unicode()),
+ ('cookie', amp.Integer())]
+ response = [('cookieplus', amp.Integer())]
+
+
+
+class TestLocator(amp.CommandLocator):
+ """
+ A locator which implements a responder to the 'simple' command.
+ """
+ def __init__(self):
+ self.greetings = []
+
+
+ def greetingResponder(self, greeting, cookie):
+ self.greetings.append((greeting, cookie))
+ return dict(cookieplus=cookie + 3)
+ greetingResponder = SimpleGreeting.responder(greetingResponder)
+
+
+
+class OverridingLocator(TestLocator):
+ """
+ A locator which overrides the responder to the 'simple' command.
+ """
+
+ def greetingResponder(self, greeting, cookie):
+ """
+ Return a different cookieplus than L{TestLocator.greetingResponder}.
+ """
+ self.greetings.append((greeting, cookie))
+ return dict(cookieplus=cookie + 4)
+ greetingResponder = SimpleGreeting.responder(greetingResponder)
+
+
+
+class InheritingLocator(OverridingLocator):
+ """
+ This locator should inherit the responder from L{OverridingLocator}.
+ """
+
+
+
+class OverrideLocatorAMP(amp.AMP):
+ def __init__(self):
+ amp.AMP.__init__(self)
+ self.customResponder = object()
+ self.expectations = {"custom": self.customResponder}
+ self.greetings = []
+
+
+ def lookupFunction(self, name):
+ """
+ Override the deprecated lookupFunction function.
+ """
+ if name in self.expectations:
+ result = self.expectations[name]
+ return result
+ else:
+ return super(OverrideLocatorAMP, self).lookupFunction(name)
+
+
+ def greetingResponder(self, greeting, cookie):
+ self.greetings.append((greeting, cookie))
+ return dict(cookieplus=cookie + 3)
+ greetingResponder = SimpleGreeting.responder(greetingResponder)
+
+
+
+
+class CommandLocatorTests(unittest.TestCase):
+ """
+ The CommandLocator should enable users to specify responders to commands as
+ functions that take structured objects, annotated with metadata.
+ """
+
+ def _checkSimpleGreeting(self, locatorClass, expected):
+ """
+ Check that a locator of type C{locatorClass} finds a responder
+ for command named I{simple} and that the found responder answers
+ with the C{expected} result to a C{SimpleGreeting<"ni hao", 5>}
+ command.
+ """
+ locator = locatorClass()
+ responderCallable = locator.locateResponder("simple")
+ result = responderCallable(amp.Box(greeting="ni hao", cookie="5"))
+ def done(values):
+ self.assertEqual(values, amp.AmpBox(cookieplus=str(expected)))
+ return result.addCallback(done)
+
+
+ def test_responderDecorator(self):
+ """
+ A method on a L{CommandLocator} subclass decorated with a L{Command}
+ subclass's L{responder} decorator should be returned from
+ locateResponder, wrapped in logic to serialize and deserialize its
+ arguments.
+ """
+ return self._checkSimpleGreeting(TestLocator, 8)
+
+
+ def test_responderOverriding(self):
+ """
+ L{CommandLocator} subclasses can override a responder inherited from
+ a base class by using the L{Command.responder} decorator to register
+ a new responder method.
+ """
+ return self._checkSimpleGreeting(OverridingLocator, 9)
+
+
+ def test_responderInheritance(self):
+ """
+ Responder lookup follows the same rules as normal method lookup
+ rules, particularly with respect to inheritance.
+ """
+ return self._checkSimpleGreeting(InheritingLocator, 9)
+
+
+ def test_lookupFunctionDeprecatedOverride(self):
+ """
+ Subclasses which override locateResponder under its old name,
+ lookupFunction, should have the override invoked instead. (This tests
+ an AMP subclass, because in the version of the code that could invoke
+ this deprecated code path, there was no L{CommandLocator}.)
+ """
+ locator = OverrideLocatorAMP()
+ customResponderObject = self.assertWarns(
+ PendingDeprecationWarning,
+ "Override locateResponder, not lookupFunction.",
+ __file__, lambda : locator.locateResponder("custom"))
+ self.assertEqual(locator.customResponder, customResponderObject)
+ # Make sure upcalling works too
+ normalResponderObject = self.assertWarns(
+ PendingDeprecationWarning,
+ "Override locateResponder, not lookupFunction.",
+ __file__, lambda : locator.locateResponder("simple"))
+ result = normalResponderObject(amp.Box(greeting="ni hao", cookie="5"))
+ def done(values):
+ self.assertEqual(values, amp.AmpBox(cookieplus='8'))
+ return result.addCallback(done)
+
+
+ def test_lookupFunctionDeprecatedInvoke(self):
+ """
+ Invoking locateResponder under its old name, lookupFunction, should
+ emit a deprecation warning, but do the same thing.
+ """
+ locator = TestLocator()
+ responderCallable = self.assertWarns(
+ PendingDeprecationWarning,
+ "Call locateResponder, not lookupFunction.", __file__,
+ lambda : locator.lookupFunction("simple"))
+ result = responderCallable(amp.Box(greeting="ni hao", cookie="5"))
+ def done(values):
+ self.assertEqual(values, amp.AmpBox(cookieplus='8'))
+ return result.addCallback(done)
+
+
+
+SWITCH_CLIENT_DATA = 'Success!'
+SWITCH_SERVER_DATA = 'No, really. Success.'
+
+
+class BinaryProtocolTests(unittest.TestCase):
+ """
+ Tests for L{amp.BinaryBoxProtocol}.
+
+ @ivar _boxSender: After C{startReceivingBoxes} is called, the L{IBoxSender}
+ which was passed to it.
+ """
+
+ def setUp(self):
+ """
+ Keep track of all boxes received by this test in its capacity as an
+ L{IBoxReceiver} implementor.
+ """
+ self.boxes = []
+ self.data = []
+
+
+ def startReceivingBoxes(self, sender):
+ """
+ Implement L{IBoxReceiver.startReceivingBoxes} to just remember the
+ value passed in.
+ """
+ self._boxSender = sender
+
+
+ def ampBoxReceived(self, box):
+ """
+ A box was received by the protocol.
+ """
+ self.boxes.append(box)
+
+ stopReason = None
+ def stopReceivingBoxes(self, reason):
+ """
+ Record the reason that we stopped receiving boxes.
+ """
+ self.stopReason = reason
+
+
+ # fake ITransport
+ def getPeer(self):
+ return 'no peer'
+
+
+ def getHost(self):
+ return 'no host'
+
+
+ def write(self, data):
+ self.data.append(data)
+
+
+ def test_startReceivingBoxes(self):
+ """
+ When L{amp.BinaryBoxProtocol} is connected to a transport, it calls
+ C{startReceivingBoxes} on its L{IBoxReceiver} with itself as the
+ L{IBoxSender} parameter.
+ """
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(None)
+ self.assertIdentical(self._boxSender, protocol)
+
+
+ def test_sendBoxInStartReceivingBoxes(self):
+ """
+ The L{IBoxReceiver} which is started when L{amp.BinaryBoxProtocol} is
+ connected to a transport can call C{sendBox} on the L{IBoxSender}
+ passed to it before C{startReceivingBoxes} returns and have that box
+ sent.
+ """
+ class SynchronouslySendingReceiver:
+ def startReceivingBoxes(self, sender):
+ sender.sendBox(amp.Box({'foo': 'bar'}))
+
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(SynchronouslySendingReceiver())
+ protocol.makeConnection(transport)
+ self.assertEqual(
+ transport.value(),
+ '\x00\x03foo\x00\x03bar\x00\x00')
+
+
+ def test_receiveBoxStateMachine(self):
+ """
+ When a binary box protocol receives:
+ * a key
+ * a value
+ * an empty string
+ it should emit a box and send it to its boxReceiver.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.stringReceived("hello")
+ a.stringReceived("world")
+ a.stringReceived("")
+ self.assertEqual(self.boxes, [amp.AmpBox(hello="world")])
+
+
+ def test_firstBoxFirstKeyExcessiveLength(self):
+ """
+ L{amp.BinaryBoxProtocol} drops its connection if the length prefix for
+ the first a key it receives is larger than 255.
+ """
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(transport)
+ protocol.dataReceived('\x01\x00')
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_firstBoxSubsequentKeyExcessiveLength(self):
+ """
+ L{amp.BinaryBoxProtocol} drops its connection if the length prefix for
+ a subsequent key in the first box it receives is larger than 255.
+ """
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(transport)
+ protocol.dataReceived('\x00\x01k\x00\x01v')
+ self.assertFalse(transport.disconnecting)
+ protocol.dataReceived('\x01\x00')
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_subsequentBoxFirstKeyExcessiveLength(self):
+ """
+ L{amp.BinaryBoxProtocol} drops its connection if the length prefix for
+ the first key in a subsequent box it receives is larger than 255.
+ """
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(transport)
+ protocol.dataReceived('\x00\x01k\x00\x01v\x00\x00')
+ self.assertFalse(transport.disconnecting)
+ protocol.dataReceived('\x01\x00')
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_excessiveKeyFailure(self):
+ """
+ If L{amp.BinaryBoxProtocol} disconnects because it received a key
+ length prefix which was too large, the L{IBoxReceiver}'s
+ C{stopReceivingBoxes} method is called with a L{TooLong} failure.
+ """
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('\x01\x00')
+ protocol.connectionLost(
+ Failure(error.ConnectionDone("simulated connection done")))
+ self.stopReason.trap(amp.TooLong)
+ self.assertTrue(self.stopReason.value.isKey)
+ self.assertFalse(self.stopReason.value.isLocal)
+ self.assertIdentical(self.stopReason.value.value, None)
+ self.assertIdentical(self.stopReason.value.keyName, None)
+
+
+ def test_unhandledErrorWithTransport(self):
+ """
+ L{amp.BinaryBoxProtocol.unhandledError} logs the failure passed to it
+ and disconnects its transport.
+ """
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(transport)
+ protocol.unhandledError(Failure(RuntimeError("Fake error")))
+ self.assertEqual(1, len(self.flushLoggedErrors(RuntimeError)))
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_unhandledErrorWithoutTransport(self):
+ """
+ L{amp.BinaryBoxProtocol.unhandledError} completes without error when
+ there is no associated transport.
+ """
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(StringTransport())
+ protocol.connectionLost(Failure(Exception("Simulated")))
+ protocol.unhandledError(Failure(RuntimeError("Fake error")))
+ self.assertEqual(1, len(self.flushLoggedErrors(RuntimeError)))
+
+
+ def test_receiveBoxData(self):
+ """
+ When a binary box protocol receives the serialized form of an AMP box,
+ it should emit a similar box to its boxReceiver.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.dataReceived(amp.Box({"testKey": "valueTest",
+ "anotherKey": "anotherValue"}).serialize())
+ self.assertEqual(self.boxes,
+ [amp.Box({"testKey": "valueTest",
+ "anotherKey": "anotherValue"})])
+
+
+ def test_receiveLongerBoxData(self):
+ """
+ An L{amp.BinaryBoxProtocol} can receive serialized AMP boxes with
+ values of up to (2 ** 16 - 1) bytes.
+ """
+ length = (2 ** 16 - 1)
+ value = 'x' * length
+ transport = StringTransport()
+ protocol = amp.BinaryBoxProtocol(self)
+ protocol.makeConnection(transport)
+ protocol.dataReceived(amp.Box({'k': value}).serialize())
+ self.assertEqual(self.boxes, [amp.Box({'k': value})])
+ self.assertFalse(transport.disconnecting)
+
+
+ def test_sendBox(self):
+ """
+ When a binary box protocol sends a box, it should emit the serialized
+ bytes of that box to its transport.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.makeConnection(self)
+ aBox = amp.Box({"testKey": "valueTest",
+ "someData": "hello"})
+ a.makeConnection(self)
+ a.sendBox(aBox)
+ self.assertEqual(''.join(self.data), aBox.serialize())
+
+
+ def test_connectionLostStopSendingBoxes(self):
+ """
+ When a binary box protocol loses its connection, it should notify its
+ box receiver that it has stopped receiving boxes.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.makeConnection(self)
+ connectionFailure = Failure(RuntimeError())
+ a.connectionLost(connectionFailure)
+ self.assertIdentical(self.stopReason, connectionFailure)
+
+
+ def test_protocolSwitch(self):
+ """
+ L{BinaryBoxProtocol} has the capacity to switch to a different protocol
+ on a box boundary. When a protocol is in the process of switching, it
+ cannot receive traffic.
+ """
+ otherProto = TestProto(None, "outgoing data")
+ test = self
+ class SwitchyReceiver:
+ switched = False
+ def startReceivingBoxes(self, sender):
+ pass
+ def ampBoxReceived(self, box):
+ test.assertFalse(self.switched,
+ "Should only receive one box!")
+ self.switched = True
+ a._lockForSwitch()
+ a._switchTo(otherProto)
+ a = amp.BinaryBoxProtocol(SwitchyReceiver())
+ anyOldBox = amp.Box({"include": "lots",
+ "of": "data"})
+ a.makeConnection(self)
+ # Include a 0-length box at the beginning of the next protocol's data,
+ # to make sure that AMP doesn't eat the data or try to deliver extra
+ # boxes either...
+ moreThanOneBox = anyOldBox.serialize() + "\x00\x00Hello, world!"
+ a.dataReceived(moreThanOneBox)
+ self.assertIdentical(otherProto.transport, self)
+ self.assertEqual("".join(otherProto.data), "\x00\x00Hello, world!")
+ self.assertEqual(self.data, ["outgoing data"])
+ a.dataReceived("more data")
+ self.assertEqual("".join(otherProto.data),
+ "\x00\x00Hello, world!more data")
+ self.assertRaises(amp.ProtocolSwitched, a.sendBox, anyOldBox)
+
+
+ def test_protocolSwitchEmptyBuffer(self):
+ """
+ After switching to a different protocol, if no extra bytes beyond
+ the switch box were delivered, an empty string is not passed to the
+ switched protocol's C{dataReceived} method.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.makeConnection(self)
+ otherProto = TestProto(None, "")
+ a._switchTo(otherProto)
+ self.assertEqual(otherProto.data, [])
+
+
+ def test_protocolSwitchInvalidStates(self):
+ """
+ In order to make sure the protocol never gets any invalid data sent
+ into the middle of a box, it must be locked for switching before it is
+ switched. It can only be unlocked if the switch failed, and attempting
+ to send a box while it is locked should raise an exception.
+ """
+ a = amp.BinaryBoxProtocol(self)
+ a.makeConnection(self)
+ sampleBox = amp.Box({"some": "data"})
+ a._lockForSwitch()
+ self.assertRaises(amp.ProtocolSwitched, a.sendBox, sampleBox)
+ a._unlockFromSwitch()
+ a.sendBox(sampleBox)
+ self.assertEqual(''.join(self.data), sampleBox.serialize())
+ a._lockForSwitch()
+ otherProto = TestProto(None, "outgoing data")
+ a._switchTo(otherProto)
+ self.assertRaises(amp.ProtocolSwitched, a._unlockFromSwitch)
+
+
+ def test_protocolSwitchLoseConnection(self):
+ """
+ When the protocol is switched, it should notify its nested protocol of
+ disconnection.
+ """
+ class Loser(protocol.Protocol):
+ reason = None
+ def connectionLost(self, reason):
+ self.reason = reason
+ connectionLoser = Loser()
+ a = amp.BinaryBoxProtocol(self)
+ a.makeConnection(self)
+ a._lockForSwitch()
+ a._switchTo(connectionLoser)
+ connectionFailure = Failure(RuntimeError())
+ a.connectionLost(connectionFailure)
+ self.assertEqual(connectionLoser.reason, connectionFailure)
+
+
+ def test_protocolSwitchLoseClientConnection(self):
+ """
+ When the protocol is switched, it should notify its nested client
+ protocol factory of disconnection.
+ """
+ class ClientLoser:
+ reason = None
+ def clientConnectionLost(self, connector, reason):
+ self.reason = reason
+ a = amp.BinaryBoxProtocol(self)
+ connectionLoser = protocol.Protocol()
+ clientLoser = ClientLoser()
+ a.makeConnection(self)
+ a._lockForSwitch()
+ a._switchTo(connectionLoser, clientLoser)
+ connectionFailure = Failure(RuntimeError())
+ a.connectionLost(connectionFailure)
+ self.assertEqual(clientLoser.reason, connectionFailure)
+
+
+
+class AMPTest(unittest.TestCase):
+
+ def test_interfaceDeclarations(self):
+ """
+ The classes in the amp module ought to implement the interfaces that
+ are declared for their benefit.
+ """
+ for interface, implementation in [(amp.IBoxSender, amp.BinaryBoxProtocol),
+ (amp.IBoxReceiver, amp.BoxDispatcher),
+ (amp.IResponderLocator, amp.CommandLocator),
+ (amp.IResponderLocator, amp.SimpleStringLocator),
+ (amp.IBoxSender, amp.AMP),
+ (amp.IBoxReceiver, amp.AMP),
+ (amp.IResponderLocator, amp.AMP)]:
+ self.failUnless(interface.implementedBy(implementation),
+ "%s does not implements(%s)" % (implementation, interface))
+
+
+ def test_helloWorld(self):
+ """
+ Verify that a simple command can be sent and its response received with
+ the simple low-level string-based API.
+ """
+ c, s, p = connectedServerAndClient()
+ L = []
+ HELLO = 'world'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_wireFormatRoundTrip(self):
+ """
+ Verify that mixed-case, underscored and dashed arguments are mapped to
+ their python names properly.
+ """
+ c, s, p = connectedServerAndClient()
+ L = []
+ HELLO = 'world'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_helloWorldUnicode(self):
+ """
+ Verify that unicode arguments can be encoded and decoded.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ HELLO = 'world'
+ HELLO_UNICODE = 'wor\u1234ld'
+ c.sendUnicodeHello(HELLO, HELLO_UNICODE).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+ self.assertEqual(L[0]['Print'], HELLO_UNICODE)
+
+
+ def test_callRemoteStringRequiresAnswerFalse(self):
+ """
+ L{BoxDispatcher.callRemoteString} returns C{None} if C{requiresAnswer}
+ is C{False}.
+ """
+ c, s, p = connectedServerAndClient()
+ ret = c.callRemoteString("WTF", requiresAnswer=False)
+ self.assertIdentical(ret, None)
+
+
+ def test_unknownCommandLow(self):
+ """
+ Verify that unknown commands using low-level APIs will be rejected with an
+ error, but will NOT terminate the connection.
+ """
+ c, s, p = connectedServerAndClient()
+ L = []
+ def clearAndAdd(e):
+ """
+ You can't propagate the error...
+ """
+ e.trap(amp.UnhandledCommand)
+ return "OK"
+ c.callRemoteString("WTF").addErrback(clearAndAdd).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L.pop(), "OK")
+ HELLO = 'world'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_unknownCommandHigh(self):
+ """
+ Verify that unknown commands using high-level APIs will be rejected with an
+ error, but will NOT terminate the connection.
+ """
+ c, s, p = connectedServerAndClient()
+ L = []
+ def clearAndAdd(e):
+ """
+ You can't propagate the error...
+ """
+ e.trap(amp.UnhandledCommand)
+ return "OK"
+ c.callRemote(WTF).addErrback(clearAndAdd).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L.pop(), "OK")
+ HELLO = 'world'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_brokenReturnValue(self):
+ """
+ It can be very confusing if you write some code which responds to a
+ command, but gets the return value wrong. Most commonly you end up
+ returning None instead of a dictionary.
+
+ Verify that if that happens, the framework logs a useful error.
+ """
+ L = []
+ SimpleSymmetricCommandProtocol().dispatchCommand(
+ amp.AmpBox(_command=BrokenReturn.commandName)).addErrback(L.append)
+ L[0].trap(amp.BadLocalReturn)
+ self.failUnlessIn('None', repr(L[0].value))
+
+
+ def test_unknownArgument(self):
+ """
+ Verify that unknown arguments are ignored, and not passed to a Python
+ function which can't accept them.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ HELLO = 'world'
+ # c.sendHello(HELLO).addCallback(L.append)
+ c.callRemote(FutureHello,
+ hello=HELLO,
+ bonus="I'm not in the book!").addCallback(
+ L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_simpleReprs(self):
+ """
+ Verify that the various Box objects repr properly, for debugging.
+ """
+ self.assertEqual(type(repr(amp._SwitchBox('a'))), str)
+ self.assertEqual(type(repr(amp.QuitBox())), str)
+ self.assertEqual(type(repr(amp.AmpBox())), str)
+ self.failUnless("AmpBox" in repr(amp.AmpBox()))
+
+
+ def test_innerProtocolInRepr(self):
+ """
+ Verify that L{AMP} objects output their innerProtocol when set.
+ """
+ otherProto = TestProto(None, "outgoing data")
+ a = amp.AMP()
+ a.innerProtocol = otherProto
+ def fakeID(obj):
+ return {a: 0x1234}.get(obj, id(obj))
+ self.addCleanup(setIDFunction, setIDFunction(fakeID))
+
+ self.assertEqual(
+ repr(a), "<AMP inner <TestProto #%d> at 0x1234>" % (
+ otherProto.instanceId,))
+
+
+ def test_innerProtocolNotInRepr(self):
+ """
+ Verify that L{AMP} objects do not output 'inner' when no innerProtocol
+ is set.
+ """
+ a = amp.AMP()
+ def fakeID(obj):
+ return {a: 0x4321}.get(obj, id(obj))
+ self.addCleanup(setIDFunction, setIDFunction(fakeID))
+ self.assertEqual(repr(a), "<AMP at 0x4321>")
+
+
+ def test_simpleSSLRepr(self):
+ """
+ L{amp._TLSBox.__repr__} returns a string.
+ """
+ self.assertEqual(type(repr(amp._TLSBox())), str)
+
+ test_simpleSSLRepr.skip = skipSSL
+
+
+ def test_keyTooLong(self):
+ """
+ Verify that a key that is too long will immediately raise a synchronous
+ exception.
+ """
+ c, s, p = connectedServerAndClient()
+ x = "H" * (0xff+1)
+ tl = self.assertRaises(amp.TooLong,
+ c.callRemoteString, "Hello",
+ **{x: "hi"})
+ self.assertTrue(tl.isKey)
+ self.assertTrue(tl.isLocal)
+ self.assertIdentical(tl.keyName, None)
+ self.assertEqual(tl.value, x)
+ self.assertIn(str(len(x)), repr(tl))
+ self.assertIn("key", repr(tl))
+
+
+ def test_valueTooLong(self):
+ """
+ Verify that attempting to send value longer than 64k will immediately
+ raise an exception.
+ """
+ c, s, p = connectedServerAndClient()
+ x = "H" * (0xffff+1)
+ tl = self.assertRaises(amp.TooLong, c.sendHello, x)
+ p.flush()
+ self.failIf(tl.isKey)
+ self.failUnless(tl.isLocal)
+ self.assertEqual(tl.keyName, 'hello')
+ self.failUnlessIdentical(tl.value, x)
+ self.failUnless(str(len(x)) in repr(tl))
+ self.failUnless("value" in repr(tl))
+ self.failUnless('hello' in repr(tl))
+
+
+ def test_helloWorldCommand(self):
+ """
+ Verify that a simple command can be sent and its response received with
+ the high-level value parsing API.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ HELLO = 'world'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0]['hello'], HELLO)
+
+
+ def test_helloErrorHandling(self):
+ """
+ Verify that if a known error type is raised and handled, it will be
+ properly relayed to the other end of the connection and translated into
+ an exception, and no error will be logged.
+ """
+ L=[]
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ HELLO = 'fuck you'
+ c.sendHello(HELLO).addErrback(L.append)
+ p.flush()
+ L[0].trap(UnfriendlyGreeting)
+ self.assertEqual(str(L[0].value), "Don't be a dick.")
+
+
+ def test_helloFatalErrorHandling(self):
+ """
+ Verify that if a known, fatal error type is raised and handled, it will
+ be properly relayed to the other end of the connection and translated
+ into an exception, no error will be logged, and the connection will be
+ terminated.
+ """
+ L=[]
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ HELLO = 'die'
+ c.sendHello(HELLO).addErrback(L.append)
+ p.flush()
+ L.pop().trap(DeathThreat)
+ c.sendHello(HELLO).addErrback(L.append)
+ p.flush()
+ L.pop().trap(error.ConnectionDone)
+
+
+
+ def test_helloNoErrorHandling(self):
+ """
+ Verify that if an unknown error type is raised, it will be relayed to
+ the other end of the connection and translated into an exception, it
+ will be logged, and then the connection will be dropped.
+ """
+ L=[]
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ HELLO = THING_I_DONT_UNDERSTAND
+ c.sendHello(HELLO).addErrback(L.append)
+ p.flush()
+ ure = L.pop()
+ ure.trap(amp.UnknownRemoteError)
+ c.sendHello(HELLO).addErrback(L.append)
+ cl = L.pop()
+ cl.trap(error.ConnectionDone)
+ # The exception should have been logged.
+ self.failUnless(self.flushLoggedErrors(ThingIDontUnderstandError))
+
+
+
+ def test_lateAnswer(self):
+ """
+ Verify that a command that does not get answered until after the
+ connection terminates will not cause any errors.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ c.callRemote(WaitForever).addErrback(L.append)
+ p.flush()
+ self.assertEqual(L, [])
+ s.transport.loseConnection()
+ p.flush()
+ L.pop().trap(error.ConnectionDone)
+ # Just make sure that it doesn't error...
+ s.waiting.callback({})
+ return s.waiting
+
+
+ def test_requiresNoAnswer(self):
+ """
+ Verify that a command that requires no answer is run.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ HELLO = 'world'
+ c.callRemote(NoAnswerHello, hello=HELLO)
+ p.flush()
+ self.failUnless(s.greeted)
+
+
+ def test_requiresNoAnswerFail(self):
+ """
+ Verify that commands sent after a failed no-answer request do not complete.
+ """
+ L=[]
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ HELLO = 'fuck you'
+ c.callRemote(NoAnswerHello, hello=HELLO)
+ p.flush()
+ # This should be logged locally.
+ self.failUnless(self.flushLoggedErrors(amp.RemoteAmpError))
+ HELLO = 'world'
+ c.callRemote(Hello, hello=HELLO).addErrback(L.append)
+ p.flush()
+ L.pop().trap(error.ConnectionDone)
+ self.failIf(s.greeted)
+
+
+ def test_noAnswerResponderBadAnswer(self):
+ """
+ Verify that responders of requiresAnswer=False commands have to return
+ a dictionary anyway.
+
+ (requiresAnswer is a hint from the _client_ - the server may be called
+ upon to answer commands in any case, if the client wants to know when
+ they complete.)
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=BadNoAnswerCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ c.callRemote(NoAnswerHello, hello="hello")
+ p.flush()
+ le = self.flushLoggedErrors(amp.BadLocalReturn)
+ self.assertEqual(len(le), 1)
+
+
+ def test_noAnswerResponderAskedForAnswer(self):
+ """
+ Verify that responders with requiresAnswer=False will actually respond
+ if the client sets requiresAnswer=True. In other words, verify that
+ requiresAnswer is a hint honored only by the client.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=NoAnswerCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ c.callRemote(Hello, hello="Hello!").addCallback(L.append)
+ p.flush()
+ self.assertEqual(len(L), 1)
+ self.assertEqual(L, [dict(hello="Hello!-noanswer",
+ Print=None)]) # Optional response argument
+
+
+ def test_ampListCommand(self):
+ """
+ Test encoding of an argument that uses the AmpList encoding.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ c.callRemote(GetList, length=10).addCallback(L.append)
+ p.flush()
+ values = L.pop().get('body')
+ self.assertEqual(values, [{'x': 1}] * 10)
+
+
+ def test_optionalAmpListOmitted(self):
+ """
+ Sending a command with an omitted AmpList argument that is
+ designated as optional does not raise an InvalidSignature error.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ c.callRemote(DontRejectMe, magicWord=u'please').addCallback(L.append)
+ p.flush()
+ response = L.pop().get('response')
+ self.assertEqual(response, 'list omitted')
+
+
+ def test_optionalAmpListPresent(self):
+ """
+ Sanity check that optional AmpList arguments are processed normally.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+ L = []
+ c.callRemote(DontRejectMe, magicWord=u'please',
+ list=[{'name': 'foo'}]).addCallback(L.append)
+ p.flush()
+ response = L.pop().get('response')
+ self.assertEqual(response, 'foo accepted')
+
+
+ def test_failEarlyOnArgSending(self):
+ """
+ Verify that if we pass an invalid argument list (omitting an argument),
+ an exception will be raised.
+ """
+ self.assertRaises(amp.InvalidSignature, Hello)
+
+
+ def test_doubleProtocolSwitch(self):
+ """
+ As a debugging aid, a protocol system should raise a
+ L{ProtocolSwitched} exception when asked to switch a protocol that is
+ already switched.
+ """
+ serverDeferred = defer.Deferred()
+ serverProto = SimpleSymmetricCommandProtocol(serverDeferred)
+ clientDeferred = defer.Deferred()
+ clientProto = SimpleSymmetricCommandProtocol(clientDeferred)
+ c, s, p = connectedServerAndClient(ServerClass=lambda: serverProto,
+ ClientClass=lambda: clientProto)
+ def switched(result):
+ self.assertRaises(amp.ProtocolSwitched, c.switchToTestProtocol)
+ self.testSucceeded = True
+ c.switchToTestProtocol().addCallback(switched)
+ p.flush()
+ self.failUnless(self.testSucceeded)
+
+
+ def test_protocolSwitch(self, switcher=SimpleSymmetricCommandProtocol,
+ spuriousTraffic=False,
+ spuriousError=False):
+ """
+ Verify that it is possible to switch to another protocol mid-connection and
+ send data to it successfully.
+ """
+ self.testSucceeded = False
+
+ serverDeferred = defer.Deferred()
+ serverProto = switcher(serverDeferred)
+ clientDeferred = defer.Deferred()
+ clientProto = switcher(clientDeferred)
+ c, s, p = connectedServerAndClient(ServerClass=lambda: serverProto,
+ ClientClass=lambda: clientProto)
+
+ if spuriousTraffic:
+ wfdr = [] # remote
+ c.callRemote(WaitForever).addErrback(wfdr.append)
+ switchDeferred = c.switchToTestProtocol()
+ if spuriousTraffic:
+ self.assertRaises(amp.ProtocolSwitched, c.sendHello, 'world')
+
+ def cbConnsLost(((serverSuccess, serverData),
+ (clientSuccess, clientData))):
+ self.failUnless(serverSuccess)
+ self.failUnless(clientSuccess)
+ self.assertEqual(''.join(serverData), SWITCH_CLIENT_DATA)
+ self.assertEqual(''.join(clientData), SWITCH_SERVER_DATA)
+ self.testSucceeded = True
+
+ def cbSwitch(proto):
+ return defer.DeferredList(
+ [serverDeferred, clientDeferred]).addCallback(cbConnsLost)
+
+ switchDeferred.addCallback(cbSwitch)
+ p.flush()
+ if serverProto.maybeLater is not None:
+ serverProto.maybeLater.callback(serverProto.maybeLaterProto)
+ p.flush()
+ if spuriousTraffic:
+ # switch is done here; do this here to make sure that if we're
+ # going to corrupt the connection, we do it before it's closed.
+ if spuriousError:
+ s.waiting.errback(amp.RemoteAmpError(
+ "SPURIOUS",
+ "Here's some traffic in the form of an error."))
+ else:
+ s.waiting.callback({})
+ p.flush()
+ c.transport.loseConnection() # close it
+ p.flush()
+ self.failUnless(self.testSucceeded)
+
+
+ def test_protocolSwitchDeferred(self):
+ """
+ Verify that protocol-switching even works if the value returned from
+ the command that does the switch is deferred.
+ """
+ return self.test_protocolSwitch(switcher=DeferredSymmetricCommandProtocol)
+
+
+ def test_protocolSwitchFail(self, switcher=SimpleSymmetricCommandProtocol):
+ """
+ Verify that if we try to switch protocols and it fails, the connection
+ stays up and we can go back to speaking AMP.
+ """
+ self.testSucceeded = False
+
+ serverDeferred = defer.Deferred()
+ serverProto = switcher(serverDeferred)
+ clientDeferred = defer.Deferred()
+ clientProto = switcher(clientDeferred)
+ c, s, p = connectedServerAndClient(ServerClass=lambda: serverProto,
+ ClientClass=lambda: clientProto)
+ L = []
+ c.switchToTestProtocol(fail=True).addErrback(L.append)
+ p.flush()
+ L.pop().trap(UnknownProtocol)
+ self.failIf(self.testSucceeded)
+ # It's a known error, so let's send a "hello" on the same connection;
+ # it should work.
+ c.sendHello('world').addCallback(L.append)
+ p.flush()
+ self.assertEqual(L.pop()['hello'], 'world')
+
+
+ def test_trafficAfterSwitch(self):
+ """
+ Verify that attempts to send traffic after a switch will not corrupt
+ the nested protocol.
+ """
+ return self.test_protocolSwitch(spuriousTraffic=True)
+
+
+ def test_errorAfterSwitch(self):
+ """
+ Returning an error after a protocol switch should record the underlying
+ error.
+ """
+ return self.test_protocolSwitch(spuriousTraffic=True,
+ spuriousError=True)
+
+
+ def test_quitBoxQuits(self):
+ """
+ Verify that commands with a responseType of QuitBox will in fact
+ terminate the connection.
+ """
+ c, s, p = connectedServerAndClient(
+ ServerClass=SimpleSymmetricCommandProtocol,
+ ClientClass=SimpleSymmetricCommandProtocol)
+
+ L = []
+ HELLO = 'world'
+ GOODBYE = 'everyone'
+ c.sendHello(HELLO).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L.pop()['hello'], HELLO)
+ c.callRemote(Goodbye).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L.pop()['goodbye'], GOODBYE)
+ c.sendHello(HELLO).addErrback(L.append)
+ L.pop().trap(error.ConnectionDone)
+
+
+ def test_basicLiteralEmit(self):
+ """
+ Verify that the command dictionaries for a callRemoteN look correct
+ after being serialized and parsed.
+ """
+ c, s, p = connectedServerAndClient()
+ L = []
+ s.ampBoxReceived = L.append
+ c.callRemote(Hello, hello='hello test', mixedCase='mixed case arg test',
+ dash_arg='x', underscore_arg='y')
+ p.flush()
+ self.assertEqual(len(L), 1)
+ for k, v in [('_command', Hello.commandName),
+ ('hello', 'hello test'),
+ ('mixedCase', 'mixed case arg test'),
+ ('dash-arg', 'x'),
+ ('underscore_arg', 'y')]:
+ self.assertEqual(L[-1].pop(k), v)
+ L[-1].pop('_ask')
+ self.assertEqual(L[-1], {})
+
+
+ def test_basicStructuredEmit(self):
+ """
+ Verify that a call similar to basicLiteralEmit's is handled properly with
+ high-level quoting and passing to Python methods, and that argument
+ names are correctly handled.
+ """
+ L = []
+ class StructuredHello(amp.AMP):
+ def h(self, *a, **k):
+ L.append((a, k))
+ return dict(hello='aaa')
+ Hello.responder(h)
+ c, s, p = connectedServerAndClient(ServerClass=StructuredHello)
+ c.callRemote(Hello, hello='hello test', mixedCase='mixed case arg test',
+ dash_arg='x', underscore_arg='y').addCallback(L.append)
+ p.flush()
+ self.assertEqual(len(L), 2)
+ self.assertEqual(L[0],
+ ((), dict(
+ hello='hello test',
+ mixedCase='mixed case arg test',
+ dash_arg='x',
+ underscore_arg='y',
+
+ # XXX - should optional arguments just not be passed?
+ # passing None seems a little odd, looking at the way it
+ # turns out here... -glyph
+ From=('file', 'file'),
+ Print=None,
+ optional=None,
+ )))
+ self.assertEqual(L[1], dict(Print=None, hello='aaa'))
+
+class PretendRemoteCertificateAuthority:
+ def checkIsPretendRemote(self):
+ return True
+
+class IOSimCert:
+ verifyCount = 0
+
+ def options(self, *ign):
+ return self
+
+ def iosimVerify(self, otherCert):
+ """
+ This isn't a real certificate, and wouldn't work on a real socket, but
+ iosim specifies a different API so that we don't have to do any crypto
+ math to demonstrate that the right functions get called in the right
+ places.
+ """
+ assert otherCert is self
+ self.verifyCount += 1
+ return True
+
+class OKCert(IOSimCert):
+ def options(self, x):
+ assert x.checkIsPretendRemote()
+ return self
+
+class GrumpyCert(IOSimCert):
+ def iosimVerify(self, otherCert):
+ self.verifyCount += 1
+ return False
+
+class DroppyCert(IOSimCert):
+ def __init__(self, toDrop):
+ self.toDrop = toDrop
+
+ def iosimVerify(self, otherCert):
+ self.verifyCount += 1
+ self.toDrop.loseConnection()
+ return True
+
+class SecurableProto(FactoryNotifier):
+
+ factory = None
+
+ def verifyFactory(self):
+ return [PretendRemoteCertificateAuthority()]
+
+ def getTLSVars(self):
+ cert = self.certFactory()
+ verify = self.verifyFactory()
+ return dict(
+ tls_localCertificate=cert,
+ tls_verifyAuthorities=verify)
+ amp.StartTLS.responder(getTLSVars)
+
+
+
+class TLSTest(unittest.TestCase):
+ def test_startingTLS(self):
+ """
+ Verify that starting TLS and succeeding at handshaking sends all the
+ notifications to all the right places.
+ """
+ cli, svr, p = connectedServerAndClient(
+ ServerClass=SecurableProto,
+ ClientClass=SecurableProto)
+
+ okc = OKCert()
+ svr.certFactory = lambda : okc
+
+ cli.callRemote(
+ amp.StartTLS, tls_localCertificate=okc,
+ tls_verifyAuthorities=[PretendRemoteCertificateAuthority()])
+
+ # let's buffer something to be delivered securely
+ L = []
+ cli.callRemote(SecuredPing).addCallback(L.append)
+ p.flush()
+ # once for client once for server
+ self.assertEqual(okc.verifyCount, 2)
+ L = []
+ cli.callRemote(SecuredPing).addCallback(L.append)
+ p.flush()
+ self.assertEqual(L[0], {'pinged': True})
+
+
+ def test_startTooManyTimes(self):
+ """
+ Verify that the protocol will complain if we attempt to renegotiate TLS,
+ which we don't support.
+ """
+ cli, svr, p = connectedServerAndClient(
+ ServerClass=SecurableProto,
+ ClientClass=SecurableProto)
+
+ okc = OKCert()
+ svr.certFactory = lambda : okc
+
+ cli.callRemote(amp.StartTLS,
+ tls_localCertificate=okc,
+ tls_verifyAuthorities=[PretendRemoteCertificateAuthority()])
+ p.flush()
+ cli.noPeerCertificate = True # this is totally fake
+ self.assertRaises(
+ amp.OnlyOneTLS,
+ cli.callRemote,
+ amp.StartTLS,
+ tls_localCertificate=okc,
+ tls_verifyAuthorities=[PretendRemoteCertificateAuthority()])
+
+
+ def test_negotiationFailed(self):
+ """
+ Verify that starting TLS and failing on both sides at handshaking sends
+ notifications to all the right places and terminates the connection.
+ """
+
+ badCert = GrumpyCert()
+
+ cli, svr, p = connectedServerAndClient(
+ ServerClass=SecurableProto,
+ ClientClass=SecurableProto)
+ svr.certFactory = lambda : badCert
+
+ cli.callRemote(amp.StartTLS,
+ tls_localCertificate=badCert)
+
+ p.flush()
+ # once for client once for server - but both fail
+ self.assertEqual(badCert.verifyCount, 2)
+ d = cli.callRemote(SecuredPing)
+ p.flush()
+ self.assertFailure(d, iosim.NativeOpenSSLError)
+
+
+ def test_negotiationFailedByClosing(self):
+ """
+ Verify that starting TLS and failing by way of a lost connection
+ notices that it is probably an SSL problem.
+ """
+
+ cli, svr, p = connectedServerAndClient(
+ ServerClass=SecurableProto,
+ ClientClass=SecurableProto)
+ droppyCert = DroppyCert(svr.transport)
+ svr.certFactory = lambda : droppyCert
+
+ cli.callRemote(amp.StartTLS, tls_localCertificate=droppyCert)
+
+ p.flush()
+
+ self.assertEqual(droppyCert.verifyCount, 2)
+
+ d = cli.callRemote(SecuredPing)
+ p.flush()
+
+ # it might be a good idea to move this exception somewhere more
+ # reasonable.
+ self.assertFailure(d, error.PeerVerifyError)
+
+ skip = skipSSL
+
+
+
+class TLSNotAvailableTest(unittest.TestCase):
+ """
+ Tests what happened when ssl is not available in current installation.
+ """
+
+ def setUp(self):
+ """
+ Disable ssl in amp.
+ """
+ self.ssl = amp.ssl
+ amp.ssl = None
+
+
+ def tearDown(self):
+ """
+ Restore ssl module.
+ """
+ amp.ssl = self.ssl
+
+
+ def test_callRemoteError(self):
+ """
+ Check that callRemote raises an exception when called with a
+ L{amp.StartTLS}.
+ """
+ cli, svr, p = connectedServerAndClient(
+ ServerClass=SecurableProto,
+ ClientClass=SecurableProto)
+
+ okc = OKCert()
+ svr.certFactory = lambda : okc
+
+ return self.assertFailure(cli.callRemote(
+ amp.StartTLS, tls_localCertificate=okc,
+ tls_verifyAuthorities=[PretendRemoteCertificateAuthority()]),
+ RuntimeError)
+
+
+ def test_messageReceivedError(self):
+ """
+ When a client with SSL enabled talks to a server without SSL, it
+ should return a meaningful error.
+ """
+ svr = SecurableProto()
+ okc = OKCert()
+ svr.certFactory = lambda : okc
+ box = amp.Box()
+ box['_command'] = 'StartTLS'
+ box['_ask'] = '1'
+ boxes = []
+ svr.sendBox = boxes.append
+ svr.makeConnection(StringTransport())
+ svr.ampBoxReceived(box)
+ self.assertEqual(boxes,
+ [{'_error_code': 'TLS_ERROR',
+ '_error': '1',
+ '_error_description': 'TLS not available'}])
+
+
+
+class InheritedError(Exception):
+ """
+ This error is used to check inheritance.
+ """
+
+
+
+class OtherInheritedError(Exception):
+ """
+ This is a distinct error for checking inheritance.
+ """
+
+
+
+class BaseCommand(amp.Command):
+ """
+ This provides a command that will be subclassed.
+ """
+ errors = {InheritedError: 'INHERITED_ERROR'}
+
+
+
+class InheritedCommand(BaseCommand):
+ """
+ This is a command which subclasses another command but does not override
+ anything.
+ """
+
+
+
+class AddErrorsCommand(BaseCommand):
+ """
+ This is a command which subclasses another command but adds errors to the
+ list.
+ """
+ arguments = [('other', amp.Boolean())]
+ errors = {OtherInheritedError: 'OTHER_INHERITED_ERROR'}
+
+
+
+class NormalCommandProtocol(amp.AMP):
+ """
+ This is a protocol which responds to L{BaseCommand}, and is used to test
+ that inheritance does not interfere with the normal handling of errors.
+ """
+ def resp(self):
+ raise InheritedError()
+ BaseCommand.responder(resp)
+
+
+
+class InheritedCommandProtocol(amp.AMP):
+ """
+ This is a protocol which responds to L{InheritedCommand}, and is used to
+ test that inherited commands inherit their bases' errors if they do not
+ respond to any of their own.
+ """
+ def resp(self):
+ raise InheritedError()
+ InheritedCommand.responder(resp)
+
+
+
+class AddedCommandProtocol(amp.AMP):
+ """
+ This is a protocol which responds to L{AddErrorsCommand}, and is used to
+ test that inherited commands can add their own new types of errors, but
+ still respond in the same way to their parents types of errors.
+ """
+ def resp(self, other):
+ if other:
+ raise OtherInheritedError()
+ else:
+ raise InheritedError()
+ AddErrorsCommand.responder(resp)
+
+
+
+class CommandInheritanceTests(unittest.TestCase):
+ """
+ These tests verify that commands inherit error conditions properly.
+ """
+
+ def errorCheck(self, err, proto, cmd, **kw):
+ """
+ Check that the appropriate kind of error is raised when a given command
+ is sent to a given protocol.
+ """
+ c, s, p = connectedServerAndClient(ServerClass=proto,
+ ClientClass=proto)
+ d = c.callRemote(cmd, **kw)
+ d2 = self.failUnlessFailure(d, err)
+ p.flush()
+ return d2
+
+
+ def test_basicErrorPropagation(self):
+ """
+ Verify that errors specified in a superclass are respected normally
+ even if it has subclasses.
+ """
+ return self.errorCheck(
+ InheritedError, NormalCommandProtocol, BaseCommand)
+
+
+ def test_inheritedErrorPropagation(self):
+ """
+ Verify that errors specified in a superclass command are propagated to
+ its subclasses.
+ """
+ return self.errorCheck(
+ InheritedError, InheritedCommandProtocol, InheritedCommand)
+
+
+ def test_inheritedErrorAddition(self):
+ """
+ Verify that new errors specified in a subclass of an existing command
+ are honored even if the superclass defines some errors.
+ """
+ return self.errorCheck(
+ OtherInheritedError, AddedCommandProtocol, AddErrorsCommand, other=True)
+
+
+ def test_additionWithOriginalError(self):
+ """
+ Verify that errors specified in a command's superclass are respected
+ even if that command defines new errors itself.
+ """
+ return self.errorCheck(
+ InheritedError, AddedCommandProtocol, AddErrorsCommand, other=False)
+
+
+def _loseAndPass(err, proto):
+ # be specific, pass on the error to the client.
+ err.trap(error.ConnectionLost, error.ConnectionDone)
+ del proto.connectionLost
+ proto.connectionLost(err)
+
+
+class LiveFireBase:
+ """
+ Utility for connected reactor-using tests.
+ """
+
+ def setUp(self):
+ """
+ Create an amp server and connect a client to it.
+ """
+ from twisted.internet import reactor
+ self.serverFactory = protocol.ServerFactory()
+ self.serverFactory.protocol = self.serverProto
+ self.clientFactory = protocol.ClientFactory()
+ self.clientFactory.protocol = self.clientProto
+ self.clientFactory.onMade = defer.Deferred()
+ self.serverFactory.onMade = defer.Deferred()
+ self.serverPort = reactor.listenTCP(0, self.serverFactory)
+ self.addCleanup(self.serverPort.stopListening)
+ self.clientConn = reactor.connectTCP(
+ '127.0.0.1', self.serverPort.getHost().port,
+ self.clientFactory)
+ self.addCleanup(self.clientConn.disconnect)
+ def getProtos(rlst):
+ self.cli = self.clientFactory.theProto
+ self.svr = self.serverFactory.theProto
+ dl = defer.DeferredList([self.clientFactory.onMade,
+ self.serverFactory.onMade])
+ return dl.addCallback(getProtos)
+
+ def tearDown(self):
+ """
+ Cleanup client and server connections, and check the error got at
+ C{connectionLost}.
+ """
+ L = []
+ for conn in self.cli, self.svr:
+ if conn.transport is not None:
+ # depend on amp's function connection-dropping behavior
+ d = defer.Deferred().addErrback(_loseAndPass, conn)
+ conn.connectionLost = d.errback
+ conn.transport.loseConnection()
+ L.append(d)
+ return defer.gatherResults(L
+ ).addErrback(lambda first: first.value.subFailure)
+
+
+def show(x):
+ import sys
+ sys.stdout.write(x+'\n')
+ sys.stdout.flush()
+
+
+def tempSelfSigned():
+ from twisted.internet import ssl
+
+ sharedDN = ssl.DN(CN='shared')
+ key = ssl.KeyPair.generate()
+ cr = key.certificateRequest(sharedDN)
+ sscrd = key.signCertificateRequest(
+ sharedDN, cr, lambda dn: True, 1234567)
+ cert = key.newCertificate(sscrd)
+ return cert
+
+if ssl is not None:
+ tempcert = tempSelfSigned()
+
+
+class LiveFireTLSTestCase(LiveFireBase, unittest.TestCase):
+ clientProto = SecurableProto
+ serverProto = SecurableProto
+ def test_liveFireCustomTLS(self):
+ """
+ Using real, live TLS, actually negotiate a connection.
+
+ This also looks at the 'peerCertificate' attribute's correctness, since
+ that's actually loaded using OpenSSL calls, but the main purpose is to
+ make sure that we didn't miss anything obvious in iosim about TLS
+ negotiations.
+ """
+
+ cert = tempcert
+
+ self.svr.verifyFactory = lambda : [cert]
+ self.svr.certFactory = lambda : cert
+ # only needed on the server, we specify the client below.
+
+ def secured(rslt):
+ x = cert.digest()
+ def pinged(rslt2):
+ # Interesting. OpenSSL won't even _tell_ us about the peer
+ # cert until we negotiate. we should be able to do this in
+ # 'secured' instead, but it looks like we can't. I think this
+ # is a bug somewhere far deeper than here.
+ self.assertEqual(x, self.cli.hostCertificate.digest())
+ self.assertEqual(x, self.cli.peerCertificate.digest())
+ self.assertEqual(x, self.svr.hostCertificate.digest())
+ self.assertEqual(x, self.svr.peerCertificate.digest())
+ return self.cli.callRemote(SecuredPing).addCallback(pinged)
+ return self.cli.callRemote(amp.StartTLS,
+ tls_localCertificate=cert,
+ tls_verifyAuthorities=[cert]).addCallback(secured)
+
+ skip = skipSSL
+
+
+
+class SlightlySmartTLS(SimpleSymmetricCommandProtocol):
+ """
+ Specific implementation of server side protocol with different
+ management of TLS.
+ """
+ def getTLSVars(self):
+ """
+ @return: the global C{tempcert} certificate as local certificate.
+ """
+ return dict(tls_localCertificate=tempcert)
+ amp.StartTLS.responder(getTLSVars)
+
+
+class PlainVanillaLiveFire(LiveFireBase, unittest.TestCase):
+
+ clientProto = SimpleSymmetricCommandProtocol
+ serverProto = SimpleSymmetricCommandProtocol
+
+ def test_liveFireDefaultTLS(self):
+ """
+ Verify that out of the box, we can start TLS to at least encrypt the
+ connection, even if we don't have any certificates to use.
+ """
+ def secured(result):
+ return self.cli.callRemote(SecuredPing)
+ return self.cli.callRemote(amp.StartTLS).addCallback(secured)
+
+ skip = skipSSL
+
+
+
+class WithServerTLSVerification(LiveFireBase, unittest.TestCase):
+ clientProto = SimpleSymmetricCommandProtocol
+ serverProto = SlightlySmartTLS
+
+ def test_anonymousVerifyingClient(self):
+ """
+ Verify that anonymous clients can verify server certificates.
+ """
+ def secured(result):
+ return self.cli.callRemote(SecuredPing)
+ return self.cli.callRemote(amp.StartTLS,
+ tls_verifyAuthorities=[tempcert]
+ ).addCallback(secured)
+
+ skip = skipSSL
+
+
+
+class ProtocolIncludingArgument(amp.Argument):
+ """
+ An L{amp.Argument} which encodes its parser and serializer
+ arguments *including the protocol* into its parsed and serialized
+ forms.
+ """
+
+ def fromStringProto(self, string, protocol):
+ """
+ Don't decode anything; just return all possible information.
+
+ @return: A two-tuple of the input string and the protocol.
+ """
+ return (string, protocol)
+
+ def toStringProto(self, obj, protocol):
+ """
+ Encode identifying information about L{object} and protocol
+ into a string for later verification.
+
+ @type obj: L{object}
+ @type protocol: L{amp.AMP}
+ """
+ return "%s:%s" % (id(obj), id(protocol))
+
+
+
+class ProtocolIncludingCommand(amp.Command):
+ """
+ A command that has argument and response schemas which use
+ L{ProtocolIncludingArgument}.
+ """
+ arguments = [('weird', ProtocolIncludingArgument())]
+ response = [('weird', ProtocolIncludingArgument())]
+
+
+
+class MagicSchemaCommand(amp.Command):
+ """
+ A command which overrides L{parseResponse}, L{parseArguments}, and
+ L{makeResponse}.
+ """
+ def parseResponse(self, strings, protocol):
+ """
+ Don't do any parsing, just jam the input strings and protocol
+ onto the C{protocol.parseResponseArguments} attribute as a
+ two-tuple. Return the original strings.
+ """
+ protocol.parseResponseArguments = (strings, protocol)
+ return strings
+ parseResponse = classmethod(parseResponse)
+
+
+ def parseArguments(cls, strings, protocol):
+ """
+ Don't do any parsing, just jam the input strings and protocol
+ onto the C{protocol.parseArgumentsArguments} attribute as a
+ two-tuple. Return the original strings.
+ """
+ protocol.parseArgumentsArguments = (strings, protocol)
+ return strings
+ parseArguments = classmethod(parseArguments)
+
+
+ def makeArguments(cls, objects, protocol):
+ """
+ Don't do any serializing, just jam the input strings and protocol
+ onto the C{protocol.makeArgumentsArguments} attribute as a
+ two-tuple. Return the original strings.
+ """
+ protocol.makeArgumentsArguments = (objects, protocol)
+ return objects
+ makeArguments = classmethod(makeArguments)
+
+
+
+class NoNetworkProtocol(amp.AMP):
+ """
+ An L{amp.AMP} subclass which overrides private methods to avoid
+ testing the network. It also provides a responder for
+ L{MagicSchemaCommand} that does nothing, so that tests can test
+ aspects of the interaction of L{amp.Command}s and L{amp.AMP}.
+
+ @ivar parseArgumentsArguments: Arguments that have been passed to any
+ L{MagicSchemaCommand}, if L{MagicSchemaCommand} has been handled by
+ this protocol.
+
+ @ivar parseResponseArguments: Responses that have been returned from a
+ L{MagicSchemaCommand}, if L{MagicSchemaCommand} has been handled by
+ this protocol.
+
+ @ivar makeArgumentsArguments: Arguments that have been serialized by any
+ L{MagicSchemaCommand}, if L{MagicSchemaCommand} has been handled by
+ this protocol.
+ """
+ def _sendBoxCommand(self, commandName, strings, requiresAnswer):
+ """
+ Return a Deferred which fires with the original strings.
+ """
+ return defer.succeed(strings)
+
+ MagicSchemaCommand.responder(lambda s, weird: {})
+
+
+
+class MyBox(dict):
+ """
+ A unique dict subclass.
+ """
+
+
+
+class ProtocolIncludingCommandWithDifferentCommandType(
+ ProtocolIncludingCommand):
+ """
+ A L{ProtocolIncludingCommand} subclass whose commandType is L{MyBox}
+ """
+ commandType = MyBox
+
+
+
+class CommandTestCase(unittest.TestCase):
+ """
+ Tests for L{amp.Argument} and L{amp.Command}.
+ """
+ def test_argumentInterface(self):
+ """
+ L{Argument} instances provide L{amp.IArgumentType}.
+ """
+ self.assertTrue(verifyObject(amp.IArgumentType, amp.Argument()))
+
+
+ def test_parseResponse(self):
+ """
+ There should be a class method of Command which accepts a
+ mapping of argument names to serialized forms and returns a
+ similar mapping whose values have been parsed via the
+ Command's response schema.
+ """
+ protocol = object()
+ result = 'whatever'
+ strings = {'weird': result}
+ self.assertEqual(
+ ProtocolIncludingCommand.parseResponse(strings, protocol),
+ {'weird': (result, protocol)})
+
+
+ def test_callRemoteCallsParseResponse(self):
+ """
+ Making a remote call on a L{amp.Command} subclass which
+ overrides the C{parseResponse} method should call that
+ C{parseResponse} method to get the response.
+ """
+ client = NoNetworkProtocol()
+ thingy = "weeoo"
+ response = client.callRemote(MagicSchemaCommand, weird=thingy)
+ def gotResponse(ign):
+ self.assertEqual(client.parseResponseArguments,
+ ({"weird": thingy}, client))
+ response.addCallback(gotResponse)
+ return response
+
+
+ def test_parseArguments(self):
+ """
+ There should be a class method of L{amp.Command} which accepts
+ a mapping of argument names to serialized forms and returns a
+ similar mapping whose values have been parsed via the
+ command's argument schema.
+ """
+ protocol = object()
+ result = 'whatever'
+ strings = {'weird': result}
+ self.assertEqual(
+ ProtocolIncludingCommand.parseArguments(strings, protocol),
+ {'weird': (result, protocol)})
+
+
+ def test_responderCallsParseArguments(self):
+ """
+ Making a remote call on a L{amp.Command} subclass which
+ overrides the C{parseArguments} method should call that
+ C{parseArguments} method to get the arguments.
+ """
+ protocol = NoNetworkProtocol()
+ responder = protocol.locateResponder(MagicSchemaCommand.commandName)
+ argument = object()
+ response = responder(dict(weird=argument))
+ response.addCallback(
+ lambda ign: self.assertEqual(protocol.parseArgumentsArguments,
+ ({"weird": argument}, protocol)))
+ return response
+
+
+ def test_makeArguments(self):
+ """
+ There should be a class method of L{amp.Command} which accepts
+ a mapping of argument names to objects and returns a similar
+ mapping whose values have been serialized via the command's
+ argument schema.
+ """
+ protocol = object()
+ argument = object()
+ objects = {'weird': argument}
+ self.assertEqual(
+ ProtocolIncludingCommand.makeArguments(objects, protocol),
+ {'weird': "%d:%d" % (id(argument), id(protocol))})
+
+
+ def test_makeArgumentsUsesCommandType(self):
+ """
+ L{amp.Command.makeArguments}'s return type should be the type
+ of the result of L{amp.Command.commandType}.
+ """
+ protocol = object()
+ objects = {"weird": "whatever"}
+
+ result = ProtocolIncludingCommandWithDifferentCommandType.makeArguments(
+ objects, protocol)
+ self.assertIdentical(type(result), MyBox)
+
+
+ def test_callRemoteCallsMakeArguments(self):
+ """
+ Making a remote call on a L{amp.Command} subclass which
+ overrides the C{makeArguments} method should call that
+ C{makeArguments} method to get the response.
+ """
+ client = NoNetworkProtocol()
+ argument = object()
+ response = client.callRemote(MagicSchemaCommand, weird=argument)
+ def gotResponse(ign):
+ self.assertEqual(client.makeArgumentsArguments,
+ ({"weird": argument}, client))
+ response.addCallback(gotResponse)
+ return response
+
+
+ def test_extraArgumentsDisallowed(self):
+ """
+ L{Command.makeArguments} raises L{amp.InvalidSignature} if the objects
+ dictionary passed to it includes a key which does not correspond to the
+ Python identifier for a defined argument.
+ """
+ self.assertRaises(
+ amp.InvalidSignature,
+ Hello.makeArguments,
+ dict(hello="hello", bogusArgument=object()), None)
+
+
+ def test_wireSpellingDisallowed(self):
+ """
+ If a command argument conflicts with a Python keyword, the
+ untransformed argument name is not allowed as a key in the dictionary
+ passed to L{Command.makeArguments}. If it is supplied,
+ L{amp.InvalidSignature} is raised.
+
+ This may be a pointless implementation restriction which may be lifted.
+ The current behavior is tested to verify that such arguments are not
+ silently dropped on the floor (the previous behavior).
+ """
+ self.assertRaises(
+ amp.InvalidSignature,
+ Hello.makeArguments,
+ dict(hello="required", **{"print": "print value"}),
+ None)
+
+
+class ListOfTestsMixin:
+ """
+ Base class for testing L{ListOf}, a parameterized zero-or-more argument
+ type.
+
+ @ivar elementType: Subclasses should set this to an L{Argument}
+ instance. The tests will make a L{ListOf} using this.
+
+ @ivar strings: Subclasses should set this to a dictionary mapping some
+ number of keys to the correct serialized form for some example
+ values. These should agree with what L{elementType}
+ produces/accepts.
+
+ @ivar objects: Subclasses should set this to a dictionary with the same
+ keys as C{strings} and with values which are the lists which should
+ serialize to the values in the C{strings} dictionary.
+ """
+ def test_toBox(self):
+ """
+ L{ListOf.toBox} extracts the list of objects from the C{objects}
+ dictionary passed to it, using the C{name} key also passed to it,
+ serializes each of the elements in that list using the L{Argument}
+ instance previously passed to its initializer, combines the serialized
+ results, and inserts the result into the C{strings} dictionary using
+ the same C{name} key.
+ """
+ stringList = amp.ListOf(self.elementType)
+ strings = amp.AmpBox()
+ for key in self.objects:
+ stringList.toBox(key, strings, self.objects.copy(), None)
+ self.assertEqual(strings, self.strings)
+
+
+ def test_fromBox(self):
+ """
+ L{ListOf.fromBox} reverses the operation performed by L{ListOf.toBox}.
+ """
+ stringList = amp.ListOf(self.elementType)
+ objects = {}
+ for key in self.strings:
+ stringList.fromBox(key, self.strings.copy(), objects, None)
+ self.assertEqual(objects, self.objects)
+
+
+
+class ListOfStringsTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.String}.
+ """
+ elementType = amp.String()
+
+ strings = {
+ "empty": "",
+ "single": "\x00\x03foo",
+ "multiple": "\x00\x03bar\x00\x03baz\x00\x04quux"}
+
+ objects = {
+ "empty": [],
+ "single": ["foo"],
+ "multiple": ["bar", "baz", "quux"]}
+
+
+class ListOfIntegersTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.Integer}.
+ """
+ elementType = amp.Integer()
+
+ huge = (
+ 9999999999999999999999999999999999999999999999999999999999 *
+ 9999999999999999999999999999999999999999999999999999999999)
+
+ strings = {
+ "empty": "",
+ "single": "\x00\x0210",
+ "multiple": "\x00\x011\x00\x0220\x00\x03500",
+ "huge": "\x00\x74%d" % (huge,),
+ "negative": "\x00\x02-1"}
+
+ objects = {
+ "empty": [],
+ "single": [10],
+ "multiple": [1, 20, 500],
+ "huge": [huge],
+ "negative": [-1]}
+
+
+
+class ListOfUnicodeTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.Unicode}.
+ """
+ elementType = amp.Unicode()
+
+ strings = {
+ "empty": "",
+ "single": "\x00\x03foo",
+ "multiple": "\x00\x03\xe2\x98\x83\x00\x05Hello\x00\x05world"}
+
+ objects = {
+ "empty": [],
+ "single": [u"foo"],
+ "multiple": [u"\N{SNOWMAN}", u"Hello", u"world"]}
+
+
+
+class ListOfDecimalTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.Decimal}.
+ """
+ elementType = amp.Decimal()
+
+ strings = {
+ "empty": "",
+ "single": "\x00\x031.1",
+ "extreme": "\x00\x08Infinity\x00\x09-Infinity",
+ "scientist": "\x00\x083.141E+5\x00\x0a0.00003141\x00\x083.141E-7"
+ "\x00\x09-3.141E+5\x00\x0b-0.00003141\x00\x09-3.141E-7",
+ "engineer": "\x00\x04%s\x00\x06%s" % (
+ decimal.Decimal("0e6").to_eng_string(),
+ decimal.Decimal("1.5E-9").to_eng_string()),
+ }
+
+ objects = {
+ "empty": [],
+ "single": [decimal.Decimal("1.1")],
+ "extreme": [
+ decimal.Decimal("Infinity"),
+ decimal.Decimal("-Infinity"),
+ ],
+ # exarkun objected to AMP supporting engineering notation because
+ # it was redundant, until we realised that 1E6 has less precision
+ # than 1000000 and is represented differently. But they compare
+ # and even hash equally. There were tears.
+ "scientist": [
+ decimal.Decimal("3.141E5"),
+ decimal.Decimal("3.141e-5"),
+ decimal.Decimal("3.141E-7"),
+ decimal.Decimal("-3.141e5"),
+ decimal.Decimal("-3.141E-5"),
+ decimal.Decimal("-3.141e-7"),
+ ],
+ "engineer": [
+ decimal.Decimal("0e6"),
+ decimal.Decimal("1.5E-9"),
+ ],
+ }
+
+
+
+class ListOfDecimalNanTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.Decimal} for not-a-number values.
+ """
+ elementType = amp.Decimal()
+
+ strings = {
+ "nan": "\x00\x03NaN\x00\x04-NaN\x00\x04sNaN\x00\x05-sNaN",
+ }
+
+ objects = {
+ "nan": [
+ decimal.Decimal("NaN"),
+ decimal.Decimal("-NaN"),
+ decimal.Decimal("sNaN"),
+ decimal.Decimal("-sNaN"),
+ ]
+ }
+
+ def test_fromBox(self):
+ """
+ L{ListOf.fromBox} reverses the operation performed by L{ListOf.toBox}.
+ """
+ # Helpers. Decimal.is_{qnan,snan,signed}() are new in 2.6 (or 2.5.2,
+ # but who's counting).
+ def is_qnan(decimal):
+ return 'NaN' in str(decimal) and 'sNaN' not in str(decimal)
+
+ def is_snan(decimal):
+ return 'sNaN' in str(decimal)
+
+ def is_signed(decimal):
+ return '-' in str(decimal)
+
+ # NaN values have unusual equality semantics, so this method is
+ # overridden to compare the resulting objects in a way which works with
+ # NaNs.
+ stringList = amp.ListOf(self.elementType)
+ objects = {}
+ for key in self.strings:
+ stringList.fromBox(key, self.strings.copy(), objects, None)
+ n = objects["nan"]
+ self.assertTrue(is_qnan(n[0]) and not is_signed(n[0]))
+ self.assertTrue(is_qnan(n[1]) and is_signed(n[1]))
+ self.assertTrue(is_snan(n[2]) and not is_signed(n[2]))
+ self.assertTrue(is_snan(n[3]) and is_signed(n[3]))
+
+
+
+class DecimalTests(unittest.TestCase):
+ """
+ Tests for L{amp.Decimal}.
+ """
+ def test_nonDecimal(self):
+ """
+ L{amp.Decimal.toString} raises L{ValueError} if passed an object which
+ is not an instance of C{decimal.Decimal}.
+ """
+ argument = amp.Decimal()
+ self.assertRaises(ValueError, argument.toString, "1.234")
+ self.assertRaises(ValueError, argument.toString, 1.234)
+ self.assertRaises(ValueError, argument.toString, 1234)
+
+
+
+class ListOfDateTimeTests(unittest.TestCase, ListOfTestsMixin):
+ """
+ Tests for L{ListOf} combined with L{amp.DateTime}.
+ """
+ elementType = amp.DateTime()
+
+ strings = {
+ "christmas":
+ "\x00\x202010-12-25T00:00:00.000000-00:00"
+ "\x00\x202010-12-25T00:00:00.000000-00:00",
+ "christmas in eu": "\x00\x202010-12-25T00:00:00.000000+01:00",
+ "christmas in iran": "\x00\x202010-12-25T00:00:00.000000+03:30",
+ "christmas in nyc": "\x00\x202010-12-25T00:00:00.000000-05:00",
+ "previous tests": "\x00\x202010-12-25T00:00:00.000000+03:19"
+ "\x00\x202010-12-25T00:00:00.000000-06:59",
+ }
+
+ objects = {
+ "christmas": [
+ datetime.datetime(2010, 12, 25, 0, 0, 0, tzinfo=amp.utc),
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('+', 0, 0)),
+ ],
+ "christmas in eu": [
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('+', 1, 0)),
+ ],
+ "christmas in iran": [
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('+', 3, 30)),
+ ],
+ "christmas in nyc": [
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('-', 5, 0)),
+ ],
+ "previous tests": [
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('+', 3, 19)),
+ datetime.datetime(2010, 12, 25, 0, 0, 0,
+ tzinfo=amp._FixedOffsetTZInfo('-', 6, 59)),
+ ],
+ }
+
+
+
+class ListOfOptionalTests(unittest.TestCase):
+ """
+ Tests to ensure L{ListOf} AMP arguments can be omitted from AMP commands
+ via the 'optional' flag.
+ """
+ def test_requiredArgumentWithNoneValueRaisesTypeError(self):
+ """
+ L{ListOf.toBox} raises C{TypeError} when passed a value of C{None}
+ for the argument.
+ """
+ stringList = amp.ListOf(amp.Integer())
+ self.assertRaises(
+ TypeError, stringList.toBox, 'omitted', amp.AmpBox(),
+ {'omitted': None}, None)
+
+
+ def test_optionalArgumentWithNoneValueOmitted(self):
+ """
+ L{ListOf.toBox} silently omits serializing any argument with a
+ value of C{None} that is designated as optional for the protocol.
+ """
+ stringList = amp.ListOf(amp.Integer(), optional=True)
+ strings = amp.AmpBox()
+ stringList.toBox('omitted', strings, {'omitted': None}, None)
+ self.assertEqual(strings, {})
+
+
+ def test_requiredArgumentWithKeyMissingRaisesKeyError(self):
+ """
+ L{ListOf.toBox} raises C{KeyError} if the argument's key is not
+ present in the objects dictionary.
+ """
+ stringList = amp.ListOf(amp.Integer())
+ self.assertRaises(
+ KeyError, stringList.toBox, 'ommited', amp.AmpBox(),
+ {'someOtherKey': 0}, None)
+
+
+ def test_optionalArgumentWithKeyMissingOmitted(self):
+ """
+ L{ListOf.toBox} silently omits serializing any argument designated
+ as optional whose key is not present in the objects dictionary.
+ """
+ stringList = amp.ListOf(amp.Integer(), optional=True)
+ stringList.toBox('ommited', amp.AmpBox(), {'someOtherKey': 0}, None)
+
+
+ def test_omittedOptionalArgumentDeserializesAsNone(self):
+ """
+ L{ListOf.fromBox} correctly reverses the operation performed by
+ L{ListOf.toBox} for optional arguments.
+ """
+ stringList = amp.ListOf(amp.Integer(), optional=True)
+ objects = {}
+ stringList.fromBox('omitted', {}, objects, None)
+ self.assertEqual(objects, {'omitted': None})
+
+
+
+class UNIXStringTransport(object):
+ """
+ An in-memory implementation of L{interfaces.IUNIXTransport} which collects
+ all data given to it for later inspection.
+
+ @ivar _queue: A C{list} of the data which has been given to this transport,
+ eg via C{write} or C{sendFileDescriptor}. Elements are two-tuples of a
+ string (identifying the destination of the data) and the data itself.
+ """
+ implements(interfaces.IUNIXTransport)
+
+ def __init__(self, descriptorFuzz):
+ """
+ @param descriptorFuzz: An offset to apply to descriptors.
+ @type descriptorFuzz: C{int}
+ """
+ self._fuzz = descriptorFuzz
+ self._queue = []
+
+
+ def sendFileDescriptor(self, descriptor):
+ self._queue.append((
+ 'fileDescriptorReceived', descriptor + self._fuzz))
+
+
+ def write(self, data):
+ self._queue.append(('dataReceived', data))
+
+
+ def writeSequence(self, seq):
+ for data in seq:
+ self.write(data)
+
+
+ def loseConnection(self):
+ self._queue.append(('connectionLost', Failure(ConnectionLost())))
+
+
+ def getHost(self):
+ return UNIXAddress('/tmp/some-path')
+
+
+ def getPeer(self):
+ return UNIXAddress('/tmp/another-path')
+
+# Minimal evidence that we got the signatures right
+verifyClass(interfaces.ITransport, UNIXStringTransport)
+verifyClass(interfaces.IUNIXTransport, UNIXStringTransport)
+
+
+class DescriptorTests(unittest.TestCase):
+ """
+ Tests for L{amp.Descriptor}, an argument type for passing a file descriptor
+ over an AMP connection over a UNIX domain socket.
+ """
+ def setUp(self):
+ self.fuzz = 3
+ self.transport = UNIXStringTransport(descriptorFuzz=self.fuzz)
+ self.protocol = amp.BinaryBoxProtocol(
+ amp.BoxDispatcher(amp.CommandLocator()))
+ self.protocol.makeConnection(self.transport)
+
+
+ def test_fromStringProto(self):
+ """
+ L{Descriptor.fromStringProto} constructs a file descriptor value by
+ extracting a previously received file descriptor corresponding to the
+ wire value of the argument from the L{_DescriptorExchanger} state of the
+ protocol passed to it.
+
+ This is a whitebox test which involves direct L{_DescriptorExchanger}
+ state inspection.
+ """
+ argument = amp.Descriptor()
+ self.protocol.fileDescriptorReceived(5)
+ self.protocol.fileDescriptorReceived(3)
+ self.protocol.fileDescriptorReceived(1)
+ self.assertEqual(
+ 5, argument.fromStringProto("0", self.protocol))
+ self.assertEqual(
+ 3, argument.fromStringProto("1", self.protocol))
+ self.assertEqual(
+ 1, argument.fromStringProto("2", self.protocol))
+ self.assertEqual({}, self.protocol._descriptors)
+
+
+ def test_toStringProto(self):
+ """
+ To send a file descriptor, L{Descriptor.toStringProto} uses the
+ L{IUNIXTransport.sendFileDescriptor} implementation of the transport of
+ the protocol passed to it to copy the file descriptor. Each subsequent
+ descriptor sent over a particular AMP connection is assigned the next
+ integer value, starting from 0. The base ten string representation of
+ this value is the byte encoding of the argument.
+
+ This is a whitebox test which involves direct L{_DescriptorExchanger}
+ state inspection and mutation.
+ """
+ argument = amp.Descriptor()
+ self.assertEqual("0", argument.toStringProto(2, self.protocol))
+ self.assertEqual(
+ ("fileDescriptorReceived", 2 + self.fuzz), self.transport._queue.pop(0))
+ self.assertEqual("1", argument.toStringProto(4, self.protocol))
+ self.assertEqual(
+ ("fileDescriptorReceived", 4 + self.fuzz), self.transport._queue.pop(0))
+ self.assertEqual("2", argument.toStringProto(6, self.protocol))
+ self.assertEqual(
+ ("fileDescriptorReceived", 6 + self.fuzz), self.transport._queue.pop(0))
+ self.assertEqual({}, self.protocol._descriptors)
+
+
+ def test_roundTrip(self):
+ """
+ L{amp.Descriptor.fromBox} can interpret an L{amp.AmpBox} constructed by
+ L{amp.Descriptor.toBox} to reconstruct a file descriptor value.
+ """
+ name = "alpha"
+ strings = {}
+ descriptor = 17
+ sendObjects = {name: descriptor}
+
+ argument = amp.Descriptor()
+ argument.toBox(name, strings, sendObjects.copy(), self.protocol)
+
+ receiver = amp.BinaryBoxProtocol(
+ amp.BoxDispatcher(amp.CommandLocator()))
+ for event in self.transport._queue:
+ getattr(receiver, event[0])(*event[1:])
+
+ receiveObjects = {}
+ argument.fromBox(name, strings.copy(), receiveObjects, receiver)
+
+ # Make sure we got the descriptor. Adjust by fuzz to be more convincing
+ # of having gone through L{IUNIXTransport.sendFileDescriptor}, not just
+ # converted to a string and then parsed back into an integer.
+ self.assertEqual(descriptor + self.fuzz, receiveObjects[name])
+
+
+
+class DateTimeTests(unittest.TestCase):
+ """
+ Tests for L{amp.DateTime}, L{amp._FixedOffsetTZInfo}, and L{amp.utc}.
+ """
+ string = '9876-01-23T12:34:56.054321-01:23'
+ tzinfo = amp._FixedOffsetTZInfo('-', 1, 23)
+ object = datetime.datetime(9876, 1, 23, 12, 34, 56, 54321, tzinfo)
+
+ def test_invalidString(self):
+ """
+ L{amp.DateTime.fromString} raises L{ValueError} when passed a string
+ which does not represent a timestamp in the proper format.
+ """
+ d = amp.DateTime()
+ self.assertRaises(ValueError, d.fromString, 'abc')
+
+
+ def test_invalidDatetime(self):
+ """
+ L{amp.DateTime.toString} raises L{ValueError} when passed a naive
+ datetime (a datetime with no timezone information).
+ """
+ d = amp.DateTime()
+ self.assertRaises(ValueError, d.toString,
+ datetime.datetime(2010, 12, 25, 0, 0, 0))
+
+
+ def test_fromString(self):
+ """
+ L{amp.DateTime.fromString} returns a C{datetime.datetime} with all of
+ its fields populated from the string passed to it.
+ """
+ argument = amp.DateTime()
+ value = argument.fromString(self.string)
+ self.assertEqual(value, self.object)
+
+
+ def test_toString(self):
+ """
+ L{amp.DateTime.toString} returns a C{str} in the wire format including
+ all of the information from the C{datetime.datetime} passed into it,
+ including the timezone offset.
+ """
+ argument = amp.DateTime()
+ value = argument.toString(self.object)
+ self.assertEqual(value, self.string)
+
+
+
+class FixedOffsetTZInfoTests(unittest.TestCase):
+ """
+ Tests for L{amp._FixedOffsetTZInfo} and L{amp.utc}.
+ """
+
+ def test_tzname(self):
+ """
+ L{amp.utc.tzname} returns C{"+00:00"}.
+ """
+ self.assertEqual(amp.utc.tzname(None), '+00:00')
+
+
+ def test_dst(self):
+ """
+ L{amp.utc.dst} returns a zero timedelta.
+ """
+ self.assertEqual(amp.utc.dst(None), datetime.timedelta(0))
+
+
+ def test_utcoffset(self):
+ """
+ L{amp.utc.utcoffset} returns a zero timedelta.
+ """
+ self.assertEqual(amp.utc.utcoffset(None), datetime.timedelta(0))
+
+
+ def test_badSign(self):
+ """
+ L{amp._FixedOffsetTZInfo} raises L{ValueError} if passed an offset sign
+ other than C{'+'} or C{'-'}.
+ """
+ self.assertRaises(ValueError, amp._FixedOffsetTZInfo, '?', 0, 0)
+
+
+
+if not interfaces.IReactorSSL.providedBy(reactor):
+ skipMsg = 'This test case requires SSL support in the reactor'
+ TLSTest.skip = skipMsg
+ LiveFireTLSTestCase.skip = skipMsg
+ PlainVanillaLiveFire.skip = skipMsg
+ WithServerTLSVerification.skip = skipMsg
diff --git a/twisted/test/test_application.py b/twisted/test/test_application.py
new file mode 100644
index 0000000..7736c5b
--- /dev/null
+++ b/twisted/test/test_application.py
@@ -0,0 +1,878 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.application} and its interaction with
+L{twisted.persisted.sob}.
+"""
+
+import copy, os, pickle
+from StringIO import StringIO
+
+from twisted.trial import unittest, util
+from twisted.application import service, internet, app
+from twisted.persisted import sob
+from twisted.python import usage
+from twisted.internet import interfaces, defer
+from twisted.protocols import wire, basic
+from twisted.internet import protocol, reactor
+from twisted.application import reactors
+from twisted.test.proto_helpers import MemoryReactor
+
+
+class Dummy:
+ processName=None
+
+class TestService(unittest.TestCase):
+
+ def testName(self):
+ s = service.Service()
+ s.setName("hello")
+ self.assertEqual(s.name, "hello")
+
+ def testParent(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setServiceParent(p)
+ self.assertEqual(list(p), [s])
+ self.assertEqual(s.parent, p)
+
+ def testApplicationAsParent(self):
+ s = service.Service()
+ p = service.Application("")
+ s.setServiceParent(p)
+ self.assertEqual(list(service.IServiceCollection(p)), [s])
+ self.assertEqual(s.parent, service.IServiceCollection(p))
+
+ def testNamedChild(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setName("hello")
+ s.setServiceParent(p)
+ self.assertEqual(list(p), [s])
+ self.assertEqual(s.parent, p)
+ self.assertEqual(p.getServiceNamed("hello"), s)
+
+ def testDoublyNamedChild(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setName("hello")
+ s.setServiceParent(p)
+ self.failUnlessRaises(RuntimeError, s.setName, "lala")
+
+ def testDuplicateNamedChild(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setName("hello")
+ s.setServiceParent(p)
+ s = service.Service()
+ s.setName("hello")
+ self.failUnlessRaises(RuntimeError, s.setServiceParent, p)
+
+ def testDisowning(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setServiceParent(p)
+ self.assertEqual(list(p), [s])
+ self.assertEqual(s.parent, p)
+ s.disownServiceParent()
+ self.assertEqual(list(p), [])
+ self.assertEqual(s.parent, None)
+
+ def testRunning(self):
+ s = service.Service()
+ self.assert_(not s.running)
+ s.startService()
+ self.assert_(s.running)
+ s.stopService()
+ self.assert_(not s.running)
+
+ def testRunningChildren1(self):
+ s = service.Service()
+ p = service.MultiService()
+ s.setServiceParent(p)
+ self.assert_(not s.running)
+ self.assert_(not p.running)
+ p.startService()
+ self.assert_(s.running)
+ self.assert_(p.running)
+ p.stopService()
+ self.assert_(not s.running)
+ self.assert_(not p.running)
+
+ def testRunningChildren2(self):
+ s = service.Service()
+ def checkRunning():
+ self.assert_(s.running)
+ t = service.Service()
+ t.stopService = checkRunning
+ t.startService = checkRunning
+ p = service.MultiService()
+ s.setServiceParent(p)
+ t.setServiceParent(p)
+ p.startService()
+ p.stopService()
+
+ def testAddingIntoRunning(self):
+ p = service.MultiService()
+ p.startService()
+ s = service.Service()
+ self.assert_(not s.running)
+ s.setServiceParent(p)
+ self.assert_(s.running)
+ s.disownServiceParent()
+ self.assert_(not s.running)
+
+ def testPrivileged(self):
+ s = service.Service()
+ def pss():
+ s.privilegedStarted = 1
+ s.privilegedStartService = pss
+ s1 = service.Service()
+ p = service.MultiService()
+ s.setServiceParent(p)
+ s1.setServiceParent(p)
+ p.privilegedStartService()
+ self.assert_(s.privilegedStarted)
+
+ def testCopying(self):
+ s = service.Service()
+ s.startService()
+ s1 = copy.copy(s)
+ self.assert_(not s1.running)
+ self.assert_(s.running)
+
+
+if hasattr(os, "getuid"):
+ curuid = os.getuid()
+ curgid = os.getgid()
+else:
+ curuid = curgid = 0
+
+
+class TestProcess(unittest.TestCase):
+
+ def testID(self):
+ p = service.Process(5, 6)
+ self.assertEqual(p.uid, 5)
+ self.assertEqual(p.gid, 6)
+
+ def testDefaults(self):
+ p = service.Process(5)
+ self.assertEqual(p.uid, 5)
+ self.assertEqual(p.gid, None)
+ p = service.Process(gid=5)
+ self.assertEqual(p.uid, None)
+ self.assertEqual(p.gid, 5)
+ p = service.Process()
+ self.assertEqual(p.uid, None)
+ self.assertEqual(p.gid, None)
+
+ def testProcessName(self):
+ p = service.Process()
+ self.assertEqual(p.processName, None)
+ p.processName = 'hello'
+ self.assertEqual(p.processName, 'hello')
+
+
+class TestInterfaces(unittest.TestCase):
+
+ def testService(self):
+ self.assert_(service.IService.providedBy(service.Service()))
+
+ def testMultiService(self):
+ self.assert_(service.IService.providedBy(service.MultiService()))
+ self.assert_(service.IServiceCollection.providedBy(service.MultiService()))
+
+ def testProcess(self):
+ self.assert_(service.IProcess.providedBy(service.Process()))
+
+
+class TestApplication(unittest.TestCase):
+
+ def testConstructor(self):
+ service.Application("hello")
+ service.Application("hello", 5)
+ service.Application("hello", 5, 6)
+
+ def testProcessComponent(self):
+ a = service.Application("hello")
+ self.assertEqual(service.IProcess(a).uid, None)
+ self.assertEqual(service.IProcess(a).gid, None)
+ a = service.Application("hello", 5)
+ self.assertEqual(service.IProcess(a).uid, 5)
+ self.assertEqual(service.IProcess(a).gid, None)
+ a = service.Application("hello", 5, 6)
+ self.assertEqual(service.IProcess(a).uid, 5)
+ self.assertEqual(service.IProcess(a).gid, 6)
+
+ def testServiceComponent(self):
+ a = service.Application("hello")
+ self.assert_(service.IService(a) is service.IServiceCollection(a))
+ self.assertEqual(service.IService(a).name, "hello")
+ self.assertEqual(service.IService(a).parent, None)
+
+ def testPersistableComponent(self):
+ a = service.Application("hello")
+ p = sob.IPersistable(a)
+ self.assertEqual(p.style, 'pickle')
+ self.assertEqual(p.name, 'hello')
+ self.assert_(p.original is a)
+
+class TestLoading(unittest.TestCase):
+
+ def test_simpleStoreAndLoad(self):
+ a = service.Application("hello")
+ p = sob.IPersistable(a)
+ for style in 'source pickle'.split():
+ p.setStyle(style)
+ p.save()
+ a1 = service.loadApplication("hello.ta"+style[0], style)
+ self.assertEqual(service.IService(a1).name, "hello")
+ f = open("hello.tac", 'w')
+ f.writelines([
+ "from twisted.application import service\n",
+ "application = service.Application('hello')\n",
+ ])
+ f.close()
+ a1 = service.loadApplication("hello.tac", 'python')
+ self.assertEqual(service.IService(a1).name, "hello")
+
+
+
+class TestAppSupport(unittest.TestCase):
+
+ def testPassphrase(self):
+ self.assertEqual(app.getPassphrase(0), None)
+
+ def testLoadApplication(self):
+ """
+ Test loading an application file in different dump format.
+ """
+ a = service.Application("hello")
+ baseconfig = {'file': None, 'source': None, 'python':None}
+ for style in 'source pickle'.split():
+ config = baseconfig.copy()
+ config[{'pickle': 'file'}.get(style, style)] = 'helloapplication'
+ sob.IPersistable(a).setStyle(style)
+ sob.IPersistable(a).save(filename='helloapplication')
+ a1 = app.getApplication(config, None)
+ self.assertEqual(service.IService(a1).name, "hello")
+ config = baseconfig.copy()
+ config['python'] = 'helloapplication'
+ f = open("helloapplication", 'w')
+ f.writelines([
+ "from twisted.application import service\n",
+ "application = service.Application('hello')\n",
+ ])
+ f.close()
+ a1 = app.getApplication(config, None)
+ self.assertEqual(service.IService(a1).name, "hello")
+
+ def test_convertStyle(self):
+ appl = service.Application("lala")
+ for instyle in 'source pickle'.split():
+ for outstyle in 'source pickle'.split():
+ sob.IPersistable(appl).setStyle(instyle)
+ sob.IPersistable(appl).save(filename="converttest")
+ app.convertStyle("converttest", instyle, None,
+ "converttest.out", outstyle, 0)
+ appl2 = service.loadApplication("converttest.out", outstyle)
+ self.assertEqual(service.IService(appl2).name, "lala")
+
+
+ def test_startApplication(self):
+ appl = service.Application("lala")
+ app.startApplication(appl, 0)
+ self.assert_(service.IService(appl).running)
+
+
+class Foo(basic.LineReceiver):
+ def connectionMade(self):
+ self.transport.write('lalala\r\n')
+ def lineReceived(self, line):
+ self.factory.line = line
+ self.transport.loseConnection()
+ def connectionLost(self, reason):
+ self.factory.d.callback(self.factory.line)
+
+
+class DummyApp:
+ processName = None
+ def addService(self, service):
+ self.services[service.name] = service
+ def removeService(self, service):
+ del self.services[service.name]
+
+
+class TimerTarget:
+ def __init__(self):
+ self.l = []
+ def append(self, what):
+ self.l.append(what)
+
+class TestEcho(wire.Echo):
+ def connectionLost(self, reason):
+ self.d.callback(True)
+
+class TestInternet2(unittest.TestCase):
+
+ def testTCP(self):
+ s = service.MultiService()
+ s.startService()
+ factory = protocol.ServerFactory()
+ factory.protocol = TestEcho
+ TestEcho.d = defer.Deferred()
+ t = internet.TCPServer(0, factory)
+ t.setServiceParent(s)
+ num = t._port.getHost().port
+ factory = protocol.ClientFactory()
+ factory.d = defer.Deferred()
+ factory.protocol = Foo
+ factory.line = None
+ internet.TCPClient('127.0.0.1', num, factory).setServiceParent(s)
+ factory.d.addCallback(self.assertEqual, 'lalala')
+ factory.d.addCallback(lambda x : s.stopService())
+ factory.d.addCallback(lambda x : TestEcho.d)
+ return factory.d
+
+
+ def test_UDP(self):
+ """
+ Test L{internet.UDPServer} with a random port: starting the service
+ should give it valid port, and stopService should free it so that we
+ can start a server on the same port again.
+ """
+ if not interfaces.IReactorUDP(reactor, None):
+ raise unittest.SkipTest("This reactor does not support UDP sockets")
+ p = protocol.DatagramProtocol()
+ t = internet.UDPServer(0, p)
+ t.startService()
+ num = t._port.getHost().port
+ self.assertNotEquals(num, 0)
+ def onStop(ignored):
+ t = internet.UDPServer(num, p)
+ t.startService()
+ return t.stopService()
+ return defer.maybeDeferred(t.stopService).addCallback(onStop)
+
+
+ def testPrivileged(self):
+ factory = protocol.ServerFactory()
+ factory.protocol = TestEcho
+ TestEcho.d = defer.Deferred()
+ t = internet.TCPServer(0, factory)
+ t.privileged = 1
+ t.privilegedStartService()
+ num = t._port.getHost().port
+ factory = protocol.ClientFactory()
+ factory.d = defer.Deferred()
+ factory.protocol = Foo
+ factory.line = None
+ c = internet.TCPClient('127.0.0.1', num, factory)
+ c.startService()
+ factory.d.addCallback(self.assertEqual, 'lalala')
+ factory.d.addCallback(lambda x : c.stopService())
+ factory.d.addCallback(lambda x : t.stopService())
+ factory.d.addCallback(lambda x : TestEcho.d)
+ return factory.d
+
+ def testConnectionGettingRefused(self):
+ factory = protocol.ServerFactory()
+ factory.protocol = wire.Echo
+ t = internet.TCPServer(0, factory)
+ t.startService()
+ num = t._port.getHost().port
+ t.stopService()
+ d = defer.Deferred()
+ factory = protocol.ClientFactory()
+ factory.clientConnectionFailed = lambda *args: d.callback(None)
+ c = internet.TCPClient('127.0.0.1', num, factory)
+ c.startService()
+ return d
+
+ def testUNIX(self):
+ # FIXME: This test is far too dense. It needs comments.
+ # -- spiv, 2004-11-07
+ if not interfaces.IReactorUNIX(reactor, None):
+ raise unittest.SkipTest, "This reactor does not support UNIX domain sockets"
+ s = service.MultiService()
+ s.startService()
+ factory = protocol.ServerFactory()
+ factory.protocol = TestEcho
+ TestEcho.d = defer.Deferred()
+ t = internet.UNIXServer('echo.skt', factory)
+ t.setServiceParent(s)
+ factory = protocol.ClientFactory()
+ factory.protocol = Foo
+ factory.d = defer.Deferred()
+ factory.line = None
+ internet.UNIXClient('echo.skt', factory).setServiceParent(s)
+ factory.d.addCallback(self.assertEqual, 'lalala')
+ factory.d.addCallback(lambda x : s.stopService())
+ factory.d.addCallback(lambda x : TestEcho.d)
+ factory.d.addCallback(self._cbTestUnix, factory, s)
+ return factory.d
+
+ def _cbTestUnix(self, ignored, factory, s):
+ TestEcho.d = defer.Deferred()
+ factory.line = None
+ factory.d = defer.Deferred()
+ s.startService()
+ factory.d.addCallback(self.assertEqual, 'lalala')
+ factory.d.addCallback(lambda x : s.stopService())
+ factory.d.addCallback(lambda x : TestEcho.d)
+ return factory.d
+
+ def testVolatile(self):
+ if not interfaces.IReactorUNIX(reactor, None):
+ raise unittest.SkipTest, "This reactor does not support UNIX domain sockets"
+ factory = protocol.ServerFactory()
+ factory.protocol = wire.Echo
+ t = internet.UNIXServer('echo.skt', factory)
+ t.startService()
+ self.failIfIdentical(t._port, None)
+ t1 = copy.copy(t)
+ self.assertIdentical(t1._port, None)
+ t.stopService()
+ self.assertIdentical(t._port, None)
+ self.failIf(t.running)
+
+ factory = protocol.ClientFactory()
+ factory.protocol = wire.Echo
+ t = internet.UNIXClient('echo.skt', factory)
+ t.startService()
+ self.failIfIdentical(t._connection, None)
+ t1 = copy.copy(t)
+ self.assertIdentical(t1._connection, None)
+ t.stopService()
+ self.assertIdentical(t._connection, None)
+ self.failIf(t.running)
+
+ def testStoppingServer(self):
+ if not interfaces.IReactorUNIX(reactor, None):
+ raise unittest.SkipTest, "This reactor does not support UNIX domain sockets"
+ factory = protocol.ServerFactory()
+ factory.protocol = wire.Echo
+ t = internet.UNIXServer('echo.skt', factory)
+ t.startService()
+ t.stopService()
+ self.failIf(t.running)
+ factory = protocol.ClientFactory()
+ d = defer.Deferred()
+ factory.clientConnectionFailed = lambda *args: d.callback(None)
+ reactor.connectUNIX('echo.skt', factory)
+ return d
+
+ def testPickledTimer(self):
+ target = TimerTarget()
+ t0 = internet.TimerService(1, target.append, "hello")
+ t0.startService()
+ s = pickle.dumps(t0)
+ t0.stopService()
+
+ t = pickle.loads(s)
+ self.failIf(t.running)
+
+ def testBrokenTimer(self):
+ d = defer.Deferred()
+ t = internet.TimerService(1, lambda: 1 / 0)
+ oldFailed = t._failed
+ def _failed(why):
+ oldFailed(why)
+ d.callback(None)
+ t._failed = _failed
+ t.startService()
+ d.addCallback(lambda x : t.stopService)
+ d.addCallback(lambda x : self.assertEqual(
+ [ZeroDivisionError],
+ [o.value.__class__ for o in self.flushLoggedErrors(ZeroDivisionError)]))
+ return d
+
+
+ def test_genericServerDeprecated(self):
+ """
+ Instantiating L{GenericServer} emits a deprecation warning.
+ """
+ internet.GenericServer()
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_genericServerDeprecated])
+ self.assertEqual(
+ warnings[0]['message'],
+ 'GenericServer was deprecated in Twisted 10.1.')
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_genericClientDeprecated(self):
+ """
+ Instantiating L{GenericClient} emits a deprecation warning.
+ """
+ internet.GenericClient()
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_genericClientDeprecated])
+ self.assertEqual(
+ warnings[0]['message'],
+ 'GenericClient was deprecated in Twisted 10.1.')
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_everythingThere(self):
+ """
+ L{twisted.application.internet} dynamically defines a set of
+ L{service.Service} subclasses that in general have corresponding
+ reactor.listenXXX or reactor.connectXXX calls.
+ """
+ trans = 'TCP UNIX SSL UDP UNIXDatagram Multicast'.split()
+ for tran in trans[:]:
+ if not getattr(interfaces, "IReactor" + tran)(reactor, None):
+ trans.remove(tran)
+ if interfaces.IReactorArbitrary(reactor, None) is not None:
+ trans.insert(0, "Generic")
+ for tran in trans:
+ for side in 'Server Client'.split():
+ if tran == "Multicast" and side == "Client":
+ continue
+ self.assertTrue(hasattr(internet, tran + side))
+ method = getattr(internet, tran + side).method
+ prefix = {'Server': 'listen', 'Client': 'connect'}[side]
+ self.assertTrue(hasattr(reactor, prefix + method) or
+ (prefix == "connect" and method == "UDP"))
+ o = getattr(internet, tran + side)()
+ self.assertEqual(service.IService(o), o)
+ test_everythingThere.suppress = [
+ util.suppress(message='GenericServer was deprecated in Twisted 10.1.',
+ category=DeprecationWarning),
+ util.suppress(message='GenericClient was deprecated in Twisted 10.1.',
+ category=DeprecationWarning),
+ util.suppress(message='twisted.internet.interfaces.IReactorArbitrary was '
+ 'deprecated in Twisted 10.1.0: See IReactorFDSet.')]
+
+
+ def test_importAll(self):
+ """
+ L{twisted.application.internet} dynamically defines L{service.Service}
+ subclasses. This test ensures that the subclasses exposed by C{__all__}
+ are valid attributes of the module.
+ """
+ for cls in internet.__all__:
+ self.assertTrue(
+ hasattr(internet, cls),
+ '%s not importable from twisted.application.internet' % (cls,))
+
+
+ def test_reactorParametrizationInServer(self):
+ """
+ L{internet._AbstractServer} supports a C{reactor} keyword argument
+ that can be used to parametrize the reactor used to listen for
+ connections.
+ """
+ reactor = MemoryReactor()
+
+ factory = object()
+ t = internet.TCPServer(1234, factory, reactor=reactor)
+ t.startService()
+ self.assertEqual(reactor.tcpServers.pop()[:2], (1234, factory))
+
+
+ def test_reactorParametrizationInClient(self):
+ """
+ L{internet._AbstractClient} supports a C{reactor} keyword arguments
+ that can be used to parametrize the reactor used to create new client
+ connections.
+ """
+ reactor = MemoryReactor()
+
+ factory = protocol.ClientFactory()
+ t = internet.TCPClient('127.0.0.1', 1234, factory, reactor=reactor)
+ t.startService()
+ self.assertEqual(
+ reactor.tcpClients.pop()[:3], ('127.0.0.1', 1234, factory))
+
+
+ def test_reactorParametrizationInServerMultipleStart(self):
+ """
+ Like L{test_reactorParametrizationInServer}, but stop and restart the
+ service and check that the given reactor is still used.
+ """
+ reactor = MemoryReactor()
+
+ factory = protocol.Factory()
+ t = internet.TCPServer(1234, factory, reactor=reactor)
+ t.startService()
+ self.assertEqual(reactor.tcpServers.pop()[:2], (1234, factory))
+ t.stopService()
+ t.startService()
+ self.assertEqual(reactor.tcpServers.pop()[:2], (1234, factory))
+
+
+ def test_reactorParametrizationInClientMultipleStart(self):
+ """
+ Like L{test_reactorParametrizationInClient}, but stop and restart the
+ service and check that the given reactor is still used.
+ """
+ reactor = MemoryReactor()
+
+ factory = protocol.ClientFactory()
+ t = internet.TCPClient('127.0.0.1', 1234, factory, reactor=reactor)
+ t.startService()
+ self.assertEqual(
+ reactor.tcpClients.pop()[:3], ('127.0.0.1', 1234, factory))
+ t.stopService()
+ t.startService()
+ self.assertEqual(
+ reactor.tcpClients.pop()[:3], ('127.0.0.1', 1234, factory))
+
+
+
+class TestTimerBasic(unittest.TestCase):
+
+ def testTimerRuns(self):
+ d = defer.Deferred()
+ self.t = internet.TimerService(1, d.callback, 'hello')
+ self.t.startService()
+ d.addCallback(self.assertEqual, 'hello')
+ d.addCallback(lambda x : self.t.stopService())
+ d.addCallback(lambda x : self.failIf(self.t.running))
+ return d
+
+ def tearDown(self):
+ return self.t.stopService()
+
+ def testTimerRestart(self):
+ # restart the same TimerService
+ d1 = defer.Deferred()
+ d2 = defer.Deferred()
+ work = [(d2, "bar"), (d1, "foo")]
+ def trigger():
+ d, arg = work.pop()
+ d.callback(arg)
+ self.t = internet.TimerService(1, trigger)
+ self.t.startService()
+ def onFirstResult(result):
+ self.assertEqual(result, 'foo')
+ return self.t.stopService()
+ def onFirstStop(ignored):
+ self.failIf(self.t.running)
+ self.t.startService()
+ return d2
+ def onSecondResult(result):
+ self.assertEqual(result, 'bar')
+ self.t.stopService()
+ d1.addCallback(onFirstResult)
+ d1.addCallback(onFirstStop)
+ d1.addCallback(onSecondResult)
+ return d1
+
+ def testTimerLoops(self):
+ l = []
+ def trigger(data, number, d):
+ l.append(data)
+ if len(l) == number:
+ d.callback(l)
+ d = defer.Deferred()
+ self.t = internet.TimerService(0.01, trigger, "hello", 10, d)
+ self.t.startService()
+ d.addCallback(self.assertEqual, ['hello'] * 10)
+ d.addCallback(lambda x : self.t.stopService())
+ return d
+
+
+class FakeReactor(reactors.Reactor):
+ """
+ A fake reactor with a hooked install method.
+ """
+
+ def __init__(self, install, *args, **kwargs):
+ """
+ @param install: any callable that will be used as install method.
+ @type install: C{callable}
+ """
+ reactors.Reactor.__init__(self, *args, **kwargs)
+ self.install = install
+
+
+
+class PluggableReactorTestCase(unittest.TestCase):
+ """
+ Tests for the reactor discovery/inspection APIs.
+ """
+
+ def setUp(self):
+ """
+ Override the L{reactors.getPlugins} function, normally bound to
+ L{twisted.plugin.getPlugins}, in order to control which
+ L{IReactorInstaller} plugins are seen as available.
+
+ C{self.pluginResults} can be customized and will be used as the
+ result of calls to C{reactors.getPlugins}.
+ """
+ self.pluginCalls = []
+ self.pluginResults = []
+ self.originalFunction = reactors.getPlugins
+ reactors.getPlugins = self._getPlugins
+
+
+ def tearDown(self):
+ """
+ Restore the original L{reactors.getPlugins}.
+ """
+ reactors.getPlugins = self.originalFunction
+
+
+ def _getPlugins(self, interface, package=None):
+ """
+ Stand-in for the real getPlugins method which records its arguments
+ and returns a fixed result.
+ """
+ self.pluginCalls.append((interface, package))
+ return list(self.pluginResults)
+
+
+ def test_getPluginReactorTypes(self):
+ """
+ Test that reactor plugins are returned from L{getReactorTypes}
+ """
+ name = 'fakereactortest'
+ package = __name__ + '.fakereactor'
+ description = 'description'
+ self.pluginResults = [reactors.Reactor(name, package, description)]
+ reactorTypes = reactors.getReactorTypes()
+
+ self.assertEqual(
+ self.pluginCalls,
+ [(reactors.IReactorInstaller, None)])
+
+ for r in reactorTypes:
+ if r.shortName == name:
+ self.assertEqual(r.description, description)
+ break
+ else:
+ self.fail("Reactor plugin not present in getReactorTypes() result")
+
+
+ def test_reactorInstallation(self):
+ """
+ Test that L{reactors.Reactor.install} loads the correct module and
+ calls its install attribute.
+ """
+ installed = []
+ def install():
+ installed.append(True)
+ installer = FakeReactor(install,
+ 'fakereactortest', __name__, 'described')
+ installer.install()
+ self.assertEqual(installed, [True])
+
+
+ def test_installReactor(self):
+ """
+ Test that the L{reactors.installReactor} function correctly installs
+ the specified reactor.
+ """
+ installed = []
+ def install():
+ installed.append(True)
+ name = 'fakereactortest'
+ package = __name__
+ description = 'description'
+ self.pluginResults = [FakeReactor(install, name, package, description)]
+ reactors.installReactor(name)
+ self.assertEqual(installed, [True])
+
+
+ def test_installNonExistentReactor(self):
+ """
+ Test that L{reactors.installReactor} raises L{reactors.NoSuchReactor}
+ when asked to install a reactor which it cannot find.
+ """
+ self.pluginResults = []
+ self.assertRaises(
+ reactors.NoSuchReactor,
+ reactors.installReactor, 'somereactor')
+
+
+ def test_installNotAvailableReactor(self):
+ """
+ Test that L{reactors.installReactor} raises an exception when asked to
+ install a reactor which doesn't work in this environment.
+ """
+ def install():
+ raise ImportError("Missing foo bar")
+ name = 'fakereactortest'
+ package = __name__
+ description = 'description'
+ self.pluginResults = [FakeReactor(install, name, package, description)]
+ self.assertRaises(ImportError, reactors.installReactor, name)
+
+
+ def test_reactorSelectionMixin(self):
+ """
+ Test that the reactor selected is installed as soon as possible, ie
+ when the option is parsed.
+ """
+ executed = []
+ INSTALL_EVENT = 'reactor installed'
+ SUBCOMMAND_EVENT = 'subcommands loaded'
+
+ class ReactorSelectionOptions(usage.Options, app.ReactorSelectionMixin):
+ def subCommands(self):
+ executed.append(SUBCOMMAND_EVENT)
+ return [('subcommand', None, lambda: self, 'test subcommand')]
+ subCommands = property(subCommands)
+
+ def install():
+ executed.append(INSTALL_EVENT)
+ self.pluginResults = [
+ FakeReactor(install, 'fakereactortest', __name__, 'described')
+ ]
+
+ options = ReactorSelectionOptions()
+ options.parseOptions(['--reactor', 'fakereactortest', 'subcommand'])
+ self.assertEqual(executed[0], INSTALL_EVENT)
+ self.assertEqual(executed.count(INSTALL_EVENT), 1)
+ self.assertEqual(options["reactor"], "fakereactortest")
+
+
+ def test_reactorSelectionMixinNonExistent(self):
+ """
+ Test that the usage mixin exits when trying to use a non existent
+ reactor (the name not matching to any reactor), giving an error
+ message.
+ """
+ class ReactorSelectionOptions(usage.Options, app.ReactorSelectionMixin):
+ pass
+ self.pluginResults = []
+
+ options = ReactorSelectionOptions()
+ options.messageOutput = StringIO()
+ e = self.assertRaises(usage.UsageError, options.parseOptions,
+ ['--reactor', 'fakereactortest', 'subcommand'])
+ self.assertIn("fakereactortest", e.args[0])
+ self.assertIn("help-reactors", e.args[0])
+
+
+ def test_reactorSelectionMixinNotAvailable(self):
+ """
+ Test that the usage mixin exits when trying to use a reactor not
+ available (the reactor raises an error at installation), giving an
+ error message.
+ """
+ class ReactorSelectionOptions(usage.Options, app.ReactorSelectionMixin):
+ pass
+ message = "Missing foo bar"
+ def install():
+ raise ImportError(message)
+
+ name = 'fakereactortest'
+ package = __name__
+ description = 'description'
+ self.pluginResults = [FakeReactor(install, name, package, description)]
+
+ options = ReactorSelectionOptions()
+ options.messageOutput = StringIO()
+ e = self.assertRaises(usage.UsageError, options.parseOptions,
+ ['--reactor', 'fakereactortest', 'subcommand'])
+ self.assertIn(message, e.args[0])
+ self.assertIn("help-reactors", e.args[0])
diff --git a/twisted/test/test_banana.py b/twisted/test/test_banana.py
new file mode 100644
index 0000000..88ad2e6
--- /dev/null
+++ b/twisted/test/test_banana.py
@@ -0,0 +1,278 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import StringIO
+import sys
+
+# Twisted Imports
+from twisted.trial import unittest
+from twisted.spread import banana
+from twisted.python import failure
+from twisted.internet import protocol, main
+
+
+class MathTestCase(unittest.TestCase):
+ def testInt2b128(self):
+ funkylist = range(0,100) + range(1000,1100) + range(1000000,1000100) + [1024 **10l]
+ for i in funkylist:
+ x = StringIO.StringIO()
+ banana.int2b128(i, x.write)
+ v = x.getvalue()
+ y = banana.b1282int(v)
+ assert y == i, "y = %s; i = %s" % (y,i)
+
+class BananaTestCase(unittest.TestCase):
+
+ encClass = banana.Banana
+
+ def setUp(self):
+ self.io = StringIO.StringIO()
+ self.enc = self.encClass()
+ self.enc.makeConnection(protocol.FileWrapper(self.io))
+ self.enc._selectDialect("none")
+ self.enc.expressionReceived = self.putResult
+
+ def putResult(self, result):
+ self.result = result
+
+ def tearDown(self):
+ self.enc.connectionLost(failure.Failure(main.CONNECTION_DONE))
+ del self.enc
+
+ def testString(self):
+ self.enc.sendEncoded("hello")
+ l = []
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == 'hello'
+
+ def test_int(self):
+ """
+ A positive integer less than 2 ** 32 should round-trip through
+ banana without changing value and should come out represented
+ as an C{int} (regardless of the type which was encoded).
+ """
+ for value in (10151, 10151L):
+ self.enc.sendEncoded(value)
+ self.enc.dataReceived(self.io.getvalue())
+ self.assertEqual(self.result, 10151)
+ self.assertIsInstance(self.result, int)
+
+
+ def test_largeLong(self):
+ """
+ Integers greater than 2 ** 32 and less than -2 ** 32 should
+ round-trip through banana without changing value and should
+ come out represented as C{int} instances if the value fits
+ into that type on the receiving platform.
+ """
+ for exp in (32, 64, 128, 256):
+ for add in (0, 1):
+ m = 2 ** exp + add
+ for n in (m, -m-1):
+ self.io.truncate(0)
+ self.enc.sendEncoded(n)
+ self.enc.dataReceived(self.io.getvalue())
+ self.assertEqual(self.result, n)
+ if n > sys.maxint or n < -sys.maxint - 1:
+ self.assertIsInstance(self.result, long)
+ else:
+ self.assertIsInstance(self.result, int)
+
+
+ def _getSmallest(self):
+ # How many bytes of prefix our implementation allows
+ bytes = self.enc.prefixLimit
+ # How many useful bits we can extract from that based on Banana's
+ # base-128 representation.
+ bits = bytes * 7
+ # The largest number we _should_ be able to encode
+ largest = 2 ** bits - 1
+ # The smallest number we _shouldn't_ be able to encode
+ smallest = largest + 1
+ return smallest
+
+
+ def test_encodeTooLargeLong(self):
+ """
+ Test that a long above the implementation-specific limit is rejected
+ as too large to be encoded.
+ """
+ smallest = self._getSmallest()
+ self.assertRaises(banana.BananaError, self.enc.sendEncoded, smallest)
+
+
+ def test_decodeTooLargeLong(self):
+ """
+ Test that a long above the implementation specific limit is rejected
+ as too large to be decoded.
+ """
+ smallest = self._getSmallest()
+ self.enc.setPrefixLimit(self.enc.prefixLimit * 2)
+ self.enc.sendEncoded(smallest)
+ encoded = self.io.getvalue()
+ self.io.truncate(0)
+ self.enc.setPrefixLimit(self.enc.prefixLimit / 2)
+
+ self.assertRaises(banana.BananaError, self.enc.dataReceived, encoded)
+
+
+ def _getLargest(self):
+ return -self._getSmallest()
+
+
+ def test_encodeTooSmallLong(self):
+ """
+ Test that a negative long below the implementation-specific limit is
+ rejected as too small to be encoded.
+ """
+ largest = self._getLargest()
+ self.assertRaises(banana.BananaError, self.enc.sendEncoded, largest)
+
+
+ def test_decodeTooSmallLong(self):
+ """
+ Test that a negative long below the implementation specific limit is
+ rejected as too small to be decoded.
+ """
+ largest = self._getLargest()
+ self.enc.setPrefixLimit(self.enc.prefixLimit * 2)
+ self.enc.sendEncoded(largest)
+ encoded = self.io.getvalue()
+ self.io.truncate(0)
+ self.enc.setPrefixLimit(self.enc.prefixLimit / 2)
+
+ self.assertRaises(banana.BananaError, self.enc.dataReceived, encoded)
+
+
+ def testNegativeLong(self):
+ self.enc.sendEncoded(-1015l)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == -1015l, "should be -1015l, got %s" % self.result
+
+ def testInteger(self):
+ self.enc.sendEncoded(1015)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == 1015, "should be 1015, got %s" % self.result
+
+ def testNegative(self):
+ self.enc.sendEncoded(-1015)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == -1015, "should be -1015, got %s" % self.result
+
+ def testFloat(self):
+ self.enc.sendEncoded(1015.)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == 1015.
+
+ def testList(self):
+ foo = [1, 2, [3, 4], [30.5, 40.2], 5, ["six", "seven", ["eight", 9]], [10], []]
+ self.enc.sendEncoded(foo)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == foo, "%s!=%s" % (repr(self.result), repr(self.result))
+
+ def testPartial(self):
+ foo = [1, 2, [3, 4], [30.5, 40.2], 5,
+ ["six", "seven", ["eight", 9]], [10],
+ # TODO: currently the C implementation's a bit buggy...
+ sys.maxint * 3l, sys.maxint * 2l, sys.maxint * -2l]
+ self.enc.sendEncoded(foo)
+ for byte in self.io.getvalue():
+ self.enc.dataReceived(byte)
+ assert self.result == foo, "%s!=%s" % (repr(self.result), repr(foo))
+
+ def feed(self, data):
+ for byte in data:
+ self.enc.dataReceived(byte)
+ def testOversizedList(self):
+ data = '\x02\x01\x01\x01\x01\x80'
+ # list(size=0x0101010102, about 4.3e9)
+ self.failUnlessRaises(banana.BananaError, self.feed, data)
+ def testOversizedString(self):
+ data = '\x02\x01\x01\x01\x01\x82'
+ # string(size=0x0101010102, about 4.3e9)
+ self.failUnlessRaises(banana.BananaError, self.feed, data)
+
+ def testCrashString(self):
+ crashString = '\x00\x00\x00\x00\x04\x80'
+ # string(size=0x0400000000, about 17.2e9)
+
+ # cBanana would fold that into a 32-bit 'int', then try to allocate
+ # a list with PyList_New(). cBanana ignored the NULL return value,
+ # so it would segfault when trying to free the imaginary list.
+
+ # This variant doesn't segfault straight out in my environment.
+ # Instead, it takes up large amounts of CPU and memory...
+ #crashString = '\x00\x00\x00\x00\x01\x80'
+ # print repr(crashString)
+ #self.failUnlessRaises(Exception, self.enc.dataReceived, crashString)
+ try:
+ # should now raise MemoryError
+ self.enc.dataReceived(crashString)
+ except banana.BananaError:
+ pass
+
+ def testCrashNegativeLong(self):
+ # There was a bug in cBanana which relied on negating a negative integer
+ # always giving a postive result, but for the lowest possible number in
+ # 2s-complement arithmetic, that's not true, i.e.
+ # long x = -2147483648;
+ # long y = -x;
+ # x == y; /* true! */
+ # (assuming 32-bit longs)
+ self.enc.sendEncoded(-2147483648)
+ self.enc.dataReceived(self.io.getvalue())
+ assert self.result == -2147483648, "should be -2147483648, got %s" % self.result
+
+
+ def test_sizedIntegerTypes(self):
+ """
+ Test that integers below the maximum C{INT} token size cutoff are
+ serialized as C{INT} or C{NEG} and that larger integers are
+ serialized as C{LONGINT} or C{LONGNEG}.
+ """
+ def encoded(n):
+ self.io.seek(0)
+ self.io.truncate()
+ self.enc.sendEncoded(n)
+ return self.io.getvalue()
+
+ baseIntIn = +2147483647
+ baseNegIn = -2147483648
+
+ baseIntOut = '\x7f\x7f\x7f\x07\x81'
+ self.assertEqual(encoded(baseIntIn - 2), '\x7d' + baseIntOut)
+ self.assertEqual(encoded(baseIntIn - 1), '\x7e' + baseIntOut)
+ self.assertEqual(encoded(baseIntIn - 0), '\x7f' + baseIntOut)
+
+ baseLongIntOut = '\x00\x00\x00\x08\x85'
+ self.assertEqual(encoded(baseIntIn + 1), '\x00' + baseLongIntOut)
+ self.assertEqual(encoded(baseIntIn + 2), '\x01' + baseLongIntOut)
+ self.assertEqual(encoded(baseIntIn + 3), '\x02' + baseLongIntOut)
+
+ baseNegOut = '\x7f\x7f\x7f\x07\x83'
+ self.assertEqual(encoded(baseNegIn + 2), '\x7e' + baseNegOut)
+ self.assertEqual(encoded(baseNegIn + 1), '\x7f' + baseNegOut)
+ self.assertEqual(encoded(baseNegIn + 0), '\x00\x00\x00\x00\x08\x83')
+
+ baseLongNegOut = '\x00\x00\x00\x08\x86'
+ self.assertEqual(encoded(baseNegIn - 1), '\x01' + baseLongNegOut)
+ self.assertEqual(encoded(baseNegIn - 2), '\x02' + baseLongNegOut)
+ self.assertEqual(encoded(baseNegIn - 3), '\x03' + baseLongNegOut)
+
+
+
+class GlobalCoderTests(unittest.TestCase):
+ """
+ Tests for the free functions L{banana.encode} and L{banana.decode}.
+ """
+ def test_statelessDecode(self):
+ """
+ Test that state doesn't carry over between calls to L{banana.decode}.
+ """
+ # Banana encoding of 2 ** 449
+ undecodable = '\x7f' * 65 + '\x85'
+ self.assertRaises(banana.BananaError, banana.decode, undecodable)
+
+ # Banana encoding of 1
+ decodable = '\x01\x81'
+ self.assertEqual(banana.decode(decodable), 1)
diff --git a/twisted/test/test_compat.py b/twisted/test/test_compat.py
new file mode 100644
index 0000000..aae49a7
--- /dev/null
+++ b/twisted/test/test_compat.py
@@ -0,0 +1,199 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Tests for L{twisted.python.compat}.
+"""
+
+import types, socket
+
+from twisted.trial import unittest
+
+from twisted.python.compat import set, frozenset, reduce
+
+
+
+class IterableCounter:
+ def __init__(self, lim=0):
+ self.lim = lim
+ self.i = -1
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ self.i += 1
+ if self.i >= self.lim:
+ raise StopIteration
+ return self.i
+
+class CompatTestCase(unittest.TestCase):
+ def testDict(self):
+ d1 = {'a': 'b'}
+ d2 = dict(d1)
+ self.assertEqual(d1, d2)
+ d1['a'] = 'c'
+ self.assertNotEquals(d1, d2)
+ d2 = dict(d1.items())
+ self.assertEqual(d1, d2)
+
+ def testBool(self):
+ self.assertEqual(bool('hi'), True)
+ self.assertEqual(bool(True), True)
+ self.assertEqual(bool(''), False)
+ self.assertEqual(bool(False), False)
+
+ def testIteration(self):
+ lst1, lst2 = range(10), []
+
+ for i in iter(lst1):
+ lst2.append(i)
+ self.assertEqual(lst1, lst2)
+ del lst2[:]
+
+ try:
+ iterable = iter(lst1)
+ while 1:
+ lst2.append(iterable.next())
+ except StopIteration:
+ pass
+ self.assertEqual(lst1, lst2)
+ del lst2[:]
+
+ for i in iter(IterableCounter(10)):
+ lst2.append(i)
+ self.assertEqual(lst1, lst2)
+ del lst2[:]
+
+ try:
+ iterable = iter(IterableCounter(10))
+ while 1:
+ lst2.append(iterable.next())
+ except StopIteration:
+ pass
+ self.assertEqual(lst1, lst2)
+ del lst2[:]
+
+ for i in iter(IterableCounter(20).next, 10):
+ lst2.append(i)
+ self.assertEqual(lst1, lst2)
+
+ def testIsinstance(self):
+ self.assert_(isinstance(u'hi', types.StringTypes))
+ self.assert_(isinstance(self, unittest.TestCase))
+ # I'm pretty sure it's impossible to implement this
+ # without replacing isinstance on 2.2 as well :(
+ # self.assert_(isinstance({}, dict))
+
+ def testStrip(self):
+ self.assertEqual(' x '.lstrip(' '), 'x ')
+ self.assertEqual(' x x'.lstrip(' '), 'x x')
+ self.assertEqual(' x '.rstrip(' '), ' x')
+ self.assertEqual('x x '.rstrip(' '), 'x x')
+
+ self.assertEqual('\t x '.lstrip('\t '), 'x ')
+ self.assertEqual(' \tx x'.lstrip('\t '), 'x x')
+ self.assertEqual(' x\t '.rstrip(' \t'), ' x')
+ self.assertEqual('x x \t'.rstrip(' \t'), 'x x')
+
+ self.assertEqual('\t x '.strip('\t '), 'x')
+ self.assertEqual(' \tx x'.strip('\t '), 'x x')
+ self.assertEqual(' x\t '.strip(' \t'), 'x')
+ self.assertEqual('x x \t'.strip(' \t'), 'x x')
+
+ def testNToP(self):
+ from twisted.python.compat import inet_ntop
+
+ f = lambda a: inet_ntop(socket.AF_INET6, a)
+ g = lambda a: inet_ntop(socket.AF_INET, a)
+
+ self.assertEqual('::', f('\x00' * 16))
+ self.assertEqual('::1', f('\x00' * 15 + '\x01'))
+ self.assertEqual(
+ 'aef:b01:506:1001:ffff:9997:55:170',
+ f('\x0a\xef\x0b\x01\x05\x06\x10\x01\xff\xff\x99\x97\x00\x55\x01\x70'))
+
+ self.assertEqual('1.0.1.0', g('\x01\x00\x01\x00'))
+ self.assertEqual('170.85.170.85', g('\xaa\x55\xaa\x55'))
+ self.assertEqual('255.255.255.255', g('\xff\xff\xff\xff'))
+
+ self.assertEqual('100::', f('\x01' + '\x00' * 15))
+ self.assertEqual('100::1', f('\x01' + '\x00' * 14 + '\x01'))
+
+ def testPToN(self):
+ from twisted.python.compat import inet_pton
+
+ f = lambda a: inet_pton(socket.AF_INET6, a)
+ g = lambda a: inet_pton(socket.AF_INET, a)
+
+ self.assertEqual('\x00\x00\x00\x00', g('0.0.0.0'))
+ self.assertEqual('\xff\x00\xff\x00', g('255.0.255.0'))
+ self.assertEqual('\xaa\xaa\xaa\xaa', g('170.170.170.170'))
+
+ self.assertEqual('\x00' * 16, f('::'))
+ self.assertEqual('\x00' * 16, f('0::0'))
+ self.assertEqual('\x00\x01' + '\x00' * 14, f('1::'))
+ self.assertEqual(
+ '\x45\xef\x76\xcb\x00\x1a\x56\xef\xaf\xeb\x0b\xac\x19\x24\xae\xae',
+ f('45ef:76cb:1a:56ef:afeb:bac:1924:aeae'))
+
+ self.assertEqual('\x00' * 14 + '\x00\x01', f('::1'))
+ self.assertEqual('\x00' * 12 + '\x01\x02\x03\x04', f('::1.2.3.4'))
+ self.assertEqual(
+ '\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x01\x02\x03\xff',
+ f('1:2:3:4:5:6:1.2.3.255'))
+
+ for badaddr in ['1:2:3:4:5:6:7:8:', ':1:2:3:4:5:6:7:8', '1::2::3',
+ '1:::3', ':::', '1:2', '::1.2', '1.2.3.4::',
+ 'abcd:1.2.3.4:abcd:abcd:abcd:abcd:abcd',
+ '1234:1.2.3.4:1234:1234:1234:1234:1234:1234',
+ '1.2.3.4']:
+ self.assertRaises(ValueError, f, badaddr)
+
+ def test_set(self):
+ """
+ L{set} should behave like the expected set interface.
+ """
+ a = set()
+ a.add('b')
+ a.add('c')
+ a.add('a')
+ b = list(a)
+ b.sort()
+ self.assertEqual(b, ['a', 'b', 'c'])
+ a.remove('b')
+ b = list(a)
+ b.sort()
+ self.assertEqual(b, ['a', 'c'])
+
+ a.discard('d')
+
+ b = set(['r', 's'])
+ d = a.union(b)
+ b = list(d)
+ b.sort()
+ self.assertEqual(b, ['a', 'c', 'r', 's'])
+
+
+ def test_frozenset(self):
+ """
+ L{frozenset} should behave like the expected frozenset interface.
+ """
+ a = frozenset(['a', 'b'])
+ self.assertRaises(AttributeError, getattr, a, "add")
+ self.assertEqual(list(a), ['a', 'b'])
+
+ b = frozenset(['r', 's'])
+ d = a.union(b)
+ b = list(d)
+ b.sort()
+ self.assertEqual(b, ['a', 'b', 'r', 's'])
+
+
+ def test_reduce(self):
+ """
+ L{reduce} should behave like the builtin reduce.
+ """
+ self.assertEqual(15, reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]))
+ self.assertEqual(16, reduce(lambda x, y: x + y, [1, 2, 3, 4, 5], 1))
diff --git a/twisted/test/test_context.py b/twisted/test/test_context.py
new file mode 100644
index 0000000..c8d25f7
--- /dev/null
+++ b/twisted/test/test_context.py
@@ -0,0 +1,15 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python import context
+
+class ContextTest(TestCase):
+
+ def testBasicContext(self):
+ self.assertEqual(context.get("x"), None)
+ self.assertEqual(context.call({"x": "y"}, context.get, "x"), "y")
+ self.assertEqual(context.get("x"), None)
diff --git a/twisted/test/test_cooperator.py b/twisted/test/test_cooperator.py
new file mode 100644
index 0000000..260ce03
--- /dev/null
+++ b/twisted/test/test_cooperator.py
@@ -0,0 +1,669 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+This module contains tests for L{twisted.internet.task.Cooperator} and
+related functionality.
+"""
+
+from twisted.internet import reactor, defer, task
+from twisted.trial import unittest
+
+
+
+class FakeDelayedCall(object):
+ """
+ Fake delayed call which lets us simulate the scheduler.
+ """
+ def __init__(self, func):
+ """
+ A function to run, later.
+ """
+ self.func = func
+ self.cancelled = False
+
+
+ def cancel(self):
+ """
+ Don't run my function later.
+ """
+ self.cancelled = True
+
+
+
+class FakeScheduler(object):
+ """
+ A fake scheduler for testing against.
+ """
+ def __init__(self):
+ """
+ Create a fake scheduler with a list of work to do.
+ """
+ self.work = []
+
+
+ def __call__(self, thunk):
+ """
+ Schedule a unit of work to be done later.
+ """
+ unit = FakeDelayedCall(thunk)
+ self.work.append(unit)
+ return unit
+
+
+ def pump(self):
+ """
+ Do all of the work that is currently available to be done.
+ """
+ work, self.work = self.work, []
+ for unit in work:
+ if not unit.cancelled:
+ unit.func()
+
+
+
+class TestCooperator(unittest.TestCase):
+ RESULT = 'done'
+
+ def ebIter(self, err):
+ err.trap(task.SchedulerStopped)
+ return self.RESULT
+
+
+ def cbIter(self, ign):
+ self.fail()
+
+
+ def testStoppedRejectsNewTasks(self):
+ """
+ Test that Cooperators refuse new tasks when they have been stopped.
+ """
+ def testwith(stuff):
+ c = task.Cooperator()
+ c.stop()
+ d = c.coiterate(iter(()), stuff)
+ d.addCallback(self.cbIter)
+ d.addErrback(self.ebIter)
+ return d.addCallback(lambda result:
+ self.assertEqual(result, self.RESULT))
+ return testwith(None).addCallback(lambda ign: testwith(defer.Deferred()))
+
+
+ def testStopRunning(self):
+ """
+ Test that a running iterator will not run to completion when the
+ cooperator is stopped.
+ """
+ c = task.Cooperator()
+ def myiter():
+ for myiter.value in range(3):
+ yield myiter.value
+ myiter.value = -1
+ d = c.coiterate(myiter())
+ d.addCallback(self.cbIter)
+ d.addErrback(self.ebIter)
+ c.stop()
+ def doasserts(result):
+ self.assertEqual(result, self.RESULT)
+ self.assertEqual(myiter.value, -1)
+ d.addCallback(doasserts)
+ return d
+
+
+ def testStopOutstanding(self):
+ """
+ An iterator run with L{Cooperator.coiterate} paused on a L{Deferred}
+ yielded by that iterator will fire its own L{Deferred} (the one
+ returned by C{coiterate}) when L{Cooperator.stop} is called.
+ """
+ testControlD = defer.Deferred()
+ outstandingD = defer.Deferred()
+ def myiter():
+ reactor.callLater(0, testControlD.callback, None)
+ yield outstandingD
+ self.fail()
+ c = task.Cooperator()
+ d = c.coiterate(myiter())
+ def stopAndGo(ign):
+ c.stop()
+ outstandingD.callback('arglebargle')
+
+ testControlD.addCallback(stopAndGo)
+ d.addCallback(self.cbIter)
+ d.addErrback(self.ebIter)
+
+ return d.addCallback(
+ lambda result: self.assertEqual(result, self.RESULT))
+
+
+ def testUnexpectedError(self):
+ c = task.Cooperator()
+ def myiter():
+ if 0:
+ yield None
+ else:
+ raise RuntimeError()
+ d = c.coiterate(myiter())
+ return self.assertFailure(d, RuntimeError)
+
+
+ def testUnexpectedErrorActuallyLater(self):
+ def myiter():
+ D = defer.Deferred()
+ reactor.callLater(0, D.errback, RuntimeError())
+ yield D
+
+ c = task.Cooperator()
+ d = c.coiterate(myiter())
+ return self.assertFailure(d, RuntimeError)
+
+
+ def testUnexpectedErrorNotActuallyLater(self):
+ def myiter():
+ yield defer.fail(RuntimeError())
+
+ c = task.Cooperator()
+ d = c.coiterate(myiter())
+ return self.assertFailure(d, RuntimeError)
+
+
+ def testCooperation(self):
+ L = []
+ def myiter(things):
+ for th in things:
+ L.append(th)
+ yield None
+
+ groupsOfThings = ['abc', (1, 2, 3), 'def', (4, 5, 6)]
+
+ c = task.Cooperator()
+ tasks = []
+ for stuff in groupsOfThings:
+ tasks.append(c.coiterate(myiter(stuff)))
+
+ return defer.DeferredList(tasks).addCallback(
+ lambda ign: self.assertEqual(tuple(L), sum(zip(*groupsOfThings), ())))
+
+
+ def testResourceExhaustion(self):
+ output = []
+ def myiter():
+ for i in range(100):
+ output.append(i)
+ if i == 9:
+ _TPF.stopped = True
+ yield i
+
+ class _TPF:
+ stopped = False
+ def __call__(self):
+ return self.stopped
+
+ c = task.Cooperator(terminationPredicateFactory=_TPF)
+ c.coiterate(myiter()).addErrback(self.ebIter)
+ c._delayedCall.cancel()
+ # testing a private method because only the test case will ever care
+ # about this, so we have to carefully clean up after ourselves.
+ c._tick()
+ c.stop()
+ self.failUnless(_TPF.stopped)
+ self.assertEqual(output, range(10))
+
+
+ def testCallbackReCoiterate(self):
+ """
+ If a callback to a deferred returned by coiterate calls coiterate on
+ the same Cooperator, we should make sure to only do the minimal amount
+ of scheduling work. (This test was added to demonstrate a specific bug
+ that was found while writing the scheduler.)
+ """
+ calls = []
+
+ class FakeCall:
+ def __init__(self, func):
+ self.func = func
+
+ def __repr__(self):
+ return '<FakeCall %r>' % (self.func,)
+
+ def sched(f):
+ self.failIf(calls, repr(calls))
+ calls.append(FakeCall(f))
+ return calls[-1]
+
+ c = task.Cooperator(scheduler=sched, terminationPredicateFactory=lambda: lambda: True)
+ d = c.coiterate(iter(()))
+
+ done = []
+ def anotherTask(ign):
+ c.coiterate(iter(())).addBoth(done.append)
+
+ d.addCallback(anotherTask)
+
+ work = 0
+ while not done:
+ work += 1
+ while calls:
+ calls.pop(0).func()
+ work += 1
+ if work > 50:
+ self.fail("Cooperator took too long")
+
+
+ def test_removingLastTaskStopsScheduledCall(self):
+ """
+ If the last task in a Cooperator is removed, the scheduled call for
+ the next tick is cancelled, since it is no longer necessary.
+
+ This behavior is useful for tests that want to assert they have left
+ no reactor state behind when they're done.
+ """
+ calls = [None]
+ def sched(f):
+ calls[0] = FakeDelayedCall(f)
+ return calls[0]
+ coop = task.Cooperator(scheduler=sched)
+
+ # Add two task; this should schedule the tick:
+ task1 = coop.cooperate(iter([1, 2]))
+ task2 = coop.cooperate(iter([1, 2]))
+ self.assertEqual(calls[0].func, coop._tick)
+
+ # Remove first task; scheduled call should still be going:
+ task1.stop()
+ self.assertEqual(calls[0].cancelled, False)
+ self.assertEqual(coop._delayedCall, calls[0])
+
+ # Remove second task; scheduled call should be cancelled:
+ task2.stop()
+ self.assertEqual(calls[0].cancelled, True)
+ self.assertEqual(coop._delayedCall, None)
+
+ # Add another task; scheduled call will be recreated:
+ task3 = coop.cooperate(iter([1, 2]))
+ self.assertEqual(calls[0].cancelled, False)
+ self.assertEqual(coop._delayedCall, calls[0])
+
+
+
+class UnhandledException(Exception):
+ """
+ An exception that should go unhandled.
+ """
+
+
+
+class AliasTests(unittest.TestCase):
+ """
+ Integration test to verify that the global singleton aliases do what
+ they're supposed to.
+ """
+
+ def test_cooperate(self):
+ """
+ L{twisted.internet.task.cooperate} ought to run the generator that it is
+ """
+ d = defer.Deferred()
+ def doit():
+ yield 1
+ yield 2
+ yield 3
+ d.callback("yay")
+ it = doit()
+ theTask = task.cooperate(it)
+ self.assertIn(theTask, task._theCooperator._tasks)
+ return d
+
+
+
+class RunStateTests(unittest.TestCase):
+ """
+ Tests to verify the behavior of L{CooperativeTask.pause},
+ L{CooperativeTask.resume}, L{CooperativeTask.stop}, exhausting the
+ underlying iterator, and their interactions with each other.
+ """
+
+ def setUp(self):
+ """
+ Create a cooperator with a fake scheduler and a termination predicate
+ that ensures only one unit of work will take place per tick.
+ """
+ self._doDeferNext = False
+ self._doStopNext = False
+ self._doDieNext = False
+ self.work = []
+ self.scheduler = FakeScheduler()
+ self.cooperator = task.Cooperator(
+ scheduler=self.scheduler,
+ # Always stop after one iteration of work (return a function which
+ # returns a function which always returns True)
+ terminationPredicateFactory=lambda: lambda: True)
+ self.task = self.cooperator.cooperate(self.worker())
+ self.cooperator.start()
+
+
+ def worker(self):
+ """
+ This is a sample generator which yields Deferreds when we are testing
+ deferral and an ascending integer count otherwise.
+ """
+ i = 0
+ while True:
+ i += 1
+ if self._doDeferNext:
+ self._doDeferNext = False
+ d = defer.Deferred()
+ self.work.append(d)
+ yield d
+ elif self._doStopNext:
+ return
+ elif self._doDieNext:
+ raise UnhandledException()
+ else:
+ self.work.append(i)
+ yield i
+
+
+ def tearDown(self):
+ """
+ Drop references to interesting parts of the fixture to allow Deferred
+ errors to be noticed when things start failing.
+ """
+ del self.task
+ del self.scheduler
+
+
+ def deferNext(self):
+ """
+ Defer the next result from my worker iterator.
+ """
+ self._doDeferNext = True
+
+
+ def stopNext(self):
+ """
+ Make the next result from my worker iterator be completion (raising
+ StopIteration).
+ """
+ self._doStopNext = True
+
+
+ def dieNext(self):
+ """
+ Make the next result from my worker iterator be raising an
+ L{UnhandledException}.
+ """
+ def ignoreUnhandled(failure):
+ failure.trap(UnhandledException)
+ return None
+ self._doDieNext = True
+
+
+ def test_pauseResume(self):
+ """
+ Cooperators should stop running their tasks when they're paused, and
+ start again when they're resumed.
+ """
+ # first, sanity check
+ self.scheduler.pump()
+ self.assertEqual(self.work, [1])
+ self.scheduler.pump()
+ self.assertEqual(self.work, [1, 2])
+
+ # OK, now for real
+ self.task.pause()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [1, 2])
+ self.task.resume()
+ # Resuming itself shoult not do any work
+ self.assertEqual(self.work, [1, 2])
+ self.scheduler.pump()
+ # But when the scheduler rolls around again...
+ self.assertEqual(self.work, [1, 2, 3])
+
+
+ def test_resumeNotPaused(self):
+ """
+ L{CooperativeTask.resume} should raise a L{TaskNotPaused} exception if
+ it was not paused; e.g. if L{CooperativeTask.pause} was not invoked
+ more times than L{CooperativeTask.resume} on that object.
+ """
+ self.assertRaises(task.NotPaused, self.task.resume)
+ self.task.pause()
+ self.task.resume()
+ self.assertRaises(task.NotPaused, self.task.resume)
+
+
+ def test_pauseTwice(self):
+ """
+ Pauses on tasks should behave like a stack. If a task is paused twice,
+ it needs to be resumed twice.
+ """
+ # pause once
+ self.task.pause()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [])
+ # pause twice
+ self.task.pause()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [])
+ # resume once (it shouldn't)
+ self.task.resume()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [])
+ # resume twice (now it should go)
+ self.task.resume()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [1])
+
+
+ def test_pauseWhileDeferred(self):
+ """
+ C{pause()}ing a task while it is waiting on an outstanding
+ L{defer.Deferred} should put the task into a state where the
+ outstanding L{defer.Deferred} must be called back I{and} the task is
+ C{resume}d before it will continue processing.
+ """
+ self.deferNext()
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 1)
+ self.failUnless(isinstance(self.work[0], defer.Deferred))
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 1)
+ self.task.pause()
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 1)
+ self.task.resume()
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 1)
+ self.work[0].callback("STUFF!")
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 2)
+ self.assertEqual(self.work[1], 2)
+
+
+ def test_whenDone(self):
+ """
+ L{CooperativeTask.whenDone} returns a Deferred which fires when the
+ Cooperator's iterator is exhausted. It returns a new Deferred each
+ time it is called; callbacks added to other invocations will not modify
+ the value that subsequent invocations will fire with.
+ """
+
+ deferred1 = self.task.whenDone()
+ deferred2 = self.task.whenDone()
+ results1 = []
+ results2 = []
+ final1 = []
+ final2 = []
+
+ def callbackOne(result):
+ results1.append(result)
+ return 1
+
+ def callbackTwo(result):
+ results2.append(result)
+ return 2
+
+ deferred1.addCallback(callbackOne)
+ deferred2.addCallback(callbackTwo)
+
+ deferred1.addCallback(final1.append)
+ deferred2.addCallback(final2.append)
+
+ # exhaust the task iterator
+ # callbacks fire
+ self.stopNext()
+ self.scheduler.pump()
+
+ self.assertEqual(len(results1), 1)
+ self.assertEqual(len(results2), 1)
+
+ self.assertIdentical(results1[0], self.task._iterator)
+ self.assertIdentical(results2[0], self.task._iterator)
+
+ self.assertEqual(final1, [1])
+ self.assertEqual(final2, [2])
+
+
+ def test_whenDoneError(self):
+ """
+ L{CooperativeTask.whenDone} returns a L{defer.Deferred} that will fail
+ when the iterable's C{next} method raises an exception, with that
+ exception.
+ """
+ deferred1 = self.task.whenDone()
+ results = []
+ deferred1.addErrback(results.append)
+ self.dieNext()
+ self.scheduler.pump()
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0].check(UnhandledException), UnhandledException)
+
+
+ def test_whenDoneStop(self):
+ """
+ L{CooperativeTask.whenDone} returns a L{defer.Deferred} that fails with
+ L{TaskStopped} when the C{stop} method is called on that
+ L{CooperativeTask}.
+ """
+ deferred1 = self.task.whenDone()
+ errors = []
+ deferred1.addErrback(errors.append)
+ self.task.stop()
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].check(task.TaskStopped), task.TaskStopped)
+
+
+ def test_whenDoneAlreadyDone(self):
+ """
+ L{CooperativeTask.whenDone} will return a L{defer.Deferred} that will
+ succeed immediately if its iterator has already completed.
+ """
+ self.stopNext()
+ self.scheduler.pump()
+ results = []
+ self.task.whenDone().addCallback(results.append)
+ self.assertEqual(results, [self.task._iterator])
+
+
+ def test_stopStops(self):
+ """
+ C{stop()}ping a task should cause it to be removed from the run just as
+ C{pause()}ing, with the distinction that C{resume()} will raise a
+ L{TaskStopped} exception.
+ """
+ self.task.stop()
+ self.scheduler.pump()
+ self.assertEqual(len(self.work), 0)
+ self.assertRaises(task.TaskStopped, self.task.stop)
+ self.assertRaises(task.TaskStopped, self.task.pause)
+ # Sanity check - it's still not scheduled, is it?
+ self.scheduler.pump()
+ self.assertEqual(self.work, [])
+
+
+ def test_pauseStopResume(self):
+ """
+ C{resume()}ing a paused, stopped task should be a no-op; it should not
+ raise an exception, because it's paused, but neither should it actually
+ do more work from the task.
+ """
+ self.task.pause()
+ self.task.stop()
+ self.task.resume()
+ self.scheduler.pump()
+ self.assertEqual(self.work, [])
+
+
+ def test_stopDeferred(self):
+ """
+ As a corrolary of the interaction of C{pause()} and C{unpause()},
+ C{stop()}ping a task which is waiting on a L{Deferred} should cause the
+ task to gracefully shut down, meaning that it should not be unpaused
+ when the deferred fires.
+ """
+ self.deferNext()
+ self.scheduler.pump()
+ d = self.work.pop()
+ self.assertEqual(self.task._pauseCount, 1)
+ results = []
+ d.addBoth(results.append)
+ self.scheduler.pump()
+ self.task.stop()
+ self.scheduler.pump()
+ d.callback(7)
+ self.scheduler.pump()
+ # Let's make sure that Deferred doesn't come out fried with an
+ # unhandled error that will be logged. The value is None, rather than
+ # our test value, 7, because this Deferred is returned to and consumed
+ # by the cooperator code. Its callback therefore has no contract.
+ self.assertEqual(results, [None])
+ # But more importantly, no further work should have happened.
+ self.assertEqual(self.work, [])
+
+
+ def test_stopExhausted(self):
+ """
+ C{stop()}ping a L{CooperativeTask} whose iterator has been exhausted
+ should raise L{TaskDone}.
+ """
+ self.stopNext()
+ self.scheduler.pump()
+ self.assertRaises(task.TaskDone, self.task.stop)
+
+
+ def test_stopErrored(self):
+ """
+ C{stop()}ping a L{CooperativeTask} whose iterator has encountered an
+ error should raise L{TaskFailed}.
+ """
+ self.dieNext()
+ self.scheduler.pump()
+ self.assertRaises(task.TaskFailed, self.task.stop)
+
+
+ def test_stopCooperatorReentrancy(self):
+ """
+ If a callback of a L{Deferred} from L{CooperativeTask.whenDone} calls
+ C{Cooperator.stop} on its L{CooperativeTask._cooperator}, the
+ L{Cooperator} will stop, but the L{CooperativeTask} whose callback is
+ calling C{stop} should already be considered 'stopped' by the time the
+ callback is running, and therefore removed from the
+ L{CoooperativeTask}.
+ """
+ callbackPhases = []
+ def stopit(result):
+ callbackPhases.append(result)
+ self.cooperator.stop()
+ # "done" here is a sanity check to make sure that we get all the
+ # way through the callback; i.e. stop() shouldn't be raising an
+ # exception due to the stopped-ness of our main task.
+ callbackPhases.append("done")
+ self.task.whenDone().addCallback(stopit)
+ self.stopNext()
+ self.scheduler.pump()
+ self.assertEqual(callbackPhases, [self.task._iterator, "done"])
+
+
+
diff --git a/twisted/test/test_defer.py b/twisted/test/test_defer.py
new file mode 100644
index 0000000..f6f1596
--- /dev/null
+++ b/twisted/test/test_defer.py
@@ -0,0 +1,2002 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for defer module.
+"""
+
+import gc, traceback
+
+from twisted.trial import unittest
+from twisted.internet import reactor, defer
+from twisted.internet.task import Clock
+from twisted.python import failure, log
+from twisted.python.util import unsignedID
+
+class GenericError(Exception):
+ pass
+
+
+
+class DeferredTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.callbackResults = None
+ self.errbackResults = None
+ self.callback2Results = None
+ # Restore the debug flag to its original state when done.
+ self.addCleanup(defer.setDebugging, defer.getDebugging())
+
+ def _callback(self, *args, **kw):
+ self.callbackResults = args, kw
+ return args[0]
+
+ def _callback2(self, *args, **kw):
+ self.callback2Results = args, kw
+
+ def _errback(self, *args, **kw):
+ self.errbackResults = args, kw
+
+ def testCallbackWithoutArgs(self):
+ deferred = defer.Deferred()
+ deferred.addCallback(self._callback)
+ deferred.callback("hello")
+ self.assertEqual(self.errbackResults, None)
+ self.assertEqual(self.callbackResults, (('hello',), {}))
+
+ def testCallbackWithArgs(self):
+ deferred = defer.Deferred()
+ deferred.addCallback(self._callback, "world")
+ deferred.callback("hello")
+ self.assertEqual(self.errbackResults, None)
+ self.assertEqual(self.callbackResults, (('hello', 'world'), {}))
+
+ def testCallbackWithKwArgs(self):
+ deferred = defer.Deferred()
+ deferred.addCallback(self._callback, world="world")
+ deferred.callback("hello")
+ self.assertEqual(self.errbackResults, None)
+ self.assertEqual(self.callbackResults,
+ (('hello',), {'world': 'world'}))
+
+ def testTwoCallbacks(self):
+ deferred = defer.Deferred()
+ deferred.addCallback(self._callback)
+ deferred.addCallback(self._callback2)
+ deferred.callback("hello")
+ self.assertEqual(self.errbackResults, None)
+ self.assertEqual(self.callbackResults,
+ (('hello',), {}))
+ self.assertEqual(self.callback2Results,
+ (('hello',), {}))
+
+ def testDeferredList(self):
+ defr1 = defer.Deferred()
+ defr2 = defer.Deferred()
+ defr3 = defer.Deferred()
+ dl = defer.DeferredList([defr1, defr2, defr3])
+ result = []
+ def cb(resultList, result=result):
+ result.extend(resultList)
+ def catch(err):
+ return None
+ dl.addCallbacks(cb, cb)
+ defr1.callback("1")
+ defr2.addErrback(catch)
+ # "catch" is added to eat the GenericError that will be passed on by
+ # the DeferredList's callback on defr2. If left unhandled, the
+ # Failure object would cause a log.err() warning about "Unhandled
+ # error in Deferred". Twisted's pyunit watches for log.err calls and
+ # treats them as failures. So "catch" must eat the error to prevent
+ # it from flunking the test.
+ defr2.errback(GenericError("2"))
+ defr3.callback("3")
+ self.assertEqual([result[0],
+ #result[1][1] is now a Failure instead of an Exception
+ (result[1][0], str(result[1][1].value)),
+ result[2]],
+
+ [(defer.SUCCESS, "1"),
+ (defer.FAILURE, "2"),
+ (defer.SUCCESS, "3")])
+
+ def testEmptyDeferredList(self):
+ result = []
+ def cb(resultList, result=result):
+ result.append(resultList)
+
+ dl = defer.DeferredList([])
+ dl.addCallbacks(cb)
+ self.assertEqual(result, [[]])
+
+ result[:] = []
+ dl = defer.DeferredList([], fireOnOneCallback=1)
+ dl.addCallbacks(cb)
+ self.assertEqual(result, [])
+
+ def testDeferredListFireOnOneError(self):
+ defr1 = defer.Deferred()
+ defr2 = defer.Deferred()
+ defr3 = defer.Deferred()
+ dl = defer.DeferredList([defr1, defr2, defr3], fireOnOneErrback=1)
+ result = []
+ dl.addErrback(result.append)
+
+ # consume errors after they pass through the DeferredList (to avoid
+ # 'Unhandled error in Deferred'.
+ def catch(err):
+ return None
+ defr2.addErrback(catch)
+
+ # fire one Deferred's callback, no result yet
+ defr1.callback("1")
+ self.assertEqual(result, [])
+
+ # fire one Deferred's errback -- now we have a result
+ defr2.errback(GenericError("from def2"))
+ self.assertEqual(len(result), 1)
+
+ # extract the result from the list
+ aFailure = result[0]
+
+ # the type of the failure is a FirstError
+ self.failUnless(issubclass(aFailure.type, defer.FirstError),
+ 'issubclass(aFailure.type, defer.FirstError) failed: '
+ "failure's type is %r" % (aFailure.type,)
+ )
+
+ firstError = aFailure.value
+
+ # check that the GenericError("2") from the deferred at index 1
+ # (defr2) is intact inside failure.value
+ self.assertEqual(firstError.subFailure.type, GenericError)
+ self.assertEqual(firstError.subFailure.value.args, ("from def2",))
+ self.assertEqual(firstError.index, 1)
+
+
+ def testDeferredListDontConsumeErrors(self):
+ d1 = defer.Deferred()
+ dl = defer.DeferredList([d1])
+
+ errorTrap = []
+ d1.addErrback(errorTrap.append)
+
+ result = []
+ dl.addCallback(result.append)
+
+ d1.errback(GenericError('Bang'))
+ self.assertEqual('Bang', errorTrap[0].value.args[0])
+ self.assertEqual(1, len(result))
+ self.assertEqual('Bang', result[0][0][1].value.args[0])
+
+ def testDeferredListConsumeErrors(self):
+ d1 = defer.Deferred()
+ dl = defer.DeferredList([d1], consumeErrors=True)
+
+ errorTrap = []
+ d1.addErrback(errorTrap.append)
+
+ result = []
+ dl.addCallback(result.append)
+
+ d1.errback(GenericError('Bang'))
+ self.assertEqual([], errorTrap)
+ self.assertEqual(1, len(result))
+ self.assertEqual('Bang', result[0][0][1].value.args[0])
+
+ def testDeferredListFireOnOneErrorWithAlreadyFiredDeferreds(self):
+ # Create some deferreds, and errback one
+ d1 = defer.Deferred()
+ d2 = defer.Deferred()
+ d1.errback(GenericError('Bang'))
+
+ # *Then* build the DeferredList, with fireOnOneErrback=True
+ dl = defer.DeferredList([d1, d2], fireOnOneErrback=True)
+ result = []
+ dl.addErrback(result.append)
+ self.assertEqual(1, len(result))
+
+ d1.addErrback(lambda e: None) # Swallow error
+
+ def testDeferredListWithAlreadyFiredDeferreds(self):
+ # Create some deferreds, and err one, call the other
+ d1 = defer.Deferred()
+ d2 = defer.Deferred()
+ d1.errback(GenericError('Bang'))
+ d2.callback(2)
+
+ # *Then* build the DeferredList
+ dl = defer.DeferredList([d1, d2])
+
+ result = []
+ dl.addCallback(result.append)
+
+ self.assertEqual(1, len(result))
+
+ d1.addErrback(lambda e: None) # Swallow error
+
+
+ def testImmediateSuccess(self):
+ l = []
+ d = defer.succeed("success")
+ d.addCallback(l.append)
+ self.assertEqual(l, ["success"])
+
+
+ def testImmediateFailure(self):
+ l = []
+ d = defer.fail(GenericError("fail"))
+ d.addErrback(l.append)
+ self.assertEqual(str(l[0].value), "fail")
+
+ def testPausedFailure(self):
+ l = []
+ d = defer.fail(GenericError("fail"))
+ d.pause()
+ d.addErrback(l.append)
+ self.assertEqual(l, [])
+ d.unpause()
+ self.assertEqual(str(l[0].value), "fail")
+
+ def testCallbackErrors(self):
+ l = []
+ d = defer.Deferred().addCallback(lambda _: 1 / 0).addErrback(l.append)
+ d.callback(1)
+ self.assert_(isinstance(l[0].value, ZeroDivisionError))
+ l = []
+ d = defer.Deferred().addCallback(
+ lambda _: failure.Failure(ZeroDivisionError())).addErrback(l.append)
+ d.callback(1)
+ self.assert_(isinstance(l[0].value, ZeroDivisionError))
+
+ def testUnpauseBeforeCallback(self):
+ d = defer.Deferred()
+ d.pause()
+ d.addCallback(self._callback)
+ d.unpause()
+
+ def testReturnDeferred(self):
+ d = defer.Deferred()
+ d2 = defer.Deferred()
+ d2.pause()
+ d.addCallback(lambda r, d2=d2: d2)
+ d.addCallback(self._callback)
+ d.callback(1)
+ assert self.callbackResults is None, "Should not have been called yet."
+ d2.callback(2)
+ assert self.callbackResults is None, "Still should not have been called yet."
+ d2.unpause()
+ assert self.callbackResults[0][0] == 2, "Result should have been from second deferred:%s" % (self.callbackResults,)
+
+
+ def test_chainedPausedDeferredWithResult(self):
+ """
+ When a paused Deferred with a result is returned from a callback on
+ another Deferred, the other Deferred is chained to the first and waits
+ for it to be unpaused.
+ """
+ expected = object()
+ paused = defer.Deferred()
+ paused.callback(expected)
+ paused.pause()
+ chained = defer.Deferred()
+ chained.addCallback(lambda ignored: paused)
+ chained.callback(None)
+
+ result = []
+ chained.addCallback(result.append)
+ self.assertEqual(result, [])
+ paused.unpause()
+ self.assertEqual(result, [expected])
+
+
+ def test_pausedDeferredChained(self):
+ """
+ A paused Deferred encountered while pushing a result forward through a
+ chain does not prevent earlier Deferreds from continuing to execute
+ their callbacks.
+ """
+ first = defer.Deferred()
+ second = defer.Deferred()
+ first.addCallback(lambda ignored: second)
+ first.callback(None)
+ first.pause()
+ second.callback(None)
+ result = []
+ second.addCallback(result.append)
+ self.assertEqual(result, [None])
+
+
+ def test_gatherResults(self):
+ # test successful list of deferreds
+ l = []
+ defer.gatherResults([defer.succeed(1), defer.succeed(2)]).addCallback(l.append)
+ self.assertEqual(l, [[1, 2]])
+ # test failing list of deferreds
+ l = []
+ dl = [defer.succeed(1), defer.fail(ValueError)]
+ defer.gatherResults(dl).addErrback(l.append)
+ self.assertEqual(len(l), 1)
+ self.assert_(isinstance(l[0], failure.Failure))
+ # get rid of error
+ dl[1].addErrback(lambda e: 1)
+
+
+ def test_gatherResultsWithConsumeErrors(self):
+ """
+ If a L{Deferred} in the list passed to L{gatherResults} fires with a
+ failure and C{consumerErrors} is C{True}, the failure is converted to a
+ C{None} result on that L{Deferred}.
+ """
+ # test successful list of deferreds
+ dgood = defer.succeed(1)
+ dbad = defer.fail(RuntimeError("oh noes"))
+ d = defer.gatherResults([dgood, dbad], consumeErrors=True)
+ unconsumedErrors = []
+ dbad.addErrback(unconsumedErrors.append)
+ gatheredErrors = []
+ d.addErrback(gatheredErrors.append)
+
+ self.assertEqual((len(unconsumedErrors), len(gatheredErrors)),
+ (0, 1))
+ self.assertIsInstance(gatheredErrors[0].value, defer.FirstError)
+ firstError = gatheredErrors[0].value.subFailure
+ self.assertIsInstance(firstError.value, RuntimeError)
+
+
+ def test_maybeDeferredSync(self):
+ """
+ L{defer.maybeDeferred} should retrieve the result of a synchronous
+ function and pass it to its resulting L{defer.Deferred}.
+ """
+ S, E = [], []
+ d = defer.maybeDeferred((lambda x: x + 5), 10)
+ d.addCallbacks(S.append, E.append)
+ self.assertEqual(E, [])
+ self.assertEqual(S, [15])
+ return d
+
+
+ def test_maybeDeferredSyncError(self):
+ """
+ L{defer.maybeDeferred} should catch exception raised by a synchronous
+ function and errback its resulting L{defer.Deferred} with it.
+ """
+ S, E = [], []
+ try:
+ '10' + 5
+ except TypeError, e:
+ expected = str(e)
+ d = defer.maybeDeferred((lambda x: x + 5), '10')
+ d.addCallbacks(S.append, E.append)
+ self.assertEqual(S, [])
+ self.assertEqual(len(E), 1)
+ self.assertEqual(str(E[0].value), expected)
+ return d
+
+
+ def test_maybeDeferredAsync(self):
+ """
+ L{defer.maybeDeferred} should let L{defer.Deferred} instance pass by
+ so that original result is the same.
+ """
+ d = defer.Deferred()
+ d2 = defer.maybeDeferred(lambda: d)
+ d.callback('Success')
+ return d2.addCallback(self.assertEqual, 'Success')
+
+
+ def test_maybeDeferredAsyncError(self):
+ """
+ L{defer.maybeDeferred} should let L{defer.Deferred} instance pass by
+ so that L{failure.Failure} returned by the original instance is the
+ same.
+ """
+ d = defer.Deferred()
+ d2 = defer.maybeDeferred(lambda: d)
+ d.errback(failure.Failure(RuntimeError()))
+ return self.assertFailure(d2, RuntimeError)
+
+
+ def test_innerCallbacksPreserved(self):
+ """
+ When a L{Deferred} encounters a result which is another L{Deferred}
+ which is waiting on a third L{Deferred}, the middle L{Deferred}'s
+ callbacks are executed after the third L{Deferred} fires and before the
+ first receives a result.
+ """
+ results = []
+ failures = []
+ inner = defer.Deferred()
+ def cb(result):
+ results.append(('start-of-cb', result))
+ d = defer.succeed('inner')
+ def firstCallback(result):
+ results.append(('firstCallback', 'inner'))
+ return inner
+ def secondCallback(result):
+ results.append(('secondCallback', result))
+ return result * 2
+ d.addCallback(firstCallback).addCallback(secondCallback)
+ d.addErrback(failures.append)
+ return d
+ outer = defer.succeed('outer')
+ outer.addCallback(cb)
+ inner.callback('orange')
+ outer.addCallback(results.append)
+ inner.addErrback(failures.append)
+ outer.addErrback(failures.append)
+ self.assertEqual([], failures)
+ self.assertEqual(
+ results,
+ [('start-of-cb', 'outer'),
+ ('firstCallback', 'inner'),
+ ('secondCallback', 'orange'),
+ 'orangeorange'])
+
+
+ def test_continueCallbackNotFirst(self):
+ """
+ The continue callback of a L{Deferred} waiting for another L{Deferred}
+ is not necessarily the first one. This is somewhat a whitebox test
+ checking that we search for that callback among the whole list of
+ callbacks.
+ """
+ results = []
+ failures = []
+ a = defer.Deferred()
+
+ def cb(result):
+ results.append(('cb', result))
+ d = defer.Deferred()
+
+ def firstCallback(ignored):
+ results.append(('firstCallback', ignored))
+ return defer.gatherResults([a])
+
+ def secondCallback(result):
+ results.append(('secondCallback', result))
+
+ d.addCallback(firstCallback)
+ d.addCallback(secondCallback)
+ d.addErrback(failures.append)
+ d.callback(None)
+ return d
+
+ outer = defer.succeed('outer')
+ outer.addCallback(cb)
+ outer.addErrback(failures.append)
+ self.assertEqual([('cb', 'outer'), ('firstCallback', None)], results)
+ a.callback('withers')
+ self.assertEqual([], failures)
+ self.assertEqual(
+ results,
+ [('cb', 'outer'),
+ ('firstCallback', None),
+ ('secondCallback', ['withers'])])
+
+
+ def test_callbackOrderPreserved(self):
+ """
+ A callback added to a L{Deferred} after a previous callback attached
+ another L{Deferred} as a result is run after the callbacks of the other
+ L{Deferred} are run.
+ """
+ results = []
+ failures = []
+ a = defer.Deferred()
+
+ def cb(result):
+ results.append(('cb', result))
+ d = defer.Deferred()
+
+ def firstCallback(ignored):
+ results.append(('firstCallback', ignored))
+ return defer.gatherResults([a])
+
+ def secondCallback(result):
+ results.append(('secondCallback', result))
+
+ d.addCallback(firstCallback)
+ d.addCallback(secondCallback)
+ d.addErrback(failures.append)
+ d.callback(None)
+ return d
+
+ outer = defer.Deferred()
+ outer.addCallback(cb)
+ outer.addCallback(lambda x: results.append('final'))
+ outer.addErrback(failures.append)
+ outer.callback('outer')
+ self.assertEqual([('cb', 'outer'), ('firstCallback', None)], results)
+ a.callback('withers')
+ self.assertEqual([], failures)
+ self.assertEqual(
+ results,
+ [('cb', 'outer'),
+ ('firstCallback', None),
+ ('secondCallback', ['withers']), 'final'])
+
+
+ def test_reentrantRunCallbacks(self):
+ """
+ A callback added to a L{Deferred} by a callback on that L{Deferred}
+ should be added to the end of the callback chain.
+ """
+ deferred = defer.Deferred()
+ called = []
+ def callback3(result):
+ called.append(3)
+ def callback2(result):
+ called.append(2)
+ def callback1(result):
+ called.append(1)
+ deferred.addCallback(callback3)
+ deferred.addCallback(callback1)
+ deferred.addCallback(callback2)
+ deferred.callback(None)
+ self.assertEqual(called, [1, 2, 3])
+
+
+ def test_nonReentrantCallbacks(self):
+ """
+ A callback added to a L{Deferred} by a callback on that L{Deferred}
+ should not be executed until the running callback returns.
+ """
+ deferred = defer.Deferred()
+ called = []
+ def callback2(result):
+ called.append(2)
+ def callback1(result):
+ called.append(1)
+ deferred.addCallback(callback2)
+ self.assertEqual(called, [1])
+ deferred.addCallback(callback1)
+ deferred.callback(None)
+ self.assertEqual(called, [1, 2])
+
+
+ def test_reentrantRunCallbacksWithFailure(self):
+ """
+ After an exception is raised by a callback which was added to a
+ L{Deferred} by a callback on that L{Deferred}, the L{Deferred} should
+ call the first errback with a L{Failure} wrapping that exception.
+ """
+ exceptionMessage = "callback raised exception"
+ deferred = defer.Deferred()
+ def callback2(result):
+ raise Exception(exceptionMessage)
+ def callback1(result):
+ deferred.addCallback(callback2)
+ deferred.addCallback(callback1)
+ deferred.callback(None)
+ self.assertFailure(deferred, Exception)
+ def cbFailed(exception):
+ self.assertEqual(exception.args, (exceptionMessage,))
+ deferred.addCallback(cbFailed)
+ return deferred
+
+
+ def test_synchronousImplicitChain(self):
+ """
+ If a first L{Deferred} with a result is returned from a callback on a
+ second L{Deferred}, the result of the second L{Deferred} becomes the
+ result of the first L{Deferred} and the result of the first L{Deferred}
+ becomes C{None}.
+ """
+ result = object()
+ first = defer.succeed(result)
+ second = defer.Deferred()
+ second.addCallback(lambda ign: first)
+ second.callback(None)
+
+ results = []
+ first.addCallback(results.append)
+ self.assertIdentical(results[0], None)
+ second.addCallback(results.append)
+ self.assertIdentical(results[1], result)
+
+
+ def test_asynchronousImplicitChain(self):
+ """
+ If a first L{Deferred} without a result is returned from a callback on
+ a second L{Deferred}, the result of the second L{Deferred} becomes the
+ result of the first L{Deferred} as soon as the first L{Deferred} has
+ one and the result of the first L{Deferred} becomes C{None}.
+ """
+ first = defer.Deferred()
+ second = defer.Deferred()
+ second.addCallback(lambda ign: first)
+ second.callback(None)
+
+ firstResult = []
+ first.addCallback(firstResult.append)
+ secondResult = []
+ second.addCallback(secondResult.append)
+
+ self.assertEqual(firstResult, [])
+ self.assertEqual(secondResult, [])
+
+ result = object()
+ first.callback(result)
+
+ self.assertEqual(firstResult, [None])
+ self.assertEqual(secondResult, [result])
+
+
+ def test_synchronousImplicitErrorChain(self):
+ """
+ If a first L{Deferred} with a L{Failure} result is returned from a
+ callback on a second L{Deferred}, the first L{Deferred}'s result is
+ converted to L{None} and no unhandled error is logged when it is
+ garbage collected.
+ """
+ first = defer.fail(RuntimeError("First Deferred's Failure"))
+ second = defer.Deferred()
+ second.addCallback(lambda ign, first=first: first)
+ self.assertFailure(second, RuntimeError)
+ second.callback(None)
+ firstResult = []
+ first.addCallback(firstResult.append)
+ self.assertIdentical(firstResult[0], None)
+ return second
+
+
+ def test_asynchronousImplicitErrorChain(self):
+ """
+ Let C{a} and C{b} be two L{Deferred}s.
+
+ If C{a} has no result and is returned from a callback on C{b} then when
+ C{a} fails, C{b}'s result becomes the L{Failure} that was C{a}'s result,
+ the result of C{a} becomes C{None} so that no unhandled error is logged
+ when it is garbage collected.
+ """
+ first = defer.Deferred()
+ second = defer.Deferred()
+ second.addCallback(lambda ign: first)
+ second.callback(None)
+ self.assertFailure(second, RuntimeError)
+
+ firstResult = []
+ first.addCallback(firstResult.append)
+ secondResult = []
+ second.addCallback(secondResult.append)
+
+ self.assertEqual(firstResult, [])
+ self.assertEqual(secondResult, [])
+
+ first.errback(RuntimeError("First Deferred's Failure"))
+
+ self.assertEqual(firstResult, [None])
+ self.assertEqual(len(secondResult), 1)
+
+
+ def test_doubleAsynchronousImplicitChaining(self):
+ """
+ L{Deferred} chaining is transitive.
+
+ In other words, let A, B, and C be Deferreds. If C is returned from a
+ callback on B and B is returned from a callback on A then when C fires,
+ A fires.
+ """
+ first = defer.Deferred()
+ second = defer.Deferred()
+ second.addCallback(lambda ign: first)
+ third = defer.Deferred()
+ third.addCallback(lambda ign: second)
+
+ thirdResult = []
+ third.addCallback(thirdResult.append)
+
+ result = object()
+ # After this, second is waiting for first to tell it to continue.
+ second.callback(None)
+ # And after this, third is waiting for second to tell it to continue.
+ third.callback(None)
+
+ # Still waiting
+ self.assertEqual(thirdResult, [])
+
+ # This will tell second to continue which will tell third to continue.
+ first.callback(result)
+
+ self.assertEqual(thirdResult, [result])
+
+
+ def test_nestedAsynchronousChainedDeferreds(self):
+ """
+ L{Deferred}s can have callbacks that themselves return L{Deferred}s.
+ When these "inner" L{Deferred}s fire (even asynchronously), the
+ callback chain continues.
+ """
+ results = []
+ failures = []
+
+ # A Deferred returned in the inner callback.
+ inner = defer.Deferred()
+
+ def cb(result):
+ results.append(('start-of-cb', result))
+ d = defer.succeed('inner')
+
+ def firstCallback(result):
+ results.append(('firstCallback', 'inner'))
+ # Return a Deferred that definitely has not fired yet, so we
+ # can fire the Deferreds out of order.
+ return inner
+
+ def secondCallback(result):
+ results.append(('secondCallback', result))
+ return result * 2
+
+ d.addCallback(firstCallback).addCallback(secondCallback)
+ d.addErrback(failures.append)
+ return d
+
+ # Create a synchronous Deferred that has a callback 'cb' that returns
+ # a Deferred 'd' that has fired but is now waiting on an unfired
+ # Deferred 'inner'.
+ outer = defer.succeed('outer')
+ outer.addCallback(cb)
+ outer.addCallback(results.append)
+ # At this point, the callback 'cb' has been entered, and the first
+ # callback of 'd' has been called.
+ self.assertEqual(
+ results, [('start-of-cb', 'outer'), ('firstCallback', 'inner')])
+
+ # Once the inner Deferred is fired, processing of the outer Deferred's
+ # callback chain continues.
+ inner.callback('orange')
+
+ # Make sure there are no errors.
+ inner.addErrback(failures.append)
+ outer.addErrback(failures.append)
+ self.assertEqual(
+ [], failures, "Got errbacks but wasn't expecting any.")
+
+ self.assertEqual(
+ results,
+ [('start-of-cb', 'outer'),
+ ('firstCallback', 'inner'),
+ ('secondCallback', 'orange'),
+ 'orangeorange'])
+
+
+ def test_nestedAsynchronousChainedDeferredsWithExtraCallbacks(self):
+ """
+ L{Deferred}s can have callbacks that themselves return L{Deferred}s.
+ These L{Deferred}s can have other callbacks added before they are
+ returned, which subtly changes the callback chain. When these "inner"
+ L{Deferred}s fire (even asynchronously), the outer callback chain
+ continues.
+ """
+ results = []
+ failures = []
+
+ # A Deferred returned in the inner callback after a callback is
+ # added explicitly and directly to it.
+ inner = defer.Deferred()
+
+ def cb(result):
+ results.append(('start-of-cb', result))
+ d = defer.succeed('inner')
+
+ def firstCallback(ignored):
+ results.append(('firstCallback', ignored))
+ # Return a Deferred that definitely has not fired yet with a
+ # result-transforming callback so we can fire the Deferreds
+ # out of order and see how the callback affects the ultimate
+ # results.
+ return inner.addCallback(lambda x: [x])
+
+ def secondCallback(result):
+ results.append(('secondCallback', result))
+ return result * 2
+
+ d.addCallback(firstCallback)
+ d.addCallback(secondCallback)
+ d.addErrback(failures.append)
+ return d
+
+ # Create a synchronous Deferred that has a callback 'cb' that returns
+ # a Deferred 'd' that has fired but is now waiting on an unfired
+ # Deferred 'inner'.
+ outer = defer.succeed('outer')
+ outer.addCallback(cb)
+ outer.addCallback(results.append)
+ # At this point, the callback 'cb' has been entered, and the first
+ # callback of 'd' has been called.
+ self.assertEqual(
+ results, [('start-of-cb', 'outer'), ('firstCallback', 'inner')])
+
+ # Once the inner Deferred is fired, processing of the outer Deferred's
+ # callback chain continues.
+ inner.callback('withers')
+
+ # Make sure there are no errors.
+ outer.addErrback(failures.append)
+ inner.addErrback(failures.append)
+ self.assertEqual(
+ [], failures, "Got errbacks but wasn't expecting any.")
+
+ self.assertEqual(
+ results,
+ [('start-of-cb', 'outer'),
+ ('firstCallback', 'inner'),
+ ('secondCallback', ['withers']),
+ ['withers', 'withers']])
+
+
+ def test_chainDeferredRecordsExplicitChain(self):
+ """
+ When we chain a L{Deferred}, that chaining is recorded explicitly.
+ """
+ a = defer.Deferred()
+ b = defer.Deferred()
+ b.chainDeferred(a)
+ self.assertIdentical(a._chainedTo, b)
+
+
+ def test_explicitChainClearedWhenResolved(self):
+ """
+ Any recorded chaining is cleared once the chaining is resolved, since
+ it no longer exists.
+
+ In other words, if one L{Deferred} is recorded as depending on the
+ result of another, and I{that} L{Deferred} has fired, then the
+ dependency is resolved and we no longer benefit from recording it.
+ """
+ a = defer.Deferred()
+ b = defer.Deferred()
+ b.chainDeferred(a)
+ b.callback(None)
+ self.assertIdentical(a._chainedTo, None)
+
+
+ def test_chainDeferredRecordsImplicitChain(self):
+ """
+ We can chain L{Deferred}s implicitly by adding callbacks that return
+ L{Deferred}s. When this chaining happens, we record it explicitly as
+ soon as we can find out about it.
+ """
+ a = defer.Deferred()
+ b = defer.Deferred()
+ a.addCallback(lambda ignored: b)
+ a.callback(None)
+ self.assertIdentical(a._chainedTo, b)
+
+
+ def test_repr(self):
+ """
+ The C{repr()} of a L{Deferred} contains the class name and a
+ representation of the internal Python ID.
+ """
+ d = defer.Deferred()
+ address = hex(unsignedID(d))
+ self.assertEqual(
+ repr(d), '<Deferred at %s>' % (address,))
+
+
+ def test_reprWithResult(self):
+ """
+ If a L{Deferred} has been fired, then its C{repr()} contains its
+ result.
+ """
+ d = defer.Deferred()
+ d.callback('orange')
+ self.assertEqual(
+ repr(d), "<Deferred at %s current result: 'orange'>" % (
+ hex(unsignedID(d))))
+
+
+ def test_reprWithChaining(self):
+ """
+ If a L{Deferred} C{a} has been fired, but is waiting on another
+ L{Deferred} C{b} that appears in its callback chain, then C{repr(a)}
+ says that it is waiting on C{b}.
+ """
+ a = defer.Deferred()
+ b = defer.Deferred()
+ b.chainDeferred(a)
+ self.assertEqual(
+ repr(a), "<Deferred at %s waiting on Deferred at %s>" % (
+ hex(unsignedID(a)), hex(unsignedID(b))))
+
+
+ def test_boundedStackDepth(self):
+ """
+ The depth of the call stack does not grow as more L{Deferred} instances
+ are chained together.
+ """
+ def chainDeferreds(howMany):
+ stack = []
+ def recordStackDepth(ignored):
+ stack.append(len(traceback.extract_stack()))
+
+ top = defer.Deferred()
+ innerDeferreds = [defer.Deferred() for ignored in range(howMany)]
+ originalInners = innerDeferreds[:]
+ last = defer.Deferred()
+
+ inner = innerDeferreds.pop()
+ top.addCallback(lambda ign, inner=inner: inner)
+ top.addCallback(recordStackDepth)
+
+ while innerDeferreds:
+ newInner = innerDeferreds.pop()
+ inner.addCallback(lambda ign, inner=newInner: inner)
+ inner = newInner
+ inner.addCallback(lambda ign: last)
+
+ top.callback(None)
+ for inner in originalInners:
+ inner.callback(None)
+
+ # Sanity check - the record callback is not intended to have
+ # fired yet.
+ self.assertEqual(stack, [])
+
+ # Now fire the last thing and return the stack depth at which the
+ # callback was invoked.
+ last.callback(None)
+ return stack[0]
+
+ # Callbacks should be invoked at the same stack depth regardless of
+ # how many Deferreds are chained.
+ self.assertEqual(chainDeferreds(1), chainDeferreds(2))
+
+
+ def test_resultOfDeferredResultOfDeferredOfFiredDeferredCalled(self):
+ """
+ Given three Deferreds, one chained to the next chained to the next,
+ callbacks on the middle Deferred which are added after the chain is
+ created are called once the last Deferred fires.
+
+ This is more of a regression-style test. It doesn't exercise any
+ particular code path through the current implementation of Deferred, but
+ it does exercise a broken codepath through one of the variations of the
+ implementation proposed as a resolution to ticket #411.
+ """
+ first = defer.Deferred()
+ second = defer.Deferred()
+ third = defer.Deferred()
+ first.addCallback(lambda ignored: second)
+ second.addCallback(lambda ignored: third)
+ second.callback(None)
+ first.callback(None)
+ third.callback(None)
+ L = []
+ second.addCallback(L.append)
+ self.assertEqual(L, [None])
+
+
+ def test_errbackWithNoArgsNoDebug(self):
+ """
+ C{Deferred.errback()} creates a failure from the current Python
+ exception. When Deferred.debug is not set no globals or locals are
+ captured in that failure.
+ """
+ defer.setDebugging(False)
+ d = defer.Deferred()
+ l = []
+ exc = GenericError("Bang")
+ try:
+ raise exc
+ except:
+ d.errback()
+ d.addErrback(l.append)
+ fail = l[0]
+ self.assertEqual(fail.value, exc)
+ localz, globalz = fail.frames[0][-2:]
+ self.assertEqual([], localz)
+ self.assertEqual([], globalz)
+
+
+ def test_errbackWithNoArgs(self):
+ """
+ C{Deferred.errback()} creates a failure from the current Python
+ exception. When Deferred.debug is set globals and locals are captured
+ in that failure.
+ """
+ defer.setDebugging(True)
+ d = defer.Deferred()
+ l = []
+ exc = GenericError("Bang")
+ try:
+ raise exc
+ except:
+ d.errback()
+ d.addErrback(l.append)
+ fail = l[0]
+ self.assertEqual(fail.value, exc)
+ localz, globalz = fail.frames[0][-2:]
+ self.assertNotEquals([], localz)
+ self.assertNotEquals([], globalz)
+
+
+ def test_errorInCallbackDoesNotCaptureVars(self):
+ """
+ An error raised by a callback creates a Failure. The Failure captures
+ locals and globals if and only if C{Deferred.debug} is set.
+ """
+ d = defer.Deferred()
+ d.callback(None)
+ defer.setDebugging(False)
+ def raiseError(ignored):
+ raise GenericError("Bang")
+ d.addCallback(raiseError)
+ l = []
+ d.addErrback(l.append)
+ fail = l[0]
+ localz, globalz = fail.frames[0][-2:]
+ self.assertEqual([], localz)
+ self.assertEqual([], globalz)
+
+
+ def test_errorInCallbackCapturesVarsWhenDebugging(self):
+ """
+ An error raised by a callback creates a Failure. The Failure captures
+ locals and globals if and only if C{Deferred.debug} is set.
+ """
+ d = defer.Deferred()
+ d.callback(None)
+ defer.setDebugging(True)
+ def raiseError(ignored):
+ raise GenericError("Bang")
+ d.addCallback(raiseError)
+ l = []
+ d.addErrback(l.append)
+ fail = l[0]
+ localz, globalz = fail.frames[0][-2:]
+ self.assertNotEquals([], localz)
+ self.assertNotEquals([], globalz)
+
+
+
+class FirstErrorTests(unittest.TestCase):
+ """
+ Tests for L{FirstError}.
+ """
+ def test_repr(self):
+ """
+ The repr of a L{FirstError} instance includes the repr of the value of
+ the sub-failure and the index which corresponds to the L{FirstError}.
+ """
+ exc = ValueError("some text")
+ try:
+ raise exc
+ except:
+ f = failure.Failure()
+
+ error = defer.FirstError(f, 3)
+ self.assertEqual(
+ repr(error),
+ "FirstError[#3, %s]" % (repr(exc),))
+
+
+ def test_str(self):
+ """
+ The str of a L{FirstError} instance includes the str of the
+ sub-failure and the index which corresponds to the L{FirstError}.
+ """
+ exc = ValueError("some text")
+ try:
+ raise exc
+ except:
+ f = failure.Failure()
+
+ error = defer.FirstError(f, 5)
+ self.assertEqual(
+ str(error),
+ "FirstError[#5, %s]" % (str(f),))
+
+
+ def test_comparison(self):
+ """
+ L{FirstError} instances compare equal to each other if and only if
+ their failure and index compare equal. L{FirstError} instances do not
+ compare equal to instances of other types.
+ """
+ try:
+ 1 / 0
+ except:
+ firstFailure = failure.Failure()
+
+ one = defer.FirstError(firstFailure, 13)
+ anotherOne = defer.FirstError(firstFailure, 13)
+
+ try:
+ raise ValueError("bar")
+ except:
+ secondFailure = failure.Failure()
+
+ another = defer.FirstError(secondFailure, 9)
+
+ self.assertTrue(one == anotherOne)
+ self.assertFalse(one == another)
+ self.assertTrue(one != another)
+ self.assertFalse(one != anotherOne)
+
+ self.assertFalse(one == 10)
+
+
+
+class AlreadyCalledTestCase(unittest.TestCase):
+ def setUp(self):
+ self._deferredWasDebugging = defer.getDebugging()
+ defer.setDebugging(True)
+
+ def tearDown(self):
+ defer.setDebugging(self._deferredWasDebugging)
+
+ def _callback(self, *args, **kw):
+ pass
+ def _errback(self, *args, **kw):
+ pass
+
+ def _call_1(self, d):
+ d.callback("hello")
+ def _call_2(self, d):
+ d.callback("twice")
+ def _err_1(self, d):
+ d.errback(failure.Failure(RuntimeError()))
+ def _err_2(self, d):
+ d.errback(failure.Failure(RuntimeError()))
+
+ def testAlreadyCalled_CC(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._call_1(d)
+ self.failUnlessRaises(defer.AlreadyCalledError, self._call_2, d)
+
+ def testAlreadyCalled_CE(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._call_1(d)
+ self.failUnlessRaises(defer.AlreadyCalledError, self._err_2, d)
+
+ def testAlreadyCalled_EE(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._err_1(d)
+ self.failUnlessRaises(defer.AlreadyCalledError, self._err_2, d)
+
+ def testAlreadyCalled_EC(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._err_1(d)
+ self.failUnlessRaises(defer.AlreadyCalledError, self._call_2, d)
+
+
+ def _count(self, linetype, func, lines, expected):
+ count = 0
+ for line in lines:
+ if (line.startswith(' %s:' % linetype) and
+ line.endswith(' %s' % func)):
+ count += 1
+ self.failUnless(count == expected)
+
+ def _check(self, e, caller, invoker1, invoker2):
+ # make sure the debugging information is vaguely correct
+ lines = e.args[0].split("\n")
+ # the creator should list the creator (testAlreadyCalledDebug) but not
+ # _call_1 or _call_2 or other invokers
+ self._count('C', caller, lines, 1)
+ self._count('C', '_call_1', lines, 0)
+ self._count('C', '_call_2', lines, 0)
+ self._count('C', '_err_1', lines, 0)
+ self._count('C', '_err_2', lines, 0)
+ # invoker should list the first invoker but not the second
+ self._count('I', invoker1, lines, 1)
+ self._count('I', invoker2, lines, 0)
+
+ def testAlreadyCalledDebug_CC(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._call_1(d)
+ try:
+ self._call_2(d)
+ except defer.AlreadyCalledError, e:
+ self._check(e, "testAlreadyCalledDebug_CC", "_call_1", "_call_2")
+ else:
+ self.fail("second callback failed to raise AlreadyCalledError")
+
+ def testAlreadyCalledDebug_CE(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._call_1(d)
+ try:
+ self._err_2(d)
+ except defer.AlreadyCalledError, e:
+ self._check(e, "testAlreadyCalledDebug_CE", "_call_1", "_err_2")
+ else:
+ self.fail("second errback failed to raise AlreadyCalledError")
+
+ def testAlreadyCalledDebug_EC(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._err_1(d)
+ try:
+ self._call_2(d)
+ except defer.AlreadyCalledError, e:
+ self._check(e, "testAlreadyCalledDebug_EC", "_err_1", "_call_2")
+ else:
+ self.fail("second callback failed to raise AlreadyCalledError")
+
+ def testAlreadyCalledDebug_EE(self):
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._err_1(d)
+ try:
+ self._err_2(d)
+ except defer.AlreadyCalledError, e:
+ self._check(e, "testAlreadyCalledDebug_EE", "_err_1", "_err_2")
+ else:
+ self.fail("second errback failed to raise AlreadyCalledError")
+
+ def testNoDebugging(self):
+ defer.setDebugging(False)
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ self._call_1(d)
+ try:
+ self._call_2(d)
+ except defer.AlreadyCalledError, e:
+ self.failIf(e.args)
+ else:
+ self.fail("second callback failed to raise AlreadyCalledError")
+
+
+ def testSwitchDebugging(self):
+ # Make sure Deferreds can deal with debug state flipping
+ # around randomly. This is covering a particular fixed bug.
+ defer.setDebugging(False)
+ d = defer.Deferred()
+ d.addBoth(lambda ign: None)
+ defer.setDebugging(True)
+ d.callback(None)
+
+ defer.setDebugging(False)
+ d = defer.Deferred()
+ d.callback(None)
+ defer.setDebugging(True)
+ d.addBoth(lambda ign: None)
+
+
+
+class DeferredCancellerTest(unittest.TestCase):
+ def setUp(self):
+ self.callbackResults = None
+ self.errbackResults = None
+ self.callback2Results = None
+ self.cancellerCallCount = 0
+
+
+ def tearDown(self):
+ # Sanity check that the canceller was called at most once.
+ self.assertTrue(self.cancellerCallCount in (0, 1))
+
+
+ def _callback(self, data):
+ self.callbackResults = data
+ return data
+
+
+ def _callback2(self, data):
+ self.callback2Results = data
+
+
+ def _errback(self, data):
+ self.errbackResults = data
+
+
+ def test_noCanceller(self):
+ """
+ A L{defer.Deferred} without a canceller must errback with a
+ L{defer.CancelledError} and not callback.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+ self.assertEqual(self.callbackResults, None)
+
+
+ def test_raisesAfterCancelAndCallback(self):
+ """
+ A L{defer.Deferred} without a canceller, when cancelled must allow
+ a single extra call to callback, and raise
+ L{defer.AlreadyCalledError} if callbacked or errbacked thereafter.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+
+ # A single extra callback should be swallowed.
+ d.callback(None)
+
+ # But a second call to callback or errback is not.
+ self.assertRaises(defer.AlreadyCalledError, d.callback, None)
+ self.assertRaises(defer.AlreadyCalledError, d.errback, Exception())
+
+
+ def test_raisesAfterCancelAndErrback(self):
+ """
+ A L{defer.Deferred} without a canceller, when cancelled must allow
+ a single extra call to errback, and raise
+ L{defer.AlreadyCalledError} if callbacked or errbacked thereafter.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+
+ # A single extra errback should be swallowed.
+ d.errback(Exception())
+
+ # But a second call to callback or errback is not.
+ self.assertRaises(defer.AlreadyCalledError, d.callback, None)
+ self.assertRaises(defer.AlreadyCalledError, d.errback, Exception())
+
+
+ def test_noCancellerMultipleCancelsAfterCancelAndCallback(self):
+ """
+ A L{Deferred} without a canceller, when cancelled and then
+ callbacked, ignores multiple cancels thereafter.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ currentFailure = self.errbackResults
+ # One callback will be ignored
+ d.callback(None)
+ # Cancel should have no effect.
+ d.cancel()
+ self.assertIdentical(currentFailure, self.errbackResults)
+
+
+ def test_noCancellerMultipleCancelsAfterCancelAndErrback(self):
+ """
+ A L{defer.Deferred} without a canceller, when cancelled and then
+ errbacked, ignores multiple cancels thereafter.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+ currentFailure = self.errbackResults
+ # One errback will be ignored
+ d.errback(GenericError())
+ # I.e., we should still have a CancelledError.
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+ d.cancel()
+ self.assertIdentical(currentFailure, self.errbackResults)
+
+
+ def test_noCancellerMultipleCancel(self):
+ """
+ Calling cancel multiple times on a deferred with no canceller
+ results in a L{defer.CancelledError}. Subsequent calls to cancel
+ do not cause an error.
+ """
+ d = defer.Deferred()
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+ currentFailure = self.errbackResults
+ d.cancel()
+ self.assertIdentical(currentFailure, self.errbackResults)
+
+
+ def test_cancellerMultipleCancel(self):
+ """
+ Verify that calling cancel multiple times on a deferred with a
+ canceller that does not errback results in a
+ L{defer.CancelledError} and that subsequent calls to cancel do not
+ cause an error and that after all that, the canceller was only
+ called once.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+ currentFailure = self.errbackResults
+ d.cancel()
+ self.assertIdentical(currentFailure, self.errbackResults)
+ self.assertEqual(self.cancellerCallCount, 1)
+
+
+ def test_simpleCanceller(self):
+ """
+ Verify that a L{defer.Deferred} calls its specified canceller when
+ it is cancelled, and that further call/errbacks raise
+ L{defer.AlreadyCalledError}.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.cancellerCallCount, 1)
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+
+ # Test that further call/errbacks are *not* swallowed
+ self.assertRaises(defer.AlreadyCalledError, d.callback, None)
+ self.assertRaises(defer.AlreadyCalledError, d.errback, Exception())
+
+
+ def test_cancellerArg(self):
+ """
+ Verify that a canceller is given the correct deferred argument.
+ """
+ def cancel(d1):
+ self.assertIdentical(d1, d)
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+
+
+ def test_cancelAfterCallback(self):
+ """
+ Test that cancelling a deferred after it has been callbacked does
+ not cause an error.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+ d.errback(GenericError())
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.callback('biff!')
+ d.cancel()
+ self.assertEqual(self.cancellerCallCount, 0)
+ self.assertEqual(self.errbackResults, None)
+ self.assertEqual(self.callbackResults, 'biff!')
+
+
+ def test_cancelAfterErrback(self):
+ """
+ Test that cancelling a L{Deferred} after it has been errbacked does
+ not result in a L{defer.CancelledError}.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+ d.errback(GenericError())
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.errback(GenericError())
+ d.cancel()
+ self.assertEqual(self.cancellerCallCount, 0)
+ self.assertEqual(self.errbackResults.type, GenericError)
+ self.assertEqual(self.callbackResults, None)
+
+
+ def test_cancellerThatErrbacks(self):
+ """
+ Test a canceller which errbacks its deferred.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+ d.errback(GenericError())
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.cancellerCallCount, 1)
+ self.assertEqual(self.errbackResults.type, GenericError)
+
+
+ def test_cancellerThatCallbacks(self):
+ """
+ Test a canceller which calls its deferred.
+ """
+ def cancel(d):
+ self.cancellerCallCount += 1
+ d.callback('hello!')
+ d = defer.Deferred(canceller=cancel)
+ d.addCallbacks(self._callback, self._errback)
+ d.cancel()
+ self.assertEqual(self.cancellerCallCount, 1)
+ self.assertEqual(self.callbackResults, 'hello!')
+ self.assertEqual(self.errbackResults, None)
+
+
+ def test_cancelNestedDeferred(self):
+ """
+ Verify that a Deferred, a, which is waiting on another Deferred, b,
+ returned from one of its callbacks, will propagate
+ L{defer.CancelledError} when a is cancelled.
+ """
+ def innerCancel(d):
+ self.cancellerCallCount += 1
+ def cancel(d):
+ self.assert_(False)
+
+ b = defer.Deferred(canceller=innerCancel)
+ a = defer.Deferred(canceller=cancel)
+ a.callback(None)
+ a.addCallback(lambda data: b)
+ a.cancel()
+ a.addCallbacks(self._callback, self._errback)
+ # The cancel count should be one (the cancellation done by B)
+ self.assertEqual(self.cancellerCallCount, 1)
+ # B's canceller didn't errback, so defer.py will have called errback
+ # with a CancelledError.
+ self.assertEqual(self.errbackResults.type, defer.CancelledError)
+
+
+
+class LogTestCase(unittest.TestCase):
+ """
+ Test logging of unhandled errors.
+ """
+
+ def setUp(self):
+ """
+ Add a custom observer to observer logging.
+ """
+ self.c = []
+ log.addObserver(self.c.append)
+
+ def tearDown(self):
+ """
+ Remove the observer.
+ """
+ log.removeObserver(self.c.append)
+
+
+ def _loggedErrors(self):
+ return [e for e in self.c if e["isError"]]
+
+
+ def _check(self):
+ """
+ Check the output of the log observer to see if the error is present.
+ """
+ c2 = self._loggedErrors()
+ self.assertEqual(len(c2), 2)
+ c2[1]["failure"].trap(ZeroDivisionError)
+ self.flushLoggedErrors(ZeroDivisionError)
+
+ def test_errorLog(self):
+ """
+ Verify that when a L{Deferred} with no references to it is fired,
+ and its final result (the one not handled by any callback) is an
+ exception, that exception will be logged immediately.
+ """
+ defer.Deferred().addCallback(lambda x: 1 / 0).callback(1)
+ gc.collect()
+ self._check()
+
+ def test_errorLogWithInnerFrameRef(self):
+ """
+ Same as L{test_errorLog}, but with an inner frame.
+ """
+ def _subErrorLogWithInnerFrameRef():
+ d = defer.Deferred()
+ d.addCallback(lambda x: 1 / 0)
+ d.callback(1)
+
+ _subErrorLogWithInnerFrameRef()
+ gc.collect()
+ self._check()
+
+ def test_errorLogWithInnerFrameCycle(self):
+ """
+ Same as L{test_errorLogWithInnerFrameRef}, plus create a cycle.
+ """
+ def _subErrorLogWithInnerFrameCycle():
+ d = defer.Deferred()
+ d.addCallback(lambda x, d=d: 1 / 0)
+ d._d = d
+ d.callback(1)
+
+ _subErrorLogWithInnerFrameCycle()
+ gc.collect()
+ self._check()
+
+
+ def test_chainedErrorCleanup(self):
+ """
+ If one Deferred with an error result is returned from a callback on
+ another Deferred, when the first Deferred is garbage collected it does
+ not log its error.
+ """
+ d = defer.Deferred()
+ d.addCallback(lambda ign: defer.fail(RuntimeError("zoop")))
+ d.callback(None)
+
+ # Sanity check - this isn't too interesting, but we do want the original
+ # Deferred to have gotten the failure.
+ results = []
+ errors = []
+ d.addCallbacks(results.append, errors.append)
+ self.assertEqual(results, [])
+ self.assertEqual(len(errors), 1)
+ errors[0].trap(Exception)
+
+ # Get rid of any references we might have to the inner Deferred (none of
+ # these should really refer to it, but we're just being safe).
+ del results, errors, d
+ # Force a collection cycle so that there's a chance for an error to be
+ # logged, if it's going to be logged.
+ gc.collect()
+ # And make sure it is not.
+ self.assertEqual(self._loggedErrors(), [])
+
+
+ def test_errorClearedByChaining(self):
+ """
+ If a Deferred with a failure result has an errback which chains it to
+ another Deferred, the initial failure is cleared by the errback so it is
+ not logged.
+ """
+ # Start off with a Deferred with a failure for a result
+ bad = defer.fail(Exception("oh no"))
+ good = defer.Deferred()
+ # Give it a callback that chains it to another Deferred
+ bad.addErrback(lambda ignored: good)
+ # That's all, clean it up. No Deferred here still has a failure result,
+ # so nothing should be logged.
+ good = bad = None
+ gc.collect()
+ self.assertEqual(self._loggedErrors(), [])
+
+
+
+class DeferredTestCaseII(unittest.TestCase):
+ def setUp(self):
+ self.callbackRan = 0
+
+ def testDeferredListEmpty(self):
+ """Testing empty DeferredList."""
+ dl = defer.DeferredList([])
+ dl.addCallback(self.cb_empty)
+
+ def cb_empty(self, res):
+ self.callbackRan = 1
+ self.assertEqual([], res)
+
+ def tearDown(self):
+ self.failUnless(self.callbackRan, "Callback was never run.")
+
+class OtherPrimitives(unittest.TestCase):
+ def _incr(self, result):
+ self.counter += 1
+
+ def setUp(self):
+ self.counter = 0
+
+ def testLock(self):
+ lock = defer.DeferredLock()
+ lock.acquire().addCallback(self._incr)
+ self.failUnless(lock.locked)
+ self.assertEqual(self.counter, 1)
+
+ lock.acquire().addCallback(self._incr)
+ self.failUnless(lock.locked)
+ self.assertEqual(self.counter, 1)
+
+ lock.release()
+ self.failUnless(lock.locked)
+ self.assertEqual(self.counter, 2)
+
+ lock.release()
+ self.failIf(lock.locked)
+ self.assertEqual(self.counter, 2)
+
+ self.assertRaises(TypeError, lock.run)
+
+ firstUnique = object()
+ secondUnique = object()
+
+ controlDeferred = defer.Deferred()
+ def helper(self, b):
+ self.b = b
+ return controlDeferred
+
+ resultDeferred = lock.run(helper, self=self, b=firstUnique)
+ self.failUnless(lock.locked)
+ self.assertEqual(self.b, firstUnique)
+
+ resultDeferred.addCallback(lambda x: setattr(self, 'result', x))
+
+ lock.acquire().addCallback(self._incr)
+ self.failUnless(lock.locked)
+ self.assertEqual(self.counter, 2)
+
+ controlDeferred.callback(secondUnique)
+ self.assertEqual(self.result, secondUnique)
+ self.failUnless(lock.locked)
+ self.assertEqual(self.counter, 3)
+
+ d = lock.acquire().addBoth(lambda x: setattr(self, 'result', x))
+ d.cancel()
+ self.assertEqual(self.result.type, defer.CancelledError)
+
+ lock.release()
+ self.failIf(lock.locked)
+
+
+ def test_cancelLockAfterAcquired(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredLock} that already
+ has the lock, the cancel should have no effect.
+ """
+ def _failOnErrback(_):
+ self.fail("Unexpected errback call!")
+ lock = defer.DeferredLock()
+ d = lock.acquire()
+ d.addErrback(_failOnErrback)
+ d.cancel()
+
+
+ def test_cancelLockBeforeAcquired(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredLock} that does not
+ yet have the lock (i.e., the L{Deferred} has not fired), the cancel
+ should cause a L{defer.CancelledError} failure.
+ """
+ lock = defer.DeferredLock()
+ lock.acquire()
+ d = lock.acquire()
+ self.assertFailure(d, defer.CancelledError)
+ d.cancel()
+
+
+ def testSemaphore(self):
+ N = 13
+ sem = defer.DeferredSemaphore(N)
+
+ controlDeferred = defer.Deferred()
+ def helper(self, arg):
+ self.arg = arg
+ return controlDeferred
+
+ results = []
+ uniqueObject = object()
+ resultDeferred = sem.run(helper, self=self, arg=uniqueObject)
+ resultDeferred.addCallback(results.append)
+ resultDeferred.addCallback(self._incr)
+ self.assertEqual(results, [])
+ self.assertEqual(self.arg, uniqueObject)
+ controlDeferred.callback(None)
+ self.assertEqual(results.pop(), None)
+ self.assertEqual(self.counter, 1)
+
+ self.counter = 0
+ for i in range(1, 1 + N):
+ sem.acquire().addCallback(self._incr)
+ self.assertEqual(self.counter, i)
+
+
+ success = []
+ def fail(r):
+ success.append(False)
+ def succeed(r):
+ success.append(True)
+ d = sem.acquire().addCallbacks(fail, succeed)
+ d.cancel()
+ self.assertEqual(success, [True])
+
+ sem.acquire().addCallback(self._incr)
+ self.assertEqual(self.counter, N)
+
+ sem.release()
+ self.assertEqual(self.counter, N + 1)
+
+ for i in range(1, 1 + N):
+ sem.release()
+ self.assertEqual(self.counter, N + 1)
+
+
+ def test_semaphoreInvalidTokens(self):
+ """
+ If the token count passed to L{DeferredSemaphore} is less than one
+ then L{ValueError} is raised.
+ """
+ self.assertRaises(ValueError, defer.DeferredSemaphore, 0)
+ self.assertRaises(ValueError, defer.DeferredSemaphore, -1)
+
+
+ def test_cancelSemaphoreAfterAcquired(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredSemaphore} that
+ already has the semaphore, the cancel should have no effect.
+ """
+ def _failOnErrback(_):
+ self.fail("Unexpected errback call!")
+
+ sem = defer.DeferredSemaphore(1)
+ d = sem.acquire()
+ d.addErrback(_failOnErrback)
+ d.cancel()
+
+
+ def test_cancelSemaphoreBeforeAcquired(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredSemaphore} that does
+ not yet have the semaphore (i.e., the L{Deferred} has not fired),
+ the cancel should cause a L{defer.CancelledError} failure.
+ """
+ sem = defer.DeferredSemaphore(1)
+ sem.acquire()
+ d = sem.acquire()
+ self.assertFailure(d, defer.CancelledError)
+ d.cancel()
+ return d
+
+
+ def testQueue(self):
+ N, M = 2, 2
+ queue = defer.DeferredQueue(N, M)
+
+ gotten = []
+
+ for i in range(M):
+ queue.get().addCallback(gotten.append)
+ self.assertRaises(defer.QueueUnderflow, queue.get)
+
+ for i in range(M):
+ queue.put(i)
+ self.assertEqual(gotten, range(i + 1))
+ for i in range(N):
+ queue.put(N + i)
+ self.assertEqual(gotten, range(M))
+ self.assertRaises(defer.QueueOverflow, queue.put, None)
+
+ gotten = []
+ for i in range(N):
+ queue.get().addCallback(gotten.append)
+ self.assertEqual(gotten, range(N, N + i + 1))
+
+ queue = defer.DeferredQueue()
+ gotten = []
+ for i in range(N):
+ queue.get().addCallback(gotten.append)
+ for i in range(N):
+ queue.put(i)
+ self.assertEqual(gotten, range(N))
+
+ queue = defer.DeferredQueue(size=0)
+ self.assertRaises(defer.QueueOverflow, queue.put, None)
+
+ queue = defer.DeferredQueue(backlog=0)
+ self.assertRaises(defer.QueueUnderflow, queue.get)
+
+
+ def test_cancelQueueAfterSynchronousGet(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredQueue} that already has
+ a result, the cancel should have no effect.
+ """
+ def _failOnErrback(_):
+ self.fail("Unexpected errback call!")
+
+ queue = defer.DeferredQueue()
+ d = queue.get()
+ d.addErrback(_failOnErrback)
+ queue.put(None)
+ d.cancel()
+
+
+ def test_cancelQueueAfterGet(self):
+ """
+ When canceling a L{Deferred} from a L{DeferredQueue} that does not
+ have a result (i.e., the L{Deferred} has not fired), the cancel
+ causes a L{defer.CancelledError} failure. If the queue has a result
+ later on, it doesn't try to fire the deferred.
+ """
+ queue = defer.DeferredQueue()
+ d = queue.get()
+ self.assertFailure(d, defer.CancelledError)
+ d.cancel()
+ def cb(ignore):
+ # If the deferred is still linked with the deferred queue, it will
+ # fail with an AlreadyCalledError
+ queue.put(None)
+ return queue.get().addCallback(self.assertIdentical, None)
+ return d.addCallback(cb)
+
+
+
+class DeferredFilesystemLockTestCase(unittest.TestCase):
+ """
+ Test the behavior of L{DeferredFilesystemLock}
+ """
+ def setUp(self):
+ self.clock = Clock()
+ self.lock = defer.DeferredFilesystemLock(self.mktemp(),
+ scheduler=self.clock)
+
+
+ def test_waitUntilLockedWithNoLock(self):
+ """
+ Test that the lock can be acquired when no lock is held
+ """
+ d = self.lock.deferUntilLocked(timeout=1)
+
+ return d
+
+
+ def test_waitUntilLockedWithTimeoutLocked(self):
+ """
+ Test that the lock can not be acquired when the lock is held
+ for longer than the timeout.
+ """
+ self.failUnless(self.lock.lock())
+
+ d = self.lock.deferUntilLocked(timeout=5.5)
+ self.assertFailure(d, defer.TimeoutError)
+
+ self.clock.pump([1] * 10)
+
+ return d
+
+
+ def test_waitUntilLockedWithTimeoutUnlocked(self):
+ """
+ Test that a lock can be acquired while a lock is held
+ but the lock is unlocked before our timeout.
+ """
+ def onTimeout(f):
+ f.trap(defer.TimeoutError)
+ self.fail("Should not have timed out")
+
+ self.failUnless(self.lock.lock())
+
+ self.clock.callLater(1, self.lock.unlock)
+ d = self.lock.deferUntilLocked(timeout=10)
+ d.addErrback(onTimeout)
+
+ self.clock.pump([1] * 10)
+
+ return d
+
+
+ def test_defaultScheduler(self):
+ """
+ Test that the default scheduler is set up properly.
+ """
+ lock = defer.DeferredFilesystemLock(self.mktemp())
+
+ self.assertEqual(lock._scheduler, reactor)
+
+
+ def test_concurrentUsage(self):
+ """
+ Test that an appropriate exception is raised when attempting
+ to use deferUntilLocked concurrently.
+ """
+ self.lock.lock()
+ self.clock.callLater(1, self.lock.unlock)
+
+ d = self.lock.deferUntilLocked()
+ d2 = self.lock.deferUntilLocked()
+
+ self.assertFailure(d2, defer.AlreadyTryingToLockError)
+
+ self.clock.advance(1)
+
+ return d
+
+
+ def test_multipleUsages(self):
+ """
+ Test that a DeferredFilesystemLock can be used multiple times
+ """
+ def lockAquired(ign):
+ self.lock.unlock()
+ d = self.lock.deferUntilLocked()
+ return d
+
+ self.lock.lock()
+ self.clock.callLater(1, self.lock.unlock)
+
+ d = self.lock.deferUntilLocked()
+ d.addCallback(lockAquired)
+
+ self.clock.advance(1)
+
+ return d
diff --git a/twisted/test/test_defgen.py b/twisted/test/test_defgen.py
new file mode 100644
index 0000000..1d1ef1c
--- /dev/null
+++ b/twisted/test/test_defgen.py
@@ -0,0 +1,309 @@
+from __future__ import generators, nested_scopes
+
+import sys
+
+from twisted.internet import reactor
+
+from twisted.trial import unittest
+
+from twisted.internet.defer import waitForDeferred, deferredGenerator, Deferred
+from twisted.internet import defer
+
+def getThing():
+ d = Deferred()
+ reactor.callLater(0, d.callback, "hi")
+ return d
+
+def getOwie():
+ d = Deferred()
+ def CRAP():
+ d.errback(ZeroDivisionError('OMG'))
+ reactor.callLater(0, CRAP)
+ return d
+
+# NOTE: most of the tests in DeferredGeneratorTests are duplicated
+# with slightly different syntax for the InlineCallbacksTests below.
+
+class TerminalException(Exception):
+ pass
+
+class BaseDefgenTests:
+ """
+ This class sets up a bunch of test cases which will test both
+ deferredGenerator and inlineCallbacks based generators. The subclasses
+ DeferredGeneratorTests and InlineCallbacksTests each provide the actual
+ generator implementations tested.
+ """
+
+ def testBasics(self):
+ """
+ Test that a normal deferredGenerator works. Tests yielding a
+ deferred which callbacks, as well as a deferred errbacks. Also
+ ensures returning a final value works.
+ """
+
+ return self._genBasics().addCallback(self.assertEqual, 'WOOSH')
+
+ def testBuggy(self):
+ """
+ Ensure that a buggy generator properly signals a Failure
+ condition on result deferred.
+ """
+ return self.assertFailure(self._genBuggy(), ZeroDivisionError)
+
+ def testNothing(self):
+ """Test that a generator which never yields results in None."""
+
+ return self._genNothing().addCallback(self.assertEqual, None)
+
+ def testHandledTerminalFailure(self):
+ """
+ Create a Deferred Generator which yields a Deferred which fails and
+ handles the exception which results. Assert that the Deferred
+ Generator does not errback its Deferred.
+ """
+ return self._genHandledTerminalFailure().addCallback(self.assertEqual, None)
+
+ def testHandledTerminalAsyncFailure(self):
+ """
+ Just like testHandledTerminalFailure, only with a Deferred which fires
+ asynchronously with an error.
+ """
+ d = defer.Deferred()
+ deferredGeneratorResultDeferred = self._genHandledTerminalAsyncFailure(d)
+ d.errback(TerminalException("Handled Terminal Failure"))
+ return deferredGeneratorResultDeferred.addCallback(
+ self.assertEqual, None)
+
+ def testStackUsage(self):
+ """
+ Make sure we don't blow the stack when yielding immediately
+ available deferreds.
+ """
+ return self._genStackUsage().addCallback(self.assertEqual, 0)
+
+ def testStackUsage2(self):
+ """
+ Make sure we don't blow the stack when yielding immediately
+ available values.
+ """
+ return self._genStackUsage2().addCallback(self.assertEqual, 0)
+
+
+
+
+class DeferredGeneratorTests(BaseDefgenTests, unittest.TestCase):
+
+ # First provide all the generator impls necessary for BaseDefgenTests
+ def _genBasics(self):
+
+ x = waitForDeferred(getThing())
+ yield x
+ x = x.getResult()
+
+ self.assertEqual(x, "hi")
+
+ ow = waitForDeferred(getOwie())
+ yield ow
+ try:
+ ow.getResult()
+ except ZeroDivisionError, e:
+ self.assertEqual(str(e), 'OMG')
+ yield "WOOSH"
+ return
+ _genBasics = deferredGenerator(_genBasics)
+
+ def _genBuggy(self):
+ yield waitForDeferred(getThing())
+ 1/0
+ _genBuggy = deferredGenerator(_genBuggy)
+
+
+ def _genNothing(self):
+ if 0: yield 1
+ _genNothing = deferredGenerator(_genNothing)
+
+ def _genHandledTerminalFailure(self):
+ x = waitForDeferred(defer.fail(TerminalException("Handled Terminal Failure")))
+ yield x
+ try:
+ x.getResult()
+ except TerminalException:
+ pass
+ _genHandledTerminalFailure = deferredGenerator(_genHandledTerminalFailure)
+
+
+ def _genHandledTerminalAsyncFailure(self, d):
+ x = waitForDeferred(d)
+ yield x
+ try:
+ x.getResult()
+ except TerminalException:
+ pass
+ _genHandledTerminalAsyncFailure = deferredGenerator(_genHandledTerminalAsyncFailure)
+
+
+ def _genStackUsage(self):
+ for x in range(5000):
+ # Test with yielding a deferred
+ x = waitForDeferred(defer.succeed(1))
+ yield x
+ x = x.getResult()
+ yield 0
+ _genStackUsage = deferredGenerator(_genStackUsage)
+
+ def _genStackUsage2(self):
+ for x in range(5000):
+ # Test with yielding a random value
+ yield 1
+ yield 0
+ _genStackUsage2 = deferredGenerator(_genStackUsage2)
+
+ # Tests unique to deferredGenerator
+
+ def testDeferredYielding(self):
+ """
+ Ensure that yielding a Deferred directly is trapped as an
+ error.
+ """
+ # See the comment _deferGenerator about d.callback(Deferred).
+ def _genDeferred():
+ yield getThing()
+ _genDeferred = deferredGenerator(_genDeferred)
+
+ return self.assertFailure(_genDeferred(), TypeError)
+
+
+
+## This has to be in a string so the new yield syntax doesn't cause a
+## syntax error in Python 2.4 and before.
+inlineCallbacksTestsSource = '''
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+class InlineCallbacksTests(BaseDefgenTests, unittest.TestCase):
+ # First provide all the generator impls necessary for BaseDefgenTests
+
+ def _genBasics(self):
+
+ x = yield getThing()
+
+ self.assertEqual(x, "hi")
+
+ try:
+ ow = yield getOwie()
+ except ZeroDivisionError, e:
+ self.assertEqual(str(e), 'OMG')
+ returnValue("WOOSH")
+ _genBasics = inlineCallbacks(_genBasics)
+
+ def _genBuggy(self):
+ yield getThing()
+ 1/0
+ _genBuggy = inlineCallbacks(_genBuggy)
+
+
+ def _genNothing(self):
+ if 0: yield 1
+ _genNothing = inlineCallbacks(_genNothing)
+
+
+ def _genHandledTerminalFailure(self):
+ try:
+ x = yield defer.fail(TerminalException("Handled Terminal Failure"))
+ except TerminalException:
+ pass
+ _genHandledTerminalFailure = inlineCallbacks(_genHandledTerminalFailure)
+
+
+ def _genHandledTerminalAsyncFailure(self, d):
+ try:
+ x = yield d
+ except TerminalException:
+ pass
+ _genHandledTerminalAsyncFailure = inlineCallbacks(
+ _genHandledTerminalAsyncFailure)
+
+
+ def _genStackUsage(self):
+ for x in range(5000):
+ # Test with yielding a deferred
+ x = yield defer.succeed(1)
+ returnValue(0)
+ _genStackUsage = inlineCallbacks(_genStackUsage)
+
+ def _genStackUsage2(self):
+ for x in range(5000):
+ # Test with yielding a random value
+ yield 1
+ returnValue(0)
+ _genStackUsage2 = inlineCallbacks(_genStackUsage2)
+
+ # Tests unique to inlineCallbacks
+
+ def testYieldNonDeferrred(self):
+ """
+ Ensure that yielding a non-deferred passes it back as the
+ result of the yield expression.
+ """
+ def _test():
+ x = yield 5
+ returnValue(5)
+ _test = inlineCallbacks(_test)
+
+ return _test().addCallback(self.assertEqual, 5)
+
+ def testReturnNoValue(self):
+ """Ensure a standard python return results in a None result."""
+ def _noReturn():
+ yield 5
+ return
+ _noReturn = inlineCallbacks(_noReturn)
+
+ return _noReturn().addCallback(self.assertEqual, None)
+
+ def testReturnValue(self):
+ """Ensure that returnValue works."""
+ def _return():
+ yield 5
+ returnValue(6)
+ _return = inlineCallbacks(_return)
+
+ return _return().addCallback(self.assertEqual, 6)
+
+
+ def test_nonGeneratorReturn(self):
+ """
+ Ensure that C{TypeError} with a message about L{inlineCallbacks} is
+ raised when a non-generator returns something other than a generator.
+ """
+ def _noYield():
+ return 5
+ _noYield = inlineCallbacks(_noYield)
+
+ self.assertIn("inlineCallbacks",
+ str(self.assertRaises(TypeError, _noYield)))
+
+
+ def test_nonGeneratorReturnValue(self):
+ """
+ Ensure that C{TypeError} with a message about L{inlineCallbacks} is
+ raised when a non-generator calls L{returnValue}.
+ """
+ def _noYield():
+ returnValue(5)
+ _noYield = inlineCallbacks(_noYield)
+
+ self.assertIn("inlineCallbacks",
+ str(self.assertRaises(TypeError, _noYield)))
+
+'''
+
+if sys.version_info > (2, 5):
+ # Load tests
+ exec inlineCallbacksTestsSource
+else:
+ # Make a placeholder test case
+ class InlineCallbacksTests(unittest.TestCase):
+ skip = "defer.defgen doesn't run on python < 2.5."
+ def test_everything(self):
+ pass
diff --git a/twisted/test/test_dict.py b/twisted/test/test_dict.py
new file mode 100644
index 0000000..3ebb67e
--- /dev/null
+++ b/twisted/test/test_dict.py
@@ -0,0 +1,22 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+from twisted.protocols import dict
+
+paramString = "\"This is a dqstring \\w\\i\\t\\h boring stuff like: \\\"\" and t\\hes\\\"e are a\\to\\ms"
+goodparams = ["This is a dqstring with boring stuff like: \"", "and", "thes\"e", "are", "atoms"]
+
+class ParamTest(unittest.TestCase):
+ def testParseParam(self):
+ """Testing command response handling"""
+ params = []
+ rest = paramString
+ while 1:
+ (param, rest) = dict.parseParam(rest)
+ if param == None:
+ break
+ params.append(param)
+ self.assertEqual(params, goodparams)#, "DictClient.parseParam returns unexpected results")
diff --git a/twisted/test/test_digestauth.py b/twisted/test/test_digestauth.py
new file mode 100644
index 0000000..41368a0
--- /dev/null
+++ b/twisted/test/test_digestauth.py
@@ -0,0 +1,671 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.cred._digest} and the associated bits in
+L{twisted.cred.credentials}.
+"""
+
+from zope.interface.verify import verifyObject
+from twisted.trial.unittest import TestCase
+from twisted.python.hashlib import md5, sha1
+from twisted.internet.address import IPv4Address
+from twisted.cred.error import LoginFailed
+from twisted.cred.credentials import calcHA1, calcHA2, IUsernameDigestHash
+from twisted.cred.credentials import calcResponse, DigestCredentialFactory
+
+def b64encode(s):
+ return s.encode('base64').strip()
+
+
+class FakeDigestCredentialFactory(DigestCredentialFactory):
+ """
+ A Fake Digest Credential Factory that generates a predictable
+ nonce and opaque
+ """
+ def __init__(self, *args, **kwargs):
+ super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+ self.privateKey = "0"
+
+
+ def _generateNonce(self):
+ """
+ Generate a static nonce
+ """
+ return '178288758716122392881254770685'
+
+
+ def _getTime(self):
+ """
+ Return a stable time
+ """
+ return 0
+
+
+
+class DigestAuthTests(TestCase):
+ """
+ L{TestCase} mixin class which defines a number of tests for
+ L{DigestCredentialFactory}. Because this mixin defines C{setUp}, it
+ must be inherited before L{TestCase}.
+ """
+ def setUp(self):
+ """
+ Create a DigestCredentialFactory for testing
+ """
+ self.username = "foobar"
+ self.password = "bazquux"
+ self.realm = "test realm"
+ self.algorithm = "md5"
+ self.cnonce = "29fc54aa1641c6fa0e151419361c8f23"
+ self.qop = "auth"
+ self.uri = "/write/"
+ self.clientAddress = IPv4Address('TCP', '10.2.3.4', 43125)
+ self.method = 'GET'
+ self.credentialFactory = DigestCredentialFactory(
+ self.algorithm, self.realm)
+
+
+ def test_MD5HashA1(self, _algorithm='md5', _hash=md5):
+ """
+ L{calcHA1} accepts the C{'md5'} algorithm and returns an MD5 hash of
+ its parameters, excluding the nonce and cnonce.
+ """
+ nonce = 'abc123xyz'
+ hashA1 = calcHA1(_algorithm, self.username, self.realm, self.password,
+ nonce, self.cnonce)
+ a1 = '%s:%s:%s' % (self.username, self.realm, self.password)
+ expected = _hash(a1).hexdigest()
+ self.assertEqual(hashA1, expected)
+
+
+ def test_MD5SessionHashA1(self):
+ """
+ L{calcHA1} accepts the C{'md5-sess'} algorithm and returns an MD5 hash
+ of its parameters, including the nonce and cnonce.
+ """
+ nonce = 'xyz321abc'
+ hashA1 = calcHA1('md5-sess', self.username, self.realm, self.password,
+ nonce, self.cnonce)
+ a1 = '%s:%s:%s' % (self.username, self.realm, self.password)
+ ha1 = md5(a1).digest()
+ a1 = '%s:%s:%s' % (ha1, nonce, self.cnonce)
+ expected = md5(a1).hexdigest()
+ self.assertEqual(hashA1, expected)
+
+
+ def test_SHAHashA1(self):
+ """
+ L{calcHA1} accepts the C{'sha'} algorithm and returns a SHA hash of its
+ parameters, excluding the nonce and cnonce.
+ """
+ self.test_MD5HashA1('sha', sha1)
+
+
+ def test_MD5HashA2Auth(self, _algorithm='md5', _hash=md5):
+ """
+ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
+ its arguments, excluding the entity hash for QOP other than
+ C{'auth-int'}.
+ """
+ method = 'GET'
+ hashA2 = calcHA2(_algorithm, method, self.uri, 'auth', None)
+ a2 = '%s:%s' % (method, self.uri)
+ expected = _hash(a2).hexdigest()
+ self.assertEqual(hashA2, expected)
+
+
+ def test_MD5HashA2AuthInt(self, _algorithm='md5', _hash=md5):
+ """
+ L{calcHA2} accepts the C{'md5'} algorithm and returns an MD5 hash of
+ its arguments, including the entity hash for QOP of C{'auth-int'}.
+ """
+ method = 'GET'
+ hentity = 'foobarbaz'
+ hashA2 = calcHA2(_algorithm, method, self.uri, 'auth-int', hentity)
+ a2 = '%s:%s:%s' % (method, self.uri, hentity)
+ expected = _hash(a2).hexdigest()
+ self.assertEqual(hashA2, expected)
+
+
+ def test_MD5SessHashA2Auth(self):
+ """
+ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth'} and
+ returns the same value as it does for the C{'md5'} algorithm.
+ """
+ self.test_MD5HashA2Auth('md5-sess')
+
+
+ def test_MD5SessHashA2AuthInt(self):
+ """
+ L{calcHA2} accepts the C{'md5-sess'} algorithm and QOP of C{'auth-int'}
+ and returns the same value as it does for the C{'md5'} algorithm.
+ """
+ self.test_MD5HashA2AuthInt('md5-sess')
+
+
+ def test_SHAHashA2Auth(self):
+ """
+ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
+ its arguments, excluding the entity hash for QOP other than
+ C{'auth-int'}.
+ """
+ self.test_MD5HashA2Auth('sha', sha1)
+
+
+ def test_SHAHashA2AuthInt(self):
+ """
+ L{calcHA2} accepts the C{'sha'} algorithm and returns a SHA hash of
+ its arguments, including the entity hash for QOP of C{'auth-int'}.
+ """
+ self.test_MD5HashA2AuthInt('sha', sha1)
+
+
+ def test_MD5HashResponse(self, _algorithm='md5', _hash=md5):
+ """
+ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
+ of its parameters, excluding the nonce count, client nonce, and QoP
+ value if the nonce count and client nonce are C{None}
+ """
+ hashA1 = 'abc123'
+ hashA2 = '789xyz'
+ nonce = 'lmnopq'
+
+ response = '%s:%s:%s' % (hashA1, nonce, hashA2)
+ expected = _hash(response).hexdigest()
+
+ digest = calcResponse(hashA1, hashA2, _algorithm, nonce, None, None,
+ None)
+ self.assertEqual(expected, digest)
+
+
+ def test_MD5SessionHashResponse(self):
+ """
+ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
+ hash of its parameters, excluding the nonce count, client nonce, and
+ QoP value if the nonce count and client nonce are C{None}
+ """
+ self.test_MD5HashResponse('md5-sess')
+
+
+ def test_SHAHashResponse(self):
+ """
+ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
+ of its parameters, excluding the nonce count, client nonce, and QoP
+ value if the nonce count and client nonce are C{None}
+ """
+ self.test_MD5HashResponse('sha', sha1)
+
+
+ def test_MD5HashResponseExtra(self, _algorithm='md5', _hash=md5):
+ """
+ L{calcResponse} accepts the C{'md5'} algorithm and returns an MD5 hash
+ of its parameters, including the nonce count, client nonce, and QoP
+ value if they are specified.
+ """
+ hashA1 = 'abc123'
+ hashA2 = '789xyz'
+ nonce = 'lmnopq'
+ nonceCount = '00000004'
+ clientNonce = 'abcxyz123'
+ qop = 'auth'
+
+ response = '%s:%s:%s:%s:%s:%s' % (
+ hashA1, nonce, nonceCount, clientNonce, qop, hashA2)
+ expected = _hash(response).hexdigest()
+
+ digest = calcResponse(
+ hashA1, hashA2, _algorithm, nonce, nonceCount, clientNonce, qop)
+ self.assertEqual(expected, digest)
+
+
+ def test_MD5SessionHashResponseExtra(self):
+ """
+ L{calcResponse} accepts the C{'md5-sess'} algorithm and returns an MD5
+ hash of its parameters, including the nonce count, client nonce, and
+ QoP value if they are specified.
+ """
+ self.test_MD5HashResponseExtra('md5-sess')
+
+
+ def test_SHAHashResponseExtra(self):
+ """
+ L{calcResponse} accepts the C{'sha'} algorithm and returns a SHA hash
+ of its parameters, including the nonce count, client nonce, and QoP
+ value if they are specified.
+ """
+ self.test_MD5HashResponseExtra('sha', sha1)
+
+
+ def formatResponse(self, quotes=True, **kw):
+ """
+ Format all given keyword arguments and their values suitably for use as
+ the value of an HTTP header.
+
+ @types quotes: C{bool}
+ @param quotes: A flag indicating whether to quote the values of each
+ field in the response.
+
+ @param **kw: Keywords and C{str} values which will be treated as field
+ name/value pairs to include in the result.
+
+ @rtype: C{str}
+ @return: The given fields formatted for use as an HTTP header value.
+ """
+ if 'username' not in kw:
+ kw['username'] = self.username
+ if 'realm' not in kw:
+ kw['realm'] = self.realm
+ if 'algorithm' not in kw:
+ kw['algorithm'] = self.algorithm
+ if 'qop' not in kw:
+ kw['qop'] = self.qop
+ if 'cnonce' not in kw:
+ kw['cnonce'] = self.cnonce
+ if 'uri' not in kw:
+ kw['uri'] = self.uri
+ if quotes:
+ quote = '"'
+ else:
+ quote = ''
+ return ', '.join([
+ '%s=%s%s%s' % (k, quote, v, quote)
+ for (k, v)
+ in kw.iteritems()
+ if v is not None])
+
+
+ def getDigestResponse(self, challenge, ncount):
+ """
+ Calculate the response for the given challenge
+ """
+ nonce = challenge.get('nonce')
+ algo = challenge.get('algorithm').lower()
+ qop = challenge.get('qop')
+
+ ha1 = calcHA1(
+ algo, self.username, self.realm, self.password, nonce, self.cnonce)
+ ha2 = calcHA2(algo, "GET", self.uri, qop, None)
+ expected = calcResponse(ha1, ha2, algo, nonce, ncount, self.cnonce, qop)
+ return expected
+
+
+ def test_response(self, quotes=True):
+ """
+ L{DigestCredentialFactory.decode} accepts a digest challenge response
+ and parses it into an L{IUsernameHashedPassword} provider.
+ """
+ challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
+
+ nc = "00000001"
+ clientResponse = self.formatResponse(
+ quotes=quotes,
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+ creds = self.credentialFactory.decode(
+ clientResponse, self.method, self.clientAddress.host)
+ self.assertTrue(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+
+ def test_responseWithoutQuotes(self):
+ """
+ L{DigestCredentialFactory.decode} accepts a digest challenge response
+ which does not quote the values of its fields and parses it into an
+ L{IUsernameHashedPassword} provider in the same way it would a
+ response which included quoted field values.
+ """
+ self.test_response(False)
+
+
+ def test_caseInsensitiveAlgorithm(self):
+ """
+ The case of the algorithm value in the response is ignored when
+ checking the credentials.
+ """
+ self.algorithm = 'MD5'
+ self.test_response()
+
+
+ def test_md5DefaultAlgorithm(self):
+ """
+ The algorithm defaults to MD5 if it is not supplied in the response.
+ """
+ self.algorithm = None
+ self.test_response()
+
+
+ def test_responseWithoutClientIP(self):
+ """
+ L{DigestCredentialFactory.decode} accepts a digest challenge response
+ even if the client address it is passed is C{None}.
+ """
+ challenge = self.credentialFactory.getChallenge(None)
+
+ nc = "00000001"
+ clientResponse = self.formatResponse(
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+ creds = self.credentialFactory.decode(clientResponse, self.method, None)
+ self.assertTrue(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+
+ def test_multiResponse(self):
+ """
+ L{DigestCredentialFactory.decode} handles multiple responses to a
+ single challenge.
+ """
+ challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
+
+ nc = "00000001"
+ clientResponse = self.formatResponse(
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+
+ creds = self.credentialFactory.decode(clientResponse, self.method,
+ self.clientAddress.host)
+ self.assertTrue(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+ nc = "00000002"
+ clientResponse = self.formatResponse(
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+
+ creds = self.credentialFactory.decode(clientResponse, self.method,
+ self.clientAddress.host)
+ self.assertTrue(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+
+ def test_failsWithDifferentMethod(self):
+ """
+ L{DigestCredentialFactory.decode} returns an L{IUsernameHashedPassword}
+ provider which rejects a correct password for the given user if the
+ challenge response request is made using a different HTTP method than
+ was used to request the initial challenge.
+ """
+ challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
+
+ nc = "00000001"
+ clientResponse = self.formatResponse(
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+ creds = self.credentialFactory.decode(clientResponse, 'POST',
+ self.clientAddress.host)
+ self.assertFalse(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+
+ def test_noUsername(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
+ has no username field or if the username field is empty.
+ """
+ # Check for no username
+ e = self.assertRaises(
+ LoginFailed,
+ self.credentialFactory.decode,
+ self.formatResponse(username=None),
+ self.method, self.clientAddress.host)
+ self.assertEqual(str(e), "Invalid response, no username given.")
+
+ # Check for an empty username
+ e = self.assertRaises(
+ LoginFailed,
+ self.credentialFactory.decode,
+ self.formatResponse(username=""),
+ self.method, self.clientAddress.host)
+ self.assertEqual(str(e), "Invalid response, no username given.")
+
+
+ def test_noNonce(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
+ has no nonce.
+ """
+ e = self.assertRaises(
+ LoginFailed,
+ self.credentialFactory.decode,
+ self.formatResponse(opaque="abc123"),
+ self.method, self.clientAddress.host)
+ self.assertEqual(str(e), "Invalid response, no nonce given.")
+
+
+ def test_noOpaque(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} if the response
+ has no opaque.
+ """
+ e = self.assertRaises(
+ LoginFailed,
+ self.credentialFactory.decode,
+ self.formatResponse(),
+ self.method, self.clientAddress.host)
+ self.assertEqual(str(e), "Invalid response, no opaque given.")
+
+
+ def test_checkHash(self):
+ """
+ L{DigestCredentialFactory.decode} returns an L{IUsernameDigestHash}
+ provider which can verify a hash of the form 'username:realm:password'.
+ """
+ challenge = self.credentialFactory.getChallenge(self.clientAddress.host)
+
+ nc = "00000001"
+ clientResponse = self.formatResponse(
+ nonce=challenge['nonce'],
+ response=self.getDigestResponse(challenge, nc),
+ nc=nc,
+ opaque=challenge['opaque'])
+
+ creds = self.credentialFactory.decode(clientResponse, self.method,
+ self.clientAddress.host)
+ self.assertTrue(verifyObject(IUsernameDigestHash, creds))
+
+ cleartext = '%s:%s:%s' % (self.username, self.realm, self.password)
+ hash = md5(cleartext)
+ self.assertTrue(creds.checkHash(hash.hexdigest()))
+ hash.update('wrong')
+ self.assertFalse(creds.checkHash(hash.hexdigest()))
+
+
+ def test_invalidOpaque(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
+ value does not contain all the required parts.
+ """
+ credentialFactory = FakeDigestCredentialFactory(self.algorithm,
+ self.realm)
+ challenge = credentialFactory.getChallenge(self.clientAddress.host)
+
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ 'badOpaque',
+ challenge['nonce'],
+ self.clientAddress.host)
+ self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
+
+ badOpaque = 'foo-' + b64encode('nonce,clientip')
+
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badOpaque,
+ challenge['nonce'],
+ self.clientAddress.host)
+ self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
+
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ '',
+ challenge['nonce'],
+ self.clientAddress.host)
+ self.assertEqual(str(exc), 'Invalid response, invalid opaque value')
+
+ badOpaque = (
+ 'foo-' + b64encode('%s,%s,foobar' % (
+ challenge['nonce'],
+ self.clientAddress.host)))
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badOpaque,
+ challenge['nonce'],
+ self.clientAddress.host)
+ self.assertEqual(
+ str(exc), 'Invalid response, invalid opaque/time values')
+
+
+ def test_incompatibleNonce(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
+ nonce from the response does not match the nonce encoded in the opaque.
+ """
+ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
+ challenge = credentialFactory.getChallenge(self.clientAddress.host)
+
+ badNonceOpaque = credentialFactory._generateOpaque(
+ '1234567890',
+ self.clientAddress.host)
+
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badNonceOpaque,
+ challenge['nonce'],
+ self.clientAddress.host)
+ self.assertEqual(
+ str(exc),
+ 'Invalid response, incompatible opaque/nonce values')
+
+ exc = self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badNonceOpaque,
+ '',
+ self.clientAddress.host)
+ self.assertEqual(
+ str(exc),
+ 'Invalid response, incompatible opaque/nonce values')
+
+
+ def test_incompatibleClientIP(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the
+ request comes from a client IP other than what is encoded in the
+ opaque.
+ """
+ credentialFactory = FakeDigestCredentialFactory(self.algorithm, self.realm)
+ challenge = credentialFactory.getChallenge(self.clientAddress.host)
+
+ badAddress = '10.0.0.1'
+ # Sanity check
+ self.assertNotEqual(self.clientAddress.host, badAddress)
+
+ badNonceOpaque = credentialFactory._generateOpaque(
+ challenge['nonce'], badAddress)
+
+ self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badNonceOpaque,
+ challenge['nonce'],
+ self.clientAddress.host)
+
+
+ def test_oldNonce(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the given
+ opaque is older than C{DigestCredentialFactory.CHALLENGE_LIFETIME_SECS}
+ """
+ credentialFactory = FakeDigestCredentialFactory(self.algorithm,
+ self.realm)
+ challenge = credentialFactory.getChallenge(self.clientAddress.host)
+
+ key = '%s,%s,%s' % (challenge['nonce'],
+ self.clientAddress.host,
+ '-137876876')
+ digest = md5(key + credentialFactory.privateKey).hexdigest()
+ ekey = b64encode(key)
+
+ oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
+
+ self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ oldNonceOpaque,
+ challenge['nonce'],
+ self.clientAddress.host)
+
+
+ def test_mismatchedOpaqueChecksum(self):
+ """
+ L{DigestCredentialFactory.decode} raises L{LoginFailed} when the opaque
+ checksum fails verification.
+ """
+ credentialFactory = FakeDigestCredentialFactory(self.algorithm,
+ self.realm)
+ challenge = credentialFactory.getChallenge(self.clientAddress.host)
+
+ key = '%s,%s,%s' % (challenge['nonce'],
+ self.clientAddress.host,
+ '0')
+
+ digest = md5(key + 'this is not the right pkey').hexdigest()
+ badChecksum = '%s-%s' % (digest, b64encode(key))
+
+ self.assertRaises(
+ LoginFailed,
+ credentialFactory._verifyOpaque,
+ badChecksum,
+ challenge['nonce'],
+ self.clientAddress.host)
+
+
+ def test_incompatibleCalcHA1Options(self):
+ """
+ L{calcHA1} raises L{TypeError} when any of the pszUsername, pszRealm,
+ or pszPassword arguments are specified with the preHA1 keyword
+ argument.
+ """
+ arguments = (
+ ("user", "realm", "password", "preHA1"),
+ (None, "realm", None, "preHA1"),
+ (None, None, "password", "preHA1"),
+ )
+
+ for pszUsername, pszRealm, pszPassword, preHA1 in arguments:
+ self.assertRaises(
+ TypeError,
+ calcHA1,
+ "md5",
+ pszUsername,
+ pszRealm,
+ pszPassword,
+ "nonce",
+ "cnonce",
+ preHA1=preHA1)
+
+
+ def test_noNewlineOpaque(self):
+ """
+ L{DigestCredentialFactory._generateOpaque} returns a value without
+ newlines, regardless of the length of the nonce.
+ """
+ opaque = self.credentialFactory._generateOpaque(
+ "long nonce " * 10, None)
+ self.assertNotIn('\n', opaque)
diff --git a/twisted/test/test_dirdbm.py b/twisted/test/test_dirdbm.py
new file mode 100644
index 0000000..8bd240f
--- /dev/null
+++ b/twisted/test/test_dirdbm.py
@@ -0,0 +1,170 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for dirdbm module.
+"""
+
+import os, shutil, glob
+
+from twisted.trial import unittest
+from twisted.persisted import dirdbm
+
+
+
+class DirDbmTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.path = self.mktemp()
+ self.dbm = dirdbm.open(self.path)
+ self.items = (('abc', 'foo'), ('/lalal', '\000\001'), ('\000\012', 'baz'))
+
+
+ def testAll(self):
+ k = "//==".decode("base64")
+ self.dbm[k] = "a"
+ self.dbm[k] = "a"
+ self.assertEqual(self.dbm[k], "a")
+
+
+ def testRebuildInteraction(self):
+ from twisted.persisted import dirdbm
+ from twisted.python import rebuild
+
+ s = dirdbm.Shelf('dirdbm.rebuild.test')
+ s['key'] = 'value'
+ rebuild.rebuild(dirdbm)
+ # print s['key']
+
+
+ def testDbm(self):
+ d = self.dbm
+
+ # insert keys
+ keys = []
+ values = set()
+ for k, v in self.items:
+ d[k] = v
+ keys.append(k)
+ values.add(v)
+ keys.sort()
+
+ # check they exist
+ for k, v in self.items:
+ assert d.has_key(k), "has_key() failed"
+ assert d[k] == v, "database has wrong value"
+
+ # check non existent key
+ try:
+ d["XXX"]
+ except KeyError:
+ pass
+ else:
+ assert 0, "didn't raise KeyError on non-existent key"
+
+ # check keys(), values() and items()
+ dbkeys = list(d.keys())
+ dbvalues = set(d.values())
+ dbitems = set(d.items())
+ dbkeys.sort()
+ items = set(self.items)
+ assert keys == dbkeys, ".keys() output didn't match: %s != %s" % (repr(keys), repr(dbkeys))
+ assert values == dbvalues, ".values() output didn't match: %s != %s" % (repr(values), repr(dbvalues))
+ assert items == dbitems, "items() didn't match: %s != %s" % (repr(items), repr(dbitems))
+
+ copyPath = self.mktemp()
+ d2 = d.copyTo(copyPath)
+
+ copykeys = list(d.keys())
+ copyvalues = set(d.values())
+ copyitems = set(d.items())
+ copykeys.sort()
+
+ assert dbkeys == copykeys, ".copyTo().keys() didn't match: %s != %s" % (repr(dbkeys), repr(copykeys))
+ assert dbvalues == copyvalues, ".copyTo().values() didn't match: %s != %s" % (repr(dbvalues), repr(copyvalues))
+ assert dbitems == copyitems, ".copyTo().items() didn't match: %s != %s" % (repr(dbkeys), repr(copyitems))
+
+ d2.clear()
+ assert len(d2.keys()) == len(d2.values()) == len(d2.items()) == 0, ".clear() failed"
+ shutil.rmtree(copyPath)
+
+ # delete items
+ for k, v in self.items:
+ del d[k]
+ assert not d.has_key(k), "has_key() even though we deleted it"
+ assert len(d.keys()) == 0, "database has keys"
+ assert len(d.values()) == 0, "database has values"
+ assert len(d.items()) == 0, "database has items"
+
+
+ def testModificationTime(self):
+ import time
+ # the mtime value for files comes from a different place than the
+ # gettimeofday() system call. On linux, gettimeofday() can be
+ # slightly ahead (due to clock drift which gettimeofday() takes into
+ # account but which open()/write()/close() do not), and if we are
+ # close to the edge of the next second, time.time() can give a value
+ # which is larger than the mtime which results from a subsequent
+ # write(). I consider this a kernel bug, but it is beyond the scope
+ # of this test. Thus we keep the range of acceptability to 3 seconds time.
+ # -warner
+ self.dbm["k"] = "v"
+ self.assert_(abs(time.time() - self.dbm.getModificationTime("k")) <= 3)
+
+
+ def testRecovery(self):
+ """DirDBM: test recovery from directory after a faked crash"""
+ k = self.dbm._encode("key1")
+ f = open(os.path.join(self.path, k + ".rpl"), "wb")
+ f.write("value")
+ f.close()
+
+ k2 = self.dbm._encode("key2")
+ f = open(os.path.join(self.path, k2), "wb")
+ f.write("correct")
+ f.close()
+ f = open(os.path.join(self.path, k2 + ".rpl"), "wb")
+ f.write("wrong")
+ f.close()
+
+ f = open(os.path.join(self.path, "aa.new"), "wb")
+ f.write("deleted")
+ f.close()
+
+ dbm = dirdbm.DirDBM(self.path)
+ assert dbm["key1"] == "value"
+ assert dbm["key2"] == "correct"
+ assert not glob.glob(os.path.join(self.path, "*.new"))
+ assert not glob.glob(os.path.join(self.path, "*.rpl"))
+
+
+ def test_nonStringKeys(self):
+ """
+ L{dirdbm.DirDBM} operations only support string keys: other types
+ should raise a C{AssertionError}. This really ought to be a
+ C{TypeError}, but it'll stay like this for backward compatibility.
+ """
+ self.assertRaises(AssertionError, self.dbm.__setitem__, 2, "3")
+ try:
+ self.assertRaises(AssertionError, self.dbm.__setitem__, "2", 3)
+ except unittest.FailTest:
+ # dirdbm.Shelf.__setitem__ supports non-string values
+ self.assertIsInstance(self.dbm, dirdbm.Shelf)
+ self.assertRaises(AssertionError, self.dbm.__getitem__, 2)
+ self.assertRaises(AssertionError, self.dbm.__delitem__, 2)
+ self.assertRaises(AssertionError, self.dbm.has_key, 2)
+ self.assertRaises(AssertionError, self.dbm.__contains__, 2)
+ self.assertRaises(AssertionError, self.dbm.getModificationTime, 2)
+
+
+
+class ShelfTestCase(DirDbmTestCase):
+
+ def setUp(self):
+ self.path = self.mktemp()
+ self.dbm = dirdbm.Shelf(self.path)
+ self.items = (('abc', 'foo'), ('/lalal', '\000\001'), ('\000\012', 'baz'),
+ ('int', 12), ('float', 12.0), ('tuple', (None, 12)))
+
+
+testCases = [DirDbmTestCase, ShelfTestCase]
diff --git a/twisted/test/test_doc.py b/twisted/test/test_doc.py
new file mode 100644
index 0000000..795fd87
--- /dev/null
+++ b/twisted/test/test_doc.py
@@ -0,0 +1,104 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import inspect, glob
+from os import path
+
+from twisted.trial import unittest
+from twisted.python import reflect
+from twisted.python.modules import getModule
+
+
+
+def errorInFile(f, line=17, name=''):
+ """
+ Return a filename formatted so emacs will recognize it as an error point
+
+ @param line: Line number in file. Defaults to 17 because that's about how
+ long the copyright headers are.
+ """
+ return '%s:%d:%s' % (f, line, name)
+ # return 'File "%s", line %d, in %s' % (f, line, name)
+
+
+class DocCoverage(unittest.TestCase):
+ """
+ Looking for docstrings in all modules and packages.
+ """
+ def setUp(self):
+ self.packageNames = []
+ for mod in getModule('twisted').walkModules():
+ if mod.isPackage():
+ self.packageNames.append(mod.name)
+
+
+ def testModules(self):
+ """
+ Looking for docstrings in all modules.
+ """
+ docless = []
+ for packageName in self.packageNames:
+ if packageName in ('twisted.test',):
+ # because some stuff in here behaves oddly when imported
+ continue
+ try:
+ package = reflect.namedModule(packageName)
+ except ImportError, e:
+ # This is testing doc coverage, not importability.
+ # (Really, I don't want to deal with the fact that I don't
+ # have pyserial installed.)
+ # print e
+ pass
+ else:
+ docless.extend(self.modulesInPackage(packageName, package))
+ self.failIf(docless, "No docstrings in module files:\n"
+ "%s" % ('\n'.join(map(errorInFile, docless)),))
+
+
+ def modulesInPackage(self, packageName, package):
+ docless = []
+ directory = path.dirname(package.__file__)
+ for modfile in glob.glob(path.join(directory, '*.py')):
+ moduleName = inspect.getmodulename(modfile)
+ if moduleName == '__init__':
+ # These are tested by test_packages.
+ continue
+ elif moduleName in ('spelunk_gnome','gtkmanhole'):
+ # argh special case pygtk evil argh. How does epydoc deal
+ # with this?
+ continue
+ try:
+ module = reflect.namedModule('.'.join([packageName,
+ moduleName]))
+ except Exception, e:
+ # print moduleName, "misbehaved:", e
+ pass
+ else:
+ if not inspect.getdoc(module):
+ docless.append(modfile)
+ return docless
+
+
+ def testPackages(self):
+ """
+ Looking for docstrings in all packages.
+ """
+ docless = []
+ for packageName in self.packageNames:
+ try:
+ package = reflect.namedModule(packageName)
+ except Exception, e:
+ # This is testing doc coverage, not importability.
+ # (Really, I don't want to deal with the fact that I don't
+ # have pyserial installed.)
+ # print e
+ pass
+ else:
+ if not inspect.getdoc(package):
+ docless.append(package.__file__.replace('.pyc','.py'))
+ self.failIf(docless, "No docstrings for package files\n"
+ "%s" % ('\n'.join(map(errorInFile, docless),)))
+
+
+ # This test takes a while and doesn't come close to passing. :(
+ testModules.skip = "Activate me when you feel like writing docstrings, and fixing GTK crashing bugs."
diff --git a/twisted/test/test_epoll.py b/twisted/test/test_epoll.py
new file mode 100644
index 0000000..b96e06f
--- /dev/null
+++ b/twisted/test/test_epoll.py
@@ -0,0 +1,158 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for epoll wrapper.
+"""
+
+import socket, errno, time
+
+from twisted.trial import unittest
+from twisted.python.util import untilConcludes
+
+try:
+ from twisted.python import _epoll
+except ImportError:
+ _epoll = None
+
+
+class EPoll(unittest.TestCase):
+ """
+ Tests for the low-level epoll bindings.
+ """
+ def setUp(self):
+ """
+ Create a listening server port and a list with which to keep track
+ of created sockets.
+ """
+ self.serverSocket = socket.socket()
+ self.serverSocket.bind(('127.0.0.1', 0))
+ self.serverSocket.listen(1)
+ self.connections = [self.serverSocket]
+
+
+ def tearDown(self):
+ """
+ Close any sockets which were opened by the test.
+ """
+ for skt in self.connections:
+ skt.close()
+
+
+ def _connectedPair(self):
+ """
+ Return the two sockets which make up a new TCP connection.
+ """
+ client = socket.socket()
+ client.setblocking(False)
+ try:
+ client.connect(('127.0.0.1', self.serverSocket.getsockname()[1]))
+ except socket.error, e:
+ self.assertEqual(e.args[0], errno.EINPROGRESS)
+ else:
+ raise unittest.FailTest("Connect should have raised EINPROGRESS")
+ server, addr = self.serverSocket.accept()
+
+ self.connections.extend((client, server))
+ return client, server
+
+
+ def test_create(self):
+ """
+ Test the creation of an epoll object.
+ """
+ try:
+ p = _epoll.epoll(16)
+ except OSError, e:
+ raise unittest.FailTest(str(e))
+ else:
+ p.close()
+
+
+ def test_badCreate(self):
+ """
+ Test that attempting to create an epoll object with some random
+ objects raises a TypeError.
+ """
+ self.assertRaises(TypeError, _epoll.epoll, 1, 2, 3)
+ self.assertRaises(TypeError, _epoll.epoll, 'foo')
+ self.assertRaises(TypeError, _epoll.epoll, None)
+ self.assertRaises(TypeError, _epoll.epoll, ())
+ self.assertRaises(TypeError, _epoll.epoll, ['foo'])
+ self.assertRaises(TypeError, _epoll.epoll, {})
+
+
+ def test_add(self):
+ """
+ Test adding a socket to an epoll object.
+ """
+ server, client = self._connectedPair()
+
+ p = _epoll.epoll(2)
+ try:
+ p._control(_epoll.CTL_ADD, server.fileno(), _epoll.IN | _epoll.OUT)
+ p._control(_epoll.CTL_ADD, client.fileno(), _epoll.IN | _epoll.OUT)
+ finally:
+ p.close()
+
+
+ def test_controlAndWait(self):
+ """
+ Test waiting on an epoll object which has had some sockets added to
+ it.
+ """
+ client, server = self._connectedPair()
+
+ p = _epoll.epoll(16)
+ p._control(_epoll.CTL_ADD, client.fileno(), _epoll.IN | _epoll.OUT |
+ _epoll.ET)
+ p._control(_epoll.CTL_ADD, server.fileno(), _epoll.IN | _epoll.OUT |
+ _epoll.ET)
+
+ now = time.time()
+ events = untilConcludes(p.wait, 4, 1000)
+ then = time.time()
+ self.failIf(then - now > 0.01)
+
+ events.sort()
+ expected = [(client.fileno(), _epoll.OUT),
+ (server.fileno(), _epoll.OUT)]
+ expected.sort()
+
+ self.assertEqual(events, expected)
+
+ now = time.time()
+ events = untilConcludes(p.wait, 4, 200)
+ then = time.time()
+ self.failUnless(then - now > 0.1)
+ self.failIf(events)
+
+ client.send("Hello!")
+ server.send("world!!!")
+
+ now = time.time()
+ events = untilConcludes(p.wait, 4, 1000)
+ then = time.time()
+ self.failIf(then - now > 0.01)
+
+ events.sort()
+ expected = [(client.fileno(), _epoll.IN | _epoll.OUT),
+ (server.fileno(), _epoll.IN | _epoll.OUT)]
+ expected.sort()
+
+ self.assertEqual(events, expected)
+
+if _epoll is None:
+ EPoll.skip = "_epoll module unavailable"
+else:
+ try:
+ e = _epoll.epoll(16)
+ except IOError, exc:
+ if exc.errno == errno.ENOSYS:
+ del exc
+ EPoll.skip = "epoll support missing from platform"
+ else:
+ raise
+ else:
+ e.close()
+ del e
diff --git a/twisted/test/test_error.py b/twisted/test/test_error.py
new file mode 100644
index 0000000..3dbbe89
--- /dev/null
+++ b/twisted/test/test_error.py
@@ -0,0 +1,170 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+from twisted.internet import error
+import socket
+
+class TestStringification(unittest.TestCase):
+ """Test that the exceptions have useful stringifications.
+ """
+
+ listOfTests = [
+ #(output, exception[, args[, kwargs]]),
+
+ ("An error occurred binding to an interface.",
+ error.BindError),
+
+ ("An error occurred binding to an interface: foo.",
+ error.BindError, ['foo']),
+
+ ("An error occurred binding to an interface: foo bar.",
+ error.BindError, ['foo', 'bar']),
+
+ ("Couldn't listen on eth0:4242: Foo.",
+ error.CannotListenError,
+ ('eth0', 4242, socket.error('Foo'))),
+
+ ("Message is too long to send.",
+ error.MessageLengthError),
+
+ ("Message is too long to send: foo bar.",
+ error.MessageLengthError, ['foo', 'bar']),
+
+ ("DNS lookup failed.",
+ error.DNSLookupError),
+
+ ("DNS lookup failed: foo bar.",
+ error.DNSLookupError, ['foo', 'bar']),
+
+ ("An error occurred while connecting.",
+ error.ConnectError),
+
+ ("An error occurred while connecting: someOsError.",
+ error.ConnectError, ['someOsError']),
+
+ ("An error occurred while connecting: foo.",
+ error.ConnectError, [], {'string': 'foo'}),
+
+ ("An error occurred while connecting: someOsError: foo.",
+ error.ConnectError, ['someOsError', 'foo']),
+
+ ("Couldn't bind.",
+ error.ConnectBindError),
+
+ ("Couldn't bind: someOsError.",
+ error.ConnectBindError, ['someOsError']),
+
+ ("Couldn't bind: someOsError: foo.",
+ error.ConnectBindError, ['someOsError', 'foo']),
+
+ ("Hostname couldn't be looked up.",
+ error.UnknownHostError),
+
+ ("No route to host.",
+ error.NoRouteError),
+
+ ("Connection was refused by other side.",
+ error.ConnectionRefusedError),
+
+ ("TCP connection timed out.",
+ error.TCPTimedOutError),
+
+ ("File used for UNIX socket is no good.",
+ error.BadFileError),
+
+ ("Service name given as port is unknown.",
+ error.ServiceNameUnknownError),
+
+ ("User aborted connection.",
+ error.UserError),
+
+ ("User timeout caused connection failure.",
+ error.TimeoutError),
+
+ ("An SSL error occurred.",
+ error.SSLError),
+
+ ("Connection to the other side was lost in a non-clean fashion.",
+ error.ConnectionLost),
+
+ ("Connection to the other side was lost in a non-clean fashion: foo bar.",
+ error.ConnectionLost, ['foo', 'bar']),
+
+ ("Connection was closed cleanly.",
+ error.ConnectionDone),
+
+ ("Connection was closed cleanly: foo bar.",
+ error.ConnectionDone, ['foo', 'bar']),
+
+ ("Uh.", #TODO nice docstring, you've got there.
+ error.ConnectionFdescWentAway),
+
+ ("Tried to cancel an already-called event.",
+ error.AlreadyCalled),
+
+ ("Tried to cancel an already-called event: foo bar.",
+ error.AlreadyCalled, ['foo', 'bar']),
+
+ ("Tried to cancel an already-cancelled event.",
+ error.AlreadyCancelled),
+
+ ("A process has ended without apparent errors: process finished with exit code 0.",
+ error.ProcessDone,
+ [None]),
+
+ ("A process has ended with a probable error condition: process ended.",
+ error.ProcessTerminated),
+
+ ("A process has ended with a probable error condition: process ended with exit code 42.",
+ error.ProcessTerminated,
+ [],
+ {'exitCode': 42}),
+
+ ("A process has ended with a probable error condition: process ended by signal SIGBUS.",
+ error.ProcessTerminated,
+ [],
+ {'signal': 'SIGBUS'}),
+
+ ("The Connector was not connecting when it was asked to stop connecting.",
+ error.NotConnectingError),
+
+ ("The Port was not listening when it was asked to stop listening.",
+ error.NotListeningError),
+
+ ]
+
+ def testThemAll(self):
+ for entry in self.listOfTests:
+ output = entry[0]
+ exception = entry[1]
+ try:
+ args = entry[2]
+ except IndexError:
+ args = ()
+ try:
+ kwargs = entry[3]
+ except IndexError:
+ kwargs = {}
+
+ self.assertEqual(
+ str(exception(*args, **kwargs)),
+ output)
+
+
+ def test_connectionLostSubclassOfConnectionClosed(self):
+ """
+ L{error.ConnectionClosed} is a superclass of L{error.ConnectionLost}.
+ """
+ self.assertTrue(issubclass(error.ConnectionLost,
+ error.ConnectionClosed))
+
+
+ def test_connectionDoneSubclassOfConnectionClosed(self):
+ """
+ L{error.ConnectionClosed} is a superclass of L{error.ConnectionDone}.
+ """
+ self.assertTrue(issubclass(error.ConnectionDone,
+ error.ConnectionClosed))
+
diff --git a/twisted/test/test_explorer.py b/twisted/test/test_explorer.py
new file mode 100644
index 0000000..2b8fcf0
--- /dev/null
+++ b/twisted/test/test_explorer.py
@@ -0,0 +1,236 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for explorer
+"""
+
+from twisted.trial import unittest
+
+from twisted.manhole import explorer
+
+import types
+
+"""
+# Tests:
+
+ Get an ObjectLink. Browse ObjectLink.identifier. Is it the same?
+
+ Watch Object. Make sure an ObjectLink is received when:
+ Call a method.
+ Set an attribute.
+
+ Have an Object with a setattr class. Watch it.
+ Do both the navite setattr and the watcher get called?
+
+ Sequences with circular references. Does it blow up?
+"""
+
+class SomeDohickey:
+ def __init__(self, *a):
+ self.__dict__['args'] = a
+
+ def bip(self):
+ return self.args
+
+
+class TestBrowser(unittest.TestCase):
+ def setUp(self):
+ self.pool = explorer.explorerPool
+ self.pool.clear()
+ self.testThing = ["How many stairs must a man climb down?",
+ SomeDohickey(42)]
+
+ def test_chain(self):
+ "Following a chain of Explorers."
+ xplorer = self.pool.getExplorer(self.testThing, 'testThing')
+ self.assertEqual(xplorer.id, id(self.testThing))
+ self.assertEqual(xplorer.identifier, 'testThing')
+
+ dxplorer = xplorer.get_elements()[1]
+ self.assertEqual(dxplorer.id, id(self.testThing[1]))
+
+class Watcher:
+ zero = 0
+ def __init__(self):
+ self.links = []
+
+ def receiveBrowserObject(self, olink):
+ self.links.append(olink)
+
+ def setZero(self):
+ self.zero = len(self.links)
+
+ def len(self):
+ return len(self.links) - self.zero
+
+
+class SetattrDohickey:
+ def __setattr__(self, k, v):
+ v = list(str(v))
+ v.reverse()
+ self.__dict__[k] = ''.join(v)
+
+class MiddleMan(SomeDohickey, SetattrDohickey):
+ pass
+
+# class TestWatch(unittest.TestCase):
+class FIXME_Watch:
+ def setUp(self):
+ self.globalNS = globals().copy()
+ self.localNS = {}
+ self.browser = explorer.ObjectBrowser(self.globalNS, self.localNS)
+ self.watcher = Watcher()
+
+ def test_setAttrPlain(self):
+ "Triggering a watcher response by setting an attribute."
+
+ testThing = SomeDohickey('pencil')
+ self.browser.watchObject(testThing, 'testThing',
+ self.watcher.receiveBrowserObject)
+ self.watcher.setZero()
+
+ testThing.someAttr = 'someValue'
+
+ self.assertEqual(testThing.someAttr, 'someValue')
+ self.failUnless(self.watcher.len())
+ olink = self.watcher.links[-1]
+ self.assertEqual(olink.id, id(testThing))
+
+ def test_setAttrChain(self):
+ "Setting an attribute on a watched object that has __setattr__"
+ testThing = MiddleMan('pencil')
+
+ self.browser.watchObject(testThing, 'testThing',
+ self.watcher.receiveBrowserObject)
+ self.watcher.setZero()
+
+ testThing.someAttr = 'ZORT'
+
+ self.assertEqual(testThing.someAttr, 'TROZ')
+ self.failUnless(self.watcher.len())
+ olink = self.watcher.links[-1]
+ self.assertEqual(olink.id, id(testThing))
+
+
+ def test_method(self):
+ "Triggering a watcher response by invoking a method."
+
+ for testThing in (SomeDohickey('pencil'), MiddleMan('pencil')):
+ self.browser.watchObject(testThing, 'testThing',
+ self.watcher.receiveBrowserObject)
+ self.watcher.setZero()
+
+ rval = testThing.bip()
+ self.assertEqual(rval, ('pencil',))
+
+ self.failUnless(self.watcher.len())
+ olink = self.watcher.links[-1]
+ self.assertEqual(olink.id, id(testThing))
+
+
+def function_noArgs():
+ "A function which accepts no arguments at all."
+ return
+
+def function_simple(a, b, c):
+ "A function which accepts several arguments."
+ return a, b, c
+
+def function_variable(*a, **kw):
+ "A function which accepts a variable number of args and keywords."
+ return a, kw
+
+def function_crazy((alpha, beta), c, d=range(4), **kw):
+ "A function with a mad crazy signature."
+ return alpha, beta, c, d, kw
+
+class TestBrowseFunction(unittest.TestCase):
+
+ def setUp(self):
+ self.pool = explorer.explorerPool
+ self.pool.clear()
+
+ def test_sanity(self):
+ """Basic checks for browse_function.
+
+ Was the proper type returned? Does it have the right name and ID?
+ """
+ for f_name in ('function_noArgs', 'function_simple',
+ 'function_variable', 'function_crazy'):
+ f = eval(f_name)
+
+ xplorer = self.pool.getExplorer(f, f_name)
+
+ self.assertEqual(xplorer.id, id(f))
+
+ self.failUnless(isinstance(xplorer, explorer.ExplorerFunction))
+
+ self.assertEqual(xplorer.name, f_name)
+
+ def test_signature_noArgs(self):
+ """Testing zero-argument function signature.
+ """
+
+ xplorer = self.pool.getExplorer(function_noArgs, 'function_noArgs')
+
+ self.assertEqual(len(xplorer.signature), 0)
+
+ def test_signature_simple(self):
+ """Testing simple function signature.
+ """
+
+ xplorer = self.pool.getExplorer(function_simple, 'function_simple')
+
+ expected_signature = ('a','b','c')
+
+ self.assertEqual(xplorer.signature.name, expected_signature)
+
+ def test_signature_variable(self):
+ """Testing variable-argument function signature.
+ """
+
+ xplorer = self.pool.getExplorer(function_variable,
+ 'function_variable')
+
+ expected_names = ('a','kw')
+ signature = xplorer.signature
+
+ self.assertEqual(signature.name, expected_names)
+ self.failUnless(signature.is_varlist(0))
+ self.failUnless(signature.is_keyword(1))
+
+ def test_signature_crazy(self):
+ """Testing function with crazy signature.
+ """
+ xplorer = self.pool.getExplorer(function_crazy, 'function_crazy')
+
+ signature = xplorer.signature
+
+ expected_signature = [{'name': 'c'},
+ {'name': 'd',
+ 'default': range(4)},
+ {'name': 'kw',
+ 'keywords': 1}]
+
+ # The name of the first argument seems to be indecipherable,
+ # but make sure it has one (and no default).
+ self.failUnless(signature.get_name(0))
+ self.failUnless(not signature.get_default(0)[0])
+
+ self.assertEqual(signature.get_name(1), 'c')
+
+ # Get a list of values from a list of ExplorerImmutables.
+ arg_2_default = map(lambda l: l.value,
+ signature.get_default(2)[1].get_elements())
+
+ self.assertEqual(signature.get_name(2), 'd')
+ self.assertEqual(arg_2_default, range(4))
+
+ self.assertEqual(signature.get_name(3), 'kw')
+ self.failUnless(signature.is_keyword(3))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/twisted/test/test_factories.py b/twisted/test/test_factories.py
new file mode 100644
index 0000000..8ffb4da
--- /dev/null
+++ b/twisted/test/test_factories.py
@@ -0,0 +1,197 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test code for basic Factory classes.
+"""
+
+import pickle
+
+from twisted.trial.unittest import TestCase
+
+from twisted.internet import reactor, defer
+from twisted.internet.task import Clock
+from twisted.internet.protocol import Factory, ReconnectingClientFactory
+from twisted.protocols.basic import Int16StringReceiver
+
+
+
+class In(Int16StringReceiver):
+ def __init__(self):
+ self.msgs = {}
+
+ def connectionMade(self):
+ self.factory.connections += 1
+
+ def stringReceived(self, msg):
+ n, msg = pickle.loads(msg)
+ self.msgs[n] = msg
+ self.sendString(pickle.dumps(n))
+
+ def connectionLost(self, reason):
+ self.factory.allMessages.append(self.msgs)
+ if len(self.factory.allMessages) >= self.factory.goal:
+ self.factory.d.callback(None)
+
+
+
+class Out(Int16StringReceiver):
+ msgs = dict([(x, 'X' * x) for x in range(10)])
+
+ def __init__(self):
+ self.msgs = Out.msgs.copy()
+
+ def connectionMade(self):
+ for i in self.msgs.keys():
+ self.sendString(pickle.dumps( (i, self.msgs[i])))
+
+ def stringReceived(self, msg):
+ n = pickle.loads(msg)
+ del self.msgs[n]
+ if not self.msgs:
+ self.transport.loseConnection()
+ self.factory.howManyTimes -= 1
+ if self.factory.howManyTimes <= 0:
+ self.factory.stopTrying()
+
+
+
+class FakeConnector(object):
+ """
+ A fake connector class, to be used to mock connections failed or lost.
+ """
+
+ def stopConnecting(self):
+ pass
+
+
+ def connect(self):
+ pass
+
+
+
+class ReconnectingFactoryTestCase(TestCase):
+ """
+ Tests for L{ReconnectingClientFactory}.
+ """
+
+ def testStopTrying(self):
+ f = Factory()
+ f.protocol = In
+ f.connections = 0
+ f.allMessages = []
+ f.goal = 2
+ f.d = defer.Deferred()
+
+ c = ReconnectingClientFactory()
+ c.initialDelay = c.delay = 0.2
+ c.protocol = Out
+ c.howManyTimes = 2
+
+ port = reactor.listenTCP(0, f)
+ self.addCleanup(port.stopListening)
+ PORT = port.getHost().port
+ reactor.connectTCP('127.0.0.1', PORT, c)
+
+ f.d.addCallback(self._testStopTrying_1, f, c)
+ return f.d
+ testStopTrying.timeout = 10
+
+
+ def _testStopTrying_1(self, res, f, c):
+ self.assertEqual(len(f.allMessages), 2,
+ "not enough messages -- %s" % f.allMessages)
+ self.assertEqual(f.connections, 2,
+ "Number of successful connections incorrect %d" %
+ f.connections)
+ self.assertEqual(f.allMessages, [Out.msgs] * 2)
+ self.failIf(c.continueTrying, "stopTrying never called or ineffective")
+
+
+ def test_stopTryingDoesNotReconnect(self):
+ """
+ Calling stopTrying on a L{ReconnectingClientFactory} doesn't attempt a
+ retry on any active connector.
+ """
+ class FactoryAwareFakeConnector(FakeConnector):
+ attemptedRetry = False
+
+ def stopConnecting(self):
+ """
+ Behave as though an ongoing connection attempt has now
+ failed, and notify the factory of this.
+ """
+ f.clientConnectionFailed(self, None)
+
+ def connect(self):
+ """
+ Record an attempt to reconnect, since this is what we
+ are trying to avoid.
+ """
+ self.attemptedRetry = True
+
+ f = ReconnectingClientFactory()
+ f.clock = Clock()
+
+ # simulate an active connection - stopConnecting on this connector should
+ # be triggered when we call stopTrying
+ f.connector = FactoryAwareFakeConnector()
+ f.stopTrying()
+
+ # make sure we never attempted to retry
+ self.assertFalse(f.connector.attemptedRetry)
+ self.assertFalse(f.clock.getDelayedCalls())
+
+
+ def test_serializeUnused(self):
+ """
+ A L{ReconnectingClientFactory} which hasn't been used for anything
+ can be pickled and unpickled and end up with the same state.
+ """
+ original = ReconnectingClientFactory()
+ reconstituted = pickle.loads(pickle.dumps(original))
+ self.assertEqual(original.__dict__, reconstituted.__dict__)
+
+
+ def test_serializeWithClock(self):
+ """
+ The clock attribute of L{ReconnectingClientFactory} is not serialized,
+ and the restored value sets it to the default value, the reactor.
+ """
+ clock = Clock()
+ original = ReconnectingClientFactory()
+ original.clock = clock
+ reconstituted = pickle.loads(pickle.dumps(original))
+ self.assertIdentical(reconstituted.clock, None)
+
+
+ def test_deserializationResetsParameters(self):
+ """
+ A L{ReconnectingClientFactory} which is unpickled does not have an
+ L{IConnector} and has its reconnecting timing parameters reset to their
+ initial values.
+ """
+ factory = ReconnectingClientFactory()
+ factory.clientConnectionFailed(FakeConnector(), None)
+ self.addCleanup(factory.stopTrying)
+
+ serialized = pickle.dumps(factory)
+ unserialized = pickle.loads(serialized)
+ self.assertEqual(unserialized.connector, None)
+ self.assertEqual(unserialized._callID, None)
+ self.assertEqual(unserialized.retries, 0)
+ self.assertEqual(unserialized.delay, factory.initialDelay)
+ self.assertEqual(unserialized.continueTrying, True)
+
+
+ def test_parametrizedClock(self):
+ """
+ The clock used by L{ReconnectingClientFactory} can be parametrized, so
+ that one can cleanly test reconnections.
+ """
+ clock = Clock()
+ factory = ReconnectingClientFactory()
+ factory.clock = clock
+
+ factory.clientConnectionLost(FakeConnector(), None)
+ self.assertEqual(len(clock.calls), 1)
diff --git a/twisted/test/test_failure.py b/twisted/test/test_failure.py
new file mode 100644
index 0000000..8a3670e
--- /dev/null
+++ b/twisted/test/test_failure.py
@@ -0,0 +1,594 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for failure module.
+"""
+
+import re
+import sys
+import StringIO
+import traceback
+import pdb
+
+from twisted.trial import unittest, util
+
+from twisted.python import failure
+
+try:
+ from twisted.test import raiser
+except ImportError:
+ raiser = None
+
+
+def getDivisionFailure(*args, **kwargs):
+ """
+ Make a C{Failure} of a divide-by-zero error.
+
+ @param args: Any C{*args} are passed to Failure's constructor.
+ @param kwargs: Any C{**kwargs} are passed to Failure's constructor.
+ """
+ try:
+ 1/0
+ except:
+ f = failure.Failure(*args, **kwargs)
+ return f
+
+
+class FailureTestCase(unittest.TestCase):
+
+ def testFailAndTrap(self):
+ """Trapping a failure."""
+ try:
+ raise NotImplementedError('test')
+ except:
+ f = failure.Failure()
+ error = f.trap(SystemExit, RuntimeError)
+ self.assertEqual(error, RuntimeError)
+ self.assertEqual(f.type, NotImplementedError)
+
+
+ def test_notTrapped(self):
+ """Making sure trap doesn't trap what it shouldn't."""
+ try:
+ raise ValueError()
+ except:
+ f = failure.Failure()
+ self.assertRaises(failure.Failure, f.trap, OverflowError)
+
+
+ def assertStartsWith(self, s, prefix):
+ """
+ Assert that s starts with a particular prefix.
+ """
+ self.assertTrue(s.startswith(prefix),
+ '%r is not the start of %r' % (prefix, s))
+
+
+ def test_printingSmokeTest(self):
+ """
+ None of the print* methods fail when called.
+ """
+ f = getDivisionFailure()
+ out = StringIO.StringIO()
+ f.printDetailedTraceback(out)
+ self.assertStartsWith(out.getvalue(), '*--- Failure')
+ out = StringIO.StringIO()
+ f.printBriefTraceback(out)
+ self.assertStartsWith(out.getvalue(), 'Traceback')
+ out = StringIO.StringIO()
+ f.printTraceback(out)
+ self.assertStartsWith(out.getvalue(), 'Traceback')
+
+
+ def test_printingCapturedVarsSmokeTest(self):
+ """
+ None of the print* methods fail when called on a L{Failure} constructed
+ with C{captureVars=True}.
+
+ Local variables on the stack can be seen in the detailed traceback.
+ """
+ exampleLocalVar = 'xyzzy'
+ f = getDivisionFailure(captureVars=True)
+ out = StringIO.StringIO()
+ f.printDetailedTraceback(out)
+ self.assertStartsWith(out.getvalue(), '*--- Failure')
+ self.assertNotEqual(None, re.search('exampleLocalVar.*xyzzy',
+ out.getvalue()))
+ out = StringIO.StringIO()
+ f.printBriefTraceback(out)
+ self.assertStartsWith(out.getvalue(), 'Traceback')
+ out = StringIO.StringIO()
+ f.printTraceback(out)
+ self.assertStartsWith(out.getvalue(), 'Traceback')
+
+
+ def test_printingCapturedVarsCleanedSmokeTest(self):
+ """
+ C{printDetailedTraceback} includes information about local variables on
+ the stack after C{cleanFailure} has been called.
+ """
+ exampleLocalVar = 'xyzzy'
+ f = getDivisionFailure(captureVars=True)
+ f.cleanFailure()
+ out = StringIO.StringIO()
+ f.printDetailedTraceback(out)
+ self.assertNotEqual(None, re.search('exampleLocalVar.*xyzzy',
+ out.getvalue()))
+
+
+ def test_printingNoVars(self):
+ """
+ Calling C{Failure()} with no arguments does not capture any locals or
+ globals, so L{printDetailedTraceback} cannot show them in its output.
+ """
+ out = StringIO.StringIO()
+ f = getDivisionFailure()
+ f.printDetailedTraceback(out)
+ # There should be no variables in the detailed output. Variables are
+ # printed on lines with 2 leading spaces.
+ linesWithVars = [line for line in out.getvalue().splitlines()
+ if line.startswith(' ')]
+ self.assertEqual([], linesWithVars)
+ self.assertSubstring(
+ 'Capture of Locals and Globals disabled', out.getvalue())
+
+
+ def test_printingCaptureVars(self):
+ """
+ Calling C{Failure(captureVars=True)} captures the locals and globals
+ for its stack frames, so L{printDetailedTraceback} will show them in
+ its output.
+ """
+ out = StringIO.StringIO()
+ f = getDivisionFailure(captureVars=True)
+ f.printDetailedTraceback(out)
+ # Variables are printed on lines with 2 leading spaces.
+ linesWithVars = [line for line in out.getvalue().splitlines()
+ if line.startswith(' ')]
+ self.assertNotEqual([], linesWithVars)
+
+
+ def testExplictPass(self):
+ e = RuntimeError()
+ f = failure.Failure(e)
+ f.trap(RuntimeError)
+ self.assertEqual(f.value, e)
+
+
+ def _getInnermostFrameLine(self, f):
+ try:
+ f.raiseException()
+ except ZeroDivisionError:
+ tb = traceback.extract_tb(sys.exc_info()[2])
+ return tb[-1][-1]
+ else:
+ raise Exception(
+ "f.raiseException() didn't raise ZeroDivisionError!?")
+
+
+ def testRaiseExceptionWithTB(self):
+ f = getDivisionFailure()
+ innerline = self._getInnermostFrameLine(f)
+ self.assertEqual(innerline, '1/0')
+
+
+ def testLackOfTB(self):
+ f = getDivisionFailure()
+ f.cleanFailure()
+ innerline = self._getInnermostFrameLine(f)
+ self.assertEqual(innerline, '1/0')
+
+ testLackOfTB.todo = "the traceback is not preserved, exarkun said he'll try to fix this! god knows how"
+
+
+ _stringException = "bugger off"
+ def _getStringFailure(self):
+ try:
+ raise self._stringException
+ except:
+ f = failure.Failure()
+ return f
+
+
+ def test_raiseStringExceptions(self):
+ # String exceptions used to totally bugged f.raiseException
+ f = self._getStringFailure()
+ try:
+ f.raiseException()
+ except:
+ self.assertEqual(sys.exc_info()[0], self._stringException)
+ else:
+ raise AssertionError("Should have raised")
+ test_raiseStringExceptions.suppress = [
+ util.suppress(message='raising a string exception is deprecated')]
+
+
+ def test_printStringExceptions(self):
+ """
+ L{Failure.printTraceback} should write out stack and exception
+ information, even for string exceptions.
+ """
+ failure = self._getStringFailure()
+ output = StringIO.StringIO()
+ failure.printTraceback(file=output)
+ lines = output.getvalue().splitlines()
+ # The last line should be the value of the raised string
+ self.assertEqual(lines[-1], self._stringException)
+
+ test_printStringExceptions.suppress = [
+ util.suppress(message='raising a string exception is deprecated')]
+
+ if sys.version_info[:2] >= (2, 6):
+ skipMsg = ("String exceptions aren't supported anymore starting "
+ "Python 2.6")
+ test_raiseStringExceptions.skip = skipMsg
+ test_printStringExceptions.skip = skipMsg
+
+
+ def testConstructionFails(self):
+ """
+ Creating a Failure with no arguments causes it to try to discover the
+ current interpreter exception state. If no such state exists, creating
+ the Failure should raise a synchronous exception.
+ """
+ self.assertRaises(failure.NoCurrentExceptionError, failure.Failure)
+
+
+ def test_getTracebackObject(self):
+ """
+ If the C{Failure} has not been cleaned, then C{getTracebackObject}
+ returns the traceback object that captured in its constructor.
+ """
+ f = getDivisionFailure()
+ self.assertEqual(f.getTracebackObject(), f.tb)
+
+
+ def test_getTracebackObjectFromCaptureVars(self):
+ """
+ C{captureVars=True} has no effect on the result of
+ C{getTracebackObject}.
+ """
+ try:
+ 1/0
+ except ZeroDivisionError:
+ noVarsFailure = failure.Failure()
+ varsFailure = failure.Failure(captureVars=True)
+ self.assertEqual(noVarsFailure.getTracebackObject(), varsFailure.tb)
+
+
+ def test_getTracebackObjectFromClean(self):
+ """
+ If the Failure has been cleaned, then C{getTracebackObject} returns an
+ object that looks the same to L{traceback.extract_tb}.
+ """
+ f = getDivisionFailure()
+ expected = traceback.extract_tb(f.getTracebackObject())
+ f.cleanFailure()
+ observed = traceback.extract_tb(f.getTracebackObject())
+ self.assertNotEqual(None, expected)
+ self.assertEqual(expected, observed)
+
+
+ def test_getTracebackObjectFromCaptureVarsAndClean(self):
+ """
+ If the Failure was created with captureVars, then C{getTracebackObject}
+ returns an object that looks the same to L{traceback.extract_tb}.
+ """
+ f = getDivisionFailure(captureVars=True)
+ expected = traceback.extract_tb(f.getTracebackObject())
+ f.cleanFailure()
+ observed = traceback.extract_tb(f.getTracebackObject())
+ self.assertEqual(expected, observed)
+
+
+ def test_getTracebackObjectWithoutTraceback(self):
+ """
+ L{failure.Failure}s need not be constructed with traceback objects. If
+ a C{Failure} has no traceback information at all, C{getTracebackObject}
+ just returns None.
+
+ None is a good value, because traceback.extract_tb(None) -> [].
+ """
+ f = failure.Failure(Exception("some error"))
+ self.assertEqual(f.getTracebackObject(), None)
+
+
+
+class BrokenStr(Exception):
+ """
+ An exception class the instances of which cannot be presented as strings via
+ C{str}.
+ """
+ def __str__(self):
+ # Could raise something else, but there's no point as yet.
+ raise self
+
+
+
+class BrokenExceptionMetaclass(type):
+ """
+ A metaclass for an exception type which cannot be presented as a string via
+ C{str}.
+ """
+ def __str__(self):
+ raise ValueError("You cannot make a string out of me.")
+
+
+
+class BrokenExceptionType(Exception, object):
+ """
+ The aforementioned exception type which cnanot be presented as a string via
+ C{str}.
+ """
+ __metaclass__ = BrokenExceptionMetaclass
+
+
+
+class GetTracebackTests(unittest.TestCase):
+ """
+ Tests for L{Failure.getTraceback}.
+ """
+ def _brokenValueTest(self, detail):
+ """
+ Construct a L{Failure} with an exception that raises an exception from
+ its C{__str__} method and then call C{getTraceback} with the specified
+ detail and verify that it returns a string.
+ """
+ x = BrokenStr()
+ f = failure.Failure(x)
+ traceback = f.getTraceback(detail=detail)
+ self.assertIsInstance(traceback, str)
+
+
+ def test_brokenValueBriefDetail(self):
+ """
+ A L{Failure} might wrap an exception with a C{__str__} method which
+ raises an exception. In this case, calling C{getTraceback} on the
+ failure with the C{"brief"} detail does not raise an exception.
+ """
+ self._brokenValueTest("brief")
+
+
+ def test_brokenValueDefaultDetail(self):
+ """
+ Like test_brokenValueBriefDetail, but for the C{"default"} detail case.
+ """
+ self._brokenValueTest("default")
+
+
+ def test_brokenValueVerboseDetail(self):
+ """
+ Like test_brokenValueBriefDetail, but for the C{"default"} detail case.
+ """
+ self._brokenValueTest("verbose")
+
+
+ def _brokenTypeTest(self, detail):
+ """
+ Construct a L{Failure} with an exception type that raises an exception
+ from its C{__str__} method and then call C{getTraceback} with the
+ specified detail and verify that it returns a string.
+ """
+ f = failure.Failure(BrokenExceptionType())
+ traceback = f.getTraceback(detail=detail)
+ self.assertIsInstance(traceback, str)
+
+
+ def test_brokenTypeBriefDetail(self):
+ """
+ A L{Failure} might wrap an exception the type object of which has a
+ C{__str__} method which raises an exception. In this case, calling
+ C{getTraceback} on the failure with the C{"brief"} detail does not raise
+ an exception.
+ """
+ self._brokenTypeTest("brief")
+
+
+ def test_brokenTypeDefaultDetail(self):
+ """
+ Like test_brokenTypeBriefDetail, but for the C{"default"} detail case.
+ """
+ self._brokenTypeTest("default")
+
+
+ def test_brokenTypeVerboseDetail(self):
+ """
+ Like test_brokenTypeBriefDetail, but for the C{"verbose"} detail case.
+ """
+ self._brokenTypeTest("verbose")
+
+
+
+class FindFailureTests(unittest.TestCase):
+ """
+ Tests for functionality related to L{Failure._findFailure}.
+ """
+
+ def test_findNoFailureInExceptionHandler(self):
+ """
+ Within an exception handler, _findFailure should return
+ C{None} in case no Failure is associated with the current
+ exception.
+ """
+ try:
+ 1/0
+ except:
+ self.assertEqual(failure.Failure._findFailure(), None)
+ else:
+ self.fail("No exception raised from 1/0!?")
+
+
+ def test_findNoFailure(self):
+ """
+ Outside of an exception handler, _findFailure should return None.
+ """
+ self.assertEqual(sys.exc_info()[-1], None) #environment sanity check
+ self.assertEqual(failure.Failure._findFailure(), None)
+
+
+ def test_findFailure(self):
+ """
+ Within an exception handler, it should be possible to find the
+ original Failure that caused the current exception (if it was
+ caused by raiseException).
+ """
+ f = getDivisionFailure()
+ f.cleanFailure()
+ try:
+ f.raiseException()
+ except:
+ self.assertEqual(failure.Failure._findFailure(), f)
+ else:
+ self.fail("No exception raised from raiseException!?")
+
+
+ def test_failureConstructionFindsOriginalFailure(self):
+ """
+ When a Failure is constructed in the context of an exception
+ handler that is handling an exception raised by
+ raiseException, the new Failure should be chained to that
+ original Failure.
+ """
+ f = getDivisionFailure()
+ f.cleanFailure()
+ try:
+ f.raiseException()
+ except:
+ newF = failure.Failure()
+ self.assertEqual(f.getTraceback(), newF.getTraceback())
+ else:
+ self.fail("No exception raised from raiseException!?")
+
+
+ def test_failureConstructionWithMungedStackSucceeds(self):
+ """
+ Pyrex and Cython are known to insert fake stack frames so as to give
+ more Python-like tracebacks. These stack frames with empty code objects
+ should not break extraction of the exception.
+ """
+ try:
+ raiser.raiseException()
+ except raiser.RaiserException:
+ f = failure.Failure()
+ self.assertTrue(f.check(raiser.RaiserException))
+ else:
+ self.fail("No exception raised from extension?!")
+
+
+ if raiser is None:
+ skipMsg = "raiser extension not available"
+ test_failureConstructionWithMungedStackSucceeds.skip = skipMsg
+
+
+
+class TestFormattableTraceback(unittest.TestCase):
+ """
+ Whitebox tests that show that L{failure._Traceback} constructs objects that
+ can be used by L{traceback.extract_tb}.
+
+ If the objects can be used by L{traceback.extract_tb}, then they can be
+ formatted using L{traceback.format_tb} and friends.
+ """
+
+ def test_singleFrame(self):
+ """
+ A C{_Traceback} object constructed with a single frame should be able
+ to be passed to L{traceback.extract_tb}, and we should get a singleton
+ list containing a (filename, lineno, methodname, line) tuple.
+ """
+ tb = failure._Traceback([['method', 'filename.py', 123, {}, {}]])
+ # Note that we don't need to test that extract_tb correctly extracts
+ # the line's contents. In this case, since filename.py doesn't exist,
+ # it will just use None.
+ self.assertEqual(traceback.extract_tb(tb),
+ [('filename.py', 123, 'method', None)])
+
+
+ def test_manyFrames(self):
+ """
+ A C{_Traceback} object constructed with multiple frames should be able
+ to be passed to L{traceback.extract_tb}, and we should get a list
+ containing a tuple for each frame.
+ """
+ tb = failure._Traceback([
+ ['method1', 'filename.py', 123, {}, {}],
+ ['method2', 'filename.py', 235, {}, {}]])
+ self.assertEqual(traceback.extract_tb(tb),
+ [('filename.py', 123, 'method1', None),
+ ('filename.py', 235, 'method2', None)])
+
+
+
+class TestFrameAttributes(unittest.TestCase):
+ """
+ _Frame objects should possess some basic attributes that qualify them as
+ fake python Frame objects.
+ """
+
+ def test_fakeFrameAttributes(self):
+ """
+ L{_Frame} instances have the C{f_globals} and C{f_locals} attributes
+ bound to C{dict} instance. They also have the C{f_code} attribute
+ bound to something like a code object.
+ """
+ frame = failure._Frame("dummyname", "dummyfilename")
+ self.assertIsInstance(frame.f_globals, dict)
+ self.assertIsInstance(frame.f_locals, dict)
+ self.assertIsInstance(frame.f_code, failure._Code)
+
+
+
+class TestDebugMode(unittest.TestCase):
+ """
+ Failure's debug mode should allow jumping into the debugger.
+ """
+
+ def setUp(self):
+ """
+ Override pdb.post_mortem so we can make sure it's called.
+ """
+ # Make sure any changes we make are reversed:
+ post_mortem = pdb.post_mortem
+ origInit = failure.Failure.__dict__['__init__']
+ def restore():
+ pdb.post_mortem = post_mortem
+ failure.Failure.__dict__['__init__'] = origInit
+ self.addCleanup(restore)
+
+ self.result = []
+ pdb.post_mortem = self.result.append
+ failure.startDebugMode()
+
+
+ def test_regularFailure(self):
+ """
+ If startDebugMode() is called, calling Failure() will first call
+ pdb.post_mortem with the traceback.
+ """
+ try:
+ 1/0
+ except:
+ typ, exc, tb = sys.exc_info()
+ f = failure.Failure()
+ self.assertEqual(self.result, [tb])
+ self.assertEqual(f.captureVars, False)
+
+
+ def test_captureVars(self):
+ """
+ If startDebugMode() is called, passing captureVars to Failure() will
+ not blow up.
+ """
+ try:
+ 1/0
+ except:
+ typ, exc, tb = sys.exc_info()
+ f = failure.Failure(captureVars=True)
+ self.assertEqual(self.result, [tb])
+ self.assertEqual(f.captureVars, True)
+
+
+
+if sys.version_info[:2] >= (2, 5):
+ from twisted.test.generator_failure_tests import TwoPointFiveFailureTests
diff --git a/twisted/test/test_fdesc.py b/twisted/test/test_fdesc.py
new file mode 100644
index 0000000..285b9cc
--- /dev/null
+++ b/twisted/test/test_fdesc.py
@@ -0,0 +1,235 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.fdesc}.
+"""
+
+import os, sys
+import errno
+
+try:
+ import fcntl
+except ImportError:
+ skip = "not supported on this platform"
+else:
+ from twisted.internet import fdesc
+
+from twisted.python.util import untilConcludes
+from twisted.trial import unittest
+
+
+class ReadWriteTestCase(unittest.TestCase):
+ """
+ Tests for fdesc.readFromFD, fdesc.writeToFD.
+ """
+
+ def setUp(self):
+ """
+ Create two non-blocking pipes that can be used in tests.
+ """
+ self.r, self.w = os.pipe()
+ fdesc.setNonBlocking(self.r)
+ fdesc.setNonBlocking(self.w)
+
+
+ def tearDown(self):
+ """
+ Close pipes.
+ """
+ try:
+ os.close(self.w)
+ except OSError:
+ pass
+ try:
+ os.close(self.r)
+ except OSError:
+ pass
+
+
+ def write(self, d):
+ """
+ Write data to the pipe.
+ """
+ return fdesc.writeToFD(self.w, d)
+
+
+ def read(self):
+ """
+ Read data from the pipe.
+ """
+ l = []
+ res = fdesc.readFromFD(self.r, l.append)
+ if res is None:
+ if l:
+ return l[0]
+ else:
+ return ""
+ else:
+ return res
+
+
+ def test_writeAndRead(self):
+ """
+ Test that the number of bytes L{fdesc.writeToFD} reports as written
+ with its return value are seen by L{fdesc.readFromFD}.
+ """
+ n = self.write("hello")
+ self.failUnless(n > 0)
+ s = self.read()
+ self.assertEqual(len(s), n)
+ self.assertEqual("hello"[:n], s)
+
+
+ def test_writeAndReadLarge(self):
+ """
+ Similar to L{test_writeAndRead}, but use a much larger string to verify
+ the behavior for that case.
+ """
+ orig = "0123456879" * 10000
+ written = self.write(orig)
+ self.failUnless(written > 0)
+ result = []
+ resultlength = 0
+ i = 0
+ while resultlength < written or i < 50:
+ result.append(self.read())
+ resultlength += len(result[-1])
+ # Increment a counter to be sure we'll exit at some point
+ i += 1
+ result = "".join(result)
+ self.assertEqual(len(result), written)
+ self.assertEqual(orig[:written], result)
+
+
+ def test_readFromEmpty(self):
+ """
+ Verify that reading from a file descriptor with no data does not raise
+ an exception and does not result in the callback function being called.
+ """
+ l = []
+ result = fdesc.readFromFD(self.r, l.append)
+ self.assertEqual(l, [])
+ self.assertEqual(result, None)
+
+
+ def test_readFromCleanClose(self):
+ """
+ Test that using L{fdesc.readFromFD} on a cleanly closed file descriptor
+ returns a connection done indicator.
+ """
+ os.close(self.w)
+ self.assertEqual(self.read(), fdesc.CONNECTION_DONE)
+
+
+ def test_writeToClosed(self):
+ """
+ Verify that writing with L{fdesc.writeToFD} when the read end is closed
+ results in a connection lost indicator.
+ """
+ os.close(self.r)
+ self.assertEqual(self.write("s"), fdesc.CONNECTION_LOST)
+
+
+ def test_readFromInvalid(self):
+ """
+ Verify that reading with L{fdesc.readFromFD} when the read end is
+ closed results in a connection lost indicator.
+ """
+ os.close(self.r)
+ self.assertEqual(self.read(), fdesc.CONNECTION_LOST)
+
+
+ def test_writeToInvalid(self):
+ """
+ Verify that writing with L{fdesc.writeToFD} when the write end is
+ closed results in a connection lost indicator.
+ """
+ os.close(self.w)
+ self.assertEqual(self.write("s"), fdesc.CONNECTION_LOST)
+
+
+ def test_writeErrors(self):
+ """
+ Test error path for L{fdesc.writeTod}.
+ """
+ oldOsWrite = os.write
+ def eagainWrite(fd, data):
+ err = OSError()
+ err.errno = errno.EAGAIN
+ raise err
+ os.write = eagainWrite
+ try:
+ self.assertEqual(self.write("s"), 0)
+ finally:
+ os.write = oldOsWrite
+
+ def eintrWrite(fd, data):
+ err = OSError()
+ err.errno = errno.EINTR
+ raise err
+ os.write = eintrWrite
+ try:
+ self.assertEqual(self.write("s"), 0)
+ finally:
+ os.write = oldOsWrite
+
+
+
+class CloseOnExecTests(unittest.TestCase):
+ """
+ Tests for L{fdesc._setCloseOnExec} and L{fdesc._unsetCloseOnExec}.
+ """
+ program = '''
+import os, errno
+try:
+ os.write(%d, 'lul')
+except OSError, e:
+ if e.errno == errno.EBADF:
+ os._exit(0)
+ os._exit(5)
+except:
+ os._exit(10)
+else:
+ os._exit(20)
+'''
+
+ def _execWithFileDescriptor(self, fObj):
+ pid = os.fork()
+ if pid == 0:
+ try:
+ os.execv(sys.executable, [sys.executable, '-c', self.program % (fObj.fileno(),)])
+ except:
+ import traceback
+ traceback.print_exc()
+ os._exit(30)
+ else:
+ # On Linux wait(2) doesn't seem ever able to fail with EINTR but
+ # POSIX seems to allow it and on OS X it happens quite a lot.
+ return untilConcludes(os.waitpid, pid, 0)[1]
+
+
+ def test_setCloseOnExec(self):
+ """
+ A file descriptor passed to L{fdesc._setCloseOnExec} is not inherited
+ by a new process image created with one of the exec family of
+ functions.
+ """
+ fObj = file(self.mktemp(), 'w')
+ fdesc._setCloseOnExec(fObj.fileno())
+ status = self._execWithFileDescriptor(fObj)
+ self.assertTrue(os.WIFEXITED(status))
+ self.assertEqual(os.WEXITSTATUS(status), 0)
+
+
+ def test_unsetCloseOnExec(self):
+ """
+ A file descriptor passed to L{fdesc._unsetCloseOnExec} is inherited by
+ a new process image created with one of the exec family of functions.
+ """
+ fObj = file(self.mktemp(), 'w')
+ fdesc._setCloseOnExec(fObj.fileno())
+ fdesc._unsetCloseOnExec(fObj.fileno())
+ status = self._execWithFileDescriptor(fObj)
+ self.assertTrue(os.WIFEXITED(status))
+ self.assertEqual(os.WEXITSTATUS(status), 20)
diff --git a/twisted/test/test_finger.py b/twisted/test/test_finger.py
new file mode 100644
index 0000000..c0c2e09
--- /dev/null
+++ b/twisted/test/test_finger.py
@@ -0,0 +1,67 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.protocols.finger}.
+"""
+
+from twisted.trial import unittest
+from twisted.protocols import finger
+from twisted.test.proto_helpers import StringTransport
+
+
+class FingerTestCase(unittest.TestCase):
+ """
+ Tests for L{finger.Finger}.
+ """
+ def setUp(self):
+ """
+ Create and connect a L{finger.Finger} instance.
+ """
+ self.transport = StringTransport()
+ self.protocol = finger.Finger()
+ self.protocol.makeConnection(self.transport)
+
+
+ def test_simple(self):
+ """
+ When L{finger.Finger} receives a CR LF terminated line, it responds
+ with the default user status message - that no such user exists.
+ """
+ self.protocol.dataReceived("moshez\r\n")
+ self.assertEqual(
+ self.transport.value(),
+ "Login: moshez\nNo such user\n")
+
+
+ def test_simpleW(self):
+ """
+ The behavior for a query which begins with C{"/w"} is the same as the
+ behavior for one which does not. The user is reported as not existing.
+ """
+ self.protocol.dataReceived("/w moshez\r\n")
+ self.assertEqual(
+ self.transport.value(),
+ "Login: moshez\nNo such user\n")
+
+
+ def test_forwarding(self):
+ """
+ When L{finger.Finger} receives a request for a remote user, it responds
+ with a message rejecting the request.
+ """
+ self.protocol.dataReceived("moshez@example.com\r\n")
+ self.assertEqual(
+ self.transport.value(),
+ "Finger forwarding service denied\n")
+
+
+ def test_list(self):
+ """
+ When L{finger.Finger} receives a blank line, it responds with a message
+ rejecting the request for all online users.
+ """
+ self.protocol.dataReceived("\r\n")
+ self.assertEqual(
+ self.transport.value(),
+ "Finger online list denied\n")
diff --git a/twisted/test/test_formmethod.py b/twisted/test/test_formmethod.py
new file mode 100644
index 0000000..a23328b
--- /dev/null
+++ b/twisted/test/test_formmethod.py
@@ -0,0 +1,77 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for formmethod module.
+"""
+
+from twisted.trial import unittest
+
+from twisted.python import formmethod
+
+
+class ArgumentTestCase(unittest.TestCase):
+
+ def argTest(self, argKlass, testPairs, badValues, *args, **kwargs):
+ arg = argKlass("name", *args, **kwargs)
+ for val, result in testPairs:
+ self.assertEqual(arg.coerce(val), result)
+ for val in badValues:
+ self.assertRaises(formmethod.InputError, arg.coerce, val)
+
+ def testString(self):
+ self.argTest(formmethod.String, [("a", "a"), (1, "1"), ("", "")], ())
+ self.argTest(formmethod.String, [("ab", "ab"), ("abc", "abc")], ("2", ""), min=2)
+ self.argTest(formmethod.String, [("ab", "ab"), ("a", "a")], ("223213", "345x"), max=3)
+ self.argTest(formmethod.String, [("ab", "ab"), ("add", "add")], ("223213", "x"), min=2, max=3)
+
+ def testInt(self):
+ self.argTest(formmethod.Integer, [("3", 3), ("-2", -2), ("", None)], ("q", "2.3"))
+ self.argTest(formmethod.Integer, [("3", 3), ("-2", -2)], ("q", "2.3", ""), allowNone=0)
+
+ def testFloat(self):
+ self.argTest(formmethod.Float, [("3", 3.0), ("-2.3", -2.3), ("", None)], ("q", "2.3z"))
+ self.argTest(formmethod.Float, [("3", 3.0), ("-2.3", -2.3)], ("q", "2.3z", ""),
+ allowNone=0)
+
+ def testChoice(self):
+ choices = [("a", "apple", "an apple"),
+ ("b", "banana", "ook")]
+ self.argTest(formmethod.Choice, [("a", "apple"), ("b", "banana")],
+ ("c", 1), choices=choices)
+
+ def testFlags(self):
+ flags = [("a", "apple", "an apple"),
+ ("b", "banana", "ook")]
+ self.argTest(formmethod.Flags,
+ [(["a"], ["apple"]), (["b", "a"], ["banana", "apple"])],
+ (["a", "c"], ["fdfs"]),
+ flags=flags)
+
+ def testBoolean(self):
+ tests = [("yes", 1), ("", 0), ("False", 0), ("no", 0)]
+ self.argTest(formmethod.Boolean, tests, ())
+
+ def testDate(self):
+ goodTests = {
+ ("2002", "12", "21"): (2002, 12, 21),
+ ("1996", "2", "29"): (1996, 2, 29),
+ ("", "", ""): None,
+ }.items()
+ badTests = [("2002", "2", "29"), ("xx", "2", "3"),
+ ("2002", "13", "1"), ("1999", "12","32"),
+ ("2002", "1"), ("2002", "2", "3", "4")]
+ self.argTest(formmethod.Date, goodTests, badTests)
+
+ def testRangedInteger(self):
+ goodTests = {"0": 0, "12": 12, "3": 3}.items()
+ badTests = ["-1", "x", "13", "-2000", "3.4"]
+ self.argTest(formmethod.IntegerRange, goodTests, badTests, 0, 12)
+
+ def testVerifiedPassword(self):
+ goodTests = {("foo", "foo"): "foo", ("ab", "ab"): "ab"}.items()
+ badTests = [("ab", "a"), ("12345", "12345"), ("", ""), ("a", "a"), ("a",), ("a", "a", "a")]
+ self.argTest(formmethod.VerifiedPassword, goodTests, badTests, min=2, max=4)
+
+
diff --git a/twisted/test/test_ftp.py b/twisted/test/test_ftp.py
new file mode 100644
index 0000000..8058590
--- /dev/null
+++ b/twisted/test/test_ftp.py
@@ -0,0 +1,3017 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+FTP tests.
+"""
+
+import os
+import errno
+from StringIO import StringIO
+import getpass
+
+from zope.interface import implements
+from zope.interface.verify import verifyClass
+
+from twisted.trial import unittest, util
+from twisted.python.randbytes import insecureRandom
+from twisted.cred.portal import IRealm
+from twisted.protocols import basic
+from twisted.internet import reactor, task, protocol, defer, error
+from twisted.internet.interfaces import IConsumer
+from twisted.cred.error import UnauthorizedLogin
+from twisted.cred import portal, checkers, credentials
+from twisted.python import failure, filepath, runtime
+from twisted.test import proto_helpers
+
+from twisted.protocols import ftp, loopback
+
+
+_changeDirectorySuppression = util.suppress(
+ category=DeprecationWarning,
+ message=(
+ r"FTPClient\.changeDirectory is deprecated in Twisted 8\.2 and "
+ r"newer\. Use FTPClient\.cwd instead\."))
+
+if runtime.platform.isWindows():
+ nonPOSIXSkip = "Cannot run on Windows"
+else:
+ nonPOSIXSkip = None
+
+
+class Dummy(basic.LineReceiver):
+ logname = None
+ def __init__(self):
+ self.lines = []
+ self.rawData = []
+ def connectionMade(self):
+ self.f = self.factory # to save typing in pdb :-)
+ def lineReceived(self,line):
+ self.lines.append(line)
+ def rawDataReceived(self, data):
+ self.rawData.append(data)
+ def lineLengthExceeded(self, line):
+ pass
+
+
+class _BufferingProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.buffer = ''
+ self.d = defer.Deferred()
+ def dataReceived(self, data):
+ self.buffer += data
+ def connectionLost(self, reason):
+ self.d.callback(self)
+
+
+
+class FTPServerTestCase(unittest.TestCase):
+ """
+ Simple tests for an FTP server with the default settings.
+
+ @ivar clientFactory: class used as ftp client.
+ """
+ clientFactory = ftp.FTPClientBasic
+ userAnonymous = "anonymous"
+
+ def setUp(self):
+ # Create a directory
+ self.directory = self.mktemp()
+ os.mkdir(self.directory)
+ self.dirPath = filepath.FilePath(self.directory)
+
+ # Start the server
+ p = portal.Portal(ftp.FTPRealm(self.directory))
+ p.registerChecker(checkers.AllowAnonymousAccess(),
+ credentials.IAnonymous)
+ self.factory = ftp.FTPFactory(portal=p,
+ userAnonymous=self.userAnonymous)
+ port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+
+ # Hook the server's buildProtocol to make the protocol instance
+ # accessible to tests.
+ buildProtocol = self.factory.buildProtocol
+ d1 = defer.Deferred()
+ def _rememberProtocolInstance(addr):
+ # Done hooking this.
+ del self.factory.buildProtocol
+
+ protocol = buildProtocol(addr)
+ self.serverProtocol = protocol.wrappedProtocol
+ def cleanupServer():
+ if self.serverProtocol.transport is not None:
+ self.serverProtocol.transport.loseConnection()
+ self.addCleanup(cleanupServer)
+ d1.callback(None)
+ return protocol
+ self.factory.buildProtocol = _rememberProtocolInstance
+
+ # Connect a client to it
+ portNum = port.getHost().port
+ clientCreator = protocol.ClientCreator(reactor, self.clientFactory)
+ d2 = clientCreator.connectTCP("127.0.0.1", portNum)
+ def gotClient(client):
+ self.client = client
+ self.addCleanup(self.client.transport.loseConnection)
+ d2.addCallback(gotClient)
+ return defer.gatherResults([d1, d2])
+
+ def assertCommandResponse(self, command, expectedResponseLines,
+ chainDeferred=None):
+ """Asserts that a sending an FTP command receives the expected
+ response.
+
+ Returns a Deferred. Optionally accepts a deferred to chain its actions
+ to.
+ """
+ if chainDeferred is None:
+ chainDeferred = defer.succeed(None)
+
+ def queueCommand(ignored):
+ d = self.client.queueStringCommand(command)
+ def gotResponse(responseLines):
+ self.assertEqual(expectedResponseLines, responseLines)
+ return d.addCallback(gotResponse)
+ return chainDeferred.addCallback(queueCommand)
+
+ def assertCommandFailed(self, command, expectedResponse=None,
+ chainDeferred=None):
+ if chainDeferred is None:
+ chainDeferred = defer.succeed(None)
+
+ def queueCommand(ignored):
+ return self.client.queueStringCommand(command)
+ chainDeferred.addCallback(queueCommand)
+ self.assertFailure(chainDeferred, ftp.CommandFailed)
+ def failed(exception):
+ if expectedResponse is not None:
+ self.assertEqual(
+ expectedResponse, exception.args[0])
+ return chainDeferred.addCallback(failed)
+
+ def _anonymousLogin(self):
+ d = self.assertCommandResponse(
+ 'USER anonymous',
+ ['331 Guest login ok, type your email address as password.'])
+ return self.assertCommandResponse(
+ 'PASS test@twistedmatrix.com',
+ ['230 Anonymous login ok, access restrictions apply.'],
+ chainDeferred=d)
+
+
+
+class FTPAnonymousTestCase(FTPServerTestCase):
+ """
+ Simple tests for an FTP server with different anonymous username.
+ The new anonymous username used in this test case is "guest"
+ """
+ userAnonymous = "guest"
+
+ def test_anonymousLogin(self):
+ """
+ Tests whether the changing of the anonymous username is working or not.
+ The FTP server should not comply about the need of password for the
+ username 'guest', letting it login as anonymous asking just an email
+ address as password.
+ """
+ d = self.assertCommandResponse(
+ 'USER guest',
+ ['331 Guest login ok, type your email address as password.'])
+ return self.assertCommandResponse(
+ 'PASS test@twistedmatrix.com',
+ ['230 Anonymous login ok, access restrictions apply.'],
+ chainDeferred=d)
+
+
+
+class BasicFTPServerTestCase(FTPServerTestCase):
+ def testNotLoggedInReply(self):
+ """When not logged in, all commands other than USER and PASS should
+ get NOT_LOGGED_IN errors.
+ """
+ commandList = ['CDUP', 'CWD', 'LIST', 'MODE', 'PASV',
+ 'PWD', 'RETR', 'STRU', 'SYST', 'TYPE']
+
+ # Issue commands, check responses
+ def checkResponse(exception):
+ failureResponseLines = exception.args[0]
+ self.failUnless(failureResponseLines[-1].startswith("530"),
+ "Response didn't start with 530: %r"
+ % (failureResponseLines[-1],))
+ deferreds = []
+ for command in commandList:
+ deferred = self.client.queueStringCommand(command)
+ self.assertFailure(deferred, ftp.CommandFailed)
+ deferred.addCallback(checkResponse)
+ deferreds.append(deferred)
+ return defer.DeferredList(deferreds, fireOnOneErrback=True)
+
+ def testPASSBeforeUSER(self):
+ """Issuing PASS before USER should give an error."""
+ return self.assertCommandFailed(
+ 'PASS foo',
+ ["503 Incorrect sequence of commands: "
+ "USER required before PASS"])
+
+ def testNoParamsForUSER(self):
+ """Issuing USER without a username is a syntax error."""
+ return self.assertCommandFailed(
+ 'USER',
+ ['500 Syntax error: USER requires an argument.'])
+
+ def testNoParamsForPASS(self):
+ """Issuing PASS without a password is a syntax error."""
+ d = self.client.queueStringCommand('USER foo')
+ return self.assertCommandFailed(
+ 'PASS',
+ ['500 Syntax error: PASS requires an argument.'],
+ chainDeferred=d)
+
+ def testAnonymousLogin(self):
+ return self._anonymousLogin()
+
+ def testQuit(self):
+ """Issuing QUIT should return a 221 message."""
+ d = self._anonymousLogin()
+ return self.assertCommandResponse(
+ 'QUIT',
+ ['221 Goodbye.'],
+ chainDeferred=d)
+
+ def testAnonymousLoginDenied(self):
+ # Reconfigure the server to disallow anonymous access, and to have an
+ # IUsernamePassword checker that always rejects.
+ self.factory.allowAnonymous = False
+ denyAlwaysChecker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ self.factory.portal.registerChecker(denyAlwaysChecker,
+ credentials.IUsernamePassword)
+
+ # Same response code as allowAnonymous=True, but different text.
+ d = self.assertCommandResponse(
+ 'USER anonymous',
+ ['331 Password required for anonymous.'])
+
+ # It will be denied. No-one can login.
+ d = self.assertCommandFailed(
+ 'PASS test@twistedmatrix.com',
+ ['530 Sorry, Authentication failed.'],
+ chainDeferred=d)
+
+ # It's not just saying that. You aren't logged in.
+ d = self.assertCommandFailed(
+ 'PWD',
+ ['530 Please login with USER and PASS.'],
+ chainDeferred=d)
+ return d
+
+
+ def test_anonymousWriteDenied(self):
+ """
+ When an anonymous user attempts to edit the server-side filesystem, they
+ will receive a 550 error with a descriptive message.
+ """
+ d = self._anonymousLogin()
+ return self.assertCommandFailed(
+ 'MKD newdir',
+ ['550 Anonymous users are forbidden to change the filesystem'],
+ chainDeferred=d)
+
+
+ def testUnknownCommand(self):
+ d = self._anonymousLogin()
+ return self.assertCommandFailed(
+ 'GIBBERISH',
+ ["502 Command 'GIBBERISH' not implemented"],
+ chainDeferred=d)
+
+ def testRETRBeforePORT(self):
+ d = self._anonymousLogin()
+ return self.assertCommandFailed(
+ 'RETR foo',
+ ["503 Incorrect sequence of commands: "
+ "PORT or PASV required before RETR"],
+ chainDeferred=d)
+
+ def testSTORBeforePORT(self):
+ d = self._anonymousLogin()
+ return self.assertCommandFailed(
+ 'STOR foo',
+ ["503 Incorrect sequence of commands: "
+ "PORT or PASV required before STOR"],
+ chainDeferred=d)
+
+ def testBadCommandArgs(self):
+ d = self._anonymousLogin()
+ self.assertCommandFailed(
+ 'MODE z',
+ ["504 Not implemented for parameter 'z'."],
+ chainDeferred=d)
+ self.assertCommandFailed(
+ 'STRU I',
+ ["504 Not implemented for parameter 'I'."],
+ chainDeferred=d)
+ return d
+
+ def testDecodeHostPort(self):
+ self.assertEqual(ftp.decodeHostPort('25,234,129,22,100,23'),
+ ('25.234.129.22', 25623))
+ nums = range(6)
+ for i in range(6):
+ badValue = list(nums)
+ badValue[i] = 256
+ s = ','.join(map(str, badValue))
+ self.assertRaises(ValueError, ftp.decodeHostPort, s)
+
+ def testPASV(self):
+ # Login
+ wfd = defer.waitForDeferred(self._anonymousLogin())
+ yield wfd
+ wfd.getResult()
+
+ # Issue a PASV command, and extract the host and port from the response
+ pasvCmd = defer.waitForDeferred(self.client.queueStringCommand('PASV'))
+ yield pasvCmd
+ responseLines = pasvCmd.getResult()
+ host, port = ftp.decodeHostPort(responseLines[-1][4:])
+
+ # Make sure the server is listening on the port it claims to be
+ self.assertEqual(port, self.serverProtocol.dtpPort.getHost().port)
+
+ # Semi-reasonable way to force cleanup
+ self.serverProtocol.transport.loseConnection()
+ testPASV = defer.deferredGenerator(testPASV)
+
+ def testSYST(self):
+ d = self._anonymousLogin()
+ self.assertCommandResponse('SYST', ["215 UNIX Type: L8"],
+ chainDeferred=d)
+ return d
+
+
+ def test_portRangeForwardError(self):
+ """
+ Exceptions other than L{error.CannotListenError} which are raised by
+ C{listenFactory} should be raised to the caller of L{FTP.getDTPPort}.
+ """
+ def listenFactory(portNumber, factory):
+ raise RuntimeError()
+ self.serverProtocol.listenFactory = listenFactory
+
+ self.assertRaises(RuntimeError, self.serverProtocol.getDTPPort,
+ protocol.Factory())
+
+
+ def test_portRange(self):
+ """
+ L{FTP.passivePortRange} should determine the ports which
+ L{FTP.getDTPPort} attempts to bind. If no port from that iterator can
+ be bound, L{error.CannotListenError} should be raised, otherwise the
+ first successful result from L{FTP.listenFactory} should be returned.
+ """
+ def listenFactory(portNumber, factory):
+ if portNumber in (22032, 22033, 22034):
+ raise error.CannotListenError('localhost', portNumber, 'error')
+ return portNumber
+ self.serverProtocol.listenFactory = listenFactory
+
+ port = self.serverProtocol.getDTPPort(protocol.Factory())
+ self.assertEqual(port, 0)
+
+ self.serverProtocol.passivePortRange = xrange(22032, 65536)
+ port = self.serverProtocol.getDTPPort(protocol.Factory())
+ self.assertEqual(port, 22035)
+
+ self.serverProtocol.passivePortRange = xrange(22032, 22035)
+ self.assertRaises(error.CannotListenError,
+ self.serverProtocol.getDTPPort,
+ protocol.Factory())
+
+
+ def test_portRangeInheritedFromFactory(self):
+ """
+ The L{FTP} instances created by L{ftp.FTPFactory.buildProtocol} have
+ their C{passivePortRange} attribute set to the same object the
+ factory's C{passivePortRange} attribute is set to.
+ """
+ portRange = xrange(2017, 2031)
+ self.factory.passivePortRange = portRange
+ protocol = self.factory.buildProtocol(None)
+ self.assertEqual(portRange, protocol.wrappedProtocol.passivePortRange)
+
+
+
+class FTPServerTestCaseAdvancedClient(FTPServerTestCase):
+ """
+ Test FTP server with the L{ftp.FTPClient} class.
+ """
+ clientFactory = ftp.FTPClient
+
+ def test_anonymousSTOR(self):
+ """
+ Try to make an STOR as anonymous, and check that we got a permission
+ denied error.
+ """
+ def eb(res):
+ res.trap(ftp.CommandFailed)
+ self.assertEqual(res.value.args[0][0],
+ '550 foo: Permission denied.')
+ d1, d2 = self.client.storeFile('foo')
+ d2.addErrback(eb)
+ return defer.gatherResults([d1, d2])
+
+
+ def test_STORwriteError(self):
+ """
+ Any errors during writing a file inside a STOR should be returned to
+ the client.
+ """
+ # Make a failing file writer.
+ class FailingFileWriter(ftp._FileWriter):
+ def receive(self):
+ return defer.fail(ftp.IsNotADirectoryError("blah"))
+
+ def failingSTOR(a, b):
+ return defer.succeed(FailingFileWriter(None))
+
+ # Monkey patch the shell so it returns a file writer that will
+ # fail.
+ self.patch(ftp.FTPAnonymousShell, 'openForWriting', failingSTOR)
+
+ def eb(res):
+ self.flushLoggedErrors()
+ res.trap(ftp.CommandFailed)
+ self.assertEqual(
+ res.value.args[0][0],
+ "550 Cannot rmd, blah is not a directory")
+ d1, d2 = self.client.storeFile('failing_file')
+ d2.addErrback(eb)
+ return defer.gatherResults([d1, d2])
+
+
+
+class FTPServerPasvDataConnectionTestCase(FTPServerTestCase):
+ def _makeDataConnection(self, ignored=None):
+ # Establish a passive data connection (i.e. client connecting to
+ # server).
+ d = self.client.queueStringCommand('PASV')
+ def gotPASV(responseLines):
+ host, port = ftp.decodeHostPort(responseLines[-1][4:])
+ cc = protocol.ClientCreator(reactor, _BufferingProtocol)
+ return cc.connectTCP('127.0.0.1', port)
+ return d.addCallback(gotPASV)
+
+ def _download(self, command, chainDeferred=None):
+ if chainDeferred is None:
+ chainDeferred = defer.succeed(None)
+
+ chainDeferred.addCallback(self._makeDataConnection)
+ def queueCommand(downloader):
+ # wait for the command to return, and the download connection to be
+ # closed.
+ d1 = self.client.queueStringCommand(command)
+ d2 = downloader.d
+ return defer.gatherResults([d1, d2])
+ chainDeferred.addCallback(queueCommand)
+
+ def downloadDone((ignored, downloader)):
+ return downloader.buffer
+ return chainDeferred.addCallback(downloadDone)
+
+ def testEmptyLIST(self):
+ # Login
+ d = self._anonymousLogin()
+
+ # No files, so the file listing should be empty
+ self._download('LIST', chainDeferred=d)
+ def checkEmpty(result):
+ self.assertEqual('', result)
+ return d.addCallback(checkEmpty)
+
+ def testTwoDirLIST(self):
+ # Make some directories
+ os.mkdir(os.path.join(self.directory, 'foo'))
+ os.mkdir(os.path.join(self.directory, 'bar'))
+
+ # Login
+ d = self._anonymousLogin()
+
+ # We expect 2 lines because there are two files.
+ self._download('LIST', chainDeferred=d)
+ def checkDownload(download):
+ self.assertEqual(2, len(download[:-2].split('\r\n')))
+ d.addCallback(checkDownload)
+
+ # Download a names-only listing.
+ self._download('NLST ', chainDeferred=d)
+ def checkDownload(download):
+ filenames = download[:-2].split('\r\n')
+ filenames.sort()
+ self.assertEqual(['bar', 'foo'], filenames)
+ d.addCallback(checkDownload)
+
+ # Download a listing of the 'foo' subdirectory. 'foo' has no files, so
+ # the file listing should be empty.
+ self._download('LIST foo', chainDeferred=d)
+ def checkDownload(download):
+ self.assertEqual('', download)
+ d.addCallback(checkDownload)
+
+ # Change the current working directory to 'foo'.
+ def chdir(ignored):
+ return self.client.queueStringCommand('CWD foo')
+ d.addCallback(chdir)
+
+ # Download a listing from within 'foo', and again it should be empty,
+ # because LIST uses the working directory by default.
+ self._download('LIST', chainDeferred=d)
+ def checkDownload(download):
+ self.assertEqual('', download)
+ return d.addCallback(checkDownload)
+
+ def testManyLargeDownloads(self):
+ # Login
+ d = self._anonymousLogin()
+
+ # Download a range of different size files
+ for size in range(100000, 110000, 500):
+ fObj = file(os.path.join(self.directory, '%d.txt' % (size,)), 'wb')
+ fObj.write('x' * size)
+ fObj.close()
+
+ self._download('RETR %d.txt' % (size,), chainDeferred=d)
+ def checkDownload(download, size=size):
+ self.assertEqual(size, len(download))
+ d.addCallback(checkDownload)
+ return d
+
+
+ def test_downloadFolder(self):
+ """
+ When RETR is called for a folder, it will fail complaining that
+ the path is a folder.
+ """
+ # Make a directory in the current working directory
+ self.dirPath.child('foo').createDirectory()
+ # Login
+ d = self._anonymousLogin()
+ d.addCallback(self._makeDataConnection)
+
+ def retrFolder(downloader):
+ downloader.transport.loseConnection()
+ deferred = self.client.queueStringCommand('RETR foo')
+ return deferred
+ d.addCallback(retrFolder)
+
+ def failOnSuccess(result):
+ raise AssertionError('Downloading a folder should not succeed.')
+ d.addCallback(failOnSuccess)
+
+ def checkError(failure):
+ failure.trap(ftp.CommandFailed)
+ self.assertEqual(
+ ['550 foo: is a directory'], failure.value.message)
+ current_errors = self.flushLoggedErrors()
+ self.assertEqual(
+ 0, len(current_errors),
+ 'No errors should be logged while downloading a folder.')
+ d.addErrback(checkError)
+ return d
+
+
+ def test_NLSTEmpty(self):
+ """
+ NLST with no argument returns the directory listing for the current
+ working directory.
+ """
+ # Login
+ d = self._anonymousLogin()
+
+ # Touch a file in the current working directory
+ self.dirPath.child('test.txt').touch()
+ # Make a directory in the current working directory
+ self.dirPath.child('foo').createDirectory()
+
+ self._download('NLST ', chainDeferred=d)
+ def checkDownload(download):
+ filenames = download[:-2].split('\r\n')
+ filenames.sort()
+ self.assertEqual(['foo', 'test.txt'], filenames)
+ return d.addCallback(checkDownload)
+
+
+ def test_NLSTNonexistent(self):
+ """
+ NLST on a non-existent file/directory returns nothing.
+ """
+ # Login
+ d = self._anonymousLogin()
+
+ self._download('NLST nonexistent.txt', chainDeferred=d)
+ def checkDownload(download):
+ self.assertEqual('', download)
+ return d.addCallback(checkDownload)
+
+
+ def test_NLSTOnPathToFile(self):
+ """
+ NLST on an existent file returns only the path to that file.
+ """
+ # Login
+ d = self._anonymousLogin()
+
+ # Touch a file in the current working directory
+ self.dirPath.child('test.txt').touch()
+
+ self._download('NLST test.txt', chainDeferred=d)
+ def checkDownload(download):
+ filenames = download[:-2].split('\r\n')
+ self.assertEqual(['test.txt'], filenames)
+ return d.addCallback(checkDownload)
+
+
+
+class FTPServerPortDataConnectionTestCase(FTPServerPasvDataConnectionTestCase):
+ def setUp(self):
+ self.dataPorts = []
+ return FTPServerPasvDataConnectionTestCase.setUp(self)
+
+ def _makeDataConnection(self, ignored=None):
+ # Establish an active data connection (i.e. server connecting to
+ # client).
+ deferred = defer.Deferred()
+ class DataFactory(protocol.ServerFactory):
+ protocol = _BufferingProtocol
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ reactor.callLater(0, deferred.callback, p)
+ return p
+ dataPort = reactor.listenTCP(0, DataFactory(), interface='127.0.0.1')
+ self.dataPorts.append(dataPort)
+ cmd = 'PORT ' + ftp.encodeHostPort('127.0.0.1', dataPort.getHost().port)
+ self.client.queueStringCommand(cmd)
+ return deferred
+
+ def tearDown(self):
+ l = [defer.maybeDeferred(port.stopListening) for port in self.dataPorts]
+ d = defer.maybeDeferred(
+ FTPServerPasvDataConnectionTestCase.tearDown, self)
+ l.append(d)
+ return defer.DeferredList(l, fireOnOneErrback=True)
+
+ def testPORTCannotConnect(self):
+ # Login
+ d = self._anonymousLogin()
+
+ # Listen on a port, and immediately stop listening as a way to find a
+ # port number that is definitely closed.
+ def loggedIn(ignored):
+ port = reactor.listenTCP(0, protocol.Factory(),
+ interface='127.0.0.1')
+ portNum = port.getHost().port
+ d = port.stopListening()
+ d.addCallback(lambda _: portNum)
+ return d
+ d.addCallback(loggedIn)
+
+ # Tell the server to connect to that port with a PORT command, and
+ # verify that it fails with the right error.
+ def gotPortNum(portNum):
+ return self.assertCommandFailed(
+ 'PORT ' + ftp.encodeHostPort('127.0.0.1', portNum),
+ ["425 Can't open data connection."])
+ return d.addCallback(gotPortNum)
+
+
+
+class DTPFactoryTests(unittest.TestCase):
+ """
+ Tests for L{ftp.DTPFactory}.
+ """
+ def setUp(self):
+ """
+ Create a fake protocol interpreter and a L{ftp.DTPFactory} instance to
+ test.
+ """
+ self.reactor = task.Clock()
+
+ class ProtocolInterpreter(object):
+ dtpInstance = None
+
+ self.protocolInterpreter = ProtocolInterpreter()
+ self.factory = ftp.DTPFactory(
+ self.protocolInterpreter, None, self.reactor)
+
+
+ def test_setTimeout(self):
+ """
+ L{ftp.DTPFactory.setTimeout} uses the reactor passed to its initializer
+ to set up a timed event to time out the DTP setup after the specified
+ number of seconds.
+ """
+ # Make sure the factory's deferred fails with the right exception, and
+ # make it so we can tell exactly when it fires.
+ finished = []
+ d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
+ d.addCallback(finished.append)
+
+ self.factory.setTimeout(6)
+
+ # Advance the clock almost to the timeout
+ self.reactor.advance(5)
+
+ # Nothing should have happened yet.
+ self.assertFalse(finished)
+
+ # Advance it to the configured timeout.
+ self.reactor.advance(1)
+
+ # Now the Deferred should have failed with TimeoutError.
+ self.assertTrue(finished)
+
+ # There should also be no calls left in the reactor.
+ self.assertFalse(self.reactor.calls)
+
+
+ def test_buildProtocolOnce(self):
+ """
+ A L{ftp.DTPFactory} instance's C{buildProtocol} method can be used once
+ to create a L{ftp.DTP} instance.
+ """
+ protocol = self.factory.buildProtocol(None)
+ self.assertIsInstance(protocol, ftp.DTP)
+
+ # A subsequent call returns None.
+ self.assertIdentical(self.factory.buildProtocol(None), None)
+
+
+ def test_timeoutAfterConnection(self):
+ """
+ If a timeout has been set up using L{ftp.DTPFactory.setTimeout}, it is
+ cancelled by L{ftp.DTPFactory.buildProtocol}.
+ """
+ self.factory.setTimeout(10)
+ protocol = self.factory.buildProtocol(None)
+ # Make sure the call is no longer active.
+ self.assertFalse(self.reactor.calls)
+
+
+ def test_connectionAfterTimeout(self):
+ """
+ If L{ftp.DTPFactory.buildProtocol} is called after the timeout
+ specified by L{ftp.DTPFactory.setTimeout} has elapsed, C{None} is
+ returned.
+ """
+ # Handle the error so it doesn't get logged.
+ d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
+
+ # Set up the timeout and then cause it to elapse so the Deferred does
+ # fail.
+ self.factory.setTimeout(10)
+ self.reactor.advance(10)
+
+ # Try to get a protocol - we should not be able to.
+ self.assertIdentical(self.factory.buildProtocol(None), None)
+
+ # Make sure the Deferred is doing the right thing.
+ return d
+
+
+ def test_timeoutAfterConnectionFailed(self):
+ """
+ L{ftp.DTPFactory.deferred} fails with L{PortConnectionError} when
+ L{ftp.DTPFactory.clientConnectionFailed} is called. If the timeout
+ specified with L{ftp.DTPFactory.setTimeout} expires after that, nothing
+ additional happens.
+ """
+ finished = []
+ d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
+ d.addCallback(finished.append)
+
+ self.factory.setTimeout(10)
+ self.assertFalse(finished)
+ self.factory.clientConnectionFailed(None, None)
+ self.assertTrue(finished)
+ self.reactor.advance(10)
+ return d
+
+
+ def test_connectionFailedAfterTimeout(self):
+ """
+ If L{ftp.DTPFactory.clientConnectionFailed} is called after the timeout
+ specified by L{ftp.DTPFactory.setTimeout} has elapsed, nothing beyond
+ the normal timeout before happens.
+ """
+ # Handle the error so it doesn't get logged.
+ d = self.assertFailure(self.factory.deferred, ftp.PortConnectionError)
+
+ # Set up the timeout and then cause it to elapse so the Deferred does
+ # fail.
+ self.factory.setTimeout(10)
+ self.reactor.advance(10)
+
+ # Now fail the connection attempt. This should do nothing. In
+ # particular, it should not raise an exception.
+ self.factory.clientConnectionFailed(None, defer.TimeoutError("foo"))
+
+ # Give the Deferred to trial so it can make sure it did what we
+ # expected.
+ return d
+
+
+
+# -- Client Tests -----------------------------------------------------------
+
+class PrintLines(protocol.Protocol):
+ """Helper class used by FTPFileListingTests."""
+
+ def __init__(self, lines):
+ self._lines = lines
+
+ def connectionMade(self):
+ for line in self._lines:
+ self.transport.write(line + "\r\n")
+ self.transport.loseConnection()
+
+
+class MyFTPFileListProtocol(ftp.FTPFileListProtocol):
+ def __init__(self):
+ self.other = []
+ ftp.FTPFileListProtocol.__init__(self)
+
+ def unknownLine(self, line):
+ self.other.append(line)
+
+
+class FTPFileListingTests(unittest.TestCase):
+ def getFilesForLines(self, lines):
+ fileList = MyFTPFileListProtocol()
+ d = loopback.loopbackAsync(PrintLines(lines), fileList)
+ d.addCallback(lambda _: (fileList.files, fileList.other))
+ return d
+
+ def testOneLine(self):
+ # This example line taken from the docstring for FTPFileListProtocol
+ line = '-rw-r--r-- 1 root other 531 Jan 29 03:26 README'
+ def check(((file,), other)):
+ self.failIf(other, 'unexpect unparsable lines: %s' % repr(other))
+ self.failUnless(file['filetype'] == '-', 'misparsed fileitem')
+ self.failUnless(file['perms'] == 'rw-r--r--', 'misparsed perms')
+ self.failUnless(file['owner'] == 'root', 'misparsed fileitem')
+ self.failUnless(file['group'] == 'other', 'misparsed fileitem')
+ self.failUnless(file['size'] == 531, 'misparsed fileitem')
+ self.failUnless(file['date'] == 'Jan 29 03:26', 'misparsed fileitem')
+ self.failUnless(file['filename'] == 'README', 'misparsed fileitem')
+ self.failUnless(file['nlinks'] == 1, 'misparsed nlinks')
+ self.failIf(file['linktarget'], 'misparsed linktarget')
+ return self.getFilesForLines([line]).addCallback(check)
+
+ def testVariantLines(self):
+ line1 = 'drw-r--r-- 2 root other 531 Jan 9 2003 A'
+ line2 = 'lrw-r--r-- 1 root other 1 Jan 29 03:26 B -> A'
+ line3 = 'woohoo! '
+ def check(((file1, file2), (other,))):
+ self.failUnless(other == 'woohoo! \r', 'incorrect other line')
+ # file 1
+ self.failUnless(file1['filetype'] == 'd', 'misparsed fileitem')
+ self.failUnless(file1['perms'] == 'rw-r--r--', 'misparsed perms')
+ self.failUnless(file1['owner'] == 'root', 'misparsed owner')
+ self.failUnless(file1['group'] == 'other', 'misparsed group')
+ self.failUnless(file1['size'] == 531, 'misparsed size')
+ self.failUnless(file1['date'] == 'Jan 9 2003', 'misparsed date')
+ self.failUnless(file1['filename'] == 'A', 'misparsed filename')
+ self.failUnless(file1['nlinks'] == 2, 'misparsed nlinks')
+ self.failIf(file1['linktarget'], 'misparsed linktarget')
+ # file 2
+ self.failUnless(file2['filetype'] == 'l', 'misparsed fileitem')
+ self.failUnless(file2['perms'] == 'rw-r--r--', 'misparsed perms')
+ self.failUnless(file2['owner'] == 'root', 'misparsed owner')
+ self.failUnless(file2['group'] == 'other', 'misparsed group')
+ self.failUnless(file2['size'] == 1, 'misparsed size')
+ self.failUnless(file2['date'] == 'Jan 29 03:26', 'misparsed date')
+ self.failUnless(file2['filename'] == 'B', 'misparsed filename')
+ self.failUnless(file2['nlinks'] == 1, 'misparsed nlinks')
+ self.failUnless(file2['linktarget'] == 'A', 'misparsed linktarget')
+ return self.getFilesForLines([line1, line2, line3]).addCallback(check)
+
+ def testUnknownLine(self):
+ def check((files, others)):
+ self.failIf(files, 'unexpected file entries')
+ self.failUnless(others == ['ABC\r', 'not a file\r'],
+ 'incorrect unparsable lines: %s' % repr(others))
+ return self.getFilesForLines(['ABC', 'not a file']).addCallback(check)
+
+ def testYear(self):
+ # This example derived from bug description in issue 514.
+ fileList = ftp.FTPFileListProtocol()
+ exampleLine = (
+ '-rw-r--r-- 1 root other 531 Jan 29 2003 README\n')
+ class PrintLine(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.write(exampleLine)
+ self.transport.loseConnection()
+
+ def check(ignored):
+ file = fileList.files[0]
+ self.failUnless(file['size'] == 531, 'misparsed fileitem')
+ self.failUnless(file['date'] == 'Jan 29 2003', 'misparsed fileitem')
+ self.failUnless(file['filename'] == 'README', 'misparsed fileitem')
+
+ d = loopback.loopbackAsync(PrintLine(), fileList)
+ return d.addCallback(check)
+
+
+class FTPClientTests(unittest.TestCase):
+
+ def testFailedRETR(self):
+ f = protocol.Factory()
+ f.noisy = 0
+ port = reactor.listenTCP(0, f, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+ portNum = port.getHost().port
+ # This test data derived from a bug report by ranty on #twisted
+ responses = ['220 ready, dude (vsFTPd 1.0.0: beat me, break me)',
+ # USER anonymous
+ '331 Please specify the password.',
+ # PASS twisted@twistedmatrix.com
+ '230 Login successful. Have fun.',
+ # TYPE I
+ '200 Binary it is, then.',
+ # PASV
+ '227 Entering Passive Mode (127,0,0,1,%d,%d)' %
+ (portNum >> 8, portNum & 0xff),
+ # RETR /file/that/doesnt/exist
+ '550 Failed to open file.']
+ f.buildProtocol = lambda addr: PrintLines(responses)
+
+ client = ftp.FTPClient(passive=1)
+ cc = protocol.ClientCreator(reactor, ftp.FTPClient, passive=1)
+ d = cc.connectTCP('127.0.0.1', portNum)
+ def gotClient(client):
+ p = protocol.Protocol()
+ return client.retrieveFile('/file/that/doesnt/exist', p)
+ d.addCallback(gotClient)
+ return self.assertFailure(d, ftp.CommandFailed)
+
+ def test_errbacksUponDisconnect(self):
+ """
+ Test the ftp command errbacks when a connection lost happens during
+ the operation.
+ """
+ ftpClient = ftp.FTPClient()
+ tr = proto_helpers.StringTransportWithDisconnection()
+ ftpClient.makeConnection(tr)
+ tr.protocol = ftpClient
+ d = ftpClient.list('some path', Dummy())
+ m = []
+ def _eb(failure):
+ m.append(failure)
+ return None
+ d.addErrback(_eb)
+ from twisted.internet.main import CONNECTION_LOST
+ ftpClient.connectionLost(failure.Failure(CONNECTION_LOST))
+ self.failUnless(m, m)
+ return d
+
+
+
+class FTPClientTestCase(unittest.TestCase):
+ """
+ Test advanced FTP client commands.
+ """
+ def setUp(self):
+ """
+ Create a FTP client and connect it to fake transport.
+ """
+ self.client = ftp.FTPClient()
+ self.transport = proto_helpers.StringTransportWithDisconnection()
+ self.client.makeConnection(self.transport)
+ self.transport.protocol = self.client
+
+
+ def tearDown(self):
+ """
+ Deliver disconnection notification to the client so that it can
+ perform any cleanup which may be required.
+ """
+ self.client.connectionLost(error.ConnectionLost())
+
+
+ def _testLogin(self):
+ """
+ Test the login part.
+ """
+ self.assertEqual(self.transport.value(), '')
+ self.client.lineReceived(
+ '331 Guest login ok, type your email address as password.')
+ self.assertEqual(self.transport.value(), 'USER anonymous\r\n')
+ self.transport.clear()
+ self.client.lineReceived(
+ '230 Anonymous login ok, access restrictions apply.')
+ self.assertEqual(self.transport.value(), 'TYPE I\r\n')
+ self.transport.clear()
+ self.client.lineReceived('200 Type set to I.')
+
+
+ def test_CDUP(self):
+ """
+ Test the CDUP command.
+
+ L{ftp.FTPClient.cdup} should return a Deferred which fires with a
+ sequence of one element which is the string the server sent
+ indicating that the command was executed successfully.
+
+ (XXX - This is a bad API)
+ """
+ def cbCdup(res):
+ self.assertEqual(res[0], '250 Requested File Action Completed OK')
+
+ self._testLogin()
+ d = self.client.cdup().addCallback(cbCdup)
+ self.assertEqual(self.transport.value(), 'CDUP\r\n')
+ self.transport.clear()
+ self.client.lineReceived('250 Requested File Action Completed OK')
+ return d
+
+
+ def test_failedCDUP(self):
+ """
+ Test L{ftp.FTPClient.cdup}'s handling of a failed CDUP command.
+
+ When the CDUP command fails, the returned Deferred should errback
+ with L{ftp.CommandFailed}.
+ """
+ self._testLogin()
+ d = self.client.cdup()
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'CDUP\r\n')
+ self.transport.clear()
+ self.client.lineReceived('550 ..: No such file or directory')
+ return d
+
+
+ def test_PWD(self):
+ """
+ Test the PWD command.
+
+ L{ftp.FTPClient.pwd} should return a Deferred which fires with a
+ sequence of one element which is a string representing the current
+ working directory on the server.
+
+ (XXX - This is a bad API)
+ """
+ def cbPwd(res):
+ self.assertEqual(ftp.parsePWDResponse(res[0]), "/bar/baz")
+
+ self._testLogin()
+ d = self.client.pwd().addCallback(cbPwd)
+ self.assertEqual(self.transport.value(), 'PWD\r\n')
+ self.client.lineReceived('257 "/bar/baz"')
+ return d
+
+
+ def test_failedPWD(self):
+ """
+ Test a failure in PWD command.
+
+ When the PWD command fails, the returned Deferred should errback
+ with L{ftp.CommandFailed}.
+ """
+ self._testLogin()
+ d = self.client.pwd()
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PWD\r\n')
+ self.client.lineReceived('550 /bar/baz: No such file or directory')
+ return d
+
+
+ def test_CWD(self):
+ """
+ Test the CWD command.
+
+ L{ftp.FTPClient.cwd} should return a Deferred which fires with a
+ sequence of one element which is the string the server sent
+ indicating that the command was executed successfully.
+
+ (XXX - This is a bad API)
+ """
+ def cbCwd(res):
+ self.assertEqual(res[0], '250 Requested File Action Completed OK')
+
+ self._testLogin()
+ d = self.client.cwd("bar/foo").addCallback(cbCwd)
+ self.assertEqual(self.transport.value(), 'CWD bar/foo\r\n')
+ self.client.lineReceived('250 Requested File Action Completed OK')
+ return d
+
+
+ def test_failedCWD(self):
+ """
+ Test a failure in CWD command.
+
+ When the PWD command fails, the returned Deferred should errback
+ with L{ftp.CommandFailed}.
+ """
+ self._testLogin()
+ d = self.client.cwd("bar/foo")
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'CWD bar/foo\r\n')
+ self.client.lineReceived('550 bar/foo: No such file or directory')
+ return d
+
+
+ def test_passiveRETR(self):
+ """
+ Test the RETR command in passive mode: get a file and verify its
+ content.
+
+ L{ftp.FTPClient.retrieveFile} should return a Deferred which fires
+ with the protocol instance passed to it after the download has
+ completed.
+
+ (XXX - This API should be based on producers and consumers)
+ """
+ def cbRetr(res, proto):
+ self.assertEqual(proto.buffer, 'x' * 1000)
+
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ proto.dataReceived("x" * 1000)
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ proto = _BufferingProtocol()
+ d = self.client.retrieveFile("spam", proto)
+ d.addCallback(cbRetr, proto)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'RETR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_RETR(self):
+ """
+ Test the RETR command in non-passive mode.
+
+ Like L{test_passiveRETR} but in the configuration where the server
+ establishes the data connection to the client, rather than the other
+ way around.
+ """
+ self.client.passive = False
+
+ def generatePort(portCmd):
+ portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
+ portCmd.protocol.makeConnection(proto_helpers.StringTransport())
+ portCmd.protocol.dataReceived("x" * 1000)
+ portCmd.protocol.connectionLost(
+ failure.Failure(error.ConnectionDone("")))
+
+ def cbRetr(res, proto):
+ self.assertEqual(proto.buffer, 'x' * 1000)
+
+ self.client.generatePortCommand = generatePort
+ self._testLogin()
+ proto = _BufferingProtocol()
+ d = self.client.retrieveFile("spam", proto)
+ d.addCallback(cbRetr, proto)
+ self.assertEqual(self.transport.value(), 'PORT %s\r\n' %
+ (ftp.encodeHostPort('127.0.0.1', 9876),))
+ self.transport.clear()
+ self.client.lineReceived('200 PORT OK')
+ self.assertEqual(self.transport.value(), 'RETR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_failedRETR(self):
+ """
+ Try to RETR an unexisting file.
+
+ L{ftp.FTPClient.retrieveFile} should return a Deferred which
+ errbacks with L{ftp.CommandFailed} if the server indicates the file
+ cannot be transferred for some reason.
+ """
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ proto = _BufferingProtocol()
+ d = self.client.retrieveFile("spam", proto)
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'RETR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived('550 spam: No such file or directory')
+ return d
+
+
+ def test_lostRETR(self):
+ """
+ Try a RETR, but disconnect during the transfer.
+ L{ftp.FTPClient.retrieveFile} should return a Deferred which
+ errbacks with L{ftp.ConnectionLost)
+ """
+ self.client.passive = False
+
+ l = []
+ def generatePort(portCmd):
+ portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
+ tr = proto_helpers.StringTransportWithDisconnection()
+ portCmd.protocol.makeConnection(tr)
+ tr.protocol = portCmd.protocol
+ portCmd.protocol.dataReceived("x" * 500)
+ l.append(tr)
+
+ self.client.generatePortCommand = generatePort
+ self._testLogin()
+ proto = _BufferingProtocol()
+ d = self.client.retrieveFile("spam", proto)
+ self.assertEqual(self.transport.value(), 'PORT %s\r\n' %
+ (ftp.encodeHostPort('127.0.0.1', 9876),))
+ self.transport.clear()
+ self.client.lineReceived('200 PORT OK')
+ self.assertEqual(self.transport.value(), 'RETR spam\r\n')
+
+ self.assert_(l)
+ l[0].loseConnection()
+ self.transport.loseConnection()
+ self.assertFailure(d, ftp.ConnectionLost)
+ return d
+
+
+ def test_passiveSTOR(self):
+ """
+ Test the STOR command: send a file and verify its content.
+
+ L{ftp.FTPClient.storeFile} should return a two-tuple of Deferreds.
+ The first of which should fire with a protocol instance when the
+ data connection has been established and is responsible for sending
+ the contents of the file. The second of which should fire when the
+ upload has completed, the data connection has been closed, and the
+ server has acknowledged receipt of the file.
+
+ (XXX - storeFile should take a producer as an argument, instead, and
+ only return a Deferred which fires when the upload has succeeded or
+ failed).
+ """
+ tr = proto_helpers.StringTransport()
+ def cbStore(sender):
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ sender.transport.write("x" * 1000)
+ sender.finish()
+ sender.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ def cbFinish(ign):
+ self.assertEqual(tr.value(), "x" * 1000)
+
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(tr)
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ d1, d2 = self.client.storeFile("spam")
+ d1.addCallback(cbStore)
+ d2.addCallback(cbFinish)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'STOR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived('226 Transfer Complete.')
+ return defer.gatherResults([d1, d2])
+
+
+ def test_failedSTOR(self):
+ """
+ Test a failure in the STOR command.
+
+ If the server does not acknowledge successful receipt of the
+ uploaded file, the second Deferred returned by
+ L{ftp.FTPClient.storeFile} should errback with L{ftp.CommandFailed}.
+ """
+ tr = proto_helpers.StringTransport()
+ def cbStore(sender):
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ sender.transport.write("x" * 1000)
+ sender.finish()
+ sender.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(tr)
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ d1, d2 = self.client.storeFile("spam")
+ d1.addCallback(cbStore)
+ self.assertFailure(d2, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'STOR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived(
+ '426 Transfer aborted. Data connection closed.')
+ return defer.gatherResults([d1, d2])
+
+
+ def test_STOR(self):
+ """
+ Test the STOR command in non-passive mode.
+
+ Like L{test_passiveSTOR} but in the configuration where the server
+ establishes the data connection to the client, rather than the other
+ way around.
+ """
+ tr = proto_helpers.StringTransport()
+ self.client.passive = False
+ def generatePort(portCmd):
+ portCmd.text = 'PORT %s' % ftp.encodeHostPort('127.0.0.1', 9876)
+ portCmd.protocol.makeConnection(tr)
+
+ def cbStore(sender):
+ self.assertEqual(self.transport.value(), 'PORT %s\r\n' %
+ (ftp.encodeHostPort('127.0.0.1', 9876),))
+ self.transport.clear()
+ self.client.lineReceived('200 PORT OK')
+ self.assertEqual(self.transport.value(), 'STOR spam\r\n')
+ self.transport.clear()
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ sender.transport.write("x" * 1000)
+ sender.finish()
+ sender.connectionLost(failure.Failure(error.ConnectionDone("")))
+ self.client.lineReceived('226 Transfer Complete.')
+
+ def cbFinish(ign):
+ self.assertEqual(tr.value(), "x" * 1000)
+
+ self.client.generatePortCommand = generatePort
+ self._testLogin()
+ d1, d2 = self.client.storeFile("spam")
+ d1.addCallback(cbStore)
+ d2.addCallback(cbFinish)
+ return defer.gatherResults([d1, d2])
+
+
+ def test_passiveLIST(self):
+ """
+ Test the LIST command.
+
+ L{ftp.FTPClient.list} should return a Deferred which fires with a
+ protocol instance which was passed to list after the command has
+ succeeded.
+
+ (XXX - This is a very unfortunate API; if my understanding is
+ correct, the results are always at least line-oriented, so allowing
+ a per-line parser function to be specified would make this simpler,
+ but a default implementation should really be provided which knows
+ how to deal with all the formats used in real servers, so
+ application developers never have to care about this insanity. It
+ would also be nice to either get back a Deferred of a list of
+ filenames or to be able to consume the files as they are received
+ (which the current API does allow, but in a somewhat inconvenient
+ fashion) -exarkun)
+ """
+ def cbList(res, fileList):
+ fls = [f["filename"] for f in fileList.files]
+ expected = ["foo", "bar", "baz"]
+ expected.sort()
+ fls.sort()
+ self.assertEqual(fls, expected)
+
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ sending = [
+ '-rw-r--r-- 0 spam egg 100 Oct 10 2006 foo\r\n',
+ '-rw-r--r-- 3 spam egg 100 Oct 10 2006 bar\r\n',
+ '-rw-r--r-- 4 spam egg 100 Oct 10 2006 baz\r\n',
+ ]
+ for i in sending:
+ proto.dataReceived(i)
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ fileList = ftp.FTPFileListProtocol()
+ d = self.client.list('foo/bar', fileList).addCallback(cbList, fileList)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'LIST foo/bar\r\n')
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_LIST(self):
+ """
+ Test the LIST command in non-passive mode.
+
+ Like L{test_passiveLIST} but in the configuration where the server
+ establishes the data connection to the client, rather than the other
+ way around.
+ """
+ self.client.passive = False
+ def generatePort(portCmd):
+ portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
+ portCmd.protocol.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ sending = [
+ '-rw-r--r-- 0 spam egg 100 Oct 10 2006 foo\r\n',
+ '-rw-r--r-- 3 spam egg 100 Oct 10 2006 bar\r\n',
+ '-rw-r--r-- 4 spam egg 100 Oct 10 2006 baz\r\n',
+ ]
+ for i in sending:
+ portCmd.protocol.dataReceived(i)
+ portCmd.protocol.connectionLost(
+ failure.Failure(error.ConnectionDone("")))
+
+ def cbList(res, fileList):
+ fls = [f["filename"] for f in fileList.files]
+ expected = ["foo", "bar", "baz"]
+ expected.sort()
+ fls.sort()
+ self.assertEqual(fls, expected)
+
+ self.client.generatePortCommand = generatePort
+ self._testLogin()
+ fileList = ftp.FTPFileListProtocol()
+ d = self.client.list('foo/bar', fileList).addCallback(cbList, fileList)
+ self.assertEqual(self.transport.value(), 'PORT %s\r\n' %
+ (ftp.encodeHostPort('127.0.0.1', 9876),))
+ self.transport.clear()
+ self.client.lineReceived('200 PORT OK')
+ self.assertEqual(self.transport.value(), 'LIST foo/bar\r\n')
+ self.transport.clear()
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_failedLIST(self):
+ """
+ Test a failure in LIST command.
+
+ L{ftp.FTPClient.list} should return a Deferred which fails with
+ L{ftp.CommandFailed} if the server indicates the indicated path is
+ invalid for some reason.
+ """
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ fileList = ftp.FTPFileListProtocol()
+ d = self.client.list('foo/bar', fileList)
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'LIST foo/bar\r\n')
+ self.client.lineReceived('550 foo/bar: No such file or directory')
+ return d
+
+
+ def test_NLST(self):
+ """
+ Test the NLST command in non-passive mode.
+
+ L{ftp.FTPClient.nlst} should return a Deferred which fires with a
+ list of filenames when the list command has completed.
+ """
+ self.client.passive = False
+ def generatePort(portCmd):
+ portCmd.text = 'PORT %s' % (ftp.encodeHostPort('127.0.0.1', 9876),)
+ portCmd.protocol.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ portCmd.protocol.dataReceived('foo\r\n')
+ portCmd.protocol.dataReceived('bar\r\n')
+ portCmd.protocol.dataReceived('baz\r\n')
+ portCmd.protocol.connectionLost(
+ failure.Failure(error.ConnectionDone("")))
+
+ def cbList(res, proto):
+ fls = proto.buffer.splitlines()
+ expected = ["foo", "bar", "baz"]
+ expected.sort()
+ fls.sort()
+ self.assertEqual(fls, expected)
+
+ self.client.generatePortCommand = generatePort
+ self._testLogin()
+ lstproto = _BufferingProtocol()
+ d = self.client.nlst('foo/bar', lstproto).addCallback(cbList, lstproto)
+ self.assertEqual(self.transport.value(), 'PORT %s\r\n' %
+ (ftp.encodeHostPort('127.0.0.1', 9876),))
+ self.transport.clear()
+ self.client.lineReceived('200 PORT OK')
+ self.assertEqual(self.transport.value(), 'NLST foo/bar\r\n')
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_passiveNLST(self):
+ """
+ Test the NLST command.
+
+ Like L{test_passiveNLST} but in the configuration where the server
+ establishes the data connection to the client, rather than the other
+ way around.
+ """
+ def cbList(res, proto):
+ fls = proto.buffer.splitlines()
+ expected = ["foo", "bar", "baz"]
+ expected.sort()
+ fls.sort()
+ self.assertEqual(fls, expected)
+
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(proto_helpers.StringTransport())
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ proto.dataReceived('foo\r\n')
+ proto.dataReceived('bar\r\n')
+ proto.dataReceived('baz\r\n')
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ lstproto = _BufferingProtocol()
+ d = self.client.nlst('foo/bar', lstproto).addCallback(cbList, lstproto)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'NLST foo/bar\r\n')
+ self.client.lineReceived('226 Transfer Complete.')
+ return d
+
+
+ def test_failedNLST(self):
+ """
+ Test a failure in NLST command.
+
+ L{ftp.FTPClient.nlst} should return a Deferred which fails with
+ L{ftp.CommandFailed} if the server indicates the indicated path is
+ invalid for some reason.
+ """
+ tr = proto_helpers.StringTransport()
+ def cbConnect(host, port, factory):
+ self.assertEqual(host, '127.0.0.1')
+ self.assertEqual(port, 12345)
+ proto = factory.buildProtocol((host, port))
+ proto.makeConnection(tr)
+ self.client.lineReceived(
+ '150 File status okay; about to open data connection.')
+ proto.connectionLost(failure.Failure(error.ConnectionDone("")))
+
+ self.client.connectFactory = cbConnect
+ self._testLogin()
+ lstproto = _BufferingProtocol()
+ d = self.client.nlst('foo/bar', lstproto)
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PASV\r\n')
+ self.transport.clear()
+ self.client.lineReceived('227 Entering Passive Mode (%s).' %
+ (ftp.encodeHostPort('127.0.0.1', 12345),))
+ self.assertEqual(self.transport.value(), 'NLST foo/bar\r\n')
+ self.client.lineReceived('550 foo/bar: No such file or directory')
+ return d
+
+
+ def test_changeDirectoryDeprecated(self):
+ """
+ L{ftp.FTPClient.changeDirectory} is deprecated and the direct caller of
+ it is warned of this.
+ """
+ self._testLogin()
+ d = self.assertWarns(
+ DeprecationWarning,
+ "FTPClient.changeDirectory is deprecated in Twisted 8.2 and "
+ "newer. Use FTPClient.cwd instead.",
+ __file__,
+ lambda: self.client.changeDirectory('.'))
+ # This is necessary to make the Deferred fire. The Deferred needs
+ # to fire so that tearDown doesn't cause it to errback and fail this
+ # or (more likely) a later test.
+ self.client.lineReceived('250 success')
+ return d
+
+
+ def test_changeDirectory(self):
+ """
+ Test the changeDirectory method.
+
+ L{ftp.FTPClient.changeDirectory} should return a Deferred which fires
+ with True if succeeded.
+ """
+ def cbCd(res):
+ self.assertEqual(res, True)
+
+ self._testLogin()
+ d = self.client.changeDirectory("bar/foo").addCallback(cbCd)
+ self.assertEqual(self.transport.value(), 'CWD bar/foo\r\n')
+ self.client.lineReceived('250 Requested File Action Completed OK')
+ return d
+ test_changeDirectory.suppress = [_changeDirectorySuppression]
+
+
+ def test_failedChangeDirectory(self):
+ """
+ Test a failure in the changeDirectory method.
+
+ The behaviour here is the same as a failed CWD.
+ """
+ self._testLogin()
+ d = self.client.changeDirectory("bar/foo")
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'CWD bar/foo\r\n')
+ self.client.lineReceived('550 bar/foo: No such file or directory')
+ return d
+ test_failedChangeDirectory.suppress = [_changeDirectorySuppression]
+
+
+ def test_strangeFailedChangeDirectory(self):
+ """
+ Test a strange failure in changeDirectory method.
+
+ L{ftp.FTPClient.changeDirectory} is stricter than CWD as it checks
+ code 250 for success.
+ """
+ self._testLogin()
+ d = self.client.changeDirectory("bar/foo")
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'CWD bar/foo\r\n')
+ self.client.lineReceived('252 I do what I want !')
+ return d
+ test_strangeFailedChangeDirectory.suppress = [_changeDirectorySuppression]
+
+
+ def test_renameFromTo(self):
+ """
+ L{ftp.FTPClient.rename} issues I{RNTO} and I{RNFR} commands and returns
+ a L{Deferred} which fires when a file has successfully been renamed.
+ """
+ self._testLogin()
+
+ d = self.client.rename("/spam", "/ham")
+ self.assertEqual(self.transport.value(), 'RNFR /spam\r\n')
+ self.transport.clear()
+
+ fromResponse = (
+ '350 Requested file action pending further information.\r\n')
+ self.client.lineReceived(fromResponse)
+ self.assertEqual(self.transport.value(), 'RNTO /ham\r\n')
+ toResponse = (
+ '250 Requested File Action Completed OK')
+ self.client.lineReceived(toResponse)
+
+ d.addCallback(self.assertEqual, ([fromResponse], [toResponse]))
+ return d
+
+
+ def test_renameFromToEscapesPaths(self):
+ """
+ L{ftp.FTPClient.rename} issues I{RNTO} and I{RNFR} commands with paths
+ escaped according to U{http://cr.yp.to/ftp/filesystem.html}.
+ """
+ self._testLogin()
+
+ fromFile = "/foo/ba\nr/baz"
+ toFile = "/qu\nux"
+ self.client.rename(fromFile, toFile)
+ self.client.lineReceived("350 ")
+ self.client.lineReceived("250 ")
+ self.assertEqual(
+ self.transport.value(),
+ "RNFR /foo/ba\x00r/baz\r\n"
+ "RNTO /qu\x00ux\r\n")
+
+
+ def test_renameFromToFailingOnFirstError(self):
+ """
+ The L{Deferred} returned by L{ftp.FTPClient.rename} is errbacked with
+ L{CommandFailed} if the I{RNFR} command receives an error response code
+ (for example, because the file does not exist).
+ """
+ self._testLogin()
+
+ d = self.client.rename("/spam", "/ham")
+ self.assertEqual(self.transport.value(), 'RNFR /spam\r\n')
+ self.transport.clear()
+
+ self.client.lineReceived('550 Requested file unavailable.\r\n')
+ # The RNTO should not execute since the RNFR failed.
+ self.assertEqual(self.transport.value(), '')
+
+ return self.assertFailure(d, ftp.CommandFailed)
+
+
+ def test_renameFromToFailingOnRenameTo(self):
+ """
+ The L{Deferred} returned by L{ftp.FTPClient.rename} is errbacked with
+ L{CommandFailed} if the I{RNTO} command receives an error response code
+ (for example, because the destination directory does not exist).
+ """
+ self._testLogin()
+
+ d = self.client.rename("/spam", "/ham")
+ self.assertEqual(self.transport.value(), 'RNFR /spam\r\n')
+ self.transport.clear()
+
+ self.client.lineReceived('350 Requested file action pending further information.\r\n')
+ self.assertEqual(self.transport.value(), 'RNTO /ham\r\n')
+ self.client.lineReceived('550 Requested file unavailable.\r\n')
+ return self.assertFailure(d, ftp.CommandFailed)
+
+
+ def test_makeDirectory(self):
+ """
+ L{ftp.FTPClient.makeDirectory} issues a I{MKD} command and returns a
+ L{Deferred} which is called back with the server's response if the
+ directory is created.
+ """
+ self._testLogin()
+
+ d = self.client.makeDirectory("/spam")
+ self.assertEqual(self.transport.value(), 'MKD /spam\r\n')
+ self.client.lineReceived('257 "/spam" created.')
+ return d.addCallback(self.assertEqual, ['257 "/spam" created.'])
+
+
+ def test_makeDirectoryPathEscape(self):
+ """
+ L{ftp.FTPClient.makeDirectory} escapes the path name it sends according
+ to U{http://cr.yp.to/ftp/filesystem.html}.
+ """
+ self._testLogin()
+ d = self.client.makeDirectory("/sp\nam")
+ self.assertEqual(self.transport.value(), 'MKD /sp\x00am\r\n')
+ # This is necessary to make the Deferred fire. The Deferred needs
+ # to fire so that tearDown doesn't cause it to errback and fail this
+ # or (more likely) a later test.
+ self.client.lineReceived('257 win')
+ return d
+
+
+ def test_failedMakeDirectory(self):
+ """
+ L{ftp.FTPClient.makeDirectory} returns a L{Deferred} which is errbacked
+ with L{CommandFailed} if the server returns an error response code.
+ """
+ self._testLogin()
+
+ d = self.client.makeDirectory("/spam")
+ self.assertEqual(self.transport.value(), 'MKD /spam\r\n')
+ self.client.lineReceived('550 PERMISSION DENIED')
+ return self.assertFailure(d, ftp.CommandFailed)
+
+
+ def test_getDirectory(self):
+ """
+ Test the getDirectory method.
+
+ L{ftp.FTPClient.getDirectory} should return a Deferred which fires with
+ the current directory on the server. It wraps PWD command.
+ """
+ def cbGet(res):
+ self.assertEqual(res, "/bar/baz")
+
+ self._testLogin()
+ d = self.client.getDirectory().addCallback(cbGet)
+ self.assertEqual(self.transport.value(), 'PWD\r\n')
+ self.client.lineReceived('257 "/bar/baz"')
+ return d
+
+
+ def test_failedGetDirectory(self):
+ """
+ Test a failure in getDirectory method.
+
+ The behaviour should be the same as PWD.
+ """
+ self._testLogin()
+ d = self.client.getDirectory()
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PWD\r\n')
+ self.client.lineReceived('550 /bar/baz: No such file or directory')
+ return d
+
+
+ def test_anotherFailedGetDirectory(self):
+ """
+ Test a different failure in getDirectory method.
+
+ The response should be quoted to be parsed, so it returns an error
+ otherwise.
+ """
+ self._testLogin()
+ d = self.client.getDirectory()
+ self.assertFailure(d, ftp.CommandFailed)
+ self.assertEqual(self.transport.value(), 'PWD\r\n')
+ self.client.lineReceived('257 /bar/baz')
+ return d
+
+
+ def test_removeFile(self):
+ """
+ L{ftp.FTPClient.removeFile} sends a I{DELE} command to the server for
+ the indicated file and returns a Deferred which fires after the server
+ sends a 250 response code.
+ """
+ self._testLogin()
+ d = self.client.removeFile("/tmp/test")
+ self.assertEqual(self.transport.value(), 'DELE /tmp/test\r\n')
+ response = '250 Requested file action okay, completed.'
+ self.client.lineReceived(response)
+ return d.addCallback(self.assertEqual, [response])
+
+
+ def test_failedRemoveFile(self):
+ """
+ If the server returns a response code other than 250 in response to a
+ I{DELE} sent by L{ftp.FTPClient.removeFile}, the L{Deferred} returned
+ by C{removeFile} is errbacked with a L{Failure} wrapping a
+ L{CommandFailed}.
+ """
+ self._testLogin()
+ d = self.client.removeFile("/tmp/test")
+ self.assertEqual(self.transport.value(), 'DELE /tmp/test\r\n')
+ response = '501 Syntax error in parameters or arguments.'
+ self.client.lineReceived(response)
+ d = self.assertFailure(d, ftp.CommandFailed)
+ d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
+ return d
+
+
+ def test_unparsableRemoveFileResponse(self):
+ """
+ If the server returns a response line which cannot be parsed, the
+ L{Deferred} returned by L{ftp.FTPClient.removeFile} is errbacked with a
+ L{BadResponse} containing the response.
+ """
+ self._testLogin()
+ d = self.client.removeFile("/tmp/test")
+ response = '765 blah blah blah'
+ self.client.lineReceived(response)
+ d = self.assertFailure(d, ftp.BadResponse)
+ d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
+ return d
+
+
+ def test_multilineRemoveFileResponse(self):
+ """
+ If the server returns multiple response lines, the L{Deferred} returned
+ by L{ftp.FTPClient.removeFile} is still fired with a true value if the
+ ultimate response code is 250.
+ """
+ self._testLogin()
+ d = self.client.removeFile("/tmp/test")
+ response = ['250-perhaps a progress report',
+ '250 okay']
+ map(self.client.lineReceived, response)
+ return d.addCallback(self.assertTrue)
+
+
+ def test_removeDirectory(self):
+ """
+ L{ftp.FTPClient.removeDirectory} sends a I{RMD} command to the server
+ for the indicated directory and returns a Deferred which fires after
+ the server sends a 250 response code.
+ """
+ self._testLogin()
+ d = self.client.removeDirectory('/tmp/test')
+ self.assertEqual(self.transport.value(), 'RMD /tmp/test\r\n')
+ response = '250 Requested file action okay, completed.'
+ self.client.lineReceived(response)
+ return d.addCallback(self.assertEqual, [response])
+
+
+ def test_failedRemoveDirectory(self):
+ """
+ If the server returns a response code other than 250 in response to a
+ I{RMD} sent by L{ftp.FTPClient.removeDirectory}, the L{Deferred}
+ returned by C{removeDirectory} is errbacked with a L{Failure} wrapping
+ a L{CommandFailed}.
+ """
+ self._testLogin()
+ d = self.client.removeDirectory("/tmp/test")
+ self.assertEqual(self.transport.value(), 'RMD /tmp/test\r\n')
+ response = '501 Syntax error in parameters or arguments.'
+ self.client.lineReceived(response)
+ d = self.assertFailure(d, ftp.CommandFailed)
+ d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
+ return d
+
+
+ def test_unparsableRemoveDirectoryResponse(self):
+ """
+ If the server returns a response line which cannot be parsed, the
+ L{Deferred} returned by L{ftp.FTPClient.removeDirectory} is errbacked
+ with a L{BadResponse} containing the response.
+ """
+ self._testLogin()
+ d = self.client.removeDirectory("/tmp/test")
+ response = '765 blah blah blah'
+ self.client.lineReceived(response)
+ d = self.assertFailure(d, ftp.BadResponse)
+ d.addCallback(lambda exc: self.assertEqual(exc.args, ([response],)))
+ return d
+
+
+ def test_multilineRemoveDirectoryResponse(self):
+ """
+ If the server returns multiple response lines, the L{Deferred} returned
+ by L{ftp.FTPClient.removeDirectory} is still fired with a true value
+ if the ultimate response code is 250.
+ """
+ self._testLogin()
+ d = self.client.removeDirectory("/tmp/test")
+ response = ['250-perhaps a progress report',
+ '250 okay']
+ map(self.client.lineReceived, response)
+ return d.addCallback(self.assertTrue)
+
+
+
+class FTPClientBasicTests(unittest.TestCase):
+
+ def testGreeting(self):
+ # The first response is captured as a greeting.
+ ftpClient = ftp.FTPClientBasic()
+ ftpClient.lineReceived('220 Imaginary FTP.')
+ self.assertEqual(['220 Imaginary FTP.'], ftpClient.greeting)
+
+ def testResponseWithNoMessage(self):
+ # Responses with no message are still valid, i.e. three digits followed
+ # by a space is complete response.
+ ftpClient = ftp.FTPClientBasic()
+ ftpClient.lineReceived('220 ')
+ self.assertEqual(['220 '], ftpClient.greeting)
+
+ def testMultilineResponse(self):
+ ftpClient = ftp.FTPClientBasic()
+ ftpClient.transport = proto_helpers.StringTransport()
+ ftpClient.lineReceived('220 Imaginary FTP.')
+
+ # Queue (and send) a dummy command, and set up a callback to capture the
+ # result
+ deferred = ftpClient.queueStringCommand('BLAH')
+ result = []
+ deferred.addCallback(result.append)
+ deferred.addErrback(self.fail)
+
+ # Send the first line of a multiline response.
+ ftpClient.lineReceived('210-First line.')
+ self.assertEqual([], result)
+
+ # Send a second line, again prefixed with "nnn-".
+ ftpClient.lineReceived('123-Second line.')
+ self.assertEqual([], result)
+
+ # Send a plain line of text, no prefix.
+ ftpClient.lineReceived('Just some text.')
+ self.assertEqual([], result)
+
+ # Now send a short (less than 4 chars) line.
+ ftpClient.lineReceived('Hi')
+ self.assertEqual([], result)
+
+ # Now send an empty line.
+ ftpClient.lineReceived('')
+ self.assertEqual([], result)
+
+ # And a line with 3 digits in it, and nothing else.
+ ftpClient.lineReceived('321')
+ self.assertEqual([], result)
+
+ # Now finish it.
+ ftpClient.lineReceived('210 Done.')
+ self.assertEqual(
+ ['210-First line.',
+ '123-Second line.',
+ 'Just some text.',
+ 'Hi',
+ '',
+ '321',
+ '210 Done.'], result[0])
+
+
+ def test_noPasswordGiven(self):
+ """
+ Passing None as the password avoids sending the PASS command.
+ """
+ # Create a client, and give it a greeting.
+ ftpClient = ftp.FTPClientBasic()
+ ftpClient.transport = proto_helpers.StringTransport()
+ ftpClient.lineReceived('220 Welcome to Imaginary FTP.')
+
+ # Queue a login with no password
+ ftpClient.queueLogin('bob', None)
+ self.assertEqual('USER bob\r\n', ftpClient.transport.value())
+
+ # Clear the test buffer, acknowledge the USER command.
+ ftpClient.transport.clear()
+ ftpClient.lineReceived('200 Hello bob.')
+
+ # The client shouldn't have sent anything more (i.e. it shouldn't have
+ # sent a PASS command).
+ self.assertEqual('', ftpClient.transport.value())
+
+
+ def test_noPasswordNeeded(self):
+ """
+ Receiving a 230 response to USER prevents PASS from being sent.
+ """
+ # Create a client, and give it a greeting.
+ ftpClient = ftp.FTPClientBasic()
+ ftpClient.transport = proto_helpers.StringTransport()
+ ftpClient.lineReceived('220 Welcome to Imaginary FTP.')
+
+ # Queue a login with no password
+ ftpClient.queueLogin('bob', 'secret')
+ self.assertEqual('USER bob\r\n', ftpClient.transport.value())
+
+ # Clear the test buffer, acknowledge the USER command with a 230
+ # response code.
+ ftpClient.transport.clear()
+ ftpClient.lineReceived('230 Hello bob. No password needed.')
+
+ # The client shouldn't have sent anything more (i.e. it shouldn't have
+ # sent a PASS command).
+ self.assertEqual('', ftpClient.transport.value())
+
+
+
+class PathHandling(unittest.TestCase):
+ def testNormalizer(self):
+ for inp, outp in [('a', ['a']),
+ ('/a', ['a']),
+ ('/', []),
+ ('a/b/c', ['a', 'b', 'c']),
+ ('/a/b/c', ['a', 'b', 'c']),
+ ('/a/', ['a']),
+ ('a/', ['a'])]:
+ self.assertEqual(ftp.toSegments([], inp), outp)
+
+ for inp, outp in [('b', ['a', 'b']),
+ ('b/', ['a', 'b']),
+ ('/b', ['b']),
+ ('/b/', ['b']),
+ ('b/c', ['a', 'b', 'c']),
+ ('b/c/', ['a', 'b', 'c']),
+ ('/b/c', ['b', 'c']),
+ ('/b/c/', ['b', 'c'])]:
+ self.assertEqual(ftp.toSegments(['a'], inp), outp)
+
+ for inp, outp in [('//', []),
+ ('//a', ['a']),
+ ('a//', ['a']),
+ ('a//b', ['a', 'b'])]:
+ self.assertEqual(ftp.toSegments([], inp), outp)
+
+ for inp, outp in [('//', []),
+ ('//b', ['b']),
+ ('b//c', ['a', 'b', 'c'])]:
+ self.assertEqual(ftp.toSegments(['a'], inp), outp)
+
+ for inp, outp in [('..', []),
+ ('../', []),
+ ('a/..', ['x']),
+ ('/a/..', []),
+ ('/a/b/..', ['a']),
+ ('/a/b/../', ['a']),
+ ('/a/b/../c', ['a', 'c']),
+ ('/a/b/../c/', ['a', 'c']),
+ ('/a/b/../../c', ['c']),
+ ('/a/b/../../c/', ['c']),
+ ('/a/b/../../c/..', []),
+ ('/a/b/../../c/../', [])]:
+ self.assertEqual(ftp.toSegments(['x'], inp), outp)
+
+ for inp in ['..', '../', 'a/../..', 'a/../../',
+ '/..', '/../', '/a/../..', '/a/../../',
+ '/a/b/../../..']:
+ self.assertRaises(ftp.InvalidPath, ftp.toSegments, [], inp)
+
+ for inp in ['../..', '../../', '../a/../..']:
+ self.assertRaises(ftp.InvalidPath, ftp.toSegments, ['x'], inp)
+
+
+class BaseFTPRealmTests(unittest.TestCase):
+ """
+ Tests for L{ftp.BaseFTPRealm}, a base class to help define L{IFTPShell}
+ realms with different user home directory policies.
+ """
+ def test_interface(self):
+ """
+ L{ftp.BaseFTPRealm} implements L{IRealm}.
+ """
+ self.assertTrue(verifyClass(IRealm, ftp.BaseFTPRealm))
+
+
+ def test_getHomeDirectory(self):
+ """
+ L{ftp.BaseFTPRealm} calls its C{getHomeDirectory} method with the
+ avatarId being requested to determine the home directory for that
+ avatar.
+ """
+ result = filepath.FilePath(self.mktemp())
+ avatars = []
+ class TestRealm(ftp.BaseFTPRealm):
+ def getHomeDirectory(self, avatarId):
+ avatars.append(avatarId)
+ return result
+
+ realm = TestRealm(self.mktemp())
+ iface, avatar, logout = realm.requestAvatar(
+ "alice@example.com", None, ftp.IFTPShell)
+ self.assertIsInstance(avatar, ftp.FTPShell)
+ self.assertEqual(avatar.filesystemRoot, result)
+
+
+ def test_anonymous(self):
+ """
+ L{ftp.BaseFTPRealm} returns an L{ftp.FTPAnonymousShell} instance for
+ anonymous avatar requests.
+ """
+ anonymous = self.mktemp()
+ realm = ftp.BaseFTPRealm(anonymous)
+ iface, avatar, logout = realm.requestAvatar(
+ checkers.ANONYMOUS, None, ftp.IFTPShell)
+ self.assertIsInstance(avatar, ftp.FTPAnonymousShell)
+ self.assertEqual(avatar.filesystemRoot, filepath.FilePath(anonymous))
+
+
+ def test_notImplemented(self):
+ """
+ L{ftp.BaseFTPRealm.getHomeDirectory} should be overridden by a subclass
+ and raises L{NotImplementedError} if it is not.
+ """
+ realm = ftp.BaseFTPRealm(self.mktemp())
+ self.assertRaises(NotImplementedError, realm.getHomeDirectory, object())
+
+
+
+class FTPRealmTestCase(unittest.TestCase):
+ """
+ Tests for L{ftp.FTPRealm}.
+ """
+ def test_getHomeDirectory(self):
+ """
+ L{ftp.FTPRealm} accepts an extra directory to its initializer and treats
+ the avatarId passed to L{ftp.FTPRealm.getHomeDirectory} as a single path
+ segment to construct a child of that directory.
+ """
+ base = '/path/to/home'
+ realm = ftp.FTPRealm(self.mktemp(), base)
+ home = realm.getHomeDirectory('alice@example.com')
+ self.assertEqual(
+ filepath.FilePath(base).child('alice@example.com'), home)
+
+
+ def test_defaultHomeDirectory(self):
+ """
+ If no extra directory is passed to L{ftp.FTPRealm}, it uses C{"/home"}
+ as the base directory containing all user home directories.
+ """
+ realm = ftp.FTPRealm(self.mktemp())
+ home = realm.getHomeDirectory('alice@example.com')
+ self.assertEqual(filepath.FilePath('/home/alice@example.com'), home)
+
+
+
+class SystemFTPRealmTests(unittest.TestCase):
+ """
+ Tests for L{ftp.SystemFTPRealm}.
+ """
+ skip = nonPOSIXSkip
+
+ def test_getHomeDirectory(self):
+ """
+ L{ftp.SystemFTPRealm.getHomeDirectory} treats the avatarId passed to it
+ as a username in the underlying platform and returns that account's home
+ directory.
+ """
+ # Try to pick a username that will have a home directory.
+ user = getpass.getuser()
+
+ # Try to find their home directory in a different way than used by the
+ # implementation. Maybe this is silly and can only introduce spurious
+ # failures due to system-specific configurations.
+ import pwd
+ expected = pwd.getpwnam(user).pw_dir
+
+ realm = ftp.SystemFTPRealm(self.mktemp())
+ home = realm.getHomeDirectory(user)
+ self.assertEqual(home, filepath.FilePath(expected))
+
+
+ def test_noSuchUser(self):
+ """
+ L{ftp.SystemFTPRealm.getHomeDirectory} raises L{UnauthorizedLogin} when
+ passed a username which has no corresponding home directory in the
+ system's accounts database.
+ """
+ user = insecureRandom(4).encode('hex')
+ realm = ftp.SystemFTPRealm(self.mktemp())
+ self.assertRaises(UnauthorizedLogin, realm.getHomeDirectory, user)
+
+
+
+class ErrnoToFailureTestCase(unittest.TestCase):
+ """
+ Tests for L{ftp.errnoToFailure} errno checking.
+ """
+
+ def test_notFound(self):
+ """
+ C{errno.ENOENT} should be translated to L{ftp.FileNotFoundError}.
+ """
+ d = ftp.errnoToFailure(errno.ENOENT, "foo")
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_permissionDenied(self):
+ """
+ C{errno.EPERM} should be translated to L{ftp.PermissionDeniedError}.
+ """
+ d = ftp.errnoToFailure(errno.EPERM, "foo")
+ return self.assertFailure(d, ftp.PermissionDeniedError)
+
+
+ def test_accessDenied(self):
+ """
+ C{errno.EACCES} should be translated to L{ftp.PermissionDeniedError}.
+ """
+ d = ftp.errnoToFailure(errno.EACCES, "foo")
+ return self.assertFailure(d, ftp.PermissionDeniedError)
+
+
+ def test_notDirectory(self):
+ """
+ C{errno.ENOTDIR} should be translated to L{ftp.IsNotADirectoryError}.
+ """
+ d = ftp.errnoToFailure(errno.ENOTDIR, "foo")
+ return self.assertFailure(d, ftp.IsNotADirectoryError)
+
+
+ def test_fileExists(self):
+ """
+ C{errno.EEXIST} should be translated to L{ftp.FileExistsError}.
+ """
+ d = ftp.errnoToFailure(errno.EEXIST, "foo")
+ return self.assertFailure(d, ftp.FileExistsError)
+
+
+ def test_isDirectory(self):
+ """
+ C{errno.EISDIR} should be translated to L{ftp.IsADirectoryError}.
+ """
+ d = ftp.errnoToFailure(errno.EISDIR, "foo")
+ return self.assertFailure(d, ftp.IsADirectoryError)
+
+
+ def test_passThrough(self):
+ """
+ If an unknown errno is passed to L{ftp.errnoToFailure}, it should let
+ the originating exception pass through.
+ """
+ try:
+ raise RuntimeError("bar")
+ except:
+ d = ftp.errnoToFailure(-1, "foo")
+ return self.assertFailure(d, RuntimeError)
+
+
+
+class AnonymousFTPShellTestCase(unittest.TestCase):
+ """
+ Test anynomous shell properties.
+ """
+
+ def test_anonymousWrite(self):
+ """
+ Check that L{ftp.FTPAnonymousShell} returns an error when trying to
+ open it in write mode.
+ """
+ shell = ftp.FTPAnonymousShell('')
+ d = shell.openForWriting(('foo',))
+ self.assertFailure(d, ftp.PermissionDeniedError)
+ return d
+
+
+
+class IFTPShellTestsMixin:
+ """
+ Generic tests for the C{IFTPShell} interface.
+ """
+
+ def directoryExists(self, path):
+ """
+ Test if the directory exists at C{path}.
+
+ @param path: the relative path to check.
+ @type path: C{str}.
+
+ @return: C{True} if C{path} exists and is a directory, C{False} if
+ it's not the case
+ @rtype: C{bool}
+ """
+ raise NotImplementedError()
+
+
+ def createDirectory(self, path):
+ """
+ Create a directory in C{path}.
+
+ @param path: the relative path of the directory to create, with one
+ segment.
+ @type path: C{str}
+ """
+ raise NotImplementedError()
+
+
+ def fileExists(self, path):
+ """
+ Test if the file exists at C{path}.
+
+ @param path: the relative path to check.
+ @type path: C{str}.
+
+ @return: C{True} if C{path} exists and is a file, C{False} if it's not
+ the case.
+ @rtype: C{bool}
+ """
+ raise NotImplementedError()
+
+
+ def createFile(self, path, fileContent=''):
+ """
+ Create a file named C{path} with some content.
+
+ @param path: the relative path of the file to create, without
+ directory.
+ @type path: C{str}
+
+ @param fileContent: the content of the file.
+ @type fileContent: C{str}
+ """
+ raise NotImplementedError()
+
+
+ def test_createDirectory(self):
+ """
+ C{directoryExists} should report correctly about directory existence,
+ and C{createDirectory} should create a directory detectable by
+ C{directoryExists}.
+ """
+ self.assertFalse(self.directoryExists('bar'))
+ self.createDirectory('bar')
+ self.assertTrue(self.directoryExists('bar'))
+
+
+ def test_createFile(self):
+ """
+ C{fileExists} should report correctly about file existence, and
+ C{createFile} should create a file detectable by C{fileExists}.
+ """
+ self.assertFalse(self.fileExists('file.txt'))
+ self.createFile('file.txt')
+ self.assertTrue(self.fileExists('file.txt'))
+
+
+ def test_makeDirectory(self):
+ """
+ Create a directory and check it ends in the filesystem.
+ """
+ d = self.shell.makeDirectory(('foo',))
+ def cb(result):
+ self.assertTrue(self.directoryExists('foo'))
+ return d.addCallback(cb)
+
+
+ def test_makeDirectoryError(self):
+ """
+ Creating a directory that already exists should fail with a
+ C{ftp.FileExistsError}.
+ """
+ self.createDirectory('foo')
+ d = self.shell.makeDirectory(('foo',))
+ return self.assertFailure(d, ftp.FileExistsError)
+
+
+ def test_removeDirectory(self):
+ """
+ Try to remove a directory and check it's removed from the filesystem.
+ """
+ self.createDirectory('bar')
+ d = self.shell.removeDirectory(('bar',))
+ def cb(result):
+ self.assertFalse(self.directoryExists('bar'))
+ return d.addCallback(cb)
+
+
+ def test_removeDirectoryOnFile(self):
+ """
+ removeDirectory should not work in file and fail with a
+ C{ftp.IsNotADirectoryError}.
+ """
+ self.createFile('file.txt')
+ d = self.shell.removeDirectory(('file.txt',))
+ return self.assertFailure(d, ftp.IsNotADirectoryError)
+
+
+ def test_removeNotExistingDirectory(self):
+ """
+ Removing directory that doesn't exist should fail with a
+ C{ftp.FileNotFoundError}.
+ """
+ d = self.shell.removeDirectory(('bar',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_removeFile(self):
+ """
+ Try to remove a file and check it's removed from the filesystem.
+ """
+ self.createFile('file.txt')
+ d = self.shell.removeFile(('file.txt',))
+ def cb(res):
+ self.assertFalse(self.fileExists('file.txt'))
+ d.addCallback(cb)
+ return d
+
+
+ def test_removeFileOnDirectory(self):
+ """
+ removeFile should not work on directory.
+ """
+ self.createDirectory('ned')
+ d = self.shell.removeFile(('ned',))
+ return self.assertFailure(d, ftp.IsADirectoryError)
+
+
+ def test_removeNotExistingFile(self):
+ """
+ Try to remove a non existent file, and check it raises a
+ L{ftp.FileNotFoundError}.
+ """
+ d = self.shell.removeFile(('foo',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_list(self):
+ """
+ Check the output of the list method.
+ """
+ self.createDirectory('ned')
+ self.createFile('file.txt')
+ d = self.shell.list(('.',))
+ def cb(l):
+ l.sort()
+ self.assertEqual(l,
+ [('file.txt', []), ('ned', [])])
+ return d.addCallback(cb)
+
+
+ def test_listWithStat(self):
+ """
+ Check the output of list with asked stats.
+ """
+ self.createDirectory('ned')
+ self.createFile('file.txt')
+ d = self.shell.list(('.',), ('size', 'permissions',))
+ def cb(l):
+ l.sort()
+ self.assertEqual(len(l), 2)
+ self.assertEqual(l[0][0], 'file.txt')
+ self.assertEqual(l[1][0], 'ned')
+ # Size and permissions are reported differently between platforms
+ # so just check they are present
+ self.assertEqual(len(l[0][1]), 2)
+ self.assertEqual(len(l[1][1]), 2)
+ return d.addCallback(cb)
+
+
+ def test_listWithInvalidStat(self):
+ """
+ Querying an invalid stat should result to a C{AttributeError}.
+ """
+ self.createDirectory('ned')
+ d = self.shell.list(('.',), ('size', 'whateverstat',))
+ return self.assertFailure(d, AttributeError)
+
+
+ def test_listFile(self):
+ """
+ Check the output of the list method on a file.
+ """
+ self.createFile('file.txt')
+ d = self.shell.list(('file.txt',))
+ def cb(l):
+ l.sort()
+ self.assertEqual(l,
+ [('file.txt', [])])
+ return d.addCallback(cb)
+
+
+ def test_listNotExistingDirectory(self):
+ """
+ list on a directory that doesn't exist should fail with a
+ L{ftp.FileNotFoundError}.
+ """
+ d = self.shell.list(('foo',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_access(self):
+ """
+ Try to access a resource.
+ """
+ self.createDirectory('ned')
+ d = self.shell.access(('ned',))
+ return d
+
+
+ def test_accessNotFound(self):
+ """
+ access should fail on a resource that doesn't exist.
+ """
+ d = self.shell.access(('foo',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_openForReading(self):
+ """
+ Check that openForReading returns an object providing C{ftp.IReadFile}.
+ """
+ self.createFile('file.txt')
+ d = self.shell.openForReading(('file.txt',))
+ def cb(res):
+ self.assertTrue(ftp.IReadFile.providedBy(res))
+ d.addCallback(cb)
+ return d
+
+
+ def test_openForReadingNotFound(self):
+ """
+ openForReading should fail with a C{ftp.FileNotFoundError} on a file
+ that doesn't exist.
+ """
+ d = self.shell.openForReading(('ned',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_openForReadingOnDirectory(self):
+ """
+ openForReading should not work on directory.
+ """
+ self.createDirectory('ned')
+ d = self.shell.openForReading(('ned',))
+ return self.assertFailure(d, ftp.IsADirectoryError)
+
+
+ def test_openForWriting(self):
+ """
+ Check that openForWriting returns an object providing C{ftp.IWriteFile}.
+ """
+ d = self.shell.openForWriting(('foo',))
+ def cb1(res):
+ self.assertTrue(ftp.IWriteFile.providedBy(res))
+ return res.receive().addCallback(cb2)
+ def cb2(res):
+ self.assertTrue(IConsumer.providedBy(res))
+ d.addCallback(cb1)
+ return d
+
+
+ def test_openForWritingExistingDirectory(self):
+ """
+ openForWriting should not be able to open a directory that already
+ exists.
+ """
+ self.createDirectory('ned')
+ d = self.shell.openForWriting(('ned',))
+ return self.assertFailure(d, ftp.IsADirectoryError)
+
+
+ def test_openForWritingInNotExistingDirectory(self):
+ """
+ openForWring should fail with a L{ftp.FileNotFoundError} if you specify
+ a file in a directory that doesn't exist.
+ """
+ self.createDirectory('ned')
+ d = self.shell.openForWriting(('ned', 'idonotexist', 'foo'))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_statFile(self):
+ """
+ Check the output of the stat method on a file.
+ """
+ fileContent = 'wobble\n'
+ self.createFile('file.txt', fileContent)
+ d = self.shell.stat(('file.txt',), ('size', 'directory'))
+ def cb(res):
+ self.assertEqual(res[0], len(fileContent))
+ self.assertFalse(res[1])
+ d.addCallback(cb)
+ return d
+
+
+ def test_statDirectory(self):
+ """
+ Check the output of the stat method on a directory.
+ """
+ self.createDirectory('ned')
+ d = self.shell.stat(('ned',), ('size', 'directory'))
+ def cb(res):
+ self.assertTrue(res[1])
+ d.addCallback(cb)
+ return d
+
+
+ def test_statOwnerGroup(self):
+ """
+ Check the owner and groups stats.
+ """
+ self.createDirectory('ned')
+ d = self.shell.stat(('ned',), ('owner', 'group'))
+ def cb(res):
+ self.assertEqual(len(res), 2)
+ d.addCallback(cb)
+ return d
+
+
+ def test_statNotExisting(self):
+ """
+ stat should fail with L{ftp.FileNotFoundError} on a file that doesn't
+ exist.
+ """
+ d = self.shell.stat(('foo',), ('size', 'directory'))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+ def test_invalidStat(self):
+ """
+ Querying an invalid stat should result to a C{AttributeError}.
+ """
+ self.createDirectory('ned')
+ d = self.shell.stat(('ned',), ('size', 'whateverstat'))
+ return self.assertFailure(d, AttributeError)
+
+
+ def test_rename(self):
+ """
+ Try to rename a directory.
+ """
+ self.createDirectory('ned')
+ d = self.shell.rename(('ned',), ('foo',))
+ def cb(res):
+ self.assertTrue(self.directoryExists('foo'))
+ self.assertFalse(self.directoryExists('ned'))
+ return d.addCallback(cb)
+
+
+ def test_renameNotExisting(self):
+ """
+ Renaming a directory that doesn't exist should fail with
+ L{ftp.FileNotFoundError}.
+ """
+ d = self.shell.rename(('foo',), ('bar',))
+ return self.assertFailure(d, ftp.FileNotFoundError)
+
+
+
+class FTPShellTestCase(unittest.TestCase, IFTPShellTestsMixin):
+ """
+ Tests for the C{ftp.FTPShell} object.
+ """
+
+ def setUp(self):
+ """
+ Create a root directory and instantiate a shell.
+ """
+ self.root = filepath.FilePath(self.mktemp())
+ self.root.createDirectory()
+ self.shell = ftp.FTPShell(self.root)
+
+
+ def directoryExists(self, path):
+ """
+ Test if the directory exists at C{path}.
+ """
+ return self.root.child(path).isdir()
+
+
+ def createDirectory(self, path):
+ """
+ Create a directory in C{path}.
+ """
+ return self.root.child(path).createDirectory()
+
+
+ def fileExists(self, path):
+ """
+ Test if the file exists at C{path}.
+ """
+ return self.root.child(path).isfile()
+
+
+ def createFile(self, path, fileContent=''):
+ """
+ Create a file named C{path} with some content.
+ """
+ return self.root.child(path).setContent(fileContent)
+
+
+
+class TestConsumer(object):
+ """
+ A simple consumer for tests. It only works with non-streaming producers.
+
+ @ivar producer: an object providing
+ L{twisted.internet.interfaces.IPullProducer}.
+ """
+
+ implements(IConsumer)
+ producer = None
+
+ def registerProducer(self, producer, streaming):
+ """
+ Simple register of producer, checks that no register has happened
+ before.
+ """
+ assert self.producer is None
+ self.buffer = []
+ self.producer = producer
+ self.producer.resumeProducing()
+
+
+ def unregisterProducer(self):
+ """
+ Unregister the producer, it should be done after a register.
+ """
+ assert self.producer is not None
+ self.producer = None
+
+
+ def write(self, data):
+ """
+ Save the data received.
+ """
+ self.buffer.append(data)
+ self.producer.resumeProducing()
+
+
+
+class TestProducer(object):
+ """
+ A dumb producer.
+ """
+
+ def __init__(self, toProduce, consumer):
+ """
+ @param toProduce: data to write
+ @type toProduce: C{str}
+ @param consumer: the consumer of data.
+ @type consumer: C{IConsumer}
+ """
+ self.toProduce = toProduce
+ self.consumer = consumer
+
+
+ def start(self):
+ """
+ Send the data to consume.
+ """
+ self.consumer.write(self.toProduce)
+
+
+
+class IReadWriteTestsMixin:
+ """
+ Generic tests for the C{IReadFile} and C{IWriteFile} interfaces.
+ """
+
+ def getFileReader(self, content):
+ """
+ Return an object providing C{IReadFile}, ready to send data C{content}.
+ """
+ raise NotImplementedError()
+
+
+ def getFileWriter(self):
+ """
+ Return an object providing C{IWriteFile}, ready to receive data.
+ """
+ raise NotImplementedError()
+
+
+ def getFileContent(self):
+ """
+ Return the content of the file used.
+ """
+ raise NotImplementedError()
+
+
+ def test_read(self):
+ """
+ Test L{ftp.IReadFile}: the implementation should have a send method
+ returning a C{Deferred} which fires when all the data has been sent
+ to the consumer, and the data should be correctly send to the consumer.
+ """
+ content = 'wobble\n'
+ consumer = TestConsumer()
+ def cbGet(reader):
+ return reader.send(consumer).addCallback(cbSend)
+ def cbSend(res):
+ self.assertEqual("".join(consumer.buffer), content)
+ return self.getFileReader(content).addCallback(cbGet)
+
+
+ def test_write(self):
+ """
+ Test L{ftp.IWriteFile}: the implementation should have a receive
+ method returning a C{Deferred} which fires with a consumer ready to
+ receive data to be written. It should also have a close() method that
+ returns a Deferred.
+ """
+ content = 'elbbow\n'
+ def cbGet(writer):
+ return writer.receive().addCallback(cbReceive, writer)
+ def cbReceive(consumer, writer):
+ producer = TestProducer(content, consumer)
+ consumer.registerProducer(None, True)
+ producer.start()
+ consumer.unregisterProducer()
+ return writer.close().addCallback(cbClose)
+ def cbClose(ignored):
+ self.assertEqual(self.getFileContent(), content)
+ return self.getFileWriter().addCallback(cbGet)
+
+
+
+class FTPReadWriteTestCase(unittest.TestCase, IReadWriteTestsMixin):
+ """
+ Tests for C{ftp._FileReader} and C{ftp._FileWriter}, the objects returned
+ by the shell in C{openForReading}/C{openForWriting}.
+ """
+
+ def setUp(self):
+ """
+ Create a temporary file used later.
+ """
+ self.root = filepath.FilePath(self.mktemp())
+ self.root.createDirectory()
+ self.shell = ftp.FTPShell(self.root)
+ self.filename = "file.txt"
+
+
+ def getFileReader(self, content):
+ """
+ Return a C{ftp._FileReader} instance with a file opened for reading.
+ """
+ self.root.child(self.filename).setContent(content)
+ return self.shell.openForReading((self.filename,))
+
+
+ def getFileWriter(self):
+ """
+ Return a C{ftp._FileWriter} instance with a file opened for writing.
+ """
+ return self.shell.openForWriting((self.filename,))
+
+
+ def getFileContent(self):
+ """
+ Return the content of the temporary file.
+ """
+ return self.root.child(self.filename).getContent()
+
+
+class CloseTestWriter:
+ implements(ftp.IWriteFile)
+ closeStarted = False
+ def receive(self):
+ self.s = StringIO()
+ fc = ftp.FileConsumer(self.s)
+ return defer.succeed(fc)
+ def close(self):
+ self.closeStarted = True
+ return self.d
+
+class CloseTestShell:
+ def openForWriting(self, segs):
+ return defer.succeed(self.writer)
+
+class FTPCloseTest(unittest.TestCase):
+ """Tests that the server invokes IWriteFile.close"""
+
+ def test_write(self):
+ """Confirm that FTP uploads (i.e. ftp_STOR) correctly call and wait
+ upon the IWriteFile object's close() method"""
+ f = ftp.FTP()
+ f.workingDirectory = ["root"]
+ f.shell = CloseTestShell()
+ f.shell.writer = CloseTestWriter()
+ f.shell.writer.d = defer.Deferred()
+ f.factory = ftp.FTPFactory()
+ f.factory.timeOut = None
+ f.makeConnection(StringIO())
+
+ di = ftp.DTP()
+ di.factory = ftp.DTPFactory(f)
+ f.dtpInstance = di
+ di.makeConnection(None)#
+
+ stor_done = []
+ d = f.ftp_STOR("path")
+ d.addCallback(stor_done.append)
+ # the writer is still receiving data
+ self.assertFalse(f.shell.writer.closeStarted, "close() called early")
+ di.dataReceived("some data here")
+ self.assertFalse(f.shell.writer.closeStarted, "close() called early")
+ di.connectionLost("reason is ignored")
+ # now we should be waiting in close()
+ self.assertTrue(f.shell.writer.closeStarted, "close() not called")
+ self.assertFalse(stor_done)
+ f.shell.writer.d.callback("allow close() to finish")
+ self.assertTrue(stor_done)
+
+ return d # just in case an errback occurred
+
+
+
+class FTPResponseCodeTests(unittest.TestCase):
+ """
+ Tests relating directly to response codes.
+ """
+ def test_unique(self):
+ """
+ All of the response code globals (for example C{RESTART_MARKER_REPLY} or
+ C{USR_NAME_OK_NEED_PASS}) have unique values and are present in the
+ C{RESPONSE} dictionary.
+ """
+ allValues = set(ftp.RESPONSE)
+ seenValues = set()
+
+ for key, value in vars(ftp).items():
+ if isinstance(value, str) and key.isupper():
+ self.assertIn(
+ value, allValues,
+ "Code %r with value %r missing from RESPONSE dict" % (
+ key, value))
+ self.assertNotIn(
+ value, seenValues,
+ "Duplicate code %r with value %r" % (key, value))
+ seenValues.add(value)
+
diff --git a/twisted/test/test_ftp_options.py b/twisted/test/test_ftp_options.py
new file mode 100644
index 0000000..e668502
--- /dev/null
+++ b/twisted/test/test_ftp_options.py
@@ -0,0 +1,80 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.tap.ftp}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.cred import credentials, error
+from twisted.tap.ftp import Options
+from twisted.python import versions
+from twisted.python.filepath import FilePath
+
+
+
+class FTPOptionsTestCase(TestCase):
+ """
+ Tests for the command line option parser used for C{twistd ftp}.
+ """
+
+ usernamePassword = ('iamuser', 'thisispassword')
+
+ def setUp(self):
+ """
+ Create a file with two users.
+ """
+ self.filename = self.mktemp()
+ f = FilePath(self.filename)
+ f.setContent(':'.join(self.usernamePassword))
+ self.options = Options()
+
+
+ def test_passwordfileDeprecation(self):
+ """
+ The C{--password-file} option will emit a warning stating that
+ said option is deprecated.
+ """
+ self.callDeprecated(
+ versions.Version("Twisted", 11, 1, 0),
+ self.options.opt_password_file, self.filename)
+
+
+ def test_authAdded(self):
+ """
+ The C{--auth} command-line option will add a checker to the list of
+ checkers
+ """
+ numCheckers = len(self.options['credCheckers'])
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ self.assertEqual(len(self.options['credCheckers']), numCheckers + 1)
+
+
+ def test_authFailure(self):
+ """
+ The checker created by the C{--auth} command-line option returns a
+ L{Deferred} that fails with L{UnauthorizedLogin} when
+ presented with credentials that are unknown to that checker.
+ """
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ checker = self.options['credCheckers'][-1]
+ invalid = credentials.UsernamePassword(self.usernamePassword[0], 'fake')
+ return (checker.requestAvatarId(invalid)
+ .addCallbacks(
+ lambda ignore: self.fail("Wrong password should raise error"),
+ lambda err: err.trap(error.UnauthorizedLogin)))
+
+
+ def test_authSuccess(self):
+ """
+ The checker created by the C{--auth} command-line option returns a
+ L{Deferred} that returns the avatar id when presented with credentials
+ that are known to that checker.
+ """
+ self.options.parseOptions(['--auth', 'file:' + self.filename])
+ checker = self.options['credCheckers'][-1]
+ correct = credentials.UsernamePassword(*self.usernamePassword)
+ return checker.requestAvatarId(correct).addCallback(
+ lambda username: self.assertEqual(username, correct.username)
+ )
diff --git a/twisted/test/test_hook.py b/twisted/test/test_hook.py
new file mode 100644
index 0000000..7d17f76
--- /dev/null
+++ b/twisted/test/test_hook.py
@@ -0,0 +1,150 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for twisted.hook module.
+"""
+
+from twisted.python import hook
+from twisted.trial import unittest
+
+class BaseClass:
+ """
+ dummy class to help in testing.
+ """
+ def __init__(self):
+ """
+ dummy initializer
+ """
+ self.calledBasePre = 0
+ self.calledBasePost = 0
+ self.calledBase = 0
+
+ def func(self, a, b):
+ """
+ dummy method
+ """
+ assert a == 1
+ assert b == 2
+ self.calledBase = self.calledBase + 1
+
+
+class SubClass(BaseClass):
+ """
+ another dummy class
+ """
+ def __init__(self):
+ """
+ another dummy initializer
+ """
+ BaseClass.__init__(self)
+ self.calledSubPre = 0
+ self.calledSubPost = 0
+ self.calledSub = 0
+
+ def func(self, a, b):
+ """
+ another dummy function
+ """
+ assert a == 1
+ assert b == 2
+ BaseClass.func(self, a, b)
+ self.calledSub = self.calledSub + 1
+
+_clean_BaseClass = BaseClass.__dict__.copy()
+_clean_SubClass = SubClass.__dict__.copy()
+
+def basePre(base, a, b):
+ """
+ a pre-hook for the base class
+ """
+ base.calledBasePre = base.calledBasePre + 1
+
+def basePost(base, a, b):
+ """
+ a post-hook for the base class
+ """
+ base.calledBasePost = base.calledBasePost + 1
+
+def subPre(sub, a, b):
+ """
+ a pre-hook for the subclass
+ """
+ sub.calledSubPre = sub.calledSubPre + 1
+
+def subPost(sub, a, b):
+ """
+ a post-hook for the subclass
+ """
+ sub.calledSubPost = sub.calledSubPost + 1
+
+class HookTestCase(unittest.TestCase):
+ """
+ test case to make sure hooks are called
+ """
+ def setUp(self):
+ """Make sure we have clean versions of our classes."""
+ BaseClass.__dict__.clear()
+ BaseClass.__dict__.update(_clean_BaseClass)
+ SubClass.__dict__.clear()
+ SubClass.__dict__.update(_clean_SubClass)
+
+ def testBaseHook(self):
+ """make sure that the base class's hook is called reliably
+ """
+ base = BaseClass()
+ self.assertEqual(base.calledBase, 0)
+ self.assertEqual(base.calledBasePre, 0)
+ base.func(1,2)
+ self.assertEqual(base.calledBase, 1)
+ self.assertEqual(base.calledBasePre, 0)
+ hook.addPre(BaseClass, "func", basePre)
+ base.func(1, b=2)
+ self.assertEqual(base.calledBase, 2)
+ self.assertEqual(base.calledBasePre, 1)
+ hook.addPost(BaseClass, "func", basePost)
+ base.func(1, b=2)
+ self.assertEqual(base.calledBasePost, 1)
+ self.assertEqual(base.calledBase, 3)
+ self.assertEqual(base.calledBasePre, 2)
+ hook.removePre(BaseClass, "func", basePre)
+ hook.removePost(BaseClass, "func", basePost)
+ base.func(1, b=2)
+ self.assertEqual(base.calledBasePost, 1)
+ self.assertEqual(base.calledBase, 4)
+ self.assertEqual(base.calledBasePre, 2)
+
+ def testSubHook(self):
+ """test interactions between base-class hooks and subclass hooks
+ """
+ sub = SubClass()
+ self.assertEqual(sub.calledSub, 0)
+ self.assertEqual(sub.calledBase, 0)
+ sub.func(1, b=2)
+ self.assertEqual(sub.calledSub, 1)
+ self.assertEqual(sub.calledBase, 1)
+ hook.addPre(SubClass, 'func', subPre)
+ self.assertEqual(sub.calledSub, 1)
+ self.assertEqual(sub.calledBase, 1)
+ self.assertEqual(sub.calledSubPre, 0)
+ self.assertEqual(sub.calledBasePre, 0)
+ sub.func(1, b=2)
+ self.assertEqual(sub.calledSub, 2)
+ self.assertEqual(sub.calledBase, 2)
+ self.assertEqual(sub.calledSubPre, 1)
+ self.assertEqual(sub.calledBasePre, 0)
+ # let the pain begin
+ hook.addPre(BaseClass, 'func', basePre)
+ BaseClass.func(sub, 1, b=2)
+ # sub.func(1, b=2)
+ self.assertEqual(sub.calledBase, 3)
+ self.assertEqual(sub.calledBasePre, 1, str(sub.calledBasePre))
+ sub.func(1, b=2)
+ self.assertEqual(sub.calledBasePre, 2)
+ self.assertEqual(sub.calledBase, 4)
+ self.assertEqual(sub.calledSubPre, 2)
+ self.assertEqual(sub.calledSub, 3)
+
+testCases = [HookTestCase]
diff --git a/twisted/test/test_htb.py b/twisted/test/test_htb.py
new file mode 100644
index 0000000..ee4cc27
--- /dev/null
+++ b/twisted/test/test_htb.py
@@ -0,0 +1,109 @@
+# -*- Python -*-
+
+__version__ = '$Revision: 1.3 $'[11:-2]
+
+from twisted.trial import unittest
+from twisted.protocols import htb
+
+class DummyClock:
+ time = 0
+ def set(self, when):
+ self.time = when
+
+ def __call__(self):
+ return self.time
+
+class SomeBucket(htb.Bucket):
+ maxburst = 100
+ rate = 2
+
+class TestBucketBase(unittest.TestCase):
+ def setUp(self):
+ self._realTimeFunc = htb.time
+ self.clock = DummyClock()
+ htb.time = self.clock
+
+ def tearDown(self):
+ htb.time = self._realTimeFunc
+
+class TestBucket(TestBucketBase):
+ def testBucketSize(self):
+ """Testing the size of the bucket."""
+ b = SomeBucket()
+ fit = b.add(1000)
+ self.assertEqual(100, fit)
+
+ def testBucketDrain(self):
+ """Testing the bucket's drain rate."""
+ b = SomeBucket()
+ fit = b.add(1000)
+ self.clock.set(10)
+ fit = b.add(1000)
+ self.assertEqual(20, fit)
+
+ def test_bucketEmpty(self):
+ """
+ L{htb.Bucket.drip} returns C{True} if the bucket is empty after that drip.
+ """
+ b = SomeBucket()
+ b.add(20)
+ self.clock.set(9)
+ empty = b.drip()
+ self.assertFalse(empty)
+ self.clock.set(10)
+ empty = b.drip()
+ self.assertTrue(empty)
+
+class TestBucketNesting(TestBucketBase):
+ def setUp(self):
+ TestBucketBase.setUp(self)
+ self.parent = SomeBucket()
+ self.child1 = SomeBucket(self.parent)
+ self.child2 = SomeBucket(self.parent)
+
+ def testBucketParentSize(self):
+ # Use up most of the parent bucket.
+ self.child1.add(90)
+ fit = self.child2.add(90)
+ self.assertEqual(10, fit)
+
+ def testBucketParentRate(self):
+ # Make the parent bucket drain slower.
+ self.parent.rate = 1
+ # Fill both child1 and parent.
+ self.child1.add(100)
+ self.clock.set(10)
+ fit = self.child1.add(100)
+ # How much room was there? The child bucket would have had 20,
+ # but the parent bucket only ten (so no, it wouldn't make too much
+ # sense to have a child bucket draining faster than its parent in a real
+ # application.)
+ self.assertEqual(10, fit)
+
+
+# TODO: Test the Transport stuff?
+
+from test_pcp import DummyConsumer
+
+class ConsumerShaperTest(TestBucketBase):
+ def setUp(self):
+ TestBucketBase.setUp(self)
+ self.underlying = DummyConsumer()
+ self.bucket = SomeBucket()
+ self.shaped = htb.ShapedConsumer(self.underlying, self.bucket)
+
+ def testRate(self):
+ # Start off with a full bucket, so the burst-size dosen't factor in
+ # to the calculations.
+ delta_t = 10
+ self.bucket.add(100)
+ self.shaped.write("x" * 100)
+ self.clock.set(delta_t)
+ self.shaped.resumeProducing()
+ self.assertEqual(len(self.underlying.getvalue()),
+ delta_t * self.bucket.rate)
+
+ def testBucketRefs(self):
+ self.assertEqual(self.bucket._refcount, 1)
+ self.shaped.stopProducing()
+ self.assertEqual(self.bucket._refcount, 0)
diff --git a/twisted/test/test_ident.py b/twisted/test/test_ident.py
new file mode 100644
index 0000000..9f69322
--- /dev/null
+++ b/twisted/test/test_ident.py
@@ -0,0 +1,194 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for twisted.protocols.ident module.
+"""
+
+import struct
+
+from twisted.protocols import ident
+from twisted.python import failure
+from twisted.internet import error
+from twisted.internet import defer
+
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+
+
+
+class ClassParserTestCase(unittest.TestCase):
+ """
+ Test parsing of ident responses.
+ """
+
+ def setUp(self):
+ """
+ Create a ident client used in tests.
+ """
+ self.client = ident.IdentClient()
+
+
+ def test_indentError(self):
+ """
+ 'UNKNOWN-ERROR' error should map to the L{ident.IdentError} exception.
+ """
+ d = defer.Deferred()
+ self.client.queries.append((d, 123, 456))
+ self.client.lineReceived('123, 456 : ERROR : UNKNOWN-ERROR')
+ return self.assertFailure(d, ident.IdentError)
+
+
+ def test_noUSerError(self):
+ """
+ 'NO-USER' error should map to the L{ident.NoUser} exception.
+ """
+ d = defer.Deferred()
+ self.client.queries.append((d, 234, 456))
+ self.client.lineReceived('234, 456 : ERROR : NO-USER')
+ return self.assertFailure(d, ident.NoUser)
+
+
+ def test_invalidPortError(self):
+ """
+ 'INVALID-PORT' error should map to the L{ident.InvalidPort} exception.
+ """
+ d = defer.Deferred()
+ self.client.queries.append((d, 345, 567))
+ self.client.lineReceived('345, 567 : ERROR : INVALID-PORT')
+ return self.assertFailure(d, ident.InvalidPort)
+
+
+ def test_hiddenUserError(self):
+ """
+ 'HIDDEN-USER' error should map to the L{ident.HiddenUser} exception.
+ """
+ d = defer.Deferred()
+ self.client.queries.append((d, 567, 789))
+ self.client.lineReceived('567, 789 : ERROR : HIDDEN-USER')
+ return self.assertFailure(d, ident.HiddenUser)
+
+
+ def test_lostConnection(self):
+ """
+ A pending query which failed because of a ConnectionLost should
+ receive an L{ident.IdentError}.
+ """
+ d = defer.Deferred()
+ self.client.queries.append((d, 765, 432))
+ self.client.connectionLost(failure.Failure(error.ConnectionLost()))
+ return self.assertFailure(d, ident.IdentError)
+
+
+
+class TestIdentServer(ident.IdentServer):
+ def lookup(self, serverAddress, clientAddress):
+ return self.resultValue
+
+
+class TestErrorIdentServer(ident.IdentServer):
+ def lookup(self, serverAddress, clientAddress):
+ raise self.exceptionType()
+
+
+class NewException(RuntimeError):
+ pass
+
+
+class ServerParserTestCase(unittest.TestCase):
+ def testErrors(self):
+ p = TestErrorIdentServer()
+ p.makeConnection(StringTransport())
+ L = []
+ p.sendLine = L.append
+
+ p.exceptionType = ident.IdentError
+ p.lineReceived('123, 345')
+ self.assertEqual(L[0], '123, 345 : ERROR : UNKNOWN-ERROR')
+
+ p.exceptionType = ident.NoUser
+ p.lineReceived('432, 210')
+ self.assertEqual(L[1], '432, 210 : ERROR : NO-USER')
+
+ p.exceptionType = ident.InvalidPort
+ p.lineReceived('987, 654')
+ self.assertEqual(L[2], '987, 654 : ERROR : INVALID-PORT')
+
+ p.exceptionType = ident.HiddenUser
+ p.lineReceived('756, 827')
+ self.assertEqual(L[3], '756, 827 : ERROR : HIDDEN-USER')
+
+ p.exceptionType = NewException
+ p.lineReceived('987, 789')
+ self.assertEqual(L[4], '987, 789 : ERROR : UNKNOWN-ERROR')
+ errs = self.flushLoggedErrors(NewException)
+ self.assertEqual(len(errs), 1)
+
+ for port in -1, 0, 65536, 65537:
+ del L[:]
+ p.lineReceived('%d, 5' % (port,))
+ p.lineReceived('5, %d' % (port,))
+ self.assertEqual(
+ L, ['%d, 5 : ERROR : INVALID-PORT' % (port,),
+ '5, %d : ERROR : INVALID-PORT' % (port,)])
+
+ def testSuccess(self):
+ p = TestIdentServer()
+ p.makeConnection(StringTransport())
+ L = []
+ p.sendLine = L.append
+
+ p.resultValue = ('SYS', 'USER')
+ p.lineReceived('123, 456')
+ self.assertEqual(L[0], '123, 456 : USERID : SYS : USER')
+
+
+if struct.pack('=L', 1)[0] == '\x01':
+ _addr1 = '0100007F'
+ _addr2 = '04030201'
+else:
+ _addr1 = '7F000001'
+ _addr2 = '01020304'
+
+
+class ProcMixinTestCase(unittest.TestCase):
+ line = ('4: %s:0019 %s:02FA 0A 00000000:00000000 '
+ '00:00000000 00000000 0 0 10927 1 f72a5b80 '
+ '3000 0 0 2 -1') % (_addr1, _addr2)
+
+ def testDottedQuadFromHexString(self):
+ p = ident.ProcServerMixin()
+ self.assertEqual(p.dottedQuadFromHexString(_addr1), '127.0.0.1')
+
+ def testUnpackAddress(self):
+ p = ident.ProcServerMixin()
+ self.assertEqual(p.unpackAddress(_addr1 + ':0277'),
+ ('127.0.0.1', 631))
+
+ def testLineParser(self):
+ p = ident.ProcServerMixin()
+ self.assertEqual(
+ p.parseLine(self.line),
+ (('127.0.0.1', 25), ('1.2.3.4', 762), 0))
+
+ def testExistingAddress(self):
+ username = []
+ p = ident.ProcServerMixin()
+ p.entries = lambda: iter([self.line])
+ p.getUsername = lambda uid: (username.append(uid), 'root')[1]
+ self.assertEqual(
+ p.lookup(('127.0.0.1', 25), ('1.2.3.4', 762)),
+ (p.SYSTEM_NAME, 'root'))
+ self.assertEqual(username, [0])
+
+ def testNonExistingAddress(self):
+ p = ident.ProcServerMixin()
+ p.entries = lambda: iter([self.line])
+ self.assertRaises(ident.NoUser, p.lookup, ('127.0.0.1', 26),
+ ('1.2.3.4', 762))
+ self.assertRaises(ident.NoUser, p.lookup, ('127.0.0.1', 25),
+ ('1.2.3.5', 762))
+ self.assertRaises(ident.NoUser, p.lookup, ('127.0.0.1', 25),
+ ('1.2.3.4', 763))
+
diff --git a/twisted/test/test_import.py b/twisted/test/test_import.py
new file mode 100644
index 0000000..821b9bf
--- /dev/null
+++ b/twisted/test/test_import.py
@@ -0,0 +1,75 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.python.runtime import platformType
+
+
+class AtLeastImportTestCase(unittest.TestCase):
+ """
+ I test that there are no syntax errors which will not allow importing.
+ """
+
+ failureException = ImportError
+
+ def test_misc(self):
+ """
+ Test importing other miscellaneous modules.
+ """
+ from twisted import copyright
+
+ def test_persisted(self):
+ """
+ Test importing persisted.
+ """
+ from twisted.persisted import dirdbm
+ from twisted.persisted import styles
+
+ def test_internet(self):
+ """
+ Test importing internet.
+ """
+ from twisted.internet import tcp
+ from twisted.internet import main
+ from twisted.internet import abstract
+ from twisted.internet import udp
+ from twisted.internet import protocol
+ from twisted.internet import defer
+
+ def test_unix(self):
+ """
+ Test internet modules for unix.
+ """
+ from twisted.internet import stdio
+ from twisted.internet import process
+ from twisted.internet import unix
+
+ if platformType != "posix":
+ test_unix.skip = "UNIX-only modules"
+
+ def test_spread(self):
+ """
+ Test importing spreadables.
+ """
+ from twisted.spread import pb
+ from twisted.spread import jelly
+ from twisted.spread import banana
+ from twisted.spread import flavors
+
+ def test_twistedPython(self):
+ """
+ Test importing C{twisted.python}.
+ """
+ from twisted.python import hook
+ from twisted.python import log
+ from twisted.python import reflect
+ from twisted.python import usage
+
+ def test_protocols(self):
+ """
+ Test importing protocols.
+ """
+ from twisted.protocols import basic
+ from twisted.protocols import ftp
+ from twisted.protocols import telnet
+ from twisted.protocols import policies
diff --git a/twisted/test/test_internet.py b/twisted/test/test_internet.py
new file mode 100644
index 0000000..0e14bb4
--- /dev/null
+++ b/twisted/test/test_internet.py
@@ -0,0 +1,1396 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for lots of functionality provided by L{twisted.internet}.
+"""
+
+import os
+import sys
+import time
+
+from twisted.trial import unittest
+from twisted.internet import reactor, protocol, error, abstract, defer
+from twisted.internet import interfaces, base
+
+try:
+ from twisted.internet import ssl
+except ImportError:
+ ssl = None
+if ssl and not ssl.supported:
+ ssl = None
+
+from twisted.internet.defer import Deferred, maybeDeferred
+from twisted.python import util, runtime
+
+
+
+class ThreePhaseEventTests(unittest.TestCase):
+ """
+ Tests for the private implementation helpers for system event triggers.
+ """
+ def setUp(self):
+ """
+ Create a trigger, an argument, and an event to be used by tests.
+ """
+ self.trigger = lambda x: None
+ self.arg = object()
+ self.event = base._ThreePhaseEvent()
+
+
+ def test_addInvalidPhase(self):
+ """
+ L{_ThreePhaseEvent.addTrigger} should raise L{KeyError} when called
+ with an invalid phase.
+ """
+ self.assertRaises(
+ KeyError,
+ self.event.addTrigger, 'xxx', self.trigger, self.arg)
+
+
+ def test_addBeforeTrigger(self):
+ """
+ L{_ThreePhaseEvent.addTrigger} should accept C{'before'} as a phase, a
+ callable, and some arguments and add the callable with the arguments to
+ the before list.
+ """
+ self.event.addTrigger('before', self.trigger, self.arg)
+ self.assertEqual(
+ self.event.before,
+ [(self.trigger, (self.arg,), {})])
+
+
+ def test_addDuringTrigger(self):
+ """
+ L{_ThreePhaseEvent.addTrigger} should accept C{'during'} as a phase, a
+ callable, and some arguments and add the callable with the arguments to
+ the during list.
+ """
+ self.event.addTrigger('during', self.trigger, self.arg)
+ self.assertEqual(
+ self.event.during,
+ [(self.trigger, (self.arg,), {})])
+
+
+ def test_addAfterTrigger(self):
+ """
+ L{_ThreePhaseEvent.addTrigger} should accept C{'after'} as a phase, a
+ callable, and some arguments and add the callable with the arguments to
+ the after list.
+ """
+ self.event.addTrigger('after', self.trigger, self.arg)
+ self.assertEqual(
+ self.event.after,
+ [(self.trigger, (self.arg,), {})])
+
+
+ def test_removeTrigger(self):
+ """
+ L{_ThreePhaseEvent.removeTrigger} should accept an opaque object
+ previously returned by L{_ThreePhaseEvent.addTrigger} and remove the
+ associated trigger.
+ """
+ handle = self.event.addTrigger('before', self.trigger, self.arg)
+ self.event.removeTrigger(handle)
+ self.assertEqual(self.event.before, [])
+
+
+ def test_removeNonexistentTrigger(self):
+ """
+ L{_ThreePhaseEvent.removeTrigger} should raise L{ValueError} when given
+ an object not previously returned by L{_ThreePhaseEvent.addTrigger}.
+ """
+ self.assertRaises(ValueError, self.event.removeTrigger, object())
+
+
+ def test_removeRemovedTrigger(self):
+ """
+ L{_ThreePhaseEvent.removeTrigger} should raise L{ValueError} the second
+ time it is called with an object returned by
+ L{_ThreePhaseEvent.addTrigger}.
+ """
+ handle = self.event.addTrigger('before', self.trigger, self.arg)
+ self.event.removeTrigger(handle)
+ self.assertRaises(ValueError, self.event.removeTrigger, handle)
+
+
+ def test_removeAlmostValidTrigger(self):
+ """
+ L{_ThreePhaseEvent.removeTrigger} should raise L{ValueError} if it is
+ given a trigger handle which resembles a valid trigger handle aside
+ from its phase being incorrect.
+ """
+ self.assertRaises(
+ KeyError,
+ self.event.removeTrigger, ('xxx', self.trigger, (self.arg,), {}))
+
+
+ def test_fireEvent(self):
+ """
+ L{_ThreePhaseEvent.fireEvent} should call I{before}, I{during}, and
+ I{after} phase triggers in that order.
+ """
+ events = []
+ self.event.addTrigger('after', events.append, ('first', 'after'))
+ self.event.addTrigger('during', events.append, ('first', 'during'))
+ self.event.addTrigger('before', events.append, ('first', 'before'))
+ self.event.addTrigger('before', events.append, ('second', 'before'))
+ self.event.addTrigger('during', events.append, ('second', 'during'))
+ self.event.addTrigger('after', events.append, ('second', 'after'))
+
+ self.assertEqual(events, [])
+ self.event.fireEvent()
+ self.assertEqual(events,
+ [('first', 'before'), ('second', 'before'),
+ ('first', 'during'), ('second', 'during'),
+ ('first', 'after'), ('second', 'after')])
+
+
+ def test_asynchronousBefore(self):
+ """
+ L{_ThreePhaseEvent.fireEvent} should wait for any L{Deferred} returned
+ by a I{before} phase trigger before proceeding to I{during} events.
+ """
+ events = []
+ beforeResult = Deferred()
+ self.event.addTrigger('before', lambda: beforeResult)
+ self.event.addTrigger('during', events.append, 'during')
+ self.event.addTrigger('after', events.append, 'after')
+
+ self.assertEqual(events, [])
+ self.event.fireEvent()
+ self.assertEqual(events, [])
+ beforeResult.callback(None)
+ self.assertEqual(events, ['during', 'after'])
+
+
+ def test_beforeTriggerException(self):
+ """
+ If a before-phase trigger raises a synchronous exception, it should be
+ logged and the remaining triggers should be run.
+ """
+ events = []
+
+ class DummyException(Exception):
+ pass
+
+ def raisingTrigger():
+ raise DummyException()
+
+ self.event.addTrigger('before', raisingTrigger)
+ self.event.addTrigger('before', events.append, 'before')
+ self.event.addTrigger('during', events.append, 'during')
+ self.event.fireEvent()
+ self.assertEqual(events, ['before', 'during'])
+ errors = self.flushLoggedErrors(DummyException)
+ self.assertEqual(len(errors), 1)
+
+
+ def test_duringTriggerException(self):
+ """
+ If a during-phase trigger raises a synchronous exception, it should be
+ logged and the remaining triggers should be run.
+ """
+ events = []
+
+ class DummyException(Exception):
+ pass
+
+ def raisingTrigger():
+ raise DummyException()
+
+ self.event.addTrigger('during', raisingTrigger)
+ self.event.addTrigger('during', events.append, 'during')
+ self.event.addTrigger('after', events.append, 'after')
+ self.event.fireEvent()
+ self.assertEqual(events, ['during', 'after'])
+ errors = self.flushLoggedErrors(DummyException)
+ self.assertEqual(len(errors), 1)
+
+
+ def test_synchronousRemoveAlreadyExecutedBefore(self):
+ """
+ If a before-phase trigger tries to remove another before-phase trigger
+ which has already run, a warning should be emitted.
+ """
+ events = []
+
+ def removeTrigger():
+ self.event.removeTrigger(beforeHandle)
+
+ beforeHandle = self.event.addTrigger('before', events.append, ('first', 'before'))
+ self.event.addTrigger('before', removeTrigger)
+ self.event.addTrigger('before', events.append, ('second', 'before'))
+ self.assertWarns(
+ DeprecationWarning,
+ "Removing already-fired system event triggers will raise an "
+ "exception in a future version of Twisted.",
+ __file__,
+ self.event.fireEvent)
+ self.assertEqual(events, [('first', 'before'), ('second', 'before')])
+
+
+ def test_synchronousRemovePendingBefore(self):
+ """
+ If a before-phase trigger removes another before-phase trigger which
+ has not yet run, the removed trigger should not be run.
+ """
+ events = []
+ self.event.addTrigger(
+ 'before', lambda: self.event.removeTrigger(beforeHandle))
+ beforeHandle = self.event.addTrigger(
+ 'before', events.append, ('first', 'before'))
+ self.event.addTrigger('before', events.append, ('second', 'before'))
+ self.event.fireEvent()
+ self.assertEqual(events, [('second', 'before')])
+
+
+ def test_synchronousBeforeRemovesDuring(self):
+ """
+ If a before-phase trigger removes a during-phase trigger, the
+ during-phase trigger should not be run.
+ """
+ events = []
+ self.event.addTrigger(
+ 'before', lambda: self.event.removeTrigger(duringHandle))
+ duringHandle = self.event.addTrigger('during', events.append, 'during')
+ self.event.addTrigger('after', events.append, 'after')
+ self.event.fireEvent()
+ self.assertEqual(events, ['after'])
+
+
+ def test_asynchronousBeforeRemovesDuring(self):
+ """
+ If a before-phase trigger returns a L{Deferred} and later removes a
+ during-phase trigger before the L{Deferred} fires, the during-phase
+ trigger should not be run.
+ """
+ events = []
+ beforeResult = Deferred()
+ self.event.addTrigger('before', lambda: beforeResult)
+ duringHandle = self.event.addTrigger('during', events.append, 'during')
+ self.event.addTrigger('after', events.append, 'after')
+ self.event.fireEvent()
+ self.event.removeTrigger(duringHandle)
+ beforeResult.callback(None)
+ self.assertEqual(events, ['after'])
+
+
+ def test_synchronousBeforeRemovesConspicuouslySimilarDuring(self):
+ """
+ If a before-phase trigger removes a during-phase trigger which is
+ identical to an already-executed before-phase trigger aside from their
+ phases, no warning should be emitted and the during-phase trigger
+ should not be run.
+ """
+ events = []
+ def trigger():
+ events.append('trigger')
+ self.event.addTrigger('before', trigger)
+ self.event.addTrigger(
+ 'before', lambda: self.event.removeTrigger(duringTrigger))
+ duringTrigger = self.event.addTrigger('during', trigger)
+ self.event.fireEvent()
+ self.assertEqual(events, ['trigger'])
+
+
+ def test_synchronousRemovePendingDuring(self):
+ """
+ If a during-phase trigger removes another during-phase trigger which
+ has not yet run, the removed trigger should not be run.
+ """
+ events = []
+ self.event.addTrigger(
+ 'during', lambda: self.event.removeTrigger(duringHandle))
+ duringHandle = self.event.addTrigger(
+ 'during', events.append, ('first', 'during'))
+ self.event.addTrigger(
+ 'during', events.append, ('second', 'during'))
+ self.event.fireEvent()
+ self.assertEqual(events, [('second', 'during')])
+
+
+ def test_triggersRunOnce(self):
+ """
+ A trigger should only be called on the first call to
+ L{_ThreePhaseEvent.fireEvent}.
+ """
+ events = []
+ self.event.addTrigger('before', events.append, 'before')
+ self.event.addTrigger('during', events.append, 'during')
+ self.event.addTrigger('after', events.append, 'after')
+ self.event.fireEvent()
+ self.event.fireEvent()
+ self.assertEqual(events, ['before', 'during', 'after'])
+
+
+ def test_finishedBeforeTriggersCleared(self):
+ """
+ The temporary list L{_ThreePhaseEvent.finishedBefore} should be emptied
+ and the state reset to C{'BASE'} before the first during-phase trigger
+ executes.
+ """
+ events = []
+ def duringTrigger():
+ events.append('during')
+ self.assertEqual(self.event.finishedBefore, [])
+ self.assertEqual(self.event.state, 'BASE')
+ self.event.addTrigger('before', events.append, 'before')
+ self.event.addTrigger('during', duringTrigger)
+ self.event.fireEvent()
+ self.assertEqual(events, ['before', 'during'])
+
+
+
+class SystemEventTestCase(unittest.TestCase):
+ """
+ Tests for the reactor's implementation of the C{fireSystemEvent},
+ C{addSystemEventTrigger}, and C{removeSystemEventTrigger} methods of the
+ L{IReactorCore} interface.
+
+ @ivar triggers: A list of the handles to triggers which have been added to
+ the reactor.
+ """
+ def setUp(self):
+ """
+ Create an empty list in which to store trigger handles.
+ """
+ self.triggers = []
+
+
+ def tearDown(self):
+ """
+ Remove all remaining triggers from the reactor.
+ """
+ while self.triggers:
+ trigger = self.triggers.pop()
+ try:
+ reactor.removeSystemEventTrigger(trigger)
+ except (ValueError, KeyError):
+ pass
+
+
+ def addTrigger(self, event, phase, func):
+ """
+ Add a trigger to the reactor and remember it in C{self.triggers}.
+ """
+ t = reactor.addSystemEventTrigger(event, phase, func)
+ self.triggers.append(t)
+ return t
+
+
+ def removeTrigger(self, trigger):
+ """
+ Remove a trigger by its handle from the reactor and from
+ C{self.triggers}.
+ """
+ reactor.removeSystemEventTrigger(trigger)
+ self.triggers.remove(trigger)
+
+
+ def _addSystemEventTriggerTest(self, phase):
+ eventType = 'test'
+ events = []
+ def trigger():
+ events.append(None)
+ self.addTrigger(phase, eventType, trigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, [None])
+
+
+ def test_beforePhase(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should accept the C{'before'}
+ phase and not call the given object until the right event is fired.
+ """
+ self._addSystemEventTriggerTest('before')
+
+
+ def test_duringPhase(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should accept the C{'during'}
+ phase and not call the given object until the right event is fired.
+ """
+ self._addSystemEventTriggerTest('during')
+
+
+ def test_afterPhase(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should accept the C{'after'}
+ phase and not call the given object until the right event is fired.
+ """
+ self._addSystemEventTriggerTest('after')
+
+
+ def test_unknownPhase(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should reject phases other than
+ C{'before'}, C{'during'}, or C{'after'}.
+ """
+ eventType = 'test'
+ self.assertRaises(
+ KeyError, self.addTrigger, 'xxx', eventType, lambda: None)
+
+
+ def test_beforePreceedsDuring(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should call triggers added to the
+ C{'before'} phase before it calls triggers added to the C{'during'}
+ phase.
+ """
+ eventType = 'test'
+ events = []
+ def beforeTrigger():
+ events.append('before')
+ def duringTrigger():
+ events.append('during')
+ self.addTrigger('before', eventType, beforeTrigger)
+ self.addTrigger('during', eventType, duringTrigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, ['before', 'during'])
+
+
+ def test_duringPreceedsAfter(self):
+ """
+ L{IReactorCore.addSystemEventTrigger} should call triggers added to the
+ C{'during'} phase before it calls triggers added to the C{'after'}
+ phase.
+ """
+ eventType = 'test'
+ events = []
+ def duringTrigger():
+ events.append('during')
+ def afterTrigger():
+ events.append('after')
+ self.addTrigger('during', eventType, duringTrigger)
+ self.addTrigger('after', eventType, afterTrigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, ['during', 'after'])
+
+
+ def test_beforeReturnsDeferred(self):
+ """
+ If a trigger added to the C{'before'} phase of an event returns a
+ L{Deferred}, the C{'during'} phase should be delayed until it is called
+ back.
+ """
+ triggerDeferred = Deferred()
+ eventType = 'test'
+ events = []
+ def beforeTrigger():
+ return triggerDeferred
+ def duringTrigger():
+ events.append('during')
+ self.addTrigger('before', eventType, beforeTrigger)
+ self.addTrigger('during', eventType, duringTrigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, [])
+ triggerDeferred.callback(None)
+ self.assertEqual(events, ['during'])
+
+
+ def test_multipleBeforeReturnDeferred(self):
+ """
+ If more than one trigger added to the C{'before'} phase of an event
+ return L{Deferred}s, the C{'during'} phase should be delayed until they
+ are all called back.
+ """
+ firstDeferred = Deferred()
+ secondDeferred = Deferred()
+ eventType = 'test'
+ events = []
+ def firstBeforeTrigger():
+ return firstDeferred
+ def secondBeforeTrigger():
+ return secondDeferred
+ def duringTrigger():
+ events.append('during')
+ self.addTrigger('before', eventType, firstBeforeTrigger)
+ self.addTrigger('before', eventType, secondBeforeTrigger)
+ self.addTrigger('during', eventType, duringTrigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, [])
+ firstDeferred.callback(None)
+ self.assertEqual(events, [])
+ secondDeferred.callback(None)
+ self.assertEqual(events, ['during'])
+
+
+ def test_subsequentBeforeTriggerFiresPriorBeforeDeferred(self):
+ """
+ If a trigger added to the C{'before'} phase of an event calls back a
+ L{Deferred} returned by an earlier trigger in the C{'before'} phase of
+ the same event, the remaining C{'before'} triggers for that event
+ should be run and any further L{Deferred}s waited on before proceeding
+ to the C{'during'} events.
+ """
+ eventType = 'test'
+ events = []
+ firstDeferred = Deferred()
+ secondDeferred = Deferred()
+ def firstBeforeTrigger():
+ return firstDeferred
+ def secondBeforeTrigger():
+ firstDeferred.callback(None)
+ def thirdBeforeTrigger():
+ events.append('before')
+ return secondDeferred
+ def duringTrigger():
+ events.append('during')
+ self.addTrigger('before', eventType, firstBeforeTrigger)
+ self.addTrigger('before', eventType, secondBeforeTrigger)
+ self.addTrigger('before', eventType, thirdBeforeTrigger)
+ self.addTrigger('during', eventType, duringTrigger)
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, ['before'])
+ secondDeferred.callback(None)
+ self.assertEqual(events, ['before', 'during'])
+
+
+ def test_removeSystemEventTrigger(self):
+ """
+ A trigger removed with L{IReactorCore.removeSystemEventTrigger} should
+ not be called when the event fires.
+ """
+ eventType = 'test'
+ events = []
+ def firstBeforeTrigger():
+ events.append('first')
+ def secondBeforeTrigger():
+ events.append('second')
+ self.addTrigger('before', eventType, firstBeforeTrigger)
+ self.removeTrigger(
+ self.addTrigger('before', eventType, secondBeforeTrigger))
+ self.assertEqual(events, [])
+ reactor.fireSystemEvent(eventType)
+ self.assertEqual(events, ['first'])
+
+
+ def test_removeNonExistentSystemEventTrigger(self):
+ """
+ Passing an object to L{IReactorCore.removeSystemEventTrigger} which was
+ not returned by a previous call to
+ L{IReactorCore.addSystemEventTrigger} or which has already been passed
+ to C{removeSystemEventTrigger} should result in L{TypeError},
+ L{KeyError}, or L{ValueError} being raised.
+ """
+ b = self.addTrigger('during', 'test', lambda: None)
+ self.removeTrigger(b)
+ self.assertRaises(
+ TypeError, reactor.removeSystemEventTrigger, None)
+ self.assertRaises(
+ ValueError, reactor.removeSystemEventTrigger, b)
+ self.assertRaises(
+ KeyError,
+ reactor.removeSystemEventTrigger,
+ (b[0], ('xxx',) + b[1][1:]))
+
+
+ def test_interactionBetweenDifferentEvents(self):
+ """
+ L{IReactorCore.fireSystemEvent} should behave the same way for a
+ particular system event regardless of whether Deferreds are being
+ waited on for a different system event.
+ """
+ events = []
+
+ firstEvent = 'first-event'
+ firstDeferred = Deferred()
+ def beforeFirstEvent():
+ events.append(('before', 'first'))
+ return firstDeferred
+ def afterFirstEvent():
+ events.append(('after', 'first'))
+
+ secondEvent = 'second-event'
+ secondDeferred = Deferred()
+ def beforeSecondEvent():
+ events.append(('before', 'second'))
+ return secondDeferred
+ def afterSecondEvent():
+ events.append(('after', 'second'))
+
+ self.addTrigger('before', firstEvent, beforeFirstEvent)
+ self.addTrigger('after', firstEvent, afterFirstEvent)
+ self.addTrigger('before', secondEvent, beforeSecondEvent)
+ self.addTrigger('after', secondEvent, afterSecondEvent)
+
+ self.assertEqual(events, [])
+
+ # After this, firstEvent should be stuck before 'during' waiting for
+ # firstDeferred.
+ reactor.fireSystemEvent(firstEvent)
+ self.assertEqual(events, [('before', 'first')])
+
+ # After this, secondEvent should be stuck before 'during' waiting for
+ # secondDeferred.
+ reactor.fireSystemEvent(secondEvent)
+ self.assertEqual(events, [('before', 'first'), ('before', 'second')])
+
+ # After this, firstEvent should have finished completely, but
+ # secondEvent should be at the same place.
+ firstDeferred.callback(None)
+ self.assertEqual(events, [('before', 'first'), ('before', 'second'),
+ ('after', 'first')])
+
+ # After this, secondEvent should have finished completely.
+ secondDeferred.callback(None)
+ self.assertEqual(events, [('before', 'first'), ('before', 'second'),
+ ('after', 'first'), ('after', 'second')])
+
+
+
+class TimeTestCase(unittest.TestCase):
+ """
+ Tests for the IReactorTime part of the reactor.
+ """
+
+
+ def test_seconds(self):
+ """
+ L{twisted.internet.reactor.seconds} should return something
+ like a number.
+
+ 1. This test specifically does not assert any relation to the
+ "system time" as returned by L{time.time} or
+ L{twisted.python.runtime.seconds}, because at some point we
+ may find a better option for scheduling calls than
+ wallclock-time.
+ 2. This test *also* does not assert anything about the type of
+ the result, because operations may not return ints or
+ floats: For example, datetime-datetime == timedelta(0).
+ """
+ now = reactor.seconds()
+ self.assertEqual(now-now+now, now)
+
+
+ def test_callLaterUsesReactorSecondsInDelayedCall(self):
+ """
+ L{reactor.callLater} should use the reactor's seconds factory
+ to produce the time at which the DelayedCall will be called.
+ """
+ oseconds = reactor.seconds
+ reactor.seconds = lambda: 100
+ try:
+ call = reactor.callLater(5, lambda: None)
+ self.assertEqual(call.getTime(), 105)
+ finally:
+ reactor.seconds = oseconds
+
+
+ def test_callLaterUsesReactorSecondsAsDelayedCallSecondsFactory(self):
+ """
+ L{reactor.callLater} should propagate its own seconds factory
+ to the DelayedCall to use as its own seconds factory.
+ """
+ oseconds = reactor.seconds
+ reactor.seconds = lambda: 100
+ try:
+ call = reactor.callLater(5, lambda: None)
+ self.assertEqual(call.seconds(), 100)
+ finally:
+ reactor.seconds = oseconds
+
+
+ def test_callLater(self):
+ """
+ Test that a DelayedCall really calls the function it is
+ supposed to call.
+ """
+ d = Deferred()
+ reactor.callLater(0, d.callback, None)
+ d.addCallback(self.assertEqual, None)
+ return d
+
+
+ def test_cancelDelayedCall(self):
+ """
+ Test that when a DelayedCall is cancelled it does not run.
+ """
+ called = []
+ def function():
+ called.append(None)
+ call = reactor.callLater(0, function)
+ call.cancel()
+
+ # Schedule a call in two "iterations" to check to make sure that the
+ # above call never ran.
+ d = Deferred()
+ def check():
+ try:
+ self.assertEqual(called, [])
+ except:
+ d.errback()
+ else:
+ d.callback(None)
+ reactor.callLater(0, reactor.callLater, 0, check)
+ return d
+
+
+ def test_cancelCancelledDelayedCall(self):
+ """
+ Test that cancelling a DelayedCall which has already been cancelled
+ raises the appropriate exception.
+ """
+ call = reactor.callLater(0, lambda: None)
+ call.cancel()
+ self.assertRaises(error.AlreadyCancelled, call.cancel)
+
+
+ def test_cancelCalledDelayedCallSynchronous(self):
+ """
+ Test that cancelling a DelayedCall in the DelayedCall's function as
+ that function is being invoked by the DelayedCall raises the
+ appropriate exception.
+ """
+ d = Deferred()
+ def later():
+ try:
+ self.assertRaises(error.AlreadyCalled, call.cancel)
+ except:
+ d.errback()
+ else:
+ d.callback(None)
+ call = reactor.callLater(0, later)
+ return d
+
+
+ def test_cancelCalledDelayedCallAsynchronous(self):
+ """
+ Test that cancelling a DelayedCall after it has run its function
+ raises the appropriate exception.
+ """
+ d = Deferred()
+ def check():
+ try:
+ self.assertRaises(error.AlreadyCalled, call.cancel)
+ except:
+ d.errback()
+ else:
+ d.callback(None)
+ def later():
+ reactor.callLater(0, check)
+ call = reactor.callLater(0, later)
+ return d
+
+
+ def testCallLaterTime(self):
+ d = reactor.callLater(10, lambda: None)
+ try:
+ self.failUnless(d.getTime() - (time.time() + 10) < 1)
+ finally:
+ d.cancel()
+
+ def testCallLaterOrder(self):
+ l = []
+ l2 = []
+ def f(x):
+ l.append(x)
+ def f2(x):
+ l2.append(x)
+ def done():
+ self.assertEqual(l, range(20))
+ def done2():
+ self.assertEqual(l2, range(10))
+
+ for n in range(10):
+ reactor.callLater(0, f, n)
+ for n in range(10):
+ reactor.callLater(0, f, n+10)
+ reactor.callLater(0.1, f2, n)
+
+ reactor.callLater(0, done)
+ reactor.callLater(0.1, done2)
+ d = Deferred()
+ reactor.callLater(0.2, d.callback, None)
+ return d
+
+ testCallLaterOrder.todo = "See bug 1396"
+ testCallLaterOrder.skip = "Trial bug, todo doesn't work! See bug 1397"
+ def testCallLaterOrder2(self):
+ # This time destroy the clock resolution so that it fails reliably
+ # even on systems that don't have a crappy clock resolution.
+
+ def seconds():
+ return int(time.time())
+
+ base_original = base.seconds
+ runtime_original = runtime.seconds
+ base.seconds = seconds
+ runtime.seconds = seconds
+
+ def cleanup(x):
+ runtime.seconds = runtime_original
+ base.seconds = base_original
+ return x
+ return maybeDeferred(self.testCallLaterOrder).addBoth(cleanup)
+
+ testCallLaterOrder2.todo = "See bug 1396"
+ testCallLaterOrder2.skip = "Trial bug, todo doesn't work! See bug 1397"
+
+ def testDelayedCallStringification(self):
+ # Mostly just make sure str() isn't going to raise anything for
+ # DelayedCalls within reason.
+ dc = reactor.callLater(0, lambda x, y: None, 'x', y=10)
+ str(dc)
+ dc.reset(5)
+ str(dc)
+ dc.cancel()
+ str(dc)
+
+ dc = reactor.callLater(0, lambda: None, x=[({'hello': u'world'}, 10j), reactor], *range(10))
+ str(dc)
+ dc.cancel()
+ str(dc)
+
+ def calledBack(ignored):
+ str(dc)
+ d = Deferred().addCallback(calledBack)
+ dc = reactor.callLater(0, d.callback, None)
+ str(dc)
+ return d
+
+
+ def testDelayedCallSecondsOverride(self):
+ """
+ Test that the C{seconds} argument to DelayedCall gets used instead of
+ the default timing function, if it is not None.
+ """
+ def seconds():
+ return 10
+ dc = base.DelayedCall(5, lambda: None, (), {}, lambda dc: None,
+ lambda dc: None, seconds)
+ self.assertEqual(dc.getTime(), 5)
+ dc.reset(3)
+ self.assertEqual(dc.getTime(), 13)
+
+
+class CallFromThreadTests(unittest.TestCase):
+ def testWakeUp(self):
+ # Make sure other threads can wake up the reactor
+ d = Deferred()
+ def wake():
+ time.sleep(0.1)
+ # callFromThread will call wakeUp for us
+ reactor.callFromThread(d.callback, None)
+ reactor.callInThread(wake)
+ return d
+
+ if interfaces.IReactorThreads(reactor, None) is None:
+ testWakeUp.skip = "Nothing to wake up for without thread support"
+
+ def _stopCallFromThreadCallback(self):
+ self.stopped = True
+
+ def _callFromThreadCallback(self, d):
+ reactor.callFromThread(self._callFromThreadCallback2, d)
+ reactor.callLater(0, self._stopCallFromThreadCallback)
+
+ def _callFromThreadCallback2(self, d):
+ try:
+ self.assert_(self.stopped)
+ except:
+ # Send the error to the deferred
+ d.errback()
+ else:
+ d.callback(None)
+
+ def testCallFromThreadStops(self):
+ """
+ Ensure that callFromThread from inside a callFromThread
+ callback doesn't sit in an infinite loop and lets other
+ things happen too.
+ """
+ self.stopped = False
+ d = defer.Deferred()
+ reactor.callFromThread(self._callFromThreadCallback, d)
+ return d
+
+
+class DelayedTestCase(unittest.TestCase):
+ def setUp(self):
+ self.finished = 0
+ self.counter = 0
+ self.timers = {}
+ self.deferred = defer.Deferred()
+
+ def tearDown(self):
+ for t in self.timers.values():
+ t.cancel()
+
+ def checkTimers(self):
+ l1 = self.timers.values()
+ l2 = list(reactor.getDelayedCalls())
+
+ # There should be at least the calls we put in. There may be other
+ # calls that are none of our business and that we should ignore,
+ # though.
+
+ missing = []
+ for dc in l1:
+ if dc not in l2:
+ missing.append(dc)
+ if missing:
+ self.finished = 1
+ self.failIf(missing, "Should have been missing no calls, instead was missing " + repr(missing))
+
+ def callback(self, tag):
+ del self.timers[tag]
+ self.checkTimers()
+
+ def addCallback(self, tag):
+ self.callback(tag)
+ self.addTimer(15, self.callback)
+
+ def done(self, tag):
+ self.finished = 1
+ self.callback(tag)
+ self.deferred.callback(None)
+
+ def addTimer(self, when, callback):
+ self.timers[self.counter] = reactor.callLater(when * 0.01, callback,
+ self.counter)
+ self.counter += 1
+ self.checkTimers()
+
+ def testGetDelayedCalls(self):
+ if not hasattr(reactor, "getDelayedCalls"):
+ return
+ # This is not a race because we don't do anything which might call
+ # the reactor until we have all the timers set up. If we did, this
+ # test might fail on slow systems.
+ self.checkTimers()
+ self.addTimer(35, self.done)
+ self.addTimer(20, self.callback)
+ self.addTimer(30, self.callback)
+ which = self.counter
+ self.addTimer(29, self.callback)
+ self.addTimer(25, self.addCallback)
+ self.addTimer(26, self.callback)
+
+ self.timers[which].cancel()
+ del self.timers[which]
+ self.checkTimers()
+
+ self.deferred.addCallback(lambda x : self.checkTimers())
+ return self.deferred
+
+
+ def test_active(self):
+ """
+ L{IDelayedCall.active} returns False once the call has run.
+ """
+ dcall = reactor.callLater(0.01, self.deferred.callback, True)
+ self.assertEqual(dcall.active(), True)
+
+ def checkDeferredCall(success):
+ self.assertEqual(dcall.active(), False)
+ return success
+
+ self.deferred.addCallback(checkDeferredCall)
+
+ return self.deferred
+
+
+
+resolve_helper = """
+import %(reactor)s
+%(reactor)s.install()
+from twisted.internet import reactor
+
+class Foo:
+ def __init__(self):
+ reactor.callWhenRunning(self.start)
+ self.timer = reactor.callLater(3, self.failed)
+ def start(self):
+ reactor.resolve('localhost').addBoth(self.done)
+ def done(self, res):
+ print 'done', res
+ reactor.stop()
+ def failed(self):
+ print 'failed'
+ self.timer = None
+ reactor.stop()
+f = Foo()
+reactor.run()
+"""
+
+class ChildResolveProtocol(protocol.ProcessProtocol):
+ def __init__(self, onCompletion):
+ self.onCompletion = onCompletion
+
+ def connectionMade(self):
+ self.output = []
+ self.error = []
+
+ def outReceived(self, out):
+ self.output.append(out)
+
+ def errReceived(self, err):
+ self.error.append(err)
+
+ def processEnded(self, reason):
+ self.onCompletion.callback((reason, self.output, self.error))
+ self.onCompletion = None
+
+
+class Resolve(unittest.TestCase):
+ def testChildResolve(self):
+ # I've seen problems with reactor.run under gtk2reactor. Spawn a
+ # child which just does reactor.resolve after the reactor has
+ # started, fail if it does not complete in a timely fashion.
+ helperPath = os.path.abspath(self.mktemp())
+ helperFile = open(helperPath, 'w')
+
+ # Eeueuuggg
+ reactorName = reactor.__module__
+
+ helperFile.write(resolve_helper % {'reactor': reactorName})
+ helperFile.close()
+
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
+
+ helperDeferred = Deferred()
+ helperProto = ChildResolveProtocol(helperDeferred)
+
+ reactor.spawnProcess(helperProto, sys.executable, ("python", "-u", helperPath), env)
+
+ def cbFinished((reason, output, error)):
+ # If the output is "done 127.0.0.1\n" we don't really care what
+ # else happened.
+ output = ''.join(output)
+ if output != 'done 127.0.0.1\n':
+ self.fail((
+ "The child process failed to produce the desired results:\n"
+ " Reason for termination was: %r\n"
+ " Output stream was: %r\n"
+ " Error stream was: %r\n") % (reason.getErrorMessage(), output, ''.join(error)))
+
+ helperDeferred.addCallback(cbFinished)
+ return helperDeferred
+
+if not interfaces.IReactorProcess(reactor, None):
+ Resolve.skip = "cannot run test: reactor doesn't support IReactorProcess"
+
+
+
+class CallFromThreadTestCase(unittest.TestCase):
+ """
+ Task scheduling from threads tests.
+ """
+ if interfaces.IReactorThreads(reactor, None) is None:
+ skip = "Nothing to test without thread support"
+
+ def setUp(self):
+ self.counter = 0
+ self.deferred = Deferred()
+
+
+ def schedule(self, *args, **kwargs):
+ """
+ Override in subclasses.
+ """
+ reactor.callFromThread(*args, **kwargs)
+
+
+ def test_lotsOfThreadsAreScheduledCorrectly(self):
+ """
+ L{IReactorThreads.callFromThread} can be used to schedule a large
+ number of calls in the reactor thread.
+ """
+ def addAndMaybeFinish():
+ self.counter += 1
+ if self.counter == 100:
+ self.deferred.callback(True)
+
+ for i in xrange(100):
+ self.schedule(addAndMaybeFinish)
+
+ return self.deferred
+
+
+ def test_threadsAreRunInScheduledOrder(self):
+ """
+ Callbacks should be invoked in the order they were scheduled.
+ """
+ order = []
+
+ def check(_):
+ self.assertEqual(order, [1, 2, 3])
+
+ self.deferred.addCallback(check)
+ self.schedule(order.append, 1)
+ self.schedule(order.append, 2)
+ self.schedule(order.append, 3)
+ self.schedule(reactor.callFromThread, self.deferred.callback, None)
+
+ return self.deferred
+
+
+ def test_scheduledThreadsNotRunUntilReactorRuns(self):
+ """
+ Scheduled tasks should not be run until the reactor starts running.
+ """
+ def incAndFinish():
+ self.counter = 1
+ self.deferred.callback(True)
+ self.schedule(incAndFinish)
+
+ # Callback shouldn't have fired yet.
+ self.assertEqual(self.counter, 0)
+
+ return self.deferred
+
+
+
+class MyProtocol(protocol.Protocol):
+ """
+ Sample protocol.
+ """
+
+class MyFactory(protocol.Factory):
+ """
+ Sample factory.
+ """
+
+ protocol = MyProtocol
+
+
+class ProtocolTestCase(unittest.TestCase):
+
+ def testFactory(self):
+ factory = MyFactory()
+ protocol = factory.buildProtocol(None)
+ self.assertEqual(protocol.factory, factory)
+ self.assert_( isinstance(protocol, factory.protocol) )
+
+
+class DummyProducer(object):
+ """
+ Very uninteresting producer implementation used by tests to ensure the
+ right methods are called by the consumer with which it is registered.
+
+ @type events: C{list} of C{str}
+ @ivar events: The producer/consumer related events which have happened to
+ this producer. Strings in this list may be C{'resume'}, C{'stop'}, or
+ C{'pause'}. Elements are added as they occur.
+ """
+
+ def __init__(self):
+ self.events = []
+
+
+ def resumeProducing(self):
+ self.events.append('resume')
+
+
+ def stopProducing(self):
+ self.events.append('stop')
+
+
+ def pauseProducing(self):
+ self.events.append('pause')
+
+
+
+class SillyDescriptor(abstract.FileDescriptor):
+ """
+ A descriptor whose data buffer gets filled very fast.
+
+ Useful for testing FileDescriptor's IConsumer interface, since
+ the data buffer fills as soon as at least four characters are
+ written to it, and gets emptied in a single doWrite() cycle.
+ """
+ bufferSize = 3
+ connected = True
+
+ def writeSomeData(self, data):
+ """
+ Always write all data.
+ """
+ return len(data)
+
+
+ def startWriting(self):
+ """
+ Do nothing: bypass the reactor.
+ """
+ stopWriting = startWriting
+
+
+
+class ReentrantProducer(DummyProducer):
+ """
+ Similar to L{DummyProducer}, but with a resumeProducing method which calls
+ back into an L{IConsumer} method of the consumer against which it is
+ registered.
+
+ @ivar consumer: The consumer with which this producer has been or will
+ be registered.
+
+ @ivar methodName: The name of the method to call on the consumer inside
+ C{resumeProducing}.
+
+ @ivar methodArgs: The arguments to pass to the consumer method invoked in
+ C{resumeProducing}.
+ """
+ def __init__(self, consumer, methodName, *methodArgs):
+ super(ReentrantProducer, self).__init__()
+ self.consumer = consumer
+ self.methodName = methodName
+ self.methodArgs = methodArgs
+
+
+ def resumeProducing(self):
+ super(ReentrantProducer, self).resumeProducing()
+ getattr(self.consumer, self.methodName)(*self.methodArgs)
+
+
+
+class TestProducer(unittest.TestCase):
+ """
+ Test abstract.FileDescriptor's consumer interface.
+ """
+ def test_doubleProducer(self):
+ """
+ Verify that registering a non-streaming producer invokes its
+ resumeProducing() method and that you can only register one producer
+ at a time.
+ """
+ fd = abstract.FileDescriptor()
+ fd.connected = 1
+ dp = DummyProducer()
+ fd.registerProducer(dp, 0)
+ self.assertEqual(dp.events, ['resume'])
+ self.assertRaises(RuntimeError, fd.registerProducer, DummyProducer(), 0)
+
+
+ def test_unconnectedFileDescriptor(self):
+ """
+ Verify that registering a producer when the connection has already
+ been closed invokes its stopProducing() method.
+ """
+ fd = abstract.FileDescriptor()
+ fd.disconnected = 1
+ dp = DummyProducer()
+ fd.registerProducer(dp, 0)
+ self.assertEqual(dp.events, ['stop'])
+
+
+ def _dontPausePullConsumerTest(self, methodName):
+ descriptor = SillyDescriptor()
+ producer = DummyProducer()
+ descriptor.registerProducer(producer, streaming=False)
+ self.assertEqual(producer.events, ['resume'])
+ del producer.events[:]
+
+ # Fill up the descriptor's write buffer so we can observe whether or
+ # not it pauses its producer in that case.
+ getattr(descriptor, methodName)('1234')
+
+ self.assertEqual(producer.events, [])
+
+
+ def test_dontPausePullConsumerOnWrite(self):
+ """
+ Verify that FileDescriptor does not call producer.pauseProducing() on a
+ non-streaming pull producer in response to a L{IConsumer.write} call
+ which results in a full write buffer. Issue #2286.
+ """
+ return self._dontPausePullConsumerTest('write')
+
+
+ def test_dontPausePullConsumerOnWriteSequence(self):
+ """
+ Like L{test_dontPausePullConsumerOnWrite}, but for a call to
+ C{writeSequence} rather than L{IConsumer.write}.
+
+ C{writeSequence} is not part of L{IConsumer}, but
+ L{abstract.FileDescriptor} has supported consumery behavior in response
+ to calls to L{writeSequence} forever.
+ """
+ return self._dontPausePullConsumerTest('writeSequence')
+
+
+ def _reentrantStreamingProducerTest(self, methodName):
+ descriptor = SillyDescriptor()
+ producer = ReentrantProducer(descriptor, methodName, 'spam')
+ descriptor.registerProducer(producer, streaming=True)
+
+ # Start things off by filling up the descriptor's buffer so it will
+ # pause its producer.
+ getattr(descriptor, methodName)('spam')
+
+ # Sanity check - make sure that worked.
+ self.assertEqual(producer.events, ['pause'])
+ del producer.events[:]
+
+ # After one call to doWrite, the buffer has been emptied so the
+ # FileDescriptor should resume its producer. That will result in an
+ # immediate call to FileDescriptor.write which will again fill the
+ # buffer and result in the producer being paused.
+ descriptor.doWrite()
+ self.assertEqual(producer.events, ['resume', 'pause'])
+ del producer.events[:]
+
+ # After a second call to doWrite, the exact same thing should have
+ # happened. Prior to the bugfix for which this test was written,
+ # FileDescriptor would have incorrectly believed its producer was
+ # already resumed (it was paused) and so not resume it again.
+ descriptor.doWrite()
+ self.assertEqual(producer.events, ['resume', 'pause'])
+
+
+ def test_reentrantStreamingProducerUsingWrite(self):
+ """
+ Verify that FileDescriptor tracks producer's paused state correctly.
+ Issue #811, fixed in revision r12857.
+ """
+ return self._reentrantStreamingProducerTest('write')
+
+
+ def test_reentrantStreamingProducerUsingWriteSequence(self):
+ """
+ Like L{test_reentrantStreamingProducerUsingWrite}, but for calls to
+ C{writeSequence}.
+
+ C{writeSequence} is B{not} part of L{IConsumer}, however
+ C{abstract.FileDescriptor} has supported consumery behavior in response
+ to calls to C{writeSequence} forever.
+ """
+ return self._reentrantStreamingProducerTest('writeSequence')
+
+
+
+class PortStringification(unittest.TestCase):
+ if interfaces.IReactorTCP(reactor, None) is not None:
+ def testTCP(self):
+ p = reactor.listenTCP(0, protocol.ServerFactory())
+ portNo = p.getHost().port
+ self.assertNotEqual(str(p).find(str(portNo)), -1,
+ "%d not found in %s" % (portNo, p))
+ return p.stopListening()
+
+ if interfaces.IReactorUDP(reactor, None) is not None:
+ def testUDP(self):
+ p = reactor.listenUDP(0, protocol.DatagramProtocol())
+ portNo = p.getHost().port
+ self.assertNotEqual(str(p).find(str(portNo)), -1,
+ "%d not found in %s" % (portNo, p))
+ return p.stopListening()
+
+ if interfaces.IReactorSSL(reactor, None) is not None and ssl:
+ def testSSL(self, ssl=ssl):
+ pem = util.sibpath(__file__, 'server.pem')
+ p = reactor.listenSSL(0, protocol.ServerFactory(), ssl.DefaultOpenSSLContextFactory(pem, pem))
+ portNo = p.getHost().port
+ self.assertNotEqual(str(p).find(str(portNo)), -1,
+ "%d not found in %s" % (portNo, p))
+ return p.stopListening()
diff --git a/twisted/test/test_iutils.py b/twisted/test/test_iutils.py
new file mode 100644
index 0000000..d89ab56
--- /dev/null
+++ b/twisted/test/test_iutils.py
@@ -0,0 +1,296 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test running processes with the APIs in L{twisted.internet.utils}.
+"""
+
+import warnings, os, stat, sys, signal
+
+from twisted.python.runtime import platform
+from twisted.trial import unittest
+from twisted.internet import error, reactor, utils, interfaces
+
+
+class ProcessUtilsTests(unittest.TestCase):
+ """
+ Test running a process using L{getProcessOutput}, L{getProcessValue}, and
+ L{getProcessOutputAndValue}.
+ """
+
+ if interfaces.IReactorProcess(reactor, None) is None:
+ skip = "reactor doesn't implement IReactorProcess"
+
+ output = None
+ value = None
+ exe = sys.executable
+
+ def makeSourceFile(self, sourceLines):
+ """
+ Write the given list of lines to a text file and return the absolute
+ path to it.
+ """
+ script = self.mktemp()
+ scriptFile = file(script, 'wt')
+ scriptFile.write(os.linesep.join(sourceLines) + os.linesep)
+ scriptFile.close()
+ return os.path.abspath(script)
+
+
+ def test_output(self):
+ """
+ L{getProcessOutput} returns a L{Deferred} which fires with the complete
+ output of the process it runs after that process exits.
+ """
+ scriptFile = self.makeSourceFile([
+ "import sys",
+ "for s in 'hello world\\n':",
+ " sys.stdout.write(s)",
+ " sys.stdout.flush()"])
+ d = utils.getProcessOutput(self.exe, ['-u', scriptFile])
+ return d.addCallback(self.assertEqual, "hello world\n")
+
+
+ def test_outputWithErrorIgnored(self):
+ """
+ The L{Deferred} returned by L{getProcessOutput} is fired with an
+ L{IOError} L{Failure} if the child process writes to stderr.
+ """
+ # make sure stderr raises an error normally
+ scriptFile = self.makeSourceFile([
+ 'import sys',
+ 'sys.stderr.write("hello world\\n")'
+ ])
+
+ d = utils.getProcessOutput(self.exe, ['-u', scriptFile])
+ d = self.assertFailure(d, IOError)
+ def cbFailed(err):
+ return self.assertFailure(err.processEnded, error.ProcessDone)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_outputWithErrorCollected(self):
+ """
+ If a C{True} value is supplied for the C{errortoo} parameter to
+ L{getProcessOutput}, the returned L{Deferred} fires with the child's
+ stderr output as well as its stdout output.
+ """
+ scriptFile = self.makeSourceFile([
+ 'import sys',
+ # Write the same value to both because ordering isn't guaranteed so
+ # this simplifies the test.
+ 'sys.stdout.write("foo")',
+ 'sys.stdout.flush()',
+ 'sys.stderr.write("foo")',
+ 'sys.stderr.flush()'])
+
+ d = utils.getProcessOutput(self.exe, ['-u', scriptFile], errortoo=True)
+ return d.addCallback(self.assertEqual, "foofoo")
+
+
+ def test_value(self):
+ """
+ The L{Deferred} returned by L{getProcessValue} is fired with the exit
+ status of the child process.
+ """
+ scriptFile = self.makeSourceFile(["raise SystemExit(1)"])
+
+ d = utils.getProcessValue(self.exe, ['-u', scriptFile])
+ return d.addCallback(self.assertEqual, 1)
+
+
+ def test_outputAndValue(self):
+ """
+ The L{Deferred} returned by L{getProcessOutputAndValue} fires with a
+ three-tuple, the elements of which give the data written to the child's
+ stdout, the data written to the child's stderr, and the exit status of
+ the child.
+ """
+ exe = sys.executable
+ scriptFile = self.makeSourceFile([
+ "import sys",
+ "sys.stdout.write('hello world!\\n')",
+ "sys.stderr.write('goodbye world!\\n')",
+ "sys.exit(1)"
+ ])
+
+ def gotOutputAndValue((out, err, code)):
+ self.assertEqual(out, "hello world!\n")
+ self.assertEqual(err, "goodbye world!" + os.linesep)
+ self.assertEqual(code, 1)
+ d = utils.getProcessOutputAndValue(self.exe, ["-u", scriptFile])
+ return d.addCallback(gotOutputAndValue)
+
+
+ def test_outputSignal(self):
+ """
+ If the child process exits because of a signal, the L{Deferred}
+ returned by L{getProcessOutputAndValue} fires a L{Failure} of a tuple
+ containing the the child's stdout, stderr, and the signal which caused
+ it to exit.
+ """
+ # Use SIGKILL here because it's guaranteed to be delivered. Using
+ # SIGHUP might not work in, e.g., a buildbot slave run under the
+ # 'nohup' command.
+ scriptFile = self.makeSourceFile([
+ "import sys, os, signal",
+ "sys.stdout.write('stdout bytes\\n')",
+ "sys.stderr.write('stderr bytes\\n')",
+ "sys.stdout.flush()",
+ "sys.stderr.flush()",
+ "os.kill(os.getpid(), signal.SIGKILL)"])
+
+ def gotOutputAndValue((out, err, sig)):
+ self.assertEqual(out, "stdout bytes\n")
+ self.assertEqual(err, "stderr bytes\n")
+ self.assertEqual(sig, signal.SIGKILL)
+
+ d = utils.getProcessOutputAndValue(self.exe, ['-u', scriptFile])
+ d = self.assertFailure(d, tuple)
+ return d.addCallback(gotOutputAndValue)
+
+ if platform.isWindows():
+ test_outputSignal.skip = "Windows doesn't have real signals."
+
+
+ def _pathTest(self, utilFunc, check):
+ dir = os.path.abspath(self.mktemp())
+ os.makedirs(dir)
+ scriptFile = self.makeSourceFile([
+ "import os, sys",
+ "sys.stdout.write(os.getcwd())"])
+ d = utilFunc(self.exe, ['-u', scriptFile], path=dir)
+ d.addCallback(check, dir)
+ return d
+
+
+ def test_getProcessOutputPath(self):
+ """
+ L{getProcessOutput} runs the given command with the working directory
+ given by the C{path} parameter.
+ """
+ return self._pathTest(utils.getProcessOutput, self.assertEqual)
+
+
+ def test_getProcessValuePath(self):
+ """
+ L{getProcessValue} runs the given command with the working directory
+ given by the C{path} parameter.
+ """
+ def check(result, ignored):
+ self.assertEqual(result, 0)
+ return self._pathTest(utils.getProcessValue, check)
+
+
+ def test_getProcessOutputAndValuePath(self):
+ """
+ L{getProcessOutputAndValue} runs the given command with the working
+ directory given by the C{path} parameter.
+ """
+ def check((out, err, status), dir):
+ self.assertEqual(out, dir)
+ self.assertEqual(status, 0)
+ return self._pathTest(utils.getProcessOutputAndValue, check)
+
+
+ def _defaultPathTest(self, utilFunc, check):
+ # Make another directory to mess around with.
+ dir = os.path.abspath(self.mktemp())
+ os.makedirs(dir)
+
+ scriptFile = self.makeSourceFile([
+ "import os, sys, stat",
+ # Fix the permissions so we can report the working directory.
+ # On OS X (and maybe elsewhere), os.getcwd() fails with EACCES
+ # if +x is missing from the working directory.
+ "os.chmod(%r, stat.S_IXUSR)" % (dir,),
+ "sys.stdout.write(os.getcwd())"])
+
+ # Switch to it, but make sure we switch back
+ self.addCleanup(os.chdir, os.getcwd())
+ os.chdir(dir)
+
+ # Get rid of all its permissions, but make sure they get cleaned up
+ # later, because otherwise it might be hard to delete the trial
+ # temporary directory.
+ self.addCleanup(
+ os.chmod, dir, stat.S_IMODE(os.stat('.').st_mode))
+ os.chmod(dir, 0)
+
+ d = utilFunc(self.exe, ['-u', scriptFile])
+ d.addCallback(check, dir)
+ return d
+
+
+ def test_getProcessOutputDefaultPath(self):
+ """
+ If no value is supplied for the C{path} parameter, L{getProcessOutput}
+ runs the given command in the same working directory as the parent
+ process and succeeds even if the current working directory is not
+ accessible.
+ """
+ return self._defaultPathTest(utils.getProcessOutput, self.assertEqual)
+
+
+ def test_getProcessValueDefaultPath(self):
+ """
+ If no value is supplied for the C{path} parameter, L{getProcessValue}
+ runs the given command in the same working directory as the parent
+ process and succeeds even if the current working directory is not
+ accessible.
+ """
+ def check(result, ignored):
+ self.assertEqual(result, 0)
+ return self._defaultPathTest(utils.getProcessValue, check)
+
+
+ def test_getProcessOutputAndValueDefaultPath(self):
+ """
+ If no value is supplied for the C{path} parameter,
+ L{getProcessOutputAndValue} runs the given command in the same working
+ directory as the parent process and succeeds even if the current
+ working directory is not accessible.
+ """
+ def check((out, err, status), dir):
+ self.assertEqual(out, dir)
+ self.assertEqual(status, 0)
+ return self._defaultPathTest(
+ utils.getProcessOutputAndValue, check)
+
+
+
+class WarningSuppression(unittest.TestCase):
+ def setUp(self):
+ self.warnings = []
+ self.originalshow = warnings.showwarning
+ warnings.showwarning = self.showwarning
+
+
+ def tearDown(self):
+ warnings.showwarning = self.originalshow
+
+
+ def showwarning(self, *a, **kw):
+ self.warnings.append((a, kw))
+
+
+ def testSuppressWarnings(self):
+ def f(msg):
+ warnings.warn(msg)
+ g = utils.suppressWarnings(f, (('ignore',), dict(message="This is message")))
+
+ # Start off with a sanity check - calling the original function
+ # should emit the warning.
+ f("Sanity check message")
+ self.assertEqual(len(self.warnings), 1)
+
+ # Now that that's out of the way, call the wrapped function, and
+ # make sure no new warnings show up.
+ g("This is message")
+ self.assertEqual(len(self.warnings), 1)
+
+ # Finally, emit another warning which should not be ignored, and
+ # make sure it is not.
+ g("Unignored message")
+ self.assertEqual(len(self.warnings), 2)
diff --git a/twisted/test/test_jelly.py b/twisted/test/test_jelly.py
new file mode 100644
index 0000000..132e05f
--- /dev/null
+++ b/twisted/test/test_jelly.py
@@ -0,0 +1,671 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for L{jelly} object serialization.
+"""
+
+import datetime
+
+try:
+ import decimal
+except ImportError:
+ decimal = None
+
+from twisted.spread import jelly, pb
+from twisted.python.compat import set, frozenset
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+
+
+class TestNode(object, jelly.Jellyable):
+ """
+ An object to test jellyfying of new style class instances.
+ """
+ classAttr = 4
+
+ def __init__(self, parent=None):
+ if parent:
+ self.id = parent.id + 1
+ parent.children.append(self)
+ else:
+ self.id = 1
+ self.parent = parent
+ self.children = []
+
+
+
+class A:
+ """
+ Dummy class.
+ """
+
+ def amethod(self):
+ """
+ Method tp be used in serialization tests.
+ """
+
+
+
+def afunc(self):
+ """
+ A dummy function to test function serialization.
+ """
+
+
+
+class B:
+ """
+ Dummy class.
+ """
+
+ def bmethod(self):
+ """
+ Method to be used in serialization tests.
+ """
+
+
+
+class C:
+ """
+ Dummy class.
+ """
+
+ def cmethod(self):
+ """
+ Method to be used in serialization tests.
+ """
+
+
+
+class D(object):
+ """
+ Dummy new-style class.
+ """
+
+
+
+class E(object):
+ """
+ Dummy new-style class with slots.
+ """
+
+ __slots__ = ("x", "y")
+
+ def __init__(self, x=None, y=None):
+ self.x = x
+ self.y = y
+
+
+ def __getstate__(self):
+ return {"x" : self.x, "y" : self.y}
+
+
+ def __setstate__(self, state):
+ self.x = state["x"]
+ self.y = state["y"]
+
+
+
+class SimpleJellyTest:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def isTheSameAs(self, other):
+ return self.__dict__ == other.__dict__
+
+
+
+class JellyTestCase(unittest.TestCase):
+ """
+ Testcases for L{jelly} module serialization.
+
+ @cvar decimalData: serialized version of decimal data, to be used in tests.
+ @type decimalData: C{list}
+ """
+
+ def _testSecurity(self, inputList, atom):
+ """
+ Helper test method to test security options for a type.
+
+ @param inputList: a sample input for the type.
+ @param inputList: C{list}
+
+ @param atom: atom identifier for the type.
+ @type atom: C{str}
+ """
+ c = jelly.jelly(inputList)
+ taster = jelly.SecurityOptions()
+ taster.allowBasicTypes()
+ # By default, it should succeed
+ jelly.unjelly(c, taster)
+ taster.allowedTypes.pop(atom)
+ # But it should raise an exception when disallowed
+ self.assertRaises(jelly.InsecureJelly, jelly.unjelly, c, taster)
+
+
+ def test_methodSelfIdentity(self):
+ a = A()
+ b = B()
+ a.bmethod = b.bmethod
+ b.a = a
+ im_ = jelly.unjelly(jelly.jelly(b)).a.bmethod
+ self.assertEqual(im_.im_class, im_.im_self.__class__)
+
+
+ def test_methodsNotSelfIdentity(self):
+ """
+ If a class change after an instance has been created, L{jelly.unjelly}
+ shoud raise a C{TypeError} when trying to unjelly the instance.
+ """
+ a = A()
+ b = B()
+ c = C()
+ a.bmethod = c.cmethod
+ b.a = a
+ savecmethod = C.cmethod
+ del C.cmethod
+ try:
+ self.assertRaises(TypeError, jelly.unjelly, jelly.jelly(b))
+ finally:
+ C.cmethod = savecmethod
+
+
+ def test_newStyle(self):
+ n = D()
+ n.x = 1
+ n2 = D()
+ n.n2 = n2
+ n.n3 = n2
+ c = jelly.jelly(n)
+ m = jelly.unjelly(c)
+ self.assertIsInstance(m, D)
+ self.assertIdentical(m.n2, m.n3)
+
+
+ def test_newStyleWithSlots(self):
+ """
+ A class defined with I{slots} can be jellied and unjellied with the
+ values for its attributes preserved.
+ """
+ n = E()
+ n.x = 1
+ c = jelly.jelly(n)
+ m = jelly.unjelly(c)
+ self.assertIsInstance(m, E)
+ self.assertEqual(n.x, 1)
+
+
+ def test_typeOldStyle(self):
+ """
+ Test that an old style class type can be jellied and unjellied
+ to the original type.
+ """
+ t = [C]
+ r = jelly.unjelly(jelly.jelly(t))
+ self.assertEqual(t, r)
+
+
+ def test_typeNewStyle(self):
+ """
+ Test that a new style class type can be jellied and unjellied
+ to the original type.
+ """
+ t = [D]
+ r = jelly.unjelly(jelly.jelly(t))
+ self.assertEqual(t, r)
+
+
+ def test_typeBuiltin(self):
+ """
+ Test that a builtin type can be jellied and unjellied to the original
+ type.
+ """
+ t = [str]
+ r = jelly.unjelly(jelly.jelly(t))
+ self.assertEqual(t, r)
+
+
+ def test_dateTime(self):
+ dtn = datetime.datetime.now()
+ dtd = datetime.datetime.now() - dtn
+ input = [dtn, dtd]
+ c = jelly.jelly(input)
+ output = jelly.unjelly(c)
+ self.assertEqual(input, output)
+ self.assertNotIdentical(input, output)
+
+
+ def test_decimal(self):
+ """
+ Jellying L{decimal.Decimal} instances and then unjellying the result
+ should produce objects which represent the values of the original
+ inputs.
+ """
+ inputList = [decimal.Decimal('9.95'),
+ decimal.Decimal(0),
+ decimal.Decimal(123456),
+ decimal.Decimal('-78.901')]
+ c = jelly.jelly(inputList)
+ output = jelly.unjelly(c)
+ self.assertEqual(inputList, output)
+ self.assertNotIdentical(inputList, output)
+
+
+ decimalData = ['list', ['decimal', 995, -2], ['decimal', 0, 0],
+ ['decimal', 123456, 0], ['decimal', -78901, -3]]
+
+
+ def test_decimalUnjelly(self):
+ """
+ Unjellying the s-expressions produced by jelly for L{decimal.Decimal}
+ instances should result in L{decimal.Decimal} instances with the values
+ represented by the s-expressions.
+
+ This test also verifies that C{self.decimalData} contains valid jellied
+ data. This is important since L{test_decimalMissing} re-uses
+ C{self.decimalData} and is expected to be unable to produce
+ L{decimal.Decimal} instances even though the s-expression correctly
+ represents a list of them.
+ """
+ expected = [decimal.Decimal('9.95'),
+ decimal.Decimal(0),
+ decimal.Decimal(123456),
+ decimal.Decimal('-78.901')]
+ output = jelly.unjelly(self.decimalData)
+ self.assertEqual(output, expected)
+
+
+ def test_decimalMissing(self):
+ """
+ If decimal is unavailable on the unjelly side, L{jelly.unjelly} should
+ gracefully return L{jelly.Unpersistable} objects.
+ """
+ self.patch(jelly, 'decimal', None)
+ output = jelly.unjelly(self.decimalData)
+ self.assertEqual(len(output), 4)
+ for i in range(4):
+ self.assertIsInstance(output[i], jelly.Unpersistable)
+ self.assertEqual(output[0].reason,
+ "Could not unpersist decimal: 9.95")
+ self.assertEqual(output[1].reason,
+ "Could not unpersist decimal: 0")
+ self.assertEqual(output[2].reason,
+ "Could not unpersist decimal: 123456")
+ self.assertEqual(output[3].reason,
+ "Could not unpersist decimal: -78.901")
+
+
+ def test_decimalSecurity(self):
+ """
+ By default, C{decimal} objects should be allowed by
+ L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
+ L{jelly.InsecureJelly} when trying to unjelly it.
+ """
+ inputList = [decimal.Decimal('9.95')]
+ self._testSecurity(inputList, "decimal")
+
+ if decimal is None:
+ skipReason = "decimal not available"
+ test_decimal.skip = skipReason
+ test_decimalUnjelly.skip = skipReason
+ test_decimalSecurity.skip = skipReason
+
+
+ def test_set(self):
+ """
+ Jellying C{set} instances and then unjellying the result
+ should produce objects which represent the values of the original
+ inputs.
+ """
+ inputList = [set([1, 2, 3])]
+ output = jelly.unjelly(jelly.jelly(inputList))
+ self.assertEqual(inputList, output)
+ self.assertNotIdentical(inputList, output)
+
+
+ def test_frozenset(self):
+ """
+ Jellying C{frozenset} instances and then unjellying the result
+ should produce objects which represent the values of the original
+ inputs.
+ """
+ inputList = [frozenset([1, 2, 3])]
+ output = jelly.unjelly(jelly.jelly(inputList))
+ self.assertEqual(inputList, output)
+ self.assertNotIdentical(inputList, output)
+
+
+ def test_setSecurity(self):
+ """
+ By default, C{set} objects should be allowed by
+ L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
+ L{jelly.InsecureJelly} when trying to unjelly it.
+ """
+ inputList = [set([1, 2, 3])]
+ self._testSecurity(inputList, "set")
+
+
+ def test_frozensetSecurity(self):
+ """
+ By default, C{frozenset} objects should be allowed by
+ L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
+ L{jelly.InsecureJelly} when trying to unjelly it.
+ """
+ inputList = [frozenset([1, 2, 3])]
+ self._testSecurity(inputList, "frozenset")
+
+
+ def test_oldSets(self):
+ """
+ Test jellying C{sets.Set}: it should serialize to the same thing as
+ C{set} jelly, and be unjellied as C{set} if available.
+ """
+ inputList = [jelly._sets.Set([1, 2, 3])]
+ inputJelly = jelly.jelly(inputList)
+ self.assertEqual(inputJelly, jelly.jelly([set([1, 2, 3])]))
+ output = jelly.unjelly(inputJelly)
+ # Even if the class is different, it should coerce to the same list
+ self.assertEqual(list(inputList[0]), list(output[0]))
+ if set is jelly._sets.Set:
+ self.assertIsInstance(output[0], jelly._sets.Set)
+ else:
+ self.assertIsInstance(output[0], set)
+
+
+ def test_oldImmutableSets(self):
+ """
+ Test jellying C{sets.ImmutableSet}: it should serialize to the same
+ thing as C{frozenset} jelly, and be unjellied as C{frozenset} if
+ available.
+ """
+ inputList = [jelly._sets.ImmutableSet([1, 2, 3])]
+ inputJelly = jelly.jelly(inputList)
+ self.assertEqual(inputJelly, jelly.jelly([frozenset([1, 2, 3])]))
+ output = jelly.unjelly(inputJelly)
+ # Even if the class is different, it should coerce to the same list
+ self.assertEqual(list(inputList[0]), list(output[0]))
+ if frozenset is jelly._sets.ImmutableSet:
+ self.assertIsInstance(output[0], jelly._sets.ImmutableSet)
+ else:
+ self.assertIsInstance(output[0], frozenset)
+
+
+ def test_simple(self):
+ """
+ Simplest test case.
+ """
+ self.failUnless(SimpleJellyTest('a', 'b').isTheSameAs(
+ SimpleJellyTest('a', 'b')))
+ a = SimpleJellyTest(1, 2)
+ cereal = jelly.jelly(a)
+ b = jelly.unjelly(cereal)
+ self.failUnless(a.isTheSameAs(b))
+
+
+ def test_identity(self):
+ """
+ Test to make sure that objects retain identity properly.
+ """
+ x = []
+ y = (x)
+ x.append(y)
+ x.append(y)
+ self.assertIdentical(x[0], x[1])
+ self.assertIdentical(x[0][0], x)
+ s = jelly.jelly(x)
+ z = jelly.unjelly(s)
+ self.assertIdentical(z[0], z[1])
+ self.assertIdentical(z[0][0], z)
+
+
+ def test_unicode(self):
+ x = unicode('blah')
+ y = jelly.unjelly(jelly.jelly(x))
+ self.assertEqual(x, y)
+ self.assertEqual(type(x), type(y))
+
+
+ def test_stressReferences(self):
+ reref = []
+ toplevelTuple = ({'list': reref}, reref)
+ reref.append(toplevelTuple)
+ s = jelly.jelly(toplevelTuple)
+ z = jelly.unjelly(s)
+ self.assertIdentical(z[0]['list'], z[1])
+ self.assertIdentical(z[0]['list'][0], z)
+
+
+ def test_moreReferences(self):
+ a = []
+ t = (a,)
+ a.append((t,))
+ s = jelly.jelly(t)
+ z = jelly.unjelly(s)
+ self.assertIdentical(z[0][0][0], z)
+
+
+ def test_typeSecurity(self):
+ """
+ Test for type-level security of serialization.
+ """
+ taster = jelly.SecurityOptions()
+ dct = jelly.jelly({})
+ self.assertRaises(jelly.InsecureJelly, jelly.unjelly, dct, taster)
+
+
+ def test_newStyleClasses(self):
+ j = jelly.jelly(D)
+ uj = jelly.unjelly(D)
+ self.assertIdentical(D, uj)
+
+
+ def test_lotsaTypes(self):
+ """
+ Test for all types currently supported in jelly
+ """
+ a = A()
+ jelly.unjelly(jelly.jelly(a))
+ jelly.unjelly(jelly.jelly(a.amethod))
+ items = [afunc, [1, 2, 3], not bool(1), bool(1), 'test', 20.3,
+ (1, 2, 3), None, A, unittest, {'a': 1}, A.amethod]
+ for i in items:
+ self.assertEqual(i, jelly.unjelly(jelly.jelly(i)))
+
+
+ def test_setState(self):
+ global TupleState
+ class TupleState:
+ def __init__(self, other):
+ self.other = other
+ def __getstate__(self):
+ return (self.other,)
+ def __setstate__(self, state):
+ self.other = state[0]
+ def __hash__(self):
+ return hash(self.other)
+ a = A()
+ t1 = TupleState(a)
+ t2 = TupleState(a)
+ t3 = TupleState((t1, t2))
+ d = {t1: t1, t2: t2, t3: t3, "t3": t3}
+ t3prime = jelly.unjelly(jelly.jelly(d))["t3"]
+ self.assertIdentical(t3prime.other[0].other, t3prime.other[1].other)
+
+
+ def test_classSecurity(self):
+ """
+ Test for class-level security of serialization.
+ """
+ taster = jelly.SecurityOptions()
+ taster.allowInstancesOf(A, B)
+ a = A()
+ b = B()
+ c = C()
+ # add a little complexity to the data
+ a.b = b
+ a.c = c
+ # and a backreference
+ a.x = b
+ b.c = c
+ # first, a friendly insecure serialization
+ friendly = jelly.jelly(a, taster)
+ x = jelly.unjelly(friendly, taster)
+ self.assertIsInstance(x.c, jelly.Unpersistable)
+ # now, a malicious one
+ mean = jelly.jelly(a)
+ self.assertRaises(jelly.InsecureJelly, jelly.unjelly, mean, taster)
+ self.assertIdentical(x.x, x.b, "Identity mismatch")
+ # test class serialization
+ friendly = jelly.jelly(A, taster)
+ x = jelly.unjelly(friendly, taster)
+ self.assertIdentical(x, A, "A came back: %s" % x)
+
+
+ def test_unjellyable(self):
+ """
+ Test that if Unjellyable is used to deserialize a jellied object,
+ state comes out right.
+ """
+ class JellyableTestClass(jelly.Jellyable):
+ pass
+ jelly.setUnjellyableForClass(JellyableTestClass, jelly.Unjellyable)
+ input = JellyableTestClass()
+ input.attribute = 'value'
+ output = jelly.unjelly(jelly.jelly(input))
+ self.assertEqual(output.attribute, 'value')
+ self.assertIsInstance(output, jelly.Unjellyable)
+
+
+ def test_persistentStorage(self):
+ perst = [{}, 1]
+ def persistentStore(obj, jel, perst = perst):
+ perst[1] = perst[1] + 1
+ perst[0][perst[1]] = obj
+ return str(perst[1])
+
+ def persistentLoad(pidstr, unj, perst = perst):
+ pid = int(pidstr)
+ return perst[0][pid]
+
+ a = SimpleJellyTest(1, 2)
+ b = SimpleJellyTest(3, 4)
+ c = SimpleJellyTest(5, 6)
+
+ a.b = b
+ a.c = c
+ c.b = b
+
+ jel = jelly.jelly(a, persistentStore = persistentStore)
+ x = jelly.unjelly(jel, persistentLoad = persistentLoad)
+
+ self.assertIdentical(x.b, x.c.b)
+ self.failUnless(perst[0], "persistentStore was not called.")
+ self.assertIdentical(x.b, a.b, "Persistent storage identity failure.")
+
+
+ def test_newStyleClassesAttributes(self):
+ n = TestNode()
+ n1 = TestNode(n)
+ n11 = TestNode(n1)
+ n2 = TestNode(n)
+ # Jelly it
+ jel = jelly.jelly(n)
+ m = jelly.unjelly(jel)
+ # Check that it has been restored ok
+ self._check_newstyle(n, m)
+
+
+ def _check_newstyle(self, a, b):
+ self.assertEqual(a.id, b.id)
+ self.assertEqual(a.classAttr, 4)
+ self.assertEqual(b.classAttr, 4)
+ self.assertEqual(len(a.children), len(b.children))
+ for x, y in zip(a.children, b.children):
+ self._check_newstyle(x, y)
+
+
+ def test_referenceable(self):
+ """
+ A L{pb.Referenceable} instance jellies to a structure which unjellies to
+ a L{pb.RemoteReference}. The C{RemoteReference} has a I{luid} that
+ matches up with the local object key in the L{pb.Broker} which sent the
+ L{Referenceable}.
+ """
+ ref = pb.Referenceable()
+ jellyBroker = pb.Broker()
+ jellyBroker.makeConnection(StringTransport())
+ j = jelly.jelly(ref, invoker=jellyBroker)
+
+ unjellyBroker = pb.Broker()
+ unjellyBroker.makeConnection(StringTransport())
+
+ uj = jelly.unjelly(j, invoker=unjellyBroker)
+ self.assertIn(uj.luid, jellyBroker.localObjects)
+
+
+
+class ClassA(pb.Copyable, pb.RemoteCopy):
+ def __init__(self):
+ self.ref = ClassB(self)
+
+
+
+class ClassB(pb.Copyable, pb.RemoteCopy):
+ def __init__(self, ref):
+ self.ref = ref
+
+
+
+class CircularReferenceTestCase(unittest.TestCase):
+ """
+ Tests for circular references handling in the jelly/unjelly process.
+ """
+
+ def test_simpleCircle(self):
+ jelly.setUnjellyableForClass(ClassA, ClassA)
+ jelly.setUnjellyableForClass(ClassB, ClassB)
+ a = jelly.unjelly(jelly.jelly(ClassA()))
+ self.assertIdentical(a.ref.ref, a,
+ "Identity not preserved in circular reference")
+
+
+ def test_circleWithInvoker(self):
+ class DummyInvokerClass:
+ pass
+ dummyInvoker = DummyInvokerClass()
+ dummyInvoker.serializingPerspective = None
+ a0 = ClassA()
+ jelly.setUnjellyableForClass(ClassA, ClassA)
+ jelly.setUnjellyableForClass(ClassB, ClassB)
+ j = jelly.jelly(a0, invoker=dummyInvoker)
+ a1 = jelly.unjelly(j)
+ self.failUnlessIdentical(a1.ref.ref, a1,
+ "Identity not preserved in circular reference")
+
+
+ def test_set(self):
+ """
+ Check that a C{set} can contain a circular reference and be serialized
+ and unserialized without losing the reference.
+ """
+ s = set()
+ a = SimpleJellyTest(s, None)
+ s.add(a)
+ res = jelly.unjelly(jelly.jelly(a))
+ self.assertIsInstance(res.x, set)
+ self.assertEqual(list(res.x), [res])
+
+
+ def test_frozenset(self):
+ """
+ Check that a C{frozenset} can contain a circular reference and be
+ serializeserialized without losing the reference.
+ """
+ a = SimpleJellyTest(None, None)
+ s = frozenset([a])
+ a.x = s
+ res = jelly.unjelly(jelly.jelly(a))
+ self.assertIsInstance(res.x, frozenset)
+ self.assertEqual(list(res.x), [res])
diff --git a/twisted/test/test_lockfile.py b/twisted/test/test_lockfile.py
new file mode 100644
index 0000000..41cfb65
--- /dev/null
+++ b/twisted/test/test_lockfile.py
@@ -0,0 +1,445 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.lockfile}.
+"""
+
+import os, errno
+
+from twisted.trial import unittest
+from twisted.python import lockfile
+from twisted.python.runtime import platform
+
+skipKill = None
+if platform.isWindows():
+ try:
+ from win32api import OpenProcess
+ import pywintypes
+ except ImportError:
+ skipKill = ("On windows, lockfile.kill is not implemented in the "
+ "absence of win32api and/or pywintypes.")
+
+class UtilTests(unittest.TestCase):
+ """
+ Tests for the helper functions used to implement L{FilesystemLock}.
+ """
+ def test_symlinkEEXIST(self):
+ """
+ L{lockfile.symlink} raises L{OSError} with C{errno} set to L{EEXIST}
+ when an attempt is made to create a symlink which already exists.
+ """
+ name = self.mktemp()
+ lockfile.symlink('foo', name)
+ exc = self.assertRaises(OSError, lockfile.symlink, 'foo', name)
+ self.assertEqual(exc.errno, errno.EEXIST)
+
+
+ def test_symlinkEIOWindows(self):
+ """
+ L{lockfile.symlink} raises L{OSError} with C{errno} set to L{EIO} when
+ the underlying L{rename} call fails with L{EIO}.
+
+ Renaming a file on Windows may fail if the target of the rename is in
+ the process of being deleted (directory deletion appears not to be
+ atomic).
+ """
+ name = self.mktemp()
+ def fakeRename(src, dst):
+ raise IOError(errno.EIO, None)
+ self.patch(lockfile, 'rename', fakeRename)
+ exc = self.assertRaises(IOError, lockfile.symlink, name, "foo")
+ self.assertEqual(exc.errno, errno.EIO)
+ if not platform.isWindows():
+ test_symlinkEIOWindows.skip = (
+ "special rename EIO handling only necessary and correct on "
+ "Windows.")
+
+
+ def test_readlinkENOENT(self):
+ """
+ L{lockfile.readlink} raises L{OSError} with C{errno} set to L{ENOENT}
+ when an attempt is made to read a symlink which does not exist.
+ """
+ name = self.mktemp()
+ exc = self.assertRaises(OSError, lockfile.readlink, name)
+ self.assertEqual(exc.errno, errno.ENOENT)
+
+
+ def test_readlinkEACCESWindows(self):
+ """
+ L{lockfile.readlink} raises L{OSError} with C{errno} set to L{EACCES}
+ on Windows when the underlying file open attempt fails with C{EACCES}.
+
+ Opening a file on Windows may fail if the path is inside a directory
+ which is in the process of being deleted (directory deletion appears
+ not to be atomic).
+ """
+ name = self.mktemp()
+ def fakeOpen(path, mode):
+ raise IOError(errno.EACCES, None)
+ self.patch(lockfile, '_open', fakeOpen)
+ exc = self.assertRaises(IOError, lockfile.readlink, name)
+ self.assertEqual(exc.errno, errno.EACCES)
+ if not platform.isWindows():
+ test_readlinkEACCESWindows.skip = (
+ "special readlink EACCES handling only necessary and correct on "
+ "Windows.")
+
+
+ def test_kill(self):
+ """
+ L{lockfile.kill} returns without error if passed the PID of a
+ process which exists and signal C{0}.
+ """
+ lockfile.kill(os.getpid(), 0)
+ test_kill.skip = skipKill
+
+
+ def test_killESRCH(self):
+ """
+ L{lockfile.kill} raises L{OSError} with errno of L{ESRCH} if
+ passed a PID which does not correspond to any process.
+ """
+ # Hopefully there is no process with PID 2 ** 31 - 1
+ exc = self.assertRaises(OSError, lockfile.kill, 2 ** 31 - 1, 0)
+ self.assertEqual(exc.errno, errno.ESRCH)
+ test_killESRCH.skip = skipKill
+
+
+ def test_noKillCall(self):
+ """
+ Verify that when L{lockfile.kill} does end up as None (e.g. on Windows
+ without pywin32), it doesn't end up being called and raising a
+ L{TypeError}.
+ """
+ self.patch(lockfile, "kill", None)
+ fl = lockfile.FilesystemLock(self.mktemp())
+ fl.lock()
+ self.assertFalse(fl.lock())
+
+
+
+class LockingTestCase(unittest.TestCase):
+ def _symlinkErrorTest(self, errno):
+ def fakeSymlink(source, dest):
+ raise OSError(errno, None)
+ self.patch(lockfile, 'symlink', fakeSymlink)
+
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ exc = self.assertRaises(OSError, lock.lock)
+ self.assertEqual(exc.errno, errno)
+
+
+ def test_symlinkError(self):
+ """
+ An exception raised by C{symlink} other than C{EEXIST} is passed up to
+ the caller of L{FilesystemLock.lock}.
+ """
+ self._symlinkErrorTest(errno.ENOSYS)
+
+
+ def test_symlinkErrorPOSIX(self):
+ """
+ An L{OSError} raised by C{symlink} on a POSIX platform with an errno of
+ C{EACCES} or C{EIO} is passed to the caller of L{FilesystemLock.lock}.
+
+ On POSIX, unlike on Windows, these are unexpected errors which cannot
+ be handled by L{FilesystemLock}.
+ """
+ self._symlinkErrorTest(errno.EACCES)
+ self._symlinkErrorTest(errno.EIO)
+ if platform.isWindows():
+ test_symlinkErrorPOSIX.skip = (
+ "POSIX-specific error propagation not expected on Windows.")
+
+
+ def test_cleanlyAcquire(self):
+ """
+ If the lock has never been held, it can be acquired and the C{clean}
+ and C{locked} attributes are set to C{True}.
+ """
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(lock.lock())
+ self.assertTrue(lock.clean)
+ self.assertTrue(lock.locked)
+
+
+ def test_cleanlyRelease(self):
+ """
+ If a lock is released cleanly, it can be re-acquired and the C{clean}
+ and C{locked} attributes are set to C{True}.
+ """
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(lock.lock())
+ lock.unlock()
+ self.assertFalse(lock.locked)
+
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(lock.lock())
+ self.assertTrue(lock.clean)
+ self.assertTrue(lock.locked)
+
+
+ def test_cannotLockLocked(self):
+ """
+ If a lock is currently locked, it cannot be locked again.
+ """
+ lockf = self.mktemp()
+ firstLock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(firstLock.lock())
+
+ secondLock = lockfile.FilesystemLock(lockf)
+ self.assertFalse(secondLock.lock())
+ self.assertFalse(secondLock.locked)
+
+
+ def test_uncleanlyAcquire(self):
+ """
+ If a lock was held by a process which no longer exists, it can be
+ acquired, the C{clean} attribute is set to C{False}, and the
+ C{locked} attribute is set to C{True}.
+ """
+ owner = 12345
+
+ def fakeKill(pid, signal):
+ if signal != 0:
+ raise OSError(errno.EPERM, None)
+ if pid == owner:
+ raise OSError(errno.ESRCH, None)
+
+ lockf = self.mktemp()
+ self.patch(lockfile, 'kill', fakeKill)
+ lockfile.symlink(str(owner), lockf)
+
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(lock.lock())
+ self.assertFalse(lock.clean)
+ self.assertTrue(lock.locked)
+
+ self.assertEqual(lockfile.readlink(lockf), str(os.getpid()))
+
+
+ def test_lockReleasedBeforeCheck(self):
+ """
+ If the lock is initially held but then released before it can be
+ examined to determine if the process which held it still exists, it is
+ acquired and the C{clean} and C{locked} attributes are set to C{True}.
+ """
+ def fakeReadlink(name):
+ # Pretend to be another process releasing the lock.
+ lockfile.rmlink(lockf)
+ # Fall back to the real implementation of readlink.
+ readlinkPatch.restore()
+ return lockfile.readlink(name)
+ readlinkPatch = self.patch(lockfile, 'readlink', fakeReadlink)
+
+ def fakeKill(pid, signal):
+ if signal != 0:
+ raise OSError(errno.EPERM, None)
+ if pid == 43125:
+ raise OSError(errno.ESRCH, None)
+ self.patch(lockfile, 'kill', fakeKill)
+
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ lockfile.symlink(str(43125), lockf)
+ self.assertTrue(lock.lock())
+ self.assertTrue(lock.clean)
+ self.assertTrue(lock.locked)
+
+
+ def test_lockReleasedDuringAcquireSymlink(self):
+ """
+ If the lock is released while an attempt is made to acquire
+ it, the lock attempt fails and C{FilesystemLock.lock} returns
+ C{False}. This can happen on Windows when L{lockfile.symlink}
+ fails with L{IOError} of C{EIO} because another process is in
+ the middle of a call to L{os.rmdir} (implemented in terms of
+ RemoveDirectory) which is not atomic.
+ """
+ def fakeSymlink(src, dst):
+ # While another process id doing os.rmdir which the Windows
+ # implementation of rmlink does, a rename call will fail with EIO.
+ raise OSError(errno.EIO, None)
+
+ self.patch(lockfile, 'symlink', fakeSymlink)
+
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertFalse(lock.lock())
+ self.assertFalse(lock.locked)
+ if not platform.isWindows():
+ test_lockReleasedDuringAcquireSymlink.skip = (
+ "special rename EIO handling only necessary and correct on "
+ "Windows.")
+
+
+ def test_lockReleasedDuringAcquireReadlink(self):
+ """
+ If the lock is initially held but is released while an attempt
+ is made to acquire it, the lock attempt fails and
+ L{FilesystemLock.lock} returns C{False}.
+ """
+ def fakeReadlink(name):
+ # While another process is doing os.rmdir which the
+ # Windows implementation of rmlink does, a readlink call
+ # will fail with EACCES.
+ raise IOError(errno.EACCES, None)
+ readlinkPatch = self.patch(lockfile, 'readlink', fakeReadlink)
+
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ lockfile.symlink(str(43125), lockf)
+ self.assertFalse(lock.lock())
+ self.assertFalse(lock.locked)
+ if not platform.isWindows():
+ test_lockReleasedDuringAcquireReadlink.skip = (
+ "special readlink EACCES handling only necessary and correct on "
+ "Windows.")
+
+
+ def _readlinkErrorTest(self, exceptionType, errno):
+ def fakeReadlink(name):
+ raise exceptionType(errno, None)
+ self.patch(lockfile, 'readlink', fakeReadlink)
+
+ lockf = self.mktemp()
+
+ # Make it appear locked so it has to use readlink
+ lockfile.symlink(str(43125), lockf)
+
+ lock = lockfile.FilesystemLock(lockf)
+ exc = self.assertRaises(exceptionType, lock.lock)
+ self.assertEqual(exc.errno, errno)
+ self.assertFalse(lock.locked)
+
+
+ def test_readlinkError(self):
+ """
+ An exception raised by C{readlink} other than C{ENOENT} is passed up to
+ the caller of L{FilesystemLock.lock}.
+ """
+ self._readlinkErrorTest(OSError, errno.ENOSYS)
+ self._readlinkErrorTest(IOError, errno.ENOSYS)
+
+
+ def test_readlinkErrorPOSIX(self):
+ """
+ Any L{IOError} raised by C{readlink} on a POSIX platform passed to the
+ caller of L{FilesystemLock.lock}.
+
+ On POSIX, unlike on Windows, these are unexpected errors which cannot
+ be handled by L{FilesystemLock}.
+ """
+ self._readlinkErrorTest(IOError, errno.ENOSYS)
+ self._readlinkErrorTest(IOError, errno.EACCES)
+ if platform.isWindows():
+ test_readlinkErrorPOSIX.skip = (
+ "POSIX-specific error propagation not expected on Windows.")
+
+
+ def test_lockCleanedUpConcurrently(self):
+ """
+ If a second process cleans up the lock after a first one checks the
+ lock and finds that no process is holding it, the first process does
+ not fail when it tries to clean up the lock.
+ """
+ def fakeRmlink(name):
+ rmlinkPatch.restore()
+ # Pretend to be another process cleaning up the lock.
+ lockfile.rmlink(lockf)
+ # Fall back to the real implementation of rmlink.
+ return lockfile.rmlink(name)
+ rmlinkPatch = self.patch(lockfile, 'rmlink', fakeRmlink)
+
+ def fakeKill(pid, signal):
+ if signal != 0:
+ raise OSError(errno.EPERM, None)
+ if pid == 43125:
+ raise OSError(errno.ESRCH, None)
+ self.patch(lockfile, 'kill', fakeKill)
+
+ lockf = self.mktemp()
+ lock = lockfile.FilesystemLock(lockf)
+ lockfile.symlink(str(43125), lockf)
+ self.assertTrue(lock.lock())
+ self.assertTrue(lock.clean)
+ self.assertTrue(lock.locked)
+
+
+ def test_rmlinkError(self):
+ """
+ An exception raised by L{rmlink} other than C{ENOENT} is passed up
+ to the caller of L{FilesystemLock.lock}.
+ """
+ def fakeRmlink(name):
+ raise OSError(errno.ENOSYS, None)
+ self.patch(lockfile, 'rmlink', fakeRmlink)
+
+ def fakeKill(pid, signal):
+ if signal != 0:
+ raise OSError(errno.EPERM, None)
+ if pid == 43125:
+ raise OSError(errno.ESRCH, None)
+ self.patch(lockfile, 'kill', fakeKill)
+
+ lockf = self.mktemp()
+
+ # Make it appear locked so it has to use readlink
+ lockfile.symlink(str(43125), lockf)
+
+ lock = lockfile.FilesystemLock(lockf)
+ exc = self.assertRaises(OSError, lock.lock)
+ self.assertEqual(exc.errno, errno.ENOSYS)
+ self.assertFalse(lock.locked)
+
+
+ def test_killError(self):
+ """
+ If L{kill} raises an exception other than L{OSError} with errno set to
+ C{ESRCH}, the exception is passed up to the caller of
+ L{FilesystemLock.lock}.
+ """
+ def fakeKill(pid, signal):
+ raise OSError(errno.EPERM, None)
+ self.patch(lockfile, 'kill', fakeKill)
+
+ lockf = self.mktemp()
+
+ # Make it appear locked so it has to use readlink
+ lockfile.symlink(str(43125), lockf)
+
+ lock = lockfile.FilesystemLock(lockf)
+ exc = self.assertRaises(OSError, lock.lock)
+ self.assertEqual(exc.errno, errno.EPERM)
+ self.assertFalse(lock.locked)
+
+
+ def test_unlockOther(self):
+ """
+ L{FilesystemLock.unlock} raises L{ValueError} if called for a lock
+ which is held by a different process.
+ """
+ lockf = self.mktemp()
+ lockfile.symlink(str(os.getpid() + 1), lockf)
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertRaises(ValueError, lock.unlock)
+
+
+ def test_isLocked(self):
+ """
+ L{isLocked} returns C{True} if the named lock is currently locked,
+ C{False} otherwise.
+ """
+ lockf = self.mktemp()
+ self.assertFalse(lockfile.isLocked(lockf))
+ lock = lockfile.FilesystemLock(lockf)
+ self.assertTrue(lock.lock())
+ self.assertTrue(lockfile.isLocked(lockf))
+ lock.unlock()
+ self.assertFalse(lockfile.isLocked(lockf))
diff --git a/twisted/test/test_log.py b/twisted/test/test_log.py
new file mode 100644
index 0000000..86f03d5
--- /dev/null
+++ b/twisted/test/test_log.py
@@ -0,0 +1,773 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.log}.
+"""
+
+import os, sys, time, logging, warnings, calendar
+from cStringIO import StringIO
+
+from twisted.trial import unittest
+
+from twisted.python import log, failure
+
+
+class FakeWarning(Warning):
+ """
+ A unique L{Warning} subclass used by tests for interactions of
+ L{twisted.python.log} with the L{warnings} module.
+ """
+
+
+
+class LogTest(unittest.TestCase):
+
+ def setUp(self):
+ self.catcher = []
+ self.observer = self.catcher.append
+ log.addObserver(self.observer)
+ self.addCleanup(log.removeObserver, self.observer)
+
+
+ def testObservation(self):
+ catcher = self.catcher
+ log.msg("test", testShouldCatch=True)
+ i = catcher.pop()
+ self.assertEqual(i["message"][0], "test")
+ self.assertEqual(i["testShouldCatch"], True)
+ self.failUnless(i.has_key("time"))
+ self.assertEqual(len(catcher), 0)
+
+
+ def testContext(self):
+ catcher = self.catcher
+ log.callWithContext({"subsystem": "not the default",
+ "subsubsystem": "a",
+ "other": "c"},
+ log.callWithContext,
+ {"subsubsystem": "b"}, log.msg, "foo", other="d")
+ i = catcher.pop()
+ self.assertEqual(i['subsubsystem'], 'b')
+ self.assertEqual(i['subsystem'], 'not the default')
+ self.assertEqual(i['other'], 'd')
+ self.assertEqual(i['message'][0], 'foo')
+
+ def testErrors(self):
+ for e, ig in [("hello world","hello world"),
+ (KeyError(), KeyError),
+ (failure.Failure(RuntimeError()), RuntimeError)]:
+ log.err(e)
+ i = self.catcher.pop()
+ self.assertEqual(i['isError'], 1)
+ self.flushLoggedErrors(ig)
+
+ def testErrorsWithWhy(self):
+ for e, ig in [("hello world","hello world"),
+ (KeyError(), KeyError),
+ (failure.Failure(RuntimeError()), RuntimeError)]:
+ log.err(e, 'foobar')
+ i = self.catcher.pop()
+ self.assertEqual(i['isError'], 1)
+ self.assertEqual(i['why'], 'foobar')
+ self.flushLoggedErrors(ig)
+
+
+ def test_erroneousErrors(self):
+ """
+ Exceptions raised by log observers are logged but the observer which
+ raised the exception remains registered with the publisher. These
+ exceptions do not prevent the event from being sent to other observers
+ registered with the publisher.
+ """
+ L1 = []
+ L2 = []
+ def broken(events):
+ 1 / 0
+
+ for observer in [L1.append, broken, L2.append]:
+ log.addObserver(observer)
+ self.addCleanup(log.removeObserver, observer)
+
+ for i in xrange(3):
+ # Reset the lists for simpler comparison.
+ L1[:] = []
+ L2[:] = []
+
+ # Send out the event which will break one of the observers.
+ log.msg("Howdy, y'all.")
+
+ # The broken observer should have caused this to be logged.
+ excs = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEqual(len(excs), 1)
+
+ # Both other observers should have seen the message.
+ self.assertEqual(len(L1), 2)
+ self.assertEqual(len(L2), 2)
+
+ # The order is slightly wrong here. The first event should be
+ # delivered to all observers; then, errors should be delivered.
+ self.assertEqual(L1[1]['message'], ("Howdy, y'all.",))
+ self.assertEqual(L2[0]['message'], ("Howdy, y'all.",))
+
+
+ def test_doubleErrorDoesNotRemoveObserver(self):
+ """
+ If logging causes an error, make sure that if logging the fact that
+ logging failed also causes an error, the log observer is not removed.
+ """
+ events = []
+ errors = []
+ publisher = log.LogPublisher()
+
+ class FailingObserver(object):
+ calls = 0
+ def log(self, msg, **kwargs):
+ # First call raises RuntimeError:
+ self.calls += 1
+ if self.calls < 2:
+ raise RuntimeError("Failure #%s" % (self.calls,))
+ else:
+ events.append(msg)
+
+ observer = FailingObserver()
+ publisher.addObserver(observer.log)
+ self.assertEqual(publisher.observers, [observer.log])
+
+ try:
+ # When observer throws, the publisher attempts to log the fact by
+ # calling self._err()... which also fails with recursion error:
+ oldError = publisher._err
+
+ def failingErr(failure, why, **kwargs):
+ errors.append(failure.value)
+ raise RuntimeError("Fake recursion error")
+
+ publisher._err = failingErr
+ publisher.msg("error in first observer")
+ finally:
+ publisher._err = oldError
+ # Observer should still exist; we do this in finally since before
+ # bug was fixed the test would fail due to uncaught exception, so
+ # we want failing assert too in that case:
+ self.assertEqual(publisher.observers, [observer.log])
+
+ # The next message should succeed:
+ publisher.msg("but this should succeed")
+
+ self.assertEqual(observer.calls, 2)
+ self.assertEqual(len(events), 1)
+ self.assertEqual(events[0]['message'], ("but this should succeed",))
+ self.assertEqual(len(errors), 1)
+ self.assertIsInstance(errors[0], RuntimeError)
+
+
+ def test_showwarning(self):
+ """
+ L{twisted.python.log.showwarning} emits the warning as a message
+ to the Twisted logging system.
+ """
+ publisher = log.LogPublisher()
+ publisher.addObserver(self.observer)
+
+ publisher.showwarning(
+ FakeWarning("unique warning message"), FakeWarning,
+ "warning-filename.py", 27)
+ event = self.catcher.pop()
+ self.assertEqual(
+ event['format'] % event,
+ 'warning-filename.py:27: twisted.test.test_log.FakeWarning: '
+ 'unique warning message')
+ self.assertEqual(self.catcher, [])
+
+ # Python 2.6 requires that any function used to override the
+ # warnings.showwarning API accept a "line" parameter or a
+ # deprecation warning is emitted.
+ publisher.showwarning(
+ FakeWarning("unique warning message"), FakeWarning,
+ "warning-filename.py", 27, line=object())
+ event = self.catcher.pop()
+ self.assertEqual(
+ event['format'] % event,
+ 'warning-filename.py:27: twisted.test.test_log.FakeWarning: '
+ 'unique warning message')
+ self.assertEqual(self.catcher, [])
+
+
+ def test_warningToFile(self):
+ """
+ L{twisted.python.log.showwarning} passes warnings with an explicit file
+ target on to the underlying Python warning system.
+ """
+ message = "another unique message"
+ category = FakeWarning
+ filename = "warning-filename.py"
+ lineno = 31
+
+ output = StringIO()
+ log.showwarning(message, category, filename, lineno, file=output)
+
+ self.assertEqual(
+ output.getvalue(),
+ warnings.formatwarning(message, category, filename, lineno))
+
+ # In Python 2.6, warnings.showwarning accepts a "line" argument which
+ # gives the source line the warning message is to include.
+ if sys.version_info >= (2, 6):
+ line = "hello world"
+ output = StringIO()
+ log.showwarning(message, category, filename, lineno, file=output,
+ line=line)
+
+ self.assertEqual(
+ output.getvalue(),
+ warnings.formatwarning(message, category, filename, lineno,
+ line))
+
+
+ def test_publisherReportsBrokenObserversPrivately(self):
+ """
+ Log publisher does not use the global L{log.err} when reporting broken
+ observers.
+ """
+ errors = []
+ def logError(eventDict):
+ if eventDict.get("isError"):
+ errors.append(eventDict["failure"].value)
+
+ def fail(eventDict):
+ raise RuntimeError("test_publisherLocalyReportsBrokenObservers")
+
+ publisher = log.LogPublisher()
+ publisher.addObserver(logError)
+ publisher.addObserver(fail)
+
+ publisher.msg("Hello!")
+ self.assertEqual(publisher.observers, [logError, fail])
+ self.assertEqual(len(errors), 1)
+ self.assertIsInstance(errors[0], RuntimeError)
+
+
+
+class FakeFile(list):
+ def write(self, bytes):
+ self.append(bytes)
+
+ def flush(self):
+ pass
+
+class EvilStr:
+ def __str__(self):
+ 1/0
+
+class EvilRepr:
+ def __str__(self):
+ return "Happy Evil Repr"
+ def __repr__(self):
+ 1/0
+
+class EvilReprStr(EvilStr, EvilRepr):
+ pass
+
+class LogPublisherTestCaseMixin:
+ def setUp(self):
+ """
+ Add a log observer which records log events in C{self.out}. Also,
+ make sure the default string encoding is ASCII so that
+ L{testSingleUnicode} can test the behavior of logging unencodable
+ unicode messages.
+ """
+ self.out = FakeFile()
+ self.lp = log.LogPublisher()
+ self.flo = log.FileLogObserver(self.out)
+ self.lp.addObserver(self.flo.emit)
+
+ try:
+ str(u'\N{VULGAR FRACTION ONE HALF}')
+ except UnicodeEncodeError:
+ # This is the behavior we want - don't change anything.
+ self._origEncoding = None
+ else:
+ reload(sys)
+ self._origEncoding = sys.getdefaultencoding()
+ sys.setdefaultencoding('ascii')
+
+
+ def tearDown(self):
+ """
+ Verify that everything written to the fake file C{self.out} was a
+ C{str}. Also, restore the default string encoding to its previous
+ setting, if it was modified by L{setUp}.
+ """
+ for chunk in self.out:
+ self.failUnless(isinstance(chunk, str), "%r was not a string" % (chunk,))
+
+ if self._origEncoding is not None:
+ sys.setdefaultencoding(self._origEncoding)
+ del sys.setdefaultencoding
+
+
+
+class LogPublisherTestCase(LogPublisherTestCaseMixin, unittest.TestCase):
+ def testSingleString(self):
+ self.lp.msg("Hello, world.")
+ self.assertEqual(len(self.out), 1)
+
+
+ def testMultipleString(self):
+ # Test some stupid behavior that will be deprecated real soon.
+ # If you are reading this and trying to learn how the logging
+ # system works, *do not use this feature*.
+ self.lp.msg("Hello, ", "world.")
+ self.assertEqual(len(self.out), 1)
+
+
+ def testSingleUnicode(self):
+ self.lp.msg(u"Hello, \N{VULGAR FRACTION ONE HALF} world.")
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('with str error', self.out[0])
+ self.assertIn('UnicodeEncodeError', self.out[0])
+
+
+
+class FileObserverTestCase(LogPublisherTestCaseMixin, unittest.TestCase):
+ def test_getTimezoneOffset(self):
+ """
+ Attempt to verify that L{FileLogObserver.getTimezoneOffset} returns
+ correct values for the current C{TZ} environment setting. Do this
+ by setting C{TZ} to various well-known values and asserting that the
+ reported offset is correct.
+ """
+ localDaylightTuple = (2006, 6, 30, 0, 0, 0, 4, 181, 1)
+ utcDaylightTimestamp = time.mktime(localDaylightTuple)
+ localStandardTuple = (2007, 1, 31, 0, 0, 0, 2, 31, 0)
+ utcStandardTimestamp = time.mktime(localStandardTuple)
+
+ originalTimezone = os.environ.get('TZ', None)
+ try:
+ # Test something west of UTC
+ os.environ['TZ'] = 'America/New_York'
+ time.tzset()
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcDaylightTimestamp),
+ 14400)
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcStandardTimestamp),
+ 18000)
+
+ # Test something east of UTC
+ os.environ['TZ'] = 'Europe/Berlin'
+ time.tzset()
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcDaylightTimestamp),
+ -7200)
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcStandardTimestamp),
+ -3600)
+
+ # Test a timezone that doesn't have DST
+ os.environ['TZ'] = 'Africa/Johannesburg'
+ time.tzset()
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcDaylightTimestamp),
+ -7200)
+ self.assertEqual(
+ self.flo.getTimezoneOffset(utcStandardTimestamp),
+ -7200)
+ finally:
+ if originalTimezone is None:
+ del os.environ['TZ']
+ else:
+ os.environ['TZ'] = originalTimezone
+ time.tzset()
+ if getattr(time, 'tzset', None) is None:
+ test_getTimezoneOffset.skip = (
+ "Platform cannot change timezone, cannot verify correct offsets "
+ "in well-known timezones.")
+
+
+ def test_timeFormatting(self):
+ """
+ Test the method of L{FileLogObserver} which turns a timestamp into a
+ human-readable string.
+ """
+ when = calendar.timegm((2001, 2, 3, 4, 5, 6, 7, 8, 0))
+
+ # Pretend to be in US/Eastern for a moment
+ self.flo.getTimezoneOffset = lambda when: 18000
+ self.assertEqual(self.flo.formatTime(when), '2001-02-02 23:05:06-0500')
+
+ # Okay now we're in Eastern Europe somewhere
+ self.flo.getTimezoneOffset = lambda when: -3600
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 05:05:06+0100')
+
+ # And off in the Pacific or someplace like that
+ self.flo.getTimezoneOffset = lambda when: -39600
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 15:05:06+1100')
+
+ # One of those weird places with a half-hour offset timezone
+ self.flo.getTimezoneOffset = lambda when: 5400
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 02:35:06-0130')
+
+ # Half-hour offset in the other direction
+ self.flo.getTimezoneOffset = lambda when: -5400
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 05:35:06+0130')
+
+ # Test an offset which is between 0 and 60 minutes to make sure the
+ # sign comes out properly in that case.
+ self.flo.getTimezoneOffset = lambda when: 1800
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 03:35:06-0030')
+
+ # Test an offset between 0 and 60 minutes in the other direction.
+ self.flo.getTimezoneOffset = lambda when: -1800
+ self.assertEqual(self.flo.formatTime(when), '2001-02-03 04:35:06+0030')
+
+ # If a strftime-format string is present on the logger, it should
+ # use that instead. Note we don't assert anything about day, hour
+ # or minute because we cannot easily control what time.strftime()
+ # thinks the local timezone is.
+ self.flo.timeFormat = '%Y %m'
+ self.assertEqual(self.flo.formatTime(when), '2001 02')
+
+
+ def test_loggingAnObjectWithBroken__str__(self):
+ #HELLO, MCFLY
+ self.lp.msg(EvilStr())
+ self.assertEqual(len(self.out), 1)
+ # Logging system shouldn't need to crap itself for this trivial case
+ self.assertNotIn('UNFORMATTABLE', self.out[0])
+
+
+ def test_formattingAnObjectWithBroken__str__(self):
+ self.lp.msg(format='%(blat)s', blat=EvilStr())
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('Invalid format string or unformattable object', self.out[0])
+
+
+ def test_brokenSystem__str__(self):
+ self.lp.msg('huh', system=EvilStr())
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('Invalid format string or unformattable object', self.out[0])
+
+
+ def test_formattingAnObjectWithBroken__repr__Indirect(self):
+ self.lp.msg(format='%(blat)s', blat=[EvilRepr()])
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('UNFORMATTABLE OBJECT', self.out[0])
+
+
+ def test_systemWithBroker__repr__Indirect(self):
+ self.lp.msg('huh', system=[EvilRepr()])
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('UNFORMATTABLE OBJECT', self.out[0])
+
+
+ def test_simpleBrokenFormat(self):
+ self.lp.msg(format='hooj %s %s', blat=1)
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('Invalid format string or unformattable object', self.out[0])
+
+
+ def test_ridiculousFormat(self):
+ self.lp.msg(format=42, blat=1)
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('Invalid format string or unformattable object', self.out[0])
+
+
+ def test_evilFormat__repr__And__str__(self):
+ self.lp.msg(format=EvilReprStr(), blat=1)
+ self.assertEqual(len(self.out), 1)
+ self.assertIn('PATHOLOGICAL', self.out[0])
+
+
+ def test_strangeEventDict(self):
+ """
+ This kind of eventDict used to fail silently, so test it does.
+ """
+ self.lp.msg(message='', isError=False)
+ self.assertEqual(len(self.out), 0)
+
+
+ def test_startLogging(self):
+ """
+ startLogging() installs FileLogObserver and overrides sys.stdout and
+ sys.stderr.
+ """
+ # When done with test, reset stdout and stderr to current values:
+ origStdout, origStderr = sys.stdout, sys.stderr
+ self.addCleanup(setattr, sys, 'stdout', sys.stdout)
+ self.addCleanup(setattr, sys, 'stderr', sys.stderr)
+ fakeFile = StringIO()
+ observer = log.startLogging(fakeFile)
+ self.addCleanup(observer.stop)
+ log.msg("Hello!")
+ self.assertIn("Hello!", fakeFile.getvalue())
+ self.assertIsInstance(sys.stdout, log.StdioOnnaStick)
+ self.assertEqual(sys.stdout.isError, False)
+ self.assertEqual(sys.stdout.encoding,
+ origStdout.encoding or sys.getdefaultencoding())
+ self.assertIsInstance(sys.stderr, log.StdioOnnaStick)
+ self.assertEqual(sys.stderr.isError, True)
+ self.assertEqual(sys.stderr.encoding,
+ origStderr.encoding or sys.getdefaultencoding())
+
+
+ def test_startLoggingTwice(self):
+ """
+ There are some obscure error conditions that can occur when logging is
+ started twice. See http://twistedmatrix.com/trac/ticket/3289 for more
+ information.
+ """
+ # The bug is particular to the way that the t.p.log 'global' function
+ # handle stdout. If we use our own stream, the error doesn't occur. If
+ # we use our own LogPublisher, the error doesn't occur.
+ sys.stdout = StringIO()
+ self.addCleanup(setattr, sys, 'stdout', sys.stdout)
+ self.addCleanup(setattr, sys, 'stderr', sys.stderr)
+
+ def showError(eventDict):
+ if eventDict['isError']:
+ sys.__stdout__.write(eventDict['failure'].getTraceback())
+
+ log.addObserver(showError)
+ self.addCleanup(log.removeObserver, showError)
+ observer = log.startLogging(sys.stdout)
+ self.addCleanup(observer.stop)
+ # At this point, we expect that sys.stdout is a StdioOnnaStick object.
+ self.assertIsInstance(sys.stdout, log.StdioOnnaStick)
+ fakeStdout = sys.stdout
+ observer = log.startLogging(sys.stdout)
+ self.assertIdentical(sys.stdout, fakeStdout)
+
+
+class PythonLoggingObserverTestCase(unittest.TestCase):
+ """
+ Test the bridge with python logging module.
+ """
+ def setUp(self):
+ self.out = StringIO()
+
+ rootLogger = logging.getLogger("")
+ self.originalLevel = rootLogger.getEffectiveLevel()
+ rootLogger.setLevel(logging.DEBUG)
+ self.hdlr = logging.StreamHandler(self.out)
+ fmt = logging.Formatter(logging.BASIC_FORMAT)
+ self.hdlr.setFormatter(fmt)
+ rootLogger.addHandler(self.hdlr)
+
+ self.lp = log.LogPublisher()
+ self.obs = log.PythonLoggingObserver()
+ self.lp.addObserver(self.obs.emit)
+
+ def tearDown(self):
+ rootLogger = logging.getLogger("")
+ rootLogger.removeHandler(self.hdlr)
+ rootLogger.setLevel(self.originalLevel)
+ logging.shutdown()
+
+ def test_singleString(self):
+ """
+ Test simple output, and default log level.
+ """
+ self.lp.msg("Hello, world.")
+ self.assertIn("Hello, world.", self.out.getvalue())
+ self.assertIn("INFO", self.out.getvalue())
+
+ def test_errorString(self):
+ """
+ Test error output.
+ """
+ self.lp.msg(failure=failure.Failure(ValueError("That is bad.")), isError=True)
+ self.assertIn("ERROR", self.out.getvalue())
+
+ def test_formatString(self):
+ """
+ Test logging with a format.
+ """
+ self.lp.msg(format="%(bar)s oo %(foo)s", bar="Hello", foo="world")
+ self.assertIn("Hello oo world", self.out.getvalue())
+
+ def test_customLevel(self):
+ """
+ Test the logLevel keyword for customizing level used.
+ """
+ self.lp.msg("Spam egg.", logLevel=logging.DEBUG)
+ self.assertIn("Spam egg.", self.out.getvalue())
+ self.assertIn("DEBUG", self.out.getvalue())
+ self.out.reset()
+ self.lp.msg("Foo bar.", logLevel=logging.WARNING)
+ self.assertIn("Foo bar.", self.out.getvalue())
+ self.assertIn("WARNING", self.out.getvalue())
+
+ def test_strangeEventDict(self):
+ """
+ Verify that an event dictionary which is not an error and has an empty
+ message isn't recorded.
+ """
+ self.lp.msg(message='', isError=False)
+ self.assertEqual(self.out.getvalue(), '')
+
+
+class PythonLoggingIntegrationTestCase(unittest.TestCase):
+ """
+ Test integration of python logging bridge.
+ """
+ def test_startStopObserver(self):
+ """
+ Test that start and stop methods of the observer actually register
+ and unregister to the log system.
+ """
+ oldAddObserver = log.addObserver
+ oldRemoveObserver = log.removeObserver
+ l = []
+ try:
+ log.addObserver = l.append
+ log.removeObserver = l.remove
+ obs = log.PythonLoggingObserver()
+ obs.start()
+ self.assertEqual(l[0], obs.emit)
+ obs.stop()
+ self.assertEqual(len(l), 0)
+ finally:
+ log.addObserver = oldAddObserver
+ log.removeObserver = oldRemoveObserver
+
+ def test_inheritance(self):
+ """
+ Test that we can inherit L{log.PythonLoggingObserver} and use super:
+ that's basically a validation that L{log.PythonLoggingObserver} is
+ new-style class.
+ """
+ class MyObserver(log.PythonLoggingObserver):
+ def emit(self, eventDict):
+ super(MyObserver, self).emit(eventDict)
+ obs = MyObserver()
+ l = []
+ oldEmit = log.PythonLoggingObserver.emit
+ try:
+ log.PythonLoggingObserver.emit = l.append
+ obs.emit('foo')
+ self.assertEqual(len(l), 1)
+ finally:
+ log.PythonLoggingObserver.emit = oldEmit
+
+
+class DefaultObserverTestCase(unittest.TestCase):
+ """
+ Test the default observer.
+ """
+
+ def test_failureLogger(self):
+ """
+ The reason argument passed to log.err() appears in the report
+ generated by DefaultObserver.
+ """
+ from StringIO import StringIO
+
+ obs = log.DefaultObserver()
+ obs.stderr = StringIO()
+ obs.start()
+
+ reason = "The reason."
+ log.err(Exception(), reason)
+ errors = self.flushLoggedErrors()
+
+ self.assertSubstring(reason, obs.stderr.getvalue())
+ self.assertEqual(len(errors), 1)
+
+ obs.stop()
+
+
+
+class StdioOnnaStickTestCase(unittest.TestCase):
+ """
+ StdioOnnaStick should act like the normal sys.stdout object.
+ """
+
+ def setUp(self):
+ self.resultLogs = []
+ log.addObserver(self.resultLogs.append)
+
+
+ def tearDown(self):
+ log.removeObserver(self.resultLogs.append)
+
+
+ def getLogMessages(self):
+ return ["".join(d['message']) for d in self.resultLogs]
+
+
+ def test_write(self):
+ """
+ Writing to a StdioOnnaStick instance results in Twisted log messages.
+
+ Log messages are generated every time a '\n' is encountered.
+ """
+ stdio = log.StdioOnnaStick()
+ stdio.write("Hello there\nThis is a test")
+ self.assertEqual(self.getLogMessages(), ["Hello there"])
+ stdio.write("!\n")
+ self.assertEqual(self.getLogMessages(), ["Hello there", "This is a test!"])
+
+
+ def test_metadata(self):
+ """
+ The log messages written by StdioOnnaStick have printed=1 keyword, and
+ by default are not errors.
+ """
+ stdio = log.StdioOnnaStick()
+ stdio.write("hello\n")
+ self.assertEqual(self.resultLogs[0]['isError'], False)
+ self.assertEqual(self.resultLogs[0]['printed'], True)
+
+
+ def test_writeLines(self):
+ """
+ Writing lines to a StdioOnnaStick results in Twisted log messages.
+ """
+ stdio = log.StdioOnnaStick()
+ stdio.writelines(["log 1", "log 2"])
+ self.assertEqual(self.getLogMessages(), ["log 1", "log 2"])
+
+
+ def test_print(self):
+ """
+ When StdioOnnaStick is set as sys.stdout, prints become log messages.
+ """
+ oldStdout = sys.stdout
+ sys.stdout = log.StdioOnnaStick()
+ self.addCleanup(setattr, sys, "stdout", oldStdout)
+ print "This",
+ print "is a test"
+ self.assertEqual(self.getLogMessages(), ["This is a test"])
+
+
+ def test_error(self):
+ """
+ StdioOnnaStick created with isError=True log messages as errors.
+ """
+ stdio = log.StdioOnnaStick(isError=True)
+ stdio.write("log 1\n")
+ self.assertEqual(self.resultLogs[0]['isError'], True)
+
+
+ def test_unicode(self):
+ """
+ StdioOnnaStick converts unicode prints to strings, in order to be
+ compatible with the normal stdout/stderr objects.
+ """
+ unicodeString = u"Hello, \N{VULGAR FRACTION ONE HALF} world."
+ stdio = log.StdioOnnaStick(encoding="utf-8")
+ self.assertEqual(stdio.encoding, "utf-8")
+ stdio.write(unicodeString + u"\n")
+ stdio.writelines([u"Also, " + unicodeString])
+ oldStdout = sys.stdout
+ sys.stdout = stdio
+ self.addCleanup(setattr, sys, "stdout", oldStdout)
+ # This should go to the log, utf-8 encoded too:
+ print unicodeString
+ self.assertEqual(self.getLogMessages(),
+ [unicodeString.encode("utf-8"),
+ (u"Also, " + unicodeString).encode("utf-8"),
+ unicodeString.encode("utf-8")])
+
diff --git a/twisted/test/test_logfile.py b/twisted/test/test_logfile.py
new file mode 100644
index 0000000..e7db238
--- /dev/null
+++ b/twisted/test/test_logfile.py
@@ -0,0 +1,320 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import os, time, stat, errno
+
+from twisted.trial import unittest
+from twisted.python import logfile, runtime
+
+
+class LogFileTestCase(unittest.TestCase):
+ """
+ Test the rotating log file.
+ """
+
+ def setUp(self):
+ self.dir = self.mktemp()
+ os.makedirs(self.dir)
+ self.name = "test.log"
+ self.path = os.path.join(self.dir, self.name)
+
+
+ def tearDown(self):
+ """
+ Restore back write rights on created paths: if tests modified the
+ rights, that will allow the paths to be removed easily afterwards.
+ """
+ os.chmod(self.dir, 0777)
+ if os.path.exists(self.path):
+ os.chmod(self.path, 0777)
+
+
+ def testWriting(self):
+ log = logfile.LogFile(self.name, self.dir)
+ log.write("123")
+ log.write("456")
+ log.flush()
+ log.write("7890")
+ log.close()
+
+ f = open(self.path, "r")
+ self.assertEqual(f.read(), "1234567890")
+ f.close()
+
+ def testRotation(self):
+ # this logfile should rotate every 10 bytes
+ log = logfile.LogFile(self.name, self.dir, rotateLength=10)
+
+ # test automatic rotation
+ log.write("123")
+ log.write("4567890")
+ log.write("1" * 11)
+ self.assert_(os.path.exists("%s.1" % self.path))
+ self.assert_(not os.path.exists("%s.2" % self.path))
+ log.write('')
+ self.assert_(os.path.exists("%s.1" % self.path))
+ self.assert_(os.path.exists("%s.2" % self.path))
+ self.assert_(not os.path.exists("%s.3" % self.path))
+ log.write("3")
+ self.assert_(not os.path.exists("%s.3" % self.path))
+
+ # test manual rotation
+ log.rotate()
+ self.assert_(os.path.exists("%s.3" % self.path))
+ self.assert_(not os.path.exists("%s.4" % self.path))
+ log.close()
+
+ self.assertEqual(log.listLogs(), [1, 2, 3])
+
+ def testAppend(self):
+ log = logfile.LogFile(self.name, self.dir)
+ log.write("0123456789")
+ log.close()
+
+ log = logfile.LogFile(self.name, self.dir)
+ self.assertEqual(log.size, 10)
+ self.assertEqual(log._file.tell(), log.size)
+ log.write("abc")
+ self.assertEqual(log.size, 13)
+ self.assertEqual(log._file.tell(), log.size)
+ f = log._file
+ f.seek(0, 0)
+ self.assertEqual(f.read(), "0123456789abc")
+ log.close()
+
+ def testLogReader(self):
+ log = logfile.LogFile(self.name, self.dir)
+ log.write("abc\n")
+ log.write("def\n")
+ log.rotate()
+ log.write("ghi\n")
+ log.flush()
+
+ # check reading logs
+ self.assertEqual(log.listLogs(), [1])
+ reader = log.getCurrentLog()
+ reader._file.seek(0)
+ self.assertEqual(reader.readLines(), ["ghi\n"])
+ self.assertEqual(reader.readLines(), [])
+ reader.close()
+ reader = log.getLog(1)
+ self.assertEqual(reader.readLines(), ["abc\n", "def\n"])
+ self.assertEqual(reader.readLines(), [])
+ reader.close()
+
+ # check getting illegal log readers
+ self.assertRaises(ValueError, log.getLog, 2)
+ self.assertRaises(TypeError, log.getLog, "1")
+
+ # check that log numbers are higher for older logs
+ log.rotate()
+ self.assertEqual(log.listLogs(), [1, 2])
+ reader = log.getLog(1)
+ reader._file.seek(0)
+ self.assertEqual(reader.readLines(), ["ghi\n"])
+ self.assertEqual(reader.readLines(), [])
+ reader.close()
+ reader = log.getLog(2)
+ self.assertEqual(reader.readLines(), ["abc\n", "def\n"])
+ self.assertEqual(reader.readLines(), [])
+ reader.close()
+
+ def testModePreservation(self):
+ """
+ Check rotated files have same permissions as original.
+ """
+ f = open(self.path, "w").close()
+ os.chmod(self.path, 0707)
+ mode = os.stat(self.path)[stat.ST_MODE]
+ log = logfile.LogFile(self.name, self.dir)
+ log.write("abc")
+ log.rotate()
+ self.assertEqual(mode, os.stat(self.path)[stat.ST_MODE])
+
+
+ def test_noPermission(self):
+ """
+ Check it keeps working when permission on dir changes.
+ """
+ log = logfile.LogFile(self.name, self.dir)
+ log.write("abc")
+
+ # change permissions so rotation would fail
+ os.chmod(self.dir, 0555)
+
+ # if this succeeds, chmod doesn't restrict us, so we can't
+ # do the test
+ try:
+ f = open(os.path.join(self.dir,"xxx"), "w")
+ except (OSError, IOError):
+ pass
+ else:
+ f.close()
+ return
+
+ log.rotate() # this should not fail
+
+ log.write("def")
+ log.flush()
+
+ f = log._file
+ self.assertEqual(f.tell(), 6)
+ f.seek(0, 0)
+ self.assertEqual(f.read(), "abcdef")
+ log.close()
+
+
+ def test_maxNumberOfLog(self):
+ """
+ Test it respect the limit on the number of files when maxRotatedFiles
+ is not None.
+ """
+ log = logfile.LogFile(self.name, self.dir, rotateLength=10,
+ maxRotatedFiles=3)
+ log.write("1" * 11)
+ log.write("2" * 11)
+ self.failUnless(os.path.exists("%s.1" % self.path))
+
+ log.write("3" * 11)
+ self.failUnless(os.path.exists("%s.2" % self.path))
+
+ log.write("4" * 11)
+ self.failUnless(os.path.exists("%s.3" % self.path))
+ self.assertEqual(file("%s.3" % self.path).read(), "1" * 11)
+
+ log.write("5" * 11)
+ self.assertEqual(file("%s.3" % self.path).read(), "2" * 11)
+ self.failUnless(not os.path.exists("%s.4" % self.path))
+
+ def test_fromFullPath(self):
+ """
+ Test the fromFullPath method.
+ """
+ log1 = logfile.LogFile(self.name, self.dir, 10, defaultMode=0777)
+ log2 = logfile.LogFile.fromFullPath(self.path, 10, defaultMode=0777)
+ self.assertEqual(log1.name, log2.name)
+ self.assertEqual(os.path.abspath(log1.path), log2.path)
+ self.assertEqual(log1.rotateLength, log2.rotateLength)
+ self.assertEqual(log1.defaultMode, log2.defaultMode)
+
+ def test_defaultPermissions(self):
+ """
+ Test the default permission of the log file: if the file exist, it
+ should keep the permission.
+ """
+ f = file(self.path, "w")
+ os.chmod(self.path, 0707)
+ currentMode = stat.S_IMODE(os.stat(self.path)[stat.ST_MODE])
+ f.close()
+ log1 = logfile.LogFile(self.name, self.dir)
+ self.assertEqual(stat.S_IMODE(os.stat(self.path)[stat.ST_MODE]),
+ currentMode)
+
+
+ def test_specifiedPermissions(self):
+ """
+ Test specifying the permissions used on the log file.
+ """
+ log1 = logfile.LogFile(self.name, self.dir, defaultMode=0066)
+ mode = stat.S_IMODE(os.stat(self.path)[stat.ST_MODE])
+ if runtime.platform.isWindows():
+ # The only thing we can get here is global read-only
+ self.assertEqual(mode, 0444)
+ else:
+ self.assertEqual(mode, 0066)
+
+
+ def test_reopen(self):
+ """
+ L{logfile.LogFile.reopen} allows to rename the currently used file and
+ make L{logfile.LogFile} create a new file.
+ """
+ log1 = logfile.LogFile(self.name, self.dir)
+ log1.write("hello1")
+ savePath = os.path.join(self.dir, "save.log")
+ os.rename(self.path, savePath)
+ log1.reopen()
+ log1.write("hello2")
+ log1.close()
+
+ f = open(self.path, "r")
+ self.assertEqual(f.read(), "hello2")
+ f.close()
+ f = open(savePath, "r")
+ self.assertEqual(f.read(), "hello1")
+ f.close()
+
+ if runtime.platform.isWindows():
+ test_reopen.skip = "Can't test reopen on Windows"
+
+
+ def test_nonExistentDir(self):
+ """
+ Specifying an invalid directory to L{LogFile} raises C{IOError}.
+ """
+ e = self.assertRaises(
+ IOError, logfile.LogFile, self.name, 'this_dir_does_not_exist')
+ self.assertEqual(e.errno, errno.ENOENT)
+
+
+
+class RiggedDailyLogFile(logfile.DailyLogFile):
+ _clock = 0.0
+
+ def _openFile(self):
+ logfile.DailyLogFile._openFile(self)
+ # rig the date to match _clock, not mtime
+ self.lastDate = self.toDate()
+
+ def toDate(self, *args):
+ if args:
+ return time.gmtime(*args)[:3]
+ return time.gmtime(self._clock)[:3]
+
+class DailyLogFileTestCase(unittest.TestCase):
+ """
+ Test rotating log file.
+ """
+
+ def setUp(self):
+ self.dir = self.mktemp()
+ os.makedirs(self.dir)
+ self.name = "testdaily.log"
+ self.path = os.path.join(self.dir, self.name)
+
+
+ def testWriting(self):
+ log = RiggedDailyLogFile(self.name, self.dir)
+ log.write("123")
+ log.write("456")
+ log.flush()
+ log.write("7890")
+ log.close()
+
+ f = open(self.path, "r")
+ self.assertEqual(f.read(), "1234567890")
+ f.close()
+
+ def testRotation(self):
+ # this logfile should rotate every 10 bytes
+ log = RiggedDailyLogFile(self.name, self.dir)
+ days = [(self.path + '.' + log.suffix(day * 86400)) for day in range(3)]
+
+ # test automatic rotation
+ log._clock = 0.0 # 1970/01/01 00:00.00
+ log.write("123")
+ log._clock = 43200 # 1970/01/01 12:00.00
+ log.write("4567890")
+ log._clock = 86400 # 1970/01/02 00:00.00
+ log.write("1" * 11)
+ self.assert_(os.path.exists(days[0]))
+ self.assert_(not os.path.exists(days[1]))
+ log._clock = 172800 # 1970/01/03 00:00.00
+ log.write('')
+ self.assert_(os.path.exists(days[0]))
+ self.assert_(os.path.exists(days[1]))
+ self.assert_(not os.path.exists(days[2]))
+ log._clock = 259199 # 1970/01/03 23:59.59
+ log.write("3")
+ self.assert_(not os.path.exists(days[2]))
+
diff --git a/twisted/test/test_loopback.py b/twisted/test/test_loopback.py
new file mode 100644
index 0000000..f09908f
--- /dev/null
+++ b/twisted/test/test_loopback.py
@@ -0,0 +1,419 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test case for L{twisted.protocols.loopback}.
+"""
+
+from zope.interface import implements
+
+from twisted.trial import unittest
+from twisted.trial.util import suppress as SUPPRESS
+from twisted.protocols import basic, loopback
+from twisted.internet import defer
+from twisted.internet.protocol import Protocol
+from twisted.internet.defer import Deferred
+from twisted.internet.interfaces import IAddress, IPushProducer, IPullProducer
+from twisted.internet import reactor, interfaces
+
+
+class SimpleProtocol(basic.LineReceiver):
+ def __init__(self):
+ self.conn = defer.Deferred()
+ self.lines = []
+ self.connLost = []
+
+ def connectionMade(self):
+ self.conn.callback(None)
+
+ def lineReceived(self, line):
+ self.lines.append(line)
+
+ def connectionLost(self, reason):
+ self.connLost.append(reason)
+
+
+class DoomProtocol(SimpleProtocol):
+ i = 0
+ def lineReceived(self, line):
+ self.i += 1
+ if self.i < 4:
+ # by this point we should have connection closed,
+ # but just in case we didn't we won't ever send 'Hello 4'
+ self.sendLine("Hello %d" % self.i)
+ SimpleProtocol.lineReceived(self, line)
+ if self.lines[-1] == "Hello 3":
+ self.transport.loseConnection()
+
+
+class LoopbackTestCaseMixin:
+ def testRegularFunction(self):
+ s = SimpleProtocol()
+ c = SimpleProtocol()
+
+ def sendALine(result):
+ s.sendLine("THIS IS LINE ONE!")
+ s.transport.loseConnection()
+ s.conn.addCallback(sendALine)
+
+ def check(ignored):
+ self.assertEqual(c.lines, ["THIS IS LINE ONE!"])
+ self.assertEqual(len(s.connLost), 1)
+ self.assertEqual(len(c.connLost), 1)
+ d = defer.maybeDeferred(self.loopbackFunc, s, c)
+ d.addCallback(check)
+ return d
+
+ def testSneakyHiddenDoom(self):
+ s = DoomProtocol()
+ c = DoomProtocol()
+
+ def sendALine(result):
+ s.sendLine("DOOM LINE")
+ s.conn.addCallback(sendALine)
+
+ def check(ignored):
+ self.assertEqual(s.lines, ['Hello 1', 'Hello 2', 'Hello 3'])
+ self.assertEqual(c.lines, ['DOOM LINE', 'Hello 1', 'Hello 2', 'Hello 3'])
+ self.assertEqual(len(s.connLost), 1)
+ self.assertEqual(len(c.connLost), 1)
+ d = defer.maybeDeferred(self.loopbackFunc, s, c)
+ d.addCallback(check)
+ return d
+
+
+
+class LoopbackAsyncTestCase(LoopbackTestCaseMixin, unittest.TestCase):
+ loopbackFunc = staticmethod(loopback.loopbackAsync)
+
+
+ def test_makeConnection(self):
+ """
+ Test that the client and server protocol both have makeConnection
+ invoked on them by loopbackAsync.
+ """
+ class TestProtocol(Protocol):
+ transport = None
+ def makeConnection(self, transport):
+ self.transport = transport
+
+ server = TestProtocol()
+ client = TestProtocol()
+ loopback.loopbackAsync(server, client)
+ self.failIfEqual(client.transport, None)
+ self.failIfEqual(server.transport, None)
+
+
+ def _hostpeertest(self, get, testServer):
+ """
+ Test one of the permutations of client/server host/peer.
+ """
+ class TestProtocol(Protocol):
+ def makeConnection(self, transport):
+ Protocol.makeConnection(self, transport)
+ self.onConnection.callback(transport)
+
+ if testServer:
+ server = TestProtocol()
+ d = server.onConnection = Deferred()
+ client = Protocol()
+ else:
+ server = Protocol()
+ client = TestProtocol()
+ d = client.onConnection = Deferred()
+
+ loopback.loopbackAsync(server, client)
+
+ def connected(transport):
+ host = getattr(transport, get)()
+ self.failUnless(IAddress.providedBy(host))
+
+ return d.addCallback(connected)
+
+
+ def test_serverHost(self):
+ """
+ Test that the server gets a transport with a properly functioning
+ implementation of L{ITransport.getHost}.
+ """
+ return self._hostpeertest("getHost", True)
+
+
+ def test_serverPeer(self):
+ """
+ Like C{test_serverHost} but for L{ITransport.getPeer}
+ """
+ return self._hostpeertest("getPeer", True)
+
+
+ def test_clientHost(self, get="getHost"):
+ """
+ Test that the client gets a transport with a properly functioning
+ implementation of L{ITransport.getHost}.
+ """
+ return self._hostpeertest("getHost", False)
+
+
+ def test_clientPeer(self):
+ """
+ Like C{test_clientHost} but for L{ITransport.getPeer}.
+ """
+ return self._hostpeertest("getPeer", False)
+
+
+ def _greetingtest(self, write, testServer):
+ """
+ Test one of the permutations of write/writeSequence client/server.
+ """
+ class GreeteeProtocol(Protocol):
+ bytes = ""
+ def dataReceived(self, bytes):
+ self.bytes += bytes
+ if self.bytes == "bytes":
+ self.received.callback(None)
+
+ class GreeterProtocol(Protocol):
+ def connectionMade(self):
+ getattr(self.transport, write)("bytes")
+
+ if testServer:
+ server = GreeterProtocol()
+ client = GreeteeProtocol()
+ d = client.received = Deferred()
+ else:
+ server = GreeteeProtocol()
+ d = server.received = Deferred()
+ client = GreeterProtocol()
+
+ loopback.loopbackAsync(server, client)
+ return d
+
+
+ def test_clientGreeting(self):
+ """
+ Test that on a connection where the client speaks first, the server
+ receives the bytes sent by the client.
+ """
+ return self._greetingtest("write", False)
+
+
+ def test_clientGreetingSequence(self):
+ """
+ Like C{test_clientGreeting}, but use C{writeSequence} instead of
+ C{write} to issue the greeting.
+ """
+ return self._greetingtest("writeSequence", False)
+
+
+ def test_serverGreeting(self, write="write"):
+ """
+ Test that on a connection where the server speaks first, the client
+ receives the bytes sent by the server.
+ """
+ return self._greetingtest("write", True)
+
+
+ def test_serverGreetingSequence(self):
+ """
+ Like C{test_serverGreeting}, but use C{writeSequence} instead of
+ C{write} to issue the greeting.
+ """
+ return self._greetingtest("writeSequence", True)
+
+
+ def _producertest(self, producerClass):
+ toProduce = map(str, range(0, 10))
+
+ class ProducingProtocol(Protocol):
+ def connectionMade(self):
+ self.producer = producerClass(list(toProduce))
+ self.producer.start(self.transport)
+
+ class ReceivingProtocol(Protocol):
+ bytes = ""
+ def dataReceived(self, bytes):
+ self.bytes += bytes
+ if self.bytes == ''.join(toProduce):
+ self.received.callback((client, server))
+
+ server = ProducingProtocol()
+ client = ReceivingProtocol()
+ client.received = Deferred()
+
+ loopback.loopbackAsync(server, client)
+ return client.received
+
+
+ def test_pushProducer(self):
+ """
+ Test a push producer registered against a loopback transport.
+ """
+ class PushProducer(object):
+ implements(IPushProducer)
+ resumed = False
+
+ def __init__(self, toProduce):
+ self.toProduce = toProduce
+
+ def resumeProducing(self):
+ self.resumed = True
+
+ def start(self, consumer):
+ self.consumer = consumer
+ consumer.registerProducer(self, True)
+ self._produceAndSchedule()
+
+ def _produceAndSchedule(self):
+ if self.toProduce:
+ self.consumer.write(self.toProduce.pop(0))
+ reactor.callLater(0, self._produceAndSchedule)
+ else:
+ self.consumer.unregisterProducer()
+ d = self._producertest(PushProducer)
+
+ def finished((client, server)):
+ self.failIf(
+ server.producer.resumed,
+ "Streaming producer should not have been resumed.")
+ d.addCallback(finished)
+ return d
+
+
+ def test_pullProducer(self):
+ """
+ Test a pull producer registered against a loopback transport.
+ """
+ class PullProducer(object):
+ implements(IPullProducer)
+
+ def __init__(self, toProduce):
+ self.toProduce = toProduce
+
+ def start(self, consumer):
+ self.consumer = consumer
+ self.consumer.registerProducer(self, False)
+
+ def resumeProducing(self):
+ self.consumer.write(self.toProduce.pop(0))
+ if not self.toProduce:
+ self.consumer.unregisterProducer()
+ return self._producertest(PullProducer)
+
+
+ def test_writeNotReentrant(self):
+ """
+ L{loopback.loopbackAsync} does not call a protocol's C{dataReceived}
+ method while that protocol's transport's C{write} method is higher up
+ on the stack.
+ """
+ class Server(Protocol):
+ def dataReceived(self, bytes):
+ self.transport.write("bytes")
+
+ class Client(Protocol):
+ ready = False
+
+ def connectionMade(self):
+ reactor.callLater(0, self.go)
+
+ def go(self):
+ self.transport.write("foo")
+ self.ready = True
+
+ def dataReceived(self, bytes):
+ self.wasReady = self.ready
+ self.transport.loseConnection()
+
+
+ server = Server()
+ client = Client()
+ d = loopback.loopbackAsync(client, server)
+ def cbFinished(ignored):
+ self.assertTrue(client.wasReady)
+ d.addCallback(cbFinished)
+ return d
+
+
+ def test_pumpPolicy(self):
+ """
+ The callable passed as the value for the C{pumpPolicy} parameter to
+ L{loopbackAsync} is called with a L{_LoopbackQueue} of pending bytes
+ and a protocol to which they should be delivered.
+ """
+ pumpCalls = []
+ def dummyPolicy(queue, target):
+ bytes = []
+ while queue:
+ bytes.append(queue.get())
+ pumpCalls.append((target, bytes))
+
+ client = Protocol()
+ server = Protocol()
+
+ finished = loopback.loopbackAsync(server, client, dummyPolicy)
+ self.assertEqual(pumpCalls, [])
+
+ client.transport.write("foo")
+ client.transport.write("bar")
+ server.transport.write("baz")
+ server.transport.write("quux")
+ server.transport.loseConnection()
+
+ def cbComplete(ignored):
+ self.assertEqual(
+ pumpCalls,
+ # The order here is somewhat arbitrary. The implementation
+ # happens to always deliver data to the client first.
+ [(client, ["baz", "quux", None]),
+ (server, ["foo", "bar"])])
+ finished.addCallback(cbComplete)
+ return finished
+
+
+ def test_identityPumpPolicy(self):
+ """
+ L{identityPumpPolicy} is a pump policy which calls the target's
+ C{dataReceived} method one for each string in the queue passed to it.
+ """
+ bytes = []
+ client = Protocol()
+ client.dataReceived = bytes.append
+ queue = loopback._LoopbackQueue()
+ queue.put("foo")
+ queue.put("bar")
+ queue.put(None)
+
+ loopback.identityPumpPolicy(queue, client)
+
+ self.assertEqual(bytes, ["foo", "bar"])
+
+
+ def test_collapsingPumpPolicy(self):
+ """
+ L{collapsingPumpPolicy} is a pump policy which calls the target's
+ C{dataReceived} only once with all of the strings in the queue passed
+ to it joined together.
+ """
+ bytes = []
+ client = Protocol()
+ client.dataReceived = bytes.append
+ queue = loopback._LoopbackQueue()
+ queue.put("foo")
+ queue.put("bar")
+ queue.put(None)
+
+ loopback.collapsingPumpPolicy(queue, client)
+
+ self.assertEqual(bytes, ["foobar"])
+
+
+
+class LoopbackTCPTestCase(LoopbackTestCaseMixin, unittest.TestCase):
+ loopbackFunc = staticmethod(loopback.loopbackTCP)
+
+
+class LoopbackUNIXTestCase(LoopbackTestCaseMixin, unittest.TestCase):
+ loopbackFunc = staticmethod(loopback.loopbackUNIX)
+
+ if interfaces.IReactorUNIX(reactor, None) is None:
+ skip = "Current reactor does not support UNIX sockets"
diff --git a/twisted/test/test_manhole.py b/twisted/test/test_manhole.py
new file mode 100644
index 0000000..fa7d0c7
--- /dev/null
+++ b/twisted/test/test_manhole.py
@@ -0,0 +1,75 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+from twisted.manhole import service
+from twisted.spread.util import LocalAsRemote
+
+class Dummy:
+ pass
+
+class DummyTransport:
+ def getHost(self):
+ return 'INET', '127.0.0.1', 0
+
+class DummyManholeClient(LocalAsRemote):
+ zero = 0
+ broker = Dummy()
+ broker.transport = DummyTransport()
+
+ def __init__(self):
+ self.messages = []
+
+ def console(self, messages):
+ self.messages.extend(messages)
+
+ def receiveExplorer(self, xplorer):
+ pass
+
+ def setZero(self):
+ self.zero = len(self.messages)
+
+ def getMessages(self):
+ return self.messages[self.zero:]
+
+ # local interface
+ sync_console = console
+ sync_receiveExplorer = receiveExplorer
+ sync_setZero = setZero
+ sync_getMessages = getMessages
+
+class ManholeTest(unittest.TestCase):
+ """Various tests for the manhole service.
+
+ Both the the importIdentity and importMain tests are known to fail
+ when the __name__ in the manhole namespace is set to certain
+ values.
+ """
+ def setUp(self):
+ self.service = service.Service()
+ self.p = service.Perspective(self.service)
+ self.client = DummyManholeClient()
+ self.p.attached(self.client, None)
+
+ def test_importIdentity(self):
+ """Making sure imported module is the same as one previously loaded.
+ """
+ self.p.perspective_do("from twisted.manhole import service")
+ self.client.setZero()
+ self.p.perspective_do("int(service is sys.modules['twisted.manhole.service'])")
+ msg = self.client.getMessages()[0]
+ self.assertEqual(msg, ('result',"1\n"))
+
+ def test_importMain(self):
+ """Trying to import __main__"""
+ self.client.setZero()
+ self.p.perspective_do("import __main__")
+ if self.client.getMessages():
+ msg = self.client.getMessages()[0]
+ if msg[0] in ("exception","stderr"):
+ self.fail(msg[1])
+
+#if __name__=='__main__':
+# unittest.main()
diff --git a/twisted/test/test_memcache.py b/twisted/test/test_memcache.py
new file mode 100644
index 0000000..7c25e98
--- /dev/null
+++ b/twisted/test/test_memcache.py
@@ -0,0 +1,663 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test the memcache client protocol.
+"""
+
+from twisted.internet.error import ConnectionDone
+
+from twisted.protocols.memcache import MemCacheProtocol, NoSuchCommand
+from twisted.protocols.memcache import ClientError, ServerError
+
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransportWithDisconnection
+from twisted.internet.task import Clock
+from twisted.internet.defer import Deferred, gatherResults, TimeoutError
+from twisted.internet.defer import DeferredList
+
+
+
+class CommandMixin:
+ """
+ Setup and tests for basic invocation of L{MemCacheProtocol} commands.
+ """
+
+ def _test(self, d, send, recv, result):
+ """
+ Helper test method to test the resulting C{Deferred} of a
+ L{MemCacheProtocol} command.
+ """
+ raise NotImplementedError()
+
+
+ def test_get(self):
+ """
+ L{MemCacheProtocol.get} returns a L{Deferred} which is called back with
+ the value and the flag associated with the given key if the server
+ returns a successful result.
+ """
+ return self._test(self.proto.get("foo"), "get foo\r\n",
+ "VALUE foo 0 3\r\nbar\r\nEND\r\n", (0, "bar"))
+
+
+ def test_emptyGet(self):
+ """
+ Test getting a non-available key: it succeeds but return C{None} as
+ value and C{0} as flag.
+ """
+ return self._test(self.proto.get("foo"), "get foo\r\n",
+ "END\r\n", (0, None))
+
+
+ def test_getMultiple(self):
+ """
+ L{MemCacheProtocol.getMultiple} returns a L{Deferred} which is called
+ back with a dictionary of flag, value for each given key.
+ """
+ return self._test(self.proto.getMultiple(['foo', 'cow']),
+ "get foo cow\r\n",
+ "VALUE foo 0 3\r\nbar\r\nVALUE cow 0 7\r\nchicken\r\nEND\r\n",
+ {'cow': (0, 'chicken'), 'foo': (0, 'bar')})
+
+
+ def test_getMultipleWithEmpty(self):
+ """
+ When L{MemCacheProtocol.getMultiple} is called with non-available keys,
+ the corresponding tuples are (0, None).
+ """
+ return self._test(self.proto.getMultiple(['foo', 'cow']),
+ "get foo cow\r\n",
+ "VALUE cow 1 3\r\nbar\r\nEND\r\n",
+ {'cow': (1, 'bar'), 'foo': (0, None)})
+
+
+ def test_set(self):
+ """
+ L{MemCacheProtocol.set} returns a L{Deferred} which is called back with
+ C{True} when the operation succeeds.
+ """
+ return self._test(self.proto.set("foo", "bar"),
+ "set foo 0 0 3\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_add(self):
+ """
+ L{MemCacheProtocol.add} returns a L{Deferred} which is called back with
+ C{True} when the operation succeeds.
+ """
+ return self._test(self.proto.add("foo", "bar"),
+ "add foo 0 0 3\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_replace(self):
+ """
+ L{MemCacheProtocol.replace} returns a L{Deferred} which is called back
+ with C{True} when the operation succeeds.
+ """
+ return self._test(self.proto.replace("foo", "bar"),
+ "replace foo 0 0 3\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_errorAdd(self):
+ """
+ Test an erroneous add: if a L{MemCacheProtocol.add} is called but the
+ key already exists on the server, it returns a B{NOT STORED} answer,
+ which calls back the resulting L{Deferred} with C{False}.
+ """
+ return self._test(self.proto.add("foo", "bar"),
+ "add foo 0 0 3\r\nbar\r\n", "NOT STORED\r\n", False)
+
+
+ def test_errorReplace(self):
+ """
+ Test an erroneous replace: if a L{MemCacheProtocol.replace} is called
+ but the key doesn't exist on the server, it returns a B{NOT STORED}
+ answer, which calls back the resulting L{Deferred} with C{False}.
+ """
+ return self._test(self.proto.replace("foo", "bar"),
+ "replace foo 0 0 3\r\nbar\r\n", "NOT STORED\r\n", False)
+
+
+ def test_delete(self):
+ """
+ L{MemCacheProtocol.delete} returns a L{Deferred} which is called back
+ with C{True} when the server notifies a success.
+ """
+ return self._test(self.proto.delete("bar"), "delete bar\r\n",
+ "DELETED\r\n", True)
+
+
+ def test_errorDelete(self):
+ """
+ Test a error during a delete: if key doesn't exist on the server, it
+ returns a B{NOT FOUND} answer which calls back the resulting L{Deferred}
+ with C{False}.
+ """
+ return self._test(self.proto.delete("bar"), "delete bar\r\n",
+ "NOT FOUND\r\n", False)
+
+
+ def test_increment(self):
+ """
+ Test incrementing a variable: L{MemCacheProtocol.increment} returns a
+ L{Deferred} which is called back with the incremented value of the
+ given key.
+ """
+ return self._test(self.proto.increment("foo"), "incr foo 1\r\n",
+ "4\r\n", 4)
+
+
+ def test_decrement(self):
+ """
+ Test decrementing a variable: L{MemCacheProtocol.decrement} returns a
+ L{Deferred} which is called back with the decremented value of the
+ given key.
+ """
+ return self._test(
+ self.proto.decrement("foo"), "decr foo 1\r\n", "5\r\n", 5)
+
+
+ def test_incrementVal(self):
+ """
+ L{MemCacheProtocol.increment} takes an optional argument C{value} which
+ replaces the default value of 1 when specified.
+ """
+ return self._test(self.proto.increment("foo", 8), "incr foo 8\r\n",
+ "4\r\n", 4)
+
+
+ def test_decrementVal(self):
+ """
+ L{MemCacheProtocol.decrement} takes an optional argument C{value} which
+ replaces the default value of 1 when specified.
+ """
+ return self._test(self.proto.decrement("foo", 3), "decr foo 3\r\n",
+ "5\r\n", 5)
+
+
+ def test_stats(self):
+ """
+ Test retrieving server statistics via the L{MemCacheProtocol.stats}
+ command: it parses the data sent by the server and calls back the
+ resulting L{Deferred} with a dictionary of the received statistics.
+ """
+ return self._test(self.proto.stats(), "stats\r\n",
+ "STAT foo bar\r\nSTAT egg spam\r\nEND\r\n",
+ {"foo": "bar", "egg": "spam"})
+
+
+ def test_statsWithArgument(self):
+ """
+ L{MemCacheProtocol.stats} takes an optional C{str} argument which,
+ if specified, is sent along with the I{STAT} command. The I{STAT}
+ responses from the server are parsed as key/value pairs and returned
+ as a C{dict} (as in the case where the argument is not specified).
+ """
+ return self._test(self.proto.stats("blah"), "stats blah\r\n",
+ "STAT foo bar\r\nSTAT egg spam\r\nEND\r\n",
+ {"foo": "bar", "egg": "spam"})
+
+
+ def test_version(self):
+ """
+ Test version retrieval via the L{MemCacheProtocol.version} command: it
+ returns a L{Deferred} which is called back with the version sent by the
+ server.
+ """
+ return self._test(self.proto.version(), "version\r\n",
+ "VERSION 1.1\r\n", "1.1")
+
+
+ def test_flushAll(self):
+ """
+ L{MemCacheProtocol.flushAll} returns a L{Deferred} which is called back
+ with C{True} if the server acknowledges success.
+ """
+ return self._test(self.proto.flushAll(), "flush_all\r\n",
+ "OK\r\n", True)
+
+
+
+class MemCacheTestCase(CommandMixin, TestCase):
+ """
+ Test client protocol class L{MemCacheProtocol}.
+ """
+
+ def setUp(self):
+ """
+ Create a memcache client, connect it to a string protocol, and make it
+ use a deterministic clock.
+ """
+ self.proto = MemCacheProtocol()
+ self.clock = Clock()
+ self.proto.callLater = self.clock.callLater
+ self.transport = StringTransportWithDisconnection()
+ self.transport.protocol = self.proto
+ self.proto.makeConnection(self.transport)
+
+
+ def _test(self, d, send, recv, result):
+ """
+ Implementation of C{_test} which checks that the command sends C{send}
+ data, and that upon reception of C{recv} the result is C{result}.
+
+ @param d: the resulting deferred from the memcache command.
+ @type d: C{Deferred}
+
+ @param send: the expected data to be sent.
+ @type send: C{str}
+
+ @param recv: the data to simulate as reception.
+ @type recv: C{str}
+
+ @param result: the expected result.
+ @type result: C{any}
+ """
+ def cb(res):
+ self.assertEqual(res, result)
+ self.assertEqual(self.transport.value(), send)
+ d.addCallback(cb)
+ self.proto.dataReceived(recv)
+ return d
+
+
+ def test_invalidGetResponse(self):
+ """
+ If the value returned doesn't match the expected key of the current
+ C{get} command, an error is raised in L{MemCacheProtocol.dataReceived}.
+ """
+ self.proto.get("foo")
+ s = "spamegg"
+ self.assertRaises(RuntimeError,
+ self.proto.dataReceived,
+ "VALUE bar 0 %s\r\n%s\r\nEND\r\n" % (len(s), s))
+
+
+ def test_invalidMultipleGetResponse(self):
+ """
+ If the value returned doesn't match one the expected keys of the
+ current multiple C{get} command, an error is raised error in
+ L{MemCacheProtocol.dataReceived}.
+ """
+ self.proto.getMultiple(["foo", "bar"])
+ s = "spamegg"
+ self.assertRaises(RuntimeError,
+ self.proto.dataReceived,
+ "VALUE egg 0 %s\r\n%s\r\nEND\r\n" % (len(s), s))
+
+
+ def test_timeOut(self):
+ """
+ Test the timeout on outgoing requests: when timeout is detected, all
+ current commands fail with a L{TimeoutError}, and the connection is
+ closed.
+ """
+ d1 = self.proto.get("foo")
+ d2 = self.proto.get("bar")
+ d3 = Deferred()
+ self.proto.connectionLost = d3.callback
+
+ self.clock.advance(self.proto.persistentTimeOut)
+ self.assertFailure(d1, TimeoutError)
+ self.assertFailure(d2, TimeoutError)
+ def checkMessage(error):
+ self.assertEqual(str(error), "Connection timeout")
+ d1.addCallback(checkMessage)
+ return gatherResults([d1, d2, d3])
+
+
+ def test_timeoutRemoved(self):
+ """
+ When a request gets a response, no pending timeout call remains around.
+ """
+ d = self.proto.get("foo")
+
+ self.clock.advance(self.proto.persistentTimeOut - 1)
+ self.proto.dataReceived("VALUE foo 0 3\r\nbar\r\nEND\r\n")
+
+ def check(result):
+ self.assertEqual(result, (0, "bar"))
+ self.assertEqual(len(self.clock.calls), 0)
+ d.addCallback(check)
+ return d
+
+
+ def test_timeOutRaw(self):
+ """
+ Test the timeout when raw mode was started: the timeout is not reset
+ until all the data has been received, so we can have a L{TimeoutError}
+ when waiting for raw data.
+ """
+ d1 = self.proto.get("foo")
+ d2 = Deferred()
+ self.proto.connectionLost = d2.callback
+
+ self.proto.dataReceived("VALUE foo 0 10\r\n12345")
+ self.clock.advance(self.proto.persistentTimeOut)
+ self.assertFailure(d1, TimeoutError)
+ return gatherResults([d1, d2])
+
+
+ def test_timeOutStat(self):
+ """
+ Test the timeout when stat command has started: the timeout is not
+ reset until the final B{END} is received.
+ """
+ d1 = self.proto.stats()
+ d2 = Deferred()
+ self.proto.connectionLost = d2.callback
+
+ self.proto.dataReceived("STAT foo bar\r\n")
+ self.clock.advance(self.proto.persistentTimeOut)
+ self.assertFailure(d1, TimeoutError)
+ return gatherResults([d1, d2])
+
+
+ def test_timeoutPipelining(self):
+ """
+ When two requests are sent, a timeout call remains around for the
+ second request, and its timeout time is correct.
+ """
+ d1 = self.proto.get("foo")
+ d2 = self.proto.get("bar")
+ d3 = Deferred()
+ self.proto.connectionLost = d3.callback
+
+ self.clock.advance(self.proto.persistentTimeOut - 1)
+ self.proto.dataReceived("VALUE foo 0 3\r\nbar\r\nEND\r\n")
+
+ def check(result):
+ self.assertEqual(result, (0, "bar"))
+ self.assertEqual(len(self.clock.calls), 1)
+ for i in range(self.proto.persistentTimeOut):
+ self.clock.advance(1)
+ return self.assertFailure(d2, TimeoutError).addCallback(checkTime)
+ def checkTime(ignored):
+ # Check that the timeout happened C{self.proto.persistentTimeOut}
+ # after the last response
+ self.assertEqual(
+ self.clock.seconds(), 2 * self.proto.persistentTimeOut - 1)
+ d1.addCallback(check)
+ return d1
+
+
+ def test_timeoutNotReset(self):
+ """
+ Check that timeout is not resetted for every command, but keep the
+ timeout from the first command without response.
+ """
+ d1 = self.proto.get("foo")
+ d3 = Deferred()
+ self.proto.connectionLost = d3.callback
+
+ self.clock.advance(self.proto.persistentTimeOut - 1)
+ d2 = self.proto.get("bar")
+ self.clock.advance(1)
+ self.assertFailure(d1, TimeoutError)
+ self.assertFailure(d2, TimeoutError)
+ return gatherResults([d1, d2, d3])
+
+
+ def test_timeoutCleanDeferreds(self):
+ """
+ C{timeoutConnection} cleans the list of commands that it fires with
+ C{TimeoutError}: C{connectionLost} doesn't try to fire them again, but
+ sets the disconnected state so that future commands fail with a
+ C{RuntimeError}.
+ """
+ d1 = self.proto.get("foo")
+ self.clock.advance(self.proto.persistentTimeOut)
+ self.assertFailure(d1, TimeoutError)
+ d2 = self.proto.get("bar")
+ self.assertFailure(d2, RuntimeError)
+ return gatherResults([d1, d2])
+
+
+ def test_connectionLost(self):
+ """
+ When disconnection occurs while commands are still outstanding, the
+ commands fail.
+ """
+ d1 = self.proto.get("foo")
+ d2 = self.proto.get("bar")
+ self.transport.loseConnection()
+ done = DeferredList([d1, d2], consumeErrors=True)
+ def checkFailures(results):
+ for success, result in results:
+ self.assertFalse(success)
+ result.trap(ConnectionDone)
+ return done.addCallback(checkFailures)
+
+
+ def test_tooLongKey(self):
+ """
+ An error is raised when trying to use a too long key: the called
+ command returns a L{Deferred} which fails with a L{ClientError}.
+ """
+ d1 = self.assertFailure(self.proto.set("a" * 500, "bar"), ClientError)
+ d2 = self.assertFailure(self.proto.increment("a" * 500), ClientError)
+ d3 = self.assertFailure(self.proto.get("a" * 500), ClientError)
+ d4 = self.assertFailure(
+ self.proto.append("a" * 500, "bar"), ClientError)
+ d5 = self.assertFailure(
+ self.proto.prepend("a" * 500, "bar"), ClientError)
+ d6 = self.assertFailure(
+ self.proto.getMultiple(["foo", "a" * 500]), ClientError)
+ return gatherResults([d1, d2, d3, d4, d5, d6])
+
+
+ def test_invalidCommand(self):
+ """
+ When an unknown command is sent directly (not through public API), the
+ server answers with an B{ERROR} token, and the command fails with
+ L{NoSuchCommand}.
+ """
+ d = self.proto._set("egg", "foo", "bar", 0, 0, "")
+ self.assertEqual(self.transport.value(), "egg foo 0 0 3\r\nbar\r\n")
+ self.assertFailure(d, NoSuchCommand)
+ self.proto.dataReceived("ERROR\r\n")
+ return d
+
+
+ def test_clientError(self):
+ """
+ Test the L{ClientError} error: when the server sends a B{CLIENT_ERROR}
+ token, the originating command fails with L{ClientError}, and the error
+ contains the text sent by the server.
+ """
+ a = "eggspamm"
+ d = self.proto.set("foo", a)
+ self.assertEqual(self.transport.value(),
+ "set foo 0 0 8\r\neggspamm\r\n")
+ self.assertFailure(d, ClientError)
+ def check(err):
+ self.assertEqual(str(err), "We don't like egg and spam")
+ d.addCallback(check)
+ self.proto.dataReceived("CLIENT_ERROR We don't like egg and spam\r\n")
+ return d
+
+
+ def test_serverError(self):
+ """
+ Test the L{ServerError} error: when the server sends a B{SERVER_ERROR}
+ token, the originating command fails with L{ServerError}, and the error
+ contains the text sent by the server.
+ """
+ a = "eggspamm"
+ d = self.proto.set("foo", a)
+ self.assertEqual(self.transport.value(),
+ "set foo 0 0 8\r\neggspamm\r\n")
+ self.assertFailure(d, ServerError)
+ def check(err):
+ self.assertEqual(str(err), "zomg")
+ d.addCallback(check)
+ self.proto.dataReceived("SERVER_ERROR zomg\r\n")
+ return d
+
+
+ def test_unicodeKey(self):
+ """
+ Using a non-string key as argument to commands raises an error.
+ """
+ d1 = self.assertFailure(self.proto.set(u"foo", "bar"), ClientError)
+ d2 = self.assertFailure(self.proto.increment(u"egg"), ClientError)
+ d3 = self.assertFailure(self.proto.get(1), ClientError)
+ d4 = self.assertFailure(self.proto.delete(u"bar"), ClientError)
+ d5 = self.assertFailure(self.proto.append(u"foo", "bar"), ClientError)
+ d6 = self.assertFailure(self.proto.prepend(u"foo", "bar"), ClientError)
+ d7 = self.assertFailure(
+ self.proto.getMultiple(["egg", 1]), ClientError)
+ return gatherResults([d1, d2, d3, d4, d5, d6, d7])
+
+
+ def test_unicodeValue(self):
+ """
+ Using a non-string value raises an error.
+ """
+ return self.assertFailure(self.proto.set("foo", u"bar"), ClientError)
+
+
+ def test_pipelining(self):
+ """
+ Multiple requests can be sent subsequently to the server, and the
+ protocol orders the responses correctly and dispatch to the
+ corresponding client command.
+ """
+ d1 = self.proto.get("foo")
+ d1.addCallback(self.assertEqual, (0, "bar"))
+ d2 = self.proto.set("bar", "spamspamspam")
+ d2.addCallback(self.assertEqual, True)
+ d3 = self.proto.get("egg")
+ d3.addCallback(self.assertEqual, (0, "spam"))
+ self.assertEqual(self.transport.value(),
+ "get foo\r\nset bar 0 0 12\r\nspamspamspam\r\nget egg\r\n")
+ self.proto.dataReceived("VALUE foo 0 3\r\nbar\r\nEND\r\n"
+ "STORED\r\n"
+ "VALUE egg 0 4\r\nspam\r\nEND\r\n")
+ return gatherResults([d1, d2, d3])
+
+
+ def test_getInChunks(self):
+ """
+ If the value retrieved by a C{get} arrive in chunks, the protocol
+ is able to reconstruct it and to produce the good value.
+ """
+ d = self.proto.get("foo")
+ d.addCallback(self.assertEqual, (0, "0123456789"))
+ self.assertEqual(self.transport.value(), "get foo\r\n")
+ self.proto.dataReceived("VALUE foo 0 10\r\n0123456")
+ self.proto.dataReceived("789")
+ self.proto.dataReceived("\r\nEND")
+ self.proto.dataReceived("\r\n")
+ return d
+
+
+ def test_append(self):
+ """
+ L{MemCacheProtocol.append} behaves like a L{MemCacheProtocol.set}
+ method: it returns a L{Deferred} which is called back with C{True} when
+ the operation succeeds.
+ """
+ return self._test(self.proto.append("foo", "bar"),
+ "append foo 0 0 3\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_prepend(self):
+ """
+ L{MemCacheProtocol.prepend} behaves like a L{MemCacheProtocol.set}
+ method: it returns a L{Deferred} which is called back with C{True} when
+ the operation succeeds.
+ """
+ return self._test(self.proto.prepend("foo", "bar"),
+ "prepend foo 0 0 3\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_gets(self):
+ """
+ L{MemCacheProtocol.get} handles an additional cas result when
+ C{withIdentifier} is C{True} and forward it in the resulting
+ L{Deferred}.
+ """
+ return self._test(self.proto.get("foo", True), "gets foo\r\n",
+ "VALUE foo 0 3 1234\r\nbar\r\nEND\r\n", (0, "1234", "bar"))
+
+
+ def test_emptyGets(self):
+ """
+ Test getting a non-available key with gets: it succeeds but return
+ C{None} as value, C{0} as flag and an empty cas value.
+ """
+ return self._test(self.proto.get("foo", True), "gets foo\r\n",
+ "END\r\n", (0, "", None))
+
+
+ def test_getsMultiple(self):
+ """
+ L{MemCacheProtocol.getMultiple} handles an additional cas field in the
+ returned tuples if C{withIdentifier} is C{True}.
+ """
+ return self._test(self.proto.getMultiple(["foo", "bar"], True),
+ "gets foo bar\r\n",
+ "VALUE foo 0 3 1234\r\negg\r\nVALUE bar 0 4 2345\r\nspam\r\nEND\r\n",
+ {'bar': (0, '2345', 'spam'), 'foo': (0, '1234', 'egg')})
+
+
+ def test_getsMultipleWithEmpty(self):
+ """
+ When getting a non-available key with L{MemCacheProtocol.getMultiple}
+ when C{withIdentifier} is C{True}, the other keys are retrieved
+ correctly, and the non-available key gets a tuple of C{0} as flag,
+ C{None} as value, and an empty cas value.
+ """
+ return self._test(self.proto.getMultiple(["foo", "bar"], True),
+ "gets foo bar\r\n",
+ "VALUE foo 0 3 1234\r\negg\r\nEND\r\n",
+ {'bar': (0, '', None), 'foo': (0, '1234', 'egg')})
+
+
+ def test_checkAndSet(self):
+ """
+ L{MemCacheProtocol.checkAndSet} passes an additional cas identifier
+ that the server handles to check if the data has to be updated.
+ """
+ return self._test(self.proto.checkAndSet("foo", "bar", cas="1234"),
+ "cas foo 0 0 3 1234\r\nbar\r\n", "STORED\r\n", True)
+
+
+ def test_casUnknowKey(self):
+ """
+ When L{MemCacheProtocol.checkAndSet} response is C{EXISTS}, the
+ resulting L{Deferred} fires with C{False}.
+ """
+ return self._test(self.proto.checkAndSet("foo", "bar", cas="1234"),
+ "cas foo 0 0 3 1234\r\nbar\r\n", "EXISTS\r\n", False)
+
+
+
+class CommandFailureTests(CommandMixin, TestCase):
+ """
+ Tests for correct failure of commands on a disconnected
+ L{MemCacheProtocol}.
+ """
+
+ def setUp(self):
+ """
+ Create a disconnected memcache client, using a deterministic clock.
+ """
+ self.proto = MemCacheProtocol()
+ self.clock = Clock()
+ self.proto.callLater = self.clock.callLater
+ self.transport = StringTransportWithDisconnection()
+ self.transport.protocol = self.proto
+ self.proto.makeConnection(self.transport)
+ self.transport.loseConnection()
+
+
+ def _test(self, d, send, recv, result):
+ """
+ Implementation of C{_test} which checks that the command fails with
+ C{RuntimeError} because the transport is disconnected. All the
+ parameters except C{d} are ignored.
+ """
+ return self.assertFailure(d, RuntimeError)
diff --git a/twisted/test/test_modules.py b/twisted/test/test_modules.py
new file mode 100644
index 0000000..4b74f0d
--- /dev/null
+++ b/twisted/test/test_modules.py
@@ -0,0 +1,478 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for twisted.python.modules, abstract access to imported or importable
+objects.
+"""
+
+import sys
+import itertools
+import zipfile
+import compileall
+
+import twisted
+from twisted.trial.unittest import TestCase
+
+from twisted.python import modules
+from twisted.python.filepath import FilePath
+from twisted.python.reflect import namedAny
+
+from twisted.python.test.modules_helpers import TwistedModulesTestCase
+from twisted.test.test_paths import zipit
+
+
+
+class BasicTests(TwistedModulesTestCase):
+
+ def test_namespacedPackages(self):
+ """
+ Duplicate packages are not yielded when iterating over namespace
+ packages.
+ """
+ # Force pkgutil to be loaded already, since the probe package being
+ # created depends on it, and the replaceSysPath call below will make
+ # pretty much everything unimportable.
+ __import__('pkgutil')
+
+ namespaceBoilerplate = (
+ 'import pkgutil; '
+ '__path__ = pkgutil.extend_path(__path__, __name__)')
+
+ # Create two temporary directories with packages:
+ #
+ # entry:
+ # test_package/
+ # __init__.py
+ # nested_package/
+ # __init__.py
+ # module.py
+ #
+ # anotherEntry:
+ # test_package/
+ # __init__.py
+ # nested_package/
+ # __init__.py
+ # module2.py
+ #
+ # test_package and test_package.nested_package are namespace packages,
+ # and when both of these are in sys.path, test_package.nested_package
+ # should become a virtual package containing both "module" and
+ # "module2"
+
+ entry = self.pathEntryWithOnePackage()
+ testPackagePath = entry.child('test_package')
+ testPackagePath.child('__init__.py').setContent(namespaceBoilerplate)
+
+ nestedEntry = testPackagePath.child('nested_package')
+ nestedEntry.makedirs()
+ nestedEntry.child('__init__.py').setContent(namespaceBoilerplate)
+ nestedEntry.child('module.py').setContent('')
+
+ anotherEntry = self.pathEntryWithOnePackage()
+ anotherPackagePath = anotherEntry.child('test_package')
+ anotherPackagePath.child('__init__.py').setContent(namespaceBoilerplate)
+
+ anotherNestedEntry = anotherPackagePath.child('nested_package')
+ anotherNestedEntry.makedirs()
+ anotherNestedEntry.child('__init__.py').setContent(namespaceBoilerplate)
+ anotherNestedEntry.child('module2.py').setContent('')
+
+ self.replaceSysPath([entry.path, anotherEntry.path])
+
+ module = modules.getModule('test_package')
+
+ # We have to use importPackages=True in order to resolve the namespace
+ # packages, so we remove the imported packages from sys.modules after
+ # walking
+ try:
+ walkedNames = [
+ mod.name for mod in module.walkModules(importPackages=True)]
+ finally:
+ for module in sys.modules.keys():
+ if module.startswith('test_package'):
+ del sys.modules[module]
+
+ expected = [
+ 'test_package',
+ 'test_package.nested_package',
+ 'test_package.nested_package.module',
+ 'test_package.nested_package.module2',
+ ]
+
+ self.assertEqual(walkedNames, expected)
+
+
+ def test_unimportablePackageGetItem(self):
+ """
+ If a package has been explicitly forbidden from importing by setting a
+ C{None} key in sys.modules under its name,
+ L{modules.PythonPath.__getitem__} should still be able to retrieve an
+ unloaded L{modules.PythonModule} for that package.
+ """
+ shouldNotLoad = []
+ path = modules.PythonPath(sysPath=[self.pathEntryWithOnePackage().path],
+ moduleLoader=shouldNotLoad.append,
+ importerCache={},
+ sysPathHooks={},
+ moduleDict={'test_package': None})
+ self.assertEqual(shouldNotLoad, [])
+ self.assertEqual(path['test_package'].isLoaded(), False)
+
+
+ def test_unimportablePackageWalkModules(self):
+ """
+ If a package has been explicitly forbidden from importing by setting a
+ C{None} key in sys.modules under its name, L{modules.walkModules} should
+ still be able to retrieve an unloaded L{modules.PythonModule} for that
+ package.
+ """
+ existentPath = self.pathEntryWithOnePackage()
+ self.replaceSysPath([existentPath.path])
+ self.replaceSysModules({"test_package": None})
+
+ walked = list(modules.walkModules())
+ self.assertEqual([m.name for m in walked],
+ ["test_package"])
+ self.assertEqual(walked[0].isLoaded(), False)
+
+
+ def test_nonexistentPaths(self):
+ """
+ Verify that L{modules.walkModules} ignores entries in sys.path which
+ do not exist in the filesystem.
+ """
+ existentPath = self.pathEntryWithOnePackage()
+
+ nonexistentPath = FilePath(self.mktemp())
+ self.failIf(nonexistentPath.exists())
+
+ self.replaceSysPath([existentPath.path])
+
+ expected = [modules.getModule("test_package")]
+
+ beforeModules = list(modules.walkModules())
+ sys.path.append(nonexistentPath.path)
+ afterModules = list(modules.walkModules())
+
+ self.assertEqual(beforeModules, expected)
+ self.assertEqual(afterModules, expected)
+
+
+ def test_nonDirectoryPaths(self):
+ """
+ Verify that L{modules.walkModules} ignores entries in sys.path which
+ refer to regular files in the filesystem.
+ """
+ existentPath = self.pathEntryWithOnePackage()
+
+ nonDirectoryPath = FilePath(self.mktemp())
+ self.failIf(nonDirectoryPath.exists())
+ nonDirectoryPath.setContent("zip file or whatever\n")
+
+ self.replaceSysPath([existentPath.path])
+
+ beforeModules = list(modules.walkModules())
+ sys.path.append(nonDirectoryPath.path)
+ afterModules = list(modules.walkModules())
+
+ self.assertEqual(beforeModules, afterModules)
+
+
+ def test_twistedShowsUp(self):
+ """
+ Scrounge around in the top-level module namespace and make sure that
+ Twisted shows up, and that the module thusly obtained is the same as
+ the module that we find when we look for it explicitly by name.
+ """
+ self.assertEqual(modules.getModule('twisted'),
+ self.findByIteration("twisted"))
+
+
+ def test_dottedNames(self):
+ """
+ Verify that the walkModules APIs will give us back subpackages, not just
+ subpackages.
+ """
+ self.assertEqual(
+ modules.getModule('twisted.python'),
+ self.findByIteration("twisted.python",
+ where=modules.getModule('twisted')))
+
+
+ def test_onlyTopModules(self):
+ """
+ Verify that the iterModules API will only return top-level modules and
+ packages, not submodules or subpackages.
+ """
+ for module in modules.iterModules():
+ self.failIf(
+ '.' in module.name,
+ "no nested modules should be returned from iterModules: %r"
+ % (module.filePath))
+
+
+ def test_loadPackagesAndModules(self):
+ """
+ Verify that we can locate and load packages, modules, submodules, and
+ subpackages.
+ """
+ for n in ['os',
+ 'twisted',
+ 'twisted.python',
+ 'twisted.python.reflect']:
+ m = namedAny(n)
+ self.failUnlessIdentical(
+ modules.getModule(n).load(),
+ m)
+ self.failUnlessIdentical(
+ self.findByIteration(n).load(),
+ m)
+
+
+ def test_pathEntriesOnPath(self):
+ """
+ Verify that path entries discovered via module loading are, in fact, on
+ sys.path somewhere.
+ """
+ for n in ['os',
+ 'twisted',
+ 'twisted.python',
+ 'twisted.python.reflect']:
+ self.failUnlessIn(
+ modules.getModule(n).pathEntry.filePath.path,
+ sys.path)
+
+
+ def test_alwaysPreferPy(self):
+ """
+ Verify that .py files will always be preferred to .pyc files, regardless of
+ directory listing order.
+ """
+ mypath = FilePath(self.mktemp())
+ mypath.createDirectory()
+ pp = modules.PythonPath(sysPath=[mypath.path])
+ originalSmartPath = pp._smartPath
+ def _evilSmartPath(pathName):
+ o = originalSmartPath(pathName)
+ originalChildren = o.children
+ def evilChildren():
+ # normally this order is random; let's make sure it always
+ # comes up .pyc-first.
+ x = originalChildren()
+ x.sort()
+ x.reverse()
+ return x
+ o.children = evilChildren
+ return o
+ mypath.child("abcd.py").setContent('\n')
+ compileall.compile_dir(mypath.path, quiet=True)
+ # sanity check
+ self.assertEqual(len(mypath.children()), 2)
+ pp._smartPath = _evilSmartPath
+ self.assertEqual(pp['abcd'].filePath,
+ mypath.child('abcd.py'))
+
+
+ def test_packageMissingPath(self):
+ """
+ A package can delete its __path__ for some reasons,
+ C{modules.PythonPath} should be able to deal with it.
+ """
+ mypath = FilePath(self.mktemp())
+ mypath.createDirectory()
+ pp = modules.PythonPath(sysPath=[mypath.path])
+ subpath = mypath.child("abcd")
+ subpath.createDirectory()
+ subpath.child("__init__.py").setContent('del __path__\n')
+ sys.path.append(mypath.path)
+ __import__("abcd")
+ try:
+ l = list(pp.walkModules())
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].name, 'abcd')
+ finally:
+ del sys.modules['abcd']
+ sys.path.remove(mypath.path)
+
+
+
+class PathModificationTest(TwistedModulesTestCase):
+ """
+ These tests share setup/cleanup behavior of creating a dummy package and
+ stuffing some code in it.
+ """
+
+ _serialnum = itertools.count().next # used to generate serial numbers for
+ # package names.
+
+ def setUp(self):
+ self.pathExtensionName = self.mktemp()
+ self.pathExtension = FilePath(self.pathExtensionName)
+ self.pathExtension.createDirectory()
+ self.packageName = "pyspacetests%d" % (self._serialnum(),)
+ self.packagePath = self.pathExtension.child(self.packageName)
+ self.packagePath.createDirectory()
+ self.packagePath.child("__init__.py").setContent("")
+ self.packagePath.child("a.py").setContent("")
+ self.packagePath.child("b.py").setContent("")
+ self.packagePath.child("c__init__.py").setContent("")
+ self.pathSetUp = False
+
+
+ def _setupSysPath(self):
+ assert not self.pathSetUp
+ self.pathSetUp = True
+ sys.path.append(self.pathExtensionName)
+
+
+ def _underUnderPathTest(self, doImport=True):
+ moddir2 = self.mktemp()
+ fpmd = FilePath(moddir2)
+ fpmd.createDirectory()
+ fpmd.child("foozle.py").setContent("x = 123\n")
+ self.packagePath.child("__init__.py").setContent(
+ "__path__.append(%r)\n" % (moddir2,))
+ # Cut here
+ self._setupSysPath()
+ modinfo = modules.getModule(self.packageName)
+ self.assertEqual(
+ self.findByIteration(self.packageName+".foozle", modinfo,
+ importPackages=doImport),
+ modinfo['foozle'])
+ self.assertEqual(modinfo['foozle'].load().x, 123)
+
+
+ def test_underUnderPathAlreadyImported(self):
+ """
+ Verify that iterModules will honor the __path__ of already-loaded packages.
+ """
+ self._underUnderPathTest()
+
+
+ def test_underUnderPathNotAlreadyImported(self):
+ """
+ Verify that iterModules will honor the __path__ of already-loaded packages.
+ """
+ self._underUnderPathTest(False)
+
+
+ test_underUnderPathNotAlreadyImported.todo = (
+ "This may be impossible but it sure would be nice.")
+
+
+ def _listModules(self):
+ pkginfo = modules.getModule(self.packageName)
+ nfni = [modinfo.name.split(".")[-1] for modinfo in
+ pkginfo.iterModules()]
+ nfni.sort()
+ self.assertEqual(nfni, ['a', 'b', 'c__init__'])
+
+
+ def test_listingModules(self):
+ """
+ Make sure the module list comes back as we expect from iterModules on a
+ package, whether zipped or not.
+ """
+ self._setupSysPath()
+ self._listModules()
+
+
+ def test_listingModulesAlreadyImported(self):
+ """
+ Make sure the module list comes back as we expect from iterModules on a
+ package, whether zipped or not, even if the package has already been
+ imported.
+ """
+ self._setupSysPath()
+ namedAny(self.packageName)
+ self._listModules()
+
+
+ def tearDown(self):
+ # Intentionally using 'assert' here, this is not a test assertion, this
+ # is just an "oh fuck what is going ON" assertion. -glyph
+ if self.pathSetUp:
+ HORK = "path cleanup failed: don't be surprised if other tests break"
+ assert sys.path.pop() is self.pathExtensionName, HORK+", 1"
+ assert self.pathExtensionName not in sys.path, HORK+", 2"
+
+
+
+class RebindingTest(PathModificationTest):
+ """
+ These tests verify that the default path interrogation API works properly
+ even when sys.path has been rebound to a different object.
+ """
+ def _setupSysPath(self):
+ assert not self.pathSetUp
+ self.pathSetUp = True
+ self.savedSysPath = sys.path
+ sys.path = sys.path[:]
+ sys.path.append(self.pathExtensionName)
+
+
+ def tearDown(self):
+ """
+ Clean up sys.path by re-binding our original object.
+ """
+ if self.pathSetUp:
+ sys.path = self.savedSysPath
+
+
+
+class ZipPathModificationTest(PathModificationTest):
+ def _setupSysPath(self):
+ assert not self.pathSetUp
+ zipit(self.pathExtensionName, self.pathExtensionName+'.zip')
+ self.pathExtensionName += '.zip'
+ assert zipfile.is_zipfile(self.pathExtensionName)
+ PathModificationTest._setupSysPath(self)
+
+
+class PythonPathTestCase(TestCase):
+ """
+ Tests for the class which provides the implementation for all of the
+ public API of L{twisted.python.modules}, L{PythonPath}.
+ """
+ def test_unhandledImporter(self):
+ """
+ Make sure that the behavior when encountering an unknown importer
+ type is not catastrophic failure.
+ """
+ class SecretImporter(object):
+ pass
+
+ def hook(name):
+ return SecretImporter()
+
+ syspath = ['example/path']
+ sysmodules = {}
+ syshooks = [hook]
+ syscache = {}
+ def sysloader(name):
+ return None
+ space = modules.PythonPath(
+ syspath, sysmodules, syshooks, syscache, sysloader)
+ entries = list(space.iterEntries())
+ self.assertEqual(len(entries), 1)
+ self.assertRaises(KeyError, lambda: entries[0]['module'])
+
+
+ def test_inconsistentImporterCache(self):
+ """
+ If the path a module loaded with L{PythonPath.__getitem__} is not
+ present in the path importer cache, a warning is emitted, but the
+ L{PythonModule} is returned as usual.
+ """
+ space = modules.PythonPath([], sys.modules, [], {})
+ thisModule = space[__name__]
+ warnings = self.flushWarnings([self.test_inconsistentImporterCache])
+ self.assertEqual(warnings[0]['category'], UserWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ FilePath(twisted.__file__).parent().dirname() +
+ " (for module " + __name__ + ") not in path importer cache "
+ "(PEP 302 violation - check your local configuration).")
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(thisModule.name, __name__)
diff --git a/twisted/test/test_monkey.py b/twisted/test/test_monkey.py
new file mode 100644
index 0000000..95c0454
--- /dev/null
+++ b/twisted/test/test_monkey.py
@@ -0,0 +1,161 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.monkey}.
+"""
+
+from twisted.trial import unittest
+from twisted.python.monkey import MonkeyPatcher
+
+
+class TestObj:
+ def __init__(self):
+ self.foo = 'foo value'
+ self.bar = 'bar value'
+ self.baz = 'baz value'
+
+
+class MonkeyPatcherTest(unittest.TestCase):
+ """
+ Tests for L{MonkeyPatcher} monkey-patching class.
+ """
+
+ def setUp(self):
+ self.testObject = TestObj()
+ self.originalObject = TestObj()
+ self.monkeyPatcher = MonkeyPatcher()
+
+
+ def test_empty(self):
+ """
+ A monkey patcher without patches shouldn't change a thing.
+ """
+ self.monkeyPatcher.patch()
+
+ # We can't assert that all state is unchanged, but at least we can
+ # check our test object.
+ self.assertEqual(self.originalObject.foo, self.testObject.foo)
+ self.assertEqual(self.originalObject.bar, self.testObject.bar)
+ self.assertEqual(self.originalObject.baz, self.testObject.baz)
+
+
+ def test_constructWithPatches(self):
+ """
+ Constructing a L{MonkeyPatcher} with patches should add all of the
+ given patches to the patch list.
+ """
+ patcher = MonkeyPatcher((self.testObject, 'foo', 'haha'),
+ (self.testObject, 'bar', 'hehe'))
+ patcher.patch()
+ self.assertEqual('haha', self.testObject.foo)
+ self.assertEqual('hehe', self.testObject.bar)
+ self.assertEqual(self.originalObject.baz, self.testObject.baz)
+
+
+ def test_patchExisting(self):
+ """
+ Patching an attribute that exists sets it to the value defined in the
+ patch.
+ """
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'haha')
+ self.monkeyPatcher.patch()
+ self.assertEqual(self.testObject.foo, 'haha')
+
+
+ def test_patchNonExisting(self):
+ """
+ Patching a non-existing attribute fails with an C{AttributeError}.
+ """
+ self.monkeyPatcher.addPatch(self.testObject, 'nowhere',
+ 'blow up please')
+ self.assertRaises(AttributeError, self.monkeyPatcher.patch)
+
+
+ def test_patchAlreadyPatched(self):
+ """
+ Adding a patch for an object and attribute that already have a patch
+ overrides the existing patch.
+ """
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'blah')
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'BLAH')
+ self.monkeyPatcher.patch()
+ self.assertEqual(self.testObject.foo, 'BLAH')
+ self.monkeyPatcher.restore()
+ self.assertEqual(self.testObject.foo, self.originalObject.foo)
+
+
+ def test_restoreTwiceIsANoOp(self):
+ """
+ Restoring an already-restored monkey patch is a no-op.
+ """
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'blah')
+ self.monkeyPatcher.patch()
+ self.monkeyPatcher.restore()
+ self.assertEqual(self.testObject.foo, self.originalObject.foo)
+ self.monkeyPatcher.restore()
+ self.assertEqual(self.testObject.foo, self.originalObject.foo)
+
+
+ def test_runWithPatchesDecoration(self):
+ """
+ runWithPatches should run the given callable, passing in all arguments
+ and keyword arguments, and return the return value of the callable.
+ """
+ log = []
+
+ def f(a, b, c=None):
+ log.append((a, b, c))
+ return 'foo'
+
+ result = self.monkeyPatcher.runWithPatches(f, 1, 2, c=10)
+ self.assertEqual('foo', result)
+ self.assertEqual([(1, 2, 10)], log)
+
+
+ def test_repeatedRunWithPatches(self):
+ """
+ We should be able to call the same function with runWithPatches more
+ than once. All patches should apply for each call.
+ """
+ def f():
+ return (self.testObject.foo, self.testObject.bar,
+ self.testObject.baz)
+
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'haha')
+ result = self.monkeyPatcher.runWithPatches(f)
+ self.assertEqual(
+ ('haha', self.originalObject.bar, self.originalObject.baz), result)
+ result = self.monkeyPatcher.runWithPatches(f)
+ self.assertEqual(
+ ('haha', self.originalObject.bar, self.originalObject.baz),
+ result)
+
+
+ def test_runWithPatchesRestores(self):
+ """
+ C{runWithPatches} should restore the original values after the function
+ has executed.
+ """
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'haha')
+ self.assertEqual(self.originalObject.foo, self.testObject.foo)
+ self.monkeyPatcher.runWithPatches(lambda: None)
+ self.assertEqual(self.originalObject.foo, self.testObject.foo)
+
+
+ def test_runWithPatchesRestoresOnException(self):
+ """
+ Test runWithPatches restores the original values even when the function
+ raises an exception.
+ """
+ def _():
+ self.assertEqual(self.testObject.foo, 'haha')
+ self.assertEqual(self.testObject.bar, 'blahblah')
+ raise RuntimeError, "Something went wrong!"
+
+ self.monkeyPatcher.addPatch(self.testObject, 'foo', 'haha')
+ self.monkeyPatcher.addPatch(self.testObject, 'bar', 'blahblah')
+
+ self.assertRaises(RuntimeError, self.monkeyPatcher.runWithPatches, _)
+ self.assertEqual(self.testObject.foo, self.originalObject.foo)
+ self.assertEqual(self.testObject.bar, self.originalObject.bar)
diff --git a/twisted/test/test_newcred.py b/twisted/test/test_newcred.py
new file mode 100644
index 0000000..870833a
--- /dev/null
+++ b/twisted/test/test_newcred.py
@@ -0,0 +1,445 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.cred}, now with 30% more starch.
+"""
+
+
+import hmac
+from zope.interface import implements, Interface
+
+from twisted.trial import unittest
+from twisted.cred import portal, checkers, credentials, error
+from twisted.python import components
+from twisted.internet import defer
+from twisted.internet.defer import deferredGenerator as dG, waitForDeferred as wFD
+
+try:
+ from crypt import crypt
+except ImportError:
+ crypt = None
+
+try:
+ from twisted.cred.pamauth import callIntoPAM
+except ImportError:
+ pamauth = None
+else:
+ from twisted.cred import pamauth
+
+
+class ITestable(Interface):
+ pass
+
+class TestAvatar:
+ def __init__(self, name):
+ self.name = name
+ self.loggedIn = False
+ self.loggedOut = False
+
+ def login(self):
+ assert not self.loggedIn
+ self.loggedIn = True
+
+ def logout(self):
+ self.loggedOut = True
+
+class Testable(components.Adapter):
+ implements(ITestable)
+
+# components.Interface(TestAvatar).adaptWith(Testable, ITestable)
+
+components.registerAdapter(Testable, TestAvatar, ITestable)
+
+class IDerivedCredentials(credentials.IUsernamePassword):
+ pass
+
+class DerivedCredentials(object):
+ implements(IDerivedCredentials, ITestable)
+
+ def __init__(self, username, password):
+ self.username = username
+ self.password = password
+
+ def checkPassword(self, password):
+ return password == self.password
+
+
+class TestRealm:
+ implements(portal.IRealm)
+ def __init__(self):
+ self.avatars = {}
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if self.avatars.has_key(avatarId):
+ avatar = self.avatars[avatarId]
+ else:
+ avatar = TestAvatar(avatarId)
+ self.avatars[avatarId] = avatar
+ avatar.login()
+ return (interfaces[0], interfaces[0](avatar),
+ avatar.logout)
+
+class NewCredTest(unittest.TestCase):
+ def setUp(self):
+ r = self.realm = TestRealm()
+ p = self.portal = portal.Portal(r)
+ up = self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ up.addUser("bob", "hello")
+ p.registerChecker(up)
+
+ def testListCheckers(self):
+ expected = [credentials.IUsernamePassword, credentials.IUsernameHashedPassword]
+ got = self.portal.listCredentialsInterfaces()
+ expected.sort()
+ got.sort()
+ self.assertEqual(got, expected)
+
+ def testBasicLogin(self):
+ l = []; f = []
+ self.portal.login(credentials.UsernamePassword("bob", "hello"),
+ self, ITestable).addCallback(
+ l.append).addErrback(f.append)
+ if f:
+ raise f[0]
+ # print l[0].getBriefTraceback()
+ iface, impl, logout = l[0]
+ # whitebox
+ self.assertEqual(iface, ITestable)
+ self.failUnless(iface.providedBy(impl),
+ "%s does not implement %s" % (impl, iface))
+ # greybox
+ self.failUnless(impl.original.loggedIn)
+ self.failUnless(not impl.original.loggedOut)
+ logout()
+ self.failUnless(impl.original.loggedOut)
+
+ def test_derivedInterface(self):
+ """
+ Login with credentials implementing an interface inheriting from an
+ interface registered with a checker (but not itself registered).
+ """
+ l = []
+ f = []
+ self.portal.login(DerivedCredentials("bob", "hello"), self, ITestable
+ ).addCallback(l.append
+ ).addErrback(f.append)
+ if f:
+ raise f[0]
+ iface, impl, logout = l[0]
+ # whitebox
+ self.assertEqual(iface, ITestable)
+ self.failUnless(iface.providedBy(impl),
+ "%s does not implement %s" % (impl, iface))
+ # greybox
+ self.failUnless(impl.original.loggedIn)
+ self.failUnless(not impl.original.loggedOut)
+ logout()
+ self.failUnless(impl.original.loggedOut)
+
+ def testFailedLogin(self):
+ l = []
+ self.portal.login(credentials.UsernamePassword("bob", "h3llo"),
+ self, ITestable).addErrback(
+ lambda x: x.trap(error.UnauthorizedLogin)).addCallback(l.append)
+ self.failUnless(l)
+ self.assertEqual(error.UnauthorizedLogin, l[0])
+
+ def testFailedLoginName(self):
+ l = []
+ self.portal.login(credentials.UsernamePassword("jay", "hello"),
+ self, ITestable).addErrback(
+ lambda x: x.trap(error.UnauthorizedLogin)).addCallback(l.append)
+ self.failUnless(l)
+ self.assertEqual(error.UnauthorizedLogin, l[0])
+
+
+class CramMD5CredentialsTestCase(unittest.TestCase):
+ def testIdempotentChallenge(self):
+ c = credentials.CramMD5Credentials()
+ chal = c.getChallenge()
+ self.assertEqual(chal, c.getChallenge())
+
+ def testCheckPassword(self):
+ c = credentials.CramMD5Credentials()
+ chal = c.getChallenge()
+ c.response = hmac.HMAC('secret', chal).hexdigest()
+ self.failUnless(c.checkPassword('secret'))
+
+ def testWrongPassword(self):
+ c = credentials.CramMD5Credentials()
+ self.failIf(c.checkPassword('secret'))
+
+class OnDiskDatabaseTestCase(unittest.TestCase):
+ users = [
+ ('user1', 'pass1'),
+ ('user2', 'pass2'),
+ ('user3', 'pass3'),
+ ]
+
+
+ def testUserLookup(self):
+ dbfile = self.mktemp()
+ db = checkers.FilePasswordDB(dbfile)
+ f = file(dbfile, 'w')
+ for (u, p) in self.users:
+ f.write('%s:%s\n' % (u, p))
+ f.close()
+
+ for (u, p) in self.users:
+ self.failUnlessRaises(KeyError, db.getUser, u.upper())
+ self.assertEqual(db.getUser(u), (u, p))
+
+ def testCaseInSensitivity(self):
+ dbfile = self.mktemp()
+ db = checkers.FilePasswordDB(dbfile, caseSensitive=0)
+ f = file(dbfile, 'w')
+ for (u, p) in self.users:
+ f.write('%s:%s\n' % (u, p))
+ f.close()
+
+ for (u, p) in self.users:
+ self.assertEqual(db.getUser(u.upper()), (u, p))
+
+ def testRequestAvatarId(self):
+ dbfile = self.mktemp()
+ db = checkers.FilePasswordDB(dbfile, caseSensitive=0)
+ f = file(dbfile, 'w')
+ for (u, p) in self.users:
+ f.write('%s:%s\n' % (u, p))
+ f.close()
+ creds = [credentials.UsernamePassword(u, p) for u, p in self.users]
+ d = defer.gatherResults(
+ [defer.maybeDeferred(db.requestAvatarId, c) for c in creds])
+ d.addCallback(self.assertEqual, [u for u, p in self.users])
+ return d
+
+ def testRequestAvatarId_hashed(self):
+ dbfile = self.mktemp()
+ db = checkers.FilePasswordDB(dbfile, caseSensitive=0)
+ f = file(dbfile, 'w')
+ for (u, p) in self.users:
+ f.write('%s:%s\n' % (u, p))
+ f.close()
+ creds = [credentials.UsernameHashedPassword(u, p) for u, p in self.users]
+ d = defer.gatherResults(
+ [defer.maybeDeferred(db.requestAvatarId, c) for c in creds])
+ d.addCallback(self.assertEqual, [u for u, p in self.users])
+ return d
+
+
+
+class HashedPasswordOnDiskDatabaseTestCase(unittest.TestCase):
+ users = [
+ ('user1', 'pass1'),
+ ('user2', 'pass2'),
+ ('user3', 'pass3'),
+ ]
+
+
+ def hash(self, u, p, s):
+ return crypt(p, s)
+
+ def setUp(self):
+ dbfile = self.mktemp()
+ self.db = checkers.FilePasswordDB(dbfile, hash=self.hash)
+ f = file(dbfile, 'w')
+ for (u, p) in self.users:
+ f.write('%s:%s\n' % (u, crypt(p, u[:2])))
+ f.close()
+ r = TestRealm()
+ self.port = portal.Portal(r)
+ self.port.registerChecker(self.db)
+
+ def testGoodCredentials(self):
+ goodCreds = [credentials.UsernamePassword(u, p) for u, p in self.users]
+ d = defer.gatherResults([self.db.requestAvatarId(c) for c in goodCreds])
+ d.addCallback(self.assertEqual, [u for u, p in self.users])
+ return d
+
+ def testGoodCredentials_login(self):
+ goodCreds = [credentials.UsernamePassword(u, p) for u, p in self.users]
+ d = defer.gatherResults([self.port.login(c, None, ITestable)
+ for c in goodCreds])
+ d.addCallback(lambda x: [a.original.name for i, a, l in x])
+ d.addCallback(self.assertEqual, [u for u, p in self.users])
+ return d
+
+ def testBadCredentials(self):
+ badCreds = [credentials.UsernamePassword(u, 'wrong password')
+ for u, p in self.users]
+ d = defer.DeferredList([self.port.login(c, None, ITestable)
+ for c in badCreds], consumeErrors=True)
+ d.addCallback(self._assertFailures, error.UnauthorizedLogin)
+ return d
+
+ def testHashedCredentials(self):
+ hashedCreds = [credentials.UsernameHashedPassword(u, crypt(p, u[:2]))
+ for u, p in self.users]
+ d = defer.DeferredList([self.port.login(c, None, ITestable)
+ for c in hashedCreds], consumeErrors=True)
+ d.addCallback(self._assertFailures, error.UnhandledCredentials)
+ return d
+
+ def _assertFailures(self, failures, *expectedFailures):
+ for flag, failure in failures:
+ self.assertEqual(flag, defer.FAILURE)
+ failure.trap(*expectedFailures)
+ return None
+
+ if crypt is None:
+ skip = "crypt module not available"
+
+class PluggableAuthenticationModulesTest(unittest.TestCase):
+
+ def setUp(self):
+ """
+ Replace L{pamauth.callIntoPAM} with a dummy implementation with
+ easily-controlled behavior.
+ """
+ self._oldCallIntoPAM = pamauth.callIntoPAM
+ pamauth.callIntoPAM = self.callIntoPAM
+
+
+ def tearDown(self):
+ """
+ Restore the original value of L{pamauth.callIntoPAM}.
+ """
+ pamauth.callIntoPAM = self._oldCallIntoPAM
+
+
+ def callIntoPAM(self, service, user, conv):
+ if service != 'Twisted':
+ raise error.UnauthorizedLogin('bad service: %s' % service)
+ if user != 'testuser':
+ raise error.UnauthorizedLogin('bad username: %s' % user)
+ questions = [
+ (1, "Password"),
+ (2, "Message w/ Input"),
+ (3, "Message w/o Input"),
+ ]
+ replies = conv(questions)
+ if replies != [
+ ("password", 0),
+ ("entry", 0),
+ ("", 0)
+ ]:
+ raise error.UnauthorizedLogin('bad conversion: %s' % repr(replies))
+ return 1
+
+ def _makeConv(self, d):
+ def conv(questions):
+ return defer.succeed([(d[t], 0) for t, q in questions])
+ return conv
+
+ def testRequestAvatarId(self):
+ db = checkers.PluggableAuthenticationModulesChecker()
+ conv = self._makeConv({1:'password', 2:'entry', 3:''})
+ creds = credentials.PluggableAuthenticationModules('testuser',
+ conv)
+ d = db.requestAvatarId(creds)
+ d.addCallback(self.assertEqual, 'testuser')
+ return d
+
+ def testBadCredentials(self):
+ db = checkers.PluggableAuthenticationModulesChecker()
+ conv = self._makeConv({1:'', 2:'', 3:''})
+ creds = credentials.PluggableAuthenticationModules('testuser',
+ conv)
+ d = db.requestAvatarId(creds)
+ self.assertFailure(d, error.UnauthorizedLogin)
+ return d
+
+ def testBadUsername(self):
+ db = checkers.PluggableAuthenticationModulesChecker()
+ conv = self._makeConv({1:'password', 2:'entry', 3:''})
+ creds = credentials.PluggableAuthenticationModules('baduser',
+ conv)
+ d = db.requestAvatarId(creds)
+ self.assertFailure(d, error.UnauthorizedLogin)
+ return d
+
+ if not pamauth:
+ skip = "Can't run without PyPAM"
+
+class CheckersMixin:
+ def testPositive(self):
+ for chk in self.getCheckers():
+ for (cred, avatarId) in self.getGoodCredentials():
+ r = wFD(chk.requestAvatarId(cred))
+ yield r
+ self.assertEqual(r.getResult(), avatarId)
+ testPositive = dG(testPositive)
+
+ def testNegative(self):
+ for chk in self.getCheckers():
+ for cred in self.getBadCredentials():
+ r = wFD(chk.requestAvatarId(cred))
+ yield r
+ self.assertRaises(error.UnauthorizedLogin, r.getResult)
+ testNegative = dG(testNegative)
+
+class HashlessFilePasswordDBMixin:
+ credClass = credentials.UsernamePassword
+ diskHash = None
+ networkHash = staticmethod(lambda x: x)
+
+ _validCredentials = [
+ ('user1', 'password1'),
+ ('user2', 'password2'),
+ ('user3', 'password3')]
+
+ def getGoodCredentials(self):
+ for u, p in self._validCredentials:
+ yield self.credClass(u, self.networkHash(p)), u
+
+ def getBadCredentials(self):
+ for u, p in [('user1', 'password3'),
+ ('user2', 'password1'),
+ ('bloof', 'blarf')]:
+ yield self.credClass(u, self.networkHash(p))
+
+ def getCheckers(self):
+ diskHash = self.diskHash or (lambda x: x)
+ hashCheck = self.diskHash and (lambda username, password, stored: self.diskHash(password))
+
+ for cache in True, False:
+ fn = self.mktemp()
+ fObj = file(fn, 'w')
+ for u, p in self._validCredentials:
+ fObj.write('%s:%s\n' % (u, diskHash(p)))
+ fObj.close()
+ yield checkers.FilePasswordDB(fn, cache=cache, hash=hashCheck)
+
+ fn = self.mktemp()
+ fObj = file(fn, 'w')
+ for u, p in self._validCredentials:
+ fObj.write('%s dingle dongle %s\n' % (diskHash(p), u))
+ fObj.close()
+ yield checkers.FilePasswordDB(fn, ' ', 3, 0, cache=cache, hash=hashCheck)
+
+ fn = self.mktemp()
+ fObj = file(fn, 'w')
+ for u, p in self._validCredentials:
+ fObj.write('zip,zap,%s,zup,%s\n' % (u.title(), diskHash(p)))
+ fObj.close()
+ yield checkers.FilePasswordDB(fn, ',', 2, 4, False, cache=cache, hash=hashCheck)
+
+class LocallyHashedFilePasswordDBMixin(HashlessFilePasswordDBMixin):
+ diskHash = staticmethod(lambda x: x.encode('hex'))
+
+class NetworkHashedFilePasswordDBMixin(HashlessFilePasswordDBMixin):
+ networkHash = staticmethod(lambda x: x.encode('hex'))
+ class credClass(credentials.UsernameHashedPassword):
+ def checkPassword(self, password):
+ return self.hashed.decode('hex') == password
+
+class HashlessFilePasswordDBCheckerTestCase(HashlessFilePasswordDBMixin, CheckersMixin, unittest.TestCase):
+ pass
+
+class LocallyHashedFilePasswordDBCheckerTestCase(LocallyHashedFilePasswordDBMixin, CheckersMixin, unittest.TestCase):
+ pass
+
+class NetworkHashedFilePasswordDBCheckerTestCase(NetworkHashedFilePasswordDBMixin, CheckersMixin, unittest.TestCase):
+ pass
+
diff --git a/twisted/test/test_nmea.py b/twisted/test/test_nmea.py
new file mode 100644
index 0000000..9c4afbc
--- /dev/null
+++ b/twisted/test/test_nmea.py
@@ -0,0 +1,115 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Test cases for the NMEA GPS protocol"""
+
+import StringIO
+
+from twisted.trial import unittest
+from twisted.internet import reactor, protocol
+from twisted.python import reflect
+
+from twisted.protocols.gps import nmea
+
+class StringIOWithNoClose(StringIO.StringIO):
+ def close(self):
+ pass
+
+class ResultHarvester:
+ def __init__(self):
+ self.results = []
+
+ def __call__(self, *args):
+ self.results.append(args)
+
+ def performTest(self, function, *args, **kwargs):
+ l = len(self.results)
+ try:
+ function(*args, **kwargs)
+ except Exception, e:
+ self.results.append(e)
+ if l == len(self.results):
+ self.results.append(NotImplementedError())
+
+class NMEATester(nmea.NMEAReceiver):
+ ignore_invalid_sentence = 0
+ ignore_checksum_mismatch = 0
+ ignore_unknown_sentencetypes = 0
+ convert_dates_before_y2k = 1
+
+ def connectionMade(self):
+ self.resultHarvester = ResultHarvester()
+ for fn in reflect.prefixedMethodNames(self.__class__, 'decode_'):
+ setattr(self, 'handle_' + fn, self.resultHarvester)
+
+class NMEAReceiverTestCase(unittest.TestCase):
+ messages = (
+ # fix - signal acquired
+ "$GPGGA,231713.0,3910.413,N,07641.994,W,1,05,1.35,00044,M,-033,M,,*69",
+ # fix - signal not acquired
+ "$GPGGA,235947.000,0000.0000,N,00000.0000,E,0,00,0.0,0.0,M,,,,0000*00",
+ # junk
+ "lkjasdfkl!@#(*$!@(*#(ASDkfjasdfLMASDCVKAW!@#($)!(@#)(*",
+ # fix - signal acquired (invalid checksum)
+ "$GPGGA,231713.0,3910.413,N,07641.994,W,1,05,1.35,00044,M,-033,M,,*68",
+ # invalid sentence
+ "$GPGGX,231713.0,3910.413,N,07641.994,W,1,05,1.35,00044,M,-033,M,,*68",
+ # position acquired
+ "$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D",
+ # position not acquired
+ "$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D",
+ # active satellites (no fix)
+ "$GPGSA,A,1,,,,,,,,,,,,,0.0,0.0,0.0*30",
+ # active satellites
+ "$GPGSA,A,3,01,20,19,13,,,,,,,,,40.4,24.4,32.2*0A",
+ # positiontime (no fix)
+ "$GPRMC,235947.000,V,0000.0000,N,00000.0000,E,,,041299,,*1D",
+ # positiontime
+ "$GPRMC,092204.999,A,4250.5589,S,14718.5084,E,0.00,89.68,211200,,*25",
+ # course over ground (no fix - not implemented)
+ "$GPVTG,,T,,M,,N,,K*4E",
+ # course over ground (not implemented)
+ "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F",
+ )
+ results = (
+ (83833.0, 39.17355, -76.6999, nmea.POSFIX_SPS, 5, 1.35, (44.0, 'M'), (-33.0, 'M'), None),
+ (86387.0, 0.0, 0.0, 0, 0, 0.0, (0.0, 'M'), None, None),
+ nmea.InvalidSentence(),
+ nmea.InvalidChecksum(),
+ nmea.InvalidSentence(),
+ (-42.842648333333337, 147.30847333333332, 33724.999000000003, 1),
+ (0.0, 0.0, 86387.0, 0),
+ ((None, None, None, None, None, None, None, None, None, None, None, None), (nmea.MODE_AUTO, nmea.MODE_NOFIX), 0.0, 0.0, 0.0),
+ ((1, 20, 19, 13, None, None, None, None, None, None, None, None), (nmea.MODE_AUTO, nmea.MODE_3D), 40.4, 24.4, 32.2),
+ (0.0, 0.0, None, None, 86387.0, (1999, 12, 4), None),
+ (-42.842648333333337, 147.30847333333332, 0.0, 89.68, 33724.999, (2000, 12, 21), None),
+ NotImplementedError(),
+ NotImplementedError(),
+ )
+ def testGPSMessages(self):
+ dummy = NMEATester()
+ dummy.makeConnection(protocol.FileWrapper(StringIOWithNoClose()))
+ for line in self.messages:
+ dummy.resultHarvester.performTest(dummy.lineReceived, line)
+ def munge(myTuple):
+ if type(myTuple) != type(()):
+ return
+ newTuple = []
+ for v in myTuple:
+ if type(v) == type(1.1):
+ v = float(int(v * 10000.0)) * 0.0001
+ newTuple.append(v)
+ return tuple(newTuple)
+ for (message, expectedResult, actualResult) in zip(self.messages, self.results, dummy.resultHarvester.results):
+ expectedResult = munge(expectedResult)
+ actualResult = munge(actualResult)
+ if isinstance(expectedResult, Exception):
+ if isinstance(actualResult, Exception):
+ self.assertEqual(expectedResult.__class__, actualResult.__class__, "\nInput:\n%s\nExpected:\n%s.%s\nResults:\n%s.%s\n" % (message, expectedResult.__class__.__module__, expectedResult.__class__.__name__, actualResult.__class__.__module__, actualResult.__class__.__name__))
+ else:
+ self.assertEqual(1, 0, "\nInput:\n%s\nExpected:\n%s.%s\nResults:\n%r\n" % (message, expectedResult.__class__.__module__, expectedResult.__class__.__name__, actualResult))
+ else:
+ self.assertEqual(expectedResult, actualResult, "\nInput:\n%s\nExpected: %r\nResults: %r\n" % (message, expectedResult, actualResult))
+
+testCases = [NMEAReceiverTestCase]
diff --git a/twisted/test/test_paths.py b/twisted/test/test_paths.py
new file mode 100644
index 0000000..f2761df
--- /dev/null
+++ b/twisted/test/test_paths.py
@@ -0,0 +1,1622 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases covering L{twisted.python.filepath} and L{twisted.python.zippath}.
+"""
+
+import os, time, pickle, errno, zipfile, stat
+
+from twisted.python.compat import set
+from twisted.python.win32 import WindowsError, ERROR_DIRECTORY
+from twisted.python import filepath
+from twisted.python.zippath import ZipArchive
+from twisted.python.runtime import platform
+
+from twisted.trial import unittest
+
+from zope.interface.verify import verifyObject
+
+
+class AbstractFilePathTestCase(unittest.TestCase):
+
+ f1content = "file 1"
+ f2content = "file 2"
+
+
+ def _mkpath(self, *p):
+ x = os.path.abspath(os.path.join(self.cmn, *p))
+ self.all.append(x)
+ return x
+
+
+ def subdir(self, *dirname):
+ os.mkdir(self._mkpath(*dirname))
+
+
+ def subfile(self, *dirname):
+ return open(self._mkpath(*dirname), "wb")
+
+
+ def setUp(self):
+ self.now = time.time()
+ cmn = self.cmn = os.path.abspath(self.mktemp())
+ self.all = [cmn]
+ os.mkdir(cmn)
+ self.subdir("sub1")
+ f = self.subfile("file1")
+ f.write(self.f1content)
+ f.close()
+ f = self.subfile("sub1", "file2")
+ f.write(self.f2content)
+ f.close()
+ self.subdir('sub3')
+ f = self.subfile("sub3", "file3.ext1")
+ f.close()
+ f = self.subfile("sub3", "file3.ext2")
+ f.close()
+ f = self.subfile("sub3", "file3.ext3")
+ f.close()
+ self.path = filepath.FilePath(cmn)
+ self.root = filepath.FilePath("/")
+
+
+ def test_segmentsFromPositive(self):
+ """
+ Verify that the segments between two paths are correctly identified.
+ """
+ self.assertEqual(
+ self.path.child("a").child("b").child("c").segmentsFrom(self.path),
+ ["a", "b", "c"])
+
+ def test_segmentsFromNegative(self):
+ """
+ Verify that segmentsFrom notices when the ancestor isn't an ancestor.
+ """
+ self.assertRaises(
+ ValueError,
+ self.path.child("a").child("b").child("c").segmentsFrom,
+ self.path.child("d").child("c").child("e"))
+
+
+ def test_walk(self):
+ """
+ Verify that walking the path gives the same result as the known file
+ hierarchy.
+ """
+ x = [foo.path for foo in self.path.walk()]
+ self.assertEqual(set(x), set(self.all))
+
+
+ def test_parents(self):
+ """
+ L{FilePath.parents()} should return an iterator of every ancestor of
+ the L{FilePath} in question.
+ """
+ L = []
+ pathobj = self.path.child("a").child("b").child("c")
+ fullpath = pathobj.path
+ lastpath = fullpath
+ thispath = os.path.dirname(fullpath)
+ while lastpath != self.root.path:
+ L.append(thispath)
+ lastpath = thispath
+ thispath = os.path.dirname(thispath)
+ self.assertEqual([x.path for x in pathobj.parents()], L)
+
+
+ def test_validSubdir(self):
+ """
+ Verify that a valid subdirectory will show up as a directory, but not as a
+ file, not as a symlink, and be listable.
+ """
+ sub1 = self.path.child('sub1')
+ self.failUnless(sub1.exists(),
+ "This directory does exist.")
+ self.failUnless(sub1.isdir(),
+ "It's a directory.")
+ self.failUnless(not sub1.isfile(),
+ "It's a directory.")
+ self.failUnless(not sub1.islink(),
+ "It's a directory.")
+ self.assertEqual(sub1.listdir(),
+ ['file2'])
+
+
+ def test_invalidSubdir(self):
+ """
+ Verify that a subdirectory that doesn't exist is reported as such.
+ """
+ sub2 = self.path.child('sub2')
+ self.failIf(sub2.exists(),
+ "This directory does not exist.")
+
+ def test_validFiles(self):
+ """
+ Make sure that we can read existent non-empty files.
+ """
+ f1 = self.path.child('file1')
+ self.assertEqual(f1.open().read(), self.f1content)
+ f2 = self.path.child('sub1').child('file2')
+ self.assertEqual(f2.open().read(), self.f2content)
+
+
+ def test_multipleChildSegments(self):
+ """
+ C{fp.descendant([a, b, c])} returns the same L{FilePath} as is returned
+ by C{fp.child(a).child(b).child(c)}.
+ """
+ multiple = self.path.descendant(['a', 'b', 'c'])
+ single = self.path.child('a').child('b').child('c')
+ self.assertEqual(multiple, single)
+
+
+ def test_dictionaryKeys(self):
+ """
+ Verify that path instances are usable as dictionary keys.
+ """
+ f1 = self.path.child('file1')
+ f1prime = self.path.child('file1')
+ f2 = self.path.child('file2')
+ dictoid = {}
+ dictoid[f1] = 3
+ dictoid[f1prime] = 4
+ self.assertEqual(dictoid[f1], 4)
+ self.assertEqual(dictoid.keys(), [f1])
+ self.assertIdentical(dictoid.keys()[0], f1)
+ self.assertNotIdentical(dictoid.keys()[0], f1prime) # sanity check
+ dictoid[f2] = 5
+ self.assertEqual(dictoid[f2], 5)
+ self.assertEqual(len(dictoid), 2)
+
+
+ def test_dictionaryKeyWithString(self):
+ """
+ Verify that path instances are usable as dictionary keys which do not clash
+ with their string counterparts.
+ """
+ f1 = self.path.child('file1')
+ dictoid = {f1: 'hello'}
+ dictoid[f1.path] = 'goodbye'
+ self.assertEqual(len(dictoid), 2)
+
+
+ def test_childrenNonexistentError(self):
+ """
+ Verify that children raises the appropriate exception for non-existent
+ directories.
+ """
+ self.assertRaises(filepath.UnlistableError,
+ self.path.child('not real').children)
+
+ def test_childrenNotDirectoryError(self):
+ """
+ Verify that listdir raises the appropriate exception for attempting to list
+ a file rather than a directory.
+ """
+ self.assertRaises(filepath.UnlistableError,
+ self.path.child('file1').children)
+
+
+ def test_newTimesAreFloats(self):
+ """
+ Verify that all times returned from the various new time functions are ints
+ (and hopefully therefore 'high precision').
+ """
+ for p in self.path, self.path.child('file1'):
+ self.assertEqual(type(p.getAccessTime()), float)
+ self.assertEqual(type(p.getModificationTime()), float)
+ self.assertEqual(type(p.getStatusChangeTime()), float)
+
+
+ def test_oldTimesAreInts(self):
+ """
+ Verify that all times returned from the various time functions are
+ integers, for compatibility.
+ """
+ for p in self.path, self.path.child('file1'):
+ self.assertEqual(type(p.getatime()), int)
+ self.assertEqual(type(p.getmtime()), int)
+ self.assertEqual(type(p.getctime()), int)
+
+
+
+class FakeWindowsPath(filepath.FilePath):
+ """
+ A test version of FilePath which overrides listdir to raise L{WindowsError}.
+ """
+
+ def listdir(self):
+ """
+ @raise WindowsError: always.
+ """
+ raise WindowsError(
+ ERROR_DIRECTORY,
+ "A directory's validness was called into question")
+
+
+class ListingCompatibilityTests(unittest.TestCase):
+ """
+ These tests verify compatibility with legacy behavior of directory listing.
+ """
+
+ def test_windowsErrorExcept(self):
+ """
+ Verify that when a WindowsError is raised from listdir, catching
+ WindowsError works.
+ """
+ fwp = FakeWindowsPath(self.mktemp())
+ self.assertRaises(filepath.UnlistableError, fwp.children)
+ self.assertRaises(WindowsError, fwp.children)
+
+
+ def test_alwaysCatchOSError(self):
+ """
+ Verify that in the normal case where a directory does not exist, we will
+ get an OSError.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ self.assertRaises(OSError, fp.children)
+
+
+ def test_keepOriginalAttributes(self):
+ """
+ Verify that the Unlistable exception raised will preserve the attributes of
+ the previously-raised exception.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ ose = self.assertRaises(OSError, fp.children)
+ d1 = ose.__dict__.keys()
+ d1.remove('originalException')
+ d2 = ose.originalException.__dict__.keys()
+ d1.sort()
+ d2.sort()
+ self.assertEqual(d1, d2)
+
+
+
+def zipit(dirname, zfname):
+ """
+ Create a zipfile on zfname, containing the contents of dirname'
+ """
+ zf = zipfile.ZipFile(zfname, "w")
+ for root, ignored, files, in os.walk(dirname):
+ for fname in files:
+ fspath = os.path.join(root, fname)
+ arcpath = os.path.join(root, fname)[len(dirname)+1:]
+ # print fspath, '=>', arcpath
+ zf.write(fspath, arcpath)
+ zf.close()
+
+
+
+class ZipFilePathTestCase(AbstractFilePathTestCase):
+ """
+ Test various L{ZipPath} path manipulations as well as reprs for L{ZipPath}
+ and L{ZipArchive}.
+ """
+ def setUp(self):
+ AbstractFilePathTestCase.setUp(self)
+ zipit(self.cmn, self.cmn + '.zip')
+ self.path = ZipArchive(self.cmn + '.zip')
+ self.root = self.path
+ self.all = [x.replace(self.cmn, self.cmn + '.zip') for x in self.all]
+
+
+ def test_verifyObject(self):
+ """
+ ZipPaths implement IFilePath.
+ """
+
+ self.assertTrue(verifyObject(filepath.IFilePath, self.path))
+
+
+ def test_zipPathRepr(self):
+ """
+ Make sure that invoking ZipPath's repr prints the correct class name
+ and an absolute path to the zip file.
+ """
+ child = self.path.child("foo")
+ pathRepr = "ZipPath(%r)" % (
+ os.path.abspath(self.cmn + ".zip" + os.sep + 'foo'),)
+
+ # Check for an absolute path
+ self.assertEqual(repr(child), pathRepr)
+
+ # Create a path to the file rooted in the current working directory
+ relativeCommon = self.cmn.replace(os.getcwd() + os.sep, "", 1) + ".zip"
+ relpath = ZipArchive(relativeCommon)
+ child = relpath.child("foo")
+
+ # Check using a path without the cwd prepended
+ self.assertEqual(repr(child), pathRepr)
+
+
+ def test_zipPathReprParentDirSegment(self):
+ """
+ The repr of a ZipPath with C{".."} in the internal part of its path
+ includes the C{".."} rather than applying the usual parent directory
+ meaning.
+ """
+ child = self.path.child("foo").child("..").child("bar")
+ pathRepr = "ZipPath(%r)" % (
+ self.cmn + ".zip" + os.sep.join(["", "foo", "..", "bar"]))
+ self.assertEqual(repr(child), pathRepr)
+
+
+ def test_zipPathReprEscaping(self):
+ """
+ Bytes in the ZipPath path which have special meaning in Python
+ string literals are escaped in the ZipPath repr.
+ """
+ child = self.path.child("'")
+ path = self.cmn + ".zip" + os.sep.join(["", "'"])
+ pathRepr = "ZipPath('%s')" % (path.encode('string-escape'),)
+ self.assertEqual(repr(child), pathRepr)
+
+
+ def test_zipArchiveRepr(self):
+ """
+ Make sure that invoking ZipArchive's repr prints the correct class
+ name and an absolute path to the zip file.
+ """
+ pathRepr = 'ZipArchive(%r)' % (os.path.abspath(self.cmn + '.zip'),)
+
+ # Check for an absolute path
+ self.assertEqual(repr(self.path), pathRepr)
+
+ # Create a path to the file rooted in the current working directory
+ relativeCommon = self.cmn.replace(os.getcwd() + os.sep, "", 1) + ".zip"
+ relpath = ZipArchive(relativeCommon)
+
+ # Check using a path without the cwd prepended
+ self.assertEqual(repr(relpath), pathRepr)
+
+
+
+class ExplodingFile:
+ """
+ A C{file}-alike which raises exceptions from its I/O methods and keeps track
+ of whether it has been closed.
+
+ @ivar closed: A C{bool} which is C{False} until C{close} is called, then it
+ is C{True}.
+ """
+ closed = False
+
+ def read(self, n=0):
+ """
+ @raise IOError: Always raised.
+ """
+ raise IOError()
+
+
+ def write(self, what):
+ """
+ @raise IOError: Always raised.
+ """
+ raise IOError()
+
+
+ def close(self):
+ """
+ Mark the file as having been closed.
+ """
+ self.closed = True
+
+
+
+class TrackingFilePath(filepath.FilePath):
+ """
+ A subclass of L{filepath.FilePath} which maintains a list of all other paths
+ created by clonePath.
+
+ @ivar trackingList: A list of all paths created by this path via
+ C{clonePath} (which also includes paths created by methods like
+ C{parent}, C{sibling}, C{child}, etc (and all paths subsequently created
+ by those paths, etc).
+
+ @type trackingList: C{list} of L{TrackingFilePath}
+
+ @ivar openedFiles: A list of all file objects opened by this
+ L{TrackingFilePath} or any other L{TrackingFilePath} in C{trackingList}.
+
+ @type openedFiles: C{list} of C{file}
+ """
+
+ def __init__(self, path, alwaysCreate=False, trackingList=None):
+ filepath.FilePath.__init__(self, path, alwaysCreate)
+ if trackingList is None:
+ trackingList = []
+ self.trackingList = trackingList
+ self.openedFiles = []
+
+
+ def open(self, *a, **k):
+ """
+ Override 'open' to track all files opened by this path.
+ """
+ f = filepath.FilePath.open(self, *a, **k)
+ self.openedFiles.append(f)
+ return f
+
+
+ def openedPaths(self):
+ """
+ Return a list of all L{TrackingFilePath}s associated with this
+ L{TrackingFilePath} that have had their C{open()} method called.
+ """
+ return [path for path in self.trackingList if path.openedFiles]
+
+
+ def clonePath(self, name):
+ """
+ Override L{filepath.FilePath.clonePath} to give the new path a reference
+ to the same tracking list.
+ """
+ clone = TrackingFilePath(name, trackingList=self.trackingList)
+ self.trackingList.append(clone)
+ return clone
+
+
+
+class ExplodingFilePath(filepath.FilePath):
+ """
+ A specialized L{FilePath} which always returns an instance of
+ L{ExplodingFile} from its C{open} method.
+
+ @ivar fp: The L{ExplodingFile} instance most recently returned from the
+ C{open} method.
+ """
+
+ def __init__(self, pathName, originalExploder=None):
+ """
+ Initialize an L{ExplodingFilePath} with a name and a reference to the
+
+ @param pathName: The path name as passed to L{filepath.FilePath}.
+ @type pathName: C{str}
+
+ @param originalExploder: The L{ExplodingFilePath} to associate opened
+ files with.
+ @type originalExploder: L{ExplodingFilePath}
+ """
+ filepath.FilePath.__init__(self, pathName)
+ if originalExploder is None:
+ originalExploder = self
+ self._originalExploder = originalExploder
+
+
+ def open(self, mode=None):
+ """
+ Create, save, and return a new C{ExplodingFile}.
+
+ @param mode: Present for signature compatibility. Ignored.
+
+ @return: A new C{ExplodingFile}.
+ """
+ f = self._originalExploder.fp = ExplodingFile()
+ return f
+
+
+ def clonePath(self, name):
+ return ExplodingFilePath(name, self._originalExploder)
+
+
+
+class PermissionsTestCase(unittest.TestCase):
+ """
+ Test Permissions and RWX classes
+ """
+
+ def assertNotUnequal(self, first, second, msg=None):
+ """
+ Tests that C{first} != C{second} is false. This method tests the
+ __ne__ method, as opposed to L{assertEqual} (C{first} == C{second}),
+ which tests the __eq__ method.
+
+ Note: this should really be part of trial
+ """
+ if first != second:
+ if msg is None:
+ msg = '';
+ if len(msg) > 0:
+ msg += '\n'
+ raise self.failureException(
+ '%snot not unequal (__ne__ not implemented correctly):'
+ '\na = %s\nb = %s\n'
+ % (msg, pformat(first), pformat(second)))
+ return first
+
+
+ def test_rwxFromBools(self):
+ """
+ L{RWX}'s constructor takes a set of booleans
+ """
+ for r in (True, False):
+ for w in (True, False):
+ for x in (True, False):
+ rwx = filepath.RWX(r, w, x)
+ self.assertEqual(rwx.read, r)
+ self.assertEqual(rwx.write, w)
+ self.assertEqual(rwx.execute, x)
+ rwx = filepath.RWX(True, True, True)
+ self.assertTrue(rwx.read and rwx.write and rwx.execute)
+
+
+ def test_rwxEqNe(self):
+ """
+ L{RWX}'s created with the same booleans are equivalent. If booleans
+ are different, they are not equal.
+ """
+ for r in (True, False):
+ for w in (True, False):
+ for x in (True, False):
+ self.assertEqual(filepath.RWX(r, w, x),
+ filepath.RWX(r, w, x))
+ self.assertNotUnequal(filepath.RWX(r, w, x),
+ filepath.RWX(r, w, x))
+ self.assertNotEqual(filepath.RWX(True, True, True),
+ filepath.RWX(True, True, False))
+ self.assertNotEqual(3, filepath.RWX(True, True, True))
+
+
+ def test_rwxShorthand(self):
+ """
+ L{RWX}'s shorthand string should be 'rwx' if read, write, and execute
+ permission bits are true. If any of those permissions bits are false,
+ the character is replaced by a '-'.
+ """
+
+ def getChar(val, letter):
+ if val:
+ return letter
+ return '-'
+
+ for r in (True, False):
+ for w in (True, False):
+ for x in (True, False):
+ rwx = filepath.RWX(r, w, x)
+ self.assertEqual(rwx.shorthand(),
+ getChar(r, 'r') +
+ getChar(w, 'w') +
+ getChar(x, 'x'))
+ self.assertEqual(filepath.RWX(True, False, True).shorthand(), "r-x")
+
+
+ def test_permissionsFromStat(self):
+ """
+ L{Permissions}'s constructor takes a valid permissions bitmask and
+ parsaes it to produce the correct set of boolean permissions.
+ """
+ def _rwxFromStat(statModeInt, who):
+ def getPermissionBit(what, who):
+ return (statModeInt &
+ getattr(stat, "S_I%s%s" % (what, who))) > 0
+ return filepath.RWX(*[getPermissionBit(what, who) for what in
+ ('R', 'W', 'X')])
+
+ for u in range(0, 8):
+ for g in range(0, 8):
+ for o in range(0, 8):
+ chmodString = "%d%d%d" % (u, g, o)
+ chmodVal = int(chmodString, 8)
+ perm = filepath.Permissions(chmodVal)
+ self.assertEqual(perm.user,
+ _rwxFromStat(chmodVal, "USR"),
+ "%s: got user: %s" %
+ (chmodString, perm.user))
+ self.assertEqual(perm.group,
+ _rwxFromStat(chmodVal, "GRP"),
+ "%s: got group: %s" %
+ (chmodString, perm.group))
+ self.assertEqual(perm.other,
+ _rwxFromStat(chmodVal, "OTH"),
+ "%s: got other: %s" %
+ (chmodString, perm.other))
+ perm = filepath.Permissions(0777)
+ for who in ("user", "group", "other"):
+ for what in ("read", "write", "execute"):
+ self.assertTrue(getattr(getattr(perm, who), what))
+
+
+ def test_permissionsEq(self):
+ """
+ Two L{Permissions}'s that are created with the same bitmask
+ are equivalent
+ """
+ self.assertEqual(filepath.Permissions(0777),
+ filepath.Permissions(0777))
+ self.assertNotUnequal(filepath.Permissions(0777),
+ filepath.Permissions(0777))
+ self.assertNotEqual(filepath.Permissions(0777),
+ filepath.Permissions(0700))
+ self.assertNotEqual(3, filepath.Permissions(0777))
+
+
+ def test_permissionsShorthand(self):
+ """
+ L{Permissions}'s shorthand string is the RWX shorthand string for its
+ user permission bits, group permission bits, and other permission bits
+ concatenated together, without a space.
+ """
+ for u in range(0, 8):
+ for g in range(0, 8):
+ for o in range(0, 8):
+ perm = filepath.Permissions(eval("0%d%d%d" % (u, g, o)))
+ self.assertEqual(perm.shorthand(),
+ ''.join(x.shorthand() for x in (
+ perm.user, perm.group, perm.other)))
+ self.assertEqual(filepath.Permissions(0770).shorthand(), "rwxrwx---")
+
+
+
+class FilePathTestCase(AbstractFilePathTestCase):
+ """
+ Test various L{FilePath} path manipulations.
+ """
+
+
+ def test_verifyObject(self):
+ """
+ FilePaths implement IFilePath.
+ """
+
+ self.assertTrue(verifyObject(filepath.IFilePath, self.path))
+
+
+ def test_chmod(self):
+ """
+ L{FilePath.chmod} modifies the permissions of
+ the passed file as expected (using C{os.stat} to check). We use some
+ basic modes that should work everywhere (even on Windows).
+ """
+ for mode in (0555, 0777):
+ self.path.child("sub1").chmod(mode)
+ self.assertEqual(
+ stat.S_IMODE(os.stat(self.path.child("sub1").path).st_mode),
+ mode)
+
+
+ def symlink(self, target, name):
+ """
+ Create a symbolic link named C{name} pointing at C{target}.
+
+ @type target: C{str}
+ @type name: C{str}
+ @raise SkipTest: raised if symbolic links are not supported on the
+ host platform.
+ """
+ if getattr(os, 'symlink', None) is None:
+ raise unittest.SkipTest(
+ "Platform does not support symbolic links.")
+ os.symlink(target, name)
+
+
+ def createLinks(self):
+ """
+ Create several symbolic links to files and directories.
+ """
+ subdir = self.path.child("sub1")
+ self.symlink(subdir.path, self._mkpath("sub1.link"))
+ self.symlink(subdir.child("file2").path, self._mkpath("file2.link"))
+ self.symlink(subdir.child("file2").path,
+ self._mkpath("sub1", "sub1.file2.link"))
+
+
+ def test_realpathSymlink(self):
+ """
+ L{FilePath.realpath} returns the path of the ultimate target of a
+ symlink.
+ """
+ self.createLinks()
+ self.symlink(self.path.child("file2.link").path,
+ self.path.child("link.link").path)
+ self.assertEqual(self.path.child("link.link").realpath(),
+ self.path.child("sub1").child("file2"))
+
+
+ def test_realpathCyclicalSymlink(self):
+ """
+ L{FilePath.realpath} raises L{filepath.LinkError} if the path is a
+ symbolic link which is part of a cycle.
+ """
+ self.symlink(self.path.child("link1").path, self.path.child("link2").path)
+ self.symlink(self.path.child("link2").path, self.path.child("link1").path)
+ self.assertRaises(filepath.LinkError,
+ self.path.child("link2").realpath)
+
+
+ def test_realpathNoSymlink(self):
+ """
+ L{FilePath.realpath} returns the path itself if the path is not a
+ symbolic link.
+ """
+ self.assertEqual(self.path.child("sub1").realpath(),
+ self.path.child("sub1"))
+
+
+ def test_walkCyclicalSymlink(self):
+ """
+ Verify that walking a path with a cyclical symlink raises an error
+ """
+ self.createLinks()
+ self.symlink(self.path.child("sub1").path,
+ self.path.child("sub1").child("sub1.loopylink").path)
+ def iterateOverPath():
+ return [foo.path for foo in self.path.walk()]
+ self.assertRaises(filepath.LinkError, iterateOverPath)
+
+
+ def test_walkObeysDescendWithCyclicalSymlinks(self):
+ """
+ Verify that, after making a path with cyclical symlinks, when the
+ supplied C{descend} predicate returns C{False}, the target is not
+ traversed, as if it was a simple symlink.
+ """
+ self.createLinks()
+ # we create cyclical symlinks
+ self.symlink(self.path.child("sub1").path,
+ self.path.child("sub1").child("sub1.loopylink").path)
+ def noSymLinks(path):
+ return not path.islink()
+ def iterateOverPath():
+ return [foo.path for foo in self.path.walk(descend=noSymLinks)]
+ self.assertTrue(iterateOverPath())
+
+
+ def test_walkObeysDescend(self):
+ """
+ Verify that when the supplied C{descend} predicate returns C{False},
+ the target is not traversed.
+ """
+ self.createLinks()
+ def noSymLinks(path):
+ return not path.islink()
+ x = [foo.path for foo in self.path.walk(descend=noSymLinks)]
+ self.assertEqual(set(x), set(self.all))
+
+
+ def test_getAndSet(self):
+ content = 'newcontent'
+ self.path.child('new').setContent(content)
+ newcontent = self.path.child('new').getContent()
+ self.assertEqual(content, newcontent)
+ content = 'content'
+ self.path.child('new').setContent(content, '.tmp')
+ newcontent = self.path.child('new').getContent()
+ self.assertEqual(content, newcontent)
+
+
+ def test_getContentFileClosing(self):
+ """
+ If reading from the underlying file raises an exception,
+ L{FilePath.getContent} raises that exception after closing the file.
+ """
+ fp = ExplodingFilePath("")
+ self.assertRaises(IOError, fp.getContent)
+ self.assertTrue(fp.fp.closed)
+
+
+ def test_setContentFileClosing(self):
+ """
+ If writing to the underlying file raises an exception,
+ L{FilePath.setContent} raises that exception after closing the file.
+ """
+ fp = ExplodingFilePath("")
+ self.assertRaises(IOError, fp.setContent, "blah")
+ self.assertTrue(fp.fp.closed)
+
+
+ def test_setContentNameCollision(self):
+ """
+ L{FilePath.setContent} will use a different temporary filename on each
+ invocation, so that multiple processes, threads, or reentrant
+ invocations will not collide with each other.
+ """
+ fp = TrackingFilePath(self.mktemp())
+ fp.setContent("alpha")
+ fp.setContent("beta")
+
+ # Sanity check: setContent should only open one derivative path each
+ # time to store the temporary file.
+ openedSiblings = fp.openedPaths()
+ self.assertEqual(len(openedSiblings), 2)
+ self.assertNotEquals(openedSiblings[0], openedSiblings[1])
+
+
+ def test_setContentExtension(self):
+ """
+ L{FilePath.setContent} creates temporary files with a user-supplied
+ extension, so that if it is somehow interrupted while writing them, the
+ file that it leaves behind will be identifiable.
+ """
+ fp = TrackingFilePath(self.mktemp())
+ fp.setContent("hello")
+ opened = fp.openedPaths()
+ self.assertEqual(len(opened), 1)
+ self.assertTrue(opened[0].basename().endswith(".new"),
+ "%s does not end with default '.new' extension" % (
+ opened[0].basename()))
+ fp.setContent("goodbye", "-something-else")
+ opened = fp.openedPaths()
+ self.assertEqual(len(opened), 2)
+ self.assertTrue(opened[1].basename().endswith("-something-else"),
+ "%s does not end with -something-else extension" % (
+ opened[1].basename()))
+
+
+ def test_symbolicLink(self):
+ """
+ Verify the behavior of the C{isLink} method against links and
+ non-links. Also check that the symbolic link shares the directory
+ property with its target.
+ """
+ s4 = self.path.child("sub4")
+ s3 = self.path.child("sub3")
+ self.symlink(s3.path, s4.path)
+ self.assertTrue(s4.islink())
+ self.assertFalse(s3.islink())
+ self.assertTrue(s4.isdir())
+ self.assertTrue(s3.isdir())
+
+
+ def test_linkTo(self):
+ """
+ Verify that symlink creates a valid symlink that is both a link and a
+ file if its target is a file, or a directory if its target is a
+ directory.
+ """
+ targetLinks = [
+ (self.path.child("sub2"), self.path.child("sub2.link")),
+ (self.path.child("sub2").child("file3.ext1"),
+ self.path.child("file3.ext1.link"))
+ ]
+ for target, link in targetLinks:
+ target.linkTo(link)
+ self.assertTrue(link.islink(), "This is a link")
+ self.assertEqual(target.isdir(), link.isdir())
+ self.assertEqual(target.isfile(), link.isfile())
+
+
+ def test_linkToErrors(self):
+ """
+ Verify C{linkTo} fails in the following case:
+ - the target is in a directory that doesn't exist
+ - the target already exists
+ """
+ self.assertRaises(OSError, self.path.child("file1").linkTo,
+ self.path.child('nosub').child('file1'))
+ self.assertRaises(OSError, self.path.child("file1").linkTo,
+ self.path.child('sub1').child('file2'))
+
+
+ if not getattr(os, "symlink", None):
+ skipMsg = "Your platform does not support symbolic links."
+ test_symbolicLink.skip = skipMsg
+ test_linkTo.skip = skipMsg
+ test_linkToErrors.skip = skipMsg
+
+
+ def testMultiExt(self):
+ f3 = self.path.child('sub3').child('file3')
+ exts = '.foo','.bar', 'ext1','ext2','ext3'
+ self.failIf(f3.siblingExtensionSearch(*exts))
+ f3e = f3.siblingExtension(".foo")
+ f3e.touch()
+ self.failIf(not f3.siblingExtensionSearch(*exts).exists())
+ self.failIf(not f3.siblingExtensionSearch('*').exists())
+ f3e.remove()
+ self.failIf(f3.siblingExtensionSearch(*exts))
+
+ def testPreauthChild(self):
+ fp = filepath.FilePath('.')
+ fp.preauthChild('foo/bar')
+ self.assertRaises(filepath.InsecurePath, fp.child, '/foo')
+
+ def testStatCache(self):
+ p = self.path.child('stattest')
+ p.touch()
+ self.assertEqual(p.getsize(), 0)
+ self.assertEqual(abs(p.getmtime() - time.time()) // 20, 0)
+ self.assertEqual(abs(p.getctime() - time.time()) // 20, 0)
+ self.assertEqual(abs(p.getatime() - time.time()) // 20, 0)
+ self.assertEqual(p.exists(), True)
+ self.assertEqual(p.exists(), True)
+ # OOB removal: FilePath.remove() will automatically restat
+ os.remove(p.path)
+ # test caching
+ self.assertEqual(p.exists(), True)
+ p.restat(reraise=False)
+ self.assertEqual(p.exists(), False)
+ self.assertEqual(p.islink(), False)
+ self.assertEqual(p.isdir(), False)
+ self.assertEqual(p.isfile(), False)
+
+ def testPersist(self):
+ newpath = pickle.loads(pickle.dumps(self.path))
+ self.assertEqual(self.path.__class__, newpath.__class__)
+ self.assertEqual(self.path.path, newpath.path)
+
+ def testInsecureUNIX(self):
+ self.assertRaises(filepath.InsecurePath, self.path.child, "..")
+ self.assertRaises(filepath.InsecurePath, self.path.child, "/etc")
+ self.assertRaises(filepath.InsecurePath, self.path.child, "../..")
+
+ def testInsecureWin32(self):
+ self.assertRaises(filepath.InsecurePath, self.path.child, r"..\..")
+ self.assertRaises(filepath.InsecurePath, self.path.child, r"C:randomfile")
+
+ if platform.getType() != 'win32':
+ testInsecureWin32.skip = "Test will run only on Windows."
+
+ def testInsecureWin32Whacky(self):
+ """
+ Windows has 'special' filenames like NUL and CON and COM1 and LPR
+ and PRN and ... god knows what else. They can be located anywhere in
+ the filesystem. For obvious reasons, we do not wish to normally permit
+ access to these.
+ """
+ self.assertRaises(filepath.InsecurePath, self.path.child, "CON")
+ self.assertRaises(filepath.InsecurePath, self.path.child, "C:CON")
+ self.assertRaises(filepath.InsecurePath, self.path.child, r"C:\CON")
+
+ if platform.getType() != 'win32':
+ testInsecureWin32Whacky.skip = "Test will run only on Windows."
+
+ def testComparison(self):
+ self.assertEqual(filepath.FilePath('a'),
+ filepath.FilePath('a'))
+ self.failUnless(filepath.FilePath('z') >
+ filepath.FilePath('a'))
+ self.failUnless(filepath.FilePath('z') >=
+ filepath.FilePath('a'))
+ self.failUnless(filepath.FilePath('a') >=
+ filepath.FilePath('a'))
+ self.failUnless(filepath.FilePath('a') <=
+ filepath.FilePath('a'))
+ self.failUnless(filepath.FilePath('a') <
+ filepath.FilePath('z'))
+ self.failUnless(filepath.FilePath('a') <=
+ filepath.FilePath('z'))
+ self.failUnless(filepath.FilePath('a') !=
+ filepath.FilePath('z'))
+ self.failUnless(filepath.FilePath('z') !=
+ filepath.FilePath('a'))
+
+ self.failIf(filepath.FilePath('z') !=
+ filepath.FilePath('z'))
+
+
+ def test_descendantOnly(self):
+ """
+ If C{".."} is in the sequence passed to L{FilePath.descendant},
+ L{InsecurePath} is raised.
+ """
+ self.assertRaises(
+ filepath.InsecurePath, self.path.descendant, ['a', '..'])
+
+
+ def testSibling(self):
+ p = self.path.child('sibling_start')
+ ts = p.sibling('sibling_test')
+ self.assertEqual(ts.dirname(), p.dirname())
+ self.assertEqual(ts.basename(), 'sibling_test')
+ ts.createDirectory()
+ self.assertIn(ts, self.path.children())
+
+ def testTemporarySibling(self):
+ ts = self.path.temporarySibling()
+ self.assertEqual(ts.dirname(), self.path.dirname())
+ self.assertNotIn(ts.basename(), self.path.listdir())
+ ts.createDirectory()
+ self.assertIn(ts, self.path.parent().children())
+
+
+ def test_temporarySiblingExtension(self):
+ """
+ If L{FilePath.temporarySibling} is given an extension argument, it will
+ produce path objects with that extension appended to their names.
+ """
+ testExtension = ".test-extension"
+ ts = self.path.temporarySibling(testExtension)
+ self.assertTrue(ts.basename().endswith(testExtension),
+ "%s does not end with %s" % (
+ ts.basename(), testExtension))
+
+
+ def test_removeDirectory(self):
+ """
+ L{FilePath.remove} on a L{FilePath} that refers to a directory will
+ recursively delete its contents.
+ """
+ self.path.remove()
+ self.failIf(self.path.exists())
+
+
+ def test_removeWithSymlink(self):
+ """
+ For a path which is a symbolic link, L{FilePath.remove} just deletes
+ the link, not the target.
+ """
+ link = self.path.child("sub1.link")
+ # setUp creates the sub1 child
+ self.symlink(self.path.child("sub1").path, link.path)
+ link.remove()
+ self.assertFalse(link.exists())
+ self.assertTrue(self.path.child("sub1").exists())
+
+
+ def test_copyToDirectory(self):
+ """
+ L{FilePath.copyTo} makes a copy of all the contents of the directory
+ named by that L{FilePath} if it is able to do so.
+ """
+ oldPaths = list(self.path.walk()) # Record initial state
+ fp = filepath.FilePath(self.mktemp())
+ self.path.copyTo(fp)
+ self.path.remove()
+ fp.copyTo(self.path)
+ newPaths = list(self.path.walk()) # Record double-copy state
+ newPaths.sort()
+ oldPaths.sort()
+ self.assertEqual(newPaths, oldPaths)
+
+
+ def test_copyToMissingDestFileClosing(self):
+ """
+ If an exception is raised while L{FilePath.copyTo} is trying to open
+ source file to read from, the destination file is closed and the
+ exception is raised to the caller of L{FilePath.copyTo}.
+ """
+ nosuch = self.path.child("nothere")
+ # Make it look like something to copy, even though it doesn't exist.
+ # This could happen if the file is deleted between the isfile check and
+ # the file actually being opened.
+ nosuch.isfile = lambda: True
+
+ # We won't get as far as writing to this file, but it's still useful for
+ # tracking whether we closed it.
+ destination = ExplodingFilePath(self.mktemp())
+
+ self.assertRaises(IOError, nosuch.copyTo, destination)
+ self.assertTrue(destination.fp.closed)
+
+
+ def test_copyToFileClosing(self):
+ """
+ If an exception is raised while L{FilePath.copyTo} is copying bytes
+ between two regular files, the source and destination files are closed
+ and the exception propagates to the caller of L{FilePath.copyTo}.
+ """
+ destination = ExplodingFilePath(self.mktemp())
+ source = ExplodingFilePath(__file__)
+ self.assertRaises(IOError, source.copyTo, destination)
+ self.assertTrue(source.fp.closed)
+ self.assertTrue(destination.fp.closed)
+
+
+ def test_copyToDirectoryItself(self):
+ """
+ L{FilePath.copyTo} fails with an OSError or IOError (depending on
+ platform, as it propagates errors from open() and write()) when
+ attempting to copy a directory to a child of itself.
+ """
+ self.assertRaises((OSError, IOError),
+ self.path.copyTo, self.path.child('file1'))
+
+
+ def test_copyToWithSymlink(self):
+ """
+ Verify that copying with followLinks=True copies symlink targets
+ instead of symlinks
+ """
+ self.symlink(self.path.child("sub1").path,
+ self.path.child("link1").path)
+ fp = filepath.FilePath(self.mktemp())
+ self.path.copyTo(fp)
+ self.assertFalse(fp.child("link1").islink())
+ self.assertEqual([x.basename() for x in fp.child("sub1").children()],
+ [x.basename() for x in fp.child("link1").children()])
+
+
+ def test_copyToWithoutSymlink(self):
+ """
+ Verify that copying with followLinks=False copies symlinks as symlinks
+ """
+ self.symlink("sub1", self.path.child("link1").path)
+ fp = filepath.FilePath(self.mktemp())
+ self.path.copyTo(fp, followLinks=False)
+ self.assertTrue(fp.child("link1").islink())
+ self.assertEqual(os.readlink(self.path.child("link1").path),
+ os.readlink(fp.child("link1").path))
+
+
+ def test_copyToMissingSource(self):
+ """
+ If the source path is missing, L{FilePath.copyTo} raises L{OSError}.
+ """
+ path = filepath.FilePath(self.mktemp())
+ exc = self.assertRaises(OSError, path.copyTo, 'some other path')
+ self.assertEqual(exc.errno, errno.ENOENT)
+
+
+ def test_moveTo(self):
+ """
+ Verify that moving an entire directory results into another directory
+ with the same content.
+ """
+ oldPaths = list(self.path.walk()) # Record initial state
+ fp = filepath.FilePath(self.mktemp())
+ self.path.moveTo(fp)
+ fp.moveTo(self.path)
+ newPaths = list(self.path.walk()) # Record double-move state
+ newPaths.sort()
+ oldPaths.sort()
+ self.assertEqual(newPaths, oldPaths)
+
+
+ def test_moveToExistsCache(self):
+ """
+ A L{FilePath} that has been moved aside with L{FilePath.moveTo} no
+ longer registers as existing. Its previously non-existent target
+ exists, though, as it was created by the call to C{moveTo}.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ fp2 = filepath.FilePath(self.mktemp())
+ fp.touch()
+
+ # Both a sanity check (make sure the file status looks right) and an
+ # enticement for stat-caching logic to kick in and remember that these
+ # exist / don't exist.
+ self.assertEqual(fp.exists(), True)
+ self.assertEqual(fp2.exists(), False)
+
+ fp.moveTo(fp2)
+ self.assertEqual(fp.exists(), False)
+ self.assertEqual(fp2.exists(), True)
+
+
+ def test_moveToExistsCacheCrossMount(self):
+ """
+ The assertion of test_moveToExistsCache should hold in the case of a
+ cross-mount move.
+ """
+ self.setUpFaultyRename()
+ self.test_moveToExistsCache()
+
+
+ def test_moveToSizeCache(self, hook=lambda : None):
+ """
+ L{FilePath.moveTo} clears its destination's status cache, such that
+ calls to L{FilePath.getsize} after the call to C{moveTo} will report the
+ new size, not the old one.
+
+ This is a separate test from C{test_moveToExistsCache} because it is
+ intended to cover the fact that the destination's cache is dropped;
+ test_moveToExistsCache doesn't cover this case because (currently) a
+ file that doesn't exist yet does not cache the fact of its non-
+ existence.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ fp2 = filepath.FilePath(self.mktemp())
+ fp.setContent("1234")
+ fp2.setContent("1234567890")
+ hook()
+
+ # Sanity check / kick off caching.
+ self.assertEqual(fp.getsize(), 4)
+ self.assertEqual(fp2.getsize(), 10)
+ # Actually attempting to replace a file on Windows would fail with
+ # ERROR_ALREADY_EXISTS, but we don't need to test that, just the cached
+ # metadata, so, delete the file ...
+ os.remove(fp2.path)
+ # ... but don't clear the status cache, as fp2.remove() would.
+ self.assertEqual(fp2.getsize(), 10)
+
+ fp.moveTo(fp2)
+ self.assertEqual(fp2.getsize(), 4)
+
+
+ def test_moveToSizeCacheCrossMount(self):
+ """
+ The assertion of test_moveToSizeCache should hold in the case of a
+ cross-mount move.
+ """
+ self.test_moveToSizeCache(hook=self.setUpFaultyRename)
+
+
+ def test_moveToError(self):
+ """
+ Verify error behavior of moveTo: it should raises one of OSError or
+ IOError if you want to move a path into one of its child. It's simply
+ the error raised by the underlying rename system call.
+ """
+ self.assertRaises((OSError, IOError), self.path.moveTo, self.path.child('file1'))
+
+
+ def setUpFaultyRename(self):
+ """
+ Set up a C{os.rename} that will fail with L{errno.EXDEV} on first call.
+ This is used to simulate a cross-device rename failure.
+
+ @return: a list of pair (src, dest) of calls to C{os.rename}
+ @rtype: C{list} of C{tuple}
+ """
+ invokedWith = []
+ def faultyRename(src, dest):
+ invokedWith.append((src, dest))
+ if len(invokedWith) == 1:
+ raise OSError(errno.EXDEV, 'Test-induced failure simulating '
+ 'cross-device rename failure')
+ return originalRename(src, dest)
+
+ originalRename = os.rename
+ self.patch(os, "rename", faultyRename)
+ return invokedWith
+
+
+ def test_crossMountMoveTo(self):
+ """
+ C{moveTo} should be able to handle C{EXDEV} error raised by
+ C{os.rename} when trying to move a file on a different mounted
+ filesystem.
+ """
+ invokedWith = self.setUpFaultyRename()
+ # Bit of a whitebox test - force os.rename, which moveTo tries
+ # before falling back to a slower method, to fail, forcing moveTo to
+ # use the slower behavior.
+ self.test_moveTo()
+ # A bit of a sanity check for this whitebox test - if our rename
+ # was never invoked, the test has probably fallen into disrepair!
+ self.assertTrue(invokedWith)
+
+
+ def test_crossMountMoveToWithSymlink(self):
+ """
+ By default, when moving a symlink, it should follow the link and
+ actually copy the content of the linked node.
+ """
+ invokedWith = self.setUpFaultyRename()
+ f2 = self.path.child('file2')
+ f3 = self.path.child('file3')
+ self.symlink(self.path.child('file1').path, f2.path)
+ f2.moveTo(f3)
+ self.assertFalse(f3.islink())
+ self.assertEqual(f3.getContent(), 'file 1')
+ self.assertTrue(invokedWith)
+
+
+ def test_crossMountMoveToWithoutSymlink(self):
+ """
+ Verify that moveTo called with followLinks=False actually create
+ another symlink.
+ """
+ invokedWith = self.setUpFaultyRename()
+ f2 = self.path.child('file2')
+ f3 = self.path.child('file3')
+ self.symlink(self.path.child('file1').path, f2.path)
+ f2.moveTo(f3, followLinks=False)
+ self.assertTrue(f3.islink())
+ self.assertEqual(f3.getContent(), 'file 1')
+ self.assertTrue(invokedWith)
+
+
+ def test_createBinaryMode(self):
+ """
+ L{FilePath.create} should always open (and write to) files in binary
+ mode; line-feed octets should be unmodified.
+
+ (While this test should pass on all platforms, it is only really
+ interesting on platforms which have the concept of binary mode, i.e.
+ Windows platforms.)
+ """
+ path = filepath.FilePath(self.mktemp())
+ f = path.create()
+ self.failUnless("b" in f.mode)
+ f.write("\n")
+ f.close()
+ read = open(path.path, "rb").read()
+ self.assertEqual(read, "\n")
+
+
+ def testOpen(self):
+ # Opening a file for reading when it does not already exist is an error
+ nonexistent = self.path.child('nonexistent')
+ e = self.assertRaises(IOError, nonexistent.open)
+ self.assertEqual(e.errno, errno.ENOENT)
+
+ # Opening a file for writing when it does not exist is okay
+ writer = self.path.child('writer')
+ f = writer.open('w')
+ f.write('abc\ndef')
+ f.close()
+
+ # Make sure those bytes ended up there - and test opening a file for
+ # reading when it does exist at the same time
+ f = writer.open()
+ self.assertEqual(f.read(), 'abc\ndef')
+ f.close()
+
+ # Re-opening that file in write mode should erase whatever was there.
+ f = writer.open('w')
+ f.close()
+ f = writer.open()
+ self.assertEqual(f.read(), '')
+ f.close()
+
+ # Put some bytes in a file so we can test that appending does not
+ # destroy them.
+ appender = self.path.child('appender')
+ f = appender.open('w')
+ f.write('abc')
+ f.close()
+
+ f = appender.open('a')
+ f.write('def')
+ f.close()
+
+ f = appender.open('r')
+ self.assertEqual(f.read(), 'abcdef')
+ f.close()
+
+ # read/write should let us do both without erasing those bytes
+ f = appender.open('r+')
+ self.assertEqual(f.read(), 'abcdef')
+ # ANSI C *requires* an fseek or an fgetpos between an fread and an
+ # fwrite or an fwrite and a fread. We can't reliable get Python to
+ # invoke fgetpos, so we seek to a 0 byte offset from the current
+ # position instead. Also, Python sucks for making this seek
+ # relative to 1 instead of a symbolic constant representing the
+ # current file position.
+ f.seek(0, 1)
+ # Put in some new bytes for us to test for later.
+ f.write('ghi')
+ f.close()
+
+ # Make sure those new bytes really showed up
+ f = appender.open('r')
+ self.assertEqual(f.read(), 'abcdefghi')
+ f.close()
+
+ # write/read should let us do both, but erase anything that's there
+ # already.
+ f = appender.open('w+')
+ self.assertEqual(f.read(), '')
+ f.seek(0, 1) # Don't forget this!
+ f.write('123')
+ f.close()
+
+ # super append mode should let us read and write and also position the
+ # cursor at the end of the file, without erasing everything.
+ f = appender.open('a+')
+
+ # The order of these lines may seem surprising, but it is necessary.
+ # The cursor is not at the end of the file until after the first write.
+ f.write('456')
+ f.seek(0, 1) # Asinine.
+ self.assertEqual(f.read(), '')
+
+ f.seek(0, 0)
+ self.assertEqual(f.read(), '123456')
+ f.close()
+
+ # Opening a file exclusively must fail if that file exists already.
+ nonexistent.requireCreate(True)
+ nonexistent.open('w').close()
+ existent = nonexistent
+ del nonexistent
+ self.assertRaises((OSError, IOError), existent.open)
+
+
+ def test_openWithExplicitBinaryMode(self):
+ """
+ Due to a bug in Python 2.7 on Windows including multiple 'b'
+ characters in the mode passed to the built-in open() will cause an
+ error. FilePath.open() ensures that only a single 'b' character is
+ included in the mode passed to the built-in open().
+
+ See http://bugs.python.org/issue7686 for details about the bug.
+ """
+ writer = self.path.child('explicit-binary')
+ file = writer.open('wb')
+ file.write('abc\ndef')
+ file.close()
+ self.assertTrue(writer.exists)
+
+
+ def test_openWithRedundantExplicitBinaryModes(self):
+ """
+ Due to a bug in Python 2.7 on Windows including multiple 'b'
+ characters in the mode passed to the built-in open() will cause an
+ error. No matter how many 'b' modes are specified, FilePath.open()
+ ensures that only a single 'b' character is included in the mode
+ passed to the built-in open().
+
+ See http://bugs.python.org/issue7686 for details about the bug.
+ """
+ writer = self.path.child('multiple-binary')
+ file = writer.open('wbb')
+ file.write('abc\ndef')
+ file.close()
+ self.assertTrue(writer.exists)
+
+
+ def test_existsCache(self):
+ """
+ Check that C{filepath.FilePath.exists} correctly restat the object if
+ an operation has occurred in the mean time.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ self.assertEqual(fp.exists(), False)
+
+ fp.makedirs()
+ self.assertEqual(fp.exists(), True)
+
+
+ def test_changed(self):
+ """
+ L{FilePath.changed} indicates that the L{FilePath} has changed, but does
+ not re-read the status information from the filesystem until it is
+ queried again via another method, such as C{getsize}.
+ """
+ fp = filepath.FilePath(self.mktemp())
+ fp.setContent("12345")
+ self.assertEqual(fp.getsize(), 5)
+
+ # Someone else comes along and changes the file.
+ fObj = open(fp.path, 'wb')
+ fObj.write("12345678")
+ fObj.close()
+
+ # Sanity check for caching: size should still be 5.
+ self.assertEqual(fp.getsize(), 5)
+ fp.changed()
+
+ # This path should look like we don't know what status it's in, not that
+ # we know that it didn't exist when last we checked.
+ self.assertEqual(fp.statinfo, None)
+ self.assertEqual(fp.getsize(), 8)
+
+
+ def test_getPermissions_POSIX(self):
+ """
+ Getting permissions for a file returns a L{Permissions} object for
+ POSIX platforms (which supports separate user, group, and other
+ permissions bits.
+ """
+ for mode in (0777, 0700):
+ self.path.child("sub1").chmod(mode)
+ self.assertEqual(self.path.child("sub1").getPermissions(),
+ filepath.Permissions(mode))
+ self.path.child("sub1").chmod(0764) #sanity check
+ self.assertEqual(self.path.child("sub1").getPermissions().shorthand(),
+ "rwxrw-r--")
+
+
+ def test_getPermissions_Windows(self):
+ """
+ Getting permissions for a file returns a L{Permissions} object in
+ Windows. Windows requires a different test, because user permissions
+ = group permissions = other permissions. Also, chmod may not be able
+ to set the execute bit, so we are skipping tests that set the execute
+ bit.
+ """
+ for mode in (0777, 0555):
+ self.path.child("sub1").chmod(mode)
+ self.assertEqual(self.path.child("sub1").getPermissions(),
+ filepath.Permissions(mode))
+ self.path.child("sub1").chmod(0511) #sanity check to make sure that
+ # user=group=other permissions
+ self.assertEqual(self.path.child("sub1").getPermissions().shorthand(),
+ "r-xr-xr-x")
+
+
+ def test_whetherBlockOrSocket(self):
+ """
+ Ensure that a file is not a block or socket
+ """
+ self.assertFalse(self.path.isBlockDevice())
+ self.assertFalse(self.path.isSocket())
+
+
+ def test_statinfoBitsNotImplementedInWindows(self):
+ """
+ Verify that certain file stats are not available on Windows
+ """
+ self.assertRaises(NotImplementedError, self.path.getInodeNumber)
+ self.assertRaises(NotImplementedError, self.path.getDevice)
+ self.assertRaises(NotImplementedError, self.path.getNumberOfHardLinks)
+ self.assertRaises(NotImplementedError, self.path.getUserID)
+ self.assertRaises(NotImplementedError, self.path.getGroupID)
+
+
+ def test_statinfoBitsAreNumbers(self):
+ """
+ Verify that file inode/device/nlinks/uid/gid stats are numbers in
+ a POSIX environment
+ """
+ c = self.path.child('file1')
+ for p in self.path, c:
+ self.assertIsInstance(p.getInodeNumber(), long)
+ self.assertIsInstance(p.getDevice(), long)
+ self.assertIsInstance(p.getNumberOfHardLinks(), int)
+ self.assertIsInstance(p.getUserID(), int)
+ self.assertIsInstance(p.getGroupID(), int)
+ self.assertEqual(self.path.getUserID(), c.getUserID())
+ self.assertEqual(self.path.getGroupID(), c.getGroupID())
+
+
+ def test_statinfoNumbersAreValid(self):
+ """
+ Verify that the right numbers come back from the right accessor methods
+ for file inode/device/nlinks/uid/gid (in a POSIX environment)
+ """
+ # specify fake statinfo information
+ class FakeStat:
+ st_ino = 200
+ st_dev = 300
+ st_nlink = 400
+ st_uid = 500
+ st_gid = 600
+
+ # monkey patch in a fake restat method for self.path
+ fake = FakeStat()
+ def fakeRestat(*args, **kwargs):
+ self.path.statinfo = fake
+ self.path.restat = fakeRestat
+
+ # ensure that restat will need to be called to get values
+ self.path.statinfo = None
+
+ self.assertEqual(self.path.getInodeNumber(), fake.st_ino)
+ self.assertEqual(self.path.getDevice(), fake.st_dev)
+ self.assertEqual(self.path.getNumberOfHardLinks(), fake.st_nlink)
+ self.assertEqual(self.path.getUserID(), fake.st_uid)
+ self.assertEqual(self.path.getGroupID(), fake.st_gid)
+
+
+ if platform.isWindows():
+ test_statinfoBitsAreNumbers.skip = True
+ test_statinfoNumbersAreValid.skip = True
+ test_getPermissions_POSIX.skip = True
+ else:
+ test_statinfoBitsNotImplementedInWindows.skip = "Test will run only on Windows."
+ test_getPermissions_Windows.skip = "Test will run only on Windows."
+
+
+
+from twisted.python import urlpath
+
+class URLPathTestCase(unittest.TestCase):
+ def setUp(self):
+ self.path = urlpath.URLPath.fromString("http://example.com/foo/bar?yes=no&no=yes#footer")
+
+ def testStringConversion(self):
+ self.assertEqual(str(self.path), "http://example.com/foo/bar?yes=no&no=yes#footer")
+
+ def testChildString(self):
+ self.assertEqual(str(self.path.child('hello')), "http://example.com/foo/bar/hello")
+ self.assertEqual(str(self.path.child('hello').child('')), "http://example.com/foo/bar/hello/")
+
+ def testSiblingString(self):
+ self.assertEqual(str(self.path.sibling('baz')), 'http://example.com/foo/baz')
+
+ # The sibling of http://example.com/foo/bar/
+ # is http://example.comf/foo/bar/baz
+ # because really we are constructing a sibling of
+ # http://example.com/foo/bar/index.html
+ self.assertEqual(str(self.path.child('').sibling('baz')), 'http://example.com/foo/bar/baz')
+
+ def testParentString(self):
+ # parent should be equivalent to '..'
+ # 'foo' is the current directory, '/' is the parent directory
+ self.assertEqual(str(self.path.parent()), 'http://example.com/')
+ self.assertEqual(str(self.path.child('').parent()), 'http://example.com/foo/')
+ self.assertEqual(str(self.path.child('baz').parent()), 'http://example.com/foo/')
+ self.assertEqual(str(self.path.parent().parent().parent().parent().parent()), 'http://example.com/')
+
+ def testHereString(self):
+ # here should be equivalent to '.'
+ self.assertEqual(str(self.path.here()), 'http://example.com/foo/')
+ self.assertEqual(str(self.path.child('').here()), 'http://example.com/foo/bar/')
+
diff --git a/twisted/test/test_pb.py b/twisted/test/test_pb.py
new file mode 100644
index 0000000..4616708
--- /dev/null
+++ b/twisted/test/test_pb.py
@@ -0,0 +1,1846 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for Perspective Broker module.
+
+TODO: update protocol level tests to use new connection API, leaving
+only specific tests for old API.
+"""
+
+# issue1195 TODOs: replace pump.pump() with something involving Deferreds.
+# Clean up warning suppression.
+
+import sys, os, time, gc, weakref
+
+from cStringIO import StringIO
+from zope.interface import implements, Interface
+
+from twisted.trial import unittest
+from twisted.spread import pb, util, publish, jelly
+from twisted.internet import protocol, main, reactor
+from twisted.internet.error import ConnectionRefusedError
+from twisted.internet.defer import Deferred, gatherResults, succeed
+from twisted.protocols.policies import WrappingFactory
+from twisted.python import failure, log
+from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
+from twisted.cred import portal, checkers, credentials
+
+
+class Dummy(pb.Viewable):
+ def view_doNothing(self, user):
+ if isinstance(user, DummyPerspective):
+ return 'hello world!'
+ else:
+ return 'goodbye, cruel world!'
+
+
+class DummyPerspective(pb.Avatar):
+ """
+ An L{IPerspective} avatar which will be used in some tests.
+ """
+ def perspective_getDummyViewPoint(self):
+ return Dummy()
+
+
+
+class DummyRealm(object):
+ implements(portal.IRealm)
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ for iface in interfaces:
+ if iface is pb.IPerspective:
+ return iface, DummyPerspective(avatarId), lambda: None
+
+
+class IOPump:
+ """
+ Utility to pump data between clients and servers for protocol testing.
+
+ Perhaps this is a utility worthy of being in protocol.py?
+ """
+ def __init__(self, client, server, clientIO, serverIO):
+ self.client = client
+ self.server = server
+ self.clientIO = clientIO
+ self.serverIO = serverIO
+
+
+ def flush(self):
+ """
+ Pump until there is no more input or output or until L{stop} is called.
+ This does not run any timers, so don't use it with any code that calls
+ reactor.callLater.
+ """
+ # failsafe timeout
+ self._stop = False
+ timeout = time.time() + 5
+ while not self._stop and self.pump():
+ if time.time() > timeout:
+ return
+
+
+ def stop(self):
+ """
+ Stop a running L{flush} operation, even if data remains to be
+ transferred.
+ """
+ self._stop = True
+
+
+ def pump(self):
+ """
+ Move data back and forth.
+
+ Returns whether any data was moved.
+ """
+ self.clientIO.seek(0)
+ self.serverIO.seek(0)
+ cData = self.clientIO.read()
+ sData = self.serverIO.read()
+ self.clientIO.seek(0)
+ self.serverIO.seek(0)
+ self.clientIO.truncate()
+ self.serverIO.truncate()
+ self.client.transport._checkProducer()
+ self.server.transport._checkProducer()
+ for byte in cData:
+ self.server.dataReceived(byte)
+ for byte in sData:
+ self.client.dataReceived(byte)
+ if cData or sData:
+ return 1
+ else:
+ return 0
+
+
+
+def connectedServerAndClient(realm=None):
+ """
+ Connect a client and server L{Broker} together with an L{IOPump}
+
+ @param realm: realm to use, defaulting to a L{DummyRealm}
+
+ @returns: a 3-tuple (client, server, pump).
+ """
+ realm = realm or DummyRealm()
+ clientBroker = pb.Broker()
+ checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(guest='guest')
+ factory = pb.PBServerFactory(portal.Portal(realm, [checker]))
+ serverBroker = factory.buildProtocol(('127.0.0.1',))
+
+ clientTransport = StringIO()
+ serverTransport = StringIO()
+ clientBroker.makeConnection(protocol.FileWrapper(clientTransport))
+ serverBroker.makeConnection(protocol.FileWrapper(serverTransport))
+ pump = IOPump(clientBroker, serverBroker, clientTransport, serverTransport)
+ # Challenge-response authentication:
+ pump.flush()
+ return clientBroker, serverBroker, pump
+
+
+class SimpleRemote(pb.Referenceable):
+ def remote_thunk(self, arg):
+ self.arg = arg
+ return arg + 1
+
+ def remote_knuth(self, arg):
+ raise Exception()
+
+
+class NestedRemote(pb.Referenceable):
+ def remote_getSimple(self):
+ return SimpleRemote()
+
+
+class SimpleCopy(pb.Copyable):
+ def __init__(self):
+ self.x = 1
+ self.y = {"Hello":"World"}
+ self.z = ['test']
+
+
+class SimpleLocalCopy(pb.RemoteCopy):
+ pass
+
+pb.setUnjellyableForClass(SimpleCopy, SimpleLocalCopy)
+
+
+class SimpleFactoryCopy(pb.Copyable):
+ """
+ @cvar allIDs: hold every created instances of this class.
+ @type allIDs: C{dict}
+ """
+ allIDs = {}
+ def __init__(self, id):
+ self.id = id
+ SimpleFactoryCopy.allIDs[id] = self
+
+
+def createFactoryCopy(state):
+ """
+ Factory of L{SimpleFactoryCopy}, getting a created instance given the
+ C{id} found in C{state}.
+ """
+ stateId = state.get("id", None)
+ if stateId is None:
+ raise RuntimeError("factory copy state has no 'id' member %s" %
+ (repr(state),))
+ if not stateId in SimpleFactoryCopy.allIDs:
+ raise RuntimeError("factory class has no ID: %s" %
+ (SimpleFactoryCopy.allIDs,))
+ inst = SimpleFactoryCopy.allIDs[stateId]
+ if not inst:
+ raise RuntimeError("factory method found no object with id")
+ return inst
+
+pb.setUnjellyableFactoryForClass(SimpleFactoryCopy, createFactoryCopy)
+
+
+class NestedCopy(pb.Referenceable):
+ def remote_getCopy(self):
+ return SimpleCopy()
+
+ def remote_getFactory(self, value):
+ return SimpleFactoryCopy(value)
+
+
+
+class SimpleCache(pb.Cacheable):
+ def __init___(self):
+ self.x = 1
+ self.y = {"Hello":"World"}
+ self.z = ['test']
+
+
+class NestedComplicatedCache(pb.Referenceable):
+ def __init__(self):
+ self.c = VeryVeryComplicatedCacheable()
+
+ def remote_getCache(self):
+ return self.c
+
+
+class VeryVeryComplicatedCacheable(pb.Cacheable):
+ def __init__(self):
+ self.x = 1
+ self.y = 2
+ self.foo = 3
+
+ def setFoo4(self):
+ self.foo = 4
+ self.observer.callRemote('foo',4)
+
+ def getStateToCacheAndObserveFor(self, perspective, observer):
+ self.observer = observer
+ return {"x": self.x,
+ "y": self.y,
+ "foo": self.foo}
+
+ def stoppedObserving(self, perspective, observer):
+ log.msg("stopped observing")
+ observer.callRemote("end")
+ if observer == self.observer:
+ self.observer = None
+
+
+class RatherBaroqueCache(pb.RemoteCache):
+ def observe_foo(self, newFoo):
+ self.foo = newFoo
+
+ def observe_end(self):
+ log.msg("the end of things")
+
+pb.setUnjellyableForClass(VeryVeryComplicatedCacheable, RatherBaroqueCache)
+
+
+class SimpleLocalCache(pb.RemoteCache):
+ def setCopyableState(self, state):
+ self.__dict__.update(state)
+
+ def checkMethod(self):
+ return self.check
+
+ def checkSelf(self):
+ return self
+
+ def check(self):
+ return 1
+
+pb.setUnjellyableForClass(SimpleCache, SimpleLocalCache)
+
+
+class NestedCache(pb.Referenceable):
+ def __init__(self):
+ self.x = SimpleCache()
+
+ def remote_getCache(self):
+ return [self.x,self.x]
+
+ def remote_putCache(self, cache):
+ return (self.x is cache)
+
+
+class Observable(pb.Referenceable):
+ def __init__(self):
+ self.observers = []
+
+ def remote_observe(self, obs):
+ self.observers.append(obs)
+
+ def remote_unobserve(self, obs):
+ self.observers.remove(obs)
+
+ def notify(self, obj):
+ for observer in self.observers:
+ observer.callRemote('notify', self, obj)
+
+
+class DeferredRemote(pb.Referenceable):
+ def __init__(self):
+ self.run = 0
+
+ def runMe(self, arg):
+ self.run = arg
+ return arg + 1
+
+ def dontRunMe(self, arg):
+ assert 0, "shouldn't have been run!"
+
+ def remote_doItLater(self):
+ """
+ Return a L{Deferred} to be fired on client side. When fired,
+ C{self.runMe} is called.
+ """
+ d = Deferred()
+ d.addCallbacks(self.runMe, self.dontRunMe)
+ self.d = d
+ return d
+
+
+class Observer(pb.Referenceable):
+ notified = 0
+ obj = None
+ def remote_notify(self, other, obj):
+ self.obj = obj
+ self.notified = self.notified + 1
+ other.callRemote('unobserve',self)
+
+
+class NewStyleCopy(pb.Copyable, pb.RemoteCopy, object):
+ def __init__(self, s):
+ self.s = s
+pb.setUnjellyableForClass(NewStyleCopy, NewStyleCopy)
+
+
+class NewStyleCopy2(pb.Copyable, pb.RemoteCopy, object):
+ allocated = 0
+ initialized = 0
+ value = 1
+
+ def __new__(self):
+ NewStyleCopy2.allocated += 1
+ inst = object.__new__(self)
+ inst.value = 2
+ return inst
+
+ def __init__(self):
+ NewStyleCopy2.initialized += 1
+
+pb.setUnjellyableForClass(NewStyleCopy2, NewStyleCopy2)
+
+
+class NewStyleCacheCopy(pb.Cacheable, pb.RemoteCache, object):
+ def getStateToCacheAndObserveFor(self, perspective, observer):
+ return self.__dict__
+
+pb.setUnjellyableForClass(NewStyleCacheCopy, NewStyleCacheCopy)
+
+
+class Echoer(pb.Root):
+ def remote_echo(self, st):
+ return st
+
+
+class CachedReturner(pb.Root):
+ def __init__(self, cache):
+ self.cache = cache
+ def remote_giveMeCache(self, st):
+ return self.cache
+
+
+class NewStyleTestCase(unittest.TestCase):
+ def setUp(self):
+ """
+ Create a pb server using L{Echoer} protocol and connect a client to it.
+ """
+ self.serverFactory = pb.PBServerFactory(Echoer())
+ self.wrapper = WrappingFactory(self.serverFactory)
+ self.server = reactor.listenTCP(0, self.wrapper)
+ clientFactory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", self.server.getHost().port,
+ clientFactory)
+ def gotRoot(ref):
+ self.ref = ref
+ return clientFactory.getRootObject().addCallback(gotRoot)
+
+
+ def tearDown(self):
+ """
+ Close client and server connections, reset values of L{NewStyleCopy2}
+ class variables.
+ """
+ NewStyleCopy2.allocated = 0
+ NewStyleCopy2.initialized = 0
+ NewStyleCopy2.value = 1
+ self.ref.broker.transport.loseConnection()
+ # Disconnect any server-side connections too.
+ for proto in self.wrapper.protocols:
+ proto.transport.loseConnection()
+ return self.server.stopListening()
+
+ def test_newStyle(self):
+ """
+ Create a new style object, send it over the wire, and check the result.
+ """
+ orig = NewStyleCopy("value")
+ d = self.ref.callRemote("echo", orig)
+ def cb(res):
+ self.failUnless(isinstance(res, NewStyleCopy))
+ self.assertEqual(res.s, "value")
+ self.failIf(res is orig) # no cheating :)
+ d.addCallback(cb)
+ return d
+
+ def test_alloc(self):
+ """
+ Send a new style object and check the number of allocations.
+ """
+ orig = NewStyleCopy2()
+ self.assertEqual(NewStyleCopy2.allocated, 1)
+ self.assertEqual(NewStyleCopy2.initialized, 1)
+ d = self.ref.callRemote("echo", orig)
+ def cb(res):
+ # receiving the response creates a third one on the way back
+ self.failUnless(isinstance(res, NewStyleCopy2))
+ self.assertEqual(res.value, 2)
+ self.assertEqual(NewStyleCopy2.allocated, 3)
+ self.assertEqual(NewStyleCopy2.initialized, 1)
+ self.failIf(res is orig) # no cheating :)
+ # sending the object creates a second one on the far side
+ d.addCallback(cb)
+ return d
+
+
+
+class ConnectionNotifyServerFactory(pb.PBServerFactory):
+ """
+ A server factory which stores the last connection and fires a
+ L{Deferred} on connection made. This factory can handle only one
+ client connection.
+
+ @ivar protocolInstance: the last protocol instance.
+ @type protocolInstance: C{pb.Broker}
+
+ @ivar connectionMade: the deferred fired upon connection.
+ @type connectionMade: C{Deferred}
+ """
+ protocolInstance = None
+
+ def __init__(self, root):
+ """
+ Initialize the factory.
+ """
+ pb.PBServerFactory.__init__(self, root)
+ self.connectionMade = Deferred()
+
+
+ def clientConnectionMade(self, protocol):
+ """
+ Store the protocol and fire the connection deferred.
+ """
+ self.protocolInstance = protocol
+ d, self.connectionMade = self.connectionMade, None
+ if d is not None:
+ d.callback(None)
+
+
+
+class NewStyleCachedTestCase(unittest.TestCase):
+ def setUp(self):
+ """
+ Create a pb server using L{CachedReturner} protocol and connect a
+ client to it.
+ """
+ self.orig = NewStyleCacheCopy()
+ self.orig.s = "value"
+ self.server = reactor.listenTCP(0,
+ ConnectionNotifyServerFactory(CachedReturner(self.orig)))
+ clientFactory = pb.PBClientFactory()
+ reactor.connectTCP("localhost", self.server.getHost().port,
+ clientFactory)
+ def gotRoot(ref):
+ self.ref = ref
+ d1 = clientFactory.getRootObject().addCallback(gotRoot)
+ d2 = self.server.factory.connectionMade
+ return gatherResults([d1, d2])
+
+
+ def tearDown(self):
+ """
+ Close client and server connections.
+ """
+ self.server.factory.protocolInstance.transport.loseConnection()
+ self.ref.broker.transport.loseConnection()
+ return self.server.stopListening()
+
+
+ def test_newStyleCache(self):
+ """
+ A new-style cacheable object can be retrieved and re-retrieved over a
+ single connection. The value of an attribute of the cacheable can be
+ accessed on the receiving side.
+ """
+ d = self.ref.callRemote("giveMeCache", self.orig)
+ def cb(res, again):
+ self.assertIsInstance(res, NewStyleCacheCopy)
+ self.assertEqual("value", res.s)
+ # no cheating :)
+ self.assertNotIdentical(self.orig, res)
+
+ if again:
+ # Save a reference so it stays alive for the rest of this test
+ self.res = res
+ # And ask for it again to exercise the special re-jelly logic in
+ # Cacheable.
+ return self.ref.callRemote("giveMeCache", self.orig)
+ d.addCallback(cb, True)
+ d.addCallback(cb, False)
+ return d
+
+
+
+class BrokerTestCase(unittest.TestCase):
+ thunkResult = None
+
+ def tearDown(self):
+ try:
+ # from RemotePublished.getFileName
+ os.unlink('None-None-TESTING.pub')
+ except OSError:
+ pass
+
+ def thunkErrorBad(self, error):
+ self.fail("This should cause a return value, not %s" % (error,))
+
+ def thunkResultGood(self, result):
+ self.thunkResult = result
+
+ def thunkErrorGood(self, tb):
+ pass
+
+ def thunkResultBad(self, result):
+ self.fail("This should cause an error, not %s" % (result,))
+
+ def test_reference(self):
+ c, s, pump = connectedServerAndClient()
+
+ class X(pb.Referenceable):
+ def remote_catch(self,arg):
+ self.caught = arg
+
+ class Y(pb.Referenceable):
+ def remote_throw(self, a, b):
+ a.callRemote('catch', b)
+
+ s.setNameForLocal("y", Y())
+ y = c.remoteForName("y")
+ x = X()
+ z = X()
+ y.callRemote('throw', x, z)
+ pump.pump()
+ pump.pump()
+ pump.pump()
+ self.assertIdentical(x.caught, z, "X should have caught Z")
+
+ # make sure references to remote methods are equals
+ self.assertEqual(y.remoteMethod('throw'), y.remoteMethod('throw'))
+
+ def test_result(self):
+ c, s, pump = connectedServerAndClient()
+ for x, y in (c, s), (s, c):
+ # test reflexivity
+ foo = SimpleRemote()
+ x.setNameForLocal("foo", foo)
+ bar = y.remoteForName("foo")
+ self.expectedThunkResult = 8
+ bar.callRemote('thunk',self.expectedThunkResult - 1
+ ).addCallbacks(self.thunkResultGood, self.thunkErrorBad)
+ # Send question.
+ pump.pump()
+ # Send response.
+ pump.pump()
+ # Shouldn't require any more pumping than that...
+ self.assertEqual(self.thunkResult, self.expectedThunkResult,
+ "result wasn't received.")
+
+ def refcountResult(self, result):
+ self.nestedRemote = result
+
+ def test_tooManyRefs(self):
+ l = []
+ e = []
+ c, s, pump = connectedServerAndClient()
+ foo = NestedRemote()
+ s.setNameForLocal("foo", foo)
+ x = c.remoteForName("foo")
+ for igno in xrange(pb.MAX_BROKER_REFS + 10):
+ if s.transport.closed or c.transport.closed:
+ break
+ x.callRemote("getSimple").addCallbacks(l.append, e.append)
+ pump.pump()
+ expected = (pb.MAX_BROKER_REFS - 1)
+ self.assertTrue(s.transport.closed, "transport was not closed")
+ self.assertEqual(len(l), expected,
+ "expected %s got %s" % (expected, len(l)))
+
+ def test_copy(self):
+ c, s, pump = connectedServerAndClient()
+ foo = NestedCopy()
+ s.setNameForLocal("foo", foo)
+ x = c.remoteForName("foo")
+ x.callRemote('getCopy'
+ ).addCallbacks(self.thunkResultGood, self.thunkErrorBad)
+ pump.pump()
+ pump.pump()
+ self.assertEqual(self.thunkResult.x, 1)
+ self.assertEqual(self.thunkResult.y['Hello'], 'World')
+ self.assertEqual(self.thunkResult.z[0], 'test')
+
+ def test_observe(self):
+ c, s, pump = connectedServerAndClient()
+
+ # this is really testing the comparison between remote objects, to make
+ # sure that you can *UN*observe when you have an observer architecture.
+ a = Observable()
+ b = Observer()
+ s.setNameForLocal("a", a)
+ ra = c.remoteForName("a")
+ ra.callRemote('observe',b)
+ pump.pump()
+ a.notify(1)
+ pump.pump()
+ pump.pump()
+ a.notify(10)
+ pump.pump()
+ pump.pump()
+ self.assertNotIdentical(b.obj, None, "didn't notify")
+ self.assertEqual(b.obj, 1, 'notified too much')
+
+ def test_defer(self):
+ c, s, pump = connectedServerAndClient()
+ d = DeferredRemote()
+ s.setNameForLocal("d", d)
+ e = c.remoteForName("d")
+ pump.pump(); pump.pump()
+ results = []
+ e.callRemote('doItLater').addCallback(results.append)
+ pump.pump(); pump.pump()
+ self.assertFalse(d.run, "Deferred method run too early.")
+ d.d.callback(5)
+ self.assertEqual(d.run, 5, "Deferred method run too late.")
+ pump.pump(); pump.pump()
+ self.assertEqual(results[0], 6, "Incorrect result.")
+
+
+ def test_refcount(self):
+ c, s, pump = connectedServerAndClient()
+ foo = NestedRemote()
+ s.setNameForLocal("foo", foo)
+ bar = c.remoteForName("foo")
+ bar.callRemote('getSimple'
+ ).addCallbacks(self.refcountResult, self.thunkErrorBad)
+
+ # send question
+ pump.pump()
+ # send response
+ pump.pump()
+
+ # delving into internal structures here, because GC is sort of
+ # inherently internal.
+ rluid = self.nestedRemote.luid
+ self.assertIn(rluid, s.localObjects)
+ del self.nestedRemote
+ # nudge the gc
+ if sys.hexversion >= 0x2000000:
+ gc.collect()
+ # try to nudge the GC even if we can't really
+ pump.pump()
+ pump.pump()
+ pump.pump()
+ self.assertNotIn(rluid, s.localObjects)
+
+ def test_cache(self):
+ c, s, pump = connectedServerAndClient()
+ obj = NestedCache()
+ obj2 = NestedComplicatedCache()
+ vcc = obj2.c
+ s.setNameForLocal("obj", obj)
+ s.setNameForLocal("xxx", obj2)
+ o2 = c.remoteForName("obj")
+ o3 = c.remoteForName("xxx")
+ coll = []
+ o2.callRemote("getCache"
+ ).addCallback(coll.append).addErrback(coll.append)
+ o2.callRemote("getCache"
+ ).addCallback(coll.append).addErrback(coll.append)
+ complex = []
+ o3.callRemote("getCache").addCallback(complex.append)
+ o3.callRemote("getCache").addCallback(complex.append)
+ pump.flush()
+ # `worst things first'
+ self.assertEqual(complex[0].x, 1)
+ self.assertEqual(complex[0].y, 2)
+ self.assertEqual(complex[0].foo, 3)
+
+ vcc.setFoo4()
+ pump.flush()
+ self.assertEqual(complex[0].foo, 4)
+ self.assertEqual(len(coll), 2)
+ cp = coll[0][0]
+ self.assertIdentical(cp.checkMethod().im_self, cp,
+ "potential refcounting issue")
+ self.assertIdentical(cp.checkSelf(), cp,
+ "other potential refcounting issue")
+ col2 = []
+ o2.callRemote('putCache',cp).addCallback(col2.append)
+ pump.flush()
+ # The objects were the same (testing lcache identity)
+ self.assertTrue(col2[0])
+ # test equality of references to methods
+ self.assertEqual(o2.remoteMethod("getCache"),
+ o2.remoteMethod("getCache"))
+
+ # now, refcounting (similiar to testRefCount)
+ luid = cp.luid
+ baroqueLuid = complex[0].luid
+ self.assertIn(luid, s.remotelyCachedObjects,
+ "remote cache doesn't have it")
+ del coll
+ del cp
+ pump.flush()
+ del complex
+ del col2
+ # extra nudge...
+ pump.flush()
+ # del vcc.observer
+ # nudge the gc
+ if sys.hexversion >= 0x2000000:
+ gc.collect()
+ # try to nudge the GC even if we can't really
+ pump.flush()
+ # The GC is done with it.
+ self.assertNotIn(luid, s.remotelyCachedObjects,
+ "Server still had it after GC")
+ self.assertNotIn(luid, c.locallyCachedObjects,
+ "Client still had it after GC")
+ self.assertNotIn(baroqueLuid, s.remotelyCachedObjects,
+ "Server still had complex after GC")
+ self.assertNotIn(baroqueLuid, c.locallyCachedObjects,
+ "Client still had complex after GC")
+ self.assertIdentical(vcc.observer, None, "observer was not removed")
+
+ def test_publishable(self):
+ try:
+ os.unlink('None-None-TESTING.pub') # from RemotePublished.getFileName
+ except OSError:
+ pass # Sometimes it's not there.
+ c, s, pump = connectedServerAndClient()
+ foo = GetPublisher()
+ # foo.pub.timestamp = 1.0
+ s.setNameForLocal("foo", foo)
+ bar = c.remoteForName("foo")
+ accum = []
+ bar.callRemote('getPub').addCallbacks(accum.append, self.thunkErrorBad)
+ pump.flush()
+ obj = accum.pop()
+ self.assertEqual(obj.activateCalled, 1)
+ self.assertEqual(obj.isActivated, 1)
+ self.assertEqual(obj.yayIGotPublished, 1)
+ # timestamp's dirty, we don't have a cache file
+ self.assertEqual(obj._wasCleanWhenLoaded, 0)
+ c, s, pump = connectedServerAndClient()
+ s.setNameForLocal("foo", foo)
+ bar = c.remoteForName("foo")
+ bar.callRemote('getPub').addCallbacks(accum.append, self.thunkErrorBad)
+ pump.flush()
+ obj = accum.pop()
+ # timestamp's clean, our cache file is up-to-date
+ self.assertEqual(obj._wasCleanWhenLoaded, 1)
+
+ def gotCopy(self, val):
+ self.thunkResult = val.id
+
+
+ def test_factoryCopy(self):
+ c, s, pump = connectedServerAndClient()
+ ID = 99
+ obj = NestedCopy()
+ s.setNameForLocal("foo", obj)
+ x = c.remoteForName("foo")
+ x.callRemote('getFactory', ID
+ ).addCallbacks(self.gotCopy, self.thunkResultBad)
+ pump.pump()
+ pump.pump()
+ pump.pump()
+ self.assertEqual(self.thunkResult, ID,
+ "ID not correct on factory object %s" % (self.thunkResult,))
+
+
+bigString = "helloworld" * 50
+
+callbackArgs = None
+callbackKeyword = None
+
+def finishedCallback(*args, **kw):
+ global callbackArgs, callbackKeyword
+ callbackArgs = args
+ callbackKeyword = kw
+
+
+class Pagerizer(pb.Referenceable):
+ def __init__(self, callback, *args, **kw):
+ self.callback, self.args, self.kw = callback, args, kw
+
+ def remote_getPages(self, collector):
+ util.StringPager(collector, bigString, 100,
+ self.callback, *self.args, **self.kw)
+ self.args = self.kw = None
+
+
+class FilePagerizer(pb.Referenceable):
+ pager = None
+
+ def __init__(self, filename, callback, *args, **kw):
+ self.filename = filename
+ self.callback, self.args, self.kw = callback, args, kw
+
+ def remote_getPages(self, collector):
+ self.pager = util.FilePager(collector, file(self.filename),
+ self.callback, *self.args, **self.kw)
+ self.args = self.kw = None
+
+
+
+class PagingTestCase(unittest.TestCase):
+ """
+ Test pb objects sending data by pages.
+ """
+
+ def setUp(self):
+ """
+ Create a file used to test L{util.FilePager}.
+ """
+ self.filename = self.mktemp()
+ fd = file(self.filename, 'w')
+ fd.write(bigString)
+ fd.close()
+
+
+ def test_pagingWithCallback(self):
+ """
+ Test L{util.StringPager}, passing a callback to fire when all pages
+ are sent.
+ """
+ c, s, pump = connectedServerAndClient()
+ s.setNameForLocal("foo", Pagerizer(finishedCallback, 'hello', value=10))
+ x = c.remoteForName("foo")
+ l = []
+ util.getAllPages(x, "getPages").addCallback(l.append)
+ while not l:
+ pump.pump()
+ self.assertEqual(''.join(l[0]), bigString,
+ "Pages received not equal to pages sent!")
+ self.assertEqual(callbackArgs, ('hello',),
+ "Completed callback not invoked")
+ self.assertEqual(callbackKeyword, {'value': 10},
+ "Completed callback not invoked")
+
+
+ def test_pagingWithoutCallback(self):
+ """
+ Test L{util.StringPager} without a callback.
+ """
+ c, s, pump = connectedServerAndClient()
+ s.setNameForLocal("foo", Pagerizer(None))
+ x = c.remoteForName("foo")
+ l = []
+ util.getAllPages(x, "getPages").addCallback(l.append)
+ while not l:
+ pump.pump()
+ self.assertEqual(''.join(l[0]), bigString,
+ "Pages received not equal to pages sent!")
+
+
+ def test_emptyFilePaging(self):
+ """
+ Test L{util.FilePager}, sending an empty file.
+ """
+ filenameEmpty = self.mktemp()
+ fd = file(filenameEmpty, 'w')
+ fd.close()
+ c, s, pump = connectedServerAndClient()
+ pagerizer = FilePagerizer(filenameEmpty, None)
+ s.setNameForLocal("bar", pagerizer)
+ x = c.remoteForName("bar")
+ l = []
+ util.getAllPages(x, "getPages").addCallback(l.append)
+ ttl = 10
+ while not l and ttl > 0:
+ pump.pump()
+ ttl -= 1
+ if not ttl:
+ self.fail('getAllPages timed out')
+ self.assertEqual(''.join(l[0]), '',
+ "Pages received not equal to pages sent!")
+
+
+ def test_filePagingWithCallback(self):
+ """
+ Test L{util.FilePager}, passing a callback to fire when all pages
+ are sent, and verify that the pager doesn't keep chunks in memory.
+ """
+ c, s, pump = connectedServerAndClient()
+ pagerizer = FilePagerizer(self.filename, finishedCallback,
+ 'frodo', value = 9)
+ s.setNameForLocal("bar", pagerizer)
+ x = c.remoteForName("bar")
+ l = []
+ util.getAllPages(x, "getPages").addCallback(l.append)
+ while not l:
+ pump.pump()
+ self.assertEqual(''.join(l[0]), bigString,
+ "Pages received not equal to pages sent!")
+ self.assertEqual(callbackArgs, ('frodo',),
+ "Completed callback not invoked")
+ self.assertEqual(callbackKeyword, {'value': 9},
+ "Completed callback not invoked")
+ self.assertEqual(pagerizer.pager.chunks, [])
+
+
+ def test_filePagingWithoutCallback(self):
+ """
+ Test L{util.FilePager} without a callback.
+ """
+ c, s, pump = connectedServerAndClient()
+ pagerizer = FilePagerizer(self.filename, None)
+ s.setNameForLocal("bar", pagerizer)
+ x = c.remoteForName("bar")
+ l = []
+ util.getAllPages(x, "getPages").addCallback(l.append)
+ while not l:
+ pump.pump()
+ self.assertEqual(''.join(l[0]), bigString,
+ "Pages received not equal to pages sent!")
+ self.assertEqual(pagerizer.pager.chunks, [])
+
+
+
+class DumbPublishable(publish.Publishable):
+ def getStateToPublish(self):
+ return {"yayIGotPublished": 1}
+
+
+class DumbPub(publish.RemotePublished):
+ def activated(self):
+ self.activateCalled = 1
+
+
+class GetPublisher(pb.Referenceable):
+ def __init__(self):
+ self.pub = DumbPublishable("TESTING")
+
+ def remote_getPub(self):
+ return self.pub
+
+
+pb.setUnjellyableForClass(DumbPublishable, DumbPub)
+
+class DisconnectionTestCase(unittest.TestCase):
+ """
+ Test disconnection callbacks.
+ """
+
+ def error(self, *args):
+ raise RuntimeError("I shouldn't have been called: %s" % (args,))
+
+
+ def gotDisconnected(self):
+ """
+ Called on broker disconnect.
+ """
+ self.gotCallback = 1
+
+ def objectDisconnected(self, o):
+ """
+ Called on RemoteReference disconnect.
+ """
+ self.assertEqual(o, self.remoteObject)
+ self.objectCallback = 1
+
+ def test_badSerialization(self):
+ c, s, pump = connectedServerAndClient()
+ pump.pump()
+ s.setNameForLocal("o", BadCopySet())
+ g = c.remoteForName("o")
+ l = []
+ g.callRemote("setBadCopy", BadCopyable()).addErrback(l.append)
+ pump.flush()
+ self.assertEqual(len(l), 1)
+
+ def test_disconnection(self):
+ c, s, pump = connectedServerAndClient()
+ pump.pump()
+ s.setNameForLocal("o", SimpleRemote())
+
+ # get a client reference to server object
+ r = c.remoteForName("o")
+ pump.pump()
+ pump.pump()
+ pump.pump()
+
+ # register and then unregister disconnect callbacks
+ # making sure they get unregistered
+ c.notifyOnDisconnect(self.error)
+ self.assertIn(self.error, c.disconnects)
+ c.dontNotifyOnDisconnect(self.error)
+ self.assertNotIn(self.error, c.disconnects)
+
+ r.notifyOnDisconnect(self.error)
+ self.assertIn(r._disconnected, c.disconnects)
+ self.assertIn(self.error, r.disconnectCallbacks)
+ r.dontNotifyOnDisconnect(self.error)
+ self.assertNotIn(r._disconnected, c.disconnects)
+ self.assertNotIn(self.error, r.disconnectCallbacks)
+
+ # register disconnect callbacks
+ c.notifyOnDisconnect(self.gotDisconnected)
+ r.notifyOnDisconnect(self.objectDisconnected)
+ self.remoteObject = r
+
+ # disconnect
+ c.connectionLost(failure.Failure(main.CONNECTION_DONE))
+ self.assertTrue(self.gotCallback)
+ self.assertTrue(self.objectCallback)
+
+
+class FreakOut(Exception):
+ pass
+
+
+class BadCopyable(pb.Copyable):
+ def getStateToCopyFor(self, p):
+ raise FreakOut()
+
+
+class BadCopySet(pb.Referenceable):
+ def remote_setBadCopy(self, bc):
+ return None
+
+
+class LocalRemoteTest(util.LocalAsRemote):
+ reportAllTracebacks = 0
+
+ def sync_add1(self, x):
+ return x + 1
+
+ def async_add(self, x=0, y=1):
+ return x + y
+
+ def async_fail(self):
+ raise RuntimeError()
+
+
+
+class MyPerspective(pb.Avatar):
+ """
+ @ivar loggedIn: set to C{True} when the avatar is logged in.
+ @type loggedIn: C{bool}
+
+ @ivar loggedOut: set to C{True} when the avatar is logged out.
+ @type loggedOut: C{bool}
+ """
+ implements(pb.IPerspective)
+
+ loggedIn = loggedOut = False
+
+ def __init__(self, avatarId):
+ self.avatarId = avatarId
+
+
+ def perspective_getAvatarId(self):
+ """
+ Return the avatar identifier which was used to access this avatar.
+ """
+ return self.avatarId
+
+
+ def perspective_getViewPoint(self):
+ return MyView()
+
+
+ def perspective_add(self, a, b):
+ """
+ Add the given objects and return the result. This is a method
+ unavailable on L{Echoer}, so it can only be invoked by authenticated
+ users who received their avatar from L{TestRealm}.
+ """
+ return a + b
+
+
+ def logout(self):
+ self.loggedOut = True
+
+
+
+class TestRealm(object):
+ """
+ A realm which repeatedly gives out a single instance of L{MyPerspective}
+ for non-anonymous logins and which gives out a new instance of L{Echoer}
+ for each anonymous login.
+
+ @ivar lastPerspective: The L{MyPerspective} most recently created and
+ returned from C{requestAvatar}.
+
+ @ivar perspectiveFactory: A one-argument callable which will be used to
+ create avatars to be returned from C{requestAvatar}.
+ """
+ perspectiveFactory = MyPerspective
+
+ lastPerspective = None
+
+ def requestAvatar(self, avatarId, mind, interface):
+ """
+ Verify that the mind and interface supplied have the expected values
+ (this should really be done somewhere else, like inside a test method)
+ and return an avatar appropriate for the given identifier.
+ """
+ assert interface == pb.IPerspective
+ assert mind == "BRAINS!"
+ if avatarId is checkers.ANONYMOUS:
+ return pb.IPerspective, Echoer(), lambda: None
+ else:
+ self.lastPerspective = self.perspectiveFactory(avatarId)
+ self.lastPerspective.loggedIn = True
+ return (
+ pb.IPerspective, self.lastPerspective,
+ self.lastPerspective.logout)
+
+
+
+class MyView(pb.Viewable):
+
+ def view_check(self, user):
+ return isinstance(user, MyPerspective)
+
+
+
+class LeakyRealm(TestRealm):
+ """
+ A realm which hangs onto a reference to the mind object in its logout
+ function.
+ """
+ def __init__(self, mindEater):
+ """
+ Create a L{LeakyRealm}.
+
+ @param mindEater: a callable that will be called with the C{mind}
+ object when it is available
+ """
+ self._mindEater = mindEater
+
+
+ def requestAvatar(self, avatarId, mind, interface):
+ self._mindEater(mind)
+ persp = self.perspectiveFactory(avatarId)
+ return (pb.IPerspective, persp, lambda : (mind, persp.logout()))
+
+
+
+class NewCredLeakTests(unittest.TestCase):
+ """
+ Tests to try to trigger memory leaks.
+ """
+ def test_logoutLeak(self):
+ """
+ The server does not leak a reference when the client disconnects
+ suddenly, even if the cred logout function forms a reference cycle with
+ the perspective.
+ """
+ # keep a weak reference to the mind object, which we can verify later
+ # evaluates to None, thereby ensuring the reference leak is fixed.
+ self.mindRef = None
+ def setMindRef(mind):
+ self.mindRef = weakref.ref(mind)
+
+ clientBroker, serverBroker, pump = connectedServerAndClient(
+ LeakyRealm(setMindRef))
+
+ # log in from the client
+ connectionBroken = []
+ root = clientBroker.remoteForName("root")
+ d = root.callRemote("login", 'guest')
+ def cbResponse((challenge, challenger)):
+ mind = SimpleRemote()
+ return challenger.callRemote("respond",
+ pb.respond(challenge, 'guest'), mind)
+ d.addCallback(cbResponse)
+ def connectionLost(_):
+ pump.stop() # don't try to pump data anymore - it won't work
+ connectionBroken.append(1)
+ serverBroker.connectionLost(failure.Failure(RuntimeError("boom")))
+ d.addCallback(connectionLost)
+
+ # flush out the response and connectionLost
+ pump.flush()
+ self.assertEqual(connectionBroken, [1])
+
+ # and check for lingering references - requestAvatar sets mindRef
+ # to a weakref to the mind; this object should be gc'd, and thus
+ # the ref should return None
+ gc.collect()
+ self.assertEqual(self.mindRef(), None)
+
+
+
+class NewCredTestCase(unittest.TestCase):
+ """
+ Tests related to the L{twisted.cred} support in PB.
+ """
+ def setUp(self):
+ """
+ Create a portal with no checkers and wrap it around a simple test
+ realm. Set up a PB server on a TCP port which serves perspectives
+ using that portal.
+ """
+ self.realm = TestRealm()
+ self.portal = portal.Portal(self.realm)
+ self.factory = ConnectionNotifyServerFactory(self.portal)
+ self.port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
+ self.portno = self.port.getHost().port
+
+
+ def tearDown(self):
+ """
+ Shut down the TCP port created by L{setUp}.
+ """
+ return self.port.stopListening()
+
+
+ def getFactoryAndRootObject(self, clientFactory=pb.PBClientFactory):
+ """
+ Create a connection to the test server.
+
+ @param clientFactory: the factory class used to create the connection.
+
+ @return: a tuple (C{factory}, C{deferred}), where factory is an
+ instance of C{clientFactory} and C{deferred} the L{Deferred} firing
+ with the PB root object.
+ """
+ factory = clientFactory()
+ rootObjDeferred = factory.getRootObject()
+ connector = reactor.connectTCP('127.0.0.1', self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return factory, rootObjDeferred
+
+
+ def test_getRootObject(self):
+ """
+ Assert only that L{PBClientFactory.getRootObject}'s Deferred fires with
+ a L{RemoteReference}.
+ """
+ factory, rootObjDeferred = self.getFactoryAndRootObject()
+
+ def gotRootObject(rootObj):
+ self.assertIsInstance(rootObj, pb.RemoteReference)
+ disconnectedDeferred = Deferred()
+ rootObj.notifyOnDisconnect(disconnectedDeferred.callback)
+ factory.disconnect()
+ return disconnectedDeferred
+
+ return rootObjDeferred.addCallback(gotRootObject)
+
+
+ def test_deadReferenceError(self):
+ """
+ Test that when a connection is lost, calling a method on a
+ RemoteReference obtained from it raises DeadReferenceError.
+ """
+ factory, rootObjDeferred = self.getFactoryAndRootObject()
+
+ def gotRootObject(rootObj):
+ disconnectedDeferred = Deferred()
+ rootObj.notifyOnDisconnect(disconnectedDeferred.callback)
+
+ def lostConnection(ign):
+ self.assertRaises(
+ pb.DeadReferenceError,
+ rootObj.callRemote, 'method')
+
+ disconnectedDeferred.addCallback(lostConnection)
+ factory.disconnect()
+ return disconnectedDeferred
+
+ return rootObjDeferred.addCallback(gotRootObject)
+
+
+ def test_clientConnectionLost(self):
+ """
+ Test that if the L{reconnecting} flag is passed with a True value then
+ a remote call made from a disconnection notification callback gets a
+ result successfully.
+ """
+ class ReconnectOnce(pb.PBClientFactory):
+ reconnectedAlready = False
+ def clientConnectionLost(self, connector, reason):
+ reconnecting = not self.reconnectedAlready
+ self.reconnectedAlready = True
+ if reconnecting:
+ connector.connect()
+ return pb.PBClientFactory.clientConnectionLost(
+ self, connector, reason, reconnecting)
+
+ factory, rootObjDeferred = self.getFactoryAndRootObject(ReconnectOnce)
+
+ def gotRootObject(rootObj):
+ self.assertIsInstance(rootObj, pb.RemoteReference)
+
+ d = Deferred()
+ rootObj.notifyOnDisconnect(d.callback)
+ factory.disconnect()
+
+ def disconnected(ign):
+ d = factory.getRootObject()
+
+ def gotAnotherRootObject(anotherRootObj):
+ self.assertIsInstance(anotherRootObj, pb.RemoteReference)
+
+ d = Deferred()
+ anotherRootObj.notifyOnDisconnect(d.callback)
+ factory.disconnect()
+ return d
+ return d.addCallback(gotAnotherRootObject)
+ return d.addCallback(disconnected)
+ return rootObjDeferred.addCallback(gotRootObject)
+
+
+ def test_immediateClose(self):
+ """
+ Test that if a Broker loses its connection without receiving any bytes,
+ it doesn't raise any exceptions or log any errors.
+ """
+ serverProto = self.factory.buildProtocol(('127.0.0.1', 12345))
+ serverProto.makeConnection(protocol.FileWrapper(StringIO()))
+ serverProto.connectionLost(failure.Failure(main.CONNECTION_DONE))
+
+
+ def test_loginConnectionRefused(self):
+ """
+ L{PBClientFactory.login} returns a L{Deferred} which is errbacked
+ with the L{ConnectionRefusedError} if the underlying connection is
+ refused.
+ """
+ clientFactory = pb.PBClientFactory()
+ loginDeferred = clientFactory.login(
+ credentials.UsernamePassword("foo", "bar"))
+ clientFactory.clientConnectionFailed(
+ None,
+ failure.Failure(
+ ConnectionRefusedError("Test simulated refused connection")))
+ return self.assertFailure(loginDeferred, ConnectionRefusedError)
+
+
+ def _disconnect(self, ignore, factory):
+ """
+ Helper method disconnecting the given client factory and returning a
+ C{Deferred} that will fire when the server connection has noticed the
+ disconnection.
+ """
+ disconnectedDeferred = Deferred()
+ self.factory.protocolInstance.notifyOnDisconnect(
+ lambda: disconnectedDeferred.callback(None))
+ factory.disconnect()
+ return disconnectedDeferred
+
+
+ def test_loginLogout(self):
+ """
+ Test that login can be performed with IUsernamePassword credentials and
+ that when the connection is dropped the avatar is logged out.
+ """
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+ creds = credentials.UsernamePassword("user", "pass")
+
+ # NOTE: real code probably won't need anything where we have the
+ # "BRAINS!" argument, passing None is fine. We just do it here to
+ # test that it is being passed. It is used to give additional info to
+ # the realm to aid perspective creation, if you don't need that,
+ # ignore it.
+ mind = "BRAINS!"
+
+ d = factory.login(creds, mind)
+ def cbLogin(perspective):
+ self.assertTrue(self.realm.lastPerspective.loggedIn)
+ self.assertIsInstance(perspective, pb.RemoteReference)
+ return self._disconnect(None, factory)
+ d.addCallback(cbLogin)
+
+ def cbLogout(ignored):
+ self.assertTrue(self.realm.lastPerspective.loggedOut)
+ d.addCallback(cbLogout)
+
+ connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_logoutAfterDecref(self):
+ """
+ If a L{RemoteReference} to an L{IPerspective} avatar is decrefed and
+ there remain no other references to the avatar on the server, the
+ avatar is garbage collected and the logout method called.
+ """
+ loggedOut = Deferred()
+
+ class EventPerspective(pb.Avatar):
+ """
+ An avatar which fires a Deferred when it is logged out.
+ """
+ def __init__(self, avatarId):
+ pass
+
+ def logout(self):
+ loggedOut.callback(None)
+
+ self.realm.perspectiveFactory = EventPerspective
+
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(foo='bar'))
+ factory = pb.PBClientFactory()
+ d = factory.login(
+ credentials.UsernamePassword('foo', 'bar'), "BRAINS!")
+ def cbLoggedIn(avatar):
+ # Just wait for the logout to happen, as it should since the
+ # reference to the avatar will shortly no longer exists.
+ return loggedOut
+ d.addCallback(cbLoggedIn)
+ def cbLoggedOut(ignored):
+ # Verify that the server broker's _localCleanup dict isn't growing
+ # without bound.
+ self.assertEqual(self.factory.protocolInstance._localCleanup, {})
+ d.addCallback(cbLoggedOut)
+ d.addCallback(self._disconnect, factory)
+ connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_concurrentLogin(self):
+ """
+ Two different correct login attempts can be made on the same root
+ object at the same time and produce two different resulting avatars.
+ """
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(
+ foo='bar', baz='quux'))
+ factory = pb.PBClientFactory()
+
+ firstLogin = factory.login(
+ credentials.UsernamePassword('foo', 'bar'), "BRAINS!")
+ secondLogin = factory.login(
+ credentials.UsernamePassword('baz', 'quux'), "BRAINS!")
+ d = gatherResults([firstLogin, secondLogin])
+ def cbLoggedIn((first, second)):
+ return gatherResults([
+ first.callRemote('getAvatarId'),
+ second.callRemote('getAvatarId')])
+ d.addCallback(cbLoggedIn)
+ def cbAvatarIds((first, second)):
+ self.assertEqual(first, 'foo')
+ self.assertEqual(second, 'baz')
+ d.addCallback(cbAvatarIds)
+ d.addCallback(self._disconnect, factory)
+
+ connector = reactor.connectTCP('127.0.0.1', self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_badUsernamePasswordLogin(self):
+ """
+ Test that a login attempt with an invalid user or invalid password
+ fails in the appropriate way.
+ """
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+
+ firstLogin = factory.login(
+ credentials.UsernamePassword('nosuchuser', 'pass'))
+ secondLogin = factory.login(
+ credentials.UsernamePassword('user', 'wrongpass'))
+
+ self.assertFailure(firstLogin, UnauthorizedLogin)
+ self.assertFailure(secondLogin, UnauthorizedLogin)
+ d = gatherResults([firstLogin, secondLogin])
+
+ def cleanup(ignore):
+ errors = self.flushLoggedErrors(UnauthorizedLogin)
+ self.assertEqual(len(errors), 2)
+ return self._disconnect(None, factory)
+ d.addCallback(cleanup)
+
+ connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_anonymousLogin(self):
+ """
+ Verify that a PB server using a portal configured with an checker which
+ allows IAnonymous credentials can be logged into using IAnonymous
+ credentials.
+ """
+ self.portal.registerChecker(checkers.AllowAnonymousAccess())
+ factory = pb.PBClientFactory()
+ d = factory.login(credentials.Anonymous(), "BRAINS!")
+
+ def cbLoggedIn(perspective):
+ return perspective.callRemote('echo', 123)
+ d.addCallback(cbLoggedIn)
+
+ d.addCallback(self.assertEqual, 123)
+
+ d.addCallback(self._disconnect, factory)
+
+ connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_anonymousLoginNotPermitted(self):
+ """
+ Verify that without an anonymous checker set up, anonymous login is
+ rejected.
+ """
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+ d = factory.login(credentials.Anonymous(), "BRAINS!")
+ self.assertFailure(d, UnhandledCredentials)
+
+ def cleanup(ignore):
+ errors = self.flushLoggedErrors(UnhandledCredentials)
+ self.assertEqual(len(errors), 1)
+ return self._disconnect(None, factory)
+ d.addCallback(cleanup)
+
+ connector = reactor.connectTCP('127.0.0.1', self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_anonymousLoginWithMultipleCheckers(self):
+ """
+ Like L{test_anonymousLogin} but against a portal with a checker for
+ both IAnonymous and IUsernamePassword.
+ """
+ self.portal.registerChecker(checkers.AllowAnonymousAccess())
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+ d = factory.login(credentials.Anonymous(), "BRAINS!")
+
+ def cbLogin(perspective):
+ return perspective.callRemote('echo', 123)
+ d.addCallback(cbLogin)
+
+ d.addCallback(self.assertEqual, 123)
+
+ d.addCallback(self._disconnect, factory)
+
+ connector = reactor.connectTCP('127.0.0.1', self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_authenticatedLoginWithMultipleCheckers(self):
+ """
+ Like L{test_anonymousLoginWithMultipleCheckers} but check that
+ username/password authentication works.
+ """
+ self.portal.registerChecker(checkers.AllowAnonymousAccess())
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+ d = factory.login(
+ credentials.UsernamePassword('user', 'pass'), "BRAINS!")
+
+ def cbLogin(perspective):
+ return perspective.callRemote('add', 100, 23)
+ d.addCallback(cbLogin)
+
+ d.addCallback(self.assertEqual, 123)
+
+ d.addCallback(self._disconnect, factory)
+
+ connector = reactor.connectTCP('127.0.0.1', self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+ def test_view(self):
+ """
+ Verify that a viewpoint can be retrieved after authenticating with
+ cred.
+ """
+ self.portal.registerChecker(
+ checkers.InMemoryUsernamePasswordDatabaseDontUse(user='pass'))
+ factory = pb.PBClientFactory()
+ d = factory.login(
+ credentials.UsernamePassword("user", "pass"), "BRAINS!")
+
+ def cbLogin(perspective):
+ return perspective.callRemote("getViewPoint")
+ d.addCallback(cbLogin)
+
+ def cbView(viewpoint):
+ return viewpoint.callRemote("check")
+ d.addCallback(cbView)
+
+ d.addCallback(self.assertTrue)
+
+ d.addCallback(self._disconnect, factory)
+
+ connector = reactor.connectTCP("127.0.0.1", self.portno, factory)
+ self.addCleanup(connector.disconnect)
+ return d
+
+
+
+class NonSubclassingPerspective:
+ implements(pb.IPerspective)
+
+ def __init__(self, avatarId):
+ pass
+
+ # IPerspective implementation
+ def perspectiveMessageReceived(self, broker, message, args, kwargs):
+ args = broker.unserialize(args, self)
+ kwargs = broker.unserialize(kwargs, self)
+ return broker.serialize((message, args, kwargs))
+
+ # Methods required by TestRealm
+ def logout(self):
+ self.loggedOut = True
+
+
+
+class NSPTestCase(unittest.TestCase):
+ """
+ Tests for authentication against a realm where the L{IPerspective}
+ implementation is not a subclass of L{Avatar}.
+ """
+ def setUp(self):
+ self.realm = TestRealm()
+ self.realm.perspectiveFactory = NonSubclassingPerspective
+ self.portal = portal.Portal(self.realm)
+ self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ self.checker.addUser("user", "pass")
+ self.portal.registerChecker(self.checker)
+ self.factory = WrappingFactory(pb.PBServerFactory(self.portal))
+ self.port = reactor.listenTCP(0, self.factory, interface="127.0.0.1")
+ self.addCleanup(self.port.stopListening)
+ self.portno = self.port.getHost().port
+
+
+ def test_NSP(self):
+ """
+ An L{IPerspective} implementation which does not subclass
+ L{Avatar} can expose remote methods for the client to call.
+ """
+ factory = pb.PBClientFactory()
+ d = factory.login(credentials.UsernamePassword('user', 'pass'),
+ "BRAINS!")
+ reactor.connectTCP('127.0.0.1', self.portno, factory)
+ d.addCallback(lambda p: p.callRemote('ANYTHING', 'here', bar='baz'))
+ d.addCallback(self.assertEqual,
+ ('ANYTHING', ('here',), {'bar': 'baz'}))
+ def cleanup(ignored):
+ factory.disconnect()
+ for p in self.factory.protocols:
+ p.transport.loseConnection()
+ d.addCallback(cleanup)
+ return d
+
+
+
+class IForwarded(Interface):
+ """
+ Interface used for testing L{util.LocalAsyncForwarder}.
+ """
+
+ def forwardMe():
+ """
+ Simple synchronous method.
+ """
+
+ def forwardDeferred():
+ """
+ Simple asynchronous method.
+ """
+
+
+class Forwarded:
+ """
+ Test implementation of L{IForwarded}.
+
+ @ivar forwarded: set if C{forwardMe} is called.
+ @type forwarded: C{bool}
+ @ivar unforwarded: set if C{dontForwardMe} is called.
+ @type unforwarded: C{bool}
+ """
+ implements(IForwarded)
+ forwarded = False
+ unforwarded = False
+
+ def forwardMe(self):
+ """
+ Set a local flag to test afterwards.
+ """
+ self.forwarded = True
+
+ def dontForwardMe(self):
+ """
+ Set a local flag to test afterwards. This should not be called as it's
+ not in the interface.
+ """
+ self.unforwarded = True
+
+ def forwardDeferred(self):
+ """
+ Asynchronously return C{True}.
+ """
+ return succeed(True)
+
+
+class SpreadUtilTestCase(unittest.TestCase):
+ """
+ Tests for L{twisted.spread.util}.
+ """
+
+ def test_sync(self):
+ """
+ Call a synchronous method of a L{util.LocalAsRemote} object and check
+ the result.
+ """
+ o = LocalRemoteTest()
+ self.assertEqual(o.callRemote("add1", 2), 3)
+
+ def test_async(self):
+ """
+ Call an asynchronous method of a L{util.LocalAsRemote} object and check
+ the result.
+ """
+ o = LocalRemoteTest()
+ o = LocalRemoteTest()
+ d = o.callRemote("add", 2, y=4)
+ self.assertIsInstance(d, Deferred)
+ d.addCallback(self.assertEqual, 6)
+ return d
+
+ def test_asyncFail(self):
+ """
+ Test a asynchronous failure on a remote method call.
+ """
+ o = LocalRemoteTest()
+ d = o.callRemote("fail")
+ def eb(f):
+ self.assertTrue(isinstance(f, failure.Failure))
+ f.trap(RuntimeError)
+ d.addCallbacks(lambda res: self.fail("supposed to fail"), eb)
+ return d
+
+ def test_remoteMethod(self):
+ """
+ Test the C{remoteMethod} facility of L{util.LocalAsRemote}.
+ """
+ o = LocalRemoteTest()
+ m = o.remoteMethod("add1")
+ self.assertEqual(m(3), 4)
+
+ def test_localAsyncForwarder(self):
+ """
+ Test a call to L{util.LocalAsyncForwarder} using L{Forwarded} local
+ object.
+ """
+ f = Forwarded()
+ lf = util.LocalAsyncForwarder(f, IForwarded)
+ lf.callRemote("forwardMe")
+ self.assertTrue(f.forwarded)
+ lf.callRemote("dontForwardMe")
+ self.assertFalse(f.unforwarded)
+ rr = lf.callRemote("forwardDeferred")
+ l = []
+ rr.addCallback(l.append)
+ self.assertEqual(l[0], 1)
+
+
+
+class PBWithSecurityOptionsTest(unittest.TestCase):
+ """
+ Test security customization.
+ """
+
+ def test_clientDefaultSecurityOptions(self):
+ """
+ By default, client broker should use C{jelly.globalSecurity} as
+ security settings.
+ """
+ factory = pb.PBClientFactory()
+ broker = factory.buildProtocol(None)
+ self.assertIdentical(broker.security, jelly.globalSecurity)
+
+
+ def test_serverDefaultSecurityOptions(self):
+ """
+ By default, server broker should use C{jelly.globalSecurity} as
+ security settings.
+ """
+ factory = pb.PBServerFactory(Echoer())
+ broker = factory.buildProtocol(None)
+ self.assertIdentical(broker.security, jelly.globalSecurity)
+
+
+ def test_clientSecurityCustomization(self):
+ """
+ Check that the security settings are passed from the client factory to
+ the broker object.
+ """
+ security = jelly.SecurityOptions()
+ factory = pb.PBClientFactory(security=security)
+ broker = factory.buildProtocol(None)
+ self.assertIdentical(broker.security, security)
+
+
+ def test_serverSecurityCustomization(self):
+ """
+ Check that the security settings are passed from the server factory to
+ the broker object.
+ """
+ security = jelly.SecurityOptions()
+ factory = pb.PBServerFactory(Echoer(), security=security)
+ broker = factory.buildProtocol(None)
+ self.assertIdentical(broker.security, security)
diff --git a/twisted/test/test_pbfailure.py b/twisted/test/test_pbfailure.py
new file mode 100644
index 0000000..91cd6ba
--- /dev/null
+++ b/twisted/test/test_pbfailure.py
@@ -0,0 +1,475 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for error handling in PB.
+"""
+
+import sys
+from StringIO import StringIO
+
+from twisted.trial import unittest
+
+from twisted.spread import pb, flavors, jelly
+from twisted.internet import reactor, defer
+from twisted.python import log
+
+##
+# test exceptions
+##
+class AsynchronousException(Exception):
+ """
+ Helper used to test remote methods which return Deferreds which fail with
+ exceptions which are not L{pb.Error} subclasses.
+ """
+
+
+class SynchronousException(Exception):
+ """
+ Helper used to test remote methods which raise exceptions which are not
+ L{pb.Error} subclasses.
+ """
+
+
+class AsynchronousError(pb.Error):
+ """
+ Helper used to test remote methods which return Deferreds which fail with
+ exceptions which are L{pb.Error} subclasses.
+ """
+
+
+class SynchronousError(pb.Error):
+ """
+ Helper used to test remote methods which raise exceptions which are
+ L{pb.Error} subclasses.
+ """
+
+
+#class JellyError(flavors.Jellyable, pb.Error): pass
+class JellyError(flavors.Jellyable, pb.Error, pb.RemoteCopy):
+ pass
+
+
+class SecurityError(pb.Error, pb.RemoteCopy):
+ pass
+
+pb.setUnjellyableForClass(JellyError, JellyError)
+pb.setUnjellyableForClass(SecurityError, SecurityError)
+pb.globalSecurity.allowInstancesOf(SecurityError)
+
+
+####
+# server-side
+####
+class SimpleRoot(pb.Root):
+ def remote_asynchronousException(self):
+ """
+ Fail asynchronously with a non-pb.Error exception.
+ """
+ return defer.fail(AsynchronousException("remote asynchronous exception"))
+
+ def remote_synchronousException(self):
+ """
+ Fail synchronously with a non-pb.Error exception.
+ """
+ raise SynchronousException("remote synchronous exception")
+
+ def remote_asynchronousError(self):
+ """
+ Fail asynchronously with a pb.Error exception.
+ """
+ return defer.fail(AsynchronousError("remote asynchronous error"))
+
+ def remote_synchronousError(self):
+ """
+ Fail synchronously with a pb.Error exception.
+ """
+ raise SynchronousError("remote synchronous error")
+
+ def remote_unknownError(self):
+ """
+ Fail with error that is not known to client.
+ """
+ class UnknownError(pb.Error):
+ pass
+ raise UnknownError("I'm not known to client!")
+
+ def remote_jelly(self):
+ self.raiseJelly()
+
+ def remote_security(self):
+ self.raiseSecurity()
+
+ def remote_deferredJelly(self):
+ d = defer.Deferred()
+ d.addCallback(self.raiseJelly)
+ d.callback(None)
+ return d
+
+ def remote_deferredSecurity(self):
+ d = defer.Deferred()
+ d.addCallback(self.raiseSecurity)
+ d.callback(None)
+ return d
+
+ def raiseJelly(self, results=None):
+ raise JellyError("I'm jellyable!")
+
+ def raiseSecurity(self, results=None):
+ raise SecurityError("I'm secure!")
+
+
+
+class SaveProtocolServerFactory(pb.PBServerFactory):
+ """
+ A L{pb.PBServerFactory} that saves the latest connected client in
+ C{protocolInstance}.
+ """
+ protocolInstance = None
+
+ def clientConnectionMade(self, protocol):
+ """
+ Keep track of the given protocol.
+ """
+ self.protocolInstance = protocol
+
+
+
+class PBConnTestCase(unittest.TestCase):
+ unsafeTracebacks = 0
+
+ def setUp(self):
+ self._setUpServer()
+ self._setUpClient()
+
+ def _setUpServer(self):
+ self.serverFactory = SaveProtocolServerFactory(SimpleRoot())
+ self.serverFactory.unsafeTracebacks = self.unsafeTracebacks
+ self.serverPort = reactor.listenTCP(0, self.serverFactory, interface="127.0.0.1")
+
+ def _setUpClient(self):
+ portNo = self.serverPort.getHost().port
+ self.clientFactory = pb.PBClientFactory()
+ self.clientConnector = reactor.connectTCP("127.0.0.1", portNo, self.clientFactory)
+
+ def tearDown(self):
+ if self.serverFactory.protocolInstance is not None:
+ self.serverFactory.protocolInstance.transport.loseConnection()
+ return defer.gatherResults([
+ self._tearDownServer(),
+ self._tearDownClient()])
+
+ def _tearDownServer(self):
+ return defer.maybeDeferred(self.serverPort.stopListening)
+
+ def _tearDownClient(self):
+ self.clientConnector.disconnect()
+ return defer.succeed(None)
+
+
+
+class PBFailureTest(PBConnTestCase):
+ compare = unittest.TestCase.assertEqual
+
+
+ def _exceptionTest(self, method, exceptionType, flush):
+ def eb(err):
+ err.trap(exceptionType)
+ self.compare(err.traceback, "Traceback unavailable\n")
+ if flush:
+ errs = self.flushLoggedErrors(exceptionType)
+ self.assertEqual(len(errs), 1)
+ return (err.type, err.value, err.traceback)
+ d = self.clientFactory.getRootObject()
+ def gotRootObject(root):
+ d = root.callRemote(method)
+ d.addErrback(eb)
+ return d
+ d.addCallback(gotRootObject)
+ return d
+
+
+ def test_asynchronousException(self):
+ """
+ Test that a Deferred returned by a remote method which already has a
+ Failure correctly has that error passed back to the calling side.
+ """
+ return self._exceptionTest(
+ 'asynchronousException', AsynchronousException, True)
+
+
+ def test_synchronousException(self):
+ """
+ Like L{test_asynchronousException}, but for a method which raises an
+ exception synchronously.
+ """
+ return self._exceptionTest(
+ 'synchronousException', SynchronousException, True)
+
+
+ def test_asynchronousError(self):
+ """
+ Like L{test_asynchronousException}, but for a method which returns a
+ Deferred failing with an L{pb.Error} subclass.
+ """
+ return self._exceptionTest(
+ 'asynchronousError', AsynchronousError, False)
+
+
+ def test_synchronousError(self):
+ """
+ Like L{test_asynchronousError}, but for a method which synchronously
+ raises a L{pb.Error} subclass.
+ """
+ return self._exceptionTest(
+ 'synchronousError', SynchronousError, False)
+
+
+ def _success(self, result, expectedResult):
+ self.assertEqual(result, expectedResult)
+ return result
+
+
+ def _addFailingCallbacks(self, remoteCall, expectedResult, eb):
+ remoteCall.addCallbacks(self._success, eb,
+ callbackArgs=(expectedResult,))
+ return remoteCall
+
+
+ def _testImpl(self, method, expected, eb, exc=None):
+ """
+ Call the given remote method and attach the given errback to the
+ resulting Deferred. If C{exc} is not None, also assert that one
+ exception of that type was logged.
+ """
+ rootDeferred = self.clientFactory.getRootObject()
+ def gotRootObj(obj):
+ failureDeferred = self._addFailingCallbacks(obj.callRemote(method), expected, eb)
+ if exc is not None:
+ def gotFailure(err):
+ self.assertEqual(len(self.flushLoggedErrors(exc)), 1)
+ return err
+ failureDeferred.addBoth(gotFailure)
+ return failureDeferred
+ rootDeferred.addCallback(gotRootObj)
+ return rootDeferred
+
+
+ def test_jellyFailure(self):
+ """
+ Test that an exception which is a subclass of L{pb.Error} has more
+ information passed across the network to the calling side.
+ """
+ def failureJelly(fail):
+ fail.trap(JellyError)
+ self.failIf(isinstance(fail.type, str))
+ self.failUnless(isinstance(fail.value, fail.type))
+ return 43
+ return self._testImpl('jelly', 43, failureJelly)
+
+
+ def test_deferredJellyFailure(self):
+ """
+ Test that a Deferred which fails with a L{pb.Error} is treated in
+ the same way as a synchronously raised L{pb.Error}.
+ """
+ def failureDeferredJelly(fail):
+ fail.trap(JellyError)
+ self.failIf(isinstance(fail.type, str))
+ self.failUnless(isinstance(fail.value, fail.type))
+ return 430
+ return self._testImpl('deferredJelly', 430, failureDeferredJelly)
+
+
+ def test_unjellyableFailure(self):
+ """
+ An non-jellyable L{pb.Error} subclass raised by a remote method is
+ turned into a Failure with a type set to the FQPN of the exception
+ type.
+ """
+ def failureUnjellyable(fail):
+ self.assertEqual(
+ fail.type, 'twisted.test.test_pbfailure.SynchronousError')
+ return 431
+ return self._testImpl('synchronousError', 431, failureUnjellyable)
+
+
+ def test_unknownFailure(self):
+ """
+ Test that an exception which is a subclass of L{pb.Error} but not
+ known on the client side has its type set properly.
+ """
+ def failureUnknown(fail):
+ self.assertEqual(
+ fail.type, 'twisted.test.test_pbfailure.UnknownError')
+ return 4310
+ return self._testImpl('unknownError', 4310, failureUnknown)
+
+
+ def test_securityFailure(self):
+ """
+ Test that even if an exception is not explicitly jellyable (by being
+ a L{pb.Jellyable} subclass), as long as it is an L{pb.Error}
+ subclass it receives the same special treatment.
+ """
+ def failureSecurity(fail):
+ fail.trap(SecurityError)
+ self.failIf(isinstance(fail.type, str))
+ self.failUnless(isinstance(fail.value, fail.type))
+ return 4300
+ return self._testImpl('security', 4300, failureSecurity)
+
+
+ def test_deferredSecurity(self):
+ """
+ Test that a Deferred which fails with a L{pb.Error} which is not
+ also a L{pb.Jellyable} is treated in the same way as a synchronously
+ raised exception of the same type.
+ """
+ def failureDeferredSecurity(fail):
+ fail.trap(SecurityError)
+ self.failIf(isinstance(fail.type, str))
+ self.failUnless(isinstance(fail.value, fail.type))
+ return 43000
+ return self._testImpl('deferredSecurity', 43000, failureDeferredSecurity)
+
+
+ def test_noSuchMethodFailure(self):
+ """
+ Test that attempting to call a method which is not defined correctly
+ results in an AttributeError on the calling side.
+ """
+ def failureNoSuch(fail):
+ fail.trap(pb.NoSuchMethod)
+ self.compare(fail.traceback, "Traceback unavailable\n")
+ return 42000
+ return self._testImpl('nosuch', 42000, failureNoSuch, AttributeError)
+
+
+ def test_copiedFailureLogging(self):
+ """
+ Test that a copied failure received from a PB call can be logged
+ locally.
+
+ Note: this test needs some serious help: all it really tests is that
+ log.err(copiedFailure) doesn't raise an exception.
+ """
+ d = self.clientFactory.getRootObject()
+
+ def connected(rootObj):
+ return rootObj.callRemote('synchronousException')
+ d.addCallback(connected)
+
+ def exception(failure):
+ log.err(failure)
+ errs = self.flushLoggedErrors(SynchronousException)
+ self.assertEqual(len(errs), 2)
+ d.addErrback(exception)
+
+ return d
+
+
+ def test_throwExceptionIntoGenerator(self):
+ """
+ L{pb.CopiedFailure.throwExceptionIntoGenerator} will throw a
+ L{RemoteError} into the given paused generator at the point where it
+ last yielded.
+ """
+ original = pb.CopyableFailure(AttributeError("foo"))
+ copy = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
+ exception = []
+ def generatorFunc():
+ try:
+ yield None
+ except pb.RemoteError, exc:
+ exception.append(exc)
+ else:
+ self.fail("RemoteError not raised")
+ gen = generatorFunc()
+ gen.send(None)
+ self.assertRaises(StopIteration, copy.throwExceptionIntoGenerator, gen)
+ self.assertEqual(len(exception), 1)
+ exc = exception[0]
+ self.assertEqual(exc.remoteType, "exceptions.AttributeError")
+ self.assertEqual(exc.args, ("foo",))
+ self.assertEqual(exc.remoteTraceback, 'Traceback unavailable\n')
+
+ if sys.version_info[:2] < (2, 5):
+ test_throwExceptionIntoGenerator.skip = (
+ "throwExceptionIntoGenerator is not supported in Python < 2.5")
+
+
+
+class PBFailureTestUnsafe(PBFailureTest):
+ compare = unittest.TestCase.failIfEquals
+ unsafeTracebacks = 1
+
+
+
+class DummyInvoker(object):
+ """
+ A behaviorless object to be used as the invoker parameter to
+ L{jelly.jelly}.
+ """
+ serializingPerspective = None
+
+
+
+class FailureJellyingTests(unittest.TestCase):
+ """
+ Tests for the interaction of jelly and failures.
+ """
+ def test_unjelliedFailureCheck(self):
+ """
+ An unjellied L{CopyableFailure} has a check method which behaves the
+ same way as the original L{CopyableFailure}'s check method.
+ """
+ original = pb.CopyableFailure(ZeroDivisionError())
+ self.assertIdentical(
+ original.check(ZeroDivisionError), ZeroDivisionError)
+ self.assertIdentical(original.check(ArithmeticError), ArithmeticError)
+ copied = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
+ self.assertIdentical(
+ copied.check(ZeroDivisionError), ZeroDivisionError)
+ self.assertIdentical(copied.check(ArithmeticError), ArithmeticError)
+
+
+ def test_twiceUnjelliedFailureCheck(self):
+ """
+ The object which results from jellying a L{CopyableFailure}, unjellying
+ the result, creating a new L{CopyableFailure} from the result of that,
+ jellying it, and finally unjellying the result of that has a check
+ method which behaves the same way as the original L{CopyableFailure}'s
+ check method.
+ """
+ original = pb.CopyableFailure(ZeroDivisionError())
+ self.assertIdentical(
+ original.check(ZeroDivisionError), ZeroDivisionError)
+ self.assertIdentical(original.check(ArithmeticError), ArithmeticError)
+ copiedOnce = jelly.unjelly(
+ jelly.jelly(original, invoker=DummyInvoker()))
+ derivative = pb.CopyableFailure(copiedOnce)
+ copiedTwice = jelly.unjelly(
+ jelly.jelly(derivative, invoker=DummyInvoker()))
+ self.assertIdentical(
+ copiedTwice.check(ZeroDivisionError), ZeroDivisionError)
+ self.assertIdentical(
+ copiedTwice.check(ArithmeticError), ArithmeticError)
+
+
+ def test_printTracebackIncludesValue(self):
+ """
+ When L{CopiedFailure.printTraceback} is used to print a copied failure
+ which was unjellied from a L{CopyableFailure} with C{unsafeTracebacks}
+ set to C{False}, the string representation of the exception value is
+ included in the output.
+ """
+ original = pb.CopyableFailure(Exception("some reason"))
+ copied = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
+ output = StringIO()
+ copied.printTraceback(output)
+ self.assertEqual(
+ "Traceback from remote host -- Traceback unavailable\n"
+ "exceptions.Exception: some reason\n",
+ output.getvalue())
+
diff --git a/twisted/test/test_pcp.py b/twisted/test/test_pcp.py
new file mode 100644
index 0000000..71de8bb
--- /dev/null
+++ b/twisted/test/test_pcp.py
@@ -0,0 +1,368 @@
+# -*- Python -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+__version__ = '$Revision: 1.5 $'[11:-2]
+
+from StringIO import StringIO
+from twisted.trial import unittest
+from twisted.protocols import pcp
+
+# Goal:
+
+# Take a Protocol instance. Own all outgoing data - anything that
+# would go to p.transport.write. Own all incoming data - anything
+# that comes to p.dataReceived.
+
+# I need:
+# Something with the AbstractFileDescriptor interface.
+# That is:
+# - acts as a Transport
+# - has a method write()
+# - which buffers
+# - acts as a Consumer
+# - has a registerProducer, unRegisterProducer
+# - tells the Producer to back off (pauseProducing) when its buffer is full.
+# - tells the Producer to resumeProducing when its buffer is not so full.
+# - acts as a Producer
+# - calls registerProducer
+# - calls write() on consumers
+# - honors requests to pause/resume producing
+# - honors stopProducing, and passes it along to upstream Producers
+
+
+class DummyTransport:
+ """A dumb transport to wrap around."""
+
+ def __init__(self):
+ self._writes = []
+
+ def write(self, data):
+ self._writes.append(data)
+
+ def getvalue(self):
+ return ''.join(self._writes)
+
+class DummyProducer:
+ resumed = False
+ stopped = False
+ paused = False
+
+ def __init__(self, consumer):
+ self.consumer = consumer
+
+ def resumeProducing(self):
+ self.resumed = True
+ self.paused = False
+
+ def pauseProducing(self):
+ self.paused = True
+
+ def stopProducing(self):
+ self.stopped = True
+
+
+class DummyConsumer(DummyTransport):
+ producer = None
+ finished = False
+ unregistered = True
+
+ def registerProducer(self, producer, streaming):
+ self.producer = (producer, streaming)
+
+ def unregisterProducer(self):
+ self.unregistered = True
+
+ def finish(self):
+ self.finished = True
+
+class TransportInterfaceTest(unittest.TestCase):
+ proxyClass = pcp.BasicProducerConsumerProxy
+
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.transport = self.proxyClass(self.underlying)
+
+ def testWrite(self):
+ self.transport.write("some bytes")
+
+class ConsumerInterfaceTest:
+ """Test ProducerConsumerProxy as a Consumer.
+
+ Normally we have ProducingServer -> ConsumingTransport.
+
+ If I am to go between (Server -> Shaper -> Transport), I have to
+ play the role of Consumer convincingly for the ProducingServer.
+ """
+
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.consumer = self.proxyClass(self.underlying)
+ self.producer = DummyProducer(self.consumer)
+
+ def testRegisterPush(self):
+ self.consumer.registerProducer(self.producer, True)
+ ## Consumer should NOT have called PushProducer.resumeProducing
+ self.failIf(self.producer.resumed)
+
+ ## I'm I'm just a proxy, should I only do resumeProducing when
+ ## I get poked myself?
+ #def testRegisterPull(self):
+ # self.consumer.registerProducer(self.producer, False)
+ # ## Consumer SHOULD have called PushProducer.resumeProducing
+ # self.failUnless(self.producer.resumed)
+
+ def testUnregister(self):
+ self.consumer.registerProducer(self.producer, False)
+ self.consumer.unregisterProducer()
+ # Now when the consumer would ordinarily want more data, it
+ # shouldn't ask producer for it.
+ # The most succinct way to trigger "want more data" is to proxy for
+ # a PullProducer and have someone ask me for data.
+ self.producer.resumed = False
+ self.consumer.resumeProducing()
+ self.failIf(self.producer.resumed)
+
+ def testFinish(self):
+ self.consumer.registerProducer(self.producer, False)
+ self.consumer.finish()
+ # I guess finish should behave like unregister?
+ self.producer.resumed = False
+ self.consumer.resumeProducing()
+ self.failIf(self.producer.resumed)
+
+
+class ProducerInterfaceTest:
+ """Test ProducerConsumerProxy as a Producer.
+
+ Normally we have ProducingServer -> ConsumingTransport.
+
+ If I am to go between (Server -> Shaper -> Transport), I have to
+ play the role of Producer convincingly for the ConsumingTransport.
+ """
+
+ def setUp(self):
+ self.consumer = DummyConsumer()
+ self.producer = self.proxyClass(self.consumer)
+
+ def testRegistersProducer(self):
+ self.assertEqual(self.consumer.producer[0], self.producer)
+
+ def testPause(self):
+ self.producer.pauseProducing()
+ self.producer.write("yakkity yak")
+ self.failIf(self.consumer.getvalue(),
+ "Paused producer should not have sent data.")
+
+ def testResume(self):
+ self.producer.pauseProducing()
+ self.producer.resumeProducing()
+ self.producer.write("yakkity yak")
+ self.assertEqual(self.consumer.getvalue(), "yakkity yak")
+
+ def testResumeNoEmptyWrite(self):
+ self.producer.pauseProducing()
+ self.producer.resumeProducing()
+ self.assertEqual(len(self.consumer._writes), 0,
+ "Resume triggered an empty write.")
+
+ def testResumeBuffer(self):
+ self.producer.pauseProducing()
+ self.producer.write("buffer this")
+ self.producer.resumeProducing()
+ self.assertEqual(self.consumer.getvalue(), "buffer this")
+
+ def testStop(self):
+ self.producer.stopProducing()
+ self.producer.write("yakkity yak")
+ self.failIf(self.consumer.getvalue(),
+ "Stopped producer should not have sent data.")
+
+
+class PCP_ConsumerInterfaceTest(ConsumerInterfaceTest, unittest.TestCase):
+ proxyClass = pcp.BasicProducerConsumerProxy
+
+class PCPII_ConsumerInterfaceTest(ConsumerInterfaceTest, unittest.TestCase):
+ proxyClass = pcp.ProducerConsumerProxy
+
+class PCP_ProducerInterfaceTest(ProducerInterfaceTest, unittest.TestCase):
+ proxyClass = pcp.BasicProducerConsumerProxy
+
+class PCPII_ProducerInterfaceTest(ProducerInterfaceTest, unittest.TestCase):
+ proxyClass = pcp.ProducerConsumerProxy
+
+class ProducerProxyTest(unittest.TestCase):
+ """Producer methods on me should be relayed to the Producer I proxy.
+ """
+ proxyClass = pcp.BasicProducerConsumerProxy
+
+ def setUp(self):
+ self.proxy = self.proxyClass(None)
+ self.parentProducer = DummyProducer(self.proxy)
+ self.proxy.registerProducer(self.parentProducer, True)
+
+ def testStop(self):
+ self.proxy.stopProducing()
+ self.failUnless(self.parentProducer.stopped)
+
+
+class ConsumerProxyTest(unittest.TestCase):
+ """Consumer methods on me should be relayed to the Consumer I proxy.
+ """
+ proxyClass = pcp.BasicProducerConsumerProxy
+
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.consumer = self.proxyClass(self.underlying)
+
+ def testWrite(self):
+ # NOTE: This test only valid for streaming (Push) systems.
+ self.consumer.write("some bytes")
+ self.assertEqual(self.underlying.getvalue(), "some bytes")
+
+ def testFinish(self):
+ self.consumer.finish()
+ self.failUnless(self.underlying.finished)
+
+ def testUnregister(self):
+ self.consumer.unregisterProducer()
+ self.failUnless(self.underlying.unregistered)
+
+
+class PullProducerTest:
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.proxy = self.proxyClass(self.underlying)
+ self.parentProducer = DummyProducer(self.proxy)
+ self.proxy.registerProducer(self.parentProducer, True)
+
+ def testHoldWrites(self):
+ self.proxy.write("hello")
+ # Consumer should get no data before it says resumeProducing.
+ self.failIf(self.underlying.getvalue(),
+ "Pulling Consumer got data before it pulled.")
+
+ def testPull(self):
+ self.proxy.write("hello")
+ self.proxy.resumeProducing()
+ self.assertEqual(self.underlying.getvalue(), "hello")
+
+ def testMergeWrites(self):
+ self.proxy.write("hello ")
+ self.proxy.write("sunshine")
+ self.proxy.resumeProducing()
+ nwrites = len(self.underlying._writes)
+ self.assertEqual(nwrites, 1, "Pull resulted in %d writes instead "
+ "of 1." % (nwrites,))
+ self.assertEqual(self.underlying.getvalue(), "hello sunshine")
+
+
+ def testLateWrite(self):
+ # consumer sends its initial pull before we have data
+ self.proxy.resumeProducing()
+ self.proxy.write("data")
+ # This data should answer that pull request.
+ self.assertEqual(self.underlying.getvalue(), "data")
+
+class PCP_PullProducerTest(PullProducerTest, unittest.TestCase):
+ class proxyClass(pcp.BasicProducerConsumerProxy):
+ iAmStreaming = False
+
+class PCPII_PullProducerTest(PullProducerTest, unittest.TestCase):
+ class proxyClass(pcp.ProducerConsumerProxy):
+ iAmStreaming = False
+
+# Buffering!
+
+class BufferedConsumerTest(unittest.TestCase):
+ """As a consumer, ask the producer to pause after too much data."""
+
+ proxyClass = pcp.ProducerConsumerProxy
+
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.proxy = self.proxyClass(self.underlying)
+ self.proxy.bufferSize = 100
+
+ self.parentProducer = DummyProducer(self.proxy)
+ self.proxy.registerProducer(self.parentProducer, True)
+
+ def testRegisterPull(self):
+ self.proxy.registerProducer(self.parentProducer, False)
+ ## Consumer SHOULD have called PushProducer.resumeProducing
+ self.failUnless(self.parentProducer.resumed)
+
+ def testPauseIntercept(self):
+ self.proxy.pauseProducing()
+ self.failIf(self.parentProducer.paused)
+
+ def testResumeIntercept(self):
+ self.proxy.pauseProducing()
+ self.proxy.resumeProducing()
+ # With a streaming producer, just because the proxy was resumed is
+ # not necessarily a reason to resume the parent producer. The state
+ # of the buffer should decide that.
+ self.failIf(self.parentProducer.resumed)
+
+ def testTriggerPause(self):
+ """Make sure I say \"when.\""""
+
+ # Pause the proxy so data sent to it builds up in its buffer.
+ self.proxy.pauseProducing()
+ self.failIf(self.parentProducer.paused, "don't pause yet")
+ self.proxy.write("x" * 51)
+ self.failIf(self.parentProducer.paused, "don't pause yet")
+ self.proxy.write("x" * 51)
+ self.failUnless(self.parentProducer.paused)
+
+ def testTriggerResume(self):
+ """Make sure I resumeProducing when my buffer empties."""
+ self.proxy.pauseProducing()
+ self.proxy.write("x" * 102)
+ self.failUnless(self.parentProducer.paused, "should be paused")
+ self.proxy.resumeProducing()
+ # Resuming should have emptied my buffer, so I should tell my
+ # parent to resume too.
+ self.failIf(self.parentProducer.paused,
+ "Producer should have resumed.")
+ self.failIf(self.proxy.producerPaused)
+
+class BufferedPullTests(unittest.TestCase):
+ class proxyClass(pcp.ProducerConsumerProxy):
+ iAmStreaming = False
+
+ def _writeSomeData(self, data):
+ pcp.ProducerConsumerProxy._writeSomeData(self, data[:100])
+ return min(len(data), 100)
+
+ def setUp(self):
+ self.underlying = DummyConsumer()
+ self.proxy = self.proxyClass(self.underlying)
+ self.proxy.bufferSize = 100
+
+ self.parentProducer = DummyProducer(self.proxy)
+ self.proxy.registerProducer(self.parentProducer, False)
+
+ def testResumePull(self):
+ # If proxy has no data to send on resumeProducing, it had better pull
+ # some from its PullProducer.
+ self.parentProducer.resumed = False
+ self.proxy.resumeProducing()
+ self.failUnless(self.parentProducer.resumed)
+
+ def testLateWriteBuffering(self):
+ # consumer sends its initial pull before we have data
+ self.proxy.resumeProducing()
+ self.proxy.write("datum" * 21)
+ # This data should answer that pull request.
+ self.assertEqual(self.underlying.getvalue(), "datum" * 20)
+ # but there should be some left over
+ self.assertEqual(self.proxy._buffer, ["datum"])
+
+
+# TODO:
+# test that web request finishing bug (when we weren't proxying
+# unregisterProducer but were proxying finish, web file transfers
+# would hang on the last block.)
+# test what happens if writeSomeBytes decided to write zero bytes.
diff --git a/twisted/test/test_persisted.py b/twisted/test/test_persisted.py
new file mode 100644
index 0000000..4a80791
--- /dev/null
+++ b/twisted/test/test_persisted.py
@@ -0,0 +1,377 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+# System Imports
+import sys
+
+from twisted.trial import unittest
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+# Twisted Imports
+from twisted.persisted import styles, aot, crefutil
+
+
+class VersionTestCase(unittest.TestCase):
+ def testNullVersionUpgrade(self):
+ global NullVersioned
+ class NullVersioned:
+ ok = 0
+ pkcl = pickle.dumps(NullVersioned())
+ class NullVersioned(styles.Versioned):
+ persistenceVersion = 1
+ def upgradeToVersion1(self):
+ self.ok = 1
+ mnv = pickle.loads(pkcl)
+ styles.doUpgrade()
+ assert mnv.ok, "initial upgrade not run!"
+
+ def testVersionUpgrade(self):
+ global MyVersioned
+ class MyVersioned(styles.Versioned):
+ persistenceVersion = 2
+ persistenceForgets = ['garbagedata']
+ v3 = 0
+ v4 = 0
+
+ def __init__(self):
+ self.somedata = 'xxx'
+ self.garbagedata = lambda q: 'cant persist'
+
+ def upgradeToVersion3(self):
+ self.v3 += 1
+
+ def upgradeToVersion4(self):
+ self.v4 += 1
+ mv = MyVersioned()
+ assert not (mv.v3 or mv.v4), "hasn't been upgraded yet"
+ pickl = pickle.dumps(mv)
+ MyVersioned.persistenceVersion = 4
+ obj = pickle.loads(pickl)
+ styles.doUpgrade()
+ assert obj.v3, "didn't do version 3 upgrade"
+ assert obj.v4, "didn't do version 4 upgrade"
+ pickl = pickle.dumps(obj)
+ obj = pickle.loads(pickl)
+ styles.doUpgrade()
+ assert obj.v3 == 1, "upgraded unnecessarily"
+ assert obj.v4 == 1, "upgraded unnecessarily"
+
+ def testNonIdentityHash(self):
+ global ClassWithCustomHash
+ class ClassWithCustomHash(styles.Versioned):
+ def __init__(self, unique, hash):
+ self.unique = unique
+ self.hash = hash
+ def __hash__(self):
+ return self.hash
+
+ v1 = ClassWithCustomHash('v1', 0)
+ v2 = ClassWithCustomHash('v2', 0)
+
+ pkl = pickle.dumps((v1, v2))
+ del v1, v2
+ ClassWithCustomHash.persistenceVersion = 1
+ ClassWithCustomHash.upgradeToVersion1 = lambda self: setattr(self, 'upgraded', True)
+ v1, v2 = pickle.loads(pkl)
+ styles.doUpgrade()
+ self.assertEqual(v1.unique, 'v1')
+ self.assertEqual(v2.unique, 'v2')
+ self.failUnless(v1.upgraded)
+ self.failUnless(v2.upgraded)
+
+ def testUpgradeDeserializesObjectsRequiringUpgrade(self):
+ global ToyClassA, ToyClassB
+ class ToyClassA(styles.Versioned):
+ pass
+ class ToyClassB(styles.Versioned):
+ pass
+ x = ToyClassA()
+ y = ToyClassB()
+ pklA, pklB = pickle.dumps(x), pickle.dumps(y)
+ del x, y
+ ToyClassA.persistenceVersion = 1
+ def upgradeToVersion1(self):
+ self.y = pickle.loads(pklB)
+ styles.doUpgrade()
+ ToyClassA.upgradeToVersion1 = upgradeToVersion1
+ ToyClassB.persistenceVersion = 1
+ ToyClassB.upgradeToVersion1 = lambda self: setattr(self, 'upgraded', True)
+
+ x = pickle.loads(pklA)
+ styles.doUpgrade()
+ self.failUnless(x.y.upgraded)
+
+
+
+class VersionedSubClass(styles.Versioned):
+ pass
+
+
+
+class SecondVersionedSubClass(styles.Versioned):
+ pass
+
+
+
+class VersionedSubSubClass(VersionedSubClass):
+ pass
+
+
+
+class VersionedDiamondSubClass(VersionedSubSubClass, SecondVersionedSubClass):
+ pass
+
+
+
+class AybabtuTests(unittest.TestCase):
+ """
+ L{styles._aybabtu} gets all of classes in the inheritance hierarchy of its
+ argument that are strictly between L{Versioned} and the class itself.
+ """
+
+ def test_aybabtuStrictEmpty(self):
+ """
+ L{styles._aybabtu} of L{Versioned} itself is an empty list.
+ """
+ self.assertEqual(styles._aybabtu(styles.Versioned), [])
+
+
+ def test_aybabtuStrictSubclass(self):
+ """
+ There are no classes I{between} L{VersionedSubClass} and L{Versioned},
+ so L{styles._aybabtu} returns an empty list.
+ """
+ self.assertEqual(styles._aybabtu(VersionedSubClass), [])
+
+
+ def test_aybabtuSubsubclass(self):
+ """
+ With a sub-sub-class of L{Versioned}, L{styles._aybabtu} returns a list
+ containing the intervening subclass.
+ """
+ self.assertEqual(styles._aybabtu(VersionedSubSubClass),
+ [VersionedSubClass])
+
+
+ def test_aybabtuStrict(self):
+ """
+ For a diamond-shaped inheritance graph, L{styles._aybabtu} returns a
+ list containing I{both} intermediate subclasses.
+ """
+ self.assertEqual(
+ styles._aybabtu(VersionedDiamondSubClass),
+ [VersionedSubSubClass, VersionedSubClass, SecondVersionedSubClass])
+
+
+
+class MyEphemeral(styles.Ephemeral):
+
+ def __init__(self, x):
+ self.x = x
+
+
+class EphemeralTestCase(unittest.TestCase):
+
+ def testEphemeral(self):
+ o = MyEphemeral(3)
+ self.assertEqual(o.__class__, MyEphemeral)
+ self.assertEqual(o.x, 3)
+
+ pickl = pickle.dumps(o)
+ o = pickle.loads(pickl)
+
+ self.assertEqual(o.__class__, styles.Ephemeral)
+ self.assert_(not hasattr(o, 'x'))
+
+
+class Pickleable:
+
+ def __init__(self, x):
+ self.x = x
+
+ def getX(self):
+ return self.x
+
+class A:
+ """
+ dummy class
+ """
+ def amethod(self):
+ pass
+
+class B:
+ """
+ dummy class
+ """
+ def bmethod(self):
+ pass
+
+def funktion():
+ pass
+
+class PicklingTestCase(unittest.TestCase):
+ """Test pickling of extra object types."""
+
+ def testModule(self):
+ pickl = pickle.dumps(styles)
+ o = pickle.loads(pickl)
+ self.assertEqual(o, styles)
+
+ def testClassMethod(self):
+ pickl = pickle.dumps(Pickleable.getX)
+ o = pickle.loads(pickl)
+ self.assertEqual(o, Pickleable.getX)
+
+ def testInstanceMethod(self):
+ obj = Pickleable(4)
+ pickl = pickle.dumps(obj.getX)
+ o = pickle.loads(pickl)
+ self.assertEqual(o(), 4)
+ self.assertEqual(type(o), type(obj.getX))
+
+ def testStringIO(self):
+ f = StringIO.StringIO()
+ f.write("abc")
+ pickl = pickle.dumps(f)
+ o = pickle.loads(pickl)
+ self.assertEqual(type(o), type(f))
+ self.assertEqual(f.getvalue(), "abc")
+
+
+class EvilSourceror:
+ def __init__(self, x):
+ self.a = self
+ self.a.b = self
+ self.a.b.c = x
+
+class NonDictState:
+ def __getstate__(self):
+ return self.state
+ def __setstate__(self, state):
+ self.state = state
+
+class AOTTestCase(unittest.TestCase):
+ def testSimpleTypes(self):
+ obj = (1, 2.0, 3j, True, slice(1, 2, 3), 'hello', u'world', sys.maxint + 1, None, Ellipsis)
+ rtObj = aot.unjellyFromSource(aot.jellyToSource(obj))
+ self.assertEqual(obj, rtObj)
+
+ def testMethodSelfIdentity(self):
+ a = A()
+ b = B()
+ a.bmethod = b.bmethod
+ b.a = a
+ im_ = aot.unjellyFromSource(aot.jellyToSource(b)).a.bmethod
+ self.assertEqual(im_.im_class, im_.im_self.__class__)
+
+
+ def test_methodNotSelfIdentity(self):
+ """
+ If a class change after an instance has been created,
+ L{aot.unjellyFromSource} shoud raise a C{TypeError} when trying to
+ unjelly the instance.
+ """
+ a = A()
+ b = B()
+ a.bmethod = b.bmethod
+ b.a = a
+ savedbmethod = B.bmethod
+ del B.bmethod
+ try:
+ self.assertRaises(TypeError, aot.unjellyFromSource,
+ aot.jellyToSource(b))
+ finally:
+ B.bmethod = savedbmethod
+
+
+ def test_unsupportedType(self):
+ """
+ L{aot.jellyToSource} should raise a C{TypeError} when trying to jelly
+ an unknown type.
+ """
+ try:
+ set
+ except:
+ from sets import Set as set
+ self.assertRaises(TypeError, aot.jellyToSource, set())
+
+
+ def testBasicIdentity(self):
+ # Anyone wanting to make this datastructure more complex, and thus this
+ # test more comprehensive, is welcome to do so.
+ aj = aot.AOTJellier().jellyToAO
+ d = {'hello': 'world', "method": aj}
+ l = [1, 2, 3,
+ "he\tllo\n\n\"x world!",
+ u"goodbye \n\t\u1010 world!",
+ 1, 1.0, 100 ** 100l, unittest, aot.AOTJellier, d,
+ funktion
+ ]
+ t = tuple(l)
+ l.append(l)
+ l.append(t)
+ l.append(t)
+ uj = aot.unjellyFromSource(aot.jellyToSource([l, l]))
+ assert uj[0] is uj[1]
+ assert uj[1][0:5] == l[0:5]
+
+
+ def testNonDictState(self):
+ a = NonDictState()
+ a.state = "meringue!"
+ assert aot.unjellyFromSource(aot.jellyToSource(a)).state == a.state
+
+ def testCopyReg(self):
+ s = "foo_bar"
+ sio = StringIO.StringIO()
+ sio.write(s)
+ uj = aot.unjellyFromSource(aot.jellyToSource(sio))
+ # print repr(uj.__dict__)
+ assert uj.getvalue() == s
+
+ def testFunkyReferences(self):
+ o = EvilSourceror(EvilSourceror([]))
+ j1 = aot.jellyToAOT(o)
+ oj = aot.unjellyFromAOT(j1)
+
+ assert oj.a is oj
+ assert oj.a.b is oj.b
+ assert oj.c is not oj.c.c
+
+
+class CrefUtilTestCase(unittest.TestCase):
+ """
+ Tests for L{crefutil}.
+ """
+
+ def test_dictUnknownKey(self):
+ """
+ L{crefutil._DictKeyAndValue} only support keys C{0} and C{1}.
+ """
+ d = crefutil._DictKeyAndValue({})
+ self.assertRaises(RuntimeError, d.__setitem__, 2, 3)
+
+
+ def test_deferSetMultipleTimes(self):
+ """
+ L{crefutil._Defer} can be assigned a key only one time.
+ """
+ d = crefutil._Defer()
+ d[0] = 1
+ self.assertRaises(RuntimeError, d.__setitem__, 0, 1)
+
+
+
+testCases = [VersionTestCase, EphemeralTestCase, PicklingTestCase]
+
diff --git a/twisted/test/test_plugin.py b/twisted/test/test_plugin.py
new file mode 100644
index 0000000..c33ecf1
--- /dev/null
+++ b/twisted/test/test_plugin.py
@@ -0,0 +1,719 @@
+# Copyright (c) 2005 Divmod, Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for Twisted plugin system.
+"""
+
+import sys, errno, os, time
+import compileall
+
+from zope.interface import Interface
+
+from twisted.trial import unittest
+from twisted.python.log import textFromEventDict, addObserver, removeObserver
+from twisted.python.filepath import FilePath
+from twisted.python.util import mergeFunctionMetadata
+
+from twisted import plugin
+
+
+
+class ITestPlugin(Interface):
+ """
+ A plugin for use by the plugin system's unit tests.
+
+ Do not use this.
+ """
+
+
+
+class ITestPlugin2(Interface):
+ """
+ See L{ITestPlugin}.
+ """
+
+
+
+class PluginTestCase(unittest.TestCase):
+ """
+ Tests which verify the behavior of the current, active Twisted plugins
+ directory.
+ """
+
+ def setUp(self):
+ """
+ Save C{sys.path} and C{sys.modules}, and create a package for tests.
+ """
+ self.originalPath = sys.path[:]
+ self.savedModules = sys.modules.copy()
+
+ self.root = FilePath(self.mktemp())
+ self.root.createDirectory()
+ self.package = self.root.child('mypackage')
+ self.package.createDirectory()
+ self.package.child('__init__.py').setContent("")
+
+ FilePath(__file__).sibling('plugin_basic.py'
+ ).copyTo(self.package.child('testplugin.py'))
+
+ self.originalPlugin = "testplugin"
+
+ sys.path.insert(0, self.root.path)
+ import mypackage
+ self.module = mypackage
+
+
+ def tearDown(self):
+ """
+ Restore C{sys.path} and C{sys.modules} to their original values.
+ """
+ sys.path[:] = self.originalPath
+ sys.modules.clear()
+ sys.modules.update(self.savedModules)
+
+
+ def _unimportPythonModule(self, module, deleteSource=False):
+ modulePath = module.__name__.split('.')
+ packageName = '.'.join(modulePath[:-1])
+ moduleName = modulePath[-1]
+
+ delattr(sys.modules[packageName], moduleName)
+ del sys.modules[module.__name__]
+ for ext in ['c', 'o'] + (deleteSource and [''] or []):
+ try:
+ os.remove(module.__file__ + ext)
+ except OSError, ose:
+ if ose.errno != errno.ENOENT:
+ raise
+
+
+ def _clearCache(self):
+ """
+ Remove the plugins B{droping.cache} file.
+ """
+ self.package.child('dropin.cache').remove()
+
+
+ def _withCacheness(meth):
+ """
+ This is a paranoid test wrapper, that calls C{meth} 2 times, clear the
+ cache, and calls it 2 other times. It's supposed to ensure that the
+ plugin system behaves correctly no matter what the state of the cache
+ is.
+ """
+ def wrapped(self):
+ meth(self)
+ meth(self)
+ self._clearCache()
+ meth(self)
+ meth(self)
+ return mergeFunctionMetadata(meth, wrapped)
+
+
+ def test_cache(self):
+ """
+ Check that the cache returned by L{plugin.getCache} hold the plugin
+ B{testplugin}, and that this plugin has the properties we expect:
+ provide L{TestPlugin}, has the good name and description, and can be
+ loaded successfully.
+ """
+ cache = plugin.getCache(self.module)
+
+ dropin = cache[self.originalPlugin]
+ self.assertEqual(dropin.moduleName,
+ 'mypackage.%s' % (self.originalPlugin,))
+ self.assertIn("I'm a test drop-in.", dropin.description)
+
+ # Note, not the preferred way to get a plugin by its interface.
+ p1 = [p for p in dropin.plugins if ITestPlugin in p.provided][0]
+ self.assertIdentical(p1.dropin, dropin)
+ self.assertEqual(p1.name, "TestPlugin")
+
+ # Check the content of the description comes from the plugin module
+ # docstring
+ self.assertEqual(
+ p1.description.strip(),
+ "A plugin used solely for testing purposes.")
+ self.assertEqual(p1.provided, [ITestPlugin, plugin.IPlugin])
+ realPlugin = p1.load()
+ # The plugin should match the class present in sys.modules
+ self.assertIdentical(
+ realPlugin,
+ sys.modules['mypackage.%s' % (self.originalPlugin,)].TestPlugin)
+
+ # And it should also match if we import it classicly
+ import mypackage.testplugin as tp
+ self.assertIdentical(realPlugin, tp.TestPlugin)
+
+ test_cache = _withCacheness(test_cache)
+
+
+ def test_plugins(self):
+ """
+ L{plugin.getPlugins} should return the list of plugins matching the
+ specified interface (here, L{ITestPlugin2}), and these plugins
+ should be instances of classes with a C{test} method, to be sure
+ L{plugin.getPlugins} load classes correctly.
+ """
+ plugins = list(plugin.getPlugins(ITestPlugin2, self.module))
+
+ self.assertEqual(len(plugins), 2)
+
+ names = ['AnotherTestPlugin', 'ThirdTestPlugin']
+ for p in plugins:
+ names.remove(p.__name__)
+ p.test()
+
+ test_plugins = _withCacheness(test_plugins)
+
+
+ def test_detectNewFiles(self):
+ """
+ Check that L{plugin.getPlugins} is able to detect plugins added at
+ runtime.
+ """
+ FilePath(__file__).sibling('plugin_extra1.py'
+ ).copyTo(self.package.child('pluginextra.py'))
+ try:
+ # Check that the current situation is clean
+ self.failIfIn('mypackage.pluginextra', sys.modules)
+ self.failIf(hasattr(sys.modules['mypackage'], 'pluginextra'),
+ "mypackage still has pluginextra module")
+
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+
+ # We should find 2 plugins: the one in testplugin, and the one in
+ # pluginextra
+ self.assertEqual(len(plgs), 2)
+
+ names = ['TestPlugin', 'FourthTestPlugin']
+ for p in plgs:
+ names.remove(p.__name__)
+ p.test1()
+ finally:
+ self._unimportPythonModule(
+ sys.modules['mypackage.pluginextra'],
+ True)
+
+ test_detectNewFiles = _withCacheness(test_detectNewFiles)
+
+
+ def test_detectFilesChanged(self):
+ """
+ Check that if the content of a plugin change, L{plugin.getPlugins} is
+ able to detect the new plugins added.
+ """
+ FilePath(__file__).sibling('plugin_extra1.py'
+ ).copyTo(self.package.child('pluginextra.py'))
+ try:
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+ # Sanity check
+ self.assertEqual(len(plgs), 2)
+
+ FilePath(__file__).sibling('plugin_extra2.py'
+ ).copyTo(self.package.child('pluginextra.py'))
+
+ # Fake out Python.
+ self._unimportPythonModule(sys.modules['mypackage.pluginextra'])
+
+ # Make sure additions are noticed
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+
+ self.assertEqual(len(plgs), 3)
+
+ names = ['TestPlugin', 'FourthTestPlugin', 'FifthTestPlugin']
+ for p in plgs:
+ names.remove(p.__name__)
+ p.test1()
+ finally:
+ self._unimportPythonModule(
+ sys.modules['mypackage.pluginextra'],
+ True)
+
+ test_detectFilesChanged = _withCacheness(test_detectFilesChanged)
+
+
+ def test_detectFilesRemoved(self):
+ """
+ Check that when a dropin file is removed, L{plugin.getPlugins} doesn't
+ return it anymore.
+ """
+ FilePath(__file__).sibling('plugin_extra1.py'
+ ).copyTo(self.package.child('pluginextra.py'))
+ try:
+ # Generate a cache with pluginextra in it.
+ list(plugin.getPlugins(ITestPlugin, self.module))
+
+ finally:
+ self._unimportPythonModule(
+ sys.modules['mypackage.pluginextra'],
+ True)
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+ self.assertEqual(1, len(plgs))
+
+ test_detectFilesRemoved = _withCacheness(test_detectFilesRemoved)
+
+
+ def test_nonexistentPathEntry(self):
+ """
+ Test that getCache skips over any entries in a plugin package's
+ C{__path__} which do not exist.
+ """
+ path = self.mktemp()
+ self.failIf(os.path.exists(path))
+ # Add the test directory to the plugins path
+ self.module.__path__.append(path)
+ try:
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+ self.assertEqual(len(plgs), 1)
+ finally:
+ self.module.__path__.remove(path)
+
+ test_nonexistentPathEntry = _withCacheness(test_nonexistentPathEntry)
+
+
+ def test_nonDirectoryChildEntry(self):
+ """
+ Test that getCache skips over any entries in a plugin package's
+ C{__path__} which refer to children of paths which are not directories.
+ """
+ path = FilePath(self.mktemp())
+ self.failIf(path.exists())
+ path.touch()
+ child = path.child("test_package").path
+ self.module.__path__.append(child)
+ try:
+ plgs = list(plugin.getPlugins(ITestPlugin, self.module))
+ self.assertEqual(len(plgs), 1)
+ finally:
+ self.module.__path__.remove(child)
+
+ test_nonDirectoryChildEntry = _withCacheness(test_nonDirectoryChildEntry)
+
+
+ def test_deployedMode(self):
+ """
+ The C{dropin.cache} file may not be writable: the cache should still be
+ attainable, but an error should be logged to show that the cache
+ couldn't be updated.
+ """
+ # Generate the cache
+ plugin.getCache(self.module)
+
+ cachepath = self.package.child('dropin.cache')
+
+ # Add a new plugin
+ FilePath(__file__).sibling('plugin_extra1.py'
+ ).copyTo(self.package.child('pluginextra.py'))
+
+ os.chmod(self.package.path, 0500)
+ # Change the right of dropin.cache too for windows
+ os.chmod(cachepath.path, 0400)
+ self.addCleanup(os.chmod, self.package.path, 0700)
+ self.addCleanup(os.chmod, cachepath.path, 0700)
+
+ # Start observing log events to see the warning
+ events = []
+ addObserver(events.append)
+ self.addCleanup(removeObserver, events.append)
+
+ cache = plugin.getCache(self.module)
+ # The new plugin should be reported
+ self.assertIn('pluginextra', cache)
+ self.assertIn(self.originalPlugin, cache)
+
+ # Make sure something was logged about the cache.
+ expected = "Unable to write to plugin cache %s: error number %d" % (
+ cachepath.path, errno.EPERM)
+ for event in events:
+ if expected in textFromEventDict(event):
+ break
+ else:
+ self.fail(
+ "Did not observe unwriteable cache warning in log "
+ "events: %r" % (events,))
+
+
+
+# This is something like the Twisted plugins file.
+pluginInitFile = """
+from twisted.plugin import pluginPackagePaths
+__path__.extend(pluginPackagePaths(__name__))
+__all__ = []
+"""
+
+def pluginFileContents(name):
+ return (
+ "from zope.interface import classProvides\n"
+ "from twisted.plugin import IPlugin\n"
+ "from twisted.test.test_plugin import ITestPlugin\n"
+ "\n"
+ "class %s(object):\n"
+ " classProvides(IPlugin, ITestPlugin)\n") % (name,)
+
+
+def _createPluginDummy(entrypath, pluginContent, real, pluginModule):
+ """
+ Create a plugindummy package.
+ """
+ entrypath.createDirectory()
+ pkg = entrypath.child('plugindummy')
+ pkg.createDirectory()
+ if real:
+ pkg.child('__init__.py').setContent('')
+ plugs = pkg.child('plugins')
+ plugs.createDirectory()
+ if real:
+ plugs.child('__init__.py').setContent(pluginInitFile)
+ plugs.child(pluginModule + '.py').setContent(pluginContent)
+ return plugs
+
+
+
+class DeveloperSetupTests(unittest.TestCase):
+ """
+ These tests verify things about the plugin system without actually
+ interacting with the deployed 'twisted.plugins' package, instead creating a
+ temporary package.
+ """
+
+ def setUp(self):
+ """
+ Create a complex environment with multiple entries on sys.path, akin to
+ a developer's environment who has a development (trunk) checkout of
+ Twisted, a system installed version of Twisted (for their operating
+ system's tools) and a project which provides Twisted plugins.
+ """
+ self.savedPath = sys.path[:]
+ self.savedModules = sys.modules.copy()
+ self.fakeRoot = FilePath(self.mktemp())
+ self.fakeRoot.createDirectory()
+ self.systemPath = self.fakeRoot.child('system_path')
+ self.devPath = self.fakeRoot.child('development_path')
+ self.appPath = self.fakeRoot.child('application_path')
+ self.systemPackage = _createPluginDummy(
+ self.systemPath, pluginFileContents('system'),
+ True, 'plugindummy_builtin')
+ self.devPackage = _createPluginDummy(
+ self.devPath, pluginFileContents('dev'),
+ True, 'plugindummy_builtin')
+ self.appPackage = _createPluginDummy(
+ self.appPath, pluginFileContents('app'),
+ False, 'plugindummy_app')
+
+ # Now we're going to do the system installation.
+ sys.path.extend([x.path for x in [self.systemPath,
+ self.appPath]])
+ # Run all the way through the plugins list to cause the
+ # L{plugin.getPlugins} generator to write cache files for the system
+ # installation.
+ self.getAllPlugins()
+ self.sysplug = self.systemPath.child('plugindummy').child('plugins')
+ self.syscache = self.sysplug.child('dropin.cache')
+ # Make sure there's a nice big difference in modification times so that
+ # we won't re-build the system cache.
+ now = time.time()
+ os.utime(
+ self.sysplug.child('plugindummy_builtin.py').path,
+ (now - 5000,) * 2)
+ os.utime(self.syscache.path, (now - 2000,) * 2)
+ # For extra realism, let's make sure that the system path is no longer
+ # writable.
+ self.lockSystem()
+ self.resetEnvironment()
+
+
+ def lockSystem(self):
+ """
+ Lock the system directories, as if they were unwritable by this user.
+ """
+ os.chmod(self.sysplug.path, 0555)
+ os.chmod(self.syscache.path, 0555)
+
+
+ def unlockSystem(self):
+ """
+ Unlock the system directories, as if they were writable by this user.
+ """
+ os.chmod(self.sysplug.path, 0777)
+ os.chmod(self.syscache.path, 0777)
+
+
+ def getAllPlugins(self):
+ """
+ Get all the plugins loadable from our dummy package, and return their
+ short names.
+ """
+ # Import the module we just added to our path. (Local scope because
+ # this package doesn't exist outside of this test.)
+ import plugindummy.plugins
+ x = list(plugin.getPlugins(ITestPlugin, plugindummy.plugins))
+ return [plug.__name__ for plug in x]
+
+
+ def resetEnvironment(self):
+ """
+ Change the environment to what it should be just as the test is
+ starting.
+ """
+ self.unsetEnvironment()
+ sys.path.extend([x.path for x in [self.devPath,
+ self.systemPath,
+ self.appPath]])
+
+ def unsetEnvironment(self):
+ """
+ Change the Python environment back to what it was before the test was
+ started.
+ """
+ sys.modules.clear()
+ sys.modules.update(self.savedModules)
+ sys.path[:] = self.savedPath
+
+
+ def tearDown(self):
+ """
+ Reset the Python environment to what it was before this test ran, and
+ restore permissions on files which were marked read-only so that the
+ directory may be cleanly cleaned up.
+ """
+ self.unsetEnvironment()
+ # Normally we wouldn't "clean up" the filesystem like this (leaving
+ # things for post-test inspection), but if we left the permissions the
+ # way they were, we'd be leaving files around that the buildbots
+ # couldn't delete, and that would be bad.
+ self.unlockSystem()
+
+
+ def test_developmentPluginAvailability(self):
+ """
+ Plugins added in the development path should be loadable, even when
+ the (now non-importable) system path contains its own idea of the
+ list of plugins for a package. Inversely, plugins added in the
+ system path should not be available.
+ """
+ # Run 3 times: uncached, cached, and then cached again to make sure we
+ # didn't overwrite / corrupt the cache on the cached try.
+ for x in range(3):
+ names = self.getAllPlugins()
+ names.sort()
+ self.assertEqual(names, ['app', 'dev'])
+
+
+ def test_freshPyReplacesStalePyc(self):
+ """
+ Verify that if a stale .pyc file on the PYTHONPATH is replaced by a
+ fresh .py file, the plugins in the new .py are picked up rather than
+ the stale .pyc, even if the .pyc is still around.
+ """
+ mypath = self.appPackage.child("stale.py")
+ mypath.setContent(pluginFileContents('one'))
+ # Make it super stale
+ x = time.time() - 1000
+ os.utime(mypath.path, (x, x))
+ pyc = mypath.sibling('stale.pyc')
+ # compile it
+ compileall.compile_dir(self.appPackage.path, quiet=1)
+ os.utime(pyc.path, (x, x))
+ # Eliminate the other option.
+ mypath.remove()
+ # Make sure it's the .pyc path getting cached.
+ self.resetEnvironment()
+ # Sanity check.
+ self.assertIn('one', self.getAllPlugins())
+ self.failIfIn('two', self.getAllPlugins())
+ self.resetEnvironment()
+ mypath.setContent(pluginFileContents('two'))
+ self.failIfIn('one', self.getAllPlugins())
+ self.assertIn('two', self.getAllPlugins())
+
+
+ def test_newPluginsOnReadOnlyPath(self):
+ """
+ Verify that a failure to write the dropin.cache file on a read-only
+ path will not affect the list of plugins returned.
+
+ Note: this test should pass on both Linux and Windows, but may not
+ provide useful coverage on Windows due to the different meaning of
+ "read-only directory".
+ """
+ self.unlockSystem()
+ self.sysplug.child('newstuff.py').setContent(pluginFileContents('one'))
+ self.lockSystem()
+
+ # Take the developer path out, so that the system plugins are actually
+ # examined.
+ sys.path.remove(self.devPath.path)
+
+ # Start observing log events to see the warning
+ events = []
+ addObserver(events.append)
+ self.addCleanup(removeObserver, events.append)
+
+ self.assertIn('one', self.getAllPlugins())
+
+ # Make sure something was logged about the cache.
+ expected = "Unable to write to plugin cache %s: error number %d" % (
+ self.syscache.path, errno.EPERM)
+ for event in events:
+ if expected in textFromEventDict(event):
+ break
+ else:
+ self.fail(
+ "Did not observe unwriteable cache warning in log "
+ "events: %r" % (events,))
+
+
+
+class AdjacentPackageTests(unittest.TestCase):
+ """
+ Tests for the behavior of the plugin system when there are multiple
+ installed copies of the package containing the plugins being loaded.
+ """
+
+ def setUp(self):
+ """
+ Save the elements of C{sys.path} and the items of C{sys.modules}.
+ """
+ self.originalPath = sys.path[:]
+ self.savedModules = sys.modules.copy()
+
+
+ def tearDown(self):
+ """
+ Restore C{sys.path} and C{sys.modules} to their original values.
+ """
+ sys.path[:] = self.originalPath
+ sys.modules.clear()
+ sys.modules.update(self.savedModules)
+
+
+ def createDummyPackage(self, root, name, pluginName):
+ """
+ Create a directory containing a Python package named I{dummy} with a
+ I{plugins} subpackage.
+
+ @type root: L{FilePath}
+ @param root: The directory in which to create the hierarchy.
+
+ @type name: C{str}
+ @param name: The name of the directory to create which will contain
+ the package.
+
+ @type pluginName: C{str}
+ @param pluginName: The name of a module to create in the
+ I{dummy.plugins} package.
+
+ @rtype: L{FilePath}
+ @return: The directory which was created to contain the I{dummy}
+ package.
+ """
+ directory = root.child(name)
+ package = directory.child('dummy')
+ package.makedirs()
+ package.child('__init__.py').setContent('')
+ plugins = package.child('plugins')
+ plugins.makedirs()
+ plugins.child('__init__.py').setContent(pluginInitFile)
+ pluginModule = plugins.child(pluginName + '.py')
+ pluginModule.setContent(pluginFileContents(name))
+ return directory
+
+
+ def test_hiddenPackageSamePluginModuleNameObscured(self):
+ """
+ Only plugins from the first package in sys.path should be returned by
+ getPlugins in the case where there are two Python packages by the same
+ name installed, each with a plugin module by a single name.
+ """
+ root = FilePath(self.mktemp())
+ root.makedirs()
+
+ firstDirectory = self.createDummyPackage(root, 'first', 'someplugin')
+ secondDirectory = self.createDummyPackage(root, 'second', 'someplugin')
+
+ sys.path.append(firstDirectory.path)
+ sys.path.append(secondDirectory.path)
+
+ import dummy.plugins
+
+ plugins = list(plugin.getPlugins(ITestPlugin, dummy.plugins))
+ self.assertEqual(['first'], [p.__name__ for p in plugins])
+
+
+ def test_hiddenPackageDifferentPluginModuleNameObscured(self):
+ """
+ Plugins from the first package in sys.path should be returned by
+ getPlugins in the case where there are two Python packages by the same
+ name installed, each with a plugin module by a different name.
+ """
+ root = FilePath(self.mktemp())
+ root.makedirs()
+
+ firstDirectory = self.createDummyPackage(root, 'first', 'thisplugin')
+ secondDirectory = self.createDummyPackage(root, 'second', 'thatplugin')
+
+ sys.path.append(firstDirectory.path)
+ sys.path.append(secondDirectory.path)
+
+ import dummy.plugins
+
+ plugins = list(plugin.getPlugins(ITestPlugin, dummy.plugins))
+ self.assertEqual(['first'], [p.__name__ for p in plugins])
+
+
+
+class PackagePathTests(unittest.TestCase):
+ """
+ Tests for L{plugin.pluginPackagePaths} which constructs search paths for
+ plugin packages.
+ """
+
+ def setUp(self):
+ """
+ Save the elements of C{sys.path}.
+ """
+ self.originalPath = sys.path[:]
+
+
+ def tearDown(self):
+ """
+ Restore C{sys.path} to its original value.
+ """
+ sys.path[:] = self.originalPath
+
+
+ def test_pluginDirectories(self):
+ """
+ L{plugin.pluginPackagePaths} should return a list containing each
+ directory in C{sys.path} with a suffix based on the supplied package
+ name.
+ """
+ foo = FilePath('foo')
+ bar = FilePath('bar')
+ sys.path = [foo.path, bar.path]
+ self.assertEqual(
+ plugin.pluginPackagePaths('dummy.plugins'),
+ [foo.child('dummy').child('plugins').path,
+ bar.child('dummy').child('plugins').path])
+
+
+ def test_pluginPackagesExcluded(self):
+ """
+ L{plugin.pluginPackagePaths} should exclude directories which are
+ Python packages. The only allowed plugin package (the only one
+ associated with a I{dummy} package which Python will allow to be
+ imported) will already be known to the caller of
+ L{plugin.pluginPackagePaths} and will most commonly already be in
+ the C{__path__} they are about to mutate.
+ """
+ root = FilePath(self.mktemp())
+ foo = root.child('foo').child('dummy').child('plugins')
+ foo.makedirs()
+ foo.child('__init__.py').setContent('')
+ sys.path = [root.child('foo').path, root.child('bar').path]
+ self.assertEqual(
+ plugin.pluginPackagePaths('dummy.plugins'),
+ [root.child('bar').child('dummy').child('plugins').path])
diff --git a/twisted/test/test_policies.py b/twisted/test/test_policies.py
new file mode 100644
index 0000000..3cdf096
--- /dev/null
+++ b/twisted/test/test_policies.py
@@ -0,0 +1,736 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test code for policies.
+"""
+
+from zope.interface import Interface, implements, implementedBy
+
+from StringIO import StringIO
+
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport
+from twisted.test.proto_helpers import StringTransportWithDisconnection
+
+from twisted.internet import protocol, reactor, address, defer, task
+from twisted.protocols import policies
+
+
+
+class SimpleProtocol(protocol.Protocol):
+
+ connected = disconnected = 0
+ buffer = ""
+
+ def __init__(self):
+ self.dConnected = defer.Deferred()
+ self.dDisconnected = defer.Deferred()
+
+ def connectionMade(self):
+ self.connected = 1
+ self.dConnected.callback('')
+
+ def connectionLost(self, reason):
+ self.disconnected = 1
+ self.dDisconnected.callback('')
+
+ def dataReceived(self, data):
+ self.buffer += data
+
+
+
+class SillyFactory(protocol.ClientFactory):
+
+ def __init__(self, p):
+ self.p = p
+
+ def buildProtocol(self, addr):
+ return self.p
+
+
+class EchoProtocol(protocol.Protocol):
+ paused = False
+
+ def pauseProducing(self):
+ self.paused = True
+
+ def resumeProducing(self):
+ self.paused = False
+
+ def stopProducing(self):
+ pass
+
+ def dataReceived(self, data):
+ self.transport.write(data)
+
+
+
+class Server(protocol.ServerFactory):
+ """
+ A simple server factory using L{EchoProtocol}.
+ """
+ protocol = EchoProtocol
+
+
+
+class TestableThrottlingFactory(policies.ThrottlingFactory):
+ """
+ L{policies.ThrottlingFactory} using a L{task.Clock} for tests.
+ """
+
+ def __init__(self, clock, *args, **kwargs):
+ """
+ @param clock: object providing a callLater method that can be used
+ for tests.
+ @type clock: C{task.Clock} or alike.
+ """
+ policies.ThrottlingFactory.__init__(self, *args, **kwargs)
+ self.clock = clock
+
+
+ def callLater(self, period, func):
+ """
+ Forward to the testable clock.
+ """
+ return self.clock.callLater(period, func)
+
+
+
+class TestableTimeoutFactory(policies.TimeoutFactory):
+ """
+ L{policies.TimeoutFactory} using a L{task.Clock} for tests.
+ """
+
+ def __init__(self, clock, *args, **kwargs):
+ """
+ @param clock: object providing a callLater method that can be used
+ for tests.
+ @type clock: C{task.Clock} or alike.
+ """
+ policies.TimeoutFactory.__init__(self, *args, **kwargs)
+ self.clock = clock
+
+
+ def callLater(self, period, func):
+ """
+ Forward to the testable clock.
+ """
+ return self.clock.callLater(period, func)
+
+
+
+class WrapperTestCase(unittest.TestCase):
+ """
+ Tests for L{WrappingFactory} and L{ProtocolWrapper}.
+ """
+ def test_protocolFactoryAttribute(self):
+ """
+ Make sure protocol.factory is the wrapped factory, not the wrapping
+ factory.
+ """
+ f = Server()
+ wf = policies.WrappingFactory(f)
+ p = wf.buildProtocol(address.IPv4Address('TCP', '127.0.0.1', 35))
+ self.assertIdentical(p.wrappedProtocol.factory, f)
+
+
+ def test_transportInterfaces(self):
+ """
+ The transport wrapper passed to the wrapped protocol's
+ C{makeConnection} provides the same interfaces as are provided by the
+ original transport.
+ """
+ class IStubTransport(Interface):
+ pass
+
+ class StubTransport:
+ implements(IStubTransport)
+
+ # Looking up what ProtocolWrapper implements also mutates the class.
+ # It adds __implemented__ and __providedBy__ attributes to it. These
+ # prevent __getattr__ from causing the IStubTransport.providedBy call
+ # below from returning True. If, by accident, nothing else causes
+ # these attributes to be added to ProtocolWrapper, the test will pass,
+ # but the interface will only be provided until something does trigger
+ # their addition. So we just trigger it right now to be sure.
+ implementedBy(policies.ProtocolWrapper)
+
+ proto = protocol.Protocol()
+ wrapper = policies.ProtocolWrapper(policies.WrappingFactory(None), proto)
+
+ wrapper.makeConnection(StubTransport())
+ self.assertTrue(IStubTransport.providedBy(proto.transport))
+
+
+ def test_factoryLogPrefix(self):
+ """
+ L{WrappingFactory.logPrefix} is customized to mention both the original
+ factory and the wrapping factory.
+ """
+ server = Server()
+ factory = policies.WrappingFactory(server)
+ self.assertEqual("Server (WrappingFactory)", factory.logPrefix())
+
+
+ def test_factoryLogPrefixFallback(self):
+ """
+ If the wrapped factory doesn't have a L{logPrefix} method,
+ L{WrappingFactory.logPrefix} falls back to the factory class name.
+ """
+ class NoFactory(object):
+ pass
+
+ server = NoFactory()
+ factory = policies.WrappingFactory(server)
+ self.assertEqual("NoFactory (WrappingFactory)", factory.logPrefix())
+
+
+ def test_protocolLogPrefix(self):
+ """
+ L{ProtocolWrapper.logPrefix} is customized to mention both the original
+ protocol and the wrapper.
+ """
+ server = Server()
+ factory = policies.WrappingFactory(server)
+ protocol = factory.buildProtocol(
+ address.IPv4Address('TCP', '127.0.0.1', 35))
+ self.assertEqual("EchoProtocol (ProtocolWrapper)",
+ protocol.logPrefix())
+
+
+ def test_protocolLogPrefixFallback(self):
+ """
+ If the wrapped protocol doesn't have a L{logPrefix} method,
+ L{ProtocolWrapper.logPrefix} falls back to the protocol class name.
+ """
+ class NoProtocol(object):
+ pass
+
+ server = Server()
+ server.protocol = NoProtocol
+ factory = policies.WrappingFactory(server)
+ protocol = factory.buildProtocol(
+ address.IPv4Address('TCP', '127.0.0.1', 35))
+ self.assertEqual("NoProtocol (ProtocolWrapper)",
+ protocol.logPrefix())
+
+
+
+class WrappingFactory(policies.WrappingFactory):
+ protocol = lambda s, f, p: p
+
+ def startFactory(self):
+ policies.WrappingFactory.startFactory(self)
+ self.deferred.callback(None)
+
+
+
+class ThrottlingTestCase(unittest.TestCase):
+ """
+ Tests for L{policies.ThrottlingFactory}.
+ """
+
+ def test_limit(self):
+ """
+ Full test using a custom server limiting number of connections.
+ """
+ server = Server()
+ c1, c2, c3, c4 = [SimpleProtocol() for i in range(4)]
+ tServer = policies.ThrottlingFactory(server, 2)
+ wrapTServer = WrappingFactory(tServer)
+ wrapTServer.deferred = defer.Deferred()
+
+ # Start listening
+ p = reactor.listenTCP(0, wrapTServer, interface="127.0.0.1")
+ n = p.getHost().port
+
+ def _connect123(results):
+ reactor.connectTCP("127.0.0.1", n, SillyFactory(c1))
+ c1.dConnected.addCallback(
+ lambda r: reactor.connectTCP("127.0.0.1", n, SillyFactory(c2)))
+ c2.dConnected.addCallback(
+ lambda r: reactor.connectTCP("127.0.0.1", n, SillyFactory(c3)))
+ return c3.dDisconnected
+
+ def _check123(results):
+ self.assertEqual([c.connected for c in c1, c2, c3], [1, 1, 1])
+ self.assertEqual([c.disconnected for c in c1, c2, c3], [0, 0, 1])
+ self.assertEqual(len(tServer.protocols.keys()), 2)
+ return results
+
+ def _lose1(results):
+ # disconnect one protocol and now another should be able to connect
+ c1.transport.loseConnection()
+ return c1.dDisconnected
+
+ def _connect4(results):
+ reactor.connectTCP("127.0.0.1", n, SillyFactory(c4))
+ return c4.dConnected
+
+ def _check4(results):
+ self.assertEqual(c4.connected, 1)
+ self.assertEqual(c4.disconnected, 0)
+ return results
+
+ def _cleanup(results):
+ for c in c2, c4:
+ c.transport.loseConnection()
+ return defer.DeferredList([
+ defer.maybeDeferred(p.stopListening),
+ c2.dDisconnected,
+ c4.dDisconnected])
+
+ wrapTServer.deferred.addCallback(_connect123)
+ wrapTServer.deferred.addCallback(_check123)
+ wrapTServer.deferred.addCallback(_lose1)
+ wrapTServer.deferred.addCallback(_connect4)
+ wrapTServer.deferred.addCallback(_check4)
+ wrapTServer.deferred.addCallback(_cleanup)
+ return wrapTServer.deferred
+
+
+ def test_writeLimit(self):
+ """
+ Check the writeLimit parameter: write data, and check for the pause
+ status.
+ """
+ server = Server()
+ tServer = TestableThrottlingFactory(task.Clock(), server, writeLimit=10)
+ port = tServer.buildProtocol(address.IPv4Address('TCP', '127.0.0.1', 0))
+ tr = StringTransportWithDisconnection()
+ tr.protocol = port
+ port.makeConnection(tr)
+ port.producer = port.wrappedProtocol
+
+ port.dataReceived("0123456789")
+ port.dataReceived("abcdefghij")
+ self.assertEqual(tr.value(), "0123456789abcdefghij")
+ self.assertEqual(tServer.writtenThisSecond, 20)
+ self.assertFalse(port.wrappedProtocol.paused)
+
+ # at this point server should've written 20 bytes, 10 bytes
+ # above the limit so writing should be paused around 1 second
+ # from 'now', and resumed a second after that
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.writtenThisSecond, 0)
+ self.assertTrue(port.wrappedProtocol.paused)
+
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.writtenThisSecond, 0)
+ self.assertFalse(port.wrappedProtocol.paused)
+
+
+ def test_readLimit(self):
+ """
+ Check the readLimit parameter: read data and check for the pause
+ status.
+ """
+ server = Server()
+ tServer = TestableThrottlingFactory(task.Clock(), server, readLimit=10)
+ port = tServer.buildProtocol(address.IPv4Address('TCP', '127.0.0.1', 0))
+ tr = StringTransportWithDisconnection()
+ tr.protocol = port
+ port.makeConnection(tr)
+
+ port.dataReceived("0123456789")
+ port.dataReceived("abcdefghij")
+ self.assertEqual(tr.value(), "0123456789abcdefghij")
+ self.assertEqual(tServer.readThisSecond, 20)
+
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.readThisSecond, 0)
+ self.assertEqual(tr.producerState, 'paused')
+
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.readThisSecond, 0)
+ self.assertEqual(tr.producerState, 'producing')
+
+ tr.clear()
+ port.dataReceived("0123456789")
+ port.dataReceived("abcdefghij")
+ self.assertEqual(tr.value(), "0123456789abcdefghij")
+ self.assertEqual(tServer.readThisSecond, 20)
+
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.readThisSecond, 0)
+ self.assertEqual(tr.producerState, 'paused')
+
+ tServer.clock.advance(1.05)
+ self.assertEqual(tServer.readThisSecond, 0)
+ self.assertEqual(tr.producerState, 'producing')
+
+
+
+class TimeoutTestCase(unittest.TestCase):
+ """
+ Tests for L{policies.TimeoutFactory}.
+ """
+
+ def setUp(self):
+ """
+ Create a testable, deterministic clock, and a set of
+ server factory/protocol/transport.
+ """
+ self.clock = task.Clock()
+ wrappedFactory = protocol.ServerFactory()
+ wrappedFactory.protocol = SimpleProtocol
+ self.factory = TestableTimeoutFactory(self.clock, wrappedFactory, 3)
+ self.proto = self.factory.buildProtocol(
+ address.IPv4Address('TCP', '127.0.0.1', 12345))
+ self.transport = StringTransportWithDisconnection()
+ self.transport.protocol = self.proto
+ self.proto.makeConnection(self.transport)
+
+
+ def test_timeout(self):
+ """
+ Make sure that when a TimeoutFactory accepts a connection, it will
+ time out that connection if no data is read or written within the
+ timeout period.
+ """
+ # Let almost 3 time units pass
+ self.clock.pump([0.0, 0.5, 1.0, 1.0, 0.4])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Now let the timer elapse
+ self.clock.pump([0.0, 0.2])
+ self.failUnless(self.proto.wrappedProtocol.disconnected)
+
+
+ def test_sendAvoidsTimeout(self):
+ """
+ Make sure that writing data to a transport from a protocol
+ constructed by a TimeoutFactory resets the timeout countdown.
+ """
+ # Let half the countdown period elapse
+ self.clock.pump([0.0, 0.5, 1.0])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Send some data (self.proto is the /real/ proto's transport, so this
+ # is the write that gets called)
+ self.proto.write('bytes bytes bytes')
+
+ # More time passes, putting us past the original timeout
+ self.clock.pump([0.0, 1.0, 1.0])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Make sure writeSequence delays timeout as well
+ self.proto.writeSequence(['bytes'] * 3)
+
+ # Tick tock
+ self.clock.pump([0.0, 1.0, 1.0])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Don't write anything more, just let the timeout expire
+ self.clock.pump([0.0, 2.0])
+ self.failUnless(self.proto.wrappedProtocol.disconnected)
+
+
+ def test_receiveAvoidsTimeout(self):
+ """
+ Make sure that receiving data also resets the timeout countdown.
+ """
+ # Let half the countdown period elapse
+ self.clock.pump([0.0, 1.0, 0.5])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Some bytes arrive, they should reset the counter
+ self.proto.dataReceived('bytes bytes bytes')
+
+ # We pass the original timeout
+ self.clock.pump([0.0, 1.0, 1.0])
+ self.failIf(self.proto.wrappedProtocol.disconnected)
+
+ # Nothing more arrives though, the new timeout deadline is passed,
+ # the connection should be dropped.
+ self.clock.pump([0.0, 1.0, 1.0])
+ self.failUnless(self.proto.wrappedProtocol.disconnected)
+
+
+
+class TimeoutTester(protocol.Protocol, policies.TimeoutMixin):
+ """
+ A testable protocol with timeout facility.
+
+ @ivar timedOut: set to C{True} if a timeout has been detected.
+ @type timedOut: C{bool}
+ """
+ timeOut = 3
+ timedOut = False
+
+ def __init__(self, clock):
+ """
+ Initialize the protocol with a C{task.Clock} object.
+ """
+ self.clock = clock
+
+
+ def connectionMade(self):
+ """
+ Upon connection, set the timeout.
+ """
+ self.setTimeout(self.timeOut)
+
+
+ def dataReceived(self, data):
+ """
+ Reset the timeout on data.
+ """
+ self.resetTimeout()
+ protocol.Protocol.dataReceived(self, data)
+
+
+ def connectionLost(self, reason=None):
+ """
+ On connection lost, cancel all timeout operations.
+ """
+ self.setTimeout(None)
+
+
+ def timeoutConnection(self):
+ """
+ Flags the timedOut variable to indicate the timeout of the connection.
+ """
+ self.timedOut = True
+
+
+ def callLater(self, timeout, func, *args, **kwargs):
+ """
+ Override callLater to use the deterministic clock.
+ """
+ return self.clock.callLater(timeout, func, *args, **kwargs)
+
+
+
+class TestTimeout(unittest.TestCase):
+ """
+ Tests for L{policies.TimeoutMixin}.
+ """
+
+ def setUp(self):
+ """
+ Create a testable, deterministic clock and a C{TimeoutTester} instance.
+ """
+ self.clock = task.Clock()
+ self.proto = TimeoutTester(self.clock)
+
+
+ def test_overriddenCallLater(self):
+ """
+ Test that the callLater of the clock is used instead of
+ C{reactor.callLater}.
+ """
+ self.proto.setTimeout(10)
+ self.assertEqual(len(self.clock.calls), 1)
+
+
+ def test_timeout(self):
+ """
+ Check that the protocol does timeout at the time specified by its
+ C{timeOut} attribute.
+ """
+ self.proto.makeConnection(StringTransport())
+
+ # timeOut value is 3
+ self.clock.pump([0, 0.5, 1.0, 1.0])
+ self.failIf(self.proto.timedOut)
+ self.clock.pump([0, 1.0])
+ self.failUnless(self.proto.timedOut)
+
+
+ def test_noTimeout(self):
+ """
+ Check that receiving data is delaying the timeout of the connection.
+ """
+ self.proto.makeConnection(StringTransport())
+
+ self.clock.pump([0, 0.5, 1.0, 1.0])
+ self.failIf(self.proto.timedOut)
+ self.proto.dataReceived('hello there')
+ self.clock.pump([0, 1.0, 1.0, 0.5])
+ self.failIf(self.proto.timedOut)
+ self.clock.pump([0, 1.0])
+ self.failUnless(self.proto.timedOut)
+
+
+ def test_resetTimeout(self):
+ """
+ Check that setting a new value for timeout cancel the previous value
+ and install a new timeout.
+ """
+ self.proto.timeOut = None
+ self.proto.makeConnection(StringTransport())
+
+ self.proto.setTimeout(1)
+ self.assertEqual(self.proto.timeOut, 1)
+
+ self.clock.pump([0, 0.9])
+ self.failIf(self.proto.timedOut)
+ self.clock.pump([0, 0.2])
+ self.failUnless(self.proto.timedOut)
+
+
+ def test_cancelTimeout(self):
+ """
+ Setting the timeout to C{None} cancel any timeout operations.
+ """
+ self.proto.timeOut = 5
+ self.proto.makeConnection(StringTransport())
+
+ self.proto.setTimeout(None)
+ self.assertEqual(self.proto.timeOut, None)
+
+ self.clock.pump([0, 5, 5, 5])
+ self.failIf(self.proto.timedOut)
+
+
+ def test_return(self):
+ """
+ setTimeout should return the value of the previous timeout.
+ """
+ self.proto.timeOut = 5
+
+ self.assertEqual(self.proto.setTimeout(10), 5)
+ self.assertEqual(self.proto.setTimeout(None), 10)
+ self.assertEqual(self.proto.setTimeout(1), None)
+ self.assertEqual(self.proto.timeOut, 1)
+
+ # Clean up the DelayedCall
+ self.proto.setTimeout(None)
+
+
+
+class LimitTotalConnectionsFactoryTestCase(unittest.TestCase):
+ """Tests for policies.LimitTotalConnectionsFactory"""
+ def testConnectionCounting(self):
+ # Make a basic factory
+ factory = policies.LimitTotalConnectionsFactory()
+ factory.protocol = protocol.Protocol
+
+ # connectionCount starts at zero
+ self.assertEqual(0, factory.connectionCount)
+
+ # connectionCount increments as connections are made
+ p1 = factory.buildProtocol(None)
+ self.assertEqual(1, factory.connectionCount)
+ p2 = factory.buildProtocol(None)
+ self.assertEqual(2, factory.connectionCount)
+
+ # and decrements as they are lost
+ p1.connectionLost(None)
+ self.assertEqual(1, factory.connectionCount)
+ p2.connectionLost(None)
+ self.assertEqual(0, factory.connectionCount)
+
+ def testConnectionLimiting(self):
+ # Make a basic factory with a connection limit of 1
+ factory = policies.LimitTotalConnectionsFactory()
+ factory.protocol = protocol.Protocol
+ factory.connectionLimit = 1
+
+ # Make a connection
+ p = factory.buildProtocol(None)
+ self.assertNotEqual(None, p)
+ self.assertEqual(1, factory.connectionCount)
+
+ # Try to make a second connection, which will exceed the connection
+ # limit. This should return None, because overflowProtocol is None.
+ self.assertEqual(None, factory.buildProtocol(None))
+ self.assertEqual(1, factory.connectionCount)
+
+ # Define an overflow protocol
+ class OverflowProtocol(protocol.Protocol):
+ def connectionMade(self):
+ factory.overflowed = True
+ factory.overflowProtocol = OverflowProtocol
+ factory.overflowed = False
+
+ # Try to make a second connection again, now that we have an overflow
+ # protocol. Note that overflow connections count towards the connection
+ # count.
+ op = factory.buildProtocol(None)
+ op.makeConnection(None) # to trigger connectionMade
+ self.assertEqual(True, factory.overflowed)
+ self.assertEqual(2, factory.connectionCount)
+
+ # Close the connections.
+ p.connectionLost(None)
+ self.assertEqual(1, factory.connectionCount)
+ op.connectionLost(None)
+ self.assertEqual(0, factory.connectionCount)
+
+
+class WriteSequenceEchoProtocol(EchoProtocol):
+ def dataReceived(self, bytes):
+ if bytes.find('vector!') != -1:
+ self.transport.writeSequence([bytes])
+ else:
+ EchoProtocol.dataReceived(self, bytes)
+
+class TestLoggingFactory(policies.TrafficLoggingFactory):
+ openFile = None
+ def open(self, name):
+ assert self.openFile is None, "open() called too many times"
+ self.openFile = StringIO()
+ return self.openFile
+
+
+
+class LoggingFactoryTestCase(unittest.TestCase):
+ """
+ Tests for L{policies.TrafficLoggingFactory}.
+ """
+
+ def test_thingsGetLogged(self):
+ """
+ Check the output produced by L{policies.TrafficLoggingFactory}.
+ """
+ wrappedFactory = Server()
+ wrappedFactory.protocol = WriteSequenceEchoProtocol
+ t = StringTransportWithDisconnection()
+ f = TestLoggingFactory(wrappedFactory, 'test')
+ p = f.buildProtocol(('1.2.3.4', 5678))
+ t.protocol = p
+ p.makeConnection(t)
+
+ v = f.openFile.getvalue()
+ self.failUnless('*' in v, "* not found in %r" % (v,))
+ self.failIf(t.value())
+
+ p.dataReceived('here are some bytes')
+
+ v = f.openFile.getvalue()
+ self.assertIn("C 1: 'here are some bytes'", v)
+ self.assertIn("S 1: 'here are some bytes'", v)
+ self.assertEqual(t.value(), 'here are some bytes')
+
+ t.clear()
+ p.dataReceived('prepare for vector! to the extreme')
+ v = f.openFile.getvalue()
+ self.assertIn("SV 1: ['prepare for vector! to the extreme']", v)
+ self.assertEqual(t.value(), 'prepare for vector! to the extreme')
+
+ p.loseConnection()
+
+ v = f.openFile.getvalue()
+ self.assertIn('ConnectionDone', v)
+
+
+ def test_counter(self):
+ """
+ Test counter management with the resetCounter method.
+ """
+ wrappedFactory = Server()
+ f = TestLoggingFactory(wrappedFactory, 'test')
+ self.assertEqual(f._counter, 0)
+ f.buildProtocol(('1.2.3.4', 5678))
+ self.assertEqual(f._counter, 1)
+ # Reset log file
+ f.openFile = None
+ f.buildProtocol(('1.2.3.4', 5679))
+ self.assertEqual(f._counter, 2)
+
+ f.resetCounter()
+ self.assertEqual(f._counter, 0)
+
diff --git a/twisted/test/test_postfix.py b/twisted/test/test_postfix.py
new file mode 100644
index 0000000..0f80a46
--- /dev/null
+++ b/twisted/test/test_postfix.py
@@ -0,0 +1,108 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.protocols.postfix module.
+"""
+
+from twisted.trial import unittest
+from twisted.protocols import postfix
+from twisted.test.proto_helpers import StringTransport
+
+
+class PostfixTCPMapQuoteTestCase(unittest.TestCase):
+ data = [
+ # (raw, quoted, [aliasQuotedForms]),
+ ('foo', 'foo'),
+ ('foo bar', 'foo%20bar'),
+ ('foo\tbar', 'foo%09bar'),
+ ('foo\nbar', 'foo%0Abar', 'foo%0abar'),
+ ('foo\r\nbar', 'foo%0D%0Abar', 'foo%0D%0abar', 'foo%0d%0Abar', 'foo%0d%0abar'),
+ ('foo ', 'foo%20'),
+ (' foo', '%20foo'),
+ ]
+
+ def testData(self):
+ for entry in self.data:
+ raw = entry[0]
+ quoted = entry[1:]
+
+ self.assertEqual(postfix.quote(raw), quoted[0])
+ for q in quoted:
+ self.assertEqual(postfix.unquote(q), raw)
+
+class PostfixTCPMapServerTestCase:
+ data = {
+ # 'key': 'value',
+ }
+
+ chat = [
+ # (input, expected_output),
+ ]
+
+ def test_chat(self):
+ """
+ Test that I{get} and I{put} commands are responded to correctly by
+ L{postfix.PostfixTCPMapServer} when its factory is an instance of
+ L{postifx.PostfixTCPMapDictServerFactory}.
+ """
+ factory = postfix.PostfixTCPMapDictServerFactory(self.data)
+ transport = StringTransport()
+
+ protocol = postfix.PostfixTCPMapServer()
+ protocol.service = factory
+ protocol.factory = factory
+ protocol.makeConnection(transport)
+
+ for input, expected_output in self.chat:
+ protocol.lineReceived(input)
+ self.assertEqual(
+ transport.value(), expected_output,
+ 'For %r, expected %r but got %r' % (
+ input, expected_output, transport.value()))
+ transport.clear()
+ protocol.setTimeout(None)
+
+
+ def test_deferredChat(self):
+ """
+ Test that I{get} and I{put} commands are responded to correctly by
+ L{postfix.PostfixTCPMapServer} when its factory is an instance of
+ L{postifx.PostfixTCPMapDeferringDictServerFactory}.
+ """
+ factory = postfix.PostfixTCPMapDeferringDictServerFactory(self.data)
+ transport = StringTransport()
+
+ protocol = postfix.PostfixTCPMapServer()
+ protocol.service = factory
+ protocol.factory = factory
+ protocol.makeConnection(transport)
+
+ for input, expected_output in self.chat:
+ protocol.lineReceived(input)
+ self.assertEqual(
+ transport.value(), expected_output,
+ 'For %r, expected %r but got %r' % (
+ input, expected_output, transport.value()))
+ transport.clear()
+ protocol.setTimeout(None)
+
+
+
+class Valid(PostfixTCPMapServerTestCase, unittest.TestCase):
+ data = {
+ 'foo': 'ThisIs Foo',
+ 'bar': ' bar really is found\r\n',
+ }
+ chat = [
+ ('get', "400 Command 'get' takes 1 parameters.\n"),
+ ('get foo bar', "500 \n"),
+ ('put', "400 Command 'put' takes 2 parameters.\n"),
+ ('put foo', "400 Command 'put' takes 2 parameters.\n"),
+ ('put foo bar baz', "500 put is not implemented yet.\n"),
+ ('put foo bar', '500 put is not implemented yet.\n'),
+ ('get foo', '200 ThisIs%20Foo\n'),
+ ('get bar', '200 %20bar%20really%20is%20found%0D%0A\n'),
+ ('get baz', '500 \n'),
+ ('foo', '400 unknown command\n'),
+ ]
diff --git a/twisted/test/test_process.py b/twisted/test/test_process.py
new file mode 100644
index 0000000..7d9fda1
--- /dev/null
+++ b/twisted/test/test_process.py
@@ -0,0 +1,2482 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test running processes.
+"""
+
+import gzip
+import os
+import sys
+import signal
+import StringIO
+import errno
+import gc
+import stat
+import operator
+try:
+ import fcntl
+except ImportError:
+ fcntl = process = None
+else:
+ from twisted.internet import process
+
+
+from zope.interface.verify import verifyObject
+
+from twisted.python.log import msg
+from twisted.internet import reactor, protocol, error, interfaces, defer
+from twisted.trial import unittest
+from twisted.python import util, runtime, procutils
+from twisted.python.compat import set
+
+
+
+class StubProcessProtocol(protocol.ProcessProtocol):
+ """
+ ProcessProtocol counter-implementation: all methods on this class raise an
+ exception, so instances of this may be used to verify that only certain
+ methods are called.
+ """
+ def outReceived(self, data):
+ raise NotImplementedError()
+
+ def errReceived(self, data):
+ raise NotImplementedError()
+
+ def inConnectionLost(self):
+ raise NotImplementedError()
+
+ def outConnectionLost(self):
+ raise NotImplementedError()
+
+ def errConnectionLost(self):
+ raise NotImplementedError()
+
+
+
+class ProcessProtocolTests(unittest.TestCase):
+ """
+ Tests for behavior provided by the process protocol base class,
+ L{protocol.ProcessProtocol}.
+ """
+ def test_interface(self):
+ """
+ L{ProcessProtocol} implements L{IProcessProtocol}.
+ """
+ verifyObject(interfaces.IProcessProtocol, protocol.ProcessProtocol())
+
+
+ def test_outReceived(self):
+ """
+ Verify that when stdout is delivered to
+ L{ProcessProtocol.childDataReceived}, it is forwarded to
+ L{ProcessProtocol.outReceived}.
+ """
+ received = []
+ class OutProtocol(StubProcessProtocol):
+ def outReceived(self, data):
+ received.append(data)
+
+ bytes = "bytes"
+ p = OutProtocol()
+ p.childDataReceived(1, bytes)
+ self.assertEqual(received, [bytes])
+
+
+ def test_errReceived(self):
+ """
+ Similar to L{test_outReceived}, but for stderr.
+ """
+ received = []
+ class ErrProtocol(StubProcessProtocol):
+ def errReceived(self, data):
+ received.append(data)
+
+ bytes = "bytes"
+ p = ErrProtocol()
+ p.childDataReceived(2, bytes)
+ self.assertEqual(received, [bytes])
+
+
+ def test_inConnectionLost(self):
+ """
+ Verify that when stdin close notification is delivered to
+ L{ProcessProtocol.childConnectionLost}, it is forwarded to
+ L{ProcessProtocol.inConnectionLost}.
+ """
+ lost = []
+ class InLostProtocol(StubProcessProtocol):
+ def inConnectionLost(self):
+ lost.append(None)
+
+ p = InLostProtocol()
+ p.childConnectionLost(0)
+ self.assertEqual(lost, [None])
+
+
+ def test_outConnectionLost(self):
+ """
+ Similar to L{test_inConnectionLost}, but for stdout.
+ """
+ lost = []
+ class OutLostProtocol(StubProcessProtocol):
+ def outConnectionLost(self):
+ lost.append(None)
+
+ p = OutLostProtocol()
+ p.childConnectionLost(1)
+ self.assertEqual(lost, [None])
+
+
+ def test_errConnectionLost(self):
+ """
+ Similar to L{test_inConnectionLost}, but for stderr.
+ """
+ lost = []
+ class ErrLostProtocol(StubProcessProtocol):
+ def errConnectionLost(self):
+ lost.append(None)
+
+ p = ErrLostProtocol()
+ p.childConnectionLost(2)
+ self.assertEqual(lost, [None])
+
+
+
+class TrivialProcessProtocol(protocol.ProcessProtocol):
+ """
+ Simple process protocol for tests purpose.
+
+ @ivar outData: data received from stdin
+ @ivar errData: data received from stderr
+ """
+
+ def __init__(self, d):
+ """
+ Create the deferred that will be fired at the end, and initialize
+ data structures.
+ """
+ self.deferred = d
+ self.outData = []
+ self.errData = []
+
+ def processEnded(self, reason):
+ self.reason = reason
+ self.deferred.callback(None)
+
+ def outReceived(self, data):
+ self.outData.append(data)
+
+ def errReceived(self, data):
+ self.errData.append(data)
+
+
+class TestProcessProtocol(protocol.ProcessProtocol):
+
+ def connectionMade(self):
+ self.stages = [1]
+ self.data = ''
+ self.err = ''
+ self.transport.write("abcd")
+
+ def childDataReceived(self, childFD, data):
+ """
+ Override and disable the dispatch provided by the base class to ensure
+ that it is really this method which is being called, and the transport
+ is not going directly to L{outReceived} or L{errReceived}.
+ """
+ if childFD == 1:
+ self.data += data
+ elif childFD == 2:
+ self.err += data
+
+
+ def childConnectionLost(self, childFD):
+ """
+ Similarly to L{childDataReceived}, disable the automatic dispatch
+ provided by the base implementation to verify that the transport is
+ calling this method directly.
+ """
+ if childFD == 1:
+ self.stages.append(2)
+ if self.data != "abcd":
+ raise RuntimeError(
+ "Data was %r instead of 'abcd'" % (self.data,))
+ self.transport.write("1234")
+ elif childFD == 2:
+ self.stages.append(3)
+ if self.err != "1234":
+ raise RuntimeError(
+ "Err was %r instead of '1234'" % (self.err,))
+ self.transport.write("abcd")
+ self.stages.append(4)
+ elif childFD == 0:
+ self.stages.append(5)
+
+ def processEnded(self, reason):
+ self.reason = reason
+ self.deferred.callback(None)
+
+
+class EchoProtocol(protocol.ProcessProtocol):
+
+ s = "1234567" * 1001
+ n = 10
+ finished = 0
+
+ failure = None
+
+ def __init__(self, onEnded):
+ self.onEnded = onEnded
+ self.count = 0
+
+ def connectionMade(self):
+ assert self.n > 2
+ for i in range(self.n - 2):
+ self.transport.write(self.s)
+ # test writeSequence
+ self.transport.writeSequence([self.s, self.s])
+ self.buffer = self.s * self.n
+
+ def outReceived(self, data):
+ if buffer(self.buffer, self.count, len(data)) != buffer(data):
+ self.failure = ("wrong bytes received", data, self.count)
+ self.transport.closeStdin()
+ else:
+ self.count += len(data)
+ if self.count == len(self.buffer):
+ self.transport.closeStdin()
+
+ def processEnded(self, reason):
+ self.finished = 1
+ if not reason.check(error.ProcessDone):
+ self.failure = "process didn't terminate normally: " + str(reason)
+ self.onEnded.callback(self)
+
+
+
+class SignalProtocol(protocol.ProcessProtocol):
+ """
+ A process protocol that sends a signal when data is first received.
+
+ @ivar deferred: deferred firing on C{processEnded}.
+ @type deferred: L{defer.Deferred}
+
+ @ivar signal: the signal to send to the process.
+ @type signal: C{str}
+
+ @ivar signaled: A flag tracking whether the signal has been sent to the
+ child or not yet. C{False} until it is sent, then C{True}.
+ @type signaled: C{bool}
+ """
+
+ def __init__(self, deferred, sig):
+ self.deferred = deferred
+ self.signal = sig
+ self.signaled = False
+
+
+ def outReceived(self, data):
+ """
+ Handle the first output from the child process (which indicates it
+ is set up and ready to receive the signal) by sending the signal to
+ it. Also log all output to help with debugging.
+ """
+ msg("Received %r from child stdout" % (data,))
+ if not self.signaled:
+ self.signaled = True
+ self.transport.signalProcess(self.signal)
+
+
+ def errReceived(self, data):
+ """
+ Log all data received from the child's stderr to help with
+ debugging.
+ """
+ msg("Received %r from child stderr" % (data,))
+
+
+ def processEnded(self, reason):
+ """
+ Callback C{self.deferred} with C{None} if C{reason} is a
+ L{error.ProcessTerminated} failure with C{exitCode} set to C{None},
+ C{signal} set to C{self.signal}, and C{status} holding the status code
+ of the exited process. Otherwise, errback with a C{ValueError}
+ describing the problem.
+ """
+ msg("Child exited: %r" % (reason.getTraceback(),))
+ if not reason.check(error.ProcessTerminated):
+ return self.deferred.errback(
+ ValueError("wrong termination: %s" % (reason,)))
+ v = reason.value
+ if isinstance(self.signal, str):
+ signalValue = getattr(signal, 'SIG' + self.signal)
+ else:
+ signalValue = self.signal
+ if v.exitCode is not None:
+ return self.deferred.errback(
+ ValueError("SIG%s: exitCode is %s, not None" %
+ (self.signal, v.exitCode)))
+ if v.signal != signalValue:
+ return self.deferred.errback(
+ ValueError("SIG%s: .signal was %s, wanted %s" %
+ (self.signal, v.signal, signalValue)))
+ if os.WTERMSIG(v.status) != signalValue:
+ return self.deferred.errback(
+ ValueError('SIG%s: %s' % (self.signal, os.WTERMSIG(v.status))))
+ self.deferred.callback(None)
+
+
+
+class TestManyProcessProtocol(TestProcessProtocol):
+ def __init__(self):
+ self.deferred = defer.Deferred()
+
+ def processEnded(self, reason):
+ self.reason = reason
+ if reason.check(error.ProcessDone):
+ self.deferred.callback(None)
+ else:
+ self.deferred.errback(reason)
+
+
+
+class UtilityProcessProtocol(protocol.ProcessProtocol):
+ """
+ Helper class for launching a Python process and getting a result from it.
+
+ @ivar program: A string giving a Python program for the child process to
+ run.
+ """
+ program = None
+
+ def run(cls, reactor, argv, env):
+ """
+ Run a Python process connected to a new instance of this protocol
+ class. Return the protocol instance.
+
+ The Python process is given C{self.program} on the command line to
+ execute, in addition to anything specified by C{argv}. C{env} is
+ the complete environment.
+ """
+ exe = sys.executable
+ self = cls()
+ reactor.spawnProcess(
+ self, exe, [exe, "-c", self.program] + argv, env=env)
+ return self
+ run = classmethod(run)
+
+
+ def __init__(self):
+ self.bytes = []
+ self.requests = []
+
+
+ def parseChunks(self, bytes):
+ """
+ Called with all bytes received on stdout when the process exits.
+ """
+ raise NotImplementedError()
+
+
+ def getResult(self):
+ """
+ Return a Deferred which will fire with the result of L{parseChunks}
+ when the child process exits.
+ """
+ d = defer.Deferred()
+ self.requests.append(d)
+ return d
+
+
+ def _fireResultDeferreds(self, result):
+ """
+ Callback all Deferreds returned up until now by L{getResult}
+ with the given result object.
+ """
+ requests = self.requests
+ self.requests = None
+ for d in requests:
+ d.callback(result)
+
+
+ def outReceived(self, bytes):
+ """
+ Accumulate output from the child process in a list.
+ """
+ self.bytes.append(bytes)
+
+
+ def processEnded(self, reason):
+ """
+ Handle process termination by parsing all received output and firing
+ any waiting Deferreds.
+ """
+ self._fireResultDeferreds(self.parseChunks(self.bytes))
+
+
+
+
+class GetArgumentVector(UtilityProcessProtocol):
+ """
+ Protocol which will read a serialized argv from a process and
+ expose it to interested parties.
+ """
+ program = (
+ "from sys import stdout, argv\n"
+ "stdout.write(chr(0).join(argv))\n"
+ "stdout.flush()\n")
+
+ def parseChunks(self, chunks):
+ """
+ Parse the output from the process to which this protocol was
+ connected, which is a single unterminated line of \\0-separated
+ strings giving the argv of that process. Return this as a list of
+ str objects.
+ """
+ return ''.join(chunks).split('\0')
+
+
+
+class GetEnvironmentDictionary(UtilityProcessProtocol):
+ """
+ Protocol which will read a serialized environment dict from a process
+ and expose it to interested parties.
+ """
+ program = (
+ "from sys import stdout\n"
+ "from os import environ\n"
+ "items = environ.iteritems()\n"
+ "stdout.write(chr(0).join([k + chr(0) + v for k, v in items]))\n"
+ "stdout.flush()\n")
+
+ def parseChunks(self, chunks):
+ """
+ Parse the output from the process to which this protocol was
+ connected, which is a single unterminated line of \\0-separated
+ strings giving key value pairs of the environment from that process.
+ Return this as a dictionary.
+ """
+ environString = ''.join(chunks)
+ if not environString:
+ return {}
+ environ = iter(environString.split('\0'))
+ d = {}
+ while 1:
+ try:
+ k = environ.next()
+ except StopIteration:
+ break
+ else:
+ v = environ.next()
+ d[k] = v
+ return d
+
+
+
+class ProcessTestCase(unittest.TestCase):
+ """Test running a process."""
+
+ usePTY = False
+
+ def testStdio(self):
+ """twisted.internet.stdio test."""
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_twisted.py")
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ env = {"PYTHONPATH": os.pathsep.join(sys.path)}
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=env,
+ path=None, usePTY=self.usePTY)
+ p.transport.write("hello, world")
+ p.transport.write("abc")
+ p.transport.write("123")
+ p.transport.closeStdin()
+
+ def processEnded(ign):
+ self.assertEqual(p.outF.getvalue(), "hello, worldabc123",
+ "Output follows:\n"
+ "%s\n"
+ "Error message from process_twisted follows:\n"
+ "%s\n" % (p.outF.getvalue(), p.errF.getvalue()))
+ return d.addCallback(processEnded)
+
+
+ def test_unsetPid(self):
+ """
+ Test if pid is None/non-None before/after process termination. This
+ reuses process_echoer.py to get a process that blocks on stdin.
+ """
+ finished = defer.Deferred()
+ p = TrivialProcessProtocol(finished)
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_echoer.py")
+ procTrans = reactor.spawnProcess(p, exe,
+ [exe, scriptPath], env=None)
+ self.failUnless(procTrans.pid)
+
+ def afterProcessEnd(ignored):
+ self.assertEqual(procTrans.pid, None)
+
+ p.transport.closeStdin()
+ return finished.addCallback(afterProcessEnd)
+
+
+ def test_process(self):
+ """
+ Test running a process: check its output, it exitCode, some property of
+ signalProcess.
+ """
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_tester.py")
+ d = defer.Deferred()
+ p = TestProcessProtocol()
+ p.deferred = d
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None)
+ def check(ignored):
+ self.assertEqual(p.stages, [1, 2, 3, 4, 5])
+ f = p.reason
+ f.trap(error.ProcessTerminated)
+ self.assertEqual(f.value.exitCode, 23)
+ # would .signal be available on non-posix?
+ # self.assertEqual(f.value.signal, None)
+ self.assertRaises(
+ error.ProcessExitedAlready, p.transport.signalProcess, 'INT')
+ try:
+ import process_tester, glob
+ for f in glob.glob(process_tester.test_file_match):
+ os.remove(f)
+ except:
+ pass
+ d.addCallback(check)
+ return d
+
+ def testManyProcesses(self):
+
+ def _check(results, protocols):
+ for p in protocols:
+ self.assertEqual(p.stages, [1, 2, 3, 4, 5], "[%d] stages = %s" % (id(p.transport), str(p.stages)))
+ # test status code
+ f = p.reason
+ f.trap(error.ProcessTerminated)
+ self.assertEqual(f.value.exitCode, 23)
+
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_tester.py")
+ args = [exe, "-u", scriptPath]
+ protocols = []
+ deferreds = []
+
+ for i in xrange(50):
+ p = TestManyProcessProtocol()
+ protocols.append(p)
+ reactor.spawnProcess(p, exe, args, env=None)
+ deferreds.append(p.deferred)
+
+ deferredList = defer.DeferredList(deferreds, consumeErrors=True)
+ deferredList.addCallback(_check, protocols)
+ return deferredList
+
+
+ def test_echo(self):
+ """
+ A spawning a subprocess which echoes its stdin to its stdout via
+ C{reactor.spawnProcess} will result in that echoed output being
+ delivered to outReceived.
+ """
+ finished = defer.Deferred()
+ p = EchoProtocol(finished)
+
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_echoer.py")
+ reactor.spawnProcess(p, exe, [exe, scriptPath], env=None)
+
+ def asserts(ignored):
+ self.failIf(p.failure, p.failure)
+ self.failUnless(hasattr(p, 'buffer'))
+ self.assertEqual(len(''.join(p.buffer)), len(p.s * p.n))
+
+ def takedownProcess(err):
+ p.transport.closeStdin()
+ return err
+
+ return finished.addCallback(asserts).addErrback(takedownProcess)
+
+
+ def testCommandLine(self):
+ args = [r'a\"b ', r'a\b ', r' a\\"b', r' a\\b', r'"foo bar" "', '\tab', '"\\', 'a"b', "a'b"]
+ pyExe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_cmdline.py")
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, pyExe, [pyExe, "-u", scriptPath]+args, env=None,
+ path=None)
+
+ def processEnded(ign):
+ self.assertEqual(p.errF.getvalue(), "")
+ recvdArgs = p.outF.getvalue().splitlines()
+ self.assertEqual(recvdArgs, args)
+ return d.addCallback(processEnded)
+
+
+ def test_wrongArguments(self):
+ """
+ Test invalid arguments to spawnProcess: arguments and environment
+ must only contains string or unicode, and not null bytes.
+ """
+ exe = sys.executable
+ p = protocol.ProcessProtocol()
+
+ badEnvs = [
+ {"foo": 2},
+ {"foo": "egg\0a"},
+ {3: "bar"},
+ {"bar\0foo": "bar"}]
+
+ badArgs = [
+ [exe, 2],
+ "spam",
+ [exe, "foo\0bar"]]
+
+ # Sanity check - this will fail for people who have mucked with
+ # their site configuration in a stupid way, but there's nothing we
+ # can do about that.
+ badUnicode = u'\N{SNOWMAN}'
+ try:
+ badUnicode.encode(sys.getdefaultencoding())
+ except UnicodeEncodeError:
+ # Okay, that unicode doesn't encode, put it in as a bad environment
+ # key.
+ badEnvs.append({badUnicode: 'value for bad unicode key'})
+ badEnvs.append({'key for bad unicode value': badUnicode})
+ badArgs.append([exe, badUnicode])
+ else:
+ # It _did_ encode. Most likely, Gtk2 is being used and the
+ # default system encoding is UTF-8, which can encode anything.
+ # In any case, if implicit unicode -> str conversion works for
+ # that string, we can't test that TypeError gets raised instead,
+ # so just leave it off.
+ pass
+
+ for env in badEnvs:
+ self.assertRaises(
+ TypeError,
+ reactor.spawnProcess, p, exe, [exe, "-c", ""], env=env)
+
+ for args in badArgs:
+ self.assertRaises(
+ TypeError,
+ reactor.spawnProcess, p, exe, args, env=None)
+
+
+ # Use upper-case so that the environment key test uses an upper case
+ # name: some versions of Windows only support upper case environment
+ # variable names, and I think Python (as of 2.5) doesn't use the right
+ # syscall for lowercase or mixed case names to work anyway.
+ okayUnicode = u"UNICODE"
+ encodedValue = "UNICODE"
+
+ def _deprecatedUnicodeSupportTest(self, processProtocolClass, argv=[], env={}):
+ """
+ Check that a deprecation warning is emitted when passing unicode to
+ spawnProcess for an argv value or an environment key or value.
+ Check that the warning is of the right type, has the right message,
+ and refers to the correct file. Unfortunately, don't check that the
+ line number is correct, because that is too hard for me to figure
+ out.
+
+ @param processProtocolClass: A L{UtilityProcessProtocol} subclass
+ which will be instantiated to communicate with the child process.
+
+ @param argv: The argv argument to spawnProcess.
+
+ @param env: The env argument to spawnProcess.
+
+ @return: A Deferred which fires when the test is complete.
+ """
+ # Sanity to check to make sure we can actually encode this unicode
+ # with the default system encoding. This may be excessively
+ # paranoid. -exarkun
+ self.assertEqual(
+ self.okayUnicode.encode(sys.getdefaultencoding()),
+ self.encodedValue)
+
+ p = self.assertWarns(DeprecationWarning,
+ "Argument strings and environment keys/values passed to "
+ "reactor.spawnProcess should be str, not unicode.", __file__,
+ processProtocolClass.run, reactor, argv, env)
+ return p.getResult()
+
+
+ def test_deprecatedUnicodeArgvSupport(self):
+ """
+ Test that a unicode string passed for an argument value is allowed
+ if it can be encoded with the default system encoding, but that a
+ deprecation warning is emitted.
+ """
+ d = self._deprecatedUnicodeSupportTest(GetArgumentVector, argv=[self.okayUnicode])
+ def gotArgVector(argv):
+ self.assertEqual(argv, ['-c', self.encodedValue])
+ d.addCallback(gotArgVector)
+ return d
+
+
+ def test_deprecatedUnicodeEnvKeySupport(self):
+ """
+ Test that a unicode string passed for the key of the environment
+ dictionary is allowed if it can be encoded with the default system
+ encoding, but that a deprecation warning is emitted.
+ """
+ d = self._deprecatedUnicodeSupportTest(
+ GetEnvironmentDictionary, env={self.okayUnicode: self.encodedValue})
+ def gotEnvironment(environ):
+ self.assertEqual(environ[self.encodedValue], self.encodedValue)
+ d.addCallback(gotEnvironment)
+ return d
+
+
+ def test_deprecatedUnicodeEnvValueSupport(self):
+ """
+ Test that a unicode string passed for the value of the environment
+ dictionary is allowed if it can be encoded with the default system
+ encoding, but that a deprecation warning is emitted.
+ """
+ d = self._deprecatedUnicodeSupportTest(
+ GetEnvironmentDictionary, env={self.encodedValue: self.okayUnicode})
+ def gotEnvironment(environ):
+ # On Windows, the environment contains more things than we
+ # specified, so only make sure that at least the key we wanted
+ # is there, rather than testing the dictionary for exact
+ # equality.
+ self.assertEqual(environ[self.encodedValue], self.encodedValue)
+ d.addCallback(gotEnvironment)
+ return d
+
+
+
+class TwoProcessProtocol(protocol.ProcessProtocol):
+ num = -1
+ finished = 0
+ def __init__(self):
+ self.deferred = defer.Deferred()
+ def outReceived(self, data):
+ pass
+ def processEnded(self, reason):
+ self.finished = 1
+ self.deferred.callback(None)
+
+class TestTwoProcessesBase:
+ def setUp(self):
+ self.processes = [None, None]
+ self.pp = [None, None]
+ self.done = 0
+ self.verbose = 0
+
+ def createProcesses(self, usePTY=0):
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_reader.py")
+ for num in (0,1):
+ self.pp[num] = TwoProcessProtocol()
+ self.pp[num].num = num
+ p = reactor.spawnProcess(self.pp[num],
+ exe, [exe, "-u", scriptPath], env=None,
+ usePTY=usePTY)
+ self.processes[num] = p
+
+ def close(self, num):
+ if self.verbose: print "closing stdin [%d]" % num
+ p = self.processes[num]
+ pp = self.pp[num]
+ self.failIf(pp.finished, "Process finished too early")
+ p.loseConnection()
+ if self.verbose: print self.pp[0].finished, self.pp[1].finished
+
+ def _onClose(self):
+ return defer.gatherResults([ p.deferred for p in self.pp ])
+
+ def testClose(self):
+ if self.verbose: print "starting processes"
+ self.createProcesses()
+ reactor.callLater(1, self.close, 0)
+ reactor.callLater(2, self.close, 1)
+ return self._onClose()
+
+class TestTwoProcessesNonPosix(TestTwoProcessesBase, unittest.TestCase):
+ pass
+
+class TestTwoProcessesPosix(TestTwoProcessesBase, unittest.TestCase):
+ def tearDown(self):
+ for pp, pr in zip(self.pp, self.processes):
+ if not pp.finished:
+ try:
+ os.kill(pr.pid, signal.SIGTERM)
+ except OSError:
+ # If the test failed the process may already be dead
+ # The error here is only noise
+ pass
+ return self._onClose()
+
+ def kill(self, num):
+ if self.verbose: print "kill [%d] with SIGTERM" % num
+ p = self.processes[num]
+ pp = self.pp[num]
+ self.failIf(pp.finished, "Process finished too early")
+ os.kill(p.pid, signal.SIGTERM)
+ if self.verbose: print self.pp[0].finished, self.pp[1].finished
+
+ def testKill(self):
+ if self.verbose: print "starting processes"
+ self.createProcesses(usePTY=0)
+ reactor.callLater(1, self.kill, 0)
+ reactor.callLater(2, self.kill, 1)
+ return self._onClose()
+
+ def testClosePty(self):
+ if self.verbose: print "starting processes"
+ self.createProcesses(usePTY=1)
+ reactor.callLater(1, self.close, 0)
+ reactor.callLater(2, self.close, 1)
+ return self._onClose()
+
+ def testKillPty(self):
+ if self.verbose: print "starting processes"
+ self.createProcesses(usePTY=1)
+ reactor.callLater(1, self.kill, 0)
+ reactor.callLater(2, self.kill, 1)
+ return self._onClose()
+
+class FDChecker(protocol.ProcessProtocol):
+ state = 0
+ data = ""
+ failed = None
+
+ def __init__(self, d):
+ self.deferred = d
+
+ def fail(self, why):
+ self.failed = why
+ self.deferred.callback(None)
+
+ def connectionMade(self):
+ self.transport.writeToChild(0, "abcd")
+ self.state = 1
+
+ def childDataReceived(self, childFD, data):
+ if self.state == 1:
+ if childFD != 1:
+ self.fail("read '%s' on fd %d (not 1) during state 1" \
+ % (childFD, data))
+ return
+ self.data += data
+ #print "len", len(self.data)
+ if len(self.data) == 6:
+ if self.data != "righto":
+ self.fail("got '%s' on fd1, expected 'righto'" \
+ % self.data)
+ return
+ self.data = ""
+ self.state = 2
+ #print "state2", self.state
+ self.transport.writeToChild(3, "efgh")
+ return
+ if self.state == 2:
+ self.fail("read '%s' on fd %s during state 2" % (childFD, data))
+ return
+ if self.state == 3:
+ if childFD != 1:
+ self.fail("read '%s' on fd %s (not 1) during state 3" \
+ % (childFD, data))
+ return
+ self.data += data
+ if len(self.data) == 6:
+ if self.data != "closed":
+ self.fail("got '%s' on fd1, expected 'closed'" \
+ % self.data)
+ return
+ self.state = 4
+ return
+ if self.state == 4:
+ self.fail("read '%s' on fd %s during state 4" % (childFD, data))
+ return
+
+ def childConnectionLost(self, childFD):
+ if self.state == 1:
+ self.fail("got connectionLost(%d) during state 1" % childFD)
+ return
+ if self.state == 2:
+ if childFD != 4:
+ self.fail("got connectionLost(%d) (not 4) during state 2" \
+ % childFD)
+ return
+ self.state = 3
+ self.transport.closeChildFD(5)
+ return
+
+ def processEnded(self, status):
+ rc = status.value.exitCode
+ if self.state != 4:
+ self.fail("processEnded early, rc %d" % rc)
+ return
+ if status.value.signal != None:
+ self.fail("processEnded with signal %s" % status.value.signal)
+ return
+ if rc != 0:
+ self.fail("processEnded with rc %d" % rc)
+ return
+ self.deferred.callback(None)
+
+
+class FDTest(unittest.TestCase):
+
+ def testFD(self):
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_fds.py")
+ d = defer.Deferred()
+ p = FDChecker(d)
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None,
+ path=None,
+ childFDs={0:"w", 1:"r", 2:2,
+ 3:"w", 4:"r", 5:"w"})
+ d.addCallback(lambda x : self.failIf(p.failed, p.failed))
+ return d
+
+ def testLinger(self):
+ # See what happens when all the pipes close before the process
+ # actually stops. This test *requires* SIGCHLD catching to work,
+ # as there is no other way to find out the process is done.
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_linger.py")
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None,
+ path=None,
+ childFDs={1:"r", 2:2},
+ )
+ def processEnded(ign):
+ self.assertEqual(p.outF.getvalue(),
+ "here is some text\ngoodbye\n")
+ return d.addCallback(processEnded)
+
+
+
+class Accumulator(protocol.ProcessProtocol):
+ """Accumulate data from a process."""
+
+ closed = 0
+ endedDeferred = None
+
+ def connectionMade(self):
+ self.outF = StringIO.StringIO()
+ self.errF = StringIO.StringIO()
+
+ def outReceived(self, d):
+ self.outF.write(d)
+
+ def errReceived(self, d):
+ self.errF.write(d)
+
+ def outConnectionLost(self):
+ pass
+
+ def errConnectionLost(self):
+ pass
+
+ def processEnded(self, reason):
+ self.closed = 1
+ if self.endedDeferred is not None:
+ d, self.endedDeferred = self.endedDeferred, None
+ d.callback(None)
+
+
+class PosixProcessBase:
+ """
+ Test running processes.
+ """
+ usePTY = False
+
+ def getCommand(self, commandName):
+ """
+ Return the path of the shell command named C{commandName}, looking at
+ common locations.
+ """
+ if os.path.exists('/bin/%s' % (commandName,)):
+ cmd = '/bin/%s' % (commandName,)
+ elif os.path.exists('/usr/bin/%s' % (commandName,)):
+ cmd = '/usr/bin/%s' % (commandName,)
+ else:
+ raise RuntimeError(
+ "%s not found in /bin or /usr/bin" % (commandName,))
+ return cmd
+
+ def testNormalTermination(self):
+ cmd = self.getCommand('true')
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ reactor.spawnProcess(p, cmd, ['true'], env=None,
+ usePTY=self.usePTY)
+ def check(ignored):
+ p.reason.trap(error.ProcessDone)
+ self.assertEqual(p.reason.value.exitCode, 0)
+ self.assertEqual(p.reason.value.signal, None)
+ d.addCallback(check)
+ return d
+
+
+ def test_abnormalTermination(self):
+ """
+ When a process terminates with a system exit code set to 1,
+ C{processEnded} is called with a L{error.ProcessTerminated} error,
+ the C{exitCode} attribute reflecting the system exit code.
+ """
+ exe = sys.executable
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ reactor.spawnProcess(p, exe, [exe, '-c', 'import sys; sys.exit(1)'],
+ env=None, usePTY=self.usePTY)
+
+ def check(ignored):
+ p.reason.trap(error.ProcessTerminated)
+ self.assertEqual(p.reason.value.exitCode, 1)
+ self.assertEqual(p.reason.value.signal, None)
+ d.addCallback(check)
+ return d
+
+
+ def _testSignal(self, sig):
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_signal.py")
+ d = defer.Deferred()
+ p = SignalProtocol(d, sig)
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None,
+ usePTY=self.usePTY)
+ return d
+
+
+ def test_signalHUP(self):
+ """
+ Sending the SIGHUP signal to a running process interrupts it, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} set to C{None} and the C{signal} attribute set to
+ C{signal.SIGHUP}. C{os.WTERMSIG} can also be used on the C{status}
+ attribute to extract the signal value.
+ """
+ return self._testSignal('HUP')
+
+
+ def test_signalINT(self):
+ """
+ Sending the SIGINT signal to a running process interrupts it, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} set to C{None} and the C{signal} attribute set to
+ C{signal.SIGINT}. C{os.WTERMSIG} can also be used on the C{status}
+ attribute to extract the signal value.
+ """
+ return self._testSignal('INT')
+
+
+ def test_signalKILL(self):
+ """
+ Sending the SIGKILL signal to a running process interrupts it, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} set to C{None} and the C{signal} attribute set to
+ C{signal.SIGKILL}. C{os.WTERMSIG} can also be used on the C{status}
+ attribute to extract the signal value.
+ """
+ return self._testSignal('KILL')
+
+
+ def test_signalTERM(self):
+ """
+ Sending the SIGTERM signal to a running process interrupts it, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} set to C{None} and the C{signal} attribute set to
+ C{signal.SIGTERM}. C{os.WTERMSIG} can also be used on the C{status}
+ attribute to extract the signal value.
+ """
+ return self._testSignal('TERM')
+
+
+ def test_childSignalHandling(self):
+ """
+ The disposition of signals which are ignored in the parent
+ process is reset to the default behavior for the child
+ process.
+ """
+ # Somewhat arbitrarily select SIGUSR1 here. It satisfies our
+ # requirements that:
+ # - The interpreter not fiddle around with the handler
+ # behind our backs at startup time (this disqualifies
+ # signals like SIGINT and SIGPIPE).
+ # - The default behavior is to exit.
+ #
+ # This lets us send the signal to the child and then verify
+ # that it exits with a status code indicating that it was
+ # indeed the signal which caused it to exit.
+ which = signal.SIGUSR1
+
+ # Ignore the signal in the parent (and make sure we clean it
+ # up).
+ handler = signal.signal(which, signal.SIG_IGN)
+ self.addCleanup(signal.signal, signal.SIGUSR1, handler)
+
+ # Now do the test.
+ return self._testSignal(signal.SIGUSR1)
+
+
+ def test_executionError(self):
+ """
+ Raise an error during execvpe to check error management.
+ """
+ cmd = self.getCommand('false')
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ def buggyexecvpe(command, args, environment):
+ raise RuntimeError("Ouch")
+ oldexecvpe = os.execvpe
+ os.execvpe = buggyexecvpe
+ try:
+ reactor.spawnProcess(p, cmd, ['false'], env=None,
+ usePTY=self.usePTY)
+
+ def check(ignored):
+ errData = "".join(p.errData + p.outData)
+ self.assertIn("Upon execvpe", errData)
+ self.assertIn("Ouch", errData)
+ d.addCallback(check)
+ finally:
+ os.execvpe = oldexecvpe
+ return d
+
+
+ def test_errorInProcessEnded(self):
+ """
+ The handler which reaps a process is removed when the process is
+ reaped, even if the protocol's C{processEnded} method raises an
+ exception.
+ """
+ connected = defer.Deferred()
+ ended = defer.Deferred()
+
+ # This script runs until we disconnect its transport.
+ pythonExecutable = sys.executable
+ scriptPath = util.sibpath(__file__, "process_echoer.py")
+
+ class ErrorInProcessEnded(protocol.ProcessProtocol):
+ """
+ A protocol that raises an error in C{processEnded}.
+ """
+ def makeConnection(self, transport):
+ connected.callback(transport)
+
+ def processEnded(self, reason):
+ reactor.callLater(0, ended.callback, None)
+ raise RuntimeError("Deliberate error")
+
+ # Launch the process.
+ reactor.spawnProcess(
+ ErrorInProcessEnded(), pythonExecutable,
+ [pythonExecutable, scriptPath],
+ env=None, path=None)
+
+ pid = []
+ def cbConnected(transport):
+ pid.append(transport.pid)
+ # There's now a reap process handler registered.
+ self.assertIn(transport.pid, process.reapProcessHandlers)
+
+ # Kill the process cleanly, triggering an error in the protocol.
+ transport.loseConnection()
+ connected.addCallback(cbConnected)
+
+ def checkTerminated(ignored):
+ # The exception was logged.
+ excs = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(excs), 1)
+ # The process is no longer scheduled for reaping.
+ self.assertNotIn(pid[0], process.reapProcessHandlers)
+ ended.addCallback(checkTerminated)
+
+ return ended
+
+
+
+class MockSignal(object):
+ """
+ Neuter L{signal.signal}, but pass other attributes unscathed
+ """
+ def signal(self, sig, action):
+ return signal.getsignal(sig)
+
+ def __getattr__(self, attr):
+ return getattr(signal, attr)
+
+
+class MockOS(object):
+ """
+ The mock OS: overwrite L{os}, L{fcntl} and {sys} functions with fake ones.
+
+ @ivar exited: set to True when C{_exit} is called.
+ @type exited: C{bool}
+
+ @ivar O_RDWR: dumb value faking C{os.O_RDWR}.
+ @type O_RDWR: C{int}
+
+ @ivar O_NOCTTY: dumb value faking C{os.O_NOCTTY}.
+ @type O_NOCTTY: C{int}
+
+ @ivar WNOHANG: dumb value faking C{os.WNOHANG}.
+ @type WNOHANG: C{int}
+
+ @ivar raiseFork: if not C{None}, subsequent calls to fork will raise this
+ object.
+ @type raiseFork: C{NoneType} or C{Exception}
+
+ @ivar raiseExec: if set, subsequent calls to execvpe will raise an error.
+ @type raiseExec: C{bool}
+
+ @ivar fdio: fake file object returned by calls to fdopen.
+ @type fdio: C{StringIO.StringIO}
+
+ @ivar actions: hold names of some actions executed by the object, in order
+ of execution.
+
+ @type actions: C{list} of C{str}
+
+ @ivar closed: keep track of the file descriptor closed.
+ @param closed: C{list} of C{int}
+
+ @ivar child: whether fork return for the child or the parent.
+ @type child: C{bool}
+
+ @ivar pipeCount: count the number of time that C{os.pipe} has been called.
+ @type pipeCount: C{int}
+
+ @ivar raiseWaitPid: if set, subsequent calls to waitpid will raise an
+ the error specified.
+ @type raiseWaitPid: C{None} or a class
+
+ @ivar waitChild: if set, subsequent calls to waitpid will return it.
+ @type waitChild: C{None} or a tuple
+
+ @ivar euid: the uid returned by the fake C{os.geteuid}
+ @type euid: C{int}
+
+ @ivar egid: the gid returned by the fake C{os.getegid}
+ @type egid: C{int}
+
+ @ivar seteuidCalls: stored results of C{os.seteuid} calls.
+ @type seteuidCalls: C{list}
+
+ @ivar setegidCalls: stored results of C{os.setegid} calls.
+ @type setegidCalls: C{list}
+
+ @ivar path: the path returned by C{os.path.expanduser}.
+ @type path: C{str}
+ """
+ exited = False
+ raiseExec = False
+ fdio = None
+ child = True
+ raiseWaitPid = None
+ raiseFork = None
+ waitChild = None
+ euid = 0
+ egid = 0
+ path = None
+
+ def __init__(self):
+ """
+ Initialize data structures.
+ """
+ self.actions = []
+ self.closed = []
+ self.pipeCount = 0
+ self.O_RDWR = -1
+ self.O_NOCTTY = -2
+ self.WNOHANG = -4
+ self.WEXITSTATUS = lambda x: 0
+ self.WIFEXITED = lambda x: 1
+ self.seteuidCalls = []
+ self.setegidCalls = []
+
+
+ def open(self, dev, flags):
+ """
+ Fake C{os.open}. Return a non fd number to be sure it's not used
+ elsewhere.
+ """
+ return -3
+
+
+ def fstat(self, fd):
+ """
+ Fake C{os.fstat}. Return a C{os.stat_result} filled with garbage.
+ """
+ return os.stat_result((0,) * 10)
+
+
+ def fdopen(self, fd, flag):
+ """
+ Fake C{os.fdopen}. Return a StringIO object whose content can be tested
+ later via C{self.fdio}.
+ """
+ self.fdio = StringIO.StringIO()
+ return self.fdio
+
+
+ def setsid(self):
+ """
+ Fake C{os.setsid}. Do nothing.
+ """
+
+
+ def fork(self):
+ """
+ Fake C{os.fork}. Save the action in C{self.actions}, and return 0 if
+ C{self.child} is set, or a dumb number.
+ """
+ self.actions.append(('fork', gc.isenabled()))
+ if self.raiseFork is not None:
+ raise self.raiseFork
+ elif self.child:
+ # Child result is 0
+ return 0
+ else:
+ return 21
+
+
+ def close(self, fd):
+ """
+ Fake C{os.close}, saving the closed fd in C{self.closed}.
+ """
+ self.closed.append(fd)
+
+
+ def dup2(self, fd1, fd2):
+ """
+ Fake C{os.dup2}. Do nothing.
+ """
+
+
+ def write(self, fd, data):
+ """
+ Fake C{os.write}. Do nothing.
+ """
+
+
+ def execvpe(self, command, args, env):
+ """
+ Fake C{os.execvpe}. Save the action, and raise an error if
+ C{self.raiseExec} is set.
+ """
+ self.actions.append('exec')
+ if self.raiseExec:
+ raise RuntimeError("Bar")
+
+
+ def pipe(self):
+ """
+ Fake C{os.pipe}. Return non fd numbers to be sure it's not used
+ elsewhere, and increment C{self.pipeCount}. This is used to uniquify
+ the result.
+ """
+ self.pipeCount += 1
+ return - 2 * self.pipeCount + 1, - 2 * self.pipeCount
+
+
+ def ttyname(self, fd):
+ """
+ Fake C{os.ttyname}. Return a dumb string.
+ """
+ return "foo"
+
+
+ def _exit(self, code):
+ """
+ Fake C{os._exit}. Save the action, set the C{self.exited} flag, and
+ raise C{SystemError}.
+ """
+ self.actions.append('exit')
+ self.exited = True
+ # Don't forget to raise an error, or you'll end up in parent
+ # code path.
+ raise SystemError()
+
+
+ def ioctl(self, fd, flags, arg):
+ """
+ Override C{fcntl.ioctl}. Do nothing.
+ """
+
+
+ def setNonBlocking(self, fd):
+ """
+ Override C{fdesc.setNonBlocking}. Do nothing.
+ """
+
+
+ def waitpid(self, pid, options):
+ """
+ Override C{os.waitpid}. Return values meaning that the child process
+ has exited, save executed action.
+ """
+ self.actions.append('waitpid')
+ if self.raiseWaitPid is not None:
+ raise self.raiseWaitPid
+ if self.waitChild is not None:
+ return self.waitChild
+ return 1, 0
+
+
+ def settrace(self, arg):
+ """
+ Override C{sys.settrace} to keep coverage working.
+ """
+
+
+ def getgid(self):
+ """
+ Override C{os.getgid}. Return a dumb number.
+ """
+ return 1235
+
+
+ def getuid(self):
+ """
+ Override C{os.getuid}. Return a dumb number.
+ """
+ return 1237
+
+
+ def setuid(self, val):
+ """
+ Override C{os.setuid}. Do nothing.
+ """
+ self.actions.append(('setuid', val))
+
+
+ def setgid(self, val):
+ """
+ Override C{os.setgid}. Do nothing.
+ """
+ self.actions.append(('setgid', val))
+
+
+ def setregid(self, val1, val2):
+ """
+ Override C{os.setregid}. Do nothing.
+ """
+ self.actions.append(('setregid', val1, val2))
+
+
+ def setreuid(self, val1, val2):
+ """
+ Override C{os.setreuid}. Save the action.
+ """
+ self.actions.append(('setreuid', val1, val2))
+
+
+ def switchUID(self, uid, gid):
+ """
+ Override C{util.switchuid}. Save the action.
+ """
+ self.actions.append(('switchuid', uid, gid))
+
+
+ def openpty(self):
+ """
+ Override C{pty.openpty}, returning fake file descriptors.
+ """
+ return -12, -13
+
+
+ def geteuid(self):
+ """
+ Mock C{os.geteuid}, returning C{self.euid} instead.
+ """
+ return self.euid
+
+
+ def getegid(self):
+ """
+ Mock C{os.getegid}, returning C{self.egid} instead.
+ """
+ return self.egid
+
+
+ def seteuid(self, egid):
+ """
+ Mock C{os.seteuid}, store result.
+ """
+ self.seteuidCalls.append(egid)
+
+
+ def setegid(self, egid):
+ """
+ Mock C{os.setegid}, store result.
+ """
+ self.setegidCalls.append(egid)
+
+
+ def expanduser(self, path):
+ """
+ Mock C{os.path.expanduser}.
+ """
+ return self.path
+
+
+ def getpwnam(self, user):
+ """
+ Mock C{pwd.getpwnam}.
+ """
+ return 0, 0, 1, 2
+
+ def listdir(self, path):
+ """
+ Override C{os.listdir}, returning fake contents of '/dev/fd'
+ """
+ return "-1", "-2"
+
+
+
+if process is not None:
+ class DumbProcessWriter(process.ProcessWriter):
+ """
+ A fake L{process.ProcessWriter} used for tests.
+ """
+
+ def startReading(self):
+ """
+ Here's the faking: don't do anything here.
+ """
+
+
+
+ class DumbProcessReader(process.ProcessReader):
+ """
+ A fake L{process.ProcessReader} used for tests.
+ """
+
+ def startReading(self):
+ """
+ Here's the faking: don't do anything here.
+ """
+
+
+
+ class DumbPTYProcess(process.PTYProcess):
+ """
+ A fake L{process.PTYProcess} used for tests.
+ """
+
+ def startReading(self):
+ """
+ Here's the faking: don't do anything here.
+ """
+
+
+
+class MockProcessTestCase(unittest.TestCase):
+ """
+ Mock a process runner to test forked child code path.
+ """
+ if process is None:
+ skip = "twisted.internet.process is never used on Windows"
+
+ def setUp(self):
+ """
+ Replace L{process} os, fcntl, sys, switchUID, fdesc and pty modules
+ with the mock class L{MockOS}.
+ """
+ if gc.isenabled():
+ self.addCleanup(gc.enable)
+ else:
+ self.addCleanup(gc.disable)
+ self.mockos = MockOS()
+ self.mockos.euid = 1236
+ self.mockos.egid = 1234
+ self.patch(process, "os", self.mockos)
+ self.patch(process, "fcntl", self.mockos)
+ self.patch(process, "sys", self.mockos)
+ self.patch(process, "switchUID", self.mockos.switchUID)
+ self.patch(process, "fdesc", self.mockos)
+ self.patch(process.Process, "processReaderFactory", DumbProcessReader)
+ self.patch(process.Process, "processWriterFactory", DumbProcessWriter)
+ self.patch(process, "pty", self.mockos)
+
+ self.mocksig = MockSignal()
+ self.patch(process, "signal", self.mocksig)
+
+
+ def tearDown(self):
+ """
+ Reset processes registered for reap.
+ """
+ process.reapProcessHandlers = {}
+
+
+ def test_mockFork(self):
+ """
+ Test a classic spawnProcess. Check the path of the client code:
+ fork, exec, exit.
+ """
+ gc.enable()
+
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ try:
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False)
+ except SystemError:
+ self.assert_(self.mockos.exited)
+ self.assertEqual(
+ self.mockos.actions, [("fork", False), "exec", "exit"])
+ else:
+ self.fail("Should not be here")
+
+ # It should leave the garbage collector disabled.
+ self.assertFalse(gc.isenabled())
+
+
+ def _mockForkInParentTest(self):
+ """
+ Assert that in the main process, spawnProcess disables the garbage
+ collector, calls fork, closes the pipe file descriptors it created for
+ the child process, and calls waitpid.
+ """
+ self.mockos.child = False
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False)
+ # It should close the first read pipe, and the 2 last write pipes
+ self.assertEqual(set(self.mockos.closed), set([-1, -4, -6]))
+ self.assertEqual(self.mockos.actions, [("fork", False), "waitpid"])
+
+
+ def test_mockForkInParentGarbageCollectorEnabled(self):
+ """
+ The garbage collector should be enabled when L{reactor.spawnProcess}
+ returns if it was initially enabled.
+
+ @see L{_mockForkInParentTest}
+ """
+ gc.enable()
+ self._mockForkInParentTest()
+ self.assertTrue(gc.isenabled())
+
+
+ def test_mockForkInParentGarbageCollectorDisabled(self):
+ """
+ The garbage collector should be disabled when L{reactor.spawnProcess}
+ returns if it was initially disabled.
+
+ @see L{_mockForkInParentTest}
+ """
+ gc.disable()
+ self._mockForkInParentTest()
+ self.assertFalse(gc.isenabled())
+
+
+ def test_mockForkTTY(self):
+ """
+ Test a TTY spawnProcess: check the path of the client code:
+ fork, exec, exit.
+ """
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ try:
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=True)
+ except SystemError:
+ self.assert_(self.mockos.exited)
+ self.assertEqual(
+ self.mockos.actions, [("fork", False), "exec", "exit"])
+ else:
+ self.fail("Should not be here")
+
+
+ def _mockWithForkError(self):
+ """
+ Assert that if the fork call fails, no other process setup calls are
+ made and that spawnProcess raises the exception fork raised.
+ """
+ self.mockos.raiseFork = OSError(errno.EAGAIN, None)
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None)
+ self.assertEqual(self.mockos.actions, [("fork", False)])
+
+
+ def test_mockWithForkErrorGarbageCollectorEnabled(self):
+ """
+ The garbage collector should be enabled when L{reactor.spawnProcess}
+ raises because L{os.fork} raised, if it was initially enabled.
+ """
+ gc.enable()
+ self._mockWithForkError()
+ self.assertTrue(gc.isenabled())
+
+
+ def test_mockWithForkErrorGarbageCollectorDisabled(self):
+ """
+ The garbage collector should be disabled when
+ L{reactor.spawnProcess} raises because L{os.fork} raised, if it was
+ initially disabled.
+ """
+ gc.disable()
+ self._mockWithForkError()
+ self.assertFalse(gc.isenabled())
+
+
+ def test_mockForkErrorCloseFDs(self):
+ """
+ When C{os.fork} raises an exception, the file descriptors created
+ before are closed and don't leak.
+ """
+ self._mockWithForkError()
+ self.assertEqual(set(self.mockos.closed), set([-1, -4, -6, -2, -3, -5]))
+
+
+ def test_mockForkErrorGivenFDs(self):
+ """
+ When C{os.forks} raises an exception and that file descriptors have
+ been specified with the C{childFDs} arguments of
+ L{reactor.spawnProcess}, they are not closed.
+ """
+ self.mockos.raiseFork = OSError(errno.EAGAIN, None)
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None,
+ childFDs={0: -10, 1: -11, 2: -13})
+ self.assertEqual(self.mockos.actions, [("fork", False)])
+ self.assertEqual(self.mockos.closed, [])
+
+ # We can also put "r" or "w" to let twisted create the pipes
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None,
+ childFDs={0: "r", 1: -11, 2: -13})
+ self.assertEqual(set(self.mockos.closed), set([-1, -2]))
+
+
+ def test_mockForkErrorClosePTY(self):
+ """
+ When C{os.fork} raises an exception, the file descriptors created by
+ C{pty.openpty} are closed and don't leak, when C{usePTY} is set to
+ C{True}.
+ """
+ self.mockos.raiseFork = OSError(errno.EAGAIN, None)
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None,
+ usePTY=True)
+ self.assertEqual(self.mockos.actions, [("fork", False)])
+ self.assertEqual(set(self.mockos.closed), set([-12, -13]))
+
+
+ def test_mockForkErrorPTYGivenFDs(self):
+ """
+ If a tuple is passed to C{usePTY} to specify slave and master file
+ descriptors and that C{os.fork} raises an exception, these file
+ descriptors aren't closed.
+ """
+ self.mockos.raiseFork = OSError(errno.EAGAIN, None)
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None,
+ usePTY=(-20, -21, 'foo'))
+ self.assertEqual(self.mockos.actions, [("fork", False)])
+ self.assertEqual(self.mockos.closed, [])
+
+
+ def test_mockWithExecError(self):
+ """
+ Spawn a process but simulate an error during execution in the client
+ path: C{os.execvpe} raises an error. It should close all the standard
+ fds, try to print the error encountered, and exit cleanly.
+ """
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ self.mockos.raiseExec = True
+ try:
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False)
+ except SystemError:
+ self.assert_(self.mockos.exited)
+ self.assertEqual(
+ self.mockos.actions, [("fork", False), "exec", "exit"])
+ # Check that fd have been closed
+ self.assertIn(0, self.mockos.closed)
+ self.assertIn(1, self.mockos.closed)
+ self.assertIn(2, self.mockos.closed)
+ # Check content of traceback
+ self.assertIn("RuntimeError: Bar", self.mockos.fdio.getvalue())
+ else:
+ self.fail("Should not be here")
+
+
+ def test_mockSetUid(self):
+ """
+ Try creating a process with setting its uid: it's almost the same path
+ as the standard path, but with a C{switchUID} call before the exec.
+ """
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ try:
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False, uid=8080)
+ except SystemError:
+ self.assert_(self.mockos.exited)
+ self.assertEqual(self.mockos.actions,
+ [('setuid', 0), ('setgid', 0), ('fork', False),
+ ('switchuid', 8080, 1234), 'exec', 'exit'])
+ else:
+ self.fail("Should not be here")
+
+
+ def test_mockSetUidInParent(self):
+ """
+ Try creating a process with setting its uid, in the parent path: it
+ should switch to root before fork, then restore initial uid/gids.
+ """
+ self.mockos.child = False
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False, uid=8080)
+ self.assertEqual(self.mockos.actions,
+ [('setuid', 0), ('setgid', 0), ('fork', False),
+ ('setregid', 1235, 1234), ('setreuid', 1237, 1236), 'waitpid'])
+
+
+ def test_mockPTYSetUid(self):
+ """
+ Try creating a PTY process with setting its uid: it's almost the same
+ path as the standard path, but with a C{switchUID} call before the
+ exec.
+ """
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ try:
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=True, uid=8081)
+ except SystemError:
+ self.assert_(self.mockos.exited)
+ self.assertEqual(self.mockos.actions,
+ [('setuid', 0), ('setgid', 0), ('fork', False),
+ ('switchuid', 8081, 1234), 'exec', 'exit'])
+ else:
+ self.fail("Should not be here")
+
+
+ def test_mockPTYSetUidInParent(self):
+ """
+ Try creating a PTY process with setting its uid, in the parent path: it
+ should switch to root before fork, then restore initial uid/gids.
+ """
+ self.mockos.child = False
+ cmd = '/mock/ouch'
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ oldPTYProcess = process.PTYProcess
+ try:
+ process.PTYProcess = DumbPTYProcess
+ reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=True, uid=8080)
+ finally:
+ process.PTYProcess = oldPTYProcess
+ self.assertEqual(self.mockos.actions,
+ [('setuid', 0), ('setgid', 0), ('fork', False),
+ ('setregid', 1235, 1234), ('setreuid', 1237, 1236), 'waitpid'])
+
+
+ def test_mockWithWaitError(self):
+ """
+ Test that reapProcess logs errors raised.
+ """
+ self.mockos.child = False
+ cmd = '/mock/ouch'
+ self.mockos.waitChild = (0, 0)
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ proc = reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False)
+ self.assertEqual(self.mockos.actions, [("fork", False), "waitpid"])
+
+ self.mockos.raiseWaitPid = OSError()
+ proc.reapProcess()
+ errors = self.flushLoggedErrors()
+ self.assertEqual(len(errors), 1)
+ errors[0].trap(OSError)
+
+
+ def test_mockErrorECHILDInReapProcess(self):
+ """
+ Test that reapProcess doesn't log anything when waitpid raises a
+ C{OSError} with errno C{ECHILD}.
+ """
+ self.mockos.child = False
+ cmd = '/mock/ouch'
+ self.mockos.waitChild = (0, 0)
+
+ d = defer.Deferred()
+ p = TrivialProcessProtocol(d)
+ proc = reactor.spawnProcess(p, cmd, ['ouch'], env=None,
+ usePTY=False)
+ self.assertEqual(self.mockos.actions, [("fork", False), "waitpid"])
+
+ self.mockos.raiseWaitPid = OSError()
+ self.mockos.raiseWaitPid.errno = errno.ECHILD
+ # This should not produce any errors
+ proc.reapProcess()
+
+
+ def test_mockErrorInPipe(self):
+ """
+ If C{os.pipe} raises an exception after some pipes where created, the
+ created pipes are closed and don't leak.
+ """
+ pipes = [-1, -2, -3, -4]
+ def pipe():
+ try:
+ return pipes.pop(0), pipes.pop(0)
+ except IndexError:
+ raise OSError()
+ self.mockos.pipe = pipe
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None)
+ self.assertEqual(self.mockos.actions, [])
+ self.assertEqual(set(self.mockos.closed), set([-4, -3, -2, -1]))
+
+
+ def test_mockErrorInForkRestoreUID(self):
+ """
+ If C{os.fork} raises an exception and a UID change has been made, the
+ previous UID and GID are restored.
+ """
+ self.mockos.raiseFork = OSError(errno.EAGAIN, None)
+ protocol = TrivialProcessProtocol(None)
+ self.assertRaises(OSError, reactor.spawnProcess, protocol, None,
+ uid=8080)
+ self.assertEqual(self.mockos.actions,
+ [('setuid', 0), ('setgid', 0), ("fork", False),
+ ('setregid', 1235, 1234), ('setreuid', 1237, 1236)])
+
+
+
+class PosixProcessTestCase(unittest.TestCase, PosixProcessBase):
+ # add two non-pty test cases
+
+ def test_stderr(self):
+ """
+ Bytes written to stderr by the spawned process are passed to the
+ C{errReceived} callback on the C{ProcessProtocol} passed to
+ C{spawnProcess}.
+ """
+ cmd = sys.executable
+
+ value = "42"
+
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, cmd,
+ [cmd, "-c",
+ "import sys; sys.stderr.write('%s')" % (value,)],
+ env=None, path="/tmp",
+ usePTY=self.usePTY)
+
+ def processEnded(ign):
+ self.assertEqual(value, p.errF.getvalue())
+ return d.addCallback(processEnded)
+
+
+ def testProcess(self):
+ cmd = self.getCommand('gzip')
+ s = "there's no place like home!\n" * 3
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, cmd, [cmd, "-c"], env=None, path="/tmp",
+ usePTY=self.usePTY)
+ p.transport.write(s)
+ p.transport.closeStdin()
+
+ def processEnded(ign):
+ f = p.outF
+ f.seek(0, 0)
+ gf = gzip.GzipFile(fileobj=f)
+ self.assertEqual(gf.read(), s)
+ return d.addCallback(processEnded)
+
+
+
+class PosixProcessTestCasePTY(unittest.TestCase, PosixProcessBase):
+ """
+ Just like PosixProcessTestCase, but use ptys instead of pipes.
+ """
+ usePTY = True
+ # PTYs only offer one input and one output. What still makes sense?
+ # testNormalTermination
+ # test_abnormalTermination
+ # testSignal
+ # testProcess, but not without p.transport.closeStdin
+ # might be solveable: TODO: add test if so
+
+ def testOpeningTTY(self):
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_tty.py")
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None,
+ path=None, usePTY=self.usePTY)
+ p.transport.write("hello world!\n")
+
+ def processEnded(ign):
+ self.assertRaises(
+ error.ProcessExitedAlready, p.transport.signalProcess, 'HUP')
+ self.assertEqual(
+ p.outF.getvalue(),
+ "hello world!\r\nhello world!\r\n",
+ "Error message from process_tty follows:\n\n%s\n\n" % p.outF.getvalue())
+ return d.addCallback(processEnded)
+
+
+ def testBadArgs(self):
+ pyExe = sys.executable
+ pyArgs = [pyExe, "-u", "-c", "print 'hello'"]
+ p = Accumulator()
+ self.assertRaises(ValueError, reactor.spawnProcess, p, pyExe, pyArgs,
+ usePTY=1, childFDs={1:'r'})
+
+
+
+class Win32SignalProtocol(SignalProtocol):
+ """
+ A win32-specific process protocol that handles C{processEnded}
+ differently: processes should exit with exit code 1.
+ """
+
+ def processEnded(self, reason):
+ """
+ Callback C{self.deferred} with C{None} if C{reason} is a
+ L{error.ProcessTerminated} failure with C{exitCode} set to 1.
+ Otherwise, errback with a C{ValueError} describing the problem.
+ """
+ if not reason.check(error.ProcessTerminated):
+ return self.deferred.errback(
+ ValueError("wrong termination: %s" % (reason,)))
+ v = reason.value
+ if v.exitCode != 1:
+ return self.deferred.errback(
+ ValueError("Wrong exit code: %s" % (reason.exitCode,)))
+ self.deferred.callback(None)
+
+
+
+class Win32ProcessTestCase(unittest.TestCase):
+ """
+ Test process programs that are packaged with twisted.
+ """
+
+ def testStdinReader(self):
+ pyExe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_stdinreader.py")
+ p = Accumulator()
+ d = p.endedDeferred = defer.Deferred()
+ reactor.spawnProcess(p, pyExe, [pyExe, "-u", scriptPath], env=None,
+ path=None)
+ p.transport.write("hello, world")
+ p.transport.closeStdin()
+
+ def processEnded(ign):
+ self.assertEqual(p.errF.getvalue(), "err\nerr\n")
+ self.assertEqual(p.outF.getvalue(), "out\nhello, world\nout\n")
+ return d.addCallback(processEnded)
+
+
+ def testBadArgs(self):
+ pyExe = sys.executable
+ pyArgs = [pyExe, "-u", "-c", "print 'hello'"]
+ p = Accumulator()
+ self.assertRaises(ValueError,
+ reactor.spawnProcess, p, pyExe, pyArgs, uid=1)
+ self.assertRaises(ValueError,
+ reactor.spawnProcess, p, pyExe, pyArgs, gid=1)
+ self.assertRaises(ValueError,
+ reactor.spawnProcess, p, pyExe, pyArgs, usePTY=1)
+ self.assertRaises(ValueError,
+ reactor.spawnProcess, p, pyExe, pyArgs, childFDs={1:'r'})
+
+
+ def _testSignal(self, sig):
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_signal.py")
+ d = defer.Deferred()
+ p = Win32SignalProtocol(d, sig)
+ reactor.spawnProcess(p, exe, [exe, "-u", scriptPath], env=None)
+ return d
+
+
+ def test_signalTERM(self):
+ """
+ Sending the SIGTERM signal terminates a created process, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} attribute set to 1.
+ """
+ return self._testSignal('TERM')
+
+
+ def test_signalINT(self):
+ """
+ Sending the SIGINT signal terminates a created process, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} attribute set to 1.
+ """
+ return self._testSignal('INT')
+
+
+ def test_signalKILL(self):
+ """
+ Sending the SIGKILL signal terminates a created process, and
+ C{processEnded} is called with a L{error.ProcessTerminated} instance
+ with the C{exitCode} attribute set to 1.
+ """
+ return self._testSignal('KILL')
+
+
+ def test_closeHandles(self):
+ """
+ The win32 handles should be properly closed when the process exits.
+ """
+ import win32api
+
+ connected = defer.Deferred()
+ ended = defer.Deferred()
+
+ class SimpleProtocol(protocol.ProcessProtocol):
+ """
+ A protocol that fires deferreds when connected and disconnected.
+ """
+ def makeConnection(self, transport):
+ connected.callback(transport)
+
+ def processEnded(self, reason):
+ ended.callback(None)
+
+ p = SimpleProtocol()
+
+ pyExe = sys.executable
+ pyArgs = [pyExe, "-u", "-c", "print 'hello'"]
+ proc = reactor.spawnProcess(p, pyExe, pyArgs)
+
+ def cbConnected(transport):
+ self.assertIdentical(transport, proc)
+ # perform a basic validity test on the handles
+ win32api.GetHandleInformation(proc.hProcess)
+ win32api.GetHandleInformation(proc.hThread)
+ # And save their values for later
+ self.hProcess = proc.hProcess
+ self.hThread = proc.hThread
+ connected.addCallback(cbConnected)
+
+ def checkTerminated(ignored):
+ # The attributes on the process object must be reset...
+ self.assertIdentical(proc.pid, None)
+ self.assertIdentical(proc.hProcess, None)
+ self.assertIdentical(proc.hThread, None)
+ # ...and the handles must be closed.
+ self.assertRaises(win32api.error,
+ win32api.GetHandleInformation, self.hProcess)
+ self.assertRaises(win32api.error,
+ win32api.GetHandleInformation, self.hThread)
+ ended.addCallback(checkTerminated)
+
+ return defer.gatherResults([connected, ended])
+
+
+
+class Win32UnicodeEnvironmentTest(unittest.TestCase):
+ """
+ Tests for Unicode environment on Windows
+ """
+ goodKey = u'UNICODE'
+ goodValue = u'UNICODE'
+
+ def test_encodableUnicodeEnvironment(self):
+ """
+ Test C{os.environ} (inherited by every subprocess on Windows) that
+ contains an ascii-encodable Unicode string. This is different from
+ passing Unicode environment explicitly to spawnProcess (which is not
+ supported).
+ """
+ os.environ[self.goodKey] = self.goodValue
+ self.addCleanup(operator.delitem, os.environ, self.goodKey)
+
+ p = GetEnvironmentDictionary.run(reactor, [], {})
+ def gotEnvironment(environ):
+ self.assertEqual(
+ environ[self.goodKey.encode('ascii')],
+ self.goodValue.encode('ascii'))
+ return p.getResult().addCallback(gotEnvironment)
+
+
+
+class Dumbwin32procPidTest(unittest.TestCase):
+ """
+ Simple test for the pid attribute of Process on win32.
+ """
+
+ def test_pid(self):
+ """
+ Launch process with mock win32process. The only mock aspect of this
+ module is that the pid of the process created will always be 42.
+ """
+ from twisted.internet import _dumbwin32proc
+ from twisted.test import mock_win32process
+ self.patch(_dumbwin32proc, "win32process", mock_win32process)
+ exe = sys.executable
+ scriptPath = util.sibpath(__file__, "process_cmdline.py")
+
+ d = defer.Deferred()
+ processProto = TrivialProcessProtocol(d)
+ comspec = str(os.environ["COMSPEC"])
+ cmd = [comspec, "/c", exe, scriptPath]
+
+ p = _dumbwin32proc.Process(reactor,
+ processProto,
+ None,
+ cmd,
+ {},
+ None)
+ self.assertEqual(42, p.pid)
+ self.assertEqual("<Process pid=42>", repr(p))
+
+ def pidCompleteCb(result):
+ self.assertEqual(None, p.pid)
+ return d.addCallback(pidCompleteCb)
+
+
+
+class UtilTestCase(unittest.TestCase):
+ """
+ Tests for process-related helper functions (currently only
+ L{procutils.which}.
+ """
+ def setUp(self):
+ """
+ Create several directories and files, some of which are executable
+ and some of which are not. Save the current PATH setting.
+ """
+ j = os.path.join
+
+ base = self.mktemp()
+
+ self.foo = j(base, "foo")
+ self.baz = j(base, "baz")
+ self.foobar = j(self.foo, "bar")
+ self.foobaz = j(self.foo, "baz")
+ self.bazfoo = j(self.baz, "foo")
+ self.bazbar = j(self.baz, "bar")
+
+ for d in self.foobar, self.foobaz, self.bazfoo, self.bazbar:
+ os.makedirs(d)
+
+ for name, mode in [(j(self.foobaz, "executable"), 0700),
+ (j(self.foo, "executable"), 0700),
+ (j(self.bazfoo, "executable"), 0700),
+ (j(self.bazfoo, "executable.bin"), 0700),
+ (j(self.bazbar, "executable"), 0)]:
+ f = file(name, "w")
+ f.close()
+ os.chmod(name, mode)
+
+ self.oldPath = os.environ.get('PATH', None)
+ os.environ['PATH'] = os.pathsep.join((
+ self.foobar, self.foobaz, self.bazfoo, self.bazbar))
+
+
+ def tearDown(self):
+ """
+ Restore the saved PATH setting, and set all created files readable
+ again so that they can be deleted easily.
+ """
+ os.chmod(os.path.join(self.bazbar, "executable"), stat.S_IWUSR)
+ if self.oldPath is None:
+ try:
+ del os.environ['PATH']
+ except KeyError:
+ pass
+ else:
+ os.environ['PATH'] = self.oldPath
+
+
+ def test_whichWithoutPATH(self):
+ """
+ Test that if C{os.environ} does not have a C{'PATH'} key,
+ L{procutils.which} returns an empty list.
+ """
+ del os.environ['PATH']
+ self.assertEqual(procutils.which("executable"), [])
+
+
+ def testWhich(self):
+ j = os.path.join
+ paths = procutils.which("executable")
+ expectedPaths = [j(self.foobaz, "executable"),
+ j(self.bazfoo, "executable")]
+ if runtime.platform.isWindows():
+ expectedPaths.append(j(self.bazbar, "executable"))
+ self.assertEqual(paths, expectedPaths)
+
+
+ def testWhichPathExt(self):
+ j = os.path.join
+ old = os.environ.get('PATHEXT', None)
+ os.environ['PATHEXT'] = os.pathsep.join(('.bin', '.exe', '.sh'))
+ try:
+ paths = procutils.which("executable")
+ finally:
+ if old is None:
+ del os.environ['PATHEXT']
+ else:
+ os.environ['PATHEXT'] = old
+ expectedPaths = [j(self.foobaz, "executable"),
+ j(self.bazfoo, "executable"),
+ j(self.bazfoo, "executable.bin")]
+ if runtime.platform.isWindows():
+ expectedPaths.append(j(self.bazbar, "executable"))
+ self.assertEqual(paths, expectedPaths)
+
+
+
+class ClosingPipesProcessProtocol(protocol.ProcessProtocol):
+ output = ''
+ errput = ''
+
+ def __init__(self, outOrErr):
+ self.deferred = defer.Deferred()
+ self.outOrErr = outOrErr
+
+ def processEnded(self, reason):
+ self.deferred.callback(reason)
+
+ def outReceived(self, data):
+ self.output += data
+
+ def errReceived(self, data):
+ self.errput += data
+
+
+
+class ClosingPipes(unittest.TestCase):
+
+ def doit(self, fd):
+ """
+ Create a child process and close one of its output descriptors using
+ L{IProcessTransport.closeStdout} or L{IProcessTransport.closeStderr}.
+ Return a L{Deferred} which fires after verifying that the descriptor was
+ really closed.
+ """
+ p = ClosingPipesProcessProtocol(True)
+ self.assertFailure(p.deferred, error.ProcessTerminated)
+ p.deferred.addCallback(self._endProcess, p)
+ reactor.spawnProcess(
+ p, sys.executable, [
+ sys.executable, '-u', '-c',
+ 'raw_input()\n'
+ 'import sys, os, time\n'
+ # Give the system a bit of time to notice the closed
+ # descriptor. Another option would be to poll() for HUP
+ # instead of relying on an os.write to fail with SIGPIPE.
+ # However, that wouldn't work on OS X (or Windows?).
+ 'for i in range(1000):\n'
+ ' os.write(%d, "foo\\n")\n'
+ ' time.sleep(0.01)\n'
+ 'sys.exit(42)\n' % (fd,)
+ ],
+ env=None)
+
+ if fd == 1:
+ p.transport.closeStdout()
+ elif fd == 2:
+ p.transport.closeStderr()
+ else:
+ raise RuntimeError
+
+ # Give the close time to propagate
+ p.transport.write('go\n')
+
+ # make the buggy case not hang
+ p.transport.closeStdin()
+ return p.deferred
+
+
+ def _endProcess(self, reason, p):
+ """
+ Check that a failed write prevented the process from getting to its
+ custom exit code.
+ """
+ # child must not get past that write without raising
+ self.assertNotEquals(
+ reason.exitCode, 42, 'process reason was %r' % reason)
+ self.assertEqual(p.output, '')
+ return p.errput
+
+
+ def test_stdout(self):
+ """
+ ProcessProtocol.transport.closeStdout actually closes the pipe.
+ """
+ d = self.doit(1)
+ def _check(errput):
+ self.assertIn('OSError', errput)
+ if runtime.platform.getType() != 'win32':
+ self.assertIn('Broken pipe', errput)
+ d.addCallback(_check)
+ return d
+
+
+ def test_stderr(self):
+ """
+ ProcessProtocol.transport.closeStderr actually closes the pipe.
+ """
+ d = self.doit(2)
+ def _check(errput):
+ # there should be no stderr open, so nothing for it to
+ # write the error to.
+ self.assertEqual(errput, '')
+ d.addCallback(_check)
+ return d
+
+
+skipMessage = "wrong platform or reactor doesn't support IReactorProcess"
+if (runtime.platform.getType() != 'posix') or (not interfaces.IReactorProcess(reactor, None)):
+ PosixProcessTestCase.skip = skipMessage
+ PosixProcessTestCasePTY.skip = skipMessage
+ TestTwoProcessesPosix.skip = skipMessage
+ FDTest.skip = skipMessage
+
+if (runtime.platform.getType() != 'win32') or (not interfaces.IReactorProcess(reactor, None)):
+ Win32ProcessTestCase.skip = skipMessage
+ TestTwoProcessesNonPosix.skip = skipMessage
+ Dumbwin32procPidTest.skip = skipMessage
+ Win32UnicodeEnvironmentTest.skip = skipMessage
+
+if not interfaces.IReactorProcess(reactor, None):
+ ProcessTestCase.skip = skipMessage
+ ClosingPipes.skip = skipMessage
+
diff --git a/twisted/test/test_protocols.py b/twisted/test/test_protocols.py
new file mode 100644
index 0000000..0e03ad9
--- /dev/null
+++ b/twisted/test/test_protocols.py
@@ -0,0 +1,1260 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.protocols package.
+"""
+
+import struct
+
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+from twisted.protocols import basic, wire, portforward
+from twisted.internet import reactor, protocol, defer, task, error, address
+from twisted.internet.interfaces import IProtocolFactory, ILoggingContext
+from twisted.test import proto_helpers
+
+
+class LineTester(basic.LineReceiver):
+ """
+ A line receiver that parses data received and make actions on some tokens.
+
+ @type delimiter: C{str}
+ @ivar delimiter: character used between received lines.
+ @type MAX_LENGTH: C{int}
+ @ivar MAX_LENGTH: size of a line when C{lineLengthExceeded} will be called.
+ @type clock: L{twisted.internet.task.Clock}
+ @ivar clock: clock simulating reactor callLater. Pass it to constructor if
+ you want to use the pause/rawpause functionalities.
+ """
+
+ delimiter = '\n'
+ MAX_LENGTH = 64
+
+ def __init__(self, clock=None):
+ """
+ If given, use a clock to make callLater calls.
+ """
+ self.clock = clock
+
+
+ def connectionMade(self):
+ """
+ Create/clean data received on connection.
+ """
+ self.received = []
+
+
+ def lineReceived(self, line):
+ """
+ Receive line and make some action for some tokens: pause, rawpause,
+ stop, len, produce, unproduce.
+ """
+ self.received.append(line)
+ if line == '':
+ self.setRawMode()
+ elif line == 'pause':
+ self.pauseProducing()
+ self.clock.callLater(0, self.resumeProducing)
+ elif line == 'rawpause':
+ self.pauseProducing()
+ self.setRawMode()
+ self.received.append('')
+ self.clock.callLater(0, self.resumeProducing)
+ elif line == 'stop':
+ self.stopProducing()
+ elif line[:4] == 'len ':
+ self.length = int(line[4:])
+ elif line.startswith('produce'):
+ self.transport.registerProducer(self, False)
+ elif line.startswith('unproduce'):
+ self.transport.unregisterProducer()
+
+
+ def rawDataReceived(self, data):
+ """
+ Read raw data, until the quantity specified by a previous 'len' line is
+ reached.
+ """
+ data, rest = data[:self.length], data[self.length:]
+ self.length = self.length - len(data)
+ self.received[-1] = self.received[-1] + data
+ if self.length == 0:
+ self.setLineMode(rest)
+
+
+ def lineLengthExceeded(self, line):
+ """
+ Adjust line mode when long lines received.
+ """
+ if len(line) > self.MAX_LENGTH + 1:
+ self.setLineMode(line[self.MAX_LENGTH + 1:])
+
+
+
+class LineOnlyTester(basic.LineOnlyReceiver):
+ """
+ A buffering line only receiver.
+ """
+ delimiter = '\n'
+ MAX_LENGTH = 64
+
+ def connectionMade(self):
+ """
+ Create/clean data received on connection.
+ """
+ self.received = []
+
+
+ def lineReceived(self, line):
+ """
+ Save received data.
+ """
+ self.received.append(line)
+
+
+
+class FactoryTests(unittest.TestCase):
+ """
+ Tests for L{protocol.Factory}.
+ """
+ def test_interfaces(self):
+ """
+ L{protocol.Factory} instances provide both L{IProtocolFactory} and
+ L{ILoggingContext}.
+ """
+ factory = protocol.Factory()
+ self.assertTrue(verifyObject(IProtocolFactory, factory))
+ self.assertTrue(verifyObject(ILoggingContext, factory))
+
+
+ def test_logPrefix(self):
+ """
+ L{protocol.Factory.logPrefix} returns the name of the factory class.
+ """
+ class SomeKindOfFactory(protocol.Factory):
+ pass
+
+ self.assertEqual("SomeKindOfFactory", SomeKindOfFactory().logPrefix())
+
+
+
+class WireTestCase(unittest.TestCase):
+ """
+ Test wire protocols.
+ """
+
+ def test_echo(self):
+ """
+ Test wire.Echo protocol: send some data and check it send it back.
+ """
+ t = proto_helpers.StringTransport()
+ a = wire.Echo()
+ a.makeConnection(t)
+ a.dataReceived("hello")
+ a.dataReceived("world")
+ a.dataReceived("how")
+ a.dataReceived("are")
+ a.dataReceived("you")
+ self.assertEqual(t.value(), "helloworldhowareyou")
+
+
+ def test_who(self):
+ """
+ Test wire.Who protocol.
+ """
+ t = proto_helpers.StringTransport()
+ a = wire.Who()
+ a.makeConnection(t)
+ self.assertEqual(t.value(), "root\r\n")
+
+
+ def test_QOTD(self):
+ """
+ Test wire.QOTD protocol.
+ """
+ t = proto_helpers.StringTransport()
+ a = wire.QOTD()
+ a.makeConnection(t)
+ self.assertEqual(t.value(),
+ "An apple a day keeps the doctor away.\r\n")
+
+
+ def test_discard(self):
+ """
+ Test wire.Discard protocol.
+ """
+ t = proto_helpers.StringTransport()
+ a = wire.Discard()
+ a.makeConnection(t)
+ a.dataReceived("hello")
+ a.dataReceived("world")
+ a.dataReceived("how")
+ a.dataReceived("are")
+ a.dataReceived("you")
+ self.assertEqual(t.value(), "")
+
+
+
+class LineReceiverTestCase(unittest.TestCase):
+ """
+ Test LineReceiver, using the C{LineTester} wrapper.
+ """
+ buffer = '''\
+len 10
+
+0123456789len 5
+
+1234
+len 20
+foo 123
+
+0123456789
+012345678len 0
+foo 5
+
+1234567890123456789012345678901234567890123456789012345678901234567890
+len 1
+
+a'''
+
+ output = ['len 10', '0123456789', 'len 5', '1234\n',
+ 'len 20', 'foo 123', '0123456789\n012345678',
+ 'len 0', 'foo 5', '', '67890', 'len 1', 'a']
+
+ def testBuffer(self):
+ """
+ Test buffering for different packet size, checking received matches
+ expected data.
+ """
+ for packet_size in range(1, 10):
+ t = proto_helpers.StringIOWithoutClosing()
+ a = LineTester()
+ a.makeConnection(protocol.FileWrapper(t))
+ for i in range(len(self.buffer)/packet_size + 1):
+ s = self.buffer[i*packet_size:(i+1)*packet_size]
+ a.dataReceived(s)
+ self.assertEqual(self.output, a.received)
+
+
+ pause_buf = 'twiddle1\ntwiddle2\npause\ntwiddle3\n'
+
+ pause_output1 = ['twiddle1', 'twiddle2', 'pause']
+ pause_output2 = pause_output1+['twiddle3']
+
+
+ def test_pausing(self):
+ """
+ Test pause inside data receiving. It uses fake clock to see if
+ pausing/resuming work.
+ """
+ for packet_size in range(1, 10):
+ t = proto_helpers.StringIOWithoutClosing()
+ clock = task.Clock()
+ a = LineTester(clock)
+ a.makeConnection(protocol.FileWrapper(t))
+ for i in range(len(self.pause_buf)/packet_size + 1):
+ s = self.pause_buf[i*packet_size:(i+1)*packet_size]
+ a.dataReceived(s)
+ self.assertEqual(self.pause_output1, a.received)
+ clock.advance(0)
+ self.assertEqual(self.pause_output2, a.received)
+
+ rawpause_buf = 'twiddle1\ntwiddle2\nlen 5\nrawpause\n12345twiddle3\n'
+
+ rawpause_output1 = ['twiddle1', 'twiddle2', 'len 5', 'rawpause', '']
+ rawpause_output2 = ['twiddle1', 'twiddle2', 'len 5', 'rawpause', '12345',
+ 'twiddle3']
+
+
+ def test_rawPausing(self):
+ """
+ Test pause inside raw date receiving.
+ """
+ for packet_size in range(1, 10):
+ t = proto_helpers.StringIOWithoutClosing()
+ clock = task.Clock()
+ a = LineTester(clock)
+ a.makeConnection(protocol.FileWrapper(t))
+ for i in range(len(self.rawpause_buf)/packet_size + 1):
+ s = self.rawpause_buf[i*packet_size:(i+1)*packet_size]
+ a.dataReceived(s)
+ self.assertEqual(self.rawpause_output1, a.received)
+ clock.advance(0)
+ self.assertEqual(self.rawpause_output2, a.received)
+
+ stop_buf = 'twiddle1\ntwiddle2\nstop\nmore\nstuff\n'
+
+ stop_output = ['twiddle1', 'twiddle2', 'stop']
+
+
+ def test_stopProducing(self):
+ """
+ Test stop inside producing.
+ """
+ for packet_size in range(1, 10):
+ t = proto_helpers.StringIOWithoutClosing()
+ a = LineTester()
+ a.makeConnection(protocol.FileWrapper(t))
+ for i in range(len(self.stop_buf)/packet_size + 1):
+ s = self.stop_buf[i*packet_size:(i+1)*packet_size]
+ a.dataReceived(s)
+ self.assertEqual(self.stop_output, a.received)
+
+
+ def test_lineReceiverAsProducer(self):
+ """
+ Test produce/unproduce in receiving.
+ """
+ a = LineTester()
+ t = proto_helpers.StringIOWithoutClosing()
+ a.makeConnection(protocol.FileWrapper(t))
+ a.dataReceived('produce\nhello world\nunproduce\ngoodbye\n')
+ self.assertEqual(a.received,
+ ['produce', 'hello world', 'unproduce', 'goodbye'])
+
+
+ def test_clearLineBuffer(self):
+ """
+ L{LineReceiver.clearLineBuffer} removes all buffered data and returns
+ it as a C{str} and can be called from beneath C{dataReceived}.
+ """
+ class ClearingReceiver(basic.LineReceiver):
+ def lineReceived(self, line):
+ self.line = line
+ self.rest = self.clearLineBuffer()
+
+ protocol = ClearingReceiver()
+ protocol.dataReceived('foo\r\nbar\r\nbaz')
+ self.assertEqual(protocol.line, 'foo')
+ self.assertEqual(protocol.rest, 'bar\r\nbaz')
+
+ # Deliver another line to make sure the previously buffered data is
+ # really gone.
+ protocol.dataReceived('quux\r\n')
+ self.assertEqual(protocol.line, 'quux')
+ self.assertEqual(protocol.rest, '')
+
+
+
+class LineOnlyReceiverTestCase(unittest.TestCase):
+ """
+ Test line only receiveer.
+ """
+ buffer = """foo
+ bleakness
+ desolation
+ plastic forks
+ """
+
+ def test_buffer(self):
+ """
+ Test buffering over line protocol: data received should match buffer.
+ """
+ t = proto_helpers.StringTransport()
+ a = LineOnlyTester()
+ a.makeConnection(t)
+ for c in self.buffer:
+ a.dataReceived(c)
+ self.assertEqual(a.received, self.buffer.split('\n')[:-1])
+
+
+ def test_lineTooLong(self):
+ """
+ Test sending a line too long: it should close the connection.
+ """
+ t = proto_helpers.StringTransport()
+ a = LineOnlyTester()
+ a.makeConnection(t)
+ res = a.dataReceived('x'*200)
+ self.assertIsInstance(res, error.ConnectionLost)
+
+
+
+class TestMixin:
+
+ def connectionMade(self):
+ self.received = []
+
+
+ def stringReceived(self, s):
+ self.received.append(s)
+
+ MAX_LENGTH = 50
+ closed = 0
+
+
+ def connectionLost(self, reason):
+ self.closed = 1
+
+
+
+class TestNetstring(TestMixin, basic.NetstringReceiver):
+
+ def stringReceived(self, s):
+ self.received.append(s)
+ self.transport.write(s)
+
+
+
+class LPTestCaseMixin:
+
+ illegalStrings = []
+ protocol = None
+
+
+ def getProtocol(self):
+ """
+ Return a new instance of C{self.protocol} connected to a new instance
+ of L{proto_helpers.StringTransport}.
+ """
+ t = proto_helpers.StringTransport()
+ a = self.protocol()
+ a.makeConnection(t)
+ return a
+
+
+ def test_illegal(self):
+ """
+ Assert that illegal strings cause the transport to be closed.
+ """
+ for s in self.illegalStrings:
+ r = self.getProtocol()
+ for c in s:
+ r.dataReceived(c)
+ self.assertTrue(r.transport.disconnecting)
+
+
+
+class NetstringReceiverTestCase(unittest.TestCase, LPTestCaseMixin):
+
+ strings = ['hello', 'world', 'how', 'are', 'you123', ':today', "a"*515]
+
+ illegalStrings = [
+ '9999999999999999999999', 'abc', '4:abcde',
+ '51:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,',]
+
+ protocol = TestNetstring
+
+ def setUp(self):
+ self.transport = proto_helpers.StringTransport()
+ self.netstringReceiver = TestNetstring()
+ self.netstringReceiver.makeConnection(self.transport)
+
+
+ def test_buffer(self):
+ """
+ Strings can be received in chunks of different lengths.
+ """
+ for packet_size in range(1, 10):
+ t = proto_helpers.StringTransport()
+ a = TestNetstring()
+ a.MAX_LENGTH = 699
+ a.makeConnection(t)
+ for s in self.strings:
+ a.sendString(s)
+ out = t.value()
+ for i in range(len(out)/packet_size + 1):
+ s = out[i*packet_size:(i+1)*packet_size]
+ if s:
+ a.dataReceived(s)
+ self.assertEqual(a.received, self.strings)
+
+
+ def test_sendNonStrings(self):
+ """
+ L{basic.NetstringReceiver.sendString} will send objects that are not
+ strings by sending their string representation according to str().
+ """
+ nonStrings = [ [], { 1 : 'a', 2 : 'b' }, ['a', 'b', 'c'], 673,
+ (12, "fine", "and", "you?") ]
+ a = TestNetstring()
+ t = proto_helpers.StringTransport()
+ a.MAX_LENGTH = 100
+ a.makeConnection(t)
+ for s in nonStrings:
+ a.sendString(s)
+ out = t.value()
+ t.clear()
+ length = out[:out.find(":")]
+ data = out[out.find(":") + 1:-1] #[:-1] to ignore the trailing ","
+ self.assertEqual(int(length), len(str(s)))
+ self.assertEqual(data, str(s))
+
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_sendNonStrings])
+ self.assertEqual(len(warnings), 5)
+ self.assertEqual(
+ warnings[0]["message"],
+ "Data passed to sendString() must be a string. Non-string support "
+ "is deprecated since Twisted 10.0")
+ self.assertEqual(
+ warnings[0]['category'],
+ DeprecationWarning)
+
+
+ def test_receiveEmptyNetstring(self):
+ """
+ Empty netstrings (with length '0') can be received.
+ """
+ self.netstringReceiver.dataReceived("0:,")
+ self.assertEqual(self.netstringReceiver.received, [""])
+
+
+ def test_receiveOneCharacter(self):
+ """
+ One-character netstrings can be received.
+ """
+ self.netstringReceiver.dataReceived("1:a,")
+ self.assertEqual(self.netstringReceiver.received, ["a"])
+
+
+ def test_receiveTwoCharacters(self):
+ """
+ Two-character netstrings can be received.
+ """
+ self.netstringReceiver.dataReceived("2:ab,")
+ self.assertEqual(self.netstringReceiver.received, ["ab"])
+
+
+ def test_receiveNestedNetstring(self):
+ """
+ Netstrings with embedded netstrings. This test makes sure that
+ the parser does not become confused about the ',' and ':'
+ characters appearing inside the data portion of the netstring.
+ """
+ self.netstringReceiver.dataReceived("4:1:a,,")
+ self.assertEqual(self.netstringReceiver.received, ["1:a,"])
+
+
+ def test_moreDataThanSpecified(self):
+ """
+ Netstrings containing more data than expected are refused.
+ """
+ self.netstringReceiver.dataReceived("2:aaa,")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_moreDataThanSpecifiedBorderCase(self):
+ """
+ Netstrings that should be empty according to their length
+ specification are refused if they contain data.
+ """
+ self.netstringReceiver.dataReceived("0:a,")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_missingNumber(self):
+ """
+ Netstrings without leading digits that specify the length
+ are refused.
+ """
+ self.netstringReceiver.dataReceived(":aaa,")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_missingColon(self):
+ """
+ Netstrings without a colon between length specification and
+ data are refused.
+ """
+ self.netstringReceiver.dataReceived("3aaa,")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_missingNumberAndColon(self):
+ """
+ Netstrings that have no leading digits nor a colon are
+ refused.
+ """
+ self.netstringReceiver.dataReceived("aaa,")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_onlyData(self):
+ """
+ Netstrings consisting only of data are refused.
+ """
+ self.netstringReceiver.dataReceived("aaa")
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_receiveNetstringPortions_1(self):
+ """
+ Netstrings can be received in two portions.
+ """
+ self.netstringReceiver.dataReceived("4:aa")
+ self.netstringReceiver.dataReceived("aa,")
+ self.assertEqual(self.netstringReceiver.received, ["aaaa"])
+ self.assertTrue(self.netstringReceiver._payloadComplete())
+
+
+ def test_receiveNetstringPortions_2(self):
+ """
+ Netstrings can be received in more than two portions, even if
+ the length specification is split across two portions.
+ """
+ for part in ["1", "0:01234", "56789", ","]:
+ self.netstringReceiver.dataReceived(part)
+ self.assertEqual(self.netstringReceiver.received, ["0123456789"])
+
+
+ def test_receiveNetstringPortions_3(self):
+ """
+ Netstrings can be received one character at a time.
+ """
+ for part in "2:ab,":
+ self.netstringReceiver.dataReceived(part)
+ self.assertEqual(self.netstringReceiver.received, ["ab"])
+
+
+ def test_receiveTwoNetstrings(self):
+ """
+ A stream of two netstrings can be received in two portions,
+ where the first portion contains the complete first netstring
+ and the length specification of the second netstring.
+ """
+ self.netstringReceiver.dataReceived("1:a,1")
+ self.assertTrue(self.netstringReceiver._payloadComplete())
+ self.assertEqual(self.netstringReceiver.received, ["a"])
+ self.netstringReceiver.dataReceived(":b,")
+ self.assertEqual(self.netstringReceiver.received, ["a", "b"])
+
+
+ def test_maxReceiveLimit(self):
+ """
+ Netstrings with a length specification exceeding the specified
+ C{MAX_LENGTH} are refused.
+ """
+ tooLong = self.netstringReceiver.MAX_LENGTH + 1
+ self.netstringReceiver.dataReceived("%s:%s" %
+ (tooLong, "a" * tooLong))
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_consumeLength(self):
+ """
+ C{_consumeLength} returns the expected length of the
+ netstring, including the trailing comma.
+ """
+ self.netstringReceiver._remainingData = "12:"
+ self.netstringReceiver._consumeLength()
+ self.assertEqual(self.netstringReceiver._expectedPayloadSize, 13)
+
+
+ def test_consumeLengthBorderCase1(self):
+ """
+ C{_consumeLength} works as expected if the length specification
+ contains the value of C{MAX_LENGTH} (border case).
+ """
+ self.netstringReceiver._remainingData = "12:"
+ self.netstringReceiver.MAX_LENGTH = 12
+ self.netstringReceiver._consumeLength()
+ self.assertEqual(self.netstringReceiver._expectedPayloadSize, 13)
+
+
+ def test_consumeLengthBorderCase2(self):
+ """
+ C{_consumeLength} raises a L{basic.NetstringParseError} if
+ the length specification exceeds the value of C{MAX_LENGTH}
+ by 1 (border case).
+ """
+ self.netstringReceiver._remainingData = "12:"
+ self.netstringReceiver.MAX_LENGTH = 11
+ self.assertRaises(basic.NetstringParseError,
+ self.netstringReceiver._consumeLength)
+
+
+ def test_consumeLengthBorderCase3(self):
+ """
+ C{_consumeLength} raises a L{basic.NetstringParseError} if
+ the length specification exceeds the value of C{MAX_LENGTH}
+ by more than 1.
+ """
+ self.netstringReceiver._remainingData = "1000:"
+ self.netstringReceiver.MAX_LENGTH = 11
+ self.assertRaises(basic.NetstringParseError,
+ self.netstringReceiver._consumeLength)
+
+
+ def test_deprecatedModuleAttributes(self):
+ """
+ Accessing one of the old module attributes used by the
+ NetstringReceiver parser emits a deprecation warning.
+ """
+ basic.LENGTH, basic.DATA, basic.COMMA, basic.NUMBER
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedModuleAttributes])
+
+ self.assertEqual(len(warnings), 4)
+ for warning in warnings:
+ self.assertEqual(warning['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ ("twisted.protocols.basic.LENGTH was deprecated in Twisted 10.2.0: "
+ "NetstringReceiver parser state is private."))
+ self.assertEqual(
+ warnings[1]['message'],
+ ("twisted.protocols.basic.DATA was deprecated in Twisted 10.2.0: "
+ "NetstringReceiver parser state is private."))
+ self.assertEqual(
+ warnings[2]['message'],
+ ("twisted.protocols.basic.COMMA was deprecated in Twisted 10.2.0: "
+ "NetstringReceiver parser state is private."))
+ self.assertEqual(
+ warnings[3]['message'],
+ ("twisted.protocols.basic.NUMBER was deprecated in Twisted 10.2.0: "
+ "NetstringReceiver parser state is private."))
+
+
+
+class IntNTestCaseMixin(LPTestCaseMixin):
+ """
+ TestCase mixin for int-prefixed protocols.
+ """
+
+ protocol = None
+ strings = None
+ illegalStrings = None
+ partialStrings = None
+
+ def test_receive(self):
+ """
+ Test receiving data find the same data send.
+ """
+ r = self.getProtocol()
+ for s in self.strings:
+ for c in struct.pack(r.structFormat,len(s)) + s:
+ r.dataReceived(c)
+ self.assertEqual(r.received, self.strings)
+
+
+ def test_partial(self):
+ """
+ Send partial data, nothing should be definitely received.
+ """
+ for s in self.partialStrings:
+ r = self.getProtocol()
+ for c in s:
+ r.dataReceived(c)
+ self.assertEqual(r.received, [])
+
+
+ def test_send(self):
+ """
+ Test sending data over protocol.
+ """
+ r = self.getProtocol()
+ r.sendString("b" * 16)
+ self.assertEqual(r.transport.value(),
+ struct.pack(r.structFormat, 16) + "b" * 16)
+
+
+ def test_lengthLimitExceeded(self):
+ """
+ When a length prefix is received which is greater than the protocol's
+ C{MAX_LENGTH} attribute, the C{lengthLimitExceeded} method is called
+ with the received length prefix.
+ """
+ length = []
+ r = self.getProtocol()
+ r.lengthLimitExceeded = length.append
+ r.MAX_LENGTH = 10
+ r.dataReceived(struct.pack(r.structFormat, 11))
+ self.assertEqual(length, [11])
+
+
+ def test_longStringNotDelivered(self):
+ """
+ If a length prefix for a string longer than C{MAX_LENGTH} is delivered
+ to C{dataReceived} at the same time as the entire string, the string is
+ not passed to C{stringReceived}.
+ """
+ r = self.getProtocol()
+ r.MAX_LENGTH = 10
+ r.dataReceived(
+ struct.pack(r.structFormat, 11) + 'x' * 11)
+ self.assertEqual(r.received, [])
+
+
+
+class RecvdAttributeMixin(object):
+ """
+ Mixin defining tests for string receiving protocols with a C{recvd}
+ attribute which should be settable by application code, to be combined with
+ L{IntNTestCaseMixin} on a L{TestCase} subclass
+ """
+
+ def makeMessage(self, protocol, data):
+ """
+ Return C{data} prefixed with message length in C{protocol.structFormat}
+ form.
+ """
+ return struct.pack(protocol.structFormat, len(data)) + data
+
+
+ def test_recvdContainsRemainingData(self):
+ """
+ In stringReceived, recvd contains the remaining data that was passed to
+ dataReceived that was not part of the current message.
+ """
+ result = []
+ r = self.getProtocol()
+ def stringReceived(receivedString):
+ result.append(r.recvd)
+ r.stringReceived = stringReceived
+ completeMessage = (struct.pack(r.structFormat, 5) + ('a' * 5))
+ incompleteMessage = (struct.pack(r.structFormat, 5) + ('b' * 4))
+ # Receive a complete message, followed by an incomplete one
+ r.dataReceived(completeMessage + incompleteMessage)
+ self.assertEquals(result, [incompleteMessage])
+
+
+ def test_recvdChanged(self):
+ """
+ In stringReceived, if recvd is changed, messages should be parsed from
+ it rather than the input to dataReceived.
+ """
+ r = self.getProtocol()
+ result = []
+ payloadC = 'c' * 5
+ messageC = self.makeMessage(r, payloadC)
+ def stringReceived(receivedString):
+ if not result:
+ r.recvd = messageC
+ result.append(receivedString)
+ r.stringReceived = stringReceived
+ payloadA = 'a' * 5
+ payloadB = 'b' * 5
+ messageA = self.makeMessage(r, payloadA)
+ messageB = self.makeMessage(r, payloadB)
+ r.dataReceived(messageA + messageB)
+ self.assertEquals(result, [payloadA, payloadC])
+
+
+ def test_switching(self):
+ """
+ Data already parsed by L{IntNStringReceiver.dataReceived} is not
+ reparsed if C{stringReceived} consumes some of the
+ L{IntNStringReceiver.recvd} buffer.
+ """
+ proto = self.getProtocol()
+ mix = []
+ SWITCH = "\x00\x00\x00\x00"
+ for s in self.strings:
+ mix.append(self.makeMessage(proto, s))
+ mix.append(SWITCH)
+
+ result = []
+ def stringReceived(receivedString):
+ result.append(receivedString)
+ proto.recvd = proto.recvd[len(SWITCH):]
+
+ proto.stringReceived = stringReceived
+ proto.dataReceived("".join(mix))
+ # Just another byte, to trigger processing of anything that might have
+ # been left in the buffer (should be nothing).
+ proto.dataReceived("\x01")
+ self.assertEqual(result, self.strings)
+ # And verify that another way
+ self.assertEqual(proto.recvd, "\x01")
+
+
+ def test_recvdInLengthLimitExceeded(self):
+ """
+ The L{IntNStringReceiver.recvd} buffer contains all data not yet
+ processed by L{IntNStringReceiver.dataReceived} if the
+ C{lengthLimitExceeded} event occurs.
+ """
+ proto = self.getProtocol()
+ DATA = "too long"
+ proto.MAX_LENGTH = len(DATA) - 1
+ message = self.makeMessage(proto, DATA)
+
+ result = []
+ def lengthLimitExceeded(length):
+ result.append(length)
+ result.append(proto.recvd)
+
+ proto.lengthLimitExceeded = lengthLimitExceeded
+ proto.dataReceived(message)
+ self.assertEqual(result[0], len(DATA))
+ self.assertEqual(result[1], message)
+
+
+
+class TestInt32(TestMixin, basic.Int32StringReceiver):
+ """
+ A L{basic.Int32StringReceiver} storing received strings in an array.
+
+ @ivar received: array holding received strings.
+ """
+
+
+
+class Int32TestCase(unittest.TestCase, IntNTestCaseMixin, RecvdAttributeMixin):
+ """
+ Test case for int32-prefixed protocol
+ """
+ protocol = TestInt32
+ strings = ["a", "b" * 16]
+ illegalStrings = ["\x10\x00\x00\x00aaaaaa"]
+ partialStrings = ["\x00\x00\x00", "hello there", ""]
+
+ def test_data(self):
+ """
+ Test specific behavior of the 32-bits length.
+ """
+ r = self.getProtocol()
+ r.sendString("foo")
+ self.assertEqual(r.transport.value(), "\x00\x00\x00\x03foo")
+ r.dataReceived("\x00\x00\x00\x04ubar")
+ self.assertEqual(r.received, ["ubar"])
+
+
+
+class TestInt16(TestMixin, basic.Int16StringReceiver):
+ """
+ A L{basic.Int16StringReceiver} storing received strings in an array.
+
+ @ivar received: array holding received strings.
+ """
+
+
+
+class Int16TestCase(unittest.TestCase, IntNTestCaseMixin, RecvdAttributeMixin):
+ """
+ Test case for int16-prefixed protocol
+ """
+ protocol = TestInt16
+ strings = ["a", "b" * 16]
+ illegalStrings = ["\x10\x00aaaaaa"]
+ partialStrings = ["\x00", "hello there", ""]
+
+ def test_data(self):
+ """
+ Test specific behavior of the 16-bits length.
+ """
+ r = self.getProtocol()
+ r.sendString("foo")
+ self.assertEqual(r.transport.value(), "\x00\x03foo")
+ r.dataReceived("\x00\x04ubar")
+ self.assertEqual(r.received, ["ubar"])
+
+
+ def test_tooLongSend(self):
+ """
+ Send too much data: that should cause an error.
+ """
+ r = self.getProtocol()
+ tooSend = "b" * (2**(r.prefixLength*8) + 1)
+ self.assertRaises(AssertionError, r.sendString, tooSend)
+
+
+
+class NewStyleTestInt16(TestInt16, object):
+ """
+ A new-style class version of TestInt16
+ """
+
+
+
+class NewStyleInt16TestCase(Int16TestCase):
+ """
+ This test case verifies that IntNStringReceiver still works when inherited
+ by a new-style class.
+ """
+ protocol = NewStyleTestInt16
+
+
+
+class TestInt8(TestMixin, basic.Int8StringReceiver):
+ """
+ A L{basic.Int8StringReceiver} storing received strings in an array.
+
+ @ivar received: array holding received strings.
+ """
+
+
+
+class Int8TestCase(unittest.TestCase, IntNTestCaseMixin, RecvdAttributeMixin):
+ """
+ Test case for int8-prefixed protocol
+ """
+ protocol = TestInt8
+ strings = ["a", "b" * 16]
+ illegalStrings = ["\x00\x00aaaaaa"]
+ partialStrings = ["\x08", "dzadz", ""]
+
+
+ def test_data(self):
+ """
+ Test specific behavior of the 8-bits length.
+ """
+ r = self.getProtocol()
+ r.sendString("foo")
+ self.assertEqual(r.transport.value(), "\x03foo")
+ r.dataReceived("\x04ubar")
+ self.assertEqual(r.received, ["ubar"])
+
+
+ def test_tooLongSend(self):
+ """
+ Send too much data: that should cause an error.
+ """
+ r = self.getProtocol()
+ tooSend = "b" * (2**(r.prefixLength*8) + 1)
+ self.assertRaises(AssertionError, r.sendString, tooSend)
+
+
+
+class OnlyProducerTransport(object):
+ # Transport which isn't really a transport, just looks like one to
+ # someone not looking very hard.
+
+ paused = False
+ disconnecting = False
+
+ def __init__(self):
+ self.data = []
+
+
+ def pauseProducing(self):
+ self.paused = True
+
+
+ def resumeProducing(self):
+ self.paused = False
+
+
+ def write(self, bytes):
+ self.data.append(bytes)
+
+
+
+class ConsumingProtocol(basic.LineReceiver):
+ # Protocol that really, really doesn't want any more bytes.
+
+ def lineReceived(self, line):
+ self.transport.write(line)
+ self.pauseProducing()
+
+
+
+class ProducerTestCase(unittest.TestCase):
+
+ def testPauseResume(self):
+ p = ConsumingProtocol()
+ t = OnlyProducerTransport()
+ p.makeConnection(t)
+
+ p.dataReceived('hello, ')
+ self.failIf(t.data)
+ self.failIf(t.paused)
+ self.failIf(p.paused)
+
+ p.dataReceived('world\r\n')
+
+ self.assertEqual(t.data, ['hello, world'])
+ self.failUnless(t.paused)
+ self.failUnless(p.paused)
+
+ p.resumeProducing()
+
+ self.failIf(t.paused)
+ self.failIf(p.paused)
+
+ p.dataReceived('hello\r\nworld\r\n')
+
+ self.assertEqual(t.data, ['hello, world', 'hello'])
+ self.failUnless(t.paused)
+ self.failUnless(p.paused)
+
+ p.resumeProducing()
+ p.dataReceived('goodbye\r\n')
+
+ self.assertEqual(t.data, ['hello, world', 'hello', 'world'])
+ self.failUnless(t.paused)
+ self.failUnless(p.paused)
+
+ p.resumeProducing()
+
+ self.assertEqual(t.data, ['hello, world', 'hello', 'world', 'goodbye'])
+ self.failUnless(t.paused)
+ self.failUnless(p.paused)
+
+ p.resumeProducing()
+
+ self.assertEqual(t.data, ['hello, world', 'hello', 'world', 'goodbye'])
+ self.failIf(t.paused)
+ self.failIf(p.paused)
+
+
+
+class TestableProxyClientFactory(portforward.ProxyClientFactory):
+ """
+ Test proxy client factory that keeps the last created protocol instance.
+
+ @ivar protoInstance: the last instance of the protocol.
+ @type protoInstance: L{portforward.ProxyClient}
+ """
+
+ def buildProtocol(self, addr):
+ """
+ Create the protocol instance and keeps track of it.
+ """
+ proto = portforward.ProxyClientFactory.buildProtocol(self, addr)
+ self.protoInstance = proto
+ return proto
+
+
+
+class TestableProxyFactory(portforward.ProxyFactory):
+ """
+ Test proxy factory that keeps the last created protocol instance.
+
+ @ivar protoInstance: the last instance of the protocol.
+ @type protoInstance: L{portforward.ProxyServer}
+
+ @ivar clientFactoryInstance: client factory used by C{protoInstance} to
+ create forward connections.
+ @type clientFactoryInstance: L{TestableProxyClientFactory}
+ """
+
+ def buildProtocol(self, addr):
+ """
+ Create the protocol instance, keeps track of it, and makes it use
+ C{clientFactoryInstance} as client factory.
+ """
+ proto = portforward.ProxyFactory.buildProtocol(self, addr)
+ self.clientFactoryInstance = TestableProxyClientFactory()
+ # Force the use of this specific instance
+ proto.clientProtocolFactory = lambda: self.clientFactoryInstance
+ self.protoInstance = proto
+ return proto
+
+
+
+class Portforwarding(unittest.TestCase):
+ """
+ Test port forwarding.
+ """
+
+ def setUp(self):
+ self.serverProtocol = wire.Echo()
+ self.clientProtocol = protocol.Protocol()
+ self.openPorts = []
+
+
+ def tearDown(self):
+ try:
+ self.proxyServerFactory.protoInstance.transport.loseConnection()
+ except AttributeError:
+ pass
+ try:
+ pi = self.proxyServerFactory.clientFactoryInstance.protoInstance
+ pi.transport.loseConnection()
+ except AttributeError:
+ pass
+ try:
+ self.clientProtocol.transport.loseConnection()
+ except AttributeError:
+ pass
+ try:
+ self.serverProtocol.transport.loseConnection()
+ except AttributeError:
+ pass
+ return defer.gatherResults(
+ [defer.maybeDeferred(p.stopListening) for p in self.openPorts])
+
+
+ def test_portforward(self):
+ """
+ Test port forwarding through Echo protocol.
+ """
+ realServerFactory = protocol.ServerFactory()
+ realServerFactory.protocol = lambda: self.serverProtocol
+ realServerPort = reactor.listenTCP(0, realServerFactory,
+ interface='127.0.0.1')
+ self.openPorts.append(realServerPort)
+ self.proxyServerFactory = TestableProxyFactory('127.0.0.1',
+ realServerPort.getHost().port)
+ proxyServerPort = reactor.listenTCP(0, self.proxyServerFactory,
+ interface='127.0.0.1')
+ self.openPorts.append(proxyServerPort)
+
+ nBytes = 1000
+ received = []
+ d = defer.Deferred()
+
+ def testDataReceived(data):
+ received.extend(data)
+ if len(received) >= nBytes:
+ self.assertEqual(''.join(received), 'x' * nBytes)
+ d.callback(None)
+
+ self.clientProtocol.dataReceived = testDataReceived
+
+ def testConnectionMade():
+ self.clientProtocol.transport.write('x' * nBytes)
+
+ self.clientProtocol.connectionMade = testConnectionMade
+
+ clientFactory = protocol.ClientFactory()
+ clientFactory.protocol = lambda: self.clientProtocol
+
+ reactor.connectTCP(
+ '127.0.0.1', proxyServerPort.getHost().port, clientFactory)
+
+ return d
+
+
+ def test_registerProducers(self):
+ """
+ The proxy client registers itself as a producer of the proxy server and
+ vice versa.
+ """
+ # create a ProxyServer instance
+ addr = address.IPv4Address('TCP', '127.0.0.1', 0)
+ server = portforward.ProxyFactory('127.0.0.1', 0).buildProtocol(addr)
+
+ # set the reactor for this test
+ reactor = proto_helpers.MemoryReactor()
+ server.reactor = reactor
+
+ # make the connection
+ serverTransport = proto_helpers.StringTransport()
+ server.makeConnection(serverTransport)
+
+ # check that the ProxyClientFactory is connecting to the backend
+ self.assertEqual(len(reactor.tcpClients), 1)
+ # get the factory instance and check it's the one we expect
+ host, port, clientFactory, timeout, _ = reactor.tcpClients[0]
+ self.assertIsInstance(clientFactory, portforward.ProxyClientFactory)
+
+ # Connect it
+ client = clientFactory.buildProtocol(addr)
+ clientTransport = proto_helpers.StringTransport()
+ client.makeConnection(clientTransport)
+
+ # check that the producers are registered
+ self.assertIdentical(clientTransport.producer, serverTransport)
+ self.assertIdentical(serverTransport.producer, clientTransport)
+ # check the streaming attribute in both transports
+ self.assertTrue(clientTransport.streaming)
+ self.assertTrue(serverTransport.streaming)
+
+
+
+class StringTransportTestCase(unittest.TestCase):
+ """
+ Test L{proto_helpers.StringTransport} helper behaviour.
+ """
+
+ def test_noUnicode(self):
+ """
+ Test that L{proto_helpers.StringTransport} doesn't accept unicode data.
+ """
+ s = proto_helpers.StringTransport()
+ self.assertRaises(TypeError, s.write, u'foo')
diff --git a/twisted/test/test_randbytes.py b/twisted/test/test_randbytes.py
new file mode 100644
index 0000000..cb0997c
--- /dev/null
+++ b/twisted/test/test_randbytes.py
@@ -0,0 +1,119 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for L{twisted.python.randbytes}.
+"""
+
+import os
+
+from twisted.trial import unittest
+from twisted.python import randbytes
+
+
+
+class SecureRandomTestCaseBase(object):
+ """
+ Base class for secureRandom test cases.
+ """
+
+ def _check(self, source):
+ """
+ The given random bytes source should return the number of bytes
+ requested each time it is called and should probably not return the
+ same bytes on two consecutive calls (although this is a perfectly
+ legitimate occurrence and rejecting it may generate a spurious failure
+ -- maybe we'll get lucky and the heat death with come first).
+ """
+ for nbytes in range(17, 25):
+ s = source(nbytes)
+ self.assertEqual(len(s), nbytes)
+ s2 = source(nbytes)
+ self.assertEqual(len(s2), nbytes)
+ # This is crude but hey
+ self.assertNotEquals(s2, s)
+
+
+
+class SecureRandomTestCase(SecureRandomTestCaseBase, unittest.TestCase):
+ """
+ Test secureRandom under normal conditions.
+ """
+
+ def test_normal(self):
+ """
+ L{randbytes.secureRandom} should return a string of the requested
+ length and make some effort to make its result otherwise unpredictable.
+ """
+ self._check(randbytes.secureRandom)
+
+
+
+class ConditionalSecureRandomTestCase(SecureRandomTestCaseBase,
+ unittest.TestCase):
+ """
+ Test random sources one by one, then remove it to.
+ """
+
+ def setUp(self):
+ """
+ Create a L{randbytes.RandomFactory} to use in the tests.
+ """
+ self.factory = randbytes.RandomFactory()
+
+
+ def errorFactory(self, nbytes):
+ """
+ A factory raising an error when a source is not available.
+ """
+ raise randbytes.SourceNotAvailable()
+
+
+ def test_osUrandom(self):
+ """
+ L{RandomFactory._osUrandom} should work as a random source whenever
+ L{os.urandom} is available.
+ """
+ self._check(self.factory._osUrandom)
+
+
+ def test_withoutAnything(self):
+ """
+ Remove all secure sources and assert it raises a failure. Then try the
+ fallback parameter.
+ """
+ self.factory._osUrandom = self.errorFactory
+ self.assertRaises(randbytes.SecureRandomNotAvailable,
+ self.factory.secureRandom, 18)
+ def wrapper():
+ return self.factory.secureRandom(18, fallback=True)
+ s = self.assertWarns(
+ RuntimeWarning,
+ "urandom unavailable - "
+ "proceeding with non-cryptographically secure random source",
+ __file__,
+ wrapper)
+ self.assertEqual(len(s), 18)
+
+
+
+class RandomTestCaseBase(SecureRandomTestCaseBase, unittest.TestCase):
+ """
+ 'Normal' random test cases.
+ """
+
+ def test_normal(self):
+ """
+ Test basic case.
+ """
+ self._check(randbytes.insecureRandom)
+
+
+ def test_withoutGetrandbits(self):
+ """
+ Test C{insecureRandom} without C{random.getrandbits}.
+ """
+ factory = randbytes.RandomFactory()
+ factory.getrandbits = None
+ self._check(factory.insecureRandom)
+
diff --git a/twisted/test/test_rebuild.py b/twisted/test/test_rebuild.py
new file mode 100644
index 0000000..dfeca9d
--- /dev/null
+++ b/twisted/test/test_rebuild.py
@@ -0,0 +1,252 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import sys, os
+import types
+
+from twisted.trial import unittest
+from twisted.python import rebuild
+
+import crash_test_dummy
+f = crash_test_dummy.foo
+
+class Foo: pass
+class Bar(Foo): pass
+class Baz(object): pass
+class Buz(Bar, Baz): pass
+
+class HashRaisesRuntimeError:
+ """
+ Things that don't hash (raise an Exception) should be ignored by the
+ rebuilder.
+
+ @ivar hashCalled: C{bool} set to True when __hash__ is called.
+ """
+ def __init__(self):
+ self.hashCalled = False
+
+ def __hash__(self):
+ self.hashCalled = True
+ raise RuntimeError('not a TypeError!')
+
+
+
+unhashableObject = None # set in test_hashException
+
+
+
+class RebuildTestCase(unittest.TestCase):
+ """
+ Simple testcase for rebuilding, to at least exercise the code.
+ """
+ def setUp(self):
+ self.libPath = self.mktemp()
+ os.mkdir(self.libPath)
+ self.fakelibPath = os.path.join(self.libPath, 'twisted_rebuild_fakelib')
+ os.mkdir(self.fakelibPath)
+ file(os.path.join(self.fakelibPath, '__init__.py'), 'w').close()
+ sys.path.insert(0, self.libPath)
+
+ def tearDown(self):
+ sys.path.remove(self.libPath)
+
+ def testFileRebuild(self):
+ from twisted.python.util import sibpath
+ import shutil, time
+ shutil.copyfile(sibpath(__file__, "myrebuilder1.py"),
+ os.path.join(self.fakelibPath, "myrebuilder.py"))
+ from twisted_rebuild_fakelib import myrebuilder
+ a = myrebuilder.A()
+ try:
+ object
+ except NameError:
+ pass
+ else:
+ from twisted.test import test_rebuild
+ b = myrebuilder.B()
+ class C(myrebuilder.B):
+ pass
+ test_rebuild.C = C
+ c = C()
+ i = myrebuilder.Inherit()
+ self.assertEqual(a.a(), 'a')
+ # necessary because the file has not "changed" if a second has not gone
+ # by in unix. This sucks, but it's not often that you'll be doing more
+ # than one reload per second.
+ time.sleep(1.1)
+ shutil.copyfile(sibpath(__file__, "myrebuilder2.py"),
+ os.path.join(self.fakelibPath, "myrebuilder.py"))
+ rebuild.rebuild(myrebuilder)
+ try:
+ object
+ except NameError:
+ pass
+ else:
+ b2 = myrebuilder.B()
+ self.assertEqual(b2.b(), 'c')
+ self.assertEqual(b.b(), 'c')
+ self.assertEqual(i.a(), 'd')
+ self.assertEqual(a.a(), 'b')
+ # more work to be done on new-style classes
+ # self.assertEqual(c.b(), 'c')
+
+ def testRebuild(self):
+ """
+ Rebuilding an unchanged module.
+ """
+ # This test would actually pass if rebuild was a no-op, but it
+ # ensures rebuild doesn't break stuff while being a less
+ # complex test than testFileRebuild.
+
+ x = crash_test_dummy.X('a')
+
+ rebuild.rebuild(crash_test_dummy, doLog=False)
+ # Instance rebuilding is triggered by attribute access.
+ x.do()
+ self.failUnlessIdentical(x.__class__, crash_test_dummy.X)
+
+ self.failUnlessIdentical(f, crash_test_dummy.foo)
+
+ def testComponentInteraction(self):
+ x = crash_test_dummy.XComponent()
+ x.setAdapter(crash_test_dummy.IX, crash_test_dummy.XA)
+ oldComponent = x.getComponent(crash_test_dummy.IX)
+ rebuild.rebuild(crash_test_dummy, 0)
+ newComponent = x.getComponent(crash_test_dummy.IX)
+
+ newComponent.method()
+
+ self.assertEqual(newComponent.__class__, crash_test_dummy.XA)
+
+ # Test that a duplicate registerAdapter is not allowed
+ from twisted.python import components
+ self.failUnlessRaises(ValueError, components.registerAdapter,
+ crash_test_dummy.XA, crash_test_dummy.X,
+ crash_test_dummy.IX)
+
+ def testUpdateInstance(self):
+ global Foo, Buz
+
+ b = Buz()
+
+ class Foo:
+ def foo(self):
+ pass
+ class Buz(Bar, Baz):
+ x = 10
+
+ rebuild.updateInstance(b)
+ assert hasattr(b, 'foo'), "Missing method on rebuilt instance"
+ assert hasattr(b, 'x'), "Missing class attribute on rebuilt instance"
+
+ def testBananaInteraction(self):
+ from twisted.python import rebuild
+ from twisted.spread import banana
+ rebuild.latestClass(banana.Banana)
+
+
+ def test_hashException(self):
+ """
+ Rebuilding something that has a __hash__ that raises a non-TypeError
+ shouldn't cause rebuild to die.
+ """
+ global unhashableObject
+ unhashableObject = HashRaisesRuntimeError()
+ def _cleanup():
+ global unhashableObject
+ unhashableObject = None
+ self.addCleanup(_cleanup)
+ rebuild.rebuild(rebuild)
+ self.assertEqual(unhashableObject.hashCalled, True)
+
+
+
+class NewStyleTestCase(unittest.TestCase):
+ """
+ Tests for rebuilding new-style classes of various sorts.
+ """
+ def setUp(self):
+ self.m = types.ModuleType('whipping')
+ sys.modules['whipping'] = self.m
+
+
+ def tearDown(self):
+ del sys.modules['whipping']
+ del self.m
+
+
+ def test_slots(self):
+ """
+ Try to rebuild a new style class with slots defined.
+ """
+ classDefinition = (
+ "class SlottedClass(object):\n"
+ " __slots__ = ['a']\n")
+
+ exec classDefinition in self.m.__dict__
+ inst = self.m.SlottedClass()
+ inst.a = 7
+ exec classDefinition in self.m.__dict__
+ rebuild.updateInstance(inst)
+ self.assertEqual(inst.a, 7)
+ self.assertIdentical(type(inst), self.m.SlottedClass)
+
+ if sys.version_info < (2, 6):
+ test_slots.skip = "__class__ assignment for class with slots is only available starting Python 2.6"
+
+
+ def test_errorSlots(self):
+ """
+ Try to rebuild a new style class with slots defined: this should fail.
+ """
+ classDefinition = (
+ "class SlottedClass(object):\n"
+ " __slots__ = ['a']\n")
+
+ exec classDefinition in self.m.__dict__
+ inst = self.m.SlottedClass()
+ inst.a = 7
+ exec classDefinition in self.m.__dict__
+ self.assertRaises(rebuild.RebuildError, rebuild.updateInstance, inst)
+
+ if sys.version_info >= (2, 6):
+ test_errorSlots.skip = "__class__ assignment for class with slots should work starting Python 2.6"
+
+
+ def test_typeSubclass(self):
+ """
+ Try to rebuild a base type subclass.
+ """
+ classDefinition = (
+ "class ListSubclass(list):\n"
+ " pass\n")
+
+ exec classDefinition in self.m.__dict__
+ inst = self.m.ListSubclass()
+ inst.append(2)
+ exec classDefinition in self.m.__dict__
+ rebuild.updateInstance(inst)
+ self.assertEqual(inst[0], 2)
+ self.assertIdentical(type(inst), self.m.ListSubclass)
+
+
+ def test_instanceSlots(self):
+ """
+ Test that when rebuilding an instance with a __slots__ attribute, it
+ fails accurately instead of giving a L{rebuild.RebuildError}.
+ """
+ classDefinition = (
+ "class NotSlottedClass(object):\n"
+ " pass\n")
+
+ exec classDefinition in self.m.__dict__
+ inst = self.m.NotSlottedClass()
+ inst.__slots__ = ['a']
+ classDefinition = (
+ "class NotSlottedClass:\n"
+ " pass\n")
+ exec classDefinition in self.m.__dict__
+ # Moving from new-style class to old-style should fail.
+ self.assertRaises(TypeError, rebuild.updateInstance, inst)
+
diff --git a/twisted/test/test_reflect.py b/twisted/test/test_reflect.py
new file mode 100644
index 0000000..eb2ba79
--- /dev/null
+++ b/twisted/test/test_reflect.py
@@ -0,0 +1,867 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.reflect module.
+"""
+
+import weakref, os
+from ihooks import ModuleImporter
+
+try:
+ from collections import deque
+except ImportError:
+ deque = None
+
+from twisted.trial import unittest
+from twisted.python import reflect, util
+from twisted.python.versions import Version
+
+
+
+class SettableTest(unittest.TestCase):
+ def setUp(self):
+ self.setter = reflect.Settable()
+
+ def tearDown(self):
+ del self.setter
+
+ def testSet(self):
+ self.setter(a=1, b=2)
+ self.assertEqual(self.setter.a, 1)
+ self.assertEqual(self.setter.b, 2)
+
+
+
+class AccessorTester(reflect.Accessor):
+
+ def set_x(self, x):
+ self.y = x
+ self.reallySet('x', x)
+
+
+ def get_z(self):
+ self.q = 1
+ return 1
+
+
+ def del_z(self):
+ self.reallyDel("q")
+
+
+
+class PropertyAccessorTester(reflect.PropertyAccessor):
+ """
+ Test class to check L{reflect.PropertyAccessor} functionalities.
+ """
+ r = 0
+
+ def set_r(self, r):
+ self.s = r
+
+
+ def set_x(self, x):
+ self.y = x
+ self.reallySet('x', x)
+
+
+ def get_z(self):
+ self.q = 1
+ return 1
+
+
+ def del_z(self):
+ self.reallyDel("q")
+
+
+
+class AccessorTest(unittest.TestCase):
+ def setUp(self):
+ self.tester = AccessorTester()
+
+ def testSet(self):
+ self.tester.x = 1
+ self.assertEqual(self.tester.x, 1)
+ self.assertEqual(self.tester.y, 1)
+
+ def testGet(self):
+ self.assertEqual(self.tester.z, 1)
+ self.assertEqual(self.tester.q, 1)
+
+ def testDel(self):
+ self.tester.z
+ self.assertEqual(self.tester.q, 1)
+ del self.tester.z
+ self.assertEqual(hasattr(self.tester, "q"), 0)
+ self.tester.x = 1
+ del self.tester.x
+ self.assertEqual(hasattr(self.tester, "x"), 0)
+
+
+
+class PropertyAccessorTest(AccessorTest):
+ """
+ Tests for L{reflect.PropertyAccessor}, using L{PropertyAccessorTester}.
+ """
+
+ def setUp(self):
+ self.tester = PropertyAccessorTester()
+
+
+ def test_setWithDefaultValue(self):
+ """
+ If an attribute is present in the class, it can be retrieved by
+ default.
+ """
+ self.assertEqual(self.tester.r, 0)
+ self.tester.r = 1
+ self.assertEqual(self.tester.r, 0)
+ self.assertEqual(self.tester.s, 1)
+
+
+ def test_getValueInDict(self):
+ """
+ The attribute value can be overriden by directly modifying the value in
+ C{__dict__}.
+ """
+ self.tester.__dict__["r"] = 10
+ self.assertEqual(self.tester.r, 10)
+
+
+ def test_notYetInDict(self):
+ """
+ If a getter is defined on an attribute but without any default value,
+ it raises C{AttributeError} when trying to access it.
+ """
+ self.assertRaises(AttributeError, getattr, self.tester, "x")
+
+
+
+class LookupsTestCase(unittest.TestCase):
+ """
+ Tests for L{namedClass}, L{namedModule}, and L{namedAny}.
+ """
+
+ def test_namedClassLookup(self):
+ """
+ L{namedClass} should return the class object for the name it is passed.
+ """
+ self.assertIdentical(
+ reflect.namedClass("twisted.python.reflect.Summer"),
+ reflect.Summer)
+
+
+ def test_namedModuleLookup(self):
+ """
+ L{namedModule} should return the module object for the name it is
+ passed.
+ """
+ self.assertIdentical(
+ reflect.namedModule("twisted.python.reflect"), reflect)
+
+
+ def test_namedAnyPackageLookup(self):
+ """
+ L{namedAny} should return the package object for the name it is passed.
+ """
+ import twisted.python
+ self.assertIdentical(
+ reflect.namedAny("twisted.python"), twisted.python)
+
+ def test_namedAnyModuleLookup(self):
+ """
+ L{namedAny} should return the module object for the name it is passed.
+ """
+ self.assertIdentical(
+ reflect.namedAny("twisted.python.reflect"), reflect)
+
+
+ def test_namedAnyClassLookup(self):
+ """
+ L{namedAny} should return the class object for the name it is passed.
+ """
+ self.assertIdentical(
+ reflect.namedAny("twisted.python.reflect.Summer"), reflect.Summer)
+
+
+ def test_namedAnyAttributeLookup(self):
+ """
+ L{namedAny} should return the object an attribute of a non-module,
+ non-package object is bound to for the name it is passed.
+ """
+ # Note - not assertEqual because unbound method lookup creates a new
+ # object every time. This is a foolishness of Python's object
+ # implementation, not a bug in Twisted.
+ self.assertEqual(
+ reflect.namedAny("twisted.python.reflect.Summer.reallySet"),
+ reflect.Summer.reallySet)
+
+
+ def test_namedAnySecondAttributeLookup(self):
+ """
+ L{namedAny} should return the object an attribute of an object which
+ itself was an attribute of a non-module, non-package object is bound to
+ for the name it is passed.
+ """
+ self.assertIdentical(
+ reflect.namedAny(
+ "twisted.python.reflect.Summer.reallySet.__doc__"),
+ reflect.Summer.reallySet.__doc__)
+
+
+ def test_importExceptions(self):
+ """
+ Exceptions raised by modules which L{namedAny} causes to be imported
+ should pass through L{namedAny} to the caller.
+ """
+ self.assertRaises(
+ ZeroDivisionError,
+ reflect.namedAny, "twisted.test.reflect_helper_ZDE")
+ # Make sure that this behavior is *consistent* for 2.3, where there is
+ # no post-failed-import cleanup
+ self.assertRaises(
+ ZeroDivisionError,
+ reflect.namedAny, "twisted.test.reflect_helper_ZDE")
+ self.assertRaises(
+ ValueError,
+ reflect.namedAny, "twisted.test.reflect_helper_VE")
+ # Modules which themselves raise ImportError when imported should result in an ImportError
+ self.assertRaises(
+ ImportError,
+ reflect.namedAny, "twisted.test.reflect_helper_IE")
+
+
+ def test_attributeExceptions(self):
+ """
+ If segments on the end of a fully-qualified Python name represents
+ attributes which aren't actually present on the object represented by
+ the earlier segments, L{namedAny} should raise an L{AttributeError}.
+ """
+ self.assertRaises(
+ AttributeError,
+ reflect.namedAny, "twisted.nosuchmoduleintheworld")
+ # ImportError behaves somewhat differently between "import
+ # extant.nonextant" and "import extant.nonextant.nonextant", so test
+ # the latter as well.
+ self.assertRaises(
+ AttributeError,
+ reflect.namedAny, "twisted.nosuch.modulein.theworld")
+ self.assertRaises(
+ AttributeError,
+ reflect.namedAny, "twisted.python.reflect.Summer.nosuchattributeintheworld")
+
+
+ def test_invalidNames(self):
+ """
+ Passing a name which isn't a fully-qualified Python name to L{namedAny}
+ should result in one of the following exceptions:
+ - L{InvalidName}: the name is not a dot-separated list of Python objects
+ - L{ObjectNotFound}: the object doesn't exist
+ - L{ModuleNotFound}: the object doesn't exist and there is only one
+ component in the name
+ """
+ err = self.assertRaises(reflect.ModuleNotFound, reflect.namedAny,
+ 'nosuchmoduleintheworld')
+ self.assertEqual(str(err), "No module named 'nosuchmoduleintheworld'")
+
+ # This is a dot-separated list, but it isn't valid!
+ err = self.assertRaises(reflect.ObjectNotFound, reflect.namedAny,
+ "@#$@(#.!@(#!@#")
+ self.assertEqual(str(err), "'@#$@(#.!@(#!@#' does not name an object")
+
+ err = self.assertRaises(reflect.ObjectNotFound, reflect.namedAny,
+ "tcelfer.nohtyp.detsiwt")
+ self.assertEqual(
+ str(err),
+ "'tcelfer.nohtyp.detsiwt' does not name an object")
+
+ err = self.assertRaises(reflect.InvalidName, reflect.namedAny, '')
+ self.assertEqual(str(err), 'Empty module name')
+
+ for invalidName in ['.twisted', 'twisted.', 'twisted..python']:
+ err = self.assertRaises(
+ reflect.InvalidName, reflect.namedAny, invalidName)
+ self.assertEqual(
+ str(err),
+ "name must be a string giving a '.'-separated list of Python "
+ "identifiers, not %r" % (invalidName,))
+
+
+
+class ImportHooksLookupTests(LookupsTestCase):
+ """
+ Tests for lookup methods in the presence of L{ihooks}-style import hooks.
+ Runs all of the tests from L{LookupsTestCase} after installing a custom
+ import hook.
+ """
+ def setUp(self):
+ """
+ Perturb the normal import behavior subtly by installing an import
+ hook. No custom behavior is provided, but this adds some extra
+ frames to the call stack, which L{namedAny} must be able to account
+ for.
+ """
+ self.importer = ModuleImporter()
+ self.importer.install()
+
+
+ def tearDown(self):
+ """
+ Uninstall the custom import hook.
+ """
+ self.importer.uninstall()
+
+
+
+class ObjectGrep(unittest.TestCase):
+ def test_dictionary(self):
+ """
+ Test references search through a dictionnary, as a key or as a value.
+ """
+ o = object()
+ d1 = {None: o}
+ d2 = {o: None}
+
+ self.assertIn("[None]", reflect.objgrep(d1, o, reflect.isSame))
+ self.assertIn("{None}", reflect.objgrep(d2, o, reflect.isSame))
+
+ def test_list(self):
+ """
+ Test references search through a list.
+ """
+ o = object()
+ L = [None, o]
+
+ self.assertIn("[1]", reflect.objgrep(L, o, reflect.isSame))
+
+ def test_tuple(self):
+ """
+ Test references search through a tuple.
+ """
+ o = object()
+ T = (o, None)
+
+ self.assertIn("[0]", reflect.objgrep(T, o, reflect.isSame))
+
+ def test_instance(self):
+ """
+ Test references search through an object attribute.
+ """
+ class Dummy:
+ pass
+ o = object()
+ d = Dummy()
+ d.o = o
+
+ self.assertIn(".o", reflect.objgrep(d, o, reflect.isSame))
+
+ def test_weakref(self):
+ """
+ Test references search through a weakref object.
+ """
+ class Dummy:
+ pass
+ o = Dummy()
+ w1 = weakref.ref(o)
+
+ self.assertIn("()", reflect.objgrep(w1, o, reflect.isSame))
+
+ def test_boundMethod(self):
+ """
+ Test references search through method special attributes.
+ """
+ class Dummy:
+ def dummy(self):
+ pass
+ o = Dummy()
+ m = o.dummy
+
+ self.assertIn(".im_self", reflect.objgrep(m, m.im_self, reflect.isSame))
+ self.assertIn(".im_class", reflect.objgrep(m, m.im_class, reflect.isSame))
+ self.assertIn(".im_func", reflect.objgrep(m, m.im_func, reflect.isSame))
+
+ def test_everything(self):
+ """
+ Test references search using complex set of objects.
+ """
+ class Dummy:
+ def method(self):
+ pass
+
+ o = Dummy()
+ D1 = {(): "baz", None: "Quux", o: "Foosh"}
+ L = [None, (), D1, 3]
+ T = (L, {}, Dummy())
+ D2 = {0: "foo", 1: "bar", 2: T}
+ i = Dummy()
+ i.attr = D2
+ m = i.method
+ w = weakref.ref(m)
+
+ self.assertIn("().im_self.attr[2][0][2]{'Foosh'}", reflect.objgrep(w, o, reflect.isSame))
+
+ def test_depthLimit(self):
+ """
+ Test the depth of references search.
+ """
+ a = []
+ b = [a]
+ c = [a, b]
+ d = [a, c]
+
+ self.assertEqual(['[0]'], reflect.objgrep(d, a, reflect.isSame, maxDepth=1))
+ self.assertEqual(['[0]', '[1][0]'], reflect.objgrep(d, a, reflect.isSame, maxDepth=2))
+ self.assertEqual(['[0]', '[1][0]', '[1][1][0]'], reflect.objgrep(d, a, reflect.isSame, maxDepth=3))
+
+ def test_deque(self):
+ """
+ Test references search through a deque object. Only for Python > 2.3.
+ """
+ o = object()
+ D = deque()
+ D.append(None)
+ D.append(o)
+
+ self.assertIn("[1]", reflect.objgrep(D, o, reflect.isSame))
+
+ if deque is None:
+ test_deque.skip = "Deque not available"
+
+
+class GetClass(unittest.TestCase):
+ def testOld(self):
+ class OldClass:
+ pass
+ old = OldClass()
+ self.assertIn(reflect.getClass(OldClass).__name__, ('class', 'classobj'))
+ self.assertEqual(reflect.getClass(old).__name__, 'OldClass')
+
+ def testNew(self):
+ class NewClass(object):
+ pass
+ new = NewClass()
+ self.assertEqual(reflect.getClass(NewClass).__name__, 'type')
+ self.assertEqual(reflect.getClass(new).__name__, 'NewClass')
+
+
+
+class Breakable(object):
+
+ breakRepr = False
+ breakStr = False
+
+ def __str__(self):
+ if self.breakStr:
+ raise RuntimeError("str!")
+ else:
+ return '<Breakable>'
+
+ def __repr__(self):
+ if self.breakRepr:
+ raise RuntimeError("repr!")
+ else:
+ return 'Breakable()'
+
+
+
+class BrokenType(Breakable, type):
+ breakName = False
+
+ def get___name__(self):
+ if self.breakName:
+ raise RuntimeError("no name")
+ return 'BrokenType'
+ __name__ = property(get___name__)
+
+
+
+class BTBase(Breakable):
+ __metaclass__ = BrokenType
+ breakRepr = True
+ breakStr = True
+
+
+
+class NoClassAttr(Breakable):
+ __class__ = property(lambda x: x.not_class)
+
+
+
+class SafeRepr(unittest.TestCase):
+ """
+ Tests for L{reflect.safe_repr} function.
+ """
+
+ def test_workingRepr(self):
+ """
+ L{reflect.safe_repr} produces the same output as C{repr} on a working
+ object.
+ """
+ x = [1, 2, 3]
+ self.assertEqual(reflect.safe_repr(x), repr(x))
+
+
+ def test_brokenRepr(self):
+ """
+ L{reflect.safe_repr} returns a string with class name, address, and
+ traceback when the repr call failed.
+ """
+ b = Breakable()
+ b.breakRepr = True
+ bRepr = reflect.safe_repr(b)
+ self.assertIn("Breakable instance at 0x", bRepr)
+ # Check that the file is in the repr, but without the extension as it
+ # can be .py/.pyc
+ self.assertIn(os.path.splitext(__file__)[0], bRepr)
+ self.assertIn("RuntimeError: repr!", bRepr)
+
+
+ def test_brokenStr(self):
+ """
+ L{reflect.safe_repr} isn't affected by a broken C{__str__} method.
+ """
+ b = Breakable()
+ b.breakStr = True
+ self.assertEqual(reflect.safe_repr(b), repr(b))
+
+
+ def test_brokenClassRepr(self):
+ class X(BTBase):
+ breakRepr = True
+ reflect.safe_repr(X)
+ reflect.safe_repr(X())
+
+
+ def test_unsignedID(self):
+ """
+ L{unsignedID} is used to print ID of the object in case of error, not
+ standard ID value which can be negative.
+ """
+ class X(BTBase):
+ breakRepr = True
+
+ ids = {X: 100}
+ def fakeID(obj):
+ try:
+ return ids[obj]
+ except (TypeError, KeyError):
+ return id(obj)
+ self.addCleanup(util.setIDFunction, util.setIDFunction(fakeID))
+
+ xRepr = reflect.safe_repr(X)
+ self.assertIn("0x64", xRepr)
+
+
+ def test_brokenClassStr(self):
+ class X(BTBase):
+ breakStr = True
+ reflect.safe_repr(X)
+ reflect.safe_repr(X())
+
+
+ def test_brokenClassAttribute(self):
+ """
+ If an object raises an exception when accessing its C{__class__}
+ attribute, L{reflect.safe_repr} uses C{type} to retrieve the class
+ object.
+ """
+ b = NoClassAttr()
+ b.breakRepr = True
+ bRepr = reflect.safe_repr(b)
+ self.assertIn("NoClassAttr instance at 0x", bRepr)
+ self.assertIn(os.path.splitext(__file__)[0], bRepr)
+ self.assertIn("RuntimeError: repr!", bRepr)
+
+
+ def test_brokenClassNameAttribute(self):
+ """
+ If a class raises an exception when accessing its C{__name__} attribute
+ B{and} when calling its C{__str__} implementation, L{reflect.safe_repr}
+ returns 'BROKEN CLASS' instead of the class name.
+ """
+ class X(BTBase):
+ breakName = True
+ xRepr = reflect.safe_repr(X())
+ self.assertIn("<BROKEN CLASS AT 0x", xRepr)
+ self.assertIn(os.path.splitext(__file__)[0], xRepr)
+ self.assertIn("RuntimeError: repr!", xRepr)
+
+
+
+class SafeStr(unittest.TestCase):
+ """
+ Tests for L{reflect.safe_str} function.
+ """
+
+ def test_workingStr(self):
+ x = [1, 2, 3]
+ self.assertEqual(reflect.safe_str(x), str(x))
+
+
+ def test_brokenStr(self):
+ b = Breakable()
+ b.breakStr = True
+ reflect.safe_str(b)
+
+
+ def test_brokenRepr(self):
+ b = Breakable()
+ b.breakRepr = True
+ reflect.safe_str(b)
+
+
+ def test_brokenClassStr(self):
+ class X(BTBase):
+ breakStr = True
+ reflect.safe_str(X)
+ reflect.safe_str(X())
+
+
+ def test_brokenClassRepr(self):
+ class X(BTBase):
+ breakRepr = True
+ reflect.safe_str(X)
+ reflect.safe_str(X())
+
+
+ def test_brokenClassAttribute(self):
+ """
+ If an object raises an exception when accessing its C{__class__}
+ attribute, L{reflect.safe_str} uses C{type} to retrieve the class
+ object.
+ """
+ b = NoClassAttr()
+ b.breakStr = True
+ bStr = reflect.safe_str(b)
+ self.assertIn("NoClassAttr instance at 0x", bStr)
+ self.assertIn(os.path.splitext(__file__)[0], bStr)
+ self.assertIn("RuntimeError: str!", bStr)
+
+
+ def test_brokenClassNameAttribute(self):
+ """
+ If a class raises an exception when accessing its C{__name__} attribute
+ B{and} when calling its C{__str__} implementation, L{reflect.safe_str}
+ returns 'BROKEN CLASS' instead of the class name.
+ """
+ class X(BTBase):
+ breakName = True
+ xStr = reflect.safe_str(X())
+ self.assertIn("<BROKEN CLASS AT 0x", xStr)
+ self.assertIn(os.path.splitext(__file__)[0], xStr)
+ self.assertIn("RuntimeError: str!", xStr)
+
+
+
+class FilenameToModule(unittest.TestCase):
+ """
+ Test L{reflect.filenameToModuleName} detection.
+ """
+ def test_directory(self):
+ """
+ Tests it finds good name for directories/packages.
+ """
+ module = reflect.filenameToModuleName(os.path.join('twisted', 'test'))
+ self.assertEqual(module, 'test')
+ module = reflect.filenameToModuleName(os.path.join('twisted', 'test')
+ + os.path.sep)
+ self.assertEqual(module, 'test')
+
+ def test_file(self):
+ """
+ Test it finds good name for files.
+ """
+ module = reflect.filenameToModuleName(
+ os.path.join('twisted', 'test', 'test_reflect.py'))
+ self.assertEqual(module, 'test_reflect')
+
+
+
+class FullyQualifiedNameTests(unittest.TestCase):
+ """
+ Test for L{reflect.fullyQualifiedName}.
+ """
+
+ def _checkFullyQualifiedName(self, obj, expected):
+ """
+ Helper to check that fully qualified name of C{obj} results to
+ C{expected}.
+ """
+ self.assertEqual(
+ reflect.fullyQualifiedName(obj), expected)
+
+
+ def test_package(self):
+ """
+ L{reflect.fullyQualifiedName} returns the full name of a package and
+ a subpackage.
+ """
+ import twisted
+ self._checkFullyQualifiedName(twisted, 'twisted')
+ import twisted.python
+ self._checkFullyQualifiedName(twisted.python, 'twisted.python')
+
+
+ def test_module(self):
+ """
+ L{reflect.fullyQualifiedName} returns the name of a module inside a a
+ package.
+ """
+ self._checkFullyQualifiedName(reflect, 'twisted.python.reflect')
+ import twisted.trial.unittest
+ self._checkFullyQualifiedName(twisted.trial.unittest,
+ 'twisted.trial.unittest')
+
+
+ def test_class(self):
+ """
+ L{reflect.fullyQualifiedName} returns the name of a class and its
+ module.
+ """
+ self._checkFullyQualifiedName(reflect.Settable,
+ 'twisted.python.reflect.Settable')
+
+
+ def test_function(self):
+ """
+ L{reflect.fullyQualifiedName} returns the name of a function inside its
+ module.
+ """
+ self._checkFullyQualifiedName(reflect.fullyQualifiedName,
+ "twisted.python.reflect.fullyQualifiedName")
+
+
+ def test_boundMethod(self):
+ """
+ L{reflect.fullyQualifiedName} returns the name of a bound method inside
+ its class and its module.
+ """
+ self._checkFullyQualifiedName(
+ reflect.PropertyAccessor().reallyDel,
+ "twisted.python.reflect.PropertyAccessor.reallyDel")
+
+
+ def test_unboundMethod(self):
+ """
+ L{reflect.fullyQualifiedName} returns the name of an unbound method
+ inside its class and its module.
+ """
+ self._checkFullyQualifiedName(
+ reflect.PropertyAccessor.reallyDel,
+ "twisted.python.reflect.PropertyAccessor.reallyDel")
+
+
+
+class DeprecationTestCase(unittest.TestCase):
+ """
+ Test deprecations in twisted.python.reflect
+ """
+
+ def test_allYourBase(self):
+ """
+ Test deprecation of L{reflect.allYourBase}. See #5481 for removal.
+ """
+ self.callDeprecated(
+ (Version("Twisted", 11, 0, 0), "inspect.getmro"),
+ reflect.allYourBase, DeprecationTestCase)
+
+
+ def test_accumulateBases(self):
+ """
+ Test deprecation of L{reflect.accumulateBases}. See #5481 for removal.
+ """
+ l = []
+ self.callDeprecated(
+ (Version("Twisted", 11, 0, 0), "inspect.getmro"),
+ reflect.accumulateBases, DeprecationTestCase, l, None)
+
+
+ def lookForDeprecationWarning(self, testMethod, attributeName, warningMsg):
+ """
+ Test deprecation of attribute 'reflect.attributeName' by calling
+ 'reflect.testMethod' and verifying the warning message
+ 'reflect.warningMsg'
+
+ @param testMethod: Name of the offending function to be used with
+ flushWarnings
+ @type testmethod: C{str}
+
+ @param attributeName: Name of attribute to be checked for deprecation
+ @type attributeName: C{str}
+
+ @param warningMsg: Deprecation warning message
+ @type warningMsg: C{str}
+ """
+ warningsShown = self.flushWarnings([testMethod])
+ self.assertEqual(len(warningsShown), 1)
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warningsShown[0]['message'],
+ "twisted.python.reflect." + attributeName + " "
+ "was deprecated in Twisted 12.1.0: " + warningMsg + ".")
+
+
+ def test_settable(self):
+ """
+ Test deprecation of L{reflect.Settable}.
+ """
+ reflect.Settable()
+ self.lookForDeprecationWarning(
+ self.test_settable, "Settable",
+ "Settable is old and untested. Please write your own version of this "
+ "functionality if you need it")
+
+
+ def test_accessorType(self):
+ """
+ Test deprecation of L{reflect.AccessorType}.
+ """
+ reflect.AccessorType(' ', ( ), { })
+ self.lookForDeprecationWarning(
+ self.test_accessorType, "AccessorType",
+ "AccessorType is old and untested. Please write your own version of "
+ "this functionality if you need it")
+
+
+ def test_propertyAccessor(self):
+ """
+ Test deprecation of L{reflect.PropertyAccessor}.
+ """
+ reflect.PropertyAccessor()
+ self.lookForDeprecationWarning(
+ self.test_propertyAccessor, "PropertyAccessor",
+ "PropertyAccessor is old and untested. Please write your own "
+ "version of this functionality if you need it")
+
+
+ def test_accessor(self):
+ """
+ Test deprecation of L{reflect.Accessor}.
+ """
+ reflect.Accessor()
+ self.lookForDeprecationWarning(
+ self.test_accessor, "Accessor",
+ "Accessor is an implementation for Python 2.1 which is no longer "
+ "supported by Twisted")
+
+
+ def test_originalAccessor(self):
+ """
+ Test deprecation of L{reflect.OriginalAccessor}.
+ """
+ reflect.OriginalAccessor()
+ self.lookForDeprecationWarning(
+ self.test_originalAccessor, "OriginalAccessor",
+ "OriginalAccessor is a reference to class "
+ "twisted.python.reflect.Accessor which is deprecated")
+
+
+ def test_summer(self):
+ """
+ Test deprecation of L{reflect.Summer}.
+ """
+ reflect.Summer()
+ self.lookForDeprecationWarning(
+ self.test_summer, "Summer",
+ "Summer is a child class of twisted.python.reflect.Accessor which "
+ "is deprecated")
diff --git a/twisted/test/test_roots.py b/twisted/test/test_roots.py
new file mode 100644
index 0000000..c9fd39e
--- /dev/null
+++ b/twisted/test/test_roots.py
@@ -0,0 +1,63 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.python import roots
+import types
+
+class RootsTest(unittest.TestCase):
+
+ def testExceptions(self):
+ request = roots.Request()
+ try:
+ request.write("blah")
+ except NotImplementedError:
+ pass
+ else:
+ self.fail()
+ try:
+ request.finish()
+ except NotImplementedError:
+ pass
+ else:
+ self.fail()
+
+ def testCollection(self):
+ collection = roots.Collection()
+ collection.putEntity("x", 'test')
+ self.assertEqual(collection.getStaticEntity("x"),
+ 'test')
+ collection.delEntity("x")
+ self.assertEqual(collection.getStaticEntity('x'),
+ None)
+ try:
+ collection.storeEntity("x", None)
+ except NotImplementedError:
+ pass
+ else:
+ self.fail()
+ try:
+ collection.removeEntity("x", None)
+ except NotImplementedError:
+ pass
+ else:
+ self.fail()
+
+ def testConstrained(self):
+ class const(roots.Constrained):
+ def nameConstraint(self, name):
+ return (name == 'x')
+ c = const()
+ self.assertEqual(c.putEntity('x', 'test'), None)
+ self.failUnlessRaises(roots.ConstraintViolation,
+ c.putEntity, 'y', 'test')
+
+
+ def testHomogenous(self):
+ h = roots.Homogenous()
+ h.entityType = types.IntType
+ h.putEntity('a', 1)
+ self.assertEqual(h.getStaticEntity('a'),1 )
+ self.failUnlessRaises(roots.ConstraintViolation,
+ h.putEntity, 'x', 'y')
+
diff --git a/twisted/test/test_shortcut.py b/twisted/test/test_shortcut.py
new file mode 100644
index 0000000..fdcb775
--- /dev/null
+++ b/twisted/test/test_shortcut.py
@@ -0,0 +1,26 @@
+"""Test win32 shortcut script
+"""
+
+from twisted.trial import unittest
+
+import os
+if os.name == 'nt':
+
+ skipWindowsNopywin32 = None
+ try:
+ from twisted.python import shortcut
+ except ImportError:
+ skipWindowsNopywin32 = ("On windows, twisted.python.shortcut is not "
+ "available in the absence of win32com.")
+ import os.path
+ import sys
+
+ class ShortcutTest(unittest.TestCase):
+ def testCreate(self):
+ s1=shortcut.Shortcut("test_shortcut.py")
+ tempname=self.mktemp() + '.lnk'
+ s1.save(tempname)
+ self.assert_(os.path.exists(tempname))
+ sc=shortcut.open(tempname)
+ self.assert_(sc.GetPath(0)[0].endswith('test_shortcut.py'))
+ ShortcutTest.skip = skipWindowsNopywin32
diff --git a/twisted/test/test_sip.py b/twisted/test/test_sip.py
new file mode 100644
index 0000000..e999d0a
--- /dev/null
+++ b/twisted/test/test_sip.py
@@ -0,0 +1,942 @@
+# -*- test-case-name: twisted.test.test_sip -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Session Initialization Protocol tests."""
+
+from twisted.trial import unittest, util
+from twisted.protocols import sip
+from twisted.internet import defer, reactor, utils
+from twisted.python.versions import Version
+
+from twisted.test import proto_helpers
+
+from twisted import cred
+import twisted.cred.portal
+import twisted.cred.checkers
+
+from zope.interface import implements
+
+
+# request, prefixed by random CRLFs
+request1 = "\n\r\n\n\r" + """\
+INVITE sip:foo SIP/2.0
+From: mo
+To: joe
+Content-Length: 4
+
+abcd""".replace("\n", "\r\n")
+
+# request, no content-length
+request2 = """INVITE sip:foo SIP/2.0
+From: mo
+To: joe
+
+1234""".replace("\n", "\r\n")
+
+# request, with garbage after
+request3 = """INVITE sip:foo SIP/2.0
+From: mo
+To: joe
+Content-Length: 4
+
+1234
+
+lalalal""".replace("\n", "\r\n")
+
+# three requests
+request4 = """INVITE sip:foo SIP/2.0
+From: mo
+To: joe
+Content-Length: 0
+
+INVITE sip:loop SIP/2.0
+From: foo
+To: bar
+Content-Length: 4
+
+abcdINVITE sip:loop SIP/2.0
+From: foo
+To: bar
+Content-Length: 4
+
+1234""".replace("\n", "\r\n")
+
+# response, no content
+response1 = """SIP/2.0 200 OK
+From: foo
+To:bar
+Content-Length: 0
+
+""".replace("\n", "\r\n")
+
+# short header version
+request_short = """\
+INVITE sip:foo SIP/2.0
+f: mo
+t: joe
+l: 4
+
+abcd""".replace("\n", "\r\n")
+
+request_natted = """\
+INVITE sip:foo SIP/2.0
+Via: SIP/2.0/UDP 10.0.0.1:5060;rport
+
+""".replace("\n", "\r\n")
+
+class TestRealm:
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ return sip.IContact, None, lambda: None
+
+class MessageParsingTestCase(unittest.TestCase):
+ def setUp(self):
+ self.l = []
+ self.parser = sip.MessagesParser(self.l.append)
+
+ def feedMessage(self, message):
+ self.parser.dataReceived(message)
+ self.parser.dataDone()
+
+ def validateMessage(self, m, method, uri, headers, body):
+ """Validate Requests."""
+ self.assertEqual(m.method, method)
+ self.assertEqual(m.uri.toString(), uri)
+ self.assertEqual(m.headers, headers)
+ self.assertEqual(m.body, body)
+ self.assertEqual(m.finished, 1)
+
+ def testSimple(self):
+ l = self.l
+ self.feedMessage(request1)
+ self.assertEqual(len(l), 1)
+ self.validateMessage(
+ l[0], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"], "content-length": ["4"]},
+ "abcd")
+
+ def testTwoMessages(self):
+ l = self.l
+ self.feedMessage(request1)
+ self.feedMessage(request2)
+ self.assertEqual(len(l), 2)
+ self.validateMessage(
+ l[0], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"], "content-length": ["4"]},
+ "abcd")
+ self.validateMessage(l[1], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"]},
+ "1234")
+
+ def testGarbage(self):
+ l = self.l
+ self.feedMessage(request3)
+ self.assertEqual(len(l), 1)
+ self.validateMessage(
+ l[0], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"], "content-length": ["4"]},
+ "1234")
+
+ def testThreeInOne(self):
+ l = self.l
+ self.feedMessage(request4)
+ self.assertEqual(len(l), 3)
+ self.validateMessage(
+ l[0], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"], "content-length": ["0"]},
+ "")
+ self.validateMessage(
+ l[1], "INVITE", "sip:loop",
+ {"from": ["foo"], "to": ["bar"], "content-length": ["4"]},
+ "abcd")
+ self.validateMessage(
+ l[2], "INVITE", "sip:loop",
+ {"from": ["foo"], "to": ["bar"], "content-length": ["4"]},
+ "1234")
+
+ def testShort(self):
+ l = self.l
+ self.feedMessage(request_short)
+ self.assertEqual(len(l), 1)
+ self.validateMessage(
+ l[0], "INVITE", "sip:foo",
+ {"from": ["mo"], "to": ["joe"], "content-length": ["4"]},
+ "abcd")
+
+ def testSimpleResponse(self):
+ l = self.l
+ self.feedMessage(response1)
+ self.assertEqual(len(l), 1)
+ m = l[0]
+ self.assertEqual(m.code, 200)
+ self.assertEqual(m.phrase, "OK")
+ self.assertEqual(
+ m.headers,
+ {"from": ["foo"], "to": ["bar"], "content-length": ["0"]})
+ self.assertEqual(m.body, "")
+ self.assertEqual(m.finished, 1)
+
+
+class MessageParsingTestCase2(MessageParsingTestCase):
+ """Same as base class, but feed data char by char."""
+
+ def feedMessage(self, message):
+ for c in message:
+ self.parser.dataReceived(c)
+ self.parser.dataDone()
+
+
+class MakeMessageTestCase(unittest.TestCase):
+
+ def testRequest(self):
+ r = sip.Request("INVITE", "sip:foo")
+ r.addHeader("foo", "bar")
+ self.assertEqual(
+ r.toString(),
+ "INVITE sip:foo SIP/2.0\r\nFoo: bar\r\n\r\n")
+
+ def testResponse(self):
+ r = sip.Response(200, "OK")
+ r.addHeader("foo", "bar")
+ r.addHeader("Content-Length", "4")
+ r.bodyDataReceived("1234")
+ self.assertEqual(
+ r.toString(),
+ "SIP/2.0 200 OK\r\nFoo: bar\r\nContent-Length: 4\r\n\r\n1234")
+
+ def testStatusCode(self):
+ r = sip.Response(200)
+ self.assertEqual(r.toString(), "SIP/2.0 200 OK\r\n\r\n")
+
+
+class ViaTestCase(unittest.TestCase):
+
+ def checkRoundtrip(self, v):
+ s = v.toString()
+ self.assertEqual(s, sip.parseViaHeader(s).toString())
+
+ def testExtraWhitespace(self):
+ v1 = sip.parseViaHeader('SIP/2.0/UDP 192.168.1.1:5060')
+ v2 = sip.parseViaHeader('SIP/2.0/UDP 192.168.1.1:5060')
+ self.assertEqual(v1.transport, v2.transport)
+ self.assertEqual(v1.host, v2.host)
+ self.assertEqual(v1.port, v2.port)
+
+ def test_complex(self):
+ """
+ Test parsing a Via header with one of everything.
+ """
+ s = ("SIP/2.0/UDP first.example.com:4000;ttl=16;maddr=224.2.0.1"
+ " ;branch=a7c6a8dlze (Example)")
+ v = sip.parseViaHeader(s)
+ self.assertEqual(v.transport, "UDP")
+ self.assertEqual(v.host, "first.example.com")
+ self.assertEqual(v.port, 4000)
+ self.assertEqual(v.rport, None)
+ self.assertEqual(v.rportValue, None)
+ self.assertEqual(v.rportRequested, False)
+ self.assertEqual(v.ttl, 16)
+ self.assertEqual(v.maddr, "224.2.0.1")
+ self.assertEqual(v.branch, "a7c6a8dlze")
+ self.assertEqual(v.hidden, 0)
+ self.assertEqual(v.toString(),
+ "SIP/2.0/UDP first.example.com:4000"
+ ";ttl=16;branch=a7c6a8dlze;maddr=224.2.0.1")
+ self.checkRoundtrip(v)
+
+ def test_simple(self):
+ """
+ Test parsing a simple Via header.
+ """
+ s = "SIP/2.0/UDP example.com;hidden"
+ v = sip.parseViaHeader(s)
+ self.assertEqual(v.transport, "UDP")
+ self.assertEqual(v.host, "example.com")
+ self.assertEqual(v.port, 5060)
+ self.assertEqual(v.rport, None)
+ self.assertEqual(v.rportValue, None)
+ self.assertEqual(v.rportRequested, False)
+ self.assertEqual(v.ttl, None)
+ self.assertEqual(v.maddr, None)
+ self.assertEqual(v.branch, None)
+ self.assertEqual(v.hidden, True)
+ self.assertEqual(v.toString(),
+ "SIP/2.0/UDP example.com:5060;hidden")
+ self.checkRoundtrip(v)
+
+ def testSimpler(self):
+ v = sip.Via("example.com")
+ self.checkRoundtrip(v)
+
+
+ def test_deprecatedRPort(self):
+ """
+ Setting rport to True is deprecated, but still produces a Via header
+ with the expected properties.
+ """
+ v = sip.Via("foo.bar", rport=True)
+
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedRPort])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'rport=True is deprecated since Twisted 9.0.')
+ self.assertEqual(
+ warnings[0]['category'],
+ DeprecationWarning)
+
+ self.assertEqual(v.toString(), "SIP/2.0/UDP foo.bar:5060;rport")
+ self.assertEqual(v.rport, True)
+ self.assertEqual(v.rportRequested, True)
+ self.assertEqual(v.rportValue, None)
+
+
+ def test_rport(self):
+ """
+ An rport setting of None should insert the parameter with no value.
+ """
+ v = sip.Via("foo.bar", rport=None)
+ self.assertEqual(v.toString(), "SIP/2.0/UDP foo.bar:5060;rport")
+ self.assertEqual(v.rportRequested, True)
+ self.assertEqual(v.rportValue, None)
+
+
+ def test_rportValue(self):
+ """
+ An rport numeric setting should insert the parameter with the number
+ value given.
+ """
+ v = sip.Via("foo.bar", rport=1)
+ self.assertEqual(v.toString(), "SIP/2.0/UDP foo.bar:5060;rport=1")
+ self.assertEqual(v.rportRequested, False)
+ self.assertEqual(v.rportValue, 1)
+ self.assertEqual(v.rport, 1)
+
+
+ def testNAT(self):
+ s = "SIP/2.0/UDP 10.0.0.1:5060;received=22.13.1.5;rport=12345"
+ v = sip.parseViaHeader(s)
+ self.assertEqual(v.transport, "UDP")
+ self.assertEqual(v.host, "10.0.0.1")
+ self.assertEqual(v.port, 5060)
+ self.assertEqual(v.received, "22.13.1.5")
+ self.assertEqual(v.rport, 12345)
+
+ self.assertNotEquals(v.toString().find("rport=12345"), -1)
+
+
+ def test_unknownParams(self):
+ """
+ Parsing and serializing Via headers with unknown parameters should work.
+ """
+ s = "SIP/2.0/UDP example.com:5060;branch=a12345b;bogus;pie=delicious"
+ v = sip.parseViaHeader(s)
+ self.assertEqual(v.toString(), s)
+
+
+
+class URLTestCase(unittest.TestCase):
+
+ def testRoundtrip(self):
+ for url in [
+ "sip:j.doe@big.com",
+ "sip:j.doe:secret@big.com;transport=tcp",
+ "sip:j.doe@big.com?subject=project",
+ "sip:example.com",
+ ]:
+ self.assertEqual(sip.parseURL(url).toString(), url)
+
+ def testComplex(self):
+ s = ("sip:user:pass@hosta:123;transport=udp;user=phone;method=foo;"
+ "ttl=12;maddr=1.2.3.4;blah;goo=bar?a=b&c=d")
+ url = sip.parseURL(s)
+ for k, v in [("username", "user"), ("password", "pass"),
+ ("host", "hosta"), ("port", 123),
+ ("transport", "udp"), ("usertype", "phone"),
+ ("method", "foo"), ("ttl", 12),
+ ("maddr", "1.2.3.4"), ("other", ["blah", "goo=bar"]),
+ ("headers", {"a": "b", "c": "d"})]:
+ self.assertEqual(getattr(url, k), v)
+
+
+class ParseTestCase(unittest.TestCase):
+
+ def testParseAddress(self):
+ for address, name, urls, params in [
+ ('"A. G. Bell" <sip:foo@example.com>',
+ "A. G. Bell", "sip:foo@example.com", {}),
+ ("Anon <sip:foo@example.com>", "Anon", "sip:foo@example.com", {}),
+ ("sip:foo@example.com", "", "sip:foo@example.com", {}),
+ ("<sip:foo@example.com>", "", "sip:foo@example.com", {}),
+ ("foo <sip:foo@example.com>;tag=bar;foo=baz", "foo",
+ "sip:foo@example.com", {"tag": "bar", "foo": "baz"}),
+ ]:
+ gname, gurl, gparams = sip.parseAddress(address)
+ self.assertEqual(name, gname)
+ self.assertEqual(gurl.toString(), urls)
+ self.assertEqual(gparams, params)
+
+
+class DummyLocator:
+ implements(sip.ILocator)
+ def getAddress(self, logicalURL):
+ return defer.succeed(sip.URL("server.com", port=5060))
+
+class FailingLocator:
+ implements(sip.ILocator)
+ def getAddress(self, logicalURL):
+ return defer.fail(LookupError())
+
+
+class ProxyTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.proxy = sip.Proxy("127.0.0.1")
+ self.proxy.locator = DummyLocator()
+ self.sent = []
+ self.proxy.sendMessage = lambda dest, msg: self.sent.append((dest, msg))
+
+ def testRequestForward(self):
+ r = sip.Request("INVITE", "sip:foo")
+ r.addHeader("via", sip.Via("1.2.3.4").toString())
+ r.addHeader("via", sip.Via("1.2.3.5").toString())
+ r.addHeader("foo", "bar")
+ r.addHeader("to", "<sip:joe@server.com>")
+ r.addHeader("contact", "<sip:joe@1.2.3.5>")
+ self.proxy.datagramReceived(r.toString(), ("1.2.3.4", 5060))
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual(dest.port, 5060)
+ self.assertEqual(dest.host, "server.com")
+ self.assertEqual(m.uri.toString(), "sip:foo")
+ self.assertEqual(m.method, "INVITE")
+ self.assertEqual(m.headers["via"],
+ ["SIP/2.0/UDP 127.0.0.1:5060",
+ "SIP/2.0/UDP 1.2.3.4:5060",
+ "SIP/2.0/UDP 1.2.3.5:5060"])
+
+
+ def testReceivedRequestForward(self):
+ r = sip.Request("INVITE", "sip:foo")
+ r.addHeader("via", sip.Via("1.2.3.4").toString())
+ r.addHeader("foo", "bar")
+ r.addHeader("to", "<sip:joe@server.com>")
+ r.addHeader("contact", "<sip:joe@1.2.3.4>")
+ self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060))
+ dest, m = self.sent[0]
+ self.assertEqual(m.headers["via"],
+ ["SIP/2.0/UDP 127.0.0.1:5060",
+ "SIP/2.0/UDP 1.2.3.4:5060;received=1.1.1.1"])
+
+
+ def testResponseWrongVia(self):
+ # first via must match proxy's address
+ r = sip.Response(200)
+ r.addHeader("via", sip.Via("foo.com").toString())
+ self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060))
+ self.assertEqual(len(self.sent), 0)
+
+ def testResponseForward(self):
+ r = sip.Response(200)
+ r.addHeader("via", sip.Via("127.0.0.1").toString())
+ r.addHeader("via", sip.Via("client.com", port=1234).toString())
+ self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060))
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual((dest.host, dest.port), ("client.com", 1234))
+ self.assertEqual(m.code, 200)
+ self.assertEqual(m.headers["via"], ["SIP/2.0/UDP client.com:1234"])
+
+ def testReceivedResponseForward(self):
+ r = sip.Response(200)
+ r.addHeader("via", sip.Via("127.0.0.1").toString())
+ r.addHeader(
+ "via",
+ sip.Via("10.0.0.1", received="client.com").toString())
+ self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060))
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual((dest.host, dest.port), ("client.com", 5060))
+
+ def testResponseToUs(self):
+ r = sip.Response(200)
+ r.addHeader("via", sip.Via("127.0.0.1").toString())
+ l = []
+ self.proxy.gotResponse = lambda *a: l.append(a)
+ self.proxy.datagramReceived(r.toString(), ("1.1.1.1", 5060))
+ self.assertEqual(len(l), 1)
+ m, addr = l[0]
+ self.assertEqual(len(m.headers.get("via", [])), 0)
+ self.assertEqual(m.code, 200)
+
+ def testLoop(self):
+ r = sip.Request("INVITE", "sip:foo")
+ r.addHeader("via", sip.Via("1.2.3.4").toString())
+ r.addHeader("via", sip.Via("127.0.0.1").toString())
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+ self.assertEqual(self.sent, [])
+
+ def testCantForwardRequest(self):
+ r = sip.Request("INVITE", "sip:foo")
+ r.addHeader("via", sip.Via("1.2.3.4").toString())
+ r.addHeader("to", "<sip:joe@server.com>")
+ self.proxy.locator = FailingLocator()
+ self.proxy.datagramReceived(r.toString(), ("1.2.3.4", 5060))
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual((dest.host, dest.port), ("1.2.3.4", 5060))
+ self.assertEqual(m.code, 404)
+ self.assertEqual(m.headers["via"], ["SIP/2.0/UDP 1.2.3.4:5060"])
+
+ def testCantForwardResponse(self):
+ pass
+
+ #testCantForwardResponse.skip = "not implemented yet"
+
+
+class RegistrationTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.proxy = sip.RegisterProxy(host="127.0.0.1")
+ self.registry = sip.InMemoryRegistry("bell.example.com")
+ self.proxy.registry = self.proxy.locator = self.registry
+ self.sent = []
+ self.proxy.sendMessage = lambda dest, msg: self.sent.append((dest, msg))
+ setUp = utils.suppressWarnings(setUp,
+ util.suppress(category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestAuthorizer was deprecated'))
+
+ def tearDown(self):
+ for d, uri in self.registry.users.values():
+ d.cancel()
+ del self.proxy
+
+ def register(self):
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@client.com:1234")
+ r.addHeader("via", sip.Via("client.com").toString())
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+
+ def unregister(self):
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "*")
+ r.addHeader("via", sip.Via("client.com").toString())
+ r.addHeader("expires", "0")
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+
+ def testRegister(self):
+ self.register()
+ dest, m = self.sent[0]
+ self.assertEqual((dest.host, dest.port), ("client.com", 5060))
+ self.assertEqual(m.code, 200)
+ self.assertEqual(m.headers["via"], ["SIP/2.0/UDP client.com:5060"])
+ self.assertEqual(m.headers["to"], ["sip:joe@bell.example.com"])
+ self.assertEqual(m.headers["contact"], ["sip:joe@client.com:5060"])
+ self.failUnless(
+ int(m.headers["expires"][0]) in (3600, 3601, 3599, 3598))
+ self.assertEqual(len(self.registry.users), 1)
+ dc, uri = self.registry.users["joe"]
+ self.assertEqual(uri.toString(), "sip:joe@client.com:5060")
+ d = self.proxy.locator.getAddress(sip.URL(username="joe",
+ host="bell.example.com"))
+ d.addCallback(lambda desturl : (desturl.host, desturl.port))
+ d.addCallback(self.assertEqual, ('client.com', 5060))
+ return d
+
+ def testUnregister(self):
+ self.register()
+ self.unregister()
+ dest, m = self.sent[1]
+ self.assertEqual((dest.host, dest.port), ("client.com", 5060))
+ self.assertEqual(m.code, 200)
+ self.assertEqual(m.headers["via"], ["SIP/2.0/UDP client.com:5060"])
+ self.assertEqual(m.headers["to"], ["sip:joe@bell.example.com"])
+ self.assertEqual(m.headers["contact"], ["sip:joe@client.com:5060"])
+ self.assertEqual(m.headers["expires"], ["0"])
+ self.assertEqual(self.registry.users, {})
+
+
+ def addPortal(self):
+ r = TestRealm()
+ p = cred.portal.Portal(r)
+ c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ c.addUser('userXname@127.0.0.1', 'passXword')
+ p.registerChecker(c)
+ self.proxy.portal = p
+
+ def testFailedAuthentication(self):
+ self.addPortal()
+ self.register()
+
+ self.assertEqual(len(self.registry.users), 0)
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual(m.code, 401)
+
+
+ def test_basicAuthentication(self):
+ """
+ Test that registration with basic authentication suceeds.
+ """
+ self.addPortal()
+ self.proxy.authorizers = self.proxy.authorizers.copy()
+ self.proxy.authorizers['basic'] = sip.BasicAuthorizer()
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_basicAuthentication])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.protocols.sip.BasicAuthorizer was deprecated in "
+ "Twisted 9.0.0")
+ self.assertEqual(
+ warnings[0]['category'],
+ DeprecationWarning)
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@client.com:1234")
+ r.addHeader("via", sip.Via("client.com").toString())
+ r.addHeader("authorization",
+ "Basic " + "userXname:passXword".encode('base64'))
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+
+ self.assertEqual(len(self.registry.users), 1)
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual(m.code, 200)
+
+
+ def test_failedBasicAuthentication(self):
+ """
+ Failed registration with basic authentication results in an
+ unauthorized error response.
+ """
+ self.addPortal()
+ self.proxy.authorizers = self.proxy.authorizers.copy()
+ self.proxy.authorizers['basic'] = sip.BasicAuthorizer()
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_failedBasicAuthentication])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.protocols.sip.BasicAuthorizer was deprecated in "
+ "Twisted 9.0.0")
+ self.assertEqual(
+ warnings[0]['category'],
+ DeprecationWarning)
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@client.com:1234")
+ r.addHeader("via", sip.Via("client.com").toString())
+ r.addHeader(
+ "authorization", "Basic " + "userXname:password".encode('base64'))
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+
+ self.assertEqual(len(self.registry.users), 0)
+ self.assertEqual(len(self.sent), 1)
+ dest, m = self.sent[0]
+ self.assertEqual(m.code, 401)
+
+
+ def testWrongDomainRegister(self):
+ r = sip.Request("REGISTER", "sip:wrong.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@client.com:1234")
+ r.addHeader("via", sip.Via("client.com").toString())
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+ self.assertEqual(len(self.sent), 0)
+
+ def testWrongToDomainRegister(self):
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@foo.com")
+ r.addHeader("contact", "sip:joe@client.com:1234")
+ r.addHeader("via", sip.Via("client.com").toString())
+ self.proxy.datagramReceived(r.toString(), ("client.com", 5060))
+ self.assertEqual(len(self.sent), 0)
+
+ def testWrongDomainLookup(self):
+ self.register()
+ url = sip.URL(username="joe", host="foo.com")
+ d = self.proxy.locator.getAddress(url)
+ self.assertFailure(d, LookupError)
+ return d
+
+ def testNoContactLookup(self):
+ self.register()
+ url = sip.URL(username="jane", host="bell.example.com")
+ d = self.proxy.locator.getAddress(url)
+ self.assertFailure(d, LookupError)
+ return d
+
+
+class Client(sip.Base):
+
+ def __init__(self):
+ sip.Base.__init__(self)
+ self.received = []
+ self.deferred = defer.Deferred()
+
+ def handle_response(self, response, addr):
+ self.received.append(response)
+ self.deferred.callback(self.received)
+
+
+class LiveTest(unittest.TestCase):
+
+ def setUp(self):
+ self.proxy = sip.RegisterProxy(host="127.0.0.1")
+ self.registry = sip.InMemoryRegistry("bell.example.com")
+ self.proxy.registry = self.proxy.locator = self.registry
+ self.serverPort = reactor.listenUDP(
+ 0, self.proxy, interface="127.0.0.1")
+ self.client = Client()
+ self.clientPort = reactor.listenUDP(
+ 0, self.client, interface="127.0.0.1")
+ self.serverAddress = (self.serverPort.getHost().host,
+ self.serverPort.getHost().port)
+ setUp = utils.suppressWarnings(setUp,
+ util.suppress(category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestAuthorizer was deprecated'))
+
+ def tearDown(self):
+ for d, uri in self.registry.users.values():
+ d.cancel()
+ d1 = defer.maybeDeferred(self.clientPort.stopListening)
+ d2 = defer.maybeDeferred(self.serverPort.stopListening)
+ return defer.gatherResults([d1, d2])
+
+ def testRegister(self):
+ p = self.clientPort.getHost().port
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@127.0.0.1:%d" % p)
+ r.addHeader("via", sip.Via("127.0.0.1", port=p).toString())
+ self.client.sendMessage(
+ sip.URL(host="127.0.0.1", port=self.serverAddress[1]), r)
+ d = self.client.deferred
+ def check(received):
+ self.assertEqual(len(received), 1)
+ r = received[0]
+ self.assertEqual(r.code, 200)
+ d.addCallback(check)
+ return d
+
+ def test_amoralRPort(self):
+ """
+ rport is allowed without a value, apparently because server
+ implementors might be too stupid to check the received port
+ against 5060 and see if they're equal, and because client
+ implementors might be too stupid to bind to port 5060, or set a
+ value on the rport parameter they send if they bind to another
+ port.
+ """
+ p = self.clientPort.getHost().port
+ r = sip.Request("REGISTER", "sip:bell.example.com")
+ r.addHeader("to", "sip:joe@bell.example.com")
+ r.addHeader("contact", "sip:joe@127.0.0.1:%d" % p)
+ r.addHeader("via", sip.Via("127.0.0.1", port=p, rport=True).toString())
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_amoralRPort])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'rport=True is deprecated since Twisted 9.0.')
+ self.assertEqual(
+ warnings[0]['category'],
+ DeprecationWarning)
+ self.client.sendMessage(sip.URL(host="127.0.0.1",
+ port=self.serverAddress[1]),
+ r)
+ d = self.client.deferred
+ def check(received):
+ self.assertEqual(len(received), 1)
+ r = received[0]
+ self.assertEqual(r.code, 200)
+ d.addCallback(check)
+ return d
+
+
+
+registerRequest = """
+REGISTER sip:intarweb.us SIP/2.0\r
+Via: SIP/2.0/UDP 192.168.1.100:50609\r
+From: <sip:exarkun@intarweb.us:50609>\r
+To: <sip:exarkun@intarweb.us:50609>\r
+Contact: "exarkun" <sip:exarkun@192.168.1.100:50609>\r
+Call-ID: 94E7E5DAF39111D791C6000393764646@intarweb.us\r
+CSeq: 9898 REGISTER\r
+Expires: 500\r
+User-Agent: X-Lite build 1061\r
+Content-Length: 0\r
+\r
+"""
+
+challengeResponse = """\
+SIP/2.0 401 Unauthorized\r
+Via: SIP/2.0/UDP 192.168.1.100:50609;received=127.0.0.1;rport=5632\r
+To: <sip:exarkun@intarweb.us:50609>\r
+From: <sip:exarkun@intarweb.us:50609>\r
+Call-ID: 94E7E5DAF39111D791C6000393764646@intarweb.us\r
+CSeq: 9898 REGISTER\r
+WWW-Authenticate: Digest nonce="92956076410767313901322208775",opaque="1674186428",qop-options="auth",algorithm="MD5",realm="intarweb.us"\r
+\r
+"""
+
+authRequest = """\
+REGISTER sip:intarweb.us SIP/2.0\r
+Via: SIP/2.0/UDP 192.168.1.100:50609\r
+From: <sip:exarkun@intarweb.us:50609>\r
+To: <sip:exarkun@intarweb.us:50609>\r
+Contact: "exarkun" <sip:exarkun@192.168.1.100:50609>\r
+Call-ID: 94E7E5DAF39111D791C6000393764646@intarweb.us\r
+CSeq: 9899 REGISTER\r
+Expires: 500\r
+Authorization: Digest username="exarkun",realm="intarweb.us",nonce="92956076410767313901322208775",response="4a47980eea31694f997369214292374b",uri="sip:intarweb.us",algorithm=MD5,opaque="1674186428"\r
+User-Agent: X-Lite build 1061\r
+Content-Length: 0\r
+\r
+"""
+
+okResponse = """\
+SIP/2.0 200 OK\r
+Via: SIP/2.0/UDP 192.168.1.100:50609;received=127.0.0.1;rport=5632\r
+To: <sip:exarkun@intarweb.us:50609>\r
+From: <sip:exarkun@intarweb.us:50609>\r
+Call-ID: 94E7E5DAF39111D791C6000393764646@intarweb.us\r
+CSeq: 9899 REGISTER\r
+Contact: sip:exarkun@127.0.0.1:5632\r
+Expires: 3600\r
+Content-Length: 0\r
+\r
+"""
+
+class FakeDigestAuthorizer(sip.DigestAuthorizer):
+ def generateNonce(self):
+ return '92956076410767313901322208775'
+ def generateOpaque(self):
+ return '1674186428'
+
+
+class FakeRegistry(sip.InMemoryRegistry):
+ """Make sure expiration is always seen to be 3600.
+
+ Otherwise slow reactors fail tests incorrectly.
+ """
+
+ def _cbReg(self, reg):
+ if 3600 < reg.secondsToExpiry or reg.secondsToExpiry < 3598:
+ raise RuntimeError(
+ "bad seconds to expire: %s" % reg.secondsToExpiry)
+ reg.secondsToExpiry = 3600
+ return reg
+
+ def getRegistrationInfo(self, uri):
+ d = sip.InMemoryRegistry.getRegistrationInfo(self, uri)
+ return d.addCallback(self._cbReg)
+
+ def registerAddress(self, domainURL, logicalURL, physicalURL):
+ d = sip.InMemoryRegistry.registerAddress(
+ self, domainURL, logicalURL, physicalURL)
+ return d.addCallback(self._cbReg)
+
+class AuthorizationTestCase(unittest.TestCase):
+ def setUp(self):
+ self.proxy = sip.RegisterProxy(host="intarweb.us")
+ self.proxy.authorizers = self.proxy.authorizers.copy()
+ self.proxy.authorizers['digest'] = FakeDigestAuthorizer()
+
+ self.registry = FakeRegistry("intarweb.us")
+ self.proxy.registry = self.proxy.locator = self.registry
+ self.transport = proto_helpers.FakeDatagramTransport()
+ self.proxy.transport = self.transport
+
+ r = TestRealm()
+ p = cred.portal.Portal(r)
+ c = cred.checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ c.addUser('exarkun@intarweb.us', 'password')
+ p.registerChecker(c)
+ self.proxy.portal = p
+ setUp = utils.suppressWarnings(setUp,
+ util.suppress(category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestAuthorizer was deprecated'))
+
+ def tearDown(self):
+ for d, uri in self.registry.users.values():
+ d.cancel()
+ del self.proxy
+
+ def testChallenge(self):
+ self.proxy.datagramReceived(registerRequest, ("127.0.0.1", 5632))
+
+ self.assertEqual(
+ self.transport.written[-1],
+ ((challengeResponse, ("127.0.0.1", 5632)))
+ )
+ self.transport.written = []
+
+ self.proxy.datagramReceived(authRequest, ("127.0.0.1", 5632))
+
+ self.assertEqual(
+ self.transport.written[-1],
+ ((okResponse, ("127.0.0.1", 5632)))
+ )
+ testChallenge.suppress = [
+ util.suppress(
+ category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestAuthorizer was deprecated'),
+ util.suppress(
+ category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestedCredentials was deprecated'),
+ util.suppress(
+ category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestCalcHA1 was deprecated'),
+ util.suppress(
+ category=DeprecationWarning,
+ message=r'twisted.protocols.sip.DigestCalcResponse was deprecated')]
+
+
+
+class DeprecationTests(unittest.TestCase):
+ """
+ Tests for deprecation of obsolete components of L{twisted.protocols.sip}.
+ """
+
+ def test_deprecatedDigestCalcHA1(self):
+ """
+ L{sip.DigestCalcHA1} is deprecated.
+ """
+ self.callDeprecated(Version("Twisted", 9, 0, 0),
+ sip.DigestCalcHA1, '', '', '', '', '', '')
+
+
+ def test_deprecatedDigestCalcResponse(self):
+ """
+ L{sip.DigestCalcResponse} is deprecated.
+ """
+ self.callDeprecated(Version("Twisted", 9, 0, 0),
+ sip.DigestCalcResponse, '', '', '', '', '', '', '',
+ '')
+
+ def test_deprecatedBasicAuthorizer(self):
+ """
+ L{sip.BasicAuthorizer} is deprecated.
+ """
+ self.callDeprecated(Version("Twisted", 9, 0, 0), sip.BasicAuthorizer)
+
+
+ def test_deprecatedDigestAuthorizer(self):
+ """
+ L{sip.DigestAuthorizer} is deprecated.
+ """
+ self.callDeprecated(Version("Twisted", 9, 0, 0), sip.DigestAuthorizer)
+
+
+ def test_deprecatedDigestedCredentials(self):
+ """
+ L{sip.DigestedCredentials} is deprecated.
+ """
+ self.callDeprecated(Version("Twisted", 9, 0, 0),
+ sip.DigestedCredentials, '', {}, {})
diff --git a/twisted/test/test_sob.py b/twisted/test/test_sob.py
new file mode 100644
index 0000000..76c33a8
--- /dev/null
+++ b/twisted/test/test_sob.py
@@ -0,0 +1,172 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+import sys, os
+
+try:
+ import Crypto.Cipher.AES
+except ImportError:
+ Crypto = None
+
+from twisted.trial import unittest
+from twisted.persisted import sob
+from twisted.python import components
+
+class Dummy(components.Componentized):
+ pass
+
+objects = [
+1,
+"hello",
+(1, "hello"),
+[1, "hello"],
+{1:"hello"},
+]
+
+class FakeModule(object):
+ pass
+
+class PersistTestCase(unittest.TestCase):
+ def testStyles(self):
+ for o in objects:
+ p = sob.Persistent(o, '')
+ for style in 'source pickle'.split():
+ p.setStyle(style)
+ p.save(filename='persisttest.'+style)
+ o1 = sob.load('persisttest.'+style, style)
+ self.assertEqual(o, o1)
+
+ def testStylesBeingSet(self):
+ o = Dummy()
+ o.foo = 5
+ o.setComponent(sob.IPersistable, sob.Persistent(o, 'lala'))
+ for style in 'source pickle'.split():
+ sob.IPersistable(o).setStyle(style)
+ sob.IPersistable(o).save(filename='lala.'+style)
+ o1 = sob.load('lala.'+style, style)
+ self.assertEqual(o.foo, o1.foo)
+ self.assertEqual(sob.IPersistable(o1).style, style)
+
+
+ def testNames(self):
+ o = [1,2,3]
+ p = sob.Persistent(o, 'object')
+ for style in 'source pickle'.split():
+ p.setStyle(style)
+ p.save()
+ o1 = sob.load('object.ta'+style[0], style)
+ self.assertEqual(o, o1)
+ for tag in 'lala lolo'.split():
+ p.save(tag)
+ o1 = sob.load('object-'+tag+'.ta'+style[0], style)
+ self.assertEqual(o, o1)
+
+ def testEncryptedStyles(self):
+ for o in objects:
+ phrase='once I was the king of spain'
+ p = sob.Persistent(o, '')
+ for style in 'source pickle'.split():
+ p.setStyle(style)
+ p.save(filename='epersisttest.'+style, passphrase=phrase)
+ o1 = sob.load('epersisttest.'+style, style, phrase)
+ self.assertEqual(o, o1)
+ if Crypto is None:
+ testEncryptedStyles.skip = "PyCrypto required for encrypted config"
+
+ def testPython(self):
+ f = open("persisttest.python", 'w')
+ f.write('foo=[1,2,3] ')
+ f.close()
+ o = sob.loadValueFromFile('persisttest.python', 'foo')
+ self.assertEqual(o, [1,2,3])
+
+ def testEncryptedPython(self):
+ phrase='once I was the king of spain'
+ f = open("epersisttest.python", 'w')
+ f.write(
+ sob._encrypt(phrase, 'foo=[1,2,3]'))
+ f.close()
+ o = sob.loadValueFromFile('epersisttest.python', 'foo', phrase)
+ self.assertEqual(o, [1,2,3])
+ if Crypto is None:
+ testEncryptedPython.skip = "PyCrypto required for encrypted config"
+
+ def testTypeGuesser(self):
+ self.assertRaises(KeyError, sob.guessType, "file.blah")
+ self.assertEqual('python', sob.guessType("file.py"))
+ self.assertEqual('python', sob.guessType("file.tac"))
+ self.assertEqual('python', sob.guessType("file.etac"))
+ self.assertEqual('pickle', sob.guessType("file.tap"))
+ self.assertEqual('pickle', sob.guessType("file.etap"))
+ self.assertEqual('source', sob.guessType("file.tas"))
+ self.assertEqual('source', sob.guessType("file.etas"))
+
+ def testEverythingEphemeralGetattr(self):
+ """
+ Verify that _EverythingEphermal.__getattr__ works.
+ """
+ self.fakeMain.testMainModGetattr = 1
+
+ dirname = self.mktemp()
+ os.mkdir(dirname)
+
+ filename = os.path.join(dirname, 'persisttest.ee_getattr')
+
+ f = file(filename, 'w')
+ f.write('import __main__\n')
+ f.write('if __main__.testMainModGetattr != 1: raise AssertionError\n')
+ f.write('app = None\n')
+ f.close()
+
+ sob.load(filename, 'source')
+
+ def testEverythingEphemeralSetattr(self):
+ """
+ Verify that _EverythingEphemeral.__setattr__ won't affect __main__.
+ """
+ self.fakeMain.testMainModSetattr = 1
+
+ dirname = self.mktemp()
+ os.mkdir(dirname)
+
+ filename = os.path.join(dirname, 'persisttest.ee_setattr')
+ f = file(filename, 'w')
+ f.write('import __main__\n')
+ f.write('__main__.testMainModSetattr = 2\n')
+ f.write('app = None\n')
+ f.close()
+
+ sob.load(filename, 'source')
+
+ self.assertEqual(self.fakeMain.testMainModSetattr, 1)
+
+ def testEverythingEphemeralException(self):
+ """
+ Test that an exception during load() won't cause _EE to mask __main__
+ """
+ dirname = self.mktemp()
+ os.mkdir(dirname)
+ filename = os.path.join(dirname, 'persisttest.ee_exception')
+
+ f = file(filename, 'w')
+ f.write('raise ValueError\n')
+ f.close()
+
+ self.assertRaises(ValueError, sob.load, filename, 'source')
+ self.assertEqual(type(sys.modules['__main__']), FakeModule)
+
+ def setUp(self):
+ """
+ Replace the __main__ module with a fake one, so that it can be mutated
+ in tests
+ """
+ self.realMain = sys.modules['__main__']
+ self.fakeMain = sys.modules['__main__'] = FakeModule()
+
+ def tearDown(self):
+ """
+ Restore __main__ to its original value
+ """
+ sys.modules['__main__'] = self.realMain
+
diff --git a/twisted/test/test_socks.py b/twisted/test/test_socks.py
new file mode 100644
index 0000000..ebcb843
--- /dev/null
+++ b/twisted/test/test_socks.py
@@ -0,0 +1,498 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.protocol.socks}, an implementation of the SOCKSv4 and
+SOCKSv4a protocols.
+"""
+
+import struct, socket
+
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+from twisted.internet import defer, address, reactor
+from twisted.internet.error import DNSLookupError
+from twisted.protocols import socks
+
+
+class StringTCPTransport(proto_helpers.StringTransport):
+ stringTCPTransport_closing = False
+ peer = None
+
+ def getPeer(self):
+ return self.peer
+
+ def getHost(self):
+ return address.IPv4Address('TCP', '2.3.4.5', 42)
+
+ def loseConnection(self):
+ self.stringTCPTransport_closing = True
+
+
+
+class FakeResolverReactor:
+ """
+ Bare-bones reactor with deterministic behavior for the resolve method.
+ """
+ def __init__(self, names):
+ """
+ @type names: C{dict} containing C{str} keys and C{str} values.
+ @param names: A hostname to IP address mapping. The IP addresses are
+ stringified dotted quads.
+ """
+ self.names = names
+
+
+ def resolve(self, hostname):
+ """
+ Resolve a hostname by looking it up in the C{names} dictionary.
+ """
+ try:
+ return defer.succeed(self.names[hostname])
+ except KeyError:
+ return defer.fail(
+ DNSLookupError("FakeResolverReactor couldn't find " + hostname))
+
+
+
+class SOCKSv4Driver(socks.SOCKSv4):
+ # last SOCKSv4Outgoing instantiated
+ driver_outgoing = None
+
+ # last SOCKSv4IncomingFactory instantiated
+ driver_listen = None
+
+ def connectClass(self, host, port, klass, *args):
+ # fake it
+ proto = klass(*args)
+ proto.transport = StringTCPTransport()
+ proto.transport.peer = address.IPv4Address('TCP', host, port)
+ proto.connectionMade()
+ self.driver_outgoing = proto
+ return defer.succeed(proto)
+
+ def listenClass(self, port, klass, *args):
+ # fake it
+ factory = klass(*args)
+ self.driver_listen = factory
+ if port == 0:
+ port = 1234
+ return defer.succeed(('6.7.8.9', port))
+
+
+
+class Connect(unittest.TestCase):
+ """
+ Tests for SOCKS and SOCKSv4a connect requests using the L{SOCKSv4} protocol.
+ """
+ def setUp(self):
+ self.sock = SOCKSv4Driver()
+ self.sock.transport = StringTCPTransport()
+ self.sock.connectionMade()
+ self.sock.reactor = FakeResolverReactor({"localhost":"127.0.0.1"})
+
+
+ def tearDown(self):
+ outgoing = self.sock.driver_outgoing
+ if outgoing is not None:
+ self.assert_(outgoing.transport.stringTCPTransport_closing,
+ "Outgoing SOCKS connections need to be closed.")
+
+
+ def test_simple(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 1, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 34)
+ + socket.inet_aton('1.2.3.4'))
+ self.assert_(not self.sock.transport.stringTCPTransport_closing)
+ self.assert_(self.sock.driver_outgoing is not None)
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(self.sock.driver_outgoing.transport.value(),
+ 'hello, world')
+
+ # the other way around
+ self.sock.driver_outgoing.dataReceived('hi there')
+ self.assertEqual(self.sock.transport.value(), 'hi there')
+
+ self.sock.connectionLost('fake reason')
+
+
+ def test_socks4aSuccessfulResolution(self):
+ """
+ If the destination IP address has zeros for the first three octets and
+ non-zero for the fourth octet, the client is attempting a v4a
+ connection. A hostname is specified after the user ID string and the
+ server connects to the address that hostname resolves to.
+
+ @see: U{http://en.wikipedia.org/wiki/SOCKS#SOCKS_4a_protocol}
+ """
+ # send the domain name "localhost" to be resolved
+ clientRequest = (
+ struct.pack('!BBH', 4, 1, 34)
+ + socket.inet_aton('0.0.0.1')
+ + 'fooBAZ\0'
+ + 'localhost\0')
+
+ # Deliver the bytes one by one to exercise the protocol's buffering
+ # logic. FakeResolverReactor's resolve method is invoked to "resolve"
+ # the hostname.
+ for byte in clientRequest:
+ self.sock.dataReceived(byte)
+
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # Verify that the server responded with the address which will be
+ # connected to.
+ self.assertEqual(
+ sent,
+ struct.pack('!BBH', 0, 90, 34) + socket.inet_aton('127.0.0.1'))
+ self.assertFalse(self.sock.transport.stringTCPTransport_closing)
+ self.assertNotIdentical(self.sock.driver_outgoing, None)
+
+ # Pass some data through and verify it is forwarded to the outgoing
+ # connection.
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(
+ self.sock.driver_outgoing.transport.value(), 'hello, world')
+
+ # Deliver some data from the output connection and verify it is
+ # passed along to the incoming side.
+ self.sock.driver_outgoing.dataReceived('hi there')
+ self.assertEqual(self.sock.transport.value(), 'hi there')
+
+ self.sock.connectionLost('fake reason')
+
+
+ def test_socks4aFailedResolution(self):
+ """
+ Failed hostname resolution on a SOCKSv4a packet results in a 91 error
+ response and the connection getting closed.
+ """
+ # send the domain name "failinghost" to be resolved
+ clientRequest = (
+ struct.pack('!BBH', 4, 1, 34)
+ + socket.inet_aton('0.0.0.1')
+ + 'fooBAZ\0'
+ + 'failinghost\0')
+
+ # Deliver the bytes one by one to exercise the protocol's buffering
+ # logic. FakeResolverReactor's resolve method is invoked to "resolve"
+ # the hostname.
+ for byte in clientRequest:
+ self.sock.dataReceived(byte)
+
+ # Verify that the server responds with a 91 error.
+ sent = self.sock.transport.value()
+ self.assertEqual(
+ sent,
+ struct.pack('!BBH', 0, 91, 0) + socket.inet_aton('0.0.0.0'))
+
+ # A failed resolution causes the transport to drop the connection.
+ self.assertTrue(self.sock.transport.stringTCPTransport_closing)
+ self.assertIdentical(self.sock.driver_outgoing, None)
+
+
+ def test_accessDenied(self):
+ self.sock.authorize = lambda code, server, port, user: 0
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 1, 4242)
+ + socket.inet_aton('10.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ self.assertEqual(self.sock.transport.value(),
+ struct.pack('!BBH', 0, 91, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(self.sock.transport.stringTCPTransport_closing)
+ self.assertIdentical(self.sock.driver_outgoing, None)
+
+
+ def test_eofRemote(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 1, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(self.sock.driver_outgoing.transport.value(),
+ 'hello, world')
+
+ # now close it from the server side
+ self.sock.driver_outgoing.transport.loseConnection()
+ self.sock.driver_outgoing.connectionLost('fake reason')
+
+
+ def test_eofLocal(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 1, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(self.sock.driver_outgoing.transport.value(),
+ 'hello, world')
+
+ # now close it from the client side
+ self.sock.connectionLost('fake reason')
+
+
+
+class Bind(unittest.TestCase):
+ """
+ Tests for SOCKS and SOCKSv4a bind requests using the L{SOCKSv4} protocol.
+ """
+ def setUp(self):
+ self.sock = SOCKSv4Driver()
+ self.sock.transport = StringTCPTransport()
+ self.sock.connectionMade()
+ self.sock.reactor = FakeResolverReactor({"localhost":"127.0.0.1"})
+
+## def tearDown(self):
+## # TODO ensure the listen port is closed
+## listen = self.sock.driver_listen
+## if listen is not None:
+## self.assert_(incoming.transport.stringTCPTransport_closing,
+## "Incoming SOCKS connections need to be closed.")
+
+ def test_simple(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 1234)
+ + socket.inet_aton('6.7.8.9'))
+ self.assert_(not self.sock.transport.stringTCPTransport_closing)
+ self.assert_(self.sock.driver_listen is not None)
+
+ # connect
+ incoming = self.sock.driver_listen.buildProtocol(('1.2.3.4', 5345))
+ self.assertNotIdentical(incoming, None)
+ incoming.transport = StringTCPTransport()
+ incoming.connectionMade()
+
+ # now we should have the second reply packet
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(not self.sock.transport.stringTCPTransport_closing)
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(incoming.transport.value(),
+ 'hello, world')
+
+ # the other way around
+ incoming.dataReceived('hi there')
+ self.assertEqual(self.sock.transport.value(), 'hi there')
+
+ self.sock.connectionLost('fake reason')
+
+
+ def test_socks4a(self):
+ """
+ If the destination IP address has zeros for the first three octets and
+ non-zero for the fourth octet, the client is attempting a v4a
+ connection. A hostname is specified after the user ID string and the
+ server connects to the address that hostname resolves to.
+
+ @see: U{http://en.wikipedia.org/wiki/SOCKS#SOCKS_4a_protocol}
+ """
+ # send the domain name "localhost" to be resolved
+ clientRequest = (
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('0.0.0.1')
+ + 'fooBAZ\0'
+ + 'localhost\0')
+
+ # Deliver the bytes one by one to exercise the protocol's buffering
+ # logic. FakeResolverReactor's resolve method is invoked to "resolve"
+ # the hostname.
+ for byte in clientRequest:
+ self.sock.dataReceived(byte)
+
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # Verify that the server responded with the address which will be
+ # connected to.
+ self.assertEqual(
+ sent,
+ struct.pack('!BBH', 0, 90, 1234) + socket.inet_aton('6.7.8.9'))
+ self.assertFalse(self.sock.transport.stringTCPTransport_closing)
+ self.assertNotIdentical(self.sock.driver_listen, None)
+
+ # connect
+ incoming = self.sock.driver_listen.buildProtocol(('127.0.0.1', 5345))
+ self.assertNotIdentical(incoming, None)
+ incoming.transport = StringTCPTransport()
+ incoming.connectionMade()
+
+ # now we should have the second reply packet
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assertNotIdentical(
+ self.sock.transport.stringTCPTransport_closing, None)
+
+ # Deliver some data from the output connection and verify it is
+ # passed along to the incoming side.
+ self.sock.dataReceived('hi there')
+ self.assertEqual(incoming.transport.value(), 'hi there')
+
+ # the other way around
+ incoming.dataReceived('hi there')
+ self.assertEqual(self.sock.transport.value(), 'hi there')
+
+ self.sock.connectionLost('fake reason')
+
+
+ def test_socks4aFailedResolution(self):
+ """
+ Failed hostname resolution on a SOCKSv4a packet results in a 91 error
+ response and the connection getting closed.
+ """
+ # send the domain name "failinghost" to be resolved
+ clientRequest = (
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('0.0.0.1')
+ + 'fooBAZ\0'
+ + 'failinghost\0')
+
+ # Deliver the bytes one by one to exercise the protocol's buffering
+ # logic. FakeResolverReactor's resolve method is invoked to "resolve"
+ # the hostname.
+ for byte in clientRequest:
+ self.sock.dataReceived(byte)
+
+ # Verify that the server responds with a 91 error.
+ sent = self.sock.transport.value()
+ self.assertEqual(
+ sent,
+ struct.pack('!BBH', 0, 91, 0) + socket.inet_aton('0.0.0.0'))
+
+ # A failed resolution causes the transport to drop the connection.
+ self.assertTrue(self.sock.transport.stringTCPTransport_closing)
+ self.assertIdentical(self.sock.driver_outgoing, None)
+
+
+ def test_accessDenied(self):
+ self.sock.authorize = lambda code, server, port, user: 0
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 2, 4242)
+ + socket.inet_aton('10.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ self.assertEqual(self.sock.transport.value(),
+ struct.pack('!BBH', 0, 91, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(self.sock.transport.stringTCPTransport_closing)
+ self.assertIdentical(self.sock.driver_listen, None)
+
+ def test_eofRemote(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # connect
+ incoming = self.sock.driver_listen.buildProtocol(('1.2.3.4', 5345))
+ self.assertNotIdentical(incoming, None)
+ incoming.transport = StringTCPTransport()
+ incoming.connectionMade()
+
+ # now we should have the second reply packet
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(not self.sock.transport.stringTCPTransport_closing)
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(incoming.transport.value(),
+ 'hello, world')
+
+ # now close it from the server side
+ incoming.transport.loseConnection()
+ incoming.connectionLost('fake reason')
+
+ def test_eofLocal(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # connect
+ incoming = self.sock.driver_listen.buildProtocol(('1.2.3.4', 5345))
+ self.assertNotIdentical(incoming, None)
+ incoming.transport = StringTCPTransport()
+ incoming.connectionMade()
+
+ # now we should have the second reply packet
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 90, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(not self.sock.transport.stringTCPTransport_closing)
+
+ # pass some data through
+ self.sock.dataReceived('hello, world')
+ self.assertEqual(incoming.transport.value(),
+ 'hello, world')
+
+ # now close it from the client side
+ self.sock.connectionLost('fake reason')
+
+ def test_badSource(self):
+ self.sock.dataReceived(
+ struct.pack('!BBH', 4, 2, 34)
+ + socket.inet_aton('1.2.3.4')
+ + 'fooBAR'
+ + '\0')
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+
+ # connect from WRONG address
+ incoming = self.sock.driver_listen.buildProtocol(('1.6.6.6', 666))
+ self.assertIdentical(incoming, None)
+
+ # Now we should have the second reply packet and it should
+ # be a failure. The connection should be closing.
+ sent = self.sock.transport.value()
+ self.sock.transport.clear()
+ self.assertEqual(sent,
+ struct.pack('!BBH', 0, 91, 0)
+ + socket.inet_aton('0.0.0.0'))
+ self.assert_(self.sock.transport.stringTCPTransport_closing)
diff --git a/twisted/test/test_ssl.py b/twisted/test/test_ssl.py
new file mode 100644
index 0000000..6d1a3ec
--- /dev/null
+++ b/twisted/test/test_ssl.py
@@ -0,0 +1,726 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for twisted SSL support.
+"""
+
+from twisted.trial import unittest
+from twisted.internet import protocol, reactor, interfaces, defer
+from twisted.internet.error import ConnectionDone
+from twisted.protocols import basic
+from twisted.python import util
+from twisted.python.runtime import platform
+from twisted.test.test_tcp import ProperlyCloseFilesMixin
+
+import os, errno
+
+try:
+ from OpenSSL import SSL, crypto
+ from twisted.internet import ssl
+ from twisted.test.ssl_helpers import ClientTLSContext
+except ImportError:
+ def _noSSL():
+ # ugh, make pyflakes happy.
+ global SSL
+ global ssl
+ SSL = ssl = None
+ _noSSL()
+
+try:
+ from twisted.protocols import tls as newTLS
+except ImportError:
+ # Assuming SSL exists, we're using old version in reactor (i.e. non-protocol)
+ newTLS = None
+
+certPath = util.sibpath(__file__, "server.pem")
+
+
+
+class UnintelligentProtocol(basic.LineReceiver):
+ """
+ @ivar deferred: a deferred that will fire at connection lost.
+ @type deferred: L{defer.Deferred}
+
+ @cvar pretext: text sent before TLS is set up.
+ @type pretext: C{str}
+
+ @cvar posttext: text sent after TLS is set up.
+ @type posttext: C{str}
+ """
+ pretext = [
+ "first line",
+ "last thing before tls starts",
+ "STARTTLS"]
+
+ posttext = [
+ "first thing after tls started",
+ "last thing ever"]
+
+ def __init__(self):
+ self.deferred = defer.Deferred()
+
+
+ def connectionMade(self):
+ for l in self.pretext:
+ self.sendLine(l)
+
+
+ def lineReceived(self, line):
+ if line == "READY":
+ self.transport.startTLS(ClientTLSContext(), self.factory.client)
+ for l in self.posttext:
+ self.sendLine(l)
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ self.deferred.callback(None)
+
+
+
+class LineCollector(basic.LineReceiver):
+ """
+ @ivar deferred: a deferred that will fire at connection lost.
+ @type deferred: L{defer.Deferred}
+
+ @ivar doTLS: whether the protocol is initiate TLS or not.
+ @type doTLS: C{bool}
+
+ @ivar fillBuffer: if set to True, it will send lots of data once
+ C{STARTTLS} is received.
+ @type fillBuffer: C{bool}
+ """
+
+ def __init__(self, doTLS, fillBuffer=False):
+ self.doTLS = doTLS
+ self.fillBuffer = fillBuffer
+ self.deferred = defer.Deferred()
+
+
+ def connectionMade(self):
+ self.factory.rawdata = ''
+ self.factory.lines = []
+
+
+ def lineReceived(self, line):
+ self.factory.lines.append(line)
+ if line == 'STARTTLS':
+ if self.fillBuffer:
+ for x in range(500):
+ self.sendLine('X' * 1000)
+ self.sendLine('READY')
+ if self.doTLS:
+ ctx = ServerTLSContext(
+ privateKeyFileName=certPath,
+ certificateFileName=certPath,
+ )
+ self.transport.startTLS(ctx, self.factory.server)
+ else:
+ self.setRawMode()
+
+
+ def rawDataReceived(self, data):
+ self.factory.rawdata += data
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ self.deferred.callback(None)
+
+
+
+class SingleLineServerProtocol(protocol.Protocol):
+ """
+ A protocol that sends a single line of data at C{connectionMade}.
+ """
+
+ def connectionMade(self):
+ self.transport.write("+OK <some crap>\r\n")
+ self.transport.getPeerCertificate()
+
+
+
+class RecordingClientProtocol(protocol.Protocol):
+ """
+ @ivar deferred: a deferred that will fire with first received content.
+ @type deferred: L{defer.Deferred}
+ """
+
+ def __init__(self):
+ self.deferred = defer.Deferred()
+
+
+ def connectionMade(self):
+ self.transport.getPeerCertificate()
+
+
+ def dataReceived(self, data):
+ self.deferred.callback(data)
+
+
+
+class ImmediatelyDisconnectingProtocol(protocol.Protocol):
+ """
+ A protocol that disconnect immediately on connection. It fires the
+ C{connectionDisconnected} deferred of its factory on connetion lost.
+ """
+
+ def connectionMade(self):
+ self.transport.loseConnection()
+
+
+ def connectionLost(self, reason):
+ self.factory.connectionDisconnected.callback(None)
+
+
+
+def generateCertificateObjects(organization, organizationalUnit):
+ """
+ Create a certificate for given C{organization} and C{organizationalUnit}.
+
+ @return: a tuple of (key, request, certificate) objects.
+ """
+ pkey = crypto.PKey()
+ pkey.generate_key(crypto.TYPE_RSA, 512)
+ req = crypto.X509Req()
+ subject = req.get_subject()
+ subject.O = organization
+ subject.OU = organizationalUnit
+ req.set_pubkey(pkey)
+ req.sign(pkey, "md5")
+
+ # Here comes the actual certificate
+ cert = crypto.X509()
+ cert.set_serial_number(1)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(60) # Testing certificates need not be long lived
+ cert.set_issuer(req.get_subject())
+ cert.set_subject(req.get_subject())
+ cert.set_pubkey(req.get_pubkey())
+ cert.sign(pkey, "md5")
+
+ return pkey, req, cert
+
+
+
+def generateCertificateFiles(basename, organization, organizationalUnit):
+ """
+ Create certificate files key, req and cert prefixed by C{basename} for
+ given C{organization} and C{organizationalUnit}.
+ """
+ pkey, req, cert = generateCertificateObjects(organization, organizationalUnit)
+
+ for ext, obj, dumpFunc in [
+ ('key', pkey, crypto.dump_privatekey),
+ ('req', req, crypto.dump_certificate_request),
+ ('cert', cert, crypto.dump_certificate)]:
+ fName = os.extsep.join((basename, ext))
+ fObj = file(fName, 'w')
+ fObj.write(dumpFunc(crypto.FILETYPE_PEM, obj))
+ fObj.close()
+
+
+
+class ContextGeneratingMixin:
+ """
+ Offer methods to create L{ssl.DefaultOpenSSLContextFactory} for both client
+ and server.
+
+ @ivar clientBase: prefix of client certificate files.
+ @type clientBase: C{str}
+
+ @ivar serverBase: prefix of server certificate files.
+ @type serverBase: C{str}
+
+ @ivar clientCtxFactory: a generated context factory to be used in
+ C{reactor.connectSSL}.
+ @type clientCtxFactory: L{ssl.DefaultOpenSSLContextFactory}
+
+ @ivar serverCtxFactory: a generated context factory to be used in
+ C{reactor.listenSSL}.
+ @type serverCtxFactory: L{ssl.DefaultOpenSSLContextFactory}
+ """
+
+ def makeContextFactory(self, org, orgUnit, *args, **kwArgs):
+ base = self.mktemp()
+ generateCertificateFiles(base, org, orgUnit)
+ serverCtxFactory = ssl.DefaultOpenSSLContextFactory(
+ os.extsep.join((base, 'key')),
+ os.extsep.join((base, 'cert')),
+ *args, **kwArgs)
+
+ return base, serverCtxFactory
+
+
+ def setupServerAndClient(self, clientArgs, clientKwArgs, serverArgs,
+ serverKwArgs):
+ self.clientBase, self.clientCtxFactory = self.makeContextFactory(
+ *clientArgs, **clientKwArgs)
+ self.serverBase, self.serverCtxFactory = self.makeContextFactory(
+ *serverArgs, **serverKwArgs)
+
+
+
+if SSL is not None:
+ class ServerTLSContext(ssl.DefaultOpenSSLContextFactory):
+ """
+ A context factory with a default method set to L{SSL.TLSv1_METHOD}.
+ """
+ isClient = False
+
+ def __init__(self, *args, **kw):
+ kw['sslmethod'] = SSL.TLSv1_METHOD
+ ssl.DefaultOpenSSLContextFactory.__init__(self, *args, **kw)
+
+
+
+class StolenTCPTestCase(ProperlyCloseFilesMixin, unittest.TestCase):
+ """
+ For SSL transports, test many of the same things which are tested for
+ TCP transports.
+ """
+
+ def createServer(self, address, portNumber, factory):
+ """
+ Create an SSL server with a certificate using L{IReactorSSL.listenSSL}.
+ """
+ cert = ssl.PrivateCertificate.loadPEM(file(certPath).read())
+ contextFactory = cert.options()
+ return reactor.listenSSL(
+ portNumber, factory, contextFactory, interface=address)
+
+
+ def connectClient(self, address, portNumber, clientCreator):
+ """
+ Create an SSL client using L{IReactorSSL.connectSSL}.
+ """
+ contextFactory = ssl.CertificateOptions()
+ return clientCreator.connectSSL(address, portNumber, contextFactory)
+
+
+ def getHandleExceptionType(self):
+ """
+ Return L{SSL.Error} as the expected error type which will be raised by
+ a write to the L{OpenSSL.SSL.Connection} object after it has been
+ closed.
+ """
+ return SSL.Error
+
+
+ def getHandleErrorCode(self):
+ """
+ Return the argument L{SSL.Error} will be constructed with for this
+ case. This is basically just a random OpenSSL implementation detail.
+ It would be better if this test worked in a way which did not require
+ this.
+ """
+ # Windows 2000 SP 4 and Windows XP SP 2 give back WSAENOTSOCK for
+ # SSL.Connection.write for some reason. The twisted.protocols.tls
+ # implementation of IReactorSSL doesn't suffer from this imprecation,
+ # though, since it is isolated from the Windows I/O layer (I suppose?).
+
+ # If test_properlyCloseFiles waited for the SSL handshake to complete
+ # and performed an orderly shutdown, then this would probably be a
+ # little less weird: writing to a shutdown SSL connection has a more
+ # well-defined failure mode (or at least it should).
+
+ # So figure out if twisted.protocols.tls is in use. If it can be
+ # imported, it should be.
+ try:
+ import twisted.protocols.tls
+ except ImportError:
+ # It isn't available, so we expect WSAENOTSOCK if we're on Windows.
+ if platform.getType() == 'win32':
+ return errno.WSAENOTSOCK
+
+ # Otherwise, we expect an error about how we tried to write to a
+ # shutdown connection. This is terribly implementation-specific.
+ return [('SSL routines', 'SSL_write', 'protocol is shutdown')]
+
+
+
+class TLSTestCase(unittest.TestCase):
+ """
+ Tests for startTLS support.
+
+ @ivar fillBuffer: forwarded to L{LineCollector.fillBuffer}
+ @type fillBuffer: C{bool}
+ """
+ fillBuffer = False
+
+ clientProto = None
+ serverProto = None
+
+
+ def tearDown(self):
+ if self.clientProto.transport is not None:
+ self.clientProto.transport.loseConnection()
+ if self.serverProto.transport is not None:
+ self.serverProto.transport.loseConnection()
+
+
+ def _runTest(self, clientProto, serverProto, clientIsServer=False):
+ """
+ Helper method to run TLS tests.
+
+ @param clientProto: protocol instance attached to the client
+ connection.
+ @param serverProto: protocol instance attached to the server
+ connection.
+ @param clientIsServer: flag indicated if client should initiate
+ startTLS instead of server.
+
+ @return: a L{defer.Deferred} that will fire when both connections are
+ lost.
+ """
+ self.clientProto = clientProto
+ cf = self.clientFactory = protocol.ClientFactory()
+ cf.protocol = lambda: clientProto
+ if clientIsServer:
+ cf.server = False
+ else:
+ cf.client = True
+
+ self.serverProto = serverProto
+ sf = self.serverFactory = protocol.ServerFactory()
+ sf.protocol = lambda: serverProto
+ if clientIsServer:
+ sf.client = False
+ else:
+ sf.server = True
+
+ port = reactor.listenTCP(0, sf, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+
+ reactor.connectTCP('127.0.0.1', port.getHost().port, cf)
+
+ return defer.gatherResults([clientProto.deferred, serverProto.deferred])
+
+
+ def test_TLS(self):
+ """
+ Test for server and client startTLS: client should received data both
+ before and after the startTLS.
+ """
+ def check(ignore):
+ self.assertEqual(
+ self.serverFactory.lines,
+ UnintelligentProtocol.pretext + UnintelligentProtocol.posttext
+ )
+ d = self._runTest(UnintelligentProtocol(),
+ LineCollector(True, self.fillBuffer))
+ return d.addCallback(check)
+
+
+ def test_unTLS(self):
+ """
+ Test for server startTLS not followed by a startTLS in client: the data
+ received after server startTLS should be received as raw.
+ """
+ def check(ignored):
+ self.assertEqual(
+ self.serverFactory.lines,
+ UnintelligentProtocol.pretext
+ )
+ self.failUnless(self.serverFactory.rawdata,
+ "No encrypted bytes received")
+ d = self._runTest(UnintelligentProtocol(),
+ LineCollector(False, self.fillBuffer))
+ return d.addCallback(check)
+
+
+ def test_backwardsTLS(self):
+ """
+ Test startTLS first initiated by client.
+ """
+ def check(ignored):
+ self.assertEqual(
+ self.clientFactory.lines,
+ UnintelligentProtocol.pretext + UnintelligentProtocol.posttext
+ )
+ d = self._runTest(LineCollector(True, self.fillBuffer),
+ UnintelligentProtocol(), True)
+ return d.addCallback(check)
+
+
+
+class SpammyTLSTestCase(TLSTestCase):
+ """
+ Test TLS features with bytes sitting in the out buffer.
+ """
+ fillBuffer = True
+
+
+
+class BufferingTestCase(unittest.TestCase):
+ serverProto = None
+ clientProto = None
+
+
+ def tearDown(self):
+ if self.serverProto.transport is not None:
+ self.serverProto.transport.loseConnection()
+ if self.clientProto.transport is not None:
+ self.clientProto.transport.loseConnection()
+
+
+ def test_openSSLBuffering(self):
+ serverProto = self.serverProto = SingleLineServerProtocol()
+ clientProto = self.clientProto = RecordingClientProtocol()
+
+ server = protocol.ServerFactory()
+ client = self.client = protocol.ClientFactory()
+
+ server.protocol = lambda: serverProto
+ client.protocol = lambda: clientProto
+
+ sCTX = ssl.DefaultOpenSSLContextFactory(certPath, certPath)
+ cCTX = ssl.ClientContextFactory()
+
+ port = reactor.listenSSL(0, server, sCTX, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+
+ reactor.connectSSL('127.0.0.1', port.getHost().port, client, cCTX)
+
+ return clientProto.deferred.addCallback(
+ self.assertEqual, "+OK <some crap>\r\n")
+
+
+
+class ConnectionLostTestCase(unittest.TestCase, ContextGeneratingMixin):
+ """
+ SSL connection closing tests.
+ """
+
+ def testImmediateDisconnect(self):
+ org = "twisted.test.test_ssl"
+ self.setupServerAndClient(
+ (org, org + ", client"), {},
+ (org, org + ", server"), {})
+
+ # Set up a server, connect to it with a client, which should work since our verifiers
+ # allow anything, then disconnect.
+ serverProtocolFactory = protocol.ServerFactory()
+ serverProtocolFactory.protocol = protocol.Protocol
+ self.serverPort = serverPort = reactor.listenSSL(0,
+ serverProtocolFactory, self.serverCtxFactory)
+
+ clientProtocolFactory = protocol.ClientFactory()
+ clientProtocolFactory.protocol = ImmediatelyDisconnectingProtocol
+ clientProtocolFactory.connectionDisconnected = defer.Deferred()
+ clientConnector = reactor.connectSSL('127.0.0.1',
+ serverPort.getHost().port, clientProtocolFactory, self.clientCtxFactory)
+
+ return clientProtocolFactory.connectionDisconnected.addCallback(
+ lambda ignoredResult: self.serverPort.stopListening())
+
+
+ def test_bothSidesLoseConnection(self):
+ """
+ Both sides of SSL connection close connection; the connections should
+ close cleanly, and only after the underlying TCP connection has
+ disconnected.
+ """
+ class CloseAfterHandshake(protocol.Protocol):
+ def __init__(self):
+ self.done = defer.Deferred()
+
+ def connectionMade(self):
+ self.transport.write("a")
+
+ def dataReceived(self, data):
+ # If we got data, handshake is over:
+ self.transport.loseConnection()
+
+ def connectionLost(self2, reason):
+ self2.done.errback(reason)
+ del self2.done
+
+ org = "twisted.test.test_ssl"
+ self.setupServerAndClient(
+ (org, org + ", client"), {},
+ (org, org + ", server"), {})
+
+ serverProtocol = CloseAfterHandshake()
+ serverProtocolFactory = protocol.ServerFactory()
+ serverProtocolFactory.protocol = lambda: serverProtocol
+ serverPort = reactor.listenSSL(0,
+ serverProtocolFactory, self.serverCtxFactory)
+ self.addCleanup(serverPort.stopListening)
+
+ clientProtocol = CloseAfterHandshake()
+ clientProtocolFactory = protocol.ClientFactory()
+ clientProtocolFactory.protocol = lambda: clientProtocol
+ clientConnector = reactor.connectSSL('127.0.0.1',
+ serverPort.getHost().port, clientProtocolFactory, self.clientCtxFactory)
+
+ def checkResult(failure):
+ failure.trap(ConnectionDone)
+ return defer.gatherResults(
+ [clientProtocol.done.addErrback(checkResult),
+ serverProtocol.done.addErrback(checkResult)])
+
+ if newTLS is None:
+ test_bothSidesLoseConnection.skip = "Old SSL code doesn't always close cleanly."
+
+
+ def testFailedVerify(self):
+ org = "twisted.test.test_ssl"
+ self.setupServerAndClient(
+ (org, org + ", client"), {},
+ (org, org + ", server"), {})
+
+ def verify(*a):
+ return False
+ self.clientCtxFactory.getContext().set_verify(SSL.VERIFY_PEER, verify)
+
+ serverConnLost = defer.Deferred()
+ serverProtocol = protocol.Protocol()
+ serverProtocol.connectionLost = serverConnLost.callback
+ serverProtocolFactory = protocol.ServerFactory()
+ serverProtocolFactory.protocol = lambda: serverProtocol
+ self.serverPort = serverPort = reactor.listenSSL(0,
+ serverProtocolFactory, self.serverCtxFactory)
+
+ clientConnLost = defer.Deferred()
+ clientProtocol = protocol.Protocol()
+ clientProtocol.connectionLost = clientConnLost.callback
+ clientProtocolFactory = protocol.ClientFactory()
+ clientProtocolFactory.protocol = lambda: clientProtocol
+ clientConnector = reactor.connectSSL('127.0.0.1',
+ serverPort.getHost().port, clientProtocolFactory, self.clientCtxFactory)
+
+ dl = defer.DeferredList([serverConnLost, clientConnLost], consumeErrors=True)
+ return dl.addCallback(self._cbLostConns)
+
+
+ def _cbLostConns(self, results):
+ (sSuccess, sResult), (cSuccess, cResult) = results
+
+ self.failIf(sSuccess)
+ self.failIf(cSuccess)
+
+ acceptableErrors = [SSL.Error]
+
+ # Rather than getting a verification failure on Windows, we are getting
+ # a connection failure. Without something like sslverify proxying
+ # in-between we can't fix up the platform's errors, so let's just
+ # specifically say it is only OK in this one case to keep the tests
+ # passing. Normally we'd like to be as strict as possible here, so
+ # we're not going to allow this to report errors incorrectly on any
+ # other platforms.
+
+ if platform.isWindows():
+ from twisted.internet.error import ConnectionLost
+ acceptableErrors.append(ConnectionLost)
+
+ sResult.trap(*acceptableErrors)
+ cResult.trap(*acceptableErrors)
+
+ return self.serverPort.stopListening()
+
+
+
+class FakeContext:
+ """
+ L{OpenSSL.SSL.Context} double which can more easily be inspected.
+ """
+ def __init__(self, method):
+ self._method = method
+ self._options = 0
+
+
+ def set_options(self, options):
+ self._options |= options
+
+
+ def use_certificate_file(self, fileName):
+ pass
+
+
+ def use_privatekey_file(self, fileName):
+ pass
+
+
+
+class DefaultOpenSSLContextFactoryTests(unittest.TestCase):
+ """
+ Tests for L{ssl.DefaultOpenSSLContextFactory}.
+ """
+ def setUp(self):
+ # pyOpenSSL Context objects aren't introspectable enough. Pass in
+ # an alternate context factory so we can inspect what is done to it.
+ self.contextFactory = ssl.DefaultOpenSSLContextFactory(
+ certPath, certPath, _contextFactory=FakeContext)
+ self.context = self.contextFactory.getContext()
+
+
+ def test_method(self):
+ """
+ L{ssl.DefaultOpenSSLContextFactory.getContext} returns an SSL context
+ which can use SSLv3 or TLSv1 but not SSLv2.
+ """
+ # SSLv23_METHOD allows SSLv2, SSLv3, or TLSv1
+ self.assertEqual(self.context._method, SSL.SSLv23_METHOD)
+
+ # And OP_NO_SSLv2 disables the SSLv2 support.
+ self.assertTrue(self.context._options & SSL.OP_NO_SSLv2)
+
+ # Make sure SSLv3 and TLSv1 aren't disabled though.
+ self.assertFalse(self.context._options & SSL.OP_NO_SSLv3)
+ self.assertFalse(self.context._options & SSL.OP_NO_TLSv1)
+
+
+ def test_missingCertificateFile(self):
+ """
+ Instantiating L{ssl.DefaultOpenSSLContextFactory} with a certificate
+ filename which does not identify an existing file results in the
+ initializer raising L{OpenSSL.SSL.Error}.
+ """
+ self.assertRaises(
+ SSL.Error,
+ ssl.DefaultOpenSSLContextFactory, certPath, self.mktemp())
+
+
+ def test_missingPrivateKeyFile(self):
+ """
+ Instantiating L{ssl.DefaultOpenSSLContextFactory} with a private key
+ filename which does not identify an existing file results in the
+ initializer raising L{OpenSSL.SSL.Error}.
+ """
+ self.assertRaises(
+ SSL.Error,
+ ssl.DefaultOpenSSLContextFactory, self.mktemp(), certPath)
+
+
+
+class ClientContextFactoryTests(unittest.TestCase):
+ """
+ Tests for L{ssl.ClientContextFactory}.
+ """
+ def setUp(self):
+ self.contextFactory = ssl.ClientContextFactory()
+ self.contextFactory._contextFactory = FakeContext
+ self.context = self.contextFactory.getContext()
+
+
+ def test_method(self):
+ """
+ L{ssl.ClientContextFactory.getContext} returns a context which can use
+ SSLv3 or TLSv1 but not SSLv2.
+ """
+ self.assertEqual(self.context._method, SSL.SSLv23_METHOD)
+ self.assertTrue(self.context._options & SSL.OP_NO_SSLv2)
+ self.assertFalse(self.context._options & SSL.OP_NO_SSLv3)
+ self.assertFalse(self.context._options & SSL.OP_NO_TLSv1)
+
+
+
+if interfaces.IReactorSSL(reactor, None) is None:
+ for tCase in [StolenTCPTestCase, TLSTestCase, SpammyTLSTestCase,
+ BufferingTestCase, ConnectionLostTestCase,
+ DefaultOpenSSLContextFactoryTests,
+ ClientContextFactoryTests]:
+ tCase.skip = "Reactor does not support SSL, cannot run SSL tests"
+
diff --git a/twisted/test/test_sslverify.py b/twisted/test/test_sslverify.py
new file mode 100644
index 0000000..97fc6bc
--- /dev/null
+++ b/twisted/test/test_sslverify.py
@@ -0,0 +1,558 @@
+# Copyright 2005 Divmod, Inc. See LICENSE file for details
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet._sslverify}.
+"""
+
+import itertools
+
+try:
+ from OpenSSL import SSL
+ from OpenSSL.crypto import PKey, X509, X509Req
+ from OpenSSL.crypto import TYPE_RSA
+ from twisted.internet import _sslverify as sslverify
+except ImportError:
+ pass
+
+from twisted.trial import unittest
+from twisted.internet import protocol, defer, reactor
+from twisted.python.reflect import objgrep, isSame
+from twisted.python import log
+
+from twisted.internet.error import CertificateError, ConnectionLost
+from twisted.internet import interfaces
+
+
+# A couple of static PEM-format certificates to be used by various tests.
+A_HOST_CERTIFICATE_PEM = """
+-----BEGIN CERTIFICATE-----
+ MIIC2jCCAkMCAjA5MA0GCSqGSIb3DQEBBAUAMIG0MQswCQYDVQQGEwJVUzEiMCAG
+ A1UEAxMZZXhhbXBsZS50d2lzdGVkbWF0cml4LmNvbTEPMA0GA1UEBxMGQm9zdG9u
+ MRwwGgYDVQQKExNUd2lzdGVkIE1hdHJpeCBMYWJzMRYwFAYDVQQIEw1NYXNzYWNo
+ dXNldHRzMScwJQYJKoZIhvcNAQkBFhhub2JvZHlAdHdpc3RlZG1hdHJpeC5jb20x
+ ETAPBgNVBAsTCFNlY3VyaXR5MB4XDTA2MDgxNjAxMDEwOFoXDTA3MDgxNjAxMDEw
+ OFowgbQxCzAJBgNVBAYTAlVTMSIwIAYDVQQDExlleGFtcGxlLnR3aXN0ZWRtYXRy
+ aXguY29tMQ8wDQYDVQQHEwZCb3N0b24xHDAaBgNVBAoTE1R3aXN0ZWQgTWF0cml4
+ IExhYnMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxJzAlBgkqhkiG9w0BCQEWGG5v
+ Ym9keUB0d2lzdGVkbWF0cml4LmNvbTERMA8GA1UECxMIU2VjdXJpdHkwgZ8wDQYJ
+ KoZIhvcNAQEBBQADgY0AMIGJAoGBAMzH8CDF/U91y/bdbdbJKnLgnyvQ9Ig9ZNZp
+ 8hpsu4huil60zF03+Lexg2l1FIfURScjBuaJMR6HiMYTMjhzLuByRZ17KW4wYkGi
+ KXstz03VIKy4Tjc+v4aXFI4XdRw10gGMGQlGGscXF/RSoN84VoDKBfOMWdXeConJ
+ VyC4w3iJAgMBAAEwDQYJKoZIhvcNAQEEBQADgYEAviMT4lBoxOgQy32LIgZ4lVCj
+ JNOiZYg8GMQ6y0ugp86X80UjOvkGtNf/R7YgED/giKRN/q/XJiLJDEhzknkocwmO
+ S+4b2XpiaZYxRyKWwL221O7CGmtWYyZl2+92YYmmCiNzWQPfP6BOMlfax0AGLHls
+ fXzCWdG0O/3Lk2SRM0I=
+-----END CERTIFICATE-----
+"""
+
+A_PEER_CERTIFICATE_PEM = """
+-----BEGIN CERTIFICATE-----
+ MIIC3jCCAkcCAjA6MA0GCSqGSIb3DQEBBAUAMIG2MQswCQYDVQQGEwJVUzEiMCAG
+ A1UEAxMZZXhhbXBsZS50d2lzdGVkbWF0cml4LmNvbTEPMA0GA1UEBxMGQm9zdG9u
+ MRwwGgYDVQQKExNUd2lzdGVkIE1hdHJpeCBMYWJzMRYwFAYDVQQIEw1NYXNzYWNo
+ dXNldHRzMSkwJwYJKoZIhvcNAQkBFhpzb21lYm9keUB0d2lzdGVkbWF0cml4LmNv
+ bTERMA8GA1UECxMIU2VjdXJpdHkwHhcNMDYwODE2MDEwMTU2WhcNMDcwODE2MDEw
+ MTU2WjCBtjELMAkGA1UEBhMCVVMxIjAgBgNVBAMTGWV4YW1wbGUudHdpc3RlZG1h
+ dHJpeC5jb20xDzANBgNVBAcTBkJvc3RvbjEcMBoGA1UEChMTVHdpc3RlZCBNYXRy
+ aXggTGFiczEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEpMCcGCSqGSIb3DQEJARYa
+ c29tZWJvZHlAdHdpc3RlZG1hdHJpeC5jb20xETAPBgNVBAsTCFNlY3VyaXR5MIGf
+ MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnm+WBlgFNbMlHehib9ePGGDXF+Nz4
+ CjGuUmVBaXCRCiVjg3kSDecwqfb0fqTksBZ+oQ1UBjMcSh7OcvFXJZnUesBikGWE
+ JE4V8Bjh+RmbJ1ZAlUPZ40bAkww0OpyIRAGMvKG+4yLFTO4WDxKmfDcrOb6ID8WJ
+ e1u+i3XGkIf/5QIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAD4Oukm3YYkhedUepBEA
+ vvXIQhVDqL7mk6OqYdXmNj6R7ZMC8WWvGZxrzDI1bZuB+4aIxxd1FXC3UOHiR/xg
+ i9cDl1y8P/qRp4aEBNF6rI0D4AxTbfnHQx4ERDAOShJdYZs/2zifPJ6va6YvrEyr
+ yqDtGhklsWW3ZwBzEh5VEOUp
+-----END CERTIFICATE-----
+"""
+
+
+
+counter = itertools.count().next
+def makeCertificate(**kw):
+ keypair = PKey()
+ keypair.generate_key(TYPE_RSA, 512)
+
+ certificate = X509()
+ certificate.gmtime_adj_notBefore(0)
+ certificate.gmtime_adj_notAfter(60 * 60 * 24 * 365) # One year
+ for xname in certificate.get_issuer(), certificate.get_subject():
+ for (k, v) in kw.items():
+ setattr(xname, k, v)
+
+ certificate.set_serial_number(counter())
+ certificate.set_pubkey(keypair)
+ certificate.sign(keypair, "md5")
+
+ return keypair, certificate
+
+
+
+class DataCallbackProtocol(protocol.Protocol):
+ def dataReceived(self, data):
+ d, self.factory.onData = self.factory.onData, None
+ if d is not None:
+ d.callback(data)
+
+ def connectionLost(self, reason):
+ d, self.factory.onLost = self.factory.onLost, None
+ if d is not None:
+ d.errback(reason)
+
+class WritingProtocol(protocol.Protocol):
+ byte = 'x'
+ def connectionMade(self):
+ self.transport.write(self.byte)
+
+ def connectionLost(self, reason):
+ self.factory.onLost.errback(reason)
+
+
+class OpenSSLOptions(unittest.TestCase):
+ serverPort = clientConn = None
+ onServerLost = onClientLost = None
+
+ sKey = None
+ sCert = None
+ cKey = None
+ cCert = None
+
+ def setUp(self):
+ """
+ Create class variables of client and server certificates.
+ """
+ self.sKey, self.sCert = makeCertificate(
+ O="Server Test Certificate",
+ CN="server")
+ self.cKey, self.cCert = makeCertificate(
+ O="Client Test Certificate",
+ CN="client")
+
+ def tearDown(self):
+ if self.serverPort is not None:
+ self.serverPort.stopListening()
+ if self.clientConn is not None:
+ self.clientConn.disconnect()
+
+ L = []
+ if self.onServerLost is not None:
+ L.append(self.onServerLost)
+ if self.onClientLost is not None:
+ L.append(self.onClientLost)
+
+ return defer.DeferredList(L, consumeErrors=True)
+
+ def loopback(self, serverCertOpts, clientCertOpts,
+ onServerLost=None, onClientLost=None, onData=None):
+ if onServerLost is None:
+ self.onServerLost = onServerLost = defer.Deferred()
+ if onClientLost is None:
+ self.onClientLost = onClientLost = defer.Deferred()
+ if onData is None:
+ onData = defer.Deferred()
+
+ serverFactory = protocol.ServerFactory()
+ serverFactory.protocol = DataCallbackProtocol
+ serverFactory.onLost = onServerLost
+ serverFactory.onData = onData
+
+ clientFactory = protocol.ClientFactory()
+ clientFactory.protocol = WritingProtocol
+ clientFactory.onLost = onClientLost
+
+ self.serverPort = reactor.listenSSL(0, serverFactory, serverCertOpts)
+ self.clientConn = reactor.connectSSL('127.0.0.1',
+ self.serverPort.getHost().port, clientFactory, clientCertOpts)
+
+ def test_abbreviatingDistinguishedNames(self):
+ """
+ Check that abbreviations used in certificates correctly map to
+ complete names.
+ """
+ self.assertEqual(
+ sslverify.DN(CN='a', OU='hello'),
+ sslverify.DistinguishedName(commonName='a',
+ organizationalUnitName='hello'))
+ self.assertNotEquals(
+ sslverify.DN(CN='a', OU='hello'),
+ sslverify.DN(CN='a', OU='hello', emailAddress='xxx'))
+ dn = sslverify.DN(CN='abcdefg')
+ self.assertRaises(AttributeError, setattr, dn, 'Cn', 'x')
+ self.assertEqual(dn.CN, dn.commonName)
+ dn.CN = 'bcdefga'
+ self.assertEqual(dn.CN, dn.commonName)
+
+
+ def testInspectDistinguishedName(self):
+ n = sslverify.DN(commonName='common name',
+ organizationName='organization name',
+ organizationalUnitName='organizational unit name',
+ localityName='locality name',
+ stateOrProvinceName='state or province name',
+ countryName='country name',
+ emailAddress='email address')
+ s = n.inspect()
+ for k in [
+ 'common name',
+ 'organization name',
+ 'organizational unit name',
+ 'locality name',
+ 'state or province name',
+ 'country name',
+ 'email address']:
+ self.assertIn(k, s, "%r was not in inspect output." % (k,))
+ self.assertIn(k.title(), s, "%r was not in inspect output." % (k,))
+
+
+ def testInspectDistinguishedNameWithoutAllFields(self):
+ n = sslverify.DN(localityName='locality name')
+ s = n.inspect()
+ for k in [
+ 'common name',
+ 'organization name',
+ 'organizational unit name',
+ 'state or province name',
+ 'country name',
+ 'email address']:
+ self.assertNotIn(k, s, "%r was in inspect output." % (k,))
+ self.assertNotIn(k.title(), s, "%r was in inspect output." % (k,))
+ self.assertIn('locality name', s)
+ self.assertIn('Locality Name', s)
+
+
+ def test_inspectCertificate(self):
+ """
+ Test that the C{inspect} method of L{sslverify.Certificate} returns
+ a human-readable string containing some basic information about the
+ certificate.
+ """
+ c = sslverify.Certificate.loadPEM(A_HOST_CERTIFICATE_PEM)
+ self.assertEqual(
+ c.inspect().split('\n'),
+ ["Certificate For Subject:",
+ " Organizational Unit Name: Security",
+ " Organization Name: Twisted Matrix Labs",
+ " Common Name: example.twistedmatrix.com",
+ " State Or Province Name: Massachusetts",
+ " Country Name: US",
+ " Email Address: nobody@twistedmatrix.com",
+ " Locality Name: Boston",
+ "",
+ "Issuer:",
+ " Organizational Unit Name: Security",
+ " Organization Name: Twisted Matrix Labs",
+ " Common Name: example.twistedmatrix.com",
+ " State Or Province Name: Massachusetts",
+ " Country Name: US",
+ " Email Address: nobody@twistedmatrix.com",
+ " Locality Name: Boston",
+ "",
+ "Serial Number: 12345",
+ "Digest: C4:96:11:00:30:C3:EC:EE:A3:55:AA:ED:8C:84:85:18",
+ "Public Key with Hash: ff33994c80812aa95a79cdb85362d054"])
+
+
+ def test_certificateOptionsSerialization(self):
+ """
+ Test that __setstate__(__getstate__()) round-trips properly.
+ """
+ firstOpts = sslverify.OpenSSLCertificateOptions(
+ privateKey=self.sKey,
+ certificate=self.sCert,
+ method=SSL.SSLv3_METHOD,
+ verify=True,
+ caCerts=[self.sCert],
+ verifyDepth=2,
+ requireCertificate=False,
+ verifyOnce=False,
+ enableSingleUseKeys=False,
+ enableSessions=False,
+ fixBrokenPeers=True,
+ enableSessionTickets=True)
+ context = firstOpts.getContext()
+ state = firstOpts.__getstate__()
+
+ # The context shouldn't be in the state to serialize
+ self.failIf(objgrep(state, context, isSame),
+ objgrep(state, context, isSame))
+
+ opts = sslverify.OpenSSLCertificateOptions()
+ opts.__setstate__(state)
+ self.assertEqual(opts.privateKey, self.sKey)
+ self.assertEqual(opts.certificate, self.sCert)
+ self.assertEqual(opts.method, SSL.SSLv3_METHOD)
+ self.assertEqual(opts.verify, True)
+ self.assertEqual(opts.caCerts, [self.sCert])
+ self.assertEqual(opts.verifyDepth, 2)
+ self.assertEqual(opts.requireCertificate, False)
+ self.assertEqual(opts.verifyOnce, False)
+ self.assertEqual(opts.enableSingleUseKeys, False)
+ self.assertEqual(opts.enableSessions, False)
+ self.assertEqual(opts.fixBrokenPeers, True)
+ self.assertEqual(opts.enableSessionTickets, True)
+
+
+ def test_certificateOptionsSessionTickets(self):
+ """
+ Enabling session tickets should not set the OP_NO_TICKET option.
+ """
+ opts = sslverify.OpenSSLCertificateOptions(enableSessionTickets=True)
+ ctx = opts.getContext()
+ self.assertEqual(0, ctx.set_options(0) & 0x00004000)
+
+
+ def test_certificateOptionsSessionTicketsDisabled(self):
+ """
+ Enabling session tickets should set the OP_NO_TICKET option.
+ """
+ opts = sslverify.OpenSSLCertificateOptions(enableSessionTickets=False)
+ ctx = opts.getContext()
+ self.assertEqual(0x00004000, ctx.set_options(0) & 0x00004000)
+
+
+ def test_allowedAnonymousClientConnection(self):
+ """
+ Check that anonymous connections are allowed when certificates aren't
+ required on the server.
+ """
+ onData = defer.Deferred()
+ self.loopback(sslverify.OpenSSLCertificateOptions(privateKey=self.sKey,
+ certificate=self.sCert, requireCertificate=False),
+ sslverify.OpenSSLCertificateOptions(
+ requireCertificate=False),
+ onData=onData)
+
+ return onData.addCallback(
+ lambda result: self.assertEqual(result, WritingProtocol.byte))
+
+ def test_refusedAnonymousClientConnection(self):
+ """
+ Check that anonymous connections are refused when certificates are
+ required on the server.
+ """
+ onServerLost = defer.Deferred()
+ onClientLost = defer.Deferred()
+ self.loopback(sslverify.OpenSSLCertificateOptions(privateKey=self.sKey,
+ certificate=self.sCert, verify=True,
+ caCerts=[self.sCert], requireCertificate=True),
+ sslverify.OpenSSLCertificateOptions(
+ requireCertificate=False),
+ onServerLost=onServerLost,
+ onClientLost=onClientLost)
+
+ d = defer.DeferredList([onClientLost, onServerLost],
+ consumeErrors=True)
+
+
+ def afterLost(((cSuccess, cResult), (sSuccess, sResult))):
+
+ self.failIf(cSuccess)
+ self.failIf(sSuccess)
+ # Win32 fails to report the SSL Error, and report a connection lost
+ # instead: there is a race condition so that's not totally
+ # surprising (see ticket #2877 in the tracker)
+ self.assertIsInstance(cResult.value, (SSL.Error, ConnectionLost))
+ self.assertIsInstance(sResult.value, SSL.Error)
+
+ return d.addCallback(afterLost)
+
+ def test_failedCertificateVerification(self):
+ """
+ Check that connecting with a certificate not accepted by the server CA
+ fails.
+ """
+ onServerLost = defer.Deferred()
+ onClientLost = defer.Deferred()
+ self.loopback(sslverify.OpenSSLCertificateOptions(privateKey=self.sKey,
+ certificate=self.sCert, verify=False,
+ requireCertificate=False),
+ sslverify.OpenSSLCertificateOptions(verify=True,
+ requireCertificate=False, caCerts=[self.cCert]),
+ onServerLost=onServerLost,
+ onClientLost=onClientLost)
+
+ d = defer.DeferredList([onClientLost, onServerLost],
+ consumeErrors=True)
+ def afterLost(((cSuccess, cResult), (sSuccess, sResult))):
+
+ self.failIf(cSuccess)
+ self.failIf(sSuccess)
+
+ return d.addCallback(afterLost)
+
+ def test_successfulCertificateVerification(self):
+ """
+ Test a successful connection with client certificate validation on
+ server side.
+ """
+ onData = defer.Deferred()
+ self.loopback(sslverify.OpenSSLCertificateOptions(privateKey=self.sKey,
+ certificate=self.sCert, verify=False,
+ requireCertificate=False),
+ sslverify.OpenSSLCertificateOptions(verify=True,
+ requireCertificate=True, caCerts=[self.sCert]),
+ onData=onData)
+
+ return onData.addCallback(
+ lambda result: self.assertEqual(result, WritingProtocol.byte))
+
+ def test_successfulSymmetricSelfSignedCertificateVerification(self):
+ """
+ Test a successful connection with validation on both server and client
+ sides.
+ """
+ onData = defer.Deferred()
+ self.loopback(sslverify.OpenSSLCertificateOptions(privateKey=self.sKey,
+ certificate=self.sCert, verify=True,
+ requireCertificate=True, caCerts=[self.cCert]),
+ sslverify.OpenSSLCertificateOptions(privateKey=self.cKey,
+ certificate=self.cCert, verify=True,
+ requireCertificate=True, caCerts=[self.sCert]),
+ onData=onData)
+
+ return onData.addCallback(
+ lambda result: self.assertEqual(result, WritingProtocol.byte))
+
+ def test_verification(self):
+ """
+ Check certificates verification building custom certificates data.
+ """
+ clientDN = sslverify.DistinguishedName(commonName='client')
+ clientKey = sslverify.KeyPair.generate()
+ clientCertReq = clientKey.certificateRequest(clientDN)
+
+ serverDN = sslverify.DistinguishedName(commonName='server')
+ serverKey = sslverify.KeyPair.generate()
+ serverCertReq = serverKey.certificateRequest(serverDN)
+
+ clientSelfCertReq = clientKey.certificateRequest(clientDN)
+ clientSelfCertData = clientKey.signCertificateRequest(
+ clientDN, clientSelfCertReq, lambda dn: True, 132)
+ clientSelfCert = clientKey.newCertificate(clientSelfCertData)
+
+ serverSelfCertReq = serverKey.certificateRequest(serverDN)
+ serverSelfCertData = serverKey.signCertificateRequest(
+ serverDN, serverSelfCertReq, lambda dn: True, 516)
+ serverSelfCert = serverKey.newCertificate(serverSelfCertData)
+
+ clientCertData = serverKey.signCertificateRequest(
+ serverDN, clientCertReq, lambda dn: True, 7)
+ clientCert = clientKey.newCertificate(clientCertData)
+
+ serverCertData = clientKey.signCertificateRequest(
+ clientDN, serverCertReq, lambda dn: True, 42)
+ serverCert = serverKey.newCertificate(serverCertData)
+
+ onData = defer.Deferred()
+
+ serverOpts = serverCert.options(serverSelfCert)
+ clientOpts = clientCert.options(clientSelfCert)
+
+ self.loopback(serverOpts,
+ clientOpts,
+ onData=onData)
+
+ return onData.addCallback(
+ lambda result: self.assertEqual(result, WritingProtocol.byte))
+
+
+
+if interfaces.IReactorSSL(reactor, None) is None:
+ OpenSSLOptions.skip = "Reactor does not support SSL, cannot run SSL tests"
+
+
+
+class _NotSSLTransport:
+ def getHandle(self):
+ return self
+
+class _MaybeSSLTransport:
+ def getHandle(self):
+ return self
+
+ def get_peer_certificate(self):
+ return None
+
+ def get_host_certificate(self):
+ return None
+
+
+class _ActualSSLTransport:
+ def getHandle(self):
+ return self
+
+ def get_host_certificate(self):
+ return sslverify.Certificate.loadPEM(A_HOST_CERTIFICATE_PEM).original
+
+ def get_peer_certificate(self):
+ return sslverify.Certificate.loadPEM(A_PEER_CERTIFICATE_PEM).original
+
+
+class Constructors(unittest.TestCase):
+ def test_peerFromNonSSLTransport(self):
+ """
+ Verify that peerFromTransport raises an exception if the transport
+ passed is not actually an SSL transport.
+ """
+ x = self.assertRaises(CertificateError,
+ sslverify.Certificate.peerFromTransport,
+ _NotSSLTransport())
+ self.failUnless(str(x).startswith("non-TLS"))
+
+ def test_peerFromBlankSSLTransport(self):
+ """
+ Verify that peerFromTransport raises an exception if the transport
+ passed is an SSL transport, but doesn't have a peer certificate.
+ """
+ x = self.assertRaises(CertificateError,
+ sslverify.Certificate.peerFromTransport,
+ _MaybeSSLTransport())
+ self.failUnless(str(x).startswith("TLS"))
+
+ def test_hostFromNonSSLTransport(self):
+ """
+ Verify that hostFromTransport raises an exception if the transport
+ passed is not actually an SSL transport.
+ """
+ x = self.assertRaises(CertificateError,
+ sslverify.Certificate.hostFromTransport,
+ _NotSSLTransport())
+ self.failUnless(str(x).startswith("non-TLS"))
+
+ def test_hostFromBlankSSLTransport(self):
+ """
+ Verify that hostFromTransport raises an exception if the transport
+ passed is an SSL transport, but doesn't have a host certificate.
+ """
+ x = self.assertRaises(CertificateError,
+ sslverify.Certificate.hostFromTransport,
+ _MaybeSSLTransport())
+ self.failUnless(str(x).startswith("TLS"))
+
+
+ def test_hostFromSSLTransport(self):
+ """
+ Verify that hostFromTransport successfully creates the correct
+ certificate if passed a valid SSL transport.
+ """
+ self.assertEqual(
+ sslverify.Certificate.hostFromTransport(
+ _ActualSSLTransport()).serialNumber(),
+ 12345)
+
+ def test_peerFromSSLTransport(self):
+ """
+ Verify that peerFromTransport successfully creates the correct
+ certificate if passed a valid SSL transport.
+ """
+ self.assertEqual(
+ sslverify.Certificate.peerFromTransport(
+ _ActualSSLTransport()).serialNumber(),
+ 12346)
+
+
+
+if interfaces.IReactorSSL(reactor, None) is None:
+ Constructors.skip = "Reactor does not support SSL, cannot run SSL tests"
diff --git a/twisted/test/test_stateful.py b/twisted/test/test_stateful.py
new file mode 100644
index 0000000..365bfac
--- /dev/null
+++ b/twisted/test/test_stateful.py
@@ -0,0 +1,81 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test cases for twisted.protocols.stateful
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.test import test_protocols
+from twisted.protocols.stateful import StatefulProtocol
+
+from struct import pack, unpack, calcsize
+
+
+class MyInt32StringReceiver(StatefulProtocol):
+ """
+ A stateful Int32StringReceiver.
+ """
+ MAX_LENGTH = 99999
+ structFormat = "!I"
+ prefixLength = calcsize(structFormat)
+
+ def getInitialState(self):
+ return self._getHeader, 4
+
+ def lengthLimitExceeded(self, length):
+ self.transport.loseConnection()
+
+ def _getHeader(self, msg):
+ length, = unpack("!i", msg)
+ if length > self.MAX_LENGTH:
+ self.lengthLimitExceeded(length)
+ return
+ return self._getString, length
+
+ def _getString(self, msg):
+ self.stringReceived(msg)
+ return self._getHeader, 4
+
+ def stringReceived(self, msg):
+ """
+ Override this.
+ """
+ raise NotImplementedError
+
+ def sendString(self, data):
+ """
+ Send an int32-prefixed string to the other end of the connection.
+ """
+ self.transport.write(pack(self.structFormat, len(data)) + data)
+
+
+class TestInt32(MyInt32StringReceiver):
+ def connectionMade(self):
+ self.received = []
+
+ def stringReceived(self, s):
+ self.received.append(s)
+
+ MAX_LENGTH = 50
+ closed = 0
+
+ def connectionLost(self, reason):
+ self.closed = 1
+
+
+class Int32TestCase(TestCase, test_protocols.IntNTestCaseMixin):
+ protocol = TestInt32
+ strings = ["a", "b" * 16]
+ illegalStrings = ["\x10\x00\x00\x00aaaaaa"]
+ partialStrings = ["\x00\x00\x00", "hello there", ""]
+
+ def test_bigReceive(self):
+ r = self.getProtocol()
+ big = ""
+ for s in self.strings * 4:
+ big += pack("!i", len(s)) + s
+ r.dataReceived(big)
+ self.assertEqual(r.received, self.strings * 4)
+
diff --git a/twisted/test/test_stdio.py b/twisted/test/test_stdio.py
new file mode 100644
index 0000000..3da754c
--- /dev/null
+++ b/twisted/test/test_stdio.py
@@ -0,0 +1,371 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.internet.stdio}.
+"""
+
+import os, sys, itertools
+
+from twisted.trial import unittest
+from twisted.python import filepath, log
+from twisted.python.runtime import platform
+from twisted.internet import error, defer, protocol, stdio, reactor
+from twisted.test.test_tcp import ConnectionLostNotifyingProtocol
+
+
+# A short string which is intended to appear here and nowhere else,
+# particularly not in any random garbage output CPython unavoidable
+# generates (such as in warning text and so forth). This is searched
+# for in the output from stdio_test_lastwrite.py and if it is found at
+# the end, the functionality works.
+UNIQUE_LAST_WRITE_STRING = 'xyz123abc Twisted is great!'
+
+skipWindowsNopywin32 = None
+if platform.isWindows():
+ try:
+ import win32process
+ except ImportError:
+ skipWindowsNopywin32 = ("On windows, spawnProcess is not available "
+ "in the absence of win32process.")
+
+
+class StandardIOTestProcessProtocol(protocol.ProcessProtocol):
+ """
+ Test helper for collecting output from a child process and notifying
+ something when it exits.
+
+ @ivar onConnection: A L{defer.Deferred} which will be called back with
+ C{None} when the connection to the child process is established.
+
+ @ivar onCompletion: A L{defer.Deferred} which will be errbacked with the
+ failure associated with the child process exiting when it exits.
+
+ @ivar onDataReceived: A L{defer.Deferred} which will be called back with
+ this instance whenever C{childDataReceived} is called, or C{None} to
+ suppress these callbacks.
+
+ @ivar data: A C{dict} mapping file descriptors to strings containing all
+ bytes received from the child process on each file descriptor.
+ """
+ onDataReceived = None
+
+ def __init__(self):
+ self.onConnection = defer.Deferred()
+ self.onCompletion = defer.Deferred()
+ self.data = {}
+
+
+ def connectionMade(self):
+ self.onConnection.callback(None)
+
+
+ def childDataReceived(self, name, bytes):
+ """
+ Record all bytes received from the child process in the C{data}
+ dictionary. Fire C{onDataReceived} if it is not C{None}.
+ """
+ self.data[name] = self.data.get(name, '') + bytes
+ if self.onDataReceived is not None:
+ d, self.onDataReceived = self.onDataReceived, None
+ d.callback(self)
+
+
+ def processEnded(self, reason):
+ self.onCompletion.callback(reason)
+
+
+
+class StandardInputOutputTestCase(unittest.TestCase):
+
+ skip = skipWindowsNopywin32
+
+ def _spawnProcess(self, proto, sibling, *args, **kw):
+ """
+ Launch a child Python process and communicate with it using the
+ given ProcessProtocol.
+
+ @param proto: A L{ProcessProtocol} instance which will be connected
+ to the child process.
+
+ @param sibling: The basename of a file containing the Python program
+ to run in the child process.
+
+ @param *args: strings which will be passed to the child process on
+ the command line as C{argv[2:]}.
+
+ @param **kw: additional arguments to pass to L{reactor.spawnProcess}.
+
+ @return: The L{IProcessTransport} provider for the spawned process.
+ """
+ import twisted
+ subenv = dict(os.environ)
+ subenv['PYTHONPATH'] = os.pathsep.join(
+ [os.path.abspath(
+ os.path.dirname(os.path.dirname(twisted.__file__))),
+ subenv.get('PYTHONPATH', '')
+ ])
+ args = [sys.executable,
+ filepath.FilePath(__file__).sibling(sibling).path,
+ reactor.__class__.__module__] + list(args)
+ return reactor.spawnProcess(
+ proto,
+ sys.executable,
+ args,
+ env=subenv,
+ **kw)
+
+
+ def _requireFailure(self, d, callback):
+ def cb(result):
+ self.fail("Process terminated with non-Failure: %r" % (result,))
+ def eb(err):
+ return callback(err)
+ return d.addCallbacks(cb, eb)
+
+
+ def test_loseConnection(self):
+ """
+ Verify that a protocol connected to L{StandardIO} can disconnect
+ itself using C{transport.loseConnection}.
+ """
+ errorLogFile = self.mktemp()
+ log.msg("Child process logging to " + errorLogFile)
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+ self._spawnProcess(p, 'stdio_test_loseconn.py', errorLogFile)
+
+ def processEnded(reason):
+ # Copy the child's log to ours so it's more visible.
+ for line in file(errorLogFile):
+ log.msg("Child logged: " + line.rstrip())
+
+ self.failIfIn(1, p.data)
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def test_readConnectionLost(self):
+ """
+ When stdin is closed and the protocol connected to it implements
+ L{IHalfCloseableProtocol}, the protocol's C{readConnectionLost} method
+ is called.
+ """
+ errorLogFile = self.mktemp()
+ log.msg("Child process logging to " + errorLogFile)
+ p = StandardIOTestProcessProtocol()
+ p.onDataReceived = defer.Deferred()
+
+ def cbBytes(ignored):
+ d = p.onCompletion
+ p.transport.closeStdin()
+ return d
+ p.onDataReceived.addCallback(cbBytes)
+
+ def processEnded(reason):
+ reason.trap(error.ProcessDone)
+ d = self._requireFailure(p.onDataReceived, processEnded)
+
+ self._spawnProcess(
+ p, 'stdio_test_halfclose.py', errorLogFile)
+ return d
+
+
+ def test_lastWriteReceived(self):
+ """
+ Verify that a write made directly to stdout using L{os.write}
+ after StandardIO has finished is reliably received by the
+ process reading that stdout.
+ """
+ p = StandardIOTestProcessProtocol()
+
+ # Note: the OS X bug which prompted the addition of this test
+ # is an apparent race condition involving non-blocking PTYs.
+ # Delaying the parent process significantly increases the
+ # likelihood of the race going the wrong way. If you need to
+ # fiddle with this code at all, uncommenting the next line
+ # will likely make your life much easier. It is commented out
+ # because it makes the test quite slow.
+
+ # p.onConnection.addCallback(lambda ign: __import__('time').sleep(5))
+
+ try:
+ self._spawnProcess(
+ p, 'stdio_test_lastwrite.py', UNIQUE_LAST_WRITE_STRING,
+ usePTY=True)
+ except ValueError, e:
+ # Some platforms don't work with usePTY=True
+ raise unittest.SkipTest(str(e))
+
+ def processEnded(reason):
+ """
+ Asserts that the parent received the bytes written by the child
+ immediately after the child starts.
+ """
+ self.assertTrue(
+ p.data[1].endswith(UNIQUE_LAST_WRITE_STRING),
+ "Received %r from child, did not find expected bytes." % (
+ p.data,))
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(p.onCompletion, processEnded)
+
+
+ def test_hostAndPeer(self):
+ """
+ Verify that the transport of a protocol connected to L{StandardIO}
+ has C{getHost} and C{getPeer} methods.
+ """
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+ self._spawnProcess(p, 'stdio_test_hostpeer.py')
+
+ def processEnded(reason):
+ host, peer = p.data[1].splitlines()
+ self.failUnless(host)
+ self.failUnless(peer)
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def test_write(self):
+ """
+ Verify that the C{write} method of the transport of a protocol
+ connected to L{StandardIO} sends bytes to standard out.
+ """
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+
+ self._spawnProcess(p, 'stdio_test_write.py')
+
+ def processEnded(reason):
+ self.assertEqual(p.data[1], 'ok!')
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def test_writeSequence(self):
+ """
+ Verify that the C{writeSequence} method of the transport of a
+ protocol connected to L{StandardIO} sends bytes to standard out.
+ """
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+
+ self._spawnProcess(p, 'stdio_test_writeseq.py')
+
+ def processEnded(reason):
+ self.assertEqual(p.data[1], 'ok!')
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def _junkPath(self):
+ junkPath = self.mktemp()
+ junkFile = file(junkPath, 'w')
+ for i in xrange(1024):
+ junkFile.write(str(i) + '\n')
+ junkFile.close()
+ return junkPath
+
+
+ def test_producer(self):
+ """
+ Verify that the transport of a protocol connected to L{StandardIO}
+ is a working L{IProducer} provider.
+ """
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+
+ written = []
+ toWrite = range(100)
+
+ def connectionMade(ign):
+ if toWrite:
+ written.append(str(toWrite.pop()) + "\n")
+ proc.write(written[-1])
+ reactor.callLater(0.01, connectionMade, None)
+
+ proc = self._spawnProcess(p, 'stdio_test_producer.py')
+
+ p.onConnection.addCallback(connectionMade)
+
+ def processEnded(reason):
+ self.assertEqual(p.data[1], ''.join(written))
+ self.failIf(toWrite, "Connection lost with %d writes left to go." % (len(toWrite),))
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def test_consumer(self):
+ """
+ Verify that the transport of a protocol connected to L{StandardIO}
+ is a working L{IConsumer} provider.
+ """
+ p = StandardIOTestProcessProtocol()
+ d = p.onCompletion
+
+ junkPath = self._junkPath()
+
+ self._spawnProcess(p, 'stdio_test_consumer.py', junkPath)
+
+ def processEnded(reason):
+ self.assertEqual(p.data[1], file(junkPath).read())
+ reason.trap(error.ProcessDone)
+ return self._requireFailure(d, processEnded)
+
+
+ def test_normalFileStandardOut(self):
+ """
+ If L{StandardIO} is created with a file descriptor which refers to a
+ normal file (ie, a file from the filesystem), L{StandardIO.write}
+ writes bytes to that file. In particular, it does not immediately
+ consider the file closed or call its protocol's C{connectionLost}
+ method.
+ """
+ onConnLost = defer.Deferred()
+ proto = ConnectionLostNotifyingProtocol(onConnLost)
+ path = filepath.FilePath(self.mktemp())
+ self.normal = normal = path.open('w')
+ self.addCleanup(normal.close)
+
+ kwargs = dict(stdout=normal.fileno())
+ if not platform.isWindows():
+ # Make a fake stdin so that StandardIO doesn't mess with the *real*
+ # stdin.
+ r, w = os.pipe()
+ self.addCleanup(os.close, r)
+ self.addCleanup(os.close, w)
+ kwargs['stdin'] = r
+ connection = stdio.StandardIO(proto, **kwargs)
+
+ # The reactor needs to spin a bit before it might have incorrectly
+ # decided stdout is closed. Use this counter to keep track of how
+ # much we've let it spin. If it closes before we expected, this
+ # counter will have a value that's too small and we'll know.
+ howMany = 5
+ count = itertools.count()
+
+ def spin():
+ for value in count:
+ if value == howMany:
+ connection.loseConnection()
+ return
+ connection.write(str(value))
+ break
+ reactor.callLater(0, spin)
+ reactor.callLater(0, spin)
+
+ # Once the connection is lost, make sure the counter is at the
+ # appropriate value.
+ def cbLost(reason):
+ self.assertEqual(count.next(), howMany + 1)
+ self.assertEqual(
+ path.getContent(),
+ ''.join(map(str, range(howMany))))
+ onConnLost.addCallback(cbLost)
+ return onConnLost
+
+ if platform.isWindows():
+ test_normalFileStandardOut.skip = (
+ "StandardIO does not accept stdout as an argument to Windows. "
+ "Testing redirection to a file is therefore harder.")
diff --git a/twisted/test/test_strcred.py b/twisted/test/test_strcred.py
new file mode 100644
index 0000000..7233a58
--- /dev/null
+++ b/twisted/test/test_strcred.py
@@ -0,0 +1,657 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.cred.strcred}.
+"""
+
+import os
+import StringIO
+
+from twisted import plugin
+from twisted.trial import unittest
+from twisted.cred import credentials, checkers, error, strcred
+from twisted.plugins import cred_file, cred_anonymous
+from twisted.python import usage
+from twisted.python.filepath import FilePath
+from twisted.python.fakepwd import UserDatabase
+
+try:
+ import crypt
+except ImportError:
+ crypt = None
+
+try:
+ import pwd
+except ImportError:
+ pwd = None
+
+try:
+ import spwd
+except ImportError:
+ spwd = None
+
+
+
+def getInvalidAuthType():
+ """
+ Helper method to produce an auth type that doesn't exist.
+ """
+ invalidAuthType = 'ThisPluginDoesNotExist'
+ while (invalidAuthType in
+ [factory.authType for factory in strcred.findCheckerFactories()]):
+ invalidAuthType += '_'
+ return invalidAuthType
+
+
+
+class TestPublicAPI(unittest.TestCase):
+
+ def test_emptyDescription(self):
+ """
+ Test that the description string cannot be empty.
+ """
+ iat = getInvalidAuthType()
+ self.assertRaises(strcred.InvalidAuthType, strcred.makeChecker, iat)
+ self.assertRaises(
+ strcred.InvalidAuthType, strcred.findCheckerFactory, iat)
+
+
+ def test_invalidAuthType(self):
+ """
+ Test that an unrecognized auth type raises an exception.
+ """
+ iat = getInvalidAuthType()
+ self.assertRaises(strcred.InvalidAuthType, strcred.makeChecker, iat)
+ self.assertRaises(
+ strcred.InvalidAuthType, strcred.findCheckerFactory, iat)
+
+
+
+class TestStrcredFunctions(unittest.TestCase):
+
+ def test_findCheckerFactories(self):
+ """
+ Test that findCheckerFactories returns all available plugins.
+ """
+ availablePlugins = list(strcred.findCheckerFactories())
+ for plg in plugin.getPlugins(strcred.ICheckerFactory):
+ self.assertIn(plg, availablePlugins)
+
+
+ def test_findCheckerFactory(self):
+ """
+ Test that findCheckerFactory returns the first plugin
+ available for a given authentication type.
+ """
+ self.assertIdentical(strcred.findCheckerFactory('file'),
+ cred_file.theFileCheckerFactory)
+
+
+
+class TestMemoryChecker(unittest.TestCase):
+
+ def setUp(self):
+ self.admin = credentials.UsernamePassword('admin', 'asdf')
+ self.alice = credentials.UsernamePassword('alice', 'foo')
+ self.badPass = credentials.UsernamePassword('alice', 'foobar')
+ self.badUser = credentials.UsernamePassword('x', 'yz')
+ self.checker = strcred.makeChecker('memory:admin:asdf:alice:foo')
+
+
+ def test_isChecker(self):
+ """
+ Verifies that strcred.makeChecker('memory') returns an object
+ that implements the L{ICredentialsChecker} interface.
+ """
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
+ self.assertIn(credentials.IUsernamePassword,
+ self.checker.credentialInterfaces)
+
+
+ def test_badFormatArgString(self):
+ """
+ Test that an argument string which does not contain user:pass
+ pairs (i.e., an odd number of ':' characters) raises an exception.
+ """
+ self.assertRaises(strcred.InvalidAuthArgumentString,
+ strcred.makeChecker, 'memory:a:b:c')
+
+
+ def test_memoryCheckerSucceeds(self):
+ """
+ Test that the checker works with valid credentials.
+ """
+ def _gotAvatar(username):
+ self.assertEqual(username, self.admin.username)
+ return (self.checker
+ .requestAvatarId(self.admin)
+ .addCallback(_gotAvatar))
+
+
+ def test_memoryCheckerFailsUsername(self):
+ """
+ Test that the checker fails with an invalid username.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badUser),
+ error.UnauthorizedLogin)
+
+
+ def test_memoryCheckerFailsPassword(self):
+ """
+ Test that the checker fails with an invalid password.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badPass),
+ error.UnauthorizedLogin)
+
+
+
+class TestAnonymousChecker(unittest.TestCase):
+
+ def test_isChecker(self):
+ """
+ Verifies that strcred.makeChecker('anonymous') returns an object
+ that implements the L{ICredentialsChecker} interface.
+ """
+ checker = strcred.makeChecker('anonymous')
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(checker))
+ self.assertIn(credentials.IAnonymous, checker.credentialInterfaces)
+
+
+ def testAnonymousAccessSucceeds(self):
+ """
+ Test that we can log in anonymously using this checker.
+ """
+ checker = strcred.makeChecker('anonymous')
+ request = checker.requestAvatarId(credentials.Anonymous())
+ def _gotAvatar(avatar):
+ self.assertIdentical(checkers.ANONYMOUS, avatar)
+ return request.addCallback(_gotAvatar)
+
+
+
+class TestUnixChecker(unittest.TestCase):
+ users = {
+ 'admin': 'asdf',
+ 'alice': 'foo',
+ }
+
+
+ def _spwd(self, username):
+ return (username, crypt.crypt(self.users[username], 'F/'),
+ 0, 0, 99999, 7, -1, -1, -1)
+
+
+ def setUp(self):
+ self.admin = credentials.UsernamePassword('admin', 'asdf')
+ self.alice = credentials.UsernamePassword('alice', 'foo')
+ self.badPass = credentials.UsernamePassword('alice', 'foobar')
+ self.badUser = credentials.UsernamePassword('x', 'yz')
+ self.checker = strcred.makeChecker('unix')
+
+ # Hack around the pwd and spwd modules, since we can't really
+ # go about reading your /etc/passwd or /etc/shadow files
+ if pwd:
+ database = UserDatabase()
+ for username, password in self.users.items():
+ database.addUser(
+ username, crypt.crypt(password, 'F/'),
+ 1000, 1000, username, '/home/' + username, '/bin/sh')
+ self.patch(pwd, 'getpwnam', database.getpwnam)
+ if spwd:
+ self._spwd_getspnam = spwd.getspnam
+ spwd.getspnam = self._spwd
+
+
+ def tearDown(self):
+ if spwd:
+ spwd.getspnam = self._spwd_getspnam
+
+
+ def test_isChecker(self):
+ """
+ Verifies that strcred.makeChecker('unix') returns an object
+ that implements the L{ICredentialsChecker} interface.
+ """
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
+ self.assertIn(credentials.IUsernamePassword,
+ self.checker.credentialInterfaces)
+
+
+ def test_unixCheckerSucceeds(self):
+ """
+ Test that the checker works with valid credentials.
+ """
+ def _gotAvatar(username):
+ self.assertEqual(username, self.admin.username)
+ return (self.checker
+ .requestAvatarId(self.admin)
+ .addCallback(_gotAvatar))
+
+
+ def test_unixCheckerFailsUsername(self):
+ """
+ Test that the checker fails with an invalid username.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badUser),
+ error.UnauthorizedLogin)
+
+
+ def test_unixCheckerFailsPassword(self):
+ """
+ Test that the checker fails with an invalid password.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badPass),
+ error.UnauthorizedLogin)
+
+
+ if None in (pwd, spwd, crypt):
+ availability = []
+ for module, name in ((pwd, "pwd"), (spwd, "swpd"), (crypt, "crypt")):
+ if module is None:
+ availability += [name]
+ for method in (test_unixCheckerSucceeds,
+ test_unixCheckerFailsUsername,
+ test_unixCheckerFailsPassword):
+ method.skip = ("Required module(s) are unavailable: " +
+ ", ".join(availability))
+
+
+
+class TestFileDBChecker(unittest.TestCase):
+ """
+ Test for the --auth=file:... file checker.
+ """
+
+ def setUp(self):
+ self.admin = credentials.UsernamePassword('admin', 'asdf')
+ self.alice = credentials.UsernamePassword('alice', 'foo')
+ self.badPass = credentials.UsernamePassword('alice', 'foobar')
+ self.badUser = credentials.UsernamePassword('x', 'yz')
+ self.filename = self.mktemp()
+ FilePath(self.filename).setContent('admin:asdf\nalice:foo\n')
+ self.checker = strcred.makeChecker('file:' + self.filename)
+
+
+ def _fakeFilename(self):
+ filename = '/DoesNotExist'
+ while os.path.exists(filename):
+ filename += '_'
+ return filename
+
+
+ def test_isChecker(self):
+ """
+ Verifies that strcred.makeChecker('memory') returns an object
+ that implements the L{ICredentialsChecker} interface.
+ """
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(self.checker))
+ self.assertIn(credentials.IUsernamePassword,
+ self.checker.credentialInterfaces)
+
+
+ def test_fileCheckerSucceeds(self):
+ """
+ Test that the checker works with valid credentials.
+ """
+ def _gotAvatar(username):
+ self.assertEqual(username, self.admin.username)
+ return (self.checker
+ .requestAvatarId(self.admin)
+ .addCallback(_gotAvatar))
+
+
+ def test_fileCheckerFailsUsername(self):
+ """
+ Test that the checker fails with an invalid username.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badUser),
+ error.UnauthorizedLogin)
+
+
+ def test_fileCheckerFailsPassword(self):
+ """
+ Test that the checker fails with an invalid password.
+ """
+ return self.assertFailure(self.checker.requestAvatarId(self.badPass),
+ error.UnauthorizedLogin)
+
+
+ def test_failsWithEmptyFilename(self):
+ """
+ Test that an empty filename raises an error.
+ """
+ self.assertRaises(ValueError, strcred.makeChecker, 'file')
+ self.assertRaises(ValueError, strcred.makeChecker, 'file:')
+
+
+ def test_warnWithBadFilename(self):
+ """
+ When the file auth plugin is given a file that doesn't exist, it
+ should produce a warning.
+ """
+ oldOutput = cred_file.theFileCheckerFactory.errorOutput
+ newOutput = StringIO.StringIO()
+ cred_file.theFileCheckerFactory.errorOutput = newOutput
+ checker = strcred.makeChecker('file:' + self._fakeFilename())
+ cred_file.theFileCheckerFactory.errorOutput = oldOutput
+ self.assertIn(cred_file.invalidFileWarning, newOutput.getvalue())
+
+
+
+class TestSSHChecker(unittest.TestCase):
+ """
+ Tests for the --auth=sshkey:... checker. The majority of the tests for the
+ ssh public key database checker are in
+ L{twisted.conch.test.test_checkers.SSHPublicKeyDatabaseTestCase}.
+ """
+
+ try:
+ import Crypto
+ import pyasn1
+ except ImportError:
+ skip = "PyCrypto is not available"
+
+
+ def test_isChecker(self):
+ """
+ Verifies that strcred.makeChecker('sshkey') returns an object
+ that implements the L{ICredentialsChecker} interface.
+ """
+ sshChecker = strcred.makeChecker('sshkey')
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(sshChecker))
+ self.assertIn(
+ credentials.ISSHPrivateKey, sshChecker.credentialInterfaces)
+
+
+
+class DummyOptions(usage.Options, strcred.AuthOptionMixin):
+ """
+ Simple options for testing L{strcred.AuthOptionMixin}.
+ """
+
+
+
+class TestCheckerOptions(unittest.TestCase):
+
+ def test_createsList(self):
+ """
+ Test that the --auth command line creates a list in the
+ Options instance and appends values to it.
+ """
+ options = DummyOptions()
+ options.parseOptions(['--auth', 'memory'])
+ self.assertEqual(len(options['credCheckers']), 1)
+ options = DummyOptions()
+ options.parseOptions(['--auth', 'memory', '--auth', 'memory'])
+ self.assertEqual(len(options['credCheckers']), 2)
+
+
+ def test_invalidAuthError(self):
+ """
+ Test that the --auth command line raises an exception when it
+ gets a parameter it doesn't understand.
+ """
+ options = DummyOptions()
+ # If someone adds a 'ThisPluginDoesNotExist' then this unit
+ # test should still run.
+ invalidParameter = getInvalidAuthType()
+ self.assertRaises(
+ usage.UsageError,
+ options.parseOptions, ['--auth', invalidParameter])
+ self.assertRaises(
+ usage.UsageError,
+ options.parseOptions, ['--help-auth-type', invalidParameter])
+
+
+ def test_createsDictionary(self):
+ """
+ Test that the --auth command line creates a dictionary
+ mapping supported interfaces to the list of credentials
+ checkers that support it.
+ """
+ options = DummyOptions()
+ options.parseOptions(['--auth', 'memory', '--auth', 'anonymous'])
+ chd = options['credInterfaces']
+ self.assertEqual(len(chd[credentials.IAnonymous]), 1)
+ self.assertEqual(len(chd[credentials.IUsernamePassword]), 1)
+ chdAnonymous = chd[credentials.IAnonymous][0]
+ chdUserPass = chd[credentials.IUsernamePassword][0]
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(chdAnonymous))
+ self.assertTrue(checkers.ICredentialsChecker.providedBy(chdUserPass))
+ self.assertIn(credentials.IAnonymous,
+ chdAnonymous.credentialInterfaces)
+ self.assertIn(credentials.IUsernamePassword,
+ chdUserPass.credentialInterfaces)
+
+
+ def test_credInterfacesProvidesLists(self):
+ """
+ Test that when two --auth arguments are passed along which
+ support the same interface, a list with both is created.
+ """
+ options = DummyOptions()
+ options.parseOptions(['--auth', 'memory', '--auth', 'unix'])
+ self.assertEqual(
+ options['credCheckers'],
+ options['credInterfaces'][credentials.IUsernamePassword])
+
+
+ def test_listDoesNotDisplayDuplicates(self):
+ """
+ Test that the list for --help-auth does not duplicate items.
+ """
+ authTypes = []
+ options = DummyOptions()
+ for cf in options._checkerFactoriesForOptHelpAuth():
+ self.assertNotIn(cf.authType, authTypes)
+ authTypes.append(cf.authType)
+
+
+ def test_displaysListCorrectly(self):
+ """
+ Test that the --help-auth argument correctly displays all
+ available authentication plugins, then exits.
+ """
+ newStdout = StringIO.StringIO()
+ options = DummyOptions()
+ options.authOutput = newStdout
+ self.assertRaises(SystemExit, options.parseOptions, ['--help-auth'])
+ for checkerFactory in strcred.findCheckerFactories():
+ self.assertIn(checkerFactory.authType, newStdout.getvalue())
+
+
+ def test_displaysHelpCorrectly(self):
+ """
+ Test that the --help-auth-for argument will correctly display
+ the help file for a particular authentication plugin.
+ """
+ newStdout = StringIO.StringIO()
+ options = DummyOptions()
+ options.authOutput = newStdout
+ self.assertRaises(
+ SystemExit, options.parseOptions, ['--help-auth-type', 'file'])
+ for line in cred_file.theFileCheckerFactory.authHelp:
+ if line.strip():
+ self.assertIn(line.strip(), newStdout.getvalue())
+
+
+ def test_unexpectedException(self):
+ """
+ When the checker specified by --auth raises an unexpected error, it
+ should be caught and re-raised within a L{usage.UsageError}.
+ """
+ options = DummyOptions()
+ err = self.assertRaises(usage.UsageError, options.parseOptions,
+ ['--auth', 'file'])
+ self.assertEqual(str(err),
+ "Unexpected error: 'file' requires a filename")
+
+
+
+class OptionsForUsernamePassword(usage.Options, strcred.AuthOptionMixin):
+ supportedInterfaces = (credentials.IUsernamePassword,)
+
+
+
+class OptionsForUsernameHashedPassword(usage.Options, strcred.AuthOptionMixin):
+ supportedInterfaces = (credentials.IUsernameHashedPassword,)
+
+
+
+class OptionsSupportsAllInterfaces(usage.Options, strcred.AuthOptionMixin):
+ supportedInterfaces = None
+
+
+
+class OptionsSupportsNoInterfaces(usage.Options, strcred.AuthOptionMixin):
+ supportedInterfaces = []
+
+
+
+class TestLimitingInterfaces(unittest.TestCase):
+ """
+ Tests functionality that allows an application to limit the
+ credential interfaces it can support. For the purposes of this
+ test, we use IUsernameHashedPassword, although this will never
+ really be used by the command line.
+
+ (I have, to date, not thought of a half-decent way for a user to
+ specify a hash algorithm via the command-line. Nor do I think it's
+ very useful.)
+
+ I should note that, at first, this test is counter-intuitive,
+ because we're using the checker with a pre-defined hash function
+ as the 'bad' checker. See the documentation for
+ L{twisted.cred.checkers.FilePasswordDB.hash} for more details.
+ """
+
+ def setUp(self):
+ self.filename = self.mktemp()
+ file(self.filename, 'w').write('admin:asdf\nalice:foo\n')
+ self.goodChecker = checkers.FilePasswordDB(self.filename)
+ self.badChecker = checkers.FilePasswordDB(
+ self.filename, hash=self._hash)
+ self.anonChecker = checkers.AllowAnonymousAccess()
+
+
+ def _hash(self, networkUsername, networkPassword, storedPassword):
+ """
+ A dumb hash that doesn't really do anything.
+ """
+ return networkPassword
+
+
+ def test_supportsInterface(self):
+ """
+ Test that the supportsInterface method behaves appropriately.
+ """
+ options = OptionsForUsernamePassword()
+ self.assertTrue(
+ options.supportsInterface(credentials.IUsernamePassword))
+ self.assertFalse(
+ options.supportsInterface(credentials.IAnonymous))
+ self.assertRaises(
+ strcred.UnsupportedInterfaces, options.addChecker,
+ self.anonChecker)
+
+
+ def test_supportsAllInterfaces(self):
+ """
+ Test that the supportsInterface method behaves appropriately
+ when the supportedInterfaces attribute is None.
+ """
+ options = OptionsSupportsAllInterfaces()
+ self.assertTrue(
+ options.supportsInterface(credentials.IUsernamePassword))
+ self.assertTrue(
+ options.supportsInterface(credentials.IAnonymous))
+
+
+ def test_supportsCheckerFactory(self):
+ """
+ Test that the supportsCheckerFactory method behaves appropriately.
+ """
+ options = OptionsForUsernamePassword()
+ fileCF = cred_file.theFileCheckerFactory
+ anonCF = cred_anonymous.theAnonymousCheckerFactory
+ self.assertTrue(options.supportsCheckerFactory(fileCF))
+ self.assertFalse(options.supportsCheckerFactory(anonCF))
+
+
+ def test_canAddSupportedChecker(self):
+ """
+ Test that when addChecker is called with a checker that
+ implements at least one of the interfaces our application
+ supports, it is successful.
+ """
+ options = OptionsForUsernamePassword()
+ options.addChecker(self.goodChecker)
+ iface = options.supportedInterfaces[0]
+ # Test that we did get IUsernamePassword
+ self.assertIdentical(
+ options['credInterfaces'][iface][0], self.goodChecker)
+ self.assertIdentical(options['credCheckers'][0], self.goodChecker)
+ # Test that we didn't get IUsernameHashedPassword
+ self.assertEqual(len(options['credInterfaces'][iface]), 1)
+ self.assertEqual(len(options['credCheckers']), 1)
+
+
+ def test_failOnAddingUnsupportedChecker(self):
+ """
+ Test that when addChecker is called with a checker that does
+ not implement any supported interfaces, it fails.
+ """
+ options = OptionsForUsernameHashedPassword()
+ self.assertRaises(strcred.UnsupportedInterfaces,
+ options.addChecker, self.badChecker)
+
+
+ def test_unsupportedInterfaceError(self):
+ """
+ Test that the --auth command line raises an exception when it
+ gets a checker we don't support.
+ """
+ options = OptionsSupportsNoInterfaces()
+ authType = cred_anonymous.theAnonymousCheckerFactory.authType
+ self.assertRaises(
+ usage.UsageError,
+ options.parseOptions, ['--auth', authType])
+
+
+ def test_helpAuthLimitsOutput(self):
+ """
+ Test that --help-auth will only list checkers that purport to
+ supply at least one of the credential interfaces our
+ application can use.
+ """
+ options = OptionsForUsernamePassword()
+ for factory in options._checkerFactoriesForOptHelpAuth():
+ invalid = True
+ for interface in factory.credentialInterfaces:
+ if options.supportsInterface(interface):
+ invalid = False
+ if invalid:
+ raise strcred.UnsupportedInterfaces()
+
+
+ def test_helpAuthTypeLimitsOutput(self):
+ """
+ Test that --help-auth-type will display a warning if you get
+ help for an authType that does not supply at least one of the
+ credential interfaces our application can use.
+ """
+ options = OptionsForUsernamePassword()
+ # Find an interface that we can use for our test
+ invalidFactory = None
+ for factory in strcred.findCheckerFactories():
+ if not options.supportsCheckerFactory(factory):
+ invalidFactory = factory
+ break
+ self.assertNotIdentical(invalidFactory, None)
+ # Capture output and make sure the warning is there
+ newStdout = StringIO.StringIO()
+ options.authOutput = newStdout
+ self.assertRaises(SystemExit, options.parseOptions,
+ ['--help-auth-type', 'anonymous'])
+ self.assertIn(strcred.notSupportedWarning, newStdout.getvalue())
diff --git a/twisted/test/test_strerror.py b/twisted/test/test_strerror.py
new file mode 100644
index 0000000..ce14051
--- /dev/null
+++ b/twisted/test/test_strerror.py
@@ -0,0 +1,151 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test strerror
+"""
+
+import socket
+import os
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.tcp import ECONNABORTED
+from twisted.python.win32 import _ErrorFormatter, formatError
+from twisted.python.runtime import platform
+
+
+class _MyWindowsException(OSError):
+ """
+ An exception type like L{ctypes.WinError}, but available on all platforms.
+ """
+
+
+
+class ErrorFormatingTestCase(TestCase):
+ """
+ Tests for C{_ErrorFormatter.formatError}.
+ """
+ probeErrorCode = ECONNABORTED
+ probeMessage = "correct message value"
+
+ def test_strerrorFormatting(self):
+ """
+ L{_ErrorFormatter.formatError} should use L{os.strerror} to format
+ error messages if it is constructed without any better mechanism.
+ """
+ formatter = _ErrorFormatter(None, None, None)
+ message = formatter.formatError(self.probeErrorCode)
+ self.assertEqual(message, os.strerror(self.probeErrorCode))
+
+
+ def test_emptyErrorTab(self):
+ """
+ L{_ErrorFormatter.formatError} should use L{os.strerror} to format
+ error messages if it is constructed with only an error tab which does
+ not contain the error code it is called with.
+ """
+ error = 1
+ # Sanity check
+ self.assertNotEqual(self.probeErrorCode, error)
+ formatter = _ErrorFormatter(None, None, {error: 'wrong message'})
+ message = formatter.formatError(self.probeErrorCode)
+ self.assertEqual(message, os.strerror(self.probeErrorCode))
+
+
+ def test_errorTab(self):
+ """
+ L{_ErrorFormatter.formatError} should use C{errorTab} if it is supplied
+ and contains the requested error code.
+ """
+ formatter = _ErrorFormatter(
+ None, None, {self.probeErrorCode: self.probeMessage})
+ message = formatter.formatError(self.probeErrorCode)
+ self.assertEqual(message, self.probeMessage)
+
+
+ def test_formatMessage(self):
+ """
+ L{_ErrorFormatter.formatError} should return the return value of
+ C{formatMessage} if it is supplied.
+ """
+ formatCalls = []
+ def formatMessage(errorCode):
+ formatCalls.append(errorCode)
+ return self.probeMessage
+ formatter = _ErrorFormatter(
+ None, formatMessage, {self.probeErrorCode: 'wrong message'})
+ message = formatter.formatError(self.probeErrorCode)
+ self.assertEqual(message, self.probeMessage)
+ self.assertEqual(formatCalls, [self.probeErrorCode])
+
+
+ def test_winError(self):
+ """
+ L{_ErrorFormatter.formatError} should return the message argument from
+ the exception L{winError} returns, if L{winError} is supplied.
+ """
+ winCalls = []
+ def winError(errorCode):
+ winCalls.append(errorCode)
+ return _MyWindowsException(errorCode, self.probeMessage)
+ formatter = _ErrorFormatter(
+ winError,
+ lambda error: 'formatMessage: wrong message',
+ {self.probeErrorCode: 'errorTab: wrong message'})
+ message = formatter.formatError(self.probeErrorCode)
+ self.assertEqual(message, self.probeMessage)
+
+
+ def test_fromEnvironment(self):
+ """
+ L{_ErrorFormatter.fromEnvironment} should create an L{_ErrorFormatter}
+ instance with attributes populated from available modules.
+ """
+ formatter = _ErrorFormatter.fromEnvironment()
+
+ if formatter.winError is not None:
+ from ctypes import WinError
+ self.assertEqual(
+ formatter.formatError(self.probeErrorCode),
+ WinError(self.probeErrorCode).strerror)
+ formatter.winError = None
+
+ if formatter.formatMessage is not None:
+ from win32api import FormatMessage
+ self.assertEqual(
+ formatter.formatError(self.probeErrorCode),
+ FormatMessage(self.probeErrorCode))
+ formatter.formatMessage = None
+
+ if formatter.errorTab is not None:
+ from socket import errorTab
+ self.assertEqual(
+ formatter.formatError(self.probeErrorCode),
+ errorTab[self.probeErrorCode])
+
+ if platform.getType() != "win32":
+ test_fromEnvironment.skip = "Test will run only on Windows."
+
+
+ def test_correctLookups(self):
+ """
+ Given an known-good errno, make sure that formatMessage gives results
+ matching either C{socket.errorTab}, C{ctypes.WinError}, or
+ C{win32api.FormatMessage}.
+ """
+ acceptable = [socket.errorTab[ECONNABORTED]]
+ try:
+ from ctypes import WinError
+ acceptable.append(WinError(ECONNABORTED).strerror)
+ except ImportError:
+ pass
+ try:
+ from win32api import FormatMessage
+ acceptable.append(FormatMessage(ECONNABORTED))
+ except ImportError:
+ pass
+
+ self.assertIn(formatError(ECONNABORTED), acceptable)
+
+ if platform.getType() != "win32":
+ test_correctLookups.skip = "Test will run only on Windows."
diff --git a/twisted/test/test_stringtransport.py b/twisted/test/test_stringtransport.py
new file mode 100644
index 0000000..ca12098
--- /dev/null
+++ b/twisted/test/test_stringtransport.py
@@ -0,0 +1,279 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.test.proto_helpers}.
+"""
+
+from zope.interface.verify import verifyObject
+
+from twisted.internet.interfaces import (ITransport, IPushProducer, IConsumer,
+ IReactorTCP, IReactorSSL, IReactorUNIX, IAddress, IListeningPort,
+ IConnector)
+from twisted.internet.address import IPv4Address
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import (StringTransport, MemoryReactor,
+ RaisingMemoryReactor)
+from twisted.internet.protocol import ClientFactory, Factory
+
+
+class StringTransportTests(TestCase):
+ """
+ Tests for L{twisted.test.proto_helpers.StringTransport}.
+ """
+ def setUp(self):
+ self.transport = StringTransport()
+
+
+ def test_interfaces(self):
+ """
+ L{StringTransport} instances provide L{ITransport}, L{IPushProducer},
+ and L{IConsumer}.
+ """
+ self.assertTrue(verifyObject(ITransport, self.transport))
+ self.assertTrue(verifyObject(IPushProducer, self.transport))
+ self.assertTrue(verifyObject(IConsumer, self.transport))
+
+
+ def test_registerProducer(self):
+ """
+ L{StringTransport.registerProducer} records the arguments supplied to
+ it as instance attributes.
+ """
+ producer = object()
+ streaming = object()
+ self.transport.registerProducer(producer, streaming)
+ self.assertIdentical(self.transport.producer, producer)
+ self.assertIdentical(self.transport.streaming, streaming)
+
+
+ def test_disallowedRegisterProducer(self):
+ """
+ L{StringTransport.registerProducer} raises L{RuntimeError} if a
+ producer is already registered.
+ """
+ producer = object()
+ self.transport.registerProducer(producer, True)
+ self.assertRaises(
+ RuntimeError, self.transport.registerProducer, object(), False)
+ self.assertIdentical(self.transport.producer, producer)
+ self.assertTrue(self.transport.streaming)
+
+
+ def test_unregisterProducer(self):
+ """
+ L{StringTransport.unregisterProducer} causes the transport to forget
+ about the registered producer and makes it possible to register a new
+ one.
+ """
+ oldProducer = object()
+ newProducer = object()
+ self.transport.registerProducer(oldProducer, False)
+ self.transport.unregisterProducer()
+ self.assertIdentical(self.transport.producer, None)
+ self.transport.registerProducer(newProducer, True)
+ self.assertIdentical(self.transport.producer, newProducer)
+ self.assertTrue(self.transport.streaming)
+
+
+ def test_invalidUnregisterProducer(self):
+ """
+ L{StringTransport.unregisterProducer} raises L{RuntimeError} if called
+ when no producer is registered.
+ """
+ self.assertRaises(RuntimeError, self.transport.unregisterProducer)
+
+
+ def test_initialProducerState(self):
+ """
+ L{StringTransport.producerState} is initially C{'producing'}.
+ """
+ self.assertEqual(self.transport.producerState, 'producing')
+
+
+ def test_pauseProducing(self):
+ """
+ L{StringTransport.pauseProducing} changes the C{producerState} of the
+ transport to C{'paused'}.
+ """
+ self.transport.pauseProducing()
+ self.assertEqual(self.transport.producerState, 'paused')
+
+
+ def test_resumeProducing(self):
+ """
+ L{StringTransport.resumeProducing} changes the C{producerState} of the
+ transport to C{'producing'}.
+ """
+ self.transport.pauseProducing()
+ self.transport.resumeProducing()
+ self.assertEqual(self.transport.producerState, 'producing')
+
+
+ def test_stopProducing(self):
+ """
+ L{StringTransport.stopProducing} changes the C{'producerState'} of the
+ transport to C{'stopped'}.
+ """
+ self.transport.stopProducing()
+ self.assertEqual(self.transport.producerState, 'stopped')
+
+
+ def test_stoppedTransportCannotPause(self):
+ """
+ L{StringTransport.pauseProducing} raises L{RuntimeError} if the
+ transport has been stopped.
+ """
+ self.transport.stopProducing()
+ self.assertRaises(RuntimeError, self.transport.pauseProducing)
+
+
+ def test_stoppedTransportCannotResume(self):
+ """
+ L{StringTransport.resumeProducing} raises L{RuntimeError} if the
+ transport has been stopped.
+ """
+ self.transport.stopProducing()
+ self.assertRaises(RuntimeError, self.transport.resumeProducing)
+
+
+ def test_disconnectingTransportCannotPause(self):
+ """
+ L{StringTransport.pauseProducing} raises L{RuntimeError} if the
+ transport is being disconnected.
+ """
+ self.transport.loseConnection()
+ self.assertRaises(RuntimeError, self.transport.pauseProducing)
+
+
+ def test_disconnectingTransportCannotResume(self):
+ """
+ L{StringTransport.resumeProducing} raises L{RuntimeError} if the
+ transport is being disconnected.
+ """
+ self.transport.loseConnection()
+ self.assertRaises(RuntimeError, self.transport.resumeProducing)
+
+
+ def test_loseConnectionSetsDisconnecting(self):
+ """
+ L{StringTransport.loseConnection} toggles the C{disconnecting} instance
+ variable to C{True}.
+ """
+ self.assertFalse(self.transport.disconnecting)
+ self.transport.loseConnection()
+ self.assertTrue(self.transport.disconnecting)
+
+
+ def test_specifiedHostAddress(self):
+ """
+ If a host address is passed to L{StringTransport.__init__}, that
+ value is returned from L{StringTransport.getHost}.
+ """
+ address = object()
+ self.assertIdentical(StringTransport(address).getHost(), address)
+
+
+ def test_specifiedPeerAddress(self):
+ """
+ If a peer address is passed to L{StringTransport.__init__}, that
+ value is returned from L{StringTransport.getPeer}.
+ """
+ address = object()
+ self.assertIdentical(
+ StringTransport(peerAddress=address).getPeer(), address)
+
+
+ def test_defaultHostAddress(self):
+ """
+ If no host address is passed to L{StringTransport.__init__}, an
+ L{IPv4Address} is returned from L{StringTransport.getHost}.
+ """
+ address = StringTransport().getHost()
+ self.assertIsInstance(address, IPv4Address)
+
+
+ def test_defaultPeerAddress(self):
+ """
+ If no peer address is passed to L{StringTransport.__init__}, an
+ L{IPv4Address} is returned from L{StringTransport.getPeer}.
+ """
+ address = StringTransport().getPeer()
+ self.assertIsInstance(address, IPv4Address)
+
+
+
+class ReactorTests(TestCase):
+ """
+ Tests for L{MemoryReactor} and L{RaisingMemoryReactor}.
+ """
+
+ def test_memoryReactorProvides(self):
+ """
+ L{MemoryReactor} provides all of the attributes described by the
+ interfaces it advertises.
+ """
+ memoryReactor = MemoryReactor()
+ verifyObject(IReactorTCP, memoryReactor)
+ verifyObject(IReactorSSL, memoryReactor)
+ verifyObject(IReactorUNIX, memoryReactor)
+
+
+ def test_raisingReactorProvides(self):
+ """
+ L{RaisingMemoryReactor} provides all of the attributes described by the
+ interfaces it advertises.
+ """
+ raisingReactor = RaisingMemoryReactor()
+ verifyObject(IReactorTCP, raisingReactor)
+ verifyObject(IReactorSSL, raisingReactor)
+ verifyObject(IReactorUNIX, raisingReactor)
+
+
+ def test_connectDestination(self):
+ """
+ L{MemoryReactor.connectTCP}, L{MemoryReactor.connectSSL}, and
+ L{MemoryReactor.connectUNIX} will return an L{IConnector} whose
+ C{getDestination} method returns an L{IAddress} with attributes which
+ reflect the values passed.
+ """
+ memoryReactor = MemoryReactor()
+ for connector in [memoryReactor.connectTCP(
+ "test.example.com", 8321, ClientFactory()),
+ memoryReactor.connectSSL(
+ "test.example.com", 8321, ClientFactory(),
+ None)]:
+ verifyObject(IConnector, connector)
+ address = connector.getDestination()
+ verifyObject(IAddress, address)
+ self.assertEqual(address.host, "test.example.com")
+ self.assertEqual(address.port, 8321)
+ connector = memoryReactor.connectUNIX("/fake/path", ClientFactory())
+ verifyObject(IConnector, connector)
+ address = connector.getDestination()
+ verifyObject(IAddress, address)
+ self.assertEqual(address.name, "/fake/path")
+
+
+ def test_listenDefaultHost(self):
+ """
+ L{MemoryReactor.listenTCP}, L{MemoryReactor.listenSSL} and
+ L{MemoryReactor.listenUNIX} will return an L{IListeningPort} whose
+ C{getHost} method returns an L{IAddress}; C{listenTCP} and C{listenSSL}
+ will have a default host of C{'0.0.0.0'}, and a port that reflects the
+ value passed, and C{listenUNIX} will have a name that reflects the path
+ passed.
+ """
+ memoryReactor = MemoryReactor()
+ for port in [memoryReactor.listenTCP(8242, Factory()),
+ memoryReactor.listenSSL(8242, Factory(), None)]:
+ verifyObject(IListeningPort, port)
+ address = port.getHost()
+ verifyObject(IAddress, address)
+ self.assertEqual(address.host, '0.0.0.0')
+ self.assertEqual(address.port, 8242)
+ port = memoryReactor.listenUNIX("/path/to/socket", Factory())
+ verifyObject(IListeningPort, port)
+ address = port.getHost()
+ verifyObject(IAddress, address)
+ self.assertEqual(address.name, "/path/to/socket")
diff --git a/twisted/test/test_strports.py b/twisted/test/test_strports.py
new file mode 100644
index 0000000..fd081ec
--- /dev/null
+++ b/twisted/test/test_strports.py
@@ -0,0 +1,133 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.application.strports}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.application import strports
+from twisted.application import internet
+from twisted.internet.test.test_endpoints import ParserTestCase
+from twisted.internet.protocol import Factory
+from twisted.internet.endpoints import TCP4ServerEndpoint, UNIXServerEndpoint
+
+
+
+class DeprecatedParseTestCase(ParserTestCase):
+ """
+ L{strports.parse} is deprecated. It's an alias for a method that is now
+ private in L{twisted.internet.endpoints}.
+ """
+
+ def parse(self, *a, **kw):
+ result = strports.parse(*a, **kw)
+ warnings = self.flushWarnings([self.parse])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.application.strports.parse was deprecated "
+ "in Twisted 10.2.0: in favor of twisted.internet.endpoints.serverFromString")
+ return result
+
+
+ def test_simpleNumeric(self):
+ """
+ Base numeric ports should be parsed as TCP.
+ """
+ self.assertEqual(self.parse('80', self.f),
+ ('TCP', (80, self.f), {'interface':'', 'backlog':50}))
+
+
+ def test_allKeywords(self):
+ """
+ A collection of keyword arguments with no prefixed type, like 'port=80',
+ will be parsed as keyword arguments to 'tcp'.
+ """
+ self.assertEqual(self.parse('port=80', self.f),
+ ('TCP', (80, self.f), {'interface':'', 'backlog':50}))
+
+
+
+class ServiceTestCase(TestCase):
+ """
+ Tests for L{strports.service}.
+ """
+
+ def test_service(self):
+ """
+ L{strports.service} returns a L{StreamServerEndpointService}
+ constructed with an endpoint produced from
+ L{endpoint.serverFromString}, using the same syntax.
+ """
+ reactor = object() # the cake is a lie
+ aFactory = Factory()
+ aGoodPort = 1337
+ svc = strports.service(
+ 'tcp:'+str(aGoodPort), aFactory, reactor=reactor)
+ self.assertIsInstance(svc, internet.StreamServerEndpointService)
+
+ # See twisted.application.test.test_internet.TestEndpointService.
+ # test_synchronousRaiseRaisesSynchronously
+ self.assertEqual(svc._raiseSynchronously, True)
+ self.assertIsInstance(svc.endpoint, TCP4ServerEndpoint)
+ # Maybe we should implement equality for endpoints.
+ self.assertEqual(svc.endpoint._port, aGoodPort)
+ self.assertIdentical(svc.factory, aFactory)
+ self.assertIdentical(svc.endpoint._reactor, reactor)
+
+
+ def test_serviceDefaultReactor(self):
+ """
+ L{strports.service} will use the default reactor when none is provided
+ as an argument.
+ """
+ from twisted.internet import reactor as globalReactor
+ aService = strports.service("tcp:80", None)
+ self.assertIdentical(aService.endpoint._reactor, globalReactor)
+
+
+ def test_serviceDeprecatedDefault(self):
+ """
+ L{strports.service} still accepts a 'default' argument, which will
+ affect the parsing of 'default' (i.e. 'not containing a colon')
+ endpoint descriptions, but this behavior is deprecated.
+ """
+ svc = strports.service("8080", None, "unix")
+ self.assertIsInstance(svc.endpoint, UNIXServerEndpoint)
+ warnings = self.flushWarnings([self.test_serviceDeprecatedDefault])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "The 'default' parameter was deprecated in Twisted 10.2.0. "
+ "Use qualified endpoint descriptions; for example, 'tcp:8080'.")
+ self.assertEqual(len(warnings), 1)
+
+ # Almost the same case, but slightly tricky - explicitly passing the old
+ # default value, None, also must trigger a deprecation warning.
+ svc = strports.service("tcp:8080", None, None)
+ self.assertIsInstance(svc.endpoint, TCP4ServerEndpoint)
+ warnings = self.flushWarnings([self.test_serviceDeprecatedDefault])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "The 'default' parameter was deprecated in Twisted 10.2.0.")
+ self.assertEqual(len(warnings), 1)
+
+
+ def test_serviceDeprecatedUnqualified(self):
+ """
+ Unqualified strport descriptions, i.e. "8080", are deprecated.
+ """
+ svc = strports.service("8080", None)
+ self.assertIsInstance(svc.endpoint, TCP4ServerEndpoint)
+ warnings = self.flushWarnings(
+ [self.test_serviceDeprecatedUnqualified])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Unqualified strport description passed to 'service'."
+ "Use qualified endpoint descriptions; for example, 'tcp:8080'.")
+ self.assertEqual(len(warnings), 1)
+
+
diff --git a/twisted/test/test_task.py b/twisted/test/test_task.py
new file mode 100644
index 0000000..75be888
--- /dev/null
+++ b/twisted/test/test_task.py
@@ -0,0 +1,739 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.python.compat import set
+
+from twisted.trial import unittest
+
+from twisted.internet import interfaces, task, reactor, defer, error
+
+# Be compatible with any jerks who used our private stuff
+Clock = task.Clock
+
+from twisted.python import failure
+
+
+class TestableLoopingCall(task.LoopingCall):
+ def __init__(self, clock, *a, **kw):
+ super(TestableLoopingCall, self).__init__(*a, **kw)
+ self.clock = clock
+
+
+
+class TestException(Exception):
+ pass
+
+
+
+class ClockTestCase(unittest.TestCase):
+ """
+ Test the non-wallclock based clock implementation.
+ """
+ def testSeconds(self):
+ """
+ Test that the L{seconds} method of the fake clock returns fake time.
+ """
+ c = task.Clock()
+ self.assertEqual(c.seconds(), 0)
+
+
+ def testCallLater(self):
+ """
+ Test that calls can be scheduled for later with the fake clock and
+ hands back an L{IDelayedCall}.
+ """
+ c = task.Clock()
+ call = c.callLater(1, lambda a, b: None, 1, b=2)
+ self.failUnless(interfaces.IDelayedCall.providedBy(call))
+ self.assertEqual(call.getTime(), 1)
+ self.failUnless(call.active())
+
+
+ def testCallLaterCancelled(self):
+ """
+ Test that calls can be cancelled.
+ """
+ c = task.Clock()
+ call = c.callLater(1, lambda a, b: None, 1, b=2)
+ call.cancel()
+ self.failIf(call.active())
+
+
+ def test_callLaterOrdering(self):
+ """
+ Test that the DelayedCall returned is not one previously
+ created.
+ """
+ c = task.Clock()
+ call1 = c.callLater(10, lambda a, b: None, 1, b=2)
+ call2 = c.callLater(1, lambda a, b: None, 3, b=4)
+ self.failIf(call1 is call2)
+
+
+ def testAdvance(self):
+ """
+ Test that advancing the clock will fire some calls.
+ """
+ events = []
+ c = task.Clock()
+ call = c.callLater(2, lambda: events.append(None))
+ c.advance(1)
+ self.assertEqual(events, [])
+ c.advance(1)
+ self.assertEqual(events, [None])
+ self.failIf(call.active())
+
+
+ def testAdvanceCancel(self):
+ """
+ Test attemping to cancel the call in a callback.
+
+ AlreadyCalled should be raised, not for example a ValueError from
+ removing the call from Clock.calls. This requires call.called to be
+ set before the callback is called.
+ """
+ c = task.Clock()
+ def cb():
+ self.assertRaises(error.AlreadyCalled, call.cancel)
+ call = c.callLater(1, cb)
+ c.advance(1)
+
+
+ def testCallLaterDelayed(self):
+ """
+ Test that calls can be delayed.
+ """
+ events = []
+ c = task.Clock()
+ call = c.callLater(1, lambda a, b: events.append((a, b)), 1, b=2)
+ call.delay(1)
+ self.assertEqual(call.getTime(), 2)
+ c.advance(1.5)
+ self.assertEqual(events, [])
+ c.advance(1.0)
+ self.assertEqual(events, [(1, 2)])
+
+
+ def testCallLaterResetLater(self):
+ """
+ Test that calls can have their time reset to a later time.
+ """
+ events = []
+ c = task.Clock()
+ call = c.callLater(2, lambda a, b: events.append((a, b)), 1, b=2)
+ c.advance(1)
+ call.reset(3)
+ self.assertEqual(call.getTime(), 4)
+ c.advance(2)
+ self.assertEqual(events, [])
+ c.advance(1)
+ self.assertEqual(events, [(1, 2)])
+
+
+ def testCallLaterResetSooner(self):
+ """
+ Test that calls can have their time reset to an earlier time.
+ """
+ events = []
+ c = task.Clock()
+ call = c.callLater(4, lambda a, b: events.append((a, b)), 1, b=2)
+ call.reset(3)
+ self.assertEqual(call.getTime(), 3)
+ c.advance(3)
+ self.assertEqual(events, [(1, 2)])
+
+
+ def test_getDelayedCalls(self):
+ """
+ Test that we can get a list of all delayed calls
+ """
+ c = task.Clock()
+ call = c.callLater(1, lambda x: None)
+ call2 = c.callLater(2, lambda x: None)
+
+ calls = c.getDelayedCalls()
+
+ self.assertEqual(set([call, call2]), set(calls))
+
+
+ def test_getDelayedCallsEmpty(self):
+ """
+ Test that we get an empty list from getDelayedCalls on a newly
+ constructed Clock.
+ """
+ c = task.Clock()
+ self.assertEqual(c.getDelayedCalls(), [])
+
+
+ def test_providesIReactorTime(self):
+ c = task.Clock()
+ self.failUnless(interfaces.IReactorTime.providedBy(c),
+ "Clock does not provide IReactorTime")
+
+
+ def test_callLaterKeepsCallsOrdered(self):
+ """
+ The order of calls scheduled by L{task.Clock.callLater} is honored when
+ adding a new call via calling L{task.Clock.callLater} again.
+
+ For example, if L{task.Clock.callLater} is invoked with a callable "A"
+ and a time t0, and then the L{IDelayedCall} which results from that is
+ C{reset} to a later time t2 which is greater than t0, and I{then}
+ L{task.Clock.callLater} is invoked again with a callable "B", and time
+ t1 which is less than t2 but greater than t0, "B" will be invoked before
+ "A".
+ """
+ result = []
+ expected = [('b', 2.0), ('a', 3.0)]
+ clock = task.Clock()
+ logtime = lambda n: result.append((n, clock.seconds()))
+
+ call_a = clock.callLater(1.0, logtime, "a")
+ call_a.reset(3.0)
+ clock.callLater(2.0, logtime, "b")
+
+ clock.pump([1]*3)
+ self.assertEqual(result, expected)
+
+
+ def test_callLaterResetKeepsCallsOrdered(self):
+ """
+ The order of calls scheduled by L{task.Clock.callLater} is honored when
+ re-scheduling an existing call via L{IDelayedCall.reset} on the result
+ of a previous call to C{callLater}.
+
+ For example, if L{task.Clock.callLater} is invoked with a callable "A"
+ and a time t0, and then L{task.Clock.callLater} is invoked again with a
+ callable "B", and time t1 greater than t0, and finally the
+ L{IDelayedCall} for "A" is C{reset} to a later time, t2, which is
+ greater than t1, "B" will be invoked before "A".
+ """
+ result = []
+ expected = [('b', 2.0), ('a', 3.0)]
+ clock = task.Clock()
+ logtime = lambda n: result.append((n, clock.seconds()))
+
+ call_a = clock.callLater(1.0, logtime, "a")
+ clock.callLater(2.0, logtime, "b")
+ call_a.reset(3.0)
+
+ clock.pump([1]*3)
+ self.assertEqual(result, expected)
+
+
+ def test_callLaterResetInsideCallKeepsCallsOrdered(self):
+ """
+ The order of calls scheduled by L{task.Clock.callLater} is honored when
+ re-scheduling an existing call via L{IDelayedCall.reset} on the result
+ of a previous call to C{callLater}, even when that call to C{reset}
+ occurs within the callable scheduled by C{callLater} itself.
+ """
+ result = []
+ expected = [('c', 3.0), ('b', 4.0)]
+ clock = task.Clock()
+ logtime = lambda n: result.append((n, clock.seconds()))
+
+ call_b = clock.callLater(2.0, logtime, "b")
+ def a():
+ call_b.reset(3.0)
+
+ clock.callLater(1.0, a)
+ clock.callLater(3.0, logtime, "c")
+
+ clock.pump([0.5] * 10)
+ self.assertEqual(result, expected)
+
+
+
+class LoopTestCase(unittest.TestCase):
+ """
+ Tests for L{task.LoopingCall} based on a fake L{IReactorTime}
+ implementation.
+ """
+ def test_defaultClock(self):
+ """
+ L{LoopingCall}'s default clock should be the reactor.
+ """
+ call = task.LoopingCall(lambda: None)
+ self.assertEqual(call.clock, reactor)
+
+
+ def test_callbackTimeSkips(self):
+ """
+ When more time than the defined interval passes during the execution
+ of a callback, L{LoopingCall} should schedule the next call for the
+ next interval which is still in the future.
+ """
+ times = []
+ callDuration = None
+ clock = task.Clock()
+ def aCallback():
+ times.append(clock.seconds())
+ clock.advance(callDuration)
+ call = task.LoopingCall(aCallback)
+ call.clock = clock
+
+ # Start a LoopingCall with a 0.5 second increment, and immediately call
+ # the callable.
+ callDuration = 2
+ call.start(0.5)
+
+ # Verify that the callable was called, and since it was immediate, with
+ # no skips.
+ self.assertEqual(times, [0])
+
+ # The callback should have advanced the clock by the callDuration.
+ self.assertEqual(clock.seconds(), callDuration)
+
+ # An iteration should have occurred at 2, but since 2 is the present
+ # and not the future, it is skipped.
+
+ clock.advance(0)
+ self.assertEqual(times, [0])
+
+ # 2.5 is in the future, and is not skipped.
+ callDuration = 1
+ clock.advance(0.5)
+ self.assertEqual(times, [0, 2.5])
+ self.assertEqual(clock.seconds(), 3.5)
+
+ # Another iteration should have occurred, but it is again the
+ # present and not the future, so it is skipped as well.
+ clock.advance(0)
+ self.assertEqual(times, [0, 2.5])
+
+ # 4 is in the future, and is not skipped.
+ callDuration = 0
+ clock.advance(0.5)
+ self.assertEqual(times, [0, 2.5, 4])
+ self.assertEqual(clock.seconds(), 4)
+
+
+ def test_reactorTimeSkips(self):
+ """
+ When more time than the defined interval passes between when
+ L{LoopingCall} schedules itself to run again and when it actually
+ runs again, it should schedule the next call for the next interval
+ which is still in the future.
+ """
+ times = []
+ clock = task.Clock()
+ def aCallback():
+ times.append(clock.seconds())
+
+ # Start a LoopingCall that tracks the time passed, with a 0.5 second
+ # increment.
+ call = task.LoopingCall(aCallback)
+ call.clock = clock
+ call.start(0.5)
+
+ # Initially, no time should have passed!
+ self.assertEqual(times, [0])
+
+ # Advance the clock by 2 seconds (2 seconds should have passed)
+ clock.advance(2)
+ self.assertEqual(times, [0, 2])
+
+ # Advance the clock by 1 second (3 total should have passed)
+ clock.advance(1)
+ self.assertEqual(times, [0, 2, 3])
+
+ # Advance the clock by 0 seconds (this should have no effect!)
+ clock.advance(0)
+ self.assertEqual(times, [0, 2, 3])
+
+
+ def test_reactorTimeCountSkips(self):
+ """
+ When L{LoopingCall} schedules itself to run again, if more than the
+ specified interval has passed, it should schedule the next call for the
+ next interval which is still in the future. If it was created
+ using L{LoopingCall.withCount}, a positional argument will be
+ inserted at the beginning of the argument list, indicating the number
+ of calls that should have been made.
+ """
+ times = []
+ clock = task.Clock()
+ def aCallback(numCalls):
+ times.append((clock.seconds(), numCalls))
+
+ # Start a LoopingCall that tracks the time passed, and the number of
+ # skips, with a 0.5 second increment.
+ call = task.LoopingCall.withCount(aCallback)
+ call.clock = clock
+ INTERVAL = 0.5
+ REALISTIC_DELAY = 0.01
+ call.start(INTERVAL)
+
+ # Initially, no seconds should have passed, and one calls should have
+ # been made.
+ self.assertEqual(times, [(0, 1)])
+
+ # After the interval (plus a small delay, to account for the time that
+ # the reactor takes to wake up and process the LoopingCall), we should
+ # still have only made one call.
+ clock.advance(INTERVAL + REALISTIC_DELAY)
+ self.assertEqual(times, [(0, 1), (INTERVAL + REALISTIC_DELAY, 1)])
+
+ # After advancing the clock by three intervals (plus a small delay to
+ # account for the reactor), we should have skipped two calls; one less
+ # than the number of intervals which have completely elapsed. Along
+ # with the call we did actually make, the final number of calls is 3.
+ clock.advance((3 * INTERVAL) + REALISTIC_DELAY)
+ self.assertEqual(times,
+ [(0, 1), (INTERVAL + REALISTIC_DELAY, 1),
+ ((4 * INTERVAL) + (2 * REALISTIC_DELAY), 3)])
+
+ # Advancing the clock by 0 seconds should not cause any changes!
+ clock.advance(0)
+ self.assertEqual(times,
+ [(0, 1), (INTERVAL + REALISTIC_DELAY, 1),
+ ((4 * INTERVAL) + (2 * REALISTIC_DELAY), 3)])
+
+
+ def test_countLengthyIntervalCounts(self):
+ """
+ L{LoopingCall.withCount} counts only calls that were expected to be
+ made. So, if more than one, but less than two intervals pass between
+ invocations, it won't increase the count above 1. For example, a
+ L{LoopingCall} with interval T expects to be invoked at T, 2T, 3T, etc.
+ However, the reactor takes some time to get around to calling it, so in
+ practice it will be called at T+something, 2T+something, 3T+something;
+ and due to other things going on in the reactor, "something" is
+ variable. It won't increase the count unless "something" is greater
+ than T. So if the L{LoopingCall} is invoked at T, 2.75T, and 3T,
+ the count has not increased, even though the distance between
+ invocation 1 and invocation 2 is 1.75T.
+ """
+ times = []
+ clock = task.Clock()
+ def aCallback(count):
+ times.append((clock.seconds(), count))
+
+ # Start a LoopingCall that tracks the time passed, and the number of
+ # calls, with a 0.5 second increment.
+ call = task.LoopingCall.withCount(aCallback)
+ call.clock = clock
+ INTERVAL = 0.5
+ REALISTIC_DELAY = 0.01
+ call.start(INTERVAL)
+ self.assertEqual(times.pop(), (0, 1))
+
+ # About one interval... So far, so good
+ clock.advance(INTERVAL + REALISTIC_DELAY)
+ self.assertEqual(times.pop(), (INTERVAL + REALISTIC_DELAY, 1))
+
+ # Oh no, something delayed us for a while.
+ clock.advance(INTERVAL * 1.75)
+ self.assertEqual(times.pop(), ((2.75 * INTERVAL) + REALISTIC_DELAY, 1))
+
+ # Back on track! We got invoked when we expected this time.
+ clock.advance(INTERVAL * 0.25)
+ self.assertEqual(times.pop(), ((3.0 * INTERVAL) + REALISTIC_DELAY, 1))
+
+
+ def testBasicFunction(self):
+ # Arrange to have time advanced enough so that our function is
+ # called a few times.
+ # Only need to go to 2.5 to get 3 calls, since the first call
+ # happens before any time has elapsed.
+ timings = [0.05, 0.1, 0.1]
+
+ clock = task.Clock()
+
+ L = []
+ def foo(a, b, c=None, d=None):
+ L.append((a, b, c, d))
+
+ lc = TestableLoopingCall(clock, foo, "a", "b", d="d")
+ D = lc.start(0.1)
+
+ theResult = []
+ def saveResult(result):
+ theResult.append(result)
+ D.addCallback(saveResult)
+
+ clock.pump(timings)
+
+ self.assertEqual(len(L), 3,
+ "got %d iterations, not 3" % (len(L),))
+
+ for (a, b, c, d) in L:
+ self.assertEqual(a, "a")
+ self.assertEqual(b, "b")
+ self.assertEqual(c, None)
+ self.assertEqual(d, "d")
+
+ lc.stop()
+ self.assertIdentical(theResult[0], lc)
+
+ # Make sure it isn't planning to do anything further.
+ self.failIf(clock.calls)
+
+
+ def testDelayedStart(self):
+ timings = [0.05, 0.1, 0.1]
+
+ clock = task.Clock()
+
+ L = []
+ lc = TestableLoopingCall(clock, L.append, None)
+ d = lc.start(0.1, now=False)
+
+ theResult = []
+ def saveResult(result):
+ theResult.append(result)
+ d.addCallback(saveResult)
+
+ clock.pump(timings)
+
+ self.assertEqual(len(L), 2,
+ "got %d iterations, not 2" % (len(L),))
+ lc.stop()
+ self.assertIdentical(theResult[0], lc)
+
+ self.failIf(clock.calls)
+
+
+ def testBadDelay(self):
+ lc = task.LoopingCall(lambda: None)
+ self.assertRaises(ValueError, lc.start, -1)
+
+
+ # Make sure that LoopingCall.stop() prevents any subsequent calls.
+ def _stoppingTest(self, delay):
+ ran = []
+ def foo():
+ ran.append(None)
+
+ clock = task.Clock()
+ lc = TestableLoopingCall(clock, foo)
+ lc.start(delay, now=False)
+ lc.stop()
+ self.failIf(ran)
+ self.failIf(clock.calls)
+
+
+ def testStopAtOnce(self):
+ return self._stoppingTest(0)
+
+
+ def testStoppingBeforeDelayedStart(self):
+ return self._stoppingTest(10)
+
+
+ def test_reset(self):
+ """
+ Test that L{LoopingCall} can be reset.
+ """
+ ran = []
+ def foo():
+ ran.append(None)
+
+ c = task.Clock()
+ lc = TestableLoopingCall(c, foo)
+ lc.start(2, now=False)
+ c.advance(1)
+ lc.reset()
+ c.advance(1)
+ self.assertEqual(ran, [])
+ c.advance(1)
+ self.assertEqual(ran, [None])
+
+
+
+class ReactorLoopTestCase(unittest.TestCase):
+ # Slightly inferior tests which exercise interactions with an actual
+ # reactor.
+ def testFailure(self):
+ def foo(x):
+ raise TestException(x)
+
+ lc = task.LoopingCall(foo, "bar")
+ return self.assertFailure(lc.start(0.1), TestException)
+
+
+ def testFailAndStop(self):
+ def foo(x):
+ lc.stop()
+ raise TestException(x)
+
+ lc = task.LoopingCall(foo, "bar")
+ return self.assertFailure(lc.start(0.1), TestException)
+
+
+ def testEveryIteration(self):
+ ran = []
+
+ def foo():
+ ran.append(None)
+ if len(ran) > 5:
+ lc.stop()
+
+ lc = task.LoopingCall(foo)
+ d = lc.start(0)
+ def stopped(ign):
+ self.assertEqual(len(ran), 6)
+ return d.addCallback(stopped)
+
+
+ def testStopAtOnceLater(self):
+ # Ensure that even when LoopingCall.stop() is called from a
+ # reactor callback, it still prevents any subsequent calls.
+ d = defer.Deferred()
+ def foo():
+ d.errback(failure.DefaultException(
+ "This task also should never get called."))
+ self._lc = task.LoopingCall(foo)
+ self._lc.start(1, now=False)
+ reactor.callLater(0, self._callback_for_testStopAtOnceLater, d)
+ return d
+
+
+ def _callback_for_testStopAtOnceLater(self, d):
+ self._lc.stop()
+ reactor.callLater(0, d.callback, "success")
+
+ def testWaitDeferred(self):
+ # Tests if the callable isn't scheduled again before the returned
+ # deferred has fired.
+ timings = [0.2, 0.8]
+ clock = task.Clock()
+
+ def foo():
+ d = defer.Deferred()
+ d.addCallback(lambda _: lc.stop())
+ clock.callLater(1, d.callback, None)
+ return d
+
+ lc = TestableLoopingCall(clock, foo)
+ lc.start(0.2)
+ clock.pump(timings)
+ self.failIf(clock.calls)
+
+ def testFailurePropagation(self):
+ # Tests if the failure of the errback of the deferred returned by the
+ # callable is propagated to the lc errback.
+ #
+ # To make sure this test does not hang trial when LoopingCall does not
+ # wait for the callable's deferred, it also checks there are no
+ # calls in the clock's callLater queue.
+ timings = [0.3]
+ clock = task.Clock()
+
+ def foo():
+ d = defer.Deferred()
+ clock.callLater(0.3, d.errback, TestException())
+ return d
+
+ lc = TestableLoopingCall(clock, foo)
+ d = lc.start(1)
+ self.assertFailure(d, TestException)
+
+ clock.pump(timings)
+ self.failIf(clock.calls)
+ return d
+
+
+ def test_deferredWithCount(self):
+ """
+ In the case that the function passed to L{LoopingCall.withCount}
+ returns a deferred, which does not fire before the next interval
+ elapses, the function should not be run again. And if a function call
+ is skipped in this fashion, the appropriate count should be
+ provided.
+ """
+ testClock = task.Clock()
+ d = defer.Deferred()
+ deferredCounts = []
+
+ def countTracker(possibleCount):
+ # Keep a list of call counts
+ deferredCounts.append(possibleCount)
+ # Return a deferred, but only on the first request
+ if len(deferredCounts) == 1:
+ return d
+ else:
+ return None
+
+ # Start a looping call for our countTracker function
+ # Set the increment to 0.2, and do not call the function on startup.
+ lc = task.LoopingCall.withCount(countTracker)
+ lc.clock = testClock
+ d = lc.start(0.2, now=False)
+
+ # Confirm that nothing has happened yet.
+ self.assertEqual(deferredCounts, [])
+
+ # Advance the clock by 0.2 and then 0.4;
+ testClock.pump([0.2, 0.4])
+ # We should now have exactly one count (of 1 call)
+ self.assertEqual(len(deferredCounts), 1)
+
+ # Fire the deferred, and advance the clock by another 0.2
+ d.callback(None)
+ testClock.pump([0.2])
+ # We should now have exactly 2 counts...
+ self.assertEqual(len(deferredCounts), 2)
+ # The first count should be 1 (one call)
+ # The second count should be 3 (calls were missed at about 0.6 and 0.8)
+ self.assertEqual(deferredCounts, [1, 3])
+
+
+
+class DeferLaterTests(unittest.TestCase):
+ """
+ Tests for L{task.deferLater}.
+ """
+ def test_callback(self):
+ """
+ The L{Deferred} returned by L{task.deferLater} is called back after
+ the specified delay with the result of the function passed in.
+ """
+ results = []
+ flag = object()
+ def callable(foo, bar):
+ results.append((foo, bar))
+ return flag
+
+ clock = task.Clock()
+ d = task.deferLater(clock, 3, callable, 'foo', bar='bar')
+ d.addCallback(self.assertIdentical, flag)
+ clock.advance(2)
+ self.assertEqual(results, [])
+ clock.advance(1)
+ self.assertEqual(results, [('foo', 'bar')])
+ return d
+
+
+ def test_errback(self):
+ """
+ The L{Deferred} returned by L{task.deferLater} is errbacked if the
+ supplied function raises an exception.
+ """
+ def callable():
+ raise TestException()
+
+ clock = task.Clock()
+ d = task.deferLater(clock, 1, callable)
+ clock.advance(1)
+ return self.assertFailure(d, TestException)
+
+
+ def test_cancel(self):
+ """
+ The L{Deferred} returned by L{task.deferLater} can be
+ cancelled to prevent the call from actually being performed.
+ """
+ called = []
+ clock = task.Clock()
+ d = task.deferLater(clock, 1, called.append, None)
+ d.cancel()
+ def cbCancelled(ignored):
+ # Make sure there are no calls outstanding.
+ self.assertEqual([], clock.getDelayedCalls())
+ # And make sure the call didn't somehow happen already.
+ self.assertFalse(called)
+ self.assertFailure(d, defer.CancelledError)
+ d.addCallback(cbCancelled)
+ return d
diff --git a/twisted/test/test_tcp.py b/twisted/test/test_tcp.py
new file mode 100644
index 0000000..aac8888
--- /dev/null
+++ b/twisted/test/test_tcp.py
@@ -0,0 +1,1820 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorTCP}.
+"""
+
+import socket, random, errno
+
+from zope.interface import implements
+
+from twisted.trial import unittest
+
+from twisted.python.log import msg
+from twisted.internet import protocol, reactor, defer, interfaces
+from twisted.internet import error
+from twisted.internet.address import IPv4Address
+from twisted.internet.interfaces import IHalfCloseableProtocol, IPullProducer
+from twisted.protocols import policies
+from twisted.test.proto_helpers import AccumulatingProtocol
+
+
+def loopUntil(predicate, interval=0):
+ """
+ Poor excuse for an event notification helper. This polls a condition and
+ calls back a Deferred when it is seen to be true.
+
+ Do not use this function.
+ """
+ from twisted.internet import task
+ d = defer.Deferred()
+ def check():
+ res = predicate()
+ if res:
+ d.callback(res)
+ call = task.LoopingCall(check)
+ def stop(result):
+ call.stop()
+ return result
+ d.addCallback(stop)
+ d2 = call.start(interval)
+ d2.addErrback(d.errback)
+ return d
+
+
+
+class ClosingProtocol(protocol.Protocol):
+
+ def connectionMade(self):
+ msg("ClosingProtocol.connectionMade")
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ msg("ClosingProtocol.connectionLost")
+ reason.trap(error.ConnectionDone)
+
+
+
+class ClosingFactory(protocol.ServerFactory):
+ """
+ Factory that closes port immediately.
+ """
+
+ _cleanerUpper = None
+
+ def buildProtocol(self, conn):
+ self._cleanerUpper = self.port.stopListening()
+ return ClosingProtocol()
+
+
+ def cleanUp(self):
+ """
+ Clean-up for tests to wait for the port to stop listening.
+ """
+ if self._cleanerUpper is None:
+ return self.port.stopListening()
+ return self._cleanerUpper
+
+
+
+class MyProtocolFactoryMixin(object):
+ """
+ Mixin for factories which create L{AccumulatingProtocol} instances.
+
+ @type protocolFactory: no-argument callable
+ @ivar protocolFactory: Factory for protocols - takes the place of the
+ typical C{protocol} attribute of factories (but that name is used by
+ this class for something else).
+
+ @type protocolConnectionMade: L{NoneType} or L{defer.Deferred}
+ @ivar protocolConnectionMade: When an instance of L{AccumulatingProtocol}
+ is connected, if this is not C{None}, the L{Deferred} will be called
+ back with the protocol instance and the attribute set to C{None}.
+
+ @type protocolConnectionLost: L{NoneType} or L{defer.Deferred}
+ @ivar protocolConnectionLost: When an instance of L{AccumulatingProtocol}
+ is created, this will be set as its C{closedDeferred} attribute and
+ then this attribute will be set to C{None} so the L{defer.Deferred} is
+ not used by more than one protocol.
+
+ @ivar protocol: The most recently created L{AccumulatingProtocol} instance
+ which was returned from C{buildProtocol}.
+
+ @type called: C{int}
+ @ivar called: A counter which is incremented each time C{buildProtocol}
+ is called.
+
+ @ivar peerAddresses: A C{list} of the addresses passed to C{buildProtocol}.
+ """
+ protocolFactory = AccumulatingProtocol
+
+ protocolConnectionMade = None
+ protocolConnectionLost = None
+ protocol = None
+ called = 0
+
+ def __init__(self):
+ self.peerAddresses = []
+
+
+ def buildProtocol(self, addr):
+ """
+ Create a L{AccumulatingProtocol} and set it up to be able to perform
+ callbacks.
+ """
+ self.peerAddresses.append(addr)
+ self.called += 1
+ p = self.protocolFactory()
+ p.factory = self
+ p.closedDeferred = self.protocolConnectionLost
+ self.protocolConnectionLost = None
+ self.protocol = p
+ return p
+
+
+
+class MyServerFactory(MyProtocolFactoryMixin, protocol.ServerFactory):
+ """
+ Server factory which creates L{AccumulatingProtocol} instances.
+ """
+
+
+
+class MyClientFactory(MyProtocolFactoryMixin, protocol.ClientFactory):
+ """
+ Client factory which creates L{AccumulatingProtocol} instances.
+ """
+ failed = 0
+ stopped = 0
+
+ def __init__(self):
+ MyProtocolFactoryMixin.__init__(self)
+ self.deferred = defer.Deferred()
+ self.failDeferred = defer.Deferred()
+
+ def clientConnectionFailed(self, connector, reason):
+ self.failed = 1
+ self.reason = reason
+ self.failDeferred.callback(None)
+
+ def clientConnectionLost(self, connector, reason):
+ self.lostReason = reason
+ self.deferred.callback(None)
+
+ def stopFactory(self):
+ self.stopped = 1
+
+
+
+class ListeningTestCase(unittest.TestCase):
+
+ def test_listen(self):
+ """
+ L{IReactorTCP.listenTCP} returns an object which provides
+ L{IListeningPort}.
+ """
+ f = MyServerFactory()
+ p1 = reactor.listenTCP(0, f, interface="127.0.0.1")
+ self.addCleanup(p1.stopListening)
+ self.failUnless(interfaces.IListeningPort.providedBy(p1))
+
+
+ def testStopListening(self):
+ """
+ The L{IListeningPort} returned by L{IReactorTCP.listenTCP} can be
+ stopped with its C{stopListening} method. After the L{Deferred} it
+ (optionally) returns has been called back, the port number can be bound
+ to a new server.
+ """
+ f = MyServerFactory()
+ port = reactor.listenTCP(0, f, interface="127.0.0.1")
+ n = port.getHost().port
+
+ def cbStopListening(ignored):
+ # Make sure we can rebind the port right away
+ port = reactor.listenTCP(n, f, interface="127.0.0.1")
+ return port.stopListening()
+
+ d = defer.maybeDeferred(port.stopListening)
+ d.addCallback(cbStopListening)
+ return d
+
+
+ def testNumberedInterface(self):
+ f = MyServerFactory()
+ # listen only on the loopback interface
+ p1 = reactor.listenTCP(0, f, interface='127.0.0.1')
+ return p1.stopListening()
+
+ def testPortRepr(self):
+ f = MyServerFactory()
+ p = reactor.listenTCP(0, f)
+ portNo = str(p.getHost().port)
+ self.failIf(repr(p).find(portNo) == -1)
+ def stoppedListening(ign):
+ self.failIf(repr(p).find(portNo) != -1)
+ d = defer.maybeDeferred(p.stopListening)
+ return d.addCallback(stoppedListening)
+
+
+ def test_serverRepr(self):
+ """
+ Check that the repr string of the server transport get the good port
+ number if the server listens on 0.
+ """
+ server = MyServerFactory()
+ serverConnMade = server.protocolConnectionMade = defer.Deferred()
+ port = reactor.listenTCP(0, server)
+ self.addCleanup(port.stopListening)
+
+ client = MyClientFactory()
+ clientConnMade = client.protocolConnectionMade = defer.Deferred()
+ connector = reactor.connectTCP("127.0.0.1",
+ port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+ def check((serverProto, clientProto)):
+ portNumber = port.getHost().port
+ self.assertEqual(
+ repr(serverProto.transport),
+ "<AccumulatingProtocol #0 on %s>" % (portNumber,))
+ serverProto.transport.loseConnection()
+ clientProto.transport.loseConnection()
+ return defer.gatherResults([serverConnMade, clientConnMade]
+ ).addCallback(check)
+
+
+ def test_restartListening(self):
+ """
+ Stop and then try to restart a L{tcp.Port}: after a restart, the
+ server should be able to handle client connections.
+ """
+ serverFactory = MyServerFactory()
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+
+ def cbStopListening(ignored):
+ port.startListening()
+
+ client = MyClientFactory()
+ serverFactory.protocolConnectionMade = defer.Deferred()
+ client.protocolConnectionMade = defer.Deferred()
+ connector = reactor.connectTCP("127.0.0.1",
+ port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+ return defer.gatherResults([serverFactory.protocolConnectionMade,
+ client.protocolConnectionMade]
+ ).addCallback(close)
+
+ def close((serverProto, clientProto)):
+ clientProto.transport.loseConnection()
+ serverProto.transport.loseConnection()
+
+ d = defer.maybeDeferred(port.stopListening)
+ d.addCallback(cbStopListening)
+ return d
+
+
+ def test_exceptInStop(self):
+ """
+ If the server factory raises an exception in C{stopFactory}, the
+ deferred returned by L{tcp.Port.stopListening} should fail with the
+ corresponding error.
+ """
+ serverFactory = MyServerFactory()
+ def raiseException():
+ raise RuntimeError("An error")
+ serverFactory.stopFactory = raiseException
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+
+ return self.assertFailure(port.stopListening(), RuntimeError)
+
+
+ def test_restartAfterExcept(self):
+ """
+ Even if the server factory raise an exception in C{stopFactory}, the
+ corresponding C{tcp.Port} instance should be in a sane state and can
+ be restarted.
+ """
+ serverFactory = MyServerFactory()
+ def raiseException():
+ raise RuntimeError("An error")
+ serverFactory.stopFactory = raiseException
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+
+ def cbStopListening(ignored):
+ del serverFactory.stopFactory
+ port.startListening()
+
+ client = MyClientFactory()
+ serverFactory.protocolConnectionMade = defer.Deferred()
+ client.protocolConnectionMade = defer.Deferred()
+ connector = reactor.connectTCP("127.0.0.1",
+ port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+ return defer.gatherResults([serverFactory.protocolConnectionMade,
+ client.protocolConnectionMade]
+ ).addCallback(close)
+
+ def close((serverProto, clientProto)):
+ clientProto.transport.loseConnection()
+ serverProto.transport.loseConnection()
+
+ return self.assertFailure(port.stopListening(), RuntimeError
+ ).addCallback(cbStopListening)
+
+
+ def test_directConnectionLostCall(self):
+ """
+ If C{connectionLost} is called directly on a port object, it succeeds
+ (and doesn't expect the presence of a C{deferred} attribute).
+
+ C{connectionLost} is called by L{reactor.disconnectAll} at shutdown.
+ """
+ serverFactory = MyServerFactory()
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ portNumber = port.getHost().port
+ port.connectionLost(None)
+
+ client = MyClientFactory()
+ serverFactory.protocolConnectionMade = defer.Deferred()
+ client.protocolConnectionMade = defer.Deferred()
+ reactor.connectTCP("127.0.0.1", portNumber, client)
+ def check(ign):
+ client.reason.trap(error.ConnectionRefusedError)
+ return client.failDeferred.addCallback(check)
+
+
+ def test_exceptInConnectionLostCall(self):
+ """
+ If C{connectionLost} is called directory on a port object and that the
+ server factory raises an exception in C{stopFactory}, the exception is
+ passed through to the caller.
+
+ C{connectionLost} is called by L{reactor.disconnectAll} at shutdown.
+ """
+ serverFactory = MyServerFactory()
+ def raiseException():
+ raise RuntimeError("An error")
+ serverFactory.stopFactory = raiseException
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ self.assertRaises(RuntimeError, port.connectionLost, None)
+
+
+
+def callWithSpew(f):
+ from twisted.python.util import spewerWithLinenums as spewer
+ import sys
+ sys.settrace(spewer)
+ try:
+ f()
+ finally:
+ sys.settrace(None)
+
+class LoopbackTestCase(unittest.TestCase):
+ """
+ Test loopback connections.
+ """
+ def test_closePortInProtocolFactory(self):
+ """
+ A port created with L{IReactorTCP.listenTCP} can be connected to with
+ L{IReactorTCP.connectTCP}.
+ """
+ f = ClosingFactory()
+ port = reactor.listenTCP(0, f, interface="127.0.0.1")
+ f.port = port
+ self.addCleanup(f.cleanUp)
+ portNumber = port.getHost().port
+ clientF = MyClientFactory()
+ reactor.connectTCP("127.0.0.1", portNumber, clientF)
+ def check(x):
+ self.assertTrue(clientF.protocol.made)
+ self.assertTrue(port.disconnected)
+ clientF.lostReason.trap(error.ConnectionDone)
+ return clientF.deferred.addCallback(check)
+
+ def _trapCnxDone(self, obj):
+ getattr(obj, 'trap', lambda x: None)(error.ConnectionDone)
+
+
+ def _connectedClientAndServerTest(self, callback):
+ """
+ Invoke the given callback with a client protocol and a server protocol
+ which have been connected to each other.
+ """
+ serverFactory = MyServerFactory()
+ serverConnMade = defer.Deferred()
+ serverFactory.protocolConnectionMade = serverConnMade
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+
+ portNumber = port.getHost().port
+ clientF = MyClientFactory()
+ clientConnMade = defer.Deferred()
+ clientF.protocolConnectionMade = clientConnMade
+ reactor.connectTCP("127.0.0.1", portNumber, clientF)
+
+ connsMade = defer.gatherResults([serverConnMade, clientConnMade])
+ def connected((serverProtocol, clientProtocol)):
+ callback(serverProtocol, clientProtocol)
+ serverProtocol.transport.loseConnection()
+ clientProtocol.transport.loseConnection()
+ connsMade.addCallback(connected)
+ return connsMade
+
+
+ def test_tcpNoDelay(self):
+ """
+ The transport of a protocol connected with L{IReactorTCP.connectTCP} or
+ L{IReactor.TCP.listenTCP} can have its I{TCP_NODELAY} state inspected
+ and manipulated with L{ITCPTransport.getTcpNoDelay} and
+ L{ITCPTransport.setTcpNoDelay}.
+ """
+ def check(serverProtocol, clientProtocol):
+ for p in [serverProtocol, clientProtocol]:
+ transport = p.transport
+ self.assertEqual(transport.getTcpNoDelay(), 0)
+ transport.setTcpNoDelay(1)
+ self.assertEqual(transport.getTcpNoDelay(), 1)
+ transport.setTcpNoDelay(0)
+ self.assertEqual(transport.getTcpNoDelay(), 0)
+ return self._connectedClientAndServerTest(check)
+
+
+ def test_tcpKeepAlive(self):
+ """
+ The transport of a protocol connected with L{IReactorTCP.connectTCP} or
+ L{IReactor.TCP.listenTCP} can have its I{SO_KEEPALIVE} state inspected
+ and manipulated with L{ITCPTransport.getTcpKeepAlive} and
+ L{ITCPTransport.setTcpKeepAlive}.
+ """
+ def check(serverProtocol, clientProtocol):
+ for p in [serverProtocol, clientProtocol]:
+ transport = p.transport
+ self.assertEqual(transport.getTcpKeepAlive(), 0)
+ transport.setTcpKeepAlive(1)
+ self.assertEqual(transport.getTcpKeepAlive(), 1)
+ transport.setTcpKeepAlive(0)
+ self.assertEqual(transport.getTcpKeepAlive(), 0)
+ return self._connectedClientAndServerTest(check)
+
+
+ def testFailing(self):
+ clientF = MyClientFactory()
+ # XXX we assume no one is listening on TCP port 69
+ reactor.connectTCP("127.0.0.1", 69, clientF, timeout=5)
+ def check(ignored):
+ clientF.reason.trap(error.ConnectionRefusedError)
+ return clientF.failDeferred.addCallback(check)
+
+
+ def test_connectionRefusedErrorNumber(self):
+ """
+ Assert that the error number of the ConnectionRefusedError is
+ ECONNREFUSED, and not some other socket related error.
+ """
+
+ # Bind a number of ports in the operating system. We will attempt
+ # to connect to these in turn immediately after closing them, in the
+ # hopes that no one else has bound them in the mean time. Any
+ # connection which succeeds is ignored and causes us to move on to
+ # the next port. As soon as a connection attempt fails, we move on
+ # to making an assertion about how it failed. If they all succeed,
+ # the test will fail.
+
+ # It would be nice to have a simpler, reliable way to cause a
+ # connection failure from the platform.
+ #
+ # On Linux (2.6.15), connecting to port 0 always fails. FreeBSD
+ # (5.4) rejects the connection attempt with EADDRNOTAVAIL.
+ #
+ # On FreeBSD (5.4), listening on a port and then repeatedly
+ # connecting to it without ever accepting any connections eventually
+ # leads to an ECONNREFUSED. On Linux (2.6.15), a seemingly
+ # unbounded number of connections succeed.
+
+ serverSockets = []
+ for i in xrange(10):
+ serverSocket = socket.socket()
+ serverSocket.bind(('127.0.0.1', 0))
+ serverSocket.listen(1)
+ serverSockets.append(serverSocket)
+ random.shuffle(serverSockets)
+
+ clientCreator = protocol.ClientCreator(reactor, protocol.Protocol)
+
+ def tryConnectFailure():
+ def connected(proto):
+ """
+ Darn. Kill it and try again, if there are any tries left.
+ """
+ proto.transport.loseConnection()
+ if serverSockets:
+ return tryConnectFailure()
+ self.fail("Could not fail to connect - could not test errno for that case.")
+
+ serverSocket = serverSockets.pop()
+ serverHost, serverPort = serverSocket.getsockname()
+ serverSocket.close()
+
+ connectDeferred = clientCreator.connectTCP(serverHost, serverPort)
+ connectDeferred.addCallback(connected)
+ return connectDeferred
+
+ refusedDeferred = tryConnectFailure()
+ self.assertFailure(refusedDeferred, error.ConnectionRefusedError)
+ def connRefused(exc):
+ self.assertEqual(exc.osError, errno.ECONNREFUSED)
+ refusedDeferred.addCallback(connRefused)
+ def cleanup(passthrough):
+ while serverSockets:
+ serverSockets.pop().close()
+ return passthrough
+ refusedDeferred.addBoth(cleanup)
+ return refusedDeferred
+
+
+ def test_connectByServiceFail(self):
+ """
+ Connecting to a named service which does not exist raises
+ L{error.ServiceNameUnknownError}.
+ """
+ self.assertRaises(
+ error.ServiceNameUnknownError,
+ reactor.connectTCP,
+ "127.0.0.1", "thisbetternotexist", MyClientFactory())
+
+
+ def test_connectByService(self):
+ """
+ L{IReactorTCP.connectTCP} accepts the name of a service instead of a
+ port number and connects to the port number associated with that
+ service, as defined by L{socket.getservbyname}.
+ """
+ serverFactory = MyServerFactory()
+ serverConnMade = defer.Deferred()
+ serverFactory.protocolConnectionMade = serverConnMade
+ port = reactor.listenTCP(0, serverFactory, interface="127.0.0.1")
+ self.addCleanup(port.stopListening)
+ portNumber = port.getHost().port
+ clientFactory = MyClientFactory()
+ clientConnMade = defer.Deferred()
+ clientFactory.protocolConnectionMade = clientConnMade
+
+ def fakeGetServicePortByName(serviceName, protocolName):
+ if serviceName == 'http' and protocolName == 'tcp':
+ return portNumber
+ return 10
+ self.patch(socket, 'getservbyname', fakeGetServicePortByName)
+
+ reactor.connectTCP('127.0.0.1', 'http', clientFactory)
+
+ connMade = defer.gatherResults([serverConnMade, clientConnMade])
+ def connected((serverProtocol, clientProtocol)):
+ self.assertTrue(
+ serverFactory.called,
+ "Server factory was not called upon to build a protocol.")
+ serverProtocol.transport.loseConnection()
+ clientProtocol.transport.loseConnection()
+ connMade.addCallback(connected)
+ return connMade
+
+
+class StartStopFactory(protocol.Factory):
+
+ started = 0
+ stopped = 0
+
+ def startFactory(self):
+ if self.started or self.stopped:
+ raise RuntimeError
+ self.started = 1
+
+ def stopFactory(self):
+ if not self.started or self.stopped:
+ raise RuntimeError
+ self.stopped = 1
+
+
+class ClientStartStopFactory(MyClientFactory):
+
+ started = 0
+ stopped = 0
+
+ def __init__(self, *a, **kw):
+ MyClientFactory.__init__(self, *a, **kw)
+ self.whenStopped = defer.Deferred()
+
+ def startFactory(self):
+ if self.started or self.stopped:
+ raise RuntimeError
+ self.started = 1
+
+ def stopFactory(self):
+ if not self.started or self.stopped:
+ raise RuntimeError
+ self.stopped = 1
+ self.whenStopped.callback(True)
+
+
+class FactoryTestCase(unittest.TestCase):
+ """Tests for factories."""
+
+ def test_serverStartStop(self):
+ """
+ The factory passed to L{IReactorTCP.listenTCP} should be started only
+ when it transitions from being used on no ports to being used on one
+ port and should be stopped only when it transitions from being used on
+ one port to being used on no ports.
+ """
+ # Note - this test doesn't need to use listenTCP. It is exercising
+ # logic implemented in Factory.doStart and Factory.doStop, so it could
+ # just call that directly. Some other test can make sure that
+ # listenTCP and stopListening correctly call doStart and
+ # doStop. -exarkun
+
+ f = StartStopFactory()
+
+ # listen on port
+ p1 = reactor.listenTCP(0, f, interface='127.0.0.1')
+ self.addCleanup(p1.stopListening)
+
+ self.assertEqual((f.started, f.stopped), (1, 0))
+
+ # listen on two more ports
+ p2 = reactor.listenTCP(0, f, interface='127.0.0.1')
+ p3 = reactor.listenTCP(0, f, interface='127.0.0.1')
+
+ self.assertEqual((f.started, f.stopped), (1, 0))
+
+ # close two ports
+ d1 = defer.maybeDeferred(p1.stopListening)
+ d2 = defer.maybeDeferred(p2.stopListening)
+ closedDeferred = defer.gatherResults([d1, d2])
+ def cbClosed(ignored):
+ self.assertEqual((f.started, f.stopped), (1, 0))
+ # Close the last port
+ return p3.stopListening()
+ closedDeferred.addCallback(cbClosed)
+
+ def cbClosedAll(ignored):
+ self.assertEqual((f.started, f.stopped), (1, 1))
+ closedDeferred.addCallback(cbClosedAll)
+ return closedDeferred
+
+
+ def test_clientStartStop(self):
+ """
+ The factory passed to L{IReactorTCP.connectTCP} should be started when
+ the connection attempt starts and stopped when it is over.
+ """
+ f = ClosingFactory()
+ p = reactor.listenTCP(0, f, interface="127.0.0.1")
+ f.port = p
+ self.addCleanup(f.cleanUp)
+ portNumber = p.getHost().port
+
+ factory = ClientStartStopFactory()
+ reactor.connectTCP("127.0.0.1", portNumber, factory)
+ self.assertTrue(factory.started)
+ return loopUntil(lambda: factory.stopped)
+
+
+
+class CannotBindTestCase(unittest.TestCase):
+ """
+ Tests for correct behavior when a reactor cannot bind to the required TCP
+ port.
+ """
+
+ def test_cannotBind(self):
+ """
+ L{IReactorTCP.listenTCP} raises L{error.CannotListenError} if the
+ address to listen on is already in use.
+ """
+ f = MyServerFactory()
+
+ p1 = reactor.listenTCP(0, f, interface='127.0.0.1')
+ self.addCleanup(p1.stopListening)
+ n = p1.getHost().port
+ dest = p1.getHost()
+ self.assertEqual(dest.type, "TCP")
+ self.assertEqual(dest.host, "127.0.0.1")
+ self.assertEqual(dest.port, n)
+
+ # make sure new listen raises error
+ self.assertRaises(error.CannotListenError,
+ reactor.listenTCP, n, f, interface='127.0.0.1')
+
+
+
+ def _fireWhenDoneFunc(self, d, f):
+ """Returns closure that when called calls f and then callbacks d.
+ """
+ from twisted.python import util as tputil
+ def newf(*args, **kw):
+ rtn = f(*args, **kw)
+ d.callback('')
+ return rtn
+ return tputil.mergeFunctionMetadata(f, newf)
+
+
+ def test_clientBind(self):
+ """
+ L{IReactorTCP.connectTCP} calls C{Factory.clientConnectionFailed} with
+ L{error.ConnectBindError} if the bind address specified is already in
+ use.
+ """
+ theDeferred = defer.Deferred()
+ sf = MyServerFactory()
+ sf.startFactory = self._fireWhenDoneFunc(theDeferred, sf.startFactory)
+ p = reactor.listenTCP(0, sf, interface="127.0.0.1")
+ self.addCleanup(p.stopListening)
+
+ def _connect1(results):
+ d = defer.Deferred()
+ cf1 = MyClientFactory()
+ cf1.buildProtocol = self._fireWhenDoneFunc(d, cf1.buildProtocol)
+ reactor.connectTCP("127.0.0.1", p.getHost().port, cf1,
+ bindAddress=("127.0.0.1", 0))
+ d.addCallback(_conmade, cf1)
+ return d
+
+ def _conmade(results, cf1):
+ d = defer.Deferred()
+ cf1.protocol.connectionMade = self._fireWhenDoneFunc(
+ d, cf1.protocol.connectionMade)
+ d.addCallback(_check1connect2, cf1)
+ return d
+
+ def _check1connect2(results, cf1):
+ self.assertEqual(cf1.protocol.made, 1)
+
+ d1 = defer.Deferred()
+ d2 = defer.Deferred()
+ port = cf1.protocol.transport.getHost().port
+ cf2 = MyClientFactory()
+ cf2.clientConnectionFailed = self._fireWhenDoneFunc(
+ d1, cf2.clientConnectionFailed)
+ cf2.stopFactory = self._fireWhenDoneFunc(d2, cf2.stopFactory)
+ reactor.connectTCP("127.0.0.1", p.getHost().port, cf2,
+ bindAddress=("127.0.0.1", port))
+ d1.addCallback(_check2failed, cf1, cf2)
+ d2.addCallback(_check2stopped, cf1, cf2)
+ dl = defer.DeferredList([d1, d2])
+ dl.addCallback(_stop, cf1, cf2)
+ return dl
+
+ def _check2failed(results, cf1, cf2):
+ self.assertEqual(cf2.failed, 1)
+ cf2.reason.trap(error.ConnectBindError)
+ self.assertTrue(cf2.reason.check(error.ConnectBindError))
+ return results
+
+ def _check2stopped(results, cf1, cf2):
+ self.assertEqual(cf2.stopped, 1)
+ return results
+
+ def _stop(results, cf1, cf2):
+ d = defer.Deferred()
+ d.addCallback(_check1cleanup, cf1)
+ cf1.stopFactory = self._fireWhenDoneFunc(d, cf1.stopFactory)
+ cf1.protocol.transport.loseConnection()
+ return d
+
+ def _check1cleanup(results, cf1):
+ self.assertEqual(cf1.stopped, 1)
+
+ theDeferred.addCallback(_connect1)
+ return theDeferred
+
+
+
+class MyOtherClientFactory(protocol.ClientFactory):
+ def buildProtocol(self, address):
+ self.address = address
+ self.protocol = AccumulatingProtocol()
+ return self.protocol
+
+
+
+class LocalRemoteAddressTestCase(unittest.TestCase):
+ """
+ Tests for correct getHost/getPeer values and that the correct address is
+ passed to buildProtocol.
+ """
+ def test_hostAddress(self):
+ """
+ L{IListeningPort.getHost} returns the same address as a client
+ connection's L{ITCPTransport.getPeer}.
+ """
+ serverFactory = MyServerFactory()
+ serverFactory.protocolConnectionLost = defer.Deferred()
+ serverConnectionLost = serverFactory.protocolConnectionLost
+ port = reactor.listenTCP(0, serverFactory, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+ n = port.getHost().port
+
+ clientFactory = MyClientFactory()
+ onConnection = clientFactory.protocolConnectionMade = defer.Deferred()
+ connector = reactor.connectTCP('127.0.0.1', n, clientFactory)
+
+ def check(ignored):
+ self.assertEqual([port.getHost()], clientFactory.peerAddresses)
+ self.assertEqual(
+ port.getHost(), clientFactory.protocol.transport.getPeer())
+ onConnection.addCallback(check)
+
+ def cleanup(ignored):
+ # Clean up the client explicitly here so that tear down of
+ # the server side of the connection begins, then wait for
+ # the server side to actually disconnect.
+ connector.disconnect()
+ return serverConnectionLost
+ onConnection.addCallback(cleanup)
+
+ return onConnection
+
+
+
+class WriterProtocol(protocol.Protocol):
+ def connectionMade(self):
+ # use everything ITransport claims to provide. If something here
+ # fails, the exception will be written to the log, but it will not
+ # directly flunk the test. The test will fail when maximum number of
+ # iterations have passed and the writer's factory.done has not yet
+ # been set.
+ self.transport.write("Hello Cleveland!\n")
+ seq = ["Goodbye", " cruel", " world", "\n"]
+ self.transport.writeSequence(seq)
+ peer = self.transport.getPeer()
+ if peer.type != "TCP":
+ print "getPeer returned non-TCP socket:", peer
+ self.factory.problem = 1
+ us = self.transport.getHost()
+ if us.type != "TCP":
+ print "getHost returned non-TCP socket:", us
+ self.factory.problem = 1
+ self.factory.done = 1
+
+ self.transport.loseConnection()
+
+class ReaderProtocol(protocol.Protocol):
+ def dataReceived(self, data):
+ self.factory.data += data
+ def connectionLost(self, reason):
+ self.factory.done = 1
+
+class WriterClientFactory(protocol.ClientFactory):
+ def __init__(self):
+ self.done = 0
+ self.data = ""
+ def buildProtocol(self, addr):
+ p = ReaderProtocol()
+ p.factory = self
+ self.protocol = p
+ return p
+
+class WriteDataTestCase(unittest.TestCase):
+ """
+ Test that connected TCP sockets can actually write data. Try to exercise
+ the entire ITransport interface.
+ """
+
+ def test_writer(self):
+ """
+ L{ITCPTransport.write} and L{ITCPTransport.writeSequence} send bytes to
+ the other end of the connection.
+ """
+ f = protocol.Factory()
+ f.protocol = WriterProtocol
+ f.done = 0
+ f.problem = 0
+ wrappedF = WiredFactory(f)
+ p = reactor.listenTCP(0, wrappedF, interface="127.0.0.1")
+ self.addCleanup(p.stopListening)
+ n = p.getHost().port
+ clientF = WriterClientFactory()
+ wrappedClientF = WiredFactory(clientF)
+ reactor.connectTCP("127.0.0.1", n, wrappedClientF)
+
+ def check(ignored):
+ self.failUnless(f.done, "writer didn't finish, it probably died")
+ self.failUnless(f.problem == 0, "writer indicated an error")
+ self.failUnless(clientF.done,
+ "client didn't see connection dropped")
+ expected = "".join(["Hello Cleveland!\n",
+ "Goodbye", " cruel", " world", "\n"])
+ self.failUnless(clientF.data == expected,
+ "client didn't receive all the data it expected")
+ d = defer.gatherResults([wrappedF.onDisconnect,
+ wrappedClientF.onDisconnect])
+ return d.addCallback(check)
+
+
+ def test_writeAfterShutdownWithoutReading(self):
+ """
+ A TCP transport which is written to after the connection has been shut
+ down should notify its protocol that the connection has been lost, even
+ if the TCP transport is not actively being monitored for read events
+ (ie, pauseProducing was called on it).
+ """
+ # This is an unpleasant thing. Generally tests shouldn't skip or
+ # run based on the name of the reactor being used (most tests
+ # shouldn't care _at all_ what reactor is being used, in fact). The
+ # Gtk reactor cannot pass this test, though, because it fails to
+ # implement IReactorTCP entirely correctly. Gtk is quite old at
+ # this point, so it's more likely that gtkreactor will be deprecated
+ # and removed rather than fixed to handle this case correctly.
+ # Since this is a pre-existing (and very long-standing) issue with
+ # the Gtk reactor, there's no reason for it to prevent this test
+ # being added to exercise the other reactors, for which the behavior
+ # was also untested but at least works correctly (now). See #2833
+ # for information on the status of gtkreactor.
+ if reactor.__class__.__name__ == 'IOCPReactor':
+ raise unittest.SkipTest(
+ "iocpreactor does not, in fact, stop reading immediately after "
+ "pauseProducing is called. This results in a bonus disconnection "
+ "notification. Under some circumstances, it might be possible to "
+ "not receive this notifications (specifically, pauseProducing, "
+ "deliver some data, proceed with this test).")
+ if reactor.__class__.__name__ == 'GtkReactor':
+ raise unittest.SkipTest(
+ "gtkreactor does not implement unclean disconnection "
+ "notification correctly. This might more properly be "
+ "a todo, but due to technical limitations it cannot be.")
+
+ # Called back after the protocol for the client side of the connection
+ # has paused its transport, preventing it from reading, therefore
+ # preventing it from noticing the disconnection before the rest of the
+ # actions which are necessary to trigger the case this test is for have
+ # been taken.
+ clientPaused = defer.Deferred()
+
+ # Called back when the protocol for the server side of the connection
+ # has received connection lost notification.
+ serverLost = defer.Deferred()
+
+ class Disconnecter(protocol.Protocol):
+ """
+ Protocol for the server side of the connection which disconnects
+ itself in a callback on clientPaused and publishes notification
+ when its connection is actually lost.
+ """
+ def connectionMade(self):
+ """
+ Set up a callback on clientPaused to lose the connection.
+ """
+ msg('Disconnector.connectionMade')
+ def disconnect(ignored):
+ msg('Disconnector.connectionMade disconnect')
+ self.transport.loseConnection()
+ msg('loseConnection called')
+ clientPaused.addCallback(disconnect)
+
+ def connectionLost(self, reason):
+ """
+ Notify observers that the server side of the connection has
+ ended.
+ """
+ msg('Disconnecter.connectionLost')
+ serverLost.callback(None)
+ msg('serverLost called back')
+
+ # Create the server port to which a connection will be made.
+ server = protocol.ServerFactory()
+ server.protocol = Disconnecter
+ port = reactor.listenTCP(0, server, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+ addr = port.getHost()
+
+ class Infinite(object):
+ """
+ A producer which will write to its consumer as long as
+ resumeProducing is called.
+
+ @ivar consumer: The L{IConsumer} which will be written to.
+ """
+ implements(IPullProducer)
+
+ def __init__(self, consumer):
+ self.consumer = consumer
+
+ def resumeProducing(self):
+ msg('Infinite.resumeProducing')
+ self.consumer.write('x')
+ msg('Infinite.resumeProducing wrote to consumer')
+
+ def stopProducing(self):
+ msg('Infinite.stopProducing')
+
+
+ class UnreadingWriter(protocol.Protocol):
+ """
+ Trivial protocol which pauses its transport immediately and then
+ writes some bytes to it.
+ """
+ def connectionMade(self):
+ msg('UnreadingWriter.connectionMade')
+ self.transport.pauseProducing()
+ clientPaused.callback(None)
+ msg('clientPaused called back')
+ def write(ignored):
+ msg('UnreadingWriter.connectionMade write')
+ # This needs to be enough bytes to spill over into the
+ # userspace Twisted send buffer - if it all fits into
+ # the kernel, Twisted won't even poll for OUT events,
+ # which means it won't poll for any events at all, so
+ # the disconnection is never noticed. This is due to
+ # #1662. When #1662 is fixed, this test will likely
+ # need to be adjusted, otherwise connection lost
+ # notification will happen too soon and the test will
+ # probably begin to fail with ConnectionDone instead of
+ # ConnectionLost (in any case, it will no longer be
+ # entirely correct).
+ producer = Infinite(self.transport)
+ msg('UnreadingWriter.connectionMade write created producer')
+ self.transport.registerProducer(producer, False)
+ msg('UnreadingWriter.connectionMade write registered producer')
+ serverLost.addCallback(write)
+
+ # Create the client and initiate the connection
+ client = MyClientFactory()
+ client.protocolFactory = UnreadingWriter
+ clientConnectionLost = client.deferred
+ def cbClientLost(ignored):
+ msg('cbClientLost')
+ return client.lostReason
+ clientConnectionLost.addCallback(cbClientLost)
+ msg('Connecting to %s:%s' % (addr.host, addr.port))
+ reactor.connectTCP(addr.host, addr.port, client)
+
+ # By the end of the test, the client should have received notification
+ # of unclean disconnection.
+ msg('Returning Deferred')
+ return self.assertFailure(clientConnectionLost, error.ConnectionLost)
+
+
+
+class ConnectionLosingProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.transport.write("1")
+ self.transport.loseConnection()
+ self.master._connectionMade()
+ self.master.ports.append(self.transport)
+
+
+
+class NoopProtocol(protocol.Protocol):
+ def connectionMade(self):
+ self.d = defer.Deferred()
+ self.master.serverConns.append(self.d)
+
+ def connectionLost(self, reason):
+ self.d.callback(True)
+
+
+
+class ConnectionLostNotifyingProtocol(protocol.Protocol):
+ """
+ Protocol which fires a Deferred which was previously passed to
+ its initializer when the connection is lost.
+
+ @ivar onConnectionLost: The L{Deferred} which will be fired in
+ C{connectionLost}.
+
+ @ivar lostConnectionReason: C{None} until the connection is lost, then a
+ reference to the reason passed to C{connectionLost}.
+ """
+ def __init__(self, onConnectionLost):
+ self.lostConnectionReason = None
+ self.onConnectionLost = onConnectionLost
+
+
+ def connectionLost(self, reason):
+ self.lostConnectionReason = reason
+ self.onConnectionLost.callback(self)
+
+
+
+class HandleSavingProtocol(ConnectionLostNotifyingProtocol):
+ """
+ Protocol which grabs the platform-specific socket handle and
+ saves it as an attribute on itself when the connection is
+ established.
+ """
+ def makeConnection(self, transport):
+ """
+ Save the platform-specific socket handle for future
+ introspection.
+ """
+ self.handle = transport.getHandle()
+ return protocol.Protocol.makeConnection(self, transport)
+
+
+
+class ProperlyCloseFilesMixin:
+ """
+ Tests for platform resources properly being cleaned up.
+ """
+ def createServer(self, address, portNumber, factory):
+ """
+ Bind a server port to which connections will be made. The server
+ should use the given protocol factory.
+
+ @return: The L{IListeningPort} for the server created.
+ """
+ raise NotImplementedError()
+
+
+ def connectClient(self, address, portNumber, clientCreator):
+ """
+ Establish a connection to the given address using the given
+ L{ClientCreator} instance.
+
+ @return: A Deferred which will fire with the connected protocol instance.
+ """
+ raise NotImplementedError()
+
+
+ def getHandleExceptionType(self):
+ """
+ Return the exception class which will be raised when an operation is
+ attempted on a closed platform handle.
+ """
+ raise NotImplementedError()
+
+
+ def getHandleErrorCode(self):
+ """
+ Return the errno expected to result from writing to a closed
+ platform socket handle.
+ """
+ # These platforms have been seen to give EBADF:
+ #
+ # Linux 2.4.26, Linux 2.6.15, OS X 10.4, FreeBSD 5.4
+ # Windows 2000 SP 4, Windows XP SP 2
+ return errno.EBADF
+
+
+ def test_properlyCloseFiles(self):
+ """
+ Test that lost connections properly have their underlying socket
+ resources cleaned up.
+ """
+ onServerConnectionLost = defer.Deferred()
+ serverFactory = protocol.ServerFactory()
+ serverFactory.protocol = lambda: ConnectionLostNotifyingProtocol(
+ onServerConnectionLost)
+ serverPort = self.createServer('127.0.0.1', 0, serverFactory)
+
+ onClientConnectionLost = defer.Deferred()
+ serverAddr = serverPort.getHost()
+ clientCreator = protocol.ClientCreator(
+ reactor, lambda: HandleSavingProtocol(onClientConnectionLost))
+ clientDeferred = self.connectClient(
+ serverAddr.host, serverAddr.port, clientCreator)
+
+ def clientConnected(client):
+ """
+ Disconnect the client. Return a Deferred which fires when both
+ the client and the server have received disconnect notification.
+ """
+ client.transport.write(
+ 'some bytes to make sure the connection is set up')
+ client.transport.loseConnection()
+ return defer.gatherResults([
+ onClientConnectionLost, onServerConnectionLost])
+ clientDeferred.addCallback(clientConnected)
+
+ def clientDisconnected((client, server)):
+ """
+ Verify that the underlying platform socket handle has been
+ cleaned up.
+ """
+ client.lostConnectionReason.trap(error.ConnectionClosed)
+ server.lostConnectionReason.trap(error.ConnectionClosed)
+ expectedErrorCode = self.getHandleErrorCode()
+ err = self.assertRaises(
+ self.getHandleExceptionType(), client.handle.send, 'bytes')
+ self.assertEqual(err.args[0], expectedErrorCode)
+ clientDeferred.addCallback(clientDisconnected)
+
+ def cleanup(passthrough):
+ """
+ Shut down the server port. Return a Deferred which fires when
+ this has completed.
+ """
+ result = defer.maybeDeferred(serverPort.stopListening)
+ result.addCallback(lambda ign: passthrough)
+ return result
+ clientDeferred.addBoth(cleanup)
+
+ return clientDeferred
+
+
+
+class ProperlyCloseFilesTestCase(unittest.TestCase, ProperlyCloseFilesMixin):
+ """
+ Test that the sockets created by L{IReactorTCP.connectTCP} are cleaned up
+ when the connection they are associated with is closed.
+ """
+ def createServer(self, address, portNumber, factory):
+ """
+ Create a TCP server using L{IReactorTCP.listenTCP}.
+ """
+ return reactor.listenTCP(portNumber, factory, interface=address)
+
+
+ def connectClient(self, address, portNumber, clientCreator):
+ """
+ Create a TCP client using L{IReactorTCP.connectTCP}.
+ """
+ return clientCreator.connectTCP(address, portNumber)
+
+
+ def getHandleExceptionType(self):
+ """
+ Return L{socket.error} as the expected error type which will be
+ raised by a write to the low-level socket object after it has been
+ closed.
+ """
+ return socket.error
+
+
+
+class WiredForDeferreds(policies.ProtocolWrapper):
+ def __init__(self, factory, wrappedProtocol):
+ policies.ProtocolWrapper.__init__(self, factory, wrappedProtocol)
+
+ def connectionMade(self):
+ policies.ProtocolWrapper.connectionMade(self)
+ self.factory.onConnect.callback(None)
+
+ def connectionLost(self, reason):
+ policies.ProtocolWrapper.connectionLost(self, reason)
+ self.factory.onDisconnect.callback(None)
+
+
+
+class WiredFactory(policies.WrappingFactory):
+ protocol = WiredForDeferreds
+
+ def __init__(self, wrappedFactory):
+ policies.WrappingFactory.__init__(self, wrappedFactory)
+ self.onConnect = defer.Deferred()
+ self.onDisconnect = defer.Deferred()
+
+
+
+class AddressTestCase(unittest.TestCase):
+ """
+ Tests for address-related interactions with client and server protocols.
+ """
+ def setUp(self):
+ """
+ Create a port and connected client/server pair which can be used
+ to test factory behavior related to addresses.
+
+ @return: A L{defer.Deferred} which will be called back when both the
+ client and server protocols have received their connection made
+ callback.
+ """
+ class RememberingWrapper(protocol.ClientFactory):
+ """
+ Simple wrapper factory which records the addresses which are
+ passed to its L{buildProtocol} method and delegates actual
+ protocol creation to another factory.
+
+ @ivar addresses: A list of the objects passed to buildProtocol.
+ @ivar factory: The wrapped factory to which protocol creation is
+ delegated.
+ """
+ def __init__(self, factory):
+ self.addresses = []
+ self.factory = factory
+
+ # Only bother to pass on buildProtocol calls to the wrapped
+ # factory - doStart, doStop, etc aren't necessary for this test
+ # to pass.
+ def buildProtocol(self, addr):
+ """
+ Append the given address to C{self.addresses} and forward
+ the call to C{self.factory}.
+ """
+ self.addresses.append(addr)
+ return self.factory.buildProtocol(addr)
+
+ # Make a server which we can receive connection and disconnection
+ # notification for, and which will record the address passed to its
+ # buildProtocol.
+ self.server = MyServerFactory()
+ self.serverConnMade = self.server.protocolConnectionMade = defer.Deferred()
+ self.serverConnLost = self.server.protocolConnectionLost = defer.Deferred()
+ # RememberingWrapper is a ClientFactory, but ClientFactory is-a
+ # ServerFactory, so this is okay.
+ self.serverWrapper = RememberingWrapper(self.server)
+
+ # Do something similar for a client.
+ self.client = MyClientFactory()
+ self.clientConnMade = self.client.protocolConnectionMade = defer.Deferred()
+ self.clientConnLost = self.client.protocolConnectionLost = defer.Deferred()
+ self.clientWrapper = RememberingWrapper(self.client)
+
+ self.port = reactor.listenTCP(0, self.serverWrapper, interface='127.0.0.1')
+ self.connector = reactor.connectTCP(
+ self.port.getHost().host, self.port.getHost().port, self.clientWrapper)
+
+ return defer.gatherResults([self.serverConnMade, self.clientConnMade])
+
+
+ def tearDown(self):
+ """
+ Disconnect the client/server pair and shutdown the port created in
+ L{setUp}.
+ """
+ self.connector.disconnect()
+ return defer.gatherResults([
+ self.serverConnLost, self.clientConnLost,
+ defer.maybeDeferred(self.port.stopListening)])
+
+
+ def test_buildProtocolClient(self):
+ """
+ L{ClientFactory.buildProtocol} should be invoked with the address of
+ the server to which a connection has been established, which should
+ be the same as the address reported by the C{getHost} method of the
+ transport of the server protocol and as the C{getPeer} method of the
+ transport of the client protocol.
+ """
+ serverHost = self.server.protocol.transport.getHost()
+ clientPeer = self.client.protocol.transport.getPeer()
+
+ self.assertEqual(
+ self.clientWrapper.addresses,
+ [IPv4Address('TCP', serverHost.host, serverHost.port)])
+ self.assertEqual(
+ self.clientWrapper.addresses,
+ [IPv4Address('TCP', clientPeer.host, clientPeer.port)])
+
+
+
+class LargeBufferWriterProtocol(protocol.Protocol):
+
+ # Win32 sockets cannot handle single huge chunks of bytes. Write one
+ # massive string to make sure Twisted deals with this fact.
+
+ def connectionMade(self):
+ # write 60MB
+ self.transport.write('X'*self.factory.len)
+ self.factory.done = 1
+ self.transport.loseConnection()
+
+class LargeBufferReaderProtocol(protocol.Protocol):
+ def dataReceived(self, data):
+ self.factory.len += len(data)
+ def connectionLost(self, reason):
+ self.factory.done = 1
+
+class LargeBufferReaderClientFactory(protocol.ClientFactory):
+ def __init__(self):
+ self.done = 0
+ self.len = 0
+ def buildProtocol(self, addr):
+ p = LargeBufferReaderProtocol()
+ p.factory = self
+ self.protocol = p
+ return p
+
+
+class FireOnClose(policies.ProtocolWrapper):
+ """A wrapper around a protocol that makes it fire a deferred when
+ connectionLost is called.
+ """
+ def connectionLost(self, reason):
+ policies.ProtocolWrapper.connectionLost(self, reason)
+ self.factory.deferred.callback(None)
+
+
+class FireOnCloseFactory(policies.WrappingFactory):
+ protocol = FireOnClose
+
+ def __init__(self, wrappedFactory):
+ policies.WrappingFactory.__init__(self, wrappedFactory)
+ self.deferred = defer.Deferred()
+
+
+class LargeBufferTestCase(unittest.TestCase):
+ """Test that buffering large amounts of data works.
+ """
+
+ datalen = 60*1024*1024
+ def testWriter(self):
+ f = protocol.Factory()
+ f.protocol = LargeBufferWriterProtocol
+ f.done = 0
+ f.problem = 0
+ f.len = self.datalen
+ wrappedF = FireOnCloseFactory(f)
+ p = reactor.listenTCP(0, wrappedF, interface="127.0.0.1")
+ self.addCleanup(p.stopListening)
+ n = p.getHost().port
+ clientF = LargeBufferReaderClientFactory()
+ wrappedClientF = FireOnCloseFactory(clientF)
+ reactor.connectTCP("127.0.0.1", n, wrappedClientF)
+
+ d = defer.gatherResults([wrappedF.deferred, wrappedClientF.deferred])
+ def check(ignored):
+ self.failUnless(f.done, "writer didn't finish, it probably died")
+ self.failUnless(clientF.len == self.datalen,
+ "client didn't receive all the data it expected "
+ "(%d != %d)" % (clientF.len, self.datalen))
+ self.failUnless(clientF.done,
+ "client didn't see connection dropped")
+ return d.addCallback(check)
+
+
+class MyHCProtocol(AccumulatingProtocol):
+
+ implements(IHalfCloseableProtocol)
+
+ readHalfClosed = False
+ writeHalfClosed = False
+
+ def readConnectionLost(self):
+ self.readHalfClosed = True
+ # Invoke notification logic from the base class to simplify testing.
+ if self.writeHalfClosed:
+ self.connectionLost(None)
+
+ def writeConnectionLost(self):
+ self.writeHalfClosed = True
+ # Invoke notification logic from the base class to simplify testing.
+ if self.readHalfClosed:
+ self.connectionLost(None)
+
+
+class MyHCFactory(protocol.ServerFactory):
+
+ called = 0
+ protocolConnectionMade = None
+
+ def buildProtocol(self, addr):
+ self.called += 1
+ p = MyHCProtocol()
+ p.factory = self
+ self.protocol = p
+ return p
+
+
+class HalfCloseTestCase(unittest.TestCase):
+ """Test half-closing connections."""
+
+ def setUp(self):
+ self.f = f = MyHCFactory()
+ self.p = p = reactor.listenTCP(0, f, interface="127.0.0.1")
+ self.addCleanup(p.stopListening)
+ d = loopUntil(lambda :p.connected)
+
+ self.cf = protocol.ClientCreator(reactor, MyHCProtocol)
+
+ d.addCallback(lambda _: self.cf.connectTCP(p.getHost().host,
+ p.getHost().port))
+ d.addCallback(self._setUp)
+ return d
+
+ def _setUp(self, client):
+ self.client = client
+ self.clientProtoConnectionLost = self.client.closedDeferred = defer.Deferred()
+ self.assertEqual(self.client.transport.connected, 1)
+ # Wait for the server to notice there is a connection, too.
+ return loopUntil(lambda: getattr(self.f, 'protocol', None) is not None)
+
+ def tearDown(self):
+ self.assertEqual(self.client.closed, 0)
+ self.client.transport.loseConnection()
+ d = defer.maybeDeferred(self.p.stopListening)
+ d.addCallback(lambda ign: self.clientProtoConnectionLost)
+ d.addCallback(self._tearDown)
+ return d
+
+ def _tearDown(self, ignored):
+ self.assertEqual(self.client.closed, 1)
+ # because we did half-close, the server also needs to
+ # closed explicitly.
+ self.assertEqual(self.f.protocol.closed, 0)
+ d = defer.Deferred()
+ def _connectionLost(reason):
+ self.f.protocol.closed = 1
+ d.callback(None)
+ self.f.protocol.connectionLost = _connectionLost
+ self.f.protocol.transport.loseConnection()
+ d.addCallback(lambda x:self.assertEqual(self.f.protocol.closed, 1))
+ return d
+
+ def testCloseWriteCloser(self):
+ client = self.client
+ f = self.f
+ t = client.transport
+
+ t.write("hello")
+ d = loopUntil(lambda :len(t._tempDataBuffer) == 0)
+ def loseWrite(ignored):
+ t.loseWriteConnection()
+ return loopUntil(lambda :t._writeDisconnected)
+ def check(ignored):
+ self.assertEqual(client.closed, False)
+ self.assertEqual(client.writeHalfClosed, True)
+ self.assertEqual(client.readHalfClosed, False)
+ return loopUntil(lambda :f.protocol.readHalfClosed)
+ def write(ignored):
+ w = client.transport.write
+ w(" world")
+ w("lalala fooled you")
+ self.assertEqual(0, len(client.transport._tempDataBuffer))
+ self.assertEqual(f.protocol.data, "hello")
+ self.assertEqual(f.protocol.closed, False)
+ self.assertEqual(f.protocol.readHalfClosed, True)
+ return d.addCallback(loseWrite).addCallback(check).addCallback(write)
+
+ def testWriteCloseNotification(self):
+ f = self.f
+ f.protocol.transport.loseWriteConnection()
+
+ d = defer.gatherResults([
+ loopUntil(lambda :f.protocol.writeHalfClosed),
+ loopUntil(lambda :self.client.readHalfClosed)])
+ d.addCallback(lambda _: self.assertEqual(
+ f.protocol.readHalfClosed, False))
+ return d
+
+
+class HalfClose2TestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.f = f = MyServerFactory()
+ self.f.protocolConnectionMade = defer.Deferred()
+ self.p = p = reactor.listenTCP(0, f, interface="127.0.0.1")
+
+ # XXX we don't test server side yet since we don't do it yet
+ d = protocol.ClientCreator(reactor, AccumulatingProtocol).connectTCP(
+ p.getHost().host, p.getHost().port)
+ d.addCallback(self._gotClient)
+ return d
+
+ def _gotClient(self, client):
+ self.client = client
+ # Now wait for the server to catch up - it doesn't matter if this
+ # Deferred has already fired and gone away, in that case we'll
+ # return None and not wait at all, which is precisely correct.
+ return self.f.protocolConnectionMade
+
+ def tearDown(self):
+ self.client.transport.loseConnection()
+ return self.p.stopListening()
+
+ def testNoNotification(self):
+ """
+ TCP protocols support half-close connections, but not all of them
+ support being notified of write closes. In this case, test that
+ half-closing the connection causes the peer's connection to be
+ closed.
+ """
+ self.client.transport.write("hello")
+ self.client.transport.loseWriteConnection()
+ self.f.protocol.closedDeferred = d = defer.Deferred()
+ self.client.closedDeferred = d2 = defer.Deferred()
+ d.addCallback(lambda x:
+ self.assertEqual(self.f.protocol.data, 'hello'))
+ d.addCallback(lambda x: self.assertEqual(self.f.protocol.closed, True))
+ return defer.gatherResults([d, d2])
+
+ def testShutdownException(self):
+ """
+ If the other side has already closed its connection,
+ loseWriteConnection should pass silently.
+ """
+ self.f.protocol.transport.loseConnection()
+ self.client.transport.write("X")
+ self.client.transport.loseWriteConnection()
+ self.f.protocol.closedDeferred = d = defer.Deferred()
+ self.client.closedDeferred = d2 = defer.Deferred()
+ d.addCallback(lambda x:
+ self.assertEqual(self.f.protocol.closed, True))
+ return defer.gatherResults([d, d2])
+
+
+class HalfCloseBuggyApplicationTests(unittest.TestCase):
+ """
+ Test half-closing connections where notification code has bugs.
+ """
+
+ def setUp(self):
+ """
+ Set up a server and connect a client to it. Return a Deferred which
+ only fires once this is done.
+ """
+ self.serverFactory = MyHCFactory()
+ self.serverFactory.protocolConnectionMade = defer.Deferred()
+ self.port = reactor.listenTCP(
+ 0, self.serverFactory, interface="127.0.0.1")
+ self.addCleanup(self.port.stopListening)
+ addr = self.port.getHost()
+ creator = protocol.ClientCreator(reactor, MyHCProtocol)
+ clientDeferred = creator.connectTCP(addr.host, addr.port)
+ def setClient(clientProtocol):
+ self.clientProtocol = clientProtocol
+ clientDeferred.addCallback(setClient)
+ return defer.gatherResults([
+ self.serverFactory.protocolConnectionMade,
+ clientDeferred])
+
+
+ def aBug(self, *args):
+ """
+ Fake implementation of a callback which illegally raises an
+ exception.
+ """
+ raise RuntimeError("ONO I AM BUGGY CODE")
+
+
+ def _notificationRaisesTest(self):
+ """
+ Helper for testing that an exception is logged by the time the
+ client protocol loses its connection.
+ """
+ closed = self.clientProtocol.closedDeferred = defer.Deferred()
+ self.clientProtocol.transport.loseWriteConnection()
+ def check(ignored):
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+ closed.addCallback(check)
+ return closed
+
+
+ def test_readNotificationRaises(self):
+ """
+ If C{readConnectionLost} raises an exception when the transport
+ calls it to notify the protocol of that event, the exception should
+ be logged and the protocol should be disconnected completely.
+ """
+ self.serverFactory.protocol.readConnectionLost = self.aBug
+ return self._notificationRaisesTest()
+
+
+ def test_writeNotificationRaises(self):
+ """
+ If C{writeConnectionLost} raises an exception when the transport
+ calls it to notify the protocol of that event, the exception should
+ be logged and the protocol should be disconnected completely.
+ """
+ self.clientProtocol.writeConnectionLost = self.aBug
+ return self._notificationRaisesTest()
+
+
+
+class LogTestCase(unittest.TestCase):
+ """
+ Test logging facility of TCP base classes.
+ """
+
+ def test_logstrClientSetup(self):
+ """
+ Check that the log customization of the client transport happens
+ once the client is connected.
+ """
+ server = MyServerFactory()
+
+ client = MyClientFactory()
+ client.protocolConnectionMade = defer.Deferred()
+
+ port = reactor.listenTCP(0, server, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+
+ connector = reactor.connectTCP(
+ port.getHost().host, port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+
+ # It should still have the default value
+ self.assertEqual(connector.transport.logstr,
+ "Uninitialized")
+
+ def cb(ign):
+ self.assertEqual(connector.transport.logstr,
+ "AccumulatingProtocol,client")
+ client.protocolConnectionMade.addCallback(cb)
+ return client.protocolConnectionMade
+
+
+
+class PauseProducingTestCase(unittest.TestCase):
+ """
+ Test some behaviors of pausing the production of a transport.
+ """
+
+ def test_pauseProducingInConnectionMade(self):
+ """
+ In C{connectionMade} of a client protocol, C{pauseProducing} used to be
+ ignored: this test is here to ensure it's not ignored.
+ """
+ server = MyServerFactory()
+
+ client = MyClientFactory()
+ client.protocolConnectionMade = defer.Deferred()
+
+ port = reactor.listenTCP(0, server, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+
+ connector = reactor.connectTCP(
+ port.getHost().host, port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+
+ def checkInConnectionMade(proto):
+ tr = proto.transport
+ # The transport should already be monitored
+ self.assertIn(tr, reactor.getReaders() +
+ reactor.getWriters())
+ proto.transport.pauseProducing()
+ self.assertNotIn(tr, reactor.getReaders() +
+ reactor.getWriters())
+ d = defer.Deferred()
+ d.addCallback(checkAfterConnectionMade)
+ reactor.callLater(0, d.callback, proto)
+ return d
+ def checkAfterConnectionMade(proto):
+ tr = proto.transport
+ # The transport should still not be monitored
+ self.assertNotIn(tr, reactor.getReaders() +
+ reactor.getWriters())
+ client.protocolConnectionMade.addCallback(checkInConnectionMade)
+ return client.protocolConnectionMade
+
+ if not interfaces.IReactorFDSet.providedBy(reactor):
+ test_pauseProducingInConnectionMade.skip = "Reactor not providing IReactorFDSet"
+
+
+
+class CallBackOrderTestCase(unittest.TestCase):
+ """
+ Test the order of reactor callbacks
+ """
+
+ def test_loseOrder(self):
+ """
+ Check that Protocol.connectionLost is called before factory's
+ clientConnectionLost
+ """
+ server = MyServerFactory()
+ server.protocolConnectionMade = (defer.Deferred()
+ .addCallback(lambda proto: self.addCleanup(
+ proto.transport.loseConnection)))
+
+ client = MyClientFactory()
+ client.protocolConnectionLost = defer.Deferred()
+ client.protocolConnectionMade = defer.Deferred()
+
+ def _cbCM(res):
+ """
+ protocol.connectionMade callback
+ """
+ reactor.callLater(0, client.protocol.transport.loseConnection)
+
+ client.protocolConnectionMade.addCallback(_cbCM)
+
+ port = reactor.listenTCP(0, server, interface='127.0.0.1')
+ self.addCleanup(port.stopListening)
+
+ connector = reactor.connectTCP(
+ port.getHost().host, port.getHost().port, client)
+ self.addCleanup(connector.disconnect)
+
+ def _cbCCL(res):
+ """
+ factory.clientConnectionLost callback
+ """
+ return 'CCL'
+
+ def _cbCL(res):
+ """
+ protocol.connectionLost callback
+ """
+ return 'CL'
+
+ def _cbGather(res):
+ self.assertEqual(res, ['CL', 'CCL'])
+
+ d = defer.gatherResults([
+ client.protocolConnectionLost.addCallback(_cbCL),
+ client.deferred.addCallback(_cbCCL)])
+ return d.addCallback(_cbGather)
+
+
+
+try:
+ import resource
+except ImportError:
+ pass
+else:
+ numRounds = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + 10
+ ProperlyCloseFilesTestCase.numberRounds = numRounds
diff --git a/twisted/test/test_tcp_internals.py b/twisted/test/test_tcp_internals.py
new file mode 100644
index 0000000..aa7aeac
--- /dev/null
+++ b/twisted/test/test_tcp_internals.py
@@ -0,0 +1,249 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Whitebox tests for TCP APIs.
+"""
+
+import errno, socket, os
+
+try:
+ import resource
+except ImportError:
+ resource = None
+
+from twisted.trial.unittest import TestCase
+
+from twisted.python import log
+from twisted.internet.tcp import ECONNABORTED, ENOMEM, ENFILE, EMFILE, ENOBUFS, EINPROGRESS, Port
+from twisted.internet.protocol import ServerFactory
+from twisted.python.runtime import platform
+from twisted.internet.defer import maybeDeferred, gatherResults
+from twisted.internet import reactor, interfaces
+
+
+class PlatformAssumptionsTestCase(TestCase):
+ """
+ Test assumptions about platform behaviors.
+ """
+ socketLimit = 8192
+
+ def setUp(self):
+ self.openSockets = []
+ if resource is not None:
+ # On some buggy platforms we might leak FDs, and the test will
+ # fail creating the initial two sockets we *do* want to
+ # succeed. So, we make the soft limit the current number of fds
+ # plus two more (for the two sockets we want to succeed). If we've
+ # leaked too many fds for that to work, there's nothing we can
+ # do.
+ from twisted.internet.process import _listOpenFDs
+ newLimit = len(_listOpenFDs()) + 2
+ self.originalFileLimit = resource.getrlimit(resource.RLIMIT_NOFILE)
+ resource.setrlimit(resource.RLIMIT_NOFILE, (newLimit, self.originalFileLimit[1]))
+ self.socketLimit = newLimit + 100
+
+
+ def tearDown(self):
+ while self.openSockets:
+ self.openSockets.pop().close()
+ if resource is not None:
+ # OS X implicitly lowers the hard limit in the setrlimit call
+ # above. Retrieve the new hard limit to pass in to this
+ # setrlimit call, so that it doesn't give us a permission denied
+ # error.
+ currentHardLimit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+ newSoftLimit = min(self.originalFileLimit[0], currentHardLimit)
+ resource.setrlimit(resource.RLIMIT_NOFILE, (newSoftLimit, currentHardLimit))
+
+
+ def socket(self):
+ """
+ Create and return a new socket object, also tracking it so it can be
+ closed in the test tear down.
+ """
+ s = socket.socket()
+ self.openSockets.append(s)
+ return s
+
+
+ def test_acceptOutOfFiles(self):
+ """
+ Test that the platform accept(2) call fails with either L{EMFILE} or
+ L{ENOBUFS} when there are too many file descriptors open.
+ """
+ # Make a server to which to connect
+ port = self.socket()
+ port.bind(('127.0.0.1', 0))
+ serverPortNumber = port.getsockname()[1]
+ port.listen(5)
+
+ # Make a client to use to connect to the server
+ client = self.socket()
+ client.setblocking(False)
+
+ # Use up all the rest of the file descriptors.
+ for i in xrange(self.socketLimit):
+ try:
+ self.socket()
+ except socket.error, e:
+ if e.args[0] in (EMFILE, ENOBUFS):
+ # The desired state has been achieved.
+ break
+ else:
+ # Some unexpected error occurred.
+ raise
+ else:
+ self.fail("Could provoke neither EMFILE nor ENOBUFS from platform.")
+
+ # Non-blocking connect is supposed to fail, but this is not true
+ # everywhere (e.g. freeBSD)
+ self.assertIn(client.connect_ex(('127.0.0.1', serverPortNumber)),
+ (0, EINPROGRESS))
+
+ # Make sure that the accept call fails in the way we expect.
+ exc = self.assertRaises(socket.error, port.accept)
+ self.assertIn(exc.args[0], (EMFILE, ENOBUFS))
+ if platform.getType() == "win32":
+ test_acceptOutOfFiles.skip = (
+ "Windows requires an unacceptably large amount of resources to "
+ "provoke this behavior in the naive manner.")
+
+
+
+class SelectReactorTestCase(TestCase):
+ """
+ Tests for select-specific failure conditions.
+ """
+
+ def setUp(self):
+ self.ports = []
+ self.messages = []
+ log.addObserver(self.messages.append)
+
+
+ def tearDown(self):
+ log.removeObserver(self.messages.append)
+ return gatherResults([
+ maybeDeferred(p.stopListening)
+ for p in self.ports])
+
+
+ def port(self, portNumber, factory, interface):
+ """
+ Create, start, and return a new L{Port}, also tracking it so it can
+ be stopped in the test tear down.
+ """
+ p = Port(portNumber, factory, interface=interface)
+ p.startListening()
+ self.ports.append(p)
+ return p
+
+
+ def _acceptFailureTest(self, socketErrorNumber):
+ """
+ Test behavior in the face of an exception from C{accept(2)}.
+
+ On any exception which indicates the platform is unable or unwilling
+ to allocate further resources to us, the existing port should remain
+ listening, a message should be logged, and the exception should not
+ propagate outward from doRead.
+
+ @param socketErrorNumber: The errno to simulate from accept.
+ """
+ class FakeSocket(object):
+ """
+ Pretend to be a socket in an overloaded system.
+ """
+ def accept(self):
+ raise socket.error(
+ socketErrorNumber, os.strerror(socketErrorNumber))
+
+ factory = ServerFactory()
+ port = self.port(0, factory, interface='127.0.0.1')
+ originalSocket = port.socket
+ try:
+ port.socket = FakeSocket()
+
+ port.doRead()
+
+ expectedFormat = "Could not accept new connection (%s)"
+ expectedErrorCode = errno.errorcode[socketErrorNumber]
+ expectedMessage = expectedFormat % (expectedErrorCode,)
+ for msg in self.messages:
+ if msg.get('message') == (expectedMessage,):
+ break
+ else:
+ self.fail("Log event for failed accept not found in "
+ "%r" % (self.messages,))
+ finally:
+ port.socket = originalSocket
+
+
+ def test_tooManyFilesFromAccept(self):
+ """
+ C{accept(2)} can fail with C{EMFILE} when there are too many open file
+ descriptors in the process. Test that this doesn't negatively impact
+ any other existing connections.
+
+ C{EMFILE} mainly occurs on Linux when the open file rlimit is
+ encountered.
+ """
+ return self._acceptFailureTest(EMFILE)
+
+
+ def test_noBufferSpaceFromAccept(self):
+ """
+ Similar to L{test_tooManyFilesFromAccept}, but test the case where
+ C{accept(2)} fails with C{ENOBUFS}.
+
+ This mainly occurs on Windows and FreeBSD, but may be possible on
+ Linux and other platforms as well.
+ """
+ return self._acceptFailureTest(ENOBUFS)
+
+
+ def test_connectionAbortedFromAccept(self):
+ """
+ Similar to L{test_tooManyFilesFromAccept}, but test the case where
+ C{accept(2)} fails with C{ECONNABORTED}.
+
+ It is not clear whether this is actually possible for TCP
+ connections on modern versions of Linux.
+ """
+ return self._acceptFailureTest(ECONNABORTED)
+
+
+ def test_noFilesFromAccept(self):
+ """
+ Similar to L{test_tooManyFilesFromAccept}, but test the case where
+ C{accept(2)} fails with C{ENFILE}.
+
+ This can occur on Linux when the system has exhausted (!) its supply
+ of inodes.
+ """
+ return self._acceptFailureTest(ENFILE)
+ if platform.getType() == 'win32':
+ test_noFilesFromAccept.skip = "Windows accept(2) cannot generate ENFILE"
+
+
+ def test_noMemoryFromAccept(self):
+ """
+ Similar to L{test_tooManyFilesFromAccept}, but test the case where
+ C{accept(2)} fails with C{ENOMEM}.
+
+ On Linux at least, this can sensibly occur, even in a Python program
+ (which eats memory like no ones business), when memory has become
+ fragmented or low memory has been filled (d_alloc calls
+ kmem_cache_alloc calls kmalloc - kmalloc only allocates out of low
+ memory).
+ """
+ return self._acceptFailureTest(ENOMEM)
+ if platform.getType() == 'win32':
+ test_noMemoryFromAccept.skip = "Windows accept(2) cannot generate ENOMEM"
+
+if not interfaces.IReactorFDSet.providedBy(reactor):
+ skipMsg = 'This test only applies to reactors that implement IReactorFDset'
+ PlatformAssumptionsTestCase.skip = skipMsg
+ SelectReactorTestCase.skip = skipMsg
+
diff --git a/twisted/test/test_text.py b/twisted/test/test_text.py
new file mode 100644
index 0000000..92fad77
--- /dev/null
+++ b/twisted/test/test_text.py
@@ -0,0 +1,158 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+from twisted.python import text
+from cStringIO import StringIO
+
+
+sampleText = \
+"""Every attempt to employ mathematical methods in the study of chemical
+questions must be considered profoundly irrational and contrary to the
+spirit of chemistry ... If mathematical analysis should ever hold a
+prominent place in chemistry - an aberration which is happily almost
+impossible - it would occasion a rapid and widespread degeneration of that
+science.
+
+ -- Auguste Comte, Philosophie Positive, Paris, 1838
+"""
+
+lineWidth = 72
+
+def set_lineWidth(n):
+ global lineWidth
+ lineWidth = n
+
+class WrapTest(unittest.TestCase):
+ def setUp(self):
+ self.sampleSplitText = sampleText.split()
+
+ self.output = text.wordWrap(sampleText, lineWidth)
+
+ def test_wordCount(self):
+ """Compare the number of words."""
+ words = []
+ for line in self.output:
+ words.extend(line.split())
+ wordCount = len(words)
+ sampleTextWordCount = len(self.sampleSplitText)
+
+ self.assertEqual(wordCount, sampleTextWordCount)
+
+ def test_wordMatch(self):
+ """Compare the lists of words."""
+
+ words = []
+ for line in self.output:
+ words.extend(line.split())
+
+ # Using assertEqual here prints out some
+ # rather too long lists.
+ self.failUnless(self.sampleSplitText == words)
+
+ def test_lineLength(self):
+ """Check the length of the lines."""
+ failures = []
+ for line in self.output:
+ if not len(line) <= lineWidth:
+ failures.append(len(line))
+
+ if failures:
+ self.fail("%d of %d lines were too long.\n"
+ "%d < %s" % (len(failures), len(self.output),
+ lineWidth, failures))
+
+
+class SplitTest(unittest.TestCase):
+ """Tests for text.splitQuoted()"""
+
+ def test_oneWord(self):
+ """Splitting strings with one-word phrases."""
+ s = 'This code "works."'
+ r = text.splitQuoted(s)
+ self.assertEqual(['This', 'code', 'works.'], r)
+
+ def test_multiWord(self):
+ s = 'The "hairy monkey" likes pie.'
+ r = text.splitQuoted(s)
+ self.assertEqual(['The', 'hairy monkey', 'likes', 'pie.'], r)
+
+ # Some of the many tests that would fail:
+
+ #def test_preserveWhitespace(self):
+ # phrase = '"MANY SPACES"'
+ # s = 'With %s between.' % (phrase,)
+ # r = text.splitQuoted(s)
+ # self.assertEqual(['With', phrase, 'between.'], r)
+
+ #def test_escapedSpace(self):
+ # s = r"One\ Phrase"
+ # r = text.splitQuoted(s)
+ # self.assertEqual(["One Phrase"], r)
+
+class StrFileTest(unittest.TestCase):
+ def setUp(self):
+ self.io = StringIO("this is a test string")
+
+ def tearDown(self):
+ pass
+
+ def test_1_f(self):
+ self.assertEqual(False, text.strFile("x", self.io))
+
+ def test_1_1(self):
+ self.assertEqual(True, text.strFile("t", self.io))
+
+ def test_1_2(self):
+ self.assertEqual(True, text.strFile("h", self.io))
+
+ def test_1_3(self):
+ self.assertEqual(True, text.strFile("i", self.io))
+
+ def test_1_4(self):
+ self.assertEqual(True, text.strFile("s", self.io))
+
+ def test_1_5(self):
+ self.assertEqual(True, text.strFile("n", self.io))
+
+ def test_1_6(self):
+ self.assertEqual(True, text.strFile("g", self.io))
+
+ def test_3_1(self):
+ self.assertEqual(True, text.strFile("thi", self.io))
+
+ def test_3_2(self):
+ self.assertEqual(True, text.strFile("his", self.io))
+
+ def test_3_3(self):
+ self.assertEqual(True, text.strFile("is ", self.io))
+
+ def test_3_4(self):
+ self.assertEqual(True, text.strFile("ing", self.io))
+
+ def test_3_f(self):
+ self.assertEqual(False, text.strFile("bla", self.io))
+
+ def test_large_1(self):
+ self.assertEqual(True, text.strFile("this is a test", self.io))
+
+ def test_large_2(self):
+ self.assertEqual(True, text.strFile("is a test string", self.io))
+
+ def test_large_f(self):
+ self.assertEqual(False, text.strFile("ds jhfsa k fdas", self.io))
+
+ def test_overlarge_f(self):
+ self.assertEqual(False, text.strFile("djhsakj dhsa fkhsa s,mdbnfsauiw bndasdf hreew", self.io))
+
+ def test_self(self):
+ self.assertEqual(True, text.strFile("this is a test string", self.io))
+
+ def test_insensitive(self):
+ self.assertEqual(True, text.strFile("ThIs is A test STRING", self.io, False))
+
+
+
+testCases = [WrapTest, SplitTest, StrFileTest]
diff --git a/twisted/test/test_threadable.py b/twisted/test/test_threadable.py
new file mode 100644
index 0000000..f23515a
--- /dev/null
+++ b/twisted/test/test_threadable.py
@@ -0,0 +1,103 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, pickle
+
+try:
+ import threading
+except ImportError:
+ threading = None
+
+from twisted.trial import unittest
+from twisted.python import threadable
+from twisted.internet import defer, reactor
+
+class TestObject:
+ synchronized = ['aMethod']
+
+ x = -1
+ y = 1
+
+ def aMethod(self):
+ for i in xrange(10):
+ self.x, self.y = self.y, self.x
+ self.z = self.x + self.y
+ assert self.z == 0, "z == %d, not 0 as expected" % (self.z,)
+
+threadable.synchronize(TestObject)
+
+class SynchronizationTestCase(unittest.TestCase):
+ def setUp(self):
+ """
+ Reduce the CPython check interval so that thread switches happen much
+ more often, hopefully exercising more possible race conditions. Also,
+ delay actual test startup until the reactor has been started.
+ """
+ if hasattr(sys, 'getcheckinterval'):
+ self.addCleanup(sys.setcheckinterval, sys.getcheckinterval())
+ sys.setcheckinterval(7)
+ # XXX This is a trial hack. We need to make sure the reactor
+ # actually *starts* for isInIOThread() to have a meaningful result.
+ # Returning a Deferred here should force that to happen, if it has
+ # not happened already. In the future, this should not be
+ # necessary.
+ d = defer.Deferred()
+ reactor.callLater(0, d.callback, None)
+ return d
+
+
+ def testIsInIOThread(self):
+ foreignResult = []
+ t = threading.Thread(target=lambda: foreignResult.append(threadable.isInIOThread()))
+ t.start()
+ t.join()
+ self.failIf(foreignResult[0], "Non-IO thread reported as IO thread")
+ self.failUnless(threadable.isInIOThread(), "IO thread reported as not IO thread")
+
+
+ def testThreadedSynchronization(self):
+ o = TestObject()
+
+ errors = []
+
+ def callMethodLots():
+ try:
+ for i in xrange(1000):
+ o.aMethod()
+ except AssertionError, e:
+ errors.append(str(e))
+
+ threads = []
+ for x in range(5):
+ t = threading.Thread(target=callMethodLots)
+ threads.append(t)
+ t.start()
+
+ for t in threads:
+ t.join()
+
+ if errors:
+ raise unittest.FailTest(errors)
+
+ def testUnthreadedSynchronization(self):
+ o = TestObject()
+ for i in xrange(1000):
+ o.aMethod()
+
+class SerializationTestCase(unittest.TestCase):
+ def testPickling(self):
+ lock = threadable.XLock()
+ lockType = type(lock)
+ lockPickle = pickle.dumps(lock)
+ newLock = pickle.loads(lockPickle)
+ self.failUnless(isinstance(newLock, lockType))
+
+ def testUnpickling(self):
+ lockPickle = 'ctwisted.python.threadable\nunpickle_lock\np0\n(tp1\nRp2\n.'
+ lock = pickle.loads(lockPickle)
+ newPickle = pickle.dumps(lock, 2)
+ newLock = pickle.loads(newPickle)
+
+if threading is None:
+ SynchronizationTestCase.testThreadedSynchronization.skip = "Platform lacks thread support"
+ SerializationTestCase.testPickling.skip = "Platform lacks thread support"
diff --git a/twisted/test/test_threadpool.py b/twisted/test/test_threadpool.py
new file mode 100644
index 0000000..3b1ff83
--- /dev/null
+++ b/twisted/test/test_threadpool.py
@@ -0,0 +1,526 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.threadpool}
+"""
+
+import pickle, time, weakref, gc, threading
+
+from twisted.trial import unittest
+from twisted.python import threadpool, threadable, failure, context
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred
+
+#
+# See the end of this module for the remainder of the imports.
+#
+
+class Synchronization(object):
+ failures = 0
+
+ def __init__(self, N, waiting):
+ self.N = N
+ self.waiting = waiting
+ self.lock = threading.Lock()
+ self.runs = []
+
+ def run(self):
+ # This is the testy part: this is supposed to be invoked
+ # serially from multiple threads. If that is actually the
+ # case, we will never fail to acquire this lock. If it is
+ # *not* the case, we might get here while someone else is
+ # holding the lock.
+ if self.lock.acquire(False):
+ if not len(self.runs) % 5:
+ time.sleep(0.0002) # Constant selected based on
+ # empirical data to maximize the
+ # chance of a quick failure if this
+ # code is broken.
+ self.lock.release()
+ else:
+ self.failures += 1
+
+ # This is just the only way I can think of to wake up the test
+ # method. It doesn't actually have anything to do with the
+ # test.
+ self.lock.acquire()
+ self.runs.append(None)
+ if len(self.runs) == self.N:
+ self.waiting.release()
+ self.lock.release()
+
+ synchronized = ["run"]
+threadable.synchronize(Synchronization)
+
+
+
+class ThreadPoolTestCase(unittest.TestCase):
+ """
+ Test threadpools.
+ """
+ def _waitForLock(self, lock):
+ for i in xrange(1000000):
+ if lock.acquire(False):
+ break
+ time.sleep(1e-5)
+ else:
+ self.fail("A long time passed without succeeding")
+
+
+ def test_attributes(self):
+ """
+ L{ThreadPool.min} and L{ThreadPool.max} are set to the values passed to
+ L{ThreadPool.__init__}.
+ """
+ pool = threadpool.ThreadPool(12, 22)
+ self.assertEqual(pool.min, 12)
+ self.assertEqual(pool.max, 22)
+
+
+ def test_start(self):
+ """
+ L{ThreadPool.start} creates the minimum number of threads specified.
+ """
+ pool = threadpool.ThreadPool(0, 5)
+ pool.start()
+ self.addCleanup(pool.stop)
+ self.assertEqual(len(pool.threads), 0)
+
+ pool = threadpool.ThreadPool(3, 10)
+ self.assertEqual(len(pool.threads), 0)
+ pool.start()
+ self.addCleanup(pool.stop)
+ self.assertEqual(len(pool.threads), 3)
+
+
+ def test_threadCreationArguments(self):
+ """
+ Test that creating threads in the threadpool with application-level
+ objects as arguments doesn't results in those objects never being
+ freed, with the thread maintaining a reference to them as long as it
+ exists.
+ """
+ tp = threadpool.ThreadPool(0, 1)
+ tp.start()
+ self.addCleanup(tp.stop)
+
+ # Sanity check - no threads should have been started yet.
+ self.assertEqual(tp.threads, [])
+
+ # Here's our function
+ def worker(arg):
+ pass
+ # weakref needs an object subclass
+ class Dumb(object):
+ pass
+ # And here's the unique object
+ unique = Dumb()
+
+ workerRef = weakref.ref(worker)
+ uniqueRef = weakref.ref(unique)
+
+ # Put some work in
+ tp.callInThread(worker, unique)
+
+ # Add an event to wait completion
+ event = threading.Event()
+ tp.callInThread(event.set)
+ event.wait(self.getTimeout())
+
+ del worker
+ del unique
+ gc.collect()
+ self.assertEqual(uniqueRef(), None)
+ self.assertEqual(workerRef(), None)
+
+
+ def test_threadCreationArgumentsCallInThreadWithCallback(self):
+ """
+ As C{test_threadCreationArguments} above, but for
+ callInThreadWithCallback.
+ """
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.start()
+ self.addCleanup(tp.stop)
+
+ # Sanity check - no threads should have been started yet.
+ self.assertEqual(tp.threads, [])
+
+ # this holds references obtained in onResult
+ refdict = {} # name -> ref value
+
+ onResultWait = threading.Event()
+ onResultDone = threading.Event()
+
+ resultRef = []
+
+ # result callback
+ def onResult(success, result):
+ onResultWait.wait(self.getTimeout())
+ refdict['workerRef'] = workerRef()
+ refdict['uniqueRef'] = uniqueRef()
+ onResultDone.set()
+ resultRef.append(weakref.ref(result))
+
+ # Here's our function
+ def worker(arg, test):
+ return Dumb()
+
+ # weakref needs an object subclass
+ class Dumb(object):
+ pass
+
+ # And here's the unique object
+ unique = Dumb()
+
+ onResultRef = weakref.ref(onResult)
+ workerRef = weakref.ref(worker)
+ uniqueRef = weakref.ref(unique)
+
+ # Put some work in
+ tp.callInThreadWithCallback(onResult, worker, unique, test=unique)
+
+ del worker
+ del unique
+ gc.collect()
+
+ # let onResult collect the refs
+ onResultWait.set()
+ # wait for onResult
+ onResultDone.wait(self.getTimeout())
+
+ self.assertEqual(uniqueRef(), None)
+ self.assertEqual(workerRef(), None)
+
+ # XXX There's a race right here - has onResult in the worker thread
+ # returned and the locals in _worker holding it and the result been
+ # deleted yet?
+
+ del onResult
+ gc.collect()
+ self.assertEqual(onResultRef(), None)
+ self.assertEqual(resultRef[0](), None)
+
+
+ def test_persistence(self):
+ """
+ Threadpools can be pickled and unpickled, which should preserve the
+ number of threads and other parameters.
+ """
+ pool = threadpool.ThreadPool(7, 20)
+
+ self.assertEqual(pool.min, 7)
+ self.assertEqual(pool.max, 20)
+
+ # check that unpickled threadpool has same number of threads
+ copy = pickle.loads(pickle.dumps(pool))
+
+ self.assertEqual(copy.min, 7)
+ self.assertEqual(copy.max, 20)
+
+
+ def _threadpoolTest(self, method):
+ """
+ Test synchronization of calls made with C{method}, which should be
+ one of the mechanisms of the threadpool to execute work in threads.
+ """
+ # This is a schizophrenic test: it seems to be trying to test
+ # both the callInThread()/dispatch() behavior of the ThreadPool as well
+ # as the serialization behavior of threadable.synchronize(). It
+ # would probably make more sense as two much simpler tests.
+ N = 10
+
+ tp = threadpool.ThreadPool()
+ tp.start()
+ self.addCleanup(tp.stop)
+
+ waiting = threading.Lock()
+ waiting.acquire()
+ actor = Synchronization(N, waiting)
+
+ for i in xrange(N):
+ method(tp, actor)
+
+ self._waitForLock(waiting)
+
+ self.failIf(actor.failures, "run() re-entered %d times" %
+ (actor.failures,))
+
+
+ def test_callInThread(self):
+ """
+ Call C{_threadpoolTest} with C{callInThread}.
+ """
+ return self._threadpoolTest(
+ lambda tp, actor: tp.callInThread(actor.run))
+
+
+ def test_callInThreadException(self):
+ """
+ L{ThreadPool.callInThread} logs exceptions raised by the callable it
+ is passed.
+ """
+ class NewError(Exception):
+ pass
+
+ def raiseError():
+ raise NewError()
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThread(raiseError)
+ tp.start()
+ tp.stop()
+
+ errors = self.flushLoggedErrors(NewError)
+ self.assertEqual(len(errors), 1)
+
+
+ def test_callInThreadWithCallback(self):
+ """
+ L{ThreadPool.callInThreadWithCallback} calls C{onResult} with a
+ two-tuple of C{(True, result)} where C{result} is the value returned
+ by the callable supplied.
+ """
+ waiter = threading.Lock()
+ waiter.acquire()
+
+ results = []
+
+ def onResult(success, result):
+ waiter.release()
+ results.append(success)
+ results.append(result)
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThreadWithCallback(onResult, lambda : "test")
+ tp.start()
+
+ try:
+ self._waitForLock(waiter)
+ finally:
+ tp.stop()
+
+ self.assertTrue(results[0])
+ self.assertEqual(results[1], "test")
+
+
+ def test_callInThreadWithCallbackExceptionInCallback(self):
+ """
+ L{ThreadPool.callInThreadWithCallback} calls C{onResult} with a
+ two-tuple of C{(False, failure)} where C{failure} represents the
+ exception raised by the callable supplied.
+ """
+ class NewError(Exception):
+ pass
+
+ def raiseError():
+ raise NewError()
+
+ waiter = threading.Lock()
+ waiter.acquire()
+
+ results = []
+
+ def onResult(success, result):
+ waiter.release()
+ results.append(success)
+ results.append(result)
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThreadWithCallback(onResult, raiseError)
+ tp.start()
+
+ try:
+ self._waitForLock(waiter)
+ finally:
+ tp.stop()
+
+ self.assertFalse(results[0])
+ self.assertTrue(isinstance(results[1], failure.Failure))
+ self.assertTrue(issubclass(results[1].type, NewError))
+
+
+ def test_callInThreadWithCallbackExceptionInOnResult(self):
+ """
+ L{ThreadPool.callInThreadWithCallback} logs the exception raised by
+ C{onResult}.
+ """
+ class NewError(Exception):
+ pass
+
+ waiter = threading.Lock()
+ waiter.acquire()
+
+ results = []
+
+ def onResult(success, result):
+ results.append(success)
+ results.append(result)
+ raise NewError()
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThreadWithCallback(onResult, lambda : None)
+ tp.callInThread(waiter.release)
+ tp.start()
+
+ try:
+ self._waitForLock(waiter)
+ finally:
+ tp.stop()
+
+ errors = self.flushLoggedErrors(NewError)
+ self.assertEqual(len(errors), 1)
+
+ self.assertTrue(results[0])
+ self.assertEqual(results[1], None)
+
+
+ def test_callbackThread(self):
+ """
+ L{ThreadPool.callInThreadWithCallback} calls the function it is
+ given and the C{onResult} callback in the same thread.
+ """
+ threadIds = []
+
+ import thread
+
+ event = threading.Event()
+
+ def onResult(success, result):
+ threadIds.append(thread.get_ident())
+ event.set()
+
+ def func():
+ threadIds.append(thread.get_ident())
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThreadWithCallback(onResult, func)
+ tp.start()
+ self.addCleanup(tp.stop)
+
+ event.wait(self.getTimeout())
+ self.assertEqual(len(threadIds), 2)
+ self.assertEqual(threadIds[0], threadIds[1])
+
+
+ def test_callbackContext(self):
+ """
+ The context L{ThreadPool.callInThreadWithCallback} is invoked in is
+ shared by the context the callable and C{onResult} callback are
+ invoked in.
+ """
+ myctx = context.theContextTracker.currentContext().contexts[-1]
+ myctx['testing'] = 'this must be present'
+
+ contexts = []
+
+ event = threading.Event()
+
+ def onResult(success, result):
+ ctx = context.theContextTracker.currentContext().contexts[-1]
+ contexts.append(ctx)
+ event.set()
+
+ def func():
+ ctx = context.theContextTracker.currentContext().contexts[-1]
+ contexts.append(ctx)
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThreadWithCallback(onResult, func)
+ tp.start()
+ self.addCleanup(tp.stop)
+
+ event.wait(self.getTimeout())
+
+ self.assertEqual(len(contexts), 2)
+ self.assertEqual(myctx, contexts[0])
+ self.assertEqual(myctx, contexts[1])
+
+
+ def test_existingWork(self):
+ """
+ Work added to the threadpool before its start should be executed once
+ the threadpool is started: this is ensured by trying to release a lock
+ previously acquired.
+ """
+ waiter = threading.Lock()
+ waiter.acquire()
+
+ tp = threadpool.ThreadPool(0, 1)
+ tp.callInThread(waiter.release) # before start()
+ tp.start()
+
+ try:
+ self._waitForLock(waiter)
+ finally:
+ tp.stop()
+
+
+
+class RaceConditionTestCase(unittest.TestCase):
+ def setUp(self):
+ self.event = threading.Event()
+ self.threadpool = threadpool.ThreadPool(0, 10)
+ self.threadpool.start()
+
+
+ def tearDown(self):
+ del self.event
+ self.threadpool.stop()
+ del self.threadpool
+
+
+ def test_synchronization(self):
+ """
+ Test a race condition: ensure that actions run in the pool synchronize
+ with actions run in the main thread.
+ """
+ timeout = self.getTimeout()
+ self.threadpool.callInThread(self.event.set)
+ self.event.wait(timeout)
+ self.event.clear()
+ for i in range(3):
+ self.threadpool.callInThread(self.event.wait)
+ self.threadpool.callInThread(self.event.set)
+ self.event.wait(timeout)
+ if not self.event.isSet():
+ self.event.set()
+ self.fail("Actions not synchronized")
+
+
+ def test_singleThread(self):
+ """
+ The submission of a new job to a thread pool in response to the
+ C{onResult} callback does not cause a new thread to be added to the
+ thread pool.
+
+ This requires that the thread which calls C{onResult} to have first
+ marked itself as available so that when the new job is queued, that
+ thread may be considered to run it. This is desirable so that when
+ only N jobs are ever being executed in the thread pool at once only
+ N threads will ever be created.
+ """
+ # Ensure no threads running
+ self.assertEqual(self.threadpool.workers, 0)
+
+ loopDeferred = Deferred()
+
+ def onResult(success, counter):
+ reactor.callFromThread(submit, counter)
+
+ def submit(counter):
+ if counter:
+ self.threadpool.callInThreadWithCallback(
+ onResult, lambda: counter - 1)
+ else:
+ loopDeferred.callback(None)
+
+ def cbLoop(ignored):
+ # Ensure there is only one thread running.
+ self.assertEqual(self.threadpool.workers, 1)
+
+ loopDeferred.addCallback(cbLoop)
+ submit(10)
+ return loopDeferred
diff --git a/twisted/test/test_threads.py b/twisted/test/test_threads.py
new file mode 100644
index 0000000..e1ddd82
--- /dev/null
+++ b/twisted/test/test_threads.py
@@ -0,0 +1,412 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Test methods in twisted.internet.threads and reactor thread APIs.
+"""
+
+import sys, os, time
+
+from twisted.trial import unittest
+
+from twisted.internet import reactor, defer, interfaces, threads, protocol, error
+from twisted.python import failure, threadable, log, threadpool
+
+
+
+class ReactorThreadsTestCase(unittest.TestCase):
+ """
+ Tests for the reactor threading API.
+ """
+
+ def test_suggestThreadPoolSize(self):
+ """
+ Try to change maximum number of threads.
+ """
+ reactor.suggestThreadPoolSize(34)
+ self.assertEqual(reactor.threadpool.max, 34)
+ reactor.suggestThreadPoolSize(4)
+ self.assertEqual(reactor.threadpool.max, 4)
+
+
+ def _waitForThread(self):
+ """
+ The reactor's threadpool is only available when the reactor is running,
+ so to have a sane behavior during the tests we make a dummy
+ L{threads.deferToThread} call.
+ """
+ return threads.deferToThread(time.sleep, 0)
+
+
+ def test_callInThread(self):
+ """
+ Test callInThread functionality: set a C{threading.Event}, and check
+ that it's not in the main thread.
+ """
+ def cb(ign):
+ waiter = threading.Event()
+ result = []
+ def threadedFunc():
+ result.append(threadable.isInIOThread())
+ waiter.set()
+
+ reactor.callInThread(threadedFunc)
+ waiter.wait(120)
+ if not waiter.isSet():
+ self.fail("Timed out waiting for event.")
+ else:
+ self.assertEqual(result, [False])
+ return self._waitForThread().addCallback(cb)
+
+
+ def test_callFromThread(self):
+ """
+ Test callFromThread functionality: from the main thread, and from
+ another thread.
+ """
+ def cb(ign):
+ firedByReactorThread = defer.Deferred()
+ firedByOtherThread = defer.Deferred()
+
+ def threadedFunc():
+ reactor.callFromThread(firedByOtherThread.callback, None)
+
+ reactor.callInThread(threadedFunc)
+ reactor.callFromThread(firedByReactorThread.callback, None)
+
+ return defer.DeferredList(
+ [firedByReactorThread, firedByOtherThread],
+ fireOnOneErrback=True)
+ return self._waitForThread().addCallback(cb)
+
+
+ def test_wakerOverflow(self):
+ """
+ Try to make an overflow on the reactor waker using callFromThread.
+ """
+ def cb(ign):
+ self.failure = None
+ waiter = threading.Event()
+ def threadedFunction():
+ # Hopefully a hundred thousand queued calls is enough to
+ # trigger the error condition
+ for i in xrange(100000):
+ try:
+ reactor.callFromThread(lambda: None)
+ except:
+ self.failure = failure.Failure()
+ break
+ waiter.set()
+ reactor.callInThread(threadedFunction)
+ waiter.wait(120)
+ if not waiter.isSet():
+ self.fail("Timed out waiting for event")
+ if self.failure is not None:
+ return defer.fail(self.failure)
+ return self._waitForThread().addCallback(cb)
+
+ def _testBlockingCallFromThread(self, reactorFunc):
+ """
+ Utility method to test L{threads.blockingCallFromThread}.
+ """
+ waiter = threading.Event()
+ results = []
+ errors = []
+ def cb1(ign):
+ def threadedFunc():
+ try:
+ r = threads.blockingCallFromThread(reactor, reactorFunc)
+ except Exception, e:
+ errors.append(e)
+ else:
+ results.append(r)
+ waiter.set()
+
+ reactor.callInThread(threadedFunc)
+ return threads.deferToThread(waiter.wait, self.getTimeout())
+
+ def cb2(ign):
+ if not waiter.isSet():
+ self.fail("Timed out waiting for event")
+ return results, errors
+
+ return self._waitForThread().addCallback(cb1).addBoth(cb2)
+
+ def test_blockingCallFromThread(self):
+ """
+ Test blockingCallFromThread facility: create a thread, call a function
+ in the reactor using L{threads.blockingCallFromThread}, and verify the
+ result returned.
+ """
+ def reactorFunc():
+ return defer.succeed("foo")
+ def cb(res):
+ self.assertEqual(res[0][0], "foo")
+
+ return self._testBlockingCallFromThread(reactorFunc).addCallback(cb)
+
+ def test_asyncBlockingCallFromThread(self):
+ """
+ Test blockingCallFromThread as above, but be sure the resulting
+ Deferred is not already fired.
+ """
+ def reactorFunc():
+ d = defer.Deferred()
+ reactor.callLater(0.1, d.callback, "egg")
+ return d
+ def cb(res):
+ self.assertEqual(res[0][0], "egg")
+
+ return self._testBlockingCallFromThread(reactorFunc).addCallback(cb)
+
+ def test_errorBlockingCallFromThread(self):
+ """
+ Test error report for blockingCallFromThread.
+ """
+ def reactorFunc():
+ return defer.fail(RuntimeError("bar"))
+ def cb(res):
+ self.assert_(isinstance(res[1][0], RuntimeError))
+ self.assertEqual(res[1][0].args[0], "bar")
+
+ return self._testBlockingCallFromThread(reactorFunc).addCallback(cb)
+
+ def test_asyncErrorBlockingCallFromThread(self):
+ """
+ Test error report for blockingCallFromThread as above, but be sure the
+ resulting Deferred is not already fired.
+ """
+ def reactorFunc():
+ d = defer.Deferred()
+ reactor.callLater(0.1, d.errback, RuntimeError("spam"))
+ return d
+ def cb(res):
+ self.assert_(isinstance(res[1][0], RuntimeError))
+ self.assertEqual(res[1][0].args[0], "spam")
+
+ return self._testBlockingCallFromThread(reactorFunc).addCallback(cb)
+
+
+class Counter:
+ index = 0
+ problem = 0
+
+ def add(self):
+ """A non thread-safe method."""
+ next = self.index + 1
+ # another thread could jump in here and increment self.index on us
+ if next != self.index + 1:
+ self.problem = 1
+ raise ValueError
+ # or here, same issue but we wouldn't catch it. We'd overwrite
+ # their results, and the index will have lost a count. If
+ # several threads get in here, we will actually make the count
+ # go backwards when we overwrite it.
+ self.index = next
+
+
+
+class DeferredResultTestCase(unittest.TestCase):
+ """
+ Test twisted.internet.threads.
+ """
+
+ def setUp(self):
+ reactor.suggestThreadPoolSize(8)
+
+
+ def tearDown(self):
+ reactor.suggestThreadPoolSize(0)
+
+
+ def testCallMultiple(self):
+ L = []
+ N = 10
+ d = defer.Deferred()
+
+ def finished():
+ self.assertEqual(L, range(N))
+ d.callback(None)
+
+ threads.callMultipleInThread([
+ (L.append, (i,), {}) for i in xrange(N)
+ ] + [(reactor.callFromThread, (finished,), {})])
+ return d
+
+
+ def test_deferredResult(self):
+ """
+ L{threads.deferToThread} executes the function passed, and correctly
+ handles the positional and keyword arguments given.
+ """
+ d = threads.deferToThread(lambda x, y=5: x + y, 3, y=4)
+ d.addCallback(self.assertEqual, 7)
+ return d
+
+
+ def test_deferredFailure(self):
+ """
+ Check that L{threads.deferToThread} return a failure object
+ with an appropriate exception instance when the called
+ function raises an exception.
+ """
+ class NewError(Exception):
+ pass
+ def raiseError():
+ raise NewError()
+ d = threads.deferToThread(raiseError)
+ return self.assertFailure(d, NewError)
+
+
+ def test_deferredFailureAfterSuccess(self):
+ """
+ Check that a successfull L{threads.deferToThread} followed by a one
+ that raises an exception correctly result as a failure.
+ """
+ # set up a condition that causes cReactor to hang. These conditions
+ # can also be set by other tests when the full test suite is run in
+ # alphabetical order (test_flow.FlowTest.testThreaded followed by
+ # test_internet.ReactorCoreTestCase.testStop, to be precise). By
+ # setting them up explicitly here, we can reproduce the hang in a
+ # single precise test case instead of depending upon side effects of
+ # other tests.
+ #
+ # alas, this test appears to flunk the default reactor too
+
+ d = threads.deferToThread(lambda: None)
+ d.addCallback(lambda ign: threads.deferToThread(lambda: 1/0))
+ return self.assertFailure(d, ZeroDivisionError)
+
+
+
+class DeferToThreadPoolTestCase(unittest.TestCase):
+ """
+ Test L{twisted.internet.threads.deferToThreadPool}.
+ """
+
+ def setUp(self):
+ self.tp = threadpool.ThreadPool(0, 8)
+ self.tp.start()
+
+
+ def tearDown(self):
+ self.tp.stop()
+
+
+ def test_deferredResult(self):
+ """
+ L{threads.deferToThreadPool} executes the function passed, and
+ correctly handles the positional and keyword arguments given.
+ """
+ d = threads.deferToThreadPool(reactor, self.tp,
+ lambda x, y=5: x + y, 3, y=4)
+ d.addCallback(self.assertEqual, 7)
+ return d
+
+
+ def test_deferredFailure(self):
+ """
+ Check that L{threads.deferToThreadPool} return a failure object with an
+ appropriate exception instance when the called function raises an
+ exception.
+ """
+ class NewError(Exception):
+ pass
+ def raiseError():
+ raise NewError()
+ d = threads.deferToThreadPool(reactor, self.tp, raiseError)
+ return self.assertFailure(d, NewError)
+
+
+
+_callBeforeStartupProgram = """
+import time
+import %(reactor)s
+%(reactor)s.install()
+
+from twisted.internet import reactor
+
+def threadedCall():
+ print 'threaded call'
+
+reactor.callInThread(threadedCall)
+
+# Spin very briefly to try to give the thread a chance to run, if it
+# is going to. Is there a better way to achieve this behavior?
+for i in xrange(100):
+ time.sleep(0.0)
+"""
+
+
+class ThreadStartupProcessProtocol(protocol.ProcessProtocol):
+ def __init__(self, finished):
+ self.finished = finished
+ self.out = []
+ self.err = []
+
+ def outReceived(self, out):
+ self.out.append(out)
+
+ def errReceived(self, err):
+ self.err.append(err)
+
+ def processEnded(self, reason):
+ self.finished.callback((self.out, self.err, reason))
+
+
+
+class StartupBehaviorTestCase(unittest.TestCase):
+ """
+ Test cases for the behavior of the reactor threadpool near startup
+ boundary conditions.
+
+ In particular, this asserts that no threaded calls are attempted
+ until the reactor starts up, that calls attempted before it starts
+ are in fact executed once it has started, and that in both cases,
+ the reactor properly cleans itself up (which is tested for
+ somewhat implicitly, by requiring a child process be able to exit,
+ something it cannot do unless the threadpool has been properly
+ torn down).
+ """
+
+
+ def testCallBeforeStartupUnexecuted(self):
+ progname = self.mktemp()
+ progfile = file(progname, 'w')
+ progfile.write(_callBeforeStartupProgram % {'reactor': reactor.__module__})
+ progfile.close()
+
+ def programFinished((out, err, reason)):
+ if reason.check(error.ProcessTerminated):
+ self.fail("Process did not exit cleanly (out: %s err: %s)" % (out, err))
+
+ if err:
+ log.msg("Unexpected output on standard error: %s" % (err,))
+ self.failIf(out, "Expected no output, instead received:\n%s" % (out,))
+
+ def programTimeout(err):
+ err.trap(error.TimeoutError)
+ proto.signalProcess('KILL')
+ return err
+
+ env = os.environ.copy()
+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
+ d = defer.Deferred().addCallbacks(programFinished, programTimeout)
+ proto = ThreadStartupProcessProtocol(d)
+ reactor.spawnProcess(proto, sys.executable, ('python', progname), env)
+ return d
+
+
+
+if interfaces.IReactorThreads(reactor, None) is None:
+ for cls in (ReactorThreadsTestCase,
+ DeferredResultTestCase,
+ StartupBehaviorTestCase):
+ cls.skip = "No thread support, nothing to test here."
+else:
+ import threading
+
+if interfaces.IReactorProcess(reactor, None) is None:
+ for cls in (StartupBehaviorTestCase,):
+ cls.skip = "No process support, cannot run subprocess thread tests."
diff --git a/twisted/test/test_timehelpers.py b/twisted/test/test_timehelpers.py
new file mode 100644
index 0000000..c78335c
--- /dev/null
+++ b/twisted/test/test_timehelpers.py
@@ -0,0 +1,31 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the deprecated L{twisted.test.time_helpers} module.
+"""
+
+import sys
+
+from twisted.trial.unittest import TestCase
+
+
+class TimeHelpersTests(TestCase):
+ """
+ A test for the deprecation of the module.
+ """
+ def test_deprecated(self):
+ """
+ Importing L{twisted.test.time_helpers} causes a deprecation warning
+ to be emitted.
+ """
+ # Make sure we're really importing it
+ sys.modules.pop('twisted.test.time_helpers', None)
+ import twisted.test.time_helpers
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.test.time_helpers is deprecated since Twisted 10.0. "
+ "See twisted.internet.task.Clock instead.")
diff --git a/twisted/test/test_tpfile.py b/twisted/test/test_tpfile.py
new file mode 100644
index 0000000..655a166
--- /dev/null
+++ b/twisted/test/test_tpfile.py
@@ -0,0 +1,52 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+from twisted.protocols import loopback
+from twisted.protocols import basic
+from twisted.internet import protocol, abstract
+
+import StringIO
+
+class BufferingServer(protocol.Protocol):
+ buffer = ''
+ def dataReceived(self, data):
+ self.buffer += data
+
+class FileSendingClient(protocol.Protocol):
+ def __init__(self, f):
+ self.f = f
+
+ def connectionMade(self):
+ s = basic.FileSender()
+ d = s.beginFileTransfer(self.f, self.transport, lambda x: x)
+ d.addCallback(lambda r: self.transport.loseConnection())
+
+class FileSenderTestCase(unittest.TestCase):
+ def testSendingFile(self):
+ testStr = 'xyz' * 100 + 'abc' * 100 + '123' * 100
+ s = BufferingServer()
+ c = FileSendingClient(StringIO.StringIO(testStr))
+
+ d = loopback.loopbackTCP(s, c)
+ d.addCallback(lambda x : self.assertEqual(s.buffer, testStr))
+ return d
+
+ def testSendingEmptyFile(self):
+ fileSender = basic.FileSender()
+ consumer = abstract.FileDescriptor()
+ consumer.connected = 1
+ emptyFile = StringIO.StringIO('')
+
+ d = fileSender.beginFileTransfer(emptyFile, consumer, lambda x: x)
+
+ # The producer will be immediately exhausted, and so immediately
+ # unregistered
+ self.assertEqual(consumer.producer, None)
+
+ # Which means the Deferred from FileSender should have been called
+ self.failUnless(d.called,
+ 'producer unregistered with deferred being called')
+
diff --git a/twisted/test/test_twistd.py b/twisted/test/test_twistd.py
new file mode 100644
index 0000000..d8ae688
--- /dev/null
+++ b/twisted/test/test_twistd.py
@@ -0,0 +1,1549 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.application.app} and L{twisted.scripts.twistd}.
+"""
+
+import signal, inspect, errno
+
+import os, sys, StringIO
+
+try:
+ import pwd, grp
+except ImportError:
+ pwd = grp = None
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+from twisted.test.test_process import MockOS
+
+from twisted import plugin
+from twisted.application.service import IServiceMaker
+from twisted.application import service, app, reactors
+from twisted.scripts import twistd
+from twisted.python import log
+from twisted.python.usage import UsageError
+from twisted.python.log import ILogObserver
+from twisted.python.versions import Version
+from twisted.python.components import Componentized
+from twisted.internet.defer import Deferred
+from twisted.internet.interfaces import IReactorDaemonize
+from twisted.python.fakepwd import UserDatabase
+
+try:
+ from twisted.python import syslog
+except ImportError:
+ syslog = None
+
+try:
+ from twisted.scripts import _twistd_unix
+except ImportError:
+ _twistd_unix = None
+else:
+ from twisted.scripts._twistd_unix import UnixApplicationRunner
+ from twisted.scripts._twistd_unix import UnixAppLogger
+
+try:
+ import profile
+except ImportError:
+ profile = None
+
+try:
+ import hotshot
+ import hotshot.stats
+except (ImportError, SystemExit):
+ # For some reasons, hotshot.stats seems to raise SystemExit on some
+ # distributions, probably when considered non-free. See the import of
+ # this module in twisted.application.app for more details.
+ hotshot = None
+
+try:
+ import pstats
+ import cProfile
+except ImportError:
+ cProfile = None
+
+if getattr(os, 'setuid', None) is None:
+ setuidSkip = "Platform does not support --uid/--gid twistd options."
+else:
+ setuidSkip = None
+
+
+def patchUserDatabase(patch, user, uid, group, gid):
+ """
+ Patch L{pwd.getpwnam} so that it behaves as though only one user exists
+ and patch L{grp.getgrnam} so that it behaves as though only one group
+ exists.
+
+ @param patch: A function like L{TestCase.patch} which will be used to
+ install the fake implementations.
+
+ @type user: C{str}
+ @param user: The name of the single user which will exist.
+
+ @type uid: C{int}
+ @param uid: The UID of the single user which will exist.
+
+ @type group: C{str}
+ @param group: The name of the single user which will exist.
+
+ @type gid: C{int}
+ @param gid: The GID of the single group which will exist.
+ """
+ # Try not to be an unverified fake, but try not to depend on quirks of
+ # the system either (eg, run as a process with a uid and gid which
+ # equal each other, and so doesn't reliably test that uid is used where
+ # uid should be used and gid is used where gid should be used). -exarkun
+ pwent = pwd.getpwuid(os.getuid())
+ grent = grp.getgrgid(os.getgid())
+
+ database = UserDatabase()
+ database.addUser(
+ user, pwent.pw_passwd, uid, pwent.pw_gid,
+ pwent.pw_gecos, pwent.pw_dir, pwent.pw_shell)
+
+ def getgrnam(name):
+ result = list(grent)
+ result[result.index(grent.gr_name)] = group
+ result[result.index(grent.gr_gid)] = gid
+ result = tuple(result)
+ return {group: result}[name]
+
+ patch(pwd, "getpwnam", database.getpwnam)
+ patch(grp, "getgrnam", getgrnam)
+
+
+
+class MockServiceMaker(object):
+ """
+ A non-implementation of L{twisted.application.service.IServiceMaker}.
+ """
+ tapname = 'ueoa'
+
+ def makeService(self, options):
+ """
+ Take a L{usage.Options} instance and return a
+ L{service.IService} provider.
+ """
+ self.options = options
+ self.service = service.Service()
+ return self.service
+
+
+
+class CrippledAppLogger(app.AppLogger):
+ """
+ @see: CrippledApplicationRunner.
+ """
+
+ def start(self, application):
+ pass
+
+
+
+class CrippledApplicationRunner(twistd._SomeApplicationRunner):
+ """
+ An application runner that cripples the platform-specific runner and
+ nasty side-effect-having code so that we can use it without actually
+ running any environment-affecting code.
+ """
+ loggerFactory = CrippledAppLogger
+
+ def preApplication(self):
+ pass
+
+
+ def postApplication(self):
+ pass
+
+
+
+class ServerOptionsTest(unittest.TestCase):
+ """
+ Non-platform-specific tests for the pltaform-specific ServerOptions class.
+ """
+ def test_subCommands(self):
+ """
+ subCommands is built from IServiceMaker plugins, and is sorted
+ alphabetically.
+ """
+ class FakePlugin(object):
+ def __init__(self, name):
+ self.tapname = name
+ self._options = 'options for ' + name
+ self.description = 'description of ' + name
+
+ def options(self):
+ return self._options
+
+ apple = FakePlugin('apple')
+ banana = FakePlugin('banana')
+ coconut = FakePlugin('coconut')
+ donut = FakePlugin('donut')
+
+ def getPlugins(interface):
+ self.assertEqual(interface, IServiceMaker)
+ yield coconut
+ yield banana
+ yield donut
+ yield apple
+
+ config = twistd.ServerOptions()
+ self.assertEqual(config._getPlugins, plugin.getPlugins)
+ config._getPlugins = getPlugins
+
+ # "subCommands is a list of 4-tuples of (command name, command
+ # shortcut, parser class, documentation)."
+ subCommands = config.subCommands
+ expectedOrder = [apple, banana, coconut, donut]
+
+ for subCommand, expectedCommand in zip(subCommands, expectedOrder):
+ name, shortcut, parserClass, documentation = subCommand
+ self.assertEqual(name, expectedCommand.tapname)
+ self.assertEqual(shortcut, None)
+ self.assertEqual(parserClass(), expectedCommand._options),
+ self.assertEqual(documentation, expectedCommand.description)
+
+
+ def test_sortedReactorHelp(self):
+ """
+ Reactor names are listed alphabetically by I{--help-reactors}.
+ """
+ class FakeReactorInstaller(object):
+ def __init__(self, name):
+ self.shortName = 'name of ' + name
+ self.description = 'description of ' + name
+
+ apple = FakeReactorInstaller('apple')
+ banana = FakeReactorInstaller('banana')
+ coconut = FakeReactorInstaller('coconut')
+ donut = FakeReactorInstaller('donut')
+
+ def getReactorTypes():
+ yield coconut
+ yield banana
+ yield donut
+ yield apple
+
+ config = twistd.ServerOptions()
+ self.assertEqual(config._getReactorTypes, reactors.getReactorTypes)
+ config._getReactorTypes = getReactorTypes
+ config.messageOutput = StringIO.StringIO()
+
+ self.assertRaises(SystemExit, config.parseOptions, ['--help-reactors'])
+ helpOutput = config.messageOutput.getvalue()
+ indexes = []
+ for reactor in apple, banana, coconut, donut:
+ def getIndex(s):
+ self.assertIn(s, helpOutput)
+ indexes.append(helpOutput.index(s))
+
+ getIndex(reactor.shortName)
+ getIndex(reactor.description)
+
+ self.assertEqual(
+ indexes, sorted(indexes),
+ 'reactor descriptions were not in alphabetical order: %r' % (
+ helpOutput,))
+
+
+ def test_postOptionsSubCommandCausesNoSave(self):
+ """
+ postOptions should set no_save to True when a subcommand is used.
+ """
+ config = twistd.ServerOptions()
+ config.subCommand = 'ueoa'
+ config.postOptions()
+ self.assertEqual(config['no_save'], True)
+
+
+ def test_postOptionsNoSubCommandSavesAsUsual(self):
+ """
+ If no sub command is used, postOptions should not touch no_save.
+ """
+ config = twistd.ServerOptions()
+ config.postOptions()
+ self.assertEqual(config['no_save'], False)
+
+
+ def test_listAllProfilers(self):
+ """
+ All the profilers that can be used in L{app.AppProfiler} are listed in
+ the help output.
+ """
+ config = twistd.ServerOptions()
+ helpOutput = str(config)
+ for profiler in app.AppProfiler.profilers:
+ self.assertIn(profiler, helpOutput)
+
+
+ def test_defaultUmask(self):
+ """
+ The default value for the C{umask} option is C{None}.
+ """
+ config = twistd.ServerOptions()
+ self.assertEqual(config['umask'], None)
+
+
+ def test_umask(self):
+ """
+ The value given for the C{umask} option is parsed as an octal integer
+ literal.
+ """
+ config = twistd.ServerOptions()
+ config.parseOptions(['--umask', '123'])
+ self.assertEqual(config['umask'], 83)
+ config.parseOptions(['--umask', '0123'])
+ self.assertEqual(config['umask'], 83)
+
+
+ def test_invalidUmask(self):
+ """
+ If a value is given for the C{umask} option which cannot be parsed as
+ an integer, L{UsageError} is raised by L{ServerOptions.parseOptions}.
+ """
+ config = twistd.ServerOptions()
+ self.assertRaises(UsageError, config.parseOptions, ['--umask', 'abcdef'])
+
+ if _twistd_unix is None:
+ msg = "twistd unix not available"
+ test_defaultUmask.skip = test_umask.skip = test_invalidUmask.skip = msg
+
+
+ def test_unimportableConfiguredLogObserver(self):
+ """
+ C{--logger} with an unimportable module raises a L{UsageError}.
+ """
+ config = twistd.ServerOptions()
+ e = self.assertRaises(UsageError, config.parseOptions,
+ ['--logger', 'no.such.module.I.hope'])
+ self.assertTrue(e.args[0].startswith(
+ "Logger 'no.such.module.I.hope' could not be imported: "
+ "'no.such.module.I.hope' does not name an object"))
+ self.assertNotIn('\n', e.args[0])
+
+
+ def test_badAttributeWithConfiguredLogObserver(self):
+ """
+ C{--logger} with a non-existent object raises a L{UsageError}.
+ """
+ config = twistd.ServerOptions()
+ e = self.assertRaises(UsageError, config.parseOptions,
+ ["--logger", "twisted.test.test_twistd.FOOBAR"])
+ self.assertTrue(e.args[0].startswith(
+ "Logger 'twisted.test.test_twistd.FOOBAR' could not be "
+ "imported: 'module' object has no attribute 'FOOBAR'"))
+ self.assertNotIn('\n', e.args[0])
+
+
+
+class TapFileTest(unittest.TestCase):
+ """
+ Test twistd-related functionality that requires a tap file on disk.
+ """
+
+ def setUp(self):
+ """
+ Create a trivial Application and put it in a tap file on disk.
+ """
+ self.tapfile = self.mktemp()
+ f = file(self.tapfile, 'wb')
+ pickle.dump(service.Application("Hi!"), f)
+ f.close()
+
+
+ def test_createOrGetApplicationWithTapFile(self):
+ """
+ Ensure that the createOrGetApplication call that 'twistd -f foo.tap'
+ makes will load the Application out of foo.tap.
+ """
+ config = twistd.ServerOptions()
+ config.parseOptions(['-f', self.tapfile])
+ application = CrippledApplicationRunner(config).createOrGetApplication()
+ self.assertEqual(service.IService(application).name, 'Hi!')
+
+
+
+class TestLoggerFactory(object):
+ """
+ A logger factory for L{TestApplicationRunner}.
+ """
+
+ def __init__(self, runner):
+ self.runner = runner
+
+
+ def start(self, application):
+ """
+ Save the logging start on the C{runner} instance.
+ """
+ self.runner.order.append("log")
+ self.runner.hadApplicationLogObserver = hasattr(self.runner,
+ 'application')
+
+
+ def stop(self):
+ """
+ Don't log anything.
+ """
+
+
+
+class TestApplicationRunner(app.ApplicationRunner):
+ """
+ An ApplicationRunner which tracks the environment in which its methods are
+ called.
+ """
+
+ def __init__(self, options):
+ app.ApplicationRunner.__init__(self, options)
+ self.order = []
+ self.logger = TestLoggerFactory(self)
+
+
+ def preApplication(self):
+ self.order.append("pre")
+ self.hadApplicationPreApplication = hasattr(self, 'application')
+
+
+ def postApplication(self):
+ self.order.append("post")
+ self.hadApplicationPostApplication = hasattr(self, 'application')
+
+
+
+class ApplicationRunnerTest(unittest.TestCase):
+ """
+ Non-platform-specific tests for the platform-specific ApplicationRunner.
+ """
+ def setUp(self):
+ config = twistd.ServerOptions()
+ self.serviceMaker = MockServiceMaker()
+ # Set up a config object like it's been parsed with a subcommand
+ config.loadedPlugins = {'test_command': self.serviceMaker}
+ config.subOptions = object()
+ config.subCommand = 'test_command'
+ self.config = config
+
+
+ def test_applicationRunnerGetsCorrectApplication(self):
+ """
+ Ensure that a twistd plugin gets used in appropriate ways: it
+ is passed its Options instance, and the service it returns is
+ added to the application.
+ """
+ arunner = CrippledApplicationRunner(self.config)
+ arunner.run()
+
+ self.assertIdentical(
+ self.serviceMaker.options, self.config.subOptions,
+ "ServiceMaker.makeService needs to be passed the correct "
+ "sub Command object.")
+ self.assertIdentical(
+ self.serviceMaker.service,
+ service.IService(arunner.application).services[0],
+ "ServiceMaker.makeService's result needs to be set as a child "
+ "of the Application.")
+
+
+ def test_preAndPostApplication(self):
+ """
+ Test thet preApplication and postApplication methods are
+ called by ApplicationRunner.run() when appropriate.
+ """
+ s = TestApplicationRunner(self.config)
+ s.run()
+ self.assertFalse(s.hadApplicationPreApplication)
+ self.assertTrue(s.hadApplicationPostApplication)
+ self.assertTrue(s.hadApplicationLogObserver)
+ self.assertEqual(s.order, ["pre", "log", "post"])
+
+
+ def _applicationStartsWithConfiguredID(self, argv, uid, gid):
+ """
+ Assert that given a particular command line, an application is started
+ as a particular UID/GID.
+
+ @param argv: A list of strings giving the options to parse.
+ @param uid: An integer giving the expected UID.
+ @param gid: An integer giving the expected GID.
+ """
+ self.config.parseOptions(argv)
+
+ events = []
+ class FakeUnixApplicationRunner(twistd._SomeApplicationRunner):
+ def setupEnvironment(self, chroot, rundir, nodaemon, umask,
+ pidfile):
+ events.append('environment')
+
+ def shedPrivileges(self, euid, uid, gid):
+ events.append(('privileges', euid, uid, gid))
+
+ def startReactor(self, reactor, oldstdout, oldstderr):
+ events.append('reactor')
+
+ def removePID(self, pidfile):
+ pass
+
+
+ class FakeService(object):
+ implements(service.IService, service.IProcess)
+
+ processName = None
+ uid = None
+ gid = None
+
+ def setName(self, name):
+ pass
+
+ def setServiceParent(self, parent):
+ pass
+
+ def disownServiceParent(self):
+ pass
+
+ def privilegedStartService(self):
+ events.append('privilegedStartService')
+
+ def startService(self):
+ events.append('startService')
+
+ def stopService(self):
+ pass
+
+ application = FakeService()
+ verifyObject(service.IService, application)
+ verifyObject(service.IProcess, application)
+
+ runner = FakeUnixApplicationRunner(self.config)
+ runner.preApplication()
+ runner.application = application
+ runner.postApplication()
+
+ self.assertEqual(
+ events,
+ ['environment', 'privilegedStartService',
+ ('privileges', False, uid, gid), 'startService', 'reactor'])
+
+
+ def test_applicationStartsWithConfiguredNumericIDs(self):
+ """
+ L{postApplication} should change the UID and GID to the values
+ specified as numeric strings by the configuration after running
+ L{service.IService.privilegedStartService} and before running
+ L{service.IService.startService}.
+ """
+ uid = 1234
+ gid = 4321
+ self._applicationStartsWithConfiguredID(
+ ["--uid", str(uid), "--gid", str(gid)], uid, gid)
+ test_applicationStartsWithConfiguredNumericIDs.skip = setuidSkip
+
+
+ def test_applicationStartsWithConfiguredNameIDs(self):
+ """
+ L{postApplication} should change the UID and GID to the values
+ specified as user and group names by the configuration after running
+ L{service.IService.privilegedStartService} and before running
+ L{service.IService.startService}.
+ """
+ user = "foo"
+ uid = 1234
+ group = "bar"
+ gid = 4321
+ patchUserDatabase(self.patch, user, uid, group, gid)
+ self._applicationStartsWithConfiguredID(
+ ["--uid", user, "--gid", group], uid, gid)
+ test_applicationStartsWithConfiguredNameIDs.skip = setuidSkip
+
+
+ def test_startReactorRunsTheReactor(self):
+ """
+ L{startReactor} calls L{reactor.run}.
+ """
+ reactor = DummyReactor()
+ runner = app.ApplicationRunner({
+ "profile": False,
+ "profiler": "profile",
+ "debug": False})
+ runner.startReactor(reactor, None, None)
+ self.assertTrue(
+ reactor.called, "startReactor did not call reactor.run()")
+
+
+
+class UnixApplicationRunnerSetupEnvironmentTests(unittest.TestCase):
+ """
+ Tests for L{UnixApplicationRunner.setupEnvironment}.
+
+ @ivar root: The root of the filesystem, or C{unset} if none has been
+ specified with a call to L{os.chroot} (patched for this TestCase with
+ L{UnixApplicationRunnerSetupEnvironmentTests.chroot ).
+
+ @ivar cwd: The current working directory of the process, or C{unset} if
+ none has been specified with a call to L{os.chdir} (patched for this
+ TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.chdir).
+
+ @ivar mask: The current file creation mask of the process, or C{unset} if
+ none has been specified with a call to L{os.umask} (patched for this
+ TestCase with L{UnixApplicationRunnerSetupEnvironmentTests.umask).
+
+ @ivar daemon: A boolean indicating whether daemonization has been performed
+ by a call to L{_twistd_unix.daemonize} (patched for this TestCase with
+ L{UnixApplicationRunnerSetupEnvironmentTests.
+ """
+ if _twistd_unix is None:
+ skip = "twistd unix not available"
+
+ unset = object()
+
+ def setUp(self):
+ self.root = self.unset
+ self.cwd = self.unset
+ self.mask = self.unset
+ self.daemon = False
+ self.pid = os.getpid()
+ self.patch(os, 'chroot', lambda path: setattr(self, 'root', path))
+ self.patch(os, 'chdir', lambda path: setattr(self, 'cwd', path))
+ self.patch(os, 'umask', lambda mask: setattr(self, 'mask', mask))
+ self.patch(_twistd_unix, "daemonize", self.daemonize)
+ self.runner = UnixApplicationRunner({})
+
+
+ def daemonize(self, reactor, os):
+ """
+ Indicate that daemonization has happened and change the PID so that the
+ value written to the pidfile can be tested in the daemonization case.
+ """
+ self.daemon = True
+ self.patch(os, 'getpid', lambda: self.pid + 1)
+
+
+ def test_chroot(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} changes the root of the
+ filesystem if passed a non-C{None} value for the C{chroot} parameter.
+ """
+ self.runner.setupEnvironment("/foo/bar", ".", True, None, None)
+ self.assertEqual(self.root, "/foo/bar")
+
+
+ def test_noChroot(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} does not change the root of
+ the filesystem if passed C{None} for the C{chroot} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", True, None, None)
+ self.assertIdentical(self.root, self.unset)
+
+
+ def test_changeWorkingDirectory(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} changes the working directory
+ of the process to the path given for the C{rundir} parameter.
+ """
+ self.runner.setupEnvironment(None, "/foo/bar", True, None, None)
+ self.assertEqual(self.cwd, "/foo/bar")
+
+
+ def test_daemonize(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} daemonizes the process if
+ C{False} is passed for the C{nodaemon} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", False, None, None)
+ self.assertTrue(self.daemon)
+
+
+ def test_noDaemonize(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} does not daemonize the
+ process if C{True} is passed for the C{nodaemon} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", True, None, None)
+ self.assertFalse(self.daemon)
+
+
+ def test_nonDaemonPIDFile(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} writes the process's PID to
+ the file specified by the C{pidfile} parameter.
+ """
+ pidfile = self.mktemp()
+ self.runner.setupEnvironment(None, ".", True, None, pidfile)
+ fObj = file(pidfile)
+ pid = int(fObj.read())
+ fObj.close()
+ self.assertEqual(pid, self.pid)
+
+
+ def test_daemonPIDFile(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} writes the daemonized
+ process's PID to the file specified by the C{pidfile} parameter if
+ C{nodaemon} is C{False}.
+ """
+ pidfile = self.mktemp()
+ self.runner.setupEnvironment(None, ".", False, None, pidfile)
+ fObj = file(pidfile)
+ pid = int(fObj.read())
+ fObj.close()
+ self.assertEqual(pid, self.pid + 1)
+
+
+ def test_umask(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} changes the process umask to
+ the value specified by the C{umask} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", False, 123, None)
+ self.assertEqual(self.mask, 123)
+
+
+ def test_noDaemonizeNoUmask(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} doesn't change the process
+ umask if C{None} is passed for the C{umask} parameter and C{True} is
+ passed for the C{nodaemon} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", True, None, None)
+ self.assertIdentical(self.mask, self.unset)
+
+
+ def test_daemonizedNoUmask(self):
+ """
+ L{UnixApplicationRunner.setupEnvironment} changes the process umask to
+ C{0077} if C{None} is passed for the C{umask} parameter and C{False} is
+ passed for the C{nodaemon} parameter.
+ """
+ self.runner.setupEnvironment(None, ".", False, None, None)
+ self.assertEqual(self.mask, 0077)
+
+
+
+class UnixApplicationRunnerStartApplicationTests(unittest.TestCase):
+ """
+ Tests for L{UnixApplicationRunner.startApplication}.
+ """
+ if _twistd_unix is None:
+ skip = "twistd unix not available"
+
+ def test_setupEnvironment(self):
+ """
+ L{UnixApplicationRunner.startApplication} calls
+ L{UnixApplicationRunner.setupEnvironment} with the chroot, rundir,
+ nodaemon, umask, and pidfile parameters from the configuration it is
+ constructed with.
+ """
+ options = twistd.ServerOptions()
+ options.parseOptions([
+ '--nodaemon',
+ '--umask', '0070',
+ '--chroot', '/foo/chroot',
+ '--rundir', '/foo/rundir',
+ '--pidfile', '/foo/pidfile'])
+ application = service.Application("test_setupEnvironment")
+ self.runner = UnixApplicationRunner(options)
+
+ args = []
+ def fakeSetupEnvironment(self, chroot, rundir, nodaemon, umask, pidfile):
+ args.extend((chroot, rundir, nodaemon, umask, pidfile))
+
+ # Sanity check
+ self.assertEqual(
+ inspect.getargspec(self.runner.setupEnvironment),
+ inspect.getargspec(fakeSetupEnvironment))
+
+ self.patch(UnixApplicationRunner, 'setupEnvironment', fakeSetupEnvironment)
+ self.patch(UnixApplicationRunner, 'shedPrivileges', lambda *a, **kw: None)
+ self.patch(app, 'startApplication', lambda *a, **kw: None)
+ self.runner.startApplication(application)
+
+ self.assertEqual(
+ args,
+ ['/foo/chroot', '/foo/rundir', True, 56, '/foo/pidfile'])
+
+
+
+class UnixApplicationRunnerRemovePID(unittest.TestCase):
+ """
+ Tests for L{UnixApplicationRunner.removePID}.
+ """
+ if _twistd_unix is None:
+ skip = "twistd unix not available"
+
+
+ def test_removePID(self):
+ """
+ L{UnixApplicationRunner.removePID} deletes the file the name of
+ which is passed to it.
+ """
+ runner = UnixApplicationRunner({})
+ path = self.mktemp()
+ os.makedirs(path)
+ pidfile = os.path.join(path, "foo.pid")
+ file(pidfile, "w").close()
+ runner.removePID(pidfile)
+ self.assertFalse(os.path.exists(pidfile))
+
+
+ def test_removePIDErrors(self):
+ """
+ Calling L{UnixApplicationRunner.removePID} with a non-existent filename logs
+ an OSError.
+ """
+ runner = UnixApplicationRunner({})
+ runner.removePID("fakepid")
+ errors = self.flushLoggedErrors(OSError)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].value.errno, errno.ENOENT)
+
+
+
+class FakeNonDaemonizingReactor(object):
+ """
+ A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize} methods,
+ but not announcing this, and logging whether the methods have been called.
+
+ @ivar _beforeDaemonizeCalled: if C{beforeDaemonize} has been called or not.
+ @type _beforeDaemonizeCalled: C{bool}
+ @ivar _afterDaemonizeCalled: if C{afterDaemonize} has been called or not.
+ @type _afterDaemonizeCalled: C{bool}
+ """
+
+ def __init__(self):
+ self._beforeDaemonizeCalled = False
+ self._afterDaemonizeCalled = False
+
+ def beforeDaemonize(self):
+ self._beforeDaemonizeCalled = True
+
+ def afterDaemonize(self):
+ self._afterDaemonizeCalled = True
+
+
+
+class FakeDaemonizingReactor(FakeNonDaemonizingReactor):
+ """
+ A dummy reactor, providing C{beforeDaemonize} and C{afterDaemonize} methods,
+ announcing this, and logging whether the methods have been called.
+ """
+
+ implements(IReactorDaemonize)
+
+
+
+class ReactorDaemonizationTests(unittest.TestCase):
+ """
+ Tests for L{_twistd_unix.daemonize} and L{IReactorDaemonize}.
+ """
+ if _twistd_unix is None:
+ skip = "twistd unix not available"
+
+
+ def test_daemonizationHooksCalled(self):
+ """
+ L{_twistd_unix.daemonize} indeed calls
+ L{IReactorDaemonize.beforeDaemonize} and
+ L{IReactorDaemonize.afterDaemonize} if the reactor implements
+ L{IReactorDaemonize}.
+ """
+ reactor = FakeDaemonizingReactor()
+ os = MockOS()
+ _twistd_unix.daemonize(reactor, os)
+ self.assertTrue(reactor._beforeDaemonizeCalled)
+ self.assertTrue(reactor._afterDaemonizeCalled)
+
+
+ def test_daemonizationHooksNotCalled(self):
+ """
+ L{_twistd_unix.daemonize} does NOT call
+ L{IReactorDaemonize.beforeDaemonize} or
+ L{IReactorDaemonize.afterDaemonize} if the reactor does NOT
+ implement L{IReactorDaemonize}.
+ """
+ reactor = FakeNonDaemonizingReactor()
+ os = MockOS()
+ _twistd_unix.daemonize(reactor, os)
+ self.assertFalse(reactor._beforeDaemonizeCalled)
+ self.assertFalse(reactor._afterDaemonizeCalled)
+
+
+
+class DummyReactor(object):
+ """
+ A dummy reactor, only providing a C{run} method and checking that it
+ has been called.
+
+ @ivar called: if C{run} has been called or not.
+ @type called: C{bool}
+ """
+ called = False
+
+ def run(self):
+ """
+ A fake run method, checking that it's been called one and only time.
+ """
+ if self.called:
+ raise RuntimeError("Already called")
+ self.called = True
+
+
+
+class AppProfilingTestCase(unittest.TestCase):
+ """
+ Tests for L{app.AppProfiler}.
+ """
+
+ def test_profile(self):
+ """
+ L{app.ProfileRunner.run} should call the C{run} method of the reactor
+ and save profile data in the specified file.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "profile"
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ data = file(config["profile"]).read()
+ self.assertIn("DummyReactor.run", data)
+ self.assertIn("function calls", data)
+
+ if profile is None:
+ test_profile.skip = "profile module not available"
+
+
+ def _testStats(self, statsClass, profile):
+ out = StringIO.StringIO()
+
+ # Patch before creating the pstats, because pstats binds self.stream to
+ # sys.stdout early in 2.5 and newer.
+ stdout = self.patch(sys, 'stdout', out)
+
+ # If pstats.Stats can load the data and then reformat it, then the
+ # right thing probably happened.
+ stats = statsClass(profile)
+ stats.print_stats()
+ stdout.restore()
+
+ data = out.getvalue()
+ self.assertIn("function calls", data)
+ self.assertIn("(run)", data)
+
+
+ def test_profileSaveStats(self):
+ """
+ With the C{savestats} option specified, L{app.ProfileRunner.run}
+ should save the raw stats object instead of a summary output.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "profile"
+ config["savestats"] = True
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ self._testStats(pstats.Stats, config['profile'])
+
+ if profile is None:
+ test_profileSaveStats.skip = "profile module not available"
+
+
+ def test_withoutProfile(self):
+ """
+ When the C{profile} module is not present, L{app.ProfilerRunner.run}
+ should raise a C{SystemExit} exception.
+ """
+ savedModules = sys.modules.copy()
+
+ config = twistd.ServerOptions()
+ config["profiler"] = "profile"
+ profiler = app.AppProfiler(config)
+
+ sys.modules["profile"] = None
+ try:
+ self.assertRaises(SystemExit, profiler.run, None)
+ finally:
+ sys.modules.clear()
+ sys.modules.update(savedModules)
+
+
+ def test_profilePrintStatsError(self):
+ """
+ When an error happens during the print of the stats, C{sys.stdout}
+ should be restored to its initial value.
+ """
+ class ErroneousProfile(profile.Profile):
+ def print_stats(self):
+ raise RuntimeError("Boom")
+ self.patch(profile, "Profile", ErroneousProfile)
+
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "profile"
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ oldStdout = sys.stdout
+ self.assertRaises(RuntimeError, profiler.run, reactor)
+ self.assertIdentical(sys.stdout, oldStdout)
+
+ if profile is None:
+ test_profilePrintStatsError.skip = "profile module not available"
+
+
+ def test_hotshot(self):
+ """
+ L{app.HotshotRunner.run} should call the C{run} method of the reactor
+ and save profile data in the specified file.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "hotshot"
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ data = file(config["profile"]).read()
+ self.assertIn("run", data)
+ self.assertIn("function calls", data)
+
+ if hotshot is None:
+ test_hotshot.skip = "hotshot module not available"
+
+
+ def test_hotshotSaveStats(self):
+ """
+ With the C{savestats} option specified, L{app.HotshotRunner.run} should
+ save the raw stats object instead of a summary output.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "hotshot"
+ config["savestats"] = True
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ self._testStats(hotshot.stats.load, config['profile'])
+
+ if hotshot is None:
+ test_hotshotSaveStats.skip = "hotshot module not available"
+
+
+ def test_withoutHotshot(self):
+ """
+ When the C{hotshot} module is not present, L{app.HotshotRunner.run}
+ should raise a C{SystemExit} exception and log the C{ImportError}.
+ """
+ savedModules = sys.modules.copy()
+ sys.modules["hotshot"] = None
+
+ config = twistd.ServerOptions()
+ config["profiler"] = "hotshot"
+ profiler = app.AppProfiler(config)
+ try:
+ self.assertRaises(SystemExit, profiler.run, None)
+ finally:
+ sys.modules.clear()
+ sys.modules.update(savedModules)
+
+
+ def test_hotshotPrintStatsError(self):
+ """
+ When an error happens while printing the stats, C{sys.stdout}
+ should be restored to its initial value.
+ """
+ class ErroneousStats(pstats.Stats):
+ def print_stats(self):
+ raise RuntimeError("Boom")
+ self.patch(pstats, "Stats", ErroneousStats)
+
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "hotshot"
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ oldStdout = sys.stdout
+ self.assertRaises(RuntimeError, profiler.run, reactor)
+ self.assertIdentical(sys.stdout, oldStdout)
+
+ if hotshot is None:
+ test_hotshotPrintStatsError.skip = "hotshot module not available"
+
+
+ def test_cProfile(self):
+ """
+ L{app.CProfileRunner.run} should call the C{run} method of the
+ reactor and save profile data in the specified file.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "cProfile"
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ data = file(config["profile"]).read()
+ self.assertIn("run", data)
+ self.assertIn("function calls", data)
+
+ if cProfile is None:
+ test_cProfile.skip = "cProfile module not available"
+
+
+ def test_cProfileSaveStats(self):
+ """
+ With the C{savestats} option specified,
+ L{app.CProfileRunner.run} should save the raw stats object
+ instead of a summary output.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "cProfile"
+ config["savestats"] = True
+ profiler = app.AppProfiler(config)
+ reactor = DummyReactor()
+
+ profiler.run(reactor)
+
+ self.assertTrue(reactor.called)
+ self._testStats(pstats.Stats, config['profile'])
+
+ if cProfile is None:
+ test_cProfileSaveStats.skip = "cProfile module not available"
+
+
+ def test_withoutCProfile(self):
+ """
+ When the C{cProfile} module is not present,
+ L{app.CProfileRunner.run} should raise a C{SystemExit}
+ exception and log the C{ImportError}.
+ """
+ savedModules = sys.modules.copy()
+ sys.modules["cProfile"] = None
+
+ config = twistd.ServerOptions()
+ config["profiler"] = "cProfile"
+ profiler = app.AppProfiler(config)
+ try:
+ self.assertRaises(SystemExit, profiler.run, None)
+ finally:
+ sys.modules.clear()
+ sys.modules.update(savedModules)
+
+
+ def test_unknownProfiler(self):
+ """
+ Check that L{app.AppProfiler} raises L{SystemExit} when given an
+ unknown profiler name.
+ """
+ config = twistd.ServerOptions()
+ config["profile"] = self.mktemp()
+ config["profiler"] = "foobar"
+
+ error = self.assertRaises(SystemExit, app.AppProfiler, config)
+ self.assertEqual(str(error), "Unsupported profiler name: foobar")
+
+
+ def test_defaultProfiler(self):
+ """
+ L{app.Profiler} defaults to the hotshot profiler if not specified.
+ """
+ profiler = app.AppProfiler({})
+ self.assertEqual(profiler.profiler, "hotshot")
+
+
+ def test_profilerNameCaseInsentive(self):
+ """
+ The case of the profiler name passed to L{app.AppProfiler} is not
+ relevant.
+ """
+ profiler = app.AppProfiler({"profiler": "HotShot"})
+ self.assertEqual(profiler.profiler, "hotshot")
+
+
+
+def _patchFileLogObserver(patch):
+ """
+ Patch L{log.FileLogObserver} to record every call and keep a reference to
+ the passed log file for tests.
+
+ @param patch: a callback for patching (usually L{unittest.TestCase.patch}).
+
+ @return: the list that keeps track of the log files.
+ @rtype: C{list}
+ """
+ logFiles = []
+ oldFileLobObserver = log.FileLogObserver
+ def FileLogObserver(logFile):
+ logFiles.append(logFile)
+ return oldFileLobObserver(logFile)
+ patch(log, 'FileLogObserver', FileLogObserver)
+ return logFiles
+
+
+
+def _setupSyslog(testCase):
+ """
+ Make fake syslog, and return list to which prefix and then log
+ messages will be appended if it is used.
+ """
+ logMessages = []
+ class fakesyslogobserver(object):
+ def __init__(self, prefix):
+ logMessages.append(prefix)
+ def emit(self, eventDict):
+ logMessages.append(eventDict)
+ testCase.patch(syslog, "SyslogObserver", fakesyslogobserver)
+ return logMessages
+
+
+
+class AppLoggerTestCase(unittest.TestCase):
+ """
+ Tests for L{app.AppLogger}.
+
+ @ivar observers: list of observers installed during the tests.
+ @type observers: C{list}
+ """
+
+ def setUp(self):
+ """
+ Override L{log.addObserver} so that we can trace the observers
+ installed in C{self.observers}.
+ """
+ self.observers = []
+ def startLoggingWithObserver(observer):
+ self.observers.append(observer)
+ log.addObserver(observer)
+ self.patch(log, 'startLoggingWithObserver', startLoggingWithObserver)
+
+
+ def tearDown(self):
+ """
+ Remove all installed observers.
+ """
+ for observer in self.observers:
+ log.removeObserver(observer)
+
+
+ def _checkObserver(self, logs):
+ """
+ Ensure that initial C{twistd} logs are written to the given list.
+
+ @type logs: C{list}
+ @param logs: The list whose C{append} method was specified as the
+ initial log observer.
+ """
+ self.assertEqual(self.observers, [logs.append])
+ self.assertIn("starting up", logs[0]["message"][0])
+ self.assertIn("reactor class", logs[1]["message"][0])
+
+
+ def test_start(self):
+ """
+ L{app.AppLogger.start} calls L{log.addObserver}, and then writes some
+ messages about twistd and the reactor.
+ """
+ logger = app.AppLogger({})
+ observer = []
+ logger._getLogObserver = lambda: observer.append
+ logger.start(Componentized())
+ self._checkObserver(observer)
+
+
+ def test_startUsesApplicationLogObserver(self):
+ """
+ When the L{ILogObserver} component is available on the application,
+ that object will be used as the log observer instead of constructing a
+ new one.
+ """
+ application = Componentized()
+ logs = []
+ application.setComponent(ILogObserver, logs.append)
+ logger = app.AppLogger({})
+ logger.start(application)
+ self._checkObserver(logs)
+
+
+ def _setupConfiguredLogger(self, application, extraLogArgs={},
+ appLogger=app.AppLogger):
+ """
+ Set up an AppLogger which exercises the C{logger} configuration option.
+
+ @type application: L{Componentized}
+ @param application: The L{Application} object to pass to
+ L{app.AppLogger.start}.
+ @type extraLogArgs: C{dict}
+ @param extraLogArgs: extra values to pass to AppLogger.
+ @type appLogger: L{AppLogger} class, or a subclass
+ @param appLogger: factory for L{AppLogger} instances.
+
+ @rtype: C{list}
+ @return: The logs accumulated by the log observer.
+ """
+ logs = []
+ logArgs = {"logger": lambda: logs.append}
+ logArgs.update(extraLogArgs)
+ logger = appLogger(logArgs)
+ logger.start(application)
+ return logs
+
+
+ def test_startUsesConfiguredLogObserver(self):
+ """
+ When the C{logger} key is specified in the configuration dictionary
+ (i.e., when C{--logger} is passed to twistd), the initial log observer
+ will be the log observer returned from the callable which the value
+ refers to in FQPN form.
+ """
+ application = Componentized()
+ self._checkObserver(self._setupConfiguredLogger(application))
+
+
+ def test_configuredLogObserverBeatsComponent(self):
+ """
+ C{--logger} takes precedence over a ILogObserver component set on
+ Application.
+ """
+ nonlogs = []
+ application = Componentized()
+ application.setComponent(ILogObserver, nonlogs.append)
+ self._checkObserver(self._setupConfiguredLogger(application))
+ self.assertEqual(nonlogs, [])
+
+
+ def test_configuredLogObserverBeatsSyslog(self):
+ """
+ C{--logger} takes precedence over a C{--syslog} command line
+ argument.
+ """
+ logs = _setupSyslog(self)
+ application = Componentized()
+ self._checkObserver(self._setupConfiguredLogger(application,
+ {"syslog": True},
+ UnixAppLogger))
+ self.assertEqual(logs, [])
+
+ if _twistd_unix is None or syslog is None:
+ test_configuredLogObserverBeatsSyslog.skip = "Not on POSIX, or syslog not available."
+
+
+ def test_configuredLogObserverBeatsLogfile(self):
+ """
+ C{--logger} takes precedence over a C{--logfile} command line
+ argument.
+ """
+ application = Componentized()
+ path = self.mktemp()
+ self._checkObserver(self._setupConfiguredLogger(application,
+ {"logfile": "path"}))
+ self.assertFalse(os.path.exists(path))
+
+
+ def test_getLogObserverStdout(self):
+ """
+ When logfile is empty or set to C{-}, L{app.AppLogger._getLogObserver}
+ returns a log observer pointing at C{sys.stdout}.
+ """
+ logger = app.AppLogger({"logfile": "-"})
+ logFiles = _patchFileLogObserver(self.patch)
+
+ observer = logger._getLogObserver()
+
+ self.assertEqual(len(logFiles), 1)
+ self.assertIdentical(logFiles[0], sys.stdout)
+
+ logger = app.AppLogger({"logfile": ""})
+ observer = logger._getLogObserver()
+
+ self.assertEqual(len(logFiles), 2)
+ self.assertIdentical(logFiles[1], sys.stdout)
+
+
+ def test_getLogObserverFile(self):
+ """
+ When passing the C{logfile} option, L{app.AppLogger._getLogObserver}
+ returns a log observer pointing at the specified path.
+ """
+ logFiles = _patchFileLogObserver(self.patch)
+ filename = self.mktemp()
+ logger = app.AppLogger({"logfile": filename})
+
+ observer = logger._getLogObserver()
+
+ self.assertEqual(len(logFiles), 1)
+ self.assertEqual(logFiles[0].path,
+ os.path.abspath(filename))
+
+
+ def test_stop(self):
+ """
+ L{app.AppLogger.stop} removes the observer created in C{start}, and
+ reinitialize its C{_observer} so that if C{stop} is called several
+ times it doesn't break.
+ """
+ removed = []
+ observer = object()
+ def remove(observer):
+ removed.append(observer)
+ self.patch(log, 'removeObserver', remove)
+ logger = app.AppLogger({})
+ logger._observer = observer
+ logger.stop()
+ self.assertEqual(removed, [observer])
+ logger.stop()
+ self.assertEqual(removed, [observer])
+ self.assertIdentical(logger._observer, None)
+
+
+
+class UnixAppLoggerTestCase(unittest.TestCase):
+ """
+ Tests for L{UnixAppLogger}.
+
+ @ivar signals: list of signal handlers installed.
+ @type signals: C{list}
+ """
+ if _twistd_unix is None:
+ skip = "twistd unix not available"
+
+ def setUp(self):
+ """
+ Fake C{signal.signal} for not installing the handlers but saving them
+ in C{self.signals}.
+ """
+ self.signals = []
+ def fakeSignal(sig, f):
+ self.signals.append((sig, f))
+ self.patch(signal, "signal", fakeSignal)
+
+
+ def test_getLogObserverStdout(self):
+ """
+ When non-daemonized and C{logfile} is empty or set to C{-},
+ L{UnixAppLogger._getLogObserver} returns a log observer pointing at
+ C{sys.stdout}.
+ """
+ logFiles = _patchFileLogObserver(self.patch)
+
+ logger = UnixAppLogger({"logfile": "-", "nodaemon": True})
+ observer = logger._getLogObserver()
+ self.assertEqual(len(logFiles), 1)
+ self.assertIdentical(logFiles[0], sys.stdout)
+
+ logger = UnixAppLogger({"logfile": "", "nodaemon": True})
+ observer = logger._getLogObserver()
+ self.assertEqual(len(logFiles), 2)
+ self.assertIdentical(logFiles[1], sys.stdout)
+
+
+ def test_getLogObserverStdoutDaemon(self):
+ """
+ When daemonized and C{logfile} is set to C{-},
+ L{UnixAppLogger._getLogObserver} raises C{SystemExit}.
+ """
+ logger = UnixAppLogger({"logfile": "-", "nodaemon": False})
+ error = self.assertRaises(SystemExit, logger._getLogObserver)
+ self.assertEqual(str(error), "Daemons cannot log to stdout, exiting!")
+
+
+ def test_getLogObserverFile(self):
+ """
+ When C{logfile} contains a file name, L{app.AppLogger._getLogObserver}
+ returns a log observer pointing at the specified path, and a signal
+ handler rotating the log is installed.
+ """
+ logFiles = _patchFileLogObserver(self.patch)
+ filename = self.mktemp()
+ logger = UnixAppLogger({"logfile": filename})
+ observer = logger._getLogObserver()
+
+ self.assertEqual(len(logFiles), 1)
+ self.assertEqual(logFiles[0].path,
+ os.path.abspath(filename))
+
+ self.assertEqual(len(self.signals), 1)
+ self.assertEqual(self.signals[0][0], signal.SIGUSR1)
+
+ d = Deferred()
+ def rotate():
+ d.callback(None)
+ logFiles[0].rotate = rotate
+
+ rotateLog = self.signals[0][1]
+ rotateLog(None, None)
+ return d
+
+
+ def test_getLogObserverDontOverrideSignalHandler(self):
+ """
+ If a signal handler is already installed,
+ L{UnixAppLogger._getLogObserver} doesn't override it.
+ """
+ def fakeGetSignal(sig):
+ self.assertEqual(sig, signal.SIGUSR1)
+ return object()
+ self.patch(signal, "getsignal", fakeGetSignal)
+ filename = self.mktemp()
+ logger = UnixAppLogger({"logfile": filename})
+ observer = logger._getLogObserver()
+
+ self.assertEqual(self.signals, [])
+
+
+ def test_getLogObserverDefaultFile(self):
+ """
+ When daemonized and C{logfile} is empty, the observer returned by
+ L{UnixAppLogger._getLogObserver} points at C{twistd.log} in the current
+ directory.
+ """
+ logFiles = _patchFileLogObserver(self.patch)
+ logger = UnixAppLogger({"logfile": "", "nodaemon": False})
+ observer = logger._getLogObserver()
+
+ self.assertEqual(len(logFiles), 1)
+ self.assertEqual(logFiles[0].path,
+ os.path.abspath("twistd.log"))
+
+
+ def test_getLogObserverSyslog(self):
+ """
+ If C{syslog} is set to C{True}, L{UnixAppLogger._getLogObserver} starts
+ a L{syslog.SyslogObserver} with given C{prefix}.
+ """
+ logs = _setupSyslog(self)
+ logger = UnixAppLogger({"syslog": True, "prefix": "test-prefix"})
+ observer = logger._getLogObserver()
+ self.assertEqual(logs, ["test-prefix"])
+ observer({"a": "b"})
+ self.assertEqual(logs, ["test-prefix", {"a": "b"}])
+
+ if syslog is None:
+ test_getLogObserverSyslog.skip = "Syslog not available"
+
+
+
diff --git a/twisted/test/test_udp.py b/twisted/test/test_udp.py
new file mode 100644
index 0000000..92ebcec
--- /dev/null
+++ b/twisted/test/test_udp.py
@@ -0,0 +1,721 @@
+# -*- test-case-name: twisted.test.test_udp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorUDP} and L{IReactorMulticast}.
+"""
+
+from twisted.trial import unittest
+
+from twisted.internet.defer import Deferred, gatherResults, maybeDeferred
+from twisted.internet import protocol, reactor, error, defer, interfaces, udp
+from twisted.python import runtime
+
+
+class Mixin:
+
+ started = 0
+ stopped = 0
+
+ startedDeferred = None
+
+ def __init__(self):
+ self.packets = []
+
+ def startProtocol(self):
+ self.started = 1
+ if self.startedDeferred is not None:
+ d, self.startedDeferred = self.startedDeferred, None
+ d.callback(None)
+
+ def stopProtocol(self):
+ self.stopped = 1
+
+
+class Server(Mixin, protocol.DatagramProtocol):
+ packetReceived = None
+ refused = 0
+
+
+ def datagramReceived(self, data, addr):
+ self.packets.append((data, addr))
+ if self.packetReceived is not None:
+ d, self.packetReceived = self.packetReceived, None
+ d.callback(None)
+
+
+
+class Client(Mixin, protocol.ConnectedDatagramProtocol):
+
+ packetReceived = None
+ refused = 0
+
+ def datagramReceived(self, data):
+ self.packets.append(data)
+ if self.packetReceived is not None:
+ d, self.packetReceived = self.packetReceived, None
+ d.callback(None)
+
+ def connectionFailed(self, failure):
+ if self.startedDeferred is not None:
+ d, self.startedDeferred = self.startedDeferred, None
+ d.errback(failure)
+ self.failure = failure
+
+ def connectionRefused(self):
+ if self.startedDeferred is not None:
+ d, self.startedDeferred = self.startedDeferred, None
+ d.errback(error.ConnectionRefusedError("yup"))
+ self.refused = 1
+
+
+class GoodClient(Server):
+
+ def connectionRefused(self):
+ if self.startedDeferred is not None:
+ d, self.startedDeferred = self.startedDeferred, None
+ d.errback(error.ConnectionRefusedError("yup"))
+ self.refused = 1
+
+
+
+class BadClientError(Exception):
+ """
+ Raised by BadClient at the end of every datagramReceived call to try and
+ screw stuff up.
+ """
+
+
+
+class BadClient(protocol.DatagramProtocol):
+ """
+ A DatagramProtocol which always raises an exception from datagramReceived.
+ Used to test error handling behavior in the reactor for that method.
+ """
+ d = None
+
+ def setDeferred(self, d):
+ """
+ Set the Deferred which will be called back when datagramReceived is
+ called.
+ """
+ self.d = d
+
+
+ def datagramReceived(self, bytes, addr):
+ if self.d is not None:
+ d, self.d = self.d, None
+ d.callback(bytes)
+ raise BadClientError("Application code is very buggy!")
+
+
+
+class UDPTestCase(unittest.TestCase):
+
+ def test_oldAddress(self):
+ """
+ The C{type} of the host address of a listening L{DatagramProtocol}'s
+ transport is C{"UDP"}.
+ """
+ server = Server()
+ d = server.startedDeferred = defer.Deferred()
+ p = reactor.listenUDP(0, server, interface="127.0.0.1")
+ def cbStarted(ignored):
+ addr = p.getHost()
+ self.assertEqual(addr.type, 'UDP')
+ return p.stopListening()
+ return d.addCallback(cbStarted)
+
+
+ def test_startStop(self):
+ """
+ The L{DatagramProtocol}'s C{startProtocol} and C{stopProtocol}
+ methods are called when its transports starts and stops listening,
+ respectively.
+ """
+ server = Server()
+ d = server.startedDeferred = defer.Deferred()
+ port1 = reactor.listenUDP(0, server, interface="127.0.0.1")
+ def cbStarted(ignored):
+ self.assertEqual(server.started, 1)
+ self.assertEqual(server.stopped, 0)
+ return port1.stopListening()
+ def cbStopped(ignored):
+ self.assertEqual(server.stopped, 1)
+ return d.addCallback(cbStarted).addCallback(cbStopped)
+
+
+ def test_rebind(self):
+ """
+ Re-listening with the same L{DatagramProtocol} re-invokes the
+ C{startProtocol} callback.
+ """
+ server = Server()
+ d = server.startedDeferred = defer.Deferred()
+ p = reactor.listenUDP(0, server, interface="127.0.0.1")
+
+ def cbStarted(ignored, port):
+ return port.stopListening()
+
+ def cbStopped(ignored):
+ d = server.startedDeferred = defer.Deferred()
+ p = reactor.listenUDP(0, server, interface="127.0.0.1")
+ return d.addCallback(cbStarted, p)
+
+ return d.addCallback(cbStarted, p)
+
+
+ def test_bindError(self):
+ """
+ A L{CannotListenError} exception is raised when attempting to bind a
+ second protocol instance to an already bound port
+ """
+ server = Server()
+ d = server.startedDeferred = defer.Deferred()
+ port = reactor.listenUDP(0, server, interface='127.0.0.1')
+
+ def cbStarted(ignored):
+ self.assertEqual(port.getHost(), server.transport.getHost())
+ server2 = Server()
+ self.assertRaises(
+ error.CannotListenError,
+ reactor.listenUDP, port.getHost().port, server2,
+ interface='127.0.0.1')
+ d.addCallback(cbStarted)
+
+ def cbFinished(ignored):
+ return port.stopListening()
+ d.addCallback(cbFinished)
+ return d
+
+
+ def test_sendPackets(self):
+ """
+ Datagrams can be sent with the transport's C{write} method and
+ received via the C{datagramReceived} callback method.
+ """
+ server = Server()
+ serverStarted = server.startedDeferred = defer.Deferred()
+ port1 = reactor.listenUDP(0, server, interface="127.0.0.1")
+
+ client = GoodClient()
+ clientStarted = client.startedDeferred = defer.Deferred()
+
+ def cbServerStarted(ignored):
+ self.port2 = reactor.listenUDP(0, client, interface="127.0.0.1")
+ return clientStarted
+
+ d = serverStarted.addCallback(cbServerStarted)
+
+ def cbClientStarted(ignored):
+ client.transport.connect("127.0.0.1",
+ server.transport.getHost().port)
+ cAddr = client.transport.getHost()
+ sAddr = server.transport.getHost()
+
+ serverSend = client.packetReceived = defer.Deferred()
+ server.transport.write("hello", (cAddr.host, cAddr.port))
+
+ clientWrites = [
+ ("a",),
+ ("b", None),
+ ("c", (sAddr.host, sAddr.port))]
+
+ def cbClientSend(ignored):
+ if clientWrites:
+ nextClientWrite = server.packetReceived = defer.Deferred()
+ nextClientWrite.addCallback(cbClientSend)
+ client.transport.write(*clientWrites.pop(0))
+ return nextClientWrite
+
+ # No one will ever call .errback on either of these Deferreds,
+ # but there is a non-trivial amount of test code which might
+ # cause them to fail somehow. So fireOnOneErrback=True.
+ return defer.DeferredList([
+ cbClientSend(None),
+ serverSend],
+ fireOnOneErrback=True)
+
+ d.addCallback(cbClientStarted)
+
+ def cbSendsFinished(ignored):
+ cAddr = client.transport.getHost()
+ sAddr = server.transport.getHost()
+ self.assertEqual(
+ client.packets,
+ [("hello", (sAddr.host, sAddr.port))])
+ clientAddr = (cAddr.host, cAddr.port)
+ self.assertEqual(
+ server.packets,
+ [("a", clientAddr),
+ ("b", clientAddr),
+ ("c", clientAddr)])
+
+ d.addCallback(cbSendsFinished)
+
+ def cbFinished(ignored):
+ return defer.DeferredList([
+ defer.maybeDeferred(port1.stopListening),
+ defer.maybeDeferred(self.port2.stopListening)],
+ fireOnOneErrback=True)
+
+ d.addCallback(cbFinished)
+ return d
+
+
+ def test_connectionRefused(self):
+ """
+ A L{ConnectionRefusedError} exception is raised when a connection
+ attempt is actively refused by the other end.
+
+ Note: This test assumes no one is listening on port 80 UDP.
+ """
+ client = GoodClient()
+ clientStarted = client.startedDeferred = defer.Deferred()
+ port = reactor.listenUDP(0, client, interface="127.0.0.1")
+
+ server = Server()
+ serverStarted = server.startedDeferred = defer.Deferred()
+ port2 = reactor.listenUDP(0, server, interface="127.0.0.1")
+
+ d = defer.DeferredList(
+ [clientStarted, serverStarted],
+ fireOnOneErrback=True)
+
+ def cbStarted(ignored):
+ connectionRefused = client.startedDeferred = defer.Deferred()
+ client.transport.connect("127.0.0.1", 80)
+
+ for i in range(10):
+ client.transport.write(str(i))
+ server.transport.write(str(i), ("127.0.0.1", 80))
+
+ return self.assertFailure(
+ connectionRefused,
+ error.ConnectionRefusedError)
+
+ d.addCallback(cbStarted)
+
+ def cbFinished(ignored):
+ return defer.DeferredList([
+ defer.maybeDeferred(port.stopListening),
+ defer.maybeDeferred(port2.stopListening)],
+ fireOnOneErrback=True)
+
+ d.addCallback(cbFinished)
+ return d
+
+
+ def test_badConnect(self):
+ """
+ A call to the transport's connect method fails with a L{ValueError}
+ when a non-IP address is passed as the host value.
+
+ A call to a transport's connect method fails with a L{RuntimeError}
+ when the transport is already connected.
+ """
+ client = GoodClient()
+ port = reactor.listenUDP(0, client, interface="127.0.0.1")
+ self.assertRaises(ValueError, client.transport.connect,
+ "localhost", 80)
+ client.transport.connect("127.0.0.1", 80)
+ self.assertRaises(RuntimeError, client.transport.connect,
+ "127.0.0.1", 80)
+ return port.stopListening()
+
+
+
+ def test_datagramReceivedError(self):
+ """
+ When datagramReceived raises an exception it is logged but the port
+ is not disconnected.
+ """
+ finalDeferred = defer.Deferred()
+
+ def cbCompleted(ign):
+ """
+ Flush the exceptions which the reactor should have logged and make
+ sure they're actually there.
+ """
+ errs = self.flushLoggedErrors(BadClientError)
+ self.assertEqual(len(errs), 2, "Incorrectly found %d errors, expected 2" % (len(errs),))
+ finalDeferred.addCallback(cbCompleted)
+
+ client = BadClient()
+ port = reactor.listenUDP(0, client, interface='127.0.0.1')
+
+ def cbCleanup(result):
+ """
+ Disconnect the port we started and pass on whatever was given to us
+ in case it was a Failure.
+ """
+ return defer.maybeDeferred(port.stopListening).addBoth(lambda ign: result)
+ finalDeferred.addBoth(cbCleanup)
+
+ addr = port.getHost()
+
+ # UDP is not reliable. Try to send as many as 60 packets before giving
+ # up. Conceivably, all sixty could be lost, but they probably won't be
+ # unless all UDP traffic is being dropped, and then the rest of these
+ # UDP tests will likely fail as well. Ideally, this test (and probably
+ # others) wouldn't even use actual UDP traffic: instead, they would
+ # stub out the socket with a fake one which could be made to behave in
+ # whatever way the test desires. Unfortunately, this is hard because
+ # of differences in various reactor implementations.
+ attempts = range(60)
+ succeededAttempts = []
+
+ def makeAttempt():
+ """
+ Send one packet to the listening BadClient. Set up a 0.1 second
+ timeout to do re-transmits in case the packet is dropped. When two
+ packets have been received by the BadClient, stop sending and let
+ the finalDeferred's callbacks do some assertions.
+ """
+ if not attempts:
+ try:
+ self.fail("Not enough packets received")
+ except:
+ finalDeferred.errback()
+
+ self.failIfIdentical(client.transport, None, "UDP Protocol lost its transport")
+
+ packet = str(attempts.pop(0))
+ packetDeferred = defer.Deferred()
+ client.setDeferred(packetDeferred)
+ client.transport.write(packet, (addr.host, addr.port))
+
+ def cbPacketReceived(packet):
+ """
+ A packet arrived. Cancel the timeout for it, record it, and
+ maybe finish the test.
+ """
+ timeoutCall.cancel()
+ succeededAttempts.append(packet)
+ if len(succeededAttempts) == 2:
+ # The second error has not yet been logged, since the
+ # exception which causes it hasn't even been raised yet.
+ # Give the datagramReceived call a chance to finish, then
+ # let the test finish asserting things.
+ reactor.callLater(0, finalDeferred.callback, None)
+ else:
+ makeAttempt()
+
+ def ebPacketTimeout(err):
+ """
+ The packet wasn't received quickly enough. Try sending another
+ one. It doesn't matter if the packet for which this was the
+ timeout eventually arrives: makeAttempt throws away the
+ Deferred on which this function is the errback, so when
+ datagramReceived callbacks, so it won't be on this Deferred, so
+ it won't raise an AlreadyCalledError.
+ """
+ makeAttempt()
+
+ packetDeferred.addCallbacks(cbPacketReceived, ebPacketTimeout)
+ packetDeferred.addErrback(finalDeferred.errback)
+
+ timeoutCall = reactor.callLater(
+ 0.1, packetDeferred.errback,
+ error.TimeoutError(
+ "Timed out in testDatagramReceivedError"))
+
+ makeAttempt()
+ return finalDeferred
+
+
+ def test_portRepr(self):
+ """
+ The port number being listened on can be found in the string
+ returned from calling repr() on L{twisted.internet.udp.Port}.
+ """
+ client = GoodClient()
+ p = reactor.listenUDP(0, client)
+ portNo = str(p.getHost().port)
+ self.failIf(repr(p).find(portNo) == -1)
+ def stoppedListening(ign):
+ self.failIf(repr(p).find(portNo) != -1)
+ d = defer.maybeDeferred(p.stopListening)
+ d.addCallback(stoppedListening)
+ return d
+
+
+ def test_NoWarningOnBroadcast(self):
+ """
+ C{'<broadcast>'} is an alternative way to say C{'255.255.255.255'}
+ ({socket.gethostbyname("<broadcast>")} returns C{'255.255.255.255'}),
+ so because it becomes a valid IP address, no deprecation warning about
+ passing hostnames to L{twisted.internet.udp.Port.write} needs to be
+ emitted by C{write()} in this case.
+ """
+ class fakeSocket:
+ def sendto(self, foo, bar):
+ pass
+
+ p = udp.Port(0, Server())
+ p.socket = fakeSocket()
+ p.write("test", ("<broadcast>", 1234))
+
+ warnings = self.flushWarnings([self.test_NoWarningOnBroadcast])
+ self.assertEqual(len(warnings), 0)
+
+
+
+class ReactorShutdownInteraction(unittest.TestCase):
+ """Test reactor shutdown interaction"""
+
+ def setUp(self):
+ """Start a UDP port"""
+ self.server = Server()
+ self.port = reactor.listenUDP(0, self.server, interface='127.0.0.1')
+
+ def tearDown(self):
+ """Stop the UDP port"""
+ return self.port.stopListening()
+
+ def testShutdownFromDatagramReceived(self):
+ """Test reactor shutdown while in a recvfrom() loop"""
+
+ # udp.Port's doRead calls recvfrom() in a loop, as an optimization.
+ # It is important this loop terminate under various conditions.
+ # Previously, if datagramReceived synchronously invoked
+ # reactor.stop(), under certain reactors, the Port's socket would
+ # synchronously disappear, causing an AttributeError inside that
+ # loop. This was mishandled, causing the loop to spin forever.
+ # This test is primarily to ensure that the loop never spins
+ # forever.
+
+ finished = defer.Deferred()
+ pr = self.server.packetReceived = defer.Deferred()
+
+ def pktRece(ignored):
+ # Simulate reactor.stop() behavior :(
+ self.server.transport.connectionLost()
+ # Then delay this Deferred chain until the protocol has been
+ # disconnected, as the reactor should do in an error condition
+ # such as we are inducing. This is very much a whitebox test.
+ reactor.callLater(0, finished.callback, None)
+ pr.addCallback(pktRece)
+
+ def flushErrors(ignored):
+ # We are breaking abstraction and calling private APIs, any
+ # number of horrible errors might occur. As long as the reactor
+ # doesn't hang, this test is satisfied. (There may be room for
+ # another, stricter test.)
+ self.flushLoggedErrors()
+ finished.addCallback(flushErrors)
+ self.server.transport.write('\0' * 64, ('127.0.0.1',
+ self.server.transport.getHost().port))
+ return finished
+
+
+
+class MulticastTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.server = Server()
+ self.client = Client()
+ # multicast won't work if we listen over loopback, apparently
+ self.port1 = reactor.listenMulticast(0, self.server)
+ self.port2 = reactor.listenMulticast(0, self.client)
+ self.client.transport.connect(
+ "127.0.0.1", self.server.transport.getHost().port)
+
+
+ def tearDown(self):
+ return gatherResults([
+ maybeDeferred(self.port1.stopListening),
+ maybeDeferred(self.port2.stopListening)])
+
+
+ def testTTL(self):
+ for o in self.client, self.server:
+ self.assertEqual(o.transport.getTTL(), 1)
+ o.transport.setTTL(2)
+ self.assertEqual(o.transport.getTTL(), 2)
+
+
+ def test_loopback(self):
+ """
+ Test that after loopback mode has been set, multicast packets are
+ delivered to their sender.
+ """
+ self.assertEqual(self.server.transport.getLoopbackMode(), 1)
+ addr = self.server.transport.getHost()
+ joined = self.server.transport.joinGroup("225.0.0.250")
+
+ def cbJoined(ignored):
+ d = self.server.packetReceived = Deferred()
+ self.server.transport.write("hello", ("225.0.0.250", addr.port))
+ return d
+ joined.addCallback(cbJoined)
+
+ def cbPacket(ignored):
+ self.assertEqual(len(self.server.packets), 1)
+ self.server.transport.setLoopbackMode(0)
+ self.assertEqual(self.server.transport.getLoopbackMode(), 0)
+ self.server.transport.write("hello", ("225.0.0.250", addr.port))
+
+ # This is fairly lame.
+ d = Deferred()
+ reactor.callLater(0, d.callback, None)
+ return d
+ joined.addCallback(cbPacket)
+
+ def cbNoPacket(ignored):
+ self.assertEqual(len(self.server.packets), 1)
+ joined.addCallback(cbNoPacket)
+
+ return joined
+
+
+ def test_interface(self):
+ """
+ Test C{getOutgoingInterface} and C{setOutgoingInterface}.
+ """
+ self.assertEqual(
+ self.client.transport.getOutgoingInterface(), "0.0.0.0")
+ self.assertEqual(
+ self.server.transport.getOutgoingInterface(), "0.0.0.0")
+
+ d1 = self.client.transport.setOutgoingInterface("127.0.0.1")
+ d2 = self.server.transport.setOutgoingInterface("127.0.0.1")
+ result = gatherResults([d1, d2])
+
+ def cbInterfaces(ignored):
+ self.assertEqual(
+ self.client.transport.getOutgoingInterface(), "127.0.0.1")
+ self.assertEqual(
+ self.server.transport.getOutgoingInterface(), "127.0.0.1")
+ result.addCallback(cbInterfaces)
+ return result
+
+
+ def test_joinLeave(self):
+ """
+ Test that multicast a group can be joined and left.
+ """
+ d = self.client.transport.joinGroup("225.0.0.250")
+
+ def clientJoined(ignored):
+ return self.client.transport.leaveGroup("225.0.0.250")
+ d.addCallback(clientJoined)
+
+ def clientLeft(ignored):
+ return self.server.transport.joinGroup("225.0.0.250")
+ d.addCallback(clientLeft)
+
+ def serverJoined(ignored):
+ return self.server.transport.leaveGroup("225.0.0.250")
+ d.addCallback(serverJoined)
+
+ return d
+
+
+ def test_joinFailure(self):
+ """
+ Test that an attempt to join an address which is not a multicast
+ address fails with L{error.MulticastJoinError}.
+ """
+ # 127.0.0.1 is not a multicast address, so joining it should fail.
+ return self.assertFailure(
+ self.client.transport.joinGroup("127.0.0.1"),
+ error.MulticastJoinError)
+ if runtime.platform.isWindows() and not runtime.platform.isVista():
+ test_joinFailure.todo = "Windows' multicast is wonky"
+
+
+ def test_multicast(self):
+ """
+ Test that a multicast group can be joined and messages sent to and
+ received from it.
+ """
+ c = Server()
+ p = reactor.listenMulticast(0, c)
+ addr = self.server.transport.getHost()
+
+ joined = self.server.transport.joinGroup("225.0.0.250")
+
+ def cbJoined(ignored):
+ d = self.server.packetReceived = Deferred()
+ c.transport.write("hello world", ("225.0.0.250", addr.port))
+ return d
+ joined.addCallback(cbJoined)
+
+ def cbPacket(ignored):
+ self.assertEqual(self.server.packets[0][0], "hello world")
+ joined.addCallback(cbPacket)
+
+ def cleanup(passthrough):
+ result = maybeDeferred(p.stopListening)
+ result.addCallback(lambda ign: passthrough)
+ return result
+ joined.addCallback(cleanup)
+
+ return joined
+
+
+ def test_multiListen(self):
+ """
+ Test that multiple sockets can listen on the same multicast port and
+ that they both receive multicast messages directed to that address.
+ """
+ firstClient = Server()
+ firstPort = reactor.listenMulticast(
+ 0, firstClient, listenMultiple=True)
+
+ portno = firstPort.getHost().port
+
+ secondClient = Server()
+ secondPort = reactor.listenMulticast(
+ portno, secondClient, listenMultiple=True)
+
+ theGroup = "225.0.0.250"
+ joined = gatherResults([self.server.transport.joinGroup(theGroup),
+ firstPort.joinGroup(theGroup),
+ secondPort.joinGroup(theGroup)])
+
+
+ def serverJoined(ignored):
+ d1 = firstClient.packetReceived = Deferred()
+ d2 = secondClient.packetReceived = Deferred()
+ firstClient.transport.write("hello world", (theGroup, portno))
+ return gatherResults([d1, d2])
+ joined.addCallback(serverJoined)
+
+ def gotPackets(ignored):
+ self.assertEqual(firstClient.packets[0][0], "hello world")
+ self.assertEqual(secondClient.packets[0][0], "hello world")
+ joined.addCallback(gotPackets)
+
+ def cleanup(passthrough):
+ result = gatherResults([
+ maybeDeferred(firstPort.stopListening),
+ maybeDeferred(secondPort.stopListening)])
+ result.addCallback(lambda ign: passthrough)
+ return result
+ joined.addBoth(cleanup)
+ return joined
+ if runtime.platform.isWindows():
+ test_multiListen.skip = ("on non-linux platforms it appears multiple "
+ "processes can listen, but not multiple sockets "
+ "in same process?")
+
+
+if not interfaces.IReactorUDP(reactor, None):
+ UDPTestCase.skip = "This reactor does not support UDP"
+ ReactorShutdownInteraction.skip = "This reactor does not support UDP"
+if not interfaces.IReactorMulticast(reactor, None):
+ MulticastTestCase.skip = "This reactor does not support multicast"
+
+def checkForLinux22():
+ import os
+ if os.path.exists("/proc/version"):
+ s = open("/proc/version").read()
+ if s.startswith("Linux version"):
+ s = s.split()[2]
+ if s.split(".")[:2] == ["2", "2"]:
+ f = MulticastTestCase.testInterface.im_func
+ f.todo = "figure out why this fails in linux 2.2"
+checkForLinux22()
diff --git a/twisted/test/test_unix.py b/twisted/test/test_unix.py
new file mode 100644
index 0000000..863f665
--- /dev/null
+++ b/twisted/test/test_unix.py
@@ -0,0 +1,405 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for implementations of L{IReactorUNIX} and L{IReactorUNIXDatagram}.
+"""
+
+import stat, os, sys, types
+import socket
+
+from twisted.internet import interfaces, reactor, protocol, error, address, defer, utils
+from twisted.python import lockfile
+from twisted.trial import unittest
+
+from twisted.test.test_tcp import MyServerFactory, MyClientFactory
+
+
+class FailedConnectionClientFactory(protocol.ClientFactory):
+ def __init__(self, onFail):
+ self.onFail = onFail
+
+ def clientConnectionFailed(self, connector, reason):
+ self.onFail.errback(reason)
+
+
+
+class UnixSocketTestCase(unittest.TestCase):
+ """
+ Test unix sockets.
+ """
+ def test_peerBind(self):
+ """
+ The address passed to the server factory's C{buildProtocol} method and
+ the address returned by the connected protocol's transport's C{getPeer}
+ method match the address the client socket is bound to.
+ """
+ filename = self.mktemp()
+ peername = self.mktemp()
+ serverFactory = MyServerFactory()
+ connMade = serverFactory.protocolConnectionMade = defer.Deferred()
+ unixPort = reactor.listenUNIX(filename, serverFactory)
+ self.addCleanup(unixPort.stopListening)
+ unixSocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.addCleanup(unixSocket.close)
+ unixSocket.bind(peername)
+ unixSocket.connect(filename)
+ def cbConnMade(proto):
+ expected = address.UNIXAddress(peername)
+ self.assertEqual(serverFactory.peerAddresses, [expected])
+ self.assertEqual(proto.transport.getPeer(), expected)
+ connMade.addCallback(cbConnMade)
+ return connMade
+
+
+ def test_dumber(self):
+ """
+ L{IReactorUNIX.connectUNIX} can be used to connect a client to a server
+ started with L{IReactorUNIX.listenUNIX}.
+ """
+ filename = self.mktemp()
+ serverFactory = MyServerFactory()
+ serverConnMade = defer.Deferred()
+ serverFactory.protocolConnectionMade = serverConnMade
+ unixPort = reactor.listenUNIX(filename, serverFactory)
+ self.addCleanup(unixPort.stopListening)
+ clientFactory = MyClientFactory()
+ clientConnMade = defer.Deferred()
+ clientFactory.protocolConnectionMade = clientConnMade
+ c = reactor.connectUNIX(filename, clientFactory)
+ d = defer.gatherResults([serverConnMade, clientConnMade])
+ def allConnected((serverProtocol, clientProtocol)):
+
+ # Incidental assertion which may or may not be redundant with some
+ # other test. This probably deserves its own test method.
+ self.assertEqual(clientFactory.peerAddresses,
+ [address.UNIXAddress(filename)])
+
+ clientProtocol.transport.loseConnection()
+ serverProtocol.transport.loseConnection()
+ d.addCallback(allConnected)
+ return d
+
+
+ def test_pidFile(self):
+ """
+ A lockfile is created and locked when L{IReactorUNIX.listenUNIX} is
+ called and released when the Deferred returned by the L{IListeningPort}
+ provider's C{stopListening} method is called back.
+ """
+ filename = self.mktemp()
+ serverFactory = MyServerFactory()
+ serverConnMade = defer.Deferred()
+ serverFactory.protocolConnectionMade = serverConnMade
+ unixPort = reactor.listenUNIX(filename, serverFactory, wantPID=True)
+ self.assertTrue(lockfile.isLocked(filename + ".lock"))
+
+ # XXX This part would test something about the checkPID parameter, but
+ # it doesn't actually. It should be rewritten to test the several
+ # different possible behaviors. -exarkun
+ clientFactory = MyClientFactory()
+ clientConnMade = defer.Deferred()
+ clientFactory.protocolConnectionMade = clientConnMade
+ c = reactor.connectUNIX(filename, clientFactory, checkPID=1)
+
+ d = defer.gatherResults([serverConnMade, clientConnMade])
+ def _portStuff((serverProtocol, clientProto)):
+
+ # Incidental assertion which may or may not be redundant with some
+ # other test. This probably deserves its own test method.
+ self.assertEqual(clientFactory.peerAddresses,
+ [address.UNIXAddress(filename)])
+
+ clientProto.transport.loseConnection()
+ serverProtocol.transport.loseConnection()
+ return unixPort.stopListening()
+ d.addCallback(_portStuff)
+
+ def _check(ignored):
+ self.failIf(lockfile.isLocked(filename + ".lock"), 'locked')
+ d.addCallback(_check)
+ return d
+
+
+ def test_socketLocking(self):
+ """
+ L{IReactorUNIX.listenUNIX} raises L{error.CannotListenError} if passed
+ the name of a file on which a server is already listening.
+ """
+ filename = self.mktemp()
+ serverFactory = MyServerFactory()
+ unixPort = reactor.listenUNIX(filename, serverFactory, wantPID=True)
+
+ self.assertRaises(
+ error.CannotListenError,
+ reactor.listenUNIX, filename, serverFactory, wantPID=True)
+
+ def stoppedListening(ign):
+ unixPort = reactor.listenUNIX(filename, serverFactory, wantPID=True)
+ return unixPort.stopListening()
+
+ return unixPort.stopListening().addCallback(stoppedListening)
+
+
+ def _uncleanSocketTest(self, callback):
+ self.filename = self.mktemp()
+ source = ("from twisted.internet import protocol, reactor\n"
+ "reactor.listenUNIX(%r, protocol.ServerFactory(), wantPID=True)\n") % (self.filename,)
+ env = {'PYTHONPATH': os.pathsep.join(sys.path)}
+
+ d = utils.getProcessValue(sys.executable, ("-u", "-c", source), env=env)
+ d.addCallback(callback)
+ return d
+
+
+ def test_uncleanServerSocketLocking(self):
+ """
+ If passed C{True} for the C{wantPID} parameter, a server can be started
+ listening with L{IReactorUNIX.listenUNIX} when passed the name of a
+ file on which a previous server which has not exited cleanly has been
+ listening using the C{wantPID} option.
+ """
+ def ranStupidChild(ign):
+ # If this next call succeeds, our lock handling is correct.
+ p = reactor.listenUNIX(self.filename, MyServerFactory(), wantPID=True)
+ return p.stopListening()
+ return self._uncleanSocketTest(ranStupidChild)
+
+
+ def test_connectToUncleanServer(self):
+ """
+ If passed C{True} for the C{checkPID} parameter, a client connection
+ attempt made with L{IReactorUNIX.connectUNIX} fails with
+ L{error.BadFileError}.
+ """
+ def ranStupidChild(ign):
+ d = defer.Deferred()
+ f = FailedConnectionClientFactory(d)
+ c = reactor.connectUNIX(self.filename, f, checkPID=True)
+ return self.assertFailure(d, error.BadFileError)
+ return self._uncleanSocketTest(ranStupidChild)
+
+
+ def _reprTest(self, serverFactory, factoryName):
+ """
+ Test the C{__str__} and C{__repr__} implementations of a UNIX port when
+ used with the given factory.
+ """
+ filename = self.mktemp()
+ unixPort = reactor.listenUNIX(filename, serverFactory)
+
+ connectedString = "<%s on %r>" % (factoryName, filename)
+ self.assertEqual(repr(unixPort), connectedString)
+ self.assertEqual(str(unixPort), connectedString)
+
+ d = defer.maybeDeferred(unixPort.stopListening)
+ def stoppedListening(ign):
+ unconnectedString = "<%s (not listening)>" % (factoryName,)
+ self.assertEqual(repr(unixPort), unconnectedString)
+ self.assertEqual(str(unixPort), unconnectedString)
+ d.addCallback(stoppedListening)
+ return d
+
+
+ def test_reprWithClassicFactory(self):
+ """
+ The two string representations of the L{IListeningPort} returned by
+ L{IReactorUNIX.listenUNIX} contains the name of the classic factory
+ class being used and the filename on which the port is listening or
+ indicates that the port is not listening.
+ """
+ class ClassicFactory:
+ def doStart(self):
+ pass
+
+ def doStop(self):
+ pass
+
+ # Sanity check
+ self.assertIsInstance(ClassicFactory, types.ClassType)
+
+ return self._reprTest(
+ ClassicFactory(), "twisted.test.test_unix.ClassicFactory")
+
+
+ def test_reprWithNewStyleFactory(self):
+ """
+ The two string representations of the L{IListeningPort} returned by
+ L{IReactorUNIX.listenUNIX} contains the name of the new-style factory
+ class being used and the filename on which the port is listening or
+ indicates that the port is not listening.
+ """
+ class NewStyleFactory(object):
+ def doStart(self):
+ pass
+
+ def doStop(self):
+ pass
+
+ # Sanity check
+ self.assertIsInstance(NewStyleFactory, type)
+
+ return self._reprTest(
+ NewStyleFactory(), "twisted.test.test_unix.NewStyleFactory")
+
+
+
+class ClientProto(protocol.ConnectedDatagramProtocol):
+ started = stopped = False
+ gotback = None
+
+ def __init__(self):
+ self.deferredStarted = defer.Deferred()
+ self.deferredGotBack = defer.Deferred()
+
+ def stopProtocol(self):
+ self.stopped = True
+
+ def startProtocol(self):
+ self.started = True
+ self.deferredStarted.callback(None)
+
+ def datagramReceived(self, data):
+ self.gotback = data
+ self.deferredGotBack.callback(None)
+
+class ServerProto(protocol.DatagramProtocol):
+ started = stopped = False
+ gotwhat = gotfrom = None
+
+ def __init__(self):
+ self.deferredStarted = defer.Deferred()
+ self.deferredGotWhat = defer.Deferred()
+
+ def stopProtocol(self):
+ self.stopped = True
+
+ def startProtocol(self):
+ self.started = True
+ self.deferredStarted.callback(None)
+
+ def datagramReceived(self, data, addr):
+ self.gotfrom = addr
+ self.transport.write("hi back", addr)
+ self.gotwhat = data
+ self.deferredGotWhat.callback(None)
+
+
+
+class DatagramUnixSocketTestCase(unittest.TestCase):
+ """
+ Test datagram UNIX sockets.
+ """
+ def test_exchange(self):
+ """
+ Test that a datagram can be sent to and received by a server and vice
+ versa.
+ """
+ clientaddr = self.mktemp()
+ serveraddr = self.mktemp()
+ sp = ServerProto()
+ cp = ClientProto()
+ s = reactor.listenUNIXDatagram(serveraddr, sp)
+ self.addCleanup(s.stopListening)
+ c = reactor.connectUNIXDatagram(serveraddr, cp, bindAddress=clientaddr)
+ self.addCleanup(c.stopListening)
+
+ d = defer.gatherResults([sp.deferredStarted, cp.deferredStarted])
+ def write(ignored):
+ cp.transport.write("hi")
+ return defer.gatherResults([sp.deferredGotWhat,
+ cp.deferredGotBack])
+
+ def _cbTestExchange(ignored):
+ self.assertEqual("hi", sp.gotwhat)
+ self.assertEqual(clientaddr, sp.gotfrom)
+ self.assertEqual("hi back", cp.gotback)
+
+ d.addCallback(write)
+ d.addCallback(_cbTestExchange)
+ return d
+
+
+ def test_cannotListen(self):
+ """
+ L{IReactorUNIXDatagram.listenUNIXDatagram} raises
+ L{error.CannotListenError} if the unix socket specified is already in
+ use.
+ """
+ addr = self.mktemp()
+ p = ServerProto()
+ s = reactor.listenUNIXDatagram(addr, p)
+ self.failUnlessRaises(error.CannotListenError, reactor.listenUNIXDatagram, addr, p)
+ s.stopListening()
+ os.unlink(addr)
+
+ # test connecting to bound and connected (somewhere else) address
+
+ def _reprTest(self, serverProto, protocolName):
+ """
+ Test the C{__str__} and C{__repr__} implementations of a UNIX datagram
+ port when used with the given protocol.
+ """
+ filename = self.mktemp()
+ unixPort = reactor.listenUNIXDatagram(filename, serverProto)
+
+ connectedString = "<%s on %r>" % (protocolName, filename)
+ self.assertEqual(repr(unixPort), connectedString)
+ self.assertEqual(str(unixPort), connectedString)
+
+ stopDeferred = defer.maybeDeferred(unixPort.stopListening)
+ def stoppedListening(ign):
+ unconnectedString = "<%s (not listening)>" % (protocolName,)
+ self.assertEqual(repr(unixPort), unconnectedString)
+ self.assertEqual(str(unixPort), unconnectedString)
+ stopDeferred.addCallback(stoppedListening)
+ return stopDeferred
+
+
+ def test_reprWithClassicProtocol(self):
+ """
+ The two string representations of the L{IListeningPort} returned by
+ L{IReactorUNIXDatagram.listenUNIXDatagram} contains the name of the
+ classic protocol class being used and the filename on which the port is
+ listening or indicates that the port is not listening.
+ """
+ class ClassicProtocol:
+ def makeConnection(self, transport):
+ pass
+
+ def doStop(self):
+ pass
+
+ # Sanity check
+ self.assertIsInstance(ClassicProtocol, types.ClassType)
+
+ return self._reprTest(
+ ClassicProtocol(), "twisted.test.test_unix.ClassicProtocol")
+
+
+ def test_reprWithNewStyleProtocol(self):
+ """
+ The two string representations of the L{IListeningPort} returned by
+ L{IReactorUNIXDatagram.listenUNIXDatagram} contains the name of the
+ new-style protocol class being used and the filename on which the port
+ is listening or indicates that the port is not listening.
+ """
+ class NewStyleProtocol(object):
+ def makeConnection(self, transport):
+ pass
+
+ def doStop(self):
+ pass
+
+ # Sanity check
+ self.assertIsInstance(NewStyleProtocol, type)
+
+ return self._reprTest(
+ NewStyleProtocol(), "twisted.test.test_unix.NewStyleProtocol")
+
+
+
+if not interfaces.IReactorUNIX(reactor, None):
+ UnixSocketTestCase.skip = "This reactor does not support UNIX domain sockets"
+if not interfaces.IReactorUNIXDatagram(reactor, None):
+ DatagramUnixSocketTestCase.skip = "This reactor does not support UNIX datagram sockets"
diff --git a/twisted/test/test_usage.py b/twisted/test/test_usage.py
new file mode 100644
index 0000000..5a20f01
--- /dev/null
+++ b/twisted/test/test_usage.py
@@ -0,0 +1,584 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.python.usage}, a command line option parsing library.
+"""
+
+from twisted.trial import unittest
+from twisted.python import usage
+
+
+class WellBehaved(usage.Options):
+ optParameters = [['long', 'w', 'default', 'and a docstring'],
+ ['another', 'n', 'no docstring'],
+ ['longonly', None, 'noshort'],
+ ['shortless', None, 'except',
+ 'this one got docstring'],
+ ]
+ optFlags = [['aflag', 'f',
+ """
+
+ flagallicious docstringness for this here
+
+ """],
+ ['flout', 'o'],
+ ]
+
+ def opt_myflag(self):
+ self.opts['myflag'] = "PONY!"
+
+
+ def opt_myparam(self, value):
+ self.opts['myparam'] = "%s WITH A PONY!" % (value,)
+
+
+
+class ParseCorrectnessTest(unittest.TestCase):
+ """
+ Test Options.parseArgs for correct values under good conditions.
+ """
+ def setUp(self):
+ """
+ Instantiate and parseOptions a well-behaved Options class.
+ """
+
+ self.niceArgV = ("--long Alpha -n Beta "
+ "--shortless Gamma -f --myflag "
+ "--myparam Tofu").split()
+
+ self.nice = WellBehaved()
+
+ self.nice.parseOptions(self.niceArgV)
+
+ def test_checkParameters(self):
+ """
+ Checking that parameters have correct values.
+ """
+ self.assertEqual(self.nice.opts['long'], "Alpha")
+ self.assertEqual(self.nice.opts['another'], "Beta")
+ self.assertEqual(self.nice.opts['longonly'], "noshort")
+ self.assertEqual(self.nice.opts['shortless'], "Gamma")
+
+ def test_checkFlags(self):
+ """
+ Checking that flags have correct values.
+ """
+ self.assertEqual(self.nice.opts['aflag'], 1)
+ self.assertEqual(self.nice.opts['flout'], 0)
+
+ def test_checkCustoms(self):
+ """
+ Checking that custom flags and parameters have correct values.
+ """
+ self.assertEqual(self.nice.opts['myflag'], "PONY!")
+ self.assertEqual(self.nice.opts['myparam'], "Tofu WITH A PONY!")
+
+
+
+class TypedOptions(usage.Options):
+ optParameters = [
+ ['fooint', None, 392, 'Foo int', int],
+ ['foofloat', None, 4.23, 'Foo float', float],
+ ['eggint', None, None, 'Egg int without default', int],
+ ['eggfloat', None, None, 'Egg float without default', float],
+ ]
+
+ def opt_under_score(self, value):
+ """
+ This option has an underscore in its name to exercise the _ to -
+ translation.
+ """
+ self.underscoreValue = value
+ opt_u = opt_under_score
+
+
+
+class TypedTestCase(unittest.TestCase):
+ """
+ Test Options.parseArgs for options with forced types.
+ """
+ def setUp(self):
+ self.usage = TypedOptions()
+
+ def test_defaultValues(self):
+ """
+ Test parsing of default values.
+ """
+ argV = []
+ self.usage.parseOptions(argV)
+ self.assertEqual(self.usage.opts['fooint'], 392)
+ self.assert_(isinstance(self.usage.opts['fooint'], int))
+ self.assertEqual(self.usage.opts['foofloat'], 4.23)
+ self.assert_(isinstance(self.usage.opts['foofloat'], float))
+ self.assertEqual(self.usage.opts['eggint'], None)
+ self.assertEqual(self.usage.opts['eggfloat'], None)
+
+
+ def test_parsingValues(self):
+ """
+ Test basic parsing of int and float values.
+ """
+ argV = ("--fooint 912 --foofloat -823.1 "
+ "--eggint 32 --eggfloat 21").split()
+ self.usage.parseOptions(argV)
+ self.assertEqual(self.usage.opts['fooint'], 912)
+ self.assert_(isinstance(self.usage.opts['fooint'], int))
+ self.assertEqual(self.usage.opts['foofloat'], -823.1)
+ self.assert_(isinstance(self.usage.opts['foofloat'], float))
+ self.assertEqual(self.usage.opts['eggint'], 32)
+ self.assert_(isinstance(self.usage.opts['eggint'], int))
+ self.assertEqual(self.usage.opts['eggfloat'], 21.)
+ self.assert_(isinstance(self.usage.opts['eggfloat'], float))
+
+
+ def test_underscoreOption(self):
+ """
+ A dash in an option name is translated to an underscore before being
+ dispatched to a handler.
+ """
+ self.usage.parseOptions(['--under-score', 'foo'])
+ self.assertEqual(self.usage.underscoreValue, 'foo')
+
+
+ def test_underscoreOptionAlias(self):
+ """
+ An option name with a dash in it can have an alias.
+ """
+ self.usage.parseOptions(['-u', 'bar'])
+ self.assertEqual(self.usage.underscoreValue, 'bar')
+
+
+ def test_invalidValues(self):
+ """
+ Check that passing wrong values raises an error.
+ """
+ argV = "--fooint egg".split()
+ self.assertRaises(usage.UsageError, self.usage.parseOptions, argV)
+
+
+
+class WrongTypedOptions(usage.Options):
+ optParameters = [
+ ['barwrong', None, None, 'Bar with wrong coerce', 'he']
+ ]
+
+
+class WeirdCallableOptions(usage.Options):
+ def _bar(value):
+ raise RuntimeError("Ouch")
+ def _foo(value):
+ raise ValueError("Yay")
+ optParameters = [
+ ['barwrong', None, None, 'Bar with strange callable', _bar],
+ ['foowrong', None, None, 'Foo with strange callable', _foo]
+ ]
+
+
+class WrongTypedTestCase(unittest.TestCase):
+ """
+ Test Options.parseArgs for wrong coerce options.
+ """
+ def test_nonCallable(self):
+ """
+ Check that using a non callable type fails.
+ """
+ us = WrongTypedOptions()
+ argV = "--barwrong egg".split()
+ self.assertRaises(TypeError, us.parseOptions, argV)
+
+ def test_notCalledInDefault(self):
+ """
+ Test that the coerce functions are not called if no values are
+ provided.
+ """
+ us = WeirdCallableOptions()
+ argV = []
+ us.parseOptions(argV)
+
+ def test_weirdCallable(self):
+ """
+ Test what happens when coerce functions raise errors.
+ """
+ us = WeirdCallableOptions()
+ argV = "--foowrong blah".split()
+ # ValueError is swallowed as UsageError
+ e = self.assertRaises(usage.UsageError, us.parseOptions, argV)
+ self.assertEqual(str(e), "Parameter type enforcement failed: Yay")
+
+ us = WeirdCallableOptions()
+ argV = "--barwrong blah".split()
+ # RuntimeError is not swallowed
+ self.assertRaises(RuntimeError, us.parseOptions, argV)
+
+
+class OutputTest(unittest.TestCase):
+ def test_uppercasing(self):
+ """
+ Error output case adjustment does not mangle options
+ """
+ opt = WellBehaved()
+ e = self.assertRaises(usage.UsageError,
+ opt.parseOptions, ['-Z'])
+ self.assertEqual(str(e), 'option -Z not recognized')
+
+
+class InquisitionOptions(usage.Options):
+ optFlags = [
+ ('expect', 'e'),
+ ]
+ optParameters = [
+ ('torture-device', 't',
+ 'comfy-chair',
+ 'set preferred torture device'),
+ ]
+
+
+class HolyQuestOptions(usage.Options):
+ optFlags = [('horseback', 'h',
+ 'use a horse'),
+ ('for-grail', 'g'),
+ ]
+
+
+class SubCommandOptions(usage.Options):
+ optFlags = [('europian-swallow', None,
+ 'set default swallow type to Europian'),
+ ]
+ subCommands = [
+ ('inquisition', 'inquest', InquisitionOptions,
+ 'Perform an inquisition'),
+ ('holyquest', 'quest', HolyQuestOptions,
+ 'Embark upon a holy quest'),
+ ]
+
+
+class SubCommandTest(unittest.TestCase):
+
+ def test_simpleSubcommand(self):
+ o = SubCommandOptions()
+ o.parseOptions(['--europian-swallow', 'inquisition'])
+ self.assertEqual(o['europian-swallow'], True)
+ self.assertEqual(o.subCommand, 'inquisition')
+ self.failUnless(isinstance(o.subOptions, InquisitionOptions))
+ self.assertEqual(o.subOptions['expect'], False)
+ self.assertEqual(o.subOptions['torture-device'], 'comfy-chair')
+
+ def test_subcommandWithFlagsAndOptions(self):
+ o = SubCommandOptions()
+ o.parseOptions(['inquisition', '--expect', '--torture-device=feather'])
+ self.assertEqual(o['europian-swallow'], False)
+ self.assertEqual(o.subCommand, 'inquisition')
+ self.failUnless(isinstance(o.subOptions, InquisitionOptions))
+ self.assertEqual(o.subOptions['expect'], True)
+ self.assertEqual(o.subOptions['torture-device'], 'feather')
+
+ def test_subcommandAliasWithFlagsAndOptions(self):
+ o = SubCommandOptions()
+ o.parseOptions(['inquest', '--expect', '--torture-device=feather'])
+ self.assertEqual(o['europian-swallow'], False)
+ self.assertEqual(o.subCommand, 'inquisition')
+ self.failUnless(isinstance(o.subOptions, InquisitionOptions))
+ self.assertEqual(o.subOptions['expect'], True)
+ self.assertEqual(o.subOptions['torture-device'], 'feather')
+
+ def test_anotherSubcommandWithFlagsAndOptions(self):
+ o = SubCommandOptions()
+ o.parseOptions(['holyquest', '--for-grail'])
+ self.assertEqual(o['europian-swallow'], False)
+ self.assertEqual(o.subCommand, 'holyquest')
+ self.failUnless(isinstance(o.subOptions, HolyQuestOptions))
+ self.assertEqual(o.subOptions['horseback'], False)
+ self.assertEqual(o.subOptions['for-grail'], True)
+
+ def test_noSubcommand(self):
+ o = SubCommandOptions()
+ o.parseOptions(['--europian-swallow'])
+ self.assertEqual(o['europian-swallow'], True)
+ self.assertEqual(o.subCommand, None)
+ self.failIf(hasattr(o, 'subOptions'))
+
+ def test_defaultSubcommand(self):
+ o = SubCommandOptions()
+ o.defaultSubCommand = 'inquest'
+ o.parseOptions(['--europian-swallow'])
+ self.assertEqual(o['europian-swallow'], True)
+ self.assertEqual(o.subCommand, 'inquisition')
+ self.failUnless(isinstance(o.subOptions, InquisitionOptions))
+ self.assertEqual(o.subOptions['expect'], False)
+ self.assertEqual(o.subOptions['torture-device'], 'comfy-chair')
+
+ def test_subCommandParseOptionsHasParent(self):
+ class SubOpt(usage.Options):
+ def parseOptions(self, *a, **kw):
+ self.sawParent = self.parent
+ usage.Options.parseOptions(self, *a, **kw)
+ class Opt(usage.Options):
+ subCommands = [
+ ('foo', 'f', SubOpt, 'bar'),
+ ]
+ o = Opt()
+ o.parseOptions(['foo'])
+ self.failUnless(hasattr(o.subOptions, 'sawParent'))
+ self.assertEqual(o.subOptions.sawParent , o)
+
+ def test_subCommandInTwoPlaces(self):
+ """
+ The .parent pointer is correct even when the same Options class is
+ used twice.
+ """
+ class SubOpt(usage.Options):
+ pass
+ class OptFoo(usage.Options):
+ subCommands = [
+ ('foo', 'f', SubOpt, 'quux'),
+ ]
+ class OptBar(usage.Options):
+ subCommands = [
+ ('bar', 'b', SubOpt, 'quux'),
+ ]
+ oFoo = OptFoo()
+ oFoo.parseOptions(['foo'])
+ oBar=OptBar()
+ oBar.parseOptions(['bar'])
+ self.failUnless(hasattr(oFoo.subOptions, 'parent'))
+ self.failUnless(hasattr(oBar.subOptions, 'parent'))
+ self.failUnlessIdentical(oFoo.subOptions.parent, oFoo)
+ self.failUnlessIdentical(oBar.subOptions.parent, oBar)
+
+
+class HelpStringTest(unittest.TestCase):
+ def setUp(self):
+ """
+ Instantiate a well-behaved Options class.
+ """
+
+ self.niceArgV = ("--long Alpha -n Beta "
+ "--shortless Gamma -f --myflag "
+ "--myparam Tofu").split()
+
+ self.nice = WellBehaved()
+
+ def test_noGoBoom(self):
+ """
+ __str__ shouldn't go boom.
+ """
+ try:
+ self.nice.__str__()
+ except Exception, e:
+ self.fail(e)
+
+ def test_whitespaceStripFlagsAndParameters(self):
+ """
+ Extra whitespace in flag and parameters docs is stripped.
+ """
+ # We test this by making sure aflag and it's help string are on the
+ # same line.
+ lines = [s for s in str(self.nice).splitlines() if s.find("aflag")>=0]
+ self.failUnless(len(lines) > 0)
+ self.failUnless(lines[0].find("flagallicious") >= 0)
+
+
+class PortCoerceTestCase(unittest.TestCase):
+ """
+ Test the behavior of L{usage.portCoerce}.
+ """
+ def test_validCoerce(self):
+ """
+ Test the answers with valid input.
+ """
+ self.assertEqual(0, usage.portCoerce("0"))
+ self.assertEqual(3210, usage.portCoerce("3210"))
+ self.assertEqual(65535, usage.portCoerce("65535"))
+
+ def test_errorCoerce(self):
+ """
+ Test error path.
+ """
+ self.assertRaises(ValueError, usage.portCoerce, "")
+ self.assertRaises(ValueError, usage.portCoerce, "-21")
+ self.assertRaises(ValueError, usage.portCoerce, "212189")
+ self.assertRaises(ValueError, usage.portCoerce, "foo")
+
+
+
+class ZshCompleterTestCase(unittest.TestCase):
+ """
+ Test the behavior of the various L{twisted.usage.Completer} classes
+ for producing output usable by zsh tab-completion system.
+ """
+ def test_completer(self):
+ """
+ Completer produces zsh shell-code that produces no completion matches.
+ """
+ c = usage.Completer()
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:')
+
+ c = usage.Completer(descr='some action', repeat=True)
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, '*:some action:')
+
+
+ def test_files(self):
+ """
+ CompleteFiles produces zsh shell-code that completes file names
+ according to a glob.
+ """
+ c = usage.CompleteFiles()
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option (*):_files -g "*"')
+
+ c = usage.CompleteFiles('*.py')
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option (*.py):_files -g "*.py"')
+
+ c = usage.CompleteFiles('*.py', descr="some action", repeat=True)
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, '*:some action (*.py):_files -g "*.py"')
+
+
+ def test_dirs(self):
+ """
+ CompleteDirs produces zsh shell-code that completes directory names.
+ """
+ c = usage.CompleteDirs()
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:_directories')
+
+ c = usage.CompleteDirs(descr="some action", repeat=True)
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, '*:some action:_directories')
+
+
+ def test_list(self):
+ """
+ CompleteList produces zsh shell-code that completes words from a fixed
+ list of possibilities.
+ """
+ c = usage.CompleteList('ABC')
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:(A B C)')
+
+ c = usage.CompleteList(['1', '2', '3'])
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:(1 2 3)')
+
+ c = usage.CompleteList(['1', '2', '3'], descr='some action',
+ repeat=True)
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, '*:some action:(1 2 3)')
+
+
+ def test_multiList(self):
+ """
+ CompleteMultiList produces zsh shell-code that completes multiple
+ comma-separated words from a fixed list of possibilities.
+ """
+ c = usage.CompleteMultiList('ABC')
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:_values -s , \'some-option\' A B C')
+
+ c = usage.CompleteMultiList(['1','2','3'])
+ got = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(got, ':some-option:_values -s , \'some-option\' 1 2 3')
+
+ c = usage.CompleteMultiList(['1','2','3'], descr='some action',
+ repeat=True)
+ got = c._shellCode('some-option', usage._ZSH)
+ expected = '*:some action:_values -s , \'some action\' 1 2 3'
+ self.assertEqual(got, expected)
+
+
+ def test_usernames(self):
+ """
+ CompleteUsernames produces zsh shell-code that completes system
+ usernames.
+ """
+ c = usage.CompleteUsernames()
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, ':some-option:_users')
+
+ c = usage.CompleteUsernames(descr='some action', repeat=True)
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, '*:some action:_users')
+
+
+ def test_groups(self):
+ """
+ CompleteGroups produces zsh shell-code that completes system group
+ names.
+ """
+ c = usage.CompleteGroups()
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, ':group:_groups')
+
+ c = usage.CompleteGroups(descr='some action', repeat=True)
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, '*:some action:_groups')
+
+
+ def test_hostnames(self):
+ """
+ CompleteHostnames produces zsh shell-code that completes hostnames.
+ """
+ c = usage.CompleteHostnames()
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, ':some-option:_hosts')
+
+ c = usage.CompleteHostnames(descr='some action', repeat=True)
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, '*:some action:_hosts')
+
+
+ def test_userAtHost(self):
+ """
+ CompleteUserAtHost produces zsh shell-code that completes hostnames or
+ a word of the form <username>@<hostname>.
+ """
+ c = usage.CompleteUserAtHost()
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertTrue(out.startswith(':host | user@host:'))
+
+ c = usage.CompleteUserAtHost(descr='some action', repeat=True)
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertTrue(out.startswith('*:some action:'))
+
+
+ def test_netInterfaces(self):
+ """
+ CompleteNetInterfaces produces zsh shell-code that completes system
+ network interface names.
+ """
+ c = usage.CompleteNetInterfaces()
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, ':some-option:_net_interfaces')
+
+ c = usage.CompleteNetInterfaces(descr='some action', repeat=True)
+ out = c._shellCode('some-option', usage._ZSH)
+ self.assertEqual(out, '*:some action:_net_interfaces')
+
+
+
+class CompleterNotImplementedTestCase(unittest.TestCase):
+ """
+ Using an unknown shell constant with the various Completer() classes
+ should raise NotImplementedError
+ """
+ def test_unknownShell(self):
+ """
+ Using an unknown shellType should raise NotImplementedError
+ """
+ classes = [usage.Completer, usage.CompleteFiles,
+ usage.CompleteDirs, usage.CompleteList,
+ usage.CompleteMultiList, usage.CompleteUsernames,
+ usage.CompleteGroups, usage.CompleteHostnames,
+ usage.CompleteUserAtHost, usage.CompleteNetInterfaces]
+
+ for cls in classes:
+ try:
+ action = cls()
+ except:
+ action = cls(None)
+ self.assertRaises(NotImplementedError, action._shellCode,
+ None, "bad_shell_type")
diff --git a/twisted/test/testutils.py b/twisted/test/testutils.py
new file mode 100644
index 0000000..a310ea2
--- /dev/null
+++ b/twisted/test/testutils.py
@@ -0,0 +1,55 @@
+from cStringIO import StringIO
+from twisted.internet.protocol import FileWrapper
+
+class IOPump:
+ """Utility to pump data between clients and servers for protocol testing.
+
+ Perhaps this is a utility worthy of being in protocol.py?
+ """
+ def __init__(self, client, server, clientIO, serverIO):
+ self.client = client
+ self.server = server
+ self.clientIO = clientIO
+ self.serverIO = serverIO
+
+ def flush(self):
+ "Pump until there is no more input or output."
+ while self.pump():
+ pass
+
+ def pump(self):
+ """Move data back and forth.
+
+ Returns whether any data was moved.
+ """
+ self.clientIO.seek(0)
+ self.serverIO.seek(0)
+ cData = self.clientIO.read()
+ sData = self.serverIO.read()
+ self.clientIO.seek(0)
+ self.serverIO.seek(0)
+ self.clientIO.truncate()
+ self.serverIO.truncate()
+ for byte in cData:
+ self.server.dataReceived(byte)
+ for byte in sData:
+ self.client.dataReceived(byte)
+ if cData or sData:
+ return 1
+ else:
+ return 0
+
+
+def returnConnected(server, client):
+ """Take two Protocol instances and connect them.
+ """
+ cio = StringIO()
+ sio = StringIO()
+ client.makeConnection(FileWrapper(cio))
+ server.makeConnection(FileWrapper(sio))
+ pump = IOPump(client, server, cio, sio)
+ # Challenge-response authentication:
+ pump.flush()
+ # Uh...
+ pump.flush()
+ return pump
diff --git a/twisted/test/time_helpers.py b/twisted/test/time_helpers.py
new file mode 100644
index 0000000..a64f95c
--- /dev/null
+++ b/twisted/test/time_helpers.py
@@ -0,0 +1,72 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Helper class to writing deterministic time-based unit tests.
+
+Do not use this module. It is a lie. See L{twisted.internet.task.Clock}
+instead.
+"""
+
+import warnings
+warnings.warn(
+ "twisted.test.time_helpers is deprecated since Twisted 10.0. "
+ "See twisted.internet.task.Clock instead.",
+ category=DeprecationWarning, stacklevel=2)
+
+
+class Clock(object):
+ """
+ A utility for monkey-patches various parts of Twisted to use a
+ simulated timing mechanism. DO NOT use this class. Use
+ L{twisted.internet.task.Clock}.
+ """
+ rightNow = 0.0
+
+ def __call__(self):
+ """
+ Return the current simulated time.
+ """
+ return self.rightNow
+
+ def install(self):
+ """
+ Monkeypatch L{twisted.internet.reactor.seconds} to use
+ L{__call__} as a time source
+ """
+ # Violation is fun.
+ from twisted.internet import reactor
+ self.reactor_original = reactor.seconds
+ reactor.seconds = self
+
+ def uninstall(self):
+ """
+ Remove the monkeypatching of L{twisted.internet.reactor.seconds}.
+ """
+ from twisted.internet import reactor
+ reactor.seconds = self.reactor_original
+
+ def adjust(self, amount):
+ """
+ Adjust the current simulated time upward by the given C{amount}.
+
+ Note that this does not cause any scheduled calls to be run.
+ """
+ self.rightNow += amount
+
+ def pump(self, reactor, timings):
+ """
+ Iterate the given C{reactor} with increments of time specified
+ by C{timings}.
+
+ For each timing, the simulated time will be L{adjust}ed and
+ the reactor will be iterated twice.
+ """
+ timings = list(timings)
+ timings.reverse()
+ self.adjust(timings.pop())
+ while timings:
+ self.adjust(timings.pop())
+ reactor.iterate()
+ reactor.iterate()
+
diff --git a/twisted/topfiles/CREDITS b/twisted/topfiles/CREDITS
new file mode 100644
index 0000000..a4eeece
--- /dev/null
+++ b/twisted/topfiles/CREDITS
@@ -0,0 +1,60 @@
+The Matrix
+
+- Glyph "Glyph" Lefkowitz <glyph@twistedmatrix.com>
+ electric violin
+- Sean "Riley" Riley <sean@twistedmatrix.com>
+ grand piano
+- Allen "Dash" Short <washort@twistedmatrix.com>
+ vocals and guitar
+- Christopher "Radix" Armstrong <radix@twistedmatrix.com>
+ percussion
+- Paul "z3p" Swartz <z3p@twistedmatrix.com>
+ oboe
+- Jürgen "snibril" Hermann <jh@twistedmatrix.com>
+ synthesizer
+- Moshe "vertical" Zadka <moshez@twistedmatrix.com>
+ accordion
+- Benjamin Bruheim <grolgh@online.no>
+ kazoo
+- Travis B. "Nafai" Hartwell <nafai@twistedmatrix.com>
+ keyboards
+- Itamar "itamar" Shtull-Trauring <twisted@itamarst.org>
+ alto recorder
+- Andrew "spiv" Bennetts <andrew@puzzling.org>
+ glockenspiel
+- Kevin "Acapnotic" Turner <acapnotic@twistedmatrix.com>
+ trombone
+- Donovan "fzZzy" Preston <dp@twistedmatrix.com>
+ bass and harmonium
+- Jp "exarkun" Calderone <exarkun@twistedmatrix.com>
+ geopolitical sociographic dissonance engine
+- Gavin "skreech" Cooper <coop@coopweb.org>
+ torque wrench
+- Jonathan "jml" Lange <jml@twistedmatrix.com>
+ pipe organ
+- Bob "etrepum" Ippolito <bob@redivi.com>
+ low frequency oscillator
+- Pavel "PenguinOfDoom" Pergamenshchik <ppergame@gmail.com>
+ electronic balalaika
+- Jonathan D. "slyphon" Simms <slyphon@twistedmatrix.com>
+ theramin and drums
+- Brian "warner" Warner <warner@twistedmatrix.com>
+ hertzian field renderer
+- Mary Gardiner <mary-twisted@puzzling.org>
+ krummhorn
+- Eric "teratorn" Mangold <teratorn@twistedmatrix.com>
+ serpentine bassoon
+- Tommi "Tv" Virtanen <tv@twistedmatrix.com>
+ didgeridoo
+- Justin "justinj" Johnson <justinj@twistedmatrix.com>
+ bass mandolin
+- Ralph "ralphm" Meijer <twisted@ralphm.ik.nu>
+ vocals and timbales
+- David "dreid" Reid <dreid@dreid.org>
+ banjo
+
+Extras
+
+- Jerry Hebert <jerry@cynics.org>
+- Nick Moffit <nick@zork.org>
+- Jeremy Fincher
diff --git a/twisted/topfiles/ChangeLog.Old b/twisted/topfiles/ChangeLog.Old
new file mode 100644
index 0000000..30594b2
--- /dev/null
+++ b/twisted/topfiles/ChangeLog.Old
@@ -0,0 +1,3888 @@
+2005-03-12 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/scripts/mktap.py, twisted/scripts/twistd.py,
+ twisted/application/app.py: Changed UID and GID defaults for Process
+ to None. Changed mktap behavior to not specify UID and GID if they
+ are not given on the command line. Changed application startup to
+ not change UID or GID if they are not given. Changed twistd to add
+ UID and GID setting command line arguments.
+
+2005-02-10 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/defer.py: DeferredLock, DeferredSemaphore, and
+ DeferredQueue added.
+
+ * twisted/test/test_defer.py: Tests for above mentioned three new
+ classes.
+
+2004-11-27 Brian Warner <warner@lothar.com>
+
+ * util.py (SignalStateManager.save): don't save signal handlers
+ for SIGKILL and SIGSTOP, since we can't set them anyway.
+ Python2.4c1 raises an error when you try.
+
+2004-11-07 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_internet.py: correctly check for SSL support.
+ Improve timeout for testCallLater and testGetDelayedCalls to avoid
+ spurious failures on slow test systems. Close sockets in
+ PortStringification to fix trial warnings.
+
+ * twisted/internet/ssl.py: add a comment describing the correct
+ way to import twisted.internet.ssl (since it might partially fail
+ if OpenSSL is not available)
+
+2004-11-06 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/trial/assertions.py: assertRaises/failUnlessRaises now
+ returns the caught exception to allow tests to inspect the contents.
+
+2004-11-02 Brian Warner <warner@lothar.com>
+
+ * loopback.py (loopbackTCP): use trial's spinWhile and spinUntil
+ primitives instead of doing reactor.iterate() ourselves. Make sure
+ to wait for everything before finishing.
+
+2004-10-26 Cory Dodt <corydodt@twistedmatrix.com>
+
+ * twisted/python/{which,process}.py,
+ twisted/test/{test_wprocess,wprocess_for_testing}.py,
+ twisted/internet/{default,error,wprocess,process}.py: back out
+ wprocess due to test failures in wprocess and new trial. Resolves
+ issue 760.
+
+2004-10-24 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * TCP: Half-close of write and read for TCP connections, including
+ protocol notification for protocols that implement
+ IHalfCloseableProtocol.
+
+2004-10-07 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * Transports: Add a maximum to the number of bytes that will be
+ held in the write buffer even after they have been sent. This
+ puts a maximum on the cost of writing faster than the network
+ can accommodate.
+
+2004-10-06 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * Transports: New TCP/SSL/etc. buffering algorithm. All writes are
+ now stored until next iteration before being written, and many
+ small writes are not expensive.
+
+2004-09-30 Brian Warner <warner@lothar.com>
+
+ * glib2reactor.py: new reactor that uses just glib2, not gtk2.
+ This one doesn't require a DISPLAY, and cannot be used for GUI
+ apps.
+
+ * gtk2reactor.py: import gobject *after* pygtk.require, to make
+ sure we get the same versions of both
+
+2004-09-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/internet/defer.py: Add deferredGenerator and
+ waitForDeferred. This lets you write kinda-sorta
+ synchronous-looking code that uses Deferreds. See the
+ waitForDeferred docstring.
+
+2004-09-11 Cory Dodt <corydodt@twistedmatrix.com>
+
+ * twisted/python/{which,process}.py,
+ twisted/test/{test_wprocess,wprocess_for_testing}.py,
+ twisted/internet/{default,error,wprocess,process}.py: merge the
+ "wprocess" branch which uses Trent Mick's process.py to enable
+ spawnProcess in the default reactor on Windows
+
+2004-08-24 Brian Warner <warner@lothar.com>
+
+ * twisted/application/internet.py (TimerService): make it possible
+ to restart a stopped TimerService. Threw out a lot of (apparently)
+ unnecessary code in the process. Make sure it gets pickled in a
+ not-running state too.
+ * twisted/test/test_application.py (TestInternet2.testTimer): test
+ the changes, and update the way the test peeks inside TimerService
+
+2004-07-18 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/internet/utils.py: By passing errortoo=1, you can get
+ stderr from getProcessOutput
+
+2004-07-18 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/unix.py: if the utmp module is available, record
+ user logins/logouts into utmp/wtmp.
+
+2004-06-25 Paul Swartz <z3p@twistedmatrix.com>
+ * twisted/conch/checkers.py: Use functionality of crypt module instead
+ of an external module.
+
+2004-06-25 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/spread/banana.py: Disabled automatic import and use of
+ cBanana. PB will now use the pure-Python version of banana unless
+ cBanana is manually installed by the application.
+
+2004-06-12 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/client: added -r flag to reconnect to the server if
+ the connection is lost (closes 623).
+
+2004-06-06 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: test open callback and
+ connect/disconnect.
+
+ * twisted/enterprise/adbapi.py: add open callback support
+ and disconnect() method. Issue 480.
+
+2004-06-05 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/adbapi.py: Don't log sql exceptions (issue 631).
+ Remove deprecated api.
+
+ * twisted/news/database.py: do not use adbapi.Augmentation
+
+2004-06-03 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/gtk2reactor.py: The choice between glib event
+ loop and gtk+ event loop is determined by argument at reactor
+ install time.
+
+2004-05-31 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/sqlreflector.py: don't use Augmentation
+
+ * twisted/enterprise/populate.sql: remove
+
+ * twisted/enterprise/schema.sql: remove
+
+ * twisted/enterprise/row.py: remove deprecated classes
+
+ * twisted/enterprise/dbgadgets.py: remove
+
+ * twisted/enterprise/dbcred.py: remove
+
+ * twisted/test/test_enterprise.py: Fix Firebird test case.
+
+2004-05-21 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/gtk2reactor.py: use glib event loop directly
+ instead of gtk2's event loop if possible.
+
+2004-05-04 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted.news, twisted.protocols.nntp: Moved back into trunk
+ pending an alternate split-up strategy.
+
+2004-05-04 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted.internet.reactor.listenUDP: transport.write() on UDP
+ ports no longer supports unresolved hostnames (though deprecated
+ support still exists).
+
+2004-4-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/lore/nevowlore.py, twisted/plugins.tml: Added Nevow
+ support for lore. See docstring of twisted.lore.nevowlore.
+
+2004-4-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted.news, twisted.protocols.nntp: Moved into a third party
+ package. Deprecated backwards-compatibility exists by importing
+ from the third-party package if available.
+
+2004-4-11 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted.conch: refactored the Conch client to separate connecting
+ to a server from user authentication from client-specific actions.
+
+2004-03-23 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted.protocols.http: Small optimisation to HTTP implementation.
+ This changes return value of toChunk to a tuple of strings, rather
+ than one string.
+
+2004-4-3 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted.python.lockfile: added lockfile support, based on
+ liblockfile.
+ * twisted.internet.unix.Port: added a wantPID kwarg. If True, it
+ checks for and gets a lockfile for the UNIX socket.
+ * twisted.internet.unix.Connector: added a checkPID kwarg. If True,
+ it checks that the lockfile for the socket is current.
+
+2004-03-23 Pavel Pergamenshchik <pp64@cornell.edu>
+
+ * twisted.internet.iocp: Support for Windows IO Completion Ports.
+ Use with "--reactor=iocp" parameter to twistd or trial.
+
+2004-03-20 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted.internet: getHost(), getPeer(), buildProtocol() etc.
+ all use address objects from twisted.internet.address.
+
+ * twisted/internet/udp.py: Connected UDP support is now part of
+ the standard listenUDP-resulting UDP transport using a connect()
+ method.
+
+2004-03-18 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/application/internet.py: Changed TimerService to
+ log errors from the function it calls.
+
+ * twisted/application/test_application.py: Added test case
+ for logging of exceptions from functions TimerService calls.
+
+2004-03-07 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.2.1alpha1.
+
+2004-03-03 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/web/server.py: Fix UnsupportedMethod so that users'
+ allowedMethods are actually honored.
+
+ * twisted/web/resource.py: (Resource.render) If the resource has
+ an 'allowedMethods' attribute, pass it to UnsupportedMethod.
+
+2004-02-27 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/internet/defer.py: Add consumeErrors flag to DeferredList.
+ This takes care of the most common use-case for the recently
+ deprecated addDeferred method.
+
+2004-02-28 Dave Peticolas <dave@krondo.com>
+
+ * setup.py: install tap2rpm as a bin script
+
+ * twisted/test/test_enterprise.py: Test Firebird db. Fix typos.
+
+2004-02-27 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/internet/defer.py: Deprecated DeferredList.addDeferred. It
+ isn't as useful as it looks, and can have surprising behaviour.
+
+2004-02-25 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/protocols/dns.py: Fixed a bug in TCP support: It
+ wouldn't process any messages after the first, causing AXFR
+ queries to be totally broken (in addition to other problems in the
+ implementation of AXFR).
+
+ * twisted/names/client.py: Fixed the AXFR client (lookupZone),
+ thanks to DJB's wonderful documentation of the horribleness of
+ DNS.
+
+2004-02-25 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.2.0 final! Same as rc3.
+
+2004-02-24 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.2.0rc3 (same as rc2, with cBanana bug
+ fixed).
+
+2004-02-19 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/application/service.py (IService.disownServiceParent)
+ (IServiceCollection.removeService): These may return Deferred if they
+ have asynchronous side effects.
+
+2004-02-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.2.0rc2. Brown-paper bag release bug.
+
+2004-02-17 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.2.0rc1.
+
+2004-02-13 Brian Warner <warner@lothar.com>
+
+ * doc/howto/faq.xhtml: add entry on transport.getPeer()
+
+2004-01-31 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.2alpha2 (problem with Debian packaging).
+
+2004-01-30 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.2alpha1.
+
+2004-01-23 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/scripts/trial.py: trial now supports a --coverage
+ option, requiring Python 2.3.3. Give it a directory name (relative
+ to _trial_temp) to put code-coverage info in. It uses the stdlib
+ 'trace' module.
+
+2004-01-21 Pavel Pergamenshchik <pp64@cornell.edu>
+
+ * twisted/protocols/stateful.py: A new way to write protocols!
+ Current state is encoded as a pair (func, len). As soon as len
+ of data arrives, func is called with that amount of data. New
+ state is returned from func.
+ * twisted/test/test_stateful.py: Tests and an example, an
+ Int32StringReceiver implementation.
+
+2004-01-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/web/resource.py: The default render method of Resource
+ now supports delegating to methods of the form "render_*" where
+ "*" is the HTTP method that was used to make the
+ request. Examples: request_GET, request_HEAD, request_CONNECT, and
+ so on. This won't break any existing code - when people want to
+ use the better API, they can stop overriding 'render' and instead
+ override individual render_* methods.
+
+2004-01-13 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/soap.py: Beginning of client SOAP support.
+
+2004-01-10 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/protocols/ftp.py: Added support for partial downloads
+ and uploads to FTPClient (see the offset parameter of retrieveFile).
+
+2004-01-09 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: Add IMessageCopier interface to allow
+ for optimized implementations of message copying.
+
+2004-01-06 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/default.py (PosixReactorBase.spawnProcess): add
+ a 'childFDs' argument which allows the child's file descriptors to
+ be arbitrarily mapped to parent FDs or pipes. This allows you to
+ set up additional pipes into the child (say for a GPG passphrase
+ or separate status information).
+
+ * twisted/internet/process.py (Process): add childFDs, split out
+ ProcessReader and ProcessWriter (so that Process itself is no
+ longer also reading stdout).
+
+ * twisted/internet/protocol.py (ProcessProtocol): add new
+ childDataReceived and childConnectionLost methods, which default
+ to invoking the old methods for backwards compatibility
+
+ * twisted/test/test_process.py (FDTest): add test for childFDs
+ mapping. Also add timeouts to most tests, and make all
+ reactor.iterate() loops wait 10ms between iterations to avoid
+ spamming the CPU quite so badly. Closes issue435.
+ * twisted/test/process_fds.py: new child process for FDTest
+
+ * doc/howto/process.xhtml: document childFDs argument, add example
+
+2004-01-04 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/gladereactor.py: logs all network traffic for
+ TCP/SSL/Unix sockets, allowing traffic to be displayed.
+
+2004-01-04 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: test deleting rows not in cache
+
+ * twisted/enterprise/reflector.py: deleted rows don't have to be
+ in cache
+
+ * doc/examples/row_example.py: use KeyFactory from row_util
+
+ * doc/examples/row_util.py: add KeyFactory
+
+2003-12-31 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/defer.py (Deferred.setTimeout): if the Deferred
+ has already been called, don't bother with the timeout. This
+ happens when trial.util.deferredResult is used with a timeout
+ argument and the Deferred was created by defer.succeed().
+ * twisted/test/test_defer.py
+ (DeferredTestCase.testImmediateSuccess2): test for same
+
+2003-12-31 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/ident.py: Client and server ident implementation
+ * twisted/test/test_ident.py: Test cases for ident protocol
+
+2003-12-29 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/spread/pb.py: Changed PBServerFactory to use "protocol"
+ instance attribute for Broker creation.
+
+2003-12-26 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/server.py: display of tracebacks on web pages can
+ now be disabled by setting displayTracebacks to False on the Site
+ or by using applicable tap option. Woven does not yet use
+ this attribute.
+
+2003-12-23 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/client.py: if Host header is passed, use that
+ instead of extracting from request URL.
+
+2003-12-14 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: Frederico Di Gregorio's patch
+ adding a psycopg test case.
+
+2003-12-09 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.1, based on rc4.
+
+2003-12-06 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/wxreactor.py: Added experimental wxPython reactor,
+ which seems to work better than the twisted.internet.wxsupport.
+
+2003-12-05 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/filetransfer.py, session.py: added SFTPv3 support
+ to the Conch server.
+
+2003-12-04 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.1rc4, based on rc2. rc3 never happened!
+
+2003-12-04 Brian Warner <warner@lothar.com>
+
+ * twisted/persisted/sob.py (Persistent): fix misspelled class name,
+ add compatibility binding to "Persistant" (sic).
+
+ * twisted/test/test_sob.py: use Persistent
+ * twisted/application/service.py (Application): use Persistent
+
+2003-12-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: Added support for the
+ IDLE command (RFC 2177).
+
+2003-12-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/python/log.py: Added exception handling to
+ log publishing code. Observers which raise exceptions
+ will now be removed from the observer list.
+
+2003-12-02 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.1rc3.
+
+2003-12-01 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.1rc2 (from CVS HEAD).
+
+2003-12-01 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/python/runtime.py: Added seconds method to Platform
+ class.
+
+ * twisted/internet/base.py, twisted/internet/task.py: Changed
+ use of time.time() to use Platform.seconds() instead.
+
+2003-11-24 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/abstract.py: Changed FileDescriptor's
+ registerProducer method to immediately call the given producer's
+ stopProducing method if the FileDescriptor is in the process of
+ or has finished disconnecting.
+
+2003-11-24 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: Fix incorrect behavior of closing the
+ mailbox in response to an EXPUNGE command.
+
+2003-11-21 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/trial/runner.py: Added missing calls to setUpClass and
+ tearDownClass in SingletonRunner.
+
+2003-11-21 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.1rc1.
+
+2003-11-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: Fixed incorrect generation of
+ INTERNALDATE information.
+
+2003-11-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/abstract.py: Added an assert to
+ FileDescriptor.resumeProducing to prevent it from being
+ called when the transport is no longer connected.
+
+2003-11-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/tasks.py: LoopingCall added.
+
+2003-10-14 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/tasks.py: Deprecated scheduling API removed.
+
+2003-11-18 Jonathan Simms <jonathan@embassynetworks.com>
+
+ * twisted/protocols/ftp.py: refactored to add cred support,
+ pipelining, security.
+ * twisted/test/test_ftp.py: tests for the new ftp
+
+2003-11-18 Sam Jordan <sam@twistedmatrix.com>
+
+ * twisted/protocols/msn.py: support for MSNP8
+ * doc/examples/msn_example.py: small msn example
+
+2003-11-13 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/agent.py: support for the OpenSSH agent protocol
+ * twisted/conch/ssh/connection.py: fix broken channel retrieval code
+ * twisted/conch/ssh/userauth.py: refactoring to allow use of the agent
+ * twisted/conch/ssj/transport.py: fix intermittent test failure
+ * twisted/internet/protocol.py: add UNIX socket support to
+ ClientCreator
+ * twisted/scripts/conch.py: use the key agent if available, also
+ agent forwarding
+
+2003-11-07 Brian Warner <warner@lothar.com>
+
+ * twisted/application/app.py (getApplication): provide a more
+ constructive error message when a .tac file doesn't define
+ 'application'. Closes issue387.
+
+2003-11-01 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/common.py: use GMPy for faster math if it's
+ available
+
+2003-10-24 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.0 final. Same codebase as rc2.
+
+2003-10-24 Brian Warner <warner@lothar.com>
+
+ * doc/howto/test-standard.xhtml: Add section on how to clean up.
+
+ * twisted/test/test_conch.py: improve post-test cleanup. Addresses
+ problems seen in issue343.
+
+ * twisted/internet/base.py (ReactorBase.callLater): prefix
+ "internal" parameter names with an underscore, to avoid colliding
+ with named parameters in the user's callback invocation. Closes
+ issue347.
+ (ReactorBase.addSystemEventTrigger)
+ (ReactorBase.callWhenRunning)
+ (ReactorBase.callInThread): same
+ * doc/howto/coding-standard.xhtml (Callback Arguments): explain why
+
+2003-10-22 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.0rc2.
+
+2003-10-21 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/lore/tree.py, twisted/lore/lint.py,
+ doc/howto/stylesheet.css: add a plain 'listing' class, for file
+ listings that aren't python source or HTML. This has slightly changed
+ the classes in the generated HTML, so custom stylesheets may need
+ updating.
+
+2003-10-16 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.0alpha3.
+
+2003-10-16 Brian Warner <warner@lothar.com>
+
+ * doc/howto/pb-cred.xhtml: update for newcred. Closes issue172.
+
+2003-10-15 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/base.py: add optional debug code, enabled with
+ base.DelayedCall.debug=True . If active, the call stack which
+ invoked reactor.callLater will be recorded in each DelayedCall. If
+ an exception happens when the timer function is run, the creator
+ stack will be logged in addition to the usual log.deferr().
+
+ * twisted/internet/defer.py: add some optional debug code, enabled
+ with defer.Deferred.debug=True . If active, it will record a stack
+ trace when the Deferred is created, and another when it is first
+ invoked. AlreadyCalledErrors will be given these two stack traces,
+ making it slightly easier to find the source of the problem.
+
+2003-10-15 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.0alpha2 (alpha1 was dead in the water).
+
+2003-10-15 Brian Warner <warner@lothar.com>
+
+ * setup.py: remove cReactor/ to the sandbox. Closes issue318.
+
+2003-10-14 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/static.py: registry no longer has support for
+ getting services based on their interfaces.
+
+2003-10-14 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.1.0alpha1.
+
+2003-10-13 Bob Ippolito <bob@redivi.com>
+
+ * doc/howto/choosing-reactor.xhtml:
+ Added cfreactor/Cocoa information.
+
+ * doc/examples/cocoaDemo:
+ Removed, replaced by doc/examples/Cocoa cfreactor demos.
+
+ * doc/examples/Cocoa:
+ Moved from sandbox/etrepum/examples/PyObjC, cleaned up.
+
+ * twisted/internet/cfsupport, twisted/internet/cfreactor.py:
+ Moved from sandbox/etrepum, cleaned up.
+
+ * twisted/application/app.py:
+ Added 'cf' -> twisted.internet.cfreactor to reactorTypes
+
+ * setup.py:
+ sys.platform=='darwin' - build cfsupport, do not build cReactor.
+
+ * INSTALL:
+ Changed URL of pimp repository to shorter version.
+
+2003-10-12 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * bin/tktwistd, twisted/scripts/tktwistd.py, doc/man/tktwistd.1:
+ Removed.
+
+2003-10-12 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/spread/pb.py: Perspective Broker no longer sends
+ detailed tracebacks over the wire unless the "unsafeTracebacks"
+ attribute is set of the factory.
+
+2003-10-02 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * setup.py, twisted/test/test_dir.py, twisted/python/_c_dir.c:
+ Removed _c_dir extension module for portability and maintenance
+ reasons.
+
+2003-10-03 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/spread/util.py twisted/test/test_spread.py: Fix issue
+ 286
+
+2003-10-01 Brian Warner <warner@lothar.com>
+
+ * twisted/web/client.py (HTTPDownloader): accept either a filename
+ or a file-like object (it must respond to .write and .close, and
+ partial requests will not be used with file-like objects). errback
+ the deferred if an IOError occurs in .open, .write. or .close,
+ usually something like "permission denied" or "file system full".
+ Closes issue234.
+ * twisted/test/test_webclient.py (WebClientTestCase.write): verify
+ that the errback gets called
+
+ * twisted/scripts/trial.py (run): add --until-failure option to
+ re-run the test until something fails. Closes issue87.
+
+2003-09-30 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_conch.py (testOurServerOpenSSHClient): replace
+ reactor.run() with .iterate calls: when using .run, exceptions in
+ the server cause a hang.
+
+2003-9-29 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/tap/procmon.py twisted/plugins.tml: remove procmon
+ tap. It was crufty and hard to port properly to new application.
+
+2003-09-29 Brian Warner <warner@lothar.com>
+
+ * twisted/scripts/trial.py (Options.opt_reactor): make trial
+ accept the same reactor-name abbreviations as twistd does. Closes
+ issue69.
+ (top): add test-case-name tag
+
+ * doc/man/trial.1: document the change
+
+2003-09-28 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.8alpha3.
+
+2003-09-27 Cory Dodt <corydodt@yahoo.com>
+
+ * win32/main.aap win32/pyx.x-foo.iss.template win32/README.win32:
+ Be nice to people who don't install Python for "All Users" on win32.
+
+2003-9-18 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/application/strports.py twisted/test/test_strports.py:
+ New API/mini-language for defining ports
+
+2003-9-18 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/spider.py: removed, it was unmaintained.
+
+2003-09-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/names/authority.py twisted/test/test_names.py
+ twisted/protocols/dns.py: Client and server support for TTLs on
+ all records. All Record_* types now take a ttl= keyword
+ argument. You can pass the ttl= argument to all the record classes
+ in your pyzones, too.
+
+2003-09-19 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/application/__init__.py twisted/application/app.py
+ twisted/application/compat.py twisted/application/internet.py
+ twisted/application/service.py twisted/scripts/twistd.py
+ twisted/scripts/twistw.py twisted/scripts/mktap.py
+ twisted/scripts/tapconvert.py bin/twistw: Update to new-style
+ applications.
+
+2003-09-19 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/names/client.py: Instantiation of theResolver global made
+ lazy. As a result importing it directly will now fail if it has not
+ yet been created. It should not be used directly anymore; instead,
+ use the module-scope lookup methods, or instantiate your own
+ resolver.
+
+ * twisted/mail/relaymanager.py: Instantiation of MXCalculator made
+ lazy.
+
+2003-09-18 Stephen Thorne <stephen@thorne.id.au>
+
+ * twisted/web/distrib.py: Removed dependancy on twisted.web.widgets, and
+ instead using woven.
+
+2003-09-18 Stephen Thorne <stephen@thorne.id.au>
+
+ * doc/howto/woven-reference.html: Added this new documentation file.
+ * doc/howto/index.html: Added woven-reference to index
+ * admin/: Added woven-reference.tex to book.tex
+
+2003-09-18 Stephen Thorne <stephen@thorne.id.au>
+
+ * twisted/web/woven/widgets.py: Stop the 'Option' widget from having a
+ name="" attribute. Closes issue255.
+
+2003-09-16 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.8alpha1.
+
+ * .: Releasing Twisted 1.0.8alpha2 (Fixed Debian packages).
+
+2003-09-13 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7 (no code changes since 1.0.7rc1).
+
+ * twisted/web/vhost.py: Un-gobble the path segment that a vhost eats
+ when the resource we're wrapping isLeaf. Potentially closes issue125.
+
+2003-09-12 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/microdom.py: lenient mode correctly handles <script>
+ tags with CDATA or comments protecting the code (closes issue #231).
+
+2003-09-10 Tommi Virtanen <tv@twistedmatrix.com>
+
+ * HTTPS support for XML-RPC and web clients (closes issue #236).
+
+2003-08-29 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7rc1.
+
+2003-09-12 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/spread/pb.py: new cred support for Perspective Broker.
+
+2003-08-26 Dave Peticolas <dave@krondo.com>
+
+ * doc/howto/xmlrpc.html: document sub-handler and introspection
+
+ * twisted/test/test_xmlrpc.py: test introspection support
+
+ * twisted/web/xmlrpc.py: implement sub-handlers and introspection
+ support
+
+2003-08-23 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/gtk2reactor.py: force timeout values to be
+ integers, because recent pygtk's complain when they get floats
+
+2003-08-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7alpha5.
+
+2003-08-18 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: Remove support code for old versions
+ of IMailbox.fetch(); also change the interface once again (no
+ backwards compat this time) to require sequence numbers to be
+ returned, not just whatever the MessageSet spit out.
+
+2003-08-16 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_import.py: update for enterprise
+
+ * twisted/enterprise/sqlreflector.py: use dbpool directly
+
+ * twisted/enterprise/row.py: deprecate KeyFactory and StatementBatch
+
+ * twisted/enterprise/dbpassport.py: remove
+
+ * twisted/enterprise/dbgadgets.py: deprecate all
+
+ * twisted/enterprise/dbcred.py: deprecate all
+
+ * twisted/enterprise/adbapi.py: deprecate Augmentation. deprecate
+ crufty bits of ConnectionPool API.
+
+2003-08-11 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/sqlreflector.py: fix docs
+
+2003-08-08 Donovan Preston <dp@twistedmatrix.com>
+
+ * Added getAllPatterns API to Widget, which returns all nodes
+ which have the given pattern name.
+
+ * Refactored List widget to use getAllPatterns, so you can have
+ more than one listHeader, listFooter, and emptyList node.
+
+2003-08-08 Dave Peticolas <dave@krondo.com>
+
+ * twisted/internet/base.py: remove unused internal function.
+
+ * twisted/internet/gladereactor.py: remove unused internal function.
+ clean up imports.
+
+2003-08-07 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7alpha4.
+
+2003-08-06 Donovan Preston <dp@twistedmatrix.com>
+
+ * Major woven optimizations.
+
+ * Removal of inspect-based hacks allowing backwards compatibility
+ with the old IModel interface. All your IModel methods should take
+ the request as the first argument now.
+
+ * Default to non-case-preserving when importing Woven templates,
+ and case-insensitive microdom. If you are using getPattern or
+ getAttribute in any of your woven code, you will have to make sure
+ to pass all lowercase strings.
+
+ * Removal of __eq__ magic methods in microdom. This was just
+ slowing woven down far too much, since without it python can
+ use identity when looking for a node in replaceChild. This means
+ you will have to explicitly use the isEqualToDocument or
+ isEqualToNode call if you are testing for the equality of microdom
+ nodes.
+
+ * Removal of usage of hasAttribute, getAttribute, removeAttribute
+ from woven for a speed gain at the expense of tying woven slightly
+ closer to microdom. Nobody will notice.
+
+ * Improved getPattern semantics thanks to a patch by Rich
+ Cavenaugh. getPattern will now not look for a pattern below any
+ nodes which have model= or view= directives on them.
+
+2003-08-04 Dave Peticolas <dave@krondo.com>
+
+ * twisted/python/usage.py: use parameter docs if handler
+ method has none. fixes bug displaying trial help.
+
+2003-07-31 Brian Warner <warner@lothar.com>
+
+ * twisted/python/filepath.py (FilePath.__getstate__): allow
+ FilePath objects to survive unpersisting.
+
+2003-07-30 Brian Warner <warner@lothar.com>
+
+ * doc/howto/faq.html: mention spawnProcess vs. os.environ
+
+ * doc/howto/test-standard.html: document usage of .todo and .skip
+
+2003-07-28 Brian Warner <warner@lothar.com>
+
+ * twisted/python/_c_dir.c: hush compiler warning
+
+ * setup.py: add twisted.xish
+
+2003-07-28 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/spread/pb.py (PBClientFactory): a new, superior API for
+ starting PB connections. Create a factory, do a
+ reactor.connectTCP/SSL() etc., then factory.getPerspective().
+
+2003-07-27 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: enable tests that depend on
+ cp_min and cp_max
+
+ * twisted/enterprise/adbapi.py: use threadpool to handle cp_min and
+ cp_max arguments
+
+ * twisted/test/test_threadpool.py: test existing work
+
+ * twisted/python/threadpool.py: check for existing work in start()
+
+2003-07-25 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: The fetch method of the IMailbox
+ interface has been changed to accept only a MessageSet and a uid
+ argument and to return an IMessage implementor.
+
+2003-07-24 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/cReactor/cDelayedCall.c: implement .active and
+ .getTime methods
+
+ * twisted/test/test_internet.py (InterfaceTestCase.wake): remove
+ reactor.initThreads() call. This is a private method which is
+ triggered internally by the current reactor when threadable.init
+ is called. It does not need to be called independently, and not
+ all reactors implement this particular method.
+
+ * twisted/test/test_threads.py: shuffle test cases, add timeouts
+ to avoid hanging tests. Added (disabled) test to trigger cReactor
+ hang (but unfortunately it fails under the default reactor)
+
+2003-07-23 Dave Peticolas <dave@krondo.com>
+
+ * twisted/internet/threads.py: avoid top-level reactor import
+
+2003-07-23 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/imap4.py: The fetch method of the IMailbox
+ interface has been changed to accept a list of (non-string)
+ objects representing the requested message parts. Less knowledge
+ of the IMAP4 protocol should be required to properly implement
+ the interface.
+
+2003-07-23 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: more tests
+
+2003-07-21 Dave Peticolas <dave@krondo.com>
+
+ * twisted/internet/base.py: implement callWhenRunning
+
+ * twisted/internet/interfaces.py: add callWhenRunning API
+
+ * twisted/test/test_pop3.py: string in string only works in 2.3
+
+2003-07-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7alpha3 (for form and twisted.names
+ updates mentioned below).
+
+2003-07-19 Ying Li <cyli@ai.mit.edu>
+
+ * twisted/web/woven/form.py: Changed form widgets so that if the
+ template already has the widget coded, merges the template widget
+ with the model widget (sets default values, etc.).
+
+ * twisted/web/woven/form.py, twisted/python/formmethod.py: Can
+ format layout of checkgroups and radiogroups into tables, rows, or
+ columns.
+
+ * twisted/web/woven/form.py, twisted/python/formmethod.py: Added
+ file input widget (unable to retrieve filename or file type - have
+ to ask for that separately).
+
+2003-07-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/protocols/dns.py, twisted/names: Twisted Names can now
+ return the `authoritative' bit. All of the resolvers in
+ twisted/names/authority.py now set it.
+
+2003-07-17 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7alpha2 (Debian packages should be
+ correct now)
+
+2003-07-17 Dave Peticolas <dave@krondo.com>
+
+ * doc/howto/components.html: methods in interfaces do have self
+ parameters
+
+2003-07-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/web/client.py: Added a `timeout' keyword argument to
+ getPage; If the web page takes longer than `timeout' to fetch,
+ defer.TimeoutError is errbacked.
+
+ * twisted/web/server.py, twisted/protocols/http.py: add `timeout'
+ argument to HTTPFactory and Site to specify how long to allow
+ connections to sit without communication before disconnecting
+ them.
+
+2003-07-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.7alpha1.
+
+2003-07-17 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/smtp.py: Address class changed to provide a
+ default domain for addresses missing a domain part.
+
+2003-07-16 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/protocols/sux.py: In beExtremelyLenient mode, all data
+ in script elements is considered plain text and will not be parsed
+ for tags or entity references.
+
+2003-07-15 Dave Peticolas <dave@krondo.com>
+
+ * twisted/persisted/styles.py: better debugging output
+ for Ephemeral
+
+2003-07-14 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/cred/checkers.py, twisted/cred/credentials.py:
+ CramMD5Credentials and OnDiskUsernamePasswordDatabase added;
+ IUsernameHashedPassword also created for use by protocols that
+ do not receive plaintext passwords over the network.
+
+ * twisted/mail/, twisted/protocols/smtp.py: Addition of alias
+ support and authenticated ESMTP connections. Several interfaces
+ changed, but deprecation warnings and backwards compatibility code
+ has been put in place to ease the change.
+
+2003-07-12 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/web/util.py: Add a new ChildRedirector that, when placed
+ at /foo to redirect to /bar, will also redirect /foo/abc to
+ /bar/abc.
+
+ * twisted/web/scripts.py: Fixed ResourceScriptWrapper so that you
+ can now .putChild on the resource you create in an .rpy file that
+ is wrapped with this class.
+
+2003-07-06 Paul Swartz <z3p@twistedmatrix.com>
+ * twisted/conch/[checkers,credentials,pamauth].py,
+ twisted/conch/ssh/userauth.py, twisted/tap/conch.py: made PAM
+ work again as an authentication.
+
+2003-07-05 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: more tests. Add mysql test.
+
+2003-07-05 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/soap.py: Now requires SOAPpy v0.10.1, allow subclasses
+ to determine method publishing strategy.
+
+2004-07-05 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * bin/mailmail, doc/man/mailmail.1, twisted/scripts/mailmail.py:
+ sendmail replacement
+
+2003-07-04 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: add sqlite. more tests.
+ Add Postgres test.
+
+ * twisted/enterprise/util.py: fix bug in getKeyColumn
+
+ * twisted/enterprise/sqlreflector.py: clean up imports
+
+ * twisted/enterprise/row.py: clean up imports
+
+ * twisted/enterprise/reflector.py: clean up imports
+
+2004-07-04 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/python/dir.c: Wrapper around opendir(3), readdir(3),
+ and scandir(3) for use by twisted.python.plugins.
+
+2003-07-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/news/database.py: NewsShelf.articleRequest() and
+ NewsShelf.bodyRequest() now expected to return a file-like object
+ in the last position of its returned three-tuple. The old API
+ is still supported, but deprecated.
+
+2003-07-03 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_enterprise.py: add gadfly test
+
+ * twisted/web/woven/input.py: remove excess newline.
+
+ * twisted/trial/unittest.py: take out unused methodPrefix var
+
+ * twisted/enterprise/adbapi.py: accept 'noisy' kw arg. persist
+ noisy, min, and max args. just warn about non-dbapi db libs.
+
+ * twisted/enterprise/reflector.py: fix spelling
+
+ * twisted/enterprise/sqlreflector.py 80 columns, don't addToCache
+ in insertRow
+
+ * twisted/enterprise/xmlreflector.py: 80 columns
+
+2003-07-01 Brian Warner <warner@lothar.com>
+
+ * sandbox/warner/fusd_twisted.py: experimental glue code for FUSD,
+ a system for implementing Linux device drivers in userspace
+
+2003-06-27 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.6rc3. Fixed a security bug in
+ twisted.web.
+
+ * .: Releasing Twisted 1.0.6rc4. One more twisted.web bug.
+
+ * .: Releasing Twisted 1.0.6.
+
+2003-06-26 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.6rc1.
+
+ * .: Releasing Twisted 1.0.6rc2. Pop3 had failing tests.
+
+2003-06-26 Clark C. Evans <cce@twistedmatrix.com>
+
+ * twisted/flow/*.py: Moved Flow from the sandbox to
+ twisted.flow. The callback is dead. Long live the callback!
+
+2003-06-26 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/pop3.py: POP3.authenticateUserXYZ no longer
+ returns a Mailbox object. It now returns a 3-tuple. See
+ twisted.cred.portal.Portal.login for more details about the return
+ value.
+
+2003-06-24 Brian Warner <warner@lothar.com>
+
+ * doc/howto/upgrading.html: Explain Versioned and rebuild()
+
+2003-06-23 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/scripts/trial.py twisted/trial/reporter.py
+ doc/man/trial.1:
+
+ Added a --tbformat={plain,emacs} option to trial. Now the default
+ is to show the regular python traceback; if you want tracebacks
+ that look like compiler output for emacs, use --tbformat=emacs.
+
+2003-06-23 Cory Dodt <corydodt@yahoo.com>
+
+ * twisted/python/util.py twisted/web/microdom.py
+ twisted/test/test_{util,xml}.py: preserveCase and caseInsensitive
+ work on attribute names as well as element names.
+
+2003-06-22 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/defer.py: Changed maybeDeferred API from
+ maybeDeferred(deferred, f, *args, **kw) to maybeDeferred(f, *args,
+ **kw).
+
+2003-06-19 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/{checkers,credentials,realm}.py,
+ twisted/conch/ssh/userauth.py: Moved the Conch user authentication
+ code to use the new version of Cred.
+
+2003-06-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.6alpha3. There was a problem in
+ twisted.python.compat that was breaking the documentation
+ building. It is now fixed.
+
+2003-06-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.6alpha2.
+
+2003-06-16 Donovan Preston <dp@twistedmatrix.com>
+
+ * twisted/web/woven/{controller,view,widgets}.py: Cleaned up the
+ output of Woven so it never leaves any woven-specific attributes
+ on the output HTML. Also, id attributes are not set on every
+ node with a View unless you are using LivePage.
+
+2003-06-11 Brian Warner <warner@lothar.com>
+
+ * doc/howto/cvs-dev.html: add "Working from CVS" hints
+
+2003-06-10 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/protocol.py: connection refused errors for
+ connected datagram protocols (connectUDP) are indicated using
+ callback, ConnectedDatagramProtocol.connectionRefused, rather
+ than an exception as before.
+
+2003-06-09 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/trial/{unittest,runner}.py: Added setUpClass and
+ tearDownClass methods and invocations to twisted.trial. Implement
+ those methods in your TestCases if you want to manage resources on
+ a per-class level.
+
+2003-06-09 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/mail/relay.py: Default relaying rule change from all
+ local and all non-INET connections to all local and all UNIX
+ connections.
+
+2003-06-08 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/interfaces.py: Added ITLSTransport interface,
+ subclassing ITCPTransport and adding one method - startTLS()
+
+ * twisted/internet/tcp.py: Connector class made to implement
+ ITLSTransport if TLS is available.
+
+2003-06-05 Brian Warner <warner@lothar.com>
+
+ * twisted/conch/ssh/transport.py (ssh_KEX_DH_GEX_INIT): don't use
+ small values for DH parameter 'y'. openssh rejects these because they
+ make it trivial to reconstruct the shared secret. This caused a test
+ failure about 1024 times out of every 65536.
+
+ * twisted/test/test_dirdbm.py (DirDbmTestCase.testModificationTime):
+ dodge a kernel bug that lets mtime get skewed from time(), causing
+ an occasional test failure
+
+2003-06-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/__init__.py twisted/internet/app.py
+ * twisted/internet/unix.py twisted/internet/tcp.py
+ * twisted/manhole/ui/gtk2manhole.py twisted/protocols/dns.py
+ * twisted/protocols/smtp.py twisted/protocols/sux.py
+ * twisted/protocols/imap4.py twisted/protocols/sip.py
+ * twisted/protocols/htb.py twisted/protocols/pcp.py
+ * twisted/python/formmethod.py twisted/python/reflect.py
+ * twisted/python/util.py twisted/python/components.py
+ * twisted/spread/jelly.py twisted/spread/newjelly.py
+ * twisted/test/test_components.py twisted/test/test_rebuild.py
+ * twisted/test/test_trial.py twisted/test/test_world.py
+ * twisted/test/test_setup.py twisted/test/test_newjelly.py
+ * twisted/test/test_compat.py twisted/test/test_pcp.py
+ * twisted/test/test_log.py twisted/web/microdom.py
+ * twisted/web/woven/page.py twisted/popsicle/mailsicle.py
+ * twisted/trial/remote.py twisted/trial/unittest.py
+ * twisted/world/allocator.py twisted/world/compound.py
+ * twisted/world/database.py twisted/world/storable.py
+ * twisted/world/structfile.py twisted/world/typemap.py:
+
+ Remove direct usage of twisted.python.compat; Modify __builtin__
+ module to include forward-compatibility hacks.
+
+2003-05-30 Brian Warner <warner@lothar.com>
+
+ * twisted/conch/ssh/keys.py (signData_dsa): Force DSS signature
+ blobs to be 20 bytes long. About 1% of the time, the sig numbers
+ would come out small and fit into 19 bytes, which would result in
+ an invalid signature.
+ * twisted/test/test_conch.py: remove special hacked test case used
+ to find that invalid-signature problem.
+
+2003-05-29 Brian Warner <warner@lothar.com>
+
+ * twisted/python/formmethod.py: this module needs False from compat
+
+ * twisted/internet/process.py (ProcessWriter.writeSomeData):
+ Accomodate Mac OS-X, which sometimes raises OSError(EAGAIN)
+ instead of IOError(EAGAIN) when the pipe is full.
+
+2003-05-27 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_process.py (EchoProtocol): try to close
+ occasional test failure. Do transport.closeStdin() instead of
+ loseConnection() because the child still has data to write (to
+ stderr). Closing all three streams takes away its voice, forces it
+ to exit with an error, and is probably causing problems.
+
+ * twisted/test/test_factories.py (testStopTrying): stop test after
+ 5 seconds rather than 2000 iterations. Some reactors iterate at
+ different rates.
+
+2003-05-24 Brian Warner <warner@lothar.com>
+
+ * twisted/scripts/trial.py (Options.opt_testmodule): ignore
+ deleted files, recognize twisted/test/* files as test cases
+
+2003-05-22 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_newjelly.py (JellyTestCase.testUnicode): make
+ sure unicode strings don't mutate into plain ones
+
+2003-05-21 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/tcp.py (Connection.getTcpKeepAlive): Add
+ functions to control SO_KEEPALIVE bit on TCP sockets.
+ * twisted/internet/interfaces.py (ITCPTransport): ditto
+ * twisted/test/test_tcp.py (LoopbackTestCase.testTcpKeepAlive):
+ test it
+
+ * doc/howto/test-standard.html: document test-case-name format
+
+ * doc/howto/coding-standard.html: encourage test-case-name tags
+
+ * twisted/protocols/htb.py, twisted/protocols/irc.py,
+ twisted/protocols/pcp.py, twisted/python/text.py,
+ twisted/spread/pb.py, twisted/trial/remote.py: clean up
+ test-case-name tags
+
+ * twisted/scripts/trial.py (Options.opt_testmodule): try to handle
+ test-case-name tags the same way emacs does
+
+2003-05-21 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * bin/coil, doc/man/coil.1, doc/man/index.html: removed. Coil
+ isn't being maintained, pending a total rewrite.
+
+2003-05-20 Brian Warner <warner@lothar.com>
+
+ * twisted/python/reflect.py (namedAny): re-raise ImportErrors that
+ happen inside the module being imported, instead of assuming that
+ it means the module doesn't exist.
+
+2003-05-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/web/server.py: Added two new methods to Request objects:
+ rememberRootURL and getRootURL. Calling rememberRootURL will store
+ the already-processed part of the URL on the request, and calling
+ getRootURL will return it. This is so you can more easily link to
+ disparate parts of your web application.
+
+ * twisted/web/woven/{page,widgets}.py: Updated Woven to take
+ advantage of previously-mentioned Request changes. You can now say
+ `appRoot = True' in the Page subclass that is instantiated by your
+ .rpy (for example), and then use a RootRelativeLink widget
+ (exactly the same way you use a Link widget) to get a link
+ relative to your root .rpy.
+
+2003-05-16 Brian Warner <warner@lothar.com>
+
+ * twisted/scripts/trial.py: catch failures during import of test
+ modules named on the command line too.
+
+ * twisted/trial/unittest.py (TestSuite.addModule): catch all failures
+ during import so that syntax errors in test files don't prevent
+ other tests from being run.
+
+ * twisted/trial/reporter.py (TextReporter): handle both Failures
+ and exception tuples in import errors. Emit the messages before the
+ last summary line so that test-result parsers can still find the
+ pass/fail counts.
+
+ * doc/howto/faq.html: Add note about Ephemeral in the
+ import-from-self twistd entry.
+
+2003-05-13 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/runner.py: sort tests by name within a TestCase
+
+2003-05-13 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/internet/{default,internet}.py: Add an `active' method to
+ DelayedCall, which returns True if it hasn't been called or
+ cancelled.
+
+2003-05-13 Jonathan Lange <jml@twistedmatrix.com>
+
+ * twisted/trial/unittest.py twisted/scripts/trial.py
+ doc/man/trial.1: Add --recurse option to make trial search within
+ sub-packages for test modules.
+
+2003-5-12 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/lore/default.py twisted/lore/latex.py
+ twisted/lore/lint.py twisted/lore/math.py twisted/lore/tree.py
+ twisted/lore/lmath.py twisted/lore/slides.py:
+ Added indexing support to LaTeX and lint, and made sure the
+ config dictionary is passed to the tree processors [this is an
+ API change which might have effect on Lore extensions!]. Rename
+ math to lmath, to avoid some corner-case bugs where it gets mixed
+ with the Python standard module "math".
+
+2003-05-11 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.6alpha1. There was a problem
+ with file descriptors in 1.0.5; some debugging information
+ has been added to this release. The problem should be fixed
+ by alpha2.
+
+2003-05-08 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.5 (same code-base as rc2).
+
+2003-05-08 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/world: Added an object database to Twisted. This is
+ still highly experimental!
+
+2003-5-6 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/trial/reporter.py twisted/scripts/trial.py: Add --timing
+ option to make the reporter output wall-clock time.
+
+2003-05-05 Brian Warner <warner@lothar.com>
+
+ * setup.py (setup_args): s/licence/license/, preferred in python-2.3
+
+2003-05-05 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 1.0.5rc1.
+
+ * .: Releasing Twisted 1.0.5rc2 (only a Debian build problem fixed).
+
+2003-05-05 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/reporter.py: remove ResultTypes, it doesn't really
+ accomplish its goal
+
+ * twisted/trial/unittest.py: move log.startKeepingErrors() from
+ top-level to TestSuite.run(). This fixes the problem of errors
+ being eaten by code which imports unittest for other reasons (like
+ to use trial.remote reporting)
+
+2003-05-04 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/reporter.py (ResultTypes): export legal values for
+ Reporter.reportResults() so remote reporters know what to expect
+
+2003-05-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/tcp.py, twisted/internet/ssl.py: TLS support
+ added to TCP connections; startTLS() method added to transport
+ objects to switch from unencrypted to encrypted mode.
+
+2003-05-02 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/protocol.py: Added continueTrying attribute to
+ ReconnectingClientFactory, and increased the number of states where
+ stopTrying() will actually stop further connection attempts.
+
+2003-05-01 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_trial.py: handle new trial layout
+ * twisted/trial/runner.py (runTest): utility function to help
+ test_trial
+ * twisted/trial/util.py (extract_tb): handle new trial layout,
+ ignore the right framework functions.
+
+2003-05-01 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/python/context.py: call-stack context tree.
+
+ * twisted/python/components.py: support interface-to-interface
+ adapatation, IFoo(o) syntax for adaptation, context-based
+ registries and more.
+
+ * twisted/python/log.py: Totally rewritten logging system.
+
+2003-05-01 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/gtk2reactor.py (Gtk2Reactor._doReadOrWrite):
+ add Anthony's cached-Failure speedup to gtk2 too.
+
+2003-05-01 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/tcp.py, twisted/internet/default.py: cache
+ Failures whose contents are always identical. Speeds up lost
+ connections considerably.
+
+ * twisted/python/failure.py: If you pass only an exception object
+ to Failure(), a stack will not be constructed. Speeds up Failure
+ creation in certain common cases where traceback printing isn't
+ required.
+
+2003-04-29 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_process.py: make all child processes inherit
+ their parent's environment
+
+ * twisted/web/resource.py, twisted/python/roots.py: add
+ test-case-name tag
+
+ * twisted/web/resource.py (IResource)
+ twisted/spread/refpath.py (PathReferenceAcquisitionContext.getIndex)
+ twisted/python/roots.py (Collection.getEntity): appease pychecker
+
+2003-04-27 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * doc/examples/bananabench.py, twisted/internet/utils.py,
+ twisted/mail/bounce.py, twisted/persisted/styles.py,
+ twisted/python/log.py, twisted/python/reflect.py,
+ twisted/spread/pb.py, twisted/test/test_banana.py,
+ twisted/test/test_iutils.py, twisted/test/test_persisted.py,
+ twisted/test/test_process.py, twisted/web/domhelpers.py,
+ twisted/web/script.py, twisted/web/server.py, twisted/web/test.py:
+ Change the usage of cStringIO to fallback to StringIO if the former
+ is not available.
+
+ * twisted/im/gtkaccount.py, twisted/internet/app.py,
+ twisted/mail/relay.py, twisted/mail/relaymanager.py,
+ twisted/persisted/journal/base.py, twisted/persisted/dirdbm.py,
+ twisted/scripts/conch.py, twisted/scripts/tapconvert.py,
+ twisted/scripts/twistd.py, twisted/scripts/websetroot.py,
+ twisted/test/test_mvc.py, twisted/test/test_persisted.py,
+ twisted/web/woven/template.py, twisted/web/woven/view.py,
+ twisted/popsicle/picklesicle.py: Change the usage of cPickle to
+ fallback to pickle if the former is not available.
+
+ * doc/howto/coding-standard.html: Document the way to use extension
+ versions of modules for which there is a pure-python equivalent.
+
+2003-04-26 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/adbapi.py: commit successful _runQuery calls
+ instead of rolling back
+
+2003-04-23 Brian Warner <warner@lothar.com>
+
+ * doc/howto/telnet.html: Update example from twisted-0.15.5(!) to
+ 1.0.4
+
+ * twisted/protocols/loopback.py: use reactor.iterate(0.01) so the
+ tests hammer the CPU slightly less
+
+ * twisted/test/test_trial.py (LoopbackTests.testError): .type is a
+ string
+ * twisted/trial/remote.py (JellyReporter.reportResults): stringify
+ .type and .value from Failures before jellying them.
+
+ * twisted/internet/base.py (ReactorBase.suggestThreadPoolSize):
+ don't let suggestThreadPoolSize(0) be the only reason threads are
+ initialized.
+
+ * twisted/python/log.py (err): always log Failures to the logfile. If
+ we're doing _keepErrors, then also add them to _keptErrors.
+
+ * twisted/trial/unittest.py (TestSuite.runOneTest): only do
+ reportResults once per test. Handle reactor.threadpool being None.
+
+2003-04-22 Bob Ippolito <bob@redivi.com>
+
+ * twisted/python/compat.py: Complete iter implementation with
+ __getitem__ hack for 2.1. dict now supports the full 2.3 featureset.
+
+ * twisted/test/test_compat.py: Tests for compat module, so we know if
+ it works or not now ;)
+
+2003-04-22 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/lore/latex.py: Handle cross-references and labels slightly
+ better, so that e.g. man/lore.html and howto/lore.html don't generate
+ conflicting labels. Also, emit \loreref{...} instead of \pageref{...}
+ -- this isn't a standard LaTeX command, see admin/book.tex for an
+ example definition. In HTML generation, all relative hrefs in <a>
+ tags are now munged from .html to .xhtml, unless class="absolute".
+
+2003-04-21 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/interfaces.py: Added getServiceNamed, addService,
+ and removeService to IServiceCollection.
+
+2003-04-21 Brian Warner <warner@lothar.com>
+
+ * twisted/web/woven/*.py: add test-case-name tags
+
+2003-04-21 Bob Ippolito <bob@redivi.com>
+
+ * twisted/web/static.py (File, DirectoryListing): DirectoryListing
+ now gets the directory listing from File.listNames, and no longer
+ calls os.listdir directly (unless a directory listing is not
+ specified in the DirectoryListing constructor).
+
+2003-04-19 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/remote.py (JellyReporter.cleanResults): handle
+ strings as testClass/method to unbreak tests
+
+ * twisted/trial/remote.py (JellyReporter.reportResults): send only
+ name of testClass/method to remote reporter, not whole class and
+ method. Also add .taster hook to DecodeReport to let users specify
+ their own security options.
+
+2003-04-17 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * .: Release 1.0.4 Final.
+
+2003-04-16 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * .: Release 1.0.4rc1.
+
+2003-04-15 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * admin/accepttests, admin/accepttests.py: Acceptance tests
+ turned into a Python module with no unguarded top-level code,
+ to make running acceptance tests selectively possible.
+
+2003-04-14 Brian Warner <warner@lothar.com>
+
+ * twisted/python/threadable.py (init):
+ * twisted/spread/newjelly.py (SecurityOptions.allowBasicTypes):
+ * twisted/spread/jelly.py (SecurityOptions.allowBasicTypes):
+ Remove old apply() calls.
+
+ * twisted/spread/flavors.py (Copyable.jellyFor): Use proper
+ jellier .prepare/.preserve dance when .invoker is non-None. This
+ fixes jellying of circular references when passed through PB
+ connections.
+
+ * twisted/test/test_newjelly.py: add test case that sets .invoker
+ to verify that code path too
+
+2003-04-14 Jonathan Lange <jml@ids.org.au>
+
+ * twisted/web/woven/controller.py (Controller): now, if getChild
+ cannot find the requested child, it will ask getDynamicChild -- a
+ method like getChild, but designed to be overriden by users.
+
+2003-04-13 Bob Ippolito <bob@redivi.com>
+
+ * twisted/internet/app.py (DependentMultiService): a MultiService
+ to start services in insert order and stop them in reverse. Uses
+ chained deferreds to ensure that if a startService or stopService
+ returns a deferred, then the next service in the queue will wait
+ until its dependency has finished.
+
+2003-04-12 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_process.py (PosixProcessTestCasePTY): skip
+ testStdio, testStderr, and testProcess. PTYs do not have separate
+ stdout/stderr, so the tests just aren't relevant. testProcess
+ might be, but it requires support for closing the write side
+ separately from the read side, and I don't think our processPTY
+ can do that quite yet.
+
+ * twisted/test/test_tcp.py (LocalRemoteAddressTestCase): iterate
+ harder. some systems might not connect to localhost before
+ iterate() is called, flunking the test
+
+ * twisted/test/test_process.py: only install SIGCHLD handler if the
+ reactor offers a hook for it.
+
+ * twisted/test/test_policies.py (ThrottlingTestCase.doIterations):
+ add more iterations to accomodate reactors that do less IO per pass
+
+ * twisted/test/process_signal.py: reset SIGHUP to default handler,
+ fixes test failures in a 'nohup' environment
+
+ * twisted/test/test_process.py (PosixProcessTestCasePTY): remove
+ testClosePty.todo now that it works
+ (SignalProtocol.processEnded): Improve testSignal error messages
+
+ * twisted/internet/process.py (PTYProcess.connectionLost): Treat
+ PTYs more like sockets: loseConnection sets .disconnecting and
+ lets the write pipe drain, then the PTY is closed in
+ connectionLost.
+
+2003-04-12 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/plugins.tml, twisted/tap/ssh.py, twisted/tap/conch.py: moved
+ the conch server from 'mktap ssh' to 'mktap conch'.
+
+2003-04-12 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/gtk2reactor.py (Gtk2Reactor.doIteration): don't
+ process *all* events before exiting: lots of IO (like test cases which
+ do connect()s from inside connectionMade) will keep us from surfacing
+ from reactor.iterate(), causing a lockup.
+ * twisted/internet/gtkreactor.py (GtkReactor.doIteration): same. Use
+ the same code as gtk2reactor with minor gtk1-vs-gtk2 variations.
+
+2003-04-11 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/gtk2reactor.py (Gtk2Reactor.doIteration): use
+ timers to match the behavior of select()-based reactors.
+ reactor.iterate(delay) is thus defined to return after 'delay'
+ seconds, or earlier if something woke it up (like IO, or timers
+ expiring).
+
+2003-04-11 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/defer.py: Added new, experimental function,
+ "maybeDeferred". API is subject to change.
+
+2003-04-11 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/scripts/mktap.py: Sped up --debug and --progress by
+ introducing a two-pass option parser.
+
+2003-04-11 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/gtk2reactor.py: major fixes. Use different
+ POLLIN/OUT flags to robustly work around pygtk bug, change
+ callback() to behave more like pollreactor (since gtk uses poll
+ internally). doIteration now calls gtk.main_iteration in a
+ non-blocking way. Attempt to emulate doIteration(delay!=0) by
+ using time.sleep().
+
+ * twisted/internet/gtkreactor.py: same fixes as for gtk2reactor.
+ Instead of a pygtk bug we've got the limited gtk_input_add API,
+ which hides POLLHUP/POLLERR, so detecting closed fds might not be
+ as reliable.
+
+2003-04-11 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/lore:
+ Added a "lore-slides" plugin, with HTML, Magicpoint and Prosper output
+ targets. It's still a bit rough, but functional.
+
+2003-04-10 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * .: Release 1.0.4alpha2.
+
+2003-04-09 Brian Warner <warner@lothar.com>
+
+ * twisted/scripts/trial.py (Options.opt_reactor): install reactor
+ before parseArgs() does an import and installs the default one
+
+ * twisted/internet/process.py: fix typo,
+ s/registerReapProccessHandler/registerReapProcessHandler)/
+
+2003-04-09 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/base.py: Change the sort order of DelayedCalls
+ and remove them from the end of the list instead of the beginning.
+ This changes O(n) complexity to O(1) complexity.
+
+2003-04-09 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_jelly.py, test_newjelly: Test cleanup.
+ Parameterize the jelly module used by the tests, make test_jelly a
+ subclass of test_newjelly using a different jelly module: tests
+ should now be unified. Also change tests to use proper trial
+ self.failUnless() methods instead of bare assert().
+
+2003-04-09 Bob Ippolito <bob@redivi.com>
+
+ * twisted/python/util.py (OrderedDict): added a UserDict subclass
+ that preserves insert order (for __repr__, items, values, keys).
+
+ * twisted/internet/app.py (Application, _AbstractServiceCollection):
+ Preserve service order, start services in order, stop them in reverse.
+
+2003-04-09 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted/protocols/ftp.py (FTPClient):
+ Added STOR support to FTPClient, as well as support for using
+ Producers or Consumers instead of Protocols for uploading/downloading.
+ * twisted/protocols/policies.py (TimeoutWrapper):
+ Added a timeout policy that can be used to automatically disconnect
+ inactive connections.
+
+2003-04-07 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_banana.py (BananaTestCase): add Acapnotic's
+ crash-cBanana test case, and some others.
+
+ * twisted/spread/banana.py (Pynana.dataReceived): add 640k limit on
+ lists/tuples, parameterize the limit into banana.SIZE_LIMIT, define
+ and use BananaError on all problems. Impose 640k limit on outbound
+ lists/tuples/strings to catch problems on transmit side too.
+
+ * twisted/spread/cBanana.c (cBanana_dataReceived): check malloc()
+ return values to avoid segfault from oversized lists. Impose 640k
+ limit on length of incoming lists. Raise BananaError on these
+ checks instead of the previously-unreachable
+ cBanana.'cBanana.error' exception.
+
+ * twisted/test/test_process.py (TwoProcessProtocol): add test to make
+ sure killing one process doesn't take out a second one
+ (PosixProcessTestCasePTY): add variant that sets usePTY=1
+
+2003-04-06 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/{unittest.py,remote.py}, twisted/test/test_trial.py:
+ Collapse most reportFoo methods into a single reportResults() that
+ takes a resultType parameter. This anticipates the addition of .todo
+ test-case flags that will add two more resultTypes.
+ * twisted/trial/unittest.py: Add .todo flags: creates EXPECTED_FAILURE
+ and UNEXPECTED_SUCCESS resultTypes. Like .skip, the .todo can be
+ added either to the TestCase object or as a method attribute.
+
+2003-04-04 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/scripts/trial.py: Now takes whatever you throw at it on
+ the command line, be it a filename, or a dotted python name for a
+ package, module, TestCase, or test method; you no longer need to
+ use the -pmcfM switches (unless you really want to).
+
+ * twisted/protocols/htb.py: Egress traffic shaping for Consumers
+ and Transports, using Heirarchial Token Buckets, patterened after
+ Martin Devera's Hierarchical Token Bucket traffic shaper for the
+ Linux kernel.
+
+ * doc/examples/shaper.py: Demonstration of shaping traffic on a
+ web server.
+
+ * twisted/protocols/pcp.py: Producer/Consumer proxy, for when you
+ wish to install yourself between a Producer and a Consumer and
+ subvert the flow of data.
+
+2003-04-04 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/web/microdom.py: parseXML and parseXMLString functions
+ that are setup to use the correct settings for strict XML parsing
+ and manipulation.
+
+2003-03-31 Brian Warner <warner@lothar.com>
+
+ * twisted/trial/unittest.py: use SkipTest's argument as a reason
+ and display it in the test results instead of the traceback. Allow
+ test methods and TestCase classes to define a .skip attribute
+ instead of raising SkipTest.
+
+2003-03-31 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/trial/remote.py: machine-readable trial output to allow
+ for the test runner and the results Reporter to be in seperate
+ processes.
+
+2003-03-15 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/app.py: Renamed "factory" argument to
+ Application.listenUDP() to "proto"
+
+2003-03-13 Tommi Virtanen <tv@twistedmatrix.com>
+
+ * twisted/tap/procmon.py, twisted/plugins.tml: support for mktapping
+ ProcessMonitors.
+
+2003-03-11 Bob Ippolito <bob@redivi.com>
+
+ * twisted/internet/: Replaced apply() in non-deprecated
+ twisted.internet modules with Direct Function Calls per
+ recommendation from PEP 290.
+
+ * twisted/web/client.py: HTTPPageGetter will now write
+ self.factory.postdata to the transport after the headers if the
+ attribute is present and is not None. The factories, getPage and
+ downloadPage now accept keyword arguments for method, postdata,
+ and headers. A Content-Length header will be automatically provided
+ for the given postdata if one isn't already present. Note that
+ postdata is passed through raw; it is the user's responsibility to
+ provide a Content-Type header and preformatted postdata. This change
+ should be backwards compatible.
+
+2003-03-05 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/: reactor.run() now accepts a keyword
+ argument, installSignalHandlers, indicating if signal handlers
+ should be installed.
+
+2003-03-04 Tommi Virtanen <tv@twistedmatrix.com>
+
+ * twisted/scripts/mktap.py, twisted/internet/app.py: mktap now
+ accepts --uid=0 and --gid=0 to really mean root, has command line
+ help for --uid=/--gid=, and understands user and group names in
+ addition to numbers.
+
+2003-03-04 Tommi Virtanen <tv@twistedmatrix.com>
+
+ * twisted/scripts/tap2deb.py, doc/man/tap2deb.1: Option --version=
+ collided with global options, renamed to --set-version=.
+
+2003-03-01 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/scripts/twistd.py: Added --report-profile flag to twistd
+ daemon.
+
+2003-02-24 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/tcp.py, base.py: set FD_CLOEXEC on all new
+ sockets (if available), so they will be closed when spawnProcess
+ does its fork-and-exec.
+
+2003-02-23 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/scripts/manhole.py: 1.4 manhole now defaults to using a
+ GTK2 client where available. Start manhole with the "--toolkit gtk1"
+ parameter if you want the old one back.
+
+2003-2-19 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/monitor.py: Monitor web sites.
+
+2003-2-20 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/internet/{app,default,interface,unix}.py: Add 'mode' argument
+ to the listenUNIX interface, which sets the filesystem mode for the
+ socket.
+
+2003-2-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Release 1.0.4alpha1.
+
+2003-2-18 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/server.py twisted/protocols/http.py: Add a way for
+ resources (and other interested parties) to know when a request has
+ finished, for normal or abnormal reasons.
+
+2003-02-17 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/scripts/conch.py: Added experimental support for connection
+ caching, where if a connection is already available to a server, the
+ client will multiplex another session over the existing connection,
+ rather that creating a new one.
+
+2003-02-16 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * doc/examples/echoserv.py: Rewrote main code to not create a .tap
+ file (examples should be simple, and demonstrate as few things as
+ possible each).
+
+ * doc/examples/echoclient.py: Added UDP echo protocol
+ implementation; it is unused by default, but easily enabled.
+
+2003-02-16 Cory Dodt <corydodt@yahoo.com>
+
+ * twisted/lore/{latex,default}.py: provide a --config book option
+ to Lore, for producing book-level documents from an index page.
+
+2003-02-15 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/scripts/mktap.py, twisted/scripts/twistd.py: Added the
+ --appname and --originalname parameters, respectively.
+
+ * twisted/doc/man/mktap.py, twisted/doc/man/twistd.py: Documented
+ the above two new parameters.
+
+2003-02-12 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/python/text.py (docstringLStrip): 1.6 This will be going
+ away in favor of inspect.getdoc.
+
+2003-02-11 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/im/interfaces.py (IAccount): 1.4 New instance attribute:
+ "client". Also, added methods getGroup and getPerson.
+
+ * twisted/im/basechat.py (ChatUI.getPerson, .getGroup): 1.7 No
+ longer accept a Class parameter. The class of the person/group is
+ determined by the account they are obtained through.
+
+ * twisted/im/basesupport.py (AbstractPerson, AbstractGroup): 1.15
+ Hold a reference to account, not client. Also, lose the "chatui"
+ parameter -- this may require follow-up.
+ (AbstractAccount.__setstate__): 1.15 remove this method. (Why
+ was self.port = int(self.port) in __setstate__?)
+ (AbstractAccount): 1.15 implement getGroup and getPerson here,
+ using _groupFactory and _personFactory factory attributes.
+
+ * twisted/im/gtkchat.py (GtkChatClientUI.getPerson, .getGroup): 1.15
+ follow ChatUI interface changes.
+
+2003-02-09 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/error.py (ProcessDone,ProcessTerminated):
+ * twisted/internet/process.py (Process.maybeCallProcessEnded,
+ * twisted/internet/process.py (PTYProcess.maybeCallProcessEnded,
+ record the signal that killed the process in .signal, set .signal
+ to None if the process died of natural causes, set .exitCode to None
+ if the process died of a signal.
+ * twisted/test/test_process.py: verify .signal, .exitCode are set
+ to None when they ought to be, verify signal-death is reported with
+ ProcessTerminated and not ProcessDone
+
+ * ChangeLog: Set add-log-time-format to iso8601.
+
+2003-02-09 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.3rc1.
+
+2003-02-08 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/tap/mail.py twisted/mail/tap.py twisted/plugins.tml:
+ Moved from tap to mail, trying to thin down twisted.tap a little.
+
+2003-02-07 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/lore/default.py twisted/lore/tree.py twisted/lore/latex.py
+ twisted/lore/man2lore.py twisted/lore/math.py
+ twisted/scripts/html2latex.py twisted/scripts/generatelore.py
+ twisted/scripts/hlint.py twisted/scripts/lore.py bin/lore
+ bin/generatelore bin/hlint bin/html2latex twisted/plugins.tml:
+ refactor lore to be cleaner, more usable and more extendible.
+ Removed old scripts, and combined them into one plugin-based script
+ which supports Lore, Math-Lore and Man pages and converts to
+ LaTeX, HTML and (man pages) to Lore.
+
+2003-02-06 Bob Ippolito <bob@redivi.com>
+
+ * twisted/protocols/smtp.py: sendEmail supports multipartboundary
+ keyword argument, which is useful for doing HTML emails if passed
+ "alternative" as opposed to the default "mixed". Uses 7bit
+ encoding for mime types that start with 'text', base64 otherwise.
+
+2003-02-04 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/app.py: listenUNIX and unlistenUNIX methods added
+ to Application class. These should be used in place of listenTCP
+ and unlistenTCP when UNIX sockets are desired. The old,
+ undocumented behavior no longer works! Also added connectUDP and
+ unlistenUDP to Application.
+
+2003-01-31 Cory Dodt <corydodt@yahoo.com>
+
+ * twisted/lore/latex.py: Don't treat comments like text nodes, just
+ drop them.
+
+2003-01-30 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/default.py
+ twisted/internet/base.py
+ twisted/internet/tcp.py
+ twisted/internet/ssl.py
+ twisted/internet/udp.py
+ twisted/internet/unix.py
+
+ Refactor of many internal classes, including Clients and
+ Connectors. UNIX socket functionality moved out of the TCP classes
+ and into a new module, unix.py, and implementation of IReactorUNIX
+ by PosixReactorBase made conditional on platform UNIX socket
+ support. Redundant inheritance cruft removed from various classes.
+
+ * twisted/internet/app.py: listenWith, unlistenWith, and connectWith
+ methods added to Application.
+
+ * twisted/internet/interfaces.py: IReactorArbitrary added.
+
+2003-01-30 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/manhole/service.py (IManholeClient.console): 1.35
+ exception messages now use a Failure.
+ (IManholeClient.listCapabilities): 1.35 Method to describe what
+ capabilities a client has, i.e. "I can receive Failures for
+ exceptions."
+
+2003-01-29 Donovan Preston <dp@twistedmatrix.com>
+
+ * twisted/web/woven/controller.py
+ twisted/web/woven/template.py
+ twisted/web/woven/view.py
+ twisted/web/woven/widgets.py Major woven codepath cleanup
+
+ * Uses a flat list of outstanding DOM nodes instead of
+ recursion to keep track of where Woven is in the page
+ rendering process
+
+ * Removes View's dependency on DOMTemplate as a base
+ class, in preparation for deprecation of DOMTemplate
+ (all of the same semantics are now directly implemented
+ in View). As a result, View has no base classes, making
+ the inheritance chain cleaner.
+
+ * Stores the namespace stacks (model, view, and controller
+ name lookup chain) in the View directly, and each widget
+ gets an immutable reference to it's position in the lookup
+ chain when it is created, making re-rendering Widgets more
+ reliable
+
+ * Represents the namespace stacks as a cons-like tuple
+ structure instead of mutable python lists, reducing
+ confusion and list-copying; instead of copying the current
+ stack lists each time a Widget is created, it just gets a
+ reference to the current tuples for each of the stacks
+
+2003-01-29 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.2 Final.
+
+ * .: Releasing 1.0.3alpha1. Release Often :-D
+
+2003-01-29 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/internet/abstract.py (FileDescriptor.__init__): 1.36
+ Ephemeral.
+
+ * twisted/internet/tcp.py (Port.__getstate__): 1.100 As an
+ Ephemeral, this needs no __getstate__.
+
+2003-01-27 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/spread/ui/gtk2util.py (login): Perspective Broker login
+ dialog for GTK+ version 2.
+
+2003-01-26 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.2rc1.
+
+ * .: Releasing 1.0.2rc2 (rc1 was dead in the water; hlint bug now
+ fixed).
+
+ * .: Releasing 1.0.2rc3 (rc2 was dead in the water;
+ twisted.lore.latex bug now fixed)
+
+2003-01-26 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/im/interfaces.py (IClient.__init__): 1.3 Accept a
+ logonDeferred parameter. The client should call this back when
+ it is successfully logged in.
+
+ * twisted/im/basesupport.py
+ (AbstractClientMixin.registerAsAccountClient): 1.13 Gone.
+ chatui.registerAccountClient is called in AbstractAccount.logOn
+ instead.
+
+2003-01-22 Dave Peticolas <dave@krondo.com>
+
+ * twisted/web/xmlrpc.py: add docstring for Proxy. handle
+ serialization errors. check for empty deferred on connectionLost.
+
+ * twisted/test/test_internet.py: make sure wakeUp actually works
+
+2003-01-21 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/internet/defer.py: added utility method for
+ getting result of list of Deferreds as simple list.
+
+2003-1-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/interfaces.py: type argument removed from
+ IReactorCore.resolve method. IReactorPluggableResolver interface
+ added.
+
+ * twisted/internet/base.py: IReactorPluggable added to
+ ReactorBase.__implements__ and ReactorBase.installResolver added.
+
+2003-1-18 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/trial/unittest.py twisted/scripts/trial.py: adding --summary
+
+2003-01-15 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.2alpha3.
+
+2003-01-13 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.2alpha2.
+
+2003-01-11 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/protocols/shoutcast.py: add client support for
+ Shoutcast MP3 streaming protocol.
+
+2003-01-10 Itamar Shtull-Trauring <itamar@itamarst.org>
+
+ * twisted/scripts/twistd.py: in debug mode, jump into debugger for any
+ logged exception.
+
+2003-01-10 Dave Peticolas <dave@krondo.com>
+
+ * twisted/trial/unittest.py: enable test cruft checking
+
+ * twisted/test/test_policies.py: cleanup timers
+
+ * twisted/protocols/policies.py: start/stop bandwidth timers as needed
+
+ * twisted/test/test_internet.py: cleanup timers
+
+ * twisted/test/test_woven.py: expire sessions to clean up timers
+
+ * twisted/web/woven/guard.py: stop timer when session expires
+
+2003-1-9 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/google.py: Search google for best matches
+
+2003-01-09 Dave Peticolas <dave@krondo.com>
+
+ * twisted/protocols/http.py: start/stop log timer as needed
+
+2003-01-08 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_smtp.py: cleanup timers after test
+
+ * twisted/trial/unittest.py: keep errors that are logged and
+ submit them as test failures when tests are finished.
+
+ * twisted/python/log.py: if errors are being kept, don't print
+ them
+
+2003-1-8 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * doc/man/trial.1 twisted/scripts/trial.py: Add -l/--logfile argument
+ to allow giving a log file.
+
+ * twisted/trial/unittest.py: add SkipTest exception, which tests can
+ raise in their various test* method to skip a test which is not
+ excpected to pass.
+
+2003-01-08 Jonathan M. Lange <jml@mumak.net>
+
+ * twisted/trial/*, bin/trial, twisted/scripts/trial.py,
+ doc/man/trial.1: Added 'trial', a new unit testing framework for
+ Twisted.
+
+ * twisted/test/test_*, admin/runtests: Moved existing tests over to
+ trial.
+
+2003-01-06 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/python/microdom.py: Added beExtremelyLenient mode (for
+ parsing "tag soup"). While this isn't quite as lenient as Mozilla
+ or IE's code (it will, for example, translate
+ <div><i><b>foo</i>bar</b></div> to <div><i><b>foo</b></i>bar</div>
+ ) I am still rather proud of the wide range of complete garbage
+ that it will mangle into at least reasonably similar XHTML-esque
+ documents.
+
+2003-01-05 Brian Warner <warner@lothar.com>
+
+ * twisted/internet/cReactor/*, setup.py: Implement getDelayedCalls for
+ cReactor. Create cDelayedCall class, implement .cancel(), .reset(),
+ and .delay() for them.
+
+2003-01-03 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/python/components.py: Fix bug due to interaction between
+ Componentized subclasses and twisted.python.rebuild.rebuild()
+
+ * twisted/python/reflect.py: Removed backwards compatability hack
+ for deprecated name twisted.protocols.telnet.ShellFactory and empty
+ oldModules dictionary.
+
+2003-01-02 Brian Warner <warner@lothar.com>
+
+ * twisted/test/test_internet.py (DelayedTestCase): add test
+ coverage for IReactorTime.getDelayedCalls
+
+2002-12-30 Brian Warner <warner@lothar.com>
+
+ * pyunit/unittest.py (TestCase.__call__): clean the reactor between
+ tests: cancel any leftover reactor.callLater() timers. This helps
+ to keep deferred failures isolated to the test that caused them.
+
+2002-12-30 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/*: added docstrings to most conch classes and functions
+
+2002-12-30 Brian Warner <warner@lothar.com>
+
+ * twisted/spread/pb.py (Broker.connectionLost): clear localObjects
+ too, to break a circular reference involving AuthServs that could
+ keep the Broker (and any outstanding pb.Referenceables) alive
+ forever.
+
+2002-12-29 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/python/compat.py: Single module where all compatability
+ code for supporting old Python versions should be placed.
+
+2002-12-28 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/web/woven/guard.py: Newer, better wrappers for
+ authentication and session management. In particular a nice
+ feature of this new code is automatic negotiation with browsers on
+ whether cookies are enabled or not.
+
+2002-12-27 Paul Swartz <z3p@twistedmatrix.com>
+
+ * bin/tkconch: initial commit of tkconch, a SSH client using Tkinter
+ as a terminal emulator. puts up a menu to configure when run without
+ arguments.
+
+ * twisted/conch/ui: moved ansi.py and tkvt100.py to t.c.ui so they are
+ away from the purely conch stuff.
+
+2002-12-25 Christmas Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.2alpha1 - Merry Christmas!
+
+2002-12-25 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/dict.py: dict client protocol implementation
+ from Pavel "Pahan" Pergamenshchik (<pp64@cornell.edu>)
+
+2002-12-23 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * doc/examples/testdns.py and doc/examples/dns-service.py added as
+ simple example of how to use new DNS client API.
+
+2002-12-23 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/xmlrpc.py: added XML RPC client support
+
+2002-12-22 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/keys.py, twisted/conch/ssh/asn1.py: support for
+ writing public and private keys.
+
+ * bin/ckeygen: new script to create public/private key pairs
+
+2002-12-22 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/protocols/dns.py: Support for AFSDB, RP, and SRV RRs
+ added.
+
+2002-12-18 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/persisted/dirdbm.py: copyTo and clear methods added
+ to DirDBM class
+
+2002-12-18 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/connection.py, twisted/test/test_conch: fixes to
+ work on Python 2.1.
+
+ * twisted/internet/process.py: usePTY now can be an optional tuple of
+ (masterfd, slavefd, ttyname).
+
+2002-12-18 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/rewrite.py: it works now, even when used as a rootish
+ resource. Also, the request.path is massaged.
+
+2002-12-13 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/util.py: support numeric type
+
+2002-12-13 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/client.py: add 301/302 support
+
+2002-12-13 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_ftp.py: give client time to start up (fixes
+ one test for gtk/gtk2 reactors)
+
+ * twisted/protocols/ftp.py: ftp client in passive mode should not
+ close data until both command and protocol are finished. (fixes
+ one test in gtk/gtk2 reactors)
+
+ * twisted/internet/gtkreactor.py: remove redundant code
+
+ * twisted/internet/gtk2reactor.py: remove redundant code
+
+ * twisted/internet/abstract.py: fix spelling in documentation
+
+2002-12-12 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_jelly.py: test class serialization
+
+ * twisted/spread/jelly.py: join module names with '.' in
+ _unjelly_class
+
+2002-12-12 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/pamauth.py: added, gives support for authentication
+ using PAM.
+
+ * twisted/conch/*: support for the keyboard-interactive authentication
+ method which uses PAM.
+
+2002-12-12 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/python/log.py: add setStdout, set logfile to NullFile by
+ default.
+
+2002-12-11 Donovan Preston <dp@twistedmatrix.com>
+
+ * Added new woven example, Hello World.
+
+ * Updated woven howto to talk about Hello World. TODO: Finish refactoring
+ woven quotes example, then write more advanced woven howtos on writing
+ Widgets and InputHandlers.
+
+2002-12-11 Paul Swartz <z3p@twistedmatix.com>
+
+ * twisted/conch/*: enabled 'exec' on the server, disabled core dumps,
+ and some fixes
+
+2002-12-10 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/*: many fixes to conch server, now works and can run
+ as root.
+
+ * twisted/conh/ssh/session.py: fix root exploit where a python shell was
+ left acessable to anyone.
+
+2002-12-10 Cory Dodt <corydodt@yahoo.com>
+
+ * t/scripts/postinstall.py: new. Create shortcut icons on win32.
+
+ * twisted-post-install.py: new. Runs t/scripts/postinstall.py
+
+ * setup.py: copy twisted-post-install.py during install_scripts
+
+2002-12-09 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/internet/app.py: actually set the euid/egid if users ask
+
+2002-12-09 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_conch.py: wait for ssh process to finish
+
+ * twisted/scripts/postinstall.py: fix indentation
+
+ * twisted/conch/identity.py: fix indentation
+
+2002-12-09 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/conch/ssh/transport.py: don't accept host keys by default
+ because it's a huge security hole.
+
+2002-12-09 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/util.py: handle None as null
+
+ * twisted/internet/interfaces.py: add missing 'self' argument
+
+2002-12-08 Dave Peticolas <dave@krondo.com>
+
+ * pyunit/unittest.py: add missing 'self.' prefix to data member
+ reference
+
+ * twisted/enterprise/util.py: make sure quoted values are strings
+ (fixes bug storing boolean types)
+
+2002-12-06 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_internet.py: flush error to prevent failure
+ with non-destructive DeferredLists.
+
+ * twisted/test/test_ftp.py: flush FTPErrors to prevent failures
+ with non-destructive DeferredLists.
+
+ * twisted/test/test_defer.py: catch the errors to prevent failure
+ with non-destructive DeferredLists
+
+ * twisted/enterprise/util.py: add some postgres types. boolean
+ types need to be quoted. remove unused selectSQL variable.
+
+2002-12-05 Dave Peticolas <dave@krondo.com>
+
+ * twisted/enterprise/sqlreflector.py: fix some sql escaping
+ bugs. allow subclasses to override escaping semantics.
+
+ * twisted/enterprise/util.py: allow quote function's string escape
+ routine to be overridden with a keyword argument.
+
+2002-12-5 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/python/plugin.py: fixed a bug that got the wrong plugins.tml
+ if the package was installed in two different places
+
+ * twisted/inetd/*, twisted/runner/*: moved inetd to runner, to live in
+ harmony with procmon
+
+2002-12-04 Dave Peticolas <dave@krondo.com>
+
+ * twisted/test/test_policies.py: Take the start time timestamp
+ immediately before creating the ThrottlingFactory, since the
+ factory starts timing when it is created.
+
+ * admin/runtests: Add a 'gtk2' test type to use the gtk2reactor
+ for the test suite.
+
+2002-12-2 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/client.py: web client
+
+2002-11-30 Paul Swartz <z3p@twistedmatrix.com>
+
+ * Summary of Conch changes: An actual client (bin/conch) which is
+ mostly compatible with the OpenSSH client. An optional C module to
+ speed up some of the math operations. A bunch of other stuff has
+ changed too, but it's hard to summarize a month of work.
+
+2002-11-24 Donovan Preston <dp@twistedmatrix.com>
+
+ * twisted/web/woven/*: Added the beginnings of a general framework for
+ asynchronously updating portions of woven pages that have already been
+ sent to the browser. Added controller.LiveController, page.LivePage,
+ and utils.ILivePage to contain code for dealing with keeping Views alive
+ for as long as the user is still looking at a page and has a live
+ Session object on the server; code for responding to model changed
+ notifications, rerendering Views that depend on those models that have
+ changed; code for sending these rerendered views as html fragments to
+ the browser; and javascript code to mutate the DOM of the live page
+ with the updated HTML. Mozilla only for the moment; ie to come soon.
+
+ * twisted/web/woven/widgets.py: Added API for attaching Python functions
+ to widgets that fire when a given javascript event occurs in the
+ browser.
+ Widget.addEventHandler(self, eventName, handler, *args) and
+ Widget.onEvent(self, request, eventName, *args). The default onEvent
+ will dispatch to event handlers registered with addEventHandler.
+
+2002-11-24 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.1.
+
+2002-11-23 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/names/client.py, twisted/names/server.py: Client and
+ server domain name APIs
+
+ * twisted/tap/dns.py: 'mktap dns'
+
+2002-11-23 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/scripts/twistd.py twisted/python/syslog.py: Add syslog support
+
+2002-11-23 Kevin Turner <acapnotic@twistedmatrix.com>, Sam Jordan <sam@twistedmatrix.com>
+
+ * twisted/protocols/irc.py (IRCClient.dccResume, dccAcceptResume):
+ Methods for mIRC-style resumed file transfers.
+ (IRCClient.dccDoSend, IRCClient.dccDoResume)
+ (IRCClient.dccDoAcceptResume, IRCClient.dccDoChat): These are for
+ clients to override to make DCC things happen.
+ (IRCClient.dcc_SEND, dcc_ACCEPT, dcc_RESUME, dcc_CHAT)
+ (IRCClient.ctcpQuery_DCC): Refactored to dispatch to dcc_* methods.
+ (DccFileReceiveBasic.__init__): takes a resumeOffset
+
+2002-11-20 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing 1.0.1rc1
+
+2002-11-16 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * Multicast UDP socket support in most reactors.
+
+2002-11-11 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * .: Releasing 1.0.1alpha4
+
+ * .: Releasing 1.0.1alpha3
+
+2002-11-10 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * .: Releasing 1.0.1alpha2
+
+ * twisted/web/static.py, twisted/tap/web.py: Changed 'mktap web'
+ to use --ignore-ext .ext so that you can assign order to the
+ extensions you want to ignore, and not accidentally catch bad
+ extensions.
+
+2002-11-04 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/internet/tksupport.py: new, better Tkinter integration.
+ Unlike before, run the reactor as usual, do *not* call Tkinter's
+ mainloop() yourself.
+
+2002-10-25 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/domhelpers.py twisted/python/domhelpers.py
+ twisted/lore/tree.py twisted/web/woven/widgets.py: Moved domhelpers
+ to twisted.web, and add to it all the generic dom-query functions
+ from twisted.lore.tree
+
+ * twisted/scripts/generatelore.py twisted/scripts/html2latex.py
+ bin/html2latex bin/generatelore twisted/lore/__init__.py
+ twisted/lore/latex.py twisted/lore/tree.py: Add the document generation
+ Twisted uses internally to the public interface.
+
+ * twisted/python/htmlizer.py: a Python->HTML colouriser
+
+2002-10-23 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/web/soap.py: experimental SOAP support, using SOAPpy.
+ See doc/examples/soap.py for sample usage.
+
+2002-10-22 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/python/log.py: Two new features.
+ 1) a stupid `debug' method that simply prefixes a message with "debug"
+ and indents it so it's easier to distinguish from normal messages.
+ This can eventually log to some magic "debug channel", once we have
+ that implemented.
+
+ 2) implemented a custom warning handler; now warnings look sexy.
+ (the hackish overriding of warnings.showwarning is the recommended way
+ to do so, according to the library reference.)
+
+2002-10-22 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * setup.py: conditionalize cReactor on threads support too. This
+ is somewhat of a hack as it it done currently, but it's only necessary
+ on weird OSes like NetBSD. I assume any UNIX with thread support has
+ pthreads.
+
+ * twisted/internet/tksupport.py: tunable reactor iterate delay
+ parameter [by Jp Calderone]
+
+2002-10-17 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * bin/websetroot twisted/scripts/websetroot.py: Added a program to set
+ the root of a web server after the tap exists
+
+2002-10-14 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/vhost.py: add a virtual host monster to support twisted
+ sites behind a reverse proxy
+
+ * twisted/tap/web.py twisted/web/script.py
+ doc/man/mktap.1: adding an option to have a resource script as the root
+
+2002-10-13 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/internet/utils.py twisted/internet/process.py
+ twisted/internet/interfaces.py twisted/internet/default.py: Moved
+ utility functions into twisted.internet.utils
+
+2002-10-12 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/internet/process.py twisted/internet/interfaces.py
+ twisted/internet/default.py: Add utility method to get output of
+ programs.
+
+2002-10-11 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/internet/wxsupport.py: improved responsiveness of wxPython
+ GUI (50 FPS instead of 10 FPS).
+
+2002-10-08 Brian Warner <warner@twistedmatrix.com>
+
+ * doc/howto: Added PB/cred and Application docs, updated Manhole
+ and Process docs. Moved Manhole from "Administrators" section to
+ "Developers" section.
+
+2002-10-10 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * .: Releasing 0.99.4
+
+2002-10-07 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * .: Release 0.99.4rc1
+
+ * twisted/protocols/http.py: backed out changes to HTTP that
+ broke 0.99.3 twisted.web.distrib.
+
+2002-10-7 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/script.py: Add ResourceTemplate which uses PTL for
+ creation of resources.
+
+2002-10-7 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/tap/web.py: It is now possibly to add processors via
+ the command line
+
+
+2002-10-04 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twistd: when running in debug mode (-b), sending a SIGINT signal
+ to the process will drop into the debugger prompt.
+
+2002-10-5 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * .: Releasing 0.99.3
+
+2002-10-01 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/protocols/http.py: Fixed many bugs in protocol parsing,
+ found by new unit tests.
+
+2002-9-30 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/protocols/sux.py twisted/web/microdom.py: Made is possible
+ to sanely handle parse errors
+
+2002-09-26 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/internet/app.py (_AbstractServiceCollection.removeService):
+ (MultiService.removeService): inverse of addService
+ (ApplicationService.disownServiceParent): inverse of setServiceParent
+
+2002-9-27 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * .: Releasing 0.99.2
+
+2002-09-26 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/web/microdom.py: Better string formatting of XML
+ elements is now available, to aid with debugging of web.woven
+ (among other applications).
+
+2002-09-25 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/tap/manhole.py: mktap manhole will now prompt for a
+ password or accept one from stdin if one is not provided on the
+ command line.
+
+2002-09-25 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * bin/tapconvert: made sure tapconvert program gets installed.
+
+2002-09-24 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/web/resource.py (Resource.wasModifiedSince): revoked,
+ not adding this after all. Instead,
+
+ * twisted/protocols/http.py (Request.setLastModified)
+ (Request.setETag): these methods to set cache validation headers
+ for the request will return http.CACHED if the request is
+ conditional and this setting causes the condition to fail.
+
+2002-9-24 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * .: Releasing 0.99.2rc2
+
+2002-9-23 Donovan Preston <dp@twistedmatrix.com>
+
+ * Renaming domtemplate/domwidgets/dominput/wmvc to Woven
+ Woven - The Web Object Visualization Environment
+
+ * Created package twisted/web/woven
+
+ * Renamed domtemplate to template, domwidgets to widgets,
+ and dominput to input
+
+ * Refactored wmvc into three modules, model, view, and controller
+
+2002-9-23 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/spread/pb.py: add getObjectAtSSL, refactored into
+ getObjectRetreiver so more transports can be easily supported
+
+2002-09-21 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/protocols/http.py (Request.setLastModified): Use
+ setLastModified to set a timestamp on a http.Request object, and
+ it will add a Last-Modified header to the outgoing reply.
+
+ * twisted/web/resource.py (Resource.wasModifiedSince): companion
+ method, override this to get sensible handling of
+ If-Modified-Since conditional requests.
+
+2002-09-21 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/web/static.py, twisted/web/script.py: Previously, it was
+ not possible to use the same xmlmvc application (directory full
+ of files and all) to interface to separate instances in the same
+ server, without a considerable amount of hassle. We have
+ introduced a new "Registry" object which is passed to all .rpy
+ and .epy scripts as "registry" in the namespace. This is a
+ componentized, so it can be used to associate different
+ components for the same interface for different File instances
+ which represent the same underlying directory.
+
+2002-09-20 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/web/microdom.py: You can now specify tags that the
+ parser will automatically close if they are not closed
+ immediately. This is to support output from HTML editors which
+ will not output XML, but still have a predictable
+ almost-but-not-quite XML structure. Specifically it has been
+ tested with Mozilla Composer.
+
+2002-9-20 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * Documenting for others
+
+ * setup.py: now setup.py can function as a module
+
+ * twisted/enterprise/xmlreflector.py: deprintified
+
+ * twisted/internet/abstract.py, twisted/internet/fdesc.py,
+ twisted/internet/app.py, twisted/internet/gtkreactor.py,
+ twisted/internet/main.py, twisted/internet/protocol.py,
+ twisted/internet/ssl.py, twisted/internet/tksupport.py,
+ twisted/internet/pollreactor.py, twisted/internet/defer.py:
+ added and modified __all__
+
+ * twisted/internet/base.py: changed ReactorBase's __name__, added
+ __all__
+
+ * twisted/internet/default.py, twisted/internet/error.py,
+ twisted/internet/process.py,
+ twisted/internet/win32eventreactor.py: reaping all processes on
+ SIGCHLD, changes in process's API
+
+ * twisted/python/components.py: added Adapter and setComponent
+
+ * twisted/python/log.py: logging several strings works
+
+ * twisted/python/reflect.py: fixed namedModule() to handle packages
+
+ * twisted/web/dom*.py: added submodels, moved to microdom, removed
+ unsafe code
+
+ * twisted/python/mvc.py: changed submodel support, added ListModel,
+ Wrapper
+
+ * twisted/web/microdom.py: minidom compat fixes
+
+2002-9-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted/internet/error.py twisted/internet/process.py:
+ ProcessEnded -> ProcessTerminated/ProcessDone. Now it is possible
+ to read off the error code.
+
+2002-9-19 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/scripts/twistd.py: Added ability to chroot. Moved directory
+ change to after loading of application.
+
+2002-9-19 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/*: changed print to log.msg
+
+ * bin/* twisted/scripts/*.py: move code from bin/ to modules
+
+ * twisted/inetd/*.py: inetd server in twisted
+
+ * twisted/protocols/sux.py twisted/web/microdom.py: XML parsing
+
+ * twisted/conch/*.py: better logging and protocol support
+
+ * twisted/cred/*.py: deprecation fixes
+
+ * twisted/internet/app.py: add encryption
+
+ * twisted/internet/base.py: fix deprecation, add DelayedCall,
+ move to connect* from client*
+
+ * twisted/internet/error.py: errno mapping works on more platforms,
+ AlreadyCalled, AlreadyCancelled errors
+
+ * twisted/internet/gtkreactor.py: try requiring gtk1.2, timeout->idle
+
+ * twisted/internet/interfaces.py: added IDelayedCall IProcessTransports
+
+ * twisted/internet/javareactor.py: using failure, better dealing with
+ connection losing, new connect* API
+
+ * twisted/internet/process.py: dealing better with ending
+
+ * twisted/internet/protocol.py: factories have a "noisy" attribute,
+ added ReconnectingClientFactory BaseProtocol
+
+ * twisted/internet/ptypro.py: fixed traceback
+
+ * twisted/internet/reactor.py: better guessing of default
+
+ * twisted/internet/tcp.py: failure
+
+ * twisted/internet/win32eventreactor.py: update to new API, support GUI
+
+ * twisted/manhole/service.py: fix deprecation
+
+ * twisted/news/database.py: fix to be 2.1 compat., generating
+ message-id, bytes, lines, date headers, improved storage
+
+ * twisted/news/news.py: UsenetClientFactory, UsenetServerFactory
+
+ * twisted/persisted/marmalade.py: use twisted.web.microdom
+
+ * twisted/protocols/ftp.py: dito, data port uses new client API
+
+ * twisted/protocols/http.py: StringTransport instead of StringIO
+
+ * twisted/protocols/irc.py: stricter parsing, avoid flooding
+
+ * twisted/protocols/loopback.py: new reactor API, loopback over UNIX
+ sockets
+
+ * twisted/protocols/nntp.py: more lenient parsing, more protocol support
+
+ * twisted/protocols/oscar.py: new reactor API
+
+ * twisted/python/components.py: fix setAdapter add removeComponent
+
+ * twisted/python/failure.py: cleanFailure
+
+ * twisted/python/log.py: can now log multiple strings in one go
+
+ * twisted/python/logfile.py: fixed rotation
+
+ * twisted/python/rebuild.py: better 2.2 support
+
+ * twisted/python/util.py: getPassword
+
+ * twisted/scripts/mktap.py: better --help, --type, encryption
+
+ * twisted/spread/*.py: removed deprecation warnings
+
+ * twisted/spread/util.py: improved Pager
+
+ * twisted/tap/news.py: works saner now
+
+ * twisted/tap/ssh.py: can specify authorizer
+
+ * twisted/tap/words.py: can bind services to specific interfaces
+
+ * twisted/web/distrib.py: now works on java too
+
+ * twisted/web/domtemplate.py: improved cache
+
+ * twisted/web/error.py: ForbiddenResource
+
+ * twisted/web/html.py: lower-case tags
+
+ * twisted/web/server.py: use components
+
+ * twisted/web/static.py: added .flac, .ogg, properly 404/403,
+ lower-case tags
+
+ * twisted/web/twcgi.py: fixed for new process API
+
+ * twisted/web/widgets.py: lower-case tags
+
+ * twisted/web/xmlrpc.py: new abstraction for long running xml-rpc
+ commands, add __all__
+
+ * twisted/words/ircservice.py: new connectionLost API
+
+ * twisted/words/service.py: refactoring and error handling
+
+ * twisted/words/tendril.py: lots of fixes, it works now
+
+2002-09-17 Donovan Preston <dp@twistedmatrix.com>
+
+ * Added better error reporting to WebMVC. To do this, I had to
+ remove the use of "class" and "id" attributes on nodes as
+ synonyms for "model", "view", and "controller". Overloading
+ these attributes for three purposes, not to mention their
+ usage by JavaScript and CSS, was just far too error-prone.
+
+2002-09-09 Andrew Bennetts <spiv@twistedmatrix.com>
+
+ * twisted.inetd: An inetd(8) replacement. TCP support should be
+ complete, but UDP and Sun-RPC support is still buggy. This was
+ mainly written as a proof-of-concept for how to do a forking
+ super-server with Twisted, but is already usable.
+
+2002-08-30 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.1rc4. There was a bug in the acquisition
+ code, as well as a typo in TwistedQuotes.
+
+2002-08-29 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.1rc3. A bug in the release script
+ left .pyc files in the tarball.
+
+2002-08-29 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.1rc2. There was a bug with circular
+ imports between modules in twisted.python.
+
+2002-08-28 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.1rc1.
+
+2002-08-27 Donovan Preston <dp@twistedmatrix.com>
+
+ * twisted.web.domtemplate: Look up templates in the directory of
+ the module containing the DOMTemplate doing the lookup before
+ going along with regular acquisition.
+
+2002-08-27 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.*: Lots of minor fixes to make JavaReactor work again.
+
+2002-08-26 Andrew Bennetts <andrew-twisted@puzzling.org>
+
+ * twisted.python.logfile: Added the ability to disable log
+ rotation if logRotation is None.
+
+2002-08-22 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted.news: Added a decent RDBM storage backend.
+
+2002-08-21 Paul Swartz <z3p@twistedmatrix.com>
+
+ * doc/howto/process.html: Process documentation, too!
+
+2002-08-20 Paul Swartz <z3p@twistedmatrix.com>
+
+ * doc/howto/clients.html: Client-writing documentation.
+
+2002-08-20 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted.protocols.nntp: More protocol implemented: SLAVE, XPATH,
+ XINDEX, XROVER, TAKETHIS, and CHECK.
+
+2002-08-19 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * bin, twisted.scripts.*: Migrated all bin/* scripts'
+ implementations to twisted/scripts. This means win32 users will
+ finally have access to all of the twisted scripts through .bat
+ files!
+
+2002-08-19 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted.news, twisted.protocols.nntp: Additional RFC977 support:
+ HELP and IHAVE implemented.
+
+2002-08-19 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.internet.{process,win32eventreactor,etc}: New and
+ hopefully final Process API, and improved Win32 GUI support.
+
+2002-08-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Everything: Got rid of almost all usage of the `print' statement
+ as well as any usage of stdout. This will make it easier to
+ redirect boring log output and still write to stdout in your
+ scripts.
+
+2002-08-18 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.0 final. No changes since rc9.
+
+2002-08-17 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.0rc8, with a fix to tap2deb and
+ slightly updated options documentation.
+
+ * Releasing Twisted 0.99.0rc9 with fixes to release-twisted
+ and doc/howto/options.html.
+
+2002-08-16 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.0rc6, with some fixes to setup.py
+ * Releasing Twisted 0.99.0rc7, __init__.py fixes.
+
+2002-08-15 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.0rc5, with some one severe bug-fix and
+ a few smaller ones.
+
+2002-08-14 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.99.0rc1! ON THE WAY TO 1.0, BABY!
+ * Releasing Twisted 0.99.0rc2! Sorry, typoed the version number in
+ copyright.py
+ * Releasing Twisted 0.99.0rc3! I HATE TAGGING!
+ * Releasing Twisted 0.99.0rc4, some very minor errors fixed.
+
+2002-08-14 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.internet, twisted.cred: Applications and Authorizers are
+ now completely decoupled, save for a tiny backwards-compatibility.
+
+2002-08-10 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted.internet.defer, twisted.python.failure: Changes to
+ Deferred and Failure to make errbacks more consistent. error
+ callbacks are now *guaranteed* to be passed a Failure instance,
+ no matter what was passed to Deferred.errback().
+
+2002-08-07 Jp Calderone <exarkun@twistedmatrix.com>
+
+ * twisted.python.usage: New "subcommands" feature for
+ usage.Options: Now, you can have nested commands
+ (`cvs commit'-style) for your usage.Options programs.
+
+2002-08-04 Bruce Mitchener <bruce@twistedmatrix.com>
+
+ * twisted.internet: New `writeSequence' method on transport
+ objects: This can increase efficiency as compared to `write`ing
+ concatenated strings, by copying less data in memory.
+
+2002-08-02 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.cred.service, twisted.internet.app: Application/Service
+ refactor: These two things should be less dependant on each other,
+ now.
+
+2002-07-31 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.issues: After weeks of hacking in the secret (Austin,
+ TX) hideout with Allen Short, twisted.issues, the successor to
+ Twisted Bugs, is born. Featuring a paranoia-inducing chat-bot
+ interface!
+
+2002-07-30 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.internet.kqueue: Thanks to Matt Campbell, we now have a
+ new FreeBSD KQueue Reactor.
+
+2002-07-27 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * doc/fun/Twisted.Quotes: Added our seekrut Twisted.Quotes file to
+ Twisted proper.
+
+2002-07-26 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.spread: "Paging" for PB: this is an abstraction for
+ sending big streams of data across a PB connection.
+
+
+2002-07-23 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.internet: Rewrite of client APIs. `reactor.clientXXX'
+ methods are now deprecated. See new reactor.connect*
+ documentation. Also Application-level client methods have been
+ reworked, see the Application documentation.
+
+2002-07-23 Bryce Wilcox-O'Hearn <zooko@twistedmatrix.com>
+
+ * twisted.zoot: Application-level implementation of Gnutella.
+
+2002-07-21 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.im, bin/im: GUI improvements to t-im, and renamed
+ bin/t-im to bin/im (and get rid of old twisted.words client).
+
+2002-07-15 Bryce Wilcox-O'Hearn <zooko@twistedmatrix.com>
+
+ * twisted.protocols.gnutella: Twisted now has an implementation of
+ the Gnutella protocol.
+
+2002-07-15 Sean Riley <sean@twistedmatrix.com>
+
+ * twisted.sister: Now featuring distributed login.
+
+2002-07-15 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted.conch: A new implementation of ssh2, bringing Twisted
+ one step closer to being a complete replacement of all unix
+ services ;-)
+
+2002-07-14 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.19.0! It's exactly the same as rc4.
+
+2002-07-13 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.19.0rc4. All Known Issues in the README have
+ been fixed. This will hopefully be the last release candidate for
+ 0.19.0.
+
+2002-07-07 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.19.0rc3.
+
+2002-07-07 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.19.0rc2.
+
+2002-07-07 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * Releasing Twisted 0.19.0rc1.
+
+2002-07-07 Keith Zaback <krz@twistedmatrix.com>
+
+ * twisted.internet.cReactor: A new poll-based reactor written in
+ C. This is still very experimental and incomplete.
+
+2002-07-07 Donovan Preston <dp@twistedmatrix.com>
+
+ * twisted.web.dom*: Better support in domtemplate/domwidgets etc
+ for Deferreds and Widgets. Also deprecated getTemplateMethods
+ method in favor of automatically looking up methods on the class
+ based on the attributes found in the template. There are some
+ minimal docs already, and better ones coming soon.
+
+2002-06-26 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.internet.process,interfaces,default: Process now
+ supports SetUID: there are new UID/GID arguments to the process
+ spawning methods/constructors.
+
+2002-06-22 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted.protocols.oscar: totally rewrote OSCAR protocol
+ implementation.
+
+2002-06-18 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.internet.defer: Deprecated the arm method of Deferred
+ objects: the replacement is a pair of methods, pause and
+ unpause. After the pause method is called, it is guaranteed that
+ no call/errbacks will be called (at least) until unpause is
+ called.
+
+2002-06-10 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/persisted/aot.py, bin/twistd,mktap, twisted/internet/app.py:
+
+ AOT (Abstract Object Tree) experimental source-persistence
+ mechanism. This is a more-concise, easier-to-edit alternative to
+ Twisted's XML persistence, for people who know how to edit Python
+ code. Also added appropriate options to mktap and twistd to
+ load/save .tas (Twisted Application Source) files.
+
+ I will be working on making the formatting better, soon, but it's
+ workable for now.
+
+2002-06-08 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted.internet, twisted.tap.web: Add a --https and related
+ options to 'mktap web'; web is now much more SSL-friendly.
+
+
+2002-06-02 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted.internet: changed protocol factory interface - it now has
+ doStop and doStart which are called in reactors, not app.Application.
+ This turns start/stopFactory into an implementation-specific feature,
+ and also ensures they are only called once.
+
+2002-06-01 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.18.0
+
+2002-05-31 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/coil/plugins/portforward.py, twisted/tap/portforward.py:
+ Forgot to add these before rc1 :-) You can use the portforwarder
+ with Coil and mktap again (previously "stupidproxy")
+
+ * twisted/web/static.py: Fixed a bunch of bugs related to redirection
+ for directories.
+
+ * .: Releasing Twisted 0.18.0rc2
+
+2002-05-30 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * Twisted no longer barfs when the Python XML packages aren't available.
+
+2002-05-29 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.18.0rc1
+
+2002-05-25 Christopher Armstrong <radix@twistedmatrix.com>
+
+ * twisted/spread/pb.py, twisted/internet/defer.py,
+ twisted/python/failure.py, etc:
+
+ Perspective broker now supports Failures! This should make writing
+ robust PB clients *much* easier. What this means is that errbacks will
+ recieve instances of t.python.failure.Failure instead of just strings
+ containing the traceback -- so you can easily .trap() particular
+ errors and handle them appropriately.
+
+2002-05-24 Itamar Shtull-Trauring, Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted.mail cleanups:
+
+ * basic bounce support.
+
+ * removed telnet from mail tap
+
+ * mail domains now receive service in __init__
+
+ * split file system stuff into Queue (renamed from
+ MessageCollection)
+
+ * Put a Queue in service
+
+ * twisted/protocol/smtp.py: changed SMTPClient API so that it returns
+ a file for the message content, instead of a string.
+
+2002-05-23 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * Twisted applications can now be persisted to XML files (.tax) with
+ the --xml option -- this is pretty verbose and needs some optimizations.
+
+2002-05-22 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/persisted/marmalade.py: Marmalade: Jelly, with just a hint
+ of bitterness. An XML object serialization module designed so
+ people can hand-edit persisted objects (like Twisted Applications).
+
+2002-05-21 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/internet/gtkreactor.py: GTK+ support for win32; input_add
+ is not supported in win32 and had to be worked around.
+
+2002-05-20 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/pythor/defer.py, twisted/protocols/protocol.py,
+ twisted/internet/defer.py, twisted/internet/protocol.py:
+
+ Moved defer and protocol to twisted.internet to straighten
+ out dependancies.
+
+2002-05-18 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/metrics, twisted/forum: Metrics and Forum are no longer
+ a part of Twisted proper; They are now in different CVS modules, and
+ will be released separately.
+
+2002-05-15 Andrew Bennetts <andrew-twisted@puzzling.org>
+
+ * twisted/protocols/ftp.py: Small fixes to FTPClient that have
+ changed the interface slightly -- return values from callbacks
+ are now consistent for active and passive FTP. Have a look at
+ doc/examples/ftpclient.py for details.
+
+2002-05-12 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * doc/specifications/banana.html: Documentation of the Banana protocol.
+
+2002-05-06 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/im/gtkchat.py: Some more UI improvements to InstanceMessenger:
+ Nicks are now colorful (each nick is hashed to get a color) and
+ messages now have timestamps.
+
+2002-05-04 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * Reactor Refactor! Pretty much all of the twisted.internet.* classes
+ are being depracated in favor of a single, central class called the
+ "reactor". Interfaces are defined in twisted.internet.interfaces.
+ For a much more descriptive comment about this change, see
+ http://twistedmatrix.com/pipermail/twisted-commits/2002-May/002104.html.
+
+2002-05-04 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/spread/pb.py: There is now some resource limiting in PB.
+ Clients can now have the number of references to an object limited.
+
+2002-04-29 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/im/*: Refactored Twisted InstanceMessenger to seperate GUI
+ and logic. Also improved the UI a bit.
+
+2002-04-28 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/protocols/http.py: log hits using extended log format
+ and make web taps logfile configurable.
+
+2002-04-26 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/lumberjack/logfile.py: reversed order of rotated
+ logs - higer numbers are now older.
+
+2002-04-24 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * doc/examples/ircLogBot.py: We now have a sample IRC bot that logs
+ all messages to a file.
+
+2002-04-24 Itamar Shtull-Trauring <twisted@itamarst.org>
+
+ * twisted/python/components.py: Twisted's interfaces are now
+ more like Zope's - __implements__ is an Interface subclass
+ or a tuple (or tuple of tuples). Additonally, an instance can
+ implement an interface even if its class doesn't have an
+ __implements__.
+
+2002-04-22 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/python/usage.py: Minor niceties for usage.Options:
+ You can now look up the options of an Options object with
+ optObj['optName'], and you if you define opt_* methods with
+ underscores in them, using dashes on the command line will work.
+
+2002-04-21 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/scripts/mktap.py: No more --manhole* options, use
+ '--append=my.tap manhole' now.
+
+2002-04-20 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.17.4.
+
+ * twisted/internet/tcp.py: Make unix domain sockets *really*
+ world-accessible, rather than just accessible by "other".
+
+2002-04-19 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/web/{server,twcgi}.py: Fixed POST bug in distributed
+ web servers.
+
+2002-04-19 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.17.3.
+
+2002-04-19 Glyph Lefkowitz <carmstro@twistedmatrix.com>
+
+ * twisted/web/distrib.py: Fix a bug where static.File transfers
+ over a distributed-web connection would not finish up properly.
+
+2002-04-18 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.17.2.
+
+2002-04-18 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/news: A news server and NNTP protocol support courtesy of
+ exarkun. Another step towards Twisted implementations of EVERYTHING
+ IN THE WORLD!
+
+2002-04-17 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/spread/pb.py: Errors during jelly serialization used to
+ just blow up; now they more properly return a Deferred Failure. This
+ will make hangs in PB apps (most notably distributed web) less common.
+
+2002-04-17 Donovan Preston <dp@twistedmatrix.com>
+
+ * Major changes to the capabilities of the static web server, in an
+ attempt to be able to use Twisted instead of Zope at work; my plan is to
+ capture many of the conveniences of Zope without the implicitness and
+ complexity that comes with working around implicit behavior when it fails.
+
+ 1) .trp and .rpy support in the static web server:
+ Very simple handlers to allow you to easily add Resource objects
+ dynamically to a running server, by merely changing files on the
+ filesystem.
+ An .rpy file will be executed, and if a "resource" variable exists upon the
+ execution's completion, it will be returned.
+ A .trp file (twisted resource pickle) will be unpickled and returned. An
+ object unpickled from a .trp should either implement IResource itself,
+ or have a registered adapter in twisted.python.components.
+
+ 2) Acquisition:
+ As resources are being looked up by repeated calls to getChild, this
+ change creates instances of
+ twisted.spread.refpath.PathReferenceAcquisitionContext and puts
+ them in the request as "request.pathRef"
+ Any method that has an instance of the request can then climb up
+ the parent tree using "request.pathRef['parentRef']['parentRef']
+ PathReferenceAcquisitionContext instances can be dereferenced to the
+ actual object using getObject
+ Convenience method: "locate" returns a PathReference to first place
+ in the parent heirarchy a name is seen
+ Convenience method: "acquire" somewhat like Zope acquisition;
+ mostly untested, may need fixes
+
+ 3) DOM-based templating system:
+ A new templating system that allows python scripts to use the DOM
+ to manipulate the HTML node tree. Loosely based on Enhydra.
+ Subclasses of twisted.web.domtemplate.DOMTemplate can override
+ the templateFile attribute and the getTemplateMethods method;
+ ultimately, while templateFile is being parsed, the methods
+ specified will be called with instances of xml.dom.mindom.Node
+ as the first parameter, allowing the python code to manipulate
+ (see twisted.web.blog for an example)
+
+2002-04-17 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/web/static.py, twisted/tap/web.py: Added a new feature
+ that allows requests for /foo to return /foo.extension, which is
+ disabled by default. If you want a --static webserver that
+ uses this feature, use 'mktap web --static <dir> --allow_ignore_ext'.
+
+ * twisted/tap/web.py: Also switched --static to --path; it doesn't
+ make sense to call something that automatically executes cgis, epys,
+ rpys, php, etc., "static". :-)
+
+2002-04-14 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * HTTP 1.1 now supports persistent and pipelined connections.
+
+ User-visible API changes:
+ - Request.content is now a file-like object, instead of a string.
+ - Functions that incorrectly used Request.received instead of
+ Request.getAllHeaders() will break.
+ - sendHeader, finishHeaders, sendStatus are all hidden now.
+
+2002-04-12 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/coil/plugins/tendril.py (TendrilConfigurator): New coil
+ configurator for words.tendril.
+
+2002-04-10 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.17.0
+
+2002-04-10 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/bugs: Gone. Separate plugin package.
+ * twisted/eco: Gone. The king is dead. Long live the king!
+ (eco is no longer going to be developed, Pyrex has obviated it.)
+
+2002-04-10 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/protocols/irc.py: Some fix-ups to IRCClient and
+ DccFileReceive, from Joe Jordan (psy).
+
+2002-04-10 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/reality: Gone. This is now in a completely separate plugin
+ package.
+
+2002-04-09 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * win32 process support seems to *finally* be working correctly. Many
+ thanks to Drew Whitehouse for help with testing and debugging.
+
+2002-04-08 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * coil refactored yet again, this time to use components and adapters.
+ The design is now much cleaner.
+
+2002-04-08 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * twisted/spread/jelly.py: Refactored jelly to provide (a) more
+ sane, language-portable API for efficient extensibility and (b)
+ final version of "wire" protocol. This should be very close to
+ the last wire-protocol-breaking change to PB before
+ standardization happens.
+
+2002-04-04 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * Removed __getattr__ backwards compatibility in PB
+
+2002-04-03 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/python/usage.py, twisted/test/test_usage.py, bin/mktap, twisted/tap/*.py:
+ Made the usage.Options interface better -- options are now stored in the
+ 'opts' dict. This is backwards compatible, and I added a deprecation warning.
+
+2002-04-01 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.16.0.
+
+2002-03-29 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Added Qt event loop support, written by Sirtaj Singh Kang and
+ Aleksandar Erkalovic.
+
+2002-03-29 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Added a 'coil' command for configuring TAP files
+
+2002-03-15 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * XML-RPC published methods can now return Deferreds, and Twisted
+ will Do The Right Thing.
+
+2002-03-13 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Refactored coil, the configuration mechanism for Twisted.
+ See twisted.coil and twisted.coil.plugins for examples of how
+ to use the new interface. Chris Armstrong did some UI improvements
+ for coil as well.
+
+ * Checked in win32 Process support, and fixed win32 event loop.
+
+2002-03-11 Glyph Lefkowitz <glyph@janus.twistedmatrix.com>
+
+ * More robust shutdown sequence for default mainloop (other
+ mainloops should follow suit, but they didn't implement shutdown
+ callbacks properly before anyway...). This allows for shutdown
+ callbacks to continue using the main loop.
+
+2002-03-09 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Automatic log rotation for twistd. In addition, sending SIGUSR1
+ to twistd will rotate the log.
+
+2002-03-07 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.15.5.
+
+2002-03-06 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * twisted/web/html.py: Got rid of html.Interface. This was a really
+ old, really deprecated API.
+
+2002-03-06 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/web/widgets.py: Deprecated usage of Gadget.addFile(path)
+ and replaced it with Gadget.putPath(path, pathname). This is
+ a lot more flexible.
+
+2002-03-05 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/internet/win32.py: New win32 event loop, written by
+ Andrew Bennetts.
+
+ * twisted/tap/*: Changed the interface for creating tap modules - use
+ a method called updateApplication instead of getPorts. this
+ is a much more generic and useful mechanism.
+
+ * twisted/internet/task.py: Fixed a bug where the schedular wasn't
+ installed in some cases.
+
+2002-03-04 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/web/server.py: authorizer.Unauthorized->util.Unauthorized
+ (leftovers from removing .passport references.)
+
+ * twisted/names/dns.py: Added support for TTL.
+
+2002-03-02 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.15.4.
+
+2002-03-02 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/words/ircservice.py: Send End-Of-MOTD message --
+ some clients rely on this for automatic joining of channels
+ and whatnot.
+
+2002-03-02 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/names/dns.py: Fixed bugs in DNS client
+
+2002-03-01 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * twisted/protocols/dns.py: Can now correctly serialize answers
+
+ * twisted/names/dns.py: Can now do simple serving of domains
+
+ * twisted/internet/stupid.py: Removed spurious debugging print
+
+2002-02-28 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing 0.15.3.
+
+2002-02-27 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/mail/*, twisted/plugins.tml: The Mail server is now
+ COILable.
+
+ * bin/twistd: security fix: use a secure umask (077, rather than 0)
+ for twistd.pid.
+
+2002-02-26 Allen Short <washort@twistedmatrix.com>
+
+ * twisted/eco/eco.py, twisted/eco/sexpy.py: ECO now supports
+ backquoting and macros.
+
+2002-02-26 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/protocols/ftp.py, twisted/plugins.tml: Made the FTP
+ server COILable!
+
+2002-02-26 Benjamin Bruheim <phed@twistedmatrix.com>
+
+ * twisted/web/distrib.py: Fixed a win32-compatibility bug.
+
+2002-02-24 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/protocols/socks.py: Made SOCKSv4 coilable, and fixed a
+ bug so it'd work with Mozilla.
+
+2002-02-24 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * .: Releasing Twisted 0.15.2.
+
+2002-02-24 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * setup.py: Added plugins.tml and instancemessenger.glade installs
+ so mktap and t-im work in a 'setup.py install' install.
+
+ * debian/rules: Install plugins.tml so mktap works in debian installs.
+
+ * doc/man/mktap.1, twistd.1: Updated the man pages to be more accurate.
+
+2002-02-24 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * bin/mktap: Better error reporting when we don't find
+ the plugins files.
+
+ * bin/twistd: Print out the *real* usage description rather than
+ barfing when we get bad command line arguments.
+
+2002-02-24 Moshe Zadka <moshez@twistedmatrix.com>
+
+ * debian/rules: Install the instancemessenger.glade file, so IM
+ will work in debian installs.
+
+2002-02-24 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/protocols/oscar.py, socks.py, toc.py: Fixed a security
+ hole in TOC where clients could call any method on the server.
+
+2002-02-23 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/tap/coil.py: There is now a tap-creator for COIL.
+
+ * twisted/internet/stupidproxy.py: Now with COILability!
+
+2002-02-23 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * bin/mktap: mktap now uses Plugins instead of searching through
+ twisted.tap. Yay for unified configuration systems!
+
+
+2002-02-22 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/im, twisted/words: t-im can now do topic setting (words
+ only), fixed the Group Metadata-setting interface in the service.
+
+2002-02-22 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * twisted/manhole: COIL can now load Plugins.
+
+2002-02-21 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * twisted.spread.pb: Changed remote method invocations to be
+ called through .callRemote rather than implicitly by getattr, and
+ added LocalAsRemote utility class for emulating remote behavior.
+
+2002-02-21 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.protocols.ftp: Fixed a lot of serious bugs.
+
+2002-02-20 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.protocols.telnet: the python shell now supports
+ multi-line commands and can be configured using coil.
+
+2002-02-13 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted.lumberjack: a log rotation and viewing service.
+ Currently only log rotation is supported.
+
+2002-02-12 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/words/ircservice.py (IRCChatter.irc_AWAY): Fix bug
+ where you can never come back from being away (at least using
+ epic4). Closes: #%d
+
+2002-02-11 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/web/widgets.py: Changed Gadget.page to Gadget.pageFactory
+ for clarity (this is backwards-compatible).
+
+2002-02-10 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/spread/jelly.py:
+ * twisted/spread/banana.py:
+ * twisted/spread/pb.py: fixed bugs found by pychecker, got rid
+ of __ping__ method support, and added 'local_' methods to
+ RemoteReference
+
+ * twisted/persisted/styles.py: pychecker bug fixes
+
+2002-02-09 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * bin/eco: Created a command-line interpreter for ECO.
+
+ * doc/man/eco.1: man page for bin/eco
+
+2002-02-09 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/eco/eco.py: Reverted evaluator state back to functional-ness
+ :) And added functions (anonymous and global), and broke various
+ interfaces
+
+2002-02-09 Allen Short <washort@twistedmatrix.com>
+
+ * twisted/eco/eco.py: Refactored evaluator into a class, improved
+ python-function argument signatures, and added and/or/not functions.
+
+2002-02-08 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/words/service.py, ircservice.py: Fixed annoying PING
+ bug, and added /topic support.
+
+2002-02-08 Glyph Lefkowitz <glyph@twistedmatrix.com>
+
+ * twisted/eco: Initial prototype of ECO, the Elegant C Overlay
+ macro engine.
+
+2002-02-02 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/im/ircsupport.py: Added support for the IRC protocol
+ to IM.
+
+2002-02-02 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/python/deferred.py: added Deferred.addErrback, so now
+ it's easy to attach errbacks to deferreds when you don't care
+ about plain results.
+
+ * twisted/im/chat.py, twisted/im/pbsupport.py: added support for
+ displaying topics.
+
+2002-02-02 Paul Swartz <z3p@twistedmatrix.com>
+
+ * SOCKSv4 support: there is now a protocols.socks, which contains
+ support for SOCKSv4, a TCP proxying protocol. mktap also has
+ support for the new protocol.
+
+2002-02-02 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/words/ircservice.py (IRCChatter.receiveDirectMessage),
+ (IRCChatter.receiveGroupMessage),
+ (IRCChatter.irc_PRIVMSG): Added CTCP ACTION <-> emote translation
+
+2002-02-01 Paul Swartz <z3p@twistedmatrix.com>
+
+ * twisted/im/tocsupport.py: Added support for most of the TOC
+ protocol to IM.
+
+
+2002-02-01 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/im/*.py: added metadata/emote support to IM. "/me foo"
+ now triggers a backwards-compatible emote.
+
+
+2002-01-30 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * twisted/internet/tcp.py: Fixed the bug where startFactory() would
+ get called twice.
+
+2002-01-30 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/im: a new client for twisted.words (and eventually
+ much more) based on GTK+ and Glade. This is mainly glyph's
+ code, but I organized it for him to check in.
+
+ * twisted/words/service.py: metadata support for words messages
+ (only {'style': 'emote'} is standardized as of yet)
+
+2002-01-29 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Added hook to tcp.Port and ssl.Port for limiting acceptable
+ connections - approveConnection(socket, addr).
+
+2002-01-27 Chris Armstrong <carmstro@twistedmatrix.com>
+
+ * twisted/words/ircservice.py: You can now change the topic
+ of a channel with '/msg channelName topic <topic>' - note that
+ 'channelName' does *not* include the '#'.
+
+2002-01-23 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * Incompatible change to PB: all remote methods now return
+ Deferreds. This doesn't break code in as many places as possible,
+ but the connection methods now work differently and have different
+ signatures.
+
+ * Incompatible change to Banana: Banana now really supports floats
+ and long integers. This involved removing some nasty hackery that
+ was previously part of the protocol spec, so you'll need to
+ upgrade.
+
+ * Added a feature to Jelly: Jelly now supports unicode strings.
+
+ * Improved Twisted.Forums considerably: still needs work, but it's
+ growing into an example of what you can do with a Twisted.Web
+ application.
+
+ * Added Twisted.Web.Webpassport -- generic mechanism for web-based
+ login to arbitrary services. This in conjunction with some code
+ in Forum that uses it.
+
+ * Incompatible change in Enterprise: all query methods now return
+ Deferreds, as well as take arguments in an order which makes it
+ possible to pass arbitrary argument lists for using the database's
+ formatting characters rather than python's.
+
+2002-01-15 Glyph Lefkowitz <glyph@zelda.twistedmatrix.com>
+
+ * twisted/internet/passport.py: (and friends) Retrieval of
+ perspectives is now asynchronous, hooray (this took way too long)!
+ Perspectives may now be stored in external data sources. Lurching
+ slowly towards a stable API for the Passport system, along with
+ Sean's recent commits of tools to manipulate it.
+
+2002-01-14 Kevin Turner <acapnotic@twistedmatrix.com>
+
+ * twisted/python/explorer.py: reimplementated. So it's better.
+ And yes, I broke the API.
+
+ * twisted/manhole/ui/spelunk_gnome.py: Less duplication of visages,
+ and they're draggable now too.
+
+2002-01-13 Itamar Shtull-Trauring <itamarst@twistedmatrix.com>
+
+ * Changed twisted.enterprise.adabi so operations can accept lists
+ of arguments. This allows us to use the database adaptor's native
+ SQL quoting ability instead of either doing it ourselves, or the
+ *current* way twisted does it (not doing it at all, AFAICT!).
+
+ cursor.execute("INSERT INTO foo VALUES (%s, %d), "it's magic", 12)
+
+ Problem is that different adaptors may have different codes for
+ quoting.
+
+ * First go at database for twisted.bugs. I hate RDBMS. I hate web.
+
+--- 0.13.0 Release ---
+
+# Local Variables:
+# add-log-time-format: add-log-iso8601-time-string
+# End:
diff --git a/twisted/topfiles/NEWS b/twisted/topfiles/NEWS
new file mode 100644
index 0000000..a140371
--- /dev/null
+++ b/twisted/topfiles/NEWS
@@ -0,0 +1,1744 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Core 12.1.0 (2012-06-02)
+================================
+
+Features
+--------
+ - The kqueue reactor has been revived. (#1918)
+ - twisted.python.filepath now provides IFilePath, an interface for
+ file path objects. (#2176)
+ - New gtk3 and gobject-introspection reactors have been added.
+ (#4558)
+ - gtk and glib reactors now run I/O and scheduled events with lower
+ priority, to ensure the UI stays responsive. (#5067)
+ - IReactorTCP.connectTCP() can now accept IPv6 address literals
+ (although not hostnames) in order to support connecting to IPv6
+ hosts. (#5085)
+ - twisted.internet.interfaces.IReactorSocket, a new interface, is now
+ supported by some reactors to listen on sockets set up by external
+ software (eg systemd or launchd). (#5248)
+ - twisted.internet.endpoints.clientFromString now also supports
+ strings in the form of tcp:example.com:80 and ssl:example.com:4321
+ (#5358)
+ - twisted.python.constants.Flags now provides a way to define
+ collections of flags for bitvector-type uses. (#5384)
+ - The epoll(7)-based reactor is now the default reactor on Linux.
+ (#5478)
+ - twisted.python.runtime.platform.isLinux can be used to check if
+ Twisted is running on Linux. (#5491)
+ - twisted.internet.endpoints.serverFromString now recognizes a
+ "systemd" endpoint type, for listening on a server port inherited
+ from systemd. (#5575)
+ - Connections created using twisted.internet.interfaces.IReactorUNIX
+ now support sending and receiving file descriptors between
+ different processes. (#5615)
+ - twisted.internet.endpoints.clientFromString now supports UNIX
+ client endpoint strings with the path argument specified like
+ "unix:/foo/bar" in addition to the old style, "unix:path=/foo/bar".
+ (#5640)
+ - twisted.protocols.amp.Descriptor is a new AMP argument type which
+ supports passing file descriptors as AMP command arguments over
+ UNIX connections. (#5650)
+
+Bugfixes
+--------
+ - twisted.internet.abstract.FileDescriptor implements
+ twisted.internet.interfaces.IPushProducer instead of
+ twisted.internet.interfaces.IProducer.
+ twisted.internet.iocpreactor.abstract.FileHandle implements
+ twisted.internet.interfaces.IPushProducer instead of
+ twisted.internet.interfaces.IProducer. (#4386)
+ - The epoll reactor now supports reading/writing to regular files on
+ stdin/stdout. (#4429)
+ - Calling .cancel() on any Twisted-provided client endpoint
+ (TCP4ClientEndpoint, UNIXClientEndpoint, SSL4ClientEndpoint) now
+ works as documented, rather than logging an AlreadyCalledError.
+ (#4710)
+ - A leak of OVERLAPPED structures in some IOCP error cases has been
+ fixed. (#5372)
+ - twisted.internet._pollingfile._PollableWritePipe now checks for
+ outgoing unicode data in write() and writeSequence() instead of
+ checkWork(). (#5412)
+
+Improved Documentation
+----------------------
+ - "Working from Twisted's Subversion repository" links to UQDS and
+ Combinator are now updated. (#5545)
+ - Added tkinterdemo.py, an example of Tkinter integration. (#5631)
+
+Deprecations and Removals
+-------------------------
+ - The 'unsigned' flag to twisted.scripts.tap2rpm.MyOptions is now
+ deprecated. (#4086)
+ - Removed the unreachable _fileUrandom method from
+ twisted.python.randbytes.RandomFactory. (#4530)
+ - twisted.persisted.journal is removed, deprecated since Twisted
+ 11.0. (#4805)
+ - Support for pyOpenSSL 0.9 and older is now deprecated. pyOpenSSL
+ 0.10 or newer will soon be required in order to use Twisted's SSL
+ features. (#4974)
+ - backwardsCompatImplements and fixClassImplements are removed from
+ twisted.python.components, deprecated in 2006. (#5034)
+ - twisted.python.reflect.macro was removed, deprecated since Twisted
+ 8.2. (#5035)
+ - twisted.python.text.docstringLStrip, deprecated since Twisted
+ 10.2.0, has been removed (#5036)
+ - Removed the deprecated dispatch and dispatchWithCallback methods
+ from twisted.python.threadpool.ThreadPool (deprecated since 8.0)
+ (#5037)
+ - twisted.scripts.tapconvert is now deprecated. (#5038)
+ - twisted.python.reflect's Settable, AccessorType, PropertyAccessor,
+ Accessor, OriginalAccessor and Summer are now deprecated. (#5451)
+ - twisted.python.threadpool.ThreadSafeList (deprecated in 10.1) is
+ removed. (#5473)
+ - twisted.application.app.initialLog, deprecated since Twisted 8.2.0,
+ has been removed. (#5480)
+ - twisted.spread.refpath was deleted, deprecated since Twisted 9.0.
+ (#5482)
+ - twisted.python.otp, deprecated since 9.0, is removed. (#5493)
+ - Removed `dsu`, `moduleMovedForSplit`, and `dict` from
+ twisted.python.util (deprecated since 10.2) (#5516)
+
+Other
+-----
+ - #2723, #3114, #3398, #4388, #4489, #5055, #5116, #5242, #5380,
+ #5392, #5447, #5457, #5484, #5489, #5492, #5494, #5512, #5523,
+ #5558, #5572, #5583, #5593, #5620, #5621, #5623, #5625, #5637,
+ #5652, #5653, #5656, #5657, #5660, #5673
+
+
+Twisted Core 12.0.0 (2012-02-10)
+================================
+
+Features
+--------
+ - The interface argument to IReactorTCP.listenTCP may now be an IPv6
+ address literal, allowing the creation of IPv6 TCP servers. (#5084)
+ - twisted.python.constants.Names now provides a way to define
+ collections of named constants, similar to the "enum type" feature
+ of C or Java. (#5382)
+ - twisted.python.constants.Values now provides a way to define
+ collections of named constants with arbitrary values. (#5383)
+
+Bugfixes
+--------
+ - Fixed an obscure case where connectionLost wasn't called on the
+ protocol when using half-close. (#3037)
+ - UDP ports handle socket errors better on Windows. (#3396)
+ - When idle, the gtk2 and glib2 reactors no longer wake up 10 times a
+ second. (#4376)
+ - Prevent a rare situation involving TLS transports, where a producer
+ may be erroneously left unpaused. (#5347)
+ - twisted.internet.iocpreactor.iocpsupport now has fewer 64-bit
+ compile warnings. (#5373)
+ - The GTK2 reactor is now more responsive on Windows. (#5396)
+ - TLS transports now correctly handle producer registration after the
+ connection has been lost. (#5439)
+ - twisted.protocols.htb.Bucket now empties properly with a non-zero
+ drip rate. (#5448)
+ - IReactorSSL and ITCPTransport.startTLS now synchronously propagate
+ errors from the getContext method of context factories, instead of
+ being capturing them and logging them as unhandled. (#5449)
+
+Improved Documentation
+----------------------
+ - The multicast documentation has been expanded. (#4262)
+ - twisted.internet.defer.Deferred now documents more return values.
+ (#5399)
+ - Show a better starting page at
+ http://twistedmatrix.com/documents/current (#5429)
+
+Deprecations and Removals
+-------------------------
+ - Remove the deprecated module twisted.enterprise.reflector. (#4108)
+ - Removed the deprecated module twisted.enterprise.row. (#4109)
+ - Remove the deprecated module twisted.enterprise.sqlreflector.
+ (#4110)
+ - Removed the deprecated module twisted.enterprise.util, as well as
+ twisted.enterprise.adbapi.safe. (#4111)
+ - Python 2.4 is no longer supported on any platform. (#5060)
+ - Removed printTraceback and noOperation from twisted.spread.pb,
+ deprecated since Twisted 8.2. (#5370)
+
+Other
+-----
+ - #1712, #2725, #5284, #5325, #5331, #5362, #5364, #5371, #5407,
+ #5427, #5430, #5431, #5440, #5441
+
+
+Twisted Core 11.1.0 (2011-11-15)
+================================
+
+Features
+--------
+ - TCP and TLS transports now support abortConnection() which, unlike
+ loseConnection(), always closes the connection immediately. (#78)
+ - Failures received over PB when tracebacks are disabled now display
+ the wrapped exception value when they are printed. (#581)
+ - twistd now has a --logger option, allowing the use of custom log
+ observers. (#638)
+ - The default reactor is now poll(2) on platforms that support it.
+ (#2234)
+ - twisted.internet.defer.inlineCallbacks(f) now raises TypeError when
+ f returns something other than a generator or uses returnValue as a
+ non-generator. (#2501)
+ - twisted.python.usage.Options now supports performing Zsh tab-
+ completion on demand. Tab-completion for Twisted commands is
+ supported out-of-the-box on any recent zsh release. Third-party
+ commands may take advantage of zsh completion by copying the
+ provided stub file. (#3078)
+ - twisted.protocols.portforward now uses flow control between its
+ client and server connections to avoid having to buffer an
+ unbounded amount of data when one connection is slower than the
+ other. (#3350)
+ - On Windows, the select, IOCP, and Gtk2 reactors now implement
+ IReactorWin32Events (most notably adding support for serial ports
+ to these reactors). (#4862)
+ - twisted.python.failure.Failure no longer captures the state of
+ locals and globals of all stack frames by default, because it is
+ expensive to do and rarely used. You can pass captureVars=True to
+ Failure's constructor if you want to capture this data. (#5011)
+ - twisted.web.client now supports automatic content-decoding via
+ twisted.web.client.ContentDecoderAgent, gzip being supported for
+ now. (#5053)
+ - Protocols may now implement ILoggingContext to customize their
+ logging prefix. twisted.protocols.policies.ProtocolWrapper and the
+ endpoints wrapper now take advantage of this feature to ensure the
+ application protocol is still reflected in logs. (#5062)
+ - AMP's raw message-parsing performance was increased by
+ approximately 12%. (#5075)
+ - Twisted is now installable on PyPy, because some incompatible C
+ extensions are no longer built. (#5158)
+ - twisted.internet.defer.gatherResults now accepts a consumeErrors
+ parameter, with the same meaning as the corresponding argument for
+ DeferredList. (#5159)
+ - Added RMD (remove directory) support to the FTP client. (#5259)
+ - Server factories may now implement ILoggingContext to customize the
+ name that is logged when the reactor uses one to start listening on
+ a port. (#5292)
+ - The implementations of ITransport.writeSequence will now raise
+ TypeError if passed unicode strings. (#3896)
+ - iocp reactor now operates correctly on 64 bit Python runtimes.
+ (#4669)
+ - twistd ftp now supports the cred plugin. (#4752)
+ - twisted.python.filepath.FilePath now has an API to retrieve the
+ permissions of the underlying file, and two methods to determine
+ whether it is a block device or a socket. (#4813)
+ - twisted.trial.unittest.TestCase is now compatible with Python 2.7's
+ assertDictEqual method. (#5291)
+
+Bugfixes
+--------
+ - The IOCP reactor now does not try to erroneously pause non-
+ streaming producers. (#745)
+ - Unicode print statements no longer blow up when using Twisted's
+ logging system. (#1990)
+ - Process transports on Windows now support the `writeToChild` method
+ (but only for stdin). (#2838)
+ - Zsh tab-completion of Twisted commands no longer relies on
+ statically generated files, but instead generates results on-the-
+ fly - ensuring accurate tab-completion for the version of Twisted
+ actually in use. (#3078)
+ - LogPublishers don't use the global log publisher for reporting
+ broken observers anymore. (#3307)
+ - trial and twistd now add the current directory to sys.path even
+ when running as root or on Windows. mktap, tapconvert, and
+ pyhtmlizer no longer add the current directory to sys.path. (#3526)
+ - twisted.internet.win32eventreactor now stops immediately if
+ reactor.stop() is called from an IWriteDescriptor.doWrite
+ implementation instead of delaying shutdown for an arbitrary period
+ of time. (#3824)
+ - twisted.python.log now handles RuntimeErrors more gracefully, and
+ always restores log observers after an exception is raised. (#4379)
+ - twisted.spread now supports updating new-style RemoteCache
+ instances. (#4447)
+ - twisted.spread.pb.CopiedFailure will no longer be thrown into a
+ generator as a (deprecated) string exception but as a
+ twisted.spread.pb.RemoteException. (#4520)
+ - trial now gracefully handles the presence of objects in sys.modules
+ which respond to attributes being set on them by modifying
+ sys.modules. (#4748)
+ - twisted.python.deprecate.deprecatedModuleAttribute no longer
+ spuriously warns twice when used to deprecate a module within a
+ package. This should make it easier to write unit tests for
+ deprecated modules. (#4806)
+ - When pyOpenSSL 0.10 or newer is available, SSL support now uses
+ Twisted for all I/O and only relies on OpenSSL for cryptography,
+ avoiding a number of tricky, potentially broken edge cases. (#4854)
+ - IStreamClientEndpointStringParser.parseStreamClient now correctly
+ describes how it will be called by clientFromString (#4956)
+ - twisted.internet.defer.Deferreds are 10 times faster at handling
+ exceptions raised from callbacks, except when setDebugging(True)
+ has been called. (#5011)
+ - twisted.python.filepath.FilePath.copyTo now raises OSError(ENOENT)
+ if the source path being copied does not exist. (#5017)
+ - twisted.python.modules now supports iterating over namespace
+ packages without yielding duplicates. (#5030)
+ - reactor.spawnProcess now uses the resource module to guess the
+ maximum possible open file descriptor when /dev/fd exists but gives
+ incorrect results. (#5052)
+ - The memory BIO TLS/SSL implementation now supports producers
+ correctly. (#5063)
+ - twisted.spread.pb.Broker no longer creates an uncollectable
+ reference cycle when the logout callback holds a reference to the
+ client mind object. (#5079)
+ - twisted.protocols.tls, and SSL/TLS support in general, now do clean
+ TLS close alerts when disconnecting. (#5118)
+ - twisted.persisted.styles no longer uses the deprecated allYourBase
+ function (#5193)
+ - Stream client endpoints now start (doStart) and stop (doStop) the
+ factory passed to the connect method, instead of a different
+ implementation-detail factory. (#5278)
+ - SSL ports now consistently report themselves as SSL rather than TCP
+ when logging their close message. (#5292)
+ - Serial ports now deliver connectionLost to the protocol when
+ closed. (#3690)
+ - win32eventreactor now behaves better in certain rare cases in which
+ it previously would have failed to deliver connection lost
+ notification to a protocol. (#5233)
+
+Improved Documentation
+----------------------
+ - Test driven development with Twisted and Trial is now documented in
+ a how-to. (#2443)
+ - A new howto-style document covering twisted.protocols.amp has been
+ added. (#3476)
+ - Added sample implementation of a Twisted push producer/consumer
+ system. (#3835)
+ - The "Deferred in Depth" tutorial now includes accurate output for
+ the deferred_ex2.py example. (#3941)
+ - The server howto now covers the Factory.buildProtocol method.
+ (#4761)
+ - The testing standard and the trial tutorial now recommend the
+ `assertEqual` form of assertions rather than the `assertEquals` to
+ coincide with the standard library unittest's preference. (#4989)
+ - twisted.python.filepath.FilePath's methods now have more complete
+ API documentation (docstrings). (#5027)
+ - The Clients howto now uses buildProtocol more explicitly, hopefully
+ making it easier to understand where Protocol instances come from.
+ (#5044)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.interfaces.IFinishableConsumer is now deprecated.
+ (#2661)
+ - twisted.python.zshcomp is now deprecated in favor of the tab-
+ completion system in twisted.python.usage (#3078)
+ - The unzip and unzipIter functions in twisted.python.zipstream are
+ now deprecated. (#3666)
+ - Options.optStrings, deprecated for 7 years, has been removed. Use
+ Options.optParameters instead. (#4552)
+ - Removed the deprecated twisted.python.dispatch module. (#5023)
+ - Removed the twisted.runner.procutils module that was deprecated in
+ Twisted 2.3. (#5049)
+ - Removed twisted.trial.runner.DocTestSuite, deprecated in Twisted
+ 8.0. (#5111)
+ - twisted.scripts.tkunzip is now deprecated. (#5140)
+ - Deprecated option --password-file in twistd ftp (#4752)
+ - mktap, deprecated since Twisted 8.0, has been removed. (#5293)
+
+Other
+-----
+ - #1946, #2562, #2674, #3074, #3077, #3776, #4227, #4539, #4587,
+ #4619, #4624, #4629, #4683, #4690, #4702, #4778, #4944, #4945,
+ #4949, #4952, #4957, #4979, #4980, #4987, #4990, #4994, #4995,
+ #4997, #5003, #5008, #5009, #5012, #5019, #5042, #5046, #5051,
+ #5065, #5083, #5088, #5089, #5090, #5101, #5108, #5109, #5112,
+ #5114, #5125, #5128, #5131, #5136, #5139, #5144, #5146, #5147,
+ #5156, #5160, #5165, #5191, #5205, #5215, #5217, #5218, #5223,
+ #5243, #5244, #5250, #5254, #5261, #5266, #5273, #5299, #5301,
+ #5302, #5304, #5308, #5311, #5321, #5322, #5327, #5328, #5332,
+ #5336
+
+
+Twisted Core 11.0.0 (2011-04-01)
+================================
+
+Features
+--------
+ - The reactor is not restartable, but it would previously fail to
+ complain. Now, when you restart an unrestartable reactor, you get
+ an exception. (#2066)
+ - twisted.plugin now only emits a short log message, rather than a
+ full traceback, if there is a problem writing out the dropin cache
+ file. (#2409)
+ - Added a 'replacement' parameter to the
+ 'twisted.python.deprecate.deprecated' decorator. This allows
+ deprecations to unambiguously specify what they have been
+ deprecated in favor of. (#3047)
+ - Added access methods to FilePath for FilePath.statinfo's st_ino,
+ st_dev, st_nlink, st_uid, and st_gid fields. This is in
+ preparation for the deprecation of FilePath.statinfo. (#4712)
+ - IPv4Address and UNIXAddress now have a __hash__ method. (#4783)
+ - twisted.protocols.ftp.FTP.ftp_STOR now catches `FTPCmdError`s
+ raised by the file writer, and returns the error back to the
+ client. (#4909)
+
+Bugfixes
+--------
+ - twistd will no longer fail if a non-root user passes --uid 'myuid'
+ as a command-line argument. Instead, it will emit an error message.
+ (#3172)
+ - IOCPReactor now sends immediate completions to the main loop
+ (#3233)
+ - trial can now load test methods from multiple classes, even if the
+ methods all happen to be inherited from the same base class.
+ (#3383)
+ - twisted.web.server will now produce a correct Allow header when a
+ particular render_FOO method is missing. (#3678)
+ - HEAD requests made to resources whose HEAD handling defaults to
+ calling render_GET now always receive a response with no body.
+ (#3684)
+ - trial now loads decorated test methods whether or not the decorator
+ preserves the original method name. (#3909)
+ - t.p.amp.AmpBox.serialize will now correctly consistently complain
+ when being fed Unicode. (#3931)
+ - twisted.internet.wxreactor now supports stopping more reliably.
+ (#3948)
+ - reactor.spawnProcess on Windows can now handle ASCII-encodable
+ Unicode strings in the system environment (#3964)
+ - When C-extensions are not complied for twisted, on python2.4, skip
+ a test in twisted.internet.test.test_process that may hang due to a
+ SIGCHLD related problem. Running 'python setup.py build_ext
+ --inplace' will compile the extension and cause the test to both
+ run and pass. (#4331)
+ - twisted.python.logfile.LogFile now raises a descriptive exception
+ when passed a log directoy which does not exist. (#4701)
+ - Fixed a bug where Inotify will fail to add a filepatch to watchlist
+ after it has been added/ignored previously. (#4708)
+ - IPv4Address and UNIXAddress object comparison operators fixed
+ (#4817)
+ - twisted.internet.task.Clock now sorts the list of pending calls
+ before and after processing each call (#4823)
+ - ConnectionLost is now in twisted.internet.error.__all__ instead of
+ twisted.words.protocols.jabber.xmlstream.__all__. (#4856)
+ - twisted.internet.process now detects the most appropriate mechanism
+ to use for detecting the open file descriptors on a system, getting
+ Twisted working on FreeBSD even when fdescfs is not mounted.
+ (#4881)
+ - twisted.words.services referenced nonexistent
+ twisted.words.protocols.irc.IRC_NOSUCHCHANNEL. This has been fixed.
+ Related code has also received test cases. (#4915)
+
+Improved Documentation
+----------------------
+ - The INSTALL file now lists all of Twisted's dependencies. (#967)
+ - Added the stopService and startService methods to all finger
+ example files. (#3375)
+ - Missing reactor.run() calls were added in the UDP and client howto
+ documents. (#3834)
+ - The maxRetries attribute of
+ twisted.internet.protocols.RetryingClientFactory now has API
+ documentation. (#4618)
+ - Lore docs pointed to a template that no longer existed, this has
+ been fixed. (#4682)
+ - The `servers` argument to `twisted.names.client.createResolver` now
+ has more complete API documentation. (#4713)
+ - Linked to the Twisted endpoints tutorial from the Twisted core
+ howto list. (#4773)
+ - The Endpoints howto now links to the API documentation. (#4774)
+ - The Quotes howto is now more clear in its PYTHONPATH setup
+ instructions. (#4785)
+ - The API documentation for DeferredList's fireOnOneCallback
+ parameter now gives the correct order of the elements of the result
+ tuple. (#4882)
+
+Deprecations and Removals
+-------------------------
+ - returning a value other than None from IProtocol.dataReceived was
+ deprecated (#2491)
+ - Deprecated the --extra option in trial. (#3372)
+ - twisted.protocols._c_urlarg has been removed. (#4162)
+ - Remove the --report-profile option for twistd, deprecated since
+ 2007. (#4236)
+ - Deprecated twisted.persisted.journal. This library is no longer
+ maintained. (#4298)
+ - Removed twisted.protocols.loopback.loopback, which has been
+ deprecated since Twisted 2.5. (#4547)
+ - __getitem__ __getslice__ and __eq__ (tuple comparison, indexing)
+ removed from twisted.internet.address.IPv4Address and
+ twisted.internet.address.UNIXAddress classes UNIXAddress and
+ IPv4Address properties _bwHack are now deprecated in
+ twisted.internet.address (#4817)
+ - twisted.python.reflect.allYourBase is now no longer used, replaced
+ with inspect.getmro (#4928)
+ - allYourBase and accumulateBases are now deprecated in favor of
+ inspect.getmro. (#4946)
+
+Other
+-----
+ - #555, #1982, #2618, #2665, #2666, #4035, #4247, #4567, #4636,
+ #4717, #4733, #4750, #4821, #4842, #4846, #4853, #4857, #4858,
+ #4863, #4864, #4865, #4866, #4867, #4868, #4869, #4870, #4871,
+ #4872, #4873, #4874, #4875, #4876, #4877, #4878, #4879, #4905,
+ #4906, #4908, #4934, #4955, #4960
+
+
+Twisted Core 10.2.0 (2010-11-29)
+================================
+
+Features
+--------
+ - twisted.internet.cfreactor has been significantly improved. It now
+ runs, and passes, the test suite. Many, many bugs in it have been
+ fixed, including several segfaults, as it now uses PyObjC and
+ longer requires C code in Twisted. (#1833)
+ - twisted.protocols.ftp.FTPRealm now accepts a parameter to override
+ "/home" as the container for user directories. The new
+ BaseFTPRealm class in the same module also allows easy
+ implementation of custom user directory schemes. (#2179)
+ - twisted.python.filepath.FilePath and twisted.python.zippath.ZipPath
+ now have a descendant method to simplify code which calls the child
+ method repeatedly. (#3169)
+ - twisted.python.failure._Frame objects now support fake f_locals
+ attribute. (#4045)
+ - twisted.internet.endpoints now has 'serverFromString' and
+ 'clientFromString' APIs for constructing endpoints from descriptive
+ strings. (#4473)
+ - The default trial reporter now combines reporting of tests with the
+ same result to shorten its summary output. (#4487)
+ - The new class twisted.protocols.ftp.SystemFTPRealm implements an
+ FTP realm which uses system accounts to select home directories.
+ (#4494)
+ - twisted.internet.reactor.spawnProcess now wastes less time trying
+ to close non-existent file descriptors on POSIX platforms. (#4522)
+ - twisted.internet.win32eventreactor now declares that it implements
+ a new twisted.internet.interfaces.IReactorWin32Events interface.
+ (#4523)
+ - twisted.application.service.IProcess now documents its attributes
+ using zope.interface.Attribute. (#4534)
+ - twisted.application.app.ReactorSelectionMixin now saves the value
+ of the --reactor option in the "reactor" key of the options object.
+ (#4563)
+ - twisted.internet.endpoints.serverFromString and clientFromString,
+ and therefore also twisted.application.strports.service, now
+ support plugins, so third parties may implement their own endpoint
+ types. (#4695)
+
+Bugfixes
+--------
+ - twisted.internet.defer.Deferred now handles chains iteratively
+ instead of recursively, preventing RuntimeError due to excessive
+ recursion when handling long Deferred chains. (#411)
+ - twisted.internet.cfreactor now works with trial. (#2556)
+ - twisted.enterprise.adbapi.ConnectionPool.close may now be called
+ even if the connection pool has not yet been started. This will
+ prevent the pool from ever starting. (#2680)
+ - twisted.protocols.basic.NetstringReceiver raises
+ NetstringParseErrors for invalid netstrings now. It handles empty
+ netstrings ("0:,") correctly, and the performance for receiving
+ netstrings has been improved. (#4378)
+ - reactor.listenUDP now returns an object which declares that it
+ implements IListeningPort. (#4462)
+ - twisted.python.randbytes no longer uses PyCrypto as a secure random
+ number source (since it is not one). (#4468)
+ - twisted.internet.main.installReactor now blocks installation of
+ another reactor when using python -O (#4476)
+ - twisted.python.deprecate.deprecatedModuleAttribute now emits only
+ one warning when used to deprecate a package attribute which is a
+ module. (#4492)
+ - The "brief" mode of twisted.python.failure.Failure.getTraceback now
+ handles exceptions raised by the underlying exception's __str__
+ method. (#4501)
+ - twisted.words.xish.domish now correctly parses XML with namespaces
+ which include whitespace. (#4503)
+ - twisted.names.authority.FileAuthority now generates correct
+ negative caching hints, marks its referral NS RRs as non-
+ authoritative, and correctly generates referrals for ALL_RECORDS
+ requests. (#4513)
+ - twisted.internet.test.reactormixins.ReactorBuilder's attribute
+ `requiredInterface` (which should an interface) is now
+ `requiredInterfaces` (a list of interfaces) as originally described
+ per the documentation. (#4527)
+ - twisted.python.zippath.ZipPath.__repr__ now correctly formats paths
+ with ".." in them (by including it). (#4535)
+ - twisted.names.hosts.searchFileFor has been fixed against
+ refcounting dependency. (#4540)
+ - The POSIX process transports now declare that they implement
+ IProcessTransport. (#4585)
+ - Twisted can now be built with the LLVM clang compiler, with
+ 'CC=clang python setup.py build'. C code that caused errors with
+ this compiler has been removed. (#4652)
+ - trial now puts coverage data in the path specified by --temp-
+ directory, even if that option comes after --coverage on the
+ command line. (#4657)
+ - The unregisterProducer method of connection-oriented transports
+ will now cause the connection to be closed if there was a prior
+ call to loseConnection. (#4719)
+ - Fixed an issue where the new StreamServerEndpointService didn't log
+ listen errors. (This was a bug not present in any previous
+ releases, as this class is new.) (#4731)
+
+Improved Documentation
+----------------------
+ - The trial man page now documents the meaning of the final line of
+ output of the default reporter. (#1384)
+ - The API documentation for twisted.internet.defer.DeferredList now
+ goes into more depth about the effects each of the __init__ flags
+ that class accepts. (#3595)
+ - There is now narrative documentation for the endpoints APIs, in the
+ 'endpoints' core howto, as well as modifications to the 'writing
+ clients' and 'writing servers' core howto documents to indicate
+ that endpoints are now the preferred style of listening and
+ connecting. (#4478)
+ - trial's man page now documents the --disablegc option in more
+ detail. (#4511)
+ - trial's coverage output format is now documented in the trial man
+ page. (#4512)
+ - Broken links and spelling errors in the finger tutorial are now
+ fixed. (#4516)
+ - twisted.internet.threads.blockingCallFromThread's docstring is now
+ explicit about Deferred support. (#4517)
+ - twisted.python.zippath.ZipPath.child now documents its handling of
+ ".." (which is not special, making it different from
+ FilePath.child). (#4535)
+ - The API docs for twisted.internet.defer.Deferred now cover several
+ more of its (less interesting) attributes. (#4538)
+ - LineReceiver, NetstringReceiver, and IntNStringReceiver from
+ twisted.protocols.basic now have improved API documentation for
+ read callbacks and write methods. (#4542)
+ - Tidied up the Twisted Conch documentation for easier conversion.
+ (#4566)
+ - Use correct Twisted version for when cancellation was introduced in
+ the Deferred docstring. (#4614)
+ - The logging howto is now more clear about how the standard library
+ logging module and twisted.python.log can be integrated. (#4642)
+ - The finger tutorial still had references to .tap files. This
+ reference has now been removed. The documentation clarifies
+ "finger.tap" is a module and not a filename. (#4679)
+ - The finger tutorial had a broken link to the
+ twisted.application.service.Service class, which is now fixed.
+ Additionally, a minor typo ('verison') was fixed. (#4681)
+ - twisted.protocols.policies.TimeoutMixin now has clearer API
+ documentation. (#4684)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.defer.Deferred.setTimeout has been removed, after
+ being deprecated since Twisted 2.0. (#1702)
+ - twisted.internet.interfaces.IReactorTime.cancelCallLater
+ (deprecated since 2007) and
+ twisted.internet.interfaces.base.ReactorBase.cancelCallLater
+ (deprecated since 2002) have been removed. (#4076)
+ - Removed twisted.cred.util.py, which has been deprecated since
+ Twisted 8.3. (#4107)
+ - twisted.python.text.docstringLStrip was deprecated. (#4328)
+ - The module attributes `LENGTH`, `DATA`, `COMMA`, and `NUMBER` of
+ twisted.protocols.basic (previously used by `NetstringReceiver`)
+ are now deprecated. (#4541)
+ - twisted.protocols.basic.SafeNetstringReceiver, deprecated since
+ 2001 (before Twisted 2.0), was removed. (#4546)
+ - twisted.python.threadable.whenThreaded, deprecated since Twisted
+ 2.2.0, has been removed. (#4550)
+ - twisted.python.timeoutqueue, deprecated since Twisted 8.0, has been
+ removed. (#4551)
+ - iocpreactor transports can no longer be pickled. (#4617)
+
+Other
+-----
+ - #4300, #4475, #4477, #4504, #4556, #4562, #4564, #4569, #4608,
+ #4616, #4617, #4626, #4630, #4650, #4705
+
+
+Twisted Core 10.1.0 (2010-06-27)
+================================
+
+Features
+--------
+ - Add linux inotify support, allowing monitoring of file system
+ events. (#972)
+ - Deferreds now support cancellation. (#990)
+ - Added new "endpoint" interfaces in twisted.internet.interfaces,
+ which abstractly describe stream transport endpoints which can be
+ listened on or connected to. Implementations for TCP and SSL
+ clients and servers are present in twisted.internet.endpoints.
+ Notably, client endpoints' connect() methods return cancellable
+ Deferreds, so code written to use them can bypass the awkward
+ "ClientFactory.clientConnectionFailed" and
+ "Connector.stopConnecting" methods, and handle errbacks from or
+ cancel the returned deferred, respectively. (#1442)
+ - twisted.protocols.amp.Integer's documentation now clarifies that
+ integers of arbitrary size are supported and that the wire format
+ is a base-10 representation. (#2650)
+ - twisted.protocols.amp now includes support for transferring
+ timestamps (amp.DateTime) and decimal values (amp.Decimal). (#2651)
+ - twisted.protocol.ftp.IWriteFile now has a close() method, which can
+ return a Deferred. Previously a STOR command would finish
+ immediately upon the receipt of the last byte of the uploaded file.
+ With close(), the backend can delay the finish until it has
+ performed some other slow action (like storing the data to a
+ virtual filesystem). (#3462)
+ - FilePath now calls os.stat() only when new status information is
+ required, rather than immediately when anything changes. For some
+ applications this may result in fewer stat() calls. Additionally,
+ FilePath has a new method, 'changed', which applications may use to
+ indicate that the FilePath may have been changed on disk and
+ therefore the next status information request must fetch a new
+ stat result. This is useful if external systems, such as C
+ libraries, may have changed files that Twisted applications are
+ referencing via a FilePath. (#4130)
+ - Documentation improvements are now summarized in the NEWS file.
+ (#4224)
+ - twisted.internet.task.deferLater now returns a cancellable
+ Deferred. (#4318)
+ - The connect methods of twisted.internet.protocol.ClientCreator now
+ return cancellable Deferreds. (#4329)
+ - twisted.spread.pb now has documentation covering some of its
+ limitations. (#4402)
+ - twisted.spread.jelly now supports jellying and unjellying classes
+ defined with slots if they also implement __getstate__ and
+ __setstate__. (#4430)
+ - twisted.protocols.amp.ListOf arguments can now be specified as
+ optional. (#4474)
+
+Bugfixes
+--------
+ - On POSIX platforms, reactors now support child processes in a way
+ which doesn't cause other syscalls to sometimes fail with EINTR (if
+ running on Python 2.6 or if Twisted's extension modules have been
+ built). (#733)
+ - Substrings are escaped before being passed to a regular expression
+ for searching to ensure that they don't get interpreted as part of
+ the expression. (#1893)
+ - twisted.internet.stdio now supports stdout being redirected to a
+ normal file (except when using epollreactor). (#2259)
+ - (#2367)
+ - The tap2rpm script now works with modern versions of RPM. (#3292)
+ - twisted.python.modules.walkModules will now handle packages
+ explicitly precluded from importing by a None placed in
+ sys.modules. (#3419)
+ - ConnectedDatagramPort now uses stopListening when a connection
+ fails instead of the deprecated loseConnection. (#3425)
+ - twisted.python.filepath.FilePath.setContent is now safe for
+ multiple processes to use concurrently. (#3694)
+ - The mode argument to the methods of
+ twisted.internet.interfaces.IReactorUNIX is no longer deprecated.
+ (#4078)
+ - Do not include blacklisted projects when generating NEWS. (#4190)
+ - When generating NEWS for a project that had no significant changes,
+ include a section for that project and say that there were no
+ interesting changes. (#4191)
+ - Redundant 'b' mode is no longer passed to calls to FilePath.open
+ and FilePath.open itself now corrects the mode when multiple 'b'
+ characters are present, ensuring only one instance of 'b' is
+ provided, as a workaround for http://bugs.python.org/issue7686.
+ (#4207)
+ - HTML tags inside <pre> tags in the code snippets are now escaped.
+ (#4336)
+ - twisted.protocols.amp.CommandLocator now allows subclasses to
+ override responders inherited from base classes. (#4343)
+ - Fix a bunch of small but important defects in the INSTALL, README
+ and so forth. (#4346)
+ - The poll, epoll, glib2, and gtk2 reactors now all support half-
+ close in the twisted.internet.stdio.StandardIO transport. (#4352)
+ - twisted.application.internet no longer generates an extra and
+ invalid entry in its __all__ list for the nonexistent
+ MulticastClient. (#4373)
+ - Choosing a reactor documentation now says that only the select-
+ based reactor is a truly cross-platform reactor. (#4384)
+ - twisted.python.filepath.FilePath now no longer leaves files open,
+ to be closed by the garbage collector, when an exception is raised
+ in the implementation of setContent, getContent, or copyTo. (#4400)
+ - twisted.test.proto_helpers.StringTransport's getHost and getPeer
+ methods now return IPv4Address instances by default. (#4401)
+ - twisted.protocols.amp.BinaryBoxProtocol will no longer deliver an
+ empty string to a switched-to protocol's dataReceived method when
+ the BinaryBoxProtocol's buffer happened to be empty at the time of
+ the protocol switch. (#4405)
+ - IReactorUNIX.listenUNIX implementations now support abstract
+ namespace sockets on Linux. (#4421)
+ - Files opened with FilePath.create() (and therefore also files
+ opened via FilePath.open() on a path with alwaysCreate=True) will
+ now be opened in binary mode as advertised, so that they will
+ behave portably across platforms. (#4453)
+ - The subunit reporter now correctly reports import errors as errors,
+ rather than by crashing with an unrelated error. (#4496)
+
+Improved Documentation
+----------------------
+ - The finger tutorial example which introduces services now avoids
+ double-starting the loop to re-read its users file. (#4420)
+ - twisted.internet.defer.Deferred.callback's docstring now mentions
+ the implicit chaining feature. (#4439)
+ - doc/core/howto/listing/pb/chatclient.py can now actually send a
+ group message. (#4459)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.interfaces.IReactorArbitrary,
+ twisted.application.internet.GenericServer, and
+ twisted.application.internet.GenericClient are now deprecated.
+ (#367)
+ - twisted.internet.gtkreactor is now deprecated. (#2833)
+ - twisted.trial.util.findObject has been deprecated. (#3108)
+ - twisted.python.threadpool.ThreadSafeList is deprecated and Jython
+ platform detection in Twisted core removed (#3725)
+ - twisted.internet.interfaces.IUDPConnectedTransport has been removed
+ (deprecated since Twisted 9.0). (#4077)
+ - Removed twisted.application.app.runWithProfiler, which has been
+ deprecated since Twisted 8.0. (#4090)
+ - Removed twisted.application.app.runWithHotshot, which has been
+ deprecated since Twisted 8.0. (#4091)
+ - Removed twisted.application.app.ApplicationRunner.startLogging,
+ which has been deprecated (doesn't say since when), as well as
+ support for the legacy
+ twisted.application.app.ApplicationRunner.getLogObserver method.
+ (#4092)
+ - twisted.application.app.reportProfile has been removed. (#4093)
+ - twisted.application.app.getLogFile has been removed. (#4094)
+ - Removed twisted.cred.util.py, which has been deprecated since
+ Twisted 8.3. (#4107)
+ - twisted.python.util.dsu is now deprecated. (#4339)
+ - In twisted.trial.util: FailureError, DirtyReactorWarning,
+ DirtyReactorError, and PendingTimedCallsError, which have all been
+ deprecated since Twisted 8.0, have been removed. (#4505)
+
+Other
+-----
+ - #1363, #1742, #3170, #3359, #3431, #3738, #4088, #4206, #4221,
+ #4239, #4257, #4272, #4274, #4287, #4291, #4293, #4309, #4316,
+ #4319, #4324, #4332, #4335, #4348, #4358, #4394, #4399, #4409,
+ #4418, #4443, #4449, #4479, #4485, #4486, #4497
+
+
+Twisted Core 10.0.0 (2010-03-01)
+================================
+
+Features
+--------
+ - The twistd man page now has a SIGNALS section. (#689)
+
+ - reactor.spawnProcess now will not emit a PotentialZombieWarning
+ when called before reactor.run, and there will be no potential for
+ zombie processes in this case. (#2078)
+
+ - High-throughput applications based on Perspective Broker should now
+ run noticably faster thanks to the use of a more efficient decoding
+ function in Twisted Spread. (#2310)
+
+ - Documentation for trac-post-commit-hook functionality in svn-dev
+ policy. (#3867)
+
+ - twisted.protocols.socks.SOCKSv4 now supports the SOCKSv4a protocol.
+ (#3886)
+
+ - Trial can now output test results according to the subunit
+ protocol, as long as Subunit is installed (see
+ https://launchpad.net/subunit). (#4004)
+
+ - twisted.protocols.amp now provides a ListOf argument type which can
+ be composed with some other argument types to create a zero or more
+ element sequence of that type. (#4116)
+
+ - If returnValue is invoked outside of a function decorated with
+ @inlineCallbacks, but causes a function thusly decorated to exit, a
+ DeprecationWarning will be emitted explaining this potentially
+ confusing behavior. In a future release, this will cause an
+ exception. (#4157)
+
+ - twisted.python.logfile.BaseLogFile now has a reopen method allowing
+ you to use an external logrotate mechanism. (#4255)
+
+Bugfixes
+--------
+ - FTP.ftp_NLST now handles requests on invalid paths in a way
+ consistent with RFC 959. (#1342)
+
+ - twisted.python.util.initgroups now calls the low-level C initgroups
+ by default if available: the python version can create lots of I/O
+ with certain authentication setup to retrieve all the necessary
+ information. (#3226)
+
+ - startLogging now does nothing on subsequent invocations, thus
+ fixing a terrible infinite recursion bug that's only on edge case.
+ (#3289)
+
+ - Stringify non-string data to NetstringReceiver.sendString before
+ calculating the length so that the calculated length is equal to
+ the actual length of the transported data. (#3299)
+
+ - twisted.python.win32.cmdLineQuote now correctly quotes empty
+ strings arguments (#3876)
+
+ - Change the behavior of the Gtk2Reactor to register only one source
+ watch for each file descriptor, instead of one for reading and one
+ for writing. In particular, it fixes a bug with Glib under Windows
+ where we failed to notify when a client is connected. (#3925)
+
+ - Twisted Trial no longer crashes if it can't remove an old
+ _trial_temp directory. (#4020)
+
+ - The optional _c_urlarg extension now handles unquote("") correctly
+ on platforms where malloc(0) returns NULL, such as AIX. It also
+ compiles with less warnings. (#4142)
+
+ - On POSIX, child processes created with reactor.spawnProcess will no
+ longer automatically ignore the signals which the parent process
+ has set to be ignored. (#4199)
+
+ - All SOCKSv4a tests now use a dummy reactor with a deterministic
+ resolve method. (#4275)
+
+ - Prevent extraneous server, date and content-type headers in proxy
+ responses. (#4277)
+
+Deprecations and Removals
+-------------------------
+ - twisted.internet.error.PotentialZombieWarning is now deprecated.
+ (#2078)
+
+ - twisted.test.time_helpers is now deprecated. (#3719)
+
+ - The deprecated connectUDP method of IReactorUDP has now been
+ removed. (#4075)
+
+ - twisted.trial.unittest.TestCase now ignores the previously
+ deprecated setUpClass and tearDownClass methods. (#4175)
+
+Other
+-----
+ - #917, #2406, #2481, #2608, #2689, #2884, #3056, #3082, #3199,
+ #3480, #3592, #3718, #3935, #4066, #4083, #4154, #4166, #4169,
+ #4176, #4183, #4186, #4188, #4189, #4194, #4201, #4204, #4209,
+ #4222, #4234, #4235, #4238, #4240, #4245, #4251, #4264, #4268,
+ #4269, #4282
+
+
+Twisted Core 9.0.0 (2009-11-24)
+===============================
+
+Features
+--------
+ - LineReceiver.clearLineBuffer now returns the bytes that it cleared (#3573)
+ - twisted.protocols.amp now raises InvalidSignature when bad arguments are
+ passed to Command.makeArguments (#2808)
+ - IArgumentType was added to represent an existing but previously unspecified
+ interface in amp (#3468)
+ - Obscure python tricks have been removed from the finger tutorials (#2110)
+ - The digest auth implementations in twisted.web and twisted.protocolos.sip
+ have been merged together in twisted.cred (#3575)
+ - FilePath and ZipPath now has a parents() method which iterates up all of its
+ parents (#3588)
+ - reactors which support threads now have a getThreadPool method (#3591)
+ - The MemCache client implementation now allows arguments to the "stats"
+ command (#3661)
+ - The MemCache client now has a getMultiple method which allows fetching of
+ multiple values (#3171)
+ - twisted.spread.jelly can now unserialize some new-style classes (#2950)
+ - twisted.protocols.loopback.loopbackAsync now accepts a parameter to control
+ the data passed between client and server (#3820)
+ - The IOCP reactor now supports SSL (#593)
+ - Tasks in a twisted.internet.task.Cooperator can now be paused, resumed, and
+ cancelled (#2712)
+ - AmpList arguments can now be made optional (#3891)
+ - The syslog output observer now supports log levels (#3300)
+ - LoopingCall now supports reporting the number of intervals missed if it
+ isn't able to schedule calls fast enough (#3671)
+
+Fixes
+-----
+ - The deprecated md5 and sha modules are no longer used if the stdlib hashlib
+ module is available (#2763)
+ - An obscure deadlock involving waking up the reactor within signal handlers
+ in particular threads was fixed (#1997)
+ - The passivePortRange attribute of FTPFactory is now honored (#3593)
+ - TestCase.flushWarnings now flushes warnings even if they were produced by a
+ file that was renamed since it was byte compiled (#3598)
+ - Some internal file descriptors are now marked as close-on-exec, so these will
+ no longer be leaked to child processes (#3576)
+ - twisted.python.zipstream now correctly extracts the first file in a directory
+ as a file, and not an empty directory (#3625)
+ - proxyForInterface now returns classes which correctly *implement* interfaces
+ rather than *providing* them (#3646)
+ - SIP Via header parameters should now be correctly generated (#2194)
+ - The Deferred returned by stopListening would sometimes previously never fire
+ if an exception was raised by the underlying file descriptor's connectionLost
+ method. Now the Deferred will fire with a failure (#3654)
+ - The command-line tool "manhole" should now work with newer versions of pygtk
+ (#2464)
+ - When a DefaultOpenSSLContextFactory is instantiated with invalid parameters,
+ it will now raise an exception immediately instead of waiting for the first
+ connection (#3700)
+ - Twisted command line scripts should now work when installed in a virtualenv
+ (#3750)
+ - Trial will no longer delete temp directories which it did not create (#3481)
+ - Processes started on Windows should now be cleaned up properly in more cases
+ (#3893)
+ - Certain misbehaving importers will no longer cause twisted.python.modules
+ (and thus trial) to raise an exception, but rather issue a warning (#3913)
+ - MemCache client protocol methods will now fail when the transport has been
+ disconnected (#3643)
+ - In the AMP method callRemoteString, the requiresAnswer parameter is now
+ honored (#3999)
+ - Spawning a "script" (a file which starts with a #! line) on Windows running
+ Python 2.6 will now work instead of raising an exception about file mode
+ "ru" (#3567)
+ - FilePath's walk method now calls its "descend" parameter even on the first
+ level of children, instead of only on grandchildren. This allows for better
+ symlink cycle detection (#3911)
+ - Attempting to write unicode data to process pipes on Windows will no longer
+ result in arbitrarily encoded messages being written to the pipe, but instead
+ will immediately raise an error (#3930)
+ - The various twisted command line utilities will no longer print
+ ModuleType.__doc__ when Twisted was installed with setuptools (#4030)
+ - A Failure object will now be passed to connectionLost on stdio connections
+ on Windows, instead of an Exception object (#3922)
+
+Deprecations and Removals
+-------------------------
+ - twisted.persisted.marmalade was deleted after a long period of deprecation
+ (#876)
+ - Some remaining references to the long-gone plugins.tml system were removed
+ (#3246)
+ - SSLv2 is now disabled by default, but it can be re-enabled explicitly
+ (#3330)
+ - twisted.python.plugin has been removed (#1911)
+ - reactor.run will now raise a ReactorAlreadyRunning exception when it is
+ called reentrantly instead of warning a DeprecationWarning (#1785)
+ - twisted.spread.refpath is now deprecated because it is unmaintained,
+ untested, and has dubious value (#3723)
+ - The unused --quiet flag has been removed from the twistd command (#3003)
+
+Other
+-----
+ - #3545, #3490, #3544, #3537, #3455, #3315, #2281, #3564, #3570, #3571, #3486,
+ #3241, #3599, #3220, #1522, #3611, #3596, #3606, #3609, #3602, #3637, #3647,
+ #3632, #3675, #3673, #3686, #2217, #3685, #3688, #2456, #506, #3635, #2153,
+ #3581, #3708, #3714, #3717, #3698, #3747, #3704, #3707, #3713, #3720, #3692,
+ #3376, #3652, #3695, #3735, #3786, #3783, #3699, #3340, #3810, #3822, #3817,
+ #3791, #3859, #2459, #3677, #3883, #3894, #3861, #3822, #3852, #3875, #2722,
+ #3768, #3914, #3885, #2719, #3905, #3942, #2820, #3990, #3954, #1627, #2326,
+ #2972, #3253, #3937, #4058, #1200, #3639, #4079, #4063, #4050
+
+
+Core 8.2.0 (2008-12-16)
+=======================
+
+Features
+--------
+ - Reactors are slowly but surely becoming more isolated, thus improving
+ testability (#3198)
+ - FilePath has gained a realpath method, and FilePath.walk no longer infinitely
+ recurses in the case of a symlink causing a self-recursing filesystem tree
+ (#3098)
+ - FilePath's moveTo and copyTo methods now have an option to disable following
+ of symlinks (#3105)
+ - Private APIs are now included in the API documentation (#3268)
+ - hotshot is now the default profiler for the twistd --profile parameter and
+ using cProfile is now documented (#3355, #3356)
+ - Process protocols can now implement a processExited method, which is
+ distinct from processEnded in that it is called immediately when the child
+ has died, instead of waiting for all the file descriptors to be closed
+ (#1291)
+ - twistd now has a --umask option (#966, #3024)
+ - A new deferToThreadPool function exists in twisted.internet.threads (#2845)
+ - There is now an example of writing an FTP server in examples/ftpserver.py
+ (#1579)
+ - A new runAsEffectiveUser function has been added to twisted.python.util
+ (#2607)
+ - twisted.internet.utils.getProcessOutput now offers a mechanism for
+ waiting for the process to actually end, in the event of data received on
+ stderr (#3239)
+ - A fullyQualifiedName function has been added to twisted.python.reflect
+ (#3254)
+ - strports now defaults to managing access to a UNIX socket with a lock;
+ lockfile=0 can be included in the strports specifier to disable this
+ behavior (#2295)
+ - FTPClient now has a 'rename' method (#3335)
+ - FTPClient now has a 'makeDirectory' method (#3500)
+ - FTPClient now has a 'removeFile' method (#3491)
+ - flushWarnings, A new Trial method for testing warnings, has been added
+ (#3487, #3427, #3506)
+ - The log observer can now be configured in .tac files (#3534)
+
+Fixes
+-----
+ - TLS Session Tickets are now disabled by default, allowing connections to
+ certain servers which hang when an empty session ticket is received (like
+ GTalk) (#3463)
+ - twisted.enterprise.adbapi.ConnectionPool's noisy attribute now defaults to
+ False, as documented (#1806)
+ - Error handling and logging in adbapi is now much improved (#3244)
+ - TCP listeners can now be restarted (#2913)
+ - Doctests can now be rerun with trial's --until-failure option (#2713)
+ - Some memory leaks have been fixed in trial's --until-failure
+ implementation (#3119, #3269)
+ - Trial's summary reporter now prints correct runtime information and handles
+ the case of 0 tests (#3184)
+ - Trial and any other user of the 'namedAny' function now has better error
+ reporting in the case of invalid module names (#3259)
+ - Multiple instances of trial can now run in parallel in the same directory
+ by creating _trial_temp directories with an incremental suffix (#2338)
+ - Trial's failUnlessWarns method now works on Python 2.6 (#3223)
+ - twisted.python.log now hooks into the warnings system in a way compatible
+ with Python 2.6 (#3211)
+ - The GTK2 reactor is now better supported on Windows, but still not passing
+ the entire test suite (#3203)
+ - low-level failure handling in spawnProcess has been improved and no longer
+ leaks file descriptors (#2305, #1410)
+ - Perspective Broker avatars now have their logout functions called in more
+ cases (#392)
+ - Log observers which raise exceptions are no longer removed (#1069)
+ - transport.getPeer now always includes an IP address in the Address returned
+ instead of a hostname (#3059)
+ - Functions in twisted.internet.utils which spawn processes now avoid calling
+ chdir in the case where no working directory is passed, to avoid some
+ obscure permission errors (#3159)
+ - twisted.spread.publish.Publishable no longer corrupts line endings on
+ Windows (#2327)
+ - SelectReactor now properly detects when a TLS/TCP connection has been
+ disconnected (#3218)
+ - twisted.python.lockfile no longer raises an EEXIST OSError and is much
+ better supported on Windows (#3367)
+ - When ITLSTransport.startTLS is called while there is data in the write
+ buffer, TLS negotiation will now be delayed instead of the method raising
+ an exception (#686)
+ - The userAnonymous argument to FTPFactory is now honored (#3390)
+ - twisted.python.modules no longer tries to "fix" sys.modules after an import
+ error, which was just causing problems (#3388)
+ - setup.py no longer attempts to build extension modules when run with Jython
+ (#3410)
+ - AMP boxes can now be sent in IBoxReceiver.startReceivingBoxes (#3477)
+ - AMP connections are closed as soon as a key length larger than 255 is
+ received (#3478)
+ - Log events with timezone offsets between -1 and -59 minutes are now
+ correctly reported as negative (#3515)
+
+Deprecations and Removals
+-------------------------
+ - Trial's setUpClass and tearDownClass methods are now deprecated (#2903)
+ - problemsFromTransport has been removed in favor of the argument passed to
+ connectionLost (#2874)
+ - The mode parameter to methods of IReactorUNIX and IReactorUNIXDatagram are
+ deprecated in favor of applications taking other security precautions, since
+ the mode of a Unix socket is often not respected (#1068)
+ - Index access on instances of twisted.internet.defer.FirstError has been
+ removed in favor of the subFailure attribute (#3298)
+ - The 'changeDirectory' method of FTPClient has been deprecated in favor of
+ the 'cwd' method (#3491)
+
+Other
+-----
+
+ - #3202, #2869, #3225, #2955, #3237, #3196, #2355, #2881, #3054, #2374, #2918,
+ #3210, #3052, #3267, #3288, #2985, #3295, #3297, #2512, #3302, #1222, #2631,
+ #3306, #3116, #3215, #1489, #3319, #3320, #3321, #1255, #2169, #3182, #3323,
+ #3301, #3318, #3029, #3338, #3346, #1144, #3173, #3165, #685, #3357, #2582,
+ #3370, #2438, #1253, #637, #1971, #2208, #979, #1790, #1888, #1882, #1793,
+ #754, #1890, #1931, #1246, #1025, #3177, #2496, #2567, #3400, #2213, #2027,
+ #3415, #1262, #3422, #2500, #3414, #3045, #3111, #2974, #2947, #3222, #2878,
+ #3402, #2909, #3423, #1328, #1852, #3382, #3393, #2029, #3489, #1853, #2026,
+ #2375, #3502, #3482, #3504, #3505, #3507, #2605, #3519, #3520, #3121, #3484,
+ #3439, #3216, #3511, #3524, #3521, #3197, #2486, #2449, #2748, #3381, #3236,
+ #671
+
+
+8.1.0 (2008-05-18)
+==================
+
+Features
+--------
+
+ - twisted.internet.error.ConnectionClosed is a new exception which is the
+ superclass of ConnectionLost and ConnectionDone (#3137)
+ - Trial's CPU and memory performance should be better now (#3034)
+ - twisted.python.filepath.FilePath now has a chmod method (#3124)
+
+Fixes
+-----
+
+ - Some reactor re-entrancy regressions were fixed (#3146, #3168)
+ - A regression was fixed whereby constructing a Failure for an exception and
+ traceback raised out of a Pyrex extension would fail (#3132)
+ - CopyableFailures in PB can again be created from CopiedFailures (#3174)
+ - FilePath.remove, when called on a FilePath representing a symlink to a
+ directory, no longer removes the contents of the targeted directory, and
+ instead removes the symlink (#3097)
+ - FilePath now has a linkTo method for creating new symlinks (#3122)
+ - The docstring for Trial's addCleanup method now correctly specifies when
+ cleanup functions are run (#3131)
+ - assertWarns now deals better with multiple identical warnings (#2904)
+ - Various windows installer bugs were fixed (#3115, #3144, #3150, #3151, #3164)
+ - API links in the howto documentation have been corrected (#3130)
+ - The Win32 Process transport object now has a pid attribute (#1836)
+ - A doc bug in the twistd plugin howto which would inevitably lead to
+ confusion was fixed (#3183)
+ - A regression breaking IOCP introduced after the last release was fixed
+ (#3200)
+
+
+Deprecations and Removals
+-------------------------
+
+ - mktap is now fully deprecated, and will emit DeprecationWarnings when used
+ (#3127)
+
+Other
+-----
+ - #3079, #3118, #3120, #3145, #3069, #3149, #3186, #3208, #2762
+
+
+8.0.1 (2008-03-26)
+==================
+
+Fixes
+-----
+ - README no longer refers to obsolete trial command line option
+ - twistd no longer causes a bizarre DeprecationWarning about mktap
+
+
+8.0.0 (2008-03-17)
+==================
+
+Features
+--------
+
+ - The IOCP reactor has had many changes and is now greatly improved
+ (#1760, #3055)
+ - The main Twisted distribution is now easy_installable (#1286, #3110)
+ - twistd can now profile with cProfile (#2469)
+ - twisted.internet.defer contains a DeferredFilesystemLock which gives a
+ Deferred interface to lock file acquisition (#2180)
+ - twisted.python.modules is a new system for representing and manipulating
+ module paths (i.e. sys.path) (#1951)
+ - twisted.internet.fdesc now contains a writeToFD function, along with other
+ minor fixes (#2419)
+ - twisted.python.usage now allows optional type enforcement (#739)
+ - The reactor now has a blockingCallFromThread method for non-reactor threads
+ to use to wait for a reactor-scheduled call to return a result (#1042, #3030)
+ - Exceptions raised inside of inlineCallbacks-using functions now have a
+ better chance of coming with a meaningful traceback (#2639, #2803)
+ - twisted.python.randbytes now contains code for generating secure random
+ bytes (#2685)
+ - The classes in twisted.application.internet now accept a reactor parameter
+ for specifying the reactor to use for underlying calls to allow for better
+ testability (#2937)
+ - LoopingCall now allows you to specify the reactor to use to schedule new
+ calls, allowing much better testing techniques (#2633, #2634)
+ - twisted.internet.task.deferLater is a new API for scheduling calls and
+ getting deferreds which are fired with their results (#1875)
+ - objgrep now knows how to search through deque objects (#2323)
+ - twisted.python.log now contains a Twisted log observer which can forward
+ messages to the Python logging system (#1351)
+ - Log files now include seconds in the timestamps (#867)
+ - It is now possible to limit the number of log files to create during log
+ rotation (#1095)
+ - The interface required by the log context system is now documented as
+ ILoggingContext, and abstract.FileDescriptor now declares that it implements
+ it (#1272)
+ - There is now an example cred checker that uses a database via adbapi (#460)
+ - The epoll reactor is now documented in the choosing-reactors howto (#2539)
+ - There were improvements to the client howto (#222)
+ - Int8Receiver was added (#2315)
+ - Various refactorings to AMP introduced better testability and public
+ interfaces (#2657, #2667, #2656, #2664, #2810)
+ - twisted.protocol.policies.TrafficLoggingFactory now has a resetCounter
+ method (#2757)
+ - The FTP client can be told which port range within which to bind passive
+ transfer ports (#1904)
+ - twisted.protocols.memcache contains a new asynchronous memcache client
+ (#2506, #2957)
+ - PB now supports anonymous login (#439, #2312)
+ - twisted.spread.jelly now supports decimal objects (#2920)
+ - twisted.spread.jelly now supports all forms of sets (#2958)
+ - There is now an interface describing the API that process protocols must
+ provide (#3020)
+ - Trial reporting to core unittest TestResult objects has been improved (#2495)
+ - Trial's TestCase now has an addCleanup method which allows easy setup of
+ tear-down code (#2610, #2899)
+ - Trial's TestCase now has an assertIsInstance method (#2749)
+ - Trial's memory footprint and speed are greatly improved (#2275)
+ - At the end of trial runs, "PASSED" and "FAILED" messages are now colorized
+ (#2856)
+ - Tests which leave global state around in the reactor will now fail in
+ trial. A new option, --unclean-warnings, will convert these errors back into
+ warnings (#2091)
+ - Trial now has a --without-module command line for testing code in an
+ environment that lacks a particular Python module (#1795)
+ - Error reporting of failed assertEquals assertions now has much nicer
+ formatting (#2893)
+ - Trial now has methods for monkey-patching (#2598)
+ - Trial now has an ITestCase (#2898, #1950)
+ - The trial reporter API now has a 'done' method which is called at the end of
+ a test run (#2883)
+ - TestCase now has an assertWarns method which allows testing that functions
+ emit warnings (#2626, #2703)
+ - There are now no string exceptions in the entire Twisted code base (#2063)
+ - There is now a system for specifying credentials checkers with a string
+ (#2570)
+
+Fixes
+-----
+
+ - Some tests which were asserting the value of stderr have been changed
+ because Python uncontrollably writes bytes to stderr (#2405)
+ - Log files handle time zones with DST better (#2404)
+ - Subprocesses using PTYs on OS X that are handled by Twisted will now be able
+ to more reliably write the final bytes before they exit, allowing Twisted
+ code to more reliably receive them (#2371, #2858)
+ - Trial unit test reporting has been improved (#1901)
+ - The kqueue reactor handles connection failures better (#2172)
+ - It's now possible to run "trial foo/bar/" without an exception: trailing
+ slashes no longer cause problems (#2005)
+ - cred portals now better deal with implementations of inherited interfaces
+ (#2523)
+ - FTP error handling has been improved (#1160, 1107)
+ - Trial behaves better with respect to file locking on Windows (#2482)
+ - The FTP server now gives a better error when STOR is attempted during an
+ anonymous session (#1575)
+ - Trial now behaves better with tests that use the reactor's threadpool (#1832)
+ - twisted.python.reload now behaves better with new-style objects (#2297)
+ - LogFile's defaultMode parameter is now better implemented, preventing
+ potential security exploits (#2586)
+ - A minor obscure leak in thread pools was corrected (#1134)
+ - twisted.internet.task.Clock now returns the correct DelayedCall from
+ callLater, instead of returning the one scheduled for the furthest in the
+ future (#2691)
+ - twisted.spread.util.FilePager no longer unnecessarily buffers data in
+ memory (#1843, 2321)
+ - Asking for twistd or trial to use an unavailable reactor no longer prints a
+ traceback (#2457)
+ - System event triggers have fewer obscure bugs (#2509)
+ - Plugin discovery code is much better behaved, allowing multiple
+ installations of a package with plugins (#2339, #2769)
+ - Process and PTYProcess have been merged and some minor bugs have been fixed
+ (#2341)
+ - The reactor has less global state (#2545)
+ - Failure can now correctly represent and format errors caused by string
+ exceptions (#2830)
+ - The epoll reactor now has better error handling which now avoids the bug
+ causing 100% CPU usage in some cases (#2809)
+ - Errors raised during trial setUp or tearDown methods are now handled better
+ (#2837)
+ - A problem when deferred callbacks add new callbacks to the deferred that
+ they are a callback of was fixed (#2849)
+ - Log messages that are emitted during connectionMade now have the protocol
+ prefix correctly set (#2813)
+ - The string representation of a TCP Server connection now contains the actual
+ port that it's bound to when it was configured to listen on port 0 (#2826)
+ - There is better reporting of error codes for TCP failures on Windows (#2425)
+ - Process spawning has been made slightly more robust by disabling garbage
+ collection temporarily immediately after forking so that finalizers cannot
+ be executed in an unexpected environment (#2483)
+ - namedAny now detects import errors better (#698)
+ - Many fixes and improvements to the twisted.python.zipstream module have
+ been made (#2996)
+ - FilePager no longer blows up on empty files (#3023)
+ - twisted.python.util.FancyEqMixin has been improved to cooperate with objects
+ of other types (#2944)
+ - twisted.python.FilePath.exists now restats to prevent incorrect result
+ (#2896)
+ - twisted.python.util.mergeFunctionMetadata now also merges the __module__
+ attribute (#3049)
+ - It is now possible to call transport.pauseProducing within connectionMade on
+ TCP transports without it being ignored (#1780)
+ - twisted.python.versions now understands new SVN metadata format for fetching
+ the SVN revision number (#3058)
+ - It's now possible to use reactor.callWhenRunning(reactor.stop) on gtk2 and
+ glib2 reactors (#3011)
+
+Deprecations and removals
+-------------------------
+ - twisted.python.timeoutqueue is now deprecated (#2536)
+ - twisted.enterprise.row and twisted.enterprise.reflector are now deprecated
+ (#2387)
+ - twisted.enterprise.util is now deprecated (#3022)
+ - The dispatch and dispatchWithCallback methods of ThreadPool are now
+ deprecated (#2684)
+ - Starting the same reactor multiple times is now deprecated (#1785)
+ - The visit method of various test classes in trial has been deprecated (#2897)
+ - The --report-profile option to twistd and twisted.python.dxprofile are
+ deprecated (#2908)
+ - The upDownError method of Trial reporters is deprecated (#2883)
+
+Other
+-----
+
+ - #2396, #2211, #1921, #2378, #2247, #1603, #2463, #2530, #2426, #2356, #2574,
+ - #1844, #2575, #2655, #2640, #2670, #2688, #2543, #2743, #2744, #2745, #2746,
+ - #2742, #2741, #1730, #2831, #2216, #1192, #2848, #2767, #1220, #2727, #2643,
+ - #2669, #2866, #2867, #1879, #2766, #2855, #2547, #2857, #2862, #1264, #2735,
+ - #942, #2885, #2739, #2901, #2928, #2954, #2906, #2925, #2942, #2894, #2793,
+ - #2761, #2977, #2968, #2895, #3000, #2990, #2919, #2969, #2921, #3005, #421,
+ - #3031, #2940, #1181, #2783, #1049, #3053, #2847, #2941, #2876, #2886, #3086,
+ - #3095, #3109
+
+
+2.5.0 (2006-12-29)
+==================
+
+Twisted 2.5.0 is a major feature release, with several interesting new
+developments and a great number of bug fixes. Some of the highlights
+follow.
+
+ * AMP, the Asynchronous Messaging Protocol, was introduced. AMP is
+ a protocol which provides request/response semantics over a
+ persistent connection in a very simple and extensible manner.
+
+ * An Epoll-based reactor was added, which can be used with twistd or
+ trial by passing "-r epoll" on the command line. This may improve
+ performance of certain high-traffic network applications.
+
+ * The 'twistd' command can now accept sub-commands which name an
+ application to run. For example, 'twistd web --path .' will start a
+ web server serving files out of the current directory. This
+ functionality is meant to replace the old way of doing things with
+ 'mktap' and 'twistd -f'.
+
+ * Python 2.5 is now supported. Previous releases of Twisted were
+ broken by changes in the release of Python 2.5.
+
+ * 'inlineCallbacks' was added, which allows taking advantage of the
+ new 'yield' expression syntax in Python 2.5 to avoid writing
+ callbacks for Deferreds.
+
+In addition to these changes, there are many other minor features and
+a large number of bug fixes.
+
+Features
+--------
+ - log.err can now take a second argument for specifying information
+ about an error (#1399)
+ - A time-simulating test helper class, twisted.internet.task.Clock,
+ was added (#1757)
+ - Trial docstring improvements were made (#1604, #2133)
+ - New SSL features were added to twisted.internet.ssl, such as client
+ validation (#302)
+ - Python 2.5 is now supported (#1867)
+ - Trial's assertFailure now provides more information on failure (#1869)
+ - Trial can now be run on tests within a zipfile (#1940)
+ - AMP, a new simple protocol for asynchronous messaging, was added (#1715)
+ - Trial's colorful reporter now works on win32 (#1646)
+ - Trial test modules may now dynamically construct TestSuites (#1638, #2165)
+ - twistd can now make use of plugins to run applications (#1922, #2013)
+ - Twisted now works with the latest (unreleased) zope.interface (#2160)
+ - An epoll-based reactor, epollreactor, was added. It is selectable
+ with the -r options to twistd and trial (#1953)
+ - twistd and trial now use the plugin system to find reactors which
+ can be selected (#719)
+ - twisted.internet.defer.inlineCallbacks was added. It takes
+ advantage of Python 2.5's generators to offer a way to deal with
+ Deferreds without callbacks (#2100)
+
+Fixes
+-----
+ - Traceback formatting in Trial was improved (#1454, #1610)
+ - twisted.python.filepath.FilePath.islink now actually returns True when
+ appropriate (#1773)
+ - twisted.plugin now no longer raises spurious errors (#926)
+ - twisted.pb Cacheables may now be new-style classes (#1324)
+ - FileDescriptor now deals with producers in a more
+ interface-compliant and robust manner (#2286, #811)
+ - "setup.py build" and other setup.py commands which don't actually
+ install the software now work (#1835)
+ - wxreactor has had various fixes (#1235, #1574, #1688)
+
+Deprecations and Removals
+-------------------------
+ - The old twisted.cred API (Perspectives, Identities and such) was
+ removed (#1440)
+ - twisted.spread.newjelly was removed (#1831)
+ - Various deprecated things in twisted.python.components were
+ removed: Interface, MetaInterface, getAdapterClass, and
+ getAdapterClassWithInheritance (#1636)
+ - twisted.enterprise.xmlreflector was removed (#661)
+ - mktap is slowly on its way out, now that twistd supports plugins. It
+ is not yet officially deprecated (#2013)
+ - tkmktap was removed, because it wasn't working anyway (#2020)
+ - reactor.iterate calls made inside of a Trial test case are
+ deprecated (#2090)
+ - twisted.internet.qtreactor was removed: It has been moved to a
+ separate project. See http://twistedmatrix.com/trac/wiki/QTReactor
+ (#2130, #2137)
+ - threadedselectreactor is now not a directly usable reactor; it is
+ only meant to help in writing other reactors (#2126)
+ - twisted.python.reflect.funcinfo is deprecated (#2079)
+ - twisted.spread.sturdy, which was already completely broken, was
+ removed (#2299)
+
+
+Other
+-----
+The following changes are minor or closely related to other changes.
+
+ - #1783, #1786, #1788, #1648, #1734, #1609, #1800, #1818,
+ #1629, #1829, #491, #1816, #1824, #1855, #1797, #1637, #1371,
+ #1892, #1887, #1897, #1563, #1741, #1943, #1952, #1276,
+ #1837, #1726, #1963, #1965, #1973, #1976, #1991, #1936, #1113,
+ #630, #2002, #2040, #2044, #1617, #2045, #2055, #2056, #2022,
+ #2052, #1552, #1999, #1507, #2054, #1970, #1968, #662, #1910,
+ #1694, #1999, #1409, #2150, #2127, #2155, #1983, #2014, #2222,
+ #1067, #2136, #2065, #1430, #2173, #2212, #1871, #2147, #1199,
+ #2273, #428, #992, #815, #2024, #2292, #2125, #2139, #2291, #2174,
+ #2306, #2228, #2309, #2319, #2317, #2313, #2154, #1985, #1201
+
+
+2.4.0 (2006-05-21)
+==================
+
+Features
+--------
+ - twisted.internet.task.Cooperator (Added along with #1701).
+
+Fixes
+-----
+ - Errors in UDP protocols no longer unbind the UDP port (#1695).
+ - Misc: #1717, #1705, #1563, #1719, #1721, #1722, #1728.
+
+
+2.3.0 (2006-05-14)
+==================
+
+Features
+--------
+ - twisted-dev-mode's F9 now uses trial's --testmodule feature, rather than
+ trying to guess what tests to run. This will break files using the "-x"
+ test-case-name hack (just use a comma separated list instead).
+ - API Documentation improvements.
+ - A new Producer/Consumer guide (#53)
+ - Better-defined error behavior in IReactorMulticast (#1578)
+ - IOCP Multicast support (#1500)
+ - Improved STDIO support on Windows. (#1553)
+ - LoopingCall supports Deferreds such that it will wait until a
+ Deferred has fired before rescheduling the next call (#1487)
+ - Added twisted.python.versions.Version, a structured representation
+ of Version information, including support for SVN revision numbers
+ (#1663)
+
+Fixes
+-----
+
+ - Many trial fixes, as usual
+ - All API documentation is now correctly formatted as epytext (#1545)
+ - twisted.python.filepath.FilePath.__repr__ is safer.
+ - Fix trial's "until-failure" mode. (#1453)
+ - deferredGenerator now no longer causes handled exceptions (or
+ results) to propagate to the resulting Deferred (#1709).
+ - Misc: #1483, #1495, #1503, #1532, #1539, #1559, #1509, #1538,
+ #1571, #1331, #1561, #737, #1562, #1573, #1594, #1607, #1407, #1615,
+ #1645, #1634, #1620, #1664, #1666, #1650, #1670, #1675, #1692, #1710,
+ #1668.
+
+Deprecations
+------------
+
+ - Removal of already-deprecated trial APIs: the assertions module,
+ util.deferredResult, util.deferredError, util.fireWhenDoneFunc,
+ util.spinUntil, util.spinWhile, util.extract_tb,
+ util.format_exception, util.suppress_warnings, unittest.wait,
+ util.wait
+ - The backwards compatibility layer of twisted.python.components
+ (e.g., backwardsCompatImplements, fixClassImplements, etc) has been
+ disabled. The functions still exist, but do nothing as to not break
+ user code outright (#1511)
+ - Deprecate the usage of the 'default' argument as a keyword argument
+ in Interface.__call__. Passing a second positional argument to
+ specify the default return value of the adaptation is still
+ supported.
+
+
+2.2.0 (2006-02-12)
+==================
+
+Features
+--------
+ - Twisted no longer works with Python 2.2
+ - FTP server supports more clients
+ - Process support on Windows
+ - twisted.internet.stdio improved (including Windows support!)
+ - Trial:
+ - Continued Trial refactoring
+ - Default trial reporter is verbose black&white when color isn't supported
+ - Deferreds returned in trial tests that don't fire before the
+ unittest timeout now have their errback fired with a TimeoutError
+ - raising SkipTest in setUp and setUpClass skips tests
+ - Test suites are failed if there are import errors
+
+Fixes
+-----
+ - iocpreactor fixes
+ - Threadpool fixes
+ - Fixed infinite loops in datagramReceived edge cases
+ - Issues resolved: 654, 773, 998, 1005, 1008, 1116, 1123, 1198, 1221,
+ 1232, 1233, 1236, 1240, 1244, 1258, 1263, 1265, 1266, 1271, 1275,
+ 1293, 1294, 1298, 1308, 1316, 1317, 1321, 1341, 1344, 1353, 1359,
+ 1372, 1374, 1377, 1379, 1380, 1385, 1388, 1389, 1413, 1422, 1426,
+ 1434, 1435, 1448, 1449, 1456
+
+Deprecations
+------------
+ - Trial:
+ - spinWhile and spinUntil
+ - util.wait
+ - extract_tb and format_exception
+ - util.suppressWarnings
+ - runReactor is gone
+
+
+2.1.0 (2005-11-06)
+==================
+
+Features
+--------
+ - threadedselectreactor, a reactor which potentially makes
+ integration with foreign event loops much simpler.
+ - major improvements to twisted.conch.insults, including many new widgets.
+ - adbapi ConnectionPools now have 'runWithConnection' which is
+ similar to runInteraction but gives you a connection object instead of
+ a transaction. [975]
+ - __file__ is now usable in tac files
+ - twisted.cred.pamauth now contains a PAM checker (moved from twisted.conch)
+ - twisted.protocols.policies.LimitTotalConnectionsFactory now exists,
+ which does as the name suggests
+ - twisted.protocols.ident now uses /proc/net/tcp on Linux [233]
+ - trial now recurses packages by default (a la the old -R parameter)
+ - (PB) Calling a remote method that doesn't exist now raises
+ NoSuchMethod instead of AttributeError.
+
+Fixes
+-----
+ - FTP client and server improvements
+ - Trial improvements: The code is now much simpler, and more stable.
+ - twisted.protocols.basic.FileSender now works with empty files
+ - Twisted should now be much more usable on Pythons without thread support.
+ - minor improvements to process code in win32eventreactor
+ - twistd -y (--python) now implies -o (--nosave). [539]
+ - improved lockfile handling especially with respect to unix sockets.
+ - deferredGenerator now no longer overuses the stack, which sometimes
+ caused stack overflows.
+ - Failure.raiseException now at least always raises the correct Exception.
+ - minor improvements to serialport code
+
+Deprecations
+------------
+ - twisted.python.componts.getAdapter. Use IFoo(o) instead.
+ - Adapter persistence (IFoo(x, persist=True)). Just don't use it.
+ - log.debug. It was equivalent to log.msg(), just use that.
+ - twisted.protocols.telnet. twisted.conch.telnet replaces it.
+ - Setting a trial reporter using a flag to 'trial'. Instead of 'trial
+ --bwverbose', for example, use 'trial --reporter=bwverbose'.
+ - trial --coverage will become a flag in Twisted 2.2.
+ - passing a fully-qualified python name to --reporter is
+ deprecated. Pass only names of Reporter plugins.
+ - trial --psyco.
+ - trial -R (--recurse) is now the default, so passing it is deprecated.
+ - trial --reporter-args. Use the plugin system to do this sort of thing.
+ - trial.assertions.assertionMethod and trial.unittest.assertionMethod
+ are both deprecated. Use instance methods on TestCases instead.
+ - trial's deferredResult, deferredError, and wait functions. Return
+ Deferreds from your test methods instead of using them.
+ - Raising unittest.SkipTest with no arguments. Give a reason for your skip.
+ - The Failure returned from a gatherResults and DeferredList is now
+ of type FirstError instead of a tuple of (Exception, index). It
+ supports a firstError[idx] syntax but that is deprecated. Use
+ firstError.subFailure and firstError.index instead.
+ - whenThreaded now simply calls the passed function synchronously.
+
+2.0.1 (2005-05-09)
+===================
+Minor bug fix release.
+
+SVN rev (file) - [bug number] description
+-----------------------------------------
+13307 (twisted/topfiles/README) - Mention support for python 2.4, too
+13324 (twisted/internet/defer.py) - [947] Fix DeferredQueue backlog/size limit.
+13354 (twisted/plugins/__init__.py) - Correct maintainer address.
+13355 (twisted/test/test_defer.py) - improvements to DeferredQueue test case
+13387 (setup.py) - add news to list of subprojects to install
+13332 (twisted/internet/posixbase.py) - Fix spelling error
+13366 (twisted/internet/qtreactor.py) - [957] [954] reactor.iterate fixes
+13368 (twisted/test/test_internet.py) - Fix DelayedCall test case
+13422 (twisted/internet/posixbase.py) - Remove log from _Win32Waker creation.
+13437 (twisted/plugin.py) - [958] Only write cache if there were changes.
+13666 (twisted/internet/gtkreactor.py,gtk2reactor.py) - Don't run callbacks
+ until the reactor is actually up and running
+13748 (twisted/internet/gtk2reactor.py) - [552] [994] Initialize threading properly.
+
+
+2.0.0 (2005-03-25)
+==================
+
+Major new features
+------------------
+ - Replaced home-grown components system with zope.interface.
+ - Split Twisted into multiple pieces.
+ - Relicensed: Now under the MIT license, rather than LGPL.
+ - Python 2.4 compatibility fixes
+ - Major efficiency improvements in TCP buffering algorithm.
+ - Major efficiency improvements in reactor.callLater/DelayedCall.
+ - Half-close support for TCP/SSL. (loseWriteConnection).
+
+Miscellaneous features/fixes
+----------------------------
+ - New plugin system: twisted.plugin
+ - Better debugging support. Control-C will break you into PDB.
+ - The twistd command has --uid --gid command line arguments.
+ - *Incompatibility: mktap defaults to not change UID/GID, instead of saving
+ the invoking user's UID/GID.
+ - Removed some functions that were deprecated since Twisted 1.0.
+ - ZSH tab-completion for twisted commands.
+
+ - More correct daemonization in twistd.
+ - twisted.python.log: do not close the log because of invalid format string.
+ - Disabled automatic import of cBanana.
+ - Boolean support for twisted.persisted.marmalade.
+ - Refactor of plugin and application HOWTO documentation
+ - Async HOWTO expanded greatly.
+ - twisted.python.usage outputs the actual defaults, not passed in values.
+
+twisted.trial
+-------------
+ - Rewritten, a bunch of bugs fixed, a few more added.
+
+twisted.internet
+----------------
+ - Multi-listen UDP multicast support
+ - protocol.ClientCreator has a connectSSL.
+ - defer.deferredGenerator: allows you to write Deferred code w/o callbacks.
+ - Deferred.setTimeout is now deprecated.
+ - New defer.DeferredLock/DeferredSemaphore/DeferredQueue.
+ - Add utils.getProcessOutputAndValue to get stdout/err/value.
+
+ - Default DNS resolver is now non-blocking.
+ - Increased default TCP accept backlog from 5 to 50.
+ - Make buffering large amounts of TCP data work on Windows.
+ - Fixed SSL disconnect to not wait for remote host. Fixes issue with firefox.
+ - Separate state for Deferred finalization so that GC-loops preventing
+ finalization don't occur.
+ - Many Process bugfixes
+ - Processes spawned on windows can successfully use sockets
+ - gtk2reactor can optionally use glib event loop instead of gtk
+ - gtk2reactor notifies gobject to initialize thread support
+ - Fix registering a streaming producer on a transport.
+ - Close client sockets explicitly after failed connections.
+ - ReconnectingClientFactory now continues attempting to reconnect after all
+ errors, not just those which are not UserErrors.
+
+twisted.protocols
+-----------------
+ - Portforward doesn't start reading from a client until a connection is made.
+ - Bugfixes in twisted.protocols.loopback
+ - Improve speed of twisted.protocols.LineReceiver.
+ - LineReceiver implements IProducer. (stop/pause/resumeProducing)
+ - SOCKSv4 properly closes connections
+
+twisted.enterprise
+------------------
+ - Add "new connection" callback to adbapi.ConnectionPool to allow for
+ custom db connection setup (cp_openfun)
+ - adbapi.ConnectionPool automatic reconnection support
+ - Don't log exceptions extraneously
+
+
+1.3.0 (2004-05-14)
+==================
+
+- Address objects for IPv4 and Unix addresses throughout twisted.internet.
+- Improved connected UDP APIs.
+- Refactored SSH client support.
+- Initial implementation of Windows I/O Completion Ports event loop.
+- Bug fixes and feature enhancements.
+- Nevow support for Lore (so your Lore documents can use Nevow directives).
+- This is the last release before Twisted begins splitting up.
diff --git a/twisted/topfiles/README b/twisted/topfiles/README
new file mode 100644
index 0000000..cd7c636
--- /dev/null
+++ b/twisted/topfiles/README
@@ -0,0 +1,14 @@
+Twisted Core 12.1.0
+===================
+
+Twisted Core makes up the core parts of Twisted, including:
+
+ * Networking support (twisted.internet)
+ * Trial, the unit testing framework (twisted.trial)
+ * AMP, the Asynchronous Messaging Protocol (twisted.protocols.amp)
+ * Twisted Spread, a remote object system (twisted.spread)
+ * Utility code (twisted.python)
+ * Basic abstractions that multiple subprojects use
+ (twisted.cred, twisted.application, twisted.plugin)
+ * Database connectivity support (twisted.enterprise)
+ * A few basic protocols and protocol abstractions (twisted.protocols)
diff --git a/twisted/topfiles/setup.py b/twisted/topfiles/setup.py
new file mode 100644
index 0000000..6cb667d
--- /dev/null
+++ b/twisted/topfiles/setup.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Distutils installer for Twisted.
+"""
+
+import os
+import sys
+
+if sys.version_info < (2, 5):
+ print >>sys.stderr, "You must use at least Python 2.5 for Twisted"
+ sys.exit(3)
+
+if os.path.exists('twisted'):
+ sys.path.insert(0, '.') # eek! need this to import twisted. sorry.
+from twisted import copyright
+from twisted.python.dist import setup, ConditionalExtension as Extension
+from twisted.python.dist import getPackages, getDataFiles, getScripts
+from twisted.python.dist import twisted_subprojects, _isCPython, _hasEpoll
+
+
+extensions = [
+ Extension("twisted.test.raiser",
+ ["twisted/test/raiser.c"],
+ condition=lambda _: _isCPython),
+
+ Extension("twisted.python._epoll",
+ ["twisted/python/_epoll.c"],
+ condition=lambda builder: (_isCPython and _hasEpoll(builder) and
+ sys.version_info[:2] < (2, 6))),
+
+ Extension("twisted.internet.iocpreactor.iocpsupport",
+ ["twisted/internet/iocpreactor/iocpsupport/iocpsupport.c",
+ "twisted/internet/iocpreactor/iocpsupport/winsock_pointers.c"],
+ libraries=["ws2_32"],
+ condition=lambda _: _isCPython and sys.platform == "win32"),
+
+ Extension("twisted.python._initgroups",
+ ["twisted/python/_initgroups.c"]),
+ Extension("twisted.python.sendmsg",
+ sources=["twisted/python/sendmsg.c"],
+ condition=lambda _: sys.platform != "win32"),
+ Extension("twisted.internet._sigchld",
+ ["twisted/internet/_sigchld.c"],
+ condition=lambda _: sys.platform != "win32"),
+]
+
+# Figure out which plugins to include: all plugins except subproject ones
+subProjectsPlugins = ['twisted_%s.py' % subProject
+ for subProject in twisted_subprojects]
+plugins = os.listdir(os.path.join(
+ os.path.dirname(os.path.abspath(copyright.__file__)), 'plugins'))
+plugins = [plugin[:-3] for plugin in plugins if plugin.endswith('.py') and
+ plugin not in subProjectsPlugins]
+
+
+
+setup_args = dict(
+ # metadata
+ name="Twisted Core",
+ version=copyright.version,
+ description="The core parts of the Twisted networking framework",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Glyph Lefkowitz",
+ url="http://twistedmatrix.com/",
+ license="MIT",
+ long_description="""\
+This is the core of Twisted, including:
+ * Networking support (twisted.internet)
+ * Trial, the unit testing framework (twisted.trial)
+ * AMP, the Asynchronous Messaging Protocol (twisted.protocols.amp)
+ * Twisted Spread, a remote object system (twisted.spread)
+ * Utility code (twisted.python)
+ * Basic abstractions that multiple subprojects use
+ (twisted.cred, twisted.application, twisted.plugin)
+ * Database connectivity support (twisted.enterprise)
+ * A few basic protocols and protocol abstractions (twisted.protocols)
+""",
+
+ # build stuff
+ packages=getPackages('twisted',
+ ignore=twisted_subprojects + ['plugins']),
+ plugins=plugins,
+ data_files=getDataFiles('twisted', ignore=twisted_subprojects),
+ conditionalExtensions=extensions,
+ scripts = getScripts(""),
+)
+
+
+if __name__ == '__main__':
+ setup(**setup_args)
diff --git a/twisted/trial/__init__.py b/twisted/trial/__init__.py
new file mode 100644
index 0000000..ad9423c
--- /dev/null
+++ b/twisted/trial/__init__.py
@@ -0,0 +1,52 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+
+"""
+Asynchronous unit testing framework.
+
+Trial extends Python's builtin C{unittest} to provide support for asynchronous
+tests.
+
+Maintainer: Jonathan Lange
+
+Trial strives to be compatible with other Python xUnit testing frameworks.
+"Compatibility" is a difficult things to define. In practice, it means that:
+
+ - L{twisted.trial.unittest.TestCase} objects should be able to be used by
+ other test runners without those runners requiring special support for
+ Trial tests.
+
+ - Tests that subclass the standard library C{TestCase} and don't do anything
+ "too weird" should be able to be discoverable and runnable by the Trial
+ test runner without the authors of those tests having to jump through
+ hoops.
+
+ - Tests that implement the interface provided by the standard library
+ C{TestCase} should be runnable by the Trial runner.
+
+ - The Trial test runner and Trial L{unittest.TestCase} objects ought to be
+ able to use standard library C{TestResult} objects, and third party
+ C{TestResult} objects based on the standard library.
+
+This list is not necessarily exhaustive -- compatibility is hard to define.
+Contributors who discover more helpful ways of defining compatibility are
+encouraged to update this document.
+
+
+Examples:
+
+B{Timeouts} for tests should be implemented in the runner. If this is done,
+then timeouts could work for third-party TestCase objects as well as for
+L{twisted.trial.unittest.TestCase} objects. Further, Twisted C{TestCase}
+objects will run in other runners without timing out.
+See U{http://twistedmatrix.com/trac/ticket/2675}.
+
+Running tests in a temporary directory should be a feature of the test case,
+because often tests themselves rely on this behaviour. If the feature is
+implemented in the runner, then tests will change behaviour (possibly
+breaking) when run in a different test runner. Further, many tests don't even
+care about the filesystem.
+See U{http://twistedmatrix.com/trac/ticket/2916}.
+"""
diff --git a/twisted/trial/itrial.py b/twisted/trial/itrial.py
new file mode 100644
index 0000000..d92884c
--- /dev/null
+++ b/twisted/trial/itrial.py
@@ -0,0 +1,251 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Interfaces for Trial.
+
+Maintainer: Jonathan Lange
+"""
+
+import zope.interface as zi
+from zope.interface import Attribute
+
+
+class ITestCase(zi.Interface):
+ """
+ The interface that a test case must implement in order to be used in Trial.
+ """
+
+ failureException = zi.Attribute(
+ "The exception class that is raised by failed assertions")
+
+
+ def __call__(result):
+ """
+ Run the test. Should always do exactly the same thing as run().
+ """
+
+
+ def countTestCases():
+ """
+ Return the number of tests in this test case. Usually 1.
+ """
+
+
+ def id():
+ """
+ Return a unique identifier for the test, usually the fully-qualified
+ Python name.
+ """
+
+
+ def run(result):
+ """
+ Run the test, storing the results in C{result}.
+
+ @param result: A L{TestResult}.
+ """
+
+
+ def shortDescription():
+ """
+ Return a short description of the test.
+ """
+
+
+
+class IReporter(zi.Interface):
+ """
+ I report results from a run of a test suite.
+ """
+
+ stream = zi.Attribute(
+ "Deprecated in Twisted 8.0. "
+ "The io-stream that this reporter will write to")
+ tbformat = zi.Attribute("Either 'default', 'brief', or 'verbose'")
+ args = zi.Attribute(
+ "Additional string argument passed from the command line")
+ shouldStop = zi.Attribute(
+ """
+ A boolean indicating that this reporter would like the test run to stop.
+ """)
+ separator = Attribute(
+ "Deprecated in Twisted 8.0. "
+ "A value which will occasionally be passed to the L{write} method.")
+ testsRun = Attribute(
+ """
+ The number of tests that seem to have been run according to this
+ reporter.
+ """)
+
+
+ def startTest(method):
+ """
+ Report the beginning of a run of a single test method.
+
+ @param method: an object that is adaptable to ITestMethod
+ """
+
+
+ def stopTest(method):
+ """
+ Report the status of a single test method
+
+ @param method: an object that is adaptable to ITestMethod
+ """
+
+
+ def startSuite(name):
+ """
+ Deprecated in Twisted 8.0.
+
+ Suites which wish to appear in reporter output should call this
+ before running their tests.
+ """
+
+
+ def endSuite(name):
+ """
+ Deprecated in Twisted 8.0.
+
+ Called at the end of a suite, if and only if that suite has called
+ C{startSuite}.
+ """
+
+
+ def cleanupErrors(errs):
+ """
+ Deprecated in Twisted 8.0.
+
+ Called when the reactor has been left in a 'dirty' state
+
+ @param errs: a list of L{twisted.python.failure.Failure}s
+ """
+
+
+ def upDownError(userMeth, warn=True, printStatus=True):
+ """
+ Deprecated in Twisted 8.0.
+
+ Called when an error occurs in a setUp* or tearDown* method
+
+ @param warn: indicates whether or not the reporter should emit a
+ warning about the error
+ @type warn: Boolean
+ @param printStatus: indicates whether or not the reporter should
+ print the name of the method and the status
+ message appropriate for the type of error
+ @type printStatus: Boolean
+ """
+
+
+ def addSuccess(test):
+ """
+ Record that test passed.
+ """
+
+
+ def addError(test, error):
+ """
+ Record that a test has raised an unexpected exception.
+
+ @param test: The test that has raised an error.
+ @param error: The error that the test raised. It will either be a
+ three-tuple in the style of C{sys.exc_info()} or a
+ L{Failure<twisted.python.failure.Failure>} object.
+ """
+
+
+ def addFailure(test, failure):
+ """
+ Record that a test has failed with the given failure.
+
+ @param test: The test that has failed.
+ @param failure: The failure that the test failed with. It will
+ either be a three-tuple in the style of C{sys.exc_info()}
+ or a L{Failure<twisted.python.failure.Failure>} object.
+ """
+
+
+ def addExpectedFailure(test, failure, todo):
+ """
+ Record that the given test failed, and was expected to do so.
+
+ @type test: L{pyunit.TestCase}
+ @param test: The test which this is about.
+ @type error: L{failure.Failure}
+ @param error: The error which this test failed with.
+ @type todo: L{unittest.Todo}
+ @param todo: The reason for the test's TODO status.
+ """
+
+
+ def addUnexpectedSuccess(test, todo):
+ """
+ Record that the given test failed, and was expected to do so.
+
+ @type test: L{pyunit.TestCase}
+ @param test: The test which this is about.
+ @type todo: L{unittest.Todo}
+ @param todo: The reason for the test's TODO status.
+ """
+
+
+ def addSkip(test, reason):
+ """
+ Record that a test has been skipped for the given reason.
+
+ @param test: The test that has been skipped.
+ @param reason: An object that the test case has specified as the reason
+ for skipping the test.
+ """
+
+
+ def printSummary():
+ """
+ Deprecated in Twisted 8.0, use L{done} instead.
+
+ Present a summary of the test results.
+ """
+
+
+ def printErrors():
+ """
+ Deprecated in Twisted 8.0, use L{done} instead.
+
+ Present the errors that have occured during the test run. This method
+ will be called after all tests have been run.
+ """
+
+
+ def write(string):
+ """
+ Deprecated in Twisted 8.0, use L{done} instead.
+
+ Display a string to the user, without appending a new line.
+ """
+
+
+ def writeln(string):
+ """
+ Deprecated in Twisted 8.0, use L{done} instead.
+
+ Display a string to the user, appending a new line.
+ """
+
+ def wasSuccessful():
+ """
+ Return a boolean indicating whether all test results that were reported
+ to this reporter were successful or not.
+ """
+
+
+ def done():
+ """
+ Called when the test run is complete.
+
+ This gives the result object an opportunity to display a summary of
+ information to the user. Once you have called C{done} on an
+ L{IReporter} object, you should assume that the L{IReporter} object is
+ no longer usable.
+ """
diff --git a/twisted/trial/reporter.py b/twisted/trial/reporter.py
new file mode 100644
index 0000000..dabe746
--- /dev/null
+++ b/twisted/trial/reporter.py
@@ -0,0 +1,1233 @@
+# -*- test-case-name: twisted.trial.test.test_reporter -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+
+"""
+Defines classes that handle the results of tests.
+"""
+
+import sys, os
+import time
+import warnings
+
+from twisted.python.compat import set
+from twisted.python import reflect, log
+from twisted.python.components import proxyForInterface
+from twisted.python.failure import Failure
+from twisted.python.util import OrderedDict, untilConcludes
+from twisted.trial import itrial, util
+
+try:
+ from subunit import TestProtocolClient
+except ImportError:
+ TestProtocolClient = None
+from zope.interface import implements
+
+pyunit = __import__('unittest')
+
+
+class BrokenTestCaseWarning(Warning):
+ """
+ Emitted as a warning when an exception occurs in one of setUp or tearDown.
+ """
+
+
+class SafeStream(object):
+ """
+ Wraps a stream object so that all C{write} calls are wrapped in
+ L{untilConcludes}.
+ """
+
+ def __init__(self, original):
+ self.original = original
+
+ def __getattr__(self, name):
+ return getattr(self.original, name)
+
+ def write(self, *a, **kw):
+ return untilConcludes(self.original.write, *a, **kw)
+
+
+class TestResult(pyunit.TestResult, object):
+ """
+ Accumulates the results of several L{twisted.trial.unittest.TestCase}s.
+
+ @ivar successes: count the number of successes achieved by the test run.
+ @type successes: C{int}
+ """
+ implements(itrial.IReporter)
+
+ def __init__(self):
+ super(TestResult, self).__init__()
+ self.skips = []
+ self.expectedFailures = []
+ self.unexpectedSuccesses = []
+ self.successes = 0
+ self._timings = []
+
+ def __repr__(self):
+ return ('<%s run=%d errors=%d failures=%d todos=%d dones=%d skips=%d>'
+ % (reflect.qual(self.__class__), self.testsRun,
+ len(self.errors), len(self.failures),
+ len(self.expectedFailures), len(self.skips),
+ len(self.unexpectedSuccesses)))
+
+ def _getTime(self):
+ return time.time()
+
+ def _getFailure(self, error):
+ """
+ Convert a C{sys.exc_info()}-style tuple to a L{Failure}, if necessary.
+ """
+ if isinstance(error, tuple):
+ return Failure(error[1], error[0], error[2])
+ return error
+
+ def startTest(self, test):
+ """
+ This must be called before the given test is commenced.
+
+ @type test: L{pyunit.TestCase}
+ """
+ super(TestResult, self).startTest(test)
+ self._testStarted = self._getTime()
+
+ def stopTest(self, test):
+ """
+ This must be called after the given test is completed.
+
+ @type test: L{pyunit.TestCase}
+ """
+ super(TestResult, self).stopTest(test)
+ self._lastTime = self._getTime() - self._testStarted
+
+ def addFailure(self, test, fail):
+ """
+ Report a failed assertion for the given test.
+
+ @type test: L{pyunit.TestCase}
+ @type fail: L{Failure} or L{tuple}
+ """
+ self.failures.append((test, self._getFailure(fail)))
+
+ def addError(self, test, error):
+ """
+ Report an error that occurred while running the given test.
+
+ @type test: L{pyunit.TestCase}
+ @type error: L{Failure} or L{tuple}
+ """
+ self.errors.append((test, self._getFailure(error)))
+
+ def addSkip(self, test, reason):
+ """
+ Report that the given test was skipped.
+
+ In Trial, tests can be 'skipped'. Tests are skipped mostly because there
+ is some platform or configuration issue that prevents them from being
+ run correctly.
+
+ @type test: L{pyunit.TestCase}
+ @type reason: L{str}
+ """
+ self.skips.append((test, reason))
+
+ def addUnexpectedSuccess(self, test, todo):
+ """Report that the given test succeeded against expectations.
+
+ In Trial, tests can be marked 'todo'. That is, they are expected to fail.
+ When a test that is expected to fail instead succeeds, it should call
+ this method to report the unexpected success.
+
+ @type test: L{pyunit.TestCase}
+ @type todo: L{unittest.Todo}
+ """
+ # XXX - 'todo' should just be a string
+ self.unexpectedSuccesses.append((test, todo))
+
+ def addExpectedFailure(self, test, error, todo):
+ """Report that the given test failed, and was expected to do so.
+
+ In Trial, tests can be marked 'todo'. That is, they are expected to fail.
+
+ @type test: L{pyunit.TestCase}
+ @type error: L{Failure}
+ @type todo: L{unittest.Todo}
+ """
+ # XXX - 'todo' should just be a string
+ self.expectedFailures.append((test, error, todo))
+
+ def addSuccess(self, test):
+ """Report that the given test succeeded.
+
+ @type test: L{pyunit.TestCase}
+ """
+ self.successes += 1
+
+ def upDownError(self, method, error, warn, printStatus):
+ warnings.warn("upDownError is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=3)
+
+ def cleanupErrors(self, errs):
+ """Report an error that occurred during the cleanup between tests.
+ """
+ warnings.warn("Cleanup errors are actual errors. Use addError. "
+ "Deprecated in Twisted 8.0",
+ category=DeprecationWarning, stacklevel=2)
+
+ def startSuite(self, name):
+ warnings.warn("startSuite deprecated in Twisted 8.0",
+ category=DeprecationWarning, stacklevel=2)
+
+ def endSuite(self, name):
+ warnings.warn("endSuite deprecated in Twisted 8.0",
+ category=DeprecationWarning, stacklevel=2)
+
+
+ def done(self):
+ """
+ The test suite has finished running.
+ """
+
+
+
+class TestResultDecorator(proxyForInterface(itrial.IReporter,
+ "_originalReporter")):
+ """
+ Base class for TestResult decorators.
+
+ @ivar _originalReporter: The wrapped instance of reporter.
+ @type _originalReporter: A provider of L{itrial.IReporter}
+ """
+
+ implements(itrial.IReporter)
+
+
+
+class UncleanWarningsReporterWrapper(TestResultDecorator):
+ """
+ A wrapper for a reporter that converts L{util.DirtyReactorAggregateError}s
+ to warnings.
+ """
+ implements(itrial.IReporter)
+
+ def addError(self, test, error):
+ """
+ If the error is a L{util.DirtyReactorAggregateError}, instead of
+ reporting it as a normal error, throw a warning.
+ """
+
+ if (isinstance(error, Failure)
+ and error.check(util.DirtyReactorAggregateError)):
+ warnings.warn(error.getErrorMessage())
+ else:
+ self._originalReporter.addError(test, error)
+
+
+
+class _AdaptedReporter(TestResultDecorator):
+ """
+ TestResult decorator that makes sure that addError only gets tests that
+ have been adapted with a particular test adapter.
+ """
+
+ def __init__(self, original, testAdapter):
+ """
+ Construct an L{_AdaptedReporter}.
+
+ @param original: An {itrial.IReporter}.
+ @param testAdapter: A callable that returns an L{itrial.ITestCase}.
+ """
+ TestResultDecorator.__init__(self, original)
+ self.testAdapter = testAdapter
+
+
+ def addError(self, test, error):
+ """
+ See L{itrial.IReporter}.
+ """
+ test = self.testAdapter(test)
+ return self._originalReporter.addError(test, error)
+
+
+ def addExpectedFailure(self, test, failure, todo):
+ """
+ See L{itrial.IReporter}.
+ """
+ return self._originalReporter.addExpectedFailure(
+ self.testAdapter(test), failure, todo)
+
+
+ def addFailure(self, test, failure):
+ """
+ See L{itrial.IReporter}.
+ """
+ test = self.testAdapter(test)
+ return self._originalReporter.addFailure(test, failure)
+
+
+ def addSkip(self, test, skip):
+ """
+ See L{itrial.IReporter}.
+ """
+ test = self.testAdapter(test)
+ return self._originalReporter.addSkip(test, skip)
+
+
+ def addUnexpectedSuccess(self, test, todo):
+ """
+ See L{itrial.IReporter}.
+ """
+ test = self.testAdapter(test)
+ return self._originalReporter.addUnexpectedSuccess(test, todo)
+
+
+ def startTest(self, test):
+ """
+ See L{itrial.IReporter}.
+ """
+ return self._originalReporter.startTest(self.testAdapter(test))
+
+
+ def stopTest(self, test):
+ """
+ See L{itrial.IReporter}.
+ """
+ return self._originalReporter.stopTest(self.testAdapter(test))
+
+
+
+class Reporter(TestResult):
+ """
+ A basic L{TestResult} with support for writing to a stream.
+
+ @ivar _startTime: The time when the first test was started. It defaults to
+ C{None}, which means that no test was actually launched.
+ @type _startTime: C{float} or C{NoneType}
+
+ @ivar _warningCache: A C{set} of tuples of warning message (file, line,
+ text, category) which have already been written to the output stream
+ during the currently executing test. This is used to avoid writing
+ duplicates of the same warning to the output stream.
+ @type _warningCache: C{set}
+
+ @ivar _publisher: The log publisher which will be observed for warning
+ events.
+ @type _publisher: L{LogPublisher} (or another type sufficiently similar)
+ """
+
+ implements(itrial.IReporter)
+
+ _separator = '-' * 79
+ _doubleSeparator = '=' * 79
+
+ def __init__(self, stream=sys.stdout, tbformat='default', realtime=False,
+ publisher=None):
+ super(Reporter, self).__init__()
+ self._stream = SafeStream(stream)
+ self.tbformat = tbformat
+ self.realtime = realtime
+ self._startTime = None
+ self._warningCache = set()
+
+ # Start observing log events so as to be able to report warnings.
+ self._publisher = publisher
+ if publisher is not None:
+ publisher.addObserver(self._observeWarnings)
+
+
+ def _observeWarnings(self, event):
+ """
+ Observe warning events and write them to C{self._stream}.
+
+ This method is a log observer which will be registered with
+ C{self._publisher.addObserver}.
+
+ @param event: A C{dict} from the logging system. If it has a
+ C{'warning'} key, a logged warning will be extracted from it and
+ possibly written to C{self.stream}.
+ """
+ if 'warning' in event:
+ key = (event['filename'], event['lineno'],
+ event['category'].split('.')[-1],
+ str(event['warning']))
+ if key not in self._warningCache:
+ self._warningCache.add(key)
+ self._stream.write('%s:%s: %s: %s\n' % key)
+
+
+ def stream(self):
+ warnings.warn("stream is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ return self._stream
+ stream = property(stream)
+
+
+ def separator(self):
+ warnings.warn("separator is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ return self._separator
+ separator = property(separator)
+
+
+ def startTest(self, test):
+ """
+ Called when a test begins to run. Records the time when it was first
+ called and resets the warning cache.
+
+ @param test: L{ITestCase}
+ """
+ super(Reporter, self).startTest(test)
+ if self._startTime is None:
+ self._startTime = self._getTime()
+ self._warningCache = set()
+
+
+ def addFailure(self, test, fail):
+ """
+ Called when a test fails. If L{realtime} is set, then it prints the
+ error to the stream.
+
+ @param test: L{ITestCase} that failed.
+ @param fail: L{failure.Failure} containing the error.
+ """
+ super(Reporter, self).addFailure(test, fail)
+ if self.realtime:
+ fail = self.failures[-1][1] # guarantee it's a Failure
+ self._write(self._formatFailureTraceback(fail))
+
+
+ def addError(self, test, error):
+ """
+ Called when a test raises an error. If L{realtime} is set, then it
+ prints the error to the stream.
+
+ @param test: L{ITestCase} that raised the error.
+ @param error: L{failure.Failure} containing the error.
+ """
+ error = self._getFailure(error)
+ super(Reporter, self).addError(test, error)
+ if self.realtime:
+ error = self.errors[-1][1] # guarantee it's a Failure
+ self._write(self._formatFailureTraceback(error))
+
+
+ def write(self, format, *args):
+ warnings.warn("write is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ self._write(format, *args)
+
+
+ def _write(self, format, *args):
+ """
+ Safely write to the reporter's stream.
+
+ @param format: A format string to write.
+ @param *args: The arguments for the format string.
+ """
+ s = str(format)
+ assert isinstance(s, type(''))
+ if args:
+ self._stream.write(s % args)
+ else:
+ self._stream.write(s)
+ untilConcludes(self._stream.flush)
+
+
+ def writeln(self, format, *args):
+ warnings.warn("writeln is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ self._writeln(format, *args)
+
+
+ def _writeln(self, format, *args):
+ """
+ Safely write a line to the reporter's stream. Newline is appended to
+ the format string.
+
+ @param format: A format string to write.
+ @param *args: The arguments for the format string.
+ """
+ self._write(format, *args)
+ self._write('\n')
+
+
+ def upDownError(self, method, error, warn, printStatus):
+ super(Reporter, self).upDownError(method, error, warn, printStatus)
+ if warn:
+ tbStr = self._formatFailureTraceback(error)
+ log.msg(tbStr)
+ msg = ("caught exception in %s, your TestCase is broken\n\n%s"
+ % (method, tbStr))
+ warnings.warn(msg, BrokenTestCaseWarning, stacklevel=2)
+
+
+ def cleanupErrors(self, errs):
+ super(Reporter, self).cleanupErrors(errs)
+ warnings.warn("%s\n%s" % ("REACTOR UNCLEAN! traceback(s) follow: ",
+ self._formatFailureTraceback(errs)),
+ BrokenTestCaseWarning)
+
+
+ def _trimFrames(self, frames):
+ # when a method fails synchronously, the stack looks like this:
+ # [0]: defer.maybeDeferred()
+ # [1]: utils.runWithWarningsSuppressed()
+ # [2:-2]: code in the test method which failed
+ # [-1]: unittest.fail
+
+ # when a method fails inside a Deferred (i.e., when the test method
+ # returns a Deferred, and that Deferred's errback fires), the stack
+ # captured inside the resulting Failure looks like this:
+ # [0]: defer.Deferred._runCallbacks
+ # [1:-2]: code in the testmethod which failed
+ # [-1]: unittest.fail
+
+ # as a result, we want to trim either [maybeDeferred,runWWS] or
+ # [Deferred._runCallbacks] from the front, and trim the
+ # [unittest.fail] from the end.
+
+ # There is also another case, when the test method is badly defined and
+ # contains extra arguments.
+
+ newFrames = list(frames)
+
+ if len(frames) < 2:
+ return newFrames
+
+ first = newFrames[0]
+ second = newFrames[1]
+ if (first[0] == "maybeDeferred"
+ and os.path.splitext(os.path.basename(first[1]))[0] == 'defer'
+ and second[0] == "runWithWarningsSuppressed"
+ and os.path.splitext(os.path.basename(second[1]))[0] == 'utils'):
+ newFrames = newFrames[2:]
+ elif (first[0] == "_runCallbacks"
+ and os.path.splitext(os.path.basename(first[1]))[0] == 'defer'):
+ newFrames = newFrames[1:]
+
+ if not newFrames:
+ # The method fails before getting called, probably an argument problem
+ return newFrames
+
+ last = newFrames[-1]
+ if (last[0].startswith('fail')
+ and os.path.splitext(os.path.basename(last[1]))[0] == 'unittest'):
+ newFrames = newFrames[:-1]
+
+ return newFrames
+
+
+ def _formatFailureTraceback(self, fail):
+ if isinstance(fail, str):
+ return fail.rstrip() + '\n'
+ fail.frames, frames = self._trimFrames(fail.frames), fail.frames
+ result = fail.getTraceback(detail=self.tbformat, elideFrameworkCode=True)
+ fail.frames = frames
+ return result
+
+
+ def _groupResults(self, results, formatter):
+ """
+ Group tests together based on their results.
+
+ @param results: An iterable of tuples of two or more elements. The
+ first element of each tuple is a test case. The remaining
+ elements describe the outcome of that test case.
+
+ @param formatter: A callable which turns a test case result into a
+ string. The elements after the first of the tuples in
+ C{results} will be passed as positional arguments to
+ C{formatter}.
+
+ @return: A C{list} of two-tuples. The first element of each tuple
+ is a unique string describing one result from at least one of
+ the test cases in C{results}. The second element is a list of
+ the test cases which had that result.
+ """
+ groups = OrderedDict()
+ for content in results:
+ case = content[0]
+ outcome = content[1:]
+ key = formatter(*outcome)
+ groups.setdefault(key, []).append(case)
+ return groups.items()
+
+
+ def _printResults(self, flavor, errors, formatter):
+ """
+ Print a group of errors to the stream.
+
+ @param flavor: A string indicating the kind of error (e.g. 'TODO').
+ @param errors: A list of errors, often L{failure.Failure}s, but
+ sometimes 'todo' errors.
+ @param formatter: A callable that knows how to format the errors.
+ """
+ for reason, cases in self._groupResults(errors, formatter):
+ self._writeln(self._doubleSeparator)
+ self._writeln(flavor)
+ self._write(reason)
+ self._writeln('')
+ for case in cases:
+ self._writeln(case.id())
+
+
+ def _printExpectedFailure(self, error, todo):
+ return 'Reason: %r\n%s' % (todo.reason,
+ self._formatFailureTraceback(error))
+
+
+ def _printUnexpectedSuccess(self, todo):
+ ret = 'Reason: %r\n' % (todo.reason,)
+ if todo.errors:
+ ret += 'Expected errors: %s\n' % (', '.join(todo.errors),)
+ return ret
+
+
+ def printErrors(self):
+ """
+ Print all of the non-success results in full to the stream.
+ """
+ warnings.warn("printErrors is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ self._printErrors()
+
+
+ def _printErrors(self):
+ """
+ Print all of the non-success results to the stream in full.
+ """
+ self._write('\n')
+ self._printResults('[SKIPPED]', self.skips, lambda x : '%s\n' % x)
+ self._printResults('[TODO]', self.expectedFailures,
+ self._printExpectedFailure)
+ self._printResults('[FAIL]', self.failures,
+ self._formatFailureTraceback)
+ self._printResults('[ERROR]', self.errors,
+ self._formatFailureTraceback)
+ self._printResults('[SUCCESS!?!]', self.unexpectedSuccesses,
+ self._printUnexpectedSuccess)
+
+
+ def _getSummary(self):
+ """
+ Return a formatted count of tests status results.
+ """
+ summaries = []
+ for stat in ("skips", "expectedFailures", "failures", "errors",
+ "unexpectedSuccesses"):
+ num = len(getattr(self, stat))
+ if num:
+ summaries.append('%s=%d' % (stat, num))
+ if self.successes:
+ summaries.append('successes=%d' % (self.successes,))
+ summary = (summaries and ' ('+', '.join(summaries)+')') or ''
+ return summary
+
+
+ def printSummary(self):
+ """
+ Print a line summarising the test results to the stream.
+ """
+ warnings.warn("printSummary is deprecated in Twisted 8.0.",
+ category=DeprecationWarning, stacklevel=2)
+ self._printSummary()
+
+
+ def _printSummary(self):
+ """
+ Print a line summarising the test results to the stream.
+ """
+ summary = self._getSummary()
+ if self.wasSuccessful():
+ status = "PASSED"
+ else:
+ status = "FAILED"
+ self._write("%s%s\n", status, summary)
+
+
+ def done(self):
+ """
+ Summarize the result of the test run.
+
+ The summary includes a report of all of the errors, todos, skips and
+ so forth that occurred during the run. It also includes the number of
+ tests that were run and how long it took to run them (not including
+ load time).
+
+ Expects that L{_printErrors}, L{_writeln}, L{_write}, L{_printSummary}
+ and L{_separator} are all implemented.
+ """
+ if self._publisher is not None:
+ self._publisher.removeObserver(self._observeWarnings)
+ self._printErrors()
+ self._writeln(self._separator)
+ if self._startTime is not None:
+ self._writeln('Ran %d tests in %.3fs', self.testsRun,
+ time.time() - self._startTime)
+ self._write('\n')
+ self._printSummary()
+
+
+
+class MinimalReporter(Reporter):
+ """
+ A minimalist reporter that prints only a summary of the test result, in
+ the form of (timeTaken, #tests, #tests, #errors, #failures, #skips).
+ """
+
+ def _printErrors(self):
+ """
+ Don't print a detailed summary of errors. We only care about the
+ counts.
+ """
+
+
+ def _printSummary(self):
+ """
+ Print out a one-line summary of the form:
+ '%(runtime) %(number_of_tests) %(number_of_tests) %(num_errors)
+ %(num_failures) %(num_skips)'
+ """
+ numTests = self.testsRun
+ if self._startTime is not None:
+ timing = self._getTime() - self._startTime
+ else:
+ timing = 0
+ t = (timing, numTests, numTests,
+ len(self.errors), len(self.failures), len(self.skips))
+ self._writeln(' '.join(map(str, t)))
+
+
+
+class TextReporter(Reporter):
+ """
+ Simple reporter that prints a single character for each test as it runs,
+ along with the standard Trial summary text.
+ """
+
+ def addSuccess(self, test):
+ super(TextReporter, self).addSuccess(test)
+ self._write('.')
+
+
+ def addError(self, *args):
+ super(TextReporter, self).addError(*args)
+ self._write('E')
+
+
+ def addFailure(self, *args):
+ super(TextReporter, self).addFailure(*args)
+ self._write('F')
+
+
+ def addSkip(self, *args):
+ super(TextReporter, self).addSkip(*args)
+ self._write('S')
+
+
+ def addExpectedFailure(self, *args):
+ super(TextReporter, self).addExpectedFailure(*args)
+ self._write('T')
+
+
+ def addUnexpectedSuccess(self, *args):
+ super(TextReporter, self).addUnexpectedSuccess(*args)
+ self._write('!')
+
+
+
+class VerboseTextReporter(Reporter):
+ """
+ A verbose reporter that prints the name of each test as it is running.
+
+ Each line is printed with the name of the test, followed by the result of
+ that test.
+ """
+
+ # This is actually the bwverbose option
+
+ def startTest(self, tm):
+ self._write('%s ... ', tm.id())
+ super(VerboseTextReporter, self).startTest(tm)
+
+
+ def addSuccess(self, test):
+ super(VerboseTextReporter, self).addSuccess(test)
+ self._write('[OK]')
+
+
+ def addError(self, *args):
+ super(VerboseTextReporter, self).addError(*args)
+ self._write('[ERROR]')
+
+
+ def addFailure(self, *args):
+ super(VerboseTextReporter, self).addFailure(*args)
+ self._write('[FAILURE]')
+
+
+ def addSkip(self, *args):
+ super(VerboseTextReporter, self).addSkip(*args)
+ self._write('[SKIPPED]')
+
+
+ def addExpectedFailure(self, *args):
+ super(VerboseTextReporter, self).addExpectedFailure(*args)
+ self._write('[TODO]')
+
+
+ def addUnexpectedSuccess(self, *args):
+ super(VerboseTextReporter, self).addUnexpectedSuccess(*args)
+ self._write('[SUCCESS!?!]')
+
+
+ def stopTest(self, test):
+ super(VerboseTextReporter, self).stopTest(test)
+ self._write('\n')
+
+
+
+class TimingTextReporter(VerboseTextReporter):
+ """
+ Prints out each test as it is running, followed by the time taken for each
+ test to run.
+ """
+
+ def stopTest(self, method):
+ """
+ Mark the test as stopped, and write the time it took to run the test
+ to the stream.
+ """
+ super(TimingTextReporter, self).stopTest(method)
+ self._write("(%.03f secs)\n" % self._lastTime)
+
+
+
+class _AnsiColorizer(object):
+ """
+ A colorizer is an object that loosely wraps around a stream, allowing
+ callers to write text to the stream in a particular color.
+
+ Colorizer classes must implement C{supported()} and C{write(text, color)}.
+ """
+ _colors = dict(black=30, red=31, green=32, yellow=33,
+ blue=34, magenta=35, cyan=36, white=37)
+
+ def __init__(self, stream):
+ self.stream = stream
+
+ def supported(cls, stream=sys.stdout):
+ """
+ A class method that returns True if the current platform supports
+ coloring terminal output using this method. Returns False otherwise.
+ """
+ if not stream.isatty():
+ return False # auto color only on TTYs
+ try:
+ import curses
+ except ImportError:
+ return False
+ else:
+ try:
+ try:
+ return curses.tigetnum("colors") > 2
+ except curses.error:
+ curses.setupterm()
+ return curses.tigetnum("colors") > 2
+ except:
+ # guess false in case of error
+ return False
+ supported = classmethod(supported)
+
+ def write(self, text, color):
+ """
+ Write the given text to the stream in the given color.
+
+ @param text: Text to be written to the stream.
+
+ @param color: A string label for a color. e.g. 'red', 'white'.
+ """
+ color = self._colors[color]
+ self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
+
+
+class _Win32Colorizer(object):
+ """
+ See _AnsiColorizer docstring.
+ """
+ def __init__(self, stream):
+ from win32console import GetStdHandle, STD_OUTPUT_HANDLE, \
+ FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
+ FOREGROUND_INTENSITY
+ red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
+ FOREGROUND_BLUE, FOREGROUND_INTENSITY)
+ self.stream = stream
+ self.screenBuffer = GetStdHandle(STD_OUTPUT_HANDLE)
+ self._colors = {
+ 'normal': red | green | blue,
+ 'red': red | bold,
+ 'green': green | bold,
+ 'blue': blue | bold,
+ 'yellow': red | green | bold,
+ 'magenta': red | blue | bold,
+ 'cyan': green | blue | bold,
+ 'white': red | green | blue | bold
+ }
+
+ def supported(cls, stream=sys.stdout):
+ try:
+ import win32console
+ screenBuffer = win32console.GetStdHandle(
+ win32console.STD_OUTPUT_HANDLE)
+ except ImportError:
+ return False
+ import pywintypes
+ try:
+ screenBuffer.SetConsoleTextAttribute(
+ win32console.FOREGROUND_RED |
+ win32console.FOREGROUND_GREEN |
+ win32console.FOREGROUND_BLUE)
+ except pywintypes.error:
+ return False
+ else:
+ return True
+ supported = classmethod(supported)
+
+ def write(self, text, color):
+ color = self._colors[color]
+ self.screenBuffer.SetConsoleTextAttribute(color)
+ self.stream.write(text)
+ self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
+
+
+class _NullColorizer(object):
+ """
+ See _AnsiColorizer docstring.
+ """
+ def __init__(self, stream):
+ self.stream = stream
+
+ def supported(cls, stream=sys.stdout):
+ return True
+ supported = classmethod(supported)
+
+ def write(self, text, color):
+ self.stream.write(text)
+
+
+
+class SubunitReporter(object):
+ """
+ Reports test output via Subunit.
+
+ @ivar _subunit: The subunit protocol client that we are wrapping.
+
+ @ivar _successful: An internal variable, used to track whether we have
+ received only successful results.
+
+ @since: 10.0
+ """
+ implements(itrial.IReporter)
+
+
+ def __init__(self, stream=sys.stdout, tbformat='default',
+ realtime=False, publisher=None):
+ """
+ Construct a L{SubunitReporter}.
+
+ @param stream: A file-like object representing the stream to print
+ output to. Defaults to stdout.
+ @param tbformat: The format for tracebacks. Ignored, since subunit
+ always uses Python's standard format.
+ @param realtime: Whether or not to print exceptions in the middle
+ of the test results. Ignored, since subunit always does this.
+ @param publisher: The log publisher which will be preserved for
+ reporting events. Ignored, as it's not relevant to subunit.
+ """
+ if TestProtocolClient is None:
+ raise Exception("Subunit not available")
+ self._subunit = TestProtocolClient(stream)
+ self._successful = True
+
+
+ def done(self):
+ """
+ Record that the entire test suite run is finished.
+
+ We do nothing, since a summary clause is irrelevant to the subunit
+ protocol.
+ """
+ pass
+
+
+ def shouldStop(self):
+ """
+ Whether or not the test runner should stop running tests.
+ """
+ return self._subunit.shouldStop
+ shouldStop = property(shouldStop)
+
+
+ def stop(self):
+ """
+ Signal that the test runner should stop running tests.
+ """
+ return self._subunit.stop()
+
+
+ def wasSuccessful(self):
+ """
+ Has the test run been successful so far?
+
+ @return: C{True} if we have received no reports of errors or failures,
+ C{False} otherwise.
+ """
+ # Subunit has a bug in its implementation of wasSuccessful, see
+ # https://bugs.edge.launchpad.net/subunit/+bug/491090, so we can't
+ # simply forward it on.
+ return self._successful
+
+
+ def startTest(self, test):
+ """
+ Record that C{test} has started.
+ """
+ return self._subunit.startTest(test)
+
+
+ def stopTest(self, test):
+ """
+ Record that C{test} has completed.
+ """
+ return self._subunit.stopTest(test)
+
+
+ def addSuccess(self, test):
+ """
+ Record that C{test} was successful.
+ """
+ return self._subunit.addSuccess(test)
+
+
+ def addSkip(self, test, reason):
+ """
+ Record that C{test} was skipped for C{reason}.
+
+ Some versions of subunit don't have support for addSkip. In those
+ cases, the skip is reported as a success.
+
+ @param test: A unittest-compatible C{TestCase}.
+ @param reason: The reason for it being skipped. The C{str()} of this
+ object will be included in the subunit output stream.
+ """
+ addSkip = getattr(self._subunit, 'addSkip', None)
+ if addSkip is None:
+ self.addSuccess(test)
+ else:
+ self._subunit.addSkip(test, reason)
+
+
+ def addError(self, test, err):
+ """
+ Record that C{test} failed with an unexpected error C{err}.
+
+ Also marks the run as being unsuccessful, causing
+ L{SubunitReporter.wasSuccessful} to return C{False}.
+ """
+ self._successful = False
+ return self._subunit.addError(
+ test, util.excInfoOrFailureToExcInfo(err))
+
+
+ def addFailure(self, test, err):
+ """
+ Record that C{test} failed an assertion with the error C{err}.
+
+ Also marks the run as being unsuccessful, causing
+ L{SubunitReporter.wasSuccessful} to return C{False}.
+ """
+ self._successful = False
+ return self._subunit.addFailure(
+ test, util.excInfoOrFailureToExcInfo(err))
+
+
+ def addExpectedFailure(self, test, failure, todo):
+ """
+ Record an expected failure from a test.
+
+ Some versions of subunit do not implement this. For those versions, we
+ record a success.
+ """
+ failure = util.excInfoOrFailureToExcInfo(failure)
+ addExpectedFailure = getattr(self._subunit, 'addExpectedFailure', None)
+ if addExpectedFailure is None:
+ self.addSuccess(test)
+ else:
+ addExpectedFailure(test, failure)
+
+
+ def addUnexpectedSuccess(self, test, todo):
+ """
+ Record an unexpected success.
+
+ Since subunit has no way of expressing this concept, we record a
+ success on the subunit stream.
+ """
+ # Not represented in pyunit/subunit.
+ self.addSuccess(test)
+
+
+
+class TreeReporter(Reporter):
+ """
+ Print out the tests in the form a tree.
+
+ Tests are indented according to which class and module they belong.
+ Results are printed in ANSI color.
+ """
+
+ currentLine = ''
+ indent = ' '
+ columns = 79
+
+ FAILURE = 'red'
+ ERROR = 'red'
+ TODO = 'blue'
+ SKIP = 'blue'
+ TODONE = 'red'
+ SUCCESS = 'green'
+
+ def __init__(self, stream=sys.stdout, *args, **kwargs):
+ super(TreeReporter, self).__init__(stream, *args, **kwargs)
+ self._lastTest = []
+ for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
+ if colorizer.supported(stream):
+ self._colorizer = colorizer(stream)
+ break
+
+ def getDescription(self, test):
+ """
+ Return the name of the method which 'test' represents. This is
+ what gets displayed in the leaves of the tree.
+
+ e.g. getDescription(TestCase('test_foo')) ==> test_foo
+ """
+ return test.id().split('.')[-1]
+
+ def addSuccess(self, test):
+ super(TreeReporter, self).addSuccess(test)
+ self.endLine('[OK]', self.SUCCESS)
+
+ def addError(self, *args):
+ super(TreeReporter, self).addError(*args)
+ self.endLine('[ERROR]', self.ERROR)
+
+ def addFailure(self, *args):
+ super(TreeReporter, self).addFailure(*args)
+ self.endLine('[FAIL]', self.FAILURE)
+
+ def addSkip(self, *args):
+ super(TreeReporter, self).addSkip(*args)
+ self.endLine('[SKIPPED]', self.SKIP)
+
+ def addExpectedFailure(self, *args):
+ super(TreeReporter, self).addExpectedFailure(*args)
+ self.endLine('[TODO]', self.TODO)
+
+ def addUnexpectedSuccess(self, *args):
+ super(TreeReporter, self).addUnexpectedSuccess(*args)
+ self.endLine('[SUCCESS!?!]', self.TODONE)
+
+ def _write(self, format, *args):
+ if args:
+ format = format % args
+ self.currentLine = format
+ super(TreeReporter, self)._write(self.currentLine)
+
+
+ def _getPreludeSegments(self, testID):
+ """
+ Return a list of all non-leaf segments to display in the tree.
+
+ Normally this is the module and class name.
+ """
+ segments = testID.split('.')[:-1]
+ if len(segments) == 0:
+ return segments
+ segments = [
+ seg for seg in '.'.join(segments[:-1]), segments[-1]
+ if len(seg) > 0]
+ return segments
+
+
+ def _testPrelude(self, testID):
+ """
+ Write the name of the test to the stream, indenting it appropriately.
+
+ If the test is the first test in a new 'branch' of the tree, also
+ write all of the parents in that branch.
+ """
+ segments = self._getPreludeSegments(testID)
+ indentLevel = 0
+ for seg in segments:
+ if indentLevel < len(self._lastTest):
+ if seg != self._lastTest[indentLevel]:
+ self._write('%s%s\n' % (self.indent * indentLevel, seg))
+ else:
+ self._write('%s%s\n' % (self.indent * indentLevel, seg))
+ indentLevel += 1
+ self._lastTest = segments
+
+
+ def cleanupErrors(self, errs):
+ self._colorizer.write(' cleanup errors', self.ERROR)
+ self.endLine('[ERROR]', self.ERROR)
+ super(TreeReporter, self).cleanupErrors(errs)
+
+ def upDownError(self, method, error, warn, printStatus):
+ self._colorizer.write(" %s" % method, self.ERROR)
+ if printStatus:
+ self.endLine('[ERROR]', self.ERROR)
+ super(TreeReporter, self).upDownError(method, error, warn, printStatus)
+
+ def startTest(self, test):
+ """
+ Called when C{test} starts. Writes the tests name to the stream using
+ a tree format.
+ """
+ self._testPrelude(test.id())
+ self._write('%s%s ... ' % (self.indent * (len(self._lastTest)),
+ self.getDescription(test)))
+ super(TreeReporter, self).startTest(test)
+
+
+ def endLine(self, message, color):
+ """
+ Print 'message' in the given color.
+
+ @param message: A string message, usually '[OK]' or something similar.
+ @param color: A string color, 'red', 'green' and so forth.
+ """
+ spaces = ' ' * (self.columns - len(self.currentLine) - len(message))
+ super(TreeReporter, self)._write(spaces)
+ self._colorizer.write(message, color)
+ super(TreeReporter, self)._write("\n")
+
+
+ def _printSummary(self):
+ """
+ Print a line summarising the test results to the stream, and color the
+ status result.
+ """
+ summary = self._getSummary()
+ if self.wasSuccessful():
+ status = "PASSED"
+ color = self.SUCCESS
+ else:
+ status = "FAILED"
+ color = self.FAILURE
+ self._colorizer.write(status, color)
+ self._write("%s\n", summary)
diff --git a/twisted/trial/runner.py b/twisted/trial/runner.py
new file mode 100644
index 0000000..24f5d13
--- /dev/null
+++ b/twisted/trial/runner.py
@@ -0,0 +1,877 @@
+# -*- test-case-name: twisted.trial.test.test_runner -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A miscellany of code used to run Trial tests.
+
+Maintainer: Jonathan Lange
+"""
+
+__all__ = [
+ 'suiteVisit', 'TestSuite',
+
+ 'DestructiveTestSuite', 'DocTestCase', 'DryRunVisitor',
+ 'ErrorHolder', 'LoggedSuite', 'PyUnitTestCase',
+ 'TestHolder', 'TestLoader', 'TrialRunner', 'TrialSuite',
+
+ 'filenameToModule', 'isPackage', 'isPackageDirectory', 'isTestCase',
+ 'name', 'samefile', 'NOT_IN_TEST',
+ ]
+
+import pdb
+import os, types, warnings, sys, inspect, imp
+import doctest, time
+
+from twisted.python import reflect, log, failure, modules, filepath
+from twisted.python.compat import set
+
+from twisted.internet import defer
+from twisted.trial import util, unittest
+from twisted.trial.itrial import ITestCase
+from twisted.trial.reporter import UncleanWarningsReporterWrapper
+
+# These are imported so that they remain in the public API for t.trial.runner
+from twisted.trial.unittest import suiteVisit, TestSuite
+
+from zope.interface import implements
+
+pyunit = __import__('unittest')
+
+
+
+def isPackage(module):
+ """Given an object return True if the object looks like a package"""
+ if not isinstance(module, types.ModuleType):
+ return False
+ basename = os.path.splitext(os.path.basename(module.__file__))[0]
+ return basename == '__init__'
+
+
+def isPackageDirectory(dirname):
+ """Is the directory at path 'dirname' a Python package directory?
+ Returns the name of the __init__ file (it may have a weird extension)
+ if dirname is a package directory. Otherwise, returns False"""
+ for ext in zip(*imp.get_suffixes())[0]:
+ initFile = '__init__' + ext
+ if os.path.exists(os.path.join(dirname, initFile)):
+ return initFile
+ return False
+
+
+def samefile(filename1, filename2):
+ """
+ A hacky implementation of C{os.path.samefile}. Used by L{filenameToModule}
+ when the platform doesn't provide C{os.path.samefile}. Do not use this.
+ """
+ return os.path.abspath(filename1) == os.path.abspath(filename2)
+
+
+def filenameToModule(fn):
+ """
+ Given a filename, do whatever possible to return a module object matching
+ that file.
+
+ If the file in question is a module in Python path, properly import and
+ return that module. Otherwise, load the source manually.
+
+ @param fn: A filename.
+ @return: A module object.
+ @raise ValueError: If C{fn} does not exist.
+ """
+ if not os.path.exists(fn):
+ raise ValueError("%r doesn't exist" % (fn,))
+ try:
+ ret = reflect.namedAny(reflect.filenameToModuleName(fn))
+ except (ValueError, AttributeError):
+ # Couldn't find module. The file 'fn' is not in PYTHONPATH
+ return _importFromFile(fn)
+ # ensure that the loaded module matches the file
+ retFile = os.path.splitext(ret.__file__)[0] + '.py'
+ # not all platforms (e.g. win32) have os.path.samefile
+ same = getattr(os.path, 'samefile', samefile)
+ if os.path.isfile(fn) and not same(fn, retFile):
+ del sys.modules[ret.__name__]
+ ret = _importFromFile(fn)
+ return ret
+
+
+def _importFromFile(fn, moduleName=None):
+ fn = _resolveDirectory(fn)
+ if not moduleName:
+ moduleName = os.path.splitext(os.path.split(fn)[-1])[0]
+ if moduleName in sys.modules:
+ return sys.modules[moduleName]
+ fd = open(fn, 'r')
+ try:
+ module = imp.load_source(moduleName, fn, fd)
+ finally:
+ fd.close()
+ return module
+
+
+def _resolveDirectory(fn):
+ if os.path.isdir(fn):
+ initFile = isPackageDirectory(fn)
+ if initFile:
+ fn = os.path.join(fn, initFile)
+ else:
+ raise ValueError('%r is not a package directory' % (fn,))
+ return fn
+
+
+def _getMethodNameInClass(method):
+ """
+ Find the attribute name on the method's class which refers to the method.
+
+ For some methods, notably decorators which have not had __name__ set correctly:
+
+ getattr(method.im_class, method.__name__) != method
+ """
+ if getattr(method.im_class, method.__name__, object()) != method:
+ for alias in dir(method.im_class):
+ if getattr(method.im_class, alias, object()) == method:
+ return alias
+ return method.__name__
+
+
+class DestructiveTestSuite(TestSuite):
+ """
+ A test suite which remove the tests once run, to minimize memory usage.
+ """
+
+ def run(self, result):
+ """
+ Almost the same as L{TestSuite.run}, but with C{self._tests} being
+ empty at the end.
+ """
+ while self._tests:
+ if result.shouldStop:
+ break
+ test = self._tests.pop(0)
+ test(result)
+ return result
+
+
+
+# When an error occurs outside of any test, the user will see this string
+# in place of a test's name.
+NOT_IN_TEST = "<not in test>"
+
+
+
+class LoggedSuite(TestSuite):
+ """
+ Any errors logged in this suite will be reported to the L{TestResult}
+ object.
+ """
+
+ def run(self, result):
+ """
+ Run the suite, storing all errors in C{result}. If an error is logged
+ while no tests are running, then it will be added as an error to
+ C{result}.
+
+ @param result: A L{TestResult} object.
+ """
+ observer = unittest._logObserver
+ observer._add()
+ super(LoggedSuite, self).run(result)
+ observer._remove()
+ for error in observer.getErrors():
+ result.addError(TestHolder(NOT_IN_TEST), error)
+ observer.flushErrors()
+
+
+
+class PyUnitTestCase(object):
+ """
+ DEPRECATED in Twisted 8.0.
+
+ This class decorates the pyunit.TestCase class, mainly to work around the
+ differences between unittest in Python 2.3, 2.4, and 2.5. These
+ differences are::
+
+ - The way doctest unittests describe themselves
+ - Where the implementation of TestCase.run is (used to be in __call__)
+ - Where the test method name is kept (mangled-private or non-mangled
+ private variable)
+
+ It also implements visit, which we like.
+ """
+
+ def __init__(self, test):
+ warnings.warn("Deprecated in Twisted 8.0.",
+ category=DeprecationWarning)
+ self._test = test
+ test.id = self.id
+
+ def id(self):
+ cls = self._test.__class__
+ tmn = getattr(self._test, '_TestCase__testMethodName', None)
+ if tmn is None:
+ # python2.5's 'unittest' module is more sensible; but different.
+ tmn = self._test._testMethodName
+ return (cls.__module__ + '.' + cls.__name__ + '.' +
+ tmn)
+
+ def __repr__(self):
+ return 'PyUnitTestCase<%r>'%(self.id(),)
+
+ def __call__(self, results):
+ return self._test(results)
+
+
+ def visit(self, visitor):
+ """
+ Call the given visitor with the original, standard library, test case
+ that C{self} wraps. See L{unittest.TestCase.visit}.
+
+ Deprecated in Twisted 8.0.
+ """
+ warnings.warn("Test visitors deprecated in Twisted 8.0",
+ category=DeprecationWarning)
+ visitor(self._test)
+
+
+ def __getattr__(self, name):
+ return getattr(self._test, name)
+
+
+
+class DocTestCase(PyUnitTestCase):
+ """
+ DEPRECATED in Twisted 8.0.
+ """
+
+ def id(self):
+ """
+ In Python 2.4, doctests have correct id() behaviour. In Python 2.3,
+ id() returns 'runit'.
+
+ Here we override id() so that at least it will always contain the
+ fully qualified Python name of the doctest.
+ """
+ return self._test.shortDescription()
+
+
+class TrialSuite(TestSuite):
+ """
+ Suite to wrap around every single test in a C{trial} run. Used internally
+ by Trial to set up things necessary for Trial tests to work, regardless of
+ what context they are run in.
+ """
+
+ def __init__(self, tests=()):
+ suite = LoggedSuite(tests)
+ super(TrialSuite, self).__init__([suite])
+
+
+ def _bail(self):
+ from twisted.internet import reactor
+ d = defer.Deferred()
+ reactor.addSystemEventTrigger('after', 'shutdown',
+ lambda: d.callback(None))
+ reactor.fireSystemEvent('shutdown') # radix's suggestion
+ # As long as TestCase does crap stuff with the reactor we need to
+ # manually shutdown the reactor here, and that requires util.wait
+ # :(
+ # so that the shutdown event completes
+ unittest.TestCase('mktemp')._wait(d)
+
+ def run(self, result):
+ try:
+ TestSuite.run(self, result)
+ finally:
+ self._bail()
+
+
+def name(thing):
+ """
+ @param thing: an object from modules (instance of PythonModule,
+ PythonAttribute), a TestCase subclass, or an instance of a TestCase.
+ """
+ if isTestCase(thing):
+ # TestCase subclass
+ theName = reflect.qual(thing)
+ else:
+ # thing from trial, or thing from modules.
+ # this monstrosity exists so that modules' objects do not have to
+ # implement id(). -jml
+ try:
+ theName = thing.id()
+ except AttributeError:
+ theName = thing.name
+ return theName
+
+
+def isTestCase(obj):
+ """
+ @return: C{True} if C{obj} is a class that contains test cases, C{False}
+ otherwise. Used to find all the tests in a module.
+ """
+ try:
+ return issubclass(obj, pyunit.TestCase)
+ except TypeError:
+ return False
+
+
+
+class TestHolder(object):
+ """
+ Placeholder for a L{TestCase} inside a reporter. As far as a L{TestResult}
+ is concerned, this looks exactly like a unit test.
+ """
+
+ implements(ITestCase)
+
+ failureException = None
+
+ def __init__(self, description):
+ """
+ @param description: A string to be displayed L{TestResult}.
+ """
+ self.description = description
+
+
+ def __call__(self, result):
+ return self.run(result)
+
+
+ def id(self):
+ return self.description
+
+
+ def countTestCases(self):
+ return 0
+
+
+ def run(self, result):
+ """
+ This test is just a placeholder. Run the test successfully.
+
+ @param result: The C{TestResult} to store the results in.
+ @type result: L{twisted.trial.itrial.ITestResult}.
+ """
+ result.startTest(self)
+ result.addSuccess(self)
+ result.stopTest(self)
+
+
+ def shortDescription(self):
+ return self.description
+
+
+
+class ErrorHolder(TestHolder):
+ """
+ Used to insert arbitrary errors into a test suite run. Provides enough
+ methods to look like a C{TestCase}, however, when it is run, it simply adds
+ an error to the C{TestResult}. The most common use-case is for when a
+ module fails to import.
+ """
+
+ def __init__(self, description, error):
+ """
+ @param description: A string used by C{TestResult}s to identify this
+ error. Generally, this is the name of a module that failed to import.
+
+ @param error: The error to be added to the result. Can be an `exc_info`
+ tuple or a L{twisted.python.failure.Failure}.
+ """
+ super(ErrorHolder, self).__init__(description)
+ self.error = util.excInfoOrFailureToExcInfo(error)
+
+
+ def __repr__(self):
+ return "<ErrorHolder description=%r error=%s%s>" % (
+ # Format the exception type and arguments explicitly, as exception
+ # objects do not have nice looking string formats on Python 2.4.
+ self.description, self.error[0].__name__, self.error[1].args)
+
+
+ def run(self, result):
+ """
+ Run the test, reporting the error.
+
+ @param result: The C{TestResult} to store the results in.
+ @type result: L{twisted.trial.itrial.ITestResult}.
+ """
+ result.startTest(self)
+ result.addError(self, self.error)
+ result.stopTest(self)
+
+
+ def visit(self, visitor):
+ """
+ See L{unittest.TestCase.visit}.
+ """
+ visitor(self)
+
+
+
+class TestLoader(object):
+ """
+ I find tests inside function, modules, files -- whatever -- then return
+ them wrapped inside a Test (either a L{TestSuite} or a L{TestCase}).
+
+ @ivar methodPrefix: A string prefix. C{TestLoader} will assume that all the
+ methods in a class that begin with C{methodPrefix} are test cases.
+
+ @ivar modulePrefix: A string prefix. Every module in a package that begins
+ with C{modulePrefix} is considered a module full of tests.
+
+ @ivar forceGarbageCollection: A flag applied to each C{TestCase} loaded.
+ See L{unittest.TestCase} for more information.
+
+ @ivar sorter: A key function used to sort C{TestCase}s, test classes,
+ modules and packages.
+
+ @ivar suiteFactory: A callable which is passed a list of tests (which
+ themselves may be suites of tests). Must return a test suite.
+ """
+
+ methodPrefix = 'test'
+ modulePrefix = 'test_'
+
+ def __init__(self):
+ self.suiteFactory = TestSuite
+ self.sorter = name
+ self._importErrors = []
+
+ def sort(self, xs):
+ """
+ Sort the given things using L{sorter}.
+
+ @param xs: A list of test cases, class or modules.
+ """
+ return sorted(xs, key=self.sorter)
+
+ def findTestClasses(self, module):
+ """Given a module, return all Trial test classes"""
+ classes = []
+ for name, val in inspect.getmembers(module):
+ if isTestCase(val):
+ classes.append(val)
+ return self.sort(classes)
+
+ def findByName(self, name):
+ """
+ Return a Python object given a string describing it.
+
+ @param name: a string which may be either a filename or a
+ fully-qualified Python name.
+
+ @return: If C{name} is a filename, return the module. If C{name} is a
+ fully-qualified Python name, return the object it refers to.
+ """
+ if os.path.exists(name):
+ return filenameToModule(name)
+ return reflect.namedAny(name)
+
+ def loadModule(self, module):
+ """
+ Return a test suite with all the tests from a module.
+
+ Included are TestCase subclasses and doctests listed in the module's
+ __doctests__ module. If that's not good for you, put a function named
+ either C{testSuite} or C{test_suite} in your module that returns a
+ TestSuite, and I'll use the results of that instead.
+
+ If C{testSuite} and C{test_suite} are both present, then I'll use
+ C{testSuite}.
+ """
+ ## XXX - should I add an optional parameter to disable the check for
+ ## a custom suite.
+ ## OR, should I add another method
+ if not isinstance(module, types.ModuleType):
+ raise TypeError("%r is not a module" % (module,))
+ if hasattr(module, 'testSuite'):
+ return module.testSuite()
+ elif hasattr(module, 'test_suite'):
+ return module.test_suite()
+ suite = self.suiteFactory()
+ for testClass in self.findTestClasses(module):
+ suite.addTest(self.loadClass(testClass))
+ if not hasattr(module, '__doctests__'):
+ return suite
+ docSuite = self.suiteFactory()
+ for doctest in module.__doctests__:
+ docSuite.addTest(self.loadDoctests(doctest))
+ return self.suiteFactory([suite, docSuite])
+ loadTestsFromModule = loadModule
+
+ def loadClass(self, klass):
+ """
+ Given a class which contains test cases, return a sorted list of
+ C{TestCase} instances.
+ """
+ if not (isinstance(klass, type) or isinstance(klass, types.ClassType)):
+ raise TypeError("%r is not a class" % (klass,))
+ if not isTestCase(klass):
+ raise ValueError("%r is not a test case" % (klass,))
+ names = self.getTestCaseNames(klass)
+ tests = self.sort([self._makeCase(klass, self.methodPrefix+name)
+ for name in names])
+ return self.suiteFactory(tests)
+ loadTestsFromTestCase = loadClass
+
+ def getTestCaseNames(self, klass):
+ """
+ Given a class that contains C{TestCase}s, return a list of names of
+ methods that probably contain tests.
+ """
+ return reflect.prefixedMethodNames(klass, self.methodPrefix)
+
+ def loadMethod(self, method):
+ """
+ Given a method of a C{TestCase} that represents a test, return a
+ C{TestCase} instance for that test.
+ """
+ if not isinstance(method, types.MethodType):
+ raise TypeError("%r not a method" % (method,))
+ return self._makeCase(method.im_class, _getMethodNameInClass(method))
+
+ def _makeCase(self, klass, methodName):
+ return klass(methodName)
+
+ def loadPackage(self, package, recurse=False):
+ """
+ Load tests from a module object representing a package, and return a
+ TestSuite containing those tests.
+
+ Tests are only loaded from modules whose name begins with 'test_'
+ (or whatever C{modulePrefix} is set to).
+
+ @param package: a types.ModuleType object (or reasonable facsimilie
+ obtained by importing) which may contain tests.
+
+ @param recurse: A boolean. If True, inspect modules within packages
+ within the given package (and so on), otherwise, only inspect modules
+ in the package itself.
+
+ @raise: TypeError if 'package' is not a package.
+
+ @return: a TestSuite created with my suiteFactory, containing all the
+ tests.
+ """
+ if not isPackage(package):
+ raise TypeError("%r is not a package" % (package,))
+ pkgobj = modules.getModule(package.__name__)
+ if recurse:
+ discovery = pkgobj.walkModules()
+ else:
+ discovery = pkgobj.iterModules()
+ discovered = []
+ for disco in discovery:
+ if disco.name.split(".")[-1].startswith(self.modulePrefix):
+ discovered.append(disco)
+ suite = self.suiteFactory()
+ for modinfo in self.sort(discovered):
+ try:
+ module = modinfo.load()
+ except:
+ thingToAdd = ErrorHolder(modinfo.name, failure.Failure())
+ else:
+ thingToAdd = self.loadModule(module)
+ suite.addTest(thingToAdd)
+ return suite
+
+ def loadDoctests(self, module):
+ """
+ Return a suite of tests for all the doctests defined in C{module}.
+
+ @param module: A module object or a module name.
+ """
+ if isinstance(module, str):
+ try:
+ module = reflect.namedAny(module)
+ except:
+ return ErrorHolder(module, failure.Failure())
+ if not inspect.ismodule(module):
+ warnings.warn("trial only supports doctesting modules")
+ return
+ extraArgs = {}
+ if sys.version_info > (2, 4):
+ # Work around Python issue2604: DocTestCase.tearDown clobbers globs
+ def saveGlobals(test):
+ """
+ Save C{test.globs} and replace it with a copy so that if
+ necessary, the original will be available for the next test
+ run.
+ """
+ test._savedGlobals = getattr(test, '_savedGlobals', test.globs)
+ test.globs = test._savedGlobals.copy()
+ extraArgs['setUp'] = saveGlobals
+ return doctest.DocTestSuite(module, **extraArgs)
+
+ def loadAnything(self, thing, recurse=False):
+ """
+ Given a Python object, return whatever tests that are in it. Whatever
+ 'in' might mean.
+
+ @param thing: A Python object. A module, method, class or package.
+ @param recurse: Whether or not to look in subpackages of packages.
+ Defaults to False.
+
+ @return: A C{TestCase} or C{TestSuite}.
+ """
+ if isinstance(thing, types.ModuleType):
+ if isPackage(thing):
+ return self.loadPackage(thing, recurse)
+ return self.loadModule(thing)
+ elif isinstance(thing, types.ClassType):
+ return self.loadClass(thing)
+ elif isinstance(thing, type):
+ return self.loadClass(thing)
+ elif isinstance(thing, types.MethodType):
+ return self.loadMethod(thing)
+ raise TypeError("No loader for %r. Unrecognized type" % (thing,))
+
+ def loadByName(self, name, recurse=False):
+ """
+ Given a string representing a Python object, return whatever tests
+ are in that object.
+
+ If C{name} is somehow inaccessible (e.g. the module can't be imported,
+ there is no Python object with that name etc) then return an
+ L{ErrorHolder}.
+
+ @param name: The fully-qualified name of a Python object.
+ """
+ try:
+ thing = self.findByName(name)
+ except:
+ return ErrorHolder(name, failure.Failure())
+ return self.loadAnything(thing, recurse)
+ loadTestsFromName = loadByName
+
+ def loadByNames(self, names, recurse=False):
+ """
+ Construct a TestSuite containing all the tests found in 'names', where
+ names is a list of fully qualified python names and/or filenames. The
+ suite returned will have no duplicate tests, even if the same object
+ is named twice.
+ """
+ things = []
+ errors = []
+ for name in names:
+ try:
+ things.append(self.findByName(name))
+ except:
+ errors.append(ErrorHolder(name, failure.Failure()))
+ suites = [self.loadAnything(thing, recurse)
+ for thing in self._uniqueTests(things)]
+ suites.extend(errors)
+ return self.suiteFactory(suites)
+
+
+ def _uniqueTests(self, things):
+ """
+ Gather unique suite objects from loaded things. This will guarantee
+ uniqueness of inherited methods on TestCases which would otherwise hash
+ to same value and collapse to one test unexpectedly if using simpler
+ means: e.g. set().
+ """
+ entries = []
+ for thing in things:
+ if isinstance(thing, types.MethodType):
+ entries.append((thing, thing.im_class))
+ else:
+ entries.append((thing,))
+ return [entry[0] for entry in set(entries)]
+
+
+
+class DryRunVisitor(object):
+ """
+ A visitor that makes a reporter think that every test visited has run
+ successfully.
+ """
+
+ def __init__(self, reporter):
+ """
+ @param reporter: A C{TestResult} object.
+ """
+ self.reporter = reporter
+
+
+ def markSuccessful(self, testCase):
+ """
+ Convince the reporter that this test has been run successfully.
+ """
+ self.reporter.startTest(testCase)
+ self.reporter.addSuccess(testCase)
+ self.reporter.stopTest(testCase)
+
+
+
+class TrialRunner(object):
+ """
+ A specialised runner that the trial front end uses.
+ """
+
+ DEBUG = 'debug'
+ DRY_RUN = 'dry-run'
+
+ def _getDebugger(self):
+ dbg = pdb.Pdb()
+ try:
+ import readline
+ except ImportError:
+ print "readline module not available"
+ sys.exc_clear()
+ for path in ('.pdbrc', 'pdbrc'):
+ if os.path.exists(path):
+ try:
+ rcFile = file(path, 'r')
+ except IOError:
+ sys.exc_clear()
+ else:
+ dbg.rcLines.extend(rcFile.readlines())
+ return dbg
+
+
+ def _setUpTestdir(self):
+ self._tearDownLogFile()
+ currentDir = os.getcwd()
+ base = filepath.FilePath(self.workingDirectory)
+ testdir, self._testDirLock = util._unusedTestDirectory(base)
+ os.chdir(testdir.path)
+ return currentDir
+
+
+ def _tearDownTestdir(self, oldDir):
+ os.chdir(oldDir)
+ self._testDirLock.unlock()
+
+
+ _log = log
+ def _makeResult(self):
+ reporter = self.reporterFactory(self.stream, self.tbformat,
+ self.rterrors, self._log)
+ if self.uncleanWarnings:
+ reporter = UncleanWarningsReporterWrapper(reporter)
+ return reporter
+
+ def __init__(self, reporterFactory,
+ mode=None,
+ logfile='test.log',
+ stream=sys.stdout,
+ profile=False,
+ tracebackFormat='default',
+ realTimeErrors=False,
+ uncleanWarnings=False,
+ workingDirectory=None,
+ forceGarbageCollection=False):
+ self.reporterFactory = reporterFactory
+ self.logfile = logfile
+ self.mode = mode
+ self.stream = stream
+ self.tbformat = tracebackFormat
+ self.rterrors = realTimeErrors
+ self.uncleanWarnings = uncleanWarnings
+ self._result = None
+ self.workingDirectory = workingDirectory or '_trial_temp'
+ self._logFileObserver = None
+ self._logFileObject = None
+ self._forceGarbageCollection = forceGarbageCollection
+ if profile:
+ self.run = util.profiled(self.run, 'profile.data')
+
+ def _tearDownLogFile(self):
+ if self._logFileObserver is not None:
+ log.removeObserver(self._logFileObserver.emit)
+ self._logFileObserver = None
+ if self._logFileObject is not None:
+ self._logFileObject.close()
+ self._logFileObject = None
+
+ def _setUpLogFile(self):
+ self._tearDownLogFile()
+ if self.logfile == '-':
+ logFile = sys.stdout
+ else:
+ logFile = file(self.logfile, 'a')
+ self._logFileObject = logFile
+ self._logFileObserver = log.FileLogObserver(logFile)
+ log.startLoggingWithObserver(self._logFileObserver.emit, 0)
+
+
+ def run(self, test):
+ """
+ Run the test or suite and return a result object.
+ """
+ test = unittest.decorate(test, ITestCase)
+ if self._forceGarbageCollection:
+ test = unittest.decorate(
+ test, unittest._ForceGarbageCollectionDecorator)
+ return self._runWithoutDecoration(test)
+
+
+ def _runWithoutDecoration(self, test):
+ """
+ Private helper that runs the given test but doesn't decorate it.
+ """
+ result = self._makeResult()
+ # decorate the suite with reactor cleanup and log starting
+ # This should move out of the runner and be presumed to be
+ # present
+ suite = TrialSuite([test])
+ startTime = time.time()
+ if self.mode == self.DRY_RUN:
+ for single in unittest._iterateTests(suite):
+ result.startTest(single)
+ result.addSuccess(single)
+ result.stopTest(single)
+ else:
+ if self.mode == self.DEBUG:
+ # open question - should this be self.debug() instead.
+ debugger = self._getDebugger()
+ run = lambda: debugger.runcall(suite.run, result)
+ else:
+ run = lambda: suite.run(result)
+
+ oldDir = self._setUpTestdir()
+ try:
+ self._setUpLogFile()
+ run()
+ finally:
+ self._tearDownLogFile()
+ self._tearDownTestdir(oldDir)
+
+ endTime = time.time()
+ done = getattr(result, 'done', None)
+ if done is None:
+ warnings.warn(
+ "%s should implement done() but doesn't. Falling back to "
+ "printErrors() and friends." % reflect.qual(result.__class__),
+ category=DeprecationWarning, stacklevel=3)
+ result.printErrors()
+ result.writeln(result.separator)
+ result.writeln('Ran %d tests in %.3fs', result.testsRun,
+ endTime - startTime)
+ result.write('\n')
+ result.printSummary()
+ else:
+ result.done()
+ return result
+
+
+ def runUntilFailure(self, test):
+ """
+ Repeatedly run C{test} until it fails.
+ """
+ count = 0
+ while True:
+ count += 1
+ self.stream.write("Test Pass %d\n" % (count,))
+ if count == 1:
+ result = self.run(test)
+ else:
+ result = self._runWithoutDecoration(test)
+ if result.testsRun == 0:
+ break
+ if not result.wasSuccessful():
+ break
+ return result
diff --git a/twisted/trial/test/__init__.py b/twisted/trial/test/__init__.py
new file mode 100644
index 0000000..e239537
--- /dev/null
+++ b/twisted/trial/test/__init__.py
@@ -0,0 +1 @@
+"""unittesting framework tests"""
diff --git a/twisted/trial/test/detests.py b/twisted/trial/test/detests.py
new file mode 100644
index 0000000..b131bda
--- /dev/null
+++ b/twisted/trial/test/detests.py
@@ -0,0 +1,195 @@
+from __future__ import generators
+from twisted.trial import unittest
+from twisted.internet import defer, threads, reactor
+
+
+class DeferredSetUpOK(unittest.TestCase):
+ def setUp(self):
+ d = defer.succeed('value')
+ d.addCallback(self._cb_setUpCalled)
+ return d
+
+ def _cb_setUpCalled(self, ignored):
+ self._setUpCalled = True
+
+ def test_ok(self):
+ self.failUnless(self._setUpCalled)
+
+
+class DeferredSetUpFail(unittest.TestCase):
+ testCalled = False
+
+ def setUp(self):
+ return defer.fail(unittest.FailTest('i fail'))
+
+ def test_ok(self):
+ DeferredSetUpFail.testCalled = True
+ self.fail("I should not get called")
+
+
+class DeferredSetUpCallbackFail(unittest.TestCase):
+ testCalled = False
+
+ def setUp(self):
+ d = defer.succeed('value')
+ d.addCallback(self._cb_setUpCalled)
+ return d
+
+ def _cb_setUpCalled(self, ignored):
+ self.fail('deliberate failure')
+
+ def test_ok(self):
+ DeferredSetUpCallbackFail.testCalled = True
+
+
+class DeferredSetUpError(unittest.TestCase):
+ testCalled = False
+
+ def setUp(self):
+ return defer.fail(RuntimeError('deliberate error'))
+
+ def test_ok(self):
+ DeferredSetUpError.testCalled = True
+
+
+class DeferredSetUpNeverFire(unittest.TestCase):
+ testCalled = False
+
+ def setUp(self):
+ return defer.Deferred()
+
+ def test_ok(self):
+ DeferredSetUpNeverFire.testCalled = True
+
+
+class DeferredSetUpSkip(unittest.TestCase):
+ testCalled = False
+
+ def setUp(self):
+ d = defer.succeed('value')
+ d.addCallback(self._cb1)
+ return d
+
+ def _cb1(self, ignored):
+ raise unittest.SkipTest("skip me")
+
+ def test_ok(self):
+ DeferredSetUpSkip.testCalled = True
+
+
+class DeferredTests(unittest.TestCase):
+ touched = False
+
+ def _cb_fail(self, reason):
+ self.fail(reason)
+
+ def _cb_error(self, reason):
+ raise RuntimeError(reason)
+
+ def _cb_skip(self, reason):
+ raise unittest.SkipTest(reason)
+
+ def _touchClass(self, ignored):
+ self.__class__.touched = True
+
+ def setUp(self):
+ self.__class__.touched = False
+
+ def test_pass(self):
+ return defer.succeed('success')
+
+ def test_passGenerated(self):
+ self._touchClass(None)
+ yield None
+ test_passGenerated = defer.deferredGenerator(test_passGenerated)
+
+ def test_fail(self):
+ return defer.fail(self.failureException('I fail'))
+
+ def test_failureInCallback(self):
+ d = defer.succeed('fail')
+ d.addCallback(self._cb_fail)
+ return d
+
+ def test_errorInCallback(self):
+ d = defer.succeed('error')
+ d.addCallback(self._cb_error)
+ return d
+
+ def test_skip(self):
+ d = defer.succeed('skip')
+ d.addCallback(self._cb_skip)
+ d.addCallback(self._touchClass)
+ return d
+
+ def test_thread(self):
+ return threads.deferToThread(lambda : None)
+
+ def test_expectedFailure(self):
+ d = defer.succeed('todo')
+ d.addCallback(self._cb_error)
+ return d
+ test_expectedFailure.todo = "Expected failure"
+
+
+class TimeoutTests(unittest.TestCase):
+ timedOut = None
+
+ def test_pass(self):
+ d = defer.Deferred()
+ reactor.callLater(0, d.callback, 'hoorj!')
+ return d
+ test_pass.timeout = 2
+
+ def test_passDefault(self):
+ # test default timeout
+ d = defer.Deferred()
+ reactor.callLater(0, d.callback, 'hoorj!')
+ return d
+
+ def test_timeout(self):
+ return defer.Deferred()
+ test_timeout.timeout = 0.1
+
+ def test_timeoutZero(self):
+ return defer.Deferred()
+ test_timeoutZero.timeout = 0
+
+ def test_expectedFailure(self):
+ return defer.Deferred()
+ test_expectedFailure.timeout = 0.1
+ test_expectedFailure.todo = "i will get it right, eventually"
+
+ def test_skip(self):
+ return defer.Deferred()
+ test_skip.timeout = 0.1
+ test_skip.skip = "i will get it right, eventually"
+
+ def test_errorPropagation(self):
+ def timedOut(err):
+ self.__class__.timedOut = err
+ return err
+ d = defer.Deferred()
+ d.addErrback(timedOut)
+ return d
+ test_errorPropagation.timeout = 0.1
+
+ def test_calledButNeverCallback(self):
+ d = defer.Deferred()
+ def neverFire(r):
+ return defer.Deferred()
+ d.addCallback(neverFire)
+ d.callback(1)
+ return d
+ test_calledButNeverCallback.timeout = 0.1
+
+
+class TestClassTimeoutAttribute(unittest.TestCase):
+ timeout = 0.2
+
+ def setUp(self):
+ self.d = defer.Deferred()
+
+ def testMethod(self):
+ self.methodCalled = True
+ return self.d
diff --git a/twisted/trial/test/erroneous.py b/twisted/trial/test/erroneous.py
new file mode 100644
index 0000000..e1fd21c
--- /dev/null
+++ b/twisted/trial/test/erroneous.py
@@ -0,0 +1,130 @@
+# -*- test-case-name: twisted.trial.test.test_tests -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest, util
+from twisted.internet import reactor, protocol, defer
+
+
+class FoolishError(Exception):
+ pass
+
+
+class TestFailureInSetUp(unittest.TestCase):
+ def setUp(self):
+ raise FoolishError, "I am a broken setUp method"
+
+ def test_noop(self):
+ pass
+
+
+class TestFailureInTearDown(unittest.TestCase):
+ def tearDown(self):
+ raise FoolishError, "I am a broken tearDown method"
+
+ def test_noop(self):
+ pass
+
+
+class TestRegularFail(unittest.TestCase):
+ def test_fail(self):
+ self.fail("I fail")
+
+ def test_subfail(self):
+ self.subroutine()
+
+ def subroutine(self):
+ self.fail("I fail inside")
+
+class TestFailureInDeferredChain(unittest.TestCase):
+ def test_fail(self):
+ d = defer.Deferred()
+ d.addCallback(self._later)
+ reactor.callLater(0, d.callback, None)
+ return d
+ def _later(self, res):
+ self.fail("I fail later")
+
+
+
+class ErrorTest(unittest.TestCase):
+ """
+ A test case which has a L{test_foo} which will raise an error.
+
+ @ivar ran: boolean indicating whether L{test_foo} has been run.
+ """
+ ran = False
+
+ def test_foo(self):
+ """
+ Set C{self.ran} to True and raise a C{ZeroDivisionError}
+ """
+ self.ran = True
+ 1/0
+
+
+
+class TestSkipTestCase(unittest.TestCase):
+ pass
+
+TestSkipTestCase.skip = "skipping this test"
+
+
+class DelayedCall(unittest.TestCase):
+ hiddenExceptionMsg = "something blew up"
+
+ def go(self):
+ raise RuntimeError(self.hiddenExceptionMsg)
+
+ def testHiddenException(self):
+ """
+ What happens if an error is raised in a DelayedCall and an error is
+ also raised in the test?
+
+ L{test_reporter.TestErrorReporting.testHiddenException} checks that
+ both errors get reported.
+
+ Note that this behaviour is deprecated. A B{real} test would return a
+ Deferred that got triggered by the callLater. This would guarantee the
+ delayed call error gets reported.
+ """
+ reactor.callLater(0, self.go)
+ reactor.iterate(0.01)
+ self.fail("Deliberate failure to mask the hidden exception")
+ testHiddenException.suppress = [util.suppress(
+ message=r'reactor\.iterate cannot be used.*',
+ category=DeprecationWarning)]
+
+
+class ReactorCleanupTests(unittest.TestCase):
+ def test_leftoverPendingCalls(self):
+ def _():
+ print 'foo!'
+ reactor.callLater(10000.0, _)
+
+class SocketOpenTest(unittest.TestCase):
+ def test_socketsLeftOpen(self):
+ f = protocol.Factory()
+ f.protocol = protocol.Protocol
+ reactor.listenTCP(0, f)
+
+class TimingOutDeferred(unittest.TestCase):
+ def test_alpha(self):
+ pass
+
+ def test_deferredThatNeverFires(self):
+ self.methodCalled = True
+ d = defer.Deferred()
+ return d
+
+ def test_omega(self):
+ pass
+
+
+def unexpectedException(self):
+ """i will raise an unexpected exception...
+ ... *CAUSE THAT'S THE KINDA GUY I AM*
+
+ >>> 1/0
+ """
+
diff --git a/twisted/trial/test/mockcustomsuite.py b/twisted/trial/test/mockcustomsuite.py
new file mode 100644
index 0000000..89ad162
--- /dev/null
+++ b/twisted/trial/test/mockcustomsuite.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
+
+"""
+Mock test module that contains a C{test_suite} method. L{runner.TestLoader}
+should load the tests from the C{test_suite}, not from the C{Foo} C{TestCase}.
+
+See {twisted.trial.test.test_loader.LoaderTest.test_loadModuleWith_test_suite}.
+"""
+
+
+from twisted.trial import unittest, runner
+
+class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+
+
+def test_suite():
+ ts = runner.TestSuite()
+ ts.name = "MyCustomSuite"
+ return ts
diff --git a/twisted/trial/test/mockcustomsuite2.py b/twisted/trial/test/mockcustomsuite2.py
new file mode 100644
index 0000000..6d05457
--- /dev/null
+++ b/twisted/trial/test/mockcustomsuite2.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
+
+"""
+Mock test module that contains a C{testSuite} method. L{runner.TestLoader}
+should load the tests from the C{testSuite}, not from the C{Foo} C{TestCase}.
+
+See L{twisted.trial.test.test_loader.LoaderTest.test_loadModuleWith_testSuite}.
+"""
+
+
+from twisted.trial import unittest, runner
+
+class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+
+
+def testSuite():
+ ts = runner.TestSuite()
+ ts.name = "MyCustomSuite"
+ return ts
diff --git a/twisted/trial/test/mockcustomsuite3.py b/twisted/trial/test/mockcustomsuite3.py
new file mode 100644
index 0000000..c5b89d4
--- /dev/null
+++ b/twisted/trial/test/mockcustomsuite3.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2006 Twisted Matrix Laboratories. See LICENSE for details
+
+"""
+Mock test module that contains both a C{test_suite} and a C{testSuite} method.
+L{runner.TestLoader} should load the tests from the C{testSuite}, not from the
+C{Foo} C{TestCase} nor from the C{test_suite} method.
+
+See {twisted.trial.test.test_loader.LoaderTest.test_loadModuleWithBothCustom}.
+"""
+
+
+from twisted.trial import unittest, runner
+
+class Foo(unittest.TestCase):
+ def test_foo(self):
+ pass
+
+
+def test_suite():
+ ts = runner.TestSuite()
+ ts.name = "test_suite"
+ return ts
+
+
+def testSuite():
+ ts = runner.TestSuite()
+ ts.name = "testSuite"
+ return ts
diff --git a/twisted/trial/test/mockdoctest.py b/twisted/trial/test/mockdoctest.py
new file mode 100644
index 0000000..6d5bce7
--- /dev/null
+++ b/twisted/trial/test/mockdoctest.py
@@ -0,0 +1,104 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# this module is a trivial class with doctests and a __test__ attribute
+# to test trial's doctest support with python2.4
+
+
+class Counter(object):
+ """a simple counter object for testing trial's doctest support
+
+ >>> c = Counter()
+ >>> c.value()
+ 0
+ >>> c += 3
+ >>> c.value()
+ 3
+ >>> c.incr()
+ >>> c.value() == 4
+ True
+ >>> c == 4
+ True
+ >>> c != 9
+ True
+
+ """
+ _count = 0
+
+ def __init__(self, initialValue=0, maxval=None):
+ self._count = initialValue
+ self.maxval = maxval
+
+ def __iadd__(self, other):
+ """add other to my value and return self
+
+ >>> c = Counter(100)
+ >>> c += 333
+ >>> c == 433
+ True
+ """
+ if self.maxval is not None and ((self._count + other) > self.maxval):
+ raise ValueError, "sorry, counter got too big"
+ else:
+ self._count += other
+ return self
+
+ def __eq__(self, other):
+ """equality operator, compare other to my value()
+
+ >>> c = Counter()
+ >>> c == 0
+ True
+ >>> c += 10
+ >>> c.incr()
+ >>> c == 10 # fail this test on purpose
+ True
+
+ """
+ return self._count == other
+
+ def __ne__(self, other):
+ """inequality operator
+
+ >>> c = Counter()
+ >>> c != 10
+ True
+ """
+ return not self.__eq__(other)
+
+ def incr(self):
+ """increment my value by 1
+
+ >>> from twisted.trial.test.mockdoctest import Counter
+ >>> c = Counter(10, 11)
+ >>> c.incr()
+ >>> c.value() == 11
+ True
+ >>> c.incr()
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in ?
+ File "twisted/trial/test/mockdoctest.py", line 51, in incr
+ self.__iadd__(1)
+ File "twisted/trial/test/mockdoctest.py", line 39, in __iadd__
+ raise ValueError, "sorry, counter got too big"
+ ValueError: sorry, counter got too big
+ """
+ self.__iadd__(1)
+
+ def value(self):
+ """return this counter's value
+
+ >>> c = Counter(555)
+ >>> c.value() == 555
+ True
+ """
+ return self._count
+
+ def unexpectedException(self):
+ """i will raise an unexpected exception...
+ ... *CAUSE THAT'S THE KINDA GUY I AM*
+
+ >>> 1/0
+ """
+
+
diff --git a/twisted/trial/test/moduleself.py b/twisted/trial/test/moduleself.py
new file mode 100644
index 0000000..1f87c82
--- /dev/null
+++ b/twisted/trial/test/moduleself.py
@@ -0,0 +1,7 @@
+# -*- test-case-name: twisted.trial.test.moduleself -*-
+from twisted.trial import unittest
+
+class Foo(unittest.TestCase):
+
+ def testFoo(self):
+ pass
diff --git a/twisted/trial/test/moduletest.py b/twisted/trial/test/moduletest.py
new file mode 100644
index 0000000..c5e1d70
--- /dev/null
+++ b/twisted/trial/test/moduletest.py
@@ -0,0 +1,11 @@
+# -*- test-case-name: twisted.trial.test.test_test_visitor -*-
+
+# fodder for test_script, which parses files for emacs local variable
+# declarations. This one is supposed to have:
+# test-case-name: twisted.trial.test.test_test_visitor.
+# in the first line
+# The class declaration is irrelevant
+
+class Foo(object):
+ pass
+
diff --git a/twisted/trial/test/notpython b/twisted/trial/test/notpython
new file mode 100644
index 0000000..311485c
--- /dev/null
+++ b/twisted/trial/test/notpython
@@ -0,0 +1,2 @@
+
+this isn't python
diff --git a/twisted/trial/test/novars.py b/twisted/trial/test/novars.py
new file mode 100644
index 0000000..93bc03d
--- /dev/null
+++ b/twisted/trial/test/novars.py
@@ -0,0 +1,6 @@
+# fodder for test_script, which parses files for emacs local variable
+# declarations. This one is supposed to have none.
+# The class declaration is irrelevant
+
+class Bar(object):
+ pass
diff --git a/twisted/trial/test/packages.py b/twisted/trial/test/packages.py
new file mode 100644
index 0000000..7a18364
--- /dev/null
+++ b/twisted/trial/test/packages.py
@@ -0,0 +1,156 @@
+import sys, os
+from twisted.trial import unittest
+
+testModule = """
+from twisted.trial import unittest
+
+class FooTest(unittest.TestCase):
+ def testFoo(self):
+ pass
+"""
+
+dosModule = testModule.replace('\n', '\r\n')
+
+
+testSample = """
+'''This module is used by test_loader to test the Trial test loading
+functionality. Do NOT change the number of tests in this module.
+Do NOT change the names the tests in this module.
+'''
+
+import unittest as pyunit
+from twisted.trial import unittest
+
+class FooTest(unittest.TestCase):
+ def test_foo(self):
+ pass
+
+ def test_bar(self):
+ pass
+
+
+class PyunitTest(pyunit.TestCase):
+ def test_foo(self):
+ pass
+
+ def test_bar(self):
+ pass
+
+
+class NotATest(object):
+ def test_foo(self):
+ pass
+
+
+class AlphabetTest(unittest.TestCase):
+ def test_a(self):
+ pass
+
+ def test_b(self):
+ pass
+
+ def test_c(self):
+ pass
+"""
+
+testInheritanceSample = """
+'''This module is used by test_loader to test the Trial test loading
+functionality. Do NOT change the number of tests in this module.
+Do NOT change the names the tests in this module.
+'''
+
+from twisted.trial import unittest
+
+class X(object):
+
+ def test_foo(self):
+ pass
+
+class A(unittest.TestCase, X):
+ pass
+
+class B(unittest.TestCase, X):
+ pass
+
+"""
+
+class PackageTest(unittest.TestCase):
+ files = [
+ ('badpackage/__init__.py', 'frotz\n'),
+ ('badpackage/test_module.py', ''),
+ ('package2/__init__.py', ''),
+ ('package2/test_module.py', 'import frotz\n'),
+ ('package/__init__.py', ''),
+ ('package/frotz.py', 'frotz\n'),
+ ('package/test_bad_module.py',
+ 'raise ZeroDivisionError("fake error")'),
+ ('package/test_dos_module.py', dosModule),
+ ('package/test_import_module.py', 'import frotz'),
+ ('package/test_module.py', testModule),
+ ('goodpackage/__init__.py', ''),
+ ('goodpackage/test_sample.py', testSample),
+ ('goodpackage/sub/__init__.py', ''),
+ ('goodpackage/sub/test_sample.py', testSample),
+ ('inheritancepackage/__init__.py', ''),
+ ('inheritancepackage/test_x.py', testInheritanceSample),
+ ]
+
+ def _toModuleName(self, filename):
+ name = os.path.splitext(filename)[0]
+ segs = name.split('/')
+ if segs[-1] == '__init__':
+ segs = segs[:-1]
+ return '.'.join(segs)
+
+ def getModules(self):
+ return map(self._toModuleName, zip(*self.files)[0])
+
+ def cleanUpModules(self):
+ modules = self.getModules()
+ modules.sort()
+ modules.reverse()
+ for module in modules:
+ try:
+ del sys.modules[module]
+ except KeyError:
+ pass
+
+ def createFiles(self, files, parentDir='.'):
+ for filename, contents in self.files:
+ filename = os.path.join(parentDir, filename)
+ self._createDirectory(filename)
+ fd = open(filename, 'w')
+ fd.write(contents)
+ fd.close()
+
+ def _createDirectory(self, filename):
+ directory = os.path.dirname(filename)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ def setUp(self, parentDir=None):
+ if parentDir is None:
+ parentDir = self.mktemp()
+ self.parent = parentDir
+ self.createFiles(self.files, parentDir)
+
+ def tearDown(self):
+ self.cleanUpModules()
+
+class SysPathManglingTest(PackageTest):
+ def setUp(self, parent=None):
+ self.oldPath = sys.path[:]
+ self.newPath = sys.path[:]
+ if parent is None:
+ parent = self.mktemp()
+ PackageTest.setUp(self, parent)
+ self.newPath.append(self.parent)
+ self.mangleSysPath(self.newPath)
+
+ def tearDown(self):
+ PackageTest.tearDown(self)
+ self.mangleSysPath(self.oldPath)
+
+ def mangleSysPath(self, pathVar):
+ sys.path[:] = pathVar
+
diff --git a/twisted/trial/test/sample.py b/twisted/trial/test/sample.py
new file mode 100644
index 0000000..d864419
--- /dev/null
+++ b/twisted/trial/test/sample.py
@@ -0,0 +1,108 @@
+"""This module is used by test_loader to test the Trial test loading
+functionality. Do NOT change the number of tests in this module. Do NOT change
+the names the tests in this module.
+"""
+
+import unittest as pyunit
+from twisted.trial import unittest
+from twisted.python.util import mergeFunctionMetadata
+
+
+
+class FooTest(unittest.TestCase):
+
+
+ def test_foo(self):
+ pass
+
+
+ def test_bar(self):
+ pass
+
+
+
+def badDecorator(fn):
+ """
+ Decorate a function without preserving the name of the original function.
+ Always return a function with the same name.
+ """
+ def nameCollision(*args, **kwargs):
+ return fn(*args, **kwargs)
+ return nameCollision
+
+
+
+def goodDecorator(fn):
+ """
+ Decorate a function and preserve the original name.
+ """
+ def nameCollision(*args, **kwargs):
+ return fn(*args, **kwargs)
+ return mergeFunctionMetadata(fn, nameCollision)
+
+
+
+class DecorationTest(unittest.TestCase):
+ def test_badDecorator(self):
+ """
+ This test method is decorated in a way that gives it a confusing name
+ that collides with another method.
+ """
+ test_badDecorator = badDecorator(test_badDecorator)
+
+
+ def test_goodDecorator(self):
+ """
+ This test method is decorated in a way that preserves its name.
+ """
+ test_goodDecorator = goodDecorator(test_goodDecorator)
+
+
+ def renamedDecorator(self):
+ """
+ This is secretly a test method and will be decorated and then renamed so
+ test discovery can find it.
+ """
+ test_renamedDecorator = goodDecorator(renamedDecorator)
+
+
+ def nameCollision(self):
+ """
+ This isn't a test, it's just here to collide with tests.
+ """
+
+
+
+class PyunitTest(pyunit.TestCase):
+
+
+ def test_foo(self):
+ pass
+
+
+ def test_bar(self):
+ pass
+
+
+
+class NotATest(object):
+
+
+ def test_foo(self):
+ pass
+
+
+
+class AlphabetTest(unittest.TestCase):
+
+
+ def test_a(self):
+ pass
+
+
+ def test_b(self):
+ pass
+
+
+ def test_c(self):
+ pass
diff --git a/twisted/trial/test/scripttest.py b/twisted/trial/test/scripttest.py
new file mode 100644
index 0000000..267c189
--- /dev/null
+++ b/twisted/trial/test/scripttest.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- test-case-name: twisted.trial.test.test_test_visitor,twisted.trial.test.test_class -*-
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# fodder for test_script, which parses files for emacs local variable
+# declarations. This one is supposed to have:
+# test-case-name: twisted.trial.test.test_test_visitor
+# in the second line
+# The class declaration is irrelevant
+
+class Foo(object):
+ pass
diff --git a/twisted/trial/test/suppression.py b/twisted/trial/test/suppression.py
new file mode 100644
index 0000000..826fcdb
--- /dev/null
+++ b/twisted/trial/test/suppression.py
@@ -0,0 +1,57 @@
+# -*- test-case-name: twisted.trial.test.test_tests -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases used to make sure that warning supression works at the module,
+method, and class levels.
+"""
+
+import warnings
+
+from twisted.trial import unittest, util
+
+
+
+METHOD_WARNING_MSG = "method warning message"
+CLASS_WARNING_MSG = "class warning message"
+MODULE_WARNING_MSG = "module warning message"
+
+class MethodWarning(Warning):
+ pass
+
+class ClassWarning(Warning):
+ pass
+
+class ModuleWarning(Warning):
+ pass
+
+class EmitMixin:
+ def _emit(self):
+ warnings.warn(METHOD_WARNING_MSG, MethodWarning)
+ warnings.warn(CLASS_WARNING_MSG, ClassWarning)
+ warnings.warn(MODULE_WARNING_MSG, ModuleWarning)
+
+
+class TestSuppression(unittest.TestCase, EmitMixin):
+ def testSuppressMethod(self):
+ self._emit()
+ testSuppressMethod.suppress = [util.suppress(message=METHOD_WARNING_MSG)]
+
+ def testSuppressClass(self):
+ self._emit()
+
+ def testOverrideSuppressClass(self):
+ self._emit()
+ testOverrideSuppressClass.suppress = []
+
+TestSuppression.suppress = [util.suppress(message=CLASS_WARNING_MSG)]
+
+
+class TestSuppression2(unittest.TestCase, EmitMixin):
+ def testSuppressModule(self):
+ self._emit()
+
+suppress = [util.suppress(message=MODULE_WARNING_MSG)]
+
+
diff --git a/twisted/trial/test/test_assertions.py b/twisted/trial/test/test_assertions.py
new file mode 100644
index 0000000..c720a64
--- /dev/null
+++ b/twisted/trial/test/test_assertions.py
@@ -0,0 +1,817 @@
+# Copyright (c) 2001-2011 Twisted Matrix Laboratories.
+# See LICENSE for details
+
+"""
+Tests for assertions provided by L{twisted.trial.unittest.TestCase}.
+"""
+
+import warnings
+from pprint import pformat
+
+from twisted.python import reflect, failure
+from twisted.python.deprecate import deprecated, getVersionString
+from twisted.python.versions import Version
+from twisted.internet import defer
+from twisted.trial import unittest, runner, reporter
+
+class MockEquality(object):
+ def __init__(self, name):
+ self.name = name
+
+ def __repr__(self):
+ return "MockEquality(%s)" % (self.name,)
+
+ def __eq__(self, other):
+ if not hasattr(other, 'name'):
+ raise ValueError("%r not comparable to %r" % (other, self))
+ return self.name[0] == other.name[0]
+
+
+class TestAssertions(unittest.TestCase):
+ """
+ Tests for TestCase's assertion methods. That is, failUnless*,
+ failIf*, assert*.
+
+ Note: As of 11.2, assertEqual is preferred over the failUnlessEqual(s)
+ variants. Tests have been modified to reflect this preference.
+
+ This is pretty paranoid. Still, a certain paranoia is healthy if you
+ are testing a unit testing framework.
+ """
+
+ class FailingTest(unittest.TestCase):
+ def test_fails(self):
+ raise self.failureException()
+
+ def testFail(self):
+ try:
+ self.fail("failed")
+ except self.failureException, e:
+ if not str(e) == 'failed':
+ raise self.failureException("Exception had msg %s instead of %s"
+ % str(e), 'failed')
+ else:
+ raise self.failureException("Call to self.fail() didn't fail test")
+
+ def test_failingException_fails(self):
+ test = runner.TestLoader().loadClass(TestAssertions.FailingTest)
+ result = reporter.TestResult()
+ test.run(result)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.errors, [])
+ self.assertEqual(len(result.failures), 1)
+
+ def test_failIf(self):
+ for notTrue in [0, 0.0, False, None, (), []]:
+ self.failIf(notTrue, "failed on %r" % (notTrue,))
+ for true in [1, True, 'cat', [1,2], (3,4)]:
+ try:
+ self.failIf(true, "failed on %r" % (true,))
+ except self.failureException, e:
+ self.assertEqual(str(e), "failed on %r" % (true,))
+ else:
+ self.fail("Call to failIf(%r) didn't fail" % (true,))
+
+ def test_failUnless(self):
+ for notTrue in [0, 0.0, False, None, (), []]:
+ try:
+ self.failUnless(notTrue, "failed on %r" % (notTrue,))
+ except self.failureException, e:
+ self.assertEqual(str(e), "failed on %r" % (notTrue,))
+ else:
+ self.fail("Call to failUnless(%r) didn't fail" % (notTrue,))
+ for true in [1, True, 'cat', [1,2], (3,4)]:
+ self.failUnless(true, "failed on %r" % (true,))
+
+
+ def _testEqualPair(self, first, second):
+ x = self.assertEqual(first, second)
+ if x != first:
+ self.fail("assertEqual should return first parameter")
+
+
+ def _testUnequalPair(self, first, second):
+ try:
+ self.assertEqual(first, second)
+ except self.failureException, e:
+ expected = 'not equal:\na = %s\nb = %s\n' % (
+ pformat(first), pformat(second))
+ if str(e) != expected:
+ self.fail("Expected: %r; Got: %s" % (expected, str(e)))
+ else:
+ self.fail("Call to assertEqual(%r, %r) didn't fail"
+ % (first, second))
+
+
+ def test_assertEqual_basic(self):
+ self._testEqualPair('cat', 'cat')
+ self._testUnequalPair('cat', 'dog')
+ self._testEqualPair([1], [1])
+ self._testUnequalPair([1], 'orange')
+
+
+ def test_assertEqual_custom(self):
+ x = MockEquality('first')
+ y = MockEquality('second')
+ z = MockEquality('fecund')
+ self._testEqualPair(x, x)
+ self._testEqualPair(x, z)
+ self._testUnequalPair(x, y)
+ self._testUnequalPair(y, z)
+
+
+ def test_assertEqualMessage(self):
+ """
+ When a message is passed to L{assertEqual}, it is included in the
+ error message.
+ """
+ exception = self.assertRaises(
+ self.failureException, self.assertEqual,
+ 'foo', 'bar', 'message')
+ self.assertEqual(
+ str(exception),
+ "message\nnot equal:\na = 'foo'\nb = 'bar'\n")
+
+
+ def test_assertEqualNoneMessage(self):
+ """
+ If a message is specified as C{None}, it is not included in the error
+ message of L{assertEqual}.
+ """
+ exception = self.assertRaises(
+ self.failureException, self.assertEqual, 'foo', 'bar', None)
+ self.assertEqual(str(exception), "not equal:\na = 'foo'\nb = 'bar'\n")
+
+
+ def test_assertEqual_incomparable(self):
+ apple = MockEquality('apple')
+ orange = ['orange']
+ try:
+ self.assertEqual(apple, orange)
+ except self.failureException:
+ self.fail("Fail raised when ValueError ought to have been raised.")
+ except ValueError:
+ # good. error not swallowed
+ pass
+ else:
+ self.fail("Comparing %r and %r should have raised an exception"
+ % (apple, orange))
+
+
+ def _raiseError(self, error):
+ raise error
+
+ def test_failUnlessRaises_expected(self):
+ x = self.failUnlessRaises(ValueError, self._raiseError, ValueError)
+ self.failUnless(isinstance(x, ValueError),
+ "Expect failUnlessRaises to return instance of raised "
+ "exception.")
+
+ def test_failUnlessRaises_unexpected(self):
+ try:
+ self.failUnlessRaises(ValueError, self._raiseError, TypeError)
+ except TypeError:
+ self.fail("failUnlessRaises shouldn't re-raise unexpected "
+ "exceptions")
+ except self.failureException:
+ # what we expect
+ pass
+ else:
+ self.fail("Expected exception wasn't raised. Should have failed")
+
+ def test_failUnlessRaises_noException(self):
+ try:
+ self.failUnlessRaises(ValueError, lambda : None)
+ except self.failureException, e:
+ self.assertEqual(str(e),
+ 'ValueError not raised (None returned)')
+ else:
+ self.fail("Exception not raised. Should have failed")
+
+ def test_failUnlessRaises_failureException(self):
+ x = self.failUnlessRaises(self.failureException, self._raiseError,
+ self.failureException)
+ self.failUnless(isinstance(x, self.failureException),
+ "Expected %r instance to be returned"
+ % (self.failureException,))
+ try:
+ x = self.failUnlessRaises(self.failureException, self._raiseError,
+ ValueError)
+ except self.failureException:
+ # what we expect
+ pass
+ else:
+ self.fail("Should have raised exception")
+
+ def test_failIfEqual_basic(self):
+ x, y, z = [1], [2], [1]
+ ret = self.failIfEqual(x, y)
+ self.assertEqual(ret, x,
+ "failIfEqual should return first parameter")
+ self.failUnlessRaises(self.failureException,
+ self.failIfEqual, x, x)
+ self.failUnlessRaises(self.failureException,
+ self.failIfEqual, x, z)
+
+ def test_failIfEqual_customEq(self):
+ x = MockEquality('first')
+ y = MockEquality('second')
+ z = MockEquality('fecund')
+ ret = self.failIfEqual(x, y)
+ self.assertEqual(ret, x,
+ "failIfEqual should return first parameter")
+ self.failUnlessRaises(self.failureException,
+ self.failIfEqual, x, x)
+ # test when __ne__ is not defined
+ self.failIfEqual(x, z, "__ne__ not defined, so not equal")
+
+ def test_failUnlessIdentical(self):
+ x, y, z = [1], [1], [2]
+ ret = self.failUnlessIdentical(x, x)
+ self.assertEqual(ret, x,
+ 'failUnlessIdentical should return first '
+ 'parameter')
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessIdentical, x, y)
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessIdentical, x, z)
+
+ def test_failUnlessApproximates(self):
+ x, y, z = 1.0, 1.1, 1.2
+ self.failUnlessApproximates(x, x, 0.2)
+ ret = self.failUnlessApproximates(x, y, 0.2)
+ self.assertEqual(ret, x, "failUnlessApproximates should return "
+ "first parameter")
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessApproximates, x, z, 0.1)
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessApproximates, x, y, 0.1)
+
+ def test_failUnlessAlmostEqual(self):
+ precision = 5
+ x = 8.000001
+ y = 8.00001
+ z = 8.000002
+ self.failUnlessAlmostEqual(x, x, precision)
+ ret = self.failUnlessAlmostEqual(x, z, precision)
+ self.assertEqual(ret, x, "failUnlessAlmostEqual should return "
+ "first parameter (%r, %r)" % (ret, x))
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessAlmostEqual, x, y, precision)
+
+ def test_failIfAlmostEqual(self):
+ precision = 5
+ x = 8.000001
+ y = 8.00001
+ z = 8.000002
+ ret = self.failIfAlmostEqual(x, y, precision)
+ self.assertEqual(ret, x, "failIfAlmostEqual should return "
+ "first parameter (%r, %r)" % (ret, x))
+ self.failUnlessRaises(self.failureException,
+ self.failIfAlmostEqual, x, x, precision)
+ self.failUnlessRaises(self.failureException,
+ self.failIfAlmostEqual, x, z, precision)
+
+ def test_failUnlessSubstring(self):
+ x = "cat"
+ y = "the dog sat"
+ z = "the cat sat"
+ self.failUnlessSubstring(x, x)
+ ret = self.failUnlessSubstring(x, z)
+ self.assertEqual(ret, x, 'should return first parameter')
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessSubstring, x, y)
+ self.failUnlessRaises(self.failureException,
+ self.failUnlessSubstring, z, x)
+
+ def test_failIfSubstring(self):
+ x = "cat"
+ y = "the dog sat"
+ z = "the cat sat"
+ self.failIfSubstring(z, x)
+ ret = self.failIfSubstring(x, y)
+ self.assertEqual(ret, x, 'should return first parameter')
+ self.failUnlessRaises(self.failureException,
+ self.failIfSubstring, x, x)
+ self.failUnlessRaises(self.failureException,
+ self.failIfSubstring, x, z)
+
+ def test_assertFailure(self):
+ d = defer.maybeDeferred(lambda: 1/0)
+ return self.assertFailure(d, ZeroDivisionError)
+
+ def test_assertFailure_wrongException(self):
+ d = defer.maybeDeferred(lambda: 1/0)
+ self.assertFailure(d, OverflowError)
+ d.addCallbacks(lambda x: self.fail('Should have failed'),
+ lambda x: x.trap(self.failureException))
+ return d
+
+ def test_assertFailure_noException(self):
+ d = defer.succeed(None)
+ self.assertFailure(d, ZeroDivisionError)
+ d.addCallbacks(lambda x: self.fail('Should have failed'),
+ lambda x: x.trap(self.failureException))
+ return d
+
+ def test_assertFailure_moreInfo(self):
+ """
+ In the case of assertFailure failing, check that we get lots of
+ information about the exception that was raised.
+ """
+ try:
+ 1/0
+ except ZeroDivisionError:
+ f = failure.Failure()
+ d = defer.fail(f)
+ d = self.assertFailure(d, RuntimeError)
+ d.addErrback(self._checkInfo, f)
+ return d
+
+ def _checkInfo(self, assertionFailure, f):
+ assert assertionFailure.check(self.failureException)
+ output = assertionFailure.getErrorMessage()
+ self.assertIn(f.getErrorMessage(), output)
+ self.assertIn(f.getBriefTraceback(), output)
+
+ def test_assertFailure_masked(self):
+ """
+ A single wrong assertFailure should fail the whole test.
+ """
+ class ExampleFailure(Exception):
+ pass
+
+ class TC(unittest.TestCase):
+ failureException = ExampleFailure
+ def test_assertFailure(self):
+ d = defer.maybeDeferred(lambda: 1/0)
+ self.assertFailure(d, OverflowError)
+ self.assertFailure(d, ZeroDivisionError)
+ return d
+
+ test = TC('test_assertFailure')
+ result = reporter.TestResult()
+ test.run(result)
+ self.assertEqual(1, len(result.failures))
+
+
+ def test_assertWarns(self):
+ """
+ Test basic assertWarns report.
+ """
+ def deprecated(a):
+ warnings.warn("Woo deprecated", category=DeprecationWarning)
+ return a
+ r = self.assertWarns(DeprecationWarning, "Woo deprecated", __file__,
+ deprecated, 123)
+ self.assertEqual(r, 123)
+
+
+ def test_assertWarnsRegistryClean(self):
+ """
+ Test that assertWarns cleans the warning registry, so the warning is
+ not swallowed the second time.
+ """
+ def deprecated(a):
+ warnings.warn("Woo deprecated", category=DeprecationWarning)
+ return a
+ r1 = self.assertWarns(DeprecationWarning, "Woo deprecated", __file__,
+ deprecated, 123)
+ self.assertEqual(r1, 123)
+ # The warning should be raised again
+ r2 = self.assertWarns(DeprecationWarning, "Woo deprecated", __file__,
+ deprecated, 321)
+ self.assertEqual(r2, 321)
+
+
+ def test_assertWarnsError(self):
+ """
+ Test assertWarns failure when no warning is generated.
+ """
+ def normal(a):
+ return a
+ self.assertRaises(self.failureException,
+ self.assertWarns, DeprecationWarning, "Woo deprecated", __file__,
+ normal, 123)
+
+
+ def test_assertWarnsWrongCategory(self):
+ """
+ Test assertWarns failure when the category is wrong.
+ """
+ def deprecated(a):
+ warnings.warn("Foo deprecated", category=DeprecationWarning)
+ return a
+ self.assertRaises(self.failureException,
+ self.assertWarns, UserWarning, "Foo deprecated", __file__,
+ deprecated, 123)
+
+
+ def test_assertWarnsWrongMessage(self):
+ """
+ Test assertWarns failure when the message is wrong.
+ """
+ def deprecated(a):
+ warnings.warn("Foo deprecated", category=DeprecationWarning)
+ return a
+ self.assertRaises(self.failureException,
+ self.assertWarns, DeprecationWarning, "Bar deprecated", __file__,
+ deprecated, 123)
+
+
+ def test_assertWarnsWrongFile(self):
+ """
+ If the warning emitted by a function refers to a different file than is
+ passed to C{assertWarns}, C{failureException} is raised.
+ """
+ def deprecated(a):
+ # stacklevel=2 points at the direct caller of the function. The
+ # way assertRaises is invoked below, the direct caller will be
+ # something somewhere in trial, not something in this file. In
+ # Python 2.5 and earlier, stacklevel of 0 resulted in a warning
+ # pointing to the warnings module itself. Starting in Python 2.6,
+ # stacklevel of 0 and 1 both result in a warning pointing to *this*
+ # file, presumably due to the fact that the warn function is
+ # implemented in C and has no convenient Python
+ # filename/linenumber.
+ warnings.warn(
+ "Foo deprecated", category=DeprecationWarning, stacklevel=2)
+ self.assertRaises(
+ self.failureException,
+ # Since the direct caller isn't in this file, try to assert that
+ # the warning *does* point to this file, so that assertWarns raises
+ # an exception.
+ self.assertWarns, DeprecationWarning, "Foo deprecated", __file__,
+ deprecated, 123)
+
+ def test_assertWarnsOnClass(self):
+ """
+ Test assertWarns works when creating a class instance.
+ """
+ class Warn:
+ def __init__(self):
+ warnings.warn("Do not call me", category=RuntimeWarning)
+ r = self.assertWarns(RuntimeWarning, "Do not call me", __file__,
+ Warn)
+ self.assertTrue(isinstance(r, Warn))
+ r = self.assertWarns(RuntimeWarning, "Do not call me", __file__,
+ Warn)
+ self.assertTrue(isinstance(r, Warn))
+
+
+ def test_assertWarnsOnMethod(self):
+ """
+ Test assertWarns works when used on an instance method.
+ """
+ class Warn:
+ def deprecated(self, a):
+ warnings.warn("Bar deprecated", category=DeprecationWarning)
+ return a
+ w = Warn()
+ r = self.assertWarns(DeprecationWarning, "Bar deprecated", __file__,
+ w.deprecated, 321)
+ self.assertEqual(r, 321)
+ r = self.assertWarns(DeprecationWarning, "Bar deprecated", __file__,
+ w.deprecated, 321)
+ self.assertEqual(r, 321)
+
+
+ def test_assertWarnsOnCall(self):
+ """
+ Test assertWarns works on instance with C{__call__} method.
+ """
+ class Warn:
+ def __call__(self, a):
+ warnings.warn("Egg deprecated", category=DeprecationWarning)
+ return a
+ w = Warn()
+ r = self.assertWarns(DeprecationWarning, "Egg deprecated", __file__,
+ w, 321)
+ self.assertEqual(r, 321)
+ r = self.assertWarns(DeprecationWarning, "Egg deprecated", __file__,
+ w, 321)
+ self.assertEqual(r, 321)
+
+
+ def test_assertWarnsFilter(self):
+ """
+ Test assertWarns on a warning filterd by default.
+ """
+ def deprecated(a):
+ warnings.warn("Woo deprecated", category=PendingDeprecationWarning)
+ return a
+ r = self.assertWarns(PendingDeprecationWarning, "Woo deprecated",
+ __file__, deprecated, 123)
+ self.assertEqual(r, 123)
+
+
+ def test_assertWarnsMultipleWarnings(self):
+ """
+ C{assertWarns} does not raise an exception if the function it is passed
+ triggers the same warning more than once.
+ """
+ def deprecated():
+ warnings.warn("Woo deprecated", category=PendingDeprecationWarning)
+ def f():
+ deprecated()
+ deprecated()
+ self.assertWarns(
+ PendingDeprecationWarning, "Woo deprecated", __file__, f)
+
+
+ def test_assertWarnsDifferentWarnings(self):
+ """
+ For now, assertWarns is unable to handle multiple different warnings,
+ so it should raise an exception if it's the case.
+ """
+ def deprecated(a):
+ warnings.warn("Woo deprecated", category=DeprecationWarning)
+ warnings.warn("Another one", category=PendingDeprecationWarning)
+ e = self.assertRaises(self.failureException,
+ self.assertWarns, DeprecationWarning, "Woo deprecated",
+ __file__, deprecated, 123)
+ self.assertEqual(str(e), "Can't handle different warnings")
+
+
+ def test_assertWarnsAfterUnassertedWarning(self):
+ """
+ Warnings emitted before L{TestCase.assertWarns} is called do not get
+ flushed and do not alter the behavior of L{TestCase.assertWarns}.
+ """
+ class TheWarning(Warning):
+ pass
+
+ def f(message):
+ warnings.warn(message, category=TheWarning)
+ f("foo")
+ self.assertWarns(TheWarning, "bar", __file__, f, "bar")
+ [warning] = self.flushWarnings([f])
+ self.assertEqual(warning['message'], "foo")
+
+
+ def test_assertIsInstance(self):
+ """
+ Test a true condition of assertIsInstance.
+ """
+ A = type('A', (object,), {})
+ a = A()
+ self.assertIsInstance(a, A)
+
+ def test_assertIsInstanceMultipleClasses(self):
+ """
+ Test a true condition of assertIsInstance with multiple classes.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ a = A()
+ self.assertIsInstance(a, (A, B))
+
+ def test_assertIsInstanceError(self):
+ """
+ Test an error with assertIsInstance.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ a = A()
+ self.assertRaises(self.failureException, self.assertIsInstance, a, B)
+
+ def test_assertIsInstanceErrorMultipleClasses(self):
+ """
+ Test an error with assertIsInstance and multiple classes.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ C = type('C', (object,), {})
+ a = A()
+ self.assertRaises(self.failureException, self.assertIsInstance, a, (B, C))
+
+
+ def test_assertIsInstanceCustomMessage(self):
+ """
+ If L{TestCase.assertIsInstance} is passed a custom message as its 3rd
+ argument, the message is included in the failure exception raised when
+ the assertion fails.
+ """
+ exc = self.assertRaises(
+ self.failureException,
+ self.assertIsInstance, 3, str, "Silly assertion")
+ self.assertIn("Silly assertion", str(exc))
+
+
+ def test_assertNotIsInstance(self):
+ """
+ Test a true condition of assertNotIsInstance.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ a = A()
+ self.assertNotIsInstance(a, B)
+
+ def test_assertNotIsInstanceMultipleClasses(self):
+ """
+ Test a true condition of assertNotIsInstance and multiple classes.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ C = type('C', (object,), {})
+ a = A()
+ self.assertNotIsInstance(a, (B, C))
+
+ def test_assertNotIsInstanceError(self):
+ """
+ Test an error with assertNotIsInstance.
+ """
+ A = type('A', (object,), {})
+ a = A()
+ error = self.assertRaises(self.failureException,
+ self.assertNotIsInstance, a, A)
+ self.assertEqual(str(error), "%r is an instance of %s" % (a, A))
+
+
+ def test_assertNotIsInstanceErrorMultipleClasses(self):
+ """
+ Test an error with assertNotIsInstance and multiple classes.
+ """
+ A = type('A', (object,), {})
+ B = type('B', (object,), {})
+ a = A()
+ self.assertRaises(self.failureException, self.assertNotIsInstance, a, (A, B))
+
+
+ def test_assertDictEqual(self):
+ """
+ L{twisted.trial.unittest.TestCase} supports the C{assertDictEqual}
+ method inherited from the standard library in Python 2.7.
+ """
+ self.assertDictEqual({'a': 1}, {'a': 1})
+ if getattr(unittest.TestCase, 'assertDictEqual', None) is None:
+ test_assertDictEqual.skip = (
+ "assertDictEqual is not available on this version of Python")
+
+
+
+class TestAssertionNames(unittest.TestCase):
+ """
+ Tests for consistency of naming within TestCase assertion methods
+ """
+ def _getAsserts(self):
+ dct = {}
+ reflect.accumulateMethods(self, dct, 'assert')
+ return [ dct[k] for k in dct if not k.startswith('Not') and k != '_' ]
+
+ def _name(self, x):
+ return x.__name__
+
+
+ def test_failUnlessMatchesAssert(self):
+ """
+ The C{failUnless*} test methods are a subset of the C{assert*} test
+ methods. This is intended to ensure that methods using the
+ I{failUnless} naming scheme are not added without corresponding methods
+ using the I{assert} naming scheme. The I{assert} naming scheme is
+ preferred, and new I{assert}-prefixed methods may be added without
+ corresponding I{failUnless}-prefixed methods.
+ """
+ asserts = set(self._getAsserts())
+ failUnlesses = set(reflect.prefixedMethods(self, 'failUnless'))
+ self.assertEqual(
+ failUnlesses, asserts.intersection(failUnlesses))
+
+
+ def test_failIf_matches_assertNot(self):
+ asserts = reflect.prefixedMethods(unittest.TestCase, 'assertNot')
+ failIfs = reflect.prefixedMethods(unittest.TestCase, 'failIf')
+ self.assertEqual(sorted(asserts, key=self._name),
+ sorted(failIfs, key=self._name))
+
+ def test_equalSpelling(self):
+ for name, value in vars(self).items():
+ if not callable(value):
+ continue
+ if name.endswith('Equal'):
+ self.failUnless(hasattr(self, name+'s'),
+ "%s but no %ss" % (name, name))
+ self.assertEqual(value, getattr(self, name+'s'))
+ if name.endswith('Equals'):
+ self.failUnless(hasattr(self, name[:-1]),
+ "%s but no %s" % (name, name[:-1]))
+ self.assertEqual(value, getattr(self, name[:-1]))
+
+
+class TestCallDeprecated(unittest.TestCase):
+ """
+ Test use of the L{TestCase.callDeprecated} method with version objects.
+ """
+
+ version = Version('Twisted', 8, 0, 0)
+
+ def test_callDeprecatedSuppressesWarning(self):
+ """
+ callDeprecated calls a deprecated callable, suppressing the
+ deprecation warning.
+ """
+ self.callDeprecated(self.version, oldMethod, 'foo')
+ self.assertEqual(
+ self.flushWarnings(), [], "No warnings should be shown")
+
+
+ def test_callDeprecatedCallsFunction(self):
+ """
+ L{callDeprecated} actually calls the callable passed to it, and
+ forwards the result.
+ """
+ result = self.callDeprecated(self.version, oldMethod, 'foo')
+ self.assertEqual('foo', result)
+
+
+ def test_failsWithoutDeprecation(self):
+ """
+ L{callDeprecated} raises a test failure if the callable is not
+ deprecated.
+ """
+ def notDeprecated():
+ pass
+ exception = self.assertRaises(
+ self.failureException,
+ self.callDeprecated, self.version, notDeprecated)
+ self.assertEqual(
+ "%r is not deprecated." % notDeprecated, str(exception))
+
+
+ def test_failsWithIncorrectDeprecation(self):
+ """
+ callDeprecated raises a test failure if the callable was deprecated
+ at a different version to the one expected.
+ """
+ differentVersion = Version('Foo', 1, 2, 3)
+ exception = self.assertRaises(
+ self.failureException,
+ self.callDeprecated,
+ differentVersion, oldMethod, 'foo')
+ self.assertIn(getVersionString(self.version), str(exception))
+ self.assertIn(getVersionString(differentVersion), str(exception))
+
+
+ def test_nestedDeprecation(self):
+ """
+ L{callDeprecated} ignores all deprecations apart from the first.
+
+ Multiple warnings are generated when a deprecated function calls
+ another deprecated function. The first warning is the one generated by
+ the explicitly called function. That's the warning that we care about.
+ """
+ differentVersion = Version('Foo', 1, 2, 3)
+
+ def nestedDeprecation(*args):
+ return oldMethod(*args)
+ nestedDeprecation = deprecated(differentVersion)(nestedDeprecation)
+
+ self.callDeprecated(differentVersion, nestedDeprecation, 24)
+
+ # The oldMethod deprecation should have been emitted too, not captured
+ # by callDeprecated. Flush it now to make sure it did happen and to
+ # prevent it from showing up on stdout.
+ warningsShown = self.flushWarnings()
+ self.assertEqual(len(warningsShown), 1)
+
+
+ def test_callDeprecationWithMessage(self):
+ """
+ L{callDeprecated} can take a message argument used to check the warning
+ emitted.
+ """
+ self.callDeprecated((self.version, "newMethod"),
+ oldMethodReplaced, 1)
+
+
+ def test_callDeprecationWithWrongMessage(self):
+ """
+ If the message passed to L{callDeprecated} doesn't match,
+ L{callDeprecated} raises a test failure.
+ """
+ exception = self.assertRaises(
+ self.failureException,
+ self.callDeprecated,
+ (self.version, "something.wrong"),
+ oldMethodReplaced, 1)
+ self.assertIn(getVersionString(self.version), str(exception))
+ self.assertIn("please use newMethod instead", str(exception))
+
+
+
+
+@deprecated(TestCallDeprecated.version)
+def oldMethod(x):
+ """
+ Deprecated method for testing.
+ """
+ return x
+
+
+@deprecated(TestCallDeprecated.version, replacement="newMethod")
+def oldMethodReplaced(x):
+ """
+ Another deprecated method, which has been deprecated in favor of the
+ mythical 'newMethod'.
+ """
+ return 2 * x
diff --git a/twisted/trial/test/test_deferred.py b/twisted/trial/test/test_deferred.py
new file mode 100644
index 0000000..5ecc8ce
--- /dev/null
+++ b/twisted/trial/test/test_deferred.py
@@ -0,0 +1,220 @@
+from twisted.internet import defer
+from twisted.trial import unittest
+from twisted.trial import runner, reporter, util
+from twisted.trial.test import detests
+
+
+class TestSetUp(unittest.TestCase):
+ def _loadSuite(self, klass):
+ loader = runner.TestLoader()
+ r = reporter.TestResult()
+ s = loader.loadClass(klass)
+ return r, s
+
+ def test_success(self):
+ result, suite = self._loadSuite(detests.DeferredSetUpOK)
+ suite(result)
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+
+ def test_fail(self):
+ self.failIf(detests.DeferredSetUpFail.testCalled)
+ result, suite = self._loadSuite(detests.DeferredSetUpFail)
+ suite(result)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.errors), 1)
+ self.failIf(detests.DeferredSetUpFail.testCalled)
+
+ def test_callbackFail(self):
+ self.failIf(detests.DeferredSetUpCallbackFail.testCalled)
+ result, suite = self._loadSuite(detests.DeferredSetUpCallbackFail)
+ suite(result)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.errors), 1)
+ self.failIf(detests.DeferredSetUpCallbackFail.testCalled)
+
+ def test_error(self):
+ self.failIf(detests.DeferredSetUpError.testCalled)
+ result, suite = self._loadSuite(detests.DeferredSetUpError)
+ suite(result)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.errors), 1)
+ self.failIf(detests.DeferredSetUpError.testCalled)
+
+ def test_skip(self):
+ self.failIf(detests.DeferredSetUpSkip.testCalled)
+ result, suite = self._loadSuite(detests.DeferredSetUpSkip)
+ suite(result)
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.errors), 0)
+ self.assertEqual(len(result.skips), 1)
+ self.failIf(detests.DeferredSetUpSkip.testCalled)
+
+
+class TestNeverFire(unittest.TestCase):
+ def setUp(self):
+ self._oldTimeout = util.DEFAULT_TIMEOUT_DURATION
+ util.DEFAULT_TIMEOUT_DURATION = 0.1
+
+ def tearDown(self):
+ util.DEFAULT_TIMEOUT_DURATION = self._oldTimeout
+
+ def _loadSuite(self, klass):
+ loader = runner.TestLoader()
+ r = reporter.TestResult()
+ s = loader.loadClass(klass)
+ return r, s
+
+ def test_setUp(self):
+ self.failIf(detests.DeferredSetUpNeverFire.testCalled)
+ result, suite = self._loadSuite(detests.DeferredSetUpNeverFire)
+ suite(result)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.errors), 1)
+ self.failIf(detests.DeferredSetUpNeverFire.testCalled)
+ self.failUnless(result.errors[0][1].check(defer.TimeoutError))
+
+
+class TestTester(unittest.TestCase):
+ def getTest(self, name):
+ raise NotImplementedError("must override me")
+
+ def runTest(self, name):
+ result = reporter.TestResult()
+ self.getTest(name).run(result)
+ return result
+
+
+class TestDeferred(TestTester):
+ def getTest(self, name):
+ return detests.DeferredTests(name)
+
+ def test_pass(self):
+ result = self.runTest('test_pass')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+
+ def test_passGenerated(self):
+ result = self.runTest('test_passGenerated')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.failUnless(detests.DeferredTests.touched)
+
+ def test_fail(self):
+ result = self.runTest('test_fail')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 1)
+
+ def test_failureInCallback(self):
+ result = self.runTest('test_failureInCallback')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 1)
+
+ def test_errorInCallback(self):
+ result = self.runTest('test_errorInCallback')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.errors), 1)
+
+ def test_skip(self):
+ result = self.runTest('test_skip')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.skips), 1)
+ self.failIf(detests.DeferredTests.touched)
+
+ def test_todo(self):
+ result = self.runTest('test_expectedFailure')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.errors), 0)
+ self.assertEqual(len(result.failures), 0)
+ self.assertEqual(len(result.expectedFailures), 1)
+
+ def test_thread(self):
+ result = self.runTest('test_thread')
+ self.assertEqual(result.testsRun, 1)
+ self.failUnless(result.wasSuccessful(), result.errors)
+
+
+class TestTimeout(TestTester):
+ def getTest(self, name):
+ return detests.TimeoutTests(name)
+
+ def _wasTimeout(self, error):
+ self.assertEqual(error.check(defer.TimeoutError),
+ defer.TimeoutError)
+
+ def test_pass(self):
+ result = self.runTest('test_pass')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+
+ def test_passDefault(self):
+ result = self.runTest('test_passDefault')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+
+ def test_timeout(self):
+ result = self.runTest('test_timeout')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.errors), 1)
+ self._wasTimeout(result.errors[0][1])
+
+ def test_timeoutZero(self):
+ result = self.runTest('test_timeoutZero')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.errors), 1)
+ self._wasTimeout(result.errors[0][1])
+
+ def test_skip(self):
+ result = self.runTest('test_skip')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.skips), 1)
+
+ def test_todo(self):
+ result = self.runTest('test_expectedFailure')
+ self.failUnless(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.expectedFailures), 1)
+ self._wasTimeout(result.expectedFailures[0][1])
+
+ def test_errorPropagation(self):
+ result = self.runTest('test_errorPropagation')
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(result.testsRun, 1)
+ self._wasTimeout(detests.TimeoutTests.timedOut)
+
+ def test_classTimeout(self):
+ loader = runner.TestLoader()
+ suite = loader.loadClass(detests.TestClassTimeoutAttribute)
+ result = reporter.TestResult()
+ suite.run(result)
+ self.assertEqual(len(result.errors), 1)
+ self._wasTimeout(result.errors[0][1])
+
+ def test_callbackReturnsNonCallingDeferred(self):
+ #hacky timeout
+ # raises KeyboardInterrupt because Trial sucks
+ from twisted.internet import reactor
+ call = reactor.callLater(2, reactor.crash)
+ result = self.runTest('test_calledButNeverCallback')
+ if call.active():
+ call.cancel()
+ self.failIf(result.wasSuccessful())
+ self._wasTimeout(result.errors[0][1])
diff --git a/twisted/trial/test/test_doctest.py b/twisted/trial/test/test_doctest.py
new file mode 100644
index 0000000..4506bed
--- /dev/null
+++ b/twisted/trial/test/test_doctest.py
@@ -0,0 +1,64 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test Twisted's doctest support.
+"""
+
+from twisted.trial import itrial, runner, unittest, reporter
+from twisted.trial.test import mockdoctest
+
+
+class TestRunners(unittest.TestCase):
+ """
+ Tests for Twisted's doctest support.
+ """
+
+ def test_id(self):
+ """
+ Check that the id() of the doctests' case object contains the FQPN of
+ the actual tests. We need this because id() has weird behaviour w/
+ doctest in Python 2.3.
+ """
+ loader = runner.TestLoader()
+ suite = loader.loadDoctests(mockdoctest)
+ idPrefix = 'twisted.trial.test.mockdoctest.Counter'
+ for test in suite._tests:
+ self.assertIn(idPrefix, itrial.ITestCase(test).id())
+
+
+ def test_basicTrialIntegration(self):
+ """
+ L{loadDoctests} loads all of the doctests in the given module.
+ """
+ loader = runner.TestLoader()
+ suite = loader.loadDoctests(mockdoctest)
+ self.assertEqual(7, suite.countTestCases())
+
+
+ def _testRun(self, suite):
+ """
+ Run C{suite} and check the result.
+ """
+ result = reporter.TestResult()
+ suite.run(result)
+ self.assertEqual(5, result.successes)
+ # doctest reports failures as errors in 2.3
+ self.assertEqual(2, len(result.errors) + len(result.failures))
+
+
+ def test_expectedResults(self, count=1):
+ """
+ Trial can correctly run doctests with its xUnit test APIs.
+ """
+ suite = runner.TestLoader().loadDoctests(mockdoctest)
+ self._testRun(suite)
+
+
+ def test_repeatable(self):
+ """
+ Doctests should be runnable repeatably.
+ """
+ suite = runner.TestLoader().loadDoctests(mockdoctest)
+ self._testRun(suite)
+ self._testRun(suite)
diff --git a/twisted/trial/test/test_keyboard.py b/twisted/trial/test/test_keyboard.py
new file mode 100644
index 0000000..c5471a5
--- /dev/null
+++ b/twisted/trial/test/test_keyboard.py
@@ -0,0 +1,113 @@
+import StringIO
+from twisted.trial import unittest
+from twisted.trial import reporter, runner
+
+
+class TrialTest(unittest.TestCase):
+ def setUp(self):
+ self.output = StringIO.StringIO()
+ self.reporter = reporter.TestResult()
+ self.loader = runner.TestLoader()
+
+
+class TestInterruptInTest(TrialTest):
+ class InterruptedTest(unittest.TestCase):
+ def test_02_raiseInterrupt(self):
+ raise KeyboardInterrupt
+
+ def test_01_doNothing(self):
+ pass
+
+ def test_03_doNothing(self):
+ TestInterruptInTest.test_03_doNothing_run = True
+
+ def setUp(self):
+ super(TestInterruptInTest, self).setUp()
+ self.suite = self.loader.loadClass(TestInterruptInTest.InterruptedTest)
+ TestInterruptInTest.test_03_doNothing_run = None
+
+ def test_setUpOK(self):
+ self.assertEqual(3, self.suite.countTestCases())
+ self.assertEqual(0, self.reporter.testsRun)
+ self.failIf(self.reporter.shouldStop)
+
+ def test_interruptInTest(self):
+ runner.TrialSuite([self.suite]).run(self.reporter)
+ self.failUnless(self.reporter.shouldStop)
+ self.assertEqual(2, self.reporter.testsRun)
+ self.failIf(TestInterruptInTest.test_03_doNothing_run,
+ "test_03_doNothing ran.")
+
+
+class TestInterruptInSetUp(TrialTest):
+ testsRun = 0
+
+ class InterruptedTest(unittest.TestCase):
+ def setUp(self):
+ if TestInterruptInSetUp.testsRun > 0:
+ raise KeyboardInterrupt
+
+ def test_01(self):
+ TestInterruptInSetUp.testsRun += 1
+
+ def test_02(self):
+ TestInterruptInSetUp.testsRun += 1
+ TestInterruptInSetUp.test_02_run = True
+
+ def setUp(self):
+ super(TestInterruptInSetUp, self).setUp()
+ self.suite = self.loader.loadClass(
+ TestInterruptInSetUp.InterruptedTest)
+ TestInterruptInSetUp.test_02_run = False
+ TestInterruptInSetUp.testsRun = 0
+
+ def test_setUpOK(self):
+ self.assertEqual(0, TestInterruptInSetUp.testsRun)
+ self.assertEqual(2, self.suite.countTestCases())
+ self.assertEqual(0, self.reporter.testsRun)
+ self.failIf(self.reporter.shouldStop)
+
+ def test_interruptInSetUp(self):
+ runner.TrialSuite([self.suite]).run(self.reporter)
+ self.failUnless(self.reporter.shouldStop)
+ self.assertEqual(2, self.reporter.testsRun)
+ self.failIf(TestInterruptInSetUp.test_02_run,
+ "test_02 ran")
+
+
+class TestInterruptInTearDown(TrialTest):
+ testsRun = 0
+
+ class InterruptedTest(unittest.TestCase):
+ def tearDown(self):
+ if TestInterruptInTearDown.testsRun > 0:
+ raise KeyboardInterrupt
+
+ def test_01(self):
+ TestInterruptInTearDown.testsRun += 1
+
+ def test_02(self):
+ TestInterruptInTearDown.testsRun += 1
+ TestInterruptInTearDown.test_02_run = True
+
+ def setUp(self):
+ super(TestInterruptInTearDown, self).setUp()
+ self.suite = self.loader.loadClass(
+ TestInterruptInTearDown.InterruptedTest)
+ TestInterruptInTearDown.testsRun = 0
+ TestInterruptInTearDown.test_02_run = False
+
+ def test_setUpOK(self):
+ self.assertEqual(0, TestInterruptInTearDown.testsRun)
+ self.assertEqual(2, self.suite.countTestCases())
+ self.assertEqual(0, self.reporter.testsRun)
+ self.failIf(self.reporter.shouldStop)
+
+ def test_interruptInTearDown(self):
+ runner.TrialSuite([self.suite]).run(self.reporter)
+ self.assertEqual(1, self.reporter.testsRun)
+ self.failUnless(self.reporter.shouldStop)
+ self.failIf(TestInterruptInTearDown.test_02_run,
+ "test_02 ran")
+
+
diff --git a/twisted/trial/test/test_loader.py b/twisted/trial/test/test_loader.py
new file mode 100644
index 0000000..f08588e
--- /dev/null
+++ b/twisted/trial/test/test_loader.py
@@ -0,0 +1,611 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for loading tests by name.
+"""
+
+import os
+import shutil
+import sys
+
+from twisted.python import util
+from twisted.python.hashlib import md5
+from twisted.trial.test import packages
+from twisted.trial import runner, reporter, unittest
+from twisted.trial.itrial import ITestCase
+
+from twisted.python.modules import getModule
+
+
+
+def testNames(tests):
+ """
+ Return the id of each test within the given test suite or case.
+ """
+ names = []
+ for test in unittest._iterateTests(tests):
+ names.append(test.id())
+ return names
+
+
+
+class FinderTest(packages.PackageTest):
+ def setUp(self):
+ packages.PackageTest.setUp(self)
+ self.loader = runner.TestLoader()
+
+ def tearDown(self):
+ packages.PackageTest.tearDown(self)
+
+ def test_findPackage(self):
+ sample1 = self.loader.findByName('twisted')
+ import twisted as sample2
+ self.assertEqual(sample1, sample2)
+
+ def test_findModule(self):
+ sample1 = self.loader.findByName('twisted.trial.test.sample')
+ import sample as sample2
+ self.assertEqual(sample1, sample2)
+
+ def test_findFile(self):
+ path = util.sibpath(__file__, 'sample.py')
+ sample1 = self.loader.findByName(path)
+ import sample as sample2
+ self.assertEqual(sample1, sample2)
+
+ def test_findObject(self):
+ sample1 = self.loader.findByName('twisted.trial.test.sample.FooTest')
+ import sample
+ self.assertEqual(sample.FooTest, sample1)
+
+ def test_findNonModule(self):
+ self.failUnlessRaises(AttributeError,
+ self.loader.findByName,
+ 'twisted.trial.test.nonexistent')
+
+ def test_findNonPackage(self):
+ self.failUnlessRaises(ValueError,
+ self.loader.findByName,
+ 'nonextant')
+
+ def test_findNonFile(self):
+ path = util.sibpath(__file__, 'nonexistent.py')
+ self.failUnlessRaises(ValueError, self.loader.findByName, path)
+
+
+
+class FileTest(packages.SysPathManglingTest):
+ """
+ Tests for L{runner.filenameToModule}.
+ """
+ def test_notFile(self):
+ self.failUnlessRaises(ValueError,
+ runner.filenameToModule, 'doesntexist')
+
+ def test_moduleInPath(self):
+ sample1 = runner.filenameToModule(util.sibpath(__file__, 'sample.py'))
+ import sample as sample2
+ self.assertEqual(sample2, sample1)
+
+
+ def test_moduleNotInPath(self):
+ """
+ If passed the path to a file containing the implementation of a
+ module within a package which is not on the import path,
+ L{runner.filenameToModule} returns a module object loosely
+ resembling the module defined by that file anyway.
+ """
+ # "test_sample" isn't actually the name of this module. However,
+ # filenameToModule can't seem to figure that out. So clean up this
+ # mis-named module. It would be better if this weren't necessary
+ # and filenameToModule either didn't exist or added a correctly
+ # named module to sys.modules.
+ self.addCleanup(sys.modules.pop, 'test_sample', None)
+
+ self.mangleSysPath(self.oldPath)
+ sample1 = runner.filenameToModule(
+ os.path.join(self.parent, 'goodpackage', 'test_sample.py'))
+ self.mangleSysPath(self.newPath)
+ from goodpackage import test_sample as sample2
+ self.assertEqual(os.path.splitext(sample2.__file__)[0],
+ os.path.splitext(sample1.__file__)[0])
+
+
+ def test_packageInPath(self):
+ package1 = runner.filenameToModule(os.path.join(self.parent,
+ 'goodpackage'))
+ import goodpackage
+ self.assertEqual(goodpackage, package1)
+
+
+ def test_packageNotInPath(self):
+ """
+ If passed the path to a directory which represents a package which
+ is not on the import path, L{runner.filenameToModule} returns a
+ module object loosely resembling the package defined by that
+ directory anyway.
+ """
+ # "__init__" isn't actually the name of the package! However,
+ # filenameToModule is pretty stupid and decides that is its name
+ # after all. Make sure it gets cleaned up. See the comment in
+ # test_moduleNotInPath for possible courses of action related to
+ # this.
+ self.addCleanup(sys.modules.pop, "__init__")
+
+ self.mangleSysPath(self.oldPath)
+ package1 = runner.filenameToModule(
+ os.path.join(self.parent, 'goodpackage'))
+ self.mangleSysPath(self.newPath)
+ import goodpackage
+ self.assertEqual(os.path.splitext(goodpackage.__file__)[0],
+ os.path.splitext(package1.__file__)[0])
+
+
+ def test_directoryNotPackage(self):
+ self.failUnlessRaises(ValueError, runner.filenameToModule,
+ util.sibpath(__file__, 'directory'))
+
+ def test_filenameNotPython(self):
+ self.failUnlessRaises(ValueError, runner.filenameToModule,
+ util.sibpath(__file__, 'notpython.py'))
+
+ def test_filenameMatchesPackage(self):
+ filename = os.path.join(self.parent, 'goodpackage.py')
+ fd = open(filename, 'w')
+ fd.write(packages.testModule)
+ fd.close()
+ try:
+ module = runner.filenameToModule(filename)
+ self.assertEqual(filename, module.__file__)
+ finally:
+ os.remove(filename)
+
+ def test_directory(self):
+ """
+ Test loader against a filesystem directory. It should handle
+ 'path' and 'path/' the same way.
+ """
+ path = util.sibpath(__file__, 'goodDirectory')
+ os.mkdir(path)
+ f = file(os.path.join(path, '__init__.py'), "w")
+ f.close()
+ try:
+ module = runner.filenameToModule(path)
+ self.assert_(module.__name__.endswith('goodDirectory'))
+ module = runner.filenameToModule(path + os.path.sep)
+ self.assert_(module.__name__.endswith('goodDirectory'))
+ finally:
+ shutil.rmtree(path)
+
+
+
+class LoaderTest(packages.SysPathManglingTest):
+ """
+ Tests for L{trial.TestLoader}.
+ """
+
+ def setUp(self):
+ self.loader = runner.TestLoader()
+ packages.SysPathManglingTest.setUp(self)
+
+
+ def test_sortCases(self):
+ import sample
+ suite = self.loader.loadClass(sample.AlphabetTest)
+ self.assertEqual(['test_a', 'test_b', 'test_c'],
+ [test._testMethodName for test in suite._tests])
+ newOrder = ['test_b', 'test_c', 'test_a']
+ sortDict = dict(zip(newOrder, range(3)))
+ self.loader.sorter = lambda x : sortDict.get(x.shortDescription(), -1)
+ suite = self.loader.loadClass(sample.AlphabetTest)
+ self.assertEqual(newOrder,
+ [test._testMethodName for test in suite._tests])
+
+
+ def test_loadMethod(self):
+ import sample
+ suite = self.loader.loadMethod(sample.FooTest.test_foo)
+ self.assertEqual(1, suite.countTestCases())
+ self.assertEqual('test_foo', suite._testMethodName)
+
+
+ def test_loadFailingMethod(self):
+ # test added for issue1353
+ import erroneous
+ suite = self.loader.loadMethod(erroneous.TestRegularFail.test_fail)
+ result = reporter.TestResult()
+ suite.run(result)
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 1)
+
+
+ def test_loadNonMethod(self):
+ import sample
+ self.failUnlessRaises(TypeError, self.loader.loadMethod, sample)
+ self.failUnlessRaises(TypeError,
+ self.loader.loadMethod, sample.FooTest)
+ self.failUnlessRaises(TypeError, self.loader.loadMethod, "string")
+ self.failUnlessRaises(TypeError,
+ self.loader.loadMethod, ('foo', 'bar'))
+
+
+ def test_loadBadDecorator(self):
+ """
+ A decorated test method for which the decorator has failed to set the
+ method's __name__ correctly is loaded and its name in the class scope
+ discovered.
+ """
+ import sample
+ suite = self.loader.loadMethod(sample.DecorationTest.test_badDecorator)
+ self.assertEqual(1, suite.countTestCases())
+ self.assertEqual('test_badDecorator', suite._testMethodName)
+
+
+ def test_loadGoodDecorator(self):
+ """
+ A decorated test method for which the decorator has set the method's
+ __name__ correctly is loaded and the only name by which it goes is used.
+ """
+ import sample
+ suite = self.loader.loadMethod(
+ sample.DecorationTest.test_goodDecorator)
+ self.assertEqual(1, suite.countTestCases())
+ self.assertEqual('test_goodDecorator', suite._testMethodName)
+
+
+ def test_loadRenamedDecorator(self):
+ """
+ Load a decorated method which has been copied to a new name inside the
+ class. Thus its __name__ and its key in the class's __dict__ no
+ longer match.
+ """
+ import sample
+ suite = self.loader.loadMethod(
+ sample.DecorationTest.test_renamedDecorator)
+ self.assertEqual(1, suite.countTestCases())
+ self.assertEqual('test_renamedDecorator', suite._testMethodName)
+
+
+ def test_loadClass(self):
+ import sample
+ suite = self.loader.loadClass(sample.FooTest)
+ self.assertEqual(2, suite.countTestCases())
+ self.assertEqual(['test_bar', 'test_foo'],
+ [test._testMethodName for test in suite._tests])
+
+
+ def test_loadNonClass(self):
+ import sample
+ self.failUnlessRaises(TypeError, self.loader.loadClass, sample)
+ self.failUnlessRaises(TypeError,
+ self.loader.loadClass, sample.FooTest.test_foo)
+ self.failUnlessRaises(TypeError, self.loader.loadClass, "string")
+ self.failUnlessRaises(TypeError,
+ self.loader.loadClass, ('foo', 'bar'))
+
+
+ def test_loadNonTestCase(self):
+ import sample
+ self.failUnlessRaises(ValueError, self.loader.loadClass,
+ sample.NotATest)
+
+
+ def test_loadModule(self):
+ import sample
+ suite = self.loader.loadModule(sample)
+ self.assertEqual(10, suite.countTestCases())
+
+
+ def test_loadNonModule(self):
+ import sample
+ self.failUnlessRaises(TypeError,
+ self.loader.loadModule, sample.FooTest)
+ self.failUnlessRaises(TypeError,
+ self.loader.loadModule, sample.FooTest.test_foo)
+ self.failUnlessRaises(TypeError, self.loader.loadModule, "string")
+ self.failUnlessRaises(TypeError,
+ self.loader.loadModule, ('foo', 'bar'))
+
+
+ def test_loadPackage(self):
+ import goodpackage
+ suite = self.loader.loadPackage(goodpackage)
+ self.assertEqual(7, suite.countTestCases())
+
+
+ def test_loadNonPackage(self):
+ import sample
+ self.failUnlessRaises(TypeError,
+ self.loader.loadPackage, sample.FooTest)
+ self.failUnlessRaises(TypeError,
+ self.loader.loadPackage, sample.FooTest.test_foo)
+ self.failUnlessRaises(TypeError, self.loader.loadPackage, "string")
+ self.failUnlessRaises(TypeError,
+ self.loader.loadPackage, ('foo', 'bar'))
+
+
+ def test_loadModuleAsPackage(self):
+ import sample
+ ## XXX -- should this instead raise a ValueError? -- jml
+ self.failUnlessRaises(TypeError, self.loader.loadPackage, sample)
+
+
+ def test_loadPackageRecursive(self):
+ import goodpackage
+ suite = self.loader.loadPackage(goodpackage, recurse=True)
+ self.assertEqual(14, suite.countTestCases())
+
+
+ def test_loadAnythingOnModule(self):
+ import sample
+ suite = self.loader.loadAnything(sample)
+ self.assertEqual(sample.__name__,
+ suite._tests[0]._tests[0].__class__.__module__)
+
+
+ def test_loadAnythingOnClass(self):
+ import sample
+ suite = self.loader.loadAnything(sample.FooTest)
+ self.assertEqual(2, suite.countTestCases())
+
+
+ def test_loadAnythingOnMethod(self):
+ import sample
+ suite = self.loader.loadAnything(sample.FooTest.test_foo)
+ self.assertEqual(1, suite.countTestCases())
+
+
+ def test_loadAnythingOnPackage(self):
+ import goodpackage
+ suite = self.loader.loadAnything(goodpackage)
+ self.failUnless(isinstance(suite, self.loader.suiteFactory))
+ self.assertEqual(7, suite.countTestCases())
+
+
+ def test_loadAnythingOnPackageRecursive(self):
+ import goodpackage
+ suite = self.loader.loadAnything(goodpackage, recurse=True)
+ self.failUnless(isinstance(suite, self.loader.suiteFactory))
+ self.assertEqual(14, suite.countTestCases())
+
+
+ def test_loadAnythingOnString(self):
+ # the important thing about this test is not the string-iness
+ # but the non-handledness.
+ self.failUnlessRaises(TypeError,
+ self.loader.loadAnything, "goodpackage")
+
+
+ def test_importErrors(self):
+ import package
+ suite = self.loader.loadPackage(package, recurse=True)
+ result = reporter.Reporter()
+ suite.run(result)
+ self.assertEqual(False, result.wasSuccessful())
+ self.assertEqual(2, len(result.errors))
+ errors = [test.id() for test, error in result.errors]
+ errors.sort()
+ self.assertEqual(errors, ['package.test_bad_module',
+ 'package.test_import_module'])
+
+
+ def test_differentInstances(self):
+ """
+ L{TestLoader.loadClass} returns a suite with each test method
+ represented by a different instances of the L{TestCase} they are
+ defined on.
+ """
+ class DistinctInstances(unittest.TestCase):
+ def test_1(self):
+ self.first = 'test1Run'
+
+ def test_2(self):
+ self.assertFalse(hasattr(self, 'first'))
+
+ suite = self.loader.loadClass(DistinctInstances)
+ result = reporter.Reporter()
+ suite.run(result)
+ self.assertTrue(result.wasSuccessful())
+
+
+ def test_loadModuleWith_test_suite(self):
+ """
+ Check that C{test_suite} is used when present and other L{TestCase}s are
+ not included.
+ """
+ from twisted.trial.test import mockcustomsuite
+ suite = self.loader.loadModule(mockcustomsuite)
+ self.assertEqual(0, suite.countTestCases())
+ self.assertEqual("MyCustomSuite", getattr(suite, 'name', None))
+
+
+ def test_loadModuleWith_testSuite(self):
+ """
+ Check that C{testSuite} is used when present and other L{TestCase}s are
+ not included.
+ """
+ from twisted.trial.test import mockcustomsuite2
+ suite = self.loader.loadModule(mockcustomsuite2)
+ self.assertEqual(0, suite.countTestCases())
+ self.assertEqual("MyCustomSuite", getattr(suite, 'name', None))
+
+
+ def test_loadModuleWithBothCustom(self):
+ """
+ Check that if C{testSuite} and C{test_suite} are both present in a
+ module then C{testSuite} gets priority.
+ """
+ from twisted.trial.test import mockcustomsuite3
+ suite = self.loader.loadModule(mockcustomsuite3)
+ self.assertEqual('testSuite', getattr(suite, 'name', None))
+
+
+ def test_customLoadRaisesAttributeError(self):
+ """
+ Make sure that any C{AttributeError}s raised by C{testSuite} are not
+ swallowed by L{TestLoader}.
+ """
+ def testSuite():
+ raise AttributeError('should be reraised')
+ from twisted.trial.test import mockcustomsuite2
+ mockcustomsuite2.testSuite, original = (testSuite,
+ mockcustomsuite2.testSuite)
+ try:
+ self.assertRaises(AttributeError, self.loader.loadModule,
+ mockcustomsuite2)
+ finally:
+ mockcustomsuite2.testSuite = original
+
+
+ # XXX - duplicated and modified from test_script
+ def assertSuitesEqual(self, test1, test2):
+ names1 = testNames(test1)
+ names2 = testNames(test2)
+ names1.sort()
+ names2.sort()
+ self.assertEqual(names1, names2)
+
+
+ def test_loadByNamesDuplicate(self):
+ """
+ Check that loadByNames ignores duplicate names
+ """
+ module = 'twisted.trial.test.test_test_visitor'
+ suite1 = self.loader.loadByNames([module, module], True)
+ suite2 = self.loader.loadByName(module, True)
+ self.assertSuitesEqual(suite1, suite2)
+
+
+ def test_loadDifferentNames(self):
+ """
+ Check that loadByNames loads all the names that it is given
+ """
+ modules = ['goodpackage', 'package.test_module']
+ suite1 = self.loader.loadByNames(modules)
+ suite2 = runner.TestSuite(map(self.loader.loadByName, modules))
+ self.assertSuitesEqual(suite1, suite2)
+
+ def test_loadInheritedMethods(self):
+ """
+ Check that test methods names which are inherited from are all
+ loaded rather than just one.
+ """
+ methods = ['inheritancepackage.test_x.A.test_foo',
+ 'inheritancepackage.test_x.B.test_foo']
+ suite1 = self.loader.loadByNames(methods)
+ suite2 = runner.TestSuite(map(self.loader.loadByName, methods))
+ self.assertSuitesEqual(suite1, suite2)
+
+
+
+class ZipLoadingTest(LoaderTest):
+ def setUp(self):
+ from twisted.test.test_paths import zipit
+ LoaderTest.setUp(self)
+ zipit(self.parent, self.parent+'.zip')
+ self.parent += '.zip'
+ self.mangleSysPath(self.oldPath+[self.parent])
+
+
+
+class PackageOrderingTest(packages.SysPathManglingTest):
+ if sys.version_info < (2, 4):
+ skip = (
+ "Python 2.3 import semantics make this behavior incorrect on that "
+ "version of Python as well as difficult to test. The second "
+ "import of a package which raised an exception the first time it "
+ "was imported will succeed on Python 2.3, whereas it will fail on "
+ "later versions of Python. Trial does not account for this, so "
+ "this test fails with inconsistencies between the expected and "
+ "the received loader errors.")
+
+ def setUp(self):
+ self.loader = runner.TestLoader()
+ self.topDir = self.mktemp()
+ parent = os.path.join(self.topDir, "uberpackage")
+ os.makedirs(parent)
+ file(os.path.join(parent, "__init__.py"), "wb").close()
+ packages.SysPathManglingTest.setUp(self, parent)
+ self.mangleSysPath(self.oldPath + [self.topDir])
+
+ def _trialSortAlgorithm(self, sorter):
+ """
+ Right now, halfway by accident, trial sorts like this:
+
+ 1. all modules are grouped together in one list and sorted.
+
+ 2. within each module, the classes are grouped together in one list
+ and sorted.
+
+ 3. finally within each class, each test method is grouped together
+ in a list and sorted.
+
+ This attempts to return a sorted list of testable thingies following
+ those rules, so that we can compare the behavior of loadPackage.
+
+ The things that show as 'cases' are errors from modules which failed to
+ import, and test methods. Let's gather all those together.
+ """
+ pkg = getModule('uberpackage')
+ testModules = []
+ for testModule in pkg.walkModules():
+ if testModule.name.split(".")[-1].startswith("test_"):
+ testModules.append(testModule)
+ sortedModules = sorted(testModules, key=sorter) # ONE
+ for modinfo in sortedModules:
+ # Now let's find all the classes.
+ module = modinfo.load(None)
+ if module is None:
+ yield modinfo
+ else:
+ testClasses = []
+ for attrib in modinfo.iterAttributes():
+ if runner.isTestCase(attrib.load()):
+ testClasses.append(attrib)
+ sortedClasses = sorted(testClasses, key=sorter) # TWO
+ for clsinfo in sortedClasses:
+ testMethods = []
+ for attr in clsinfo.iterAttributes():
+ if attr.name.split(".")[-1].startswith('test'):
+ testMethods.append(attr)
+ sortedMethods = sorted(testMethods, key=sorter) # THREE
+ for methinfo in sortedMethods:
+ yield methinfo
+
+
+ def loadSortedPackages(self, sorter=runner.name):
+ """
+ Verify that packages are loaded in the correct order.
+ """
+ import uberpackage
+ self.loader.sorter = sorter
+ suite = self.loader.loadPackage(uberpackage, recurse=True)
+ # XXX: Work around strange, unexplained Zope crap.
+ # jml, 2007-11-15.
+ suite = unittest.decorate(suite, ITestCase)
+ resultingTests = list(unittest._iterateTests(suite))
+ manifest = list(self._trialSortAlgorithm(sorter))
+ for number, (manifestTest, actualTest) in enumerate(
+ zip(manifest, resultingTests)):
+ self.assertEqual(
+ manifestTest.name, actualTest.id(),
+ "#%d: %s != %s" %
+ (number, manifestTest.name, actualTest.id()))
+ self.assertEqual(len(manifest), len(resultingTests))
+
+
+ def test_sortPackagesDefaultOrder(self):
+ self.loadSortedPackages()
+
+
+ def test_sortPackagesSillyOrder(self):
+ def sillySorter(s):
+ # This has to work on fully-qualified class names and class
+ # objects, which is silly, but it's the "spec", such as it is.
+# if isinstance(s, type) or isinstance(s, types.ClassType):
+# return s.__module__+'.'+s.__name__
+ n = runner.name(s)
+ d = md5(n).hexdigest()
+ return d
+ self.loadSortedPackages(sillySorter)
diff --git a/twisted/trial/test/test_log.py b/twisted/trial/test/test_log.py
new file mode 100644
index 0000000..7afdcdf
--- /dev/null
+++ b/twisted/trial/test/test_log.py
@@ -0,0 +1,197 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test the interaction between trial and errors logged during test run.
+"""
+
+import time
+
+from twisted.internet import reactor, task
+from twisted.python import failure, log
+from twisted.trial import unittest, reporter
+
+
+def makeFailure():
+ """
+ Return a new, realistic failure.
+ """
+ try:
+ 1/0
+ except ZeroDivisionError:
+ f = failure.Failure()
+ return f
+
+
+class Mask(object):
+ """
+ Hide C{MockTest}s from Trial's automatic test finder.
+ """
+
+ class MockTest(unittest.TestCase):
+ def test_silent(self):
+ """
+ Don't log any errors.
+ """
+
+ def test_single(self):
+ """
+ Log a single error.
+ """
+ log.err(makeFailure())
+
+ def test_double(self):
+ """
+ Log two errors.
+ """
+ log.err(makeFailure())
+ log.err(makeFailure())
+
+ def test_inCallback(self):
+ """
+ Log an error in an asynchronous callback.
+ """
+ return task.deferLater(reactor, 0, lambda: log.err(makeFailure()))
+
+
+class TestObserver(unittest.TestCase):
+ """
+ Tests for L{unittest._LogObserver}, a helper for the implementation of
+ L{TestCase.flushLoggedErrors}.
+ """
+ def setUp(self):
+ self.result = reporter.TestResult()
+ self.observer = unittest._LogObserver()
+
+
+ def test_msg(self):
+ """
+ Test that a standard log message doesn't go anywhere near the result.
+ """
+ self.observer.gotEvent({'message': ('some message',),
+ 'time': time.time(), 'isError': 0,
+ 'system': '-'})
+ self.assertEqual(self.observer.getErrors(), [])
+
+
+ def test_error(self):
+ """
+ Test that an observed error gets added to the result
+ """
+ f = makeFailure()
+ self.observer.gotEvent({'message': (),
+ 'time': time.time(), 'isError': 1,
+ 'system': '-', 'failure': f,
+ 'why': None})
+ self.assertEqual(self.observer.getErrors(), [f])
+
+
+ def test_flush(self):
+ """
+ Check that flushing the observer with no args removes all errors.
+ """
+ self.test_error()
+ flushed = self.observer.flushErrors()
+ self.assertEqual(self.observer.getErrors(), [])
+ self.assertEqual(len(flushed), 1)
+ self.assertTrue(flushed[0].check(ZeroDivisionError))
+
+
+ def _makeRuntimeFailure(self):
+ return failure.Failure(RuntimeError('test error'))
+
+
+ def test_flushByType(self):
+ """
+ Check that flushing the observer remove all failures of the given type.
+ """
+ self.test_error() # log a ZeroDivisionError to the observer
+ f = self._makeRuntimeFailure()
+ self.observer.gotEvent(dict(message=(), time=time.time(), isError=1,
+ system='-', failure=f, why=None))
+ flushed = self.observer.flushErrors(ZeroDivisionError)
+ self.assertEqual(self.observer.getErrors(), [f])
+ self.assertEqual(len(flushed), 1)
+ self.assertTrue(flushed[0].check(ZeroDivisionError))
+
+
+ def test_ignoreErrors(self):
+ """
+ Check that C{_ignoreErrors} actually causes errors to be ignored.
+ """
+ self.observer._ignoreErrors(ZeroDivisionError)
+ f = makeFailure()
+ self.observer.gotEvent({'message': (),
+ 'time': time.time(), 'isError': 1,
+ 'system': '-', 'failure': f,
+ 'why': None})
+ self.assertEqual(self.observer.getErrors(), [])
+
+
+ def test_clearIgnores(self):
+ """
+ Check that C{_clearIgnores} ensures that previously ignored errors
+ get captured.
+ """
+ self.observer._ignoreErrors(ZeroDivisionError)
+ self.observer._clearIgnores()
+ f = makeFailure()
+ self.observer.gotEvent({'message': (),
+ 'time': time.time(), 'isError': 1,
+ 'system': '-', 'failure': f,
+ 'why': None})
+ self.assertEqual(self.observer.getErrors(), [f])
+
+
+
+class LogErrors(unittest.TestCase):
+ """
+ High-level tests demonstrating the expected behaviour of logged errors
+ during tests.
+ """
+
+ def setUp(self):
+ self.result = reporter.TestResult()
+
+ def tearDown(self):
+ self.flushLoggedErrors(ZeroDivisionError)
+
+ def test_singleError(self):
+ """
+ Test that a logged error gets reported as a test error.
+ """
+ test = Mask.MockTest('test_single')
+ test(self.result)
+ self.assertEqual(len(self.result.errors), 1)
+ self.assertTrue(self.result.errors[0][1].check(ZeroDivisionError),
+ self.result.errors[0][1])
+
+ def test_twoErrors(self):
+ """
+ Test that when two errors get logged, they both get reported as test
+ errors.
+ """
+ test = Mask.MockTest('test_double')
+ test(self.result)
+ self.assertEqual(len(self.result.errors), 2)
+
+ def test_inCallback(self):
+ """
+ Test that errors logged in callbacks get reported as test errors.
+ """
+ test = Mask.MockTest('test_inCallback')
+ test(self.result)
+ self.assertEqual(len(self.result.errors), 1)
+ self.assertTrue(self.result.errors[0][1].check(ZeroDivisionError),
+ self.result.errors[0][1])
+
+ def test_errorsIsolated(self):
+ """
+ Check that an error logged in one test doesn't fail the next test.
+ """
+ t1 = Mask.MockTest('test_single')
+ t2 = Mask.MockTest('test_silent')
+ t1(self.result)
+ t2(self.result)
+ self.assertEqual(len(self.result.errors), 1)
+ self.assertEqual(self.result.errors[0][0], t1)
diff --git a/twisted/trial/test/test_output.py b/twisted/trial/test/test_output.py
new file mode 100644
index 0000000..bedde9e
--- /dev/null
+++ b/twisted/trial/test/test_output.py
@@ -0,0 +1,162 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the output generated by trial.
+"""
+
+import os, StringIO
+
+from twisted.scripts import trial
+from twisted.trial import runner
+from twisted.trial.test import packages
+
+
+def runTrial(*args):
+ from twisted.trial import reporter
+ config = trial.Options()
+ config.parseOptions(args)
+ output = StringIO.StringIO()
+ myRunner = runner.TrialRunner(
+ reporter.VerboseTextReporter,
+ stream=output,
+ workingDirectory=config['temp-directory'])
+ suite = trial._getSuite(config)
+ result = myRunner.run(suite)
+ return output.getvalue()
+
+
+class TestImportErrors(packages.SysPathManglingTest):
+ """Actually run trial as if on the command line and check that the output
+ is what we expect.
+ """
+
+ debug = False
+ parent = "_testImportErrors"
+ def runTrial(self, *args):
+ return runTrial('--temp-directory', self.mktemp(), *args)
+
+ def _print(self, stuff):
+ print stuff
+ return stuff
+
+ def failUnlessIn(self, container, containee, *args, **kwargs):
+ # redefined to be useful in callbacks
+ super(TestImportErrors, self).failUnlessIn(
+ containee, container, *args, **kwargs)
+ return container
+
+ def failIfIn(self, container, containee, *args, **kwargs):
+ # redefined to be useful in callbacks
+ super(TestImportErrors, self).failIfIn(
+ containee, container, *args, **kwargs)
+ return container
+
+ def test_trialRun(self):
+ self.runTrial()
+
+ def test_nonexistentModule(self):
+ d = self.runTrial('twisted.doesntexist')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'twisted.doesntexist')
+ return d
+
+ def test_nonexistentPackage(self):
+ d = self.runTrial('doesntexist')
+ self.failUnlessIn(d, 'doesntexist')
+ self.failUnlessIn(d, 'ModuleNotFound')
+ self.failUnlessIn(d, '[ERROR]')
+ return d
+
+ def test_nonexistentPackageWithModule(self):
+ d = self.runTrial('doesntexist.barney')
+ self.failUnlessIn(d, 'doesntexist.barney')
+ self.failUnlessIn(d, 'ObjectNotFound')
+ self.failUnlessIn(d, '[ERROR]')
+ return d
+
+ def test_badpackage(self):
+ d = self.runTrial('badpackage')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'badpackage')
+ self.failIfIn(d, 'IOError')
+ return d
+
+ def test_moduleInBadpackage(self):
+ d = self.runTrial('badpackage.test_module')
+ self.failUnlessIn(d, "[ERROR]")
+ self.failUnlessIn(d, "badpackage.test_module")
+ self.failIfIn(d, 'IOError')
+ return d
+
+ def test_badmodule(self):
+ d = self.runTrial('package.test_bad_module')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'package.test_bad_module')
+ self.failIfIn(d, 'IOError')
+ self.failIfIn(d, '<module ')
+ return d
+
+ def test_badimport(self):
+ d = self.runTrial('package.test_import_module')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'package.test_import_module')
+ self.failIfIn(d, 'IOError')
+ self.failIfIn(d, '<module ')
+ return d
+
+ def test_recurseImport(self):
+ d = self.runTrial('package')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'test_bad_module')
+ self.failUnlessIn(d, 'test_import_module')
+ self.failIfIn(d, '<module ')
+ self.failIfIn(d, 'IOError')
+ return d
+
+ def test_recurseImportErrors(self):
+ d = self.runTrial('package2')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, 'package2')
+ self.failUnlessIn(d, 'test_module')
+ self.failUnlessIn(d, "No module named frotz")
+ self.failIfIn(d, '<module ')
+ self.failIfIn(d, 'IOError')
+ return d
+
+ def test_nonRecurseImportErrors(self):
+ d = self.runTrial('-N', 'package2')
+ self.failUnlessIn(d, '[ERROR]')
+ self.failUnlessIn(d, "No module named frotz")
+ self.failIfIn(d, '<module ')
+ return d
+
+ def test_regularRun(self):
+ d = self.runTrial('package.test_module')
+ self.failIfIn(d, '[ERROR]')
+ self.failIfIn(d, 'IOError')
+ self.failUnlessIn(d, 'OK')
+ self.failUnlessIn(d, 'PASSED (successes=1)')
+ return d
+
+ def test_filename(self):
+ self.mangleSysPath(self.oldPath)
+ d = self.runTrial(
+ os.path.join(self.parent, 'package', 'test_module.py'))
+ self.failIfIn(d, '[ERROR]')
+ self.failIfIn(d, 'IOError')
+ self.failUnlessIn(d, 'OK')
+ self.failUnlessIn(d, 'PASSED (successes=1)')
+ return d
+
+ def test_dosFile(self):
+ ## XXX -- not really an output test, more of a script test
+ self.mangleSysPath(self.oldPath)
+ d = self.runTrial(
+ os.path.join(self.parent,
+ 'package', 'test_dos_module.py'))
+ self.failIfIn(d, '[ERROR]')
+ self.failIfIn(d, 'IOError')
+ self.failUnlessIn(d, 'OK')
+ self.failUnlessIn(d, 'PASSED (successes=1)')
+ return d
diff --git a/twisted/trial/test/test_plugins.py b/twisted/trial/test/test_plugins.py
new file mode 100644
index 0000000..e1ec6aa
--- /dev/null
+++ b/twisted/trial/test/test_plugins.py
@@ -0,0 +1,46 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+
+"""
+Tests for L{twisted.plugins.twisted_trial}.
+"""
+
+from twisted.plugin import getPlugins
+from twisted.trial import unittest
+from twisted.trial.itrial import IReporter
+
+
+class TestPlugins(unittest.TestCase):
+ """
+ Tests for Trial's reporter plugins.
+ """
+
+ def getPluginsByLongOption(self, longOption):
+ """
+ Return the Trial reporter plugin with the given long option.
+
+ If more than one is found, raise ValueError. If none are found, raise
+ IndexError.
+ """
+ plugins = [
+ plugin for plugin in getPlugins(IReporter)
+ if plugin.longOpt == longOption]
+ if len(plugins) > 1:
+ raise ValueError(
+ "More than one plugin found with long option %r: %r"
+ % (longOption, plugins))
+ return plugins[0]
+
+
+ def test_subunitPlugin(self):
+ """
+ One of the reporter plugins is the subunit reporter plugin.
+ """
+ subunitPlugin = self.getPluginsByLongOption('subunit')
+ self.assertEqual('Subunit Reporter', subunitPlugin.name)
+ self.assertEqual('twisted.trial.reporter', subunitPlugin.module)
+ self.assertEqual('subunit', subunitPlugin.longOpt)
+ self.assertIdentical(None, subunitPlugin.shortOpt)
+ self.assertEqual('SubunitReporter', subunitPlugin.klass)
diff --git a/twisted/trial/test/test_pyunitcompat.py b/twisted/trial/test/test_pyunitcompat.py
new file mode 100644
index 0000000..72e7f11
--- /dev/null
+++ b/twisted/trial/test/test_pyunitcompat.py
@@ -0,0 +1,222 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+
+
+import sys
+import traceback
+
+from zope.interface import implements
+
+from twisted.python import reflect
+from twisted.python.failure import Failure
+from twisted.trial import util
+from twisted.trial.unittest import TestCase, PyUnitResultAdapter
+from twisted.trial.itrial import IReporter, ITestCase
+from twisted.trial.test import erroneous
+
+pyunit = __import__('unittest')
+
+
+class TestPyUnitTestCase(TestCase):
+
+ class PyUnitTest(pyunit.TestCase):
+
+ def test_pass(self):
+ pass
+
+
+ def setUp(self):
+ self.original = self.PyUnitTest('test_pass')
+ self.test = ITestCase(self.original)
+
+
+ def test_visit(self):
+ """
+ Trial assumes that test cases implement visit().
+ """
+ log = []
+ def visitor(test):
+ log.append(test)
+ self.test.visit(visitor)
+ self.assertEqual(log, [self.test])
+ test_visit.suppress = [
+ util.suppress(category=DeprecationWarning,
+ message="Test visitors deprecated in Twisted 8.0")]
+
+
+ def test_callable(self):
+ """
+ Tests must be callable in order to be used with Python's unittest.py.
+ """
+ self.assertTrue(callable(self.test),
+ "%r is not callable." % (self.test,))
+
+
+class TestPyUnitResult(TestCase):
+ """
+ Tests to show that PyUnitResultAdapter wraps TestResult objects from the
+ standard library 'unittest' module in such a way as to make them usable and
+ useful from Trial.
+ """
+
+ def test_dontUseAdapterWhenReporterProvidesIReporter(self):
+ """
+ The L{PyUnitResultAdapter} is only used when the result passed to
+ C{run} does *not* provide L{IReporter}.
+ """
+ class StubReporter(object):
+ """
+ A reporter which records data about calls made to it.
+
+ @ivar errors: Errors passed to L{addError}.
+ @ivar failures: Failures passed to L{addFailure}.
+ """
+
+ implements(IReporter)
+
+ def __init__(self):
+ self.errors = []
+ self.failures = []
+
+ def startTest(self, test):
+ """
+ Do nothing.
+ """
+
+ def stopTest(self, test):
+ """
+ Do nothing.
+ """
+
+ def addError(self, test, error):
+ """
+ Record the error.
+ """
+ self.errors.append(error)
+
+ test = erroneous.ErrorTest("test_foo")
+ result = StubReporter()
+ test.run(result)
+ self.assertIsInstance(result.errors[0], Failure)
+
+
+ def test_success(self):
+ class SuccessTest(TestCase):
+ ran = False
+ def test_foo(s):
+ s.ran = True
+ test = SuccessTest('test_foo')
+ result = pyunit.TestResult()
+ test.run(result)
+
+ self.failUnless(test.ran)
+ self.assertEqual(1, result.testsRun)
+ self.failUnless(result.wasSuccessful())
+
+ def test_failure(self):
+ class FailureTest(TestCase):
+ ran = False
+ def test_foo(s):
+ s.ran = True
+ s.fail('boom!')
+ test = FailureTest('test_foo')
+ result = pyunit.TestResult()
+ test.run(result)
+
+ self.failUnless(test.ran)
+ self.assertEqual(1, result.testsRun)
+ self.assertEqual(1, len(result.failures))
+ self.failIf(result.wasSuccessful())
+
+ def test_error(self):
+ test = erroneous.ErrorTest('test_foo')
+ result = pyunit.TestResult()
+ test.run(result)
+
+ self.failUnless(test.ran)
+ self.assertEqual(1, result.testsRun)
+ self.assertEqual(1, len(result.errors))
+ self.failIf(result.wasSuccessful())
+
+ def test_setUpError(self):
+ class ErrorTest(TestCase):
+ ran = False
+ def setUp(self):
+ 1/0
+ def test_foo(s):
+ s.ran = True
+ test = ErrorTest('test_foo')
+ result = pyunit.TestResult()
+ test.run(result)
+
+ self.failIf(test.ran)
+ self.assertEqual(1, result.testsRun)
+ self.assertEqual(1, len(result.errors))
+ self.failIf(result.wasSuccessful())
+
+ def test_tracebackFromFailure(self):
+ """
+ Errors added through the L{PyUnitResultAdapter} have the same traceback
+ information as if there were no adapter at all.
+ """
+ try:
+ 1/0
+ except ZeroDivisionError:
+ exc_info = sys.exc_info()
+ f = Failure()
+ pyresult = pyunit.TestResult()
+ result = PyUnitResultAdapter(pyresult)
+ result.addError(self, f)
+ self.assertEqual(pyresult.errors[0][1],
+ ''.join(traceback.format_exception(*exc_info)))
+
+
+ def test_traceback(self):
+ """
+ As test_tracebackFromFailure, but covering more code.
+ """
+ class ErrorTest(TestCase):
+ exc_info = None
+ def test_foo(self):
+ try:
+ 1/0
+ except ZeroDivisionError:
+ self.exc_info = sys.exc_info()
+ raise
+ test = ErrorTest('test_foo')
+ result = pyunit.TestResult()
+ test.run(result)
+
+ # We can't test that the tracebacks are equal, because Trial's
+ # machinery inserts a few extra frames on the top and we don't really
+ # want to trim them off without an extremely good reason.
+ #
+ # So, we just test that the result's stack ends with the the
+ # exception's stack.
+
+ expected_stack = ''.join(traceback.format_tb(test.exc_info[2]))
+ observed_stack = '\n'.join(result.errors[0][1].splitlines()[:-1])
+
+ self.assertEqual(expected_stack.strip(),
+ observed_stack[-len(expected_stack):].strip())
+
+
+ def test_tracebackFromCleanFailure(self):
+ """
+ Errors added through the L{PyUnitResultAdapter} have the same
+ traceback information as if there were no adapter at all, even
+ if the Failure that held the information has been cleaned.
+ """
+ try:
+ 1/0
+ except ZeroDivisionError:
+ exc_info = sys.exc_info()
+ f = Failure()
+ f.cleanFailure()
+ pyresult = pyunit.TestResult()
+ result = PyUnitResultAdapter(pyresult)
+ result.addError(self, f)
+ self.assertEqual(pyresult.errors[0][1],
+ ''.join(traceback.format_exception(*exc_info)))
diff --git a/twisted/trial/test/test_reporter.py b/twisted/trial/test/test_reporter.py
new file mode 100644
index 0000000..9c5af7b
--- /dev/null
+++ b/twisted/trial/test/test_reporter.py
@@ -0,0 +1,1649 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+
+"""
+Tests for L{twisted.trial.reporter}.
+"""
+
+
+import errno, sys, os, re, StringIO
+from inspect import getmro
+
+from twisted.internet.utils import suppressWarnings
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.trial import itrial, unittest, runner, reporter, util
+from twisted.trial.reporter import UncleanWarningsReporterWrapper
+from twisted.trial.test import erroneous
+from twisted.trial.unittest import makeTodo, SkipTest, Todo
+from twisted.trial.test import sample
+
+
+class BrokenStream(object):
+ """
+ Stream-ish object that raises a signal interrupt error. We use this to make
+ sure that Trial still manages to write what it needs to write.
+ """
+ written = False
+ flushed = False
+
+ def __init__(self, fObj):
+ self.fObj = fObj
+
+ def write(self, s):
+ if self.written:
+ return self.fObj.write(s)
+ self.written = True
+ raise IOError(errno.EINTR, "Interrupted write")
+
+ def flush(self):
+ if self.flushed:
+ return self.fObj.flush()
+ self.flushed = True
+ raise IOError(errno.EINTR, "Interrupted flush")
+
+
+class StringTest(unittest.TestCase):
+ def stringComparison(self, expect, output):
+ output = filter(None, output)
+ self.failUnless(len(expect) <= len(output),
+ "Must have more observed than expected"
+ "lines %d < %d" % (len(output), len(expect)))
+ REGEX_PATTERN_TYPE = type(re.compile(''))
+ for line_number, (exp, out) in enumerate(zip(expect, output)):
+ if exp is None:
+ continue
+ elif isinstance(exp, str):
+ self.assertSubstring(exp, out, "Line %d: %r not in %r"
+ % (line_number, exp, out))
+ elif isinstance(exp, REGEX_PATTERN_TYPE):
+ self.failUnless(exp.match(out),
+ "Line %d: %r did not match string %r"
+ % (line_number, exp.pattern, out))
+ else:
+ raise TypeError("don't know what to do with object %r"
+ % (exp,))
+
+
+class TestTestResult(unittest.TestCase):
+ def setUp(self):
+ self.result = reporter.TestResult()
+
+ def test_pyunitAddError(self):
+ # pyunit passes an exc_info tuple directly to addError
+ try:
+ raise RuntimeError('foo')
+ except RuntimeError, excValue:
+ self.result.addError(self, sys.exc_info())
+ failure = self.result.errors[0][1]
+ self.assertEqual(excValue, failure.value)
+ self.assertEqual(RuntimeError, failure.type)
+
+ def test_pyunitAddFailure(self):
+ # pyunit passes an exc_info tuple directly to addFailure
+ try:
+ raise self.failureException('foo')
+ except self.failureException, excValue:
+ self.result.addFailure(self, sys.exc_info())
+ failure = self.result.failures[0][1]
+ self.assertEqual(excValue, failure.value)
+ self.assertEqual(self.failureException, failure.type)
+
+
+class TestReporterRealtime(TestTestResult):
+ def setUp(self):
+ output = StringIO.StringIO()
+ self.result = reporter.Reporter(output, realtime=True)
+
+
+class TestErrorReporting(StringTest):
+ doubleSeparator = re.compile(r'^=+$')
+
+ def setUp(self):
+ self.loader = runner.TestLoader()
+ self.output = StringIO.StringIO()
+ self.result = reporter.Reporter(self.output)
+
+ def getOutput(self, suite):
+ result = self.getResult(suite)
+ result.done()
+ return self.output.getvalue()
+
+ def getResult(self, suite):
+ suite.run(self.result)
+ return self.result
+
+ def test_formatErroredMethod(self):
+ """
+ A test method which runs and has an error recorded against it is
+ reported in the output stream with the I{ERROR} tag along with a summary
+ of what error was reported and the ID of the test.
+ """
+ suite = self.loader.loadClass(erroneous.TestFailureInSetUp)
+ output = self.getOutput(suite).splitlines()
+ match = [
+ self.doubleSeparator,
+ '[ERROR]',
+ 'Traceback (most recent call last):',
+ re.compile(r'^\s+File .*erroneous\.py., line \d+, in setUp$'),
+ re.compile(r'^\s+raise FoolishError, '
+ r'.I am a broken setUp method.$'),
+ ('twisted.trial.test.erroneous.FoolishError: '
+ 'I am a broken setUp method'),
+ 'twisted.trial.test.erroneous.TestFailureInSetUp.test_noop']
+ self.stringComparison(match, output)
+
+
+ def test_formatFailedMethod(self):
+ """
+ A test method which runs and has a failure recorded against it is
+ reported in the output stream with the I{FAIL} tag along with a summary
+ of what failure was reported and the ID of the test.
+ """
+ suite = self.loader.loadMethod(erroneous.TestRegularFail.test_fail)
+ output = self.getOutput(suite).splitlines()
+ match = [
+ self.doubleSeparator,
+ '[FAIL]',
+ 'Traceback (most recent call last):',
+ re.compile(r'^\s+File .*erroneous\.py., line \d+, in test_fail$'),
+ re.compile(r'^\s+self\.fail\("I fail"\)$'),
+ 'twisted.trial.unittest.FailTest: I fail',
+ 'twisted.trial.test.erroneous.TestRegularFail.test_fail',
+ ]
+ self.stringComparison(match, output)
+
+
+ def test_doctestError(self):
+ """
+ A problem encountered while running a doctest is reported in the output
+ stream with a I{FAIL} or I{ERROR} tag along with a summary of what
+ problem was encountered and the ID of the test.
+ """
+ from twisted.trial.test import erroneous
+ suite = unittest.decorate(
+ self.loader.loadDoctests(erroneous), itrial.ITestCase)
+ output = self.getOutput(suite)
+ path = 'twisted.trial.test.erroneous.unexpectedException'
+ for substring in ['1/0', 'ZeroDivisionError',
+ 'Exception raised:', path]:
+ self.assertSubstring(substring, output)
+ self.failUnless(re.search('Fail(ed|ure in) example:', output),
+ "Couldn't match 'Failure in example: ' "
+ "or 'Failed example: '")
+ expect = [self.doubleSeparator,
+ re.compile(r'\[(ERROR|FAIL)\]')]
+ self.stringComparison(expect, output.splitlines())
+
+
+ def test_hiddenException(self):
+ """
+ Check that errors in C{DelayedCall}s get reported, even if the
+ test already has a failure.
+
+ Only really necessary for testing the deprecated style of tests that
+ use iterate() directly. See
+ L{erroneous.DelayedCall.testHiddenException} for more details.
+ """
+ test = erroneous.DelayedCall('testHiddenException')
+ output = self.getOutput(test).splitlines()
+ match = [
+ self.doubleSeparator,
+ '[FAIL]',
+ 'Traceback (most recent call last):',
+ re.compile(r'^\s+File .*erroneous\.py., line \d+, in '
+ 'testHiddenException$'),
+ re.compile(r'^\s+self\.fail\("Deliberate failure to mask the '
+ 'hidden exception"\)$'),
+ 'twisted.trial.unittest.FailTest: '
+ 'Deliberate failure to mask the hidden exception',
+ 'twisted.trial.test.erroneous.DelayedCall.testHiddenException',
+ self.doubleSeparator,
+ '[ERROR]',
+ 'Traceback (most recent call last):',
+ re.compile(r'^\s+File .* in runUntilCurrent'),
+ re.compile(r'^\s+.*'),
+ re.compile('^\s+File .*erroneous\.py", line \d+, in go'),
+ re.compile('^\s+raise RuntimeError\(self.hiddenExceptionMsg\)'),
+ 'exceptions.RuntimeError: something blew up',
+ 'twisted.trial.test.erroneous.DelayedCall.testHiddenException',
+ ]
+ self.stringComparison(match, output)
+
+
+
+class TestUncleanWarningWrapperErrorReporting(TestErrorReporting):
+ """
+ Tests that the L{UncleanWarningsReporterWrapper} can sufficiently proxy
+ IReporter failure and error reporting methods to a L{reporter.Reporter}.
+ """
+ def setUp(self):
+ self.loader = runner.TestLoader()
+ self.output = StringIO.StringIO()
+ self.result = UncleanWarningsReporterWrapper(
+ reporter.Reporter(self.output))
+
+
+
+class TracebackHandling(unittest.TestCase):
+ def getErrorFrames(self, test):
+ stream = StringIO.StringIO()
+ result = reporter.Reporter(stream)
+ test.run(result)
+ bads = result.failures + result.errors
+ assert len(bads) == 1
+ assert bads[0][0] == test
+ return result._trimFrames(bads[0][1].frames)
+
+ def checkFrames(self, observedFrames, expectedFrames):
+ for observed, expected in zip(observedFrames, expectedFrames):
+ self.assertEqual(observed[0], expected[0])
+ observedSegs = os.path.splitext(observed[1])[0].split(os.sep)
+ expectedSegs = expected[1].split('/')
+ self.assertEqual(observedSegs[-len(expectedSegs):],
+ expectedSegs)
+ self.assertEqual(len(observedFrames), len(expectedFrames))
+
+ def test_basic(self):
+ test = erroneous.TestRegularFail('test_fail')
+ frames = self.getErrorFrames(test)
+ self.checkFrames(frames,
+ [('test_fail', 'twisted/trial/test/erroneous')])
+
+ def test_subroutine(self):
+ test = erroneous.TestRegularFail('test_subfail')
+ frames = self.getErrorFrames(test)
+ self.checkFrames(frames,
+ [('test_subfail', 'twisted/trial/test/erroneous'),
+ ('subroutine', 'twisted/trial/test/erroneous')])
+
+ def test_deferred(self):
+ test = erroneous.TestFailureInDeferredChain('test_fail')
+ frames = self.getErrorFrames(test)
+ self.checkFrames(frames,
+ [('_later', 'twisted/trial/test/erroneous')])
+
+ def test_noFrames(self):
+ result = reporter.Reporter(None)
+ self.assertEqual([], result._trimFrames([]))
+
+ def test_oneFrame(self):
+ result = reporter.Reporter(None)
+ self.assertEqual(['fake frame'], result._trimFrames(['fake frame']))
+
+
+class FormatFailures(StringTest):
+ def setUp(self):
+ try:
+ raise RuntimeError('foo')
+ except RuntimeError:
+ self.f = Failure()
+ self.f.frames = [
+ ['foo', 'foo/bar.py', 5, [('x', 5)], [('y', 'orange')]],
+ ['qux', 'foo/bar.py', 10, [('a', 'two')], [('b', 'MCMXCIX')]]
+ ]
+ self.stream = StringIO.StringIO()
+ self.result = reporter.Reporter(self.stream)
+
+ def test_formatDefault(self):
+ tb = self.result._formatFailureTraceback(self.f)
+ self.stringComparison([
+ 'Traceback (most recent call last):',
+ ' File "foo/bar.py", line 5, in foo',
+ re.compile(r'^\s*$'),
+ ' File "foo/bar.py", line 10, in qux',
+ re.compile(r'^\s*$'),
+ 'RuntimeError: foo'], tb.splitlines())
+
+ def test_formatString(self):
+ tb = '''
+ File "twisted/trial/unittest.py", line 256, in failUnlessSubstring
+ return self.failUnlessIn(substring, astring, msg)
+exceptions.TypeError: iterable argument required
+
+'''
+ expected = '''
+ File "twisted/trial/unittest.py", line 256, in failUnlessSubstring
+ return self.failUnlessIn(substring, astring, msg)
+exceptions.TypeError: iterable argument required
+'''
+ formatted = self.result._formatFailureTraceback(tb)
+ self.assertEqual(expected, formatted)
+
+ def test_mutation(self):
+ frames = self.f.frames[:]
+ # The call shouldn't mutate the frames.
+ self.result._formatFailureTraceback(self.f)
+ self.assertEqual(self.f.frames, frames)
+
+
+class PyunitTestNames(unittest.TestCase):
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.test = sample.PyunitTest('test_foo')
+
+ def test_verboseReporter(self):
+ result = reporter.VerboseTextReporter(self.stream)
+ result.startTest(self.test)
+ output = self.stream.getvalue()
+ self.assertEqual(
+ output, 'twisted.trial.test.sample.PyunitTest.test_foo ... ')
+
+ def test_treeReporter(self):
+ result = reporter.TreeReporter(self.stream)
+ result.startTest(self.test)
+ output = self.stream.getvalue()
+ output = output.splitlines()[-1].strip()
+ self.assertEqual(output, result.getDescription(self.test) + ' ...')
+
+ def test_getDescription(self):
+ result = reporter.TreeReporter(self.stream)
+ output = result.getDescription(self.test)
+ self.assertEqual(output, 'test_foo')
+
+
+ def test_minimalReporter(self):
+ """
+ The summary of L{reporter.MinimalReporter} is a simple list of numbers,
+ indicating how many tests ran, how many failed etc.
+
+ The numbers represents:
+ * the run time of the tests
+ * the number of tests run, printed 2 times for legacy reasons
+ * the number of errors
+ * the number of failures
+ * the number of skips
+ """
+ result = reporter.MinimalReporter(self.stream)
+ self.test.run(result)
+ result._printSummary()
+ output = self.stream.getvalue().strip().split(' ')
+ self.assertEqual(output[1:], ['1', '1', '0', '0', '0'])
+
+
+ def test_minimalReporterTime(self):
+ """
+ L{reporter.MinimalReporter} reports the time to run the tests as first
+ data in its output.
+ """
+ times = [1.0, 1.2, 1.5, 1.9]
+ result = reporter.MinimalReporter(self.stream)
+ result._getTime = lambda: times.pop(0)
+ self.test.run(result)
+ result._printSummary()
+ output = self.stream.getvalue().strip().split(' ')
+ timer = output[0]
+ self.assertEqual(timer, "0.7")
+
+
+ def test_emptyMinimalReporter(self):
+ """
+ The summary of L{reporter.MinimalReporter} is a list of zeroes when no
+ test is actually run.
+ """
+ result = reporter.MinimalReporter(self.stream)
+ result._printSummary()
+ output = self.stream.getvalue().strip().split(' ')
+ self.assertEqual(output, ['0', '0', '0', '0', '0', '0'])
+
+
+
+class TestDirtyReactor(unittest.TestCase):
+ """
+ The trial script has an option to treat L{DirtyReactorAggregateError}s as
+ warnings, as a migration tool for test authors. It causes a wrapper to be
+ placed around reporters that replaces L{DirtyReactorAggregatErrors} with
+ warnings.
+ """
+
+ def setUp(self):
+ self.dirtyError = Failure(
+ util.DirtyReactorAggregateError(['foo'], ['bar']))
+ self.output = StringIO.StringIO()
+ self.test = TestDirtyReactor('test_errorByDefault')
+
+
+ def test_errorByDefault(self):
+ """
+ L{DirtyReactorAggregateError}s are reported as errors with the default
+ Reporter.
+ """
+ result = reporter.Reporter(stream=self.output)
+ result.addError(self.test, self.dirtyError)
+ self.assertEqual(len(result.errors), 1)
+ self.assertEqual(result.errors[0][1], self.dirtyError)
+
+
+ def test_warningsEnabled(self):
+ """
+ L{DirtyReactorAggregateError}s are reported as warnings when using
+ the L{UncleanWarningsReporterWrapper}.
+ """
+ result = UncleanWarningsReporterWrapper(
+ reporter.Reporter(stream=self.output))
+ self.assertWarns(UserWarning, self.dirtyError.getErrorMessage(),
+ reporter.__file__,
+ result.addError, self.test, self.dirtyError)
+
+
+ def test_warningsMaskErrors(self):
+ """
+ L{DirtyReactorAggregateError}s are I{not} reported as errors if the
+ L{UncleanWarningsReporterWrapper} is used.
+ """
+ result = UncleanWarningsReporterWrapper(
+ reporter.Reporter(stream=self.output))
+ self.assertWarns(UserWarning, self.dirtyError.getErrorMessage(),
+ reporter.__file__,
+ result.addError, self.test, self.dirtyError)
+ self.assertEqual(result._originalReporter.errors, [])
+
+
+ def test_dealsWithThreeTuples(self):
+ """
+ Some annoying stuff can pass three-tuples to addError instead of
+ Failures (like PyUnit). The wrapper, of course, handles this case,
+ since it is a part of L{twisted.trial.itrial.IReporter}! But it does
+ not convert L{DirtyReactorAggregateError} to warnings in this case,
+ because nobody should be passing those in the form of three-tuples.
+ """
+ result = UncleanWarningsReporterWrapper(
+ reporter.Reporter(stream=self.output))
+ result.addError(self.test,
+ (self.dirtyError.type, self.dirtyError.value, None))
+ self.assertEqual(len(result._originalReporter.errors), 1)
+ self.assertEqual(result._originalReporter.errors[0][1].type,
+ self.dirtyError.type)
+ self.assertEqual(result._originalReporter.errors[0][1].value,
+ self.dirtyError.value)
+
+
+
+class TrialTestNames(unittest.TestCase):
+
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.test = sample.FooTest('test_foo')
+
+ def test_verboseReporter(self):
+ result = reporter.VerboseTextReporter(self.stream)
+ result.startTest(self.test)
+ output = self.stream.getvalue()
+ self.assertEqual(output, self.test.id() + ' ... ')
+
+ def test_treeReporter(self):
+ result = reporter.TreeReporter(self.stream)
+ result.startTest(self.test)
+ output = self.stream.getvalue()
+ output = output.splitlines()[-1].strip()
+ self.assertEqual(output, result.getDescription(self.test) + ' ...')
+
+ def test_treeReporterWithDocstrings(self):
+ """A docstring"""
+ result = reporter.TreeReporter(self.stream)
+ self.assertEqual(result.getDescription(self),
+ 'test_treeReporterWithDocstrings')
+
+ def test_getDescription(self):
+ result = reporter.TreeReporter(self.stream)
+ output = result.getDescription(self.test)
+ self.assertEqual(output, "test_foo")
+
+
+class TestSkip(unittest.TestCase):
+ """
+ Tests for L{reporter.Reporter}'s handling of skips.
+ """
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.result = reporter.Reporter(self.stream)
+ self.test = sample.FooTest('test_foo')
+
+ def _getSkips(self, result):
+ """
+ Get the number of skips that happened to a reporter.
+ """
+ return len(result.skips)
+
+ def test_accumulation(self):
+ self.result.addSkip(self.test, 'some reason')
+ self.assertEqual(self._getSkips(self.result), 1)
+
+ def test_success(self):
+ self.result.addSkip(self.test, 'some reason')
+ self.assertEqual(True, self.result.wasSuccessful())
+
+
+ def test_summary(self):
+ """
+ The summary of a successful run with skips indicates that the test
+ suite passed and includes the number of skips.
+ """
+ self.result.addSkip(self.test, 'some reason')
+ self.result.done()
+ output = self.stream.getvalue().splitlines()[-1]
+ prefix = 'PASSED '
+ self.failUnless(output.startswith(prefix))
+ self.assertEqual(output[len(prefix):].strip(), '(skips=1)')
+
+
+ def test_basicErrors(self):
+ """
+ The output at the end of a test run with skips includes the reasons
+ for skipping those tests.
+ """
+ self.result.addSkip(self.test, 'some reason')
+ self.result.done()
+ output = self.stream.getvalue().splitlines()[3]
+ self.assertEqual(output.strip(), 'some reason')
+
+
+ def test_booleanSkip(self):
+ """
+ Tests can be skipped without specifying a reason by setting the 'skip'
+ attribute to True. When this happens, the test output includes 'True'
+ as the reason.
+ """
+ self.result.addSkip(self.test, True)
+ self.result.done()
+ output = self.stream.getvalue().splitlines()[3]
+ self.assertEqual(output, 'True')
+
+
+ def test_exceptionSkip(self):
+ """
+ Skips can be raised as errors. When this happens, the error is
+ included in the summary at the end of the test suite.
+ """
+ try:
+ 1/0
+ except Exception, e:
+ error = e
+ self.result.addSkip(self.test, error)
+ self.result.done()
+ output = '\n'.join(self.stream.getvalue().splitlines()[3:5]).strip()
+ self.assertEqual(output, str(e))
+
+
+class UncleanWarningSkipTest(TestSkip):
+ """
+ Tests for skips on a L{reporter.Reporter} wrapped by an
+ L{UncleanWarningsReporterWrapper}.
+ """
+ def setUp(self):
+ TestSkip.setUp(self)
+ self.result = UncleanWarningsReporterWrapper(self.result)
+
+ def _getSkips(self, result):
+ """
+ Get the number of skips that happened to a reporter inside of an
+ unclean warnings reporter wrapper.
+ """
+ return len(result._originalReporter.skips)
+
+
+
+class TodoTest(unittest.TestCase):
+ """
+ Tests for L{reporter.Reporter}'s handling of todos.
+ """
+
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.result = reporter.Reporter(self.stream)
+ self.test = sample.FooTest('test_foo')
+
+
+ def _getTodos(self, result):
+ """
+ Get the number of todos that happened to a reporter.
+ """
+ return len(result.expectedFailures)
+
+
+ def _getUnexpectedSuccesses(self, result):
+ """
+ Get the number of unexpected successes that happened to a reporter.
+ """
+ return len(result.unexpectedSuccesses)
+
+
+ def test_accumulation(self):
+ """
+ L{reporter.Reporter} accumulates the expected failures that it
+ is notified of.
+ """
+ self.result.addExpectedFailure(self.test, Failure(Exception()),
+ makeTodo('todo!'))
+ self.assertEqual(self._getTodos(self.result), 1)
+
+
+ def test_success(self):
+ """
+ A test run is still successful even if there are expected failures.
+ """
+ self.result.addExpectedFailure(self.test, Failure(Exception()),
+ makeTodo('todo!'))
+ self.assertEqual(True, self.result.wasSuccessful())
+
+
+ def test_unexpectedSuccess(self):
+ """
+ A test which is marked as todo but succeeds will have an unexpected
+ success reported to its result. A test run is still successful even
+ when this happens.
+ """
+ self.result.addUnexpectedSuccess(self.test, makeTodo("Heya!"))
+ self.assertEqual(True, self.result.wasSuccessful())
+ self.assertEqual(self._getUnexpectedSuccesses(self.result), 1)
+
+
+ def test_summary(self):
+ """
+ The reporter's C{printSummary} method should print the number of
+ expected failures that occured.
+ """
+ self.result.addExpectedFailure(self.test, Failure(Exception()),
+ makeTodo('some reason'))
+ self.result.done()
+ output = self.stream.getvalue().splitlines()[-1]
+ prefix = 'PASSED '
+ self.failUnless(output.startswith(prefix))
+ self.assertEqual(output[len(prefix):].strip(),
+ '(expectedFailures=1)')
+
+
+ def test_basicErrors(self):
+ """
+ The reporter's L{printErrors} method should include the value of the
+ Todo.
+ """
+ self.result.addExpectedFailure(self.test, Failure(Exception()),
+ makeTodo('some reason'))
+ self.result.done()
+ output = self.stream.getvalue().splitlines()[3].strip()
+ self.assertEqual(output, "Reason: 'some reason'")
+
+
+ def test_booleanTodo(self):
+ """
+ Booleans CAN'T be used as the value of a todo. Maybe this sucks. This
+ is a test for current behavior, not a requirement.
+ """
+ self.result.addExpectedFailure(self.test, Failure(Exception()),
+ makeTodo(True))
+ self.assertRaises(Exception, self.result.done)
+
+
+ def test_exceptionTodo(self):
+ """
+ The exception for expected failures should be shown in the
+ C{printErrors} output.
+ """
+ try:
+ 1/0
+ except Exception, e:
+ error = e
+ self.result.addExpectedFailure(self.test, Failure(error),
+ makeTodo("todo!"))
+ self.result.done()
+ output = '\n'.join(self.stream.getvalue().splitlines()[3:]).strip()
+ self.assertTrue(str(e) in output)
+
+
+
+class UncleanWarningTodoTest(TodoTest):
+ """
+ Tests for L{UncleanWarningsReporterWrapper}'s handling of todos.
+ """
+
+ def setUp(self):
+ TodoTest.setUp(self)
+ self.result = UncleanWarningsReporterWrapper(self.result)
+
+
+ def _getTodos(self, result):
+ """
+ Get the number of todos that happened to a reporter inside of an
+ unclean warnings reporter wrapper.
+ """
+ return len(result._originalReporter.expectedFailures)
+
+
+ def _getUnexpectedSuccesses(self, result):
+ """
+ Get the number of unexpected successes that happened to a reporter
+ inside of an unclean warnings reporter wrapper.
+ """
+ return len(result._originalReporter.unexpectedSuccesses)
+
+
+
+class MockColorizer:
+ """
+ Used by TestTreeReporter to make sure that output is colored correctly.
+ """
+
+ def __init__(self, stream):
+ self.log = []
+
+
+ def write(self, text, color):
+ self.log.append((color, text))
+
+
+
+class TestTreeReporter(unittest.TestCase):
+ def setUp(self):
+ self.test = sample.FooTest('test_foo')
+ self.stream = StringIO.StringIO()
+ self.result = reporter.TreeReporter(self.stream)
+ self.result._colorizer = MockColorizer(self.stream)
+ self.log = self.result._colorizer.log
+
+ def makeError(self):
+ try:
+ 1/0
+ except ZeroDivisionError:
+ f = Failure()
+ return f
+
+ def test_cleanupError(self):
+ """
+ Run cleanupErrors and check that the output is correct, and colored
+ correctly.
+ """
+ f = self.makeError()
+ self.result.cleanupErrors(f)
+ color, text = self.log[0]
+ self.assertEqual(color.strip(), self.result.ERROR)
+ self.assertEqual(text.strip(), 'cleanup errors')
+ color, text = self.log[1]
+ self.assertEqual(color.strip(), self.result.ERROR)
+ self.assertEqual(text.strip(), '[ERROR]')
+ test_cleanupError = suppressWarnings(
+ test_cleanupError,
+ util.suppress(category=reporter.BrokenTestCaseWarning),
+ util.suppress(category=DeprecationWarning))
+
+
+ def test_upDownError(self):
+ """
+ Run upDownError and check that the output is correct and colored
+ correctly.
+ """
+ self.result.upDownError("method", None, None, False)
+ color, text = self.log[0]
+ self.assertEqual(color.strip(), self.result.ERROR)
+ self.assertEqual(text.strip(), 'method')
+ test_upDownError = suppressWarnings(
+ test_upDownError,
+ util.suppress(category=DeprecationWarning,
+ message="upDownError is deprecated in Twisted 8.0."))
+
+
+ def test_summaryColoredSuccess(self):
+ """
+ The summary in case of success should have a good count of successes
+ and be colored properly.
+ """
+ self.result.addSuccess(self.test)
+ self.result.done()
+ self.assertEqual(self.log[1], (self.result.SUCCESS, 'PASSED'))
+ self.assertEqual(
+ self.stream.getvalue().splitlines()[-1].strip(), "(successes=1)")
+
+
+ def test_summaryColoredFailure(self):
+ """
+ The summary in case of failure should have a good count of errors
+ and be colored properly.
+ """
+ try:
+ raise RuntimeError('foo')
+ except RuntimeError:
+ self.result.addError(self, sys.exc_info())
+ self.result.done()
+ self.assertEqual(self.log[1], (self.result.FAILURE, 'FAILED'))
+ self.assertEqual(
+ self.stream.getvalue().splitlines()[-1].strip(), "(errors=1)")
+
+
+ def test_getPrelude(self):
+ """
+ The tree needs to get the segments of the test ID that correspond
+ to the module and class that it belongs to.
+ """
+ self.assertEqual(
+ ['foo.bar', 'baz'],
+ self.result._getPreludeSegments('foo.bar.baz.qux'))
+ self.assertEqual(
+ ['foo', 'bar'],
+ self.result._getPreludeSegments('foo.bar.baz'))
+ self.assertEqual(
+ ['foo'],
+ self.result._getPreludeSegments('foo.bar'))
+ self.assertEqual([], self.result._getPreludeSegments('foo'))
+
+
+ def test_groupResults(self):
+ """
+ If two different tests have the same error, L{Reporter._groupResults}
+ includes them together in one of the tuples in the list it returns.
+ """
+ try:
+ raise RuntimeError('foo')
+ except RuntimeError:
+ self.result.addError(self, sys.exc_info())
+ self.result.addError(self.test, sys.exc_info())
+ try:
+ raise RuntimeError('bar')
+ except RuntimeError:
+ extra = sample.FooTest('test_bar')
+ self.result.addError(extra, sys.exc_info())
+ self.result.done()
+ grouped = self.result._groupResults(
+ self.result.errors, self.result._formatFailureTraceback)
+ self.assertEqual(grouped[0][1], [self, self.test])
+ self.assertEqual(grouped[1][1], [extra])
+
+
+ def test_printResults(self):
+ """
+ L{Reporter._printResults} uses the results list and formatter callable
+ passed to it to produce groups of results to write to its output stream.
+ """
+ def formatter(n):
+ return str(n) + '\n'
+ first = sample.FooTest('test_foo')
+ second = sample.FooTest('test_bar')
+ third = sample.PyunitTest('test_foo')
+ self.result._printResults(
+ 'FOO', [(first, 1), (second, 1), (third, 2)], formatter)
+ self.assertEqual(
+ self.stream.getvalue(),
+ "%(double separator)s\n"
+ "FOO\n"
+ "1\n"
+ "\n"
+ "%(first)s\n"
+ "%(second)s\n"
+ "%(double separator)s\n"
+ "FOO\n"
+ "2\n"
+ "\n"
+ "%(third)s\n" % {
+ 'double separator': self.result._doubleSeparator,
+ 'first': first.id(),
+ 'second': second.id(),
+ 'third': third.id(),
+ })
+
+
+
+class TestReporterInterface(unittest.TestCase):
+ """
+ Tests for the bare interface of a trial reporter.
+
+ Subclass this test case and provide a different 'resultFactory' to test
+ that a particular reporter implementation will work with the rest of
+ Trial.
+
+ @cvar resultFactory: A callable that returns a reporter to be tested. The
+ callable must take the same parameters as L{reporter.Reporter}.
+ """
+
+ resultFactory = reporter.Reporter
+
+ def setUp(self):
+ self.test = sample.FooTest('test_foo')
+ self.stream = StringIO.StringIO()
+ self.publisher = log.LogPublisher()
+ self.result = self.resultFactory(self.stream, publisher=self.publisher)
+
+
+ def test_shouldStopInitiallyFalse(self):
+ """
+ shouldStop is False to begin with.
+ """
+ self.assertEqual(False, self.result.shouldStop)
+
+
+ def test_shouldStopTrueAfterStop(self):
+ """
+ shouldStop becomes True soon as someone calls stop().
+ """
+ self.result.stop()
+ self.assertEqual(True, self.result.shouldStop)
+
+
+ def test_wasSuccessfulInitiallyTrue(self):
+ """
+ wasSuccessful() is True when there have been no results reported.
+ """
+ self.assertEqual(True, self.result.wasSuccessful())
+
+
+ def test_wasSuccessfulTrueAfterSuccesses(self):
+ """
+ wasSuccessful() is True when there have been only successes, False
+ otherwise.
+ """
+ self.result.addSuccess(self.test)
+ self.assertEqual(True, self.result.wasSuccessful())
+
+
+ def test_wasSuccessfulFalseAfterErrors(self):
+ """
+ wasSuccessful() becomes False after errors have been reported.
+ """
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ self.result.addError(self.test, sys.exc_info())
+ self.assertEqual(False, self.result.wasSuccessful())
+
+
+ def test_wasSuccessfulFalseAfterFailures(self):
+ """
+ wasSuccessful() becomes False after failures have been reported.
+ """
+ try:
+ self.fail("foo")
+ except self.failureException:
+ self.result.addFailure(self.test, sys.exc_info())
+ self.assertEqual(False, self.result.wasSuccessful())
+
+
+
+class TestReporter(TestReporterInterface):
+ """
+ Tests for the base L{reporter.Reporter} class.
+ """
+
+ def setUp(self):
+ TestReporterInterface.setUp(self)
+ self._timer = 0
+ self.result._getTime = self._getTime
+
+
+ def _getTime(self):
+ self._timer += 1
+ return self._timer
+
+
+ def test_startStop(self):
+ self.result.startTest(self.test)
+ self.result.stopTest(self.test)
+ self.assertTrue(self.result._lastTime > 0)
+ self.assertEqual(self.result.testsRun, 1)
+ self.assertEqual(self.result.wasSuccessful(), True)
+
+
+ def test_brokenStream(self):
+ """
+ Test that the reporter safely writes to its stream.
+ """
+ result = self.resultFactory(stream=BrokenStream(self.stream))
+ result._writeln("Hello")
+ self.assertEqual(self.stream.getvalue(), 'Hello\n')
+ self.stream.truncate(0)
+ result._writeln("Hello %s!", 'World')
+ self.assertEqual(self.stream.getvalue(), 'Hello World!\n')
+
+
+ def test_printErrorsDeprecated(self):
+ """
+ L{IReporter.printErrors} was deprecated in Twisted 8.0.
+ """
+ def f():
+ self.result.printErrors()
+ self.assertWarns(
+ DeprecationWarning, "printErrors is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_printSummaryDeprecated(self):
+ """
+ L{IReporter.printSummary} was deprecated in Twisted 8.0.
+ """
+ def f():
+ self.result.printSummary()
+ self.assertWarns(
+ DeprecationWarning, "printSummary is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_writeDeprecated(self):
+ """
+ L{IReporter.write} was deprecated in Twisted 8.0.
+ """
+ def f():
+ self.result.write("")
+ self.assertWarns(
+ DeprecationWarning, "write is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_writelnDeprecated(self):
+ """
+ L{IReporter.writeln} was deprecated in Twisted 8.0.
+ """
+ def f():
+ self.result.writeln("")
+ self.assertWarns(
+ DeprecationWarning, "writeln is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_separatorDeprecated(self):
+ """
+ L{IReporter.separator} was deprecated in Twisted 8.0.
+ """
+ def f():
+ return self.result.separator
+ self.assertWarns(
+ DeprecationWarning, "separator is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_streamDeprecated(self):
+ """
+ L{IReporter.stream} was deprecated in Twisted 8.0.
+ """
+ def f():
+ return self.result.stream
+ self.assertWarns(
+ DeprecationWarning, "stream is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_upDownErrorDeprecated(self):
+ """
+ L{IReporter.upDownError} was deprecated in Twisted 8.0.
+ """
+ def f():
+ self.result.upDownError(None, None, None, None)
+ self.assertWarns(
+ DeprecationWarning, "upDownError is deprecated in Twisted 8.0.",
+ __file__, f)
+
+
+ def test_warning(self):
+ """
+ L{reporter.Reporter} observes warnings emitted by the Twisted log
+ system and writes them to its output stream.
+ """
+ message = RuntimeWarning("some warning text")
+ category = 'exceptions.RuntimeWarning'
+ filename = "path/to/some/file.py"
+ lineno = 71
+ self.publisher.msg(
+ warning=message, category=category,
+ filename=filename, lineno=lineno)
+ self.assertEqual(
+ self.stream.getvalue(),
+ "%s:%d: %s: %s\n" % (
+ filename, lineno, category.split('.')[-1], message))
+
+
+ def test_duplicateWarningSuppressed(self):
+ """
+ A warning emitted twice within a single test is only written to the
+ stream once.
+ """
+ # Emit the warning and assert that it shows up
+ self.test_warning()
+ # Emit the warning again and assert that the stream still only has one
+ # warning on it.
+ self.test_warning()
+
+
+ def test_warningEmittedForNewTest(self):
+ """
+ A warning emitted again after a new test has started is written to the
+ stream again.
+ """
+ test = self.__class__('test_warningEmittedForNewTest')
+ self.result.startTest(test)
+
+ # Clear whatever startTest wrote to the stream
+ self.stream.seek(0)
+ self.stream.truncate()
+
+ # Emit a warning (and incidentally, assert that it was emitted)
+ self.test_warning()
+
+ # Clean up from the first warning to simplify the rest of the
+ # assertions.
+ self.stream.seek(0)
+ self.stream.truncate()
+
+ # Stop the first test and start another one (it just happens to be the
+ # same one, but that doesn't matter)
+ self.result.stopTest(test)
+ self.result.startTest(test)
+
+ # Clean up the stopTest/startTest output
+ self.stream.seek(0)
+ self.stream.truncate()
+
+ # Emit the warning again and make sure it shows up
+ self.test_warning()
+
+
+ def test_stopObserving(self):
+ """
+ L{reporter.Reporter} stops observing log events when its C{done} method
+ is called.
+ """
+ self.result.done()
+ self.stream.seek(0)
+ self.stream.truncate()
+ self.publisher.msg(
+ warning=RuntimeWarning("some message"),
+ category='exceptions.RuntimeWarning',
+ filename="file/name.py", lineno=17)
+ self.assertEqual(self.stream.getvalue(), "")
+
+
+
+class TestSafeStream(unittest.TestCase):
+ def test_safe(self):
+ """
+ Test that L{reporter.SafeStream} successfully write to its original
+ stream even if an interrupt happens during the write.
+ """
+ stream = StringIO.StringIO()
+ broken = BrokenStream(stream)
+ safe = reporter.SafeStream(broken)
+ safe.write("Hello")
+ self.assertEqual(stream.getvalue(), "Hello")
+
+
+
+class TestSubunitReporter(TestReporterInterface):
+ """
+ Tests for the subunit reporter.
+
+ This just tests that the subunit reporter implements the basic interface.
+ """
+
+ resultFactory = reporter.SubunitReporter
+
+
+ def setUp(self):
+ if reporter.TestProtocolClient is None:
+ raise SkipTest(
+ "Subunit not installed, cannot test SubunitReporter")
+ TestReporterInterface.setUp(self)
+
+
+ def assertForwardsToSubunit(self, methodName, *args, **kwargs):
+ """
+ Assert that 'methodName' on L{SubunitReporter} forwards to the
+ equivalent method on subunit.
+
+ Checks that the return value from subunit is returned from the
+ L{SubunitReporter} and that the reporter writes the same data to its
+ stream as subunit does to its own.
+
+ Assumes that the method on subunit has the same name as the method on
+ L{SubunitReporter}.
+ """
+ stream = StringIO.StringIO()
+ subunitClient = reporter.TestProtocolClient(stream)
+ subunitReturn = getattr(subunitClient, methodName)(*args, **kwargs)
+ subunitOutput = stream.getvalue()
+ reporterReturn = getattr(self.result, methodName)(*args, **kwargs)
+ self.assertEqual(subunitReturn, reporterReturn)
+ self.assertEqual(subunitOutput, self.stream.getvalue())
+
+
+ def removeMethod(self, klass, methodName):
+ """
+ Remove 'methodName' from 'klass'.
+
+ If 'klass' does not have a method named 'methodName', then
+ 'removeMethod' succeeds silently.
+
+ If 'klass' does have a method named 'methodName', then it is removed
+ using delattr. Also, methods of the same name are removed from all
+ base classes of 'klass', thus removing the method entirely.
+
+ @param klass: The class to remove the method from.
+ @param methodName: The name of the method to remove.
+ """
+ method = getattr(klass, methodName, None)
+ if method is None:
+ return
+ for base in getmro(klass):
+ try:
+ delattr(base, methodName)
+ except (AttributeError, TypeError):
+ break
+ else:
+ self.addCleanup(setattr, base, methodName, method)
+
+
+ def test_subunitWithoutAddExpectedFailureInstalled(self):
+ """
+ Some versions of subunit don't have "addExpectedFailure". For these
+ versions, we report expected failures as successes.
+ """
+ self.removeMethod(reporter.TestProtocolClient, 'addExpectedFailure')
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ self.result.addExpectedFailure(self.test, sys.exc_info(), "todo")
+ expectedFailureOutput = self.stream.getvalue()
+ self.stream.truncate(0)
+ self.result.addSuccess(self.test)
+ successOutput = self.stream.getvalue()
+ self.assertEqual(successOutput, expectedFailureOutput)
+
+
+ def test_subunitWithoutAddSkipInstalled(self):
+ """
+ Some versions of subunit don't have "addSkip". For these versions, we
+ report skips as successes.
+ """
+ self.removeMethod(reporter.TestProtocolClient, 'addSkip')
+ self.result.addSkip(self.test, "reason")
+ skipOutput = self.stream.getvalue()
+ self.stream.truncate(0)
+ self.result.addSuccess(self.test)
+ successOutput = self.stream.getvalue()
+ self.assertEqual(successOutput, skipOutput)
+
+
+ def test_addExpectedFailurePassedThrough(self):
+ """
+ Some versions of subunit have "addExpectedFailure". For these
+ versions, when we call 'addExpectedFailure' on the test result, we
+ pass the error and test through to the subunit client.
+ """
+ addExpectedFailureCalls = []
+ def addExpectedFailure(test, error):
+ addExpectedFailureCalls.append((test, error))
+
+ # Provide our own addExpectedFailure, whether or not the locally
+ # installed subunit has addExpectedFailure.
+ self.result._subunit.addExpectedFailure = addExpectedFailure
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ exc_info = sys.exc_info()
+ self.result.addExpectedFailure(self.test, exc_info, 'todo')
+ self.assertEqual(addExpectedFailureCalls, [(self.test, exc_info)])
+
+
+ def test_addSkipSendsSubunitAddSkip(self):
+ """
+ Some versions of subunit have "addSkip". For these versions, when we
+ call 'addSkip' on the test result, we pass the test and reason through
+ to the subunit client.
+ """
+ addSkipCalls = []
+ def addSkip(test, reason):
+ addSkipCalls.append((test, reason))
+
+ # Provide our own addSkip, whether or not the locally-installed
+ # subunit has addSkip.
+ self.result._subunit.addSkip = addSkip
+ self.result.addSkip(self.test, 'reason')
+ self.assertEqual(addSkipCalls, [(self.test, 'reason')])
+
+
+ def test_doneDoesNothing(self):
+ """
+ The subunit reporter doesn't need to print out a summary -- the stream
+ of results is everything. Thus, done() does nothing.
+ """
+ self.result.done()
+ self.assertEqual('', self.stream.getvalue())
+
+
+ def test_startTestSendsSubunitStartTest(self):
+ """
+ SubunitReporter.startTest() sends the subunit 'startTest' message.
+ """
+ self.assertForwardsToSubunit('startTest', self.test)
+
+
+ def test_stopTestSendsSubunitStopTest(self):
+ """
+ SubunitReporter.stopTest() sends the subunit 'stopTest' message.
+ """
+ self.assertForwardsToSubunit('stopTest', self.test)
+
+
+ def test_addSuccessSendsSubunitAddSuccess(self):
+ """
+ SubunitReporter.addSuccess() sends the subunit 'addSuccess' message.
+ """
+ self.assertForwardsToSubunit('addSuccess', self.test)
+
+
+ def test_addErrorSendsSubunitAddError(self):
+ """
+ SubunitReporter.addError() sends the subunit 'addError' message.
+ """
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ error = sys.exc_info()
+ self.assertForwardsToSubunit('addError', self.test, error)
+
+
+ def test_addFailureSendsSubunitAddFailure(self):
+ """
+ SubunitReporter.addFailure() sends the subunit 'addFailure' message.
+ """
+ try:
+ self.fail('hello')
+ except self.failureException:
+ failure = sys.exc_info()
+ self.assertForwardsToSubunit('addFailure', self.test, failure)
+
+
+ def test_addUnexpectedSuccessSendsSubunitAddSuccess(self):
+ """
+ SubunitReporter.addFailure() sends the subunit 'addSuccess' message,
+ since subunit doesn't model unexpected success.
+ """
+ stream = StringIO.StringIO()
+ subunitClient = reporter.TestProtocolClient(stream)
+ subunitClient.addSuccess(self.test)
+ subunitOutput = stream.getvalue()
+ self.result.addUnexpectedSuccess(self.test, 'todo')
+ self.assertEqual(subunitOutput, self.stream.getvalue())
+
+
+ def test_loadTimeErrors(self):
+ """
+ Load-time errors are reported like normal errors.
+ """
+ test = runner.TestLoader().loadByName('doesntexist')
+ test.run(self.result)
+ output = self.stream.getvalue()
+ # Just check that 'doesntexist' is in the output, rather than
+ # assembling the expected stack trace.
+ self.assertIn('doesntexist', output)
+
+
+
+class TestSubunitReporterNotInstalled(unittest.TestCase):
+ """
+ Test behaviour when the subunit reporter is not installed.
+ """
+
+ def test_subunitNotInstalled(self):
+ """
+ If subunit is not installed, TestProtocolClient will be None, and
+ SubunitReporter will raise an error when you try to construct it.
+ """
+ stream = StringIO.StringIO()
+ self.patch(reporter, 'TestProtocolClient', None)
+ e = self.assertRaises(Exception, reporter.SubunitReporter, stream)
+ self.assertEqual("Subunit not available", str(e))
+
+
+
+class TestTimingReporter(TestReporter):
+ resultFactory = reporter.TimingTextReporter
+
+
+
+class LoggingReporter(reporter.Reporter):
+ """
+ Simple reporter that stores the last test that was passed to it.
+ """
+
+ def __init__(self, *args, **kwargs):
+ reporter.Reporter.__init__(self, *args, **kwargs)
+ self.test = None
+
+ def addError(self, test, error):
+ self.test = test
+
+ def addExpectedFailure(self, test, failure, todo):
+ self.test = test
+
+ def addFailure(self, test, failure):
+ self.test = test
+
+ def addSkip(self, test, skip):
+ self.test = test
+
+ def addUnexpectedSuccess(self, test, todo):
+ self.test = test
+
+ def startTest(self, test):
+ self.test = test
+
+ def stopTest(self, test):
+ self.test = test
+
+
+
+class TestAdaptedReporter(unittest.TestCase):
+ """
+ L{reporter._AdaptedReporter} is a reporter wrapper that wraps all of the
+ tests it receives before passing them on to the original reporter.
+ """
+
+ def setUp(self):
+ self.wrappedResult = self.getWrappedResult()
+
+
+ def _testAdapter(self, test):
+ return test.id()
+
+
+ def assertWrapped(self, wrappedResult, test):
+ self.assertEqual(wrappedResult._originalReporter.test, self._testAdapter(test))
+
+
+ def getFailure(self, exceptionInstance):
+ """
+ Return a L{Failure} from raising the given exception.
+
+ @param exceptionInstance: The exception to raise.
+ @return: L{Failure}
+ """
+ try:
+ raise exceptionInstance
+ except:
+ return Failure()
+
+
+ def getWrappedResult(self):
+ result = LoggingReporter()
+ return reporter._AdaptedReporter(result, self._testAdapter)
+
+
+ def test_addError(self):
+ """
+ C{addError} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.addError(self, self.getFailure(RuntimeError()))
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_addFailure(self):
+ """
+ C{addFailure} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.addFailure(self, self.getFailure(AssertionError()))
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_addSkip(self):
+ """
+ C{addSkip} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.addSkip(self, self.getFailure(SkipTest('no reason')))
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_startTest(self):
+ """
+ C{startTest} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.startTest(self)
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_stopTest(self):
+ """
+ C{stopTest} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.stopTest(self)
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_addExpectedFailure(self):
+ """
+ C{addExpectedFailure} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.addExpectedFailure(
+ self, self.getFailure(RuntimeError()), Todo("no reason"))
+ self.assertWrapped(self.wrappedResult, self)
+
+
+ def test_addUnexpectedSuccess(self):
+ """
+ C{addUnexpectedSuccess} wraps its test with the provided adapter.
+ """
+ self.wrappedResult.addUnexpectedSuccess(self, Todo("no reason"))
+ self.assertWrapped(self.wrappedResult, self)
+
+
+
+class FakeStream(object):
+ """
+ A fake stream which C{isatty} method returns some predictable.
+
+ @ivar tty: returned value of C{isatty}.
+ @type tty: C{bool}
+ """
+
+ def __init__(self, tty=True):
+ self.tty = tty
+
+
+ def isatty(self):
+ return self.tty
+
+
+
+class AnsiColorizerTests(unittest.TestCase):
+ """
+ Tests for L{reporter._AnsiColorizer}.
+ """
+
+ def setUp(self):
+ self.savedModules = sys.modules.copy()
+
+
+ def tearDown(self):
+ sys.modules.clear()
+ sys.modules.update(self.savedModules)
+
+
+ def test_supportedStdOutTTY(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{False} if the given
+ stream is not a TTY.
+ """
+ self.assertFalse(reporter._AnsiColorizer.supported(FakeStream(False)))
+
+
+ def test_supportedNoCurses(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{False} if the curses
+ module can't be imported.
+ """
+ sys.modules['curses'] = None
+ self.assertFalse(reporter._AnsiColorizer.supported(FakeStream()))
+
+
+ def test_supportedSetupTerm(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{True} if
+ C{curses.tigetnum} returns more than 2 supported colors. It only tries
+ to call C{curses.setupterm} if C{curses.tigetnum} previously failed
+ with a C{curses.error}.
+ """
+ class fakecurses(object):
+ error = RuntimeError
+ setUp = 0
+
+ def setupterm(self):
+ self.setUp += 1
+
+ def tigetnum(self, value):
+ if self.setUp:
+ return 3
+ else:
+ raise self.error()
+
+ sys.modules['curses'] = fakecurses()
+ self.assertTrue(reporter._AnsiColorizer.supported(FakeStream()))
+ self.assertTrue(reporter._AnsiColorizer.supported(FakeStream()))
+
+ self.assertEqual(sys.modules['curses'].setUp, 1)
+
+
+ def test_supportedTigetNumWrongError(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{False} and doesn't try
+ to call C{curses.setupterm} if C{curses.tigetnum} returns something
+ different than C{curses.error}.
+ """
+ class fakecurses(object):
+ error = RuntimeError
+
+ def tigetnum(self, value):
+ raise ValueError()
+
+ sys.modules['curses'] = fakecurses()
+ self.assertFalse(reporter._AnsiColorizer.supported(FakeStream()))
+
+
+ def test_supportedTigetNumNotEnoughColor(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{False} if
+ C{curses.tigetnum} returns less than 2 supported colors.
+ """
+ class fakecurses(object):
+ error = RuntimeError
+
+ def tigetnum(self, value):
+ return 1
+
+ sys.modules['curses'] = fakecurses()
+ self.assertFalse(reporter._AnsiColorizer.supported(FakeStream()))
+
+
+ def test_supportedTigetNumErrors(self):
+ """
+ L{reporter._AnsiColorizer.supported} returns C{False} if
+ C{curses.tigetnum} raises an error, and calls C{curses.setupterm} once.
+ """
+ class fakecurses(object):
+ error = RuntimeError
+ setUp = 0
+
+ def setupterm(self):
+ self.setUp += 1
+
+ def tigetnum(self, value):
+ raise self.error()
+
+ sys.modules['curses'] = fakecurses()
+ self.assertFalse(reporter._AnsiColorizer.supported(FakeStream()))
+ self.assertEqual(sys.modules['curses'].setUp, 1)
diff --git a/twisted/trial/test/test_runner.py b/twisted/trial/test/test_runner.py
new file mode 100644
index 0000000..12fcc86
--- /dev/null
+++ b/twisted/trial/test/test_runner.py
@@ -0,0 +1,1034 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# Maintainer: Jonathan Lange
+# Author: Robert Collins
+
+
+import StringIO, os, sys
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.trial.itrial import IReporter, ITestCase
+from twisted.trial import unittest, runner, reporter, util
+from twisted.python import failure, log, reflect, filepath
+from twisted.python.filepath import FilePath
+from twisted.scripts import trial
+from twisted.plugins import twisted_trial
+from twisted import plugin
+from twisted.internet import defer
+
+
+pyunit = __import__('unittest')
+
+
+class CapturingDebugger(object):
+
+ def __init__(self):
+ self._calls = []
+
+ def runcall(self, *args, **kwargs):
+ self._calls.append('runcall')
+ args[0](*args[1:], **kwargs)
+
+
+
+class CapturingReporter(object):
+ """
+ Reporter that keeps a log of all actions performed on it.
+ """
+
+ implements(IReporter)
+
+ stream = None
+ tbformat = None
+ args = None
+ separator = None
+ testsRun = None
+
+ def __init__(self, stream=None, tbformat=None, rterrors=None,
+ publisher=None):
+ """
+ Create a capturing reporter.
+ """
+ self._calls = []
+ self.shouldStop = False
+ self._stream = stream
+ self._tbformat = tbformat
+ self._rterrors = rterrors
+ self._publisher = publisher
+
+
+ def startTest(self, method):
+ """
+ Report the beginning of a run of a single test method
+ @param method: an object that is adaptable to ITestMethod
+ """
+ self._calls.append('startTest')
+
+
+ def stopTest(self, method):
+ """
+ Report the status of a single test method
+ @param method: an object that is adaptable to ITestMethod
+ """
+ self._calls.append('stopTest')
+
+
+ def cleanupErrors(self, errs):
+ """called when the reactor has been left in a 'dirty' state
+ @param errs: a list of L{twisted.python.failure.Failure}s
+ """
+ self._calls.append('cleanupError')
+
+
+ def addSuccess(self, test):
+ self._calls.append('addSuccess')
+
+
+ def done(self):
+ """
+ Do nothing. These tests don't care about done.
+ """
+
+
+
+class TrialRunnerTestsMixin:
+ """
+ Mixin defining tests for L{runner.TrialRunner}.
+ """
+ def tearDown(self):
+ self.runner._tearDownLogFile()
+
+
+ def test_empty(self):
+ """
+ Empty test method, used by the other tests.
+ """
+
+
+ def _getObservers(self):
+ return log.theLogPublisher.observers
+
+
+ def test_addObservers(self):
+ """
+ Any log system observers L{TrialRunner.run} adds are removed by the
+ time it returns.
+ """
+ originalCount = len(self._getObservers())
+ self.runner.run(self.test)
+ newCount = len(self._getObservers())
+ self.assertEqual(newCount, originalCount)
+
+
+ def test_logFileAlwaysActive(self):
+ """
+ Test that a new file is opened on each run.
+ """
+ oldSetUpLogFile = self.runner._setUpLogFile
+ l = []
+ def setUpLogFile():
+ oldSetUpLogFile()
+ l.append(self.runner._logFileObserver)
+ self.runner._setUpLogFile = setUpLogFile
+ self.runner.run(self.test)
+ self.runner.run(self.test)
+ self.assertEqual(len(l), 2)
+ self.failIf(l[0] is l[1], "Should have created a new file observer")
+
+
+ def test_logFileGetsClosed(self):
+ """
+ Test that file created is closed during the run.
+ """
+ oldSetUpLogFile = self.runner._setUpLogFile
+ l = []
+ def setUpLogFile():
+ oldSetUpLogFile()
+ l.append(self.runner._logFileObject)
+ self.runner._setUpLogFile = setUpLogFile
+ self.runner.run(self.test)
+ self.assertEqual(len(l), 1)
+ self.failUnless(l[0].closed)
+
+
+
+class TestTrialRunner(TrialRunnerTestsMixin, unittest.TestCase):
+ """
+ Tests for L{runner.TrialRunner} with the feature to turn unclean errors
+ into warnings disabled.
+ """
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.runner = runner.TrialRunner(CapturingReporter, stream=self.stream)
+ self.test = TestTrialRunner('test_empty')
+
+
+ def test_publisher(self):
+ """
+ The reporter constructed by L{runner.TrialRunner} is passed
+ L{twisted.python.log} as the value for the C{publisher} parameter.
+ """
+ result = self.runner._makeResult()
+ self.assertIdentical(result._publisher, log)
+
+
+
+class TrialRunnerWithUncleanWarningsReporter(TrialRunnerTestsMixin,
+ unittest.TestCase):
+ """
+ Tests for the TrialRunner's interaction with an unclean-error suppressing
+ reporter.
+ """
+
+ def setUp(self):
+ self.stream = StringIO.StringIO()
+ self.runner = runner.TrialRunner(CapturingReporter, stream=self.stream,
+ uncleanWarnings=True)
+ self.test = TestTrialRunner('test_empty')
+
+
+
+class DryRunMixin(object):
+
+ suppress = [util.suppress(
+ category=DeprecationWarning,
+ message="Test visitors deprecated in Twisted 8.0")]
+
+
+ def setUp(self):
+ self.log = []
+ self.stream = StringIO.StringIO()
+ self.runner = runner.TrialRunner(CapturingReporter,
+ runner.TrialRunner.DRY_RUN,
+ stream=self.stream)
+ self.makeTestFixtures()
+
+
+ def makeTestFixtures(self):
+ """
+ Set C{self.test} and C{self.suite}, where C{self.suite} is an empty
+ TestSuite.
+ """
+
+
+ def test_empty(self):
+ """
+ If there are no tests, the reporter should not receive any events to
+ report.
+ """
+ result = self.runner.run(runner.TestSuite())
+ self.assertEqual(result._calls, [])
+
+
+ def test_singleCaseReporting(self):
+ """
+ If we are running a single test, check the reporter starts, passes and
+ then stops the test during a dry run.
+ """
+ result = self.runner.run(self.test)
+ self.assertEqual(result._calls, ['startTest', 'addSuccess', 'stopTest'])
+
+
+ def test_testsNotRun(self):
+ """
+ When we are doing a dry run, the tests should not actually be run.
+ """
+ self.runner.run(self.test)
+ self.assertEqual(self.log, [])
+
+
+
+class DryRunTest(DryRunMixin, unittest.TestCase):
+ """
+ Check that 'dry run' mode works well with Trial tests.
+ """
+ def makeTestFixtures(self):
+ class MockTest(unittest.TestCase):
+ def test_foo(test):
+ self.log.append('test_foo')
+ self.test = MockTest('test_foo')
+ self.suite = runner.TestSuite()
+
+
+
+class PyUnitDryRunTest(DryRunMixin, unittest.TestCase):
+ """
+ Check that 'dry run' mode works well with stdlib unittest tests.
+ """
+ def makeTestFixtures(self):
+ class PyunitCase(pyunit.TestCase):
+ def test_foo(self):
+ pass
+ self.test = PyunitCase('test_foo')
+ self.suite = pyunit.TestSuite()
+
+
+
+class TestRunner(unittest.TestCase):
+ def setUp(self):
+ self.config = trial.Options()
+ # whitebox hack a reporter in, because plugins are CACHED and will
+ # only reload if the FILE gets changed.
+
+ parts = reflect.qual(CapturingReporter).split('.')
+ package = '.'.join(parts[:-1])
+ klass = parts[-1]
+ plugins = [twisted_trial._Reporter(
+ "Test Helper Reporter",
+ package,
+ description="Utility for unit testing.",
+ longOpt="capturing",
+ shortOpt=None,
+ klass=klass)]
+
+
+ # XXX There should really be a general way to hook the plugin system
+ # for tests.
+ def getPlugins(iface, *a, **kw):
+ self.assertEqual(iface, IReporter)
+ return plugins + list(self.original(iface, *a, **kw))
+
+ self.original = plugin.getPlugins
+ plugin.getPlugins = getPlugins
+
+ self.standardReport = ['startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest',
+ 'startTest', 'addSuccess', 'stopTest']
+
+
+ def tearDown(self):
+ plugin.getPlugins = self.original
+
+
+ def parseOptions(self, args):
+ self.config.parseOptions(args)
+
+
+ def getRunner(self):
+ r = trial._makeRunner(self.config)
+ r.stream = StringIO.StringIO()
+ # XXX The runner should always take care of cleaning this up itself.
+ # It's not clear why this is necessary. The runner always tears down
+ # its log file.
+ self.addCleanup(r._tearDownLogFile)
+ # XXX The runner should always take care of cleaning this up itself as
+ # well. It's necessary because TrialRunner._setUpTestdir might raise
+ # an exception preventing Reporter.done from being run, leaving the
+ # observer added by Reporter.__init__ still present in the system.
+ # Something better needs to happen inside
+ # TrialRunner._runWithoutDecoration to remove the need for this cludge.
+ r._log = log.LogPublisher()
+ return r
+
+
+ def test_runner_can_get_reporter(self):
+ self.parseOptions([])
+ result = self.config['reporter']
+ runner = self.getRunner()
+ self.assertEqual(result, runner._makeResult().__class__)
+
+
+ def test_runner_get_result(self):
+ self.parseOptions([])
+ runner = self.getRunner()
+ result = runner._makeResult()
+ self.assertEqual(result.__class__, self.config['reporter'])
+
+
+ def test_uncleanWarningsOffByDefault(self):
+ """
+ By default Trial sets the 'uncleanWarnings' option on the runner to
+ False. This means that dirty reactor errors will be reported as
+ errors. See L{test_reporter.TestDirtyReactor}.
+ """
+ self.parseOptions([])
+ runner = self.getRunner()
+ self.assertNotIsInstance(runner._makeResult(),
+ reporter.UncleanWarningsReporterWrapper)
+
+
+ def test_getsUncleanWarnings(self):
+ """
+ Specifying '--unclean-warnings' on the trial command line will cause
+ reporters to be wrapped in a device which converts unclean errors to
+ warnings. See L{test_reporter.TestDirtyReactor} for implications.
+ """
+ self.parseOptions(['--unclean-warnings'])
+ runner = self.getRunner()
+ self.assertIsInstance(runner._makeResult(),
+ reporter.UncleanWarningsReporterWrapper)
+
+
+ def test_runner_working_directory(self):
+ self.parseOptions(['--temp-directory', 'some_path'])
+ runner = self.getRunner()
+ self.assertEqual(runner.workingDirectory, 'some_path')
+
+
+ def test_concurrentImplicitWorkingDirectory(self):
+ """
+ If no working directory is explicitly specified and the default
+ working directory is in use by another runner, L{TrialRunner.run}
+ selects a different default working directory to use.
+ """
+ self.parseOptions([])
+
+ # Make sure we end up with the same working directory after this test
+ # as we had before it.
+ self.addCleanup(os.chdir, os.getcwd())
+
+ # Make a new directory and change into it. This isolates us from state
+ # that other tests might have dumped into this process's temp
+ # directory.
+ runDirectory = FilePath(self.mktemp())
+ runDirectory.makedirs()
+ os.chdir(runDirectory.path)
+
+ firstRunner = self.getRunner()
+ secondRunner = self.getRunner()
+
+ where = {}
+
+ class ConcurrentCase(unittest.TestCase):
+ def test_first(self):
+ """
+ Start a second test run which will have a default working
+ directory which is the same as the working directory of the
+ test run already in progress.
+ """
+ # Change the working directory to the value it had before this
+ # test suite was started.
+ where['concurrent'] = subsequentDirectory = os.getcwd()
+ os.chdir(runDirectory.path)
+ self.addCleanup(os.chdir, subsequentDirectory)
+
+ secondRunner.run(ConcurrentCase('test_second'))
+
+ def test_second(self):
+ """
+ Record the working directory for later analysis.
+ """
+ where['record'] = os.getcwd()
+
+ result = firstRunner.run(ConcurrentCase('test_first'))
+ bad = result.errors + result.failures
+ if bad:
+ self.fail(bad[0][1])
+ self.assertEqual(
+ where, {
+ 'concurrent': runDirectory.child('_trial_temp').path,
+ 'record': runDirectory.child('_trial_temp-1').path})
+
+
+ def test_concurrentExplicitWorkingDirectory(self):
+ """
+ If a working directory which is already in use is explicitly specified,
+ L{TrialRunner.run} raises L{_WorkingDirectoryBusy}.
+ """
+ self.parseOptions(['--temp-directory', os.path.abspath(self.mktemp())])
+
+ initialDirectory = os.getcwd()
+ self.addCleanup(os.chdir, initialDirectory)
+
+ firstRunner = self.getRunner()
+ secondRunner = self.getRunner()
+
+ class ConcurrentCase(unittest.TestCase):
+ def test_concurrent(self):
+ """
+ Try to start another runner in the same working directory and
+ assert that it raises L{_WorkingDirectoryBusy}.
+ """
+ self.assertRaises(
+ util._WorkingDirectoryBusy,
+ secondRunner.run, ConcurrentCase('test_failure'))
+
+ def test_failure(self):
+ """
+ Should not be called, always fails.
+ """
+ self.fail("test_failure should never be called.")
+
+ result = firstRunner.run(ConcurrentCase('test_concurrent'))
+ bad = result.errors + result.failures
+ if bad:
+ self.fail(bad[0][1])
+
+
+ def test_runner_normal(self):
+ self.parseOptions(['--temp-directory', self.mktemp(),
+ '--reporter', 'capturing',
+ 'twisted.trial.test.sample'])
+ my_runner = self.getRunner()
+ loader = runner.TestLoader()
+ suite = loader.loadByName('twisted.trial.test.sample', True)
+ result = my_runner.run(suite)
+ self.assertEqual(self.standardReport, result._calls)
+
+
+ def test_runner_debug(self):
+ self.parseOptions(['--reporter', 'capturing',
+ '--debug', 'twisted.trial.test.sample'])
+ my_runner = self.getRunner()
+ debugger = CapturingDebugger()
+ def get_debugger():
+ return debugger
+ my_runner._getDebugger = get_debugger
+ loader = runner.TestLoader()
+ suite = loader.loadByName('twisted.trial.test.sample', True)
+ result = my_runner.run(suite)
+ self.assertEqual(self.standardReport, result._calls)
+ self.assertEqual(['runcall'], debugger._calls)
+
+
+
+class RemoveSafelyTests(unittest.TestCase):
+ """
+ Tests for L{_removeSafely}.
+ """
+ def test_removeSafelyNoTrialMarker(self):
+ """
+ If a path doesn't contain a node named C{"_trial_marker"}, that path is
+ not removed by L{runner._removeSafely} and a L{runner._NoTrialMarker}
+ exception is raised instead.
+ """
+ directory = self.mktemp()
+ os.mkdir(directory)
+ dirPath = filepath.FilePath(directory)
+ self.assertRaises(util._NoTrialMarker, util._removeSafely, dirPath)
+
+
+ def test_removeSafelyRemoveFailsMoveSucceeds(self):
+ """
+ If an L{OSError} is raised while removing a path in
+ L{runner._removeSafely}, an attempt is made to move the path to a new
+ name.
+ """
+ def dummyRemove():
+ """
+ Raise an C{OSError} to emulate the branch of L{runner._removeSafely}
+ in which path removal fails.
+ """
+ raise OSError()
+
+ # Patch stdout so we can check the print statements in _removeSafely
+ out = StringIO.StringIO()
+ self.patch(sys, 'stdout', out)
+
+ # Set up a trial directory with a _trial_marker
+ directory = self.mktemp()
+ os.mkdir(directory)
+ dirPath = filepath.FilePath(directory)
+ dirPath.child('_trial_marker').touch()
+ # Ensure that path.remove() raises an OSError
+ dirPath.remove = dummyRemove
+
+ util._removeSafely(dirPath)
+ self.assertIn("could not remove FilePath", out.getvalue())
+
+
+ def test_removeSafelyRemoveFailsMoveFails(self):
+ """
+ If an L{OSError} is raised while removing a path in
+ L{runner._removeSafely}, an attempt is made to move the path to a new
+ name. If that attempt fails, the L{OSError} is re-raised.
+ """
+ def dummyRemove():
+ """
+ Raise an C{OSError} to emulate the branch of L{runner._removeSafely}
+ in which path removal fails.
+ """
+ raise OSError("path removal failed")
+
+ def dummyMoveTo(path):
+ """
+ Raise an C{OSError} to emulate the branch of L{runner._removeSafely}
+ in which path movement fails.
+ """
+ raise OSError("path movement failed")
+
+ # Patch stdout so we can check the print statements in _removeSafely
+ out = StringIO.StringIO()
+ self.patch(sys, 'stdout', out)
+
+ # Set up a trial directory with a _trial_marker
+ directory = self.mktemp()
+ os.mkdir(directory)
+ dirPath = filepath.FilePath(directory)
+ dirPath.child('_trial_marker').touch()
+
+ # Ensure that path.remove() and path.moveTo() both raise OSErrors
+ dirPath.remove = dummyRemove
+ dirPath.moveTo = dummyMoveTo
+
+ error = self.assertRaises(OSError, util._removeSafely, dirPath)
+ self.assertEqual(str(error), "path movement failed")
+ self.assertIn("could not remove FilePath", out.getvalue())
+
+
+
+class TestTrialSuite(unittest.TestCase):
+
+ def test_imports(self):
+ # FIXME, HTF do you test the reactor can be cleaned up ?!!!
+ from twisted.trial.runner import TrialSuite
+
+
+
+
+class TestUntilFailure(unittest.TestCase):
+ class FailAfter(unittest.TestCase):
+ """
+ A test case that fails when run 3 times in a row.
+ """
+ count = []
+ def test_foo(self):
+ self.count.append(None)
+ if len(self.count) == 3:
+ self.fail('Count reached 3')
+
+
+ def setUp(self):
+ TestUntilFailure.FailAfter.count = []
+ self.test = TestUntilFailure.FailAfter('test_foo')
+ self.stream = StringIO.StringIO()
+ self.runner = runner.TrialRunner(reporter.Reporter, stream=self.stream)
+
+
+ def test_runUntilFailure(self):
+ """
+ Test that the runUntilFailure method of the runner actually fail after
+ a few runs.
+ """
+ result = self.runner.runUntilFailure(self.test)
+ self.assertEqual(result.testsRun, 1)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(self._getFailures(result), 1)
+
+
+ def _getFailures(self, result):
+ """
+ Get the number of failures that were reported to a result.
+ """
+ return len(result.failures)
+
+
+ def test_runUntilFailureDecorate(self):
+ """
+ C{runUntilFailure} doesn't decorate the tests uselessly: it does it one
+ time when run starts, but not at each turn.
+ """
+ decorated = []
+ def decorate(test, interface):
+ decorated.append((test, interface))
+ return test
+ self.patch(unittest, "decorate", decorate)
+ result = self.runner.runUntilFailure(self.test)
+ self.assertEqual(result.testsRun, 1)
+
+ self.assertEqual(len(decorated), 1)
+ self.assertEqual(decorated, [(self.test, ITestCase)])
+
+
+ def test_runUntilFailureForceGCDecorate(self):
+ """
+ C{runUntilFailure} applies the force-gc decoration after the standard
+ L{ITestCase} decoration, but only one time.
+ """
+ decorated = []
+ def decorate(test, interface):
+ decorated.append((test, interface))
+ return test
+ self.patch(unittest, "decorate", decorate)
+ self.runner._forceGarbageCollection = True
+ result = self.runner.runUntilFailure(self.test)
+ self.assertEqual(result.testsRun, 1)
+
+ self.assertEqual(len(decorated), 2)
+ self.assertEqual(decorated,
+ [(self.test, ITestCase),
+ (self.test, unittest._ForceGarbageCollectionDecorator)])
+
+
+
+class UncleanUntilFailureTests(TestUntilFailure):
+ """
+ Test that the run-until-failure feature works correctly with the unclean
+ error suppressor.
+ """
+
+ def setUp(self):
+ TestUntilFailure.setUp(self)
+ self.runner = runner.TrialRunner(reporter.Reporter, stream=self.stream,
+ uncleanWarnings=True)
+
+ def _getFailures(self, result):
+ """
+ Get the number of failures that were reported to a result that
+ is wrapped in an UncleanFailureWrapper.
+ """
+ return len(result._originalReporter.failures)
+
+
+
+class BreakingSuite(runner.TestSuite):
+ """
+ A L{TestSuite} that logs an error when it is run.
+ """
+
+ def run(self, result):
+ try:
+ raise RuntimeError("error that occurs outside of a test")
+ except RuntimeError:
+ log.err(failure.Failure())
+
+
+
+class TestLoggedErrors(unittest.TestCase):
+ """
+ It is possible for an error generated by a test to be logged I{outside} of
+ any test. The log observers constructed by L{TestCase} won't catch these
+ errors. Here we try to generate such errors and ensure they are reported to
+ a L{TestResult} object.
+ """
+
+ def tearDown(self):
+ self.flushLoggedErrors(RuntimeError)
+
+
+ def test_construct(self):
+ """
+ Check that we can construct a L{runner.LoggedSuite} and that it
+ starts empty.
+ """
+ suite = runner.LoggedSuite()
+ self.assertEqual(suite.countTestCases(), 0)
+
+
+ def test_capturesError(self):
+ """
+ Chek that a L{LoggedSuite} reports any logged errors to its result.
+ """
+ result = reporter.TestResult()
+ suite = runner.LoggedSuite([BreakingSuite()])
+ suite.run(result)
+ self.assertEqual(len(result.errors), 1)
+ self.assertEqual(result.errors[0][0].id(), runner.NOT_IN_TEST)
+ self.failUnless(result.errors[0][1].check(RuntimeError))
+
+
+
+class TestTestHolder(unittest.TestCase):
+
+ def setUp(self):
+ self.description = "description"
+ self.holder = runner.TestHolder(self.description)
+
+
+ def test_holder(self):
+ """
+ Check that L{runner.TestHolder} takes a description as a parameter
+ and that this description is returned by the C{id} and
+ C{shortDescription} methods.
+ """
+ self.assertEqual(self.holder.id(), self.description)
+ self.assertEqual(self.holder.shortDescription(), self.description)
+
+
+ def test_holderImplementsITestCase(self):
+ """
+ L{runner.TestHolder} implements L{ITestCase}.
+ """
+ self.assertIdentical(self.holder, ITestCase(self.holder))
+ self.assertTrue(
+ verifyObject(ITestCase, self.holder),
+ "%r claims to provide %r but does not do so correctly."
+ % (self.holder, ITestCase))
+
+
+ def test_runsWithStandardResult(self):
+ """
+ A L{runner.TestHolder} can run against the standard Python
+ C{TestResult}.
+ """
+ result = pyunit.TestResult()
+ self.holder.run(result)
+ self.assertTrue(result.wasSuccessful())
+ self.assertEqual(1, result.testsRun)
+
+
+
+class ErrorHolderTestsMixin(object):
+ """
+ This mixin defines test methods which can be applied to a
+ L{runner.ErrorHolder} constructed with either a L{Failure} or a
+ C{exc_info}-style tuple.
+
+ Subclass this and implement C{setUp} to create C{self.holder} referring to a
+ L{runner.ErrorHolder} instance and C{self.error} referring to a L{Failure}
+ which the holder holds.
+ """
+ exceptionForTests = ZeroDivisionError('integer division or modulo by zero')
+
+ class TestResultStub(object):
+ """
+ Stub for L{TestResult}.
+ """
+ def __init__(self):
+ self.errors = []
+
+ def startTest(self, test):
+ pass
+
+ def stopTest(self, test):
+ pass
+
+ def addError(self, test, error):
+ self.errors.append((test, error))
+
+
+ def test_runsWithStandardResult(self):
+ """
+ A L{runner.ErrorHolder} can run against the standard Python
+ C{TestResult}.
+ """
+ result = pyunit.TestResult()
+ self.holder.run(result)
+ self.assertFalse(result.wasSuccessful())
+ self.assertEqual(1, result.testsRun)
+
+
+ def test_run(self):
+ """
+ L{runner.ErrorHolder} adds an error to the result when run.
+ """
+ self.holder.run(self.result)
+ self.assertEqual(
+ self.result.errors,
+ [(self.holder, (self.error.type, self.error.value, self.error.tb))])
+
+
+ def test_call(self):
+ """
+ L{runner.ErrorHolder} adds an error to the result when called.
+ """
+ self.holder(self.result)
+ self.assertEqual(
+ self.result.errors,
+ [(self.holder, (self.error.type, self.error.value, self.error.tb))])
+
+
+ def test_countTestCases(self):
+ """
+ L{runner.ErrorHolder.countTestCases} always returns 0.
+ """
+ self.assertEqual(self.holder.countTestCases(), 0)
+
+
+ def test_repr(self):
+ """
+ L{runner.ErrorHolder.__repr__} returns a string describing the error it
+ holds.
+ """
+ self.assertEqual(repr(self.holder),
+ "<ErrorHolder description='description' "
+ "error=ZeroDivisionError('integer division or modulo by zero',)>")
+
+
+
+class FailureHoldingErrorHolderTests(ErrorHolderTestsMixin, TestTestHolder):
+ """
+ Tests for L{runner.ErrorHolder} behaving similarly to L{runner.TestHolder}
+ when constructed with a L{Failure} representing its error.
+ """
+ def setUp(self):
+ self.description = "description"
+ # make a real Failure so we can construct ErrorHolder()
+ try:
+ raise self.exceptionForTests
+ except ZeroDivisionError:
+ self.error = failure.Failure()
+ self.holder = runner.ErrorHolder(self.description, self.error)
+ self.result = self.TestResultStub()
+
+
+
+class ExcInfoHoldingErrorHolderTests(ErrorHolderTestsMixin, TestTestHolder):
+ """
+ Tests for L{runner.ErrorHolder} behaving similarly to L{runner.TestHolder}
+ when constructed with a C{exc_info}-style tuple representing its error.
+ """
+ def setUp(self):
+ self.description = "description"
+ # make a real Failure so we can construct ErrorHolder()
+ try:
+ raise self.exceptionForTests
+ except ZeroDivisionError:
+ exceptionInfo = sys.exc_info()
+ self.error = failure.Failure()
+ self.holder = runner.ErrorHolder(self.description, exceptionInfo)
+ self.result = self.TestResultStub()
+
+
+
+class TestMalformedMethod(unittest.TestCase):
+ """
+ Test that trial manages when test methods don't have correct signatures.
+ """
+ class ContainMalformed(unittest.TestCase):
+ """
+ This TestCase holds malformed test methods that trial should handle.
+ """
+ def test_foo(self, blah):
+ pass
+ def test_bar():
+ pass
+ test_spam = defer.deferredGenerator(test_bar)
+
+ def _test(self, method):
+ """
+ Wrapper for one of the test method of L{ContainMalformed}.
+ """
+ stream = StringIO.StringIO()
+ trialRunner = runner.TrialRunner(reporter.Reporter, stream=stream)
+ test = TestMalformedMethod.ContainMalformed(method)
+ result = trialRunner.run(test)
+ self.assertEqual(result.testsRun, 1)
+ self.failIf(result.wasSuccessful())
+ self.assertEqual(len(result.errors), 1)
+
+ def test_extraArg(self):
+ """
+ Test when the method has extra (useless) arguments.
+ """
+ self._test('test_foo')
+
+ def test_noArg(self):
+ """
+ Test when the method doesn't have even self as argument.
+ """
+ self._test('test_bar')
+
+ def test_decorated(self):
+ """
+ Test a decorated method also fails.
+ """
+ self._test('test_spam')
+
+
+
+class DestructiveTestSuiteTestCase(unittest.TestCase):
+ """
+ Test for L{runner.DestructiveTestSuite}.
+ """
+
+ def test_basic(self):
+ """
+ Thes destructive test suite should run the tests normally.
+ """
+ called = []
+ class MockTest(unittest.TestCase):
+ def test_foo(test):
+ called.append(True)
+ test = MockTest('test_foo')
+ result = reporter.TestResult()
+ suite = runner.DestructiveTestSuite([test])
+ self.assertEqual(called, [])
+ suite.run(result)
+ self.assertEqual(called, [True])
+ self.assertEqual(suite.countTestCases(), 0)
+
+
+ def test_shouldStop(self):
+ """
+ Test the C{shouldStop} management: raising a C{KeyboardInterrupt} must
+ interrupt the suite.
+ """
+ called = []
+ class MockTest(unittest.TestCase):
+ def test_foo1(test):
+ called.append(1)
+ def test_foo2(test):
+ raise KeyboardInterrupt()
+ def test_foo3(test):
+ called.append(2)
+ result = reporter.TestResult()
+ loader = runner.TestLoader()
+ loader.suiteFactory = runner.DestructiveTestSuite
+ suite = loader.loadClass(MockTest)
+ self.assertEqual(called, [])
+ suite.run(result)
+ self.assertEqual(called, [1])
+ # The last test shouldn't have been run
+ self.assertEqual(suite.countTestCases(), 1)
+
+
+ def test_cleanup(self):
+ """
+ Checks that the test suite cleanups its tests during the run, so that
+ it ends empty.
+ """
+ class MockTest(unittest.TestCase):
+ def test_foo(test):
+ pass
+ test = MockTest('test_foo')
+ result = reporter.TestResult()
+ suite = runner.DestructiveTestSuite([test])
+ self.assertEqual(suite.countTestCases(), 1)
+ suite.run(result)
+ self.assertEqual(suite.countTestCases(), 0)
+
+
+
+class TestRunnerDeprecation(unittest.TestCase):
+
+ class FakeReporter(reporter.Reporter):
+ """
+ Fake reporter that does *not* implement done() but *does* implement
+ printErrors, separator, printSummary, stream, write and writeln
+ without deprecations.
+ """
+
+ done = None
+ separator = None
+ stream = None
+
+ def printErrors(self, *args):
+ pass
+
+ def printSummary(self, *args):
+ pass
+
+ def write(self, *args):
+ pass
+
+ def writeln(self, *args):
+ pass
+
+
+ def test_reporterDeprecations(self):
+ """
+ The runner emits a warning if it is using a result that doesn't
+ implement 'done'.
+ """
+ trialRunner = runner.TrialRunner(None)
+ result = self.FakeReporter()
+ trialRunner._makeResult = lambda: result
+ def f():
+ # We have to use a pyunit test, otherwise we'll get deprecation
+ # warnings about using iterate() in a test.
+ trialRunner.run(pyunit.TestCase('id'))
+ self.assertWarns(
+ DeprecationWarning,
+ "%s should implement done() but doesn't. Falling back to "
+ "printErrors() and friends." % reflect.qual(result.__class__),
+ __file__, f)
diff --git a/twisted/trial/test/test_script.py b/twisted/trial/test/test_script.py
new file mode 100644
index 0000000..6c93ebe
--- /dev/null
+++ b/twisted/trial/test/test_script.py
@@ -0,0 +1,482 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import gc
+import StringIO, sys, types
+
+from twisted.trial import unittest, runner
+from twisted.scripts import trial
+from twisted.python import util, deprecate, versions
+from twisted.python.compat import set
+from twisted.python.filepath import FilePath
+
+from twisted.trial.test.test_loader import testNames
+
+pyunit = __import__('unittest')
+
+
+def sibpath(filename):
+ """For finding files in twisted/trial/test"""
+ return util.sibpath(__file__, filename)
+
+
+
+class ForceGarbageCollection(unittest.TestCase):
+ """
+ Tests for the --force-gc option.
+ """
+
+ def setUp(self):
+ self.config = trial.Options()
+ self.log = []
+ self.patch(gc, 'collect', self.collect)
+ test = pyunit.FunctionTestCase(self.simpleTest)
+ self.test = runner.TestSuite([test, test])
+
+
+ def simpleTest(self):
+ """
+ A simple test method that records that it was run.
+ """
+ self.log.append('test')
+
+
+ def collect(self):
+ """
+ A replacement for gc.collect that logs calls to itself.
+ """
+ self.log.append('collect')
+
+
+ def makeRunner(self):
+ """
+ Return a L{runner.TrialRunner} object that is safe to use in tests.
+ """
+ runner = trial._makeRunner(self.config)
+ runner.stream = StringIO.StringIO()
+ return runner
+
+
+ def test_forceGc(self):
+ """
+ Passing the --force-gc option to the trial script forces the garbage
+ collector to run before and after each test.
+ """
+ self.config['force-gc'] = True
+ self.config.postOptions()
+ runner = self.makeRunner()
+ runner.run(self.test)
+ self.assertEqual(self.log, ['collect', 'test', 'collect',
+ 'collect', 'test', 'collect'])
+
+
+ def test_unforceGc(self):
+ """
+ By default, no garbage collection is forced.
+ """
+ self.config.postOptions()
+ runner = self.makeRunner()
+ runner.run(self.test)
+ self.assertEqual(self.log, ['test', 'test'])
+
+
+
+class TestSuiteUsed(unittest.TestCase):
+ """
+ Check the category of tests suite used by the loader.
+ """
+
+ def setUp(self):
+ """
+ Create a trial configuration object.
+ """
+ self.config = trial.Options()
+
+
+ def test_defaultSuite(self):
+ """
+ By default, the loader should use L{runner.DestructiveTestSuite}
+ """
+ loader = trial._getLoader(self.config)
+ self.assertEqual(loader.suiteFactory, runner.DestructiveTestSuite)
+
+
+ def test_untilFailureSuite(self):
+ """
+ The C{until-failure} configuration uses the L{runner.TestSuite} to keep
+ instances alive across runs.
+ """
+ self.config['until-failure'] = True
+ loader = trial._getLoader(self.config)
+ self.assertEqual(loader.suiteFactory, runner.TestSuite)
+
+
+
+class TestModuleTest(unittest.TestCase):
+ def setUp(self):
+ self.config = trial.Options()
+
+ def tearDown(self):
+ self.config = None
+
+ def test_testNames(self):
+ """
+ Check that the testNames helper method accurately collects the
+ names of tests in suite.
+ """
+ self.assertEqual(testNames(self), [self.id()])
+
+ def assertSuitesEqual(self, test1, names):
+ loader = runner.TestLoader()
+ names1 = testNames(test1)
+ names2 = testNames(runner.TestSuite(map(loader.loadByName, names)))
+ names1.sort()
+ names2.sort()
+ self.assertEqual(names1, names2)
+
+ def test_baseState(self):
+ self.assertEqual(0, len(self.config['tests']))
+
+ def test_testmoduleOnModule(self):
+ """
+ Check that --testmodule loads a suite which contains the tests
+ referred to in test-case-name inside its parameter.
+ """
+ self.config.opt_testmodule(sibpath('moduletest.py'))
+ self.assertSuitesEqual(trial._getSuite(self.config),
+ ['twisted.trial.test.test_test_visitor'])
+
+ def test_testmoduleTwice(self):
+ """
+ When the same module is specified with two --testmodule flags, it
+ should only appear once in the suite.
+ """
+ self.config.opt_testmodule(sibpath('moduletest.py'))
+ self.config.opt_testmodule(sibpath('moduletest.py'))
+ self.assertSuitesEqual(trial._getSuite(self.config),
+ ['twisted.trial.test.test_test_visitor'])
+
+ def test_testmoduleOnSourceAndTarget(self):
+ """
+ If --testmodule is specified twice, once for module A and once for
+ a module which refers to module A, then make sure module A is only
+ added once.
+ """
+ self.config.opt_testmodule(sibpath('moduletest.py'))
+ self.config.opt_testmodule(sibpath('test_test_visitor.py'))
+ self.assertSuitesEqual(trial._getSuite(self.config),
+ ['twisted.trial.test.test_test_visitor'])
+
+ def test_testmoduleOnSelfModule(self):
+ """
+ When given a module that refers to *itself* in the test-case-name
+ variable, check that --testmodule only adds the tests once.
+ """
+ self.config.opt_testmodule(sibpath('moduleself.py'))
+ self.assertSuitesEqual(trial._getSuite(self.config),
+ ['twisted.trial.test.moduleself'])
+
+ def test_testmoduleOnScript(self):
+ """
+ Check that --testmodule loads tests referred to in test-case-name
+ buffer variables.
+ """
+ self.config.opt_testmodule(sibpath('scripttest.py'))
+ self.assertSuitesEqual(trial._getSuite(self.config),
+ ['twisted.trial.test.test_test_visitor',
+ 'twisted.trial.test.test_class'])
+
+ def test_testmoduleOnNonexistentFile(self):
+ """
+ Check that --testmodule displays a meaningful error message when
+ passed a non-existent filename.
+ """
+ buffy = StringIO.StringIO()
+ stderr, sys.stderr = sys.stderr, buffy
+ filename = 'test_thisbetternoteverexist.py'
+ try:
+ self.config.opt_testmodule(filename)
+ self.assertEqual(0, len(self.config['tests']))
+ self.assertEqual("File %r doesn't exist\n" % (filename,),
+ buffy.getvalue())
+ finally:
+ sys.stderr = stderr
+
+ def test_testmoduleOnEmptyVars(self):
+ """
+ Check that --testmodule adds no tests to the suite for modules
+ which lack test-case-name buffer variables.
+ """
+ self.config.opt_testmodule(sibpath('novars.py'))
+ self.assertEqual(0, len(self.config['tests']))
+
+ def test_testmoduleOnModuleName(self):
+ """
+ Check that --testmodule does *not* support module names as arguments
+ and that it displays a meaningful error message.
+ """
+ buffy = StringIO.StringIO()
+ stderr, sys.stderr = sys.stderr, buffy
+ moduleName = 'twisted.trial.test.test_script'
+ try:
+ self.config.opt_testmodule(moduleName)
+ self.assertEqual(0, len(self.config['tests']))
+ self.assertEqual("File %r doesn't exist\n" % (moduleName,),
+ buffy.getvalue())
+ finally:
+ sys.stderr = stderr
+
+ def test_parseLocalVariable(self):
+ declaration = '-*- test-case-name: twisted.trial.test.test_tests -*-'
+ localVars = trial._parseLocalVariables(declaration)
+ self.assertEqual({'test-case-name':
+ 'twisted.trial.test.test_tests'},
+ localVars)
+
+ def test_trailingSemicolon(self):
+ declaration = '-*- test-case-name: twisted.trial.test.test_tests; -*-'
+ localVars = trial._parseLocalVariables(declaration)
+ self.assertEqual({'test-case-name':
+ 'twisted.trial.test.test_tests'},
+ localVars)
+
+ def test_parseLocalVariables(self):
+ declaration = ('-*- test-case-name: twisted.trial.test.test_tests; '
+ 'foo: bar -*-')
+ localVars = trial._parseLocalVariables(declaration)
+ self.assertEqual({'test-case-name':
+ 'twisted.trial.test.test_tests',
+ 'foo': 'bar'},
+ localVars)
+
+ def test_surroundingGuff(self):
+ declaration = ('## -*- test-case-name: '
+ 'twisted.trial.test.test_tests -*- #')
+ localVars = trial._parseLocalVariables(declaration)
+ self.assertEqual({'test-case-name':
+ 'twisted.trial.test.test_tests'},
+ localVars)
+
+ def test_invalidLine(self):
+ self.failUnlessRaises(ValueError, trial._parseLocalVariables,
+ 'foo')
+
+ def test_invalidDeclaration(self):
+ self.failUnlessRaises(ValueError, trial._parseLocalVariables,
+ '-*- foo -*-')
+ self.failUnlessRaises(ValueError, trial._parseLocalVariables,
+ '-*- foo: bar; qux -*-')
+ self.failUnlessRaises(ValueError, trial._parseLocalVariables,
+ '-*- foo: bar: baz; qux: qax -*-')
+
+ def test_variablesFromFile(self):
+ localVars = trial.loadLocalVariables(sibpath('moduletest.py'))
+ self.assertEqual({'test-case-name':
+ 'twisted.trial.test.test_test_visitor'},
+ localVars)
+
+ def test_noVariablesInFile(self):
+ localVars = trial.loadLocalVariables(sibpath('novars.py'))
+ self.assertEqual({}, localVars)
+
+ def test_variablesFromScript(self):
+ localVars = trial.loadLocalVariables(sibpath('scripttest.py'))
+ self.assertEqual(
+ {'test-case-name': ('twisted.trial.test.test_test_visitor,'
+ 'twisted.trial.test.test_class')},
+ localVars)
+
+ def test_getTestModules(self):
+ modules = trial.getTestModules(sibpath('moduletest.py'))
+ self.assertEqual(modules, ['twisted.trial.test.test_test_visitor'])
+
+ def test_getTestModules_noVars(self):
+ modules = trial.getTestModules(sibpath('novars.py'))
+ self.assertEqual(len(modules), 0)
+
+ def test_getTestModules_multiple(self):
+ modules = trial.getTestModules(sibpath('scripttest.py'))
+ self.assertEqual(set(modules),
+ set(['twisted.trial.test.test_test_visitor',
+ 'twisted.trial.test.test_class']))
+
+ def test_looksLikeTestModule(self):
+ for filename in ['test_script.py', 'twisted/trial/test/test_script.py']:
+ self.failUnless(trial.isTestFile(filename),
+ "%r should be a test file" % (filename,))
+ for filename in ['twisted/trial/test/moduletest.py',
+ sibpath('scripttest.py'), sibpath('test_foo.bat')]:
+ self.failIf(trial.isTestFile(filename),
+ "%r should *not* be a test file" % (filename,))
+
+
+class WithoutModuleTests(unittest.TestCase):
+ """
+ Test the C{without-module} flag.
+ """
+
+ def setUp(self):
+ """
+ Create a L{trial.Options} object to be used in the tests, and save
+ C{sys.modules}.
+ """
+ self.config = trial.Options()
+ self.savedModules = dict(sys.modules)
+
+
+ def tearDown(self):
+ """
+ Restore C{sys.modules}.
+ """
+ for module in ('imaplib', 'smtplib'):
+ if module in self.savedModules:
+ sys.modules[module] = self.savedModules[module]
+ else:
+ sys.modules.pop(module, None)
+
+
+ def _checkSMTP(self):
+ """
+ Try to import the C{smtplib} module, and return it.
+ """
+ import smtplib
+ return smtplib
+
+
+ def _checkIMAP(self):
+ """
+ Try to import the C{imaplib} module, and return it.
+ """
+ import imaplib
+ return imaplib
+
+
+ def test_disableOneModule(self):
+ """
+ Check that after disabling a module, it can't be imported anymore.
+ """
+ self.config.parseOptions(["--without-module", "smtplib"])
+ self.assertRaises(ImportError, self._checkSMTP)
+ # Restore sys.modules
+ del sys.modules["smtplib"]
+ # Then the function should succeed
+ self.assertIsInstance(self._checkSMTP(), types.ModuleType)
+
+
+ def test_disableMultipleModules(self):
+ """
+ Check that several modules can be disabled at once.
+ """
+ self.config.parseOptions(["--without-module", "smtplib,imaplib"])
+ self.assertRaises(ImportError, self._checkSMTP)
+ self.assertRaises(ImportError, self._checkIMAP)
+ # Restore sys.modules
+ del sys.modules["smtplib"]
+ del sys.modules["imaplib"]
+ # Then the functions should succeed
+ self.assertIsInstance(self._checkSMTP(), types.ModuleType)
+ self.assertIsInstance(self._checkIMAP(), types.ModuleType)
+
+
+ def test_disableAlreadyImportedModule(self):
+ """
+ Disabling an already imported module should produce a warning.
+ """
+ self.assertIsInstance(self._checkSMTP(), types.ModuleType)
+ self.assertWarns(RuntimeWarning,
+ "Module 'smtplib' already imported, disabling anyway.",
+ trial.__file__,
+ self.config.parseOptions, ["--without-module", "smtplib"])
+ self.assertRaises(ImportError, self._checkSMTP)
+
+
+
+class CoverageTests(unittest.TestCase):
+ """
+ Tests for the I{coverage} option.
+ """
+ if getattr(sys, 'gettrace', None) is None:
+ skip = (
+ "Cannot test trace hook installation without inspection API.")
+
+ def setUp(self):
+ """
+ Arrange for the current trace hook to be restored when the
+ test is complete.
+ """
+ self.addCleanup(sys.settrace, sys.gettrace())
+
+
+ def test_tracerInstalled(self):
+ """
+ L{trial.Options} handles C{"--coverage"} by installing a trace
+ hook to record coverage information.
+ """
+ options = trial.Options()
+ options.parseOptions(["--coverage"])
+ self.assertEqual(sys.gettrace(), options.tracer.globaltrace)
+
+
+ def test_coverdirDefault(self):
+ """
+ L{trial.Options.coverdir} returns a L{FilePath} based on the default
+ for the I{temp-directory} option if that option is not specified.
+ """
+ options = trial.Options()
+ self.assertEqual(
+ options.coverdir(),
+ FilePath(".").descendant([options["temp-directory"], "coverage"]))
+
+
+ def test_coverdirOverridden(self):
+ """
+ If a value is specified for the I{temp-directory} option,
+ L{trial.Options.coverdir} returns a child of that path.
+ """
+ path = self.mktemp()
+ options = trial.Options()
+ options.parseOptions(["--temp-directory", path])
+ self.assertEqual(
+ options.coverdir(), FilePath(path).child("coverage"))
+
+
+class ExtraTests(unittest.TestCase):
+ """
+ Tests for the I{extra} option.
+ """
+
+ def setUp(self):
+ self.config = trial.Options()
+
+
+ def tearDown(self):
+ self.config = None
+
+
+ def assertDeprecationWarning(self, deprecatedCallable, warnings):
+ """
+ Check for a deprecation warning
+ """
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(warnings[0]['message'],
+ deprecate.getDeprecationWarningString(
+ deprecatedCallable, versions.Version('Twisted', 11, 0, 0)))
+
+
+ def test_extraDeprecation(self):
+ """
+ Check that --extra will emit a deprecation warning
+ """
+ self.config.opt_extra('some.sample.test')
+ self.assertDeprecationWarning(self.config.opt_extra,
+ self.flushWarnings([self.test_extraDeprecation]))
+
+ def test_xDeprecation(self):
+ """
+ Check that -x will emit a deprecation warning
+ """
+ self.config.opt_x('some.sample.text')
+ self.assertDeprecationWarning(self.config.opt_extra,
+ self.flushWarnings([self.test_xDeprecation]))
+
diff --git a/twisted/trial/test/test_test_visitor.py b/twisted/trial/test/test_test_visitor.py
new file mode 100644
index 0000000..b5c3484
--- /dev/null
+++ b/twisted/trial/test/test_test_visitor.py
@@ -0,0 +1,82 @@
+from twisted.trial import unittest
+from twisted.trial.runner import TestSuite, suiteVisit
+
+pyunit = __import__('unittest')
+
+
+
+class MockVisitor(object):
+ def __init__(self):
+ self.calls = []
+
+
+ def __call__(self, testCase):
+ self.calls.append(testCase)
+
+
+
+class TestTestVisitor(unittest.TestCase):
+ def setUp(self):
+ self.visitor = MockVisitor()
+
+
+ def test_visitCase(self):
+ """
+ Test that C{visit} works for a single test case.
+ """
+ testCase = TestTestVisitor('test_visitCase')
+ testCase.visit(self.visitor)
+ self.assertEqual(self.visitor.calls, [testCase])
+
+
+ def test_visitSuite(self):
+ """
+ Test that C{visit} hits all tests in a suite.
+ """
+ tests = [TestTestVisitor('test_visitCase'),
+ TestTestVisitor('test_visitSuite')]
+ testSuite = TestSuite(tests)
+ testSuite.visit(self.visitor)
+ self.assertEqual(self.visitor.calls, tests)
+
+
+ def test_visitEmptySuite(self):
+ """
+ Test that C{visit} on an empty suite hits nothing.
+ """
+ TestSuite().visit(self.visitor)
+ self.assertEqual(self.visitor.calls, [])
+
+
+ def test_visitNestedSuite(self):
+ """
+ Test that C{visit} recurses through suites.
+ """
+ tests = [TestTestVisitor('test_visitCase'),
+ TestTestVisitor('test_visitSuite')]
+ testSuite = TestSuite([TestSuite([test]) for test in tests])
+ testSuite.visit(self.visitor)
+ self.assertEqual(self.visitor.calls, tests)
+
+
+ def test_visitPyunitSuite(self):
+ """
+ Test that C{suiteVisit} visits stdlib unittest suites
+ """
+ test = TestTestVisitor('test_visitPyunitSuite')
+ suite = pyunit.TestSuite([test])
+ suiteVisit(suite, self.visitor)
+ self.assertEqual(self.visitor.calls, [test])
+
+
+ def test_visitPyunitCase(self):
+ """
+ Test that a stdlib test case in a suite gets visited.
+ """
+ class PyunitCase(pyunit.TestCase):
+ def test_foo(self):
+ pass
+ test = PyunitCase('test_foo')
+ TestSuite([test]).visit(self.visitor)
+ self.assertEqual(
+ [call.id() for call in self.visitor.calls], [test.id()])
diff --git a/twisted/trial/test/test_testcase.py b/twisted/trial/test/test_testcase.py
new file mode 100644
index 0000000..8fe02b4
--- /dev/null
+++ b/twisted/trial/test/test_testcase.py
@@ -0,0 +1,51 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Direct unit tests for L{twisted.trial.unittest.TestCase}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+
+class TestCaseTests(TestCase):
+ """
+ L{TestCase} tests.
+ """
+ class MyTestCase(TestCase):
+ """
+ Some test methods which can be used to test behaviors of
+ L{TestCase}.
+ """
+ def test_1(self):
+ pass
+
+ def setUp(self):
+ """
+ Create a couple instances of C{MyTestCase}, each for the same test
+ method, to be used in the test methods of this class.
+ """
+ self.first = self.MyTestCase('test_1')
+ self.second = self.MyTestCase('test_1')
+
+
+ def test_equality(self):
+ """
+ In order for one test method to be runnable twice, two TestCase
+ instances with the same test method name must not compare as equal.
+ """
+ self.assertTrue(self.first == self.first)
+ self.assertTrue(self.first != self.second)
+ self.assertFalse(self.first == self.second)
+
+
+ def test_hashability(self):
+ """
+ In order for one test method to be runnable twice, two TestCase
+ instances with the same test method name should not have the same
+ hash value.
+ """
+ container = {}
+ container[self.first] = None
+ container[self.second] = None
+ self.assertEqual(len(container), 2)
diff --git a/twisted/trial/test/test_tests.py b/twisted/trial/test/test_tests.py
new file mode 100644
index 0000000..5262c06
--- /dev/null
+++ b/twisted/trial/test/test_tests.py
@@ -0,0 +1,1056 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for the behaviour of unit tests.
+"""
+
+import gc, StringIO, sys, weakref
+
+from twisted.internet import defer, reactor
+from twisted.trial import unittest, runner, reporter, util
+from twisted.trial.test import erroneous, suppression
+from twisted.trial.test.test_reporter import LoggingReporter
+
+
+class ResultsTestMixin:
+ def loadSuite(self, suite):
+ self.loader = runner.TestLoader()
+ self.suite = self.loader.loadClass(suite)
+ self.reporter = reporter.TestResult()
+
+ def test_setUp(self):
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(self.reporter.skips, [])
+
+ def assertCount(self, numTests):
+ self.assertEqual(self.suite.countTestCases(), numTests)
+ self.suite(self.reporter)
+ self.assertEqual(self.reporter.testsRun, numTests)
+
+
+
+class TestSuccess(unittest.TestCase):
+ """
+ Test that successful tests are reported as such.
+ """
+
+ def setUp(self):
+ self.result = reporter.TestResult()
+
+
+ def test_successful(self):
+ """
+ A successful test, used by other tests.
+ """
+
+
+ def assertSuccessful(self, test, result):
+ self.assertEqual(result.successes, 1)
+ self.assertEqual(result.failures, [])
+ self.assertEqual(result.errors, [])
+ self.assertEqual(result.expectedFailures, [])
+ self.assertEqual(result.unexpectedSuccesses, [])
+ self.assertEqual(result.skips, [])
+
+
+ def test_successfulIsReported(self):
+ """
+ Test that when a successful test is run, it is reported as a success,
+ and not as any other kind of result.
+ """
+ test = TestSuccess('test_successful')
+ test.run(self.result)
+ self.assertSuccessful(test, self.result)
+
+
+ def test_defaultIsSuccessful(self):
+ """
+ Test that L{unittest.TestCase} itself can be instantiated, run, and
+ reported as being successful.
+ """
+ test = unittest.TestCase()
+ test.run(self.result)
+ self.assertSuccessful(test, self.result)
+
+
+ def test_noReference(self):
+ """
+ Test that no reference is kept on a successful test.
+ """
+ test = TestSuccess('test_successful')
+ ref = weakref.ref(test)
+ test.run(self.result)
+ self.assertSuccessful(test, self.result)
+ del test
+ gc.collect()
+ self.assertIdentical(ref(), None)
+
+
+
+class TestSkipMethods(unittest.TestCase, ResultsTestMixin):
+ class SkippingTests(unittest.TestCase):
+ def test_skip1(self):
+ raise unittest.SkipTest('skip1')
+
+ def test_skip2(self):
+ raise RuntimeError("I should not get raised")
+ test_skip2.skip = 'skip2'
+
+ def test_skip3(self):
+ self.fail('I should not fail')
+ test_skip3.skip = 'skip3'
+
+ class SkippingSetUp(unittest.TestCase):
+ def setUp(self):
+ raise unittest.SkipTest('skipSetUp')
+
+ def test_1(self):
+ pass
+
+ def test_2(self):
+ pass
+
+ def setUp(self):
+ self.loadSuite(TestSkipMethods.SkippingTests)
+
+ def test_counting(self):
+ self.assertCount(3)
+
+ def test_results(self):
+ self.suite(self.reporter)
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(len(self.reporter.skips), 3)
+
+ def test_setUp(self):
+ self.loadSuite(TestSkipMethods.SkippingSetUp)
+ self.suite(self.reporter)
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(len(self.reporter.skips), 2)
+
+ def test_reasons(self):
+ self.suite(self.reporter)
+ prefix = 'test_'
+ # whiteboxing reporter
+ for test, reason in self.reporter.skips:
+ self.assertEqual(test.shortDescription()[len(prefix):],
+ str(reason))
+
+
+class TestSkipClasses(unittest.TestCase, ResultsTestMixin):
+ class SkippedClass(unittest.TestCase):
+ skip = 'class'
+ def setUp(self):
+ self.__class__._setUpRan = True
+ def test_skip1(self):
+ raise unittest.SkipTest('skip1')
+ def test_skip2(self):
+ raise RuntimeError("Ought to skip me")
+ test_skip2.skip = 'skip2'
+ def test_skip3(self):
+ pass
+ def test_skip4(self):
+ raise RuntimeError("Skip me too")
+
+
+ def setUp(self):
+ self.loadSuite(TestSkipClasses.SkippedClass)
+ TestSkipClasses.SkippedClass._setUpRan = False
+
+
+ def test_counting(self):
+ """
+ Skipped test methods still contribute to the total test count.
+ """
+ self.assertCount(4)
+
+
+ def test_setUpRan(self):
+ """
+ The C{setUp} method is not called if the class is set to skip.
+ """
+ self.suite(self.reporter)
+ self.assertFalse(TestSkipClasses.SkippedClass._setUpRan)
+
+
+ def test_results(self):
+ """
+ Skipped test methods don't cause C{wasSuccessful} to return C{False},
+ nor do they contribute to the C{errors} or C{failures} of the reporter.
+ They do, however, add elements to the reporter's C{skips} list.
+ """
+ self.suite(self.reporter)
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(len(self.reporter.skips), 4)
+
+
+ def test_reasons(self):
+ """
+ Test methods which raise L{unittest.SkipTest} or have their C{skip}
+ attribute set to something are skipped.
+ """
+ self.suite(self.reporter)
+ expectedReasons = ['class', 'skip2', 'class', 'class']
+ # whitebox reporter
+ reasonsGiven = [reason for test, reason in self.reporter.skips]
+ self.assertEqual(expectedReasons, reasonsGiven)
+
+
+
+class TestTodo(unittest.TestCase, ResultsTestMixin):
+ class TodoTests(unittest.TestCase):
+ def test_todo1(self):
+ self.fail("deliberate failure")
+ test_todo1.todo = "todo1"
+
+ def test_todo2(self):
+ raise RuntimeError("deliberate error")
+ test_todo2.todo = "todo2"
+
+ def test_todo3(self):
+ """unexpected success"""
+ test_todo3.todo = 'todo3'
+
+ def setUp(self):
+ self.loadSuite(TestTodo.TodoTests)
+
+ def test_counting(self):
+ self.assertCount(3)
+
+ def test_results(self):
+ self.suite(self.reporter)
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(self.reporter.skips, [])
+ self.assertEqual(len(self.reporter.expectedFailures), 2)
+ self.assertEqual(len(self.reporter.unexpectedSuccesses), 1)
+
+ def test_expectedFailures(self):
+ self.suite(self.reporter)
+ expectedReasons = ['todo1', 'todo2']
+ reasonsGiven = [ r.reason
+ for t, e, r in self.reporter.expectedFailures ]
+ self.assertEqual(expectedReasons, reasonsGiven)
+
+ def test_unexpectedSuccesses(self):
+ self.suite(self.reporter)
+ expectedReasons = ['todo3']
+ reasonsGiven = [ r.reason
+ for t, r in self.reporter.unexpectedSuccesses ]
+ self.assertEqual(expectedReasons, reasonsGiven)
+
+
+class TestTodoClass(unittest.TestCase, ResultsTestMixin):
+ class TodoClass(unittest.TestCase):
+ def test_todo1(self):
+ pass
+ test_todo1.todo = "method"
+ def test_todo2(self):
+ pass
+ def test_todo3(self):
+ self.fail("Deliberate Failure")
+ test_todo3.todo = "method"
+ def test_todo4(self):
+ self.fail("Deliberate Failure")
+ TodoClass.todo = "class"
+
+ def setUp(self):
+ self.loadSuite(TestTodoClass.TodoClass)
+
+ def test_counting(self):
+ self.assertCount(4)
+
+ def test_results(self):
+ self.suite(self.reporter)
+ self.failUnless(self.reporter.wasSuccessful())
+ self.assertEqual(self.reporter.errors, [])
+ self.assertEqual(self.reporter.failures, [])
+ self.assertEqual(self.reporter.skips, [])
+ self.assertEqual(len(self.reporter.expectedFailures), 2)
+ self.assertEqual(len(self.reporter.unexpectedSuccesses), 2)
+
+ def test_expectedFailures(self):
+ self.suite(self.reporter)
+ expectedReasons = ['method', 'class']
+ reasonsGiven = [ r.reason
+ for t, e, r in self.reporter.expectedFailures ]
+ self.assertEqual(expectedReasons, reasonsGiven)
+
+ def test_unexpectedSuccesses(self):
+ self.suite(self.reporter)
+ expectedReasons = ['method', 'class']
+ reasonsGiven = [ r.reason
+ for t, r in self.reporter.unexpectedSuccesses ]
+ self.assertEqual(expectedReasons, reasonsGiven)
+
+
+class TestStrictTodo(unittest.TestCase, ResultsTestMixin):
+ class Todos(unittest.TestCase):
+ def test_todo1(self):
+ raise RuntimeError, "expected failure"
+ test_todo1.todo = (RuntimeError, "todo1")
+
+ def test_todo2(self):
+ raise RuntimeError, "expected failure"
+ test_todo2.todo = ((RuntimeError, OSError), "todo2")
+
+ def test_todo3(self):
+ raise RuntimeError, "we had no idea!"
+ test_todo3.todo = (OSError, "todo3")
+
+ def test_todo4(self):
+ raise RuntimeError, "we had no idea!"
+ test_todo4.todo = ((OSError, SyntaxError), "todo4")
+
+ def test_todo5(self):
+ self.fail("deliberate failure")
+ test_todo5.todo = (unittest.FailTest, "todo5")
+
+ def test_todo6(self):
+ self.fail("deliberate failure")
+ test_todo6.todo = (RuntimeError, "todo6")
+
+ def test_todo7(self):
+ pass
+ test_todo7.todo = (RuntimeError, "todo7")
+
+ def setUp(self):
+ self.loadSuite(TestStrictTodo.Todos)
+
+ def test_counting(self):
+ self.assertCount(7)
+
+ def test_results(self):
+ self.suite(self.reporter)
+ self.failIf(self.reporter.wasSuccessful())
+ self.assertEqual(len(self.reporter.errors), 2)
+ self.assertEqual(len(self.reporter.failures), 1)
+ self.assertEqual(len(self.reporter.expectedFailures), 3)
+ self.assertEqual(len(self.reporter.unexpectedSuccesses), 1)
+ self.assertEqual(self.reporter.skips, [])
+
+ def test_expectedFailures(self):
+ self.suite(self.reporter)
+ expectedReasons = ['todo1', 'todo2', 'todo5']
+ reasonsGotten = [ r.reason
+ for t, e, r in self.reporter.expectedFailures ]
+ self.assertEqual(expectedReasons, reasonsGotten)
+
+ def test_unexpectedSuccesses(self):
+ self.suite(self.reporter)
+ expectedReasons = [([RuntimeError], 'todo7')]
+ reasonsGotten = [ (r.errors, r.reason)
+ for t, r in self.reporter.unexpectedSuccesses ]
+ self.assertEqual(expectedReasons, reasonsGotten)
+
+
+
+class TestCleanup(unittest.TestCase):
+
+ def setUp(self):
+ self.result = reporter.Reporter(StringIO.StringIO())
+ self.loader = runner.TestLoader()
+
+
+ def testLeftoverSockets(self):
+ """
+ Trial reports a L{util.DirtyReactorAggregateError} if a test leaves
+ sockets behind.
+ """
+ suite = self.loader.loadMethod(
+ erroneous.SocketOpenTest.test_socketsLeftOpen)
+ suite.run(self.result)
+ self.failIf(self.result.wasSuccessful())
+ # socket cleanup happens at end of class's tests.
+ # all the tests in the class are successful, even if the suite
+ # fails
+ self.assertEqual(self.result.successes, 1)
+ failure = self.result.errors[0][1]
+ self.failUnless(failure.check(util.DirtyReactorAggregateError))
+
+
+ def testLeftoverPendingCalls(self):
+ """
+ Trial reports a L{util.DirtyReactorAggregateError} and fails the test
+ if a test leaves a L{DelayedCall} hanging.
+ """
+ suite = erroneous.ReactorCleanupTests('test_leftoverPendingCalls')
+ suite.run(self.result)
+ self.failIf(self.result.wasSuccessful())
+ failure = self.result.errors[0][1]
+ self.assertEqual(self.result.successes, 0)
+ self.failUnless(failure.check(util.DirtyReactorAggregateError))
+
+
+
+class FixtureTest(unittest.TestCase):
+ """
+ Tests for broken fixture helper methods (e.g. setUp, tearDown).
+ """
+
+ def setUp(self):
+ self.reporter = reporter.Reporter()
+ self.loader = runner.TestLoader()
+
+
+ def testBrokenSetUp(self):
+ """
+ When setUp fails, the error is recorded in the result object.
+ """
+ self.loader.loadClass(erroneous.TestFailureInSetUp).run(self.reporter)
+ self.assert_(len(self.reporter.errors) > 0)
+ self.assert_(isinstance(self.reporter.errors[0][1].value,
+ erroneous.FoolishError))
+
+
+ def testBrokenTearDown(self):
+ """
+ When tearDown fails, the error is recorded in the result object.
+ """
+ suite = self.loader.loadClass(erroneous.TestFailureInTearDown)
+ suite.run(self.reporter)
+ errors = self.reporter.errors
+ self.assert_(len(errors) > 0)
+ self.assert_(isinstance(errors[0][1].value, erroneous.FoolishError))
+
+
+
+class SuppressionTest(unittest.TestCase):
+
+ def runTests(self, suite):
+ suite.run(reporter.TestResult())
+
+
+ def setUp(self):
+ self.loader = runner.TestLoader()
+
+
+ def test_suppressMethod(self):
+ """
+ A suppression set on a test method prevents warnings emitted by that
+ test method which the suppression matches from being emitted.
+ """
+ self.runTests(self.loader.loadMethod(
+ suppression.TestSuppression.testSuppressMethod))
+ warningsShown = self.flushWarnings([
+ suppression.TestSuppression._emit])
+ self.assertEqual(
+ warningsShown[0]['message'], suppression.CLASS_WARNING_MSG)
+ self.assertEqual(
+ warningsShown[1]['message'], suppression.MODULE_WARNING_MSG)
+ self.assertEqual(len(warningsShown), 2)
+
+
+ def test_suppressClass(self):
+ """
+ A suppression set on a L{TestCase} subclass prevents warnings emitted
+ by any test methods defined on that class which match the suppression
+ from being emitted.
+ """
+ self.runTests(self.loader.loadMethod(
+ suppression.TestSuppression.testSuppressClass))
+ warningsShown = self.flushWarnings([
+ suppression.TestSuppression._emit])
+ self.assertEqual(
+ warningsShown[0]['message'], suppression.METHOD_WARNING_MSG)
+ self.assertEqual(
+ warningsShown[1]['message'], suppression.MODULE_WARNING_MSG)
+ self.assertEqual(len(warningsShown), 2)
+
+
+ def test_suppressModule(self):
+ """
+ A suppression set on a module prevents warnings emitted by any test
+ mewthods defined in that module which match the suppression from being
+ emitted.
+ """
+ self.runTests(self.loader.loadMethod(
+ suppression.TestSuppression2.testSuppressModule))
+ warningsShown = self.flushWarnings([
+ suppression.TestSuppression._emit])
+ self.assertEqual(
+ warningsShown[0]['message'], suppression.METHOD_WARNING_MSG)
+ self.assertEqual(
+ warningsShown[1]['message'], suppression.CLASS_WARNING_MSG)
+ self.assertEqual(len(warningsShown), 2)
+
+
+ def test_overrideSuppressClass(self):
+ """
+ The suppression set on a test method completely overrides a suppression
+ with wider scope; if it does not match a warning emitted by that test
+ method, the warning is emitted, even if a wider suppression matches.
+ """
+ case = self.loader.loadMethod(
+ suppression.TestSuppression.testOverrideSuppressClass)
+ self.runTests(case)
+ warningsShown = self.flushWarnings([
+ suppression.TestSuppression._emit])
+ self.assertEqual(
+ warningsShown[0]['message'], suppression.METHOD_WARNING_MSG)
+ self.assertEqual(
+ warningsShown[1]['message'], suppression.CLASS_WARNING_MSG)
+ self.assertEqual(
+ warningsShown[2]['message'], suppression.MODULE_WARNING_MSG)
+ self.assertEqual(len(warningsShown), 3)
+
+
+
+class GCMixin:
+ """
+ I provide a few mock tests that log setUp, tearDown, test execution and
+ garbage collection. I'm used to test whether gc.collect gets called.
+ """
+
+ class BasicTest(unittest.TestCase):
+ def setUp(self):
+ self._log('setUp')
+ def test_foo(self):
+ self._log('test')
+ def tearDown(self):
+ self._log('tearDown')
+
+ class ClassTest(unittest.TestCase):
+ def test_1(self):
+ self._log('test1')
+ def test_2(self):
+ self._log('test2')
+
+ def _log(self, msg):
+ self._collectCalled.append(msg)
+
+ def collect(self):
+ """Fake gc.collect"""
+ self._log('collect')
+
+ def setUp(self):
+ self._collectCalled = []
+ self.BasicTest._log = self.ClassTest._log = self._log
+ self._oldCollect = gc.collect
+ gc.collect = self.collect
+
+ def tearDown(self):
+ gc.collect = self._oldCollect
+
+
+
+class TestGarbageCollectionDefault(GCMixin, unittest.TestCase):
+
+ def test_collectNotDefault(self):
+ """
+ By default, tests should not force garbage collection.
+ """
+ test = self.BasicTest('test_foo')
+ result = reporter.TestResult()
+ test.run(result)
+ self.assertEqual(self._collectCalled, ['setUp', 'test', 'tearDown'])
+
+
+
+class TestGarbageCollection(GCMixin, unittest.TestCase):
+
+ def test_collectCalled(self):
+ """
+ test gc.collect is called before and after each test.
+ """
+ test = TestGarbageCollection.BasicTest('test_foo')
+ test = unittest._ForceGarbageCollectionDecorator(test)
+ result = reporter.TestResult()
+ test.run(result)
+ self.assertEqual(
+ self._collectCalled,
+ ['collect', 'setUp', 'test', 'tearDown', 'collect'])
+
+
+
+class TestUnhandledDeferred(unittest.TestCase):
+
+ def setUp(self):
+ from twisted.trial.test import weird
+ # test_unhandledDeferred creates a cycle. we need explicit control of gc
+ gc.disable()
+ self.test1 = unittest._ForceGarbageCollectionDecorator(
+ weird.TestBleeding('test_unhandledDeferred'))
+
+ def test_isReported(self):
+ """
+ Forcing garbage collection should cause unhandled Deferreds to be
+ reported as errors.
+ """
+ result = reporter.TestResult()
+ self.test1(result)
+ self.assertEqual(len(result.errors), 1,
+ 'Unhandled deferred passed without notice')
+
+ def test_doesntBleed(self):
+ """
+ Forcing garbage collection in the test should mean that there are
+ no unreachable cycles immediately after the test completes.
+ """
+ result = reporter.TestResult()
+ self.test1(result)
+ self.flushLoggedErrors() # test1 logs errors that get caught be us.
+ # test1 created unreachable cycle.
+ # it & all others should have been collected by now.
+ n = gc.collect()
+ self.assertEqual(n, 0, 'unreachable cycle still existed')
+ # check that last gc.collect didn't log more errors
+ x = self.flushLoggedErrors()
+ self.assertEqual(len(x), 0, 'Errors logged after gc.collect')
+
+ def tearDown(self):
+ gc.collect()
+ gc.enable()
+ self.flushLoggedErrors()
+
+
+
+class TestAddCleanup(unittest.TestCase):
+ """
+ Test the addCleanup method of TestCase.
+ """
+
+ class MockTest(unittest.TestCase):
+
+ def setUp(self):
+ self.log = ['setUp']
+
+ def brokenSetUp(self):
+ self.log = ['setUp']
+ raise RuntimeError("Deliberate failure")
+
+ def skippingSetUp(self):
+ self.log = ['setUp']
+ raise unittest.SkipTest("Don't do this")
+
+ def append(self, thing):
+ self.log.append(thing)
+
+ def tearDown(self):
+ self.log.append('tearDown')
+
+ def runTest(self):
+ self.log.append('runTest')
+
+
+ def setUp(self):
+ unittest.TestCase.setUp(self)
+ self.result = reporter.TestResult()
+ self.test = TestAddCleanup.MockTest()
+
+
+ def test_addCleanupCalledIfSetUpFails(self):
+ """
+ Callables added with C{addCleanup} are run even if setUp fails.
+ """
+ self.test.setUp = self.test.brokenSetUp
+ self.test.addCleanup(self.test.append, 'foo')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'foo'], self.test.log)
+
+
+ def test_addCleanupCalledIfSetUpSkips(self):
+ """
+ Callables added with C{addCleanup} are run even if setUp raises
+ L{SkipTest}. This allows test authors to reliably provide clean up
+ code using C{addCleanup}.
+ """
+ self.test.setUp = self.test.skippingSetUp
+ self.test.addCleanup(self.test.append, 'foo')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'foo'], self.test.log)
+
+
+ def test_addCleanupCalledInReverseOrder(self):
+ """
+ Callables added with C{addCleanup} should be called before C{tearDown}
+ in reverse order of addition.
+ """
+ self.test.addCleanup(self.test.append, "foo")
+ self.test.addCleanup(self.test.append, 'bar')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'runTest', 'bar', 'foo', 'tearDown'],
+ self.test.log)
+
+
+ def test_addCleanupWaitsForDeferreds(self):
+ """
+ If an added callable returns a L{Deferred}, then the test should wait
+ until that L{Deferred} has fired before running the next cleanup
+ method.
+ """
+ def cleanup(message):
+ d = defer.Deferred()
+ reactor.callLater(0, d.callback, message)
+ return d.addCallback(self.test.append)
+ self.test.addCleanup(self.test.append, 'foo')
+ self.test.addCleanup(cleanup, 'bar')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'runTest', 'bar', 'foo', 'tearDown'],
+ self.test.log)
+
+
+ def test_errorInCleanupIsCaptured(self):
+ """
+ Errors raised in cleanup functions should be treated like errors in
+ C{tearDown}. They should be added as errors and fail the test. Skips,
+ todos and failures are all treated as errors.
+ """
+ self.test.addCleanup(self.test.fail, 'foo')
+ self.test.run(self.result)
+ self.failIf(self.result.wasSuccessful())
+ self.assertEqual(1, len(self.result.errors))
+ [(test, error)] = self.result.errors
+ self.assertEqual(test, self.test)
+ self.assertEqual(error.getErrorMessage(), 'foo')
+
+
+ def test_cleanupsContinueRunningAfterError(self):
+ """
+ If a cleanup raises an error then that does not stop the other
+ cleanups from being run.
+ """
+ self.test.addCleanup(self.test.append, 'foo')
+ self.test.addCleanup(self.test.fail, 'bar')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'runTest', 'foo', 'tearDown'],
+ self.test.log)
+ self.assertEqual(1, len(self.result.errors))
+ [(test, error)] = self.result.errors
+ self.assertEqual(test, self.test)
+ self.assertEqual(error.getErrorMessage(), 'bar')
+
+
+ def test_multipleErrorsReported(self):
+ """
+ If more than one cleanup fails, then the test should fail with more
+ than one error.
+ """
+ self.test.addCleanup(self.test.fail, 'foo')
+ self.test.addCleanup(self.test.fail, 'bar')
+ self.test.run(self.result)
+ self.assertEqual(['setUp', 'runTest', 'tearDown'],
+ self.test.log)
+ self.assertEqual(2, len(self.result.errors))
+ [(test1, error1), (test2, error2)] = self.result.errors
+ self.assertEqual(test1, self.test)
+ self.assertEqual(test2, self.test)
+ self.assertEqual(error1.getErrorMessage(), 'bar')
+ self.assertEqual(error2.getErrorMessage(), 'foo')
+
+
+
+class TestSuiteClearing(unittest.TestCase):
+ """
+ Tests for our extension that allows us to clear out a L{TestSuite}.
+ """
+
+
+ def test_clearSuite(self):
+ """
+ Calling L{unittest._clearSuite} on a populated L{TestSuite} removes
+ all tests.
+ """
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.TestCase())
+ # Double check that the test suite actually has something in it.
+ self.assertEqual(1, suite.countTestCases())
+ unittest._clearSuite(suite)
+ self.assertEqual(0, suite.countTestCases())
+
+
+ def test_clearPyunitSuite(self):
+ """
+ Calling L{unittest._clearSuite} on a populated standard library
+ L{TestSuite} removes all tests.
+
+ This test is important since C{_clearSuite} operates by mutating
+ internal variables.
+ """
+ pyunit = __import__('unittest')
+ suite = pyunit.TestSuite()
+ suite.addTest(unittest.TestCase())
+ # Double check that the test suite actually has something in it.
+ self.assertEqual(1, suite.countTestCases())
+ unittest._clearSuite(suite)
+ self.assertEqual(0, suite.countTestCases())
+
+
+
+class TestTestDecorator(unittest.TestCase):
+ """
+ Tests for our test decoration features.
+ """
+
+
+ def assertTestsEqual(self, observed, expected):
+ """
+ Assert that the given decorated tests are equal.
+ """
+ self.assertEqual(observed.__class__, expected.__class__,
+ "Different class")
+ observedOriginal = getattr(observed, '_originalTest', None)
+ expectedOriginal = getattr(expected, '_originalTest', None)
+ self.assertIdentical(observedOriginal, expectedOriginal)
+ if observedOriginal is expectedOriginal is None:
+ self.assertIdentical(observed, expected)
+
+
+ def assertSuitesEqual(self, observed, expected):
+ """
+ Assert that the given test suites with decorated tests are equal.
+ """
+ self.assertEqual(observed.__class__, expected.__class__,
+ "Different class")
+ self.assertEqual(len(observed._tests), len(expected._tests),
+ "Different number of tests.")
+ for observedTest, expectedTest in zip(observed._tests,
+ expected._tests):
+ if getattr(observedTest, '_tests', None) is not None:
+ self.assertSuitesEqual(observedTest, expectedTest)
+ else:
+ self.assertTestsEqual(observedTest, expectedTest)
+
+
+ def test_usesAdaptedReporterWithRun(self):
+ """
+ For decorated tests, C{run} uses a result adapter that preserves the
+ test decoration for calls to C{addError}, C{startTest} and the like.
+
+ See L{reporter._AdaptedReporter}.
+ """
+ test = unittest.TestCase()
+ decoratedTest = unittest.TestDecorator(test)
+ result = LoggingReporter()
+ decoratedTest.run(result)
+ self.assertTestsEqual(result.test, decoratedTest)
+
+
+ def test_usesAdaptedReporterWithCall(self):
+ """
+ For decorated tests, C{__call__} uses a result adapter that preserves
+ the test decoration for calls to C{addError}, C{startTest} and the
+ like.
+
+ See L{reporter._AdaptedReporter}.
+ """
+ test = unittest.TestCase()
+ decoratedTest = unittest.TestDecorator(test)
+ result = LoggingReporter()
+ decoratedTest(result)
+ self.assertTestsEqual(result.test, decoratedTest)
+
+
+ def test_decorateSingleTest(self):
+ """
+ Calling L{decorate} on a single test case returns the test case
+ decorated with the provided decorator.
+ """
+ test = unittest.TestCase()
+ decoratedTest = unittest.decorate(test, unittest.TestDecorator)
+ self.assertTestsEqual(unittest.TestDecorator(test), decoratedTest)
+
+
+ def test_decorateTestSuite(self):
+ """
+ Calling L{decorate} on a test suite will return a test suite with
+ each test decorated with the provided decorator.
+ """
+ test = unittest.TestCase()
+ suite = unittest.TestSuite([test])
+ decoratedTest = unittest.decorate(suite, unittest.TestDecorator)
+ self.assertSuitesEqual(
+ decoratedTest, unittest.TestSuite([unittest.TestDecorator(test)]))
+
+
+ def test_decorateInPlaceMutatesOriginal(self):
+ """
+ Calling L{decorate} on a test suite will mutate the original suite.
+ """
+ test = unittest.TestCase()
+ suite = unittest.TestSuite([test])
+ decoratedTest = unittest.decorate(
+ suite, unittest.TestDecorator)
+ self.assertSuitesEqual(
+ decoratedTest, unittest.TestSuite([unittest.TestDecorator(test)]))
+ self.assertSuitesEqual(
+ suite, unittest.TestSuite([unittest.TestDecorator(test)]))
+
+
+ def test_decorateTestSuiteReferences(self):
+ """
+ When decorating a test suite in-place, the number of references to the
+ test objects in that test suite should stay the same.
+
+ Previously, L{unittest.decorate} recreated a test suite, so the
+ original suite kept references to the test objects. This test is here
+ to ensure the problem doesn't reappear again.
+ """
+ getrefcount = getattr(sys, 'getrefcount', None)
+ if getrefcount is None:
+ raise unittest.SkipTest(
+ "getrefcount not supported on this platform")
+ test = unittest.TestCase()
+ suite = unittest.TestSuite([test])
+ count1 = getrefcount(test)
+ decoratedTest = unittest.decorate(suite, unittest.TestDecorator)
+ count2 = getrefcount(test)
+ self.assertEqual(count1, count2)
+
+
+ def test_decorateNestedTestSuite(self):
+ """
+ Calling L{decorate} on a test suite with nested suites will return a
+ test suite that maintains the same structure, but with all tests
+ decorated.
+ """
+ test = unittest.TestCase()
+ suite = unittest.TestSuite([unittest.TestSuite([test])])
+ decoratedTest = unittest.decorate(suite, unittest.TestDecorator)
+ expected = unittest.TestSuite(
+ [unittest.TestSuite([unittest.TestDecorator(test)])])
+ self.assertSuitesEqual(decoratedTest, expected)
+
+
+ def test_decorateDecoratedSuite(self):
+ """
+ Calling L{decorate} on a test suite with already-decorated tests
+ decorates all of the tests in the suite again.
+ """
+ test = unittest.TestCase()
+ decoratedTest = unittest.decorate(test, unittest.TestDecorator)
+ redecoratedTest = unittest.decorate(decoratedTest,
+ unittest.TestDecorator)
+ self.assertTestsEqual(redecoratedTest,
+ unittest.TestDecorator(decoratedTest))
+
+
+ def test_decoratePreservesSuite(self):
+ """
+ Tests can be in non-standard suites. L{decorate} preserves the
+ non-standard suites when it decorates the tests.
+ """
+ test = unittest.TestCase()
+ suite = runner.DestructiveTestSuite([test])
+ decorated = unittest.decorate(suite, unittest.TestDecorator)
+ self.assertSuitesEqual(
+ decorated,
+ runner.DestructiveTestSuite([unittest.TestDecorator(test)]))
+
+
+class TestMonkeyPatchSupport(unittest.TestCase):
+ """
+ Tests for the patch() helper method in L{unittest.TestCase}.
+ """
+
+
+ def setUp(self):
+ self.originalValue = 'original'
+ self.patchedValue = 'patched'
+ self.objectToPatch = self.originalValue
+ self.test = unittest.TestCase()
+
+
+ def test_patch(self):
+ """
+ Calling C{patch()} on a test monkey patches the specified object and
+ attribute.
+ """
+ self.test.patch(self, 'objectToPatch', self.patchedValue)
+ self.assertEqual(self.objectToPatch, self.patchedValue)
+
+
+ def test_patchRestoredAfterRun(self):
+ """
+ Any monkey patches introduced by a test using C{patch()} are reverted
+ after the test has run.
+ """
+ self.test.patch(self, 'objectToPatch', self.patchedValue)
+ self.test.run(reporter.Reporter())
+ self.assertEqual(self.objectToPatch, self.originalValue)
+
+
+ def test_revertDuringTest(self):
+ """
+ C{patch()} return a L{monkey.MonkeyPatcher} object that can be used to
+ restore the original values before the end of the test.
+ """
+ patch = self.test.patch(self, 'objectToPatch', self.patchedValue)
+ patch.restore()
+ self.assertEqual(self.objectToPatch, self.originalValue)
+
+
+ def test_revertAndRepatch(self):
+ """
+ The returned L{monkey.MonkeyPatcher} object can re-apply the patch
+ during the test run.
+ """
+ patch = self.test.patch(self, 'objectToPatch', self.patchedValue)
+ patch.restore()
+ patch.patch()
+ self.assertEqual(self.objectToPatch, self.patchedValue)
+
+
+ def test_successivePatches(self):
+ """
+ Successive patches are applied and reverted just like a single patch.
+ """
+ self.test.patch(self, 'objectToPatch', self.patchedValue)
+ self.assertEqual(self.objectToPatch, self.patchedValue)
+ self.test.patch(self, 'objectToPatch', 'second value')
+ self.assertEqual(self.objectToPatch, 'second value')
+ self.test.run(reporter.Reporter())
+ self.assertEqual(self.objectToPatch, self.originalValue)
+
+
+
+class TestIterateTests(unittest.TestCase):
+ """
+ L{_iterateTests} returns a list of all test cases in a test suite or test
+ case.
+ """
+
+ def test_iterateTestCase(self):
+ """
+ L{_iterateTests} on a single test case returns a list containing that
+ test case.
+ """
+ test = unittest.TestCase()
+ self.assertEqual([test], list(unittest._iterateTests(test)))
+
+
+ def test_iterateSingletonTestSuite(self):
+ """
+ L{_iterateTests} on a test suite that contains a single test case
+ returns a list containing that test case.
+ """
+ test = unittest.TestCase()
+ suite = runner.TestSuite([test])
+ self.assertEqual([test], list(unittest._iterateTests(suite)))
+
+
+ def test_iterateNestedTestSuite(self):
+ """
+ L{_iterateTests} returns tests that are in nested test suites.
+ """
+ test = unittest.TestCase()
+ suite = runner.TestSuite([runner.TestSuite([test])])
+ self.assertEqual([test], list(unittest._iterateTests(suite)))
+
+
+ def test_iterateIsLeftToRightDepthFirst(self):
+ """
+ L{_iterateTests} returns tests in left-to-right, depth-first order.
+ """
+ test = unittest.TestCase()
+ suite = runner.TestSuite([runner.TestSuite([test]), self])
+ self.assertEqual([test, self], list(unittest._iterateTests(suite)))
diff --git a/twisted/trial/test/test_util.py b/twisted/trial/test/test_util.py
new file mode 100644
index 0000000..1c8611a
--- /dev/null
+++ b/twisted/trial/test/test_util.py
@@ -0,0 +1,559 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+
+"""
+Tests for L{twisted.trial.util}
+"""
+
+import os
+
+from zope.interface import implements
+
+from twisted.internet.interfaces import IProcessTransport
+from twisted.internet import defer
+from twisted.internet.base import DelayedCall
+
+from twisted.trial.unittest import TestCase
+from twisted.trial import util
+from twisted.trial.util import DirtyReactorAggregateError, _Janitor
+from twisted.trial.test import packages
+
+
+
+class TestMktemp(TestCase):
+ def test_name(self):
+ name = self.mktemp()
+ dirs = os.path.dirname(name).split(os.sep)[:-1]
+ self.assertEqual(
+ dirs, ['twisted.trial.test.test_util', 'TestMktemp', 'test_name'])
+
+ def test_unique(self):
+ name = self.mktemp()
+ self.failIfEqual(name, self.mktemp())
+
+ def test_created(self):
+ name = self.mktemp()
+ dirname = os.path.dirname(name)
+ self.failUnless(os.path.exists(dirname))
+ self.failIf(os.path.exists(name))
+
+ def test_location(self):
+ path = os.path.abspath(self.mktemp())
+ self.failUnless(path.startswith(os.getcwd()))
+
+
+class TestIntrospection(TestCase):
+ def test_containers(self):
+ import suppression
+ parents = util.getPythonContainers(
+ suppression.TestSuppression2.testSuppressModule)
+ expected = [suppression.TestSuppression2, suppression]
+ for a, b in zip(parents, expected):
+ self.assertEqual(a, b)
+
+
+class TestFindObject(packages.SysPathManglingTest):
+ """
+ Tests for L{twisted.trial.util.findObject}
+ """
+
+ def test_deprecation(self):
+ """
+ Calling L{findObject} results in a deprecation warning
+ """
+ util.findObject('')
+ warningsShown = self.flushWarnings()
+ self.assertEqual(len(warningsShown), 1)
+ self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
+ self.assertEqual(warningsShown[0]['message'],
+ "twisted.trial.util.findObject was deprecated "
+ "in Twisted 10.1.0: Please use "
+ "twisted.python.reflect.namedAny instead.")
+
+
+ def test_importPackage(self):
+ package1 = util.findObject('package')
+ import package as package2
+ self.assertEqual(package1, (True, package2))
+
+ def test_importModule(self):
+ test_sample2 = util.findObject('goodpackage.test_sample')
+ from goodpackage import test_sample
+ self.assertEqual((True, test_sample), test_sample2)
+
+ def test_importError(self):
+ self.failUnlessRaises(ZeroDivisionError,
+ util.findObject, 'package.test_bad_module')
+
+ def test_sophisticatedImportError(self):
+ self.failUnlessRaises(ImportError,
+ util.findObject, 'package2.test_module')
+
+ def test_importNonexistentPackage(self):
+ self.assertEqual(util.findObject('doesntexist')[0], False)
+
+ def test_findNonexistentModule(self):
+ self.assertEqual(util.findObject('package.doesntexist')[0], False)
+
+ def test_findNonexistentObject(self):
+ self.assertEqual(util.findObject(
+ 'goodpackage.test_sample.doesnt')[0], False)
+ self.assertEqual(util.findObject(
+ 'goodpackage.test_sample.AlphabetTest.doesntexist')[0], False)
+
+ def test_findObjectExist(self):
+ alpha1 = util.findObject('goodpackage.test_sample.AlphabetTest')
+ from goodpackage import test_sample
+ self.assertEqual(alpha1, (True, test_sample.AlphabetTest))
+
+
+
+class TestRunSequentially(TestCase):
+ """
+ Sometimes it is useful to be able to run an arbitrary list of callables,
+ one after the other.
+
+ When some of those callables can return Deferreds, things become complex.
+ """
+
+ def test_emptyList(self):
+ """
+ When asked to run an empty list of callables, runSequentially returns a
+ successful Deferred that fires an empty list.
+ """
+ d = util._runSequentially([])
+ d.addCallback(self.assertEqual, [])
+ return d
+
+
+ def test_singleSynchronousSuccess(self):
+ """
+ When given a callable that succeeds without returning a Deferred,
+ include the return value in the results list, tagged with a SUCCESS
+ flag.
+ """
+ d = util._runSequentially([lambda: None])
+ d.addCallback(self.assertEqual, [(defer.SUCCESS, None)])
+ return d
+
+
+ def test_singleSynchronousFailure(self):
+ """
+ When given a callable that raises an exception, include a Failure for
+ that exception in the results list, tagged with a FAILURE flag.
+ """
+ d = util._runSequentially([lambda: self.fail('foo')])
+ def check(results):
+ [(flag, fail)] = results
+ fail.trap(self.failureException)
+ self.assertEqual(fail.getErrorMessage(), 'foo')
+ self.assertEqual(flag, defer.FAILURE)
+ return d.addCallback(check)
+
+
+ def test_singleAsynchronousSuccess(self):
+ """
+ When given a callable that returns a successful Deferred, include the
+ result of the Deferred in the results list, tagged with a SUCCESS flag.
+ """
+ d = util._runSequentially([lambda: defer.succeed(None)])
+ d.addCallback(self.assertEqual, [(defer.SUCCESS, None)])
+ return d
+
+
+ def test_singleAsynchronousFailure(self):
+ """
+ When given a callable that returns a failing Deferred, include the
+ failure the results list, tagged with a FAILURE flag.
+ """
+ d = util._runSequentially([lambda: defer.fail(ValueError('foo'))])
+ def check(results):
+ [(flag, fail)] = results
+ fail.trap(ValueError)
+ self.assertEqual(fail.getErrorMessage(), 'foo')
+ self.assertEqual(flag, defer.FAILURE)
+ return d.addCallback(check)
+
+
+ def test_callablesCalledInOrder(self):
+ """
+ Check that the callables are called in the given order, one after the
+ other.
+ """
+ log = []
+ deferreds = []
+
+ def append(value):
+ d = defer.Deferred()
+ log.append(value)
+ deferreds.append(d)
+ return d
+
+ d = util._runSequentially([lambda: append('foo'),
+ lambda: append('bar')])
+
+ # runSequentially should wait until the Deferred has fired before
+ # running the second callable.
+ self.assertEqual(log, ['foo'])
+ deferreds[-1].callback(None)
+ self.assertEqual(log, ['foo', 'bar'])
+
+ # Because returning created Deferreds makes jml happy.
+ deferreds[-1].callback(None)
+ return d
+
+
+ def test_continuesAfterError(self):
+ """
+ If one of the callables raises an error, then runSequentially continues
+ to run the remaining callables.
+ """
+ d = util._runSequentially([lambda: self.fail('foo'), lambda: 'bar'])
+ def check(results):
+ [(flag1, fail), (flag2, result)] = results
+ fail.trap(self.failureException)
+ self.assertEqual(flag1, defer.FAILURE)
+ self.assertEqual(fail.getErrorMessage(), 'foo')
+ self.assertEqual(flag2, defer.SUCCESS)
+ self.assertEqual(result, 'bar')
+ return d.addCallback(check)
+
+
+ def test_stopOnFirstError(self):
+ """
+ If the C{stopOnFirstError} option is passed to C{runSequentially}, then
+ no further callables are called after the first exception is raised.
+ """
+ d = util._runSequentially([lambda: self.fail('foo'), lambda: 'bar'],
+ stopOnFirstError=True)
+ def check(results):
+ [(flag1, fail)] = results
+ fail.trap(self.failureException)
+ self.assertEqual(flag1, defer.FAILURE)
+ self.assertEqual(fail.getErrorMessage(), 'foo')
+ return d.addCallback(check)
+
+
+ def test_stripFlags(self):
+ """
+ If the C{stripFlags} option is passed to C{runSequentially} then the
+ SUCCESS / FAILURE flags are stripped from the output. Instead, the
+ Deferred fires a flat list of results containing only the results and
+ failures.
+ """
+ d = util._runSequentially([lambda: self.fail('foo'), lambda: 'bar'],
+ stripFlags=True)
+ def check(results):
+ [fail, result] = results
+ fail.trap(self.failureException)
+ self.assertEqual(fail.getErrorMessage(), 'foo')
+ self.assertEqual(result, 'bar')
+ return d.addCallback(check)
+ test_stripFlags.todo = "YAGNI"
+
+
+
+class DirtyReactorAggregateErrorTest(TestCase):
+ """
+ Tests for the L{DirtyReactorAggregateError}.
+ """
+
+ def test_formatDelayedCall(self):
+ """
+ Delayed calls are formatted nicely.
+ """
+ error = DirtyReactorAggregateError(["Foo", "bar"])
+ self.assertEqual(str(error),
+ """\
+Reactor was unclean.
+DelayedCalls: (set twisted.internet.base.DelayedCall.debug = True to debug)
+Foo
+bar""")
+
+
+ def test_formatSelectables(self):
+ """
+ Selectables are formatted nicely.
+ """
+ error = DirtyReactorAggregateError([], ["selectable 1", "selectable 2"])
+ self.assertEqual(str(error),
+ """\
+Reactor was unclean.
+Selectables:
+selectable 1
+selectable 2""")
+
+
+ def test_formatDelayedCallsAndSelectables(self):
+ """
+ Both delayed calls and selectables can appear in the same error.
+ """
+ error = DirtyReactorAggregateError(["bleck", "Boozo"],
+ ["Sel1", "Sel2"])
+ self.assertEqual(str(error),
+ """\
+Reactor was unclean.
+DelayedCalls: (set twisted.internet.base.DelayedCall.debug = True to debug)
+bleck
+Boozo
+Selectables:
+Sel1
+Sel2""")
+
+
+
+class StubReactor(object):
+ """
+ A reactor stub which contains enough functionality to be used with the
+ L{_Janitor}.
+
+ @ivar iterations: A list of the arguments passed to L{iterate}.
+ @ivar removeAllCalled: Number of times that L{removeAll} was called.
+ @ivar selectables: The value that will be returned from L{removeAll}.
+ @ivar delayedCalls: The value to return from L{getDelayedCalls}.
+ """
+
+ def __init__(self, delayedCalls, selectables=None):
+ """
+ @param delayedCalls: See L{StubReactor.delayedCalls}.
+ @param selectables: See L{StubReactor.selectables}.
+ """
+ self.delayedCalls = delayedCalls
+ self.iterations = []
+ self.removeAllCalled = 0
+ if not selectables:
+ selectables = []
+ self.selectables = selectables
+
+
+ def iterate(self, timeout=None):
+ """
+ Increment C{self.iterations}.
+ """
+ self.iterations.append(timeout)
+
+
+ def getDelayedCalls(self):
+ """
+ Return C{self.delayedCalls}.
+ """
+ return self.delayedCalls
+
+
+ def removeAll(self):
+ """
+ Increment C{self.removeAllCalled} and return C{self.selectables}.
+ """
+ self.removeAllCalled += 1
+ return self.selectables
+
+
+
+class StubErrorReporter(object):
+ """
+ A subset of L{twisted.trial.itrial.IReporter} which records L{addError}
+ calls.
+
+ @ivar errors: List of two-tuples of (test, error) which were passed to
+ L{addError}.
+ """
+
+ def __init__(self):
+ self.errors = []
+
+
+ def addError(self, test, error):
+ """
+ Record parameters in C{self.errors}.
+ """
+ self.errors.append((test, error))
+
+
+
+class JanitorTests(TestCase):
+ """
+ Tests for L{_Janitor}!
+ """
+
+ def test_cleanPendingSpinsReactor(self):
+ """
+ During pending-call cleanup, the reactor will be spun twice with an
+ instant timeout. This is not a requirement, it is only a test for
+ current behavior. Hopefully Trial will eventually not do this kind of
+ reactor stuff.
+ """
+ reactor = StubReactor([])
+ jan = _Janitor(None, None, reactor=reactor)
+ jan._cleanPending()
+ self.assertEqual(reactor.iterations, [0, 0])
+
+
+ def test_cleanPendingCancelsCalls(self):
+ """
+ During pending-call cleanup, the janitor cancels pending timed calls.
+ """
+ def func():
+ return "Lulz"
+ cancelled = []
+ delayedCall = DelayedCall(300, func, (), {},
+ cancelled.append, lambda x: None)
+ reactor = StubReactor([delayedCall])
+ jan = _Janitor(None, None, reactor=reactor)
+ jan._cleanPending()
+ self.assertEqual(cancelled, [delayedCall])
+
+
+ def test_cleanPendingReturnsDelayedCallStrings(self):
+ """
+ The Janitor produces string representations of delayed calls from the
+ delayed call cleanup method. It gets the string representations
+ *before* cancelling the calls; this is important because cancelling the
+ call removes critical debugging information from the string
+ representation.
+ """
+ delayedCall = DelayedCall(300, lambda: None, (), {},
+ lambda x: None, lambda x: None,
+ seconds=lambda: 0)
+ delayedCallString = str(delayedCall)
+ reactor = StubReactor([delayedCall])
+ jan = _Janitor(None, None, reactor=reactor)
+ strings = jan._cleanPending()
+ self.assertEqual(strings, [delayedCallString])
+
+
+ def test_cleanReactorRemovesSelectables(self):
+ """
+ The Janitor will remove selectables during reactor cleanup.
+ """
+ reactor = StubReactor([])
+ jan = _Janitor(None, None, reactor=reactor)
+ jan._cleanReactor()
+ self.assertEqual(reactor.removeAllCalled, 1)
+
+
+ def test_cleanReactorKillsProcesses(self):
+ """
+ The Janitor will kill processes during reactor cleanup.
+ """
+ class StubProcessTransport(object):
+ """
+ A stub L{IProcessTransport} provider which records signals.
+ @ivar signals: The signals passed to L{signalProcess}.
+ """
+ implements(IProcessTransport)
+
+ def __init__(self):
+ self.signals = []
+
+ def signalProcess(self, signal):
+ """
+ Append C{signal} to C{self.signals}.
+ """
+ self.signals.append(signal)
+
+ pt = StubProcessTransport()
+ reactor = StubReactor([], [pt])
+ jan = _Janitor(None, None, reactor=reactor)
+ jan._cleanReactor()
+ self.assertEqual(pt.signals, ["KILL"])
+
+
+ def test_cleanReactorReturnsSelectableStrings(self):
+ """
+ The Janitor returns string representations of the selectables that it
+ cleaned up from the reactor cleanup method.
+ """
+ class Selectable(object):
+ """
+ A stub Selectable which only has an interesting string
+ representation.
+ """
+ def __repr__(self):
+ return "(SELECTABLE!)"
+
+ reactor = StubReactor([], [Selectable()])
+ jan = _Janitor(None, None, reactor=reactor)
+ self.assertEqual(jan._cleanReactor(), ["(SELECTABLE!)"])
+
+
+ def test_postCaseCleanupNoErrors(self):
+ """
+ The post-case cleanup method will return True and not call C{addError}
+ on the result if there are no pending calls.
+ """
+ reactor = StubReactor([])
+ test = object()
+ reporter = StubErrorReporter()
+ jan = _Janitor(test, reporter, reactor=reactor)
+ self.assertTrue(jan.postCaseCleanup())
+ self.assertEqual(reporter.errors, [])
+
+
+ def test_postCaseCleanupWithErrors(self):
+ """
+ The post-case cleanup method will return False and call C{addError} on
+ the result with a L{DirtyReactorAggregateError} Failure if there are
+ pending calls.
+ """
+ delayedCall = DelayedCall(300, lambda: None, (), {},
+ lambda x: None, lambda x: None,
+ seconds=lambda: 0)
+ delayedCallString = str(delayedCall)
+ reactor = StubReactor([delayedCall], [])
+ test = object()
+ reporter = StubErrorReporter()
+ jan = _Janitor(test, reporter, reactor=reactor)
+ self.assertFalse(jan.postCaseCleanup())
+ self.assertEqual(len(reporter.errors), 1)
+ self.assertEqual(reporter.errors[0][1].value.delayedCalls,
+ [delayedCallString])
+
+
+ def test_postClassCleanupNoErrors(self):
+ """
+ The post-class cleanup method will not call C{addError} on the result
+ if there are no pending calls or selectables.
+ """
+ reactor = StubReactor([])
+ test = object()
+ reporter = StubErrorReporter()
+ jan = _Janitor(test, reporter, reactor=reactor)
+ jan.postClassCleanup()
+ self.assertEqual(reporter.errors, [])
+
+
+ def test_postClassCleanupWithPendingCallErrors(self):
+ """
+ The post-class cleanup method call C{addError} on the result with a
+ L{DirtyReactorAggregateError} Failure if there are pending calls.
+ """
+ delayedCall = DelayedCall(300, lambda: None, (), {},
+ lambda x: None, lambda x: None,
+ seconds=lambda: 0)
+ delayedCallString = str(delayedCall)
+ reactor = StubReactor([delayedCall], [])
+ test = object()
+ reporter = StubErrorReporter()
+ jan = _Janitor(test, reporter, reactor=reactor)
+ jan.postClassCleanup()
+ self.assertEqual(len(reporter.errors), 1)
+ self.assertEqual(reporter.errors[0][1].value.delayedCalls,
+ [delayedCallString])
+
+
+ def test_postClassCleanupWithSelectableErrors(self):
+ """
+ The post-class cleanup method call C{addError} on the result with a
+ L{DirtyReactorAggregateError} Failure if there are selectables.
+ """
+ selectable = "SELECTABLE HERE"
+ reactor = StubReactor([], [selectable])
+ test = object()
+ reporter = StubErrorReporter()
+ jan = _Janitor(test, reporter, reactor=reactor)
+ jan.postClassCleanup()
+ self.assertEqual(len(reporter.errors), 1)
+ self.assertEqual(reporter.errors[0][1].value.selectables,
+ [repr(selectable)])
+
diff --git a/twisted/trial/test/test_warning.py b/twisted/trial/test/test_warning.py
new file mode 100644
index 0000000..ca4ccdd
--- /dev/null
+++ b/twisted/trial/test/test_warning.py
@@ -0,0 +1,472 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for Trial's interaction with the Python warning system.
+"""
+
+import sys, warnings
+from StringIO import StringIO
+
+from twisted.python.filepath import FilePath
+from twisted.trial.unittest import (TestCase, _collectWarnings,
+ _setWarningRegistryToNone)
+from twisted.trial.reporter import TestResult
+
+class Mask(object):
+ """
+ Hide a L{TestCase} definition from trial's automatic discovery mechanism.
+ """
+ class MockTests(TestCase):
+ """
+ A test case which is used by L{FlushWarningsTests} to verify behavior
+ which cannot be verified by code inside a single test method.
+ """
+ message = "some warning text"
+ category = UserWarning
+
+ def test_unflushed(self):
+ """
+ Generate a warning and don't flush it.
+ """
+ warnings.warn(self.message, self.category)
+
+
+ def test_flushed(self):
+ """
+ Generate a warning and flush it.
+ """
+ warnings.warn(self.message, self.category)
+ self.assertEqual(len(self.flushWarnings()), 1)
+
+
+
+class FlushWarningsTests(TestCase):
+ """
+ Tests for L{TestCase.flushWarnings}, an API for examining the warnings
+ emitted so far in a test.
+ """
+
+ def assertDictSubset(self, set, subset):
+ """
+ Assert that all the keys present in C{subset} are also present in
+ C{set} and that the corresponding values are equal.
+ """
+ for k, v in subset.iteritems():
+ self.assertEqual(set[k], v)
+
+
+ def assertDictSubsets(self, sets, subsets):
+ """
+ For each pair of corresponding elements in C{sets} and C{subsets},
+ assert that the element from C{subsets} is a subset of the element from
+ C{sets}.
+ """
+ self.assertEqual(len(sets), len(subsets))
+ for a, b in zip(sets, subsets):
+ self.assertDictSubset(a, b)
+
+
+ def test_none(self):
+ """
+ If no warnings are emitted by a test, L{TestCase.flushWarnings} returns
+ an empty list.
+ """
+ self.assertEqual(self.flushWarnings(), [])
+
+
+ def test_several(self):
+ """
+ If several warnings are emitted by a test, L{TestCase.flushWarnings}
+ returns a list containing all of them.
+ """
+ firstMessage = "first warning message"
+ firstCategory = UserWarning
+ warnings.warn(message=firstMessage, category=firstCategory)
+
+ secondMessage = "second warning message"
+ secondCategory = RuntimeWarning
+ warnings.warn(message=secondMessage, category=secondCategory)
+
+ self.assertDictSubsets(
+ self.flushWarnings(),
+ [{'category': firstCategory, 'message': firstMessage},
+ {'category': secondCategory, 'message': secondMessage}])
+
+
+ def test_repeated(self):
+ """
+ The same warning triggered twice from the same place is included twice
+ in the list returned by L{TestCase.flushWarnings}.
+ """
+ message = "the message"
+ category = RuntimeWarning
+ for i in range(2):
+ warnings.warn(message=message, category=category)
+
+ self.assertDictSubsets(
+ self.flushWarnings(),
+ [{'category': category, 'message': message}] * 2)
+
+
+ def test_cleared(self):
+ """
+ After a particular warning event has been returned by
+ L{TestCase.flushWarnings}, it is not returned by subsequent calls.
+ """
+ message = "the message"
+ category = RuntimeWarning
+ warnings.warn(message=message, category=category)
+ self.assertDictSubsets(
+ self.flushWarnings(),
+ [{'category': category, 'message': message}])
+ self.assertEqual(self.flushWarnings(), [])
+
+
+ def test_unflushed(self):
+ """
+ Any warnings emitted by a test which are not flushed are emitted to the
+ Python warning system.
+ """
+ result = TestResult()
+ case = Mask.MockTests('test_unflushed')
+ case.run(result)
+ warningsShown = self.flushWarnings([Mask.MockTests.test_unflushed])
+ self.assertEqual(warningsShown[0]['message'], 'some warning text')
+ self.assertIdentical(warningsShown[0]['category'], UserWarning)
+
+ where = case.test_unflushed.im_func.func_code
+ filename = where.co_filename
+ # If someone edits MockTests.test_unflushed, the value added to
+ # firstlineno might need to change.
+ lineno = where.co_firstlineno + 4
+
+ self.assertEqual(warningsShown[0]['filename'], filename)
+ self.assertEqual(warningsShown[0]['lineno'], lineno)
+
+ self.assertEqual(len(warningsShown), 1)
+
+
+ def test_flushed(self):
+ """
+ Any warnings emitted by a test which are flushed are not emitted to the
+ Python warning system.
+ """
+ result = TestResult()
+ case = Mask.MockTests('test_flushed')
+ output = StringIO()
+ monkey = self.patch(sys, 'stdout', output)
+ case.run(result)
+ monkey.restore()
+ self.assertEqual(output.getvalue(), "")
+
+
+ def test_warningsConfiguredAsErrors(self):
+ """
+ If a warnings filter has been installed which turns warnings into
+ exceptions, tests have an error added to the reporter for them for each
+ unflushed warning.
+ """
+ class CustomWarning(Warning):
+ pass
+
+ result = TestResult()
+ case = Mask.MockTests('test_unflushed')
+ case.category = CustomWarning
+
+ originalWarnings = warnings.filters[:]
+ try:
+ warnings.simplefilter('error')
+ case.run(result)
+ self.assertEqual(len(result.errors), 1)
+ self.assertIdentical(result.errors[0][0], case)
+ result.errors[0][1].trap(CustomWarning)
+ finally:
+ warnings.filters[:] = originalWarnings
+
+
+ def test_flushedWarningsConfiguredAsErrors(self):
+ """
+ If a warnings filter has been installed which turns warnings into
+ exceptions, tests which emit those warnings but flush them do not have
+ an error added to the reporter.
+ """
+ class CustomWarning(Warning):
+ pass
+
+ result = TestResult()
+ case = Mask.MockTests('test_flushed')
+ case.category = CustomWarning
+
+ originalWarnings = warnings.filters[:]
+ try:
+ warnings.simplefilter('error')
+ case.run(result)
+ self.assertEqual(result.errors, [])
+ finally:
+ warnings.filters[:] = originalWarnings
+
+
+ def test_multipleFlushes(self):
+ """
+ Any warnings emitted after a call to L{TestCase.flushWarnings} can be
+ flushed by another call to L{TestCase.flushWarnings}.
+ """
+ warnings.warn("first message")
+ self.assertEqual(len(self.flushWarnings()), 1)
+ warnings.warn("second message")
+ self.assertEqual(len(self.flushWarnings()), 1)
+
+
+ def test_filterOnOffendingFunction(self):
+ """
+ The list returned by L{TestCase.flushWarnings} includes only those
+ warnings which refer to the source of the function passed as the value
+ for C{offendingFunction}, if a value is passed for that parameter.
+ """
+ firstMessage = "first warning text"
+ firstCategory = UserWarning
+ def one():
+ warnings.warn(firstMessage, firstCategory, stacklevel=1)
+
+ secondMessage = "some text"
+ secondCategory = RuntimeWarning
+ def two():
+ warnings.warn(secondMessage, secondCategory, stacklevel=1)
+
+ one()
+ two()
+
+ self.assertDictSubsets(
+ self.flushWarnings(offendingFunctions=[one]),
+ [{'category': firstCategory, 'message': firstMessage}])
+ self.assertDictSubsets(
+ self.flushWarnings(offendingFunctions=[two]),
+ [{'category': secondCategory, 'message': secondMessage}])
+
+
+ def test_functionBoundaries(self):
+ """
+ Verify that warnings emitted at the very edges of a function are still
+ determined to be emitted from that function.
+ """
+ def warner():
+ warnings.warn("first line warning")
+ warnings.warn("internal line warning")
+ warnings.warn("last line warning")
+
+ warner()
+ self.assertEqual(
+ len(self.flushWarnings(offendingFunctions=[warner])), 3)
+
+
+ def test_invalidFilter(self):
+ """
+ If an object which is neither a function nor a method is included in
+ the C{offendingFunctions} list, L{TestCase.flushWarnings} raises
+ L{ValueError}. Such a call flushes no warnings.
+ """
+ warnings.warn("oh no")
+ self.assertRaises(ValueError, self.flushWarnings, [None])
+ self.assertEqual(len(self.flushWarnings()), 1)
+
+
+ def test_missingSource(self):
+ """
+ Warnings emitted by a function the source code of which is not
+ available can still be flushed.
+ """
+ package = FilePath(self.mktemp()).child('twisted_private_helper')
+ package.makedirs()
+ package.child('__init__.py').setContent('')
+ package.child('missingsourcefile.py').setContent('''
+import warnings
+def foo():
+ warnings.warn("oh no")
+''')
+ sys.path.insert(0, package.parent().path)
+ self.addCleanup(sys.path.remove, package.parent().path)
+ from twisted_private_helper import missingsourcefile
+ self.addCleanup(sys.modules.pop, 'twisted_private_helper')
+ self.addCleanup(sys.modules.pop, missingsourcefile.__name__)
+ package.child('missingsourcefile.py').remove()
+
+ missingsourcefile.foo()
+ self.assertEqual(len(self.flushWarnings([missingsourcefile.foo])), 1)
+
+
+ def test_renamedSource(self):
+ """
+ Warnings emitted by a function defined in a file which has been renamed
+ since it was initially compiled can still be flushed.
+
+ This is testing the code which specifically supports working around the
+ unfortunate behavior of CPython to write a .py source file name into
+ the .pyc files it generates and then trust that it is correct in
+ various places. If source files are renamed, .pyc files may not be
+ regenerated, but they will contain incorrect filenames.
+ """
+ package = FilePath(self.mktemp()).child('twisted_private_helper')
+ package.makedirs()
+ package.child('__init__.py').setContent('')
+ package.child('module.py').setContent('''
+import warnings
+def foo():
+ warnings.warn("oh no")
+''')
+ sys.path.insert(0, package.parent().path)
+ self.addCleanup(sys.path.remove, package.parent().path)
+
+ # Import it to cause pycs to be generated
+ from twisted_private_helper import module
+
+ # Clean up the state resulting from that import; we're not going to use
+ # this module, so it should go away.
+ del sys.modules['twisted_private_helper']
+ del sys.modules[module.__name__]
+
+ # Rename the source directory
+ package.moveTo(package.sibling('twisted_renamed_helper'))
+
+ # Import the newly renamed version
+ from twisted_renamed_helper import module
+ self.addCleanup(sys.modules.pop, 'twisted_renamed_helper')
+ self.addCleanup(sys.modules.pop, module.__name__)
+
+ # Generate the warning
+ module.foo()
+
+ # Flush it
+ self.assertEqual(len(self.flushWarnings([module.foo])), 1)
+
+
+
+class FakeWarning(Warning):
+ pass
+
+
+
+class CollectWarningsTests(TestCase):
+ """
+ Tests for L{_collectWarnings}.
+ """
+ def test_callsObserver(self):
+ """
+ L{_collectWarnings} calls the observer with each emitted warning.
+ """
+ firstMessage = "dummy calls observer warning"
+ secondMessage = firstMessage[::-1]
+ events = []
+ def f():
+ events.append('call')
+ warnings.warn(firstMessage)
+ warnings.warn(secondMessage)
+ events.append('returning')
+
+ _collectWarnings(events.append, f)
+
+ self.assertEqual(events[0], 'call')
+ self.assertEqual(events[1].message, firstMessage)
+ self.assertEqual(events[2].message, secondMessage)
+ self.assertEqual(events[3], 'returning')
+ self.assertEqual(len(events), 4)
+
+
+ def test_suppresses(self):
+ """
+ Any warnings emitted by a call to a function passed to
+ L{_collectWarnings} are not actually emitted to the warning system.
+ """
+ output = StringIO()
+ self.patch(sys, 'stdout', output)
+ _collectWarnings(lambda x: None, warnings.warn, "text")
+ self.assertEqual(output.getvalue(), "")
+
+
+ def test_callsFunction(self):
+ """
+ L{_collectWarnings} returns the result of calling the callable passed to
+ it with the parameters given.
+ """
+ arguments = []
+ value = object()
+
+ def f(*args, **kwargs):
+ arguments.append((args, kwargs))
+ return value
+
+ result = _collectWarnings(lambda x: None, f, 1, 'a', b=2, c='d')
+ self.assertEqual(arguments, [((1, 'a'), {'b': 2, 'c': 'd'})])
+ self.assertIdentical(result, value)
+
+
+ def test_duplicateWarningCollected(self):
+ """
+ Subsequent emissions of a warning from a particular source site can be
+ collected by L{_collectWarnings}. In particular, the per-module
+ emitted-warning cache should be bypassed (I{__warningregistry__}).
+ """
+ # Make sure the worst case is tested: if __warningregistry__ isn't in a
+ # module's globals, then the warning system will add it and start using
+ # it to avoid emitting duplicate warnings. Delete __warningregistry__
+ # to ensure that even modules which are first imported as a test is
+ # running still interact properly with the warning system.
+ global __warningregistry__
+ del __warningregistry__
+
+ def f():
+ warnings.warn("foo")
+ warnings.simplefilter('default')
+ f()
+ events = []
+ _collectWarnings(events.append, f)
+ self.assertEqual(len(events), 1)
+ self.assertEqual(events[0].message, "foo")
+ self.assertEqual(len(self.flushWarnings()), 1)
+
+
+ def test_immutableObject(self):
+ """
+ L{_collectWarnings}'s behavior is not altered by the presence of an
+ object which cannot have attributes set on it as a value in
+ C{sys.modules}.
+ """
+ key = object()
+ sys.modules[key] = key
+ self.addCleanup(sys.modules.pop, key)
+ self.test_duplicateWarningCollected()
+
+
+ def test_setWarningRegistryChangeWhileIterating(self):
+ """
+ If the dictionary passed to L{_setWarningRegistryToNone} changes size
+ partway through the process, C{_setWarningRegistryToNone} continues to
+ set C{__warningregistry__} to C{None} on the rest of the values anyway.
+
+
+ This might be caused by C{sys.modules} containing something that's not
+ really a module and imports things on setattr. py.test does this, as
+ does L{twisted.python.deprecate.deprecatedModuleAttribute}.
+ """
+ d = {}
+
+ class A(object):
+ def __init__(self, key):
+ self.__dict__['_key'] = key
+
+ def __setattr__(self, value, item):
+ d[self._key] = None
+
+ key1 = object()
+ key2 = object()
+ d[key1] = A(key2)
+
+ key3 = object()
+ key4 = object()
+ d[key3] = A(key4)
+
+ _setWarningRegistryToNone(d)
+
+ # If both key2 and key4 were added, then both A instanced were
+ # processed.
+ self.assertEqual(set([key1, key2, key3, key4]), set(d.keys()))
diff --git a/twisted/trial/test/weird.py b/twisted/trial/test/weird.py
new file mode 100644
index 0000000..e35526d
--- /dev/null
+++ b/twisted/trial/test/weird.py
@@ -0,0 +1,20 @@
+from twisted.trial import unittest
+from twisted.internet import defer
+
+# Used in test_tests.TestUnhandledDeferred
+
+class TestBleeding(unittest.TestCase):
+ """This test creates an unhandled Deferred and leaves it in a cycle.
+
+ The Deferred is left in a cycle so that the garbage collector won't pick it
+ up immediately. We were having some problems where unhandled Deferreds in
+ one test were failing random other tests. (See #1507, #1213)
+ """
+ def test_unhandledDeferred(self):
+ try:
+ 1/0
+ except ZeroDivisionError:
+ f = defer.fail()
+ # these two lines create the cycle. don't remove them
+ l = [f]
+ l.append(l)
diff --git a/twisted/trial/unittest.py b/twisted/trial/unittest.py
new file mode 100644
index 0000000..c6522fe
--- /dev/null
+++ b/twisted/trial/unittest.py
@@ -0,0 +1,1620 @@
+# -*- test-case-name: twisted.trial.test.test_tests -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Things likely to be used by writers of unit tests.
+
+Maintainer: Jonathan Lange
+"""
+
+
+import doctest, inspect
+import os, warnings, sys, tempfile, gc, types
+from pprint import pformat
+from dis import findlinestarts as _findlinestarts
+
+from twisted.internet import defer, utils
+from twisted.python import components, failure, log, monkey
+from twisted.python.deprecate import getDeprecationWarningString
+
+from twisted.trial import itrial, reporter, util
+
+pyunit = __import__('unittest')
+
+from zope.interface import implements
+
+
+
+class SkipTest(Exception):
+ """
+ Raise this (with a reason) to skip the current test. You may also set
+ method.skip to a reason string to skip it, or set class.skip to skip the
+ entire TestCase.
+ """
+
+
+class FailTest(AssertionError):
+ """Raised to indicate the current test has failed to pass."""
+
+
+class Todo(object):
+ """
+ Internal object used to mark a L{TestCase} as 'todo'. Tests marked 'todo'
+ are reported differently in Trial L{TestResult}s. If todo'd tests fail,
+ they do not fail the suite and the errors are reported in a separate
+ category. If todo'd tests succeed, Trial L{TestResult}s will report an
+ unexpected success.
+ """
+
+ def __init__(self, reason, errors=None):
+ """
+ @param reason: A string explaining why the test is marked 'todo'
+
+ @param errors: An iterable of exception types that the test is
+ expected to raise. If one of these errors is raised by the test, it
+ will be trapped. Raising any other kind of error will fail the test.
+ If C{None} is passed, then all errors will be trapped.
+ """
+ self.reason = reason
+ self.errors = errors
+
+ def __repr__(self):
+ return "<Todo reason=%r errors=%r>" % (self.reason, self.errors)
+
+ def expected(self, failure):
+ """
+ @param failure: A L{twisted.python.failure.Failure}.
+
+ @return: C{True} if C{failure} is expected, C{False} otherwise.
+ """
+ if self.errors is None:
+ return True
+ for error in self.errors:
+ if failure.check(error):
+ return True
+ return False
+
+
+def makeTodo(value):
+ """
+ Return a L{Todo} object built from C{value}.
+
+ If C{value} is a string, return a Todo that expects any exception with
+ C{value} as a reason. If C{value} is a tuple, the second element is used
+ as the reason and the first element as the excepted error(s).
+
+ @param value: A string or a tuple of C{(errors, reason)}, where C{errors}
+ is either a single exception class or an iterable of exception classes.
+
+ @return: A L{Todo} object.
+ """
+ if isinstance(value, str):
+ return Todo(reason=value)
+ if isinstance(value, tuple):
+ errors, reason = value
+ try:
+ errors = list(errors)
+ except TypeError:
+ errors = [errors]
+ return Todo(reason=reason, errors=errors)
+
+
+
+class _Warning(object):
+ """
+ A L{_Warning} instance represents one warning emitted through the Python
+ warning system (L{warnings}). This is used to insulate callers of
+ L{_collectWarnings} from changes to the Python warnings system which might
+ otherwise require changes to the warning objects that function passes to
+ the observer object it accepts.
+
+ @ivar message: The string which was passed as the message parameter to
+ L{warnings.warn}.
+
+ @ivar category: The L{Warning} subclass which was passed as the category
+ parameter to L{warnings.warn}.
+
+ @ivar filename: The name of the file containing the definition of the code
+ object which was C{stacklevel} frames above the call to
+ L{warnings.warn}, where C{stacklevel} is the value of the C{stacklevel}
+ parameter passed to L{warnings.warn}.
+
+ @ivar lineno: The source line associated with the active instruction of the
+ code object object which was C{stacklevel} frames above the call to
+ L{warnings.warn}, where C{stacklevel} is the value of the C{stacklevel}
+ parameter passed to L{warnings.warn}.
+ """
+ def __init__(self, message, category, filename, lineno):
+ self.message = message
+ self.category = category
+ self.filename = filename
+ self.lineno = lineno
+
+
+def _setWarningRegistryToNone(modules):
+ """
+ Disable the per-module cache for every module found in C{modules}, typically
+ C{sys.modules}.
+
+ @param modules: Dictionary of modules, typically sys.module dict
+ """
+ for v in modules.values():
+ if v is not None:
+ try:
+ v.__warningregistry__ = None
+ except:
+ # Don't specify a particular exception type to handle in case
+ # some wacky object raises some wacky exception in response to
+ # the setattr attempt.
+ pass
+
+
+def _collectWarnings(observeWarning, f, *args, **kwargs):
+ """
+ Call C{f} with C{args} positional arguments and C{kwargs} keyword arguments
+ and collect all warnings which are emitted as a result in a list.
+
+ @param observeWarning: A callable which will be invoked with a L{_Warning}
+ instance each time a warning is emitted.
+
+ @return: The return value of C{f(*args, **kwargs)}.
+ """
+ def showWarning(message, category, filename, lineno, file=None, line=None):
+ assert isinstance(message, Warning)
+ observeWarning(_Warning(
+ message.args[0], category, filename, lineno))
+
+ # Disable the per-module cache for every module otherwise if the warning
+ # which the caller is expecting us to collect was already emitted it won't
+ # be re-emitted by the call to f which happens below.
+ _setWarningRegistryToNone(sys.modules)
+
+ origFilters = warnings.filters[:]
+ origShow = warnings.showwarning
+ warnings.simplefilter('always')
+ try:
+ warnings.showwarning = showWarning
+ result = f(*args, **kwargs)
+ finally:
+ warnings.filters[:] = origFilters
+ warnings.showwarning = origShow
+ return result
+
+
+
+class _Assertions(pyunit.TestCase, object):
+ """
+ Replaces many of the built-in TestCase assertions. In general, these
+ assertions provide better error messages and are easier to use in
+ callbacks. Also provides new assertions such as L{failUnlessFailure}.
+
+ Although the tests are defined as 'failIf*' and 'failUnless*', they can
+ also be called as 'assertNot*' and 'assert*'.
+ """
+
+ def fail(self, msg=None):
+ """
+ Absolutely fail the test. Do not pass go, do not collect $200.
+
+ @param msg: the message that will be displayed as the reason for the
+ failure
+ """
+ raise self.failureException(msg)
+
+ def failIf(self, condition, msg=None):
+ """
+ Fail the test if C{condition} evaluates to True.
+
+ @param condition: any object that defines __nonzero__
+ """
+ if condition:
+ raise self.failureException(msg)
+ return condition
+ assertNot = assertFalse = failUnlessFalse = failIf
+
+ def failUnless(self, condition, msg=None):
+ """
+ Fail the test if C{condition} evaluates to False.
+
+ @param condition: any object that defines __nonzero__
+ """
+ if not condition:
+ raise self.failureException(msg)
+ return condition
+ assert_ = assertTrue = failUnlessTrue = failUnless
+
+ def failUnlessRaises(self, exception, f, *args, **kwargs):
+ """
+ Fail the test unless calling the function C{f} with the given
+ C{args} and C{kwargs} raises C{exception}. The failure will report
+ the traceback and call stack of the unexpected exception.
+
+ @param exception: exception type that is to be expected
+ @param f: the function to call
+
+ @return: The raised exception instance, if it is of the given type.
+ @raise self.failureException: Raised if the function call does
+ not raise an exception or if it raises an exception of a
+ different type.
+ """
+ try:
+ result = f(*args, **kwargs)
+ except exception, inst:
+ return inst
+ except:
+ raise self.failureException('%s raised instead of %s:\n %s'
+ % (sys.exc_info()[0],
+ exception.__name__,
+ failure.Failure().getTraceback()))
+ else:
+ raise self.failureException('%s not raised (%r returned)'
+ % (exception.__name__, result))
+ assertRaises = failUnlessRaises
+
+
+ def assertEqual(self, first, second, msg=''):
+ """
+ Fail the test if C{first} and C{second} are not equal.
+
+ @param msg: A string describing the failure that's included in the
+ exception.
+ """
+ if not first == second:
+ if msg is None:
+ msg = ''
+ if len(msg) > 0:
+ msg += '\n'
+ raise self.failureException(
+ '%snot equal:\na = %s\nb = %s\n'
+ % (msg, pformat(first), pformat(second)))
+ return first
+ failUnlessEqual = failUnlessEquals = assertEquals = assertEqual
+
+
+ def failUnlessIdentical(self, first, second, msg=None):
+ """
+ Fail the test if C{first} is not C{second}. This is an
+ obect-identity-equality test, not an object equality
+ (i.e. C{__eq__}) test.
+
+ @param msg: if msg is None, then the failure message will be
+ '%r is not %r' % (first, second)
+ """
+ if first is not second:
+ raise self.failureException(msg or '%r is not %r' % (first, second))
+ return first
+ assertIdentical = failUnlessIdentical
+
+ def failIfIdentical(self, first, second, msg=None):
+ """
+ Fail the test if C{first} is C{second}. This is an
+ obect-identity-equality test, not an object equality
+ (i.e. C{__eq__}) test.
+
+ @param msg: if msg is None, then the failure message will be
+ '%r is %r' % (first, second)
+ """
+ if first is second:
+ raise self.failureException(msg or '%r is %r' % (first, second))
+ return first
+ assertNotIdentical = failIfIdentical
+
+ def failIfEqual(self, first, second, msg=None):
+ """
+ Fail the test if C{first} == C{second}.
+
+ @param msg: if msg is None, then the failure message will be
+ '%r == %r' % (first, second)
+ """
+ if not first != second:
+ raise self.failureException(msg or '%r == %r' % (first, second))
+ return first
+ assertNotEqual = assertNotEquals = failIfEquals = failIfEqual
+
+ def failUnlessIn(self, containee, container, msg=None):
+ """
+ Fail the test if C{containee} is not found in C{container}.
+
+ @param containee: the value that should be in C{container}
+ @param container: a sequence type, or in the case of a mapping type,
+ will follow semantics of 'if key in dict.keys()'
+ @param msg: if msg is None, then the failure message will be
+ '%r not in %r' % (first, second)
+ """
+ if containee not in container:
+ raise self.failureException(msg or "%r not in %r"
+ % (containee, container))
+ return containee
+ assertIn = failUnlessIn
+
+ def failIfIn(self, containee, container, msg=None):
+ """
+ Fail the test if C{containee} is found in C{container}.
+
+ @param containee: the value that should not be in C{container}
+ @param container: a sequence type, or in the case of a mapping type,
+ will follow semantics of 'if key in dict.keys()'
+ @param msg: if msg is None, then the failure message will be
+ '%r in %r' % (first, second)
+ """
+ if containee in container:
+ raise self.failureException(msg or "%r in %r"
+ % (containee, container))
+ return containee
+ assertNotIn = failIfIn
+
+ def failIfAlmostEqual(self, first, second, places=7, msg=None):
+ """
+ Fail if the two objects are equal as determined by their
+ difference rounded to the given number of decimal places
+ (default 7) and comparing to zero.
+
+ @note: decimal places (from zero) is usually not the same
+ as significant digits (measured from the most
+ signficant digit).
+
+ @note: included for compatiblity with PyUnit test cases
+ """
+ if round(second-first, places) == 0:
+ raise self.failureException(msg or '%r == %r within %r places'
+ % (first, second, places))
+ return first
+ assertNotAlmostEqual = assertNotAlmostEquals = failIfAlmostEqual
+ failIfAlmostEquals = failIfAlmostEqual
+
+ def failUnlessAlmostEqual(self, first, second, places=7, msg=None):
+ """
+ Fail if the two objects are unequal as determined by their
+ difference rounded to the given number of decimal places
+ (default 7) and comparing to zero.
+
+ @note: decimal places (from zero) is usually not the same
+ as significant digits (measured from the most
+ signficant digit).
+
+ @note: included for compatiblity with PyUnit test cases
+ """
+ if round(second-first, places) != 0:
+ raise self.failureException(msg or '%r != %r within %r places'
+ % (first, second, places))
+ return first
+ assertAlmostEqual = assertAlmostEquals = failUnlessAlmostEqual
+ failUnlessAlmostEquals = failUnlessAlmostEqual
+
+ def failUnlessApproximates(self, first, second, tolerance, msg=None):
+ """
+ Fail if C{first} - C{second} > C{tolerance}
+
+ @param msg: if msg is None, then the failure message will be
+ '%r ~== %r' % (first, second)
+ """
+ if abs(first - second) > tolerance:
+ raise self.failureException(msg or "%s ~== %s" % (first, second))
+ return first
+ assertApproximates = failUnlessApproximates
+
+ def failUnlessFailure(self, deferred, *expectedFailures):
+ """
+ Fail if C{deferred} does not errback with one of C{expectedFailures}.
+ Returns the original Deferred with callbacks added. You will need
+ to return this Deferred from your test case.
+ """
+ def _cb(ignore):
+ raise self.failureException(
+ "did not catch an error, instead got %r" % (ignore,))
+
+ def _eb(failure):
+ if failure.check(*expectedFailures):
+ return failure.value
+ else:
+ output = ('\nExpected: %r\nGot:\n%s'
+ % (expectedFailures, str(failure)))
+ raise self.failureException(output)
+ return deferred.addCallbacks(_cb, _eb)
+ assertFailure = failUnlessFailure
+
+ def failUnlessSubstring(self, substring, astring, msg=None):
+ """
+ Fail if C{substring} does not exist within C{astring}.
+ """
+ return self.failUnlessIn(substring, astring, msg)
+ assertSubstring = failUnlessSubstring
+
+ def failIfSubstring(self, substring, astring, msg=None):
+ """
+ Fail if C{astring} contains C{substring}.
+ """
+ return self.failIfIn(substring, astring, msg)
+ assertNotSubstring = failIfSubstring
+
+ def failUnlessWarns(self, category, message, filename, f,
+ *args, **kwargs):
+ """
+ Fail if the given function doesn't generate the specified warning when
+ called. It calls the function, checks the warning, and forwards the
+ result of the function if everything is fine.
+
+ @param category: the category of the warning to check.
+ @param message: the output message of the warning to check.
+ @param filename: the filename where the warning should come from.
+ @param f: the function which is supposed to generate the warning.
+ @type f: any callable.
+ @param args: the arguments to C{f}.
+ @param kwargs: the keywords arguments to C{f}.
+
+ @return: the result of the original function C{f}.
+ """
+ warningsShown = []
+ result = _collectWarnings(warningsShown.append, f, *args, **kwargs)
+
+ if not warningsShown:
+ self.fail("No warnings emitted")
+ first = warningsShown[0]
+ for other in warningsShown[1:]:
+ if ((other.message, other.category)
+ != (first.message, first.category)):
+ self.fail("Can't handle different warnings")
+ self.assertEqual(first.message, message)
+ self.assertIdentical(first.category, category)
+
+ # Use starts with because of .pyc/.pyo issues.
+ self.failUnless(
+ filename.startswith(first.filename),
+ 'Warning in %r, expected %r' % (first.filename, filename))
+
+ # It would be nice to be able to check the line number as well, but
+ # different configurations actually end up reporting different line
+ # numbers (generally the variation is only 1 line, but that's enough
+ # to fail the test erroneously...).
+ # self.assertEqual(lineno, xxx)
+
+ return result
+ assertWarns = failUnlessWarns
+
+ def failUnlessIsInstance(self, instance, classOrTuple, message=None):
+ """
+ Fail if C{instance} is not an instance of the given class or of
+ one of the given classes.
+
+ @param instance: the object to test the type (first argument of the
+ C{isinstance} call).
+ @type instance: any.
+ @param classOrTuple: the class or classes to test against (second
+ argument of the C{isinstance} call).
+ @type classOrTuple: class, type, or tuple.
+
+ @param message: Custom text to include in the exception text if the
+ assertion fails.
+ """
+ if not isinstance(instance, classOrTuple):
+ if message is None:
+ suffix = ""
+ else:
+ suffix = ": " + message
+ self.fail("%r is not an instance of %s%s" % (
+ instance, classOrTuple, suffix))
+ assertIsInstance = failUnlessIsInstance
+
+ def failIfIsInstance(self, instance, classOrTuple):
+ """
+ Fail if C{instance} is not an instance of the given class or of
+ one of the given classes.
+
+ @param instance: the object to test the type (first argument of the
+ C{isinstance} call).
+ @type instance: any.
+ @param classOrTuple: the class or classes to test against (second
+ argument of the C{isinstance} call).
+ @type classOrTuple: class, type, or tuple.
+ """
+ if isinstance(instance, classOrTuple):
+ self.fail("%r is an instance of %s" % (instance, classOrTuple))
+ assertNotIsInstance = failIfIsInstance
+
+
+class _LogObserver(object):
+ """
+ Observes the Twisted logs and catches any errors.
+
+ @ivar _errors: A C{list} of L{Failure} instances which were received as
+ error events from the Twisted logging system.
+
+ @ivar _added: A C{int} giving the number of times C{_add} has been called
+ less the number of times C{_remove} has been called; used to only add
+ this observer to the Twisted logging since once, regardless of the
+ number of calls to the add method.
+
+ @ivar _ignored: A C{list} of exception types which will not be recorded.
+ """
+
+ def __init__(self):
+ self._errors = []
+ self._added = 0
+ self._ignored = []
+
+
+ def _add(self):
+ if self._added == 0:
+ log.addObserver(self.gotEvent)
+ self._oldFE, log._flushErrors = (log._flushErrors, self.flushErrors)
+ self._oldIE, log._ignore = (log._ignore, self._ignoreErrors)
+ self._oldCI, log._clearIgnores = (log._clearIgnores,
+ self._clearIgnores)
+ self._added += 1
+
+ def _remove(self):
+ self._added -= 1
+ if self._added == 0:
+ log.removeObserver(self.gotEvent)
+ log._flushErrors = self._oldFE
+ log._ignore = self._oldIE
+ log._clearIgnores = self._oldCI
+
+
+ def _ignoreErrors(self, *errorTypes):
+ """
+ Do not store any errors with any of the given types.
+ """
+ self._ignored.extend(errorTypes)
+
+
+ def _clearIgnores(self):
+ """
+ Stop ignoring any errors we might currently be ignoring.
+ """
+ self._ignored = []
+
+
+ def flushErrors(self, *errorTypes):
+ """
+ Flush errors from the list of caught errors. If no arguments are
+ specified, remove all errors. If arguments are specified, only remove
+ errors of those types from the stored list.
+ """
+ if errorTypes:
+ flushed = []
+ remainder = []
+ for f in self._errors:
+ if f.check(*errorTypes):
+ flushed.append(f)
+ else:
+ remainder.append(f)
+ self._errors = remainder
+ else:
+ flushed = self._errors
+ self._errors = []
+ return flushed
+
+
+ def getErrors(self):
+ """
+ Return a list of errors caught by this observer.
+ """
+ return self._errors
+
+
+ def gotEvent(self, event):
+ """
+ The actual observer method. Called whenever a message is logged.
+
+ @param event: A dictionary containing the log message. Actual
+ structure undocumented (see source for L{twisted.python.log}).
+ """
+ if event.get('isError', False) and 'failure' in event:
+ f = event['failure']
+ if len(self._ignored) == 0 or not f.check(*self._ignored):
+ self._errors.append(f)
+
+
+
+_logObserver = _LogObserver()
+
+_wait_is_running = []
+
+class TestCase(_Assertions):
+ """
+ A unit test. The atom of the unit testing universe.
+
+ This class extends C{unittest.TestCase} from the standard library. The
+ main feature is the ability to return C{Deferred}s from tests and fixture
+ methods and to have the suite wait for those C{Deferred}s to fire.
+
+ To write a unit test, subclass C{TestCase} and define a method (say,
+ 'test_foo') on the subclass. To run the test, instantiate your subclass
+ with the name of the method, and call L{run} on the instance, passing a
+ L{TestResult} object.
+
+ The C{trial} script will automatically find any C{TestCase} subclasses
+ defined in modules beginning with 'test_' and construct test cases for all
+ methods beginning with 'test'.
+
+ If an error is logged during the test run, the test will fail with an
+ error. See L{log.err}.
+
+ @ivar failureException: An exception class, defaulting to C{FailTest}. If
+ the test method raises this exception, it will be reported as a failure,
+ rather than an exception. All of the assertion methods raise this if the
+ assertion fails.
+
+ @ivar skip: C{None} or a string explaining why this test is to be
+ skipped. If defined, the test will not be run. Instead, it will be
+ reported to the result object as 'skipped' (if the C{TestResult} supports
+ skipping).
+
+ @ivar suppress: C{None} or a list of tuples of C{(args, kwargs)} to be
+ passed to C{warnings.filterwarnings}. Use these to suppress warnings
+ raised in a test. Useful for testing deprecated code. See also
+ L{util.suppress}.
+
+ @ivar timeout: A real number of seconds. If set, the test will
+ raise an error if it takes longer than C{timeout} seconds.
+ If not set, util.DEFAULT_TIMEOUT_DURATION is used.
+
+ @ivar todo: C{None}, a string or a tuple of C{(errors, reason)} where
+ C{errors} is either an exception class or an iterable of exception
+ classes, and C{reason} is a string. See L{Todo} or L{makeTodo} for more
+ information.
+ """
+
+ implements(itrial.ITestCase)
+ failureException = FailTest
+
+ def __init__(self, methodName='runTest'):
+ """
+ Construct an asynchronous test case for C{methodName}.
+
+ @param methodName: The name of a method on C{self}. This method should
+ be a unit test. That is, it should be a short method that calls some of
+ the assert* methods. If C{methodName} is unspecified, L{runTest} will
+ be used as the test method. This is mostly useful for testing Trial.
+ """
+ super(TestCase, self).__init__(methodName)
+ self._testMethodName = methodName
+ testMethod = getattr(self, methodName)
+ self._parents = [testMethod, self]
+ self._parents.extend(util.getPythonContainers(testMethod))
+ self._passed = False
+ self._cleanups = []
+
+ if sys.version_info >= (2, 6):
+ # Override the comparison defined by the base TestCase which considers
+ # instances of the same class with the same _testMethodName to be
+ # equal. Since trial puts TestCase instances into a set, that
+ # definition of comparison makes it impossible to run the same test
+ # method twice. Most likely, trial should stop using a set to hold
+ # tests, but until it does, this is necessary on Python 2.6. Only
+ # __eq__ and __ne__ are required here, not __hash__, since the
+ # inherited __hash__ is compatible with these equality semantics. A
+ # different __hash__ might be slightly more efficient (by reducing
+ # collisions), but who cares? -exarkun
+ def __eq__(self, other):
+ return self is other
+
+ def __ne__(self, other):
+ return self is not other
+
+
+ def _run(self, methodName, result):
+ from twisted.internet import reactor
+ timeout = self.getTimeout()
+ def onTimeout(d):
+ e = defer.TimeoutError("%r (%s) still running at %s secs"
+ % (self, methodName, timeout))
+ f = failure.Failure(e)
+ # try to errback the deferred that the test returns (for no gorram
+ # reason) (see issue1005 and test_errorPropagation in
+ # test_deferred)
+ try:
+ d.errback(f)
+ except defer.AlreadyCalledError:
+ # if the deferred has been called already but the *back chain
+ # is still unfinished, crash the reactor and report timeout
+ # error ourself.
+ reactor.crash()
+ self._timedOut = True # see self._wait
+ todo = self.getTodo()
+ if todo is not None and todo.expected(f):
+ result.addExpectedFailure(self, f, todo)
+ else:
+ result.addError(self, f)
+ onTimeout = utils.suppressWarnings(
+ onTimeout, util.suppress(category=DeprecationWarning))
+ method = getattr(self, methodName)
+ d = defer.maybeDeferred(utils.runWithWarningsSuppressed,
+ self.getSuppress(), method)
+ call = reactor.callLater(timeout, onTimeout, d)
+ d.addBoth(lambda x : call.active() and call.cancel() or x)
+ return d
+
+ def shortDescription(self):
+ desc = super(TestCase, self).shortDescription()
+ if desc is None:
+ return self._testMethodName
+ return desc
+
+ def __call__(self, *args, **kwargs):
+ return self.run(*args, **kwargs)
+
+ def deferSetUp(self, ignored, result):
+ d = self._run('setUp', result)
+ d.addCallbacks(self.deferTestMethod, self._ebDeferSetUp,
+ callbackArgs=(result,),
+ errbackArgs=(result,))
+ return d
+
+ def _ebDeferSetUp(self, failure, result):
+ if failure.check(SkipTest):
+ result.addSkip(self, self._getReason(failure))
+ else:
+ result.addError(self, failure)
+ if failure.check(KeyboardInterrupt):
+ result.stop()
+ return self.deferRunCleanups(None, result)
+
+ def deferTestMethod(self, ignored, result):
+ d = self._run(self._testMethodName, result)
+ d.addCallbacks(self._cbDeferTestMethod, self._ebDeferTestMethod,
+ callbackArgs=(result,),
+ errbackArgs=(result,))
+ d.addBoth(self.deferRunCleanups, result)
+ d.addBoth(self.deferTearDown, result)
+ return d
+
+ def _cbDeferTestMethod(self, ignored, result):
+ if self.getTodo() is not None:
+ result.addUnexpectedSuccess(self, self.getTodo())
+ else:
+ self._passed = True
+ return ignored
+
+ def _ebDeferTestMethod(self, f, result):
+ todo = self.getTodo()
+ if todo is not None and todo.expected(f):
+ result.addExpectedFailure(self, f, todo)
+ elif f.check(self.failureException, FailTest):
+ result.addFailure(self, f)
+ elif f.check(KeyboardInterrupt):
+ result.addError(self, f)
+ result.stop()
+ elif f.check(SkipTest):
+ result.addSkip(self, self._getReason(f))
+ else:
+ result.addError(self, f)
+
+ def deferTearDown(self, ignored, result):
+ d = self._run('tearDown', result)
+ d.addErrback(self._ebDeferTearDown, result)
+ return d
+
+ def _ebDeferTearDown(self, failure, result):
+ result.addError(self, failure)
+ if failure.check(KeyboardInterrupt):
+ result.stop()
+ self._passed = False
+
+ def deferRunCleanups(self, ignored, result):
+ """
+ Run any scheduled cleanups and report errors (if any to the result
+ object.
+ """
+ d = self._runCleanups()
+ d.addCallback(self._cbDeferRunCleanups, result)
+ return d
+
+ def _cbDeferRunCleanups(self, cleanupResults, result):
+ for flag, failure in cleanupResults:
+ if flag == defer.FAILURE:
+ result.addError(self, failure)
+ if failure.check(KeyboardInterrupt):
+ result.stop()
+ self._passed = False
+
+ def _cleanUp(self, result):
+ try:
+ clean = util._Janitor(self, result).postCaseCleanup()
+ if not clean:
+ self._passed = False
+ except:
+ result.addError(self, failure.Failure())
+ self._passed = False
+ for error in self._observer.getErrors():
+ result.addError(self, error)
+ self._passed = False
+ self.flushLoggedErrors()
+ self._removeObserver()
+ if self._passed:
+ result.addSuccess(self)
+
+ def _classCleanUp(self, result):
+ try:
+ util._Janitor(self, result).postClassCleanup()
+ except:
+ result.addError(self, failure.Failure())
+
+ def _makeReactorMethod(self, name):
+ """
+ Create a method which wraps the reactor method C{name}. The new
+ method issues a deprecation warning and calls the original.
+ """
+ def _(*a, **kw):
+ warnings.warn("reactor.%s cannot be used inside unit tests. "
+ "In the future, using %s will fail the test and may "
+ "crash or hang the test run."
+ % (name, name),
+ stacklevel=2, category=DeprecationWarning)
+ return self._reactorMethods[name](*a, **kw)
+ return _
+
+ def _deprecateReactor(self, reactor):
+ """
+ Deprecate C{iterate}, C{crash} and C{stop} on C{reactor}. That is,
+ each method is wrapped in a function that issues a deprecation
+ warning, then calls the original.
+
+ @param reactor: The Twisted reactor.
+ """
+ self._reactorMethods = {}
+ for name in ['crash', 'iterate', 'stop']:
+ self._reactorMethods[name] = getattr(reactor, name)
+ setattr(reactor, name, self._makeReactorMethod(name))
+
+ def _undeprecateReactor(self, reactor):
+ """
+ Restore the deprecated reactor methods. Undoes what
+ L{_deprecateReactor} did.
+
+ @param reactor: The Twisted reactor.
+ """
+ for name, method in self._reactorMethods.iteritems():
+ setattr(reactor, name, method)
+ self._reactorMethods = {}
+
+ def _installObserver(self):
+ self._observer = _logObserver
+ self._observer._add()
+
+ def _removeObserver(self):
+ self._observer._remove()
+
+ def flushLoggedErrors(self, *errorTypes):
+ """
+ Remove stored errors received from the log.
+
+ C{TestCase} stores each error logged during the run of the test and
+ reports them as errors during the cleanup phase (after C{tearDown}).
+
+ @param *errorTypes: If unspecifed, flush all errors. Otherwise, only
+ flush errors that match the given types.
+
+ @return: A list of failures that have been removed.
+ """
+ return self._observer.flushErrors(*errorTypes)
+
+
+ def flushWarnings(self, offendingFunctions=None):
+ """
+ Remove stored warnings from the list of captured warnings and return
+ them.
+
+ @param offendingFunctions: If C{None}, all warnings issued during the
+ currently running test will be flushed. Otherwise, only warnings
+ which I{point} to a function included in this list will be flushed.
+ All warnings include a filename and source line number; if these
+ parts of a warning point to a source line which is part of a
+ function, then the warning I{points} to that function.
+ @type offendingFunctions: L{NoneType} or L{list} of functions or methods.
+
+ @raise ValueError: If C{offendingFunctions} is not C{None} and includes
+ an object which is not a L{FunctionType} or L{MethodType} instance.
+
+ @return: A C{list}, each element of which is a C{dict} giving
+ information about one warning which was flushed by this call. The
+ keys of each C{dict} are:
+
+ - C{'message'}: The string which was passed as the I{message}
+ parameter to L{warnings.warn}.
+
+ - C{'category'}: The warning subclass which was passed as the
+ I{category} parameter to L{warnings.warn}.
+
+ - C{'filename'}: The name of the file containing the definition
+ of the code object which was C{stacklevel} frames above the
+ call to L{warnings.warn}, where C{stacklevel} is the value of
+ the C{stacklevel} parameter passed to L{warnings.warn}.
+
+ - C{'lineno'}: The source line associated with the active
+ instruction of the code object object which was C{stacklevel}
+ frames above the call to L{warnings.warn}, where
+ C{stacklevel} is the value of the C{stacklevel} parameter
+ passed to L{warnings.warn}.
+ """
+ if offendingFunctions is None:
+ toFlush = self._warnings[:]
+ self._warnings[:] = []
+ else:
+ toFlush = []
+ for aWarning in self._warnings:
+ for aFunction in offendingFunctions:
+ if not isinstance(aFunction, (
+ types.FunctionType, types.MethodType)):
+ raise ValueError("%r is not a function or method" % (
+ aFunction,))
+
+ # inspect.getabsfile(aFunction) sometimes returns a
+ # filename which disagrees with the filename the warning
+ # system generates. This seems to be because a
+ # function's code object doesn't deal with source files
+ # being renamed. inspect.getabsfile(module) seems
+ # better (or at least agrees with the warning system
+ # more often), and does some normalization for us which
+ # is desirable. inspect.getmodule() is attractive, but
+ # somewhat broken in Python < 2.6. See Python bug 4845.
+ aModule = sys.modules[aFunction.__module__]
+ filename = inspect.getabsfile(aModule)
+
+ if filename != os.path.normcase(aWarning.filename):
+ continue
+ lineStarts = list(_findlinestarts(aFunction.func_code))
+ first = lineStarts[0][1]
+ last = lineStarts[-1][1]
+ if not (first <= aWarning.lineno <= last):
+ continue
+ # The warning points to this function, flush it and move on
+ # to the next warning.
+ toFlush.append(aWarning)
+ break
+ # Remove everything which is being flushed.
+ map(self._warnings.remove, toFlush)
+
+ return [
+ {'message': w.message, 'category': w.category,
+ 'filename': w.filename, 'lineno': w.lineno}
+ for w in toFlush]
+
+
+ def addCleanup(self, f, *args, **kwargs):
+ """
+ Add the given function to a list of functions to be called after the
+ test has run, but before C{tearDown}.
+
+ Functions will be run in reverse order of being added. This helps
+ ensure that tear down complements set up.
+
+ The function C{f} may return a Deferred. If so, C{TestCase} will wait
+ until the Deferred has fired before proceeding to the next function.
+ """
+ self._cleanups.append((f, args, kwargs))
+
+
+ def callDeprecated(self, version, f, *args, **kwargs):
+ """
+ Call a function that should have been deprecated at a specific version
+ and in favor of a specific alternative, and assert that it was thusly
+ deprecated.
+
+ @param version: A 2-sequence of (since, replacement), where C{since} is
+ a the first L{version<twisted.python.versions.Version>} that C{f}
+ should have been deprecated since, and C{replacement} is a suggested
+ replacement for the deprecated functionality, as described by
+ L{twisted.python.deprecate.deprecated}. If there is no suggested
+ replacement, this parameter may also be simply a
+ L{version<twisted.python.versions.Version>} by itself.
+
+ @param f: The deprecated function to call.
+
+ @param args: The arguments to pass to C{f}.
+
+ @param kwargs: The keyword arguments to pass to C{f}.
+
+ @return: Whatever C{f} returns.
+
+ @raise: Whatever C{f} raises. If any exception is
+ raised by C{f}, though, no assertions will be made about emitted
+ deprecations.
+
+ @raise FailTest: if no warnings were emitted by C{f}, or if the
+ L{DeprecationWarning} emitted did not produce the canonical
+ please-use-something-else message that is standard for Twisted
+ deprecations according to the given version and replacement.
+ """
+ result = f(*args, **kwargs)
+ warningsShown = self.flushWarnings([self.callDeprecated])
+ try:
+ info = list(version)
+ except TypeError:
+ since = version
+ replacement = None
+ else:
+ [since, replacement] = info
+
+ if len(warningsShown) == 0:
+ self.fail('%r is not deprecated.' % (f,))
+
+ observedWarning = warningsShown[0]['message']
+ expectedWarning = getDeprecationWarningString(
+ f, since, replacement=replacement)
+ self.assertEqual(expectedWarning, observedWarning)
+
+ return result
+
+
+ def _runCleanups(self):
+ """
+ Run the cleanups added with L{addCleanup} in order.
+
+ @return: A C{Deferred} that fires when all cleanups are run.
+ """
+ def _makeFunction(f, args, kwargs):
+ return lambda: f(*args, **kwargs)
+ callables = []
+ while len(self._cleanups) > 0:
+ f, args, kwargs = self._cleanups.pop()
+ callables.append(_makeFunction(f, args, kwargs))
+ return util._runSequentially(callables)
+
+
+ def patch(self, obj, attribute, value):
+ """
+ Monkey patch an object for the duration of the test.
+
+ The monkey patch will be reverted at the end of the test using the
+ L{addCleanup} mechanism.
+
+ The L{MonkeyPatcher} is returned so that users can restore and
+ re-apply the monkey patch within their tests.
+
+ @param obj: The object to monkey patch.
+ @param attribute: The name of the attribute to change.
+ @param value: The value to set the attribute to.
+ @return: A L{monkey.MonkeyPatcher} object.
+ """
+ monkeyPatch = monkey.MonkeyPatcher((obj, attribute, value))
+ monkeyPatch.patch()
+ self.addCleanup(monkeyPatch.restore)
+ return monkeyPatch
+
+
+ def runTest(self):
+ """
+ If no C{methodName} argument is passed to the constructor, L{run} will
+ treat this method as the thing with the actual test inside.
+ """
+
+
+ def run(self, result):
+ """
+ Run the test case, storing the results in C{result}.
+
+ First runs C{setUp} on self, then runs the test method (defined in the
+ constructor), then runs C{tearDown}. Any of these may return
+ L{Deferred}s. After they complete, does some reactor cleanup.
+
+ @param result: A L{TestResult} object.
+ """
+ log.msg("--> %s <--" % (self.id()))
+ from twisted.internet import reactor
+ new_result = itrial.IReporter(result, None)
+ if new_result is None:
+ result = PyUnitResultAdapter(result)
+ else:
+ result = new_result
+ self._timedOut = False
+ result.startTest(self)
+ if self.getSkip(): # don't run test methods that are marked as .skip
+ result.addSkip(self, self.getSkip())
+ result.stopTest(self)
+ return
+ self._installObserver()
+
+ # All the code inside runThunk will be run such that warnings emitted
+ # by it will be collected and retrievable by flushWarnings.
+ def runThunk():
+ self._passed = False
+ self._deprecateReactor(reactor)
+ try:
+ d = self.deferSetUp(None, result)
+ try:
+ self._wait(d)
+ finally:
+ self._cleanUp(result)
+ self._classCleanUp(result)
+ finally:
+ self._undeprecateReactor(reactor)
+
+ self._warnings = []
+ _collectWarnings(self._warnings.append, runThunk)
+
+ # Any collected warnings which the test method didn't flush get
+ # re-emitted so they'll be logged or show up on stdout or whatever.
+ for w in self.flushWarnings():
+ try:
+ warnings.warn_explicit(**w)
+ except:
+ result.addError(self, failure.Failure())
+
+ result.stopTest(self)
+
+
+ def _getReason(self, f):
+ if len(f.value.args) > 0:
+ reason = f.value.args[0]
+ else:
+ warnings.warn(("Do not raise unittest.SkipTest with no "
+ "arguments! Give a reason for skipping tests!"),
+ stacklevel=2)
+ reason = f
+ return reason
+
+ def getSkip(self):
+ """
+ Return the skip reason set on this test, if any is set. Checks on the
+ instance first, then the class, then the module, then packages. As
+ soon as it finds something with a C{skip} attribute, returns that.
+ Returns C{None} if it cannot find anything. See L{TestCase} docstring
+ for more details.
+ """
+ return util.acquireAttribute(self._parents, 'skip', None)
+
+ def getTodo(self):
+ """
+ Return a L{Todo} object if the test is marked todo. Checks on the
+ instance first, then the class, then the module, then packages. As
+ soon as it finds something with a C{todo} attribute, returns that.
+ Returns C{None} if it cannot find anything. See L{TestCase} docstring
+ for more details.
+ """
+ todo = util.acquireAttribute(self._parents, 'todo', None)
+ if todo is None:
+ return None
+ return makeTodo(todo)
+
+ def getTimeout(self):
+ """
+ Returns the timeout value set on this test. Checks on the instance
+ first, then the class, then the module, then packages. As soon as it
+ finds something with a C{timeout} attribute, returns that. Returns
+ L{util.DEFAULT_TIMEOUT_DURATION} if it cannot find anything. See
+ L{TestCase} docstring for more details.
+ """
+ timeout = util.acquireAttribute(self._parents, 'timeout',
+ util.DEFAULT_TIMEOUT_DURATION)
+ try:
+ return float(timeout)
+ except (ValueError, TypeError):
+ # XXX -- this is here because sometimes people will have methods
+ # called 'timeout', or set timeout to 'orange', or something
+ # Particularly, test_news.NewsTestCase and ReactorCoreTestCase
+ # both do this.
+ warnings.warn("'timeout' attribute needs to be a number.",
+ category=DeprecationWarning)
+ return util.DEFAULT_TIMEOUT_DURATION
+
+ def getSuppress(self):
+ """
+ Returns any warning suppressions set for this test. Checks on the
+ instance first, then the class, then the module, then packages. As
+ soon as it finds something with a C{suppress} attribute, returns that.
+ Returns any empty list (i.e. suppress no warnings) if it cannot find
+ anything. See L{TestCase} docstring for more details.
+ """
+ return util.acquireAttribute(self._parents, 'suppress', [])
+
+
+ def visit(self, visitor):
+ """
+ Visit this test case. Call C{visitor} with C{self} as a parameter.
+
+ Deprecated in Twisted 8.0.
+
+ @param visitor: A callable which expects a single parameter: a test
+ case.
+
+ @return: None
+ """
+ warnings.warn("Test visitors deprecated in Twisted 8.0",
+ category=DeprecationWarning)
+ visitor(self)
+
+
+ def mktemp(self):
+ """Returns a unique name that may be used as either a temporary
+ directory or filename.
+
+ @note: you must call os.mkdir on the value returned from this
+ method if you wish to use it as a directory!
+ """
+ MAX_FILENAME = 32 # some platforms limit lengths of filenames
+ base = os.path.join(self.__class__.__module__[:MAX_FILENAME],
+ self.__class__.__name__[:MAX_FILENAME],
+ self._testMethodName[:MAX_FILENAME])
+ if not os.path.exists(base):
+ os.makedirs(base)
+ dirname = tempfile.mkdtemp('', '', base)
+ return os.path.join(dirname, 'temp')
+
+ def _wait(self, d, running=_wait_is_running):
+ """Take a Deferred that only ever callbacks. Block until it happens.
+ """
+ from twisted.internet import reactor
+ if running:
+ raise RuntimeError("_wait is not reentrant")
+
+ results = []
+ def append(any):
+ if results is not None:
+ results.append(any)
+ def crash(ign):
+ if results is not None:
+ reactor.crash()
+ crash = utils.suppressWarnings(
+ crash, util.suppress(message=r'reactor\.crash cannot be used.*',
+ category=DeprecationWarning))
+ def stop():
+ reactor.crash()
+ stop = utils.suppressWarnings(
+ stop, util.suppress(message=r'reactor\.crash cannot be used.*',
+ category=DeprecationWarning))
+
+ running.append(None)
+ try:
+ d.addBoth(append)
+ if results:
+ # d might have already been fired, in which case append is
+ # called synchronously. Avoid any reactor stuff.
+ return
+ d.addBoth(crash)
+ reactor.stop = stop
+ try:
+ reactor.run()
+ finally:
+ del reactor.stop
+
+ # If the reactor was crashed elsewhere due to a timeout, hopefully
+ # that crasher also reported an error. Just return.
+ # _timedOut is most likely to be set when d has fired but hasn't
+ # completed its callback chain (see self._run)
+ if results or self._timedOut: #defined in run() and _run()
+ return
+
+ # If the timeout didn't happen, and we didn't get a result or
+ # a failure, then the user probably aborted the test, so let's
+ # just raise KeyboardInterrupt.
+
+ # FIXME: imagine this:
+ # web/test/test_webclient.py:
+ # exc = self.assertRaises(error.Error, wait, method(url))
+ #
+ # wait() will raise KeyboardInterrupt, and assertRaises will
+ # swallow it. Therefore, wait() raising KeyboardInterrupt is
+ # insufficient to stop trial. A suggested solution is to have
+ # this code set a "stop trial" flag, or otherwise notify trial
+ # that it should really try to stop as soon as possible.
+ raise KeyboardInterrupt()
+ finally:
+ results = None
+ running.pop()
+
+
+class UnsupportedTrialFeature(Exception):
+ """A feature of twisted.trial was used that pyunit cannot support."""
+
+
+
+class PyUnitResultAdapter(object):
+ """
+ Wrap a C{TestResult} from the standard library's C{unittest} so that it
+ supports the extended result types from Trial, and also supports
+ L{twisted.python.failure.Failure}s being passed to L{addError} and
+ L{addFailure}.
+ """
+
+ def __init__(self, original):
+ """
+ @param original: A C{TestResult} instance from C{unittest}.
+ """
+ self.original = original
+
+ def _exc_info(self, err):
+ return util.excInfoOrFailureToExcInfo(err)
+
+ def startTest(self, method):
+ self.original.startTest(method)
+
+ def stopTest(self, method):
+ self.original.stopTest(method)
+
+ def addFailure(self, test, fail):
+ self.original.addFailure(test, self._exc_info(fail))
+
+ def addError(self, test, error):
+ self.original.addError(test, self._exc_info(error))
+
+ def _unsupported(self, test, feature, info):
+ self.original.addFailure(
+ test,
+ (UnsupportedTrialFeature,
+ UnsupportedTrialFeature(feature, info),
+ None))
+
+ def addSkip(self, test, reason):
+ """
+ Report the skip as a failure.
+ """
+ self._unsupported(test, 'skip', reason)
+
+ def addUnexpectedSuccess(self, test, todo):
+ """
+ Report the unexpected success as a failure.
+ """
+ self._unsupported(test, 'unexpected success', todo)
+
+ def addExpectedFailure(self, test, error):
+ """
+ Report the expected failure (i.e. todo) as a failure.
+ """
+ self._unsupported(test, 'expected failure', error)
+
+ def addSuccess(self, test):
+ self.original.addSuccess(test)
+
+ def upDownError(self, method, error, warn, printStatus):
+ pass
+
+
+
+def suiteVisit(suite, visitor):
+ """
+ Visit each test in C{suite} with C{visitor}.
+
+ Deprecated in Twisted 8.0.
+
+ @param visitor: A callable which takes a single argument, the L{TestCase}
+ instance to visit.
+ @return: None
+ """
+ warnings.warn("Test visitors deprecated in Twisted 8.0",
+ category=DeprecationWarning)
+ for case in suite._tests:
+ visit = getattr(case, 'visit', None)
+ if visit is not None:
+ visit(visitor)
+ elif isinstance(case, pyunit.TestCase):
+ case = itrial.ITestCase(case)
+ case.visit(visitor)
+ elif isinstance(case, pyunit.TestSuite):
+ suiteVisit(case, visitor)
+ else:
+ case.visit(visitor)
+
+
+
+class TestSuite(pyunit.TestSuite):
+ """
+ Extend the standard library's C{TestSuite} with support for the visitor
+ pattern and a consistently overrideable C{run} method.
+ """
+
+ visit = suiteVisit
+
+ def __call__(self, result):
+ return self.run(result)
+
+
+ def run(self, result):
+ """
+ Call C{run} on every member of the suite.
+ """
+ # we implement this because Python 2.3 unittest defines this code
+ # in __call__, whereas 2.4 defines the code in run.
+ for test in self._tests:
+ if result.shouldStop:
+ break
+ test(result)
+ return result
+
+
+
+class TestDecorator(components.proxyForInterface(itrial.ITestCase,
+ "_originalTest")):
+ """
+ Decorator for test cases.
+
+ @param _originalTest: The wrapped instance of test.
+ @type _originalTest: A provider of L{itrial.ITestCase}
+ """
+
+ implements(itrial.ITestCase)
+
+
+ def __call__(self, result):
+ """
+ Run the unit test.
+
+ @param result: A TestResult object.
+ """
+ return self.run(result)
+
+
+ def run(self, result):
+ """
+ Run the unit test.
+
+ @param result: A TestResult object.
+ """
+ return self._originalTest.run(
+ reporter._AdaptedReporter(result, self.__class__))
+
+
+
+def _clearSuite(suite):
+ """
+ Clear all tests from C{suite}.
+
+ This messes with the internals of C{suite}. In particular, it assumes that
+ the suite keeps all of its tests in a list in an instance variable called
+ C{_tests}.
+ """
+ suite._tests = []
+
+
+def decorate(test, decorator):
+ """
+ Decorate all test cases in C{test} with C{decorator}.
+
+ C{test} can be a test case or a test suite. If it is a test suite, then the
+ structure of the suite is preserved.
+
+ L{decorate} tries to preserve the class of the test suites it finds, but
+ assumes the presence of the C{_tests} attribute on the suite.
+
+ @param test: The C{TestCase} or C{TestSuite} to decorate.
+
+ @param decorator: A unary callable used to decorate C{TestCase}s.
+
+ @return: A decorated C{TestCase} or a C{TestSuite} containing decorated
+ C{TestCase}s.
+ """
+
+ try:
+ tests = iter(test)
+ except TypeError:
+ return decorator(test)
+
+ # At this point, we know that 'test' is a test suite.
+ _clearSuite(test)
+
+ for case in tests:
+ test.addTest(decorate(case, decorator))
+ return test
+
+
+
+class _PyUnitTestCaseAdapter(TestDecorator):
+ """
+ Adapt from pyunit.TestCase to ITestCase.
+ """
+
+
+ def visit(self, visitor):
+ """
+ Deprecated in Twisted 8.0.
+ """
+ warnings.warn("Test visitors deprecated in Twisted 8.0",
+ category=DeprecationWarning)
+ visitor(self)
+
+
+
+class _BrokenIDTestCaseAdapter(_PyUnitTestCaseAdapter):
+ """
+ Adapter for pyunit-style C{TestCase} subclasses that have undesirable id()
+ methods. That is L{pyunit.FunctionTestCase} and L{pyunit.DocTestCase}.
+ """
+
+ def id(self):
+ """
+ Return the fully-qualified Python name of the doctest.
+ """
+ testID = self._originalTest.shortDescription()
+ if testID is not None:
+ return testID
+ return self._originalTest.id()
+
+
+
+class _ForceGarbageCollectionDecorator(TestDecorator):
+ """
+ Forces garbage collection to be run before and after the test. Any errors
+ logged during the post-test collection are added to the test result as
+ errors.
+ """
+
+ def run(self, result):
+ gc.collect()
+ TestDecorator.run(self, result)
+ _logObserver._add()
+ gc.collect()
+ for error in _logObserver.getErrors():
+ result.addError(self, error)
+ _logObserver.flushErrors()
+ _logObserver._remove()
+
+
+components.registerAdapter(
+ _PyUnitTestCaseAdapter, pyunit.TestCase, itrial.ITestCase)
+
+
+components.registerAdapter(
+ _BrokenIDTestCaseAdapter, pyunit.FunctionTestCase, itrial.ITestCase)
+
+
+_docTestCase = getattr(doctest, 'DocTestCase', None)
+if _docTestCase:
+ components.registerAdapter(
+ _BrokenIDTestCaseAdapter, _docTestCase, itrial.ITestCase)
+
+
+def _iterateTests(testSuiteOrCase):
+ """
+ Iterate through all of the test cases in C{testSuiteOrCase}.
+ """
+ try:
+ suite = iter(testSuiteOrCase)
+ except TypeError:
+ yield testSuiteOrCase
+ else:
+ for test in suite:
+ for subtest in _iterateTests(test):
+ yield subtest
+
+
+
+# Support for Python 2.3
+try:
+ iter(pyunit.TestSuite())
+except TypeError:
+ # Python 2.3's TestSuite doesn't support iteration. Let's monkey patch it!
+ def __iter__(self):
+ return iter(self._tests)
+ pyunit.TestSuite.__iter__ = __iter__
+
+
+
+class _SubTestCase(TestCase):
+ def __init__(self):
+ TestCase.__init__(self, 'run')
+
+_inst = _SubTestCase()
+
+def _deprecate(name):
+ """
+ Internal method used to deprecate top-level assertions. Do not use this.
+ """
+ def _(*args, **kwargs):
+ warnings.warn("unittest.%s is deprecated. Instead use the %r "
+ "method on unittest.TestCase" % (name, name),
+ stacklevel=2, category=DeprecationWarning)
+ return getattr(_inst, name)(*args, **kwargs)
+ return _
+
+
+_assertions = ['fail', 'failUnlessEqual', 'failIfEqual', 'failIfEquals',
+ 'failUnless', 'failUnlessIdentical', 'failUnlessIn',
+ 'failIfIdentical', 'failIfIn', 'failIf',
+ 'failUnlessAlmostEqual', 'failIfAlmostEqual',
+ 'failUnlessRaises', 'assertApproximates',
+ 'assertFailure', 'failUnlessSubstring', 'failIfSubstring',
+ 'assertAlmostEqual', 'assertAlmostEquals',
+ 'assertNotAlmostEqual', 'assertNotAlmostEquals', 'assertEqual',
+ 'assertEquals', 'assertNotEqual', 'assertNotEquals',
+ 'assertRaises', 'assert_', 'assertIdentical',
+ 'assertNotIdentical', 'assertIn', 'assertNotIn',
+ 'failUnlessFailure', 'assertSubstring', 'assertNotSubstring']
+
+
+for methodName in _assertions:
+ globals()[methodName] = _deprecate(methodName)
+
+
+__all__ = ['TestCase', 'FailTest', 'SkipTest']
diff --git a/twisted/trial/util.py b/twisted/trial/util.py
new file mode 100644
index 0000000..90d4437
--- /dev/null
+++ b/twisted/trial/util.py
@@ -0,0 +1,430 @@
+# -*- test-case-name: twisted.trial.test.test_util -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+
+"""
+A collection of utility functions and classes, used internally by Trial.
+
+This code is for Trial's internal use. Do NOT use this code if you are writing
+tests. It is subject to change at the Trial maintainer's whim. There is
+nothing here in this module for you to use unless you are maintaining Trial.
+
+Any non-Trial Twisted code that uses this module will be shot.
+
+Maintainer: Jonathan Lange
+"""
+
+import traceback, sys
+from random import randrange
+
+from twisted.internet import defer, utils, interfaces
+from twisted.python.failure import Failure
+from twisted.python import deprecate, versions
+from twisted.python.lockfile import FilesystemLock
+from twisted.python.filepath import FilePath
+
+DEFAULT_TIMEOUT = object()
+DEFAULT_TIMEOUT_DURATION = 120.0
+
+
+
+class DirtyReactorAggregateError(Exception):
+ """
+ Passed to L{twisted.trial.itrial.IReporter.addError} when the reactor is
+ left in an unclean state after a test.
+
+ @ivar delayedCalls: The L{DelayedCall} objects which weren't cleaned up.
+ @ivar selectables: The selectables which weren't cleaned up.
+ """
+
+ def __init__(self, delayedCalls, selectables=None):
+ self.delayedCalls = delayedCalls
+ self.selectables = selectables
+
+ def __str__(self):
+ """
+ Return a multi-line message describing all of the unclean state.
+ """
+ msg = "Reactor was unclean."
+ if self.delayedCalls:
+ msg += ("\nDelayedCalls: (set "
+ "twisted.internet.base.DelayedCall.debug = True to "
+ "debug)\n")
+ msg += "\n".join(map(str, self.delayedCalls))
+ if self.selectables:
+ msg += "\nSelectables:\n"
+ msg += "\n".join(map(str, self.selectables))
+ return msg
+
+
+
+class _Janitor(object):
+ """
+ The guy that cleans up after you.
+
+ @ivar test: The L{TestCase} to report errors about.
+ @ivar result: The L{IReporter} to report errors to.
+ @ivar reactor: The reactor to use. If None, the global reactor
+ will be used.
+ """
+ def __init__(self, test, result, reactor=None):
+ """
+ @param test: See L{_Janitor.test}.
+ @param result: See L{_Janitor.result}.
+ @param reactor: See L{_Janitor.reactor}.
+ """
+ self.test = test
+ self.result = result
+ self.reactor = reactor
+
+
+ def postCaseCleanup(self):
+ """
+ Called by L{unittest.TestCase} after a test to catch any logged errors
+ or pending L{DelayedCall}s.
+ """
+ calls = self._cleanPending()
+ if calls:
+ aggregate = DirtyReactorAggregateError(calls)
+ self.result.addError(self.test, Failure(aggregate))
+ return False
+ return True
+
+
+ def postClassCleanup(self):
+ """
+ Called by L{unittest.TestCase} after the last test in a C{TestCase}
+ subclass. Ensures the reactor is clean by murdering the threadpool,
+ catching any pending L{DelayedCall}s, open sockets etc.
+ """
+ selectables = self._cleanReactor()
+ calls = self._cleanPending()
+ if selectables or calls:
+ aggregate = DirtyReactorAggregateError(calls, selectables)
+ self.result.addError(self.test, Failure(aggregate))
+ self._cleanThreads()
+
+
+ def _getReactor(self):
+ """
+ Get either the passed-in reactor or the global reactor.
+ """
+ if self.reactor is not None:
+ reactor = self.reactor
+ else:
+ from twisted.internet import reactor
+ return reactor
+
+
+ def _cleanPending(self):
+ """
+ Cancel all pending calls and return their string representations.
+ """
+ reactor = self._getReactor()
+
+ # flush short-range timers
+ reactor.iterate(0)
+ reactor.iterate(0)
+
+ delayedCallStrings = []
+ for p in reactor.getDelayedCalls():
+ if p.active():
+ delayedString = str(p)
+ p.cancel()
+ else:
+ print "WEIRDNESS! pending timed call not active!"
+ delayedCallStrings.append(delayedString)
+ return delayedCallStrings
+ _cleanPending = utils.suppressWarnings(
+ _cleanPending, (('ignore',), {'category': DeprecationWarning,
+ 'message':
+ r'reactor\.iterate cannot be used.*'}))
+
+ def _cleanThreads(self):
+ reactor = self._getReactor()
+ if interfaces.IReactorThreads.providedBy(reactor):
+ if reactor.threadpool is not None:
+ # Stop the threadpool now so that a new one is created.
+ # This improves test isolation somewhat (although this is a
+ # post class cleanup hook, so it's only isolating classes
+ # from each other, not methods from each other).
+ reactor._stopThreadPool()
+
+ def _cleanReactor(self):
+ """
+ Remove all selectables from the reactor, kill any of them that were
+ processes, and return their string representation.
+ """
+ reactor = self._getReactor()
+ selectableStrings = []
+ for sel in reactor.removeAll():
+ if interfaces.IProcessTransport.providedBy(sel):
+ sel.signalProcess('KILL')
+ selectableStrings.append(repr(sel))
+ return selectableStrings
+
+
+def excInfoOrFailureToExcInfo(err):
+ """
+ Coerce a Failure to an _exc_info, if err is a Failure.
+
+ @param err: Either a tuple such as returned by L{sys.exc_info} or a
+ L{Failure} object.
+ @return: A tuple like the one returned by L{sys.exc_info}. e.g.
+ C{exception_type, exception_object, traceback_object}.
+ """
+ if isinstance(err, Failure):
+ # Unwrap the Failure into a exc_info tuple.
+ err = (err.type, err.value, err.getTracebackObject())
+ return err
+
+
+def suppress(action='ignore', **kwarg):
+ """
+ Sets up the .suppress tuple properly, pass options to this method as you
+ would the stdlib warnings.filterwarnings()
+
+ So, to use this with a .suppress magic attribute you would do the
+ following:
+
+ >>> from twisted.trial import unittest, util
+ >>> import warnings
+ >>>
+ >>> class TestFoo(unittest.TestCase):
+ ... def testFooBar(self):
+ ... warnings.warn("i am deprecated", DeprecationWarning)
+ ... testFooBar.suppress = [util.suppress(message='i am deprecated')]
+ ...
+ >>>
+
+ Note that as with the todo and timeout attributes: the module level
+ attribute acts as a default for the class attribute which acts as a default
+ for the method attribute. The suppress attribute can be overridden at any
+ level by specifying C{.suppress = []}
+ """
+ return ((action,), kwarg)
+
+
+def profiled(f, outputFile):
+ def _(*args, **kwargs):
+ if sys.version_info[0:2] != (2, 4):
+ import profile
+ prof = profile.Profile()
+ try:
+ result = prof.runcall(f, *args, **kwargs)
+ prof.dump_stats(outputFile)
+ except SystemExit:
+ pass
+ prof.print_stats()
+ return result
+ else: # use hotshot, profile is broken in 2.4
+ import hotshot.stats
+ prof = hotshot.Profile(outputFile)
+ try:
+ return prof.runcall(f, *args, **kwargs)
+ finally:
+ stats = hotshot.stats.load(outputFile)
+ stats.strip_dirs()
+ stats.sort_stats('cum') # 'time'
+ stats.print_stats(100)
+ return _
+
+
+def getPythonContainers(meth):
+ """Walk up the Python tree from method 'meth', finding its class, its module
+ and all containing packages."""
+ containers = []
+ containers.append(meth.im_class)
+ moduleName = meth.im_class.__module__
+ while moduleName is not None:
+ module = sys.modules.get(moduleName, None)
+ if module is None:
+ module = __import__(moduleName)
+ containers.append(module)
+ moduleName = getattr(module, '__module__', None)
+ return containers
+
+
+_DEFAULT = object()
+def acquireAttribute(objects, attr, default=_DEFAULT):
+ """Go through the list 'objects' sequentially until we find one which has
+ attribute 'attr', then return the value of that attribute. If not found,
+ return 'default' if set, otherwise, raise AttributeError. """
+ for obj in objects:
+ if hasattr(obj, attr):
+ return getattr(obj, attr)
+ if default is not _DEFAULT:
+ return default
+ raise AttributeError('attribute %r not found in %r' % (attr, objects))
+
+
+
+deprecate.deprecatedModuleAttribute(
+ versions.Version("Twisted", 10, 1, 0),
+ "Please use twisted.python.reflect.namedAny instead.",
+ __name__, "findObject")
+
+
+
+def findObject(name):
+ """Get a fully-named package, module, module-global object or attribute.
+ Forked from twisted.python.reflect.namedAny.
+
+ Returns a tuple of (bool, obj). If bool is True, the named object exists
+ and is returned as obj. If bool is False, the named object does not exist
+ and the value of obj is unspecified.
+ """
+ names = name.split('.')
+ topLevelPackage = None
+ moduleNames = names[:]
+ while not topLevelPackage:
+ trialname = '.'.join(moduleNames)
+ if len(trialname) == 0:
+ return (False, None)
+ try:
+ topLevelPackage = __import__(trialname)
+ except ImportError:
+ # if the ImportError happened in the module being imported,
+ # this is a failure that should be handed to our caller.
+ # count stack frames to tell the difference.
+ exc_info = sys.exc_info()
+ if len(traceback.extract_tb(exc_info[2])) > 1:
+ try:
+ # Clean up garbage left in sys.modules.
+ del sys.modules[trialname]
+ except KeyError:
+ # Python 2.4 has fixed this. Yay!
+ pass
+ raise exc_info[0], exc_info[1], exc_info[2]
+ moduleNames.pop()
+ obj = topLevelPackage
+ for n in names[1:]:
+ try:
+ obj = getattr(obj, n)
+ except AttributeError:
+ return (False, obj)
+ return (True, obj)
+
+
+
+def _runSequentially(callables, stopOnFirstError=False):
+ """
+ Run the given callables one after the other. If a callable returns a
+ Deferred, wait until it has finished before running the next callable.
+
+ @param callables: An iterable of callables that take no parameters.
+
+ @param stopOnFirstError: If True, then stop running callables as soon as
+ one raises an exception or fires an errback. False by default.
+
+ @return: A L{Deferred} that fires a list of C{(flag, value)} tuples. Each
+ tuple will be either C{(SUCCESS, <return value>)} or C{(FAILURE,
+ <Failure>)}.
+ """
+ results = []
+ for f in callables:
+ d = defer.maybeDeferred(f)
+ thing = defer.waitForDeferred(d)
+ yield thing
+ try:
+ results.append((defer.SUCCESS, thing.getResult()))
+ except:
+ results.append((defer.FAILURE, Failure()))
+ if stopOnFirstError:
+ break
+ yield results
+_runSequentially = defer.deferredGenerator(_runSequentially)
+
+
+
+class _NoTrialMarker(Exception):
+ """
+ No trial marker file could be found.
+
+ Raised when trial attempts to remove a trial temporary working directory
+ that does not contain a marker file.
+ """
+
+
+
+def _removeSafely(path):
+ """
+ Safely remove a path, recursively.
+
+ If C{path} does not contain a node named C{_trial_marker}, a
+ L{_NoTrialmarker} exception is raised and the path is not removed.
+ """
+ if not path.child('_trial_marker').exists():
+ raise _NoTrialMarker(
+ '%r is not a trial temporary path, refusing to remove it'
+ % (path,))
+ try:
+ path.remove()
+ except OSError, e:
+ print ("could not remove %r, caught OSError [Errno %s]: %s"
+ % (path, e.errno, e.strerror))
+ try:
+ newPath = FilePath('_trial_temp_old%s' % (randrange(1000000),))
+ path.moveTo(newPath)
+ except OSError, e:
+ print ("could not rename path, caught OSError [Errno %s]: %s"
+ % (e.errno,e.strerror))
+ raise
+
+
+
+class _WorkingDirectoryBusy(Exception):
+ """
+ A working directory was specified to the runner, but another test run is
+ currently using that directory.
+ """
+
+
+
+def _unusedTestDirectory(base):
+ """
+ Find an unused directory named similarly to C{base}.
+
+ Once a directory is found, it will be locked and a marker dropped into it to
+ identify it as a trial temporary directory.
+
+ @param base: A template path for the discovery process. If this path
+ exactly cannot be used, a path which varies only in a suffix of the
+ basename will be used instead.
+ @type base: L{FilePath}
+
+ @return: A two-tuple. The first element is a L{FilePath} representing the
+ directory which was found and created. The second element is a locked
+ L{FilesystemLock}. Another call to C{_unusedTestDirectory} will not be
+ able to reused the the same name until the lock is released, either
+ explicitly or by this process exiting.
+ """
+ counter = 0
+ while True:
+ if counter:
+ testdir = base.sibling('%s-%d' % (base.basename(), counter))
+ else:
+ testdir = base
+
+ testDirLock = FilesystemLock(testdir.path + '.lock')
+ if testDirLock.lock():
+ # It is not in use
+ if testdir.exists():
+ # It exists though - delete it
+ _removeSafely(testdir)
+
+ # Create it anew and mark it as ours so the next _removeSafely on it
+ # succeeds.
+ testdir.makedirs()
+ testdir.child('_trial_marker').setContent('')
+ return testdir, testDirLock
+ else:
+ # It is in use
+ if base.basename() == '_trial_temp':
+ counter += 1
+ else:
+ raise _WorkingDirectoryBusy()
+
+
+__all__ = ['excInfoOrFailureToExcInfo', 'suppress']
diff --git a/twisted/web/__init__.py b/twisted/web/__init__.py
new file mode 100644
index 0000000..08c74e6
--- /dev/null
+++ b/twisted/web/__init__.py
@@ -0,0 +1,21 @@
+# -*- test-case-name: twisted.web.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted Web: a L{web server<twisted.web.server>} (including an
+L{HTTP implementation<twisted.web.http>} and a
+L{resource model<twisted.web.resource>}) and
+a L{web client<twisted.web.client>}.
+"""
+
+from twisted.web._version import version
+from twisted.python.versions import Version
+from twisted.python.deprecate import deprecatedModuleAttribute
+
+__version__ = version.short()
+
+deprecatedModuleAttribute(
+ Version('Twisted', 11, 1, 0),
+ "Google module is deprecated. Use Google's API instead",
+ __name__, "google")
diff --git a/twisted/web/_auth/__init__.py b/twisted/web/_auth/__init__.py
new file mode 100644
index 0000000..6a58870
--- /dev/null
+++ b/twisted/web/_auth/__init__.py
@@ -0,0 +1,7 @@
+# -*- test-case-name: twisted.web.test.test_httpauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HTTP header-based authentication migrated from web2
+"""
diff --git a/twisted/web/_auth/basic.py b/twisted/web/_auth/basic.py
new file mode 100644
index 0000000..8b588fb
--- /dev/null
+++ b/twisted/web/_auth/basic.py
@@ -0,0 +1,59 @@
+# -*- test-case-name: twisted.web.test.test_httpauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HTTP BASIC authentication.
+
+@see: U{http://tools.ietf.org/html/rfc1945}
+@see: U{http://tools.ietf.org/html/rfc2616}
+@see: U{http://tools.ietf.org/html/rfc2617}
+"""
+
+import binascii
+
+from zope.interface import implements
+
+from twisted.cred import credentials, error
+from twisted.web.iweb import ICredentialFactory
+
+
+class BasicCredentialFactory(object):
+ """
+ Credential Factory for HTTP Basic Authentication
+
+ @type authenticationRealm: C{str}
+ @ivar authenticationRealm: The HTTP authentication realm which will be issued in
+ challenges.
+ """
+ implements(ICredentialFactory)
+
+ scheme = 'basic'
+
+ def __init__(self, authenticationRealm):
+ self.authenticationRealm = authenticationRealm
+
+
+ def getChallenge(self, request):
+ """
+ Return a challenge including the HTTP authentication realm with which
+ this factory was created.
+ """
+ return {'realm': self.authenticationRealm}
+
+
+ def decode(self, response, request):
+ """
+ Parse the base64-encoded, colon-separated username and password into a
+ L{credentials.UsernamePassword} instance.
+ """
+ try:
+ creds = binascii.a2b_base64(response + '===')
+ except binascii.Error:
+ raise error.LoginFailed('Invalid credentials')
+
+ creds = creds.split(':', 1)
+ if len(creds) == 2:
+ return credentials.UsernamePassword(*creds)
+ else:
+ raise error.LoginFailed('Invalid credentials')
diff --git a/twisted/web/_auth/digest.py b/twisted/web/_auth/digest.py
new file mode 100644
index 0000000..90ebf20
--- /dev/null
+++ b/twisted/web/_auth/digest.py
@@ -0,0 +1,54 @@
+# -*- test-case-name: twisted.web.test.test_httpauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation of RFC2617: HTTP Digest Authentication
+
+@see: U{http://www.faqs.org/rfcs/rfc2617.html}
+"""
+
+from zope.interface import implements
+from twisted.cred import credentials
+from twisted.web.iweb import ICredentialFactory
+
+class DigestCredentialFactory(object):
+ """
+ Wrapper for L{digest.DigestCredentialFactory} that implements the
+ L{ICredentialFactory} interface.
+ """
+ implements(ICredentialFactory)
+
+ scheme = 'digest'
+
+ def __init__(self, algorithm, authenticationRealm):
+ """
+ Create the digest credential factory that this object wraps.
+ """
+ self.digest = credentials.DigestCredentialFactory(algorithm,
+ authenticationRealm)
+
+
+ def getChallenge(self, request):
+ """
+ Generate the challenge for use in the WWW-Authenticate header
+
+ @param request: The L{IRequest} to with access was denied and for the
+ response to which this challenge is being generated.
+
+ @return: The C{dict} that can be used to generate a WWW-Authenticate
+ header.
+ """
+ return self.digest.getChallenge(request.getClientIP())
+
+
+ def decode(self, response, request):
+ """
+ Create a L{twisted.cred.digest.DigestedCredentials} object from the
+ given response and request.
+
+ @see: L{ICredentialFactory.decode}
+ """
+ return self.digest.decode(response,
+ request.method,
+ request.getClientIP())
diff --git a/twisted/web/_auth/wrapper.py b/twisted/web/_auth/wrapper.py
new file mode 100644
index 0000000..29f479e
--- /dev/null
+++ b/twisted/web/_auth/wrapper.py
@@ -0,0 +1,225 @@
+# -*- test-case-name: twisted.web.test.test_httpauth -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A guard implementation which supports HTTP header-based authentication
+schemes.
+
+If no I{Authorization} header is supplied, an anonymous login will be
+attempted by using a L{Anonymous} credentials object. If such a header is
+supplied and does not contain allowed credentials, or if anonymous login is
+denied, a 401 will be sent in the response along with I{WWW-Authenticate}
+headers for each of the allowed authentication schemes.
+"""
+
+from zope.interface import implements
+
+from twisted.python import log
+from twisted.python.components import proxyForInterface
+from twisted.web.resource import IResource, ErrorPage
+from twisted.web import util
+from twisted.cred import error
+from twisted.cred.credentials import Anonymous
+
+
+class UnauthorizedResource(object):
+ """
+ Simple IResource to escape Resource dispatch
+ """
+ implements(IResource)
+ isLeaf = True
+
+
+ def __init__(self, factories):
+ self._credentialFactories = factories
+
+
+ def render(self, request):
+ """
+ Send www-authenticate headers to the client
+ """
+ def generateWWWAuthenticate(scheme, challenge):
+ l = []
+ for k,v in challenge.iteritems():
+ l.append("%s=%s" % (k, quoteString(v)))
+ return "%s %s" % (scheme, ", ".join(l))
+
+ def quoteString(s):
+ return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
+
+ request.setResponseCode(401)
+ for fact in self._credentialFactories:
+ challenge = fact.getChallenge(request)
+ request.responseHeaders.addRawHeader(
+ 'www-authenticate',
+ generateWWWAuthenticate(fact.scheme, challenge))
+ if request.method == 'HEAD':
+ return ''
+ return 'Unauthorized'
+
+
+ def getChildWithDefault(self, path, request):
+ """
+ Disable resource dispatch
+ """
+ return self
+
+
+
+class HTTPAuthSessionWrapper(object):
+ """
+ Wrap a portal, enforcing supported header-based authentication schemes.
+
+ @ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
+ avatars.
+
+ @ivar _credentialFactories: A list of L{ICredentialFactory} providers which
+ will be used to decode I{Authorization} headers into L{ICredentials}
+ providers.
+ """
+ implements(IResource)
+ isLeaf = False
+
+ def __init__(self, portal, credentialFactories):
+ """
+ Initialize a session wrapper
+
+ @type portal: C{Portal}
+ @param portal: The portal that will authenticate the remote client
+
+ @type credentialFactories: C{Iterable}
+ @param credentialFactories: The portal that will authenticate the
+ remote client based on one submitted C{ICredentialFactory}
+ """
+ self._portal = portal
+ self._credentialFactories = credentialFactories
+
+
+ def _authorizedResource(self, request):
+ """
+ Get the L{IResource} which the given request is authorized to receive.
+ If the proper authorization headers are present, the resource will be
+ requested from the portal. If not, an anonymous login attempt will be
+ made.
+ """
+ authheader = request.getHeader('authorization')
+ if not authheader:
+ return util.DeferredResource(self._login(Anonymous()))
+
+ factory, respString = self._selectParseHeader(authheader)
+ if factory is None:
+ return UnauthorizedResource(self._credentialFactories)
+ try:
+ credentials = factory.decode(respString, request)
+ except error.LoginFailed:
+ return UnauthorizedResource(self._credentialFactories)
+ except:
+ log.err(None, "Unexpected failure from credentials factory")
+ return ErrorPage(500, None, None)
+ else:
+ return util.DeferredResource(self._login(credentials))
+
+
+ def render(self, request):
+ """
+ Find the L{IResource} avatar suitable for the given request, if
+ possible, and render it. Otherwise, perhaps render an error page
+ requiring authorization or describing an internal server failure.
+ """
+ return self._authorizedResource(request).render(request)
+
+
+ def getChildWithDefault(self, path, request):
+ """
+ Inspect the Authorization HTTP header, and return a deferred which,
+ when fired after successful authentication, will return an authorized
+ C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
+ be returned, essentially halting further dispatch on the wrapped
+ resource and all children
+ """
+ # Don't consume any segments of the request - this class should be
+ # transparent!
+ request.postpath.insert(0, request.prepath.pop())
+ return self._authorizedResource(request)
+
+
+ def _login(self, credentials):
+ """
+ Get the L{IResource} avatar for the given credentials.
+
+ @return: A L{Deferred} which will be called back with an L{IResource}
+ avatar or which will errback if authentication fails.
+ """
+ d = self._portal.login(credentials, None, IResource)
+ d.addCallbacks(self._loginSucceeded, self._loginFailed)
+ return d
+
+
+ def _loginSucceeded(self, (interface, avatar, logout)):
+ """
+ Handle login success by wrapping the resulting L{IResource} avatar
+ so that the C{logout} callback will be invoked when rendering is
+ complete.
+ """
+ class ResourceWrapper(proxyForInterface(IResource, 'resource')):
+ """
+ Wrap an L{IResource} so that whenever it or a child of it
+ completes rendering, the cred logout hook will be invoked.
+
+ An assumption is made here that exactly one L{IResource} from
+ among C{avatar} and all of its children will be rendered. If
+ more than one is rendered, C{logout} will be invoked multiple
+ times and probably earlier than desired.
+ """
+ def getChildWithDefault(self, name, request):
+ """
+ Pass through the lookup to the wrapped resource, wrapping
+ the result in L{ResourceWrapper} to ensure C{logout} is
+ called when rendering of the child is complete.
+ """
+ return ResourceWrapper(self.resource.getChildWithDefault(name, request))
+
+ def render(self, request):
+ """
+ Hook into response generation so that when rendering has
+ finished completely (with or without error), C{logout} is
+ called.
+ """
+ request.notifyFinish().addBoth(lambda ign: logout())
+ return super(ResourceWrapper, self).render(request)
+
+ return ResourceWrapper(avatar)
+
+
+ def _loginFailed(self, result):
+ """
+ Handle login failure by presenting either another challenge (for
+ expected authentication/authorization-related failures) or a server
+ error page (for anything else).
+ """
+ if result.check(error.Unauthorized, error.LoginFailed):
+ return UnauthorizedResource(self._credentialFactories)
+ else:
+ log.err(
+ result,
+ "HTTPAuthSessionWrapper.getChildWithDefault encountered "
+ "unexpected error")
+ return ErrorPage(500, None, None)
+
+
+ def _selectParseHeader(self, header):
+ """
+ Choose an C{ICredentialFactory} from C{_credentialFactories}
+ suitable to use to decode the given I{Authenticate} header.
+
+ @return: A two-tuple of a factory and the remaining portion of the
+ header value to be decoded or a two-tuple of C{None} if no
+ factory can decode the header value.
+ """
+ elements = header.split(' ')
+ scheme = elements[0].lower()
+ for fact in self._credentialFactories:
+ if fact.scheme == scheme:
+ return (fact, ' '.join(elements[1:]))
+ return (None, None)
diff --git a/twisted/web/_element.py b/twisted/web/_element.py
new file mode 100644
index 0000000..3c15b3b
--- /dev/null
+++ b/twisted/web/_element.py
@@ -0,0 +1,185 @@
+# -*- test-case-name: twisted.web.test.test_template -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+
+from twisted.web.iweb import IRenderable
+
+from twisted.web.error import MissingRenderMethod, UnexposedMethodError
+from twisted.web.error import MissingTemplateLoader
+
+
+class Expose(object):
+ """
+ Helper for exposing methods for various uses using a simple decorator-style
+ callable.
+
+ Instances of this class can be called with one or more functions as
+ positional arguments. The names of these functions will be added to a list
+ on the class object of which they are methods.
+
+ @ivar attributeName: The attribute with which exposed methods will be
+ tracked.
+ """
+ def __init__(self, doc=None):
+ self.doc = doc
+
+
+ def __call__(self, *funcObjs):
+ """
+ Add one or more functions to the set of exposed functions.
+
+ This is a way to declare something about a class definition, similar to
+ L{zope.interface.implements}. Use it like this::
+
+ magic = Expose('perform extra magic')
+ class Foo(Bar):
+ def twiddle(self, x, y):
+ ...
+ def frob(self, a, b):
+ ...
+ magic(twiddle, frob)
+
+ Later you can query the object::
+
+ aFoo = Foo()
+ magic.get(aFoo, 'twiddle')(x=1, y=2)
+
+ The call to C{get} will fail if the name it is given has not been
+ exposed using C{magic}.
+
+ @param funcObjs: One or more function objects which will be exposed to
+ the client.
+
+ @return: The first of C{funcObjs}.
+ """
+ if not funcObjs:
+ raise TypeError("expose() takes at least 1 argument (0 given)")
+ for fObj in funcObjs:
+ fObj.exposedThrough = getattr(fObj, 'exposedThrough', [])
+ fObj.exposedThrough.append(self)
+ return funcObjs[0]
+
+
+ _nodefault = object()
+ def get(self, instance, methodName, default=_nodefault):
+ """
+ Retrieve an exposed method with the given name from the given instance.
+
+ @raise UnexposedMethodError: Raised if C{default} is not specified and
+ there is no exposed method with the given name.
+
+ @return: A callable object for the named method assigned to the given
+ instance.
+ """
+ method = getattr(instance, methodName, None)
+ exposedThrough = getattr(method, 'exposedThrough', [])
+ if self not in exposedThrough:
+ if default is self._nodefault:
+ raise UnexposedMethodError(self, methodName)
+ return default
+ return method
+
+
+ @classmethod
+ def _withDocumentation(cls, thunk):
+ """
+ Slight hack to make users of this class appear to have a docstring to
+ documentation generators, by defining them with a decorator. (This hack
+ should be removed when epydoc can be convinced to use some other method
+ for documenting.)
+ """
+ return cls(thunk.__doc__)
+
+
+# Avoid exposing the ugly, private classmethod name in the docs. Luckily this
+# namespace is private already so this doesn't leak further.
+exposer = Expose._withDocumentation
+
+@exposer
+def renderer():
+ """
+ Decorate with L{renderer} to use methods as template render directives.
+
+ For example::
+
+ class Foo(Element):
+ @renderer
+ def twiddle(self, request, tag):
+ return tag('Hello, world.')
+
+ <div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+ <span t:render="twiddle" />
+ </div>
+
+ Will result in this final output::
+
+ <div>
+ <span>Hello, world.</span>
+ </div>
+ """
+
+
+
+class Element(object):
+ """
+ Base for classes which can render part of a page.
+
+ An Element is a renderer that can be embedded in a stan document and can
+ hook its template (from the loader) up to render methods.
+
+ An Element might be used to encapsulate the rendering of a complex piece of
+ data which is to be displayed in multiple different contexts. The Element
+ allows the rendering logic to be easily re-used in different ways.
+
+ Element returns render methods which are registered using
+ L{twisted.web.element.renderer}. For example::
+
+ class Menu(Element):
+ @renderer
+ def items(self, request, tag):
+ ....
+
+ Render methods are invoked with two arguments: first, the
+ L{twisted.web.http.Request} being served and second, the tag object which
+ "invoked" the render method.
+
+ @type loader: L{ITemplateLoader} provider
+ @ivar loader: The factory which will be used to load documents to
+ return from C{render}.
+ """
+ implements(IRenderable)
+ loader = None
+
+ def __init__(self, loader=None):
+ if loader is not None:
+ self.loader = loader
+
+
+ def lookupRenderMethod(self, name):
+ """
+ Look up and return the named render method.
+ """
+ method = renderer.get(self, name, None)
+ if method is None:
+ raise MissingRenderMethod(self, name)
+ return method
+
+
+ def render(self, request):
+ """
+ Implement L{IRenderable} to allow one L{Element} to be embedded in
+ another's template or rendering output.
+
+ (This will simply load the template from the C{loader}; when used in a
+ template, the flattening engine will keep track of this object
+ separately as the object to lookup renderers on and call
+ L{Element.renderer} to look them up. The resulting object from this
+ method is not directly associated with this L{Element}.)
+ """
+ loader = self.loader
+ if loader is None:
+ raise MissingTemplateLoader(self)
+ return loader.load()
+
diff --git a/twisted/web/_flatten.py b/twisted/web/_flatten.py
new file mode 100644
index 0000000..bfdc776
--- /dev/null
+++ b/twisted/web/_flatten.py
@@ -0,0 +1,314 @@
+# -*- test-case-name: twisted.web.test.test_flatten -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Context-free flattener/serializer for rendering Python objects, possibly
+complex or arbitrarily nested, as strings.
+
+"""
+
+from cStringIO import StringIO
+from sys import exc_info
+from types import GeneratorType
+from traceback import extract_tb
+from twisted.internet.defer import Deferred
+from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
+
+from twisted.web.iweb import IRenderable
+from twisted.web._stan import (
+ Tag, slot, voidElements, Comment, CDATA, CharRef)
+
+
+
+def escapedData(data, inAttribute):
+ """
+ Escape a string for inclusion in a document.
+
+ @type data: C{str} or C{unicode}
+ @param data: The string to escape.
+
+ @type inAttribute: C{bool}
+ @param inAttribute: A flag which, if set, indicates that the string should
+ be quoted for use as the value of an XML tag value.
+
+ @rtype: C{str}
+ @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
+ encoded string.
+ """
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ data = data.replace('&', '&amp;'
+ ).replace('<', '&lt;'
+ ).replace('>', '&gt;')
+ if inAttribute:
+ data = data.replace('"', '&quot;')
+ return data
+
+
+def escapedCDATA(data):
+ """
+ Escape CDATA for inclusion in a document.
+
+ @type data: C{str} or C{unicode}
+ @param data: The string to escape.
+
+ @rtype: C{str}
+ @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
+ encoded string.
+ """
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ return data.replace(']]>', ']]]]><![CDATA[>')
+
+
+def escapedComment(data):
+ """
+ Escape a comment for inclusion in a document.
+
+ @type data: C{str} or C{unicode}
+ @param data: The string to escape.
+
+ @rtype: C{str}
+ @return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
+ encoded string.
+ """
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ data = data.replace('--', '- - ').replace('>', '&gt;')
+ if data and data[-1] == '-':
+ data += ' '
+ return data
+
+
+def _getSlotValue(name, slotData, default=None):
+ """
+ Find the value of the named slot in the given stack of slot data.
+ """
+ for slotFrame in slotData[::-1]:
+ if slotFrame is not None and name in slotFrame:
+ return slotFrame[name]
+ else:
+ if default is not None:
+ return default
+ raise UnfilledSlot(name)
+
+
+def _flattenElement(request, root, slotData, renderFactory, inAttribute):
+ """
+ Make C{root} slightly more flat by yielding all its immediate contents
+ as strings, deferreds or generators that are recursive calls to itself.
+
+ @param request: A request object which will be passed to
+ L{IRenderable.render}.
+
+ @param root: An object to be made flatter. This may be of type C{unicode},
+ C{str}, L{slot}, L{Tag}, L{URL}, L{tuple}, L{list}, L{GeneratorType},
+ L{Deferred}, or an object that implements L{IRenderable}.
+
+ @param slotData: A C{list} of C{dict} mapping C{str} slot names to data
+ with which those slots will be replaced.
+
+ @param renderFactory: If not C{None}, An object that provides L{IRenderable}.
+
+ @param inAttribute: A flag which, if set, indicates that C{str} and
+ C{unicode} instances encountered must be quoted as for XML tag
+ attribute values.
+
+ @return: An iterator which yields C{str}, L{Deferred}, and more iterators
+ of the same type.
+ """
+
+ if isinstance(root, (str, unicode)):
+ yield escapedData(root, inAttribute)
+ elif isinstance(root, slot):
+ slotValue = _getSlotValue(root.name, slotData, root.default)
+ yield _flattenElement(request, slotValue, slotData, renderFactory,
+ inAttribute)
+ elif isinstance(root, CDATA):
+ yield '<![CDATA['
+ yield escapedCDATA(root.data)
+ yield ']]>'
+ elif isinstance(root, Comment):
+ yield '<!--'
+ yield escapedComment(root.data)
+ yield '-->'
+ elif isinstance(root, Tag):
+ slotData.append(root.slotData)
+ if root.render is not None:
+ rendererName = root.render
+ rootClone = root.clone(False)
+ rootClone.render = None
+ renderMethod = renderFactory.lookupRenderMethod(rendererName)
+ result = renderMethod(request, rootClone)
+ yield _flattenElement(request, result, slotData, renderFactory,
+ False)
+ slotData.pop()
+ return
+
+ if not root.tagName:
+ yield _flattenElement(request, root.children, slotData, renderFactory, False)
+ return
+
+ yield '<'
+ if isinstance(root.tagName, unicode):
+ tagName = root.tagName.encode('ascii')
+ else:
+ tagName = str(root.tagName)
+ yield tagName
+ for k, v in root.attributes.iteritems():
+ if isinstance(k, unicode):
+ k = k.encode('ascii')
+ yield ' ' + k + '="'
+ yield _flattenElement(request, v, slotData, renderFactory, True)
+ yield '"'
+ if root.children or tagName not in voidElements:
+ yield '>'
+ yield _flattenElement(request, root.children, slotData, renderFactory, False)
+ yield '</' + tagName + '>'
+ else:
+ yield ' />'
+
+ elif isinstance(root, (tuple, list, GeneratorType)):
+ for element in root:
+ yield _flattenElement(request, element, slotData, renderFactory,
+ inAttribute)
+ elif isinstance(root, CharRef):
+ yield '&#%d;' % (root.ordinal,)
+ elif isinstance(root, Deferred):
+ yield root.addCallback(
+ lambda result: (result, _flattenElement(request, result, slotData,
+ renderFactory, inAttribute)))
+ elif IRenderable.providedBy(root):
+ result = root.render(request)
+ yield _flattenElement(request, result, slotData, root, inAttribute)
+ else:
+ raise UnsupportedType(root)
+
+
+def _flattenTree(request, root):
+ """
+ Make C{root} into an iterable of C{str} and L{Deferred} by doing a
+ depth first traversal of the tree.
+
+ @param request: A request object which will be passed to
+ L{IRenderable.render}.
+
+ @param root: An object to be made flatter. This may be of type C{unicode},
+ C{str}, L{slot}, L{Tag}, L{tuple}, L{list}, L{GeneratorType},
+ L{Deferred}, or something providing L{IRenderable}.
+
+ @return: An iterator which yields objects of type C{str} and L{Deferred}.
+ A L{Deferred} is only yielded when one is encountered in the process of
+ flattening C{root}. The returned iterator must not be iterated again
+ until the L{Deferred} is called back.
+ """
+ stack = [_flattenElement(request, root, [], None, False)]
+ while stack:
+ try:
+ # In Python 2.5, after an exception, a generator's gi_frame is
+ # None.
+ frame = stack[-1].gi_frame
+ element = stack[-1].next()
+ except StopIteration:
+ stack.pop()
+ except Exception, e:
+ stack.pop()
+ roots = []
+ for generator in stack:
+ roots.append(generator.gi_frame.f_locals['root'])
+ roots.append(frame.f_locals['root'])
+ raise FlattenerError(e, roots, extract_tb(exc_info()[2]))
+ else:
+ if type(element) is str:
+ yield element
+ elif isinstance(element, Deferred):
+ def cbx((original, toFlatten)):
+ stack.append(toFlatten)
+ return original
+ yield element.addCallback(cbx)
+ else:
+ stack.append(element)
+
+
+def _writeFlattenedData(state, write, result):
+ """
+ Take strings from an iterator and pass them to a writer function.
+
+ @param state: An iterator of C{str} and L{Deferred}. C{str} instances will
+ be passed to C{write}. L{Deferred} instances will be waited on before
+ resuming iteration of C{state}.
+
+ @param write: A callable which will be invoked with each C{str}
+ produced by iterating C{state}.
+
+ @param result: A L{Deferred} which will be called back when C{state} has
+ been completely flattened into C{write} or which will be errbacked if
+ an exception in a generator passed to C{state} or an errback from a
+ L{Deferred} from state occurs.
+
+ @return: C{None}
+ """
+ while True:
+ try:
+ element = state.next()
+ except StopIteration:
+ result.callback(None)
+ except:
+ result.errback()
+ else:
+ if type(element) is str:
+ write(element)
+ continue
+ else:
+ def cby(original):
+ _writeFlattenedData(state, write, result)
+ return original
+ element.addCallbacks(cby, result.errback)
+ break
+
+
+def flatten(request, root, write):
+ """
+ Incrementally write out a string representation of C{root} using C{write}.
+
+ In order to create a string representation, C{root} will be decomposed into
+ simpler objects which will themselves be decomposed and so on until strings
+ or objects which can easily be converted to strings are encountered.
+
+ @param request: A request object which will be passed to the C{render}
+ method of any L{IRenderable} provider which is encountered.
+
+ @param root: An object to be made flatter. This may be of type C{unicode},
+ C{str}, L{slot}, L{Tag}, L{tuple}, L{list}, L{GeneratorType},
+ L{Deferred}, or something that provides L{IRenderable}.
+
+ @param write: A callable which will be invoked with each C{str}
+ produced by flattening C{root}.
+
+ @return: A L{Deferred} which will be called back when C{root} has
+ been completely flattened into C{write} or which will be errbacked if
+ an unexpected exception occurs.
+ """
+ result = Deferred()
+ state = _flattenTree(request, root)
+ _writeFlattenedData(state, write, result)
+ return result
+
+
+def flattenString(request, root):
+ """
+ Collate a string representation of C{root} into a single string.
+
+ This is basically gluing L{flatten} to a C{StringIO} and returning the
+ results. See L{flatten} for the exact meanings of C{request} and
+ C{root}.
+
+ @return: A L{Deferred} which will be called back with a single string as
+ its result when C{root} has been completely flattened into C{write} or
+ which will be errbacked if an unexpected exception occurs.
+ """
+ io = StringIO()
+ d = flatten(request, root, io.write)
+ d.addCallback(lambda _: io.getvalue())
+ return d
diff --git a/twisted/web/_newclient.py b/twisted/web/_newclient.py
new file mode 100644
index 0000000..431e029
--- /dev/null
+++ b/twisted/web/_newclient.py
@@ -0,0 +1,1502 @@
+# -*- test-case-name: twisted.web.test.test_newclient -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An U{HTTP 1.1<http://www.w3.org/Protocols/rfc2616/rfc2616.html>} client.
+
+The way to use the functionality provided by this module is to:
+
+ - Connect a L{HTTP11ClientProtocol} to an HTTP server
+ - Create a L{Request} with the appropriate data
+ - Pass the request to L{HTTP11ClientProtocol.request}
+ - The returned Deferred will fire with a L{Response} object
+ - Create a L{IProtocol} provider which can handle the response body
+ - Connect it to the response with L{Response.deliverBody}
+ - When the protocol's C{connectionLost} method is called, the response is
+ complete. See L{Response.deliverBody} for details.
+
+Various other classes in this module support this usage:
+
+ - HTTPParser is the basic HTTP parser. It can handle the parts of HTTP which
+ are symmetric between requests and responses.
+
+ - HTTPClientParser extends HTTPParser to handle response-specific parts of
+ HTTP. One instance is created for each request to parse the corresponding
+ response.
+"""
+
+__metaclass__ = type
+
+from zope.interface import implements
+
+from twisted.python import log
+from twisted.python.reflect import fullyQualifiedName
+from twisted.python.failure import Failure
+from twisted.python.compat import set
+from twisted.internet.interfaces import IConsumer, IPushProducer
+from twisted.internet.error import ConnectionDone
+from twisted.internet.defer import Deferred, succeed, fail, maybeDeferred
+from twisted.internet.protocol import Protocol
+from twisted.protocols.basic import LineReceiver
+from twisted.web.iweb import UNKNOWN_LENGTH, IResponse
+from twisted.web.http_headers import Headers
+from twisted.web.http import NO_CONTENT, NOT_MODIFIED
+from twisted.web.http import _DataLoss, PotentialDataLoss
+from twisted.web.http import _IdentityTransferDecoder, _ChunkedTransferDecoder
+
+# States HTTPParser can be in
+STATUS = 'STATUS'
+HEADER = 'HEADER'
+BODY = 'BODY'
+DONE = 'DONE'
+
+
+class BadHeaders(Exception):
+ """
+ Headers passed to L{Request} were in some way invalid.
+ """
+
+
+
+class ExcessWrite(Exception):
+ """
+ The body L{IBodyProducer} for a request tried to write data after
+ indicating it had finished writing data.
+ """
+
+
+class ParseError(Exception):
+ """
+ Some received data could not be parsed.
+
+ @ivar data: The string which could not be parsed.
+ """
+ def __init__(self, reason, data):
+ Exception.__init__(self, reason, data)
+ self.data = data
+
+
+
+class BadResponseVersion(ParseError):
+ """
+ The version string in a status line was unparsable.
+ """
+
+
+
+class _WrapperException(Exception):
+ """
+ L{_WrapperException} is the base exception type for exceptions which
+ include one or more other exceptions as the low-level causes.
+
+ @ivar reasons: A list of exceptions. See subclass documentation for more
+ details.
+ """
+ def __init__(self, reasons):
+ Exception.__init__(self, reasons)
+ self.reasons = reasons
+
+
+
+class RequestGenerationFailed(_WrapperException):
+ """
+ There was an error while creating the bytes which make up a request.
+
+ @ivar reasons: A C{list} of one or more L{Failure} instances giving the
+ reasons the request generation was considered to have failed.
+ """
+
+
+
+class RequestTransmissionFailed(_WrapperException):
+ """
+ There was an error while sending the bytes which make up a request.
+
+ @ivar reasons: A C{list} of one or more L{Failure} instances giving the
+ reasons the request transmission was considered to have failed.
+ """
+
+
+
+class ConnectionAborted(Exception):
+ """
+ The connection was explicitly aborted by application code.
+ """
+
+
+
+class WrongBodyLength(Exception):
+ """
+ An L{IBodyProducer} declared the number of bytes it was going to
+ produce (via its C{length} attribute) and then produced a different number
+ of bytes.
+ """
+
+
+
+class ResponseDone(Exception):
+ """
+ L{ResponseDone} may be passed to L{IProtocol.connectionLost} on the
+ protocol passed to L{Response.deliverBody} and indicates that the entire
+ response has been delivered.
+ """
+
+
+
+class ResponseFailed(_WrapperException):
+ """
+ L{ResponseFailed} indicates that all of the response to a request was not
+ received for some reason.
+
+ @ivar reasons: A C{list} of one or more L{Failure} instances giving the
+ reasons the response was considered to have failed.
+
+ @ivar response: If specified, the L{Response} received from the server (and
+ in particular the status code and the headers).
+ """
+
+ def __init__(self, reasons, response=None):
+ _WrapperException.__init__(self, reasons)
+ self.response = response
+
+
+
+class ResponseNeverReceived(ResponseFailed):
+ """
+ A L{ResponseFailed} that knows no response bytes at all have been received.
+ """
+
+
+
+class RequestNotSent(Exception):
+ """
+ L{RequestNotSent} indicates that an attempt was made to issue a request but
+ for reasons unrelated to the details of the request itself, the request
+ could not be sent. For example, this may indicate that an attempt was made
+ to send a request using a protocol which is no longer connected to a
+ server.
+ """
+
+
+
+def _callAppFunction(function):
+ """
+ Call C{function}. If it raises an exception, log it with a minimal
+ description of the source.
+
+ @return: C{None}
+ """
+ try:
+ function()
+ except:
+ log.err(None, "Unexpected exception from %s" % (
+ fullyQualifiedName(function),))
+
+
+
+class HTTPParser(LineReceiver):
+ """
+ L{HTTPParser} handles the parsing side of HTTP processing. With a suitable
+ subclass, it can parse either the client side or the server side of the
+ connection.
+
+ @ivar headers: All of the non-connection control message headers yet
+ received.
+
+ @ivar state: State indicator for the response parsing state machine. One
+ of C{STATUS}, C{HEADER}, C{BODY}, C{DONE}.
+
+ @ivar _partialHeader: C{None} or a C{list} of the lines of a multiline
+ header while that header is being received.
+ """
+
+ # NOTE: According to HTTP spec, we're supposed to eat the
+ # 'Proxy-Authenticate' and 'Proxy-Authorization' headers also, but that
+ # doesn't sound like a good idea to me, because it makes it impossible to
+ # have a non-authenticating transparent proxy in front of an authenticating
+ # proxy. An authenticating proxy can eat them itself. -jknight
+ #
+ # Further, quoting
+ # http://homepages.tesco.net/J.deBoynePollard/FGA/web-proxy-connection-header.html
+ # regarding the 'Proxy-Connection' header:
+ #
+ # The Proxy-Connection: header is a mistake in how some web browsers
+ # use HTTP. Its name is the result of a false analogy. It is not a
+ # standard part of the protocol. There is a different standard
+ # protocol mechanism for doing what it does. And its existence
+ # imposes a requirement upon HTTP servers such that no proxy HTTP
+ # server can be standards-conforming in practice.
+ #
+ # -exarkun
+
+ # Some servers (like http://news.ycombinator.com/) return status lines and
+ # HTTP headers delimited by \n instead of \r\n.
+ delimiter = '\n'
+
+ CONNECTION_CONTROL_HEADERS = set([
+ 'content-length', 'connection', 'keep-alive', 'te', 'trailers',
+ 'transfer-encoding', 'upgrade', 'proxy-connection'])
+
+ def connectionMade(self):
+ self.headers = Headers()
+ self.connHeaders = Headers()
+ self.state = STATUS
+ self._partialHeader = None
+
+
+ def switchToBodyMode(self, decoder):
+ """
+ Switch to body parsing mode - interpret any more bytes delivered as
+ part of the message body and deliver them to the given decoder.
+ """
+ if self.state == BODY:
+ raise RuntimeError("already in body mode")
+
+ self.bodyDecoder = decoder
+ self.state = BODY
+ self.setRawMode()
+
+
+ def lineReceived(self, line):
+ """
+ Handle one line from a response.
+ """
+ # Handle the normal CR LF case.
+ if line[-1:] == '\r':
+ line = line[:-1]
+
+ if self.state == STATUS:
+ self.statusReceived(line)
+ self.state = HEADER
+ elif self.state == HEADER:
+ if not line or line[0] not in ' \t':
+ if self._partialHeader is not None:
+ header = ''.join(self._partialHeader)
+ name, value = header.split(':', 1)
+ value = value.strip()
+ self.headerReceived(name, value)
+ if not line:
+ # Empty line means the header section is over.
+ self.allHeadersReceived()
+ else:
+ # Line not beginning with LWS is another header.
+ self._partialHeader = [line]
+ else:
+ # A line beginning with LWS is a continuation of a header
+ # begun on a previous line.
+ self._partialHeader.append(line)
+
+
+ def rawDataReceived(self, data):
+ """
+ Pass data from the message body to the body decoder object.
+ """
+ self.bodyDecoder.dataReceived(data)
+
+
+ def isConnectionControlHeader(self, name):
+ """
+ Return C{True} if the given lower-cased name is the name of a
+ connection control header (rather than an entity header).
+
+ According to RFC 2616, section 14.10, the tokens in the Connection
+ header are probably relevant here. However, I am not sure what the
+ practical consequences of either implementing or ignoring that are.
+ So I leave it unimplemented for the time being.
+ """
+ return name in self.CONNECTION_CONTROL_HEADERS
+
+
+ def statusReceived(self, status):
+ """
+ Callback invoked whenever the first line of a new message is received.
+ Override this.
+
+ @param status: The first line of an HTTP request or response message
+ without trailing I{CR LF}.
+ @type status: C{str}
+ """
+
+
+ def headerReceived(self, name, value):
+ """
+ Store the given header in C{self.headers}.
+ """
+ name = name.lower()
+ if self.isConnectionControlHeader(name):
+ headers = self.connHeaders
+ else:
+ headers = self.headers
+ headers.addRawHeader(name, value)
+
+
+ def allHeadersReceived(self):
+ """
+ Callback invoked after the last header is passed to C{headerReceived}.
+ Override this to change to the C{BODY} or C{DONE} state.
+ """
+ self.switchToBodyMode(None)
+
+
+
+class HTTPClientParser(HTTPParser):
+ """
+ An HTTP parser which only handles HTTP responses.
+
+ @ivar request: The request with which the expected response is associated.
+ @type request: L{Request}
+
+ @ivar NO_BODY_CODES: A C{set} of response codes which B{MUST NOT} have a
+ body.
+
+ @ivar finisher: A callable to invoke when this response is fully parsed.
+
+ @ivar _responseDeferred: A L{Deferred} which will be called back with the
+ response when all headers in the response have been received.
+ Thereafter, C{None}.
+
+ @ivar _everReceivedData: C{True} if any bytes have been received.
+ """
+ NO_BODY_CODES = set([NO_CONTENT, NOT_MODIFIED])
+
+ _transferDecoders = {
+ 'chunked': _ChunkedTransferDecoder,
+ }
+
+ bodyDecoder = None
+
+ def __init__(self, request, finisher):
+ self.request = request
+ self.finisher = finisher
+ self._responseDeferred = Deferred()
+ self._everReceivedData = False
+
+
+ def dataReceived(self, data):
+ """
+ Override so that we know if any response has been received.
+ """
+ self._everReceivedData = True
+ HTTPParser.dataReceived(self, data)
+
+
+ def parseVersion(self, strversion):
+ """
+ Parse version strings of the form Protocol '/' Major '.' Minor. E.g.
+ 'HTTP/1.1'. Returns (protocol, major, minor). Will raise ValueError
+ on bad syntax.
+ """
+ try:
+ proto, strnumber = strversion.split('/')
+ major, minor = strnumber.split('.')
+ major, minor = int(major), int(minor)
+ except ValueError, e:
+ raise BadResponseVersion(str(e), strversion)
+ if major < 0 or minor < 0:
+ raise BadResponseVersion("version may not be negative", strversion)
+ return (proto, major, minor)
+
+
+ def statusReceived(self, status):
+ """
+ Parse the status line into its components and create a response object
+ to keep track of this response's state.
+ """
+ parts = status.split(' ', 2)
+ if len(parts) != 3:
+ raise ParseError("wrong number of parts", status)
+
+ try:
+ statusCode = int(parts[1])
+ except ValueError:
+ raise ParseError("non-integer status code", status)
+
+ self.response = Response(
+ self.parseVersion(parts[0]),
+ statusCode,
+ parts[2],
+ self.headers,
+ self.transport)
+
+
+ def _finished(self, rest):
+ """
+ Called to indicate that an entire response has been received. No more
+ bytes will be interpreted by this L{HTTPClientParser}. Extra bytes are
+ passed up and the state of this L{HTTPClientParser} is set to I{DONE}.
+
+ @param rest: A C{str} giving any extra bytes delivered to this
+ L{HTTPClientParser} which are not part of the response being
+ parsed.
+ """
+ self.state = DONE
+ self.finisher(rest)
+
+
+ def isConnectionControlHeader(self, name):
+ """
+ Content-Length in the response to a HEAD request is an entity header,
+ not a connection control header.
+ """
+ if self.request.method == 'HEAD' and name == 'content-length':
+ return False
+ return HTTPParser.isConnectionControlHeader(self, name)
+
+
+ def allHeadersReceived(self):
+ """
+ Figure out how long the response body is going to be by examining
+ headers and stuff.
+ """
+ if (self.response.code in self.NO_BODY_CODES
+ or self.request.method == 'HEAD'):
+ self.response.length = 0
+ self._finished(self.clearLineBuffer())
+ else:
+ transferEncodingHeaders = self.connHeaders.getRawHeaders(
+ 'transfer-encoding')
+ if transferEncodingHeaders:
+
+ # This could be a KeyError. However, that would mean we do not
+ # know how to decode the response body, so failing the request
+ # is as good a behavior as any. Perhaps someday we will want
+ # to normalize/document/test this specifically, but failing
+ # seems fine to me for now.
+ transferDecoder = self._transferDecoders[transferEncodingHeaders[0].lower()]
+
+ # If anyone ever invents a transfer encoding other than
+ # chunked (yea right), and that transfer encoding can predict
+ # the length of the response body, it might be sensible to
+ # allow the transfer decoder to set the response object's
+ # length attribute.
+ else:
+ contentLengthHeaders = self.connHeaders.getRawHeaders('content-length')
+ if contentLengthHeaders is None:
+ contentLength = None
+ elif len(contentLengthHeaders) == 1:
+ contentLength = int(contentLengthHeaders[0])
+ self.response.length = contentLength
+ else:
+ # "HTTP Message Splitting" or "HTTP Response Smuggling"
+ # potentially happening. Or it's just a buggy server.
+ raise ValueError(
+ "Too many Content-Length headers; response is invalid")
+
+ if contentLength == 0:
+ self._finished(self.clearLineBuffer())
+ transferDecoder = None
+ else:
+ transferDecoder = lambda x, y: _IdentityTransferDecoder(
+ contentLength, x, y)
+
+ if transferDecoder is None:
+ self.response._bodyDataFinished()
+ else:
+ # Make sure as little data as possible from the response body
+ # gets delivered to the response object until the response
+ # object actually indicates it is ready to handle bytes
+ # (probably because an application gave it a way to interpret
+ # them).
+ self.transport.pauseProducing()
+ self.switchToBodyMode(transferDecoder(
+ self.response._bodyDataReceived,
+ self._finished))
+
+ # This must be last. If it were first, then application code might
+ # change some state (for example, registering a protocol to receive the
+ # response body). Then the pauseProducing above would be wrong since
+ # the response is ready for bytes and nothing else would ever resume
+ # the transport.
+ self._responseDeferred.callback(self.response)
+ del self._responseDeferred
+
+
+ def connectionLost(self, reason):
+ if self.bodyDecoder is not None:
+ try:
+ try:
+ self.bodyDecoder.noMoreData()
+ except PotentialDataLoss:
+ self.response._bodyDataFinished(Failure())
+ except _DataLoss:
+ self.response._bodyDataFinished(
+ Failure(ResponseFailed([reason, Failure()],
+ self.response)))
+ else:
+ self.response._bodyDataFinished()
+ except:
+ # Handle exceptions from both the except suites and the else
+ # suite. Those functions really shouldn't raise exceptions,
+ # but maybe there's some buggy application code somewhere
+ # making things difficult.
+ log.err()
+ elif self.state != DONE:
+ if self._everReceivedData:
+ exceptionClass = ResponseFailed
+ else:
+ exceptionClass = ResponseNeverReceived
+ self._responseDeferred.errback(Failure(exceptionClass([reason])))
+ del self._responseDeferred
+
+
+
+class Request:
+ """
+ A L{Request} instance describes an HTTP request to be sent to an HTTP
+ server.
+
+ @ivar method: The HTTP method to for this request, ex: 'GET', 'HEAD',
+ 'POST', etc.
+ @type method: C{str}
+
+ @ivar uri: The relative URI of the resource to request. For example,
+ C{'/foo/bar?baz=quux'}.
+ @type uri: C{str}
+
+ @ivar headers: Headers to be sent to the server. It is important to
+ note that this object does not create any implicit headers. So it
+ is up to the HTTP Client to add required headers such as 'Host'.
+ @type headers: L{twisted.web.http_headers.Headers}
+
+ @ivar bodyProducer: C{None} or an L{IBodyProducer} provider which
+ produces the content body to send to the remote HTTP server.
+
+ @ivar persistent: Set to C{True} when you use HTTP persistent connection.
+ @type persistent: C{bool}
+ """
+ def __init__(self, method, uri, headers, bodyProducer, persistent=False):
+ self.method = method
+ self.uri = uri
+ self.headers = headers
+ self.bodyProducer = bodyProducer
+ self.persistent = persistent
+
+
+ def _writeHeaders(self, transport, TEorCL):
+ hosts = self.headers.getRawHeaders('host', ())
+ if len(hosts) != 1:
+ raise BadHeaders("Exactly one Host header required")
+
+ # In the future, having the protocol version be a parameter to this
+ # method would probably be good. It would be nice if this method
+ # weren't limited to issueing HTTP/1.1 requests.
+ requestLines = []
+ requestLines.append(
+ '%s %s HTTP/1.1\r\n' % (self.method, self.uri))
+ if not self.persistent:
+ requestLines.append('Connection: close\r\n')
+ if TEorCL is not None:
+ requestLines.append(TEorCL)
+ for name, values in self.headers.getAllRawHeaders():
+ requestLines.extend(['%s: %s\r\n' % (name, v) for v in values])
+ requestLines.append('\r\n')
+ transport.writeSequence(requestLines)
+
+
+ def _writeToChunked(self, transport):
+ """
+ Write this request to the given transport using chunked
+ transfer-encoding to frame the body.
+ """
+ self._writeHeaders(transport, 'Transfer-Encoding: chunked\r\n')
+ encoder = ChunkedEncoder(transport)
+ encoder.registerProducer(self.bodyProducer, True)
+ d = self.bodyProducer.startProducing(encoder)
+
+ def cbProduced(ignored):
+ encoder.unregisterProducer()
+ def ebProduced(err):
+ encoder._allowNoMoreWrites()
+ # Don't call the encoder's unregisterProducer because it will write
+ # a zero-length chunk. This would indicate to the server that the
+ # request body is complete. There was an error, though, so we
+ # don't want to do that.
+ transport.unregisterProducer()
+ return err
+ d.addCallbacks(cbProduced, ebProduced)
+ return d
+
+
+ def _writeToContentLength(self, transport):
+ """
+ Write this request to the given transport using content-length to frame
+ the body.
+ """
+ self._writeHeaders(
+ transport,
+ 'Content-Length: %d\r\n' % (self.bodyProducer.length,))
+
+ # This Deferred is used to signal an error in the data written to the
+ # encoder below. It can only errback and it will only do so before too
+ # many bytes have been written to the encoder and before the producer
+ # Deferred fires.
+ finishedConsuming = Deferred()
+
+ # This makes sure the producer writes the correct number of bytes for
+ # the request body.
+ encoder = LengthEnforcingConsumer(
+ self.bodyProducer, transport, finishedConsuming)
+
+ transport.registerProducer(self.bodyProducer, True)
+
+ finishedProducing = self.bodyProducer.startProducing(encoder)
+
+ def combine(consuming, producing):
+ # This Deferred is returned and will be fired when the first of
+ # consuming or producing fires.
+ ultimate = Deferred()
+
+ # Keep track of what has happened so far. This initially
+ # contains None, then an integer uniquely identifying what
+ # sequence of events happened. See the callbacks and errbacks
+ # defined below for the meaning of each value.
+ state = [None]
+
+ def ebConsuming(err):
+ if state == [None]:
+ # The consuming Deferred failed first. This means the
+ # overall writeTo Deferred is going to errback now. The
+ # producing Deferred should not fire later (because the
+ # consumer should have called stopProducing on the
+ # producer), but if it does, a callback will be ignored
+ # and an errback will be logged.
+ state[0] = 1
+ ultimate.errback(err)
+ else:
+ # The consuming Deferred errbacked after the producing
+ # Deferred fired. This really shouldn't ever happen.
+ # If it does, I goofed. Log the error anyway, just so
+ # there's a chance someone might notice and complain.
+ log.err(
+ err,
+ "Buggy state machine in %r/[%d]: "
+ "ebConsuming called" % (self, state[0]))
+
+ def cbProducing(result):
+ if state == [None]:
+ # The producing Deferred succeeded first. Nothing will
+ # ever happen to the consuming Deferred. Tell the
+ # encoder we're done so it can check what the producer
+ # wrote and make sure it was right.
+ state[0] = 2
+ try:
+ encoder._noMoreWritesExpected()
+ except:
+ # Fail the overall writeTo Deferred - something the
+ # producer did was wrong.
+ ultimate.errback()
+ else:
+ # Success - succeed the overall writeTo Deferred.
+ ultimate.callback(None)
+ # Otherwise, the consuming Deferred already errbacked. The
+ # producing Deferred wasn't supposed to fire, but it did
+ # anyway. It's buggy, but there's not really anything to be
+ # done about it. Just ignore this result.
+
+ def ebProducing(err):
+ if state == [None]:
+ # The producing Deferred failed first. This means the
+ # overall writeTo Deferred is going to errback now.
+ # Tell the encoder that we're done so it knows to reject
+ # further writes from the producer (which should not
+ # happen, but the producer may be buggy).
+ state[0] = 3
+ encoder._allowNoMoreWrites()
+ ultimate.errback(err)
+ else:
+ # The producing Deferred failed after the consuming
+ # Deferred failed. It shouldn't have, so it's buggy.
+ # Log the exception in case anyone who can fix the code
+ # is watching.
+ log.err(err, "Producer is buggy")
+
+ consuming.addErrback(ebConsuming)
+ producing.addCallbacks(cbProducing, ebProducing)
+
+ return ultimate
+
+ d = combine(finishedConsuming, finishedProducing)
+ def f(passthrough):
+ # Regardless of what happens with the overall Deferred, once it
+ # fires, the producer registered way up above the definition of
+ # combine should be unregistered.
+ transport.unregisterProducer()
+ return passthrough
+ d.addBoth(f)
+ return d
+
+
+ def writeTo(self, transport):
+ """
+ Format this L{Request} as an HTTP/1.1 request and write it to the given
+ transport. If bodyProducer is not None, it will be associated with an
+ L{IConsumer}.
+
+ @return: A L{Deferred} which fires with C{None} when the request has
+ been completely written to the transport or with a L{Failure} if
+ there is any problem generating the request bytes.
+ """
+ if self.bodyProducer is not None:
+ if self.bodyProducer.length is UNKNOWN_LENGTH:
+ return self._writeToChunked(transport)
+ else:
+ return self._writeToContentLength(transport)
+ else:
+ self._writeHeaders(transport, None)
+ return succeed(None)
+
+
+ def stopWriting(self):
+ """
+ Stop writing this request to the transport. This can only be called
+ after C{writeTo} and before the L{Deferred} returned by C{writeTo}
+ fires. It should cancel any asynchronous task started by C{writeTo}.
+ The L{Deferred} returned by C{writeTo} need not be fired if this method
+ is called.
+ """
+ # If bodyProducer is None, then the Deferred returned by writeTo has
+ # fired already and this method cannot be called.
+ _callAppFunction(self.bodyProducer.stopProducing)
+
+
+
+class LengthEnforcingConsumer:
+ """
+ An L{IConsumer} proxy which enforces an exact length requirement on the
+ total data written to it.
+
+ @ivar _length: The number of bytes remaining to be written.
+
+ @ivar _producer: The L{IBodyProducer} which is writing to this
+ consumer.
+
+ @ivar _consumer: The consumer to which at most C{_length} bytes will be
+ forwarded.
+
+ @ivar _finished: A L{Deferred} which will be fired with a L{Failure} if too
+ many bytes are written to this consumer.
+ """
+ def __init__(self, producer, consumer, finished):
+ self._length = producer.length
+ self._producer = producer
+ self._consumer = consumer
+ self._finished = finished
+
+
+ def _allowNoMoreWrites(self):
+ """
+ Indicate that no additional writes are allowed. Attempts to write
+ after calling this method will be met with an exception.
+ """
+ self._finished = None
+
+
+ def write(self, bytes):
+ """
+ Write C{bytes} to the underlying consumer unless
+ C{_noMoreWritesExpected} has been called or there are/have been too
+ many bytes.
+ """
+ if self._finished is None:
+ # No writes are supposed to happen any more. Try to convince the
+ # calling code to stop calling this method by calling its
+ # stopProducing method and then throwing an exception at it. This
+ # exception isn't documented as part of the API because you're
+ # never supposed to expect it: only buggy code will ever receive
+ # it.
+ self._producer.stopProducing()
+ raise ExcessWrite()
+
+ if len(bytes) <= self._length:
+ self._length -= len(bytes)
+ self._consumer.write(bytes)
+ else:
+ # No synchronous exception is raised in *this* error path because
+ # we still have _finished which we can use to report the error to a
+ # better place than the direct caller of this method (some
+ # arbitrary application code).
+ _callAppFunction(self._producer.stopProducing)
+ self._finished.errback(WrongBodyLength("too many bytes written"))
+ self._allowNoMoreWrites()
+
+
+ def _noMoreWritesExpected(self):
+ """
+ Called to indicate no more bytes will be written to this consumer.
+ Check to see that the correct number have been written.
+
+ @raise WrongBodyLength: If not enough bytes have been written.
+ """
+ if self._finished is not None:
+ self._allowNoMoreWrites()
+ if self._length:
+ raise WrongBodyLength("too few bytes written")
+
+
+
+def makeStatefulDispatcher(name, template):
+ """
+ Given a I{dispatch} name and a function, return a function which can be
+ used as a method and which, when called, will call another method defined
+ on the instance and return the result. The other method which is called is
+ determined by the value of the C{_state} attribute of the instance.
+
+ @param name: A string which is used to construct the name of the subsidiary
+ method to invoke. The subsidiary method is named like C{'_%s_%s' %
+ (name, _state)}.
+
+ @param template: A function object which is used to give the returned
+ function a docstring.
+
+ @return: The dispatcher function.
+ """
+ def dispatcher(self, *args, **kwargs):
+ func = getattr(self, '_' + name + '_' + self._state, None)
+ if func is None:
+ raise RuntimeError(
+ "%r has no %s method in state %s" % (self, name, self._state))
+ return func(*args, **kwargs)
+ dispatcher.__doc__ = template.__doc__
+ return dispatcher
+
+
+
+class Response:
+ """
+ A L{Response} instance describes an HTTP response received from an HTTP
+ server.
+
+ L{Response} should not be subclassed or instantiated.
+
+ @ivar _transport: The transport which is delivering this response.
+
+ @ivar _bodyProtocol: The L{IProtocol} provider to which the body is
+ delivered. C{None} before one has been registered with
+ C{deliverBody}.
+
+ @ivar _bodyBuffer: A C{list} of the strings passed to C{bodyDataReceived}
+ before C{deliverBody} is called. C{None} afterwards.
+
+ @ivar _state: Indicates what state this L{Response} instance is in,
+ particularly with respect to delivering bytes from the response body
+ to an application-suppled protocol object. This may be one of
+ C{'INITIAL'}, C{'CONNECTED'}, C{'DEFERRED_CLOSE'}, or C{'FINISHED'},
+ with the following meanings:
+
+ - INITIAL: This is the state L{Response} objects start in. No
+ protocol has yet been provided and the underlying transport may
+ still have bytes to deliver to it.
+
+ - DEFERRED_CLOSE: If the underlying transport indicates all bytes
+ have been delivered but no application-provided protocol is yet
+ available, the L{Response} moves to this state. Data is
+ buffered and waiting for a protocol to be delivered to.
+
+ - CONNECTED: If a protocol is provided when the state is INITIAL,
+ the L{Response} moves to this state. Any buffered data is
+ delivered and any data which arrives from the transport
+ subsequently is given directly to the protocol.
+
+ - FINISHED: If a protocol is provided in the DEFERRED_CLOSE state,
+ the L{Response} moves to this state after delivering all
+ buffered data to the protocol. Otherwise, if the L{Response} is
+ in the CONNECTED state, if the transport indicates there is no
+ more data, the L{Response} moves to this state. Nothing else
+ can happen once the L{Response} is in this state.
+ """
+ implements(IResponse)
+
+ length = UNKNOWN_LENGTH
+
+ _bodyProtocol = None
+ _bodyFinished = False
+
+ def __init__(self, version, code, phrase, headers, _transport):
+ self.version = version
+ self.code = code
+ self.phrase = phrase
+ self.headers = headers
+ self._transport = _transport
+ self._bodyBuffer = []
+ self._state = 'INITIAL'
+
+
+ def deliverBody(self, protocol):
+ """
+ Dispatch the given L{IProtocol} depending of the current state of the
+ response.
+ """
+ deliverBody = makeStatefulDispatcher('deliverBody', deliverBody)
+
+
+ def _deliverBody_INITIAL(self, protocol):
+ """
+ Deliver any buffered data to C{protocol} and prepare to deliver any
+ future data to it. Move to the C{'CONNECTED'} state.
+ """
+ # Now that there's a protocol to consume the body, resume the
+ # transport. It was previously paused by HTTPClientParser to avoid
+ # reading too much data before it could be handled.
+ self._transport.resumeProducing()
+
+ protocol.makeConnection(self._transport)
+ self._bodyProtocol = protocol
+ for data in self._bodyBuffer:
+ self._bodyProtocol.dataReceived(data)
+ self._bodyBuffer = None
+ self._state = 'CONNECTED'
+
+
+ def _deliverBody_CONNECTED(self, protocol):
+ """
+ It is invalid to attempt to deliver data to a protocol when it is
+ already being delivered to another protocol.
+ """
+ raise RuntimeError(
+ "Response already has protocol %r, cannot deliverBody "
+ "again" % (self._bodyProtocol,))
+
+
+ def _deliverBody_DEFERRED_CLOSE(self, protocol):
+ """
+ Deliver any buffered data to C{protocol} and then disconnect the
+ protocol. Move to the C{'FINISHED'} state.
+ """
+ # Unlike _deliverBody_INITIAL, there is no need to resume the
+ # transport here because all of the response data has been received
+ # already. Some higher level code may want to resume the transport if
+ # that code expects further data to be received over it.
+
+ protocol.makeConnection(self._transport)
+
+ for data in self._bodyBuffer:
+ protocol.dataReceived(data)
+ self._bodyBuffer = None
+ protocol.connectionLost(self._reason)
+ self._state = 'FINISHED'
+
+
+ def _deliverBody_FINISHED(self, protocol):
+ """
+ It is invalid to attempt to deliver data to a protocol after the
+ response body has been delivered to another protocol.
+ """
+ raise RuntimeError(
+ "Response already finished, cannot deliverBody now.")
+
+
+ def _bodyDataReceived(self, data):
+ """
+ Called by HTTPClientParser with chunks of data from the response body.
+ They will be buffered or delivered to the protocol passed to
+ deliverBody.
+ """
+ _bodyDataReceived = makeStatefulDispatcher('bodyDataReceived',
+ _bodyDataReceived)
+
+
+ def _bodyDataReceived_INITIAL(self, data):
+ """
+ Buffer any data received for later delivery to a protocol passed to
+ C{deliverBody}.
+
+ Little or no data should be buffered by this method, since the
+ transport has been paused and will not be resumed until a protocol
+ is supplied.
+ """
+ self._bodyBuffer.append(data)
+
+
+ def _bodyDataReceived_CONNECTED(self, data):
+ """
+ Deliver any data received to the protocol to which this L{Response}
+ is connected.
+ """
+ self._bodyProtocol.dataReceived(data)
+
+
+ def _bodyDataReceived_DEFERRED_CLOSE(self, data):
+ """
+ It is invalid for data to be delivered after it has been indicated
+ that the response body has been completely delivered.
+ """
+ raise RuntimeError("Cannot receive body data after _bodyDataFinished")
+
+
+ def _bodyDataReceived_FINISHED(self, data):
+ """
+ It is invalid for data to be delivered after the response body has
+ been delivered to a protocol.
+ """
+ raise RuntimeError("Cannot receive body data after protocol disconnected")
+
+
+ def _bodyDataFinished(self, reason=None):
+ """
+ Called by HTTPClientParser when no more body data is available. If the
+ optional reason is supplied, this indicates a problem or potential
+ problem receiving all of the response body.
+ """
+ _bodyDataFinished = makeStatefulDispatcher('bodyDataFinished',
+ _bodyDataFinished)
+
+
+ def _bodyDataFinished_INITIAL(self, reason=None):
+ """
+ Move to the C{'DEFERRED_CLOSE'} state to wait for a protocol to
+ which to deliver the response body.
+ """
+ self._state = 'DEFERRED_CLOSE'
+ if reason is None:
+ reason = Failure(ResponseDone("Response body fully received"))
+ self._reason = reason
+
+
+ def _bodyDataFinished_CONNECTED(self, reason=None):
+ """
+ Disconnect the protocol and move to the C{'FINISHED'} state.
+ """
+ if reason is None:
+ reason = Failure(ResponseDone("Response body fully received"))
+ self._bodyProtocol.connectionLost(reason)
+ self._bodyProtocol = None
+ self._state = 'FINISHED'
+
+
+ def _bodyDataFinished_DEFERRED_CLOSE(self):
+ """
+ It is invalid to attempt to notify the L{Response} of the end of the
+ response body data more than once.
+ """
+ raise RuntimeError("Cannot finish body data more than once")
+
+
+ def _bodyDataFinished_FINISHED(self):
+ """
+ It is invalid to attempt to notify the L{Response} of the end of the
+ response body data more than once.
+ """
+ raise RuntimeError("Cannot finish body data after protocol disconnected")
+
+
+
+class ChunkedEncoder:
+ """
+ Helper object which exposes L{IConsumer} on top of L{HTTP11ClientProtocol}
+ for streaming request bodies to the server.
+ """
+ implements(IConsumer)
+
+ def __init__(self, transport):
+ self.transport = transport
+
+
+ def _allowNoMoreWrites(self):
+ """
+ Indicate that no additional writes are allowed. Attempts to write
+ after calling this method will be met with an exception.
+ """
+ self.transport = None
+
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register the given producer with C{self.transport}.
+ """
+ self.transport.registerProducer(producer, streaming)
+
+
+ def write(self, data):
+ """
+ Write the given request body bytes to the transport using chunked
+ encoding.
+
+ @type data: C{str}
+ """
+ if self.transport is None:
+ raise ExcessWrite()
+ self.transport.writeSequence(("%x\r\n" % len(data), data, "\r\n"))
+
+
+ def unregisterProducer(self):
+ """
+ Indicate that the request body is complete and finish the request.
+ """
+ self.write('')
+ self.transport.unregisterProducer()
+ self._allowNoMoreWrites()
+
+
+
+class TransportProxyProducer:
+ """
+ An L{IPushProducer} implementation which wraps another such thing and
+ proxies calls to it until it is told to stop.
+
+ @ivar _producer: The wrapped L{IPushProducer} provider or C{None} after
+ this proxy has been stopped.
+ """
+ implements(IPushProducer)
+
+ # LineReceiver uses this undocumented attribute of transports to decide
+ # when to stop calling lineReceived or rawDataReceived (if it finds it to
+ # be true, it doesn't bother to deliver any more data). Set disconnecting
+ # to False here and never change it to true so that all data is always
+ # delivered to us and so that LineReceiver doesn't fail with an
+ # AttributeError.
+ disconnecting = False
+
+ def __init__(self, producer):
+ self._producer = producer
+
+
+ def _stopProxying(self):
+ """
+ Stop forwarding calls of L{IPushProducer} methods to the underlying
+ L{IPushProvider} provider.
+ """
+ self._producer = None
+
+
+ def stopProducing(self):
+ """
+ Proxy the stoppage to the underlying producer, unless this proxy has
+ been stopped.
+ """
+ if self._producer is not None:
+ self._producer.stopProducing()
+
+
+ def resumeProducing(self):
+ """
+ Proxy the resumption to the underlying producer, unless this proxy has
+ been stopped.
+ """
+ if self._producer is not None:
+ self._producer.resumeProducing()
+
+
+ def pauseProducing(self):
+ """
+ Proxy the pause to the underlying producer, unless this proxy has been
+ stopped.
+ """
+ if self._producer is not None:
+ self._producer.pauseProducing()
+
+
+
+class HTTP11ClientProtocol(Protocol):
+ """
+ L{HTTP11ClientProtocol} is an implementation of the HTTP 1.1 client
+ protocol. It supports as few features as possible.
+
+ @ivar _parser: After a request is issued, the L{HTTPClientParser} to
+ which received data making up the response to that request is
+ delivered.
+
+ @ivar _finishedRequest: After a request is issued, the L{Deferred} which
+ will fire when a L{Response} object corresponding to that request is
+ available. This allows L{HTTP11ClientProtocol} to fail the request
+ if there is a connection or parsing problem.
+
+ @ivar _currentRequest: After a request is issued, the L{Request}
+ instance used to make that request. This allows
+ L{HTTP11ClientProtocol} to stop request generation if necessary (for
+ example, if the connection is lost).
+
+ @ivar _transportProxy: After a request is issued, the
+ L{TransportProxyProducer} to which C{_parser} is connected. This
+ allows C{_parser} to pause and resume the transport in a way which
+ L{HTTP11ClientProtocol} can exert some control over.
+
+ @ivar _responseDeferred: After a request is issued, the L{Deferred} from
+ C{_parser} which will fire with a L{Response} when one has been
+ received. This is eventually chained with C{_finishedRequest}, but
+ only in certain cases to avoid double firing that Deferred.
+
+ @ivar _state: Indicates what state this L{HTTP11ClientProtocol} instance
+ is in with respect to transmission of a request and reception of a
+ response. This may be one of the following strings:
+
+ - QUIESCENT: This is the state L{HTTP11ClientProtocol} instances
+ start in. Nothing is happening: no request is being sent and no
+ response is being received or expected.
+
+ - TRANSMITTING: When a request is made (via L{request}), the
+ instance moves to this state. L{Request.writeTo} has been used
+ to start to send a request but it has not yet finished.
+
+ - TRANSMITTING_AFTER_RECEIVING_RESPONSE: The server has returned a
+ complete response but the request has not yet been fully sent
+ yet. The instance will remain in this state until the request
+ is fully sent.
+
+ - GENERATION_FAILED: There was an error while the request. The
+ request was not fully sent to the network.
+
+ - WAITING: The request was fully sent to the network. The
+ instance is now waiting for the response to be fully received.
+
+ - ABORTING: Application code has requested that the HTTP connection
+ be aborted.
+
+ - CONNECTION_LOST: The connection has been lost.
+
+ @ivar _abortDeferreds: A list of C{Deferred} instances that will fire when
+ the connection is lost.
+ """
+ _state = 'QUIESCENT'
+ _parser = None
+ _finishedRequest = None
+ _currentRequest = None
+ _transportProxy = None
+ _responseDeferred = None
+
+
+ def __init__(self, quiescentCallback=lambda c: None):
+ self._quiescentCallback = quiescentCallback
+ self._abortDeferreds = []
+
+
+ @property
+ def state(self):
+ return self._state
+
+
+ def request(self, request):
+ """
+ Issue C{request} over C{self.transport} and return a L{Deferred} which
+ will fire with a L{Response} instance or an error.
+
+ @param request: The object defining the parameters of the request to
+ issue.
+ @type request: L{Request}
+
+ @rtype: L{Deferred}
+ @return: The deferred may errback with L{RequestGenerationFailed} if
+ the request was not fully written to the transport due to a local
+ error. It may errback with L{RequestTransmissionFailed} if it was
+ not fully written to the transport due to a network error. It may
+ errback with L{ResponseFailed} if the request was sent (not
+ necessarily received) but some or all of the response was lost. It
+ may errback with L{RequestNotSent} if it is not possible to send
+ any more requests using this L{HTTP11ClientProtocol}.
+ """
+ if self._state != 'QUIESCENT':
+ return fail(RequestNotSent())
+
+ self._state = 'TRANSMITTING'
+ _requestDeferred = maybeDeferred(request.writeTo, self.transport)
+ self._finishedRequest = Deferred()
+
+ # Keep track of the Request object in case we need to call stopWriting
+ # on it.
+ self._currentRequest = request
+
+ self._transportProxy = TransportProxyProducer(self.transport)
+ self._parser = HTTPClientParser(request, self._finishResponse)
+ self._parser.makeConnection(self._transportProxy)
+ self._responseDeferred = self._parser._responseDeferred
+
+ def cbRequestWrotten(ignored):
+ if self._state == 'TRANSMITTING':
+ self._state = 'WAITING'
+ self._responseDeferred.chainDeferred(self._finishedRequest)
+
+ def ebRequestWriting(err):
+ if self._state == 'TRANSMITTING':
+ self._state = 'GENERATION_FAILED'
+ self.transport.loseConnection()
+ self._finishedRequest.errback(
+ Failure(RequestGenerationFailed([err])))
+ else:
+ log.err(err, 'Error writing request, but not in valid state '
+ 'to finalize request: %s' % self._state)
+
+ _requestDeferred.addCallbacks(cbRequestWrotten, ebRequestWriting)
+
+ return self._finishedRequest
+
+
+ def _finishResponse(self, rest):
+ """
+ Called by an L{HTTPClientParser} to indicate that it has parsed a
+ complete response.
+
+ @param rest: A C{str} giving any trailing bytes which were given to
+ the L{HTTPClientParser} which were not part of the response it
+ was parsing.
+ """
+ _finishResponse = makeStatefulDispatcher('finishResponse', _finishResponse)
+
+
+ def _finishResponse_WAITING(self, rest):
+ # Currently the rest parameter is ignored. Don't forget to use it if
+ # we ever add support for pipelining. And maybe check what trailers
+ # mean.
+ if self._state == 'WAITING':
+ self._state = 'QUIESCENT'
+ else:
+ # The server sent the entire response before we could send the
+ # whole request. That sucks. Oh well. Fire the request()
+ # Deferred with the response. But first, make sure that if the
+ # request does ever finish being written that it won't try to fire
+ # that Deferred.
+ self._state = 'TRANSMITTING_AFTER_RECEIVING_RESPONSE'
+ self._responseDeferred.chainDeferred(self._finishedRequest)
+
+ # This will happen if we're being called due to connection being lost;
+ # if so, no need to disconnect parser again, or to call
+ # _quiescentCallback.
+ if self._parser is None:
+ return
+
+ reason = ConnectionDone("synthetic!")
+ connHeaders = self._parser.connHeaders.getRawHeaders('connection', ())
+ if (('close' in connHeaders) or self._state != "QUIESCENT" or
+ not self._currentRequest.persistent):
+ self._giveUp(Failure(reason))
+ else:
+ # We call the quiescent callback first, to ensure connection gets
+ # added back to connection pool before we finish the request.
+ try:
+ self._quiescentCallback(self)
+ except:
+ # If callback throws exception, just log it and disconnect;
+ # keeping persistent connections around is an optimisation:
+ log.err()
+ self.transport.loseConnection()
+ self._disconnectParser(reason)
+
+
+ _finishResponse_TRANSMITTING = _finishResponse_WAITING
+
+
+ def _disconnectParser(self, reason):
+ """
+ If there is still a parser, call its C{connectionLost} method with the
+ given reason. If there is not, do nothing.
+
+ @type reason: L{Failure}
+ """
+ if self._parser is not None:
+ parser = self._parser
+ self._parser = None
+ self._currentRequest = None
+ self._finishedRequest = None
+ self._responseDeferred = None
+
+ # The parser is no longer allowed to do anything to the real
+ # transport. Stop proxying from the parser's transport to the real
+ # transport before telling the parser it's done so that it can't do
+ # anything.
+ self._transportProxy._stopProxying()
+ self._transportProxy = None
+ parser.connectionLost(reason)
+
+
+ def _giveUp(self, reason):
+ """
+ Lose the underlying connection and disconnect the parser with the given
+ L{Failure}.
+
+ Use this method instead of calling the transport's loseConnection
+ method directly otherwise random things will break.
+ """
+ self.transport.loseConnection()
+ self._disconnectParser(reason)
+
+
+ def dataReceived(self, bytes):
+ """
+ Handle some stuff from some place.
+ """
+ try:
+ self._parser.dataReceived(bytes)
+ except:
+ self._giveUp(Failure())
+
+
+ def connectionLost(self, reason):
+ """
+ The underlying transport went away. If appropriate, notify the parser
+ object.
+ """
+ connectionLost = makeStatefulDispatcher('connectionLost', connectionLost)
+
+
+ def _connectionLost_QUIESCENT(self, reason):
+ """
+ Nothing is currently happening. Move to the C{'CONNECTION_LOST'}
+ state but otherwise do nothing.
+ """
+ self._state = 'CONNECTION_LOST'
+
+
+ def _connectionLost_GENERATION_FAILED(self, reason):
+ """
+ The connection was in an inconsistent state. Move to the
+ C{'CONNECTION_LOST'} state but otherwise do nothing.
+ """
+ self._state = 'CONNECTION_LOST'
+
+
+ def _connectionLost_TRANSMITTING(self, reason):
+ """
+ Fail the L{Deferred} for the current request, notify the request
+ object that it does not need to continue transmitting itself, and
+ move to the C{'CONNECTION_LOST'} state.
+ """
+ self._state = 'CONNECTION_LOST'
+ self._finishedRequest.errback(
+ Failure(RequestTransmissionFailed([reason])))
+ del self._finishedRequest
+
+ # Tell the request that it should stop bothering now.
+ self._currentRequest.stopWriting()
+
+
+ def _connectionLost_TRANSMITTING_AFTER_RECEIVING_RESPONSE(self, reason):
+ """
+ Move to the C{'CONNECTION_LOST'} state.
+ """
+ self._state = 'CONNECTION_LOST'
+
+
+ def _connectionLost_WAITING(self, reason):
+ """
+ Disconnect the response parser so that it can propagate the event as
+ necessary (for example, to call an application protocol's
+ C{connectionLost} method, or to fail a request L{Deferred}) and move
+ to the C{'CONNECTION_LOST'} state.
+ """
+ self._disconnectParser(reason)
+ self._state = 'CONNECTION_LOST'
+
+
+ def _connectionLost_ABORTING(self, reason):
+ """
+ Disconnect the response parser with a L{ConnectionAborted} failure, and
+ move to the C{'CONNECTION_LOST'} state.
+ """
+ self._disconnectParser(Failure(ConnectionAborted()))
+ self._state = 'CONNECTION_LOST'
+ for d in self._abortDeferreds:
+ d.callback(None)
+ self._abortDeferreds = []
+
+
+ def abort(self):
+ """
+ Close the connection and cause all outstanding L{request} L{Deferred}s
+ to fire with an error.
+ """
+ if self._state == "CONNECTION_LOST":
+ return succeed(None)
+ self.transport.loseConnection()
+ self._state = 'ABORTING'
+ d = Deferred()
+ self._abortDeferreds.append(d)
+ return d
diff --git a/twisted/web/_stan.py b/twisted/web/_stan.py
new file mode 100644
index 0000000..004761f
--- /dev/null
+++ b/twisted/web/_stan.py
@@ -0,0 +1,325 @@
+# -*- test-case-name: twisted.web.test.test_stan -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An s-expression-like syntax for expressing xml in pure python.
+
+Stan tags allow you to build XML documents using Python.
+
+Stan is a DOM, or Document Object Model, implemented using basic Python types
+and functions called "flatteners". A flattener is a function that knows how to
+turn an object of a specific type into something that is closer to an HTML
+string. Stan differs from the W3C DOM by not being as cumbersome and heavy
+weight. Since the object model is built using simple python types such as lists,
+strings, and dictionaries, the API is simpler and constructing a DOM less
+cumbersome.
+
+@var voidElements: the names of HTML 'U{void
+ elements<http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#void-elements>}';
+ those which can't have contents and can therefore be self-closing in the
+ output.
+"""
+
+
+class slot(object):
+ """
+ Marker for markup insertion in a template.
+
+ @type name: C{str}
+ @ivar name: The name of this slot. The key which must be used in
+ L{Tag.fillSlots} to fill it.
+
+ @type children: C{list}
+ @ivar children: The L{Tag} objects included in this L{slot}'s template.
+
+ @type default: anything flattenable, or C{NoneType}
+ @ivar default: The default contents of this slot, if it is left unfilled.
+ If this is C{None}, an L{UnfilledSlot} will be raised, rather than
+ C{None} actually being used.
+
+ @type filename: C{str} or C{NoneType}
+ @ivar filename: The name of the XML file from which this tag was parsed.
+ If it was not parsed from an XML file, C{None}.
+
+ @type lineNumber: C{int} or C{NoneType}
+ @ivar lineNumber: The line number on which this tag was encountered in the
+ XML file from which it was parsed. If it was not parsed from an XML
+ file, C{None}.
+
+ @type columnNumber: C{int} or C{NoneType}
+ @ivar columnNumber: The column number at which this tag was encountered in
+ the XML file from which it was parsed. If it was not parsed from an
+ XML file, C{None}.
+ """
+
+ def __init__(self, name, default=None, filename=None, lineNumber=None,
+ columnNumber=None):
+ self.name = name
+ self.children = []
+ self.default = default
+ self.filename = filename
+ self.lineNumber = lineNumber
+ self.columnNumber = columnNumber
+
+
+ def __repr__(self):
+ return "slot(%r)" % (self.name,)
+
+
+
+class Tag(object):
+ """
+ A L{Tag} represents an XML tags with a tag name, attributes, and children.
+ A L{Tag} can be constructed using the special L{twisted.web.template.tags}
+ object, or it may be constructed directly with a tag name. L{Tag}s have a
+ special method, C{__call__}, which makes representing trees of XML natural
+ using pure python syntax.
+
+ @ivar tagName: The name of the represented element. For a tag like
+ C{<div></div>}, this would be C{"div"}.
+ @type tagName: C{str}
+
+ @ivar attributes: The attributes of the element.
+ @type attributes: C{dict} mapping C{str} to renderable objects.
+
+ @ivar children: The child L{Tag}s of this C{Tag}.
+ @type children: C{list} of renderable objects.
+
+ @ivar render: The name of the render method to use for this L{Tag}. This
+ name will be looked up at render time by the
+ L{twisted.web.template.Element} doing the rendering, via
+ L{twisted.web.template.Element.lookupRenderMethod}, to determine which
+ method to call.
+ @type render: C{str}
+
+ @type filename: C{str} or C{NoneType}
+ @ivar filename: The name of the XML file from which this tag was parsed.
+ If it was not parsed from an XML file, C{None}.
+
+ @type lineNumber: C{int} or C{NoneType}
+ @ivar lineNumber: The line number on which this tag was encountered in the
+ XML file from which it was parsed. If it was not parsed from an XML
+ file, C{None}.
+
+ @type columnNumber: C{int} or C{NoneType}
+ @ivar columnNumber: The column number at which this tag was encountered in
+ the XML file from which it was parsed. If it was not parsed from an
+ XML file, C{None}.
+
+ @type slotData: C{dict} or C{NoneType}
+ @ivar slotData: The data which can fill slots. If present, a dictionary
+ mapping slot names to renderable values. The values in this dict might
+ be anything that can be present as the child of a L{Tag}; strings,
+ lists, L{Tag}s, generators, etc.
+ """
+
+ slotData = None
+ filename = None
+ lineNumber = None
+ columnNumber = None
+
+ def __init__(self, tagName, attributes=None, children=None, render=None,
+ filename=None, lineNumber=None, columnNumber=None):
+ self.tagName = tagName
+ self.render = render
+ if attributes is None:
+ self.attributes = {}
+ else:
+ self.attributes = attributes
+ if children is None:
+ self.children = []
+ else:
+ self.children = children
+ if filename is not None:
+ self.filename = filename
+ if lineNumber is not None:
+ self.lineNumber = lineNumber
+ if columnNumber is not None:
+ self.columnNumber = columnNumber
+
+
+ def fillSlots(self, **slots):
+ """
+ Remember the slots provided at this position in the DOM.
+
+ During the rendering of children of this node, slots with names in
+ C{slots} will be rendered as their corresponding values.
+
+ @return: C{self}. This enables the idiom C{return tag.fillSlots(...)} in
+ renderers.
+ """
+ if self.slotData is None:
+ self.slotData = {}
+ self.slotData.update(slots)
+ return self
+
+
+ def __call__(self, *children, **kw):
+ """
+ Add children and change attributes on this tag.
+
+ This is implemented using __call__ because it then allows the natural
+ syntax::
+
+ table(tr1, tr2, width="100%", height="50%", border="1")
+
+ Children may be other tag instances, strings, functions, or any other
+ object which has a registered flatten.
+
+ Attributes may be 'transparent' tag instances (so that
+ C{a(href=transparent(data="foo", render=myhrefrenderer))} works),
+ strings, functions, or any other object which has a registered
+ flattener.
+
+ If the attribute is a python keyword, such as 'class', you can add an
+ underscore to the name, like 'class_'.
+
+ There is one special keyword argument, 'render', which will be used as
+ the name of the renderer and saved as the 'render' attribute of this
+ instance, rather than the DOM 'render' attribute in the attributes
+ dictionary.
+ """
+ self.children.extend(children)
+
+ for k, v in kw.iteritems():
+ if k[-1] == '_':
+ k = k[:-1]
+
+ if k == 'render':
+ self.render = v
+ else:
+ self.attributes[k] = v
+ return self
+
+
+ def _clone(self, obj, deep):
+ """
+ Clone an arbitrary object; used by L{Tag.clone}.
+
+ @param obj: an object with a clone method, a list or tuple, or something
+ which should be immutable.
+
+ @param deep: whether to continue cloning child objects; i.e. the
+ contents of lists, the sub-tags within a tag.
+
+ @return: a clone of C{obj}.
+ """
+ if hasattr(obj, 'clone'):
+ return obj.clone(deep)
+ elif isinstance(obj, (list, tuple)):
+ return [self._clone(x, deep) for x in obj]
+ else:
+ return obj
+
+
+ def clone(self, deep=True):
+ """
+ Return a clone of this tag. If deep is True, clone all of this tag's
+ children. Otherwise, just shallow copy the children list without copying
+ the children themselves.
+ """
+ if deep:
+ newchildren = [self._clone(x, True) for x in self.children]
+ else:
+ newchildren = self.children[:]
+ newattrs = self.attributes.copy()
+ for key in newattrs:
+ newattrs[key] = self._clone(newattrs[key], True)
+
+ newslotdata = None
+ if self.slotData:
+ newslotdata = self.slotData.copy()
+ for key in newslotdata:
+ newslotdata[key] = self._clone(newslotdata[key], True)
+
+ newtag = Tag(
+ self.tagName,
+ attributes=newattrs,
+ children=newchildren,
+ render=self.render,
+ filename=self.filename,
+ lineNumber=self.lineNumber,
+ columnNumber=self.columnNumber)
+ newtag.slotData = newslotdata
+
+ return newtag
+
+
+ def clear(self):
+ """
+ Clear any existing children from this tag.
+ """
+ self.children = []
+ return self
+
+
+ def __repr__(self):
+ rstr = ''
+ if self.attributes:
+ rstr += ', attributes=%r' % self.attributes
+ if self.children:
+ rstr += ', children=%r' % self.children
+ return "Tag(%r%s)" % (self.tagName, rstr)
+
+
+
+voidElements = ('img', 'br', 'hr', 'base', 'meta', 'link', 'param', 'area',
+ 'input', 'col', 'basefont', 'isindex', 'frame', 'command',
+ 'embed', 'keygen', 'source', 'track', 'wbs')
+
+
+class CDATA(object):
+ """
+ A C{<![CDATA[]]>} block from a template. Given a separate representation in
+ the DOM so that they may be round-tripped through rendering without losing
+ information.
+
+ @ivar data: The data between "C{<![CDATA[}" and "C{]]>}".
+ @type data: C{unicode}
+ """
+ def __init__(self, data):
+ self.data = data
+
+
+ def __repr__(self):
+ return 'CDATA(%r)' % (self.data,)
+
+
+
+class Comment(object):
+ """
+ A C{<!-- -->} comment from a template. Given a separate representation in
+ the DOM so that they may be round-tripped through rendering without losing
+ information.
+
+ @ivar data: The data between "C{<!--}" and "C{-->}".
+ @type data: C{unicode}
+ """
+
+ def __init__(self, data):
+ self.data = data
+
+
+ def __repr__(self):
+ return 'Comment(%r)' % (self.data,)
+
+
+
+class CharRef(object):
+ """
+ A numeric character reference. Given a separate representation in the DOM
+ so that non-ASCII characters may be output as pure ASCII.
+
+ @ivar ordinal: The ordinal value of the unicode character to which this is
+ object refers.
+ @type ordinal: C{int}
+
+ @since: 12.0
+ """
+ def __init__(self, ordinal):
+ self.ordinal = ordinal
+
+
+ def __repr__(self):
+ return "CharRef(%d)" % (self.ordinal,)
diff --git a/twisted/web/_version.py b/twisted/web/_version.py
new file mode 100644
index 0000000..6df081e
--- /dev/null
+++ b/twisted/web/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.web', 12, 1, 0)
diff --git a/twisted/web/client.py b/twisted/web/client.py
new file mode 100644
index 0000000..7e9a488
--- /dev/null
+++ b/twisted/web/client.py
@@ -0,0 +1,1600 @@
+# -*- test-case-name: twisted.web.test.test_webclient -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HTTP client.
+"""
+
+import os, types
+from urlparse import urlunparse
+from urllib import splithost, splittype
+import zlib
+
+from zope.interface import implements
+
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.web import http
+from twisted.internet import defer, protocol, task, reactor
+from twisted.internet.interfaces import IProtocol
+from twisted.internet.endpoints import TCP4ClientEndpoint, SSL4ClientEndpoint
+from twisted.python import failure
+from twisted.python.util import InsensitiveDict
+from twisted.python.components import proxyForInterface
+from twisted.web import error
+from twisted.web.iweb import UNKNOWN_LENGTH, IBodyProducer, IResponse
+from twisted.web.http_headers import Headers
+from twisted.python.compat import set
+
+
+class PartialDownloadError(error.Error):
+ """
+ Page was only partially downloaded, we got disconnected in middle.
+
+ @ivar response: All of the response body which was downloaded.
+ """
+
+
+class HTTPPageGetter(http.HTTPClient):
+ """
+ Gets a resource via HTTP, then quits.
+
+ Typically used with L{HTTPClientFactory}. Note that this class does not, by
+ itself, do anything with the response. If you want to download a resource
+ into a file, use L{HTTPPageDownloader} instead.
+
+ @ivar _completelyDone: A boolean indicating whether any further requests are
+ necessary after this one completes in order to provide a result to
+ C{self.factory.deferred}. If it is C{False}, then a redirect is going
+ to be followed. Otherwise, this protocol's connection is the last one
+ before firing the result Deferred. This is used to make sure the result
+ Deferred is only fired after the connection is cleaned up.
+ """
+
+ quietLoss = 0
+ followRedirect = True
+ failed = 0
+
+ _completelyDone = True
+
+ _specialHeaders = set(('host', 'user-agent', 'cookie', 'content-length'))
+
+ def connectionMade(self):
+ method = getattr(self.factory, 'method', 'GET')
+ self.sendCommand(method, self.factory.path)
+ if self.factory.scheme == 'http' and self.factory.port != 80:
+ host = '%s:%s' % (self.factory.host, self.factory.port)
+ elif self.factory.scheme == 'https' and self.factory.port != 443:
+ host = '%s:%s' % (self.factory.host, self.factory.port)
+ else:
+ host = self.factory.host
+ self.sendHeader('Host', self.factory.headers.get("host", host))
+ self.sendHeader('User-Agent', self.factory.agent)
+ data = getattr(self.factory, 'postdata', None)
+ if data is not None:
+ self.sendHeader("Content-Length", str(len(data)))
+
+ cookieData = []
+ for (key, value) in self.factory.headers.items():
+ if key.lower() not in self._specialHeaders:
+ # we calculated it on our own
+ self.sendHeader(key, value)
+ if key.lower() == 'cookie':
+ cookieData.append(value)
+ for cookie, cookval in self.factory.cookies.items():
+ cookieData.append('%s=%s' % (cookie, cookval))
+ if cookieData:
+ self.sendHeader('Cookie', '; '.join(cookieData))
+ self.endHeaders()
+ self.headers = {}
+
+ if data is not None:
+ self.transport.write(data)
+
+ def handleHeader(self, key, value):
+ """
+ Called every time a header is received. Stores the header information
+ as key-value pairs in the C{headers} attribute.
+
+ @type key: C{str}
+ @param key: An HTTP header field name.
+
+ @type value: C{str}
+ @param value: An HTTP header field value.
+ """
+ key = key.lower()
+ l = self.headers.setdefault(key, [])
+ l.append(value)
+
+ def handleStatus(self, version, status, message):
+ self.version, self.status, self.message = version, status, message
+ self.factory.gotStatus(version, status, message)
+
+ def handleEndHeaders(self):
+ self.factory.gotHeaders(self.headers)
+ m = getattr(self, 'handleStatus_'+self.status, self.handleStatusDefault)
+ m()
+
+ def handleStatus_200(self):
+ pass
+
+ handleStatus_201 = lambda self: self.handleStatus_200()
+ handleStatus_202 = lambda self: self.handleStatus_200()
+
+ def handleStatusDefault(self):
+ self.failed = 1
+
+ def handleStatus_301(self):
+ l = self.headers.get('location')
+ if not l:
+ self.handleStatusDefault()
+ return
+ url = l[0]
+ if self.followRedirect:
+ scheme, host, port, path = \
+ _parse(url, defaultPort=self.transport.getPeer().port)
+
+ self.factory._redirectCount += 1
+ if self.factory._redirectCount >= self.factory.redirectLimit:
+ err = error.InfiniteRedirection(
+ self.status,
+ 'Infinite redirection detected',
+ location=url)
+ self.factory.noPage(failure.Failure(err))
+ self.quietLoss = True
+ self.transport.loseConnection()
+ return
+
+ self._completelyDone = False
+ self.factory.setURL(url)
+
+ if self.factory.scheme == 'https':
+ from twisted.internet import ssl
+ contextFactory = ssl.ClientContextFactory()
+ reactor.connectSSL(self.factory.host, self.factory.port,
+ self.factory, contextFactory)
+ else:
+ reactor.connectTCP(self.factory.host, self.factory.port,
+ self.factory)
+ else:
+ self.handleStatusDefault()
+ self.factory.noPage(
+ failure.Failure(
+ error.PageRedirect(
+ self.status, self.message, location = url)))
+ self.quietLoss = True
+ self.transport.loseConnection()
+
+ def handleStatus_302(self):
+ if self.afterFoundGet:
+ self.handleStatus_303()
+ else:
+ self.handleStatus_301()
+
+
+ def handleStatus_303(self):
+ self.factory.method = 'GET'
+ self.handleStatus_301()
+
+
+ def connectionLost(self, reason):
+ """
+ When the connection used to issue the HTTP request is closed, notify the
+ factory if we have not already, so it can produce a result.
+ """
+ if not self.quietLoss:
+ http.HTTPClient.connectionLost(self, reason)
+ self.factory.noPage(reason)
+ if self._completelyDone:
+ # Only if we think we're completely done do we tell the factory that
+ # we're "disconnected". This way when we're following redirects,
+ # only the last protocol used will fire the _disconnectedDeferred.
+ self.factory._disconnectedDeferred.callback(None)
+
+
+ def handleResponse(self, response):
+ if self.quietLoss:
+ return
+ if self.failed:
+ self.factory.noPage(
+ failure.Failure(
+ error.Error(
+ self.status, self.message, response)))
+ if self.factory.method == 'HEAD':
+ # Callback with empty string, since there is never a response
+ # body for HEAD requests.
+ self.factory.page('')
+ elif self.length != None and self.length != 0:
+ self.factory.noPage(failure.Failure(
+ PartialDownloadError(self.status, self.message, response)))
+ else:
+ self.factory.page(response)
+ # server might be stupid and not close connection. admittedly
+ # the fact we do only one request per connection is also
+ # stupid...
+ self.transport.loseConnection()
+
+ def timeout(self):
+ self.quietLoss = True
+ self.transport.loseConnection()
+ self.factory.noPage(defer.TimeoutError("Getting %s took longer than %s seconds." % (self.factory.url, self.factory.timeout)))
+
+
+class HTTPPageDownloader(HTTPPageGetter):
+
+ transmittingPage = 0
+
+ def handleStatus_200(self, partialContent=0):
+ HTTPPageGetter.handleStatus_200(self)
+ self.transmittingPage = 1
+ self.factory.pageStart(partialContent)
+
+ def handleStatus_206(self):
+ self.handleStatus_200(partialContent=1)
+
+ def handleResponsePart(self, data):
+ if self.transmittingPage:
+ self.factory.pagePart(data)
+
+ def handleResponseEnd(self):
+ if self.length:
+ self.transmittingPage = 0
+ self.factory.noPage(
+ failure.Failure(
+ PartialDownloadError(self.status)))
+ if self.transmittingPage:
+ self.factory.pageEnd()
+ self.transmittingPage = 0
+ if self.failed:
+ self.factory.noPage(
+ failure.Failure(
+ error.Error(
+ self.status, self.message, None)))
+ self.transport.loseConnection()
+
+
+class HTTPClientFactory(protocol.ClientFactory):
+ """Download a given URL.
+
+ @type deferred: Deferred
+ @ivar deferred: A Deferred that will fire when the content has
+ been retrieved. Once this is fired, the ivars `status', `version',
+ and `message' will be set.
+
+ @type status: str
+ @ivar status: The status of the response.
+
+ @type version: str
+ @ivar version: The version of the response.
+
+ @type message: str
+ @ivar message: The text message returned with the status.
+
+ @type response_headers: dict
+ @ivar response_headers: The headers that were specified in the
+ response from the server.
+
+ @type method: str
+ @ivar method: The HTTP method to use in the request. This should be one of
+ OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, or CONNECT (case
+ matters). Other values may be specified if the server being contacted
+ supports them.
+
+ @type redirectLimit: int
+ @ivar redirectLimit: The maximum number of HTTP redirects that can occur
+ before it is assumed that the redirection is endless.
+
+ @type afterFoundGet: C{bool}
+ @ivar afterFoundGet: Deviate from the HTTP 1.1 RFC by handling redirects
+ the same way as most web browsers; if the request method is POST and a
+ 302 status is encountered, the redirect is followed with a GET method
+
+ @type _redirectCount: int
+ @ivar _redirectCount: The current number of HTTP redirects encountered.
+
+ @ivar _disconnectedDeferred: A L{Deferred} which only fires after the last
+ connection associated with the request (redirects may cause multiple
+ connections to be required) has closed. The result Deferred will only
+ fire after this Deferred, so that callers can be assured that there are
+ no more event sources in the reactor once they get the result.
+ """
+
+ protocol = HTTPPageGetter
+
+ url = None
+ scheme = None
+ host = ''
+ port = None
+ path = None
+
+ def __init__(self, url, method='GET', postdata=None, headers=None,
+ agent="Twisted PageGetter", timeout=0, cookies=None,
+ followRedirect=True, redirectLimit=20,
+ afterFoundGet=False):
+ self.followRedirect = followRedirect
+ self.redirectLimit = redirectLimit
+ self._redirectCount = 0
+ self.timeout = timeout
+ self.agent = agent
+ self.afterFoundGet = afterFoundGet
+ if cookies is None:
+ cookies = {}
+ self.cookies = cookies
+ if headers is not None:
+ self.headers = InsensitiveDict(headers)
+ else:
+ self.headers = InsensitiveDict()
+ if postdata is not None:
+ self.headers.setdefault('Content-Length', len(postdata))
+ # just in case a broken http/1.1 decides to keep connection alive
+ self.headers.setdefault("connection", "close")
+ self.postdata = postdata
+ self.method = method
+
+ self.setURL(url)
+
+ self.waiting = 1
+ self._disconnectedDeferred = defer.Deferred()
+ self.deferred = defer.Deferred()
+ # Make sure the first callback on the result Deferred pauses the
+ # callback chain until the request connection is closed.
+ self.deferred.addBoth(self._waitForDisconnect)
+ self.response_headers = None
+
+
+ def _waitForDisconnect(self, passthrough):
+ """
+ Chain onto the _disconnectedDeferred, preserving C{passthrough}, so that
+ the result is only available after the associated connection has been
+ closed.
+ """
+ self._disconnectedDeferred.addCallback(lambda ignored: passthrough)
+ return self._disconnectedDeferred
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self.url)
+
+ def setURL(self, url):
+ self.url = url
+ scheme, host, port, path = _parse(url)
+ if scheme and host:
+ self.scheme = scheme
+ self.host = host
+ self.port = port
+ self.path = path
+
+ def buildProtocol(self, addr):
+ p = protocol.ClientFactory.buildProtocol(self, addr)
+ p.followRedirect = self.followRedirect
+ p.afterFoundGet = self.afterFoundGet
+ if self.timeout:
+ timeoutCall = reactor.callLater(self.timeout, p.timeout)
+ self.deferred.addBoth(self._cancelTimeout, timeoutCall)
+ return p
+
+ def _cancelTimeout(self, result, timeoutCall):
+ if timeoutCall.active():
+ timeoutCall.cancel()
+ return result
+
+ def gotHeaders(self, headers):
+ self.response_headers = headers
+ if headers.has_key('set-cookie'):
+ for cookie in headers['set-cookie']:
+ cookparts = cookie.split(';')
+ cook = cookparts[0]
+ cook.lstrip()
+ k, v = cook.split('=', 1)
+ self.cookies[k.lstrip()] = v.lstrip()
+
+ def gotStatus(self, version, status, message):
+ self.version, self.status, self.message = version, status, message
+
+ def page(self, page):
+ if self.waiting:
+ self.waiting = 0
+ self.deferred.callback(page)
+
+ def noPage(self, reason):
+ if self.waiting:
+ self.waiting = 0
+ self.deferred.errback(reason)
+
+ def clientConnectionFailed(self, _, reason):
+ """
+ When a connection attempt fails, the request cannot be issued. If no
+ result has yet been provided to the result Deferred, provide the
+ connection failure reason as an error result.
+ """
+ if self.waiting:
+ self.waiting = 0
+ # If the connection attempt failed, there is nothing more to
+ # disconnect, so just fire that Deferred now.
+ self._disconnectedDeferred.callback(None)
+ self.deferred.errback(reason)
+
+
+
+class HTTPDownloader(HTTPClientFactory):
+ """Download to a file."""
+
+ protocol = HTTPPageDownloader
+ value = None
+
+ def __init__(self, url, fileOrName,
+ method='GET', postdata=None, headers=None,
+ agent="Twisted client", supportPartial=0,
+ timeout=0, cookies=None, followRedirect=1,
+ redirectLimit=20, afterFoundGet=False):
+ self.requestedPartial = 0
+ if isinstance(fileOrName, types.StringTypes):
+ self.fileName = fileOrName
+ self.file = None
+ if supportPartial and os.path.exists(self.fileName):
+ fileLength = os.path.getsize(self.fileName)
+ if fileLength:
+ self.requestedPartial = fileLength
+ if headers == None:
+ headers = {}
+ headers["range"] = "bytes=%d-" % fileLength
+ else:
+ self.file = fileOrName
+ HTTPClientFactory.__init__(
+ self, url, method=method, postdata=postdata, headers=headers,
+ agent=agent, timeout=timeout, cookies=cookies,
+ followRedirect=followRedirect, redirectLimit=redirectLimit,
+ afterFoundGet=afterFoundGet)
+
+
+ def gotHeaders(self, headers):
+ HTTPClientFactory.gotHeaders(self, headers)
+ if self.requestedPartial:
+ contentRange = headers.get("content-range", None)
+ if not contentRange:
+ # server doesn't support partial requests, oh well
+ self.requestedPartial = 0
+ return
+ start, end, realLength = http.parseContentRange(contentRange[0])
+ if start != self.requestedPartial:
+ # server is acting wierdly
+ self.requestedPartial = 0
+
+
+ def openFile(self, partialContent):
+ if partialContent:
+ file = open(self.fileName, 'rb+')
+ file.seek(0, 2)
+ else:
+ file = open(self.fileName, 'wb')
+ return file
+
+ def pageStart(self, partialContent):
+ """Called on page download start.
+
+ @param partialContent: tells us if the download is partial download we requested.
+ """
+ if partialContent and not self.requestedPartial:
+ raise ValueError, "we shouldn't get partial content response if we didn't want it!"
+ if self.waiting:
+ try:
+ if not self.file:
+ self.file = self.openFile(partialContent)
+ except IOError:
+ #raise
+ self.deferred.errback(failure.Failure())
+
+ def pagePart(self, data):
+ if not self.file:
+ return
+ try:
+ self.file.write(data)
+ except IOError:
+ #raise
+ self.file = None
+ self.deferred.errback(failure.Failure())
+
+
+ def noPage(self, reason):
+ """
+ Close the storage file and errback the waiting L{Deferred} with the
+ given reason.
+ """
+ if self.waiting:
+ self.waiting = 0
+ if self.file:
+ try:
+ self.file.close()
+ except:
+ log.err(None, "Error closing HTTPDownloader file")
+ self.deferred.errback(reason)
+
+
+ def pageEnd(self):
+ self.waiting = 0
+ if not self.file:
+ return
+ try:
+ self.file.close()
+ except IOError:
+ self.deferred.errback(failure.Failure())
+ return
+ self.deferred.callback(self.value)
+
+
+
+class _URL(tuple):
+ """
+ A parsed URL.
+
+ At some point this should be replaced with a better URL implementation.
+ """
+ def __new__(self, scheme, host, port, path):
+ return tuple.__new__(_URL, (scheme, host, port, path))
+
+
+ def __init__(self, scheme, host, port, path):
+ self.scheme = scheme
+ self.host = host
+ self.port = port
+ self.path = path
+
+
+def _parse(url, defaultPort=None):
+ """
+ Split the given URL into the scheme, host, port, and path.
+
+ @type url: C{str}
+ @param url: An URL to parse.
+
+ @type defaultPort: C{int} or C{None}
+ @param defaultPort: An alternate value to use as the port if the URL does
+ not include one.
+
+ @return: A four-tuple of the scheme, host, port, and path of the URL. All
+ of these are C{str} instances except for port, which is an C{int}.
+ """
+ url = url.strip()
+ parsed = http.urlparse(url)
+ scheme = parsed[0]
+ path = urlunparse(('', '') + parsed[2:])
+
+ if defaultPort is None:
+ if scheme == 'https':
+ defaultPort = 443
+ else:
+ defaultPort = 80
+
+ host, port = parsed[1], defaultPort
+ if ':' in host:
+ host, port = host.split(':')
+ try:
+ port = int(port)
+ except ValueError:
+ port = defaultPort
+
+ if path == '':
+ path = '/'
+
+ return _URL(scheme, host, port, path)
+
+
+def _makeGetterFactory(url, factoryFactory, contextFactory=None,
+ *args, **kwargs):
+ """
+ Create and connect an HTTP page getting factory.
+
+ Any additional positional or keyword arguments are used when calling
+ C{factoryFactory}.
+
+ @param factoryFactory: Factory factory that is called with C{url}, C{args}
+ and C{kwargs} to produce the getter
+
+ @param contextFactory: Context factory to use when creating a secure
+ connection, defaulting to C{None}
+
+ @return: The factory created by C{factoryFactory}
+ """
+ scheme, host, port, path = _parse(url)
+ factory = factoryFactory(url, *args, **kwargs)
+ if scheme == 'https':
+ from twisted.internet import ssl
+ if contextFactory is None:
+ contextFactory = ssl.ClientContextFactory()
+ reactor.connectSSL(host, port, factory, contextFactory)
+ else:
+ reactor.connectTCP(host, port, factory)
+ return factory
+
+
+def getPage(url, contextFactory=None, *args, **kwargs):
+ """
+ Download a web page as a string.
+
+ Download a page. Return a deferred, which will callback with a
+ page (as a string) or errback with a description of the error.
+
+ See L{HTTPClientFactory} to see what extra arguments can be passed.
+ """
+ return _makeGetterFactory(
+ url,
+ HTTPClientFactory,
+ contextFactory=contextFactory,
+ *args, **kwargs).deferred
+
+
+def downloadPage(url, file, contextFactory=None, *args, **kwargs):
+ """
+ Download a web page to a file.
+
+ @param file: path to file on filesystem, or file-like object.
+
+ See HTTPDownloader to see what extra args can be passed.
+ """
+ factoryFactory = lambda url, *a, **kw: HTTPDownloader(url, file, *a, **kw)
+ return _makeGetterFactory(
+ url,
+ factoryFactory,
+ contextFactory=contextFactory,
+ *args, **kwargs).deferred
+
+
+# The code which follows is based on the new HTTP client implementation. It
+# should be significantly better than anything above, though it is not yet
+# feature equivalent.
+
+from twisted.web.error import SchemeNotSupported
+from twisted.web._newclient import Request, Response, HTTP11ClientProtocol
+from twisted.web._newclient import ResponseDone, ResponseFailed
+from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed
+from twisted.web._newclient import ResponseNeverReceived
+
+try:
+ from twisted.internet.ssl import ClientContextFactory
+except ImportError:
+ class WebClientContextFactory(object):
+ """
+ A web context factory which doesn't work because the necessary SSL
+ support is missing.
+ """
+ def getContext(self, hostname, port):
+ raise NotImplementedError("SSL support unavailable")
+else:
+ class WebClientContextFactory(ClientContextFactory):
+ """
+ A web context factory which ignores the hostname and port and does no
+ certificate verification.
+ """
+ def getContext(self, hostname, port):
+ return ClientContextFactory.getContext(self)
+
+
+
+class _WebToNormalContextFactory(object):
+ """
+ Adapt a web context factory to a normal context factory.
+
+ @ivar _webContext: A web context factory which accepts a hostname and port
+ number to its C{getContext} method.
+
+ @ivar _hostname: The hostname which will be passed to
+ C{_webContext.getContext}.
+
+ @ivar _port: The port number which will be passed to
+ C{_webContext.getContext}.
+ """
+ def __init__(self, webContext, hostname, port):
+ self._webContext = webContext
+ self._hostname = hostname
+ self._port = port
+
+
+ def getContext(self):
+ """
+ Called the wrapped web context factory's C{getContext} method with a
+ hostname and port number and return the resulting context object.
+ """
+ return self._webContext.getContext(self._hostname, self._port)
+
+
+
+class FileBodyProducer(object):
+ """
+ L{FileBodyProducer} produces bytes from an input file object incrementally
+ and writes them to a consumer.
+
+ Since file-like objects cannot be read from in an event-driven manner,
+ L{FileBodyProducer} uses a L{Cooperator} instance to schedule reads from
+ the file. This process is also paused and resumed based on notifications
+ from the L{IConsumer} provider being written to.
+
+ The file is closed after it has been read, or if the producer is stopped
+ early.
+
+ @ivar _inputFile: Any file-like object, bytes read from which will be
+ written to a consumer.
+
+ @ivar _cooperate: A method like L{Cooperator.cooperate} which is used to
+ schedule all reads.
+
+ @ivar _readSize: The number of bytes to read from C{_inputFile} at a time.
+ """
+ implements(IBodyProducer)
+
+ # Python 2.4 doesn't have these symbolic constants
+ _SEEK_SET = getattr(os, 'SEEK_SET', 0)
+ _SEEK_END = getattr(os, 'SEEK_END', 2)
+
+ def __init__(self, inputFile, cooperator=task, readSize=2 ** 16):
+ self._inputFile = inputFile
+ self._cooperate = cooperator.cooperate
+ self._readSize = readSize
+ self.length = self._determineLength(inputFile)
+
+
+ def _determineLength(self, fObj):
+ """
+ Determine how many bytes can be read out of C{fObj} (assuming it is not
+ modified from this point on). If the determination cannot be made,
+ return C{UNKNOWN_LENGTH}.
+ """
+ try:
+ seek = fObj.seek
+ tell = fObj.tell
+ except AttributeError:
+ return UNKNOWN_LENGTH
+ originalPosition = tell()
+ seek(0, self._SEEK_END)
+ end = tell()
+ seek(originalPosition, self._SEEK_SET)
+ return end - originalPosition
+
+
+ def stopProducing(self):
+ """
+ Permanently stop writing bytes from the file to the consumer by
+ stopping the underlying L{CooperativeTask}.
+ """
+ self._inputFile.close()
+ self._task.stop()
+
+
+ def startProducing(self, consumer):
+ """
+ Start a cooperative task which will read bytes from the input file and
+ write them to C{consumer}. Return a L{Deferred} which fires after all
+ bytes have been written.
+
+ @param consumer: Any L{IConsumer} provider
+ """
+ self._task = self._cooperate(self._writeloop(consumer))
+ d = self._task.whenDone()
+ def maybeStopped(reason):
+ # IBodyProducer.startProducing's Deferred isn't support to fire if
+ # stopProducing is called.
+ reason.trap(task.TaskStopped)
+ return defer.Deferred()
+ d.addCallbacks(lambda ignored: None, maybeStopped)
+ return d
+
+
+ def _writeloop(self, consumer):
+ """
+ Return an iterator which reads one chunk of bytes from the input file
+ and writes them to the consumer for each time it is iterated.
+ """
+ while True:
+ bytes = self._inputFile.read(self._readSize)
+ if not bytes:
+ self._inputFile.close()
+ break
+ consumer.write(bytes)
+ yield None
+
+
+ def pauseProducing(self):
+ """
+ Temporarily suspend copying bytes from the input file to the consumer
+ by pausing the L{CooperativeTask} which drives that activity.
+ """
+ self._task.pause()
+
+
+ def resumeProducing(self):
+ """
+ Undo the effects of a previous C{pauseProducing} and resume copying
+ bytes to the consumer by resuming the L{CooperativeTask} which drives
+ the write activity.
+ """
+ self._task.resume()
+
+
+
+class _HTTP11ClientFactory(protocol.Factory):
+ """
+ A factory for L{HTTP11ClientProtocol}, used by L{HTTPConnectionPool}.
+
+ @ivar _quiescentCallback: The quiescent callback to be passed to protocol
+ instances, used to return them to the connection pool.
+
+ @since: 11.1
+ """
+ def __init__(self, quiescentCallback):
+ self._quiescentCallback = quiescentCallback
+
+
+ def buildProtocol(self, addr):
+ return HTTP11ClientProtocol(self._quiescentCallback)
+
+
+
+class _RetryingHTTP11ClientProtocol(object):
+ """
+ A wrapper for L{HTTP11ClientProtocol} that automatically retries requests.
+
+ @ivar _clientProtocol: The underlying L{HTTP11ClientProtocol}.
+
+ @ivar _newConnection: A callable that creates a new connection for a
+ retry.
+ """
+
+ def __init__(self, clientProtocol, newConnection):
+ self._clientProtocol = clientProtocol
+ self._newConnection = newConnection
+
+
+ def _shouldRetry(self, method, exception, bodyProducer):
+ """
+ Indicate whether request should be retried.
+
+ Only returns C{True} if method is idempotent, no response was
+ received, and no body was sent. The latter requirement may be relaxed
+ in the future, and PUT added to approved method list.
+ """
+ if method not in ("GET", "HEAD", "OPTIONS", "DELETE", "TRACE"):
+ return False
+ if not isinstance(exception, (RequestNotSent, RequestTransmissionFailed,
+ ResponseNeverReceived)):
+ return False
+ if bodyProducer is not None:
+ return False
+ return True
+
+
+ def request(self, request):
+ """
+ Do a request, and retry once (with a new connection) it it fails in
+ a retryable manner.
+
+ @param request: A L{Request} instance that will be requested using the
+ wrapped protocol.
+ """
+ d = self._clientProtocol.request(request)
+
+ def failed(reason):
+ if self._shouldRetry(request.method, reason.value,
+ request.bodyProducer):
+ return self._newConnection().addCallback(
+ lambda connection: connection.request(request))
+ else:
+ return reason
+ d.addErrback(failed)
+ return d
+
+
+
+class HTTPConnectionPool(object):
+ """
+ A pool of persistent HTTP connections.
+
+ Features:
+ - Cached connections will eventually time out.
+ - Limits on maximum number of persistent connections.
+
+ Connections are stored using keys, which should be chosen such that any
+ connections stored under a given key can be used interchangeably.
+
+ Failed requests done using previously cached connections will be retried
+ once if they use an idempotent method (e.g. GET), in case the HTTP server
+ timed them out.
+
+ @ivar persistent: Boolean indicating whether connections should be
+ persistent. Connections are persistent by default.
+
+ @ivar maxPersistentPerHost: The maximum number of cached persistent
+ connections for a C{host:port} destination.
+ @type maxPersistentPerHost: C{int}
+
+ @ivar cachedConnectionTimeout: Number of seconds a cached persistent
+ connection will stay open before disconnecting.
+
+ @ivar retryAutomatically: C{boolean} indicating whether idempotent
+ requests should be retried once if no response was received.
+
+ @ivar _factory: The factory used to connect to the proxy.
+
+ @ivar _connections: Map (scheme, host, port) to lists of
+ L{HTTP11ClientProtocol} instances.
+
+ @ivar _timeouts: Map L{HTTP11ClientProtocol} instances to a
+ C{IDelayedCall} instance of their timeout.
+
+ @since: 12.1
+ """
+
+ _factory = _HTTP11ClientFactory
+ maxPersistentPerHost = 2
+ cachedConnectionTimeout = 240
+ retryAutomatically = True
+
+ def __init__(self, reactor, persistent=True):
+ self._reactor = reactor
+ self.persistent = persistent
+ self._connections = {}
+ self._timeouts = {}
+
+
+ def getConnection(self, key, endpoint):
+ """
+ Retrieve a connection, either new or cached, to be used for a HTTP
+ request.
+
+ If a cached connection is returned, it will not be used for other
+ requests until it is put back (which will happen automatically), since
+ we do not support pipelined requests. If no cached connection is
+ available, the passed in endpoint is used to create the connection.
+
+ If the connection doesn't disconnect at the end of its request, it
+ will be returned to this pool automatically. As such, only a single
+ request should be sent using the returned connection.
+
+ @param key: A unique key identifying connections that can be used
+ interchangeably.
+
+ @param endpoint: An endpoint that can be used to open a new connection
+ if no cached connection is available.
+
+ @return: A C{Deferred} that will fire with a L{HTTP11ClientProtocol}
+ (or a wrapper) that can be used to send a single HTTP request.
+ """
+ # Try to get cached version:
+ connections = self._connections.get(key)
+ while connections:
+ connection = connections.pop(0)
+ # Cancel timeout:
+ self._timeouts[connection].cancel()
+ del self._timeouts[connection]
+ if connection.state == "QUIESCENT":
+ if self.retryAutomatically:
+ newConnection = lambda: self._newConnection(key, endpoint)
+ connection = _RetryingHTTP11ClientProtocol(
+ connection, newConnection)
+ return defer.succeed(connection)
+
+ return self._newConnection(key, endpoint)
+
+
+ def _newConnection(self, key, endpoint):
+ """
+ Create a new connection.
+
+ This implements the new connection code path for L{getConnection}.
+ """
+ def quiescentCallback(protocol):
+ self._putConnection(key, protocol)
+ factory = self._factory(quiescentCallback)
+ return endpoint.connect(factory)
+
+
+ def _removeConnection(self, key, connection):
+ """
+ Remove a connection from the cache and disconnect it.
+ """
+ connection.transport.loseConnection()
+ self._connections[key].remove(connection)
+ del self._timeouts[connection]
+
+
+ def _putConnection(self, key, connection):
+ """
+ Return a persistent connection to the pool. This will be called by
+ L{HTTP11ClientProtocol} when the connection becomes quiescent.
+ """
+ if connection.state != "QUIESCENT":
+ # Log with traceback for debugging purposes:
+ try:
+ raise RuntimeError(
+ "BUG: Non-quiescent protocol added to connection pool.")
+ except:
+ log.err()
+ return
+ connections = self._connections.setdefault(key, [])
+ if len(connections) == self.maxPersistentPerHost:
+ dropped = connections.pop(0)
+ dropped.transport.loseConnection()
+ self._timeouts[dropped].cancel()
+ del self._timeouts[dropped]
+ connections.append(connection)
+ cid = self._reactor.callLater(self.cachedConnectionTimeout,
+ self._removeConnection,
+ key, connection)
+ self._timeouts[connection] = cid
+
+
+ def closeCachedConnections(self):
+ """
+ Close all persistent connections and remove them from the pool.
+
+ @return: L{defer.Deferred} that fires when all connections have been
+ closed.
+ """
+ results = []
+ for protocols in self._connections.itervalues():
+ for p in protocols:
+ results.append(p.abort())
+ self._connections = {}
+ for dc in self._timeouts.values():
+ dc.cancel()
+ self._timeouts = {}
+ return defer.gatherResults(results).addCallback(lambda ign: None)
+
+
+
+class _AgentBase(object):
+ """
+ Base class offering common facilities for L{Agent}-type classes.
+
+ @ivar _reactor: The C{IReactorTime} implementation which will be used by
+ the pool, and perhaps by subclasses as well.
+
+ @ivar _pool: The L{HTTPConnectionPool} used to manage HTTP connections.
+ """
+
+ def __init__(self, reactor, pool):
+ if pool is None:
+ pool = HTTPConnectionPool(reactor, False)
+ self._reactor = reactor
+ self._pool = pool
+
+
+ def _computeHostValue(self, scheme, host, port):
+ """
+ Compute the string to use for the value of the I{Host} header, based on
+ the given scheme, host name, and port number.
+ """
+ if (scheme, port) in (('http', 80), ('https', 443)):
+ return host
+ return '%s:%d' % (host, port)
+
+
+ def _requestWithEndpoint(self, key, endpoint, method, parsedURI,
+ headers, bodyProducer, requestPath):
+ """
+ Issue a new request, given the endpoint and the path sent as part of
+ the request.
+ """
+ # Create minimal headers, if necessary:
+ if headers is None:
+ headers = Headers()
+ if not headers.hasHeader('host'):
+ headers = headers.copy()
+ headers.addRawHeader(
+ 'host', self._computeHostValue(parsedURI.scheme, parsedURI.host,
+ parsedURI.port))
+
+ d = self._pool.getConnection(key, endpoint)
+ def cbConnected(proto):
+ return proto.request(
+ Request(method, requestPath, headers, bodyProducer,
+ persistent=self._pool.persistent))
+ d.addCallback(cbConnected)
+ return d
+
+
+
+class Agent(_AgentBase):
+ """
+ L{Agent} is a very basic HTTP client. It supports I{HTTP} and I{HTTPS}
+ scheme URIs (but performs no certificate checking by default).
+
+ @param pool: A L{HTTPConnectionPool} instance, or C{None}, in which case a
+ non-persistent L{HTTPConnectionPool} instance will be created.
+
+ @ivar _contextFactory: A web context factory which will be used to create
+ SSL context objects for any SSL connections the agent needs to make.
+
+ @ivar _connectTimeout: If not C{None}, the timeout passed to C{connectTCP}
+ or C{connectSSL} for specifying the connection timeout.
+
+ @ivar _bindAddress: If not C{None}, the address passed to C{connectTCP} or
+ C{connectSSL} for specifying the local address to bind to.
+
+ @since: 9.0
+ """
+
+ def __init__(self, reactor, contextFactory=WebClientContextFactory(),
+ connectTimeout=None, bindAddress=None,
+ pool=None):
+ _AgentBase.__init__(self, reactor, pool)
+ self._contextFactory = contextFactory
+ self._connectTimeout = connectTimeout
+ self._bindAddress = bindAddress
+
+
+ def _wrapContextFactory(self, host, port):
+ """
+ Create and return a normal context factory wrapped around
+ C{self._contextFactory} in such a way that C{self._contextFactory} will
+ have the host and port information passed to it.
+
+ @param host: A C{str} giving the hostname which will be connected to in
+ order to issue a request.
+
+ @param port: An C{int} giving the port number the connection will be
+ on.
+
+ @return: A context factory suitable to be passed to
+ C{reactor.connectSSL}.
+ """
+ return _WebToNormalContextFactory(self._contextFactory, host, port)
+
+
+ def _getEndpoint(self, scheme, host, port):
+ """
+ Get an endpoint for the given host and port, using a transport
+ selected based on scheme.
+
+ @param scheme: A string like C{'http'} or C{'https'} (the only two
+ supported values) to use to determine how to establish the
+ connection.
+
+ @param host: A C{str} giving the hostname which will be connected to in
+ order to issue a request.
+
+ @param port: An C{int} giving the port number the connection will be
+ on.
+
+ @return: An endpoint which can be used to connect to given address.
+ """
+ kwargs = {}
+ if self._connectTimeout is not None:
+ kwargs['timeout'] = self._connectTimeout
+ kwargs['bindAddress'] = self._bindAddress
+ if scheme == 'http':
+ return TCP4ClientEndpoint(self._reactor, host, port, **kwargs)
+ elif scheme == 'https':
+ return SSL4ClientEndpoint(self._reactor, host, port,
+ self._wrapContextFactory(host, port),
+ **kwargs)
+ else:
+ raise SchemeNotSupported("Unsupported scheme: %r" % (scheme,))
+
+
+ def request(self, method, uri, headers=None, bodyProducer=None):
+ """
+ Issue a new request.
+
+ @param method: The request method to send.
+ @type method: C{str}
+
+ @param uri: The request URI send.
+ @type uri: C{str}
+
+ @param headers: The request headers to send. If no I{Host} header is
+ included, one will be added based on the request URI.
+ @type headers: L{Headers}
+
+ @param bodyProducer: An object which will produce the request body or,
+ if the request body is to be empty, L{None}.
+ @type bodyProducer: L{IBodyProducer} provider
+
+ @return: A L{Deferred} which fires with the result of the request (a
+ L{twisted.web.iweb.IResponse} provider), or fails if there is a
+ problem setting up a connection over which to issue the request.
+ It may also fail with L{SchemeNotSupported} if the scheme of the
+ given URI is not supported.
+ @rtype: L{Deferred}
+ """
+ parsedURI = _parse(uri)
+ try:
+ endpoint = self._getEndpoint(parsedURI.scheme, parsedURI.host,
+ parsedURI.port)
+ except SchemeNotSupported:
+ return defer.fail(Failure())
+ key = (parsedURI.scheme, parsedURI.host, parsedURI.port)
+ return self._requestWithEndpoint(key, endpoint, method, parsedURI,
+ headers, bodyProducer, parsedURI.path)
+
+
+
+class ProxyAgent(_AgentBase):
+ """
+ An HTTP agent able to cross HTTP proxies.
+
+ @ivar _proxyEndpoint: The endpoint used to connect to the proxy.
+
+ @since: 11.1
+ """
+
+ def __init__(self, endpoint, reactor=None, pool=None):
+ if reactor is None:
+ from twisted.internet import reactor
+ _AgentBase.__init__(self, reactor, pool)
+ self._proxyEndpoint = endpoint
+
+
+ def request(self, method, uri, headers=None, bodyProducer=None):
+ """
+ Issue a new request via the configured proxy.
+ """
+ # Cache *all* connections under the same key, since we are only
+ # connecting to a single destination, the proxy:
+ key = ("http-proxy", self._proxyEndpoint)
+
+ # To support proxying HTTPS via CONNECT, we will use key
+ # ("http-proxy-CONNECT", scheme, host, port), and an endpoint that
+ # wraps _proxyEndpoint with an additional callback to do the CONNECT.
+ return self._requestWithEndpoint(key, self._proxyEndpoint, method,
+ _parse(uri), headers, bodyProducer,
+ uri)
+
+
+
+class _FakeUrllib2Request(object):
+ """
+ A fake C{urllib2.Request} object for C{cookielib} to work with.
+
+ @see: U{http://docs.python.org/library/urllib2.html#request-objects}
+
+ @type uri: C{str}
+ @ivar uri: Request URI.
+
+ @type headers: L{twisted.web.http_headers.Headers}
+ @ivar headers: Request headers.
+
+ @type type: C{str}
+ @ivar type: The scheme of the URI.
+
+ @type host: C{str}
+ @ivar host: The host[:port] of the URI.
+
+ @since: 11.1
+ """
+ def __init__(self, uri):
+ self.uri = uri
+ self.headers = Headers()
+ self.type, rest = splittype(self.uri)
+ self.host, rest = splithost(rest)
+
+
+ def has_header(self, header):
+ return self.headers.hasHeader(header)
+
+
+ def add_unredirected_header(self, name, value):
+ self.headers.addRawHeader(name, value)
+
+
+ def get_full_url(self):
+ return self.uri
+
+
+ def get_header(self, name, default=None):
+ headers = self.headers.getRawHeaders(name, default)
+ if headers is not None:
+ return headers[0]
+ return None
+
+
+ def get_host(self):
+ return self.host
+
+
+ def get_type(self):
+ return self.type
+
+
+ def is_unverifiable(self):
+ # In theory this shouldn't be hardcoded.
+ return False
+
+
+
+class _FakeUrllib2Response(object):
+ """
+ A fake C{urllib2.Response} object for C{cookielib} to work with.
+
+ @type response: C{twisted.web.iweb.IResponse}
+ @ivar response: Underlying Twisted Web response.
+
+ @since: 11.1
+ """
+ def __init__(self, response):
+ self.response = response
+
+
+ def info(self):
+ class _Meta(object):
+ def getheaders(zelf, name):
+ return self.response.headers.getRawHeaders(name, [])
+ return _Meta()
+
+
+
+class CookieAgent(object):
+ """
+ L{CookieAgent} extends the basic L{Agent} to add RFC-compliant
+ handling of HTTP cookies. Cookies are written to and extracted
+ from a C{cookielib.CookieJar} instance.
+
+ The same cookie jar instance will be used for any requests through this
+ agent, mutating it whenever a I{Set-Cookie} header appears in a response.
+
+ @type _agent: L{twisted.web.client.Agent}
+ @ivar _agent: Underlying Twisted Web agent to issue requests through.
+
+ @type cookieJar: C{cookielib.CookieJar}
+ @ivar cookieJar: Initialized cookie jar to read cookies from and store
+ cookies to.
+
+ @since: 11.1
+ """
+ def __init__(self, agent, cookieJar):
+ self._agent = agent
+ self.cookieJar = cookieJar
+
+
+ def request(self, method, uri, headers=None, bodyProducer=None):
+ """
+ Issue a new request to the wrapped L{Agent}.
+
+ Send a I{Cookie} header if a cookie for C{uri} is stored in
+ L{CookieAgent.cookieJar}. Cookies are automatically extracted and
+ stored from requests.
+
+ If a C{'cookie'} header appears in C{headers} it will override the
+ automatic cookie header obtained from the cookie jar.
+
+ @see: L{Agent.request}
+ """
+ if headers is None:
+ headers = Headers()
+ lastRequest = _FakeUrllib2Request(uri)
+ # Setting a cookie header explicitly will disable automatic request
+ # cookies.
+ if not headers.hasHeader('cookie'):
+ self.cookieJar.add_cookie_header(lastRequest)
+ cookieHeader = lastRequest.get_header('Cookie', None)
+ if cookieHeader is not None:
+ headers = headers.copy()
+ headers.addRawHeader('cookie', cookieHeader)
+
+ d = self._agent.request(method, uri, headers, bodyProducer)
+ d.addCallback(self._extractCookies, lastRequest)
+ return d
+
+
+ def _extractCookies(self, response, request):
+ """
+ Extract response cookies and store them in the cookie jar.
+
+ @type response: L{twisted.web.iweb.IResponse}
+ @param response: Twisted Web response.
+
+ @param request: A urllib2 compatible request object.
+ """
+ resp = _FakeUrllib2Response(response)
+ self.cookieJar.extract_cookies(resp, request)
+ return response
+
+
+
+class GzipDecoder(proxyForInterface(IResponse)):
+ """
+ A wrapper for a L{Response} instance which handles gzip'ed body.
+
+ @ivar original: The original L{Response} object.
+
+ @since: 11.1
+ """
+
+ def __init__(self, response):
+ self.original = response
+ self.length = UNKNOWN_LENGTH
+
+
+ def deliverBody(self, protocol):
+ """
+ Override C{deliverBody} to wrap the given C{protocol} with
+ L{_GzipProtocol}.
+ """
+ self.original.deliverBody(_GzipProtocol(protocol, self.original))
+
+
+
+class _GzipProtocol(proxyForInterface(IProtocol)):
+ """
+ A L{Protocol} implementation which wraps another one, transparently
+ decompressing received data.
+
+ @ivar _zlibDecompress: A zlib decompress object used to decompress the data
+ stream.
+
+ @ivar _response: A reference to the original response, in case of errors.
+
+ @since: 11.1
+ """
+
+ def __init__(self, protocol, response):
+ self.original = protocol
+ self._response = response
+ self._zlibDecompress = zlib.decompressobj(16 + zlib.MAX_WBITS)
+
+
+ def dataReceived(self, data):
+ """
+ Decompress C{data} with the zlib decompressor, forwarding the raw data
+ to the original protocol.
+ """
+ try:
+ rawData = self._zlibDecompress.decompress(data)
+ except zlib.error:
+ raise ResponseFailed([failure.Failure()], self._response)
+ if rawData:
+ self.original.dataReceived(rawData)
+
+
+ def connectionLost(self, reason):
+ """
+ Forward the connection lost event, flushing remaining data from the
+ decompressor if any.
+ """
+ try:
+ rawData = self._zlibDecompress.flush()
+ except zlib.error:
+ raise ResponseFailed([reason, failure.Failure()], self._response)
+ if rawData:
+ self.original.dataReceived(rawData)
+ self.original.connectionLost(reason)
+
+
+
+class ContentDecoderAgent(object):
+ """
+ An L{Agent} wrapper to handle encoded content.
+
+ It takes care of declaring the support for content in the
+ I{Accept-Encoding} header, and automatically decompresses the received data
+ if it's effectively using compression.
+
+ @param decoders: A list or tuple of (name, decoder) objects. The name
+ declares which decoding the decoder supports, and the decoder must
+ return a response object when called/instantiated. For example,
+ C{(('gzip', GzipDecoder))}. The order determines how the decoders are
+ going to be advertized to the server.
+
+ @since: 11.1
+ """
+
+ def __init__(self, agent, decoders):
+ self._agent = agent
+ self._decoders = dict(decoders)
+ self._supported = ','.join([decoder[0] for decoder in decoders])
+
+
+ def request(self, method, uri, headers=None, bodyProducer=None):
+ """
+ Send a client request which declares supporting compressed content.
+
+ @see: L{Agent.request}.
+ """
+ if headers is None:
+ headers = Headers()
+ else:
+ headers = headers.copy()
+ headers.addRawHeader('accept-encoding', self._supported)
+ deferred = self._agent.request(method, uri, headers, bodyProducer)
+ return deferred.addCallback(self._handleResponse)
+
+
+ def _handleResponse(self, response):
+ """
+ Check if the response is encoded, and wrap it to handle decompression.
+ """
+ contentEncodingHeaders = response.headers.getRawHeaders(
+ 'content-encoding', [])
+ contentEncodingHeaders = ','.join(contentEncodingHeaders).split(',')
+ while contentEncodingHeaders:
+ name = contentEncodingHeaders.pop().strip()
+ decoder = self._decoders.get(name)
+ if decoder is not None:
+ response = decoder(response)
+ else:
+ # Add it back
+ contentEncodingHeaders.append(name)
+ break
+ if contentEncodingHeaders:
+ response.headers.setRawHeaders(
+ 'content-encoding', [','.join(contentEncodingHeaders)])
+ else:
+ response.headers.removeHeader('content-encoding')
+ return response
+
+
+
+class RedirectAgent(object):
+ """
+ An L{Agent} wrapper which handles HTTP redirects.
+
+ The implementation is rather strict: 301 and 302 behaves like 307, not
+ redirecting automatically on methods different from C{GET} and C{HEAD}.
+
+ @param redirectLimit: The maximum number of times the agent is allowed to
+ follow redirects before failing with a L{error.InfiniteRedirection}.
+
+ @since: 11.1
+ """
+
+ def __init__(self, agent, redirectLimit=20):
+ self._agent = agent
+ self._redirectLimit = redirectLimit
+
+
+ def request(self, method, uri, headers=None, bodyProducer=None):
+ """
+ Send a client request following HTTP redirects.
+
+ @see: L{Agent.request}.
+ """
+ deferred = self._agent.request(method, uri, headers, bodyProducer)
+ return deferred.addCallback(
+ self._handleResponse, method, uri, headers, 0)
+
+
+ def _handleRedirect(self, response, method, uri, headers, redirectCount):
+ """
+ Handle a redirect response, checking the number of redirects already
+ followed, and extracting the location header fields.
+ """
+ if redirectCount >= self._redirectLimit:
+ err = error.InfiniteRedirection(
+ response.code,
+ 'Infinite redirection detected',
+ location=uri)
+ raise ResponseFailed([failure.Failure(err)], response)
+ locationHeaders = response.headers.getRawHeaders('location', [])
+ if not locationHeaders:
+ err = error.RedirectWithNoLocation(
+ response.code, 'No location header field', uri)
+ raise ResponseFailed([failure.Failure(err)], response)
+ location = locationHeaders[0]
+ deferred = self._agent.request(method, location, headers)
+ return deferred.addCallback(
+ self._handleResponse, method, uri, headers, redirectCount + 1)
+
+
+ def _handleResponse(self, response, method, uri, headers, redirectCount):
+ """
+ Handle the response, making another request if it indicates a redirect.
+ """
+ if response.code in (http.MOVED_PERMANENTLY, http.FOUND,
+ http.TEMPORARY_REDIRECT):
+ if method not in ('GET', 'HEAD'):
+ err = error.PageRedirect(response.code, location=uri)
+ raise ResponseFailed([failure.Failure(err)], response)
+ return self._handleRedirect(response, method, uri, headers,
+ redirectCount)
+ elif response.code == http.SEE_OTHER:
+ return self._handleRedirect(response, 'GET', uri, headers,
+ redirectCount)
+ return response
+
+
+
+__all__ = [
+ 'PartialDownloadError', 'HTTPPageGetter', 'HTTPPageDownloader',
+ 'HTTPClientFactory', 'HTTPDownloader', 'getPage', 'downloadPage',
+ 'ResponseDone', 'Response', 'ResponseFailed', 'Agent', 'CookieAgent',
+ 'ProxyAgent', 'ContentDecoderAgent', 'GzipDecoder', 'RedirectAgent',
+ 'HTTPConnectionPool']
diff --git a/twisted/web/demo.py b/twisted/web/demo.py
new file mode 100644
index 0000000..b8475f0
--- /dev/null
+++ b/twisted/web/demo.py
@@ -0,0 +1,24 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+I am a simple test resource.
+"""
+
+from twisted.web import static
+
+
+class Test(static.Data):
+ isLeaf = True
+ def __init__(self):
+ static.Data.__init__(
+ self,
+ """
+ <html>
+ <head><title>Twisted Web Demo</title><head>
+ <body>
+ Hello! This is a Twisted Web test page.
+ </body>
+ </html>
+ """,
+ "text/html")
diff --git a/twisted/web/distrib.py b/twisted/web/distrib.py
new file mode 100644
index 0000000..830675b
--- /dev/null
+++ b/twisted/web/distrib.py
@@ -0,0 +1,373 @@
+# -*- test-case-name: twisted.web.test.test_distrib -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Distributed web servers.
+
+This is going to have to be refactored so that argument parsing is done
+by each subprocess and not by the main web server (i.e. GET, POST etc.).
+"""
+
+# System Imports
+import types, os, copy, cStringIO
+try:
+ import pwd
+except ImportError:
+ pwd = None
+
+from xml.dom.minidom import Element, Text
+
+# Twisted Imports
+from twisted.spread import pb
+from twisted.spread.banana import SIZE_LIMIT
+from twisted.web import http, resource, server, html, static
+from twisted.web.http_headers import Headers
+from twisted.python import log
+from twisted.persisted import styles
+from twisted.internet import address, reactor
+
+
+class _ReferenceableProducerWrapper(pb.Referenceable):
+ def __init__(self, producer):
+ self.producer = producer
+
+ def remote_resumeProducing(self):
+ self.producer.resumeProducing()
+
+ def remote_pauseProducing(self):
+ self.producer.pauseProducing()
+
+ def remote_stopProducing(self):
+ self.producer.stopProducing()
+
+
+class Request(pb.RemoteCopy, server.Request):
+ """
+ A request which was received by a L{ResourceSubscription} and sent via
+ PB to a distributed node.
+ """
+ def setCopyableState(self, state):
+ """
+ Initialize this L{twisted.web.distrib.Request} based on the copied
+ state so that it closely resembles a L{twisted.web.server.Request}.
+ """
+ for k in 'host', 'client':
+ tup = state[k]
+ addrdesc = {'INET': 'TCP', 'UNIX': 'UNIX'}[tup[0]]
+ addr = {'TCP': lambda: address.IPv4Address(addrdesc,
+ tup[1], tup[2]),
+ 'UNIX': lambda: address.UNIXAddress(tup[1])}[addrdesc]()
+ state[k] = addr
+ state['requestHeaders'] = Headers(dict(state['requestHeaders']))
+ pb.RemoteCopy.setCopyableState(self, state)
+ # Emulate the local request interface --
+ self.content = cStringIO.StringIO(self.content_data)
+ self.finish = self.remote.remoteMethod('finish')
+ self.setHeader = self.remote.remoteMethod('setHeader')
+ self.addCookie = self.remote.remoteMethod('addCookie')
+ self.setETag = self.remote.remoteMethod('setETag')
+ self.setResponseCode = self.remote.remoteMethod('setResponseCode')
+ self.setLastModified = self.remote.remoteMethod('setLastModified')
+
+ # To avoid failing if a resource tries to write a very long string
+ # all at once, this one will be handled slightly differently.
+ self._write = self.remote.remoteMethod('write')
+
+
+ def write(self, bytes):
+ """
+ Write the given bytes to the response body.
+
+ @param bytes: The bytes to write. If this is longer than 640k, it
+ will be split up into smaller pieces.
+ """
+ start = 0
+ end = SIZE_LIMIT
+ while True:
+ self._write(bytes[start:end])
+ start += SIZE_LIMIT
+ end += SIZE_LIMIT
+ if start >= len(bytes):
+ break
+
+
+ def registerProducer(self, producer, streaming):
+ self.remote.callRemote("registerProducer",
+ _ReferenceableProducerWrapper(producer),
+ streaming).addErrback(self.fail)
+
+ def unregisterProducer(self):
+ self.remote.callRemote("unregisterProducer").addErrback(self.fail)
+
+ def fail(self, failure):
+ log.err(failure)
+
+
+pb.setUnjellyableForClass(server.Request, Request)
+
+class Issue:
+ def __init__(self, request):
+ self.request = request
+
+ def finished(self, result):
+ if result != server.NOT_DONE_YET:
+ assert isinstance(result, types.StringType),\
+ "return value not a string"
+ self.request.write(result)
+ self.request.finish()
+
+ def failed(self, failure):
+ #XXX: Argh. FIXME.
+ failure = str(failure)
+ self.request.write(
+ resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
+ "Server Connection Lost",
+ "Connection to distributed server lost:" +
+ html.PRE(failure)).
+ render(self.request))
+ self.request.finish()
+ log.msg(failure)
+
+
+class ResourceSubscription(resource.Resource):
+ isLeaf = 1
+ waiting = 0
+ def __init__(self, host, port):
+ resource.Resource.__init__(self)
+ self.host = host
+ self.port = port
+ self.pending = []
+ self.publisher = None
+
+ def __getstate__(self):
+ """Get persistent state for this ResourceSubscription.
+ """
+ # When I unserialize,
+ state = copy.copy(self.__dict__)
+ # Publisher won't be connected...
+ state['publisher'] = None
+ # I won't be making a connection
+ state['waiting'] = 0
+ # There will be no pending requests.
+ state['pending'] = []
+ return state
+
+ def connected(self, publisher):
+ """I've connected to a publisher; I'll now send all my requests.
+ """
+ log.msg('connected to publisher')
+ publisher.broker.notifyOnDisconnect(self.booted)
+ self.publisher = publisher
+ self.waiting = 0
+ for request in self.pending:
+ self.render(request)
+ self.pending = []
+
+ def notConnected(self, msg):
+ """I can't connect to a publisher; I'll now reply to all pending
+ requests.
+ """
+ log.msg("could not connect to distributed web service: %s" % msg)
+ self.waiting = 0
+ self.publisher = None
+ for request in self.pending:
+ request.write("Unable to connect to distributed server.")
+ request.finish()
+ self.pending = []
+
+ def booted(self):
+ self.notConnected("connection dropped")
+
+ def render(self, request):
+ """Render this request, from my server.
+
+ This will always be asynchronous, and therefore return NOT_DONE_YET.
+ It spins off a request to the pb client, and either adds it to the list
+ of pending issues or requests it immediately, depending on if the
+ client is already connected.
+ """
+ if not self.publisher:
+ self.pending.append(request)
+ if not self.waiting:
+ self.waiting = 1
+ bf = pb.PBClientFactory()
+ timeout = 10
+ if self.host == "unix":
+ reactor.connectUNIX(self.port, bf, timeout)
+ else:
+ reactor.connectTCP(self.host, self.port, bf, timeout)
+ d = bf.getRootObject()
+ d.addCallbacks(self.connected, self.notConnected)
+
+ else:
+ i = Issue(request)
+ self.publisher.callRemote('request', request).addCallbacks(i.finished, i.failed)
+ return server.NOT_DONE_YET
+
+
+
+class ResourcePublisher(pb.Root, styles.Versioned):
+ """
+ L{ResourcePublisher} exposes a remote API which can be used to respond
+ to request.
+
+ @ivar site: The site which will be used for resource lookup.
+ @type site: L{twisted.web.server.Site}
+ """
+ def __init__(self, site):
+ self.site = site
+
+ persistenceVersion = 2
+
+ def upgradeToVersion2(self):
+ self.application.authorizer.removeIdentity("web")
+ del self.application.services[self.serviceName]
+ del self.serviceName
+ del self.application
+ del self.perspectiveName
+
+ def getPerspectiveNamed(self, name):
+ return self
+
+
+ def remote_request(self, request):
+ """
+ Look up the resource for the given request and render it.
+ """
+ res = self.site.getResourceFor(request)
+ log.msg( request )
+ result = res.render(request)
+ if result is not server.NOT_DONE_YET:
+ request.write(result)
+ request.finish()
+ return server.NOT_DONE_YET
+
+
+
+class UserDirectory(resource.Resource):
+ """
+ A resource which lists available user resources and serves them as
+ children.
+
+ @ivar _pwd: An object like L{pwd} which is used to enumerate users and
+ their home directories.
+ """
+
+ userDirName = 'public_html'
+ userSocketName = '.twistd-web-pb'
+
+ template = """
+<html>
+ <head>
+ <title>twisted.web.distrib.UserDirectory</title>
+ <style>
+
+ a
+ {
+ font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
+ color: #369;
+ text-decoration: none;
+ }
+
+ th
+ {
+ font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ text-decoration: none;
+ text-align: left;
+ }
+
+ pre, code
+ {
+ font-family: "Courier New", Courier, monospace;
+ }
+
+ p, body, td, ol, ul, menu, blockquote, div
+ {
+ font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
+ color: #000;
+ }
+ </style>
+ </head>
+
+ <body>
+ <h1>twisted.web.distrib.UserDirectory</h1>
+
+ %(users)s
+</body>
+</html>
+"""
+
+ def __init__(self, userDatabase=None):
+ resource.Resource.__init__(self)
+ if userDatabase is None:
+ userDatabase = pwd
+ self._pwd = userDatabase
+
+
+ def _users(self):
+ """
+ Return a list of two-tuples giving links to user resources and text to
+ associate with those links.
+ """
+ users = []
+ for user in self._pwd.getpwall():
+ name, passwd, uid, gid, gecos, dir, shell = user
+ realname = gecos.split(',')[0]
+ if not realname:
+ realname = name
+ if os.path.exists(os.path.join(dir, self.userDirName)):
+ users.append((name, realname + ' (file)'))
+ twistdsock = os.path.join(dir, self.userSocketName)
+ if os.path.exists(twistdsock):
+ linkName = name + '.twistd'
+ users.append((linkName, realname + ' (twistd)'))
+ return users
+
+
+ def render_GET(self, request):
+ """
+ Render as HTML a listing of all known users with links to their
+ personal resources.
+ """
+ listing = Element('ul')
+ for link, text in self._users():
+ linkElement = Element('a')
+ linkElement.setAttribute('href', link + '/')
+ textNode = Text()
+ textNode.data = text
+ linkElement.appendChild(textNode)
+ item = Element('li')
+ item.appendChild(linkElement)
+ listing.appendChild(item)
+ return self.template % {'users': listing.toxml()}
+
+
+ def getChild(self, name, request):
+ if name == '':
+ return self
+
+ td = '.twistd'
+
+ if name[-len(td):] == td:
+ username = name[:-len(td)]
+ sub = 1
+ else:
+ username = name
+ sub = 0
+ try:
+ pw_name, pw_passwd, pw_uid, pw_gid, pw_gecos, pw_dir, pw_shell \
+ = self._pwd.getpwnam(username)
+ except KeyError:
+ return resource.NoResource()
+ if sub:
+ twistdsock = os.path.join(pw_dir, self.userSocketName)
+ rs = ResourceSubscription('unix',twistdsock)
+ self.putChild(name, rs)
+ return rs
+ else:
+ path = os.path.join(pw_dir, self.userDirName)
+ if not os.path.exists(path):
+ return resource.NoResource()
+ return static.File(path)
diff --git a/twisted/web/domhelpers.py b/twisted/web/domhelpers.py
new file mode 100644
index 0000000..e6f1b51
--- /dev/null
+++ b/twisted/web/domhelpers.py
@@ -0,0 +1,268 @@
+# -*- test-case-name: twisted.web.test.test_domhelpers -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A library for performing interesting tasks with DOM objects.
+"""
+
+import StringIO
+
+from twisted.web import microdom
+from twisted.web.microdom import getElementsByTagName, escape, unescape
+
+
+class NodeLookupError(Exception):
+ pass
+
+
+def substitute(request, node, subs):
+ """
+ Look through the given node's children for strings, and
+ attempt to do string substitution with the given parameter.
+ """
+ for child in node.childNodes:
+ if hasattr(child, 'nodeValue') and child.nodeValue:
+ child.replaceData(0, len(child.nodeValue), child.nodeValue % subs)
+ substitute(request, child, subs)
+
+def _get(node, nodeId, nodeAttrs=('id','class','model','pattern')):
+ """
+ (internal) Get a node with the specified C{nodeId} as any of the C{class},
+ C{id} or C{pattern} attributes.
+ """
+
+ if hasattr(node, 'hasAttributes') and node.hasAttributes():
+ for nodeAttr in nodeAttrs:
+ if (str (node.getAttribute(nodeAttr)) == nodeId):
+ return node
+ if node.hasChildNodes():
+ if hasattr(node.childNodes, 'length'):
+ length = node.childNodes.length
+ else:
+ length = len(node.childNodes)
+ for childNum in range(length):
+ result = _get(node.childNodes[childNum], nodeId)
+ if result: return result
+
+def get(node, nodeId):
+ """
+ Get a node with the specified C{nodeId} as any of the C{class},
+ C{id} or C{pattern} attributes. If there is no such node, raise
+ L{NodeLookupError}.
+ """
+ result = _get(node, nodeId)
+ if result: return result
+ raise NodeLookupError, nodeId
+
+def getIfExists(node, nodeId):
+ """
+ Get a node with the specified C{nodeId} as any of the C{class},
+ C{id} or C{pattern} attributes. If there is no such node, return
+ C{None}.
+ """
+ return _get(node, nodeId)
+
+def getAndClear(node, nodeId):
+ """Get a node with the specified C{nodeId} as any of the C{class},
+ C{id} or C{pattern} attributes. If there is no such node, raise
+ L{NodeLookupError}. Remove all child nodes before returning.
+ """
+ result = get(node, nodeId)
+ if result:
+ clearNode(result)
+ return result
+
+def clearNode(node):
+ """
+ Remove all children from the given node.
+ """
+ node.childNodes[:] = []
+
+def locateNodes(nodeList, key, value, noNesting=1):
+ """
+ Find subnodes in the given node where the given attribute
+ has the given value.
+ """
+ returnList = []
+ if not isinstance(nodeList, type([])):
+ return locateNodes(nodeList.childNodes, key, value, noNesting)
+ for childNode in nodeList:
+ if not hasattr(childNode, 'getAttribute'):
+ continue
+ if str(childNode.getAttribute(key)) == value:
+ returnList.append(childNode)
+ if noNesting:
+ continue
+ returnList.extend(locateNodes(childNode, key, value, noNesting))
+ return returnList
+
+def superSetAttribute(node, key, value):
+ if not hasattr(node, 'setAttribute'): return
+ node.setAttribute(key, value)
+ if node.hasChildNodes():
+ for child in node.childNodes:
+ superSetAttribute(child, key, value)
+
+def superPrependAttribute(node, key, value):
+ if not hasattr(node, 'setAttribute'): return
+ old = node.getAttribute(key)
+ if old:
+ node.setAttribute(key, value+'/'+old)
+ else:
+ node.setAttribute(key, value)
+ if node.hasChildNodes():
+ for child in node.childNodes:
+ superPrependAttribute(child, key, value)
+
+def superAppendAttribute(node, key, value):
+ if not hasattr(node, 'setAttribute'): return
+ old = node.getAttribute(key)
+ if old:
+ node.setAttribute(key, old + '/' + value)
+ else:
+ node.setAttribute(key, value)
+ if node.hasChildNodes():
+ for child in node.childNodes:
+ superAppendAttribute(child, key, value)
+
+def gatherTextNodes(iNode, dounescape=0, joinWith=""):
+ """Visit each child node and collect its text data, if any, into a string.
+For example::
+ >>> doc=microdom.parseString('<a>1<b>2<c>3</c>4</b></a>')
+ >>> gatherTextNodes(doc.documentElement)
+ '1234'
+With dounescape=1, also convert entities back into normal characters.
+@return: the gathered nodes as a single string
+@rtype: str
+"""
+ gathered=[]
+ gathered_append=gathered.append
+ slice=[iNode]
+ while len(slice)>0:
+ c=slice.pop(0)
+ if hasattr(c, 'nodeValue') and c.nodeValue is not None:
+ if dounescape:
+ val=unescape(c.nodeValue)
+ else:
+ val=c.nodeValue
+ gathered_append(val)
+ slice[:0]=c.childNodes
+ return joinWith.join(gathered)
+
+class RawText(microdom.Text):
+ """This is an evil and horrible speed hack. Basically, if you have a big
+ chunk of XML that you want to insert into the DOM, but you don't want to
+ incur the cost of parsing it, you can construct one of these and insert it
+ into the DOM. This will most certainly only work with microdom as the API
+ for converting nodes to xml is different in every DOM implementation.
+
+ This could be improved by making this class a Lazy parser, so if you
+ inserted this into the DOM and then later actually tried to mutate this
+ node, it would be parsed then.
+ """
+
+ def writexml(self, writer, indent="", addindent="", newl="", strip=0, nsprefixes=None, namespace=None):
+ writer.write("%s%s%s" % (indent, self.data, newl))
+
+def findNodes(parent, matcher, accum=None):
+ if accum is None:
+ accum = []
+ if not parent.hasChildNodes():
+ return accum
+ for child in parent.childNodes:
+ # print child, child.nodeType, child.nodeName
+ if matcher(child):
+ accum.append(child)
+ findNodes(child, matcher, accum)
+ return accum
+
+
+def findNodesShallowOnMatch(parent, matcher, recurseMatcher, accum=None):
+ if accum is None:
+ accum = []
+ if not parent.hasChildNodes():
+ return accum
+ for child in parent.childNodes:
+ # print child, child.nodeType, child.nodeName
+ if matcher(child):
+ accum.append(child)
+ if recurseMatcher(child):
+ findNodesShallowOnMatch(child, matcher, recurseMatcher, accum)
+ return accum
+
+def findNodesShallow(parent, matcher, accum=None):
+ if accum is None:
+ accum = []
+ if not parent.hasChildNodes():
+ return accum
+ for child in parent.childNodes:
+ if matcher(child):
+ accum.append(child)
+ else:
+ findNodes(child, matcher, accum)
+ return accum
+
+
+def findElementsWithAttributeShallow(parent, attribute):
+ """
+ Return an iterable of the elements which are direct children of C{parent}
+ and which have the C{attribute} attribute.
+ """
+ return findNodesShallow(parent,
+ lambda n: getattr(n, 'tagName', None) is not None and
+ n.hasAttribute(attribute))
+
+
+def findElements(parent, matcher):
+ """
+ Return an iterable of the elements which are children of C{parent} for
+ which the predicate C{matcher} returns true.
+ """
+ return findNodes(
+ parent,
+ lambda n, matcher=matcher: getattr(n, 'tagName', None) is not None and
+ matcher(n))
+
+def findElementsWithAttribute(parent, attribute, value=None):
+ if value:
+ return findElements(
+ parent,
+ lambda n, attribute=attribute, value=value:
+ n.hasAttribute(attribute) and n.getAttribute(attribute) == value)
+ else:
+ return findElements(
+ parent,
+ lambda n, attribute=attribute: n.hasAttribute(attribute))
+
+
+def findNodesNamed(parent, name):
+ return findNodes(parent, lambda n, name=name: n.nodeName == name)
+
+
+def writeNodeData(node, oldio):
+ for subnode in node.childNodes:
+ if hasattr(subnode, 'data'):
+ oldio.write(subnode.data)
+ else:
+ writeNodeData(subnode, oldio)
+
+
+def getNodeText(node):
+ oldio = StringIO.StringIO()
+ writeNodeData(node, oldio)
+ return oldio.getvalue()
+
+
+def getParents(node):
+ l = []
+ while node:
+ l.append(node)
+ node = node.parentNode
+ return l
+
+def namedChildren(parent, nodeName):
+ """namedChildren(parent, nodeName) -> children (not descendants) of parent
+ that have tagName == nodeName
+ """
+ return [n for n in parent.childNodes if getattr(n, 'tagName', '')==nodeName]
diff --git a/twisted/web/error.py b/twisted/web/error.py
new file mode 100644
index 0000000..961bd83
--- /dev/null
+++ b/twisted/web/error.py
@@ -0,0 +1,422 @@
+# -*- test-case-name: twisted.web.test.test_error -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Exception definitions for L{twisted.web}.
+"""
+
+import operator, warnings
+
+from twisted.web import http
+
+
+class Error(Exception):
+ """
+ A basic HTTP error.
+
+ @type status: C{str}
+ @ivar status: Refers to an HTTP status code, for example L{http.NOT_FOUND}.
+
+ @type message: C{str}
+ @param message: A short error message, for example "NOT FOUND".
+
+ @type response: C{str}
+ @ivar response: A complete HTML document for an error page.
+ """
+ def __init__(self, code, message=None, response=None):
+ """
+ Initializes a basic exception.
+
+ @type code: C{str}
+ @param code: Refers to an HTTP status code, for example
+ L{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to a
+ descriptive string that is used instead.
+
+ @type message: C{str}
+ @param message: A short error message, for example "NOT FOUND".
+
+ @type response: C{str}
+ @param response: A complete HTML document for an error page.
+ """
+ if not message:
+ try:
+ message = http.responses.get(int(code))
+ except ValueError:
+ # If code wasn't a stringified int, can't map the
+ # status code to a descriptive string so keep message
+ # unchanged.
+ pass
+
+ Exception.__init__(self, code, message, response)
+ self.status = code
+ self.message = message
+ self.response = response
+
+
+ def __str__(self):
+ return '%s %s' % (self[0], self[1])
+
+
+
+class PageRedirect(Error):
+ """
+ A request resulted in an HTTP redirect.
+
+ @type location: C{str}
+ @ivar location: The location of the redirect which was not followed.
+ """
+ def __init__(self, code, message=None, response=None, location=None):
+ """
+ Initializes a page redirect exception.
+
+ @type code: C{str}
+ @param code: Refers to an HTTP status code, for example
+ L{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to a
+ descriptive string that is used instead.
+
+ @type message: C{str}
+ @param message: A short error message, for example "NOT FOUND".
+
+ @type response: C{str}
+ @param response: A complete HTML document for an error page.
+
+ @type location: C{str}
+ @param location: The location response-header field value. It is an
+ absolute URI used to redirect the receiver to a location other than
+ the Request-URI so the request can be completed.
+ """
+ if not message:
+ try:
+ message = http.responses.get(int(code))
+ except ValueError:
+ # If code wasn't a stringified int, can't map the
+ # status code to a descriptive string so keep message
+ # unchanged.
+ pass
+
+ if location and message:
+ message = "%s to %s" % (message, location)
+
+ Error.__init__(self, code, message, response)
+ self.location = location
+
+
+
+class InfiniteRedirection(Error):
+ """
+ HTTP redirection is occurring endlessly.
+
+ @type location: C{str}
+ @ivar location: The first URL in the series of redirections which was
+ not followed.
+ """
+ def __init__(self, code, message=None, response=None, location=None):
+ """
+ Initializes an infinite redirection exception.
+
+ @type code: C{str}
+ @param code: Refers to an HTTP status code, for example
+ L{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to a
+ descriptive string that is used instead.
+
+ @type message: C{str}
+ @param message: A short error message, for example "NOT FOUND".
+
+ @type response: C{str}
+ @param response: A complete HTML document for an error page.
+
+ @type location: C{str}
+ @param location: The location response-header field value. It is an
+ absolute URI used to redirect the receiver to a location other than
+ the Request-URI so the request can be completed.
+ """
+ if not message:
+ try:
+ message = http.responses.get(int(code))
+ except ValueError:
+ # If code wasn't a stringified int, can't map the
+ # status code to a descriptive string so keep message
+ # unchanged.
+ pass
+
+ if location and message:
+ message = "%s to %s" % (message, location)
+
+ Error.__init__(self, code, message, response)
+ self.location = location
+
+
+
+class RedirectWithNoLocation(Error):
+ """
+ Exception passed to L{ResponseFailed} if we got a redirect without a
+ C{Location} header field.
+
+ @since: 11.1
+ """
+
+ def __init__(self, code, message, uri):
+ """
+ Initializes a page redirect exception when no location is given.
+
+ @type code: C{str}
+ @param code: Refers to an HTTP status code, for example
+ L{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to
+ a descriptive string that is used instead.
+
+ @type message: C{str}
+ @param message: A short error message.
+
+ @type uri: C{str}
+ @param uri: The URI which failed to give a proper location header
+ field.
+ """
+ message = "%s to %s" % (message, uri)
+
+ Error.__init__(self, code, message)
+ self.uri = uri
+
+
+
+class UnsupportedMethod(Exception):
+ """
+ Raised by a resource when faced with a strange request method.
+
+ RFC 2616 (HTTP 1.1) gives us two choices when faced with this situtation:
+ If the type of request is known to us, but not allowed for the requested
+ resource, respond with NOT_ALLOWED. Otherwise, if the request is something
+ we don't know how to deal with in any case, respond with NOT_IMPLEMENTED.
+
+ When this exception is raised by a Resource's render method, the server
+ will make the appropriate response.
+
+ This exception's first argument MUST be a sequence of the methods the
+ resource *does* support.
+ """
+
+ allowedMethods = ()
+
+ def __init__(self, allowedMethods, *args):
+ Exception.__init__(self, allowedMethods, *args)
+ self.allowedMethods = allowedMethods
+
+ if not operator.isSequenceType(allowedMethods):
+ why = "but my first argument is not a sequence."
+ s = ("First argument must be a sequence of"
+ " supported methods, %s" % (why,))
+ raise TypeError, s
+
+
+
+class SchemeNotSupported(Exception):
+ """
+ The scheme of a URI was not one of the supported values.
+ """
+
+
+
+from twisted.web import resource as _resource
+
+class ErrorPage(_resource.ErrorPage):
+ """
+ Deprecated alias for L{twisted.web.resource.ErrorPage}.
+ """
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "twisted.web.error.ErrorPage is deprecated since Twisted 9.0. "
+ "See twisted.web.resource.ErrorPage.", DeprecationWarning,
+ stacklevel=2)
+ _resource.ErrorPage.__init__(self, *args, **kwargs)
+
+
+
+class NoResource(_resource.NoResource):
+ """
+ Deprecated alias for L{twisted.web.resource.NoResource}.
+ """
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "twisted.web.error.NoResource is deprecated since Twisted 9.0. "
+ "See twisted.web.resource.NoResource.", DeprecationWarning,
+ stacklevel=2)
+ _resource.NoResource.__init__(self, *args, **kwargs)
+
+
+
+class ForbiddenResource(_resource.ForbiddenResource):
+ """
+ Deprecated alias for L{twisted.web.resource.ForbiddenResource}.
+ """
+ def __init__(self, *args, **kwargs):
+ warnings.warn(
+ "twisted.web.error.ForbiddenResource is deprecated since Twisted "
+ "9.0. See twisted.web.resource.ForbiddenResource.",
+ DeprecationWarning, stacklevel=2)
+ _resource.ForbiddenResource.__init__(self, *args, **kwargs)
+
+
+
+class RenderError(Exception):
+ """
+ Base exception class for all errors which can occur during template
+ rendering.
+ """
+
+
+
+class MissingRenderMethod(RenderError):
+ """
+ Tried to use a render method which does not exist.
+
+ @ivar element: The element which did not have the render method.
+ @ivar renderName: The name of the renderer which could not be found.
+ """
+ def __init__(self, element, renderName):
+ RenderError.__init__(self, element, renderName)
+ self.element = element
+ self.renderName = renderName
+
+
+ def __repr__(self):
+ return '%r: %r had no render method named %r' % (
+ self.__class__.__name__, self.element, self.renderName)
+
+
+
+class MissingTemplateLoader(RenderError):
+ """
+ L{MissingTemplateLoader} is raised when trying to render an Element without
+ a template loader, i.e. a C{loader} attribute.
+
+ @ivar element: The Element which did not have a document factory.
+ """
+ def __init__(self, element):
+ RenderError.__init__(self, element)
+ self.element = element
+
+
+ def __repr__(self):
+ return '%r: %r had no loader' % (self.__class__.__name__,
+ self.element)
+
+
+
+class UnexposedMethodError(Exception):
+ """
+ Raised on any attempt to get a method which has not been exposed.
+ """
+
+
+
+class UnfilledSlot(Exception):
+ """
+ During flattening, a slot with no associated data was encountered.
+ """
+
+
+
+class UnsupportedType(Exception):
+ """
+ During flattening, an object of a type which cannot be flattened was
+ encountered.
+ """
+
+
+
+class FlattenerError(Exception):
+ """
+ An error occurred while flattening an object.
+
+ @ivar _roots: A list of the objects on the flattener's stack at the time
+ the unflattenable object was encountered. The first element is least
+ deeply nested object and the last element is the most deeply nested.
+ """
+ def __init__(self, exception, roots, traceback):
+ self._exception = exception
+ self._roots = roots
+ self._traceback = traceback
+ Exception.__init__(self, exception, roots, traceback)
+
+
+ def _formatRoot(self, obj):
+ """
+ Convert an object from C{self._roots} to a string suitable for
+ inclusion in a render-traceback (like a normal Python traceback, but
+ can include "frame" source locations which are not in Python source
+ files).
+
+ @param obj: Any object which can be a render step I{root}.
+ Typically, L{Tag}s, strings, and other simple Python types.
+
+ @return: A string representation of C{obj}.
+ @rtype: L{str}
+ """
+ # There's a circular dependency between this class and 'Tag', although
+ # only for an isinstance() check.
+ from twisted.web.template import Tag
+ if isinstance(obj, (str, unicode)):
+ # It's somewhat unlikely that there will ever be a str in the roots
+ # list. However, something like a MemoryError during a str.replace
+ # call (eg, replacing " with &quot;) could possibly cause this.
+ # Likewise, UTF-8 encoding a unicode string to a byte string might
+ # fail like this.
+ if len(obj) > 40:
+ if isinstance(obj, str):
+ prefix = 1
+ else:
+ prefix = 2
+ return repr(obj[:20])[:-1] + '<...>' + repr(obj[-20:])[prefix:]
+ else:
+ return repr(obj)
+ elif isinstance(obj, Tag):
+ if obj.filename is None:
+ return 'Tag <' + obj.tagName + '>'
+ else:
+ return "File \"%s\", line %d, column %d, in \"%s\"" % (
+ obj.filename, obj.lineNumber,
+ obj.columnNumber, obj.tagName)
+ else:
+ return repr(obj)
+
+
+ def __repr__(self):
+ """
+ Present a string representation which includes a template traceback, so
+ we can tell where this error occurred in the template, as well as in
+ Python.
+ """
+ # Avoid importing things unnecessarily until we actually need them;
+ # since this is an 'error' module we should be extra paranoid about
+ # that.
+ from traceback import format_list
+ if self._roots:
+ roots = ' ' + '\n '.join([
+ self._formatRoot(r) for r in self._roots]) + '\n'
+ else:
+ roots = ''
+ if self._traceback:
+ traceback = '\n'.join([
+ line
+ for entry in format_list(self._traceback)
+ for line in entry.splitlines()]) + '\n'
+ else:
+ traceback = ''
+ return (
+ 'Exception while flattening:\n' +
+ roots + traceback +
+ self._exception.__class__.__name__ + ': ' +
+ str(self._exception) + '\n')
+
+
+ def __str__(self):
+ return repr(self)
+
+
+
+__all__ = [
+ 'Error', 'PageRedirect', 'InfiniteRedirection', 'ErrorPage', 'NoResource',
+ 'ForbiddenResource', 'RenderError', 'MissingRenderMethod',
+ 'MissingTemplateLoader', 'UnexposedMethodError', 'UnfilledSlot',
+ 'UnsupportedType', 'FlattenerError', 'RedirectWithNoLocation'
+]
diff --git a/twisted/web/failure.xhtml b/twisted/web/failure.xhtml
new file mode 100644
index 0000000..1e88a3a
--- /dev/null
+++ b/twisted/web/failure.xhtml
@@ -0,0 +1,71 @@
+<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
+ <style type="text/css">
+ div.error {
+ color: red;
+ font-family: Verdana, Arial, helvetica, sans-serif;
+ font-weight: bold;
+ }
+
+ div {
+ font-family: Verdana, Arial, helvetica, sans-serif;
+ }
+
+ div.stackTrace {
+ }
+
+ div.frame {
+ padding: 1em;
+ background: white;
+ border-bottom: thin black dashed;
+ }
+
+ div.frame:first-child {
+ padding: 1em;
+ background: white;
+ border-top: thin black dashed;
+ border-bottom: thin black dashed;
+ }
+
+ div.location {
+ }
+
+ span.function {
+ font-weight: bold;
+ font-family: "Courier New", courier, monospace;
+ }
+
+ div.snippet {
+ margin-bottom: 0.5em;
+ margin-left: 1em;
+ background: #FFFFDD;
+ }
+
+ div.snippetHighlightLine {
+ color: red;
+ }
+
+ span.code {
+ font-family: "Courier New", courier, monospace;
+ }
+ </style>
+
+ <div class="error">
+ <span t:render="type" />: <span t:render="value" />
+ </div>
+ <div class="stackTrace" t:render="traceback">
+ <div class="frame" t:render="frames">
+ <div class="location">
+ <span t:render="filename" />:<span t:render="lineNumber" /> in <span class="function" t:render="function" />
+ </div>
+ <div class="snippet" t:render="source">
+ <div t:render="sourceLines">
+ <span class="lineno" t:render="lineNumber" />
+ <code class="code" t:render="sourceLine" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="error">
+ <span t:render="type" />: <span t:render="value" />
+ </div>
+</div>
diff --git a/twisted/web/google.py b/twisted/web/google.py
new file mode 100644
index 0000000..a91920c
--- /dev/null
+++ b/twisted/web/google.py
@@ -0,0 +1,75 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+"""\"I'm Feeling Lucky\" with U{Google<http://google.com>}.
+"""
+import urllib
+from twisted.internet import protocol, reactor, defer
+from twisted.web import http
+
+class GoogleChecker(http.HTTPClient):
+
+ def connectionMade(self):
+ self.sendCommand('GET', self.factory.url)
+ self.sendHeader('Host', self.factory.host)
+ self.sendHeader('User-Agent', self.factory.agent)
+ self.endHeaders()
+
+ def handleHeader(self, key, value):
+ key = key.lower()
+ if key == 'location':
+ self.factory.gotLocation(value)
+
+ def handleStatus(self, version, status, message):
+ if status != '302':
+ self.factory.noLocation(ValueError("bad status"))
+
+ def handleEndHeaders(self):
+ self.factory.noLocation(ValueError("no location"))
+
+ def handleResponsePart(self, part):
+ pass
+
+ def handleResponseEnd(self):
+ pass
+
+ def connectionLost(self, reason):
+ self.factory.noLocation(reason)
+
+
+class GoogleCheckerFactory(protocol.ClientFactory):
+
+ protocol = GoogleChecker
+
+ def __init__(self, words):
+ self.url = ('/search?q=%s&btnI=%s' %
+ (urllib.quote_plus(' '.join(words)),
+ urllib.quote_plus("I'm Feeling Lucky")))
+ self.agent="Twisted/GoogleChecker"
+ self.host = "www.google.com"
+ self.deferred = defer.Deferred()
+
+ def clientConnectionFailed(self, _, reason):
+ self.noLocation(reason)
+
+ def gotLocation(self, location):
+ if self.deferred:
+ self.deferred.callback(location)
+ self.deferred = None
+
+ def noLocation(self, error):
+ if self.deferred:
+ self.deferred.errback(error)
+ self.deferred = None
+
+
+def checkGoogle(words):
+ """Check google for a match.
+
+ @returns: a Deferred which will callback with a URL or errback with a
+ Failure.
+ """
+ factory = GoogleCheckerFactory(words)
+ reactor.connectTCP('www.google.com', 80, factory)
+ return factory.deferred
diff --git a/twisted/web/guard.py b/twisted/web/guard.py
new file mode 100644
index 0000000..f3bb4d7
--- /dev/null
+++ b/twisted/web/guard.py
@@ -0,0 +1,17 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Resource traversal integration with L{twisted.cred} to allow for
+authentication and authorization of HTTP requests.
+"""
+
+# Expose HTTP authentication classes here.
+from twisted.web._auth.wrapper import HTTPAuthSessionWrapper
+from twisted.web._auth.basic import BasicCredentialFactory
+from twisted.web._auth.digest import DigestCredentialFactory
+
+__all__ = [
+ "HTTPAuthSessionWrapper",
+
+ "BasicCredentialFactory", "DigestCredentialFactory"]
diff --git a/twisted/web/html.py b/twisted/web/html.py
new file mode 100644
index 0000000..6a97d8c
--- /dev/null
+++ b/twisted/web/html.py
@@ -0,0 +1,49 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""I hold HTML generation helpers.
+"""
+
+from twisted.python import log
+#t.w imports
+from twisted.web import resource
+
+import traceback, string
+
+from cStringIO import StringIO
+from microdom import escape
+
+def PRE(text):
+ "Wrap <pre> tags around some text and HTML-escape it."
+ return "<pre>"+escape(text)+"</pre>"
+
+def UL(lst):
+ io = StringIO()
+ io.write("<ul>\n")
+ for el in lst:
+ io.write("<li> %s</li>\n" % el)
+ io.write("</ul>")
+ return io.getvalue()
+
+def linkList(lst):
+ io = StringIO()
+ io.write("<ul>\n")
+ for hr, el in lst:
+ io.write('<li> <a href="%s">%s</a></li>\n' % (hr, el))
+ io.write("</ul>")
+ return io.getvalue()
+
+def output(func, *args, **kw):
+ """output(func, *args, **kw) -> html string
+ Either return the result of a function (which presumably returns an
+ HTML-legal string) or a sparse HTMLized error message and a message
+ in the server log.
+ """
+ try:
+ return func(*args, **kw)
+ except:
+ log.msg("Error calling %r:" % (func,))
+ log.err()
+ return PRE("An error occurred.")
diff --git a/twisted/web/http.py b/twisted/web/http.py
new file mode 100644
index 0000000..390b79e
--- /dev/null
+++ b/twisted/web/http.py
@@ -0,0 +1,1812 @@
+# -*- test-case-name: twisted.web.test.test_http -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HyperText Transfer Protocol implementation.
+
+This is the basic server-side protocol implementation used by the Twisted
+Web server. It can parse HTTP 1.0 requests and supports many HTTP 1.1
+features as well. Additionally, some functionality implemented here is
+also useful for HTTP clients (such as the chunked encoding parser).
+"""
+
+# system imports
+from cStringIO import StringIO
+import tempfile
+import base64, binascii
+import cgi
+import socket
+import math
+import time
+import calendar
+import warnings
+import os
+from urlparse import urlparse as _urlparse
+
+from zope.interface import implements
+
+# twisted imports
+from twisted.internet import interfaces, reactor, protocol, address
+from twisted.internet.defer import Deferred
+from twisted.protocols import policies, basic
+from twisted.python import log
+from urllib import unquote
+
+from twisted.web.http_headers import _DictHeaders, Headers
+
+protocol_version = "HTTP/1.1"
+
+_CONTINUE = 100
+SWITCHING = 101
+
+OK = 200
+CREATED = 201
+ACCEPTED = 202
+NON_AUTHORITATIVE_INFORMATION = 203
+NO_CONTENT = 204
+RESET_CONTENT = 205
+PARTIAL_CONTENT = 206
+MULTI_STATUS = 207
+
+MULTIPLE_CHOICE = 300
+MOVED_PERMANENTLY = 301
+FOUND = 302
+SEE_OTHER = 303
+NOT_MODIFIED = 304
+USE_PROXY = 305
+TEMPORARY_REDIRECT = 307
+
+BAD_REQUEST = 400
+UNAUTHORIZED = 401
+PAYMENT_REQUIRED = 402
+FORBIDDEN = 403
+NOT_FOUND = 404
+NOT_ALLOWED = 405
+NOT_ACCEPTABLE = 406
+PROXY_AUTH_REQUIRED = 407
+REQUEST_TIMEOUT = 408
+CONFLICT = 409
+GONE = 410
+LENGTH_REQUIRED = 411
+PRECONDITION_FAILED = 412
+REQUEST_ENTITY_TOO_LARGE = 413
+REQUEST_URI_TOO_LONG = 414
+UNSUPPORTED_MEDIA_TYPE = 415
+REQUESTED_RANGE_NOT_SATISFIABLE = 416
+EXPECTATION_FAILED = 417
+
+INTERNAL_SERVER_ERROR = 500
+NOT_IMPLEMENTED = 501
+BAD_GATEWAY = 502
+SERVICE_UNAVAILABLE = 503
+GATEWAY_TIMEOUT = 504
+HTTP_VERSION_NOT_SUPPORTED = 505
+INSUFFICIENT_STORAGE_SPACE = 507
+NOT_EXTENDED = 510
+
+RESPONSES = {
+ # 100
+ _CONTINUE: "Continue",
+ SWITCHING: "Switching Protocols",
+
+ # 200
+ OK: "OK",
+ CREATED: "Created",
+ ACCEPTED: "Accepted",
+ NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information",
+ NO_CONTENT: "No Content",
+ RESET_CONTENT: "Reset Content.",
+ PARTIAL_CONTENT: "Partial Content",
+ MULTI_STATUS: "Multi-Status",
+
+ # 300
+ MULTIPLE_CHOICE: "Multiple Choices",
+ MOVED_PERMANENTLY: "Moved Permanently",
+ FOUND: "Found",
+ SEE_OTHER: "See Other",
+ NOT_MODIFIED: "Not Modified",
+ USE_PROXY: "Use Proxy",
+ # 306 not defined??
+ TEMPORARY_REDIRECT: "Temporary Redirect",
+
+ # 400
+ BAD_REQUEST: "Bad Request",
+ UNAUTHORIZED: "Unauthorized",
+ PAYMENT_REQUIRED: "Payment Required",
+ FORBIDDEN: "Forbidden",
+ NOT_FOUND: "Not Found",
+ NOT_ALLOWED: "Method Not Allowed",
+ NOT_ACCEPTABLE: "Not Acceptable",
+ PROXY_AUTH_REQUIRED: "Proxy Authentication Required",
+ REQUEST_TIMEOUT: "Request Time-out",
+ CONFLICT: "Conflict",
+ GONE: "Gone",
+ LENGTH_REQUIRED: "Length Required",
+ PRECONDITION_FAILED: "Precondition Failed",
+ REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large",
+ REQUEST_URI_TOO_LONG: "Request-URI Too Long",
+ UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type",
+ REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable",
+ EXPECTATION_FAILED: "Expectation Failed",
+
+ # 500
+ INTERNAL_SERVER_ERROR: "Internal Server Error",
+ NOT_IMPLEMENTED: "Not Implemented",
+ BAD_GATEWAY: "Bad Gateway",
+ SERVICE_UNAVAILABLE: "Service Unavailable",
+ GATEWAY_TIMEOUT: "Gateway Time-out",
+ HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported",
+ INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space",
+ NOT_EXTENDED: "Not Extended"
+ }
+
+CACHED = """Magic constant returned by http.Request methods to set cache
+validation headers when the request is conditional and the value fails
+the condition."""
+
+# backwards compatability
+responses = RESPONSES
+
+
+# datetime parsing and formatting
+weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+monthname = [None,
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+weekdayname_lower = [name.lower() for name in weekdayname]
+monthname_lower = [name and name.lower() for name in monthname]
+
+def urlparse(url):
+ """
+ Parse an URL into six components.
+
+ This is similar to L{urlparse.urlparse}, but rejects C{unicode} input
+ and always produces C{str} output.
+
+ @type url: C{str}
+
+ @raise TypeError: The given url was a C{unicode} string instead of a
+ C{str}.
+
+ @rtype: six-tuple of str
+ @return: The scheme, net location, path, params, query string, and fragment
+ of the URL.
+ """
+ if isinstance(url, unicode):
+ raise TypeError("url must be str, not unicode")
+ scheme, netloc, path, params, query, fragment = _urlparse(url)
+ if isinstance(scheme, unicode):
+ scheme = scheme.encode('ascii')
+ netloc = netloc.encode('ascii')
+ path = path.encode('ascii')
+ query = query.encode('ascii')
+ fragment = fragment.encode('ascii')
+ return scheme, netloc, path, params, query, fragment
+
+
+def parse_qs(qs, keep_blank_values=0, strict_parsing=0, unquote=unquote):
+ """
+ like cgi.parse_qs, only with custom unquote function
+ """
+ d = {}
+ items = [s2 for s1 in qs.split("&") for s2 in s1.split(";")]
+ for item in items:
+ try:
+ k, v = item.split("=", 1)
+ except ValueError:
+ if strict_parsing:
+ raise
+ continue
+ if v or keep_blank_values:
+ k = unquote(k.replace("+", " "))
+ v = unquote(v.replace("+", " "))
+ if k in d:
+ d[k].append(v)
+ else:
+ d[k] = [v]
+ return d
+
+def datetimeToString(msSinceEpoch=None):
+ """
+ Convert seconds since epoch to HTTP datetime string.
+ """
+ if msSinceEpoch == None:
+ msSinceEpoch = time.time()
+ year, month, day, hh, mm, ss, wd, y, z = time.gmtime(msSinceEpoch)
+ s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
+ weekdayname[wd],
+ day, monthname[month], year,
+ hh, mm, ss)
+ return s
+
+def datetimeToLogString(msSinceEpoch=None):
+ """
+ Convert seconds since epoch to log datetime string.
+ """
+ if msSinceEpoch == None:
+ msSinceEpoch = time.time()
+ year, month, day, hh, mm, ss, wd, y, z = time.gmtime(msSinceEpoch)
+ s = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ day, monthname[month], year,
+ hh, mm, ss)
+ return s
+
+def timegm(year, month, day, hour, minute, second):
+ """
+ Convert time tuple in GMT to seconds since epoch, GMT
+ """
+ EPOCH = 1970
+ if year < EPOCH:
+ raise ValueError("Years prior to %d not supported" % (EPOCH,))
+ assert 1 <= month <= 12
+ days = 365*(year-EPOCH) + calendar.leapdays(EPOCH, year)
+ for i in range(1, month):
+ days = days + calendar.mdays[i]
+ if month > 2 and calendar.isleap(year):
+ days = days + 1
+ days = days + day - 1
+ hours = days*24 + hour
+ minutes = hours*60 + minute
+ seconds = minutes*60 + second
+ return seconds
+
+def stringToDatetime(dateString):
+ """
+ Convert an HTTP date string (one of three formats) to seconds since epoch.
+ """
+ parts = dateString.split()
+
+ if not parts[0][0:3].lower() in weekdayname_lower:
+ # Weekday is stupid. Might have been omitted.
+ try:
+ return stringToDatetime("Sun, "+dateString)
+ except ValueError:
+ # Guess not.
+ pass
+
+ partlen = len(parts)
+ if (partlen == 5 or partlen == 6) and parts[1].isdigit():
+ # 1st date format: Sun, 06 Nov 1994 08:49:37 GMT
+ # (Note: "GMT" is literal, not a variable timezone)
+ # (also handles without "GMT")
+ # This is the normal format
+ day = parts[1]
+ month = parts[2]
+ year = parts[3]
+ time = parts[4]
+ elif (partlen == 3 or partlen == 4) and parts[1].find('-') != -1:
+ # 2nd date format: Sunday, 06-Nov-94 08:49:37 GMT
+ # (Note: "GMT" is literal, not a variable timezone)
+ # (also handles without without "GMT")
+ # Two digit year, yucko.
+ day, month, year = parts[1].split('-')
+ time = parts[2]
+ year=int(year)
+ if year < 69:
+ year = year + 2000
+ elif year < 100:
+ year = year + 1900
+ elif len(parts) == 5:
+ # 3rd date format: Sun Nov 6 08:49:37 1994
+ # ANSI C asctime() format.
+ day = parts[2]
+ month = parts[1]
+ year = parts[4]
+ time = parts[3]
+ else:
+ raise ValueError("Unknown datetime format %r" % dateString)
+
+ day = int(day)
+ month = int(monthname_lower.index(month.lower()))
+ year = int(year)
+ hour, min, sec = map(int, time.split(':'))
+ return int(timegm(year, month, day, hour, min, sec))
+
+def toChunk(data):
+ """
+ Convert string to a chunk.
+
+ @returns: a tuple of strings representing the chunked encoding of data
+ """
+ return ("%x\r\n" % len(data), data, "\r\n")
+
+def fromChunk(data):
+ """
+ Convert chunk to string.
+
+ @returns: tuple (result, remaining), may raise ValueError.
+ """
+ prefix, rest = data.split('\r\n', 1)
+ length = int(prefix, 16)
+ if length < 0:
+ raise ValueError("Chunk length must be >= 0, not %d" % (length,))
+ if not rest[length:length + 2] == '\r\n':
+ raise ValueError, "chunk must end with CRLF"
+ return rest[:length], rest[length + 2:]
+
+
+def parseContentRange(header):
+ """
+ Parse a content-range header into (start, end, realLength).
+
+ realLength might be None if real length is not known ('*').
+ """
+ kind, other = header.strip().split()
+ if kind.lower() != "bytes":
+ raise ValueError, "a range of type %r is not supported"
+ startend, realLength = other.split("/")
+ start, end = map(int, startend.split("-"))
+ if realLength == "*":
+ realLength = None
+ else:
+ realLength = int(realLength)
+ return (start, end, realLength)
+
+
+
+class StringTransport:
+ """
+ I am a StringIO wrapper that conforms for the transport API. I support
+ the `writeSequence' method.
+ """
+ def __init__(self):
+ self.s = StringIO()
+ def writeSequence(self, seq):
+ self.s.write(''.join(seq))
+ def __getattr__(self, attr):
+ return getattr(self.__dict__['s'], attr)
+
+
+class HTTPClient(basic.LineReceiver):
+ """
+ A client for HTTP 1.0.
+
+ Notes:
+ You probably want to send a 'Host' header with the name of the site you're
+ connecting to, in order to not break name based virtual hosting.
+
+ @ivar length: The length of the request body in bytes.
+ @type length: C{int}
+
+ @ivar firstLine: Are we waiting for the first header line?
+ @type firstLine: C{bool}
+
+ @ivar __buffer: The buffer that stores the response to the HTTP request.
+ @type __buffer: A C{StringIO} object.
+
+ @ivar _header: Part or all of an HTTP request header.
+ @type _header: C{str}
+ """
+ length = None
+ firstLine = True
+ __buffer = None
+ _header = ""
+
+ def sendCommand(self, command, path):
+ self.transport.write('%s %s HTTP/1.0\r\n' % (command, path))
+
+ def sendHeader(self, name, value):
+ self.transport.write('%s: %s\r\n' % (name, value))
+
+ def endHeaders(self):
+ self.transport.write('\r\n')
+
+
+ def extractHeader(self, header):
+ """
+ Given a complete HTTP header, extract the field name and value and
+ process the header.
+
+ @param header: a complete HTTP request header of the form
+ 'field-name: value'.
+ @type header: C{str}
+ """
+ key, val = header.split(':', 1)
+ val = val.lstrip()
+ self.handleHeader(key, val)
+ if key.lower() == 'content-length':
+ self.length = int(val)
+
+
+ def lineReceived(self, line):
+ """
+ Parse the status line and headers for an HTTP request.
+
+ @param line: Part of an HTTP request header. Request bodies are parsed
+ in L{rawDataReceived}.
+ @type line: C{str}
+ """
+ if self.firstLine:
+ self.firstLine = False
+ l = line.split(None, 2)
+ version = l[0]
+ status = l[1]
+ try:
+ message = l[2]
+ except IndexError:
+ # sometimes there is no message
+ message = ""
+ self.handleStatus(version, status, message)
+ return
+ if not line:
+ if self._header != "":
+ # Only extract headers if there are any
+ self.extractHeader(self._header)
+ self.__buffer = StringIO()
+ self.handleEndHeaders()
+ self.setRawMode()
+ return
+
+ if line.startswith('\t') or line.startswith(' '):
+ # This line is part of a multiline header. According to RFC 822, in
+ # "unfolding" multiline headers you do not strip the leading
+ # whitespace on the continuing line.
+ self._header = self._header + line
+ elif self._header:
+ # This line starts a new header, so process the previous one.
+ self.extractHeader(self._header)
+ self._header = line
+ else: # First header
+ self._header = line
+
+
+ def connectionLost(self, reason):
+ self.handleResponseEnd()
+
+ def handleResponseEnd(self):
+ """
+ The response has been completely received.
+
+ This callback may be invoked more than once per request.
+ """
+ if self.__buffer is not None:
+ b = self.__buffer.getvalue()
+ self.__buffer = None
+ self.handleResponse(b)
+
+ def handleResponsePart(self, data):
+ self.__buffer.write(data)
+
+ def connectionMade(self):
+ pass
+
+ def handleStatus(self, version, status, message):
+ """
+ Called when the status-line is received.
+
+ @param version: e.g. 'HTTP/1.0'
+ @param status: e.g. '200'
+ @type status: C{str}
+ @param message: e.g. 'OK'
+ """
+
+ def handleHeader(self, key, val):
+ """
+ Called every time a header is received.
+ """
+
+ def handleEndHeaders(self):
+ """
+ Called when all headers have been received.
+ """
+
+
+ def rawDataReceived(self, data):
+ if self.length is not None:
+ data, rest = data[:self.length], data[self.length:]
+ self.length -= len(data)
+ else:
+ rest = ''
+ self.handleResponsePart(data)
+ if self.length == 0:
+ self.handleResponseEnd()
+ self.setLineMode(rest)
+
+
+
+# response codes that must have empty bodies
+NO_BODY_CODES = (204, 304)
+
+class Request:
+ """
+ A HTTP request.
+
+ Subclasses should override the process() method to determine how
+ the request will be processed.
+
+ @ivar method: The HTTP method that was used.
+ @ivar uri: The full URI that was requested (includes arguments).
+ @ivar path: The path only (arguments not included).
+ @ivar args: All of the arguments, including URL and POST arguments.
+ @type args: A mapping of strings (the argument names) to lists of values.
+ i.e., ?foo=bar&foo=baz&quux=spam results in
+ {'foo': ['bar', 'baz'], 'quux': ['spam']}.
+
+ @type requestHeaders: L{http_headers.Headers}
+ @ivar requestHeaders: All received HTTP request headers.
+
+ @ivar received_headers: Backwards-compatibility access to
+ C{requestHeaders}. Use C{requestHeaders} instead. C{received_headers}
+ behaves mostly like a C{dict} and does not provide access to all header
+ values.
+
+ @type responseHeaders: L{http_headers.Headers}
+ @ivar responseHeaders: All HTTP response headers to be sent.
+
+ @ivar headers: Backwards-compatibility access to C{responseHeaders}. Use
+ C{responseHeaders} instead. C{headers} behaves mostly like a C{dict}
+ and does not provide access to all header values nor does it allow
+ multiple values for one header to be set.
+
+ @ivar notifications: A C{list} of L{Deferred}s which are waiting for
+ notification that the response to this request has been finished
+ (successfully or with an error). Don't use this attribute directly,
+ instead use the L{Request.notifyFinish} method.
+
+ @ivar _disconnected: A flag which is C{False} until the connection over
+ which this request was received is closed and which is C{True} after
+ that.
+ @type _disconnected: C{bool}
+ """
+ implements(interfaces.IConsumer)
+
+ producer = None
+ finished = 0
+ code = OK
+ code_message = RESPONSES[OK]
+ method = "(no method yet)"
+ clientproto = "(no clientproto yet)"
+ uri = "(no uri yet)"
+ startedWriting = 0
+ chunked = 0
+ sentLength = 0 # content-length of response, or total bytes sent via chunking
+ etag = None
+ lastModified = None
+ args = None
+ path = None
+ content = None
+ _forceSSL = 0
+ _disconnected = False
+
+ def __init__(self, channel, queued):
+ """
+ @param channel: the channel we're connected to.
+ @param queued: are we in the request queue, or can we start writing to
+ the transport?
+ """
+ self.notifications = []
+ self.channel = channel
+ self.queued = queued
+ self.requestHeaders = Headers()
+ self.received_cookies = {}
+ self.responseHeaders = Headers()
+ self.cookies = [] # outgoing cookies
+
+ if queued:
+ self.transport = StringTransport()
+ else:
+ self.transport = self.channel.transport
+
+
+ def __setattr__(self, name, value):
+ """
+ Support assignment of C{dict} instances to C{received_headers} for
+ backwards-compatibility.
+ """
+ if name == 'received_headers':
+ # A property would be nice, but Request is classic.
+ self.requestHeaders = headers = Headers()
+ for k, v in value.iteritems():
+ headers.setRawHeaders(k, [v])
+ elif name == 'requestHeaders':
+ self.__dict__[name] = value
+ self.__dict__['received_headers'] = _DictHeaders(value)
+ elif name == 'headers':
+ self.responseHeaders = headers = Headers()
+ for k, v in value.iteritems():
+ headers.setRawHeaders(k, [v])
+ elif name == 'responseHeaders':
+ self.__dict__[name] = value
+ self.__dict__['headers'] = _DictHeaders(value)
+ else:
+ self.__dict__[name] = value
+
+
+ def _cleanup(self):
+ """
+ Called when have finished responding and are no longer queued.
+ """
+ if self.producer:
+ log.err(RuntimeError("Producer was not unregistered for %s" % self.uri))
+ self.unregisterProducer()
+ self.channel.requestDone(self)
+ del self.channel
+ try:
+ self.content.close()
+ except OSError:
+ # win32 suckiness, no idea why it does this
+ pass
+ del self.content
+ for d in self.notifications:
+ d.callback(None)
+ self.notifications = []
+
+ # methods for channel - end users should not use these
+
+ def noLongerQueued(self):
+ """
+ Notify the object that it is no longer queued.
+
+ We start writing whatever data we have to the transport, etc.
+
+ This method is not intended for users.
+ """
+ if not self.queued:
+ raise RuntimeError, "noLongerQueued() got called unnecessarily."
+
+ self.queued = 0
+
+ # set transport to real one and send any buffer data
+ data = self.transport.getvalue()
+ self.transport = self.channel.transport
+ if data:
+ self.transport.write(data)
+
+ # if we have producer, register it with transport
+ if (self.producer is not None) and not self.finished:
+ self.transport.registerProducer(self.producer, self.streamingProducer)
+
+ # if we're finished, clean up
+ if self.finished:
+ self._cleanup()
+
+ def gotLength(self, length):
+ """
+ Called when HTTP channel got length of content in this request.
+
+ This method is not intended for users.
+
+ @param length: The length of the request body, as indicated by the
+ request headers. C{None} if the request headers do not indicate a
+ length.
+ """
+ if length is not None and length < 100000:
+ self.content = StringIO()
+ else:
+ self.content = tempfile.TemporaryFile()
+
+
+ def parseCookies(self):
+ """
+ Parse cookie headers.
+
+ This method is not intended for users.
+ """
+ cookieheaders = self.requestHeaders.getRawHeaders("cookie")
+
+ if cookieheaders is None:
+ return
+
+ for cookietxt in cookieheaders:
+ if cookietxt:
+ for cook in cookietxt.split(';'):
+ cook = cook.lstrip()
+ try:
+ k, v = cook.split('=', 1)
+ self.received_cookies[k] = v
+ except ValueError:
+ pass
+
+
+ def handleContentChunk(self, data):
+ """
+ Write a chunk of data.
+
+ This method is not intended for users.
+ """
+ self.content.write(data)
+
+
+ def requestReceived(self, command, path, version):
+ """
+ Called by channel when all data has been received.
+
+ This method is not intended for users.
+
+ @type command: C{str}
+ @param command: The HTTP verb of this request. This has the case
+ supplied by the client (eg, it maybe "get" rather than "GET").
+
+ @type path: C{str}
+ @param path: The URI of this request.
+
+ @type version: C{str}
+ @param version: The HTTP version of this request.
+ """
+ self.content.seek(0,0)
+ self.args = {}
+ self.stack = []
+
+ self.method, self.uri = command, path
+ self.clientproto = version
+ x = self.uri.split('?', 1)
+
+ if len(x) == 1:
+ self.path = self.uri
+ else:
+ self.path, argstring = x
+ self.args = parse_qs(argstring, 1)
+
+ # cache the client and server information, we'll need this later to be
+ # serialized and sent with the request so CGIs will work remotely
+ self.client = self.channel.transport.getPeer()
+ self.host = self.channel.transport.getHost()
+
+ # Argument processing
+ args = self.args
+ ctype = self.requestHeaders.getRawHeaders('content-type')
+ if ctype is not None:
+ ctype = ctype[0]
+
+ if self.method == "POST" and ctype:
+ mfd = 'multipart/form-data'
+ key, pdict = cgi.parse_header(ctype)
+ if key == 'application/x-www-form-urlencoded':
+ args.update(parse_qs(self.content.read(), 1))
+ elif key == mfd:
+ try:
+ args.update(cgi.parse_multipart(self.content, pdict))
+ except KeyError, e:
+ if e.args[0] == 'content-disposition':
+ # Parse_multipart can't cope with missing
+ # content-dispostion headers in multipart/form-data
+ # parts, so we catch the exception and tell the client
+ # it was a bad request.
+ self.channel.transport.write(
+ "HTTP/1.1 400 Bad Request\r\n\r\n")
+ self.channel.transport.loseConnection()
+ return
+ raise
+ self.content.seek(0, 0)
+
+ self.process()
+
+
+ def __repr__(self):
+ return '<%s %s %s>'% (self.method, self.uri, self.clientproto)
+
+ def process(self):
+ """
+ Override in subclasses.
+
+ This method is not intended for users.
+ """
+ pass
+
+
+ # consumer interface
+
+ def registerProducer(self, producer, streaming):
+ """
+ Register a producer.
+ """
+ if self.producer:
+ raise ValueError, "registering producer %s before previous one (%s) was unregistered" % (producer, self.producer)
+
+ self.streamingProducer = streaming
+ self.producer = producer
+
+ if self.queued:
+ if streaming:
+ producer.pauseProducing()
+ else:
+ self.transport.registerProducer(producer, streaming)
+
+ def unregisterProducer(self):
+ """
+ Unregister the producer.
+ """
+ if not self.queued:
+ self.transport.unregisterProducer()
+ self.producer = None
+
+ # private http response methods
+
+ def _sendError(self, code, resp=''):
+ self.transport.write('%s %s %s\r\n\r\n' % (self.clientproto, code, resp))
+
+
+ # The following is the public interface that people should be
+ # writing to.
+ def getHeader(self, key):
+ """
+ Get an HTTP request header.
+
+ @type key: C{str}
+ @param key: The name of the header to get the value of.
+
+ @rtype: C{str} or C{NoneType}
+ @return: The value of the specified header, or C{None} if that header
+ was not present in the request.
+ """
+ value = self.requestHeaders.getRawHeaders(key)
+ if value is not None:
+ return value[-1]
+
+
+ def getCookie(self, key):
+ """
+ Get a cookie that was sent from the network.
+ """
+ return self.received_cookies.get(key)
+
+
+ def notifyFinish(self):
+ """
+ Notify when the response to this request has finished.
+
+ @rtype: L{Deferred}
+
+ @return: A L{Deferred} which will be triggered when the request is
+ finished -- with a C{None} value if the request finishes
+ successfully or with an error if the request is interrupted by an
+ error (for example, the client closing the connection prematurely).
+ """
+ self.notifications.append(Deferred())
+ return self.notifications[-1]
+
+
+ def finish(self):
+ """
+ Indicate that all response data has been written to this L{Request}.
+ """
+ if self._disconnected:
+ raise RuntimeError(
+ "Request.finish called on a request after its connection was lost; "
+ "use Request.notifyFinish to keep track of this.")
+ if self.finished:
+ warnings.warn("Warning! request.finish called twice.", stacklevel=2)
+ return
+
+ if not self.startedWriting:
+ # write headers
+ self.write('')
+
+ if self.chunked:
+ # write last chunk and closing CRLF
+ self.transport.write("0\r\n\r\n")
+
+ # log request
+ if hasattr(self.channel, "factory"):
+ self.channel.factory.log(self)
+
+ self.finished = 1
+ if not self.queued:
+ self._cleanup()
+
+
+ def write(self, data):
+ """
+ Write some data as a result of an HTTP request. The first
+ time this is called, it writes out response data.
+
+ @type data: C{str}
+ @param data: Some bytes to be sent as part of the response body.
+ """
+ if self.finished:
+ raise RuntimeError('Request.write called on a request after '
+ 'Request.finish was called.')
+ if not self.startedWriting:
+ self.startedWriting = 1
+ version = self.clientproto
+ l = []
+ l.append('%s %s %s\r\n' % (version, self.code,
+ self.code_message))
+ # if we don't have a content length, we send data in
+ # chunked mode, so that we can support pipelining in
+ # persistent connections.
+ if ((version == "HTTP/1.1") and
+ (self.responseHeaders.getRawHeaders('content-length') is None) and
+ self.method != "HEAD" and self.code not in NO_BODY_CODES):
+ l.append("%s: %s\r\n" % ('Transfer-Encoding', 'chunked'))
+ self.chunked = 1
+
+ if self.lastModified is not None:
+ if self.responseHeaders.hasHeader('last-modified'):
+ log.msg("Warning: last-modified specified both in"
+ " header list and lastModified attribute.")
+ else:
+ self.responseHeaders.setRawHeaders(
+ 'last-modified',
+ [datetimeToString(self.lastModified)])
+
+ if self.etag is not None:
+ self.responseHeaders.setRawHeaders('ETag', [self.etag])
+
+ for name, values in self.responseHeaders.getAllRawHeaders():
+ for value in values:
+ l.append("%s: %s\r\n" % (name, value))
+
+ for cookie in self.cookies:
+ l.append('%s: %s\r\n' % ("Set-Cookie", cookie))
+
+ l.append("\r\n")
+
+ self.transport.writeSequence(l)
+
+ # if this is a "HEAD" request, we shouldn't return any data
+ if self.method == "HEAD":
+ self.write = lambda data: None
+ return
+
+ # for certain result codes, we should never return any data
+ if self.code in NO_BODY_CODES:
+ self.write = lambda data: None
+ return
+
+ self.sentLength = self.sentLength + len(data)
+ if data:
+ if self.chunked:
+ self.transport.writeSequence(toChunk(data))
+ else:
+ self.transport.write(data)
+
+ def addCookie(self, k, v, expires=None, domain=None, path=None, max_age=None, comment=None, secure=None):
+ """
+ Set an outgoing HTTP cookie.
+
+ In general, you should consider using sessions instead of cookies, see
+ L{twisted.web.server.Request.getSession} and the
+ L{twisted.web.server.Session} class for details.
+ """
+ cookie = '%s=%s' % (k, v)
+ if expires is not None:
+ cookie = cookie +"; Expires=%s" % expires
+ if domain is not None:
+ cookie = cookie +"; Domain=%s" % domain
+ if path is not None:
+ cookie = cookie +"; Path=%s" % path
+ if max_age is not None:
+ cookie = cookie +"; Max-Age=%s" % max_age
+ if comment is not None:
+ cookie = cookie +"; Comment=%s" % comment
+ if secure:
+ cookie = cookie +"; Secure"
+ self.cookies.append(cookie)
+
+ def setResponseCode(self, code, message=None):
+ """
+ Set the HTTP response code.
+ """
+ if not isinstance(code, (int, long)):
+ raise TypeError("HTTP response code must be int or long")
+ self.code = code
+ if message:
+ self.code_message = message
+ else:
+ self.code_message = RESPONSES.get(code, "Unknown Status")
+
+
+ def setHeader(self, name, value):
+ """
+ Set an HTTP response header. Overrides any previously set values for
+ this header.
+
+ @type name: C{str}
+ @param name: The name of the header for which to set the value.
+
+ @type value: C{str}
+ @param value: The value to set for the named header.
+ """
+ self.responseHeaders.setRawHeaders(name, [value])
+
+
+ def redirect(self, url):
+ """
+ Utility function that does a redirect.
+
+ The request should have finish() called after this.
+ """
+ self.setResponseCode(FOUND)
+ self.setHeader("location", url)
+
+
+ def setLastModified(self, when):
+ """
+ Set the C{Last-Modified} time for the response to this request.
+
+ If I am called more than once, I ignore attempts to set
+ Last-Modified earlier, only replacing the Last-Modified time
+ if it is to a later value.
+
+ If I am a conditional request, I may modify my response code
+ to L{NOT_MODIFIED} if appropriate for the time given.
+
+ @param when: The last time the resource being returned was
+ modified, in seconds since the epoch.
+ @type when: number
+ @return: If I am a C{If-Modified-Since} conditional request and
+ the time given is not newer than the condition, I return
+ L{http.CACHED<CACHED>} to indicate that you should write no
+ body. Otherwise, I return a false value.
+ """
+ # time.time() may be a float, but the HTTP-date strings are
+ # only good for whole seconds.
+ when = long(math.ceil(when))
+ if (not self.lastModified) or (self.lastModified < when):
+ self.lastModified = when
+
+ modifiedSince = self.getHeader('if-modified-since')
+ if modifiedSince:
+ firstPart = modifiedSince.split(';', 1)[0]
+ try:
+ modifiedSince = stringToDatetime(firstPart)
+ except ValueError:
+ return None
+ if modifiedSince >= when:
+ self.setResponseCode(NOT_MODIFIED)
+ return CACHED
+ return None
+
+ def setETag(self, etag):
+ """
+ Set an C{entity tag} for the outgoing response.
+
+ That's \"entity tag\" as in the HTTP/1.1 C{ETag} header, \"used
+ for comparing two or more entities from the same requested
+ resource.\"
+
+ If I am a conditional request, I may modify my response code
+ to L{NOT_MODIFIED} or L{PRECONDITION_FAILED}, if appropriate
+ for the tag given.
+
+ @param etag: The entity tag for the resource being returned.
+ @type etag: string
+ @return: If I am a C{If-None-Match} conditional request and
+ the tag matches one in the request, I return
+ L{http.CACHED<CACHED>} to indicate that you should write
+ no body. Otherwise, I return a false value.
+ """
+ if etag:
+ self.etag = etag
+
+ tags = self.getHeader("if-none-match")
+ if tags:
+ tags = tags.split()
+ if (etag in tags) or ('*' in tags):
+ self.setResponseCode(((self.method in ("HEAD", "GET"))
+ and NOT_MODIFIED)
+ or PRECONDITION_FAILED)
+ return CACHED
+ return None
+
+
+ def getAllHeaders(self):
+ """
+ Return dictionary mapping the names of all received headers to the last
+ value received for each.
+
+ Since this method does not return all header information,
+ C{self.requestHeaders.getAllRawHeaders()} may be preferred.
+ """
+ headers = {}
+ for k, v in self.requestHeaders.getAllRawHeaders():
+ headers[k.lower()] = v[-1]
+ return headers
+
+
+ def getRequestHostname(self):
+ """
+ Get the hostname that the user passed in to the request.
+
+ This will either use the Host: header (if it is available) or the
+ host we are listening on if the header is unavailable.
+
+ @returns: the requested hostname
+ @rtype: C{str}
+ """
+ # XXX This method probably has no unit tests. I changed it a ton and
+ # nothing failed.
+ host = self.getHeader('host')
+ if host:
+ return host.split(':', 1)[0]
+ return self.getHost().host
+
+
+ def getHost(self):
+ """
+ Get my originally requesting transport's host.
+
+ Don't rely on the 'transport' attribute, since Request objects may be
+ copied remotely. For information on this method's return value, see
+ twisted.internet.tcp.Port.
+ """
+ return self.host
+
+ def setHost(self, host, port, ssl=0):
+ """
+ Change the host and port the request thinks it's using.
+
+ This method is useful for working with reverse HTTP proxies (e.g.
+ both Squid and Apache's mod_proxy can do this), when the address
+ the HTTP client is using is different than the one we're listening on.
+
+ For example, Apache may be listening on https://www.example.com, and then
+ forwarding requests to http://localhost:8080, but we don't want HTML produced
+ by Twisted to say 'http://localhost:8080', they should say 'https://www.example.com',
+ so we do::
+
+ request.setHost('www.example.com', 443, ssl=1)
+
+ @type host: C{str}
+ @param host: The value to which to change the host header.
+
+ @type ssl: C{bool}
+ @param ssl: A flag which, if C{True}, indicates that the request is
+ considered secure (if C{True}, L{isSecure} will return C{True}).
+ """
+ self._forceSSL = ssl # set first so isSecure will work
+ if self.isSecure():
+ default = 443
+ else:
+ default = 80
+ if port == default:
+ hostHeader = host
+ else:
+ hostHeader = '%s:%d' % (host, port)
+ self.requestHeaders.setRawHeaders("host", [hostHeader])
+ self.host = address.IPv4Address("TCP", host, port)
+
+
+ def getClientIP(self):
+ """
+ Return the IP address of the client who submitted this request.
+
+ @returns: the client IP address
+ @rtype: C{str}
+ """
+ if isinstance(self.client, address.IPv4Address):
+ return self.client.host
+ else:
+ return None
+
+ def isSecure(self):
+ """
+ Return True if this request is using a secure transport.
+
+ Normally this method returns True if this request's HTTPChannel
+ instance is using a transport that implements ISSLTransport.
+
+ This will also return True if setHost() has been called
+ with ssl=True.
+
+ @returns: True if this request is secure
+ @rtype: C{bool}
+ """
+ if self._forceSSL:
+ return True
+ transport = getattr(getattr(self, 'channel', None), 'transport', None)
+ if interfaces.ISSLTransport(transport, None) is not None:
+ return True
+ return False
+
+ def _authorize(self):
+ # Authorization, (mostly) per the RFC
+ try:
+ authh = self.getHeader("Authorization")
+ if not authh:
+ self.user = self.password = ''
+ return
+ bas, upw = authh.split()
+ if bas.lower() != "basic":
+ raise ValueError
+ upw = base64.decodestring(upw)
+ self.user, self.password = upw.split(':', 1)
+ except (binascii.Error, ValueError):
+ self.user = self.password = ""
+ except:
+ log.err()
+ self.user = self.password = ""
+
+ def getUser(self):
+ """
+ Return the HTTP user sent with this request, if any.
+
+ If no user was supplied, return the empty string.
+
+ @returns: the HTTP user, if any
+ @rtype: C{str}
+ """
+ try:
+ return self.user
+ except:
+ pass
+ self._authorize()
+ return self.user
+
+ def getPassword(self):
+ """
+ Return the HTTP password sent with this request, if any.
+
+ If no password was supplied, return the empty string.
+
+ @returns: the HTTP password, if any
+ @rtype: C{str}
+ """
+ try:
+ return self.password
+ except:
+ pass
+ self._authorize()
+ return self.password
+
+ def getClient(self):
+ if self.client.type != 'TCP':
+ return None
+ host = self.client.host
+ try:
+ name, names, addresses = socket.gethostbyaddr(host)
+ except socket.error:
+ return host
+ names.insert(0, name)
+ for name in names:
+ if '.' in name:
+ return name
+ return names[0]
+
+
+ def connectionLost(self, reason):
+ """
+ There is no longer a connection for this request to respond over.
+ Clean up anything which can't be useful anymore.
+ """
+ self._disconnected = True
+ self.channel = None
+ if self.content is not None:
+ self.content.close()
+ for d in self.notifications:
+ d.errback(reason)
+ self.notifications = []
+
+
+
+class _DataLoss(Exception):
+ """
+ L{_DataLoss} indicates that not all of a message body was received. This
+ is only one of several possible exceptions which may indicate that data
+ was lost. Because of this, it should not be checked for by
+ specifically; any unexpected exception should be treated as having
+ caused data loss.
+ """
+
+
+
+class PotentialDataLoss(Exception):
+ """
+ L{PotentialDataLoss} may be raised by a transfer encoding decoder's
+ C{noMoreData} method to indicate that it cannot be determined if the
+ entire response body has been delivered. This only occurs when making
+ requests to HTTP servers which do not set I{Content-Length} or a
+ I{Transfer-Encoding} in the response because in this case the end of the
+ response is indicated by the connection being closed, an event which may
+ also be due to a transient network problem or other error.
+ """
+
+
+
+class _IdentityTransferDecoder(object):
+ """
+ Protocol for accumulating bytes up to a specified length. This handles the
+ case where no I{Transfer-Encoding} is specified.
+
+ @ivar contentLength: Counter keeping track of how many more bytes there are
+ to receive.
+
+ @ivar dataCallback: A one-argument callable which will be invoked each
+ time application data is received.
+
+ @ivar finishCallback: A one-argument callable which will be invoked when
+ the terminal chunk is received. It will be invoked with all bytes
+ which were delivered to this protocol which came after the terminal
+ chunk.
+ """
+ def __init__(self, contentLength, dataCallback, finishCallback):
+ self.contentLength = contentLength
+ self.dataCallback = dataCallback
+ self.finishCallback = finishCallback
+
+
+ def dataReceived(self, data):
+ """
+ Interpret the next chunk of bytes received. Either deliver them to the
+ data callback or invoke the finish callback if enough bytes have been
+ received.
+
+ @raise RuntimeError: If the finish callback has already been invoked
+ during a previous call to this methood.
+ """
+ if self.dataCallback is None:
+ raise RuntimeError(
+ "_IdentityTransferDecoder cannot decode data after finishing")
+
+ if self.contentLength is None:
+ self.dataCallback(data)
+ elif len(data) < self.contentLength:
+ self.contentLength -= len(data)
+ self.dataCallback(data)
+ else:
+ # Make the state consistent before invoking any code belonging to
+ # anyone else in case noMoreData ends up being called beneath this
+ # stack frame.
+ contentLength = self.contentLength
+ dataCallback = self.dataCallback
+ finishCallback = self.finishCallback
+ self.dataCallback = self.finishCallback = None
+ self.contentLength = 0
+
+ dataCallback(data[:contentLength])
+ finishCallback(data[contentLength:])
+
+
+ def noMoreData(self):
+ """
+ All data which will be delivered to this decoder has been. Check to
+ make sure as much data as was expected has been received.
+
+ @raise PotentialDataLoss: If the content length is unknown.
+ @raise _DataLoss: If the content length is known and fewer than that
+ many bytes have been delivered.
+
+ @return: C{None}
+ """
+ finishCallback = self.finishCallback
+ self.dataCallback = self.finishCallback = None
+ if self.contentLength is None:
+ finishCallback('')
+ raise PotentialDataLoss()
+ elif self.contentLength != 0:
+ raise _DataLoss()
+
+
+
+class _ChunkedTransferDecoder(object):
+ """
+ Protocol for decoding I{chunked} Transfer-Encoding, as defined by RFC 2616,
+ section 3.6.1. This protocol can interpret the contents of a request or
+ response body which uses the I{chunked} Transfer-Encoding. It cannot
+ interpret any of the rest of the HTTP protocol.
+
+ It may make sense for _ChunkedTransferDecoder to be an actual IProtocol
+ implementation. Currently, the only user of this class will only ever
+ call dataReceived on it. However, it might be an improvement if the
+ user could connect this to a transport and deliver connection lost
+ notification. This way, `dataCallback` becomes `self.transport.write`
+ and perhaps `finishCallback` becomes `self.transport.loseConnection()`
+ (although I'm not sure where the extra data goes in that case). This
+ could also allow this object to indicate to the receiver of data that
+ the stream was not completely received, an error case which should be
+ noticed. -exarkun
+
+ @ivar dataCallback: A one-argument callable which will be invoked each
+ time application data is received.
+
+ @ivar finishCallback: A one-argument callable which will be invoked when
+ the terminal chunk is received. It will be invoked with all bytes
+ which were delivered to this protocol which came after the terminal
+ chunk.
+
+ @ivar length: Counter keeping track of how many more bytes in a chunk there
+ are to receive.
+
+ @ivar state: One of C{'chunk-length'}, C{'trailer'}, C{'body'}, or
+ C{'finished'}. For C{'chunk-length'}, data for the chunk length line
+ is currently being read. For C{'trailer'}, the CR LF pair which
+ follows each chunk is being read. For C{'body'}, the contents of a
+ chunk are being read. For C{'finished'}, the last chunk has been
+ completely read and no more input is valid.
+
+ @ivar finish: A flag indicating that the last chunk has been started. When
+ it finishes, the state will change to C{'finished'} and no more data
+ will be accepted.
+ """
+ state = 'chunk-length'
+ finish = False
+
+ def __init__(self, dataCallback, finishCallback):
+ self.dataCallback = dataCallback
+ self.finishCallback = finishCallback
+ self._buffer = ''
+
+
+ def dataReceived(self, data):
+ """
+ Interpret data from a request or response body which uses the
+ I{chunked} Transfer-Encoding.
+ """
+ data = self._buffer + data
+ self._buffer = ''
+ while data:
+ if self.state == 'chunk-length':
+ if '\r\n' in data:
+ line, rest = data.split('\r\n', 1)
+ parts = line.split(';')
+ self.length = int(parts[0], 16)
+ if self.length == 0:
+ self.state = 'trailer'
+ self.finish = True
+ else:
+ self.state = 'body'
+ data = rest
+ else:
+ self._buffer = data
+ data = ''
+ elif self.state == 'trailer':
+ if data.startswith('\r\n'):
+ data = data[2:]
+ if self.finish:
+ self.state = 'finished'
+ self.finishCallback(data)
+ data = ''
+ else:
+ self.state = 'chunk-length'
+ else:
+ self._buffer = data
+ data = ''
+ elif self.state == 'body':
+ if len(data) >= self.length:
+ chunk, data = data[:self.length], data[self.length:]
+ self.dataCallback(chunk)
+ self.state = 'trailer'
+ elif len(data) < self.length:
+ self.length -= len(data)
+ self.dataCallback(data)
+ data = ''
+ elif self.state == 'finished':
+ raise RuntimeError(
+ "_ChunkedTransferDecoder.dataReceived called after last "
+ "chunk was processed")
+
+
+ def noMoreData(self):
+ """
+ Verify that all data has been received. If it has not been, raise
+ L{_DataLoss}.
+ """
+ if self.state != 'finished':
+ raise _DataLoss(
+ "Chunked decoder in %r state, still expecting more data to "
+ "get to finished state." % (self.state,))
+
+
+
+class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
+ """
+ A receiver for HTTP requests.
+
+ @ivar _transferDecoder: C{None} or an instance of
+ L{_ChunkedTransferDecoder} if the request body uses the I{chunked}
+ Transfer-Encoding.
+ """
+
+ maxHeaders = 500 # max number of headers allowed per request
+
+ length = 0
+ persistent = 1
+ __header = ''
+ __first_line = 1
+ __content = None
+
+ # set in instances or subclasses
+ requestFactory = Request
+
+ _savedTimeOut = None
+ _receivedHeaderCount = 0
+
+ def __init__(self):
+ # the request queue
+ self.requests = []
+ self._transferDecoder = None
+
+
+ def connectionMade(self):
+ self.setTimeout(self.timeOut)
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+
+ if self.__first_line:
+ # if this connection is not persistent, drop any data which
+ # the client (illegally) sent after the last request.
+ if not self.persistent:
+ self.dataReceived = self.lineReceived = lambda *args: None
+ return
+
+ # IE sends an extraneous empty line (\r\n) after a POST request;
+ # eat up such a line, but only ONCE
+ if not line and self.__first_line == 1:
+ self.__first_line = 2
+ return
+
+ # create a new Request object
+ request = self.requestFactory(self, len(self.requests))
+ self.requests.append(request)
+
+ self.__first_line = 0
+ parts = line.split()
+ if len(parts) != 3:
+ self.transport.write("HTTP/1.1 400 Bad Request\r\n\r\n")
+ self.transport.loseConnection()
+ return
+ command, request, version = parts
+ self._command = command
+ self._path = request
+ self._version = version
+ elif line == '':
+ if self.__header:
+ self.headerReceived(self.__header)
+ self.__header = ''
+ self.allHeadersReceived()
+ if self.length == 0:
+ self.allContentReceived()
+ else:
+ self.setRawMode()
+ elif line[0] in ' \t':
+ self.__header = self.__header+'\n'+line
+ else:
+ if self.__header:
+ self.headerReceived(self.__header)
+ self.__header = line
+
+
+ def _finishRequestBody(self, data):
+ self.allContentReceived()
+ self.setLineMode(data)
+
+
+ def headerReceived(self, line):
+ """
+ Do pre-processing (for content-length) and store this header away.
+ Enforce the per-request header limit.
+
+ @type line: C{str}
+ @param line: A line from the header section of a request, excluding the
+ line delimiter.
+ """
+ header, data = line.split(':', 1)
+ header = header.lower()
+ data = data.strip()
+ if header == 'content-length':
+ self.length = int(data)
+ self._transferDecoder = _IdentityTransferDecoder(
+ self.length, self.requests[-1].handleContentChunk, self._finishRequestBody)
+ elif header == 'transfer-encoding' and data.lower() == 'chunked':
+ self.length = None
+ self._transferDecoder = _ChunkedTransferDecoder(
+ self.requests[-1].handleContentChunk, self._finishRequestBody)
+ reqHeaders = self.requests[-1].requestHeaders
+ values = reqHeaders.getRawHeaders(header)
+ if values is not None:
+ values.append(data)
+ else:
+ reqHeaders.setRawHeaders(header, [data])
+
+ self._receivedHeaderCount += 1
+ if self._receivedHeaderCount > self.maxHeaders:
+ self.transport.write("HTTP/1.1 400 Bad Request\r\n\r\n")
+ self.transport.loseConnection()
+
+
+ def allContentReceived(self):
+ command = self._command
+ path = self._path
+ version = self._version
+
+ # reset ALL state variables, so we don't interfere with next request
+ self.length = 0
+ self._receivedHeaderCount = 0
+ self.__first_line = 1
+ self._transferDecoder = None
+ del self._command, self._path, self._version
+
+ # Disable the idle timeout, in case this request takes a long
+ # time to finish generating output.
+ if self.timeOut:
+ self._savedTimeOut = self.setTimeout(None)
+
+ req = self.requests[-1]
+ req.requestReceived(command, path, version)
+
+ def rawDataReceived(self, data):
+ self.resetTimeout()
+ self._transferDecoder.dataReceived(data)
+
+
+ def allHeadersReceived(self):
+ req = self.requests[-1]
+ req.parseCookies()
+ self.persistent = self.checkPersistence(req, self._version)
+ req.gotLength(self.length)
+ # Handle 'Expect: 100-continue' with automated 100 response code,
+ # a simplistic implementation of RFC 2686 8.2.3:
+ expectContinue = req.requestHeaders.getRawHeaders('expect')
+ if (expectContinue and expectContinue[0].lower() == '100-continue' and
+ self._version == 'HTTP/1.1'):
+ req.transport.write("HTTP/1.1 100 Continue\r\n\r\n")
+
+
+ def checkPersistence(self, request, version):
+ """
+ Check if the channel should close or not.
+
+ @param request: The request most recently received over this channel
+ against which checks will be made to determine if this connection
+ can remain open after a matching response is returned.
+
+ @type version: C{str}
+ @param version: The version of the request.
+
+ @rtype: C{bool}
+ @return: A flag which, if C{True}, indicates that this connection may
+ remain open to receive another request; if C{False}, the connection
+ must be closed in order to indicate the completion of the response
+ to C{request}.
+ """
+ connection = request.requestHeaders.getRawHeaders('connection')
+ if connection:
+ tokens = map(str.lower, connection[0].split(' '))
+ else:
+ tokens = []
+
+ # HTTP 1.0 persistent connection support is currently disabled,
+ # since we need a way to disable pipelining. HTTP 1.0 can't do
+ # pipelining since we can't know in advance if we'll have a
+ # content-length header, if we don't have the header we need to close the
+ # connection. In HTTP 1.1 this is not an issue since we use chunked
+ # encoding if content-length is not available.
+
+ #if version == "HTTP/1.0":
+ # if 'keep-alive' in tokens:
+ # request.setHeader('connection', 'Keep-Alive')
+ # return 1
+ # else:
+ # return 0
+ if version == "HTTP/1.1":
+ if 'close' in tokens:
+ request.responseHeaders.setRawHeaders('connection', ['close'])
+ return False
+ else:
+ return True
+ else:
+ return False
+
+
+ def requestDone(self, request):
+ """
+ Called by first request in queue when it is done.
+ """
+ if request != self.requests[0]: raise TypeError
+ del self.requests[0]
+
+ if self.persistent:
+ # notify next request it can start writing
+ if self.requests:
+ self.requests[0].noLongerQueued()
+ else:
+ if self._savedTimeOut:
+ self.setTimeout(self._savedTimeOut)
+ else:
+ self.transport.loseConnection()
+
+ def timeoutConnection(self):
+ log.msg("Timing out client: %s" % str(self.transport.getPeer()))
+ policies.TimeoutMixin.timeoutConnection(self)
+
+ def connectionLost(self, reason):
+ self.setTimeout(None)
+ for request in self.requests:
+ request.connectionLost(reason)
+
+
+class HTTPFactory(protocol.ServerFactory):
+ """
+ Factory for HTTP server.
+
+ @ivar _logDateTime: A cached datetime string for log messages, updated by
+ C{_logDateTimeCall}.
+ @type _logDateTime: L{str}
+
+ @ivar _logDateTimeCall: A delayed call for the next update to the cached log
+ datetime string.
+ @type _logDateTimeCall: L{IDelayedCall} provided
+ """
+
+ protocol = HTTPChannel
+
+ logPath = None
+
+ timeOut = 60 * 60 * 12
+
+ def __init__(self, logPath=None, timeout=60*60*12):
+ if logPath is not None:
+ logPath = os.path.abspath(logPath)
+ self.logPath = logPath
+ self.timeOut = timeout
+
+ # For storing the cached log datetime and the callback to update it
+ self._logDateTime = None
+ self._logDateTimeCall = None
+
+
+ def _updateLogDateTime(self):
+ """
+ Update log datetime periodically, so we aren't always recalculating it.
+ """
+ self._logDateTime = datetimeToLogString()
+ self._logDateTimeCall = reactor.callLater(1, self._updateLogDateTime)
+
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ # timeOut needs to be on the Protocol instance cause
+ # TimeoutMixin expects it there
+ p.timeOut = self.timeOut
+ return p
+
+
+ def startFactory(self):
+ """
+ Set up request logging if necessary.
+ """
+ if self._logDateTimeCall is None:
+ self._updateLogDateTime()
+
+ if self.logPath:
+ self.logFile = self._openLogFile(self.logPath)
+ else:
+ self.logFile = log.logfile
+
+
+ def stopFactory(self):
+ if hasattr(self, "logFile"):
+ if self.logFile != log.logfile:
+ self.logFile.close()
+ del self.logFile
+
+ if self._logDateTimeCall is not None and self._logDateTimeCall.active():
+ self._logDateTimeCall.cancel()
+ self._logDateTimeCall = None
+
+
+ def _openLogFile(self, path):
+ """
+ Override in subclasses, e.g. to use twisted.python.logfile.
+ """
+ f = open(path, "a", 1)
+ return f
+
+ def _escape(self, s):
+ # pain in the ass. Return a string like python repr, but always
+ # escaped as if surrounding quotes were "".
+ r = repr(s)
+ if r[0] == "'":
+ return r[1:-1].replace('"', '\\"').replace("\\'", "'")
+ return r[1:-1]
+
+ def log(self, request):
+ """
+ Log a request's result to the logfile, by default in combined log format.
+ """
+ if hasattr(self, "logFile"):
+ line = '%s - - %s "%s" %d %s "%s" "%s"\n' % (
+ request.getClientIP(),
+ # request.getUser() or "-", # the remote user is almost never important
+ self._logDateTime,
+ '%s %s %s' % (self._escape(request.method),
+ self._escape(request.uri),
+ self._escape(request.clientproto)),
+ request.code,
+ request.sentLength or "-",
+ self._escape(request.getHeader("referer") or "-"),
+ self._escape(request.getHeader("user-agent") or "-"))
+ self.logFile.write(line)
diff --git a/twisted/web/http_headers.py b/twisted/web/http_headers.py
new file mode 100644
index 0000000..f0d8599
--- /dev/null
+++ b/twisted/web/http_headers.py
@@ -0,0 +1,277 @@
+# -*- test-case-name: twisted.web.test.test_http_headers
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An API for storing HTTP header names and values.
+"""
+
+
+from UserDict import DictMixin
+
+
+def _dashCapitalize(name):
+ """
+ Return a string which is capitalized using '-' as a word separator.
+
+ @param name: The name of the header to capitalize.
+ @type name: str
+
+ @return: The given header capitalized using '-' as a word separator.
+ @rtype: str
+ """
+ return '-'.join([word.capitalize() for word in name.split('-')])
+
+
+
+class _DictHeaders(DictMixin):
+ """
+ A C{dict}-like wrapper around L{Headers} to provide backwards compatibility
+ for L{Request.received_headers} and L{Request.headers} which used to be
+ plain C{dict} instances.
+
+ @type _headers: L{Headers}
+ @ivar _headers: The real header storage object.
+ """
+ def __init__(self, headers):
+ self._headers = headers
+
+
+ def __getitem__(self, key):
+ """
+ Return the last value for header of C{key}.
+ """
+ if self._headers.hasHeader(key):
+ return self._headers.getRawHeaders(key)[-1]
+ raise KeyError(key)
+
+
+ def __setitem__(self, key, value):
+ """
+ Set the given header.
+ """
+ self._headers.setRawHeaders(key, [value])
+
+
+ def __delitem__(self, key):
+ """
+ Delete the given header.
+ """
+ if self._headers.hasHeader(key):
+ self._headers.removeHeader(key)
+ else:
+ raise KeyError(key)
+
+
+ def keys(self):
+ """
+ Return a list of all header names.
+ """
+ return [k.lower() for k, v in self._headers.getAllRawHeaders()]
+
+
+ def copy(self):
+ """
+ Return a C{dict} mapping each header name to the last corresponding
+ header value.
+ """
+ return dict(self.items())
+
+
+ # Python 2.3 DictMixin.setdefault is defined so as not to have a default
+ # for the value parameter. This is necessary to make this setdefault look
+ # like dict.setdefault on Python 2.3. -exarkun
+ def setdefault(self, name, value=None):
+ """
+ Retrieve the last value for the given header name. If there are no
+ values present for that header, set the value to C{value} and return
+ that instead. Note that C{None} is the default for C{value} for
+ backwards compatibility, but header values may only be of type C{str}.
+ """
+ return DictMixin.setdefault(self, name, value)
+
+
+ # The remaining methods are only for efficiency. The same behavior
+ # should remain even if they are removed. For details, see
+ # <http://docs.python.org/lib/module-UserDict.html>.
+ # -exarkun
+ def __contains__(self, name):
+ """
+ Return C{True} if the named header is present, C{False} otherwise.
+ """
+ return self._headers.getRawHeaders(name) is not None
+
+
+ def __iter__(self):
+ """
+ Return an iterator of the lowercase name of each header present.
+ """
+ for k, v in self._headers.getAllRawHeaders():
+ yield k.lower()
+
+
+ def iteritems(self):
+ """
+ Return an iterable of two-tuples of each lower-case header name and the
+ last value for that header.
+ """
+ for k, v in self._headers.getAllRawHeaders():
+ yield k.lower(), v[-1]
+
+
+
+class Headers(object):
+ """
+ This class stores the HTTP headers as both a parsed representation
+ and the raw string representation. It converts between the two on
+ demand.
+
+ @cvar _caseMappings: A C{dict} that maps lowercase header names
+ to their canonicalized representation.
+
+ @ivar _rawHeaders: A C{dict} mapping header names as C{str} to C{lists} of
+ header values as C{str}.
+ """
+ _caseMappings = {
+ 'content-md5': 'Content-MD5',
+ 'dnt': 'DNT',
+ 'etag': 'ETag',
+ 'p3p': 'P3P',
+ 'te': 'TE',
+ 'www-authenticate': 'WWW-Authenticate',
+ 'x-xss-protection': 'X-XSS-Protection'}
+
+ def __init__(self, rawHeaders=None):
+ self._rawHeaders = {}
+ if rawHeaders is not None:
+ for name, values in rawHeaders.iteritems():
+ self.setRawHeaders(name, values[:])
+
+
+ def __repr__(self):
+ """
+ Return a string fully describing the headers set on this object.
+ """
+ return '%s(%r)' % (self.__class__.__name__, self._rawHeaders,)
+
+
+ def __cmp__(self, other):
+ """
+ Define L{Headers} instances as being equal to each other if they have
+ the same raw headers.
+ """
+ if isinstance(other, Headers):
+ return cmp(self._rawHeaders, other._rawHeaders)
+ return NotImplemented
+
+
+ def copy(self):
+ """
+ Return a copy of itself with the same headers set.
+ """
+ return self.__class__(self._rawHeaders)
+
+
+ def hasHeader(self, name):
+ """
+ Check for the existence of a given header.
+
+ @type name: C{str}
+ @param name: The name of the HTTP header to check for.
+
+ @rtype: C{bool}
+ @return: C{True} if the header exists, otherwise C{False}.
+ """
+ return name.lower() in self._rawHeaders
+
+
+ def removeHeader(self, name):
+ """
+ Remove the named header from this header object.
+
+ @type name: C{str}
+ @param name: The name of the HTTP header to remove.
+
+ @return: C{None}
+ """
+ self._rawHeaders.pop(name.lower(), None)
+
+
+ def setRawHeaders(self, name, values):
+ """
+ Sets the raw representation of the given header.
+
+ @type name: C{str}
+ @param name: The name of the HTTP header to set the values for.
+
+ @type values: C{list}
+ @param values: A list of strings each one being a header value of
+ the given name.
+
+ @return: C{None}
+ """
+ if not isinstance(values, list):
+ raise TypeError("Header entry %r should be list but found "
+ "instance of %r instead" % (name, type(values)))
+ self._rawHeaders[name.lower()] = values
+
+
+ def addRawHeader(self, name, value):
+ """
+ Add a new raw value for the given header.
+
+ @type name: C{str}
+ @param name: The name of the header for which to set the value.
+
+ @type value: C{str}
+ @param value: The value to set for the named header.
+ """
+ values = self.getRawHeaders(name)
+ if values is None:
+ self.setRawHeaders(name, [value])
+ else:
+ values.append(value)
+
+
+ def getRawHeaders(self, name, default=None):
+ """
+ Returns a list of headers matching the given name as the raw string
+ given.
+
+ @type name: C{str}
+ @param name: The name of the HTTP header to get the values of.
+
+ @param default: The value to return if no header with the given C{name}
+ exists.
+
+ @rtype: C{list}
+ @return: A C{list} of values for the given header.
+ """
+ return self._rawHeaders.get(name.lower(), default)
+
+
+ def getAllRawHeaders(self):
+ """
+ Return an iterator of key, value pairs of all headers contained in this
+ object, as strings. The keys are capitalized in canonical
+ capitalization.
+ """
+ for k, v in self._rawHeaders.iteritems():
+ yield self._canonicalNameCaps(k), v
+
+
+ def _canonicalNameCaps(self, name):
+ """
+ Return the canonical name for the given header.
+
+ @type name: C{str}
+ @param name: The all-lowercase header name to capitalize in its
+ canonical form.
+
+ @rtype: C{str}
+ @return: The canonical name of the header.
+ """
+ return self._caseMappings.get(name, _dashCapitalize(name))
+
+
+__all__ = ['Headers']
diff --git a/twisted/web/iweb.py b/twisted/web/iweb.py
new file mode 100644
index 0000000..6effd91
--- /dev/null
+++ b/twisted/web/iweb.py
@@ -0,0 +1,526 @@
+# -*- test-case-name: twisted.web.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Interface definitions for L{twisted.web}.
+
+@var UNKNOWN_LENGTH: An opaque object which may be used as the value of
+ L{IBodyProducer.length} to indicate that the length of the entity
+ body is not known in advance.
+"""
+
+from zope.interface import Interface, Attribute
+
+from twisted.internet.interfaces import IPushProducer
+from twisted.cred.credentials import IUsernameDigestHash
+
+
+class IRequest(Interface):
+ """
+ An HTTP request.
+
+ @since: 9.0
+ """
+
+ method = Attribute("A C{str} giving the HTTP method that was used.")
+ uri = Attribute(
+ "A C{str} giving the full encoded URI which was requested (including "
+ "query arguments).")
+ path = Attribute(
+ "A C{str} giving the encoded query path of the request URI.")
+ args = Attribute(
+ "A mapping of decoded query argument names as C{str} to "
+ "corresponding query argument values as C{list}s of C{str}. "
+ "For example, for a URI with C{'foo=bar&foo=baz&quux=spam'} "
+ "for its query part, C{args} will be C{{'foo': ['bar', 'baz'], "
+ "'quux': ['spam']}}.")
+
+ received_headers = Attribute(
+ "Backwards-compatibility access to C{requestHeaders}. Use "
+ "C{requestHeaders} instead. C{received_headers} behaves mostly "
+ "like a C{dict} and does not provide access to all header values.")
+
+ requestHeaders = Attribute(
+ "A L{http_headers.Headers} instance giving all received HTTP request "
+ "headers.")
+
+ headers = Attribute(
+ "Backwards-compatibility access to C{responseHeaders}. Use"
+ "C{responseHeaders} instead. C{headers} behaves mostly like a "
+ "C{dict} and does not provide access to all header values nor "
+ "does it allow multiple values for one header to be set.")
+
+ responseHeaders = Attribute(
+ "A L{http_headers.Headers} instance holding all HTTP response "
+ "headers to be sent.")
+
+ def getHeader(key):
+ """
+ Get an HTTP request header.
+
+ @type key: C{str}
+ @param key: The name of the header to get the value of.
+
+ @rtype: C{str} or C{NoneType}
+ @return: The value of the specified header, or C{None} if that header
+ was not present in the request.
+ """
+
+
+ def getCookie(key):
+ """
+ Get a cookie that was sent from the network.
+ """
+
+
+ def getAllHeaders():
+ """
+ Return dictionary mapping the names of all received headers to the last
+ value received for each.
+
+ Since this method does not return all header information,
+ C{requestHeaders.getAllRawHeaders()} may be preferred.
+ """
+
+
+ def getRequestHostname():
+ """
+ Get the hostname that the user passed in to the request.
+
+ This will either use the Host: header (if it is available) or the
+ host we are listening on if the header is unavailable.
+
+ @returns: the requested hostname
+ @rtype: C{str}
+ """
+
+
+ def getHost():
+ """
+ Get my originally requesting transport's host.
+
+ @return: An L{IAddress}.
+ """
+
+
+ def getClientIP():
+ """
+ Return the IP address of the client who submitted this request.
+
+ @returns: the client IP address or C{None} if the request was submitted
+ over a transport where IP addresses do not make sense.
+ @rtype: C{str} or L{NoneType}
+ """
+
+
+ def getClient():
+ """
+ Return the hostname of the IP address of the client who submitted this
+ request, if possible.
+
+ This method is B{deprecated}. See L{getClientIP} instead.
+
+ @rtype: L{NoneType} or L{str}
+ @return: The canonical hostname of the client, as determined by
+ performing a name lookup on the IP address of the client.
+ """
+
+
+ def getUser():
+ """
+ Return the HTTP user sent with this request, if any.
+
+ If no user was supplied, return the empty string.
+
+ @returns: the HTTP user, if any
+ @rtype: C{str}
+ """
+
+
+ def getPassword():
+ """
+ Return the HTTP password sent with this request, if any.
+
+ If no password was supplied, return the empty string.
+
+ @returns: the HTTP password, if any
+ @rtype: C{str}
+ """
+
+
+ def isSecure():
+ """
+ Return True if this request is using a secure transport.
+
+ Normally this method returns True if this request's HTTPChannel
+ instance is using a transport that implements ISSLTransport.
+
+ This will also return True if setHost() has been called
+ with ssl=True.
+
+ @returns: True if this request is secure
+ @rtype: C{bool}
+ """
+
+
+ def getSession(sessionInterface=None):
+ """
+ Look up the session associated with this request or create a new one if
+ there is not one.
+
+ @return: The L{Session} instance identified by the session cookie in
+ the request, or the C{sessionInterface} component of that session
+ if C{sessionInterface} is specified.
+ """
+
+
+ def URLPath():
+ """
+ @return: A L{URLPath} instance which identifies the URL for which this
+ request is.
+ """
+
+
+ def prePathURL():
+ """
+ @return: At any time during resource traversal, a L{str} giving an
+ absolute URL to the most nested resource which has yet been
+ reached.
+ """
+
+
+ def rememberRootURL():
+ """
+ Remember the currently-processed part of the URL for later
+ recalling.
+ """
+
+
+ def getRootURL():
+ """
+ Get a previously-remembered URL.
+ """
+
+
+ # Methods for outgoing response
+ def finish():
+ """
+ Indicate that the response to this request is complete.
+ """
+
+
+ def write(data):
+ """
+ Write some data to the body of the response to this request. Response
+ headers are written the first time this method is called, after which
+ new response headers may not be added.
+ """
+
+
+ def addCookie(k, v, expires=None, domain=None, path=None, max_age=None, comment=None, secure=None):
+ """
+ Set an outgoing HTTP cookie.
+
+ In general, you should consider using sessions instead of cookies, see
+ L{twisted.web.server.Request.getSession} and the
+ L{twisted.web.server.Session} class for details.
+ """
+
+
+ def setResponseCode(code, message=None):
+ """
+ Set the HTTP response code.
+ """
+
+
+ def setHeader(k, v):
+ """
+ Set an HTTP response header. Overrides any previously set values for
+ this header.
+
+ @type name: C{str}
+ @param name: The name of the header for which to set the value.
+
+ @type value: C{str}
+ @param value: The value to set for the named header.
+ """
+
+
+ def redirect(url):
+ """
+ Utility function that does a redirect.
+
+ The request should have finish() called after this.
+ """
+
+
+ def setLastModified(when):
+ """
+ Set the C{Last-Modified} time for the response to this request.
+
+ If I am called more than once, I ignore attempts to set Last-Modified
+ earlier, only replacing the Last-Modified time if it is to a later
+ value.
+
+ If I am a conditional request, I may modify my response code to
+ L{NOT_MODIFIED} if appropriate for the time given.
+
+ @param when: The last time the resource being returned was modified, in
+ seconds since the epoch.
+ @type when: C{int}, C{long} or C{float}
+
+ @return: If I am a C{If-Modified-Since} conditional request and the
+ time given is not newer than the condition, I return
+ L{http.CACHED<CACHED>} to indicate that you should write no body.
+ Otherwise, I return a false value.
+ """
+
+
+ def setETag(etag):
+ """
+ Set an C{entity tag} for the outgoing response.
+
+ That's "entity tag" as in the HTTP/1.1 C{ETag} header, "used for
+ comparing two or more entities from the same requested resource."
+
+ If I am a conditional request, I may modify my response code to
+ L{NOT_MODIFIED} or L{PRECONDITION_FAILED}, if appropriate for the tag
+ given.
+
+ @param etag: The entity tag for the resource being returned.
+ @type etag: C{str}
+ @return: If I am a C{If-None-Match} conditional request and the tag
+ matches one in the request, I return L{http.CACHED<CACHED>} to
+ indicate that you should write no body. Otherwise, I return a
+ false value.
+ """
+
+
+ def setHost(host, port, ssl=0):
+ """
+ Change the host and port the request thinks it's using.
+
+ This method is useful for working with reverse HTTP proxies (e.g. both
+ Squid and Apache's mod_proxy can do this), when the address the HTTP
+ client is using is different than the one we're listening on.
+
+ For example, Apache may be listening on https://www.example.com, and
+ then forwarding requests to http://localhost:8080, but we don't want
+ HTML produced by Twisted to say 'http://localhost:8080', they should
+ say 'https://www.example.com', so we do::
+
+ request.setHost('www.example.com', 443, ssl=1)
+ """
+
+
+
+class ICredentialFactory(Interface):
+ """
+ A credential factory defines a way to generate a particular kind of
+ authentication challenge and a way to interpret the responses to these
+ challenges. It creates L{ICredentials} providers from responses. These
+ objects will be used with L{twisted.cred} to authenticate an authorize
+ requests.
+ """
+ scheme = Attribute(
+ "A C{str} giving the name of the authentication scheme with which "
+ "this factory is associated. For example, C{'basic'} or C{'digest'}.")
+
+
+ def getChallenge(request):
+ """
+ Generate a new challenge to be sent to a client.
+
+ @type peer: L{twisted.web.http.Request}
+ @param peer: The request the response to which this challenge will be
+ included.
+
+ @rtype: C{dict}
+ @return: A mapping from C{str} challenge fields to associated C{str}
+ values.
+ """
+
+
+ def decode(response, request):
+ """
+ Create a credentials object from the given response.
+
+ @type response: C{str}
+ @param response: scheme specific response string
+
+ @type request: L{twisted.web.http.Request}
+ @param request: The request being processed (from which the response
+ was taken).
+
+ @raise twisted.cred.error.LoginFailed: If the response is invalid.
+
+ @rtype: L{twisted.cred.credentials.ICredentials} provider
+ @return: The credentials represented by the given response.
+ """
+
+
+
+class IBodyProducer(IPushProducer):
+ """
+ Objects which provide L{IBodyProducer} write bytes to an object which
+ provides L{IConsumer} by calling its C{write} method repeatedly.
+
+ L{IBodyProducer} providers may start producing as soon as they have
+ an L{IConsumer} provider. That is, they should not wait for a
+ C{resumeProducing} call to begin writing data.
+
+ L{IConsumer.unregisterProducer} must not be called. Instead, the
+ L{Deferred} returned from C{startProducing} must be fired when all bytes
+ have been written.
+
+ L{IConsumer.write} may synchronously invoke any of C{pauseProducing},
+ C{resumeProducing}, or C{stopProducing}. These methods must be implemented
+ with this in mind.
+
+ @since: 9.0
+ """
+
+ # Despite the restrictions above and the additional requirements of
+ # stopProducing documented below, this interface still needs to be an
+ # IPushProducer subclass. Providers of it will be passed to IConsumer
+ # providers which only know about IPushProducer and IPullProducer, not
+ # about this interface. This interface needs to remain close enough to one
+ # of those interfaces for consumers to work with it.
+
+ length = Attribute(
+ """
+ C{length} is a C{int} indicating how many bytes in total this
+ L{IBodyProducer} will write to the consumer or L{UNKNOWN_LENGTH}
+ if this is not known in advance.
+ """)
+
+ def startProducing(consumer):
+ """
+ Start producing to the given L{IConsumer} provider.
+
+ @return: A L{Deferred} which fires with C{None} when all bytes have
+ been produced or with a L{Failure} if there is any problem before
+ all bytes have been produced.
+ """
+
+
+ def stopProducing():
+ """
+ In addition to the standard behavior of L{IProducer.stopProducing}
+ (stop producing data), make sure the L{Deferred} returned by
+ C{startProducing} is never fired.
+ """
+
+
+
+class IRenderable(Interface):
+ """
+ An L{IRenderable} is an object that may be rendered by the
+ L{twisted.web.template} templating system.
+ """
+
+ def lookupRenderMethod(name):
+ """
+ Look up and return the render method associated with the given name.
+
+ @type name: C{str}
+ @param name: The value of a render directive encountered in the
+ document returned by a call to L{IRenderable.render}.
+
+ @return: A two-argument callable which will be invoked with the request
+ being responded to and the tag object on which the render directive
+ was encountered.
+ """
+
+
+ def render(request):
+ """
+ Get the document for this L{IRenderable}.
+
+ @type request: L{IRequest} provider or L{NoneType}
+ @param request: The request in response to which this method is being
+ invoked.
+
+ @return: An object which can be flattened.
+ """
+
+
+
+class ITemplateLoader(Interface):
+ """
+ A loader for templates; something usable as a value for
+ L{twisted.web.template.Element}'s C{loader} attribute.
+ """
+
+ def load():
+ """
+ Load a template suitable for rendering.
+
+ @return: a C{list} of C{list}s, C{unicode} objects, C{Element}s and
+ other L{IRenderable} providers.
+ """
+
+
+
+class IResponse(Interface):
+ """
+ An object representing an HTTP response received from an HTTP server.
+
+ @since: 11.1
+ """
+
+ version = Attribute(
+ "A three-tuple describing the protocol and protocol version "
+ "of the response. The first element is of type C{str}, the second "
+ "and third are of type C{int}. For example, C{('HTTP', 1, 1)}.")
+
+
+ code = Attribute("The HTTP status code of this response, as a C{int}.")
+
+
+ phrase = Attribute(
+ "The HTTP reason phrase of this response, as a C{str}.")
+
+
+ headers = Attribute("The HTTP response L{Headers} of this response.")
+
+
+ length = Attribute(
+ "The C{int} number of bytes expected to be in the body of this "
+ "response or L{UNKNOWN_LENGTH} if the server did not indicate how "
+ "many bytes to expect. For I{HEAD} responses, this will be 0; if "
+ "the response includes a I{Content-Length} header, it will be "
+ "available in C{headers}.")
+
+
+ def deliverBody(protocol):
+ """
+ Register an L{IProtocol} provider to receive the response body.
+
+ The protocol will be connected to a transport which provides
+ L{IPushProducer}. The protocol's C{connectionLost} method will be
+ called with:
+
+ - ResponseDone, which indicates that all bytes from the response
+ have been successfully delivered.
+
+ - PotentialDataLoss, which indicates that it cannot be determined
+ if the entire response body has been delivered. This only occurs
+ when making requests to HTTP servers which do not set
+ I{Content-Length} or a I{Transfer-Encoding} in the response.
+
+ - ResponseFailed, which indicates that some bytes from the response
+ were lost. The C{reasons} attribute of the exception may provide
+ more specific indications as to why.
+ """
+
+
+
+UNKNOWN_LENGTH = u"twisted.web.iweb.UNKNOWN_LENGTH"
+
+__all__ = [
+ "IUsernameDigestHash", "ICredentialFactory", "IRequest",
+ "IBodyProducer", "IRenderable", "IResponse",
+
+ "UNKNOWN_LENGTH"]
diff --git a/twisted/web/microdom.py b/twisted/web/microdom.py
new file mode 100644
index 0000000..d8d1146
--- /dev/null
+++ b/twisted/web/microdom.py
@@ -0,0 +1,1028 @@
+# -*- test-case-name: twisted.web.test.test_xml -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Micro Document Object Model: a partial DOM implementation with SUX.
+
+This is an implementation of what we consider to be the useful subset of the
+DOM. The chief advantage of this library is that, not being burdened with
+standards compliance, it can remain very stable between versions. We can also
+implement utility 'pythonic' ways to access and mutate the XML tree.
+
+Since this has not subjected to a serious trial by fire, it is not recommended
+to use this outside of Twisted applications. However, it seems to work just
+fine for the documentation generator, which parses a fairly representative
+sample of XML.
+
+Microdom mainly focuses on working with HTML and XHTML.
+"""
+
+# System Imports
+import re
+from cStringIO import StringIO
+
+# create NodeList class
+from types import ListType as NodeList
+from types import StringTypes, UnicodeType
+
+# Twisted Imports
+from twisted.web.sux import XMLParser, ParseError
+from twisted.python.util import InsensitiveDict
+
+
+def getElementsByTagName(iNode, name):
+ """
+ Return a list of all child elements of C{iNode} with a name matching
+ C{name}.
+
+ Note that this implementation does not conform to the DOM Level 1 Core
+ specification because it may return C{iNode}.
+
+ @param iNode: An element at which to begin searching. If C{iNode} has a
+ name matching C{name}, it will be included in the result.
+
+ @param name: A C{str} giving the name of the elements to return.
+
+ @return: A C{list} of direct or indirect child elements of C{iNode} with
+ the name C{name}. This may include C{iNode}.
+ """
+ matches = []
+ matches_append = matches.append # faster lookup. don't do this at home
+ slice = [iNode]
+ while len(slice)>0:
+ c = slice.pop(0)
+ if c.nodeName == name:
+ matches_append(c)
+ slice[:0] = c.childNodes
+ return matches
+
+
+
+def getElementsByTagNameNoCase(iNode, name):
+ name = name.lower()
+ matches = []
+ matches_append = matches.append
+ slice=[iNode]
+ while len(slice)>0:
+ c = slice.pop(0)
+ if c.nodeName.lower() == name:
+ matches_append(c)
+ slice[:0] = c.childNodes
+ return matches
+
+# order is important
+HTML_ESCAPE_CHARS = (('&', '&amp;'), # don't add any entities before this one
+ ('<', '&lt;'),
+ ('>', '&gt;'),
+ ('"', '&quot;'))
+REV_HTML_ESCAPE_CHARS = list(HTML_ESCAPE_CHARS)
+REV_HTML_ESCAPE_CHARS.reverse()
+
+XML_ESCAPE_CHARS = HTML_ESCAPE_CHARS + (("'", '&apos;'),)
+REV_XML_ESCAPE_CHARS = list(XML_ESCAPE_CHARS)
+REV_XML_ESCAPE_CHARS.reverse()
+
+def unescape(text, chars=REV_HTML_ESCAPE_CHARS):
+ "Perform the exact opposite of 'escape'."
+ for s, h in chars:
+ text = text.replace(h, s)
+ return text
+
+def escape(text, chars=HTML_ESCAPE_CHARS):
+ "Escape a few XML special chars with XML entities."
+ for s, h in chars:
+ text = text.replace(s, h)
+ return text
+
+
+class MismatchedTags(Exception):
+
+ def __init__(self, filename, expect, got, endLine, endCol, begLine, begCol):
+ (self.filename, self.expect, self.got, self.begLine, self.begCol, self.endLine,
+ self.endCol) = filename, expect, got, begLine, begCol, endLine, endCol
+
+ def __str__(self):
+ return ("expected </%s>, got </%s> line: %s col: %s, began line: %s col: %s"
+ % (self.expect, self.got, self.endLine, self.endCol, self.begLine,
+ self.begCol))
+
+
+class Node(object):
+ nodeName = "Node"
+
+ def __init__(self, parentNode=None):
+ self.parentNode = parentNode
+ self.childNodes = []
+
+ def isEqualToNode(self, other):
+ """
+ Compare this node to C{other}. If the nodes have the same number of
+ children and corresponding children are equal to each other, return
+ C{True}, otherwise return C{False}.
+
+ @type other: L{Node}
+ @rtype: C{bool}
+ """
+ if len(self.childNodes) != len(other.childNodes):
+ return False
+ for a, b in zip(self.childNodes, other.childNodes):
+ if not a.isEqualToNode(b):
+ return False
+ return True
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ raise NotImplementedError()
+
+ def toxml(self, indent='', addindent='', newl='', strip=0, nsprefixes={},
+ namespace=''):
+ s = StringIO()
+ self.writexml(s, indent, addindent, newl, strip, nsprefixes, namespace)
+ rv = s.getvalue()
+ return rv
+
+ def writeprettyxml(self, stream, indent='', addindent=' ', newl='\n', strip=0):
+ return self.writexml(stream, indent, addindent, newl, strip)
+
+ def toprettyxml(self, indent='', addindent=' ', newl='\n', strip=0):
+ return self.toxml(indent, addindent, newl, strip)
+
+ def cloneNode(self, deep=0, parent=None):
+ raise NotImplementedError()
+
+ def hasChildNodes(self):
+ if self.childNodes:
+ return 1
+ else:
+ return 0
+
+
+ def appendChild(self, child):
+ """
+ Make the given L{Node} the last child of this node.
+
+ @param child: The L{Node} which will become a child of this node.
+
+ @raise TypeError: If C{child} is not a C{Node} instance.
+ """
+ if not isinstance(child, Node):
+ raise TypeError("expected Node instance")
+ self.childNodes.append(child)
+ child.parentNode = self
+
+
+ def insertBefore(self, new, ref):
+ """
+ Make the given L{Node} C{new} a child of this node which comes before
+ the L{Node} C{ref}.
+
+ @param new: A L{Node} which will become a child of this node.
+
+ @param ref: A L{Node} which is already a child of this node which
+ C{new} will be inserted before.
+
+ @raise TypeError: If C{new} or C{ref} is not a C{Node} instance.
+
+ @return: C{new}
+ """
+ if not isinstance(new, Node) or not isinstance(ref, Node):
+ raise TypeError("expected Node instance")
+ i = self.childNodes.index(ref)
+ new.parentNode = self
+ self.childNodes.insert(i, new)
+ return new
+
+
+ def removeChild(self, child):
+ """
+ Remove the given L{Node} from this node's children.
+
+ @param child: A L{Node} which is a child of this node which will no
+ longer be a child of this node after this method is called.
+
+ @raise TypeError: If C{child} is not a C{Node} instance.
+
+ @return: C{child}
+ """
+ if not isinstance(child, Node):
+ raise TypeError("expected Node instance")
+ if child in self.childNodes:
+ self.childNodes.remove(child)
+ child.parentNode = None
+ return child
+
+ def replaceChild(self, newChild, oldChild):
+ """
+ Replace a L{Node} which is already a child of this node with a
+ different node.
+
+ @param newChild: A L{Node} which will be made a child of this node.
+
+ @param oldChild: A L{Node} which is a child of this node which will
+ give up its position to C{newChild}.
+
+ @raise TypeError: If C{newChild} or C{oldChild} is not a C{Node}
+ instance.
+
+ @raise ValueError: If C{oldChild} is not a child of this C{Node}.
+ """
+ if not isinstance(newChild, Node) or not isinstance(oldChild, Node):
+ raise TypeError("expected Node instance")
+ if oldChild.parentNode is not self:
+ raise ValueError("oldChild is not a child of this node")
+ self.childNodes[self.childNodes.index(oldChild)] = newChild
+ oldChild.parentNode = None
+ newChild.parentNode = self
+
+
+ def lastChild(self):
+ return self.childNodes[-1]
+
+
+ def firstChild(self):
+ if len(self.childNodes):
+ return self.childNodes[0]
+ return None
+
+ #def get_ownerDocument(self):
+ # """This doesn't really get the owner document; microdom nodes
+ # don't even have one necessarily. This gets the root node,
+ # which is usually what you really meant.
+ # *NOT DOM COMPLIANT.*
+ # """
+ # node=self
+ # while (node.parentNode): node=node.parentNode
+ # return node
+ #ownerDocument=node.get_ownerDocument()
+ # leaving commented for discussion; see also domhelpers.getParents(node)
+
+class Document(Node):
+
+ def __init__(self, documentElement=None):
+ Node.__init__(self)
+ if documentElement:
+ self.appendChild(documentElement)
+
+ def cloneNode(self, deep=0, parent=None):
+ d = Document()
+ d.doctype = self.doctype
+ if deep:
+ newEl = self.documentElement.cloneNode(1, self)
+ else:
+ newEl = self.documentElement
+ d.appendChild(newEl)
+ return d
+
+ doctype = None
+
+ def isEqualToDocument(self, n):
+ return (self.doctype == n.doctype) and Node.isEqualToNode(self, n)
+ isEqualToNode = isEqualToDocument
+
+ def get_documentElement(self):
+ return self.childNodes[0]
+ documentElement=property(get_documentElement)
+
+ def appendChild(self, child):
+ """
+ Make the given L{Node} the I{document element} of this L{Document}.
+
+ @param child: The L{Node} to make into this L{Document}'s document
+ element.
+
+ @raise ValueError: If this document already has a document element.
+ """
+ if self.childNodes:
+ raise ValueError("Only one element per document.")
+ Node.appendChild(self, child)
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ stream.write('<?xml version="1.0"?>' + newl)
+ if self.doctype:
+ stream.write("<!DOCTYPE "+self.doctype+">" + newl)
+ self.documentElement.writexml(stream, indent, addindent, newl, strip,
+ nsprefixes, namespace)
+
+ # of dubious utility (?)
+ def createElement(self, name, **kw):
+ return Element(name, **kw)
+
+ def createTextNode(self, text):
+ return Text(text)
+
+ def createComment(self, text):
+ return Comment(text)
+
+ def getElementsByTagName(self, name):
+ if self.documentElement.caseInsensitive:
+ return getElementsByTagNameNoCase(self, name)
+ return getElementsByTagName(self, name)
+
+ def getElementById(self, id):
+ childNodes = self.childNodes[:]
+ while childNodes:
+ node = childNodes.pop(0)
+ if node.childNodes:
+ childNodes.extend(node.childNodes)
+ if hasattr(node, 'getAttribute') and node.getAttribute("id") == id:
+ return node
+
+
+class EntityReference(Node):
+
+ def __init__(self, eref, parentNode=None):
+ Node.__init__(self, parentNode)
+ self.eref = eref
+ self.nodeValue = self.data = "&" + eref + ";"
+
+ def isEqualToEntityReference(self, n):
+ if not isinstance(n, EntityReference):
+ return 0
+ return (self.eref == n.eref) and (self.nodeValue == n.nodeValue)
+ isEqualToNode = isEqualToEntityReference
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ stream.write(self.nodeValue)
+
+ def cloneNode(self, deep=0, parent=None):
+ return EntityReference(self.eref, parent)
+
+
+class CharacterData(Node):
+
+ def __init__(self, data, parentNode=None):
+ Node.__init__(self, parentNode)
+ self.value = self.data = self.nodeValue = data
+
+ def isEqualToCharacterData(self, n):
+ return self.value == n.value
+ isEqualToNode = isEqualToCharacterData
+
+
+class Comment(CharacterData):
+ """A comment node."""
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ val=self.data
+ if isinstance(val, UnicodeType):
+ val=val.encode('utf8')
+ stream.write("<!--%s-->" % val)
+
+ def cloneNode(self, deep=0, parent=None):
+ return Comment(self.nodeValue, parent)
+
+
+class Text(CharacterData):
+
+ def __init__(self, data, parentNode=None, raw=0):
+ CharacterData.__init__(self, data, parentNode)
+ self.raw = raw
+
+
+ def isEqualToNode(self, other):
+ """
+ Compare this text to C{text}. If the underlying values and the C{raw}
+ flag are the same, return C{True}, otherwise return C{False}.
+ """
+ return (
+ CharacterData.isEqualToNode(self, other) and
+ self.raw == other.raw)
+
+
+ def cloneNode(self, deep=0, parent=None):
+ return Text(self.nodeValue, parent, self.raw)
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ if self.raw:
+ val = self.nodeValue
+ if not isinstance(val, StringTypes):
+ val = str(self.nodeValue)
+ else:
+ v = self.nodeValue
+ if not isinstance(v, StringTypes):
+ v = str(v)
+ if strip:
+ v = ' '.join(v.split())
+ val = escape(v)
+ if isinstance(val, UnicodeType):
+ val = val.encode('utf8')
+ stream.write(val)
+
+ def __repr__(self):
+ return "Text(%s" % repr(self.nodeValue) + ')'
+
+
+class CDATASection(CharacterData):
+ def cloneNode(self, deep=0, parent=None):
+ return CDATASection(self.nodeValue, parent)
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ stream.write("<![CDATA[")
+ stream.write(self.nodeValue)
+ stream.write("]]>")
+
+def _genprefix():
+ i = 0
+ while True:
+ yield 'p' + str(i)
+ i = i + 1
+genprefix = _genprefix().next
+
+class _Attr(CharacterData):
+ "Support class for getAttributeNode."
+
+class Element(Node):
+
+ preserveCase = 0
+ caseInsensitive = 1
+ nsprefixes = None
+
+ def __init__(self, tagName, attributes=None, parentNode=None,
+ filename=None, markpos=None,
+ caseInsensitive=1, preserveCase=0,
+ namespace=None):
+ Node.__init__(self, parentNode)
+ self.preserveCase = preserveCase or not caseInsensitive
+ self.caseInsensitive = caseInsensitive
+ if not preserveCase:
+ tagName = tagName.lower()
+ if attributes is None:
+ self.attributes = {}
+ else:
+ self.attributes = attributes
+ for k, v in self.attributes.items():
+ self.attributes[k] = unescape(v)
+
+ if caseInsensitive:
+ self.attributes = InsensitiveDict(self.attributes,
+ preserve=preserveCase)
+
+ self.endTagName = self.nodeName = self.tagName = tagName
+ self._filename = filename
+ self._markpos = markpos
+ self.namespace = namespace
+
+ def addPrefixes(self, pfxs):
+ if self.nsprefixes is None:
+ self.nsprefixes = pfxs
+ else:
+ self.nsprefixes.update(pfxs)
+
+ def endTag(self, endTagName):
+ if not self.preserveCase:
+ endTagName = endTagName.lower()
+ self.endTagName = endTagName
+
+ def isEqualToElement(self, n):
+ if self.caseInsensitive:
+ return ((self.attributes == n.attributes)
+ and (self.nodeName.lower() == n.nodeName.lower()))
+ return (self.attributes == n.attributes) and (self.nodeName == n.nodeName)
+
+
+ def isEqualToNode(self, other):
+ """
+ Compare this element to C{other}. If the C{nodeName}, C{namespace},
+ C{attributes}, and C{childNodes} are all the same, return C{True},
+ otherwise return C{False}.
+ """
+ return (
+ self.nodeName.lower() == other.nodeName.lower() and
+ self.namespace == other.namespace and
+ self.attributes == other.attributes and
+ Node.isEqualToNode(self, other))
+
+
+ def cloneNode(self, deep=0, parent=None):
+ clone = Element(
+ self.tagName, parentNode=parent, namespace=self.namespace,
+ preserveCase=self.preserveCase, caseInsensitive=self.caseInsensitive)
+ clone.attributes.update(self.attributes)
+ if deep:
+ clone.childNodes = [child.cloneNode(1, clone) for child in self.childNodes]
+ else:
+ clone.childNodes = []
+ return clone
+
+ def getElementsByTagName(self, name):
+ if self.caseInsensitive:
+ return getElementsByTagNameNoCase(self, name)
+ return getElementsByTagName(self, name)
+
+ def hasAttributes(self):
+ return 1
+
+ def getAttribute(self, name, default=None):
+ return self.attributes.get(name, default)
+
+ def getAttributeNS(self, ns, name, default=None):
+ nsk = (ns, name)
+ if self.attributes.has_key(nsk):
+ return self.attributes[nsk]
+ if ns == self.namespace:
+ return self.attributes.get(name, default)
+ return default
+
+ def getAttributeNode(self, name):
+ return _Attr(self.getAttribute(name), self)
+
+ def setAttribute(self, name, attr):
+ self.attributes[name] = attr
+
+ def removeAttribute(self, name):
+ if name in self.attributes:
+ del self.attributes[name]
+
+ def hasAttribute(self, name):
+ return name in self.attributes
+
+
+ def writexml(self, stream, indent='', addindent='', newl='', strip=0,
+ nsprefixes={}, namespace=''):
+ """
+ Serialize this L{Element} to the given stream.
+
+ @param stream: A file-like object to which this L{Element} will be
+ written.
+
+ @param nsprefixes: A C{dict} mapping namespace URIs as C{str} to
+ prefixes as C{str}. This defines the prefixes which are already in
+ scope in the document at the point at which this L{Element} exists.
+ This is essentially an implementation detail for namespace support.
+ Applications should not try to use it.
+
+ @param namespace: The namespace URI as a C{str} which is the default at
+ the point in the document at which this L{Element} exists. This is
+ essentially an implementation detail for namespace support.
+ Applications should not try to use it.
+ """
+ # write beginning
+ ALLOWSINGLETON = ('img', 'br', 'hr', 'base', 'meta', 'link', 'param',
+ 'area', 'input', 'col', 'basefont', 'isindex',
+ 'frame')
+ BLOCKELEMENTS = ('html', 'head', 'body', 'noscript', 'ins', 'del',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'script',
+ 'ul', 'ol', 'dl', 'pre', 'hr', 'blockquote',
+ 'address', 'p', 'div', 'fieldset', 'table', 'tr',
+ 'form', 'object', 'fieldset', 'applet', 'map')
+ FORMATNICELY = ('tr', 'ul', 'ol', 'head')
+
+ # this should never be necessary unless people start
+ # changing .tagName on the fly(?)
+ if not self.preserveCase:
+ self.endTagName = self.tagName
+ w = stream.write
+ if self.nsprefixes:
+ newprefixes = self.nsprefixes.copy()
+ for ns in nsprefixes.keys():
+ if ns in newprefixes:
+ del newprefixes[ns]
+ else:
+ newprefixes = {}
+
+ begin = ['<']
+ if self.tagName in BLOCKELEMENTS:
+ begin = [newl, indent] + begin
+ bext = begin.extend
+ writeattr = lambda _atr, _val: bext((' ', _atr, '="', escape(_val), '"'))
+
+ # Make a local for tracking what end tag will be used. If namespace
+ # prefixes are involved, this will be changed to account for that
+ # before it's actually used.
+ endTagName = self.endTagName
+
+ if namespace != self.namespace and self.namespace is not None:
+ # If the current default namespace is not the namespace of this tag
+ # (and this tag has a namespace at all) then we'll write out
+ # something related to namespaces.
+ if self.namespace in nsprefixes:
+ # This tag's namespace already has a prefix bound to it. Use
+ # that prefix.
+ prefix = nsprefixes[self.namespace]
+ bext(prefix + ':' + self.tagName)
+ # Also make sure we use it for the end tag.
+ endTagName = prefix + ':' + self.endTagName
+ else:
+ # This tag's namespace has no prefix bound to it. Change the
+ # default namespace to this tag's namespace so we don't need
+ # prefixes. Alternatively, we could add a new prefix binding.
+ # I'm not sure why the code was written one way rather than the
+ # other. -exarkun
+ bext(self.tagName)
+ writeattr("xmlns", self.namespace)
+ # The default namespace just changed. Make sure any children
+ # know about this.
+ namespace = self.namespace
+ else:
+ # This tag has no namespace or its namespace is already the default
+ # namespace. Nothing extra to do here.
+ bext(self.tagName)
+
+ j = ''.join
+ for attr, val in self.attributes.iteritems():
+ if isinstance(attr, tuple):
+ ns, key = attr
+ if nsprefixes.has_key(ns):
+ prefix = nsprefixes[ns]
+ else:
+ prefix = genprefix()
+ newprefixes[ns] = prefix
+ assert val is not None
+ writeattr(prefix+':'+key,val)
+ else:
+ assert val is not None
+ writeattr(attr, val)
+ if newprefixes:
+ for ns, prefix in newprefixes.iteritems():
+ if prefix:
+ writeattr('xmlns:'+prefix, ns)
+ newprefixes.update(nsprefixes)
+ downprefixes = newprefixes
+ else:
+ downprefixes = nsprefixes
+ w(j(begin))
+ if self.childNodes:
+ w(">")
+ newindent = indent + addindent
+ for child in self.childNodes:
+ if self.tagName in BLOCKELEMENTS and \
+ self.tagName in FORMATNICELY:
+ w(j((newl, newindent)))
+ child.writexml(stream, newindent, addindent, newl, strip,
+ downprefixes, namespace)
+ if self.tagName in BLOCKELEMENTS:
+ w(j((newl, indent)))
+ w(j(('</', endTagName, '>')))
+ elif self.tagName.lower() not in ALLOWSINGLETON:
+ w(j(('></', endTagName, '>')))
+ else:
+ w(" />")
+
+
+ def __repr__(self):
+ rep = "Element(%s" % repr(self.nodeName)
+ if self.attributes:
+ rep += ", attributes=%r" % (self.attributes,)
+ if self._filename:
+ rep += ", filename=%r" % (self._filename,)
+ if self._markpos:
+ rep += ", markpos=%r" % (self._markpos,)
+ return rep + ')'
+
+ def __str__(self):
+ rep = "<" + self.nodeName
+ if self._filename or self._markpos:
+ rep += " ("
+ if self._filename:
+ rep += repr(self._filename)
+ if self._markpos:
+ rep += " line %s column %s" % self._markpos
+ if self._filename or self._markpos:
+ rep += ")"
+ for item in self.attributes.items():
+ rep += " %s=%r" % item
+ if self.hasChildNodes():
+ rep += " >...</%s>" % self.nodeName
+ else:
+ rep += " />"
+ return rep
+
+def _unescapeDict(d):
+ dd = {}
+ for k, v in d.items():
+ dd[k] = unescape(v)
+ return dd
+
+def _reverseDict(d):
+ dd = {}
+ for k, v in d.items():
+ dd[v]=k
+ return dd
+
+class MicroDOMParser(XMLParser):
+
+ # <dash> glyph: a quick scan thru the DTD says BODY, AREA, LINK, IMG, HR,
+ # P, DT, DD, LI, INPUT, OPTION, THEAD, TFOOT, TBODY, COLGROUP, COL, TR, TH,
+ # TD, HEAD, BASE, META, HTML all have optional closing tags
+
+ soonClosers = 'area link br img hr input base meta'.split()
+ laterClosers = {'p': ['p', 'dt'],
+ 'dt': ['dt','dd'],
+ 'dd': ['dt', 'dd'],
+ 'li': ['li'],
+ 'tbody': ['thead', 'tfoot', 'tbody'],
+ 'thead': ['thead', 'tfoot', 'tbody'],
+ 'tfoot': ['thead', 'tfoot', 'tbody'],
+ 'colgroup': ['colgroup'],
+ 'col': ['col'],
+ 'tr': ['tr'],
+ 'td': ['td'],
+ 'th': ['th'],
+ 'head': ['body'],
+ 'title': ['head', 'body'], # this looks wrong...
+ 'option': ['option'],
+ }
+
+
+ def __init__(self, beExtremelyLenient=0, caseInsensitive=1, preserveCase=0,
+ soonClosers=soonClosers, laterClosers=laterClosers):
+ self.elementstack = []
+ d = {'xmlns': 'xmlns', '': None}
+ dr = _reverseDict(d)
+ self.nsstack = [(d,None,dr)]
+ self.documents = []
+ self._mddoctype = None
+ self.beExtremelyLenient = beExtremelyLenient
+ self.caseInsensitive = caseInsensitive
+ self.preserveCase = preserveCase or not caseInsensitive
+ self.soonClosers = soonClosers
+ self.laterClosers = laterClosers
+ # self.indentlevel = 0
+
+ def shouldPreserveSpace(self):
+ for edx in xrange(len(self.elementstack)):
+ el = self.elementstack[-edx]
+ if el.tagName == 'pre' or el.getAttribute("xml:space", '') == 'preserve':
+ return 1
+ return 0
+
+ def _getparent(self):
+ if self.elementstack:
+ return self.elementstack[-1]
+ else:
+ return None
+
+ COMMENT = re.compile(r"\s*/[/*]\s*")
+
+ def _fixScriptElement(self, el):
+ # this deals with case where there is comment or CDATA inside
+ # <script> tag and we want to do the right thing with it
+ if not self.beExtremelyLenient or not len(el.childNodes) == 1:
+ return
+ c = el.firstChild()
+ if isinstance(c, Text):
+ # deal with nasty people who do stuff like:
+ # <script> // <!--
+ # x = 1;
+ # // --></script>
+ # tidy does this, for example.
+ prefix = ""
+ oldvalue = c.value
+ match = self.COMMENT.match(oldvalue)
+ if match:
+ prefix = match.group()
+ oldvalue = oldvalue[len(prefix):]
+
+ # now see if contents are actual node and comment or CDATA
+ try:
+ e = parseString("<a>%s</a>" % oldvalue).childNodes[0]
+ except (ParseError, MismatchedTags):
+ return
+ if len(e.childNodes) != 1:
+ return
+ e = e.firstChild()
+ if isinstance(e, (CDATASection, Comment)):
+ el.childNodes = []
+ if prefix:
+ el.childNodes.append(Text(prefix))
+ el.childNodes.append(e)
+
+ def gotDoctype(self, doctype):
+ self._mddoctype = doctype
+
+ def gotTagStart(self, name, attributes):
+ # print ' '*self.indentlevel, 'start tag',name
+ # self.indentlevel += 1
+ parent = self._getparent()
+ if (self.beExtremelyLenient and isinstance(parent, Element)):
+ parentName = parent.tagName
+ myName = name
+ if self.caseInsensitive:
+ parentName = parentName.lower()
+ myName = myName.lower()
+ if myName in self.laterClosers.get(parentName, []):
+ self.gotTagEnd(parent.tagName)
+ parent = self._getparent()
+ attributes = _unescapeDict(attributes)
+ namespaces = self.nsstack[-1][0]
+ newspaces = {}
+ for k, v in attributes.items():
+ if k.startswith('xmlns'):
+ spacenames = k.split(':',1)
+ if len(spacenames) == 2:
+ newspaces[spacenames[1]] = v
+ else:
+ newspaces[''] = v
+ del attributes[k]
+ if newspaces:
+ namespaces = namespaces.copy()
+ namespaces.update(newspaces)
+ for k, v in attributes.items():
+ ksplit = k.split(':', 1)
+ if len(ksplit) == 2:
+ pfx, tv = ksplit
+ if pfx != 'xml' and namespaces.has_key(pfx):
+ attributes[namespaces[pfx], tv] = v
+ del attributes[k]
+ el = Element(name, attributes, parent,
+ self.filename, self.saveMark(),
+ caseInsensitive=self.caseInsensitive,
+ preserveCase=self.preserveCase,
+ namespace=namespaces.get(''))
+ revspaces = _reverseDict(newspaces)
+ el.addPrefixes(revspaces)
+
+ if newspaces:
+ rscopy = self.nsstack[-1][2].copy()
+ rscopy.update(revspaces)
+ self.nsstack.append((namespaces, el, rscopy))
+ self.elementstack.append(el)
+ if parent:
+ parent.appendChild(el)
+ if (self.beExtremelyLenient and el.tagName in self.soonClosers):
+ self.gotTagEnd(name)
+
+ def _gotStandalone(self, factory, data):
+ parent = self._getparent()
+ te = factory(data, parent)
+ if parent:
+ parent.appendChild(te)
+ elif self.beExtremelyLenient:
+ self.documents.append(te)
+
+ def gotText(self, data):
+ if data.strip() or self.shouldPreserveSpace():
+ self._gotStandalone(Text, data)
+
+ def gotComment(self, data):
+ self._gotStandalone(Comment, data)
+
+ def gotEntityReference(self, entityRef):
+ self._gotStandalone(EntityReference, entityRef)
+
+ def gotCData(self, cdata):
+ self._gotStandalone(CDATASection, cdata)
+
+ def gotTagEnd(self, name):
+ # print ' '*self.indentlevel, 'end tag',name
+ # self.indentlevel -= 1
+ if not self.elementstack:
+ if self.beExtremelyLenient:
+ return
+ raise MismatchedTags(*((self.filename, "NOTHING", name)
+ +self.saveMark()+(0,0)))
+ el = self.elementstack.pop()
+ pfxdix = self.nsstack[-1][2]
+ if self.nsstack[-1][1] is el:
+ nstuple = self.nsstack.pop()
+ else:
+ nstuple = None
+ if self.caseInsensitive:
+ tn = el.tagName.lower()
+ cname = name.lower()
+ else:
+ tn = el.tagName
+ cname = name
+
+ nsplit = name.split(':',1)
+ if len(nsplit) == 2:
+ pfx, newname = nsplit
+ ns = pfxdix.get(pfx,None)
+ if ns is not None:
+ if el.namespace != ns:
+ if not self.beExtremelyLenient:
+ raise MismatchedTags(*((self.filename, el.tagName, name)
+ +self.saveMark()+el._markpos))
+ if not (tn == cname):
+ if self.beExtremelyLenient:
+ if self.elementstack:
+ lastEl = self.elementstack[0]
+ for idx in xrange(len(self.elementstack)):
+ if self.elementstack[-(idx+1)].tagName == cname:
+ self.elementstack[-(idx+1)].endTag(name)
+ break
+ else:
+ # this was a garbage close tag; wait for a real one
+ self.elementstack.append(el)
+ if nstuple is not None:
+ self.nsstack.append(nstuple)
+ return
+ del self.elementstack[-(idx+1):]
+ if not self.elementstack:
+ self.documents.append(lastEl)
+ return
+ else:
+ raise MismatchedTags(*((self.filename, el.tagName, name)
+ +self.saveMark()+el._markpos))
+ el.endTag(name)
+ if not self.elementstack:
+ self.documents.append(el)
+ if self.beExtremelyLenient and el.tagName == "script":
+ self._fixScriptElement(el)
+
+ def connectionLost(self, reason):
+ XMLParser.connectionLost(self, reason) # This can cause more events!
+ if self.elementstack:
+ if self.beExtremelyLenient:
+ self.documents.append(self.elementstack[0])
+ else:
+ raise MismatchedTags(*((self.filename, self.elementstack[-1],
+ "END_OF_FILE")
+ +self.saveMark()
+ +self.elementstack[-1]._markpos))
+
+
+def parse(readable, *args, **kwargs):
+ """Parse HTML or XML readable."""
+ if not hasattr(readable, "read"):
+ readable = open(readable, "rb")
+ mdp = MicroDOMParser(*args, **kwargs)
+ mdp.filename = getattr(readable, "name", "<xmlfile />")
+ mdp.makeConnection(None)
+ if hasattr(readable,"getvalue"):
+ mdp.dataReceived(readable.getvalue())
+ else:
+ r = readable.read(1024)
+ while r:
+ mdp.dataReceived(r)
+ r = readable.read(1024)
+ mdp.connectionLost(None)
+
+ if not mdp.documents:
+ raise ParseError(mdp.filename, 0, 0, "No top-level Nodes in document")
+
+ if mdp.beExtremelyLenient:
+ if len(mdp.documents) == 1:
+ d = mdp.documents[0]
+ if not isinstance(d, Element):
+ el = Element("html")
+ el.appendChild(d)
+ d = el
+ else:
+ d = Element("html")
+ for child in mdp.documents:
+ d.appendChild(child)
+ else:
+ d = mdp.documents[0]
+ doc = Document(d)
+ doc.doctype = mdp._mddoctype
+ return doc
+
+def parseString(st, *args, **kw):
+ if isinstance(st, UnicodeType):
+ # this isn't particularly ideal, but it does work.
+ return parse(StringIO(st.encode('UTF-16')), *args, **kw)
+ return parse(StringIO(st), *args, **kw)
+
+
+def parseXML(readable):
+ """Parse an XML readable object."""
+ return parse(readable, caseInsensitive=0, preserveCase=1)
+
+
+def parseXMLString(st):
+ """Parse an XML readable object."""
+ return parseString(st, caseInsensitive=0, preserveCase=1)
+
+
+# Utility
+
+class lmx:
+ """Easy creation of XML."""
+
+ def __init__(self, node='div'):
+ if isinstance(node, StringTypes):
+ node = Element(node)
+ self.node = node
+
+ def __getattr__(self, name):
+ if name[0] == '_':
+ raise AttributeError("no private attrs")
+ return lambda **kw: self.add(name,**kw)
+
+ def __setitem__(self, key, val):
+ self.node.setAttribute(key, val)
+
+ def __getitem__(self, key):
+ return self.node.getAttribute(key)
+
+ def text(self, txt, raw=0):
+ nn = Text(txt, raw=raw)
+ self.node.appendChild(nn)
+ return self
+
+ def add(self, tagName, **kw):
+ newNode = Element(tagName, caseInsensitive=0, preserveCase=0)
+ self.node.appendChild(newNode)
+ xf = lmx(newNode)
+ for k, v in kw.items():
+ if k[0] == '_':
+ k = k[1:]
+ xf[k]=v
+ return xf
diff --git a/twisted/web/proxy.py b/twisted/web/proxy.py
new file mode 100644
index 0000000..68bce7d
--- /dev/null
+++ b/twisted/web/proxy.py
@@ -0,0 +1,303 @@
+# -*- test-case-name: twisted.web.test.test_proxy -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Simplistic HTTP proxy support.
+
+This comes in two main variants - the Proxy and the ReverseProxy.
+
+When a Proxy is in use, a browser trying to connect to a server (say,
+www.yahoo.com) will be intercepted by the Proxy, and the proxy will covertly
+connect to the server, and return the result.
+
+When a ReverseProxy is in use, the client connects directly to the ReverseProxy
+(say, www.yahoo.com) which farms off the request to one of a pool of servers,
+and returns the result.
+
+Normally, a Proxy is used on the client end of an Internet connection, while a
+ReverseProxy is used on the server end.
+"""
+
+import urlparse
+from urllib import quote as urlquote
+
+from twisted.internet import reactor
+from twisted.internet.protocol import ClientFactory
+from twisted.web.resource import Resource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.http import HTTPClient, Request, HTTPChannel
+
+
+
+class ProxyClient(HTTPClient):
+ """
+ Used by ProxyClientFactory to implement a simple web proxy.
+
+ @ivar _finished: A flag which indicates whether or not the original request
+ has been finished yet.
+ """
+ _finished = False
+
+ def __init__(self, command, rest, version, headers, data, father):
+ self.father = father
+ self.command = command
+ self.rest = rest
+ if "proxy-connection" in headers:
+ del headers["proxy-connection"]
+ headers["connection"] = "close"
+ headers.pop('keep-alive', None)
+ self.headers = headers
+ self.data = data
+
+
+ def connectionMade(self):
+ self.sendCommand(self.command, self.rest)
+ for header, value in self.headers.items():
+ self.sendHeader(header, value)
+ self.endHeaders()
+ self.transport.write(self.data)
+
+
+ def handleStatus(self, version, code, message):
+ self.father.setResponseCode(int(code), message)
+
+
+ def handleHeader(self, key, value):
+ # t.web.server.Request sets default values for these headers in its
+ # 'process' method. When these headers are received from the remote
+ # server, they ought to override the defaults, rather than append to
+ # them.
+ if key.lower() in ['server', 'date', 'content-type']:
+ self.father.responseHeaders.setRawHeaders(key, [value])
+ else:
+ self.father.responseHeaders.addRawHeader(key, value)
+
+
+ def handleResponsePart(self, buffer):
+ self.father.write(buffer)
+
+
+ def handleResponseEnd(self):
+ """
+ Finish the original request, indicating that the response has been
+ completely written to it, and disconnect the outgoing transport.
+ """
+ if not self._finished:
+ self._finished = True
+ self.father.finish()
+ self.transport.loseConnection()
+
+
+
+class ProxyClientFactory(ClientFactory):
+ """
+ Used by ProxyRequest to implement a simple web proxy.
+ """
+
+ protocol = ProxyClient
+
+
+ def __init__(self, command, rest, version, headers, data, father):
+ self.father = father
+ self.command = command
+ self.rest = rest
+ self.headers = headers
+ self.data = data
+ self.version = version
+
+
+ def buildProtocol(self, addr):
+ return self.protocol(self.command, self.rest, self.version,
+ self.headers, self.data, self.father)
+
+
+ def clientConnectionFailed(self, connector, reason):
+ """
+ Report a connection failure in a response to the incoming request as
+ an error.
+ """
+ self.father.setResponseCode(501, "Gateway error")
+ self.father.responseHeaders.addRawHeader("Content-Type", "text/html")
+ self.father.write("<H1>Could not connect</H1>")
+ self.father.finish()
+
+
+
+class ProxyRequest(Request):
+ """
+ Used by Proxy to implement a simple web proxy.
+
+ @ivar reactor: the reactor used to create connections.
+ @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
+ """
+
+ protocols = {'http': ProxyClientFactory}
+ ports = {'http': 80}
+
+ def __init__(self, channel, queued, reactor=reactor):
+ Request.__init__(self, channel, queued)
+ self.reactor = reactor
+
+
+ def process(self):
+ parsed = urlparse.urlparse(self.uri)
+ protocol = parsed[0]
+ host = parsed[1]
+ port = self.ports[protocol]
+ if ':' in host:
+ host, port = host.split(':')
+ port = int(port)
+ rest = urlparse.urlunparse(('', '') + parsed[2:])
+ if not rest:
+ rest = rest + '/'
+ class_ = self.protocols[protocol]
+ headers = self.getAllHeaders().copy()
+ if 'host' not in headers:
+ headers['host'] = host
+ self.content.seek(0, 0)
+ s = self.content.read()
+ clientFactory = class_(self.method, rest, self.clientproto, headers,
+ s, self)
+ self.reactor.connectTCP(host, port, clientFactory)
+
+
+
+class Proxy(HTTPChannel):
+ """
+ This class implements a simple web proxy.
+
+ Since it inherits from L{twisted.web.http.HTTPChannel}, to use it you
+ should do something like this::
+
+ from twisted.web import http
+ f = http.HTTPFactory()
+ f.protocol = Proxy
+
+ Make the HTTPFactory a listener on a port as per usual, and you have
+ a fully-functioning web proxy!
+ """
+
+ requestFactory = ProxyRequest
+
+
+
+class ReverseProxyRequest(Request):
+ """
+ Used by ReverseProxy to implement a simple reverse proxy.
+
+ @ivar proxyClientFactoryClass: a proxy client factory class, used to create
+ new connections.
+ @type proxyClientFactoryClass: L{ClientFactory}
+
+ @ivar reactor: the reactor used to create connections.
+ @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
+ """
+
+ proxyClientFactoryClass = ProxyClientFactory
+
+ def __init__(self, channel, queued, reactor=reactor):
+ Request.__init__(self, channel, queued)
+ self.reactor = reactor
+
+
+ def process(self):
+ """
+ Handle this request by connecting to the proxied server and forwarding
+ it there, then forwarding the response back as the response to this
+ request.
+ """
+ self.received_headers['host'] = self.factory.host
+ clientFactory = self.proxyClientFactoryClass(
+ self.method, self.uri, self.clientproto, self.getAllHeaders(),
+ self.content.read(), self)
+ self.reactor.connectTCP(self.factory.host, self.factory.port,
+ clientFactory)
+
+
+
+class ReverseProxy(HTTPChannel):
+ """
+ Implements a simple reverse proxy.
+
+ For details of usage, see the file examples/reverse-proxy.py.
+ """
+
+ requestFactory = ReverseProxyRequest
+
+
+
+class ReverseProxyResource(Resource):
+ """
+ Resource that renders the results gotten from another server
+
+ Put this resource in the tree to cause everything below it to be relayed
+ to a different server.
+
+ @ivar proxyClientFactoryClass: a proxy client factory class, used to create
+ new connections.
+ @type proxyClientFactoryClass: L{ClientFactory}
+
+ @ivar reactor: the reactor used to create connections.
+ @type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
+ """
+
+ proxyClientFactoryClass = ProxyClientFactory
+
+
+ def __init__(self, host, port, path, reactor=reactor):
+ """
+ @param host: the host of the web server to proxy.
+ @type host: C{str}
+
+ @param port: the port of the web server to proxy.
+ @type port: C{port}
+
+ @param path: the base path to fetch data from. Note that you shouldn't
+ put any trailing slashes in it, it will be added automatically in
+ request. For example, if you put B{/foo}, a request on B{/bar} will
+ be proxied to B{/foo/bar}. Any required encoding of special
+ characters (such as " " or "/") should have been done already.
+
+ @type path: C{str}
+ """
+ Resource.__init__(self)
+ self.host = host
+ self.port = port
+ self.path = path
+ self.reactor = reactor
+
+
+ def getChild(self, path, request):
+ """
+ Create and return a proxy resource with the same proxy configuration
+ as this one, except that its path also contains the segment given by
+ C{path} at the end.
+ """
+ return ReverseProxyResource(
+ self.host, self.port, self.path + '/' + urlquote(path, safe=""),
+ self.reactor)
+
+
+ def render(self, request):
+ """
+ Render a request by forwarding it to the proxied server.
+ """
+ # RFC 2616 tells us that we can omit the port if it's the default port,
+ # but we have to provide it otherwise
+ if self.port == 80:
+ host = self.host
+ else:
+ host = "%s:%d" % (self.host, self.port)
+ request.received_headers['host'] = host
+ request.content.seek(0, 0)
+ qs = urlparse.urlparse(request.uri)[4]
+ if qs:
+ rest = self.path + '?' + qs
+ else:
+ rest = self.path
+ clientFactory = self.proxyClientFactoryClass(
+ request.method, rest, request.clientproto,
+ request.getAllHeaders(), request.content.read(), request)
+ self.reactor.connectTCP(self.host, self.port, clientFactory)
+ return NOT_DONE_YET
diff --git a/twisted/web/resource.py b/twisted/web/resource.py
new file mode 100644
index 0000000..bf76ce2
--- /dev/null
+++ b/twisted/web/resource.py
@@ -0,0 +1,319 @@
+# -*- test-case-name: twisted.web.test.test_web -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Implementation of the lowest-level Resource class.
+"""
+
+import warnings
+
+from zope.interface import Attribute, implements, Interface
+
+from twisted.python.reflect import prefixedMethodNames
+from twisted.web import http
+
+
+class IResource(Interface):
+ """
+ A web resource.
+ """
+
+ isLeaf = Attribute(
+ """
+ Signal if this IResource implementor is a "leaf node" or not. If True,
+ getChildWithDefault will not be called on this Resource.
+ """)
+
+ def getChildWithDefault(name, request):
+ """
+ Return a child with the given name for the given request.
+ This is the external interface used by the Resource publishing
+ machinery. If implementing IResource without subclassing
+ Resource, it must be provided. However, if subclassing Resource,
+ getChild overridden instead.
+ """
+
+ def putChild(path, child):
+ """
+ Put a child IResource implementor at the given path.
+ """
+
+ def render(request):
+ """
+ Render a request. This is called on the leaf resource for
+ a request. Render must return either a string, which will
+ be sent to the browser as the HTML for the request, or
+ server.NOT_DONE_YET. If NOT_DONE_YET is returned,
+ at some point later (in a Deferred callback, usually)
+ call request.write("<html>") to write data to the request,
+ and request.finish() to send the data to the browser.
+
+ L{twisted.web.error.UnsupportedMethod} can be raised if the
+ HTTP verb requested is not supported by this resource.
+ """
+
+
+
+def getChildForRequest(resource, request):
+ """
+ Traverse resource tree to find who will handle the request.
+ """
+ while request.postpath and not resource.isLeaf:
+ pathElement = request.postpath.pop(0)
+ request.prepath.append(pathElement)
+ resource = resource.getChildWithDefault(pathElement, request)
+ return resource
+
+
+
+class Resource:
+ """
+ I define a web-accessible resource.
+
+ I serve 2 main purposes; one is to provide a standard representation for
+ what HTTP specification calls an 'entity', and the other is to provide an
+ abstract directory structure for URL retrieval.
+ """
+
+ implements(IResource)
+
+ entityType = IResource
+
+ server = None
+
+ def __init__(self):
+ """Initialize.
+ """
+ self.children = {}
+
+ isLeaf = 0
+
+ ### Abstract Collection Interface
+
+ def listStaticNames(self):
+ return self.children.keys()
+
+ def listStaticEntities(self):
+ return self.children.items()
+
+ def listNames(self):
+ return self.listStaticNames() + self.listDynamicNames()
+
+ def listEntities(self):
+ return self.listStaticEntities() + self.listDynamicEntities()
+
+ def listDynamicNames(self):
+ return []
+
+ def listDynamicEntities(self, request=None):
+ return []
+
+ def getStaticEntity(self, name):
+ return self.children.get(name)
+
+ def getDynamicEntity(self, name, request):
+ if not self.children.has_key(name):
+ return self.getChild(name, request)
+ else:
+ return None
+
+ def delEntity(self, name):
+ del self.children[name]
+
+ def reallyPutEntity(self, name, entity):
+ self.children[name] = entity
+
+ # Concrete HTTP interface
+
+ def getChild(self, path, request):
+ """
+ Retrieve a 'child' resource from me.
+
+ Implement this to create dynamic resource generation -- resources which
+ are always available may be registered with self.putChild().
+
+ This will not be called if the class-level variable 'isLeaf' is set in
+ your subclass; instead, the 'postpath' attribute of the request will be
+ left as a list of the remaining path elements.
+
+ For example, the URL /foo/bar/baz will normally be::
+
+ | site.resource.getChild('foo').getChild('bar').getChild('baz').
+
+ However, if the resource returned by 'bar' has isLeaf set to true, then
+ the getChild call will never be made on it.
+
+ @param path: a string, describing the child
+
+ @param request: a twisted.web.server.Request specifying meta-information
+ about the request that is being made for this child.
+ """
+ return NoResource("No such child resource.")
+
+
+ def getChildWithDefault(self, path, request):
+ """
+ Retrieve a static or dynamically generated child resource from me.
+
+ First checks if a resource was added manually by putChild, and then
+ call getChild to check for dynamic resources. Only override if you want
+ to affect behaviour of all child lookups, rather than just dynamic
+ ones.
+
+ This will check to see if I have a pre-registered child resource of the
+ given name, and call getChild if I do not.
+ """
+ if path in self.children:
+ return self.children[path]
+ return self.getChild(path, request)
+
+
+ def getChildForRequest(self, request):
+ warnings.warn("Please use module level getChildForRequest.", DeprecationWarning, 2)
+ return getChildForRequest(self, request)
+
+
+ def putChild(self, path, child):
+ """
+ Register a static child.
+
+ You almost certainly don't want '/' in your path. If you
+ intended to have the root of a folder, e.g. /foo/, you want
+ path to be ''.
+ """
+ self.children[path] = child
+ child.server = self.server
+
+
+ def render(self, request):
+ """
+ Render a given resource. See L{IResource}'s render method.
+
+ I delegate to methods of self with the form 'render_METHOD'
+ where METHOD is the HTTP that was used to make the
+ request. Examples: render_GET, render_HEAD, render_POST, and
+ so on. Generally you should implement those methods instead of
+ overriding this one.
+
+ render_METHOD methods are expected to return a string which
+ will be the rendered page, unless the return value is
+ twisted.web.server.NOT_DONE_YET, in which case it is this
+ class's responsibility to write the results to
+ request.write(data), then call request.finish().
+
+ Old code that overrides render() directly is likewise expected
+ to return a string or NOT_DONE_YET.
+ """
+ m = getattr(self, 'render_' + request.method, None)
+ if not m:
+ # This needs to be here until the deprecated subclasses of the
+ # below three error resources in twisted.web.error are removed.
+ from twisted.web.error import UnsupportedMethod
+ allowedMethods = (getattr(self, 'allowedMethods', 0) or
+ _computeAllowedMethods(self))
+ raise UnsupportedMethod(allowedMethods)
+ return m(request)
+
+
+ def render_HEAD(self, request):
+ """
+ Default handling of HEAD method.
+
+ I just return self.render_GET(request). When method is HEAD,
+ the framework will handle this correctly.
+ """
+ return self.render_GET(request)
+
+
+
+def _computeAllowedMethods(resource):
+ """
+ Compute the allowed methods on a C{Resource} based on defined render_FOO
+ methods. Used when raising C{UnsupportedMethod} but C{Resource} does
+ not define C{allowedMethods} attribute.
+ """
+ allowedMethods = []
+ for name in prefixedMethodNames(resource.__class__, "render_"):
+ allowedMethods.append(name)
+ return allowedMethods
+
+
+
+class ErrorPage(Resource):
+ """
+ L{ErrorPage} is a resource which responds with a particular
+ (parameterized) status and a body consisting of HTML containing some
+ descriptive text. This is useful for rendering simple error pages.
+
+ @ivar template: A C{str} which will have a dictionary interpolated into
+ it to generate the response body. The dictionary has the following
+ keys:
+
+ - C{"code"}: The status code passed to L{ErrorPage.__init__}.
+ - C{"brief"}: The brief description passed to L{ErrorPage.__init__}.
+ - C{"detail"}: The detailed description passed to
+ L{ErrorPage.__init__}.
+
+ @ivar code: An integer status code which will be used for the response.
+ @ivar brief: A short string which will be included in the response body.
+ @ivar detail: A longer string which will be included in the response body.
+ """
+
+ template = """
+<html>
+ <head><title>%(code)s - %(brief)s</title></head>
+ <body>
+ <h1>%(brief)s</h1>
+ <p>%(detail)s</p>
+ </body>
+</html>
+"""
+
+ def __init__(self, status, brief, detail):
+ Resource.__init__(self)
+ self.code = status
+ self.brief = brief
+ self.detail = detail
+
+
+ def render(self, request):
+ request.setResponseCode(self.code)
+ request.setHeader("content-type", "text/html; charset=utf-8")
+ return self.template % dict(
+ code=self.code,
+ brief=self.brief,
+ detail=self.detail)
+
+
+ def getChild(self, chnam, request):
+ return self
+
+
+
+class NoResource(ErrorPage):
+ """
+ L{NoResource} is a specialization of L{ErrorPage} which returns the HTTP
+ response code I{NOT FOUND}.
+ """
+ def __init__(self, message="Sorry. No luck finding that resource."):
+ ErrorPage.__init__(self, http.NOT_FOUND,
+ "No Such Resource",
+ message)
+
+
+
+class ForbiddenResource(ErrorPage):
+ """
+ L{ForbiddenResource} is a specialization of L{ErrorPage} which returns the
+ I{FORBIDDEN} HTTP response code.
+ """
+ def __init__(self, message="Sorry, resource is forbidden."):
+ ErrorPage.__init__(self, http.FORBIDDEN,
+ "Forbidden Resource",
+ message)
+
+
+__all__ = [
+ 'IResource', 'getChildForRequest',
+ 'Resource', 'ErrorPage', 'NoResource', 'ForbiddenResource']
diff --git a/twisted/web/rewrite.py b/twisted/web/rewrite.py
new file mode 100644
index 0000000..b5366b4
--- /dev/null
+++ b/twisted/web/rewrite.py
@@ -0,0 +1,52 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+from twisted.web import resource
+
+class RewriterResource(resource.Resource):
+
+ def __init__(self, orig, *rewriteRules):
+ resource.Resource.__init__(self)
+ self.resource = orig
+ self.rewriteRules = list(rewriteRules)
+
+ def _rewrite(self, request):
+ for rewriteRule in self.rewriteRules:
+ rewriteRule(request)
+
+ def getChild(self, path, request):
+ request.postpath.insert(0, path)
+ request.prepath.pop()
+ self._rewrite(request)
+ path = request.postpath.pop(0)
+ request.prepath.append(path)
+ return self.resource.getChildWithDefault(path, request)
+
+ def render(self, request):
+ self._rewrite(request)
+ return self.resource.render(request)
+
+
+def tildeToUsers(request):
+ if request.postpath and request.postpath[0][:1]=='~':
+ request.postpath[:1] = ['users', request.postpath[0][1:]]
+ request.path = '/'+'/'.join(request.prepath+request.postpath)
+
+def alias(aliasPath, sourcePath):
+ """
+ I am not a very good aliaser. But I'm the best I can be. If I'm
+ aliasing to a Resource that generates links, and it uses any parts
+ of request.prepath to do so, the links will not be relative to the
+ aliased path, but rather to the aliased-to path. That I can't
+ alias static.File directory listings that nicely. However, I can
+ still be useful, as many resources will play nice.
+ """
+ sourcePath = sourcePath.split('/')
+ aliasPath = aliasPath.split('/')
+ def rewriter(request):
+ if request.postpath[:len(aliasPath)] == aliasPath:
+ after = request.postpath[len(aliasPath):]
+ request.postpath = sourcePath + after
+ request.path = '/'+'/'.join(request.prepath+request.postpath)
+ return rewriter
diff --git a/twisted/web/script.py b/twisted/web/script.py
new file mode 100644
index 0000000..006464a
--- /dev/null
+++ b/twisted/web/script.py
@@ -0,0 +1,169 @@
+# -*- test-case-name: twisted.web.test.test_script -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+I contain PythonScript, which is a very simple python script resource.
+"""
+
+import os, traceback
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+from twisted import copyright
+from twisted.web import http, server, static, resource, html
+
+
+rpyNoResource = """<p>You forgot to assign to the variable "resource" in your script. For example:</p>
+<pre>
+# MyCoolWebApp.rpy
+
+import mygreatresource
+
+resource = mygreatresource.MyGreatResource()
+</pre>
+"""
+
+class AlreadyCached(Exception):
+ """This exception is raised when a path has already been cached.
+ """
+
+class CacheScanner:
+ def __init__(self, path, registry):
+ self.path = path
+ self.registry = registry
+ self.doCache = 0
+
+ def cache(self):
+ c = self.registry.getCachedPath(self.path)
+ if c is not None:
+ raise AlreadyCached(c)
+ self.recache()
+
+ def recache(self):
+ self.doCache = 1
+
+noRsrc = resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource)
+
+def ResourceScript(path, registry):
+ """
+ I am a normal py file which must define a 'resource' global, which should
+ be an instance of (a subclass of) web.resource.Resource; it will be
+ renderred.
+ """
+ cs = CacheScanner(path, registry)
+ glob = {'__file__': path,
+ 'resource': noRsrc,
+ 'registry': registry,
+ 'cache': cs.cache,
+ 'recache': cs.recache}
+ try:
+ execfile(path, glob, glob)
+ except AlreadyCached, ac:
+ return ac.args[0]
+ rsrc = glob['resource']
+ if cs.doCache and rsrc is not noRsrc:
+ registry.cachePath(path, rsrc)
+ return rsrc
+
+def ResourceTemplate(path, registry):
+ from quixote import ptl_compile
+
+ glob = {'__file__': path,
+ 'resource': resource.ErrorPage(500, "Whoops! Internal Error",
+ rpyNoResource),
+ 'registry': registry}
+
+ e = ptl_compile.compile_template(open(path), path)
+ exec e in glob
+ return glob['resource']
+
+
+class ResourceScriptWrapper(resource.Resource):
+
+ def __init__(self, path, registry=None):
+ resource.Resource.__init__(self)
+ self.path = path
+ self.registry = registry or static.Registry()
+
+ def render(self, request):
+ res = ResourceScript(self.path, self.registry)
+ return res.render(request)
+
+ def getChildWithDefault(self, path, request):
+ res = ResourceScript(self.path, self.registry)
+ return res.getChildWithDefault(path, request)
+
+
+
+class ResourceScriptDirectory(resource.Resource):
+ """
+ L{ResourceScriptDirectory} is a resource which serves scripts from a
+ filesystem directory. File children of a L{ResourceScriptDirectory} will
+ be served using L{ResourceScript}. Directory children will be served using
+ another L{ResourceScriptDirectory}.
+
+ @ivar path: A C{str} giving the filesystem path in which children will be
+ looked up.
+
+ @ivar registry: A L{static.Registry} instance which will be used to decide
+ how to interpret scripts found as children of this resource.
+ """
+ def __init__(self, pathname, registry=None):
+ resource.Resource.__init__(self)
+ self.path = pathname
+ self.registry = registry or static.Registry()
+
+ def getChild(self, path, request):
+ fn = os.path.join(self.path, path)
+
+ if os.path.isdir(fn):
+ return ResourceScriptDirectory(fn, self.registry)
+ if os.path.exists(fn):
+ return ResourceScript(fn, self.registry)
+ return resource.NoResource()
+
+ def render(self, request):
+ return resource.NoResource().render(request)
+
+
+class PythonScript(resource.Resource):
+ """I am an extremely simple dynamic resource; an embedded python script.
+
+ This will execute a file (usually of the extension '.epy') as Python code,
+ internal to the webserver.
+ """
+ isLeaf = 1
+ def __init__(self, filename, registry):
+ """Initialize me with a script name.
+ """
+ self.filename = filename
+ self.registry = registry
+
+ def render(self, request):
+ """Render me to a web client.
+
+ Load my file, execute it in a special namespace (with 'request' and
+ '__file__' global vars) and finish the request. Output to the web-page
+ will NOT be handled with print - standard output goes to the log - but
+ with request.write.
+ """
+ request.setHeader("x-powered-by","Twisted/%s" % copyright.version)
+ namespace = {'request': request,
+ '__file__': self.filename,
+ 'registry': self.registry}
+ try:
+ execfile(self.filename, namespace, namespace)
+ except IOError, e:
+ if e.errno == 2: #file not found
+ request.setResponseCode(http.NOT_FOUND)
+ request.write(resource.NoResource("File not found.").render(request))
+ except:
+ io = StringIO.StringIO()
+ traceback.print_exc(file=io)
+ request.write(html.PRE(io.getvalue()))
+ request.finish()
+ return server.NOT_DONE_YET
diff --git a/twisted/web/server.py b/twisted/web/server.py
new file mode 100644
index 0000000..d03bec6
--- /dev/null
+++ b/twisted/web/server.py
@@ -0,0 +1,592 @@
+# -*- test-case-name: twisted.web.test.test_web -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+This is a web-server which integrates with the twisted.internet
+infrastructure.
+"""
+
+# System Imports
+
+import warnings
+import string
+import types
+import copy
+import os
+from urllib import quote
+
+from zope.interface import implements
+
+from urllib import unquote
+
+#some useful constants
+NOT_DONE_YET = 1
+
+# Twisted Imports
+from twisted.spread import pb
+from twisted.internet import address, task
+from twisted.web import iweb, http
+from twisted.python import log, reflect, failure, components
+from twisted import copyright
+from twisted.web import util as webutil, resource
+from twisted.web.error import UnsupportedMethod
+from twisted.web.microdom import escape
+
+from twisted.python.versions import Version
+from twisted.python.deprecate import deprecatedModuleAttribute
+
+
+__all__ = [
+ 'supportedMethods',
+ 'Request',
+ 'Session',
+ 'Site',
+ 'version',
+ 'NOT_DONE_YET'
+]
+
+
+# backwards compatability
+deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "Please use twisted.web.http.datetimeToString instead",
+ "twisted.web.server",
+ "date_time_string")
+deprecatedModuleAttribute(
+ Version("Twisted", 12, 1, 0),
+ "Please use twisted.web.http.stringToDatetime instead",
+ "twisted.web.server",
+ "string_date_time")
+date_time_string = http.datetimeToString
+string_date_time = http.stringToDatetime
+
+# Support for other methods may be implemented on a per-resource basis.
+supportedMethods = ('GET', 'HEAD', 'POST')
+
+
+def _addressToTuple(addr):
+ if isinstance(addr, address.IPv4Address):
+ return ('INET', addr.host, addr.port)
+ elif isinstance(addr, address.UNIXAddress):
+ return ('UNIX', addr.name)
+ else:
+ return tuple(addr)
+
+class Request(pb.Copyable, http.Request, components.Componentized):
+ """
+ An HTTP request.
+
+ @ivar defaultContentType: A C{str} giving the default I{Content-Type} value
+ to send in responses if no other value is set. C{None} disables the
+ default.
+ """
+ implements(iweb.IRequest)
+
+ defaultContentType = "text/html"
+
+ site = None
+ appRootURL = None
+ __pychecker__ = 'unusednames=issuer'
+ _inFakeHead = False
+
+ def __init__(self, *args, **kw):
+ http.Request.__init__(self, *args, **kw)
+ components.Componentized.__init__(self)
+
+ def getStateToCopyFor(self, issuer):
+ x = self.__dict__.copy()
+ del x['transport']
+ # XXX refactor this attribute out; it's from protocol
+ # del x['server']
+ del x['channel']
+ del x['content']
+ del x['site']
+ self.content.seek(0, 0)
+ x['content_data'] = self.content.read()
+ x['remote'] = pb.ViewPoint(issuer, self)
+
+ # Address objects aren't jellyable
+ x['host'] = _addressToTuple(x['host'])
+ x['client'] = _addressToTuple(x['client'])
+
+ # Header objects also aren't jellyable.
+ x['requestHeaders'] = list(x['requestHeaders'].getAllRawHeaders())
+
+ return x
+
+ # HTML generation helpers
+
+ def sibLink(self, name):
+ "Return the text that links to a sibling of the requested resource."
+ if self.postpath:
+ return (len(self.postpath)*"../") + name
+ else:
+ return name
+
+ def childLink(self, name):
+ "Return the text that links to a child of the requested resource."
+ lpp = len(self.postpath)
+ if lpp > 1:
+ return ((lpp-1)*"../") + name
+ elif lpp == 1:
+ return name
+ else: # lpp == 0
+ if len(self.prepath) and self.prepath[-1]:
+ return self.prepath[-1] + '/' + name
+ else:
+ return name
+
+ def process(self):
+ "Process a request."
+
+ # get site from channel
+ self.site = self.channel.site
+
+ # set various default headers
+ self.setHeader('server', version)
+ self.setHeader('date', http.datetimeToString())
+
+ # Resource Identification
+ self.prepath = []
+ self.postpath = map(unquote, string.split(self.path[1:], '/'))
+ try:
+ resrc = self.site.getResourceFor(self)
+ self.render(resrc)
+ except:
+ self.processingFailed(failure.Failure())
+
+ def write(self, data):
+ """
+ Write data to the transport (if not responding to a HEAD request).
+
+ @param data: A string to write to the response.
+ """
+ if not self.startedWriting:
+ # Before doing the first write, check to see if a default
+ # Content-Type header should be supplied.
+ modified = self.code != http.NOT_MODIFIED
+ contentType = self.responseHeaders.getRawHeaders('content-type')
+ if modified and contentType is None and self.defaultContentType is not None:
+ self.responseHeaders.setRawHeaders(
+ 'content-type', [self.defaultContentType])
+
+ # Only let the write happen if we're not generating a HEAD response by
+ # faking out the request method. Note, if we are doing that,
+ # startedWriting will never be true, and the above logic may run
+ # multiple times. It will only actually change the responseHeaders once
+ # though, so it's still okay.
+ if not self._inFakeHead:
+ http.Request.write(self, data)
+
+
+ def render(self, resrc):
+ """
+ Ask a resource to render itself.
+
+ @param resrc: a L{twisted.web.resource.IResource}.
+ """
+ try:
+ body = resrc.render(self)
+ except UnsupportedMethod, e:
+ allowedMethods = e.allowedMethods
+ if (self.method == "HEAD") and ("GET" in allowedMethods):
+ # We must support HEAD (RFC 2616, 5.1.1). If the
+ # resource doesn't, fake it by giving the resource
+ # a 'GET' request and then return only the headers,
+ # not the body.
+ log.msg("Using GET to fake a HEAD request for %s" %
+ (resrc,))
+ self.method = "GET"
+ self._inFakeHead = True
+ body = resrc.render(self)
+
+ if body is NOT_DONE_YET:
+ log.msg("Tried to fake a HEAD request for %s, but "
+ "it got away from me." % resrc)
+ # Oh well, I guess we won't include the content length.
+ else:
+ self.setHeader('content-length', str(len(body)))
+
+ self._inFakeHead = False
+ self.method = "HEAD"
+ self.write('')
+ self.finish()
+ return
+
+ if self.method in (supportedMethods):
+ # We MUST include an Allow header
+ # (RFC 2616, 10.4.6 and 14.7)
+ self.setHeader('Allow', ', '.join(allowedMethods))
+ s = ('''Your browser approached me (at %(URI)s) with'''
+ ''' the method "%(method)s". I only allow'''
+ ''' the method%(plural)s %(allowed)s here.''' % {
+ 'URI': escape(self.uri),
+ 'method': self.method,
+ 'plural': ((len(allowedMethods) > 1) and 's') or '',
+ 'allowed': string.join(allowedMethods, ', ')
+ })
+ epage = resource.ErrorPage(http.NOT_ALLOWED,
+ "Method Not Allowed", s)
+ body = epage.render(self)
+ else:
+ epage = resource.ErrorPage(
+ http.NOT_IMPLEMENTED, "Huh?",
+ "I don't know how to treat a %s request." %
+ (escape(self.method),))
+ body = epage.render(self)
+ # end except UnsupportedMethod
+
+ if body == NOT_DONE_YET:
+ return
+ if type(body) is not types.StringType:
+ body = resource.ErrorPage(
+ http.INTERNAL_SERVER_ERROR,
+ "Request did not return a string",
+ "Request: " + html.PRE(reflect.safe_repr(self)) + "<br />" +
+ "Resource: " + html.PRE(reflect.safe_repr(resrc)) + "<br />" +
+ "Value: " + html.PRE(reflect.safe_repr(body))).render(self)
+
+ if self.method == "HEAD":
+ if len(body) > 0:
+ # This is a Bad Thing (RFC 2616, 9.4)
+ log.msg("Warning: HEAD request %s for resource %s is"
+ " returning a message body."
+ " I think I'll eat it."
+ % (self, resrc))
+ self.setHeader('content-length', str(len(body)))
+ self.write('')
+ else:
+ self.setHeader('content-length', str(len(body)))
+ self.write(body)
+ self.finish()
+
+ def processingFailed(self, reason):
+ log.err(reason)
+ if self.site.displayTracebacks:
+ body = ("<html><head><title>web.Server Traceback (most recent call last)</title></head>"
+ "<body><b>web.Server Traceback (most recent call last):</b>\n\n"
+ "%s\n\n</body></html>\n"
+ % webutil.formatFailure(reason))
+ else:
+ body = ("<html><head><title>Processing Failed</title></head><body>"
+ "<b>Processing Failed</b></body></html>")
+
+ self.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ self.setHeader('content-type',"text/html")
+ self.setHeader('content-length', str(len(body)))
+ self.write(body)
+ self.finish()
+ return reason
+
+ def view_write(self, issuer, data):
+ """Remote version of write; same interface.
+ """
+ self.write(data)
+
+ def view_finish(self, issuer):
+ """Remote version of finish; same interface.
+ """
+ self.finish()
+
+ def view_addCookie(self, issuer, k, v, **kwargs):
+ """Remote version of addCookie; same interface.
+ """
+ self.addCookie(k, v, **kwargs)
+
+ def view_setHeader(self, issuer, k, v):
+ """Remote version of setHeader; same interface.
+ """
+ self.setHeader(k, v)
+
+ def view_setLastModified(self, issuer, when):
+ """Remote version of setLastModified; same interface.
+ """
+ self.setLastModified(when)
+
+ def view_setETag(self, issuer, tag):
+ """Remote version of setETag; same interface.
+ """
+ self.setETag(tag)
+
+
+ def view_setResponseCode(self, issuer, code, message=None):
+ """
+ Remote version of setResponseCode; same interface.
+ """
+ self.setResponseCode(code, message)
+
+
+ def view_registerProducer(self, issuer, producer, streaming):
+ """Remote version of registerProducer; same interface.
+ (requires a remote producer.)
+ """
+ self.registerProducer(_RemoteProducerWrapper(producer), streaming)
+
+ def view_unregisterProducer(self, issuer):
+ self.unregisterProducer()
+
+ ### these calls remain local
+
+ session = None
+
+ def getSession(self, sessionInterface = None):
+ # Session management
+ if not self.session:
+ cookiename = string.join(['TWISTED_SESSION'] + self.sitepath, "_")
+ sessionCookie = self.getCookie(cookiename)
+ if sessionCookie:
+ try:
+ self.session = self.site.getSession(sessionCookie)
+ except KeyError:
+ pass
+ # if it still hasn't been set, fix it up.
+ if not self.session:
+ self.session = self.site.makeSession()
+ self.addCookie(cookiename, self.session.uid, path='/')
+ self.session.touch()
+ if sessionInterface:
+ return self.session.getComponent(sessionInterface)
+ return self.session
+
+ def _prePathURL(self, prepath):
+ port = self.getHost().port
+ if self.isSecure():
+ default = 443
+ else:
+ default = 80
+ if port == default:
+ hostport = ''
+ else:
+ hostport = ':%d' % port
+ return 'http%s://%s%s/%s' % (
+ self.isSecure() and 's' or '',
+ self.getRequestHostname(),
+ hostport,
+ '/'.join([quote(segment, safe='') for segment in prepath]))
+
+ def prePathURL(self):
+ return self._prePathURL(self.prepath)
+
+ def URLPath(self):
+ from twisted.python import urlpath
+ return urlpath.URLPath.fromRequest(self)
+
+ def rememberRootURL(self):
+ """
+ Remember the currently-processed part of the URL for later
+ recalling.
+ """
+ url = self._prePathURL(self.prepath[:-1])
+ self.appRootURL = url
+
+ def getRootURL(self):
+ """
+ Get a previously-remembered URL.
+ """
+ return self.appRootURL
+
+
+class _RemoteProducerWrapper:
+ def __init__(self, remote):
+ self.resumeProducing = remote.remoteMethod("resumeProducing")
+ self.pauseProducing = remote.remoteMethod("pauseProducing")
+ self.stopProducing = remote.remoteMethod("stopProducing")
+
+
+class Session(components.Componentized):
+ """
+ A user's session with a system.
+
+ This utility class contains no functionality, but is used to
+ represent a session.
+
+ @ivar _reactor: An object providing L{IReactorTime} to use for scheduling
+ expiration.
+ @ivar sessionTimeout: timeout of a session, in seconds.
+ @ivar loopFactory: Deprecated in Twisted 9.0. Does nothing. Do not use.
+ """
+ sessionTimeout = 900
+ loopFactory = task.LoopingCall
+
+ _expireCall = None
+
+ def __init__(self, site, uid, reactor=None):
+ """
+ Initialize a session with a unique ID for that session.
+ """
+ components.Componentized.__init__(self)
+
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+ self.site = site
+ self.uid = uid
+ self.expireCallbacks = []
+ self.touch()
+ self.sessionNamespaces = {}
+
+
+ def startCheckingExpiration(self, lifetime=None):
+ """
+ Start expiration tracking.
+
+ @param lifetime: Ignored; deprecated.
+
+ @return: C{None}
+ """
+ if lifetime is not None:
+ warnings.warn(
+ "The lifetime parameter to startCheckingExpiration is "
+ "deprecated since Twisted 9.0. See Session.sessionTimeout "
+ "instead.", DeprecationWarning, stacklevel=2)
+ self._expireCall = self._reactor.callLater(
+ self.sessionTimeout, self.expire)
+
+
+ def notifyOnExpire(self, callback):
+ """
+ Call this callback when the session expires or logs out.
+ """
+ self.expireCallbacks.append(callback)
+
+
+ def expire(self):
+ """
+ Expire/logout of the session.
+ """
+ del self.site.sessions[self.uid]
+ for c in self.expireCallbacks:
+ c()
+ self.expireCallbacks = []
+ if self._expireCall and self._expireCall.active():
+ self._expireCall.cancel()
+ # Break reference cycle.
+ self._expireCall = None
+
+
+ def touch(self):
+ """
+ Notify session modification.
+ """
+ self.lastModified = self._reactor.seconds()
+ if self._expireCall is not None:
+ self._expireCall.reset(self.sessionTimeout)
+
+
+ def checkExpired(self):
+ """
+ Deprecated; does nothing.
+ """
+ warnings.warn(
+ "Session.checkExpired is deprecated since Twisted 9.0; sessions "
+ "check themselves now, you don't need to.",
+ stacklevel=2, category=DeprecationWarning)
+
+
+version = "TwistedWeb/%s" % copyright.version
+
+
+class Site(http.HTTPFactory):
+ """
+ A web site: manage log, sessions, and resources.
+
+ @ivar counter: increment value used for generating unique sessions ID.
+ @ivar requestFactory: factory creating requests objects. Default to
+ L{Request}.
+ @ivar displayTracebacks: if set, Twisted internal errors are displayed on
+ rendered pages. Default to C{True}.
+ @ivar sessionFactory: factory for sessions objects. Default to L{Session}.
+ @ivar sessionCheckTime: Deprecated. See L{Session.sessionTimeout} instead.
+ """
+ counter = 0
+ requestFactory = Request
+ displayTracebacks = True
+ sessionFactory = Session
+ sessionCheckTime = 1800
+
+ def __init__(self, resource, logPath=None, timeout=60*60*12):
+ """
+ Initialize.
+ """
+ http.HTTPFactory.__init__(self, logPath=logPath, timeout=timeout)
+ self.sessions = {}
+ self.resource = resource
+
+ def _openLogFile(self, path):
+ from twisted.python import logfile
+ return logfile.LogFile(os.path.basename(path), os.path.dirname(path))
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ d['sessions'] = {}
+ return d
+
+ def _mkuid(self):
+ """
+ (internal) Generate an opaque, unique ID for a user's session.
+ """
+ from twisted.python.hashlib import md5
+ import random
+ self.counter = self.counter + 1
+ return md5("%s_%s" % (str(random.random()) , str(self.counter))).hexdigest()
+
+ def makeSession(self):
+ """
+ Generate a new Session instance, and store it for future reference.
+ """
+ uid = self._mkuid()
+ session = self.sessions[uid] = self.sessionFactory(self, uid)
+ session.startCheckingExpiration()
+ return session
+
+ def getSession(self, uid):
+ """
+ Get a previously generated session, by its unique ID.
+ This raises a KeyError if the session is not found.
+ """
+ return self.sessions[uid]
+
+ def buildProtocol(self, addr):
+ """
+ Generate a channel attached to this site.
+ """
+ channel = http.HTTPFactory.buildProtocol(self, addr)
+ channel.requestFactory = self.requestFactory
+ channel.site = self
+ return channel
+
+ isLeaf = 0
+
+ def render(self, request):
+ """
+ Redirect because a Site is always a directory.
+ """
+ request.redirect(request.prePathURL() + '/')
+ request.finish()
+
+ def getChildWithDefault(self, pathEl, request):
+ """
+ Emulate a resource's getChild method.
+ """
+ request.site = self
+ return self.resource.getChildWithDefault(pathEl, request)
+
+ def getResourceFor(self, request):
+ """
+ Get a resource for a request.
+
+ This iterates through the resource heirarchy, calling
+ getChildWithDefault on each resource it finds for a path element,
+ stopping when it hits an element where isLeaf is true.
+ """
+ request.site = self
+ # Sitepath is used to determine cookie names between distributed
+ # servers and disconnected sites.
+ request.sitepath = copy.copy(request.prepath)
+ return resource.getChildForRequest(self.resource, request)
+
+
+import html
diff --git a/twisted/web/soap.py b/twisted/web/soap.py
new file mode 100644
index 0000000..1ca747b
--- /dev/null
+++ b/twisted/web/soap.py
@@ -0,0 +1,154 @@
+# -*- test-case-name: twisted.web.test.test_soap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+SOAP support for twisted.web.
+
+Requires SOAPpy 0.10.1 or later.
+
+Maintainer: Itamar Shtull-Trauring
+
+Future plans:
+SOAPContext support of some kind.
+Pluggable method lookup policies.
+"""
+
+# SOAPpy
+import SOAPpy
+
+# twisted imports
+from twisted.web import server, resource, client
+from twisted.internet import defer
+
+
+class SOAPPublisher(resource.Resource):
+ """Publish SOAP methods.
+
+ By default, publish methods beginning with 'soap_'. If the method
+ has an attribute 'useKeywords', it well get the arguments passed
+ as keyword args.
+ """
+
+ isLeaf = 1
+
+ # override to change the encoding used for responses
+ encoding = "UTF-8"
+
+ def lookupFunction(self, functionName):
+ """Lookup published SOAP function.
+
+ Override in subclasses. Default behaviour - publish methods
+ starting with soap_.
+
+ @return: callable or None if not found.
+ """
+ return getattr(self, "soap_%s" % functionName, None)
+
+ def render(self, request):
+ """Handle a SOAP command."""
+ data = request.content.read()
+
+ p, header, body, attrs = SOAPpy.parseSOAPRPC(data, 1, 1, 1)
+
+ methodName, args, kwargs, ns = p._name, p._aslist, p._asdict, p._ns
+
+ # deal with changes in SOAPpy 0.11
+ if callable(args):
+ args = args()
+ if callable(kwargs):
+ kwargs = kwargs()
+
+ function = self.lookupFunction(methodName)
+
+ if not function:
+ self._methodNotFound(request, methodName)
+ return server.NOT_DONE_YET
+ else:
+ if hasattr(function, "useKeywords"):
+ keywords = {}
+ for k, v in kwargs.items():
+ keywords[str(k)] = v
+ d = defer.maybeDeferred(function, **keywords)
+ else:
+ d = defer.maybeDeferred(function, *args)
+
+ d.addCallback(self._gotResult, request, methodName)
+ d.addErrback(self._gotError, request, methodName)
+ return server.NOT_DONE_YET
+
+ def _methodNotFound(self, request, methodName):
+ response = SOAPpy.buildSOAP(SOAPpy.faultType("%s:Client" %
+ SOAPpy.NS.ENV_T, "Method %s not found" % methodName),
+ encoding=self.encoding)
+ self._sendResponse(request, response, status=500)
+
+ def _gotResult(self, result, request, methodName):
+ if not isinstance(result, SOAPpy.voidType):
+ result = {"Result": result}
+ response = SOAPpy.buildSOAP(kw={'%sResponse' % methodName: result},
+ encoding=self.encoding)
+ self._sendResponse(request, response)
+
+ def _gotError(self, failure, request, methodName):
+ e = failure.value
+ if isinstance(e, SOAPpy.faultType):
+ fault = e
+ else:
+ fault = SOAPpy.faultType("%s:Server" % SOAPpy.NS.ENV_T,
+ "Method %s failed." % methodName)
+ response = SOAPpy.buildSOAP(fault, encoding=self.encoding)
+ self._sendResponse(request, response, status=500)
+
+ def _sendResponse(self, request, response, status=200):
+ request.setResponseCode(status)
+
+ if self.encoding is not None:
+ mimeType = 'text/xml; charset="%s"' % self.encoding
+ else:
+ mimeType = "text/xml"
+ request.setHeader("Content-type", mimeType)
+ request.setHeader("Content-length", str(len(response)))
+ request.write(response)
+ request.finish()
+
+
+class Proxy:
+ """A Proxy for making remote SOAP calls.
+
+ Pass the URL of the remote SOAP server to the constructor.
+
+ Use proxy.callRemote('foobar', 1, 2) to call remote method
+ 'foobar' with args 1 and 2, proxy.callRemote('foobar', x=1)
+ will call foobar with named argument 'x'.
+ """
+
+ # at some point this should have encoding etc. kwargs
+ def __init__(self, url, namespace=None, header=None):
+ self.url = url
+ self.namespace = namespace
+ self.header = header
+
+ def _cbGotResult(self, result):
+ result = SOAPpy.parseSOAPRPC(result)
+ if hasattr(result, 'Result'):
+ return result.Result
+ elif len(result) == 1:
+ ## SOAPpy 0.11.6 wraps the return results in a containing structure.
+ ## This check added to make Proxy behaviour emulate SOAPProxy, which
+ ## flattens the structure by default.
+ ## This behaviour is OK because even singleton lists are wrapped in
+ ## another singleton structType, which is almost always useless.
+ return result[0]
+ else:
+ return result
+
+ def callRemote(self, method, *args, **kwargs):
+ payload = SOAPpy.buildSOAP(args=args, kw=kwargs, method=method,
+ header=self.header, namespace=self.namespace)
+ return client.getPage(self.url, postdata=payload, method="POST",
+ headers={'content-type': 'text/xml',
+ 'SOAPAction': method}
+ ).addCallback(self._cbGotResult)
+
diff --git a/twisted/web/static.py b/twisted/web/static.py
new file mode 100644
index 0000000..07f136f
--- /dev/null
+++ b/twisted/web/static.py
@@ -0,0 +1,1083 @@
+# -*- test-case-name: twisted.web.test.test_static -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Static resources for L{twisted.web}.
+"""
+
+import os
+import warnings
+import urllib
+import itertools
+import cgi
+import time
+
+from zope.interface import implements
+
+from twisted.web import server
+from twisted.web import resource
+from twisted.web import http
+from twisted.web.util import redirectTo
+
+from twisted.python import components, filepath, log
+from twisted.internet import abstract, interfaces
+from twisted.spread import pb
+from twisted.persisted import styles
+from twisted.python.util import InsensitiveDict
+from twisted.python.runtime import platformType
+
+
+dangerousPathError = resource.NoResource("Invalid request URL.")
+
+def isDangerous(path):
+ return path == '..' or '/' in path or os.sep in path
+
+
+class Data(resource.Resource):
+ """
+ This is a static, in-memory resource.
+ """
+
+ def __init__(self, data, type):
+ resource.Resource.__init__(self)
+ self.data = data
+ self.type = type
+
+
+ def render_GET(self, request):
+ request.setHeader("content-type", self.type)
+ request.setHeader("content-length", str(len(self.data)))
+ if request.method == "HEAD":
+ return ''
+ return self.data
+ render_HEAD = render_GET
+
+
+def addSlash(request):
+ qs = ''
+ qindex = request.uri.find('?')
+ if qindex != -1:
+ qs = request.uri[qindex:]
+
+ return "http%s://%s%s/%s" % (
+ request.isSecure() and 's' or '',
+ request.getHeader("host"),
+ (request.uri.split('?')[0]),
+ qs)
+
+class Redirect(resource.Resource):
+ def __init__(self, request):
+ resource.Resource.__init__(self)
+ self.url = addSlash(request)
+
+ def render(self, request):
+ return redirectTo(self.url, request)
+
+
+class Registry(components.Componentized, styles.Versioned):
+ """
+ I am a Componentized object that will be made available to internal Twisted
+ file-based dynamic web content such as .rpy and .epy scripts.
+ """
+
+ def __init__(self):
+ components.Componentized.__init__(self)
+ self._pathCache = {}
+
+ persistenceVersion = 1
+
+ def upgradeToVersion1(self):
+ self._pathCache = {}
+
+ def cachePath(self, path, rsrc):
+ self._pathCache[path] = rsrc
+
+ def getCachedPath(self, path):
+ return self._pathCache.get(path)
+
+
+def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
+ """
+ Multiple file locations containing mime-types can be passed as a list.
+ The files will be sourced in that order, overriding mime-types from the
+ files sourced beforehand, but only if a new entry explicitly overrides
+ the current entry.
+ """
+ import mimetypes
+ # Grab Python's built-in mimetypes dictionary.
+ contentTypes = mimetypes.types_map
+ # Update Python's semi-erroneous dictionary with a few of the
+ # usual suspects.
+ contentTypes.update(
+ {
+ '.conf': 'text/plain',
+ '.diff': 'text/plain',
+ '.exe': 'application/x-executable',
+ '.flac': 'audio/x-flac',
+ '.java': 'text/plain',
+ '.ogg': 'application/ogg',
+ '.oz': 'text/x-oz',
+ '.swf': 'application/x-shockwave-flash',
+ '.tgz': 'application/x-gtar',
+ '.wml': 'text/vnd.wap.wml',
+ '.xul': 'application/vnd.mozilla.xul+xml',
+ '.py': 'text/plain',
+ '.patch': 'text/plain',
+ }
+ )
+ # Users can override these mime-types by loading them out configuration
+ # files (this defaults to ['/etc/mime.types']).
+ for location in mimetype_locations:
+ if os.path.exists(location):
+ more = mimetypes.read_mime_types(location)
+ if more is not None:
+ contentTypes.update(more)
+
+ return contentTypes
+
+def getTypeAndEncoding(filename, types, encodings, defaultType):
+ p, ext = os.path.splitext(filename)
+ ext = ext.lower()
+ if encodings.has_key(ext):
+ enc = encodings[ext]
+ ext = os.path.splitext(p)[1].lower()
+ else:
+ enc = None
+ type = types.get(ext, defaultType)
+ return type, enc
+
+
+
+class File(resource.Resource, styles.Versioned, filepath.FilePath):
+ """
+ File is a resource that represents a plain non-interpreted file
+ (although it can look for an extension like .rpy or .cgi and hand the
+ file to a processor for interpretation if you wish). Its constructor
+ takes a file path.
+
+ Alternatively, you can give a directory path to the constructor. In this
+ case the resource will represent that directory, and its children will
+ be files underneath that directory. This provides access to an entire
+ filesystem tree with a single Resource.
+
+ If you map the URL 'http://server/FILE' to a resource created as
+ File('/tmp'), then http://server/FILE/ will return an HTML-formatted
+ listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
+ return the contents of /tmp/foo/bar.html .
+
+ @cvar childNotFound: L{Resource} used to render 404 Not Found error pages.
+ """
+
+ contentTypes = loadMimeTypes()
+
+ contentEncodings = {
+ ".gz" : "gzip",
+ ".bz2": "bzip2"
+ }
+
+ processors = {}
+
+ indexNames = ["index", "index.html", "index.htm", "index.rpy"]
+
+ type = None
+
+ ### Versioning
+
+ persistenceVersion = 6
+
+ def upgradeToVersion6(self):
+ self.ignoredExts = []
+ if self.allowExt:
+ self.ignoreExt("*")
+ del self.allowExt
+
+
+ def upgradeToVersion5(self):
+ if not isinstance(self.registry, Registry):
+ self.registry = Registry()
+
+
+ def upgradeToVersion4(self):
+ if not hasattr(self, 'registry'):
+ self.registry = {}
+
+
+ def upgradeToVersion3(self):
+ if not hasattr(self, 'allowExt'):
+ self.allowExt = 0
+
+
+ def upgradeToVersion2(self):
+ self.defaultType = "text/html"
+
+
+ def upgradeToVersion1(self):
+ if hasattr(self, 'indexName'):
+ self.indexNames = [self.indexName]
+ del self.indexName
+
+
+ def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0):
+ """
+ Create a file with the given path.
+
+ @param path: The filename of the file from which this L{File} will
+ serve data.
+ @type path: C{str}
+
+ @param defaultType: A I{major/minor}-style MIME type specifier
+ indicating the I{Content-Type} with which this L{File}'s data
+ will be served if a MIME type cannot be determined based on
+ C{path}'s extension.
+ @type defaultType: C{str}
+
+ @param ignoredExts: A sequence giving the extensions of paths in the
+ filesystem which will be ignored for the purposes of child
+ lookup. For example, if C{ignoredExts} is C{(".bar",)} and
+ C{path} is a directory containing a file named C{"foo.bar"}, a
+ request for the C{"foo"} child of this resource will succeed
+ with a L{File} pointing to C{"foo.bar"}.
+
+ @param registry: The registry object being used to handle this
+ request. If C{None}, one will be created.
+ @type registry: L{Registry}
+
+ @param allowExt: Ignored parameter, only present for backwards
+ compatibility. Do not pass a value for this parameter.
+ """
+ resource.Resource.__init__(self)
+ filepath.FilePath.__init__(self, path)
+ self.defaultType = defaultType
+ if ignoredExts in (0, 1) or allowExt:
+ warnings.warn("ignoredExts should receive a list, not a boolean")
+ if ignoredExts or allowExt:
+ self.ignoredExts = ['*']
+ else:
+ self.ignoredExts = []
+ else:
+ self.ignoredExts = list(ignoredExts)
+ self.registry = registry or Registry()
+
+
+ def ignoreExt(self, ext):
+ """Ignore the given extension.
+
+ Serve file.ext if file is requested
+ """
+ self.ignoredExts.append(ext)
+
+ childNotFound = resource.NoResource("File not found.")
+
+ def directoryListing(self):
+ return DirectoryLister(self.path,
+ self.listNames(),
+ self.contentTypes,
+ self.contentEncodings,
+ self.defaultType)
+
+
+ def getChild(self, path, request):
+ """
+ If this L{File}'s path refers to a directory, return a L{File}
+ referring to the file named C{path} in that directory.
+
+ If C{path} is the empty string, return a L{DirectoryLister} instead.
+ """
+ self.restat(reraise=False)
+
+ if not self.isdir():
+ return self.childNotFound
+
+ if path:
+ try:
+ fpath = self.child(path)
+ except filepath.InsecurePath:
+ return self.childNotFound
+ else:
+ fpath = self.childSearchPreauth(*self.indexNames)
+ if fpath is None:
+ return self.directoryListing()
+
+ if not fpath.exists():
+ fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
+ if fpath is None:
+ return self.childNotFound
+
+ if platformType == "win32":
+ # don't want .RPY to be different than .rpy, since that would allow
+ # source disclosure.
+ processor = InsensitiveDict(self.processors).get(fpath.splitext()[1])
+ else:
+ processor = self.processors.get(fpath.splitext()[1])
+ if processor:
+ return resource.IResource(processor(fpath.path, self.registry))
+ return self.createSimilarFile(fpath.path)
+
+
+ # methods to allow subclasses to e.g. decrypt files on the fly:
+ def openForReading(self):
+ """Open a file and return it."""
+ return self.open()
+
+
+ def getFileSize(self):
+ """Return file size."""
+ return self.getsize()
+
+
+ def _parseRangeHeader(self, range):
+ """
+ Parse the value of a Range header into (start, stop) pairs.
+
+ In a given pair, either of start or stop can be None, signifying that
+ no value was provided, but not both.
+
+ @return: A list C{[(start, stop)]} of pairs of length at least one.
+
+ @raise ValueError: if the header is syntactically invalid or if the
+ Bytes-Unit is anything other than 'bytes'.
+ """
+ try:
+ kind, value = range.split('=', 1)
+ except ValueError:
+ raise ValueError("Missing '=' separator")
+ kind = kind.strip()
+ if kind != 'bytes':
+ raise ValueError("Unsupported Bytes-Unit: %r" % (kind,))
+ unparsedRanges = filter(None, map(str.strip, value.split(',')))
+ parsedRanges = []
+ for byteRange in unparsedRanges:
+ try:
+ start, end = byteRange.split('-', 1)
+ except ValueError:
+ raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
+ if start:
+ try:
+ start = int(start)
+ except ValueError:
+ raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
+ else:
+ start = None
+ if end:
+ try:
+ end = int(end)
+ except ValueError:
+ raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
+ else:
+ end = None
+ if start is not None:
+ if end is not None and start > end:
+ # Start must be less than or equal to end or it is invalid.
+ raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
+ elif end is None:
+ # One or both of start and end must be specified. Omitting
+ # both is invalid.
+ raise ValueError("Invalid Byte-Range: %r" % (byteRange,))
+ parsedRanges.append((start, end))
+ return parsedRanges
+
+
+ def _rangeToOffsetAndSize(self, start, end):
+ """
+ Convert a start and end from a Range header to an offset and size.
+
+ This method checks that the resulting range overlaps with the resource
+ being served (and so has the value of C{getFileSize()} as an indirect
+ input).
+
+ Either but not both of start or end can be C{None}:
+
+ - Omitted start means that the end value is actually a start value
+ relative to the end of the resource.
+
+ - Omitted end means the end of the resource should be the end of
+ the range.
+
+ End is interpreted as inclusive, as per RFC 2616.
+
+ If this range doesn't overlap with any of this resource, C{(0, 0)} is
+ returned, which is not otherwise a value return value.
+
+ @param start: The start value from the header, or C{None} if one was
+ not present.
+ @param end: The end value from the header, or C{None} if one was not
+ present.
+ @return: C{(offset, size)} where offset is how far into this resource
+ this resource the range begins and size is how long the range is,
+ or C{(0, 0)} if the range does not overlap this resource.
+ """
+ size = self.getFileSize()
+ if start is None:
+ start = size - end
+ end = size
+ elif end is None:
+ end = size
+ elif end < size:
+ end += 1
+ elif end > size:
+ end = size
+ if start >= size:
+ start = end = 0
+ return start, (end - start)
+
+
+ def _contentRange(self, offset, size):
+ """
+ Return a string suitable for the value of a Content-Range header for a
+ range with the given offset and size.
+
+ The offset and size are not sanity checked in any way.
+
+ @param offset: How far into this resource the range begins.
+ @param size: How long the range is.
+ @return: The value as appropriate for the value of a Content-Range
+ header.
+ """
+ return 'bytes %d-%d/%d' % (
+ offset, offset + size - 1, self.getFileSize())
+
+
+ def _doSingleRangeRequest(self, request, (start, end)):
+ """
+ Set up the response for Range headers that specify a single range.
+
+ This method checks if the request is satisfiable and sets the response
+ code and Content-Range header appropriately. The return value
+ indicates which part of the resource to return.
+
+ @param request: The Request object.
+ @param start: The start of the byte range as specified by the header.
+ @param end: The end of the byte range as specified by the header. At
+ most one of C{start} and C{end} may be C{None}.
+ @return: A 2-tuple of the offset and size of the range to return.
+ offset == size == 0 indicates that the request is not satisfiable.
+ """
+ offset, size = self._rangeToOffsetAndSize(start, end)
+ if offset == size == 0:
+ # This range doesn't overlap with any of this resource, so the
+ # request is unsatisfiable.
+ request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
+ request.setHeader(
+ 'content-range', 'bytes */%d' % (self.getFileSize(),))
+ else:
+ request.setResponseCode(http.PARTIAL_CONTENT)
+ request.setHeader(
+ 'content-range', self._contentRange(offset, size))
+ return offset, size
+
+
+ def _doMultipleRangeRequest(self, request, byteRanges):
+ """
+ Set up the response for Range headers that specify a single range.
+
+ This method checks if the request is satisfiable and sets the response
+ code and Content-Type and Content-Length headers appropriately. The
+ return value, which is a little complicated, indicates which parts of
+ the resource to return and the boundaries that should separate the
+ parts.
+
+ In detail, the return value is a tuple rangeInfo C{rangeInfo} is a
+ list of 3-tuples C{(partSeparator, partOffset, partSize)}. The
+ response to this request should be, for each element of C{rangeInfo},
+ C{partSeparator} followed by C{partSize} bytes of the resource
+ starting at C{partOffset}. Each C{partSeparator} includes the
+ MIME-style boundary and the part-specific Content-type and
+ Content-range headers. It is convenient to return the separator as a
+ concrete string from this method, becasue this method needs to compute
+ the number of bytes that will make up the response to be able to set
+ the Content-Length header of the response accurately.
+
+ @param request: The Request object.
+ @param byteRanges: A list of C{(start, end)} values as specified by
+ the header. For each range, at most one of C{start} and C{end}
+ may be C{None}.
+ @return: See above.
+ """
+ matchingRangeFound = False
+ rangeInfo = []
+ contentLength = 0
+ boundary = "%x%x" % (int(time.time()*1000000), os.getpid())
+ if self.type:
+ contentType = self.type
+ else:
+ contentType = 'bytes' # It's what Apache does...
+ for start, end in byteRanges:
+ partOffset, partSize = self._rangeToOffsetAndSize(start, end)
+ if partOffset == partSize == 0:
+ continue
+ contentLength += partSize
+ matchingRangeFound = True
+ partContentRange = self._contentRange(partOffset, partSize)
+ partSeparator = (
+ "\r\n"
+ "--%s\r\n"
+ "Content-type: %s\r\n"
+ "Content-range: %s\r\n"
+ "\r\n") % (boundary, contentType, partContentRange)
+ contentLength += len(partSeparator)
+ rangeInfo.append((partSeparator, partOffset, partSize))
+ if not matchingRangeFound:
+ request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
+ request.setHeader(
+ 'content-length', '0')
+ request.setHeader(
+ 'content-range', 'bytes */%d' % (self.getFileSize(),))
+ return [], ''
+ finalBoundary = "\r\n--" + boundary + "--\r\n"
+ rangeInfo.append((finalBoundary, 0, 0))
+ request.setResponseCode(http.PARTIAL_CONTENT)
+ request.setHeader(
+ 'content-type', 'multipart/byteranges; boundary="%s"' % (boundary,))
+ request.setHeader(
+ 'content-length', contentLength + len(finalBoundary))
+ return rangeInfo
+
+
+ def _setContentHeaders(self, request, size=None):
+ """
+ Set the Content-length and Content-type headers for this request.
+
+ This method is not appropriate for requests for multiple byte ranges;
+ L{_doMultipleRangeRequest} will set these headers in that case.
+
+ @param request: The L{Request} object.
+ @param size: The size of the response. If not specified, default to
+ C{self.getFileSize()}.
+ """
+ if size is None:
+ size = self.getFileSize()
+ request.setHeader('content-length', str(size))
+ if self.type:
+ request.setHeader('content-type', self.type)
+ if self.encoding:
+ request.setHeader('content-encoding', self.encoding)
+
+
+ def makeProducer(self, request, fileForReading):
+ """
+ Make a L{StaticProducer} that will produce the body of this response.
+
+ This method will also set the response code and Content-* headers.
+
+ @param request: The L{Request} object.
+ @param fileForReading: The file object containing the resource.
+ @return: A L{StaticProducer}. Calling C{.start()} on this will begin
+ producing the response.
+ """
+ byteRange = request.getHeader('range')
+ if byteRange is None:
+ self._setContentHeaders(request)
+ request.setResponseCode(http.OK)
+ return NoRangeStaticProducer(request, fileForReading)
+ try:
+ parsedRanges = self._parseRangeHeader(byteRange)
+ except ValueError:
+ log.msg("Ignoring malformed Range header %r" % (byteRange,))
+ self._setContentHeaders(request)
+ request.setResponseCode(http.OK)
+ return NoRangeStaticProducer(request, fileForReading)
+
+ if len(parsedRanges) == 1:
+ offset, size = self._doSingleRangeRequest(
+ request, parsedRanges[0])
+ self._setContentHeaders(request, size)
+ return SingleRangeStaticProducer(
+ request, fileForReading, offset, size)
+ else:
+ rangeInfo = self._doMultipleRangeRequest(request, parsedRanges)
+ return MultipleRangeStaticProducer(
+ request, fileForReading, rangeInfo)
+
+
+ def render_GET(self, request):
+ """
+ Begin sending the contents of this L{File} (or a subset of the
+ contents, based on the 'range' header) to the given request.
+ """
+ self.restat(False)
+
+ if self.type is None:
+ self.type, self.encoding = getTypeAndEncoding(self.basename(),
+ self.contentTypes,
+ self.contentEncodings,
+ self.defaultType)
+
+ if not self.exists():
+ return self.childNotFound.render(request)
+
+ if self.isdir():
+ return self.redirect(request)
+
+ request.setHeader('accept-ranges', 'bytes')
+
+ try:
+ fileForReading = self.openForReading()
+ except IOError, e:
+ import errno
+ if e[0] == errno.EACCES:
+ return resource.ForbiddenResource().render(request)
+ else:
+ raise
+
+ if request.setLastModified(self.getmtime()) is http.CACHED:
+ return ''
+
+
+ producer = self.makeProducer(request, fileForReading)
+
+ if request.method == 'HEAD':
+ return ''
+
+ producer.start()
+ # and make sure the connection doesn't get closed
+ return server.NOT_DONE_YET
+ render_HEAD = render_GET
+
+
+ def redirect(self, request):
+ return redirectTo(addSlash(request), request)
+
+
+ def listNames(self):
+ if not self.isdir():
+ return []
+ directory = self.listdir()
+ directory.sort()
+ return directory
+
+ def listEntities(self):
+ return map(lambda fileName, self=self: self.createSimilarFile(os.path.join(self.path, fileName)), self.listNames())
+
+
+ def createSimilarFile(self, path):
+ f = self.__class__(path, self.defaultType, self.ignoredExts, self.registry)
+ # refactoring by steps, here - constructor should almost certainly take these
+ f.processors = self.processors
+ f.indexNames = self.indexNames[:]
+ f.childNotFound = self.childNotFound
+ return f
+
+
+
+class StaticProducer(object):
+ """
+ Superclass for classes that implement the business of producing.
+
+ @ivar request: The L{IRequest} to write the contents of the file to.
+ @ivar fileObject: The file the contents of which to write to the request.
+ """
+
+ implements(interfaces.IPullProducer)
+
+ bufferSize = abstract.FileDescriptor.bufferSize
+
+
+ def __init__(self, request, fileObject):
+ """
+ Initialize the instance.
+ """
+ self.request = request
+ self.fileObject = fileObject
+
+
+ def start(self):
+ raise NotImplementedError(self.start)
+
+
+ def resumeProducing(self):
+ raise NotImplementedError(self.resumeProducing)
+
+
+ def stopProducing(self):
+ """
+ Stop producing data.
+
+ L{IPullProducer.stopProducing} is called when our consumer has died,
+ and subclasses also call this method when they are done producing
+ data.
+ """
+ self.fileObject.close()
+ self.request = None
+
+
+
+class NoRangeStaticProducer(StaticProducer):
+ """
+ A L{StaticProducer} that writes the entire file to the request.
+ """
+
+ def start(self):
+ self.request.registerProducer(self, False)
+
+
+ def resumeProducing(self):
+ if not self.request:
+ return
+ data = self.fileObject.read(self.bufferSize)
+ if data:
+ # this .write will spin the reactor, calling .doWrite and then
+ # .resumeProducing again, so be prepared for a re-entrant call
+ self.request.write(data)
+ else:
+ self.request.unregisterProducer()
+ self.request.finish()
+ self.stopProducing()
+
+
+
+class SingleRangeStaticProducer(StaticProducer):
+ """
+ A L{StaticProducer} that writes a single chunk of a file to the request.
+ """
+
+ def __init__(self, request, fileObject, offset, size):
+ """
+ Initialize the instance.
+
+ @param request: See L{StaticProducer}.
+ @param fileObject: See L{StaticProducer}.
+ @param offset: The offset into the file of the chunk to be written.
+ @param size: The size of the chunk to write.
+ """
+ StaticProducer.__init__(self, request, fileObject)
+ self.offset = offset
+ self.size = size
+
+
+ def start(self):
+ self.fileObject.seek(self.offset)
+ self.bytesWritten = 0
+ self.request.registerProducer(self, 0)
+
+
+ def resumeProducing(self):
+ if not self.request:
+ return
+ data = self.fileObject.read(
+ min(self.bufferSize, self.size - self.bytesWritten))
+ if data:
+ self.bytesWritten += len(data)
+ # this .write will spin the reactor, calling .doWrite and then
+ # .resumeProducing again, so be prepared for a re-entrant call
+ self.request.write(data)
+ if self.request and self.bytesWritten == self.size:
+ self.request.unregisterProducer()
+ self.request.finish()
+ self.stopProducing()
+
+
+
+class MultipleRangeStaticProducer(StaticProducer):
+ """
+ A L{StaticProducer} that writes several chunks of a file to the request.
+ """
+
+ def __init__(self, request, fileObject, rangeInfo):
+ """
+ Initialize the instance.
+
+ @param request: See L{StaticProducer}.
+ @param fileObject: See L{StaticProducer}.
+ @param rangeInfo: A list of tuples C{[(boundary, offset, size)]}
+ where:
+ - C{boundary} will be written to the request first.
+ - C{offset} the offset into the file of chunk to write.
+ - C{size} the size of the chunk to write.
+ """
+ StaticProducer.__init__(self, request, fileObject)
+ self.rangeInfo = rangeInfo
+
+
+ def start(self):
+ self.rangeIter = iter(self.rangeInfo)
+ self._nextRange()
+ self.request.registerProducer(self, 0)
+
+
+ def _nextRange(self):
+ self.partBoundary, partOffset, self._partSize = self.rangeIter.next()
+ self._partBytesWritten = 0
+ self.fileObject.seek(partOffset)
+
+
+ def resumeProducing(self):
+ if not self.request:
+ return
+ data = []
+ dataLength = 0
+ done = False
+ while dataLength < self.bufferSize:
+ if self.partBoundary:
+ dataLength += len(self.partBoundary)
+ data.append(self.partBoundary)
+ self.partBoundary = None
+ p = self.fileObject.read(
+ min(self.bufferSize - dataLength,
+ self._partSize - self._partBytesWritten))
+ self._partBytesWritten += len(p)
+ dataLength += len(p)
+ data.append(p)
+ if self.request and self._partBytesWritten == self._partSize:
+ try:
+ self._nextRange()
+ except StopIteration:
+ done = True
+ break
+ self.request.write(''.join(data))
+ if done:
+ self.request.unregisterProducer()
+ self.request.finish()
+ self.request = None
+
+
+class FileTransfer(pb.Viewable):
+ """
+ A class to represent the transfer of a file over the network.
+ """
+ request = None
+
+ def __init__(self, file, size, request):
+ warnings.warn(
+ "FileTransfer is deprecated since Twisted 9.0. "
+ "Use a subclass of StaticProducer instead.",
+ DeprecationWarning, stacklevel=2)
+ self.file = file
+ self.size = size
+ self.request = request
+ self.written = self.file.tell()
+ request.registerProducer(self, 0)
+
+ def resumeProducing(self):
+ if not self.request:
+ return
+ data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size - self.written))
+ if data:
+ self.written += len(data)
+ # this .write will spin the reactor, calling .doWrite and then
+ # .resumeProducing again, so be prepared for a re-entrant call
+ self.request.write(data)
+ if self.request and self.file.tell() == self.size:
+ self.request.unregisterProducer()
+ self.request.finish()
+ self.request = None
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ self.file.close()
+ self.request = None
+
+ # Remotely relay producer interface.
+
+ def view_resumeProducing(self, issuer):
+ self.resumeProducing()
+
+ def view_pauseProducing(self, issuer):
+ self.pauseProducing()
+
+ def view_stopProducing(self, issuer):
+ self.stopProducing()
+
+
+
+class ASISProcessor(resource.Resource):
+ """
+ Serve files exactly as responses without generating a status-line or any
+ headers. Inspired by Apache's mod_asis.
+ """
+
+ def __init__(self, path, registry=None):
+ resource.Resource.__init__(self)
+ self.path = path
+ self.registry = registry or Registry()
+
+
+ def render(self, request):
+ request.startedWriting = 1
+ res = File(self.path, registry=self.registry)
+ return res.render(request)
+
+
+
+def formatFileSize(size):
+ """
+ Format the given file size in bytes to human readable format.
+ """
+ if size < 1024:
+ return '%iB' % size
+ elif size < (1024 ** 2):
+ return '%iK' % (size / 1024)
+ elif size < (1024 ** 3):
+ return '%iM' % (size / (1024 ** 2))
+ else:
+ return '%iG' % (size / (1024 ** 3))
+
+
+
+class DirectoryLister(resource.Resource):
+ """
+ Print the content of a directory.
+
+ @ivar template: page template used to render the content of the directory.
+ It must contain the format keys B{header} and B{tableContent}.
+ @type template: C{str}
+
+ @ivar linePattern: template used to render one line in the listing table.
+ It must contain the format keys B{class}, B{href}, B{text}, B{size},
+ B{type} and B{encoding}.
+ @type linePattern: C{str}
+
+ @ivar contentEncodings: a mapping of extensions to encoding types.
+ @type contentEncodings: C{dict}
+
+ @ivar defaultType: default type used when no mimetype is detected.
+ @type defaultType: C{str}
+
+ @ivar dirs: filtered content of C{path}, if the whole content should not be
+ displayed (default to C{None}, which means the actual content of
+ C{path} is printed).
+ @type dirs: C{NoneType} or C{list}
+
+ @ivar path: directory which content should be listed.
+ @type path: C{str}
+ """
+
+ template = """<html>
+<head>
+<title>%(header)s</title>
+<style>
+.even-dir { background-color: #efe0ef }
+.even { background-color: #eee }
+.odd-dir {background-color: #f0d0ef }
+.odd { background-color: #dedede }
+.icon { text-align: center }
+.listing {
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%%;
+ padding: 0.1em;
+ }
+
+body { border: 0; padding: 0; margin: 0; background-color: #efefef; }
+h1 {padding: 0.1em; background-color: #777; color: white; border-bottom: thin white dashed;}
+
+</style>
+</head>
+
+<body>
+<h1>%(header)s</h1>
+
+<table>
+ <thead>
+ <tr>
+ <th>Filename</th>
+ <th>Size</th>
+ <th>Content type</th>
+ <th>Content encoding</th>
+ </tr>
+ </thead>
+ <tbody>
+%(tableContent)s
+ </tbody>
+</table>
+
+</body>
+</html>
+"""
+
+ linePattern = """<tr class="%(class)s">
+ <td><a href="%(href)s">%(text)s</a></td>
+ <td>%(size)s</td>
+ <td>%(type)s</td>
+ <td>%(encoding)s</td>
+</tr>
+"""
+
+ def __init__(self, pathname, dirs=None,
+ contentTypes=File.contentTypes,
+ contentEncodings=File.contentEncodings,
+ defaultType='text/html'):
+ resource.Resource.__init__(self)
+ self.contentTypes = contentTypes
+ self.contentEncodings = contentEncodings
+ self.defaultType = defaultType
+ # dirs allows usage of the File to specify what gets listed
+ self.dirs = dirs
+ self.path = pathname
+
+
+ def _getFilesAndDirectories(self, directory):
+ """
+ Helper returning files and directories in given directory listing, with
+ attributes to be used to build a table content with
+ C{self.linePattern}.
+
+ @return: tuple of (directories, files)
+ @rtype: C{tuple} of C{list}
+ """
+ files = []
+ dirs = []
+ for path in directory:
+ url = urllib.quote(path, "/")
+ escapedPath = cgi.escape(path)
+ if os.path.isdir(os.path.join(self.path, path)):
+ url = url + '/'
+ dirs.append({'text': escapedPath + "/", 'href': url,
+ 'size': '', 'type': '[Directory]',
+ 'encoding': ''})
+ else:
+ mimetype, encoding = getTypeAndEncoding(path, self.contentTypes,
+ self.contentEncodings,
+ self.defaultType)
+ try:
+ size = os.stat(os.path.join(self.path, path)).st_size
+ except OSError:
+ continue
+ files.append({
+ 'text': escapedPath, "href": url,
+ 'type': '[%s]' % mimetype,
+ 'encoding': (encoding and '[%s]' % encoding or ''),
+ 'size': formatFileSize(size)})
+ return dirs, files
+
+
+ def _buildTableContent(self, elements):
+ """
+ Build a table content using C{self.linePattern} and giving elements odd
+ and even classes.
+ """
+ tableContent = []
+ rowClasses = itertools.cycle(['odd', 'even'])
+ for element, rowClass in zip(elements, rowClasses):
+ element["class"] = rowClass
+ tableContent.append(self.linePattern % element)
+ return tableContent
+
+
+ def render(self, request):
+ """
+ Render a listing of the content of C{self.path}.
+ """
+ request.setHeader("content-type", "text/html; charset=utf-8")
+ if self.dirs is None:
+ directory = os.listdir(self.path)
+ directory.sort()
+ else:
+ directory = self.dirs
+
+ dirs, files = self._getFilesAndDirectories(directory)
+
+ tableContent = "".join(self._buildTableContent(dirs + files))
+
+ header = "Directory listing for %s" % (
+ cgi.escape(urllib.unquote(request.uri)),)
+
+ return self.template % {"header": header, "tableContent": tableContent}
+
+
+ def __repr__(self):
+ return '<DirectoryLister of %r>' % self.path
+
+ __str__ = __repr__
diff --git a/twisted/web/sux.py b/twisted/web/sux.py
new file mode 100644
index 0000000..13e6c76
--- /dev/null
+++ b/twisted/web/sux.py
@@ -0,0 +1,637 @@
+# -*- test-case-name: twisted.web.test.test_xml -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+*S*mall, *U*ncomplicated *X*ML.
+
+This is a very simple implementation of XML/HTML as a network
+protocol. It is not at all clever. Its main features are that it
+does not:
+
+ - support namespaces
+ - mung mnemonic entity references
+ - validate
+ - perform *any* external actions (such as fetching URLs or writing files)
+ under *any* circumstances
+ - has lots and lots of horrible hacks for supporting broken HTML (as an
+ option, they're not on by default).
+"""
+
+from twisted.internet.protocol import Protocol
+from twisted.python.reflect import prefixedMethodNames
+
+
+
+# Elements of the three-tuples in the state table.
+BEGIN_HANDLER = 0
+DO_HANDLER = 1
+END_HANDLER = 2
+
+identChars = '.-_:'
+lenientIdentChars = identChars + ';+#/%~'
+
+def nop(*args, **kw):
+ "Do nothing."
+
+
+def unionlist(*args):
+ l = []
+ for x in args:
+ l.extend(x)
+ d = dict([(x, 1) for x in l])
+ return d.keys()
+
+
+def zipfndict(*args, **kw):
+ default = kw.get('default', nop)
+ d = {}
+ for key in unionlist(*[fndict.keys() for fndict in args]):
+ d[key] = tuple([x.get(key, default) for x in args])
+ return d
+
+
+def prefixedMethodClassDict(clazz, prefix):
+ return dict([(name, getattr(clazz, prefix + name)) for name in prefixedMethodNames(clazz, prefix)])
+
+
+def prefixedMethodObjDict(obj, prefix):
+ return dict([(name, getattr(obj, prefix + name)) for name in prefixedMethodNames(obj.__class__, prefix)])
+
+
+class ParseError(Exception):
+
+ def __init__(self, filename, line, col, message):
+ self.filename = filename
+ self.line = line
+ self.col = col
+ self.message = message
+
+ def __str__(self):
+ return "%s:%s:%s: %s" % (self.filename, self.line, self.col,
+ self.message)
+
+class XMLParser(Protocol):
+
+ state = None
+ encodings = None
+ filename = "<xml />"
+ beExtremelyLenient = 0
+ _prepend = None
+
+ # _leadingBodyData will sometimes be set before switching to the
+ # 'bodydata' state, when we "accidentally" read a byte of bodydata
+ # in a different state.
+ _leadingBodyData = None
+
+ def connectionMade(self):
+ self.lineno = 1
+ self.colno = 0
+ self.encodings = []
+
+ def saveMark(self):
+ '''Get the line number and column of the last character parsed'''
+ # This gets replaced during dataReceived, restored afterwards
+ return (self.lineno, self.colno)
+
+ def _parseError(self, message):
+ raise ParseError(*((self.filename,)+self.saveMark()+(message,)))
+
+ def _buildStateTable(self):
+ '''Return a dictionary of begin, do, end state function tuples'''
+ # _buildStateTable leaves something to be desired but it does what it
+ # does.. probably slowly, so I'm doing some evil caching so it doesn't
+ # get called more than once per class.
+ stateTable = getattr(self.__class__, '__stateTable', None)
+ if stateTable is None:
+ stateTable = self.__class__.__stateTable = zipfndict(
+ *[prefixedMethodObjDict(self, prefix)
+ for prefix in ('begin_', 'do_', 'end_')])
+ return stateTable
+
+ def _decode(self, data):
+ if 'UTF-16' in self.encodings or 'UCS-2' in self.encodings:
+ assert not len(data) & 1, 'UTF-16 must come in pairs for now'
+ if self._prepend:
+ data = self._prepend + data
+ for encoding in self.encodings:
+ data = unicode(data, encoding)
+ return data
+
+ def maybeBodyData(self):
+ if self.endtag:
+ return 'bodydata'
+
+ # Get ready for fun! We're going to allow
+ # <script>if (foo < bar)</script> to work!
+ # We do this by making everything between <script> and
+ # </script> a Text
+ # BUT <script src="foo"> will be special-cased to do regular,
+ # lenient behavior, because those may not have </script>
+ # -radix
+
+ if (self.tagName == 'script'
+ and not self.tagAttributes.has_key('src')):
+ # we do this ourselves rather than having begin_waitforendscript
+ # becuase that can get called multiple times and we don't want
+ # bodydata to get reset other than the first time.
+ self.begin_bodydata(None)
+ return 'waitforendscript'
+ return 'bodydata'
+
+
+
+ def dataReceived(self, data):
+ stateTable = self._buildStateTable()
+ if not self.state:
+ # all UTF-16 starts with this string
+ if data.startswith('\xff\xfe'):
+ self._prepend = '\xff\xfe'
+ self.encodings.append('UTF-16')
+ data = data[2:]
+ elif data.startswith('\xfe\xff'):
+ self._prepend = '\xfe\xff'
+ self.encodings.append('UTF-16')
+ data = data[2:]
+ self.state = 'begin'
+ if self.encodings:
+ data = self._decode(data)
+ # bring state, lineno, colno into local scope
+ lineno, colno = self.lineno, self.colno
+ curState = self.state
+ # replace saveMark with a nested scope function
+ _saveMark = self.saveMark
+ def saveMark():
+ return (lineno, colno)
+ self.saveMark = saveMark
+ # fetch functions from the stateTable
+ beginFn, doFn, endFn = stateTable[curState]
+ try:
+ for byte in data:
+ # do newline stuff
+ if byte == '\n':
+ lineno += 1
+ colno = 0
+ else:
+ colno += 1
+ newState = doFn(byte)
+ if newState is not None and newState != curState:
+ # this is the endFn from the previous state
+ endFn()
+ curState = newState
+ beginFn, doFn, endFn = stateTable[curState]
+ beginFn(byte)
+ finally:
+ self.saveMark = _saveMark
+ self.lineno, self.colno = lineno, colno
+ # state doesn't make sense if there's an exception..
+ self.state = curState
+
+
+ def connectionLost(self, reason):
+ """
+ End the last state we were in.
+ """
+ stateTable = self._buildStateTable()
+ stateTable[self.state][END_HANDLER]()
+
+
+ # state methods
+
+ def do_begin(self, byte):
+ if byte.isspace():
+ return
+ if byte != '<':
+ if self.beExtremelyLenient:
+ self._leadingBodyData = byte
+ return 'bodydata'
+ self._parseError("First char of document [%r] wasn't <" % (byte,))
+ return 'tagstart'
+
+ def begin_comment(self, byte):
+ self.commentbuf = ''
+
+ def do_comment(self, byte):
+ self.commentbuf += byte
+ if self.commentbuf.endswith('-->'):
+ self.gotComment(self.commentbuf[:-3])
+ return 'bodydata'
+
+ def begin_tagstart(self, byte):
+ self.tagName = '' # name of the tag
+ self.tagAttributes = {} # attributes of the tag
+ self.termtag = 0 # is the tag self-terminating
+ self.endtag = 0
+
+ def do_tagstart(self, byte):
+ if byte.isalnum() or byte in identChars:
+ self.tagName += byte
+ if self.tagName == '!--':
+ return 'comment'
+ elif byte.isspace():
+ if self.tagName:
+ if self.endtag:
+ # properly strict thing to do here is probably to only
+ # accept whitespace
+ return 'waitforgt'
+ return 'attrs'
+ else:
+ self._parseError("Whitespace before tag-name")
+ elif byte == '>':
+ if self.endtag:
+ self.gotTagEnd(self.tagName)
+ return 'bodydata'
+ else:
+ self.gotTagStart(self.tagName, {})
+ return (not self.beExtremelyLenient) and 'bodydata' or self.maybeBodyData()
+ elif byte == '/':
+ if self.tagName:
+ return 'afterslash'
+ else:
+ self.endtag = 1
+ elif byte in '!?':
+ if self.tagName:
+ if not self.beExtremelyLenient:
+ self._parseError("Invalid character in tag-name")
+ else:
+ self.tagName += byte
+ self.termtag = 1
+ elif byte == '[':
+ if self.tagName == '!':
+ return 'expectcdata'
+ else:
+ self._parseError("Invalid '[' in tag-name")
+ else:
+ if self.beExtremelyLenient:
+ self.bodydata = '<'
+ return 'unentity'
+ self._parseError('Invalid tag character: %r'% byte)
+
+ def begin_unentity(self, byte):
+ self.bodydata += byte
+
+ def do_unentity(self, byte):
+ self.bodydata += byte
+ return 'bodydata'
+
+ def end_unentity(self):
+ self.gotText(self.bodydata)
+
+ def begin_expectcdata(self, byte):
+ self.cdatabuf = byte
+
+ def do_expectcdata(self, byte):
+ self.cdatabuf += byte
+ cdb = self.cdatabuf
+ cd = '[CDATA['
+ if len(cd) > len(cdb):
+ if cd.startswith(cdb):
+ return
+ elif self.beExtremelyLenient:
+ ## WHAT THE CRAP!? MSWord9 generates HTML that includes these
+ ## bizarre <![if !foo]> <![endif]> chunks, so I've gotta ignore
+ ## 'em as best I can. this should really be a separate parse
+ ## state but I don't even have any idea what these _are_.
+ return 'waitforgt'
+ else:
+ self._parseError("Mal-formed CDATA header")
+ if cd == cdb:
+ self.cdatabuf = ''
+ return 'cdata'
+ self._parseError("Mal-formed CDATA header")
+
+ def do_cdata(self, byte):
+ self.cdatabuf += byte
+ if self.cdatabuf.endswith("]]>"):
+ self.cdatabuf = self.cdatabuf[:-3]
+ return 'bodydata'
+
+ def end_cdata(self):
+ self.gotCData(self.cdatabuf)
+ self.cdatabuf = ''
+
+ def do_attrs(self, byte):
+ if byte.isalnum() or byte in identChars:
+ # XXX FIXME really handle !DOCTYPE at some point
+ if self.tagName == '!DOCTYPE':
+ return 'doctype'
+ if self.tagName[0] in '!?':
+ return 'waitforgt'
+ return 'attrname'
+ elif byte.isspace():
+ return
+ elif byte == '>':
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ return (not self.beExtremelyLenient) and 'bodydata' or self.maybeBodyData()
+ elif byte == '/':
+ return 'afterslash'
+ elif self.beExtremelyLenient:
+ # discard and move on? Only case I've seen of this so far was:
+ # <foo bar="baz"">
+ return
+ self._parseError("Unexpected character: %r" % byte)
+
+ def begin_doctype(self, byte):
+ self.doctype = byte
+
+ def do_doctype(self, byte):
+ if byte == '>':
+ return 'bodydata'
+ self.doctype += byte
+
+ def end_doctype(self):
+ self.gotDoctype(self.doctype)
+ self.doctype = None
+
+ def do_waitforgt(self, byte):
+ if byte == '>':
+ if self.endtag or not self.beExtremelyLenient:
+ return 'bodydata'
+ return self.maybeBodyData()
+
+ def begin_attrname(self, byte):
+ self.attrname = byte
+ self._attrname_termtag = 0
+
+ def do_attrname(self, byte):
+ if byte.isalnum() or byte in identChars:
+ self.attrname += byte
+ return
+ elif byte == '=':
+ return 'beforeattrval'
+ elif byte.isspace():
+ return 'beforeeq'
+ elif self.beExtremelyLenient:
+ if byte in '"\'':
+ return 'attrval'
+ if byte in lenientIdentChars or byte.isalnum():
+ self.attrname += byte
+ return
+ if byte == '/':
+ self._attrname_termtag = 1
+ return
+ if byte == '>':
+ self.attrval = 'True'
+ self.tagAttributes[self.attrname] = self.attrval
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ if self._attrname_termtag:
+ self.gotTagEnd(self.tagName)
+ return 'bodydata'
+ return self.maybeBodyData()
+ # something is really broken. let's leave this attribute where it
+ # is and move on to the next thing
+ return
+ self._parseError("Invalid attribute name: %r %r" % (self.attrname, byte))
+
+ def do_beforeattrval(self, byte):
+ if byte in '"\'':
+ return 'attrval'
+ elif byte.isspace():
+ return
+ elif self.beExtremelyLenient:
+ if byte in lenientIdentChars or byte.isalnum():
+ return 'messyattr'
+ if byte == '>':
+ self.attrval = 'True'
+ self.tagAttributes[self.attrname] = self.attrval
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ return self.maybeBodyData()
+ if byte == '\\':
+ # I saw this in actual HTML once:
+ # <font size=\"3\"><sup>SM</sup></font>
+ return
+ self._parseError("Invalid initial attribute value: %r; Attribute values must be quoted." % byte)
+
+ attrname = ''
+ attrval = ''
+
+ def begin_beforeeq(self,byte):
+ self._beforeeq_termtag = 0
+
+ def do_beforeeq(self, byte):
+ if byte == '=':
+ return 'beforeattrval'
+ elif byte.isspace():
+ return
+ elif self.beExtremelyLenient:
+ if byte.isalnum() or byte in identChars:
+ self.attrval = 'True'
+ self.tagAttributes[self.attrname] = self.attrval
+ return 'attrname'
+ elif byte == '>':
+ self.attrval = 'True'
+ self.tagAttributes[self.attrname] = self.attrval
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ if self._beforeeq_termtag:
+ self.gotTagEnd(self.tagName)
+ return 'bodydata'
+ return self.maybeBodyData()
+ elif byte == '/':
+ self._beforeeq_termtag = 1
+ return
+ self._parseError("Invalid attribute")
+
+ def begin_attrval(self, byte):
+ self.quotetype = byte
+ self.attrval = ''
+
+ def do_attrval(self, byte):
+ if byte == self.quotetype:
+ return 'attrs'
+ self.attrval += byte
+
+ def end_attrval(self):
+ self.tagAttributes[self.attrname] = self.attrval
+ self.attrname = self.attrval = ''
+
+ def begin_messyattr(self, byte):
+ self.attrval = byte
+
+ def do_messyattr(self, byte):
+ if byte.isspace():
+ return 'attrs'
+ elif byte == '>':
+ endTag = 0
+ if self.attrval.endswith('/'):
+ endTag = 1
+ self.attrval = self.attrval[:-1]
+ self.tagAttributes[self.attrname] = self.attrval
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ if endTag:
+ self.gotTagEnd(self.tagName)
+ return 'bodydata'
+ return self.maybeBodyData()
+ else:
+ self.attrval += byte
+
+ def end_messyattr(self):
+ if self.attrval:
+ self.tagAttributes[self.attrname] = self.attrval
+
+ def begin_afterslash(self, byte):
+ self._after_slash_closed = 0
+
+ def do_afterslash(self, byte):
+ # this state is only after a self-terminating slash, e.g. <foo/>
+ if self._after_slash_closed:
+ self._parseError("Mal-formed")#XXX When does this happen??
+ if byte != '>':
+ if self.beExtremelyLenient:
+ return
+ else:
+ self._parseError("No data allowed after '/'")
+ self._after_slash_closed = 1
+ self.gotTagStart(self.tagName, self.tagAttributes)
+ self.gotTagEnd(self.tagName)
+ # don't need maybeBodyData here because there better not be
+ # any javascript code after a <script/>... we'll see :(
+ return 'bodydata'
+
+ def begin_bodydata(self, byte):
+ if self._leadingBodyData:
+ self.bodydata = self._leadingBodyData
+ del self._leadingBodyData
+ else:
+ self.bodydata = ''
+
+ def do_bodydata(self, byte):
+ if byte == '<':
+ return 'tagstart'
+ if byte == '&':
+ return 'entityref'
+ self.bodydata += byte
+
+ def end_bodydata(self):
+ self.gotText(self.bodydata)
+ self.bodydata = ''
+
+ def do_waitforendscript(self, byte):
+ if byte == '<':
+ return 'waitscriptendtag'
+ self.bodydata += byte
+
+ def begin_waitscriptendtag(self, byte):
+ self.temptagdata = ''
+ self.tagName = ''
+ self.endtag = 0
+
+ def do_waitscriptendtag(self, byte):
+ # 1 enforce / as first byte read
+ # 2 enforce following bytes to be subset of "script" until
+ # tagName == "script"
+ # 2a when that happens, gotText(self.bodydata) and gotTagEnd(self.tagName)
+ # 3 spaces can happen anywhere, they're ignored
+ # e.g. < / script >
+ # 4 anything else causes all data I've read to be moved to the
+ # bodydata, and switch back to waitforendscript state
+
+ # If it turns out this _isn't_ a </script>, we need to
+ # remember all the data we've been through so we can append it
+ # to bodydata
+ self.temptagdata += byte
+
+ # 1
+ if byte == '/':
+ self.endtag = True
+ elif not self.endtag:
+ self.bodydata += "<" + self.temptagdata
+ return 'waitforendscript'
+ # 2
+ elif byte.isalnum() or byte in identChars:
+ self.tagName += byte
+ if not 'script'.startswith(self.tagName):
+ self.bodydata += "<" + self.temptagdata
+ return 'waitforendscript'
+ elif self.tagName == 'script':
+ self.gotText(self.bodydata)
+ self.gotTagEnd(self.tagName)
+ return 'waitforgt'
+ # 3
+ elif byte.isspace():
+ return 'waitscriptendtag'
+ # 4
+ else:
+ self.bodydata += "<" + self.temptagdata
+ return 'waitforendscript'
+
+
+ def begin_entityref(self, byte):
+ self.erefbuf = ''
+ self.erefextra = '' # extra bit for lenient mode
+
+ def do_entityref(self, byte):
+ if byte.isspace() or byte == "<":
+ if self.beExtremelyLenient:
+ # '&foo' probably was '&amp;foo'
+ if self.erefbuf and self.erefbuf != "amp":
+ self.erefextra = self.erefbuf
+ self.erefbuf = "amp"
+ if byte == "<":
+ return "tagstart"
+ else:
+ self.erefextra += byte
+ return 'spacebodydata'
+ self._parseError("Bad entity reference")
+ elif byte != ';':
+ self.erefbuf += byte
+ else:
+ return 'bodydata'
+
+ def end_entityref(self):
+ self.gotEntityReference(self.erefbuf)
+
+ # hacky support for space after & in entityref in beExtremelyLenient
+ # state should only happen in that case
+ def begin_spacebodydata(self, byte):
+ self.bodydata = self.erefextra
+ self.erefextra = None
+ do_spacebodydata = do_bodydata
+ end_spacebodydata = end_bodydata
+
+ # Sorta SAX-ish API
+
+ def gotTagStart(self, name, attributes):
+ '''Encountered an opening tag.
+
+ Default behaviour is to print.'''
+ print 'begin', name, attributes
+
+ def gotText(self, data):
+ '''Encountered text
+
+ Default behaviour is to print.'''
+ print 'text:', repr(data)
+
+ def gotEntityReference(self, entityRef):
+ '''Encountered mnemonic entity reference
+
+ Default behaviour is to print.'''
+ print 'entityRef: &%s;' % entityRef
+
+ def gotComment(self, comment):
+ '''Encountered comment.
+
+ Default behaviour is to ignore.'''
+ pass
+
+ def gotCData(self, cdata):
+ '''Encountered CDATA
+
+ Default behaviour is to call the gotText method'''
+ self.gotText(cdata)
+
+ def gotDoctype(self, doctype):
+ """Encountered DOCTYPE
+
+ This is really grotty: it basically just gives you everything between
+ '<!DOCTYPE' and '>' as an argument.
+ """
+ print '!DOCTYPE', repr(doctype)
+
+ def gotTagEnd(self, name):
+ '''Encountered closing tag
+
+ Default behaviour is to print.'''
+ print 'end', name
diff --git a/twisted/web/tap.py b/twisted/web/tap.py
new file mode 100644
index 0000000..addcba1
--- /dev/null
+++ b/twisted/web/tap.py
@@ -0,0 +1,232 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Support for creating a service which runs a web server.
+"""
+
+import os
+
+# Twisted Imports
+from twisted.web import server, static, twcgi, script, demo, distrib, wsgi
+from twisted.internet import interfaces, reactor
+from twisted.python import usage, reflect, threadpool
+from twisted.spread import pb
+from twisted.application import internet, service, strports
+
+
+class Options(usage.Options):
+ """
+ Define the options accepted by the I{twistd web} plugin.
+ """
+ synopsis = "[web options]"
+
+ optParameters = [["port", "p", None, "strports description of the port to "
+ "start the server on."],
+ ["logfile", "l", None, "Path to web CLF (Combined Log Format) log file."],
+ ["https", None, None, "Port to listen on for Secure HTTP."],
+ ["certificate", "c", "server.pem", "SSL certificate to use for HTTPS. "],
+ ["privkey", "k", "server.pem", "SSL certificate to use for HTTPS."],
+ ]
+
+ optFlags = [["personal", "",
+ "Instead of generating a webserver, generate a "
+ "ResourcePublisher which listens on the port given by "
+ "--port, or ~/%s " % (distrib.UserDirectory.userSocketName,) +
+ "if --port is not specified."],
+ ["notracebacks", "n", "Do not display tracebacks in broken web pages. " +
+ "Displaying tracebacks to users may be security risk!"],
+ ]
+
+ compData = usage.Completions(
+ optActions={"logfile" : usage.CompleteFiles("*.log"),
+ "certificate" : usage.CompleteFiles("*.pem"),
+ "privkey" : usage.CompleteFiles("*.pem")}
+ )
+
+ longdesc = """\
+This starts a webserver. If you specify no arguments, it will be a
+demo webserver that has the Test class from twisted.web.demo in it."""
+
+ def __init__(self):
+ usage.Options.__init__(self)
+ self['indexes'] = []
+ self['root'] = None
+
+
+ def opt_index(self, indexName):
+ """
+ Add the name of a file used to check for directory indexes.
+ [default: index, index.html]
+ """
+ self['indexes'].append(indexName)
+
+ opt_i = opt_index
+
+
+ def opt_user(self):
+ """
+ Makes a server with ~/public_html and ~/.twistd-web-pb support for
+ users.
+ """
+ self['root'] = distrib.UserDirectory()
+
+ opt_u = opt_user
+
+
+ def opt_path(self, path):
+ """
+ <path> is either a specific file or a directory to be set as the root
+ of the web server. Use this if you have a directory full of HTML, cgi,
+ epy, or rpy files or any other files that you want to be served up raw.
+ """
+ self['root'] = static.File(os.path.abspath(path))
+ self['root'].processors = {
+ '.cgi': twcgi.CGIScript,
+ '.epy': script.PythonScript,
+ '.rpy': script.ResourceScript,
+ }
+
+
+ def opt_processor(self, proc):
+ """
+ `ext=class' where `class' is added as a Processor for files ending
+ with `ext'.
+ """
+ if not isinstance(self['root'], static.File):
+ raise usage.UsageError("You can only use --processor after --path.")
+ ext, klass = proc.split('=', 1)
+ self['root'].processors[ext] = reflect.namedClass(klass)
+
+
+ def opt_class(self, className):
+ """
+ Create a Resource subclass with a zero-argument constructor.
+ """
+ classObj = reflect.namedClass(className)
+ self['root'] = classObj()
+
+
+ def opt_resource_script(self, name):
+ """
+ An .rpy file to be used as the root resource of the webserver.
+ """
+ self['root'] = script.ResourceScriptWrapper(name)
+
+
+ def opt_wsgi(self, name):
+ """
+ The FQPN of a WSGI application object to serve as the root resource of
+ the webserver.
+ """
+ pool = threadpool.ThreadPool()
+ reactor.callWhenRunning(pool.start)
+ reactor.addSystemEventTrigger('after', 'shutdown', pool.stop)
+ try:
+ application = reflect.namedAny(name)
+ except (AttributeError, ValueError):
+ raise usage.UsageError("No such WSGI application: %r" % (name,))
+ self['root'] = wsgi.WSGIResource(reactor, pool, application)
+
+
+ def opt_mime_type(self, defaultType):
+ """
+ Specify the default mime-type for static files.
+ """
+ if not isinstance(self['root'], static.File):
+ raise usage.UsageError("You can only use --mime_type after --path.")
+ self['root'].defaultType = defaultType
+ opt_m = opt_mime_type
+
+
+ def opt_allow_ignore_ext(self):
+ """
+ Specify whether or not a request for 'foo' should return 'foo.ext'
+ """
+ if not isinstance(self['root'], static.File):
+ raise usage.UsageError("You can only use --allow_ignore_ext "
+ "after --path.")
+ self['root'].ignoreExt('*')
+
+
+ def opt_ignore_ext(self, ext):
+ """
+ Specify an extension to ignore. These will be processed in order.
+ """
+ if not isinstance(self['root'], static.File):
+ raise usage.UsageError("You can only use --ignore_ext "
+ "after --path.")
+ self['root'].ignoreExt(ext)
+
+
+ def postOptions(self):
+ """
+ Set up conditional defaults and check for dependencies.
+
+ If SSL is not available but an HTTPS server was configured, raise a
+ L{UsageError} indicating that this is not possible.
+
+ If no server port was supplied, select a default appropriate for the
+ other options supplied.
+ """
+ if self['https']:
+ try:
+ from twisted.internet.ssl import DefaultOpenSSLContextFactory
+ except ImportError:
+ raise usage.UsageError("SSL support not installed")
+ if self['port'] is None:
+ if self['personal']:
+ path = os.path.expanduser(
+ os.path.join('~', distrib.UserDirectory.userSocketName))
+ self['port'] = 'unix:' + path
+ else:
+ self['port'] = 'tcp:8080'
+
+
+
+def makePersonalServerFactory(site):
+ """
+ Create and return a factory which will respond to I{distrib} requests
+ against the given site.
+
+ @type site: L{twisted.web.server.Site}
+ @rtype: L{twisted.internet.protocol.Factory}
+ """
+ return pb.PBServerFactory(distrib.ResourcePublisher(site))
+
+
+
+def makeService(config):
+ s = service.MultiService()
+ if config['root']:
+ root = config['root']
+ if config['indexes']:
+ config['root'].indexNames = config['indexes']
+ else:
+ # This really ought to be web.Admin or something
+ root = demo.Test()
+
+ if isinstance(root, static.File):
+ root.registry.setComponent(interfaces.IServiceCollection, s)
+
+ if config['logfile']:
+ site = server.Site(root, logPath=config['logfile'])
+ else:
+ site = server.Site(root)
+
+ site.displayTracebacks = not config["notracebacks"]
+
+ if config['personal']:
+ personal = strports.service(
+ config['port'], makePersonalServerFactory(site))
+ personal.setServiceParent(s)
+ else:
+ if config['https']:
+ from twisted.internet.ssl import DefaultOpenSSLContextFactory
+ i = internet.SSLServer(int(config['https']), site,
+ DefaultOpenSSLContextFactory(config['privkey'],
+ config['certificate']))
+ i.setServiceParent(s)
+ strports.service(config['port'], site).setServiceParent(s)
+
+ return s
diff --git a/twisted/web/template.py b/twisted/web/template.py
new file mode 100644
index 0000000..224a192
--- /dev/null
+++ b/twisted/web/template.py
@@ -0,0 +1,566 @@
+# -*- test-case-name: twisted.web.test.test_template -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+HTML rendering for twisted.web.
+
+@var VALID_HTML_TAG_NAMES: A list of recognized HTML tag names, used by the
+ L{tag} object.
+
+@var TEMPLATE_NAMESPACE: The XML namespace used to identify attributes and
+ elements used by the templating system, which should be removed from the
+ final output document.
+
+@var tags: A convenience object which can produce L{Tag} objects on demand via
+ attribute access. For example: C{tags.div} is equivalent to C{Tag("div")}.
+ Tags not specified in L{VALID_HTML_TAG_NAMES} will result in an
+ L{AttributeError}.
+"""
+
+__all__ = [
+ 'TEMPLATE_NAMESPACE', 'VALID_HTML_TAG_NAMES', 'Element', 'TagLoader',
+ 'XMLString', 'XMLFile', 'renderer', 'flatten', 'flattenString', 'tags',
+ 'Comment', 'CDATA', 'Tag', 'slot', 'CharRef', 'renderElement'
+ ]
+
+import warnings
+from zope.interface import implements
+
+from cStringIO import StringIO
+from xml.sax import make_parser, handler
+
+from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef
+from twisted.python.filepath import FilePath
+
+TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1'
+
+from twisted.web.iweb import ITemplateLoader
+from twisted.python import log
+
+# Go read the definition of NOT_DONE_YET. For lulz. This is totally
+# equivalent. And this turns out to be necessary, because trying to import
+# NOT_DONE_YET in this module causes a circular import which we cannot escape
+# from. From which we cannot escape. Etc. glyph is okay with this solution for
+# now, and so am I, as long as this comment stays to explain to future
+# maintainers what it means. ~ C.
+#
+# See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this.
+NOT_DONE_YET = 1
+
+class _NSContext(object):
+ """
+ A mapping from XML namespaces onto their prefixes in the document.
+ """
+
+ def __init__(self, parent=None):
+ """
+ Pull out the parent's namespaces, if there's no parent then default to
+ XML.
+ """
+ self.parent = parent
+ if parent is not None:
+ self.nss = dict(parent.nss)
+ else:
+ self.nss = {'http://www.w3.org/XML/1998/namespace':'xml'}
+
+
+ def get(self, k, d=None):
+ """
+ Get a prefix for a namespace.
+
+ @param d: The default prefix value.
+ """
+ return self.nss.get(k, d)
+
+
+ def __setitem__(self, k, v):
+ """
+ Proxy through to setting the prefix for the namespace.
+ """
+ self.nss.__setitem__(k, v)
+
+
+ def __getitem__(self, k):
+ """
+ Proxy through to getting the prefix for the namespace.
+ """
+ return self.nss.__getitem__(k)
+
+
+
+class _ToStan(handler.ContentHandler, handler.EntityResolver):
+ """
+ A SAX parser which converts an XML document to the Twisted STAN
+ Document Object Model.
+ """
+
+ def __init__(self, sourceFilename):
+ """
+ @param sourceFilename: the filename to load the XML out of.
+ """
+ self.sourceFilename = sourceFilename
+ self.prefixMap = _NSContext()
+ self.inCDATA = False
+
+
+ def setDocumentLocator(self, locator):
+ """
+ Set the document locator, which knows about line and character numbers.
+ """
+ self.locator = locator
+
+
+ def startDocument(self):
+ """
+ Initialise the document.
+ """
+ self.document = []
+ self.current = self.document
+ self.stack = []
+ self.xmlnsAttrs = []
+
+
+ def endDocument(self):
+ """
+ Document ended.
+ """
+
+
+ def processingInstruction(self, target, data):
+ """
+ Processing instructions are ignored.
+ """
+
+
+ def startPrefixMapping(self, prefix, uri):
+ """
+ Set up the prefix mapping, which maps fully qualified namespace URIs
+ onto namespace prefixes.
+
+ This gets called before startElementNS whenever an C{xmlns} attribute
+ is seen.
+ """
+
+ self.prefixMap = _NSContext(self.prefixMap)
+ self.prefixMap[uri] = prefix
+
+ # Ignore the template namespace; we'll replace those during parsing.
+ if uri == TEMPLATE_NAMESPACE:
+ return
+
+ # Add to a list that will be applied once we have the element.
+ if prefix is None:
+ self.xmlnsAttrs.append(('xmlns',uri))
+ else:
+ self.xmlnsAttrs.append(('xmlns:%s'%prefix,uri))
+
+
+ def endPrefixMapping(self, prefix):
+ """
+ "Pops the stack" on the prefix mapping.
+
+ Gets called after endElementNS.
+ """
+ self.prefixMap = self.prefixMap.parent
+
+
+ def startElementNS(self, namespaceAndName, qname, attrs):
+ """
+ Gets called when we encounter a new xmlns attribute.
+
+ @param namespaceAndName: a (namespace, name) tuple, where name
+ determines which type of action to take, if the namespace matches
+ L{TEMPLATE_NAMESPACE}.
+ @param qname: ignored.
+ @param attrs: attributes on the element being started.
+ """
+
+ filename = self.sourceFilename
+ lineNumber = self.locator.getLineNumber()
+ columnNumber = self.locator.getColumnNumber()
+
+ ns, name = namespaceAndName
+ if ns == TEMPLATE_NAMESPACE:
+ if name == 'transparent':
+ name = ''
+ elif name == 'slot':
+ try:
+ # Try to get the default value for the slot
+ default = attrs[(None, 'default')]
+ except KeyError:
+ # If there wasn't one, then use None to indicate no
+ # default.
+ default = None
+ el = slot(
+ attrs[(None, 'name')], default=default,
+ filename=filename, lineNumber=lineNumber,
+ columnNumber=columnNumber)
+ self.stack.append(el)
+ self.current.append(el)
+ self.current = el.children
+ return
+
+ render = None
+
+ attrs = dict(attrs)
+ for k, v in attrs.items():
+ attrNS, justTheName = k
+ if attrNS != TEMPLATE_NAMESPACE:
+ continue
+ if justTheName == 'render':
+ render = v
+ del attrs[k]
+
+ # nonTemplateAttrs is a dictionary mapping attributes that are *not* in
+ # TEMPLATE_NAMESPACE to their values. Those in TEMPLATE_NAMESPACE were
+ # just removed from 'attrs' in the loop immediately above. The key in
+ # nonTemplateAttrs is either simply the attribute name (if it was not
+ # specified as having a namespace in the template) or prefix:name,
+ # preserving the xml namespace prefix given in the document.
+
+ nonTemplateAttrs = {}
+ for (attrNs, attrName), v in attrs.items():
+ nsPrefix = self.prefixMap.get(attrNs)
+ if nsPrefix is None:
+ attrKey = attrName
+ else:
+ attrKey = '%s:%s' % (nsPrefix, attrName)
+ nonTemplateAttrs[attrKey] = v
+
+ if ns == TEMPLATE_NAMESPACE and name == 'attr':
+ if not self.stack:
+ # TODO: define a better exception for this?
+ raise AssertionError(
+ '<{%s}attr> as top-level element' % (TEMPLATE_NAMESPACE,))
+ if 'name' not in nonTemplateAttrs:
+ # TODO: same here
+ raise AssertionError(
+ '<{%s}attr> requires a name attribute' % (TEMPLATE_NAMESPACE,))
+ el = Tag('', render=render, filename=filename,
+ lineNumber=lineNumber, columnNumber=columnNumber)
+ self.stack[-1].attributes[nonTemplateAttrs['name']] = el
+ self.stack.append(el)
+ self.current = el.children
+ return
+
+ # Apply any xmlns attributes
+ if self.xmlnsAttrs:
+ nonTemplateAttrs.update(dict(self.xmlnsAttrs))
+ self.xmlnsAttrs = []
+
+ # Add the prefix that was used in the parsed template for non-template
+ # namespaces (which will not be consumed anyway).
+ if ns != TEMPLATE_NAMESPACE and ns is not None:
+ prefix = self.prefixMap[ns]
+ if prefix is not None:
+ name = '%s:%s' % (self.prefixMap[ns],name)
+ el = Tag(
+ name, attributes=dict(nonTemplateAttrs), render=render,
+ filename=filename, lineNumber=lineNumber,
+ columnNumber=columnNumber)
+ self.stack.append(el)
+ self.current.append(el)
+ self.current = el.children
+
+
+ def characters(self, ch):
+ """
+ Called when we receive some characters. CDATA characters get passed
+ through as is.
+
+ @type ch: C{string}
+ """
+ if self.inCDATA:
+ self.stack[-1].append(ch)
+ return
+ self.current.append(ch)
+
+
+ def endElementNS(self, name, qname):
+ """
+ A namespace tag is closed. Pop the stack, if there's anything left in
+ it, otherwise return to the document's namespace.
+ """
+ self.stack.pop()
+ if self.stack:
+ self.current = self.stack[-1].children
+ else:
+ self.current = self.document
+
+
+ def startDTD(self, name, publicId, systemId):
+ """
+ DTDs are ignored.
+ """
+
+
+ def endDTD(self, *args):
+ """
+ DTDs are ignored.
+ """
+
+
+ def startCDATA(self):
+ """
+ We're starting to be in a CDATA element, make a note of this.
+ """
+ self.inCDATA = True
+ self.stack.append([])
+
+
+ def endCDATA(self):
+ """
+ We're no longer in a CDATA element. Collect up the characters we've
+ parsed and put them in a new CDATA object.
+ """
+ self.inCDATA = False
+ comment = ''.join(self.stack.pop())
+ self.current.append(CDATA(comment))
+
+
+ def comment(self, content):
+ """
+ Add an XML comment which we've encountered.
+ """
+ self.current.append(Comment(content))
+
+
+
+def _flatsaxParse(fl):
+ """
+ Perform a SAX parse of an XML document with the _ToStan class.
+
+ @param fl: The XML document to be parsed.
+ @type fl: A file object or filename.
+
+ @return: a C{list} of Stan objects.
+ """
+ parser = make_parser()
+ parser.setFeature(handler.feature_validation, 0)
+ parser.setFeature(handler.feature_namespaces, 1)
+ parser.setFeature(handler.feature_external_ges, 0)
+ parser.setFeature(handler.feature_external_pes, 0)
+
+ s = _ToStan(getattr(fl, "name", None))
+ parser.setContentHandler(s)
+ parser.setEntityResolver(s)
+ parser.setProperty(handler.property_lexical_handler, s)
+
+ parser.parse(fl)
+
+ return s.document
+
+
+class TagLoader(object):
+ """
+ An L{ITemplateLoader} that loads existing L{IRenderable} providers.
+
+ @ivar tag: The object which will be loaded.
+ @type tag: An L{IRenderable} provider.
+ """
+ implements(ITemplateLoader)
+
+ def __init__(self, tag):
+ """
+ @param tag: The object which will be loaded.
+ @type tag: An L{IRenderable} provider.
+ """
+ self.tag = tag
+
+
+ def load(self):
+ return [self.tag]
+
+
+
+class XMLString(object):
+ """
+ An L{ITemplateLoader} that loads and parses XML from a string.
+
+ @ivar _loadedTemplate: The loaded document.
+ @type _loadedTemplate: a C{list} of Stan objects.
+ """
+ implements(ITemplateLoader)
+
+ def __init__(self, s):
+ """
+ Run the parser on a StringIO copy of the string.
+
+ @param s: The string from which to load the XML.
+ @type s: C{str}
+ """
+ self._loadedTemplate = _flatsaxParse(StringIO(s))
+
+
+ def load(self):
+ """
+ Return the document.
+
+ @return: the loaded document.
+ @rtype: a C{list} of Stan objects.
+ """
+ return self._loadedTemplate
+
+
+
+class XMLFile(object):
+ """
+ An L{ITemplateLoader} that loads and parses XML from a file.
+
+ @ivar _loadedTemplate: The loaded document, or C{None}, if not loaded.
+ @type _loadedTemplate: a C{list} of Stan objects, or C{None}.
+
+ @ivar _path: The L{FilePath}, file object, or filename that is being
+ loaded from.
+ """
+ implements(ITemplateLoader)
+
+ def __init__(self, path):
+ """
+ Run the parser on a file.
+
+ @param path: The file from which to load the XML.
+ @type path: L{FilePath}
+ """
+ if not isinstance(path, FilePath):
+ warnings.warn(
+ "Passing filenames or file objects to XMLFile is deprecated "
+ "since Twisted 12.1. Pass a FilePath instead.",
+ category=DeprecationWarning, stacklevel=2)
+ self._loadedTemplate = None
+ self._path = path
+
+
+ def _loadDoc(self):
+ """
+ Read and parse the XML.
+
+ @return: the loaded document.
+ @rtype: a C{list} of Stan objects.
+ """
+ if not isinstance(self._path, FilePath):
+ return _flatsaxParse(self._path)
+ else:
+ f = self._path.open('r')
+ try:
+ return _flatsaxParse(f)
+ finally:
+ f.close()
+
+
+ def __repr__(self):
+ return '<XMLFile of %r>' % (self._path,)
+
+
+ def load(self):
+ """
+ Return the document, first loading it if necessary.
+
+ @return: the loaded document.
+ @rtype: a C{list} of Stan objects.
+ """
+ if self._loadedTemplate is None:
+ self._loadedTemplate = self._loadDoc()
+ return self._loadedTemplate
+
+
+
+# Last updated October 2011, using W3Schools as a reference. Link:
+# http://www.w3schools.com/html5/html5_reference.asp
+# Note that <xmp> is explicitly omitted; its semantics do not work with
+# t.w.template and it is officially deprecated.
+VALID_HTML_TAG_NAMES = set([
+ 'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside',
+ 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote',
+ 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code',
+ 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn',
+ 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption',
+ 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3',
+ 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe',
+ 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'legend',
+ 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes',
+ 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param',
+ 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script',
+ 'section', 'select', 'small', 'source', 'span', 'strike', 'strong',
+ 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea',
+ 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul', 'var',
+ 'video', 'wbr',
+])
+
+
+
+class _TagFactory(object):
+ """
+ A factory for L{Tag} objects; the implementation of the L{tags} object.
+
+ This allows for the syntactic convenience of C{from twisted.web.html import
+ tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML
+ tag.
+
+ The class is not exposed publicly because you only ever need one of these,
+ and we already made it for you.
+
+ @see: L{tags}
+ """
+ def __getattr__(self, tagName):
+ if tagName == 'transparent':
+ return Tag('')
+ # allow for E.del as E.del_
+ tagName = tagName.rstrip('_')
+ if tagName not in VALID_HTML_TAG_NAMES:
+ raise AttributeError('unknown tag %r' % (tagName,))
+ return Tag(tagName)
+
+
+
+tags = _TagFactory()
+
+
+
+def renderElement(request, element,
+ doctype='<!DOCTYPE html>', _failElement=None):
+ """
+ Render an element or other C{IRenderable}.
+
+ @param request: The C{Request} being rendered to.
+ @param element: An C{IRenderable} which will be rendered.
+ @param doctype: A C{str} which will be written as the first line of
+ the request, or C{None} to disable writing of a doctype. The C{string}
+ should not include a trailing newline and will default to the HTML5
+ doctype C{'<!DOCTYPE html>'}.
+
+ @returns: NOT_DONE_YET
+
+ @since: 12.1
+ """
+ if doctype is not None:
+ request.write(doctype)
+ request.write('\n')
+
+ if _failElement is None:
+ _failElement = twisted.web.util.FailureElement
+
+ d = flatten(request, element, request.write)
+
+ def eb(failure):
+ log.err(failure, "An error occurred while rendering the response.")
+ if request.site.displayTracebacks:
+ return flatten(request, _failElement(failure), request.write)
+ else:
+ request.write(
+ ('<div style="font-size:800%;'
+ 'background-color:#FFF;'
+ 'color:#F00'
+ '">An error occurred while rendering the response.</div>'))
+
+ d.addErrback(eb)
+ d.addBoth(lambda _: request.finish())
+ return NOT_DONE_YET
+
+
+
+from twisted.web._element import Element, renderer
+from twisted.web._flatten import flatten, flattenString
+import twisted.web.util
diff --git a/twisted/web/test/__init__.py b/twisted/web/test/__init__.py
new file mode 100644
index 0000000..cdbb14e
--- /dev/null
+++ b/twisted/web/test/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web}.
+"""
+
diff --git a/twisted/web/test/_util.py b/twisted/web/test/_util.py
new file mode 100644
index 0000000..6117b72
--- /dev/null
+++ b/twisted/web/test/_util.py
@@ -0,0 +1,77 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+General helpers for L{twisted.web} unit tests.
+"""
+
+from twisted.internet.defer import succeed
+from twisted.web import server
+from twisted.trial.unittest import TestCase
+from twisted.python.failure import Failure
+from twisted.web._flatten import flattenString
+from twisted.web.error import FlattenerError
+
+
+def _render(resource, request):
+ result = resource.render(request)
+ if isinstance(result, str):
+ request.write(result)
+ request.finish()
+ return succeed(None)
+ elif result is server.NOT_DONE_YET:
+ if request.finished:
+ return succeed(None)
+ else:
+ return request.notifyFinish()
+ else:
+ raise ValueError("Unexpected return value: %r" % (result,))
+
+
+
+class FlattenTestCase(TestCase):
+ """
+ A test case that assists with testing L{twisted.web._flatten}.
+ """
+ def assertFlattensTo(self, root, target):
+ """
+ Assert that a root element, when flattened, is equal to a string.
+ """
+ d = flattenString(None, root)
+ d.addCallback(lambda s: self.assertEqual(s, target))
+ return d
+
+
+ def assertFlattensImmediately(self, root, target):
+ """
+ Assert that a root element, when flattened, is equal to a string, and
+ performs no asynchronus Deferred anything.
+
+ This version is more convenient in tests which wish to make multiple
+ assertions about flattening, since it can be called multiple times
+ without having to add multiple callbacks.
+ """
+ results = []
+ it = self.assertFlattensTo(root, target)
+ it.addBoth(results.append)
+ # Do our best to clean it up if something goes wrong.
+ self.addCleanup(it.cancel)
+ if not results:
+ self.fail("Rendering did not complete immediately.")
+ result = results[0]
+ if isinstance(result, Failure):
+ result.raiseException()
+
+
+ def assertFlatteningRaises(self, root, exn):
+ """
+ Assert flattening a root element raises a particular exception.
+ """
+ d = self.assertFailure(self.assertFlattensTo(root, ''), FlattenerError)
+ d.addCallback(lambda exc: self.assertIsInstance(exc._exception, exn))
+ return d
+
+
+
+
diff --git a/twisted/web/test/test_cgi.py b/twisted/web/test/test_cgi.py
new file mode 100755
index 0000000..db63211
--- /dev/null
+++ b/twisted/web/test/test_cgi.py
@@ -0,0 +1,270 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.twcgi}.
+"""
+
+import sys, os
+
+from twisted.trial import unittest
+from twisted.internet import reactor, interfaces, error
+from twisted.python import util, failure
+from twisted.web.http import NOT_FOUND, INTERNAL_SERVER_ERROR
+from twisted.web import client, twcgi, server, resource
+from twisted.web.test._util import _render
+from twisted.web.test.test_web import DummyRequest
+
+DUMMY_CGI = '''\
+print "Header: OK"
+print
+print "cgi output"
+'''
+
+DUAL_HEADER_CGI = '''\
+print "Header: spam"
+print "Header: eggs"
+print
+print "cgi output"
+'''
+
+SPECIAL_HEADER_CGI = '''\
+print "Server: monkeys"
+print "Date: last year"
+print
+print "cgi output"
+'''
+
+READINPUT_CGI = '''\
+# this is an example of a correctly-written CGI script which reads a body
+# from stdin, which only reads env['CONTENT_LENGTH'] bytes.
+
+import os, sys
+
+body_length = int(os.environ.get('CONTENT_LENGTH',0))
+indata = sys.stdin.read(body_length)
+print "Header: OK"
+print
+print "readinput ok"
+'''
+
+READALLINPUT_CGI = '''\
+# this is an example of the typical (incorrect) CGI script which expects
+# the server to close stdin when the body of the request is complete.
+# A correct CGI should only read env['CONTENT_LENGTH'] bytes.
+
+import sys
+
+indata = sys.stdin.read()
+print "Header: OK"
+print
+print "readallinput ok"
+'''
+
+NO_DUPLICATE_CONTENT_TYPE_HEADER_CGI = '''\
+print "content-type: text/cgi-duplicate-test"
+print
+print "cgi output"
+'''
+
+class PythonScript(twcgi.FilteredScript):
+ filter = sys.executable
+
+class CGI(unittest.TestCase):
+ """
+ Tests for L{twcgi.FilteredScript}.
+ """
+
+ if not interfaces.IReactorProcess.providedBy(reactor):
+ skip = "CGI tests require a functional reactor.spawnProcess()"
+
+ def startServer(self, cgi):
+ root = resource.Resource()
+ cgipath = util.sibpath(__file__, cgi)
+ root.putChild("cgi", PythonScript(cgipath))
+ site = server.Site(root)
+ self.p = reactor.listenTCP(0, site)
+ return self.p.getHost().port
+
+ def tearDown(self):
+ if self.p:
+ return self.p.stopListening()
+
+
+ def writeCGI(self, source):
+ cgiFilename = os.path.abspath(self.mktemp())
+ cgiFile = file(cgiFilename, 'wt')
+ cgiFile.write(source)
+ cgiFile.close()
+ return cgiFilename
+
+
+ def testCGI(self):
+ cgiFilename = self.writeCGI(DUMMY_CGI)
+
+ portnum = self.startServer(cgiFilename)
+ d = client.getPage("http://localhost:%d/cgi" % portnum)
+ d.addCallback(self._testCGI_1)
+ return d
+
+
+ def _testCGI_1(self, res):
+ self.assertEqual(res, "cgi output" + os.linesep)
+
+
+ def test_protectedServerAndDate(self):
+ """
+ If the CGI script emits a I{Server} or I{Date} header, these are
+ ignored.
+ """
+ cgiFilename = self.writeCGI(SPECIAL_HEADER_CGI)
+
+ portnum = self.startServer(cgiFilename)
+ url = "http://localhost:%d/cgi" % (portnum,)
+ factory = client.HTTPClientFactory(url)
+ reactor.connectTCP('localhost', portnum, factory)
+ def checkResponse(ignored):
+ self.assertNotIn('monkeys', factory.response_headers['server'])
+ self.assertNotIn('last year', factory.response_headers['date'])
+ factory.deferred.addCallback(checkResponse)
+ return factory.deferred
+
+
+ def test_noDuplicateContentTypeHeaders(self):
+ """
+ If the CGI script emits a I{content-type} header, make sure that the
+ server doesn't add an additional (duplicate) one, as per ticket 4786.
+ """
+ cgiFilename = self.writeCGI(NO_DUPLICATE_CONTENT_TYPE_HEADER_CGI)
+
+ portnum = self.startServer(cgiFilename)
+ url = "http://localhost:%d/cgi" % (portnum,)
+ factory = client.HTTPClientFactory(url)
+ reactor.connectTCP('localhost', portnum, factory)
+ def checkResponse(ignored):
+ self.assertEqual(
+ factory.response_headers['content-type'], ['text/cgi-duplicate-test'])
+ factory.deferred.addCallback(checkResponse)
+ return factory.deferred
+
+
+ def test_duplicateHeaderCGI(self):
+ """
+ If a CGI script emits two instances of the same header, both are sent in
+ the response.
+ """
+ cgiFilename = self.writeCGI(DUAL_HEADER_CGI)
+
+ portnum = self.startServer(cgiFilename)
+ url = "http://localhost:%d/cgi" % (portnum,)
+ factory = client.HTTPClientFactory(url)
+ reactor.connectTCP('localhost', portnum, factory)
+ def checkResponse(ignored):
+ self.assertEqual(
+ factory.response_headers['header'], ['spam', 'eggs'])
+ factory.deferred.addCallback(checkResponse)
+ return factory.deferred
+
+
+ def testReadEmptyInput(self):
+ cgiFilename = os.path.abspath(self.mktemp())
+ cgiFile = file(cgiFilename, 'wt')
+ cgiFile.write(READINPUT_CGI)
+ cgiFile.close()
+
+ portnum = self.startServer(cgiFilename)
+ d = client.getPage("http://localhost:%d/cgi" % portnum)
+ d.addCallback(self._testReadEmptyInput_1)
+ return d
+ testReadEmptyInput.timeout = 5
+ def _testReadEmptyInput_1(self, res):
+ self.assertEqual(res, "readinput ok%s" % os.linesep)
+
+ def testReadInput(self):
+ cgiFilename = os.path.abspath(self.mktemp())
+ cgiFile = file(cgiFilename, 'wt')
+ cgiFile.write(READINPUT_CGI)
+ cgiFile.close()
+
+ portnum = self.startServer(cgiFilename)
+ d = client.getPage("http://localhost:%d/cgi" % portnum,
+ method="POST",
+ postdata="Here is your stdin")
+ d.addCallback(self._testReadInput_1)
+ return d
+ testReadInput.timeout = 5
+ def _testReadInput_1(self, res):
+ self.assertEqual(res, "readinput ok%s" % os.linesep)
+
+
+ def testReadAllInput(self):
+ cgiFilename = os.path.abspath(self.mktemp())
+ cgiFile = file(cgiFilename, 'wt')
+ cgiFile.write(READALLINPUT_CGI)
+ cgiFile.close()
+
+ portnum = self.startServer(cgiFilename)
+ d = client.getPage("http://localhost:%d/cgi" % portnum,
+ method="POST",
+ postdata="Here is your stdin")
+ d.addCallback(self._testReadAllInput_1)
+ return d
+ testReadAllInput.timeout = 5
+ def _testReadAllInput_1(self, res):
+ self.assertEqual(res, "readallinput ok%s" % os.linesep)
+
+
+
+class CGIDirectoryTests(unittest.TestCase):
+ """
+ Tests for L{twcgi.CGIDirectory}.
+ """
+ def test_render(self):
+ """
+ L{twcgi.CGIDirectory.render} sets the HTTP response code to I{NOT
+ FOUND}.
+ """
+ resource = twcgi.CGIDirectory(self.mktemp())
+ request = DummyRequest([''])
+ d = _render(resource, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_notFoundChild(self):
+ """
+ L{twcgi.CGIDirectory.getChild} returns a resource which renders an
+ response with the HTTP I{NOT FOUND} status code if the indicated child
+ does not exist as an entry in the directory used to initialized the
+ L{twcgi.CGIDirectory}.
+ """
+ path = self.mktemp()
+ os.makedirs(path)
+ resource = twcgi.CGIDirectory(path)
+ request = DummyRequest(['foo'])
+ child = resource.getChild("foo", request)
+ d = _render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
+
+
+
+class CGIProcessProtocolTests(unittest.TestCase):
+ """
+ Tests for L{twcgi.CGIProcessProtocol}.
+ """
+ def test_prematureEndOfHeaders(self):
+ """
+ If the process communicating with L{CGIProcessProtocol} ends before
+ finishing writing out headers, the response has I{INTERNAL SERVER
+ ERROR} as its status code.
+ """
+ request = DummyRequest([''])
+ protocol = twcgi.CGIProcessProtocol(request)
+ protocol.processEnded(failure.Failure(error.ProcessTerminated()))
+ self.assertEqual(request.responseCode, INTERNAL_SERVER_ERROR)
+
diff --git a/twisted/web/test/test_distrib.py b/twisted/web/test/test_distrib.py
new file mode 100755
index 0000000..c6e2ae3
--- /dev/null
+++ b/twisted/web/test/test_distrib.py
@@ -0,0 +1,434 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.distrib}.
+"""
+
+from os.path import abspath
+from xml.dom.minidom import parseString
+try:
+ import pwd
+except ImportError:
+ pwd = None
+
+from zope.interface.verify import verifyObject
+
+from twisted.python import log, filepath
+from twisted.internet import reactor, defer
+from twisted.trial import unittest
+from twisted.spread import pb
+from twisted.spread.banana import SIZE_LIMIT
+from twisted.web import http, distrib, client, resource, static, server
+from twisted.web.test.test_web import DummyRequest
+from twisted.web.test._util import _render
+from twisted.test import proto_helpers
+
+
+class MySite(server.Site):
+ pass
+
+
+class PBServerFactory(pb.PBServerFactory):
+ """
+ A PB server factory which keeps track of the most recent protocol it
+ created.
+
+ @ivar proto: L{None} or the L{Broker} instance most recently returned
+ from C{buildProtocol}.
+ """
+ proto = None
+
+ def buildProtocol(self, addr):
+ self.proto = pb.PBServerFactory.buildProtocol(self, addr)
+ return self.proto
+
+
+
+class DistribTest(unittest.TestCase):
+ port1 = None
+ port2 = None
+ sub = None
+ f1 = None
+
+ def tearDown(self):
+ """
+ Clean up all the event sources left behind by either directly by
+ test methods or indirectly via some distrib API.
+ """
+ dl = [defer.Deferred(), defer.Deferred()]
+ if self.f1 is not None and self.f1.proto is not None:
+ self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
+ else:
+ dl[0].callback(None)
+ if self.sub is not None and self.sub.publisher is not None:
+ self.sub.publisher.broker.notifyOnDisconnect(
+ lambda: dl[1].callback(None))
+ self.sub.publisher.broker.transport.loseConnection()
+ else:
+ dl[1].callback(None)
+ if self.port1 is not None:
+ dl.append(self.port1.stopListening())
+ if self.port2 is not None:
+ dl.append(self.port2.stopListening())
+ return defer.gatherResults(dl)
+
+
+ def testDistrib(self):
+ # site1 is the publisher
+ r1 = resource.Resource()
+ r1.putChild("there", static.Data("root", "text/plain"))
+ site1 = server.Site(r1)
+ self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
+ self.port1 = reactor.listenTCP(0, self.f1)
+ self.sub = distrib.ResourceSubscription("127.0.0.1",
+ self.port1.getHost().port)
+ r2 = resource.Resource()
+ r2.putChild("here", self.sub)
+ f2 = MySite(r2)
+ self.port2 = reactor.listenTCP(0, f2)
+ d = client.getPage("http://127.0.0.1:%d/here/there" % \
+ self.port2.getHost().port)
+ d.addCallback(self.assertEqual, 'root')
+ return d
+
+
+ def _setupDistribServer(self, child):
+ """
+ Set up a resource on a distrib site using L{ResourcePublisher}.
+
+ @param child: The resource to publish using distrib.
+
+ @return: A tuple consisting of the host and port on which to contact
+ the created site.
+ """
+ distribRoot = resource.Resource()
+ distribRoot.putChild("child", child)
+ distribSite = server.Site(distribRoot)
+ self.f1 = distribFactory = PBServerFactory(
+ distrib.ResourcePublisher(distribSite))
+ distribPort = reactor.listenTCP(
+ 0, distribFactory, interface="127.0.0.1")
+ self.addCleanup(distribPort.stopListening)
+ addr = distribPort.getHost()
+
+ self.sub = mainRoot = distrib.ResourceSubscription(
+ addr.host, addr.port)
+ mainSite = server.Site(mainRoot)
+ mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
+ self.addCleanup(mainPort.stopListening)
+ mainAddr = mainPort.getHost()
+
+ return mainPort, mainAddr
+
+
+ def _requestTest(self, child, **kwargs):
+ """
+ Set up a resource on a distrib site using L{ResourcePublisher} and
+ then retrieve it from a L{ResourceSubscription} via an HTTP client.
+
+ @param child: The resource to publish using distrib.
+ @param **kwargs: Extra keyword arguments to pass to L{getPage} when
+ requesting the resource.
+
+ @return: A L{Deferred} which fires with the result of the request.
+ """
+ mainPort, mainAddr = self._setupDistribServer(child)
+ return client.getPage("http://%s:%s/child" % (
+ mainAddr.host, mainAddr.port), **kwargs)
+
+
+ def _requestAgentTest(self, child, **kwargs):
+ """
+ Set up a resource on a distrib site using L{ResourcePublisher} and
+ then retrieve it from a L{ResourceSubscription} via an HTTP client.
+
+ @param child: The resource to publish using distrib.
+ @param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
+ requesting the resource.
+
+ @return: A L{Deferred} which fires with a tuple consisting of a
+ L{twisted.test.proto_helpers.AccumulatingProtocol} containing the
+ body of the response and an L{IResponse} with the response itself.
+ """
+ mainPort, mainAddr = self._setupDistribServer(child)
+
+ d = client.Agent(reactor).request("GET", "http://%s:%s/child" % (
+ mainAddr.host, mainAddr.port), **kwargs)
+
+ def cbCollectBody(response):
+ protocol = proto_helpers.AccumulatingProtocol()
+ response.deliverBody(protocol)
+ d = protocol.closedDeferred = defer.Deferred()
+ d.addCallback(lambda _: (protocol, response))
+ return d
+ d.addCallback(cbCollectBody)
+ return d
+
+
+ def test_requestHeaders(self):
+ """
+ The request headers are available on the request object passed to a
+ distributed resource's C{render} method.
+ """
+ requestHeaders = {}
+
+ class ReportRequestHeaders(resource.Resource):
+ def render(self, request):
+ requestHeaders.update(dict(
+ request.requestHeaders.getAllRawHeaders()))
+ return ""
+
+ request = self._requestTest(
+ ReportRequestHeaders(), headers={'foo': 'bar'})
+ def cbRequested(result):
+ self.assertEqual(requestHeaders['Foo'], ['bar'])
+ request.addCallback(cbRequested)
+ return request
+
+
+ def test_requestResponseCode(self):
+ """
+ The response code can be set by the request object passed to a
+ distributed resource's C{render} method.
+ """
+ class SetResponseCode(resource.Resource):
+ def render(self, request):
+ request.setResponseCode(200)
+ return ""
+
+ request = self._requestAgentTest(SetResponseCode())
+ def cbRequested(result):
+ self.assertEqual(result[0].data, "")
+ self.assertEqual(result[1].code, 200)
+ self.assertEqual(result[1].phrase, "OK")
+ request.addCallback(cbRequested)
+ return request
+
+
+ def test_requestResponseCodeMessage(self):
+ """
+ The response code and message can be set by the request object passed to
+ a distributed resource's C{render} method.
+ """
+ class SetResponseCode(resource.Resource):
+ def render(self, request):
+ request.setResponseCode(200, "some-message")
+ return ""
+
+ request = self._requestAgentTest(SetResponseCode())
+ def cbRequested(result):
+ self.assertEqual(result[0].data, "")
+ self.assertEqual(result[1].code, 200)
+ self.assertEqual(result[1].phrase, "some-message")
+ request.addCallback(cbRequested)
+ return request
+
+
+ def test_largeWrite(self):
+ """
+ If a string longer than the Banana size limit is passed to the
+ L{distrib.Request} passed to the remote resource, it is broken into
+ smaller strings to be transported over the PB connection.
+ """
+ class LargeWrite(resource.Resource):
+ def render(self, request):
+ request.write('x' * SIZE_LIMIT + 'y')
+ request.finish()
+ return server.NOT_DONE_YET
+
+ request = self._requestTest(LargeWrite())
+ request.addCallback(self.assertEqual, 'x' * SIZE_LIMIT + 'y')
+ return request
+
+
+ def test_largeReturn(self):
+ """
+ Like L{test_largeWrite}, but for the case where C{render} returns a
+ long string rather than explicitly passing it to L{Request.write}.
+ """
+ class LargeReturn(resource.Resource):
+ def render(self, request):
+ return 'x' * SIZE_LIMIT + 'y'
+
+ request = self._requestTest(LargeReturn())
+ request.addCallback(self.assertEqual, 'x' * SIZE_LIMIT + 'y')
+ return request
+
+
+ def test_connectionLost(self):
+ """
+ If there is an error issuing the request to the remote publisher, an
+ error response is returned.
+ """
+ # Using pb.Root as a publisher will cause request calls to fail with an
+ # error every time. Just what we want to test.
+ self.f1 = serverFactory = PBServerFactory(pb.Root())
+ self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
+
+ self.sub = subscription = distrib.ResourceSubscription(
+ "127.0.0.1", serverPort.getHost().port)
+ request = DummyRequest([''])
+ d = _render(subscription, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, 500)
+ # This is the error we caused the request to fail with. It should
+ # have been logged.
+ self.assertEqual(len(self.flushLoggedErrors(pb.NoSuchMethod)), 1)
+ d.addCallback(cbRendered)
+ return d
+
+
+
+class _PasswordDatabase:
+ def __init__(self, users):
+ self._users = users
+
+
+ def getpwall(self):
+ return iter(self._users)
+
+
+ def getpwnam(self, username):
+ for user in self._users:
+ if user[0] == username:
+ return user
+ raise KeyError()
+
+
+
+class UserDirectoryTests(unittest.TestCase):
+ """
+ Tests for L{UserDirectory}, a resource for listing all user resources
+ available on a system.
+ """
+ def setUp(self):
+ self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
+ self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
+ self.database = _PasswordDatabase([self.alice, self.bob])
+ self.directory = distrib.UserDirectory(self.database)
+
+
+ def test_interface(self):
+ """
+ L{UserDirectory} instances provide L{resource.IResource}.
+ """
+ self.assertTrue(verifyObject(resource.IResource, self.directory))
+
+
+ def _404Test(self, name):
+ """
+ Verify that requesting the C{name} child of C{self.directory} results
+ in a 404 response.
+ """
+ request = DummyRequest([name])
+ result = self.directory.getChild(name, request)
+ d = _render(result, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, 404)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_getInvalidUser(self):
+ """
+ L{UserDirectory.getChild} returns a resource which renders a 404
+ response when passed a string which does not correspond to any known
+ user.
+ """
+ return self._404Test('carol')
+
+
+ def test_getUserWithoutResource(self):
+ """
+ L{UserDirectory.getChild} returns a resource which renders a 404
+ response when passed a string which corresponds to a known user who has
+ neither a user directory nor a user distrib socket.
+ """
+ return self._404Test('alice')
+
+
+ def test_getPublicHTMLChild(self):
+ """
+ L{UserDirectory.getChild} returns a L{static.File} instance when passed
+ the name of a user with a home directory containing a I{public_html}
+ directory.
+ """
+ home = filepath.FilePath(self.bob[-2])
+ public_html = home.child('public_html')
+ public_html.makedirs()
+ request = DummyRequest(['bob'])
+ result = self.directory.getChild('bob', request)
+ self.assertIsInstance(result, static.File)
+ self.assertEqual(result.path, public_html.path)
+
+
+ def test_getDistribChild(self):
+ """
+ L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
+ when passed the name of a user suffixed with C{".twistd"} who has a
+ home directory containing a I{.twistd-web-pb} socket.
+ """
+ home = filepath.FilePath(self.bob[-2])
+ home.makedirs()
+ web = home.child('.twistd-web-pb')
+ request = DummyRequest(['bob'])
+ result = self.directory.getChild('bob.twistd', request)
+ self.assertIsInstance(result, distrib.ResourceSubscription)
+ self.assertEqual(result.host, 'unix')
+ self.assertEqual(abspath(result.port), web.path)
+
+
+ def test_invalidMethod(self):
+ """
+ L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
+ non-I{GET} request.
+ """
+ request = DummyRequest([''])
+ request.method = 'POST'
+ self.assertRaises(
+ server.UnsupportedMethod, self.directory.render, request)
+
+
+ def test_render(self):
+ """
+ L{UserDirectory} renders a list of links to available user content
+ in response to a I{GET} request.
+ """
+ public_html = filepath.FilePath(self.alice[-2]).child('public_html')
+ public_html.makedirs()
+ web = filepath.FilePath(self.bob[-2])
+ web.makedirs()
+ # This really only works if it's a unix socket, but the implementation
+ # doesn't currently check for that. It probably should someday, and
+ # then skip users with non-sockets.
+ web.child('.twistd-web-pb').setContent("")
+
+ request = DummyRequest([''])
+ result = _render(self.directory, request)
+ def cbRendered(ignored):
+ document = parseString(''.join(request.written))
+
+ # Each user should have an li with a link to their page.
+ [alice, bob] = document.getElementsByTagName('li')
+ self.assertEqual(alice.firstChild.tagName, 'a')
+ self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
+ self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
+ self.assertEqual(bob.firstChild.tagName, 'a')
+ self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
+ self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
+
+ result.addCallback(cbRendered)
+ return result
+
+
+ def test_passwordDatabase(self):
+ """
+ If L{UserDirectory} is instantiated with no arguments, it uses the
+ L{pwd} module as its password database.
+ """
+ directory = distrib.UserDirectory()
+ self.assertIdentical(directory._pwd, pwd)
+ if pwd is None:
+ test_passwordDatabase.skip = "pwd module required"
+
diff --git a/twisted/web/test/test_domhelpers.py b/twisted/web/test/test_domhelpers.py
new file mode 100644
index 0000000..b008374
--- /dev/null
+++ b/twisted/web/test/test_domhelpers.py
@@ -0,0 +1,306 @@
+# -*- test-case-name: twisted.web.test.test_domhelpers -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Specific tests for (some of) the methods in L{twisted.web.domhelpers}.
+"""
+
+from xml.dom import minidom
+
+from twisted.trial.unittest import TestCase
+
+from twisted.web import microdom
+
+from twisted.web import domhelpers
+
+
+class DOMHelpersTestsMixin:
+ """
+ A mixin for L{TestCase} subclasses which defines test methods for
+ domhelpers functionality based on a DOM creation function provided by a
+ subclass.
+ """
+ dom = None
+
+ def test_getElementsByTagName(self):
+ doc1 = self.dom.parseString('<foo/>')
+ actual=domhelpers.getElementsByTagName(doc1, 'foo')[0].nodeName
+ expected='foo'
+ self.assertEqual(actual, expected)
+ el1=doc1.documentElement
+ actual=domhelpers.getElementsByTagName(el1, 'foo')[0].nodeName
+ self.assertEqual(actual, expected)
+
+ doc2_xml='<a><foo in="a"/><b><foo in="b"/></b><c><foo in="c"/></c><foo in="d"/><foo in="ef"/><g><foo in="g"/><h><foo in="h"/></h></g></a>'
+ doc2 = self.dom.parseString(doc2_xml)
+ tag_list=domhelpers.getElementsByTagName(doc2, 'foo')
+ actual=''.join([node.getAttribute('in') for node in tag_list])
+ expected='abcdefgh'
+ self.assertEqual(actual, expected)
+ el2=doc2.documentElement
+ tag_list=domhelpers.getElementsByTagName(el2, 'foo')
+ actual=''.join([node.getAttribute('in') for node in tag_list])
+ self.assertEqual(actual, expected)
+
+ doc3_xml='''
+<a><foo in="a"/>
+ <b><foo in="b"/>
+ <d><foo in="d"/>
+ <g><foo in="g"/></g>
+ <h><foo in="h"/></h>
+ </d>
+ <e><foo in="e"/>
+ <i><foo in="i"/></i>
+ </e>
+ </b>
+ <c><foo in="c"/>
+ <f><foo in="f"/>
+ <j><foo in="j"/></j>
+ </f>
+ </c>
+</a>'''
+ doc3 = self.dom.parseString(doc3_xml)
+ tag_list=domhelpers.getElementsByTagName(doc3, 'foo')
+ actual=''.join([node.getAttribute('in') for node in tag_list])
+ expected='abdgheicfj'
+ self.assertEqual(actual, expected)
+ el3=doc3.documentElement
+ tag_list=domhelpers.getElementsByTagName(el3, 'foo')
+ actual=''.join([node.getAttribute('in') for node in tag_list])
+ self.assertEqual(actual, expected)
+
+ doc4_xml='<foo><bar></bar><baz><foo/></baz></foo>'
+ doc4 = self.dom.parseString(doc4_xml)
+ actual=domhelpers.getElementsByTagName(doc4, 'foo')
+ root=doc4.documentElement
+ expected=[root, root.childNodes[-1].childNodes[0]]
+ self.assertEqual(actual, expected)
+ actual=domhelpers.getElementsByTagName(root, 'foo')
+ self.assertEqual(actual, expected)
+
+
+ def test_gatherTextNodes(self):
+ doc1 = self.dom.parseString('<a>foo</a>')
+ actual=domhelpers.gatherTextNodes(doc1)
+ expected='foo'
+ self.assertEqual(actual, expected)
+ actual=domhelpers.gatherTextNodes(doc1.documentElement)
+ self.assertEqual(actual, expected)
+
+ doc2_xml='<a>a<b>b</b><c>c</c>def<g>g<h>h</h></g></a>'
+ doc2 = self.dom.parseString(doc2_xml)
+ actual=domhelpers.gatherTextNodes(doc2)
+ expected='abcdefgh'
+ self.assertEqual(actual, expected)
+ actual=domhelpers.gatherTextNodes(doc2.documentElement)
+ self.assertEqual(actual, expected)
+
+ doc3_xml=('<a>a<b>b<d>d<g>g</g><h>h</h></d><e>e<i>i</i></e></b>' +
+ '<c>c<f>f<j>j</j></f></c></a>')
+ doc3 = self.dom.parseString(doc3_xml)
+ actual=domhelpers.gatherTextNodes(doc3)
+ expected='abdgheicfj'
+ self.assertEqual(actual, expected)
+ actual=domhelpers.gatherTextNodes(doc3.documentElement)
+ self.assertEqual(actual, expected)
+
+ def test_clearNode(self):
+ doc1 = self.dom.parseString('<a><b><c><d/></c></b></a>')
+ a_node=doc1.documentElement
+ domhelpers.clearNode(a_node)
+ self.assertEqual(
+ a_node.toxml(),
+ self.dom.Element('a').toxml())
+
+ doc2 = self.dom.parseString('<a><b><c><d/></c></b></a>')
+ b_node=doc2.documentElement.childNodes[0]
+ domhelpers.clearNode(b_node)
+ actual=doc2.documentElement.toxml()
+ expected = self.dom.Element('a')
+ expected.appendChild(self.dom.Element('b'))
+ self.assertEqual(actual, expected.toxml())
+
+
+ def test_get(self):
+ doc1 = self.dom.parseString('<a><b id="bar"/><c class="foo"/></a>')
+ node=domhelpers.get(doc1, "foo")
+ actual=node.toxml()
+ expected = self.dom.Element('c')
+ expected.setAttribute('class', 'foo')
+ self.assertEqual(actual, expected.toxml())
+
+ node=domhelpers.get(doc1, "bar")
+ actual=node.toxml()
+ expected = self.dom.Element('b')
+ expected.setAttribute('id', 'bar')
+ self.assertEqual(actual, expected.toxml())
+
+ self.assertRaises(domhelpers.NodeLookupError,
+ domhelpers.get,
+ doc1,
+ "pzork")
+
+ def test_getIfExists(self):
+ doc1 = self.dom.parseString('<a><b id="bar"/><c class="foo"/></a>')
+ node=domhelpers.getIfExists(doc1, "foo")
+ actual=node.toxml()
+ expected = self.dom.Element('c')
+ expected.setAttribute('class', 'foo')
+ self.assertEqual(actual, expected.toxml())
+
+ node=domhelpers.getIfExists(doc1, "pzork")
+ self.assertIdentical(node, None)
+
+
+ def test_getAndClear(self):
+ doc1 = self.dom.parseString('<a><b id="foo"><c></c></b></a>')
+ node=domhelpers.getAndClear(doc1, "foo")
+ actual=node.toxml()
+ expected = self.dom.Element('b')
+ expected.setAttribute('id', 'foo')
+ self.assertEqual(actual, expected.toxml())
+
+
+ def test_locateNodes(self):
+ doc1 = self.dom.parseString('<a><b foo="olive"><c foo="olive"/></b><d foo="poopy"/></a>')
+ node_list=domhelpers.locateNodes(
+ doc1.childNodes, 'foo', 'olive', noNesting=1)
+ actual=''.join([node.toxml() for node in node_list])
+ expected = self.dom.Element('b')
+ expected.setAttribute('foo', 'olive')
+ c = self.dom.Element('c')
+ c.setAttribute('foo', 'olive')
+ expected.appendChild(c)
+
+ self.assertEqual(actual, expected.toxml())
+
+ node_list=domhelpers.locateNodes(
+ doc1.childNodes, 'foo', 'olive', noNesting=0)
+ actual=''.join([node.toxml() for node in node_list])
+ self.assertEqual(actual, expected.toxml() + c.toxml())
+
+
+ def test_getParents(self):
+ doc1 = self.dom.parseString('<a><b><c><d/></c><e/></b><f/></a>')
+ node_list = domhelpers.getParents(
+ doc1.childNodes[0].childNodes[0].childNodes[0])
+ actual = ''.join([node.tagName for node in node_list
+ if hasattr(node, 'tagName')])
+ self.assertEqual(actual, 'cba')
+
+
+ def test_findElementsWithAttribute(self):
+ doc1 = self.dom.parseString('<a foo="1"><b foo="2"/><c foo="1"/><d/></a>')
+ node_list = domhelpers.findElementsWithAttribute(doc1, 'foo')
+ actual = ''.join([node.tagName for node in node_list])
+ self.assertEqual(actual, 'abc')
+
+ node_list = domhelpers.findElementsWithAttribute(doc1, 'foo', '1')
+ actual = ''.join([node.tagName for node in node_list])
+ self.assertEqual(actual, 'ac')
+
+
+ def test_findNodesNamed(self):
+ doc1 = self.dom.parseString('<doc><foo/><bar/><foo>a</foo></doc>')
+ node_list = domhelpers.findNodesNamed(doc1, 'foo')
+ actual = len(node_list)
+ self.assertEqual(actual, 2)
+
+ # NOT SURE WHAT THESE ARE SUPPOSED TO DO..
+ # def test_RawText FIXME
+ # def test_superSetAttribute FIXME
+ # def test_superPrependAttribute FIXME
+ # def test_superAppendAttribute FIXME
+ # def test_substitute FIXME
+
+ def test_escape(self):
+ j='this string " contains many & characters> xml< won\'t like'
+ expected='this string &quot; contains many &amp; characters&gt; xml&lt; won\'t like'
+ self.assertEqual(domhelpers.escape(j), expected)
+
+ def test_unescape(self):
+ j='this string &quot; has &&amp; entities &gt; &lt; and some characters xml won\'t like<'
+ expected='this string " has && entities > < and some characters xml won\'t like<'
+ self.assertEqual(domhelpers.unescape(j), expected)
+
+
+ def test_getNodeText(self):
+ """
+ L{getNodeText} returns the concatenation of all the text data at or
+ beneath the node passed to it.
+ """
+ node = self.dom.parseString('<foo><bar>baz</bar><bar>quux</bar></foo>')
+ self.assertEqual(domhelpers.getNodeText(node), "bazquux")
+
+
+
+class MicroDOMHelpersTests(DOMHelpersTestsMixin, TestCase):
+ dom = microdom
+
+ def test_gatherTextNodesDropsWhitespace(self):
+ """
+ Microdom discards whitespace-only text nodes, so L{gatherTextNodes}
+ returns only the text from nodes which had non-whitespace characters.
+ """
+ doc4_xml='''<html>
+ <head>
+ </head>
+ <body>
+ stuff
+ </body>
+</html>
+'''
+ doc4 = self.dom.parseString(doc4_xml)
+ actual = domhelpers.gatherTextNodes(doc4)
+ expected = '\n stuff\n '
+ self.assertEqual(actual, expected)
+ actual = domhelpers.gatherTextNodes(doc4.documentElement)
+ self.assertEqual(actual, expected)
+
+
+ def test_textEntitiesNotDecoded(self):
+ """
+ Microdom does not decode entities in text nodes.
+ """
+ doc5_xml='<x>Souffl&amp;</x>'
+ doc5 = self.dom.parseString(doc5_xml)
+ actual=domhelpers.gatherTextNodes(doc5)
+ expected='Souffl&amp;'
+ self.assertEqual(actual, expected)
+ actual=domhelpers.gatherTextNodes(doc5.documentElement)
+ self.assertEqual(actual, expected)
+
+
+
+class MiniDOMHelpersTests(DOMHelpersTestsMixin, TestCase):
+ dom = minidom
+
+ def test_textEntitiesDecoded(self):
+ """
+ Minidom does decode entities in text nodes.
+ """
+ doc5_xml='<x>Souffl&amp;</x>'
+ doc5 = self.dom.parseString(doc5_xml)
+ actual=domhelpers.gatherTextNodes(doc5)
+ expected='Souffl&'
+ self.assertEqual(actual, expected)
+ actual=domhelpers.gatherTextNodes(doc5.documentElement)
+ self.assertEqual(actual, expected)
+
+
+ def test_getNodeUnicodeText(self):
+ """
+ L{domhelpers.getNodeText} returns a C{unicode} string when text
+ nodes are represented in the DOM with unicode, whether or not there
+ are non-ASCII characters present.
+ """
+ node = self.dom.parseString("<foo>bar</foo>")
+ text = domhelpers.getNodeText(node)
+ self.assertEqual(text, u"bar")
+ self.assertIsInstance(text, unicode)
+
+ node = self.dom.parseString(u"<foo>\N{SNOWMAN}</foo>".encode('utf-8'))
+ text = domhelpers.getNodeText(node)
+ self.assertEqual(text, u"\N{SNOWMAN}")
+ self.assertIsInstance(text, unicode)
diff --git a/twisted/web/test/test_error.py b/twisted/web/test/test_error.py
new file mode 100644
index 0000000..4daa7d9
--- /dev/null
+++ b/twisted/web/test/test_error.py
@@ -0,0 +1,151 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+HTTP errors.
+"""
+
+from twisted.trial import unittest
+from twisted.web import error
+
+class ErrorTestCase(unittest.TestCase):
+ """
+ Tests for how L{Error} attributes are initialized.
+ """
+ def test_noMessageValidStatus(self):
+ """
+ If no C{message} argument is passed to the L{Error} constructor and the
+ C{code} argument is a valid HTTP status code, C{code} is mapped to a
+ descriptive string to which C{message} is assigned.
+ """
+ e = error.Error("200")
+ self.assertEqual(e.message, "OK")
+
+
+ def test_noMessageInvalidStatus(self):
+ """
+ If no C{message} argument is passed to the L{Error} constructor and
+ C{code} isn't a valid HTTP status code, C{message} stays C{None}.
+ """
+ e = error.Error("InvalidCode")
+ self.assertEqual(e.message, None)
+
+
+ def test_messageExists(self):
+ """
+ If a C{message} argument is passed to the L{Error} constructor, the
+ C{message} isn't affected by the value of C{status}.
+ """
+ e = error.Error("200", "My own message")
+ self.assertEqual(e.message, "My own message")
+
+
+
+class PageRedirectTestCase(unittest.TestCase):
+ """
+ Tests for how L{PageRedirect} attributes are initialized.
+ """
+ def test_noMessageValidStatus(self):
+ """
+ If no C{message} argument is passed to the L{PageRedirect} constructor
+ and the C{code} argument is a valid HTTP status code, C{code} is mapped
+ to a descriptive string to which C{message} is assigned.
+ """
+ e = error.PageRedirect("200", location="/foo")
+ self.assertEqual(e.message, "OK to /foo")
+
+
+ def test_noMessageValidStatusNoLocation(self):
+ """
+ If no C{message} argument is passed to the L{PageRedirect} constructor
+ and C{location} is also empty and the C{code} argument is a valid HTTP
+ status code, C{code} is mapped to a descriptive string to which
+ C{message} is assigned without trying to include an empty location.
+ """
+ e = error.PageRedirect("200")
+ self.assertEqual(e.message, "OK")
+
+
+ def test_noMessageInvalidStatusLocationExists(self):
+ """
+ If no C{message} argument is passed to the L{PageRedirect} constructor
+ and C{code} isn't a valid HTTP status code, C{message} stays C{None}.
+ """
+ e = error.PageRedirect("InvalidCode", location="/foo")
+ self.assertEqual(e.message, None)
+
+
+ def test_messageExistsLocationExists(self):
+ """
+ If a C{message} argument is passed to the L{PageRedirect} constructor,
+ the C{message} isn't affected by the value of C{status}.
+ """
+ e = error.PageRedirect("200", "My own message", location="/foo")
+ self.assertEqual(e.message, "My own message to /foo")
+
+
+ def test_messageExistsNoLocation(self):
+ """
+ If a C{message} argument is passed to the L{PageRedirect} constructor
+ and no location is provided, C{message} doesn't try to include the empty
+ location.
+ """
+ e = error.PageRedirect("200", "My own message")
+ self.assertEqual(e.message, "My own message")
+
+
+
+class InfiniteRedirectionTestCase(unittest.TestCase):
+ """
+ Tests for how L{InfiniteRedirection} attributes are initialized.
+ """
+ def test_noMessageValidStatus(self):
+ """
+ If no C{message} argument is passed to the L{InfiniteRedirection}
+ constructor and the C{code} argument is a valid HTTP status code,
+ C{code} is mapped to a descriptive string to which C{message} is
+ assigned.
+ """
+ e = error.InfiniteRedirection("200", location="/foo")
+ self.assertEqual(e.message, "OK to /foo")
+
+
+ def test_noMessageValidStatusNoLocation(self):
+ """
+ If no C{message} argument is passed to the L{InfiniteRedirection}
+ constructor and C{location} is also empty and the C{code} argument is a
+ valid HTTP status code, C{code} is mapped to a descriptive string to
+ which C{message} is assigned without trying to include an empty
+ location.
+ """
+ e = error.InfiniteRedirection("200")
+ self.assertEqual(e.message, "OK")
+
+
+ def test_noMessageInvalidStatusLocationExists(self):
+ """
+ If no C{message} argument is passed to the L{InfiniteRedirection}
+ constructor and C{code} isn't a valid HTTP status code, C{message} stays
+ C{None}.
+ """
+ e = error.InfiniteRedirection("InvalidCode", location="/foo")
+ self.assertEqual(e.message, None)
+
+
+ def test_messageExistsLocationExists(self):
+ """
+ If a C{message} argument is passed to the L{InfiniteRedirection}
+ constructor, the C{message} isn't affected by the value of C{status}.
+ """
+ e = error.InfiniteRedirection("200", "My own message", location="/foo")
+ self.assertEqual(e.message, "My own message to /foo")
+
+
+ def test_messageExistsNoLocation(self):
+ """
+ If a C{message} argument is passed to the L{InfiniteRedirection}
+ constructor and no location is provided, C{message} doesn't try to
+ include the empty location.
+ """
+ e = error.InfiniteRedirection("200", "My own message")
+ self.assertEqual(e.message, "My own message")
diff --git a/twisted/web/test/test_flatten.py b/twisted/web/test/test_flatten.py
new file mode 100644
index 0000000..c843a61
--- /dev/null
+++ b/twisted/web/test/test_flatten.py
@@ -0,0 +1,348 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+import traceback
+
+from zope.interface import implements
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import succeed, gatherResults
+from twisted.web._stan import Tag
+from twisted.web._flatten import flattenString
+from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
+from twisted.web.template import tags, Comment, CDATA, CharRef, slot
+from twisted.web.iweb import IRenderable
+from twisted.web.test._util import FlattenTestCase
+
+
+class TestSerialization(FlattenTestCase):
+ """
+ Tests for flattening various things.
+ """
+ def test_nestedTags(self):
+ """
+ Test that nested tags flatten correctly.
+ """
+ return self.assertFlattensTo(
+ tags.html(tags.body('42'), hi='there'),
+ '<html hi="there"><body>42</body></html>')
+
+
+ def test_serializeString(self):
+ """
+ Test that strings will be flattened and escaped correctly.
+ """
+ return gatherResults([
+ self.assertFlattensTo('one', 'one'),
+ self.assertFlattensTo('<abc&&>123', '&lt;abc&amp;&amp;&gt;123'),
+ ])
+
+
+ def test_serializeSelfClosingTags(self):
+ """
+ Test that some tags are normally written out as self-closing tags.
+ """
+ return self.assertFlattensTo(tags.img(src='test'), '<img src="test" />')
+
+
+ def test_serializeComment(self):
+ """
+ Test that comments are correctly flattened and escaped.
+ """
+ return self.assertFlattensTo(Comment('foo bar'), '<!--foo bar-->'),
+
+
+ def test_commentEscaping(self):
+ """
+ The data in a L{Comment} is escaped and mangled in the flattened output
+ so that the result is a legal SGML and XML comment.
+
+ SGML comment syntax is complicated and hard to use. This rule is more
+ restrictive, and more compatible:
+
+ Comments start with <!-- and end with --> and never contain -- or >.
+
+ Also by XML syntax, a comment may not end with '-'.
+
+ @see: U{http://www.w3.org/TR/REC-xml/#sec-comments}
+ """
+ def verifyComment(c):
+ self.assertTrue(
+ c.startswith('<!--'),
+ "%r does not start with the comment prefix" % (c,))
+ self.assertTrue(
+ c.endswith('-->'),
+ "%r does not end with the comment suffix" % (c,))
+ # If it is shorter than 7, then the prefix and suffix overlap
+ # illegally.
+ self.assertTrue(
+ len(c) >= 7,
+ "%r is too short to be a legal comment" % (c,))
+ content = c[4:-3]
+ self.assertNotIn('--', content)
+ self.assertNotIn('>', content)
+ if content:
+ self.assertNotEqual(content[-1], '-')
+
+ results = []
+ for c in [
+ '',
+ 'foo---bar',
+ 'foo---bar-',
+ 'foo>bar',
+ 'foo-->bar',
+ '----------------',
+ ]:
+ d = flattenString(None, Comment(c))
+ d.addCallback(verifyComment)
+ results.append(d)
+ return gatherResults(results)
+
+
+ def test_serializeCDATA(self):
+ """
+ Test that CDATA is correctly flattened and escaped.
+ """
+ return gatherResults([
+ self.assertFlattensTo(CDATA('foo bar'), '<![CDATA[foo bar]]>'),
+ self.assertFlattensTo(
+ CDATA('foo ]]> bar'),
+ '<![CDATA[foo ]]]]><![CDATA[> bar]]>'),
+ ])
+
+
+ def test_serializeUnicode(self):
+ """
+ Test that unicode is encoded correctly in the appropriate places, and
+ raises an error when it occurs in inappropriate place.
+ """
+ snowman = u'\N{SNOWMAN}'
+ return gatherResults([
+ self.assertFlattensTo(snowman, '\xe2\x98\x83'),
+ self.assertFlattensTo(tags.p(snowman), '<p>\xe2\x98\x83</p>'),
+ self.assertFlattensTo(Comment(snowman), '<!--\xe2\x98\x83-->'),
+ self.assertFlattensTo(CDATA(snowman), '<![CDATA[\xe2\x98\x83]]>'),
+ self.assertFlatteningRaises(
+ Tag(snowman), UnicodeEncodeError),
+ self.assertFlatteningRaises(
+ Tag('p', attributes={snowman: ''}), UnicodeEncodeError),
+ ])
+
+
+ def test_serializeCharRef(self):
+ """
+ A character reference is flattened to a string using the I{&#NNNN;}
+ syntax.
+ """
+ ref = CharRef(ord(u"\N{SNOWMAN}"))
+ return self.assertFlattensTo(ref, "&#9731;")
+
+
+ def test_serializeDeferred(self):
+ """
+ Test that a deferred is substituted with the current value in the
+ callback chain when flattened.
+ """
+ return self.assertFlattensTo(succeed('two'), 'two')
+
+
+ def test_serializeSameDeferredTwice(self):
+ """
+ Test that the same deferred can be flattened twice.
+ """
+ d = succeed('three')
+ return gatherResults([
+ self.assertFlattensTo(d, 'three'),
+ self.assertFlattensTo(d, 'three'),
+ ])
+
+
+ def test_serializeIRenderable(self):
+ """
+ Test that flattening respects all of the IRenderable interface.
+ """
+ class FakeElement(object):
+ implements(IRenderable)
+ def render(ign,ored):
+ return tags.p(
+ 'hello, ',
+ tags.transparent(render='test'), ' - ',
+ tags.transparent(render='test'))
+ def lookupRenderMethod(ign, name):
+ self.assertEqual(name, 'test')
+ return lambda ign, node: node('world')
+
+ return gatherResults([
+ self.assertFlattensTo(FakeElement(), '<p>hello, world - world</p>'),
+ ])
+
+
+ def test_serializeSlots(self):
+ """
+ Test that flattening a slot will use the slot value from the tag.
+ """
+ t1 = tags.p(slot('test'))
+ t2 = t1.clone()
+ t2.fillSlots(test='hello, world')
+ return gatherResults([
+ self.assertFlatteningRaises(t1, UnfilledSlot),
+ self.assertFlattensTo(t2, '<p>hello, world</p>'),
+ ])
+
+
+ def test_serializeDeferredSlots(self):
+ """
+ Test that a slot with a deferred as its value will be flattened using
+ the value from the deferred.
+ """
+ t = tags.p(slot('test'))
+ t.fillSlots(test=succeed(tags.em('four>')))
+ return self.assertFlattensTo(t, '<p><em>four&gt;</em></p>')
+
+
+ def test_unknownTypeRaises(self):
+ """
+ Test that flattening an unknown type of thing raises an exception.
+ """
+ return self.assertFlatteningRaises(None, UnsupportedType)
+
+
+# Use the co_filename mechanism (instead of the __file__ mechanism) because
+# it is the mechanism traceback formatting uses. The two do not necessarily
+# agree with each other. This requires a code object compiled in this file.
+# The easiest way to get a code object is with a new function. I'll use a
+# lambda to avoid adding anything else to this namespace. The result will
+# be a string which agrees with the one the traceback module will put into a
+# traceback for frames associated with functions defined in this file.
+
+HERE = (lambda: None).func_code.co_filename
+
+
+class FlattenerErrorTests(TestCase):
+ """
+ Tests for L{FlattenerError}.
+ """
+
+ def test_string(self):
+ """
+ If a L{FlattenerError} is created with a string root, up to around 40
+ bytes from that string are included in the string representation of the
+ exception.
+ """
+ self.assertEqual(
+ str(FlattenerError(RuntimeError("reason"), ['abc123xyz'], [])),
+ "Exception while flattening:\n"
+ " 'abc123xyz'\n"
+ "RuntimeError: reason\n")
+ self.assertEqual(
+ str(FlattenerError(
+ RuntimeError("reason"), ['0123456789' * 10], [])),
+ "Exception while flattening:\n"
+ " '01234567890123456789<...>01234567890123456789'\n"
+ "RuntimeError: reason\n")
+
+
+ def test_unicode(self):
+ """
+ If a L{FlattenerError} is created with a unicode root, up to around 40
+ characters from that string are included in the string representation
+ of the exception.
+ """
+ self.assertEqual(
+ str(FlattenerError(
+ RuntimeError("reason"), [u'abc\N{SNOWMAN}xyz'], [])),
+ "Exception while flattening:\n"
+ " u'abc\\u2603xyz'\n" # Codepoint for SNOWMAN
+ "RuntimeError: reason\n")
+ self.assertEqual(
+ str(FlattenerError(
+ RuntimeError("reason"), [u'01234567\N{SNOWMAN}9' * 10],
+ [])),
+ "Exception while flattening:\n"
+ " u'01234567\\u2603901234567\\u26039<...>01234567\\u2603901234567"
+ "\\u26039'\n"
+ "RuntimeError: reason\n")
+
+
+ def test_renderable(self):
+ """
+ If a L{FlattenerError} is created with an L{IRenderable} provider root,
+ the repr of that object is included in the string representation of the
+ exception.
+ """
+ class Renderable(object):
+ implements(IRenderable)
+
+ def __repr__(self):
+ return "renderable repr"
+
+ self.assertEqual(
+ str(FlattenerError(
+ RuntimeError("reason"), [Renderable()], [])),
+ "Exception while flattening:\n"
+ " renderable repr\n"
+ "RuntimeError: reason\n")
+
+
+ def test_tag(self):
+ """
+ If a L{FlattenerError} is created with a L{Tag} instance with source
+ location information, the source location is included in the string
+ representation of the exception.
+ """
+ tag = Tag(
+ 'div', filename='/foo/filename.xhtml', lineNumber=17, columnNumber=12)
+
+ self.assertEqual(
+ str(FlattenerError(RuntimeError("reason"), [tag], [])),
+ "Exception while flattening:\n"
+ " File \"/foo/filename.xhtml\", line 17, column 12, in \"div\"\n"
+ "RuntimeError: reason\n")
+
+
+ def test_tagWithoutLocation(self):
+ """
+ If a L{FlattenerError} is created with a L{Tag} instance without source
+ location information, only the tagName is included in the string
+ representation of the exception.
+ """
+ self.assertEqual(
+ str(FlattenerError(RuntimeError("reason"), [Tag('span')], [])),
+ "Exception while flattening:\n"
+ " Tag <span>\n"
+ "RuntimeError: reason\n")
+
+
+ def test_traceback(self):
+ """
+ If a L{FlattenerError} is created with traceback frames, they are
+ included in the string representation of the exception.
+ """
+ # Try to be realistic in creating the data passed in for the traceback
+ # frames.
+ def f():
+ g()
+ def g():
+ raise RuntimeError("reason")
+
+ try:
+ f()
+ except RuntimeError, exc:
+ # Get the traceback, minus the info for *this* frame
+ tbinfo = traceback.extract_tb(sys.exc_info()[2])[1:]
+ else:
+ self.fail("f() must raise RuntimeError")
+
+ self.assertEqual(
+ str(FlattenerError(exc, [], tbinfo)),
+ "Exception while flattening:\n"
+ " File \"%s\", line %d, in f\n"
+ " g()\n"
+ " File \"%s\", line %d, in g\n"
+ " raise RuntimeError(\"reason\")\n"
+ "RuntimeError: reason\n" % (
+ HERE, f.func_code.co_firstlineno + 1,
+ HERE, g.func_code.co_firstlineno + 1))
+
diff --git a/twisted/web/test/test_http.py b/twisted/web/test/test_http.py
new file mode 100644
index 0000000..bdb39a4
--- /dev/null
+++ b/twisted/web/test/test_http.py
@@ -0,0 +1,1663 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test HTTP support.
+"""
+
+from urlparse import urlparse, urlunsplit, clear_cache
+import random, urllib, cgi
+
+from twisted.python.compat import set
+from twisted.python.failure import Failure
+from twisted.trial import unittest
+from twisted.trial.unittest import TestCase
+from twisted.web import http, http_headers
+from twisted.web.http import PotentialDataLoss, _DataLoss
+from twisted.web.http import _IdentityTransferDecoder
+from twisted.protocols import loopback
+from twisted.internet.task import Clock
+from twisted.internet.error import ConnectionLost
+from twisted.test.proto_helpers import StringTransport
+from twisted.test.test_internet import DummyProducer
+from twisted.web.test.test_web import DummyChannel
+
+
+class DateTimeTest(unittest.TestCase):
+ """Test date parsing functions."""
+
+ def testRoundtrip(self):
+ for i in range(10000):
+ time = random.randint(0, 2000000000)
+ timestr = http.datetimeToString(time)
+ time2 = http.stringToDatetime(timestr)
+ self.assertEqual(time, time2)
+
+
+class DummyHTTPHandler(http.Request):
+
+ def process(self):
+ self.content.seek(0, 0)
+ data = self.content.read()
+ length = self.getHeader('content-length')
+ request = "'''\n"+str(length)+"\n"+data+"'''\n"
+ self.setResponseCode(200)
+ self.setHeader("Request", self.uri)
+ self.setHeader("Command", self.method)
+ self.setHeader("Version", self.clientproto)
+ self.setHeader("Content-Length", len(request))
+ self.write(request)
+ self.finish()
+
+
+class LoopbackHTTPClient(http.HTTPClient):
+
+ def connectionMade(self):
+ self.sendCommand("GET", "/foo/bar")
+ self.sendHeader("Content-Length", 10)
+ self.endHeaders()
+ self.transport.write("0123456789")
+
+
+class ResponseTestMixin(object):
+ """
+ A mixin that provides a simple means of comparing an actual response string
+ to an expected response string by performing the minimal parsing.
+ """
+
+ def assertResponseEquals(self, responses, expected):
+ """
+ Assert that the C{responses} matches the C{expected} responses.
+
+ @type responses: C{str}
+ @param responses: The bytes sent in response to one or more requests.
+
+ @type expected: C{list} of C{tuple} of C{str}
+ @param expected: The expected values for the responses. Each tuple
+ element of the list represents one response. Each string element
+ of the tuple is a full header line without delimiter, except for
+ the last element which gives the full response body.
+ """
+ for response in expected:
+ expectedHeaders, expectedContent = response[:-1], response[-1]
+ headers, rest = responses.split('\r\n\r\n', 1)
+ headers = headers.splitlines()
+ self.assertEqual(set(headers), set(expectedHeaders))
+ content = rest[:len(expectedContent)]
+ responses = rest[len(expectedContent):]
+ self.assertEqual(content, expectedContent)
+
+
+
+class HTTP1_0TestCase(unittest.TestCase, ResponseTestMixin):
+ requests = (
+ "GET / HTTP/1.0\r\n"
+ "\r\n"
+ "GET / HTTP/1.1\r\n"
+ "Accept: text/html\r\n"
+ "\r\n")
+
+ expected_response = [
+ ("HTTP/1.0 200 OK",
+ "Request: /",
+ "Command: GET",
+ "Version: HTTP/1.0",
+ "Content-Length: 13",
+ "'''\nNone\n'''\n")]
+
+ def test_buffer(self):
+ """
+ Send requests over a channel and check responses match what is expected.
+ """
+ b = StringTransport()
+ a = http.HTTPChannel()
+ a.requestFactory = DummyHTTPHandler
+ a.makeConnection(b)
+ # one byte at a time, to stress it.
+ for byte in self.requests:
+ a.dataReceived(byte)
+ a.connectionLost(IOError("all one"))
+ value = b.value()
+ self.assertResponseEquals(value, self.expected_response)
+
+
+ def test_requestBodyTimeout(self):
+ """
+ L{HTTPChannel} resets its timeout whenever data from a request body is
+ delivered to it.
+ """
+ clock = Clock()
+ transport = StringTransport()
+ protocol = http.HTTPChannel()
+ protocol.timeOut = 100
+ protocol.callLater = clock.callLater
+ protocol.makeConnection(transport)
+ protocol.dataReceived('POST / HTTP/1.0\r\nContent-Length: 2\r\n\r\n')
+ clock.advance(99)
+ self.assertFalse(transport.disconnecting)
+ protocol.dataReceived('x')
+ clock.advance(99)
+ self.assertFalse(transport.disconnecting)
+ protocol.dataReceived('x')
+ self.assertEqual(len(protocol.requests), 1)
+
+
+
+class HTTP1_1TestCase(HTTP1_0TestCase):
+
+ requests = (
+ "GET / HTTP/1.1\r\n"
+ "Accept: text/html\r\n"
+ "\r\n"
+ "POST / HTTP/1.1\r\n"
+ "Content-Length: 10\r\n"
+ "\r\n"
+ "0123456789POST / HTTP/1.1\r\n"
+ "Content-Length: 10\r\n"
+ "\r\n"
+ "0123456789HEAD / HTTP/1.1\r\n"
+ "\r\n")
+
+ expected_response = [
+ ("HTTP/1.1 200 OK",
+ "Request: /",
+ "Command: GET",
+ "Version: HTTP/1.1",
+ "Content-Length: 13",
+ "'''\nNone\n'''\n"),
+ ("HTTP/1.1 200 OK",
+ "Request: /",
+ "Command: POST",
+ "Version: HTTP/1.1",
+ "Content-Length: 21",
+ "'''\n10\n0123456789'''\n"),
+ ("HTTP/1.1 200 OK",
+ "Request: /",
+ "Command: POST",
+ "Version: HTTP/1.1",
+ "Content-Length: 21",
+ "'''\n10\n0123456789'''\n"),
+ ("HTTP/1.1 200 OK",
+ "Request: /",
+ "Command: HEAD",
+ "Version: HTTP/1.1",
+ "Content-Length: 13",
+ "")]
+
+
+
+class HTTP1_1_close_TestCase(HTTP1_0TestCase):
+
+ requests = (
+ "GET / HTTP/1.1\r\n"
+ "Accept: text/html\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "GET / HTTP/1.0\r\n"
+ "\r\n")
+
+ expected_response = [
+ ("HTTP/1.1 200 OK",
+ "Connection: close",
+ "Request: /",
+ "Command: GET",
+ "Version: HTTP/1.1",
+ "Content-Length: 13",
+ "'''\nNone\n'''\n")]
+
+
+
+class HTTP0_9TestCase(HTTP1_0TestCase):
+
+ requests = (
+ "GET /\r\n")
+
+ expected_response = "HTTP/1.1 400 Bad Request\r\n\r\n"
+
+
+ def assertResponseEquals(self, response, expectedResponse):
+ self.assertEqual(response, expectedResponse)
+
+
+class HTTPLoopbackTestCase(unittest.TestCase):
+
+ expectedHeaders = {'request' : '/foo/bar',
+ 'command' : 'GET',
+ 'version' : 'HTTP/1.0',
+ 'content-length' : '21'}
+ numHeaders = 0
+ gotStatus = 0
+ gotResponse = 0
+ gotEndHeaders = 0
+
+ def _handleStatus(self, version, status, message):
+ self.gotStatus = 1
+ self.assertEqual(version, "HTTP/1.0")
+ self.assertEqual(status, "200")
+
+ def _handleResponse(self, data):
+ self.gotResponse = 1
+ self.assertEqual(data, "'''\n10\n0123456789'''\n")
+
+ def _handleHeader(self, key, value):
+ self.numHeaders = self.numHeaders + 1
+ self.assertEqual(self.expectedHeaders[key.lower()], value)
+
+ def _handleEndHeaders(self):
+ self.gotEndHeaders = 1
+ self.assertEqual(self.numHeaders, 4)
+
+ def testLoopback(self):
+ server = http.HTTPChannel()
+ server.requestFactory = DummyHTTPHandler
+ client = LoopbackHTTPClient()
+ client.handleResponse = self._handleResponse
+ client.handleHeader = self._handleHeader
+ client.handleEndHeaders = self._handleEndHeaders
+ client.handleStatus = self._handleStatus
+ d = loopback.loopbackAsync(server, client)
+ d.addCallback(self._cbTestLoopback)
+ return d
+
+ def _cbTestLoopback(self, ignored):
+ if not (self.gotStatus and self.gotResponse and self.gotEndHeaders):
+ raise RuntimeError(
+ "didn't got all callbacks %s"
+ % [self.gotStatus, self.gotResponse, self.gotEndHeaders])
+ del self.gotEndHeaders
+ del self.gotResponse
+ del self.gotStatus
+ del self.numHeaders
+
+
+
+def _prequest(**headers):
+ """
+ Make a request with the given request headers for the persistence tests.
+ """
+ request = http.Request(DummyChannel(), None)
+ for k, v in headers.iteritems():
+ request.requestHeaders.setRawHeaders(k, v)
+ return request
+
+
+class PersistenceTestCase(unittest.TestCase):
+ """
+ Tests for persistent HTTP connections.
+ """
+
+ ptests = [#(PRequest(connection="Keep-Alive"), "HTTP/1.0", 1, {'connection' : 'Keep-Alive'}),
+ (_prequest(), "HTTP/1.0", 0, {'connection': None}),
+ (_prequest(connection=["close"]), "HTTP/1.1", 0, {'connection' : ['close']}),
+ (_prequest(), "HTTP/1.1", 1, {'connection': None}),
+ (_prequest(), "HTTP/0.9", 0, {'connection': None}),
+ ]
+
+
+ def testAlgorithm(self):
+ c = http.HTTPChannel()
+ for req, version, correctResult, resultHeaders in self.ptests:
+ result = c.checkPersistence(req, version)
+ self.assertEqual(result, correctResult)
+ for header in resultHeaders.keys():
+ self.assertEqual(req.responseHeaders.getRawHeaders(header, None), resultHeaders[header])
+
+
+
+class IdentityTransferEncodingTests(TestCase):
+ """
+ Tests for L{_IdentityTransferDecoder}.
+ """
+ def setUp(self):
+ """
+ Create an L{_IdentityTransferDecoder} with callbacks hooked up so that
+ calls to them can be inspected.
+ """
+ self.data = []
+ self.finish = []
+ self.contentLength = 10
+ self.decoder = _IdentityTransferDecoder(
+ self.contentLength, self.data.append, self.finish.append)
+
+
+ def test_exactAmountReceived(self):
+ """
+ If L{_IdentityTransferDecoder.dataReceived} is called with a string
+ with length equal to the content length passed to
+ L{_IdentityTransferDecoder}'s initializer, the data callback is invoked
+ with that string and the finish callback is invoked with a zero-length
+ string.
+ """
+ self.decoder.dataReceived('x' * self.contentLength)
+ self.assertEqual(self.data, ['x' * self.contentLength])
+ self.assertEqual(self.finish, [''])
+
+
+ def test_shortStrings(self):
+ """
+ If L{_IdentityTransferDecoder.dataReceived} is called multiple times
+ with strings which, when concatenated, are as long as the content
+ length provided, the data callback is invoked with each string and the
+ finish callback is invoked only after the second call.
+ """
+ self.decoder.dataReceived('x')
+ self.assertEqual(self.data, ['x'])
+ self.assertEqual(self.finish, [])
+ self.decoder.dataReceived('y' * (self.contentLength - 1))
+ self.assertEqual(self.data, ['x', 'y' * (self.contentLength - 1)])
+ self.assertEqual(self.finish, [''])
+
+
+ def test_longString(self):
+ """
+ If L{_IdentityTransferDecoder.dataReceived} is called with a string
+ with length greater than the provided content length, only the prefix
+ of that string up to the content length is passed to the data callback
+ and the remainder is passed to the finish callback.
+ """
+ self.decoder.dataReceived('x' * self.contentLength + 'y')
+ self.assertEqual(self.data, ['x' * self.contentLength])
+ self.assertEqual(self.finish, ['y'])
+
+
+ def test_rejectDataAfterFinished(self):
+ """
+ If data is passed to L{_IdentityTransferDecoder.dataReceived} after the
+ finish callback has been invoked, L{RuntimeError} is raised.
+ """
+ failures = []
+ def finish(bytes):
+ try:
+ decoder.dataReceived('foo')
+ except:
+ failures.append(Failure())
+ decoder = _IdentityTransferDecoder(5, self.data.append, finish)
+ decoder.dataReceived('x' * 4)
+ self.assertEqual(failures, [])
+ decoder.dataReceived('y')
+ failures[0].trap(RuntimeError)
+ self.assertEqual(
+ str(failures[0].value),
+ "_IdentityTransferDecoder cannot decode data after finishing")
+
+
+ def test_unknownContentLength(self):
+ """
+ If L{_IdentityTransferDecoder} is constructed with C{None} for the
+ content length, it passes all data delivered to it through to the data
+ callback.
+ """
+ data = []
+ finish = []
+ decoder = _IdentityTransferDecoder(None, data.append, finish.append)
+ decoder.dataReceived('x')
+ self.assertEqual(data, ['x'])
+ decoder.dataReceived('y')
+ self.assertEqual(data, ['x', 'y'])
+ self.assertEqual(finish, [])
+
+
+ def _verifyCallbacksUnreferenced(self, decoder):
+ """
+ Check the decoder's data and finish callbacks and make sure they are
+ None in order to help avoid references cycles.
+ """
+ self.assertIdentical(decoder.dataCallback, None)
+ self.assertIdentical(decoder.finishCallback, None)
+
+
+ def test_earlyConnectionLose(self):
+ """
+ L{_IdentityTransferDecoder.noMoreData} raises L{_DataLoss} if it is
+ called and the content length is known but not enough bytes have been
+ delivered.
+ """
+ self.decoder.dataReceived('x' * (self.contentLength - 1))
+ self.assertRaises(_DataLoss, self.decoder.noMoreData)
+ self._verifyCallbacksUnreferenced(self.decoder)
+
+
+ def test_unknownContentLengthConnectionLose(self):
+ """
+ L{_IdentityTransferDecoder.noMoreData} calls the finish callback and
+ raises L{PotentialDataLoss} if it is called and the content length is
+ unknown.
+ """
+ body = []
+ finished = []
+ decoder = _IdentityTransferDecoder(None, body.append, finished.append)
+ self.assertRaises(PotentialDataLoss, decoder.noMoreData)
+ self.assertEqual(body, [])
+ self.assertEqual(finished, [''])
+ self._verifyCallbacksUnreferenced(decoder)
+
+
+ def test_finishedConnectionLose(self):
+ """
+ L{_IdentityTransferDecoder.noMoreData} does not raise any exception if
+ it is called when the content length is known and that many bytes have
+ been delivered.
+ """
+ self.decoder.dataReceived('x' * self.contentLength)
+ self.decoder.noMoreData()
+ self._verifyCallbacksUnreferenced(self.decoder)
+
+
+
+class ChunkedTransferEncodingTests(unittest.TestCase):
+ """
+ Tests for L{_ChunkedTransferDecoder}, which turns a byte stream encoded
+ using HTTP I{chunked} C{Transfer-Encoding} back into the original byte
+ stream.
+ """
+ def test_decoding(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} decodes chunked-encoded data
+ and passes the result to the specified callback.
+ """
+ L = []
+ p = http._ChunkedTransferDecoder(L.append, None)
+ p.dataReceived('3\r\nabc\r\n5\r\n12345\r\n')
+ p.dataReceived('a\r\n0123456789\r\n')
+ self.assertEqual(L, ['abc', '12345', '0123456789'])
+
+
+ def test_short(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} decodes chunks broken up and
+ delivered in multiple calls.
+ """
+ L = []
+ finished = []
+ p = http._ChunkedTransferDecoder(L.append, finished.append)
+ for s in '3\r\nabc\r\n5\r\n12345\r\n0\r\n\r\n':
+ p.dataReceived(s)
+ self.assertEqual(L, ['a', 'b', 'c', '1', '2', '3', '4', '5'])
+ self.assertEqual(finished, [''])
+
+
+ def test_newlines(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} doesn't treat CR LF pairs
+ embedded in chunk bodies specially.
+ """
+ L = []
+ p = http._ChunkedTransferDecoder(L.append, None)
+ p.dataReceived('2\r\n\r\n\r\n')
+ self.assertEqual(L, ['\r\n'])
+
+
+ def test_extensions(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} disregards chunk-extension
+ fields.
+ """
+ L = []
+ p = http._ChunkedTransferDecoder(L.append, None)
+ p.dataReceived('3; x-foo=bar\r\nabc\r\n')
+ self.assertEqual(L, ['abc'])
+
+
+ def test_finish(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} interprets a zero-length
+ chunk as the end of the chunked data stream and calls the completion
+ callback.
+ """
+ finished = []
+ p = http._ChunkedTransferDecoder(None, finished.append)
+ p.dataReceived('0\r\n\r\n')
+ self.assertEqual(finished, [''])
+
+
+ def test_extra(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} passes any bytes which come
+ after the terminating zero-length chunk to the completion callback.
+ """
+ finished = []
+ p = http._ChunkedTransferDecoder(None, finished.append)
+ p.dataReceived('0\r\n\r\nhello')
+ self.assertEqual(finished, ['hello'])
+
+
+ def test_afterFinished(self):
+ """
+ L{_ChunkedTransferDecoder.dataReceived} raises L{RuntimeError} if it
+ is called after it has seen the last chunk.
+ """
+ p = http._ChunkedTransferDecoder(None, lambda bytes: None)
+ p.dataReceived('0\r\n\r\n')
+ self.assertRaises(RuntimeError, p.dataReceived, 'hello')
+
+
+ def test_earlyConnectionLose(self):
+ """
+ L{_ChunkedTransferDecoder.noMoreData} raises L{_DataLoss} if it is
+ called and the end of the last trailer has not yet been received.
+ """
+ parser = http._ChunkedTransferDecoder(None, lambda bytes: None)
+ parser.dataReceived('0\r\n\r')
+ exc = self.assertRaises(_DataLoss, parser.noMoreData)
+ self.assertEqual(
+ str(exc),
+ "Chunked decoder in 'trailer' state, still expecting more data "
+ "to get to finished state.")
+
+
+ def test_finishedConnectionLose(self):
+ """
+ L{_ChunkedTransferDecoder.noMoreData} does not raise any exception if
+ it is called after the terminal zero length chunk is received.
+ """
+ parser = http._ChunkedTransferDecoder(None, lambda bytes: None)
+ parser.dataReceived('0\r\n\r\n')
+ parser.noMoreData()
+
+
+ def test_reentrantFinishedNoMoreData(self):
+ """
+ L{_ChunkedTransferDecoder.noMoreData} can be called from the finished
+ callback without raising an exception.
+ """
+ errors = []
+ successes = []
+ def finished(extra):
+ try:
+ parser.noMoreData()
+ except:
+ errors.append(Failure())
+ else:
+ successes.append(True)
+ parser = http._ChunkedTransferDecoder(None, finished)
+ parser.dataReceived('0\r\n\r\n')
+ self.assertEqual(errors, [])
+ self.assertEqual(successes, [True])
+
+
+
+class ChunkingTestCase(unittest.TestCase):
+
+ strings = ["abcv", "", "fdfsd423", "Ffasfas\r\n",
+ "523523\n\rfsdf", "4234"]
+
+ def testChunks(self):
+ for s in self.strings:
+ self.assertEqual((s, ''), http.fromChunk(''.join(http.toChunk(s))))
+ self.assertRaises(ValueError, http.fromChunk, '-5\r\nmalformed!\r\n')
+
+ def testConcatenatedChunks(self):
+ chunked = ''.join([''.join(http.toChunk(t)) for t in self.strings])
+ result = []
+ buffer = ""
+ for c in chunked:
+ buffer = buffer + c
+ try:
+ data, buffer = http.fromChunk(buffer)
+ result.append(data)
+ except ValueError:
+ pass
+ self.assertEqual(result, self.strings)
+
+
+
+class ParsingTestCase(unittest.TestCase):
+ """
+ Tests for protocol parsing in L{HTTPChannel}.
+ """
+ def runRequest(self, httpRequest, requestClass, success=1):
+ httpRequest = httpRequest.replace("\n", "\r\n")
+ b = StringTransport()
+ a = http.HTTPChannel()
+ a.requestFactory = requestClass
+ a.makeConnection(b)
+ # one byte at a time, to stress it.
+ for byte in httpRequest:
+ if a.transport.disconnecting:
+ break
+ a.dataReceived(byte)
+ a.connectionLost(IOError("all done"))
+ if success:
+ self.assertEqual(self.didRequest, 1)
+ del self.didRequest
+ else:
+ self.assert_(not hasattr(self, "didRequest"))
+ return a
+
+
+ def test_basicAuth(self):
+ """
+ L{HTTPChannel} provides username and password information supplied in
+ an I{Authorization} header to the L{Request} which makes it available
+ via its C{getUser} and C{getPassword} methods.
+ """
+ testcase = self
+ class Request(http.Request):
+ l = []
+ def process(self):
+ testcase.assertEqual(self.getUser(), self.l[0])
+ testcase.assertEqual(self.getPassword(), self.l[1])
+ for u, p in [("foo", "bar"), ("hello", "there:z")]:
+ Request.l[:] = [u, p]
+ s = "%s:%s" % (u, p)
+ f = "GET / HTTP/1.0\nAuthorization: Basic %s\n\n" % (s.encode("base64").strip(), )
+ self.runRequest(f, Request, 0)
+
+
+ def test_headers(self):
+ """
+ Headers received by L{HTTPChannel} in a request are made available to
+ the L{Request}.
+ """
+ processed = []
+ class MyRequest(http.Request):
+ def process(self):
+ processed.append(self)
+ self.finish()
+
+ requestLines = [
+ "GET / HTTP/1.0",
+ "Foo: bar",
+ "baz: Quux",
+ "baz: quux",
+ "",
+ ""]
+
+ self.runRequest('\n'.join(requestLines), MyRequest, 0)
+ [request] = processed
+ self.assertEqual(
+ request.requestHeaders.getRawHeaders('foo'), ['bar'])
+ self.assertEqual(
+ request.requestHeaders.getRawHeaders('bAz'), ['Quux', 'quux'])
+
+
+ def test_tooManyHeaders(self):
+ """
+ L{HTTPChannel} enforces a limit of C{HTTPChannel.maxHeaders} on the
+ number of headers received per request.
+ """
+ processed = []
+ class MyRequest(http.Request):
+ def process(self):
+ processed.append(self)
+
+ requestLines = ["GET / HTTP/1.0"]
+ for i in range(http.HTTPChannel.maxHeaders + 2):
+ requestLines.append("%s: foo" % (i,))
+ requestLines.extend(["", ""])
+
+ channel = self.runRequest("\n".join(requestLines), MyRequest, 0)
+ self.assertEqual(processed, [])
+ self.assertEqual(
+ channel.transport.value(),
+ "HTTP/1.1 400 Bad Request\r\n\r\n")
+
+
+ def test_headerLimitPerRequest(self):
+ """
+ L{HTTPChannel} enforces the limit of C{HTTPChannel.maxHeaders} per
+ request so that headers received in an earlier request do not count
+ towards the limit when processing a later request.
+ """
+ processed = []
+ class MyRequest(http.Request):
+ def process(self):
+ processed.append(self)
+ self.finish()
+
+ self.patch(http.HTTPChannel, 'maxHeaders', 1)
+ requestLines = [
+ "GET / HTTP/1.1",
+ "Foo: bar",
+ "",
+ "",
+ "GET / HTTP/1.1",
+ "Bar: baz",
+ "",
+ ""]
+
+ channel = self.runRequest("\n".join(requestLines), MyRequest, 0)
+ [first, second] = processed
+ self.assertEqual(first.getHeader('foo'), 'bar')
+ self.assertEqual(second.getHeader('bar'), 'baz')
+ self.assertEqual(
+ channel.transport.value(),
+ 'HTTP/1.1 200 OK\r\n'
+ 'Transfer-Encoding: chunked\r\n'
+ '\r\n'
+ '0\r\n'
+ '\r\n'
+ 'HTTP/1.1 200 OK\r\n'
+ 'Transfer-Encoding: chunked\r\n'
+ '\r\n'
+ '0\r\n'
+ '\r\n')
+
+
+ def testCookies(self):
+ """
+ Test cookies parsing and reading.
+ """
+ httpRequest = '''\
+GET / HTTP/1.0
+Cookie: rabbit="eat carrot"; ninja=secret; spam="hey 1=1!"
+
+'''
+ testcase = self
+
+ class MyRequest(http.Request):
+ def process(self):
+ testcase.assertEqual(self.getCookie('rabbit'), '"eat carrot"')
+ testcase.assertEqual(self.getCookie('ninja'), 'secret')
+ testcase.assertEqual(self.getCookie('spam'), '"hey 1=1!"')
+ testcase.didRequest = 1
+ self.finish()
+
+ self.runRequest(httpRequest, MyRequest)
+
+ def testGET(self):
+ httpRequest = '''\
+GET /?key=value&multiple=two+words&multiple=more%20words&empty= HTTP/1.0
+
+'''
+ testcase = self
+ class MyRequest(http.Request):
+ def process(self):
+ testcase.assertEqual(self.method, "GET")
+ testcase.assertEqual(self.args["key"], ["value"])
+ testcase.assertEqual(self.args["empty"], [""])
+ testcase.assertEqual(self.args["multiple"], ["two words", "more words"])
+ testcase.didRequest = 1
+ self.finish()
+
+ self.runRequest(httpRequest, MyRequest)
+
+
+ def test_extraQuestionMark(self):
+ """
+ While only a single '?' is allowed in an URL, several other servers
+ allow several and pass all after the first through as part of the
+ query arguments. Test that we emulate this behavior.
+ """
+ httpRequest = 'GET /foo?bar=?&baz=quux HTTP/1.0\n\n'
+
+ testcase = self
+ class MyRequest(http.Request):
+ def process(self):
+ testcase.assertEqual(self.method, 'GET')
+ testcase.assertEqual(self.path, '/foo')
+ testcase.assertEqual(self.args['bar'], ['?'])
+ testcase.assertEqual(self.args['baz'], ['quux'])
+ testcase.didRequest = 1
+ self.finish()
+
+ self.runRequest(httpRequest, MyRequest)
+
+
+ def test_formPOSTRequest(self):
+ """
+ The request body of a I{POST} request with a I{Content-Type} header
+ of I{application/x-www-form-urlencoded} is parsed according to that
+ content type and made available in the C{args} attribute of the
+ request object. The original bytes of the request may still be read
+ from the C{content} attribute.
+ """
+ query = 'key=value&multiple=two+words&multiple=more%20words&empty='
+ httpRequest = '''\
+POST / HTTP/1.0
+Content-Length: %d
+Content-Type: application/x-www-form-urlencoded
+
+%s''' % (len(query), query)
+
+ testcase = self
+ class MyRequest(http.Request):
+ def process(self):
+ testcase.assertEqual(self.method, "POST")
+ testcase.assertEqual(self.args["key"], ["value"])
+ testcase.assertEqual(self.args["empty"], [""])
+ testcase.assertEqual(self.args["multiple"], ["two words", "more words"])
+
+ # Reading from the content file-like must produce the entire
+ # request body.
+ testcase.assertEqual(self.content.read(), query)
+ testcase.didRequest = 1
+ self.finish()
+
+ self.runRequest(httpRequest, MyRequest)
+
+ def testMissingContentDisposition(self):
+ req = '''\
+POST / HTTP/1.0
+Content-Type: multipart/form-data; boundary=AaB03x
+Content-Length: 103
+
+--AaB03x
+Content-Type: text/plain
+Content-Transfer-Encoding: quoted-printable
+
+abasdfg
+--AaB03x--
+'''
+ self.runRequest(req, http.Request, success=False)
+
+ def test_chunkedEncoding(self):
+ """
+ If a request uses the I{chunked} transfer encoding, the request body is
+ decoded accordingly before it is made available on the request.
+ """
+ httpRequest = '''\
+GET / HTTP/1.0
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+6
+Hello,
+14
+ spam,eggs spam spam
+0
+
+'''
+ testcase = self
+ class MyRequest(http.Request):
+ def process(self):
+ # The tempfile API used to create content returns an
+ # instance of a different type depending on what platform
+ # we're running on. The point here is to verify that the
+ # request body is in a file that's on the filesystem.
+ # Having a fileno method that returns an int is a somewhat
+ # close approximation of this. -exarkun
+ testcase.assertIsInstance(self.content.fileno(), int)
+ testcase.assertEqual(self.method, 'GET')
+ testcase.assertEqual(self.path, '/')
+ content = self.content.read()
+ testcase.assertEqual(content, 'Hello, spam,eggs spam spam')
+ testcase.assertIdentical(self.channel._transferDecoder, None)
+ testcase.didRequest = 1
+ self.finish()
+
+ self.runRequest(httpRequest, MyRequest)
+
+
+
+class QueryArgumentsTestCase(unittest.TestCase):
+ def testParseqs(self):
+ self.assertEqual(cgi.parse_qs("a=b&d=c;+=f"),
+ http.parse_qs("a=b&d=c;+=f"))
+ self.failUnlessRaises(ValueError, http.parse_qs, "blah",
+ strict_parsing = 1)
+ self.assertEqual(cgi.parse_qs("a=&b=c", keep_blank_values = 1),
+ http.parse_qs("a=&b=c", keep_blank_values = 1))
+ self.assertEqual(cgi.parse_qs("a=&b=c"),
+ http.parse_qs("a=&b=c"))
+
+
+ def test_urlparse(self):
+ """
+ For a given URL, L{http.urlparse} should behave the same as
+ L{urlparse}, except it should always return C{str}, never C{unicode}.
+ """
+ def urls():
+ for scheme in ('http', 'https'):
+ for host in ('example.com',):
+ for port in (None, 100):
+ for path in ('', 'path'):
+ if port is not None:
+ host = host + ':' + str(port)
+ yield urlunsplit((scheme, host, path, '', ''))
+
+
+ def assertSameParsing(url, decode):
+ """
+ Verify that C{url} is parsed into the same objects by both
+ L{http.urlparse} and L{urlparse}.
+ """
+ urlToStandardImplementation = url
+ if decode:
+ urlToStandardImplementation = url.decode('ascii')
+ standardResult = urlparse(urlToStandardImplementation)
+ scheme, netloc, path, params, query, fragment = http.urlparse(url)
+ self.assertEqual(
+ (scheme, netloc, path, params, query, fragment),
+ standardResult)
+ self.assertTrue(isinstance(scheme, str))
+ self.assertTrue(isinstance(netloc, str))
+ self.assertTrue(isinstance(path, str))
+ self.assertTrue(isinstance(params, str))
+ self.assertTrue(isinstance(query, str))
+ self.assertTrue(isinstance(fragment, str))
+
+ # With caching, unicode then str
+ clear_cache()
+ for url in urls():
+ assertSameParsing(url, True)
+ assertSameParsing(url, False)
+
+ # With caching, str then unicode
+ clear_cache()
+ for url in urls():
+ assertSameParsing(url, False)
+ assertSameParsing(url, True)
+
+ # Without caching
+ for url in urls():
+ clear_cache()
+ assertSameParsing(url, True)
+ clear_cache()
+ assertSameParsing(url, False)
+
+
+ def test_urlparseRejectsUnicode(self):
+ """
+ L{http.urlparse} should reject unicode input early.
+ """
+ self.assertRaises(TypeError, http.urlparse, u'http://example.org/path')
+
+
+
+class ClientDriver(http.HTTPClient):
+ def handleStatus(self, version, status, message):
+ self.version = version
+ self.status = status
+ self.message = message
+
+class ClientStatusParsing(unittest.TestCase):
+ def testBaseline(self):
+ c = ClientDriver()
+ c.lineReceived('HTTP/1.0 201 foo')
+ self.assertEqual(c.version, 'HTTP/1.0')
+ self.assertEqual(c.status, '201')
+ self.assertEqual(c.message, 'foo')
+
+ def testNoMessage(self):
+ c = ClientDriver()
+ c.lineReceived('HTTP/1.0 201')
+ self.assertEqual(c.version, 'HTTP/1.0')
+ self.assertEqual(c.status, '201')
+ self.assertEqual(c.message, '')
+
+ def testNoMessage_trailingSpace(self):
+ c = ClientDriver()
+ c.lineReceived('HTTP/1.0 201 ')
+ self.assertEqual(c.version, 'HTTP/1.0')
+ self.assertEqual(c.status, '201')
+ self.assertEqual(c.message, '')
+
+
+
+class RequestTests(unittest.TestCase, ResponseTestMixin):
+ """
+ Tests for L{http.Request}
+ """
+ def _compatHeadersTest(self, oldName, newName):
+ """
+ Verify that each of two different attributes which are associated with
+ the same state properly reflect changes made through the other.
+
+ This is used to test that the C{headers}/C{responseHeaders} and
+ C{received_headers}/C{requestHeaders} pairs interact properly.
+ """
+ req = http.Request(DummyChannel(), None)
+ getattr(req, newName).setRawHeaders("test", ["lemur"])
+ self.assertEqual(getattr(req, oldName)["test"], "lemur")
+ setattr(req, oldName, {"foo": "bar"})
+ self.assertEqual(
+ list(getattr(req, newName).getAllRawHeaders()),
+ [("Foo", ["bar"])])
+ setattr(req, newName, http_headers.Headers())
+ self.assertEqual(getattr(req, oldName), {})
+
+
+ def test_received_headers(self):
+ """
+ L{Request.received_headers} is a backwards compatible API which
+ accesses and allows mutation of the state at L{Request.requestHeaders}.
+ """
+ self._compatHeadersTest('received_headers', 'requestHeaders')
+
+
+ def test_headers(self):
+ """
+ L{Request.headers} is a backwards compatible API which accesses and
+ allows mutation of the state at L{Request.responseHeaders}.
+ """
+ self._compatHeadersTest('headers', 'responseHeaders')
+
+
+ def test_getHeader(self):
+ """
+ L{http.Request.getHeader} returns the value of the named request
+ header.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders("test", ["lemur"])
+ self.assertEqual(req.getHeader("test"), "lemur")
+
+
+ def test_getHeaderReceivedMultiples(self):
+ """
+ When there are multiple values for a single request header,
+ L{http.Request.getHeader} returns the last value.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders("test", ["lemur", "panda"])
+ self.assertEqual(req.getHeader("test"), "panda")
+
+
+ def test_getHeaderNotFound(self):
+ """
+ L{http.Request.getHeader} returns C{None} when asked for the value of a
+ request header which is not present.
+ """
+ req = http.Request(DummyChannel(), None)
+ self.assertEqual(req.getHeader("test"), None)
+
+
+ def test_getAllHeaders(self):
+ """
+ L{http.Request.getAllheaders} returns a C{dict} mapping all request
+ header names to their corresponding values.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders("test", ["lemur"])
+ self.assertEqual(req.getAllHeaders(), {"test": "lemur"})
+
+
+ def test_getAllHeadersNoHeaders(self):
+ """
+ L{http.Request.getAllHeaders} returns an empty C{dict} if there are no
+ request headers.
+ """
+ req = http.Request(DummyChannel(), None)
+ self.assertEqual(req.getAllHeaders(), {})
+
+
+ def test_getAllHeadersMultipleHeaders(self):
+ """
+ When there are multiple values for a single request header,
+ L{http.Request.getAllHeaders} returns only the last value.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders("test", ["lemur", "panda"])
+ self.assertEqual(req.getAllHeaders(), {"test": "panda"})
+
+
+ def test_setResponseCode(self):
+ """
+ L{http.Request.setResponseCode} takes a status code and causes it to be
+ used as the response status.
+ """
+ channel = DummyChannel()
+ req = http.Request(channel, None)
+ req.setResponseCode(201)
+ req.write('')
+ self.assertEqual(
+ channel.transport.written.getvalue().splitlines()[0],
+ '%s 201 Created' % (req.clientproto,))
+
+
+ def test_setResponseCodeAndMessage(self):
+ """
+ L{http.Request.setResponseCode} takes a status code and a message and
+ causes them to be used as the response status.
+ """
+ channel = DummyChannel()
+ req = http.Request(channel, None)
+ req.setResponseCode(202, "happily accepted")
+ req.write('')
+ self.assertEqual(
+ channel.transport.written.getvalue().splitlines()[0],
+ '%s 202 happily accepted' % (req.clientproto,))
+
+
+ def test_setResponseCodeAcceptsIntegers(self):
+ """
+ L{http.Request.setResponseCode} accepts C{int} or C{long} for the code
+ parameter and raises L{TypeError} if passed anything else.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.setResponseCode(1)
+ req.setResponseCode(1L)
+ self.assertRaises(TypeError, req.setResponseCode, "1")
+
+
+ def test_setHost(self):
+ """
+ L{http.Request.setHost} sets the value of the host request header.
+ The port should not be added because it is the default.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.setHost("example.com", 80)
+ self.assertEqual(
+ req.requestHeaders.getRawHeaders("host"), ["example.com"])
+
+
+ def test_setHostSSL(self):
+ """
+ L{http.Request.setHost} sets the value of the host request header.
+ The port should not be added because it is the default.
+ """
+ d = DummyChannel()
+ d.transport = DummyChannel.SSL()
+ req = http.Request(d, None)
+ req.setHost("example.com", 443)
+ self.assertEqual(
+ req.requestHeaders.getRawHeaders("host"), ["example.com"])
+
+
+ def test_setHostNonDefaultPort(self):
+ """
+ L{http.Request.setHost} sets the value of the host request header.
+ The port should be added because it is not the default.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.setHost("example.com", 81)
+ self.assertEqual(
+ req.requestHeaders.getRawHeaders("host"), ["example.com:81"])
+
+
+ def test_setHostSSLNonDefaultPort(self):
+ """
+ L{http.Request.setHost} sets the value of the host request header.
+ The port should be added because it is not the default.
+ """
+ d = DummyChannel()
+ d.transport = DummyChannel.SSL()
+ req = http.Request(d, None)
+ req.setHost("example.com", 81)
+ self.assertEqual(
+ req.requestHeaders.getRawHeaders("host"), ["example.com:81"])
+
+
+ def test_setHeader(self):
+ """
+ L{http.Request.setHeader} sets the value of the given response header.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.setHeader("test", "lemur")
+ self.assertEqual(req.responseHeaders.getRawHeaders("test"), ["lemur"])
+
+
+ def test_firstWrite(self):
+ """
+ For an HTTP 1.0 request, L{http.Request.write} sends an HTTP 1.0
+ Response-Line and whatever response headers are set.
+ """
+ req = http.Request(DummyChannel(), None)
+ trans = StringTransport()
+
+ req.transport = trans
+
+ req.setResponseCode(200)
+ req.clientproto = "HTTP/1.0"
+ req.responseHeaders.setRawHeaders("test", ["lemur"])
+ req.write('Hello')
+
+ self.assertResponseEquals(
+ trans.value(),
+ [("HTTP/1.0 200 OK",
+ "Test: lemur",
+ "Hello")])
+
+
+ def test_firstWriteHTTP11Chunked(self):
+ """
+ For an HTTP 1.1 request, L{http.Request.write} sends an HTTP 1.1
+ Response-Line, whatever response headers are set, and uses chunked
+ encoding for the response body.
+ """
+ req = http.Request(DummyChannel(), None)
+ trans = StringTransport()
+
+ req.transport = trans
+
+ req.setResponseCode(200)
+ req.clientproto = "HTTP/1.1"
+ req.responseHeaders.setRawHeaders("test", ["lemur"])
+ req.write('Hello')
+ req.write('World!')
+
+ self.assertResponseEquals(
+ trans.value(),
+ [("HTTP/1.1 200 OK",
+ "Test: lemur",
+ "Transfer-Encoding: chunked",
+ "5\r\nHello\r\n6\r\nWorld!\r\n")])
+
+
+ def test_firstWriteLastModified(self):
+ """
+ For an HTTP 1.0 request for a resource with a known last modified time,
+ L{http.Request.write} sends an HTTP Response-Line, whatever response
+ headers are set, and a last-modified header with that time.
+ """
+ req = http.Request(DummyChannel(), None)
+ trans = StringTransport()
+
+ req.transport = trans
+
+ req.setResponseCode(200)
+ req.clientproto = "HTTP/1.0"
+ req.lastModified = 0
+ req.responseHeaders.setRawHeaders("test", ["lemur"])
+ req.write('Hello')
+
+ self.assertResponseEquals(
+ trans.value(),
+ [("HTTP/1.0 200 OK",
+ "Test: lemur",
+ "Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT",
+ "Hello")])
+
+
+ def test_parseCookies(self):
+ """
+ L{http.Request.parseCookies} extracts cookies from C{requestHeaders}
+ and adds them to C{received_cookies}.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders(
+ "cookie", ['test="lemur"; test2="panda"'])
+ req.parseCookies()
+ self.assertEqual(req.received_cookies, {"test": '"lemur"',
+ "test2": '"panda"'})
+
+
+ def test_parseCookiesMultipleHeaders(self):
+ """
+ L{http.Request.parseCookies} can extract cookies from multiple Cookie
+ headers.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.requestHeaders.setRawHeaders(
+ "cookie", ['test="lemur"', 'test2="panda"'])
+ req.parseCookies()
+ self.assertEqual(req.received_cookies, {"test": '"lemur"',
+ "test2": '"panda"'})
+
+
+ def test_connectionLost(self):
+ """
+ L{http.Request.connectionLost} closes L{Request.content} and drops the
+ reference to the L{HTTPChannel} to assist with garbage collection.
+ """
+ req = http.Request(DummyChannel(), None)
+
+ # Cause Request.content to be created at all.
+ req.gotLength(10)
+
+ # Grab a reference to content in case the Request drops it later on.
+ content = req.content
+
+ # Put some bytes into it
+ req.handleContentChunk("hello")
+
+ # Then something goes wrong and content should get closed.
+ req.connectionLost(Failure(ConnectionLost("Finished")))
+ self.assertTrue(content.closed)
+ self.assertIdentical(req.channel, None)
+
+
+ def test_registerProducerTwiceFails(self):
+ """
+ Calling L{Request.registerProducer} when a producer is already
+ registered raises ValueError.
+ """
+ req = http.Request(DummyChannel(), None)
+ req.registerProducer(DummyProducer(), True)
+ self.assertRaises(
+ ValueError, req.registerProducer, DummyProducer(), True)
+
+
+ def test_registerProducerWhenQueuedPausesPushProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPushProducer when the
+ request is queued pauses the producer.
+ """
+ req = http.Request(DummyChannel(), True)
+ producer = DummyProducer()
+ req.registerProducer(producer, True)
+ self.assertEqual(['pause'], producer.events)
+
+
+ def test_registerProducerWhenQueuedDoesntPausePullProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPullProducer when the
+ request is queued does not pause the producer, because it doesn't make
+ sense to pause a pull producer.
+ """
+ req = http.Request(DummyChannel(), True)
+ producer = DummyProducer()
+ req.registerProducer(producer, False)
+ self.assertEqual([], producer.events)
+
+
+ def test_registerProducerWhenQueuedDoesntRegisterPushProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPushProducer when the
+ request is queued does not register the producer on the request's
+ transport.
+ """
+ self.assertIdentical(
+ None, getattr(http.StringTransport, 'registerProducer', None),
+ "StringTransport cannot implement registerProducer for this test "
+ "to be valid.")
+ req = http.Request(DummyChannel(), True)
+ producer = DummyProducer()
+ req.registerProducer(producer, True)
+ # This is a roundabout assertion: http.StringTransport doesn't
+ # implement registerProducer, so Request.registerProducer can't have
+ # tried to call registerProducer on the transport.
+ self.assertIsInstance(req.transport, http.StringTransport)
+
+
+ def test_registerProducerWhenQueuedDoesntRegisterPullProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPullProducer when the
+ request is queued does not register the producer on the request's
+ transport.
+ """
+ self.assertIdentical(
+ None, getattr(http.StringTransport, 'registerProducer', None),
+ "StringTransport cannot implement registerProducer for this test "
+ "to be valid.")
+ req = http.Request(DummyChannel(), True)
+ producer = DummyProducer()
+ req.registerProducer(producer, False)
+ # This is a roundabout assertion: http.StringTransport doesn't
+ # implement registerProducer, so Request.registerProducer can't have
+ # tried to call registerProducer on the transport.
+ self.assertIsInstance(req.transport, http.StringTransport)
+
+
+ def test_registerProducerWhenNotQueuedRegistersPushProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPushProducer when the
+ request is not queued registers the producer as a push producer on the
+ request's transport.
+ """
+ req = http.Request(DummyChannel(), False)
+ producer = DummyProducer()
+ req.registerProducer(producer, True)
+ self.assertEqual([(producer, True)], req.transport.producers)
+
+
+ def test_registerProducerWhenNotQueuedRegistersPullProducer(self):
+ """
+ Calling L{Request.registerProducer} with an IPullProducer when the
+ request is not queued registers the producer as a pull producer on the
+ request's transport.
+ """
+ req = http.Request(DummyChannel(), False)
+ producer = DummyProducer()
+ req.registerProducer(producer, False)
+ self.assertEqual([(producer, False)], req.transport.producers)
+
+
+ def test_connectionLostNotification(self):
+ """
+ L{Request.connectionLost} triggers all finish notification Deferreds
+ and cleans up per-request state.
+ """
+ d = DummyChannel()
+ request = http.Request(d, True)
+ finished = request.notifyFinish()
+ request.connectionLost(Failure(ConnectionLost("Connection done")))
+ self.assertIdentical(request.channel, None)
+ return self.assertFailure(finished, ConnectionLost)
+
+
+ def test_finishNotification(self):
+ """
+ L{Request.finish} triggers all finish notification Deferreds.
+ """
+ request = http.Request(DummyChannel(), False)
+ finished = request.notifyFinish()
+ # Force the request to have a non-None content attribute. This is
+ # probably a bug in Request.
+ request.gotLength(1)
+ request.finish()
+ return finished
+
+
+ def test_writeAfterFinish(self):
+ """
+ Calling L{Request.write} after L{Request.finish} has been called results
+ in a L{RuntimeError} being raised.
+ """
+ request = http.Request(DummyChannel(), False)
+ finished = request.notifyFinish()
+ # Force the request to have a non-None content attribute. This is
+ # probably a bug in Request.
+ request.gotLength(1)
+ request.write('foobar')
+ request.finish()
+ self.assertRaises(RuntimeError, request.write, 'foobar')
+ return finished
+
+
+ def test_finishAfterConnectionLost(self):
+ """
+ Calling L{Request.finish} after L{Request.connectionLost} has been
+ called results in a L{RuntimeError} being raised.
+ """
+ channel = DummyChannel()
+ transport = channel.transport
+ req = http.Request(channel, False)
+ req.connectionLost(Failure(ConnectionLost("The end.")))
+ self.assertRaises(RuntimeError, req.finish)
+
+
+
+class MultilineHeadersTestCase(unittest.TestCase):
+ """
+ Tests to exercise handling of multiline headers by L{HTTPClient}. RFCs 1945
+ (HTTP 1.0) and 2616 (HTTP 1.1) state that HTTP message header fields can
+ span multiple lines if each extra line is preceded by at least one space or
+ horizontal tab.
+ """
+ def setUp(self):
+ """
+ Initialize variables used to verify that the header-processing functions
+ are getting called.
+ """
+ self.handleHeaderCalled = False
+ self.handleEndHeadersCalled = False
+
+ # Dictionary of sample complete HTTP header key/value pairs, including
+ # multiline headers.
+ expectedHeaders = {'Content-Length': '10',
+ 'X-Multiline' : 'line-0\tline-1',
+ 'X-Multiline2' : 'line-2 line-3'}
+
+ def ourHandleHeader(self, key, val):
+ """
+ Dummy implementation of L{HTTPClient.handleHeader}.
+ """
+ self.handleHeaderCalled = True
+ self.assertEqual(val, self.expectedHeaders[key])
+
+
+ def ourHandleEndHeaders(self):
+ """
+ Dummy implementation of L{HTTPClient.handleEndHeaders}.
+ """
+ self.handleEndHeadersCalled = True
+
+
+ def test_extractHeader(self):
+ """
+ A header isn't processed by L{HTTPClient.extractHeader} until it is
+ confirmed in L{HTTPClient.lineReceived} that the header has been
+ received completely.
+ """
+ c = ClientDriver()
+ c.handleHeader = self.ourHandleHeader
+ c.handleEndHeaders = self.ourHandleEndHeaders
+
+ c.lineReceived('HTTP/1.0 201')
+ c.lineReceived('Content-Length: 10')
+ self.assertIdentical(c.length, None)
+ self.assertFalse(self.handleHeaderCalled)
+ self.assertFalse(self.handleEndHeadersCalled)
+
+ # Signal end of headers.
+ c.lineReceived('')
+ self.assertTrue(self.handleHeaderCalled)
+ self.assertTrue(self.handleEndHeadersCalled)
+
+ self.assertEqual(c.length, 10)
+
+
+ def test_noHeaders(self):
+ """
+ An HTTP request with no headers will not cause any calls to
+ L{handleHeader} but will cause L{handleEndHeaders} to be called on
+ L{HTTPClient} subclasses.
+ """
+ c = ClientDriver()
+ c.handleHeader = self.ourHandleHeader
+ c.handleEndHeaders = self.ourHandleEndHeaders
+ c.lineReceived('HTTP/1.0 201')
+
+ # Signal end of headers.
+ c.lineReceived('')
+ self.assertFalse(self.handleHeaderCalled)
+ self.assertTrue(self.handleEndHeadersCalled)
+
+ self.assertEqual(c.version, 'HTTP/1.0')
+ self.assertEqual(c.status, '201')
+
+
+ def test_multilineHeaders(self):
+ """
+ L{HTTPClient} parses multiline headers by buffering header lines until
+ an empty line or a line that does not start with whitespace hits
+ lineReceived, confirming that the header has been received completely.
+ """
+ c = ClientDriver()
+ c.handleHeader = self.ourHandleHeader
+ c.handleEndHeaders = self.ourHandleEndHeaders
+
+ c.lineReceived('HTTP/1.0 201')
+ c.lineReceived('X-Multiline: line-0')
+ self.assertFalse(self.handleHeaderCalled)
+ # Start continuing line with a tab.
+ c.lineReceived('\tline-1')
+ c.lineReceived('X-Multiline2: line-2')
+ # The previous header must be complete, so now it can be processed.
+ self.assertTrue(self.handleHeaderCalled)
+ # Start continuing line with a space.
+ c.lineReceived(' line-3')
+ c.lineReceived('Content-Length: 10')
+
+ # Signal end of headers.
+ c.lineReceived('')
+ self.assertTrue(self.handleEndHeadersCalled)
+
+ self.assertEqual(c.version, 'HTTP/1.0')
+ self.assertEqual(c.status, '201')
+ self.assertEqual(c.length, 10)
+
+
+
+class Expect100ContinueServerTests(unittest.TestCase):
+ """
+ Test that the HTTP server handles 'Expect: 100-continue' header correctly.
+
+ The tests in this class all assume a simplistic behavior where user code
+ cannot choose to deny a request. Once ticket #288 is implemented and user
+ code can run before the body of a POST is processed this should be
+ extended to support overriding this behavior.
+ """
+
+ def test_HTTP10(self):
+ """
+ HTTP/1.0 requests do not get 100-continue returned, even if 'Expect:
+ 100-continue' is included (RFC 2616 10.1.1).
+ """
+ transport = StringTransport()
+ channel = http.HTTPChannel()
+ channel.requestFactory = DummyHTTPHandler
+ channel.makeConnection(transport)
+ channel.dataReceived("GET / HTTP/1.0\r\n")
+ channel.dataReceived("Host: www.example.com\r\n")
+ channel.dataReceived("Content-Length: 3\r\n")
+ channel.dataReceived("Expect: 100-continue\r\n")
+ channel.dataReceived("\r\n")
+ self.assertEqual(transport.value(), "")
+ channel.dataReceived("abc")
+ self.assertEqual(transport.value(),
+ "HTTP/1.0 200 OK\r\n"
+ "Command: GET\r\n"
+ "Content-Length: 13\r\n"
+ "Version: HTTP/1.0\r\n"
+ "Request: /\r\n\r\n'''\n3\nabc'''\n")
+
+
+ def test_expect100ContinueHeader(self):
+ """
+ If a HTTP/1.1 client sends a 'Expect: 100-continue' header, the server
+ responds with a 100 response code before handling the request body, if
+ any. The normal resource rendering code will then be called, which
+ will send an additional response code.
+ """
+ transport = StringTransport()
+ channel = http.HTTPChannel()
+ channel.requestFactory = DummyHTTPHandler
+ channel.makeConnection(transport)
+ channel.dataReceived("GET / HTTP/1.1\r\n")
+ channel.dataReceived("Host: www.example.com\r\n")
+ channel.dataReceived("Expect: 100-continue\r\n")
+ channel.dataReceived("Content-Length: 3\r\n")
+ # The 100 continue response is not sent until all headers are
+ # received:
+ self.assertEqual(transport.value(), "")
+ channel.dataReceived("\r\n")
+ # The 100 continue response is sent *before* the body is even
+ # received:
+ self.assertEqual(transport.value(), "HTTP/1.1 100 Continue\r\n\r\n")
+ channel.dataReceived("abc")
+ self.assertEqual(transport.value(),
+ "HTTP/1.1 100 Continue\r\n\r\n"
+ "HTTP/1.1 200 OK\r\n"
+ "Command: GET\r\n"
+ "Content-Length: 13\r\n"
+ "Version: HTTP/1.1\r\n"
+ "Request: /\r\n\r\n'''\n3\nabc'''\n")
+
+
+ def test_expect100ContinueWithPipelining(self):
+ """
+ If a HTTP/1.1 client sends a 'Expect: 100-continue' header, followed
+ by another pipelined request, the 100 response does not interfere with
+ the response to the second request.
+ """
+ transport = StringTransport()
+ channel = http.HTTPChannel()
+ channel.requestFactory = DummyHTTPHandler
+ channel.makeConnection(transport)
+ channel.dataReceived(
+ "GET / HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Expect: 100-continue\r\n"
+ "Content-Length: 3\r\n"
+ "\r\nabc"
+ "POST /foo HTTP/1.1\r\n"
+ "Host: www.example.com\r\n"
+ "Content-Length: 4\r\n"
+ "\r\ndefg")
+ self.assertEqual(transport.value(),
+ "HTTP/1.1 100 Continue\r\n\r\n"
+ "HTTP/1.1 200 OK\r\n"
+ "Command: GET\r\n"
+ "Content-Length: 13\r\n"
+ "Version: HTTP/1.1\r\n"
+ "Request: /\r\n\r\n"
+ "'''\n3\nabc'''\n"
+ "HTTP/1.1 200 OK\r\n"
+ "Command: POST\r\n"
+ "Content-Length: 14\r\n"
+ "Version: HTTP/1.1\r\n"
+ "Request: /foo\r\n\r\n"
+ "'''\n4\ndefg'''\n")
diff --git a/twisted/web/test/test_http_headers.py b/twisted/web/test/test_http_headers.py
new file mode 100644
index 0000000..7ca1bc8
--- /dev/null
+++ b/twisted/web/test/test_http_headers.py
@@ -0,0 +1,616 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.http_headers}.
+"""
+
+import sys
+
+from twisted.python.compat import set
+from twisted.trial.unittest import TestCase
+from twisted.web.http_headers import _DictHeaders, Headers
+
+
+class HeadersTests(TestCase):
+ """
+ Tests for L{Headers}.
+ """
+ def test_initializer(self):
+ """
+ The header values passed to L{Headers.__init__} can be retrieved via
+ L{Headers.getRawHeaders}.
+ """
+ h = Headers({'Foo': ['bar']})
+ self.assertEqual(h.getRawHeaders('foo'), ['bar'])
+
+
+ def test_setRawHeaders(self):
+ """
+ L{Headers.setRawHeaders} sets the header values for the given
+ header name to the sequence of string values.
+ """
+ rawValue = ["value1", "value2"]
+ h = Headers()
+ h.setRawHeaders("test", rawValue)
+ self.assertTrue(h.hasHeader("test"))
+ self.assertTrue(h.hasHeader("Test"))
+ self.assertEqual(h.getRawHeaders("test"), rawValue)
+
+
+ def test_rawHeadersTypeChecking(self):
+ """
+ L{Headers.setRawHeaders} requires values to be of type list.
+ """
+ h = Headers()
+ self.assertRaises(TypeError, h.setRawHeaders, {'Foo': 'bar'})
+
+
+ def test_addRawHeader(self):
+ """
+ L{Headers.addRawHeader} adds a new value for a given header.
+ """
+ h = Headers()
+ h.addRawHeader("test", "lemur")
+ self.assertEqual(h.getRawHeaders("test"), ["lemur"])
+ h.addRawHeader("test", "panda")
+ self.assertEqual(h.getRawHeaders("test"), ["lemur", "panda"])
+
+
+ def test_getRawHeadersNoDefault(self):
+ """
+ L{Headers.getRawHeaders} returns C{None} if the header is not found and
+ no default is specified.
+ """
+ self.assertIdentical(Headers().getRawHeaders("test"), None)
+
+
+ def test_getRawHeadersDefaultValue(self):
+ """
+ L{Headers.getRawHeaders} returns the specified default value when no
+ header is found.
+ """
+ h = Headers()
+ default = object()
+ self.assertIdentical(h.getRawHeaders("test", default), default)
+
+
+ def test_getRawHeaders(self):
+ """
+ L{Headers.getRawHeaders} returns the values which have been set for a
+ given header.
+ """
+ h = Headers()
+ h.setRawHeaders("test", ["lemur"])
+ self.assertEqual(h.getRawHeaders("test"), ["lemur"])
+ self.assertEqual(h.getRawHeaders("Test"), ["lemur"])
+
+
+ def test_hasHeaderTrue(self):
+ """
+ Check that L{Headers.hasHeader} returns C{True} when the given header
+ is found.
+ """
+ h = Headers()
+ h.setRawHeaders("test", ["lemur"])
+ self.assertTrue(h.hasHeader("test"))
+ self.assertTrue(h.hasHeader("Test"))
+
+
+ def test_hasHeaderFalse(self):
+ """
+ L{Headers.hasHeader} returns C{False} when the given header is not
+ found.
+ """
+ self.assertFalse(Headers().hasHeader("test"))
+
+
+ def test_removeHeader(self):
+ """
+ Check that L{Headers.removeHeader} removes the given header.
+ """
+ h = Headers()
+
+ h.setRawHeaders("foo", ["lemur"])
+ self.assertTrue(h.hasHeader("foo"))
+ h.removeHeader("foo")
+ self.assertFalse(h.hasHeader("foo"))
+
+ h.setRawHeaders("bar", ["panda"])
+ self.assertTrue(h.hasHeader("bar"))
+ h.removeHeader("Bar")
+ self.assertFalse(h.hasHeader("bar"))
+
+
+ def test_removeHeaderDoesntExist(self):
+ """
+ L{Headers.removeHeader} is a no-operation when the specified header is
+ not found.
+ """
+ h = Headers()
+ h.removeHeader("test")
+ self.assertEqual(list(h.getAllRawHeaders()), [])
+
+
+ def test_canonicalNameCaps(self):
+ """
+ L{Headers._canonicalNameCaps} returns the canonical capitalization for
+ the given header.
+ """
+ h = Headers()
+ self.assertEqual(h._canonicalNameCaps("test"), "Test")
+ self.assertEqual(h._canonicalNameCaps("test-stuff"), "Test-Stuff")
+ self.assertEqual(h._canonicalNameCaps("content-md5"), "Content-MD5")
+ self.assertEqual(h._canonicalNameCaps("dnt"), "DNT")
+ self.assertEqual(h._canonicalNameCaps("etag"), "ETag")
+ self.assertEqual(h._canonicalNameCaps("p3p"), "P3P")
+ self.assertEqual(h._canonicalNameCaps("te"), "TE")
+ self.assertEqual(h._canonicalNameCaps("www-authenticate"),
+ "WWW-Authenticate")
+ self.assertEqual(h._canonicalNameCaps("x-xss-protection"),
+ "X-XSS-Protection")
+
+
+ def test_getAllRawHeaders(self):
+ """
+ L{Headers.getAllRawHeaders} returns an iterable of (k, v) pairs, where
+ C{k} is the canonicalized representation of the header name, and C{v}
+ is a sequence of values.
+ """
+ h = Headers()
+ h.setRawHeaders("test", ["lemurs"])
+ h.setRawHeaders("www-authenticate", ["basic aksljdlk="])
+
+ allHeaders = set([(k, tuple(v)) for k, v in h.getAllRawHeaders()])
+
+ self.assertEqual(allHeaders,
+ set([("WWW-Authenticate", ("basic aksljdlk=",)),
+ ("Test", ("lemurs",))]))
+
+
+ def test_headersComparison(self):
+ """
+ A L{Headers} instance compares equal to itself and to another
+ L{Headers} instance with the same values.
+ """
+ first = Headers()
+ first.setRawHeaders("foo", ["panda"])
+ second = Headers()
+ second.setRawHeaders("foo", ["panda"])
+ third = Headers()
+ third.setRawHeaders("foo", ["lemur", "panda"])
+ self.assertEqual(first, first)
+ self.assertEqual(first, second)
+ self.assertNotEqual(first, third)
+
+
+ def test_otherComparison(self):
+ """
+ An instance of L{Headers} does not compare equal to other unrelated
+ objects.
+ """
+ h = Headers()
+ self.assertNotEqual(h, ())
+ self.assertNotEqual(h, object())
+ self.assertNotEqual(h, "foo")
+
+
+ def test_repr(self):
+ """
+ The L{repr} of a L{Headers} instance shows the names and values of all
+ the headers it contains.
+ """
+ self.assertEqual(
+ repr(Headers({"foo": ["bar", "baz"]})),
+ "Headers({'foo': ['bar', 'baz']})")
+
+
+ def test_subclassRepr(self):
+ """
+ The L{repr} of an instance of a subclass of L{Headers} uses the name
+ of the subclass instead of the string C{"Headers"}.
+ """
+ class FunnyHeaders(Headers):
+ pass
+ self.assertEqual(
+ repr(FunnyHeaders({"foo": ["bar", "baz"]})),
+ "FunnyHeaders({'foo': ['bar', 'baz']})")
+
+
+ def test_copy(self):
+ """
+ L{Headers.copy} creates a new independant copy of an existing
+ L{Headers} instance, allowing future modifications without impacts
+ between the copies.
+ """
+ h = Headers()
+ h.setRawHeaders('test', ['foo'])
+ i = h.copy()
+ self.assertEqual(i.getRawHeaders('test'), ['foo'])
+ h.addRawHeader('test', 'bar')
+ self.assertEqual(i.getRawHeaders('test'), ['foo'])
+ i.addRawHeader('test', 'baz')
+ self.assertEqual(h.getRawHeaders('test'), ['foo', 'bar'])
+
+
+
+class HeaderDictTests(TestCase):
+ """
+ Tests for the backwards compatible C{dict} interface for L{Headers}
+ provided by L{_DictHeaders}.
+ """
+ def headers(self, **kw):
+ """
+ Create a L{Headers} instance populated with the header name/values
+ specified by C{kw} and a L{_DictHeaders} wrapped around it and return
+ them both.
+ """
+ h = Headers()
+ for k, v in kw.iteritems():
+ h.setRawHeaders(k, v)
+ return h, _DictHeaders(h)
+
+
+ def test_getItem(self):
+ """
+ L{_DictHeaders.__getitem__} returns a single header for the given name.
+ """
+ headers, wrapper = self.headers(test=["lemur"])
+ self.assertEqual(wrapper["test"], "lemur")
+
+
+ def test_getItemMultiple(self):
+ """
+ L{_DictHeaders.__getitem__} returns only the last header value for a
+ given name.
+ """
+ headers, wrapper = self.headers(test=["lemur", "panda"])
+ self.assertEqual(wrapper["test"], "panda")
+
+
+ def test_getItemMissing(self):
+ """
+ L{_DictHeaders.__getitem__} raises L{KeyError} if called with a header
+ which is not present.
+ """
+ headers, wrapper = self.headers()
+ exc = self.assertRaises(KeyError, wrapper.__getitem__, "test")
+ self.assertEqual(exc.args, ("test",))
+
+
+ def test_iteration(self):
+ """
+ L{_DictHeaders.__iter__} returns an iterator the elements of which
+ are the lowercase name of each header present.
+ """
+ headers, wrapper = self.headers(foo=["lemur", "panda"], bar=["baz"])
+ self.assertEqual(set(list(wrapper)), set(["foo", "bar"]))
+
+
+ def test_length(self):
+ """
+ L{_DictHeaders.__len__} returns the number of headers present.
+ """
+ headers, wrapper = self.headers()
+ self.assertEqual(len(wrapper), 0)
+ headers.setRawHeaders("foo", ["bar"])
+ self.assertEqual(len(wrapper), 1)
+ headers.setRawHeaders("test", ["lemur", "panda"])
+ self.assertEqual(len(wrapper), 2)
+
+
+ def test_setItem(self):
+ """
+ L{_DictHeaders.__setitem__} sets a single header value for the given
+ name.
+ """
+ headers, wrapper = self.headers()
+ wrapper["test"] = "lemur"
+ self.assertEqual(headers.getRawHeaders("test"), ["lemur"])
+
+
+ def test_setItemOverwrites(self):
+ """
+ L{_DictHeaders.__setitem__} will replace any previous header values for
+ the given name.
+ """
+ headers, wrapper = self.headers(test=["lemur", "panda"])
+ wrapper["test"] = "lemur"
+ self.assertEqual(headers.getRawHeaders("test"), ["lemur"])
+
+
+ def test_delItem(self):
+ """
+ L{_DictHeaders.__delitem__} will remove the header values for the given
+ name.
+ """
+ headers, wrapper = self.headers(test=["lemur"])
+ del wrapper["test"]
+ self.assertFalse(headers.hasHeader("test"))
+
+
+ def test_delItemMissing(self):
+ """
+ L{_DictHeaders.__delitem__} will raise L{KeyError} if the given name is
+ not present.
+ """
+ headers, wrapper = self.headers()
+ exc = self.assertRaises(KeyError, wrapper.__delitem__, "test")
+ self.assertEqual(exc.args, ("test",))
+
+
+ def test_keys(self, _method='keys', _requireList=True):
+ """
+ L{_DictHeaders.keys} will return a list of all present header names.
+ """
+ headers, wrapper = self.headers(test=["lemur"], foo=["bar"])
+ keys = getattr(wrapper, _method)()
+ if _requireList:
+ self.assertIsInstance(keys, list)
+ self.assertEqual(set(keys), set(["foo", "test"]))
+
+
+ def test_iterkeys(self):
+ """
+ L{_DictHeaders.iterkeys} will return all present header names.
+ """
+ self.test_keys('iterkeys', False)
+
+
+ def test_values(self, _method='values', _requireList=True):
+ """
+ L{_DictHeaders.values} will return a list of all present header values,
+ returning only the last value for headers with more than one.
+ """
+ headers, wrapper = self.headers(foo=["lemur"], bar=["marmot", "panda"])
+ values = getattr(wrapper, _method)()
+ if _requireList:
+ self.assertIsInstance(values, list)
+ self.assertEqual(set(values), set(["lemur", "panda"]))
+
+
+ def test_itervalues(self):
+ """
+ L{_DictHeaders.itervalues} will return all present header values,
+ returning only the last value for headers with more than one.
+ """
+ self.test_values('itervalues', False)
+
+
+ def test_items(self, _method='items', _requireList=True):
+ """
+ L{_DictHeaders.items} will return a list of all present header names
+ and values as tuples, returning only the last value for headers with
+ more than one.
+ """
+ headers, wrapper = self.headers(foo=["lemur"], bar=["marmot", "panda"])
+ items = getattr(wrapper, _method)()
+ if _requireList:
+ self.assertIsInstance(items, list)
+ self.assertEqual(set(items), set([("foo", "lemur"), ("bar", "panda")]))
+
+
+ def test_iteritems(self):
+ """
+ L{_DictHeaders.iteritems} will return all present header names and
+ values as tuples, returning only the last value for headers with more
+ than one.
+ """
+ self.test_items('iteritems', False)
+
+
+ def test_clear(self):
+ """
+ L{_DictHeaders.clear} will remove all headers.
+ """
+ headers, wrapper = self.headers(foo=["lemur"], bar=["panda"])
+ wrapper.clear()
+ self.assertEqual(list(headers.getAllRawHeaders()), [])
+
+
+ def test_copy(self):
+ """
+ L{_DictHeaders.copy} will return a C{dict} with all the same headers
+ and the last value for each.
+ """
+ headers, wrapper = self.headers(foo=["lemur", "panda"], bar=["marmot"])
+ duplicate = wrapper.copy()
+ self.assertEqual(duplicate, {"foo": "panda", "bar": "marmot"})
+
+
+ def test_get(self):
+ """
+ L{_DictHeaders.get} returns the last value for the given header name.
+ """
+ headers, wrapper = self.headers(foo=["lemur", "panda"])
+ self.assertEqual(wrapper.get("foo"), "panda")
+
+
+ def test_getMissing(self):
+ """
+ L{_DictHeaders.get} returns C{None} for a header which is not present.
+ """
+ headers, wrapper = self.headers()
+ self.assertIdentical(wrapper.get("foo"), None)
+
+
+ def test_getDefault(self):
+ """
+ L{_DictHeaders.get} returns the last value for the given header name
+ even when it is invoked with a default value.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ self.assertEqual(wrapper.get("foo", "bar"), "lemur")
+
+
+ def test_getDefaultMissing(self):
+ """
+ L{_DictHeaders.get} returns the default value specified if asked for a
+ header which is not present.
+ """
+ headers, wrapper = self.headers()
+ self.assertEqual(wrapper.get("foo", "bar"), "bar")
+
+
+ def test_has_key(self):
+ """
+ L{_DictHeaders.has_key} returns C{True} if the given header is present,
+ C{False} otherwise.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ self.assertTrue(wrapper.has_key("foo"))
+ self.assertFalse(wrapper.has_key("bar"))
+
+
+ def test_contains(self):
+ """
+ L{_DictHeaders.__contains__} returns C{True} if the given header is
+ present, C{False} otherwise.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ self.assertIn("foo", wrapper)
+ self.assertNotIn("bar", wrapper)
+
+
+ def test_pop(self):
+ """
+ L{_DictHeaders.pop} returns the last header value associated with the
+ given header name and removes the header.
+ """
+ headers, wrapper = self.headers(foo=["lemur", "panda"])
+ self.assertEqual(wrapper.pop("foo"), "panda")
+ self.assertIdentical(headers.getRawHeaders("foo"), None)
+
+
+ def test_popMissing(self):
+ """
+ L{_DictHeaders.pop} raises L{KeyError} if passed a header name which is
+ not present.
+ """
+ headers, wrapper = self.headers()
+ self.assertRaises(KeyError, wrapper.pop, "foo")
+
+
+ def test_popDefault(self):
+ """
+ L{_DictHeaders.pop} returns the last header value associated with the
+ given header name and removes the header, even if it is supplied with a
+ default value.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ self.assertEqual(wrapper.pop("foo", "bar"), "lemur")
+ self.assertIdentical(headers.getRawHeaders("foo"), None)
+
+
+ def test_popDefaultMissing(self):
+ """
+ L{_DictHeaders.pop} returns the default value is asked for a header
+ name which is not present.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ self.assertEqual(wrapper.pop("bar", "baz"), "baz")
+ self.assertEqual(headers.getRawHeaders("foo"), ["lemur"])
+
+
+ def test_popitem(self):
+ """
+ L{_DictHeaders.popitem} returns some header name/value pair.
+ """
+ headers, wrapper = self.headers(foo=["lemur", "panda"])
+ self.assertEqual(wrapper.popitem(), ("foo", "panda"))
+ self.assertIdentical(headers.getRawHeaders("foo"), None)
+
+
+ def test_popitemEmpty(self):
+ """
+ L{_DictHeaders.popitem} raises L{KeyError} if there are no headers
+ present.
+ """
+ headers, wrapper = self.headers()
+ self.assertRaises(KeyError, wrapper.popitem)
+
+
+ def test_update(self):
+ """
+ L{_DictHeaders.update} adds the header/value pairs in the C{dict} it is
+ passed, overriding any existing values for those headers.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ wrapper.update({"foo": "panda", "bar": "marmot"})
+ self.assertEqual(headers.getRawHeaders("foo"), ["panda"])
+ self.assertEqual(headers.getRawHeaders("bar"), ["marmot"])
+
+
+ def test_updateWithKeywords(self):
+ """
+ L{_DictHeaders.update} adds header names given as keyword arguments
+ with the keyword values as the header value.
+ """
+ headers, wrapper = self.headers(foo=["lemur"])
+ wrapper.update(foo="panda", bar="marmot")
+ self.assertEqual(headers.getRawHeaders("foo"), ["panda"])
+ self.assertEqual(headers.getRawHeaders("bar"), ["marmot"])
+
+ if sys.version_info < (2, 4):
+ test_updateWithKeywords.skip = (
+ "Python 2.3 does not support keyword arguments to dict.update.")
+
+
+ def test_setdefaultMissing(self):
+ """
+ If passed the name of a header which is not present,
+ L{_DictHeaders.setdefault} sets the value of the given header to the
+ specified default value and returns it.
+ """
+ headers, wrapper = self.headers(foo=["bar"])
+ self.assertEqual(wrapper.setdefault("baz", "quux"), "quux")
+ self.assertEqual(headers.getRawHeaders("foo"), ["bar"])
+ self.assertEqual(headers.getRawHeaders("baz"), ["quux"])
+
+
+ def test_setdefaultPresent(self):
+ """
+ If passed the name of a header which is present,
+ L{_DictHeaders.setdefault} makes no changes to the headers and
+ returns the last value already associated with that header.
+ """
+ headers, wrapper = self.headers(foo=["bar", "baz"])
+ self.assertEqual(wrapper.setdefault("foo", "quux"), "baz")
+ self.assertEqual(headers.getRawHeaders("foo"), ["bar", "baz"])
+
+
+ def test_setdefaultDefault(self):
+ """
+ If a value is not passed to L{_DictHeaders.setdefault}, C{None} is
+ used.
+ """
+ # This results in an invalid state for the headers, but maybe some
+ # application is doing this an intermediate step towards some other
+ # state. Anyway, it was broken with the old implementation so it's
+ # broken with the new implementation. Compatibility, for the win.
+ # -exarkun
+ headers, wrapper = self.headers()
+ self.assertIdentical(wrapper.setdefault("foo"), None)
+ self.assertEqual(headers.getRawHeaders("foo"), [None])
+
+
+ def test_dictComparison(self):
+ """
+ An instance of L{_DictHeaders} compares equal to a C{dict} which
+ contains the same header/value pairs. For header names with multiple
+ values, the last value only is considered.
+ """
+ headers, wrapper = self.headers(foo=["lemur"], bar=["panda", "marmot"])
+ self.assertNotEqual(wrapper, {"foo": "lemur", "bar": "panda"})
+ self.assertEqual(wrapper, {"foo": "lemur", "bar": "marmot"})
+
+
+ def test_otherComparison(self):
+ """
+ An instance of L{_DictHeaders} does not compare equal to other
+ unrelated objects.
+ """
+ headers, wrapper = self.headers()
+ self.assertNotEqual(wrapper, ())
+ self.assertNotEqual(wrapper, object())
+ self.assertNotEqual(wrapper, "foo")
diff --git a/twisted/web/test/test_httpauth.py b/twisted/web/test/test_httpauth.py
new file mode 100644
index 0000000..1764b0f
--- /dev/null
+++ b/twisted/web/test/test_httpauth.py
@@ -0,0 +1,634 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web._auth}.
+"""
+
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+
+from twisted.python.failure import Failure
+from twisted.internet.error import ConnectionDone
+from twisted.internet.address import IPv4Address
+
+from twisted.cred import error, portal
+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
+from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess
+from twisted.cred.credentials import IUsernamePassword
+
+from twisted.web.iweb import ICredentialFactory
+from twisted.web.resource import IResource, Resource, getChildForRequest
+from twisted.web._auth import basic, digest
+from twisted.web._auth.wrapper import HTTPAuthSessionWrapper, UnauthorizedResource
+from twisted.web._auth.basic import BasicCredentialFactory
+
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.static import Data
+
+from twisted.web.test.test_web import DummyRequest
+
+
+def b64encode(s):
+ return s.encode('base64').strip()
+
+
+class BasicAuthTestsMixin:
+ """
+ L{TestCase} mixin class which defines a number of tests for
+ L{basic.BasicCredentialFactory}. Because this mixin defines C{setUp}, it
+ must be inherited before L{TestCase}.
+ """
+ def setUp(self):
+ self.request = self.makeRequest()
+ self.realm = 'foo'
+ self.username = 'dreid'
+ self.password = 'S3CuR1Ty'
+ self.credentialFactory = basic.BasicCredentialFactory(self.realm)
+
+
+ def makeRequest(self, method='GET', clientAddress=None):
+ """
+ Create a request object to be passed to
+ L{basic.BasicCredentialFactory.decode} along with a response value.
+ Override this in a subclass.
+ """
+ raise NotImplementedError("%r did not implement makeRequest" % (
+ self.__class__,))
+
+
+ def test_interface(self):
+ """
+ L{BasicCredentialFactory} implements L{ICredentialFactory}.
+ """
+ self.assertTrue(
+ verifyObject(ICredentialFactory, self.credentialFactory))
+
+
+ def test_usernamePassword(self):
+ """
+ L{basic.BasicCredentialFactory.decode} turns a base64-encoded response
+ into a L{UsernamePassword} object with a password which reflects the
+ one which was encoded in the response.
+ """
+ response = b64encode('%s:%s' % (self.username, self.password))
+
+ creds = self.credentialFactory.decode(response, self.request)
+ self.assertTrue(IUsernamePassword.providedBy(creds))
+ self.assertTrue(creds.checkPassword(self.password))
+ self.assertFalse(creds.checkPassword(self.password + 'wrong'))
+
+
+ def test_incorrectPadding(self):
+ """
+ L{basic.BasicCredentialFactory.decode} decodes a base64-encoded
+ response with incorrect padding.
+ """
+ response = b64encode('%s:%s' % (self.username, self.password))
+ response = response.strip('=')
+
+ creds = self.credentialFactory.decode(response, self.request)
+ self.assertTrue(verifyObject(IUsernamePassword, creds))
+ self.assertTrue(creds.checkPassword(self.password))
+
+
+ def test_invalidEncoding(self):
+ """
+ L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} if passed
+ a response which is not base64-encoded.
+ """
+ response = 'x' # one byte cannot be valid base64 text
+ self.assertRaises(
+ error.LoginFailed,
+ self.credentialFactory.decode, response, self.makeRequest())
+
+
+ def test_invalidCredentials(self):
+ """
+ L{basic.BasicCredentialFactory.decode} raises L{LoginFailed} when
+ passed a response which is not valid base64-encoded text.
+ """
+ response = b64encode('123abc+/')
+ self.assertRaises(
+ error.LoginFailed,
+ self.credentialFactory.decode,
+ response, self.makeRequest())
+
+
+class RequestMixin:
+ def makeRequest(self, method='GET', clientAddress=None):
+ """
+ Create a L{DummyRequest} (change me to create a
+ L{twisted.web.http.Request} instead).
+ """
+ request = DummyRequest('/')
+ request.method = method
+ request.client = clientAddress
+ return request
+
+
+
+class BasicAuthTestCase(RequestMixin, BasicAuthTestsMixin, unittest.TestCase):
+ """
+ Basic authentication tests which use L{twisted.web.http.Request}.
+ """
+
+
+
+class DigestAuthTestCase(RequestMixin, unittest.TestCase):
+ """
+ Digest authentication tests which use L{twisted.web.http.Request}.
+ """
+
+ def setUp(self):
+ """
+ Create a DigestCredentialFactory for testing
+ """
+ self.realm = "test realm"
+ self.algorithm = "md5"
+ self.credentialFactory = digest.DigestCredentialFactory(
+ self.algorithm, self.realm)
+ self.request = self.makeRequest()
+
+
+ def test_decode(self):
+ """
+ L{digest.DigestCredentialFactory.decode} calls the C{decode} method on
+ L{twisted.cred.digest.DigestCredentialFactory} with the HTTP method and
+ host of the request.
+ """
+ host = '169.254.0.1'
+ method = 'GET'
+ done = [False]
+ response = object()
+ def check(_response, _method, _host):
+ self.assertEqual(response, _response)
+ self.assertEqual(method, _method)
+ self.assertEqual(host, _host)
+ done[0] = True
+
+ self.patch(self.credentialFactory.digest, 'decode', check)
+ req = self.makeRequest(method, IPv4Address('TCP', host, 81))
+ self.credentialFactory.decode(response, req)
+ self.assertTrue(done[0])
+
+
+ def test_interface(self):
+ """
+ L{DigestCredentialFactory} implements L{ICredentialFactory}.
+ """
+ self.assertTrue(
+ verifyObject(ICredentialFactory, self.credentialFactory))
+
+
+ def test_getChallenge(self):
+ """
+ The challenge issued by L{DigestCredentialFactory.getChallenge} must
+ include C{'qop'}, C{'realm'}, C{'algorithm'}, C{'nonce'}, and
+ C{'opaque'} keys. The values for the C{'realm'} and C{'algorithm'}
+ keys must match the values supplied to the factory's initializer.
+ None of the values may have newlines in them.
+ """
+ challenge = self.credentialFactory.getChallenge(self.request)
+ self.assertEqual(challenge['qop'], 'auth')
+ self.assertEqual(challenge['realm'], 'test realm')
+ self.assertEqual(challenge['algorithm'], 'md5')
+ self.assertIn('nonce', challenge)
+ self.assertIn('opaque', challenge)
+ for v in challenge.values():
+ self.assertNotIn('\n', v)
+
+
+ def test_getChallengeWithoutClientIP(self):
+ """
+ L{DigestCredentialFactory.getChallenge} can issue a challenge even if
+ the L{Request} it is passed returns C{None} from C{getClientIP}.
+ """
+ request = self.makeRequest('GET', None)
+ challenge = self.credentialFactory.getChallenge(request)
+ self.assertEqual(challenge['qop'], 'auth')
+ self.assertEqual(challenge['realm'], 'test realm')
+ self.assertEqual(challenge['algorithm'], 'md5')
+ self.assertIn('nonce', challenge)
+ self.assertIn('opaque', challenge)
+
+
+
+class UnauthorizedResourceTests(unittest.TestCase):
+ """
+ Tests for L{UnauthorizedResource}.
+ """
+ def test_getChildWithDefault(self):
+ """
+ An L{UnauthorizedResource} is every child of itself.
+ """
+ resource = UnauthorizedResource([])
+ self.assertIdentical(
+ resource.getChildWithDefault("foo", None), resource)
+ self.assertIdentical(
+ resource.getChildWithDefault("bar", None), resource)
+
+
+ def _unauthorizedRenderTest(self, request):
+ """
+ Render L{UnauthorizedResource} for the given request object and verify
+ that the response code is I{Unauthorized} and that a I{WWW-Authenticate}
+ header is set in the response containing a challenge.
+ """
+ resource = UnauthorizedResource([
+ BasicCredentialFactory('example.com')])
+ request.render(resource)
+ self.assertEqual(request.responseCode, 401)
+ self.assertEqual(
+ request.responseHeaders.getRawHeaders('www-authenticate'),
+ ['basic realm="example.com"'])
+
+
+ def test_render(self):
+ """
+ L{UnauthorizedResource} renders with a 401 response code and a
+ I{WWW-Authenticate} header and puts a simple unauthorized message
+ into the response body.
+ """
+ request = DummyRequest([''])
+ self._unauthorizedRenderTest(request)
+ self.assertEqual('Unauthorized', ''.join(request.written))
+
+
+ def test_renderHEAD(self):
+ """
+ The rendering behavior of L{UnauthorizedResource} for a I{HEAD} request
+ is like its handling of a I{GET} request, but no response body is
+ written.
+ """
+ request = DummyRequest([''])
+ request.method = 'HEAD'
+ self._unauthorizedRenderTest(request)
+ self.assertEqual('', ''.join(request.written))
+
+
+ def test_renderQuotesRealm(self):
+ """
+ The realm value included in the I{WWW-Authenticate} header set in
+ the response when L{UnauthorizedResounrce} is rendered has quotes
+ and backslashes escaped.
+ """
+ resource = UnauthorizedResource([
+ BasicCredentialFactory('example\\"foo')])
+ request = DummyRequest([''])
+ request.render(resource)
+ self.assertEqual(
+ request.responseHeaders.getRawHeaders('www-authenticate'),
+ ['basic realm="example\\\\\\"foo"'])
+
+
+
+class Realm(object):
+ """
+ A simple L{IRealm} implementation which gives out L{WebAvatar} for any
+ avatarId.
+
+ @type loggedIn: C{int}
+ @ivar loggedIn: The number of times C{requestAvatar} has been invoked for
+ L{IResource}.
+
+ @type loggedOut: C{int}
+ @ivar loggedOut: The number of times the logout callback has been invoked.
+ """
+ implements(portal.IRealm)
+
+ def __init__(self, avatarFactory):
+ self.loggedOut = 0
+ self.loggedIn = 0
+ self.avatarFactory = avatarFactory
+
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IResource in interfaces:
+ self.loggedIn += 1
+ return IResource, self.avatarFactory(avatarId), self.logout
+ raise NotImplementedError()
+
+
+ def logout(self):
+ self.loggedOut += 1
+
+
+
+class HTTPAuthHeaderTests(unittest.TestCase):
+ """
+ Tests for L{HTTPAuthSessionWrapper}.
+ """
+ makeRequest = DummyRequest
+
+ def setUp(self):
+ """
+ Create a realm, portal, and L{HTTPAuthSessionWrapper} to use in the tests.
+ """
+ self.username = 'foo bar'
+ self.password = 'bar baz'
+ self.avatarContent = "contents of the avatar resource itself"
+ self.childName = "foo-child"
+ self.childContent = "contents of the foo child of the avatar"
+ self.checker = InMemoryUsernamePasswordDatabaseDontUse()
+ self.checker.addUser(self.username, self.password)
+ self.avatar = Data(self.avatarContent, 'text/plain')
+ self.avatar.putChild(
+ self.childName, Data(self.childContent, 'text/plain'))
+ self.avatars = {self.username: self.avatar}
+ self.realm = Realm(self.avatars.get)
+ self.portal = portal.Portal(self.realm, [self.checker])
+ self.credentialFactories = []
+ self.wrapper = HTTPAuthSessionWrapper(
+ self.portal, self.credentialFactories)
+
+
+ def _authorizedBasicLogin(self, request):
+ """
+ Add an I{basic authorization} header to the given request and then
+ dispatch it, starting from C{self.wrapper} and returning the resulting
+ L{IResource}.
+ """
+ authorization = b64encode(self.username + ':' + self.password)
+ request.headers['authorization'] = 'Basic ' + authorization
+ return getChildForRequest(self.wrapper, request)
+
+
+ def test_getChildWithDefault(self):
+ """
+ Resource traversal which encounters an L{HTTPAuthSessionWrapper}
+ results in an L{UnauthorizedResource} instance when the request does
+ not have the required I{Authorization} headers.
+ """
+ request = self.makeRequest([self.childName])
+ child = getChildForRequest(self.wrapper, request)
+ d = request.notifyFinish()
+ def cbFinished(result):
+ self.assertEqual(request.responseCode, 401)
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
+
+
+ def _invalidAuthorizationTest(self, response):
+ """
+ Create a request with the given value as the value of an
+ I{Authorization} header and perform resource traversal with it,
+ starting at C{self.wrapper}. Assert that the result is a 401 response
+ code. Return a L{Deferred} which fires when this is all done.
+ """
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ request = self.makeRequest([self.childName])
+ request.headers['authorization'] = response
+ child = getChildForRequest(self.wrapper, request)
+ d = request.notifyFinish()
+ def cbFinished(result):
+ self.assertEqual(request.responseCode, 401)
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
+
+
+ def test_getChildWithDefaultUnauthorizedUser(self):
+ """
+ Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
+ results in an L{UnauthorizedResource} when the request has an
+ I{Authorization} header with a user which does not exist.
+ """
+ return self._invalidAuthorizationTest('Basic ' + b64encode('foo:bar'))
+
+
+ def test_getChildWithDefaultUnauthorizedPassword(self):
+ """
+ Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
+ results in an L{UnauthorizedResource} when the request has an
+ I{Authorization} header with a user which exists and the wrong
+ password.
+ """
+ return self._invalidAuthorizationTest(
+ 'Basic ' + b64encode(self.username + ':bar'))
+
+
+ def test_getChildWithDefaultUnrecognizedScheme(self):
+ """
+ Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
+ results in an L{UnauthorizedResource} when the request has an
+ I{Authorization} header with an unrecognized scheme.
+ """
+ return self._invalidAuthorizationTest('Quux foo bar baz')
+
+
+ def test_getChildWithDefaultAuthorized(self):
+ """
+ Resource traversal which encounters an L{HTTPAuthSessionWrapper}
+ results in an L{IResource} which renders the L{IResource} avatar
+ retrieved from the portal when the request has a valid I{Authorization}
+ header.
+ """
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ request = self.makeRequest([self.childName])
+ child = self._authorizedBasicLogin(request)
+ d = request.notifyFinish()
+ def cbFinished(ignored):
+ self.assertEqual(request.written, [self.childContent])
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
+
+
+ def test_renderAuthorized(self):
+ """
+ Resource traversal which terminates at an L{HTTPAuthSessionWrapper}
+ and includes correct authentication headers results in the
+ L{IResource} avatar (not one of its children) retrieved from the
+ portal being rendered.
+ """
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ # Request it exactly, not any of its children.
+ request = self.makeRequest([])
+ child = self._authorizedBasicLogin(request)
+ d = request.notifyFinish()
+ def cbFinished(ignored):
+ self.assertEqual(request.written, [self.avatarContent])
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
+
+
+ def test_getChallengeCalledWithRequest(self):
+ """
+ When L{HTTPAuthSessionWrapper} finds an L{ICredentialFactory} to issue
+ a challenge, it calls the C{getChallenge} method with the request as an
+ argument.
+ """
+ class DumbCredentialFactory(object):
+ implements(ICredentialFactory)
+ scheme = 'dumb'
+
+ def __init__(self):
+ self.requests = []
+
+ def getChallenge(self, request):
+ self.requests.append(request)
+ return {}
+
+ factory = DumbCredentialFactory()
+ self.credentialFactories.append(factory)
+ request = self.makeRequest([self.childName])
+ child = getChildForRequest(self.wrapper, request)
+ d = request.notifyFinish()
+ def cbFinished(ignored):
+ self.assertEqual(factory.requests, [request])
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
+
+
+ def _logoutTest(self):
+ """
+ Issue a request for an authentication-protected resource using valid
+ credentials and then return the C{DummyRequest} instance which was
+ used.
+
+ This is a helper for tests about the behavior of the logout
+ callback.
+ """
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+
+ class SlowerResource(Resource):
+ def render(self, request):
+ return NOT_DONE_YET
+
+ self.avatar.putChild(self.childName, SlowerResource())
+ request = self.makeRequest([self.childName])
+ child = self._authorizedBasicLogin(request)
+ request.render(child)
+ self.assertEqual(self.realm.loggedOut, 0)
+ return request
+
+
+ def test_logout(self):
+ """
+ The realm's logout callback is invoked after the resource is rendered.
+ """
+ request = self._logoutTest()
+ request.finish()
+ self.assertEqual(self.realm.loggedOut, 1)
+
+
+ def test_logoutOnError(self):
+ """
+ The realm's logout callback is also invoked if there is an error
+ generating the response (for example, if the client disconnects
+ early).
+ """
+ request = self._logoutTest()
+ request.processingFailed(
+ Failure(ConnectionDone("Simulated disconnect")))
+ self.assertEqual(self.realm.loggedOut, 1)
+
+
+ def test_decodeRaises(self):
+ """
+ Resource traversal which enouncters an L{HTTPAuthSessionWrapper}
+ results in an L{UnauthorizedResource} when the request has a I{Basic
+ Authorization} header which cannot be decoded using base64.
+ """
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ request = self.makeRequest([self.childName])
+ request.headers['authorization'] = 'Basic decode should fail'
+ child = getChildForRequest(self.wrapper, request)
+ self.assertIsInstance(child, UnauthorizedResource)
+
+
+ def test_selectParseResponse(self):
+ """
+ L{HTTPAuthSessionWrapper._selectParseHeader} returns a two-tuple giving
+ the L{ICredentialFactory} to use to parse the header and a string
+ containing the portion of the header which remains to be parsed.
+ """
+ basicAuthorization = 'Basic abcdef123456'
+ self.assertEqual(
+ self.wrapper._selectParseHeader(basicAuthorization),
+ (None, None))
+ factory = BasicCredentialFactory('example.com')
+ self.credentialFactories.append(factory)
+ self.assertEqual(
+ self.wrapper._selectParseHeader(basicAuthorization),
+ (factory, 'abcdef123456'))
+
+
+ def test_unexpectedDecodeError(self):
+ """
+ Any unexpected exception raised by the credential factory's C{decode}
+ method results in a 500 response code and causes the exception to be
+ logged.
+ """
+ class UnexpectedException(Exception):
+ pass
+
+ class BadFactory(object):
+ scheme = 'bad'
+
+ def getChallenge(self, client):
+ return {}
+
+ def decode(self, response, request):
+ raise UnexpectedException()
+
+ self.credentialFactories.append(BadFactory())
+ request = self.makeRequest([self.childName])
+ request.headers['authorization'] = 'Bad abc'
+ child = getChildForRequest(self.wrapper, request)
+ request.render(child)
+ self.assertEqual(request.responseCode, 500)
+ self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
+
+
+ def test_unexpectedLoginError(self):
+ """
+ Any unexpected failure from L{Portal.login} results in a 500 response
+ code and causes the failure to be logged.
+ """
+ class UnexpectedException(Exception):
+ pass
+
+ class BrokenChecker(object):
+ credentialInterfaces = (IUsernamePassword,)
+
+ def requestAvatarId(self, credentials):
+ raise UnexpectedException()
+
+ self.portal.registerChecker(BrokenChecker())
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ request = self.makeRequest([self.childName])
+ child = self._authorizedBasicLogin(request)
+ request.render(child)
+ self.assertEqual(request.responseCode, 500)
+ self.assertEqual(len(self.flushLoggedErrors(UnexpectedException)), 1)
+
+
+ def test_anonymousAccess(self):
+ """
+ Anonymous requests are allowed if a L{Portal} has an anonymous checker
+ registered.
+ """
+ unprotectedContents = "contents of the unprotected child resource"
+
+ self.avatars[ANONYMOUS] = Resource()
+ self.avatars[ANONYMOUS].putChild(
+ self.childName, Data(unprotectedContents, 'text/plain'))
+ self.portal.registerChecker(AllowAnonymousAccess())
+
+ self.credentialFactories.append(BasicCredentialFactory('example.com'))
+ request = self.makeRequest([self.childName])
+ child = getChildForRequest(self.wrapper, request)
+ d = request.notifyFinish()
+ def cbFinished(ignored):
+ self.assertEqual(request.written, [unprotectedContents])
+ d.addCallback(cbFinished)
+ request.render(child)
+ return d
diff --git a/twisted/web/test/test_newclient.py b/twisted/web/test/test_newclient.py
new file mode 100644
index 0000000..516d0aa
--- /dev/null
+++ b/twisted/web/test/test_newclient.py
@@ -0,0 +1,2521 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web._newclient}.
+"""
+
+__metaclass__ = type
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.internet.interfaces import IConsumer, IPushProducer
+from twisted.internet.error import ConnectionDone, ConnectionLost
+from twisted.internet.defer import Deferred, succeed, fail
+from twisted.internet.protocol import Protocol
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransport, AccumulatingProtocol
+from twisted.web._newclient import UNKNOWN_LENGTH, STATUS, HEADER, BODY, DONE
+from twisted.web._newclient import Request, Response, HTTPParser, HTTPClientParser
+from twisted.web._newclient import BadResponseVersion, ParseError, HTTP11ClientProtocol
+from twisted.web._newclient import ChunkedEncoder, RequestGenerationFailed
+from twisted.web._newclient import RequestTransmissionFailed, ResponseFailed
+from twisted.web._newclient import WrongBodyLength, RequestNotSent
+from twisted.web._newclient import ConnectionAborted, ResponseNeverReceived
+from twisted.web._newclient import BadHeaders, ResponseDone, PotentialDataLoss, ExcessWrite
+from twisted.web._newclient import TransportProxyProducer, LengthEnforcingConsumer, makeStatefulDispatcher
+from twisted.web.http_headers import Headers
+from twisted.web.http import _DataLoss
+from twisted.web.iweb import IBodyProducer, IResponse
+
+
+
+class ArbitraryException(Exception):
+ """
+ A unique, arbitrary exception type which L{twisted.web._newclient} knows
+ nothing about.
+ """
+
+
+class AnotherArbitraryException(Exception):
+ """
+ Similar to L{ArbitraryException} but with a different identity.
+ """
+
+
+# A re-usable Headers instance for tests which don't really care what headers
+# they're sending.
+_boringHeaders = Headers({'host': ['example.com']})
+
+
+def assertWrapperExceptionTypes(self, deferred, mainType, reasonTypes):
+ """
+ Assert that the given L{Deferred} fails with the exception given by
+ C{mainType} and that the exceptions wrapped by the instance of C{mainType}
+ it fails with match the list of exception types given by C{reasonTypes}.
+
+ This is a helper for testing failures of exceptions which subclass
+ L{_newclient._WrapperException}.
+
+ @param self: A L{TestCase} instance which will be used to make the
+ assertions.
+
+ @param deferred: The L{Deferred} which is expected to fail with
+ C{mainType}.
+
+ @param mainType: A L{_newclient._WrapperException} subclass which will be
+ trapped on C{deferred}.
+
+ @param reasonTypes: A sequence of exception types which will be trapped on
+ the resulting L{mainType} exception instance's C{reasons} sequence.
+
+ @return: A L{Deferred} which fires with the C{mainType} instance
+ C{deferred} fails with, or which fails somehow.
+ """
+ def cbFailed(err):
+ for reason, type in zip(err.reasons, reasonTypes):
+ reason.trap(type)
+ self.assertEqual(len(err.reasons), len(reasonTypes),
+ "len(%s) != len(%s)" % (err.reasons, reasonTypes))
+ return err
+ d = self.assertFailure(deferred, mainType)
+ d.addCallback(cbFailed)
+ return d
+
+
+
+def assertResponseFailed(self, deferred, reasonTypes):
+ """
+ A simple helper to invoke L{assertWrapperExceptionTypes} with a C{mainType}
+ of L{ResponseFailed}.
+ """
+ return assertWrapperExceptionTypes(self, deferred, ResponseFailed, reasonTypes)
+
+
+
+def assertRequestGenerationFailed(self, deferred, reasonTypes):
+ """
+ A simple helper to invoke L{assertWrapperExceptionTypes} with a C{mainType}
+ of L{RequestGenerationFailed}.
+ """
+ return assertWrapperExceptionTypes(self, deferred, RequestGenerationFailed, reasonTypes)
+
+
+
+def assertRequestTransmissionFailed(self, deferred, reasonTypes):
+ """
+ A simple helper to invoke L{assertWrapperExceptionTypes} with a C{mainType}
+ of L{RequestTransmissionFailed}.
+ """
+ return assertWrapperExceptionTypes(self, deferred, RequestTransmissionFailed, reasonTypes)
+
+
+
+def justTransportResponse(transport):
+ """
+ Helper function for creating a Response which uses the given transport.
+ All of the other parameters to L{Response.__init__} are filled with
+ arbitrary values. Only use this method if you don't care about any of
+ them.
+ """
+ return Response(('HTTP', 1, 1), 200, 'OK', _boringHeaders, transport)
+
+
+class MakeStatefulDispatcherTests(TestCase):
+ """
+ Tests for L{makeStatefulDispatcher}.
+ """
+ def test_functionCalledByState(self):
+ """
+ A method defined with L{makeStatefulDispatcher} invokes a second
+ method based on the current state of the object.
+ """
+ class Foo:
+ _state = 'A'
+
+ def bar(self):
+ pass
+ bar = makeStatefulDispatcher('quux', bar)
+
+ def _quux_A(self):
+ return 'a'
+
+ def _quux_B(self):
+ return 'b'
+
+ stateful = Foo()
+ self.assertEqual(stateful.bar(), 'a')
+ stateful._state = 'B'
+ self.assertEqual(stateful.bar(), 'b')
+ stateful._state = 'C'
+ self.assertRaises(RuntimeError, stateful.bar)
+
+
+
+class _HTTPParserTests(object):
+ """
+ Base test class for L{HTTPParser} which is responsible for the bulk of
+ the task of parsing HTTP bytes.
+ """
+ sep = None
+
+ def test_statusCallback(self):
+ """
+ L{HTTPParser} calls its C{statusReceived} method when it receives a
+ status line.
+ """
+ status = []
+ protocol = HTTPParser()
+ protocol.statusReceived = status.append
+ protocol.makeConnection(StringTransport())
+ self.assertEqual(protocol.state, STATUS)
+ protocol.dataReceived('HTTP/1.1 200 OK' + self.sep)
+ self.assertEqual(status, ['HTTP/1.1 200 OK'])
+ self.assertEqual(protocol.state, HEADER)
+
+
+ def _headerTestSetup(self):
+ header = {}
+ protocol = HTTPParser()
+ protocol.headerReceived = header.__setitem__
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK' + self.sep)
+ return header, protocol
+
+
+ def test_headerCallback(self):
+ """
+ L{HTTPParser} calls its C{headerReceived} method when it receives a
+ header.
+ """
+ header, protocol = self._headerTestSetup()
+ protocol.dataReceived('X-Foo:bar' + self.sep)
+ # Cannot tell it's not a continue header until the next line arrives
+ # and is not a continuation
+ protocol.dataReceived(self.sep)
+ self.assertEqual(header, {'X-Foo': 'bar'})
+ self.assertEqual(protocol.state, BODY)
+
+
+ def test_continuedHeaderCallback(self):
+ """
+ If a header is split over multiple lines, L{HTTPParser} calls
+ C{headerReceived} with the entire value once it is received.
+ """
+ header, protocol = self._headerTestSetup()
+ protocol.dataReceived('X-Foo: bar' + self.sep)
+ protocol.dataReceived(' baz' + self.sep)
+ protocol.dataReceived('\tquux' + self.sep)
+ protocol.dataReceived(self.sep)
+ self.assertEqual(header, {'X-Foo': 'bar baz\tquux'})
+ self.assertEqual(protocol.state, BODY)
+
+
+ def test_fieldContentWhitespace(self):
+ """
+ Leading and trailing linear whitespace is stripped from the header
+ value passed to the C{headerReceived} callback.
+ """
+ header, protocol = self._headerTestSetup()
+ value = ' \t %(sep)s bar \t%(sep)s \t%(sep)s' % dict(sep=self.sep)
+ protocol.dataReceived('X-Bar:' + value)
+ protocol.dataReceived('X-Foo:' + value)
+ protocol.dataReceived(self.sep)
+ self.assertEqual(header, {'X-Foo': 'bar',
+ 'X-Bar': 'bar'})
+
+
+ def test_allHeadersCallback(self):
+ """
+ After the last header is received, L{HTTPParser} calls
+ C{allHeadersReceived}.
+ """
+ called = []
+ header, protocol = self._headerTestSetup()
+ def allHeadersReceived():
+ called.append(protocol.state)
+ protocol.state = STATUS
+ protocol.allHeadersReceived = allHeadersReceived
+ protocol.dataReceived(self.sep)
+ self.assertEqual(called, [HEADER])
+ self.assertEqual(protocol.state, STATUS)
+
+
+ def test_noHeaderCallback(self):
+ """
+ If there are no headers in the message, L{HTTPParser} does not call
+ C{headerReceived}.
+ """
+ header, protocol = self._headerTestSetup()
+ protocol.dataReceived(self.sep)
+ self.assertEqual(header, {})
+ self.assertEqual(protocol.state, BODY)
+
+
+ def test_headersSavedOnResponse(self):
+ """
+ All headers received by L{HTTPParser} are added to
+ L{HTTPParser.headers}.
+ """
+ protocol = HTTPParser()
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK' + self.sep)
+ protocol.dataReceived('X-Foo: bar' + self.sep)
+ protocol.dataReceived('X-Foo: baz' + self.sep)
+ protocol.dataReceived(self.sep)
+ expected = [('X-Foo', ['bar', 'baz'])]
+ self.assertEqual(expected, list(protocol.headers.getAllRawHeaders()))
+
+
+ def test_connectionControlHeaders(self):
+ """
+ L{HTTPParser.isConnectionControlHeader} returns C{True} for headers
+ which are always connection control headers (similar to "hop-by-hop"
+ headers from RFC 2616 section 13.5.1) and C{False} for other headers.
+ """
+ protocol = HTTPParser()
+ connHeaderNames = [
+ 'content-length', 'connection', 'keep-alive', 'te', 'trailers',
+ 'transfer-encoding', 'upgrade', 'proxy-connection']
+
+ for header in connHeaderNames:
+ self.assertTrue(
+ protocol.isConnectionControlHeader(header),
+ "Expecting %r to be a connection control header, but "
+ "wasn't" % (header,))
+ self.assertFalse(
+ protocol.isConnectionControlHeader("date"),
+ "Expecting the arbitrarily selected 'date' header to not be "
+ "a connection control header, but was.")
+
+
+ def test_switchToBodyMode(self):
+ """
+ L{HTTPParser.switchToBodyMode} raises L{RuntimeError} if called more
+ than once.
+ """
+ protocol = HTTPParser()
+ protocol.makeConnection(StringTransport())
+ protocol.switchToBodyMode(object())
+ self.assertRaises(RuntimeError, protocol.switchToBodyMode, object())
+
+
+
+class HTTPParserTestsRFCComplaintDelimeter(_HTTPParserTests, TestCase):
+ """
+ L{_HTTPParserTests} using standard CR LF newlines.
+ """
+ sep = '\r\n'
+
+
+
+class HTTPParserTestsNonRFCComplaintDelimeter(_HTTPParserTests, TestCase):
+ """
+ L{_HTTPParserTests} using bare LF newlines.
+ """
+ sep = '\n'
+
+
+
+class HTTPClientParserTests(TestCase):
+ """
+ Tests for L{HTTPClientParser} which is responsible for parsing HTTP
+ response messages.
+ """
+ def test_parseVersion(self):
+ """
+ L{HTTPClientParser.parseVersion} parses a status line into its three
+ components.
+ """
+ protocol = HTTPClientParser(None, None)
+ self.assertEqual(
+ protocol.parseVersion('CANDY/7.2'),
+ ('CANDY', 7, 2))
+
+
+ def test_parseBadVersion(self):
+ """
+ L{HTTPClientParser.parseVersion} raises L{ValueError} when passed an
+ unparsable version.
+ """
+ protocol = HTTPClientParser(None, None)
+ e = BadResponseVersion
+ f = protocol.parseVersion
+
+ def checkParsing(s):
+ exc = self.assertRaises(e, f, s)
+ self.assertEqual(exc.data, s)
+
+ checkParsing('foo')
+ checkParsing('foo/bar/baz')
+
+ checkParsing('foo/')
+ checkParsing('foo/..')
+
+ checkParsing('foo/a.b')
+ checkParsing('foo/-1.-1')
+
+
+ def test_responseStatusParsing(self):
+ """
+ L{HTTPClientParser.statusReceived} parses the version, code, and phrase
+ from the status line and stores them on the response object.
+ """
+ request = Request('GET', '/', _boringHeaders, None)
+ protocol = HTTPClientParser(request, None)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ self.assertEqual(protocol.response.version, ('HTTP', 1, 1))
+ self.assertEqual(protocol.response.code, 200)
+ self.assertEqual(protocol.response.phrase, 'OK')
+
+
+ def test_badResponseStatus(self):
+ """
+ L{HTTPClientParser.statusReceived} raises L{ParseError} if it is called
+ with a status line which cannot be parsed.
+ """
+ protocol = HTTPClientParser(None, None)
+
+ def checkParsing(s):
+ exc = self.assertRaises(ParseError, protocol.statusReceived, s)
+ self.assertEqual(exc.data, s)
+
+ # If there are fewer than three whitespace-delimited parts to the
+ # status line, it is not valid and cannot be parsed.
+ checkParsing('foo')
+ checkParsing('HTTP/1.1 200')
+
+ # If the response code is not an integer, the status line is not valid
+ # and cannot be parsed.
+ checkParsing('HTTP/1.1 bar OK')
+
+
+ def _noBodyTest(self, request, response):
+ """
+ Assert that L{HTTPClientParser} parses the given C{response} to
+ C{request}, resulting in a response with no body and no extra bytes and
+ leaving the transport in the producing state.
+
+ @param request: A L{Request} instance which might have caused a server
+ to return the given response.
+ @param response: A string giving the response to be parsed.
+
+ @return: A C{dict} of headers from the response.
+ """
+ header = {}
+ finished = []
+ protocol = HTTPClientParser(request, finished.append)
+ protocol.headerReceived = header.__setitem__
+ body = []
+ protocol._bodyDataReceived = body.append
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.dataReceived(response)
+ self.assertEqual(transport.producerState, 'producing')
+ self.assertEqual(protocol.state, DONE)
+ self.assertEqual(body, [])
+ self.assertEqual(finished, [''])
+ self.assertEqual(protocol.response.length, 0)
+ return header
+
+
+ def test_headResponse(self):
+ """
+ If the response is to a HEAD request, no body is expected, the body
+ callback is not invoked, and the I{Content-Length} header is passed to
+ the header callback.
+ """
+ request = Request('HEAD', '/', _boringHeaders, None)
+ status = (
+ 'HTTP/1.1 200 OK\r\n'
+ 'Content-Length: 10\r\n'
+ '\r\n')
+ header = self._noBodyTest(request, status)
+ self.assertEqual(header, {'Content-Length': '10'})
+
+
+ def test_noContentResponse(self):
+ """
+ If the response code is I{NO CONTENT} (204), no body is expected and
+ the body callback is not invoked.
+ """
+ request = Request('GET', '/', _boringHeaders, None)
+ status = (
+ 'HTTP/1.1 204 NO CONTENT\r\n'
+ '\r\n')
+ self._noBodyTest(request, status)
+
+
+ def test_notModifiedResponse(self):
+ """
+ If the response code is I{NOT MODIFIED} (304), no body is expected and
+ the body callback is not invoked.
+ """
+ request = Request('GET', '/', _boringHeaders, None)
+ status = (
+ 'HTTP/1.1 304 NOT MODIFIED\r\n'
+ '\r\n')
+ self._noBodyTest(request, status)
+
+
+ def test_responseHeaders(self):
+ """
+ The response headers are added to the response object's C{headers}
+ L{Headers} instance.
+ """
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ lambda rest: None)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ protocol.dataReceived('X-Foo: bar\r\n')
+ protocol.dataReceived('\r\n')
+ self.assertEqual(
+ protocol.connHeaders,
+ Headers({}))
+ self.assertEqual(
+ protocol.response.headers,
+ Headers({'x-foo': ['bar']}))
+ self.assertIdentical(protocol.response.length, UNKNOWN_LENGTH)
+
+
+ def test_connectionHeaders(self):
+ """
+ The connection control headers are added to the parser's C{connHeaders}
+ L{Headers} instance.
+ """
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ lambda rest: None)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ protocol.dataReceived('Content-Length: 123\r\n')
+ protocol.dataReceived('Connection: close\r\n')
+ protocol.dataReceived('\r\n')
+ self.assertEqual(
+ protocol.response.headers,
+ Headers({}))
+ self.assertEqual(
+ protocol.connHeaders,
+ Headers({'content-length': ['123'],
+ 'connection': ['close']}))
+ self.assertEqual(protocol.response.length, 123)
+
+
+ def test_headResponseContentLengthEntityHeader(self):
+ """
+ If a HEAD request is made, the I{Content-Length} header in the response
+ is added to the response headers, not the connection control headers.
+ """
+ protocol = HTTPClientParser(
+ Request('HEAD', '/', _boringHeaders, None),
+ lambda rest: None)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ protocol.dataReceived('Content-Length: 123\r\n')
+ protocol.dataReceived('\r\n')
+ self.assertEqual(
+ protocol.response.headers,
+ Headers({'content-length': ['123']}))
+ self.assertEqual(
+ protocol.connHeaders,
+ Headers({}))
+ self.assertEqual(protocol.response.length, 0)
+
+
+ def test_contentLength(self):
+ """
+ If a response includes a body with a length given by the
+ I{Content-Length} header, the bytes which make up the body are passed
+ to the C{_bodyDataReceived} callback on the L{HTTPParser}.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ finished.append)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ body = []
+ protocol.response._bodyDataReceived = body.append
+ protocol.dataReceived('Content-Length: 10\r\n')
+ protocol.dataReceived('\r\n')
+
+ # Incidentally, the transport should be paused now. It is the response
+ # object's responsibility to resume this when it is ready for bytes.
+ self.assertEqual(transport.producerState, 'paused')
+
+ self.assertEqual(protocol.state, BODY)
+ protocol.dataReceived('x' * 6)
+ self.assertEqual(body, ['x' * 6])
+ self.assertEqual(protocol.state, BODY)
+ protocol.dataReceived('y' * 4)
+ self.assertEqual(body, ['x' * 6, 'y' * 4])
+ self.assertEqual(protocol.state, DONE)
+ self.assertTrue(finished, [''])
+
+
+ def test_zeroContentLength(self):
+ """
+ If a response includes a I{Content-Length} header indicating zero bytes
+ in the response, L{Response.length} is set accordingly and no data is
+ delivered to L{Response._bodyDataReceived}.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ finished.append)
+
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+
+ body = []
+ protocol.response._bodyDataReceived = body.append
+
+ protocol.dataReceived('Content-Length: 0\r\n')
+ protocol.dataReceived('\r\n')
+
+ self.assertEqual(protocol.state, DONE)
+ self.assertEqual(body, [])
+ self.assertTrue(finished, [''])
+ self.assertEqual(protocol.response.length, 0)
+
+
+
+ def test_multipleContentLengthHeaders(self):
+ """
+ If a response includes multiple I{Content-Length} headers,
+ L{HTTPClientParser.dataReceived} raises L{ValueError} to indicate that
+ the response is invalid and the transport is now unusable.
+ """
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ None)
+
+ protocol.makeConnection(StringTransport())
+ self.assertRaises(
+ ValueError,
+ protocol.dataReceived,
+ 'HTTP/1.1 200 OK\r\n'
+ 'Content-Length: 1\r\n'
+ 'Content-Length: 2\r\n'
+ '\r\n')
+
+
+ def test_extraBytesPassedBack(self):
+ """
+ If extra bytes are received past the end of a response, they are passed
+ to the finish callback.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ finished.append)
+
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ protocol.dataReceived('Content-Length: 0\r\n')
+ protocol.dataReceived('\r\nHere is another thing!')
+ self.assertEqual(protocol.state, DONE)
+ self.assertEqual(finished, ['Here is another thing!'])
+
+
+ def test_extraBytesPassedBackHEAD(self):
+ """
+ If extra bytes are received past the end of the headers of a response
+ to a HEAD request, they are passed to the finish callback.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('HEAD', '/', _boringHeaders, None),
+ finished.append)
+
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+ protocol.dataReceived('Content-Length: 12\r\n')
+ protocol.dataReceived('\r\nHere is another thing!')
+ self.assertEqual(protocol.state, DONE)
+ self.assertEqual(finished, ['Here is another thing!'])
+
+
+ def test_chunkedResponseBody(self):
+ """
+ If the response headers indicate the response body is encoded with the
+ I{chunked} transfer encoding, the body is decoded according to that
+ transfer encoding before being passed to L{Response._bodyDataReceived}.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None),
+ finished.append)
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+
+ body = []
+ protocol.response._bodyDataReceived = body.append
+
+ protocol.dataReceived('Transfer-Encoding: chunked\r\n')
+ protocol.dataReceived('\r\n')
+
+ # No data delivered yet
+ self.assertEqual(body, [])
+
+ # Cannot predict the length of a chunked encoded response body.
+ self.assertIdentical(protocol.response.length, UNKNOWN_LENGTH)
+
+ # Deliver some chunks and make sure the data arrives
+ protocol.dataReceived('3\r\na')
+ self.assertEqual(body, ['a'])
+ protocol.dataReceived('bc\r\n')
+ self.assertEqual(body, ['a', 'bc'])
+
+ # The response's _bodyDataFinished method should be called when the last
+ # chunk is received. Extra data should be passed to the finished
+ # callback.
+ protocol.dataReceived('0\r\n\r\nextra')
+ self.assertEqual(finished, ['extra'])
+
+
+ def test_unknownContentLength(self):
+ """
+ If a response does not include a I{Transfer-Encoding} or a
+ I{Content-Length}, the end of response body is indicated by the
+ connection being closed.
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None), finished.append)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+
+ body = []
+ protocol.response._bodyDataReceived = body.append
+
+ protocol.dataReceived('\r\n')
+ protocol.dataReceived('foo')
+ protocol.dataReceived('bar')
+ self.assertEqual(body, ['foo', 'bar'])
+ protocol.connectionLost(ConnectionDone("simulated end of connection"))
+ self.assertEqual(finished, [''])
+
+
+ def test_contentLengthAndTransferEncoding(self):
+ """
+ According to RFC 2616, section 4.4, point 3, if I{Content-Length} and
+ I{Transfer-Encoding: chunked} are present, I{Content-Length} MUST be
+ ignored
+ """
+ finished = []
+ protocol = HTTPClientParser(
+ Request('GET', '/', _boringHeaders, None), finished.append)
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.dataReceived('HTTP/1.1 200 OK\r\n')
+
+ body = []
+ protocol.response._bodyDataReceived = body.append
+
+ protocol.dataReceived(
+ 'Content-Length: 102\r\n'
+ 'Transfer-Encoding: chunked\r\n'
+ '\r\n'
+ '3\r\n'
+ 'abc\r\n'
+ '0\r\n'
+ '\r\n')
+
+ self.assertEqual(body, ['abc'])
+ self.assertEqual(finished, [''])
+
+
+ def test_connectionLostBeforeBody(self):
+ """
+ If L{HTTPClientParser.connectionLost} is called before the headers are
+ finished, the C{_responseDeferred} is fired with the L{Failure} passed
+ to C{connectionLost}.
+ """
+ transport = StringTransport()
+ protocol = HTTPClientParser(Request('GET', '/', _boringHeaders, None), None)
+ protocol.makeConnection(transport)
+ # Grab this here because connectionLost gets rid of the attribute
+ responseDeferred = protocol._responseDeferred
+ protocol.connectionLost(Failure(ArbitraryException()))
+
+ return assertResponseFailed(
+ self, responseDeferred, [ArbitraryException])
+
+
+ def test_connectionLostWithError(self):
+ """
+ If one of the L{Response} methods called by
+ L{HTTPClientParser.connectionLost} raises an exception, the exception
+ is logged and not re-raised.
+ """
+ transport = StringTransport()
+ protocol = HTTPClientParser(Request('GET', '/', _boringHeaders, None),
+ None)
+ protocol.makeConnection(transport)
+
+ response = []
+ protocol._responseDeferred.addCallback(response.append)
+ protocol.dataReceived(
+ 'HTTP/1.1 200 OK\r\n'
+ 'Content-Length: 1\r\n'
+ '\r\n')
+ response = response[0]
+
+ # Arrange for an exception
+ def fakeBodyDataFinished(err=None):
+ raise ArbitraryException()
+ response._bodyDataFinished = fakeBodyDataFinished
+
+ protocol.connectionLost(None)
+
+ self.assertEqual(len(self.flushLoggedErrors(ArbitraryException)), 1)
+
+
+ def test_noResponseAtAll(self):
+ """
+ If no response at all was received and the connection is lost, the
+ resulting error is L{ResponseNeverReceived}.
+ """
+ protocol = HTTPClientParser(
+ Request('HEAD', '/', _boringHeaders, None),
+ lambda ign: None)
+ d = protocol._responseDeferred
+
+ protocol.makeConnection(StringTransport())
+ protocol.connectionLost(ConnectionLost())
+ return self.assertFailure(d, ResponseNeverReceived)
+
+
+ def test_someResponseButNotAll(self):
+ """
+ If a partial response was received and the connection is lost, the
+ resulting error is L{ResponseFailed}, but not
+ L{ResponseNeverReceived}.
+ """
+ protocol = HTTPClientParser(
+ Request('HEAD', '/', _boringHeaders, None),
+ lambda ign: None)
+ d = protocol._responseDeferred
+
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived('2')
+ protocol.connectionLost(ConnectionLost())
+ return self.assertFailure(d, ResponseFailed).addCallback(
+ self.assertIsInstance, ResponseFailed)
+
+
+
+class SlowRequest:
+ """
+ L{SlowRequest} is a fake implementation of L{Request} which is easily
+ controlled externally (for example, by code in a test method).
+
+ @ivar stopped: A flag indicating whether C{stopWriting} has been called.
+
+ @ivar finished: After C{writeTo} is called, a L{Deferred} which was
+ returned by that method. L{SlowRequest} will never fire this
+ L{Deferred}.
+ """
+ method = 'GET'
+ stopped = False
+ persistent = False
+
+ def writeTo(self, transport):
+ self.finished = Deferred()
+ return self.finished
+
+
+ def stopWriting(self):
+ self.stopped = True
+
+
+
+class SimpleRequest:
+ """
+ L{SimpleRequest} is a fake implementation of L{Request} which writes a
+ short, fixed string to the transport passed to its C{writeTo} method and
+ returns a succeeded L{Deferred}. This vaguely emulates the behavior of a
+ L{Request} with no body producer.
+ """
+ persistent = False
+
+ def writeTo(self, transport):
+ transport.write('SOME BYTES')
+ return succeed(None)
+
+
+
+class HTTP11ClientProtocolTests(TestCase):
+ """
+ Tests for the HTTP 1.1 client protocol implementation,
+ L{HTTP11ClientProtocol}.
+ """
+ def setUp(self):
+ """
+ Create an L{HTTP11ClientProtocol} connected to a fake transport.
+ """
+ self.transport = StringTransport()
+ self.protocol = HTTP11ClientProtocol()
+ self.protocol.makeConnection(self.transport)
+
+
+ def test_request(self):
+ """
+ L{HTTP11ClientProtocol.request} accepts a L{Request} and calls its
+ C{writeTo} method with its own transport.
+ """
+ self.protocol.request(SimpleRequest())
+ self.assertEqual(self.transport.value(), 'SOME BYTES')
+
+
+ def test_secondRequest(self):
+ """
+ The second time L{HTTP11ClientProtocol.request} is called, it returns a
+ L{Deferred} which immediately fires with a L{Failure} wrapping a
+ L{RequestNotSent} exception.
+ """
+ self.protocol.request(SlowRequest())
+ def cbNotSent(ignored):
+ self.assertEqual(self.transport.value(), '')
+ d = self.assertFailure(
+ self.protocol.request(SimpleRequest()), RequestNotSent)
+ d.addCallback(cbNotSent)
+ return d
+
+
+ def test_requestAfterConnectionLost(self):
+ """
+ L{HTTP11ClientProtocol.request} returns a L{Deferred} which immediately
+ fires with a L{Failure} wrapping a L{RequestNotSent} if called after
+ the protocol has been disconnected.
+ """
+ self.protocol.connectionLost(
+ Failure(ConnectionDone("sad transport")))
+ def cbNotSent(ignored):
+ self.assertEqual(self.transport.value(), '')
+ d = self.assertFailure(
+ self.protocol.request(SimpleRequest()), RequestNotSent)
+ d.addCallback(cbNotSent)
+ return d
+
+
+ def test_failedWriteTo(self):
+ """
+ If the L{Deferred} returned by L{Request.writeTo} fires with a
+ L{Failure}, L{HTTP11ClientProtocol.request} disconnects its transport
+ and returns a L{Deferred} which fires with a L{Failure} of
+ L{RequestGenerationFailed} wrapping the underlying failure.
+ """
+ class BrokenRequest:
+ persistent = False
+ def writeTo(self, transport):
+ return fail(ArbitraryException())
+
+ d = self.protocol.request(BrokenRequest())
+ def cbFailed(ignored):
+ self.assertTrue(self.transport.disconnecting)
+ # Simulate what would happen if the protocol had a real transport
+ # and make sure no exception is raised.
+ self.protocol.connectionLost(
+ Failure(ConnectionDone("you asked for it")))
+ d = assertRequestGenerationFailed(self, d, [ArbitraryException])
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_synchronousWriteToError(self):
+ """
+ If L{Request.writeTo} raises an exception,
+ L{HTTP11ClientProtocol.request} returns a L{Deferred} which fires with
+ a L{Failure} of L{RequestGenerationFailed} wrapping that exception.
+ """
+ class BrokenRequest:
+ persistent = False
+ def writeTo(self, transport):
+ raise ArbitraryException()
+
+ d = self.protocol.request(BrokenRequest())
+ return assertRequestGenerationFailed(self, d, [ArbitraryException])
+
+
+ def test_connectionLostDuringRequestGeneration(self, mode=None):
+ """
+ If L{HTTP11ClientProtocol}'s transport is disconnected before the
+ L{Deferred} returned by L{Request.writeTo} fires, the L{Deferred}
+ returned by L{HTTP11ClientProtocol.request} fires with a L{Failure} of
+ L{RequestTransmissionFailed} wrapping the underlying failure.
+ """
+ request = SlowRequest()
+ d = self.protocol.request(request)
+ d = assertRequestTransmissionFailed(self, d, [ArbitraryException])
+
+ # The connection hasn't been lost yet. The request should still be
+ # allowed to do its thing.
+ self.assertFalse(request.stopped)
+
+ self.protocol.connectionLost(Failure(ArbitraryException()))
+
+ # Now the connection has been lost. The request should have been told
+ # to stop writing itself.
+ self.assertTrue(request.stopped)
+
+ if mode == 'callback':
+ request.finished.callback(None)
+ elif mode == 'errback':
+ request.finished.errback(Failure(AnotherArbitraryException()))
+ errors = self.flushLoggedErrors(AnotherArbitraryException)
+ self.assertEqual(len(errors), 1)
+ else:
+ # Don't fire the writeTo Deferred at all.
+ pass
+ return d
+
+
+ def test_connectionLostBeforeGenerationFinished(self):
+ """
+ If the request passed to L{HTTP11ClientProtocol} finishes generation
+ successfully after the L{HTTP11ClientProtocol}'s connection has been
+ lost, nothing happens.
+ """
+ return self.test_connectionLostDuringRequestGeneration('callback')
+
+
+ def test_connectionLostBeforeGenerationFailed(self):
+ """
+ If the request passed to L{HTTP11ClientProtocol} finished generation
+ with an error after the L{HTTP11ClientProtocol}'s connection has been
+ lost, nothing happens.
+ """
+ return self.test_connectionLostDuringRequestGeneration('errback')
+
+
+ def test_errorMessageOnConnectionLostBeforeGenerationFailedDoesNotConfuse(self):
+ """
+ If the request passed to L{HTTP11ClientProtocol} finished generation
+ with an error after the L{HTTP11ClientProtocol}'s connection has been
+ lost, an error is logged that gives a non-confusing hint to user on what
+ went wrong.
+ """
+ errors = []
+ log.addObserver(errors.append)
+ self.addCleanup(log.removeObserver, errors.append)
+
+ def check(ignore):
+ error = errors[0]
+ self.assertEqual(error['why'],
+ 'Error writing request, but not in valid state '
+ 'to finalize request: CONNECTION_LOST')
+
+ return self.test_connectionLostDuringRequestGeneration(
+ 'errback').addCallback(check)
+
+
+ def test_receiveSimplestResponse(self):
+ """
+ When a response is delivered to L{HTTP11ClientProtocol}, the
+ L{Deferred} previously returned by the C{request} method is called back
+ with a L{Response} instance and the connection is closed.
+ """
+ d = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ def cbRequest(response):
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.headers, Headers())
+ self.assertTrue(self.transport.disconnecting)
+ self.assertEqual(self.protocol.state, 'QUIESCENT')
+ d.addCallback(cbRequest)
+ self.protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n")
+ return d
+
+
+ def test_receiveResponseHeaders(self):
+ """
+ The headers included in a response delivered to L{HTTP11ClientProtocol}
+ are included on the L{Response} instance passed to the callback
+ returned by the C{request} method.
+ """
+ d = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ def cbRequest(response):
+ expected = Headers({'x-foo': ['bar', 'baz']})
+ self.assertEqual(response.headers, expected)
+ d.addCallback(cbRequest)
+ self.protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "X-Foo: bar\r\n"
+ "X-Foo: baz\r\n"
+ "\r\n")
+ return d
+
+
+ def test_receiveResponseBeforeRequestGenerationDone(self):
+ """
+ If response bytes are delivered to L{HTTP11ClientProtocol} before the
+ L{Deferred} returned by L{Request.writeTo} fires, those response bytes
+ are parsed as part of the response.
+
+ The connection is also closed, because we're in a confusing state, and
+ therefore the C{quiescentCallback} isn't called.
+ """
+ quiescentResult = []
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(quiescentResult.append)
+ protocol.makeConnection(transport)
+
+ request = SlowRequest()
+ d = protocol.request(request)
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "X-Foo: bar\r\n"
+ "Content-Length: 6\r\n"
+ "\r\n"
+ "foobar")
+ def cbResponse(response):
+ p = AccumulatingProtocol()
+ whenFinished = p.closedDeferred = Deferred()
+ response.deliverBody(p)
+ self.assertEqual(
+ protocol.state, 'TRANSMITTING_AFTER_RECEIVING_RESPONSE')
+ self.assertTrue(transport.disconnecting)
+ self.assertEqual(quiescentResult, [])
+ return whenFinished.addCallback(
+ lambda ign: (response, p.data))
+ d.addCallback(cbResponse)
+ def cbAllResponse((response, body)):
+ self.assertEqual(response.version, ('HTTP', 1, 1))
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.phrase, 'OK')
+ self.assertEqual(response.headers, Headers({'x-foo': ['bar']}))
+ self.assertEqual(body, "foobar")
+
+ # Also nothing bad should happen if the request does finally
+ # finish, even though it is completely irrelevant.
+ request.finished.callback(None)
+
+ d.addCallback(cbAllResponse)
+ return d
+
+
+ def test_connectionLostAfterReceivingResponseBeforeRequestGenerationDone(self):
+ """
+ If response bytes are delivered to L{HTTP11ClientProtocol} before the
+ request completes, calling L{connectionLost} on the protocol will
+ result in protocol being moved to C{'CONNECTION_LOST'} state.
+ """
+ request = SlowRequest()
+ d = self.protocol.request(request)
+ self.protocol.dataReceived(
+ "HTTP/1.1 400 BAD REQUEST\r\n"
+ "Content-Length: 9\r\n"
+ "\r\n"
+ "tisk tisk")
+ def cbResponse(response):
+ p = AccumulatingProtocol()
+ whenFinished = p.closedDeferred = Deferred()
+ response.deliverBody(p)
+ return whenFinished.addCallback(
+ lambda ign: (response, p.data))
+ d.addCallback(cbResponse)
+ def cbAllResponse(ignore):
+ request.finished.callback(None)
+ # Nothing dire will happen when the connection is lost
+ self.protocol.connectionLost(Failure(ArbitraryException()))
+ self.assertEqual(self.protocol._state, 'CONNECTION_LOST')
+ d.addCallback(cbAllResponse)
+ return d
+
+
+ def test_receiveResponseBody(self):
+ """
+ The C{deliverBody} method of the response object with which the
+ L{Deferred} returned by L{HTTP11ClientProtocol.request} fires can be
+ used to get the body of the response.
+ """
+ protocol = AccumulatingProtocol()
+ whenFinished = protocol.closedDeferred = Deferred()
+ requestDeferred = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+
+ self.protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 6\r\n"
+ "\r")
+
+ # Here's what's going on: all the response headers have been delivered
+ # by this point, so the request Deferred can fire with a Response
+ # object. The body is yet to come, but that's okay, because the
+ # Response object is how you *get* the body.
+ result = []
+ requestDeferred.addCallback(result.append)
+
+ self.assertEqual(result, [])
+ # Deliver the very last byte of the response. It is exactly at this
+ # point which the Deferred returned by request should fire.
+ self.protocol.dataReceived("\n")
+ response = result[0]
+
+ response.deliverBody(protocol)
+
+ self.protocol.dataReceived("foo")
+ self.protocol.dataReceived("bar")
+
+ def cbAllResponse(ignored):
+ self.assertEqual(protocol.data, "foobar")
+ protocol.closedReason.trap(ResponseDone)
+ whenFinished.addCallback(cbAllResponse)
+ return whenFinished
+
+
+ def test_responseBodyFinishedWhenConnectionLostWhenContentLengthIsUnknown(
+ self):
+ """
+ If the length of the response body is unknown, the protocol passed to
+ the response's C{deliverBody} method has its C{connectionLost}
+ method called with a L{Failure} wrapping a L{PotentialDataLoss}
+ exception.
+ """
+ requestDeferred = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ self.protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "\r\n")
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+
+ protocol = AccumulatingProtocol()
+ response.deliverBody(protocol)
+
+ self.protocol.dataReceived("foo")
+ self.protocol.dataReceived("bar")
+
+ self.assertEqual(protocol.data, "foobar")
+ self.protocol.connectionLost(
+ Failure(ConnectionDone("low-level transport disconnected")))
+
+ protocol.closedReason.trap(PotentialDataLoss)
+
+
+ def test_chunkedResponseBodyUnfinishedWhenConnectionLost(self):
+ """
+ If the final chunk has not been received when the connection is lost
+ (for any reason), the protocol passed to C{deliverBody} has its
+ C{connectionLost} method called with a L{Failure} wrapping the
+ exception for that reason.
+ """
+ requestDeferred = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ self.protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n")
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+
+ protocol = AccumulatingProtocol()
+ response.deliverBody(protocol)
+
+ self.protocol.dataReceived("3\r\nfoo\r\n")
+ self.protocol.dataReceived("3\r\nbar\r\n")
+
+ self.assertEqual(protocol.data, "foobar")
+
+ self.protocol.connectionLost(Failure(ArbitraryException()))
+
+ return assertResponseFailed(
+ self, fail(protocol.closedReason), [ArbitraryException, _DataLoss])
+
+
+ def test_parserDataReceivedException(self):
+ """
+ If the parser L{HTTP11ClientProtocol} delivers bytes to in
+ C{dataReceived} raises an exception, the exception is wrapped in a
+ L{Failure} and passed to the parser's C{connectionLost} and then the
+ L{HTTP11ClientProtocol}'s transport is disconnected.
+ """
+ requestDeferred = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ self.protocol.dataReceived('unparseable garbage goes here\r\n')
+ d = assertResponseFailed(self, requestDeferred, [ParseError])
+ def cbFailed(exc):
+ self.assertTrue(self.transport.disconnecting)
+ self.assertEqual(
+ exc.reasons[0].value.data, 'unparseable garbage goes here')
+
+ # Now do what StringTransport doesn't do but a real transport would
+ # have, call connectionLost on the HTTP11ClientProtocol. Nothing
+ # is asserted about this, but it's important for it to not raise an
+ # exception.
+ self.protocol.connectionLost(Failure(ConnectionDone("it is done")))
+
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_proxyStopped(self):
+ """
+ When the HTTP response parser is disconnected, the
+ L{TransportProxyProducer} which was connected to it as a transport is
+ stopped.
+ """
+ requestDeferred = self.protocol.request(Request('GET', '/', _boringHeaders, None))
+ transport = self.protocol._parser.transport
+ self.assertIdentical(transport._producer, self.transport)
+ self.protocol._disconnectParser(Failure(ConnectionDone("connection done")))
+ self.assertIdentical(transport._producer, None)
+ return assertResponseFailed(self, requestDeferred, [ConnectionDone])
+
+
+ def test_abortClosesConnection(self):
+ """
+ L{HTTP11ClientProtocol.abort} will tell the transport to close its
+ connection when it is invoked, and returns a C{Deferred} that fires
+ when the connection is lost.
+ """
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol()
+ protocol.makeConnection(transport)
+ r1 = []
+ r2 = []
+ protocol.abort().addCallback(r1.append)
+ protocol.abort().addCallback(r2.append)
+ self.assertEqual((r1, r2), ([], []))
+ self.assertTrue(transport.disconnecting)
+
+ # Disconnect protocol, the Deferreds will fire:
+ protocol.connectionLost(Failure(ConnectionDone()))
+ self.assertEqual(r1, [None])
+ self.assertEqual(r2, [None])
+
+
+ def test_abortAfterConnectionLost(self):
+ """
+ L{HTTP11ClientProtocol.abort} called after the connection is lost
+ returns a C{Deferred} that fires immediately.
+ """
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol()
+ protocol.makeConnection(transport)
+ protocol.connectionLost(Failure(ConnectionDone()))
+
+ result = []
+ protocol.abort().addCallback(result.append)
+ self.assertEqual(result, [None])
+ self.assertEqual(protocol._state, "CONNECTION_LOST")
+
+
+ def test_abortBeforeResponseBody(self):
+ """
+ The Deferred returned by L{HTTP11ClientProtocol.request} will fire
+ with a L{ResponseFailed} failure containing a L{ConnectionAborted}
+ exception, if the connection was aborted before all response headers
+ have been received.
+ """
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol()
+ protocol.makeConnection(transport)
+ result = protocol.request(Request('GET', '/', _boringHeaders, None))
+ protocol.abort()
+ self.assertTrue(transport.disconnecting)
+ protocol.connectionLost(Failure(ConnectionDone()))
+ return assertResponseFailed(self, result, [ConnectionAborted])
+
+
+ def test_abortAfterResponseHeaders(self):
+ """
+ When the connection is aborted after the response headers have
+ been received and the L{Response} has been made available to
+ application code, the response body protocol's C{connectionLost}
+ method will be invoked with a L{ResponseFailed} failure containing a
+ L{ConnectionAborted} exception.
+ """
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol()
+ protocol.makeConnection(transport)
+ result = protocol.request(Request('GET', '/', _boringHeaders, None))
+
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 1\r\n"
+ "\r\n"
+ )
+
+ testResult = Deferred()
+
+ class BodyDestination(Protocol):
+ """
+ A body response protocol which immediately aborts the HTTP
+ connection.
+ """
+ def connectionMade(self):
+ """
+ Abort the HTTP connection.
+ """
+ protocol.abort()
+
+ def connectionLost(self, reason):
+ """
+ Make the reason for the losing of the connection available to
+ the unit test via C{testResult}.
+ """
+ testResult.errback(reason)
+
+
+ def deliverBody(response):
+ """
+ Connect the L{BodyDestination} response body protocol to the
+ response, and then simulate connection loss after ensuring that
+ the HTTP connection has been aborted.
+ """
+ response.deliverBody(BodyDestination())
+ self.assertTrue(transport.disconnecting)
+ protocol.connectionLost(Failure(ConnectionDone()))
+
+
+ def checkError(error):
+ self.assertIsInstance(error.response, Response)
+
+
+ result.addCallback(deliverBody)
+ deferred = assertResponseFailed(self, testResult,
+ [ConnectionAborted, _DataLoss])
+ return deferred.addCallback(checkError)
+
+
+ def test_quiescentCallbackCalled(self):
+ """
+ If after a response is done the {HTTP11ClientProtocol} stays open and
+ returns to QUIESCENT state, all per-request state is reset and the
+ C{quiescentCallback} is called with the protocol instance.
+
+ This is useful for implementing a persistent connection pool.
+
+ The C{quiescentCallback} is called *before* the response-receiving
+ protocol's C{connectionLost}, so that new requests triggered by end of
+ first request can re-use a persistent connection.
+ """
+ quiescentResult = []
+ def callback(p):
+ self.assertEqual(p, protocol)
+ self.assertEqual(p.state, "QUIESCENT")
+ quiescentResult.append(p)
+
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(callback)
+ protocol.makeConnection(transport)
+
+ requestDeferred = protocol.request(
+ Request('GET', '/', _boringHeaders, None, persistent=True))
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-length: 3\r\n"
+ "\r\n")
+
+ # Headers done, but still no quiescent callback:
+ self.assertEqual(quiescentResult, [])
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+
+ # When response body is done (i.e. connectionLost is called), note the
+ # fact in quiescentResult:
+ bodyProtocol = AccumulatingProtocol()
+ bodyProtocol.closedDeferred = Deferred()
+ bodyProtocol.closedDeferred.addCallback(
+ lambda ign: quiescentResult.append("response done"))
+
+ response.deliverBody(bodyProtocol)
+ protocol.dataReceived("abc")
+ bodyProtocol.closedReason.trap(ResponseDone)
+ # Quiescent callback called *before* protocol handling the response
+ # body gets its connectionLost called:
+ self.assertEqual(quiescentResult, [protocol, "response done"])
+
+ # Make sure everything was cleaned up:
+ self.assertEqual(protocol._parser, None)
+ self.assertEqual(protocol._finishedRequest, None)
+ self.assertEqual(protocol._currentRequest, None)
+ self.assertEqual(protocol._transportProxy, None)
+ self.assertEqual(protocol._responseDeferred, None)
+
+
+ def test_quiescentCallbackCalledEmptyResponse(self):
+ """
+ The quiescentCallback is called before the request C{Deferred} fires,
+ in cases where the response has no body.
+ """
+ quiescentResult = []
+ def callback(p):
+ self.assertEqual(p, protocol)
+ self.assertEqual(p.state, "QUIESCENT")
+ quiescentResult.append(p)
+
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(callback)
+ protocol.makeConnection(transport)
+
+ requestDeferred = protocol.request(
+ Request('GET', '/', _boringHeaders, None, persistent=True))
+ requestDeferred.addCallback(quiescentResult.append)
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-length: 0\r\n"
+ "\r\n")
+
+ self.assertEqual(len(quiescentResult), 2)
+ self.assertIdentical(quiescentResult[0], protocol)
+ self.assertIsInstance(quiescentResult[1], Response)
+
+
+ def test_quiescentCallbackNotCalled(self):
+ """
+ If after a response is done the {HTTP11ClientProtocol} returns a
+ C{Connection: close} header in the response, the C{quiescentCallback}
+ is not called and the connection is lost.
+ """
+ quiescentResult = []
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(quiescentResult.append)
+ protocol.makeConnection(transport)
+
+ requestDeferred = protocol.request(
+ Request('GET', '/', _boringHeaders, None, persistent=True))
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-length: 0\r\n"
+ "Connection: close\r\n"
+ "\r\n")
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+
+ bodyProtocol = AccumulatingProtocol()
+ response.deliverBody(bodyProtocol)
+ bodyProtocol.closedReason.trap(ResponseDone)
+ self.assertEqual(quiescentResult, [])
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_quiescentCallbackNotCalledNonPersistentQuery(self):
+ """
+ If the request was non-persistent (i.e. sent C{Connection: close}),
+ the C{quiescentCallback} is not called and the connection is lost.
+ """
+ quiescentResult = []
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(quiescentResult.append)
+ protocol.makeConnection(transport)
+
+ requestDeferred = protocol.request(
+ Request('GET', '/', _boringHeaders, None, persistent=False))
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-length: 0\r\n"
+ "\r\n")
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+
+ bodyProtocol = AccumulatingProtocol()
+ response.deliverBody(bodyProtocol)
+ bodyProtocol.closedReason.trap(ResponseDone)
+ self.assertEqual(quiescentResult, [])
+ self.assertTrue(transport.disconnecting)
+
+
+ def test_quiescentCallbackThrows(self):
+ """
+ If C{quiescentCallback} throws an exception, the error is logged and
+ protocol is disconnected.
+ """
+ def callback(p):
+ raise ZeroDivisionError()
+
+ transport = StringTransport()
+ protocol = HTTP11ClientProtocol(callback)
+ protocol.makeConnection(transport)
+
+ requestDeferred = protocol.request(
+ Request('GET', '/', _boringHeaders, None, persistent=True))
+ protocol.dataReceived(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-length: 0\r\n"
+ "\r\n")
+
+ result = []
+ requestDeferred.addCallback(result.append)
+ response = result[0]
+ bodyProtocol = AccumulatingProtocol()
+ response.deliverBody(bodyProtocol)
+ bodyProtocol.closedReason.trap(ResponseDone)
+
+ errors = self.flushLoggedErrors(ZeroDivisionError)
+ self.assertEqual(len(errors), 1)
+ self.assertTrue(transport.disconnecting)
+
+
+
+class StringProducer:
+ """
+ L{StringProducer} is a dummy body producer.
+
+ @ivar stopped: A flag which indicates whether or not C{stopProducing} has
+ been called.
+ @ivar consumer: After C{startProducing} is called, the value of the
+ C{consumer} argument to that method.
+ @ivar finished: After C{startProducing} is called, a L{Deferred} which was
+ returned by that method. L{StringProducer} will never fire this
+ L{Deferred}.
+ """
+ implements(IBodyProducer)
+
+ stopped = False
+
+ def __init__(self, length):
+ self.length = length
+
+
+ def startProducing(self, consumer):
+ self.consumer = consumer
+ self.finished = Deferred()
+ return self.finished
+
+
+ def stopProducing(self):
+ self.stopped = True
+
+
+
+class RequestTests(TestCase):
+ """
+ Tests for L{Request}.
+ """
+ def setUp(self):
+ self.transport = StringTransport()
+
+
+ def test_sendSimplestRequest(self):
+ """
+ L{Request.writeTo} formats the request data and writes it to the given
+ transport.
+ """
+ Request('GET', '/', _boringHeaders, None).writeTo(self.transport)
+ self.assertEqual(
+ self.transport.value(),
+ "GET / HTTP/1.1\r\n"
+ "Connection: close\r\n"
+ "Host: example.com\r\n"
+ "\r\n")
+
+
+ def test_sendSimplestPersistentRequest(self):
+ """
+ A pesistent request does not send 'Connection: close' header.
+ """
+ req = Request('GET', '/', _boringHeaders, None, persistent=True)
+ req.writeTo(self.transport)
+ self.assertEqual(
+ self.transport.value(),
+ "GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n")
+
+
+ def test_sendRequestHeaders(self):
+ """
+ L{Request.writeTo} formats header data and writes it to the given
+ transport.
+ """
+ headers = Headers({'x-foo': ['bar', 'baz'], 'host': ['example.com']})
+ Request('GET', '/foo', headers, None).writeTo(self.transport)
+ lines = self.transport.value().split('\r\n')
+ self.assertEqual(lines[0], "GET /foo HTTP/1.1")
+ self.assertEqual(lines[-2:], ["", ""])
+ del lines[0], lines[-2:]
+ lines.sort()
+ self.assertEqual(
+ lines,
+ ["Connection: close",
+ "Host: example.com",
+ "X-Foo: bar",
+ "X-Foo: baz"])
+
+
+ def test_sendChunkedRequestBody(self):
+ """
+ L{Request.writeTo} uses chunked encoding to write data from the request
+ body producer to the given transport. It registers the request body
+ producer with the transport.
+ """
+ producer = StringProducer(UNKNOWN_LENGTH)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ request.writeTo(self.transport)
+
+ self.assertNotIdentical(producer.consumer, None)
+ self.assertIdentical(self.transport.producer, producer)
+ self.assertTrue(self.transport.streaming)
+
+ self.assertEqual(
+ self.transport.value(),
+ "POST /bar HTTP/1.1\r\n"
+ "Connection: close\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Host: example.com\r\n"
+ "\r\n")
+ self.transport.clear()
+
+ producer.consumer.write('x' * 3)
+ producer.consumer.write('y' * 15)
+ producer.finished.callback(None)
+ self.assertIdentical(self.transport.producer, None)
+ self.assertEqual(
+ self.transport.value(),
+ "3\r\n"
+ "xxx\r\n"
+ "f\r\n"
+ "yyyyyyyyyyyyyyy\r\n"
+ "0\r\n"
+ "\r\n")
+
+
+ def test_sendChunkedRequestBodyWithError(self):
+ """
+ If L{Request} is created with a C{bodyProducer} without a known length
+ and the L{Deferred} returned from its C{startProducing} method fires
+ with a L{Failure}, the L{Deferred} returned by L{Request.writeTo} fires
+ with that L{Failure} and the body producer is unregistered from the
+ transport. The final zero-length chunk is not written to the
+ transport.
+ """
+ producer = StringProducer(UNKNOWN_LENGTH)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+ self.transport.clear()
+ producer.finished.errback(ArbitraryException())
+ def cbFailed(ignored):
+ self.assertEqual(self.transport.value(), "")
+ self.assertIdentical(self.transport.producer, None)
+ d = self.assertFailure(writeDeferred, ArbitraryException)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_sendRequestBodyWithLength(self):
+ """
+ If L{Request} is created with a C{bodyProducer} with a known length,
+ that length is sent as the value for the I{Content-Length} header and
+ chunked encoding is not used.
+ """
+ producer = StringProducer(3)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ request.writeTo(self.transport)
+
+ self.assertNotIdentical(producer.consumer, None)
+ self.assertIdentical(self.transport.producer, producer)
+ self.assertTrue(self.transport.streaming)
+
+ self.assertEqual(
+ self.transport.value(),
+ "POST /bar HTTP/1.1\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 3\r\n"
+ "Host: example.com\r\n"
+ "\r\n")
+ self.transport.clear()
+
+ producer.consumer.write('abc')
+ producer.finished.callback(None)
+ self.assertIdentical(self.transport.producer, None)
+ self.assertEqual(self.transport.value(), "abc")
+
+
+ def test_sendRequestBodyWithTooFewBytes(self):
+ """
+ If L{Request} is created with a C{bodyProducer} with a known length and
+ the producer does not produce that many bytes, the L{Deferred} returned
+ by L{Request.writeTo} fires with a L{Failure} wrapping a
+ L{WrongBodyLength} exception.
+ """
+ producer = StringProducer(3)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+ producer.consumer.write('ab')
+ producer.finished.callback(None)
+ self.assertIdentical(self.transport.producer, None)
+ return self.assertFailure(writeDeferred, WrongBodyLength)
+
+
+ def _sendRequestBodyWithTooManyBytesTest(self, finisher):
+ """
+ Verify that when too many bytes have been written by a body producer
+ and then the body producer's C{startProducing} L{Deferred} fires that
+ the producer is unregistered from the transport and that the
+ L{Deferred} returned from L{Request.writeTo} is fired with a L{Failure}
+ wrapping a L{WrongBodyLength}.
+
+ @param finisher: A callable which will be invoked with the body
+ producer after too many bytes have been written to the transport.
+ It should fire the startProducing Deferred somehow.
+ """
+ producer = StringProducer(3)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+
+ producer.consumer.write('ab')
+
+ # The producer hasn't misbehaved yet, so it shouldn't have been
+ # stopped.
+ self.assertFalse(producer.stopped)
+
+ producer.consumer.write('cd')
+
+ # Now the producer *has* misbehaved, so we should have tried to
+ # make it stop.
+ self.assertTrue(producer.stopped)
+
+ # The transport should have had the producer unregistered from it as
+ # well.
+ self.assertIdentical(self.transport.producer, None)
+
+ def cbFailed(exc):
+ # The "cd" should not have been written to the transport because
+ # the request can now locally be recognized to be invalid. If we
+ # had written the extra bytes, the server could have decided to
+ # start processing the request, which would be bad since we're
+ # going to indicate failure locally.
+ self.assertEqual(
+ self.transport.value(),
+ "POST /bar HTTP/1.1\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 3\r\n"
+ "Host: example.com\r\n"
+ "\r\n"
+ "ab")
+ self.transport.clear()
+
+ # Subsequent writes should be ignored, as should firing the
+ # Deferred returned from startProducing.
+ self.assertRaises(ExcessWrite, producer.consumer.write, 'ef')
+
+ # Likewise, if the Deferred returned from startProducing fires,
+ # this should more or less be ignored (aside from possibly logging
+ # an error).
+ finisher(producer)
+
+ # There should have been nothing further written to the transport.
+ self.assertEqual(self.transport.value(), "")
+
+ d = self.assertFailure(writeDeferred, WrongBodyLength)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_sendRequestBodyWithTooManyBytes(self):
+ """
+ If L{Request} is created with a C{bodyProducer} with a known length and
+ the producer tries to produce more than than many bytes, the
+ L{Deferred} returned by L{Request.writeTo} fires with a L{Failure}
+ wrapping a L{WrongBodyLength} exception.
+ """
+ def finisher(producer):
+ producer.finished.callback(None)
+ return self._sendRequestBodyWithTooManyBytesTest(finisher)
+
+
+ def test_sendRequestBodyErrorWithTooManyBytes(self):
+ """
+ If L{Request} is created with a C{bodyProducer} with a known length and
+ the producer tries to produce more than than many bytes, the
+ L{Deferred} returned by L{Request.writeTo} fires with a L{Failure}
+ wrapping a L{WrongBodyLength} exception.
+ """
+ def finisher(producer):
+ producer.finished.errback(ArbitraryException())
+ errors = self.flushLoggedErrors(ArbitraryException)
+ self.assertEqual(len(errors), 1)
+ return self._sendRequestBodyWithTooManyBytesTest(finisher)
+
+
+ def test_sendRequestBodyErrorWithConsumerError(self):
+ """
+ Though there should be no way for the internal C{finishedConsuming}
+ L{Deferred} in L{Request._writeToContentLength} to fire a L{Failure}
+ after the C{finishedProducing} L{Deferred} has fired, in case this does
+ happen, the error should be logged with a message about how there's
+ probably a bug in L{Request}.
+
+ This is a whitebox test.
+ """
+ producer = StringProducer(3)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ request.writeTo(self.transport)
+
+ finishedConsuming = producer.consumer._finished
+
+ producer.consumer.write('abc')
+ producer.finished.callback(None)
+
+ finishedConsuming.errback(ArbitraryException())
+ self.assertEqual(len(self.flushLoggedErrors(ArbitraryException)), 1)
+
+
+ def _sendRequestBodyFinishedEarlyThenTooManyBytes(self, finisher):
+ """
+ Verify that if the body producer fires its Deferred and then keeps
+ writing to the consumer that the extra writes are ignored and the
+ L{Deferred} returned by L{Request.writeTo} fires with a L{Failure}
+ wrapping the most appropriate exception type.
+ """
+ producer = StringProducer(3)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+
+ producer.consumer.write('ab')
+ finisher(producer)
+ self.assertIdentical(self.transport.producer, None)
+ self.transport.clear()
+ self.assertRaises(ExcessWrite, producer.consumer.write, 'cd')
+ self.assertEqual(self.transport.value(), "")
+ return writeDeferred
+
+
+ def test_sendRequestBodyFinishedEarlyThenTooManyBytes(self):
+ """
+ If the request body producer indicates it is done by firing the
+ L{Deferred} returned from its C{startProducing} method but then goes on
+ to write too many bytes, the L{Deferred} returned by {Request.writeTo}
+ fires with a L{Failure} wrapping L{WrongBodyLength}.
+ """
+ def finisher(producer):
+ producer.finished.callback(None)
+ return self.assertFailure(
+ self._sendRequestBodyFinishedEarlyThenTooManyBytes(finisher),
+ WrongBodyLength)
+
+
+ def test_sendRequestBodyErroredEarlyThenTooManyBytes(self):
+ """
+ If the request body producer indicates an error by firing the
+ L{Deferred} returned from its C{startProducing} method but then goes on
+ to write too many bytes, the L{Deferred} returned by {Request.writeTo}
+ fires with that L{Failure} and L{WrongBodyLength} is logged.
+ """
+ def finisher(producer):
+ producer.finished.errback(ArbitraryException())
+ return self.assertFailure(
+ self._sendRequestBodyFinishedEarlyThenTooManyBytes(finisher),
+ ArbitraryException)
+
+
+ def test_sendChunkedRequestBodyFinishedThenWriteMore(self, _with=None):
+ """
+ If the request body producer with an unknown length tries to write
+ after firing the L{Deferred} returned by its C{startProducing} method,
+ the C{write} call raises an exception and does not write anything to
+ the underlying transport.
+ """
+ producer = StringProducer(UNKNOWN_LENGTH)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+ producer.finished.callback(_with)
+ self.transport.clear()
+
+ self.assertRaises(ExcessWrite, producer.consumer.write, 'foo')
+ self.assertEqual(self.transport.value(), "")
+ return writeDeferred
+
+
+ def test_sendChunkedRequestBodyFinishedWithErrorThenWriteMore(self):
+ """
+ If the request body producer with an unknown length tries to write
+ after firing the L{Deferred} returned by its C{startProducing} method
+ with a L{Failure}, the C{write} call raises an exception and does not
+ write anything to the underlying transport.
+ """
+ d = self.test_sendChunkedRequestBodyFinishedThenWriteMore(
+ Failure(ArbitraryException()))
+ return self.assertFailure(d, ArbitraryException)
+
+
+ def test_sendRequestBodyWithError(self):
+ """
+ If the L{Deferred} returned from the C{startProducing} method of the
+ L{IBodyProducer} passed to L{Request} fires with a L{Failure}, the
+ L{Deferred} returned from L{Request.writeTo} fails with that
+ L{Failure}.
+ """
+ producer = StringProducer(5)
+ request = Request('POST', '/bar', _boringHeaders, producer)
+ writeDeferred = request.writeTo(self.transport)
+
+ # Sanity check - the producer should be registered with the underlying
+ # transport.
+ self.assertIdentical(self.transport.producer, producer)
+ self.assertTrue(self.transport.streaming)
+
+ producer.consumer.write('ab')
+ self.assertEqual(
+ self.transport.value(),
+ "POST /bar HTTP/1.1\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 5\r\n"
+ "Host: example.com\r\n"
+ "\r\n"
+ "ab")
+
+ self.assertFalse(self.transport.disconnecting)
+ producer.finished.errback(Failure(ArbitraryException()))
+
+ # Disconnection is handled by a higher level. Request should leave the
+ # transport alone in this case.
+ self.assertFalse(self.transport.disconnecting)
+
+ # Oh. Except it should unregister the producer that it registered.
+ self.assertIdentical(self.transport.producer, None)
+
+ return self.assertFailure(writeDeferred, ArbitraryException)
+
+
+ def test_hostHeaderRequired(self):
+ """
+ L{Request.writeTo} raises L{BadHeaders} if there is not exactly one
+ I{Host} header and writes nothing to the given transport.
+ """
+ request = Request('GET', '/', Headers({}), None)
+ self.assertRaises(BadHeaders, request.writeTo, self.transport)
+ self.assertEqual(self.transport.value(), '')
+
+ request = Request('GET', '/', Headers({'Host': ['example.com', 'example.org']}), None)
+ self.assertRaises(BadHeaders, request.writeTo, self.transport)
+ self.assertEqual(self.transport.value(), '')
+
+
+ def test_stopWriting(self):
+ """
+ L{Request.stopWriting} calls its body producer's C{stopProducing}
+ method.
+ """
+ producer = StringProducer(3)
+ request = Request('GET', '/', _boringHeaders, producer)
+ request.writeTo(self.transport)
+ self.assertFalse(producer.stopped)
+ request.stopWriting()
+ self.assertTrue(producer.stopped)
+
+
+ def test_brokenStopProducing(self):
+ """
+ If the body producer's C{stopProducing} method raises an exception,
+ L{Request.stopWriting} logs it and does not re-raise it.
+ """
+ producer = StringProducer(3)
+ def brokenStopProducing():
+ raise ArbitraryException("stopProducing is busted")
+ producer.stopProducing = brokenStopProducing
+
+ request = Request('GET', '/', _boringHeaders, producer)
+ request.writeTo(self.transport)
+ request.stopWriting()
+ self.assertEqual(
+ len(self.flushLoggedErrors(ArbitraryException)), 1)
+
+
+
+class LengthEnforcingConsumerTests(TestCase):
+ """
+ Tests for L{LengthEnforcingConsumer}.
+ """
+ def setUp(self):
+ self.result = Deferred()
+ self.producer = StringProducer(10)
+ self.transport = StringTransport()
+ self.enforcer = LengthEnforcingConsumer(
+ self.producer, self.transport, self.result)
+
+
+ def test_write(self):
+ """
+ L{LengthEnforcingConsumer.write} calls the wrapped consumer's C{write}
+ method with the bytes it is passed as long as there are fewer of them
+ than the C{length} attribute indicates remain to be received.
+ """
+ self.enforcer.write('abc')
+ self.assertEqual(self.transport.value(), 'abc')
+ self.transport.clear()
+ self.enforcer.write('def')
+ self.assertEqual(self.transport.value(), 'def')
+
+
+ def test_finishedEarly(self):
+ """
+ L{LengthEnforcingConsumer._noMoreWritesExpected} raises
+ L{WrongBodyLength} if it is called before the indicated number of bytes
+ have been written.
+ """
+ self.enforcer.write('x' * 9)
+ self.assertRaises(WrongBodyLength, self.enforcer._noMoreWritesExpected)
+
+
+ def test_writeTooMany(self, _unregisterAfter=False):
+ """
+ If it is called with a total number of bytes exceeding the indicated
+ limit passed to L{LengthEnforcingConsumer.__init__},
+ L{LengthEnforcingConsumer.write} fires the L{Deferred} with a
+ L{Failure} wrapping a L{WrongBodyLength} and also calls the
+ C{stopProducing} method of the producer.
+ """
+ self.enforcer.write('x' * 10)
+ self.assertFalse(self.producer.stopped)
+ self.enforcer.write('x')
+ self.assertTrue(self.producer.stopped)
+ if _unregisterAfter:
+ self.enforcer._noMoreWritesExpected()
+ return self.assertFailure(self.result, WrongBodyLength)
+
+
+ def test_writeAfterNoMoreExpected(self):
+ """
+ If L{LengthEnforcingConsumer.write} is called after
+ L{LengthEnforcingConsumer._noMoreWritesExpected}, it calls the
+ producer's C{stopProducing} method and raises L{ExcessWrite}.
+ """
+ self.enforcer.write('x' * 10)
+ self.enforcer._noMoreWritesExpected()
+ self.assertFalse(self.producer.stopped)
+ self.assertRaises(ExcessWrite, self.enforcer.write, 'x')
+ self.assertTrue(self.producer.stopped)
+
+
+ def test_finishedLate(self):
+ """
+ L{LengthEnforcingConsumer._noMoreWritesExpected} does nothing (in
+ particular, it does not raise any exception) if called after too many
+ bytes have been passed to C{write}.
+ """
+ return self.test_writeTooMany(True)
+
+
+ def test_finished(self):
+ """
+ If L{LengthEnforcingConsumer._noMoreWritesExpected} is called after
+ the correct number of bytes have been written it returns C{None}.
+ """
+ self.enforcer.write('x' * 10)
+ self.assertIdentical(self.enforcer._noMoreWritesExpected(), None)
+
+
+ def test_stopProducingRaises(self):
+ """
+ If L{LengthEnforcingConsumer.write} calls the producer's
+ C{stopProducing} because too many bytes were written and the
+ C{stopProducing} method raises an exception, the exception is logged
+ and the L{LengthEnforcingConsumer} still errbacks the finished
+ L{Deferred}.
+ """
+ def brokenStopProducing():
+ StringProducer.stopProducing(self.producer)
+ raise ArbitraryException("stopProducing is busted")
+ self.producer.stopProducing = brokenStopProducing
+
+ def cbFinished(ignored):
+ self.assertEqual(
+ len(self.flushLoggedErrors(ArbitraryException)), 1)
+ d = self.test_writeTooMany()
+ d.addCallback(cbFinished)
+ return d
+
+
+
+class RequestBodyConsumerTests(TestCase):
+ """
+ Tests for L{ChunkedEncoder} which sits between an L{ITransport} and a
+ request/response body producer and chunked encodes everything written to
+ it.
+ """
+ def test_interface(self):
+ """
+ L{ChunkedEncoder} instances provide L{IConsumer}.
+ """
+ self.assertTrue(
+ verifyObject(IConsumer, ChunkedEncoder(StringTransport())))
+
+
+ def test_write(self):
+ """
+ L{ChunkedEncoder.write} writes to the transport the chunked encoded
+ form of the bytes passed to it.
+ """
+ transport = StringTransport()
+ encoder = ChunkedEncoder(transport)
+ encoder.write('foo')
+ self.assertEqual(transport.value(), '3\r\nfoo\r\n')
+ transport.clear()
+ encoder.write('x' * 16)
+ self.assertEqual(transport.value(), '10\r\n' + 'x' * 16 + '\r\n')
+
+
+ def test_producerRegistration(self):
+ """
+ L{ChunkedEncoder.registerProducer} registers the given streaming
+ producer with its transport and L{ChunkedEncoder.unregisterProducer}
+ writes a zero-length chunk to its transport and unregisters the
+ transport's producer.
+ """
+ transport = StringTransport()
+ producer = object()
+ encoder = ChunkedEncoder(transport)
+ encoder.registerProducer(producer, True)
+ self.assertIdentical(transport.producer, producer)
+ self.assertTrue(transport.streaming)
+ encoder.unregisterProducer()
+ self.assertIdentical(transport.producer, None)
+ self.assertEqual(transport.value(), '0\r\n\r\n')
+
+
+
+class TransportProxyProducerTests(TestCase):
+ """
+ Tests for L{TransportProxyProducer} which proxies the L{IPushProducer}
+ interface of a transport.
+ """
+ def test_interface(self):
+ """
+ L{TransportProxyProducer} instances provide L{IPushProducer}.
+ """
+ self.assertTrue(
+ verifyObject(IPushProducer, TransportProxyProducer(None)))
+
+
+ def test_stopProxyingUnreferencesProducer(self):
+ """
+ L{TransportProxyProducer._stopProxying} drops the reference to the
+ wrapped L{IPushProducer} provider.
+ """
+ transport = StringTransport()
+ proxy = TransportProxyProducer(transport)
+ self.assertIdentical(proxy._producer, transport)
+ proxy._stopProxying()
+ self.assertIdentical(proxy._producer, None)
+
+
+ def test_resumeProducing(self):
+ """
+ L{TransportProxyProducer.resumeProducing} calls the wrapped
+ transport's C{resumeProducing} method unless told to stop proxying.
+ """
+ transport = StringTransport()
+ transport.pauseProducing()
+
+ proxy = TransportProxyProducer(transport)
+ # The transport should still be paused.
+ self.assertEqual(transport.producerState, 'paused')
+ proxy.resumeProducing()
+ # The transport should now be resumed.
+ self.assertEqual(transport.producerState, 'producing')
+
+ transport.pauseProducing()
+ proxy._stopProxying()
+
+ # The proxy should no longer do anything to the transport.
+ proxy.resumeProducing()
+ self.assertEqual(transport.producerState, 'paused')
+
+
+ def test_pauseProducing(self):
+ """
+ L{TransportProxyProducer.pauseProducing} calls the wrapped transport's
+ C{pauseProducing} method unless told to stop proxying.
+ """
+ transport = StringTransport()
+
+ proxy = TransportProxyProducer(transport)
+ # The transport should still be producing.
+ self.assertEqual(transport.producerState, 'producing')
+ proxy.pauseProducing()
+ # The transport should now be paused.
+ self.assertEqual(transport.producerState, 'paused')
+
+ transport.resumeProducing()
+ proxy._stopProxying()
+
+ # The proxy should no longer do anything to the transport.
+ proxy.pauseProducing()
+ self.assertEqual(transport.producerState, 'producing')
+
+
+ def test_stopProducing(self):
+ """
+ L{TransportProxyProducer.stopProducing} calls the wrapped transport's
+ C{stopProducing} method unless told to stop proxying.
+ """
+ transport = StringTransport()
+ proxy = TransportProxyProducer(transport)
+ # The transport should still be producing.
+ self.assertEqual(transport.producerState, 'producing')
+ proxy.stopProducing()
+ # The transport should now be stopped.
+ self.assertEqual(transport.producerState, 'stopped')
+
+ transport = StringTransport()
+ proxy = TransportProxyProducer(transport)
+ proxy._stopProxying()
+ proxy.stopProducing()
+ # The transport should not have been stopped.
+ self.assertEqual(transport.producerState, 'producing')
+
+
+
+class ResponseTests(TestCase):
+ """
+ Tests for L{Response}.
+ """
+
+ def test_verifyInterface(self):
+ """
+ L{Response} instances provide L{IResponse}.
+ """
+ response = justTransportResponse(StringTransport())
+ self.assertTrue(verifyObject(IResponse, response))
+
+
+ def test_makeConnection(self):
+ """
+ The L{IProtocol} provider passed to L{Response.deliverBody} has its
+ C{makeConnection} method called with an L{IPushProducer} provider
+ hooked up to the response as an argument.
+ """
+ producers = []
+ transport = StringTransport()
+ class SomeProtocol(Protocol):
+ def makeConnection(self, producer):
+ producers.append(producer)
+
+ consumer = SomeProtocol()
+ response = justTransportResponse(transport)
+ response.deliverBody(consumer)
+ [theProducer] = producers
+ theProducer.pauseProducing()
+ self.assertEqual(transport.producerState, 'paused')
+ theProducer.resumeProducing()
+ self.assertEqual(transport.producerState, 'producing')
+
+
+ def test_dataReceived(self):
+ """
+ The L{IProtocol} provider passed to L{Response.deliverBody} has its
+ C{dataReceived} method called with bytes received as part of the
+ response body.
+ """
+ bytes = []
+ class ListConsumer(Protocol):
+ def dataReceived(self, data):
+ bytes.append(data)
+
+
+ consumer = ListConsumer()
+ response = justTransportResponse(StringTransport())
+ response.deliverBody(consumer)
+
+ response._bodyDataReceived('foo')
+ self.assertEqual(bytes, ['foo'])
+
+
+ def test_connectionLost(self):
+ """
+ The L{IProtocol} provider passed to L{Response.deliverBody} has its
+ C{connectionLost} method called with a L{Failure} wrapping
+ L{ResponseDone} when the response's C{_bodyDataFinished} method is
+ called.
+ """
+ lost = []
+ class ListConsumer(Protocol):
+ def connectionLost(self, reason):
+ lost.append(reason)
+
+ consumer = ListConsumer()
+ response = justTransportResponse(StringTransport())
+ response.deliverBody(consumer)
+
+ response._bodyDataFinished()
+ lost[0].trap(ResponseDone)
+ self.assertEqual(len(lost), 1)
+
+ # The protocol reference should be dropped, too, to facilitate GC or
+ # whatever.
+ self.assertIdentical(response._bodyProtocol, None)
+
+
+ def test_bufferEarlyData(self):
+ """
+ If data is delivered to the L{Response} before a protocol is registered
+ with C{deliverBody}, that data is buffered until the protocol is
+ registered and then is delivered.
+ """
+ bytes = []
+ class ListConsumer(Protocol):
+ def dataReceived(self, data):
+ bytes.append(data)
+
+ protocol = ListConsumer()
+ response = justTransportResponse(StringTransport())
+ response._bodyDataReceived('foo')
+ response._bodyDataReceived('bar')
+ response.deliverBody(protocol)
+ response._bodyDataReceived('baz')
+ self.assertEqual(bytes, ['foo', 'bar', 'baz'])
+ # Make sure the implementation-detail-byte-buffer is cleared because
+ # not clearing it wastes memory.
+ self.assertIdentical(response._bodyBuffer, None)
+
+
+ def test_multipleStartProducingFails(self):
+ """
+ L{Response.deliverBody} raises L{RuntimeError} if called more than
+ once.
+ """
+ response = justTransportResponse(StringTransport())
+ response.deliverBody(Protocol())
+ self.assertRaises(RuntimeError, response.deliverBody, Protocol())
+
+
+ def test_startProducingAfterFinishedFails(self):
+ """
+ L{Response.deliverBody} raises L{RuntimeError} if called after
+ L{Response._bodyDataFinished}.
+ """
+ response = justTransportResponse(StringTransport())
+ response.deliverBody(Protocol())
+ response._bodyDataFinished()
+ self.assertRaises(RuntimeError, response.deliverBody, Protocol())
+
+
+ def test_bodyDataReceivedAfterFinishedFails(self):
+ """
+ L{Response._bodyDataReceived} raises L{RuntimeError} if called after
+ L{Response._bodyDataFinished} but before L{Response.deliverBody}.
+ """
+ response = justTransportResponse(StringTransport())
+ response._bodyDataFinished()
+ self.assertRaises(RuntimeError, response._bodyDataReceived, 'foo')
+
+
+ def test_bodyDataReceivedAfterDeliveryFails(self):
+ """
+ L{Response._bodyDataReceived} raises L{RuntimeError} if called after
+ L{Response._bodyDataFinished} and after L{Response.deliverBody}.
+ """
+ response = justTransportResponse(StringTransport())
+ response._bodyDataFinished()
+ response.deliverBody(Protocol())
+ self.assertRaises(RuntimeError, response._bodyDataReceived, 'foo')
+
+
+ def test_bodyDataFinishedAfterFinishedFails(self):
+ """
+ L{Response._bodyDataFinished} raises L{RuntimeError} if called more
+ than once.
+ """
+ response = justTransportResponse(StringTransport())
+ response._bodyDataFinished()
+ self.assertRaises(RuntimeError, response._bodyDataFinished)
+
+
+ def test_bodyDataFinishedAfterDeliveryFails(self):
+ """
+ L{Response._bodyDataFinished} raises L{RuntimeError} if called after
+ the body has been delivered.
+ """
+ response = justTransportResponse(StringTransport())
+ response._bodyDataFinished()
+ response.deliverBody(Protocol())
+ self.assertRaises(RuntimeError, response._bodyDataFinished)
+
+
+ def test_transportResumed(self):
+ """
+ L{Response.deliverBody} resumes the HTTP connection's transport
+ before passing it to the transport's C{makeConnection} method.
+ """
+ transportState = []
+ class ListConsumer(Protocol):
+ def makeConnection(self, transport):
+ transportState.append(transport.producerState)
+
+ transport = StringTransport()
+ transport.pauseProducing()
+ protocol = ListConsumer()
+ response = justTransportResponse(transport)
+ self.assertEqual(transport.producerState, 'paused')
+ response.deliverBody(protocol)
+ self.assertEqual(transportState, ['producing'])
+
+
+ def test_bodyDataFinishedBeforeStartProducing(self):
+ """
+ If the entire body is delivered to the L{Response} before the
+ response's C{deliverBody} method is called, the protocol passed to
+ C{deliverBody} is immediately given the body data and then
+ disconnected.
+ """
+ transport = StringTransport()
+ response = justTransportResponse(transport)
+ response._bodyDataReceived('foo')
+ response._bodyDataReceived('bar')
+ response._bodyDataFinished()
+
+ protocol = AccumulatingProtocol()
+ response.deliverBody(protocol)
+ self.assertEqual(protocol.data, 'foobar')
+ protocol.closedReason.trap(ResponseDone)
+
+
+ def test_finishedWithErrorWhenConnected(self):
+ """
+ The L{Failure} passed to L{Response._bodyDataFinished} when the response
+ is in the I{connected} state is passed to the C{connectionLost} method
+ of the L{IProtocol} provider passed to the L{Response}'s
+ C{deliverBody} method.
+ """
+ transport = StringTransport()
+ response = justTransportResponse(transport)
+
+ protocol = AccumulatingProtocol()
+ response.deliverBody(protocol)
+
+ # Sanity check - this test is for the connected state
+ self.assertEqual(response._state, 'CONNECTED')
+ response._bodyDataFinished(Failure(ArbitraryException()))
+
+ protocol.closedReason.trap(ArbitraryException)
+
+
+ def test_finishedWithErrorWhenInitial(self):
+ """
+ The L{Failure} passed to L{Response._bodyDataFinished} when the response
+ is in the I{initial} state is passed to the C{connectionLost} method of
+ the L{IProtocol} provider passed to the L{Response}'s C{deliverBody}
+ method.
+ """
+ transport = StringTransport()
+ response = justTransportResponse(transport)
+
+ # Sanity check - this test is for the initial state
+ self.assertEqual(response._state, 'INITIAL')
+ response._bodyDataFinished(Failure(ArbitraryException()))
+
+ protocol = AccumulatingProtocol()
+ response.deliverBody(protocol)
+
+ protocol.closedReason.trap(ArbitraryException)
diff --git a/twisted/web/test/test_proxy.py b/twisted/web/test/test_proxy.py
new file mode 100644
index 0000000..4452fcb
--- /dev/null
+++ b/twisted/web/test/test_proxy.py
@@ -0,0 +1,544 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test for L{twisted.web.proxy}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransportWithDisconnection
+from twisted.test.proto_helpers import MemoryReactor
+
+from twisted.web.resource import Resource
+from twisted.web.server import Site
+from twisted.web.proxy import ReverseProxyResource, ProxyClientFactory
+from twisted.web.proxy import ProxyClient, ProxyRequest, ReverseProxyRequest
+from twisted.web.test.test_web import DummyRequest
+
+
+class ReverseProxyResourceTestCase(TestCase):
+ """
+ Tests for L{ReverseProxyResource}.
+ """
+
+ def _testRender(self, uri, expectedURI):
+ """
+ Check that a request pointing at C{uri} produce a new proxy connection,
+ with the path of this request pointing at C{expectedURI}.
+ """
+ root = Resource()
+ reactor = MemoryReactor()
+ resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor)
+ root.putChild('index', resource)
+ site = Site(root)
+
+ transport = StringTransportWithDisconnection()
+ channel = site.buildProtocol(None)
+ channel.makeConnection(transport)
+ # Clear the timeout if the tests failed
+ self.addCleanup(channel.connectionLost, None)
+
+ channel.dataReceived("GET %s HTTP/1.1\r\nAccept: text/html\r\n\r\n" %
+ (uri,))
+
+ # Check that one connection has been created, to the good host/port
+ self.assertEqual(len(reactor.tcpClients), 1)
+ self.assertEqual(reactor.tcpClients[0][0], "127.0.0.1")
+ self.assertEqual(reactor.tcpClients[0][1], 1234)
+
+ # Check the factory passed to the connect, and its given path
+ factory = reactor.tcpClients[0][2]
+ self.assertIsInstance(factory, ProxyClientFactory)
+ self.assertEqual(factory.rest, expectedURI)
+ self.assertEqual(factory.headers["host"], "127.0.0.1:1234")
+
+
+ def test_render(self):
+ """
+ Test that L{ReverseProxyResource.render} initiates a connection to the
+ given server with a L{ProxyClientFactory} as parameter.
+ """
+ return self._testRender("/index", "/path")
+
+
+ def test_renderWithQuery(self):
+ """
+ Test that L{ReverseProxyResource.render} passes query parameters to the
+ created factory.
+ """
+ return self._testRender("/index?foo=bar", "/path?foo=bar")
+
+
+ def test_getChild(self):
+ """
+ The L{ReverseProxyResource.getChild} method should return a resource
+ instance with the same class as the originating resource, forward
+ port, host, and reactor values, and update the path value with the
+ value passed.
+ """
+ reactor = MemoryReactor()
+ resource = ReverseProxyResource("127.0.0.1", 1234, "/path", reactor)
+ child = resource.getChild('foo', None)
+ # The child should keep the same class
+ self.assertIsInstance(child, ReverseProxyResource)
+ self.assertEqual(child.path, "/path/foo")
+ self.assertEqual(child.port, 1234)
+ self.assertEqual(child.host, "127.0.0.1")
+ self.assertIdentical(child.reactor, resource.reactor)
+
+
+ def test_getChildWithSpecial(self):
+ """
+ The L{ReverseProxyResource} return by C{getChild} has a path which has
+ already been quoted.
+ """
+ resource = ReverseProxyResource("127.0.0.1", 1234, "/path")
+ child = resource.getChild(' /%', None)
+ self.assertEqual(child.path, "/path/%20%2F%25")
+
+
+
+class DummyChannel(object):
+ """
+ A dummy HTTP channel, that does nothing but holds a transport and saves
+ connection lost.
+
+ @ivar transport: the transport used by the client.
+ @ivar lostReason: the reason saved at connection lost.
+ """
+
+ def __init__(self, transport):
+ """
+ Hold a reference to the transport.
+ """
+ self.transport = transport
+ self.lostReason = None
+
+
+ def connectionLost(self, reason):
+ """
+ Keep track of the connection lost reason.
+ """
+ self.lostReason = reason
+
+
+
+class ProxyClientTestCase(TestCase):
+ """
+ Tests for L{ProxyClient}.
+ """
+
+ def _parseOutHeaders(self, content):
+ """
+ Parse the headers out of some web content.
+
+ @param content: Bytes received from a web server.
+ @return: A tuple of (requestLine, headers, body). C{headers} is a dict
+ of headers, C{requestLine} is the first line (e.g. "POST /foo ...")
+ and C{body} is whatever is left.
+ """
+ headers, body = content.split('\r\n\r\n')
+ headers = headers.split('\r\n')
+ requestLine = headers.pop(0)
+ return (
+ requestLine, dict(header.split(': ') for header in headers), body)
+
+
+ def makeRequest(self, path):
+ """
+ Make a dummy request object for the URL path.
+
+ @param path: A URL path, beginning with a slash.
+ @return: A L{DummyRequest}.
+ """
+ return DummyRequest(path)
+
+
+ def makeProxyClient(self, request, method="GET", headers=None,
+ requestBody=""):
+ """
+ Make a L{ProxyClient} object used for testing.
+
+ @param request: The request to use.
+ @param method: The HTTP method to use, GET by default.
+ @param headers: The HTTP headers to use expressed as a dict. If not
+ provided, defaults to {'accept': 'text/html'}.
+ @param requestBody: The body of the request. Defaults to the empty
+ string.
+ @return: A L{ProxyClient}
+ """
+ if headers is None:
+ headers = {"accept": "text/html"}
+ path = '/' + request.postpath
+ return ProxyClient(
+ method, path, 'HTTP/1.0', headers, requestBody, request)
+
+
+ def connectProxy(self, proxyClient):
+ """
+ Connect a proxy client to a L{StringTransportWithDisconnection}.
+
+ @param proxyClient: A L{ProxyClient}.
+ @return: The L{StringTransportWithDisconnection}.
+ """
+ clientTransport = StringTransportWithDisconnection()
+ clientTransport.protocol = proxyClient
+ proxyClient.makeConnection(clientTransport)
+ return clientTransport
+
+
+ def assertForwardsHeaders(self, proxyClient, requestLine, headers):
+ """
+ Assert that C{proxyClient} sends C{headers} when it connects.
+
+ @param proxyClient: A L{ProxyClient}.
+ @param requestLine: The request line we expect to be sent.
+ @param headers: A dict of headers we expect to be sent.
+ @return: If the assertion is successful, return the request body as
+ bytes.
+ """
+ self.connectProxy(proxyClient)
+ requestContent = proxyClient.transport.value()
+ receivedLine, receivedHeaders, body = self._parseOutHeaders(
+ requestContent)
+ self.assertEqual(receivedLine, requestLine)
+ self.assertEqual(receivedHeaders, headers)
+ return body
+
+
+ def makeResponseBytes(self, code, message, headers, body):
+ lines = ["HTTP/1.0 %d %s" % (code, message)]
+ for header, values in headers:
+ for value in values:
+ lines.append("%s: %s" % (header, value))
+ lines.extend(['', body])
+ return '\r\n'.join(lines)
+
+
+ def assertForwardsResponse(self, request, code, message, headers, body):
+ """
+ Assert that C{request} has forwarded a response from the server.
+
+ @param request: A L{DummyRequest}.
+ @param code: The expected HTTP response code.
+ @param message: The expected HTTP message.
+ @param headers: The expected HTTP headers.
+ @param body: The expected response body.
+ """
+ self.assertEqual(request.responseCode, code)
+ self.assertEqual(request.responseMessage, message)
+ receivedHeaders = list(request.responseHeaders.getAllRawHeaders())
+ receivedHeaders.sort()
+ expectedHeaders = headers[:]
+ expectedHeaders.sort()
+ self.assertEqual(receivedHeaders, expectedHeaders)
+ self.assertEqual(''.join(request.written), body)
+
+
+ def _testDataForward(self, code, message, headers, body, method="GET",
+ requestBody="", loseConnection=True):
+ """
+ Build a fake proxy connection, and send C{data} over it, checking that
+ it's forwarded to the originating request.
+ """
+ request = self.makeRequest('foo')
+ client = self.makeProxyClient(
+ request, method, {'accept': 'text/html'}, requestBody)
+
+ receivedBody = self.assertForwardsHeaders(
+ client, '%s /foo HTTP/1.0' % (method,),
+ {'connection': 'close', 'accept': 'text/html'})
+
+ self.assertEqual(receivedBody, requestBody)
+
+ # Fake an answer
+ client.dataReceived(
+ self.makeResponseBytes(code, message, headers, body))
+
+ # Check that the response data has been forwarded back to the original
+ # requester.
+ self.assertForwardsResponse(request, code, message, headers, body)
+
+ # Check that when the response is done, the request is finished.
+ if loseConnection:
+ client.transport.loseConnection()
+
+ # Even if we didn't call loseConnection, the transport should be
+ # disconnected. This lets us not rely on the server to close our
+ # sockets for us.
+ self.assertFalse(client.transport.connected)
+ self.assertEqual(request.finished, 1)
+
+
+ def test_forward(self):
+ """
+ When connected to the server, L{ProxyClient} should send the saved
+ request, with modifications of the headers, and then forward the result
+ to the parent request.
+ """
+ return self._testDataForward(
+ 200, "OK", [("Foo", ["bar", "baz"])], "Some data\r\n")
+
+
+ def test_postData(self):
+ """
+ Try to post content in the request, and check that the proxy client
+ forward the body of the request.
+ """
+ return self._testDataForward(
+ 200, "OK", [("Foo", ["bar"])], "Some data\r\n", "POST", "Some content")
+
+
+ def test_statusWithMessage(self):
+ """
+ If the response contains a status with a message, it should be
+ forwarded to the parent request with all the information.
+ """
+ return self._testDataForward(
+ 404, "Not Found", [], "")
+
+
+ def test_contentLength(self):
+ """
+ If the response contains a I{Content-Length} header, the inbound
+ request object should still only have C{finish} called on it once.
+ """
+ data = "foo bar baz"
+ return self._testDataForward(
+ 200, "OK", [("Content-Length", [str(len(data))])], data)
+
+
+ def test_losesConnection(self):
+ """
+ If the response contains a I{Content-Length} header, the outgoing
+ connection is closed when all response body data has been received.
+ """
+ data = "foo bar baz"
+ return self._testDataForward(
+ 200, "OK", [("Content-Length", [str(len(data))])], data,
+ loseConnection=False)
+
+
+ def test_headersCleanups(self):
+ """
+ The headers given at initialization should be modified:
+ B{proxy-connection} should be removed if present, and B{connection}
+ should be added.
+ """
+ client = ProxyClient('GET', '/foo', 'HTTP/1.0',
+ {"accept": "text/html", "proxy-connection": "foo"}, '', None)
+ self.assertEqual(client.headers,
+ {"accept": "text/html", "connection": "close"})
+
+
+ def test_keepaliveNotForwarded(self):
+ """
+ The proxy doesn't really know what to do with keepalive things from
+ the remote server, so we stomp over any keepalive header we get from
+ the client.
+ """
+ headers = {
+ "accept": "text/html",
+ 'keep-alive': '300',
+ 'connection': 'keep-alive',
+ }
+ expectedHeaders = headers.copy()
+ expectedHeaders['connection'] = 'close'
+ del expectedHeaders['keep-alive']
+ client = ProxyClient('GET', '/foo', 'HTTP/1.0', headers, '', None)
+ self.assertForwardsHeaders(
+ client, 'GET /foo HTTP/1.0', expectedHeaders)
+
+
+ def test_defaultHeadersOverridden(self):
+ """
+ L{server.Request} within the proxy sets certain response headers by
+ default. When we get these headers back from the remote server, the
+ defaults are overridden rather than simply appended.
+ """
+ request = self.makeRequest('foo')
+ request.responseHeaders.setRawHeaders('server', ['old-bar'])
+ request.responseHeaders.setRawHeaders('date', ['old-baz'])
+ request.responseHeaders.setRawHeaders('content-type', ["old/qux"])
+ client = self.makeProxyClient(request, headers={'accept': 'text/html'})
+ self.connectProxy(client)
+ headers = {
+ 'Server': ['bar'],
+ 'Date': ['2010-01-01'],
+ 'Content-Type': ['application/x-baz'],
+ }
+ client.dataReceived(
+ self.makeResponseBytes(200, "OK", headers.items(), ''))
+ self.assertForwardsResponse(
+ request, 200, 'OK', headers.items(), '')
+
+
+
+class ProxyClientFactoryTestCase(TestCase):
+ """
+ Tests for L{ProxyClientFactory}.
+ """
+
+ def test_connectionFailed(self):
+ """
+ Check that L{ProxyClientFactory.clientConnectionFailed} produces
+ a B{501} response to the parent request.
+ """
+ request = DummyRequest(['foo'])
+ factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
+ {"accept": "text/html"}, '', request)
+
+ factory.clientConnectionFailed(None, None)
+ self.assertEqual(request.responseCode, 501)
+ self.assertEqual(request.responseMessage, "Gateway error")
+ self.assertEqual(
+ list(request.responseHeaders.getAllRawHeaders()),
+ [("Content-Type", ["text/html"])])
+ self.assertEqual(
+ ''.join(request.written),
+ "<H1>Could not connect</H1>")
+ self.assertEqual(request.finished, 1)
+
+
+ def test_buildProtocol(self):
+ """
+ L{ProxyClientFactory.buildProtocol} should produce a L{ProxyClient}
+ with the same values of attributes (with updates on the headers).
+ """
+ factory = ProxyClientFactory('GET', '/foo', 'HTTP/1.0',
+ {"accept": "text/html"}, 'Some data',
+ None)
+ proto = factory.buildProtocol(None)
+ self.assertIsInstance(proto, ProxyClient)
+ self.assertEqual(proto.command, 'GET')
+ self.assertEqual(proto.rest, '/foo')
+ self.assertEqual(proto.data, 'Some data')
+ self.assertEqual(proto.headers,
+ {"accept": "text/html", "connection": "close"})
+
+
+
+class ProxyRequestTestCase(TestCase):
+ """
+ Tests for L{ProxyRequest}.
+ """
+
+ def _testProcess(self, uri, expectedURI, method="GET", data=""):
+ """
+ Build a request pointing at C{uri}, and check that a proxied request
+ is created, pointing a C{expectedURI}.
+ """
+ transport = StringTransportWithDisconnection()
+ channel = DummyChannel(transport)
+ reactor = MemoryReactor()
+ request = ProxyRequest(channel, False, reactor)
+ request.gotLength(len(data))
+ request.handleContentChunk(data)
+ request.requestReceived(method, 'http://example.com%s' % (uri,),
+ 'HTTP/1.0')
+
+ self.assertEqual(len(reactor.tcpClients), 1)
+ self.assertEqual(reactor.tcpClients[0][0], "example.com")
+ self.assertEqual(reactor.tcpClients[0][1], 80)
+
+ factory = reactor.tcpClients[0][2]
+ self.assertIsInstance(factory, ProxyClientFactory)
+ self.assertEqual(factory.command, method)
+ self.assertEqual(factory.version, 'HTTP/1.0')
+ self.assertEqual(factory.headers, {'host': 'example.com'})
+ self.assertEqual(factory.data, data)
+ self.assertEqual(factory.rest, expectedURI)
+ self.assertEqual(factory.father, request)
+
+
+ def test_process(self):
+ """
+ L{ProxyRequest.process} should create a connection to the given server,
+ with a L{ProxyClientFactory} as connection factory, with the correct
+ parameters:
+ - forward comment, version and data values
+ - update headers with the B{host} value
+ - remove the host from the URL
+ - pass the request as parent request
+ """
+ return self._testProcess("/foo/bar", "/foo/bar")
+
+
+ def test_processWithoutTrailingSlash(self):
+ """
+ If the incoming request doesn't contain a slash,
+ L{ProxyRequest.process} should add one when instantiating
+ L{ProxyClientFactory}.
+ """
+ return self._testProcess("", "/")
+
+
+ def test_processWithData(self):
+ """
+ L{ProxyRequest.process} should be able to retrieve request body and
+ to forward it.
+ """
+ return self._testProcess(
+ "/foo/bar", "/foo/bar", "POST", "Some content")
+
+
+ def test_processWithPort(self):
+ """
+ Check that L{ProxyRequest.process} correctly parse port in the incoming
+ URL, and create a outgoing connection with this port.
+ """
+ transport = StringTransportWithDisconnection()
+ channel = DummyChannel(transport)
+ reactor = MemoryReactor()
+ request = ProxyRequest(channel, False, reactor)
+ request.gotLength(0)
+ request.requestReceived('GET', 'http://example.com:1234/foo/bar',
+ 'HTTP/1.0')
+
+ # That should create one connection, with the port parsed from the URL
+ self.assertEqual(len(reactor.tcpClients), 1)
+ self.assertEqual(reactor.tcpClients[0][0], "example.com")
+ self.assertEqual(reactor.tcpClients[0][1], 1234)
+
+
+
+class DummyFactory(object):
+ """
+ A simple holder for C{host} and C{port} information.
+ """
+
+ def __init__(self, host, port):
+ self.host = host
+ self.port = port
+
+
+
+class ReverseProxyRequestTestCase(TestCase):
+ """
+ Tests for L{ReverseProxyRequest}.
+ """
+
+ def test_process(self):
+ """
+ L{ReverseProxyRequest.process} should create a connection to its
+ factory host/port, using a L{ProxyClientFactory} instantiated with the
+ correct parameters, and particulary set the B{host} header to the
+ factory host.
+ """
+ transport = StringTransportWithDisconnection()
+ channel = DummyChannel(transport)
+ reactor = MemoryReactor()
+ request = ReverseProxyRequest(channel, False, reactor)
+ request.factory = DummyFactory("example.com", 1234)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+
+ # Check that one connection has been created, to the good host/port
+ self.assertEqual(len(reactor.tcpClients), 1)
+ self.assertEqual(reactor.tcpClients[0][0], "example.com")
+ self.assertEqual(reactor.tcpClients[0][1], 1234)
+
+ # Check the factory passed to the connect, and its headers
+ factory = reactor.tcpClients[0][2]
+ self.assertIsInstance(factory, ProxyClientFactory)
+ self.assertEqual(factory.headers, {'host': 'example.com'})
diff --git a/twisted/web/test/test_resource.py b/twisted/web/test/test_resource.py
new file mode 100644
index 0000000..0ba132c
--- /dev/null
+++ b/twisted/web/test/test_resource.py
@@ -0,0 +1,145 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.resource}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.web import error
+from twisted.web.http import NOT_FOUND, FORBIDDEN
+from twisted.web.resource import ErrorPage, NoResource, ForbiddenResource
+from twisted.web.test.test_web import DummyRequest
+
+
+class ErrorPageTests(TestCase):
+ """
+ Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}.
+ """
+
+ errorPage = ErrorPage
+ noResource = NoResource
+ forbiddenResource = ForbiddenResource
+
+ def test_getChild(self):
+ """
+ The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
+ called on.
+ """
+ page = self.errorPage(321, "foo", "bar")
+ self.assertIdentical(page.getChild("name", object()), page)
+
+
+ def _pageRenderingTest(self, page, code, brief, detail):
+ request = DummyRequest([''])
+ self.assertEqual(
+ page.render(request),
+ "\n"
+ "<html>\n"
+ " <head><title>%s - %s</title></head>\n"
+ " <body>\n"
+ " <h1>%s</h1>\n"
+ " <p>%s</p>\n"
+ " </body>\n"
+ "</html>\n" % (code, brief, brief, detail))
+ self.assertEqual(request.responseCode, code)
+ self.assertEqual(
+ request.outgoingHeaders,
+ {'content-type': 'text/html; charset=utf-8'})
+
+
+ def test_errorPageRendering(self):
+ """
+ L{ErrorPage.render} returns a C{str} describing the error defined by
+ the response code and message passed to L{ErrorPage.__init__}. It also
+ uses that response code to set the response code on the L{Request}
+ passed in.
+ """
+ code = 321
+ brief = "brief description text"
+ detail = "much longer text might go here"
+ page = self.errorPage(code, brief, detail)
+ self._pageRenderingTest(page, code, brief, detail)
+
+
+ def test_noResourceRendering(self):
+ """
+ L{NoResource} sets the HTTP I{NOT FOUND} code.
+ """
+ detail = "long message"
+ page = self.noResource(detail)
+ self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)
+
+
+ def test_forbiddenResourceRendering(self):
+ """
+ L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
+ """
+ detail = "longer message"
+ page = self.forbiddenResource(detail)
+ self._pageRenderingTest(page, FORBIDDEN, "Forbidden Resource", detail)
+
+
+
+class DeprecatedErrorPageTests(ErrorPageTests):
+ """
+ Tests for L{error.ErrorPage}, L{error.NoResource}, and
+ L{error.ForbiddenResource}.
+ """
+ def errorPage(self, *args):
+ return error.ErrorPage(*args)
+
+
+ def noResource(self, *args):
+ return error.NoResource(*args)
+
+
+ def forbiddenResource(self, *args):
+ return error.ForbiddenResource(*args)
+
+
+ def _assertWarning(self, name, offendingFunction):
+ warnings = self.flushWarnings([offendingFunction])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'twisted.web.error.%s is deprecated since Twisted 9.0. '
+ 'See twisted.web.resource.%s.' % (name, name))
+
+
+ def test_getChild(self):
+ """
+ Like L{ErrorPageTests.test_getChild}, but flush the deprecation warning
+ emitted by instantiating L{error.ErrorPage}.
+ """
+ ErrorPageTests.test_getChild(self)
+ self._assertWarning('ErrorPage', self.errorPage)
+
+
+ def test_errorPageRendering(self):
+ """
+ Like L{ErrorPageTests.test_errorPageRendering}, but flush the
+ deprecation warning emitted by instantiating L{error.ErrorPage}.
+ """
+ ErrorPageTests.test_errorPageRendering(self)
+ self._assertWarning('ErrorPage', self.errorPage)
+
+
+ def test_noResourceRendering(self):
+ """
+ Like L{ErrorPageTests.test_noResourceRendering}, but flush the
+ deprecation warning emitted by instantiating L{error.NoResource}.
+ """
+ ErrorPageTests.test_noResourceRendering(self)
+ self._assertWarning('NoResource', self.noResource)
+
+
+ def test_forbiddenResourceRendering(self):
+ """
+ Like L{ErrorPageTests.test_forbiddenResourceRendering}, but flush the
+ deprecation warning emitted by instantiating
+ L{error.ForbiddenResource}.
+ """
+ ErrorPageTests.test_forbiddenResourceRendering(self)
+ self._assertWarning('ForbiddenResource', self.forbiddenResource)
diff --git a/twisted/web/test/test_script.py b/twisted/web/test/test_script.py
new file mode 100644
index 0000000..b4248bf
--- /dev/null
+++ b/twisted/web/test/test_script.py
@@ -0,0 +1,70 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.script}.
+"""
+
+import os
+
+from twisted.trial.unittest import TestCase
+from twisted.web.http import NOT_FOUND
+from twisted.web.script import ResourceScriptDirectory, PythonScript
+from twisted.web.test._util import _render
+from twisted.web.test.test_web import DummyRequest
+
+
+class ResourceScriptDirectoryTests(TestCase):
+ """
+ Tests for L{ResourceScriptDirectory}.
+ """
+ def test_render(self):
+ """
+ L{ResourceScriptDirectory.render} sets the HTTP response code to I{NOT
+ FOUND}.
+ """
+ resource = ResourceScriptDirectory(self.mktemp())
+ request = DummyRequest([''])
+ d = _render(resource, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_notFoundChild(self):
+ """
+ L{ResourceScriptDirectory.getChild} returns a resource which renders an
+ response with the HTTP I{NOT FOUND} status code if the indicated child
+ does not exist as an entry in the directory used to initialized the
+ L{ResourceScriptDirectory}.
+ """
+ path = self.mktemp()
+ os.makedirs(path)
+ resource = ResourceScriptDirectory(path)
+ request = DummyRequest(['foo'])
+ child = resource.getChild("foo", request)
+ d = _render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
+
+
+
+class PythonScriptTests(TestCase):
+ """
+ Tests for L{PythonScript}.
+ """
+ def test_notFoundRender(self):
+ """
+ If the source file a L{PythonScript} is initialized with doesn't exist,
+ L{PythonScript.render} sets the HTTP response code to I{NOT FOUND}.
+ """
+ resource = PythonScript(self.mktemp(), None)
+ request = DummyRequest([''])
+ d = _render(resource, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
diff --git a/twisted/web/test/test_soap.py b/twisted/web/test/test_soap.py
new file mode 100644
index 0000000..247282f
--- /dev/null
+++ b/twisted/web/test/test_soap.py
@@ -0,0 +1,114 @@
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+
+"""Test SOAP support."""
+
+try:
+ import SOAPpy
+except ImportError:
+ SOAPpy = None
+ class SOAPPublisher: pass
+else:
+ from twisted.web import soap
+ SOAPPublisher = soap.SOAPPublisher
+
+from twisted.trial import unittest
+from twisted.web import server, error
+from twisted.internet import reactor, defer
+
+
+class Test(SOAPPublisher):
+
+ def soap_add(self, a, b):
+ return a + b
+
+ def soap_kwargs(self, a=1, b=2):
+ return a + b
+ soap_kwargs.useKeywords=True
+
+ def soap_triple(self, string, num):
+ return [string, num, None]
+
+ def soap_struct(self):
+ return SOAPpy.structType({"a": "c"})
+
+ def soap_defer(self, x):
+ return defer.succeed(x)
+
+ def soap_deferFail(self):
+ return defer.fail(ValueError())
+
+ def soap_fail(self):
+ raise RuntimeError
+
+ def soap_deferFault(self):
+ return defer.fail(ValueError())
+
+ def soap_complex(self):
+ return {"a": ["b", "c", 12, []], "D": "foo"}
+
+ def soap_dict(self, map, key):
+ return map[key]
+
+
+class SOAPTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.publisher = Test()
+ self.p = reactor.listenTCP(0, server.Site(self.publisher),
+ interface="127.0.0.1")
+ self.port = self.p.getHost().port
+
+ def tearDown(self):
+ return self.p.stopListening()
+
+ def proxy(self):
+ return soap.Proxy("http://127.0.0.1:%d/" % self.port)
+
+ def testResults(self):
+ inputOutput = [
+ ("add", (2, 3), 5),
+ ("defer", ("a",), "a"),
+ ("dict", ({"a": 1}, "a"), 1),
+ ("triple", ("a", 1), ["a", 1, None])]
+
+ dl = []
+ for meth, args, outp in inputOutput:
+ d = self.proxy().callRemote(meth, *args)
+ d.addCallback(self.assertEqual, outp)
+ dl.append(d)
+
+ # SOAPpy kinda blows.
+ d = self.proxy().callRemote('complex')
+ d.addCallback(lambda result: result._asdict())
+ d.addCallback(self.assertEqual, {"a": ["b", "c", 12, []], "D": "foo"})
+ dl.append(d)
+
+ # We now return to our regularly scheduled program, already in progress.
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def testMethodNotFound(self):
+ """
+ Check that a non existing method return error 500.
+ """
+ d = self.proxy().callRemote('doesntexist')
+ self.assertFailure(d, error.Error)
+ def cb(err):
+ self.assertEqual(int(err.status), 500)
+ d.addCallback(cb)
+ return d
+
+ def testLookupFunction(self):
+ """
+ Test lookupFunction method on publisher, to see available remote
+ methods.
+ """
+ self.assertTrue(self.publisher.lookupFunction("add"))
+ self.assertTrue(self.publisher.lookupFunction("fail"))
+ self.assertFalse(self.publisher.lookupFunction("foobar"))
+
+if not SOAPpy:
+ SOAPTestCase.skip = "SOAPpy not installed"
+
diff --git a/twisted/web/test/test_stan.py b/twisted/web/test/test_stan.py
new file mode 100644
index 0000000..9aa65a6
--- /dev/null
+++ b/twisted/web/test/test_stan.py
@@ -0,0 +1,139 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web._stan} portion of the L{twisted.web.template}
+implementation.
+"""
+
+from twisted.web.template import Comment, CDATA, CharRef, Tag
+from twisted.trial.unittest import TestCase
+
+def proto(*a, **kw):
+ """
+ Produce a new tag for testing.
+ """
+ return Tag('hello')(*a, **kw)
+
+
+class TestTag(TestCase):
+ """
+ Tests for L{Tag}.
+ """
+ def test_fillSlots(self):
+ """
+ L{Tag.fillSlots} returns self.
+ """
+ tag = proto()
+ self.assertIdentical(tag, tag.fillSlots(test='test'))
+
+
+ def test_cloneShallow(self):
+ """
+ L{Tag.clone} copies all attributes and children of a tag, including its
+ render attribute. If the shallow flag is C{False}, that's where it
+ stops.
+ """
+ innerList = ["inner list"]
+ tag = proto("How are you", innerList,
+ hello="world", render="aSampleMethod")
+ tag.fillSlots(foo='bar')
+ tag.filename = "foo/bar"
+ tag.lineNumber = 6
+ tag.columnNumber = 12
+ clone = tag.clone(deep=False)
+ self.assertEqual(clone.attributes['hello'], 'world')
+ self.assertNotIdentical(clone.attributes, tag.attributes)
+ self.assertEqual(clone.children, ["How are you", innerList])
+ self.assertNotIdentical(clone.children, tag.children)
+ self.assertIdentical(clone.children[1], innerList)
+ self.assertEqual(tag.slotData, clone.slotData)
+ self.assertNotIdentical(tag.slotData, clone.slotData)
+ self.assertEqual(clone.filename, "foo/bar")
+ self.assertEqual(clone.lineNumber, 6)
+ self.assertEqual(clone.columnNumber, 12)
+ self.assertEqual(clone.render, "aSampleMethod")
+
+
+ def test_cloneDeep(self):
+ """
+ L{Tag.clone} copies all attributes and children of a tag, including its
+ render attribute. In its normal operating mode (where the deep flag is
+ C{True}, as is the default), it will clone all sub-lists and sub-tags.
+ """
+ innerTag = proto("inner")
+ innerList = ["inner list"]
+ tag = proto("How are you", innerTag, innerList,
+ hello="world", render="aSampleMethod")
+ tag.fillSlots(foo='bar')
+ tag.filename = "foo/bar"
+ tag.lineNumber = 6
+ tag.columnNumber = 12
+ clone = tag.clone()
+ self.assertEqual(clone.attributes['hello'], 'world')
+ self.assertNotIdentical(clone.attributes, tag.attributes)
+ self.assertNotIdentical(clone.children, tag.children)
+ # sanity check
+ self.assertIdentical(tag.children[1], innerTag)
+ # clone should have sub-clone
+ self.assertNotIdentical(clone.children[1], innerTag)
+ # sanity check
+ self.assertIdentical(tag.children[2], innerList)
+ # clone should have sub-clone
+ self.assertNotIdentical(clone.children[2], innerList)
+ self.assertEqual(tag.slotData, clone.slotData)
+ self.assertNotIdentical(tag.slotData, clone.slotData)
+ self.assertEqual(clone.filename, "foo/bar")
+ self.assertEqual(clone.lineNumber, 6)
+ self.assertEqual(clone.columnNumber, 12)
+ self.assertEqual(clone.render, "aSampleMethod")
+
+
+ def test_clear(self):
+ """
+ L{Tag.clear} removes all children from a tag, but leaves its attributes
+ in place.
+ """
+ tag = proto("these are", "children", "cool", andSoIs='this-attribute')
+ tag.clear()
+ self.assertEqual(tag.children, [])
+ self.assertEqual(tag.attributes, {'andSoIs': 'this-attribute'})
+
+
+ def test_suffix(self):
+ """
+ L{Tag.__call__} accepts Python keywords with a suffixed underscore as
+ the DOM attribute of that literal suffix.
+ """
+ proto = Tag('div')
+ tag = proto()
+ tag(class_='a')
+ self.assertEqual(tag.attributes, {'class': 'a'})
+
+
+ def test_commentRepr(self):
+ """
+ L{Comment.__repr__} returns a value which makes it easy to see what's in
+ the comment.
+ """
+ self.assertEqual(repr(Comment(u"hello there")),
+ "Comment(u'hello there')")
+
+
+ def test_cdataRepr(self):
+ """
+ L{CDATA.__repr__} returns a value which makes it easy to see what's in
+ the comment.
+ """
+ self.assertEqual(repr(CDATA(u"test data")),
+ "CDATA(u'test data')")
+
+
+ def test_charrefRepr(self):
+ """
+ L{CharRef.__repr__} returns a value which makes it easy to see what
+ character is referred to.
+ """
+ snowman = ord(u"\N{SNOWMAN}")
+ self.assertEqual(repr(CharRef(snowman)), "CharRef(9731)")
diff --git a/twisted/web/test/test_static.py b/twisted/web/test/test_static.py
new file mode 100644
index 0000000..5841cbc
--- /dev/null
+++ b/twisted/web/test/test_static.py
@@ -0,0 +1,1505 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.static}.
+"""
+
+import os, re, StringIO
+
+from zope.interface.verify import verifyObject
+
+from twisted.internet import abstract, interfaces
+from twisted.python.compat import set
+from twisted.python.runtime import platform
+from twisted.python.filepath import FilePath
+from twisted.python import log
+from twisted.trial.unittest import TestCase
+from twisted.web import static, http, script, resource
+from twisted.web.server import UnsupportedMethod
+from twisted.web.test.test_web import DummyRequest
+from twisted.web.test._util import _render
+
+
+class StaticDataTests(TestCase):
+ """
+ Tests for L{Data}.
+ """
+ def test_headRequest(self):
+ """
+ L{Data.render} returns an empty response body for a I{HEAD} request.
+ """
+ data = static.Data("foo", "bar")
+ request = DummyRequest([''])
+ request.method = 'HEAD'
+ d = _render(data, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), "")
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_invalidMethod(self):
+ """
+ L{Data.render} raises L{UnsupportedMethod} in response to a non-I{GET},
+ non-I{HEAD} request.
+ """
+ data = static.Data("foo", "bar")
+ request = DummyRequest([''])
+ request.method = 'POST'
+ self.assertRaises(UnsupportedMethod, data.render, request)
+
+
+
+class StaticFileTests(TestCase):
+ """
+ Tests for the basic behavior of L{File}.
+ """
+ def _render(self, resource, request):
+ return _render(resource, request)
+
+
+ def test_invalidMethod(self):
+ """
+ L{File.render} raises L{UnsupportedMethod} in response to a non-I{GET},
+ non-I{HEAD} request.
+ """
+ request = DummyRequest([''])
+ request.method = 'POST'
+ path = FilePath(self.mktemp())
+ path.setContent("foo")
+ file = static.File(path.path)
+ self.assertRaises(UnsupportedMethod, file.render, request)
+
+
+ def test_notFound(self):
+ """
+ If a request is made which encounters a L{File} before a final segment
+ which does not correspond to any file in the path the L{File} was
+ created with, a not found response is sent.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ file = static.File(base.path)
+
+ request = DummyRequest(['foobar'])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, 404)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_emptyChild(self):
+ """
+ The C{''} child of a L{File} which corresponds to a directory in the
+ filesystem is a L{DirectoryLister}.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ file = static.File(base.path)
+
+ request = DummyRequest([''])
+ child = resource.getChildForRequest(file, request)
+ self.assertIsInstance(child, static.DirectoryLister)
+ self.assertEqual(child.path, base.path)
+
+
+ def test_securityViolationNotFound(self):
+ """
+ If a request is made which encounters a L{File} before a final segment
+ which cannot be looked up in the filesystem due to security
+ considerations, a not found response is sent.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ file = static.File(base.path)
+
+ request = DummyRequest(['..'])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, 404)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_forbiddenResource(self):
+ """
+ If the file in the filesystem which would satisfy a request cannot be
+ read, L{File.render} sets the HTTP response code to I{FORBIDDEN}.
+ """
+ base = FilePath(self.mktemp())
+ base.setContent('')
+ # Make sure we can delete the file later.
+ self.addCleanup(base.chmod, 0700)
+
+ # Get rid of our own read permission.
+ base.chmod(0)
+
+ file = static.File(base.path)
+ request = DummyRequest([''])
+ d = self._render(file, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, 403)
+ d.addCallback(cbRendered)
+ return d
+ if platform.isWindows():
+ test_forbiddenResource.skip = "Cannot remove read permission on Windows"
+
+
+ def test_indexNames(self):
+ """
+ If a request is made which encounters a L{File} before a final empty
+ segment, a file in the L{File} instance's C{indexNames} list which
+ exists in the path the L{File} was created with is served as the
+ response to the request.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ base.child("foo.bar").setContent("baz")
+ file = static.File(base.path)
+ file.indexNames = ['foo.bar']
+
+ request = DummyRequest([''])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), 'baz')
+ self.assertEqual(request.outgoingHeaders['content-length'], '3')
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_staticFile(self):
+ """
+ If a request is made which encounters a L{File} before a final segment
+ which names a file in the path the L{File} was created with, that file
+ is served as the response to the request.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ base.child("foo.bar").setContent("baz")
+ file = static.File(base.path)
+
+ request = DummyRequest(['foo.bar'])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), 'baz')
+ self.assertEqual(request.outgoingHeaders['content-length'], '3')
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_staticFileDeletedGetChild(self):
+ """
+ A L{static.File} created for a directory which does not exist should
+ return childNotFound from L{static.File.getChild}.
+ """
+ staticFile = static.File(self.mktemp())
+ request = DummyRequest(['foo.bar'])
+ child = staticFile.getChild("foo.bar", request)
+ self.assertEqual(child, staticFile.childNotFound)
+
+
+ def test_staticFileDeletedRender(self):
+ """
+ A L{static.File} created for a file which does not exist should render
+ its C{childNotFound} page.
+ """
+ staticFile = static.File(self.mktemp())
+ request = DummyRequest(['foo.bar'])
+ request2 = DummyRequest(['foo.bar'])
+ d = self._render(staticFile, request)
+ d2 = self._render(staticFile.childNotFound, request2)
+ def cbRendered2(ignored):
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written),
+ ''.join(request2.written))
+ d.addCallback(cbRendered)
+ return d
+ d2.addCallback(cbRendered2)
+ return d2
+
+
+ def test_headRequest(self):
+ """
+ L{static.File.render} returns an empty response body for I{HEAD}
+ requests.
+ """
+ path = FilePath(self.mktemp())
+ path.setContent("foo")
+ file = static.File(path.path)
+ request = DummyRequest([''])
+ request.method = 'HEAD'
+ d = _render(file, request)
+ def cbRendered(ignored):
+ self.assertEqual("".join(request.written), "")
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_processors(self):
+ """
+ If a request is made which encounters a L{File} before a final segment
+ which names a file with an extension which is in the L{File}'s
+ C{processors} mapping, the processor associated with that extension is
+ used to serve the response to the request.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ base.child("foo.bar").setContent(
+ "from twisted.web.static import Data\n"
+ "resource = Data('dynamic world','text/plain')\n")
+
+ file = static.File(base.path)
+ file.processors = {'.bar': script.ResourceScript}
+ request = DummyRequest(["foo.bar"])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), 'dynamic world')
+ self.assertEqual(request.outgoingHeaders['content-length'], '13')
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_ignoreExt(self):
+ """
+ The list of ignored extensions can be set by passing a value to
+ L{File.__init__} or by calling L{File.ignoreExt} later.
+ """
+ file = static.File(".")
+ self.assertEqual(file.ignoredExts, [])
+ file.ignoreExt(".foo")
+ file.ignoreExt(".bar")
+ self.assertEqual(file.ignoredExts, [".foo", ".bar"])
+
+ file = static.File(".", ignoredExts=(".bar", ".baz"))
+ self.assertEqual(file.ignoredExts, [".bar", ".baz"])
+
+
+ def test_ignoredExtensionsIgnored(self):
+ """
+ A request for the I{base} child of a L{File} succeeds with a resource
+ for the I{base<extension>} file in the path the L{File} was created
+ with if such a file exists and the L{File} has been configured to
+ ignore the I{<extension>} extension.
+ """
+ base = FilePath(self.mktemp())
+ base.makedirs()
+ base.child('foo.bar').setContent('baz')
+ base.child('foo.quux').setContent('foobar')
+ file = static.File(base.path, ignoredExts=(".bar",))
+
+ request = DummyRequest(["foo"])
+ child = resource.getChildForRequest(file, request)
+
+ d = self._render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), 'baz')
+ d.addCallback(cbRendered)
+ return d
+
+
+
+class StaticMakeProducerTests(TestCase):
+ """
+ Tests for L{File.makeProducer}.
+ """
+
+
+ def makeResourceWithContent(self, content, type=None, encoding=None):
+ """
+ Make a L{static.File} resource that has C{content} for its content.
+
+ @param content: The bytes to use as the contents of the resource.
+ @param type: Optional value for the content type of the resource.
+ """
+ fileName = self.mktemp()
+ fileObject = open(fileName, 'w')
+ fileObject.write(content)
+ fileObject.close()
+ resource = static.File(fileName)
+ resource.encoding = encoding
+ resource.type = type
+ return resource
+
+
+ def contentHeaders(self, request):
+ """
+ Extract the content-* headers from the L{DummyRequest} C{request}.
+
+ This returns the subset of C{request.outgoingHeaders} of headers that
+ start with 'content-'.
+ """
+ contentHeaders = {}
+ for k, v in request.outgoingHeaders.iteritems():
+ if k.startswith('content-'):
+ contentHeaders[k] = v
+ return contentHeaders
+
+
+ def test_noRangeHeaderGivesNoRangeStaticProducer(self):
+ """
+ makeProducer when no Range header is set returns an instance of
+ NoRangeStaticProducer.
+ """
+ resource = self.makeResourceWithContent('')
+ request = DummyRequest([])
+ producer = resource.makeProducer(request, resource.openForReading())
+ self.assertIsInstance(producer, static.NoRangeStaticProducer)
+
+
+ def test_noRangeHeaderSets200OK(self):
+ """
+ makeProducer when no Range header is set sets the responseCode on the
+ request to 'OK'.
+ """
+ resource = self.makeResourceWithContent('')
+ request = DummyRequest([])
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(http.OK, request.responseCode)
+
+
+ def test_noRangeHeaderSetsContentHeaders(self):
+ """
+ makeProducer when no Range header is set sets the Content-* headers
+ for the response.
+ """
+ length = 123
+ contentType = "text/plain"
+ contentEncoding = 'gzip'
+ resource = self.makeResourceWithContent(
+ 'a'*length, type=contentType, encoding=contentEncoding)
+ request = DummyRequest([])
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ {'content-type': contentType, 'content-length': str(length),
+ 'content-encoding': contentEncoding},
+ self.contentHeaders(request))
+
+
+ def test_singleRangeGivesSingleRangeStaticProducer(self):
+ """
+ makeProducer when the Range header requests a single byte range
+ returns an instance of SingleRangeStaticProducer.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3'
+ resource = self.makeResourceWithContent('abcdef')
+ producer = resource.makeProducer(request, resource.openForReading())
+ self.assertIsInstance(producer, static.SingleRangeStaticProducer)
+
+
+ def test_singleRangeSets206PartialContent(self):
+ """
+ makeProducer when the Range header requests a single, satisfiable byte
+ range sets the response code on the request to 'Partial Content'.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3'
+ resource = self.makeResourceWithContent('abcdef')
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ http.PARTIAL_CONTENT, request.responseCode)
+
+
+ def test_singleRangeSetsContentHeaders(self):
+ """
+ makeProducer when the Range header requests a single, satisfiable byte
+ range sets the Content-* headers appropriately.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3'
+ contentType = "text/plain"
+ contentEncoding = 'gzip'
+ resource = self.makeResourceWithContent('abcdef', type=contentType, encoding=contentEncoding)
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ {'content-type': contentType, 'content-encoding': contentEncoding,
+ 'content-range': 'bytes 1-3/6', 'content-length': '3'},
+ self.contentHeaders(request))
+
+
+ def test_singleUnsatisfiableRangeReturnsSingleRangeStaticProducer(self):
+ """
+ makeProducer still returns an instance of L{SingleRangeStaticProducer}
+ when the Range header requests a single unsatisfiable byte range.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=4-10'
+ resource = self.makeResourceWithContent('abc')
+ producer = resource.makeProducer(request, resource.openForReading())
+ self.assertIsInstance(producer, static.SingleRangeStaticProducer)
+
+
+ def test_singleUnsatisfiableRangeSets416ReqestedRangeNotSatisfiable(self):
+ """
+ makeProducer sets the response code of the request to of 'Requested
+ Range Not Satisfiable' when the Range header requests a single
+ unsatisfiable byte range.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=4-10'
+ resource = self.makeResourceWithContent('abc')
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ http.REQUESTED_RANGE_NOT_SATISFIABLE, request.responseCode)
+
+
+ def test_singleUnsatisfiableRangeSetsContentHeaders(self):
+ """
+ makeProducer when the Range header requests a single, unsatisfiable
+ byte range sets the Content-* headers appropriately.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=4-10'
+ contentType = "text/plain"
+ resource = self.makeResourceWithContent('abc', type=contentType)
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ {'content-type': 'text/plain', 'content-length': '0',
+ 'content-range': 'bytes */3'},
+ self.contentHeaders(request))
+
+
+ def test_singlePartiallyOverlappingRangeSetsContentHeaders(self):
+ """
+ makeProducer when the Range header requests a single byte range that
+ partly overlaps the resource sets the Content-* headers appropriately.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=2-10'
+ contentType = "text/plain"
+ resource = self.makeResourceWithContent('abc', type=contentType)
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ {'content-type': 'text/plain', 'content-length': '1',
+ 'content-range': 'bytes 2-2/3'},
+ self.contentHeaders(request))
+
+
+ def test_multipleRangeGivesMultipleRangeStaticProducer(self):
+ """
+ makeProducer when the Range header requests a single byte range
+ returns an instance of MultipleRangeStaticProducer.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3,5-6'
+ resource = self.makeResourceWithContent('abcdef')
+ producer = resource.makeProducer(request, resource.openForReading())
+ self.assertIsInstance(producer, static.MultipleRangeStaticProducer)
+
+
+ def test_multipleRangeSets206PartialContent(self):
+ """
+ makeProducer when the Range header requests a multiple satisfiable
+ byte ranges sets the response code on the request to 'Partial
+ Content'.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3,5-6'
+ resource = self.makeResourceWithContent('abcdef')
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ http.PARTIAL_CONTENT, request.responseCode)
+
+
+ def test_mutipleRangeSetsContentHeaders(self):
+ """
+ makeProducer when the Range header requests a single, satisfiable byte
+ range sets the Content-* headers appropriately.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3,5-6'
+ resource = self.makeResourceWithContent(
+ 'abcdefghijkl', encoding='gzip')
+ producer = resource.makeProducer(request, resource.openForReading())
+ contentHeaders = self.contentHeaders(request)
+ # The only content-* headers set are content-type and content-length.
+ self.assertEqual(
+ set(['content-length', 'content-type']),
+ set(contentHeaders.keys()))
+ # The content-length depends on the boundary used in the response.
+ expectedLength = 5
+ for boundary, offset, size in producer.rangeInfo:
+ expectedLength += len(boundary)
+ self.assertEqual(expectedLength, contentHeaders['content-length'])
+ # Content-type should be set to a value indicating a multipart
+ # response and the boundary used to separate the parts.
+ self.assertIn('content-type', contentHeaders)
+ contentType = contentHeaders['content-type']
+ self.assertNotIdentical(
+ None, re.match(
+ 'multipart/byteranges; boundary="[^"]*"\Z', contentType))
+ # Content-encoding is not set in the response to a multiple range
+ # response, which is a bit wussy but works well enough with the way
+ # static.File does content-encodings...
+ self.assertNotIn('content-encoding', contentHeaders)
+
+
+ def test_multipleUnsatisfiableRangesReturnsMultipleRangeStaticProducer(self):
+ """
+ makeProducer still returns an instance of L{SingleRangeStaticProducer}
+ when the Range header requests multiple ranges, none of which are
+ satisfiable.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=10-12,15-20'
+ resource = self.makeResourceWithContent('abc')
+ producer = resource.makeProducer(request, resource.openForReading())
+ self.assertIsInstance(producer, static.MultipleRangeStaticProducer)
+
+
+ def test_multipleUnsatisfiableRangesSets416ReqestedRangeNotSatisfiable(self):
+ """
+ makeProducer sets the response code of the request to of 'Requested
+ Range Not Satisfiable' when the Range header requests multiple ranges,
+ none of which are satisfiable.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=10-12,15-20'
+ resource = self.makeResourceWithContent('abc')
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ http.REQUESTED_RANGE_NOT_SATISFIABLE, request.responseCode)
+
+
+ def test_multipleUnsatisfiableRangeSetsContentHeaders(self):
+ """
+ makeProducer when the Range header requests multiple ranges, none of
+ which are satisfiable, sets the Content-* headers appropriately.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=4-10'
+ contentType = "text/plain"
+ request.headers['range'] = 'bytes=10-12,15-20'
+ resource = self.makeResourceWithContent('abc', type=contentType)
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ {'content-length': '0', 'content-range': 'bytes */3'},
+ self.contentHeaders(request))
+
+
+ def test_oneSatisfiableRangeIsEnough(self):
+ """
+ makeProducer when the Range header requests multiple ranges, at least
+ one of which matches, sets the response code to 'Partial Content'.
+ """
+ request = DummyRequest([])
+ request.headers['range'] = 'bytes=1-3,100-200'
+ resource = self.makeResourceWithContent('abcdef')
+ resource.makeProducer(request, resource.openForReading())
+ self.assertEqual(
+ http.PARTIAL_CONTENT, request.responseCode)
+
+
+
+class StaticProducerTests(TestCase):
+ """
+ Tests for the abstract L{StaticProducer}.
+ """
+
+ def test_stopProducingClosesFile(self):
+ """
+ L{StaticProducer.stopProducing} closes the file object the producer is
+ producing data from.
+ """
+ fileObject = StringIO.StringIO()
+ producer = static.StaticProducer(None, fileObject)
+ producer.stopProducing()
+ self.assertTrue(fileObject.closed)
+
+
+ def test_stopProducingSetsRequestToNone(self):
+ """
+ L{StaticProducer.stopProducing} sets the request instance variable to
+ None, which indicates to subclasses' resumeProducing methods that no
+ more data should be produced.
+ """
+ fileObject = StringIO.StringIO()
+ producer = static.StaticProducer(DummyRequest([]), fileObject)
+ producer.stopProducing()
+ self.assertIdentical(None, producer.request)
+
+
+
+class NoRangeStaticProducerTests(TestCase):
+ """
+ Tests for L{NoRangeStaticProducer}.
+ """
+
+ def test_implementsIPullProducer(self):
+ """
+ L{NoRangeStaticProducer} implements L{IPullProducer}.
+ """
+ verifyObject(
+ interfaces.IPullProducer,
+ static.NoRangeStaticProducer(None, None))
+
+
+ def test_resumeProducingProducesContent(self):
+ """
+ L{NoRangeStaticProducer.resumeProducing} writes content from the
+ resource to the request.
+ """
+ request = DummyRequest([])
+ content = 'abcdef'
+ producer = static.NoRangeStaticProducer(
+ request, StringIO.StringIO(content))
+ # start calls registerProducer on the DummyRequest, which pulls all
+ # output from the producer and so we just need this one call.
+ producer.start()
+ self.assertEqual(content, ''.join(request.written))
+
+
+ def test_resumeProducingBuffersOutput(self):
+ """
+ L{NoRangeStaticProducer.start} writes at most
+ C{abstract.FileDescriptor.bufferSize} bytes of content from the
+ resource to the request at once.
+ """
+ request = DummyRequest([])
+ bufferSize = abstract.FileDescriptor.bufferSize
+ content = 'a' * (2*bufferSize + 1)
+ producer = static.NoRangeStaticProducer(
+ request, StringIO.StringIO(content))
+ # start calls registerProducer on the DummyRequest, which pulls all
+ # output from the producer and so we just need this one call.
+ producer.start()
+ expected = [
+ content[0:bufferSize],
+ content[bufferSize:2*bufferSize],
+ content[2*bufferSize:]
+ ]
+ self.assertEqual(expected, request.written)
+
+
+ def test_finishCalledWhenDone(self):
+ """
+ L{NoRangeStaticProducer.resumeProducing} calls finish() on the request
+ after it is done producing content.
+ """
+ request = DummyRequest([])
+ finishDeferred = request.notifyFinish()
+ callbackList = []
+ finishDeferred.addCallback(callbackList.append)
+ producer = static.NoRangeStaticProducer(
+ request, StringIO.StringIO('abcdef'))
+ # start calls registerProducer on the DummyRequest, which pulls all
+ # output from the producer and so we just need this one call.
+ producer.start()
+ self.assertEqual([None], callbackList)
+
+
+
+class SingleRangeStaticProducerTests(TestCase):
+ """
+ Tests for L{SingleRangeStaticProducer}.
+ """
+
+ def test_implementsIPullProducer(self):
+ """
+ L{SingleRangeStaticProducer} implements L{IPullProducer}.
+ """
+ verifyObject(
+ interfaces.IPullProducer,
+ static.SingleRangeStaticProducer(None, None, None, None))
+
+
+ def test_resumeProducingProducesContent(self):
+ """
+ L{SingleRangeStaticProducer.resumeProducing} writes the given amount
+ of content, starting at the given offset, from the resource to the
+ request.
+ """
+ request = DummyRequest([])
+ content = 'abcdef'
+ producer = static.SingleRangeStaticProducer(
+ request, StringIO.StringIO(content), 1, 3)
+ # DummyRequest.registerProducer pulls all output from the producer, so
+ # we just need to call start.
+ producer.start()
+ self.assertEqual(content[1:4], ''.join(request.written))
+
+
+ def test_resumeProducingBuffersOutput(self):
+ """
+ L{SingleRangeStaticProducer.start} writes at most
+ C{abstract.FileDescriptor.bufferSize} bytes of content from the
+ resource to the request at once.
+ """
+ request = DummyRequest([])
+ bufferSize = abstract.FileDescriptor.bufferSize
+ content = 'abc' * bufferSize
+ producer = static.SingleRangeStaticProducer(
+ request, StringIO.StringIO(content), 1, bufferSize+10)
+ # DummyRequest.registerProducer pulls all output from the producer, so
+ # we just need to call start.
+ producer.start()
+ expected = [
+ content[1:bufferSize+1],
+ content[bufferSize+1:bufferSize+11],
+ ]
+ self.assertEqual(expected, request.written)
+
+
+ def test_finishCalledWhenDone(self):
+ """
+ L{SingleRangeStaticProducer.resumeProducing} calls finish() on the
+ request after it is done producing content.
+ """
+ request = DummyRequest([])
+ finishDeferred = request.notifyFinish()
+ callbackList = []
+ finishDeferred.addCallback(callbackList.append)
+ producer = static.SingleRangeStaticProducer(
+ request, StringIO.StringIO('abcdef'), 1, 1)
+ # start calls registerProducer on the DummyRequest, which pulls all
+ # output from the producer and so we just need this one call.
+ producer.start()
+ self.assertEqual([None], callbackList)
+
+
+
+class MultipleRangeStaticProducerTests(TestCase):
+ """
+ Tests for L{MultipleRangeStaticProducer}.
+ """
+
+ def test_implementsIPullProducer(self):
+ """
+ L{MultipleRangeStaticProducer} implements L{IPullProducer}.
+ """
+ verifyObject(
+ interfaces.IPullProducer,
+ static.MultipleRangeStaticProducer(None, None, None))
+
+
+ def test_resumeProducingProducesContent(self):
+ """
+ L{MultipleRangeStaticProducer.resumeProducing} writes the requested
+ chunks of content from the resource to the request, with the supplied
+ boundaries in between each chunk.
+ """
+ request = DummyRequest([])
+ content = 'abcdef'
+ producer = static.MultipleRangeStaticProducer(
+ request, StringIO.StringIO(content), [('1', 1, 3), ('2', 5, 1)])
+ # DummyRequest.registerProducer pulls all output from the producer, so
+ # we just need to call start.
+ producer.start()
+ self.assertEqual('1bcd2f', ''.join(request.written))
+
+
+ def test_resumeProducingBuffersOutput(self):
+ """
+ L{MultipleRangeStaticProducer.start} writes about
+ C{abstract.FileDescriptor.bufferSize} bytes of content from the
+ resource to the request at once.
+
+ To be specific about the 'about' above: it can write slightly more,
+ for example in the case where the first boundary plus the first chunk
+ is less than C{bufferSize} but first boundary plus the first chunk
+ plus the second boundary is more, but this is unimportant as in
+ practice the boundaries are fairly small. On the other side, it is
+ important for performance to bundle up several small chunks into one
+ call to request.write.
+ """
+ request = DummyRequest([])
+ content = '0123456789' * 2
+ producer = static.MultipleRangeStaticProducer(
+ request, StringIO.StringIO(content),
+ [('a', 0, 2), ('b', 5, 10), ('c', 0, 0)])
+ producer.bufferSize = 10
+ # DummyRequest.registerProducer pulls all output from the producer, so
+ # we just need to call start.
+ producer.start()
+ expected = [
+ 'a' + content[0:2] + 'b' + content[5:11],
+ content[11:15] + 'c',
+ ]
+ self.assertEqual(expected, request.written)
+
+
+ def test_finishCalledWhenDone(self):
+ """
+ L{MultipleRangeStaticProducer.resumeProducing} calls finish() on the
+ request after it is done producing content.
+ """
+ request = DummyRequest([])
+ finishDeferred = request.notifyFinish()
+ callbackList = []
+ finishDeferred.addCallback(callbackList.append)
+ producer = static.MultipleRangeStaticProducer(
+ request, StringIO.StringIO('abcdef'), [('', 1, 2)])
+ # start calls registerProducer on the DummyRequest, which pulls all
+ # output from the producer and so we just need this one call.
+ producer.start()
+ self.assertEqual([None], callbackList)
+
+
+
+class RangeTests(TestCase):
+ """
+ Tests for I{Range-Header} support in L{twisted.web.static.File}.
+
+ @type file: L{file}
+ @ivar file: Temporary (binary) file containing the content to be served.
+
+ @type resource: L{static.File}
+ @ivar resource: A leaf web resource using C{file} as content.
+
+ @type request: L{DummyRequest}
+ @ivar request: A fake request, requesting C{resource}.
+
+ @type catcher: L{list}
+ @ivar catcher: List which gathers all log information.
+ """
+ def setUp(self):
+ """
+ Create a temporary file with a fixed payload of 64 bytes. Create a
+ resource for that file and create a request which will be for that
+ resource. Each test can set a different range header to test different
+ aspects of the implementation.
+ """
+ path = FilePath(self.mktemp())
+ # This is just a jumble of random stuff. It's supposed to be a good
+ # set of data for this test, particularly in order to avoid
+ # accidentally seeing the right result by having a byte sequence
+ # repeated at different locations or by having byte values which are
+ # somehow correlated with their position in the string.
+ self.payload = ('\xf8u\xf3E\x8c7\xce\x00\x9e\xb6a0y0S\xf0\xef\xac\xb7'
+ '\xbe\xb5\x17M\x1e\x136k{\x1e\xbe\x0c\x07\x07\t\xd0'
+ '\xbckY\xf5I\x0b\xb8\x88oZ\x1d\x85b\x1a\xcdk\xf2\x1d'
+ '&\xfd%\xdd\x82q/A\x10Y\x8b')
+ path.setContent(self.payload)
+ self.file = path.open()
+ self.resource = static.File(self.file.name)
+ self.resource.isLeaf = 1
+ self.request = DummyRequest([''])
+ self.request.uri = self.file.name
+ self.catcher = []
+ log.addObserver(self.catcher.append)
+
+
+ def tearDown(self):
+ """
+ Clean up the resource file and the log observer.
+ """
+ self.file.close()
+ log.removeObserver(self.catcher.append)
+
+
+ def _assertLogged(self, expected):
+ """
+ Asserts that a given log message occurred with an expected message.
+ """
+ logItem = self.catcher.pop()
+ self.assertEqual(logItem["message"][0], expected)
+ self.assertEqual(
+ self.catcher, [], "An additional log occured: %r" % (logItem,))
+
+
+ def test_invalidRanges(self):
+ """
+ L{File._parseRangeHeader} raises L{ValueError} when passed
+ syntactically invalid byte ranges.
+ """
+ f = self.resource._parseRangeHeader
+
+ # there's no =
+ self.assertRaises(ValueError, f, 'bytes')
+
+ # unknown isn't a valid Bytes-Unit
+ self.assertRaises(ValueError, f, 'unknown=1-2')
+
+ # there's no - in =stuff
+ self.assertRaises(ValueError, f, 'bytes=3')
+
+ # both start and end are empty
+ self.assertRaises(ValueError, f, 'bytes=-')
+
+ # start isn't an integer
+ self.assertRaises(ValueError, f, 'bytes=foo-')
+
+ # end isn't an integer
+ self.assertRaises(ValueError, f, 'bytes=-foo')
+
+ # end isn't equal to or greater than start
+ self.assertRaises(ValueError, f, 'bytes=5-4')
+
+
+ def test_rangeMissingStop(self):
+ """
+ A single bytes range without an explicit stop position is parsed into a
+ two-tuple giving the start position and C{None}.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=0-'), [(0, None)])
+
+
+ def test_rangeMissingStart(self):
+ """
+ A single bytes range without an explicit start position is parsed into
+ a two-tuple of C{None} and the end position.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=-3'), [(None, 3)])
+
+
+ def test_range(self):
+ """
+ A single bytes range with explicit start and stop positions is parsed
+ into a two-tuple of those positions.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=2-5'), [(2, 5)])
+
+
+ def test_rangeWithSpace(self):
+ """
+ A single bytes range with whitespace in allowed places is parsed in
+ the same way as it would be without the whitespace.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader(' bytes=1-2 '), [(1, 2)])
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes =1-2 '), [(1, 2)])
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes= 1-2'), [(1, 2)])
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=1 -2'), [(1, 2)])
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=1- 2'), [(1, 2)])
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=1-2 '), [(1, 2)])
+
+
+ def test_nullRangeElements(self):
+ """
+ If there are multiple byte ranges but only one is non-null, the
+ non-null range is parsed and its start and stop returned.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=1-2,\r\n, ,\t'), [(1, 2)])
+
+
+ def test_multipleRanges(self):
+ """
+ If multiple byte ranges are specified their starts and stops are
+ returned.
+ """
+ self.assertEqual(
+ self.resource._parseRangeHeader('bytes=1-2,3-4'),
+ [(1, 2), (3, 4)])
+
+
+ def test_bodyLength(self):
+ """
+ A correct response to a range request is as long as the length of the
+ requested range.
+ """
+ self.request.headers['range'] = 'bytes=0-43'
+ self.resource.render(self.request)
+ self.assertEqual(len(''.join(self.request.written)), 44)
+
+
+ def test_invalidRangeRequest(self):
+ """
+ An incorrect range request (RFC 2616 defines a correct range request as
+ a Bytes-Unit followed by a '=' character followed by a specific range.
+ Only 'bytes' is defined) results in the range header value being logged
+ and a normal 200 response being sent.
+ """
+ self.request.headers['range'] = range = 'foobar=0-43'
+ self.resource.render(self.request)
+ expected = "Ignoring malformed Range header %r" % (range,)
+ self._assertLogged(expected)
+ self.assertEqual(''.join(self.request.written), self.payload)
+ self.assertEqual(self.request.responseCode, http.OK)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-length'],
+ str(len(self.payload)))
+
+
+ def parseMultipartBody(self, body, boundary):
+ """
+ Parse C{body} as a multipart MIME response separated by C{boundary}.
+
+ Note that this with fail the calling test on certain syntactic
+ problems.
+ """
+ sep = "\r\n--" + boundary
+ parts = ''.join(body).split(sep)
+ self.assertEqual('', parts[0])
+ self.assertEqual('--\r\n', parts[-1])
+ parsed_parts = []
+ for part in parts[1:-1]:
+ before, header1, header2, blank, partBody = part.split('\r\n', 4)
+ headers = header1 + '\n' + header2
+ self.assertEqual('', before)
+ self.assertEqual('', blank)
+ partContentTypeValue = re.search(
+ '^content-type: (.*)$', headers, re.I|re.M).group(1)
+ start, end, size = re.search(
+ '^content-range: bytes ([0-9]+)-([0-9]+)/([0-9]+)$',
+ headers, re.I|re.M).groups()
+ parsed_parts.append(
+ {'contentType': partContentTypeValue,
+ 'contentRange': (start, end, size),
+ 'body': partBody})
+ return parsed_parts
+
+
+ def test_multipleRangeRequest(self):
+ """
+ The response to a request for multipe bytes ranges is a MIME-ish
+ multipart response.
+ """
+ startEnds = [(0, 2), (20, 30), (40, 50)]
+ rangeHeaderValue = ','.join(["%s-%s"%(s,e) for (s, e) in startEnds])
+ self.request.headers['range'] = 'bytes=' + rangeHeaderValue
+ self.resource.render(self.request)
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ boundary = re.match(
+ '^multipart/byteranges; boundary="(.*)"$',
+ self.request.outgoingHeaders['content-type']).group(1)
+ parts = self.parseMultipartBody(''.join(self.request.written), boundary)
+ self.assertEqual(len(startEnds), len(parts))
+ for part, (s, e) in zip(parts, startEnds):
+ self.assertEqual(self.resource.type, part['contentType'])
+ start, end, size = part['contentRange']
+ self.assertEqual(int(start), s)
+ self.assertEqual(int(end), e)
+ self.assertEqual(int(size), self.resource.getFileSize())
+ self.assertEqual(self.payload[s:e+1], part['body'])
+
+
+ def test_multipleRangeRequestWithRangeOverlappingEnd(self):
+ """
+ The response to a request for multipe bytes ranges is a MIME-ish
+ multipart response, even when one of the ranged falls off the end of
+ the resource.
+ """
+ startEnds = [(0, 2), (40, len(self.payload) + 10)]
+ rangeHeaderValue = ','.join(["%s-%s"%(s,e) for (s, e) in startEnds])
+ self.request.headers['range'] = 'bytes=' + rangeHeaderValue
+ self.resource.render(self.request)
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ boundary = re.match(
+ '^multipart/byteranges; boundary="(.*)"$',
+ self.request.outgoingHeaders['content-type']).group(1)
+ parts = self.parseMultipartBody(''.join(self.request.written), boundary)
+ self.assertEqual(len(startEnds), len(parts))
+ for part, (s, e) in zip(parts, startEnds):
+ self.assertEqual(self.resource.type, part['contentType'])
+ start, end, size = part['contentRange']
+ self.assertEqual(int(start), s)
+ self.assertEqual(int(end), min(e, self.resource.getFileSize()-1))
+ self.assertEqual(int(size), self.resource.getFileSize())
+ self.assertEqual(self.payload[s:e+1], part['body'])
+
+
+ def test_implicitEnd(self):
+ """
+ If the end byte position is omitted, then it is treated as if the
+ length of the resource was specified by the end byte position.
+ """
+ self.request.headers['range'] = 'bytes=23-'
+ self.resource.render(self.request)
+ self.assertEqual(''.join(self.request.written), self.payload[23:])
+ self.assertEqual(len(''.join(self.request.written)), 41)
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-range'], 'bytes 23-63/64')
+ self.assertEqual(self.request.outgoingHeaders['content-length'], '41')
+
+
+ def test_implicitStart(self):
+ """
+ If the start byte position is omitted but the end byte position is
+ supplied, then the range is treated as requesting the last -N bytes of
+ the resource, where N is the end byte position.
+ """
+ self.request.headers['range'] = 'bytes=-17'
+ self.resource.render(self.request)
+ self.assertEqual(''.join(self.request.written), self.payload[-17:])
+ self.assertEqual(len(''.join(self.request.written)), 17)
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-range'], 'bytes 47-63/64')
+ self.assertEqual(self.request.outgoingHeaders['content-length'], '17')
+
+
+ def test_explicitRange(self):
+ """
+ A correct response to a bytes range header request from A to B starts
+ with the A'th byte and ends with (including) the B'th byte. The first
+ byte of a page is numbered with 0.
+ """
+ self.request.headers['range'] = 'bytes=3-43'
+ self.resource.render(self.request)
+ written = ''.join(self.request.written)
+ self.assertEqual(written, self.payload[3:44])
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-range'], 'bytes 3-43/64')
+ self.assertEqual(
+ str(len(written)), self.request.outgoingHeaders['content-length'])
+
+
+ def test_explicitRangeOverlappingEnd(self):
+ """
+ A correct response to a bytes range header request from A to B when B
+ is past the end of the resource starts with the A'th byte and ends
+ with the last byte of the resource. The first byte of a page is
+ numbered with 0.
+ """
+ self.request.headers['range'] = 'bytes=40-100'
+ self.resource.render(self.request)
+ written = ''.join(self.request.written)
+ self.assertEqual(written, self.payload[40:])
+ self.assertEqual(self.request.responseCode, http.PARTIAL_CONTENT)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-range'], 'bytes 40-63/64')
+ self.assertEqual(
+ str(len(written)), self.request.outgoingHeaders['content-length'])
+
+
+ def test_statusCodeRequestedRangeNotSatisfiable(self):
+ """
+ If a range is syntactically invalid due to the start being greater than
+ the end, the range header is ignored (the request is responded to as if
+ it were not present).
+ """
+ self.request.headers['range'] = 'bytes=20-13'
+ self.resource.render(self.request)
+ self.assertEqual(self.request.responseCode, http.OK)
+ self.assertEqual(''.join(self.request.written), self.payload)
+ self.assertEqual(
+ self.request.outgoingHeaders['content-length'],
+ str(len(self.payload)))
+
+
+ def test_invalidStartBytePos(self):
+ """
+ If a range is unsatisfiable due to the start not being less than the
+ length of the resource, the response is 416 (Requested range not
+ satisfiable) and no data is written to the response body (RFC 2616,
+ section 14.35.1).
+ """
+ self.request.headers['range'] = 'bytes=67-108'
+ self.resource.render(self.request)
+ self.assertEqual(
+ self.request.responseCode, http.REQUESTED_RANGE_NOT_SATISFIABLE)
+ self.assertEqual(''.join(self.request.written), '')
+ self.assertEqual(self.request.outgoingHeaders['content-length'], '0')
+ # Sections 10.4.17 and 14.16
+ self.assertEqual(
+ self.request.outgoingHeaders['content-range'],
+ 'bytes */%d' % (len(self.payload),))
+
+
+
+class DirectoryListerTest(TestCase):
+ """
+ Tests for L{static.DirectoryLister}.
+ """
+ def _request(self, uri):
+ request = DummyRequest([''])
+ request.uri = uri
+ return request
+
+
+ def test_renderHeader(self):
+ """
+ L{static.DirectoryLister} prints the request uri as header of the
+ rendered content.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+
+ lister = static.DirectoryLister(path.path)
+ data = lister.render(self._request('foo'))
+ self.assertIn("<h1>Directory listing for foo</h1>", data)
+ self.assertIn("<title>Directory listing for foo</title>", data)
+
+
+ def test_renderUnquoteHeader(self):
+ """
+ L{static.DirectoryLister} unquote the request uri before printing it.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+
+ lister = static.DirectoryLister(path.path)
+ data = lister.render(self._request('foo%20bar'))
+ self.assertIn("<h1>Directory listing for foo bar</h1>", data)
+ self.assertIn("<title>Directory listing for foo bar</title>", data)
+
+
+ def test_escapeHeader(self):
+ """
+ L{static.DirectoryLister} escape "&", "<" and ">" after unquoting the
+ request uri.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+
+ lister = static.DirectoryLister(path.path)
+ data = lister.render(self._request('foo%26bar'))
+ self.assertIn("<h1>Directory listing for foo&amp;bar</h1>", data)
+ self.assertIn("<title>Directory listing for foo&amp;bar</title>", data)
+
+
+ def test_renderFiles(self):
+ """
+ L{static.DirectoryLister} is able to list all the files inside a
+ directory.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ path.child('file1').setContent("content1")
+ path.child('file2').setContent("content2" * 1000)
+
+ lister = static.DirectoryLister(path.path)
+ data = lister.render(self._request('foo'))
+ body = """<tr class="odd">
+ <td><a href="file1">file1</a></td>
+ <td>8B</td>
+ <td>[text/html]</td>
+ <td></td>
+</tr>
+<tr class="even">
+ <td><a href="file2">file2</a></td>
+ <td>7K</td>
+ <td>[text/html]</td>
+ <td></td>
+</tr>"""
+ self.assertIn(body, data)
+
+
+ def test_renderDirectories(self):
+ """
+ L{static.DirectoryLister} is able to list all the directories inside
+ a directory.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ path.child('dir1').makedirs()
+ path.child('dir2 & 3').makedirs()
+
+ lister = static.DirectoryLister(path.path)
+ data = lister.render(self._request('foo'))
+ body = """<tr class="odd">
+ <td><a href="dir1/">dir1/</a></td>
+ <td></td>
+ <td>[Directory]</td>
+ <td></td>
+</tr>
+<tr class="even">
+ <td><a href="dir2%20%26%203/">dir2 &amp; 3/</a></td>
+ <td></td>
+ <td>[Directory]</td>
+ <td></td>
+</tr>"""
+ self.assertIn(body, data)
+
+
+ def test_renderFiltered(self):
+ """
+ L{static.DirectoryLister} takes a optional C{dirs} argument that
+ filter out the list of of directories and files printed.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ path.child('dir1').makedirs()
+ path.child('dir2').makedirs()
+ path.child('dir3').makedirs()
+ lister = static.DirectoryLister(path.path, dirs=["dir1", "dir3"])
+ data = lister.render(self._request('foo'))
+ body = """<tr class="odd">
+ <td><a href="dir1/">dir1/</a></td>
+ <td></td>
+ <td>[Directory]</td>
+ <td></td>
+</tr>
+<tr class="even">
+ <td><a href="dir3/">dir3/</a></td>
+ <td></td>
+ <td>[Directory]</td>
+ <td></td>
+</tr>"""
+ self.assertIn(body, data)
+
+
+ def test_oddAndEven(self):
+ """
+ L{static.DirectoryLister} gives an alternate class for each odd and
+ even rows in the table.
+ """
+ lister = static.DirectoryLister(None)
+ elements = [{"href": "", "text": "", "size": "", "type": "",
+ "encoding": ""} for i in xrange(5)]
+ content = lister._buildTableContent(elements)
+
+ self.assertEqual(len(content), 5)
+ self.assertTrue(content[0].startswith('<tr class="odd">'))
+ self.assertTrue(content[1].startswith('<tr class="even">'))
+ self.assertTrue(content[2].startswith('<tr class="odd">'))
+ self.assertTrue(content[3].startswith('<tr class="even">'))
+ self.assertTrue(content[4].startswith('<tr class="odd">'))
+
+
+ def test_contentType(self):
+ """
+ L{static.DirectoryLister} produces a MIME-type that indicates that it is
+ HTML, and includes its charset (UTF-8).
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ lister = static.DirectoryLister(path.path)
+ req = self._request('')
+ lister.render(req)
+ self.assertEqual(req.outgoingHeaders['content-type'],
+ "text/html; charset=utf-8")
+
+
+ def test_mimeTypeAndEncodings(self):
+ """
+ L{static.DirectoryLister} is able to detect mimetype and encoding of
+ listed files.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ path.child('file1.txt').setContent("file1")
+ path.child('file2.py').setContent("python")
+ path.child('file3.conf.gz').setContent("conf compressed")
+ path.child('file4.diff.bz2').setContent("diff compressed")
+ directory = os.listdir(path.path)
+ directory.sort()
+
+ contentTypes = {
+ ".txt": "text/plain",
+ ".py": "text/python",
+ ".conf": "text/configuration",
+ ".diff": "text/diff"
+ }
+
+ lister = static.DirectoryLister(path.path, contentTypes=contentTypes)
+ dirs, files = lister._getFilesAndDirectories(directory)
+ self.assertEqual(dirs, [])
+ self.assertEqual(files, [
+ {'encoding': '',
+ 'href': 'file1.txt',
+ 'size': '5B',
+ 'text': 'file1.txt',
+ 'type': '[text/plain]'},
+ {'encoding': '',
+ 'href': 'file2.py',
+ 'size': '6B',
+ 'text': 'file2.py',
+ 'type': '[text/python]'},
+ {'encoding': '[gzip]',
+ 'href': 'file3.conf.gz',
+ 'size': '15B',
+ 'text': 'file3.conf.gz',
+ 'type': '[text/configuration]'},
+ {'encoding': '[bzip2]',
+ 'href': 'file4.diff.bz2',
+ 'size': '15B',
+ 'text': 'file4.diff.bz2',
+ 'type': '[text/diff]'}])
+
+
+ def test_brokenSymlink(self):
+ """
+ If on the file in the listing points to a broken symlink, it should not
+ be returned by L{static.DirectoryLister._getFilesAndDirectories}.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ file1 = path.child('file1')
+ file1.setContent("file1")
+ file1.linkTo(path.child("file2"))
+ file1.remove()
+
+ lister = static.DirectoryLister(path.path)
+ directory = os.listdir(path.path)
+ directory.sort()
+ dirs, files = lister._getFilesAndDirectories(directory)
+ self.assertEqual(dirs, [])
+ self.assertEqual(files, [])
+
+ if getattr(os, "symlink", None) is None:
+ test_brokenSymlink.skip = "No symlink support"
+
+
+ def test_childrenNotFound(self):
+ """
+ Any child resource of L{static.DirectoryLister} renders an HTTP
+ I{NOT FOUND} response code.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ lister = static.DirectoryLister(path.path)
+ request = self._request('')
+ child = resource.getChildForRequest(lister, request)
+ result = _render(child, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, http.NOT_FOUND)
+ result.addCallback(cbRendered)
+ return result
+
+
+ def test_repr(self):
+ """
+ L{static.DirectoryLister.__repr__} gives the path of the lister.
+ """
+ path = FilePath(self.mktemp())
+ lister = static.DirectoryLister(path.path)
+ self.assertEqual(repr(lister),
+ "<DirectoryLister of %r>" % (path.path,))
+ self.assertEqual(str(lister),
+ "<DirectoryLister of %r>" % (path.path,))
+
+ def test_formatFileSize(self):
+ """
+ L{static.formatFileSize} format an amount of bytes into a more readable
+ format.
+ """
+ self.assertEqual(static.formatFileSize(0), "0B")
+ self.assertEqual(static.formatFileSize(123), "123B")
+ self.assertEqual(static.formatFileSize(4567), "4K")
+ self.assertEqual(static.formatFileSize(8900000), "8M")
+ self.assertEqual(static.formatFileSize(1234000000), "1G")
+ self.assertEqual(static.formatFileSize(1234567890000), "1149G")
+
+
+
+class TestFileTransferDeprecated(TestCase):
+ """
+ L{static.FileTransfer} is deprecated.
+ """
+
+ def test_deprecation(self):
+ """
+ Instantiation of L{FileTransfer} produces a deprecation warning.
+ """
+ static.FileTransfer(StringIO.StringIO(), 0, DummyRequest([]))
+ warnings = self.flushWarnings([self.test_deprecation])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ 'FileTransfer is deprecated since Twisted 9.0. '
+ 'Use a subclass of StaticProducer instead.')
diff --git a/twisted/web/test/test_tap.py b/twisted/web/test/test_tap.py
new file mode 100644
index 0000000..a3e33da
--- /dev/null
+++ b/twisted/web/test/test_tap.py
@@ -0,0 +1,196 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.tap}.
+"""
+
+import os, stat
+
+from twisted.python.usage import UsageError
+from twisted.python.filepath import FilePath
+from twisted.internet.interfaces import IReactorUNIX
+from twisted.internet import reactor
+from twisted.python.threadpool import ThreadPool
+from twisted.trial.unittest import TestCase
+from twisted.application import strports
+
+from twisted.web.server import Site
+from twisted.web.static import Data, File
+from twisted.web.distrib import ResourcePublisher, UserDirectory
+from twisted.web.wsgi import WSGIResource
+from twisted.web.tap import Options, makePersonalServerFactory, makeService
+from twisted.web.twcgi import CGIScript
+from twisted.web.script import PythonScript
+
+
+from twisted.spread.pb import PBServerFactory
+
+application = object()
+
+class ServiceTests(TestCase):
+ """
+ Tests for the service creation APIs in L{twisted.web.tap}.
+ """
+ def _pathOption(self):
+ """
+ Helper for the I{--path} tests which creates a directory and creates
+ an L{Options} object which uses that directory as its static
+ filesystem root.
+
+ @return: A two-tuple of a L{FilePath} referring to the directory and
+ the value associated with the C{'root'} key in the L{Options}
+ instance after parsing a I{--path} option.
+ """
+ path = FilePath(self.mktemp())
+ path.makedirs()
+ options = Options()
+ options.parseOptions(['--path', path.path])
+ root = options['root']
+ return path, root
+
+
+ def test_path(self):
+ """
+ The I{--path} option causes L{Options} to create a root resource
+ which serves responses from the specified path.
+ """
+ path, root = self._pathOption()
+ self.assertIsInstance(root, File)
+ self.assertEqual(root.path, path.path)
+
+
+ def test_cgiProcessor(self):
+ """
+ The I{--path} option creates a root resource which serves a
+ L{CGIScript} instance for any child with the C{".cgi"} extension.
+ """
+ path, root = self._pathOption()
+ path.child("foo.cgi").setContent("")
+ self.assertIsInstance(root.getChild("foo.cgi", None), CGIScript)
+
+
+ def test_epyProcessor(self):
+ """
+ The I{--path} option creates a root resource which serves a
+ L{PythonScript} instance for any child with the C{".epy"} extension.
+ """
+ path, root = self._pathOption()
+ path.child("foo.epy").setContent("")
+ self.assertIsInstance(root.getChild("foo.epy", None), PythonScript)
+
+
+ def test_rpyProcessor(self):
+ """
+ The I{--path} option creates a root resource which serves the
+ C{resource} global defined by the Python source in any child with
+ the C{".rpy"} extension.
+ """
+ path, root = self._pathOption()
+ path.child("foo.rpy").setContent(
+ "from twisted.web.static import Data\n"
+ "resource = Data('content', 'major/minor')\n")
+ child = root.getChild("foo.rpy", None)
+ self.assertIsInstance(child, Data)
+ self.assertEqual(child.data, 'content')
+ self.assertEqual(child.type, 'major/minor')
+
+
+ def test_makePersonalServerFactory(self):
+ """
+ L{makePersonalServerFactory} returns a PB server factory which has
+ as its root object a L{ResourcePublisher}.
+ """
+ # The fact that this pile of objects can actually be used somehow is
+ # verified by twisted.web.test.test_distrib.
+ site = Site(Data("foo bar", "text/plain"))
+ serverFactory = makePersonalServerFactory(site)
+ self.assertIsInstance(serverFactory, PBServerFactory)
+ self.assertIsInstance(serverFactory.root, ResourcePublisher)
+ self.assertIdentical(serverFactory.root.site, site)
+
+
+ def test_personalServer(self):
+ """
+ The I{--personal} option to L{makeService} causes it to return a
+ service which will listen on the server address given by the I{--port}
+ option.
+ """
+ port = self.mktemp()
+ options = Options()
+ options.parseOptions(['--port', 'unix:' + port, '--personal'])
+ service = makeService(options)
+ service.startService()
+ self.addCleanup(service.stopService)
+ self.assertTrue(os.path.exists(port))
+ self.assertTrue(stat.S_ISSOCK(os.stat(port).st_mode))
+
+ if not IReactorUNIX.providedBy(reactor):
+ test_personalServer.skip = (
+ "The reactor does not support UNIX domain sockets")
+
+
+ def test_defaultPersonalPath(self):
+ """
+ If the I{--port} option not specified but the I{--personal} option is,
+ L{Options} defaults the port to C{UserDirectory.userSocketName} in the
+ user's home directory.
+ """
+ options = Options()
+ options.parseOptions(['--personal'])
+ path = os.path.expanduser(
+ os.path.join('~', UserDirectory.userSocketName))
+ self.assertEqual(
+ strports.parse(options['port'], None)[:2],
+ ('UNIX', (path, None)))
+
+ if not IReactorUNIX.providedBy(reactor):
+ test_defaultPersonalPath.skip = (
+ "The reactor does not support UNIX domain sockets")
+
+
+ def test_defaultPort(self):
+ """
+ If the I{--port} option is not specified, L{Options} defaults the port
+ to C{8080}.
+ """
+ options = Options()
+ options.parseOptions([])
+ self.assertEqual(
+ strports.parse(options['port'], None)[:2],
+ ('TCP', (8080, None)))
+
+
+ def test_wsgi(self):
+ """
+ The I{--wsgi} option takes the fully-qualifed Python name of a WSGI
+ application object and creates a L{WSGIResource} at the root which
+ serves that application.
+ """
+ options = Options()
+ options.parseOptions(['--wsgi', __name__ + '.application'])
+ root = options['root']
+ self.assertTrue(root, WSGIResource)
+ self.assertIdentical(root._reactor, reactor)
+ self.assertTrue(isinstance(root._threadpool, ThreadPool))
+ self.assertIdentical(root._application, application)
+
+ # The threadpool should start and stop with the reactor.
+ self.assertFalse(root._threadpool.started)
+ reactor.fireSystemEvent('startup')
+ self.assertTrue(root._threadpool.started)
+ self.assertFalse(root._threadpool.joined)
+ reactor.fireSystemEvent('shutdown')
+ self.assertTrue(root._threadpool.joined)
+
+
+ def test_invalidApplication(self):
+ """
+ If I{--wsgi} is given an invalid name, L{Options.parseOptions}
+ raises L{UsageError}.
+ """
+ options = Options()
+ for name in [__name__ + '.nosuchthing', 'foo.']:
+ exc = self.assertRaises(
+ UsageError, options.parseOptions, ['--wsgi', name])
+ self.assertEqual(str(exc), "No such WSGI application: %r" % (name,))
diff --git a/twisted/web/test/test_template.py b/twisted/web/test/test_template.py
new file mode 100644
index 0000000..2e2ab94
--- /dev/null
+++ b/twisted/web/test/test_template.py
@@ -0,0 +1,810 @@
+
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Tests for L{twisted.web.template}
+"""
+
+from cStringIO import StringIO
+
+from zope.interface.verify import verifyObject
+
+from twisted.internet.defer import succeed, gatherResults
+from twisted.python.filepath import FilePath
+from twisted.trial.unittest import TestCase
+from twisted.web.template import (
+ Element, TagLoader, renderer, tags, XMLFile, XMLString)
+from twisted.web.iweb import ITemplateLoader
+
+from twisted.web.error import (FlattenerError, MissingTemplateLoader,
+ MissingRenderMethod)
+
+from twisted.web.template import renderElement
+from twisted.web._element import UnexposedMethodError
+from twisted.web.test._util import FlattenTestCase
+from twisted.web.test.test_web import DummyRequest
+from twisted.web.server import NOT_DONE_YET
+
+class TagFactoryTests(TestCase):
+ """
+ Tests for L{_TagFactory} through the publicly-exposed L{tags} object.
+ """
+ def test_lookupTag(self):
+ """
+ HTML tags can be retrieved through C{tags}.
+ """
+ tag = tags.a
+ self.assertEqual(tag.tagName, "a")
+
+
+ def test_lookupHTML5Tag(self):
+ """
+ Twisted supports the latest and greatest HTML tags from the HTML5
+ specification.
+ """
+ tag = tags.video
+ self.assertEqual(tag.tagName, "video")
+
+
+ def test_lookupTransparentTag(self):
+ """
+ To support transparent inclusion in templates, there is a special tag,
+ the transparent tag, which has no name of its own but is accessed
+ through the "transparent" attribute.
+ """
+ tag = tags.transparent
+ self.assertEqual(tag.tagName, "")
+
+
+ def test_lookupInvalidTag(self):
+ """
+ Invalid tags which are not part of HTML cause AttributeErrors when
+ accessed through C{tags}.
+ """
+ self.assertRaises(AttributeError, getattr, tags, "invalid")
+
+
+ def test_lookupXMP(self):
+ """
+ As a special case, the <xmp> tag is simply not available through
+ C{tags} or any other part of the templating machinery.
+ """
+ self.assertRaises(AttributeError, getattr, tags, "xmp")
+
+
+
+class ElementTests(TestCase):
+ """
+ Tests for the awesome new L{Element} class.
+ """
+ def test_missingTemplateLoader(self):
+ """
+ L{Element.render} raises L{MissingTemplateLoader} if the C{loader}
+ attribute is C{None}.
+ """
+ element = Element()
+ err = self.assertRaises(MissingTemplateLoader, element.render, None)
+ self.assertIdentical(err.element, element)
+
+
+ def test_missingTemplateLoaderRepr(self):
+ """
+ A L{MissingTemplateLoader} instance can be repr()'d without error.
+ """
+ class PrettyReprElement(Element):
+ def __repr__(self):
+ return 'Pretty Repr Element'
+ self.assertIn('Pretty Repr Element',
+ repr(MissingTemplateLoader(PrettyReprElement())))
+
+
+ def test_missingRendererMethod(self):
+ """
+ When called with the name which is not associated with a render method,
+ L{Element.lookupRenderMethod} raises L{MissingRenderMethod}.
+ """
+ element = Element()
+ err = self.assertRaises(
+ MissingRenderMethod, element.lookupRenderMethod, "foo")
+ self.assertIdentical(err.element, element)
+ self.assertEqual(err.renderName, "foo")
+
+
+ def test_missingRenderMethodRepr(self):
+ """
+ A L{MissingRenderMethod} instance can be repr()'d without error.
+ """
+ class PrettyReprElement(Element):
+ def __repr__(self):
+ return 'Pretty Repr Element'
+ s = repr(MissingRenderMethod(PrettyReprElement(),
+ 'expectedMethod'))
+ self.assertIn('Pretty Repr Element', s)
+ self.assertIn('expectedMethod', s)
+
+
+ def test_definedRenderer(self):
+ """
+ When called with the name of a defined render method,
+ L{Element.lookupRenderMethod} returns that render method.
+ """
+ class ElementWithRenderMethod(Element):
+ @renderer
+ def foo(self, request, tag):
+ return "bar"
+ foo = ElementWithRenderMethod().lookupRenderMethod("foo")
+ self.assertEqual(foo(None, None), "bar")
+
+
+ def test_render(self):
+ """
+ L{Element.render} loads a document from the C{loader} attribute and
+ returns it.
+ """
+ class TemplateLoader(object):
+ def load(self):
+ return "result"
+
+ class StubElement(Element):
+ loader = TemplateLoader()
+
+ element = StubElement()
+ self.assertEqual(element.render(None), "result")
+
+
+ def test_misuseRenderer(self):
+ """
+ If the L{renderer} decorator is called without any arguments, it will
+ raise a comprehensible exception.
+ """
+ te = self.assertRaises(TypeError, renderer)
+ self.assertEqual(str(te),
+ "expose() takes at least 1 argument (0 given)")
+
+
+ def test_renderGetDirectlyError(self):
+ """
+ Called directly, without a default, L{renderer.get} raises
+ L{UnexposedMethodError} when it cannot find a renderer.
+ """
+ self.assertRaises(UnexposedMethodError, renderer.get, None,
+ "notARenderer")
+
+
+
+class XMLFileReprTests(TestCase):
+ """
+ Tests for L{twisted.web.template.XMLFile}'s C{__repr__}.
+ """
+ def test_filePath(self):
+ """
+ An L{XMLFile} with a L{FilePath} returns a useful repr().
+ """
+ path = FilePath("/tmp/fake.xml")
+ self.assertEqual('<XMLFile of %r>' % (path,), repr(XMLFile(path)))
+
+
+ def test_filename(self):
+ """
+ An L{XMLFile} with a filename returns a useful repr().
+ """
+ fname = "/tmp/fake.xml"
+ self.assertEqual('<XMLFile of %r>' % (fname,), repr(XMLFile(fname)))
+
+
+ def test_file(self):
+ """
+ An L{XMLFile} with a file object returns a useful repr().
+ """
+ fobj = StringIO("not xml")
+ self.assertEqual('<XMLFile of %r>' % (fobj,), repr(XMLFile(fobj)))
+
+
+
+class XMLLoaderTestsMixin(object):
+ """
+ @ivar templateString: Simple template to use to excercise the loaders.
+
+ @ivar deprecatedUse: C{True} if this use of L{XMLFile} is deprecated and
+ should emit a C{DeprecationWarning}.
+ """
+
+ loaderFactory = None
+ templateString = '<p>Hello, world.</p>'
+ def test_load(self):
+ """
+ Verify that the loader returns a tag with the correct children.
+ """
+ loader = self.loaderFactory()
+ tag, = loader.load()
+
+ warnings = self.flushWarnings(offendingFunctions=[self.loaderFactory])
+ if self.deprecatedUse:
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Passing filenames or file objects to XMLFile is "
+ "deprecated since Twisted 12.1. Pass a FilePath instead.")
+ else:
+ self.assertEqual(len(warnings), 0)
+
+ self.assertEqual(tag.tagName, 'p')
+ self.assertEqual(tag.children, [u'Hello, world.'])
+
+
+ def test_loadTwice(self):
+ """
+ If {load()} can be called on a loader twice the result should be the
+ same.
+ """
+ loader = self.loaderFactory()
+ tags1 = loader.load()
+ tags2 = loader.load()
+ self.assertEqual(tags1, tags2)
+
+
+
+class XMLStringLoaderTests(TestCase, XMLLoaderTestsMixin):
+ """
+ Tests for L{twisted.web.template.XMLString}
+ """
+ deprecatedUse = False
+ def loaderFactory(self):
+ """
+ @return: an L{XMLString} constructed with C{self.templateString}.
+ """
+ return XMLString(self.templateString)
+
+
+
+class XMLFileWithFilePathTests(TestCase, XMLLoaderTestsMixin):
+ """
+ Tests for L{twisted.web.template.XMLFile}'s L{FilePath} support.
+ """
+ deprecatedUse = False
+ def loaderFactory(self):
+ """
+ @return: an L{XMLString} constructed with a L{FilePath} pointing to a
+ file that contains C{self.templateString}.
+ """
+ fp = FilePath(self.mktemp())
+ fp.setContent(self.templateString)
+ return XMLFile(fp)
+
+
+
+class XMLFileWithFileTests(TestCase, XMLLoaderTestsMixin):
+ """
+ Tests for L{twisted.web.template.XMLFile}'s deprecated file object support.
+ """
+ deprecatedUse = True
+ def loaderFactory(self):
+ """
+ @return: an L{XMLString} constructed with a file object that contains
+ C{self.templateString}.
+ """
+ return XMLFile(StringIO(self.templateString))
+
+
+
+class XMLFileWithFilenameTests(TestCase, XMLLoaderTestsMixin):
+ """
+ Tests for L{twisted.web.template.XMLFile}'s deprecated filename support.
+ """
+ deprecatedUse = True
+ def loaderFactory(self):
+ """
+ @return: an L{XMLString} constructed with a filename that points to a
+ file containing C{self.templateString}.
+ """
+ fp = FilePath(self.mktemp())
+ fp.setContent(self.templateString)
+ return XMLFile(fp.path)
+
+
+
+class FlattenIntegrationTests(FlattenTestCase):
+ """
+ Tests for integration between L{Element} and
+ L{twisted.web._flatten.flatten}.
+ """
+
+ def test_roundTrip(self):
+ """
+ Given a series of parsable XML strings, verify that
+ L{twisted.web._flatten.flatten} will flatten the L{Element} back to the
+ input when sent on a round trip.
+ """
+ fragments = [
+ "<p>Hello, world.</p>",
+ "<p><!-- hello, world --></p>",
+ "<p><![CDATA[Hello, world.]]></p>",
+ '<test1 xmlns:test2="urn:test2">'
+ '<test2:test3></test2:test3></test1>',
+ '<test1 xmlns="urn:test2"><test3></test3></test1>',
+ '<p>\xe2\x98\x83</p>',
+ ]
+ deferreds = [
+ self.assertFlattensTo(Element(loader=XMLString(xml)), xml)
+ for xml in fragments]
+ return gatherResults(deferreds)
+
+
+ def test_entityConversion(self):
+ """
+ When flattening an HTML entity, it should flatten out to the utf-8
+ representation if possible.
+ """
+ element = Element(loader=XMLString('<p>&#9731;</p>'))
+ return self.assertFlattensTo(element, '<p>\xe2\x98\x83</p>')
+
+
+ def test_missingTemplateLoader(self):
+ """
+ Rendering a Element without a loader attribute raises the appropriate
+ exception.
+ """
+ return self.assertFlatteningRaises(Element(), MissingTemplateLoader)
+
+
+ def test_missingRenderMethod(self):
+ """
+ Flattening an L{Element} with a C{loader} which has a tag with a render
+ directive fails with L{FlattenerError} if there is no available render
+ method to satisfy that directive.
+ """
+ element = Element(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="unknownMethod" />
+ """))
+ return self.assertFlatteningRaises(element, MissingRenderMethod)
+
+
+ def test_transparentRendering(self):
+ """
+ A C{transparent} element should be eliminated from the DOM and rendered as
+ only its children.
+ """
+ element = Element(loader=XMLString(
+ '<t:transparent '
+ 'xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
+ 'Hello, world.'
+ '</t:transparent>'
+ ))
+ return self.assertFlattensTo(element, "Hello, world.")
+
+
+ def test_attrRendering(self):
+ """
+ An Element with an attr tag renders the vaule of its attr tag as an
+ attribute of its containing tag.
+ """
+ element = Element(loader=XMLString(
+ '<a xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
+ '<t:attr name="href">http://example.com</t:attr>'
+ 'Hello, world.'
+ '</a>'
+ ))
+ return self.assertFlattensTo(element,
+ '<a href="http://example.com">Hello, world.</a>')
+
+
+ def test_errorToplevelAttr(self):
+ """
+ A template with a toplevel C{attr} tag will not load; it will raise
+ L{AssertionError} if you try.
+ """
+ self.assertRaises(
+ AssertionError,
+ XMLString,
+ """<t:attr
+ xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
+ name='something'
+ >hello</t:attr>
+ """)
+
+
+ def test_errorUnnamedAttr(self):
+ """
+ A template with an C{attr} tag with no C{name} attribute will not load;
+ it will raise L{AssertionError} if you try.
+ """
+ self.assertRaises(
+ AssertionError,
+ XMLString,
+ """<html><t:attr
+ xmlns:t='http://twistedmatrix.com/ns/twisted.web.template/0.1'
+ >hello</t:attr></html>""")
+
+
+ def test_lenientPrefixBehavior(self):
+ """
+ If the parser sees a prefix it doesn't recognize on an attribute, it
+ will pass it on through to serialization.
+ """
+ theInput = (
+ '<hello:world hello:sample="testing" '
+ 'xmlns:hello="http://made-up.example.com/ns/not-real">'
+ 'This is a made-up tag.</hello:world>')
+ element = Element(loader=XMLString(theInput))
+ self.assertFlattensTo(element, theInput)
+
+
+ def test_deferredRendering(self):
+ """
+ An Element with a render method which returns a Deferred will render
+ correctly.
+ """
+ class RenderfulElement(Element):
+ @renderer
+ def renderMethod(self, request, tag):
+ return succeed("Hello, world.")
+ element = RenderfulElement(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="renderMethod">
+ Goodbye, world.
+ </p>
+ """))
+ return self.assertFlattensTo(element, "Hello, world.")
+
+
+ def test_loaderClassAttribute(self):
+ """
+ If there is a non-None loader attribute on the class of an Element
+ instance but none on the instance itself, the class attribute is used.
+ """
+ class SubElement(Element):
+ loader = XMLString("<p>Hello, world.</p>")
+ return self.assertFlattensTo(SubElement(), "<p>Hello, world.</p>")
+
+
+ def test_directiveRendering(self):
+ """
+ An Element with a valid render directive has that directive invoked and
+ the result added to the output.
+ """
+ renders = []
+ class RenderfulElement(Element):
+ @renderer
+ def renderMethod(self, request, tag):
+ renders.append((self, request))
+ return tag("Hello, world.")
+ element = RenderfulElement(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="renderMethod" />
+ """))
+ return self.assertFlattensTo(element, "<p>Hello, world.</p>")
+
+
+ def test_directiveRenderingOmittingTag(self):
+ """
+ An Element with a render method which omits the containing tag
+ successfully removes that tag from the output.
+ """
+ class RenderfulElement(Element):
+ @renderer
+ def renderMethod(self, request, tag):
+ return "Hello, world."
+ element = RenderfulElement(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="renderMethod">
+ Goodbye, world.
+ </p>
+ """))
+ return self.assertFlattensTo(element, "Hello, world.")
+
+
+ def test_elementContainingStaticElement(self):
+ """
+ An Element which is returned by the render method of another Element is
+ rendered properly.
+ """
+ class RenderfulElement(Element):
+ @renderer
+ def renderMethod(self, request, tag):
+ return tag(Element(
+ loader=XMLString("<em>Hello, world.</em>")))
+ element = RenderfulElement(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="renderMethod" />
+ """))
+ return self.assertFlattensTo(element, "<p><em>Hello, world.</em></p>")
+
+
+ def test_elementUsingSlots(self):
+ """
+ An Element which is returned by the render method of another Element is
+ rendered properly.
+ """
+ class RenderfulElement(Element):
+ @renderer
+ def renderMethod(self, request, tag):
+ return tag.fillSlots(test2='world.')
+ element = RenderfulElement(loader=XMLString(
+ '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"'
+ ' t:render="renderMethod">'
+ '<t:slot name="test1" default="Hello, " />'
+ '<t:slot name="test2" />'
+ '</p>'
+ ))
+ return self.assertFlattensTo(element, "<p>Hello, world.</p>")
+
+
+ def test_elementContainingDynamicElement(self):
+ """
+ Directives in the document factory of a Element returned from a render
+ method of another Element are satisfied from the correct object: the
+ "inner" Element.
+ """
+ class OuterElement(Element):
+ @renderer
+ def outerMethod(self, request, tag):
+ return tag(InnerElement(loader=XMLString("""
+ <t:ignored
+ xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="innerMethod" />
+ """)))
+ class InnerElement(Element):
+ @renderer
+ def innerMethod(self, request, tag):
+ return "Hello, world."
+ element = OuterElement(loader=XMLString("""
+ <p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"
+ t:render="outerMethod" />
+ """))
+ return self.assertFlattensTo(element, "<p>Hello, world.</p>")
+
+
+ def test_sameLoaderTwice(self):
+ """
+ Rendering the output of a loader, or even the same element, should
+ return different output each time.
+ """
+ sharedLoader = XMLString(
+ '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
+ '<t:transparent t:render="classCounter" /> '
+ '<t:transparent t:render="instanceCounter" />'
+ '</p>')
+
+ class DestructiveElement(Element):
+ count = 0
+ instanceCount = 0
+ loader = sharedLoader
+
+ @renderer
+ def classCounter(self, request, tag):
+ DestructiveElement.count += 1
+ return tag(str(DestructiveElement.count))
+ @renderer
+ def instanceCounter(self, request, tag):
+ self.instanceCount += 1
+ return tag(str(self.instanceCount))
+
+ e1 = DestructiveElement()
+ e2 = DestructiveElement()
+ self.assertFlattensImmediately(e1, "<p>1 1</p>")
+ self.assertFlattensImmediately(e1, "<p>2 2</p>")
+ self.assertFlattensImmediately(e2, "<p>3 1</p>")
+
+
+
+class TagLoaderTests(FlattenTestCase):
+ """
+ Tests for L{TagLoader}.
+ """
+ def setUp(self):
+ self.loader = TagLoader(tags.i('test'))
+
+
+ def test_interface(self):
+ """
+ An instance of L{TagLoader} provides L{ITemplateLoader}.
+ """
+ self.assertTrue(verifyObject(ITemplateLoader, self.loader))
+
+
+ def test_loadsList(self):
+ """
+ L{TagLoader.load} returns a list, per L{ITemplateLoader}.
+ """
+ self.assertIsInstance(self.loader.load(), list)
+
+
+ def test_flatten(self):
+ """
+ L{TagLoader} can be used in an L{Element}, and flattens as the tag used
+ to construct the L{TagLoader} would flatten.
+ """
+ e = Element(self.loader)
+ self.assertFlattensImmediately(e, '<i>test</i>')
+
+
+
+class TestElement(Element):
+ """
+ An L{Element} that can be rendered successfully.
+ """
+ loader = XMLString(
+ '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
+ 'Hello, world.'
+ '</p>')
+
+
+
+class TestFailureElement(Element):
+ """
+ An L{Element} that can be used in place of L{FailureElement} to verify
+ that L{renderElement} can render failures properly.
+ """
+ loader = XMLString(
+ '<p xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">'
+ 'I failed.'
+ '</p>')
+
+ def __init__(self, failure, loader=None):
+ self.failure = failure
+
+
+
+class FailingElement(Element):
+ """
+ An element that raises an exception when rendered.
+ """
+ def render(self, request):
+ a = 42
+ b = 0
+ return a / b
+
+
+
+class FakeSite(object):
+ """
+ A minimal L{Site} object that we can use to test displayTracebacks
+ """
+ displayTracebacks = False
+
+
+
+class TestRenderElement(TestCase):
+ """
+ Test L{renderElement}
+ """
+
+ def setUp(self):
+ """
+ Set up a common L{DummyRequest} and L{FakeSite}.
+ """
+ self.request = DummyRequest([""])
+ self.request.site = FakeSite()
+
+
+ def test_simpleRender(self):
+ """
+ L{renderElement} returns NOT_DONE_YET and eventually
+ writes the rendered L{Element} to the request before finishing the
+ request.
+ """
+ element = TestElement()
+
+ d = self.request.notifyFinish()
+
+ def check(_):
+ self.assertEqual(
+ "".join(self.request.written),
+ "<!DOCTYPE html>\n"
+ "<p>Hello, world.</p>")
+ self.assertTrue(self.request.finished)
+
+ d.addCallback(check)
+
+ self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
+
+ return d
+
+
+ def test_simpleFailure(self):
+ """
+ L{renderElement} handles failures by writing a minimal
+ error message to the request and finishing it.
+ """
+ element = FailingElement()
+
+ d = self.request.notifyFinish()
+
+ def check(_):
+ flushed = self.flushLoggedErrors(FlattenerError)
+ self.assertEqual(len(flushed), 1)
+ self.assertEqual(
+ "".join(self.request.written),
+ ('<!DOCTYPE html>\n'
+ '<div style="font-size:800%;'
+ 'background-color:#FFF;'
+ 'color:#F00'
+ '">An error occurred while rendering the response.</div>'))
+ self.assertTrue(self.request.finished)
+
+ d.addCallback(check)
+
+ self.assertIdentical(NOT_DONE_YET, renderElement(self.request, element))
+
+ return d
+
+
+ def test_simpleFailureWithTraceback(self):
+ """
+ L{renderElement} will render a traceback when rendering of
+ the element fails and our site is configured to display tracebacks.
+ """
+ self.request.site.displayTracebacks = True
+
+ element = FailingElement()
+
+ d = self.request.notifyFinish()
+
+ def check(_):
+ flushed = self.flushLoggedErrors(FlattenerError)
+ self.assertEqual(len(flushed), 1)
+ self.assertEqual(
+ "".join(self.request.written),
+ "<!DOCTYPE html>\n<p>I failed.</p>")
+ self.assertTrue(self.request.finished)
+
+ d.addCallback(check)
+
+ renderElement(self.request, element, _failElement=TestFailureElement)
+
+ return d
+
+
+ def test_nonDefaultDoctype(self):
+ """
+ L{renderElement} will write the doctype string specified by the
+ doctype keyword argument.
+ """
+
+ element = TestElement()
+
+ d = self.request.notifyFinish()
+
+ def check(_):
+ self.assertEqual(
+ "".join(self.request.written),
+ ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
+ ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
+ '<p>Hello, world.</p>'))
+
+ d.addCallback(check)
+
+ renderElement(
+ self.request,
+ element,
+ doctype=(
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'
+ ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'))
+
+ return d
+
+
+ def test_noneDoctype(self):
+ """
+ L{renderElement} will not write out a doctype if the doctype keyword
+ argument is C{None}.
+ """
+
+ element = TestElement()
+
+ d = self.request.notifyFinish()
+
+ def check(_):
+ self.assertEqual(
+ "".join(self.request.written),
+ '<p>Hello, world.</p>')
+
+ d.addCallback(check)
+
+ renderElement(self.request, element, doctype=None)
+
+ return d
diff --git a/twisted/web/test/test_util.py b/twisted/web/test/test_util.py
new file mode 100644
index 0000000..42b54b9
--- /dev/null
+++ b/twisted/web/test/test_util.py
@@ -0,0 +1,380 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.util}.
+"""
+
+from twisted.python.failure import Failure
+from twisted.trial.unittest import TestCase
+from twisted.web import util
+from twisted.web.error import FlattenerError
+from twisted.web.util import (
+ redirectTo, _SourceLineElement,
+ _SourceFragmentElement, _FrameElement, _StackElement,
+ FailureElement, formatFailure)
+
+from twisted.web.http import FOUND
+from twisted.web.server import Request
+from twisted.web.template import TagLoader, flattenString, tags
+
+from twisted.web.test.test_web import DummyChannel
+
+
+class RedirectToTestCase(TestCase):
+ """
+ Tests for L{redirectTo}.
+ """
+
+ def test_headersAndCode(self):
+ """
+ L{redirectTo} will set the C{Location} and C{Content-Type} headers on
+ its request, and set the response code to C{FOUND}, so the browser will
+ be redirected.
+ """
+ request = Request(DummyChannel(), True)
+ request.method = 'GET'
+ targetURL = "http://target.example.com/4321"
+ redirectTo(targetURL, request)
+ self.assertEqual(request.code, FOUND)
+ self.assertEqual(
+ request.responseHeaders.getRawHeaders('location'), [targetURL])
+ self.assertEqual(
+ request.responseHeaders.getRawHeaders('content-type'),
+ ['text/html; charset=utf-8'])
+
+
+ def test_redirectToUnicodeURL(self) :
+ """
+ L{redirectTo} will raise TypeError if unicode object is passed in URL
+ """
+ request = Request(DummyChannel(), True)
+ request.method = 'GET'
+ targetURL = u'http://target.example.com/4321'
+ self.assertRaises(TypeError, redirectTo, targetURL, request)
+
+
+
+class FailureElementTests(TestCase):
+ """
+ Tests for L{FailureElement} and related helpers which can render a
+ L{Failure} as an HTML string.
+ """
+ def setUp(self):
+ """
+ Create a L{Failure} which can be used by the rendering tests.
+ """
+ def lineNumberProbeAlsoBroken():
+ message = "This is a problem"
+ raise Exception(message)
+ # Figure out the line number from which the exception will be raised.
+ self.base = lineNumberProbeAlsoBroken.func_code.co_firstlineno + 1
+
+ try:
+ lineNumberProbeAlsoBroken()
+ except:
+ self.failure = Failure(captureVars=True)
+ self.frame = self.failure.frames[-1]
+
+
+ def test_sourceLineElement(self):
+ """
+ L{_SourceLineElement} renders a source line and line number.
+ """
+ element = _SourceLineElement(
+ TagLoader(tags.div(
+ tags.span(render="lineNumber"),
+ tags.span(render="sourceLine"))),
+ 50, " print 'hello'")
+ d = flattenString(None, element)
+ expected = (
+ u"<div><span>50</span><span>"
+ u" \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}print 'hello'</span></div>")
+ d.addCallback(
+ self.assertEqual, expected.encode('utf-8'))
+ return d
+
+
+ def test_sourceFragmentElement(self):
+ """
+ L{_SourceFragmentElement} renders source lines at and around the line
+ number indicated by a frame object.
+ """
+ element = _SourceFragmentElement(
+ TagLoader(tags.div(
+ tags.span(render="lineNumber"),
+ tags.span(render="sourceLine"),
+ render="sourceLines")),
+ self.frame)
+
+ source = [
+ u' \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}message = '
+ u'"This is a problem"',
+
+ u' \N{NO-BREAK SPACE} \N{NO-BREAK SPACE}raise Exception(message)',
+ u'# Figure out the line number from which the exception will be '
+ u'raised.',
+ ]
+ d = flattenString(None, element)
+ d.addCallback(
+ self.assertEqual,
+ ''.join([
+ '<div class="snippet%sLine"><span>%d</span><span>%s</span>'
+ '</div>' % (
+ ["", "Highlight"][lineNumber == 1],
+ self.base + lineNumber,
+ (u" \N{NO-BREAK SPACE}" * 4 + sourceLine).encode(
+ 'utf-8'))
+ for (lineNumber, sourceLine)
+ in enumerate(source)]))
+ return d
+
+
+ def test_frameElementFilename(self):
+ """
+ The I{filename} renderer of L{_FrameElement} renders the filename
+ associated with the frame object used to initialize the
+ L{_FrameElement}.
+ """
+ element = _FrameElement(
+ TagLoader(tags.span(render="filename")),
+ self.frame)
+ d = flattenString(None, element)
+ d.addCallback(
+ # __file__ differs depending on whether an up-to-date .pyc file
+ # already existed.
+ self.assertEqual, "<span>" + __file__.rstrip('c') + "</span>")
+ return d
+
+
+ def test_frameElementLineNumber(self):
+ """
+ The I{lineNumber} renderer of L{_FrameElement} renders the line number
+ associated with the frame object used to initialize the
+ L{_FrameElement}.
+ """
+ element = _FrameElement(
+ TagLoader(tags.span(render="lineNumber")),
+ self.frame)
+ d = flattenString(None, element)
+ d.addCallback(
+ self.assertEqual, "<span>" + str(self.base + 1) + "</span>")
+ return d
+
+
+ def test_frameElementFunction(self):
+ """
+ The I{function} renderer of L{_FrameElement} renders the line number
+ associated with the frame object used to initialize the
+ L{_FrameElement}.
+ """
+ element = _FrameElement(
+ TagLoader(tags.span(render="function")),
+ self.frame)
+ d = flattenString(None, element)
+ d.addCallback(
+ self.assertEqual, "<span>lineNumberProbeAlsoBroken</span>")
+ return d
+
+
+ def test_frameElementSource(self):
+ """
+ The I{source} renderer of L{_FrameElement} renders the source code near
+ the source filename/line number associated with the frame object used to
+ initialize the L{_FrameElement}.
+ """
+ element = _FrameElement(None, self.frame)
+ renderer = element.lookupRenderMethod("source")
+ tag = tags.div()
+ result = renderer(None, tag)
+ self.assertIsInstance(result, _SourceFragmentElement)
+ self.assertIdentical(result.frame, self.frame)
+ self.assertEqual([tag], result.loader.load())
+
+
+ def test_stackElement(self):
+ """
+ The I{frames} renderer of L{_StackElement} renders each stack frame in
+ the list of frames used to initialize the L{_StackElement}.
+ """
+ element = _StackElement(None, self.failure.frames[:2])
+ renderer = element.lookupRenderMethod("frames")
+ tag = tags.div()
+ result = renderer(None, tag)
+ self.assertIsInstance(result, list)
+ self.assertIsInstance(result[0], _FrameElement)
+ self.assertIdentical(result[0].frame, self.failure.frames[0])
+ self.assertIsInstance(result[1], _FrameElement)
+ self.assertIdentical(result[1].frame, self.failure.frames[1])
+ # They must not share the same tag object.
+ self.assertNotEqual(result[0].loader.load(), result[1].loader.load())
+ self.assertEqual(2, len(result))
+
+
+ def test_failureElementTraceback(self):
+ """
+ The I{traceback} renderer of L{FailureElement} renders the failure's
+ stack frames using L{_StackElement}.
+ """
+ element = FailureElement(self.failure)
+ renderer = element.lookupRenderMethod("traceback")
+ tag = tags.div()
+ result = renderer(None, tag)
+ self.assertIsInstance(result, _StackElement)
+ self.assertIdentical(result.stackFrames, self.failure.frames)
+ self.assertEqual([tag], result.loader.load())
+
+
+ def test_failureElementType(self):
+ """
+ The I{type} renderer of L{FailureElement} renders the failure's
+ exception type.
+ """
+ element = FailureElement(
+ self.failure, TagLoader(tags.span(render="type")))
+ d = flattenString(None, element)
+ d.addCallback(
+ self.assertEqual, "<span>exceptions.Exception</span>")
+ return d
+
+
+ def test_failureElementValue(self):
+ """
+ The I{value} renderer of L{FailureElement} renders the value's exception
+ value.
+ """
+ element = FailureElement(
+ self.failure, TagLoader(tags.span(render="value")))
+ d = flattenString(None, element)
+ d.addCallback(
+ self.assertEqual, '<span>This is a problem</span>')
+ return d
+
+
+
+class FormatFailureTests(TestCase):
+ """
+ Tests for L{twisted.web.util.formatFailure} which returns an HTML string
+ representing the L{Failure} instance passed to it.
+ """
+ def test_flattenerError(self):
+ """
+ If there is an error flattening the L{Failure} instance,
+ L{formatFailure} raises L{FlattenerError}.
+ """
+ self.assertRaises(FlattenerError, formatFailure, object())
+
+
+ def test_returnsBytes(self):
+ """
+ The return value of L{formatFailure} is a C{str} instance (not a
+ C{unicode} instance) with numeric character references for any non-ASCII
+ characters meant to appear in the output.
+ """
+ try:
+ raise Exception("Fake bug")
+ except:
+ result = formatFailure(Failure())
+
+ self.assertIsInstance(result, str)
+ self.assertTrue(all(ord(ch) < 128 for ch in result))
+ # Indentation happens to rely on NO-BREAK SPACE
+ self.assertIn("&#160;", result)
+
+
+
+class DeprecatedHTMLHelpers(TestCase):
+ """
+ The various HTML generation helper APIs in L{twisted.web.util} are
+ deprecated.
+ """
+ def _htmlHelperDeprecationTest(self, functionName):
+ """
+ Helper method which asserts that using the name indicated by
+ C{functionName} from the L{twisted.web.util} module emits a deprecation
+ warning.
+ """
+ getattr(util, functionName)
+ warnings = self.flushWarnings([self._htmlHelperDeprecationTest])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "twisted.web.util.%s was deprecated in Twisted 12.1.0: "
+ "See twisted.web.template." % (functionName,))
+
+
+ def test_htmlrepr(self):
+ """
+ L{twisted.web.util.htmlrepr} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlrepr")
+
+
+ def test_saferepr(self):
+ """
+ L{twisted.web.util.saferepr} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("saferepr")
+
+
+ def test_htmlUnknown(self):
+ """
+ L{twisted.web.util.htmlUnknown} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlUnknown")
+
+
+ def test_htmlDict(self):
+ """
+ L{twisted.web.util.htmlDict} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlDict")
+
+
+ def test_htmlList(self):
+ """
+ L{twisted.web.util.htmlList} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlList")
+
+
+ def test_htmlInst(self):
+ """
+ L{twisted.web.util.htmlInst} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlInst")
+
+
+ def test_htmlString(self):
+ """
+ L{twisted.web.util.htmlString} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlString")
+
+
+ def test_htmlIndent(self):
+ """
+ L{twisted.web.util.htmlIndent} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlIndent")
+
+
+ def test_htmlFunc(self):
+ """
+ L{twisted.web.util.htmlFunc} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlFunc")
+
+
+ def test_htmlReprTypes(self):
+ """
+ L{twisted.web.util.htmlReprTypes} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("htmlReprTypes")
+
+
+ def test_stylesheet(self):
+ """
+ L{twisted.web.util.stylesheet} is deprecated.
+ """
+ self._htmlHelperDeprecationTest("stylesheet")
diff --git a/twisted/web/test/test_vhost.py b/twisted/web/test/test_vhost.py
new file mode 100644
index 0000000..13e6357
--- /dev/null
+++ b/twisted/web/test/test_vhost.py
@@ -0,0 +1,105 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.vhost}.
+"""
+
+from twisted.internet.defer import gatherResults
+from twisted.trial.unittest import TestCase
+from twisted.web.http import NOT_FOUND
+from twisted.web.static import Data
+from twisted.web.vhost import NameVirtualHost
+from twisted.web.test.test_web import DummyRequest
+from twisted.web.test._util import _render
+
+class NameVirtualHostTests(TestCase):
+ """
+ Tests for L{NameVirtualHost}.
+ """
+ def test_renderWithoutHost(self):
+ """
+ L{NameVirtualHost.render} returns the result of rendering the
+ instance's C{default} if it is not C{None} and there is no I{Host}
+ header in the request.
+ """
+ virtualHostResource = NameVirtualHost()
+ virtualHostResource.default = Data("correct result", "")
+ request = DummyRequest([''])
+ self.assertEqual(
+ virtualHostResource.render(request), "correct result")
+
+
+ def test_renderWithoutHostNoDefault(self):
+ """
+ L{NameVirtualHost.render} returns a response with a status of I{NOT
+ FOUND} if the instance's C{default} is C{None} and there is no I{Host}
+ header in the request.
+ """
+ virtualHostResource = NameVirtualHost()
+ request = DummyRequest([''])
+ d = _render(virtualHostResource, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_renderWithHost(self):
+ """
+ L{NameVirtualHost.render} returns the result of rendering the resource
+ which is the value in the instance's C{host} dictionary corresponding
+ to the key indicated by the value of the I{Host} header in the request.
+ """
+ virtualHostResource = NameVirtualHost()
+ virtualHostResource.addHost('example.org', Data("winner", ""))
+
+ request = DummyRequest([''])
+ request.headers['host'] = 'example.org'
+ d = _render(virtualHostResource, request)
+ def cbRendered(ignored, request):
+ self.assertEqual(''.join(request.written), "winner")
+ d.addCallback(cbRendered, request)
+
+ # The port portion of the Host header should not be considered.
+ requestWithPort = DummyRequest([''])
+ requestWithPort.headers['host'] = 'example.org:8000'
+ dWithPort = _render(virtualHostResource, requestWithPort)
+ def cbRendered(ignored, requestWithPort):
+ self.assertEqual(''.join(requestWithPort.written), "winner")
+ dWithPort.addCallback(cbRendered, requestWithPort)
+
+ return gatherResults([d, dWithPort])
+
+
+ def test_renderWithUnknownHost(self):
+ """
+ L{NameVirtualHost.render} returns the result of rendering the
+ instance's C{default} if it is not C{None} and there is no host
+ matching the value of the I{Host} header in the request.
+ """
+ virtualHostResource = NameVirtualHost()
+ virtualHostResource.default = Data("correct data", "")
+ request = DummyRequest([''])
+ request.headers['host'] = 'example.com'
+ d = _render(virtualHostResource, request)
+ def cbRendered(ignored):
+ self.assertEqual(''.join(request.written), "correct data")
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_renderWithUnknownHostNoDefault(self):
+ """
+ L{NameVirtualHost.render} returns a response with a status of I{NOT
+ FOUND} if the instance's C{default} is C{None} and there is no host
+ matching the value of the I{Host} header in the request.
+ """
+ virtualHostResource = NameVirtualHost()
+ request = DummyRequest([''])
+ request.headers['host'] = 'example.com'
+ d = _render(virtualHostResource, request)
+ def cbRendered(ignored):
+ self.assertEqual(request.responseCode, NOT_FOUND)
+ d.addCallback(cbRendered)
+ return d
diff --git a/twisted/web/test/test_web.py b/twisted/web/test/test_web.py
new file mode 100644
index 0000000..f386701
--- /dev/null
+++ b/twisted/web/test/test_web.py
@@ -0,0 +1,1100 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for various parts of L{twisted.web}.
+"""
+
+from cStringIO import StringIO
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+from twisted.internet import reactor
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import Deferred
+from twisted.web import server, resource, util
+from twisted.internet import defer, interfaces, task
+from twisted.web import iweb, http, http_headers, error
+from twisted.python import log
+
+
+class DummyRequest:
+ """
+ Represents a dummy or fake request.
+
+ @ivar _finishedDeferreds: C{None} or a C{list} of L{Deferreds} which will
+ be called back with C{None} when C{finish} is called or which will be
+ errbacked if C{processingFailed} is called.
+
+ @type headers: C{dict}
+ @ivar headers: A mapping of header name to header value for all request
+ headers.
+
+ @type outgoingHeaders: C{dict}
+ @ivar outgoingHeaders: A mapping of header name to header value for all
+ response headers.
+
+ @type responseCode: C{int}
+ @ivar responseCode: The response code which was passed to
+ C{setResponseCode}.
+
+ @type written: C{list} of C{str}
+ @ivar written: The bytes which have been written to the request.
+ """
+ uri = 'http://dummy/'
+ method = 'GET'
+ client = None
+
+ def registerProducer(self, prod,s):
+ self.go = 1
+ while self.go:
+ prod.resumeProducing()
+
+ def unregisterProducer(self):
+ self.go = 0
+
+
+ def __init__(self, postpath, session=None):
+ self.sitepath = []
+ self.written = []
+ self.finished = 0
+ self.postpath = postpath
+ self.prepath = []
+ self.session = None
+ self.protoSession = session or server.Session(0, self)
+ self.args = {}
+ self.outgoingHeaders = {}
+ self.responseHeaders = http_headers.Headers()
+ self.responseCode = None
+ self.headers = {}
+ self._finishedDeferreds = []
+
+
+ def getHeader(self, name):
+ """
+ Retrieve the value of a request header.
+
+ @type name: C{str}
+ @param name: The name of the request header for which to retrieve the
+ value. Header names are compared case-insensitively.
+
+ @rtype: C{str} or L{NoneType}
+ @return: The value of the specified request header.
+ """
+ return self.headers.get(name.lower(), None)
+
+
+ def setHeader(self, name, value):
+ """TODO: make this assert on write() if the header is content-length
+ """
+ self.outgoingHeaders[name.lower()] = value
+
+ def getSession(self):
+ if self.session:
+ return self.session
+ assert not self.written, "Session cannot be requested after data has been written."
+ self.session = self.protoSession
+ return self.session
+
+
+ def render(self, resource):
+ """
+ Render the given resource as a response to this request.
+
+ This implementation only handles a few of the most common behaviors of
+ resources. It can handle a render method that returns a string or
+ C{NOT_DONE_YET}. It doesn't know anything about the semantics of
+ request methods (eg HEAD) nor how to set any particular headers.
+ Basically, it's largely broken, but sufficient for some tests at least.
+ It should B{not} be expanded to do all the same stuff L{Request} does.
+ Instead, L{DummyRequest} should be phased out and L{Request} (or some
+ other real code factored in a different way) used.
+ """
+ result = resource.render(self)
+ if result is server.NOT_DONE_YET:
+ return
+ self.write(result)
+ self.finish()
+
+
+ def write(self, data):
+ self.written.append(data)
+
+ def notifyFinish(self):
+ """
+ Return a L{Deferred} which is called back with C{None} when the request
+ is finished. This will probably only work if you haven't called
+ C{finish} yet.
+ """
+ finished = Deferred()
+ self._finishedDeferreds.append(finished)
+ return finished
+
+
+ def finish(self):
+ """
+ Record that the request is finished and callback and L{Deferred}s
+ waiting for notification of this.
+ """
+ self.finished = self.finished + 1
+ if self._finishedDeferreds is not None:
+ observers = self._finishedDeferreds
+ self._finishedDeferreds = None
+ for obs in observers:
+ obs.callback(None)
+
+
+ def processingFailed(self, reason):
+ """
+ Errback and L{Deferreds} waiting for finish notification.
+ """
+ if self._finishedDeferreds is not None:
+ observers = self._finishedDeferreds
+ self._finishedDeferreds = None
+ for obs in observers:
+ obs.errback(reason)
+
+
+ def addArg(self, name, value):
+ self.args[name] = [value]
+
+
+ def setResponseCode(self, code, message=None):
+ """
+ Set the HTTP status response code, but takes care that this is called
+ before any data is written.
+ """
+ assert not self.written, "Response code cannot be set after data has been written: %s." % "@@@@".join(self.written)
+ self.responseCode = code
+ self.responseMessage = message
+
+
+ def setLastModified(self, when):
+ assert not self.written, "Last-Modified cannot be set after data has been written: %s." % "@@@@".join(self.written)
+
+
+ def setETag(self, tag):
+ assert not self.written, "ETag cannot be set after data has been written: %s." % "@@@@".join(self.written)
+
+
+ def getClientIP(self):
+ """
+ Return the IPv4 address of the client which made this request, if there
+ is one, otherwise C{None}.
+ """
+ if isinstance(self.client, IPv4Address):
+ return self.client.host
+ return None
+
+
+class ResourceTestCase(unittest.TestCase):
+ def testListEntities(self):
+ r = resource.Resource()
+ self.assertEqual([], r.listEntities())
+
+
+class SimpleResource(resource.Resource):
+ """
+ @ivar _contentType: C{None} or a C{str} giving the value of the
+ I{Content-Type} header in the response this resource will render. If it
+ is C{None}, no I{Content-Type} header will be set in the response.
+ """
+ def __init__(self, contentType=None):
+ resource.Resource.__init__(self)
+ self._contentType = contentType
+
+
+ def render(self, request):
+ if self._contentType is not None:
+ request.responseHeaders.setRawHeaders(
+ "content-type", [self._contentType])
+
+ if http.CACHED in (request.setLastModified(10),
+ request.setETag('MatchingTag')):
+ return ''
+ else:
+ return "correct"
+
+
+class DummyChannel:
+ class TCP:
+ port = 80
+ disconnected = False
+
+ def __init__(self):
+ self.written = StringIO()
+ self.producers = []
+
+ def getPeer(self):
+ return IPv4Address("TCP", '192.168.1.1', 12344)
+
+ def write(self, bytes):
+ assert isinstance(bytes, str)
+ self.written.write(bytes)
+
+ def writeSequence(self, iovec):
+ map(self.write, iovec)
+
+ def getHost(self):
+ return IPv4Address("TCP", '10.0.0.1', self.port)
+
+ def registerProducer(self, producer, streaming):
+ self.producers.append((producer, streaming))
+
+ def loseConnection(self):
+ self.disconnected = True
+
+
+ class SSL(TCP):
+ implements(interfaces.ISSLTransport)
+
+ site = server.Site(resource.Resource())
+
+ def __init__(self):
+ self.transport = self.TCP()
+
+
+ def requestDone(self, request):
+ pass
+
+
+
+class SiteTest(unittest.TestCase):
+ def test_simplestSite(self):
+ """
+ L{Site.getResourceFor} returns the C{""} child of the root resource it
+ is constructed with when processing a request for I{/}.
+ """
+ sres1 = SimpleResource()
+ sres2 = SimpleResource()
+ sres1.putChild("",sres2)
+ site = server.Site(sres1)
+ self.assertIdentical(
+ site.getResourceFor(DummyRequest([''])),
+ sres2, "Got the wrong resource.")
+
+
+
+class SessionTest(unittest.TestCase):
+ """
+ Tests for L{server.Session}.
+ """
+ def setUp(self):
+ """
+ Create a site with one active session using a deterministic, easily
+ controlled clock.
+ """
+ self.clock = task.Clock()
+ self.uid = 'unique'
+ self.site = server.Site(resource.Resource())
+ self.session = server.Session(self.site, self.uid, self.clock)
+ self.site.sessions[self.uid] = self.session
+
+
+ def test_defaultReactor(self):
+ """
+ If not value is passed to L{server.Session.__init__}, the global
+ reactor is used.
+ """
+ session = server.Session(server.Site(resource.Resource()), '123')
+ self.assertIdentical(session._reactor, reactor)
+
+
+ def test_startCheckingExpiration(self):
+ """
+ L{server.Session.startCheckingExpiration} causes the session to expire
+ after L{server.Session.sessionTimeout} seconds without activity.
+ """
+ self.session.startCheckingExpiration()
+
+ # Advance to almost the timeout - nothing should happen.
+ self.clock.advance(self.session.sessionTimeout - 1)
+ self.assertIn(self.uid, self.site.sessions)
+
+ # Advance to the timeout, the session should expire.
+ self.clock.advance(1)
+ self.assertNotIn(self.uid, self.site.sessions)
+
+ # There should be no calls left over, either.
+ self.assertFalse(self.clock.calls)
+
+
+ def test_expire(self):
+ """
+ L{server.Session.expire} expires the session.
+ """
+ self.session.expire()
+ # It should be gone from the session dictionary.
+ self.assertNotIn(self.uid, self.site.sessions)
+ # And there should be no pending delayed calls.
+ self.assertFalse(self.clock.calls)
+
+
+ def test_expireWhileChecking(self):
+ """
+ L{server.Session.expire} expires the session even if the timeout call
+ isn't due yet.
+ """
+ self.session.startCheckingExpiration()
+ self.test_expire()
+
+
+ def test_notifyOnExpire(self):
+ """
+ A function registered with L{server.Session.notifyOnExpire} is called
+ when the session expires.
+ """
+ callbackRan = [False]
+ def expired():
+ callbackRan[0] = True
+ self.session.notifyOnExpire(expired)
+ self.session.expire()
+ self.assertTrue(callbackRan[0])
+
+
+ def test_touch(self):
+ """
+ L{server.Session.touch} updates L{server.Session.lastModified} and
+ delays session timeout.
+ """
+ # Make sure it works before startCheckingExpiration
+ self.clock.advance(3)
+ self.session.touch()
+ self.assertEqual(self.session.lastModified, 3)
+
+ # And after startCheckingExpiration
+ self.session.startCheckingExpiration()
+ self.clock.advance(self.session.sessionTimeout - 1)
+ self.session.touch()
+ self.clock.advance(self.session.sessionTimeout - 1)
+ self.assertIn(self.uid, self.site.sessions)
+
+ # It should have advanced it by just sessionTimeout, no more.
+ self.clock.advance(1)
+ self.assertNotIn(self.uid, self.site.sessions)
+
+
+ def test_startCheckingExpirationParameterDeprecated(self):
+ """
+ L{server.Session.startCheckingExpiration} emits a deprecation warning
+ if it is invoked with a parameter.
+ """
+ self.session.startCheckingExpiration(123)
+ warnings = self.flushWarnings([
+ self.test_startCheckingExpirationParameterDeprecated])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "The lifetime parameter to startCheckingExpiration is deprecated "
+ "since Twisted 9.0. See Session.sessionTimeout instead.")
+
+
+ def test_checkExpiredDeprecated(self):
+ """
+ L{server.Session.checkExpired} is deprecated.
+ """
+ self.session.checkExpired()
+ warnings = self.flushWarnings([self.test_checkExpiredDeprecated])
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ "Session.checkExpired is deprecated since Twisted 9.0; sessions "
+ "check themselves now, you don't need to.")
+ self.assertEqual(len(warnings), 1)
+
+
+# Conditional requests:
+# If-None-Match, If-Modified-Since
+
+# make conditional request:
+# normal response if condition succeeds
+# if condition fails:
+# response code
+# no body
+
+def httpBody(whole):
+ return whole.split('\r\n\r\n', 1)[1]
+
+def httpHeader(whole, key):
+ key = key.lower()
+ headers = whole.split('\r\n\r\n', 1)[0]
+ for header in headers.split('\r\n'):
+ if header.lower().startswith(key):
+ return header.split(':', 1)[1].strip()
+ return None
+
+def httpCode(whole):
+ l1 = whole.split('\r\n', 1)[0]
+ return int(l1.split()[1])
+
+class ConditionalTest(unittest.TestCase):
+ """
+ web.server's handling of conditional requests for cache validation.
+ """
+ def setUp(self):
+ self.resrc = SimpleResource()
+ self.resrc.putChild('', self.resrc)
+ self.resrc.putChild('with-content-type', SimpleResource('image/jpeg'))
+ self.site = server.Site(self.resrc)
+ self.site.logFile = log.logfile
+
+ # HELLLLLLLLLLP! This harness is Very Ugly.
+ self.channel = self.site.buildProtocol(None)
+ self.transport = http.StringTransport()
+ self.transport.close = lambda *a, **kw: None
+ self.transport.disconnecting = lambda *a, **kw: 0
+ self.transport.getPeer = lambda *a, **kw: "peer"
+ self.transport.getHost = lambda *a, **kw: "host"
+ self.channel.makeConnection(self.transport)
+
+
+ def tearDown(self):
+ self.channel.connectionLost(None)
+
+
+ def _modifiedTest(self, modifiedSince=None, etag=None):
+ """
+ Given the value C{modifiedSince} for the I{If-Modified-Since} header or
+ the value C{etag} for the I{If-Not-Match} header, verify that a response
+ with a 200 code, a default Content-Type, and the resource as the body is
+ returned.
+ """
+ if modifiedSince is not None:
+ validator = "If-Modified-Since: " + modifiedSince
+ else:
+ validator = "If-Not-Match: " + etag
+ for line in ["GET / HTTP/1.1", validator, ""]:
+ self.channel.lineReceived(line)
+ result = self.transport.getvalue()
+ self.assertEqual(httpCode(result), http.OK)
+ self.assertEqual(httpBody(result), "correct")
+ self.assertEqual(httpHeader(result, "Content-Type"), "text/html")
+
+
+ def test_modified(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value with
+ a timestamp indicating a time before the last modification of the
+ requested resource, a 200 response is returned along with a response
+ body containing the resource.
+ """
+ self._modifiedTest(modifiedSince=http.datetimeToString(1))
+
+
+ def test_unmodified(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value with a
+ timestamp indicating a time after the last modification of the request
+ resource, a 304 response is returned along with an empty response body
+ and no Content-Type header if the application does not set one.
+ """
+ for line in ["GET / HTTP/1.1",
+ "If-Modified-Since: " + http.datetimeToString(100), ""]:
+ self.channel.lineReceived(line)
+ result = self.transport.getvalue()
+ self.assertEqual(httpCode(result), http.NOT_MODIFIED)
+ self.assertEqual(httpBody(result), "")
+ # Since there SHOULD NOT (RFC 2616, section 10.3.5) be any
+ # entity-headers, the Content-Type is not set if the application does
+ # not explicitly set it.
+ self.assertEqual(httpHeader(result, "Content-Type"), None)
+
+
+ def test_invalidTimestamp(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value which
+ cannot be parsed, the header is treated as not having been present
+ and a normal 200 response is returned with a response body
+ containing the resource.
+ """
+ self._modifiedTest(modifiedSince="like, maybe a week ago, I guess?")
+
+
+ def test_invalidTimestampYear(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value which
+ contains a string in the year position which is not an integer, the
+ header is treated as not having been present and a normal 200
+ response is returned with a response body containing the resource.
+ """
+ self._modifiedTest(modifiedSince="Thu, 01 Jan blah 00:00:10 GMT")
+
+
+ def test_invalidTimestampTooLongAgo(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value which
+ contains a year before the epoch, the header is treated as not
+ having been present and a normal 200 response is returned with a
+ response body containing the resource.
+ """
+ self._modifiedTest(modifiedSince="Thu, 01 Jan 1899 00:00:10 GMT")
+
+
+ def test_invalidTimestampMonth(self):
+ """
+ If a request is made with an I{If-Modified-Since} header value which
+ contains a string in the month position which is not a recognized
+ month abbreviation, the header is treated as not having been present
+ and a normal 200 response is returned with a response body
+ containing the resource.
+ """
+ self._modifiedTest(modifiedSince="Thu, 01 Blah 1970 00:00:10 GMT")
+
+
+ def test_etagMatchedNot(self):
+ """
+ If a request is made with an I{If-None-Match} ETag which does not match
+ the current ETag of the requested resource, the header is treated as not
+ having been present and a normal 200 response is returned with a
+ response body containing the resource.
+ """
+ self._modifiedTest(etag="unmatchedTag")
+
+
+ def test_etagMatched(self):
+ """
+ If a request is made with an I{If-None-Match} ETag which does match the
+ current ETag of the requested resource, a 304 response is returned along
+ with an empty response body.
+ """
+ for line in ["GET / HTTP/1.1", "If-None-Match: MatchingTag", ""]:
+ self.channel.lineReceived(line)
+ result = self.transport.getvalue()
+ self.assertEqual(httpHeader(result, "ETag"), "MatchingTag")
+ self.assertEqual(httpCode(result), http.NOT_MODIFIED)
+ self.assertEqual(httpBody(result), "")
+
+
+ def test_unmodifiedWithContentType(self):
+ """
+ Similar to L{test_etagMatched}, but the response should include a
+ I{Content-Type} header if the application explicitly sets one.
+
+ This I{Content-Type} header SHOULD NOT be present according to RFC 2616,
+ section 10.3.5. It will only be present if the application explicitly
+ sets it.
+ """
+ for line in ["GET /with-content-type HTTP/1.1",
+ "If-None-Match: MatchingTag", ""]:
+ self.channel.lineReceived(line)
+ result = self.transport.getvalue()
+ self.assertEqual(httpCode(result), http.NOT_MODIFIED)
+ self.assertEqual(httpBody(result), "")
+ self.assertEqual(httpHeader(result, "Content-Type"), "image/jpeg")
+
+
+
+
+from twisted.web import google
+class GoogleTestCase(unittest.TestCase):
+ def testCheckGoogle(self):
+ raise unittest.SkipTest("no violation of google ToS")
+ d = google.checkGoogle('site:www.twistedmatrix.com twisted')
+ d.addCallback(self.assertEqual, 'http://twistedmatrix.com/')
+ return d
+
+
+ def test_deprecated(self):
+ """
+ Google module is deprecated since Twisted 11.1.0
+ """
+ from twisted.web import google
+ warnings = self.flushWarnings(offendingFunctions=[self.test_deprecated])
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+
+
+
+class RequestTests(unittest.TestCase):
+ """
+ Tests for the HTTP request class, L{server.Request}.
+ """
+
+ def test_interface(self):
+ """
+ L{server.Request} instances provide L{iweb.IRequest}.
+ """
+ self.assertTrue(
+ verifyObject(iweb.IRequest, server.Request(DummyChannel(), True)))
+
+
+ def testChildLink(self):
+ request = server.Request(DummyChannel(), 1)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.childLink('baz'), 'bar/baz')
+ request = server.Request(DummyChannel(), 1)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar/', 'HTTP/1.0')
+ self.assertEqual(request.childLink('baz'), 'baz')
+
+ def testPrePathURLSimple(self):
+ request = server.Request(DummyChannel(), 1)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ request.setHost('example.com', 80)
+ self.assertEqual(request.prePathURL(), 'http://example.com/foo/bar')
+
+ def testPrePathURLNonDefault(self):
+ d = DummyChannel()
+ d.transport.port = 81
+ request = server.Request(d, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'http://example.com:81/foo/bar')
+
+ def testPrePathURLSSLPort(self):
+ d = DummyChannel()
+ d.transport.port = 443
+ request = server.Request(d, 1)
+ request.setHost('example.com', 443)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'http://example.com:443/foo/bar')
+
+ def testPrePathURLSSLPortAndSSL(self):
+ d = DummyChannel()
+ d.transport = DummyChannel.SSL()
+ d.transport.port = 443
+ request = server.Request(d, 1)
+ request.setHost('example.com', 443)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'https://example.com/foo/bar')
+
+ def testPrePathURLHTTPPortAndSSL(self):
+ d = DummyChannel()
+ d.transport = DummyChannel.SSL()
+ d.transport.port = 80
+ request = server.Request(d, 1)
+ request.setHost('example.com', 80)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'https://example.com:80/foo/bar')
+
+ def testPrePathURLSSLNonDefault(self):
+ d = DummyChannel()
+ d.transport = DummyChannel.SSL()
+ d.transport.port = 81
+ request = server.Request(d, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'https://example.com:81/foo/bar')
+
+ def testPrePathURLSetSSLHost(self):
+ d = DummyChannel()
+ d.transport.port = 81
+ request = server.Request(d, 1)
+ request.setHost('foo.com', 81, 1)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo/bar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'https://foo.com:81/foo/bar')
+
+
+ def test_prePathURLQuoting(self):
+ """
+ L{Request.prePathURL} quotes special characters in the URL segments to
+ preserve the original meaning.
+ """
+ d = DummyChannel()
+ request = server.Request(d, 1)
+ request.setHost('example.com', 80)
+ request.gotLength(0)
+ request.requestReceived('GET', '/foo%2Fbar', 'HTTP/1.0')
+ self.assertEqual(request.prePathURL(), 'http://example.com/foo%2Fbar')
+
+
+
+class RootResource(resource.Resource):
+ isLeaf=0
+ def getChildWithDefault(self, name, request):
+ request.rememberRootURL()
+ return resource.Resource.getChildWithDefault(self, name, request)
+ def render(self, request):
+ return ''
+
+class RememberURLTest(unittest.TestCase):
+ def createServer(self, r):
+ chan = DummyChannel()
+ chan.site = server.Site(r)
+ return chan
+
+ def testSimple(self):
+ r = resource.Resource()
+ r.isLeaf=0
+ rr = RootResource()
+ r.putChild('foo', rr)
+ rr.putChild('', rr)
+ rr.putChild('bar', resource.Resource())
+ chan = self.createServer(r)
+ for url in ['/foo/', '/foo/bar', '/foo/bar/baz', '/foo/bar/']:
+ request = server.Request(chan, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ request.requestReceived('GET', url, 'HTTP/1.0')
+ self.assertEqual(request.getRootURL(), "http://example.com/foo")
+
+ def testRoot(self):
+ rr = RootResource()
+ rr.putChild('', rr)
+ rr.putChild('bar', resource.Resource())
+ chan = self.createServer(rr)
+ for url in ['/', '/bar', '/bar/baz', '/bar/']:
+ request = server.Request(chan, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ request.requestReceived('GET', url, 'HTTP/1.0')
+ self.assertEqual(request.getRootURL(), "http://example.com/")
+
+
+class NewRenderResource(resource.Resource):
+ def render_GET(self, request):
+ return "hi hi"
+
+ def render_HEH(self, request):
+ return "ho ho"
+
+
+
+class HeadlessResource(object):
+ """
+ A resource that implements GET but not HEAD.
+ """
+ implements(resource.IResource)
+
+ allowedMethods = ["GET"]
+
+ def render(self, request):
+ """
+ Leave the request open for future writes.
+ """
+ self.request = request
+ if request.method not in self.allowedMethods:
+ raise error.UnsupportedMethod(self.allowedMethods)
+ self.request.write("some data")
+ return server.NOT_DONE_YET
+
+
+
+
+class NewRenderTestCase(unittest.TestCase):
+ """
+ Tests for L{server.Request.render}.
+ """
+ def _getReq(self, resource=None):
+ """
+ Create a request object with a stub channel and install the
+ passed resource at /newrender. If no resource is passed,
+ create one.
+ """
+ d = DummyChannel()
+ if resource is None:
+ resource = NewRenderResource()
+ d.site.resource.putChild('newrender', resource)
+ d.transport.port = 81
+ request = server.Request(d, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ return request
+
+ def testGoodMethods(self):
+ req = self._getReq()
+ req.requestReceived('GET', '/newrender', 'HTTP/1.0')
+ self.assertEqual(req.transport.getvalue().splitlines()[-1], 'hi hi')
+
+ req = self._getReq()
+ req.requestReceived('HEH', '/newrender', 'HTTP/1.0')
+ self.assertEqual(req.transport.getvalue().splitlines()[-1], 'ho ho')
+
+ def testBadMethods(self):
+ req = self._getReq()
+ req.requestReceived('CONNECT', '/newrender', 'HTTP/1.0')
+ self.assertEqual(req.code, 501)
+
+ req = self._getReq()
+ req.requestReceived('hlalauguG', '/newrender', 'HTTP/1.0')
+ self.assertEqual(req.code, 501)
+
+ def testImplicitHead(self):
+ req = self._getReq()
+ req.requestReceived('HEAD', '/newrender', 'HTTP/1.0')
+ self.assertEqual(req.code, 200)
+ self.assertEqual(-1, req.transport.getvalue().find('hi hi'))
+
+
+ def test_unsupportedHead(self):
+ """
+ HEAD requests against resource that only claim support for GET
+ should not include a body in the response.
+ """
+ resource = HeadlessResource()
+ req = self._getReq(resource)
+ req.requestReceived("HEAD", "/newrender", "HTTP/1.0")
+ headers, body = req.transport.getvalue().split('\r\n\r\n')
+ self.assertEqual(req.code, 200)
+ self.assertEqual(body, '')
+
+
+
+class GettableResource(resource.Resource):
+ """
+ Used by AllowedMethodsTest to simulate an allowed method.
+ """
+ def render_GET(self):
+ pass
+
+ def render_fred_render_ethel(self):
+ """
+ The unusual method name is designed to test the culling method
+ in C{twisted.web.resource._computeAllowedMethods}.
+ """
+ pass
+
+
+
+class AllowedMethodsTest(unittest.TestCase):
+ """
+ 'C{twisted.web.resource._computeAllowedMethods} is provided by a
+ default should the subclass not provide the method.
+ """
+
+
+ def _getReq(self):
+ """
+ Generate a dummy request for use by C{_computeAllowedMethod} tests.
+ """
+ d = DummyChannel()
+ d.site.resource.putChild('gettableresource', GettableResource())
+ d.transport.port = 81
+ request = server.Request(d, 1)
+ request.setHost('example.com', 81)
+ request.gotLength(0)
+ return request
+
+
+ def test_computeAllowedMethods(self):
+ """
+ C{_computeAllowedMethods} will search through the
+ 'gettableresource' for all attributes/methods of the form
+ 'render_{method}' ('render_GET', for example) and return a list of
+ the methods. 'HEAD' will always be included from the
+ resource.Resource superclass.
+ """
+ res = GettableResource()
+ allowedMethods = resource._computeAllowedMethods(res)
+ self.assertEqual(set(allowedMethods),
+ set(['GET', 'HEAD', 'fred_render_ethel']))
+
+
+ def test_notAllowed(self):
+ """
+ When an unsupported method is requested, the default
+ L{_computeAllowedMethods} method will be called to determine the
+ allowed methods, and the HTTP 405 'Method Not Allowed' status will
+ be returned with the allowed methods will be returned in the
+ 'Allow' header.
+ """
+ req = self._getReq()
+ req.requestReceived('POST', '/gettableresource', 'HTTP/1.0')
+ self.assertEqual(req.code, 405)
+ self.assertEqual(
+ set(req.responseHeaders.getRawHeaders('allow')[0].split(", ")),
+ set(['GET', 'HEAD','fred_render_ethel'])
+ )
+
+
+ def test_notAllowedQuoting(self):
+ """
+ When an unsupported method response is generated, an HTML message will
+ be displayed. That message should include a quoted form of the URI and,
+ since that value come from a browser and shouldn't necessarily be
+ trusted.
+ """
+ req = self._getReq()
+ req.requestReceived('POST', '/gettableresource?'
+ 'value=<script>bad', 'HTTP/1.0')
+ self.assertEqual(req.code, 405)
+ renderedPage = req.transport.getvalue()
+ self.assertNotIn("<script>bad", renderedPage)
+ self.assertIn('&lt;script&gt;bad', renderedPage)
+
+
+ def test_notImplementedQuoting(self):
+ """
+ When an not-implemented method response is generated, an HTML message
+ will be displayed. That message should include a quoted form of the
+ requested method, since that value come from a browser and shouldn't
+ necessarily be trusted.
+ """
+ req = self._getReq()
+ req.requestReceived('<style>bad', '/gettableresource', 'HTTP/1.0')
+ self.assertEqual(req.code, 501)
+ renderedPage = req.transport.getvalue()
+ self.assertNotIn("<style>bad", renderedPage)
+ self.assertIn('&lt;style&gt;bad', renderedPage)
+
+
+
+class SDResource(resource.Resource):
+ def __init__(self,default):
+ self.default = default
+
+
+ def getChildWithDefault(self, name, request):
+ d = defer.succeed(self.default)
+ resource = util.DeferredResource(d)
+ return resource.getChildWithDefault(name, request)
+
+
+
+class DeferredResourceTests(unittest.TestCase):
+ """
+ Tests for L{DeferredResource}.
+ """
+
+ def testDeferredResource(self):
+ r = resource.Resource()
+ r.isLeaf = 1
+ s = SDResource(r)
+ d = DummyRequest(['foo', 'bar', 'baz'])
+ resource.getChildForRequest(s, d)
+ self.assertEqual(d.postpath, ['bar', 'baz'])
+
+
+ def test_render(self):
+ """
+ L{DeferredResource} uses the request object's C{render} method to
+ render the resource which is the result of the L{Deferred} being
+ handled.
+ """
+ rendered = []
+ request = DummyRequest([])
+ request.render = rendered.append
+
+ result = resource.Resource()
+ deferredResource = util.DeferredResource(defer.succeed(result))
+ deferredResource.render(request)
+ self.assertEqual(rendered, [result])
+
+
+
+class DummyRequestForLogTest(DummyRequest):
+ uri = '/dummy' # parent class uri has "http://", which doesn't really happen
+ code = 123
+
+ clientproto = 'HTTP/1.0'
+ sentLength = None
+ client = IPv4Address('TCP', '1.2.3.4', 12345)
+
+
+
+class TestLogEscaping(unittest.TestCase):
+ def setUp(self):
+ self.site = http.HTTPFactory()
+ self.site.logFile = StringIO()
+ self.request = DummyRequestForLogTest(self.site, False)
+
+ def testSimple(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy HTTP/1.0" 123 - "-" "-"\n')
+
+ def testMethodQuote(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.request.method = 'G"T'
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "G\\"T /dummy HTTP/1.0" 123 - "-" "-"\n')
+
+ def testRequestQuote(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.request.uri='/dummy"withquote'
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy\\"withquote HTTP/1.0" 123 - "-" "-"\n')
+
+ def testProtoQuote(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.request.clientproto='HT"P/1.0'
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy HT\\"P/1.0" 123 - "-" "-"\n')
+
+ def testRefererQuote(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.request.headers['referer'] = 'http://malicious" ".website.invalid'
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy HTTP/1.0" 123 - "http://malicious\\" \\".website.invalid" "-"\n')
+
+ def testUserAgentQuote(self):
+ self.site._logDateTime = "[%02d/%3s/%4d:%02d:%02d:%02d +0000]" % (
+ 25, 'Oct', 2004, 12, 31, 59)
+ self.request.headers['user-agent'] = 'Malicious Web" Evil'
+ self.site.log(self.request)
+ self.site.logFile.seek(0)
+ self.assertEqual(
+ self.site.logFile.read(),
+ '1.2.3.4 - - [25/Oct/2004:12:31:59 +0000] "GET /dummy HTTP/1.0" 123 - "-" "Malicious Web\\" Evil"\n')
+
+
+
+class ServerAttributesTestCase(unittest.TestCase):
+ """
+ Tests that deprecated twisted.web.server attributes raise the appropriate
+ deprecation warnings when used.
+ """
+
+ def test_deprecatedAttributeDateTimeString(self):
+ """
+ twisted.web.server.date_time_string should not be used; instead use
+ twisted.web.http.datetimeToString directly
+ """
+ deprecated_func = server.date_time_string
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedAttributeDateTimeString])
+
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ ("twisted.web.server.date_time_string was deprecated in Twisted "
+ "12.1.0: Please use twisted.web.http.datetimeToString instead"))
+
+
+ def test_deprecatedAttributeStringDateTime(self):
+ """
+ twisted.web.server.string_date_time should not be used; instead use
+ twisted.web.http.stringToDatetime directly
+ """
+ deprecated_func = server.string_date_time
+ warnings = self.flushWarnings(
+ offendingFunctions=[self.test_deprecatedAttributeStringDateTime])
+
+ self.assertEqual(len(warnings), 1)
+ self.assertEqual(warnings[0]['category'], DeprecationWarning)
+ self.assertEqual(
+ warnings[0]['message'],
+ ("twisted.web.server.string_date_time was deprecated in Twisted "
+ "12.1.0: Please use twisted.web.http.stringToDatetime instead"))
diff --git a/twisted/web/test/test_webclient.py b/twisted/web/test/test_webclient.py
new file mode 100644
index 0000000..1841402
--- /dev/null
+++ b/twisted/web/test/test_webclient.py
@@ -0,0 +1,3144 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.client}.
+"""
+
+import cookielib
+import os
+from errno import ENOSPC
+import zlib
+from StringIO import StringIO
+
+from urlparse import urlparse, urljoin
+
+from zope.interface.verify import verifyObject
+
+from twisted.trial import unittest
+from twisted.web import server, static, client, error, util, resource, http_headers
+from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed
+from twisted.web._newclient import ResponseNeverReceived, ResponseFailed
+from twisted.internet import reactor, defer, interfaces, task
+from twisted.python.failure import Failure
+from twisted.python.filepath import FilePath
+from twisted.python.log import msg
+from twisted.python.components import proxyForInterface
+from twisted.protocols.policies import WrappingFactory
+from twisted.test.proto_helpers import StringTransport
+from twisted.test.proto_helpers import MemoryReactor
+from twisted.internet.task import Clock
+from twisted.internet.error import ConnectionRefusedError, ConnectionDone
+from twisted.internet.protocol import Protocol, Factory
+from twisted.internet.defer import Deferred, succeed
+from twisted.internet.endpoints import TCP4ClientEndpoint, SSL4ClientEndpoint
+from twisted.web.client import FileBodyProducer, Request, HTTPConnectionPool
+from twisted.web.client import _WebToNormalContextFactory
+from twisted.web.client import WebClientContextFactory, _HTTP11ClientFactory
+from twisted.web.iweb import UNKNOWN_LENGTH, IBodyProducer, IResponse
+from twisted.web._newclient import HTTP11ClientProtocol, Response
+from twisted.web.error import SchemeNotSupported
+
+try:
+ from twisted.internet import ssl
+except:
+ ssl = None
+
+
+
+class ExtendedRedirect(resource.Resource):
+ """
+ Redirection resource.
+
+ The HTTP status code is set according to the C{code} query parameter.
+
+ @type lastMethod: C{str}
+ @ivar lastMethod: Last handled HTTP request method
+ """
+ isLeaf = 1
+ lastMethod = None
+
+
+ def __init__(self, url):
+ resource.Resource.__init__(self)
+ self.url = url
+
+
+ def render(self, request):
+ if self.lastMethod:
+ self.lastMethod = request.method
+ return "OK Thnx!"
+ else:
+ self.lastMethod = request.method
+ code = int(request.args['code'][0])
+ return self.redirectTo(self.url, request, code)
+
+
+ def getChild(self, name, request):
+ return self
+
+
+ def redirectTo(self, url, request, code):
+ request.setResponseCode(code)
+ request.setHeader("location", url)
+ return "OK Bye!"
+
+
+
+class ForeverTakingResource(resource.Resource):
+ """
+ L{ForeverTakingResource} is a resource which never finishes responding
+ to requests.
+ """
+ def __init__(self, write=False):
+ resource.Resource.__init__(self)
+ self._write = write
+
+ def render(self, request):
+ if self._write:
+ request.write('some bytes')
+ return server.NOT_DONE_YET
+
+
+class CookieMirrorResource(resource.Resource):
+ def render(self, request):
+ l = []
+ for k,v in request.received_cookies.items():
+ l.append((k, v))
+ l.sort()
+ return repr(l)
+
+class RawCookieMirrorResource(resource.Resource):
+ def render(self, request):
+ return repr(request.getHeader('cookie'))
+
+class ErrorResource(resource.Resource):
+
+ def render(self, request):
+ request.setResponseCode(401)
+ if request.args.get("showlength"):
+ request.setHeader("content-length", "0")
+ return ""
+
+class NoLengthResource(resource.Resource):
+
+ def render(self, request):
+ return "nolength"
+
+
+
+class HostHeaderResource(resource.Resource):
+ """
+ A testing resource which renders itself as the value of the host header
+ from the request.
+ """
+ def render(self, request):
+ return request.received_headers['host']
+
+
+
+class PayloadResource(resource.Resource):
+ """
+ A testing resource which renders itself as the contents of the request body
+ as long as the request body is 100 bytes long, otherwise which renders
+ itself as C{"ERROR"}.
+ """
+ def render(self, request):
+ data = request.content.read()
+ contentLength = request.received_headers['content-length']
+ if len(data) != 100 or int(contentLength) != 100:
+ return "ERROR"
+ return data
+
+
+class DelayResource(resource.Resource):
+
+ def __init__(self, seconds):
+ self.seconds = seconds
+
+ def render(self, request):
+ def response():
+ request.write('some bytes')
+ request.finish()
+ reactor.callLater(self.seconds, response)
+ return server.NOT_DONE_YET
+
+
+class BrokenDownloadResource(resource.Resource):
+
+ def render(self, request):
+ # only sends 3 bytes even though it claims to send 5
+ request.setHeader("content-length", "5")
+ request.write('abc')
+ return ''
+
+class CountingRedirect(util.Redirect):
+ """
+ A L{util.Redirect} resource that keeps track of the number of times the
+ resource has been accessed.
+ """
+ def __init__(self, *a, **kw):
+ util.Redirect.__init__(self, *a, **kw)
+ self.count = 0
+
+ def render(self, request):
+ self.count += 1
+ return util.Redirect.render(self, request)
+
+
+class CountingResource(resource.Resource):
+ """
+ A resource that keeps track of the number of times it has been accessed.
+ """
+ def __init__(self):
+ resource.Resource.__init__(self)
+ self.count = 0
+
+ def render(self, request):
+ self.count += 1
+ return "Success"
+
+
+class ParseUrlTestCase(unittest.TestCase):
+ """
+ Test URL parsing facility and defaults values.
+ """
+
+ def test_parse(self):
+ """
+ L{client._parse} correctly parses a URL into its various components.
+ """
+ # The default port for HTTP is 80.
+ self.assertEqual(
+ client._parse('http://127.0.0.1/'),
+ ('http', '127.0.0.1', 80, '/'))
+
+ # The default port for HTTPS is 443.
+ self.assertEqual(
+ client._parse('https://127.0.0.1/'),
+ ('https', '127.0.0.1', 443, '/'))
+
+ # Specifying a port.
+ self.assertEqual(
+ client._parse('http://spam:12345/'),
+ ('http', 'spam', 12345, '/'))
+
+ # Weird (but commonly accepted) structure uses default port.
+ self.assertEqual(
+ client._parse('http://spam:/'),
+ ('http', 'spam', 80, '/'))
+
+ # Spaces in the hostname are trimmed, the default path is /.
+ self.assertEqual(
+ client._parse('http://foo '),
+ ('http', 'foo', 80, '/'))
+
+
+ def test_externalUnicodeInterference(self):
+ """
+ L{client._parse} should return C{str} for the scheme, host, and path
+ elements of its return tuple, even when passed an URL which has
+ previously been passed to L{urlparse} as a C{unicode} string.
+ """
+ badInput = u'http://example.com/path'
+ goodInput = badInput.encode('ascii')
+ urlparse(badInput)
+ scheme, host, port, path = client._parse(goodInput)
+ self.assertIsInstance(scheme, str)
+ self.assertIsInstance(host, str)
+ self.assertIsInstance(path, str)
+
+
+
+class HTTPPageGetterTests(unittest.TestCase):
+ """
+ Tests for L{HTTPPagerGetter}, the HTTP client protocol implementation
+ used to implement L{getPage}.
+ """
+ def test_earlyHeaders(self):
+ """
+ When a connection is made, L{HTTPPagerGetter} sends the headers from
+ its factory's C{headers} dict. If I{Host} or I{Content-Length} is
+ present in this dict, the values are not sent, since they are sent with
+ special values before the C{headers} dict is processed. If
+ I{User-Agent} is present in the dict, it overrides the value of the
+ C{agent} attribute of the factory. If I{Cookie} is present in the
+ dict, its value is added to the values from the factory's C{cookies}
+ attribute.
+ """
+ factory = client.HTTPClientFactory(
+ 'http://foo/bar',
+ agent="foobar",
+ cookies={'baz': 'quux'},
+ postdata="some data",
+ headers={
+ 'Host': 'example.net',
+ 'User-Agent': 'fooble',
+ 'Cookie': 'blah blah',
+ 'Content-Length': '12981',
+ 'Useful': 'value'})
+ transport = StringTransport()
+ protocol = client.HTTPPageGetter()
+ protocol.factory = factory
+ protocol.makeConnection(transport)
+ self.assertEqual(
+ transport.value(),
+ "GET /bar HTTP/1.0\r\n"
+ "Host: example.net\r\n"
+ "User-Agent: foobar\r\n"
+ "Content-Length: 9\r\n"
+ "Useful: value\r\n"
+ "connection: close\r\n"
+ "Cookie: blah blah; baz=quux\r\n"
+ "\r\n"
+ "some data")
+
+
+class GetBodyProtocol(Protocol):
+
+ def __init__(self, deferred):
+ self.deferred = deferred
+ self.buf = ''
+
+ def dataReceived(self, bytes):
+ self.buf += bytes
+
+ def connectionLost(self, reason):
+ self.deferred.callback(self.buf)
+
+
+def getBody(response):
+ d = defer.Deferred()
+ response.deliverBody(GetBodyProtocol(d))
+ return d
+
+
+class WebClientTestCase(unittest.TestCase):
+ def _listen(self, site):
+ return reactor.listenTCP(0, site, interface="127.0.0.1")
+
+ def setUp(self):
+ self.agent = None # for twisted.web.client.Agent test
+ self.cleanupServerConnections = 0
+ name = self.mktemp()
+ os.mkdir(name)
+ FilePath(name).child("file").setContent("0123456789")
+ r = static.File(name)
+ r.putChild("redirect", util.Redirect("/file"))
+ self.infiniteRedirectResource = CountingRedirect("/infiniteRedirect")
+ r.putChild("infiniteRedirect", self.infiniteRedirectResource)
+ r.putChild("wait", ForeverTakingResource())
+ r.putChild("write-then-wait", ForeverTakingResource(write=True))
+ r.putChild("error", ErrorResource())
+ r.putChild("nolength", NoLengthResource())
+ r.putChild("host", HostHeaderResource())
+ r.putChild("payload", PayloadResource())
+ r.putChild("broken", BrokenDownloadResource())
+ r.putChild("cookiemirror", CookieMirrorResource())
+ r.putChild('delay1', DelayResource(1))
+ r.putChild('delay2', DelayResource(2))
+
+ self.afterFoundGetCounter = CountingResource()
+ r.putChild("afterFoundGetCounter", self.afterFoundGetCounter)
+ r.putChild("afterFoundGetRedirect", util.Redirect("/afterFoundGetCounter"))
+
+ miscasedHead = static.Data("miscased-head GET response content", "major/minor")
+ miscasedHead.render_Head = lambda request: "miscased-head content"
+ r.putChild("miscased-head", miscasedHead)
+
+ self.extendedRedirect = ExtendedRedirect('/extendedRedirect')
+ r.putChild("extendedRedirect", self.extendedRedirect)
+ self.site = server.Site(r, timeout=None)
+ self.wrapper = WrappingFactory(self.site)
+ self.port = self._listen(self.wrapper)
+ self.portno = self.port.getHost().port
+
+ def tearDown(self):
+ if self.agent:
+ # clean up connections for twisted.web.client.Agent test.
+ self.agent.closeCachedConnections()
+ self.agent = None
+
+ # If the test indicated it might leave some server-side connections
+ # around, clean them up.
+ connections = self.wrapper.protocols.keys()
+ # If there are fewer server-side connections than requested,
+ # that's okay. Some might have noticed that the client closed
+ # the connection and cleaned up after themselves.
+ for n in range(min(len(connections), self.cleanupServerConnections)):
+ proto = connections.pop()
+ msg("Closing %r" % (proto,))
+ proto.transport.loseConnection()
+ if connections:
+ msg("Some left-over connections; this test is probably buggy.")
+ return self.port.stopListening()
+
+ def getURL(self, path):
+ host = "http://127.0.0.1:%d/" % self.portno
+ return urljoin(host, path)
+
+ def testPayload(self):
+ s = "0123456789" * 10
+ return client.getPage(self.getURL("payload"), postdata=s
+ ).addCallback(self.assertEqual, s
+ )
+
+
+ def test_getPageBrokenDownload(self):
+ """
+ If the connection is closed before the number of bytes indicated by
+ I{Content-Length} have been received, the L{Deferred} returned by
+ L{getPage} fails with L{PartialDownloadError}.
+ """
+ d = client.getPage(self.getURL("broken"))
+ d = self.assertFailure(d, client.PartialDownloadError)
+ d.addCallback(lambda exc: self.assertEqual(exc.response, "abc"))
+ return d
+
+
+ def test_downloadPageBrokenDownload(self):
+ """
+ If the connection is closed before the number of bytes indicated by
+ I{Content-Length} have been received, the L{Deferred} returned by
+ L{downloadPage} fails with L{PartialDownloadError}.
+ """
+ # test what happens when download gets disconnected in the middle
+ path = FilePath(self.mktemp())
+ d = client.downloadPage(self.getURL("broken"), path.path)
+ d = self.assertFailure(d, client.PartialDownloadError)
+
+ def checkResponse(response):
+ """
+ The HTTP status code from the server is propagated through the
+ C{PartialDownloadError}.
+ """
+ self.assertEqual(response.status, "200")
+ self.assertEqual(response.message, "OK")
+ return response
+ d.addCallback(checkResponse)
+
+ def cbFailed(ignored):
+ self.assertEqual(path.getContent(), "abc")
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_downloadPageLogsFileCloseError(self):
+ """
+ If there is an exception closing the file being written to after the
+ connection is prematurely closed, that exception is logged.
+ """
+ class BrokenFile:
+ def write(self, bytes):
+ pass
+
+ def close(self):
+ raise IOError(ENOSPC, "No file left on device")
+
+ d = client.downloadPage(self.getURL("broken"), BrokenFile())
+ d = self.assertFailure(d, client.PartialDownloadError)
+ def cbFailed(ignored):
+ self.assertEqual(len(self.flushLoggedErrors(IOError)), 1)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def testHostHeader(self):
+ # if we pass Host header explicitly, it should be used, otherwise
+ # it should extract from url
+ return defer.gatherResults([
+ client.getPage(self.getURL("host")).addCallback(self.assertEqual, "127.0.0.1:%s" % (self.portno,)),
+ client.getPage(self.getURL("host"), headers={"Host": "www.example.com"}).addCallback(self.assertEqual, "www.example.com")])
+
+
+ def test_getPage(self):
+ """
+ L{client.getPage} returns a L{Deferred} which is called back with
+ the body of the response if the default method B{GET} is used.
+ """
+ d = client.getPage(self.getURL("file"))
+ d.addCallback(self.assertEqual, "0123456789")
+ return d
+
+
+ def test_getPageHEAD(self):
+ """
+ L{client.getPage} returns a L{Deferred} which is called back with
+ the empty string if the method is I{HEAD} and there is a successful
+ response code.
+ """
+ d = client.getPage(self.getURL("file"), method="HEAD")
+ d.addCallback(self.assertEqual, "")
+ return d
+
+
+ def test_getPageNotQuiteHEAD(self):
+ """
+ If the request method is a different casing of I{HEAD} (ie, not all
+ capitalized) then it is not a I{HEAD} request and the response body
+ is returned.
+ """
+ d = client.getPage(self.getURL("miscased-head"), method='Head')
+ d.addCallback(self.assertEqual, "miscased-head content")
+ return d
+
+
+ def test_timeoutNotTriggering(self):
+ """
+ When a non-zero timeout is passed to L{getPage} and the page is
+ retrieved before the timeout period elapses, the L{Deferred} is
+ called back with the contents of the page.
+ """
+ d = client.getPage(self.getURL("host"), timeout=100)
+ d.addCallback(self.assertEqual, "127.0.0.1:%s" % (self.portno,))
+ return d
+
+
+ def test_timeoutTriggering(self):
+ """
+ When a non-zero timeout is passed to L{getPage} and that many
+ seconds elapse before the server responds to the request. the
+ L{Deferred} is errbacked with a L{error.TimeoutError}.
+ """
+ # This will probably leave some connections around.
+ self.cleanupServerConnections = 1
+ return self.assertFailure(
+ client.getPage(self.getURL("wait"), timeout=0.000001),
+ defer.TimeoutError)
+
+
+ def testDownloadPage(self):
+ downloads = []
+ downloadData = [("file", self.mktemp(), "0123456789"),
+ ("nolength", self.mktemp(), "nolength")]
+
+ for (url, name, data) in downloadData:
+ d = client.downloadPage(self.getURL(url), name)
+ d.addCallback(self._cbDownloadPageTest, data, name)
+ downloads.append(d)
+ return defer.gatherResults(downloads)
+
+ def _cbDownloadPageTest(self, ignored, data, name):
+ bytes = file(name, "rb").read()
+ self.assertEqual(bytes, data)
+
+ def testDownloadPageError1(self):
+ class errorfile:
+ def write(self, data):
+ raise IOError, "badness happened during write"
+ def close(self):
+ pass
+ ef = errorfile()
+ return self.assertFailure(
+ client.downloadPage(self.getURL("file"), ef),
+ IOError)
+
+ def testDownloadPageError2(self):
+ class errorfile:
+ def write(self, data):
+ pass
+ def close(self):
+ raise IOError, "badness happened during close"
+ ef = errorfile()
+ return self.assertFailure(
+ client.downloadPage(self.getURL("file"), ef),
+ IOError)
+
+ def testDownloadPageError3(self):
+ # make sure failures in open() are caught too. This is tricky.
+ # Might only work on posix.
+ tmpfile = open("unwritable", "wb")
+ tmpfile.close()
+ os.chmod("unwritable", 0) # make it unwritable (to us)
+ d = self.assertFailure(
+ client.downloadPage(self.getURL("file"), "unwritable"),
+ IOError)
+ d.addBoth(self._cleanupDownloadPageError3)
+ return d
+
+ def _cleanupDownloadPageError3(self, ignored):
+ os.chmod("unwritable", 0700)
+ os.unlink("unwritable")
+ return ignored
+
+ def _downloadTest(self, method):
+ dl = []
+ for (url, code) in [("nosuchfile", "404"), ("error", "401"),
+ ("error?showlength=1", "401")]:
+ d = method(url)
+ d = self.assertFailure(d, error.Error)
+ d.addCallback(lambda exc, code=code: self.assertEqual(exc.args[0], code))
+ dl.append(d)
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def testServerError(self):
+ return self._downloadTest(lambda url: client.getPage(self.getURL(url)))
+
+ def testDownloadServerError(self):
+ return self._downloadTest(lambda url: client.downloadPage(self.getURL(url), url.split('?')[0]))
+
+ def testFactoryInfo(self):
+ url = self.getURL('file')
+ scheme, host, port, path = client._parse(url)
+ factory = client.HTTPClientFactory(url)
+ reactor.connectTCP(host, port, factory)
+ return factory.deferred.addCallback(self._cbFactoryInfo, factory)
+
+ def _cbFactoryInfo(self, ignoredResult, factory):
+ self.assertEqual(factory.status, '200')
+ self.assert_(factory.version.startswith('HTTP/'))
+ self.assertEqual(factory.message, 'OK')
+ self.assertEqual(factory.response_headers['content-length'][0], '10')
+
+
+ def test_followRedirect(self):
+ """
+ By default, L{client.getPage} follows redirects and returns the content
+ of the target resource.
+ """
+ d = client.getPage(self.getURL("redirect"))
+ d.addCallback(self.assertEqual, "0123456789")
+ return d
+
+
+ def test_noFollowRedirect(self):
+ """
+ If C{followRedirect} is passed a false value, L{client.getPage} does not
+ follow redirects and returns a L{Deferred} which fails with
+ L{error.PageRedirect} when it encounters one.
+ """
+ d = self.assertFailure(
+ client.getPage(self.getURL("redirect"), followRedirect=False),
+ error.PageRedirect)
+ d.addCallback(self._cbCheckLocation)
+ return d
+
+
+ def _cbCheckLocation(self, exc):
+ self.assertEqual(exc.location, "/file")
+
+
+ def test_infiniteRedirection(self):
+ """
+ When more than C{redirectLimit} HTTP redirects are encountered, the
+ page request fails with L{InfiniteRedirection}.
+ """
+ def checkRedirectCount(*a):
+ self.assertEqual(f._redirectCount, 13)
+ self.assertEqual(self.infiniteRedirectResource.count, 13)
+
+ f = client._makeGetterFactory(
+ self.getURL('infiniteRedirect'),
+ client.HTTPClientFactory,
+ redirectLimit=13)
+ d = self.assertFailure(f.deferred, error.InfiniteRedirection)
+ d.addCallback(checkRedirectCount)
+ return d
+
+
+ def test_isolatedFollowRedirect(self):
+ """
+ C{client.HTTPPagerGetter} instances each obey the C{followRedirect}
+ value passed to the L{client.getPage} call which created them.
+ """
+ d1 = client.getPage(self.getURL('redirect'), followRedirect=True)
+ d2 = client.getPage(self.getURL('redirect'), followRedirect=False)
+
+ d = self.assertFailure(d2, error.PageRedirect
+ ).addCallback(lambda dummy: d1)
+ return d
+
+
+ def test_afterFoundGet(self):
+ """
+ Enabling unsafe redirection behaviour overwrites the method of
+ redirected C{POST} requests with C{GET}.
+ """
+ url = self.getURL('extendedRedirect?code=302')
+ f = client.HTTPClientFactory(url, followRedirect=True, method="POST")
+ self.assertFalse(
+ f.afterFoundGet,
+ "By default, afterFoundGet must be disabled")
+
+ def gotPage(page):
+ self.assertEqual(
+ self.extendedRedirect.lastMethod,
+ "GET",
+ "With afterFoundGet, the HTTP method must change to GET")
+
+ d = client.getPage(
+ url, followRedirect=True, afterFoundGet=True, method="POST")
+ d.addCallback(gotPage)
+ return d
+
+
+ def test_downloadAfterFoundGet(self):
+ """
+ Passing C{True} for C{afterFoundGet} to L{client.downloadPage} invokes
+ the same kind of redirect handling as passing that argument to
+ L{client.getPage} invokes.
+ """
+ url = self.getURL('extendedRedirect?code=302')
+
+ def gotPage(page):
+ self.assertEqual(
+ self.extendedRedirect.lastMethod,
+ "GET",
+ "With afterFoundGet, the HTTP method must change to GET")
+
+ d = client.downloadPage(url, "downloadTemp",
+ followRedirect=True, afterFoundGet=True, method="POST")
+ d.addCallback(gotPage)
+ return d
+
+
+ def test_afterFoundGetMakesOneRequest(self):
+ """
+ When C{afterFoundGet} is C{True}, L{client.getPage} only issues one
+ request to the server when following the redirect. This is a regression
+ test, see #4760.
+ """
+ def checkRedirectCount(*a):
+ self.assertEqual(self.afterFoundGetCounter.count, 1)
+
+ url = self.getURL('afterFoundGetRedirect')
+ d = client.getPage(
+ url, followRedirect=True, afterFoundGet=True, method="POST")
+ d.addCallback(checkRedirectCount)
+ return d
+
+
+ def testPartial(self):
+ name = self.mktemp()
+ f = open(name, "wb")
+ f.write("abcd")
+ f.close()
+
+ partialDownload = [(True, "abcd456789"),
+ (True, "abcd456789"),
+ (False, "0123456789")]
+
+ d = defer.succeed(None)
+ for (partial, expectedData) in partialDownload:
+ d.addCallback(self._cbRunPartial, name, partial)
+ d.addCallback(self._cbPartialTest, expectedData, name)
+
+ return d
+
+ testPartial.skip = "Cannot test until webserver can serve partial data properly"
+
+ def _cbRunPartial(self, ignored, name, partial):
+ return client.downloadPage(self.getURL("file"), name, supportPartial=partial)
+
+ def _cbPartialTest(self, ignored, expectedData, filename):
+ bytes = file(filename, "rb").read()
+ self.assertEqual(bytes, expectedData)
+
+
+ def test_downloadTimeout(self):
+ """
+ If the timeout indicated by the C{timeout} parameter to
+ L{client.HTTPDownloader.__init__} elapses without the complete response
+ being received, the L{defer.Deferred} returned by
+ L{client.downloadPage} fires with a L{Failure} wrapping a
+ L{defer.TimeoutError}.
+ """
+ self.cleanupServerConnections = 2
+ # Verify the behavior if no bytes are ever written.
+ first = client.downloadPage(
+ self.getURL("wait"),
+ self.mktemp(), timeout=0.01)
+
+ # Verify the behavior if some bytes are written but then the request
+ # never completes.
+ second = client.downloadPage(
+ self.getURL("write-then-wait"),
+ self.mktemp(), timeout=0.01)
+
+ return defer.gatherResults([
+ self.assertFailure(first, defer.TimeoutError),
+ self.assertFailure(second, defer.TimeoutError)])
+
+
+ def test_downloadHeaders(self):
+ """
+ After L{client.HTTPDownloader.deferred} fires, the
+ L{client.HTTPDownloader} instance's C{status} and C{response_headers}
+ attributes are populated with the values from the response.
+ """
+ def checkHeaders(factory):
+ self.assertEqual(factory.status, '200')
+ self.assertEqual(factory.response_headers['content-type'][0], 'text/html')
+ self.assertEqual(factory.response_headers['content-length'][0], '10')
+ os.unlink(factory.fileName)
+ factory = client._makeGetterFactory(
+ self.getURL('file'),
+ client.HTTPDownloader,
+ fileOrName=self.mktemp())
+ return factory.deferred.addCallback(lambda _: checkHeaders(factory))
+
+
+ def test_downloadCookies(self):
+ """
+ The C{cookies} dict passed to the L{client.HTTPDownloader}
+ initializer is used to populate the I{Cookie} header included in the
+ request sent to the server.
+ """
+ output = self.mktemp()
+ factory = client._makeGetterFactory(
+ self.getURL('cookiemirror'),
+ client.HTTPDownloader,
+ fileOrName=output,
+ cookies={'foo': 'bar'})
+ def cbFinished(ignored):
+ self.assertEqual(
+ FilePath(output).getContent(),
+ "[('foo', 'bar')]")
+ factory.deferred.addCallback(cbFinished)
+ return factory.deferred
+
+
+ def test_downloadRedirectLimit(self):
+ """
+ When more than C{redirectLimit} HTTP redirects are encountered, the
+ page request fails with L{InfiniteRedirection}.
+ """
+ def checkRedirectCount(*a):
+ self.assertEqual(f._redirectCount, 7)
+ self.assertEqual(self.infiniteRedirectResource.count, 7)
+
+ f = client._makeGetterFactory(
+ self.getURL('infiniteRedirect'),
+ client.HTTPDownloader,
+ fileOrName=self.mktemp(),
+ redirectLimit=7)
+ d = self.assertFailure(f.deferred, error.InfiniteRedirection)
+ d.addCallback(checkRedirectCount)
+ return d
+
+
+
+class WebClientSSLTestCase(WebClientTestCase):
+ def _listen(self, site):
+ from twisted import test
+ return reactor.listenSSL(0, site,
+ contextFactory=ssl.DefaultOpenSSLContextFactory(
+ FilePath(test.__file__).sibling('server.pem').path,
+ FilePath(test.__file__).sibling('server.pem').path,
+ ),
+ interface="127.0.0.1")
+
+ def getURL(self, path):
+ return "https://127.0.0.1:%d/%s" % (self.portno, path)
+
+ def testFactoryInfo(self):
+ url = self.getURL('file')
+ scheme, host, port, path = client._parse(url)
+ factory = client.HTTPClientFactory(url)
+ reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
+ # The base class defines _cbFactoryInfo correctly for this
+ return factory.deferred.addCallback(self._cbFactoryInfo, factory)
+
+
+
+class WebClientRedirectBetweenSSLandPlainText(unittest.TestCase):
+ def getHTTPS(self, path):
+ return "https://127.0.0.1:%d/%s" % (self.tlsPortno, path)
+
+ def getHTTP(self, path):
+ return "http://127.0.0.1:%d/%s" % (self.plainPortno, path)
+
+ def setUp(self):
+ plainRoot = static.Data('not me', 'text/plain')
+ tlsRoot = static.Data('me neither', 'text/plain')
+
+ plainSite = server.Site(plainRoot, timeout=None)
+ tlsSite = server.Site(tlsRoot, timeout=None)
+
+ from twisted import test
+ self.tlsPort = reactor.listenSSL(0, tlsSite,
+ contextFactory=ssl.DefaultOpenSSLContextFactory(
+ FilePath(test.__file__).sibling('server.pem').path,
+ FilePath(test.__file__).sibling('server.pem').path,
+ ),
+ interface="127.0.0.1")
+ self.plainPort = reactor.listenTCP(0, plainSite, interface="127.0.0.1")
+
+ self.plainPortno = self.plainPort.getHost().port
+ self.tlsPortno = self.tlsPort.getHost().port
+
+ plainRoot.putChild('one', util.Redirect(self.getHTTPS('two')))
+ tlsRoot.putChild('two', util.Redirect(self.getHTTP('three')))
+ plainRoot.putChild('three', util.Redirect(self.getHTTPS('four')))
+ tlsRoot.putChild('four', static.Data('FOUND IT!', 'text/plain'))
+
+ def tearDown(self):
+ ds = map(defer.maybeDeferred,
+ [self.plainPort.stopListening, self.tlsPort.stopListening])
+ return defer.gatherResults(ds)
+
+ def testHoppingAround(self):
+ return client.getPage(self.getHTTP("one")
+ ).addCallback(self.assertEqual, "FOUND IT!"
+ )
+
+class FakeTransport:
+ disconnecting = False
+ def __init__(self):
+ self.data = []
+ def write(self, stuff):
+ self.data.append(stuff)
+
+class CookieTestCase(unittest.TestCase):
+ def _listen(self, site):
+ return reactor.listenTCP(0, site, interface="127.0.0.1")
+
+ def setUp(self):
+ root = static.Data('El toro!', 'text/plain')
+ root.putChild("cookiemirror", CookieMirrorResource())
+ root.putChild("rawcookiemirror", RawCookieMirrorResource())
+ site = server.Site(root, timeout=None)
+ self.port = self._listen(site)
+ self.portno = self.port.getHost().port
+
+ def tearDown(self):
+ return self.port.stopListening()
+
+ def getHTTP(self, path):
+ return "http://127.0.0.1:%d/%s" % (self.portno, path)
+
+ def testNoCookies(self):
+ return client.getPage(self.getHTTP("cookiemirror")
+ ).addCallback(self.assertEqual, "[]"
+ )
+
+ def testSomeCookies(self):
+ cookies = {'foo': 'bar', 'baz': 'quux'}
+ return client.getPage(self.getHTTP("cookiemirror"), cookies=cookies
+ ).addCallback(self.assertEqual, "[('baz', 'quux'), ('foo', 'bar')]"
+ )
+
+ def testRawNoCookies(self):
+ return client.getPage(self.getHTTP("rawcookiemirror")
+ ).addCallback(self.assertEqual, "None"
+ )
+
+ def testRawSomeCookies(self):
+ cookies = {'foo': 'bar', 'baz': 'quux'}
+ return client.getPage(self.getHTTP("rawcookiemirror"), cookies=cookies
+ ).addCallback(self.assertEqual, "'foo=bar; baz=quux'"
+ )
+
+ def testCookieHeaderParsing(self):
+ factory = client.HTTPClientFactory('http://foo.example.com/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.transport = FakeTransport()
+ proto.connectionMade()
+ for line in [
+ '200 Ok',
+ 'Squash: yes',
+ 'Hands: stolen',
+ 'Set-Cookie: CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT',
+ 'Set-Cookie: PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
+ 'Set-Cookie: SHIPPING=FEDEX; path=/foo',
+ '',
+ 'body',
+ 'more body',
+ ]:
+ proto.dataReceived(line + '\r\n')
+ self.assertEqual(proto.transport.data,
+ ['GET / HTTP/1.0\r\n',
+ 'Host: foo.example.com\r\n',
+ 'User-Agent: Twisted PageGetter\r\n',
+ '\r\n'])
+ self.assertEqual(factory.cookies,
+ {
+ 'CUSTOMER': 'WILE_E_COYOTE',
+ 'PART_NUMBER': 'ROCKET_LAUNCHER_0001',
+ 'SHIPPING': 'FEDEX',
+ })
+
+
+
+class TestHostHeader(unittest.TestCase):
+ """
+ Test that L{HTTPClientFactory} includes the port in the host header
+ if needed.
+ """
+
+ def _getHost(self, bytes):
+ """
+ Retrieve the value of the I{Host} header from the serialized
+ request given by C{bytes}.
+ """
+ for line in bytes.splitlines():
+ try:
+ name, value = line.split(':', 1)
+ if name.strip().lower() == 'host':
+ return value.strip()
+ except ValueError:
+ pass
+
+
+ def test_HTTPDefaultPort(self):
+ """
+ No port should be included in the host header when connecting to the
+ default HTTP port.
+ """
+ factory = client.HTTPClientFactory('http://foo.example.com/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com')
+
+
+ def test_HTTPPort80(self):
+ """
+ No port should be included in the host header when connecting to the
+ default HTTP port even if it is in the URL.
+ """
+ factory = client.HTTPClientFactory('http://foo.example.com:80/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com')
+
+
+ def test_HTTPNotPort80(self):
+ """
+ The port should be included in the host header when connecting to the
+ a non default HTTP port.
+ """
+ factory = client.HTTPClientFactory('http://foo.example.com:8080/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com:8080')
+
+
+ def test_HTTPSDefaultPort(self):
+ """
+ No port should be included in the host header when connecting to the
+ default HTTPS port.
+ """
+ factory = client.HTTPClientFactory('https://foo.example.com/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com')
+
+
+ def test_HTTPSPort443(self):
+ """
+ No port should be included in the host header when connecting to the
+ default HTTPS port even if it is in the URL.
+ """
+ factory = client.HTTPClientFactory('https://foo.example.com:443/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com')
+
+
+ def test_HTTPSNotPort443(self):
+ """
+ The port should be included in the host header when connecting to the
+ a non default HTTPS port.
+ """
+ factory = client.HTTPClientFactory('http://foo.example.com:8080/')
+ proto = factory.buildProtocol('127.42.42.42')
+ proto.makeConnection(StringTransport())
+ self.assertEqual(self._getHost(proto.transport.value()),
+ 'foo.example.com:8080')
+
+
+
+class StubHTTPProtocol(Protocol):
+ """
+ A protocol like L{HTTP11ClientProtocol} but which does not actually know
+ HTTP/1.1 and only collects requests in a list.
+
+ @ivar requests: A C{list} of two-tuples. Each time a request is made, a
+ tuple consisting of the request and the L{Deferred} returned from the
+ request method is appended to this list.
+ """
+ def __init__(self):
+ self.requests = []
+ self.state = 'QUIESCENT'
+
+
+ def request(self, request):
+ """
+ Capture the given request for later inspection.
+
+ @return: A L{Deferred} which this code will never fire.
+ """
+ result = Deferred()
+ self.requests.append((request, result))
+ return result
+
+
+
+class FileConsumer(object):
+ def __init__(self, outputFile):
+ self.outputFile = outputFile
+
+
+ def write(self, bytes):
+ self.outputFile.write(bytes)
+
+
+
+class FileBodyProducerTests(unittest.TestCase):
+ """
+ Tests for the L{FileBodyProducer} which reads bytes from a file and writes
+ them to an L{IConsumer}.
+ """
+ _NO_RESULT = object()
+
+ def _resultNow(self, deferred):
+ """
+ Return the current result of C{deferred} if it is not a failure. If it
+ has no result, return C{self._NO_RESULT}. If it is a failure, raise an
+ exception.
+ """
+ result = []
+ failure = []
+ deferred.addCallbacks(result.append, failure.append)
+ if len(result) == 1:
+ return result[0]
+ elif len(failure) == 1:
+ raise Exception(
+ "Deferred had failure instead of success: %r" % (failure[0],))
+ return self._NO_RESULT
+
+
+ def _failureNow(self, deferred):
+ """
+ Return the current result of C{deferred} if it is a failure. If it has
+ no result, return C{self._NO_RESULT}. If it is not a failure, raise an
+ exception.
+ """
+ result = []
+ failure = []
+ deferred.addCallbacks(result.append, failure.append)
+ if len(result) == 1:
+ raise Exception(
+ "Deferred had success instead of failure: %r" % (result[0],))
+ elif len(failure) == 1:
+ return failure[0]
+ return self._NO_RESULT
+
+
+ def _termination(self):
+ """
+ This method can be used as the C{terminationPredicateFactory} for a
+ L{Cooperator}. It returns a predicate which immediately returns
+ C{False}, indicating that no more work should be done this iteration.
+ This has the result of only allowing one iteration of a cooperative
+ task to be run per L{Cooperator} iteration.
+ """
+ return lambda: True
+
+
+ def setUp(self):
+ """
+ Create a L{Cooperator} hooked up to an easily controlled, deterministic
+ scheduler to use with L{FileBodyProducer}.
+ """
+ self._scheduled = []
+ self.cooperator = task.Cooperator(
+ self._termination, self._scheduled.append)
+
+
+ def test_interface(self):
+ """
+ L{FileBodyProducer} instances provide L{IBodyProducer}.
+ """
+ self.assertTrue(verifyObject(
+ IBodyProducer, FileBodyProducer(StringIO(""))))
+
+
+ def test_unknownLength(self):
+ """
+ If the L{FileBodyProducer} is constructed with a file-like object
+ without either a C{seek} or C{tell} method, its C{length} attribute is
+ set to C{UNKNOWN_LENGTH}.
+ """
+ class HasSeek(object):
+ def seek(self, offset, whence):
+ pass
+
+ class HasTell(object):
+ def tell(self):
+ pass
+
+ producer = FileBodyProducer(HasSeek())
+ self.assertEqual(UNKNOWN_LENGTH, producer.length)
+ producer = FileBodyProducer(HasTell())
+ self.assertEqual(UNKNOWN_LENGTH, producer.length)
+
+
+ def test_knownLength(self):
+ """
+ If the L{FileBodyProducer} is constructed with a file-like object with
+ both C{seek} and C{tell} methods, its C{length} attribute is set to the
+ size of the file as determined by those methods.
+ """
+ inputBytes = "here are some bytes"
+ inputFile = StringIO(inputBytes)
+ inputFile.seek(5)
+ producer = FileBodyProducer(inputFile)
+ self.assertEqual(len(inputBytes) - 5, producer.length)
+ self.assertEqual(inputFile.tell(), 5)
+
+
+ def test_defaultCooperator(self):
+ """
+ If no L{Cooperator} instance is passed to L{FileBodyProducer}, the
+ global cooperator is used.
+ """
+ producer = FileBodyProducer(StringIO(""))
+ self.assertEqual(task.cooperate, producer._cooperate)
+
+
+ def test_startProducing(self):
+ """
+ L{FileBodyProducer.startProducing} starts writing bytes from the input
+ file to the given L{IConsumer} and returns a L{Deferred} which fires
+ when they have all been written.
+ """
+ expectedResult = "hello, world"
+ readSize = 3
+ output = StringIO()
+ consumer = FileConsumer(output)
+ producer = FileBodyProducer(
+ StringIO(expectedResult), self.cooperator, readSize)
+ complete = producer.startProducing(consumer)
+ for i in range(len(expectedResult) / readSize + 1):
+ self._scheduled.pop(0)()
+ self.assertEqual([], self._scheduled)
+ self.assertEqual(expectedResult, output.getvalue())
+ self.assertEqual(None, self._resultNow(complete))
+
+
+ def test_inputClosedAtEOF(self):
+ """
+ When L{FileBodyProducer} reaches end-of-file on the input file given to
+ it, the input file is closed.
+ """
+ readSize = 4
+ inputBytes = "some friendly bytes"
+ inputFile = StringIO(inputBytes)
+ producer = FileBodyProducer(inputFile, self.cooperator, readSize)
+ consumer = FileConsumer(StringIO())
+ producer.startProducing(consumer)
+ for i in range(len(inputBytes) / readSize + 2):
+ self._scheduled.pop(0)()
+ self.assertTrue(inputFile.closed)
+
+
+ def test_failedReadWhileProducing(self):
+ """
+ If a read from the input file fails while producing bytes to the
+ consumer, the L{Deferred} returned by
+ L{FileBodyProducer.startProducing} fires with a L{Failure} wrapping
+ that exception.
+ """
+ class BrokenFile(object):
+ def read(self, count):
+ raise IOError("Simulated bad thing")
+ producer = FileBodyProducer(BrokenFile(), self.cooperator)
+ complete = producer.startProducing(FileConsumer(StringIO()))
+ self._scheduled.pop(0)()
+ self._failureNow(complete).trap(IOError)
+
+
+ def test_stopProducing(self):
+ """
+ L{FileBodyProducer.stopProducing} stops the underlying L{IPullProducer}
+ and the cooperative task responsible for calling C{resumeProducing} and
+ closes the input file but does not cause the L{Deferred} returned by
+ C{startProducing} to fire.
+ """
+ expectedResult = "hello, world"
+ readSize = 3
+ output = StringIO()
+ consumer = FileConsumer(output)
+ inputFile = StringIO(expectedResult)
+ producer = FileBodyProducer(
+ inputFile, self.cooperator, readSize)
+ complete = producer.startProducing(consumer)
+ producer.stopProducing()
+ self.assertTrue(inputFile.closed)
+ self._scheduled.pop(0)()
+ self.assertEqual("", output.getvalue())
+ self.assertIdentical(self._NO_RESULT, self._resultNow(complete))
+
+
+ def test_pauseProducing(self):
+ """
+ L{FileBodyProducer.pauseProducing} temporarily suspends writing bytes
+ from the input file to the given L{IConsumer}.
+ """
+ expectedResult = "hello, world"
+ readSize = 5
+ output = StringIO()
+ consumer = FileConsumer(output)
+ producer = FileBodyProducer(
+ StringIO(expectedResult), self.cooperator, readSize)
+ complete = producer.startProducing(consumer)
+ self._scheduled.pop(0)()
+ self.assertEqual(output.getvalue(), expectedResult[:5])
+ producer.pauseProducing()
+
+ # Sort of depends on an implementation detail of Cooperator: even
+ # though the only task is paused, there's still a scheduled call. If
+ # this were to go away because Cooperator became smart enough to cancel
+ # this call in this case, that would be fine.
+ self._scheduled.pop(0)()
+
+ # Since the producer is paused, no new data should be here.
+ self.assertEqual(output.getvalue(), expectedResult[:5])
+ self.assertEqual([], self._scheduled)
+ self.assertIdentical(self._NO_RESULT, self._resultNow(complete))
+
+
+ def test_resumeProducing(self):
+ """
+ L{FileBodyProducer.resumeProducing} re-commences writing bytes from the
+ input file to the given L{IConsumer} after it was previously paused
+ with L{FileBodyProducer.pauseProducing}.
+ """
+ expectedResult = "hello, world"
+ readSize = 5
+ output = StringIO()
+ consumer = FileConsumer(output)
+ producer = FileBodyProducer(
+ StringIO(expectedResult), self.cooperator, readSize)
+ producer.startProducing(consumer)
+ self._scheduled.pop(0)()
+ self.assertEqual(expectedResult[:readSize], output.getvalue())
+ producer.pauseProducing()
+ producer.resumeProducing()
+ self._scheduled.pop(0)()
+ self.assertEqual(expectedResult[:readSize * 2], output.getvalue())
+
+
+
+class FakeReactorAndConnectMixin:
+ """
+ A test mixin providing a testable C{Reactor} class and a dummy C{connect}
+ method which allows instances to pretend to be endpoints.
+ """
+
+ class Reactor(MemoryReactor, Clock):
+ def __init__(self):
+ MemoryReactor.__init__(self)
+ Clock.__init__(self)
+
+
+ class StubEndpoint(object):
+ """
+ Endpoint that wraps existing endpoint, substitutes StubHTTPProtocol, and
+ resulting protocol instances are attached to the given test case.
+ """
+
+ def __init__(self, endpoint, testCase):
+ self.endpoint = endpoint
+ self.testCase = testCase
+ self.factory = _HTTP11ClientFactory(lambda p: None)
+ self.protocol = StubHTTPProtocol()
+ self.factory.buildProtocol = lambda addr: self.protocol
+
+ def connect(self, ignoredFactory):
+ self.testCase.protocol = self.protocol
+ self.endpoint.connect(self.factory)
+ return succeed(self.protocol)
+
+
+ def buildAgentForWrapperTest(self, reactor):
+ """
+ Return an Agent suitable for use in tests that wrap the Agent and want
+ both a fake reactor and StubHTTPProtocol.
+ """
+ agent = client.Agent(reactor)
+ _oldGetEndpoint = agent._getEndpoint
+ agent._getEndpoint = lambda *args: (
+ self.StubEndpoint(_oldGetEndpoint(*args), self))
+ return agent
+
+
+ def connect(self, factory):
+ """
+ Fake implementation of an endpoint which synchronously
+ succeeds with an instance of L{StubHTTPProtocol} for ease of
+ testing.
+ """
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(None)
+ self.protocol = protocol
+ return succeed(protocol)
+
+
+
+class DummyEndpoint(object):
+ """
+ An endpoint that uses a fake transport.
+ """
+
+ def connect(self, factory):
+ protocol = factory.buildProtocol(None)
+ protocol.makeConnection(StringTransport())
+ return succeed(protocol)
+
+
+
+class BadEndpoint(object):
+ """
+ An endpoint that shouldn't be called.
+ """
+
+ def connect(self, factory):
+ raise RuntimeError("This endpoint should not have been used.")
+
+
+class DummyFactory(Factory):
+ """
+ Create C{StubHTTPProtocol} instances.
+ """
+ def __init__(self, quiescentCallback):
+ pass
+
+ protocol = StubHTTPProtocol
+
+
+
+class HTTPConnectionPoolTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ Tests for the L{HTTPConnectionPool} class.
+ """
+
+ def setUp(self):
+ self.fakeReactor = self.Reactor()
+ self.pool = HTTPConnectionPool(self.fakeReactor)
+ self.pool._factory = DummyFactory
+ # The retry code path is tested in HTTPConnectionPoolRetryTests:
+ self.pool.retryAutomatically = False
+
+
+ def test_getReturnsNewIfCacheEmpty(self):
+ """
+ If there are no cached connections,
+ L{HTTPConnectionPool.getConnection} returns a new connection.
+ """
+ self.assertEqual(self.pool._connections, {})
+
+ def gotConnection(conn):
+ self.assertIsInstance(conn, StubHTTPProtocol)
+ # The new connection is not stored in the pool:
+ self.assertNotIn(conn, self.pool._connections.values())
+
+ unknownKey = 12245
+ d = self.pool.getConnection(unknownKey, DummyEndpoint())
+ return d.addCallback(gotConnection)
+
+
+ def test_putStartsTimeout(self):
+ """
+ If a connection is put back to the pool, a 240-sec timeout is started.
+
+ When the timeout hits, the connection is closed and removed from the
+ pool.
+ """
+ # We start out with one cached connection:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ self.pool._putConnection(("http", "example.com", 80), protocol)
+
+ # Connection is in pool, still not closed:
+ self.assertEqual(protocol.transport.disconnecting, False)
+ self.assertIn(protocol,
+ self.pool._connections[("http", "example.com", 80)])
+
+ # Advance 239 seconds, still not closed:
+ self.fakeReactor.advance(239)
+ self.assertEqual(protocol.transport.disconnecting, False)
+ self.assertIn(protocol,
+ self.pool._connections[("http", "example.com", 80)])
+ self.assertIn(protocol, self.pool._timeouts)
+
+ # Advance past 240 seconds, connection will be closed:
+ self.fakeReactor.advance(1.1)
+ self.assertEqual(protocol.transport.disconnecting, True)
+ self.assertNotIn(protocol,
+ self.pool._connections[("http", "example.com", 80)])
+ self.assertNotIn(protocol, self.pool._timeouts)
+
+
+ def test_putExceedsMaxPersistent(self):
+ """
+ If an idle connection is put back in the cache and the max number of
+ persistent connections has been exceeded, one of the connections is
+ closed and removed from the cache.
+ """
+ pool = self.pool
+
+ # We start out with two cached connection, the max:
+ origCached = [StubHTTPProtocol(), StubHTTPProtocol()]
+ for p in origCached:
+ p.makeConnection(StringTransport())
+ pool._putConnection(("http", "example.com", 80), p)
+ self.assertEqual(pool._connections[("http", "example.com", 80)],
+ origCached)
+ timeouts = pool._timeouts.copy()
+
+ # Now we add another one:
+ newProtocol = StubHTTPProtocol()
+ newProtocol.makeConnection(StringTransport())
+ pool._putConnection(("http", "example.com", 80), newProtocol)
+
+ # The oldest cached connections will be removed and disconnected:
+ newCached = pool._connections[("http", "example.com", 80)]
+ self.assertEqual(len(newCached), 2)
+ self.assertEqual(newCached, [origCached[1], newProtocol])
+ self.assertEqual([p.transport.disconnecting for p in newCached],
+ [False, False])
+ self.assertEqual(origCached[0].transport.disconnecting, True)
+ self.assertTrue(timeouts[origCached[0]].cancelled)
+ self.assertNotIn(origCached[0], pool._timeouts)
+
+
+ def test_maxPersistentPerHost(self):
+ """
+ C{maxPersistentPerHost} is enforced per C{(scheme, host, port)}:
+ different keys have different max connections.
+ """
+ def addProtocol(scheme, host, port):
+ p = StubHTTPProtocol()
+ p.makeConnection(StringTransport())
+ self.pool._putConnection((scheme, host, port), p)
+ return p
+ persistent = []
+ persistent.append(addProtocol("http", "example.com", 80))
+ persistent.append(addProtocol("http", "example.com", 80))
+ addProtocol("https", "example.com", 443)
+ addProtocol("http", "www2.example.com", 80)
+
+ self.assertEqual(
+ self.pool._connections[("http", "example.com", 80)], persistent)
+ self.assertEqual(
+ len(self.pool._connections[("https", "example.com", 443)]), 1)
+ self.assertEqual(
+ len(self.pool._connections[("http", "www2.example.com", 80)]), 1)
+
+
+ def test_getCachedConnection(self):
+ """
+ Getting an address which has a cached connection returns the cached
+ connection, removes it from the cache and cancels its timeout.
+ """
+ # We start out with one cached connection:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ self.pool._putConnection(("http", "example.com", 80), protocol)
+
+ def gotConnection(conn):
+ # We got the cached connection:
+ self.assertIdentical(protocol, conn)
+ self.assertNotIn(
+ conn, self.pool._connections[("http", "example.com", 80)])
+ # And the timeout was cancelled:
+ self.fakeReactor.advance(241)
+ self.assertEqual(conn.transport.disconnecting, False)
+ self.assertNotIn(conn, self.pool._timeouts)
+
+ return self.pool.getConnection(("http", "example.com", 80),
+ BadEndpoint(),
+ ).addCallback(gotConnection)
+
+
+ def test_newConnection(self):
+ """
+ The pool's C{_newConnection} method constructs a new connection.
+ """
+ # We start out with one cached connection:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ key = 12245
+ self.pool._putConnection(key, protocol)
+
+ def gotConnection(newConnection):
+ # We got a new connection:
+ self.assertNotIdentical(protocol, newConnection)
+ # And the old connection is still there:
+ self.assertIn(protocol, self.pool._connections[key])
+ # While the new connection is not:
+ self.assertNotIn(newConnection, self.pool._connections.values())
+
+ d = self.pool._newConnection(key, DummyEndpoint())
+ return d.addCallback(gotConnection)
+
+
+ def test_getSkipsDisconnected(self):
+ """
+ When getting connections out of the cache, disconnected connections
+ are removed and not returned.
+ """
+ pool = self.pool
+ key = ("http", "example.com", 80)
+
+ # We start out with two cached connection, the max:
+ origCached = [StubHTTPProtocol(), StubHTTPProtocol()]
+ for p in origCached:
+ p.makeConnection(StringTransport())
+ pool._putConnection(key, p)
+ self.assertEqual(pool._connections[key], origCached)
+
+ # We close the first one:
+ origCached[0].state = "DISCONNECTED"
+
+ # Now, when we retrive connections we should get the *second* one:
+ result = []
+ self.pool.getConnection(key,
+ BadEndpoint()).addCallback(result.append)
+ self.assertIdentical(result[0], origCached[1])
+
+ # And both the disconnected and removed connections should be out of
+ # the cache:
+ self.assertEqual(pool._connections[key], [])
+ self.assertEqual(pool._timeouts, {})
+
+
+ def test_putNotQuiescent(self):
+ """
+ If a non-quiescent connection is put back in the cache, an error is
+ logged.
+ """
+ protocol = StubHTTPProtocol()
+ # By default state is QUIESCENT
+ self.assertEqual(protocol.state, "QUIESCENT")
+
+ protocol.state = "NOTQUIESCENT"
+ self.pool._putConnection(("http", "example.com", 80), protocol)
+ error, = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(
+ error.value.args[0],
+ "BUG: Non-quiescent protocol added to connection pool.")
+ self.assertIdentical(None, self.pool._connections.get(
+ ("http", "example.com", 80)))
+
+
+ def test_getUsesQuiescentCallback(self):
+ """
+ When L{HTTPConnectionPool.getConnection} connects, it returns a
+ C{Deferred} that fires with an instance of L{HTTP11ClientProtocol}
+ that has the correct quiescent callback attached. When this callback
+ is called the protocol is returned to the cache correctly, using the
+ right key.
+ """
+ class StringEndpoint(object):
+ def connect(self, factory):
+ p = factory.buildProtocol(None)
+ p.makeConnection(StringTransport())
+ return succeed(p)
+
+ pool = HTTPConnectionPool(self.fakeReactor, True)
+ pool.retryAutomatically = False
+ result = []
+ key = "a key"
+ pool.getConnection(
+ key, StringEndpoint()).addCallback(
+ result.append)
+ protocol = result[0]
+ self.assertIsInstance(protocol, HTTP11ClientProtocol)
+
+ # Now that we have protocol instance, lets try to put it back in the
+ # pool:
+ protocol._state = "QUIESCENT"
+ protocol._quiescentCallback(protocol)
+
+ # If we try to retrive a connection to same destination again, we
+ # should get the same protocol, because it should've been added back
+ # to the pool:
+ result2 = []
+ pool.getConnection(
+ key, StringEndpoint()).addCallback(
+ result2.append)
+ self.assertIdentical(result2[0], protocol)
+
+
+ def test_closeCachedConnections(self):
+ """
+ L{HTTPConnectionPool.closeCachedConnections} closes all cached
+ connections and removes them from the cache. It returns a Deferred
+ that fires when they have all lost their connections.
+ """
+ persistent = []
+ def addProtocol(scheme, host, port):
+ p = HTTP11ClientProtocol()
+ p.makeConnection(StringTransport())
+ self.pool._putConnection((scheme, host, port), p)
+ persistent.append(p)
+ addProtocol("http", "example.com", 80)
+ addProtocol("http", "www2.example.com", 80)
+ doneDeferred = self.pool.closeCachedConnections()
+
+ # Connections have begun disconnecting:
+ for p in persistent:
+ self.assertEqual(p.transport.disconnecting, True)
+ self.assertEqual(self.pool._connections, {})
+ # All timeouts were cancelled and removed:
+ for dc in self.fakeReactor.getDelayedCalls():
+ self.assertEqual(dc.cancelled, True)
+ self.assertEqual(self.pool._timeouts, {})
+
+ # Returned Deferred fires when all connections have been closed:
+ result = []
+ doneDeferred.addCallback(result.append)
+ self.assertEqual(result, [])
+ persistent[0].connectionLost(Failure(ConnectionDone()))
+ self.assertEqual(result, [])
+ persistent[1].connectionLost(Failure(ConnectionDone()))
+ self.assertEqual(result, [None])
+
+
+
+class AgentTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ Tests for the new HTTP client API provided by L{Agent}.
+ """
+ def setUp(self):
+ """
+ Create an L{Agent} wrapped around a fake reactor.
+ """
+ self.reactor = self.Reactor()
+ self.agent = client.Agent(self.reactor)
+
+
+ def completeConnection(self):
+ """
+ Do whitebox stuff to finish any outstanding connection attempts the
+ agent may have initiated.
+
+ This spins the fake reactor clock just enough to get L{ClientCreator},
+ which agent is implemented in terms of, to fire its Deferreds.
+ """
+ self.reactor.advance(0)
+
+
+ def test_defaultPool(self):
+ """
+ If no pool is passed in, the L{Agent} creates a non-persistent pool.
+ """
+ agent = client.Agent(self.reactor)
+ self.assertIsInstance(agent._pool, HTTPConnectionPool)
+ self.assertEqual(agent._pool.persistent, False)
+ self.assertIdentical(agent._reactor, agent._pool._reactor)
+
+
+ def test_persistent(self):
+ """
+ If C{persistent} is set to C{True} on the L{HTTPConnectionPool} (the
+ default), C{Request}s are created with their C{persistent} flag set to
+ C{True}.
+ """
+ pool = HTTPConnectionPool(self.reactor)
+ agent = client.Agent(self.reactor, pool=pool)
+ agent._getEndpoint = lambda *args: self
+ agent.request("GET", "http://127.0.0.1")
+ self.assertEqual(self.protocol.requests[0][0].persistent, True)
+
+
+ def test_nonPersistent(self):
+ """
+ If C{persistent} is set to C{False} when creating the
+ L{HTTPConnectionPool}, C{Request}s are created with their
+ C{persistent} flag set to C{False}.
+
+ Elsewhere in the tests for the underlying HTTP code we ensure that
+ this will result in the disconnection of the HTTP protocol once the
+ request is done, so that the connection will not be returned to the
+ pool.
+ """
+ pool = HTTPConnectionPool(self.reactor, persistent=False)
+ agent = client.Agent(self.reactor, pool=pool)
+ agent._getEndpoint = lambda *args: self
+ agent.request("GET", "http://127.0.0.1")
+ self.assertEqual(self.protocol.requests[0][0].persistent, False)
+
+
+ def test_connectUsesConnectionPool(self):
+ """
+ When a connection is made by the Agent, it uses its pool's
+ C{getConnection} method to do so, with the endpoint returned by
+ C{self._getEndpoint}. The key used is C{(scheme, host, port)}.
+ """
+ endpoint = DummyEndpoint()
+ class MyAgent(client.Agent):
+ def _getEndpoint(this, scheme, host, port):
+ self.assertEqual((scheme, host, port),
+ ("http", "foo", 80))
+ return endpoint
+
+ class DummyPool(object):
+ connected = False
+ persistent = False
+ def getConnection(this, key, ep):
+ this.connected = True
+ self.assertEqual(ep, endpoint)
+ # This is the key the default Agent uses, others will have
+ # different keys:
+ self.assertEqual(key, ("http", "foo", 80))
+ return defer.succeed(StubHTTPProtocol())
+
+ pool = DummyPool()
+ agent = MyAgent(self.reactor, pool=pool)
+ self.assertIdentical(pool, agent._pool)
+
+ headers = http_headers.Headers()
+ headers.addRawHeader("host", "foo")
+ bodyProducer = object()
+ agent.request('GET', 'http://foo/',
+ bodyProducer=bodyProducer, headers=headers)
+ self.assertEqual(agent._pool.connected, True)
+
+
+ def test_unsupportedScheme(self):
+ """
+ L{Agent.request} returns a L{Deferred} which fails with
+ L{SchemeNotSupported} if the scheme of the URI passed to it is not
+ C{'http'}.
+ """
+ return self.assertFailure(
+ self.agent.request('GET', 'mailto:alice@example.com'),
+ SchemeNotSupported)
+
+
+ def test_connectionFailed(self):
+ """
+ The L{Deferred} returned by L{Agent.request} fires with a L{Failure} if
+ the TCP connection attempt fails.
+ """
+ result = self.agent.request('GET', 'http://foo/')
+ # Cause the connection to be refused
+ host, port, factory = self.reactor.tcpClients.pop()[:3]
+ factory.clientConnectionFailed(None, Failure(ConnectionRefusedError()))
+ self.completeConnection()
+ return self.assertFailure(result, ConnectionRefusedError)
+
+
+ def test_connectHTTP(self):
+ """
+ L{Agent._getEndpoint} return a C{TCP4ClientEndpoint} when passed a
+ scheme of C{'http'}.
+ """
+ expectedHost = 'example.com'
+ expectedPort = 1234
+ endpoint = self.agent._getEndpoint('http', expectedHost, expectedPort)
+ self.assertEqual(endpoint._host, expectedHost)
+ self.assertEqual(endpoint._port, expectedPort)
+ self.assertIsInstance(endpoint, TCP4ClientEndpoint)
+
+
+ def test_connectHTTPS(self):
+ """
+ L{Agent._getEndpoint} return a C{SSL4ClientEndpoint} when passed a
+ scheme of C{'https'}.
+ """
+ expectedHost = 'example.com'
+ expectedPort = 4321
+ endpoint = self.agent._getEndpoint('https', expectedHost, expectedPort)
+ self.assertIsInstance(endpoint, SSL4ClientEndpoint)
+ self.assertEqual(endpoint._host, expectedHost)
+ self.assertEqual(endpoint._port, expectedPort)
+ self.assertIsInstance(endpoint._sslContextFactory,
+ _WebToNormalContextFactory)
+ # Default context factory was used:
+ self.assertIsInstance(endpoint._sslContextFactory._webContext,
+ WebClientContextFactory)
+ if ssl is None:
+ test_connectHTTPS.skip = "OpenSSL not present"
+
+
+ def test_connectHTTPSCustomContextFactory(self):
+ """
+ If a context factory is passed to L{Agent.__init__} it will be used to
+ determine the SSL parameters for HTTPS requests. When an HTTPS request
+ is made, the hostname and port number of the request URL will be passed
+ to the context factory's C{getContext} method. The resulting context
+ object will be used to establish the SSL connection.
+ """
+ expectedHost = 'example.org'
+ expectedPort = 20443
+ expectedContext = object()
+
+ contextArgs = []
+ class StubWebContextFactory(object):
+ def getContext(self, hostname, port):
+ contextArgs.append((hostname, port))
+ return expectedContext
+
+ agent = client.Agent(self.reactor, StubWebContextFactory())
+ endpoint = agent._getEndpoint('https', expectedHost, expectedPort)
+ contextFactory = endpoint._sslContextFactory
+ context = contextFactory.getContext()
+ self.assertEqual(context, expectedContext)
+ self.assertEqual(contextArgs, [(expectedHost, expectedPort)])
+
+
+ def test_hostProvided(self):
+ """
+ If C{None} is passed to L{Agent.request} for the C{headers} parameter,
+ a L{Headers} instance is created for the request and a I{Host} header
+ added to it.
+ """
+ self.agent._getEndpoint = lambda *args: self
+ self.agent.request(
+ 'GET', 'http://example.com/foo?bar')
+
+ req, res = self.protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('host'), ['example.com'])
+
+
+ def test_hostOverride(self):
+ """
+ If the headers passed to L{Agent.request} includes a value for the
+ I{Host} header, that value takes precedence over the one which would
+ otherwise be automatically provided.
+ """
+ headers = http_headers.Headers({'foo': ['bar'], 'host': ['quux']})
+ self.agent._getEndpoint = lambda *args: self
+ self.agent.request(
+ 'GET', 'http://example.com/foo?bar', headers)
+
+ req, res = self.protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('host'), ['quux'])
+
+
+ def test_headersUnmodified(self):
+ """
+ If a I{Host} header must be added to the request, the L{Headers}
+ instance passed to L{Agent.request} is not modified.
+ """
+ headers = http_headers.Headers()
+ self.agent._getEndpoint = lambda *args: self
+ self.agent.request(
+ 'GET', 'http://example.com/foo', headers)
+
+ protocol = self.protocol
+
+ # The request should have been issued.
+ self.assertEqual(len(protocol.requests), 1)
+ # And the headers object passed in should not have changed.
+ self.assertEqual(headers, http_headers.Headers())
+
+
+ def test_hostValueStandardHTTP(self):
+ """
+ When passed a scheme of C{'http'} and a port of C{80},
+ L{Agent._computeHostValue} returns a string giving just
+ the host name passed to it.
+ """
+ self.assertEqual(
+ self.agent._computeHostValue('http', 'example.com', 80),
+ 'example.com')
+
+
+ def test_hostValueNonStandardHTTP(self):
+ """
+ When passed a scheme of C{'http'} and a port other than C{80},
+ L{Agent._computeHostValue} returns a string giving the
+ host passed to it joined together with the port number by C{":"}.
+ """
+ self.assertEqual(
+ self.agent._computeHostValue('http', 'example.com', 54321),
+ 'example.com:54321')
+
+
+ def test_hostValueStandardHTTPS(self):
+ """
+ When passed a scheme of C{'https'} and a port of C{443},
+ L{Agent._computeHostValue} returns a string giving just
+ the host name passed to it.
+ """
+ self.assertEqual(
+ self.agent._computeHostValue('https', 'example.com', 443),
+ 'example.com')
+
+
+ def test_hostValueNonStandardHTTPS(self):
+ """
+ When passed a scheme of C{'https'} and a port other than C{443},
+ L{Agent._computeHostValue} returns a string giving the
+ host passed to it joined together with the port number by C{":"}.
+ """
+ self.assertEqual(
+ self.agent._computeHostValue('https', 'example.com', 54321),
+ 'example.com:54321')
+
+
+ def test_request(self):
+ """
+ L{Agent.request} establishes a new connection to the host indicated by
+ the host part of the URI passed to it and issues a request using the
+ method, the path portion of the URI, the headers, and the body producer
+ passed to it. It returns a L{Deferred} which fires with an
+ L{IResponse} from the server.
+ """
+ self.agent._getEndpoint = lambda *args: self
+
+ headers = http_headers.Headers({'foo': ['bar']})
+ # Just going to check the body for identity, so it doesn't need to be
+ # real.
+ body = object()
+ self.agent.request(
+ 'GET', 'http://example.com:1234/foo?bar', headers, body)
+
+ protocol = self.protocol
+
+ # The request should be issued.
+ self.assertEqual(len(protocol.requests), 1)
+ req, res = protocol.requests.pop()
+ self.assertIsInstance(req, Request)
+ self.assertEqual(req.method, 'GET')
+ self.assertEqual(req.uri, '/foo?bar')
+ self.assertEqual(
+ req.headers,
+ http_headers.Headers({'foo': ['bar'],
+ 'host': ['example.com:1234']}))
+ self.assertIdentical(req.bodyProducer, body)
+
+
+ def test_connectTimeout(self):
+ """
+ L{Agent} takes a C{connectTimeout} argument which is forwarded to the
+ following C{connectTCP} agent.
+ """
+ agent = client.Agent(self.reactor, connectTimeout=5)
+ agent.request('GET', 'http://foo/')
+ timeout = self.reactor.tcpClients.pop()[3]
+ self.assertEqual(5, timeout)
+
+
+ def test_connectSSLTimeout(self):
+ """
+ L{Agent} takes a C{connectTimeout} argument which is forwarded to the
+ following C{connectSSL} call.
+ """
+ agent = client.Agent(self.reactor, connectTimeout=5)
+ agent.request('GET', 'https://foo/')
+ timeout = self.reactor.sslClients.pop()[4]
+ self.assertEqual(5, timeout)
+
+
+ def test_bindAddress(self):
+ """
+ L{Agent} takes a C{bindAddress} argument which is forwarded to the
+ following C{connectTCP} call.
+ """
+ agent = client.Agent(self.reactor, bindAddress='192.168.0.1')
+ agent.request('GET', 'http://foo/')
+ address = self.reactor.tcpClients.pop()[4]
+ self.assertEqual('192.168.0.1', address)
+
+
+ def test_bindAddressSSL(self):
+ """
+ L{Agent} takes a C{bindAddress} argument which is forwarded to the
+ following C{connectSSL} call.
+ """
+ agent = client.Agent(self.reactor, bindAddress='192.168.0.1')
+ agent.request('GET', 'https://foo/')
+ address = self.reactor.sslClients.pop()[5]
+ self.assertEqual('192.168.0.1', address)
+
+
+
+class HTTPConnectionPoolRetryTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ L{client.HTTPConnectionPool}, by using
+ L{client._RetryingHTTP11ClientProtocol}, supports retrying requests done
+ against previously cached connections.
+ """
+
+ def test_onlyRetryIdempotentMethods(self):
+ """
+ Only GET, HEAD, OPTIONS, TRACE, DELETE methods should cause a retry.
+ """
+ pool = client.HTTPConnectionPool(None)
+ connection = client._RetryingHTTP11ClientProtocol(None, pool)
+ self.assertTrue(connection._shouldRetry("GET", RequestNotSent(), None))
+ self.assertTrue(connection._shouldRetry("HEAD", RequestNotSent(), None))
+ self.assertTrue(connection._shouldRetry(
+ "OPTIONS", RequestNotSent(), None))
+ self.assertTrue(connection._shouldRetry(
+ "TRACE", RequestNotSent(), None))
+ self.assertTrue(connection._shouldRetry(
+ "DELETE", RequestNotSent(), None))
+ self.assertFalse(connection._shouldRetry(
+ "POST", RequestNotSent(), None))
+ self.assertFalse(connection._shouldRetry(
+ "MYMETHOD", RequestNotSent(), None))
+ # This will be covered by a different ticket, since we need support
+ #for resettable body producers:
+ # self.assertTrue(connection._doRetry("PUT", RequestNotSent(), None))
+
+
+ def test_onlyRetryIfNoResponseReceived(self):
+ """
+ Only L{RequestNotSent}, L{RequestTransmissionFailed} and
+ L{ResponseNeverReceived} exceptions should be a cause for retrying.
+ """
+ pool = client.HTTPConnectionPool(None)
+ connection = client._RetryingHTTP11ClientProtocol(None, pool)
+ self.assertTrue(connection._shouldRetry("GET", RequestNotSent(), None))
+ self.assertTrue(connection._shouldRetry(
+ "GET", RequestTransmissionFailed([]), None))
+ self.assertTrue(connection._shouldRetry(
+ "GET", ResponseNeverReceived([]),None))
+ self.assertFalse(connection._shouldRetry(
+ "GET", ResponseFailed([]), None))
+ self.assertFalse(connection._shouldRetry(
+ "GET", ConnectionRefusedError(), None))
+
+
+ def test_wrappedOnPersistentReturned(self):
+ """
+ If L{client.HTTPConnectionPool.getConnection} returns a previously
+ cached connection, it will get wrapped in a
+ L{client._RetryingHTTP11ClientProtocol}.
+ """
+ pool = client.HTTPConnectionPool(Clock())
+
+ # Add a connection to the cache:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ pool._putConnection(123, protocol)
+
+ # Retrieve it, it should come back wrapped in a
+ # _RetryingHTTP11ClientProtocol:
+ d = pool.getConnection(123, DummyEndpoint())
+
+ def gotConnection(connection):
+ self.assertIsInstance(connection,
+ client._RetryingHTTP11ClientProtocol)
+ self.assertIdentical(connection._clientProtocol, protocol)
+ return d.addCallback(gotConnection)
+
+
+ def test_notWrappedOnNewReturned(self):
+ """
+ If L{client.HTTPConnectionPool.getConnection} returns a new
+ connection, it will be returned as is.
+ """
+ pool = client.HTTPConnectionPool(None)
+ d = pool.getConnection(123, DummyEndpoint())
+
+ def gotConnection(connection):
+ # Don't want to use isinstance since potentially the wrapper might
+ # subclass it at some point:
+ self.assertIdentical(connection.__class__, HTTP11ClientProtocol)
+ return d.addCallback(gotConnection)
+
+
+ def retryAttempt(self, willWeRetry):
+ """
+ Fail a first request, possibly retrying depending on argument.
+ """
+ protocols = []
+ def newProtocol():
+ protocol = StubHTTPProtocol()
+ protocols.append(protocol)
+ return defer.succeed(protocol)
+
+ bodyProducer = object()
+ request = client.Request("FOO", "/", client.Headers(), bodyProducer,
+ persistent=True)
+ newProtocol()
+ protocol = protocols[0]
+ retrier = client._RetryingHTTP11ClientProtocol(protocol, newProtocol)
+
+ def _shouldRetry(m, e, bp):
+ self.assertEqual(m, "FOO")
+ self.assertIdentical(bp, bodyProducer)
+ self.assertIsInstance(e, (RequestNotSent, ResponseNeverReceived))
+ return willWeRetry
+ retrier._shouldRetry = _shouldRetry
+
+ d = retrier.request(request)
+
+ # So far, one request made:
+ self.assertEqual(len(protocols), 1)
+ self.assertEqual(len(protocols[0].requests), 1)
+
+ # Fail the first request:
+ protocol.requests[0][1].errback(RequestNotSent())
+ return d, protocols
+
+
+ def test_retryIfShouldRetryReturnsTrue(self):
+ """
+ L{client._RetryingHTTP11ClientProtocol} retries when
+ L{client._RetryingHTTP11ClientProtocol._shouldRetry} returns C{True}.
+ """
+ d, protocols = self.retryAttempt(True)
+ # We retried!
+ self.assertEqual(len(protocols), 2)
+ response = object()
+ protocols[1].requests[0][1].callback(response)
+ return d.addCallback(self.assertIdentical, response)
+
+
+ def test_dontRetryIfShouldRetryReturnsFalse(self):
+ """
+ L{client._RetryingHTTP11ClientProtocol} does not retry when
+ L{client._RetryingHTTP11ClientProtocol._shouldRetry} returns C{False}.
+ """
+ d, protocols = self.retryAttempt(False)
+ # We did not retry:
+ self.assertEqual(len(protocols), 1)
+ return self.assertFailure(d, RequestNotSent)
+
+
+ def test_onlyRetryWithoutBody(self):
+ """
+ L{_RetryingHTTP11ClientProtocol} only retries queries that don't have
+ a body.
+
+ This is an implementation restriction; if the restriction is fixed,
+ this test should be removed and PUT added to list of methods that
+ support retries.
+ """
+ pool = client.HTTPConnectionPool(None)
+ connection = client._RetryingHTTP11ClientProtocol(None, pool)
+ self.assertTrue(connection._shouldRetry("GET", RequestNotSent(), None))
+ self.assertFalse(connection._shouldRetry("GET", RequestNotSent(), object()))
+
+
+ def test_onlyRetryOnce(self):
+ """
+ If a L{client._RetryingHTTP11ClientProtocol} fails more than once on
+ an idempotent query before a response is received, it will not retry.
+ """
+ d, protocols = self.retryAttempt(True)
+ self.assertEqual(len(protocols), 2)
+ # Fail the second request too:
+ protocols[1].requests[0][1].errback(ResponseNeverReceived([]))
+ # We didn't retry again:
+ self.assertEqual(len(protocols), 2)
+ return self.assertFailure(d, ResponseNeverReceived)
+
+
+ def test_dontRetryIfRetryAutomaticallyFalse(self):
+ """
+ If L{HTTPConnectionPool.retryAutomatically} is set to C{False}, don't
+ wrap connections with retrying logic.
+ """
+ pool = client.HTTPConnectionPool(Clock())
+ pool.retryAutomatically = False
+
+ # Add a connection to the cache:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ pool._putConnection(123, protocol)
+
+ # Retrieve it, it should come back unwrapped:
+ d = pool.getConnection(123, DummyEndpoint())
+
+ def gotConnection(connection):
+ self.assertIdentical(connection, protocol)
+ return d.addCallback(gotConnection)
+
+
+ def test_retryWithNewConnection(self):
+ """
+ L{client.HTTPConnectionPool} creates
+ {client._RetryingHTTP11ClientProtocol} with a new connection factory
+ method that creates a new connection using the same key and endpoint
+ as the wrapped connection.
+ """
+ pool = client.HTTPConnectionPool(Clock())
+ key = 123
+ endpoint = DummyEndpoint()
+ newConnections = []
+
+ # Override the pool's _newConnection:
+ def newConnection(k, e):
+ newConnections.append((k, e))
+ pool._newConnection = newConnection
+
+ # Add a connection to the cache:
+ protocol = StubHTTPProtocol()
+ protocol.makeConnection(StringTransport())
+ pool._putConnection(key, protocol)
+
+ # Retrieve it, it should come back wrapped in a
+ # _RetryingHTTP11ClientProtocol:
+ d = pool.getConnection(key, endpoint)
+
+ def gotConnection(connection):
+ self.assertIsInstance(connection,
+ client._RetryingHTTP11ClientProtocol)
+ self.assertIdentical(connection._clientProtocol, protocol)
+ # Verify that the _newConnection method on retrying connection
+ # calls _newConnection on the pool:
+ self.assertEqual(newConnections, [])
+ connection._newConnection()
+ self.assertEqual(len(newConnections), 1)
+ self.assertEqual(newConnections[0][0], key)
+ self.assertIdentical(newConnections[0][1], endpoint)
+ return d.addCallback(gotConnection)
+
+
+
+
+class CookieTestsMixin(object):
+ """
+ Mixin for unit tests dealing with cookies.
+ """
+ def addCookies(self, cookieJar, uri, cookies):
+ """
+ Add a cookie to a cookie jar.
+ """
+ response = client._FakeUrllib2Response(
+ client.Response(
+ ('HTTP', 1, 1),
+ 200,
+ 'OK',
+ client.Headers({'Set-Cookie': cookies}),
+ None))
+ request = client._FakeUrllib2Request(uri)
+ cookieJar.extract_cookies(response, request)
+ return request, response
+
+
+
+class CookieJarTests(unittest.TestCase, CookieTestsMixin):
+ """
+ Tests for L{twisted.web.client._FakeUrllib2Response} and
+ L{twisted.web.client._FakeUrllib2Request}'s interactions with
+ C{cookielib.CookieJar} instances.
+ """
+ def makeCookieJar(self):
+ """
+ Create a C{cookielib.CookieJar} with some sample cookies.
+ """
+ cookieJar = cookielib.CookieJar()
+ reqres = self.addCookies(
+ cookieJar,
+ 'http://example.com:1234/foo?bar',
+ ['foo=1; cow=moo; Path=/foo; Comment=hello',
+ 'bar=2; Comment=goodbye'])
+ return cookieJar, reqres
+
+
+ def test_extractCookies(self):
+ """
+ L{cookielib.CookieJar.extract_cookies} extracts cookie information from
+ fake urllib2 response instances.
+ """
+ jar = self.makeCookieJar()[0]
+ cookies = dict([(c.name, c) for c in jar])
+
+ cookie = cookies['foo']
+ self.assertEqual(cookie.version, 0)
+ self.assertEqual(cookie.name, 'foo')
+ self.assertEqual(cookie.value, '1')
+ self.assertEqual(cookie.path, '/foo')
+ self.assertEqual(cookie.comment, 'hello')
+ self.assertEqual(cookie.get_nonstandard_attr('cow'), 'moo')
+
+ cookie = cookies['bar']
+ self.assertEqual(cookie.version, 0)
+ self.assertEqual(cookie.name, 'bar')
+ self.assertEqual(cookie.value, '2')
+ self.assertEqual(cookie.path, '/')
+ self.assertEqual(cookie.comment, 'goodbye')
+ self.assertIdentical(cookie.get_nonstandard_attr('cow'), None)
+
+
+ def test_sendCookie(self):
+ """
+ L{cookielib.CookieJar.add_cookie_header} adds a cookie header to a fake
+ urllib2 request instance.
+ """
+ jar, (request, response) = self.makeCookieJar()
+
+ self.assertIdentical(
+ request.get_header('Cookie', None),
+ None)
+
+ jar.add_cookie_header(request)
+ self.assertEqual(
+ request.get_header('Cookie', None),
+ 'foo=1; bar=2')
+
+
+
+class CookieAgentTests(unittest.TestCase, CookieTestsMixin,
+ FakeReactorAndConnectMixin):
+ """
+ Tests for L{twisted.web.client.CookieAgent}.
+ """
+ def setUp(self):
+ self.reactor = self.Reactor()
+
+
+ def test_emptyCookieJarRequest(self):
+ """
+ L{CookieAgent.request} does not insert any C{'Cookie'} header into the
+ L{Request} object if there is no cookie in the cookie jar for the URI
+ being requested. Cookies are extracted from the response and stored in
+ the cookie jar.
+ """
+ cookieJar = cookielib.CookieJar()
+ self.assertEqual(list(cookieJar), [])
+
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ cookieAgent = client.CookieAgent(agent, cookieJar)
+ d = cookieAgent.request(
+ 'GET', 'http://example.com:1234/foo?bar')
+
+ def _checkCookie(ignored):
+ cookies = list(cookieJar)
+ self.assertEqual(len(cookies), 1)
+ self.assertEqual(cookies[0].name, 'foo')
+ self.assertEqual(cookies[0].value, '1')
+
+ d.addCallback(_checkCookie)
+
+ req, res = self.protocol.requests.pop()
+ self.assertIdentical(req.headers.getRawHeaders('cookie'), None)
+
+ resp = client.Response(
+ ('HTTP', 1, 1),
+ 200,
+ 'OK',
+ client.Headers({'Set-Cookie': ['foo=1',]}),
+ None)
+ res.callback(resp)
+
+ return d
+
+
+ def test_requestWithCookie(self):
+ """
+ L{CookieAgent.request} inserts a C{'Cookie'} header into the L{Request}
+ object when there is a cookie matching the request URI in the cookie
+ jar.
+ """
+ uri = 'http://example.com:1234/foo?bar'
+ cookie = 'foo=1'
+
+ cookieJar = cookielib.CookieJar()
+ self.addCookies(cookieJar, uri, [cookie])
+ self.assertEqual(len(list(cookieJar)), 1)
+
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ cookieAgent = client.CookieAgent(agent, cookieJar)
+ cookieAgent.request('GET', uri)
+
+ req, res = self.protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('cookie'), [cookie])
+
+
+ def test_secureCookie(self):
+ """
+ L{CookieAgent} is able to handle secure cookies, ie cookies which
+ should only be handled over https.
+ """
+ uri = 'https://example.com:1234/foo?bar'
+ cookie = 'foo=1;secure'
+
+ cookieJar = cookielib.CookieJar()
+ self.addCookies(cookieJar, uri, [cookie])
+ self.assertEqual(len(list(cookieJar)), 1)
+
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ cookieAgent = client.CookieAgent(agent, cookieJar)
+ cookieAgent.request('GET', uri)
+
+ req, res = self.protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('cookie'), ['foo=1'])
+
+
+ def test_secureCookieOnInsecureConnection(self):
+ """
+ If a cookie is setup as secure, it won't be sent with the request if
+ it's not over HTTPS.
+ """
+ uri = 'http://example.com/foo?bar'
+ cookie = 'foo=1;secure'
+
+ cookieJar = cookielib.CookieJar()
+ self.addCookies(cookieJar, uri, [cookie])
+ self.assertEqual(len(list(cookieJar)), 1)
+
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ cookieAgent = client.CookieAgent(agent, cookieJar)
+ cookieAgent.request('GET', uri)
+
+ req, res = self.protocol.requests.pop()
+ self.assertIdentical(None, req.headers.getRawHeaders('cookie'))
+
+
+ def test_portCookie(self):
+ """
+ L{CookieAgent} supports cookies which enforces the port number they
+ need to be transferred upon.
+ """
+ uri = 'https://example.com:1234/foo?bar'
+ cookie = 'foo=1;port=1234'
+
+ cookieJar = cookielib.CookieJar()
+ self.addCookies(cookieJar, uri, [cookie])
+ self.assertEqual(len(list(cookieJar)), 1)
+
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ cookieAgent = client.CookieAgent(agent, cookieJar)
+ cookieAgent.request('GET', uri)
+
+ req, res = self.protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('cookie'), ['foo=1'])
+
+
+ def test_portCookieOnWrongPort(self):
+ """
+ When creating a cookie with a port directive, it won't be added to the
+ L{cookie.CookieJar} if the URI is on a different port.
+ """
+ uri = 'https://example.com:4567/foo?bar'
+ cookie = 'foo=1;port=1234'
+
+ cookieJar = cookielib.CookieJar()
+ self.addCookies(cookieJar, uri, [cookie])
+ self.assertEqual(len(list(cookieJar)), 0)
+
+
+
+class Decoder1(proxyForInterface(IResponse)):
+ """
+ A test decoder to be used by L{client.ContentDecoderAgent} tests.
+ """
+
+
+
+class Decoder2(Decoder1):
+ """
+ A test decoder to be used by L{client.ContentDecoderAgent} tests.
+ """
+
+
+
+class ContentDecoderAgentTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ Tests for L{client.ContentDecoderAgent}.
+ """
+
+ def setUp(self):
+ """
+ Create an L{Agent} wrapped around a fake reactor.
+ """
+ self.reactor = self.Reactor()
+ self.agent = self.buildAgentForWrapperTest(self.reactor)
+
+
+ def test_acceptHeaders(self):
+ """
+ L{client.ContentDecoderAgent} sets the I{Accept-Encoding} header to the
+ names of the available decoder objects.
+ """
+ agent = client.ContentDecoderAgent(
+ self.agent, [('decoder1', Decoder1), ('decoder2', Decoder2)])
+
+ agent.request('GET', 'http://example.com/foo')
+
+ protocol = self.protocol
+
+ self.assertEqual(len(protocol.requests), 1)
+ req, res = protocol.requests.pop()
+ self.assertEqual(req.headers.getRawHeaders('accept-encoding'),
+ ['decoder1,decoder2'])
+
+
+ def test_existingHeaders(self):
+ """
+ If there are existing I{Accept-Encoding} fields,
+ L{client.ContentDecoderAgent} creates a new field for the decoders it
+ knows about.
+ """
+ headers = http_headers.Headers({'foo': ['bar'],
+ 'accept-encoding': ['fizz']})
+ agent = client.ContentDecoderAgent(
+ self.agent, [('decoder1', Decoder1), ('decoder2', Decoder2)])
+ agent.request('GET', 'http://example.com/foo', headers=headers)
+
+ protocol = self.protocol
+
+ self.assertEqual(len(protocol.requests), 1)
+ req, res = protocol.requests.pop()
+ self.assertEqual(
+ list(req.headers.getAllRawHeaders()),
+ [('Host', ['example.com']),
+ ('Foo', ['bar']),
+ ('Accept-Encoding', ['fizz', 'decoder1,decoder2'])])
+
+
+ def test_plainEncodingResponse(self):
+ """
+ If the response is not encoded despited the request I{Accept-Encoding}
+ headers, L{client.ContentDecoderAgent} simply forwards the response.
+ """
+ agent = client.ContentDecoderAgent(
+ self.agent, [('decoder1', Decoder1), ('decoder2', Decoder2)])
+ deferred = agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ response = Response(('HTTP', 1, 1), 200, 'OK', http_headers.Headers(),
+ None)
+ res.callback(response)
+
+ return deferred.addCallback(self.assertIdentical, response)
+
+
+ def test_unsupportedEncoding(self):
+ """
+ If an encoding unknown to the L{client.ContentDecoderAgent} is found,
+ the response is unchanged.
+ """
+ agent = client.ContentDecoderAgent(
+ self.agent, [('decoder1', Decoder1), ('decoder2', Decoder2)])
+ deferred = agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'foo': ['bar'],
+ 'content-encoding': ['fizz']})
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, None)
+ res.callback(response)
+
+ return deferred.addCallback(self.assertIdentical, response)
+
+
+ def test_unknownEncoding(self):
+ """
+ When L{client.ContentDecoderAgent} encounters a decoder it doesn't know
+ about, it stops decoding even if another encoding is known afterwards.
+ """
+ agent = client.ContentDecoderAgent(
+ self.agent, [('decoder1', Decoder1), ('decoder2', Decoder2)])
+ deferred = agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'foo': ['bar'],
+ 'content-encoding':
+ ['decoder1,fizz,decoder2']})
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, None)
+ res.callback(response)
+
+ def check(result):
+ self.assertNotIdentical(response, result)
+ self.assertIsInstance(result, Decoder2)
+ self.assertEqual(['decoder1,fizz'],
+ result.headers.getRawHeaders('content-encoding'))
+
+ return deferred.addCallback(check)
+
+
+
+class SimpleAgentProtocol(Protocol):
+ """
+ A L{Protocol} to be used with an L{client.Agent} to receive data.
+
+ @ivar finished: L{Deferred} firing when C{connectionLost} is called.
+
+ @ivar made: L{Deferred} firing when C{connectionMade} is called.
+
+ @ivar received: C{list} of received data.
+ """
+
+ def __init__(self):
+ self.made = Deferred()
+ self.finished = Deferred()
+ self.received = []
+
+
+ def connectionMade(self):
+ self.made.callback(None)
+
+
+ def connectionLost(self, reason):
+ self.finished.callback(None)
+
+
+ def dataReceived(self, data):
+ self.received.append(data)
+
+
+
+class ContentDecoderAgentWithGzipTests(unittest.TestCase,
+ FakeReactorAndConnectMixin):
+
+ def setUp(self):
+ """
+ Create an L{Agent} wrapped around a fake reactor.
+ """
+ self.reactor = self.Reactor()
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ self.agent = client.ContentDecoderAgent(
+ agent, [("gzip", client.GzipDecoder)])
+
+
+ def test_gzipEncodingResponse(self):
+ """
+ If the response has a C{gzip} I{Content-Encoding} header,
+ L{GzipDecoder} wraps the response to return uncompressed data to the
+ user.
+ """
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'foo': ['bar'],
+ 'content-encoding': ['gzip']})
+ transport = StringTransport()
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, transport)
+ response.length = 12
+ res.callback(response)
+
+ compressor = zlib.compressobj(2, zlib.DEFLATED, 16 + zlib.MAX_WBITS)
+ data = (compressor.compress('x' * 6) + compressor.compress('y' * 4) +
+ compressor.flush())
+
+ def checkResponse(result):
+ self.assertNotIdentical(result, response)
+ self.assertEqual(result.version, ('HTTP', 1, 1))
+ self.assertEqual(result.code, 200)
+ self.assertEqual(result.phrase, 'OK')
+ self.assertEqual(list(result.headers.getAllRawHeaders()),
+ [('Foo', ['bar'])])
+ self.assertEqual(result.length, UNKNOWN_LENGTH)
+ self.assertRaises(AttributeError, getattr, result, 'unknown')
+
+ response._bodyDataReceived(data[:5])
+ response._bodyDataReceived(data[5:])
+ response._bodyDataFinished()
+
+ protocol = SimpleAgentProtocol()
+ result.deliverBody(protocol)
+
+ self.assertEqual(protocol.received, ['x' * 6 + 'y' * 4])
+ return defer.gatherResults([protocol.made, protocol.finished])
+
+ deferred.addCallback(checkResponse)
+
+ return deferred
+
+
+ def test_brokenContent(self):
+ """
+ If the data received by the L{GzipDecoder} isn't valid gzip-compressed
+ data, the call to C{deliverBody} fails with a C{zlib.error}.
+ """
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'foo': ['bar'],
+ 'content-encoding': ['gzip']})
+ transport = StringTransport()
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, transport)
+ response.length = 12
+ res.callback(response)
+
+ data = "not gzipped content"
+
+ def checkResponse(result):
+ response._bodyDataReceived(data)
+
+ result.deliverBody(Protocol())
+
+ deferred.addCallback(checkResponse)
+ self.assertFailure(deferred, client.ResponseFailed)
+
+ def checkFailure(error):
+ error.reasons[0].trap(zlib.error)
+ self.assertIsInstance(error.response, Response)
+
+ return deferred.addCallback(checkFailure)
+
+
+ def test_flushData(self):
+ """
+ When the connection with the server is lost, the gzip protocol calls
+ C{flush} on the zlib decompressor object to get uncompressed data which
+ may have been buffered.
+ """
+ class decompressobj(object):
+
+ def __init__(self, wbits):
+ pass
+
+ def decompress(self, data):
+ return 'x'
+
+ def flush(self):
+ return 'y'
+
+
+ oldDecompressObj = zlib.decompressobj
+ zlib.decompressobj = decompressobj
+ self.addCleanup(setattr, zlib, 'decompressobj', oldDecompressObj)
+
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'content-encoding': ['gzip']})
+ transport = StringTransport()
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, transport)
+ res.callback(response)
+
+ def checkResponse(result):
+ response._bodyDataReceived('data')
+ response._bodyDataFinished()
+
+ protocol = SimpleAgentProtocol()
+ result.deliverBody(protocol)
+
+ self.assertEqual(protocol.received, ['x', 'y'])
+ return defer.gatherResults([protocol.made, protocol.finished])
+
+ deferred.addCallback(checkResponse)
+
+ return deferred
+
+
+ def test_flushError(self):
+ """
+ If the C{flush} call in C{connectionLost} fails, the C{zlib.error}
+ exception is caught and turned into a L{ResponseFailed}.
+ """
+ class decompressobj(object):
+
+ def __init__(self, wbits):
+ pass
+
+ def decompress(self, data):
+ return 'x'
+
+ def flush(self):
+ raise zlib.error()
+
+
+ oldDecompressObj = zlib.decompressobj
+ zlib.decompressobj = decompressobj
+ self.addCleanup(setattr, zlib, 'decompressobj', oldDecompressObj)
+
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers({'content-encoding': ['gzip']})
+ transport = StringTransport()
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, transport)
+ res.callback(response)
+
+ def checkResponse(result):
+ response._bodyDataReceived('data')
+ response._bodyDataFinished()
+
+ protocol = SimpleAgentProtocol()
+ result.deliverBody(protocol)
+
+ self.assertEqual(protocol.received, ['x', 'y'])
+ return defer.gatherResults([protocol.made, protocol.finished])
+
+ deferred.addCallback(checkResponse)
+
+ self.assertFailure(deferred, client.ResponseFailed)
+
+ def checkFailure(error):
+ error.reasons[1].trap(zlib.error)
+ self.assertIsInstance(error.response, Response)
+
+ return deferred.addCallback(checkFailure)
+
+
+
+class ProxyAgentTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ Tests for L{client.ProxyAgent}.
+ """
+
+ def setUp(self):
+ self.reactor = self.Reactor()
+ self.agent = client.ProxyAgent(
+ TCP4ClientEndpoint(self.reactor, "bar", 5678), self.reactor)
+ oldEndpoint = self.agent._proxyEndpoint
+ self.agent._proxyEndpoint = self.StubEndpoint(oldEndpoint, self)
+
+
+ def test_proxyRequest(self):
+ """
+ L{client.ProxyAgent} issues an HTTP request against the proxy, with the
+ full URI as path, when C{request} is called.
+ """
+ headers = http_headers.Headers({'foo': ['bar']})
+ # Just going to check the body for identity, so it doesn't need to be
+ # real.
+ body = object()
+ self.agent.request(
+ 'GET', 'http://example.com:1234/foo?bar', headers, body)
+
+ host, port, factory = self.reactor.tcpClients.pop()[:3]
+ self.assertEqual(host, "bar")
+ self.assertEqual(port, 5678)
+
+ self.assertIsInstance(factory._wrappedFactory,
+ client._HTTP11ClientFactory)
+
+ protocol = self.protocol
+
+ # The request should be issued.
+ self.assertEqual(len(protocol.requests), 1)
+ req, res = protocol.requests.pop()
+ self.assertIsInstance(req, Request)
+ self.assertEqual(req.method, 'GET')
+ self.assertEqual(req.uri, 'http://example.com:1234/foo?bar')
+ self.assertEqual(
+ req.headers,
+ http_headers.Headers({'foo': ['bar'],
+ 'host': ['example.com:1234']}))
+ self.assertIdentical(req.bodyProducer, body)
+
+
+ def test_nonPersistent(self):
+ """
+ C{ProxyAgent} connections are not persistent by default.
+ """
+ self.assertEqual(self.agent._pool.persistent, False)
+
+
+ def test_connectUsesConnectionPool(self):
+ """
+ When a connection is made by the C{ProxyAgent}, it uses its pool's
+ C{getConnection} method to do so, with the endpoint it was constructed
+ with and a key of C{("http-proxy", endpoint)}.
+ """
+ endpoint = DummyEndpoint()
+ class DummyPool(object):
+ connected = False
+ persistent = False
+ def getConnection(this, key, ep):
+ this.connected = True
+ self.assertIdentical(ep, endpoint)
+ # The key is *not* tied to the final destination, but only to
+ # the address of the proxy, since that's where *we* are
+ # connecting:
+ self.assertEqual(key, ("http-proxy", endpoint))
+ return defer.succeed(StubHTTPProtocol())
+
+ pool = DummyPool()
+ agent = client.ProxyAgent(endpoint, self.reactor, pool=pool)
+ self.assertIdentical(pool, agent._pool)
+
+ agent.request('GET', 'http://foo/')
+ self.assertEqual(agent._pool.connected, True)
+
+
+
+class RedirectAgentTests(unittest.TestCase, FakeReactorAndConnectMixin):
+ """
+ Tests for L{client.RedirectAgent}.
+ """
+
+ def setUp(self):
+ self.reactor = self.Reactor()
+ self.agent = client.RedirectAgent(
+ self.buildAgentForWrapperTest(self.reactor))
+
+
+ def test_noRedirect(self):
+ """
+ L{client.RedirectAgent} behaves like L{client.Agent} if the response
+ doesn't contain a redirect.
+ """
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers()
+ response = Response(('HTTP', 1, 1), 200, 'OK', headers, None)
+ res.callback(response)
+
+ self.assertEqual(0, len(self.protocol.requests))
+
+ def checkResponse(result):
+ self.assertIdentical(result, response)
+
+ return deferred.addCallback(checkResponse)
+
+
+ def _testRedirectDefault(self, code):
+ """
+ When getting a redirect, L{RedirectAgent} follows the URL specified in
+ the L{Location} header field and make a new request.
+ """
+ self.agent.request('GET', 'http://example.com/foo')
+
+ host, port = self.reactor.tcpClients.pop()[:2]
+ self.assertEqual("example.com", host)
+ self.assertEqual(80, port)
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers(
+ {'location': ['https://example.com/bar']})
+ response = Response(('HTTP', 1, 1), code, 'OK', headers, None)
+ res.callback(response)
+
+ req2, res2 = self.protocol.requests.pop()
+ self.assertEqual('GET', req2.method)
+ self.assertEqual('/bar', req2.uri)
+
+ host, port = self.reactor.sslClients.pop()[:2]
+ self.assertEqual("example.com", host)
+ self.assertEqual(443, port)
+
+
+ def test_redirect301(self):
+ """
+ L{RedirectAgent} follows redirects on status code 301.
+ """
+ self._testRedirectDefault(301)
+
+
+ def test_redirect302(self):
+ """
+ L{RedirectAgent} follows redirects on status code 302.
+ """
+ self._testRedirectDefault(302)
+
+
+ def test_redirect307(self):
+ """
+ L{RedirectAgent} follows redirects on status code 307.
+ """
+ self._testRedirectDefault(307)
+
+
+ def test_redirect303(self):
+ """
+ L{RedirectAgent} changes the methods to C{GET} when getting a redirect
+ on a C{POST} request.
+ """
+ self.agent.request('POST', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers(
+ {'location': ['http://example.com/bar']})
+ response = Response(('HTTP', 1, 1), 303, 'OK', headers, None)
+ res.callback(response)
+
+ req2, res2 = self.protocol.requests.pop()
+ self.assertEqual('GET', req2.method)
+ self.assertEqual('/bar', req2.uri)
+
+
+ def test_noLocationField(self):
+ """
+ If no L{Location} header field is found when getting a redirect,
+ L{RedirectAgent} fails with a L{ResponseFailed} error wrapping a
+ L{error.RedirectWithNoLocation} exception.
+ """
+ deferred = self.agent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers()
+ response = Response(('HTTP', 1, 1), 301, 'OK', headers, None)
+ res.callback(response)
+
+ self.assertFailure(deferred, client.ResponseFailed)
+
+ def checkFailure(fail):
+ fail.reasons[0].trap(error.RedirectWithNoLocation)
+ self.assertEqual('http://example.com/foo',
+ fail.reasons[0].value.uri)
+ self.assertEqual(301, fail.response.code)
+
+ return deferred.addCallback(checkFailure)
+
+
+ def test_307OnPost(self):
+ """
+ When getting a 307 redirect on a C{POST} request, L{RedirectAgent} fais
+ with a L{ResponseFailed} error wrapping a L{error.PageRedirect}
+ exception.
+ """
+ deferred = self.agent.request('POST', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers()
+ response = Response(('HTTP', 1, 1), 307, 'OK', headers, None)
+ res.callback(response)
+
+ self.assertFailure(deferred, client.ResponseFailed)
+
+ def checkFailure(fail):
+ fail.reasons[0].trap(error.PageRedirect)
+ self.assertEqual('http://example.com/foo',
+ fail.reasons[0].value.location)
+ self.assertEqual(307, fail.response.code)
+
+ return deferred.addCallback(checkFailure)
+
+
+ def test_redirectLimit(self):
+ """
+ If the limit of redirects specified to L{RedirectAgent} is reached, the
+ deferred fires with L{ResponseFailed} error wrapping a
+ L{InfiniteRedirection} exception.
+ """
+ agent = self.buildAgentForWrapperTest(self.reactor)
+ redirectAgent = client.RedirectAgent(agent, 1)
+
+ deferred = redirectAgent.request('GET', 'http://example.com/foo')
+
+ req, res = self.protocol.requests.pop()
+
+ headers = http_headers.Headers(
+ {'location': ['http://example.com/bar']})
+ response = Response(('HTTP', 1, 1), 302, 'OK', headers, None)
+ res.callback(response)
+
+ req2, res2 = self.protocol.requests.pop()
+
+ response2 = Response(('HTTP', 1, 1), 302, 'OK', headers, None)
+ res2.callback(response2)
+
+ self.assertFailure(deferred, client.ResponseFailed)
+
+ def checkFailure(fail):
+ fail.reasons[0].trap(error.InfiniteRedirection)
+ self.assertEqual('http://example.com/foo',
+ fail.reasons[0].value.location)
+ self.assertEqual(302, fail.response.code)
+
+ return deferred.addCallback(checkFailure)
+
+
+
+if ssl is None or not hasattr(ssl, 'DefaultOpenSSLContextFactory'):
+ for case in [WebClientSSLTestCase, WebClientRedirectBetweenSSLandPlainText]:
+ case.skip = "OpenSSL not present"
+
+if not interfaces.IReactorSSL(reactor, None):
+ for case in [WebClientSSLTestCase, WebClientRedirectBetweenSSLandPlainText]:
+ case.skip = "Reactor doesn't support SSL"
diff --git a/twisted/web/test/test_wsgi.py b/twisted/web/test/test_wsgi.py
new file mode 100644
index 0000000..ddcdf11
--- /dev/null
+++ b/twisted/web/test/test_wsgi.py
@@ -0,0 +1,1572 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.web.wsgi}.
+"""
+
+__metaclass__ = type
+
+from sys import exc_info
+from urllib import quote
+from thread import get_ident
+import StringIO, cStringIO, tempfile
+
+from zope.interface.verify import verifyObject
+
+from twisted.python.compat import set
+from twisted.python.log import addObserver, removeObserver, err
+from twisted.python.failure import Failure
+from twisted.python.threadpool import ThreadPool
+from twisted.internet.defer import Deferred, gatherResults
+from twisted.internet import reactor
+from twisted.internet.error import ConnectionLost
+from twisted.trial.unittest import TestCase
+from twisted.web import http
+from twisted.web.resource import IResource, Resource
+from twisted.web.server import Request, Site, version
+from twisted.web.wsgi import WSGIResource
+from twisted.web.test.test_web import DummyChannel
+
+
+class SynchronousThreadPool:
+ """
+ A single-threaded implementation of part of the L{ThreadPool} interface.
+ This implementation calls functions synchronously rather than running
+ them in a thread pool. It is used to make the tests which are not
+ directly for thread-related behavior deterministic.
+ """
+ def callInThread(self, f, *a, **kw):
+ """
+ Call C{f(*a, **kw)} in this thread rather than scheduling it to be
+ called in a thread.
+ """
+ try:
+ f(*a, **kw)
+ except:
+ # callInThread doesn't let exceptions propagate to the caller.
+ # None is always returned and any exception raised gets logged
+ # later on.
+ err(None, "Callable passed to SynchronousThreadPool.callInThread failed")
+
+
+
+class SynchronousReactorThreads:
+ """
+ A single-threaded implementation of part of the L{IReactorThreads}
+ interface. This implementation assumes that it will only be invoked
+ from the reactor thread, so it calls functions synchronously rather than
+ trying to schedule them to run in the reactor thread. It is used in
+ conjunction with L{SynchronousThreadPool} to make the tests which are
+ not directly for thread-related behavior deterministic.
+ """
+ def callFromThread(self, f, *a, **kw):
+ """
+ Call C{f(*a, **kw)} in this thread which should also be the reactor
+ thread.
+ """
+ f(*a, **kw)
+
+
+
+class WSGIResourceTests(TestCase):
+ def setUp(self):
+ """
+ Create a L{WSGIResource} with synchronous threading objects and a no-op
+ application object. This is useful for testing certain things about
+ the resource implementation which are unrelated to WSGI.
+ """
+ self.resource = WSGIResource(
+ SynchronousReactorThreads(), SynchronousThreadPool(),
+ lambda environ, startResponse: None)
+
+
+ def test_interfaces(self):
+ """
+ L{WSGIResource} implements L{IResource} and stops resource traversal.
+ """
+ verifyObject(IResource, self.resource)
+ self.assertTrue(self.resource.isLeaf)
+
+
+ def test_unsupported(self):
+ """
+ A L{WSGIResource} cannot have L{IResource} children. Its
+ C{getChildWithDefault} and C{putChild} methods raise L{RuntimeError}.
+ """
+ self.assertRaises(
+ RuntimeError,
+ self.resource.getChildWithDefault,
+ "foo", Request(DummyChannel(), False))
+ self.assertRaises(
+ RuntimeError,
+ self.resource.putChild,
+ "foo", Resource())
+
+
+class WSGITestsMixin:
+ """
+ @ivar channelFactory: A no-argument callable which will be invoked to
+ create a new HTTP channel to associate with request objects.
+ """
+ channelFactory = DummyChannel
+
+ def setUp(self):
+ self.threadpool = SynchronousThreadPool()
+ self.reactor = SynchronousReactorThreads()
+
+
+ def lowLevelRender(
+ self, requestFactory, applicationFactory, channelFactory, method,
+ version, resourceSegments, requestSegments, query=None, headers=[],
+ body=None, safe=''):
+ """
+ @param method: A C{str} giving the request method to use.
+
+ @param version: A C{str} like C{'1.1'} giving the request version.
+
+ @param resourceSegments: A C{list} of unencoded path segments which
+ specifies the location in the resource hierarchy at which the
+ L{WSGIResource} will be placed, eg C{['']} for I{/}, C{['foo',
+ 'bar', '']} for I{/foo/bar/}, etc.
+
+ @param requestSegments: A C{list} of unencoded path segments giving the
+ request URI.
+
+ @param query: A C{list} of two-tuples of C{str} giving unencoded query
+ argument keys and values.
+
+ @param headers: A C{list} of two-tuples of C{str} giving request header
+ names and corresponding values.
+
+ @param safe: A C{str} giving the bytes which are to be considered
+ I{safe} for inclusion in the request URI and not quoted.
+
+ @return: A L{Deferred} which will be called back with a two-tuple of
+ the arguments passed which would be passed to the WSGI application
+ object for this configuration and request (ie, the environment and
+ start_response callable).
+ """
+ root = WSGIResource(
+ self.reactor, self.threadpool, applicationFactory())
+ resourceSegments.reverse()
+ for seg in resourceSegments:
+ tmp = Resource()
+ tmp.putChild(seg, root)
+ root = tmp
+
+ channel = channelFactory()
+ channel.site = Site(root)
+ request = requestFactory(channel, False)
+ for k, v in headers:
+ request.requestHeaders.addRawHeader(k, v)
+ request.gotLength(0)
+ if body:
+ request.content.write(body)
+ request.content.seek(0)
+ uri = '/' + '/'.join([quote(seg, safe) for seg in requestSegments])
+ if query is not None:
+ uri += '?' + '&'.join(['='.join([quote(k, safe), quote(v, safe)])
+ for (k, v) in query])
+ request.requestReceived(method, uri, 'HTTP/' + version)
+ return request
+
+
+ def render(self, *a, **kw):
+ result = Deferred()
+ def applicationFactory():
+ def application(*args):
+ environ, startResponse = args
+ result.callback(args)
+ startResponse('200 OK', [])
+ return iter(())
+ return application
+ self.lowLevelRender(
+ Request, applicationFactory, self.channelFactory, *a, **kw)
+ return result
+
+
+ def requestFactoryFactory(self, requestClass=Request):
+ d = Deferred()
+ def requestFactory(*a, **kw):
+ request = requestClass(*a, **kw)
+ # If notifyFinish is called after lowLevelRender returns, it won't
+ # do the right thing, because the request will have already
+ # finished. One might argue that this is a bug in
+ # Request.notifyFinish.
+ request.notifyFinish().chainDeferred(d)
+ return request
+ return d, requestFactory
+
+
+ def getContentFromResponse(self, response):
+ return response.split('\r\n\r\n', 1)[1]
+
+
+
+class EnvironTests(WSGITestsMixin, TestCase):
+ """
+ Tests for the values in the C{environ} C{dict} passed to the application
+ object by L{twisted.web.wsgi.WSGIResource}.
+ """
+ def environKeyEqual(self, key, value):
+ def assertEnvironKeyEqual((environ, startResponse)):
+ self.assertEqual(environ[key], value)
+ return assertEnvironKeyEqual
+
+
+ def test_environIsDict(self):
+ """
+ L{WSGIResource} calls the application object with an C{environ}
+ parameter which is exactly of type C{dict}.
+ """
+ d = self.render('GET', '1.1', [], [''])
+ def cbRendered((environ, startResponse)):
+ self.assertIdentical(type(environ), dict)
+ d.addCallback(cbRendered)
+ return d
+
+
+ def test_requestMethod(self):
+ """
+ The C{'REQUEST_METHOD'} key of the C{environ} C{dict} passed to the
+ application contains the HTTP method in the request (RFC 3875, section
+ 4.1.12).
+ """
+ get = self.render('GET', '1.1', [], [''])
+ get.addCallback(self.environKeyEqual('REQUEST_METHOD', 'GET'))
+
+ # Also make sure a different request method shows up as a different
+ # value in the environ dict.
+ post = self.render('POST', '1.1', [], [''])
+ post.addCallback(self.environKeyEqual('REQUEST_METHOD', 'POST'))
+
+ return gatherResults([get, post])
+
+
+ def test_scriptName(self):
+ """
+ The C{'SCRIPT_NAME'} key of the C{environ} C{dict} passed to the
+ application contains the I{abs_path} (RFC 2396, section 3) to this
+ resource (RFC 3875, section 4.1.13).
+ """
+ root = self.render('GET', '1.1', [], [''])
+ root.addCallback(self.environKeyEqual('SCRIPT_NAME', ''))
+
+ emptyChild = self.render('GET', '1.1', [''], [''])
+ emptyChild.addCallback(self.environKeyEqual('SCRIPT_NAME', '/'))
+
+ leaf = self.render('GET', '1.1', ['foo'], ['foo'])
+ leaf.addCallback(self.environKeyEqual('SCRIPT_NAME', '/foo'))
+
+ container = self.render('GET', '1.1', ['foo', ''], ['foo', ''])
+ container.addCallback(self.environKeyEqual('SCRIPT_NAME', '/foo/'))
+
+ internal = self.render('GET', '1.1', ['foo'], ['foo', 'bar'])
+ internal.addCallback(self.environKeyEqual('SCRIPT_NAME', '/foo'))
+
+ unencoded = self.render(
+ 'GET', '1.1', ['foo', '/', 'bar\xff'], ['foo', '/', 'bar\xff'])
+ # The RFC says "(not URL-encoded)", even though that makes
+ # interpretation of SCRIPT_NAME ambiguous.
+ unencoded.addCallback(
+ self.environKeyEqual('SCRIPT_NAME', '/foo///bar\xff'))
+
+ return gatherResults([
+ root, emptyChild, leaf, container, internal, unencoded])
+
+
+ def test_pathInfo(self):
+ """
+ The C{'PATH_INFO'} key of the C{environ} C{dict} passed to the
+ application contains the suffix of the request URI path which is not
+ included in the value for the C{'SCRIPT_NAME'} key (RFC 3875, section
+ 4.1.5).
+ """
+ assertKeyEmpty = self.environKeyEqual('PATH_INFO', '')
+
+ root = self.render('GET', '1.1', [], [''])
+ root.addCallback(self.environKeyEqual('PATH_INFO', '/'))
+
+ emptyChild = self.render('GET', '1.1', [''], [''])
+ emptyChild.addCallback(assertKeyEmpty)
+
+ leaf = self.render('GET', '1.1', ['foo'], ['foo'])
+ leaf.addCallback(assertKeyEmpty)
+
+ container = self.render('GET', '1.1', ['foo', ''], ['foo', ''])
+ container.addCallback(assertKeyEmpty)
+
+ internalLeaf = self.render('GET', '1.1', ['foo'], ['foo', 'bar'])
+ internalLeaf.addCallback(self.environKeyEqual('PATH_INFO', '/bar'))
+
+ internalContainer = self.render('GET', '1.1', ['foo'], ['foo', ''])
+ internalContainer.addCallback(self.environKeyEqual('PATH_INFO', '/'))
+
+ unencoded = self.render('GET', '1.1', [], ['foo', '/', 'bar\xff'])
+ unencoded.addCallback(
+ self.environKeyEqual('PATH_INFO', '/foo///bar\xff'))
+
+ return gatherResults([
+ root, leaf, container, internalLeaf,
+ internalContainer, unencoded])
+
+
+ def test_queryString(self):
+ """
+ The C{'QUERY_STRING'} key of the C{environ} C{dict} passed to the
+ application contains the portion of the request URI after the first
+ I{?} (RFC 3875, section 4.1.7).
+ """
+ missing = self.render('GET', '1.1', [], [''], None)
+ missing.addCallback(self.environKeyEqual('QUERY_STRING', ''))
+
+ empty = self.render('GET', '1.1', [], [''], [])
+ empty.addCallback(self.environKeyEqual('QUERY_STRING', ''))
+
+ present = self.render('GET', '1.1', [], [''], [('foo', 'bar')])
+ present.addCallback(self.environKeyEqual('QUERY_STRING', 'foo=bar'))
+
+ unencoded = self.render('GET', '1.1', [], [''], [('/', '/')])
+ unencoded.addCallback(self.environKeyEqual('QUERY_STRING', '%2F=%2F'))
+
+ # "?" is reserved in the <searchpart> portion of a URL. However, it
+ # seems to be a common mistake of clients to forget to quote it. So,
+ # make sure we handle that invalid case.
+ doubleQuestion = self.render(
+ 'GET', '1.1', [], [''], [('foo', '?bar')], safe='?')
+ doubleQuestion.addCallback(
+ self.environKeyEqual('QUERY_STRING', 'foo=?bar'))
+
+ return gatherResults([
+ missing, empty, present, unencoded, doubleQuestion])
+
+
+ def test_contentType(self):
+ """
+ The C{'CONTENT_TYPE'} key of the C{environ} C{dict} passed to the
+ application contains the value of the I{Content-Type} request header
+ (RFC 3875, section 4.1.3).
+ """
+ missing = self.render('GET', '1.1', [], [''])
+ missing.addCallback(self.environKeyEqual('CONTENT_TYPE', ''))
+
+ present = self.render(
+ 'GET', '1.1', [], [''], None, [('content-type', 'x-foo/bar')])
+ present.addCallback(self.environKeyEqual('CONTENT_TYPE', 'x-foo/bar'))
+
+ return gatherResults([missing, present])
+
+
+ def test_contentLength(self):
+ """
+ The C{'CONTENT_LENGTH'} key of the C{environ} C{dict} passed to the
+ application contains the value of the I{Content-Length} request header
+ (RFC 3875, section 4.1.2).
+ """
+ missing = self.render('GET', '1.1', [], [''])
+ missing.addCallback(self.environKeyEqual('CONTENT_LENGTH', ''))
+
+ present = self.render(
+ 'GET', '1.1', [], [''], None, [('content-length', '1234')])
+ present.addCallback(self.environKeyEqual('CONTENT_LENGTH', '1234'))
+
+ return gatherResults([missing, present])
+
+
+ def test_serverName(self):
+ """
+ The C{'SERVER_NAME'} key of the C{environ} C{dict} passed to the
+ application contains the best determination of the server hostname
+ possible, using either the value of the I{Host} header in the request
+ or the address the server is listening on if that header is not
+ present (RFC 3875, section 4.1.14).
+ """
+ missing = self.render('GET', '1.1', [], [''])
+ # 10.0.0.1 value comes from a bit far away -
+ # twisted.test.test_web.DummyChannel.transport.getHost().host
+ missing.addCallback(self.environKeyEqual('SERVER_NAME', '10.0.0.1'))
+
+ present = self.render(
+ 'GET', '1.1', [], [''], None, [('host', 'example.org')])
+ present.addCallback(self.environKeyEqual('SERVER_NAME', 'example.org'))
+
+ return gatherResults([missing, present])
+
+
+ def test_serverPort(self):
+ """
+ The C{'SERVER_PORT'} key of the C{environ} C{dict} passed to the
+ application contains the port number of the server which received the
+ request (RFC 3875, section 4.1.15).
+ """
+ portNumber = 12354
+ def makeChannel():
+ channel = DummyChannel()
+ channel.transport = DummyChannel.TCP()
+ channel.transport.port = portNumber
+ return channel
+ self.channelFactory = makeChannel
+
+ d = self.render('GET', '1.1', [], [''])
+ d.addCallback(self.environKeyEqual('SERVER_PORT', str(portNumber)))
+ return d
+
+
+ def test_serverProtocol(self):
+ """
+ The C{'SERVER_PROTOCOL'} key of the C{environ} C{dict} passed to the
+ application contains the HTTP version number received in the request
+ (RFC 3875, section 4.1.16).
+ """
+ old = self.render('GET', '1.0', [], [''])
+ old.addCallback(self.environKeyEqual('SERVER_PROTOCOL', 'HTTP/1.0'))
+
+ new = self.render('GET', '1.1', [], [''])
+ new.addCallback(self.environKeyEqual('SERVER_PROTOCOL', 'HTTP/1.1'))
+
+ return gatherResults([old, new])
+
+
+ def test_remoteAddr(self):
+ """
+ The C{'REMOTE_ADDR'} key of the C{environ} C{dict} passed to the
+ application contains the address of the client making the request.
+ """
+ d = self.render('GET', '1.1', [], [''])
+ d.addCallback(self.environKeyEqual('REMOTE_ADDR', '192.168.1.1'))
+
+ return d
+
+ def test_headers(self):
+ """
+ HTTP request headers are copied into the C{environ} C{dict} passed to
+ the application with a C{HTTP_} prefix added to their names.
+ """
+ singleValue = self.render(
+ 'GET', '1.1', [], [''], None, [('foo', 'bar'), ('baz', 'quux')])
+ def cbRendered((environ, startResponse)):
+ self.assertEqual(environ['HTTP_FOO'], 'bar')
+ self.assertEqual(environ['HTTP_BAZ'], 'quux')
+ singleValue.addCallback(cbRendered)
+
+ multiValue = self.render(
+ 'GET', '1.1', [], [''], None, [('foo', 'bar'), ('foo', 'baz')])
+ multiValue.addCallback(self.environKeyEqual('HTTP_FOO', 'bar,baz'))
+
+ withHyphen = self.render(
+ 'GET', '1.1', [], [''], None, [('foo-bar', 'baz')])
+ withHyphen.addCallback(self.environKeyEqual('HTTP_FOO_BAR', 'baz'))
+
+ multiLine = self.render(
+ 'GET', '1.1', [], [''], None, [('foo', 'bar\n\tbaz')])
+ multiLine.addCallback(self.environKeyEqual('HTTP_FOO', 'bar \tbaz'))
+
+ return gatherResults([singleValue, multiValue, withHyphen, multiLine])
+
+
+ def test_wsgiVersion(self):
+ """
+ The C{'wsgi.version'} key of the C{environ} C{dict} passed to the
+ application has the value C{(1, 0)} indicating that this is a WSGI 1.0
+ container.
+ """
+ versionDeferred = self.render('GET', '1.1', [], [''])
+ versionDeferred.addCallback(self.environKeyEqual('wsgi.version', (1, 0)))
+ return versionDeferred
+
+
+ def test_wsgiRunOnce(self):
+ """
+ The C{'wsgi.run_once'} key of the C{environ} C{dict} passed to the
+ application is set to C{False}.
+ """
+ once = self.render('GET', '1.1', [], [''])
+ once.addCallback(self.environKeyEqual('wsgi.run_once', False))
+ return once
+
+
+ def test_wsgiMultithread(self):
+ """
+ The C{'wsgi.multithread'} key of the C{environ} C{dict} passed to the
+ application is set to C{True}.
+ """
+ thread = self.render('GET', '1.1', [], [''])
+ thread.addCallback(self.environKeyEqual('wsgi.multithread', True))
+ return thread
+
+
+ def test_wsgiMultiprocess(self):
+ """
+ The C{'wsgi.multiprocess'} key of the C{environ} C{dict} passed to the
+ application is set to C{False}.
+ """
+ process = self.render('GET', '1.1', [], [''])
+ process.addCallback(self.environKeyEqual('wsgi.multiprocess', False))
+ return process
+
+
+ def test_wsgiURLScheme(self):
+ """
+ The C{'wsgi.url_scheme'} key of the C{environ} C{dict} passed to the
+ application has the request URL scheme.
+ """
+ # XXX Does this need to be different if the request is for an absolute
+ # URL?
+ def channelFactory():
+ channel = DummyChannel()
+ channel.transport = DummyChannel.SSL()
+ return channel
+
+ self.channelFactory = DummyChannel
+ httpDeferred = self.render('GET', '1.1', [], [''])
+ httpDeferred.addCallback(self.environKeyEqual('wsgi.url_scheme', 'http'))
+
+ self.channelFactory = channelFactory
+ httpsDeferred = self.render('GET', '1.1', [], [''])
+ httpsDeferred.addCallback(self.environKeyEqual('wsgi.url_scheme', 'https'))
+
+ return gatherResults([httpDeferred, httpsDeferred])
+
+
+ def test_wsgiErrors(self):
+ """
+ The C{'wsgi.errors'} key of the C{environ} C{dict} passed to the
+ application is a file-like object (as defined in the U{Input and Errors
+ Streams<http://www.python.org/dev/peps/pep-0333/#input-and-error-streams>}
+ section of PEP 333) which converts bytes written to it into events for
+ the logging system.
+ """
+ events = []
+ addObserver(events.append)
+ self.addCleanup(removeObserver, events.append)
+
+ errors = self.render('GET', '1.1', [], [''])
+ def cbErrors((environ, startApplication)):
+ errors = environ['wsgi.errors']
+ errors.write('some message\n')
+ errors.writelines(['another\nmessage\n'])
+ errors.flush()
+ self.assertEqual(events[0]['message'], ('some message\n',))
+ self.assertEqual(events[0]['system'], 'wsgi')
+ self.assertTrue(events[0]['isError'])
+ self.assertEqual(events[1]['message'], ('another\nmessage\n',))
+ self.assertEqual(events[1]['system'], 'wsgi')
+ self.assertTrue(events[1]['isError'])
+ self.assertEqual(len(events), 2)
+ errors.addCallback(cbErrors)
+ return errors
+
+
+class InputStreamTestMixin(WSGITestsMixin):
+ """
+ A mixin for L{TestCase} subclasses which defines a number of tests against
+ L{_InputStream}. The subclass is expected to create a file-like object to
+ be wrapped by an L{_InputStream} under test.
+ """
+ def getFileType(self):
+ raise NotImplementedError(
+ "%s.getFile must be implemented" % (self.__class__.__name__,))
+
+
+ def _renderAndReturnReaderResult(self, reader, content):
+ contentType = self.getFileType()
+ class CustomizedRequest(Request):
+ def gotLength(self, length):
+ # Always allocate a file of the specified type, instead of
+ # using the base behavior of selecting one depending on the
+ # length.
+ self.content = contentType()
+
+ def appFactoryFactory(reader):
+ result = Deferred()
+ def applicationFactory():
+ def application(*args):
+ environ, startResponse = args
+ result.callback(reader(environ['wsgi.input']))
+ startResponse('200 OK', [])
+ return iter(())
+ return application
+ return result, applicationFactory
+ d, appFactory = appFactoryFactory(reader)
+ self.lowLevelRender(
+ CustomizedRequest, appFactory, DummyChannel,
+ 'PUT', '1.1', [], [''], None, [],
+ content)
+ return d
+
+
+ def test_readAll(self):
+ """
+ Calling L{_InputStream.read} with no arguments returns the entire input
+ stream.
+ """
+ bytes = "some bytes are here"
+ d = self._renderAndReturnReaderResult(lambda input: input.read(), bytes)
+ d.addCallback(self.assertEqual, bytes)
+ return d
+
+
+ def test_readSome(self):
+ """
+ Calling L{_InputStream.read} with an integer returns that many bytes
+ from the input stream, as long as it is less than or equal to the total
+ number of bytes available.
+ """
+ bytes = "hello, world."
+ d = self._renderAndReturnReaderResult(lambda input: input.read(3), bytes)
+ d.addCallback(self.assertEqual, "hel")
+ return d
+
+
+ def test_readMoreThan(self):
+ """
+ Calling L{_InputStream.read} with an integer that is greater than the
+ total number of bytes in the input stream returns all bytes in the
+ input stream.
+ """
+ bytes = "some bytes are here"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.read(len(bytes) + 3), bytes)
+ d.addCallback(self.assertEqual, bytes)
+ return d
+
+
+ def test_readTwice(self):
+ """
+ Calling L{_InputStream.read} a second time returns bytes starting from
+ the position after the last byte returned by the previous read.
+ """
+ bytes = "some bytes, hello"
+ def read(input):
+ input.read(3)
+ return input.read()
+ d = self._renderAndReturnReaderResult(read, bytes)
+ d.addCallback(self.assertEqual, bytes[3:])
+ return d
+
+
+ def test_readNone(self):
+ """
+ Calling L{_InputStream.read} with C{None} as an argument returns all
+ bytes in the input stream.
+ """
+ bytes = "the entire stream"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.read(None), bytes)
+ d.addCallback(self.assertEqual, bytes)
+ return d
+
+
+ def test_readNegative(self):
+ """
+ Calling L{_InputStream.read} with a negative integer as an argument
+ returns all bytes in the input stream.
+ """
+ bytes = "all of the input"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.read(-1), bytes)
+ d.addCallback(self.assertEqual, bytes)
+ return d
+
+
+ def test_readline(self):
+ """
+ Calling L{_InputStream.readline} with no argument returns one line from
+ the input stream.
+ """
+ bytes = "hello\nworld"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readline(), bytes)
+ d.addCallback(self.assertEqual, "hello\n")
+ return d
+
+
+ def test_readlineSome(self):
+ """
+ Calling L{_InputStream.readline} with an integer returns at most that
+ many bytes, even if it is not enough to make up a complete line.
+
+ COMPATIBILITY NOTE: the size argument is excluded from the WSGI
+ specification, but is provided here anyhow, because useful libraries
+ such as python stdlib's cgi.py assume their input file-like-object
+ supports readline with a size argument. If you use it, be aware your
+ application may not be portable to other conformant WSGI servers.
+ """
+ bytes = "goodbye\nworld"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readline(3), bytes)
+ d.addCallback(self.assertEqual, "goo")
+ return d
+
+
+ def test_readlineMoreThan(self):
+ """
+ Calling L{_InputStream.readline} with an integer which is greater than
+ the number of bytes in the next line returns only the next line.
+ """
+ bytes = "some lines\nof text"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readline(20), bytes)
+ d.addCallback(self.assertEqual, "some lines\n")
+ return d
+
+
+ def test_readlineTwice(self):
+ """
+ Calling L{_InputStream.readline} a second time returns the line
+ following the line returned by the first call.
+ """
+ bytes = "first line\nsecond line\nlast line"
+ def readline(input):
+ input.readline()
+ return input.readline()
+ d = self._renderAndReturnReaderResult(readline, bytes)
+ d.addCallback(self.assertEqual, "second line\n")
+ return d
+
+
+ def test_readlineNone(self):
+ """
+ Calling L{_InputStream.readline} with C{None} as an argument returns
+ one line from the input stream.
+ """
+ bytes = "this is one line\nthis is another line"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readline(None), bytes)
+ d.addCallback(self.assertEqual, "this is one line\n")
+ return d
+
+
+ def test_readlineNegative(self):
+ """
+ Calling L{_InputStream.readline} with a negative integer as an argument
+ returns one line from the input stream.
+ """
+ bytes = "input stream line one\nline two"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readline(-1), bytes)
+ d.addCallback(self.assertEqual, "input stream line one\n")
+ return d
+
+
+ def test_readlines(self):
+ """
+ Calling L{_InputStream.readlines} with no arguments returns a list of
+ all lines from the input stream.
+ """
+ bytes = "alice\nbob\ncarol"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readlines(), bytes)
+ d.addCallback(self.assertEqual, ["alice\n", "bob\n", "carol"])
+ return d
+
+
+ def test_readlinesSome(self):
+ """
+ Calling L{_InputStream.readlines} with an integer as an argument
+ returns a list of lines from the input stream with the argument serving
+ as an approximate bound on the total number of bytes to read.
+ """
+ bytes = "123\n456\n789\n0"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readlines(5), bytes)
+ def cbLines(lines):
+ # Make sure we got enough lines to make 5 bytes. Anything beyond
+ # that is fine too.
+ self.assertEqual(lines[:2], ["123\n", "456\n"])
+ d.addCallback(cbLines)
+ return d
+
+
+ def test_readlinesMoreThan(self):
+ """
+ Calling L{_InputStream.readlines} with an integer which is greater than
+ the total number of bytes in the input stream returns a list of all
+ lines from the input.
+ """
+ bytes = "one potato\ntwo potato\nthree potato"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readlines(100), bytes)
+ d.addCallback(
+ self.assertEqual,
+ ["one potato\n", "two potato\n", "three potato"])
+ return d
+
+
+ def test_readlinesAfterRead(self):
+ """
+ Calling L{_InputStream.readlines} after a call to L{_InputStream.read}
+ returns lines starting at the byte after the last byte returned by the
+ C{read} call.
+ """
+ bytes = "hello\nworld\nfoo"
+ def readlines(input):
+ input.read(7)
+ return input.readlines()
+ d = self._renderAndReturnReaderResult(readlines, bytes)
+ d.addCallback(self.assertEqual, ["orld\n", "foo"])
+ return d
+
+
+ def test_readlinesNone(self):
+ """
+ Calling L{_InputStream.readlines} with C{None} as an argument returns
+ all lines from the input.
+ """
+ bytes = "one fish\ntwo fish\n"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readlines(None), bytes)
+ d.addCallback(self.assertEqual, ["one fish\n", "two fish\n"])
+ return d
+
+
+ def test_readlinesNegative(self):
+ """
+ Calling L{_InputStream.readlines} with a negative integer as an
+ argument returns a list of all lines from the input.
+ """
+ bytes = "red fish\nblue fish\n"
+ d = self._renderAndReturnReaderResult(
+ lambda input: input.readlines(-1), bytes)
+ d.addCallback(self.assertEqual, ["red fish\n", "blue fish\n"])
+ return d
+
+
+ def test_iterable(self):
+ """
+ Iterating over L{_InputStream} produces lines from the input stream.
+ """
+ bytes = "green eggs\nand ham\n"
+ d = self._renderAndReturnReaderResult(lambda input: list(input), bytes)
+ d.addCallback(self.assertEqual, ["green eggs\n", "and ham\n"])
+ return d
+
+
+ def test_iterableAfterRead(self):
+ """
+ Iterating over L{_InputStream} after calling L{_InputStream.read}
+ produces lines from the input stream starting from the first byte after
+ the last byte returned by the C{read} call.
+ """
+ bytes = "green eggs\nand ham\n"
+ def iterate(input):
+ input.read(3)
+ return list(input)
+ d = self._renderAndReturnReaderResult(iterate, bytes)
+ d.addCallback(self.assertEqual, ["en eggs\n", "and ham\n"])
+ return d
+
+
+
+class InputStreamStringIOTests(InputStreamTestMixin, TestCase):
+ """
+ Tests for L{_InputStream} when it is wrapped around a L{StringIO.StringIO}.
+ """
+ def getFileType(self):
+ return StringIO.StringIO
+
+
+
+class InputStreamCStringIOTests(InputStreamTestMixin, TestCase):
+ """
+ Tests for L{_InputStream} when it is wrapped around a
+ L{cStringIO.StringIO}.
+ """
+ def getFileType(self):
+ return cStringIO.StringIO
+
+
+
+class InputStreamTemporaryFileTests(InputStreamTestMixin, TestCase):
+ """
+ Tests for L{_InputStream} when it is wrapped around a L{tempfile.TemporaryFile}.
+ """
+ def getFileType(self):
+ return tempfile.TemporaryFile
+
+
+
+class StartResponseTests(WSGITestsMixin, TestCase):
+ """
+ Tests for the I{start_response} parameter passed to the application object
+ by L{WSGIResource}.
+ """
+ def test_status(self):
+ """
+ The response status passed to the I{start_response} callable is written
+ as the status of the response to the request.
+ """
+ channel = DummyChannel()
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('107 Strange message', [])
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertTrue(
+ channel.transport.written.getvalue().startswith(
+ 'HTTP/1.1 107 Strange message'))
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def _headersTest(self, appHeaders, expectedHeaders):
+ """
+ Verify that if the response headers given by C{appHeaders} are passed
+ to the I{start_response} callable, then the response header lines given
+ by C{expectedHeaders} plus I{Server} and I{Date} header lines are
+ included in the response.
+ """
+ # Make the Date header value deterministic
+ self.patch(http, 'datetimeToString', lambda: 'Tuesday')
+
+ channel = DummyChannel()
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', appHeaders)
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ response = channel.transport.written.getvalue()
+ headers, rest = response.split('\r\n\r\n', 1)
+ headerLines = headers.split('\r\n')[1:]
+ headerLines.sort()
+ allExpectedHeaders = expectedHeaders + [
+ 'Date: Tuesday',
+ 'Server: ' + version,
+ 'Transfer-Encoding: chunked']
+ allExpectedHeaders.sort()
+ self.assertEqual(headerLines, allExpectedHeaders)
+
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+ return d
+
+
+ def test_headers(self):
+ """
+ The headers passed to the I{start_response} callable are included in
+ the response as are the required I{Date} and I{Server} headers and the
+ necessary connection (hop to hop) header I{Transfer-Encoding}.
+ """
+ return self._headersTest(
+ [('foo', 'bar'), ('baz', 'quux')],
+ ['Baz: quux', 'Foo: bar'])
+
+
+ def test_applicationProvidedContentType(self):
+ """
+ If I{Content-Type} is included in the headers passed to the
+ I{start_response} callable, one I{Content-Type} header is included in
+ the response.
+ """
+ return self._headersTest(
+ [('content-type', 'monkeys are great')],
+ ['Content-Type: monkeys are great'])
+
+
+ def test_applicationProvidedServerAndDate(self):
+ """
+ If either I{Server} or I{Date} is included in the headers passed to the
+ I{start_response} callable, they are disregarded.
+ """
+ return self._headersTest(
+ [('server', 'foo'), ('Server', 'foo'),
+ ('date', 'bar'), ('dATE', 'bar')],
+ [])
+
+
+ def test_delayedUntilReturn(self):
+ """
+ Nothing is written in response to a request when the I{start_response}
+ callable is invoked. If the iterator returned by the application
+ object produces only empty strings, the response is written after the
+ last element is produced.
+ """
+ channel = DummyChannel()
+
+ intermediateValues = []
+ def record():
+ intermediateValues.append(channel.transport.written.getvalue())
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [('foo', 'bar'), ('baz', 'quux')])
+ yield ''
+ record()
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertEqual(intermediateValues, [''])
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_delayedUntilContent(self):
+ """
+ Nothing is written in response to a request when the I{start_response}
+ callable is invoked. Once a non-empty string has been produced by the
+ iterator returned by the application object, the response status and
+ headers are written.
+ """
+ channel = DummyChannel()
+
+ intermediateValues = []
+ def record():
+ intermediateValues.append(channel.transport.written.getvalue())
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [('foo', 'bar')])
+ yield ''
+ record()
+ yield 'foo'
+ record()
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertFalse(intermediateValues[0])
+ self.assertTrue(intermediateValues[1])
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_content(self):
+ """
+ Content produced by the iterator returned by the application object is
+ written to the request as it is produced.
+ """
+ channel = DummyChannel()
+
+ intermediateValues = []
+ def record():
+ intermediateValues.append(channel.transport.written.getvalue())
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [('content-length', '6')])
+ yield 'foo'
+ record()
+ yield 'bar'
+ record()
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertEqual(
+ self.getContentFromResponse(intermediateValues[0]),
+ 'foo')
+ self.assertEqual(
+ self.getContentFromResponse(intermediateValues[1]),
+ 'foobar')
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_multipleStartResponse(self):
+ """
+ If the I{start_response} callable is invoked multiple times before a
+ data for the response body is produced, the values from the last call
+ are used.
+ """
+ channel = DummyChannel()
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('100 Foo', [])
+ startResponse('200 Bar', [])
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertTrue(
+ channel.transport.written.getvalue().startswith(
+ 'HTTP/1.1 200 Bar\r\n'))
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_startResponseWithException(self):
+ """
+ If the I{start_response} callable is invoked with a third positional
+ argument before the status and headers have been written to the
+ response, the status and headers become the newly supplied values.
+ """
+ channel = DummyChannel()
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('100 Foo', [], (Exception, Exception("foo"), None))
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertTrue(
+ channel.transport.written.getvalue().startswith(
+ 'HTTP/1.1 100 Foo\r\n'))
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_startResponseWithExceptionTooLate(self):
+ """
+ If the I{start_response} callable is invoked with a third positional
+ argument after the status and headers have been written to the
+ response, the supplied I{exc_info} values are re-raised to the
+ application.
+ """
+ channel = DummyChannel()
+
+ class SomeException(Exception):
+ pass
+
+ try:
+ raise SomeException()
+ except:
+ excInfo = exc_info()
+
+ reraised = []
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ yield 'foo'
+ try:
+ startResponse('500 ERR', [], excInfo)
+ except:
+ reraised.append(exc_info())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertTrue(
+ channel.transport.written.getvalue().startswith(
+ 'HTTP/1.1 200 OK\r\n'))
+ self.assertEqual(reraised[0][0], excInfo[0])
+ self.assertEqual(reraised[0][1], excInfo[1])
+ self.assertEqual(reraised[0][2].tb_next, excInfo[2])
+
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_write(self):
+ """
+ I{start_response} returns the I{write} callable which can be used to
+ write bytes to the response body without buffering.
+ """
+ channel = DummyChannel()
+
+ intermediateValues = []
+ def record():
+ intermediateValues.append(channel.transport.written.getvalue())
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ write = startResponse('100 Foo', [('content-length', '6')])
+ write('foo')
+ record()
+ write('bar')
+ record()
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertEqual(
+ self.getContentFromResponse(intermediateValues[0]),
+ 'foo')
+ self.assertEqual(
+ self.getContentFromResponse(intermediateValues[1]),
+ 'foobar')
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+
+class ApplicationTests(WSGITestsMixin, TestCase):
+ """
+ Tests for things which are done to the application object and the iterator
+ it returns.
+ """
+ def enableThreads(self):
+ self.reactor = reactor
+ self.threadpool = ThreadPool()
+ self.threadpool.start()
+ self.addCleanup(self.threadpool.stop)
+
+
+ def test_close(self):
+ """
+ If the application object returns an iterator which also has a I{close}
+ method, that method is called after iteration is complete.
+ """
+ channel = DummyChannel()
+
+ class Result:
+ def __init__(self):
+ self.open = True
+
+ def __iter__(self):
+ for i in range(3):
+ if self.open:
+ yield str(i)
+
+ def close(self):
+ self.open = False
+
+ result = Result()
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [('content-length', '3')])
+ return result
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertEqual(
+ self.getContentFromResponse(
+ channel.transport.written.getvalue()),
+ '012')
+ self.assertFalse(result.open)
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''])
+
+ return d
+
+
+ def test_applicationCalledInThread(self):
+ """
+ The application object is invoked and iterated in a thread which is not
+ the reactor thread.
+ """
+ self.enableThreads()
+ invoked = []
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ def result():
+ for i in range(3):
+ invoked.append(get_ident())
+ yield str(i)
+ invoked.append(get_ident())
+ startResponse('200 OK', [('content-length', '3')])
+ return result()
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ self.assertNotIn(get_ident(), invoked)
+ self.assertEqual(len(set(invoked)), 1)
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory,
+ DummyChannel, 'GET', '1.1', [], [''])
+
+ return d
+
+
+ def test_writeCalledFromThread(self):
+ """
+ The I{write} callable returned by I{start_response} calls the request's
+ C{write} method in the reactor thread.
+ """
+ self.enableThreads()
+ invoked = []
+
+ class ThreadVerifier(Request):
+ def write(self, bytes):
+ invoked.append(get_ident())
+ return Request.write(self, bytes)
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ write = startResponse('200 OK', [])
+ write('foo')
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory(ThreadVerifier)
+ def cbRendered(ignored):
+ self.assertEqual(set(invoked), set([get_ident()]))
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory, DummyChannel,
+ 'GET', '1.1', [], [''])
+
+ return d
+
+
+ def test_iteratedValuesWrittenFromThread(self):
+ """
+ Strings produced by the iterator returned by the application object are
+ written to the request in the reactor thread.
+ """
+ self.enableThreads()
+ invoked = []
+
+ class ThreadVerifier(Request):
+ def write(self, bytes):
+ invoked.append(get_ident())
+ return Request.write(self, bytes)
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ yield 'foo'
+ return application
+
+ d, requestFactory = self.requestFactoryFactory(ThreadVerifier)
+ def cbRendered(ignored):
+ self.assertEqual(set(invoked), set([get_ident()]))
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory, DummyChannel,
+ 'GET', '1.1', [], [''])
+
+ return d
+
+
+ def test_statusWrittenFromThread(self):
+ """
+ The response status is set on the request object in the reactor thread.
+ """
+ self.enableThreads()
+ invoked = []
+
+ class ThreadVerifier(Request):
+ def setResponseCode(self, code, message):
+ invoked.append(get_ident())
+ return Request.setResponseCode(self, code, message)
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ return iter(())
+ return application
+
+ d, requestFactory = self.requestFactoryFactory(ThreadVerifier)
+ def cbRendered(ignored):
+ self.assertEqual(set(invoked), set([get_ident()]))
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory, DummyChannel,
+ 'GET', '1.1', [], [''])
+
+ return d
+
+
+ def test_connectionClosedDuringIteration(self):
+ """
+ If the request connection is lost while the application object is being
+ iterated, iteration is stopped.
+ """
+ class UnreliableConnection(Request):
+ """
+ This is a request which pretends its connection is lost immediately
+ after the first write is done to it.
+ """
+ def write(self, bytes):
+ self.connectionLost(Failure(ConnectionLost("No more connection")))
+
+ self.badIter = False
+ def appIter():
+ yield "foo"
+ self.badIter = True
+ raise Exception("Should not have gotten here")
+
+ def applicationFactory():
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ return appIter()
+ return application
+
+ d, requestFactory = self.requestFactoryFactory(UnreliableConnection)
+ def cbRendered(ignored):
+ self.assertFalse(self.badIter, "Should not have resumed iteration")
+ d.addCallback(cbRendered)
+
+ self.lowLevelRender(
+ requestFactory, applicationFactory, DummyChannel,
+ 'GET', '1.1', [], [''])
+
+ return self.assertFailure(d, ConnectionLost)
+
+
+ def _internalServerErrorTest(self, application):
+ channel = DummyChannel()
+
+ def applicationFactory():
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+ def cbRendered(ignored):
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+
+ self.assertTrue(
+ channel.transport.written.getvalue().startswith(
+ 'HTTP/1.1 500 Internal Server Error'))
+ d.addCallback(cbRendered)
+
+ request = self.lowLevelRender(
+ requestFactory, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ return d
+
+
+ def test_applicationExceptionBeforeStartResponse(self):
+ """
+ If the application raises an exception before calling I{start_response}
+ then the response status is I{500} and the exception is logged.
+ """
+ def application(environ, startResponse):
+ raise RuntimeError("This application had some error.")
+ return self._internalServerErrorTest(application)
+
+
+ def test_applicationExceptionAfterStartResponse(self):
+ """
+ If the application calls I{start_response} but then raises an exception
+ before any data is written to the response then the response status is
+ I{500} and the exception is logged.
+ """
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ raise RuntimeError("This application had some error.")
+ return self._internalServerErrorTest(application)
+
+
+ def _connectionClosedTest(self, application, responseContent):
+ channel = DummyChannel()
+
+ def applicationFactory():
+ return application
+
+ d, requestFactory = self.requestFactoryFactory()
+
+ # Capture the request so we can disconnect it later on.
+ requests = []
+ def requestFactoryWrapper(*a, **kw):
+ requests.append(requestFactory(*a, **kw))
+ return requests[-1]
+
+ def ebRendered(ignored):
+ errors = self.flushLoggedErrors(RuntimeError)
+ self.assertEqual(len(errors), 1)
+
+ response = channel.transport.written.getvalue()
+ self.assertTrue(response.startswith('HTTP/1.1 200 OK'))
+ # Chunked transfer-encoding makes this a little messy.
+ self.assertIn(responseContent, response)
+ d.addErrback(ebRendered)
+
+ request = self.lowLevelRender(
+ requestFactoryWrapper, applicationFactory,
+ lambda: channel, 'GET', '1.1', [], [''], None, [])
+
+ # By now the connection should be closed.
+ self.assertTrue(channel.transport.disconnected)
+ # Give it a little push to go the rest of the way.
+ requests[0].connectionLost(Failure(ConnectionLost("All gone")))
+
+ return d
+
+
+ def test_applicationExceptionAfterWrite(self):
+ """
+ If the application raises an exception after the response status has
+ already been sent then the connection is closed and the exception is
+ logged.
+ """
+ responseContent = (
+ 'Some bytes, triggering the server to start sending the response')
+
+ def application(environ, startResponse):
+ startResponse('200 OK', [])
+ yield responseContent
+ raise RuntimeError("This application had some error.")
+ return self._connectionClosedTest(application, responseContent)
+
+
+ def test_applicationCloseException(self):
+ """
+ If the application returns a closeable iterator and the C{close} method
+ raises an exception when called then the connection is still closed and
+ the exception is logged.
+ """
+ responseContent = 'foo'
+
+ class Application(object):
+ def __init__(self, environ, startResponse):
+ startResponse('200 OK', [])
+
+ def __iter__(self):
+ yield responseContent
+
+ def close(self):
+ raise RuntimeError("This application had some error.")
+
+ return self._connectionClosedTest(Application, responseContent)
diff --git a/twisted/web/test/test_xml.py b/twisted/web/test/test_xml.py
new file mode 100644
index 0000000..365e101
--- /dev/null
+++ b/twisted/web/test/test_xml.py
@@ -0,0 +1,1105 @@
+# -*- test-case-name: twisted.web.test.test_xml -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Some fairly inadequate testcases for Twisted XML support.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.web import sux
+from twisted.web import microdom
+from twisted.web import domhelpers
+
+
+class Sux0r(sux.XMLParser):
+ def __init__(self):
+ self.tokens = []
+
+ def getTagStarts(self):
+ return [token for token in self.tokens if token[0] == 'start']
+
+ def gotTagStart(self, name, attrs):
+ self.tokens.append(("start", name, attrs))
+
+ def gotText(self, text):
+ self.tokens.append(("text", text))
+
+class SUXTest(TestCase):
+
+ def testBork(self):
+ s = "<bork><bork><bork>"
+ ms = Sux0r()
+ ms.connectionMade()
+ ms.dataReceived(s)
+ self.assertEqual(len(ms.getTagStarts()),3)
+
+
+class MicroDOMTest(TestCase):
+
+ def test_leadingTextDropping(self):
+ """
+ Make sure that if there's no top-level node lenient-mode won't
+ drop leading text that's outside of any elements.
+ """
+ s = "Hi orders! <br>Well. <br>"
+ d = microdom.parseString(s, beExtremelyLenient=True)
+ self.assertEqual(d.firstChild().toxml(),
+ '<html>Hi orders! <br />Well. <br /></html>')
+
+ def test_trailingTextDropping(self):
+ """
+ Ensure that no *trailing* text in a mal-formed
+ no-top-level-element document(s) will not be dropped.
+ """
+ s = "<br>Hi orders!"
+ d = microdom.parseString(s, beExtremelyLenient=True)
+ self.assertEqual(d.firstChild().toxml(),
+ '<html><br />Hi orders!</html>')
+
+
+ def test_noTags(self):
+ """
+ A string with nothing that looks like a tag at all should just
+ be parsed as body text.
+ """
+ s = "Hi orders!"
+ d = microdom.parseString(s, beExtremelyLenient=True)
+ self.assertEqual(d.firstChild().toxml(),
+ "<html>Hi orders!</html>")
+
+
+ def test_surroundingCrap(self):
+ """
+ If a document is surrounded by non-xml text, the text should
+ be remain in the XML.
+ """
+ s = "Hi<br> orders!"
+ d = microdom.parseString(s, beExtremelyLenient=True)
+ self.assertEqual(d.firstChild().toxml(),
+ "<html>Hi<br /> orders!</html>")
+
+
+ def testCaseSensitiveSoonCloser(self):
+ s = """
+ <HTML><BODY>
+ <P ALIGN="CENTER">
+ <A HREF="http://www.apache.org/"><IMG SRC="/icons/apache_pb.gif"></A>
+ </P>
+
+ <P>
+ This is an insane set of text nodes that should NOT be gathered under
+ the A tag above.
+ </P>
+ </BODY></HTML>
+ """
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ l = domhelpers.findNodesNamed(d.documentElement, 'a')
+ n = domhelpers.gatherTextNodes(l[0],1).replace('&nbsp;',' ')
+ self.assertEqual(n.find('insane'), -1)
+
+
+ def test_lenientParenting(self):
+ """
+ Test that C{parentNode} attributes are set to meaningful values when
+ we are parsing HTML that lacks a root node.
+ """
+ # Spare the rod, ruin the child.
+ s = "<br/><br/>"
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ self.assertIdentical(d.documentElement,
+ d.documentElement.firstChild().parentNode)
+
+
+ def test_lenientParentSingle(self):
+ """
+ Test that the C{parentNode} attribute is set to a meaningful value
+ when we parse an HTML document that has a non-Element root node.
+ """
+ s = "Hello"
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ self.assertIdentical(d.documentElement,
+ d.documentElement.firstChild().parentNode)
+
+
+ def testUnEntities(self):
+ s = """
+ <HTML>
+ This HTML goes between Stupid <=CrAzY!=> Dumb.
+ </HTML>
+ """
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ n = domhelpers.gatherTextNodes(d)
+ self.assertNotEquals(n.find('>'), -1)
+
+ def testEmptyError(self):
+ self.assertRaises(sux.ParseError, microdom.parseString, "")
+
+ def testTameDocument(self):
+ s = """
+ <test>
+ <it>
+ <is>
+ <a>
+ test
+ </a>
+ </is>
+ </it>
+ </test>
+ """
+ d = microdom.parseString(s)
+ self.assertEqual(
+ domhelpers.gatherTextNodes(d.documentElement).strip() ,'test')
+
+ def testAwfulTagSoup(self):
+ s = """
+ <html>
+ <head><title> I send you this message to have your advice!!!!</titl e
+ </headd>
+
+ <body bgcolor alink hlink vlink>
+
+ <h1><BLINK>SALE</blINK> TWENTY MILLION EMAILS & FUR COAT NOW
+ FREE WITH `ENLARGER'</h1>
+
+ YES THIS WONDERFUL AWFER IS NOW HERER!!!
+
+ <script LANGUAGE="javascript">
+function give_answers() {
+if (score < 70) {
+alert("I hate you");
+}}
+ </script><a href=/foo.com/lalal name=foo>lalal</a>
+ </body>
+ </HTML>
+ """
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ l = domhelpers.findNodesNamed(d.documentElement, 'blink')
+ self.assertEqual(len(l), 1)
+
+ def testScriptLeniency(self):
+ s = """
+ <script>(foo < bar) and (bar > foo)</script>
+ <script language="javascript">foo </scrip bar </script>
+ <script src="foo">
+ <script src="foo">baz</script>
+ <script /><script></script>
+ """
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ self.assertEqual(d.firstChild().firstChild().firstChild().data,
+ "(foo < bar) and (bar > foo)")
+ self.assertEqual(
+ d.firstChild().getElementsByTagName("script")[1].firstChild().data,
+ "foo </scrip bar ")
+
+ def testScriptLeniencyIntelligence(self):
+ # if there is comment or CDATA in script, the autoquoting in bEL mode
+ # should not happen
+ s = """<script><!-- lalal --></script>"""
+ self.assertEqual(
+ microdom.parseString(s, beExtremelyLenient=1).firstChild().toxml(), s)
+ s = """<script><![CDATA[lalal]]></script>"""
+ self.assertEqual(
+ microdom.parseString(s, beExtremelyLenient=1).firstChild().toxml(), s)
+ s = """<script> // <![CDATA[
+ lalal
+ //]]></script>"""
+ self.assertEqual(
+ microdom.parseString(s, beExtremelyLenient=1).firstChild().toxml(), s)
+
+ def testPreserveCase(self):
+ s = '<eNcApSuLaTe><sUxor></sUxor><bOrk><w00T>TeXt</W00t></BoRk></EnCaPsUlAtE>'
+ s2 = s.lower().replace('text', 'TeXt')
+ # these are the only two option permutations that *can* parse the above
+ d = microdom.parseString(s, caseInsensitive=1, preserveCase=1)
+ d2 = microdom.parseString(s, caseInsensitive=1, preserveCase=0)
+ # caseInsensitive=0 preserveCase=0 is not valid, it's converted to
+ # caseInsensitive=0 preserveCase=1
+ d3 = microdom.parseString(s2, caseInsensitive=0, preserveCase=1)
+ d4 = microdom.parseString(s2, caseInsensitive=1, preserveCase=0)
+ d5 = microdom.parseString(s2, caseInsensitive=1, preserveCase=1)
+ # this is slightly contrived, toxml() doesn't need to be identical
+ # for the documents to be equivalent (i.e. <b></b> to <b/>),
+ # however this assertion tests preserving case for start and
+ # end tags while still matching stuff like <bOrk></BoRk>
+ self.assertEqual(d.documentElement.toxml(), s)
+ self.assert_(d.isEqualToDocument(d2), "%r != %r" % (d.toxml(), d2.toxml()))
+ self.assert_(d2.isEqualToDocument(d3), "%r != %r" % (d2.toxml(), d3.toxml()))
+ # caseInsensitive=0 on the left, NOT perserveCase=1 on the right
+ ## XXX THIS TEST IS TURNED OFF UNTIL SOMEONE WHO CARES ABOUT FIXING IT DOES
+ #self.failIf(d3.isEqualToDocument(d2), "%r == %r" % (d3.toxml(), d2.toxml()))
+ self.assert_(d3.isEqualToDocument(d4), "%r != %r" % (d3.toxml(), d4.toxml()))
+ self.assert_(d4.isEqualToDocument(d5), "%r != %r" % (d4.toxml(), d5.toxml()))
+
+ def testDifferentQuotes(self):
+ s = '<test a="a" b=\'b\' />'
+ d = microdom.parseString(s)
+ e = d.documentElement
+ self.assertEqual(e.getAttribute('a'), 'a')
+ self.assertEqual(e.getAttribute('b'), 'b')
+
+ def testLinebreaks(self):
+ s = '<test \na="a"\n\tb="#b" />'
+ d = microdom.parseString(s)
+ e = d.documentElement
+ self.assertEqual(e.getAttribute('a'), 'a')
+ self.assertEqual(e.getAttribute('b'), '#b')
+
+ def testMismatchedTags(self):
+ for s in '<test>', '<test> </tset>', '</test>':
+ self.assertRaises(microdom.MismatchedTags, microdom.parseString, s)
+
+ def testComment(self):
+ s = "<bar><!--<foo />--></bar>"
+ d = microdom.parseString(s)
+ e = d.documentElement
+ self.assertEqual(e.nodeName, "bar")
+ c = e.childNodes[0]
+ self.assert_(isinstance(c, microdom.Comment))
+ self.assertEqual(c.value, "<foo />")
+ c2 = c.cloneNode()
+ self.assert_(c is not c2)
+ self.assertEqual(c2.toxml(), "<!--<foo />-->")
+
+ def testText(self):
+ d = microdom.parseString("<bar>xxxx</bar>").documentElement
+ text = d.childNodes[0]
+ self.assert_(isinstance(text, microdom.Text))
+ self.assertEqual(text.value, "xxxx")
+ clone = text.cloneNode()
+ self.assert_(clone is not text)
+ self.assertEqual(clone.toxml(), "xxxx")
+
+ def testEntities(self):
+ nodes = microdom.parseString("<b>&amp;&#12AB;</b>").documentElement.childNodes
+ self.assertEqual(len(nodes), 2)
+ self.assertEqual(nodes[0].data, "&amp;")
+ self.assertEqual(nodes[1].data, "&#12AB;")
+ self.assertEqual(nodes[0].cloneNode().toxml(), "&amp;")
+ for n in nodes:
+ self.assert_(isinstance(n, microdom.EntityReference))
+
+ def testCData(self):
+ s = '<x><![CDATA[</x>\r\n & foo]]></x>'
+ cdata = microdom.parseString(s).documentElement.childNodes[0]
+ self.assert_(isinstance(cdata, microdom.CDATASection))
+ self.assertEqual(cdata.data, "</x>\r\n & foo")
+ self.assertEqual(cdata.cloneNode().toxml(), "<![CDATA[</x>\r\n & foo]]>")
+
+ def testSingletons(self):
+ s = "<foo><b/><b /><b\n/></foo>"
+ s2 = "<foo><b/><b/><b/></foo>"
+ nodes = microdom.parseString(s).documentElement.childNodes
+ nodes2 = microdom.parseString(s2).documentElement.childNodes
+ self.assertEqual(len(nodes), 3)
+ for (n, n2) in zip(nodes, nodes2):
+ self.assert_(isinstance(n, microdom.Element))
+ self.assertEqual(n.nodeName, "b")
+ self.assert_(n.isEqualToNode(n2))
+
+ def testAttributes(self):
+ s = '<foo a="b" />'
+ node = microdom.parseString(s).documentElement
+
+ self.assertEqual(node.getAttribute("a"), "b")
+ self.assertEqual(node.getAttribute("c"), None)
+ self.assert_(node.hasAttribute("a"))
+ self.assert_(not node.hasAttribute("c"))
+ a = node.getAttributeNode("a")
+ self.assertEqual(a.value, "b")
+
+ node.setAttribute("foo", "bar")
+ self.assertEqual(node.getAttribute("foo"), "bar")
+
+ def testChildren(self):
+ s = "<foo><bar /><baz /><bax>foo</bax></foo>"
+ d = microdom.parseString(s).documentElement
+ self.assertEqual([n.nodeName for n in d.childNodes], ["bar", "baz", "bax"])
+ self.assertEqual(d.lastChild().nodeName, "bax")
+ self.assertEqual(d.firstChild().nodeName, "bar")
+ self.assert_(d.hasChildNodes())
+ self.assert_(not d.firstChild().hasChildNodes())
+
+ def testMutate(self):
+ s = "<foo />"
+ s1 = '<foo a="b"><bar/><foo/></foo>'
+ s2 = '<foo a="b">foo</foo>'
+ d = microdom.parseString(s).documentElement
+ d1 = microdom.parseString(s1).documentElement
+ d2 = microdom.parseString(s2).documentElement
+
+ d.appendChild(d.cloneNode())
+ d.setAttribute("a", "b")
+ child = d.childNodes[0]
+ self.assertEqual(child.getAttribute("a"), None)
+ self.assertEqual(child.nodeName, "foo")
+
+ d.insertBefore(microdom.Element("bar"), child)
+ self.assertEqual(d.childNodes[0].nodeName, "bar")
+ self.assertEqual(d.childNodes[1], child)
+ for n in d.childNodes:
+ self.assertEqual(n.parentNode, d)
+ self.assert_(d.isEqualToNode(d1))
+
+ d.removeChild(child)
+ self.assertEqual(len(d.childNodes), 1)
+ self.assertEqual(d.childNodes[0].nodeName, "bar")
+
+ t = microdom.Text("foo")
+ d.replaceChild(t, d.firstChild())
+ self.assertEqual(d.firstChild(), t)
+ self.assert_(d.isEqualToNode(d2))
+
+
+ def test_replaceNonChild(self):
+ """
+ L{Node.replaceChild} raises L{ValueError} if the node given to be
+ replaced is not a child of the node C{replaceChild} is called on.
+ """
+ parent = microdom.parseString('<foo />')
+ orphan = microdom.parseString('<bar />')
+ replacement = microdom.parseString('<baz />')
+
+ self.assertRaises(
+ ValueError, parent.replaceChild, replacement, orphan)
+
+
+ def testSearch(self):
+ s = "<foo><bar id='me' /><baz><foo /></baz></foo>"
+ s2 = "<fOo><bAr id='me' /><bAz><fOO /></bAz></fOo>"
+ d = microdom.parseString(s)
+ d2 = microdom.parseString(s2, caseInsensitive=0, preserveCase=1)
+ d3 = microdom.parseString(s2, caseInsensitive=1, preserveCase=1)
+
+ root = d.documentElement
+ self.assertEqual(root.firstChild(), d.getElementById('me'))
+ self.assertEqual(d.getElementsByTagName("foo"),
+ [root, root.lastChild().firstChild()])
+
+ root = d2.documentElement
+ self.assertEqual(root.firstChild(), d2.getElementById('me'))
+ self.assertEqual(d2.getElementsByTagName('fOo'), [root])
+ self.assertEqual(d2.getElementsByTagName('fOO'),
+ [root.lastChild().firstChild()])
+ self.assertEqual(d2.getElementsByTagName('foo'), [])
+
+ root = d3.documentElement
+ self.assertEqual(root.firstChild(), d3.getElementById('me'))
+ self.assertEqual(d3.getElementsByTagName('FOO'),
+ [root, root.lastChild().firstChild()])
+ self.assertEqual(d3.getElementsByTagName('fOo'),
+ [root, root.lastChild().firstChild()])
+
+ def testDoctype(self):
+ s = ('<?xml version="1.0"?>'
+ '<!DOCTYPE foo PUBLIC "baz" "http://www.example.com/example.dtd">'
+ '<foo></foo>')
+ s2 = '<foo/>'
+ d = microdom.parseString(s)
+ d2 = microdom.parseString(s2)
+ self.assertEqual(d.doctype,
+ 'foo PUBLIC "baz" "http://www.example.com/example.dtd"')
+ self.assertEqual(d.toxml(), s)
+ self.failIf(d.isEqualToDocument(d2))
+ self.failUnless(d.documentElement.isEqualToNode(d2.documentElement))
+
+ samples = [("<img/>", "<img />"),
+ ("<foo A='b'>x</foo>", '<foo A="b">x</foo>'),
+ ("<foo><BAR /></foo>", "<foo><BAR></BAR></foo>"),
+ ("<foo>hello there &amp; yoyoy</foo>",
+ "<foo>hello there &amp; yoyoy</foo>"),
+ ]
+
+ def testOutput(self):
+ for s, out in self.samples:
+ d = microdom.parseString(s, caseInsensitive=0)
+ d2 = microdom.parseString(out, caseInsensitive=0)
+ testOut = d.documentElement.toxml()
+ self.assertEqual(out, testOut)
+ self.assert_(d.isEqualToDocument(d2))
+
+ def testErrors(self):
+ for s in ["<foo>&am</foo>", "<foo", "<f>&</f>", "<() />"]:
+ self.assertRaises(Exception, microdom.parseString, s)
+
+ def testCaseInsensitive(self):
+ s = "<foo a='b'><BAx>x</bax></FOO>"
+ s2 = '<foo a="b"><bax>x</bax></foo>'
+ s3 = "<FOO a='b'><BAx>x</BAx></FOO>"
+ s4 = "<foo A='b'>x</foo>"
+ d = microdom.parseString(s)
+ d2 = microdom.parseString(s2)
+ d3 = microdom.parseString(s3, caseInsensitive=1)
+ d4 = microdom.parseString(s4, caseInsensitive=1, preserveCase=1)
+ d5 = microdom.parseString(s4, caseInsensitive=1, preserveCase=0)
+ d6 = microdom.parseString(s4, caseInsensitive=0, preserveCase=0)
+ out = microdom.parseString(s).documentElement.toxml()
+ self.assertRaises(microdom.MismatchedTags, microdom.parseString,
+ s, caseInsensitive=0)
+ self.assertEqual(out, s2)
+ self.failUnless(d.isEqualToDocument(d2))
+ self.failUnless(d.isEqualToDocument(d3))
+ self.failUnless(d4.documentElement.hasAttribute('a'))
+ self.failIf(d6.documentElement.hasAttribute('a'))
+ self.assertEqual(d4.documentElement.toxml(), '<foo A="b">x</foo>')
+ self.assertEqual(d5.documentElement.toxml(), '<foo a="b">x</foo>')
+ def testEatingWhitespace(self):
+ s = """<hello>
+ </hello>"""
+ d = microdom.parseString(s)
+ self.failUnless(not d.documentElement.hasChildNodes(),
+ d.documentElement.childNodes)
+ self.failUnless(d.isEqualToDocument(microdom.parseString('<hello></hello>')))
+
+ def testLenientAmpersand(self):
+ prefix = "<?xml version='1.0'?>"
+ # we use <pre> so space will be preserved
+ for i, o in [("&", "&amp;"),
+ ("& ", "&amp; "),
+ ("&amp;", "&amp;"),
+ ("&hello monkey", "&amp;hello monkey")]:
+ d = microdom.parseString("%s<pre>%s</pre>"
+ % (prefix, i), beExtremelyLenient=1)
+ self.assertEqual(d.documentElement.toxml(), "<pre>%s</pre>" % o)
+ # non-space preserving
+ d = microdom.parseString("<t>hello & there</t>", beExtremelyLenient=1)
+ self.assertEqual(d.documentElement.toxml(), "<t>hello &amp; there</t>")
+
+ def testInsensitiveLenient(self):
+ # testing issue #537
+ d = microdom.parseString(
+ "<?xml version='1.0'?><bar><xA><y>c</Xa> <foo></bar>",
+ beExtremelyLenient=1)
+ self.assertEqual(d.documentElement.firstChild().toxml(), "<xa><y>c</y></xa>")
+
+ def testLaterCloserSimple(self):
+ s = "<ul><li>foo<li>bar<li>baz</ul>"
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ expected = "<ul><li>foo</li><li>bar</li><li>baz</li></ul>"
+ actual = d.documentElement.toxml()
+ self.assertEqual(expected, actual)
+
+ def testLaterCloserCaseInsensitive(self):
+ s = "<DL><p><DT>foo<DD>bar</DL>"
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ expected = "<dl><p></p><dt>foo</dt><dd>bar</dd></dl>"
+ actual = d.documentElement.toxml()
+ self.assertEqual(expected, actual)
+
+ def testLaterCloserTable(self):
+ s = ("<table>"
+ "<tr><th>name<th>value<th>comment"
+ "<tr><th>this<td>tag<td>soup"
+ "<tr><th>must<td>be<td>handled"
+ "</table>")
+ expected = ("<table>"
+ "<tr><th>name</th><th>value</th><th>comment</th></tr>"
+ "<tr><th>this</th><td>tag</td><td>soup</td></tr>"
+ "<tr><th>must</th><td>be</td><td>handled</td></tr>"
+ "</table>")
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ actual = d.documentElement.toxml()
+ self.assertEqual(expected, actual)
+ testLaterCloserTable.todo = "Table parsing needs to be fixed."
+
+ def testLaterCloserDL(self):
+ s = ("<dl>"
+ "<dt>word<dd>definition"
+ "<dt>word<dt>word<dd>definition<dd>definition"
+ "</dl>")
+ expected = ("<dl>"
+ "<dt>word</dt><dd>definition</dd>"
+ "<dt>word</dt><dt>word</dt><dd>definition</dd><dd>definition</dd>"
+ "</dl>")
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ actual = d.documentElement.toxml()
+ self.assertEqual(expected, actual)
+
+ def testLaterCloserDL2(self):
+ s = ("<dl>"
+ "<dt>word<dd>definition<p>more definition"
+ "<dt>word"
+ "</dl>")
+ expected = ("<dl>"
+ "<dt>word</dt><dd>definition<p>more definition</p></dd>"
+ "<dt>word</dt>"
+ "</dl>")
+ d = microdom.parseString(s, beExtremelyLenient=1)
+ actual = d.documentElement.toxml()
+ self.assertEqual(expected, actual)
+
+ testLaterCloserDL2.todo = "unclosed <p> messes it up."
+
+ def testUnicodeTolerance(self):
+ import struct
+ s = '<foo><bar><baz /></bar></foo>'
+ j =(u'<?xml version="1.0" encoding="UCS-2" ?>\r\n<JAPANESE>\r\n'
+ u'<TITLE>\u5c02\u9580\u5bb6\u30ea\u30b9\u30c8 </TITLE></JAPANESE>')
+ j2=('\xff\xfe<\x00?\x00x\x00m\x00l\x00 \x00v\x00e\x00r\x00s\x00i\x00o'
+ '\x00n\x00=\x00"\x001\x00.\x000\x00"\x00 \x00e\x00n\x00c\x00o\x00d'
+ '\x00i\x00n\x00g\x00=\x00"\x00U\x00C\x00S\x00-\x002\x00"\x00 \x00?'
+ '\x00>\x00\r\x00\n\x00<\x00J\x00A\x00P\x00A\x00N\x00E\x00S\x00E'
+ '\x00>\x00\r\x00\n\x00<\x00T\x00I\x00T\x00L\x00E\x00>\x00\x02\\'
+ '\x80\x95\xb6[\xea0\xb90\xc80 \x00<\x00/\x00T\x00I\x00T\x00L\x00E'
+ '\x00>\x00<\x00/\x00J\x00A\x00P\x00A\x00N\x00E\x00S\x00E\x00>\x00')
+ def reverseBytes(s):
+ fmt = str(len(s) / 2) + 'H'
+ return struct.pack('<' + fmt, *struct.unpack('>' + fmt, s))
+ urd = microdom.parseString(reverseBytes(s.encode('UTF-16')))
+ ud = microdom.parseString(s.encode('UTF-16'))
+ sd = microdom.parseString(s)
+ self.assert_(ud.isEqualToDocument(sd))
+ self.assert_(ud.isEqualToDocument(urd))
+ ud = microdom.parseString(j)
+ urd = microdom.parseString(reverseBytes(j2))
+ sd = microdom.parseString(j2)
+ self.assert_(ud.isEqualToDocument(sd))
+ self.assert_(ud.isEqualToDocument(urd))
+
+ # test that raw text still gets encoded
+ # test that comments get encoded
+ j3=microdom.parseString(u'<foo/>')
+ hdr='<?xml version="1.0"?>'
+ div=microdom.lmx().text(u'\u221a', raw=1).node
+ de=j3.documentElement
+ de.appendChild(div)
+ de.appendChild(j3.createComment(u'\u221a'))
+ self.assertEqual(j3.toxml(), hdr+
+ u'<foo><div>\u221a</div><!--\u221a--></foo>'.encode('utf8'))
+
+ def testNamedChildren(self):
+ tests = {"<foo><bar /><bar unf='1' /><bar>asdfadsf</bar>"
+ "<bam/></foo>" : 3,
+ '<foo>asdf</foo>' : 0,
+ '<foo><bar><bar></bar></bar></foo>' : 1,
+ }
+ for t in tests.keys():
+ node = microdom.parseString(t).documentElement
+ result = domhelpers.namedChildren(node, 'bar')
+ self.assertEqual(len(result), tests[t])
+ if result:
+ self.assert_(hasattr(result[0], 'tagName'))
+
+ def testCloneNode(self):
+ s = '<foo a="b"><bax>x</bax></foo>'
+ node = microdom.parseString(s).documentElement
+ clone = node.cloneNode(deep=1)
+ self.failIfEquals(node, clone)
+ self.assertEqual(len(node.childNodes), len(clone.childNodes))
+ c1, c2 = node.firstChild(), clone.firstChild()
+ self.failIfEquals(c1, c2)
+ self.assertEqual(len(c1.childNodes), len(c2.childNodes))
+ self.failIfEquals(c1.firstChild(), c2.firstChild())
+ self.assertEqual(s, clone.toxml())
+ self.assertEqual(node.namespace, clone.namespace)
+
+ def testCloneDocument(self):
+ s = ('<?xml version="1.0"?>'
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
+ '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><foo></foo>')
+
+ node = microdom.parseString(s)
+ clone = node.cloneNode(deep=1)
+ self.failIfEquals(node, clone)
+ self.assertEqual(len(node.childNodes), len(clone.childNodes))
+ self.assertEqual(s, clone.toxml())
+
+ self.failUnless(clone.isEqualToDocument(node))
+ self.failUnless(node.isEqualToDocument(clone))
+
+
+ def testLMX(self):
+ n = microdom.Element("p")
+ lmx = microdom.lmx(n)
+ lmx.text("foo")
+ b = lmx.b(a="c")
+ b.foo()["z"] = "foo"
+ b.foo()
+ b.add("bar", c="y")
+
+ s = '<p>foo<b a="c"><foo z="foo"></foo><foo></foo><bar c="y"></bar></b></p>'
+ self.assertEqual(s, n.toxml())
+
+ def testDict(self):
+ n = microdom.Element("p")
+ d = {n : 1} # will fail if Element is unhashable
+
+ def testEscaping(self):
+ # issue 590
+ raw = "&'some \"stuff\"', <what up?>"
+ cooked = "&amp;'some &quot;stuff&quot;', &lt;what up?&gt;"
+ esc1 = microdom.escape(raw)
+ self.assertEqual(esc1, cooked)
+ self.assertEqual(microdom.unescape(esc1), raw)
+
+ def testNamespaces(self):
+ s = '''
+ <x xmlns="base">
+ <y />
+ <y q="1" x:q="2" y:q="3" />
+ <y:y xml:space="1">here is some space </y:y>
+ <y:y />
+ <x:y />
+ </x>
+ '''
+ d = microdom.parseString(s)
+ # at least make sure it doesn't traceback
+ s2 = d.toprettyxml()
+ self.assertEqual(d.documentElement.namespace,
+ "base")
+ self.assertEqual(d.documentElement.getElementsByTagName("y")[0].namespace,
+ "base")
+ self.assertEqual(
+ d.documentElement.getElementsByTagName("y")[1].getAttributeNS('base','q'),
+ '1')
+
+ d2 = microdom.parseString(s2)
+ self.assertEqual(d2.documentElement.namespace,
+ "base")
+ self.assertEqual(d2.documentElement.getElementsByTagName("y")[0].namespace,
+ "base")
+ self.assertEqual(
+ d2.documentElement.getElementsByTagName("y")[1].getAttributeNS('base','q'),
+ '1')
+
+ def testNamespaceDelete(self):
+ """
+ Test that C{toxml} can support xml structures that remove namespaces.
+ """
+ s1 = ('<?xml version="1.0"?><html xmlns="http://www.w3.org/TR/REC-html40">'
+ '<body xmlns=""></body></html>')
+ s2 = microdom.parseString(s1).toxml()
+ self.assertEqual(s1, s2)
+
+ def testNamespaceInheritance(self):
+ """
+ Check that unspecified namespace is a thing separate from undefined
+ namespace. This test added after discovering some weirdness in Lore.
+ """
+ # will only work if childNodes is mutated. not sure why.
+ child = microdom.Element('ol')
+ parent = microdom.Element('div', namespace='http://www.w3.org/1999/xhtml')
+ parent.childNodes = [child]
+ self.assertEqual(parent.toxml(),
+ '<div xmlns="http://www.w3.org/1999/xhtml"><ol></ol></div>')
+
+ def test_prefixedTags(self):
+ """
+ XML elements with a prefixed name as per upper level tag definition
+ have a start-tag of C{"<prefix:tag>"} and an end-tag of
+ C{"</prefix:tag>"}.
+
+ Refer to U{http://www.w3.org/TR/xml-names/#ns-using} for details.
+ """
+ outerNamespace = "http://example.com/outer"
+ innerNamespace = "http://example.com/inner"
+
+ document = microdom.Document()
+ # Create the root in one namespace. Microdom will probably make this
+ # the default namespace.
+ root = document.createElement("root", namespace=outerNamespace)
+
+ # Give the root some prefixes to use.
+ root.addPrefixes({innerNamespace: "inner"})
+
+ # Append a child to the root from the namespace that prefix is bound
+ # to.
+ tag = document.createElement("tag", namespace=innerNamespace)
+
+ # Give that tag a child too. This way we test rendering of tags with
+ # children and without children.
+ child = document.createElement("child", namespace=innerNamespace)
+
+ tag.appendChild(child)
+ root.appendChild(tag)
+ document.appendChild(root)
+
+ # ok, the xml should appear like this
+ xmlOk = (
+ '<?xml version="1.0"?>'
+ '<root xmlns="http://example.com/outer" '
+ 'xmlns:inner="http://example.com/inner">'
+ '<inner:tag><inner:child></inner:child></inner:tag>'
+ '</root>')
+
+ xmlOut = document.toxml()
+ self.assertEqual(xmlOut, xmlOk)
+
+
+ def test_prefixPropagation(self):
+ """
+ Children of prefixed tags respect the default namespace at the point
+ where they are rendered. Specifically, they are not influenced by the
+ prefix of their parent as that prefix has no bearing on them.
+
+ See U{http://www.w3.org/TR/xml-names/#scoping} for details.
+
+ To further clarify the matter, the following::
+
+ <root xmlns="http://example.com/ns/test">
+ <mytag xmlns="http://example.com/ns/mytags">
+ <mysubtag xmlns="http://example.com/ns/mytags">
+ <element xmlns="http://example.com/ns/test"></element>
+ </mysubtag>
+ </mytag>
+ </root>
+
+ Should become this after all the namespace declarations have been
+ I{moved up}::
+
+ <root xmlns="http://example.com/ns/test"
+ xmlns:mytags="http://example.com/ns/mytags">
+ <mytags:mytag>
+ <mytags:mysubtag>
+ <element></element>
+ </mytags:mysubtag>
+ </mytags:mytag>
+ </root>
+ """
+ outerNamespace = "http://example.com/outer"
+ innerNamespace = "http://example.com/inner"
+
+ document = microdom.Document()
+ # creates a root element
+ root = document.createElement("root", namespace=outerNamespace)
+ document.appendChild(root)
+
+ # Create a child with a specific namespace with a prefix bound to it.
+ root.addPrefixes({innerNamespace: "inner"})
+ mytag = document.createElement("mytag",namespace=innerNamespace)
+ root.appendChild(mytag)
+
+ # Create a child of that which has the outer namespace.
+ mysubtag = document.createElement("mysubtag", namespace=outerNamespace)
+ mytag.appendChild(mysubtag)
+
+ xmlOk = (
+ '<?xml version="1.0"?>'
+ '<root xmlns="http://example.com/outer" '
+ 'xmlns:inner="http://example.com/inner">'
+ '<inner:mytag>'
+ '<mysubtag></mysubtag>'
+ '</inner:mytag>'
+ '</root>'
+ )
+ xmlOut = document.toxml()
+ self.assertEqual(xmlOut, xmlOk)
+
+
+
+class TestBrokenHTML(TestCase):
+ """
+ Tests for when microdom encounters very bad HTML and C{beExtremelyLenient}
+ is enabled. These tests are inspired by some HTML generated in by a mailer,
+ which breaks up very long lines by splitting them with '!\n '. The expected
+ behaviour is loosely modelled on the way Firefox treats very bad HTML.
+ """
+
+ def checkParsed(self, input, expected, beExtremelyLenient=1):
+ """
+ Check that C{input}, when parsed, produces a DOM where the XML
+ of the document element is equal to C{expected}.
+ """
+ output = microdom.parseString(input,
+ beExtremelyLenient=beExtremelyLenient)
+ self.assertEqual(output.documentElement.toxml(), expected)
+
+
+ def test_brokenAttributeName(self):
+ """
+ Check that microdom does its best to handle broken attribute names.
+ The important thing is that it doesn't raise an exception.
+ """
+ input = '<body><h1><div al!\n ign="center">Foo</div></h1></body>'
+ expected = ('<body><h1><div ign="center" al="True">'
+ 'Foo</div></h1></body>')
+ self.checkParsed(input, expected)
+
+
+ def test_brokenAttributeValue(self):
+ """
+ Check that microdom encompasses broken attribute values.
+ """
+ input = '<body><h1><div align="cen!\n ter">Foo</div></h1></body>'
+ expected = '<body><h1><div align="cen!\n ter">Foo</div></h1></body>'
+ self.checkParsed(input, expected)
+
+
+ def test_brokenOpeningTag(self):
+ """
+ Check that microdom does its best to handle broken opening tags.
+ The important thing is that it doesn't raise an exception.
+ """
+ input = '<body><h1><sp!\n an>Hello World!</span></h1></body>'
+ expected = '<body><h1><sp an="True">Hello World!</sp></h1></body>'
+ self.checkParsed(input, expected)
+
+
+ def test_brokenSelfClosingTag(self):
+ """
+ Check that microdom does its best to handle broken self-closing tags
+ The important thing is that it doesn't raise an exception.
+ """
+ self.checkParsed('<body><span /!\n></body>',
+ '<body><span></span></body>')
+ self.checkParsed('<span!\n />', '<span></span>')
+
+
+ def test_brokenClosingTag(self):
+ """
+ Check that microdom does its best to handle broken closing tags.
+ The important thing is that it doesn't raise an exception.
+ """
+ input = '<body><h1><span>Hello World!</sp!\nan></h1></body>'
+ expected = '<body><h1><span>Hello World!</span></h1></body>'
+ self.checkParsed(input, expected)
+ input = '<body><h1><span>Hello World!</!\nspan></h1></body>'
+ self.checkParsed(input, expected)
+ input = '<body><h1><span>Hello World!</span!\n></h1></body>'
+ self.checkParsed(input, expected)
+ input = '<body><h1><span>Hello World!<!\n/span></h1></body>'
+ expected = '<body><h1><span>Hello World!<!></!></span></h1></body>'
+ self.checkParsed(input, expected)
+
+
+
+
+class NodeTests(TestCase):
+ """
+ Tests for L{Node}.
+ """
+ def test_isNodeEqualTo(self):
+ """
+ L{Node.isEqualToNode} returns C{True} if and only if passed a L{Node}
+ with the same children.
+ """
+ # A node is equal to itself
+ node = microdom.Node(object())
+ self.assertTrue(node.isEqualToNode(node))
+ another = microdom.Node(object())
+ # Two nodes with no children are equal
+ self.assertTrue(node.isEqualToNode(another))
+ node.appendChild(microdom.Node(object()))
+ # A node with no children is not equal to a node with a child
+ self.assertFalse(node.isEqualToNode(another))
+ another.appendChild(microdom.Node(object()))
+ # A node with a child and no grandchildren is equal to another node
+ # with a child and no grandchildren.
+ self.assertTrue(node.isEqualToNode(another))
+ # A node with a child and a grandchild is not equal to another node
+ # with a child and no grandchildren.
+ node.firstChild().appendChild(microdom.Node(object()))
+ self.assertFalse(node.isEqualToNode(another))
+ # A node with a child and a grandchild is equal to another node with a
+ # child and a grandchild.
+ another.firstChild().appendChild(microdom.Node(object()))
+ self.assertTrue(node.isEqualToNode(another))
+
+ def test_validChildInstance(self):
+ """
+ Children of L{Node} instances must also be L{Node} instances.
+ """
+ node = microdom.Node()
+ child = microdom.Node()
+ # Node.appendChild() only accepts Node instances.
+ node.appendChild(child)
+ self.assertRaises(TypeError, node.appendChild, None)
+ # Node.insertBefore() only accepts Node instances.
+ self.assertRaises(TypeError, node.insertBefore, child, None)
+ self.assertRaises(TypeError, node.insertBefore, None, child)
+ self.assertRaises(TypeError, node.insertBefore, None, None)
+ # Node.removeChild() only accepts Node instances.
+ node.removeChild(child)
+ self.assertRaises(TypeError, node.removeChild, None)
+ # Node.replaceChild() only accepts Node instances.
+ self.assertRaises(TypeError, node.replaceChild, child, None)
+ self.assertRaises(TypeError, node.replaceChild, None, child)
+ self.assertRaises(TypeError, node.replaceChild, None, None)
+
+
+class DocumentTests(TestCase):
+ """
+ Tests for L{Document}.
+ """
+ doctype = 'foo PUBLIC "baz" "http://www.example.com/example.dtd"'
+
+ def test_isEqualToNode(self):
+ """
+ L{Document.isEqualToNode} returns C{True} if and only if passed a
+ L{Document} with the same C{doctype} and C{documentElement}.
+ """
+ # A document is equal to itself
+ document = microdom.Document()
+ self.assertTrue(document.isEqualToNode(document))
+ # A document without a doctype or documentElement is equal to another
+ # document without a doctype or documentElement.
+ another = microdom.Document()
+ self.assertTrue(document.isEqualToNode(another))
+ # A document with a doctype is not equal to a document without a
+ # doctype.
+ document.doctype = self.doctype
+ self.assertFalse(document.isEqualToNode(another))
+ # Two documents with the same doctype are equal
+ another.doctype = self.doctype
+ self.assertTrue(document.isEqualToNode(another))
+ # A document with a documentElement is not equal to a document without
+ # a documentElement
+ document.appendChild(microdom.Node(object()))
+ self.assertFalse(document.isEqualToNode(another))
+ # Two documents with equal documentElements are equal.
+ another.appendChild(microdom.Node(object()))
+ self.assertTrue(document.isEqualToNode(another))
+ # Two documents with documentElements which are not equal are not
+ # equal.
+ document.documentElement.appendChild(microdom.Node(object()))
+ self.assertFalse(document.isEqualToNode(another))
+
+
+ def test_childRestriction(self):
+ """
+ L{Document.appendChild} raises L{ValueError} if the document already
+ has a child.
+ """
+ document = microdom.Document()
+ child = microdom.Node()
+ another = microdom.Node()
+ document.appendChild(child)
+ self.assertRaises(ValueError, document.appendChild, another)
+
+
+
+class EntityReferenceTests(TestCase):
+ """
+ Tests for L{EntityReference}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{EntityReference.isEqualToNode} returns C{True} if and only if passed
+ a L{EntityReference} with the same C{eref}.
+ """
+ self.assertTrue(
+ microdom.EntityReference('quot').isEqualToNode(
+ microdom.EntityReference('quot')))
+ self.assertFalse(
+ microdom.EntityReference('quot').isEqualToNode(
+ microdom.EntityReference('apos')))
+
+
+
+class CharacterDataTests(TestCase):
+ """
+ Tests for L{CharacterData}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{CharacterData.isEqualToNode} returns C{True} if and only if passed a
+ L{CharacterData} with the same value.
+ """
+ self.assertTrue(
+ microdom.CharacterData('foo').isEqualToNode(
+ microdom.CharacterData('foo')))
+ self.assertFalse(
+ microdom.CharacterData('foo').isEqualToNode(
+ microdom.CharacterData('bar')))
+
+
+
+class CommentTests(TestCase):
+ """
+ Tests for L{Comment}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{Comment.isEqualToNode} returns C{True} if and only if passed a
+ L{Comment} with the same value.
+ """
+ self.assertTrue(
+ microdom.Comment('foo').isEqualToNode(
+ microdom.Comment('foo')))
+ self.assertFalse(
+ microdom.Comment('foo').isEqualToNode(
+ microdom.Comment('bar')))
+
+
+
+class TextTests(TestCase):
+ """
+ Tests for L{Text}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{Text.isEqualToNode} returns C{True} if and only if passed a L{Text}
+ which represents the same data.
+ """
+ self.assertTrue(
+ microdom.Text('foo', raw=True).isEqualToNode(
+ microdom.Text('foo', raw=True)))
+ self.assertFalse(
+ microdom.Text('foo', raw=True).isEqualToNode(
+ microdom.Text('foo', raw=False)))
+ self.assertFalse(
+ microdom.Text('foo', raw=True).isEqualToNode(
+ microdom.Text('bar', raw=True)))
+
+
+
+class CDATASectionTests(TestCase):
+ """
+ Tests for L{CDATASection}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{CDATASection.isEqualToNode} returns C{True} if and only if passed a
+ L{CDATASection} which represents the same data.
+ """
+ self.assertTrue(
+ microdom.CDATASection('foo').isEqualToNode(
+ microdom.CDATASection('foo')))
+ self.assertFalse(
+ microdom.CDATASection('foo').isEqualToNode(
+ microdom.CDATASection('bar')))
+
+
+
+class ElementTests(TestCase):
+ """
+ Tests for L{Element}.
+ """
+ def test_isEqualToNode(self):
+ """
+ L{Element.isEqualToNode} returns C{True} if and only if passed a
+ L{Element} with the same C{nodeName}, C{namespace}, C{childNodes}, and
+ C{attributes}.
+ """
+ self.assertTrue(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='bar').isEqualToNode(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='bar')))
+
+ # Elements with different nodeName values do not compare equal.
+ self.assertFalse(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='bar').isEqualToNode(
+ microdom.Element(
+ 'bar', {'a': 'b'}, object(), namespace='bar')))
+
+ # Elements with different namespaces do not compare equal.
+ self.assertFalse(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='bar').isEqualToNode(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='baz')))
+
+ # Elements with different childNodes do not compare equal.
+ one = microdom.Element('foo', {'a': 'b'}, object(), namespace='bar')
+ two = microdom.Element('foo', {'a': 'b'}, object(), namespace='bar')
+ two.appendChild(microdom.Node(object()))
+ self.assertFalse(one.isEqualToNode(two))
+
+ # Elements with different attributes do not compare equal.
+ self.assertFalse(
+ microdom.Element(
+ 'foo', {'a': 'b'}, object(), namespace='bar').isEqualToNode(
+ microdom.Element(
+ 'foo', {'a': 'c'}, object(), namespace='bar')))
diff --git a/twisted/web/test/test_xmlrpc.py b/twisted/web/test/test_xmlrpc.py
new file mode 100644
index 0000000..f49ff02
--- /dev/null
+++ b/twisted/web/test/test_xmlrpc.py
@@ -0,0 +1,849 @@
+# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for XML-RPC support in L{twisted.web.xmlrpc}.
+"""
+
+import datetime
+import xmlrpclib
+from StringIO import StringIO
+
+from twisted.trial import unittest
+from twisted.web import xmlrpc
+from twisted.web.xmlrpc import (
+ XMLRPC, payloadTemplate, addIntrospection, _QueryFactory, Proxy,
+ withRequest)
+from twisted.web import server, static, client, error, http
+from twisted.internet import reactor, defer
+from twisted.internet.error import ConnectionDone
+from twisted.python import failure
+from twisted.test.proto_helpers import MemoryReactor
+from twisted.web.test.test_web import DummyRequest
+try:
+ import twisted.internet.ssl
+except ImportError:
+ sslSkip = "OpenSSL not present"
+else:
+ sslSkip = None
+
+
+class AsyncXMLRPCTests(unittest.TestCase):
+ """
+ Tests for L{XMLRPC}'s support of Deferreds.
+ """
+ def setUp(self):
+ self.request = DummyRequest([''])
+ self.request.method = 'POST'
+ self.request.content = StringIO(
+ payloadTemplate % ('async', xmlrpclib.dumps(())))
+
+ result = self.result = defer.Deferred()
+ class AsyncResource(XMLRPC):
+ def xmlrpc_async(self):
+ return result
+
+ self.resource = AsyncResource()
+
+
+ def test_deferredResponse(self):
+ """
+ If an L{XMLRPC} C{xmlrpc_*} method returns a L{defer.Deferred}, the
+ response to the request is the result of that L{defer.Deferred}.
+ """
+ self.resource.render(self.request)
+ self.assertEqual(self.request.written, [])
+
+ self.result.callback("result")
+
+ resp = xmlrpclib.loads("".join(self.request.written))
+ self.assertEqual(resp, (('result',), None))
+ self.assertEqual(self.request.finished, 1)
+
+
+ def test_interruptedDeferredResponse(self):
+ """
+ While waiting for the L{Deferred} returned by an L{XMLRPC} C{xmlrpc_*}
+ method to fire, the connection the request was issued over may close.
+ If this happens, neither C{write} nor C{finish} is called on the
+ request.
+ """
+ self.resource.render(self.request)
+ self.request.processingFailed(
+ failure.Failure(ConnectionDone("Simulated")))
+ self.result.callback("result")
+ self.assertEqual(self.request.written, [])
+ self.assertEqual(self.request.finished, 0)
+
+
+
+class TestRuntimeError(RuntimeError):
+ pass
+
+
+
+class TestValueError(ValueError):
+ pass
+
+
+
+class Test(XMLRPC):
+
+ # If you add xmlrpc_ methods to this class, go change test_listMethods
+ # below.
+
+ FAILURE = 666
+ NOT_FOUND = 23
+ SESSION_EXPIRED = 42
+
+ def xmlrpc_echo(self, arg):
+ return arg
+
+ # the doc string is part of the test
+ def xmlrpc_add(self, a, b):
+ """
+ This function add two numbers.
+ """
+ return a + b
+
+ xmlrpc_add.signature = [['int', 'int', 'int'],
+ ['double', 'double', 'double']]
+
+ # the doc string is part of the test
+ def xmlrpc_pair(self, string, num):
+ """
+ This function puts the two arguments in an array.
+ """
+ return [string, num]
+
+ xmlrpc_pair.signature = [['array', 'string', 'int']]
+
+ # the doc string is part of the test
+ def xmlrpc_defer(self, x):
+ """Help for defer."""
+ return defer.succeed(x)
+
+ def xmlrpc_deferFail(self):
+ return defer.fail(TestValueError())
+
+ # don't add a doc string, it's part of the test
+ def xmlrpc_fail(self):
+ raise TestRuntimeError
+
+ def xmlrpc_fault(self):
+ return xmlrpc.Fault(12, "hello")
+
+ def xmlrpc_deferFault(self):
+ return defer.fail(xmlrpc.Fault(17, "hi"))
+
+ def xmlrpc_complex(self):
+ return {"a": ["b", "c", 12, []], "D": "foo"}
+
+ def xmlrpc_dict(self, map, key):
+ return map[key]
+ xmlrpc_dict.help = 'Help for dict.'
+
+ @withRequest
+ def xmlrpc_withRequest(self, request, other):
+ """
+ A method decorated with L{withRequest} which can be called by
+ a test to verify that the request object really is passed as
+ an argument.
+ """
+ return (
+ # as a proof that request is a request
+ request.method +
+ # plus proof other arguments are still passed along
+ ' ' + other)
+
+
+ def lookupProcedure(self, procedurePath):
+ try:
+ return XMLRPC.lookupProcedure(self, procedurePath)
+ except xmlrpc.NoSuchFunction:
+ if procedurePath.startswith("SESSION"):
+ raise xmlrpc.Fault(self.SESSION_EXPIRED,
+ "Session non-existant/expired.")
+ else:
+ raise
+
+
+
+class TestLookupProcedure(XMLRPC):
+ """
+ This is a resource which customizes procedure lookup to be used by the tests
+ of support for this customization.
+ """
+ def echo(self, x):
+ return x
+
+
+ def lookupProcedure(self, procedureName):
+ """
+ Lookup a procedure from a fixed set of choices, either I{echo} or
+ I{system.listeMethods}.
+ """
+ if procedureName == 'echo':
+ return self.echo
+ raise xmlrpc.NoSuchFunction(
+ self.NOT_FOUND, 'procedure %s not found' % (procedureName,))
+
+
+
+class TestListProcedures(XMLRPC):
+ """
+ This is a resource which customizes procedure enumeration to be used by the
+ tests of support for this customization.
+ """
+ def listProcedures(self):
+ """
+ Return a list of a single method this resource will claim to support.
+ """
+ return ['foo']
+
+
+
+class TestAuthHeader(Test):
+ """
+ This is used to get the header info so that we can test
+ authentication.
+ """
+ def __init__(self):
+ Test.__init__(self)
+ self.request = None
+
+ def render(self, request):
+ self.request = request
+ return Test.render(self, request)
+
+ def xmlrpc_authinfo(self):
+ return self.request.getUser(), self.request.getPassword()
+
+
+class TestQueryProtocol(xmlrpc.QueryProtocol):
+ """
+ QueryProtocol for tests that saves headers received inside the factory.
+ """
+
+ def connectionMade(self):
+ self.factory.transport = self.transport
+ xmlrpc.QueryProtocol.connectionMade(self)
+
+ def handleHeader(self, key, val):
+ self.factory.headers[key.lower()] = val
+
+
+class TestQueryFactory(xmlrpc._QueryFactory):
+ """
+ QueryFactory using L{TestQueryProtocol} for saving headers.
+ """
+ protocol = TestQueryProtocol
+
+ def __init__(self, *args, **kwargs):
+ self.headers = {}
+ xmlrpc._QueryFactory.__init__(self, *args, **kwargs)
+
+
+class TestQueryFactoryCancel(xmlrpc._QueryFactory):
+ """
+ QueryFactory that saves a reference to the
+ L{twisted.internet.interfaces.IConnector} to test connection lost.
+ """
+
+ def startedConnecting(self, connector):
+ self.connector = connector
+
+
+class XMLRPCTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.p = reactor.listenTCP(0, server.Site(Test()),
+ interface="127.0.0.1")
+ self.port = self.p.getHost().port
+ self.factories = []
+
+ def tearDown(self):
+ self.factories = []
+ return self.p.stopListening()
+
+ def queryFactory(self, *args, **kwargs):
+ """
+ Specific queryFactory for proxy that uses our custom
+ L{TestQueryFactory}, and save factories.
+ """
+ factory = TestQueryFactory(*args, **kwargs)
+ self.factories.append(factory)
+ return factory
+
+ def proxy(self, factory=None):
+ """
+ Return a new xmlrpc.Proxy for the test site created in
+ setUp(), using the given factory as the queryFactory, or
+ self.queryFactory if no factory is provided.
+ """
+ p = xmlrpc.Proxy("http://127.0.0.1:%d/" % self.port)
+ if factory is None:
+ p.queryFactory = self.queryFactory
+ else:
+ p.queryFactory = factory
+ return p
+
+ def test_results(self):
+ inputOutput = [
+ ("add", (2, 3), 5),
+ ("defer", ("a",), "a"),
+ ("dict", ({"a": 1}, "a"), 1),
+ ("pair", ("a", 1), ["a", 1]),
+ ("complex", (), {"a": ["b", "c", 12, []], "D": "foo"})]
+
+ dl = []
+ for meth, args, outp in inputOutput:
+ d = self.proxy().callRemote(meth, *args)
+ d.addCallback(self.assertEqual, outp)
+ dl.append(d)
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def test_errors(self):
+ """
+ Verify that for each way a method exposed via XML-RPC can fail, the
+ correct 'Content-type' header is set in the response and that the
+ client-side Deferred is errbacked with an appropriate C{Fault}
+ instance.
+ """
+ dl = []
+ for code, methodName in [(666, "fail"), (666, "deferFail"),
+ (12, "fault"), (23, "noSuchMethod"),
+ (17, "deferFault"), (42, "SESSION_TEST")]:
+ d = self.proxy().callRemote(methodName)
+ d = self.assertFailure(d, xmlrpc.Fault)
+ d.addCallback(lambda exc, code=code:
+ self.assertEqual(exc.faultCode, code))
+ dl.append(d)
+ d = defer.DeferredList(dl, fireOnOneErrback=True)
+ def cb(ign):
+ for factory in self.factories:
+ self.assertEqual(factory.headers['content-type'],
+ 'text/xml')
+ self.flushLoggedErrors(TestRuntimeError, TestValueError)
+ d.addCallback(cb)
+ return d
+
+
+ def test_cancel(self):
+ """
+ A deferred from the Proxy can be cancelled, disconnecting
+ the L{twisted.internet.interfaces.IConnector}.
+ """
+ def factory(*args, **kw):
+ factory.f = TestQueryFactoryCancel(*args, **kw)
+ return factory.f
+ d = self.proxy(factory).callRemote('add', 2, 3)
+ self.assertNotEquals(factory.f.connector.state, "disconnected")
+ d.cancel()
+ self.assertEqual(factory.f.connector.state, "disconnected")
+ d = self.assertFailure(d, defer.CancelledError)
+ return d
+
+
+ def test_errorGet(self):
+ """
+ A classic GET on the xml server should return a NOT_ALLOWED.
+ """
+ d = client.getPage("http://127.0.0.1:%d/" % (self.port,))
+ d = self.assertFailure(d, error.Error)
+ d.addCallback(
+ lambda exc: self.assertEqual(int(exc.args[0]), http.NOT_ALLOWED))
+ return d
+
+ def test_errorXMLContent(self):
+ """
+ Test that an invalid XML input returns an L{xmlrpc.Fault}.
+ """
+ d = client.getPage("http://127.0.0.1:%d/" % (self.port,),
+ method="POST", postdata="foo")
+ def cb(result):
+ self.assertRaises(xmlrpc.Fault, xmlrpclib.loads, result)
+ d.addCallback(cb)
+ return d
+
+
+ def test_datetimeRoundtrip(self):
+ """
+ If an L{xmlrpclib.DateTime} is passed as an argument to an XML-RPC
+ call and then returned by the server unmodified, the result should
+ be equal to the original object.
+ """
+ when = xmlrpclib.DateTime()
+ d = self.proxy().callRemote("echo", when)
+ d.addCallback(self.assertEqual, when)
+ return d
+
+
+ def test_doubleEncodingError(self):
+ """
+ If it is not possible to encode a response to the request (for example,
+ because L{xmlrpclib.dumps} raises an exception when encoding a
+ L{Fault}) the exception which prevents the response from being
+ generated is logged and the request object is finished anyway.
+ """
+ d = self.proxy().callRemote("echo", "")
+
+ # *Now* break xmlrpclib.dumps. Hopefully the client already used it.
+ def fakeDumps(*args, **kwargs):
+ raise RuntimeError("Cannot encode anything at all!")
+ self.patch(xmlrpclib, 'dumps', fakeDumps)
+
+ # It doesn't matter how it fails, so long as it does. Also, it happens
+ # to fail with an implementation detail exception right now, not
+ # something suitable as part of a public interface.
+ d = self.assertFailure(d, Exception)
+
+ def cbFailed(ignored):
+ # The fakeDumps exception should have been logged.
+ self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
+ d.addCallback(cbFailed)
+ return d
+
+
+ def test_closeConnectionAfterRequest(self):
+ """
+ The connection to the web server is closed when the request is done.
+ """
+ d = self.proxy().callRemote('echo', '')
+ def responseDone(ignored):
+ [factory] = self.factories
+ self.assertFalse(factory.transport.connected)
+ self.assertTrue(factory.transport.disconnected)
+ return d.addCallback(responseDone)
+
+
+ def test_tcpTimeout(self):
+ """
+ For I{HTTP} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
+ received for the C{connectTimeout} parameter as the C{timeout} argument
+ to the underlying connectTCP call.
+ """
+ reactor = MemoryReactor()
+ proxy = xmlrpc.Proxy("http://127.0.0.1:69", connectTimeout=2.0,
+ reactor=reactor)
+ proxy.callRemote("someMethod")
+ self.assertEqual(reactor.tcpClients[0][3], 2.0)
+
+
+ def test_sslTimeout(self):
+ """
+ For I{HTTPS} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
+ received for the C{connectTimeout} parameter as the C{timeout} argument
+ to the underlying connectSSL call.
+ """
+ reactor = MemoryReactor()
+ proxy = xmlrpc.Proxy("https://127.0.0.1:69", connectTimeout=3.0,
+ reactor=reactor)
+ proxy.callRemote("someMethod")
+ self.assertEqual(reactor.sslClients[0][4], 3.0)
+ test_sslTimeout.skip = sslSkip
+
+
+
+class XMLRPCTestCase2(XMLRPCTestCase):
+ """
+ Test with proxy that doesn't add a slash.
+ """
+
+ def proxy(self, factory=None):
+ p = xmlrpc.Proxy("http://127.0.0.1:%d" % self.port)
+ if factory is None:
+ p.queryFactory = self.queryFactory
+ else:
+ p.queryFactory = factory
+ return p
+
+
+
+class XMLRPCTestPublicLookupProcedure(unittest.TestCase):
+ """
+ Tests for L{XMLRPC}'s support of subclasses which override
+ C{lookupProcedure} and C{listProcedures}.
+ """
+
+ def createServer(self, resource):
+ self.p = reactor.listenTCP(
+ 0, server.Site(resource), interface="127.0.0.1")
+ self.addCleanup(self.p.stopListening)
+ self.port = self.p.getHost().port
+ self.proxy = xmlrpc.Proxy('http://127.0.0.1:%d' % self.port)
+
+
+ def test_lookupProcedure(self):
+ """
+ A subclass of L{XMLRPC} can override C{lookupProcedure} to find
+ procedures that are not defined using a C{xmlrpc_}-prefixed method name.
+ """
+ self.createServer(TestLookupProcedure())
+ what = "hello"
+ d = self.proxy.callRemote("echo", what)
+ d.addCallback(self.assertEqual, what)
+ return d
+
+
+ def test_errors(self):
+ """
+ A subclass of L{XMLRPC} can override C{lookupProcedure} to raise
+ L{NoSuchFunction} to indicate that a requested method is not available
+ to be called, signalling a fault to the XML-RPC client.
+ """
+ self.createServer(TestLookupProcedure())
+ d = self.proxy.callRemote("xxxx", "hello")
+ d = self.assertFailure(d, xmlrpc.Fault)
+ return d
+
+
+ def test_listMethods(self):
+ """
+ A subclass of L{XMLRPC} can override C{listProcedures} to define
+ Overriding listProcedures should prevent introspection from being
+ broken.
+ """
+ resource = TestListProcedures()
+ addIntrospection(resource)
+ self.createServer(resource)
+ d = self.proxy.callRemote("system.listMethods")
+ def listed(procedures):
+ # The list will also include other introspection procedures added by
+ # addIntrospection. We just want to see "foo" from our customized
+ # listProcedures.
+ self.assertIn('foo', procedures)
+ d.addCallback(listed)
+ return d
+
+
+
+class SerializationConfigMixin:
+ """
+ Mixin which defines a couple tests which should pass when a particular flag
+ is passed to L{XMLRPC}.
+
+ These are not meant to be exhaustive serialization tests, since L{xmlrpclib}
+ does all of the actual serialization work. They are just meant to exercise
+ a few codepaths to make sure we are calling into xmlrpclib correctly.
+
+ @ivar flagName: A C{str} giving the name of the flag which must be passed to
+ L{XMLRPC} to allow the tests to pass. Subclasses should set this.
+
+ @ivar value: A value which the specified flag will allow the serialization
+ of. Subclasses should set this.
+ """
+ def setUp(self):
+ """
+ Create a new XML-RPC server with C{allowNone} set to C{True}.
+ """
+ kwargs = {self.flagName: True}
+ self.p = reactor.listenTCP(
+ 0, server.Site(Test(**kwargs)), interface="127.0.0.1")
+ self.addCleanup(self.p.stopListening)
+ self.port = self.p.getHost().port
+ self.proxy = xmlrpc.Proxy(
+ "http://127.0.0.1:%d/" % (self.port,), **kwargs)
+
+
+ def test_roundtripValue(self):
+ """
+ C{self.value} can be round-tripped over an XMLRPC method call/response.
+ """
+ d = self.proxy.callRemote('defer', self.value)
+ d.addCallback(self.assertEqual, self.value)
+ return d
+
+
+ def test_roundtripNestedValue(self):
+ """
+ A C{dict} which contains C{self.value} can be round-tripped over an
+ XMLRPC method call/response.
+ """
+ d = self.proxy.callRemote('defer', {'a': self.value})
+ d.addCallback(self.assertEqual, {'a': self.value})
+ return d
+
+
+
+class XMLRPCAllowNoneTestCase(SerializationConfigMixin, unittest.TestCase):
+ """
+ Tests for passing C{None} when the C{allowNone} flag is set.
+ """
+ flagName = "allowNone"
+ value = None
+
+
+try:
+ xmlrpclib.loads(xmlrpclib.dumps(({}, {})), use_datetime=True)
+except TypeError:
+ _datetimeSupported = False
+else:
+ _datetimeSupported = True
+
+
+
+class XMLRPCUseDateTimeTestCase(SerializationConfigMixin, unittest.TestCase):
+ """
+ Tests for passing a C{datetime.datetime} instance when the C{useDateTime}
+ flag is set.
+ """
+ flagName = "useDateTime"
+ value = datetime.datetime(2000, 12, 28, 3, 45, 59)
+
+ if not _datetimeSupported:
+ skip = (
+ "Available version of xmlrpclib does not support datetime "
+ "objects.")
+
+
+
+class XMLRPCDisableUseDateTimeTestCase(unittest.TestCase):
+ """
+ Tests for the C{useDateTime} flag on Python 2.4.
+ """
+ if _datetimeSupported:
+ skip = (
+ "Available version of xmlrpclib supports datetime objects.")
+
+ def test_cannotInitializeWithDateTime(self):
+ """
+ L{XMLRPC} raises L{RuntimeError} if passed C{True} for C{useDateTime}.
+ """
+ self.assertRaises(RuntimeError, XMLRPC, useDateTime=True)
+ self.assertRaises(
+ RuntimeError, Proxy, "http://localhost/", useDateTime=True)
+
+
+ def test_cannotSetDateTime(self):
+ """
+ Setting L{XMLRPC.useDateTime} to C{True} after initialization raises
+ L{RuntimeError}.
+ """
+ xmlrpc = XMLRPC(useDateTime=False)
+ self.assertRaises(RuntimeError, setattr, xmlrpc, "useDateTime", True)
+ proxy = Proxy("http://localhost/", useDateTime=False)
+ self.assertRaises(RuntimeError, setattr, proxy, "useDateTime", True)
+
+
+
+class XMLRPCTestAuthenticated(XMLRPCTestCase):
+ """
+ Test with authenticated proxy. We run this with the same inout/ouput as
+ above.
+ """
+ user = "username"
+ password = "asecret"
+
+ def setUp(self):
+ self.p = reactor.listenTCP(0, server.Site(TestAuthHeader()),
+ interface="127.0.0.1")
+ self.port = self.p.getHost().port
+ self.factories = []
+
+
+ def test_authInfoInURL(self):
+ p = xmlrpc.Proxy("http://%s:%s@127.0.0.1:%d/" % (
+ self.user, self.password, self.port))
+ d = p.callRemote("authinfo")
+ d.addCallback(self.assertEqual, [self.user, self.password])
+ return d
+
+
+ def test_explicitAuthInfo(self):
+ p = xmlrpc.Proxy("http://127.0.0.1:%d/" % (
+ self.port,), self.user, self.password)
+ d = p.callRemote("authinfo")
+ d.addCallback(self.assertEqual, [self.user, self.password])
+ return d
+
+
+ def test_explicitAuthInfoOverride(self):
+ p = xmlrpc.Proxy("http://wrong:info@127.0.0.1:%d/" % (
+ self.port,), self.user, self.password)
+ d = p.callRemote("authinfo")
+ d.addCallback(self.assertEqual, [self.user, self.password])
+ return d
+
+
+class XMLRPCTestIntrospection(XMLRPCTestCase):
+
+ def setUp(self):
+ xmlrpc = Test()
+ addIntrospection(xmlrpc)
+ self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1")
+ self.port = self.p.getHost().port
+ self.factories = []
+
+ def test_listMethods(self):
+
+ def cbMethods(meths):
+ meths.sort()
+ self.assertEqual(
+ meths,
+ ['add', 'complex', 'defer', 'deferFail',
+ 'deferFault', 'dict', 'echo', 'fail', 'fault',
+ 'pair', 'system.listMethods',
+ 'system.methodHelp',
+ 'system.methodSignature', 'withRequest'])
+
+ d = self.proxy().callRemote("system.listMethods")
+ d.addCallback(cbMethods)
+ return d
+
+ def test_methodHelp(self):
+ inputOutputs = [
+ ("defer", "Help for defer."),
+ ("fail", ""),
+ ("dict", "Help for dict.")]
+
+ dl = []
+ for meth, expected in inputOutputs:
+ d = self.proxy().callRemote("system.methodHelp", meth)
+ d.addCallback(self.assertEqual, expected)
+ dl.append(d)
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+ def test_methodSignature(self):
+ inputOutputs = [
+ ("defer", ""),
+ ("add", [['int', 'int', 'int'],
+ ['double', 'double', 'double']]),
+ ("pair", [['array', 'string', 'int']])]
+
+ dl = []
+ for meth, expected in inputOutputs:
+ d = self.proxy().callRemote("system.methodSignature", meth)
+ d.addCallback(self.assertEqual, expected)
+ dl.append(d)
+ return defer.DeferredList(dl, fireOnOneErrback=True)
+
+
+class XMLRPCClientErrorHandling(unittest.TestCase):
+ """
+ Test error handling on the xmlrpc client.
+ """
+ def setUp(self):
+ self.resource = static.Data(
+ "This text is not a valid XML-RPC response.",
+ "text/plain")
+ self.resource.isLeaf = True
+ self.port = reactor.listenTCP(0, server.Site(self.resource),
+ interface='127.0.0.1')
+
+ def tearDown(self):
+ return self.port.stopListening()
+
+ def test_erroneousResponse(self):
+ """
+ Test that calling the xmlrpc client on a static http server raises
+ an exception.
+ """
+ proxy = xmlrpc.Proxy("http://127.0.0.1:%d/" %
+ (self.port.getHost().port,))
+ return self.assertFailure(proxy.callRemote("someMethod"), Exception)
+
+
+
+class TestQueryFactoryParseResponse(unittest.TestCase):
+ """
+ Test the behaviour of L{_QueryFactory.parseResponse}.
+ """
+
+ def setUp(self):
+ # The _QueryFactory that we are testing. We don't care about any
+ # of the constructor parameters.
+ self.queryFactory = _QueryFactory(
+ path=None, host=None, method='POST', user=None, password=None,
+ allowNone=False, args=())
+ # An XML-RPC response that will parse without raising an error.
+ self.goodContents = xmlrpclib.dumps(('',))
+ # An 'XML-RPC response' that will raise a parsing error.
+ self.badContents = 'invalid xml'
+ # A dummy 'reason' to pass to clientConnectionLost. We don't care
+ # what it is.
+ self.reason = failure.Failure(ConnectionDone())
+
+
+ def test_parseResponseCallbackSafety(self):
+ """
+ We can safely call L{_QueryFactory.clientConnectionLost} as a callback
+ of L{_QueryFactory.parseResponse}.
+ """
+ d = self.queryFactory.deferred
+ # The failure mode is that this callback raises an AlreadyCalled
+ # error. We have to add it now so that it gets called synchronously
+ # and triggers the race condition.
+ d.addCallback(self.queryFactory.clientConnectionLost, self.reason)
+ self.queryFactory.parseResponse(self.goodContents)
+ return d
+
+
+ def test_parseResponseErrbackSafety(self):
+ """
+ We can safely call L{_QueryFactory.clientConnectionLost} as an errback
+ of L{_QueryFactory.parseResponse}.
+ """
+ d = self.queryFactory.deferred
+ # The failure mode is that this callback raises an AlreadyCalled
+ # error. We have to add it now so that it gets called synchronously
+ # and triggers the race condition.
+ d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
+ self.queryFactory.parseResponse(self.badContents)
+ return d
+
+
+ def test_badStatusErrbackSafety(self):
+ """
+ We can safely call L{_QueryFactory.clientConnectionLost} as an errback
+ of L{_QueryFactory.badStatus}.
+ """
+ d = self.queryFactory.deferred
+ # The failure mode is that this callback raises an AlreadyCalled
+ # error. We have to add it now so that it gets called synchronously
+ # and triggers the race condition.
+ d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
+ self.queryFactory.badStatus('status', 'message')
+ return d
+
+ def test_parseResponseWithoutData(self):
+ """
+ Some server can send a response without any data:
+ L{_QueryFactory.parseResponse} should catch the error and call the
+ result errback.
+ """
+ content = """
+<methodResponse>
+ <params>
+ <param>
+ </param>
+ </params>
+</methodResponse>"""
+ d = self.queryFactory.deferred
+ self.queryFactory.parseResponse(content)
+ return self.assertFailure(d, IndexError)
+
+
+
+class XMLRPCTestWithRequest(unittest.TestCase):
+
+ def setUp(self):
+ self.resource = Test()
+
+
+ def test_withRequest(self):
+ """
+ When an XML-RPC method is called and the implementation is
+ decorated with L{withRequest}, the request object is passed as
+ the first argument.
+ """
+ request = DummyRequest('/RPC2')
+ request.method = "POST"
+ request.content = StringIO(xmlrpclib.dumps(("foo",), 'withRequest'))
+ def valid(n, request):
+ data = xmlrpclib.loads(request.written[0])
+ self.assertEqual(data, (('POST foo',), None))
+ d = request.notifyFinish().addCallback(valid, request)
+ self.resource.render_POST(request)
+ return d
diff --git a/twisted/web/topfiles/NEWS b/twisted/web/topfiles/NEWS
new file mode 100644
index 0000000..cfa2a46
--- /dev/null
+++ b/twisted/web/topfiles/NEWS
@@ -0,0 +1,556 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Web 12.1.0 (2012-06-02)
+===============================
+
+Features
+--------
+ - twisted.web.client.Agent and ProxyAgent now support persistent
+ connections. (#3420)
+ - Added twisted.web.template.renderElement, a function which renders
+ an Element to a response. (#5395)
+ - twisted.web.client.HTTPConnectionPool now ensures that failed
+ queries on persistent connections are retried, when possible.
+ (#5479)
+ - twisted.web.template.XMLFile now supports FilePath objects. (#5509)
+ - twisted.web.template.renderElement takes a doctype keyword
+ argument, which will be written as the first line of the response,
+ defaulting to the HTML5 doctype. (#5560)
+
+Bugfixes
+--------
+ - twisted.web.util.formatFailure now quotes all data in its output to
+ avoid it being mistakenly interpreted as markup. (#4896)
+ - twisted.web.distrib now lets distributed servers set the response
+ message. (#5525)
+
+Deprecations and Removals
+-------------------------
+ - PHP3Script and PHPScript were removed from twisted.web.twcgi,
+ deprecated since 10.1. Use twcgi.FilteredScript instead. (#5456)
+ - twisted.web.template.XMLFile's support for file objects and
+ filenames is now deprecated. Use the new support for FilePath
+ objects. (#5509)
+ - twisted.web.server.date_time_string and
+ twisted.web.server.string_date_time are now deprecated in favor of
+ twisted.web.http.datetimeToString and twisted.web.
+ http.stringToDatetime (#5535)
+
+Other
+-----
+ - #4966, #5460, #5490, #5591, #5602, #5609, #5612
+
+
+Twisted Web 12.0.0 (2012-02-10)
+===============================
+
+Features
+--------
+ - twisted.web.util.redirectTo now raises TypeError if the URL passed
+ to it is a unicode string instead of a byte string. (#5236)
+ - The new class twisted.web.template.CharRef provides support for
+ inserting numeric character references in output generated by
+ twisted.web.template. (#5408)
+
+Improved Documentation
+----------------------
+ - The Twisted Web howto now has a section on proxies and reverse
+ proxies. (#399)
+ - The web client howto now covers ContentDecoderAgent and links to an
+ example of its use. (#5415)
+
+Other
+-----
+ - #5404, #5438
+
+
+Twisted Web 11.1.0 (2011-11-15)
+===============================
+
+Features
+--------
+ - twisted.web.client.ProxyAgent is a new HTTP/1.1 web client which
+ adds proxy support. (#1774)
+ - twisted.web.client.Agent now takes optional connectTimeout and
+ bindAddress arguments which are forwarded to the subsequent
+ connectTCP/connectSSL call. (#3450)
+ - The new class twisted.web.client.FileBodyProducer makes it easy to
+ upload data in HTTP requests made using the Agent client APIs.
+ (#4017)
+ - twisted.web.xmlrpc.XMLRPC now allows its lookupProcedure method to
+ be overridden to change how XML-RPC procedures are dispatched.
+ (#4836)
+ - A new HTTP cookie-aware Twisted Web Agent wrapper is included in
+ twisted.web.client.CookieAgent (#4922)
+ - New class twisted.web.template.TagLoader provides an
+ ITemplateLoader implementation which loads already-created
+ twisted.web.iweb.IRenderable providers. (#5040)
+ - The new class twisted.web.client.RedirectAgent adds redirect
+ support to the HTTP 1.1 client stack. (#5157)
+ - twisted.web.template now supports HTML tags from the HTML5
+ standard, including <canvas> and <video>. (#5306)
+
+Bugfixes
+--------
+ - twisted.web.client.getPage and .downloadPage now only fire their
+ result Deferred after the underlying connection they use has been
+ closed. (#3796)
+ - twisted.web.server now omits the default Content-Type header from
+ NOT MODIFIED responses. (#4156)
+ - twisted.web.server now responds correctly to 'Expect: 100-continue'
+ headers, although this is not yet usefully exposed to user code.
+ (#4673)
+ - twisted.web.client.Agent no longer raises an exception if a server
+ responds and closes the connection before the request has been
+ fully transmitted. (#5013)
+ - twisted.web.http_headers.Headers now correctly capitalizes the
+ header names Content-MD5, DNT, ETag, P3P, TE, and X-XSS-Protection.
+ (#5054)
+ - twisted.web.template now escapes more inputs to comments which
+ require escaping in the output. (#5275)
+
+Improved Documentation
+----------------------
+ - The twisted.web.template howto now documents the common idiom of
+ yielding tag clones from a renderer. (#5286)
+ - CookieAgent is now documented in the twisted.web.client how-to.
+ (#5110)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.google is now deprecated. (#5209)
+
+Other
+-----
+ - #4951, #5057, #5175, #5288, #5316
+
+
+Twisted Web 11.0.0 (2011-04-01)
+===============================
+
+Features
+--------
+ - twisted.web._newclient.HTTPParser (and therefore Agent) now handles
+ HTTP headers delimited by bare LF newlines. (#3833)
+ - twisted.web.client.downloadPage now accepts the `afterFoundGet`
+ parameter, with the same meaning as the `getPage` parameter of the
+ same name. (#4364)
+ - twisted.web.xmlrpc.Proxy constructor now takes additional 'timeout'
+ and 'reactor' arguments. The 'timeout' argument defaults to 30
+ seconds. (#4741)
+ - Twisted Web now has a templating system, twisted.web.template,
+ which is a direct, simplified derivative of Divmod Nevow. (#4939)
+
+Bugfixes
+--------
+ - HTTPPageGetter now adds the port to the host header if it is not
+ the default for that scheme. (#3857)
+ - twisted.web.http.Request.write now raises an exception if it is
+ called after response generation has already finished. (#4317)
+ - twisted.web.client.HTTPPageGetter and twisted.web.client.getPage
+ now no longer make two requests when using afterFoundGet. (#4760)
+ - twisted.web.twcgi no longer adds an extra "content-type" header to
+ CGI responses. (#4786)
+ - twisted.web will now properly specify an encoding (UTF-8) on error,
+ redirect, and directory listing pages, so that IE7 and previous
+ will not improperly guess the 'utf7' encoding in these cases.
+ Please note that Twisted still sets a *default* content-type of
+ 'text/html', and you shouldn't rely on that: you should set the
+ encoding appropriately in your application. (#4900)
+ - twisted.web.http.Request.setHost now sets the port in the host
+ header if it is not the default. (#4918)
+ - default NOT_IMPLEMENTED and NOT_ALLOWED pages now quote the request
+ method and URI respectively, to protect against browsers which
+ don't quote those values for us. (#4978)
+
+Improved Documentation
+----------------------
+ - The XML-RPC howto now includes an example demonstrating how to
+ access the HTTP request object in a server-side XML-RPC method.
+ (#4732)
+ - The Twisted Web client howto now uses the correct, public name for
+ twisted.web.client.Response. (#4769)
+ - Some broken links were fixed, descriptions were updated, and new
+ API links were added in the Resource Templating documentation
+ (resource-templates.xhtml) (#4968)
+
+Other
+-----
+ - #2271, #2386, #4162, #4733, #4855, #4911, #4973
+
+
+Twisted Web 10.2.0 (2010-11-29)
+===============================
+
+Features
+--------
+ - twisted.web.xmlrpc.XMLRPC.xmlrpc_* methods can now be decorated
+ using withRequest to cause them to be passed the HTTP request
+ object. (#3073)
+
+Bugfixes
+--------
+ - twisted.web.xmlrpc.QueryProtocol.handleResponse now disconnects
+ from the server, meaning that Twisted XML-RPC clients disconnect
+ from the server as soon as they receive a response, rather than
+ relying on the server to disconnect. (#2518)
+ - twisted.web.twcgi now generates responses containing all
+ occurrences of duplicate headers produced by CGI scripts, not just
+ the last value. (#4742)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.trp, which has been deprecated since Twisted 9.0, was
+ removed. (#4299)
+
+Other
+-----
+ - #4576, #4577, #4709, #4723
+
+
+Twisted Web 10.1.0 (2010-06-27)
+===============================
+
+Features
+--------
+ - twisted.web.xmlrpc.XMLRPC and twisted.web.xmlrpc.Proxy now expose
+ xmlrpclib's support of datetime.datetime objects if useDateTime is
+ set to True. (#3219)
+ - HTTP11ClientProtocol now has an abort() method for cancelling an
+ outstanding request by closing the connection before receiving the
+ entire response. (#3811)
+ - twisted.web.http_headers.Headers initializer now rejects
+ incorrectly structured dictionaries. (#4022)
+ - twisted.web.client.Agent now supports HTTPS URLs. (#4023)
+ - twisted.web.xmlrpc.Proxy.callRemote now returns a Deferred which
+ can be cancelled to abort the attempted XML-RPC call. (#4377)
+
+Bugfixes
+--------
+ - twisted.web.guard now logs out avatars even if a request completes
+ with an error. (#4411)
+ - twisted.web.xmlrpc.XMLRPC will now no longer trigger a RuntimeError
+ by trying to write responses to closed connections. (#4423)
+
+Improved Documentation
+----------------------
+ - Fix broken links to deliverBody and iweb.UNKNOWN_LENGTH in
+ doc/web/howto/client.xhtml. (#4507)
+
+Deprecations and Removals
+-------------------------
+ - twisted.web.twcgi.PHP3Script and twisted.web.twcgi.PHPScript are
+ now deprecated. (#516)
+
+Other
+-----
+ - #4403, #4452
+
+
+Twisted Web 10.0.0 (2010-03-01)
+===============================
+
+Features
+--------
+ - Twisted Web in 60 Seconds, a series of short tutorials with self-
+ contained examples on a range of common web topics, is now a part
+ of the Twisted Web howto documentation. (#4192)
+
+Bugfixes
+--------
+ - Data and File from twisted.web.static and
+ twisted.web.distrib.UserDirectory will now only generate a 200
+ response for GET or HEAD requests.
+ twisted.web.client.HTTPPageGetter will no longer ignore the case of
+ a request method when considering whether to apply special HEAD
+ processing to a response. (#446)
+
+ - twisted.web.http.HTTPClient now supports multi-line headers.
+ (#2062)
+
+ - Resources served via twisted.web.distrib will no longer encounter a
+ Banana error when writing more than 640kB at once to the request
+ object. (#3212)
+
+ - The Error, PageRedirect, and InfiniteRedirection exception in
+ twisted.web now initialize an empty message parameter by mapping
+ the HTTP status code parameter to a descriptive string. Previously
+ the lookup would always fail, leaving message empty. (#3806)
+
+ - The 'wsgi.input' WSGI environment object now supports -1 and None
+ as arguments to the read and readlines methods. (#4114)
+
+ - twisted.web.wsgi doesn't unquote QUERY_STRING anymore, thus
+ complying with the WSGI reference implementation. (#4143)
+
+ - The HTTP proxy will no longer pass on keep-alive request headers
+ from the client, preventing pages from loading then "hanging"
+ (leaving the connection open with no hope of termination). (#4179)
+
+Deprecations and Removals
+-------------------------
+ - Remove '--static' option from twistd web, that served as an alias
+ for the '--path' option. (#3907)
+
+Other
+-----
+ - #3784, #4216, #4242
+
+
+Twisted Web 9.0.0 (2009-11-24)
+==============================
+
+Features
+--------
+ - There is now an iweb.IRequest interface which specifies the interface that
+ request objects provide (#3416)
+ - downloadPage now supports the same cookie, redirect, and timeout features
+ that getPage supports (#2971)
+ - A chapter about WSGI has been added to the twisted.web documentation (#3510)
+ - The HTTP auth support in the web server now allows anonymous sessions by
+ logging in with ANONYMOUS credentials when no Authorization header is
+ provided in a request (#3924, #3936)
+ - HTTPClientFactory now accepts a parameter to enable a common deviation from
+ the HTTP 1.1 standard by responding to redirects in a POSTed request with a
+ GET instead of another POST (#3624)
+ - A new basic HTTP/1.1 client API is included in twisted.web.client.Agent
+ (#886, #3987)
+
+Fixes
+-----
+ - Requests for "insecure" children of a static.File (such as paths containing
+ encoded directory separators) will now result in a 404 instead of a 500
+ (#3549, #3469)
+ - When specifying a followRedirect argument to the getPage function, the state
+ of redirect-following for other getPage calls should now be unaffected. It
+ was previously overwriting a class attribute which would affect outstanding
+ getPage calls (#3192)
+ - Downloading an URL of the form "http://example.com:/" will now work,
+ ignoring the extraneous colon (#2402)
+ - microdom's appendChild method will no longer issue a spurious warning, and
+ microdom's methods in general should now issue more meaningful exceptions
+ when invalid parameters are passed (#3421)
+ - WSGI applications will no longer have spurious Content-Type headers added to
+ their responses by the twisted.web server. In addition, WSGI applications
+ will no longer be able to specify the server-restricted headers Server and
+ Date (#3569)
+ - http_headers.Headers now normalizes the case of raw headers passed directly
+ to it in the same way that it normalizes the headers passed to setRawHeaders
+ (#3557)
+ - The distrib module no longer relies on the deprecated woven package (#3559)
+ - twisted.web.domhelpers now works with both microdom and minidom (#3600)
+ - twisted.web servers will now ignore invalid If-Modified-Since headers instead
+ of returning a 500 error (#3601)
+ - Certain request-bound memory and file resources are cleaned up slightly
+ sooner by the request when the connection is lost (#1621, #3176)
+ - xmlrpclib.DateTime objects should now correctly round-trip over twisted.web's
+ XMLRPC support in all supported versions of Python, and errors during error
+ serialization will no longer hang a twisted.web XMLRPC response (#2446)
+ - request.content should now always be seeked to the beginning when
+ request.process is called, so application code should never need to seek
+ back manually (#3585)
+ - Fetching a child of static.File with a double-slash in the URL (such as
+ "example//foo.html") should now return a 404 instead of a traceback and
+ 500 error (#3631)
+ - downloadPage will now fire a Failure on its returned Deferred instead of
+ indicating success when the connection is prematurely lost (#3645)
+ - static.File will now provide a 404 instead of a 500 error when it was
+ constructed with a non-existent file (#3634)
+ - microdom should now serialize namespaces correctly (#3672)
+ - The HTTP Auth support resource wrapper should no longer corrupt requests and
+ cause them to skip a segment in the request path (#3679)
+ - The twisted.web WSGI support should now include leading slashes in PATH_INFO,
+ and SCRIPT_NAME will be empty if the application is at the root of the
+ resource tree. This means that WSGI applications should no longer generate
+ URLs with double-slashes in them even if they naively concatenate the values
+ (#3721)
+ - WSGI applications should now receive the requesting client's IP in the
+ REMOTE_ADDR environment variable (#3730)
+ - The distrib module should work again. It was unfortunately broken with the
+ refactoring of twisted.web's header support (#3697)
+ - static.File now supports multiple ranges specified in the Range header
+ (#3574)
+ - static.File should now generate a correct Content-Length value when the
+ requested Range value doesn't fit entirely within the file's contents (#3814)
+ - Attempting to call request.finish() after the connection has been lost will
+ now immediately raise a RuntimeError (#4013)
+ - An HTTP-auth resource should now be able to directly render the wrapped
+ avatar, whereas before it would only allow retrieval of child resources
+ (#4014)
+ - twisted.web's wsgi support should no longer attempt to call request.finish
+ twice, which would cause errors in certain cases (#4025)
+ - WSGI applications should now be able to handle requests with large bodies
+ (#4029)
+ - Exceptions raised from WSGI applications should now more reliably be turned
+ into 500 errors on the HTTP level (#4019)
+ - DeferredResource now correctly passes through exceptions raised from the
+ wrapped resource, instead of turning them all into 500 errors (#3932)
+ - Agent.request now generates a Host header when no headers are passed at
+ (#4131)
+
+Deprecations and Removals
+-------------------------
+ - The unmaintained and untested twisted.web.monitor module was removed (#2763)
+ - The twisted.web.woven package has been removed (#1522)
+ - All of the error resources in twisted.web.error are now in
+ twisted.web.resource, and accessing them through twisted.web.error is now
+ deprecated (#3035)
+ - To facilitate a simplification of the timeout logic in server.Session,
+ various things have been deprecated (#3457)
+ - the loopFactory attribute is now ignored
+ - the checkExpired method now does nothing
+ - the lifetime parameter to startCheckingExpiration is now ignored
+ - The twisted.web.trp module is now deprecated (#2030)
+
+Other
+-----
+ - #2763, #3540, #3575, #3610, #3605, #1176, #3539, #3750, #3761, #3779, #2677,
+ #3782, #3904, #3919, #3418, #3990, #1404, #4050
+
+
+Web 8.2.0 (2008-12-16)
+======================
+
+Features
+--------
+ - The web server can now deal with multi-value headers in the new attributes of
+ Request, requestHeaders and responseHeaders (#165)
+ - There is now a resource-wrapper which implements HTTP Basic and Digest auth
+ in terms of twisted.cred (#696)
+ - It's now possible to limit the number of redirects that client.getPage will
+ follow (#2412)
+ - The directory-listing code no longer uses Woven (#3257)
+ - static.File now supports Range headers with a single range (#1493)
+ - twisted.web now has a rudimentary WSGI container (#2753)
+ - The web server now supports chunked encoding in requests (#3385)
+
+Fixes
+-----
+ - The xmlrpc client now raises an error when the server sends an empty
+ response (#3399)
+ - HTTPPageGetter no longer duplicates default headers when they're explicitly
+ overridden in the headers parameter (#1382)
+ - The server will no longer timeout clients which are still sending request
+ data (#1903)
+ - microdom's isEqualToNode now returns False when the nodes aren't equal
+ (#2542)
+
+Deprecations and Removals
+-------------------------
+
+ - Request.headers and Request.received_headers are not quite deprecated, but
+ they are discouraged in favor of requestHeaders and responseHeaders (#165)
+
+Other
+-----
+ - #909, #687, #2938, #1152, #2930, #2025, #2683, #3471
+
+
+8.1.0 (2008-05-18)
+==================
+
+Fixes
+-----
+
+ - Fixed an XMLRPC bug whereby sometimes a callRemote Deferred would
+ accidentally be fired twice when a connection was lost during the handling of
+ a response (#3152)
+ - Fixed a bug in the "Using Twisted Web" document which prevented an example
+ resource from being renderable (#3147)
+ - The deprecated mktap API is no longer used (#3127)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Features
+--------
+ - Add support to twisted.web.client.getPage for the HTTP HEAD method. (#2750)
+
+Fixes
+-----
+ - Set content-type in xmlrpc responses to "text/xml" (#2430)
+ - Add more error checking in the xmlrpc.XMLRPC render method, and enforce
+ POST requests. (#2505)
+ - Reject unicode input to twisted.web.client._parse to reject invalid
+ unicode URLs early. (#2628)
+ - Correctly re-quote URL path segments when generating an URL string to
+ return from Request.prePathURL. (#2934)
+ - Make twisted.web.proxy.ProxyClientFactory close the connection when
+ reporting a 501 error. (#1089)
+ - Fix twisted.web.proxy.ReverseProxyResource to specify the port in the
+ host header if different from 80. (#1117)
+ - Change twisted.web.proxy.ReverseProxyResource so that it correctly encodes
+ the request URI it sends on to the server for which it is a proxy. (#3013)
+ - Make "twistd web --personal" use PBServerFactory (#2681)
+
+Misc
+----
+ - #1996, #2382, #2211, #2633, #2634, #2640, #2752, #238, #2905
+
+
+0.7.0 (2007-01-02)
+==================
+
+Features
+--------
+ - Python 2.5 is now supported (#1867)
+ - twisted.web.xmlrpc now supports the <nil/> xml-rpc extension type
+ in both the server and the client (#469)
+
+Fixes
+-----
+ - Microdom and SUX now manages certain malformed XML more resiliently
+ (#1984, #2225, #2298)
+ - twisted.web.client.getPage can now deal with an URL of the form
+ "http://example.com" (no trailing slash) (#1080)
+ - The HTTP server now allows (invalid) URLs with multiple question
+ marks (#1550)
+ - '=' can now be in the value of a cookie (#1051)
+ - Microdom now correctly handles xmlns="" (#2184)
+
+Deprecations and Removals
+-------------------------
+ - websetroot was removed, because it wasn't working anyway (#945)
+ - woven.guard no longer supports the old twisted.cred API (#1440)
+
+Other
+-----
+The following changes are minor or closely related to other changes.
+
+ - #1636, #1637, #1638, #1936, #1883, #447
+
+
+0.6.0 (2006-05-21)
+==================
+
+Features
+--------
+ - Basic auth support for the XMLRPC client (#1474).
+
+Fixes
+-----
+ - More correct datetime parsing.
+ - Efficiency improvements (#974)
+ - Handle popular non-RFC compliant formats for If-Modified-Since
+ headers (#976).
+ - Improve support for certain buggy CGI scripts.
+ - CONTENT_LENGTH is now available to CGI scripts.
+ - Support for even worse HTML in microdom (#1358).
+ - Trying to view a user's home page when the user doesn't have a
+ ~/public_html no longer displays a traceback (#551).
+ - Misc: #543, #1011, #1005, #1287, #1337, #1383, #1079, #1492, #1189,
+ #737, #872.
+
+
+0.5.0
+=====
+ - Client properly reports timeouts as error
+ - "Socially deprecate" woven
+ - Fix memory leak in _c_urlarg library
+ - Stop using _c_urlarg library
+ - Fix 'gzip' and 'bzip2' content-encodings
+ - Escape log entries so remote user cannot corrupt the log
+ - Commented out range support because it's broken
+ - Fix HEAD responses without content-length
diff --git a/twisted/web/topfiles/README b/twisted/web/topfiles/README
new file mode 100644
index 0000000..7abcc46
--- /dev/null
+++ b/twisted/web/topfiles/README
@@ -0,0 +1,6 @@
+Twisted Web 12.1.0
+
+Twisted Web depends on Twisted Core. pyOpenSSL
+(<http://launchpad.net/pyopenssl>) is also required for HTTPS. SOAPpy
+(<http://pywebsvcs.sourceforge.net/>) is required for SOAP support. For Quixote
+resource templates, Quixote (<http://www.quixote.ca/>) is required.
diff --git a/twisted/web/topfiles/setup.py b/twisted/web/topfiles/setup.py
new file mode 100644
index 0000000..e54ea7e
--- /dev/null
+++ b/twisted/web/topfiles/setup.py
@@ -0,0 +1,30 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ dist.setup(
+ twisted_subproject="web",
+ scripts=dist.getScripts("web"),
+ # metadata
+ name="Twisted Web",
+ description="Twisted web server, programmable in Python.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="James Knight",
+ url="http://twistedmatrix.com/trac/wiki/TwistedWeb",
+ license="MIT",
+ long_description="""\
+Twisted Web is a complete web server, aimed at hosting web
+applications using Twisted and Python, but fully able to serve static
+pages, also.
+""",
+ )
diff --git a/twisted/web/twcgi.py b/twisted/web/twcgi.py
new file mode 100644
index 0000000..b1f001a
--- /dev/null
+++ b/twisted/web/twcgi.py
@@ -0,0 +1,299 @@
+# -*- test-case-name: twisted.web.test.test_cgi -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+I hold resource classes and helper classes that deal with CGI scripts.
+"""
+
+# System Imports
+import string
+import os
+import urllib
+
+# Twisted Imports
+from twisted.web import http
+from twisted.internet import reactor, protocol
+from twisted.spread import pb
+from twisted.python import log, filepath
+from twisted.web import resource, server, static
+
+
+class CGIDirectory(resource.Resource, filepath.FilePath):
+ def __init__(self, pathname):
+ resource.Resource.__init__(self)
+ filepath.FilePath.__init__(self, pathname)
+
+ def getChild(self, path, request):
+ fnp = self.child(path)
+ if not fnp.exists():
+ return static.File.childNotFound
+ elif fnp.isdir():
+ return CGIDirectory(fnp.path)
+ else:
+ return CGIScript(fnp.path)
+ return resource.NoResource()
+
+ def render(self, request):
+ notFound = resource.NoResource(
+ "CGI directories do not support directory listing.")
+ return notFound.render(request)
+
+
+
+class CGIScript(resource.Resource):
+ """
+ L{CGIScript} is a resource which runs child processes according to the CGI
+ specification.
+
+ The implementation is complex due to the fact that it requires asynchronous
+ IPC with an external process with an unpleasant protocol.
+ """
+ isLeaf = 1
+ def __init__(self, filename, registry=None):
+ """
+ Initialize, with the name of a CGI script file.
+ """
+ self.filename = filename
+
+
+ def render(self, request):
+ """
+ Do various things to conform to the CGI specification.
+
+ I will set up the usual slew of environment variables, then spin off a
+ process.
+
+ @type request: L{twisted.web.http.Request}
+ @param request: An HTTP request.
+ """
+ script_name = "/"+string.join(request.prepath, '/')
+ serverName = string.split(request.getRequestHostname(), ':')[0]
+ env = {"SERVER_SOFTWARE": server.version,
+ "SERVER_NAME": serverName,
+ "GATEWAY_INTERFACE": "CGI/1.1",
+ "SERVER_PROTOCOL": request.clientproto,
+ "SERVER_PORT": str(request.getHost().port),
+ "REQUEST_METHOD": request.method,
+ "SCRIPT_NAME": script_name, # XXX
+ "SCRIPT_FILENAME": self.filename,
+ "REQUEST_URI": request.uri,
+ }
+
+ client = request.getClient()
+ if client is not None:
+ env['REMOTE_HOST'] = client
+ ip = request.getClientIP()
+ if ip is not None:
+ env['REMOTE_ADDR'] = ip
+ pp = request.postpath
+ if pp:
+ env["PATH_INFO"] = "/"+string.join(pp, '/')
+
+ if hasattr(request, "content"):
+ # request.content is either a StringIO or a TemporaryFile, and
+ # the file pointer is sitting at the beginning (seek(0,0))
+ request.content.seek(0,2)
+ length = request.content.tell()
+ request.content.seek(0,0)
+ env['CONTENT_LENGTH'] = str(length)
+
+ qindex = string.find(request.uri, '?')
+ if qindex != -1:
+ qs = env['QUERY_STRING'] = request.uri[qindex+1:]
+ if '=' in qs:
+ qargs = []
+ else:
+ qargs = [urllib.unquote(x) for x in qs.split('+')]
+ else:
+ env['QUERY_STRING'] = ''
+ qargs = []
+
+ # Propogate HTTP headers
+ for title, header in request.getAllHeaders().items():
+ envname = string.upper(string.replace(title, '-', '_'))
+ if title not in ('content-type', 'content-length'):
+ envname = "HTTP_" + envname
+ env[envname] = header
+ # Propogate our environment
+ for key, value in os.environ.items():
+ if not env.has_key(key):
+ env[key] = value
+ # And they're off!
+ self.runProcess(env, request, qargs)
+ return server.NOT_DONE_YET
+
+
+ def runProcess(self, env, request, qargs=[]):
+ """
+ Run the cgi script.
+
+ @type env: A C{dict} of C{str}, or C{None}
+ @param env: The environment variables to pass to the processs that will
+ get spawned. See
+ L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
+ information about environments and process creation.
+
+ @type request: L{twisted.web.http.Request}
+ @param request: An HTTP request.
+
+ @type qargs: A C{list} of C{str}
+ @param qargs: The command line arguments to pass to the process that
+ will get spawned.
+ """
+ p = CGIProcessProtocol(request)
+ reactor.spawnProcess(p, self.filename, [self.filename] + qargs, env,
+ os.path.dirname(self.filename))
+
+
+
+class FilteredScript(CGIScript):
+ """
+ I am a special version of a CGI script, that uses a specific executable.
+
+ This is useful for interfacing with other scripting languages that adhere to
+ the CGI standard. My C{filter} attribute specifies what executable to run,
+ and my C{filename} init parameter describes which script to pass to the
+ first argument of that script.
+
+ To customize me for a particular location of a CGI interpreter, override
+ C{filter}.
+
+ @type filter: C{str}
+ @ivar filter: The absolute path to the executable.
+ """
+
+ filter = '/usr/bin/cat'
+
+
+ def runProcess(self, env, request, qargs=[]):
+ """
+ Run a script through the C{filter} executable.
+
+ @type env: A C{dict} of C{str}, or C{None}
+ @param env: The environment variables to pass to the processs that will
+ get spawned. See
+ L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
+ information about environments and process creation.
+
+ @type request: L{twisted.web.http.Request}
+ @param request: An HTTP request.
+
+ @type qargs: A C{list} of C{str}
+ @param qargs: The command line arguments to pass to the process that
+ will get spawned.
+ """
+ p = CGIProcessProtocol(request)
+ reactor.spawnProcess(p, self.filter,
+ [self.filter, self.filename] + qargs, env,
+ os.path.dirname(self.filename))
+
+
+
+class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
+ handling_headers = 1
+ headers_written = 0
+ headertext = ''
+ errortext = ''
+
+ # Remotely relay producer interface.
+
+ def view_resumeProducing(self, issuer):
+ self.resumeProducing()
+
+ def view_pauseProducing(self, issuer):
+ self.pauseProducing()
+
+ def view_stopProducing(self, issuer):
+ self.stopProducing()
+
+ def resumeProducing(self):
+ self.transport.resumeProducing()
+
+ def pauseProducing(self):
+ self.transport.pauseProducing()
+
+ def stopProducing(self):
+ self.transport.loseConnection()
+
+ def __init__(self, request):
+ self.request = request
+
+ def connectionMade(self):
+ self.request.registerProducer(self, 1)
+ self.request.content.seek(0, 0)
+ content = self.request.content.read()
+ if content:
+ self.transport.write(content)
+ self.transport.closeStdin()
+
+ def errReceived(self, error):
+ self.errortext = self.errortext + error
+
+ def outReceived(self, output):
+ """
+ Handle a chunk of input
+ """
+ # First, make sure that the headers from the script are sorted
+ # out (we'll want to do some parsing on these later.)
+ if self.handling_headers:
+ text = self.headertext + output
+ headerEnds = []
+ for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
+ headerend = text.find(delimiter)
+ if headerend != -1:
+ headerEnds.append((headerend, delimiter))
+ if headerEnds:
+ # The script is entirely in control of response headers; disable the
+ # default Content-Type value normally provided by
+ # twisted.web.server.Request.
+ self.request.defaultContentType = None
+
+ headerEnds.sort()
+ headerend, delimiter = headerEnds[0]
+ self.headertext = text[:headerend]
+ # This is a final version of the header text.
+ linebreak = delimiter[:len(delimiter)/2]
+ headers = self.headertext.split(linebreak)
+ for header in headers:
+ br = header.find(': ')
+ if br == -1:
+ log.msg( 'ignoring malformed CGI header: %s' % header )
+ else:
+ headerName = header[:br].lower()
+ headerText = header[br+2:]
+ if headerName == 'location':
+ self.request.setResponseCode(http.FOUND)
+ if headerName == 'status':
+ try:
+ statusNum = int(headerText[:3]) #"XXX <description>" sometimes happens.
+ except:
+ log.msg( "malformed status header" )
+ else:
+ self.request.setResponseCode(statusNum)
+ else:
+ # Don't allow the application to control these required headers.
+ if headerName.lower() not in ('server', 'date'):
+ self.request.responseHeaders.addRawHeader(headerName, headerText)
+ output = text[headerend+len(delimiter):]
+ self.handling_headers = 0
+ if self.handling_headers:
+ self.headertext = text
+ if not self.handling_headers:
+ self.request.write(output)
+
+ def processEnded(self, reason):
+ if reason.value.exitCode != 0:
+ log.msg("CGI %s exited with exit code %s" %
+ (self.request.uri, reason.value.exitCode))
+ if self.errortext:
+ log.msg("Errors from CGI %s: %s" % (self.request.uri, self.errortext))
+ if self.handling_headers:
+ log.msg("Premature end of headers in %s: %s" % (self.request.uri, self.headertext))
+ self.request.write(
+ resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
+ "CGI Script Error",
+ "Premature end of script headers.").render(self.request))
+ self.request.unregisterProducer()
+ self.request.finish()
diff --git a/twisted/web/util.py b/twisted/web/util.py
new file mode 100644
index 0000000..0c6cdb6
--- /dev/null
+++ b/twisted/web/util.py
@@ -0,0 +1,433 @@
+# -*- test-case-name: twisted.web.test.test_util -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An assortment of web server-related utilities.
+"""
+
+__all__ = [
+ "redirectTo", "Redirect", "ChildRedirector", "ParentRedirect",
+ "DeferredResource", "htmlIndent", "FailureElement", "formatFailure"]
+
+from cStringIO import StringIO
+import linecache
+import string
+import types
+
+from twisted.python.filepath import FilePath
+from twisted.python.reflect import fullyQualifiedName
+from twisted.python.deprecate import deprecatedModuleAttribute
+from twisted.python.versions import Version
+from twisted.python.modules import getModule
+
+from twisted.web import html, resource
+from twisted.web.template import (
+ TagLoader, XMLFile, Element, renderer, flattenString)
+
+
+def redirectTo(URL, request):
+ """
+ Generate a redirect to the given location.
+
+ @param URL: A C{str} giving the location to which to redirect.
+ @type URL: C{str}
+
+ @param request: The request object to use to generate the redirect.
+ @type request: L{IRequest<twisted.web.iweb.IRequest>} provider
+
+ @raise TypeError: If the type of C{URL} a C{unicode} instead of C{str}.
+
+ @return: A C{str} containing HTML which tries to convince the client agent
+ to visit the new location even if it doesn't respect the I{FOUND}
+ response code. This is intended to be returned from a render method,
+ eg::
+
+ def render_GET(self, request):
+ return redirectTo("http://example.com/", request)
+ """
+ if isinstance(URL, unicode) :
+ raise TypeError("Unicode object not allowed as URL")
+ request.setHeader("content-type", "text/html; charset=utf-8")
+ request.redirect(URL)
+ return """
+<html>
+ <head>
+ <meta http-equiv=\"refresh\" content=\"0;URL=%(url)s\">
+ </head>
+ <body bgcolor=\"#FFFFFF\" text=\"#000000\">
+ <a href=\"%(url)s\">click here</a>
+ </body>
+</html>
+""" % {'url': URL}
+
+class Redirect(resource.Resource):
+
+ isLeaf = 1
+
+ def __init__(self, url):
+ resource.Resource.__init__(self)
+ self.url = url
+
+ def render(self, request):
+ return redirectTo(self.url, request)
+
+ def getChild(self, name, request):
+ return self
+
+class ChildRedirector(Redirect):
+ isLeaf = 0
+ def __init__(self, url):
+ # XXX is this enough?
+ if ((url.find('://') == -1)
+ and (not url.startswith('..'))
+ and (not url.startswith('/'))):
+ raise ValueError("It seems you've given me a redirect (%s) that is a child of myself! That's not good, it'll cause an infinite redirect." % url)
+ Redirect.__init__(self, url)
+
+ def getChild(self, name, request):
+ newUrl = self.url
+ if not newUrl.endswith('/'):
+ newUrl += '/'
+ newUrl += name
+ return ChildRedirector(newUrl)
+
+
+from twisted.python import urlpath
+
+class ParentRedirect(resource.Resource):
+ """
+ I redirect to URLPath.here().
+ """
+ isLeaf = 1
+ def render(self, request):
+ return redirectTo(urlpath.URLPath.fromRequest(request).here(), request)
+
+ def getChild(self, request):
+ return self
+
+
+class DeferredResource(resource.Resource):
+ """
+ I wrap up a Deferred that will eventually result in a Resource
+ object.
+ """
+ isLeaf = 1
+
+ def __init__(self, d):
+ resource.Resource.__init__(self)
+ self.d = d
+
+ def getChild(self, name, request):
+ return self
+
+ def render(self, request):
+ self.d.addCallback(self._cbChild, request).addErrback(
+ self._ebChild,request)
+ from twisted.web.server import NOT_DONE_YET
+ return NOT_DONE_YET
+
+ def _cbChild(self, child, request):
+ request.render(resource.getChildForRequest(child, request))
+
+ def _ebChild(self, reason, request):
+ request.processingFailed(reason)
+ return reason
+
+
+stylesheet = ""
+
+def htmlrepr(x):
+ return htmlReprTypes.get(type(x), htmlUnknown)(x)
+
+def saferepr(x):
+ try:
+ rx = repr(x)
+ except:
+ rx = "<repr failed! %s instance at %s>" % (x.__class__, id(x))
+ return rx
+
+def htmlUnknown(x):
+ return '<code>'+html.escape(saferepr(x))+'</code>'
+
+def htmlDict(d):
+ io = StringIO()
+ w = io.write
+ w('<div class="dict"><span class="heading">Dictionary instance @ %s</span>' % hex(id(d)))
+ w('<table class="dict">')
+ for k, v in d.items():
+
+ if k == '__builtins__':
+ v = 'builtin dictionary'
+ w('<tr><td class="dictKey">%s</td><td class="dictValue">%s</td></tr>' % (htmlrepr(k), htmlrepr(v)))
+ w('</table></div>')
+ return io.getvalue()
+
+def htmlList(l):
+ io = StringIO()
+ w = io.write
+ w('<div class="list"><span class="heading">List instance @ %s</span>' % hex(id(l)))
+ for i in l:
+ w('<div class="listItem">%s</div>' % htmlrepr(i))
+ w('</div>')
+ return io.getvalue()
+
+def htmlInst(i):
+ if hasattr(i, "__html__"):
+ s = i.__html__()
+ else:
+ s = html.escape(saferepr(i))
+ return '''<div class="instance"><span class="instanceName">%s instance @ %s</span>
+ <span class="instanceRepr">%s</span></div>
+ ''' % (i.__class__, hex(id(i)), s)
+
+def htmlString(s):
+ return html.escape(saferepr(s))
+
+def htmlFunc(f):
+ return ('<div class="function">' +
+ html.escape("function %s in file %s at line %s" %
+ (f.__name__, f.func_code.co_filename,
+ f.func_code.co_firstlineno))+
+ '</div>')
+
+htmlReprTypes = {types.DictType: htmlDict,
+ types.ListType: htmlList,
+ types.InstanceType: htmlInst,
+ types.StringType: htmlString,
+ types.FunctionType: htmlFunc}
+
+
+
+def htmlIndent(snippetLine):
+ ret = string.replace(string.replace(html.escape(string.rstrip(snippetLine)),
+ ' ', '&nbsp;'),
+ '\t', '&nbsp; &nbsp; &nbsp; &nbsp; ')
+ return ret
+
+
+
+class _SourceLineElement(Element):
+ """
+ L{_SourceLineElement} is an L{IRenderable} which can render a single line of
+ source code.
+
+ @ivar number: A C{int} giving the line number of the source code to be
+ rendered.
+ @ivar source: A C{str} giving the source code to be rendered.
+ """
+ def __init__(self, loader, number, source):
+ Element.__init__(self, loader)
+ self.number = number
+ self.source = source
+
+
+ @renderer
+ def sourceLine(self, request, tag):
+ """
+ Render the line of source as a child of C{tag}.
+ """
+ return tag(self.source.replace(' ', u' \N{NO-BREAK SPACE}'))
+
+
+ @renderer
+ def lineNumber(self, request, tag):
+ """
+ Render the line number as a child of C{tag}.
+ """
+ return tag(str(self.number))
+
+
+
+class _SourceFragmentElement(Element):
+ """
+ L{_SourceFragmentElement} is an L{IRenderable} which can render several lines
+ of source code near the line number of a particular frame object.
+
+ @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object
+ for which to load a source line to render. This is really a tuple
+ holding some information from a frame object. See
+ L{Failure.frames<twisted.python.failure.Failure>} for specifics.
+ """
+ def __init__(self, loader, frame):
+ Element.__init__(self, loader)
+ self.frame = frame
+
+
+ def _getSourceLines(self):
+ """
+ Find the source line references by C{self.frame} and yield, in source
+ line order, it and the previous and following lines.
+
+ @return: A generator which yields two-tuples. Each tuple gives a source
+ line number and the contents of that source line.
+ """
+ filename = self.frame[1]
+ lineNumber = self.frame[2]
+ for snipLineNumber in range(lineNumber - 1, lineNumber + 2):
+ yield (snipLineNumber,
+ linecache.getline(filename, snipLineNumber).rstrip())
+
+
+ @renderer
+ def sourceLines(self, request, tag):
+ """
+ Render the source line indicated by C{self.frame} and several
+ surrounding lines. The active line will be given a I{class} of
+ C{"snippetHighlightLine"}. Other lines will be given a I{class} of
+ C{"snippetLine"}.
+ """
+ for (lineNumber, sourceLine) in self._getSourceLines():
+ newTag = tag.clone()
+ if lineNumber == self.frame[2]:
+ cssClass = "snippetHighlightLine"
+ else:
+ cssClass = "snippetLine"
+ loader = TagLoader(newTag(**{"class": cssClass}))
+ yield _SourceLineElement(loader, lineNumber, sourceLine)
+
+
+
+class _FrameElement(Element):
+ """
+ L{_FrameElement} is an L{IRenderable} which can render details about one
+ frame from a L{Failure<twisted.python.failure.Failure>}.
+
+ @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object
+ for which to load a source line to render. This is really a tuple
+ holding some information from a frame object. See
+ L{Failure.frames<twisted.python.failure.Failure>} for specifics.
+ """
+ def __init__(self, loader, frame):
+ Element.__init__(self, loader)
+ self.frame = frame
+
+
+ @renderer
+ def filename(self, request, tag):
+ """
+ Render the name of the file this frame references as a child of C{tag}.
+ """
+ return tag(self.frame[1])
+
+
+ @renderer
+ def lineNumber(self, request, tag):
+ """
+ Render the source line number this frame references as a child of
+ C{tag}.
+ """
+ return tag(str(self.frame[2]))
+
+
+ @renderer
+ def function(self, request, tag):
+ """
+ Render the function name this frame references as a child of C{tag}.
+ """
+ return tag(self.frame[0])
+
+
+ @renderer
+ def source(self, request, tag):
+ """
+ Render the source code surrounding the line this frame references,
+ replacing C{tag}.
+ """
+ return _SourceFragmentElement(TagLoader(tag), self.frame)
+
+
+
+class _StackElement(Element):
+ """
+ L{_StackElement} renders an L{IRenderable} which can render a list of frames.
+ """
+ def __init__(self, loader, stackFrames):
+ Element.__init__(self, loader)
+ self.stackFrames = stackFrames
+
+
+ @renderer
+ def frames(self, request, tag):
+ """
+ Render the list of frames in this L{_StackElement}, replacing C{tag}.
+ """
+ return [
+ _FrameElement(TagLoader(tag.clone()), frame)
+ for frame
+ in self.stackFrames]
+
+
+
+class FailureElement(Element):
+ """
+ L{FailureElement} is an L{IRenderable} which can render detailed information
+ about a L{Failure<twisted.python.failure.Failure>}.
+
+ @ivar failure: The L{Failure<twisted.python.failure.Failure>} instance which
+ will be rendered.
+
+ @since: 12.1
+ """
+ loader = XMLFile(getModule(__name__).filePath.sibling("failure.xhtml"))
+
+ def __init__(self, failure, loader=None):
+ Element.__init__(self, loader)
+ self.failure = failure
+
+
+ @renderer
+ def type(self, request, tag):
+ """
+ Render the exception type as a child of C{tag}.
+ """
+ return tag(fullyQualifiedName(self.failure.type))
+
+
+ @renderer
+ def value(self, request, tag):
+ """
+ Render the exception value as a child of C{tag}.
+ """
+ return tag(str(self.failure.value))
+
+
+ @renderer
+ def traceback(self, request, tag):
+ """
+ Render all the frames in the wrapped
+ L{Failure<twisted.python.failure.Failure>}'s traceback stack, replacing
+ C{tag}.
+ """
+ return _StackElement(TagLoader(tag), self.failure.frames)
+
+
+
+def formatFailure(myFailure):
+ """
+ Construct an HTML representation of the given failure.
+
+ Consider using L{FailureElement} instead.
+
+ @type myFailure: L{Failure<twisted.python.failure.Failure>}
+
+ @rtype: C{str}
+ @return: A string containing the HTML representation of the given failure.
+ """
+ result = []
+ flattenString(None, FailureElement(myFailure)).addBoth(result.append)
+ if isinstance(result[0], str):
+ # Ensure the result string is all ASCII, for compatibility with the
+ # default encoding expected by browsers.
+ return result[0].decode('utf-8').encode('ascii', 'xmlcharrefreplace')
+ result[0].raiseException()
+
+
+_twelveOne = Version("Twisted", 12, 1, 0)
+
+for name in ["htmlrepr", "saferepr", "htmlUnknown", "htmlString", "htmlList",
+ "htmlDict", "htmlInst", "htmlFunc", "htmlIndent", "htmlReprTypes",
+ "stylesheet"]:
+ deprecatedModuleAttribute(
+ _twelveOne, "See twisted.web.template.", __name__, name)
+del name
diff --git a/twisted/web/vhost.py b/twisted/web/vhost.py
new file mode 100644
index 0000000..1acee21
--- /dev/null
+++ b/twisted/web/vhost.py
@@ -0,0 +1,135 @@
+# -*- test-case-name: twisted.web.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+I am a virtual hosts implementation.
+"""
+
+# Twisted Imports
+from twisted.python import roots
+from twisted.web import resource
+
+
+class VirtualHostCollection(roots.Homogenous):
+ """Wrapper for virtual hosts collection.
+
+ This exists for configuration purposes.
+ """
+ entityType = resource.Resource
+
+ def __init__(self, nvh):
+ self.nvh = nvh
+
+ def listStaticEntities(self):
+ return self.nvh.hosts.items()
+
+ def getStaticEntity(self, name):
+ return self.nvh.hosts.get(self)
+
+ def reallyPutEntity(self, name, entity):
+ self.nvh.addHost(name, entity)
+
+ def delEntity(self, name):
+ self.nvh.removeHost(name)
+
+
+class NameVirtualHost(resource.Resource):
+ """I am a resource which represents named virtual hosts.
+ """
+
+ default = None
+
+ def __init__(self):
+ """Initialize.
+ """
+ resource.Resource.__init__(self)
+ self.hosts = {}
+
+ def listStaticEntities(self):
+ return resource.Resource.listStaticEntities(self) + [("Virtual Hosts", VirtualHostCollection(self))]
+
+ def getStaticEntity(self, name):
+ if name == "Virtual Hosts":
+ return VirtualHostCollection(self)
+ else:
+ return resource.Resource.getStaticEntity(self, name)
+
+ def addHost(self, name, resrc):
+ """Add a host to this virtual host.
+
+ This will take a host named `name', and map it to a resource
+ `resrc'. For example, a setup for our virtual hosts would be::
+
+ nvh.addHost('divunal.com', divunalDirectory)
+ nvh.addHost('www.divunal.com', divunalDirectory)
+ nvh.addHost('twistedmatrix.com', twistedMatrixDirectory)
+ nvh.addHost('www.twistedmatrix.com', twistedMatrixDirectory)
+ """
+ self.hosts[name] = resrc
+
+ def removeHost(self, name):
+ """Remove a host."""
+ del self.hosts[name]
+
+ def _getResourceForRequest(self, request):
+ """(Internal) Get the appropriate resource for the given host.
+ """
+ hostHeader = request.getHeader('host')
+ if hostHeader == None:
+ return self.default or resource.NoResource()
+ else:
+ host = hostHeader.lower().split(':', 1)[0]
+ return (self.hosts.get(host, self.default)
+ or resource.NoResource("host %s not in vhost map" % repr(host)))
+
+ def render(self, request):
+ """Implementation of resource.Resource's render method.
+ """
+ resrc = self._getResourceForRequest(request)
+ return resrc.render(request)
+
+ def getChild(self, path, request):
+ """Implementation of resource.Resource's getChild method.
+ """
+ resrc = self._getResourceForRequest(request)
+ if resrc.isLeaf:
+ request.postpath.insert(0,request.prepath.pop(-1))
+ return resrc
+ else:
+ return resrc.getChildWithDefault(path, request)
+
+class _HostResource(resource.Resource):
+
+ def getChild(self, path, request):
+ if ':' in path:
+ host, port = path.split(':', 1)
+ port = int(port)
+ else:
+ host, port = path, 80
+ request.setHost(host, port)
+ prefixLen = 3+request.isSecure()+4+len(path)+len(request.prepath[-3])
+ request.path = '/'+'/'.join(request.postpath)
+ request.uri = request.uri[prefixLen:]
+ del request.prepath[:3]
+ return request.site.getResourceFor(request)
+
+
+class VHostMonsterResource(resource.Resource):
+
+ """
+ Use this to be able to record the hostname and method (http vs. https)
+ in the URL without disturbing your web site. If you put this resource
+ in a URL http://foo.com/bar then requests to
+ http://foo.com/bar/http/baz.com/something will be equivalent to
+ http://foo.com/something, except that the hostname the request will
+ appear to be accessing will be "baz.com". So if "baz.com" is redirecting
+ all requests for to foo.com, while foo.com is inaccessible from the outside,
+ then redirect and url generation will work correctly
+ """
+ def getChild(self, path, request):
+ if path == 'http':
+ request.isSecure = lambda: 0
+ elif path == 'https':
+ request.isSecure = lambda: 1
+ return _HostResource()
diff --git a/twisted/web/wsgi.py b/twisted/web/wsgi.py
new file mode 100644
index 0000000..0918c4d
--- /dev/null
+++ b/twisted/web/wsgi.py
@@ -0,0 +1,403 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An implementation of
+U{Web Resource Gateway Interface<http://www.python.org/dev/peps/pep-0333/>}.
+"""
+
+__metaclass__ = type
+
+from sys import exc_info
+
+from zope.interface import implements
+
+from twisted.python.log import msg, err
+from twisted.python.failure import Failure
+from twisted.web.resource import IResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.http import INTERNAL_SERVER_ERROR
+
+
+class _ErrorStream:
+ """
+ File-like object instances of which are used as the value for the
+ C{'wsgi.errors'} key in the C{environ} dictionary passed to the application
+ object.
+
+ This simply passes writes on to L{logging<twisted.python.log>} system as
+ error events from the C{'wsgi'} system. In the future, it may be desirable
+ to expose more information in the events it logs, such as the application
+ object which generated the message.
+ """
+ def write(self, bytes):
+ """
+ Generate an event for the logging system with the given bytes as the
+ message.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+ msg(bytes, system='wsgi', isError=True)
+
+
+ def writelines(self, iovec):
+ """
+ Join the given lines and pass them to C{write} to be handled in the
+ usual way.
+
+ This is called in a WSGI application thread, not the I/O thread.
+
+ @param iovec: A C{list} of C{'\\n'}-terminated C{str} which will be
+ logged.
+ """
+ self.write(''.join(iovec))
+
+
+ def flush(self):
+ """
+ Nothing is buffered, so flushing does nothing. This method is required
+ to exist by PEP 333, though.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+
+
+
+class _InputStream:
+ """
+ File-like object instances of which are used as the value for the
+ C{'wsgi.input'} key in the C{environ} dictionary passed to the application
+ object.
+
+ This only exists to make the handling of C{readline(-1)} consistent across
+ different possible underlying file-like object implementations. The other
+ supported methods pass through directly to the wrapped object.
+ """
+ def __init__(self, input):
+ """
+ Initialize the instance.
+
+ This is called in the I/O thread, not a WSGI application thread.
+ """
+ self._wrapped = input
+
+
+ def read(self, size=None):
+ """
+ Pass through to the underlying C{read}.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+ # Avoid passing None because cStringIO and file don't like it.
+ if size is None:
+ return self._wrapped.read()
+ return self._wrapped.read(size)
+
+
+ def readline(self, size=None):
+ """
+ Pass through to the underlying C{readline}, with a size of C{-1} replaced
+ with a size of C{None}.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+ # Check for -1 because StringIO doesn't handle it correctly. Check for
+ # None because files and tempfiles don't accept that.
+ if size == -1 or size is None:
+ return self._wrapped.readline()
+ return self._wrapped.readline(size)
+
+
+ def readlines(self, size=None):
+ """
+ Pass through to the underlying C{readlines}.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+ # Avoid passing None because cStringIO and file don't like it.
+ if size is None:
+ return self._wrapped.readlines()
+ return self._wrapped.readlines(size)
+
+
+ def __iter__(self):
+ """
+ Pass through to the underlying C{__iter__}.
+
+ This is called in a WSGI application thread, not the I/O thread.
+ """
+ return iter(self._wrapped)
+
+
+
+class _WSGIResponse:
+ """
+ Helper for L{WSGIResource} which drives the WSGI application using a
+ threadpool and hooks it up to the L{Request}.
+
+ @ivar started: A C{bool} indicating whether or not the response status and
+ headers have been written to the request yet. This may only be read or
+ written in the WSGI application thread.
+
+ @ivar reactor: An L{IReactorThreads} provider which is used to call methods
+ on the request in the I/O thread.
+
+ @ivar threadpool: A L{ThreadPool} which is used to call the WSGI
+ application object in a non-I/O thread.
+
+ @ivar application: The WSGI application object.
+
+ @ivar request: The L{Request} upon which the WSGI environment is based and
+ to which the application's output will be sent.
+
+ @ivar environ: The WSGI environment C{dict}.
+
+ @ivar status: The HTTP response status C{str} supplied to the WSGI
+ I{start_response} callable by the application.
+
+ @ivar headers: A list of HTTP response headers supplied to the WSGI
+ I{start_response} callable by the application.
+
+ @ivar _requestFinished: A flag which indicates whether it is possible to
+ generate more response data or not. This is C{False} until
+ L{Request.notifyFinish} tells us the request is done, then C{True}.
+ """
+
+ _requestFinished = False
+
+ def __init__(self, reactor, threadpool, application, request):
+ self.started = False
+ self.reactor = reactor
+ self.threadpool = threadpool
+ self.application = application
+ self.request = request
+ self.request.notifyFinish().addBoth(self._finished)
+
+ if request.prepath:
+ scriptName = '/' + '/'.join(request.prepath)
+ else:
+ scriptName = ''
+
+ if request.postpath:
+ pathInfo = '/' + '/'.join(request.postpath)
+ else:
+ pathInfo = ''
+
+ parts = request.uri.split('?', 1)
+ if len(parts) == 1:
+ queryString = ''
+ else:
+ queryString = parts[1]
+
+ self.environ = {
+ 'REQUEST_METHOD': request.method,
+ 'REMOTE_ADDR': request.getClientIP(),
+ 'SCRIPT_NAME': scriptName,
+ 'PATH_INFO': pathInfo,
+ 'QUERY_STRING': queryString,
+ 'CONTENT_TYPE': request.getHeader('content-type') or '',
+ 'CONTENT_LENGTH': request.getHeader('content-length') or '',
+ 'SERVER_NAME': request.getRequestHostname(),
+ 'SERVER_PORT': str(request.getHost().port),
+ 'SERVER_PROTOCOL': request.clientproto}
+
+
+ # The application object is entirely in control of response headers;
+ # disable the default Content-Type value normally provided by
+ # twisted.web.server.Request.
+ self.request.defaultContentType = None
+
+ for name, values in request.requestHeaders.getAllRawHeaders():
+ name = 'HTTP_' + name.upper().replace('-', '_')
+ # It might be preferable for http.HTTPChannel to clear out
+ # newlines.
+ self.environ[name] = ','.join([
+ v.replace('\n', ' ') for v in values])
+
+ self.environ.update({
+ 'wsgi.version': (1, 0),
+ 'wsgi.url_scheme': request.isSecure() and 'https' or 'http',
+ 'wsgi.run_once': False,
+ 'wsgi.multithread': True,
+ 'wsgi.multiprocess': False,
+ 'wsgi.errors': _ErrorStream(),
+ # Attend: request.content was owned by the I/O thread up until
+ # this point. By wrapping it and putting the result into the
+ # environment dictionary, it is effectively being given to
+ # another thread. This means that whatever it is, it has to be
+ # safe to access it from two different threads. The access
+ # *should* all be serialized (first the I/O thread writes to
+ # it, then the WSGI thread reads from it, then the I/O thread
+ # closes it). However, since the request is made available to
+ # arbitrary application code during resource traversal, it's
+ # possible that some other code might decide to use it in the
+ # I/O thread concurrently with its use in the WSGI thread.
+ # More likely than not, this will break. This seems like an
+ # unlikely possibility to me, but if it is to be allowed,
+ # something here needs to change. -exarkun
+ 'wsgi.input': _InputStream(request.content)})
+
+
+ def _finished(self, ignored):
+ """
+ Record the end of the response generation for the request being
+ serviced.
+ """
+ self._requestFinished = True
+
+
+ def startResponse(self, status, headers, excInfo=None):
+ """
+ The WSGI I{start_response} callable. The given values are saved until
+ they are needed to generate the response.
+
+ This will be called in a non-I/O thread.
+ """
+ if self.started and excInfo is not None:
+ raise excInfo[0], excInfo[1], excInfo[2]
+ self.status = status
+ self.headers = headers
+ return self.write
+
+
+ def write(self, bytes):
+ """
+ The WSGI I{write} callable returned by the I{start_response} callable.
+ The given bytes will be written to the response body, possibly flushing
+ the status and headers first.
+
+ This will be called in a non-I/O thread.
+ """
+ def wsgiWrite(started):
+ if not started:
+ self._sendResponseHeaders()
+ self.request.write(bytes)
+ self.reactor.callFromThread(wsgiWrite, self.started)
+ self.started = True
+
+
+ def _sendResponseHeaders(self):
+ """
+ Set the response code and response headers on the request object, but
+ do not flush them. The caller is responsible for doing a write in
+ order for anything to actually be written out in response to the
+ request.
+
+ This must be called in the I/O thread.
+ """
+ code, message = self.status.split(None, 1)
+ code = int(code)
+ self.request.setResponseCode(code, message)
+
+ for name, value in self.headers:
+ # Don't allow the application to control these required headers.
+ if name.lower() not in ('server', 'date'):
+ self.request.responseHeaders.addRawHeader(name, value)
+
+
+ def start(self):
+ """
+ Start the WSGI application in the threadpool.
+
+ This must be called in the I/O thread.
+ """
+ self.threadpool.callInThread(self.run)
+
+
+ def run(self):
+ """
+ Call the WSGI application object, iterate it, and handle its output.
+
+ This must be called in a non-I/O thread (ie, a WSGI application
+ thread).
+ """
+ try:
+ appIterator = self.application(self.environ, self.startResponse)
+ for elem in appIterator:
+ if elem:
+ self.write(elem)
+ if self._requestFinished:
+ break
+ close = getattr(appIterator, 'close', None)
+ if close is not None:
+ close()
+ except:
+ def wsgiError(started, type, value, traceback):
+ err(Failure(value, type, traceback), "WSGI application error")
+ if started:
+ self.request.transport.loseConnection()
+ else:
+ self.request.setResponseCode(INTERNAL_SERVER_ERROR)
+ self.request.finish()
+ self.reactor.callFromThread(wsgiError, self.started, *exc_info())
+ else:
+ def wsgiFinish(started):
+ if not self._requestFinished:
+ if not started:
+ self._sendResponseHeaders()
+ self.request.finish()
+ self.reactor.callFromThread(wsgiFinish, self.started)
+ self.started = True
+
+
+
+class WSGIResource:
+ """
+ An L{IResource} implementation which delegates responsibility for all
+ resources hierarchically inferior to it to a WSGI application.
+
+ @ivar _reactor: An L{IReactorThreads} provider which will be passed on to
+ L{_WSGIResponse} to schedule calls in the I/O thread.
+
+ @ivar _threadpool: A L{ThreadPool} which will be passed on to
+ L{_WSGIResponse} to run the WSGI application object.
+
+ @ivar _application: The WSGI application object.
+ """
+ implements(IResource)
+
+ # Further resource segments are left up to the WSGI application object to
+ # handle.
+ isLeaf = True
+
+ def __init__(self, reactor, threadpool, application):
+ self._reactor = reactor
+ self._threadpool = threadpool
+ self._application = application
+
+
+ def render(self, request):
+ """
+ Turn the request into the appropriate C{environ} C{dict} suitable to be
+ passed to the WSGI application object and then pass it on.
+
+ The WSGI application object is given almost complete control of the
+ rendering process. C{NOT_DONE_YET} will always be returned in order
+ and response completion will be dictated by the application object, as
+ will the status, headers, and the response body.
+ """
+ response = _WSGIResponse(
+ self._reactor, self._threadpool, self._application, request)
+ response.start()
+ return NOT_DONE_YET
+
+
+ def getChildWithDefault(self, name, request):
+ """
+ Reject attempts to retrieve a child resource. All path segments beyond
+ the one which refers to this resource are handled by the WSGI
+ application object.
+ """
+ raise RuntimeError("Cannot get IResource children from WSGIResource")
+
+
+ def putChild(self, path, child):
+ """
+ Reject attempts to add a child resource to this resource. The WSGI
+ application object handles all path segments beneath this resource, so
+ L{IResource} children can never be found.
+ """
+ raise RuntimeError("Cannot put IResource children under WSGIResource")
+
+
+__all__ = ['WSGIResource']
diff --git a/twisted/web/xmlrpc.py b/twisted/web/xmlrpc.py
new file mode 100644
index 0000000..cbcb7b0
--- /dev/null
+++ b/twisted/web/xmlrpc.py
@@ -0,0 +1,590 @@
+# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A generic resource for publishing objects via XML-RPC.
+
+Maintainer: Itamar Shtull-Trauring
+"""
+
+# System Imports
+import sys, xmlrpclib, urlparse
+
+
+# Sibling Imports
+from twisted.web import resource, server, http
+from twisted.internet import defer, protocol, reactor
+from twisted.python import log, reflect, failure
+
+# These are deprecated, use the class level definitions
+NOT_FOUND = 8001
+FAILURE = 8002
+
+
+# Useful so people don't need to import xmlrpclib directly
+Fault = xmlrpclib.Fault
+Binary = xmlrpclib.Binary
+Boolean = xmlrpclib.Boolean
+DateTime = xmlrpclib.DateTime
+
+# On Python 2.4 and earlier, DateTime.decode returns unicode.
+if sys.version_info[:2] < (2, 5):
+ _decode = DateTime.decode
+ DateTime.decode = lambda self, value: _decode(self, value.encode('ascii'))
+
+
+def withRequest(f):
+ """
+ Decorator to cause the request to be passed as the first argument
+ to the method.
+
+ If an I{xmlrpc_} method is wrapped with C{withRequest}, the
+ request object is passed as the first argument to that method.
+ For example::
+
+ @withRequest
+ def xmlrpc_echo(self, request, s):
+ return s
+
+ @since: 10.2
+ """
+ f.withRequest = True
+ return f
+
+
+
+class NoSuchFunction(Fault):
+ """
+ There is no function by the given name.
+ """
+
+
+class Handler:
+ """
+ Handle a XML-RPC request and store the state for a request in progress.
+
+ Override the run() method and return result using self.result,
+ a Deferred.
+
+ We require this class since we're not using threads, so we can't
+ encapsulate state in a running function if we're going to have
+ to wait for results.
+
+ For example, lets say we want to authenticate against twisted.cred,
+ run a LDAP query and then pass its result to a database query, all
+ as a result of a single XML-RPC command. We'd use a Handler instance
+ to store the state of the running command.
+ """
+
+ def __init__(self, resource, *args):
+ self.resource = resource # the XML-RPC resource we are connected to
+ self.result = defer.Deferred()
+ self.run(*args)
+
+ def run(self, *args):
+ # event driven equivalent of 'raise UnimplementedError'
+ self.result.errback(
+ NotImplementedError("Implement run() in subclasses"))
+
+
+class XMLRPC(resource.Resource):
+ """
+ A resource that implements XML-RPC.
+
+ You probably want to connect this to '/RPC2'.
+
+ Methods published can return XML-RPC serializable results, Faults,
+ Binary, Boolean, DateTime, Deferreds, or Handler instances.
+
+ By default methods beginning with 'xmlrpc_' are published.
+
+ Sub-handlers for prefixed methods (e.g., system.listMethods)
+ can be added with putSubHandler. By default, prefixes are
+ separated with a '.'. Override self.separator to change this.
+
+ @ivar allowNone: Permit XML translating of Python constant None.
+ @type allowNone: C{bool}
+
+ @ivar useDateTime: Present datetime values as datetime.datetime objects?
+ Requires Python >= 2.5.
+ @type useDateTime: C{bool}
+ """
+
+ # Error codes for Twisted, if they conflict with yours then
+ # modify them at runtime.
+ NOT_FOUND = 8001
+ FAILURE = 8002
+
+ isLeaf = 1
+ separator = '.'
+ allowedMethods = ('POST',)
+
+ def __init__(self, allowNone=False, useDateTime=False):
+ resource.Resource.__init__(self)
+ self.subHandlers = {}
+ self.allowNone = allowNone
+ self.useDateTime = useDateTime
+
+
+ def __setattr__(self, name, value):
+ if name == "useDateTime" and value and sys.version_info[:2] < (2, 5):
+ raise RuntimeError("useDateTime requires Python 2.5 or later.")
+ self.__dict__[name] = value
+
+
+ def putSubHandler(self, prefix, handler):
+ self.subHandlers[prefix] = handler
+
+ def getSubHandler(self, prefix):
+ return self.subHandlers.get(prefix, None)
+
+ def getSubHandlerPrefixes(self):
+ return self.subHandlers.keys()
+
+ def render_POST(self, request):
+ request.content.seek(0, 0)
+ request.setHeader("content-type", "text/xml")
+ try:
+ if self.useDateTime:
+ args, functionPath = xmlrpclib.loads(request.content.read(),
+ use_datetime=True)
+ else:
+ # Maintain backwards compatibility with Python < 2.5
+ args, functionPath = xmlrpclib.loads(request.content.read())
+ except Exception, e:
+ f = Fault(self.FAILURE, "Can't deserialize input: %s" % (e,))
+ self._cbRender(f, request)
+ else:
+ try:
+ function = self.lookupProcedure(functionPath)
+ except Fault, f:
+ self._cbRender(f, request)
+ else:
+ # Use this list to track whether the response has failed or not.
+ # This will be used later on to decide if the result of the
+ # Deferred should be written out and Request.finish called.
+ responseFailed = []
+ request.notifyFinish().addErrback(responseFailed.append)
+ if getattr(function, 'withRequest', False):
+ d = defer.maybeDeferred(function, request, *args)
+ else:
+ d = defer.maybeDeferred(function, *args)
+ d.addErrback(self._ebRender)
+ d.addCallback(self._cbRender, request, responseFailed)
+ return server.NOT_DONE_YET
+
+
+ def _cbRender(self, result, request, responseFailed=None):
+ if responseFailed:
+ return
+
+ if isinstance(result, Handler):
+ result = result.result
+ if not isinstance(result, Fault):
+ result = (result,)
+ try:
+ try:
+ content = xmlrpclib.dumps(
+ result, methodresponse=True,
+ allow_none=self.allowNone)
+ except Exception, e:
+ f = Fault(self.FAILURE, "Can't serialize output: %s" % (e,))
+ content = xmlrpclib.dumps(f, methodresponse=True,
+ allow_none=self.allowNone)
+
+ request.setHeader("content-length", str(len(content)))
+ request.write(content)
+ except:
+ log.err()
+ request.finish()
+
+
+ def _ebRender(self, failure):
+ if isinstance(failure.value, Fault):
+ return failure.value
+ log.err(failure)
+ return Fault(self.FAILURE, "error")
+
+
+ def lookupProcedure(self, procedurePath):
+ """
+ Given a string naming a procedure, return a callable object for that
+ procedure or raise NoSuchFunction.
+
+ The returned object will be called, and should return the result of the
+ procedure, a Deferred, or a Fault instance.
+
+ Override in subclasses if you want your own policy. The base
+ implementation that given C{'foo'}, C{self.xmlrpc_foo} will be returned.
+ If C{procedurePath} contains C{self.separator}, the sub-handler for the
+ initial prefix is used to search for the remaining path.
+
+ If you override C{lookupProcedure}, you may also want to override
+ C{listProcedures} to accurately report the procedures supported by your
+ resource, so that clients using the I{system.listMethods} procedure
+ receive accurate results.
+
+ @since: 11.1
+ """
+ if procedurePath.find(self.separator) != -1:
+ prefix, procedurePath = procedurePath.split(self.separator, 1)
+ handler = self.getSubHandler(prefix)
+ if handler is None:
+ raise NoSuchFunction(self.NOT_FOUND,
+ "no such subHandler %s" % prefix)
+ return handler.lookupProcedure(procedurePath)
+
+ f = getattr(self, "xmlrpc_%s" % procedurePath, None)
+ if not f:
+ raise NoSuchFunction(self.NOT_FOUND,
+ "procedure %s not found" % procedurePath)
+ elif not callable(f):
+ raise NoSuchFunction(self.NOT_FOUND,
+ "procedure %s not callable" % procedurePath)
+ else:
+ return f
+
+ def listProcedures(self):
+ """
+ Return a list of the names of all xmlrpc procedures.
+
+ @since: 11.1
+ """
+ return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_')
+
+
+class XMLRPCIntrospection(XMLRPC):
+ """
+ Implement the XML-RPC Introspection API.
+
+ By default, the methodHelp method returns the 'help' method attribute,
+ if it exists, otherwise the __doc__ method attribute, if it exists,
+ otherwise the empty string.
+
+ To enable the methodSignature method, add a 'signature' method attribute
+ containing a list of lists. See methodSignature's documentation for the
+ format. Note the type strings should be XML-RPC types, not Python types.
+ """
+
+ def __init__(self, parent):
+ """
+ Implement Introspection support for an XMLRPC server.
+
+ @param parent: the XMLRPC server to add Introspection support to.
+ @type parent: L{XMLRPC}
+ """
+ XMLRPC.__init__(self)
+ self._xmlrpc_parent = parent
+
+ def xmlrpc_listMethods(self):
+ """
+ Return a list of the method names implemented by this server.
+ """
+ functions = []
+ todo = [(self._xmlrpc_parent, '')]
+ while todo:
+ obj, prefix = todo.pop(0)
+ functions.extend([prefix + name for name in obj.listProcedures()])
+ todo.extend([ (obj.getSubHandler(name),
+ prefix + name + obj.separator)
+ for name in obj.getSubHandlerPrefixes() ])
+ return functions
+
+ xmlrpc_listMethods.signature = [['array']]
+
+ def xmlrpc_methodHelp(self, method):
+ """
+ Return a documentation string describing the use of the given method.
+ """
+ method = self._xmlrpc_parent.lookupProcedure(method)
+ return (getattr(method, 'help', None)
+ or getattr(method, '__doc__', None) or '')
+
+ xmlrpc_methodHelp.signature = [['string', 'string']]
+
+ def xmlrpc_methodSignature(self, method):
+ """
+ Return a list of type signatures.
+
+ Each type signature is a list of the form [rtype, type1, type2, ...]
+ where rtype is the return type and typeN is the type of the Nth
+ argument. If no signature information is available, the empty
+ string is returned.
+ """
+ method = self._xmlrpc_parent.lookupProcedure(method)
+ return getattr(method, 'signature', None) or ''
+
+ xmlrpc_methodSignature.signature = [['array', 'string'],
+ ['string', 'string']]
+
+
+def addIntrospection(xmlrpc):
+ """
+ Add Introspection support to an XMLRPC server.
+
+ @param parent: the XMLRPC server to add Introspection support to.
+ @type parent: L{XMLRPC}
+ """
+ xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc))
+
+
+class QueryProtocol(http.HTTPClient):
+
+ def connectionMade(self):
+ self._response = None
+ self.sendCommand('POST', self.factory.path)
+ self.sendHeader('User-Agent', 'Twisted/XMLRPClib')
+ self.sendHeader('Host', self.factory.host)
+ self.sendHeader('Content-type', 'text/xml')
+ self.sendHeader('Content-length', str(len(self.factory.payload)))
+ if self.factory.user:
+ auth = '%s:%s' % (self.factory.user, self.factory.password)
+ auth = auth.encode('base64').strip()
+ self.sendHeader('Authorization', 'Basic %s' % (auth,))
+ self.endHeaders()
+ self.transport.write(self.factory.payload)
+
+ def handleStatus(self, version, status, message):
+ if status != '200':
+ self.factory.badStatus(status, message)
+
+ def handleResponse(self, contents):
+ """
+ Handle the XML-RPC response received from the server.
+
+ Specifically, disconnect from the server and store the XML-RPC
+ response so that it can be properly handled when the disconnect is
+ finished.
+ """
+ self.transport.loseConnection()
+ self._response = contents
+
+ def connectionLost(self, reason):
+ """
+ The connection to the server has been lost.
+
+ If we have a full response from the server, then parse it and fired a
+ Deferred with the return value or C{Fault} that the server gave us.
+ """
+ http.HTTPClient.connectionLost(self, reason)
+ if self._response is not None:
+ response, self._response = self._response, None
+ self.factory.parseResponse(response)
+
+
+payloadTemplate = """<?xml version="1.0"?>
+<methodCall>
+<methodName>%s</methodName>
+%s
+</methodCall>
+"""
+
+
+class _QueryFactory(protocol.ClientFactory):
+ """
+ XML-RPC Client Factory
+
+ @ivar path: The path portion of the URL to which to post method calls.
+ @type path: C{str}
+
+ @ivar host: The value to use for the Host HTTP header.
+ @type host: C{str}
+
+ @ivar user: The username with which to authenticate with the server
+ when making calls.
+ @type user: C{str} or C{NoneType}
+
+ @ivar password: The password with which to authenticate with the server
+ when making calls.
+ @type password: C{str} or C{NoneType}
+
+ @ivar useDateTime: Accept datetime values as datetime.datetime objects.
+ also passed to the underlying xmlrpclib implementation. Default to
+ False. Requires Python >= 2.5.
+ @type useDateTime: C{bool}
+ """
+
+ deferred = None
+ protocol = QueryProtocol
+
+ def __init__(self, path, host, method, user=None, password=None,
+ allowNone=False, args=(), canceller=None, useDateTime=False):
+ """
+ @param method: The name of the method to call.
+ @type method: C{str}
+
+ @param allowNone: allow the use of None values in parameters. It's
+ passed to the underlying xmlrpclib implementation. Default to False.
+ @type allowNone: C{bool} or C{NoneType}
+
+ @param args: the arguments to pass to the method.
+ @type args: C{tuple}
+
+ @param canceller: A 1-argument callable passed to the deferred as the
+ canceller callback.
+ @type canceller: callable or C{NoneType}
+ """
+ self.path, self.host = path, host
+ self.user, self.password = user, password
+ self.payload = payloadTemplate % (method,
+ xmlrpclib.dumps(args, allow_none=allowNone))
+ self.deferred = defer.Deferred(canceller)
+ self.useDateTime = useDateTime
+
+ def parseResponse(self, contents):
+ if not self.deferred:
+ return
+ try:
+ if self.useDateTime:
+ response = xmlrpclib.loads(contents,
+ use_datetime=True)[0][0]
+ else:
+ # Maintain backwards compatibility with Python < 2.5
+ response = xmlrpclib.loads(contents)[0][0]
+ except:
+ deferred, self.deferred = self.deferred, None
+ deferred.errback(failure.Failure())
+ else:
+ deferred, self.deferred = self.deferred, None
+ deferred.callback(response)
+
+ def clientConnectionLost(self, _, reason):
+ if self.deferred is not None:
+ deferred, self.deferred = self.deferred, None
+ deferred.errback(reason)
+
+ clientConnectionFailed = clientConnectionLost
+
+ def badStatus(self, status, message):
+ deferred, self.deferred = self.deferred, None
+ deferred.errback(ValueError(status, message))
+
+
+
+class Proxy:
+ """
+ A Proxy for making remote XML-RPC calls.
+
+ Pass the URL of the remote XML-RPC server to the constructor.
+
+ Use proxy.callRemote('foobar', *args) to call remote method
+ 'foobar' with *args.
+
+ @ivar user: The username with which to authenticate with the server
+ when making calls. If specified, overrides any username information
+ embedded in C{url}. If not specified, a value may be taken from
+ C{url} if present.
+ @type user: C{str} or C{NoneType}
+
+ @ivar password: The password with which to authenticate with the server
+ when making calls. If specified, overrides any password information
+ embedded in C{url}. If not specified, a value may be taken from
+ C{url} if present.
+ @type password: C{str} or C{NoneType}
+
+ @ivar allowNone: allow the use of None values in parameters. It's
+ passed to the underlying xmlrpclib implementation. Default to False.
+ @type allowNone: C{bool} or C{NoneType}
+
+ @ivar useDateTime: Accept datetime values as datetime.datetime objects.
+ also passed to the underlying xmlrpclib implementation. Default to
+ False. Requires Python >= 2.5.
+ @type useDateTime: C{bool}
+
+ @ivar connectTimeout: Number of seconds to wait before assuming the
+ connection has failed.
+ @type connectTimeout: C{float}
+
+ @ivar _reactor: the reactor used to create connections.
+ @type _reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
+
+ @ivar queryFactory: object returning a factory for XML-RPC protocol. Mainly
+ useful for tests.
+ """
+ queryFactory = _QueryFactory
+
+ def __init__(self, url, user=None, password=None, allowNone=False,
+ useDateTime=False, connectTimeout=30.0, reactor=reactor):
+ """
+ @param url: The URL to which to post method calls. Calls will be made
+ over SSL if the scheme is HTTPS. If netloc contains username or
+ password information, these will be used to authenticate, as long as
+ the C{user} and C{password} arguments are not specified.
+ @type url: C{str}
+
+ """
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
+ netlocParts = netloc.split('@')
+ if len(netlocParts) == 2:
+ userpass = netlocParts.pop(0).split(':')
+ self.user = userpass.pop(0)
+ try:
+ self.password = userpass.pop(0)
+ except:
+ self.password = None
+ else:
+ self.user = self.password = None
+ hostport = netlocParts[0].split(':')
+ self.host = hostport.pop(0)
+ try:
+ self.port = int(hostport.pop(0))
+ except:
+ self.port = None
+ self.path = path
+ if self.path in ['', None]:
+ self.path = '/'
+ self.secure = (scheme == 'https')
+ if user is not None:
+ self.user = user
+ if password is not None:
+ self.password = password
+ self.allowNone = allowNone
+ self.useDateTime = useDateTime
+ self.connectTimeout = connectTimeout
+ self._reactor = reactor
+
+
+ def __setattr__(self, name, value):
+ if name == "useDateTime" and value and sys.version_info[:2] < (2, 5):
+ raise RuntimeError("useDateTime requires Python 2.5 or later.")
+ self.__dict__[name] = value
+
+
+ def callRemote(self, method, *args):
+ """
+ Call remote XML-RPC C{method} with given arguments.
+
+ @return: a L{defer.Deferred} that will fire with the method response,
+ or a failure if the method failed. Generally, the failure type will
+ be L{Fault}, but you can also have an C{IndexError} on some buggy
+ servers giving empty responses.
+
+ If the deferred is cancelled before the request completes, the
+ connection is closed and the deferred will fire with a
+ L{defer.CancelledError}.
+ """
+ def cancel(d):
+ factory.deferred = None
+ connector.disconnect()
+ factory = self.queryFactory(
+ self.path, self.host, method, self.user,
+ self.password, self.allowNone, args, cancel, self.useDateTime)
+
+ if self.secure:
+ from twisted.internet import ssl
+ connector = self._reactor.connectSSL(
+ self.host, self.port or 443,
+ factory, ssl.ClientContextFactory(),
+ timeout=self.connectTimeout)
+ else:
+ connector = self._reactor.connectTCP(
+ self.host, self.port or 80, factory,
+ timeout=self.connectTimeout)
+ return factory.deferred
+
+
+__all__ = [
+ "XMLRPC", "Handler", "NoSuchFunction", "Proxy",
+
+ "Fault", "Binary", "Boolean", "DateTime"]
diff --git a/twisted/words/__init__.py b/twisted/words/__init__.py
new file mode 100644
index 0000000..3e38d15
--- /dev/null
+++ b/twisted/words/__init__.py
@@ -0,0 +1,10 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Twisted Words: a Twisted Chat service.
+"""
+
+from twisted.words._version import version
+__version__ = version.short()
diff --git a/twisted/words/_version.py b/twisted/words/_version.py
new file mode 100644
index 0000000..20c489b
--- /dev/null
+++ b/twisted/words/_version.py
@@ -0,0 +1,3 @@
+# This is an auto-generated file. Do not edit it.
+from twisted.python import versions
+version = versions.Version('twisted.words', 12, 1, 0)
diff --git a/twisted/words/ewords.py b/twisted/words/ewords.py
new file mode 100644
index 0000000..7621a71
--- /dev/null
+++ b/twisted/words/ewords.py
@@ -0,0 +1,34 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""Exception definitions for Words
+"""
+
+class WordsError(Exception):
+ def __str__(self):
+ return self.__class__.__name__ + ': ' + Exception.__str__(self)
+
+class NoSuchUser(WordsError):
+ pass
+
+
+class DuplicateUser(WordsError):
+ pass
+
+
+class NoSuchGroup(WordsError):
+ pass
+
+
+class DuplicateGroup(WordsError):
+ pass
+
+
+class AlreadyLoggedIn(WordsError):
+ pass
+
+__all__ = [
+ 'WordsError', 'NoSuchUser', 'DuplicateUser',
+ 'NoSuchGroup', 'DuplicateGroup', 'AlreadyLoggedIn',
+ ]
diff --git a/twisted/words/im/__init__.py b/twisted/words/im/__init__.py
new file mode 100644
index 0000000..cf3492b
--- /dev/null
+++ b/twisted/words/im/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""Instance Messenger, Pan-protocol chat client."""
+
diff --git a/twisted/words/im/baseaccount.py b/twisted/words/im/baseaccount.py
new file mode 100644
index 0000000..0261dbf
--- /dev/null
+++ b/twisted/words/im/baseaccount.py
@@ -0,0 +1,62 @@
+# -*- Python -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+
+class AccountManager:
+ """I am responsible for managing a user's accounts.
+
+ That is, remembering what accounts are available, their settings,
+ adding and removal of accounts, etc.
+
+ @ivar accounts: A collection of available accounts.
+ @type accounts: mapping of strings to L{Account<interfaces.IAccount>}s.
+ """
+ def __init__(self):
+ self.accounts = {}
+
+ def getSnapShot(self):
+ """A snapshot of all the accounts and their status.
+
+ @returns: A list of tuples, each of the form
+ (string:accountName, boolean:isOnline,
+ boolean:autoLogin, string:gatewayType)
+ """
+ data = []
+ for account in self.accounts.values():
+ data.append((account.accountName, account.isOnline(),
+ account.autoLogin, account.gatewayType))
+ return data
+
+ def isEmpty(self):
+ return len(self.accounts) == 0
+
+ def getConnectionInfo(self):
+ connectioninfo = []
+ for account in self.accounts.values():
+ connectioninfo.append(account.isOnline())
+ return connectioninfo
+
+ def addAccount(self, account):
+ self.accounts[account.accountName] = account
+
+ def delAccount(self, accountName):
+ del self.accounts[accountName]
+
+ def connect(self, accountName, chatui):
+ """
+ @returntype: Deferred L{interfaces.IClient}
+ """
+ return self.accounts[accountName].logOn(chatui)
+
+ def disconnect(self, accountName):
+ pass
+ #self.accounts[accountName].logOff() - not yet implemented
+
+ def quit(self):
+ pass
+ #for account in self.accounts.values():
+ # account.logOff() - not yet implemented
diff --git a/twisted/words/im/basechat.py b/twisted/words/im/basechat.py
new file mode 100644
index 0000000..39ead71
--- /dev/null
+++ b/twisted/words/im/basechat.py
@@ -0,0 +1,512 @@
+# -*- test-case-name: twisted.words.test.test_basechat -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Base classes for Instance Messenger clients.
+"""
+
+from twisted.words.im.locals import OFFLINE, ONLINE, AWAY
+
+
+class ContactsList:
+ """
+ A GUI object that displays a contacts list.
+
+ @ivar chatui: The GUI chat client associated with this contacts list.
+ @type chatui: L{ChatUI}
+
+ @ivar contacts: The contacts.
+ @type contacts: C{dict} mapping C{str} to a L{IPerson<interfaces.IPerson>}
+ provider
+
+ @ivar onlineContacts: The contacts who are currently online (have a status
+ that is not C{OFFLINE}).
+ @type onlineContacts: C{dict} mapping C{str} to a
+ L{IPerson<interfaces.IPerson>} provider
+
+ @ivar clients: The signed-on clients.
+ @type clients: C{list} of L{IClient<interfaces.IClient>} providers
+ """
+ def __init__(self, chatui):
+ """
+ @param chatui: The GUI chat client associated with this contacts list.
+ @type chatui: L{ChatUI}
+ """
+ self.chatui = chatui
+ self.contacts = {}
+ self.onlineContacts = {}
+ self.clients = []
+
+
+ def setContactStatus(self, person):
+ """
+ Inform the user that a person's status has changed.
+
+ @param person: The person whose status has changed.
+ @type person: L{IPerson<interfaces.IPerson>} provider
+ """
+ if not self.contacts.has_key(person.name):
+ self.contacts[person.name] = person
+ if not self.onlineContacts.has_key(person.name) and \
+ (person.status == ONLINE or person.status == AWAY):
+ self.onlineContacts[person.name] = person
+ if self.onlineContacts.has_key(person.name) and \
+ person.status == OFFLINE:
+ del self.onlineContacts[person.name]
+
+
+ def registerAccountClient(self, client):
+ """
+ Notify the user that an account client has been signed on to.
+
+ @param client: The client being added to your list of account clients.
+ @type client: L{IClient<interfaces.IClient>} provider
+ """
+ if not client in self.clients:
+ self.clients.append(client)
+
+
+ def unregisterAccountClient(self, client):
+ """
+ Notify the user that an account client has been signed off or
+ disconnected from.
+
+ @param client: The client being removed from the list of account
+ clients.
+ @type client: L{IClient<interfaces.IClient>} provider
+ """
+ if client in self.clients:
+ self.clients.remove(client)
+
+
+ def contactChangedNick(self, person, newnick):
+ """
+ Update your contact information to reflect a change to a contact's
+ nickname.
+
+ @param person: The person in your contacts list whose nickname is
+ changing.
+ @type person: L{IPerson<interfaces.IPerson>} provider
+
+ @param newnick: The new nickname for this person.
+ @type newnick: C{str}
+ """
+ oldname = person.name
+ if self.contacts.has_key(oldname):
+ del self.contacts[oldname]
+ person.name = newnick
+ self.contacts[newnick] = person
+ if self.onlineContacts.has_key(oldname):
+ del self.onlineContacts[oldname]
+ self.onlineContacts[newnick] = person
+
+
+
+class Conversation:
+ """
+ A GUI window of a conversation with a specific person.
+
+ @ivar person: The person who you're having this conversation with.
+ @type person: L{IPerson<interfaces.IPerson>} provider
+
+ @ivar chatui: The GUI chat client associated with this conversation.
+ @type chatui: L{ChatUI}
+ """
+ def __init__(self, person, chatui):
+ """
+ @param person: The person who you're having this conversation with.
+ @type person: L{IPerson<interfaces.IPerson>} provider
+
+ @param chatui: The GUI chat client associated with this conversation.
+ @type chatui: L{ChatUI}
+ """
+ self.chatui = chatui
+ self.person = person
+
+
+ def show(self):
+ """
+ Display the ConversationWindow.
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def hide(self):
+ """
+ Hide the ConversationWindow.
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def sendText(self, text):
+ """
+ Send text to the person with whom the user is conversing.
+
+ @param text: The text to be sent.
+ @type text: C{str}
+ """
+ self.person.sendMessage(text, None)
+
+
+ def showMessage(self, text, metadata=None):
+ """
+ Display a message sent from the person with whom the user is conversing.
+
+ @param text: The sent message.
+ @type text: C{str}
+
+ @param metadata: Metadata associated with this message.
+ @type metadata: C{dict}
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def contactChangedNick(self, person, newnick):
+ """
+ Change a person's name.
+
+ @param person: The person whose nickname is changing.
+ @type person: L{IPerson<interfaces.IPerson>} provider
+
+ @param newnick: The new nickname for this person.
+ @type newnick: C{str}
+ """
+ self.person.name = newnick
+
+
+
+class GroupConversation:
+ """
+ A GUI window of a conversation with a group of people.
+
+ @ivar chatui: The GUI chat client associated with this conversation.
+ @type chatui: L{ChatUI}
+
+ @ivar group: The group of people that are having this conversation.
+ @type group: L{IGroup<interfaces.IGroup>} provider
+
+ @ivar members: The names of the people in this conversation.
+ @type members: C{list} of C{str}
+ """
+ def __init__(self, group, chatui):
+ """
+ @param chatui: The GUI chat client associated with this conversation.
+ @type chatui: L{ChatUI}
+
+ @param group: The group of people that are having this conversation.
+ @type group: L{IGroup<interfaces.IGroup>} provider
+ """
+ self.chatui = chatui
+ self.group = group
+ self.members = []
+
+
+ def show(self):
+ """
+ Display the GroupConversationWindow.
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def hide(self):
+ """
+ Hide the GroupConversationWindow.
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def sendText(self, text):
+ """
+ Send text to the group.
+
+ @param: The text to be sent.
+ @type text: C{str}
+ """
+ self.group.sendGroupMessage(text, None)
+
+
+ def showGroupMessage(self, sender, text, metadata=None):
+ """
+ Display to the user a message sent to this group from the given sender.
+
+ @param sender: The person sending the message.
+ @type sender: C{str}
+
+ @param text: The sent message.
+ @type text: C{str}
+
+ @param metadata: Metadata associated with this message.
+ @type metadata: C{dict}
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def setGroupMembers(self, members):
+ """
+ Set the list of members in the group.
+
+ @param members: The names of the people that will be in this group.
+ @type members: C{list} of C{str}
+ """
+ self.members = members
+
+
+ def setTopic(self, topic, author):
+ """
+ Change the topic for the group conversation window and display this
+ change to the user.
+
+ @param topic: This group's topic.
+ @type topic: C{str}
+
+ @param author: The person changing the topic.
+ @type author: C{str}
+ """
+ raise NotImplementedError("Subclasses must implement this method")
+
+
+ def memberJoined(self, member):
+ """
+ Add the given member to the list of members in the group conversation
+ and displays this to the user.
+
+ @param member: The person joining the group conversation.
+ @type member: C{str}
+ """
+ if not member in self.members:
+ self.members.append(member)
+
+
+ def memberChangedNick(self, oldnick, newnick):
+ """
+ Change the nickname for a member of the group conversation and displays
+ this change to the user.
+
+ @param oldnick: The old nickname.
+ @type oldnick: C{str}
+
+ @param newnick: The new nickname.
+ @type newnick: C{str}
+ """
+ if oldnick in self.members:
+ self.members.remove(oldnick)
+ self.members.append(newnick)
+
+
+ def memberLeft(self, member):
+ """
+ Delete the given member from the list of members in the group
+ conversation and displays the change to the user.
+
+ @param member: The person leaving the group conversation.
+ @type member: C{str}
+ """
+ if member in self.members:
+ self.members.remove(member)
+
+
+
+class ChatUI:
+ """
+ A GUI chat client.
+
+ @type conversations: C{dict} of L{Conversation}
+ @ivar conversations: A cache of all the direct windows.
+
+ @type groupConversations: C{dict} of L{GroupConversation}
+ @ivar groupConversations: A cache of all the group windows.
+
+ @type persons: C{dict} with keys that are a C{tuple} of (C{str},
+ L{IAccount<interfaces.IAccount>} provider) and values that are
+ L{IPerson<interfaces.IPerson>} provider
+ @ivar persons: A cache of all the users associated with this client.
+
+ @type groups: C{dict} with keys that are a C{tuple} of (C{str},
+ L{IAccount<interfaces.IAccount>} provider) and values that are
+ L{IGroup<interfaces.IGroup>} provider
+ @ivar groups: A cache of all the groups associated with this client.
+
+ @type onlineClients: C{list} of L{IClient<interfaces.IClient>} providers
+ @ivar onlineClients: A list of message sources currently online.
+
+ @type contactsList: L{ContactsList}
+ @ivar contactsList: A contacts list.
+ """
+ def __init__(self):
+ self.conversations = {}
+ self.groupConversations = {}
+ self.persons = {}
+ self.groups = {}
+ self.onlineClients = []
+ self.contactsList = ContactsList(self)
+
+
+ def registerAccountClient(self, client):
+ """
+ Notify the user that an account has been signed on to.
+
+ @type client: L{IClient<interfaces.IClient>} provider
+ @param client: The client account for the person who has just signed on.
+
+ @rtype client: L{IClient<interfaces.IClient>} provider
+ @return: The client, so that it may be used in a callback chain.
+ """
+ self.onlineClients.append(client)
+ self.contactsList.registerAccountClient(client)
+ return client
+
+
+ def unregisterAccountClient(self, client):
+ """
+ Notify the user that an account has been signed off or disconnected.
+
+ @type client: L{IClient<interfaces.IClient>} provider
+ @param client: The client account for the person who has just signed
+ off.
+ """
+ self.onlineClients.remove(client)
+ self.contactsList.unregisterAccountClient(client)
+
+
+ def getContactsList(self):
+ """
+ Get the contacts list associated with this chat window.
+
+ @rtype: L{ContactsList}
+ @return: The contacts list associated with this chat window.
+ """
+ return self.contactsList
+
+
+ def getConversation(self, person, Class=Conversation, stayHidden=False):
+ """
+ For the given person object, return the conversation window or create
+ and return a new conversation window if one does not exist.
+
+ @type person: L{IPerson<interfaces.IPerson>} provider
+ @param person: The person whose conversation window we want to get.
+
+ @type Class: L{IConversation<interfaces.IConversation>} implementor
+ @param: The kind of conversation window we want. If the conversation
+ window for this person didn't already exist, create one of this type.
+
+ @type stayHidden: C{bool}
+ @param stayHidden: Whether or not the conversation window should stay
+ hidden.
+
+ @rtype: L{IConversation<interfaces.IConversation>} provider
+ @return: The conversation window.
+ """
+ conv = self.conversations.get(person)
+ if not conv:
+ conv = Class(person, self)
+ self.conversations[person] = conv
+ if stayHidden:
+ conv.hide()
+ else:
+ conv.show()
+ return conv
+
+
+ def getGroupConversation(self, group, Class=GroupConversation,
+ stayHidden=False):
+ """
+ For the given group object, return the group conversation window or
+ create and return a new group conversation window if it doesn't exist.
+
+ @type group: L{IGroup<interfaces.IGroup>} provider
+ @param group: The group whose conversation window we want to get.
+
+ @type Class: L{IConversation<interfaces.IConversation>} implementor
+ @param: The kind of conversation window we want. If the conversation
+ window for this person didn't already exist, create one of this type.
+
+ @type stayHidden: C{bool}
+ @param stayHidden: Whether or not the conversation window should stay
+ hidden.
+
+ @rtype: L{IGroupConversation<interfaces.IGroupConversation>} provider
+ @return: The group conversation window.
+ """
+ conv = self.groupConversations.get(group)
+ if not conv:
+ conv = Class(group, self)
+ self.groupConversations[group] = conv
+ if stayHidden:
+ conv.hide()
+ else:
+ conv.show()
+ return conv
+
+
+ def getPerson(self, name, client):
+ """
+ For the given name and account client, return an instance of a
+ L{IGroup<interfaces.IPerson>} provider or create and return a new
+ instance of a L{IGroup<interfaces.IPerson>} provider.
+
+ @type name: C{str}
+ @param name: The name of the person of interest.
+
+ @type client: L{IClient<interfaces.IClient>} provider
+ @param client: The client account of interest.
+
+ @rtype: L{IPerson<interfaces.IPerson>} provider
+ @return: The person with that C{name}.
+ """
+ account = client.account
+ p = self.persons.get((name, account))
+ if not p:
+ p = account.getPerson(name)
+ self.persons[name, account] = p
+ return p
+
+
+ def getGroup(self, name, client):
+ """
+ For the given name and account client, return an instance of a
+ L{IGroup<interfaces.IGroup>} provider or create and return a new instance
+ of a L{IGroup<interfaces.IGroup>} provider.
+
+ @type name: C{str}
+ @param name: The name of the group of interest.
+
+ @type client: L{IClient<interfaces.IClient>} provider
+ @param client: The client account of interest.
+
+ @rtype: L{IGroup<interfaces.IGroup>} provider
+ @return: The group with that C{name}.
+ """
+ # I accept 'client' instead of 'account' in my signature for
+ # backwards compatibility. (Groups changed to be Account-oriented
+ # in CVS revision 1.8.)
+ account = client.account
+ g = self.groups.get((name, account))
+ if not g:
+ g = account.getGroup(name)
+ self.groups[name, account] = g
+ return g
+
+
+ def contactChangedNick(self, person, newnick):
+ """
+ For the given C{person}, change the C{person}'s C{name} to C{newnick}
+ and tell the contact list and any conversation windows with that
+ C{person} to change as well.
+
+ @type person: L{IPerson<interfaces.IPerson>} provider
+ @param person: The person whose nickname will get changed.
+
+ @type newnick: C{str}
+ @param newnick: The new C{name} C{person} will take.
+ """
+ oldnick = person.name
+ if self.persons.has_key((oldnick, person.account)):
+ conv = self.conversations.get(person)
+ if conv:
+ conv.contactChangedNick(person, newnick)
+ self.contactsList.contactChangedNick(person, newnick)
+ del self.persons[oldnick, person.account]
+ person.name = newnick
+ self.persons[person.name, person.account] = person
diff --git a/twisted/words/im/basesupport.py b/twisted/words/im/basesupport.py
new file mode 100644
index 0000000..5c8b424
--- /dev/null
+++ b/twisted/words/im/basesupport.py
@@ -0,0 +1,270 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+#
+
+"""Instance Messenger base classes for protocol support.
+
+You will find these useful if you're adding a new protocol to IM.
+"""
+
+# Abstract representation of chat "model" classes
+
+from twisted.words.im.locals import ONLINE, OFFLINE, OfflineError
+from twisted.words.im import interfaces
+
+from twisted.internet.protocol import Protocol
+
+from twisted.python.reflect import prefixedMethods
+from twisted.persisted import styles
+
+from twisted.internet import error
+
+class AbstractGroup:
+ def __init__(self, name, account):
+ self.name = name
+ self.account = account
+
+ def getGroupCommands(self):
+ """finds group commands
+
+ these commands are methods on me that start with imgroup_; they are
+ called with no arguments
+ """
+ return prefixedMethods(self, "imgroup_")
+
+ def getTargetCommands(self, target):
+ """finds group commands
+
+ these commands are methods on me that start with imgroup_; they are
+ called with a user present within this room as an argument
+
+ you may want to override this in your group in order to filter for
+ appropriate commands on the given user
+ """
+ return prefixedMethods(self, "imtarget_")
+
+ def join(self):
+ if not self.account.client:
+ raise OfflineError
+ self.account.client.joinGroup(self.name)
+
+ def leave(self):
+ if not self.account.client:
+ raise OfflineError
+ self.account.client.leaveGroup(self.name)
+
+ def __repr__(self):
+ return '<%s %r>' % (self.__class__, self.name)
+
+ def __str__(self):
+ return '%s@%s' % (self.name, self.account.accountName)
+
+class AbstractPerson:
+ def __init__(self, name, baseAccount):
+ self.name = name
+ self.account = baseAccount
+ self.status = OFFLINE
+
+ def getPersonCommands(self):
+ """finds person commands
+
+ these commands are methods on me that start with imperson_; they are
+ called with no arguments
+ """
+ return prefixedMethods(self, "imperson_")
+
+ def getIdleTime(self):
+ """
+ Returns a string.
+ """
+ return '--'
+
+ def __repr__(self):
+ return '<%s %r/%s>' % (self.__class__, self.name, self.status)
+
+ def __str__(self):
+ return '%s@%s' % (self.name, self.account.accountName)
+
+class AbstractClientMixin:
+ """Designed to be mixed in to a Protocol implementing class.
+
+ Inherit from me first.
+
+ @ivar _logonDeferred: Fired when I am done logging in.
+ """
+ def __init__(self, account, chatui, logonDeferred):
+ for base in self.__class__.__bases__:
+ if issubclass(base, Protocol):
+ self.__class__._protoBase = base
+ break
+ else:
+ pass
+ self.account = account
+ self.chat = chatui
+ self._logonDeferred = logonDeferred
+
+ def connectionMade(self):
+ self._protoBase.connectionMade(self)
+
+ def connectionLost(self, reason):
+ self.account._clientLost(self, reason)
+ self.unregisterAsAccountClient()
+ return self._protoBase.connectionLost(self, reason)
+
+ def unregisterAsAccountClient(self):
+ """Tell the chat UI that I have `signed off'.
+ """
+ self.chat.unregisterAccountClient(self)
+
+
+class AbstractAccount(styles.Versioned):
+ """Base class for Accounts.
+
+ I am the start of an implementation of L{IAccount<interfaces.IAccount>}, I
+ implement L{isOnline} and most of L{logOn}, though you'll need to implement
+ L{_startLogOn} in a subclass.
+
+ @cvar _groupFactory: A Callable that will return a L{IGroup} appropriate
+ for this account type.
+ @cvar _personFactory: A Callable that will return a L{IPerson} appropriate
+ for this account type.
+
+ @type _isConnecting: boolean
+ @ivar _isConnecting: Whether I am in the process of establishing a
+ connection to the server.
+ @type _isOnline: boolean
+ @ivar _isOnline: Whether I am currently on-line with the server.
+
+ @ivar accountName:
+ @ivar autoLogin:
+ @ivar username:
+ @ivar password:
+ @ivar host:
+ @ivar port:
+ """
+
+ _isOnline = 0
+ _isConnecting = 0
+ client = None
+
+ _groupFactory = AbstractGroup
+ _personFactory = AbstractPerson
+
+ persistanceVersion = 2
+
+ def __init__(self, accountName, autoLogin, username, password, host, port):
+ self.accountName = accountName
+ self.autoLogin = autoLogin
+ self.username = username
+ self.password = password
+ self.host = host
+ self.port = port
+
+ self._groups = {}
+ self._persons = {}
+
+ def upgrateToVersion2(self):
+ # Added in CVS revision 1.16.
+ for k in ('_groups', '_persons'):
+ if not hasattr(self, k):
+ setattr(self, k, {})
+
+ def __getstate__(self):
+ state = styles.Versioned.__getstate__(self)
+ for k in ('client', '_isOnline', '_isConnecting'):
+ try:
+ del state[k]
+ except KeyError:
+ pass
+ return state
+
+ def isOnline(self):
+ return self._isOnline
+
+ def logOn(self, chatui):
+ """Log on to this account.
+
+ Takes care to not start a connection if a connection is
+ already in progress. You will need to implement
+ L{_startLogOn} for this to work, and it would be a good idea
+ to override L{_loginFailed} too.
+
+ @returntype: Deferred L{interfaces.IClient}
+ """
+ if (not self._isConnecting) and (not self._isOnline):
+ self._isConnecting = 1
+ d = self._startLogOn(chatui)
+ d.addCallback(self._cb_logOn)
+ # if chatui is not None:
+ # (I don't particularly like having to pass chatUI to this function,
+ # but we haven't factored it out yet.)
+ d.addCallback(chatui.registerAccountClient)
+ d.addErrback(self._loginFailed)
+ return d
+ else:
+ raise error.ConnectError("Connection in progress")
+
+ def getGroup(self, name):
+ """Group factory.
+
+ @param name: Name of the group on this account.
+ @type name: string
+ """
+ group = self._groups.get(name)
+ if group is None:
+ group = self._groupFactory(name, self)
+ self._groups[name] = group
+ return group
+
+ def getPerson(self, name):
+ """Person factory.
+
+ @param name: Name of the person on this account.
+ @type name: string
+ """
+ person = self._persons.get(name)
+ if person is None:
+ person = self._personFactory(name, self)
+ self._persons[name] = person
+ return person
+
+ def _startLogOn(self, chatui):
+ """Start the sign on process.
+
+ Factored out of L{logOn}.
+
+ @returntype: Deferred L{interfaces.IClient}
+ """
+ raise NotImplementedError()
+
+ def _cb_logOn(self, client):
+ self._isConnecting = 0
+ self._isOnline = 1
+ self.client = client
+ return client
+
+ def _loginFailed(self, reason):
+ """Errorback for L{logOn}.
+
+ @type reason: Failure
+
+ @returns: I{reason}, for further processing in the callback chain.
+ @returntype: Failure
+ """
+ self._isConnecting = 0
+ self._isOnline = 0 # just in case
+ return reason
+
+ def _clientLost(self, client, reason):
+ self.client = None
+ self._isConnecting = 0
+ self._isOnline = 0
+ return reason
+
+ def __repr__(self):
+ return "<%s: %s (%s@%s:%s)>" % (self.__class__,
+ self.accountName,
+ self.username,
+ self.host,
+ self.port)
diff --git a/twisted/words/im/instancemessenger.glade b/twisted/words/im/instancemessenger.glade
new file mode 100644
index 0000000..33ffaa2
--- /dev/null
+++ b/twisted/words/im/instancemessenger.glade
@@ -0,0 +1,3165 @@
+<?xml version="1.0"?>
+<GTK-Interface>
+
+<project>
+ <name>InstanceMessenger</name>
+ <program_name>instancemessenger</program_name>
+ <directory></directory>
+ <source_directory>src</source_directory>
+ <pixmaps_directory>pixmaps</pixmaps_directory>
+ <language>C</language>
+ <gnome_support>True</gnome_support>
+ <gettext_support>True</gettext_support>
+ <use_widget_names>True</use_widget_names>
+</project>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>UnseenConversationWindow</name>
+ <visible>False</visible>
+ <title>Unseen Conversation Window</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>ConversationWidget</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkVPaned</class>
+ <name>vpaned1</name>
+ <handle_size>10</handle_size>
+ <gutter_size>6</gutter_size>
+ <position>0</position>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow10</name>
+ <hscrollbar_policy>GTK_POLICY_NEVER</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <shrink>False</shrink>
+ <resize>True</resize>
+ </child>
+
+ <widget>
+ <class>GtkText</class>
+ <name>ConversationOutput</name>
+ <editable>False</editable>
+ <text></text>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow11</name>
+ <hscrollbar_policy>GTK_POLICY_NEVER</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <shrink>True</shrink>
+ <resize>False</resize>
+ </child>
+
+ <widget>
+ <class>GtkText</class>
+ <name>ConversationMessageEntry</name>
+ <can_focus>True</can_focus>
+ <has_focus>True</has_focus>
+ <signal>
+ <name>key_press_event</name>
+ <handler>handle_key_press_event</handler>
+ <last_modification_time>Tue, 29 Jan 2002 12:42:58 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text></text>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox9</name>
+ <homogeneous>True</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>3</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button42</name>
+ <can_focus>True</can_focus>
+ <label> Send Message </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>3</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>AddRemoveContact</name>
+ <can_focus>True</can_focus>
+ <label> Add Contact </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>3</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>CloseContact</name>
+ <can_focus>True</can_focus>
+ <label> Close </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>3</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>MainIMWindow</name>
+ <signal>
+ <name>destroy</name>
+ <handler>on_MainIMWindow_destroy</handler>
+ <last_modification_time>Sun, 21 Jul 2002 08:16:08 GMT</last_modification_time>
+ </signal>
+ <title>Instance Messenger</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>True</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkNotebook</class>
+ <name>ContactsNotebook</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>key_press_event</name>
+ <handler>on_ContactsWidget_key_press_event</handler>
+ <last_modification_time>Tue, 07 May 2002 03:02:33 GMT</last_modification_time>
+ </signal>
+ <show_tabs>True</show_tabs>
+ <show_border>True</show_border>
+ <tab_pos>GTK_POS_TOP</tab_pos>
+ <scrollable>False</scrollable>
+ <tab_hborder>2</tab_hborder>
+ <tab_vborder>2</tab_vborder>
+ <popup_enable>False</popup_enable>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox11</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>OnlineCount</name>
+ <label>Online: %d</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow14</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCTree</class>
+ <name>OnlineContactsTree</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>tree_select_row</name>
+ <handler>on_OnlineContactsTree_tree_select_row</handler>
+ <last_modification_time>Tue, 07 May 2002 03:06:32 GMT</last_modification_time>
+ </signal>
+ <signal>
+ <name>select_row</name>
+ <handler>on_OnlineContactsTree_select_row</handler>
+ <last_modification_time>Tue, 07 May 2002 04:36:10 GMT</last_modification_time>
+ </signal>
+ <columns>4</columns>
+ <column_widths>109,35,23,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>True</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CTree:title</child_name>
+ <name>label77</name>
+ <label>Alias</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CTree:title</child_name>
+ <name>label78</name>
+ <label>Status</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CTree:title</child_name>
+ <name>label79</name>
+ <label>Idle</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CTree:title</child_name>
+ <name>label80</name>
+ <label>Account</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox30</name>
+ <homogeneous>False</homogeneous>
+ <spacing>2</spacing>
+ <child>
+ <padding>1</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ContactNameEntry</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>activate</name>
+ <handler>on_ContactNameEntry_activate</handler>
+ <last_modification_time>Tue, 07 May 2002 04:07:25 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkOptionMenu</class>
+ <name>AccountsListPopup</name>
+ <can_focus>True</can_focus>
+ <items>Nothing
+To
+Speak
+Of
+</items>
+ <initial_choice>1</initial_choice>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox7</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>PlainSendIM</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_PlainSendIM_clicked</handler>
+ <last_modification_time>Tue, 29 Jan 2002 03:17:35 GMT</last_modification_time>
+ </signal>
+ <label> Send IM </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>PlainGetInfo</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_PlainGetInfo_clicked</handler>
+ <last_modification_time>Tue, 07 May 2002 04:06:59 GMT</last_modification_time>
+ </signal>
+ <label> Get Info </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>PlainJoinChat</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_PlainJoinChat_clicked</handler>
+ <last_modification_time>Tue, 29 Jan 2002 13:04:49 GMT</last_modification_time>
+ </signal>
+ <label> Join Group </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>PlainGoAway</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_PlainGoAway_clicked</handler>
+ <last_modification_time>Tue, 07 May 2002 04:06:53 GMT</last_modification_time>
+ </signal>
+ <label> Go Away </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox8</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>AddContactButton</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_AddContactButton_clicked</handler>
+ <last_modification_time>Tue, 07 May 2002 04:06:33 GMT</last_modification_time>
+ </signal>
+ <label> Add Contact </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>RemoveContactButton</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_RemoveContactButton_clicked</handler>
+ <last_modification_time>Tue, 07 May 2002 04:06:28 GMT</last_modification_time>
+ </signal>
+ <label> Remove Contact </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>Notebook:tab</child_name>
+ <name>label35</name>
+ <label> Online Contacts </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox14</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>OfflineContactsScroll</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>OfflineContactsList</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>select_row</name>
+ <handler>on_OfflineContactsList_select_row</handler>
+ <last_modification_time>Tue, 07 May 2002 03:00:07 GMT</last_modification_time>
+ </signal>
+ <columns>4</columns>
+ <column_widths>66,80,80,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>True</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label41</name>
+ <label>Contact</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label42</name>
+ <label>Account</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label43</name>
+ <label>Alias</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label44</name>
+ <label>Group</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>Notebook:tab</child_name>
+ <name>label36</name>
+ <label> All Contacts </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>AccountManWidget</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow12</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>accountsList</name>
+ <can_focus>True</can_focus>
+ <columns>4</columns>
+ <column_widths>80,36,34,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>True</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label45</name>
+ <label>Service Name</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label46</name>
+ <label>Online</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label47</name>
+ <label>Auto</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label48</name>
+ <label>Gateway</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>table5</name>
+ <rows>2</rows>
+ <columns>3</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+ <child>
+ <padding>3</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>NewAccountButton</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_NewAccountButton_clicked</handler>
+ <last_modification_time>Sun, 27 Jan 2002 10:32:20 GMT</last_modification_time>
+ </signal>
+ <label>New Account</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button46</name>
+ <sensitive>False</sensitive>
+ <can_default>True</can_default>
+ <label>Modify Account</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>LogOnButton</name>
+ <can_default>True</can_default>
+ <has_default>True</has_default>
+ <can_focus>True</can_focus>
+ <has_focus>True</has_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_LogOnButton_clicked</handler>
+ <last_modification_time>Mon, 28 Jan 2002 04:06:23 GMT</last_modification_time>
+ </signal>
+ <label>Logon</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>2</left_attach>
+ <right_attach>3</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>DeleteAccountButton</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_DeleteAccountButton_clicked</handler>
+ <last_modification_time>Mon, 28 Jan 2002 00:18:22 GMT</last_modification_time>
+ </signal>
+ <label>Delete Account</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>2</left_attach>
+ <right_attach>3</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>ConsoleButton</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_ConsoleButton_clicked</handler>
+ <last_modification_time>Mon, 29 Apr 2002 09:13:32 GMT</last_modification_time>
+ </signal>
+ <label>Console</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button75</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <label>Quit</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>True</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>True</yfill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>Notebook:tab</child_name>
+ <name>label107</name>
+ <label>Accounts</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>UnseenGroupWindow</name>
+ <visible>False</visible>
+ <title>Unseen Group Window</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>GroupChatBox</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox5</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>TopicEntry</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>activate</name>
+ <handler>on_TopicEntry_activate</handler>
+ <last_modification_time>Sat, 23 Feb 2002 02:57:41 GMT</last_modification_time>
+ </signal>
+ <signal>
+ <name>focus_out_event</name>
+ <handler>on_TopicEntry_focus_out_event</handler>
+ <last_modification_time>Sun, 21 Jul 2002 09:36:54 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>&lt;TOPIC NOT RECEIVED&gt;</text>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>AuthorLabel</name>
+ <label>&lt;nobody&gt;</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>HideButton</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_HideButton_clicked</handler>
+ <last_modification_time>Tue, 29 Jan 2002 14:10:00 GMT</last_modification_time>
+ </signal>
+ <label>&lt;</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkVPaned</class>
+ <name>vpaned2</name>
+ <handle_size>10</handle_size>
+ <gutter_size>6</gutter_size>
+ <position>0</position>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkHPaned</class>
+ <name>GroupHPaned</name>
+ <handle_size>6</handle_size>
+ <gutter_size>6</gutter_size>
+ <child>
+ <shrink>False</shrink>
+ <resize>True</resize>
+ </child>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow4</name>
+ <hscrollbar_policy>GTK_POLICY_NEVER</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <shrink>False</shrink>
+ <resize>True</resize>
+ </child>
+
+ <widget>
+ <class>GtkText</class>
+ <name>GroupOutput</name>
+ <can_focus>True</can_focus>
+ <editable>False</editable>
+ <text></text>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>actionvbox</name>
+ <width>110</width>
+ <homogeneous>False</homogeneous>
+ <spacing>1</spacing>
+ <child>
+ <shrink>True</shrink>
+ <resize>False</resize>
+ </child>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow5</name>
+ <hscrollbar_policy>GTK_POLICY_NEVER</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>ParticipantList</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>select_row</name>
+ <handler>on_ParticipantList_select_row</handler>
+ <last_modification_time>Sat, 13 Jul 2002 08:11:12 GMT</last_modification_time>
+ </signal>
+ <signal>
+ <name>unselect_row</name>
+ <handler>on_ParticipantList_unselect_row</handler>
+ <last_modification_time>Sat, 13 Jul 2002 08:23:25 GMT</last_modification_time>
+ </signal>
+ <columns>1</columns>
+ <column_widths>80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>False</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label18</name>
+ <label>Users</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame10</name>
+ <label>Group</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>GroupActionsBox</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>PersonFrame</name>
+ <label>Person</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>PersonActionsBox</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox6</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <shrink>True</shrink>
+ <resize>False</resize>
+ </child>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>NickLabel</name>
+ <label>&lt;no nick&gt;</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <padding>4</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow9</name>
+ <hscrollbar_policy>GTK_POLICY_NEVER</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkText</class>
+ <name>GroupInput</name>
+ <can_focus>True</can_focus>
+ <has_focus>True</has_focus>
+ <signal>
+ <name>key_press_event</name>
+ <handler>handle_key_press_event</handler>
+ <last_modification_time>Tue, 29 Jan 2002 12:41:03 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text></text>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>NewAccountWindow</name>
+ <border_width>3</border_width>
+ <visible>False</visible>
+ <signal>
+ <name>destroy</name>
+ <handler>on_NewAccountWindow_destroy</handler>
+ <last_modification_time>Sun, 27 Jan 2002 10:35:19 GMT</last_modification_time>
+ </signal>
+ <title>New Account</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>True</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox17</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox11</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>3</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label49</name>
+ <label>Gateway:</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkOptionMenu</class>
+ <name>GatewayOptionMenu</name>
+ <can_focus>True</can_focus>
+ <items>Twisted (Perspective Broker)
+Internet Relay Chat
+AIM (TOC)
+AIM (OSCAR)
+</items>
+ <initial_choice>0</initial_choice>
+ <child>
+ <padding>4</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>GatewayFrame</name>
+ <border_width>3</border_width>
+ <label>Gateway Options</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>Placeholder</class>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame2</name>
+ <border_width>3</border_width>
+ <label>Standard Options</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>table1</name>
+ <border_width>3</border_width>
+ <rows>2</rows>
+ <columns>2</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+
+ <widget>
+ <class>GtkCheckButton</class>
+ <name>AutoLogin</name>
+ <can_focus>True</can_focus>
+ <label>Automatically Log In</label>
+ <active>False</active>
+ <draw_indicator>True</draw_indicator>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>True</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>accountName</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>True</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label50</name>
+ <label> Auto-Login: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>True</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>True</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label51</name>
+ <label>Account Name: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>True</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>True</yfill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHButtonBox</class>
+ <name>hbuttonbox2</name>
+ <layout_style>GTK_BUTTONBOX_SPREAD</layout_style>
+ <spacing>30</spacing>
+ <child_min_width>85</child_min_width>
+ <child_min_height>27</child_min_height>
+ <child_ipad_x>7</child_ipad_x>
+ <child_ipad_y>0</child_ipad_y>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button50</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>createAccount</handler>
+ <last_modification_time>Sun, 27 Jan 2002 11:25:05 GMT</last_modification_time>
+ </signal>
+ <label>OK</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button51</name>
+ <can_default>True</can_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>destroyMe</handler>
+ <last_modification_time>Sun, 27 Jan 2002 11:27:12 GMT</last_modification_time>
+ </signal>
+ <label>Cancel</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>PBAccountWindow</name>
+ <visible>False</visible>
+ <title>PB Account Window</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>PBAccountWidget</name>
+ <border_width>4</border_width>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>table3</name>
+ <rows>4</rows>
+ <columns>2</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>hostname</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>twistedmatrix.com</text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>identity</name>
+ <can_focus>True</can_focus>
+ <has_focus>True</has_focus>
+ <signal>
+ <name>changed</name>
+ <handler>on_identity_changed</handler>
+ <last_modification_time>Sun, 27 Jan 2002 11:52:17 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label52</name>
+ <label> Hostname: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label54</name>
+ <label>Identity Name: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>password</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>False</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>portno</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>8787</text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label55</name>
+ <label> Password: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label53</name>
+ <label> Port Number: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame3</name>
+ <label>Perspectives</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox19</name>
+ <border_width>3</border_width>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow13</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_ALWAYS</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>serviceList</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>select_row</name>
+ <handler>on_serviceList_select_row</handler>
+ <last_modification_time>Sun, 27 Jan 2002 12:04:38 GMT</last_modification_time>
+ </signal>
+ <columns>3</columns>
+ <column_widths>80,80,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>True</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label60</name>
+ <label>Service Type</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label61</name>
+ <label>Service Name</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label62</name>
+ <label>Perspective Name</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>table4</name>
+ <rows>3</rows>
+ <columns>2</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label63</name>
+ <label>Perspective Name: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label59</name>
+ <label> Service Type: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkCombo</class>
+ <name>serviceCombo</name>
+ <value_in_list>False</value_in_list>
+ <ok_if_empty>True</ok_if_empty>
+ <case_sensitive>False</case_sensitive>
+ <use_arrows>True</use_arrows>
+ <use_arrows_always>False</use_arrows_always>
+ <items>twisted.words
+twisted.reality
+twisted.manhole
+</items>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+
+ <widget>
+ <class>GtkEntry</class>
+ <child_name>GtkCombo:entry</child_name>
+ <name>serviceType</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>changed</name>
+ <handler>on_serviceType_changed</handler>
+ <last_modification_time>Sun, 27 Jan 2002 11:49:07 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>twisted.words</text>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label64</name>
+ <label> Service Name: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>serviceName</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>perspectiveName</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox13</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button53</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>addPerspective</handler>
+ <last_modification_time>Mon, 28 Jan 2002 01:07:15 GMT</last_modification_time>
+ </signal>
+ <label> Add Perspective </label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button54</name>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>removePerspective</handler>
+ <last_modification_time>Sun, 27 Jan 2002 11:34:36 GMT</last_modification_time>
+ </signal>
+ <label>Remove Perspective</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>IRCAccountWindow</name>
+ <title>IRC Account Window</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>IRCAccountWidget</name>
+ <rows>5</rows>
+ <columns>2</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label65</name>
+ <label> Nickname: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label66</name>
+ <label> Server: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label67</name>
+ <label> Port: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label68</name>
+ <label> Channels: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label69</name>
+ <label> Password: </label>
+ <justify>GTK_JUSTIFY_RIGHT</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>4</top_attach>
+ <bottom_attach>5</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ircNick</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ircServer</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ircPort</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>6667</text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ircChannels</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>ircPassword</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>4</top_attach>
+ <bottom_attach>5</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>TOCAccountWindow</name>
+ <title>TOC Account Window</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkTable</class>
+ <name>TOCAccountWidget</name>
+ <rows>4</rows>
+ <columns>2</columns>
+ <homogeneous>False</homogeneous>
+ <row_spacing>0</row_spacing>
+ <column_spacing>0</column_spacing>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label70</name>
+ <label> Screen Name: </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label71</name>
+ <label> Password: </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label72</name>
+ <label> Host: </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label73</name>
+ <label> Port: </label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <left_attach>0</left_attach>
+ <right_attach>1</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>False</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>TOCName</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>0</top_attach>
+ <bottom_attach>1</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>TOCPass</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>False</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>1</top_attach>
+ <bottom_attach>2</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>TOCHost</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>toc.oscar.aol.com</text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>2</top_attach>
+ <bottom_attach>3</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>TOCPort</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text>9898</text>
+ <child>
+ <left_attach>1</left_attach>
+ <right_attach>2</right_attach>
+ <top_attach>3</top_attach>
+ <bottom_attach>4</bottom_attach>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <xexpand>True</xexpand>
+ <yexpand>False</yexpand>
+ <xshrink>False</xshrink>
+ <yshrink>False</yshrink>
+ <xfill>True</xfill>
+ <yfill>False</yfill>
+ </child>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>JoinGroupWindow</name>
+ <border_width>5</border_width>
+ <visible>False</visible>
+ <title>Group to Join</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox20</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkOptionMenu</class>
+ <name>AccountSelector</name>
+ <can_focus>True</can_focus>
+ <items>None
+In
+Particular
+</items>
+ <initial_choice>0</initial_choice>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox15</name>
+ <homogeneous>False</homogeneous>
+ <spacing>5</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>GroupNameEntry</name>
+ <can_focus>True</can_focus>
+ <has_focus>True</has_focus>
+ <signal>
+ <name>activate</name>
+ <handler>on_GroupJoinButton_clicked</handler>
+ <last_modification_time>Tue, 29 Jan 2002 13:27:18 GMT</last_modification_time>
+ </signal>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>GroupJoinButton</name>
+ <can_default>True</can_default>
+ <has_default>True</has_default>
+ <can_focus>True</can_focus>
+ <signal>
+ <name>clicked</name>
+ <handler>on_GroupJoinButton_clicked</handler>
+ <last_modification_time>Tue, 29 Jan 2002 13:16:50 GMT</last_modification_time>
+ </signal>
+ <label>Join</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+<widget>
+ <class>GtkWindow</class>
+ <name>UnifiedWindow</name>
+ <title>Twisted Instance Messenger</title>
+ <type>GTK_WINDOW_TOPLEVEL</type>
+ <position>GTK_WIN_POS_NONE</position>
+ <modal>False</modal>
+ <allow_shrink>False</allow_shrink>
+ <allow_grow>True</allow_grow>
+ <auto_shrink>False</auto_shrink>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox25</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox28</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button74</name>
+ <can_focus>True</can_focus>
+ <label>&gt;</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkEntry</class>
+ <name>entry3</name>
+ <can_focus>True</can_focus>
+ <editable>True</editable>
+ <text_visible>True</text_visible>
+ <text_max_length>0</text_max_length>
+ <text></text>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkOptionMenu</class>
+ <name>optionmenu3</name>
+ <items>List
+Of
+Online
+Accounts
+</items>
+ <initial_choice>0</initial_choice>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkOptionMenu</class>
+ <name>optionmenu4</name>
+ <can_focus>True</can_focus>
+ <items>Contact
+Person
+Group
+Account
+</items>
+ <initial_choice>0</initial_choice>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHPaned</class>
+ <name>hpaned1</name>
+ <handle_size>10</handle_size>
+ <gutter_size>6</gutter_size>
+ <position>0</position>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox26</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+ <child>
+ <shrink>True</shrink>
+ <resize>False</resize>
+ </child>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame7</name>
+ <border_width>2</border_width>
+ <label>Accounts</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox27</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow18</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>clist4</name>
+ <columns>4</columns>
+ <column_widths>18,25,25,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>False</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label95</name>
+ <label>label87</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label96</name>
+ <label>label88</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label97</name>
+ <label>label89</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label98</name>
+ <label>label90</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox23</name>
+ <homogeneous>True</homogeneous>
+ <spacing>2</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button65</name>
+ <label>New</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button66</name>
+ <label>Delete</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button67</name>
+ <label>Connect</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame8</name>
+ <border_width>2</border_width>
+ <label>Contacts</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox28</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow19</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>clist5</name>
+ <columns>3</columns>
+ <column_widths>18,17,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>False</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label99</name>
+ <label>label84</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label100</name>
+ <label>label85</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label101</name>
+ <label>label86</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox24</name>
+ <homogeneous>True</homogeneous>
+ <spacing>2</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button68</name>
+ <can_focus>True</can_focus>
+ <label>Talk</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button69</name>
+ <can_focus>True</can_focus>
+ <label>Info</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button70</name>
+ <can_focus>True</can_focus>
+ <label>Add</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button71</name>
+ <can_focus>True</can_focus>
+ <label>Remove</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkFrame</class>
+ <name>frame9</name>
+ <border_width>2</border_width>
+ <label>Groups</label>
+ <label_xalign>0</label_xalign>
+ <shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkVBox</class>
+ <name>vbox29</name>
+ <homogeneous>False</homogeneous>
+ <spacing>0</spacing>
+
+ <widget>
+ <class>GtkScrolledWindow</class>
+ <name>scrolledwindow20</name>
+ <hscrollbar_policy>GTK_POLICY_AUTOMATIC</hscrollbar_policy>
+ <vscrollbar_policy>GTK_POLICY_AUTOMATIC</vscrollbar_policy>
+ <hupdate_policy>GTK_UPDATE_CONTINUOUS</hupdate_policy>
+ <vupdate_policy>GTK_UPDATE_CONTINUOUS</vupdate_policy>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkCList</class>
+ <name>clist6</name>
+ <columns>3</columns>
+ <column_widths>21,75,80</column_widths>
+ <selection_mode>GTK_SELECTION_SINGLE</selection_mode>
+ <show_titles>False</show_titles>
+ <shadow_type>GTK_SHADOW_IN</shadow_type>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label102</name>
+ <label>label91</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label103</name>
+ <label>label92</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <child_name>CList:title</child_name>
+ <name>label104</name>
+ <label>label93</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHBox</class>
+ <name>hbox27</name>
+ <homogeneous>True</homogeneous>
+ <spacing>2</spacing>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>True</fill>
+ </child>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button72</name>
+ <label>Join</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkButton</class>
+ <name>button73</name>
+ <label>Leave</label>
+ <relief>GTK_RELIEF_NORMAL</relief>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkHSeparator</class>
+ <name>hseparator2</name>
+ <child>
+ <padding>0</padding>
+ <expand>True</expand>
+ <fill>True</fill>
+ </child>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label105</name>
+ <label>Twisted IM V. %s</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>3</ypad>
+ <child>
+ <padding>0</padding>
+ <expand>False</expand>
+ <fill>False</fill>
+ </child>
+ </widget>
+ </widget>
+
+ <widget>
+ <class>GtkLabel</class>
+ <name>label106</name>
+ <label>This
+Space
+Left
+Intentionally
+Blank
+(Here is where the UI for the currently
+selected element
+for interaction
+will go.)</label>
+ <justify>GTK_JUSTIFY_CENTER</justify>
+ <wrap>False</wrap>
+ <xalign>0.5</xalign>
+ <yalign>0.5</yalign>
+ <xpad>0</xpad>
+ <ypad>0</ypad>
+ <child>
+ <shrink>True</shrink>
+ <resize>True</resize>
+ </child>
+ </widget>
+ </widget>
+ </widget>
+</widget>
+
+</GTK-Interface>
diff --git a/twisted/words/im/interfaces.py b/twisted/words/im/interfaces.py
new file mode 100644
index 0000000..8f34fb1
--- /dev/null
+++ b/twisted/words/im/interfaces.py
@@ -0,0 +1,364 @@
+# -*- Python -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Pan-protocol chat client.
+"""
+
+from zope.interface import Interface, Attribute
+
+from twisted.words.im import locals
+
+# (Random musings, may not reflect on current state of code:)
+#
+# Accounts have Protocol components (clients)
+# Persons have Conversation components
+# Groups have GroupConversation components
+# Persons and Groups are associated with specific Accounts
+# At run-time, Clients/Accounts are slaved to a User Interface
+# (Note: User may be a bot, so don't assume all UIs are built on gui toolkits)
+
+
+class IAccount(Interface):
+ """
+ I represent a user's account with a chat service.
+ """
+
+ client = Attribute('The L{IClient} currently connecting to this account, if any.')
+ gatewayType = Attribute('A C{str} that identifies the protocol used by this account.')
+
+ def __init__(accountName, autoLogin, username, password, host, port):
+ """
+ @type accountName: string
+ @param accountName: A name to refer to the account by locally.
+ @type autoLogin: boolean
+ @type username: string
+ @type password: string
+ @type host: string
+ @type port: integer
+ """
+
+ def isOnline():
+ """
+ Am I online?
+
+ @rtype: boolean
+ """
+
+ def logOn(chatui):
+ """
+ Go on-line.
+
+ @type chatui: Implementor of C{IChatUI}
+
+ @rtype: L{Deferred} L{Client}
+ """
+
+ def logOff():
+ """
+ Sign off.
+ """
+
+ def getGroup(groupName):
+ """
+ @rtype: L{Group<IGroup>}
+ """
+
+ def getPerson(personName):
+ """
+ @rtype: L{Person<IPerson>}
+ """
+
+class IClient(Interface):
+
+ account = Attribute('The L{IAccount} I am a Client for')
+
+ def __init__(account, chatui, logonDeferred):
+ """
+ @type account: L{IAccount}
+ @type chatui: L{IChatUI}
+ @param logonDeferred: Will be called back once I am logged on.
+ @type logonDeferred: L{Deferred<twisted.internet.defer.Deferred>}
+ """
+
+ def joinGroup(groupName):
+ """
+ @param groupName: The name of the group to join.
+ @type groupName: string
+ """
+
+ def leaveGroup(groupName):
+ """
+ @param groupName: The name of the group to leave.
+ @type groupName: string
+ """
+
+ def getGroupConversation(name, hide=0):
+ pass
+
+ def getPerson(name):
+ pass
+
+
+class IPerson(Interface):
+
+ def __init__(name, account):
+ """
+ Initialize me.
+
+ @param name: My name, as the server knows me.
+ @type name: string
+ @param account: The account I am accessed through.
+ @type account: I{Account}
+ """
+
+ def isOnline():
+ """
+ Am I online right now?
+
+ @rtype: boolean
+ """
+
+ def getStatus():
+ """
+ What is my on-line status?
+
+ @return: L{locals.StatusEnum}
+ """
+
+ def getIdleTime():
+ """
+ @rtype: string (XXX: How about a scalar?)
+ """
+
+ def sendMessage(text, metadata=None):
+ """
+ Send a message to this person.
+
+ @type text: string
+ @type metadata: dict
+ """
+
+
+class IGroup(Interface):
+ """
+ A group which you may have a conversation with.
+
+ Groups generally have a loosely-defined set of members, who may
+ leave and join at any time.
+ """
+
+ name = Attribute('My C{str} name, as the server knows me.')
+ account = Attribute('The L{Account<IAccount>} I am accessed through.')
+
+ def __init__(name, account):
+ """
+ Initialize me.
+
+ @param name: My name, as the server knows me.
+ @type name: str
+ @param account: The account I am accessed through.
+ @type account: L{Account<IAccount>}
+ """
+
+ def setTopic(text):
+ """
+ Set this Groups topic on the server.
+
+ @type text: string
+ """
+
+ def sendGroupMessage(text, metadata=None):
+ """
+ Send a message to this group.
+
+ @type text: str
+
+ @type metadata: dict
+ @param metadata: Valid keys for this dictionary include:
+
+ - C{'style'}: associated with one of:
+ - C{'emote'}: indicates this is an action
+ """
+
+ def join():
+ """
+ Join this group.
+ """
+
+ def leave():
+ """
+ Depart this group.
+ """
+
+
+class IConversation(Interface):
+ """
+ A conversation with a specific person.
+ """
+
+ def __init__(person, chatui):
+ """
+ @type person: L{IPerson}
+ """
+
+ def show():
+ """
+ doesn't seem like it belongs in this interface.
+ """
+
+ def hide():
+ """
+ nor this neither.
+ """
+
+ def sendText(text, metadata):
+ pass
+
+ def showMessage(text, metadata):
+ pass
+
+ def changedNick(person, newnick):
+ """
+ @param person: XXX Shouldn't this always be Conversation.person?
+ """
+
+class IGroupConversation(Interface):
+
+ def show():
+ """
+ doesn't seem like it belongs in this interface.
+ """
+
+ def hide():
+ """
+ nor this neither.
+ """
+
+ def sendText(text, metadata):
+ pass
+
+ def showGroupMessage(sender, text, metadata):
+ pass
+
+ def setGroupMembers(members):
+ """
+ Sets the list of members in the group and displays it to the user.
+ """
+
+ def setTopic(topic, author):
+ """
+ Displays the topic (from the server) for the group conversation window.
+
+ @type topic: string
+ @type author: string (XXX: Not Person?)
+ """
+
+ def memberJoined(member):
+ """
+ Adds the given member to the list of members in the group conversation
+ and displays this to the user,
+
+ @type member: string (XXX: Not Person?)
+ """
+
+ def memberChangedNick(oldnick, newnick):
+ """
+ Changes the oldnick in the list of members to C{newnick} and displays this
+ change to the user,
+
+ @type oldnick: string (XXX: Not Person?)
+ @type newnick: string
+ """
+
+ def memberLeft(member):
+ """
+ Deletes the given member from the list of members in the group
+ conversation and displays the change to the user.
+
+ @type member: string (XXX: Not Person?)
+ """
+
+
+class IChatUI(Interface):
+
+ def registerAccountClient(client):
+ """
+ Notifies user that an account has been signed on to.
+
+ @type client: L{Client<IClient>}
+ """
+
+ def unregisterAccountClient(client):
+ """
+ Notifies user that an account has been signed off or disconnected.
+
+ @type client: L{Client<IClient>}
+ """
+
+ def getContactsList():
+ """
+ @rtype: L{ContactsList}
+ """
+
+ # WARNING: You'll want to be polymorphed into something with
+ # intrinsic stoning resistance before continuing.
+
+ def getConversation(person, Class, stayHidden=0):
+ """
+ For the given person object, returns the conversation window
+ or creates and returns a new conversation window if one does not exist.
+
+ @type person: L{Person<IPerson>}
+ @type Class: L{Conversation<IConversation>} class
+ @type stayHidden: boolean
+
+ @rtype: L{Conversation<IConversation>}
+ """
+
+ def getGroupConversation(group, Class, stayHidden=0):
+ """
+ For the given group object, returns the group conversation window or
+ creates and returns a new group conversation window if it doesn't exist.
+
+ @type group: L{Group<interfaces.IGroup>}
+ @type Class: L{Conversation<interfaces.IConversation>} class
+ @type stayHidden: boolean
+
+ @rtype: L{GroupConversation<interfaces.IGroupConversation>}
+ """
+
+ def getPerson(name, client):
+ """
+ Get a Person for a client.
+
+ Duplicates L{IAccount.getPerson}.
+
+ @type name: string
+ @type client: L{Client<IClient>}
+
+ @rtype: L{Person<IPerson>}
+ """
+
+ def getGroup(name, client):
+ """
+ Get a Group for a client.
+
+ Duplicates L{IAccount.getGroup}.
+
+ @type name: string
+ @type client: L{Client<IClient>}
+
+ @rtype: L{Group<IGroup>}
+ """
+
+ def contactChangedNick(oldnick, newnick):
+ """
+ For the given person, changes the person's name to newnick, and
+ tells the contact list and any conversation windows with that person
+ to change as well.
+
+ @type oldnick: string
+ @type newnick: string
+ """
diff --git a/twisted/words/im/ircsupport.py b/twisted/words/im/ircsupport.py
new file mode 100644
index 0000000..1feddeb
--- /dev/null
+++ b/twisted/words/im/ircsupport.py
@@ -0,0 +1,263 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+IRC support for Instance Messenger.
+"""
+
+import string
+
+from twisted.words.protocols import irc
+from twisted.words.im.locals import ONLINE
+from twisted.internet import defer, reactor, protocol
+from twisted.internet.defer import succeed
+from twisted.words.im import basesupport, interfaces, locals
+from zope.interface import implements
+
+
+class IRCPerson(basesupport.AbstractPerson):
+
+ def imperson_whois(self):
+ if self.account.client is None:
+ raise locals.OfflineError
+ self.account.client.sendLine("WHOIS %s" % self.name)
+
+ ### interface impl
+
+ def isOnline(self):
+ return ONLINE
+
+ def getStatus(self):
+ return ONLINE
+
+ def setStatus(self,status):
+ self.status=status
+ self.chat.getContactsList().setContactStatus(self)
+
+ def sendMessage(self, text, meta=None):
+ if self.account.client is None:
+ raise locals.OfflineError
+ for line in string.split(text, '\n'):
+ if meta and meta.get("style", None) == "emote":
+ self.account.client.ctcpMakeQuery(self.name,[('ACTION', line)])
+ else:
+ self.account.client.msg(self.name, line)
+ return succeed(text)
+
+class IRCGroup(basesupport.AbstractGroup):
+
+ implements(interfaces.IGroup)
+
+ def imgroup_testAction(self):
+ pass
+
+ def imtarget_kick(self, target):
+ if self.account.client is None:
+ raise locals.OfflineError
+ reason = "for great justice!"
+ self.account.client.sendLine("KICK #%s %s :%s" % (
+ self.name, target.name, reason))
+
+ ### Interface Implementation
+
+ def setTopic(self, topic):
+ if self.account.client is None:
+ raise locals.OfflineError
+ self.account.client.topic(self.name, topic)
+
+ def sendGroupMessage(self, text, meta={}):
+ if self.account.client is None:
+ raise locals.OfflineError
+ if meta and meta.get("style", None) == "emote":
+ self.account.client.me(self.name,text)
+ return succeed(text)
+ #standard shmandard, clients don't support plain escaped newlines!
+ for line in string.split(text, '\n'):
+ self.account.client.say(self.name, line)
+ return succeed(text)
+
+ def leave(self):
+ if self.account.client is None:
+ raise locals.OfflineError
+ self.account.client.leave(self.name)
+ self.account.client.getGroupConversation(self.name,1)
+
+
+class IRCProto(basesupport.AbstractClientMixin, irc.IRCClient):
+ def __init__(self, account, chatui, logonDeferred=None):
+ basesupport.AbstractClientMixin.__init__(self, account, chatui,
+ logonDeferred)
+ self._namreplies={}
+ self._ingroups={}
+ self._groups={}
+ self._topics={}
+
+ def getGroupConversation(self, name, hide=0):
+ name=string.lower(name)
+ return self.chat.getGroupConversation(self.chat.getGroup(name, self),
+ stayHidden=hide)
+
+ def getPerson(self,name):
+ return self.chat.getPerson(name, self)
+
+ def connectionMade(self):
+ # XXX: Why do I duplicate code in IRCClient.register?
+ try:
+ if self.account.password:
+ self.sendLine("PASS :%s" % self.account.password)
+ self.setNick(self.account.username)
+ self.sendLine("USER %s foo bar :Twisted-IM user" % (
+ self.account.username,))
+ for channel in self.account.channels:
+ self.joinGroup(channel)
+ self.account._isOnline=1
+ if self._logonDeferred is not None:
+ self._logonDeferred.callback(self)
+ self.chat.getContactsList()
+ except:
+ import traceback
+ traceback.print_exc()
+
+ def setNick(self,nick):
+ self.name=nick
+ self.accountName="%s (IRC)"%nick
+ irc.IRCClient.setNick(self,nick)
+
+ def kickedFrom(self, channel, kicker, message):
+ """
+ Called when I am kicked from a channel.
+ """
+ return self.chat.getGroupConversation(
+ self.chat.getGroup(channel[1:], self), 1)
+
+ def userKicked(self, kickee, channel, kicker, message):
+ pass
+
+ def noticed(self, username, channel, message):
+ self.privmsg(username, channel, message, {"dontAutoRespond": 1})
+
+ def privmsg(self, username, channel, message, metadata=None):
+ if metadata is None:
+ metadata = {}
+ username=string.split(username,'!',1)[0]
+ if username==self.name: return
+ if channel[0]=='#':
+ group=channel[1:]
+ self.getGroupConversation(group).showGroupMessage(username, message, metadata)
+ return
+ self.chat.getConversation(self.getPerson(username)).showMessage(message, metadata)
+
+ def action(self,username,channel,emote):
+ username=string.split(username,'!',1)[0]
+ if username==self.name: return
+ meta={'style':'emote'}
+ if channel[0]=='#':
+ group=channel[1:]
+ self.getGroupConversation(group).showGroupMessage(username, emote, meta)
+ return
+ self.chat.getConversation(self.getPerson(username)).showMessage(emote,meta)
+
+ def irc_RPL_NAMREPLY(self,prefix,params):
+ """
+ RPL_NAMREPLY
+ >> NAMES #bnl
+ << :Arlington.VA.US.Undernet.Org 353 z3p = #bnl :pSwede Dan-- SkOyg AG
+ """
+ group=string.lower(params[2][1:])
+ users=string.split(params[3])
+ for ui in range(len(users)):
+ while users[ui][0] in ["@","+"]: # channel modes
+ users[ui]=users[ui][1:]
+ if not self._namreplies.has_key(group):
+ self._namreplies[group]=[]
+ self._namreplies[group].extend(users)
+ for nickname in users:
+ try:
+ self._ingroups[nickname].append(group)
+ except:
+ self._ingroups[nickname]=[group]
+
+ def irc_RPL_ENDOFNAMES(self,prefix,params):
+ group=params[1][1:]
+ self.getGroupConversation(group).setGroupMembers(self._namreplies[string.lower(group)])
+ del self._namreplies[string.lower(group)]
+
+ def irc_RPL_TOPIC(self,prefix,params):
+ self._topics[params[1][1:]]=params[2]
+
+ def irc_333(self,prefix,params):
+ group=params[1][1:]
+ self.getGroupConversation(group).setTopic(self._topics[group],params[2])
+ del self._topics[group]
+
+ def irc_TOPIC(self,prefix,params):
+ nickname = string.split(prefix,"!")[0]
+ group = params[0][1:]
+ topic = params[1]
+ self.getGroupConversation(group).setTopic(topic,nickname)
+
+ def irc_JOIN(self,prefix,params):
+ nickname=string.split(prefix,"!")[0]
+ group=string.lower(params[0][1:])
+ if nickname!=self.nickname:
+ try:
+ self._ingroups[nickname].append(group)
+ except:
+ self._ingroups[nickname]=[group]
+ self.getGroupConversation(group).memberJoined(nickname)
+
+ def irc_PART(self,prefix,params):
+ nickname=string.split(prefix,"!")[0]
+ group=string.lower(params[0][1:])
+ if nickname!=self.nickname:
+ if group in self._ingroups[nickname]:
+ self._ingroups[nickname].remove(group)
+ self.getGroupConversation(group).memberLeft(nickname)
+
+ def irc_QUIT(self,prefix,params):
+ nickname=string.split(prefix,"!")[0]
+ if self._ingroups.has_key(nickname):
+ for group in self._ingroups[nickname]:
+ self.getGroupConversation(group).memberLeft(nickname)
+ self._ingroups[nickname]=[]
+
+ def irc_NICK(self, prefix, params):
+ fromNick = string.split(prefix, "!")[0]
+ toNick = params[0]
+ if not self._ingroups.has_key(fromNick):
+ return
+ for group in self._ingroups[fromNick]:
+ self.getGroupConversation(group).memberChangedNick(fromNick, toNick)
+ self._ingroups[toNick] = self._ingroups[fromNick]
+ del self._ingroups[fromNick]
+
+ def irc_unknown(self, prefix, command, params):
+ pass
+
+ # GTKIM calls
+ def joinGroup(self,name):
+ self.join(name)
+ self.getGroupConversation(name)
+
+class IRCAccount(basesupport.AbstractAccount):
+ implements(interfaces.IAccount)
+ gatewayType = "IRC"
+
+ _groupFactory = IRCGroup
+ _personFactory = IRCPerson
+
+ def __init__(self, accountName, autoLogin, username, password, host, port,
+ channels=''):
+ basesupport.AbstractAccount.__init__(self, accountName, autoLogin,
+ username, password, host, port)
+ self.channels = map(string.strip,string.split(channels,','))
+ if self.channels == ['']:
+ self.channels = []
+
+ def _startLogOn(self, chatui):
+ logonDeferred = defer.Deferred()
+ cc = protocol.ClientCreator(reactor, IRCProto, self, chatui,
+ logonDeferred)
+ d = cc.connectTCP(self.host, self.port)
+ d.addErrback(logonDeferred.errback)
+ return logonDeferred
diff --git a/twisted/words/im/locals.py b/twisted/words/im/locals.py
new file mode 100644
index 0000000..a63547a
--- /dev/null
+++ b/twisted/words/im/locals.py
@@ -0,0 +1,26 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+class Enum:
+ group = None
+
+ def __init__(self, label):
+ self.label = label
+
+ def __repr__(self):
+ return '<%s: %s>' % (self.group, self.label)
+
+ def __str__(self):
+ return self.label
+
+
+class StatusEnum(Enum):
+ group = 'Status'
+
+OFFLINE = Enum('Offline')
+ONLINE = Enum('Online')
+AWAY = Enum('Away')
+
+class OfflineError(Exception):
+ """The requested action can't happen while offline."""
diff --git a/twisted/words/im/pbsupport.py b/twisted/words/im/pbsupport.py
new file mode 100644
index 0000000..d3d469e
--- /dev/null
+++ b/twisted/words/im/pbsupport.py
@@ -0,0 +1,260 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""L{twisted.words} support for Instance Messenger."""
+
+from __future__ import nested_scopes
+
+from twisted.internet import defer
+from twisted.internet import error
+from twisted.python import log
+from twisted.python.failure import Failure
+from twisted.spread import pb
+
+from twisted.words.im.locals import ONLINE, OFFLINE, AWAY
+
+from twisted.words.im import basesupport, interfaces
+from zope.interface import implements
+
+
+class TwistedWordsPerson(basesupport.AbstractPerson):
+ """I a facade for a person you can talk to through a twisted.words service.
+ """
+ def __init__(self, name, wordsAccount):
+ basesupport.AbstractPerson.__init__(self, name, wordsAccount)
+ self.status = OFFLINE
+
+ def isOnline(self):
+ return ((self.status == ONLINE) or
+ (self.status == AWAY))
+
+ def getStatus(self):
+ return self.status
+
+ def sendMessage(self, text, metadata):
+ """Return a deferred...
+ """
+ if metadata:
+ d=self.account.client.perspective.directMessage(self.name,
+ text, metadata)
+ d.addErrback(self.metadataFailed, "* "+text)
+ return d
+ else:
+ return self.account.client.perspective.callRemote('directMessage',self.name, text)
+
+ def metadataFailed(self, result, text):
+ print "result:",result,"text:",text
+ return self.account.client.perspective.directMessage(self.name, text)
+
+ def setStatus(self, status):
+ self.status = status
+ self.chat.getContactsList().setContactStatus(self)
+
+class TwistedWordsGroup(basesupport.AbstractGroup):
+ implements(interfaces.IGroup)
+ def __init__(self, name, wordsClient):
+ basesupport.AbstractGroup.__init__(self, name, wordsClient)
+ self.joined = 0
+
+ def sendGroupMessage(self, text, metadata=None):
+ """Return a deferred.
+ """
+ #for backwards compatibility with older twisted.words servers.
+ if metadata:
+ d=self.account.client.perspective.callRemote(
+ 'groupMessage', self.name, text, metadata)
+ d.addErrback(self.metadataFailed, "* "+text)
+ return d
+ else:
+ return self.account.client.perspective.callRemote('groupMessage',
+ self.name, text)
+
+ def setTopic(self, text):
+ self.account.client.perspective.callRemote(
+ 'setGroupMetadata',
+ {'topic': text, 'topic_author': self.client.name},
+ self.name)
+
+ def metadataFailed(self, result, text):
+ print "result:",result,"text:",text
+ return self.account.client.perspective.callRemote('groupMessage',
+ self.name, text)
+
+ def joining(self):
+ self.joined = 1
+
+ def leaving(self):
+ self.joined = 0
+
+ def leave(self):
+ return self.account.client.perspective.callRemote('leaveGroup',
+ self.name)
+
+
+
+class TwistedWordsClient(pb.Referenceable, basesupport.AbstractClientMixin):
+ """In some cases, this acts as an Account, since it a source of text
+ messages (multiple Words instances may be on a single PB connection)
+ """
+ def __init__(self, acct, serviceName, perspectiveName, chatui,
+ _logonDeferred=None):
+ self.accountName = "%s (%s:%s)" % (acct.accountName, serviceName, perspectiveName)
+ self.name = perspectiveName
+ print "HELLO I AM A PB SERVICE", serviceName, perspectiveName
+ self.chat = chatui
+ self.account = acct
+ self._logonDeferred = _logonDeferred
+
+ def getPerson(self, name):
+ return self.chat.getPerson(name, self)
+
+ def getGroup(self, name):
+ return self.chat.getGroup(name, self)
+
+ def getGroupConversation(self, name):
+ return self.chat.getGroupConversation(self.getGroup(name))
+
+ def addContact(self, name):
+ self.perspective.callRemote('addContact', name)
+
+ def remote_receiveGroupMembers(self, names, group):
+ print 'received group members:', names, group
+ self.getGroupConversation(group).setGroupMembers(names)
+
+ def remote_receiveGroupMessage(self, sender, group, message, metadata=None):
+ print 'received a group message', sender, group, message, metadata
+ self.getGroupConversation(group).showGroupMessage(sender, message, metadata)
+
+ def remote_memberJoined(self, member, group):
+ print 'member joined', member, group
+ self.getGroupConversation(group).memberJoined(member)
+
+ def remote_memberLeft(self, member, group):
+ print 'member left'
+ self.getGroupConversation(group).memberLeft(member)
+
+ def remote_notifyStatusChanged(self, name, status):
+ self.chat.getPerson(name, self).setStatus(status)
+
+ def remote_receiveDirectMessage(self, name, message, metadata=None):
+ self.chat.getConversation(self.chat.getPerson(name, self)).showMessage(message, metadata)
+
+ def remote_receiveContactList(self, clist):
+ for name, status in clist:
+ self.chat.getPerson(name, self).setStatus(status)
+
+ def remote_setGroupMetadata(self, dict_, groupName):
+ if dict_.has_key("topic"):
+ self.getGroupConversation(groupName).setTopic(dict_["topic"], dict_.get("topic_author", None))
+
+ def joinGroup(self, name):
+ self.getGroup(name).joining()
+ return self.perspective.callRemote('joinGroup', name).addCallback(self._cbGroupJoined, name)
+
+ def leaveGroup(self, name):
+ self.getGroup(name).leaving()
+ return self.perspective.callRemote('leaveGroup', name).addCallback(self._cbGroupLeft, name)
+
+ def _cbGroupJoined(self, result, name):
+ groupConv = self.chat.getGroupConversation(self.getGroup(name))
+ groupConv.showGroupMessage("sys", "you joined")
+ self.perspective.callRemote('getGroupMembers', name)
+
+ def _cbGroupLeft(self, result, name):
+ print 'left',name
+ groupConv = self.chat.getGroupConversation(self.getGroup(name), 1)
+ groupConv.showGroupMessage("sys", "you left")
+
+ def connected(self, perspective):
+ print 'Connected Words Client!', perspective
+ if self._logonDeferred is not None:
+ self._logonDeferred.callback(self)
+ self.perspective = perspective
+ self.chat.getContactsList()
+
+
+pbFrontEnds = {
+ "twisted.words": TwistedWordsClient,
+ "twisted.reality": None
+ }
+
+
+class PBAccount(basesupport.AbstractAccount):
+ implements(interfaces.IAccount)
+ gatewayType = "PB"
+ _groupFactory = TwistedWordsGroup
+ _personFactory = TwistedWordsPerson
+
+ def __init__(self, accountName, autoLogin, username, password, host, port,
+ services=None):
+ """
+ @param username: The name of your PB Identity.
+ @type username: string
+ """
+ basesupport.AbstractAccount.__init__(self, accountName, autoLogin,
+ username, password, host, port)
+ self.services = []
+ if not services:
+ services = [('twisted.words', 'twisted.words', username)]
+ for serviceType, serviceName, perspectiveName in services:
+ self.services.append([pbFrontEnds[serviceType], serviceName,
+ perspectiveName])
+
+ def logOn(self, chatui):
+ """
+ @returns: this breaks with L{interfaces.IAccount}
+ @returntype: DeferredList of L{interfaces.IClient}s
+ """
+ # Overriding basesupport's implementation on account of the
+ # fact that _startLogOn tends to return a deferredList rather
+ # than a simple Deferred, and we need to do registerAccountClient.
+ if (not self._isConnecting) and (not self._isOnline):
+ self._isConnecting = 1
+ d = self._startLogOn(chatui)
+ d.addErrback(self._loginFailed)
+ def registerMany(results):
+ for success, result in results:
+ if success:
+ chatui.registerAccountClient(result)
+ self._cb_logOn(result)
+ else:
+ log.err(result)
+ d.addCallback(registerMany)
+ return d
+ else:
+ raise error.ConnectionError("Connection in progress")
+
+
+ def _startLogOn(self, chatui):
+ print 'Connecting...',
+ d = pb.getObjectAt(self.host, self.port)
+ d.addCallbacks(self._cbConnected, self._ebConnected,
+ callbackArgs=(chatui,))
+ return d
+
+ def _cbConnected(self, root, chatui):
+ print 'Connected!'
+ print 'Identifying...',
+ d = pb.authIdentity(root, self.username, self.password)
+ d.addCallbacks(self._cbIdent, self._ebConnected,
+ callbackArgs=(chatui,))
+ return d
+
+ def _cbIdent(self, ident, chatui):
+ if not ident:
+ print 'falsely identified.'
+ return self._ebConnected(Failure(Exception("username or password incorrect")))
+ print 'Identified!'
+ dl = []
+ for handlerClass, sname, pname in self.services:
+ d = defer.Deferred()
+ dl.append(d)
+ handler = handlerClass(self, sname, pname, chatui, d)
+ ident.callRemote('attach', sname, pname, handler).addCallback(handler.connected)
+ return defer.DeferredList(dl)
+
+ def _ebConnected(self, error):
+ print 'Not connected.'
+ return error
+
diff --git a/twisted/words/iwords.py b/twisted/words/iwords.py
new file mode 100644
index 0000000..c8ce09f
--- /dev/null
+++ b/twisted/words/iwords.py
@@ -0,0 +1,266 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import Interface, Attribute, implements
+
+class IProtocolPlugin(Interface):
+ """Interface for plugins providing an interface to a Words service
+ """
+
+ name = Attribute("A single word describing what kind of interface this is (eg, irc or web)")
+
+ def getFactory(realm, portal):
+ """Retrieve a C{twisted.internet.interfaces.IServerFactory} provider
+
+ @param realm: An object providing C{twisted.cred.portal.IRealm} and
+ C{IChatService}, with which service information should be looked up.
+
+ @param portal: An object providing C{twisted.cred.portal.IPortal},
+ through which logins should be performed.
+ """
+
+
+class IGroup(Interface):
+ name = Attribute("A short string, unique among groups.")
+
+ def add(user):
+ """Include the given user in this group.
+
+ @type user: L{IUser}
+ """
+
+ def remove(user, reason=None):
+ """Remove the given user from this group.
+
+ @type user: L{IUser}
+ @type reason: C{unicode}
+ """
+
+ def size():
+ """Return the number of participants in this group.
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with an C{int} representing the the
+ number of participants in this group.
+ """
+
+ def receive(sender, recipient, message):
+ """
+ Broadcast the given message from the given sender to other
+ users in group.
+
+ The message is not re-transmitted to the sender.
+
+ @param sender: L{IUser}
+
+ @type recipient: L{IGroup}
+ @param recipient: This is probably a wart. Maybe it will be removed
+ in the future. For now, it should be the group object the message
+ is being delivered to.
+
+ @param message: C{dict}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with None when delivery has been
+ attempted for all users.
+ """
+
+ def setMetadata(meta):
+ """Change the metadata associated with this group.
+
+ @type meta: C{dict}
+ """
+
+ def iterusers():
+ """Return an iterator of all users in this group.
+ """
+
+
+class IChatClient(Interface):
+ """Interface through which IChatService interacts with clients.
+ """
+
+ name = Attribute("A short string, unique among users. This will be set by the L{IChatService} at login time.")
+
+ def receive(sender, recipient, message):
+ """
+ Callback notifying this user of the given message sent by the
+ given user.
+
+ This will be invoked whenever another user sends a message to a
+ group this user is participating in, or whenever another user sends
+ a message directly to this user. In the former case, C{recipient}
+ will be the group to which the message was sent; in the latter, it
+ will be the same object as the user who is receiving the message.
+
+ @type sender: L{IUser}
+ @type recipient: L{IUser} or L{IGroup}
+ @type message: C{dict}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires when the message has been delivered,
+ or which fails in some way. If the Deferred fails and the message
+ was directed at a group, this user will be removed from that group.
+ """
+
+ def groupMetaUpdate(group, meta):
+ """
+ Callback notifying this user that the metadata for the given
+ group has changed.
+
+ @type group: L{IGroup}
+ @type meta: C{dict}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+
+ def userJoined(group, user):
+ """
+ Callback notifying this user that the given user has joined
+ the given group.
+
+ @type group: L{IGroup}
+ @type user: L{IUser}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+
+ def userLeft(group, user, reason=None):
+ """
+ Callback notifying this user that the given user has left the
+ given group for the given reason.
+
+ @type group: L{IGroup}
+ @type user: L{IUser}
+ @type reason: C{unicode}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+
+
+class IUser(Interface):
+ """Interface through which clients interact with IChatService.
+ """
+
+ realm = Attribute("A reference to the Realm to which this user belongs. Set if and only if the user is logged in.")
+ mind = Attribute("A reference to the mind which logged in to this user. Set if and only if the user is logged in.")
+ name = Attribute("A short string, unique among users.")
+
+ lastMessage = Attribute("A POSIX timestamp indicating the time of the last message received from this user.")
+ signOn = Attribute("A POSIX timestamp indicating this user's most recent sign on time.")
+
+ def loggedIn(realm, mind):
+ """Invoked by the associated L{IChatService} when login occurs.
+
+ @param realm: The L{IChatService} through which login is occurring.
+ @param mind: The mind object used for cred login.
+ """
+
+ def send(recipient, message):
+ """Send the given message to the given user or group.
+
+ @type recipient: Either L{IUser} or L{IGroup}
+ @type message: C{dict}
+ """
+
+ def join(group):
+ """Attempt to join the given group.
+
+ @type group: L{IGroup}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+
+ def leave(group):
+ """Discontinue participation in the given group.
+
+ @type group: L{IGroup}
+ @rtype: L{twisted.internet.defer.Deferred}
+ """
+
+ def itergroups():
+ """
+ Return an iterator of all groups of which this user is a
+ member.
+ """
+
+
+class IChatService(Interface):
+ name = Attribute("A short string identifying this chat service (eg, a hostname)")
+
+ createGroupOnRequest = Attribute(
+ "A boolean indicating whether L{getGroup} should implicitly "
+ "create groups which are requested but which do not yet exist.")
+
+ createUserOnRequest = Attribute(
+ "A boolean indicating whether L{getUser} should implicitly "
+ "create users which are requested but which do not yet exist.")
+
+ def itergroups():
+ """Return all groups available on this service.
+
+ @rtype: C{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with a list of C{IGroup} providers.
+ """
+
+ def getGroup(name):
+ """Retrieve the group by the given name.
+
+ @type name: C{str}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with the group with the given
+ name if one exists (or if one is created due to the setting of
+ L{createGroupOnRequest}, or which fails with
+ L{twisted.words.ewords.NoSuchGroup} if no such group exists.
+ """
+
+ def createGroup(name):
+ """Create a new group with the given name.
+
+ @type name: C{str}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with the created group, or
+ with fails with L{twisted.words.ewords.DuplicateGroup} if a
+ group by that name exists already.
+ """
+
+ def lookupGroup(name):
+ """Retrieve a group by name.
+
+ Unlike C{getGroup}, this will never implicitly create a group.
+
+ @type name: C{str}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with the group by the given
+ name, or which fails with L{twisted.words.ewords.NoSuchGroup}.
+ """
+
+ def getUser(name):
+ """Retrieve the user by the given name.
+
+ @type name: C{str}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with the user with the given
+ name if one exists (or if one is created due to the setting of
+ L{createUserOnRequest}, or which fails with
+ L{twisted.words.ewords.NoSuchUser} if no such user exists.
+ """
+
+ def createUser(name):
+ """Create a new user with the given name.
+
+ @type name: C{str}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with the created user, or
+ with fails with L{twisted.words.ewords.DuplicateUser} if a
+ user by that name exists already.
+ """
+
+__all__ = [
+ 'IChatInterface', 'IGroup', 'IChatClient', 'IUser', 'IChatService',
+ ]
diff --git a/twisted/words/protocols/__init__.py b/twisted/words/protocols/__init__.py
new file mode 100644
index 0000000..5b4f7e5
--- /dev/null
+++ b/twisted/words/protocols/__init__.py
@@ -0,0 +1 @@
+"Chat protocols"
diff --git a/twisted/words/protocols/irc.py b/twisted/words/protocols/irc.py
new file mode 100644
index 0000000..2bec909
--- /dev/null
+++ b/twisted/words/protocols/irc.py
@@ -0,0 +1,3302 @@
+# -*- test-case-name: twisted.words.test.test_irc -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Internet Relay Chat Protocol for client and server.
+
+Future Plans
+============
+
+The way the IRCClient class works here encourages people to implement
+IRC clients by subclassing the ephemeral protocol class, and it tends
+to end up with way more state than it should for an object which will
+be destroyed as soon as the TCP transport drops. Someone oughta do
+something about that, ya know?
+
+The DCC support needs to have more hooks for the client for it to be
+able to ask the user things like "Do you want to accept this session?"
+and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
+
+Test coverage needs to be better.
+
+@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC
+ 2812 section 2.3.
+
+@author: Kevin Turner
+
+@see: RFC 1459: Internet Relay Chat Protocol
+@see: RFC 2812: Internet Relay Chat: Client Protocol
+@see: U{The Client-To-Client-Protocol
+<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
+"""
+
+import errno, os, random, re, stat, struct, sys, time, types, traceback
+import string, socket
+import warnings
+import textwrap
+from os import path
+
+from twisted.internet import reactor, protocol, task
+from twisted.persisted import styles
+from twisted.protocols import basic
+from twisted.python import log, reflect, text
+from twisted.python.compat import set
+
+NUL = chr(0)
+CR = chr(015)
+NL = chr(012)
+LF = NL
+SPC = chr(040)
+
+# This includes the CRLF terminator characters.
+MAX_COMMAND_LENGTH = 512
+
+CHANNEL_PREFIXES = '&#!+'
+
+class IRCBadMessage(Exception):
+ pass
+
+class IRCPasswordMismatch(Exception):
+ pass
+
+
+
+class IRCBadModes(ValueError):
+ """
+ A malformed mode was encountered while attempting to parse a mode string.
+ """
+
+
+
+def parsemsg(s):
+ """Breaks a message from an IRC server into its prefix, command, and arguments.
+ """
+ prefix = ''
+ trailing = []
+ if not s:
+ raise IRCBadMessage("Empty line.")
+ if s[0] == ':':
+ prefix, s = s[1:].split(' ', 1)
+ if s.find(' :') != -1:
+ s, trailing = s.split(' :', 1)
+ args = s.split()
+ args.append(trailing)
+ else:
+ args = s.split()
+ command = args.pop(0)
+ return prefix, command, args
+
+
+
+def split(str, length=80):
+ """
+ Split a string into multiple lines.
+
+ Whitespace near C{str[length]} will be preferred as a breaking point.
+ C{"\\n"} will also be used as a breaking point.
+
+ @param str: The string to split.
+ @type str: C{str}
+
+ @param length: The maximum length which will be allowed for any string in
+ the result.
+ @type length: C{int}
+
+ @return: C{list} of C{str}
+ """
+ return [chunk
+ for line in str.split('\n')
+ for chunk in textwrap.wrap(line, length)]
+
+
+def _intOrDefault(value, default=None):
+ """
+ Convert a value to an integer if possible.
+
+ @rtype: C{int} or type of L{default}
+ @return: An integer when C{value} can be converted to an integer,
+ otherwise return C{default}
+ """
+ if value:
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ pass
+ return default
+
+
+
+class UnhandledCommand(RuntimeError):
+ """
+ A command dispatcher could not locate an appropriate command handler.
+ """
+
+
+
+class _CommandDispatcherMixin(object):
+ """
+ Dispatch commands to handlers based on their name.
+
+ Command handler names should be of the form C{prefix_commandName},
+ where C{prefix} is the value specified by L{prefix}, and must
+ accept the parameters as given to L{dispatch}.
+
+ Attempting to mix this in more than once for a single class will cause
+ strange behaviour, due to L{prefix} being overwritten.
+
+ @type prefix: C{str}
+ @ivar prefix: Command handler prefix, used to locate handler attributes
+ """
+ prefix = None
+
+ def dispatch(self, commandName, *args):
+ """
+ Perform actual command dispatch.
+ """
+ def _getMethodName(command):
+ return '%s_%s' % (self.prefix, command)
+
+ def _getMethod(name):
+ return getattr(self, _getMethodName(name), None)
+
+ method = _getMethod(commandName)
+ if method is not None:
+ return method(*args)
+
+ method = _getMethod('unknown')
+ if method is None:
+ raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),))
+ return method(commandName, *args)
+
+
+
+
+
+def parseModes(modes, params, paramModes=('', '')):
+ """
+ Parse an IRC mode string.
+
+ The mode string is parsed into two lists of mode changes (added and
+ removed), with each mode change represented as C{(mode, param)} where mode
+ is the mode character, and param is the parameter passed for that mode, or
+ C{None} if no parameter is required.
+
+ @type modes: C{str}
+ @param modes: Modes string to parse.
+
+ @type params: C{list}
+ @param params: Parameters specified along with L{modes}.
+
+ @type paramModes: C{(str, str)}
+ @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
+ parameters when added or removed.
+
+ @returns: Two lists of mode changes, one for modes added and the other for
+ modes removed respectively, mode changes in each list are represented as
+ C{(mode, param)}.
+ """
+ if len(modes) == 0:
+ raise IRCBadModes('Empty mode string')
+
+ if modes[0] not in '+-':
+ raise IRCBadModes('Malformed modes string: %r' % (modes,))
+
+ changes = ([], [])
+
+ direction = None
+ count = -1
+ for ch in modes:
+ if ch in '+-':
+ if count == 0:
+ raise IRCBadModes('Empty mode sequence: %r' % (modes,))
+ direction = '+-'.index(ch)
+ count = 0
+ else:
+ param = None
+ if ch in paramModes[direction]:
+ try:
+ param = params.pop(0)
+ except IndexError:
+ raise IRCBadModes('Not enough parameters: %r' % (ch,))
+ changes[direction].append((ch, param))
+ count += 1
+
+ if len(params) > 0:
+ raise IRCBadModes('Too many parameters: %r %r' % (modes, params))
+
+ if count == 0:
+ raise IRCBadModes('Empty mode sequence: %r' % (modes,))
+
+ return changes
+
+
+
+class IRC(protocol.Protocol):
+ """
+ Internet Relay Chat server protocol.
+ """
+
+ buffer = ""
+ hostname = None
+
+ encoding = None
+
+ def connectionMade(self):
+ self.channels = []
+ if self.hostname is None:
+ self.hostname = socket.getfqdn()
+
+
+ def sendLine(self, line):
+ if self.encoding is not None:
+ if isinstance(line, unicode):
+ line = line.encode(self.encoding)
+ self.transport.write("%s%s%s" % (line, CR, LF))
+
+
+ def sendMessage(self, command, *parameter_list, **prefix):
+ """
+ Send a line formatted as an IRC message.
+
+ First argument is the command, all subsequent arguments are parameters
+ to that command. If a prefix is desired, it may be specified with the
+ keyword argument 'prefix'.
+ """
+
+ if not command:
+ raise ValueError, "IRC message requires a command."
+
+ if ' ' in command or command[0] == ':':
+ # Not the ONLY way to screw up, but provides a little
+ # sanity checking to catch likely dumb mistakes.
+ raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
+ " look like a command to me: %s" % command
+
+ line = string.join([command] + list(parameter_list))
+ if prefix.has_key('prefix'):
+ line = ":%s %s" % (prefix['prefix'], line)
+ self.sendLine(line)
+
+ if len(parameter_list) > 15:
+ log.msg("Message has %d parameters (RFC allows 15):\n%s" %
+ (len(parameter_list), line))
+
+
+ def dataReceived(self, data):
+ """
+ This hack is to support mIRC, which sends LF only, even though the RFC
+ says CRLF. (Also, the flexibility of LineReceiver to turn "line mode"
+ on and off was not required.)
+ """
+ lines = (self.buffer + data).split(LF)
+ # Put the (possibly empty) element after the last LF back in the
+ # buffer
+ self.buffer = lines.pop()
+
+ for line in lines:
+ if len(line) <= 2:
+ # This is a blank line, at best.
+ continue
+ if line[-1] == CR:
+ line = line[:-1]
+ prefix, command, params = parsemsg(line)
+ # mIRC is a big pile of doo-doo
+ command = command.upper()
+ # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
+
+ self.handleCommand(command, prefix, params)
+
+
+ def handleCommand(self, command, prefix, params):
+ """
+ Determine the function to call for the given command and call it with
+ the given arguments.
+ """
+ method = getattr(self, "irc_%s" % command, None)
+ try:
+ if method is not None:
+ method(prefix, params)
+ else:
+ self.irc_unknown(prefix, command, params)
+ except:
+ log.deferr()
+
+
+ def irc_unknown(self, prefix, command, params):
+ """
+ Called by L{handleCommand} on a command that doesn't have a defined
+ handler. Subclasses should override this method.
+ """
+ raise NotImplementedError(command, prefix, params)
+
+
+ # Helper methods
+ def privmsg(self, sender, recip, message):
+ """
+ Send a message to a channel or user
+
+ @type sender: C{str} or C{unicode}
+ @param sender: Who is sending this message. Should be of the form
+ username!ident@hostmask (unless you know better!).
+
+ @type recip: C{str} or C{unicode}
+ @param recip: The recipient of this message. If a channel, it must
+ start with a channel prefix.
+
+ @type message: C{str} or C{unicode}
+ @param message: The message being sent.
+ """
+ self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
+
+
+ def notice(self, sender, recip, message):
+ """
+ Send a "notice" to a channel or user.
+
+ Notices differ from privmsgs in that the RFC claims they are different.
+ Robots are supposed to send notices and not respond to them. Clients
+ typically display notices differently from privmsgs.
+
+ @type sender: C{str} or C{unicode}
+ @param sender: Who is sending this message. Should be of the form
+ username!ident@hostmask (unless you know better!).
+
+ @type recip: C{str} or C{unicode}
+ @param recip: The recipient of this message. If a channel, it must
+ start with a channel prefix.
+
+ @type message: C{str} or C{unicode}
+ @param message: The message being sent.
+ """
+ self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
+
+
+ def action(self, sender, recip, message):
+ """
+ Send an action to a channel or user.
+
+ @type sender: C{str} or C{unicode}
+ @param sender: Who is sending this message. Should be of the form
+ username!ident@hostmask (unless you know better!).
+
+ @type recip: C{str} or C{unicode}
+ @param recip: The recipient of this message. If a channel, it must
+ start with a channel prefix.
+
+ @type message: C{str} or C{unicode}
+ @param message: The action being sent.
+ """
+ self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
+
+
+ def topic(self, user, channel, topic, author=None):
+ """
+ Send the topic to a user.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving the topic. Only their nick name, not
+ the full hostmask.
+
+ @type channel: C{str} or C{unicode}
+ @param channel: The channel for which this is the topic.
+
+ @type topic: C{str} or C{unicode} or C{None}
+ @param topic: The topic string, unquoted, or None if there is no topic.
+
+ @type author: C{str} or C{unicode}
+ @param author: If the topic is being changed, the full username and
+ hostmask of the person changing it.
+ """
+ if author is None:
+ if topic is None:
+ self.sendLine(':%s %s %s %s :%s' % (
+ self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
+ else:
+ self.sendLine(":%s %s %s %s :%s" % (
+ self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
+ else:
+ self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
+
+
+ def topicAuthor(self, user, channel, author, date):
+ """
+ Send the author of and time at which a topic was set for the given
+ channel.
+
+ This sends a 333 reply message, which is not part of the IRC RFC.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving the topic. Only their nick name, not
+ the full hostmask.
+
+ @type channel: C{str} or C{unicode}
+ @param channel: The channel for which this information is relevant.
+
+ @type author: C{str} or C{unicode}
+ @param author: The nickname (without hostmask) of the user who last set
+ the topic.
+
+ @type date: C{int}
+ @param date: A POSIX timestamp (number of seconds since the epoch) at
+ which the topic was last set.
+ """
+ self.sendLine(':%s %d %s %s %s %d' % (
+ self.hostname, 333, user, channel, author, date))
+
+
+ def names(self, user, channel, names):
+ """
+ Send the names of a channel's participants to a user.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving the name list. Only their nick name,
+ not the full hostmask.
+
+ @type channel: C{str} or C{unicode}
+ @param channel: The channel for which this is the namelist.
+
+ @type names: C{list} of C{str} or C{unicode}
+ @param names: The names to send.
+ """
+ # XXX If unicode is given, these limits are not quite correct
+ prefixLength = len(channel) + len(user) + 10
+ namesLength = 512 - prefixLength
+
+ L = []
+ count = 0
+ for n in names:
+ if count + len(n) + 1 > namesLength:
+ self.sendLine(":%s %s %s = %s :%s" % (
+ self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
+ L = [n]
+ count = len(n)
+ else:
+ L.append(n)
+ count += len(n) + 1
+ if L:
+ self.sendLine(":%s %s %s = %s :%s" % (
+ self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
+ self.sendLine(":%s %s %s %s :End of /NAMES list" % (
+ self.hostname, RPL_ENDOFNAMES, user, channel))
+
+
+ def who(self, user, channel, memberInfo):
+ """
+ Send a list of users participating in a channel.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving this member information. Only their
+ nick name, not the full hostmask.
+
+ @type channel: C{str} or C{unicode}
+ @param channel: The channel for which this is the member information.
+
+ @type memberInfo: C{list} of C{tuples}
+ @param memberInfo: For each member of the given channel, a 7-tuple
+ containing their username, their hostmask, the server to which they
+ are connected, their nickname, the letter "H" or "G" (standing for
+ "Here" or "Gone"), the hopcount from C{user} to this member, and
+ this member's real name.
+ """
+ for info in memberInfo:
+ (username, hostmask, server, nickname, flag, hops, realName) = info
+ assert flag in ("H", "G")
+ self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
+ self.hostname, RPL_WHOREPLY, user, channel,
+ username, hostmask, server, nickname, flag, hops, realName))
+
+ self.sendLine(":%s %s %s %s :End of /WHO list." % (
+ self.hostname, RPL_ENDOFWHO, user, channel))
+
+
+ def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
+ """
+ Send information about the state of a particular user.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving this information. Only their nick name,
+ not the full hostmask.
+
+ @type nick: C{str} or C{unicode}
+ @param nick: The nickname of the user this information describes.
+
+ @type username: C{str} or C{unicode}
+ @param username: The user's username (eg, ident response)
+
+ @type hostname: C{str}
+ @param hostname: The user's hostmask
+
+ @type realName: C{str} or C{unicode}
+ @param realName: The user's real name
+
+ @type server: C{str} or C{unicode}
+ @param server: The name of the server to which the user is connected
+
+ @type serverInfo: C{str} or C{unicode}
+ @param serverInfo: A descriptive string about that server
+
+ @type oper: C{bool}
+ @param oper: Indicates whether the user is an IRC operator
+
+ @type idle: C{int}
+ @param idle: The number of seconds since the user last sent a message
+
+ @type signOn: C{int}
+ @param signOn: A POSIX timestamp (number of seconds since the epoch)
+ indicating the time the user signed on
+
+ @type channels: C{list} of C{str} or C{unicode}
+ @param channels: A list of the channels which the user is participating in
+ """
+ self.sendLine(":%s %s %s %s %s %s * :%s" % (
+ self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
+ self.sendLine(":%s %s %s %s %s :%s" % (
+ self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
+ if oper:
+ self.sendLine(":%s %s %s %s :is an IRC operator" % (
+ self.hostname, RPL_WHOISOPERATOR, user, nick))
+ self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
+ self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
+ self.sendLine(":%s %s %s %s :%s" % (
+ self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
+ self.sendLine(":%s %s %s %s :End of WHOIS list." % (
+ self.hostname, RPL_ENDOFWHOIS, user, nick))
+
+
+ def join(self, who, where):
+ """
+ Send a join message.
+
+ @type who: C{str} or C{unicode}
+ @param who: The name of the user joining. Should be of the form
+ username!ident@hostmask (unless you know better!).
+
+ @type where: C{str} or C{unicode}
+ @param where: The channel the user is joining.
+ """
+ self.sendLine(":%s JOIN %s" % (who, where))
+
+
+ def part(self, who, where, reason=None):
+ """
+ Send a part message.
+
+ @type who: C{str} or C{unicode}
+ @param who: The name of the user joining. Should be of the form
+ username!ident@hostmask (unless you know better!).
+
+ @type where: C{str} or C{unicode}
+ @param where: The channel the user is joining.
+
+ @type reason: C{str} or C{unicode}
+ @param reason: A string describing the misery which caused this poor
+ soul to depart.
+ """
+ if reason:
+ self.sendLine(":%s PART %s :%s" % (who, where, reason))
+ else:
+ self.sendLine(":%s PART %s" % (who, where))
+
+
+ def channelMode(self, user, channel, mode, *args):
+ """
+ Send information about the mode of a channel.
+
+ @type user: C{str} or C{unicode}
+ @param user: The user receiving the name list. Only their nick name,
+ not the full hostmask.
+
+ @type channel: C{str} or C{unicode}
+ @param channel: The channel for which this is the namelist.
+
+ @type mode: C{str}
+ @param mode: A string describing this channel's modes.
+
+ @param args: Any additional arguments required by the modes.
+ """
+ self.sendLine(":%s %s %s %s %s %s" % (
+ self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
+
+
+
+class ServerSupportedFeatures(_CommandDispatcherMixin):
+ """
+ Handle ISUPPORT messages.
+
+ Feature names match those in the ISUPPORT RFC draft identically.
+
+ Information regarding the specifics of ISUPPORT was gleaned from
+ <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
+ """
+ prefix = 'isupport'
+
+ def __init__(self):
+ self._features = {
+ 'CHANNELLEN': 200,
+ 'CHANTYPES': tuple('#&'),
+ 'MODES': 3,
+ 'NICKLEN': 9,
+ 'PREFIX': self._parsePrefixParam('(ovh)@+%'),
+ # The ISUPPORT draft explicitly says that there is no default for
+ # CHANMODES, but we're defaulting it here to handle the case where
+ # the IRC server doesn't send us any ISUPPORT information, since
+ # IRCClient.getChannelModeParams relies on this value.
+ 'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])}
+
+
+ def _splitParamArgs(cls, params, valueProcessor=None):
+ """
+ Split ISUPPORT parameter arguments.
+
+ Values can optionally be processed by C{valueProcessor}.
+
+ For example::
+
+ >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
+ (('A', '1'), ('B', '2'))
+
+ @type params: C{iterable} of C{str}
+
+ @type valueProcessor: C{callable} taking {str}
+ @param valueProcessor: Callable to process argument values, or C{None}
+ to perform no processing
+
+ @rtype: C{list} of C{(str, object)}
+ @return: Sequence of C{(name, processedValue)}
+ """
+ if valueProcessor is None:
+ valueProcessor = lambda x: x
+
+ def _parse():
+ for param in params:
+ if ':' not in param:
+ param += ':'
+ a, b = param.split(':', 1)
+ yield a, valueProcessor(b)
+ return list(_parse())
+ _splitParamArgs = classmethod(_splitParamArgs)
+
+
+ def _unescapeParamValue(cls, value):
+ """
+ Unescape an ISUPPORT parameter.
+
+ The only form of supported escape is C{\\xHH}, where HH must be a valid
+ 2-digit hexadecimal number.
+
+ @rtype: C{str}
+ """
+ def _unescape():
+ parts = value.split('\\x')
+ # The first part can never be preceeded by the escape.
+ yield parts.pop(0)
+ for s in parts:
+ octet, rest = s[:2], s[2:]
+ try:
+ octet = int(octet, 16)
+ except ValueError:
+ raise ValueError('Invalid hex octet: %r' % (octet,))
+ yield chr(octet) + rest
+
+ if '\\x' not in value:
+ return value
+ return ''.join(_unescape())
+ _unescapeParamValue = classmethod(_unescapeParamValue)
+
+
+ def _splitParam(cls, param):
+ """
+ Split an ISUPPORT parameter.
+
+ @type param: C{str}
+
+ @rtype: C{(str, list)}
+ @return C{(key, arguments)}
+ """
+ if '=' not in param:
+ param += '='
+ key, value = param.split('=', 1)
+ return key, map(cls._unescapeParamValue, value.split(','))
+ _splitParam = classmethod(_splitParam)
+
+
+ def _parsePrefixParam(cls, prefix):
+ """
+ Parse the ISUPPORT "PREFIX" parameter.
+
+ The order in which the parameter arguments appear is significant, the
+ earlier a mode appears the more privileges it gives.
+
+ @rtype: C{dict} mapping C{str} to C{(str, int)}
+ @return: A dictionary mapping a mode character to a two-tuple of
+ C({symbol, priority)}, the lower a priority (the lowest being
+ C{0}) the more privileges it gives
+ """
+ if not prefix:
+ return None
+ if prefix[0] != '(' and ')' not in prefix:
+ raise ValueError('Malformed PREFIX parameter')
+ modes, symbols = prefix.split(')', 1)
+ symbols = zip(symbols, xrange(len(symbols)))
+ modes = modes[1:]
+ return dict(zip(modes, symbols))
+ _parsePrefixParam = classmethod(_parsePrefixParam)
+
+
+ def _parseChanModesParam(self, params):
+ """
+ Parse the ISUPPORT "CHANMODES" parameter.
+
+ See L{isupport_CHANMODES} for a detailed explanation of this parameter.
+ """
+ names = ('addressModes', 'param', 'setParam', 'noParam')
+ if len(params) > len(names):
+ raise ValueError(
+ 'Expecting a maximum of %d channel mode parameters, got %d' % (
+ len(names), len(params)))
+ items = map(lambda key, value: (key, value or ''), names, params)
+ return dict(items)
+ _parseChanModesParam = classmethod(_parseChanModesParam)
+
+
+ def getFeature(self, feature, default=None):
+ """
+ Get a server supported feature's value.
+
+ A feature with the value C{None} is equivalent to the feature being
+ unsupported.
+
+ @type feature: C{str}
+ @param feature: Feature name
+
+ @type default: C{object}
+ @param default: The value to default to, assuming that C{feature}
+ is not supported
+
+ @return: Feature value
+ """
+ return self._features.get(feature, default)
+
+
+ def hasFeature(self, feature):
+ """
+ Determine whether a feature is supported or not.
+
+ @rtype: C{bool}
+ """
+ return self.getFeature(feature) is not None
+
+
+ def parse(self, params):
+ """
+ Parse ISUPPORT parameters.
+
+ If an unknown parameter is encountered, it is simply added to the
+ dictionary, keyed by its name, as a tuple of the parameters provided.
+
+ @type params: C{iterable} of C{str}
+ @param params: Iterable of ISUPPORT parameters to parse
+ """
+ for param in params:
+ key, value = self._splitParam(param)
+ if key.startswith('-'):
+ self._features.pop(key[1:], None)
+ else:
+ self._features[key] = self.dispatch(key, value)
+
+
+ def isupport_unknown(self, command, params):
+ """
+ Unknown ISUPPORT parameter.
+ """
+ return tuple(params)
+
+
+ def isupport_CHANLIMIT(self, params):
+ """
+ The maximum number of each channel type a user may join.
+ """
+ return self._splitParamArgs(params, _intOrDefault)
+
+
+ def isupport_CHANMODES(self, params):
+ """
+ Available channel modes.
+
+ There are 4 categories of channel mode::
+
+ addressModes - Modes that add or remove an address to or from a
+ list, these modes always take a parameter.
+
+ param - Modes that change a setting on a channel, these modes
+ always take a parameter.
+
+ setParam - Modes that change a setting on a channel, these modes
+ only take a parameter when being set.
+
+ noParam - Modes that change a setting on a channel, these modes
+ never take a parameter.
+ """
+ try:
+ return self._parseChanModesParam(params)
+ except ValueError:
+ return self.getFeature('CHANMODES')
+
+
+ def isupport_CHANNELLEN(self, params):
+ """
+ Maximum length of a channel name a client may create.
+ """
+ return _intOrDefault(params[0], self.getFeature('CHANNELLEN'))
+
+
+ def isupport_CHANTYPES(self, params):
+ """
+ Valid channel prefixes.
+ """
+ return tuple(params[0])
+
+
+ def isupport_EXCEPTS(self, params):
+ """
+ Mode character for "ban exceptions".
+
+ The presence of this parameter indicates that the server supports
+ this functionality.
+ """
+ return params[0] or 'e'
+
+
+ def isupport_IDCHAN(self, params):
+ """
+ Safe channel identifiers.
+
+ The presence of this parameter indicates that the server supports
+ this functionality.
+ """
+ return self._splitParamArgs(params)
+
+
+ def isupport_INVEX(self, params):
+ """
+ Mode character for "invite exceptions".
+
+ The presence of this parameter indicates that the server supports
+ this functionality.
+ """
+ return params[0] or 'I'
+
+
+ def isupport_KICKLEN(self, params):
+ """
+ Maximum length of a kick message a client may provide.
+ """
+ return _intOrDefault(params[0])
+
+
+ def isupport_MAXLIST(self, params):
+ """
+ Maximum number of "list modes" a client may set on a channel at once.
+
+ List modes are identified by the "addressModes" key in CHANMODES.
+ """
+ return self._splitParamArgs(params, _intOrDefault)
+
+
+ def isupport_MODES(self, params):
+ """
+ Maximum number of modes accepting parameters that may be sent, by a
+ client, in a single MODE command.
+ """
+ return _intOrDefault(params[0])
+
+
+ def isupport_NETWORK(self, params):
+ """
+ IRC network name.
+ """
+ return params[0]
+
+
+ def isupport_NICKLEN(self, params):
+ """
+ Maximum length of a nickname the client may use.
+ """
+ return _intOrDefault(params[0], self.getFeature('NICKLEN'))
+
+
+ def isupport_PREFIX(self, params):
+ """
+ Mapping of channel modes that clients may have to status flags.
+ """
+ try:
+ return self._parsePrefixParam(params[0])
+ except ValueError:
+ return self.getFeature('PREFIX')
+
+
+ def isupport_SAFELIST(self, params):
+ """
+ Flag indicating that a client may request a LIST without being
+ disconnected due to the large amount of data generated.
+ """
+ return True
+
+
+ def isupport_STATUSMSG(self, params):
+ """
+ The server supports sending messages to only to clients on a channel
+ with a specific status.
+ """
+ return params[0]
+
+
+ def isupport_TARGMAX(self, params):
+ """
+ Maximum number of targets allowable for commands that accept multiple
+ targets.
+ """
+ return dict(self._splitParamArgs(params, _intOrDefault))
+
+
+ def isupport_TOPICLEN(self, params):
+ """
+ Maximum length of a topic that may be set.
+ """
+ return _intOrDefault(params[0])
+
+
+
+class IRCClient(basic.LineReceiver):
+ """
+ Internet Relay Chat client protocol, with sprinkles.
+
+ In addition to providing an interface for an IRC client protocol,
+ this class also contains reasonable implementations of many common
+ CTCP methods.
+
+ TODO
+ ====
+ - Limit the length of messages sent (because the IRC server probably
+ does).
+ - Add flood protection/rate limiting for my CTCP replies.
+ - NickServ cooperation. (a mix-in?)
+
+ @ivar nickname: Nickname the client will use.
+ @ivar password: Password used to log on to the server. May be C{None}.
+ @ivar realname: Supplied to the server during login as the "Real name"
+ or "ircname". May be C{None}.
+ @ivar username: Supplied to the server during login as the "User name".
+ May be C{None}
+
+ @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If C{None}, no
+ USERINFO reply will be sent.
+ "This is used to transmit a string which is settable by
+ the user (and never should be set by the client)."
+ @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If C{None}, no
+ FINGER reply will be sent.
+ @type fingerReply: Callable or String
+
+ @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION
+ reply will be sent.
+ @type versionName: C{str}, or None.
+ @ivar versionNum: CTCP VERSION reply, client version.
+ @type versionNum: C{str}, or None.
+ @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
+ @type versionEnv: C{str}, or None.
+
+ @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
+ client may be found. If C{None}, no SOURCE reply will be sent.
+
+ @ivar lineRate: Minimum delay between lines sent to the server. If
+ C{None}, no delay will be imposed.
+ @type lineRate: Number of Seconds.
+
+ @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
+ I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
+ of an I{RPL_MOTD} message.
+
+ @ivar erroneousNickFallback: Default nickname assigned when an unregistered
+ client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
+ with an illegal nickname.
+ @type erroneousNickFallback: C{str}
+
+ @ivar _registered: Whether or not the user is registered. It becomes True
+ once a welcome has been received from the server.
+ @type _registered: C{bool}
+
+ @ivar _attemptedNick: The nickname that will try to get registered. It may
+ change if it is illegal or already taken. L{nickname} becomes the
+ L{_attemptedNick} that is successfully registered.
+ @type _attemptedNick: C{str}
+
+ @type supported: L{ServerSupportedFeatures}
+ @ivar supported: Available ISUPPORT features on the server
+
+ @type hostname: C{str}
+ @ivar hostname: Host name of the IRC server the client is connected to.
+ Initially the host name is C{None} and later is set to the host name
+ from which the I{RPL_WELCOME} message is received.
+
+ @type _heartbeat: L{task.LoopingCall}
+ @ivar _heartbeat: Looping call to perform the keepalive by calling
+ L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or
+ C{None} if there is no heartbeat.
+
+ @type heartbeatInterval: C{float}
+ @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to
+ the server as a form of keepalive, defaults to 120 seconds. Use C{None}
+ to disable the heartbeat.
+ """
+ hostname = None
+ motd = None
+ nickname = 'irc'
+ password = None
+ realname = None
+ username = None
+ ### Responses to various CTCP queries.
+
+ userinfo = None
+ # fingerReply is a callable returning a string, or a str()able object.
+ fingerReply = None
+ versionName = None
+ versionNum = None
+ versionEnv = None
+
+ sourceURL = "http://twistedmatrix.com/downloads/"
+
+ dcc_destdir = '.'
+ dcc_sessions = None
+
+ # If this is false, no attempt will be made to identify
+ # ourself to the server.
+ performLogin = 1
+
+ lineRate = None
+ _queue = None
+ _queueEmptying = None
+
+ delimiter = '\n' # '\r\n' will also work (see dataReceived)
+
+ __pychecker__ = 'unusednames=params,prefix,channel'
+
+ _registered = False
+ _attemptedNick = ''
+ erroneousNickFallback = 'defaultnick'
+
+ _heartbeat = None
+ heartbeatInterval = 120
+
+
+ def _reallySendLine(self, line):
+ return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
+
+ def sendLine(self, line):
+ if self.lineRate is None:
+ self._reallySendLine(line)
+ else:
+ self._queue.append(line)
+ if not self._queueEmptying:
+ self._sendLine()
+
+ def _sendLine(self):
+ if self._queue:
+ self._reallySendLine(self._queue.pop(0))
+ self._queueEmptying = reactor.callLater(self.lineRate,
+ self._sendLine)
+ else:
+ self._queueEmptying = None
+
+
+ def connectionLost(self, reason):
+ basic.LineReceiver.connectionLost(self, reason)
+ self.stopHeartbeat()
+
+
+ def _createHeartbeat(self):
+ """
+ Create the heartbeat L{LoopingCall}.
+ """
+ return task.LoopingCall(self._sendHeartbeat)
+
+
+ def _sendHeartbeat(self):
+ """
+ Send a I{PING} message to the IRC server as a form of keepalive.
+ """
+ self.sendLine('PING ' + self.hostname)
+
+
+ def stopHeartbeat(self):
+ """
+ Stop sending I{PING} messages to keep the connection to the server
+ alive.
+
+ @since: 11.1
+ """
+ if self._heartbeat is not None:
+ self._heartbeat.stop()
+ self._heartbeat = None
+
+
+ def startHeartbeat(self):
+ """
+ Start sending I{PING} messages every L{IRCClient.heartbeatInterval}
+ seconds to keep the connection to the server alive during periods of no
+ activity.
+
+ @since: 11.1
+ """
+ self.stopHeartbeat()
+ if self.heartbeatInterval is None:
+ return
+ self._heartbeat = self._createHeartbeat()
+ self._heartbeat.start(self.heartbeatInterval, now=False)
+
+
+ ### Interface level client->user output methods
+ ###
+ ### You'll want to override these.
+
+ ### Methods relating to the server itself
+
+ def created(self, when):
+ """
+ Called with creation date information about the server, usually at logon.
+
+ @type when: C{str}
+ @param when: A string describing when the server was created, probably.
+ """
+
+ def yourHost(self, info):
+ """
+ Called with daemon information about the server, usually at logon.
+
+ @type info: C{str}
+ @param when: A string describing what software the server is running, probably.
+ """
+
+ def myInfo(self, servername, version, umodes, cmodes):
+ """
+ Called with information about the server, usually at logon.
+
+ @type servername: C{str}
+ @param servername: The hostname of this server.
+
+ @type version: C{str}
+ @param version: A description of what software this server runs.
+
+ @type umodes: C{str}
+ @param umodes: All the available user modes.
+
+ @type cmodes: C{str}
+ @param cmodes: All the available channel modes.
+ """
+
+ def luserClient(self, info):
+ """
+ Called with information about the number of connections, usually at logon.
+
+ @type info: C{str}
+ @param info: A description of the number of clients and servers
+ connected to the network, probably.
+ """
+
+ def bounce(self, info):
+ """
+ Called with information about where the client should reconnect.
+
+ @type info: C{str}
+ @param info: A plaintext description of the address that should be
+ connected to.
+ """
+
+ def isupport(self, options):
+ """
+ Called with various information about what the server supports.
+
+ @type options: C{list} of C{str}
+ @param options: Descriptions of features or limits of the server, possibly
+ in the form "NAME=VALUE".
+ """
+
+ def luserChannels(self, channels):
+ """
+ Called with the number of channels existant on the server.
+
+ @type channels: C{int}
+ """
+
+ def luserOp(self, ops):
+ """
+ Called with the number of ops logged on to the server.
+
+ @type ops: C{int}
+ """
+
+ def luserMe(self, info):
+ """
+ Called with information about the server connected to.
+
+ @type info: C{str}
+ @param info: A plaintext string describing the number of users and servers
+ connected to this server.
+ """
+
+ ### Methods involving me directly
+
+ def privmsg(self, user, channel, message):
+ """
+ Called when I have a message from a user to me or a channel.
+ """
+ pass
+
+ def joined(self, channel):
+ """
+ Called when I finish joining a channel.
+
+ channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
+ intact.
+ """
+
+ def left(self, channel):
+ """
+ Called when I have left a channel.
+
+ channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
+ intact.
+ """
+
+
+ def noticed(self, user, channel, message):
+ """
+ Called when I have a notice from a user to me or a channel.
+
+ If the client makes any automated replies, it must not do so in
+ response to a NOTICE message, per the RFC::
+
+ The difference between NOTICE and PRIVMSG is that
+ automatic replies MUST NEVER be sent in response to a
+ NOTICE message. [...] The object of this rule is to avoid
+ loops between clients automatically sending something in
+ response to something it received.
+ """
+
+
+ def modeChanged(self, user, channel, set, modes, args):
+ """
+ Called when users or channel's modes are changed.
+
+ @type user: C{str}
+ @param user: The user and hostmask which instigated this change.
+
+ @type channel: C{str}
+ @param channel: The channel where the modes are changed. If args is
+ empty the channel for which the modes are changing. If the changes are
+ at server level it could be equal to C{user}.
+
+ @type set: C{bool} or C{int}
+ @param set: True if the mode(s) is being added, False if it is being
+ removed. If some modes are added and others removed at the same time
+ this function will be called twice, the first time with all the added
+ modes, the second with the removed ones. (To change this behaviour
+ override the irc_MODE method)
+
+ @type modes: C{str}
+ @param modes: The mode or modes which are being changed.
+
+ @type args: C{tuple}
+ @param args: Any additional information required for the mode
+ change.
+ """
+
+ def pong(self, user, secs):
+ """
+ Called with the results of a CTCP PING query.
+ """
+ pass
+
+ def signedOn(self):
+ """
+ Called after sucessfully signing on to the server.
+ """
+ pass
+
+ def kickedFrom(self, channel, kicker, message):
+ """
+ Called when I am kicked from a channel.
+ """
+ pass
+
+ def nickChanged(self, nick):
+ """
+ Called when my nick has been changed.
+ """
+ self.nickname = nick
+
+
+ ### Things I observe other people doing in a channel.
+
+ def userJoined(self, user, channel):
+ """
+ Called when I see another user joining a channel.
+ """
+ pass
+
+ def userLeft(self, user, channel):
+ """
+ Called when I see another user leaving a channel.
+ """
+ pass
+
+ def userQuit(self, user, quitMessage):
+ """
+ Called when I see another user disconnect from the network.
+ """
+ pass
+
+ def userKicked(self, kickee, channel, kicker, message):
+ """
+ Called when I observe someone else being kicked from a channel.
+ """
+ pass
+
+ def action(self, user, channel, data):
+ """
+ Called when I see a user perform an ACTION on a channel.
+ """
+ pass
+
+ def topicUpdated(self, user, channel, newTopic):
+ """
+ In channel, user changed the topic to newTopic.
+
+ Also called when first joining a channel.
+ """
+ pass
+
+ def userRenamed(self, oldname, newname):
+ """
+ A user changed their name from oldname to newname.
+ """
+ pass
+
+ ### Information from the server.
+
+ def receivedMOTD(self, motd):
+ """
+ I received a message-of-the-day banner from the server.
+
+ motd is a list of strings, where each string was sent as a seperate
+ message from the server. To display, you might want to use::
+
+ '\\n'.join(motd)
+
+ to get a nicely formatted string.
+ """
+ pass
+
+ ### user input commands, client->server
+ ### Your client will want to invoke these.
+
+ def join(self, channel, key=None):
+ """
+ Join a channel.
+
+ @type channel: C{str}
+ @param channel: The name of the channel to join. If it has no prefix,
+ C{'#'} will be prepended to it.
+ @type key: C{str}
+ @param key: If specified, the key used to join the channel.
+ """
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ if key:
+ self.sendLine("JOIN %s %s" % (channel, key))
+ else:
+ self.sendLine("JOIN %s" % (channel,))
+
+ def leave(self, channel, reason=None):
+ """
+ Leave a channel.
+
+ @type channel: C{str}
+ @param channel: The name of the channel to leave. If it has no prefix,
+ C{'#'} will be prepended to it.
+ @type reason: C{str}
+ @param reason: If given, the reason for leaving.
+ """
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ if reason:
+ self.sendLine("PART %s :%s" % (channel, reason))
+ else:
+ self.sendLine("PART %s" % (channel,))
+
+ def kick(self, channel, user, reason=None):
+ """
+ Attempt to kick a user from a channel.
+
+ @type channel: C{str}
+ @param channel: The name of the channel to kick the user from. If it has
+ no prefix, C{'#'} will be prepended to it.
+ @type user: C{str}
+ @param user: The nick of the user to kick.
+ @type reason: C{str}
+ @param reason: If given, the reason for kicking the user.
+ """
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ if reason:
+ self.sendLine("KICK %s %s :%s" % (channel, user, reason))
+ else:
+ self.sendLine("KICK %s %s" % (channel, user))
+
+ part = leave
+
+
+ def invite(self, user, channel):
+ """
+ Attempt to invite user to channel
+
+ @type user: C{str}
+ @param user: The user to invite
+ @type channel: C{str}
+ @param channel: The channel to invite the user too
+
+ @since: 11.0
+ """
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ self.sendLine("INVITE %s %s" % (user, channel))
+
+
+ def topic(self, channel, topic=None):
+ """
+ Attempt to set the topic of the given channel, or ask what it is.
+
+ If topic is None, then I sent a topic query instead of trying to set the
+ topic. The server should respond with a TOPIC message containing the
+ current topic of the given channel.
+
+ @type channel: C{str}
+ @param channel: The name of the channel to change the topic on. If it
+ has no prefix, C{'#'} will be prepended to it.
+ @type topic: C{str}
+ @param topic: If specified, what to set the topic to.
+ """
+ # << TOPIC #xtestx :fff
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ if topic != None:
+ self.sendLine("TOPIC %s :%s" % (channel, topic))
+ else:
+ self.sendLine("TOPIC %s" % (channel,))
+
+
+ def mode(self, chan, set, modes, limit = None, user = None, mask = None):
+ """
+ Change the modes on a user or channel.
+
+ The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
+
+ @type chan: C{str}
+ @param chan: The name of the channel to operate on.
+ @type set: C{bool}
+ @param set: True to give the user or channel permissions and False to
+ remove them.
+ @type modes: C{str}
+ @param modes: The mode flags to set on the user or channel.
+ @type limit: C{int}
+ @param limit: In conjuction with the C{'l'} mode flag, limits the
+ number of users on the channel.
+ @type user: C{str}
+ @param user: The user to change the mode on.
+ @type mask: C{str}
+ @param mask: In conjuction with the C{'b'} mode flag, sets a mask of
+ users to be banned from the channel.
+ """
+ if set:
+ line = 'MODE %s +%s' % (chan, modes)
+ else:
+ line = 'MODE %s -%s' % (chan, modes)
+ if limit is not None:
+ line = '%s %d' % (line, limit)
+ elif user is not None:
+ line = '%s %s' % (line, user)
+ elif mask is not None:
+ line = '%s %s' % (line, mask)
+ self.sendLine(line)
+
+
+ def say(self, channel, message, length=None):
+ """
+ Send a message to a channel
+
+ @type channel: C{str}
+ @param channel: The channel to say the message on. If it has no prefix,
+ C{'#'} will be prepended to it.
+ @type message: C{str}
+ @param message: The message to say.
+ @type length: C{int}
+ @param length: The maximum number of octets to send at a time. This has
+ the effect of turning a single call to C{msg()} into multiple
+ commands to the server. This is useful when long messages may be
+ sent that would otherwise cause the server to kick us off or
+ silently truncate the text we are sending. If None is passed, the
+ entire message is always send in one command.
+ """
+ if channel[0] not in CHANNEL_PREFIXES:
+ channel = '#' + channel
+ self.msg(channel, message, length)
+
+
+ def _safeMaximumLineLength(self, command):
+ """
+ Estimate a safe maximum line length for the given command.
+
+ This is done by assuming the maximum values for nickname length,
+ realname and hostname combined with the command that needs to be sent
+ and some guessing. A theoretical maximum value is used because it is
+ possible that our nickname, username or hostname changes (on the server
+ side) while the length is still being calculated.
+ """
+ # :nickname!realname@hostname COMMAND ...
+ theoretical = ':%s!%s@%s %s' % (
+ 'a' * self.supported.getFeature('NICKLEN'),
+ # This value is based on observation.
+ 'b' * 10,
+ # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>.
+ 'c' * 63,
+ command)
+ # Fingers crossed.
+ fudge = 10
+ return MAX_COMMAND_LENGTH - len(theoretical) - fudge
+
+
+ def msg(self, user, message, length=None):
+ """
+ Send a message to a user or channel.
+
+ The message will be split into multiple commands to the server if:
+ - The message contains any newline characters
+ - Any span between newline characters is longer than the given
+ line-length.
+
+ @param user: Username or channel name to which to direct the
+ message.
+ @type user: C{str}
+
+ @param message: Text to send.
+ @type message: C{str}
+
+ @param length: Maximum number of octets to send in a single
+ command, including the IRC protocol framing. If C{None} is given
+ then L{IRCClient._safeMaximumLineLength} is used to determine a
+ value.
+ @type length: C{int}
+ """
+ fmt = 'PRIVMSG %s :' % (user,)
+
+ if length is None:
+ length = self._safeMaximumLineLength(fmt)
+
+ # Account for the line terminator.
+ minimumLength = len(fmt) + 2
+ if length <= minimumLength:
+ raise ValueError("Maximum length must exceed %d for message "
+ "to %s" % (minimumLength, user))
+ for line in split(message, length - minimumLength):
+ self.sendLine(fmt + line)
+
+
+ def notice(self, user, message):
+ """
+ Send a notice to a user.
+
+ Notices are like normal message, but should never get automated
+ replies.
+
+ @type user: C{str}
+ @param user: The user to send a notice to.
+ @type message: C{str}
+ @param message: The contents of the notice to send.
+ """
+ self.sendLine("NOTICE %s :%s" % (user, message))
+
+
+ def away(self, message=''):
+ """
+ Mark this client as away.
+
+ @type message: C{str}
+ @param message: If specified, the away message.
+ """
+ self.sendLine("AWAY :%s" % message)
+
+
+ def back(self):
+ """
+ Clear the away status.
+ """
+ # An empty away marks us as back
+ self.away()
+
+
+ def whois(self, nickname, server=None):
+ """
+ Retrieve user information about the given nick name.
+
+ @type nickname: C{str}
+ @param nickname: The nick name about which to retrieve information.
+
+ @since: 8.2
+ """
+ if server is None:
+ self.sendLine('WHOIS ' + nickname)
+ else:
+ self.sendLine('WHOIS %s %s' % (server, nickname))
+
+
+ def register(self, nickname, hostname='foo', servername='bar'):
+ """
+ Login to the server.
+
+ @type nickname: C{str}
+ @param nickname: The nickname to register.
+ @type hostname: C{str}
+ @param hostname: If specified, the hostname to logon as.
+ @type servername: C{str}
+ @param servername: If specified, the servername to logon as.
+ """
+ if self.password is not None:
+ self.sendLine("PASS %s" % self.password)
+ self.setNick(nickname)
+ if self.username is None:
+ self.username = nickname
+ self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
+
+
+ def setNick(self, nickname):
+ """
+ Set this client's nickname.
+
+ @type nickname: C{str}
+ @param nickname: The nickname to change to.
+ """
+ self._attemptedNick = nickname
+ self.sendLine("NICK %s" % nickname)
+
+
+ def quit(self, message = ''):
+ """
+ Disconnect from the server
+
+ @type message: C{str}
+
+ @param message: If specified, the message to give when quitting the
+ server.
+ """
+ self.sendLine("QUIT :%s" % message)
+
+ ### user input commands, client->client
+
+ def describe(self, channel, action):
+ """
+ Strike a pose.
+
+ @type channel: C{str}
+ @param channel: The name of the channel to have an action on. If it
+ has no prefix, it is sent to the user of that name.
+ @type action: C{str}
+ @param action: The action to preform.
+ @since: 9.0
+ """
+ self.ctcpMakeQuery(channel, [('ACTION', action)])
+
+
+ _pings = None
+ _MAX_PINGRING = 12
+
+ def ping(self, user, text = None):
+ """
+ Measure round-trip delay to another IRC client.
+ """
+ if self._pings is None:
+ self._pings = {}
+
+ if text is None:
+ chars = string.letters + string.digits + string.punctuation
+ key = ''.join([random.choice(chars) for i in range(12)])
+ else:
+ key = str(text)
+ self._pings[(user, key)] = time.time()
+ self.ctcpMakeQuery(user, [('PING', key)])
+
+ if len(self._pings) > self._MAX_PINGRING:
+ # Remove some of the oldest entries.
+ byValue = [(v, k) for (k, v) in self._pings.items()]
+ byValue.sort()
+ excess = self._MAX_PINGRING - len(self._pings)
+ for i in xrange(excess):
+ del self._pings[byValue[i][1]]
+
+
+ def dccSend(self, user, file):
+ if type(file) == types.StringType:
+ file = open(file, 'r')
+
+ size = fileSize(file)
+
+ name = getattr(file, "name", "file@%s" % (id(file),))
+
+ factory = DccSendFactory(file)
+ port = reactor.listenTCP(0, factory, 1)
+
+ raise NotImplementedError,(
+ "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
+ "(and stop accepting once we've made a single connection.)")
+
+ my_address = struct.pack("!I", my_address)
+
+ args = ['SEND', name, my_address, str(port)]
+
+ if not (size is None):
+ args.append(size)
+
+ args = string.join(args, ' ')
+
+ self.ctcpMakeQuery(user, [('DCC', args)])
+
+
+ def dccResume(self, user, fileName, port, resumePos):
+ """
+ Send a DCC RESUME request to another user.
+ """
+ self.ctcpMakeQuery(user, [
+ ('DCC', ['RESUME', fileName, port, resumePos])])
+
+
+ def dccAcceptResume(self, user, fileName, port, resumePos):
+ """
+ Send a DCC ACCEPT response to clients who have requested a resume.
+ """
+ self.ctcpMakeQuery(user, [
+ ('DCC', ['ACCEPT', fileName, port, resumePos])])
+
+ ### server->client messages
+ ### You might want to fiddle with these,
+ ### but it is safe to leave them alone.
+
+ def irc_ERR_NICKNAMEINUSE(self, prefix, params):
+ """
+ Called when we try to register or change to a nickname that is already
+ taken.
+ """
+ self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
+ self.setNick(self._attemptedNick)
+
+
+ def alterCollidedNick(self, nickname):
+ """
+ Generate an altered version of a nickname that caused a collision in an
+ effort to create an unused related name for subsequent registration.
+
+ @param nickname: The nickname a user is attempting to register.
+ @type nickname: C{str}
+
+ @returns: A string that is in some way different from the nickname.
+ @rtype: C{str}
+ """
+ return nickname + '_'
+
+
+ def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
+ """
+ Called when we try to register or change to an illegal nickname.
+
+ The server should send this reply when the nickname contains any
+ disallowed characters. The bot will stall, waiting for RPL_WELCOME, if
+ we don't handle this during sign-on.
+
+ @note: The method uses the spelling I{erroneus}, as it appears in
+ the RFC, section 6.1.
+ """
+ if not self._registered:
+ self.setNick(self.erroneousNickFallback)
+
+
+ def irc_ERR_PASSWDMISMATCH(self, prefix, params):
+ """
+ Called when the login was incorrect.
+ """
+ raise IRCPasswordMismatch("Password Incorrect.")
+
+
+ def irc_RPL_WELCOME(self, prefix, params):
+ """
+ Called when we have received the welcome from the server.
+ """
+ self.hostname = prefix
+ self._registered = True
+ self.nickname = self._attemptedNick
+ self.signedOn()
+ self.startHeartbeat()
+
+
+ def irc_JOIN(self, prefix, params):
+ """
+ Called when a user joins a channel.
+ """
+ nick = string.split(prefix,'!')[0]
+ channel = params[-1]
+ if nick == self.nickname:
+ self.joined(channel)
+ else:
+ self.userJoined(nick, channel)
+
+ def irc_PART(self, prefix, params):
+ """
+ Called when a user leaves a channel.
+ """
+ nick = string.split(prefix,'!')[0]
+ channel = params[0]
+ if nick == self.nickname:
+ self.left(channel)
+ else:
+ self.userLeft(nick, channel)
+
+ def irc_QUIT(self, prefix, params):
+ """
+ Called when a user has quit.
+ """
+ nick = string.split(prefix,'!')[0]
+ self.userQuit(nick, params[0])
+
+
+ def irc_MODE(self, user, params):
+ """
+ Parse a server mode change message.
+ """
+ channel, modes, args = params[0], params[1], params[2:]
+
+ if modes[0] not in '-+':
+ modes = '+' + modes
+
+ if channel == self.nickname:
+ # This is a mode change to our individual user, not a channel mode
+ # that involves us.
+ paramModes = self.getUserModeParams()
+ else:
+ paramModes = self.getChannelModeParams()
+
+ try:
+ added, removed = parseModes(modes, args, paramModes)
+ except IRCBadModes:
+ log.err(None, 'An error occured while parsing the following '
+ 'MODE message: MODE %s' % (' '.join(params),))
+ else:
+ if added:
+ modes, params = zip(*added)
+ self.modeChanged(user, channel, True, ''.join(modes), params)
+
+ if removed:
+ modes, params = zip(*removed)
+ self.modeChanged(user, channel, False, ''.join(modes), params)
+
+
+ def irc_PING(self, prefix, params):
+ """
+ Called when some has pinged us.
+ """
+ self.sendLine("PONG %s" % params[-1])
+
+ def irc_PRIVMSG(self, prefix, params):
+ """
+ Called when we get a message.
+ """
+ user = prefix
+ channel = params[0]
+ message = params[-1]
+
+ if not message:
+ # Don't raise an exception if we get blank message.
+ return
+
+ if message[0] == X_DELIM:
+ m = ctcpExtract(message)
+ if m['extended']:
+ self.ctcpQuery(user, channel, m['extended'])
+
+ if not m['normal']:
+ return
+
+ message = string.join(m['normal'], ' ')
+
+ self.privmsg(user, channel, message)
+
+ def irc_NOTICE(self, prefix, params):
+ """
+ Called when a user gets a notice.
+ """
+ user = prefix
+ channel = params[0]
+ message = params[-1]
+
+ if message[0]==X_DELIM:
+ m = ctcpExtract(message)
+ if m['extended']:
+ self.ctcpReply(user, channel, m['extended'])
+
+ if not m['normal']:
+ return
+
+ message = string.join(m['normal'], ' ')
+
+ self.noticed(user, channel, message)
+
+ def irc_NICK(self, prefix, params):
+ """
+ Called when a user changes their nickname.
+ """
+ nick = string.split(prefix,'!', 1)[0]
+ if nick == self.nickname:
+ self.nickChanged(params[0])
+ else:
+ self.userRenamed(nick, params[0])
+
+ def irc_KICK(self, prefix, params):
+ """
+ Called when a user is kicked from a channel.
+ """
+ kicker = string.split(prefix,'!')[0]
+ channel = params[0]
+ kicked = params[1]
+ message = params[-1]
+ if string.lower(kicked) == string.lower(self.nickname):
+ # Yikes!
+ self.kickedFrom(channel, kicker, message)
+ else:
+ self.userKicked(kicked, channel, kicker, message)
+
+ def irc_TOPIC(self, prefix, params):
+ """
+ Someone in the channel set the topic.
+ """
+ user = string.split(prefix, '!')[0]
+ channel = params[0]
+ newtopic = params[1]
+ self.topicUpdated(user, channel, newtopic)
+
+ def irc_RPL_TOPIC(self, prefix, params):
+ """
+ Called when the topic for a channel is initially reported or when it
+ subsequently changes.
+ """
+ user = string.split(prefix, '!')[0]
+ channel = params[1]
+ newtopic = params[2]
+ self.topicUpdated(user, channel, newtopic)
+
+ def irc_RPL_NOTOPIC(self, prefix, params):
+ user = string.split(prefix, '!')[0]
+ channel = params[1]
+ newtopic = ""
+ self.topicUpdated(user, channel, newtopic)
+
+ def irc_RPL_MOTDSTART(self, prefix, params):
+ if params[-1].startswith("- "):
+ params[-1] = params[-1][2:]
+ self.motd = [params[-1]]
+
+ def irc_RPL_MOTD(self, prefix, params):
+ if params[-1].startswith("- "):
+ params[-1] = params[-1][2:]
+ if self.motd is None:
+ self.motd = []
+ self.motd.append(params[-1])
+
+
+ def irc_RPL_ENDOFMOTD(self, prefix, params):
+ """
+ I{RPL_ENDOFMOTD} indicates the end of the message of the day
+ messages. Deliver the accumulated lines to C{receivedMOTD}.
+ """
+ motd = self.motd
+ self.motd = None
+ self.receivedMOTD(motd)
+
+
+ def irc_RPL_CREATED(self, prefix, params):
+ self.created(params[1])
+
+ def irc_RPL_YOURHOST(self, prefix, params):
+ self.yourHost(params[1])
+
+ def irc_RPL_MYINFO(self, prefix, params):
+ info = params[1].split(None, 3)
+ while len(info) < 4:
+ info.append(None)
+ self.myInfo(*info)
+
+ def irc_RPL_BOUNCE(self, prefix, params):
+ self.bounce(params[1])
+
+ def irc_RPL_ISUPPORT(self, prefix, params):
+ args = params[1:-1]
+ # Several ISUPPORT messages, in no particular order, may be sent
+ # to the client at any given point in time (usually only on connect,
+ # though.) For this reason, ServerSupportedFeatures.parse is intended
+ # to mutate the supported feature list.
+ self.supported.parse(args)
+ self.isupport(args)
+
+ def irc_RPL_LUSERCLIENT(self, prefix, params):
+ self.luserClient(params[1])
+
+ def irc_RPL_LUSEROP(self, prefix, params):
+ try:
+ self.luserOp(int(params[1]))
+ except ValueError:
+ pass
+
+ def irc_RPL_LUSERCHANNELS(self, prefix, params):
+ try:
+ self.luserChannels(int(params[1]))
+ except ValueError:
+ pass
+
+ def irc_RPL_LUSERME(self, prefix, params):
+ self.luserMe(params[1])
+
+ def irc_unknown(self, prefix, command, params):
+ pass
+
+ ### Receiving a CTCP query from another party
+ ### It is safe to leave these alone.
+
+
+ def ctcpQuery(self, user, channel, messages):
+ """
+ Dispatch method for any CTCP queries received.
+
+ Duplicated CTCP queries are ignored and no dispatch is
+ made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
+ """
+ seen = set()
+ for tag, data in messages:
+ method = getattr(self, 'ctcpQuery_%s' % tag, None)
+ if tag not in seen:
+ if method is not None:
+ method(user, channel, data)
+ else:
+ self.ctcpUnknownQuery(user, channel, tag, data)
+ seen.add(tag)
+
+
+ def ctcpUnknownQuery(self, user, channel, tag, data):
+ """
+ Fallback handler for unrecognized CTCP queries.
+
+ No CTCP I{ERRMSG} reply is made to remove a potential denial of service
+ avenue.
+ """
+ log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data))
+
+
+ def ctcpQuery_ACTION(self, user, channel, data):
+ self.action(user, channel, data)
+
+ def ctcpQuery_PING(self, user, channel, data):
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [("PING", data)])
+
+ def ctcpQuery_FINGER(self, user, channel, data):
+ if data is not None:
+ self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
+ % (user, data))
+ if not self.fingerReply:
+ return
+
+ if callable(self.fingerReply):
+ reply = self.fingerReply()
+ else:
+ reply = str(self.fingerReply)
+
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [('FINGER', reply)])
+
+ def ctcpQuery_VERSION(self, user, channel, data):
+ if data is not None:
+ self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
+ % (user, data))
+
+ if self.versionName:
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
+ (self.versionName,
+ self.versionNum or '',
+ self.versionEnv or ''))])
+
+ def ctcpQuery_SOURCE(self, user, channel, data):
+ if data is not None:
+ self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
+ % (user, data))
+ if self.sourceURL:
+ nick = string.split(user,"!")[0]
+ # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
+ # replies should be responded to with the location of an anonymous
+ # FTP server in host:directory:file format. I'm taking the liberty
+ # of bringing it into the 21st century by sending a URL instead.
+ self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
+ ('SOURCE', None)])
+
+ def ctcpQuery_USERINFO(self, user, channel, data):
+ if data is not None:
+ self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
+ % (user, data))
+ if self.userinfo:
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
+
+ def ctcpQuery_CLIENTINFO(self, user, channel, data):
+ """
+ A master index of what CTCP tags this client knows.
+
+ If no arguments are provided, respond with a list of known tags.
+ If an argument is provided, provide human-readable help on
+ the usage of that tag.
+ """
+
+ nick = string.split(user,"!")[0]
+ if not data:
+ # XXX: prefixedMethodNames gets methods from my *class*,
+ # but it's entirely possible that this *instance* has more
+ # methods.
+ names = reflect.prefixedMethodNames(self.__class__,
+ 'ctcpQuery_')
+
+ self.ctcpMakeReply(nick, [('CLIENTINFO',
+ string.join(names, ' '))])
+ else:
+ args = string.split(data)
+ method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
+ if not method:
+ self.ctcpMakeReply(nick, [('ERRMSG',
+ "CLIENTINFO %s :"
+ "Unknown query '%s'"
+ % (data, args[0]))])
+ return
+ doc = getattr(method, '__doc__', '')
+ self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
+
+
+ def ctcpQuery_ERRMSG(self, user, channel, data):
+ # Yeah, this seems strange, but that's what the spec says to do
+ # when faced with an ERRMSG query (not a reply).
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [('ERRMSG',
+ "%s :No error has occoured." % data)])
+
+ def ctcpQuery_TIME(self, user, channel, data):
+ if data is not None:
+ self.quirkyMessage("Why did %s send '%s' with a TIME query?"
+ % (user, data))
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick,
+ [('TIME', ':%s' %
+ time.asctime(time.localtime(time.time())))])
+
+ def ctcpQuery_DCC(self, user, channel, data):
+ """Initiate a Direct Client Connection
+ """
+
+ if not data: return
+ dcctype = data.split(None, 1)[0].upper()
+ handler = getattr(self, "dcc_" + dcctype, None)
+ if handler:
+ if self.dcc_sessions is None:
+ self.dcc_sessions = []
+ data = data[len(dcctype)+1:]
+ handler(user, channel, data)
+ else:
+ nick = string.split(user,"!")[0]
+ self.ctcpMakeReply(nick, [('ERRMSG',
+ "DCC %s :Unknown DCC type '%s'"
+ % (data, dcctype))])
+ self.quirkyMessage("%s offered unknown DCC type %s"
+ % (user, dcctype))
+
+ def dcc_SEND(self, user, channel, data):
+ # Use splitQuoted for those who send files with spaces in the names.
+ data = text.splitQuoted(data)
+ if len(data) < 3:
+ raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
+
+ (filename, address, port) = data[:3]
+
+ address = dccParseAddress(address)
+ try:
+ port = int(port)
+ except ValueError:
+ raise IRCBadMessage, "Indecipherable port %r" % (port,)
+
+ size = -1
+ if len(data) >= 4:
+ try:
+ size = int(data[3])
+ except ValueError:
+ pass
+
+ # XXX Should we bother passing this data?
+ self.dccDoSend(user, address, port, filename, size, data)
+
+ def dcc_ACCEPT(self, user, channel, data):
+ data = text.splitQuoted(data)
+ if len(data) < 3:
+ raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
+ (filename, port, resumePos) = data[:3]
+ try:
+ port = int(port)
+ resumePos = int(resumePos)
+ except ValueError:
+ return
+
+ self.dccDoAcceptResume(user, filename, port, resumePos)
+
+ def dcc_RESUME(self, user, channel, data):
+ data = text.splitQuoted(data)
+ if len(data) < 3:
+ raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
+ (filename, port, resumePos) = data[:3]
+ try:
+ port = int(port)
+ resumePos = int(resumePos)
+ except ValueError:
+ return
+ self.dccDoResume(user, filename, port, resumePos)
+
+ def dcc_CHAT(self, user, channel, data):
+ data = text.splitQuoted(data)
+ if len(data) < 3:
+ raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
+
+ (filename, address, port) = data[:3]
+
+ address = dccParseAddress(address)
+ try:
+ port = int(port)
+ except ValueError:
+ raise IRCBadMessage, "Indecipherable port %r" % (port,)
+
+ self.dccDoChat(user, channel, address, port, data)
+
+ ### The dccDo methods are the slightly higher-level siblings of
+ ### common dcc_ methods; the arguments have been parsed for them.
+
+ def dccDoSend(self, user, address, port, fileName, size, data):
+ """Called when I receive a DCC SEND offer from a client.
+
+ By default, I do nothing here."""
+ ## filename = path.basename(arg)
+ ## protocol = DccFileReceive(filename, size,
+ ## (user,channel,data),self.dcc_destdir)
+ ## reactor.clientTCP(address, port, protocol)
+ ## self.dcc_sessions.append(protocol)
+ pass
+
+ def dccDoResume(self, user, file, port, resumePos):
+ """Called when a client is trying to resume an offered file
+ via DCC send. It should be either replied to with a DCC
+ ACCEPT or ignored (default)."""
+ pass
+
+ def dccDoAcceptResume(self, user, file, port, resumePos):
+ """Called when a client has verified and accepted a DCC resume
+ request made by us. By default it will do nothing."""
+ pass
+
+ def dccDoChat(self, user, channel, address, port, data):
+ pass
+ #factory = DccChatFactory(self, queryData=(user, channel, data))
+ #reactor.connectTCP(address, port, factory)
+ #self.dcc_sessions.append(factory)
+
+ #def ctcpQuery_SED(self, user, data):
+ # """Simple Encryption Doodoo
+ #
+ # Feel free to implement this, but no specification is available.
+ # """
+ # raise NotImplementedError
+
+
+ def ctcpMakeReply(self, user, messages):
+ """
+ Send one or more C{extended messages} as a CTCP reply.
+
+ @type messages: a list of extended messages. An extended
+ message is a (tag, data) tuple, where 'data' may be C{None}.
+ """
+ self.notice(user, ctcpStringify(messages))
+
+ ### client CTCP query commands
+
+ def ctcpMakeQuery(self, user, messages):
+ """
+ Send one or more C{extended messages} as a CTCP query.
+
+ @type messages: a list of extended messages. An extended
+ message is a (tag, data) tuple, where 'data' may be C{None}.
+ """
+ self.msg(user, ctcpStringify(messages))
+
+ ### Receiving a response to a CTCP query (presumably to one we made)
+ ### You may want to add methods here, or override UnknownReply.
+
+ def ctcpReply(self, user, channel, messages):
+ """
+ Dispatch method for any CTCP replies received.
+ """
+ for m in messages:
+ method = getattr(self, "ctcpReply_%s" % m[0], None)
+ if method:
+ method(user, channel, m[1])
+ else:
+ self.ctcpUnknownReply(user, channel, m[0], m[1])
+
+ def ctcpReply_PING(self, user, channel, data):
+ nick = user.split('!', 1)[0]
+ if (not self._pings) or (not self._pings.has_key((nick, data))):
+ raise IRCBadMessage,\
+ "Bogus PING response from %s: %s" % (user, data)
+
+ t0 = self._pings[(nick, data)]
+ self.pong(user, time.time() - t0)
+
+ def ctcpUnknownReply(self, user, channel, tag, data):
+ """Called when a fitting ctcpReply_ method is not found.
+
+ XXX: If the client makes arbitrary CTCP queries,
+ this method should probably show the responses to
+ them instead of treating them as anomolies.
+ """
+ log.msg("Unknown CTCP reply from %s: %s %s\n"
+ % (user, tag, data))
+
+ ### Error handlers
+ ### You may override these with something more appropriate to your UI.
+
+ def badMessage(self, line, excType, excValue, tb):
+ """When I get a message that's so broken I can't use it.
+ """
+ log.msg(line)
+ log.msg(string.join(traceback.format_exception(excType,
+ excValue,
+ tb),''))
+
+ def quirkyMessage(self, s):
+ """This is called when I receive a message which is peculiar,
+ but not wholly indecipherable.
+ """
+ log.msg(s + '\n')
+
+ ### Protocool methods
+
+ def connectionMade(self):
+ self.supported = ServerSupportedFeatures()
+ self._queue = []
+ if self.performLogin:
+ self.register(self.nickname)
+
+ def dataReceived(self, data):
+ basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
+
+ def lineReceived(self, line):
+ line = lowDequote(line)
+ try:
+ prefix, command, params = parsemsg(line)
+ if numeric_to_symbolic.has_key(command):
+ command = numeric_to_symbolic[command]
+ self.handleCommand(command, prefix, params)
+ except IRCBadMessage:
+ self.badMessage(line, *sys.exc_info())
+
+
+ def getUserModeParams(self):
+ """
+ Get user modes that require parameters for correct parsing.
+
+ @rtype: C{[str, str]}
+ @return C{[add, remove]}
+ """
+ return ['', '']
+
+
+ def getChannelModeParams(self):
+ """
+ Get channel modes that require parameters for correct parsing.
+
+ @rtype: C{[str, str]}
+ @return C{[add, remove]}
+ """
+ # PREFIX modes are treated as "type B" CHANMODES, they always take
+ # parameter.
+ params = ['', '']
+ prefixes = self.supported.getFeature('PREFIX', {})
+ params[0] = params[1] = ''.join(prefixes.iterkeys())
+
+ chanmodes = self.supported.getFeature('CHANMODES')
+ if chanmodes is not None:
+ params[0] += chanmodes.get('addressModes', '')
+ params[0] += chanmodes.get('param', '')
+ params[1] = params[0]
+ params[0] += chanmodes.get('setParam', '')
+ return params
+
+
+ def handleCommand(self, command, prefix, params):
+ """Determine the function to call for the given command and call
+ it with the given arguments.
+ """
+ method = getattr(self, "irc_%s" % command, None)
+ try:
+ if method is not None:
+ method(prefix, params)
+ else:
+ self.irc_unknown(prefix, command, params)
+ except:
+ log.deferr()
+
+
+ def __getstate__(self):
+ dct = self.__dict__.copy()
+ dct['dcc_sessions'] = None
+ dct['_pings'] = None
+ return dct
+
+
+def dccParseAddress(address):
+ if '.' in address:
+ pass
+ else:
+ try:
+ address = long(address)
+ except ValueError:
+ raise IRCBadMessage,\
+ "Indecipherable address %r" % (address,)
+ else:
+ address = (
+ (address >> 24) & 0xFF,
+ (address >> 16) & 0xFF,
+ (address >> 8) & 0xFF,
+ address & 0xFF,
+ )
+ address = '.'.join(map(str,address))
+ return address
+
+
+class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
+ """Bare protocol to receive a Direct Client Connection SEND stream.
+
+ This does enough to keep the other guy talking, but you'll want to
+ extend my dataReceived method to *do* something with the data I get.
+ """
+
+ bytesReceived = 0
+
+ def __init__(self, resumeOffset=0):
+ self.bytesReceived = resumeOffset
+ self.resume = (resumeOffset != 0)
+
+ def dataReceived(self, data):
+ """Called when data is received.
+
+ Warning: This just acknowledges to the remote host that the
+ data has been received; it doesn't *do* anything with the
+ data, so you'll want to override this.
+ """
+ self.bytesReceived = self.bytesReceived + len(data)
+ self.transport.write(struct.pack('!i', self.bytesReceived))
+
+
+class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
+ """Protocol for an outgoing Direct Client Connection SEND.
+ """
+
+ blocksize = 1024
+ file = None
+ bytesSent = 0
+ completed = 0
+ connected = 0
+
+ def __init__(self, file):
+ if type(file) is types.StringType:
+ self.file = open(file, 'r')
+
+ def connectionMade(self):
+ self.connected = 1
+ self.sendBlock()
+
+ def dataReceived(self, data):
+ # XXX: Do we need to check to see if len(data) != fmtsize?
+
+ bytesShesGot = struct.unpack("!I", data)
+ if bytesShesGot < self.bytesSent:
+ # Wait for her.
+ # XXX? Add some checks to see if we've stalled out?
+ return
+ elif bytesShesGot > self.bytesSent:
+ # self.transport.log("DCC SEND %s: She says she has %d bytes "
+ # "but I've only sent %d. I'm stopping "
+ # "this screwy transfer."
+ # % (self.file,
+ # bytesShesGot, self.bytesSent))
+ self.transport.loseConnection()
+ return
+
+ self.sendBlock()
+
+ def sendBlock(self):
+ block = self.file.read(self.blocksize)
+ if block:
+ self.transport.write(block)
+ self.bytesSent = self.bytesSent + len(block)
+ else:
+ # Nothing more to send, transfer complete.
+ self.transport.loseConnection()
+ self.completed = 1
+
+ def connectionLost(self, reason):
+ self.connected = 0
+ if hasattr(self.file, "close"):
+ self.file.close()
+
+
+class DccSendFactory(protocol.Factory):
+ protocol = DccSendProtocol
+ def __init__(self, file):
+ self.file = file
+
+ def buildProtocol(self, connection):
+ p = self.protocol(self.file)
+ p.factory = self
+ return p
+
+
+def fileSize(file):
+ """I'll try my damndest to determine the size of this file object.
+ """
+ size = None
+ if hasattr(file, "fileno"):
+ fileno = file.fileno()
+ try:
+ stat_ = os.fstat(fileno)
+ size = stat_[stat.ST_SIZE]
+ except:
+ pass
+ else:
+ return size
+
+ if hasattr(file, "name") and path.exists(file.name):
+ try:
+ size = path.getsize(file.name)
+ except:
+ pass
+ else:
+ return size
+
+ if hasattr(file, "seek") and hasattr(file, "tell"):
+ try:
+ try:
+ file.seek(0, 2)
+ size = file.tell()
+ finally:
+ file.seek(0, 0)
+ except:
+ pass
+ else:
+ return size
+
+ return size
+
+class DccChat(basic.LineReceiver, styles.Ephemeral):
+ """Direct Client Connection protocol type CHAT.
+
+ DCC CHAT is really just your run o' the mill basic.LineReceiver
+ protocol. This class only varies from that slightly, accepting
+ either LF or CR LF for a line delimeter for incoming messages
+ while always using CR LF for outgoing.
+
+ The lineReceived method implemented here uses the DCC connection's
+ 'client' attribute (provided upon construction) to deliver incoming
+ lines from the DCC chat via IRCClient's normal privmsg interface.
+ That's something of a spoof, which you may well want to override.
+ """
+
+ queryData = None
+ delimiter = CR + NL
+ client = None
+ remoteParty = None
+ buffer = ""
+
+ def __init__(self, client, queryData=None):
+ """Initialize a new DCC CHAT session.
+
+ queryData is a 3-tuple of
+ (fromUser, targetUserOrChannel, data)
+ as received by the CTCP query.
+
+ (To be honest, fromUser is the only thing that's currently
+ used here. targetUserOrChannel is potentially useful, while
+ the 'data' argument is soley for informational purposes.)
+ """
+ self.client = client
+ if queryData:
+ self.queryData = queryData
+ self.remoteParty = self.queryData[0]
+
+ def dataReceived(self, data):
+ self.buffer = self.buffer + data
+ lines = string.split(self.buffer, LF)
+ # Put the (possibly empty) element after the last LF back in the
+ # buffer
+ self.buffer = lines.pop()
+
+ for line in lines:
+ if line[-1] == CR:
+ line = line[:-1]
+ self.lineReceived(line)
+
+ def lineReceived(self, line):
+ log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
+ self.client.privmsg(self.remoteParty,
+ self.client.nickname, line)
+
+
+class DccChatFactory(protocol.ClientFactory):
+ protocol = DccChat
+ noisy = 0
+ def __init__(self, client, queryData):
+ self.client = client
+ self.queryData = queryData
+
+
+ def buildProtocol(self, addr):
+ p = self.protocol(client=self.client, queryData=self.queryData)
+ p.factory = self
+ return p
+
+
+ def clientConnectionFailed(self, unused_connector, unused_reason):
+ self.client.dcc_sessions.remove(self)
+
+ def clientConnectionLost(self, unused_connector, unused_reason):
+ self.client.dcc_sessions.remove(self)
+
+
+def dccDescribe(data):
+ """Given the data chunk from a DCC query, return a descriptive string.
+ """
+
+ orig_data = data
+ data = string.split(data)
+ if len(data) < 4:
+ return orig_data
+
+ (dcctype, arg, address, port) = data[:4]
+
+ if '.' in address:
+ pass
+ else:
+ try:
+ address = long(address)
+ except ValueError:
+ pass
+ else:
+ address = (
+ (address >> 24) & 0xFF,
+ (address >> 16) & 0xFF,
+ (address >> 8) & 0xFF,
+ address & 0xFF,
+ )
+ # The mapping to 'int' is to get rid of those accursed
+ # "L"s which python 1.5.2 puts on the end of longs.
+ address = string.join(map(str,map(int,address)), ".")
+
+ if dcctype == 'SEND':
+ filename = arg
+
+ size_txt = ''
+ if len(data) >= 5:
+ try:
+ size = int(data[4])
+ size_txt = ' of size %d bytes' % (size,)
+ except ValueError:
+ pass
+
+ dcc_text = ("SEND for file '%s'%s at host %s, port %s"
+ % (filename, size_txt, address, port))
+ elif dcctype == 'CHAT':
+ dcc_text = ("CHAT for host %s, port %s"
+ % (address, port))
+ else:
+ dcc_text = orig_data
+
+ return dcc_text
+
+
+class DccFileReceive(DccFileReceiveBasic):
+ """Higher-level coverage for getting a file from DCC SEND.
+
+ I allow you to change the file's name and destination directory.
+ I won't overwrite an existing file unless I've been told it's okay
+ to do so. If passed the resumeOffset keyword argument I will attempt to
+ resume the file from that amount of bytes.
+
+ XXX: I need to let the client know when I am finished.
+ XXX: I need to decide how to keep a progress indicator updated.
+ XXX: Client needs a way to tell me "Do not finish until I say so."
+ XXX: I need to make sure the client understands if the file cannot be written.
+ """
+
+ filename = 'dcc'
+ fileSize = -1
+ destDir = '.'
+ overwrite = 0
+ fromUser = None
+ queryData = None
+
+ def __init__(self, filename, fileSize=-1, queryData=None,
+ destDir='.', resumeOffset=0):
+ DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
+ self.filename = filename
+ self.destDir = destDir
+ self.fileSize = fileSize
+
+ if queryData:
+ self.queryData = queryData
+ self.fromUser = self.queryData[0]
+
+ def set_directory(self, directory):
+ """Set the directory where the downloaded file will be placed.
+
+ May raise OSError if the supplied directory path is not suitable.
+ """
+ if not path.exists(directory):
+ raise OSError(errno.ENOENT, "You see no directory there.",
+ directory)
+ if not path.isdir(directory):
+ raise OSError(errno.ENOTDIR, "You cannot put a file into "
+ "something which is not a directory.",
+ directory)
+ if not os.access(directory, os.X_OK | os.W_OK):
+ raise OSError(errno.EACCES,
+ "This directory is too hard to write in to.",
+ directory)
+ self.destDir = directory
+
+ def set_filename(self, filename):
+ """Change the name of the file being transferred.
+
+ This replaces the file name provided by the sender.
+ """
+ self.filename = filename
+
+ def set_overwrite(self, boolean):
+ """May I overwrite existing files?
+ """
+ self.overwrite = boolean
+
+
+ # Protocol-level methods.
+
+ def connectionMade(self):
+ dst = path.abspath(path.join(self.destDir,self.filename))
+ exists = path.exists(dst)
+ if self.resume and exists:
+ # I have been told I want to resume, and a file already
+ # exists - Here we go
+ self.file = open(dst, 'ab')
+ log.msg("Attempting to resume %s - starting from %d bytes" %
+ (self.file, self.file.tell()))
+ elif self.overwrite or not exists:
+ self.file = open(dst, 'wb')
+ else:
+ raise OSError(errno.EEXIST,
+ "There's a file in the way. "
+ "Perhaps that's why you cannot open it.",
+ dst)
+
+ def dataReceived(self, data):
+ self.file.write(data)
+ DccFileReceiveBasic.dataReceived(self, data)
+
+ # XXX: update a progress indicator here?
+
+ def connectionLost(self, reason):
+ """When the connection is lost, I close the file.
+ """
+ self.connected = 0
+ logmsg = ("%s closed." % (self,))
+ if self.fileSize > 0:
+ logmsg = ("%s %d/%d bytes received"
+ % (logmsg, self.bytesReceived, self.fileSize))
+ if self.bytesReceived == self.fileSize:
+ pass # Hooray!
+ elif self.bytesReceived < self.fileSize:
+ logmsg = ("%s (Warning: %d bytes short)"
+ % (logmsg, self.fileSize - self.bytesReceived))
+ else:
+ logmsg = ("%s (file larger than expected)"
+ % (logmsg,))
+ else:
+ logmsg = ("%s %d bytes received"
+ % (logmsg, self.bytesReceived))
+
+ if hasattr(self, 'file'):
+ logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
+ if hasattr(self.file, 'close'): self.file.close()
+
+ # self.transport.log(logmsg)
+
+ def __str__(self):
+ if not self.connected:
+ return "<Unconnected DccFileReceive object at %x>" % (id(self),)
+ from_ = self.transport.getPeer()
+ if self.fromUser:
+ from_ = "%s (%s)" % (self.fromUser, from_)
+
+ s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
+ return s
+
+ def __repr__(self):
+ s = ("<%s at %x: GET %s>"
+ % (self.__class__, id(self), self.filename))
+ return s
+
+
+# CTCP constants and helper functions
+
+X_DELIM = chr(001)
+
+def ctcpExtract(message):
+ """
+ Extract CTCP data from a string.
+
+ @return: A C{dict} containing two keys:
+ - C{'extended'}: A list of CTCP (tag, data) tuples.
+ - C{'normal'}: A list of strings which were not inside a CTCP delimiter.
+ """
+ extended_messages = []
+ normal_messages = []
+ retval = {'extended': extended_messages,
+ 'normal': normal_messages }
+
+ messages = string.split(message, X_DELIM)
+ odd = 0
+
+ # X1 extended data X2 nomal data X3 extended data X4 normal...
+ while messages:
+ if odd:
+ extended_messages.append(messages.pop(0))
+ else:
+ normal_messages.append(messages.pop(0))
+ odd = not odd
+
+ extended_messages[:] = filter(None, extended_messages)
+ normal_messages[:] = filter(None, normal_messages)
+
+ extended_messages[:] = map(ctcpDequote, extended_messages)
+ for i in xrange(len(extended_messages)):
+ m = string.split(extended_messages[i], SPC, 1)
+ tag = m[0]
+ if len(m) > 1:
+ data = m[1]
+ else:
+ data = None
+
+ extended_messages[i] = (tag, data)
+
+ return retval
+
+# CTCP escaping
+
+M_QUOTE= chr(020)
+
+mQuoteTable = {
+ NUL: M_QUOTE + '0',
+ NL: M_QUOTE + 'n',
+ CR: M_QUOTE + 'r',
+ M_QUOTE: M_QUOTE + M_QUOTE
+ }
+
+mDequoteTable = {}
+for k, v in mQuoteTable.items():
+ mDequoteTable[v[-1]] = k
+del k, v
+
+mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
+
+def lowQuote(s):
+ for c in (M_QUOTE, NUL, NL, CR):
+ s = string.replace(s, c, mQuoteTable[c])
+ return s
+
+def lowDequote(s):
+ def sub(matchobj, mDequoteTable=mDequoteTable):
+ s = matchobj.group()[1]
+ try:
+ s = mDequoteTable[s]
+ except KeyError:
+ s = s
+ return s
+
+ return mEscape_re.sub(sub, s)
+
+X_QUOTE = '\\'
+
+xQuoteTable = {
+ X_DELIM: X_QUOTE + 'a',
+ X_QUOTE: X_QUOTE + X_QUOTE
+ }
+
+xDequoteTable = {}
+
+for k, v in xQuoteTable.items():
+ xDequoteTable[v[-1]] = k
+
+xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
+
+def ctcpQuote(s):
+ for c in (X_QUOTE, X_DELIM):
+ s = string.replace(s, c, xQuoteTable[c])
+ return s
+
+def ctcpDequote(s):
+ def sub(matchobj, xDequoteTable=xDequoteTable):
+ s = matchobj.group()[1]
+ try:
+ s = xDequoteTable[s]
+ except KeyError:
+ s = s
+ return s
+
+ return xEscape_re.sub(sub, s)
+
+def ctcpStringify(messages):
+ """
+ @type messages: a list of extended messages. An extended
+ message is a (tag, data) tuple, where 'data' may be C{None}, a
+ string, or a list of strings to be joined with whitespace.
+
+ @returns: String
+ """
+ coded_messages = []
+ for (tag, data) in messages:
+ if data:
+ if not isinstance(data, types.StringType):
+ try:
+ # data as list-of-strings
+ data = " ".join(map(str, data))
+ except TypeError:
+ # No? Then use it's %s representation.
+ pass
+ m = "%s %s" % (tag, data)
+ else:
+ m = str(tag)
+ m = ctcpQuote(m)
+ m = "%s%s%s" % (X_DELIM, m, X_DELIM)
+ coded_messages.append(m)
+
+ line = string.join(coded_messages, '')
+ return line
+
+
+# Constants (from RFC 2812)
+RPL_WELCOME = '001'
+RPL_YOURHOST = '002'
+RPL_CREATED = '003'
+RPL_MYINFO = '004'
+RPL_ISUPPORT = '005'
+RPL_BOUNCE = '010'
+RPL_USERHOST = '302'
+RPL_ISON = '303'
+RPL_AWAY = '301'
+RPL_UNAWAY = '305'
+RPL_NOWAWAY = '306'
+RPL_WHOISUSER = '311'
+RPL_WHOISSERVER = '312'
+RPL_WHOISOPERATOR = '313'
+RPL_WHOISIDLE = '317'
+RPL_ENDOFWHOIS = '318'
+RPL_WHOISCHANNELS = '319'
+RPL_WHOWASUSER = '314'
+RPL_ENDOFWHOWAS = '369'
+RPL_LISTSTART = '321'
+RPL_LIST = '322'
+RPL_LISTEND = '323'
+RPL_UNIQOPIS = '325'
+RPL_CHANNELMODEIS = '324'
+RPL_NOTOPIC = '331'
+RPL_TOPIC = '332'
+RPL_INVITING = '341'
+RPL_SUMMONING = '342'
+RPL_INVITELIST = '346'
+RPL_ENDOFINVITELIST = '347'
+RPL_EXCEPTLIST = '348'
+RPL_ENDOFEXCEPTLIST = '349'
+RPL_VERSION = '351'
+RPL_WHOREPLY = '352'
+RPL_ENDOFWHO = '315'
+RPL_NAMREPLY = '353'
+RPL_ENDOFNAMES = '366'
+RPL_LINKS = '364'
+RPL_ENDOFLINKS = '365'
+RPL_BANLIST = '367'
+RPL_ENDOFBANLIST = '368'
+RPL_INFO = '371'
+RPL_ENDOFINFO = '374'
+RPL_MOTDSTART = '375'
+RPL_MOTD = '372'
+RPL_ENDOFMOTD = '376'
+RPL_YOUREOPER = '381'
+RPL_REHASHING = '382'
+RPL_YOURESERVICE = '383'
+RPL_TIME = '391'
+RPL_USERSSTART = '392'
+RPL_USERS = '393'
+RPL_ENDOFUSERS = '394'
+RPL_NOUSERS = '395'
+RPL_TRACELINK = '200'
+RPL_TRACECONNECTING = '201'
+RPL_TRACEHANDSHAKE = '202'
+RPL_TRACEUNKNOWN = '203'
+RPL_TRACEOPERATOR = '204'
+RPL_TRACEUSER = '205'
+RPL_TRACESERVER = '206'
+RPL_TRACESERVICE = '207'
+RPL_TRACENEWTYPE = '208'
+RPL_TRACECLASS = '209'
+RPL_TRACERECONNECT = '210'
+RPL_TRACELOG = '261'
+RPL_TRACEEND = '262'
+RPL_STATSLINKINFO = '211'
+RPL_STATSCOMMANDS = '212'
+RPL_ENDOFSTATS = '219'
+RPL_STATSUPTIME = '242'
+RPL_STATSOLINE = '243'
+RPL_UMODEIS = '221'
+RPL_SERVLIST = '234'
+RPL_SERVLISTEND = '235'
+RPL_LUSERCLIENT = '251'
+RPL_LUSEROP = '252'
+RPL_LUSERUNKNOWN = '253'
+RPL_LUSERCHANNELS = '254'
+RPL_LUSERME = '255'
+RPL_ADMINME = '256'
+RPL_ADMINLOC = '257'
+RPL_ADMINLOC = '258'
+RPL_ADMINEMAIL = '259'
+RPL_TRYAGAIN = '263'
+ERR_NOSUCHNICK = '401'
+ERR_NOSUCHSERVER = '402'
+ERR_NOSUCHCHANNEL = '403'
+ERR_CANNOTSENDTOCHAN = '404'
+ERR_TOOMANYCHANNELS = '405'
+ERR_WASNOSUCHNICK = '406'
+ERR_TOOMANYTARGETS = '407'
+ERR_NOSUCHSERVICE = '408'
+ERR_NOORIGIN = '409'
+ERR_NORECIPIENT = '411'
+ERR_NOTEXTTOSEND = '412'
+ERR_NOTOPLEVEL = '413'
+ERR_WILDTOPLEVEL = '414'
+ERR_BADMASK = '415'
+ERR_UNKNOWNCOMMAND = '421'
+ERR_NOMOTD = '422'
+ERR_NOADMININFO = '423'
+ERR_FILEERROR = '424'
+ERR_NONICKNAMEGIVEN = '431'
+ERR_ERRONEUSNICKNAME = '432'
+ERR_NICKNAMEINUSE = '433'
+ERR_NICKCOLLISION = '436'
+ERR_UNAVAILRESOURCE = '437'
+ERR_USERNOTINCHANNEL = '441'
+ERR_NOTONCHANNEL = '442'
+ERR_USERONCHANNEL = '443'
+ERR_NOLOGIN = '444'
+ERR_SUMMONDISABLED = '445'
+ERR_USERSDISABLED = '446'
+ERR_NOTREGISTERED = '451'
+ERR_NEEDMOREPARAMS = '461'
+ERR_ALREADYREGISTRED = '462'
+ERR_NOPERMFORHOST = '463'
+ERR_PASSWDMISMATCH = '464'
+ERR_YOUREBANNEDCREEP = '465'
+ERR_YOUWILLBEBANNED = '466'
+ERR_KEYSET = '467'
+ERR_CHANNELISFULL = '471'
+ERR_UNKNOWNMODE = '472'
+ERR_INVITEONLYCHAN = '473'
+ERR_BANNEDFROMCHAN = '474'
+ERR_BADCHANNELKEY = '475'
+ERR_BADCHANMASK = '476'
+ERR_NOCHANMODES = '477'
+ERR_BANLISTFULL = '478'
+ERR_NOPRIVILEGES = '481'
+ERR_CHANOPRIVSNEEDED = '482'
+ERR_CANTKILLSERVER = '483'
+ERR_RESTRICTED = '484'
+ERR_UNIQOPPRIVSNEEDED = '485'
+ERR_NOOPERHOST = '491'
+ERR_NOSERVICEHOST = '492'
+ERR_UMODEUNKNOWNFLAG = '501'
+ERR_USERSDONTMATCH = '502'
+
+# And hey, as long as the strings are already intern'd...
+symbolic_to_numeric = {
+ "RPL_WELCOME": '001',
+ "RPL_YOURHOST": '002',
+ "RPL_CREATED": '003',
+ "RPL_MYINFO": '004',
+ "RPL_ISUPPORT": '005',
+ "RPL_BOUNCE": '010',
+ "RPL_USERHOST": '302',
+ "RPL_ISON": '303',
+ "RPL_AWAY": '301',
+ "RPL_UNAWAY": '305',
+ "RPL_NOWAWAY": '306',
+ "RPL_WHOISUSER": '311',
+ "RPL_WHOISSERVER": '312',
+ "RPL_WHOISOPERATOR": '313',
+ "RPL_WHOISIDLE": '317',
+ "RPL_ENDOFWHOIS": '318',
+ "RPL_WHOISCHANNELS": '319',
+ "RPL_WHOWASUSER": '314',
+ "RPL_ENDOFWHOWAS": '369',
+ "RPL_LISTSTART": '321',
+ "RPL_LIST": '322',
+ "RPL_LISTEND": '323',
+ "RPL_UNIQOPIS": '325',
+ "RPL_CHANNELMODEIS": '324',
+ "RPL_NOTOPIC": '331',
+ "RPL_TOPIC": '332',
+ "RPL_INVITING": '341',
+ "RPL_SUMMONING": '342',
+ "RPL_INVITELIST": '346',
+ "RPL_ENDOFINVITELIST": '347',
+ "RPL_EXCEPTLIST": '348',
+ "RPL_ENDOFEXCEPTLIST": '349',
+ "RPL_VERSION": '351',
+ "RPL_WHOREPLY": '352',
+ "RPL_ENDOFWHO": '315',
+ "RPL_NAMREPLY": '353',
+ "RPL_ENDOFNAMES": '366',
+ "RPL_LINKS": '364',
+ "RPL_ENDOFLINKS": '365',
+ "RPL_BANLIST": '367',
+ "RPL_ENDOFBANLIST": '368',
+ "RPL_INFO": '371',
+ "RPL_ENDOFINFO": '374',
+ "RPL_MOTDSTART": '375',
+ "RPL_MOTD": '372',
+ "RPL_ENDOFMOTD": '376',
+ "RPL_YOUREOPER": '381',
+ "RPL_REHASHING": '382',
+ "RPL_YOURESERVICE": '383',
+ "RPL_TIME": '391',
+ "RPL_USERSSTART": '392',
+ "RPL_USERS": '393',
+ "RPL_ENDOFUSERS": '394',
+ "RPL_NOUSERS": '395',
+ "RPL_TRACELINK": '200',
+ "RPL_TRACECONNECTING": '201',
+ "RPL_TRACEHANDSHAKE": '202',
+ "RPL_TRACEUNKNOWN": '203',
+ "RPL_TRACEOPERATOR": '204',
+ "RPL_TRACEUSER": '205',
+ "RPL_TRACESERVER": '206',
+ "RPL_TRACESERVICE": '207',
+ "RPL_TRACENEWTYPE": '208',
+ "RPL_TRACECLASS": '209',
+ "RPL_TRACERECONNECT": '210',
+ "RPL_TRACELOG": '261',
+ "RPL_TRACEEND": '262',
+ "RPL_STATSLINKINFO": '211',
+ "RPL_STATSCOMMANDS": '212',
+ "RPL_ENDOFSTATS": '219',
+ "RPL_STATSUPTIME": '242',
+ "RPL_STATSOLINE": '243',
+ "RPL_UMODEIS": '221',
+ "RPL_SERVLIST": '234',
+ "RPL_SERVLISTEND": '235',
+ "RPL_LUSERCLIENT": '251',
+ "RPL_LUSEROP": '252',
+ "RPL_LUSERUNKNOWN": '253',
+ "RPL_LUSERCHANNELS": '254',
+ "RPL_LUSERME": '255',
+ "RPL_ADMINME": '256',
+ "RPL_ADMINLOC": '257',
+ "RPL_ADMINLOC": '258',
+ "RPL_ADMINEMAIL": '259',
+ "RPL_TRYAGAIN": '263',
+ "ERR_NOSUCHNICK": '401',
+ "ERR_NOSUCHSERVER": '402',
+ "ERR_NOSUCHCHANNEL": '403',
+ "ERR_CANNOTSENDTOCHAN": '404',
+ "ERR_TOOMANYCHANNELS": '405',
+ "ERR_WASNOSUCHNICK": '406',
+ "ERR_TOOMANYTARGETS": '407',
+ "ERR_NOSUCHSERVICE": '408',
+ "ERR_NOORIGIN": '409',
+ "ERR_NORECIPIENT": '411',
+ "ERR_NOTEXTTOSEND": '412',
+ "ERR_NOTOPLEVEL": '413',
+ "ERR_WILDTOPLEVEL": '414',
+ "ERR_BADMASK": '415',
+ "ERR_UNKNOWNCOMMAND": '421',
+ "ERR_NOMOTD": '422',
+ "ERR_NOADMININFO": '423',
+ "ERR_FILEERROR": '424',
+ "ERR_NONICKNAMEGIVEN": '431',
+ "ERR_ERRONEUSNICKNAME": '432',
+ "ERR_NICKNAMEINUSE": '433',
+ "ERR_NICKCOLLISION": '436',
+ "ERR_UNAVAILRESOURCE": '437',
+ "ERR_USERNOTINCHANNEL": '441',
+ "ERR_NOTONCHANNEL": '442',
+ "ERR_USERONCHANNEL": '443',
+ "ERR_NOLOGIN": '444',
+ "ERR_SUMMONDISABLED": '445',
+ "ERR_USERSDISABLED": '446',
+ "ERR_NOTREGISTERED": '451',
+ "ERR_NEEDMOREPARAMS": '461',
+ "ERR_ALREADYREGISTRED": '462',
+ "ERR_NOPERMFORHOST": '463',
+ "ERR_PASSWDMISMATCH": '464',
+ "ERR_YOUREBANNEDCREEP": '465',
+ "ERR_YOUWILLBEBANNED": '466',
+ "ERR_KEYSET": '467',
+ "ERR_CHANNELISFULL": '471',
+ "ERR_UNKNOWNMODE": '472',
+ "ERR_INVITEONLYCHAN": '473',
+ "ERR_BANNEDFROMCHAN": '474',
+ "ERR_BADCHANNELKEY": '475',
+ "ERR_BADCHANMASK": '476',
+ "ERR_NOCHANMODES": '477',
+ "ERR_BANLISTFULL": '478',
+ "ERR_NOPRIVILEGES": '481',
+ "ERR_CHANOPRIVSNEEDED": '482',
+ "ERR_CANTKILLSERVER": '483',
+ "ERR_RESTRICTED": '484',
+ "ERR_UNIQOPPRIVSNEEDED": '485',
+ "ERR_NOOPERHOST": '491',
+ "ERR_NOSERVICEHOST": '492',
+ "ERR_UMODEUNKNOWNFLAG": '501',
+ "ERR_USERSDONTMATCH": '502',
+}
+
+numeric_to_symbolic = {}
+for k, v in symbolic_to_numeric.items():
+ numeric_to_symbolic[v] = k
diff --git a/twisted/words/protocols/jabber/__init__.py b/twisted/words/protocols/jabber/__init__.py
new file mode 100644
index 0000000..ad95b68
--- /dev/null
+++ b/twisted/words/protocols/jabber/__init__.py
@@ -0,0 +1,8 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Twisted Jabber: Jabber Protocol Helpers
+"""
diff --git a/twisted/words/protocols/jabber/client.py b/twisted/words/protocols/jabber/client.py
new file mode 100644
index 0000000..2a37bcb
--- /dev/null
+++ b/twisted/words/protocols/jabber/client.py
@@ -0,0 +1,368 @@
+# -*- test-case-name: twisted.words.test.test_jabberclient -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.words.xish import domish, xpath, utility
+from twisted.words.protocols.jabber import xmlstream, sasl, error
+from twisted.words.protocols.jabber.jid import JID
+
+NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
+NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
+NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'
+NS_IQ_AUTH_FEATURE = 'http://jabber.org/features/iq-auth'
+
+DigestAuthQry = xpath.internQuery("/iq/query/digest")
+PlaintextAuthQry = xpath.internQuery("/iq/query/password")
+
+def basicClientFactory(jid, secret):
+ a = BasicAuthenticator(jid, secret)
+ return xmlstream.XmlStreamFactory(a)
+
+class IQ(domish.Element):
+ """
+ Wrapper for a Info/Query packet.
+
+ This provides the necessary functionality to send IQs and get notified when
+ a result comes back. It's a subclass from L{domish.Element}, so you can use
+ the standard DOM manipulation calls to add data to the outbound request.
+
+ @type callbacks: L{utility.CallbackList}
+ @cvar callbacks: Callback list to be notified when response comes back
+
+ """
+ def __init__(self, xmlstream, type = "set"):
+ """
+ @type xmlstream: L{xmlstream.XmlStream}
+ @param xmlstream: XmlStream to use for transmission of this IQ
+
+ @type type: C{str}
+ @param type: IQ type identifier ('get' or 'set')
+ """
+
+ domish.Element.__init__(self, ("jabber:client", "iq"))
+ self.addUniqueId()
+ self["type"] = type
+ self._xmlstream = xmlstream
+ self.callbacks = utility.CallbackList()
+
+ def addCallback(self, fn, *args, **kwargs):
+ """
+ Register a callback for notification when the IQ result is available.
+ """
+
+ self.callbacks.addCallback(True, fn, *args, **kwargs)
+
+ def send(self, to = None):
+ """
+ Call this method to send this IQ request via the associated XmlStream.
+
+ @param to: Jabber ID of the entity to send the request to
+ @type to: C{str}
+
+ @returns: Callback list for this IQ. Any callbacks added to this list
+ will be fired when the result comes back.
+ """
+ if to != None:
+ self["to"] = to
+ self._xmlstream.addOnetimeObserver("/iq[@id='%s']" % self["id"], \
+ self._resultEvent)
+ self._xmlstream.send(self)
+
+ def _resultEvent(self, iq):
+ self.callbacks.callback(iq)
+ self.callbacks = None
+
+
+
+class IQAuthInitializer(object):
+ """
+ Non-SASL Authentication initializer for the initiating entity.
+
+ This protocol is defined in
+ U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>} and mainly serves for
+ compatibility with pre-XMPP-1.0 server implementations.
+ """
+
+ INVALID_USER_EVENT = "//event/client/basicauth/invaliduser"
+ AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed"
+
+ def __init__(self, xs):
+ self.xmlstream = xs
+
+
+ def initialize(self):
+ # Send request for auth fields
+ iq = xmlstream.IQ(self.xmlstream, "get")
+ iq.addElement(("jabber:iq:auth", "query"))
+ jid = self.xmlstream.authenticator.jid
+ iq.query.addElement("username", content = jid.user)
+
+ d = iq.send()
+ d.addCallbacks(self._cbAuthQuery, self._ebAuthQuery)
+ return d
+
+
+ def _cbAuthQuery(self, iq):
+ jid = self.xmlstream.authenticator.jid
+ password = self.xmlstream.authenticator.password
+
+ # Construct auth request
+ reply = xmlstream.IQ(self.xmlstream, "set")
+ reply.addElement(("jabber:iq:auth", "query"))
+ reply.query.addElement("username", content = jid.user)
+ reply.query.addElement("resource", content = jid.resource)
+
+ # Prefer digest over plaintext
+ if DigestAuthQry.matches(iq):
+ digest = xmlstream.hashPassword(self.xmlstream.sid, unicode(password))
+ reply.query.addElement("digest", content = digest)
+ else:
+ reply.query.addElement("password", content = password)
+
+ d = reply.send()
+ d.addCallbacks(self._cbAuth, self._ebAuth)
+ return d
+
+
+ def _ebAuthQuery(self, failure):
+ failure.trap(error.StanzaError)
+ e = failure.value
+ if e.condition == 'not-authorized':
+ self.xmlstream.dispatch(e.stanza, self.INVALID_USER_EVENT)
+ else:
+ self.xmlstream.dispatch(e.stanza, self.AUTH_FAILED_EVENT)
+
+ return failure
+
+
+ def _cbAuth(self, iq):
+ pass
+
+
+ def _ebAuth(self, failure):
+ failure.trap(error.StanzaError)
+ self.xmlstream.dispatch(failure.value.stanza, self.AUTH_FAILED_EVENT)
+ return failure
+
+
+
+class BasicAuthenticator(xmlstream.ConnectAuthenticator):
+ """
+ Authenticates an XmlStream against a Jabber server as a Client.
+
+ This only implements non-SASL authentication, per
+ U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this
+ authenticator provides the ability to perform inline registration, per
+ U{JEP-0077<http://www.jabber.org/jeps/jep-0077.html>}.
+
+ Under normal circumstances, the BasicAuthenticator generates the
+ L{xmlstream.STREAM_AUTHD_EVENT} once the stream has authenticated. However,
+ it can also generate other events, such as:
+ - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username
+ - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password
+ - L{REGISTER_FAILED_EVENT} : Registration failed
+
+ If authentication fails for any reason, you can attempt to register by
+ calling the L{registerAccount} method. If the registration succeeds, a
+ L{xmlstream.STREAM_AUTHD_EVENT} will be fired. Otherwise, one of the above
+ errors will be generated (again).
+ """
+
+ namespace = "jabber:client"
+
+ INVALID_USER_EVENT = IQAuthInitializer.INVALID_USER_EVENT
+ AUTH_FAILED_EVENT = IQAuthInitializer.AUTH_FAILED_EVENT
+ REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed"
+
+ def __init__(self, jid, password):
+ xmlstream.ConnectAuthenticator.__init__(self, jid.host)
+ self.jid = jid
+ self.password = password
+
+ def associateWithStream(self, xs):
+ xs.version = (0, 0)
+ xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
+
+ inits = [ (xmlstream.TLSInitiatingInitializer, False),
+ (IQAuthInitializer, True),
+ ]
+
+ for initClass, required in inits:
+ init = initClass(xs)
+ init.required = required
+ xs.initializers.append(init)
+
+ # TODO: move registration into an Initializer?
+
+ def registerAccount(self, username = None, password = None):
+ if username:
+ self.jid.user = username
+ if password:
+ self.password = password
+
+ iq = IQ(self.xmlstream, "set")
+ iq.addElement(("jabber:iq:register", "query"))
+ iq.query.addElement("username", content = self.jid.user)
+ iq.query.addElement("password", content = self.password)
+
+ iq.addCallback(self._registerResultEvent)
+
+ iq.send()
+
+ def _registerResultEvent(self, iq):
+ if iq["type"] == "result":
+ # Registration succeeded -- go ahead and auth
+ self.streamStarted()
+ else:
+ # Registration failed
+ self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT)
+
+
+
+class CheckVersionInitializer(object):
+ """
+ Initializer that checks if the minimum common stream version number is 1.0.
+ """
+
+ def __init__(self, xs):
+ self.xmlstream = xs
+
+
+ def initialize(self):
+ if self.xmlstream.version < (1, 0):
+ raise error.StreamError('unsupported-version')
+
+
+
+class BindInitializer(xmlstream.BaseFeatureInitiatingInitializer):
+ """
+ Initializer that implements Resource Binding for the initiating entity.
+
+ This protocol is documented in U{RFC 3920, section
+ 7<http://www.xmpp.org/specs/rfc3920.html#bind>}.
+ """
+
+ feature = (NS_XMPP_BIND, 'bind')
+
+ def start(self):
+ iq = xmlstream.IQ(self.xmlstream, 'set')
+ bind = iq.addElement((NS_XMPP_BIND, 'bind'))
+ resource = self.xmlstream.authenticator.jid.resource
+ if resource:
+ bind.addElement('resource', content=resource)
+ d = iq.send()
+ d.addCallback(self.onBind)
+ return d
+
+
+ def onBind(self, iq):
+ if iq.bind:
+ self.xmlstream.authenticator.jid = JID(unicode(iq.bind.jid))
+
+
+
+class SessionInitializer(xmlstream.BaseFeatureInitiatingInitializer):
+ """
+ Initializer that implements session establishment for the initiating
+ entity.
+
+ This protocol is defined in U{RFC 3921, section
+ 3<http://www.xmpp.org/specs/rfc3921.html#session>}.
+ """
+
+ feature = (NS_XMPP_SESSION, 'session')
+
+ def start(self):
+ iq = xmlstream.IQ(self.xmlstream, 'set')
+ session = iq.addElement((NS_XMPP_SESSION, 'session'))
+ return iq.send()
+
+
+
+def XMPPClientFactory(jid, password):
+ """
+ Client factory for XMPP 1.0 (only).
+
+ This returns a L{xmlstream.XmlStreamFactory} with an L{XMPPAuthenticator}
+ object to perform the stream initialization steps (such as authentication).
+
+ @see: The notes at L{XMPPAuthenticator} describe how the L{jid} and
+ L{password} parameters are to be used.
+
+ @param jid: Jabber ID to connect with.
+ @type jid: L{jid.JID}
+ @param password: password to authenticate with.
+ @type password: C{unicode}
+ @return: XML stream factory.
+ @rtype: L{xmlstream.XmlStreamFactory}
+ """
+ a = XMPPAuthenticator(jid, password)
+ return xmlstream.XmlStreamFactory(a)
+
+
+
+class XMPPAuthenticator(xmlstream.ConnectAuthenticator):
+ """
+ Initializes an XmlStream connecting to an XMPP server as a Client.
+
+ This authenticator performs the initialization steps needed to start
+ exchanging XML stanzas with an XMPP server as an XMPP client. It checks if
+ the server advertises XML stream version 1.0, negotiates TLS (when
+ available), performs SASL authentication, binds a resource and establishes
+ a session.
+
+ Upon successful stream initialization, the L{xmlstream.STREAM_AUTHD_EVENT}
+ event will be dispatched through the XML stream object. Otherwise, the
+ L{xmlstream.INIT_FAILED_EVENT} event will be dispatched with a failure
+ object.
+
+ After inspection of the failure, initialization can then be restarted by
+ calling L{initializeStream}. For example, in case of authentication
+ failure, a user may be given the opportunity to input the correct password.
+ By setting the L{password} instance variable and restarting initialization,
+ the stream authentication step is then retried, and subsequent steps are
+ performed if succesful.
+
+ @ivar jid: Jabber ID to authenticate with. This may contain a resource
+ part, as a suggestion to the server for resource binding. A
+ server may override this, though. If the resource part is left
+ off, the server will generate a unique resource identifier.
+ The server will always return the full Jabber ID in the
+ resource binding step, and this is stored in this instance
+ variable.
+ @type jid: L{jid.JID}
+ @ivar password: password to be used during SASL authentication.
+ @type password: C{unicode}
+ """
+
+ namespace = 'jabber:client'
+
+ def __init__(self, jid, password):
+ xmlstream.ConnectAuthenticator.__init__(self, jid.host)
+ self.jid = jid
+ self.password = password
+
+
+ def associateWithStream(self, xs):
+ """
+ Register with the XML stream.
+
+ Populates stream's list of initializers, along with their
+ requiredness. This list is used by
+ L{ConnectAuthenticator.initializeStream} to perform the initalization
+ steps.
+ """
+ xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
+
+ xs.initializers = [CheckVersionInitializer(xs)]
+ inits = [ (xmlstream.TLSInitiatingInitializer, False),
+ (sasl.SASLInitiatingInitializer, True),
+ (BindInitializer, False),
+ (SessionInitializer, False),
+ ]
+
+ for initClass, required in inits:
+ init = initClass(xs)
+ init.required = required
+ xs.initializers.append(init)
diff --git a/twisted/words/protocols/jabber/component.py b/twisted/words/protocols/jabber/component.py
new file mode 100644
index 0000000..1f37490
--- /dev/null
+++ b/twisted/words/protocols/jabber/component.py
@@ -0,0 +1,474 @@
+# -*- test-case-name: twisted.words.test.test_jabbercomponent -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+External server-side components.
+
+Most Jabber server implementations allow for add-on components that act as a
+seperate entity on the Jabber network, but use the server-to-server
+functionality of a regular Jabber IM server. These so-called 'external
+components' are connected to the Jabber server using the Jabber Component
+Protocol as defined in U{JEP-0114<http://www.jabber.org/jeps/jep-0114.html>}.
+
+This module allows for writing external server-side component by assigning one
+or more services implementing L{ijabber.IService} up to L{ServiceManager}. The
+ServiceManager connects to the Jabber server and is responsible for the
+corresponding XML stream.
+"""
+
+from zope.interface import implements
+
+from twisted.application import service
+from twisted.internet import defer
+from twisted.python import log
+from twisted.words.xish import domish
+from twisted.words.protocols.jabber import error, ijabber, jstrports, xmlstream
+from twisted.words.protocols.jabber.jid import internJID as JID
+
+NS_COMPONENT_ACCEPT = 'jabber:component:accept'
+
+def componentFactory(componentid, password):
+ """
+ XML stream factory for external server-side components.
+
+ @param componentid: JID of the component.
+ @type componentid: C{unicode}
+ @param password: password used to authenticate to the server.
+ @type password: C{str}
+ """
+ a = ConnectComponentAuthenticator(componentid, password)
+ return xmlstream.XmlStreamFactory(a)
+
+class ComponentInitiatingInitializer(object):
+ """
+ External server-side component authentication initializer for the
+ initiating entity.
+
+ @ivar xmlstream: XML stream between server and component.
+ @type xmlstream: L{xmlstream.XmlStream}
+ """
+
+ def __init__(self, xs):
+ self.xmlstream = xs
+ self._deferred = None
+
+ def initialize(self):
+ xs = self.xmlstream
+ hs = domish.Element((self.xmlstream.namespace, "handshake"))
+ hs.addContent(xmlstream.hashPassword(xs.sid,
+ unicode(xs.authenticator.password)))
+
+ # Setup observer to watch for handshake result
+ xs.addOnetimeObserver("/handshake", self._cbHandshake)
+ xs.send(hs)
+ self._deferred = defer.Deferred()
+ return self._deferred
+
+ def _cbHandshake(self, _):
+ # we have successfully shaken hands and can now consider this
+ # entity to represent the component JID.
+ self.xmlstream.thisEntity = self.xmlstream.otherEntity
+ self._deferred.callback(None)
+
+
+
+class ConnectComponentAuthenticator(xmlstream.ConnectAuthenticator):
+ """
+ Authenticator to permit an XmlStream to authenticate against a Jabber
+ server as an external component (where the Authenticator is initiating the
+ stream).
+ """
+ namespace = NS_COMPONENT_ACCEPT
+
+ def __init__(self, componentjid, password):
+ """
+ @type componentjid: C{str}
+ @param componentjid: Jabber ID that this component wishes to bind to.
+
+ @type password: C{str}
+ @param password: Password/secret this component uses to authenticate.
+ """
+ # Note that we are sending 'to' our desired component JID.
+ xmlstream.ConnectAuthenticator.__init__(self, componentjid)
+ self.password = password
+
+ def associateWithStream(self, xs):
+ xs.version = (0, 0)
+ xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
+
+ xs.initializers = [ComponentInitiatingInitializer(xs)]
+
+
+
+class ListenComponentAuthenticator(xmlstream.ListenAuthenticator):
+ """
+ Authenticator for accepting components.
+
+ @since: 8.2
+ @ivar secret: The shared secret used to authorized incoming component
+ connections.
+ @type secret: C{unicode}.
+ """
+
+ namespace = NS_COMPONENT_ACCEPT
+
+ def __init__(self, secret):
+ self.secret = secret
+ xmlstream.ListenAuthenticator.__init__(self)
+
+
+ def associateWithStream(self, xs):
+ """
+ Associate the authenticator with a stream.
+
+ This sets the stream's version to 0.0, because the XEP-0114 component
+ protocol was not designed for XMPP 1.0.
+ """
+ xs.version = (0, 0)
+ xmlstream.ListenAuthenticator.associateWithStream(self, xs)
+
+
+ def streamStarted(self, rootElement):
+ """
+ Called by the stream when it has started.
+
+ This examines the default namespace of the incoming stream and whether
+ there is a requested hostname for the component. Then it generates a
+ stream identifier, sends a response header and adds an observer for
+ the first incoming element, triggering L{onElement}.
+ """
+
+ xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
+
+ if rootElement.defaultUri != self.namespace:
+ exc = error.StreamError('invalid-namespace')
+ self.xmlstream.sendStreamError(exc)
+ return
+
+ # self.xmlstream.thisEntity is set to the address the component
+ # wants to assume.
+ if not self.xmlstream.thisEntity:
+ exc = error.StreamError('improper-addressing')
+ self.xmlstream.sendStreamError(exc)
+ return
+
+ self.xmlstream.sendHeader()
+ self.xmlstream.addOnetimeObserver('/*', self.onElement)
+
+
+ def onElement(self, element):
+ """
+ Called on incoming XML Stanzas.
+
+ The very first element received should be a request for handshake.
+ Otherwise, the stream is dropped with a 'not-authorized' error. If a
+ handshake request was received, the hash is extracted and passed to
+ L{onHandshake}.
+ """
+ if (element.uri, element.name) == (self.namespace, 'handshake'):
+ self.onHandshake(unicode(element))
+ else:
+ exc = error.StreamError('not-authorized')
+ self.xmlstream.sendStreamError(exc)
+
+
+ def onHandshake(self, handshake):
+ """
+ Called upon receiving the handshake request.
+
+ This checks that the given hash in C{handshake} is equal to a
+ calculated hash, responding with a handshake reply or a stream error.
+ If the handshake was ok, the stream is authorized, and XML Stanzas may
+ be exchanged.
+ """
+ calculatedHash = xmlstream.hashPassword(self.xmlstream.sid,
+ unicode(self.secret))
+ if handshake != calculatedHash:
+ exc = error.StreamError('not-authorized', text='Invalid hash')
+ self.xmlstream.sendStreamError(exc)
+ else:
+ self.xmlstream.send('<handshake/>')
+ self.xmlstream.dispatch(self.xmlstream,
+ xmlstream.STREAM_AUTHD_EVENT)
+
+
+
+class Service(service.Service):
+ """
+ External server-side component service.
+ """
+
+ implements(ijabber.IService)
+
+ def componentConnected(self, xs):
+ pass
+
+ def componentDisconnected(self):
+ pass
+
+ def transportConnected(self, xs):
+ pass
+
+ def send(self, obj):
+ """
+ Send data over service parent's XML stream.
+
+ @note: L{ServiceManager} maintains a queue for data sent using this
+ method when there is no current established XML stream. This data is
+ then sent as soon as a new stream has been established and initialized.
+ Subsequently, L{componentConnected} will be called again. If this
+ queueing is not desired, use C{send} on the XmlStream object (passed to
+ L{componentConnected}) directly.
+
+ @param obj: data to be sent over the XML stream. This is usually an
+ object providing L{domish.IElement}, or serialized XML. See
+ L{xmlstream.XmlStream} for details.
+ """
+
+ self.parent.send(obj)
+
+class ServiceManager(service.MultiService):
+ """
+ Business logic representing a managed component connection to a Jabber
+ router.
+
+ This service maintains a single connection to a Jabber router and provides
+ facilities for packet routing and transmission. Business logic modules are
+ services implementing L{ijabber.IService} (like subclasses of L{Service}), and
+ added as sub-service.
+ """
+
+ def __init__(self, jid, password):
+ service.MultiService.__init__(self)
+
+ # Setup defaults
+ self.jabberId = jid
+ self.xmlstream = None
+
+ # Internal buffer of packets
+ self._packetQueue = []
+
+ # Setup the xmlstream factory
+ self._xsFactory = componentFactory(self.jabberId, password)
+
+ # Register some lambda functions to keep the self.xmlstream var up to
+ # date
+ self._xsFactory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
+ self._connected)
+ self._xsFactory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd)
+ self._xsFactory.addBootstrap(xmlstream.STREAM_END_EVENT,
+ self._disconnected)
+
+ # Map addBootstrap and removeBootstrap to the underlying factory -- is
+ # this right? I have no clue...but it'll work for now, until i can
+ # think about it more.
+ self.addBootstrap = self._xsFactory.addBootstrap
+ self.removeBootstrap = self._xsFactory.removeBootstrap
+
+ def getFactory(self):
+ return self._xsFactory
+
+ def _connected(self, xs):
+ self.xmlstream = xs
+ for c in self:
+ if ijabber.IService.providedBy(c):
+ c.transportConnected(xs)
+
+ def _authd(self, xs):
+ # Flush all pending packets
+ for p in self._packetQueue:
+ self.xmlstream.send(p)
+ self._packetQueue = []
+
+ # Notify all child services which implement the IService interface
+ for c in self:
+ if ijabber.IService.providedBy(c):
+ c.componentConnected(xs)
+
+ def _disconnected(self, _):
+ self.xmlstream = None
+
+ # Notify all child services which implement
+ # the IService interface
+ for c in self:
+ if ijabber.IService.providedBy(c):
+ c.componentDisconnected()
+
+ def send(self, obj):
+ """
+ Send data over the XML stream.
+
+ When there is no established XML stream, the data is queued and sent
+ out when a new XML stream has been established and initialized.
+
+ @param obj: data to be sent over the XML stream. This is usually an
+ object providing L{domish.IElement}, or serialized XML. See
+ L{xmlstream.XmlStream} for details.
+ """
+
+ if self.xmlstream != None:
+ self.xmlstream.send(obj)
+ else:
+ self._packetQueue.append(obj)
+
+def buildServiceManager(jid, password, strport):
+ """
+ Constructs a pre-built L{ServiceManager}, using the specified strport
+ string.
+ """
+
+ svc = ServiceManager(jid, password)
+ client_svc = jstrports.client(strport, svc.getFactory())
+ client_svc.setServiceParent(svc)
+ return svc
+
+
+
+class Router(object):
+ """
+ XMPP Server's Router.
+
+ A router connects the different components of the XMPP service and routes
+ messages between them based on the given routing table.
+
+ Connected components are trusted to have correct addressing in the
+ stanzas they offer for routing.
+
+ A route destination of C{None} adds a default route. Traffic for which no
+ specific route exists, will be routed to this default route.
+
+ @since: 8.2
+ @ivar routes: Routes based on the host part of JIDs. Maps host names to the
+ L{EventDispatcher<utility.EventDispatcher>}s that should
+ receive the traffic. A key of C{None} means the default
+ route.
+ @type routes: C{dict}
+ """
+
+ def __init__(self):
+ self.routes = {}
+
+
+ def addRoute(self, destination, xs):
+ """
+ Add a new route.
+
+ The passed XML Stream C{xs} will have an observer for all stanzas
+ added to route its outgoing traffic. In turn, traffic for
+ C{destination} will be passed to this stream.
+
+ @param destination: Destination of the route to be added as a host name
+ or C{None} for the default route.
+ @type destination: C{str} or C{NoneType}.
+ @param xs: XML Stream to register the route for.
+ @type xs: L{EventDispatcher<utility.EventDispatcher>}.
+ """
+ self.routes[destination] = xs
+ xs.addObserver('/*', self.route)
+
+
+ def removeRoute(self, destination, xs):
+ """
+ Remove a route.
+
+ @param destination: Destination of the route that should be removed.
+ @type destination: C{str}.
+ @param xs: XML Stream to remove the route for.
+ @type xs: L{EventDispatcher<utility.EventDispatcher>}.
+ """
+ xs.removeObserver('/*', self.route)
+ if (xs == self.routes[destination]):
+ del self.routes[destination]
+
+
+ def route(self, stanza):
+ """
+ Route a stanza.
+
+ @param stanza: The stanza to be routed.
+ @type stanza: L{domish.Element}.
+ """
+ destination = JID(stanza['to'])
+
+ log.msg("Routing to %s: %r" % (destination.full(), stanza.toXml()))
+
+ if destination.host in self.routes:
+ self.routes[destination.host].send(stanza)
+ else:
+ self.routes[None].send(stanza)
+
+
+
+class XMPPComponentServerFactory(xmlstream.XmlStreamServerFactory):
+ """
+ XMPP Component Server factory.
+
+ This factory accepts XMPP external component connections and makes
+ the router service route traffic for a component's bound domain
+ to that component.
+
+ @since: 8.2
+ """
+
+ logTraffic = False
+
+ def __init__(self, router, secret='secret'):
+ self.router = router
+ self.secret = secret
+
+ def authenticatorFactory():
+ return ListenComponentAuthenticator(self.secret)
+
+ xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
+ self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
+ self.onConnectionMade)
+ self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
+ self.onAuthenticated)
+
+ self.serial = 0
+
+
+ def onConnectionMade(self, xs):
+ """
+ Called when a component connection was made.
+
+ This enables traffic debugging on incoming streams.
+ """
+ xs.serial = self.serial
+ self.serial += 1
+
+ def logDataIn(buf):
+ log.msg("RECV (%d): %r" % (xs.serial, buf))
+
+ def logDataOut(buf):
+ log.msg("SEND (%d): %r" % (xs.serial, buf))
+
+ if self.logTraffic:
+ xs.rawDataInFn = logDataIn
+ xs.rawDataOutFn = logDataOut
+
+ xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
+
+
+ def onAuthenticated(self, xs):
+ """
+ Called when a component has succesfully authenticated.
+
+ Add the component to the routing table and establish a handler
+ for a closed connection.
+ """
+ destination = xs.thisEntity.host
+
+ self.router.addRoute(destination, xs)
+ xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost, 0,
+ destination, xs)
+
+
+ def onError(self, reason):
+ log.err(reason, "Stream Error")
+
+
+ def onConnectionLost(self, destination, xs, reason):
+ self.router.removeRoute(destination, xs)
diff --git a/twisted/words/protocols/jabber/error.py b/twisted/words/protocols/jabber/error.py
new file mode 100644
index 0000000..aa5e9d2
--- /dev/null
+++ b/twisted/words/protocols/jabber/error.py
@@ -0,0 +1,336 @@
+# -*- test-case-name: twisted.words.test.test_jabbererror -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+XMPP Error support.
+"""
+
+import copy
+
+from twisted.words.xish import domish
+
+NS_XML = "http://www.w3.org/XML/1998/namespace"
+NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams"
+NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"
+
+STANZA_CONDITIONS = {
+ 'bad-request': {'code': '400', 'type': 'modify'},
+ 'conflict': {'code': '409', 'type': 'cancel'},
+ 'feature-not-implemented': {'code': '501', 'type': 'cancel'},
+ 'forbidden': {'code': '403', 'type': 'auth'},
+ 'gone': {'code': '302', 'type': 'modify'},
+ 'internal-server-error': {'code': '500', 'type': 'wait'},
+ 'item-not-found': {'code': '404', 'type': 'cancel'},
+ 'jid-malformed': {'code': '400', 'type': 'modify'},
+ 'not-acceptable': {'code': '406', 'type': 'modify'},
+ 'not-allowed': {'code': '405', 'type': 'cancel'},
+ 'not-authorized': {'code': '401', 'type': 'auth'},
+ 'payment-required': {'code': '402', 'type': 'auth'},
+ 'recipient-unavailable': {'code': '404', 'type': 'wait'},
+ 'redirect': {'code': '302', 'type': 'modify'},
+ 'registration-required': {'code': '407', 'type': 'auth'},
+ 'remote-server-not-found': {'code': '404', 'type': 'cancel'},
+ 'remote-server-timeout': {'code': '504', 'type': 'wait'},
+ 'resource-constraint': {'code': '500', 'type': 'wait'},
+ 'service-unavailable': {'code': '503', 'type': 'cancel'},
+ 'subscription-required': {'code': '407', 'type': 'auth'},
+ 'undefined-condition': {'code': '500', 'type': None},
+ 'unexpected-request': {'code': '400', 'type': 'wait'},
+}
+
+CODES_TO_CONDITIONS = {
+ '302': ('gone', 'modify'),
+ '400': ('bad-request', 'modify'),
+ '401': ('not-authorized', 'auth'),
+ '402': ('payment-required', 'auth'),
+ '403': ('forbidden', 'auth'),
+ '404': ('item-not-found', 'cancel'),
+ '405': ('not-allowed', 'cancel'),
+ '406': ('not-acceptable', 'modify'),
+ '407': ('registration-required', 'auth'),
+ '408': ('remote-server-timeout', 'wait'),
+ '409': ('conflict', 'cancel'),
+ '500': ('internal-server-error', 'wait'),
+ '501': ('feature-not-implemented', 'cancel'),
+ '502': ('service-unavailable', 'wait'),
+ '503': ('service-unavailable', 'cancel'),
+ '504': ('remote-server-timeout', 'wait'),
+ '510': ('service-unavailable', 'cancel'),
+}
+
+class BaseError(Exception):
+ """
+ Base class for XMPP error exceptions.
+
+ @cvar namespace: The namespace of the C{error} element generated by
+ C{getElement}.
+ @type namespace: C{str}
+ @ivar condition: The error condition. The valid values are defined by
+ subclasses of L{BaseError}.
+ @type contition: C{str}
+ @ivar text: Optional text message to supplement the condition or application
+ specific condition.
+ @type text: C{unicode}
+ @ivar textLang: Identifier of the language used for the message in C{text}.
+ Values are as described in RFC 3066.
+ @type textLang: C{str}
+ @ivar appCondition: Application specific condition element, supplementing
+ the error condition in C{condition}.
+ @type appCondition: object providing L{domish.IElement}.
+ """
+
+ namespace = None
+
+ def __init__(self, condition, text=None, textLang=None, appCondition=None):
+ Exception.__init__(self)
+ self.condition = condition
+ self.text = text
+ self.textLang = textLang
+ self.appCondition = appCondition
+
+
+ def __str__(self):
+ message = "%s with condition %r" % (self.__class__.__name__,
+ self.condition)
+
+ if self.text:
+ message += ': ' + self.text
+
+ return message
+
+
+ def getElement(self):
+ """
+ Get XML representation from self.
+
+ The method creates an L{domish} representation of the
+ error data contained in this exception.
+
+ @rtype: L{domish.Element}
+ """
+ error = domish.Element((None, 'error'))
+ error.addElement((self.namespace, self.condition))
+ if self.text:
+ text = error.addElement((self.namespace, 'text'),
+ content=self.text)
+ if self.textLang:
+ text[(NS_XML, 'lang')] = self.textLang
+ if self.appCondition:
+ error.addChild(self.appCondition)
+ return error
+
+
+
+class StreamError(BaseError):
+ """
+ Stream Error exception.
+
+ Refer to RFC 3920, section 4.7.3, for the allowed values for C{condition}.
+ """
+
+ namespace = NS_XMPP_STREAMS
+
+ def getElement(self):
+ """
+ Get XML representation from self.
+
+ Overrides the base L{BaseError.getElement} to make sure the returned
+ element is in the XML Stream namespace.
+
+ @rtype: L{domish.Element}
+ """
+ from twisted.words.protocols.jabber.xmlstream import NS_STREAMS
+
+ error = BaseError.getElement(self)
+ error.uri = NS_STREAMS
+ return error
+
+
+
+class StanzaError(BaseError):
+ """
+ Stanza Error exception.
+
+ Refer to RFC 3920, section 9.3, for the allowed values for C{condition} and
+ C{type}.
+
+ @ivar type: The stanza error type. Gives a suggestion to the recipient
+ of the error on how to proceed.
+ @type type: C{str}
+ @ivar code: A numeric identifier for the error condition for backwards
+ compatibility with pre-XMPP Jabber implementations.
+ """
+
+ namespace = NS_XMPP_STANZAS
+
+ def __init__(self, condition, type=None, text=None, textLang=None,
+ appCondition=None):
+ BaseError.__init__(self, condition, text, textLang, appCondition)
+
+ if type is None:
+ try:
+ type = STANZA_CONDITIONS[condition]['type']
+ except KeyError:
+ pass
+ self.type = type
+
+ try:
+ self.code = STANZA_CONDITIONS[condition]['code']
+ except KeyError:
+ self.code = None
+
+ self.children = []
+ self.iq = None
+
+
+ def getElement(self):
+ """
+ Get XML representation from self.
+
+ Overrides the base L{BaseError.getElement} to make sure the returned
+ element has a C{type} attribute and optionally a legacy C{code}
+ attribute.
+
+ @rtype: L{domish.Element}
+ """
+ error = BaseError.getElement(self)
+ error['type'] = self.type
+ if self.code:
+ error['code'] = self.code
+ return error
+
+
+ def toResponse(self, stanza):
+ """
+ Construct error response stanza.
+
+ The C{stanza} is transformed into an error response stanza by
+ swapping the C{to} and C{from} addresses and inserting an error
+ element.
+
+ @note: This creates a shallow copy of the list of child elements of the
+ stanza. The child elements themselves are not copied themselves,
+ and references to their parent element will still point to the
+ original stanza element.
+
+ The serialization of an element does not use the reference to
+ its parent, so the typical use case of immediately sending out
+ the constructed error response is not affected.
+
+ @param stanza: the stanza to respond to
+ @type stanza: L{domish.Element}
+ """
+ from twisted.words.protocols.jabber.xmlstream import toResponse
+ response = toResponse(stanza, stanzaType='error')
+ response.children = copy.copy(stanza.children)
+ response.addChild(self.getElement())
+ return response
+
+
+def _getText(element):
+ for child in element.children:
+ if isinstance(child, basestring):
+ return unicode(child)
+
+ return None
+
+
+
+def _parseError(error, errorNamespace):
+ """
+ Parses an error element.
+
+ @param error: The error element to be parsed
+ @type error: L{domish.Element}
+ @param errorNamespace: The namespace of the elements that hold the error
+ condition and text.
+ @type errorNamespace: C{str}
+ @return: Dictionary with extracted error information. If present, keys
+ C{condition}, C{text}, C{textLang} have a string value,
+ and C{appCondition} has an L{domish.Element} value.
+ @rtype: C{dict}
+ """
+ condition = None
+ text = None
+ textLang = None
+ appCondition = None
+
+ for element in error.elements():
+ if element.uri == errorNamespace:
+ if element.name == 'text':
+ text = _getText(element)
+ textLang = element.getAttribute((NS_XML, 'lang'))
+ else:
+ condition = element.name
+ else:
+ appCondition = element
+
+ return {
+ 'condition': condition,
+ 'text': text,
+ 'textLang': textLang,
+ 'appCondition': appCondition,
+ }
+
+
+
+def exceptionFromStreamError(element):
+ """
+ Build an exception object from a stream error.
+
+ @param element: the stream error
+ @type element: L{domish.Element}
+ @return: the generated exception object
+ @rtype: L{StreamError}
+ """
+ error = _parseError(element, NS_XMPP_STREAMS)
+
+ exception = StreamError(error['condition'],
+ error['text'],
+ error['textLang'],
+ error['appCondition'])
+
+ return exception
+
+
+
+def exceptionFromStanza(stanza):
+ """
+ Build an exception object from an error stanza.
+
+ @param stanza: the error stanza
+ @type stanza: L{domish.Element}
+ @return: the generated exception object
+ @rtype: L{StanzaError}
+ """
+ children = []
+ condition = text = textLang = appCondition = type = code = None
+
+ for element in stanza.elements():
+ if element.name == 'error' and element.uri == stanza.uri:
+ code = element.getAttribute('code')
+ type = element.getAttribute('type')
+ error = _parseError(element, NS_XMPP_STANZAS)
+ condition = error['condition']
+ text = error['text']
+ textLang = error['textLang']
+ appCondition = error['appCondition']
+
+ if not condition and code:
+ condition, type = CODES_TO_CONDITIONS[code]
+ text = _getText(stanza.error)
+ else:
+ children.append(element)
+
+ if condition is None:
+ # TODO: raise exception instead?
+ return StanzaError(None)
+
+ exception = StanzaError(condition, type, text, textLang, appCondition)
+
+ exception.children = children
+ exception.stanza = stanza
+
+ return exception
diff --git a/twisted/words/protocols/jabber/ijabber.py b/twisted/words/protocols/jabber/ijabber.py
new file mode 100644
index 0000000..9cc65ff
--- /dev/null
+++ b/twisted/words/protocols/jabber/ijabber.py
@@ -0,0 +1,199 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Public Jabber Interfaces.
+"""
+
+from zope.interface import Attribute, Interface
+
+class IInitializer(Interface):
+ """
+ Interface for XML stream initializers.
+
+ Initializers perform a step in getting the XML stream ready to be
+ used for the exchange of XML stanzas.
+ """
+
+
+
+class IInitiatingInitializer(IInitializer):
+ """
+ Interface for XML stream initializers for the initiating entity.
+ """
+
+ xmlstream = Attribute("""The associated XML stream""")
+
+ def initialize():
+ """
+ Initiate the initialization step.
+
+ May return a deferred when the initialization is done asynchronously.
+ """
+
+
+
+class IIQResponseTracker(Interface):
+ """
+ IQ response tracker interface.
+
+ The XMPP stanza C{iq} has a request-response nature that fits
+ naturally with deferreds. You send out a request and when the response
+ comes back a deferred is fired.
+
+ The L{IQ} class implements a C{send} method that returns a deferred. This
+ deferred is put in a dictionary that is kept in an L{XmlStream} object,
+ keyed by the request stanzas C{id} attribute.
+
+ An object providing this interface (usually an instance of L{XmlStream}),
+ keeps the said dictionary and sets observers on the iq stanzas of type
+ C{result} and C{error} and lets the callback fire the associated deferred.
+ """
+ iqDeferreds = Attribute("Dictionary of deferreds waiting for an iq "
+ "response")
+
+
+
+class IXMPPHandler(Interface):
+ """
+ Interface for XMPP protocol handlers.
+
+ Objects that provide this interface can be added to a stream manager to
+ handle of (part of) an XMPP extension protocol.
+ """
+
+ parent = Attribute("""XML stream manager for this handler""")
+ xmlstream = Attribute("""The managed XML stream""")
+
+ def setHandlerParent(parent):
+ """
+ Set the parent of the handler.
+
+ @type parent: L{IXMPPHandlerCollection}
+ """
+
+
+ def disownHandlerParent(parent):
+ """
+ Remove the parent of the handler.
+
+ @type parent: L{IXMPPHandlerCollection}
+ """
+
+
+ def makeConnection(xs):
+ """
+ A connection over the underlying transport of the XML stream has been
+ established.
+
+ At this point, no traffic has been exchanged over the XML stream
+ given in C{xs}.
+
+ This should setup L{xmlstream} and call L{connectionMade}.
+
+ @type xs: L{XmlStream<twisted.words.protocols.jabber.XmlStream>}
+ """
+
+
+ def connectionMade():
+ """
+ Called after a connection has been established.
+
+ This method can be used to change properties of the XML Stream, its
+ authenticator or the stream manager prior to stream initialization
+ (including authentication).
+ """
+
+
+ def connectionInitialized():
+ """
+ The XML stream has been initialized.
+
+ At this point, authentication was successful, and XML stanzas can be
+ exchanged over the XML stream L{xmlstream}. This method can be
+ used to setup observers for incoming stanzas.
+ """
+
+
+ def connectionLost(reason):
+ """
+ The XML stream has been closed.
+
+ Subsequent use of C{parent.send} will result in data being queued
+ until a new connection has been established.
+
+ @type reason: L{twisted.python.failure.Failure}
+ """
+
+
+
+class IXMPPHandlerCollection(Interface):
+ """
+ Collection of handlers.
+
+ Contain several handlers and manage their connection.
+ """
+
+ def __iter__():
+ """
+ Get an iterator over all child handlers.
+ """
+
+
+ def addHandler(handler):
+ """
+ Add a child handler.
+
+ @type handler: L{IXMPPHandler}
+ """
+
+
+ def removeHandler(handler):
+ """
+ Remove a child handler.
+
+ @type handler: L{IXMPPHandler}
+ """
+
+
+
+class IService(Interface):
+ """
+ External server-side component service interface.
+
+ Services that provide this interface can be added to L{ServiceManager} to
+ implement (part of) the functionality of the server-side component.
+ """
+
+ def componentConnected(xs):
+ """
+ Parent component has established a connection.
+
+ At this point, authentication was succesful, and XML stanzas
+ can be exchanged over the XML stream C{xs}. This method can be used
+ to setup observers for incoming stanzas.
+
+ @param xs: XML Stream that represents the established connection.
+ @type xs: L{xmlstream.XmlStream}
+ """
+
+
+ def componentDisconnected():
+ """
+ Parent component has lost the connection to the Jabber server.
+
+ Subsequent use of C{self.parent.send} will result in data being
+ queued until a new connection has been established.
+ """
+
+
+ def transportConnected(xs):
+ """
+ Parent component has established a connection over the underlying
+ transport.
+
+ At this point, no traffic has been exchanged over the XML stream. This
+ method can be used to change properties of the XML Stream (in C{xs}),
+ the service manager or it's authenticator prior to stream
+ initialization (including authentication).
+ """
diff --git a/twisted/words/protocols/jabber/jid.py b/twisted/words/protocols/jabber/jid.py
new file mode 100644
index 0000000..9911cee
--- /dev/null
+++ b/twisted/words/protocols/jabber/jid.py
@@ -0,0 +1,249 @@
+# -*- test-case-name: twisted.words.test.test_jabberjid -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Jabber Identifier support.
+
+This module provides an object to represent Jabber Identifiers (JIDs) and
+parse string representations into them with proper checking for illegal
+characters, case folding and canonicalisation through L{stringprep<twisted.words.protocols.jabber.xmpp_stringprep>}.
+"""
+
+from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep, resourceprep, nameprep
+
+class InvalidFormat(Exception):
+ """
+ The given string could not be parsed into a valid Jabber Identifier (JID).
+ """
+
+def parse(jidstring):
+ """
+ Parse given JID string into its respective parts and apply stringprep.
+
+ @param jidstring: string representation of a JID.
+ @type jidstring: C{unicode}
+ @return: tuple of (user, host, resource), each of type C{unicode} as
+ the parsed and stringprep'd parts of the given JID. If the
+ given string did not have a user or resource part, the respective
+ field in the tuple will hold C{None}.
+ @rtype: C{tuple}
+ """
+ user = None
+ host = None
+ resource = None
+
+ # Search for delimiters
+ user_sep = jidstring.find("@")
+ res_sep = jidstring.find("/")
+
+ if user_sep == -1:
+ if res_sep == -1:
+ # host
+ host = jidstring
+ else:
+ # host/resource
+ host = jidstring[0:res_sep]
+ resource = jidstring[res_sep + 1:] or None
+ else:
+ if res_sep == -1:
+ # user@host
+ user = jidstring[0:user_sep] or None
+ host = jidstring[user_sep + 1:]
+ else:
+ if user_sep < res_sep:
+ # user@host/resource
+ user = jidstring[0:user_sep] or None
+ host = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
+ resource = jidstring[res_sep + 1:] or None
+ else:
+ # host/resource (with an @ in resource)
+ host = jidstring[0:res_sep]
+ resource = jidstring[res_sep + 1:] or None
+
+ return prep(user, host, resource)
+
+def prep(user, host, resource):
+ """
+ Perform stringprep on all JID fragments.
+
+ @param user: The user part of the JID.
+ @type user: C{unicode}
+ @param host: The host part of the JID.
+ @type host: C{unicode}
+ @param resource: The resource part of the JID.
+ @type resource: C{unicode}
+ @return: The given parts with stringprep applied.
+ @rtype: C{tuple}
+ """
+
+ if user:
+ try:
+ user = nodeprep.prepare(unicode(user))
+ except UnicodeError:
+ raise InvalidFormat, "Invalid character in username"
+ else:
+ user = None
+
+ if not host:
+ raise InvalidFormat, "Server address required."
+ else:
+ try:
+ host = nameprep.prepare(unicode(host))
+ except UnicodeError:
+ raise InvalidFormat, "Invalid character in hostname"
+
+ if resource:
+ try:
+ resource = resourceprep.prepare(unicode(resource))
+ except UnicodeError:
+ raise InvalidFormat, "Invalid character in resource"
+ else:
+ resource = None
+
+ return (user, host, resource)
+
+__internJIDs = {}
+
+def internJID(jidstring):
+ """
+ Return interned JID.
+
+ @rtype: L{JID}
+ """
+
+ if jidstring in __internJIDs:
+ return __internJIDs[jidstring]
+ else:
+ j = JID(jidstring)
+ __internJIDs[jidstring] = j
+ return j
+
+class JID(object):
+ """
+ Represents a stringprep'd Jabber ID.
+
+ JID objects are hashable so they can be used in sets and as keys in
+ dictionaries.
+ """
+
+ def __init__(self, str=None, tuple=None):
+ if not (str or tuple):
+ raise RuntimeError("You must provide a value for either 'str' or "
+ "'tuple' arguments.")
+
+ if str:
+ user, host, res = parse(str)
+ else:
+ user, host, res = prep(*tuple)
+
+ self.user = user
+ self.host = host
+ self.resource = res
+
+ def userhost(self):
+ """
+ Extract the bare JID as a unicode string.
+
+ A bare JID does not have a resource part, so this returns either
+ C{user@host} or just C{host}.
+
+ @rtype: C{unicode}
+ """
+ if self.user:
+ return u"%s@%s" % (self.user, self.host)
+ else:
+ return self.host
+
+ def userhostJID(self):
+ """
+ Extract the bare JID.
+
+ A bare JID does not have a resource part, so this returns a
+ L{JID} object representing either C{user@host} or just C{host}.
+
+ If the object this method is called upon doesn't have a resource
+ set, it will return itself. Otherwise, the bare JID object will
+ be created, interned using L{internJID}.
+
+ @rtype: L{JID}
+ """
+ if self.resource:
+ return internJID(self.userhost())
+ else:
+ return self
+
+ def full(self):
+ """
+ Return the string representation of this JID.
+
+ @rtype: C{unicode}
+ """
+ if self.user:
+ if self.resource:
+ return u"%s@%s/%s" % (self.user, self.host, self.resource)
+ else:
+ return u"%s@%s" % (self.user, self.host)
+ else:
+ if self.resource:
+ return u"%s/%s" % (self.host, self.resource)
+ else:
+ return self.host
+
+ def __eq__(self, other):
+ """
+ Equality comparison.
+
+ L{JID}s compare equal if their user, host and resource parts all
+ compare equal. When comparing against instances of other types, it
+ uses the default comparison.
+ """
+ if isinstance(other, JID):
+ return (self.user == other.user and
+ self.host == other.host and
+ self.resource == other.resource)
+ else:
+ return NotImplemented
+
+ def __ne__(self, other):
+ """
+ Inequality comparison.
+
+ This negates L{__eq__} for comparison with JIDs and uses the default
+ comparison for other types.
+ """
+ result = self.__eq__(other)
+ if result is NotImplemented:
+ return result
+ else:
+ return not result
+
+ def __hash__(self):
+ """
+ Calculate hash.
+
+ L{JID}s with identical constituent user, host and resource parts have
+ equal hash values. In combination with the comparison defined on JIDs,
+ this allows for using L{JID}s in sets and as dictionary keys.
+ """
+ return hash((self.user, self.host, self.resource))
+
+ def __unicode__(self):
+ """
+ Get unicode representation.
+
+ Return the string representation of this JID as a unicode string.
+ @see: L{full}
+ """
+
+ return self.full()
+
+ def __repr__(self):
+ """
+ Get object representation.
+
+ Returns a string that would create a new JID object that compares equal
+ to this one.
+ """
+ return 'JID(%r)' % self.full()
diff --git a/twisted/words/protocols/jabber/jstrports.py b/twisted/words/protocols/jabber/jstrports.py
new file mode 100644
index 0000000..773b6d2
--- /dev/null
+++ b/twisted/words/protocols/jabber/jstrports.py
@@ -0,0 +1,31 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+""" A temporary placeholder for client-capable strports, until we
+sufficient use cases get identified """
+
+from twisted.internet.endpoints import _parse
+
+def _parseTCPSSL(factory, domain, port):
+ """ For the moment, parse TCP or SSL connections the same """
+ return (domain, int(port), factory), {}
+
+def _parseUNIX(factory, address):
+ return (address, factory), {}
+
+
+_funcs = { "tcp" : _parseTCPSSL,
+ "unix" : _parseUNIX,
+ "ssl" : _parseTCPSSL }
+
+
+def parse(description, factory):
+ args, kw = _parse(description)
+ return (args[0].upper(),) + _funcs[args[0]](factory, *args[1:], **kw)
+
+def client(description, factory):
+ from twisted.application import internet
+ name, args, kw = parse(description, factory)
+ return getattr(internet, name + 'Client')(*args, **kw)
diff --git a/twisted/words/protocols/jabber/sasl.py b/twisted/words/protocols/jabber/sasl.py
new file mode 100644
index 0000000..c804ad4
--- /dev/null
+++ b/twisted/words/protocols/jabber/sasl.py
@@ -0,0 +1,243 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+XMPP-specific SASL profile.
+"""
+
+import re
+from twisted.internet import defer
+from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream
+from twisted.words.xish import domish
+
+# The b64decode and b64encode functions from the base64 module are new in
+# Python 2.4. For Python 2.3 compatibility, the legacy interface is used while
+# working around MIMEisms.
+
+try:
+ from base64 import b64decode, b64encode
+except ImportError:
+ import base64
+
+ def b64encode(s):
+ return "".join(base64.encodestring(s).split("\n"))
+
+ b64decode = base64.decodestring
+
+NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
+
+def get_mechanisms(xs):
+ """
+ Parse the SASL feature to extract the available mechanism names.
+ """
+ mechanisms = []
+ for element in xs.features[(NS_XMPP_SASL, 'mechanisms')].elements():
+ if element.name == 'mechanism':
+ mechanisms.append(str(element))
+
+ return mechanisms
+
+
+class SASLError(Exception):
+ """
+ SASL base exception.
+ """
+
+
+class SASLNoAcceptableMechanism(SASLError):
+ """
+ The server did not present an acceptable SASL mechanism.
+ """
+
+
+class SASLAuthError(SASLError):
+ """
+ SASL Authentication failed.
+ """
+ def __init__(self, condition=None):
+ self.condition = condition
+
+
+ def __str__(self):
+ return "SASLAuthError with condition %r" % self.condition
+
+
+class SASLIncorrectEncodingError(SASLError):
+ """
+ SASL base64 encoding was incorrect.
+
+ RFC 3920 specifies that any characters not in the base64 alphabet
+ and padding characters present elsewhere than at the end of the string
+ MUST be rejected. See also L{fromBase64}.
+
+ This exception is raised whenever the encoded string does not adhere
+ to these additional restrictions or when the decoding itself fails.
+
+ The recommended behaviour for so-called receiving entities (like servers in
+ client-to-server connections, see RFC 3920 for terminology) is to fail the
+ SASL negotiation with a C{'incorrect-encoding'} condition. For initiating
+ entities, one should assume the receiving entity to be either buggy or
+ malevolent. The stream should be terminated and reconnecting is not
+ advised.
+ """
+
+base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$")
+
+def fromBase64(s):
+ """
+ Decode base64 encoded string.
+
+ This helper performs regular decoding of a base64 encoded string, but also
+ rejects any characters that are not in the base64 alphabet and padding
+ occurring elsewhere from the last or last two characters, as specified in
+ section 14.9 of RFC 3920. This safeguards against various attack vectors
+ among which the creation of a covert channel that "leaks" information.
+ """
+
+ if base64Pattern.match(s) is None:
+ raise SASLIncorrectEncodingError()
+
+ try:
+ return b64decode(s)
+ except Exception, e:
+ raise SASLIncorrectEncodingError(str(e))
+
+
+
+class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer):
+ """
+ Stream initializer that performs SASL authentication.
+
+ The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN}
+ and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set
+ on the authenticator, does not have a localpart (username), requesting an
+ anonymous session where the username is generated by the server.
+ Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order.
+ """
+
+ feature = (NS_XMPP_SASL, 'mechanisms')
+ _deferred = None
+
+ def setMechanism(self):
+ """
+ Select and setup authentication mechanism.
+
+ Uses the authenticator's C{jid} and C{password} attribute for the
+ authentication credentials. If no supported SASL mechanisms are
+ advertized by the receiving party, a failing deferred is returned with
+ a L{SASLNoAcceptableMechanism} exception.
+ """
+
+ jid = self.xmlstream.authenticator.jid
+ password = self.xmlstream.authenticator.password
+
+ mechanisms = get_mechanisms(self.xmlstream)
+ if jid.user is not None:
+ if 'DIGEST-MD5' in mechanisms:
+ self.mechanism = sasl_mechanisms.DigestMD5('xmpp', jid.host, None,
+ jid.user, password)
+ elif 'PLAIN' in mechanisms:
+ self.mechanism = sasl_mechanisms.Plain(None, jid.user, password)
+ else:
+ raise SASLNoAcceptableMechanism()
+ else:
+ if 'ANONYMOUS' in mechanisms:
+ self.mechanism = sasl_mechanisms.Anonymous()
+ else:
+ raise SASLNoAcceptableMechanism()
+
+
+ def start(self):
+ """
+ Start SASL authentication exchange.
+ """
+
+ self.setMechanism()
+ self._deferred = defer.Deferred()
+ self.xmlstream.addObserver('/challenge', self.onChallenge)
+ self.xmlstream.addOnetimeObserver('/success', self.onSuccess)
+ self.xmlstream.addOnetimeObserver('/failure', self.onFailure)
+ self.sendAuth(self.mechanism.getInitialResponse())
+ return self._deferred
+
+
+ def sendAuth(self, data=None):
+ """
+ Initiate authentication protocol exchange.
+
+ If an initial client response is given in C{data}, it will be
+ sent along.
+
+ @param data: initial client response.
+ @type data: C{str} or C{None}.
+ """
+
+ auth = domish.Element((NS_XMPP_SASL, 'auth'))
+ auth['mechanism'] = self.mechanism.name
+ if data is not None:
+ auth.addContent(b64encode(data) or '=')
+ self.xmlstream.send(auth)
+
+
+ def sendResponse(self, data=''):
+ """
+ Send response to a challenge.
+
+ @param data: client response.
+ @type data: C{str}.
+ """
+
+ response = domish.Element((NS_XMPP_SASL, 'response'))
+ if data:
+ response.addContent(b64encode(data))
+ self.xmlstream.send(response)
+
+
+ def onChallenge(self, element):
+ """
+ Parse challenge and send response from the mechanism.
+
+ @param element: the challenge protocol element.
+ @type element: L{domish.Element}.
+ """
+
+ try:
+ challenge = fromBase64(str(element))
+ except SASLIncorrectEncodingError:
+ self._deferred.errback()
+ else:
+ self.sendResponse(self.mechanism.getResponse(challenge))
+
+
+ def onSuccess(self, success):
+ """
+ Clean up observers, reset the XML stream and send a new header.
+
+ @param success: the success protocol element. For now unused, but
+ could hold additional data.
+ @type success: L{domish.Element}
+ """
+
+ self.xmlstream.removeObserver('/challenge', self.onChallenge)
+ self.xmlstream.removeObserver('/failure', self.onFailure)
+ self.xmlstream.reset()
+ self.xmlstream.sendHeader()
+ self._deferred.callback(xmlstream.Reset)
+
+
+ def onFailure(self, failure):
+ """
+ Clean up observers, parse the failure and errback the deferred.
+
+ @param failure: the failure protocol element. Holds details on
+ the error condition.
+ @type failure: L{domish.Element}
+ """
+
+ self.xmlstream.removeObserver('/challenge', self.onChallenge)
+ self.xmlstream.removeObserver('/success', self.onSuccess)
+ try:
+ condition = failure.firstChildElement().name
+ except AttributeError:
+ condition = None
+ self._deferred.errback(SASLAuthError(condition))
diff --git a/twisted/words/protocols/jabber/sasl_mechanisms.py b/twisted/words/protocols/jabber/sasl_mechanisms.py
new file mode 100644
index 0000000..5d51be2
--- /dev/null
+++ b/twisted/words/protocols/jabber/sasl_mechanisms.py
@@ -0,0 +1,240 @@
+# -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Protocol agnostic implementations of SASL authentication mechanisms.
+"""
+
+import binascii, random, time, os
+
+from zope.interface import Interface, Attribute, implements
+
+from twisted.python.hashlib import md5
+
+class ISASLMechanism(Interface):
+ name = Attribute("""Common name for the SASL Mechanism.""")
+
+ def getInitialResponse():
+ """
+ Get the initial client response, if defined for this mechanism.
+
+ @return: initial client response string.
+ @rtype: C{str}.
+ """
+
+
+ def getResponse(challenge):
+ """
+ Get the response to a server challenge.
+
+ @param challenge: server challenge.
+ @type challenge: C{str}.
+ @return: client response.
+ @rtype: C{str}.
+ """
+
+
+
+class Anonymous(object):
+ """
+ Implements the ANONYMOUS SASL authentication mechanism.
+
+ This mechanism is defined in RFC 2245.
+ """
+ implements(ISASLMechanism)
+ name = 'ANONYMOUS'
+
+ def getInitialResponse(self):
+ return None
+
+
+
+class Plain(object):
+ """
+ Implements the PLAIN SASL authentication mechanism.
+
+ The PLAIN SASL authentication mechanism is defined in RFC 2595.
+ """
+ implements(ISASLMechanism)
+
+ name = 'PLAIN'
+
+ def __init__(self, authzid, authcid, password):
+ self.authzid = authzid or ''
+ self.authcid = authcid or ''
+ self.password = password or ''
+
+
+ def getInitialResponse(self):
+ return "%s\x00%s\x00%s" % (self.authzid.encode('utf-8'),
+ self.authcid.encode('utf-8'),
+ self.password.encode('utf-8'))
+
+
+
+class DigestMD5(object):
+ """
+ Implements the DIGEST-MD5 SASL authentication mechanism.
+
+ The DIGEST-MD5 SASL authentication mechanism is defined in RFC 2831.
+ """
+ implements(ISASLMechanism)
+
+ name = 'DIGEST-MD5'
+
+ def __init__(self, serv_type, host, serv_name, username, password):
+ self.username = username
+ self.password = password
+ self.defaultRealm = host
+
+ self.digest_uri = '%s/%s' % (serv_type, host)
+ if serv_name is not None:
+ self.digest_uri += '/%s' % serv_name
+
+
+ def getInitialResponse(self):
+ return None
+
+
+ def getResponse(self, challenge):
+ directives = self._parse(challenge)
+
+ # Compat for implementations that do not send this along with
+ # a succesful authentication.
+ if 'rspauth' in directives:
+ return ''
+
+ try:
+ realm = directives['realm']
+ except KeyError:
+ realm = self.defaultRealm
+
+ return self._gen_response(directives['charset'],
+ realm,
+ directives['nonce'])
+
+ def _parse(self, challenge):
+ """
+ Parses the server challenge.
+
+ Splits the challenge into a dictionary of directives with values.
+
+ @return: challenge directives and their values.
+ @rtype: C{dict} of C{str} to C{str}.
+ """
+ s = challenge
+ paramDict = {}
+ cur = 0
+ remainingParams = True
+ while remainingParams:
+ # Parse a param. We can't just split on commas, because there can
+ # be some commas inside (quoted) param values, e.g.:
+ # qop="auth,auth-int"
+
+ middle = s.index("=", cur)
+ name = s[cur:middle].lstrip()
+ middle += 1
+ if s[middle] == '"':
+ middle += 1
+ end = s.index('"', middle)
+ value = s[middle:end]
+ cur = s.find(',', end) + 1
+ if cur == 0:
+ remainingParams = False
+ else:
+ end = s.find(',', middle)
+ if end == -1:
+ value = s[middle:].rstrip()
+ remainingParams = False
+ else:
+ value = s[middle:end].rstrip()
+ cur = end + 1
+ paramDict[name] = value
+
+ for param in ('qop', 'cipher'):
+ if param in paramDict:
+ paramDict[param] = paramDict[param].split(',')
+
+ return paramDict
+
+ def _unparse(self, directives):
+ """
+ Create message string from directives.
+
+ @param directives: dictionary of directives (names to their values).
+ For certain directives, extra quotes are added, as
+ needed.
+ @type directives: C{dict} of C{str} to C{str}
+ @return: message string.
+ @rtype: C{str}.
+ """
+
+ directive_list = []
+ for name, value in directives.iteritems():
+ if name in ('username', 'realm', 'cnonce',
+ 'nonce', 'digest-uri', 'authzid', 'cipher'):
+ directive = '%s="%s"' % (name, value)
+ else:
+ directive = '%s=%s' % (name, value)
+
+ directive_list.append(directive)
+
+ return ','.join(directive_list)
+
+
+ def _gen_response(self, charset, realm, nonce):
+ """
+ Generate response-value.
+
+ Creates a response to a challenge according to section 2.1.2.1 of
+ RFC 2831 using the C{charset}, C{realm} and C{nonce} directives
+ from the challenge.
+ """
+
+ def H(s):
+ return md5(s).digest()
+
+ def HEX(n):
+ return binascii.b2a_hex(n)
+
+ def KD(k, s):
+ return H('%s:%s' % (k, s))
+
+ try:
+ username = self.username.encode(charset)
+ password = self.password.encode(charset)
+ except UnicodeError:
+ # TODO - add error checking
+ raise
+
+ nc = '%08x' % 1 # TODO: support subsequent auth.
+ cnonce = self._gen_nonce()
+ qop = 'auth'
+
+ # TODO - add support for authzid
+ a1 = "%s:%s:%s" % (H("%s:%s:%s" % (username, realm, password)),
+ nonce,
+ cnonce)
+ a2 = "AUTHENTICATE:%s" % self.digest_uri
+
+ response = HEX( KD ( HEX(H(a1)),
+ "%s:%s:%s:%s:%s" % (nonce, nc,
+ cnonce, "auth", HEX(H(a2)))))
+
+ directives = {'username': username,
+ 'realm' : realm,
+ 'nonce' : nonce,
+ 'cnonce' : cnonce,
+ 'nc' : nc,
+ 'qop' : qop,
+ 'digest-uri': self.digest_uri,
+ 'response': response,
+ 'charset': charset}
+
+ return self._unparse(directives)
+
+
+ def _gen_nonce(self):
+ return md5("%s:%s:%s" % (str(random.random()) , str(time.gmtime()),str(os.getpid()))).hexdigest()
diff --git a/twisted/words/protocols/jabber/xmlstream.py b/twisted/words/protocols/jabber/xmlstream.py
new file mode 100644
index 0000000..cc2745b
--- /dev/null
+++ b/twisted/words/protocols/jabber/xmlstream.py
@@ -0,0 +1,1136 @@
+# -*- test-case-name: twisted.words.test.test_jabberxmlstream -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+XMPP XML Streams
+
+Building blocks for setting up XML Streams, including helping classes for
+doing authentication on either client or server side, and working with XML
+Stanzas.
+"""
+
+from zope.interface import directlyProvides, implements
+
+from twisted.internet import defer, protocol
+from twisted.internet.error import ConnectionLost
+from twisted.python import failure, log, randbytes
+from twisted.python.hashlib import sha1
+from twisted.words.protocols.jabber import error, ijabber, jid
+from twisted.words.xish import domish, xmlstream
+from twisted.words.xish.xmlstream import STREAM_CONNECTED_EVENT
+from twisted.words.xish.xmlstream import STREAM_START_EVENT
+from twisted.words.xish.xmlstream import STREAM_END_EVENT
+from twisted.words.xish.xmlstream import STREAM_ERROR_EVENT
+
+try:
+ from twisted.internet import ssl
+except ImportError:
+ ssl = None
+if ssl and not ssl.supported:
+ ssl = None
+
+STREAM_AUTHD_EVENT = intern("//event/stream/authd")
+INIT_FAILED_EVENT = intern("//event/xmpp/initfailed")
+
+NS_STREAMS = 'http://etherx.jabber.org/streams'
+NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
+
+Reset = object()
+
+def hashPassword(sid, password):
+ """
+ Create a SHA1-digest string of a session identifier and password.
+
+ @param sid: The stream session identifier.
+ @type sid: C{unicode}.
+ @param password: The password to be hashed.
+ @type password: C{unicode}.
+ """
+ if not isinstance(sid, unicode):
+ raise TypeError("The session identifier must be a unicode object")
+ if not isinstance(password, unicode):
+ raise TypeError("The password must be a unicode object")
+ input = u"%s%s" % (sid, password)
+ return sha1(input.encode('utf-8')).hexdigest()
+
+
+
+class Authenticator:
+ """
+ Base class for business logic of initializing an XmlStream
+
+ Subclass this object to enable an XmlStream to initialize and authenticate
+ to different types of stream hosts (such as clients, components, etc.).
+
+ Rules:
+ 1. The Authenticator MUST dispatch a L{STREAM_AUTHD_EVENT} when the
+ stream has been completely initialized.
+ 2. The Authenticator SHOULD reset all state information when
+ L{associateWithStream} is called.
+ 3. The Authenticator SHOULD override L{streamStarted}, and start
+ initialization there.
+
+ @type xmlstream: L{XmlStream}
+ @ivar xmlstream: The XmlStream that needs authentication
+
+ @note: the term authenticator is historical. Authenticators perform
+ all steps required to prepare the stream for the exchange
+ of XML stanzas.
+ """
+
+ def __init__(self):
+ self.xmlstream = None
+
+
+ def connectionMade(self):
+ """
+ Called by the XmlStream when the underlying socket connection is
+ in place.
+
+ This allows the Authenticator to send an initial root element, if it's
+ connecting, or wait for an inbound root from the peer if it's accepting
+ the connection.
+
+ Subclasses can use self.xmlstream.send() to send any initial data to
+ the peer.
+ """
+
+
+ def streamStarted(self, rootElement):
+ """
+ Called by the XmlStream when the stream has started.
+
+ A stream is considered to have started when the start tag of the root
+ element has been received.
+
+ This examines C{rootElement} to see if there is a version attribute.
+ If absent, C{0.0} is assumed per RFC 3920. Subsequently, the
+ minimum of the version from the received stream header and the
+ value stored in L{xmlstream} is taken and put back in L{xmlstream}.
+
+ Extensions of this method can extract more information from the
+ stream header and perform checks on them, optionally sending
+ stream errors and closing the stream.
+ """
+ if rootElement.hasAttribute("version"):
+ version = rootElement["version"].split(".")
+ try:
+ version = (int(version[0]), int(version[1]))
+ except (IndexError, ValueError):
+ version = (0, 0)
+ else:
+ version = (0, 0)
+
+ self.xmlstream.version = min(self.xmlstream.version, version)
+
+
+ def associateWithStream(self, xmlstream):
+ """
+ Called by the XmlStreamFactory when a connection has been made
+ to the requested peer, and an XmlStream object has been
+ instantiated.
+
+ The default implementation just saves a handle to the new
+ XmlStream.
+
+ @type xmlstream: L{XmlStream}
+ @param xmlstream: The XmlStream that will be passing events to this
+ Authenticator.
+
+ """
+ self.xmlstream = xmlstream
+
+
+
+class ConnectAuthenticator(Authenticator):
+ """
+ Authenticator for initiating entities.
+ """
+
+ namespace = None
+
+ def __init__(self, otherHost):
+ self.otherHost = otherHost
+
+
+ def connectionMade(self):
+ self.xmlstream.namespace = self.namespace
+ self.xmlstream.otherEntity = jid.internJID(self.otherHost)
+ self.xmlstream.sendHeader()
+
+
+ def initializeStream(self):
+ """
+ Perform stream initialization procedures.
+
+ An L{XmlStream} holds a list of initializer objects in its
+ C{initializers} attribute. This method calls these initializers in
+ order and dispatches the C{STREAM_AUTHD_EVENT} event when the list has
+ been successfully processed. Otherwise it dispatches the
+ C{INIT_FAILED_EVENT} event with the failure.
+
+ Initializers may return the special L{Reset} object to halt the
+ initialization processing. It signals that the current initializer was
+ successfully processed, but that the XML Stream has been reset. An
+ example is the TLSInitiatingInitializer.
+ """
+
+ def remove_first(result):
+ self.xmlstream.initializers.pop(0)
+
+ return result
+
+ def do_next(result):
+ """
+ Take the first initializer and process it.
+
+ On success, the initializer is removed from the list and
+ then next initializer will be tried.
+ """
+
+ if result is Reset:
+ return None
+
+ try:
+ init = self.xmlstream.initializers[0]
+ except IndexError:
+ self.xmlstream.dispatch(self.xmlstream, STREAM_AUTHD_EVENT)
+ return None
+ else:
+ d = defer.maybeDeferred(init.initialize)
+ d.addCallback(remove_first)
+ d.addCallback(do_next)
+ return d
+
+ d = defer.succeed(None)
+ d.addCallback(do_next)
+ d.addErrback(self.xmlstream.dispatch, INIT_FAILED_EVENT)
+
+
+ def streamStarted(self, rootElement):
+ """
+ Called by the XmlStream when the stream has started.
+
+ This extends L{Authenticator.streamStarted} to extract further stream
+ headers from C{rootElement}, optionally wait for stream features being
+ received and then call C{initializeStream}.
+ """
+
+ Authenticator.streamStarted(self, rootElement)
+
+ self.xmlstream.sid = rootElement.getAttribute("id")
+
+ if rootElement.hasAttribute("from"):
+ self.xmlstream.otherEntity = jid.internJID(rootElement["from"])
+
+ # Setup observer for stream features, if applicable
+ if self.xmlstream.version >= (1, 0):
+ def onFeatures(element):
+ features = {}
+ for feature in element.elements():
+ features[(feature.uri, feature.name)] = feature
+
+ self.xmlstream.features = features
+ self.initializeStream()
+
+ self.xmlstream.addOnetimeObserver('/features[@xmlns="%s"]' %
+ NS_STREAMS,
+ onFeatures)
+ else:
+ self.initializeStream()
+
+
+
+class ListenAuthenticator(Authenticator):
+ """
+ Authenticator for receiving entities.
+ """
+
+ namespace = None
+
+ def associateWithStream(self, xmlstream):
+ """
+ Called by the XmlStreamFactory when a connection has been made.
+
+ Extend L{Authenticator.associateWithStream} to set the L{XmlStream}
+ to be non-initiating.
+ """
+ Authenticator.associateWithStream(self, xmlstream)
+ self.xmlstream.initiating = False
+
+
+ def streamStarted(self, rootElement):
+ """
+ Called by the XmlStream when the stream has started.
+
+ This extends L{Authenticator.streamStarted} to extract further
+ information from the stream headers from C{rootElement}.
+ """
+ Authenticator.streamStarted(self, rootElement)
+
+ self.xmlstream.namespace = rootElement.defaultUri
+
+ if rootElement.hasAttribute("to"):
+ self.xmlstream.thisEntity = jid.internJID(rootElement["to"])
+
+ self.xmlstream.prefixes = {}
+ for prefix, uri in rootElement.localPrefixes.iteritems():
+ self.xmlstream.prefixes[uri] = prefix
+
+ self.xmlstream.sid = unicode(randbytes.secureRandom(8).encode('hex'))
+
+
+
+class FeatureNotAdvertized(Exception):
+ """
+ Exception indicating a stream feature was not advertized, while required by
+ the initiating entity.
+ """
+
+
+
+class BaseFeatureInitiatingInitializer(object):
+ """
+ Base class for initializers with a stream feature.
+
+ This assumes the associated XmlStream represents the initiating entity
+ of the connection.
+
+ @cvar feature: tuple of (uri, name) of the stream feature root element.
+ @type feature: tuple of (C{str}, C{str})
+ @ivar required: whether the stream feature is required to be advertized
+ by the receiving entity.
+ @type required: C{bool}
+ """
+
+ implements(ijabber.IInitiatingInitializer)
+
+ feature = None
+ required = False
+
+ def __init__(self, xs):
+ self.xmlstream = xs
+
+
+ def initialize(self):
+ """
+ Initiate the initialization.
+
+ Checks if the receiving entity advertizes the stream feature. If it
+ does, the initialization is started. If it is not advertized, and the
+ C{required} instance variable is C{True}, it raises
+ L{FeatureNotAdvertized}. Otherwise, the initialization silently
+ succeeds.
+ """
+
+ if self.feature in self.xmlstream.features:
+ return self.start()
+ elif self.required:
+ raise FeatureNotAdvertized
+ else:
+ return None
+
+
+ def start(self):
+ """
+ Start the actual initialization.
+
+ May return a deferred for asynchronous initialization.
+ """
+
+
+
+class TLSError(Exception):
+ """
+ TLS base exception.
+ """
+
+
+
+class TLSFailed(TLSError):
+ """
+ Exception indicating failed TLS negotiation
+ """
+
+
+
+class TLSRequired(TLSError):
+ """
+ Exception indicating required TLS negotiation.
+
+ This exception is raised when the receiving entity requires TLS
+ negotiation and the initiating does not desire to negotiate TLS.
+ """
+
+
+
+class TLSNotSupported(TLSError):
+ """
+ Exception indicating missing TLS support.
+
+ This exception is raised when the initiating entity wants and requires to
+ negotiate TLS when the OpenSSL library is not available.
+ """
+
+
+
+class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer):
+ """
+ TLS stream initializer for the initiating entity.
+
+ It is strongly required to include this initializer in the list of
+ initializers for an XMPP stream. By default it will try to negotiate TLS.
+ An XMPP server may indicate that TLS is required. If TLS is not desired,
+ set the C{wanted} attribute to False instead of removing it from the list
+ of initializers, so a proper exception L{TLSRequired} can be raised.
+
+ @cvar wanted: indicates if TLS negotiation is wanted.
+ @type wanted: C{bool}
+ """
+
+ feature = (NS_XMPP_TLS, 'starttls')
+ wanted = True
+ _deferred = None
+
+ def onProceed(self, obj):
+ """
+ Proceed with TLS negotiation and reset the XML stream.
+ """
+
+ self.xmlstream.removeObserver('/failure', self.onFailure)
+ ctx = ssl.CertificateOptions()
+ self.xmlstream.transport.startTLS(ctx)
+ self.xmlstream.reset()
+ self.xmlstream.sendHeader()
+ self._deferred.callback(Reset)
+
+
+ def onFailure(self, obj):
+ self.xmlstream.removeObserver('/proceed', self.onProceed)
+ self._deferred.errback(TLSFailed())
+
+
+ def start(self):
+ """
+ Start TLS negotiation.
+
+ This checks if the receiving entity requires TLS, the SSL library is
+ available and uses the C{required} and C{wanted} instance variables to
+ determine what to do in the various different cases.
+
+ For example, if the SSL library is not available, and wanted and
+ required by the user, it raises an exception. However if it is not
+ required by both parties, initialization silently succeeds, moving
+ on to the next step.
+ """
+ if self.wanted:
+ if ssl is None:
+ if self.required:
+ return defer.fail(TLSNotSupported())
+ else:
+ return defer.succeed(None)
+ else:
+ pass
+ elif self.xmlstream.features[self.feature].required:
+ return defer.fail(TLSRequired())
+ else:
+ return defer.succeed(None)
+
+ self._deferred = defer.Deferred()
+ self.xmlstream.addOnetimeObserver("/proceed", self.onProceed)
+ self.xmlstream.addOnetimeObserver("/failure", self.onFailure)
+ self.xmlstream.send(domish.Element((NS_XMPP_TLS, "starttls")))
+ return self._deferred
+
+
+
+class XmlStream(xmlstream.XmlStream):
+ """
+ XMPP XML Stream protocol handler.
+
+ @ivar version: XML stream version as a tuple (major, minor). Initially,
+ this is set to the minimally supported version. Upon
+ receiving the stream header of the peer, it is set to the
+ minimum of that value and the version on the received
+ header.
+ @type version: (C{int}, C{int})
+ @ivar namespace: default namespace URI for stream
+ @type namespace: C{unicode}
+ @ivar thisEntity: JID of this entity
+ @type thisEntity: L{JID}
+ @ivar otherEntity: JID of the peer entity
+ @type otherEntity: L{JID}
+ @ivar sid: session identifier
+ @type sid: C{unicode}
+ @ivar initiating: True if this is the initiating stream
+ @type initiating: C{bool}
+ @ivar features: map of (uri, name) to stream features element received from
+ the receiving entity.
+ @type features: C{dict} of (C{unicode}, C{unicode}) to L{domish.Element}.
+ @ivar prefixes: map of URI to prefixes that are to appear on stream
+ header.
+ @type prefixes: C{dict} of C{unicode} to C{unicode}
+ @ivar initializers: list of stream initializer objects
+ @type initializers: C{list} of objects that provide L{IInitializer}
+ @ivar authenticator: associated authenticator that uses C{initializers} to
+ initialize the XML stream.
+ """
+
+ version = (1, 0)
+ namespace = 'invalid'
+ thisEntity = None
+ otherEntity = None
+ sid = None
+ initiating = True
+
+ _headerSent = False # True if the stream header has been sent
+
+ def __init__(self, authenticator):
+ xmlstream.XmlStream.__init__(self)
+
+ self.prefixes = {NS_STREAMS: 'stream'}
+ self.authenticator = authenticator
+ self.initializers = []
+ self.features = {}
+
+ # Reset the authenticator
+ authenticator.associateWithStream(self)
+
+
+ def _callLater(self, *args, **kwargs):
+ from twisted.internet import reactor
+ return reactor.callLater(*args, **kwargs)
+
+
+ def reset(self):
+ """
+ Reset XML Stream.
+
+ Resets the XML Parser for incoming data. This is to be used after
+ successfully negotiating a new layer, e.g. TLS and SASL. Note that
+ registered event observers will continue to be in place.
+ """
+ self._headerSent = False
+ self._initializeStream()
+
+
+ def onStreamError(self, errelem):
+ """
+ Called when a stream:error element has been received.
+
+ Dispatches a L{STREAM_ERROR_EVENT} event with the error element to
+ allow for cleanup actions and drops the connection.
+
+ @param errelem: The received error element.
+ @type errelem: L{domish.Element}
+ """
+ self.dispatch(failure.Failure(error.exceptionFromStreamError(errelem)),
+ STREAM_ERROR_EVENT)
+ self.transport.loseConnection()
+
+
+ def sendHeader(self):
+ """
+ Send stream header.
+ """
+ # set up optional extra namespaces
+ localPrefixes = {}
+ for uri, prefix in self.prefixes.iteritems():
+ if uri != NS_STREAMS:
+ localPrefixes[prefix] = uri
+
+ rootElement = domish.Element((NS_STREAMS, 'stream'), self.namespace,
+ localPrefixes=localPrefixes)
+
+ if self.otherEntity:
+ rootElement['to'] = self.otherEntity.userhost()
+
+ if self.thisEntity:
+ rootElement['from'] = self.thisEntity.userhost()
+
+ if not self.initiating and self.sid:
+ rootElement['id'] = self.sid
+
+ if self.version >= (1, 0):
+ rootElement['version'] = "%d.%d" % self.version
+
+ self.send(rootElement.toXml(prefixes=self.prefixes, closeElement=0))
+ self._headerSent = True
+
+
+ def sendFooter(self):
+ """
+ Send stream footer.
+ """
+ self.send('</stream:stream>')
+
+
+ def sendStreamError(self, streamError):
+ """
+ Send stream level error.
+
+ If we are the receiving entity, and haven't sent the header yet,
+ we sent one first.
+
+ After sending the stream error, the stream is closed and the transport
+ connection dropped.
+
+ @param streamError: stream error instance
+ @type streamError: L{error.StreamError}
+ """
+ if not self._headerSent and not self.initiating:
+ self.sendHeader()
+
+ if self._headerSent:
+ self.send(streamError.getElement())
+ self.sendFooter()
+
+ self.transport.loseConnection()
+
+
+ def send(self, obj):
+ """
+ Send data over the stream.
+
+ This overrides L{xmlstream.Xmlstream.send} to use the default namespace
+ of the stream header when serializing L{domish.IElement}s. It is
+ assumed that if you pass an object that provides L{domish.IElement},
+ it represents a direct child of the stream's root element.
+ """
+ if domish.IElement.providedBy(obj):
+ obj = obj.toXml(prefixes=self.prefixes,
+ defaultUri=self.namespace,
+ prefixesInScope=self.prefixes.values())
+
+ xmlstream.XmlStream.send(self, obj)
+
+
+ def connectionMade(self):
+ """
+ Called when a connection is made.
+
+ Notifies the authenticator when a connection has been made.
+ """
+ xmlstream.XmlStream.connectionMade(self)
+ self.authenticator.connectionMade()
+
+
+ def onDocumentStart(self, rootElement):
+ """
+ Called when the stream header has been received.
+
+ Extracts the header's C{id} and C{version} attributes from the root
+ element. The C{id} attribute is stored in our C{sid} attribute and the
+ C{version} attribute is parsed and the minimum of the version we sent
+ and the parsed C{version} attribute is stored as a tuple (major, minor)
+ in this class' C{version} attribute. If no C{version} attribute was
+ present, we assume version 0.0.
+
+ If appropriate (we are the initiating stream and the minimum of our and
+ the other party's version is at least 1.0), a one-time observer is
+ registered for getting the stream features. The registered function is
+ C{onFeatures}.
+
+ Ultimately, the authenticator's C{streamStarted} method will be called.
+
+ @param rootElement: The root element.
+ @type rootElement: L{domish.Element}
+ """
+ xmlstream.XmlStream.onDocumentStart(self, rootElement)
+
+ # Setup observer for stream errors
+ self.addOnetimeObserver("/error[@xmlns='%s']" % NS_STREAMS,
+ self.onStreamError)
+
+ self.authenticator.streamStarted(rootElement)
+
+
+
+class XmlStreamFactory(xmlstream.XmlStreamFactory):
+ """
+ Factory for Jabber XmlStream objects as a reconnecting client.
+
+ Note that this differs from L{xmlstream.XmlStreamFactory} in that
+ it generates Jabber specific L{XmlStream} instances that have
+ authenticators.
+ """
+
+ protocol = XmlStream
+
+ def __init__(self, authenticator):
+ xmlstream.XmlStreamFactory.__init__(self, authenticator)
+ self.authenticator = authenticator
+
+
+
+class XmlStreamServerFactory(xmlstream.BootstrapMixin,
+ protocol.ServerFactory):
+ """
+ Factory for Jabber XmlStream objects as a server.
+
+ @since: 8.2.
+ @ivar authenticatorFactory: Factory callable that takes no arguments, to
+ create a fresh authenticator to be associated
+ with the XmlStream.
+ """
+
+ protocol = XmlStream
+
+ def __init__(self, authenticatorFactory):
+ xmlstream.BootstrapMixin.__init__(self)
+ self.authenticatorFactory = authenticatorFactory
+
+
+ def buildProtocol(self, addr):
+ """
+ Create an instance of XmlStream.
+
+ A new authenticator instance will be created and passed to the new
+ XmlStream. Registered bootstrap event observers are installed as well.
+ """
+ authenticator = self.authenticatorFactory()
+ xs = self.protocol(authenticator)
+ xs.factory = self
+ self.installBootstraps(xs)
+ return xs
+
+
+
+class TimeoutError(Exception):
+ """
+ Exception raised when no IQ response has been received before the
+ configured timeout.
+ """
+
+
+
+def upgradeWithIQResponseTracker(xs):
+ """
+ Enhances an XmlStream for iq response tracking.
+
+ This makes an L{XmlStream} object provide L{IIQResponseTracker}. When a
+ response is an error iq stanza, the deferred has its errback invoked with a
+ failure that holds a L{StanzaException<error.StanzaException>} that is
+ easier to examine.
+ """
+ def callback(iq):
+ """
+ Handle iq response by firing associated deferred.
+ """
+ if getattr(iq, 'handled', False):
+ return
+
+ try:
+ d = xs.iqDeferreds[iq["id"]]
+ except KeyError:
+ pass
+ else:
+ del xs.iqDeferreds[iq["id"]]
+ iq.handled = True
+ if iq['type'] == 'error':
+ d.errback(error.exceptionFromStanza(iq))
+ else:
+ d.callback(iq)
+
+
+ def disconnected(_):
+ """
+ Make sure deferreds do not linger on after disconnect.
+
+ This errbacks all deferreds of iq's for which no response has been
+ received with a L{ConnectionLost} failure. Otherwise, the deferreds
+ will never be fired.
+ """
+ iqDeferreds = xs.iqDeferreds
+ xs.iqDeferreds = {}
+ for d in iqDeferreds.itervalues():
+ d.errback(ConnectionLost())
+
+ xs.iqDeferreds = {}
+ xs.iqDefaultTimeout = getattr(xs, 'iqDefaultTimeout', None)
+ xs.addObserver(xmlstream.STREAM_END_EVENT, disconnected)
+ xs.addObserver('/iq[@type="result"]', callback)
+ xs.addObserver('/iq[@type="error"]', callback)
+ directlyProvides(xs, ijabber.IIQResponseTracker)
+
+
+
+class IQ(domish.Element):
+ """
+ Wrapper for an iq stanza.
+
+ Iq stanzas are used for communications with a request-response behaviour.
+ Each iq request is associated with an XML stream and has its own unique id
+ to be able to track the response.
+
+ @ivar timeout: if set, a timeout period after which the deferred returned
+ by C{send} will have its errback called with a
+ L{TimeoutError} failure.
+ @type timeout: C{float}
+ """
+
+ timeout = None
+
+ def __init__(self, xmlstream, stanzaType="set"):
+ """
+ @type xmlstream: L{xmlstream.XmlStream}
+ @param xmlstream: XmlStream to use for transmission of this IQ
+
+ @type stanzaType: C{str}
+ @param stanzaType: IQ type identifier ('get' or 'set')
+ """
+ domish.Element.__init__(self, (None, "iq"))
+ self.addUniqueId()
+ self["type"] = stanzaType
+ self._xmlstream = xmlstream
+
+
+ def send(self, to=None):
+ """
+ Send out this iq.
+
+ Returns a deferred that is fired when an iq response with the same id
+ is received. Result responses will be passed to the deferred callback.
+ Error responses will be transformed into a
+ L{StanzaError<error.StanzaError>} and result in the errback of the
+ deferred being invoked.
+
+ @rtype: L{defer.Deferred}
+ """
+ if to is not None:
+ self["to"] = to
+
+ if not ijabber.IIQResponseTracker.providedBy(self._xmlstream):
+ upgradeWithIQResponseTracker(self._xmlstream)
+
+ d = defer.Deferred()
+ self._xmlstream.iqDeferreds[self['id']] = d
+
+ timeout = self.timeout or self._xmlstream.iqDefaultTimeout
+ if timeout is not None:
+ def onTimeout():
+ del self._xmlstream.iqDeferreds[self['id']]
+ d.errback(TimeoutError("IQ timed out"))
+
+ call = self._xmlstream._callLater(timeout, onTimeout)
+
+ def cancelTimeout(result):
+ if call.active():
+ call.cancel()
+
+ return result
+
+ d.addBoth(cancelTimeout)
+
+ self._xmlstream.send(self)
+ return d
+
+
+
+def toResponse(stanza, stanzaType=None):
+ """
+ Create a response stanza from another stanza.
+
+ This takes the addressing and id attributes from a stanza to create a (new,
+ empty) response stanza. The addressing attributes are swapped and the id
+ copied. Optionally, the stanza type of the response can be specified.
+
+ @param stanza: the original stanza
+ @type stanza: L{domish.Element}
+ @param stanzaType: optional response stanza type
+ @type stanzaType: C{str}
+ @return: the response stanza.
+ @rtype: L{domish.Element}
+ """
+
+ toAddr = stanza.getAttribute('from')
+ fromAddr = stanza.getAttribute('to')
+ stanzaID = stanza.getAttribute('id')
+
+ response = domish.Element((None, stanza.name))
+ if toAddr:
+ response['to'] = toAddr
+ if fromAddr:
+ response['from'] = fromAddr
+ if stanzaID:
+ response['id'] = stanzaID
+ if stanzaType:
+ response['type'] = stanzaType
+
+ return response
+
+
+
+class XMPPHandler(object):
+ """
+ XMPP protocol handler.
+
+ Classes derived from this class implement (part of) one or more XMPP
+ extension protocols, and are referred to as a subprotocol implementation.
+ """
+
+ implements(ijabber.IXMPPHandler)
+
+ def __init__(self):
+ self.parent = None
+ self.xmlstream = None
+
+
+ def setHandlerParent(self, parent):
+ self.parent = parent
+ self.parent.addHandler(self)
+
+
+ def disownHandlerParent(self, parent):
+ self.parent.removeHandler(self)
+ self.parent = None
+
+
+ def makeConnection(self, xs):
+ self.xmlstream = xs
+ self.connectionMade()
+
+
+ def connectionMade(self):
+ """
+ Called after a connection has been established.
+
+ Can be overridden to perform work before stream initialization.
+ """
+
+
+ def connectionInitialized(self):
+ """
+ The XML stream has been initialized.
+
+ Can be overridden to perform work after stream initialization, e.g. to
+ set up observers and start exchanging XML stanzas.
+ """
+
+
+ def connectionLost(self, reason):
+ """
+ The XML stream has been closed.
+
+ This method can be extended to inspect the C{reason} argument and
+ act on it.
+ """
+ self.xmlstream = None
+
+
+ def send(self, obj):
+ """
+ Send data over the managed XML stream.
+
+ @note: The stream manager maintains a queue for data sent using this
+ method when there is no current initialized XML stream. This
+ data is then sent as soon as a new stream has been established
+ and initialized. Subsequently, L{connectionInitialized} will be
+ called again. If this queueing is not desired, use C{send} on
+ C{self.xmlstream}.
+
+ @param obj: data to be sent over the XML stream. This is usually an
+ object providing L{domish.IElement}, or serialized XML. See
+ L{xmlstream.XmlStream} for details.
+ """
+ self.parent.send(obj)
+
+
+
+class XMPPHandlerCollection(object):
+ """
+ Collection of XMPP subprotocol handlers.
+
+ This allows for grouping of subprotocol handlers, but is not an
+ L{XMPPHandler} itself, so this is not recursive.
+
+ @ivar handlers: List of protocol handlers.
+ @type handlers: C{list} of objects providing
+ L{IXMPPHandler}
+ """
+
+ implements(ijabber.IXMPPHandlerCollection)
+
+ def __init__(self):
+ self.handlers = []
+
+
+ def __iter__(self):
+ """
+ Act as a container for handlers.
+ """
+ return iter(self.handlers)
+
+
+ def addHandler(self, handler):
+ """
+ Add protocol handler.
+
+ Protocol handlers are expected to provide L{ijabber.IXMPPHandler}.
+ """
+ self.handlers.append(handler)
+
+
+ def removeHandler(self, handler):
+ """
+ Remove protocol handler.
+ """
+ self.handlers.remove(handler)
+
+
+
+class StreamManager(XMPPHandlerCollection):
+ """
+ Business logic representing a managed XMPP connection.
+
+ This maintains a single XMPP connection and provides facilities for packet
+ routing and transmission. Business logic modules are objects providing
+ L{ijabber.IXMPPHandler} (like subclasses of L{XMPPHandler}), and added
+ using L{addHandler}.
+
+ @ivar xmlstream: currently managed XML stream
+ @type xmlstream: L{XmlStream}
+ @ivar logTraffic: if true, log all traffic.
+ @type logTraffic: C{bool}
+ @ivar _initialized: Whether the stream represented by L{xmlstream} has
+ been initialized. This is used when caching outgoing
+ stanzas.
+ @type _initialized: C{bool}
+ @ivar _packetQueue: internal buffer of unsent data. See L{send} for details.
+ @type _packetQueue: C{list}
+ """
+
+ logTraffic = False
+
+ def __init__(self, factory):
+ XMPPHandlerCollection.__init__(self)
+ self.xmlstream = None
+ self._packetQueue = []
+ self._initialized = False
+
+ factory.addBootstrap(STREAM_CONNECTED_EVENT, self._connected)
+ factory.addBootstrap(STREAM_AUTHD_EVENT, self._authd)
+ factory.addBootstrap(INIT_FAILED_EVENT, self.initializationFailed)
+ factory.addBootstrap(STREAM_END_EVENT, self._disconnected)
+ self.factory = factory
+
+
+ def addHandler(self, handler):
+ """
+ Add protocol handler.
+
+ When an XML stream has already been established, the handler's
+ C{connectionInitialized} will be called to get it up to speed.
+ """
+ XMPPHandlerCollection.addHandler(self, handler)
+
+ # get protocol handler up to speed when a connection has already
+ # been established
+ if self.xmlstream and self._initialized:
+ handler.makeConnection(self.xmlstream)
+ handler.connectionInitialized()
+
+
+ def _connected(self, xs):
+ """
+ Called when the transport connection has been established.
+
+ Here we optionally set up traffic logging (depending on L{logTraffic})
+ and call each handler's C{makeConnection} method with the L{XmlStream}
+ instance.
+ """
+ def logDataIn(buf):
+ log.msg("RECV: %r" % buf)
+
+ def logDataOut(buf):
+ log.msg("SEND: %r" % buf)
+
+ if self.logTraffic:
+ xs.rawDataInFn = logDataIn
+ xs.rawDataOutFn = logDataOut
+
+ self.xmlstream = xs
+
+ for e in self:
+ e.makeConnection(xs)
+
+
+ def _authd(self, xs):
+ """
+ Called when the stream has been initialized.
+
+ Send out cached stanzas and call each handler's
+ C{connectionInitialized} method.
+ """
+ # Flush all pending packets
+ for p in self._packetQueue:
+ xs.send(p)
+ self._packetQueue = []
+ self._initialized = True
+
+ # Notify all child services which implement
+ # the IService interface
+ for e in self:
+ e.connectionInitialized()
+
+
+ def initializationFailed(self, reason):
+ """
+ Called when stream initialization has failed.
+
+ Stream initialization has halted, with the reason indicated by
+ C{reason}. It may be retried by calling the authenticator's
+ C{initializeStream}. See the respective authenticators for details.
+
+ @param reason: A failure instance indicating why stream initialization
+ failed.
+ @type reason: L{failure.Failure}
+ """
+
+
+ def _disconnected(self, reason):
+ """
+ Called when the stream has been closed.
+
+ From this point on, the manager doesn't interact with the
+ L{XmlStream} anymore and notifies each handler that the connection
+ was lost by calling its C{connectionLost} method.
+ """
+ self.xmlstream = None
+ self._initialized = False
+
+ # Notify all child services which implement
+ # the IService interface
+ for e in self:
+ e.connectionLost(reason)
+
+
+ def send(self, obj):
+ """
+ Send data over the XML stream.
+
+ When there is no established XML stream, the data is queued and sent
+ out when a new XML stream has been established and initialized.
+
+ @param obj: data to be sent over the XML stream. See
+ L{xmlstream.XmlStream.send} for details.
+ """
+ if self._initialized:
+ self.xmlstream.send(obj)
+ else:
+ self._packetQueue.append(obj)
+
+
+
+__all__ = ['Authenticator', 'BaseFeatureInitiatingInitializer',
+ 'ConnectAuthenticator', 'FeatureNotAdvertized',
+ 'INIT_FAILED_EVENT', 'IQ', 'ListenAuthenticator', 'NS_STREAMS',
+ 'NS_XMPP_TLS', 'Reset', 'STREAM_AUTHD_EVENT',
+ 'STREAM_CONNECTED_EVENT', 'STREAM_END_EVENT', 'STREAM_ERROR_EVENT',
+ 'STREAM_START_EVENT', 'StreamManager', 'TLSError', 'TLSFailed',
+ 'TLSInitiatingInitializer', 'TLSNotSupported', 'TLSRequired',
+ 'TimeoutError', 'XMPPHandler', 'XMPPHandlerCollection', 'XmlStream',
+ 'XmlStreamFactory', 'XmlStreamServerFactory', 'hashPassword',
+ 'toResponse', 'upgradeWithIQResponseTracker']
diff --git a/twisted/words/protocols/jabber/xmpp_stringprep.py b/twisted/words/protocols/jabber/xmpp_stringprep.py
new file mode 100644
index 0000000..7527412
--- /dev/null
+++ b/twisted/words/protocols/jabber/xmpp_stringprep.py
@@ -0,0 +1,253 @@
+# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys, warnings
+from zope.interface import Interface, implements
+
+if sys.version_info < (2,3,2):
+ import re
+
+ class IDNA:
+ dots = re.compile(u"[\u002E\u3002\uFF0E\uFF61]")
+ def nameprep(self, label):
+ return label.lower()
+
+ idna = IDNA()
+
+ crippled = True
+
+ warnings.warn("Accented and non-Western Jabber IDs will not be properly "
+ "case-folded with this version of Python, resulting in "
+ "incorrect protocol-level behavior. It is strongly "
+ "recommended you upgrade to Python 2.3.2 or newer if you "
+ "intend to use Twisted's Jabber support.")
+
+else:
+ import stringprep
+ # We require Unicode version 3.2. Python 2.5 and later provide this as
+ # a separate object. Before that the unicodedata module uses 3.2.
+ try:
+ from unicodedata import ucd_3_2_0 as unicodedata
+ except:
+ import unicodedata
+ from encodings import idna
+
+ crippled = False
+
+del sys, warnings
+
+class ILookupTable(Interface):
+ """ Interface for character lookup classes. """
+
+ def lookup(c):
+ """ Return whether character is in this table. """
+
+class IMappingTable(Interface):
+ """ Interface for character mapping classes. """
+
+ def map(c):
+ """ Return mapping for character. """
+
+class LookupTableFromFunction:
+
+ implements(ILookupTable)
+
+ def __init__(self, in_table_function):
+ self.lookup = in_table_function
+
+class LookupTable:
+
+ implements(ILookupTable)
+
+ def __init__(self, table):
+ self._table = table
+
+ def lookup(self, c):
+ return c in self._table
+
+class MappingTableFromFunction:
+
+ implements(IMappingTable)
+
+ def __init__(self, map_table_function):
+ self.map = map_table_function
+
+class EmptyMappingTable:
+
+ implements(IMappingTable)
+
+ def __init__(self, in_table_function):
+ self._in_table_function = in_table_function
+
+ def map(self, c):
+ if self._in_table_function(c):
+ return None
+ else:
+ return c
+
+class Profile:
+ def __init__(self, mappings=[], normalize=True, prohibiteds=[],
+ check_unassigneds=True, check_bidi=True):
+ self.mappings = mappings
+ self.normalize = normalize
+ self.prohibiteds = prohibiteds
+ self.do_check_unassigneds = check_unassigneds
+ self.do_check_bidi = check_bidi
+
+ def prepare(self, string):
+ result = self.map(string)
+ if self.normalize:
+ result = unicodedata.normalize("NFKC", result)
+ self.check_prohibiteds(result)
+ if self.do_check_unassigneds:
+ self.check_unassigneds(result)
+ if self.do_check_bidi:
+ self.check_bidirectionals(result)
+ return result
+
+ def map(self, string):
+ result = []
+
+ for c in string:
+ result_c = c
+
+ for mapping in self.mappings:
+ result_c = mapping.map(c)
+ if result_c != c:
+ break
+
+ if result_c is not None:
+ result.append(result_c)
+
+ return u"".join(result)
+
+ def check_prohibiteds(self, string):
+ for c in string:
+ for table in self.prohibiteds:
+ if table.lookup(c):
+ raise UnicodeError, "Invalid character %s" % repr(c)
+
+ def check_unassigneds(self, string):
+ for c in string:
+ if stringprep.in_table_a1(c):
+ raise UnicodeError, "Unassigned code point %s" % repr(c)
+
+ def check_bidirectionals(self, string):
+ found_LCat = False
+ found_RandALCat = False
+
+ for c in string:
+ if stringprep.in_table_d1(c):
+ found_RandALCat = True
+ if stringprep.in_table_d2(c):
+ found_LCat = True
+
+ if found_LCat and found_RandALCat:
+ raise UnicodeError, "Violation of BIDI Requirement 2"
+
+ if found_RandALCat and not (stringprep.in_table_d1(string[0]) and
+ stringprep.in_table_d1(string[-1])):
+ raise UnicodeError, "Violation of BIDI Requirement 3"
+
+
+class NamePrep:
+ """ Implements preparation of internationalized domain names.
+
+ This class implements preparing internationalized domain names using the
+ rules defined in RFC 3491, section 4 (Conversion operations).
+
+ We do not perform step 4 since we deal with unicode representations of
+ domain names and do not convert from or to ASCII representations using
+ punycode encoding. When such a conversion is needed, the C{idna} standard
+ library provides the C{ToUnicode()} and C{ToASCII()} functions. Note that
+ C{idna} itself assumes UseSTD3ASCIIRules to be false.
+
+ The following steps are performed by C{prepare()}:
+
+ - Split the domain name in labels at the dots (RFC 3490, 3.1)
+ - Apply nameprep proper on each label (RFC 3491)
+ - Enforce the restrictions on ASCII characters in host names by
+ assuming STD3ASCIIRules to be true. (STD 3)
+ - Rejoin the labels using the label separator U+002E (full stop).
+
+ """
+
+ # Prohibited characters.
+ prohibiteds = [unichr(n) for n in range(0x00, 0x2c + 1) +
+ range(0x2e, 0x2f + 1) +
+ range(0x3a, 0x40 + 1) +
+ range(0x5b, 0x60 + 1) +
+ range(0x7b, 0x7f + 1) ]
+
+ def prepare(self, string):
+ result = []
+
+ labels = idna.dots.split(string)
+
+ if labels and len(labels[-1]) == 0:
+ trailing_dot = '.'
+ del labels[-1]
+ else:
+ trailing_dot = ''
+
+ for label in labels:
+ result.append(self.nameprep(label))
+
+ return ".".join(result) + trailing_dot
+
+ def check_prohibiteds(self, string):
+ for c in string:
+ if c in self.prohibiteds:
+ raise UnicodeError, "Invalid character %s" % repr(c)
+
+ def nameprep(self, label):
+ label = idna.nameprep(label)
+ self.check_prohibiteds(label)
+ if label[0] == '-':
+ raise UnicodeError, "Invalid leading hyphen-minus"
+ if label[-1] == '-':
+ raise UnicodeError, "Invalid trailing hyphen-minus"
+ return label
+
+if crippled:
+ case_map = MappingTableFromFunction(lambda c: c.lower())
+ nodeprep = Profile(mappings=[case_map],
+ normalize=False,
+ prohibiteds=[LookupTable([u' ', u'"', u'&', u"'", u'/',
+ u':', u'<', u'>', u'@'])],
+ check_unassigneds=False,
+ check_bidi=False)
+
+ resourceprep = Profile(normalize=False,
+ check_unassigneds=False,
+ check_bidi=False)
+
+else:
+ C_11 = LookupTableFromFunction(stringprep.in_table_c11)
+ C_12 = LookupTableFromFunction(stringprep.in_table_c12)
+ C_21 = LookupTableFromFunction(stringprep.in_table_c21)
+ C_22 = LookupTableFromFunction(stringprep.in_table_c22)
+ C_3 = LookupTableFromFunction(stringprep.in_table_c3)
+ C_4 = LookupTableFromFunction(stringprep.in_table_c4)
+ C_5 = LookupTableFromFunction(stringprep.in_table_c5)
+ C_6 = LookupTableFromFunction(stringprep.in_table_c6)
+ C_7 = LookupTableFromFunction(stringprep.in_table_c7)
+ C_8 = LookupTableFromFunction(stringprep.in_table_c8)
+ C_9 = LookupTableFromFunction(stringprep.in_table_c9)
+
+ B_1 = EmptyMappingTable(stringprep.in_table_b1)
+ B_2 = MappingTableFromFunction(stringprep.map_table_b2)
+
+ nodeprep = Profile(mappings=[B_1, B_2],
+ prohibiteds=[C_11, C_12, C_21, C_22,
+ C_3, C_4, C_5, C_6, C_7, C_8, C_9,
+ LookupTable([u'"', u'&', u"'", u'/',
+ u':', u'<', u'>', u'@'])])
+
+ resourceprep = Profile(mappings=[B_1,],
+ prohibiteds=[C_12, C_21, C_22,
+ C_3, C_4, C_5, C_6, C_7, C_8, C_9])
+
+nameprep = NamePrep()
diff --git a/twisted/words/protocols/msn.py b/twisted/words/protocols/msn.py
new file mode 100644
index 0000000..5e23a4d
--- /dev/null
+++ b/twisted/words/protocols/msn.py
@@ -0,0 +1,2479 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+MSNP8 Protocol (client only) - semi-experimental
+
+This module provides support for clients using the MSN Protocol (MSNP8).
+There are basically 3 servers involved in any MSN session:
+
+I{Dispatch server}
+
+The DispatchClient class handles connections to the
+dispatch server, which basically delegates users to a
+suitable notification server.
+
+You will want to subclass this and handle the gotNotificationReferral
+method appropriately.
+
+I{Notification Server}
+
+The NotificationClient class handles connections to the
+notification server, which acts as a session server
+(state updates, message negotiation etc...)
+
+I{Switcboard Server}
+
+The SwitchboardClient handles connections to switchboard
+servers which are used to conduct conversations with other users.
+
+There are also two classes (FileSend and FileReceive) used
+for file transfers.
+
+Clients handle events in two ways.
+
+ - each client request requiring a response will return a Deferred,
+ the callback for same will be fired when the server sends the
+ required response
+ - Events which are not in response to any client request have
+ respective methods which should be overridden and handled in
+ an adequate manner
+
+Most client request callbacks require more than one argument,
+and since Deferreds can only pass the callback one result,
+most of the time the callback argument will be a tuple of
+values (documented in the respective request method).
+To make reading/writing code easier, callbacks can be defined in
+a number of ways to handle this 'cleanly'. One way would be to
+define methods like: def callBack(self, (arg1, arg2, arg)): ...
+another way would be to do something like:
+d.addCallback(lambda result: myCallback(*result)).
+
+If the server sends an error response to a client request,
+the errback of the corresponding Deferred will be called,
+the argument being the corresponding error code.
+
+B{NOTE}:
+Due to the lack of an official spec for MSNP8, extra checking
+than may be deemed necessary often takes place considering the
+server is never 'wrong'. Thus, if gotBadLine (in any of the 3
+main clients) is called, or an MSNProtocolError is raised, it's
+probably a good idea to submit a bug report. ;)
+Use of this module requires that PyOpenSSL is installed.
+
+TODO
+====
+- check message hooks with invalid x-msgsinvite messages.
+- font handling
+- switchboard factory
+
+@author: Sam Jordan
+"""
+
+import types, operator, os
+from random import randint
+from urllib import quote, unquote
+
+from twisted.python import failure, log
+from twisted.python.hashlib import md5
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, execute
+from twisted.internet.protocol import ClientFactory
+try:
+ from twisted.internet.ssl import ClientContextFactory
+except ImportError:
+ ClientContextFactory = None
+from twisted.protocols.basic import LineReceiver
+from twisted.web.http import HTTPClient
+
+
+MSN_PROTOCOL_VERSION = "MSNP8 CVR0" # protocol version
+MSN_PORT = 1863 # default dispatch server port
+MSN_MAX_MESSAGE = 1664 # max message length
+MSN_CHALLENGE_STR = "Q1P7W2E4J9R8U3S5" # used for server challenges
+MSN_CVR_STR = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
+
+# auth constants
+LOGIN_SUCCESS = 1
+LOGIN_FAILURE = 2
+LOGIN_REDIRECT = 3
+
+# list constants
+FORWARD_LIST = 1
+ALLOW_LIST = 2
+BLOCK_LIST = 4
+REVERSE_LIST = 8
+
+# phone constants
+HOME_PHONE = "PHH"
+WORK_PHONE = "PHW"
+MOBILE_PHONE = "PHM"
+HAS_PAGER = "MOB"
+
+# status constants
+STATUS_ONLINE = 'NLN'
+STATUS_OFFLINE = 'FLN'
+STATUS_HIDDEN = 'HDN'
+STATUS_IDLE = 'IDL'
+STATUS_AWAY = 'AWY'
+STATUS_BUSY = 'BSY'
+STATUS_BRB = 'BRB'
+STATUS_PHONE = 'PHN'
+STATUS_LUNCH = 'LUN'
+
+CR = "\r"
+LF = "\n"
+
+
+class SSLRequired(Exception):
+ """
+ This exception is raised when it is necessary to talk to a passport server
+ using SSL, but the necessary SSL dependencies are unavailable.
+
+ @since: 11.0
+ """
+
+
+
+def checkParamLen(num, expected, cmd, error=None):
+ if error == None:
+ error = "Invalid Number of Parameters for %s" % cmd
+ if num != expected:
+ raise MSNProtocolError, error
+
+def _parseHeader(h, v):
+ """
+ Split a certin number of known
+ header values with the format:
+ field1=val,field2=val,field3=val into
+ a dict mapping fields to values.
+ @param h: the header's key
+ @param v: the header's value as a string
+ """
+
+ if h in ('passporturls','authentication-info','www-authenticate'):
+ v = v.replace('Passport1.4','').lstrip()
+ fields = {}
+ for fieldPair in v.split(','):
+ try:
+ field,value = fieldPair.split('=',1)
+ fields[field.lower()] = value
+ except ValueError:
+ fields[field.lower()] = ''
+ return fields
+ else:
+ return v
+
+def _parsePrimitiveHost(host):
+ # Ho Ho Ho
+ h,p = host.replace('https://','').split('/',1)
+ p = '/' + p
+ return h,p
+
+
+def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
+ """
+ This function is used internally and should not ever be called
+ directly.
+
+ @raise SSLRequired: If there is no SSL support available.
+ """
+ if ClientContextFactory is None:
+ raise SSLRequired(
+ 'Connecting to the Passport server requires SSL, but SSL is '
+ 'unavailable.')
+
+ cb = Deferred()
+ def _cb(server, auth):
+ loginFac = ClientFactory()
+ loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
+ reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
+
+ if cached:
+ _cb(nexusServer, authData)
+ else:
+ fac = ClientFactory()
+ d = Deferred()
+ d.addCallbacks(_cb, callbackArgs=(authData,))
+ d.addErrback(lambda f: cb.errback(f))
+ fac.protocol = lambda : PassportNexus(d, nexusServer)
+ reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
+ return cb
+
+
+class PassportNexus(HTTPClient):
+
+ """
+ Used to obtain the URL of a valid passport
+ login HTTPS server.
+
+ This class is used internally and should
+ not be instantiated directly -- that is,
+ The passport logging in process is handled
+ transparantly by NotificationClient.
+ """
+
+ def __init__(self, deferred, host):
+ self.deferred = deferred
+ self.host, self.path = _parsePrimitiveHost(host)
+
+ def connectionMade(self):
+ HTTPClient.connectionMade(self)
+ self.sendCommand('GET', self.path)
+ self.sendHeader('Host', self.host)
+ self.endHeaders()
+ self.headers = {}
+
+ def handleHeader(self, header, value):
+ h = header.lower()
+ self.headers[h] = _parseHeader(h, value)
+
+ def handleEndHeaders(self):
+ if self.connected:
+ self.transport.loseConnection()
+ if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
+ self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
+ self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
+
+ def handleResponse(self, r):
+ pass
+
+class PassportLogin(HTTPClient):
+ """
+ This class is used internally to obtain
+ a login ticket from a passport HTTPS
+ server -- it should not be used directly.
+ """
+
+ _finished = 0
+
+ def __init__(self, deferred, userHandle, passwd, host, authData):
+ self.deferred = deferred
+ self.userHandle = userHandle
+ self.passwd = passwd
+ self.authData = authData
+ self.host, self.path = _parsePrimitiveHost(host)
+
+ def connectionMade(self):
+ self.sendCommand('GET', self.path)
+ self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
+ 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
+ self.sendHeader('Host', self.host)
+ self.endHeaders()
+ self.headers = {}
+
+ def handleHeader(self, header, value):
+ h = header.lower()
+ self.headers[h] = _parseHeader(h, value)
+
+ def handleEndHeaders(self):
+ if self._finished:
+ return
+ self._finished = 1 # I think we need this because of HTTPClient
+ if self.connected:
+ self.transport.loseConnection()
+ authHeader = 'authentication-info'
+ _interHeader = 'www-authenticate'
+ if self.headers.has_key(_interHeader):
+ authHeader = _interHeader
+ try:
+ info = self.headers[authHeader]
+ status = info['da-status']
+ handler = getattr(self, 'login_%s' % (status,), None)
+ if handler:
+ handler(info)
+ else:
+ raise Exception()
+ except Exception, e:
+ self.deferred.errback(failure.Failure(e))
+
+ def handleResponse(self, r):
+ pass
+
+ def login_success(self, info):
+ ticket = info['from-pp']
+ ticket = ticket[1:len(ticket)-1]
+ self.deferred.callback((LOGIN_SUCCESS, ticket))
+
+ def login_failed(self, info):
+ self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
+
+ def login_redir(self, info):
+ self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
+
+
+class MSNProtocolError(Exception):
+ """
+ This Exception is basically used for debugging
+ purposes, as the official MSN server should never
+ send anything _wrong_ and nobody in their right
+ mind would run their B{own} MSN server.
+ If it is raised by default command handlers
+ (handle_BLAH) the error will be logged.
+ """
+ pass
+
+
+class MSNCommandFailed(Exception):
+ """
+ The server said that the command failed.
+ """
+
+ def __init__(self, errorCode):
+ self.errorCode = errorCode
+
+ def __str__(self):
+ return ("Command failed: %s (error code %d)"
+ % (errorCodes[self.errorCode], self.errorCode))
+
+
+class MSNMessage:
+ """
+ I am the class used to represent an 'instant' message.
+
+ @ivar userHandle: The user handle (passport) of the sender
+ (this is only used when receiving a message)
+ @ivar screenName: The screen name of the sender (this is only used
+ when receiving a message)
+ @ivar message: The message
+ @ivar headers: The message headers
+ @type headers: dict
+ @ivar length: The message length (including headers and line endings)
+ @ivar ack: This variable is used to tell the server how to respond
+ once the message has been sent. If set to MESSAGE_ACK
+ (default) the server will respond with an ACK upon receiving
+ the message, if set to MESSAGE_NACK the server will respond
+ with a NACK upon failure to receive the message.
+ If set to MESSAGE_ACK_NONE the server will do nothing.
+ This is relevant for the return value of
+ SwitchboardClient.sendMessage (which will return
+ a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
+ and will fire when the respective ACK or NACK is received).
+ If set to MESSAGE_ACK_NONE sendMessage will return None.
+ """
+ MESSAGE_ACK = 'A'
+ MESSAGE_NACK = 'N'
+ MESSAGE_ACK_NONE = 'U'
+
+ ack = MESSAGE_ACK
+
+ def __init__(self, length=0, userHandle="", screenName="", message=""):
+ self.userHandle = userHandle
+ self.screenName = screenName
+ self.message = message
+ self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
+ self.length = length
+ self.readPos = 0
+
+ def _calcMessageLen(self):
+ """
+ used to calculte the number to send
+ as the message length when sending a message.
+ """
+ return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
+
+ def setHeader(self, header, value):
+ """ set the desired header """
+ self.headers[header] = value
+
+ def getHeader(self, header):
+ """
+ get the desired header value
+ @raise KeyError: if no such header exists.
+ """
+ return self.headers[header]
+
+ def hasHeader(self, header):
+ """ check to see if the desired header exists """
+ return self.headers.has_key(header)
+
+ def getMessage(self):
+ """ return the message - not including headers """
+ return self.message
+
+ def setMessage(self, message):
+ """ set the message text """
+ self.message = message
+
+class MSNContact:
+
+ """
+ This class represents a contact (user).
+
+ @ivar userHandle: The contact's user handle (passport).
+ @ivar screenName: The contact's screen name.
+ @ivar groups: A list of all the group IDs which this
+ contact belongs to.
+ @ivar lists: An integer representing the sum of all lists
+ that this contact belongs to.
+ @ivar status: The contact's status code.
+ @type status: str if contact's status is known, None otherwise.
+
+ @ivar homePhone: The contact's home phone number.
+ @type homePhone: str if known, otherwise None.
+ @ivar workPhone: The contact's work phone number.
+ @type workPhone: str if known, otherwise None.
+ @ivar mobilePhone: The contact's mobile phone number.
+ @type mobilePhone: str if known, otherwise None.
+ @ivar hasPager: Whether or not this user has a mobile pager
+ (true=yes, false=no)
+ """
+
+ def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
+ self.userHandle = userHandle
+ self.screenName = screenName
+ self.lists = lists
+ self.groups = [] # if applicable
+ self.status = status # current status
+
+ # phone details
+ self.homePhone = None
+ self.workPhone = None
+ self.mobilePhone = None
+ self.hasPager = None
+
+ def setPhone(self, phoneType, value):
+ """
+ set phone numbers/values for this specific user.
+ for phoneType check the *_PHONE constants and HAS_PAGER
+ """
+
+ t = phoneType.upper()
+ if t == HOME_PHONE:
+ self.homePhone = value
+ elif t == WORK_PHONE:
+ self.workPhone = value
+ elif t == MOBILE_PHONE:
+ self.mobilePhone = value
+ elif t == HAS_PAGER:
+ self.hasPager = value
+ else:
+ raise ValueError, "Invalid Phone Type"
+
+ def addToList(self, listType):
+ """
+ Update the lists attribute to
+ reflect being part of the
+ given list.
+ """
+ self.lists |= listType
+
+ def removeFromList(self, listType):
+ """
+ Update the lists attribute to
+ reflect being removed from the
+ given list.
+ """
+ self.lists ^= listType
+
+class MSNContactList:
+ """
+ This class represents a basic MSN contact list.
+
+ @ivar contacts: All contacts on my various lists
+ @type contacts: dict (mapping user handles to MSNContact objects)
+ @ivar version: The current contact list version (used for list syncing)
+ @ivar groups: a mapping of group ids to group names
+ (groups can only exist on the forward list)
+ @type groups: dict
+
+ B{Note}:
+ This is used only for storage and doesn't effect the
+ server's contact list.
+ """
+
+ def __init__(self):
+ self.contacts = {}
+ self.version = 0
+ self.groups = {}
+ self.autoAdd = 0
+ self.privacy = 0
+
+ def _getContactsFromList(self, listType):
+ """
+ Obtain all contacts which belong
+ to the given list type.
+ """
+ return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
+
+ def addContact(self, contact):
+ """
+ Add a contact
+ """
+ self.contacts[contact.userHandle] = contact
+
+ def remContact(self, userHandle):
+ """
+ Remove a contact
+ """
+ try:
+ del self.contacts[userHandle]
+ except KeyError:
+ pass
+
+ def getContact(self, userHandle):
+ """
+ Obtain the MSNContact object
+ associated with the given
+ userHandle.
+ @return: the MSNContact object if
+ the user exists, or None.
+ """
+ try:
+ return self.contacts[userHandle]
+ except KeyError:
+ return None
+
+ def getBlockedContacts(self):
+ """
+ Obtain all the contacts on my block list
+ """
+ return self._getContactsFromList(BLOCK_LIST)
+
+ def getAuthorizedContacts(self):
+ """
+ Obtain all the contacts on my auth list.
+ (These are contacts which I have verified
+ can view my state changes).
+ """
+ return self._getContactsFromList(ALLOW_LIST)
+
+ def getReverseContacts(self):
+ """
+ Get all contacts on my reverse list.
+ (These are contacts which have added me
+ to their forward list).
+ """
+ return self._getContactsFromList(REVERSE_LIST)
+
+ def getContacts(self):
+ """
+ Get all contacts on my forward list.
+ (These are the contacts which I have added
+ to my list).
+ """
+ return self._getContactsFromList(FORWARD_LIST)
+
+ def setGroup(self, id, name):
+ """
+ Keep a mapping from the given id
+ to the given name.
+ """
+ self.groups[id] = name
+
+ def remGroup(self, id):
+ """
+ Removed the stored group
+ mapping for the given id.
+ """
+ try:
+ del self.groups[id]
+ except KeyError:
+ pass
+ for c in self.contacts:
+ if id in c.groups:
+ c.groups.remove(id)
+
+
+class MSNEventBase(LineReceiver):
+ """
+ This class provides support for handling / dispatching events and is the
+ base class of the three main client protocols (DispatchClient,
+ NotificationClient, SwitchboardClient)
+ """
+
+ def __init__(self):
+ self.ids = {} # mapping of ids to Deferreds
+ self.currentID = 0
+ self.connected = 0
+ self.setLineMode()
+ self.currentMessage = None
+
+ def connectionLost(self, reason):
+ self.ids = {}
+ self.connected = 0
+
+ def connectionMade(self):
+ self.connected = 1
+
+ def _fireCallback(self, id, *args):
+ """
+ Fire the callback for the given id
+ if one exists and return 1, else return false
+ """
+ if self.ids.has_key(id):
+ self.ids[id][0].callback(args)
+ del self.ids[id]
+ return 1
+ return 0
+
+ def _nextTransactionID(self):
+ """ return a usable transaction ID """
+ self.currentID += 1
+ if self.currentID > 1000:
+ self.currentID = 1
+ return self.currentID
+
+ def _createIDMapping(self, data=None):
+ """
+ return a unique transaction ID that is mapped internally to a
+ deferred .. also store arbitrary data if it is needed
+ """
+ id = self._nextTransactionID()
+ d = Deferred()
+ self.ids[id] = (d, data)
+ return (id, d)
+
+ def checkMessage(self, message):
+ """
+ process received messages to check for file invitations and
+ typing notifications and other control type messages
+ """
+ raise NotImplementedError
+
+ def lineReceived(self, line):
+ if self.currentMessage:
+ self.currentMessage.readPos += len(line+CR+LF)
+ if line == "":
+ self.setRawMode()
+ if self.currentMessage.readPos == self.currentMessage.length:
+ self.rawDataReceived("") # :(
+ return
+ try:
+ header, value = line.split(':')
+ except ValueError:
+ raise MSNProtocolError, "Invalid Message Header"
+ self.currentMessage.setHeader(header, unquote(value).lstrip())
+ return
+ try:
+ cmd, params = line.split(' ', 1)
+ except ValueError:
+ raise MSNProtocolError, "Invalid Message, %s" % repr(line)
+
+ if len(cmd) != 3:
+ raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
+ if cmd.isdigit():
+ errorCode = int(cmd)
+ id = int(params.split()[0])
+ if id in self.ids:
+ self.ids[id][0].errback(MSNCommandFailed(errorCode))
+ del self.ids[id]
+ return
+ else: # we received an error which doesn't map to a sent command
+ self.gotError(errorCode)
+ return
+
+ handler = getattr(self, "handle_%s" % cmd.upper(), None)
+ if handler:
+ try:
+ handler(params.split())
+ except MSNProtocolError, why:
+ self.gotBadLine(line, why)
+ else:
+ self.handle_UNKNOWN(cmd, params.split())
+
+ def rawDataReceived(self, data):
+ extra = ""
+ self.currentMessage.readPos += len(data)
+ diff = self.currentMessage.readPos - self.currentMessage.length
+ if diff > 0:
+ self.currentMessage.message += data[:-diff]
+ extra = data[-diff:]
+ elif diff == 0:
+ self.currentMessage.message += data
+ else:
+ self.currentMessage += data
+ return
+ del self.currentMessage.readPos
+ m = self.currentMessage
+ self.currentMessage = None
+ self.setLineMode(extra)
+ if not self.checkMessage(m):
+ return
+ self.gotMessage(m)
+
+ ### protocol command handlers - no need to override these.
+
+ def handle_MSG(self, params):
+ checkParamLen(len(params), 3, 'MSG')
+ try:
+ messageLen = int(params[2])
+ except ValueError:
+ raise MSNProtocolError, "Invalid Parameter for MSG length argument"
+ self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
+
+ def handle_UNKNOWN(self, cmd, params):
+ """ implement me in subclasses if you want to handle unknown events """
+ log.msg("Received unknown command (%s), params: %s" % (cmd, params))
+
+ ### callbacks
+
+ def gotMessage(self, message):
+ """
+ called when we receive a message - override in notification
+ and switchboard clients
+ """
+ raise NotImplementedError
+
+ def gotBadLine(self, line, why):
+ """ called when a handler notifies me that this line is broken """
+ log.msg('Error in line: %s (%s)' % (line, why))
+
+ def gotError(self, errorCode):
+ """
+ called when the server sends an error which is not in
+ response to a sent command (ie. it has no matching transaction ID)
+ """
+ log.msg('Error %s' % (errorCodes[errorCode]))
+
+
+
+class DispatchClient(MSNEventBase):
+ """
+ This class provides support for clients connecting to the dispatch server
+ @ivar userHandle: your user handle (passport) needed before connecting.
+ """
+
+ # eventually this may become an attribute of the
+ # factory.
+ userHandle = ""
+
+ def connectionMade(self):
+ MSNEventBase.connectionMade(self)
+ self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+
+ ### protocol command handlers ( there is no need to override these )
+
+ def handle_VER(self, params):
+ id = self._nextTransactionID()
+ self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
+
+ def handle_CVR(self, params):
+ self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
+
+ def handle_XFR(self, params):
+ if len(params) < 4:
+ raise MSNProtocolError, "Invalid number of parameters for XFR"
+ id, refType, addr = params[:3]
+ # was addr a host:port pair?
+ try:
+ host, port = addr.split(':')
+ except ValueError:
+ host = addr
+ port = MSN_PORT
+ if refType == "NS":
+ self.gotNotificationReferral(host, int(port))
+
+ ### callbacks
+
+ def gotNotificationReferral(self, host, port):
+ """
+ called when we get a referral to the notification server.
+
+ @param host: the notification server's hostname
+ @param port: the port to connect to
+ """
+ pass
+
+
+class NotificationClient(MSNEventBase):
+ """
+ This class provides support for clients connecting
+ to the notification server.
+ """
+
+ factory = None # sssh pychecker
+
+ def __init__(self, currentID=0):
+ MSNEventBase.__init__(self)
+ self.currentID = currentID
+ self._state = ['DISCONNECTED', {}]
+
+ def _setState(self, state):
+ self._state[0] = state
+
+ def _getState(self):
+ return self._state[0]
+
+ def _getStateData(self, key):
+ return self._state[1][key]
+
+ def _setStateData(self, key, value):
+ self._state[1][key] = value
+
+ def _remStateData(self, *args):
+ for key in args:
+ del self._state[1][key]
+
+ def connectionMade(self):
+ MSNEventBase.connectionMade(self)
+ self._setState('CONNECTED')
+ self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+
+ def connectionLost(self, reason):
+ self._setState('DISCONNECTED')
+ self._state[1] = {}
+ MSNEventBase.connectionLost(self, reason)
+
+ def checkMessage(self, message):
+ """ hook used for detecting specific notification messages """
+ cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
+ if 'text/x-msmsgsprofile' in cTypes:
+ self.gotProfile(message)
+ return 0
+ return 1
+
+ ### protocol command handlers - no need to override these
+
+ def handle_VER(self, params):
+ id = self._nextTransactionID()
+ self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.factory.userHandle))
+
+ def handle_CVR(self, params):
+ self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
+
+ def handle_USR(self, params):
+ if len(params) != 4 and len(params) != 6:
+ raise MSNProtocolError, "Invalid Number of Parameters for USR"
+
+ mechanism = params[1]
+ if mechanism == "OK":
+ self.loggedIn(params[2], unquote(params[3]), int(params[4]))
+ elif params[2].upper() == "S":
+ # we need to obtain auth from a passport server
+ f = self.factory
+ d = execute(
+ _login, f.userHandle, f.password, f.passportServer,
+ authData=params[3])
+ d.addCallback(self._passportLogin)
+ d.addErrback(self._passportError)
+
+ def _passportLogin(self, result):
+ if result[0] == LOGIN_REDIRECT:
+ d = _login(self.factory.userHandle, self.factory.password,
+ result[1], cached=1, authData=result[2])
+ d.addCallback(self._passportLogin)
+ d.addErrback(self._passportError)
+ elif result[0] == LOGIN_SUCCESS:
+ self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
+ elif result[0] == LOGIN_FAILURE:
+ self.loginFailure(result[1])
+
+
+ def _passportError(self, failure):
+ """
+ Handle a problem logging in via the Passport server, passing on the
+ error as a string message to the C{loginFailure} callback.
+ """
+ if failure.check(SSLRequired):
+ failure = failure.getErrorMessage()
+ self.loginFailure("Exception while authenticating: %s" % failure)
+
+
+ def handle_CHG(self, params):
+ checkParamLen(len(params), 3, 'CHG')
+ id = int(params[0])
+ if not self._fireCallback(id, params[1]):
+ self.statusChanged(params[1])
+
+ def handle_ILN(self, params):
+ checkParamLen(len(params), 5, 'ILN')
+ self.gotContactStatus(params[1], params[2], unquote(params[3]))
+
+ def handle_CHL(self, params):
+ checkParamLen(len(params), 2, 'CHL')
+ self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
+ self.transport.write(md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
+
+ def handle_QRY(self, params):
+ pass
+
+ def handle_NLN(self, params):
+ checkParamLen(len(params), 4, 'NLN')
+ self.contactStatusChanged(params[0], params[1], unquote(params[2]))
+
+ def handle_FLN(self, params):
+ checkParamLen(len(params), 1, 'FLN')
+ self.contactOffline(params[0])
+
+ def handle_LST(self, params):
+ # support no longer exists for manually
+ # requesting lists - why do I feel cleaner now?
+ if self._getState() != 'SYNC':
+ return
+ contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
+ lists=int(params[2]))
+ if contact.lists & FORWARD_LIST:
+ contact.groups.extend(map(int, params[3].split(',')))
+ self._getStateData('list').addContact(contact)
+ self._setStateData('last_contact', contact)
+ sofar = self._getStateData('lst_sofar') + 1
+ if sofar == self._getStateData('lst_reply'):
+ # this is the best place to determine that
+ # a syn realy has finished - msn _may_ send
+ # BPR information for the last contact
+ # which is unfortunate because it means
+ # that the real end of a syn is non-deterministic.
+ # to handle this we'll keep 'last_contact' hanging
+ # around in the state data and update it if we need
+ # to later.
+ self._setState('SESSION')
+ contacts = self._getStateData('list')
+ phone = self._getStateData('phone')
+ id = self._getStateData('synid')
+ self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
+ self._fireCallback(id, contacts, phone)
+ else:
+ self._setStateData('lst_sofar',sofar)
+
+ def handle_BLP(self, params):
+ # check to see if this is in response to a SYN
+ if self._getState() == 'SYNC':
+ self._getStateData('list').privacy = listCodeToID[params[0].lower()]
+ else:
+ id = int(params[0])
+ self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
+
+ def handle_GTC(self, params):
+ # check to see if this is in response to a SYN
+ if self._getState() == 'SYNC':
+ if params[0].lower() == "a":
+ self._getStateData('list').autoAdd = 0
+ elif params[0].lower() == "n":
+ self._getStateData('list').autoAdd = 1
+ else:
+ raise MSNProtocolError, "Invalid Paramater for GTC" # debug
+ else:
+ id = int(params[0])
+ if params[1].lower() == "a":
+ self._fireCallback(id, 0)
+ elif params[1].lower() == "n":
+ self._fireCallback(id, 1)
+ else:
+ raise MSNProtocolError, "Invalid Paramater for GTC" # debug
+
+ def handle_SYN(self, params):
+ id = int(params[0])
+ if len(params) == 2:
+ self._setState('SESSION')
+ self._fireCallback(id, None, None)
+ else:
+ contacts = MSNContactList()
+ contacts.version = int(params[1])
+ self._setStateData('list', contacts)
+ self._setStateData('lst_reply', int(params[2]))
+ self._setStateData('lsg_reply', int(params[3]))
+ self._setStateData('lst_sofar', 0)
+ self._setStateData('phone', [])
+
+ def handle_LSG(self, params):
+ if self._getState() == 'SYNC':
+ self._getStateData('list').groups[int(params[0])] = unquote(params[1])
+
+ # Please see the comment above the requestListGroups / requestList methods
+ # regarding support for this
+ #
+ #else:
+ # self._getStateData('groups').append((int(params[4]), unquote(params[5])))
+ # if params[3] == params[4]: # this was the last group
+ # self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
+ # self._remStateData('groups')
+
+ def handle_PRP(self, params):
+ if self._getState() == 'SYNC':
+ self._getStateData('phone').append((params[0], unquote(params[1])))
+ else:
+ self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
+
+ def handle_BPR(self, params):
+ numParams = len(params)
+ if numParams == 2: # part of a syn
+ self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
+ elif numParams == 4:
+ self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
+
+ def handle_ADG(self, params):
+ checkParamLen(len(params), 5, 'ADG')
+ id = int(params[0])
+ if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
+ raise MSNProtocolError, "ADG response does not match up to a request" # debug
+
+ def handle_RMG(self, params):
+ checkParamLen(len(params), 3, 'RMG')
+ id = int(params[0])
+ if not self._fireCallback(id, int(params[1]), int(params[2])):
+ raise MSNProtocolError, "RMG response does not match up to a request" # debug
+
+ def handle_REG(self, params):
+ checkParamLen(len(params), 5, 'REG')
+ id = int(params[0])
+ if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
+ raise MSNProtocolError, "REG response does not match up to a request" # debug
+
+ def handle_ADD(self, params):
+ numParams = len(params)
+ if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
+ raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
+ id = int(params[0])
+ listType = params[1].lower()
+ listVer = int(params[2])
+ userHandle = params[3]
+ groupID = None
+ if numParams == 6: # they sent a group id
+ if params[1].upper() != "FL":
+ raise MSNProtocolError, "Only forward list can contain groups" # debug
+ groupID = int(params[5])
+ if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
+ self.userAddedMe(userHandle, unquote(params[4]), listVer)
+
+ def handle_REM(self, params):
+ numParams = len(params)
+ if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
+ raise MSNProtocolError, "Invalid Paramaters for REM" # debug
+ id = int(params[0])
+ listType = params[1].lower()
+ listVer = int(params[2])
+ userHandle = params[3]
+ groupID = None
+ if numParams == 5:
+ if params[1] != "FL":
+ raise MSNProtocolError, "Only forward list can contain groups" # debug
+ groupID = int(params[4])
+ if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
+ if listType.upper() == "RL":
+ self.userRemovedMe(userHandle, listVer)
+
+ def handle_REA(self, params):
+ checkParamLen(len(params), 4, 'REA')
+ id = int(params[0])
+ self._fireCallback(id, int(params[1]), unquote(params[3]))
+
+ def handle_XFR(self, params):
+ checkParamLen(len(params), 5, 'XFR')
+ id = int(params[0])
+ # check to see if they sent a host/port pair
+ try:
+ host, port = params[2].split(':')
+ except ValueError:
+ host = params[2]
+ port = MSN_PORT
+
+ if not self._fireCallback(id, host, int(port), params[4]):
+ raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
+
+ def handle_RNG(self, params):
+ checkParamLen(len(params), 6, 'RNG')
+ # check for host:port pair
+ try:
+ host, port = params[1].split(":")
+ port = int(port)
+ except ValueError:
+ host = params[1]
+ port = MSN_PORT
+ self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
+ unquote(params[5]))
+
+ def handle_OUT(self, params):
+ checkParamLen(len(params), 1, 'OUT')
+ if params[0] == "OTH":
+ self.multipleLogin()
+ elif params[0] == "SSD":
+ self.serverGoingDown()
+ else:
+ raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
+
+ # callbacks
+
+ def loggedIn(self, userHandle, screenName, verified):
+ """
+ Called when the client has logged in.
+ The default behaviour of this method is to
+ update the factory with our screenName and
+ to sync the contact list (factory.contacts).
+ When this is complete self.listSynchronized
+ will be called.
+
+ @param userHandle: our userHandle
+ @param screenName: our screenName
+ @param verified: 1 if our passport has been (verified), 0 if not.
+ (i'm not sure of the significace of this)
+ @type verified: int
+ """
+ self.factory.screenName = screenName
+ if not self.factory.contacts:
+ listVersion = 0
+ else:
+ listVersion = self.factory.contacts.version
+ self.syncList(listVersion).addCallback(self.listSynchronized)
+
+
+ def loginFailure(self, message):
+ """
+ Called when the client fails to login.
+
+ @param message: a message indicating the problem that was encountered
+ """
+
+
+ def gotProfile(self, message):
+ """
+ Called after logging in when the server sends an initial
+ message with MSN/passport specific profile information
+ such as country, number of kids, etc.
+ Check the message headers for the specific values.
+
+ @param message: The profile message
+ """
+ pass
+
+ def listSynchronized(self, *args):
+ """
+ Lists are now synchronized by default upon logging in, this
+ method is called after the synchronization has finished
+ and the factory now has the up-to-date contacts.
+ """
+ pass
+
+ def statusChanged(self, statusCode):
+ """
+ Called when our status changes and it isn't in response to
+ a client command. By default we will update the status
+ attribute of the factory.
+
+ @param statusCode: 3-letter status code
+ """
+ self.factory.status = statusCode
+
+ def gotContactStatus(self, statusCode, userHandle, screenName):
+ """
+ Called after loggin in when the server sends status of online contacts.
+ By default we will update the status attribute of the contact stored
+ on the factory.
+
+ @param statusCode: 3-letter status code
+ @param userHandle: the contact's user handle (passport)
+ @param screenName: the contact's screen name
+ """
+ self.factory.contacts.getContact(userHandle).status = statusCode
+
+ def contactStatusChanged(self, statusCode, userHandle, screenName):
+ """
+ Called when we're notified that a contact's status has changed.
+ By default we will update the status attribute of the contact
+ stored on the factory.
+
+ @param statusCode: 3-letter status code
+ @param userHandle: the contact's user handle (passport)
+ @param screenName: the contact's screen name
+ """
+ self.factory.contacts.getContact(userHandle).status = statusCode
+
+ def contactOffline(self, userHandle):
+ """
+ Called when a contact goes offline. By default this method
+ will update the status attribute of the contact stored
+ on the factory.
+
+ @param userHandle: the contact's user handle
+ """
+ self.factory.contacts.getContact(userHandle).status = STATUS_OFFLINE
+
+ def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
+ """
+ Called when the server sends us phone details about
+ a specific user (for example after a user is added
+ the server will send their status, phone details etc.
+ By default we will update the list version for the
+ factory's contact list and update the phone details
+ for the specific user.
+
+ @param listVersion: the new list version
+ @param userHandle: the contact's user handle (passport)
+ @param phoneType: the specific phoneType
+ (*_PHONE constants or HAS_PAGER)
+ @param number: the value/phone number.
+ """
+ self.factory.contacts.version = listVersion
+ self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
+
+ def userAddedMe(self, userHandle, screenName, listVersion):
+ """
+ Called when a user adds me to their list. (ie. they have been added to
+ the reverse list. By default this method will update the version of
+ the factory's contact list -- that is, if the contact already exists
+ it will update the associated lists attribute, otherwise it will create
+ a new MSNContact object and store it.
+
+ @param userHandle: the userHandle of the user
+ @param screenName: the screen name of the user
+ @param listVersion: the new list version
+ @type listVersion: int
+ """
+ self.factory.contacts.version = listVersion
+ c = self.factory.contacts.getContact(userHandle)
+ if not c:
+ c = MSNContact(userHandle=userHandle, screenName=screenName)
+ self.factory.contacts.addContact(c)
+ c.addToList(REVERSE_LIST)
+
+ def userRemovedMe(self, userHandle, listVersion):
+ """
+ Called when a user removes us from their contact list
+ (they are no longer on our reverseContacts list.
+ By default this method will update the version of
+ the factory's contact list -- that is, the user will
+ be removed from the reverse list and if they are no longer
+ part of any lists they will be removed from the contact
+ list entirely.
+
+ @param userHandle: the contact's user handle (passport)
+ @param listVersion: the new list version
+ """
+ self.factory.contacts.version = listVersion
+ c = self.factory.contacts.getContact(userHandle)
+ c.removeFromList(REVERSE_LIST)
+ if c.lists == 0:
+ self.factory.contacts.remContact(c.userHandle)
+
+ def gotSwitchboardInvitation(self, sessionID, host, port,
+ key, userHandle, screenName):
+ """
+ Called when we get an invitation to a switchboard server.
+ This happens when a user requests a chat session with us.
+
+ @param sessionID: session ID number, must be remembered for logging in
+ @param host: the hostname of the switchboard server
+ @param port: the port to connect to
+ @param key: used for authorization when connecting
+ @param userHandle: the user handle of the person who invited us
+ @param screenName: the screen name of the person who invited us
+ """
+ pass
+
+ def multipleLogin(self):
+ """
+ Called when the server says there has been another login
+ under our account, the server should disconnect us right away.
+ """
+ pass
+
+ def serverGoingDown(self):
+ """
+ Called when the server has notified us that it is going down for
+ maintenance.
+ """
+ pass
+
+ # api calls
+
+ def changeStatus(self, status):
+ """
+ Change my current status. This method will add
+ a default callback to the returned Deferred
+ which will update the status attribute of the
+ factory.
+
+ @param status: 3-letter status code (as defined by
+ the STATUS_* constants)
+ @return: A Deferred, the callback of which will be
+ fired when the server confirms the change
+ of status. The callback argument will be
+ a tuple with the new status code as the
+ only element.
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("CHG %s %s" % (id, status))
+ def _cb(r):
+ self.factory.status = r[0]
+ return r
+ return d.addCallback(_cb)
+
+ # I am no longer supporting the process of manually requesting
+ # lists or list groups -- as far as I can see this has no use
+ # if lists are synchronized and updated correctly, which they
+ # should be. If someone has a specific justified need for this
+ # then please contact me and i'll re-enable/fix support for it.
+
+ #def requestList(self, listType):
+ # """
+ # request the desired list type
+ #
+ # @param listType: (as defined by the *_LIST constants)
+ # @return: A Deferred, the callback of which will be
+ # fired when the list has been retrieved.
+ # The callback argument will be a tuple with
+ # the only element being a list of MSNContact
+ # objects.
+ # """
+ # # this doesn't need to ever be used if syncing of the lists takes place
+ # # i.e. please don't use it!
+ # warnings.warn("Please do not use this method - use the list syncing process instead")
+ # id, d = self._createIDMapping()
+ # self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
+ # self._setStateData('list',[])
+ # return d
+
+ def setPrivacyMode(self, privLevel):
+ """
+ Set my privacy mode on the server.
+
+ B{Note}:
+ This only keeps the current privacy setting on
+ the server for later retrieval, it does not
+ effect the way the server works at all.
+
+ @param privLevel: This parameter can be true, in which
+ case the server will keep the state as
+ 'al' which the official client interprets
+ as -> allow messages from only users on
+ the allow list. Alternatively it can be
+ false, in which case the server will keep
+ the state as 'bl' which the official client
+ interprets as -> allow messages from all
+ users except those on the block list.
+
+ @return: A Deferred, the callback of which will be fired when
+ the server replies with the new privacy setting.
+ The callback argument will be a tuple, the 2 elements
+ of which being the list version and either 'al'
+ or 'bl' (the new privacy setting).
+ """
+
+ id, d = self._createIDMapping()
+ if privLevel:
+ self.sendLine("BLP %s AL" % id)
+ else:
+ self.sendLine("BLP %s BL" % id)
+ return d
+
+ def syncList(self, version):
+ """
+ Used for keeping an up-to-date contact list.
+ A callback is added to the returned Deferred
+ that updates the contact list on the factory
+ and also sets my state to STATUS_ONLINE.
+
+ B{Note}:
+ This is called automatically upon signing
+ in using the version attribute of
+ factory.contacts, so you may want to persist
+ this object accordingly. Because of this there
+ is no real need to ever call this method
+ directly.
+
+ @param version: The current known list version
+
+ @return: A Deferred, the callback of which will be
+ fired when the server sends an adequate reply.
+ The callback argument will be a tuple with two
+ elements, the new list (MSNContactList) and
+ your current state (a dictionary). If the version
+ you sent _was_ the latest list version, both elements
+ will be None. To just request the list send a version of 0.
+ """
+
+ self._setState('SYNC')
+ id, d = self._createIDMapping(data=str(version))
+ self._setStateData('synid',id)
+ self.sendLine("SYN %s %s" % (id, version))
+ def _cb(r):
+ self.changeStatus(STATUS_ONLINE)
+ if r[0] is not None:
+ self.factory.contacts = r[0]
+ return r
+ return d.addCallback(_cb)
+
+
+ # I am no longer supporting the process of manually requesting
+ # lists or list groups -- as far as I can see this has no use
+ # if lists are synchronized and updated correctly, which they
+ # should be. If someone has a specific justified need for this
+ # then please contact me and i'll re-enable/fix support for it.
+
+ #def requestListGroups(self):
+ # """
+ # Request (forward) list groups.
+ #
+ # @return: A Deferred, the callback for which will be called
+ # when the server responds with the list groups.
+ # The callback argument will be a tuple with two elements,
+ # a dictionary mapping group IDs to group names and the
+ # current list version.
+ # """
+ #
+ # # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
+ # # i.e. please don't use it!
+ # warnings.warn("Please do not use this method - use the list syncing process instead")
+ # id, d = self._createIDMapping()
+ # self.sendLine("LSG %s" % id)
+ # self._setStateData('groups',{})
+ # return d
+
+ def setPhoneDetails(self, phoneType, value):
+ """
+ Set/change my phone numbers stored on the server.
+
+ @param phoneType: phoneType can be one of the following
+ constants - HOME_PHONE, WORK_PHONE,
+ MOBILE_PHONE, HAS_PAGER.
+ These are pretty self-explanatory, except
+ maybe HAS_PAGER which refers to whether or
+ not you have a pager.
+ @param value: for all of the *_PHONE constants the value is a
+ phone number (str), for HAS_PAGER accepted values
+ are 'Y' (for yes) and 'N' (for no).
+
+ @return: A Deferred, the callback for which will be fired when
+ the server confirms the change has been made. The
+ callback argument will be a tuple with 2 elements, the
+ first being the new list version (int) and the second
+ being the new phone number value (str).
+ """
+ # XXX: Add a default callback which updates
+ # factory.contacts.version and the relevant phone
+ # number
+ id, d = self._createIDMapping()
+ self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
+ return d
+
+ def addListGroup(self, name):
+ """
+ Used to create a new list group.
+ A default callback is added to the
+ returned Deferred which updates the
+ contacts attribute of the factory.
+
+ @param name: The desired name of the new group.
+
+ @return: A Deferred, the callbacck for which will be called
+ when the server clarifies that the new group has been
+ created. The callback argument will be a tuple with 3
+ elements: the new list version (int), the new group name
+ (str) and the new group ID (int).
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("ADG %s %s 0" % (id, quote(name)))
+ def _cb(r):
+ self.factory.contacts.version = r[0]
+ self.factory.contacts.setGroup(r[1], r[2])
+ return r
+ return d.addCallback(_cb)
+
+ def remListGroup(self, groupID):
+ """
+ Used to remove a list group.
+ A default callback is added to the
+ returned Deferred which updates the
+ contacts attribute of the factory.
+
+ @param groupID: the ID of the desired group to be removed.
+
+ @return: A Deferred, the callback for which will be called when
+ the server clarifies the deletion of the group.
+ The callback argument will be a tuple with 2 elements:
+ the new list version (int) and the group ID (int) of
+ the removed group.
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("RMG %s %s" % (id, groupID))
+ def _cb(r):
+ self.factory.contacts.version = r[0]
+ self.factory.contacts.remGroup(r[1])
+ return r
+ return d.addCallback(_cb)
+
+ def renameListGroup(self, groupID, newName):
+ """
+ Used to rename an existing list group.
+ A default callback is added to the returned
+ Deferred which updates the contacts attribute
+ of the factory.
+
+ @param groupID: the ID of the desired group to rename.
+ @param newName: the desired new name for the group.
+
+ @return: A Deferred, the callback for which will be called
+ when the server clarifies the renaming.
+ The callback argument will be a tuple of 3 elements,
+ the new list version (int), the group id (int) and
+ the new group name (str).
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
+ def _cb(r):
+ self.factory.contacts.version = r[0]
+ self.factory.contacts.setGroup(r[1], r[2])
+ return r
+ return d.addCallback(_cb)
+
+ def addContact(self, listType, userHandle, groupID=0):
+ """
+ Used to add a contact to the desired list.
+ A default callback is added to the returned
+ Deferred which updates the contacts attribute of
+ the factory with the new contact information.
+ If you are adding a contact to the forward list
+ and you want to associate this contact with multiple
+ groups then you will need to call this method for each
+ group you would like to add them to, changing the groupID
+ parameter. The default callback will take care of updating
+ the group information on the factory's contact list.
+
+ @param listType: (as defined by the *_LIST constants)
+ @param userHandle: the user handle (passport) of the contact
+ that is being added
+ @param groupID: the group ID for which to associate this contact
+ with. (default 0 - default group). Groups are only
+ valid for FORWARD_LIST.
+
+ @return: A Deferred, the callback for which will be called when
+ the server has clarified that the user has been added.
+ The callback argument will be a tuple with 4 elements:
+ the list type, the contact's user handle, the new list
+ version, and the group id (if relevant, otherwise it
+ will be None)
+ """
+
+ id, d = self._createIDMapping()
+ listType = listIDToCode[listType].upper()
+ if listType == "FL":
+ self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
+ else:
+ self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
+
+ def _cb(r):
+ self.factory.contacts.version = r[2]
+ c = self.factory.contacts.getContact(r[1])
+ if not c:
+ c = MSNContact(userHandle=r[1])
+ if r[3]:
+ c.groups.append(r[3])
+ c.addToList(r[0])
+ return r
+ return d.addCallback(_cb)
+
+ def remContact(self, listType, userHandle, groupID=0):
+ """
+ Used to remove a contact from the desired list.
+ A default callback is added to the returned deferred
+ which updates the contacts attribute of the factory
+ to reflect the new contact information. If you are
+ removing from the forward list then you will need to
+ supply a groupID, if the contact is in more than one
+ group then they will only be removed from this group
+ and not the entire forward list, but if this is their
+ only group they will be removed from the whole list.
+
+ @param listType: (as defined by the *_LIST constants)
+ @param userHandle: the user handle (passport) of the
+ contact being removed
+ @param groupID: the ID of the group to which this contact
+ belongs (only relevant for FORWARD_LIST,
+ default is 0)
+
+ @return: A Deferred, the callback for which will be called when
+ the server has clarified that the user has been removed.
+ The callback argument will be a tuple of 4 elements:
+ the list type, the contact's user handle, the new list
+ version, and the group id (if relevant, otherwise it will
+ be None)
+ """
+
+ id, d = self._createIDMapping()
+ listType = listIDToCode[listType].upper()
+ if listType == "FL":
+ self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
+ else:
+ self.sendLine("REM %s %s %s" % (id, listType, userHandle))
+
+ def _cb(r):
+ l = self.factory.contacts
+ l.version = r[2]
+ c = l.getContact(r[1])
+ group = r[3]
+ shouldRemove = 1
+ if group: # they may not have been removed from the list
+ c.groups.remove(group)
+ if c.groups:
+ shouldRemove = 0
+ if shouldRemove:
+ c.removeFromList(r[0])
+ if c.lists == 0:
+ l.remContact(c.userHandle)
+ return r
+ return d.addCallback(_cb)
+
+ def changeScreenName(self, newName):
+ """
+ Used to change your current screen name.
+ A default callback is added to the returned
+ Deferred which updates the screenName attribute
+ of the factory and also updates the contact list
+ version.
+
+ @param newName: the new screen name
+
+ @return: A Deferred, the callback for which will be called
+ when the server sends an adequate reply.
+ The callback argument will be a tuple of 2 elements:
+ the new list version and the new screen name.
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
+ def _cb(r):
+ self.factory.contacts.version = r[0]
+ self.factory.screenName = r[1]
+ return r
+ return d.addCallback(_cb)
+
+ def requestSwitchboardServer(self):
+ """
+ Used to request a switchboard server to use for conversations.
+
+ @return: A Deferred, the callback for which will be called when
+ the server responds with the switchboard information.
+ The callback argument will be a tuple with 3 elements:
+ the host of the switchboard server, the port and a key
+ used for logging in.
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("XFR %s SB" % id)
+ return d
+
+ def logOut(self):
+ """
+ Used to log out of the notification server.
+ After running the method the server is expected
+ to close the connection.
+ """
+
+ self.sendLine("OUT")
+
+class NotificationFactory(ClientFactory):
+ """
+ Factory for the NotificationClient protocol.
+ This is basically responsible for keeping
+ the state of the client and thus should be used
+ in a 1:1 situation with clients.
+
+ @ivar contacts: An MSNContactList instance reflecting
+ the current contact list -- this is
+ generally kept up to date by the default
+ command handlers.
+ @ivar userHandle: The client's userHandle, this is expected
+ to be set by the client and is used by the
+ protocol (for logging in etc).
+ @ivar screenName: The client's current screen-name -- this is
+ generally kept up to date by the default
+ command handlers.
+ @ivar password: The client's password -- this is (obviously)
+ expected to be set by the client.
+ @ivar passportServer: This must point to an msn passport server
+ (the whole URL is required)
+ @ivar status: The status of the client -- this is generally kept
+ up to date by the default command handlers
+ """
+
+ contacts = None
+ userHandle = ''
+ screenName = ''
+ password = ''
+ passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
+ status = 'FLN'
+ protocol = NotificationClient
+
+
+# XXX: A lot of the state currently kept in
+# instances of SwitchboardClient is likely to
+# be moved into a factory at some stage in the
+# future
+
+class SwitchboardClient(MSNEventBase):
+ """
+ This class provides support for clients connecting to a switchboard server.
+
+ Switchboard servers are used for conversations with other people
+ on the MSN network. This means that the number of conversations at
+ any given time will be directly proportional to the number of
+ connections to varioius switchboard servers.
+
+ MSN makes no distinction between single and group conversations,
+ so any number of users may be invited to join a specific conversation
+ taking place on a switchboard server.
+
+ @ivar key: authorization key, obtained when receiving
+ invitation / requesting switchboard server.
+ @ivar userHandle: your user handle (passport)
+ @ivar sessionID: unique session ID, used if you are replying
+ to a switchboard invitation
+ @ivar reply: set this to 1 in connectionMade or before to signifiy
+ that you are replying to a switchboard invitation.
+ """
+
+ key = 0
+ userHandle = ""
+ sessionID = ""
+ reply = 0
+
+ _iCookie = 0
+
+ def __init__(self):
+ MSNEventBase.__init__(self)
+ self.pendingUsers = {}
+ self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
+
+ def connectionMade(self):
+ MSNEventBase.connectionMade(self)
+ print 'sending initial stuff'
+ self._sendInit()
+
+ def connectionLost(self, reason):
+ self.cookies['iCookies'] = {}
+ self.cookies['external'] = {}
+ MSNEventBase.connectionLost(self, reason)
+
+ def _sendInit(self):
+ """
+ send initial data based on whether we are replying to an invitation
+ or starting one.
+ """
+ id = self._nextTransactionID()
+ if not self.reply:
+ self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
+ else:
+ self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
+
+ def _newInvitationCookie(self):
+ self._iCookie += 1
+ if self._iCookie > 1000:
+ self._iCookie = 1
+ return self._iCookie
+
+ def _checkTyping(self, message, cTypes):
+ """ helper method for checkMessage """
+ if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
+ self.userTyping(message)
+ return 1
+
+ def _checkFileInvitation(self, message, info):
+ """ helper method for checkMessage """
+ guid = info.get('Application-GUID', '').lower()
+ name = info.get('Application-Name', '').lower()
+
+ # Both fields are required, but we'll let some lazy clients get away
+ # with only sending a name, if it is easy for us to recognize the
+ # name (the name is localized, so this check might fail for lazy,
+ # non-english clients, but I'm not about to include "file transfer"
+ # in 80 different languages here).
+
+ if name != "file transfer" and guid != classNameToGUID["file transfer"]:
+ return 0
+ try:
+ cookie = int(info['Invitation-Cookie'])
+ fileName = info['Application-File']
+ fileSize = int(info['Application-FileSize'])
+ except KeyError:
+ log.msg('Received munged file transfer request ... ignoring.')
+ return 0
+ self.gotSendRequest(fileName, fileSize, cookie, message)
+ return 1
+
+ def _checkFileResponse(self, message, info):
+ """ helper method for checkMessage """
+ try:
+ cmd = info['Invitation-Command'].upper()
+ cookie = int(info['Invitation-Cookie'])
+ except KeyError:
+ return 0
+ accept = (cmd == 'ACCEPT') and 1 or 0
+ requested = self.cookies['iCookies'].get(cookie)
+ if not requested:
+ return 1
+ requested[0].callback((accept, cookie, info))
+ del self.cookies['iCookies'][cookie]
+ return 1
+
+ def _checkFileInfo(self, message, info):
+ """ helper method for checkMessage """
+ try:
+ ip = info['IP-Address']
+ iCookie = int(info['Invitation-Cookie'])
+ aCookie = int(info['AuthCookie'])
+ cmd = info['Invitation-Command'].upper()
+ port = int(info['Port'])
+ except KeyError:
+ return 0
+ accept = (cmd == 'ACCEPT') and 1 or 0
+ requested = self.cookies['external'].get(iCookie)
+ if not requested:
+ return 1 # we didn't ask for this
+ requested[0].callback((accept, ip, port, aCookie, info))
+ del self.cookies['external'][iCookie]
+ return 1
+
+ def checkMessage(self, message):
+ """
+ hook for detecting any notification type messages
+ (e.g. file transfer)
+ """
+ cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
+ if self._checkTyping(message, cTypes):
+ return 0
+ if 'text/x-msmsgsinvite' in cTypes:
+ # header like info is sent as part of the message body.
+ info = {}
+ for line in message.message.split('\r\n'):
+ try:
+ key, val = line.split(':')
+ info[key] = val.lstrip()
+ except ValueError:
+ continue
+ if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info):
+ return 0
+ elif 'text/x-clientcaps' in cTypes:
+ # do something with capabilities
+ return 0
+ return 1
+
+ # negotiation
+ def handle_USR(self, params):
+ checkParamLen(len(params), 4, 'USR')
+ if params[1] == "OK":
+ self.loggedIn()
+
+ # invite a user
+ def handle_CAL(self, params):
+ checkParamLen(len(params), 3, 'CAL')
+ id = int(params[0])
+ if params[1].upper() == "RINGING":
+ self._fireCallback(id, int(params[2])) # session ID as parameter
+
+ # user joined
+ def handle_JOI(self, params):
+ checkParamLen(len(params), 2, 'JOI')
+ self.userJoined(params[0], unquote(params[1]))
+
+ # users participating in the current chat
+ def handle_IRO(self, params):
+ checkParamLen(len(params), 5, 'IRO')
+ self.pendingUsers[params[3]] = unquote(params[4])
+ if params[1] == params[2]:
+ self.gotChattingUsers(self.pendingUsers)
+ self.pendingUsers = {}
+
+ # finished listing users
+ def handle_ANS(self, params):
+ checkParamLen(len(params), 2, 'ANS')
+ if params[1] == "OK":
+ self.loggedIn()
+
+ def handle_ACK(self, params):
+ checkParamLen(len(params), 1, 'ACK')
+ self._fireCallback(int(params[0]), None)
+
+ def handle_NAK(self, params):
+ checkParamLen(len(params), 1, 'NAK')
+ self._fireCallback(int(params[0]), None)
+
+ def handle_BYE(self, params):
+ #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
+ self.userLeft(params[0])
+
+ # callbacks
+
+ def loggedIn(self):
+ """
+ called when all login details have been negotiated.
+ Messages can now be sent, or new users invited.
+ """
+ pass
+
+ def gotChattingUsers(self, users):
+ """
+ called after connecting to an existing chat session.
+
+ @param users: A dict mapping user handles to screen names
+ (current users taking part in the conversation)
+ """
+ pass
+
+ def userJoined(self, userHandle, screenName):
+ """
+ called when a user has joined the conversation.
+
+ @param userHandle: the user handle (passport) of the user
+ @param screenName: the screen name of the user
+ """
+ pass
+
+ def userLeft(self, userHandle):
+ """
+ called when a user has left the conversation.
+
+ @param userHandle: the user handle (passport) of the user.
+ """
+ pass
+
+ def gotMessage(self, message):
+ """
+ called when we receive a message.
+
+ @param message: the associated MSNMessage object
+ """
+ pass
+
+ def userTyping(self, message):
+ """
+ called when we receive the special type of message notifying
+ us that a user is typing a message.
+
+ @param message: the associated MSNMessage object
+ """
+ pass
+
+ def gotSendRequest(self, fileName, fileSize, iCookie, message):
+ """
+ called when a contact is trying to send us a file.
+ To accept or reject this transfer see the
+ fileInvitationReply method.
+
+ @param fileName: the name of the file
+ @param fileSize: the size of the file
+ @param iCookie: the invitation cookie, used so the client can
+ match up your reply with this request.
+ @param message: the MSNMessage object which brought about this
+ invitation (it may contain more information)
+ """
+ pass
+
+ # api calls
+
+ def inviteUser(self, userHandle):
+ """
+ used to invite a user to the current switchboard server.
+
+ @param userHandle: the user handle (passport) of the desired user.
+
+ @return: A Deferred, the callback for which will be called
+ when the server notifies us that the user has indeed
+ been invited. The callback argument will be a tuple
+ with 1 element, the sessionID given to the invited user.
+ I'm not sure if this is useful or not.
+ """
+
+ id, d = self._createIDMapping()
+ self.sendLine("CAL %s %s" % (id, userHandle))
+ return d
+
+ def sendMessage(self, message):
+ """
+ used to send a message.
+
+ @param message: the corresponding MSNMessage object.
+
+ @return: Depending on the value of message.ack.
+ If set to MSNMessage.MESSAGE_ACK or
+ MSNMessage.MESSAGE_NACK a Deferred will be returned,
+ the callback for which will be fired when an ACK or
+ NACK is received - the callback argument will be
+ (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
+ the return value is None.
+ """
+
+ if message.ack not in ('A','N'):
+ id, d = self._nextTransactionID(), None
+ else:
+ id, d = self._createIDMapping()
+ if message.length == 0:
+ message.length = message._calcMessageLen()
+ self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
+ # apparently order matters with at least MIME-Version and Content-Type
+ self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
+ self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
+ # send the rest of the headers
+ for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
+ self.sendLine("%s: %s" % (header[0], header[1]))
+ self.transport.write(CR+LF)
+ self.transport.write(message.message)
+ return d
+
+ def sendTypingNotification(self):
+ """
+ used to send a typing notification. Upon receiving this
+ message the official client will display a 'user is typing'
+ message to all other users in the chat session for 10 seconds.
+ The official client sends one of these every 5 seconds (I think)
+ as long as you continue to type.
+ """
+ m = MSNMessage()
+ m.ack = m.MESSAGE_ACK_NONE
+ m.setHeader('Content-Type', 'text/x-msmsgscontrol')
+ m.setHeader('TypingUser', self.userHandle)
+ m.message = "\r\n"
+ self.sendMessage(m)
+
+ def sendFileInvitation(self, fileName, fileSize):
+ """
+ send an notification that we want to send a file.
+
+ @param fileName: the file name
+ @param fileSize: the file size
+
+ @return: A Deferred, the callback of which will be fired
+ when the user responds to this invitation with an
+ appropriate message. The callback argument will be
+ a tuple with 3 elements, the first being 1 or 0
+ depending on whether they accepted the transfer
+ (1=yes, 0=no), the second being an invitation cookie
+ to identify your follow-up responses and the third being
+ the message 'info' which is a dict of information they
+ sent in their reply (this doesn't really need to be used).
+ If you wish to proceed with the transfer see the
+ sendTransferInfo method.
+ """
+ cookie = self._newInvitationCookie()
+ d = Deferred()
+ m = MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Application-Name: File Transfer\r\n'
+ m.message += 'Application-GUID: %s\r\n' % (classNameToGUID["file transfer"],)
+ m.message += 'Invitation-Command: INVITE\r\n'
+ m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
+ m.message += 'Application-File: %s\r\n' % fileName
+ m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
+ m.ack = m.MESSAGE_ACK_NONE
+ self.sendMessage(m)
+ self.cookies['iCookies'][cookie] = (d, m)
+ return d
+
+ def fileInvitationReply(self, iCookie, accept=1):
+ """
+ used to reply to a file transfer invitation.
+
+ @param iCookie: the invitation cookie of the initial invitation
+ @param accept: whether or not you accept this transfer,
+ 1 = yes, 0 = no, default = 1.
+
+ @return: A Deferred, the callback for which will be fired when
+ the user responds with the transfer information.
+ The callback argument will be a tuple with 5 elements,
+ whether or not they wish to proceed with the transfer
+ (1=yes, 0=no), their ip, the port, the authentication
+ cookie (see FileReceive/FileSend) and the message
+ info (dict) (in case they send extra header-like info
+ like Internal-IP, this doesn't necessarily need to be
+ used). If you wish to proceed with the transfer see
+ FileReceive.
+ """
+ d = Deferred()
+ m = MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
+ m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
+ if not accept:
+ m.message += 'Cancel-Code: REJECT\r\n'
+ m.message += 'Launch-Application: FALSE\r\n'
+ m.message += 'Request-Data: IP-Address:\r\n'
+ m.message += '\r\n'
+ m.ack = m.MESSAGE_ACK_NONE
+ self.sendMessage(m)
+ self.cookies['external'][iCookie] = (d, m)
+ return d
+
+ def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
+ """
+ send information relating to a file transfer session.
+
+ @param accept: whether or not to go ahead with the transfer
+ (1=yes, 0=no)
+ @param iCookie: the invitation cookie of previous replies
+ relating to this transfer
+ @param authCookie: the authentication cookie obtained from
+ an FileSend instance
+ @param ip: your ip
+ @param port: the port on which an FileSend protocol is listening.
+ """
+ m = MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
+ m.message += 'Invitation-Cookie: %s\r\n' % iCookie
+ m.message += 'IP-Address: %s\r\n' % ip
+ m.message += 'Port: %s\r\n' % port
+ m.message += 'AuthCookie: %s\r\n' % authCookie
+ m.message += '\r\n'
+ m.ack = m.MESSAGE_NACK
+ self.sendMessage(m)
+
+class FileReceive(LineReceiver):
+ """
+ This class provides support for receiving files from contacts.
+
+ @ivar fileSize: the size of the receiving file. (you will have to set this)
+ @ivar connected: true if a connection has been established.
+ @ivar completed: true if the transfer is complete.
+ @ivar bytesReceived: number of bytes (of the file) received.
+ This does not include header data.
+ """
+
+ def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
+ """
+ @param auth: auth string received in the file invitation.
+ @param myUserHandle: your userhandle.
+ @param file: A string or file object represnting the file
+ to save data to.
+ @param directory: optional parameter specifiying the directory.
+ Defaults to the current directory.
+ @param overwrite: if true and a file of the same name exists on
+ your system, it will be overwritten. (0 by default)
+ """
+ self.auth = auth
+ self.myUserHandle = myUserHandle
+ self.fileSize = 0
+ self.connected = 0
+ self.completed = 0
+ self.directory = directory
+ self.bytesReceived = 0
+ self.overwrite = overwrite
+
+ # used for handling current received state
+ self.state = 'CONNECTING'
+ self.segmentLength = 0
+ self.buffer = ''
+
+ if isinstance(file, types.StringType):
+ path = os.path.join(directory, file)
+ if os.path.exists(path) and not self.overwrite:
+ log.msg('File already exists...')
+ raise IOError, "File Exists" # is this all we should do here?
+ self.file = open(os.path.join(directory, file), 'wb')
+ else:
+ self.file = file
+
+ def connectionMade(self):
+ self.connected = 1
+ self.state = 'INHEADER'
+ self.sendLine('VER MSNFTP')
+
+ def connectionLost(self, reason):
+ self.connected = 0
+ self.file.close()
+
+ def parseHeader(self, header):
+ """ parse the header of each 'message' to obtain the segment length """
+
+ if ord(header[0]) != 0: # they requested that we close the connection
+ self.transport.loseConnection()
+ return
+ try:
+ extra, factor = header[1:]
+ except ValueError:
+ # munged header, ending transfer
+ self.transport.loseConnection()
+ raise
+ extra = ord(extra)
+ factor = ord(factor)
+ return factor * 256 + extra
+
+ def lineReceived(self, line):
+ temp = line.split()
+ if len(temp) == 1:
+ params = []
+ else:
+ params = temp[1:]
+ cmd = temp[0]
+ handler = getattr(self, "handle_%s" % cmd.upper(), None)
+ if handler:
+ handler(params) # try/except
+ else:
+ self.handle_UNKNOWN(cmd, params)
+
+ def rawDataReceived(self, data):
+ bufferLen = len(self.buffer)
+ if self.state == 'INHEADER':
+ delim = 3-bufferLen
+ self.buffer += data[:delim]
+ if len(self.buffer) == 3:
+ self.segmentLength = self.parseHeader(self.buffer)
+ if not self.segmentLength:
+ return # hrm
+ self.buffer = ""
+ self.state = 'INSEGMENT'
+ extra = data[delim:]
+ if len(extra) > 0:
+ self.rawDataReceived(extra)
+ return
+
+ elif self.state == 'INSEGMENT':
+ dataSeg = data[:(self.segmentLength-bufferLen)]
+ self.buffer += dataSeg
+ self.bytesReceived += len(dataSeg)
+ if len(self.buffer) == self.segmentLength:
+ self.gotSegment(self.buffer)
+ self.buffer = ""
+ if self.bytesReceived == self.fileSize:
+ self.completed = 1
+ self.buffer = ""
+ self.file.close()
+ self.sendLine("BYE 16777989")
+ return
+ self.state = 'INHEADER'
+ extra = data[(self.segmentLength-bufferLen):]
+ if len(extra) > 0:
+ self.rawDataReceived(extra)
+ return
+
+ def handle_VER(self, params):
+ checkParamLen(len(params), 1, 'VER')
+ if params[0].upper() == "MSNFTP":
+ self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
+ else:
+ log.msg('they sent the wrong version, time to quit this transfer')
+ self.transport.loseConnection()
+
+ def handle_FIL(self, params):
+ checkParamLen(len(params), 1, 'FIL')
+ try:
+ self.fileSize = int(params[0])
+ except ValueError: # they sent the wrong file size - probably want to log this
+ self.transport.loseConnection()
+ return
+ self.setRawMode()
+ self.sendLine("TFR")
+
+ def handle_UNKNOWN(self, cmd, params):
+ log.msg('received unknown command (%s), params: %s' % (cmd, params))
+
+ def gotSegment(self, data):
+ """ called when a segment (block) of data arrives. """
+ self.file.write(data)
+
+class FileSend(LineReceiver):
+ """
+ This class provides support for sending files to other contacts.
+
+ @ivar bytesSent: the number of bytes that have currently been sent.
+ @ivar completed: true if the send has completed.
+ @ivar connected: true if a connection has been established.
+ @ivar targetUser: the target user (contact).
+ @ivar segmentSize: the segment (block) size.
+ @ivar auth: the auth cookie (number) to use when sending the
+ transfer invitation
+ """
+
+ def __init__(self, file):
+ """
+ @param file: A string or file object represnting the file to send.
+ """
+
+ if isinstance(file, types.StringType):
+ self.file = open(file, 'rb')
+ else:
+ self.file = file
+
+ self.fileSize = 0
+ self.bytesSent = 0
+ self.completed = 0
+ self.connected = 0
+ self.targetUser = None
+ self.segmentSize = 2045
+ self.auth = randint(0, 2**30)
+ self._pendingSend = None # :(
+
+ def connectionMade(self):
+ self.connected = 1
+
+ def connectionLost(self, reason):
+ if self._pendingSend.active():
+ self._pendingSend.cancel()
+ self._pendingSend = None
+ if self.bytesSent == self.fileSize:
+ self.completed = 1
+ self.connected = 0
+ self.file.close()
+
+ def lineReceived(self, line):
+ temp = line.split()
+ if len(temp) == 1:
+ params = []
+ else:
+ params = temp[1:]
+ cmd = temp[0]
+ handler = getattr(self, "handle_%s" % cmd.upper(), None)
+ if handler:
+ handler(params)
+ else:
+ self.handle_UNKNOWN(cmd, params)
+
+ def handle_VER(self, params):
+ checkParamLen(len(params), 1, 'VER')
+ if params[0].upper() == "MSNFTP":
+ self.sendLine("VER MSNFTP")
+ else: # they sent some weird version during negotiation, i'm quitting.
+ self.transport.loseConnection()
+
+ def handle_USR(self, params):
+ checkParamLen(len(params), 2, 'USR')
+ self.targetUser = params[0]
+ if self.auth == int(params[1]):
+ self.sendLine("FIL %s" % (self.fileSize))
+ else: # they failed the auth test, disconnecting.
+ self.transport.loseConnection()
+
+ def handle_TFR(self, params):
+ checkParamLen(len(params), 0, 'TFR')
+ # they are ready for me to start sending
+ self.sendPart()
+
+ def handle_BYE(self, params):
+ self.completed = (self.bytesSent == self.fileSize)
+ self.transport.loseConnection()
+
+ def handle_CCL(self, params):
+ self.completed = (self.bytesSent == self.fileSize)
+ self.transport.loseConnection()
+
+ def handle_UNKNOWN(self, cmd, params):
+ log.msg('received unknown command (%s), params: %s' % (cmd, params))
+
+ def makeHeader(self, size):
+ """ make the appropriate header given a specific segment size. """
+ quotient, remainder = divmod(size, 256)
+ return chr(0) + chr(remainder) + chr(quotient)
+
+ def sendPart(self):
+ """ send a segment of data """
+ if not self.connected:
+ self._pendingSend = None
+ return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
+ data = self.file.read(self.segmentSize)
+ if data:
+ dataSize = len(data)
+ header = self.makeHeader(dataSize)
+ self.bytesSent += dataSize
+ self.transport.write(header + data)
+ self._pendingSend = reactor.callLater(0, self.sendPart)
+ else:
+ self._pendingSend = None
+ self.completed = 1
+
+# mapping of error codes to error messages
+errorCodes = {
+
+ 200 : "Syntax error",
+ 201 : "Invalid parameter",
+ 205 : "Invalid user",
+ 206 : "Domain name missing",
+ 207 : "Already logged in",
+ 208 : "Invalid username",
+ 209 : "Invalid screen name",
+ 210 : "User list full",
+ 215 : "User already there",
+ 216 : "User already on list",
+ 217 : "User not online",
+ 218 : "Already in mode",
+ 219 : "User is in the opposite list",
+ 223 : "Too many groups",
+ 224 : "Invalid group",
+ 225 : "User not in group",
+ 229 : "Group name too long",
+ 230 : "Cannot remove group 0",
+ 231 : "Invalid group",
+ 280 : "Switchboard failed",
+ 281 : "Transfer to switchboard failed",
+
+ 300 : "Required field missing",
+ 301 : "Too many FND responses",
+ 302 : "Not logged in",
+
+ 500 : "Internal server error",
+ 501 : "Database server error",
+ 502 : "Command disabled",
+ 510 : "File operation failed",
+ 520 : "Memory allocation failed",
+ 540 : "Wrong CHL value sent to server",
+
+ 600 : "Server is busy",
+ 601 : "Server is unavaliable",
+ 602 : "Peer nameserver is down",
+ 603 : "Database connection failed",
+ 604 : "Server is going down",
+ 605 : "Server unavailable",
+
+ 707 : "Could not create connection",
+ 710 : "Invalid CVR parameters",
+ 711 : "Write is blocking",
+ 712 : "Session is overloaded",
+ 713 : "Too many active users",
+ 714 : "Too many sessions",
+ 715 : "Not expected",
+ 717 : "Bad friend file",
+ 731 : "Not expected",
+
+ 800 : "Requests too rapid",
+
+ 910 : "Server too busy",
+ 911 : "Authentication failed",
+ 912 : "Server too busy",
+ 913 : "Not allowed when offline",
+ 914 : "Server too busy",
+ 915 : "Server too busy",
+ 916 : "Server too busy",
+ 917 : "Server too busy",
+ 918 : "Server too busy",
+ 919 : "Server too busy",
+ 920 : "Not accepting new users",
+ 921 : "Server too busy",
+ 922 : "Server too busy",
+ 923 : "No parent consent",
+ 924 : "Passport account not yet verified"
+
+}
+
+# mapping of status codes to readable status format
+statusCodes = {
+
+ STATUS_ONLINE : "Online",
+ STATUS_OFFLINE : "Offline",
+ STATUS_HIDDEN : "Appear Offline",
+ STATUS_IDLE : "Idle",
+ STATUS_AWAY : "Away",
+ STATUS_BUSY : "Busy",
+ STATUS_BRB : "Be Right Back",
+ STATUS_PHONE : "On the Phone",
+ STATUS_LUNCH : "Out to Lunch"
+
+}
+
+# mapping of list ids to list codes
+listIDToCode = {
+
+ FORWARD_LIST : 'fl',
+ BLOCK_LIST : 'bl',
+ ALLOW_LIST : 'al',
+ REVERSE_LIST : 'rl'
+
+}
+
+# mapping of list codes to list ids
+listCodeToID = {}
+for id,code in listIDToCode.items():
+ listCodeToID[code] = id
+
+del id, code
+
+# Mapping of class GUIDs to simple english names
+guidToClassName = {
+ "{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
+ }
+
+# Reverse of the above
+classNameToGUID = {}
+for guid, name in guidToClassName.iteritems():
+ classNameToGUID[name] = guid
diff --git a/twisted/words/protocols/oscar.py b/twisted/words/protocols/oscar.py
new file mode 100644
index 0000000..81571d4
--- /dev/null
+++ b/twisted/words/protocols/oscar.py
@@ -0,0 +1,1235 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+An implementation of the OSCAR protocol, which AIM and ICQ use to communcate.
+
+Maintainer: Paul Swartz
+"""
+
+import struct
+import string
+import socket
+import random
+import types
+import re
+
+from twisted.internet import reactor, defer, protocol
+from twisted.python import log
+from twisted.python.hashlib import md5
+
+def logPacketData(data):
+ lines = len(data)/16
+ if lines*16 != len(data): lines=lines+1
+ for i in range(lines):
+ d = tuple(data[16*i:16*i+16])
+ hex = map(lambda x: "%02X"%ord(x),d)
+ text = map(lambda x: (len(repr(x))>3 and '.') or x, d)
+ log.msg(' '.join(hex)+ ' '*3*(16-len(d)) +''.join(text))
+ log.msg('')
+
+def SNAC(fam,sub,id,data,flags=[0,0]):
+ header="!HHBBL"
+ head=struct.pack(header,fam,sub,
+ flags[0],flags[1],
+ id)
+ return head+str(data)
+
+def readSNAC(data):
+ header="!HHBBL"
+ head=list(struct.unpack(header,data[:10]))
+ return head+[data[10:]]
+
+def TLV(type,value):
+ header="!HH"
+ head=struct.pack(header,type,len(value))
+ return head+str(value)
+
+def readTLVs(data,count=None):
+ header="!HH"
+ dict={}
+ while data and len(dict)!=count:
+ head=struct.unpack(header,data[:4])
+ dict[head[0]]=data[4:4+head[1]]
+ data=data[4+head[1]:]
+ if not count:
+ return dict
+ return dict,data
+
+def encryptPasswordMD5(password,key):
+ m=md5()
+ m.update(key)
+ m.update(md5(password).digest())
+ m.update("AOL Instant Messenger (SM)")
+ return m.digest()
+
+def encryptPasswordICQ(password):
+ key=[0xF3,0x26,0x81,0xC4,0x39,0x86,0xDB,0x92,0x71,0xA3,0xB9,0xE6,0x53,0x7A,0x95,0x7C]
+ bytes=map(ord,password)
+ r=""
+ for i in range(len(bytes)):
+ r=r+chr(bytes[i]^key[i%len(key)])
+ return r
+
+def dehtml(text):
+ text=string.replace(text,"<br>","\n")
+ text=string.replace(text,"<BR>","\n")
+ text=string.replace(text,"<Br>","\n") # XXX make this a regexp
+ text=string.replace(text,"<bR>","\n")
+ text=re.sub('<.*?>','',text)
+ text=string.replace(text,'&gt;','>')
+ text=string.replace(text,'&lt;','<')
+ text=string.replace(text,'&nbsp;',' ')
+ text=string.replace(text,'&#34;','"')
+ text=string.replace(text,'&amp;','&')
+ return text
+
+def html(text):
+ text=string.replace(text,'"','&#34;')
+ text=string.replace(text,'&','&amp;')
+ text=string.replace(text,'<','&lt;')
+ text=string.replace(text,'>','&gt;')
+ text=string.replace(text,"\n","<br>")
+ return '<html><body bgcolor="white"><font color="black">%s</font></body></html>'%text
+
+class OSCARUser:
+ def __init__(self, name, warn, tlvs):
+ self.name = name
+ self.warning = warn
+ self.flags = []
+ self.caps = []
+ for k,v in tlvs.items():
+ if k == 1: # user flags
+ v=struct.unpack('!H',v)[0]
+ for o, f in [(1,'trial'),
+ (2,'unknown bit 2'),
+ (4,'aol'),
+ (8,'unknown bit 4'),
+ (16,'aim'),
+ (32,'away'),
+ (1024,'activebuddy')]:
+ if v&o: self.flags.append(f)
+ elif k == 2: # member since date
+ self.memberSince = struct.unpack('!L',v)[0]
+ elif k == 3: # on-since
+ self.onSince = struct.unpack('!L',v)[0]
+ elif k == 4: # idle time
+ self.idleTime = struct.unpack('!H',v)[0]
+ elif k == 5: # unknown
+ pass
+ elif k == 6: # icq online status
+ if v[2] == '\x00':
+ self.icqStatus = 'online'
+ elif v[2] == '\x01':
+ self.icqStatus = 'away'
+ elif v[2] == '\x02':
+ self.icqStatus = 'dnd'
+ elif v[2] == '\x04':
+ self.icqStatus = 'out'
+ elif v[2] == '\x10':
+ self.icqStatus = 'busy'
+ else:
+ self.icqStatus = 'unknown'
+ elif k == 10: # icq ip address
+ self.icqIPaddy = socket.inet_ntoa(v)
+ elif k == 12: # icq random stuff
+ self.icqRandom = v
+ elif k == 13: # capabilities
+ caps=[]
+ while v:
+ c=v[:16]
+ if c==CAP_ICON: caps.append("icon")
+ elif c==CAP_IMAGE: caps.append("image")
+ elif c==CAP_VOICE: caps.append("voice")
+ elif c==CAP_CHAT: caps.append("chat")
+ elif c==CAP_GET_FILE: caps.append("getfile")
+ elif c==CAP_SEND_FILE: caps.append("sendfile")
+ elif c==CAP_SEND_LIST: caps.append("sendlist")
+ elif c==CAP_GAMES: caps.append("games")
+ else: caps.append(("unknown",c))
+ v=v[16:]
+ caps.sort()
+ self.caps=caps
+ elif k == 14: pass
+ elif k == 15: # session length (aim)
+ self.sessionLength = struct.unpack('!L',v)[0]
+ elif k == 16: # session length (aol)
+ self.sessionLength = struct.unpack('!L',v)[0]
+ elif k == 30: # no idea
+ pass
+ else:
+ log.msg("unknown tlv for user %s\nt: %s\nv: %s"%(self.name,k,repr(v)))
+
+ def __str__(self):
+ s = '<OSCARUser %s' % self.name
+ o = []
+ if self.warning!=0: o.append('warning level %s'%self.warning)
+ if hasattr(self, 'flags'): o.append('flags %s'%self.flags)
+ if hasattr(self, 'sessionLength'): o.append('online for %i minutes' % (self.sessionLength/60,))
+ if hasattr(self, 'idleTime'): o.append('idle for %i minutes' % self.idleTime)
+ if self.caps: o.append('caps %s'%self.caps)
+ if o:
+ s=s+', '+', '.join(o)
+ s=s+'>'
+ return s
+
+
+class SSIGroup:
+ def __init__(self, name, tlvs = {}):
+ self.name = name
+ #self.tlvs = []
+ #self.userIDs = []
+ self.usersToID = {}
+ self.users = []
+ #if not tlvs.has_key(0xC8): return
+ #buddyIDs = tlvs[0xC8]
+ #while buddyIDs:
+ # bid = struct.unpack('!H',buddyIDs[:2])[0]
+ # buddyIDs = buddyIDs[2:]
+ # self.users.append(bid)
+
+ def findIDFor(self, user):
+ return self.usersToID[user]
+
+ def addUser(self, buddyID, user):
+ self.usersToID[user] = buddyID
+ self.users.append(user)
+ user.group = self
+
+ def oscarRep(self, groupID, buddyID):
+ tlvData = TLV(0xc8, reduce(lambda x,y:x+y, [struct.pack('!H',self.usersToID[x]) for x in self.users]))
+ return struct.pack('!H', len(self.name)) + self.name + \
+ struct.pack('!HH', groupID, buddyID) + '\000\001' + tlvData
+
+
+class SSIBuddy:
+ def __init__(self, name, tlvs = {}):
+ self.name = name
+ self.tlvs = tlvs
+ for k,v in tlvs.items():
+ if k == 0x013c: # buddy comment
+ self.buddyComment = v
+ elif k == 0x013d: # buddy alerts
+ actionFlag = ord(v[0])
+ whenFlag = ord(v[1])
+ self.alertActions = []
+ self.alertWhen = []
+ if actionFlag&1:
+ self.alertActions.append('popup')
+ if actionFlag&2:
+ self.alertActions.append('sound')
+ if whenFlag&1:
+ self.alertWhen.append('online')
+ if whenFlag&2:
+ self.alertWhen.append('unidle')
+ if whenFlag&4:
+ self.alertWhen.append('unaway')
+ elif k == 0x013e:
+ self.alertSound = v
+
+ def oscarRep(self, groupID, buddyID):
+ tlvData = reduce(lambda x,y: x+y, map(lambda (k,v):TLV(k,v), self.tlvs.items()), '\000\000')
+ return struct.pack('!H', len(self.name)) + self.name + \
+ struct.pack('!HH', groupID, buddyID) + '\000\000' + tlvData
+
+
+class OscarConnection(protocol.Protocol):
+ def connectionMade(self):
+ self.state=""
+ self.seqnum=0
+ self.buf=''
+ self.stopKeepAliveID = None
+ self.setKeepAlive(4*60) # 4 minutes
+
+ def connectionLost(self, reason):
+ log.msg("Connection Lost! %s" % self)
+ self.stopKeepAlive()
+
+# def connectionFailed(self):
+# log.msg("Connection Failed! %s" % self)
+# self.stopKeepAlive()
+
+ def sendFLAP(self,data,channel = 0x02):
+ header="!cBHH"
+ self.seqnum=(self.seqnum+1)%0xFFFF
+ seqnum=self.seqnum
+ head=struct.pack(header,'*', channel,
+ seqnum, len(data))
+ self.transport.write(head+str(data))
+# if isinstance(self, ChatService):
+# logPacketData(head+str(data))
+
+ def readFlap(self):
+ header="!cBHH"
+ if len(self.buf)<6: return
+ flap=struct.unpack(header,self.buf[:6])
+ if len(self.buf)<6+flap[3]: return
+ data,self.buf=self.buf[6:6+flap[3]],self.buf[6+flap[3]:]
+ return [flap[1],data]
+
+ def dataReceived(self,data):
+# if isinstance(self, ChatService):
+# logPacketData(data)
+ self.buf=self.buf+data
+ flap=self.readFlap()
+ while flap:
+ func=getattr(self,"oscar_%s"%self.state,None)
+ if not func:
+ log.msg("no func for state: %s" % self.state)
+ state=func(flap)
+ if state:
+ self.state=state
+ flap=self.readFlap()
+
+ def setKeepAlive(self,t):
+ self.keepAliveDelay=t
+ self.stopKeepAlive()
+ self.stopKeepAliveID = reactor.callLater(t, self.sendKeepAlive)
+
+ def sendKeepAlive(self):
+ self.sendFLAP("",0x05)
+ self.stopKeepAliveID = reactor.callLater(self.keepAliveDelay, self.sendKeepAlive)
+
+ def stopKeepAlive(self):
+ if self.stopKeepAliveID:
+ self.stopKeepAliveID.cancel()
+ self.stopKeepAliveID = None
+
+ def disconnect(self):
+ """
+ send the disconnect flap, and sever the connection
+ """
+ self.sendFLAP('', 0x04)
+ def f(reason): pass
+ self.connectionLost = f
+ self.transport.loseConnection()
+
+
+class SNACBased(OscarConnection):
+ snacFamilies = {
+ # family : (version, toolID, toolVersion)
+ }
+ def __init__(self,cookie):
+ self.cookie=cookie
+ self.lastID=0
+ self.supportedFamilies = ()
+ self.requestCallbacks={} # request id:Deferred
+
+ def sendSNAC(self,fam,sub,data,flags=[0,0]):
+ """
+ send a snac and wait for the response by returning a Deferred.
+ """
+ reqid=self.lastID
+ self.lastID=reqid+1
+ d = defer.Deferred()
+ d.reqid = reqid
+
+ #d.addErrback(self._ebDeferredError,fam,sub,data) # XXX for testing
+
+ self.requestCallbacks[reqid] = d
+ self.sendFLAP(SNAC(fam,sub,reqid,data))
+ return d
+
+ def _ebDeferredError(self, error, fam, sub, data):
+ log.msg('ERROR IN DEFERRED %s' % error)
+ log.msg('on sending of message, family 0x%02x, subtype 0x%02x' % (fam, sub))
+ log.msg('data: %s' % repr(data))
+
+ def sendSNACnr(self,fam,sub,data,flags=[0,0]):
+ """
+ send a snac, but don't bother adding a deferred, we don't care.
+ """
+ self.sendFLAP(SNAC(fam,sub,0x10000*fam+sub,data))
+
+ def oscar_(self,data):
+ self.sendFLAP("\000\000\000\001"+TLV(6,self.cookie), 0x01)
+ return "Data"
+
+ def oscar_Data(self,data):
+ snac=readSNAC(data[1])
+ if self.requestCallbacks.has_key(snac[4]):
+ d = self.requestCallbacks[snac[4]]
+ del self.requestCallbacks[snac[4]]
+ if snac[1]!=1:
+ d.callback(snac)
+ else:
+ d.errback(snac)
+ return
+ func=getattr(self,'oscar_%02X_%02X'%(snac[0],snac[1]),None)
+ if not func:
+ self.oscar_unknown(snac)
+ else:
+ func(snac[2:])
+ return "Data"
+
+ def oscar_unknown(self,snac):
+ log.msg("unknown for %s" % self)
+ log.msg(snac)
+
+
+ def oscar_01_03(self, snac):
+ numFamilies = len(snac[3])/2
+ self.supportedFamilies = struct.unpack("!"+str(numFamilies)+'H', snac[3])
+ d = ''
+ for fam in self.supportedFamilies:
+ if self.snacFamilies.has_key(fam):
+ d=d+struct.pack('!2H',fam,self.snacFamilies[fam][0])
+ self.sendSNACnr(0x01,0x17, d)
+
+ def oscar_01_0A(self,snac):
+ """
+ change of rate information.
+ """
+ # this can be parsed, maybe we can even work it in
+ pass
+
+ def oscar_01_18(self,snac):
+ """
+ host versions, in the same format as we sent
+ """
+ self.sendSNACnr(0x01,0x06,"") #pass
+
+ def clientReady(self):
+ """
+ called when the client is ready to be online
+ """
+ d = ''
+ for fam in self.supportedFamilies:
+ if self.snacFamilies.has_key(fam):
+ version, toolID, toolVersion = self.snacFamilies[fam]
+ d = d + struct.pack('!4H',fam,version,toolID,toolVersion)
+ self.sendSNACnr(0x01,0x02,d)
+
+class BOSConnection(SNACBased):
+ snacFamilies = {
+ 0x01:(3, 0x0110, 0x059b),
+ 0x13:(3, 0x0110, 0x059b),
+ 0x02:(1, 0x0110, 0x059b),
+ 0x03:(1, 0x0110, 0x059b),
+ 0x04:(1, 0x0110, 0x059b),
+ 0x06:(1, 0x0110, 0x059b),
+ 0x08:(1, 0x0104, 0x0001),
+ 0x09:(1, 0x0110, 0x059b),
+ 0x0a:(1, 0x0110, 0x059b),
+ 0x0b:(1, 0x0104, 0x0001),
+ 0x0c:(1, 0x0104, 0x0001)
+ }
+
+ capabilities = None
+
+ def __init__(self,username,cookie):
+ SNACBased.__init__(self,cookie)
+ self.username=username
+ self.profile = None
+ self.awayMessage = None
+ self.services = {}
+
+ if not self.capabilities:
+ self.capabilities = [CAP_CHAT]
+
+ def parseUser(self,data,count=None):
+ l=ord(data[0])
+ name=data[1:1+l]
+ warn,foo=struct.unpack("!HH",data[1+l:5+l])
+ warn=int(warn/10)
+ tlvs=data[5+l:]
+ if count:
+ tlvs,rest = readTLVs(tlvs,foo)
+ else:
+ tlvs,rest = readTLVs(tlvs), None
+ u = OSCARUser(name, warn, tlvs)
+ if rest == None:
+ return u
+ else:
+ return u, rest
+
+ def oscar_01_05(self, snac, d = None):
+ """
+ data for a new service connection
+ d might be a deferred to be called back when the service is ready
+ """
+ tlvs = readTLVs(snac[3][2:])
+ service = struct.unpack('!H',tlvs[0x0d])[0]
+ ip = tlvs[5]
+ cookie = tlvs[6]
+ #c = serviceClasses[service](self, cookie, d)
+ c = protocol.ClientCreator(reactor, serviceClasses[service], self, cookie, d)
+ def addService(x):
+ self.services[service] = x
+ c.connectTCP(ip, 5190).addCallback(addService)
+ #self.services[service] = c
+
+ def oscar_01_07(self,snac):
+ """
+ rate paramaters
+ """
+ self.sendSNACnr(0x01,0x08,"\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05") # ack
+ self.initDone()
+ self.sendSNACnr(0x13,0x02,'') # SSI rights info
+ self.sendSNACnr(0x02,0x02,'') # location rights info
+ self.sendSNACnr(0x03,0x02,'') # buddy list rights
+ self.sendSNACnr(0x04,0x04,'') # ICBM parms
+ self.sendSNACnr(0x09,0x02,'') # BOS rights
+
+ def oscar_01_10(self,snac):
+ """
+ we've been warned
+ """
+ skip = struct.unpack('!H',snac[3][:2])[0]
+ newLevel = struct.unpack('!H',snac[3][2+skip:4+skip])[0]/10
+ if len(snac[3])>4+skip:
+ by = self.parseUser(snac[3][4+skip:])
+ else:
+ by = None
+ self.receiveWarning(newLevel, by)
+
+ def oscar_01_13(self,snac):
+ """
+ MOTD
+ """
+ pass # we don't care for now
+
+ def oscar_02_03(self, snac):
+ """
+ location rights response
+ """
+ tlvs = readTLVs(snac[3])
+ self.maxProfileLength = tlvs[1]
+
+ def oscar_03_03(self, snac):
+ """
+ buddy list rights response
+ """
+ tlvs = readTLVs(snac[3])
+ self.maxBuddies = tlvs[1]
+ self.maxWatchers = tlvs[2]
+
+ def oscar_03_0B(self, snac):
+ """
+ buddy update
+ """
+ self.updateBuddy(self.parseUser(snac[3]))
+
+ def oscar_03_0C(self, snac):
+ """
+ buddy offline
+ """
+ self.offlineBuddy(self.parseUser(snac[3]))
+
+# def oscar_04_03(self, snac):
+
+ def oscar_04_05(self, snac):
+ """
+ ICBM parms response
+ """
+ self.sendSNACnr(0x04,0x02,'\x00\x00\x00\x00\x00\x0b\x1f@\x03\xe7\x03\xe7\x00\x00\x00\x00') # IM rights
+
+ def oscar_04_07(self, snac):
+ """
+ ICBM message (instant message)
+ """
+ data = snac[3]
+ cookie, data = data[:8], data[8:]
+ channel = struct.unpack('!H',data[:2])[0]
+ data = data[2:]
+ user, data = self.parseUser(data, 1)
+ tlvs = readTLVs(data)
+ if channel == 1: # message
+ flags = []
+ multiparts = []
+ for k, v in tlvs.items():
+ if k == 2:
+ while v:
+ v = v[2:] # skip bad data
+ messageLength, charSet, charSubSet = struct.unpack('!3H', v[:6])
+ messageLength -= 4
+ message = [v[6:6+messageLength]]
+ if charSet == 0:
+ pass # don't add anything special
+ elif charSet == 2:
+ message.append('unicode')
+ elif charSet == 3:
+ message.append('iso-8859-1')
+ elif charSet == 0xffff:
+ message.append('none')
+ if charSubSet == 0xb:
+ message.append('macintosh')
+ if messageLength > 0: multiparts.append(tuple(message))
+ v = v[6+messageLength:]
+ elif k == 3:
+ flags.append('acknowledge')
+ elif k == 4:
+ flags.append('auto')
+ elif k == 6:
+ flags.append('offline')
+ elif k == 8:
+ iconLength, foo, iconSum, iconStamp = struct.unpack('!LHHL',v)
+ if iconLength:
+ flags.append('icon')
+ flags.append((iconLength, iconSum, iconStamp))
+ elif k == 9:
+ flags.append('buddyrequest')
+ elif k == 0xb: # unknown
+ pass
+ elif k == 0x17:
+ flags.append('extradata')
+ flags.append(v)
+ else:
+ log.msg('unknown TLV for incoming IM, %04x, %s' % (k,repr(v)))
+
+# unknown tlv for user SNewdorf
+# t: 29
+# v: '\x00\x00\x00\x05\x02\x01\xd2\x04r\x00\x01\x01\x10/\x8c\x8b\x8a\x1e\x94*\xbc\x80}\x8d\xc4;\x1dEM'
+# XXX what is this?
+ self.receiveMessage(user, multiparts, flags)
+ elif channel == 2: # rondevouz
+ status = struct.unpack('!H',tlvs[5][:2])[0]
+ requestClass = tlvs[5][10:26]
+ moreTLVs = readTLVs(tlvs[5][26:])
+ if requestClass == CAP_CHAT: # a chat request
+ exchange = struct.unpack('!H',moreTLVs[10001][:2])[0]
+ name = moreTLVs[10001][3:-2]
+ instance = struct.unpack('!H',moreTLVs[10001][-2:])[0]
+ if not self.services.has_key(SERVICE_CHATNAV):
+ self.connectService(SERVICE_CHATNAV,1).addCallback(lambda x: self.services[SERVICE_CHATNAV].getChatInfo(exchange, name, instance).\
+ addCallback(self._cbGetChatInfoForInvite, user, moreTLVs[12]))
+ else:
+ self.services[SERVICE_CHATNAV].getChatInfo(exchange, name, instance).\
+ addCallback(self._cbGetChatInfoForInvite, user, moreTLVs[12])
+ elif requestClass == CAP_SEND_FILE:
+ if moreTLVs.has_key(11): # cancel
+ log.msg('cancelled file request')
+ log.msg(status)
+ return # handle this later
+ name = moreTLVs[10001][9:-7]
+ desc = moreTLVs[12]
+ log.msg('file request from %s, %s, %s' % (user, name, desc))
+ self.receiveSendFileRequest(user, name, desc, cookie)
+ else:
+ log.msg('unsupported rondevouz: %s' % requestClass)
+ log.msg(repr(moreTLVs))
+ else:
+ log.msg('unknown channel %02x' % channel)
+ log.msg(tlvs)
+
+ def _cbGetChatInfoForInvite(self, info, user, message):
+ apply(self.receiveChatInvite, (user,message)+info)
+
+ def oscar_09_03(self, snac):
+ """
+ BOS rights response
+ """
+ tlvs = readTLVs(snac[3])
+ self.maxPermitList = tlvs[1]
+ self.maxDenyList = tlvs[2]
+
+ def oscar_0B_02(self, snac):
+ """
+ stats reporting interval
+ """
+ self.reportingInterval = struct.unpack('!H',snac[3][:2])[0]
+
+ def oscar_13_03(self, snac):
+ """
+ SSI rights response
+ """
+ #tlvs = readTLVs(snac[3])
+ pass # we don't know how to parse this
+
+ # methods to be called by the client, and their support methods
+ def requestSelfInfo(self):
+ """
+ ask for the OSCARUser for ourselves
+ """
+ d = defer.Deferred()
+ self.sendSNAC(0x01, 0x0E, '').addCallback(self._cbRequestSelfInfo, d)
+ return d
+
+ def _cbRequestSelfInfo(self, snac, d):
+ d.callback(self.parseUser(snac[5]))
+
+ def initSSI(self):
+ """
+ this sends the rate request for family 0x13 (Server Side Information)
+ so we can then use it
+ """
+ return self.sendSNAC(0x13, 0x02, '').addCallback(self._cbInitSSI)
+
+ def _cbInitSSI(self, snac, d):
+ return {} # don't even bother parsing this
+
+ def requestSSI(self, timestamp = 0, revision = 0):
+ """
+ request the server side information
+ if the deferred gets None, it means the SSI is the same
+ """
+ return self.sendSNAC(0x13, 0x05,
+ struct.pack('!LH',timestamp,revision)).addCallback(self._cbRequestSSI)
+
+ def _cbRequestSSI(self, snac, args = ()):
+ if snac[1] == 0x0f: # same SSI as we have
+ return
+ itemdata = snac[5][3:]
+ if args:
+ revision, groups, permit, deny, permitMode, visibility = args
+ else:
+ version, revision = struct.unpack('!BH', snac[5][:3])
+ groups = {}
+ permit = []
+ deny = []
+ permitMode = None
+ visibility = None
+ while len(itemdata)>4:
+ nameLength = struct.unpack('!H', itemdata[:2])[0]
+ name = itemdata[2:2+nameLength]
+ groupID, buddyID, itemType, restLength = \
+ struct.unpack('!4H', itemdata[2+nameLength:10+nameLength])
+ tlvs = readTLVs(itemdata[10+nameLength:10+nameLength+restLength])
+ itemdata = itemdata[10+nameLength+restLength:]
+ if itemType == 0: # buddies
+ groups[groupID].addUser(buddyID, SSIBuddy(name, tlvs))
+ elif itemType == 1: # group
+ g = SSIGroup(name, tlvs)
+ if groups.has_key(0): groups[0].addUser(groupID, g)
+ groups[groupID] = g
+ elif itemType == 2: # permit
+ permit.append(name)
+ elif itemType == 3: # deny
+ deny.append(name)
+ elif itemType == 4: # permit deny info
+ if not tlvs.has_key(0xcb):
+ continue # this happens with ICQ
+ permitMode = {1:'permitall',2:'denyall',3:'permitsome',4:'denysome',5:'permitbuddies'}[ord(tlvs[0xca])]
+ visibility = {'\xff\xff\xff\xff':'all','\x00\x00\x00\x04':'notaim'}[tlvs[0xcb]]
+ elif itemType == 5: # unknown (perhaps idle data)?
+ pass
+ else:
+ log.msg('%s %s %s %s %s' % (name, groupID, buddyID, itemType, tlvs))
+ timestamp = struct.unpack('!L',itemdata)[0]
+ if not timestamp: # we've got more packets coming
+ # which means add some deferred stuff
+ d = defer.Deferred()
+ self.requestCallbacks[snac[4]] = d
+ d.addCallback(self._cbRequestSSI, (revision, groups, permit, deny, permitMode, visibility))
+ return d
+ return (groups[0].users,permit,deny,permitMode,visibility,timestamp,revision)
+
+ def activateSSI(self):
+ """
+ active the data stored on the server (use buddy list, permit deny settings, etc.)
+ """
+ self.sendSNACnr(0x13,0x07,'')
+
+ def startModifySSI(self):
+ """
+ tell the OSCAR server to be on the lookout for SSI modifications
+ """
+ self.sendSNACnr(0x13,0x11,'')
+
+ def addItemSSI(self, item, groupID = None, buddyID = None):
+ """
+ add an item to the SSI server. if buddyID == 0, then this should be a group.
+ this gets a callback when it's finished, but you can probably ignore it.
+ """
+ if groupID is None:
+ if isinstance(item, SSIGroup):
+ groupID = 0
+ else:
+ groupID = item.group.group.findIDFor(item.group)
+ if buddyID is None:
+ buddyID = item.group.findIDFor(item)
+ return self.sendSNAC(0x13,0x08, item.oscarRep(groupID, buddyID))
+
+ def modifyItemSSI(self, item, groupID = None, buddyID = None):
+ if groupID is None:
+ if isinstance(item, SSIGroup):
+ groupID = 0
+ else:
+ groupID = item.group.group.findIDFor(item.group)
+ if buddyID is None:
+ buddyID = item.group.findIDFor(item)
+ return self.sendSNAC(0x13,0x09, item.oscarRep(groupID, buddyID))
+
+ def delItemSSI(self, item, groupID = None, buddyID = None):
+ if groupID is None:
+ if isinstance(item, SSIGroup):
+ groupID = 0
+ else:
+ groupID = item.group.group.findIDFor(item.group)
+ if buddyID is None:
+ buddyID = item.group.findIDFor(item)
+ return self.sendSNAC(0x13,0x0A, item.oscarRep(groupID, buddyID))
+
+ def endModifySSI(self):
+ self.sendSNACnr(0x13,0x12,'')
+
+ def setProfile(self, profile):
+ """
+ set the profile.
+ send None to not set a profile (different from '' for a blank one)
+ """
+ self.profile = profile
+ tlvs = ''
+ if self.profile is not None:
+ tlvs = TLV(1,'text/aolrtf; charset="us-ascii"') + \
+ TLV(2,self.profile)
+
+ tlvs = tlvs + TLV(5, ''.join(self.capabilities))
+ self.sendSNACnr(0x02, 0x04, tlvs)
+
+ def setAway(self, away = None):
+ """
+ set the away message, or return (if away == None)
+ """
+ self.awayMessage = away
+ tlvs = TLV(3,'text/aolrtf; charset="us-ascii"') + \
+ TLV(4,away or '')
+ self.sendSNACnr(0x02, 0x04, tlvs)
+
+ def setIdleTime(self, idleTime):
+ """
+ set our idle time. don't call more than once with a non-0 idle time.
+ """
+ self.sendSNACnr(0x01, 0x11, struct.pack('!L',idleTime))
+
+ def sendMessage(self, user, message, wantAck = 0, autoResponse = 0, offline = 0 ): \
+ #haveIcon = 0, ):
+ """
+ send a message to user (not an OSCARUseR).
+ message can be a string, or a multipart tuple.
+ if wantAck, we return a Deferred that gets a callback when the message is sent.
+ if autoResponse, this message is an autoResponse, as if from an away message.
+ if offline, this is an offline message (ICQ only, I think)
+ """
+ data = ''.join([chr(random.randrange(0, 127)) for i in range(8)]) # cookie
+ data = data + '\x00\x01' + chr(len(user)) + user
+ if not type(message) in (types.TupleType, types.ListType):
+ message = [[message,]]
+ if type(message[0][0]) == types.UnicodeType:
+ message[0].append('unicode')
+ messageData = ''
+ for part in message:
+ charSet = 0
+ if 'unicode' in part[1:]:
+ charSet = 2
+ part[0] = part[0].encode('utf-8')
+ elif 'iso-8859-1' in part[1:]:
+ charSet = 3
+ part[0] = part[0].encode('iso-8859-1')
+ elif 'none' in part[1:]:
+ charSet = 0xffff
+ if 'macintosh' in part[1:]:
+ charSubSet = 0xb
+ else:
+ charSubSet = 0
+ messageData = messageData + '\x01\x01' + \
+ struct.pack('!3H',len(part[0])+4,charSet,charSubSet)
+ messageData = messageData + part[0]
+ data = data + TLV(2, '\x05\x01\x00\x03\x01\x01\x02'+messageData)
+ if wantAck:
+ data = data + TLV(3,'')
+ if autoResponse:
+ data = data + TLV(4,'')
+ if offline:
+ data = data + TLV(6,'')
+ if wantAck:
+ return self.sendSNAC(0x04, 0x06, data).addCallback(self._cbSendMessageAck, user, message)
+ self.sendSNACnr(0x04, 0x06, data)
+
+ def _cbSendMessageAck(self, snac, user, message):
+ return user, message
+
+ def connectService(self, service, wantCallback = 0, extraData = ''):
+ """
+ connect to another service
+ if wantCallback, we return a Deferred that gets called back when the service is online.
+ if extraData, append that to our request.
+ """
+ if wantCallback:
+ d = defer.Deferred()
+ self.sendSNAC(0x01,0x04,struct.pack('!H',service) + extraData).addCallback(self._cbConnectService, d)
+ return d
+ else:
+ self.sendSNACnr(0x01,0x04,struct.pack('!H',service))
+
+ def _cbConnectService(self, snac, d):
+ self.oscar_01_05(snac[2:], d)
+
+ def createChat(self, shortName):
+ """
+ create a chat room
+ """
+ if self.services.has_key(SERVICE_CHATNAV):
+ return self.services[SERVICE_CHATNAV].createChat(shortName)
+ else:
+ return self.connectService(SERVICE_CHATNAV,1).addCallback(lambda s: s.createChat(shortName))
+
+
+ def joinChat(self, exchange, fullName, instance):
+ """
+ join a chat room
+ """
+ #d = defer.Deferred()
+ return self.connectService(0x0e, 1, TLV(0x01, struct.pack('!HB',exchange, len(fullName)) + fullName +
+ struct.pack('!H', instance))).addCallback(self._cbJoinChat) #, d)
+ #return d
+
+ def _cbJoinChat(self, chat):
+ del self.services[SERVICE_CHAT]
+ return chat
+
+ def warnUser(self, user, anon = 0):
+ return self.sendSNAC(0x04, 0x08, '\x00'+chr(anon)+chr(len(user))+user).addCallback(self._cbWarnUser)
+
+ def _cbWarnUser(self, snac):
+ oldLevel, newLevel = struct.unpack('!2H', snac[5])
+ return oldLevel, newLevel
+
+ def getInfo(self, user):
+ #if user.
+ return self.sendSNAC(0x02, 0x05, '\x00\x01'+chr(len(user))+user).addCallback(self._cbGetInfo)
+
+ def _cbGetInfo(self, snac):
+ user, rest = self.parseUser(snac[5],1)
+ tlvs = readTLVs(rest)
+ return tlvs.get(0x02,None)
+
+ def getAway(self, user):
+ return self.sendSNAC(0x02, 0x05, '\x00\x03'+chr(len(user))+user).addCallback(self._cbGetAway)
+
+ def _cbGetAway(self, snac):
+ user, rest = self.parseUser(snac[5],1)
+ tlvs = readTLVs(rest)
+ return tlvs.get(0x04,None) # return None if there is no away message
+
+ #def acceptSendFileRequest(self,
+
+ # methods to be overriden by the client
+ def initDone(self):
+ """
+ called when we get the rate information, which means we should do other init. stuff.
+ """
+ log.msg('%s initDone' % self)
+ pass
+
+ def updateBuddy(self, user):
+ """
+ called when a buddy changes status, with the OSCARUser for that buddy.
+ """
+ log.msg('%s updateBuddy %s' % (self, user))
+ pass
+
+ def offlineBuddy(self, user):
+ """
+ called when a buddy goes offline
+ """
+ log.msg('%s offlineBuddy %s' % (self, user))
+ pass
+
+ def receiveMessage(self, user, multiparts, flags):
+ """
+ called when someone sends us a message
+ """
+ pass
+
+ def receiveWarning(self, newLevel, user):
+ """
+ called when someone warns us.
+ user is either None (if it was anonymous) or an OSCARUser
+ """
+ pass
+
+ def receiveChatInvite(self, user, message, exchange, fullName, instance, shortName, inviteTime):
+ """
+ called when someone invites us to a chat room
+ """
+ pass
+
+ def chatReceiveMessage(self, chat, user, message):
+ """
+ called when someone in a chatroom sends us a message in the chat
+ """
+ pass
+
+ def chatMemberJoined(self, chat, member):
+ """
+ called when a member joins the chat
+ """
+ pass
+
+ def chatMemberLeft(self, chat, member):
+ """
+ called when a member leaves the chat
+ """
+ pass
+
+ def receiveSendFileRequest(self, user, file, description, cookie):
+ """
+ called when someone tries to send a file to us
+ """
+ pass
+
+class OSCARService(SNACBased):
+ def __init__(self, bos, cookie, d = None):
+ SNACBased.__init__(self, cookie)
+ self.bos = bos
+ self.d = d
+
+ def connectionLost(self, reason):
+ for k,v in self.bos.services.items():
+ if v == self:
+ del self.bos.services[k]
+ return
+
+ def clientReady(self):
+ SNACBased.clientReady(self)
+ if self.d:
+ self.d.callback(self)
+ self.d = None
+
+class ChatNavService(OSCARService):
+ snacFamilies = {
+ 0x01:(3, 0x0010, 0x059b),
+ 0x0d:(1, 0x0010, 0x059b)
+ }
+ def oscar_01_07(self, snac):
+ # rate info
+ self.sendSNACnr(0x01, 0x08, '\000\001\000\002\000\003\000\004\000\005')
+ self.sendSNACnr(0x0d, 0x02, '')
+
+ def oscar_0D_09(self, snac):
+ self.clientReady()
+
+ def getChatInfo(self, exchange, name, instance):
+ d = defer.Deferred()
+ self.sendSNAC(0x0d,0x04,struct.pack('!HB',exchange,len(name)) + \
+ name + struct.pack('!HB',instance,2)). \
+ addCallback(self._cbGetChatInfo, d)
+ return d
+
+ def _cbGetChatInfo(self, snac, d):
+ data = snac[5][4:]
+ exchange, length = struct.unpack('!HB',data[:3])
+ fullName = data[3:3+length]
+ instance = struct.unpack('!H',data[3+length:5+length])[0]
+ tlvs = readTLVs(data[8+length:])
+ shortName = tlvs[0x6a]
+ inviteTime = struct.unpack('!L',tlvs[0xca])[0]
+ info = (exchange,fullName,instance,shortName,inviteTime)
+ d.callback(info)
+
+ def createChat(self, shortName):
+ #d = defer.Deferred()
+ data = '\x00\x04\x06create\xff\xff\x01\x00\x03'
+ data = data + TLV(0xd7, 'en')
+ data = data + TLV(0xd6, 'us-ascii')
+ data = data + TLV(0xd3, shortName)
+ return self.sendSNAC(0x0d, 0x08, data).addCallback(self._cbCreateChat)
+ #return d
+
+ def _cbCreateChat(self, snac): #d):
+ exchange, length = struct.unpack('!HB',snac[5][4:7])
+ fullName = snac[5][7:7+length]
+ instance = struct.unpack('!H',snac[5][7+length:9+length])[0]
+ #d.callback((exchange, fullName, instance))
+ return exchange, fullName, instance
+
+class ChatService(OSCARService):
+ snacFamilies = {
+ 0x01:(3, 0x0010, 0x059b),
+ 0x0E:(1, 0x0010, 0x059b)
+ }
+ def __init__(self,bos,cookie, d = None):
+ OSCARService.__init__(self,bos,cookie,d)
+ self.exchange = None
+ self.fullName = None
+ self.instance = None
+ self.name = None
+ self.members = None
+
+ clientReady = SNACBased.clientReady # we'll do our own callback
+
+ def oscar_01_07(self,snac):
+ self.sendSNAC(0x01,0x08,"\000\001\000\002\000\003\000\004\000\005")
+ self.clientReady()
+
+ def oscar_0E_02(self, snac):
+# try: # this is EVIL
+# data = snac[3][4:]
+# self.exchange, length = struct.unpack('!HB',data[:3])
+# self.fullName = data[3:3+length]
+# self.instance = struct.unpack('!H',data[3+length:5+length])[0]
+# tlvs = readTLVs(data[8+length:])
+# self.name = tlvs[0xd3]
+# self.d.callback(self)
+# except KeyError:
+ data = snac[3]
+ self.exchange, length = struct.unpack('!HB',data[:3])
+ self.fullName = data[3:3+length]
+ self.instance = struct.unpack('!H',data[3+length:5+length])[0]
+ tlvs = readTLVs(data[8+length:])
+ self.name = tlvs[0xd3]
+ self.d.callback(self)
+
+ def oscar_0E_03(self,snac):
+ users=[]
+ rest=snac[3]
+ while rest:
+ user, rest = self.bos.parseUser(rest, 1)
+ users.append(user)
+ if not self.fullName:
+ self.members = users
+ else:
+ self.members.append(users[0])
+ self.bos.chatMemberJoined(self,users[0])
+
+ def oscar_0E_04(self,snac):
+ user=self.bos.parseUser(snac[3])
+ for u in self.members:
+ if u.name == user.name: # same person!
+ self.members.remove(u)
+ self.bos.chatMemberLeft(self,user)
+
+ def oscar_0E_06(self,snac):
+ data = snac[3]
+ user,rest=self.bos.parseUser(snac[3][14:],1)
+ tlvs = readTLVs(rest[8:])
+ message=tlvs[1]
+ self.bos.chatReceiveMessage(self,user,message)
+
+ def sendMessage(self,message):
+ tlvs=TLV(0x02,"us-ascii")+TLV(0x03,"en")+TLV(0x01,message)
+ self.sendSNAC(0x0e,0x05,
+ "\x46\x30\x38\x30\x44\x00\x63\x00\x00\x03\x00\x01\x00\x00\x00\x06\x00\x00\x00\x05"+
+ struct.pack("!H",len(tlvs))+
+ tlvs)
+
+ def leaveChat(self):
+ self.disconnect()
+
+class OscarAuthenticator(OscarConnection):
+ BOSClass = BOSConnection
+ def __init__(self,username,password,deferred=None,icq=0):
+ self.username=username
+ self.password=password
+ self.deferred=deferred
+ self.icq=icq # icq mode is disabled
+ #if icq and self.BOSClass==BOSConnection:
+ # self.BOSClass=ICQConnection
+
+ def oscar_(self,flap):
+ if not self.icq:
+ self.sendFLAP("\000\000\000\001", 0x01)
+ self.sendFLAP(SNAC(0x17,0x06,0,
+ TLV(TLV_USERNAME,self.username)+
+ TLV(0x004B,'')))
+ self.state="Key"
+ else:
+ encpass=encryptPasswordICQ(self.password)
+ self.sendFLAP('\000\000\000\001'+
+ TLV(0x01,self.username)+
+ TLV(0x02,encpass)+
+ TLV(0x03,'ICQ Inc. - Product of ICQ (TM).2001b.5.18.1.3659.85')+
+ TLV(0x16,"\x01\x0a")+
+ TLV(0x17,"\x00\x05")+
+ TLV(0x18,"\x00\x12")+
+ TLV(0x19,"\000\001")+
+ TLV(0x1a,"\x0eK")+
+ TLV(0x14,"\x00\x00\x00U")+
+ TLV(0x0f,"en")+
+ TLV(0x0e,"us"),0x01)
+ self.state="Cookie"
+
+ def oscar_Key(self,data):
+ snac=readSNAC(data[1])
+ key=snac[5][2:]
+ encpass=encryptPasswordMD5(self.password,key)
+ self.sendFLAP(SNAC(0x17,0x02,0,
+ TLV(TLV_USERNAME,self.username)+
+ TLV(TLV_PASSWORD,encpass)+
+ TLV(0x004C, '')+ # unknown
+ TLV(TLV_CLIENTNAME,"AOL Instant Messenger (SM), version 4.8.2790/WIN32")+
+ TLV(0x0016,"\x01\x09")+
+ TLV(TLV_CLIENTMAJOR,"\000\004")+
+ TLV(TLV_CLIENTMINOR,"\000\010")+
+ TLV(0x0019,"\000\000")+
+ TLV(TLV_CLIENTSUB,"\x0A\xE6")+
+ TLV(0x0014,"\x00\x00\x00\xBB")+
+ TLV(TLV_LANG,"en")+
+ TLV(TLV_COUNTRY,"us")+
+ TLV(TLV_USESSI,"\001")))
+ return "Cookie"
+
+ def oscar_Cookie(self,data):
+ snac=readSNAC(data[1])
+ if self.icq:
+ i=snac[5].find("\000")
+ snac[5]=snac[5][i:]
+ tlvs=readTLVs(snac[5])
+ if tlvs.has_key(6):
+ self.cookie=tlvs[6]
+ server,port=string.split(tlvs[5],":")
+ d = self.connectToBOS(server, int(port))
+ d.addErrback(lambda x: log.msg("Connection Failed! Reason: %s" % x))
+ if self.deferred:
+ d.chainDeferred(self.deferred)
+ self.disconnect()
+ elif tlvs.has_key(8):
+ errorcode=tlvs[8]
+ errorurl=tlvs[4]
+ if errorcode=='\000\030':
+ error="You are attempting to sign on again too soon. Please try again later."
+ elif errorcode=='\000\005':
+ error="Invalid Username or Password."
+ else: error=repr(errorcode)
+ self.error(error,errorurl)
+ else:
+ log.msg('hmm, weird tlvs for %s cookie packet' % str(self))
+ log.msg(tlvs)
+ log.msg('snac')
+ log.msg(str(snac))
+ return "None"
+
+ def oscar_None(self,data): pass
+
+ def connectToBOS(self, server, port):
+ c = protocol.ClientCreator(reactor, self.BOSClass, self.username, self.cookie)
+ return c.connectTCP(server, int(port))
+
+ def error(self,error,url):
+ log.msg("ERROR! %s %s" % (error,url))
+ if self.deferred: self.deferred.errback((error,url))
+ self.transport.loseConnection()
+
+FLAP_CHANNEL_NEW_CONNECTION = 0x01
+FLAP_CHANNEL_DATA = 0x02
+FLAP_CHANNEL_ERROR = 0x03
+FLAP_CHANNEL_CLOSE_CONNECTION = 0x04
+
+SERVICE_CHATNAV = 0x0d
+SERVICE_CHAT = 0x0e
+serviceClasses = {
+ SERVICE_CHATNAV:ChatNavService,
+ SERVICE_CHAT:ChatService
+}
+TLV_USERNAME = 0x0001
+TLV_CLIENTNAME = 0x0003
+TLV_COUNTRY = 0x000E
+TLV_LANG = 0x000F
+TLV_CLIENTMAJOR = 0x0017
+TLV_CLIENTMINOR = 0x0018
+TLV_CLIENTSUB = 0x001A
+TLV_PASSWORD = 0x0025
+TLV_USESSI = 0x004A
+
+CAP_ICON = '\011F\023FL\177\021\321\202"DEST\000\000'
+CAP_VOICE = '\011F\023AL\177\021\321\202"DEST\000\000'
+CAP_IMAGE = '\011F\023EL\177\021\321\202"DEST\000\000'
+CAP_CHAT = 't\217$ b\207\021\321\202"DEST\000\000'
+CAP_GET_FILE = '\011F\023HL\177\021\321\202"DEST\000\000'
+CAP_SEND_FILE = '\011F\023CL\177\021\321\202"DEST\000\000'
+CAP_GAMES = '\011F\023GL\177\021\321\202"DEST\000\000'
+CAP_SEND_LIST = '\011F\023KL\177\021\321\202"DEST\000\000'
+CAP_SERV_REL = '\011F\023IL\177\021\321\202"DEST\000\000'
diff --git a/twisted/words/service.py b/twisted/words/service.py
new file mode 100644
index 0000000..388f7e6
--- /dev/null
+++ b/twisted/words/service.py
@@ -0,0 +1,1223 @@
+# -*- test-case-name: twisted.words.test.test_service -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A module that needs a better name.
+
+Implements new cred things for words.
+
+How does this thing work?
+
+ - Network connection on some port expecting to speak some protocol
+
+ - Protocol-specific authentication, resulting in some kind of credentials object
+
+ - twisted.cred.portal login using those credentials for the interface
+ IUser and with something implementing IChatClient as the mind
+
+ - successful login results in an IUser avatar the protocol can call
+ methods on, and state added to the realm such that the mind will have
+ methods called on it as is necessary
+
+ - protocol specific actions lead to calls onto the avatar; remote events
+ lead to calls onto the mind
+
+ - protocol specific hangup, realm is notified, user is removed from active
+ play, the end.
+"""
+
+from time import time, ctime
+
+from zope.interface import implements
+
+from twisted.words import iwords, ewords
+
+from twisted.python.components import registerAdapter
+from twisted.cred import portal, credentials, error as ecred
+from twisted.spread import pb
+from twisted.words.protocols import irc
+from twisted.internet import defer, protocol
+from twisted.python import log, failure, reflect
+from twisted import copyright
+
+
+class Group(object):
+ implements(iwords.IGroup)
+
+ def __init__(self, name):
+ self.name = name
+ self.users = {}
+ self.meta = {
+ "topic": "",
+ "topic_author": "",
+ }
+
+
+ def _ebUserCall(self, err, p):
+ return failure.Failure(Exception(p, err))
+
+
+ def _cbUserCall(self, results):
+ for (success, result) in results:
+ if not success:
+ user, err = result.value # XXX
+ self.remove(user, err.getErrorMessage())
+
+
+ def add(self, user):
+ assert iwords.IChatClient.providedBy(user), "%r is not a chat client" % (user,)
+ if user.name not in self.users:
+ additions = []
+ self.users[user.name] = user
+ for p in self.users.itervalues():
+ if p is not user:
+ d = defer.maybeDeferred(p.userJoined, self, user)
+ d.addErrback(self._ebUserCall, p=p)
+ additions.append(d)
+ defer.DeferredList(additions).addCallback(self._cbUserCall)
+ return defer.succeed(None)
+
+
+ def remove(self, user, reason=None):
+ assert reason is None or isinstance(reason, unicode)
+ try:
+ del self.users[user.name]
+ except KeyError:
+ pass
+ else:
+ removals = []
+ for p in self.users.itervalues():
+ if p is not user:
+ d = defer.maybeDeferred(p.userLeft, self, user, reason)
+ d.addErrback(self._ebUserCall, p=p)
+ removals.append(d)
+ defer.DeferredList(removals).addCallback(self._cbUserCall)
+ return defer.succeed(None)
+
+
+ def size(self):
+ return defer.succeed(len(self.users))
+
+
+ def receive(self, sender, recipient, message):
+ assert recipient is self
+ receives = []
+ for p in self.users.itervalues():
+ if p is not sender:
+ d = defer.maybeDeferred(p.receive, sender, self, message)
+ d.addErrback(self._ebUserCall, p=p)
+ receives.append(d)
+ defer.DeferredList(receives).addCallback(self._cbUserCall)
+ return defer.succeed(None)
+
+
+ def setMetadata(self, meta):
+ self.meta = meta
+ sets = []
+ for p in self.users.itervalues():
+ d = defer.maybeDeferred(p.groupMetaUpdate, self, meta)
+ d.addErrback(self._ebUserCall, p=p)
+ sets.append(d)
+ defer.DeferredList(sets).addCallback(self._cbUserCall)
+ return defer.succeed(None)
+
+
+ def iterusers(self):
+ # XXX Deferred?
+ return iter(self.users.values())
+
+
+class User(object):
+ implements(iwords.IUser)
+
+ realm = None
+ mind = None
+
+ def __init__(self, name):
+ self.name = name
+ self.groups = []
+ self.lastMessage = time()
+
+
+ def loggedIn(self, realm, mind):
+ self.realm = realm
+ self.mind = mind
+ self.signOn = time()
+
+
+ def join(self, group):
+ def cbJoin(result):
+ self.groups.append(group)
+ return result
+ return group.add(self.mind).addCallback(cbJoin)
+
+
+ def leave(self, group, reason=None):
+ def cbLeave(result):
+ self.groups.remove(group)
+ return result
+ return group.remove(self.mind, reason).addCallback(cbLeave)
+
+
+ def send(self, recipient, message):
+ self.lastMessage = time()
+ return recipient.receive(self.mind, recipient, message)
+
+
+ def itergroups(self):
+ return iter(self.groups)
+
+
+ def logout(self):
+ for g in self.groups[:]:
+ self.leave(g)
+
+
+NICKSERV = 'NickServ!NickServ@services'
+
+
+class IRCUser(irc.IRC):
+ """
+ Protocol instance representing an IRC user connected to the server.
+ """
+ implements(iwords.IChatClient)
+
+ # A list of IGroups in which I am participating
+ groups = None
+
+ # A no-argument callable I should invoke when I go away
+ logout = None
+
+ # An IUser we use to interact with the chat service
+ avatar = None
+
+ # To whence I belong
+ realm = None
+
+ # How to handle unicode (TODO: Make this customizable on a per-user basis)
+ encoding = 'utf-8'
+
+ # Twisted callbacks
+ def connectionMade(self):
+ self.irc_PRIVMSG = self.irc_NICKSERV_PRIVMSG
+ self.realm = self.factory.realm
+ self.hostname = self.realm.name
+
+
+ def connectionLost(self, reason):
+ if self.logout is not None:
+ self.logout()
+ self.avatar = None
+
+
+ # Make sendMessage a bit more useful to us
+ def sendMessage(self, command, *parameter_list, **kw):
+ if not kw.has_key('prefix'):
+ kw['prefix'] = self.hostname
+ if not kw.has_key('to'):
+ kw['to'] = self.name.encode(self.encoding)
+
+ arglist = [self, command, kw['to']] + list(parameter_list)
+ irc.IRC.sendMessage(*arglist, **kw)
+
+
+ # IChatClient implementation
+ def userJoined(self, group, user):
+ self.join(
+ "%s!%s@%s" % (user.name, user.name, self.hostname),
+ '#' + group.name)
+
+
+ def userLeft(self, group, user, reason=None):
+ assert reason is None or isinstance(reason, unicode)
+ self.part(
+ "%s!%s@%s" % (user.name, user.name, self.hostname),
+ '#' + group.name,
+ (reason or u"leaving").encode(self.encoding, 'replace'))
+
+
+ def receive(self, sender, recipient, message):
+ #>> :glyph!glyph@adsl-64-123-27-108.dsl.austtx.swbell.net PRIVMSG glyph_ :hello
+
+ # omg???????????
+ if iwords.IGroup.providedBy(recipient):
+ recipientName = '#' + recipient.name
+ else:
+ recipientName = recipient.name
+
+ text = message.get('text', '<an unrepresentable message>')
+ for L in text.splitlines():
+ self.privmsg(
+ '%s!%s@%s' % (sender.name, sender.name, self.hostname),
+ recipientName,
+ L)
+
+
+ def groupMetaUpdate(self, group, meta):
+ if 'topic' in meta:
+ topic = meta['topic']
+ author = meta.get('topic_author', '')
+ self.topic(
+ self.name,
+ '#' + group.name,
+ topic,
+ '%s!%s@%s' % (author, author, self.hostname)
+ )
+
+ # irc.IRC callbacks - starting with login related stuff.
+ nickname = None
+ password = None
+
+ def irc_PASS(self, prefix, params):
+ """Password message -- Register a password.
+
+ Parameters: <password>
+
+ [REQUIRED]
+
+ Note that IRC requires the client send this *before* NICK
+ and USER.
+ """
+ self.password = params[-1]
+
+
+ def irc_NICK(self, prefix, params):
+ """Nick message -- Set your nickname.
+
+ Parameters: <nickname>
+
+ [REQUIRED]
+ """
+ try:
+ nickname = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.privmsg(
+ NICKSERV,
+ nickname,
+ 'Your nickname is cannot be decoded. Please use ASCII or UTF-8.')
+ self.transport.loseConnection()
+ return
+
+ self.nickname = nickname
+ self.name = nickname
+
+ for code, text in self._motdMessages:
+ self.sendMessage(code, text % self.factory._serverInfo)
+
+ if self.password is None:
+ self.privmsg(
+ NICKSERV,
+ nickname,
+ 'Password?')
+ else:
+ password = self.password
+ self.password = None
+ self.logInAs(nickname, password)
+
+
+ def irc_USER(self, prefix, params):
+ """User message -- Set your realname.
+
+ Parameters: <user> <mode> <unused> <realname>
+ """
+ # Note: who gives a crap about this? The IUser has the real
+ # information we care about. Save it anyway, I guess, just
+ # for fun.
+ self.realname = params[-1]
+
+
+ def irc_NICKSERV_PRIVMSG(self, prefix, params):
+ """Send a (private) message.
+
+ Parameters: <msgtarget> <text to be sent>
+ """
+ target = params[0]
+ password = params[-1]
+
+ if self.nickname is None:
+ # XXX Send an error response here
+ self.transport.loseConnection()
+ elif target.lower() != "nickserv":
+ self.privmsg(
+ NICKSERV,
+ self.nickname,
+ "Denied. Please send me (NickServ) your password.")
+ else:
+ nickname = self.nickname
+ self.nickname = None
+ self.logInAs(nickname, password)
+
+
+ def logInAs(self, nickname, password):
+ d = self.factory.portal.login(
+ credentials.UsernamePassword(nickname, password),
+ self,
+ iwords.IUser)
+ d.addCallbacks(self._cbLogin, self._ebLogin, errbackArgs=(nickname,))
+
+
+ _welcomeMessages = [
+ (irc.RPL_WELCOME,
+ ":connected to Twisted IRC"),
+ (irc.RPL_YOURHOST,
+ ":Your host is %(serviceName)s, running version %(serviceVersion)s"),
+ (irc.RPL_CREATED,
+ ":This server was created on %(creationDate)s"),
+
+ # "Bummer. This server returned a worthless 004 numeric.
+ # I'll have to guess at all the values"
+ # -- epic
+ (irc.RPL_MYINFO,
+ # w and n are the currently supported channel and user modes
+ # -- specify this better
+ "%(serviceName)s %(serviceVersion)s w n")
+ ]
+
+ _motdMessages = [
+ (irc.RPL_MOTDSTART,
+ ":- %(serviceName)s Message of the Day - "),
+ (irc.RPL_ENDOFMOTD,
+ ":End of /MOTD command.")
+ ]
+
+ def _cbLogin(self, (iface, avatar, logout)):
+ assert iface is iwords.IUser, "Realm is buggy, got %r" % (iface,)
+
+ # Let them send messages to the world
+ del self.irc_PRIVMSG
+
+ self.avatar = avatar
+ self.logout = logout
+ for code, text in self._welcomeMessages:
+ self.sendMessage(code, text % self.factory._serverInfo)
+
+
+ def _ebLogin(self, err, nickname):
+ if err.check(ewords.AlreadyLoggedIn):
+ self.privmsg(
+ NICKSERV,
+ nickname,
+ "Already logged in. No pod people allowed!")
+ elif err.check(ecred.UnauthorizedLogin):
+ self.privmsg(
+ NICKSERV,
+ nickname,
+ "Login failed. Goodbye.")
+ else:
+ log.msg("Unhandled error during login:")
+ log.err(err)
+ self.privmsg(
+ NICKSERV,
+ nickname,
+ "Server error during login. Sorry.")
+ self.transport.loseConnection()
+
+
+ # Great, now that's out of the way, here's some of the interesting
+ # bits
+ def irc_PING(self, prefix, params):
+ """Ping message
+
+ Parameters: <server1> [ <server2> ]
+ """
+ if self.realm is not None:
+ self.sendMessage('PONG', self.hostname)
+
+
+ def irc_QUIT(self, prefix, params):
+ """Quit
+
+ Parameters: [ <Quit Message> ]
+ """
+ self.transport.loseConnection()
+
+
+ def _channelMode(self, group, modes=None, *args):
+ if modes:
+ self.sendMessage(
+ irc.ERR_UNKNOWNMODE,
+ ":Unknown MODE flag.")
+ else:
+ self.channelMode(self.name, '#' + group.name, '+')
+
+
+ def _userMode(self, user, modes=None):
+ if modes:
+ self.sendMessage(
+ irc.ERR_UNKNOWNMODE,
+ ":Unknown MODE flag.")
+ elif user is self.avatar:
+ self.sendMessage(
+ irc.RPL_UMODEIS,
+ "+")
+ else:
+ self.sendMessage(
+ irc.ERR_USERSDONTMATCH,
+ ":You can't look at someone else's modes.")
+
+
+ def irc_MODE(self, prefix, params):
+ """User mode message
+
+ Parameters: <nickname>
+ *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) )
+
+ """
+ try:
+ channelOrUser = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK, params[0],
+ ":No such nickname (could not decode your unicode!)")
+ return
+
+ if channelOrUser.startswith('#'):
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, params[0],
+ ":That channel doesn't exist.")
+ d = self.realm.lookupGroup(channelOrUser[1:])
+ d.addCallbacks(
+ self._channelMode,
+ ebGroup,
+ callbackArgs=tuple(params[1:]))
+ else:
+ def ebUser(err):
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK,
+ ":No such nickname.")
+
+ d = self.realm.lookupUser(channelOrUser)
+ d.addCallbacks(
+ self._userMode,
+ ebUser,
+ callbackArgs=tuple(params[1:]))
+
+
+ def irc_USERHOST(self, prefix, params):
+ """Userhost message
+
+ Parameters: <nickname> *( SPACE <nickname> )
+
+ [Optional]
+ """
+ pass
+
+
+ def irc_PRIVMSG(self, prefix, params):
+ """Send a (private) message.
+
+ Parameters: <msgtarget> <text to be sent>
+ """
+ try:
+ targetName = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK, params[0],
+ ":No such nick/channel (could not decode your unicode!)")
+ return
+
+ messageText = params[-1]
+ if targetName.startswith('#'):
+ target = self.realm.lookupGroup(targetName[1:])
+ else:
+ target = self.realm.lookupUser(targetName).addCallback(lambda user: user.mind)
+
+ def cbTarget(targ):
+ if targ is not None:
+ return self.avatar.send(targ, {"text": messageText})
+
+ def ebTarget(err):
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK, targetName,
+ ":No such nick/channel.")
+
+ target.addCallbacks(cbTarget, ebTarget)
+
+
+ def irc_JOIN(self, prefix, params):
+ """Join message
+
+ Parameters: ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] )
+ """
+ try:
+ groupName = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, params[0],
+ ":No such channel (could not decode your unicode!)")
+ return
+
+ if groupName.startswith('#'):
+ groupName = groupName[1:]
+
+ def cbGroup(group):
+ def cbJoin(ign):
+ self.userJoined(group, self)
+ self.names(
+ self.name,
+ '#' + group.name,
+ [user.name for user in group.iterusers()])
+ self._sendTopic(group)
+ return self.avatar.join(group).addCallback(cbJoin)
+
+ def ebGroup(err):
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, '#' + groupName,
+ ":No such channel.")
+
+ self.realm.getGroup(groupName).addCallbacks(cbGroup, ebGroup)
+
+
+ def irc_PART(self, prefix, params):
+ """Part message
+
+ Parameters: <channel> *( "," <channel> ) [ <Part Message> ]
+ """
+ try:
+ groupName = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOTONCHANNEL, params[0],
+ ":Could not decode your unicode!")
+ return
+
+ if groupName.startswith('#'):
+ groupName = groupName[1:]
+
+ if len(params) > 1:
+ reason = params[1].decode('utf-8')
+ else:
+ reason = None
+
+ def cbGroup(group):
+ def cbLeave(result):
+ self.userLeft(group, self, reason)
+ return self.avatar.leave(group, reason).addCallback(cbLeave)
+
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ self.sendMessage(
+ irc.ERR_NOTONCHANNEL,
+ '#' + groupName,
+ ":" + err.getErrorMessage())
+
+ self.realm.lookupGroup(groupName).addCallbacks(cbGroup, ebGroup)
+
+
+ def irc_NAMES(self, prefix, params):
+ """Names message
+
+ Parameters: [ <channel> *( "," <channel> ) [ <target> ] ]
+ """
+ #<< NAMES #python
+ #>> :benford.openprojects.net 353 glyph = #python :Orban ... @glyph ... Zymurgy skreech
+ #>> :benford.openprojects.net 366 glyph #python :End of /NAMES list.
+ try:
+ channel = params[-1].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, params[-1],
+ ":No such channel (could not decode your unicode!)")
+ return
+
+ if channel.startswith('#'):
+ channel = channel[1:]
+
+ def cbGroup(group):
+ self.names(
+ self.name,
+ '#' + group.name,
+ [user.name for user in group.iterusers()])
+
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ # No group? Fine, no names!
+ self.names(
+ self.name,
+ '#' + channel,
+ [])
+
+ self.realm.lookupGroup(channel).addCallbacks(cbGroup, ebGroup)
+
+
+ def irc_TOPIC(self, prefix, params):
+ """Topic message
+
+ Parameters: <channel> [ <topic> ]
+ """
+ try:
+ channel = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL,
+ ":That channel doesn't exist (could not decode your unicode!)")
+ return
+
+ if channel.startswith('#'):
+ channel = channel[1:]
+
+ if len(params) > 1:
+ self._setTopic(channel, params[1])
+ else:
+ self._getTopic(channel)
+
+
+ def _sendTopic(self, group):
+ """
+ Send the topic of the given group to this user, if it has one.
+ """
+ topic = group.meta.get("topic")
+ if topic:
+ author = group.meta.get("topic_author") or "<noone>"
+ date = group.meta.get("topic_date", 0)
+ self.topic(self.name, '#' + group.name, topic)
+ self.topicAuthor(self.name, '#' + group.name, author, date)
+
+
+ def _getTopic(self, channel):
+ #<< TOPIC #python
+ #>> :benford.openprojects.net 332 glyph #python :<churchr> I really did. I sprained all my toes.
+ #>> :benford.openprojects.net 333 glyph #python itamar|nyc 994713482
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, '=', channel,
+ ":That channel doesn't exist.")
+
+ self.realm.lookupGroup(channel).addCallbacks(self._sendTopic, ebGroup)
+
+
+ def _setTopic(self, channel, topic):
+ #<< TOPIC #divunal :foo
+ #>> :glyph!glyph@adsl-64-123-27-108.dsl.austtx.swbell.net TOPIC #divunal :foo
+
+ def cbGroup(group):
+ newMeta = group.meta.copy()
+ newMeta['topic'] = topic
+ newMeta['topic_author'] = self.name
+ newMeta['topic_date'] = int(time())
+
+ def ebSet(err):
+ self.sendMessage(
+ irc.ERR_CHANOPRIVSNEEDED,
+ "#" + group.name,
+ ":You need to be a channel operator to do that.")
+
+ return group.setMetadata(newMeta).addErrback(ebSet)
+
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, '=', channel,
+ ":That channel doesn't exist.")
+
+ self.realm.lookupGroup(channel).addCallbacks(cbGroup, ebGroup)
+
+
+ def list(self, channels):
+ """Send a group of LIST response lines
+
+ @type channel: C{list} of C{(str, int, str)}
+ @param channel: Information about the channels being sent:
+ their name, the number of participants, and their topic.
+ """
+ for (name, size, topic) in channels:
+ self.sendMessage(irc.RPL_LIST, name, str(size), ":" + topic)
+ self.sendMessage(irc.RPL_LISTEND, ":End of /LIST")
+
+
+ def irc_LIST(self, prefix, params):
+ """List query
+
+ Return information about the indicated channels, or about all
+ channels if none are specified.
+
+ Parameters: [ <channel> *( "," <channel> ) [ <target> ] ]
+ """
+ #<< list #python
+ #>> :orwell.freenode.net 321 exarkun Channel :Users Name
+ #>> :orwell.freenode.net 322 exarkun #python 358 :The Python programming language
+ #>> :orwell.freenode.net 323 exarkun :End of /LIST
+ if params:
+ # Return information about indicated channels
+ try:
+ channels = params[0].decode(self.encoding).split(',')
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHCHANNEL, params[0],
+ ":No such channel (could not decode your unicode!)")
+ return
+
+ groups = []
+ for ch in channels:
+ if ch.startswith('#'):
+ ch = ch[1:]
+ groups.append(self.realm.lookupGroup(ch))
+
+ groups = defer.DeferredList(groups, consumeErrors=True)
+ groups.addCallback(lambda gs: [r for (s, r) in gs if s])
+ else:
+ # Return information about all channels
+ groups = self.realm.itergroups()
+
+ def cbGroups(groups):
+ def gotSize(size, group):
+ return group.name, size, group.meta.get('topic')
+ d = defer.DeferredList([
+ group.size().addCallback(gotSize, group) for group in groups])
+ d.addCallback(lambda results: self.list([r for (s, r) in results if s]))
+ return d
+ groups.addCallback(cbGroups)
+
+
+ def _channelWho(self, group):
+ self.who(self.name, '#' + group.name,
+ [(m.name, self.hostname, self.realm.name, m.name, "H", 0, m.name) for m in group.iterusers()])
+
+
+ def _userWho(self, user):
+ self.sendMessage(irc.RPL_ENDOFWHO,
+ ":User /WHO not implemented")
+
+
+ def irc_WHO(self, prefix, params):
+ """Who query
+
+ Parameters: [ <mask> [ "o" ] ]
+ """
+ #<< who #python
+ #>> :x.opn 352 glyph #python aquarius pc-62-31-193-114-du.blueyonder.co.uk y.opn Aquarius H :3 Aquarius
+ # ...
+ #>> :x.opn 352 glyph #python foobar europa.tranquility.net z.opn skreech H :0 skreech
+ #>> :x.opn 315 glyph #python :End of /WHO list.
+ ### also
+ #<< who glyph
+ #>> :x.opn 352 glyph #python glyph adsl-64-123-27-108.dsl.austtx.swbell.net x.opn glyph H :0 glyph
+ #>> :x.opn 315 glyph glyph :End of /WHO list.
+ if not params:
+ self.sendMessage(irc.RPL_ENDOFWHO, ":/WHO not supported.")
+ return
+
+ try:
+ channelOrUser = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.RPL_ENDOFWHO, params[0],
+ ":End of /WHO list (could not decode your unicode!)")
+ return
+
+ if channelOrUser.startswith('#'):
+ def ebGroup(err):
+ err.trap(ewords.NoSuchGroup)
+ self.sendMessage(
+ irc.RPL_ENDOFWHO, channelOrUser,
+ ":End of /WHO list.")
+ d = self.realm.lookupGroup(channelOrUser[1:])
+ d.addCallbacks(self._channelWho, ebGroup)
+ else:
+ def ebUser(err):
+ err.trap(ewords.NoSuchUser)
+ self.sendMessage(
+ irc.RPL_ENDOFWHO, channelOrUser,
+ ":End of /WHO list.")
+ d = self.realm.lookupUser(channelOrUser)
+ d.addCallbacks(self._userWho, ebUser)
+
+
+
+ def irc_WHOIS(self, prefix, params):
+ """Whois query
+
+ Parameters: [ <target> ] <mask> *( "," <mask> )
+ """
+ def cbUser(user):
+ self.whois(
+ self.name,
+ user.name, user.name, self.realm.name,
+ user.name, self.realm.name, 'Hi mom!', False,
+ int(time() - user.lastMessage), user.signOn,
+ ['#' + group.name for group in user.itergroups()])
+
+ def ebUser(err):
+ err.trap(ewords.NoSuchUser)
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK,
+ params[0],
+ ":No such nick/channel")
+
+ try:
+ user = params[0].decode(self.encoding)
+ except UnicodeDecodeError:
+ self.sendMessage(
+ irc.ERR_NOSUCHNICK,
+ params[0],
+ ":No such nick/channel")
+ return
+
+ self.realm.lookupUser(user).addCallbacks(cbUser, ebUser)
+
+
+ # Unsupported commands, here for legacy compatibility
+ def irc_OPER(self, prefix, params):
+ """Oper message
+
+ Parameters: <name> <password>
+ """
+ self.sendMessage(irc.ERR_NOOPERHOST, ":O-lines not applicable")
+
+
+class IRCFactory(protocol.ServerFactory):
+ """
+ IRC server that creates instances of the L{IRCUser} protocol.
+
+ @ivar _serverInfo: A dictionary mapping:
+ "serviceName" to the name of the server,
+ "serviceVersion" to the copyright version,
+ "creationDate" to the time that the server was started.
+ """
+ protocol = IRCUser
+
+ def __init__(self, realm, portal):
+ self.realm = realm
+ self.portal = portal
+ self._serverInfo = {
+ "serviceName": self.realm.name,
+ "serviceVersion": copyright.version,
+ "creationDate": ctime()
+ }
+
+
+
+class PBMind(pb.Referenceable):
+ def __init__(self):
+ pass
+
+ def jellyFor(self, jellier):
+ return reflect.qual(PBMind), jellier.invoker.registerReference(self)
+
+ def remote_userJoined(self, user, group):
+ pass
+
+ def remote_userLeft(self, user, group, reason):
+ pass
+
+ def remote_receive(self, sender, recipient, message):
+ pass
+
+ def remote_groupMetaUpdate(self, group, meta):
+ pass
+
+
+class PBMindReference(pb.RemoteReference):
+ implements(iwords.IChatClient)
+
+ def receive(self, sender, recipient, message):
+ if iwords.IGroup.providedBy(recipient):
+ rec = PBGroup(self.realm, self.avatar, recipient)
+ else:
+ rec = PBUser(self.realm, self.avatar, recipient)
+ return self.callRemote(
+ 'receive',
+ PBUser(self.realm, self.avatar, sender),
+ rec,
+ message)
+
+ def groupMetaUpdate(self, group, meta):
+ return self.callRemote(
+ 'groupMetaUpdate',
+ PBGroup(self.realm, self.avatar, group),
+ meta)
+
+ def userJoined(self, group, user):
+ return self.callRemote(
+ 'userJoined',
+ PBGroup(self.realm, self.avatar, group),
+ PBUser(self.realm, self.avatar, user))
+
+ def userLeft(self, group, user, reason=None):
+ assert reason is None or isinstance(reason, unicode)
+ return self.callRemote(
+ 'userLeft',
+ PBGroup(self.realm, self.avatar, group),
+ PBUser(self.realm, self.avatar, user),
+ reason)
+pb.setUnjellyableForClass(PBMind, PBMindReference)
+
+
+class PBGroup(pb.Referenceable):
+ def __init__(self, realm, avatar, group):
+ self.realm = realm
+ self.avatar = avatar
+ self.group = group
+
+
+ def processUniqueID(self):
+ return hash((self.realm.name, self.avatar.name, self.group.name))
+
+
+ def jellyFor(self, jellier):
+ return reflect.qual(self.__class__), self.group.name.encode('utf-8'), jellier.invoker.registerReference(self)
+
+
+ def remote_leave(self, reason=None):
+ return self.avatar.leave(self.group, reason)
+
+
+ def remote_send(self, message):
+ return self.avatar.send(self.group, message)
+
+
+class PBGroupReference(pb.RemoteReference):
+ implements(iwords.IGroup)
+
+ def unjellyFor(self, unjellier, unjellyList):
+ clsName, name, ref = unjellyList
+ self.name = name.decode('utf-8')
+ return pb.RemoteReference.unjellyFor(self, unjellier, [clsName, ref])
+
+ def leave(self, reason=None):
+ return self.callRemote("leave", reason)
+
+ def send(self, message):
+ return self.callRemote("send", message)
+pb.setUnjellyableForClass(PBGroup, PBGroupReference)
+
+class PBUser(pb.Referenceable):
+ def __init__(self, realm, avatar, user):
+ self.realm = realm
+ self.avatar = avatar
+ self.user = user
+
+ def processUniqueID(self):
+ return hash((self.realm.name, self.avatar.name, self.user.name))
+
+
+class ChatAvatar(pb.Referenceable):
+ implements(iwords.IChatClient)
+
+ def __init__(self, avatar):
+ self.avatar = avatar
+
+
+ def jellyFor(self, jellier):
+ return reflect.qual(self.__class__), jellier.invoker.registerReference(self)
+
+
+ def remote_join(self, groupName):
+ assert isinstance(groupName, unicode)
+ def cbGroup(group):
+ def cbJoin(ignored):
+ return PBGroup(self.avatar.realm, self.avatar, group)
+ d = self.avatar.join(group)
+ d.addCallback(cbJoin)
+ return d
+ d = self.avatar.realm.getGroup(groupName)
+ d.addCallback(cbGroup)
+ return d
+registerAdapter(ChatAvatar, iwords.IUser, pb.IPerspective)
+
+class AvatarReference(pb.RemoteReference):
+ def join(self, groupName):
+ return self.callRemote('join', groupName)
+
+ def quit(self):
+ d = defer.Deferred()
+ self.broker.notifyOnDisconnect(lambda: d.callback(None))
+ self.broker.transport.loseConnection()
+ return d
+
+pb.setUnjellyableForClass(ChatAvatar, AvatarReference)
+
+
+class WordsRealm(object):
+ implements(portal.IRealm, iwords.IChatService)
+
+ _encoding = 'utf-8'
+
+ def __init__(self, name):
+ self.name = name
+
+
+ def userFactory(self, name):
+ return User(name)
+
+
+ def groupFactory(self, name):
+ return Group(name)
+
+
+ def logoutFactory(self, avatar, facet):
+ def logout():
+ # XXX Deferred support here
+ getattr(facet, 'logout', lambda: None)()
+ avatar.realm = avatar.mind = None
+ return logout
+
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if isinstance(avatarId, str):
+ avatarId = avatarId.decode(self._encoding)
+
+ def gotAvatar(avatar):
+ if avatar.realm is not None:
+ raise ewords.AlreadyLoggedIn()
+ for iface in interfaces:
+ facet = iface(avatar, None)
+ if facet is not None:
+ avatar.loggedIn(self, mind)
+ mind.name = avatarId
+ mind.realm = self
+ mind.avatar = avatar
+ return iface, facet, self.logoutFactory(avatar, facet)
+ raise NotImplementedError(self, interfaces)
+
+ return self.getUser(avatarId).addCallback(gotAvatar)
+
+
+ # IChatService, mostly.
+ createGroupOnRequest = False
+ createUserOnRequest = True
+
+ def lookupUser(self, name):
+ raise NotImplementedError
+
+
+ def lookupGroup(self, group):
+ raise NotImplementedError
+
+
+ def addUser(self, user):
+ """Add the given user to this service.
+
+ This is an internal method intented to be overridden by
+ L{WordsRealm} subclasses, not called by external code.
+
+ @type user: L{IUser}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with C{None} when the user is
+ added, or which fails with
+ L{twisted.words.ewords.DuplicateUser} if a user with the
+ same name exists already.
+ """
+ raise NotImplementedError
+
+
+ def addGroup(self, group):
+ """Add the given group to this service.
+
+ @type group: L{IGroup}
+
+ @rtype: L{twisted.internet.defer.Deferred}
+ @return: A Deferred which fires with C{None} when the group is
+ added, or which fails with
+ L{twisted.words.ewords.DuplicateGroup} if a group with the
+ same name exists already.
+ """
+ raise NotImplementedError
+
+
+ def getGroup(self, name):
+ assert isinstance(name, unicode)
+ if self.createGroupOnRequest:
+ def ebGroup(err):
+ err.trap(ewords.DuplicateGroup)
+ return self.lookupGroup(name)
+ return self.createGroup(name).addErrback(ebGroup)
+ return self.lookupGroup(name)
+
+
+ def getUser(self, name):
+ assert isinstance(name, unicode)
+ if self.createUserOnRequest:
+ def ebUser(err):
+ err.trap(ewords.DuplicateUser)
+ return self.lookupUser(name)
+ return self.createUser(name).addErrback(ebUser)
+ return self.lookupUser(name)
+
+
+ def createUser(self, name):
+ assert isinstance(name, unicode)
+ def cbLookup(user):
+ return failure.Failure(ewords.DuplicateUser(name))
+ def ebLookup(err):
+ err.trap(ewords.NoSuchUser)
+ return self.userFactory(name)
+
+ name = name.lower()
+ d = self.lookupUser(name)
+ d.addCallbacks(cbLookup, ebLookup)
+ d.addCallback(self.addUser)
+ return d
+
+
+ def createGroup(self, name):
+ assert isinstance(name, unicode)
+ def cbLookup(group):
+ return failure.Failure(ewords.DuplicateGroup(name))
+ def ebLookup(err):
+ err.trap(ewords.NoSuchGroup)
+ return self.groupFactory(name)
+
+ name = name.lower()
+ d = self.lookupGroup(name)
+ d.addCallbacks(cbLookup, ebLookup)
+ d.addCallback(self.addGroup)
+ return d
+
+
+class InMemoryWordsRealm(WordsRealm):
+ def __init__(self, *a, **kw):
+ super(InMemoryWordsRealm, self).__init__(*a, **kw)
+ self.users = {}
+ self.groups = {}
+
+
+ def itergroups(self):
+ return defer.succeed(self.groups.itervalues())
+
+
+ def addUser(self, user):
+ if user.name in self.users:
+ return defer.fail(failure.Failure(ewords.DuplicateUser()))
+ self.users[user.name] = user
+ return defer.succeed(user)
+
+
+ def addGroup(self, group):
+ if group.name in self.groups:
+ return defer.fail(failure.Failure(ewords.DuplicateGroup()))
+ self.groups[group.name] = group
+ return defer.succeed(group)
+
+
+ def lookupUser(self, name):
+ assert isinstance(name, unicode)
+ name = name.lower()
+ try:
+ user = self.users[name]
+ except KeyError:
+ return defer.fail(failure.Failure(ewords.NoSuchUser(name)))
+ else:
+ return defer.succeed(user)
+
+
+ def lookupGroup(self, name):
+ assert isinstance(name, unicode)
+ name = name.lower()
+ try:
+ group = self.groups[name]
+ except KeyError:
+ return defer.fail(failure.Failure(ewords.NoSuchGroup(name)))
+ else:
+ return defer.succeed(group)
+
+__all__ = [
+ 'Group', 'User',
+
+ 'WordsRealm', 'InMemoryWordsRealm',
+ ]
diff --git a/twisted/words/tap.py b/twisted/words/tap.py
new file mode 100644
index 0000000..c0ba9fd
--- /dev/null
+++ b/twisted/words/tap.py
@@ -0,0 +1,74 @@
+# -*- test-case-name: twisted.words.test.test_tap -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+"""
+Shiny new words service maker
+"""
+
+import sys, socket
+
+from twisted.application import strports
+from twisted.application.service import MultiService
+from twisted.python import usage
+from twisted import plugin
+
+from twisted.words import iwords, service
+from twisted.cred import checkers, credentials, portal, strcred
+
+class Options(usage.Options, strcred.AuthOptionMixin):
+ supportedInterfaces = [credentials.IUsernamePassword]
+ optParameters = [
+ ('hostname', None, socket.gethostname(),
+ 'Name of this server; purely an informative')]
+
+ compData = usage.Completions(multiUse=["group"])
+
+ interfacePlugins = {}
+ plg = None
+ for plg in plugin.getPlugins(iwords.IProtocolPlugin):
+ assert plg.name not in interfacePlugins
+ interfacePlugins[plg.name] = plg
+ optParameters.append((
+ plg.name + '-port',
+ None, None,
+ 'strports description of the port to bind for the ' + plg.name + ' server'))
+ del plg
+
+ def __init__(self, *a, **kw):
+ usage.Options.__init__(self, *a, **kw)
+ self['groups'] = []
+
+ def opt_group(self, name):
+ """Specify a group which should exist
+ """
+ self['groups'].append(name.decode(sys.stdin.encoding))
+
+ def opt_passwd(self, filename):
+ """
+ Name of a passwd-style file. (This is for
+ backwards-compatibility only; you should use the --auth
+ command instead.)
+ """
+ self.addChecker(checkers.FilePasswordDB(filename))
+
+def makeService(config):
+ credCheckers = config.get('credCheckers', [])
+ wordsRealm = service.InMemoryWordsRealm(config['hostname'])
+ wordsPortal = portal.Portal(wordsRealm, credCheckers)
+
+ msvc = MultiService()
+
+ # XXX Attribute lookup on config is kind of bad - hrm.
+ for plgName in config.interfacePlugins:
+ port = config.get(plgName + '-port')
+ if port is not None:
+ factory = config.interfacePlugins[plgName].getFactory(wordsRealm, wordsPortal)
+ svc = strports.service(port, factory)
+ svc.setServiceParent(msvc)
+
+ # This is bogus. createGroup is async. makeService must be
+ # allowed to return a Deferred or some crap.
+ for g in config['groups']:
+ wordsRealm.createGroup(g)
+
+ return msvc
diff --git a/twisted/words/test/__init__.py b/twisted/words/test/__init__.py
new file mode 100644
index 0000000..d599f20
--- /dev/null
+++ b/twisted/words/test/__init__.py
@@ -0,0 +1 @@
+"Words tests"
diff --git a/twisted/words/test/test_basechat.py b/twisted/words/test/test_basechat.py
new file mode 100644
index 0000000..8347d30
--- /dev/null
+++ b/twisted/words/test/test_basechat.py
@@ -0,0 +1,68 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.im.basechat}.
+"""
+
+from twisted.trial import unittest
+from twisted.words.im import basechat, basesupport
+
+
+class ChatUITests(unittest.TestCase):
+ """
+ Tests for the L{basechat.ChatUI} chat client.
+ """
+ def setUp(self):
+ self.ui = basechat.ChatUI()
+ self.account = basesupport.AbstractAccount("fooAccount", False, "foo",
+ "password", "host", "port")
+ self.person = basesupport.AbstractPerson("foo", self.account)
+
+
+ def test_contactChangedNickNoKey(self):
+ """
+ L{basechat.ChatUI.contactChangedNick} on an
+ L{twisted.words.im.interfaces.IPerson} who doesn't have an account
+ associated with the L{basechat.ChatUI} instance has no effect.
+ """
+ self.assertEqual(self.person.name, "foo")
+ self.assertEqual(self.person.account, self.account)
+
+ self.ui.contactChangedNick(self.person, "bar")
+ self.assertEqual(self.person.name, "foo")
+ self.assertEqual(self.person.account, self.account)
+
+
+ def test_contactChangedNickNoConversation(self):
+ """
+ L{basechat.ChatUI.contactChangedNick} changes the name for an
+ L{twisted.words.im.interfaces.IPerson}.
+ """
+ self.ui.persons[self.person.name, self.person.account] = self.person
+
+ self.assertEqual(self.person.name, "foo")
+ self.assertEqual(self.person.account, self.account)
+
+ self.ui.contactChangedNick(self.person, "bar")
+ self.assertEqual(self.person.name, "bar")
+ self.assertEqual(self.person.account, self.account)
+
+
+ def test_contactChangedNickHasConversation(self):
+ """
+ If an L{twisted.words.im.interfaces.IPerson} is in a
+ L{basechat.Conversation}, L{basechat.ChatUI.contactChangedNick} causes a
+ name change for that person in both the L{basechat.Conversation} and the
+ L{basechat.ChatUI}.
+ """
+ self.ui.persons[self.person.name, self.person.account] = self.person
+ conversation = basechat.Conversation(self.person, self.ui)
+ self.ui.conversations[self.person] = conversation
+
+ self.assertEqual(self.person.name, "foo")
+ self.assertEqual(self.person.account, self.account)
+
+ self.ui.contactChangedNick(self.person, "bar")
+ self.assertEqual(self.person.name, "bar")
+ self.assertEqual(self.person.account, self.account)
diff --git a/twisted/words/test/test_basesupport.py b/twisted/words/test/test_basesupport.py
new file mode 100644
index 0000000..3a81963
--- /dev/null
+++ b/twisted/words/test/test_basesupport.py
@@ -0,0 +1,97 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+from twisted.words.im import basesupport
+from twisted.internet import error, defer
+
+class DummyAccount(basesupport.AbstractAccount):
+ """
+ An account object that will do nothing when asked to start to log on.
+ """
+
+ loginHasFailed = False
+ loginCallbackCalled = False
+
+ def _startLogOn(self, *args):
+ """
+ Set self.loginDeferred to the same as the deferred returned, allowing a
+ testcase to .callback or .errback.
+
+ @return: A deferred.
+ """
+ self.loginDeferred = defer.Deferred()
+ return self.loginDeferred
+
+ def _loginFailed(self, result):
+ self.loginHasFailed = True
+ return basesupport.AbstractAccount._loginFailed(self, result)
+
+ def _cb_logOn(self, result):
+ self.loginCallbackCalled = True
+ return basesupport.AbstractAccount._cb_logOn(self, result)
+
+class DummyUI(object):
+ """
+ Provide just the interface required to be passed to AbstractAccount.logOn.
+ """
+ clientRegistered = False
+
+ def registerAccountClient(self, result):
+ self.clientRegistered = True
+
+class ClientMsgTests(unittest.TestCase):
+ def makeUI(self):
+ return DummyUI()
+
+ def makeAccount(self):
+ return DummyAccount('la', False, 'la', None, 'localhost', 6667)
+
+ def test_connect(self):
+ """
+ Test that account.logOn works, and it calls the right callback when a
+ connection is established.
+ """
+ account = self.makeAccount()
+ ui = self.makeUI()
+ d = account.logOn(ui)
+ account.loginDeferred.callback(None)
+
+ def check(result):
+ self.assert_(not account.loginHasFailed,
+ "Login shouldn't have failed")
+ self.assert_(account.loginCallbackCalled,
+ "We should be logged in")
+ d.addCallback(check)
+ return d
+
+ def test_failedConnect(self):
+ """
+ Test that account.logOn works, and it calls the right callback when a
+ connection is established.
+ """
+ account = self.makeAccount()
+ ui = self.makeUI()
+ d = account.logOn(ui)
+ account.loginDeferred.errback(Exception())
+
+ def err(reason):
+ self.assert_(account.loginHasFailed, "Login should have failed")
+ self.assert_(not account.loginCallbackCalled,
+ "We shouldn't be logged in")
+ self.assert_(not ui.clientRegistered,
+ "Client shouldn't be registered in the UI")
+ cb = lambda r: self.assert_(False, "Shouldn't get called back")
+ d.addCallbacks(cb, err)
+ return d
+
+ def test_alreadyConnecting(self):
+ """
+ Test that it can fail sensibly when someone tried to connect before
+ we did.
+ """
+ account = self.makeAccount()
+ ui = self.makeUI()
+ account.logOn(ui)
+ self.assertRaises(error.ConnectError, account.logOn, ui)
+
diff --git a/twisted/words/test/test_domish.py b/twisted/words/test/test_domish.py
new file mode 100644
index 0000000..275afb7
--- /dev/null
+++ b/twisted/words/test/test_domish.py
@@ -0,0 +1,434 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.xish.domish}, a DOM-like library for XMPP.
+"""
+
+from twisted.trial import unittest
+from twisted.words.xish import domish
+
+
+class DomishTestCase(unittest.TestCase):
+ def testEscaping(self):
+ s = "&<>'\""
+ self.assertEqual(domish.escapeToXml(s), "&amp;&lt;&gt;'\"")
+ self.assertEqual(domish.escapeToXml(s, 1), "&amp;&lt;&gt;&apos;&quot;")
+
+ def testNamespaceObject(self):
+ ns = domish.Namespace("testns")
+ self.assertEqual(ns.foo, ("testns", "foo"))
+
+ def testElementInit(self):
+ e = domish.Element((None, "foo"))
+ self.assertEqual(e.name, "foo")
+ self.assertEqual(e.uri, None)
+ self.assertEqual(e.defaultUri, None)
+ self.assertEqual(e.parent, None)
+
+ e = domish.Element(("", "foo"))
+ self.assertEqual(e.name, "foo")
+ self.assertEqual(e.uri, "")
+ self.assertEqual(e.defaultUri, "")
+ self.assertEqual(e.parent, None)
+
+ e = domish.Element(("testns", "foo"))
+ self.assertEqual(e.name, "foo")
+ self.assertEqual(e.uri, "testns")
+ self.assertEqual(e.defaultUri, "testns")
+ self.assertEqual(e.parent, None)
+
+ e = domish.Element(("testns", "foo"), "test2ns")
+ self.assertEqual(e.name, "foo")
+ self.assertEqual(e.uri, "testns")
+ self.assertEqual(e.defaultUri, "test2ns")
+
+ def testChildOps(self):
+ e = domish.Element(("testns", "foo"))
+ e.addContent("somecontent")
+ b2 = e.addElement(("testns2", "bar2"))
+ e["attrib1"] = "value1"
+ e[("testns2", "attrib2")] = "value2"
+ e.addElement("bar")
+ e.addElement("bar")
+ e.addContent("abc")
+ e.addContent("123")
+
+ # Check content merging
+ self.assertEqual(e.children[-1], "abc123")
+
+ # Check str()/content extraction
+ self.assertEqual(str(e), "somecontent")
+
+ # Check direct child accessor
+ self.assertEqual(e.bar2, b2)
+ e.bar2.addContent("subcontent")
+ e.bar2["bar2value"] = "somevalue"
+
+ # Check child ops
+ self.assertEqual(e.children[1], e.bar2)
+ self.assertEqual(e.children[2], e.bar)
+
+ # Check attribute ops
+ self.assertEqual(e["attrib1"], "value1")
+ del e["attrib1"]
+ self.assertEqual(e.hasAttribute("attrib1"), 0)
+ self.assertEqual(e.hasAttribute("attrib2"), 0)
+ self.assertEqual(e[("testns2", "attrib2")], "value2")
+
+
+ def test_elements(self):
+ """
+ Calling C{elements} without arguments on a L{domish.Element} returns
+ all child elements, whatever the qualfied name.
+ """
+ e = domish.Element((u"testns", u"foo"))
+ c1 = e.addElement(u"name")
+ c2 = e.addElement((u"testns2", u"baz"))
+ c3 = e.addElement(u"quux")
+ c4 = e.addElement((u"testns", u"name"))
+
+ elts = list(e.elements())
+
+ self.assertIn(c1, elts)
+ self.assertIn(c2, elts)
+ self.assertIn(c3, elts)
+ self.assertIn(c4, elts)
+
+
+ def test_elementsWithQN(self):
+ """
+ Calling C{elements} with a namespace and local name on a
+ L{domish.Element} returns all child elements with that qualified name.
+ """
+ e = domish.Element((u"testns", u"foo"))
+ c1 = e.addElement(u"name")
+ c2 = e.addElement((u"testns2", u"baz"))
+ c3 = e.addElement(u"quux")
+ c4 = e.addElement((u"testns", u"name"))
+
+ elts = list(e.elements(u"testns", u"name"))
+
+ self.assertIn(c1, elts)
+ self.assertNotIn(c2, elts)
+ self.assertNotIn(c3, elts)
+ self.assertIn(c4, elts)
+
+
+
+class DomishStreamTestsMixin:
+ """
+ Mixin defining tests for different stream implementations.
+
+ @ivar streamClass: A no-argument callable which will be used to create an
+ XML parser which can produce a stream of elements from incremental
+ input.
+ """
+ def setUp(self):
+ self.doc_started = False
+ self.doc_ended = False
+ self.root = None
+ self.elements = []
+ self.stream = self.streamClass()
+ self.stream.DocumentStartEvent = self._docStarted
+ self.stream.ElementEvent = self.elements.append
+ self.stream.DocumentEndEvent = self._docEnded
+
+ def _docStarted(self, root):
+ self.root = root
+ self.doc_started = True
+
+ def _docEnded(self):
+ self.doc_ended = True
+
+ def doTest(self, xml):
+ self.stream.parse(xml)
+
+ def testHarness(self):
+ xml = "<root><child/><child2/></root>"
+ self.stream.parse(xml)
+ self.assertEqual(self.doc_started, True)
+ self.assertEqual(self.root.name, 'root')
+ self.assertEqual(self.elements[0].name, 'child')
+ self.assertEqual(self.elements[1].name, 'child2')
+ self.assertEqual(self.doc_ended, True)
+
+ def testBasic(self):
+ xml = "<stream:stream xmlns:stream='etherx' xmlns='jabber'>\n" + \
+ " <message to='bar'>" + \
+ " <x xmlns='xdelay'>some&amp;data&gt;</x>" + \
+ " </message>" + \
+ "</stream:stream>"
+
+ self.stream.parse(xml)
+ self.assertEqual(self.root.name, 'stream')
+ self.assertEqual(self.root.uri, 'etherx')
+ self.assertEqual(self.elements[0].name, 'message')
+ self.assertEqual(self.elements[0].uri, 'jabber')
+ self.assertEqual(self.elements[0]['to'], 'bar')
+ self.assertEqual(self.elements[0].x.uri, 'xdelay')
+ self.assertEqual(unicode(self.elements[0].x), 'some&data>')
+
+ def testNoRootNS(self):
+ xml = "<stream><error xmlns='etherx'/></stream>"
+
+ self.stream.parse(xml)
+ self.assertEqual(self.root.uri, '')
+ self.assertEqual(self.elements[0].uri, 'etherx')
+
+ def testNoDefaultNS(self):
+ xml = "<stream:stream xmlns:stream='etherx'><error/></stream:stream>"""
+
+ self.stream.parse(xml)
+ self.assertEqual(self.root.uri, 'etherx')
+ self.assertEqual(self.root.defaultUri, '')
+ self.assertEqual(self.elements[0].uri, '')
+ self.assertEqual(self.elements[0].defaultUri, '')
+
+ def testChildDefaultNS(self):
+ xml = "<root xmlns='testns'><child/></root>"
+
+ self.stream.parse(xml)
+ self.assertEqual(self.root.uri, 'testns')
+ self.assertEqual(self.elements[0].uri, 'testns')
+
+ def testEmptyChildNS(self):
+ xml = "<root xmlns='testns'><child1><child2 xmlns=''/></child1></root>"
+
+ self.stream.parse(xml)
+ self.assertEqual(self.elements[0].child2.uri, '')
+
+
+ def test_namespaceWithWhitespace(self):
+ """
+ Whitespace in an xmlns value is preserved in the resulting node's C{uri}
+ attribute.
+ """
+ xml = "<root xmlns:foo=' bar baz '><foo:bar foo:baz='quux'/></root>"
+ self.stream.parse(xml)
+ self.assertEqual(self.elements[0].uri, " bar baz ")
+ self.assertEqual(
+ self.elements[0].attributes, {(" bar baz ", "baz"): "quux"})
+
+
+ def testChildPrefix(self):
+ xml = "<root xmlns='testns' xmlns:foo='testns2'><foo:child/></root>"
+
+ self.stream.parse(xml)
+ self.assertEqual(self.root.localPrefixes['foo'], 'testns2')
+ self.assertEqual(self.elements[0].uri, 'testns2')
+
+ def testUnclosedElement(self):
+ self.assertRaises(domish.ParserError, self.stream.parse,
+ "<root><error></root>")
+
+ def test_namespaceReuse(self):
+ """
+ Test that reuse of namespaces does affect an element's serialization.
+
+ When one element uses a prefix for a certain namespace, this is
+ stored in the C{localPrefixes} attribute of the element. We want
+ to make sure that elements created after such use, won't have this
+ prefix end up in their C{localPrefixes} attribute, too.
+ """
+
+ xml = """<root>
+ <foo:child1 xmlns:foo='testns'/>
+ <child2 xmlns='testns'/>
+ </root>"""
+
+ self.stream.parse(xml)
+ self.assertEqual('child1', self.elements[0].name)
+ self.assertEqual('testns', self.elements[0].uri)
+ self.assertEqual('', self.elements[0].defaultUri)
+ self.assertEqual({'foo': 'testns'}, self.elements[0].localPrefixes)
+ self.assertEqual('child2', self.elements[1].name)
+ self.assertEqual('testns', self.elements[1].uri)
+ self.assertEqual('testns', self.elements[1].defaultUri)
+ self.assertEqual({}, self.elements[1].localPrefixes)
+
+
+
+class DomishExpatStreamTestCase(DomishStreamTestsMixin, unittest.TestCase):
+ """
+ Tests for L{domish.ExpatElementStream}, the expat-based element stream
+ implementation.
+ """
+ streamClass = domish.ExpatElementStream
+
+ try:
+ import pyexpat
+ except ImportError:
+ skip = "pyexpat is required for ExpatElementStream tests."
+
+
+
+class DomishSuxStreamTestCase(DomishStreamTestsMixin, unittest.TestCase):
+ """
+ Tests for L{domish.SuxElementStream}, the L{twisted.web.sux}-based element
+ stream implementation.
+ """
+ streamClass = domish.SuxElementStream
+
+ if domish.SuxElementStream is None:
+ skip = "twisted.web is required for SuxElementStream tests."
+
+
+
+class SerializerTests(unittest.TestCase):
+ def testNoNamespace(self):
+ e = domish.Element((None, "foo"))
+ self.assertEqual(e.toXml(), "<foo/>")
+ self.assertEqual(e.toXml(closeElement = 0), "<foo>")
+
+ def testDefaultNamespace(self):
+ e = domish.Element(("testns", "foo"))
+ self.assertEqual(e.toXml(), "<foo xmlns='testns'/>")
+
+ def testOtherNamespace(self):
+ e = domish.Element(("testns", "foo"), "testns2")
+ self.assertEqual(e.toXml({'testns': 'bar'}),
+ "<bar:foo xmlns:bar='testns' xmlns='testns2'/>")
+
+ def testChildDefaultNamespace(self):
+ e = domish.Element(("testns", "foo"))
+ e.addElement("bar")
+ self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
+
+ def testChildSameNamespace(self):
+ e = domish.Element(("testns", "foo"))
+ e.addElement(("testns", "bar"))
+ self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
+
+ def testChildSameDefaultNamespace(self):
+ e = domish.Element(("testns", "foo"))
+ e.addElement("bar", "testns")
+ self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
+
+ def testChildOtherDefaultNamespace(self):
+ e = domish.Element(("testns", "foo"))
+ e.addElement(("testns2", "bar"), 'testns2')
+ self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar xmlns='testns2'/></foo>")
+
+ def testOnlyChildDefaultNamespace(self):
+ e = domish.Element((None, "foo"))
+ e.addElement(("ns2", "bar"), 'ns2')
+ self.assertEqual(e.toXml(), "<foo><bar xmlns='ns2'/></foo>")
+
+ def testOnlyChildDefaultNamespace2(self):
+ e = domish.Element((None, "foo"))
+ e.addElement("bar")
+ self.assertEqual(e.toXml(), "<foo><bar/></foo>")
+
+ def testChildInDefaultNamespace(self):
+ e = domish.Element(("testns", "foo"), "testns2")
+ e.addElement(("testns2", "bar"))
+ self.assertEqual(e.toXml(), "<xn0:foo xmlns:xn0='testns' xmlns='testns2'><bar/></xn0:foo>")
+
+ def testQualifiedAttribute(self):
+ e = domish.Element((None, "foo"),
+ attribs = {("testns2", "bar"): "baz"})
+ self.assertEqual(e.toXml(), "<foo xmlns:xn0='testns2' xn0:bar='baz'/>")
+
+ def testQualifiedAttributeDefaultNS(self):
+ e = domish.Element(("testns", "foo"),
+ attribs = {("testns", "bar"): "baz"})
+ self.assertEqual(e.toXml(), "<foo xmlns='testns' xmlns:xn0='testns' xn0:bar='baz'/>")
+
+ def testTwoChilds(self):
+ e = domish.Element(('', "foo"))
+ child1 = e.addElement(("testns", "bar"), "testns2")
+ child1.addElement(('testns2', 'quux'))
+ child2 = e.addElement(("testns3", "baz"), "testns4")
+ child2.addElement(('testns', 'quux'))
+ self.assertEqual(e.toXml(), "<foo><xn0:bar xmlns:xn0='testns' xmlns='testns2'><quux/></xn0:bar><xn1:baz xmlns:xn1='testns3' xmlns='testns4'><xn0:quux xmlns:xn0='testns'/></xn1:baz></foo>")
+
+ def testXMLNamespace(self):
+ e = domish.Element((None, "foo"),
+ attribs = {("http://www.w3.org/XML/1998/namespace",
+ "lang"): "en_US"})
+ self.assertEqual(e.toXml(), "<foo xml:lang='en_US'/>")
+
+ def testQualifiedAttributeGivenListOfPrefixes(self):
+ e = domish.Element((None, "foo"),
+ attribs = {("testns2", "bar"): "baz"})
+ self.assertEqual(e.toXml({"testns2": "qux"}),
+ "<foo xmlns:qux='testns2' qux:bar='baz'/>")
+
+ def testNSPrefix(self):
+ e = domish.Element((None, "foo"),
+ attribs = {("testns2", "bar"): "baz"})
+ c = e.addElement(("testns2", "qux"))
+ c[("testns2", "bar")] = "quux"
+
+ self.assertEqual(e.toXml(), "<foo xmlns:xn0='testns2' xn0:bar='baz'><xn0:qux xn0:bar='quux'/></foo>")
+
+ def testDefaultNSPrefix(self):
+ e = domish.Element((None, "foo"),
+ attribs = {("testns2", "bar"): "baz"})
+ c = e.addElement(("testns2", "qux"))
+ c[("testns2", "bar")] = "quux"
+ c.addElement('foo')
+
+ self.assertEqual(e.toXml(), "<foo xmlns:xn0='testns2' xn0:bar='baz'><xn0:qux xn0:bar='quux'><xn0:foo/></xn0:qux></foo>")
+
+ def testPrefixScope(self):
+ e = domish.Element(('testns', 'foo'))
+
+ self.assertEqual(e.toXml(prefixes={'testns': 'bar'},
+ prefixesInScope=['bar']),
+ "<bar:foo/>")
+
+ def testLocalPrefixes(self):
+ e = domish.Element(('testns', 'foo'), localPrefixes={'bar': 'testns'})
+ self.assertEqual(e.toXml(), "<bar:foo xmlns:bar='testns'/>")
+
+ def testLocalPrefixesWithChild(self):
+ e = domish.Element(('testns', 'foo'), localPrefixes={'bar': 'testns'})
+ e.addElement('baz')
+ self.assertIdentical(e.baz.defaultUri, None)
+ self.assertEqual(e.toXml(), "<bar:foo xmlns:bar='testns'><baz/></bar:foo>")
+
+ def test_prefixesReuse(self):
+ """
+ Test that prefixes passed to serialization are not modified.
+
+ This test makes sure that passing a dictionary of prefixes repeatedly
+ to C{toXml} of elements does not cause serialization errors. A
+ previous implementation changed the passed in dictionary internally,
+ causing havoc later on.
+ """
+ prefixes = {'testns': 'foo'}
+
+ # test passing of dictionary
+ s = domish.SerializerClass(prefixes=prefixes)
+ self.assertNotIdentical(prefixes, s.prefixes)
+
+ # test proper serialization on prefixes reuse
+ e = domish.Element(('testns2', 'foo'),
+ localPrefixes={'quux': 'testns2'})
+ self.assertEqual("<quux:foo xmlns:quux='testns2'/>",
+ e.toXml(prefixes=prefixes))
+ e = domish.Element(('testns2', 'foo'))
+ self.assertEqual("<foo xmlns='testns2'/>",
+ e.toXml(prefixes=prefixes))
+
+ def testRawXMLSerialization(self):
+ e = domish.Element((None, "foo"))
+ e.addRawXml("<abc123>")
+ # The testcase below should NOT generate valid XML -- that's
+ # the whole point of using the raw XML call -- it's the callers
+ # responsiblity to ensure that the data inserted is valid
+ self.assertEqual(e.toXml(), "<foo><abc123></foo>")
+
+ def testRawXMLWithUnicodeSerialization(self):
+ e = domish.Element((None, "foo"))
+ e.addRawXml(u"<degree>\u00B0</degree>")
+ self.assertEqual(e.toXml(), u"<foo><degree>\u00B0</degree></foo>")
+
+ def testUnicodeSerialization(self):
+ e = domish.Element((None, "foo"))
+ e["test"] = u"my value\u0221e"
+ e.addContent(u"A degree symbol...\u00B0")
+ self.assertEqual(e.toXml(),
+ u"<foo test='my value\u0221e'>A degree symbol...\u00B0</foo>")
diff --git a/twisted/words/test/test_irc.py b/twisted/words/test/test_irc.py
new file mode 100644
index 0000000..46af3c7
--- /dev/null
+++ b/twisted/words/test/test_irc.py
@@ -0,0 +1,1898 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.irc}.
+"""
+
+import time
+
+from twisted.trial import unittest
+from twisted.trial.unittest import TestCase
+from twisted.words.protocols import irc
+from twisted.words.protocols.irc import IRCClient
+from twisted.internet import protocol, task
+from twisted.test.proto_helpers import StringTransport, StringIOWithoutClosing
+
+
+
+class ModeParsingTests(unittest.TestCase):
+ """
+ Tests for L{twisted.words.protocols.irc.parseModes}.
+ """
+ paramModes = ('klb', 'b')
+
+
+ def test_emptyModes(self):
+ """
+ Parsing an empty mode string raises L{irc.IRCBadModes}.
+ """
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '', [])
+
+
+ def test_emptyModeSequence(self):
+ """
+ Parsing a mode string that contains an empty sequence (either a C{+} or
+ C{-} followed directly by another C{+} or C{-}, or not followed by
+ anything at all) raises L{irc.IRCBadModes}.
+ """
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '++k', [])
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '-+k', [])
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', [])
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', [])
+
+
+ def test_malformedModes(self):
+ """
+ Parsing a mode string that does not start with C{+} or C{-} raises
+ L{irc.IRCBadModes}.
+ """
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, 'foo', [])
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '%', [])
+
+
+ def test_nullModes(self):
+ """
+ Parsing a mode string that contains no mode characters raises
+ L{irc.IRCBadModes}.
+ """
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', [])
+ self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', [])
+
+
+ def test_singleMode(self):
+ """
+ Parsing a single mode setting with no parameters results in that mode,
+ with no parameters, in the "added" direction and no modes in the
+ "removed" direction.
+ """
+ added, removed = irc.parseModes('+s', [])
+ self.assertEqual(added, [('s', None)])
+ self.assertEqual(removed, [])
+
+ added, removed = irc.parseModes('-s', [])
+ self.assertEqual(added, [])
+ self.assertEqual(removed, [('s', None)])
+
+
+ def test_singleDirection(self):
+ """
+ Parsing a single-direction mode setting with multiple modes and no
+ parameters, results in all modes falling into the same direction group.
+ """
+ added, removed = irc.parseModes('+stn', [])
+ self.assertEqual(added, [('s', None),
+ ('t', None),
+ ('n', None)])
+ self.assertEqual(removed, [])
+
+ added, removed = irc.parseModes('-nt', [])
+ self.assertEqual(added, [])
+ self.assertEqual(removed, [('n', None),
+ ('t', None)])
+
+
+ def test_multiDirection(self):
+ """
+ Parsing a multi-direction mode setting with no parameters.
+ """
+ added, removed = irc.parseModes('+s-n+ti', [])
+ self.assertEqual(added, [('s', None),
+ ('t', None),
+ ('i', None)])
+ self.assertEqual(removed, [('n', None)])
+
+
+ def test_consecutiveDirection(self):
+ """
+ Parsing a multi-direction mode setting containing two consecutive mode
+ sequences with the same direction results in the same result as if
+ there were only one mode sequence in the same direction.
+ """
+ added, removed = irc.parseModes('+sn+ti', [])
+ self.assertEqual(added, [('s', None),
+ ('n', None),
+ ('t', None),
+ ('i', None)])
+ self.assertEqual(removed, [])
+
+
+ def test_mismatchedParams(self):
+ """
+ If the number of mode parameters does not match the number of modes
+ expecting parameters, L{irc.IRCBadModes} is raised.
+ """
+ self.assertRaises(irc.IRCBadModes,
+ irc.parseModes,
+ '+k', [],
+ self.paramModes)
+ self.assertRaises(irc.IRCBadModes,
+ irc.parseModes,
+ '+kl', ['foo', '10', 'lulz_extra_param'],
+ self.paramModes)
+
+
+ def test_parameters(self):
+ """
+ Modes which require parameters are parsed and paired with their relevant
+ parameter, modes which do not require parameters do not consume any of
+ the parameters.
+ """
+ added, removed = irc.parseModes(
+ '+klbb',
+ ['somekey', '42', 'nick!user@host', 'other!*@*'],
+ self.paramModes)
+ self.assertEqual(added, [('k', 'somekey'),
+ ('l', '42'),
+ ('b', 'nick!user@host'),
+ ('b', 'other!*@*')])
+ self.assertEqual(removed, [])
+
+ added, removed = irc.parseModes(
+ '-klbb',
+ ['nick!user@host', 'other!*@*'],
+ self.paramModes)
+ self.assertEqual(added, [])
+ self.assertEqual(removed, [('k', None),
+ ('l', None),
+ ('b', 'nick!user@host'),
+ ('b', 'other!*@*')])
+
+ # Mix a no-argument mode in with argument modes.
+ added, removed = irc.parseModes(
+ '+knbb',
+ ['somekey', 'nick!user@host', 'other!*@*'],
+ self.paramModes)
+ self.assertEqual(added, [('k', 'somekey'),
+ ('n', None),
+ ('b', 'nick!user@host'),
+ ('b', 'other!*@*')])
+ self.assertEqual(removed, [])
+
+
+
+stringSubjects = [
+ "Hello, this is a nice string with no complications.",
+ "xargs%(NUL)smight%(NUL)slike%(NUL)sthis" % {'NUL': irc.NUL },
+ "embedded%(CR)snewline%(CR)s%(NL)sFUN%(NL)s" % {'CR': irc.CR,
+ 'NL': irc.NL},
+ "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" % {'X': irc.X_QUOTE,
+ 'M': irc.M_QUOTE}
+ ]
+
+
+class QuotingTest(unittest.TestCase):
+ def test_lowquoteSanity(self):
+ """
+ Testing client-server level quote/dequote.
+ """
+ for s in stringSubjects:
+ self.assertEqual(s, irc.lowDequote(irc.lowQuote(s)))
+
+
+ def test_ctcpquoteSanity(self):
+ """
+ Testing CTCP message level quote/dequote.
+ """
+ for s in stringSubjects:
+ self.assertEqual(s, irc.ctcpDequote(irc.ctcpQuote(s)))
+
+
+
+class Dispatcher(irc._CommandDispatcherMixin):
+ """
+ A dispatcher that exposes one known command and handles unknown commands.
+ """
+ prefix = 'disp'
+
+ def disp_working(self, a, b):
+ """
+ A known command that returns its input.
+ """
+ return a, b
+
+
+ def disp_unknown(self, name, a, b):
+ """
+ Handle unknown commands by returning their name and inputs.
+ """
+ return name, a, b
+
+
+
+class DispatcherTests(unittest.TestCase):
+ """
+ Tests for L{irc._CommandDispatcherMixin}.
+ """
+ def test_dispatch(self):
+ """
+ Dispatching a command invokes the correct handler.
+ """
+ disp = Dispatcher()
+ args = (1, 2)
+ res = disp.dispatch('working', *args)
+ self.assertEqual(res, args)
+
+
+ def test_dispatchUnknown(self):
+ """
+ Dispatching an unknown command invokes the default handler.
+ """
+ disp = Dispatcher()
+ name = 'missing'
+ args = (1, 2)
+ res = disp.dispatch(name, *args)
+ self.assertEqual(res, (name,) + args)
+
+
+ def test_dispatchMissingUnknown(self):
+ """
+ Dispatching an unknown command, when no default handler is present,
+ results in an exception being raised.
+ """
+ disp = Dispatcher()
+ disp.disp_unknown = None
+ self.assertRaises(irc.UnhandledCommand, disp.dispatch, 'bar')
+
+
+
+class ServerSupportedFeatureTests(unittest.TestCase):
+ """
+ Tests for L{ServerSupportedFeatures} and related functions.
+ """
+ def test_intOrDefault(self):
+ """
+ L{_intOrDefault} converts values to C{int} if possible, otherwise
+ returns a default value.
+ """
+ self.assertEqual(irc._intOrDefault(None), None)
+ self.assertEqual(irc._intOrDefault([]), None)
+ self.assertEqual(irc._intOrDefault(''), None)
+ self.assertEqual(irc._intOrDefault('hello', 5), 5)
+ self.assertEqual(irc._intOrDefault('123'), 123)
+ self.assertEqual(irc._intOrDefault(123), 123)
+
+
+ def test_splitParam(self):
+ """
+ L{ServerSupportedFeatures._splitParam} splits ISUPPORT parameters
+ into key and values. Parameters without a separator are split into a
+ key and a list containing only the empty string. Escaped parameters
+ are unescaped.
+ """
+ params = [('FOO', ('FOO', [''])),
+ ('FOO=', ('FOO', [''])),
+ ('FOO=1', ('FOO', ['1'])),
+ ('FOO=1,2,3', ('FOO', ['1', '2', '3'])),
+ ('FOO=A\\x20B', ('FOO', ['A B'])),
+ ('FOO=\\x5Cx', ('FOO', ['\\x'])),
+ ('FOO=\\', ('FOO', ['\\'])),
+ ('FOO=\\n', ('FOO', ['\\n']))]
+
+ _splitParam = irc.ServerSupportedFeatures._splitParam
+
+ for param, expected in params:
+ res = _splitParam(param)
+ self.assertEqual(res, expected)
+
+ self.assertRaises(ValueError, _splitParam, 'FOO=\\x')
+ self.assertRaises(ValueError, _splitParam, 'FOO=\\xNN')
+ self.assertRaises(ValueError, _splitParam, 'FOO=\\xN')
+ self.assertRaises(ValueError, _splitParam, 'FOO=\\x20\\x')
+
+
+ def test_splitParamArgs(self):
+ """
+ L{ServerSupportedFeatures._splitParamArgs} splits ISUPPORT parameter
+ arguments into key and value. Arguments without a separator are
+ split into a key and an empty string.
+ """
+ res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C:', 'D'])
+ self.assertEqual(res, [('A', '1'),
+ ('B', '2'),
+ ('C', ''),
+ ('D', '')])
+
+
+ def test_splitParamArgsProcessor(self):
+ """
+ L{ServerSupportedFeatures._splitParamArgs} uses the argument processor
+ passed to to convert ISUPPORT argument values to some more suitable
+ form.
+ """
+ res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C'],
+ irc._intOrDefault)
+ self.assertEqual(res, [('A', 1),
+ ('B', 2),
+ ('C', None)])
+
+
+ def test_parsePrefixParam(self):
+ """
+ L{ServerSupportedFeatures._parsePrefixParam} parses the ISUPPORT PREFIX
+ parameter into a mapping from modes to prefix symbols, returns
+ C{None} if there is no parseable prefix parameter or raises
+ C{ValueError} if the prefix parameter is malformed.
+ """
+ _parsePrefixParam = irc.ServerSupportedFeatures._parsePrefixParam
+ self.assertEqual(_parsePrefixParam(''), None)
+ self.assertRaises(ValueError, _parsePrefixParam, 'hello')
+ self.assertEqual(_parsePrefixParam('(ov)@+'),
+ {'o': ('@', 0),
+ 'v': ('+', 1)})
+
+
+ def test_parseChanModesParam(self):
+ """
+ L{ServerSupportedFeatures._parseChanModesParam} parses the ISUPPORT
+ CHANMODES parameter into a mapping from mode categories to mode
+ characters. Passing fewer than 4 parameters results in the empty string
+ for the relevant categories. Passing more than 4 parameters raises
+ C{ValueError}.
+ """
+ _parseChanModesParam = irc.ServerSupportedFeatures._parseChanModesParam
+ self.assertEqual(
+ _parseChanModesParam([]),
+ {'addressModes': '',
+ 'param': '',
+ 'setParam': '',
+ 'noParam': ''})
+
+ self.assertEqual(
+ _parseChanModesParam(['b', 'k', 'l', 'imnpst']),
+ {'addressModes': 'b',
+ 'param': 'k',
+ 'setParam': 'l',
+ 'noParam': 'imnpst'})
+
+ self.assertEqual(
+ _parseChanModesParam(['b', 'k', 'l']),
+ {'addressModes': 'b',
+ 'param': 'k',
+ 'setParam': 'l',
+ 'noParam': ''})
+
+ self.assertRaises(
+ ValueError,
+ _parseChanModesParam, ['a', 'b', 'c', 'd', 'e'])
+
+
+ def test_parse(self):
+ """
+ L{ServerSupportedFeatures.parse} changes the internal state of the
+ instance to reflect the features indicated by the parsed ISUPPORT
+ parameters, including unknown parameters and unsetting previously set
+ parameters.
+ """
+ supported = irc.ServerSupportedFeatures()
+ supported.parse(['MODES=4',
+ 'CHANLIMIT=#:20,&:10',
+ 'INVEX',
+ 'EXCEPTS=Z',
+ 'UNKNOWN=A,B,C'])
+
+ self.assertEqual(supported.getFeature('MODES'), 4)
+ self.assertEqual(supported.getFeature('CHANLIMIT'),
+ [('#', 20),
+ ('&', 10)])
+ self.assertEqual(supported.getFeature('INVEX'), 'I')
+ self.assertEqual(supported.getFeature('EXCEPTS'), 'Z')
+ self.assertEqual(supported.getFeature('UNKNOWN'), ('A', 'B', 'C'))
+
+ self.assertTrue(supported.hasFeature('INVEX'))
+ supported.parse(['-INVEX'])
+ self.assertFalse(supported.hasFeature('INVEX'))
+ # Unsetting a previously unset parameter should not be a problem.
+ supported.parse(['-INVEX'])
+
+
+ def _parse(self, features):
+ """
+ Parse all specified features according to the ISUPPORT specifications.
+
+ @type features: C{list} of C{(featureName, value)}
+ @param features: Feature names and values to parse
+
+ @rtype: L{irc.ServerSupportedFeatures}
+ """
+ supported = irc.ServerSupportedFeatures()
+ features = ['%s=%s' % (name, value or '')
+ for name, value in features]
+ supported.parse(features)
+ return supported
+
+
+ def _parseFeature(self, name, value=None):
+ """
+ Parse a feature, with the given name and value, according to the
+ ISUPPORT specifications and return the parsed value.
+ """
+ supported = self._parse([(name, value)])
+ return supported.getFeature(name)
+
+
+ def _testIntOrDefaultFeature(self, name, default=None):
+ """
+ Perform some common tests on a feature known to use L{_intOrDefault}.
+ """
+ self.assertEqual(
+ self._parseFeature(name, None),
+ default)
+ self.assertEqual(
+ self._parseFeature(name, 'notanint'),
+ default)
+ self.assertEqual(
+ self._parseFeature(name, '42'),
+ 42)
+
+
+ def _testFeatureDefault(self, name, features=None):
+ """
+ Features known to have default values are reported as being present by
+ L{irc.ServerSupportedFeatures.hasFeature}, and their value defaults
+ correctly, when they don't appear in an ISUPPORT message.
+ """
+ default = irc.ServerSupportedFeatures()._features[name]
+
+ if features is None:
+ features = [('DEFINITELY_NOT', 'a_feature')]
+
+ supported = self._parse(features)
+ self.assertTrue(supported.hasFeature(name))
+ self.assertEqual(supported.getFeature(name), default)
+
+
+ def test_support_CHANMODES(self):
+ """
+ The CHANMODES ISUPPORT parameter is parsed into a C{dict} giving the
+ four mode categories, C{'addressModes'}, C{'param'}, C{'setParam'}, and
+ C{'noParam'}.
+ """
+ self._testFeatureDefault('CHANMODES')
+ self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,')])
+ self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,ha,ha')])
+
+ self.assertEqual(
+ self._parseFeature('CHANMODES', ''),
+ {'addressModes': '',
+ 'param': '',
+ 'setParam': '',
+ 'noParam': ''})
+
+ self.assertEqual(
+ self._parseFeature('CHANMODES', ',A'),
+ {'addressModes': '',
+ 'param': 'A',
+ 'setParam': '',
+ 'noParam': ''})
+
+ self.assertEqual(
+ self._parseFeature('CHANMODES', 'A,Bc,Def,Ghij'),
+ {'addressModes': 'A',
+ 'param': 'Bc',
+ 'setParam': 'Def',
+ 'noParam': 'Ghij'})
+
+
+ def test_support_IDCHAN(self):
+ """
+ The IDCHAN support parameter is parsed into a sequence of two-tuples
+ giving channel prefix and ID length pairs.
+ """
+ self.assertEqual(
+ self._parseFeature('IDCHAN', '!:5'),
+ [('!', '5')])
+
+
+ def test_support_MAXLIST(self):
+ """
+ The MAXLIST support parameter is parsed into a sequence of two-tuples
+ giving modes and their limits.
+ """
+ self.assertEqual(
+ self._parseFeature('MAXLIST', 'b:25,eI:50'),
+ [('b', 25), ('eI', 50)])
+ # A non-integer parameter argument results in None.
+ self.assertEqual(
+ self._parseFeature('MAXLIST', 'b:25,eI:50,a:3.1415'),
+ [('b', 25), ('eI', 50), ('a', None)])
+ self.assertEqual(
+ self._parseFeature('MAXLIST', 'b:25,eI:50,a:notanint'),
+ [('b', 25), ('eI', 50), ('a', None)])
+
+
+ def test_support_NETWORK(self):
+ """
+ The NETWORK support parameter is parsed as the network name, as
+ specified by the server.
+ """
+ self.assertEqual(
+ self._parseFeature('NETWORK', 'IRCNet'),
+ 'IRCNet')
+
+
+ def test_support_SAFELIST(self):
+ """
+ The SAFELIST support parameter is parsed into a boolean indicating
+ whether the safe "list" command is supported or not.
+ """
+ self.assertEqual(
+ self._parseFeature('SAFELIST'),
+ True)
+
+
+ def test_support_STATUSMSG(self):
+ """
+ The STATUSMSG support parameter is parsed into a string of channel
+ status that support the exclusive channel notice method.
+ """
+ self.assertEqual(
+ self._parseFeature('STATUSMSG', '@+'),
+ '@+')
+
+
+ def test_support_TARGMAX(self):
+ """
+ The TARGMAX support parameter is parsed into a dictionary, mapping
+ strings to integers, of the maximum number of targets for a particular
+ command.
+ """
+ self.assertEqual(
+ self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3'),
+ {'PRIVMSG': 4,
+ 'NOTICE': 3})
+ # A non-integer parameter argument results in None.
+ self.assertEqual(
+ self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:3.1415'),
+ {'PRIVMSG': 4,
+ 'NOTICE': 3,
+ 'KICK': None})
+ self.assertEqual(
+ self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:notanint'),
+ {'PRIVMSG': 4,
+ 'NOTICE': 3,
+ 'KICK': None})
+
+
+ def test_support_NICKLEN(self):
+ """
+ The NICKLEN support parameter is parsed into an integer value
+ indicating the maximum length of a nickname the client may use,
+ otherwise, if the parameter is missing or invalid, the default value
+ (as specified by RFC 1459) is used.
+ """
+ default = irc.ServerSupportedFeatures()._features['NICKLEN']
+ self._testIntOrDefaultFeature('NICKLEN', default)
+
+
+ def test_support_CHANNELLEN(self):
+ """
+ The CHANNELLEN support parameter is parsed into an integer value
+ indicating the maximum channel name length, otherwise, if the
+ parameter is missing or invalid, the default value (as specified by
+ RFC 1459) is used.
+ """
+ default = irc.ServerSupportedFeatures()._features['CHANNELLEN']
+ self._testIntOrDefaultFeature('CHANNELLEN', default)
+
+
+ def test_support_CHANTYPES(self):
+ """
+ The CHANTYPES support parameter is parsed into a tuple of
+ valid channel prefix characters.
+ """
+ self._testFeatureDefault('CHANTYPES')
+
+ self.assertEqual(
+ self._parseFeature('CHANTYPES', '#&%'),
+ ('#', '&', '%'))
+
+
+ def test_support_KICKLEN(self):
+ """
+ The KICKLEN support parameter is parsed into an integer value
+ indicating the maximum length of a kick message a client may use.
+ """
+ self._testIntOrDefaultFeature('KICKLEN')
+
+
+ def test_support_PREFIX(self):
+ """
+ The PREFIX support parameter is parsed into a dictionary mapping
+ modes to two-tuples of status symbol and priority.
+ """
+ self._testFeatureDefault('PREFIX')
+ self._testFeatureDefault('PREFIX', [('PREFIX', 'hello')])
+
+ self.assertEqual(
+ self._parseFeature('PREFIX', None),
+ None)
+ self.assertEqual(
+ self._parseFeature('PREFIX', '(ohv)@%+'),
+ {'o': ('@', 0),
+ 'h': ('%', 1),
+ 'v': ('+', 2)})
+ self.assertEqual(
+ self._parseFeature('PREFIX', '(hov)@%+'),
+ {'o': ('%', 1),
+ 'h': ('@', 0),
+ 'v': ('+', 2)})
+
+
+ def test_support_TOPICLEN(self):
+ """
+ The TOPICLEN support parameter is parsed into an integer value
+ indicating the maximum length of a topic a client may set.
+ """
+ self._testIntOrDefaultFeature('TOPICLEN')
+
+
+ def test_support_MODES(self):
+ """
+ The MODES support parameter is parsed into an integer value
+ indicating the maximum number of "variable" modes (defined as being
+ modes from C{addressModes}, C{param} or C{setParam} categories for
+ the C{CHANMODES} ISUPPORT parameter) which may by set on a channel
+ by a single MODE command from a client.
+ """
+ self._testIntOrDefaultFeature('MODES')
+
+
+ def test_support_EXCEPTS(self):
+ """
+ The EXCEPTS support parameter is parsed into the mode character
+ to be used for "ban exception" modes. If no parameter is specified
+ then the character C{e} is assumed.
+ """
+ self.assertEqual(
+ self._parseFeature('EXCEPTS', 'Z'),
+ 'Z')
+ self.assertEqual(
+ self._parseFeature('EXCEPTS'),
+ 'e')
+
+
+ def test_support_INVEX(self):
+ """
+ The INVEX support parameter is parsed into the mode character to be
+ used for "invite exception" modes. If no parameter is specified then
+ the character C{I} is assumed.
+ """
+ self.assertEqual(
+ self._parseFeature('INVEX', 'Z'),
+ 'Z')
+ self.assertEqual(
+ self._parseFeature('INVEX'),
+ 'I')
+
+
+
+class IRCClientWithoutLogin(irc.IRCClient):
+ performLogin = 0
+
+
+
+class CTCPTest(unittest.TestCase):
+ """
+ Tests for L{twisted.words.protocols.irc.IRCClient} CTCP handling.
+ """
+ def setUp(self):
+ self.file = StringIOWithoutClosing()
+ self.transport = protocol.FileWrapper(self.file)
+ self.client = IRCClientWithoutLogin()
+ self.client.makeConnection(self.transport)
+
+ self.addCleanup(self.transport.loseConnection)
+ self.addCleanup(self.client.connectionLost, None)
+
+
+ def test_ERRMSG(self):
+ """Testing CTCP query ERRMSG.
+
+ Not because this is this is an especially important case in the
+ field, but it does go through the entire dispatch/decode/encode
+ process.
+ """
+
+ errQuery = (":nick!guy@over.there PRIVMSG #theChan :"
+ "%(X)cERRMSG t%(X)c%(EOL)s"
+ % {'X': irc.X_DELIM,
+ 'EOL': irc.CR + irc.LF})
+
+ errReply = ("NOTICE nick :%(X)cERRMSG t :"
+ "No error has occoured.%(X)c%(EOL)s"
+ % {'X': irc.X_DELIM,
+ 'EOL': irc.CR + irc.LF})
+
+ self.client.dataReceived(errQuery)
+ reply = self.file.getvalue()
+
+ self.assertEqual(errReply, reply)
+
+
+ def test_noNumbersVERSION(self):
+ """
+ If attributes for version information on L{IRCClient} are set to
+ C{None}, the parts of the CTCP VERSION response they correspond to
+ are omitted.
+ """
+ self.client.versionName = "FrobozzIRC"
+ self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None)
+ versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s::"
+ "%(X)c%(EOL)s"
+ % {'X': irc.X_DELIM,
+ 'EOL': irc.CR + irc.LF,
+ 'vname': self.client.versionName})
+ reply = self.file.getvalue()
+ self.assertEqual(versionReply, reply)
+
+
+ def test_fullVERSION(self):
+ """
+ The response to a CTCP VERSION query includes the version number and
+ environment information, as specified by L{IRCClient.versionNum} and
+ L{IRCClient.versionEnv}.
+ """
+ self.client.versionName = "FrobozzIRC"
+ self.client.versionNum = "1.2g"
+ self.client.versionEnv = "ZorkOS"
+ self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None)
+ versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s:%(vnum)s:%(venv)s"
+ "%(X)c%(EOL)s"
+ % {'X': irc.X_DELIM,
+ 'EOL': irc.CR + irc.LF,
+ 'vname': self.client.versionName,
+ 'vnum': self.client.versionNum,
+ 'venv': self.client.versionEnv})
+ reply = self.file.getvalue()
+ self.assertEqual(versionReply, reply)
+
+
+ def test_noDuplicateCTCPDispatch(self):
+ """
+ Duplicated CTCP messages are ignored and no reply is made.
+ """
+ def testCTCP(user, channel, data):
+ self.called += 1
+
+ self.called = 0
+ self.client.ctcpQuery_TESTTHIS = testCTCP
+
+ self.client.irc_PRIVMSG(
+ 'foo!bar@baz.quux', [
+ '#chan',
+ '%(X)sTESTTHIS%(X)sfoo%(X)sTESTTHIS%(X)s' % {'X': irc.X_DELIM}])
+ self.assertEqual(
+ self.file.getvalue(),
+ '')
+ self.assertEqual(self.called, 1)
+
+
+ def test_noDefaultDispatch(self):
+ """
+ The fallback handler is invoked for unrecognized CTCP messages.
+ """
+ def unknownQuery(user, channel, tag, data):
+ self.calledWith = (user, channel, tag, data)
+ self.called += 1
+
+ self.called = 0
+ self.patch(self.client, 'ctcpUnknownQuery', unknownQuery)
+ self.client.irc_PRIVMSG(
+ 'foo!bar@baz.quux', [
+ '#chan',
+ '%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
+ self.assertEqual(
+ self.file.getvalue(),
+ '')
+ self.assertEqual(
+ self.calledWith,
+ ('foo!bar@baz.quux', '#chan', 'NOTREAL', None))
+ self.assertEqual(self.called, 1)
+
+ # The fallback handler is not invoked for duplicate unknown CTCP
+ # messages.
+ self.client.irc_PRIVMSG(
+ 'foo!bar@baz.quux', [
+ '#chan',
+ '%(X)sNOTREAL%(X)sfoo%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}])
+ self.assertEqual(self.called, 2)
+
+
+
+class NoticingClient(IRCClientWithoutLogin, object):
+ methods = {
+ 'created': ('when',),
+ 'yourHost': ('info',),
+ 'myInfo': ('servername', 'version', 'umodes', 'cmodes'),
+ 'luserClient': ('info',),
+ 'bounce': ('info',),
+ 'isupport': ('options',),
+ 'luserChannels': ('channels',),
+ 'luserOp': ('ops',),
+ 'luserMe': ('info',),
+ 'receivedMOTD': ('motd',),
+
+ 'privmsg': ('user', 'channel', 'message'),
+ 'joined': ('channel',),
+ 'left': ('channel',),
+ 'noticed': ('user', 'channel', 'message'),
+ 'modeChanged': ('user', 'channel', 'set', 'modes', 'args'),
+ 'pong': ('user', 'secs'),
+ 'signedOn': (),
+ 'kickedFrom': ('channel', 'kicker', 'message'),
+ 'nickChanged': ('nick',),
+
+ 'userJoined': ('user', 'channel'),
+ 'userLeft': ('user', 'channel'),
+ 'userKicked': ('user', 'channel', 'kicker', 'message'),
+ 'action': ('user', 'channel', 'data'),
+ 'topicUpdated': ('user', 'channel', 'newTopic'),
+ 'userRenamed': ('oldname', 'newname')}
+
+
+ def __init__(self, *a, **kw):
+ # It is important that IRCClient.__init__ is not called since
+ # traditionally it did not exist, so it is important that nothing is
+ # initialised there that would prevent subclasses that did not (or
+ # could not) invoke the base implementation. Any protocol
+ # initialisation should happen in connectionMode.
+ self.calls = []
+
+
+ def __getattribute__(self, name):
+ if name.startswith('__') and name.endswith('__'):
+ return super(NoticingClient, self).__getattribute__(name)
+ try:
+ args = super(NoticingClient, self).__getattribute__('methods')[name]
+ except KeyError:
+ return super(NoticingClient, self).__getattribute__(name)
+ else:
+ return self.makeMethod(name, args)
+
+
+ def makeMethod(self, fname, args):
+ def method(*a, **kw):
+ if len(a) > len(args):
+ raise TypeError("TypeError: %s() takes %d arguments "
+ "(%d given)" % (fname, len(args), len(a)))
+ for (name, value) in zip(args, a):
+ if name in kw:
+ raise TypeError("TypeError: %s() got multiple values "
+ "for keyword argument '%s'" % (fname, name))
+ else:
+ kw[name] = value
+ if len(kw) != len(args):
+ raise TypeError("TypeError: %s() takes %d arguments "
+ "(%d given)" % (fname, len(args), len(a)))
+ self.calls.append((fname, kw))
+ return method
+
+
+def pop(dict, key, default):
+ try:
+ value = dict[key]
+ except KeyError:
+ return default
+ else:
+ del dict[key]
+ return value
+
+
+
+class ClientImplementationTests(unittest.TestCase):
+ def setUp(self):
+ self.transport = StringTransport()
+ self.client = NoticingClient()
+ self.client.makeConnection(self.transport)
+
+ self.addCleanup(self.transport.loseConnection)
+ self.addCleanup(self.client.connectionLost, None)
+
+
+ def _serverTestImpl(self, code, msg, func, **kw):
+ host = pop(kw, 'host', 'server.host')
+ nick = pop(kw, 'nick', 'nickname')
+ args = pop(kw, 'args', '')
+
+ message = (":" +
+ host + " " +
+ code + " " +
+ nick + " " +
+ args + " :" +
+ msg + "\r\n")
+
+ self.client.dataReceived(message)
+ self.assertEqual(
+ self.client.calls,
+ [(func, kw)])
+
+
+ def testYourHost(self):
+ msg = "Your host is some.host[blah.blah/6667], running version server-version-3"
+ self._serverTestImpl("002", msg, "yourHost", info=msg)
+
+
+ def testCreated(self):
+ msg = "This server was cobbled together Fri Aug 13 18:00:25 UTC 2004"
+ self._serverTestImpl("003", msg, "created", when=msg)
+
+
+ def testMyInfo(self):
+ msg = "server.host server-version abcDEF bcdEHI"
+ self._serverTestImpl("004", msg, "myInfo",
+ servername="server.host",
+ version="server-version",
+ umodes="abcDEF",
+ cmodes="bcdEHI")
+
+
+ def testLuserClient(self):
+ msg = "There are 9227 victims and 9542 hiding on 24 servers"
+ self._serverTestImpl("251", msg, "luserClient",
+ info=msg)
+
+
+ def _sendISUPPORT(self):
+ args = ("MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 "
+ "TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# "
+ "PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer")
+ msg = "are available on this server"
+ self._serverTestImpl("005", msg, "isupport", args=args,
+ options=['MODES=4',
+ 'CHANLIMIT=#:20',
+ 'NICKLEN=16',
+ 'USERLEN=10',
+ 'HOSTLEN=63',
+ 'TOPICLEN=450',
+ 'KICKLEN=450',
+ 'CHANNELLEN=30',
+ 'KEYLEN=23',
+ 'CHANTYPES=#',
+ 'PREFIX=(ov)@+',
+ 'CASEMAPPING=ascii',
+ 'CAPAB',
+ 'IRCD=dancer'])
+
+
+ def test_ISUPPORT(self):
+ """
+ The client parses ISUPPORT messages sent by the server and calls
+ L{IRCClient.isupport}.
+ """
+ self._sendISUPPORT()
+
+
+ def testBounce(self):
+ msg = "Try server some.host, port 321"
+ self._serverTestImpl("010", msg, "bounce",
+ info=msg)
+
+
+ def testLuserChannels(self):
+ args = "7116"
+ msg = "channels formed"
+ self._serverTestImpl("254", msg, "luserChannels", args=args,
+ channels=int(args))
+
+
+ def testLuserOp(self):
+ args = "34"
+ msg = "flagged staff members"
+ self._serverTestImpl("252", msg, "luserOp", args=args,
+ ops=int(args))
+
+
+ def testLuserMe(self):
+ msg = "I have 1937 clients and 0 servers"
+ self._serverTestImpl("255", msg, "luserMe",
+ info=msg)
+
+
+ def test_receivedMOTD(self):
+ """
+ Lines received in I{RPL_MOTDSTART} and I{RPL_MOTD} are delivered to
+ L{IRCClient.receivedMOTD} when I{RPL_ENDOFMOTD} is received.
+ """
+ lines = [
+ ":host.name 375 nickname :- host.name Message of the Day -",
+ ":host.name 372 nickname :- Welcome to host.name",
+ ":host.name 376 nickname :End of /MOTD command."]
+ for L in lines:
+ self.assertEqual(self.client.calls, [])
+ self.client.dataReceived(L + '\r\n')
+
+ self.assertEqual(
+ self.client.calls,
+ [("receivedMOTD", {"motd": ["host.name Message of the Day -", "Welcome to host.name"]})])
+
+ # After the motd is delivered, the tracking variable should be
+ # reset.
+ self.assertIdentical(self.client.motd, None)
+
+
+ def test_withoutMOTDSTART(self):
+ """
+ If L{IRCClient} receives I{RPL_MOTD} and I{RPL_ENDOFMOTD} without
+ receiving I{RPL_MOTDSTART}, L{IRCClient.receivedMOTD} is still
+ called with a list of MOTD lines.
+ """
+ lines = [
+ ":host.name 372 nickname :- Welcome to host.name",
+ ":host.name 376 nickname :End of /MOTD command."]
+
+ for L in lines:
+ self.client.dataReceived(L + '\r\n')
+
+ self.assertEqual(
+ self.client.calls,
+ [("receivedMOTD", {"motd": ["Welcome to host.name"]})])
+
+
+ def _clientTestImpl(self, sender, group, type, msg, func, **kw):
+ ident = pop(kw, 'ident', 'ident')
+ host = pop(kw, 'host', 'host')
+
+ wholeUser = sender + '!' + ident + '@' + host
+ message = (":" +
+ wholeUser + " " +
+ type + " " +
+ group + " :" +
+ msg + "\r\n")
+ self.client.dataReceived(message)
+ self.assertEqual(
+ self.client.calls,
+ [(func, kw)])
+ self.client.calls = []
+
+
+ def testPrivmsg(self):
+ msg = "Tooty toot toot."
+ self._clientTestImpl("sender", "#group", "PRIVMSG", msg, "privmsg",
+ ident="ident", host="host",
+ # Expected results below
+ user="sender!ident@host",
+ channel="#group",
+ message=msg)
+
+ self._clientTestImpl("sender", "recipient", "PRIVMSG", msg, "privmsg",
+ ident="ident", host="host",
+ # Expected results below
+ user="sender!ident@host",
+ channel="recipient",
+ message=msg)
+
+
+ def test_getChannelModeParams(self):
+ """
+ L{IRCClient.getChannelModeParams} uses ISUPPORT information, either
+ given by the server or defaults, to determine which channel modes
+ require arguments when being added or removed.
+ """
+ add, remove = map(sorted, self.client.getChannelModeParams())
+ self.assertEqual(add, ['b', 'h', 'k', 'l', 'o', 'v'])
+ self.assertEqual(remove, ['b', 'h', 'o', 'v'])
+
+ def removeFeature(name):
+ name = '-' + name
+ msg = "are available on this server"
+ self._serverTestImpl(
+ '005', msg, 'isupport', args=name, options=[name])
+ self.assertIdentical(
+ self.client.supported.getFeature(name), None)
+ self.client.calls = []
+
+ # Remove CHANMODES feature, causing getFeature('CHANMODES') to return
+ # None.
+ removeFeature('CHANMODES')
+ add, remove = map(sorted, self.client.getChannelModeParams())
+ self.assertEqual(add, ['h', 'o', 'v'])
+ self.assertEqual(remove, ['h', 'o', 'v'])
+
+ # Remove PREFIX feature, causing getFeature('PREFIX') to return None.
+ removeFeature('PREFIX')
+ add, remove = map(sorted, self.client.getChannelModeParams())
+ self.assertEqual(add, [])
+ self.assertEqual(remove, [])
+
+ # Restore ISUPPORT features.
+ self._sendISUPPORT()
+ self.assertNotIdentical(
+ self.client.supported.getFeature('PREFIX'), None)
+
+
+ def test_getUserModeParams(self):
+ """
+ L{IRCClient.getUserModeParams} returns a list of user modes (modes that
+ the user sets on themself, outside of channel modes) that require
+ parameters when added and removed, respectively.
+ """
+ add, remove = map(sorted, self.client.getUserModeParams())
+ self.assertEqual(add, [])
+ self.assertEqual(remove, [])
+
+
+ def _sendModeChange(self, msg, args='', target=None):
+ """
+ Build a MODE string and send it to the client.
+ """
+ if target is None:
+ target = '#chan'
+ message = ":Wolf!~wolf@yok.utu.fi MODE %s %s %s\r\n" % (
+ target, msg, args)
+ self.client.dataReceived(message)
+
+
+ def _parseModeChange(self, results, target=None):
+ """
+ Parse the results, do some test and return the data to check.
+ """
+ if target is None:
+ target = '#chan'
+
+ for n, result in enumerate(results):
+ method, data = result
+ self.assertEqual(method, 'modeChanged')
+ self.assertEqual(data['user'], 'Wolf!~wolf@yok.utu.fi')
+ self.assertEqual(data['channel'], target)
+ results[n] = tuple([data[key] for key in ('set', 'modes', 'args')])
+ return results
+
+
+ def _checkModeChange(self, expected, target=None):
+ """
+ Compare the expected result with the one returned by the client.
+ """
+ result = self._parseModeChange(self.client.calls, target)
+ self.assertEqual(result, expected)
+ self.client.calls = []
+
+
+ def test_modeMissingDirection(self):
+ """
+ Mode strings that do not begin with a directional character, C{'+'} or
+ C{'-'}, have C{'+'} automatically prepended.
+ """
+ self._sendModeChange('s')
+ self._checkModeChange([(True, 's', (None,))])
+
+
+ def test_noModeParameters(self):
+ """
+ No parameters are passed to L{IRCClient.modeChanged} for modes that
+ don't take any parameters.
+ """
+ self._sendModeChange('-s')
+ self._checkModeChange([(False, 's', (None,))])
+ self._sendModeChange('+n')
+ self._checkModeChange([(True, 'n', (None,))])
+
+
+ def test_oneModeParameter(self):
+ """
+ Parameters are passed to L{IRCClient.modeChanged} for modes that take
+ parameters.
+ """
+ self._sendModeChange('+o', 'a_user')
+ self._checkModeChange([(True, 'o', ('a_user',))])
+ self._sendModeChange('-o', 'a_user')
+ self._checkModeChange([(False, 'o', ('a_user',))])
+
+
+ def test_mixedModes(self):
+ """
+ Mixing adding and removing modes that do and don't take parameters
+ invokes L{IRCClient.modeChanged} with mode characters and parameters
+ that match up.
+ """
+ self._sendModeChange('+osv', 'a_user another_user')
+ self._checkModeChange([(True, 'osv', ('a_user', None, 'another_user'))])
+ self._sendModeChange('+v-os', 'a_user another_user')
+ self._checkModeChange([(True, 'v', ('a_user',)),
+ (False, 'os', ('another_user', None))])
+
+
+ def test_tooManyModeParameters(self):
+ """
+ Passing an argument to modes that take no parameters results in
+ L{IRCClient.modeChanged} not being called and an error being logged.
+ """
+ self._sendModeChange('+s', 'wrong')
+ self._checkModeChange([])
+ errors = self.flushLoggedErrors(irc.IRCBadModes)
+ self.assertEqual(len(errors), 1)
+ self.assertSubstring(
+ 'Too many parameters', errors[0].getErrorMessage())
+
+
+ def test_tooFewModeParameters(self):
+ """
+ Passing no arguments to modes that do take parameters results in
+ L{IRCClient.modeChange} not being called and an error being logged.
+ """
+ self._sendModeChange('+o')
+ self._checkModeChange([])
+ errors = self.flushLoggedErrors(irc.IRCBadModes)
+ self.assertEqual(len(errors), 1)
+ self.assertSubstring(
+ 'Not enough parameters', errors[0].getErrorMessage())
+
+
+ def test_userMode(self):
+ """
+ A C{MODE} message whose target is our user (the nickname of our user,
+ to be precise), as opposed to a channel, will be parsed according to
+ the modes specified by L{IRCClient.getUserModeParams}.
+ """
+ target = self.client.nickname
+ # Mode "o" on channels is supposed to take a parameter, but since this
+ # is not a channel this will not cause an exception.
+ self._sendModeChange('+o', target=target)
+ self._checkModeChange([(True, 'o', (None,))], target=target)
+
+ def getUserModeParams():
+ return ['Z', '']
+
+ # Introduce our own user mode that takes an argument.
+ self.patch(self.client, 'getUserModeParams', getUserModeParams)
+
+ self._sendModeChange('+Z', 'an_arg', target=target)
+ self._checkModeChange([(True, 'Z', ('an_arg',))], target=target)
+
+
+ def test_heartbeat(self):
+ """
+ When the I{RPL_WELCOME} message is received a heartbeat is started that
+ will send a I{PING} message to the IRC server every
+ L{irc.IRCClient.heartbeatInterval} seconds. When the transport is
+ closed the heartbeat looping call is stopped too.
+ """
+ def _createHeartbeat():
+ heartbeat = self._originalCreateHeartbeat()
+ heartbeat.clock = self.clock
+ return heartbeat
+
+ self.clock = task.Clock()
+ self._originalCreateHeartbeat = self.client._createHeartbeat
+ self.patch(self.client, '_createHeartbeat', _createHeartbeat)
+
+ self.assertIdentical(self.client._heartbeat, None)
+ self.client.irc_RPL_WELCOME('foo', [])
+ self.assertNotIdentical(self.client._heartbeat, None)
+ self.assertEqual(self.client.hostname, 'foo')
+
+ # Pump the clock enough to trigger one LoopingCall.
+ self.assertEqual(self.transport.value(), '')
+ self.clock.advance(self.client.heartbeatInterval)
+ self.assertEqual(self.transport.value(), 'PING foo\r\n')
+
+ # When the connection is lost the heartbeat is stopped.
+ self.transport.loseConnection()
+ self.client.connectionLost(None)
+ self.assertEqual(
+ len(self.clock.getDelayedCalls()), 0)
+ self.assertIdentical(self.client._heartbeat, None)
+
+
+ def test_heartbeatDisabled(self):
+ """
+ If L{irc.IRCClient.heartbeatInterval} is set to C{None} then no
+ heartbeat is created.
+ """
+ self.assertIdentical(self.client._heartbeat, None)
+ self.client.heartbeatInterval = None
+ self.client.irc_RPL_WELCOME('foo', [])
+ self.assertIdentical(self.client._heartbeat, None)
+
+
+
+class BasicServerFunctionalityTestCase(unittest.TestCase):
+ def setUp(self):
+ self.f = StringIOWithoutClosing()
+ self.t = protocol.FileWrapper(self.f)
+ self.p = irc.IRC()
+ self.p.makeConnection(self.t)
+
+
+ def check(self, s):
+ self.assertEqual(self.f.getvalue(), s)
+
+
+ def testPrivmsg(self):
+ self.p.privmsg("this-is-sender", "this-is-recip", "this is message")
+ self.check(":this-is-sender PRIVMSG this-is-recip :this is message\r\n")
+
+
+ def testNotice(self):
+ self.p.notice("this-is-sender", "this-is-recip", "this is notice")
+ self.check(":this-is-sender NOTICE this-is-recip :this is notice\r\n")
+
+
+ def testAction(self):
+ self.p.action("this-is-sender", "this-is-recip", "this is action")
+ self.check(":this-is-sender ACTION this-is-recip :this is action\r\n")
+
+
+ def testJoin(self):
+ self.p.join("this-person", "#this-channel")
+ self.check(":this-person JOIN #this-channel\r\n")
+
+
+ def testPart(self):
+ self.p.part("this-person", "#that-channel")
+ self.check(":this-person PART #that-channel\r\n")
+
+
+ def testWhois(self):
+ """
+ Verify that a whois by the client receives the right protocol actions
+ from the server.
+ """
+ timestamp = int(time.time()-100)
+ hostname = self.p.hostname
+ req = 'requesting-nick'
+ targ = 'target-nick'
+ self.p.whois(req, targ, 'target', 'host.com',
+ 'Target User', 'irc.host.com', 'A fake server', False,
+ 12, timestamp, ['#fakeusers', '#fakemisc'])
+ expected = '\r\n'.join([
+':%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User',
+':%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server',
+':%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time',
+':%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc',
+':%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.',
+'']) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ)
+ self.check(expected)
+
+
+
+class DummyClient(irc.IRCClient):
+ """
+ A L{twisted.words.protocols.irc.IRCClient} that stores sent lines in a
+ C{list} rather than transmitting them.
+ """
+ def __init__(self):
+ self.lines = []
+
+
+ def connectionMade(self):
+ irc.IRCClient.connectionMade(self)
+ self.lines = []
+
+
+ def _truncateLine(self, line):
+ """
+ Truncate an IRC line to the maximum allowed length.
+ """
+ return line[:irc.MAX_COMMAND_LENGTH - len(self.delimiter)]
+
+
+ def lineReceived(self, line):
+ # Emulate IRC servers throwing away our important data.
+ line = self._truncateLine(line)
+ return irc.IRCClient.lineReceived(self, line)
+
+
+ def sendLine(self, m):
+ self.lines.append(self._truncateLine(m))
+
+
+
+class ClientInviteTests(unittest.TestCase):
+ """
+ Tests for L{IRCClient.invite}.
+ """
+ def setUp(self):
+ """
+ Create a L{DummyClient} to call C{invite} on in test methods.
+ """
+ self.client = DummyClient()
+
+
+ def test_channelCorrection(self):
+ """
+ If the channel name passed to L{IRCClient.invite} does not begin with a
+ channel prefix character, one is prepended to it.
+ """
+ self.client.invite('foo', 'bar')
+ self.assertEqual(self.client.lines, ['INVITE foo #bar'])
+
+
+ def test_invite(self):
+ """
+ L{IRCClient.invite} sends an I{INVITE} message with the specified
+ username and a channel.
+ """
+ self.client.invite('foo', '#bar')
+ self.assertEqual(self.client.lines, ['INVITE foo #bar'])
+
+
+
+class ClientMsgTests(unittest.TestCase):
+ """
+ Tests for messages sent with L{twisted.words.protocols.irc.IRCClient}.
+ """
+ def setUp(self):
+ self.client = DummyClient()
+ self.client.connectionMade()
+
+
+ def test_singleLine(self):
+ """
+ A message containing no newlines is sent in a single command.
+ """
+ self.client.msg('foo', 'bar')
+ self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
+
+
+ def test_invalidMaxLength(self):
+ """
+ Specifying a C{length} value to L{IRCClient.msg} that is too short to
+ contain the protocol command to send a message raises C{ValueError}.
+ """
+ self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 0)
+ self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 3)
+
+
+ def test_multipleLine(self):
+ """
+ Messages longer than the C{length} parameter to L{IRCClient.msg} will
+ be split and sent in multiple commands.
+ """
+ maxLen = len('PRIVMSG foo :') + 3 + 2 # 2 for line endings
+ self.client.msg('foo', 'barbazbo', maxLen)
+ self.assertEqual(
+ self.client.lines,
+ ['PRIVMSG foo :bar',
+ 'PRIVMSG foo :baz',
+ 'PRIVMSG foo :bo'])
+
+
+ def test_sufficientWidth(self):
+ """
+ Messages exactly equal in length to the C{length} paramtere to
+ L{IRCClient.msg} are sent in a single command.
+ """
+ msg = 'barbazbo'
+ maxLen = len('PRIVMSG foo :%s' % (msg,)) + 2
+ self.client.msg('foo', msg, maxLen)
+ self.assertEqual(self.client.lines, ['PRIVMSG foo :%s' % (msg,)])
+ self.client.lines = []
+ self.client.msg('foo', msg, maxLen-1)
+ self.assertEqual(2, len(self.client.lines))
+ self.client.lines = []
+ self.client.msg('foo', msg, maxLen+1)
+ self.assertEqual(1, len(self.client.lines))
+
+
+ def test_newlinesAtStart(self):
+ """
+ An LF at the beginning of the message is ignored.
+ """
+ self.client.lines = []
+ self.client.msg('foo', '\nbar')
+ self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
+
+
+ def test_newlinesAtEnd(self):
+ """
+ An LF at the end of the message is ignored.
+ """
+ self.client.lines = []
+ self.client.msg('foo', 'bar\n')
+ self.assertEqual(self.client.lines, ['PRIVMSG foo :bar'])
+
+
+ def test_newlinesWithinMessage(self):
+ """
+ An LF within a message causes a new line.
+ """
+ self.client.lines = []
+ self.client.msg('foo', 'bar\nbaz')
+ self.assertEqual(
+ self.client.lines,
+ ['PRIVMSG foo :bar',
+ 'PRIVMSG foo :baz'])
+
+
+ def test_consecutiveNewlines(self):
+ """
+ Consecutive LFs do not cause a blank line.
+ """
+ self.client.lines = []
+ self.client.msg('foo', 'bar\n\nbaz')
+ self.assertEqual(
+ self.client.lines,
+ ['PRIVMSG foo :bar',
+ 'PRIVMSG foo :baz'])
+
+
+ def assertLongMessageSplitting(self, message, expectedNumCommands,
+ length=None):
+ """
+ Assert that messages sent by L{IRCClient.msg} are split into an
+ expected number of commands and the original message is transmitted in
+ its entirety over those commands.
+ """
+ responsePrefix = ':%s!%s@%s ' % (
+ self.client.nickname,
+ self.client.realname,
+ self.client.hostname)
+
+ self.client.msg('foo', message, length=length)
+
+ privmsg = []
+ self.patch(self.client, 'privmsg', lambda *a: privmsg.append(a))
+ # Deliver these to IRCClient via the normal mechanisms.
+ for line in self.client.lines:
+ self.client.lineReceived(responsePrefix + line)
+
+ self.assertEqual(len(privmsg), expectedNumCommands)
+ receivedMessage = ''.join(
+ message for user, target, message in privmsg)
+
+ # Did the long message we sent arrive as intended?
+ self.assertEqual(message, receivedMessage)
+
+
+ def test_splitLongMessagesWithDefault(self):
+ """
+ If a maximum message length is not provided to L{IRCClient.msg} a
+ best-guess effort is made to determine a safe maximum, messages longer
+ than this are split into multiple commands with the intent of
+ delivering long messages without losing data due to message truncation
+ when the server relays them.
+ """
+ message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
+ self.assertLongMessageSplitting(message, 2)
+
+
+ def test_splitLongMessagesWithOverride(self):
+ """
+ The maximum message length can be specified to L{IRCClient.msg},
+ messages longer than this are split into multiple commands with the
+ intent of delivering long messages without losing data due to message
+ truncation when the server relays them.
+ """
+ message = 'o' * (irc.MAX_COMMAND_LENGTH - 2)
+ self.assertLongMessageSplitting(
+ message, 3, length=irc.MAX_COMMAND_LENGTH / 2)
+
+
+ def test_newlinesBeforeLineBreaking(self):
+ """
+ IRCClient breaks on newlines before it breaks long lines.
+ """
+ # Because MAX_COMMAND_LENGTH includes framing characters, this long
+ # line is slightly longer than half the permissible message size.
+ longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2)
+
+ self.client.msg('foo', longline + '\n' + longline)
+ self.assertEqual(
+ self.client.lines,
+ ['PRIVMSG foo :' + longline,
+ 'PRIVMSG foo :' + longline])
+
+
+ def test_lineBreakOnWordBoundaries(self):
+ """
+ IRCClient prefers to break long lines at word boundaries.
+ """
+ # Because MAX_COMMAND_LENGTH includes framing characters, this long
+ # line is slightly longer than half the permissible message size.
+ longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2)
+
+ self.client.msg('foo', longline + ' ' + longline)
+ self.assertEqual(
+ self.client.lines,
+ ['PRIVMSG foo :' + longline,
+ 'PRIVMSG foo :' + longline])
+
+
+ def test_splitSanity(self):
+ """
+ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a
+ length less than or equal to C{0} and returns C{[]} when splitting
+ C{''}.
+ """
+ # Whiteboxing
+ self.assertRaises(ValueError, irc.split, 'foo', -1)
+ self.assertRaises(ValueError, irc.split, 'foo', 0)
+ self.assertEqual([], irc.split('', 1))
+ self.assertEqual([], irc.split(''))
+
+
+ def test_splitDelimiters(self):
+ """
+ L{twisted.words.protocols.irc.split} skips any delimiter (space or
+ newline) that it finds at the very beginning of the string segment it
+ is operating on. Nothing should be added to the output list because of
+ it.
+ """
+ r = irc.split("xx yyz", 2)
+ self.assertEqual(['xx', 'yy', 'z'], r)
+ r = irc.split("xx\nyyz", 2)
+ self.assertEqual(['xx', 'yy', 'z'], r)
+
+
+ def test_splitValidatesLength(self):
+ """
+ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a
+ length less than or equal to C{0}.
+ """
+ self.assertRaises(ValueError, irc.split, "foo", 0)
+ self.assertRaises(ValueError, irc.split, "foo", -1)
+
+
+ def test_say(self):
+ """
+ L{IRCClient.say} prepends the channel prefix C{"#"} if necessary and
+ then sends the message to the server for delivery to that channel.
+ """
+ self.client.say("thechannel", "the message")
+ self.assertEquals(
+ self.client.lines, ["PRIVMSG #thechannel :the message"])
+
+
+
+class ClientTests(TestCase):
+ """
+ Tests for the protocol-level behavior of IRCClient methods intended to
+ be called by application code.
+ """
+ def setUp(self):
+ """
+ Create and connect a new L{IRCClient} to a new L{StringTransport}.
+ """
+ self.transport = StringTransport()
+ self.protocol = IRCClient()
+ self.protocol.performLogin = False
+ self.protocol.makeConnection(self.transport)
+
+ # Sanity check - we don't want anything to have happened at this
+ # point, since we're not in a test yet.
+ self.assertEqual(self.transport.value(), "")
+
+ self.addCleanup(self.transport.loseConnection)
+ self.addCleanup(self.protocol.connectionLost, None)
+
+
+ def getLastLine(self, transport):
+ """
+ Return the last IRC message in the transport buffer.
+ """
+ return transport.value().split('\r\n')[-2]
+
+
+ def test_away(self):
+ """
+ L{IRCCLient.away} sends an AWAY command with the specified message.
+ """
+ message = "Sorry, I'm not here."
+ self.protocol.away(message)
+ expected = [
+ 'AWAY :%s' % (message,),
+ '',
+ ]
+ self.assertEqual(self.transport.value().split('\r\n'), expected)
+
+
+ def test_back(self):
+ """
+ L{IRCClient.back} sends an AWAY command with an empty message.
+ """
+ self.protocol.back()
+ expected = [
+ 'AWAY :',
+ '',
+ ]
+ self.assertEqual(self.transport.value().split('\r\n'), expected)
+
+
+ def test_whois(self):
+ """
+ L{IRCClient.whois} sends a WHOIS message.
+ """
+ self.protocol.whois('alice')
+ self.assertEqual(
+ self.transport.value().split('\r\n'),
+ ['WHOIS alice', ''])
+
+
+ def test_whoisWithServer(self):
+ """
+ L{IRCClient.whois} sends a WHOIS message with a server name if a
+ value is passed for the C{server} parameter.
+ """
+ self.protocol.whois('alice', 'example.org')
+ self.assertEqual(
+ self.transport.value().split('\r\n'),
+ ['WHOIS example.org alice', ''])
+
+
+ def test_register(self):
+ """
+ L{IRCClient.register} sends NICK and USER commands with the
+ username, name, hostname, server name, and real name specified.
+ """
+ username = 'testuser'
+ hostname = 'testhost'
+ servername = 'testserver'
+ self.protocol.realname = 'testname'
+ self.protocol.password = None
+ self.protocol.register(username, hostname, servername)
+ expected = [
+ 'NICK %s' % (username,),
+ 'USER %s %s %s :%s' % (
+ username, hostname, servername, self.protocol.realname),
+ '']
+ self.assertEqual(self.transport.value().split('\r\n'), expected)
+
+
+ def test_registerWithPassword(self):
+ """
+ If the C{password} attribute of L{IRCClient} is not C{None}, the
+ C{register} method also sends a PASS command with it as the
+ argument.
+ """
+ username = 'testuser'
+ hostname = 'testhost'
+ servername = 'testserver'
+ self.protocol.realname = 'testname'
+ self.protocol.password = 'testpass'
+ self.protocol.register(username, hostname, servername)
+ expected = [
+ 'PASS %s' % (self.protocol.password,),
+ 'NICK %s' % (username,),
+ 'USER %s %s %s :%s' % (
+ username, hostname, servername, self.protocol.realname),
+ '']
+ self.assertEqual(self.transport.value().split('\r\n'), expected)
+
+
+ def test_registerWithTakenNick(self):
+ """
+ Verify that the client repeats the L{IRCClient.setNick} method with a
+ new value when presented with an C{ERR_NICKNAMEINUSE} while trying to
+ register.
+ """
+ username = 'testuser'
+ hostname = 'testhost'
+ servername = 'testserver'
+ self.protocol.realname = 'testname'
+ self.protocol.password = 'testpass'
+ self.protocol.register(username, hostname, servername)
+ self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
+ lastLine = self.getLastLine(self.transport)
+ self.assertNotEquals(lastLine, 'NICK %s' % (username,))
+
+ # Keep chaining underscores for each collision
+ self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
+ lastLine = self.getLastLine(self.transport)
+ self.assertEqual(lastLine, 'NICK %s' % (username + '__',))
+
+
+ def test_overrideAlterCollidedNick(self):
+ """
+ L{IRCClient.alterCollidedNick} determines how a nickname is altered upon
+ collision while a user is trying to change to that nickname.
+ """
+ nick = 'foo'
+ self.protocol.alterCollidedNick = lambda nick: nick + '***'
+ self.protocol.register(nick)
+ self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param'])
+ lastLine = self.getLastLine(self.transport)
+ self.assertEqual(
+ lastLine, 'NICK %s' % (nick + '***',))
+
+
+ def test_nickChange(self):
+ """
+ When a NICK command is sent after signon, C{IRCClient.nickname} is set
+ to the new nickname I{after} the server sends an acknowledgement.
+ """
+ oldnick = 'foo'
+ newnick = 'bar'
+ self.protocol.register(oldnick)
+ self.protocol.irc_RPL_WELCOME('prefix', ['param'])
+ self.protocol.setNick(newnick)
+ self.assertEqual(self.protocol.nickname, oldnick)
+ self.protocol.irc_NICK('%s!quux@qux' % (oldnick,), [newnick])
+ self.assertEqual(self.protocol.nickname, newnick)
+
+
+ def test_erroneousNick(self):
+ """
+ Trying to register an illegal nickname results in the default legal
+ nickname being set, and trying to change a nickname to an illegal
+ nickname results in the old nickname being kept.
+ """
+ # Registration case: change illegal nickname to erroneousNickFallback
+ badnick = 'foo'
+ self.assertEqual(self.protocol._registered, False)
+ self.protocol.register(badnick)
+ self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param'])
+ lastLine = self.getLastLine(self.transport)
+ self.assertEqual(
+ lastLine, 'NICK %s' % (self.protocol.erroneousNickFallback,))
+ self.protocol.irc_RPL_WELCOME('prefix', ['param'])
+ self.assertEqual(self.protocol._registered, True)
+ self.protocol.setNick(self.protocol.erroneousNickFallback)
+ self.assertEqual(
+ self.protocol.nickname, self.protocol.erroneousNickFallback)
+
+ # Illegal nick change attempt after registration. Fall back to the old
+ # nickname instead of erroneousNickFallback.
+ oldnick = self.protocol.nickname
+ self.protocol.setNick(badnick)
+ self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param'])
+ lastLine = self.getLastLine(self.transport)
+ self.assertEqual(
+ lastLine, 'NICK %s' % (badnick,))
+ self.assertEqual(self.protocol.nickname, oldnick)
+
+
+ def test_describe(self):
+ """
+ L{IRCClient.desrcibe} sends a CTCP ACTION message to the target
+ specified.
+ """
+ target = 'foo'
+ channel = '#bar'
+ action = 'waves'
+ self.protocol.describe(target, action)
+ self.protocol.describe(channel, action)
+ expected = [
+ 'PRIVMSG %s :\01ACTION %s\01' % (target, action),
+ 'PRIVMSG %s :\01ACTION %s\01' % (channel, action),
+ '']
+ self.assertEqual(self.transport.value().split('\r\n'), expected)
+
+
+ def test_noticedDoesntPrivmsg(self):
+ """
+ The default implementation of L{IRCClient.noticed} doesn't invoke
+ C{privmsg()}
+ """
+ def privmsg(user, channel, message):
+ self.fail("privmsg() should not have been called")
+ self.protocol.privmsg = privmsg
+ self.protocol.irc_NOTICE(
+ 'spam', ['#greasyspooncafe', "I don't want any spam!"])
+
+
+
+class DccChatFactoryTests(unittest.TestCase):
+ """
+ Tests for L{DccChatFactory}
+ """
+ def test_buildProtocol(self):
+ """
+ An instance of the DccChat protocol is returned, which has the factory
+ property set to the factory which created it.
+ """
+ queryData = ('fromUser', None, None)
+ f = irc.DccChatFactory(None, queryData)
+ p = f.buildProtocol('127.0.0.1')
+ self.assertTrue(isinstance(p, irc.DccChat))
+ self.assertEqual(p.factory, f)
diff --git a/twisted/words/test/test_irc_service.py b/twisted/words/test/test_irc_service.py
new file mode 100644
index 0000000..f3ed292
--- /dev/null
+++ b/twisted/words/test/test_irc_service.py
@@ -0,0 +1,216 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for IRC portions of L{twisted.words.service}.
+"""
+
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+from twisted.words.service import InMemoryWordsRealm, IRCFactory, IRCUser
+from twisted.words.protocols import irc
+from twisted.cred import checkers, portal
+
+class IRCUserTestCase(unittest.TestCase):
+ """
+ Isolated tests for L{IRCUser}
+ """
+
+ def setUp(self):
+ """
+ Sets up a Realm, Portal, Factory, IRCUser, Transport, and Connection
+ for our tests.
+ """
+ self.wordsRealm = InMemoryWordsRealm("example.com")
+ self.portal = portal.Portal(self.wordsRealm,
+ [checkers.InMemoryUsernamePasswordDatabaseDontUse(john="pass")])
+ self.factory = IRCFactory(self.wordsRealm, self.portal)
+ self.ircUser = self.factory.buildProtocol(None)
+ self.stringTransport = proto_helpers.StringTransport()
+ self.ircUser.makeConnection(self.stringTransport)
+
+
+ def test_sendMessage(self):
+ """
+ Sending a message to a user after they have sent NICK, but before they
+ have authenticated, results in a message from "example.com".
+ """
+ self.ircUser.irc_NICK("", ["mynick"])
+ self.stringTransport.clear()
+ self.ircUser.sendMessage("foo")
+ self.assertEqual(":example.com foo mynick\r\n",
+ self.stringTransport.value())
+
+
+ def response(self):
+ """
+ Grabs our responses and then clears the transport
+ """
+ response = self.ircUser.transport.value().splitlines()
+ self.ircUser.transport.clear()
+ return map(irc.parsemsg, response)
+
+
+ def scanResponse(self, response, messageType):
+ """
+ Gets messages out of a response
+
+ @param response: The parsed IRC messages of the response, as returned
+ by L{IRCServiceTestCase.response}
+
+ @param messageType: The string type of the desired messages.
+
+ @return: An iterator which yields 2-tuples of C{(index, ircMessage)}
+ """
+ for n, message in enumerate(response):
+ if (message[1] == messageType):
+ yield n, message
+
+
+ def test_sendNickSendsGreeting(self):
+ """
+ Receiving NICK without authenticating sends the MOTD Start and MOTD End
+ messages, which is required by certain popular IRC clients (such as
+ Pidgin) before a connection is considered to be fully established.
+ """
+ self.ircUser.irc_NICK("", ["mynick"])
+ response = self.response()
+ start = list(self.scanResponse(response, irc.RPL_MOTDSTART))
+ end = list(self.scanResponse(response, irc.RPL_ENDOFMOTD))
+ self.assertEqual(start,
+ [(0, ('example.com', '375', ['mynick', '- example.com Message of the Day - ']))])
+ self.assertEqual(end,
+ [(1, ('example.com', '376', ['mynick', 'End of /MOTD command.']))])
+
+
+ def test_fullLogin(self):
+ """
+ Receiving USER, PASS, NICK will log in the user, and transmit the
+ appropriate response messages.
+ """
+ self.ircUser.irc_USER("", ["john doe"])
+ self.ircUser.irc_PASS("", ["pass"])
+ self.ircUser.irc_NICK("", ["john"])
+
+ version = ('Your host is example.com, running version %s' %
+ (self.factory._serverInfo["serviceVersion"],))
+
+ creation = ('This server was created on %s' %
+ (self.factory._serverInfo["creationDate"],))
+
+ self.assertEqual(self.response(),
+ [('example.com', '375',
+ ['john', '- example.com Message of the Day - ']),
+ ('example.com', '376', ['john', 'End of /MOTD command.']),
+ ('example.com', '001', ['john', 'connected to Twisted IRC']),
+ ('example.com', '002', ['john', version]),
+ ('example.com', '003', ['john', creation]),
+ ('example.com', '004',
+ ['john', 'example.com', self.factory._serverInfo["serviceVersion"],
+ 'w', 'n'])])
+
+
+
+class MocksyIRCUser(IRCUser):
+ def __init__(self):
+ self.mockedCodes = []
+
+ def sendMessage(self, code, *_, **__):
+ self.mockedCodes.append(code)
+
+BADTEXT = '\xff'
+
+class IRCUserBadEncodingTestCase(unittest.TestCase):
+ """
+ Verifies that L{IRCUser} sends the correct error messages back to clients
+ when given indecipherable bytes
+ """
+ # TODO: irc_NICK -- but NICKSERV is used for that, so it isn't as easy.
+
+ def setUp(self):
+ self.ircuser = MocksyIRCUser()
+
+ def assertChokesOnBadBytes(self, irc_x, error):
+ """
+ Asserts that IRCUser sends the relevant error code when a given irc_x
+ dispatch method is given undecodable bytes.
+
+ @param irc_x: the name of the irc_FOO method to test.
+ For example, irc_x = 'PRIVMSG' will check irc_PRIVMSG
+
+ @param error: the error code irc_x should send. For example,
+ irc.ERR_NOTONCHANNEL
+ """
+ getattr(self.ircuser, 'irc_%s' % irc_x)(None, [BADTEXT])
+ self.assertEqual(self.ircuser.mockedCodes, [error])
+
+ # no such channel
+
+ def test_JOIN(self):
+ """
+ Tests that irc_JOIN sends ERR_NOSUCHCHANNEL if the channel name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('JOIN', irc.ERR_NOSUCHCHANNEL)
+
+ def test_NAMES(self):
+ """
+ Tests that irc_NAMES sends ERR_NOSUCHCHANNEL if the channel name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('NAMES', irc.ERR_NOSUCHCHANNEL)
+
+ def test_TOPIC(self):
+ """
+ Tests that irc_TOPIC sends ERR_NOSUCHCHANNEL if the channel name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('TOPIC', irc.ERR_NOSUCHCHANNEL)
+
+ def test_LIST(self):
+ """
+ Tests that irc_LIST sends ERR_NOSUCHCHANNEL if the channel name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('LIST', irc.ERR_NOSUCHCHANNEL)
+
+ # no such nick
+
+ def test_MODE(self):
+ """
+ Tests that irc_MODE sends ERR_NOSUCHNICK if the target name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('MODE', irc.ERR_NOSUCHNICK)
+
+ def test_PRIVMSG(self):
+ """
+ Tests that irc_PRIVMSG sends ERR_NOSUCHNICK if the target name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('PRIVMSG', irc.ERR_NOSUCHNICK)
+
+ def test_WHOIS(self):
+ """
+ Tests that irc_WHOIS sends ERR_NOSUCHNICK if the target name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('WHOIS', irc.ERR_NOSUCHNICK)
+
+ # not on channel
+
+ def test_PART(self):
+ """
+ Tests that irc_PART sends ERR_NOTONCHANNEL if the target name can't
+ be decoded.
+ """
+ self.assertChokesOnBadBytes('PART', irc.ERR_NOTONCHANNEL)
+
+ # probably nothing
+
+ def test_WHO(self):
+ """
+ Tests that irc_WHO immediately ends the WHO list if the target name
+ can't be decoded.
+ """
+ self.assertChokesOnBadBytes('WHO', irc.RPL_ENDOFWHO)
diff --git a/twisted/words/test/test_ircsupport.py b/twisted/words/test/test_ircsupport.py
new file mode 100644
index 0000000..de1f40b
--- /dev/null
+++ b/twisted/words/test/test_ircsupport.py
@@ -0,0 +1,79 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.im.ircsupport}.
+"""
+
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransport
+
+from twisted.words.im.basechat import Conversation, ChatUI
+from twisted.words.im.ircsupport import IRCAccount, IRCProto
+
+
+
+class StubConversation(Conversation):
+ def show(self):
+ pass
+
+
+
+class StubChatUI(ChatUI):
+ def getGroupConversation(self, group, Class=StubConversation, stayHidden=0):
+ return ChatUI.getGroupConversation(self, group, Class, stayHidden)
+
+
+
+class IRCProtoTests(TestCase):
+ """
+ Tests for L{IRCProto}.
+ """
+ def setUp(self):
+ self.account = IRCAccount(
+ "Some account", False, "alice", None, "example.com", 6667)
+ self.proto = IRCProto(self.account, StubChatUI(), None)
+
+
+ def test_login(self):
+ """
+ When L{IRCProto} is connected to a transport, it sends I{NICK} and
+ I{USER} commands with the username from the account object.
+ """
+ transport = StringTransport()
+ self.proto.makeConnection(transport)
+ self.assertEqual(
+ transport.value(),
+ "NICK alice\r\n"
+ "USER alice foo bar :Twisted-IM user\r\n")
+
+
+ def test_authenticate(self):
+ """
+ If created with an account with a password, L{IRCProto} sends a
+ I{PASS} command before the I{NICK} and I{USER} commands.
+ """
+ self.account.password = "secret"
+ transport = StringTransport()
+ self.proto.makeConnection(transport)
+ self.assertEqual(
+ transport.value(),
+ "PASS :secret\r\n"
+ "NICK alice\r\n"
+ "USER alice foo bar :Twisted-IM user\r\n")
+
+
+ def test_channels(self):
+ """
+ If created with an account with a list of channels, L{IRCProto}
+ joins each of those channels after registering.
+ """
+ self.account.channels = ['#foo', '#bar']
+ transport = StringTransport()
+ self.proto.makeConnection(transport)
+ self.assertEqual(
+ transport.value(),
+ "NICK alice\r\n"
+ "USER alice foo bar :Twisted-IM user\r\n"
+ "JOIN #foo\r\n"
+ "JOIN #bar\r\n")
diff --git a/twisted/words/test/test_jabberclient.py b/twisted/words/test/test_jabberclient.py
new file mode 100644
index 0000000..87af883
--- /dev/null
+++ b/twisted/words/test/test_jabberclient.py
@@ -0,0 +1,414 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.client}
+"""
+
+from twisted.internet import defer
+from twisted.python.hashlib import sha1
+from twisted.trial import unittest
+from twisted.words.protocols.jabber import client, error, jid, xmlstream
+from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer
+from twisted.words.xish import utility
+
+IQ_AUTH_GET = '/iq[@type="get"]/query[@xmlns="jabber:iq:auth"]'
+IQ_AUTH_SET = '/iq[@type="set"]/query[@xmlns="jabber:iq:auth"]'
+NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
+IQ_BIND_SET = '/iq[@type="set"]/bind[@xmlns="%s"]' % NS_BIND
+NS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'
+IQ_SESSION_SET = '/iq[@type="set"]/session[@xmlns="%s"]' % NS_SESSION
+
+class CheckVersionInitializerTest(unittest.TestCase):
+ def setUp(self):
+ a = xmlstream.Authenticator()
+ xs = xmlstream.XmlStream(a)
+ self.init = client.CheckVersionInitializer(xs)
+
+
+ def testSupported(self):
+ """
+ Test supported version number 1.0
+ """
+ self.init.xmlstream.version = (1, 0)
+ self.init.initialize()
+
+
+ def testNotSupported(self):
+ """
+ Test unsupported version number 0.0, and check exception.
+ """
+ self.init.xmlstream.version = (0, 0)
+ exc = self.assertRaises(error.StreamError, self.init.initialize)
+ self.assertEqual('unsupported-version', exc.condition)
+
+
+
+class InitiatingInitializerHarness(object):
+ """
+ Testing harness for interacting with XML stream initializers.
+
+ This sets up an L{utility.XmlPipe} to create a communication channel between
+ the initializer and the stubbed receiving entity. It features a sink and
+ source side that both act similarly to a real L{xmlstream.XmlStream}. The
+ sink is augmented with an authenticator to which initializers can be added.
+
+ The harness also provides some utility methods to work with event observers
+ and deferreds.
+ """
+
+ def setUp(self):
+ self.output = []
+ self.pipe = utility.XmlPipe()
+ self.xmlstream = self.pipe.sink
+ self.authenticator = xmlstream.ConnectAuthenticator('example.org')
+ self.xmlstream.authenticator = self.authenticator
+
+
+ def waitFor(self, event, handler):
+ """
+ Observe an output event, returning a deferred.
+
+ The returned deferred will be fired when the given event has been
+ observed on the source end of the L{XmlPipe} tied to the protocol
+ under test. The handler is added as the first callback.
+
+ @param event: The event to be observed. See
+ L{utility.EventDispatcher.addOnetimeObserver}.
+ @param handler: The handler to be called with the observed event object.
+ @rtype: L{defer.Deferred}.
+ """
+ d = defer.Deferred()
+ d.addCallback(handler)
+ self.pipe.source.addOnetimeObserver(event, d.callback)
+ return d
+
+
+
+class IQAuthInitializerTest(InitiatingInitializerHarness, unittest.TestCase):
+ """
+ Tests for L{client.IQAuthInitializer}.
+ """
+
+ def setUp(self):
+ super(IQAuthInitializerTest, self).setUp()
+ self.init = client.IQAuthInitializer(self.xmlstream)
+ self.authenticator.jid = jid.JID('user@example.com/resource')
+ self.authenticator.password = 'secret'
+
+
+ def testPlainText(self):
+ """
+ Test plain-text authentication.
+
+ Act as a server supporting plain-text authentication and expect the
+ C{password} field to be filled with the password. Then act as if
+ authentication succeeds.
+ """
+
+ def onAuthGet(iq):
+ """
+ Called when the initializer sent a query for authentication methods.
+
+ The response informs the client that plain-text authentication
+ is supported.
+ """
+
+ # Create server response
+ response = xmlstream.toResponse(iq, 'result')
+ response.addElement(('jabber:iq:auth', 'query'))
+ response.query.addElement('username')
+ response.query.addElement('password')
+ response.query.addElement('resource')
+
+ # Set up an observer for the next request we expect.
+ d = self.waitFor(IQ_AUTH_SET, onAuthSet)
+
+ # Send server response
+ self.pipe.source.send(response)
+
+ return d
+
+ def onAuthSet(iq):
+ """
+ Called when the initializer sent the authentication request.
+
+ The server checks the credentials and responds with an empty result
+ signalling success.
+ """
+ self.assertEqual('user', unicode(iq.query.username))
+ self.assertEqual('secret', unicode(iq.query.password))
+ self.assertEqual('resource', unicode(iq.query.resource))
+
+ # Send server response
+ response = xmlstream.toResponse(iq, 'result')
+ self.pipe.source.send(response)
+
+ # Set up an observer for the request for authentication fields
+ d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
+
+ # Start the initializer
+ d2 = self.init.initialize()
+ return defer.gatherResults([d1, d2])
+
+
+ def testDigest(self):
+ """
+ Test digest authentication.
+
+ Act as a server supporting digest authentication and expect the
+ C{digest} field to be filled with a sha1 digest of the concatenated
+ stream session identifier and password. Then act as if authentication
+ succeeds.
+ """
+
+ def onAuthGet(iq):
+ """
+ Called when the initializer sent a query for authentication methods.
+
+ The response informs the client that digest authentication is
+ supported.
+ """
+
+ # Create server response
+ response = xmlstream.toResponse(iq, 'result')
+ response.addElement(('jabber:iq:auth', 'query'))
+ response.query.addElement('username')
+ response.query.addElement('digest')
+ response.query.addElement('resource')
+
+ # Set up an observer for the next request we expect.
+ d = self.waitFor(IQ_AUTH_SET, onAuthSet)
+
+ # Send server response
+ self.pipe.source.send(response)
+
+ return d
+
+ def onAuthSet(iq):
+ """
+ Called when the initializer sent the authentication request.
+
+ The server checks the credentials and responds with an empty result
+ signalling success.
+ """
+ self.assertEqual('user', unicode(iq.query.username))
+ self.assertEqual(sha1('12345secret').hexdigest(),
+ unicode(iq.query.digest).encode('utf-8'))
+ self.assertEqual('resource', unicode(iq.query.resource))
+
+ # Send server response
+ response = xmlstream.toResponse(iq, 'result')
+ self.pipe.source.send(response)
+
+ # Digest authentication relies on the stream session identifier. Set it.
+ self.xmlstream.sid = u'12345'
+
+ # Set up an observer for the request for authentication fields
+ d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
+
+ # Start the initializer
+ d2 = self.init.initialize()
+
+ return defer.gatherResults([d1, d2])
+
+
+ def testFailRequestFields(self):
+ """
+ Test initializer failure of request for fields for authentication.
+ """
+ def onAuthGet(iq):
+ """
+ Called when the initializer sent a query for authentication methods.
+
+ The server responds that the client is not authorized to authenticate.
+ """
+ response = error.StanzaError('not-authorized').toResponse(iq)
+ self.pipe.source.send(response)
+
+ # Set up an observer for the request for authentication fields
+ d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
+
+ # Start the initializer
+ d2 = self.init.initialize()
+
+ # The initialized should fail with a stanza error.
+ self.assertFailure(d2, error.StanzaError)
+
+ return defer.gatherResults([d1, d2])
+
+
+ def testFailAuth(self):
+ """
+ Test initializer failure to authenticate.
+ """
+
+ def onAuthGet(iq):
+ """
+ Called when the initializer sent a query for authentication methods.
+
+ The response informs the client that plain-text authentication
+ is supported.
+ """
+
+ # Send server response
+ response = xmlstream.toResponse(iq, 'result')
+ response.addElement(('jabber:iq:auth', 'query'))
+ response.query.addElement('username')
+ response.query.addElement('password')
+ response.query.addElement('resource')
+
+ # Set up an observer for the next request we expect.
+ d = self.waitFor(IQ_AUTH_SET, onAuthSet)
+
+ # Send server response
+ self.pipe.source.send(response)
+
+ return d
+
+ def onAuthSet(iq):
+ """
+ Called when the initializer sent the authentication request.
+
+ The server checks the credentials and responds with a not-authorized
+ stanza error.
+ """
+ response = error.StanzaError('not-authorized').toResponse(iq)
+ self.pipe.source.send(response)
+
+ # Set up an observer for the request for authentication fields
+ d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
+
+ # Start the initializer
+ d2 = self.init.initialize()
+
+ # The initializer should fail with a stanza error.
+ self.assertFailure(d2, error.StanzaError)
+
+ return defer.gatherResults([d1, d2])
+
+
+
+class BindInitializerTest(InitiatingInitializerHarness, unittest.TestCase):
+ """
+ Tests for L{client.BindInitializer}.
+ """
+
+ def setUp(self):
+ super(BindInitializerTest, self).setUp()
+ self.init = client.BindInitializer(self.xmlstream)
+ self.authenticator.jid = jid.JID('user@example.com/resource')
+
+
+ def testBasic(self):
+ """
+ Set up a stream, and act as if resource binding succeeds.
+ """
+ def onBind(iq):
+ response = xmlstream.toResponse(iq, 'result')
+ response.addElement((NS_BIND, 'bind'))
+ response.bind.addElement('jid',
+ content='user@example.com/other resource')
+ self.pipe.source.send(response)
+
+ def cb(result):
+ self.assertEqual(jid.JID('user@example.com/other resource'),
+ self.authenticator.jid)
+
+ d1 = self.waitFor(IQ_BIND_SET, onBind)
+ d2 = self.init.start()
+ d2.addCallback(cb)
+ return defer.gatherResults([d1, d2])
+
+
+ def testFailure(self):
+ """
+ Set up a stream, and act as if resource binding fails.
+ """
+ def onBind(iq):
+ response = error.StanzaError('conflict').toResponse(iq)
+ self.pipe.source.send(response)
+
+ d1 = self.waitFor(IQ_BIND_SET, onBind)
+ d2 = self.init.start()
+ self.assertFailure(d2, error.StanzaError)
+ return defer.gatherResults([d1, d2])
+
+
+
+class SessionInitializerTest(InitiatingInitializerHarness, unittest.TestCase):
+ """
+ Tests for L{client.SessionInitializer}.
+ """
+
+ def setUp(self):
+ super(SessionInitializerTest, self).setUp()
+ self.init = client.SessionInitializer(self.xmlstream)
+
+
+ def testSuccess(self):
+ """
+ Set up a stream, and act as if session establishment succeeds.
+ """
+
+ def onSession(iq):
+ response = xmlstream.toResponse(iq, 'result')
+ self.pipe.source.send(response)
+
+ d1 = self.waitFor(IQ_SESSION_SET, onSession)
+ d2 = self.init.start()
+ return defer.gatherResults([d1, d2])
+
+
+ def testFailure(self):
+ """
+ Set up a stream, and act as if session establishment fails.
+ """
+ def onSession(iq):
+ response = error.StanzaError('forbidden').toResponse(iq)
+ self.pipe.source.send(response)
+
+ d1 = self.waitFor(IQ_SESSION_SET, onSession)
+ d2 = self.init.start()
+ self.assertFailure(d2, error.StanzaError)
+ return defer.gatherResults([d1, d2])
+
+
+
+class XMPPAuthenticatorTest(unittest.TestCase):
+ """
+ Test for both XMPPAuthenticator and XMPPClientFactory.
+ """
+ def testBasic(self):
+ """
+ Test basic operations.
+
+ Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let
+ it produce a protocol instance. Then inspect the instance variables of
+ the authenticator and XML stream objects.
+ """
+ self.client_jid = jid.JID('user@example.com/resource')
+
+ # Get an XmlStream instance. Note that it gets initialized with the
+ # XMPPAuthenticator (that has its associateWithXmlStream called) that
+ # is in turn initialized with the arguments to the factory.
+ xs = client.XMPPClientFactory(self.client_jid,
+ 'secret').buildProtocol(None)
+
+ # test authenticator's instance variables
+ self.assertEqual('example.com', xs.authenticator.otherHost)
+ self.assertEqual(self.client_jid, xs.authenticator.jid)
+ self.assertEqual('secret', xs.authenticator.password)
+
+ # test list of initializers
+ version, tls, sasl, bind, session = xs.initializers
+
+ self.assert_(isinstance(tls, xmlstream.TLSInitiatingInitializer))
+ self.assert_(isinstance(sasl, SASLInitiatingInitializer))
+ self.assert_(isinstance(bind, client.BindInitializer))
+ self.assert_(isinstance(session, client.SessionInitializer))
+
+ self.assertFalse(tls.required)
+ self.assertTrue(sasl.required)
+ self.assertFalse(bind.required)
+ self.assertFalse(session.required)
diff --git a/twisted/words/test/test_jabbercomponent.py b/twisted/words/test/test_jabbercomponent.py
new file mode 100644
index 0000000..d8bb108
--- /dev/null
+++ b/twisted/words/test/test_jabbercomponent.py
@@ -0,0 +1,422 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.component}
+"""
+
+from twisted.python import failure
+from twisted.python.hashlib import sha1
+from twisted.trial import unittest
+from twisted.words.protocols.jabber import component, xmlstream
+from twisted.words.protocols.jabber.jid import JID
+from twisted.words.xish import domish
+from twisted.words.xish.utility import XmlPipe
+
+class DummyTransport:
+ def __init__(self, list):
+ self.list = list
+
+ def write(self, bytes):
+ self.list.append(bytes)
+
+class ComponentInitiatingInitializerTest(unittest.TestCase):
+ def setUp(self):
+ self.output = []
+
+ self.authenticator = xmlstream.Authenticator()
+ self.authenticator.password = 'secret'
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+ self.xmlstream.namespace = 'test:component'
+ self.xmlstream.send = self.output.append
+ self.xmlstream.connectionMade()
+ self.xmlstream.dataReceived(
+ "<stream:stream xmlns='test:component' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1.0'>")
+ self.xmlstream.sid = u'12345'
+ self.init = component.ComponentInitiatingInitializer(self.xmlstream)
+
+ def testHandshake(self):
+ """
+ Test basic operations of component handshake.
+ """
+
+ d = self.init.initialize()
+
+ # the initializer should have sent the handshake request
+
+ handshake = self.output[-1]
+ self.assertEqual('handshake', handshake.name)
+ self.assertEqual('test:component', handshake.uri)
+ self.assertEqual(sha1("%s%s" % ('12345', 'secret')).hexdigest(),
+ unicode(handshake))
+
+ # successful authentication
+
+ handshake.children = []
+ self.xmlstream.dataReceived(handshake.toXml())
+
+ return d
+
+class ComponentAuthTest(unittest.TestCase):
+ def authPassed(self, stream):
+ self.authComplete = True
+
+ def testAuth(self):
+ self.authComplete = False
+ outlist = []
+
+ ca = component.ConnectComponentAuthenticator("cjid", "secret")
+ xs = xmlstream.XmlStream(ca)
+ xs.transport = DummyTransport(outlist)
+
+ xs.addObserver(xmlstream.STREAM_AUTHD_EVENT,
+ self.authPassed)
+
+ # Go...
+ xs.connectionMade()
+ xs.dataReceived("<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' from='cjid' id='12345'>")
+
+ # Calculate what we expect the handshake value to be
+ hv = sha1("%s%s" % ("12345", "secret")).hexdigest()
+
+ self.assertEqual(outlist[1], "<handshake>%s</handshake>" % (hv))
+
+ xs.dataReceived("<handshake/>")
+
+ self.assertEqual(self.authComplete, True)
+
+
+class JabberServiceHarness(component.Service):
+ def __init__(self):
+ self.componentConnectedFlag = False
+ self.componentDisconnectedFlag = False
+ self.transportConnectedFlag = False
+
+ def componentConnected(self, xmlstream):
+ self.componentConnectedFlag = True
+
+ def componentDisconnected(self):
+ self.componentDisconnectedFlag = True
+
+ def transportConnected(self, xmlstream):
+ self.transportConnectedFlag = True
+
+
+class TestJabberServiceManager(unittest.TestCase):
+ def testSM(self):
+ # Setup service manager and test harnes
+ sm = component.ServiceManager("foo", "password")
+ svc = JabberServiceHarness()
+ svc.setServiceParent(sm)
+
+ # Create a write list
+ wlist = []
+
+ # Setup a XmlStream
+ xs = sm.getFactory().buildProtocol(None)
+ xs.transport = self
+ xs.transport.write = wlist.append
+
+ # Indicate that it's connected
+ xs.connectionMade()
+
+ # Ensure the test service harness got notified
+ self.assertEqual(True, svc.transportConnectedFlag)
+
+ # Jump ahead and pretend like the stream got auth'd
+ xs.dispatch(xs, xmlstream.STREAM_AUTHD_EVENT)
+
+ # Ensure the test service harness got notified
+ self.assertEqual(True, svc.componentConnectedFlag)
+
+ # Pretend to drop the connection
+ xs.connectionLost(None)
+
+ # Ensure the test service harness got notified
+ self.assertEqual(True, svc.componentDisconnectedFlag)
+
+
+
+class RouterTest(unittest.TestCase):
+ """
+ Tests for L{component.Router}.
+ """
+
+ def test_addRoute(self):
+ """
+ Test route registration and routing on incoming stanzas.
+ """
+ router = component.Router()
+ routed = []
+ router.route = lambda element: routed.append(element)
+
+ pipe = XmlPipe()
+ router.addRoute('example.org', pipe.sink)
+ self.assertEqual(1, len(router.routes))
+ self.assertEqual(pipe.sink, router.routes['example.org'])
+
+ element = domish.Element(('testns', 'test'))
+ pipe.source.send(element)
+ self.assertEqual([element], routed)
+
+
+ def test_route(self):
+ """
+ Test routing of a message.
+ """
+ component1 = XmlPipe()
+ component2 = XmlPipe()
+ router = component.Router()
+ router.addRoute('component1.example.org', component1.sink)
+ router.addRoute('component2.example.org', component2.sink)
+
+ outgoing = []
+ component2.source.addObserver('/*',
+ lambda element: outgoing.append(element))
+ stanza = domish.Element((None, 'presence'))
+ stanza['from'] = 'component1.example.org'
+ stanza['to'] = 'component2.example.org'
+ component1.source.send(stanza)
+ self.assertEqual([stanza], outgoing)
+
+
+ def test_routeDefault(self):
+ """
+ Test routing of a message using the default route.
+
+ The default route is the one with C{None} as its key in the
+ routing table. It is taken when there is no more specific route
+ in the routing table that matches the stanza's destination.
+ """
+ component1 = XmlPipe()
+ s2s = XmlPipe()
+ router = component.Router()
+ router.addRoute('component1.example.org', component1.sink)
+ router.addRoute(None, s2s.sink)
+
+ outgoing = []
+ s2s.source.addObserver('/*', lambda element: outgoing.append(element))
+ stanza = domish.Element((None, 'presence'))
+ stanza['from'] = 'component1.example.org'
+ stanza['to'] = 'example.com'
+ component1.source.send(stanza)
+ self.assertEqual([stanza], outgoing)
+
+
+
+class ListenComponentAuthenticatorTest(unittest.TestCase):
+ """
+ Tests for L{component.ListenComponentAuthenticator}.
+ """
+
+ def setUp(self):
+ self.output = []
+ authenticator = component.ListenComponentAuthenticator('secret')
+ self.xmlstream = xmlstream.XmlStream(authenticator)
+ self.xmlstream.send = self.output.append
+
+
+ def loseConnection(self):
+ """
+ Stub loseConnection because we are a transport.
+ """
+ self.xmlstream.connectionLost("no reason")
+
+
+ def test_streamStarted(self):
+ """
+ The received stream header should set several attributes.
+ """
+ observers = []
+
+ def addOnetimeObserver(event, observerfn):
+ observers.append((event, observerfn))
+
+ xs = self.xmlstream
+ xs.addOnetimeObserver = addOnetimeObserver
+
+ xs.makeConnection(self)
+ self.assertIdentical(None, xs.sid)
+ self.assertFalse(xs._headerSent)
+
+ xs.dataReceived("<stream:stream xmlns='jabber:component:accept' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "to='component.example.org'>")
+ self.assertEqual((0, 0), xs.version)
+ self.assertNotIdentical(None, xs.sid)
+ self.assertTrue(xs._headerSent)
+ self.assertEqual(('/*', xs.authenticator.onElement), observers[-1])
+
+
+ def test_streamStartedWrongNamespace(self):
+ """
+ The received stream header should have a correct namespace.
+ """
+ streamErrors = []
+
+ xs = self.xmlstream
+ xs.sendStreamError = streamErrors.append
+ xs.makeConnection(self)
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "to='component.example.org'>")
+ self.assertEqual(1, len(streamErrors))
+ self.assertEqual('invalid-namespace', streamErrors[-1].condition)
+
+
+ def test_streamStartedNoTo(self):
+ """
+ The received stream header should have a 'to' attribute.
+ """
+ streamErrors = []
+
+ xs = self.xmlstream
+ xs.sendStreamError = streamErrors.append
+ xs.makeConnection(self)
+ xs.dataReceived("<stream:stream xmlns='jabber:component:accept' "
+ "xmlns:stream='http://etherx.jabber.org/streams'>")
+ self.assertEqual(1, len(streamErrors))
+ self.assertEqual('improper-addressing', streamErrors[-1].condition)
+
+
+ def test_onElement(self):
+ """
+ We expect a handshake element with a hash.
+ """
+ handshakes = []
+
+ xs = self.xmlstream
+ xs.authenticator.onHandshake = handshakes.append
+
+ handshake = domish.Element(('jabber:component:accept', 'handshake'))
+ handshake.addContent('1234')
+ xs.authenticator.onElement(handshake)
+ self.assertEqual('1234', handshakes[-1])
+
+ def test_onElementNotHandshake(self):
+ """
+ Reject elements that are not handshakes
+ """
+ handshakes = []
+ streamErrors = []
+
+ xs = self.xmlstream
+ xs.authenticator.onHandshake = handshakes.append
+ xs.sendStreamError = streamErrors.append
+
+ element = domish.Element(('jabber:component:accept', 'message'))
+ xs.authenticator.onElement(element)
+ self.assertFalse(handshakes)
+ self.assertEqual('not-authorized', streamErrors[-1].condition)
+
+
+ def test_onHandshake(self):
+ """
+ Receiving a handshake matching the secret authenticates the stream.
+ """
+ authd = []
+
+ def authenticated(xs):
+ authd.append(xs)
+
+ xs = self.xmlstream
+ xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
+ xs.sid = u'1234'
+ theHash = '32532c0f7dbf1253c095b18b18e36d38d94c1256'
+ xs.authenticator.onHandshake(theHash)
+ self.assertEqual('<handshake/>', self.output[-1])
+ self.assertEqual(1, len(authd))
+
+
+ def test_onHandshakeWrongHash(self):
+ """
+ Receiving a bad handshake should yield a stream error.
+ """
+ streamErrors = []
+ authd = []
+
+ def authenticated(xs):
+ authd.append(xs)
+
+ xs = self.xmlstream
+ xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
+ xs.sendStreamError = streamErrors.append
+
+ xs.sid = u'1234'
+ theHash = '1234'
+ xs.authenticator.onHandshake(theHash)
+ self.assertEqual('not-authorized', streamErrors[-1].condition)
+ self.assertEqual(0, len(authd))
+
+
+
+class XMPPComponentServerFactoryTest(unittest.TestCase):
+ """
+ Tests for L{component.XMPPComponentServerFactory}.
+ """
+
+ def setUp(self):
+ self.router = component.Router()
+ self.factory = component.XMPPComponentServerFactory(self.router,
+ 'secret')
+ self.xmlstream = self.factory.buildProtocol(None)
+ self.xmlstream.thisEntity = JID('component.example.org')
+
+
+ def test_makeConnection(self):
+ """
+ A new connection increases the stream serial count. No logs by default.
+ """
+ self.xmlstream.dispatch(self.xmlstream,
+ xmlstream.STREAM_CONNECTED_EVENT)
+ self.assertEqual(0, self.xmlstream.serial)
+ self.assertEqual(1, self.factory.serial)
+ self.assertIdentical(None, self.xmlstream.rawDataInFn)
+ self.assertIdentical(None, self.xmlstream.rawDataOutFn)
+
+
+ def test_makeConnectionLogTraffic(self):
+ """
+ Setting logTraffic should set up raw data loggers.
+ """
+ self.factory.logTraffic = True
+ self.xmlstream.dispatch(self.xmlstream,
+ xmlstream.STREAM_CONNECTED_EVENT)
+ self.assertNotIdentical(None, self.xmlstream.rawDataInFn)
+ self.assertNotIdentical(None, self.xmlstream.rawDataOutFn)
+
+
+ def test_onError(self):
+ """
+ An observer for stream errors should trigger onError to log it.
+ """
+ self.xmlstream.dispatch(self.xmlstream,
+ xmlstream.STREAM_CONNECTED_EVENT)
+
+ class TestError(Exception):
+ pass
+
+ reason = failure.Failure(TestError())
+ self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT)
+ self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
+
+
+ def test_connectionInitialized(self):
+ """
+ Make sure a new stream is added to the routing table.
+ """
+ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+ self.assertIn('component.example.org', self.router.routes)
+ self.assertIdentical(self.xmlstream,
+ self.router.routes['component.example.org'])
+
+
+ def test_connectionLost(self):
+ """
+ Make sure a stream is removed from the routing table on disconnect.
+ """
+ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
+ self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT)
+ self.assertNotIn('component.example.org', self.router.routes)
diff --git a/twisted/words/test/test_jabbererror.py b/twisted/words/test/test_jabbererror.py
new file mode 100644
index 0000000..45d8dac
--- /dev/null
+++ b/twisted/words/test/test_jabbererror.py
@@ -0,0 +1,342 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.error}.
+"""
+
+from twisted.trial import unittest
+
+from twisted.words.protocols.jabber import error
+from twisted.words.xish import domish
+
+NS_XML = 'http://www.w3.org/XML/1998/namespace'
+NS_STREAMS = 'http://etherx.jabber.org/streams'
+NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'
+NS_XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+
+class BaseErrorTest(unittest.TestCase):
+
+ def test_getElementPlain(self):
+ """
+ Test getting an element for a plain error.
+ """
+ e = error.BaseError('feature-not-implemented')
+ element = e.getElement()
+ self.assertIdentical(element.uri, None)
+ self.assertEqual(len(element.children), 1)
+
+ def test_getElementText(self):
+ """
+ Test getting an element for an error with a text.
+ """
+ e = error.BaseError('feature-not-implemented', 'text')
+ element = e.getElement()
+ self.assertEqual(len(element.children), 2)
+ self.assertEqual(unicode(element.text), 'text')
+ self.assertEqual(element.text.getAttribute((NS_XML, 'lang')), None)
+
+ def test_getElementTextLang(self):
+ """
+ Test getting an element for an error with a text and language.
+ """
+ e = error.BaseError('feature-not-implemented', 'text', 'en_US')
+ element = e.getElement()
+ self.assertEqual(len(element.children), 2)
+ self.assertEqual(unicode(element.text), 'text')
+ self.assertEqual(element.text[(NS_XML, 'lang')], 'en_US')
+
+ def test_getElementAppCondition(self):
+ """
+ Test getting an element for an error with an app specific condition.
+ """
+ ac = domish.Element(('testns', 'myerror'))
+ e = error.BaseError('feature-not-implemented', appCondition=ac)
+ element = e.getElement()
+ self.assertEqual(len(element.children), 2)
+ self.assertEqual(element.myerror, ac)
+
+class StreamErrorTest(unittest.TestCase):
+
+ def test_getElementPlain(self):
+ """
+ Test namespace of the element representation of an error.
+ """
+ e = error.StreamError('feature-not-implemented')
+ element = e.getElement()
+ self.assertEqual(element.uri, NS_STREAMS)
+
+ def test_getElementConditionNamespace(self):
+ """
+ Test that the error condition element has the correct namespace.
+ """
+ e = error.StreamError('feature-not-implemented')
+ element = e.getElement()
+ self.assertEqual(NS_XMPP_STREAMS, getattr(element, 'feature-not-implemented').uri)
+
+ def test_getElementTextNamespace(self):
+ """
+ Test that the error text element has the correct namespace.
+ """
+ e = error.StreamError('feature-not-implemented', 'text')
+ element = e.getElement()
+ self.assertEqual(NS_XMPP_STREAMS, element.text.uri)
+
+
+
+class StanzaErrorTest(unittest.TestCase):
+ """
+ Tests for L{error.StreamError}.
+ """
+
+
+ def test_typeRemoteServerTimeout(self):
+ """
+ Remote Server Timeout should yield type wait, code 504.
+ """
+ e = error.StanzaError('remote-server-timeout')
+ self.assertEqual('wait', e.type)
+ self.assertEqual('504', e.code)
+
+
+ def test_getElementPlain(self):
+ """
+ Test getting an element for a plain stanza error.
+ """
+ e = error.StanzaError('feature-not-implemented')
+ element = e.getElement()
+ self.assertEqual(element.uri, None)
+ self.assertEqual(element['type'], 'cancel')
+ self.assertEqual(element['code'], '501')
+
+
+ def test_getElementType(self):
+ """
+ Test getting an element for a stanza error with a given type.
+ """
+ e = error.StanzaError('feature-not-implemented', 'auth')
+ element = e.getElement()
+ self.assertEqual(element.uri, None)
+ self.assertEqual(element['type'], 'auth')
+ self.assertEqual(element['code'], '501')
+
+
+ def test_getElementConditionNamespace(self):
+ """
+ Test that the error condition element has the correct namespace.
+ """
+ e = error.StanzaError('feature-not-implemented')
+ element = e.getElement()
+ self.assertEqual(NS_XMPP_STANZAS, getattr(element, 'feature-not-implemented').uri)
+
+
+ def test_getElementTextNamespace(self):
+ """
+ Test that the error text element has the correct namespace.
+ """
+ e = error.StanzaError('feature-not-implemented', text='text')
+ element = e.getElement()
+ self.assertEqual(NS_XMPP_STANZAS, element.text.uri)
+
+
+ def test_toResponse(self):
+ """
+ Test an error response is generated from a stanza.
+
+ The addressing on the (new) response stanza should be reversed, an
+ error child (with proper properties) added and the type set to
+ C{'error'}.
+ """
+ stanza = domish.Element(('jabber:client', 'message'))
+ stanza['type'] = 'chat'
+ stanza['to'] = 'user1@example.com'
+ stanza['from'] = 'user2@example.com/resource'
+ e = error.StanzaError('service-unavailable')
+ response = e.toResponse(stanza)
+ self.assertNotIdentical(response, stanza)
+ self.assertEqual(response['from'], 'user1@example.com')
+ self.assertEqual(response['to'], 'user2@example.com/resource')
+ self.assertEqual(response['type'], 'error')
+ self.assertEqual(response.error.children[0].name,
+ 'service-unavailable')
+ self.assertEqual(response.error['type'], 'cancel')
+ self.assertNotEqual(stanza.children, response.children)
+
+
+
+class ParseErrorTest(unittest.TestCase):
+ """
+ Tests for L{error._parseError}.
+ """
+
+
+ def setUp(self):
+ self.error = domish.Element((None, 'error'))
+
+
+ def test_empty(self):
+ """
+ Test parsing of the empty error element.
+ """
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual({'condition': None,
+ 'text': None,
+ 'textLang': None,
+ 'appCondition': None}, result)
+
+
+ def test_condition(self):
+ """
+ Test parsing of an error element with a condition.
+ """
+ self.error.addElement(('errorns', 'bad-request'))
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual('bad-request', result['condition'])
+
+
+ def test_text(self):
+ """
+ Test parsing of an error element with a text.
+ """
+ text = self.error.addElement(('errorns', 'text'))
+ text.addContent('test')
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual('test', result['text'])
+ self.assertEqual(None, result['textLang'])
+
+
+ def test_textLang(self):
+ """
+ Test parsing of an error element with a text with a defined language.
+ """
+ text = self.error.addElement(('errorns', 'text'))
+ text[NS_XML, 'lang'] = 'en_US'
+ text.addContent('test')
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual('en_US', result['textLang'])
+
+
+ def test_textLangInherited(self):
+ """
+ Test parsing of an error element with a text with inherited language.
+ """
+ text = self.error.addElement(('errorns', 'text'))
+ self.error[NS_XML, 'lang'] = 'en_US'
+ text.addContent('test')
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual('en_US', result['textLang'])
+ test_textLangInherited.todo = "xml:lang inheritance not implemented"
+
+
+ def test_appCondition(self):
+ """
+ Test parsing of an error element with an app specific condition.
+ """
+ condition = self.error.addElement(('testns', 'condition'))
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual(condition, result['appCondition'])
+
+
+ def test_appConditionMultiple(self):
+ """
+ Test parsing of an error element with multiple app specific conditions.
+ """
+ self.error.addElement(('testns', 'condition'))
+ condition = self.error.addElement(('testns', 'condition2'))
+ result = error._parseError(self.error, 'errorns')
+ self.assertEqual(condition, result['appCondition'])
+
+
+
+class ExceptionFromStanzaTest(unittest.TestCase):
+
+ def test_basic(self):
+ """
+ Test basic operations of exceptionFromStanza.
+
+ Given a realistic stanza, check if a sane exception is returned.
+
+ Using this stanza::
+
+ <iq type='error'
+ from='pubsub.shakespeare.lit'
+ to='francisco@denmark.lit/barracks'
+ id='subscriptions1'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ <subscriptions/>
+ </pubsub>
+ <error type='cancel'>
+ <feature-not-implemented
+ xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ <unsupported xmlns='http://jabber.org/protocol/pubsub#errors'
+ feature='retrieve-subscriptions'/>
+ </error>
+ </iq>
+ """
+
+ stanza = domish.Element((None, 'stanza'))
+ p = stanza.addElement(('http://jabber.org/protocol/pubsub', 'pubsub'))
+ p.addElement('subscriptions')
+ e = stanza.addElement('error')
+ e['type'] = 'cancel'
+ e.addElement((NS_XMPP_STANZAS, 'feature-not-implemented'))
+ uc = e.addElement(('http://jabber.org/protocol/pubsub#errors',
+ 'unsupported'))
+ uc['feature'] = 'retrieve-subscriptions'
+
+ result = error.exceptionFromStanza(stanza)
+ self.assert_(isinstance(result, error.StanzaError))
+ self.assertEqual('feature-not-implemented', result.condition)
+ self.assertEqual('cancel', result.type)
+ self.assertEqual(uc, result.appCondition)
+ self.assertEqual([p], result.children)
+
+ def test_legacy(self):
+ """
+ Test legacy operations of exceptionFromStanza.
+
+ Given a realistic stanza with only legacy (pre-XMPP) error information,
+ check if a sane exception is returned.
+
+ Using this stanza::
+
+ <message type='error'
+ to='piers@pipetree.com/Home'
+ from='qmacro@jaber.org'>
+ <body>Are you there?</body>
+ <error code='502'>Unable to resolve hostname.</error>
+ </message>
+ """
+ stanza = domish.Element((None, 'stanza'))
+ p = stanza.addElement('body', content='Are you there?')
+ e = stanza.addElement('error', content='Unable to resolve hostname.')
+ e['code'] = '502'
+
+ result = error.exceptionFromStanza(stanza)
+ self.assert_(isinstance(result, error.StanzaError))
+ self.assertEqual('service-unavailable', result.condition)
+ self.assertEqual('wait', result.type)
+ self.assertEqual('Unable to resolve hostname.', result.text)
+ self.assertEqual([p], result.children)
+
+class ExceptionFromStreamErrorTest(unittest.TestCase):
+
+ def test_basic(self):
+ """
+ Test basic operations of exceptionFromStreamError.
+
+ Given a realistic stream error, check if a sane exception is returned.
+
+ Using this error::
+
+ <stream:error xmlns:stream='http://etherx.jabber.org/streams'>
+ <xml-not-well-formed xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
+ </stream:error>
+ """
+
+ e = domish.Element(('http://etherx.jabber.org/streams', 'error'))
+ e.addElement((NS_XMPP_STREAMS, 'xml-not-well-formed'))
+
+ result = error.exceptionFromStreamError(e)
+ self.assert_(isinstance(result, error.StreamError))
+ self.assertEqual('xml-not-well-formed', result.condition)
diff --git a/twisted/words/test/test_jabberjid.py b/twisted/words/test/test_jabberjid.py
new file mode 100644
index 0000000..fa3a119
--- /dev/null
+++ b/twisted/words/test/test_jabberjid.py
@@ -0,0 +1,225 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.jid}.
+"""
+
+from twisted.trial import unittest
+
+from twisted.words.protocols.jabber import jid
+
+class JIDParsingTest(unittest.TestCase):
+ def test_parse(self):
+ """
+ Test different forms of JIDs.
+ """
+ # Basic forms
+ self.assertEqual(jid.parse("user@host/resource"),
+ ("user", "host", "resource"))
+ self.assertEqual(jid.parse("user@host"),
+ ("user", "host", None))
+ self.assertEqual(jid.parse("host"),
+ (None, "host", None))
+ self.assertEqual(jid.parse("host/resource"),
+ (None, "host", "resource"))
+
+ # More interesting forms
+ self.assertEqual(jid.parse("foo/bar@baz"),
+ (None, "foo", "bar@baz"))
+ self.assertEqual(jid.parse("boo@foo/bar@baz"),
+ ("boo", "foo", "bar@baz"))
+ self.assertEqual(jid.parse("boo@foo/bar/baz"),
+ ("boo", "foo", "bar/baz"))
+ self.assertEqual(jid.parse("boo/foo@bar@baz"),
+ (None, "boo", "foo@bar@baz"))
+ self.assertEqual(jid.parse("boo/foo/bar"),
+ (None, "boo", "foo/bar"))
+ self.assertEqual(jid.parse("boo//foo"),
+ (None, "boo", "/foo"))
+
+ def test_noHost(self):
+ """
+ Test for failure on no host part.
+ """
+ self.assertRaises(jid.InvalidFormat, jid.parse, "user@")
+
+ def test_doubleAt(self):
+ """
+ Test for failure on double @ signs.
+
+ This should fail because @ is not a valid character for the host
+ part of the JID.
+ """
+ self.assertRaises(jid.InvalidFormat, jid.parse, "user@@host")
+
+ def test_multipleAt(self):
+ """
+ Test for failure on two @ signs.
+
+ This should fail because @ is not a valid character for the host
+ part of the JID.
+ """
+ self.assertRaises(jid.InvalidFormat, jid.parse, "user@host@host")
+
+ # Basic tests for case mapping. These are fallback tests for the
+ # prepping done in twisted.words.protocols.jabber.xmpp_stringprep
+
+ def test_prepCaseMapUser(self):
+ """
+ Test case mapping of the user part of the JID.
+ """
+ self.assertEqual(jid.prep("UsEr", "host", "resource"),
+ ("user", "host", "resource"))
+
+ def test_prepCaseMapHost(self):
+ """
+ Test case mapping of the host part of the JID.
+ """
+ self.assertEqual(jid.prep("user", "hoST", "resource"),
+ ("user", "host", "resource"))
+
+ def test_prepNoCaseMapResource(self):
+ """
+ Test no case mapping of the resourcce part of the JID.
+ """
+ self.assertEqual(jid.prep("user", "hoST", "resource"),
+ ("user", "host", "resource"))
+ self.assertNotEquals(jid.prep("user", "host", "Resource"),
+ ("user", "host", "resource"))
+
+class JIDTest(unittest.TestCase):
+
+ def test_noneArguments(self):
+ """
+ Test that using no arguments raises an exception.
+ """
+ self.assertRaises(RuntimeError, jid.JID)
+
+ def test_attributes(self):
+ """
+ Test that the attributes correspond with the JID parts.
+ """
+ j = jid.JID("user@host/resource")
+ self.assertEqual(j.user, "user")
+ self.assertEqual(j.host, "host")
+ self.assertEqual(j.resource, "resource")
+
+ def test_userhost(self):
+ """
+ Test the extraction of the bare JID.
+ """
+ j = jid.JID("user@host/resource")
+ self.assertEqual("user@host", j.userhost())
+
+ def test_userhostOnlyHost(self):
+ """
+ Test the extraction of the bare JID of the full form host/resource.
+ """
+ j = jid.JID("host/resource")
+ self.assertEqual("host", j.userhost())
+
+ def test_userhostJID(self):
+ """
+ Test getting a JID object of the bare JID.
+ """
+ j1 = jid.JID("user@host/resource")
+ j2 = jid.internJID("user@host")
+ self.assertIdentical(j2, j1.userhostJID())
+
+ def test_userhostJIDNoResource(self):
+ """
+ Test getting a JID object of the bare JID when there was no resource.
+ """
+ j = jid.JID("user@host")
+ self.assertIdentical(j, j.userhostJID())
+
+ def test_fullHost(self):
+ """
+ Test giving a string representation of the JID with only a host part.
+ """
+ j = jid.JID(tuple=(None, 'host', None))
+ self.assertEqual('host', j.full())
+
+ def test_fullHostResource(self):
+ """
+ Test giving a string representation of the JID with host, resource.
+ """
+ j = jid.JID(tuple=(None, 'host', 'resource'))
+ self.assertEqual('host/resource', j.full())
+
+ def test_fullUserHost(self):
+ """
+ Test giving a string representation of the JID with user, host.
+ """
+ j = jid.JID(tuple=('user', 'host', None))
+ self.assertEqual('user@host', j.full())
+
+ def test_fullAll(self):
+ """
+ Test giving a string representation of the JID.
+ """
+ j = jid.JID(tuple=('user', 'host', 'resource'))
+ self.assertEqual('user@host/resource', j.full())
+
+ def test_equality(self):
+ """
+ Test JID equality.
+ """
+ j1 = jid.JID("user@host/resource")
+ j2 = jid.JID("user@host/resource")
+ self.assertNotIdentical(j1, j2)
+ self.assertEqual(j1, j2)
+
+ def test_equalityWithNonJIDs(self):
+ """
+ Test JID equality.
+ """
+ j = jid.JID("user@host/resource")
+ self.assertFalse(j == 'user@host/resource')
+
+ def test_inequality(self):
+ """
+ Test JID inequality.
+ """
+ j1 = jid.JID("user1@host/resource")
+ j2 = jid.JID("user2@host/resource")
+ self.assertNotEqual(j1, j2)
+
+ def test_inequalityWithNonJIDs(self):
+ """
+ Test JID equality.
+ """
+ j = jid.JID("user@host/resource")
+ self.assertNotEqual(j, 'user@host/resource')
+
+ def test_hashable(self):
+ """
+ Test JID hashability.
+ """
+ j1 = jid.JID("user@host/resource")
+ j2 = jid.JID("user@host/resource")
+ self.assertEqual(hash(j1), hash(j2))
+
+ def test_unicode(self):
+ """
+ Test unicode representation of JIDs.
+ """
+ j = jid.JID(tuple=('user', 'host', 'resource'))
+ self.assertEqual("user@host/resource", unicode(j))
+
+ def test_repr(self):
+ """
+ Test representation of JID objects.
+ """
+ j = jid.JID(tuple=('user', 'host', 'resource'))
+ self.assertEqual("JID(u'user@host/resource')", repr(j))
+
+class InternJIDTest(unittest.TestCase):
+ def test_identity(self):
+ """
+ Test that two interned JIDs yield the same object.
+ """
+ j1 = jid.internJID("user@host")
+ j2 = jid.internJID("user@host")
+ self.assertIdentical(j1, j2)
diff --git a/twisted/words/test/test_jabberjstrports.py b/twisted/words/test/test_jabberjstrports.py
new file mode 100644
index 0000000..6d8f045
--- /dev/null
+++ b/twisted/words/test/test_jabberjstrports.py
@@ -0,0 +1,34 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.jstrports}.
+"""
+
+from twisted.trial import unittest
+
+from twisted.words.protocols.jabber import jstrports
+from twisted.application.internet import TCPClient
+
+
+class JabberStrPortsPlaceHolderTest(unittest.TestCase):
+ """
+ Tests for L{jstrports}
+ """
+
+ def test_parse(self):
+ """
+ L{jstrports.parse} accepts an endpoint description string and returns a
+ tuple and dict of parsed endpoint arguments.
+ """
+ expected = ('TCP', ('DOMAIN', 65535, 'Factory'), {})
+ got = jstrports.parse("tcp:DOMAIN:65535", "Factory")
+ self.assertEqual(expected, got)
+
+
+ def test_client(self):
+ """
+ L{jstrports.client} returns a L{TCPClient} service.
+ """
+ got = jstrports.client("tcp:DOMAIN:65535", "Factory")
+ self.assertIsInstance(got, TCPClient)
diff --git a/twisted/words/test/test_jabbersasl.py b/twisted/words/test/test_jabbersasl.py
new file mode 100644
index 0000000..b22f956
--- /dev/null
+++ b/twisted/words/test/test_jabbersasl.py
@@ -0,0 +1,272 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from zope.interface import implements
+from twisted.internet import defer
+from twisted.trial import unittest
+from twisted.words.protocols.jabber import sasl, sasl_mechanisms, xmlstream, jid
+from twisted.words.xish import domish
+
+NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
+
+class DummySASLMechanism(object):
+ """
+ Dummy SASL mechanism.
+
+ This just returns the initialResponse passed on creation, stores any
+ challenges and replies with an empty response.
+
+ @ivar challenge: Last received challenge.
+ @type challenge: C{unicode}.
+ @ivar initialResponse: Initial response to be returned when requested
+ via C{getInitialResponse} or C{None}.
+ @type initialResponse: C{unicode}
+ """
+
+ implements(sasl_mechanisms.ISASLMechanism)
+
+ challenge = None
+ name = "DUMMY"
+
+ def __init__(self, initialResponse):
+ self.initialResponse = initialResponse
+
+ def getInitialResponse(self):
+ return self.initialResponse
+
+ def getResponse(self, challenge):
+ self.challenge = challenge
+ return ""
+
+class DummySASLInitiatingInitializer(sasl.SASLInitiatingInitializer):
+ """
+ Dummy SASL Initializer for initiating entities.
+
+ This hardwires the SASL mechanism to L{DummySASLMechanism}, that is
+ instantiated with the value of C{initialResponse}.
+
+ @ivar initialResponse: The initial response to be returned by the
+ dummy SASL mechanism or C{None}.
+ @type initialResponse: C{unicode}.
+ """
+
+ initialResponse = None
+
+ def setMechanism(self):
+ self.mechanism = DummySASLMechanism(self.initialResponse)
+
+
+
+class SASLInitiatingInitializerTest(unittest.TestCase):
+ """
+ Tests for L{sasl.SASLInitiatingInitializer}
+ """
+
+ def setUp(self):
+ self.output = []
+
+ self.authenticator = xmlstream.Authenticator()
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+ self.xmlstream.send = self.output.append
+ self.xmlstream.connectionMade()
+ self.xmlstream.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1.0'>")
+ self.init = DummySASLInitiatingInitializer(self.xmlstream)
+
+
+ def test_onFailure(self):
+ """
+ Test that the SASL error condition is correctly extracted.
+ """
+ failure = domish.Element(('urn:ietf:params:xml:ns:xmpp-sasl',
+ 'failure'))
+ failure.addElement('not-authorized')
+ self.init._deferred = defer.Deferred()
+ self.init.onFailure(failure)
+ self.assertFailure(self.init._deferred, sasl.SASLAuthError)
+ self.init._deferred.addCallback(lambda e:
+ self.assertEqual('not-authorized',
+ e.condition))
+ return self.init._deferred
+
+
+ def test_sendAuthInitialResponse(self):
+ """
+ Test starting authentication with an initial response.
+ """
+ self.init.initialResponse = "dummy"
+ self.init.start()
+ auth = self.output[0]
+ self.assertEqual(NS_XMPP_SASL, auth.uri)
+ self.assertEqual('auth', auth.name)
+ self.assertEqual('DUMMY', auth['mechanism'])
+ self.assertEqual('ZHVtbXk=', str(auth))
+
+
+ def test_sendAuthNoInitialResponse(self):
+ """
+ Test starting authentication without an initial response.
+ """
+ self.init.initialResponse = None
+ self.init.start()
+ auth = self.output[0]
+ self.assertEqual('', str(auth))
+
+
+ def test_sendAuthEmptyInitialResponse(self):
+ """
+ Test starting authentication where the initial response is empty.
+ """
+ self.init.initialResponse = ""
+ self.init.start()
+ auth = self.output[0]
+ self.assertEqual('=', str(auth))
+
+
+ def test_onChallenge(self):
+ """
+ Test receiving a challenge message.
+ """
+ d = self.init.start()
+ challenge = domish.Element((NS_XMPP_SASL, 'challenge'))
+ challenge.addContent('bXkgY2hhbGxlbmdl')
+ self.init.onChallenge(challenge)
+ self.assertEqual('my challenge', self.init.mechanism.challenge)
+ self.init.onSuccess(None)
+ return d
+
+
+ def test_onChallengeEmpty(self):
+ """
+ Test receiving an empty challenge message.
+ """
+ d = self.init.start()
+ challenge = domish.Element((NS_XMPP_SASL, 'challenge'))
+ self.init.onChallenge(challenge)
+ self.assertEqual('', self.init.mechanism.challenge)
+ self.init.onSuccess(None)
+ return d
+
+
+ def test_onChallengeIllegalPadding(self):
+ """
+ Test receiving a challenge message with illegal padding.
+ """
+ d = self.init.start()
+ challenge = domish.Element((NS_XMPP_SASL, 'challenge'))
+ challenge.addContent('bXkg=Y2hhbGxlbmdl')
+ self.init.onChallenge(challenge)
+ self.assertFailure(d, sasl.SASLIncorrectEncodingError)
+ return d
+
+
+ def test_onChallengeIllegalCharacters(self):
+ """
+ Test receiving a challenge message with illegal characters.
+ """
+ d = self.init.start()
+ challenge = domish.Element((NS_XMPP_SASL, 'challenge'))
+ challenge.addContent('bXkg*Y2hhbGxlbmdl')
+ self.init.onChallenge(challenge)
+ self.assertFailure(d, sasl.SASLIncorrectEncodingError)
+ return d
+
+
+ def test_onChallengeMalformed(self):
+ """
+ Test receiving a malformed challenge message.
+ """
+ d = self.init.start()
+ challenge = domish.Element((NS_XMPP_SASL, 'challenge'))
+ challenge.addContent('a')
+ self.init.onChallenge(challenge)
+ self.assertFailure(d, sasl.SASLIncorrectEncodingError)
+ return d
+
+
+class SASLInitiatingInitializerSetMechanismTest(unittest.TestCase):
+ """
+ Test for L{sasl.SASLInitiatingInitializer.setMechanism}.
+ """
+
+ def setUp(self):
+ self.output = []
+
+ self.authenticator = xmlstream.Authenticator()
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+ self.xmlstream.send = self.output.append
+ self.xmlstream.connectionMade()
+ self.xmlstream.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1.0'>")
+
+ self.init = sasl.SASLInitiatingInitializer(self.xmlstream)
+
+
+ def _setMechanism(self, name):
+ """
+ Set up the XML Stream to have a SASL feature with the given mechanism.
+ """
+ feature = domish.Element((NS_XMPP_SASL, 'mechanisms'))
+ feature.addElement('mechanism', content=name)
+ self.xmlstream.features[(feature.uri, feature.name)] = feature
+
+ self.init.setMechanism()
+ return self.init.mechanism.name
+
+
+ def test_anonymous(self):
+ """
+ Test setting ANONYMOUS as the authentication mechanism.
+ """
+ self.authenticator.jid = jid.JID('example.com')
+ self.authenticator.password = None
+ name = "ANONYMOUS"
+
+ self.assertEqual(name, self._setMechanism(name))
+
+
+ def test_plain(self):
+ """
+ Test setting PLAIN as the authentication mechanism.
+ """
+ self.authenticator.jid = jid.JID('test@example.com')
+ self.authenticator.password = 'secret'
+ name = "PLAIN"
+
+ self.assertEqual(name, self._setMechanism(name))
+
+
+ def test_digest(self):
+ """
+ Test setting DIGEST-MD5 as the authentication mechanism.
+ """
+ self.authenticator.jid = jid.JID('test@example.com')
+ self.authenticator.password = 'secret'
+ name = "DIGEST-MD5"
+
+ self.assertEqual(name, self._setMechanism(name))
+
+
+ def test_notAcceptable(self):
+ """
+ Test using an unacceptable SASL authentication mechanism.
+ """
+
+ self.authenticator.jid = jid.JID('test@example.com')
+ self.authenticator.password = 'secret'
+
+ self.assertRaises(sasl.SASLNoAcceptableMechanism,
+ self._setMechanism, 'SOMETHING_UNACCEPTABLE')
+
+
+ def test_notAcceptableWithoutUser(self):
+ """
+ Test using an unacceptable SASL authentication mechanism with no JID.
+ """
+ self.authenticator.jid = jid.JID('example.com')
+ self.authenticator.password = 'secret'
+
+ self.assertRaises(sasl.SASLNoAcceptableMechanism,
+ self._setMechanism, 'SOMETHING_UNACCEPTABLE')
diff --git a/twisted/words/test/test_jabbersaslmechanisms.py b/twisted/words/test/test_jabbersaslmechanisms.py
new file mode 100644
index 0000000..1e195ab
--- /dev/null
+++ b/twisted/words/test/test_jabbersaslmechanisms.py
@@ -0,0 +1,90 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.sasl_mechanisms}.
+"""
+
+from twisted.trial import unittest
+
+from twisted.words.protocols.jabber import sasl_mechanisms
+
+class PlainTest(unittest.TestCase):
+ def test_getInitialResponse(self):
+ """
+ Test the initial response.
+ """
+ m = sasl_mechanisms.Plain(None, 'test', 'secret')
+ self.assertEqual(m.getInitialResponse(), '\x00test\x00secret')
+
+
+
+class AnonymousTest(unittest.TestCase):
+ """
+ Tests for L{twisted.words.protocols.jabber.sasl_mechanisms.Anonymous}.
+ """
+ def test_getInitialResponse(self):
+ """
+ Test the initial response to be empty.
+ """
+ m = sasl_mechanisms.Anonymous()
+ self.assertEqual(m.getInitialResponse(), None)
+
+
+
+class DigestMD5Test(unittest.TestCase):
+ def setUp(self):
+ self.mechanism = sasl_mechanisms.DigestMD5('xmpp', 'example.org', None,
+ 'test', 'secret')
+
+
+ def test_getInitialResponse(self):
+ """
+ Test that no initial response is generated.
+ """
+ self.assertIdentical(self.mechanism.getInitialResponse(), None)
+
+ def test_getResponse(self):
+ """
+ Partially test challenge response.
+
+ Does not actually test the response-value, yet.
+ """
+
+ challenge = 'realm="localhost",nonce="1234",qop="auth",charset=utf-8,algorithm=md5-sess'
+ directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
+ self.assertEqual(directives['username'], 'test')
+ self.assertEqual(directives['nonce'], '1234')
+ self.assertEqual(directives['nc'], '00000001')
+ self.assertEqual(directives['qop'], ['auth'])
+ self.assertEqual(directives['charset'], 'utf-8')
+ self.assertEqual(directives['digest-uri'], 'xmpp/example.org')
+ self.assertEqual(directives['realm'], 'localhost')
+
+ def test_getResponseNoRealm(self):
+ """
+ Test that we accept challenges without realm.
+
+ The realm should default to the host part of the JID.
+ """
+
+ challenge = 'nonce="1234",qop="auth",charset=utf-8,algorithm=md5-sess'
+ directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
+ self.assertEqual(directives['realm'], 'example.org')
+
+ def test__parse(self):
+ """
+ Test challenge decoding.
+
+ Specifically, check for multiple values for the C{qop} and C{cipher}
+ directives.
+ """
+ challenge = 'nonce="1234",qop="auth,auth-conf",charset=utf-8,' \
+ 'algorithm=md5-sess,cipher="des,3des"'
+ directives = self.mechanism._parse(challenge)
+ self.assertEqual('1234', directives['nonce'])
+ self.assertEqual('utf-8', directives['charset'])
+ self.assertIn('auth', directives['qop'])
+ self.assertIn('auth-conf', directives['qop'])
+ self.assertIn('des', directives['cipher'])
+ self.assertIn('3des', directives['cipher'])
diff --git a/twisted/words/test/test_jabberxmlstream.py b/twisted/words/test/test_jabberxmlstream.py
new file mode 100644
index 0000000..1d80af5
--- /dev/null
+++ b/twisted/words/test/test_jabberxmlstream.py
@@ -0,0 +1,1332 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.jabber.xmlstream}.
+"""
+
+from twisted.trial import unittest
+
+from zope.interface.verify import verifyObject
+
+from twisted.internet import defer, task
+from twisted.internet.error import ConnectionLost
+from twisted.internet.interfaces import IProtocolFactory
+from twisted.python import failure
+from twisted.test import proto_helpers
+from twisted.words.test.test_xmlstream import GenericXmlStreamFactoryTestsMixin
+from twisted.words.xish import domish
+from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream
+
+
+
+NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
+
+
+
+class HashPasswordTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.hashPassword}.
+ """
+
+ def test_basic(self):
+ """
+ The sid and secret are concatenated to calculate sha1 hex digest.
+ """
+ hash = xmlstream.hashPassword(u"12345", u"secret")
+ self.assertEqual('99567ee91b2c7cabf607f10cb9f4a3634fa820e0', hash)
+
+
+ def test_sidNotUnicode(self):
+ """
+ The session identifier must be a unicode object.
+ """
+ self.assertRaises(TypeError, xmlstream.hashPassword, "\xc2\xb92345",
+ u"secret")
+
+
+ def test_passwordNotUnicode(self):
+ """
+ The password must be a unicode object.
+ """
+ self.assertRaises(TypeError, xmlstream.hashPassword, u"12345",
+ "secr\xc3\xa9t")
+
+
+ def test_unicodeSecret(self):
+ """
+ The concatenated sid and password must be encoded to UTF-8 before hashing.
+ """
+ hash = xmlstream.hashPassword(u"12345", u"secr\u00e9t")
+ self.assertEqual('659bf88d8f8e179081f7f3b4a8e7d224652d2853', hash)
+
+
+
+class IQTest(unittest.TestCase):
+ """
+ Tests both IQ and the associated IIQResponseTracker callback.
+ """
+
+ def setUp(self):
+ authenticator = xmlstream.ConnectAuthenticator('otherhost')
+ authenticator.namespace = 'testns'
+ self.xmlstream = xmlstream.XmlStream(authenticator)
+ self.clock = task.Clock()
+ self.xmlstream._callLater = self.clock.callLater
+ self.xmlstream.makeConnection(proto_helpers.StringTransport())
+ self.xmlstream.dataReceived(
+ "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
+ "xmlns='testns' from='otherhost' version='1.0'>")
+ self.iq = xmlstream.IQ(self.xmlstream, 'get')
+
+
+ def testBasic(self):
+ self.assertEqual(self.iq['type'], 'get')
+ self.assertTrue(self.iq['id'])
+
+
+ def testSend(self):
+ self.xmlstream.transport.clear()
+ self.iq.send()
+ self.assertEqual("<iq type='get' id='%s'/>" % self.iq['id'],
+ self.xmlstream.transport.value())
+
+
+ def testResultResponse(self):
+ def cb(result):
+ self.assertEqual(result['type'], 'result')
+
+ d = self.iq.send()
+ d.addCallback(cb)
+
+ xs = self.xmlstream
+ xs.dataReceived("<iq type='result' id='%s'/>" % self.iq['id'])
+ return d
+
+
+ def testErrorResponse(self):
+ d = self.iq.send()
+ self.assertFailure(d, error.StanzaError)
+
+ xs = self.xmlstream
+ xs.dataReceived("<iq type='error' id='%s'/>" % self.iq['id'])
+ return d
+
+
+ def testNonTrackedResponse(self):
+ """
+ Test that untracked iq responses don't trigger any action.
+
+ Untracked means that the id of the incoming response iq is not
+ in the stream's C{iqDeferreds} dictionary.
+ """
+ xs = self.xmlstream
+ xmlstream.upgradeWithIQResponseTracker(xs)
+
+ # Make sure we aren't tracking any iq's.
+ self.assertFalse(xs.iqDeferreds)
+
+ # Set up a fallback handler that checks the stanza's handled attribute.
+ # If that is set to True, the iq tracker claims to have handled the
+ # response.
+ def cb(iq):
+ self.assertFalse(getattr(iq, 'handled', False))
+
+ xs.addObserver("/iq", cb, -1)
+
+ # Receive an untracked iq response
+ xs.dataReceived("<iq type='result' id='test'/>")
+
+
+ def testCleanup(self):
+ """
+ Test if the deferred associated with an iq request is removed
+ from the list kept in the L{XmlStream} object after it has
+ been fired.
+ """
+
+ d = self.iq.send()
+ xs = self.xmlstream
+ xs.dataReceived("<iq type='result' id='%s'/>" % self.iq['id'])
+ self.assertNotIn(self.iq['id'], xs.iqDeferreds)
+ return d
+
+
+ def testDisconnectCleanup(self):
+ """
+ Test if deferreds for iq's that haven't yet received a response
+ have their errback called on stream disconnect.
+ """
+
+ d = self.iq.send()
+ xs = self.xmlstream
+ xs.connectionLost("Closed by peer")
+ self.assertFailure(d, ConnectionLost)
+ return d
+
+
+ def testNoModifyingDict(self):
+ """
+ Test to make sure the errbacks cannot cause the iteration of the
+ iqDeferreds to blow up in our face.
+ """
+
+ def eb(failure):
+ d = xmlstream.IQ(self.xmlstream).send()
+ d.addErrback(eb)
+
+ d = self.iq.send()
+ d.addErrback(eb)
+ self.xmlstream.connectionLost("Closed by peer")
+ return d
+
+
+ def testRequestTimingOut(self):
+ """
+ Test that an iq request with a defined timeout times out.
+ """
+ self.iq.timeout = 60
+ d = self.iq.send()
+ self.assertFailure(d, xmlstream.TimeoutError)
+
+ self.clock.pump([1, 60])
+ self.assertFalse(self.clock.calls)
+ self.assertFalse(self.xmlstream.iqDeferreds)
+ return d
+
+
+ def testRequestNotTimingOut(self):
+ """
+ Test that an iq request with a defined timeout does not time out
+ when a response was received before the timeout period elapsed.
+ """
+ self.iq.timeout = 60
+ d = self.iq.send()
+ self.clock.callLater(1, self.xmlstream.dataReceived,
+ "<iq type='result' id='%s'/>" % self.iq['id'])
+ self.clock.pump([1, 1])
+ self.assertFalse(self.clock.calls)
+ return d
+
+
+ def testDisconnectTimeoutCancellation(self):
+ """
+ Test if timeouts for iq's that haven't yet received a response
+ are cancelled on stream disconnect.
+ """
+
+ self.iq.timeout = 60
+ d = self.iq.send()
+
+ xs = self.xmlstream
+ xs.connectionLost("Closed by peer")
+ self.assertFailure(d, ConnectionLost)
+ self.assertFalse(self.clock.calls)
+ return d
+
+
+
+class XmlStreamTest(unittest.TestCase):
+
+ def onStreamStart(self, obj):
+ self.gotStreamStart = True
+
+
+ def onStreamEnd(self, obj):
+ self.gotStreamEnd = True
+
+
+ def onStreamError(self, obj):
+ self.gotStreamError = True
+
+
+ def setUp(self):
+ """
+ Set up XmlStream and several observers.
+ """
+ self.gotStreamStart = False
+ self.gotStreamEnd = False
+ self.gotStreamError = False
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ xs.addObserver('//event/stream/start', self.onStreamStart)
+ xs.addObserver('//event/stream/end', self.onStreamEnd)
+ xs.addObserver('//event/stream/error', self.onStreamError)
+ xs.makeConnection(proto_helpers.StringTransportWithDisconnection())
+ xs.transport.protocol = xs
+ xs.namespace = 'testns'
+ xs.version = (1, 0)
+ self.xmlstream = xs
+
+
+ def test_sendHeaderBasic(self):
+ """
+ Basic test on the header sent by sendHeader.
+ """
+ xs = self.xmlstream
+ xs.sendHeader()
+ splitHeader = self.xmlstream.transport.value()[0:-1].split(' ')
+ self.assertIn("<stream:stream", splitHeader)
+ self.assertIn("xmlns:stream='http://etherx.jabber.org/streams'",
+ splitHeader)
+ self.assertIn("xmlns='testns'", splitHeader)
+ self.assertIn("version='1.0'", splitHeader)
+ self.assertTrue(xs._headerSent)
+
+
+ def test_sendHeaderAdditionalNamespaces(self):
+ """
+ Test for additional namespace declarations.
+ """
+ xs = self.xmlstream
+ xs.prefixes['jabber:server:dialback'] = 'db'
+ xs.sendHeader()
+ splitHeader = self.xmlstream.transport.value()[0:-1].split(' ')
+ self.assertIn("<stream:stream", splitHeader)
+ self.assertIn("xmlns:stream='http://etherx.jabber.org/streams'",
+ splitHeader)
+ self.assertIn("xmlns:db='jabber:server:dialback'", splitHeader)
+ self.assertIn("xmlns='testns'", splitHeader)
+ self.assertIn("version='1.0'", splitHeader)
+ self.assertTrue(xs._headerSent)
+
+
+ def test_sendHeaderInitiating(self):
+ """
+ Test addressing when initiating a stream.
+ """
+ xs = self.xmlstream
+ xs.thisEntity = jid.JID('thisHost')
+ xs.otherEntity = jid.JID('otherHost')
+ xs.initiating = True
+ xs.sendHeader()
+ splitHeader = xs.transport.value()[0:-1].split(' ')
+ self.assertIn("to='otherhost'", splitHeader)
+ self.assertIn("from='thishost'", splitHeader)
+
+
+ def test_sendHeaderReceiving(self):
+ """
+ Test addressing when receiving a stream.
+ """
+ xs = self.xmlstream
+ xs.thisEntity = jid.JID('thisHost')
+ xs.otherEntity = jid.JID('otherHost')
+ xs.initiating = False
+ xs.sid = 'session01'
+ xs.sendHeader()
+ splitHeader = xs.transport.value()[0:-1].split(' ')
+ self.assertIn("to='otherhost'", splitHeader)
+ self.assertIn("from='thishost'", splitHeader)
+ self.assertIn("id='session01'", splitHeader)
+
+
+ def test_receiveStreamError(self):
+ """
+ Test events when a stream error is received.
+ """
+ xs = self.xmlstream
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1.0'>")
+ xs.dataReceived("<stream:error/>")
+ self.assertTrue(self.gotStreamError)
+ self.assertTrue(self.gotStreamEnd)
+
+
+ def test_sendStreamErrorInitiating(self):
+ """
+ Test sendStreamError on an initiating xmlstream with a header sent.
+
+ An error should be sent out and the connection lost.
+ """
+ xs = self.xmlstream
+ xs.initiating = True
+ xs.sendHeader()
+ xs.transport.clear()
+ xs.sendStreamError(error.StreamError('version-unsupported'))
+ self.assertNotEqual('', xs.transport.value())
+ self.assertTrue(self.gotStreamEnd)
+
+
+ def test_sendStreamErrorInitiatingNoHeader(self):
+ """
+ Test sendStreamError on an initiating xmlstream without having sent a
+ header.
+
+ In this case, no header should be generated. Also, the error should
+ not be sent out on the stream. Just closing the connection.
+ """
+ xs = self.xmlstream
+ xs.initiating = True
+ xs.transport.clear()
+ xs.sendStreamError(error.StreamError('version-unsupported'))
+ self.assertNot(xs._headerSent)
+ self.assertEqual('', xs.transport.value())
+ self.assertTrue(self.gotStreamEnd)
+
+
+ def test_sendStreamErrorReceiving(self):
+ """
+ Test sendStreamError on a receiving xmlstream with a header sent.
+
+ An error should be sent out and the connection lost.
+ """
+ xs = self.xmlstream
+ xs.initiating = False
+ xs.sendHeader()
+ xs.transport.clear()
+ xs.sendStreamError(error.StreamError('version-unsupported'))
+ self.assertNotEqual('', xs.transport.value())
+ self.assertTrue(self.gotStreamEnd)
+
+
+ def test_sendStreamErrorReceivingNoHeader(self):
+ """
+ Test sendStreamError on a receiving xmlstream without having sent a
+ header.
+
+ In this case, a header should be generated. Then, the error should
+ be sent out on the stream followed by closing the connection.
+ """
+ xs = self.xmlstream
+ xs.initiating = False
+ xs.transport.clear()
+ xs.sendStreamError(error.StreamError('version-unsupported'))
+ self.assertTrue(xs._headerSent)
+ self.assertNotEqual('', xs.transport.value())
+ self.assertTrue(self.gotStreamEnd)
+
+
+ def test_reset(self):
+ """
+ Test resetting the XML stream to start a new layer.
+ """
+ xs = self.xmlstream
+ xs.sendHeader()
+ stream = xs.stream
+ xs.reset()
+ self.assertNotEqual(stream, xs.stream)
+ self.assertNot(xs._headerSent)
+
+
+ def test_send(self):
+ """
+ Test send with various types of objects.
+ """
+ xs = self.xmlstream
+ xs.send('<presence/>')
+ self.assertEqual(xs.transport.value(), '<presence/>')
+
+ xs.transport.clear()
+ el = domish.Element(('testns', 'presence'))
+ xs.send(el)
+ self.assertEqual(xs.transport.value(), '<presence/>')
+
+ xs.transport.clear()
+ el = domish.Element(('http://etherx.jabber.org/streams', 'features'))
+ xs.send(el)
+ self.assertEqual(xs.transport.value(), '<stream:features/>')
+
+
+ def test_authenticator(self):
+ """
+ Test that the associated authenticator is correctly called.
+ """
+ connectionMadeCalls = []
+ streamStartedCalls = []
+ associateWithStreamCalls = []
+
+ class TestAuthenticator:
+ def connectionMade(self):
+ connectionMadeCalls.append(None)
+
+ def streamStarted(self, rootElement):
+ streamStartedCalls.append(rootElement)
+
+ def associateWithStream(self, xs):
+ associateWithStreamCalls.append(xs)
+
+ a = TestAuthenticator()
+ xs = xmlstream.XmlStream(a)
+ self.assertEqual([xs], associateWithStreamCalls)
+ xs.connectionMade()
+ self.assertEqual([None], connectionMadeCalls)
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345'>")
+ self.assertEqual(1, len(streamStartedCalls))
+ xs.reset()
+ self.assertEqual([None], connectionMadeCalls)
+
+
+
+class TestError(Exception):
+ pass
+
+
+
+class AuthenticatorTest(unittest.TestCase):
+ def setUp(self):
+ self.authenticator = xmlstream.Authenticator()
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+
+
+ def test_streamStart(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.org' to='example.com' id='12345' "
+ "version='1.0'>")
+ self.assertEqual((1, 0), xs.version)
+ self.assertIdentical(None, xs.sid)
+ self.assertEqual('invalid', xs.namespace)
+ self.assertIdentical(None, xs.otherEntity)
+ self.assertEqual(None, xs.thisEntity)
+
+
+ def test_streamStartLegacy(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header for a pre-XMPP-1.0 header.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345'>")
+ self.assertEqual((0, 0), xs.version)
+
+
+ def test_streamBadVersionOneDigit(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header for a version with only one digit.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1'>")
+ self.assertEqual((0, 0), xs.version)
+
+
+ def test_streamBadVersionNoNumber(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header for a malformed version.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='blah'>")
+ self.assertEqual((0, 0), xs.version)
+
+
+
+class ConnectAuthenticatorTest(unittest.TestCase):
+
+ def setUp(self):
+ self.gotAuthenticated = False
+ self.initFailure = None
+ self.authenticator = xmlstream.ConnectAuthenticator('otherHost')
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+ self.xmlstream.addObserver('//event/stream/authd', self.onAuthenticated)
+ self.xmlstream.addObserver('//event/xmpp/initfailed', self.onInitFailed)
+
+
+ def onAuthenticated(self, obj):
+ self.gotAuthenticated = True
+
+
+ def onInitFailed(self, failure):
+ self.initFailure = failure
+
+
+ def testSucces(self):
+ """
+ Test successful completion of an initialization step.
+ """
+ class Initializer:
+ def initialize(self):
+ pass
+
+ init = Initializer()
+ self.xmlstream.initializers = [init]
+
+ self.authenticator.initializeStream()
+ self.assertEqual([], self.xmlstream.initializers)
+ self.assertTrue(self.gotAuthenticated)
+
+
+ def testFailure(self):
+ """
+ Test failure of an initialization step.
+ """
+ class Initializer:
+ def initialize(self):
+ raise TestError
+
+ init = Initializer()
+ self.xmlstream.initializers = [init]
+
+ self.authenticator.initializeStream()
+ self.assertEqual([init], self.xmlstream.initializers)
+ self.assertFalse(self.gotAuthenticated)
+ self.assertNotIdentical(None, self.initFailure)
+ self.assertTrue(self.initFailure.check(TestError))
+
+
+ def test_streamStart(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header.
+ """
+ self.authenticator.namespace = 'testns'
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' to='example.org' id='12345' "
+ "version='1.0'>")
+ self.assertEqual((1, 0), xs.version)
+ self.assertEqual('12345', xs.sid)
+ self.assertEqual('testns', xs.namespace)
+ self.assertEqual('example.com', xs.otherEntity.host)
+ self.assertIdentical(None, xs.thisEntity)
+ self.assertNot(self.gotAuthenticated)
+ xs.dataReceived("<stream:features>"
+ "<test xmlns='testns'/>"
+ "</stream:features>")
+ self.assertIn(('testns', 'test'), xs.features)
+ self.assertTrue(self.gotAuthenticated)
+
+
+
+class ListenAuthenticatorTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.ListenAuthenticator}
+ """
+
+ def setUp(self):
+ self.authenticator = xmlstream.ListenAuthenticator()
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+
+
+ def test_streamStart(self):
+ """
+ Test streamStart to fill the appropriate attributes from the
+ stream header.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ self.assertIdentical(None, xs.sid)
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.org' to='example.com' id='12345' "
+ "version='1.0'>")
+ self.assertEqual((1, 0), xs.version)
+ self.assertNotIdentical(None, xs.sid)
+ self.assertNotEquals('12345', xs.sid)
+ self.assertEqual('jabber:client', xs.namespace)
+ self.assertIdentical(None, xs.otherEntity)
+ self.assertEqual('example.com', xs.thisEntity.host)
+
+
+ def test_streamStartUnicodeSessionID(self):
+ """
+ The generated session id must be a unicode object.
+ """
+ xs = self.xmlstream
+ xs.makeConnection(proto_helpers.StringTransport())
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.org' to='example.com' id='12345' "
+ "version='1.0'>")
+ self.assertIsInstance(xs.sid, unicode)
+
+
+
+class TLSInitiatingInitializerTest(unittest.TestCase):
+ def setUp(self):
+ self.output = []
+ self.done = []
+
+ self.savedSSL = xmlstream.ssl
+
+ self.authenticator = xmlstream.Authenticator()
+ self.xmlstream = xmlstream.XmlStream(self.authenticator)
+ self.xmlstream.send = self.output.append
+ self.xmlstream.connectionMade()
+ self.xmlstream.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345' version='1.0'>")
+ self.init = xmlstream.TLSInitiatingInitializer(self.xmlstream)
+
+
+ def tearDown(self):
+ xmlstream.ssl = self.savedSSL
+
+
+ def testWantedSupported(self):
+ """
+ Test start when TLS is wanted and the SSL library available.
+ """
+ self.xmlstream.transport = proto_helpers.StringTransport()
+ self.xmlstream.transport.startTLS = lambda ctx: self.done.append('TLS')
+ self.xmlstream.reset = lambda: self.done.append('reset')
+ self.xmlstream.sendHeader = lambda: self.done.append('header')
+
+ d = self.init.start()
+ d.addCallback(self.assertEqual, xmlstream.Reset)
+ starttls = self.output[0]
+ self.assertEqual('starttls', starttls.name)
+ self.assertEqual(NS_XMPP_TLS, starttls.uri)
+ self.xmlstream.dataReceived("<proceed xmlns='%s'/>" % NS_XMPP_TLS)
+ self.assertEqual(['TLS', 'reset', 'header'], self.done)
+
+ return d
+
+ if not xmlstream.ssl:
+ testWantedSupported.skip = "SSL not available"
+
+
+ def testWantedNotSupportedNotRequired(self):
+ """
+ Test start when TLS is wanted and the SSL library available.
+ """
+ xmlstream.ssl = None
+
+ d = self.init.start()
+ d.addCallback(self.assertEqual, None)
+ self.assertEqual([], self.output)
+
+ return d
+
+
+ def testWantedNotSupportedRequired(self):
+ """
+ Test start when TLS is wanted and the SSL library available.
+ """
+ xmlstream.ssl = None
+ self.init.required = True
+
+ d = self.init.start()
+ self.assertFailure(d, xmlstream.TLSNotSupported)
+ self.assertEqual([], self.output)
+
+ return d
+
+
+ def testNotWantedRequired(self):
+ """
+ Test start when TLS is not wanted, but required by the server.
+ """
+ tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls'))
+ tls.addElement('required')
+ self.xmlstream.features = {(tls.uri, tls.name): tls}
+ self.init.wanted = False
+
+ d = self.init.start()
+ self.assertEqual([], self.output)
+ self.assertFailure(d, xmlstream.TLSRequired)
+
+ return d
+
+
+ def testNotWantedNotRequired(self):
+ """
+ Test start when TLS is not wanted, but required by the server.
+ """
+ tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls'))
+ self.xmlstream.features = {(tls.uri, tls.name): tls}
+ self.init.wanted = False
+
+ d = self.init.start()
+ d.addCallback(self.assertEqual, None)
+ self.assertEqual([], self.output)
+ return d
+
+
+ def testFailed(self):
+ """
+ Test failed TLS negotiation.
+ """
+ # Pretend that ssl is supported, it isn't actually used when the
+ # server starts out with a failure in response to our initial
+ # C{starttls} stanza.
+ xmlstream.ssl = 1
+
+ d = self.init.start()
+ self.assertFailure(d, xmlstream.TLSFailed)
+ self.xmlstream.dataReceived("<failure xmlns='%s'/>" % NS_XMPP_TLS)
+ return d
+
+
+
+class TestFeatureInitializer(xmlstream.BaseFeatureInitiatingInitializer):
+ feature = ('testns', 'test')
+
+ def start(self):
+ return defer.succeed(None)
+
+
+
+class BaseFeatureInitiatingInitializerTest(unittest.TestCase):
+
+ def setUp(self):
+ self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator())
+ self.init = TestFeatureInitializer(self.xmlstream)
+
+
+ def testAdvertized(self):
+ """
+ Test that an advertized feature results in successful initialization.
+ """
+ self.xmlstream.features = {self.init.feature:
+ domish.Element(self.init.feature)}
+ return self.init.initialize()
+
+
+ def testNotAdvertizedRequired(self):
+ """
+ Test that when the feature is not advertized, but required by the
+ initializer, an exception is raised.
+ """
+ self.init.required = True
+ self.assertRaises(xmlstream.FeatureNotAdvertized, self.init.initialize)
+
+
+ def testNotAdvertizedNotRequired(self):
+ """
+ Test that when the feature is not advertized, and not required by the
+ initializer, the initializer silently succeeds.
+ """
+ self.init.required = False
+ self.assertIdentical(None, self.init.initialize())
+
+
+
+class ToResponseTest(unittest.TestCase):
+
+ def test_toResponse(self):
+ """
+ Test that a response stanza is generated with addressing swapped.
+ """
+ stanza = domish.Element(('jabber:client', 'iq'))
+ stanza['type'] = 'get'
+ stanza['to'] = 'user1@example.com'
+ stanza['from'] = 'user2@example.com/resource'
+ stanza['id'] = 'stanza1'
+ response = xmlstream.toResponse(stanza, 'result')
+ self.assertNotIdentical(stanza, response)
+ self.assertEqual(response['from'], 'user1@example.com')
+ self.assertEqual(response['to'], 'user2@example.com/resource')
+ self.assertEqual(response['type'], 'result')
+ self.assertEqual(response['id'], 'stanza1')
+
+
+ def test_toResponseNoFrom(self):
+ """
+ Test that a response is generated from a stanza without a from address.
+ """
+ stanza = domish.Element(('jabber:client', 'iq'))
+ stanza['type'] = 'get'
+ stanza['to'] = 'user1@example.com'
+ response = xmlstream.toResponse(stanza)
+ self.assertEqual(response['from'], 'user1@example.com')
+ self.assertFalse(response.hasAttribute('to'))
+
+
+ def test_toResponseNoTo(self):
+ """
+ Test that a response is generated from a stanza without a to address.
+ """
+ stanza = domish.Element(('jabber:client', 'iq'))
+ stanza['type'] = 'get'
+ stanza['from'] = 'user2@example.com/resource'
+ response = xmlstream.toResponse(stanza)
+ self.assertFalse(response.hasAttribute('from'))
+ self.assertEqual(response['to'], 'user2@example.com/resource')
+
+
+ def test_toResponseNoAddressing(self):
+ """
+ Test that a response is generated from a stanza without any addressing.
+ """
+ stanza = domish.Element(('jabber:client', 'message'))
+ stanza['type'] = 'chat'
+ response = xmlstream.toResponse(stanza)
+ self.assertFalse(response.hasAttribute('to'))
+ self.assertFalse(response.hasAttribute('from'))
+
+
+ def test_noID(self):
+ """
+ Test that a proper response is generated without id attribute.
+ """
+ stanza = domish.Element(('jabber:client', 'message'))
+ response = xmlstream.toResponse(stanza)
+ self.assertFalse(response.hasAttribute('id'))
+
+
+ def test_noType(self):
+ """
+ Test that a proper response is generated without type attribute.
+ """
+ stanza = domish.Element(('jabber:client', 'message'))
+ response = xmlstream.toResponse(stanza)
+ self.assertFalse(response.hasAttribute('type'))
+
+
+class DummyFactory(object):
+ """
+ Dummy XmlStream factory that only registers bootstrap observers.
+ """
+ def __init__(self):
+ self.callbacks = {}
+
+
+ def addBootstrap(self, event, callback):
+ self.callbacks[event] = callback
+
+
+
+class DummyXMPPHandler(xmlstream.XMPPHandler):
+ """
+ Dummy XMPP subprotocol handler to count the methods are called on it.
+ """
+ def __init__(self):
+ self.doneMade = 0
+ self.doneInitialized = 0
+ self.doneLost = 0
+
+
+ def makeConnection(self, xs):
+ self.connectionMade()
+
+
+ def connectionMade(self):
+ self.doneMade += 1
+
+
+ def connectionInitialized(self):
+ self.doneInitialized += 1
+
+
+ def connectionLost(self, reason):
+ self.doneLost += 1
+
+
+
+class FailureReasonXMPPHandler(xmlstream.XMPPHandler):
+ """
+ Dummy handler specifically for failure Reason tests.
+ """
+ def __init__(self):
+ self.gotFailureReason = False
+
+
+ def connectionLost(self, reason):
+ if isinstance(reason, failure.Failure):
+ self.gotFailureReason = True
+
+
+
+class XMPPHandlerTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.XMPPHandler}.
+ """
+
+ def test_interface(self):
+ """
+ L{xmlstream.XMPPHandler} implements L{ijabber.IXMPPHandler}.
+ """
+ verifyObject(ijabber.IXMPPHandler, xmlstream.XMPPHandler())
+
+
+ def test_send(self):
+ """
+ Test that data is passed on for sending by the stream manager.
+ """
+ class DummyStreamManager(object):
+ def __init__(self):
+ self.outlist = []
+
+ def send(self, data):
+ self.outlist.append(data)
+
+ handler = xmlstream.XMPPHandler()
+ handler.parent = DummyStreamManager()
+ handler.send('<presence/>')
+ self.assertEqual(['<presence/>'], handler.parent.outlist)
+
+
+ def test_makeConnection(self):
+ """
+ Test that makeConnection saves the XML stream and calls connectionMade.
+ """
+ class TestXMPPHandler(xmlstream.XMPPHandler):
+ def connectionMade(self):
+ self.doneMade = True
+
+ handler = TestXMPPHandler()
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ handler.makeConnection(xs)
+ self.assertTrue(handler.doneMade)
+ self.assertIdentical(xs, handler.xmlstream)
+
+
+ def test_connectionLost(self):
+ """
+ Test that connectionLost forgets the XML stream.
+ """
+ handler = xmlstream.XMPPHandler()
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ handler.makeConnection(xs)
+ handler.connectionLost(Exception())
+ self.assertIdentical(None, handler.xmlstream)
+
+
+
+class XMPPHandlerCollectionTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.XMPPHandlerCollection}.
+ """
+
+ def setUp(self):
+ self.collection = xmlstream.XMPPHandlerCollection()
+
+
+ def test_interface(self):
+ """
+ L{xmlstream.StreamManager} implements L{ijabber.IXMPPHandlerCollection}.
+ """
+ verifyObject(ijabber.IXMPPHandlerCollection, self.collection)
+
+
+ def test_addHandler(self):
+ """
+ Test the addition of a protocol handler.
+ """
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(self.collection)
+ self.assertIn(handler, self.collection)
+ self.assertIdentical(self.collection, handler.parent)
+
+
+ def test_removeHandler(self):
+ """
+ Test removal of a protocol handler.
+ """
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(self.collection)
+ handler.disownHandlerParent(self.collection)
+ self.assertNotIn(handler, self.collection)
+ self.assertIdentical(None, handler.parent)
+
+
+
+class StreamManagerTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.StreamManager}.
+ """
+
+ def setUp(self):
+ factory = DummyFactory()
+ self.streamManager = xmlstream.StreamManager(factory)
+
+
+ def test_basic(self):
+ """
+ Test correct initialization and setup of factory observers.
+ """
+ sm = self.streamManager
+ self.assertIdentical(None, sm.xmlstream)
+ self.assertEqual([], sm.handlers)
+ self.assertEqual(sm._connected,
+ sm.factory.callbacks['//event/stream/connected'])
+ self.assertEqual(sm._authd,
+ sm.factory.callbacks['//event/stream/authd'])
+ self.assertEqual(sm._disconnected,
+ sm.factory.callbacks['//event/stream/end'])
+ self.assertEqual(sm.initializationFailed,
+ sm.factory.callbacks['//event/xmpp/initfailed'])
+
+
+ def test_connected(self):
+ """
+ Test that protocol handlers have their connectionMade method called
+ when the XML stream is connected.
+ """
+ sm = self.streamManager
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._connected(xs)
+ self.assertEqual(1, handler.doneMade)
+ self.assertEqual(0, handler.doneInitialized)
+ self.assertEqual(0, handler.doneLost)
+
+
+ def test_connectedLogTrafficFalse(self):
+ """
+ Test raw data functions unset when logTraffic is set to False.
+ """
+ sm = self.streamManager
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._connected(xs)
+ self.assertIdentical(None, xs.rawDataInFn)
+ self.assertIdentical(None, xs.rawDataOutFn)
+
+
+ def test_connectedLogTrafficTrue(self):
+ """
+ Test raw data functions set when logTraffic is set to True.
+ """
+ sm = self.streamManager
+ sm.logTraffic = True
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._connected(xs)
+ self.assertNotIdentical(None, xs.rawDataInFn)
+ self.assertNotIdentical(None, xs.rawDataOutFn)
+
+
+ def test_authd(self):
+ """
+ Test that protocol handlers have their connectionInitialized method
+ called when the XML stream is initialized.
+ """
+ sm = self.streamManager
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._authd(xs)
+ self.assertEqual(0, handler.doneMade)
+ self.assertEqual(1, handler.doneInitialized)
+ self.assertEqual(0, handler.doneLost)
+
+
+ def test_disconnected(self):
+ """
+ Test that protocol handlers have their connectionLost method
+ called when the XML stream is disconnected.
+ """
+ sm = self.streamManager
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._disconnected(xs)
+ self.assertEqual(0, handler.doneMade)
+ self.assertEqual(0, handler.doneInitialized)
+ self.assertEqual(1, handler.doneLost)
+
+
+ def test_disconnectedReason(self):
+ """
+ A L{STREAM_END_EVENT} results in L{StreamManager} firing the handlers
+ L{connectionLost} methods, passing a L{failure.Failure} reason.
+ """
+ sm = self.streamManager
+ handler = FailureReasonXMPPHandler()
+ handler.setHandlerParent(sm)
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._disconnected(failure.Failure(Exception("no reason")))
+ self.assertEqual(True, handler.gotFailureReason)
+
+
+ def test_addHandler(self):
+ """
+ Test the addition of a protocol handler while not connected.
+ """
+ sm = self.streamManager
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+
+ self.assertEqual(0, handler.doneMade)
+ self.assertEqual(0, handler.doneInitialized)
+ self.assertEqual(0, handler.doneLost)
+
+
+ def test_addHandlerInitialized(self):
+ """
+ Test the addition of a protocol handler after the stream
+ have been initialized.
+
+ Make sure that the handler will have the connected stream
+ passed via C{makeConnection} and have C{connectionInitialized}
+ called.
+ """
+ sm = self.streamManager
+ xs = xmlstream.XmlStream(xmlstream.Authenticator())
+ sm._connected(xs)
+ sm._authd(xs)
+ handler = DummyXMPPHandler()
+ handler.setHandlerParent(sm)
+
+ self.assertEqual(1, handler.doneMade)
+ self.assertEqual(1, handler.doneInitialized)
+ self.assertEqual(0, handler.doneLost)
+
+
+ def test_sendInitialized(self):
+ """
+ Test send when the stream has been initialized.
+
+ The data should be sent directly over the XML stream.
+ """
+ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
+ sm = xmlstream.StreamManager(factory)
+ xs = factory.buildProtocol(None)
+ xs.transport = proto_helpers.StringTransport()
+ xs.connectionMade()
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345'>")
+ xs.dispatch(xs, "//event/stream/authd")
+ sm.send("<presence/>")
+ self.assertEqual("<presence/>", xs.transport.value())
+
+
+ def test_sendNotConnected(self):
+ """
+ Test send when there is no established XML stream.
+
+ The data should be cached until an XML stream has been established and
+ initialized.
+ """
+ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
+ sm = xmlstream.StreamManager(factory)
+ handler = DummyXMPPHandler()
+ sm.addHandler(handler)
+
+ xs = factory.buildProtocol(None)
+ xs.transport = proto_helpers.StringTransport()
+ sm.send("<presence/>")
+ self.assertEqual("", xs.transport.value())
+ self.assertEqual("<presence/>", sm._packetQueue[0])
+
+ xs.connectionMade()
+ self.assertEqual("", xs.transport.value())
+ self.assertEqual("<presence/>", sm._packetQueue[0])
+
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345'>")
+ xs.dispatch(xs, "//event/stream/authd")
+
+ self.assertEqual("<presence/>", xs.transport.value())
+ self.assertFalse(sm._packetQueue)
+
+
+ def test_sendNotInitialized(self):
+ """
+ Test send when the stream is connected but not yet initialized.
+
+ The data should be cached until the XML stream has been initialized.
+ """
+ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
+ sm = xmlstream.StreamManager(factory)
+ xs = factory.buildProtocol(None)
+ xs.transport = proto_helpers.StringTransport()
+ xs.connectionMade()
+ xs.dataReceived("<stream:stream xmlns='jabber:client' "
+ "xmlns:stream='http://etherx.jabber.org/streams' "
+ "from='example.com' id='12345'>")
+ sm.send("<presence/>")
+ self.assertEqual("", xs.transport.value())
+ self.assertEqual("<presence/>", sm._packetQueue[0])
+
+
+ def test_sendDisconnected(self):
+ """
+ Test send after XML stream disconnection.
+
+ The data should be cached until a new XML stream has been established
+ and initialized.
+ """
+ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
+ sm = xmlstream.StreamManager(factory)
+ handler = DummyXMPPHandler()
+ sm.addHandler(handler)
+
+ xs = factory.buildProtocol(None)
+ xs.connectionMade()
+ xs.transport = proto_helpers.StringTransport()
+ xs.connectionLost(None)
+
+ sm.send("<presence/>")
+ self.assertEqual("", xs.transport.value())
+ self.assertEqual("<presence/>", sm._packetQueue[0])
+
+
+
+class XmlStreamServerFactoryTest(GenericXmlStreamFactoryTestsMixin):
+ """
+ Tests for L{xmlstream.XmlStreamServerFactory}.
+ """
+
+ def setUp(self):
+ """
+ Set up a server factory with a authenticator factory function.
+ """
+ class TestAuthenticator(object):
+ def __init__(self):
+ self.xmlstreams = []
+
+ def associateWithStream(self, xs):
+ self.xmlstreams.append(xs)
+
+ def authenticatorFactory():
+ return TestAuthenticator()
+
+ self.factory = xmlstream.XmlStreamServerFactory(authenticatorFactory)
+
+
+ def test_interface(self):
+ """
+ L{XmlStreamServerFactory} is a L{Factory}.
+ """
+ verifyObject(IProtocolFactory, self.factory)
+
+
+ def test_buildProtocolAuthenticatorInstantiation(self):
+ """
+ The authenticator factory should be used to instantiate the
+ authenticator and pass it to the protocol.
+
+ The default protocol, L{XmlStream} stores the authenticator it is
+ passed, and calls its C{associateWithStream} method. so we use that to
+ check whether our authenticator factory is used and the protocol
+ instance gets an authenticator.
+ """
+ xs = self.factory.buildProtocol(None)
+ self.assertEqual([xs], xs.authenticator.xmlstreams)
+
+
+ def test_buildProtocolXmlStream(self):
+ """
+ The protocol factory creates Jabber XML Stream protocols by default.
+ """
+ xs = self.factory.buildProtocol(None)
+ self.assertIsInstance(xs, xmlstream.XmlStream)
+
+
+ def test_buildProtocolTwice(self):
+ """
+ Subsequent calls to buildProtocol should result in different instances
+ of the protocol, as well as their authenticators.
+ """
+ xs1 = self.factory.buildProtocol(None)
+ xs2 = self.factory.buildProtocol(None)
+ self.assertNotIdentical(xs1, xs2)
+ self.assertNotIdentical(xs1.authenticator, xs2.authenticator)
diff --git a/twisted/words/test/test_jabberxmppstringprep.py b/twisted/words/test/test_jabberxmppstringprep.py
new file mode 100644
index 0000000..4b25c0b
--- /dev/null
+++ b/twisted/words/test/test_jabberxmppstringprep.py
@@ -0,0 +1,92 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.trial import unittest
+
+from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep, resourceprep, nameprep, crippled
+
+class XMPPStringPrepTest(unittest.TestCase):
+ """
+
+ The nodeprep stringprep profile is similar to the resourceprep profile,
+ but does an extra mapping of characters (table B.2) and disallows
+ more characters (table C.1.1 and eight extra punctuation characters).
+ Due to this similarity, the resourceprep tests are more extensive, and
+ the nodeprep tests only address the mappings additional restrictions.
+
+ The nameprep profile is nearly identical to the nameprep implementation in
+ L{encodings.idna}, but that implementation assumes the C{UseSTD4ASCIIRules}
+ flag to be false. This implementation assumes it to be true, and restricts
+ the allowed set of characters. The tests here only check for the
+ differences.
+
+ """
+
+ def testResourcePrep(self):
+ self.assertEqual(resourceprep.prepare(u'resource'), u'resource')
+ self.assertNotEquals(resourceprep.prepare(u'Resource'), u'resource')
+ self.assertEqual(resourceprep.prepare(u' '), u' ')
+
+ if crippled:
+ return
+
+ self.assertEqual(resourceprep.prepare(u'Henry \u2163'), u'Henry IV')
+ self.assertEqual(resourceprep.prepare(u'foo\xad\u034f\u1806\u180b'
+ u'bar\u200b\u2060'
+ u'baz\ufe00\ufe08\ufe0f\ufeff'),
+ u'foobarbaz')
+ self.assertEqual(resourceprep.prepare(u'\u00a0'), u' ')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u1680')
+ self.assertEqual(resourceprep.prepare(u'\u2000'), u' ')
+ self.assertEqual(resourceprep.prepare(u'\u200b'), u'')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u0010\u007f')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u0085')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u180e')
+ self.assertEqual(resourceprep.prepare(u'\ufeff'), u'')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\uf123')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U000f1234')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U0010f234')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U0008fffe')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U0010ffff')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\udf42')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\ufffd')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u2ff5')
+ self.assertEqual(resourceprep.prepare(u'\u0341'), u'\u0301')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u200e')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u202a')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U000e0001')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U000e0042')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'foo\u05bebar')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'foo\ufd50bar')
+ #self.assertEqual(resourceprep.prepare(u'foo\ufb38bar'),
+ # u'foo\u064ebar')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\u06271')
+ self.assertEqual(resourceprep.prepare(u'\u06271\u0628'),
+ u'\u06271\u0628')
+ self.assertRaises(UnicodeError, resourceprep.prepare, u'\U000e0002')
+
+ def testNodePrep(self):
+ self.assertEqual(nodeprep.prepare(u'user'), u'user')
+ self.assertEqual(nodeprep.prepare(u'User'), u'user')
+ self.assertRaises(UnicodeError, nodeprep.prepare, u'us&er')
+
+
+ def test_nodeprepUnassignedInUnicode32(self):
+ """
+ Make sure unassigned code points from Unicode 3.2 are rejected.
+ """
+ self.assertRaises(UnicodeError, nodeprep.prepare, u'\u1d39')
+
+
+ def testNamePrep(self):
+ self.assertEqual(nameprep.prepare(u'example.com'), u'example.com')
+ self.assertEqual(nameprep.prepare(u'Example.com'), u'example.com')
+ self.assertRaises(UnicodeError, nameprep.prepare, u'ex@mple.com')
+ self.assertRaises(UnicodeError, nameprep.prepare, u'-example.com')
+ self.assertRaises(UnicodeError, nameprep.prepare, u'example-.com')
+
+ if crippled:
+ return
+
+ self.assertEqual(nameprep.prepare(u'stra\u00dfe.example.com'),
+ u'strasse.example.com')
diff --git a/twisted/words/test/test_msn.py b/twisted/words/test/test_msn.py
new file mode 100644
index 0000000..ece580f
--- /dev/null
+++ b/twisted/words/test/test_msn.py
@@ -0,0 +1,522 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for L{twisted.words.protocols.msn}.
+"""
+
+# System imports
+import StringIO
+
+# Twisted imports
+
+# t.w.p.msn requires an HTTP client
+try:
+ # So try to get one - do it directly instead of catching an ImportError
+ # from t.w.p.msn so that other problems which cause that module to fail
+ # to import don't cause the tests to be skipped.
+ from twisted.web import client
+except ImportError:
+ # If there isn't one, we're going to skip all the tests.
+ msn = None
+else:
+ # Otherwise importing it should work, so do it.
+ from twisted.words.protocols import msn
+
+
+from twisted.python.hashlib import md5
+from twisted.protocols import loopback
+from twisted.internet.defer import Deferred
+from twisted.trial import unittest
+from twisted.test.proto_helpers import StringTransport, StringIOWithoutClosing
+
+def printError(f):
+ print f
+
+
+class PassportTests(unittest.TestCase):
+
+ def setUp(self):
+ self.result = []
+ self.deferred = Deferred()
+ self.deferred.addCallback(lambda r: self.result.append(r))
+ self.deferred.addErrback(printError)
+
+ def test_nexus(self):
+ """
+ When L{msn.PassportNexus} receives enough information to identify the
+ address of the login server, it fires the L{Deferred} passed to its
+ initializer with that address.
+ """
+ protocol = msn.PassportNexus(self.deferred, 'https://foobar.com/somepage.quux')
+ headers = {
+ 'Content-Length' : '0',
+ 'Content-Type' : 'text/html',
+ 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
+ }
+ transport = StringTransport()
+ protocol.makeConnection(transport)
+ protocol.dataReceived('HTTP/1.0 200 OK\r\n')
+ for (h, v) in headers.items():
+ protocol.dataReceived('%s: %s\r\n' % (h,v))
+ protocol.dataReceived('\r\n')
+ self.assertEqual(self.result[0], "https://login.myserver.com/")
+
+
+ def _doLoginTest(self, response, headers):
+ protocol = msn.PassportLogin(self.deferred,'foo@foo.com','testpass','https://foo.com/', 'a')
+ protocol.makeConnection(StringTransport())
+ protocol.dataReceived(response)
+ for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v))
+ protocol.dataReceived('\r\n')
+
+ def testPassportLoginSuccess(self):
+ headers = {
+ 'Content-Length' : '0',
+ 'Content-Type' : 'text/html',
+ 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
+ "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
+ "ru=http://messenger.msn.com"
+ }
+ self._doLoginTest('HTTP/1.1 200 OK\r\n', headers)
+ self.failUnless(self.result[0] == (msn.LOGIN_SUCCESS, 'somekey'))
+
+ def testPassportLoginFailure(self):
+ headers = {
+ 'Content-Type' : 'text/html',
+ 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
+ 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
+ 'cbtxt=the%20error%20message'
+ }
+ self._doLoginTest('HTTP/1.1 401 Unauthorized\r\n', headers)
+ self.failUnless(self.result[0] == (msn.LOGIN_FAILURE, 'the error message'))
+
+ def testPassportLoginRedirect(self):
+ headers = {
+ 'Content-Type' : 'text/html',
+ 'Authentication-Info' : 'Passport1.4 da-status=redir',
+ 'Location' : 'https://newlogin.host.com/'
+ }
+ self._doLoginTest('HTTP/1.1 302 Found\r\n', headers)
+ self.failUnless(self.result[0] == (msn.LOGIN_REDIRECT, 'https://newlogin.host.com/', 'a'))
+
+
+if msn is not None:
+ class DummySwitchboardClient(msn.SwitchboardClient):
+ def userTyping(self, message):
+ self.state = 'TYPING'
+
+ def gotSendRequest(self, fileName, fileSize, cookie, message):
+ if fileName == 'foobar.ext' and fileSize == 31337 and cookie == 1234: self.state = 'INVITATION'
+
+
+ class DummyNotificationClient(msn.NotificationClient):
+ def loggedIn(self, userHandle, screenName, verified):
+ if userHandle == 'foo@bar.com' and screenName == 'Test Screen Name' and verified:
+ self.state = 'LOGIN'
+
+ def gotProfile(self, message):
+ self.state = 'PROFILE'
+
+ def gotContactStatus(self, code, userHandle, screenName):
+ if code == msn.STATUS_AWAY and userHandle == "foo@bar.com" and screenName == "Test Screen Name":
+ self.state = 'INITSTATUS'
+
+ def contactStatusChanged(self, code, userHandle, screenName):
+ if code == msn.STATUS_LUNCH and userHandle == "foo@bar.com" and screenName == "Test Name":
+ self.state = 'NEWSTATUS'
+
+ def contactOffline(self, userHandle):
+ if userHandle == "foo@bar.com": self.state = 'OFFLINE'
+
+ def statusChanged(self, code):
+ if code == msn.STATUS_HIDDEN: self.state = 'MYSTATUS'
+
+ def listSynchronized(self, *args):
+ self.state = 'GOTLIST'
+
+ def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
+ msn.NotificationClient.gotPhoneNumber(self, listVersion, userHandle, phoneType, number)
+ self.state = 'GOTPHONE'
+
+ def userRemovedMe(self, userHandle, listVersion):
+ msn.NotificationClient.userRemovedMe(self, userHandle, listVersion)
+ c = self.factory.contacts.getContact(userHandle)
+ if not c and self.factory.contacts.version == listVersion: self.state = 'USERREMOVEDME'
+
+ def userAddedMe(self, userHandle, screenName, listVersion):
+ msn.NotificationClient.userAddedMe(self, userHandle, screenName, listVersion)
+ c = self.factory.contacts.getContact(userHandle)
+ if c and (c.lists | msn.REVERSE_LIST) and (self.factory.contacts.version == listVersion) and \
+ (screenName == 'Screen Name'):
+ self.state = 'USERADDEDME'
+
+ def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
+ if sessionID == 1234 and \
+ host == '192.168.1.1' and \
+ port == 1863 and \
+ key == '123.456' and \
+ userHandle == 'foo@foo.com' and \
+ screenName == 'Screen Name':
+ self.state = 'SBINVITED'
+
+
+
+class DispatchTests(unittest.TestCase):
+ """
+ Tests for L{DispatchClient}.
+ """
+ def _versionTest(self, serverVersionResponse):
+ """
+ Test L{DispatchClient} version negotiation.
+ """
+ client = msn.DispatchClient()
+ client.userHandle = "foo"
+
+ transport = StringTransport()
+ client.makeConnection(transport)
+ self.assertEqual(
+ transport.value(), "VER 1 MSNP8 CVR0\r\n")
+ transport.clear()
+
+ client.dataReceived(serverVersionResponse)
+ self.assertEqual(
+ transport.value(),
+ "CVR 2 0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS foo\r\n")
+
+
+ def test_version(self):
+ """
+ L{DispatchClient.connectionMade} greets the server with a I{VER}
+ (version) message and then L{NotificationClient.dataReceived}
+ handles the server's I{VER} response by sending a I{CVR} (client
+ version) message.
+ """
+ self._versionTest("VER 1 MSNP8 CVR0\r\n")
+
+
+ def test_versionWithoutCVR0(self):
+ """
+ If the server responds to a I{VER} command without including the
+ I{CVR0} protocol, L{DispatchClient} behaves in the same way as if
+ that protocol were included.
+
+ Starting in August 2008, CVR0 disappeared from the I{VER} response.
+ """
+ self._versionTest("VER 1 MSNP8\r\n")
+
+
+
+class NotificationTests(unittest.TestCase):
+ """ testing the various events in NotificationClient """
+
+ def setUp(self):
+ self.client = DummyNotificationClient()
+ self.client.factory = msn.NotificationFactory()
+ self.client.state = 'START'
+
+
+ def tearDown(self):
+ self.client = None
+
+
+ def _versionTest(self, serverVersionResponse):
+ """
+ Test L{NotificationClient} version negotiation.
+ """
+ self.client.factory.userHandle = "foo"
+
+ transport = StringTransport()
+ self.client.makeConnection(transport)
+ self.assertEqual(
+ transport.value(), "VER 1 MSNP8 CVR0\r\n")
+ transport.clear()
+
+ self.client.dataReceived(serverVersionResponse)
+ self.assertEqual(
+ transport.value(),
+ "CVR 2 0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS foo\r\n")
+
+
+ def test_version(self):
+ """
+ L{NotificationClient.connectionMade} greets the server with a I{VER}
+ (version) message and then L{NotificationClient.dataReceived}
+ handles the server's I{VER} response by sending a I{CVR} (client
+ version) message.
+ """
+ self._versionTest("VER 1 MSNP8 CVR0\r\n")
+
+
+ def test_versionWithoutCVR0(self):
+ """
+ If the server responds to a I{VER} command without including the
+ I{CVR0} protocol, L{NotificationClient} behaves in the same way as
+ if that protocol were included.
+
+ Starting in August 2008, CVR0 disappeared from the I{VER} response.
+ """
+ self._versionTest("VER 1 MSNP8\r\n")
+
+
+ def test_challenge(self):
+ """
+ L{NotificationClient} responds to a I{CHL} message by sending a I{QRY}
+ back which included a hash based on the parameters of the I{CHL}.
+ """
+ transport = StringTransport()
+ self.client.makeConnection(transport)
+ transport.clear()
+
+ challenge = "15570131571988941333"
+ self.client.dataReceived('CHL 0 ' + challenge + '\r\n')
+ # md5 of the challenge and a magic string defined by the protocol
+ response = "8f2f5a91b72102cd28355e9fc9000d6e"
+ # Sanity check - the response is what the comment above says it is.
+ self.assertEqual(
+ response, md5(challenge + "Q1P7W2E4J9R8U3S5").hexdigest())
+ self.assertEqual(
+ transport.value(),
+ # 2 is the next transaction identifier. 32 is the length of the
+ # response.
+ "QRY 2 msmsgs@msnmsgr.com 32\r\n" + response)
+
+
+ def testLogin(self):
+ self.client.lineReceived('USR 1 OK foo@bar.com Test%20Screen%20Name 1 0')
+ self.failUnless((self.client.state == 'LOGIN'), msg='Failed to detect successful login')
+
+
+ def test_loginWithoutSSLFailure(self):
+ """
+ L{NotificationClient.loginFailure} is called if the necessary SSL APIs
+ are unavailable.
+ """
+ self.patch(msn, 'ClientContextFactory', None)
+ success = []
+ self.client.loggedIn = lambda *args: success.append(args)
+ failure = []
+ self.client.loginFailure = failure.append
+
+ self.client.lineReceived('USR 6 TWN S opaque-string-goes-here')
+ self.assertEqual(success, [])
+ self.assertEqual(
+ failure,
+ ["Exception while authenticating: "
+ "Connecting to the Passport server requires SSL, but SSL is "
+ "unavailable."])
+
+
+ def testProfile(self):
+ m = 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
+ m += 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
+ m += 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
+ m += 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
+ map(self.client.lineReceived, m.split('\r\n')[:-1])
+ self.failUnless((self.client.state == 'PROFILE'), msg='Failed to detect initial profile')
+
+ def testStatus(self):
+ t = [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 0', 'INITSTATUS', 'Failed to detect initial status report'),
+ ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
+ ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
+ ('CHG 1 HDN 0', 'MYSTATUS', 'Failed to detect my status changing')]
+ for i in t:
+ self.client.lineReceived(i[0])
+ self.failUnless((self.client.state == i[1]), msg=i[2])
+
+ def testListSync(self):
+ # currently this test does not take into account the fact
+ # that BPRs sent as part of the SYN reply may not be interpreted
+ # as such if they are for the last LST -- maybe I should
+ # factor this in later.
+ self.client.makeConnection(StringTransport())
+ msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foobar', 1)
+ lines = [
+ "SYN %s 100 1 1" % self.client.currentID,
+ "GTC A",
+ "BLP AL",
+ "LSG 0 Other%20Contacts 0",
+ "LST userHandle@email.com Some%20Name 11 0"
+ ]
+ map(self.client.lineReceived, lines)
+ contacts = self.client.factory.contacts
+ contact = contacts.getContact('userHandle@email.com')
+ self.failUnless(contacts.version == 100, "Invalid contact list version")
+ self.failUnless(contact.screenName == 'Some Name', "Invalid screen-name for user")
+ self.failUnless(contacts.groups == {0 : 'Other Contacts'}, "Did not get proper group list")
+ self.failUnless(contact.groups == [0] and contact.lists == 11, "Invalid contact list/group info")
+ self.failUnless(self.client.state == 'GOTLIST', "Failed to call list sync handler")
+
+ def testAsyncPhoneChange(self):
+ c = msn.MSNContact(userHandle='userHandle@email.com')
+ self.client.factory.contacts = msn.MSNContactList()
+ self.client.factory.contacts.addContact(c)
+ self.client.makeConnection(StringTransport())
+ self.client.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
+ c = self.client.factory.contacts.getContact('userHandle@email.com')
+ self.failUnless(self.client.state == 'GOTPHONE', "Did not fire phone change callback")
+ self.failUnless(c.homePhone == '123 456', "Did not update the contact's phone number")
+ self.failUnless(self.client.factory.contacts.version == 101, "Did not update list version")
+
+ def testLateBPR(self):
+ """
+ This test makes sure that if a BPR response that was meant
+ to be part of a SYN response (but came after the last LST)
+ is received, the correct contact is updated and all is well
+ """
+ self.client.makeConnection(StringTransport())
+ msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foo', 1)
+ lines = [
+ "SYN %s 100 1 1" % self.client.currentID,
+ "GTC A",
+ "BLP AL",
+ "LSG 0 Other%20Contacts 0",
+ "LST userHandle@email.com Some%20Name 11 0",
+ "BPR PHH 123%20456"
+ ]
+ map(self.client.lineReceived, lines)
+ contact = self.client.factory.contacts.getContact('userHandle@email.com')
+ self.failUnless(contact.homePhone == '123 456', "Did not update contact's phone number")
+
+ def testUserRemovedMe(self):
+ self.client.factory.contacts = msn.MSNContactList()
+ contact = msn.MSNContact(userHandle='foo@foo.com')
+ contact.addToList(msn.REVERSE_LIST)
+ self.client.factory.contacts.addContact(contact)
+ self.client.lineReceived("REM 0 RL 100 foo@foo.com")
+ self.failUnless(self.client.state == 'USERREMOVEDME', "Failed to remove user from reverse list")
+
+ def testUserAddedMe(self):
+ self.client.factory.contacts = msn.MSNContactList()
+ self.client.lineReceived("ADD 0 RL 100 foo@foo.com Screen%20Name")
+ self.failUnless(self.client.state == 'USERADDEDME', "Failed to add user to reverse lise")
+
+ def testAsyncSwitchboardInvitation(self):
+ self.client.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
+ self.failUnless(self.client.state == "SBINVITED")
+
+ def testCommandFailed(self):
+ """
+ Ensures that error responses from the server fires an errback with
+ MSNCommandFailed.
+ """
+ id, d = self.client._createIDMapping()
+ self.client.lineReceived("201 %s" % id)
+ d = self.assertFailure(d, msn.MSNCommandFailed)
+ def assertErrorCode(exception):
+ self.assertEqual(201, exception.errorCode)
+ return d.addCallback(assertErrorCode)
+
+
+class MessageHandlingTests(unittest.TestCase):
+ """ testing various message handling methods from SwichboardClient """
+
+ def setUp(self):
+ self.client = DummySwitchboardClient()
+ self.client.state = 'START'
+
+ def tearDown(self):
+ self.client = None
+
+ def testClientCapabilitiesCheck(self):
+ m = msn.MSNMessage()
+ m.setHeader('Content-Type', 'text/x-clientcaps')
+ self.assertEqual(self.client.checkMessage(m), 0, 'Failed to detect client capability message')
+
+ def testTypingCheck(self):
+ m = msn.MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgscontrol')
+ m.setHeader('TypingUser', 'foo@bar')
+ self.client.checkMessage(m)
+ self.failUnless((self.client.state == 'TYPING'), msg='Failed to detect typing notification')
+
+ def testFileInvitation(self, lazyClient=False):
+ m = msn.MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Application-Name: File Transfer\r\n'
+ if not lazyClient:
+ m.message += 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n'
+ m.message += 'Invitation-Command: Invite\r\n'
+ m.message += 'Invitation-Cookie: 1234\r\n'
+ m.message += 'Application-File: foobar.ext\r\n'
+ m.message += 'Application-FileSize: 31337\r\n\r\n'
+ self.client.checkMessage(m)
+ self.failUnless((self.client.state == 'INVITATION'), msg='Failed to detect file transfer invitation')
+
+ def testFileInvitationMissingGUID(self):
+ return self.testFileInvitation(True)
+
+ def testFileResponse(self):
+ d = Deferred()
+ d.addCallback(self.fileResponse)
+ self.client.cookies['iCookies'][1234] = (d, None)
+ m = msn.MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Invitation-Command: ACCEPT\r\n'
+ m.message += 'Invitation-Cookie: 1234\r\n\r\n'
+ self.client.checkMessage(m)
+ self.failUnless((self.client.state == 'RESPONSE'), msg='Failed to detect file transfer response')
+
+ def testFileInfo(self):
+ d = Deferred()
+ d.addCallback(self.fileInfo)
+ self.client.cookies['external'][1234] = (d, None)
+ m = msn.MSNMessage()
+ m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+ m.message += 'Invitation-Command: ACCEPT\r\n'
+ m.message += 'Invitation-Cookie: 1234\r\n'
+ m.message += 'IP-Address: 192.168.0.1\r\n'
+ m.message += 'Port: 6891\r\n'
+ m.message += 'AuthCookie: 4321\r\n\r\n'
+ self.client.checkMessage(m)
+ self.failUnless((self.client.state == 'INFO'), msg='Failed to detect file transfer info')
+
+ def fileResponse(self, (accept, cookie, info)):
+ if accept and cookie == 1234: self.client.state = 'RESPONSE'
+
+ def fileInfo(self, (accept, ip, port, aCookie, info)):
+ if accept and ip == '192.168.0.1' and port == 6891 and aCookie == 4321: self.client.state = 'INFO'
+
+
+class FileTransferTestCase(unittest.TestCase):
+ """
+ test FileSend against FileReceive
+ """
+
+ def setUp(self):
+ self.input = 'a' * 7000
+ self.output = StringIOWithoutClosing()
+
+
+ def tearDown(self):
+ self.input = None
+ self.output = None
+
+
+ def test_fileTransfer(self):
+ """
+ Test L{FileSend} against L{FileReceive} using a loopback transport.
+ """
+ auth = 1234
+ sender = msn.FileSend(StringIO.StringIO(self.input))
+ sender.auth = auth
+ sender.fileSize = 7000
+ client = msn.FileReceive(auth, "foo@bar.com", self.output)
+ client.fileSize = 7000
+ def check(ignored):
+ self.assertTrue(
+ client.completed and sender.completed,
+ msg="send failed to complete")
+ self.assertEqual(
+ self.input, self.output.getvalue(),
+ msg="saved file does not match original")
+ d = loopback.loopbackAsync(sender, client)
+ d.addCallback(check)
+ return d
+
+if msn is None:
+ for testClass in [DispatchTests, PassportTests, NotificationTests,
+ MessageHandlingTests, FileTransferTestCase]:
+ testClass.skip = (
+ "MSN requires an HTTP client but none is available, "
+ "skipping tests.")
diff --git a/twisted/words/test/test_oscar.py b/twisted/words/test/test_oscar.py
new file mode 100644
index 0000000..f807ce5
--- /dev/null
+++ b/twisted/words/test/test_oscar.py
@@ -0,0 +1,24 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.protocols.oscar}.
+"""
+
+from twisted.trial.unittest import TestCase
+
+from twisted.words.protocols.oscar import encryptPasswordMD5
+
+
+class PasswordTests(TestCase):
+ """
+ Tests for L{encryptPasswordMD5}.
+ """
+ def test_encryptPasswordMD5(self):
+ """
+ L{encryptPasswordMD5} hashes the given password and key and returns a
+ string suitable to use to authenticate against an OSCAR server.
+ """
+ self.assertEqual(
+ encryptPasswordMD5('foo', 'bar').encode('hex'),
+ 'd73475c370a7b18c6c20386bcf1339f2')
diff --git a/twisted/words/test/test_service.py b/twisted/words/test/test_service.py
new file mode 100644
index 0000000..f8152ce
--- /dev/null
+++ b/twisted/words/test/test_service.py
@@ -0,0 +1,992 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.service}.
+"""
+
+import time
+
+from twisted.trial import unittest
+from twisted.test import proto_helpers
+
+from twisted.cred import portal, credentials, checkers
+from twisted.words import ewords, service
+from twisted.words.protocols import irc
+from twisted.spread import pb
+from twisted.internet.defer import Deferred, DeferredList, maybeDeferred, succeed
+from twisted.internet.defer import deferredGenerator as dG, waitForDeferred as wFD
+from twisted.internet import address, reactor
+
+class RealmTestCase(unittest.TestCase):
+ def _entityCreationTest(self, kind):
+ # Kind is "user" or "group"
+ realm = service.InMemoryWordsRealm("realmname")
+
+ name = u'test' + kind.lower()
+ create = getattr(realm, 'create' + kind.title())
+ get = getattr(realm, 'get' + kind.title())
+ flag = 'create' + kind.title() + 'OnRequest'
+ dupExc = getattr(ewords, 'Duplicate' + kind.title())
+ noSuchExc = getattr(ewords, 'NoSuch' + kind.title())
+
+ # Creating should succeed
+ d = wFD(create(name))
+ yield d
+ p = d.getResult()
+ self.assertEqual(p.name, name)
+
+ # Creating the same user again should not
+ d = wFD(create(name))
+ yield d
+ self.assertRaises(dupExc, d.getResult)
+
+ # Getting a non-existent user should succeed if createUserOnRequest is True
+ setattr(realm, flag, True)
+ d = wFD(get(u"new" + kind.lower()))
+ yield d
+ p = d.getResult()
+ self.assertEqual(p.name, "new" + kind.lower())
+
+ # Getting that user again should return the same object
+ d = wFD(get(u"new" + kind.lower()))
+ yield d
+ newp = d.getResult()
+ self.assertIdentical(p, newp)
+
+ # Getting a non-existent user should fail if createUserOnRequest is False
+ setattr(realm, flag, False)
+ d = wFD(get(u"another" + kind.lower()))
+ yield d
+ self.assertRaises(noSuchExc, d.getResult)
+ _entityCreationTest = dG(_entityCreationTest)
+
+
+ def testUserCreation(self):
+ return self._entityCreationTest("User")
+
+
+ def testGroupCreation(self):
+ return self._entityCreationTest("Group")
+
+
+ def testUserRetrieval(self):
+ realm = service.InMemoryWordsRealm("realmname")
+
+ # Make a user to play around with
+ d = wFD(realm.createUser(u"testuser"))
+ yield d
+ user = d.getResult()
+
+ # Make sure getting the user returns the same object
+ d = wFD(realm.getUser(u"testuser"))
+ yield d
+ retrieved = d.getResult()
+ self.assertIdentical(user, retrieved)
+
+ # Make sure looking up the user also returns the same object
+ d = wFD(realm.lookupUser(u"testuser"))
+ yield d
+ lookedUp = d.getResult()
+ self.assertIdentical(retrieved, lookedUp)
+
+ # Make sure looking up a user who does not exist fails
+ d = wFD(realm.lookupUser(u"nosuchuser"))
+ yield d
+ self.assertRaises(ewords.NoSuchUser, d.getResult)
+ testUserRetrieval = dG(testUserRetrieval)
+
+
+ def testUserAddition(self):
+ realm = service.InMemoryWordsRealm("realmname")
+
+ # Create and manually add a user to the realm
+ p = service.User("testuser")
+ d = wFD(realm.addUser(p))
+ yield d
+ user = d.getResult()
+ self.assertIdentical(p, user)
+
+ # Make sure getting that user returns the same object
+ d = wFD(realm.getUser(u"testuser"))
+ yield d
+ retrieved = d.getResult()
+ self.assertIdentical(user, retrieved)
+
+ # Make sure looking up that user returns the same object
+ d = wFD(realm.lookupUser(u"testuser"))
+ yield d
+ lookedUp = d.getResult()
+ self.assertIdentical(retrieved, lookedUp)
+ testUserAddition = dG(testUserAddition)
+
+
+ def testGroupRetrieval(self):
+ realm = service.InMemoryWordsRealm("realmname")
+
+ d = wFD(realm.createGroup(u"testgroup"))
+ yield d
+ group = d.getResult()
+
+ d = wFD(realm.getGroup(u"testgroup"))
+ yield d
+ retrieved = d.getResult()
+
+ self.assertIdentical(group, retrieved)
+
+ d = wFD(realm.getGroup(u"nosuchgroup"))
+ yield d
+ self.assertRaises(ewords.NoSuchGroup, d.getResult)
+ testGroupRetrieval = dG(testGroupRetrieval)
+
+
+ def testGroupAddition(self):
+ realm = service.InMemoryWordsRealm("realmname")
+
+ p = service.Group("testgroup")
+ d = wFD(realm.addGroup(p))
+ yield d
+ d.getResult()
+
+ d = wFD(realm.getGroup(u"testGroup"))
+ yield d
+ group = d.getResult()
+
+ self.assertIdentical(p, group)
+ testGroupAddition = dG(testGroupAddition)
+
+
+ def testGroupUsernameCollision(self):
+ """
+ Try creating a group with the same name as an existing user and
+ assert that it succeeds, since users and groups should not be in the
+ same namespace and collisions should be impossible.
+ """
+ realm = service.InMemoryWordsRealm("realmname")
+
+ d = wFD(realm.createUser(u"test"))
+ yield d
+ user = d.getResult()
+
+ d = wFD(realm.createGroup(u"test"))
+ yield d
+ group = d.getResult()
+ testGroupUsernameCollision = dG(testGroupUsernameCollision)
+
+
+ def testEnumeration(self):
+ realm = service.InMemoryWordsRealm("realmname")
+ d = wFD(realm.createGroup(u"groupone"))
+ yield d
+ d.getResult()
+
+ d = wFD(realm.createGroup(u"grouptwo"))
+ yield d
+ d.getResult()
+
+ groups = wFD(realm.itergroups())
+ yield groups
+ groups = groups.getResult()
+
+ n = [g.name for g in groups]
+ n.sort()
+ self.assertEqual(n, ["groupone", "grouptwo"])
+ testEnumeration = dG(testEnumeration)
+
+
+class TestGroup(object):
+ def __init__(self, name, size, topic):
+ self.name = name
+ self.size = lambda: size
+ self.meta = {'topic': topic}
+
+
+class TestUser(object):
+ def __init__(self, name, groups, signOn, lastMessage):
+ self.name = name
+ self.itergroups = lambda: iter([TestGroup(g, 3, 'Hello') for g in groups])
+ self.signOn = signOn
+ self.lastMessage = lastMessage
+
+
+class TestPortal(object):
+ def __init__(self):
+ self.logins = []
+
+
+ def login(self, credentials, mind, *interfaces):
+ d = Deferred()
+ self.logins.append((credentials, mind, interfaces, d))
+ return d
+
+
+class TestCaseUserAgg(object):
+ def __init__(self, user, realm, factory, address=address.IPv4Address('TCP', '127.0.0.1', 54321)):
+ self.user = user
+ self.transport = proto_helpers.StringTransportWithDisconnection()
+ self.protocol = factory.buildProtocol(address)
+ self.transport.protocol = self.protocol
+ self.user.mind = self.protocol
+ self.protocol.makeConnection(self.transport)
+
+
+ def write(self, stuff):
+ if isinstance(stuff, unicode):
+ stuff = stuff.encode('utf-8')
+ self.protocol.dataReceived(stuff)
+
+
+class IRCProtocolTestCase(unittest.TestCase):
+ STATIC_USERS = [
+ u'useruser', u'otheruser', u'someguy', u'firstuser', u'username',
+ u'userone', u'usertwo', u'userthree', u'someuser']
+
+
+ def setUp(self):
+ self.realm = service.InMemoryWordsRealm("realmname")
+ self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ self.portal = portal.Portal(self.realm, [self.checker])
+ self.factory = service.IRCFactory(self.realm, self.portal)
+
+ c = []
+ for nick in self.STATIC_USERS:
+ c.append(self.realm.createUser(nick))
+ self.checker.addUser(nick.encode('ascii'), nick + "_password")
+ return DeferredList(c)
+
+
+ def _assertGreeting(self, user):
+ """
+ The user has been greeted with the four messages that are (usually)
+ considered to start an IRC session.
+
+ Asserts that the required responses were received.
+ """
+ # Make sure we get 1-4 at least
+ response = self._response(user)
+ expected = [irc.RPL_WELCOME, irc.RPL_YOURHOST, irc.RPL_CREATED,
+ irc.RPL_MYINFO]
+ for (prefix, command, args) in response:
+ if command in expected:
+ expected.remove(command)
+ self.failIf(expected, "Missing responses for %r" % (expected,))
+
+
+ def _login(self, user, nick, password=None):
+ if password is None:
+ password = nick + "_password"
+ user.write('PASS %s\r\n' % (password,))
+ user.write('NICK %s extrainfo\r\n' % (nick,))
+
+
+ def _loggedInUser(self, name):
+ d = wFD(self.realm.lookupUser(name))
+ yield d
+ user = d.getResult()
+ agg = TestCaseUserAgg(user, self.realm, self.factory)
+ self._login(agg, name)
+ yield agg
+ _loggedInUser = dG(_loggedInUser)
+
+
+ def _response(self, user, messageType=None):
+ """
+ Extracts the user's response, and returns a list of parsed lines.
+ If messageType is defined, only messages of that type will be returned.
+ """
+ response = user.transport.value().splitlines()
+ user.transport.clear()
+ result = []
+ for message in map(irc.parsemsg, response):
+ if messageType is None or message[1] == messageType:
+ result.append(message)
+ return result
+
+
+ def testPASSLogin(self):
+ user = wFD(self._loggedInUser(u'firstuser'))
+ yield user
+ user = user.getResult()
+ self._assertGreeting(user)
+ testPASSLogin = dG(testPASSLogin)
+
+
+ def test_nickServLogin(self):
+ """
+ Sending NICK without PASS will prompt the user for their password.
+ When the user sends their password to NickServ, it will respond with a
+ Greeting.
+ """
+ firstuser = wFD(self.realm.lookupUser(u'firstuser'))
+ yield firstuser
+ firstuser = firstuser.getResult()
+
+ user = TestCaseUserAgg(firstuser, self.realm, self.factory)
+ user.write('NICK firstuser extrainfo\r\n')
+ response = self._response(user, 'PRIVMSG')
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][0], service.NICKSERV)
+ self.assertEqual(response[0][1], 'PRIVMSG')
+ self.assertEqual(response[0][2], ['firstuser', 'Password?'])
+ user.transport.clear()
+
+ user.write('PRIVMSG nickserv firstuser_password\r\n')
+ self._assertGreeting(user)
+ test_nickServLogin = dG(test_nickServLogin)
+
+
+ def testFailedLogin(self):
+ firstuser = wFD(self.realm.lookupUser(u'firstuser'))
+ yield firstuser
+ firstuser = firstuser.getResult()
+
+ user = TestCaseUserAgg(firstuser, self.realm, self.factory)
+ self._login(user, "firstuser", "wrongpass")
+ response = self._response(user, "PRIVMSG")
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][2], ['firstuser', 'Login failed. Goodbye.'])
+ testFailedLogin = dG(testFailedLogin)
+
+
+ def testLogout(self):
+ logout = []
+ firstuser = wFD(self.realm.lookupUser(u'firstuser'))
+ yield firstuser
+ firstuser = firstuser.getResult()
+
+ user = TestCaseUserAgg(firstuser, self.realm, self.factory)
+ self._login(user, "firstuser")
+ user.protocol.logout = lambda: logout.append(True)
+ user.write('QUIT\r\n')
+ self.assertEqual(logout, [True])
+ testLogout = dG(testLogout)
+
+
+ def testJoin(self):
+ firstuser = wFD(self.realm.lookupUser(u'firstuser'))
+ yield firstuser
+ firstuser = firstuser.getResult()
+
+ somechannel = wFD(self.realm.createGroup(u"somechannel"))
+ yield somechannel
+ somechannel = somechannel.getResult()
+
+ somechannel.meta['topic'] = 'some random topic'
+
+ # Bring in one user, make sure he gets into the channel sanely
+ user = TestCaseUserAgg(firstuser, self.realm, self.factory)
+ self._login(user, "firstuser")
+ user.transport.clear()
+ user.write('JOIN #somechannel\r\n')
+
+ response = self._response(user)
+ self.assertEqual(len(response), 5)
+
+ # Join message
+ self.assertEqual(response[0][0], 'firstuser!firstuser@realmname')
+ self.assertEqual(response[0][1], 'JOIN')
+ self.assertEqual(response[0][2], ['#somechannel'])
+
+ # User list
+ self.assertEqual(response[1][1], '353')
+ self.assertEqual(response[2][1], '366')
+
+ # Topic (or lack thereof, as the case may be)
+ self.assertEqual(response[3][1], '332')
+ self.assertEqual(response[4][1], '333')
+
+
+ # Hook up another client! It is a CHAT SYSTEM!!!!!!!
+ other = wFD(self._loggedInUser(u'otheruser'))
+ yield other
+ other = other.getResult()
+
+ other.transport.clear()
+ user.transport.clear()
+ other.write('JOIN #somechannel\r\n')
+
+ # At this point, both users should be in the channel
+ response = self._response(other)
+
+ event = self._response(user)
+ self.assertEqual(len(event), 1)
+ self.assertEqual(event[0][0], 'otheruser!otheruser@realmname')
+ self.assertEqual(event[0][1], 'JOIN')
+ self.assertEqual(event[0][2], ['#somechannel'])
+
+ self.assertEqual(response[1][0], 'realmname')
+ self.assertEqual(response[1][1], '353')
+ self.assertEqual(response[1][2], ['otheruser', '=', '#somechannel', 'firstuser otheruser'])
+ testJoin = dG(testJoin)
+
+
+ def test_joinTopicless(self):
+ """
+ When a user joins a group without a topic, no topic information is
+ sent to that user.
+ """
+ firstuser = wFD(self.realm.lookupUser(u'firstuser'))
+ yield firstuser
+ firstuser = firstuser.getResult()
+
+ somechannel = wFD(self.realm.createGroup(u"somechannel"))
+ yield somechannel
+ somechannel = somechannel.getResult()
+
+ # Bring in one user, make sure he gets into the channel sanely
+ user = TestCaseUserAgg(firstuser, self.realm, self.factory)
+ self._login(user, "firstuser")
+ user.transport.clear()
+ user.write('JOIN #somechannel\r\n')
+
+ response = self._response(user)
+ responseCodes = [r[1] for r in response]
+ self.assertNotIn('332', responseCodes)
+ self.assertNotIn('333', responseCodes)
+ test_joinTopicless = dG(test_joinTopicless)
+
+
+ def testLeave(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ somechannel = wFD(self.realm.createGroup(u"somechannel"))
+ yield somechannel
+ somechannel = somechannel.getResult()
+
+ user.write('JOIN #somechannel\r\n')
+ user.transport.clear()
+
+ other = wFD(self._loggedInUser(u'otheruser'))
+ yield other
+ other = other.getResult()
+
+ other.write('JOIN #somechannel\r\n')
+
+ user.transport.clear()
+ other.transport.clear()
+
+ user.write('PART #somechannel\r\n')
+
+ response = self._response(user)
+ event = self._response(other)
+
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][0], 'useruser!useruser@realmname')
+ self.assertEqual(response[0][1], 'PART')
+ self.assertEqual(response[0][2], ['#somechannel', 'leaving'])
+ self.assertEqual(response, event)
+
+ # Now again, with a part message
+ user.write('JOIN #somechannel\r\n')
+
+ user.transport.clear()
+ other.transport.clear()
+
+ user.write('PART #somechannel :goodbye stupidheads\r\n')
+
+ response = self._response(user)
+ event = self._response(other)
+
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][0], 'useruser!useruser@realmname')
+ self.assertEqual(response[0][1], 'PART')
+ self.assertEqual(response[0][2], ['#somechannel', 'goodbye stupidheads'])
+ self.assertEqual(response, event)
+ testLeave = dG(testLeave)
+
+
+ def testGetTopic(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ group = service.Group("somechannel")
+ group.meta["topic"] = "This is a test topic."
+ group.meta["topic_author"] = "some_fellow"
+ group.meta["topic_date"] = 77777777
+
+ add = wFD(self.realm.addGroup(group))
+ yield add
+ add.getResult()
+
+ user.transport.clear()
+ user.write("JOIN #somechannel\r\n")
+
+ response = self._response(user)
+
+ self.assertEqual(response[3][0], 'realmname')
+ self.assertEqual(response[3][1], '332')
+
+ # XXX Sigh. irc.parsemsg() is not as correct as one might hope.
+ self.assertEqual(response[3][2], ['useruser', '#somechannel', 'This is a test topic.'])
+ self.assertEqual(response[4][1], '333')
+ self.assertEqual(response[4][2], ['useruser', '#somechannel', 'some_fellow', '77777777'])
+
+ user.transport.clear()
+
+ user.write('TOPIC #somechannel\r\n')
+
+ response = self._response(user)
+
+ self.assertEqual(response[0][1], '332')
+ self.assertEqual(response[0][2], ['useruser', '#somechannel', 'This is a test topic.'])
+ self.assertEqual(response[1][1], '333')
+ self.assertEqual(response[1][2], ['useruser', '#somechannel', 'some_fellow', '77777777'])
+ testGetTopic = dG(testGetTopic)
+
+
+ def testSetTopic(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ add = wFD(self.realm.createGroup(u"somechannel"))
+ yield add
+ somechannel = add.getResult()
+
+ user.write("JOIN #somechannel\r\n")
+
+ other = wFD(self._loggedInUser(u'otheruser'))
+ yield other
+ other = other.getResult()
+
+ other.write("JOIN #somechannel\r\n")
+
+ user.transport.clear()
+ other.transport.clear()
+
+ other.write('TOPIC #somechannel :This is the new topic.\r\n')
+
+ response = self._response(other)
+ event = self._response(user)
+
+ self.assertEqual(response, event)
+
+ self.assertEqual(response[0][0], 'otheruser!otheruser@realmname')
+ self.assertEqual(response[0][1], 'TOPIC')
+ self.assertEqual(response[0][2], ['#somechannel', 'This is the new topic.'])
+
+ other.transport.clear()
+
+ somechannel.meta['topic_date'] = 12345
+ other.write('TOPIC #somechannel\r\n')
+
+ response = self._response(other)
+ self.assertEqual(response[0][1], '332')
+ self.assertEqual(response[0][2], ['otheruser', '#somechannel', 'This is the new topic.'])
+ self.assertEqual(response[1][1], '333')
+ self.assertEqual(response[1][2], ['otheruser', '#somechannel', 'otheruser', '12345'])
+
+ other.transport.clear()
+ other.write('TOPIC #asdlkjasd\r\n')
+
+ response = self._response(other)
+ self.assertEqual(response[0][1], '403')
+ testSetTopic = dG(testSetTopic)
+
+
+ def testGroupMessage(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ add = wFD(self.realm.createGroup(u"somechannel"))
+ yield add
+ somechannel = add.getResult()
+
+ user.write("JOIN #somechannel\r\n")
+
+ other = wFD(self._loggedInUser(u'otheruser'))
+ yield other
+ other = other.getResult()
+
+ other.write("JOIN #somechannel\r\n")
+
+ user.transport.clear()
+ other.transport.clear()
+
+ user.write('PRIVMSG #somechannel :Hello, world.\r\n')
+
+ response = self._response(user)
+ event = self._response(other)
+
+ self.failIf(response)
+ self.assertEqual(len(event), 1)
+ self.assertEqual(event[0][0], 'useruser!useruser@realmname')
+ self.assertEqual(event[0][1], 'PRIVMSG', -1)
+ self.assertEqual(event[0][2], ['#somechannel', 'Hello, world.'])
+ testGroupMessage = dG(testGroupMessage)
+
+
+ def testPrivateMessage(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ other = wFD(self._loggedInUser(u'otheruser'))
+ yield other
+ other = other.getResult()
+
+ user.transport.clear()
+ other.transport.clear()
+
+ user.write('PRIVMSG otheruser :Hello, monkey.\r\n')
+
+ response = self._response(user)
+ event = self._response(other)
+
+ self.failIf(response)
+ self.assertEqual(len(event), 1)
+ self.assertEqual(event[0][0], 'useruser!useruser@realmname')
+ self.assertEqual(event[0][1], 'PRIVMSG')
+ self.assertEqual(event[0][2], ['otheruser', 'Hello, monkey.'])
+
+ user.write('PRIVMSG nousernamedthis :Hello, monkey.\r\n')
+
+ response = self._response(user)
+
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][0], 'realmname')
+ self.assertEqual(response[0][1], '401')
+ self.assertEqual(response[0][2], ['useruser', 'nousernamedthis', 'No such nick/channel.'])
+ testPrivateMessage = dG(testPrivateMessage)
+
+
+ def testOper(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ user.transport.clear()
+ user.write('OPER user pass\r\n')
+ response = self._response(user)
+
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][1], '491')
+ testOper = dG(testOper)
+
+
+ def testGetUserMode(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ user.transport.clear()
+ user.write('MODE useruser\r\n')
+
+ response = self._response(user)
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][0], 'realmname')
+ self.assertEqual(response[0][1], '221')
+ self.assertEqual(response[0][2], ['useruser', '+'])
+ testGetUserMode = dG(testGetUserMode)
+
+
+ def testSetUserMode(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ user.transport.clear()
+ user.write('MODE useruser +abcd\r\n')
+
+ response = self._response(user)
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][1], '472')
+ testSetUserMode = dG(testSetUserMode)
+
+
+ def testGetGroupMode(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ add = wFD(self.realm.createGroup(u"somechannel"))
+ yield add
+ somechannel = add.getResult()
+
+ user.write('JOIN #somechannel\r\n')
+
+ user.transport.clear()
+ user.write('MODE #somechannel\r\n')
+
+ response = self._response(user)
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][1], '324')
+ testGetGroupMode = dG(testGetGroupMode)
+
+
+ def testSetGroupMode(self):
+ user = wFD(self._loggedInUser(u'useruser'))
+ yield user
+ user = user.getResult()
+
+ group = wFD(self.realm.createGroup(u"groupname"))
+ yield group
+ group = group.getResult()
+
+ user.write('JOIN #groupname\r\n')
+
+ user.transport.clear()
+ user.write('MODE #groupname +abcd\r\n')
+
+ response = self._response(user)
+ self.assertEqual(len(response), 1)
+ self.assertEqual(response[0][1], '472')
+ testSetGroupMode = dG(testSetGroupMode)
+
+
+ def testWho(self):
+ group = service.Group('groupname')
+ add = wFD(self.realm.addGroup(group))
+ yield add
+ add.getResult()
+
+ users = []
+ for nick in u'userone', u'usertwo', u'userthree':
+ u = wFD(self._loggedInUser(nick))
+ yield u
+ u = u.getResult()
+ users.append(u)
+ users[-1].write('JOIN #groupname\r\n')
+ for user in users:
+ user.transport.clear()
+
+ users[0].write('WHO #groupname\r\n')
+
+ r = self._response(users[0])
+ self.failIf(self._response(users[1]))
+ self.failIf(self._response(users[2]))
+
+ wantusers = ['userone', 'usertwo', 'userthree']
+ for (prefix, code, stuff) in r[:-1]:
+ self.assertEqual(prefix, 'realmname')
+ self.assertEqual(code, '352')
+
+ (myname, group, theirname, theirhost, theirserver, theirnick, flag, extra) = stuff
+ self.assertEqual(myname, 'userone')
+ self.assertEqual(group, '#groupname')
+ self.failUnless(theirname in wantusers)
+ self.assertEqual(theirhost, 'realmname')
+ self.assertEqual(theirserver, 'realmname')
+ wantusers.remove(theirnick)
+ self.assertEqual(flag, 'H')
+ self.assertEqual(extra, '0 ' + theirnick)
+ self.failIf(wantusers)
+
+ prefix, code, stuff = r[-1]
+ self.assertEqual(prefix, 'realmname')
+ self.assertEqual(code, '315')
+ myname, channel, extra = stuff
+ self.assertEqual(myname, 'userone')
+ self.assertEqual(channel, '#groupname')
+ self.assertEqual(extra, 'End of /WHO list.')
+ testWho = dG(testWho)
+
+
+ def testList(self):
+ user = wFD(self._loggedInUser(u"someuser"))
+ yield user
+ user = user.getResult()
+ user.transport.clear()
+
+ somegroup = wFD(self.realm.createGroup(u"somegroup"))
+ yield somegroup
+ somegroup = somegroup.getResult()
+ somegroup.size = lambda: succeed(17)
+ somegroup.meta['topic'] = 'this is the topic woo'
+
+ # Test one group
+ user.write('LIST #somegroup\r\n')
+
+ r = self._response(user)
+ self.assertEqual(len(r), 2)
+ resp, end = r
+
+ self.assertEqual(resp[0], 'realmname')
+ self.assertEqual(resp[1], '322')
+ self.assertEqual(resp[2][0], 'someuser')
+ self.assertEqual(resp[2][1], 'somegroup')
+ self.assertEqual(resp[2][2], '17')
+ self.assertEqual(resp[2][3], 'this is the topic woo')
+
+ self.assertEqual(end[0], 'realmname')
+ self.assertEqual(end[1], '323')
+ self.assertEqual(end[2][0], 'someuser')
+ self.assertEqual(end[2][1], 'End of /LIST')
+
+ user.transport.clear()
+ # Test all groups
+
+ user.write('LIST\r\n')
+ r = self._response(user)
+ self.assertEqual(len(r), 2)
+
+ fg1, end = r
+
+ self.assertEqual(fg1[1], '322')
+ self.assertEqual(fg1[2][1], 'somegroup')
+ self.assertEqual(fg1[2][2], '17')
+ self.assertEqual(fg1[2][3], 'this is the topic woo')
+
+ self.assertEqual(end[1], '323')
+ testList = dG(testList)
+
+
+ def testWhois(self):
+ user = wFD(self._loggedInUser(u'someguy'))
+ yield user
+ user = user.getResult()
+
+ otherguy = service.User("otherguy")
+ otherguy.itergroups = lambda: iter([
+ service.Group('groupA'),
+ service.Group('groupB')])
+ otherguy.signOn = 10
+ otherguy.lastMessage = time.time() - 15
+
+ add = wFD(self.realm.addUser(otherguy))
+ yield add
+ add.getResult()
+
+ user.transport.clear()
+ user.write('WHOIS otherguy\r\n')
+ r = self._response(user)
+
+ self.assertEqual(len(r), 5)
+ wuser, wserver, idle, channels, end = r
+
+ self.assertEqual(wuser[0], 'realmname')
+ self.assertEqual(wuser[1], '311')
+ self.assertEqual(wuser[2][0], 'someguy')
+ self.assertEqual(wuser[2][1], 'otherguy')
+ self.assertEqual(wuser[2][2], 'otherguy')
+ self.assertEqual(wuser[2][3], 'realmname')
+ self.assertEqual(wuser[2][4], '*')
+ self.assertEqual(wuser[2][5], 'otherguy')
+
+ self.assertEqual(wserver[0], 'realmname')
+ self.assertEqual(wserver[1], '312')
+ self.assertEqual(wserver[2][0], 'someguy')
+ self.assertEqual(wserver[2][1], 'otherguy')
+ self.assertEqual(wserver[2][2], 'realmname')
+ self.assertEqual(wserver[2][3], 'Hi mom!')
+
+ self.assertEqual(idle[0], 'realmname')
+ self.assertEqual(idle[1], '317')
+ self.assertEqual(idle[2][0], 'someguy')
+ self.assertEqual(idle[2][1], 'otherguy')
+ self.assertEqual(idle[2][2], '15')
+ self.assertEqual(idle[2][3], '10')
+ self.assertEqual(idle[2][4], "seconds idle, signon time")
+
+ self.assertEqual(channels[0], 'realmname')
+ self.assertEqual(channels[1], '319')
+ self.assertEqual(channels[2][0], 'someguy')
+ self.assertEqual(channels[2][1], 'otherguy')
+ self.assertEqual(channels[2][2], '#groupA #groupB')
+
+ self.assertEqual(end[0], 'realmname')
+ self.assertEqual(end[1], '318')
+ self.assertEqual(end[2][0], 'someguy')
+ self.assertEqual(end[2][1], 'otherguy')
+ self.assertEqual(end[2][2], 'End of WHOIS list.')
+ testWhois = dG(testWhois)
+
+
+class TestMind(service.PBMind):
+ def __init__(self, *a, **kw):
+ self.joins = []
+ self.parts = []
+ self.messages = []
+ self.meta = []
+
+ def remote_userJoined(self, user, group):
+ self.joins.append((user, group))
+
+
+ def remote_userLeft(self, user, group, reason):
+ self.parts.append((user, group, reason))
+
+
+ def remote_receive(self, sender, recipient, message):
+ self.messages.append((sender, recipient, message))
+
+
+ def remote_groupMetaUpdate(self, group, meta):
+ self.meta.append((group, meta))
+pb.setUnjellyableForClass(TestMind, service.PBMindReference)
+
+
+class PBProtocolTestCase(unittest.TestCase):
+ def setUp(self):
+ self.realm = service.InMemoryWordsRealm("realmname")
+ self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
+ self.portal = portal.Portal(
+ self.realm, [self.checker])
+ self.serverFactory = pb.PBServerFactory(self.portal)
+ self.serverFactory.protocol = self._protocolFactory
+ self.serverFactory.unsafeTracebacks = True
+ self.clientFactory = pb.PBClientFactory()
+ self.clientFactory.unsafeTracebacks = True
+ self.serverPort = reactor.listenTCP(0, self.serverFactory)
+ self.clientConn = reactor.connectTCP(
+ '127.0.0.1',
+ self.serverPort.getHost().port,
+ self.clientFactory)
+
+
+ def _protocolFactory(self, *args, **kw):
+ self._serverProtocol = pb.Broker(0)
+ return self._serverProtocol
+
+
+ def tearDown(self):
+ d3 = Deferred()
+ self._serverProtocol.notifyOnDisconnect(lambda: d3.callback(None))
+ return DeferredList([
+ maybeDeferred(self.serverPort.stopListening),
+ maybeDeferred(self.clientConn.disconnect), d3])
+
+
+ def _loggedInAvatar(self, name, password, mind):
+ creds = credentials.UsernamePassword(name, password)
+ self.checker.addUser(name.encode('ascii'), password)
+ d = self.realm.createUser(name)
+ d.addCallback(lambda ign: self.clientFactory.login(creds, mind))
+ return d
+
+
+ def testGroups(self):
+ mindone = TestMind()
+ one = wFD(self._loggedInAvatar(u"one", "p1", mindone))
+ yield one
+ one = one.getResult()
+
+ mindtwo = TestMind()
+ two = wFD(self._loggedInAvatar(u"two", "p2", mindtwo))
+ yield two
+ two = two.getResult()
+
+ add = wFD(self.realm.createGroup(u"foobar"))
+ yield add
+ add.getResult()
+
+ groupone = wFD(one.join(u"foobar"))
+ yield groupone
+ groupone = groupone.getResult()
+
+ grouptwo = wFD(two.join(u"foobar"))
+ yield grouptwo
+ grouptwo = grouptwo.getResult()
+
+ msg = wFD(groupone.send({"text": "hello, monkeys"}))
+ yield msg
+ msg = msg.getResult()
+
+ leave = wFD(groupone.leave())
+ yield leave
+ leave = leave.getResult()
+ testGroups = dG(testGroups)
diff --git a/twisted/words/test/test_tap.py b/twisted/words/test/test_tap.py
new file mode 100644
index 0000000..099c104
--- /dev/null
+++ b/twisted/words/test/test_tap.py
@@ -0,0 +1,78 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.cred import credentials, error
+from twisted.words import tap
+from twisted.trial import unittest
+
+
+
+class WordsTap(unittest.TestCase):
+ """
+ Ensures that the twisted.words.tap API works.
+ """
+
+ PASSWD_TEXT = "admin:admin\njoe:foo\n"
+ admin = credentials.UsernamePassword('admin', 'admin')
+ joeWrong = credentials.UsernamePassword('joe', 'bar')
+
+
+ def setUp(self):
+ """
+ Create a file with two users.
+ """
+ self.filename = self.mktemp()
+ self.file = open(self.filename, 'w')
+ self.file.write(self.PASSWD_TEXT)
+ self.file.flush()
+
+
+ def tearDown(self):
+ """
+ Close the dummy user database.
+ """
+ self.file.close()
+
+
+ def test_hostname(self):
+ """
+ Tests that the --hostname parameter gets passed to Options.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--hostname', 'myhost'])
+ self.assertEqual(opt['hostname'], 'myhost')
+
+
+ def test_passwd(self):
+ """
+ Tests the --passwd command for backwards-compatibility.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--passwd', self.file.name])
+ self._loginTest(opt)
+
+
+ def test_auth(self):
+ """
+ Tests that the --auth command generates a checker.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--auth', 'file:'+self.file.name])
+ self._loginTest(opt)
+
+
+ def _loginTest(self, opt):
+ """
+ This method executes both positive and negative authentication
+ tests against whatever credentials checker has been stored in
+ the Options class.
+
+ @param opt: An instance of L{tap.Options}.
+ """
+ self.assertEqual(len(opt['credCheckers']), 1)
+ checker = opt['credCheckers'][0]
+ self.assertFailure(checker.requestAvatarId(self.joeWrong),
+ error.UnauthorizedLogin)
+ def _gotAvatar(username):
+ self.assertEqual(username, self.admin.username)
+ return checker.requestAvatarId(self.admin).addCallback(_gotAvatar)
diff --git a/twisted/words/test/test_xishutil.py b/twisted/words/test/test_xishutil.py
new file mode 100644
index 0000000..b046e6e
--- /dev/null
+++ b/twisted/words/test/test_xishutil.py
@@ -0,0 +1,345 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test cases for twisted.words.xish.utility
+"""
+
+from twisted.trial import unittest
+
+from twisted.python.util import OrderedDict
+from twisted.words.xish import utility
+from twisted.words.xish.domish import Element
+from twisted.words.xish.utility import EventDispatcher
+
+class CallbackTracker:
+ """
+ Test helper for tracking callbacks.
+
+ Increases a counter on each call to L{call} and stores the object
+ passed in the call.
+ """
+
+ def __init__(self):
+ self.called = 0
+ self.obj = None
+
+
+ def call(self, obj):
+ self.called = self.called + 1
+ self.obj = obj
+
+
+
+class OrderedCallbackTracker:
+ """
+ Test helper for tracking callbacks and their order.
+ """
+
+ def __init__(self):
+ self.callList = []
+
+
+ def call1(self, object):
+ self.callList.append(self.call1)
+
+
+ def call2(self, object):
+ self.callList.append(self.call2)
+
+
+ def call3(self, object):
+ self.callList.append(self.call3)
+
+
+
+class EventDispatcherTest(unittest.TestCase):
+ """
+ Tests for L{EventDispatcher}.
+ """
+
+ def testStuff(self):
+ d = EventDispatcher()
+ cb1 = CallbackTracker()
+ cb2 = CallbackTracker()
+ cb3 = CallbackTracker()
+
+ d.addObserver("/message/body", cb1.call)
+ d.addObserver("/message", cb1.call)
+ d.addObserver("/presence", cb2.call)
+ d.addObserver("//event/testevent", cb3.call)
+
+ msg = Element(("ns", "message"))
+ msg.addElement("body")
+
+ pres = Element(("ns", "presence"))
+ pres.addElement("presence")
+
+ d.dispatch(msg)
+ self.assertEqual(cb1.called, 2)
+ self.assertEqual(cb1.obj, msg)
+ self.assertEqual(cb2.called, 0)
+
+ d.dispatch(pres)
+ self.assertEqual(cb1.called, 2)
+ self.assertEqual(cb2.called, 1)
+ self.assertEqual(cb2.obj, pres)
+ self.assertEqual(cb3.called, 0)
+
+ d.dispatch(d, "//event/testevent")
+ self.assertEqual(cb3.called, 1)
+ self.assertEqual(cb3.obj, d)
+
+ d.removeObserver("/presence", cb2.call)
+ d.dispatch(pres)
+ self.assertEqual(cb2.called, 1)
+
+
+ def test_addObserverTwice(self):
+ """
+ Test adding two observers for the same query.
+
+ When the event is dispath both of the observers need to be called.
+ """
+ d = EventDispatcher()
+ cb1 = CallbackTracker()
+ cb2 = CallbackTracker()
+
+ d.addObserver("//event/testevent", cb1.call)
+ d.addObserver("//event/testevent", cb2.call)
+ d.dispatch(d, "//event/testevent")
+
+ self.assertEqual(cb1.called, 1)
+ self.assertEqual(cb1.obj, d)
+ self.assertEqual(cb2.called, 1)
+ self.assertEqual(cb2.obj, d)
+
+
+ def test_addObserverInDispatch(self):
+ """
+ Test for registration of an observer during dispatch.
+ """
+ d = EventDispatcher()
+ msg = Element(("ns", "message"))
+ cb = CallbackTracker()
+
+ def onMessage(_):
+ d.addObserver("/message", cb.call)
+
+ d.addOnetimeObserver("/message", onMessage)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 0)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 1)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 2)
+
+
+ def test_addOnetimeObserverInDispatch(self):
+ """
+ Test for registration of a onetime observer during dispatch.
+ """
+ d = EventDispatcher()
+ msg = Element(("ns", "message"))
+ cb = CallbackTracker()
+
+ def onMessage(msg):
+ d.addOnetimeObserver("/message", cb.call)
+
+ d.addOnetimeObserver("/message", onMessage)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 0)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 1)
+
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 1)
+
+
+ def testOnetimeDispatch(self):
+ d = EventDispatcher()
+ msg = Element(("ns", "message"))
+ cb = CallbackTracker()
+
+ d.addOnetimeObserver("/message", cb.call)
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 1)
+ d.dispatch(msg)
+ self.assertEqual(cb.called, 1)
+
+
+ def testDispatcherResult(self):
+ d = EventDispatcher()
+ msg = Element(("ns", "message"))
+ pres = Element(("ns", "presence"))
+ cb = CallbackTracker()
+
+ d.addObserver("/presence", cb.call)
+ result = d.dispatch(msg)
+ self.assertEqual(False, result)
+
+ result = d.dispatch(pres)
+ self.assertEqual(True, result)
+
+
+ def testOrderedXPathDispatch(self):
+ d = EventDispatcher()
+ cb = OrderedCallbackTracker()
+ d.addObserver("/message/body", cb.call2)
+ d.addObserver("/message", cb.call3, -1)
+ d.addObserver("/message/body", cb.call1, 1)
+
+ msg = Element(("ns", "message"))
+ msg.addElement("body")
+ d.dispatch(msg)
+ self.assertEqual(cb.callList, [cb.call1, cb.call2, cb.call3],
+ "Calls out of order: %s" %
+ repr([c.__name__ for c in cb.callList]))
+
+
+ # Observers are put into CallbackLists that are then put into dictionaries
+ # keyed by the event trigger. Upon removal of the last observer for a
+ # particular event trigger, the (now empty) CallbackList and corresponding
+ # event trigger should be removed from those dictionaries to prevent
+ # slowdown and memory leakage.
+
+ def test_cleanUpRemoveEventObserver(self):
+ """
+ Test observer clean-up after removeObserver for named events.
+ """
+
+ d = EventDispatcher()
+ cb = CallbackTracker()
+
+ d.addObserver('//event/test', cb.call)
+ d.dispatch(None, '//event/test')
+ self.assertEqual(1, cb.called)
+ d.removeObserver('//event/test', cb.call)
+ self.assertEqual(0, len(d._eventObservers.pop(0)))
+
+
+ def test_cleanUpRemoveXPathObserver(self):
+ """
+ Test observer clean-up after removeObserver for XPath events.
+ """
+
+ d = EventDispatcher()
+ cb = CallbackTracker()
+ msg = Element((None, "message"))
+
+ d.addObserver('/message', cb.call)
+ d.dispatch(msg)
+ self.assertEqual(1, cb.called)
+ d.removeObserver('/message', cb.call)
+ self.assertEqual(0, len(d._xpathObservers.pop(0)))
+
+
+ def test_cleanUpOnetimeEventObserver(self):
+ """
+ Test observer clean-up after onetime named events.
+ """
+
+ d = EventDispatcher()
+ cb = CallbackTracker()
+
+ d.addOnetimeObserver('//event/test', cb.call)
+ d.dispatch(None, '//event/test')
+ self.assertEqual(1, cb.called)
+ self.assertEqual(0, len(d._eventObservers.pop(0)))
+
+
+ def test_cleanUpOnetimeXPathObserver(self):
+ """
+ Test observer clean-up after onetime XPath events.
+ """
+
+ d = EventDispatcher()
+ cb = CallbackTracker()
+ msg = Element((None, "message"))
+
+ d.addOnetimeObserver('/message', cb.call)
+ d.dispatch(msg)
+ self.assertEqual(1, cb.called)
+ self.assertEqual(0, len(d._xpathObservers.pop(0)))
+
+
+ def test_observerRaisingException(self):
+ """
+ Test that exceptions in observers do not bubble up to dispatch.
+
+ The exceptions raised in observers should be logged and other
+ observers should be called as if nothing happened.
+ """
+
+ class OrderedCallbackList(utility.CallbackList):
+ def __init__(self):
+ self.callbacks = OrderedDict()
+
+ class TestError(Exception):
+ pass
+
+ def raiseError(_):
+ raise TestError()
+
+ d = EventDispatcher()
+ cb = CallbackTracker()
+
+ originalCallbackList = utility.CallbackList
+
+ try:
+ utility.CallbackList = OrderedCallbackList
+
+ d.addObserver('//event/test', raiseError)
+ d.addObserver('//event/test', cb.call)
+ try:
+ d.dispatch(None, '//event/test')
+ except TestError:
+ self.fail("TestError raised. Should have been logged instead.")
+
+ self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
+ self.assertEqual(1, cb.called)
+ finally:
+ utility.CallbackList = originalCallbackList
+
+
+
+class XmlPipeTest(unittest.TestCase):
+ """
+ Tests for L{twisted.words.xish.utility.XmlPipe}.
+ """
+
+ def setUp(self):
+ self.pipe = utility.XmlPipe()
+
+
+ def test_sendFromSource(self):
+ """
+ Send an element from the source and observe it from the sink.
+ """
+ def cb(obj):
+ called.append(obj)
+
+ called = []
+ self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb)
+ element = Element(('testns', 'test'))
+ self.pipe.source.send(element)
+ self.assertEqual([element], called)
+
+
+ def test_sendFromSink(self):
+ """
+ Send an element from the sink and observe it from the source.
+ """
+ def cb(obj):
+ called.append(obj)
+
+ called = []
+ self.pipe.source.addObserver('/test[@xmlns="testns"]', cb)
+ element = Element(('testns', 'test'))
+ self.pipe.sink.send(element)
+ self.assertEqual([element], called)
diff --git a/twisted/words/test/test_xmlstream.py b/twisted/words/test/test_xmlstream.py
new file mode 100644
index 0000000..4eb2446
--- /dev/null
+++ b/twisted/words/test/test_xmlstream.py
@@ -0,0 +1,224 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.xish.xmlstream}.
+"""
+
+from twisted.internet import protocol
+from twisted.python import failure
+from twisted.trial import unittest
+from twisted.words.xish import domish, utility, xmlstream
+
+class XmlStreamTest(unittest.TestCase):
+ def setUp(self):
+ self.connectionLostMsg = "no reason"
+ self.outlist = []
+ self.xmlstream = xmlstream.XmlStream()
+ self.xmlstream.transport = self
+ self.xmlstream.transport.write = self.outlist.append
+
+
+ def loseConnection(self):
+ """
+ Stub loseConnection because we are a transport.
+ """
+ self.xmlstream.connectionLost(failure.Failure(
+ Exception(self.connectionLostMsg)))
+
+
+ def test_send(self):
+ """
+ Calling L{xmlstream.XmlStream.send} results in the data being written
+ to the transport.
+ """
+ self.xmlstream.connectionMade()
+ self.xmlstream.send("<root>")
+ self.assertEqual(self.outlist[0], "<root>")
+
+
+ def test_receiveRoot(self):
+ """
+ Receiving the starttag of the root element results in stream start.
+ """
+ streamStarted = []
+
+ def streamStartEvent(rootelem):
+ streamStarted.append(None)
+
+ self.xmlstream.addObserver(xmlstream.STREAM_START_EVENT,
+ streamStartEvent)
+ self.xmlstream.connectionMade()
+ self.xmlstream.dataReceived("<root>")
+ self.assertEqual(1, len(streamStarted))
+
+
+ def test_receiveBadXML(self):
+ """
+ Receiving malformed XML results in an L{STREAM_ERROR_EVENT}.
+ """
+ streamError = []
+ streamEnd = []
+
+ def streamErrorEvent(reason):
+ streamError.append(reason)
+
+ def streamEndEvent(_):
+ streamEnd.append(None)
+
+ self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT,
+ streamErrorEvent)
+ self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
+ streamEndEvent)
+ self.xmlstream.connectionMade()
+
+ self.xmlstream.dataReceived("<root>")
+ self.assertEqual(0, len(streamError))
+ self.assertEqual(0, len(streamEnd))
+
+ self.xmlstream.dataReceived("<child><unclosed></child>")
+ self.assertEqual(1, len(streamError))
+ self.assertTrue(streamError[0].check(domish.ParserError))
+ self.assertEqual(1, len(streamEnd))
+
+
+ def test_streamEnd(self):
+ """
+ Ending the stream fires a L{STREAM_END_EVENT}.
+ """
+ streamEnd = []
+
+ def streamEndEvent(reason):
+ streamEnd.append(reason)
+
+ self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
+ streamEndEvent)
+ self.xmlstream.connectionMade()
+ self.loseConnection()
+ self.assertEqual(1, len(streamEnd))
+ self.assertIsInstance(streamEnd[0], failure.Failure)
+ self.assertEqual(streamEnd[0].getErrorMessage(),
+ self.connectionLostMsg)
+
+
+
+class DummyProtocol(protocol.Protocol, utility.EventDispatcher):
+ """
+ I am a protocol with an event dispatcher without further processing.
+
+ This protocol is only used for testing XmlStreamFactoryMixin to make
+ sure the bootstrap observers are added to the protocol instance.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ self.observers = []
+
+ utility.EventDispatcher.__init__(self)
+
+
+
+class BootstrapMixinTest(unittest.TestCase):
+ """
+ Tests for L{xmlstream.BootstrapMixin}.
+
+ @ivar factory: Instance of the factory or mixin under test.
+ """
+
+ def setUp(self):
+ self.factory = xmlstream.BootstrapMixin()
+
+
+ def test_installBootstraps(self):
+ """
+ Dispatching an event fires registered bootstrap observers.
+ """
+ called = []
+
+ def cb(data):
+ called.append(data)
+
+ dispatcher = DummyProtocol()
+ self.factory.addBootstrap('//event/myevent', cb)
+ self.factory.installBootstraps(dispatcher)
+
+ dispatcher.dispatch(None, '//event/myevent')
+ self.assertEqual(1, len(called))
+
+
+ def test_addAndRemoveBootstrap(self):
+ """
+ Test addition and removal of a bootstrap event handler.
+ """
+
+ called = []
+
+ def cb(data):
+ called.append(data)
+
+ self.factory.addBootstrap('//event/myevent', cb)
+ self.factory.removeBootstrap('//event/myevent', cb)
+
+ dispatcher = DummyProtocol()
+ self.factory.installBootstraps(dispatcher)
+
+ dispatcher.dispatch(None, '//event/myevent')
+ self.assertFalse(called)
+
+
+
+class GenericXmlStreamFactoryTestsMixin(BootstrapMixinTest):
+ """
+ Generic tests for L{XmlStream} factories.
+ """
+
+ def setUp(self):
+ self.factory = xmlstream.XmlStreamFactory()
+
+
+ def test_buildProtocolInstallsBootstraps(self):
+ """
+ The protocol factory installs bootstrap event handlers on the protocol.
+ """
+ called = []
+
+ def cb(data):
+ called.append(data)
+
+ self.factory.addBootstrap('//event/myevent', cb)
+
+ xs = self.factory.buildProtocol(None)
+ xs.dispatch(None, '//event/myevent')
+
+ self.assertEqual(1, len(called))
+
+
+ def test_buildProtocolStoresFactory(self):
+ """
+ The protocol factory is saved in the protocol.
+ """
+ xs = self.factory.buildProtocol(None)
+ self.assertIdentical(self.factory, xs.factory)
+
+
+
+class XmlStreamFactoryMixinTest(GenericXmlStreamFactoryTestsMixin):
+ """
+ Tests for L{xmlstream.XmlStreamFactoryMixin}.
+ """
+
+ def setUp(self):
+ self.factory = xmlstream.XmlStreamFactoryMixin(None, test=None)
+ self.factory.protocol = DummyProtocol
+
+
+ def test_buildProtocolFactoryArguments(self):
+ """
+ Arguments passed to the factory are passed to protocol on
+ instantiation.
+ """
+ xs = self.factory.buildProtocol(None)
+
+ self.assertEqual((None,), xs.args)
+ self.assertEqual({'test': None}, xs.kwargs)
diff --git a/twisted/words/test/test_xmpproutertap.py b/twisted/words/test/test_xmpproutertap.py
new file mode 100644
index 0000000..aaadf34
--- /dev/null
+++ b/twisted/words/test/test_xmpproutertap.py
@@ -0,0 +1,84 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Tests for L{twisted.words.xmpproutertap}.
+"""
+
+from twisted.application import internet
+from twisted.trial import unittest
+from twisted.words import xmpproutertap as tap
+from twisted.words.protocols.jabber import component
+
+class XMPPRouterTapTest(unittest.TestCase):
+
+ def test_port(self):
+ """
+ The port option is recognised as a parameter.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--port', '7001'])
+ self.assertEqual(opt['port'], '7001')
+
+
+ def test_portDefault(self):
+ """
+ The port option has '5347' as default value
+ """
+ opt = tap.Options()
+ opt.parseOptions([])
+ self.assertEqual(opt['port'], 'tcp:5347:interface=127.0.0.1')
+
+
+ def test_secret(self):
+ """
+ The secret option is recognised as a parameter.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--secret', 'hushhush'])
+ self.assertEqual(opt['secret'], 'hushhush')
+
+
+ def test_secretDefault(self):
+ """
+ The secret option has 'secret' as default value
+ """
+ opt = tap.Options()
+ opt.parseOptions([])
+ self.assertEqual(opt['secret'], 'secret')
+
+
+ def test_verbose(self):
+ """
+ The verbose option is recognised as a flag.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--verbose'])
+ self.assertTrue(opt['verbose'])
+
+
+ def test_makeService(self):
+ """
+ The service gets set up with a router and factory.
+ """
+ opt = tap.Options()
+ opt.parseOptions([])
+ s = tap.makeService(opt)
+ self.assertIsInstance(s, internet.StreamServerEndpointService)
+ self.assertEqual('127.0.0.1', s.endpoint._interface)
+ self.assertEqual(5347, s.endpoint._port)
+ factory = s.factory
+ self.assertIsInstance(factory, component.XMPPComponentServerFactory)
+ self.assertIsInstance(factory.router, component.Router)
+ self.assertEqual('secret', factory.secret)
+ self.assertFalse(factory.logTraffic)
+
+
+ def test_makeServiceVerbose(self):
+ """
+ The verbose flag enables traffic logging.
+ """
+ opt = tap.Options()
+ opt.parseOptions(['--verbose'])
+ s = tap.makeService(opt)
+ self.assertTrue(s.factory.logTraffic)
diff --git a/twisted/words/test/test_xpath.py b/twisted/words/test/test_xpath.py
new file mode 100644
index 0000000..9dbda0f
--- /dev/null
+++ b/twisted/words/test/test_xpath.py
@@ -0,0 +1,260 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+from twisted.trial import unittest
+import sys, os
+
+from twisted.words.xish.domish import Element
+from twisted.words.xish.xpath import XPathQuery
+from twisted.words.xish import xpath
+
+class XPathTest(unittest.TestCase):
+ def setUp(self):
+ # Build element:
+ # <foo xmlns='testns' attrib1='value1' attrib3="user@host/resource">
+ # somecontent
+ # <bar>
+ # <foo>
+ # <gar>DEF</gar>
+ # </foo>
+ # </bar>
+ # somemorecontent
+ # <bar attrib2="value2">
+ # <bar>
+ # <foo/>
+ # <gar>ABC</gar>
+ # </bar>
+ # <bar/>
+ # <bar attrib4='value4' attrib5='value5'>
+ # <foo/>
+ # <gar>JKL</gar>
+ # </bar>
+ # <bar attrib4='value4' attrib5='value4'>
+ # <foo/>
+ # <gar>MNO</gar>
+ # </bar>
+ # <bar attrib4='value4' attrib5='value6'/>
+ # </foo>
+ self.e = Element(("testns", "foo"))
+ self.e["attrib1"] = "value1"
+ self.e["attrib3"] = "user@host/resource"
+ self.e.addContent("somecontent")
+ self.bar1 = self.e.addElement("bar")
+ self.subfoo = self.bar1.addElement("foo")
+ self.gar1 = self.subfoo.addElement("gar")
+ self.gar1.addContent("DEF")
+ self.e.addContent("somemorecontent")
+ self.bar2 = self.e.addElement("bar")
+ self.bar2["attrib2"] = "value2"
+ self.bar3 = self.bar2.addElement("bar")
+ self.subfoo2 = self.bar3.addElement("foo")
+ self.gar2 = self.bar3.addElement("gar")
+ self.gar2.addContent("ABC")
+ self.bar4 = self.e.addElement("bar")
+ self.bar5 = self.e.addElement("bar")
+ self.bar5["attrib4"] = "value4"
+ self.bar5["attrib5"] = "value5"
+ self.subfoo3 = self.bar5.addElement("foo")
+ self.gar3 = self.bar5.addElement("gar")
+ self.gar3.addContent("JKL")
+ self.bar6 = self.e.addElement("bar")
+ self.bar6["attrib4"] = "value4"
+ self.bar6["attrib5"] = "value4"
+ self.subfoo4 = self.bar6.addElement("foo")
+ self.gar4 = self.bar6.addElement("gar")
+ self.gar4.addContent("MNO")
+ self.bar7 = self.e.addElement("bar")
+ self.bar7["attrib4"] = "value4"
+ self.bar7["attrib5"] = "value6"
+
+ def test_staticMethods(self):
+ """
+ Test basic operation of the static methods.
+ """
+ self.assertEqual(xpath.matches("/foo/bar", self.e),
+ True)
+ self.assertEqual(xpath.queryForNodes("/foo/bar", self.e),
+ [self.bar1, self.bar2, self.bar4,
+ self.bar5, self.bar6, self.bar7])
+ self.assertEqual(xpath.queryForString("/foo", self.e),
+ "somecontent")
+ self.assertEqual(xpath.queryForStringList("/foo", self.e),
+ ["somecontent", "somemorecontent"])
+
+ def test_locationFooBar(self):
+ """
+ Test matching foo with child bar.
+ """
+ xp = XPathQuery("/foo/bar")
+ self.assertEqual(xp.matches(self.e), 1)
+
+ def test_locationFooBarFoo(self):
+ """
+ Test finding foos at the second level.
+ """
+ xp = XPathQuery("/foo/bar/foo")
+ self.assertEqual(xp.matches(self.e), 1)
+ self.assertEqual(xp.queryForNodes(self.e), [self.subfoo,
+ self.subfoo3,
+ self.subfoo4])
+
+ def test_locationNoBar3(self):
+ """
+ Test not finding bar3.
+ """
+ xp = XPathQuery("/foo/bar3")
+ self.assertEqual(xp.matches(self.e), 0)
+
+ def test_locationAllChilds(self):
+ """
+ Test finding childs of foo.
+ """
+ xp = XPathQuery("/foo/*")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar1, self.bar2,
+ self.bar4, self.bar5,
+ self.bar6, self.bar7])
+
+ def test_attribute(self):
+ """
+ Test matching foo with attribute.
+ """
+ xp = XPathQuery("/foo[@attrib1]")
+ self.assertEqual(xp.matches(self.e), True)
+
+ def test_attributeWithValueAny(self):
+ """
+ Test find nodes with attribute having value.
+ """
+ xp = XPathQuery("/foo/*[@attrib2='value2']")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar2])
+
+ def test_position(self):
+ """
+ Test finding element at position.
+ """
+ xp = XPathQuery("/foo/bar[2]")
+ self.assertEqual(xp.matches(self.e), 1)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar1])
+
+ test_position.todo = "XPath queries with position are not working."
+
+ def test_namespaceFound(self):
+ """
+ Test matching node with namespace.
+ """
+ xp = XPathQuery("/foo[@xmlns='testns']/bar")
+ self.assertEqual(xp.matches(self.e), 1)
+
+ def test_namespaceNotFound(self):
+ """
+ Test not matching node with wrong namespace.
+ """
+ xp = XPathQuery("/foo[@xmlns='badns']/bar2")
+ self.assertEqual(xp.matches(self.e), 0)
+
+ def test_attributeWithValue(self):
+ """
+ Test matching node with attribute having value.
+ """
+ xp = XPathQuery("/foo[@attrib1='value1']")
+ self.assertEqual(xp.matches(self.e), 1)
+
+ def test_queryForString(self):
+ """
+ Test for queryForString and queryForStringList.
+ """
+ xp = XPathQuery("/foo")
+ self.assertEqual(xp.queryForString(self.e), "somecontent")
+ self.assertEqual(xp.queryForStringList(self.e),
+ ["somecontent", "somemorecontent"])
+
+ def test_queryForNodes(self):
+ """
+ Test finding nodes.
+ """
+ xp = XPathQuery("/foo/bar")
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar1, self.bar2,
+ self.bar4, self.bar5,
+ self.bar6, self.bar7])
+
+ def test_textCondition(self):
+ """
+ Test matching a node with given text.
+ """
+ xp = XPathQuery("/foo[text() = 'somecontent']")
+ self.assertEqual(xp.matches(self.e), True)
+
+ def test_textNotOperator(self):
+ """
+ Test for not operator.
+ """
+ xp = XPathQuery("/foo[not(@nosuchattrib)]")
+ self.assertEqual(xp.matches(self.e), True)
+
+ def test_anyLocationAndText(self):
+ """
+ Test finding any nodes named gar and getting their text contents.
+ """
+ xp = XPathQuery("//gar")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.gar1, self.gar2,
+ self.gar3, self.gar4])
+ self.assertEqual(xp.queryForStringList(self.e), ["DEF", "ABC",
+ "JKL", "MNO"])
+
+ def test_anyLocation(self):
+ """
+ Test finding any nodes named bar.
+ """
+ xp = XPathQuery("//bar")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar1, self.bar2,
+ self.bar3, self.bar4,
+ self.bar5, self.bar6,
+ self.bar7])
+
+ def test_anyLocationQueryForString(self):
+ """
+ L{XPathQuery.queryForString} should raise a L{NotImplementedError}
+ for any location.
+ """
+ xp = XPathQuery("//bar")
+ self.assertRaises(NotImplementedError, xp.queryForString, None)
+
+ def test_andOperator(self):
+ """
+ Test boolean and operator in condition.
+ """
+ xp = XPathQuery("//bar[@attrib4='value4' and @attrib5='value5']")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar5])
+
+ def test_orOperator(self):
+ """
+ Test boolean or operator in condition.
+ """
+ xp = XPathQuery("//bar[@attrib5='value4' or @attrib5='value5']")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar5, self.bar6])
+
+ def test_booleanOperatorsParens(self):
+ """
+ Test multiple boolean operators in condition with parens.
+ """
+ xp = XPathQuery("""//bar[@attrib4='value4' and
+ (@attrib5='value4' or @attrib5='value6')]""")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar6, self.bar7])
+
+ def test_booleanOperatorsNoParens(self):
+ """
+ Test multiple boolean operators in condition without parens.
+ """
+ xp = XPathQuery("""//bar[@attrib5='value4' or
+ @attrib5='value5' or
+ @attrib5='value6']""")
+ self.assertEqual(xp.matches(self.e), True)
+ self.assertEqual(xp.queryForNodes(self.e), [self.bar5, self.bar6, self.bar7])
diff --git a/twisted/words/topfiles/NEWS b/twisted/words/topfiles/NEWS
new file mode 100644
index 0000000..4cb4cbb
--- /dev/null
+++ b/twisted/words/topfiles/NEWS
@@ -0,0 +1,359 @@
+Ticket numbers in this file can be looked up by visiting
+http://twistedmatrix.com/trac/ticket/<number>
+
+Twisted Words 12.1.0 (2012-06-02)
+=================================
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.DccChatFactory.buildProtocol now
+ returns the protocol object that it creates (#3179)
+ - twisted.words.im no longer offers an empty threat of a rewrite on
+ import. (#5598)
+
+Other
+-----
+ - #5555, #5595
+
+
+Twisted Words 12.0.0 (2012-02-10)
+=================================
+
+Improved Documentation
+----------------------
+ - twisted.words.im.basechat now has improved API documentation.
+ (#2458)
+
+Other
+-----
+ - #5401
+
+
+Twisted Words 11.1.0 (2011-11-15)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient now uses a PING heartbeat as
+ a keepalive to avoid losing an IRC connection without being aware
+ of it. (#5047)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient now replies only once to
+ known CTCP queries per message and not at all to unknown CTCP
+ queries. (#5029)
+ - IRCClient.msg now determines a safe maximum command length,
+ drastically reducing the chance of relayed text being truncated on
+ the server side. (#5176)
+
+Deprecations and Removals
+-------------------------
+ - twisted.words.protocols.irc.IRCClient.me was deprecated in Twisted
+ 9.0 and has been removed. Use IRCClient.describe instead. (#5059)
+
+Other
+-----
+ - #5025, #5330
+
+
+Twisted Words 11.0.0 (2011-04-01)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient now has an invite method.
+ (#4820)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient.say is once again able to
+ send messages when using the default value for the length limit
+ argument. (#4758)
+ - twisted.words.protocols.jabber.jstrports is once again able to
+ parse jstrport description strings. (#4771)
+ - twisted.words.protocols.msn.NotificationClient now calls the
+ loginFailure callback when it is unable to connect to the Passport
+ server due to missing SSL dependencies. (#4801)
+ - twisted.words.protocols.jabber.xmpp_stringprep now always uses
+ Unicode version 3.2 for stringprep normalization. (#4850)
+
+Improved Documentation
+----------------------
+ - Removed the non-working AIM bot example, depending on the obsolete
+ twisted.words.protocols.toc functionality. (#4007)
+ - Outdated GUI-related information was removed from the IM howto.
+ (#4054)
+
+Deprecations and Removals
+-------------------------
+ - Remove twisted.words.protocols.toc, that was largely non-working
+ and useless since AOL disabled TOC on their AIM network. (#4363)
+
+Other
+-----
+ - #4733, #4902
+
+
+Twisted Words 10.2.0 (2010-11-29)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient.msg now enforces a maximum
+ length for messages, splitting up messages that are too long.
+ (#4416)
+
+Bugfixes
+--------
+ - twisted.words.protocols.irc.IRCClient no longer invokes privmsg()
+ in the default noticed() implementation. (#4419)
+ - twisted.words.im.ircsupport.IRCProto now sends the correct name in
+ the USER command. (#4641)
+
+Deprecations and Removals
+-------------------------
+ - Remove twisted.words.im.proxyui and twisted.words.im.tap. (#1823)
+
+
+Twisted Words 10.1.0 (2010-06-27)
+=================================
+
+Bugfixes
+--------
+ - twisted.words.im.basechat.ChatUI now has a functional
+ contactChangedNick with unit tests. (#229)
+ - twisted.words.protocols.jabber.error.StanzaError now correctly sets
+ a default error type and code for the remote-server-timeout
+ condition (#4311)
+ - twisted.words.protocols.jabber.xmlstream.ListenAuthenticator now
+ uses unicode objects for session identifiers (#4345)
+
+
+Twisted Words 10.0.0 (2010-03-01)
+=================================
+
+Features
+--------
+ - twisted.words.protocols.irc.IRCClient.irc_MODE now takes ISUPPORT
+ parameters into account when parsing mode messages with arguments
+ that take parameters (#3296)
+
+Bugfixes
+--------
+ - When twisted.words.protocols.irc.IRCClient's versionNum and
+ versionEnv attributes are set to None, they will no longer be
+ included in the client's response to CTCP VERSION queries. (#3660)
+
+ - twisted.words.protocols.jabber.xmlstream.hashPassword now only
+ accepts unicode as input (#3741, #3742, #3847)
+
+Other
+-----
+ - #2503, #4066, #4261
+
+
+Twisted Words 9.0.0 (2009-11-24)
+================================
+
+Features
+--------
+ - IRCClient.describe is a new method meant to replace IRCClient.me to send
+ CTCP ACTION messages with less confusing behavior (#3910)
+ - The XMPP client protocol implementation now supports ANONYMOUS SASL
+ authentication (#4067)
+ - The IRC client protocol implementation now has better support for the
+ ISUPPORT server->client message, storing the data in a new
+ ServerSupportedFeatures object accessible via IRCClient.supported (#3285)
+
+Fixes
+-----
+ - The twisted.words IRC server now always sends an MOTD, which at least makes
+ Pidgin able to successfully connect to a twisted.words IRC server (#2385)
+ - The IRC client will now dispatch "RPL MOTD" messages received before a
+ "RPL MOTD START" instead of raising an exception (#3676)
+ - The IRC client protocol implementation no longer updates its 'nickname'
+ attribute directly; instead, that attribute will be updated when the server
+ acknowledges the change (#3377)
+ - The IRC client protocol implementation now supports falling back to another
+ nickname when a nick change request fails (#3377, #4010)
+
+Deprecations and Removals
+-------------------------
+ - The TOC protocol implementation is now deprecated, since the protocol itself
+ has been deprecated and obselete for quite a long time (#3580)
+ - The gui "im" application has been removed, since it relied on GTK1, which is
+ hard to find these days (#3699, #3340)
+
+Other
+-----
+ - #2763, #3540, #3647, #3750, #3895, #3968, #4050
+
+Words 8.2.0 (2008-12-16)
+========================
+
+Feature
+-------
+ - There is now a standalone XMPP router included in twisted.words: it can be
+ used with the 'twistd xmpp-router' command line (#3407)
+ - A server factory for Jabber XML Streams has been added (#3435)
+ - Domish now allows for iterating child elements with specific qualified names
+ (#2429)
+ - IRCClient now has a 'back' method which removes the away status (#3366)
+ - IRCClient now has a 'whois' method (#3133)
+
+Fixes
+-----
+ - The IRC Client implementation can now deal with compound mode changes (#3230)
+ - The MSN protocol implementation no longer requires the CVR0 protocol to
+ be included in the VER command (#3394)
+ - In the IRC server implementation, topic messages will no longer be sent for
+ a group which has no topic (#2204)
+ - An infinite loop (which caused infinite memory usage) in irc.split has been
+ fixed. This was triggered any time a message that starts with a delimiter
+ was sent (#3446)
+ - Jabber's toResponse now generates a valid stanza even when stanzaType is not
+ specified (#3467)
+ - The lifetime of authenticator instances in XmlStreamServerFactory is no
+ longer artificially extended (#3464)
+
+Other
+-----
+ - #3365
+
+
+8.1.0 (2008-05-18)
+==================
+
+Features
+--------
+ - JID objects now have a nice __repr__ (#3156)
+ - Extending XMPP protocols is now easier (#2178)
+
+Fixes
+-----
+ - The deprecated mktap API is no longer used (#3127)
+ - A bug whereby one-time XMPP observers would be enabled permanently was fixed
+ (#3066)
+
+
+8.0.0 (2008-03-17)
+==================
+
+Features
+--------
+ - Provide function for creating XMPP response stanzas. (#2614, #2614)
+ - Log exceptions raised in Xish observers. (#2616)
+ - Add 'and' and 'or' operators for Xish XPath expressions. (#2502)
+ - Make JIDs hashable. (#2770)
+
+Fixes
+-----
+ - Respect the hostname and servername parameters to IRCClient.register. (#1649)
+ - Make EventDispatcher remove empty callback lists. (#1652)
+ - Use legacy base64 API to support Python 2.3 (#2461)
+ - Fix support of DIGEST-MD5 challenge parsing with multi-valued directives.
+ (#2606)
+ - Fix reuse of dict of prefixes in domish.Element.toXml (#2609)
+ - Properly process XMPP stream headers (#2615)
+ - Use proper namespace for XMPP stream errors. (#2630)
+ - Properly parse XMPP stream errors. (#2771)
+ - Fix toResponse for XMPP stanzas without an id attribute. (#2773)
+ - Move XMPP stream header procesing to authenticators. (#2772)
+
+Misc
+----
+ - #2617, #2640, #2741, #2063, #2570, #2847
+
+
+0.5.0 (2007-01-06)
+==================
+
+Features
+--------
+ - (Jabber) IQ.send now optionally has a 'timeout' parameter which
+ specifies a time at which to errback the Deferred with a
+ TimeoutError (#2218)
+ - (Jabber) SASL authentication, resource binding and session
+ establishment were added. (#1046) The following were done in
+ support of this change:
+ - Rework ConnectAuthenticator to work with initializer objects that
+ provide a stream initialization step.
+ - Reimplement iq:auth as an initializer.
+ - Reimplement TLS negotiation as an initializer.
+ - Add XMPPAuthenticator as a XMPP 1.0 client authenticator (only), along
+ with XMPPClientFactory.
+ - Add support for working with pre-XMPP-1.0 error stanzas.
+ - Remove hasFeature() from XmlStream as you can test (uri, name) in
+ xs.features.
+ - Add sendFooter() and sendStreamError() to XmlStream
+
+Fixes
+-----
+ - (Jabber) Deferreds from queries which were never resolved before
+ a lost connection are now errbacked (#2006)
+ - (Jabber) servers which didn't send a 'realm' directive in
+ authentication challenges no longer cause the Jabber client to
+ choke (#2098)
+ - (MSN) error responses are now properly turned into errbacks (#2019)
+ - (IRC) A trivial bug in IRCClient which would cause whois(oper=True)
+ to always raise an exception was fixed (#2089)
+ - (IM) Bugs in the error handling and already-connecting cases of
+ AbstractAccount.logOn were fixed (#2086)
+
+Misc
+----
+ - #1734, #1735, #1636, #1936, #1883, #1995, #2171, #2165, #2177
+
+
+0.4.0 (2006-05-21)
+==================
+
+Features
+--------
+ - Jabber:
+ - Add support for stream and stanza level errors
+ - Create new IQ stanza helper that works with deferreds
+ - Add TLS support for initiating entities to XmlStream
+ - Fix account registration
+ - Xish:
+ - Fix various namespace issues
+ - Add IElement
+ - Store namespace declarations in parsed XML for later serialization
+ - Fix user name/group collision in server service (#1655).
+ - Correctly recognize MSN capability messages (#861).
+
+Fixes
+-----
+ - Misc: #1283, #1296, #1302, #1424
+ - Fix unicode/str confusion in IRC server service.
+
+
+0.3.0:
+ - Jabber:
+
+ - Fix digest authentication in Jabber
+ - Add Jabber xmlstream module that contains the Jabber specific bits that
+ got factored out of Twisted Xish's xmlstream, and make it suitable for
+ implementing full XMPP support.
+ - Xish:
+ - Fixed serialization in _ListSerializer
+ - Removed unneeded extra whitespace generated in serialization
+ - Removed _Serializer in favour of _ListSerializer
+ - Use unicode objects for representing serialized XML, instead of utf-8
+ encoded str objects.
+ - Properly catch XML parser errors
+ - Rework and fix element stream test cases
+ - Strip xmlstream from all Jabber specifics that moved to Twisted Words
+ - Added exhaustive docstrings to xmlstream.
+ - Words Service:
+ - Complete rewrite
+ - Not backwards compatible
+
+0.1.0:
+ - Fix some miscellaneous bugs in OSCAR
+ - Add QUIT notification for IRC
+ - Fix message wrapping
+ - Misc Jabber fixes
+ - Add stringprep support for Jabber IDs
+ This only works properly on 2.3.2 or higher
diff --git a/twisted/words/topfiles/README b/twisted/words/topfiles/README
new file mode 100644
index 0000000..1d6f538
--- /dev/null
+++ b/twisted/words/topfiles/README
@@ -0,0 +1,5 @@
+Twisted Words 12.1.0
+
+Twisted Words depends on Twisted Core and Twisted Web. The Twisted Web
+dependency is only necessary for MSN support. MSN support also requires HTTPS,
+and therefore pyOpenSSL (<http://launchpad.net/pyopenssl>).
diff --git a/twisted/words/topfiles/setup.py b/twisted/words/topfiles/setup.py
new file mode 100644
index 0000000..2df89fd
--- /dev/null
+++ b/twisted/words/topfiles/setup.py
@@ -0,0 +1,53 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+import sys
+
+try:
+ from twisted.python import dist
+except ImportError:
+ raise SystemExit("twisted.python.dist module not found. Make sure you "
+ "have installed the Twisted core package before "
+ "attempting to install any other Twisted projects.")
+
+if __name__ == '__main__':
+ if sys.version_info[:2] >= (2, 4):
+ extraMeta = dict(
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: No Input/Output (Daemon)",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python",
+ "Topic :: Communications :: Chat",
+ "Topic :: Communications :: Chat :: AOL Instant Messenger",
+ "Topic :: Communications :: Chat :: ICQ",
+ "Topic :: Communications :: Chat :: Internet Relay Chat",
+ "Topic :: Internet",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ])
+ else:
+ extraMeta = {}
+
+ dist.setup(
+ twisted_subproject="words",
+ scripts=dist.getScripts("words"),
+ # metadata
+ name="Twisted Words",
+ description="Twisted Words contains Instant Messaging implementations.",
+ author="Twisted Matrix Laboratories",
+ author_email="twisted-python@twistedmatrix.com",
+ maintainer="Jp Calderone",
+ url="http://twistedmatrix.com/trac/wiki/TwistedWords",
+ license="MIT",
+ long_description="""\
+Twisted Words contains implementations of many Instant Messaging
+protocols, including IRC, Jabber, MSN, OSCAR (AIM & ICQ), TOC (AOL),
+and some functionality for creating bots, inter-protocol gateways, and
+a client application for many of the protocols.
+
+In support of Jabber, Twisted Words also contains X-ish, a library for
+processing XML with Twisted and Python, with support for a Pythonic DOM and
+an XPath-like toolkit.
+""",
+ **extraMeta)
diff --git a/twisted/words/xish/__init__.py b/twisted/words/xish/__init__.py
new file mode 100644
index 0000000..1d2469f
--- /dev/null
+++ b/twisted/words/xish/__init__.py
@@ -0,0 +1,10 @@
+# -*- test-case-name: twisted.words.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+
+Twisted X-ish: XML-ish DOM and XPath-ish engine
+
+"""
diff --git a/twisted/words/xish/domish.py b/twisted/words/xish/domish.py
new file mode 100644
index 0000000..407ee0c
--- /dev/null
+++ b/twisted/words/xish/domish.py
@@ -0,0 +1,848 @@
+# -*- test-case-name: twisted.words.test.test_domish -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+DOM-like XML processing support.
+
+This module provides support for parsing XML into DOM-like object structures
+and serializing such structures to an XML string representation, optimized
+for use in streaming XML applications.
+"""
+
+import types
+
+from zope.interface import implements, Interface, Attribute
+
+def _splitPrefix(name):
+ """ Internal method for splitting a prefixed Element name into its
+ respective parts """
+ ntok = name.split(":", 1)
+ if len(ntok) == 2:
+ return ntok
+ else:
+ return (None, ntok[0])
+
+# Global map of prefixes that always get injected
+# into the serializers prefix map (note, that doesn't
+# mean they're always _USED_)
+G_PREFIXES = { "http://www.w3.org/XML/1998/namespace":"xml" }
+
+class _ListSerializer:
+ """ Internal class which serializes an Element tree into a buffer """
+ def __init__(self, prefixes=None, prefixesInScope=None):
+ self.writelist = []
+ self.prefixes = {}
+ if prefixes:
+ self.prefixes.update(prefixes)
+ self.prefixes.update(G_PREFIXES)
+ self.prefixStack = [G_PREFIXES.values()] + (prefixesInScope or [])
+ self.prefixCounter = 0
+
+ def getValue(self):
+ return u"".join(self.writelist)
+
+ def getPrefix(self, uri):
+ if not self.prefixes.has_key(uri):
+ self.prefixes[uri] = "xn%d" % (self.prefixCounter)
+ self.prefixCounter = self.prefixCounter + 1
+ return self.prefixes[uri]
+
+ def prefixInScope(self, prefix):
+ stack = self.prefixStack
+ for i in range(-1, (len(self.prefixStack)+1) * -1, -1):
+ if prefix in stack[i]:
+ return True
+ return False
+
+ def serialize(self, elem, closeElement=1, defaultUri=''):
+ # Optimization shortcuts
+ write = self.writelist.append
+
+ # Shortcut, check to see if elem is actually a chunk o' serialized XML
+ if isinstance(elem, SerializedXML):
+ write(elem)
+ return
+
+ # Shortcut, check to see if elem is actually a string (aka Cdata)
+ if isinstance(elem, types.StringTypes):
+ write(escapeToXml(elem))
+ return
+
+ # Further optimizations
+ name = elem.name
+ uri = elem.uri
+ defaultUri, currentDefaultUri = elem.defaultUri, defaultUri
+
+ for p, u in elem.localPrefixes.iteritems():
+ self.prefixes[u] = p
+ self.prefixStack.append(elem.localPrefixes.keys())
+
+ # Inherit the default namespace
+ if defaultUri is None:
+ defaultUri = currentDefaultUri
+
+ if uri is None:
+ uri = defaultUri
+
+ prefix = None
+ if uri != defaultUri or uri in self.prefixes:
+ prefix = self.getPrefix(uri)
+ inScope = self.prefixInScope(prefix)
+
+ # Create the starttag
+
+ if not prefix:
+ write("<%s" % (name))
+ else:
+ write("<%s:%s" % (prefix, name))
+
+ if not inScope:
+ write(" xmlns:%s='%s'" % (prefix, uri))
+ self.prefixStack[-1].append(prefix)
+ inScope = True
+
+ if defaultUri != currentDefaultUri and \
+ (uri != defaultUri or not prefix or not inScope):
+ write(" xmlns='%s'" % (defaultUri))
+
+ for p, u in elem.localPrefixes.iteritems():
+ write(" xmlns:%s='%s'" % (p, u))
+
+ # Serialize attributes
+ for k,v in elem.attributes.items():
+ # If the attribute name is a tuple, it's a qualified attribute
+ if isinstance(k, types.TupleType):
+ attr_uri, attr_name = k
+ attr_prefix = self.getPrefix(attr_uri)
+
+ if not self.prefixInScope(attr_prefix):
+ write(" xmlns:%s='%s'" % (attr_prefix, attr_uri))
+ self.prefixStack[-1].append(attr_prefix)
+
+ write(" %s:%s='%s'" % (attr_prefix, attr_name,
+ escapeToXml(v, 1)))
+ else:
+ write((" %s='%s'" % ( k, escapeToXml(v, 1))))
+
+ # Shortcut out if this is only going to return
+ # the element (i.e. no children)
+ if closeElement == 0:
+ write(">")
+ return
+
+ # Serialize children
+ if len(elem.children) > 0:
+ write(">")
+ for c in elem.children:
+ self.serialize(c, defaultUri=defaultUri)
+ # Add closing tag
+ if not prefix:
+ write("</%s>" % (name))
+ else:
+ write("</%s:%s>" % (prefix, name))
+ else:
+ write("/>")
+
+ self.prefixStack.pop()
+
+
+SerializerClass = _ListSerializer
+
+def escapeToXml(text, isattrib = 0):
+ """ Escape text to proper XML form, per section 2.3 in the XML specification.
+
+ @type text: C{str}
+ @param text: Text to escape
+
+ @type isattrib: C{bool}
+ @param isattrib: Triggers escaping of characters necessary for use as
+ attribute values
+ """
+ text = text.replace("&", "&amp;")
+ text = text.replace("<", "&lt;")
+ text = text.replace(">", "&gt;")
+ if isattrib == 1:
+ text = text.replace("'", "&apos;")
+ text = text.replace("\"", "&quot;")
+ return text
+
+def unescapeFromXml(text):
+ text = text.replace("&lt;", "<")
+ text = text.replace("&gt;", ">")
+ text = text.replace("&apos;", "'")
+ text = text.replace("&quot;", "\"")
+ text = text.replace("&amp;", "&")
+ return text
+
+def generateOnlyInterface(list, int):
+ """ Filters items in a list by class
+ """
+ for n in list:
+ if int.providedBy(n):
+ yield n
+
+def generateElementsQNamed(list, name, uri):
+ """ Filters Element items in a list with matching name and URI. """
+ for n in list:
+ if IElement.providedBy(n) and n.name == name and n.uri == uri:
+ yield n
+
+def generateElementsNamed(list, name):
+ """ Filters Element items in a list with matching name, regardless of URI.
+ """
+ for n in list:
+ if IElement.providedBy(n) and n.name == name:
+ yield n
+
+
+class SerializedXML(unicode):
+ """ Marker class for pre-serialized XML in the DOM. """
+ pass
+
+
+class Namespace:
+ """ Convenience object for tracking namespace declarations. """
+ def __init__(self, uri):
+ self._uri = uri
+ def __getattr__(self, n):
+ return (self._uri, n)
+ def __getitem__(self, n):
+ return (self._uri, n)
+
+class IElement(Interface):
+ """
+ Interface to XML element nodes.
+
+ See L{Element} for a detailed example of its general use.
+
+ Warning: this Interface is not yet complete!
+ """
+
+ uri = Attribute(""" Element's namespace URI """)
+ name = Attribute(""" Element's local name """)
+ defaultUri = Attribute(""" Default namespace URI of child elements """)
+ attributes = Attribute(""" Dictionary of element attributes """)
+ children = Attribute(""" List of child nodes """)
+ parent = Attribute(""" Reference to element's parent element """)
+ localPrefixes = Attribute(""" Dictionary of local prefixes """)
+
+ def toXml(prefixes=None, closeElement=1, defaultUri='',
+ prefixesInScope=None):
+ """ Serializes object to a (partial) XML document
+
+ @param prefixes: dictionary that maps namespace URIs to suggested
+ prefix names.
+ @type prefixes: L{dict}
+ @param closeElement: flag that determines whether to include the
+ closing tag of the element in the serialized
+ string. A value of C{0} only generates the
+ element's start tag. A value of C{1} yields a
+ complete serialization.
+ @type closeElement: C{int}
+ @param defaultUri: Initial default namespace URI. This is most useful
+ for partial rendering, where the logical parent
+ element (of which the starttag was already
+ serialized) declares a default namespace that should
+ be inherited.
+ @type defaultUri: C{str}
+ @param prefixesInScope: list of prefixes that are assumed to be
+ declared by ancestors.
+ @type prefixesInScope: C{list}
+ @return: (partial) serialized XML
+ @rtype: C{unicode}
+ """
+
+ def addElement(name, defaultUri = None, content = None):
+ """ Create an element and add as child.
+
+ The new element is added to this element as a child, and will have
+ this element as its parent.
+
+ @param name: element name. This can be either a C{unicode} object that
+ contains the local name, or a tuple of (uri, local_name)
+ for a fully qualified name. In the former case,
+ the namespace URI is inherited from this element.
+ @type name: C{unicode} or C{tuple} of (C{unicode}, C{unicode})
+ @param defaultUri: default namespace URI for child elements. If
+ C{None}, this is inherited from this element.
+ @type defaultUri: C{unicode}
+ @param content: text contained by the new element.
+ @type content: C{unicode}
+ @return: the created element
+ @rtype: object providing L{IElement}
+ """
+
+ def addChild(node):
+ """ Adds a node as child of this element.
+
+ The C{node} will be added to the list of childs of this element, and
+ will have this element set as its parent when C{node} provides
+ L{IElement}.
+
+ @param node: the child node.
+ @type node: C{unicode} or object implementing L{IElement}
+ """
+
+class Element(object):
+ """ Represents an XML element node.
+
+ An Element contains a series of attributes (name/value pairs), content
+ (character data), and other child Element objects. When building a document
+ with markup (such as HTML or XML), use this object as the starting point.
+
+ Element objects fully support XML Namespaces. The fully qualified name of
+ the XML Element it represents is stored in the C{uri} and C{name}
+ attributes, where C{uri} holds the namespace URI. There is also a default
+ namespace, for child elements. This is stored in the C{defaultUri}
+ attribute. Note that C{''} means the empty namespace.
+
+ Serialization of Elements through C{toXml()} will use these attributes
+ for generating proper serialized XML. When both C{uri} and C{defaultUri}
+ are not None in the Element and all of its descendents, serialization
+ proceeds as expected:
+
+ >>> from twisted.words.xish import domish
+ >>> root = domish.Element(('myns', 'root'))
+ >>> root.addElement('child', content='test')
+ <twisted.words.xish.domish.Element object at 0x83002ac>
+ >>> root.toXml()
+ u"<root xmlns='myns'><child>test</child></root>"
+
+ For partial serialization, needed for streaming XML, a special value for
+ namespace URIs can be used: C{None}.
+
+ Using C{None} as the value for C{uri} means: this element is in whatever
+ namespace inherited by the closest logical ancestor when the complete XML
+ document has been serialized. The serialized start tag will have a
+ non-prefixed name, and no xmlns declaration will be generated.
+
+ Similarly, C{None} for C{defaultUri} means: the default namespace for my
+ child elements is inherited from the logical ancestors of this element,
+ when the complete XML document has been serialized.
+
+ To illustrate, an example from a Jabber stream. Assume the start tag of the
+ root element of the stream has already been serialized, along with several
+ complete child elements, and sent off, looking like this::
+
+ <stream:stream xmlns:stream='http://etherx.jabber.org/streams'
+ xmlns='jabber:client' to='example.com'>
+ ...
+
+ Now suppose we want to send a complete element represented by an
+ object C{message} created like:
+
+ >>> message = domish.Element((None, 'message'))
+ >>> message['to'] = 'user@example.com'
+ >>> message.addElement('body', content='Hi!')
+ <twisted.words.xish.domish.Element object at 0x8276e8c>
+ >>> message.toXml()
+ u"<message to='user@example.com'><body>Hi!</body></message>"
+
+ As, you can see, this XML snippet has no xmlns declaration. When sent
+ off, it inherits the C{jabber:client} namespace from the root element.
+ Note that this renders the same as using C{''} instead of C{None}:
+
+ >>> presence = domish.Element(('', 'presence'))
+ >>> presence.toXml()
+ u"<presence/>"
+
+ However, if this object has a parent defined, the difference becomes
+ clear:
+
+ >>> child = message.addElement(('http://example.com/', 'envelope'))
+ >>> child.addChild(presence)
+ <twisted.words.xish.domish.Element object at 0x8276fac>
+ >>> message.toXml()
+ u"<message to='user@example.com'><body>Hi!</body><envelope xmlns='http://example.com/'><presence xmlns=''/></envelope></message>"
+
+ As, you can see, the <presence/> element is now in the empty namespace, not
+ in the default namespace of the parent or the streams'.
+
+ @type uri: C{unicode} or None
+ @ivar uri: URI of this Element's name
+
+ @type name: C{unicode}
+ @ivar name: Name of this Element
+
+ @type defaultUri: C{unicode} or None
+ @ivar defaultUri: URI this Element exists within
+
+ @type children: C{list}
+ @ivar children: List of child Elements and content
+
+ @type parent: L{Element}
+ @ivar parent: Reference to the parent Element, if any.
+
+ @type attributes: L{dict}
+ @ivar attributes: Dictionary of attributes associated with this Element.
+
+ @type localPrefixes: L{dict}
+ @ivar localPrefixes: Dictionary of namespace declarations on this
+ element. The key is the prefix to bind the
+ namespace uri to.
+ """
+
+ implements(IElement)
+
+ _idCounter = 0
+
+ def __init__(self, qname, defaultUri=None, attribs=None,
+ localPrefixes=None):
+ """
+ @param qname: Tuple of (uri, name)
+ @param defaultUri: The default URI of the element; defaults to the URI
+ specified in C{qname}
+ @param attribs: Dictionary of attributes
+ @param localPrefixes: Dictionary of namespace declarations on this
+ element. The key is the prefix to bind the
+ namespace uri to.
+ """
+ self.localPrefixes = localPrefixes or {}
+ self.uri, self.name = qname
+ if defaultUri is None and \
+ self.uri not in self.localPrefixes.itervalues():
+ self.defaultUri = self.uri
+ else:
+ self.defaultUri = defaultUri
+ self.attributes = attribs or {}
+ self.children = []
+ self.parent = None
+
+ def __getattr__(self, key):
+ # Check child list for first Element with a name matching the key
+ for n in self.children:
+ if IElement.providedBy(n) and n.name == key:
+ return n
+
+ # Tweak the behaviour so that it's more friendly about not
+ # finding elements -- we need to document this somewhere :)
+ if key.startswith('_'):
+ raise AttributeError(key)
+ else:
+ return None
+
+ def __getitem__(self, key):
+ return self.attributes[self._dqa(key)]
+
+ def __delitem__(self, key):
+ del self.attributes[self._dqa(key)];
+
+ def __setitem__(self, key, value):
+ self.attributes[self._dqa(key)] = value
+
+ def __str__(self):
+ """ Retrieve the first CData (content) node
+ """
+ for n in self.children:
+ if isinstance(n, types.StringTypes): return n
+ return ""
+
+ def _dqa(self, attr):
+ """ Dequalify an attribute key as needed """
+ if isinstance(attr, types.TupleType) and not attr[0]:
+ return attr[1]
+ else:
+ return attr
+
+ def getAttribute(self, attribname, default = None):
+ """ Retrieve the value of attribname, if it exists """
+ return self.attributes.get(attribname, default)
+
+ def hasAttribute(self, attrib):
+ """ Determine if the specified attribute exists """
+ return self.attributes.has_key(self._dqa(attrib))
+
+ def compareAttribute(self, attrib, value):
+ """ Safely compare the value of an attribute against a provided value.
+
+ C{None}-safe.
+ """
+ return self.attributes.get(self._dqa(attrib), None) == value
+
+ def swapAttributeValues(self, left, right):
+ """ Swap the values of two attribute. """
+ d = self.attributes
+ l = d[left]
+ d[left] = d[right]
+ d[right] = l
+
+ def addChild(self, node):
+ """ Add a child to this Element. """
+ if IElement.providedBy(node):
+ node.parent = self
+ self.children.append(node)
+ return self.children[-1]
+
+ def addContent(self, text):
+ """ Add some text data to this Element. """
+ c = self.children
+ if len(c) > 0 and isinstance(c[-1], types.StringTypes):
+ c[-1] = c[-1] + text
+ else:
+ c.append(text)
+ return c[-1]
+
+ def addElement(self, name, defaultUri = None, content = None):
+ result = None
+ if isinstance(name, type(())):
+ if defaultUri is None:
+ defaultUri = name[0]
+ self.children.append(Element(name, defaultUri))
+ else:
+ if defaultUri is None:
+ defaultUri = self.defaultUri
+ self.children.append(Element((defaultUri, name), defaultUri))
+
+ result = self.children[-1]
+ result.parent = self
+
+ if content:
+ result.children.append(content)
+
+ return result
+
+ def addRawXml(self, rawxmlstring):
+ """ Add a pre-serialized chunk o' XML as a child of this Element. """
+ self.children.append(SerializedXML(rawxmlstring))
+
+ def addUniqueId(self):
+ """ Add a unique (across a given Python session) id attribute to this
+ Element.
+ """
+ self.attributes["id"] = "H_%d" % Element._idCounter
+ Element._idCounter = Element._idCounter + 1
+
+
+ def elements(self, uri=None, name=None):
+ """
+ Iterate across all children of this Element that are Elements.
+
+ Returns a generator over the child elements. If both the C{uri} and
+ C{name} parameters are set, the returned generator will only yield
+ on elements matching the qualified name.
+
+ @param uri: Optional element URI.
+ @type uri: C{unicode}
+ @param name: Optional element name.
+ @type name: C{unicode}
+ @return: Iterator that yields objects implementing L{IElement}.
+ """
+ if name is None:
+ return generateOnlyInterface(self.children, IElement)
+ else:
+ return generateElementsQNamed(self.children, name, uri)
+
+
+ def toXml(self, prefixes=None, closeElement=1, defaultUri='',
+ prefixesInScope=None):
+ """ Serialize this Element and all children to a string. """
+ s = SerializerClass(prefixes=prefixes, prefixesInScope=prefixesInScope)
+ s.serialize(self, closeElement=closeElement, defaultUri=defaultUri)
+ return s.getValue()
+
+ def firstChildElement(self):
+ for c in self.children:
+ if IElement.providedBy(c):
+ return c
+ return None
+
+
+class ParserError(Exception):
+ """ Exception thrown when a parsing error occurs """
+ pass
+
+def elementStream():
+ """ Preferred method to construct an ElementStream
+
+ Uses Expat-based stream if available, and falls back to Sux if necessary.
+ """
+ try:
+ es = ExpatElementStream()
+ return es
+ except ImportError:
+ if SuxElementStream is None:
+ raise Exception("No parsers available :(")
+ es = SuxElementStream()
+ return es
+
+try:
+ from twisted.web import sux
+except:
+ SuxElementStream = None
+else:
+ class SuxElementStream(sux.XMLParser):
+ def __init__(self):
+ self.connectionMade()
+ self.DocumentStartEvent = None
+ self.ElementEvent = None
+ self.DocumentEndEvent = None
+ self.currElem = None
+ self.rootElem = None
+ self.documentStarted = False
+ self.defaultNsStack = []
+ self.prefixStack = []
+
+ def parse(self, buffer):
+ try:
+ self.dataReceived(buffer)
+ except sux.ParseError, e:
+ raise ParserError, str(e)
+
+
+ def findUri(self, prefix):
+ # Walk prefix stack backwards, looking for the uri
+ # matching the specified prefix
+ stack = self.prefixStack
+ for i in range(-1, (len(self.prefixStack)+1) * -1, -1):
+ if prefix in stack[i]:
+ return stack[i][prefix]
+ return None
+
+ def gotTagStart(self, name, attributes):
+ defaultUri = None
+ localPrefixes = {}
+ attribs = {}
+ uri = None
+
+ # Pass 1 - Identify namespace decls
+ for k, v in attributes.items():
+ if k.startswith("xmlns"):
+ x, p = _splitPrefix(k)
+ if (x is None): # I.e. default declaration
+ defaultUri = v
+ else:
+ localPrefixes[p] = v
+ del attributes[k]
+
+ # Push namespace decls onto prefix stack
+ self.prefixStack.append(localPrefixes)
+
+ # Determine default namespace for this element; if there
+ # is one
+ if defaultUri is None:
+ if len(self.defaultNsStack) > 0:
+ defaultUri = self.defaultNsStack[-1]
+ else:
+ defaultUri = ''
+
+ # Fix up name
+ prefix, name = _splitPrefix(name)
+ if prefix is None: # This element is in the default namespace
+ uri = defaultUri
+ else:
+ # Find the URI for the prefix
+ uri = self.findUri(prefix)
+
+ # Pass 2 - Fix up and escape attributes
+ for k, v in attributes.items():
+ p, n = _splitPrefix(k)
+ if p is None:
+ attribs[n] = v
+ else:
+ attribs[(self.findUri(p)), n] = unescapeFromXml(v)
+
+ # Construct the actual Element object
+ e = Element((uri, name), defaultUri, attribs, localPrefixes)
+
+ # Save current default namespace
+ self.defaultNsStack.append(defaultUri)
+
+ # Document already started
+ if self.documentStarted:
+ # Starting a new packet
+ if self.currElem is None:
+ self.currElem = e
+ # Adding to existing element
+ else:
+ self.currElem = self.currElem.addChild(e)
+ # New document
+ else:
+ self.rootElem = e
+ self.documentStarted = True
+ self.DocumentStartEvent(e)
+
+ def gotText(self, data):
+ if self.currElem != None:
+ self.currElem.addContent(data)
+
+ def gotCData(self, data):
+ if self.currElem != None:
+ self.currElem.addContent(data)
+
+ def gotComment(self, data):
+ # Ignore comments for the moment
+ pass
+
+ entities = { "amp" : "&",
+ "lt" : "<",
+ "gt" : ">",
+ "apos": "'",
+ "quot": "\"" }
+
+ def gotEntityReference(self, entityRef):
+ # If this is an entity we know about, add it as content
+ # to the current element
+ if entityRef in SuxElementStream.entities:
+ self.currElem.addContent(SuxElementStream.entities[entityRef])
+
+ def gotTagEnd(self, name):
+ # Ensure the document hasn't already ended
+ if self.rootElem is None:
+ # XXX: Write more legible explanation
+ raise ParserError, "Element closed after end of document."
+
+ # Fix up name
+ prefix, name = _splitPrefix(name)
+ if prefix is None:
+ uri = self.defaultNsStack[-1]
+ else:
+ uri = self.findUri(prefix)
+
+ # End of document
+ if self.currElem is None:
+ # Ensure element name and uri matches
+ if self.rootElem.name != name or self.rootElem.uri != uri:
+ raise ParserError, "Mismatched root elements"
+ self.DocumentEndEvent()
+ self.rootElem = None
+
+ # Other elements
+ else:
+ # Ensure the tag being closed matches the name of the current
+ # element
+ if self.currElem.name != name or self.currElem.uri != uri:
+ # XXX: Write more legible explanation
+ raise ParserError, "Malformed element close"
+
+ # Pop prefix and default NS stack
+ self.prefixStack.pop()
+ self.defaultNsStack.pop()
+
+ # Check for parent null parent of current elem;
+ # that's the top of the stack
+ if self.currElem.parent is None:
+ self.currElem.parent = self.rootElem
+ self.ElementEvent(self.currElem)
+ self.currElem = None
+
+ # Anything else is just some element wrapping up
+ else:
+ self.currElem = self.currElem.parent
+
+
+class ExpatElementStream:
+ def __init__(self):
+ import pyexpat
+ self.DocumentStartEvent = None
+ self.ElementEvent = None
+ self.DocumentEndEvent = None
+ self.error = pyexpat.error
+ self.parser = pyexpat.ParserCreate("UTF-8", " ")
+ self.parser.StartElementHandler = self._onStartElement
+ self.parser.EndElementHandler = self._onEndElement
+ self.parser.CharacterDataHandler = self._onCdata
+ self.parser.StartNamespaceDeclHandler = self._onStartNamespace
+ self.parser.EndNamespaceDeclHandler = self._onEndNamespace
+ self.currElem = None
+ self.defaultNsStack = ['']
+ self.documentStarted = 0
+ self.localPrefixes = {}
+
+ def parse(self, buffer):
+ try:
+ self.parser.Parse(buffer)
+ except self.error, e:
+ raise ParserError, str(e)
+
+ def _onStartElement(self, name, attrs):
+ # Generate a qname tuple from the provided name. See
+ # http://docs.python.org/library/pyexpat.html#xml.parsers.expat.ParserCreate
+ # for an explanation of the formatting of name.
+ qname = name.rsplit(" ", 1)
+ if len(qname) == 1:
+ qname = ('', name)
+
+ # Process attributes
+ for k, v in attrs.items():
+ if " " in k:
+ aqname = k.rsplit(" ", 1)
+ attrs[(aqname[0], aqname[1])] = v
+ del attrs[k]
+
+ # Construct the new element
+ e = Element(qname, self.defaultNsStack[-1], attrs, self.localPrefixes)
+ self.localPrefixes = {}
+
+ # Document already started
+ if self.documentStarted == 1:
+ if self.currElem != None:
+ self.currElem.children.append(e)
+ e.parent = self.currElem
+ self.currElem = e
+
+ # New document
+ else:
+ self.documentStarted = 1
+ self.DocumentStartEvent(e)
+
+ def _onEndElement(self, _):
+ # Check for null current elem; end of doc
+ if self.currElem is None:
+ self.DocumentEndEvent()
+
+ # Check for parent that is None; that's
+ # the top of the stack
+ elif self.currElem.parent is None:
+ self.ElementEvent(self.currElem)
+ self.currElem = None
+
+ # Anything else is just some element in the current
+ # packet wrapping up
+ else:
+ self.currElem = self.currElem.parent
+
+ def _onCdata(self, data):
+ if self.currElem != None:
+ self.currElem.addContent(data)
+
+ def _onStartNamespace(self, prefix, uri):
+ # If this is the default namespace, put
+ # it on the stack
+ if prefix is None:
+ self.defaultNsStack.append(uri)
+ else:
+ self.localPrefixes[prefix] = uri
+
+ def _onEndNamespace(self, prefix):
+ # Remove last element on the stack
+ if prefix is None:
+ self.defaultNsStack.pop()
+
+## class FileParser(ElementStream):
+## def __init__(self):
+## ElementStream.__init__(self)
+## self.DocumentStartEvent = self.docStart
+## self.ElementEvent = self.elem
+## self.DocumentEndEvent = self.docEnd
+## self.done = 0
+
+## def docStart(self, elem):
+## self.document = elem
+
+## def elem(self, elem):
+## self.document.addChild(elem)
+
+## def docEnd(self):
+## self.done = 1
+
+## def parse(self, filename):
+## for l in open(filename).readlines():
+## self.parser.Parse(l)
+## assert self.done == 1
+## return self.document
+
+## def parseFile(filename):
+## return FileParser().parse(filename)
+
+
diff --git a/twisted/words/xish/utility.py b/twisted/words/xish/utility.py
new file mode 100644
index 0000000..5c54095
--- /dev/null
+++ b/twisted/words/xish/utility.py
@@ -0,0 +1,372 @@
+# -*- test-case-name: twisted.words.test.test_xishutil -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Event Dispatching and Callback utilities.
+"""
+
+from twisted.python import log
+from twisted.words.xish import xpath
+
+class _MethodWrapper(object):
+ """
+ Internal class for tracking method calls.
+ """
+ def __init__(self, method, *args, **kwargs):
+ self.method = method
+ self.args = args
+ self.kwargs = kwargs
+
+
+ def __call__(self, *args, **kwargs):
+ nargs = self.args + args
+ nkwargs = self.kwargs.copy()
+ nkwargs.update(kwargs)
+ self.method(*nargs, **nkwargs)
+
+
+
+class CallbackList:
+ """
+ Container for callbacks.
+
+ Event queries are linked to lists of callables. When a matching event
+ occurs, these callables are called in sequence. One-time callbacks
+ are removed from the list after the first time the event was triggered.
+
+ Arguments to callbacks are split spread across two sets. The first set,
+ callback specific, is passed to C{addCallback} and is used for all
+ subsequent event triggers. The second set is passed to C{callback} and is
+ event specific. Positional arguments in the second set come after the
+ positional arguments of the first set. Keyword arguments in the second set
+ override those in the first set.
+
+ @ivar callbacks: The registered callbacks as mapping from the callable to a
+ tuple of a wrapper for that callable that keeps the
+ callback specific arguments and a boolean that signifies
+ if it is to be called only once.
+ @type callbacks: C{dict}
+ """
+
+ def __init__(self):
+ self.callbacks = {}
+
+
+ def addCallback(self, onetime, method, *args, **kwargs):
+ """
+ Add callback.
+
+ The arguments passed are used as callback specific arguments.
+
+ @param onetime: If C{True}, this callback is called at most once.
+ @type onetime: C{bool}
+ @param method: The callback callable to be added.
+ @param args: Positional arguments to the callable.
+ @type args: C{list}
+ @param kwargs: Keyword arguments to the callable.
+ @type kwargs: C{dict}
+ """
+
+ if not method in self.callbacks:
+ self.callbacks[method] = (_MethodWrapper(method, *args, **kwargs),
+ onetime)
+
+
+ def removeCallback(self, method):
+ """
+ Remove callback.
+
+ @param method: The callable to be removed.
+ """
+
+ if method in self.callbacks:
+ del self.callbacks[method]
+
+
+ def callback(self, *args, **kwargs):
+ """
+ Call all registered callbacks.
+
+ The passed arguments are event specific and augment and override
+ the callback specific arguments as described above.
+
+ @note: Exceptions raised by callbacks are trapped and logged. They will
+ not propagate up to make sure other callbacks will still be
+ called, and the event dispatching always succeeds.
+
+ @param args: Positional arguments to the callable.
+ @type args: C{list}
+ @param kwargs: Keyword arguments to the callable.
+ @type kwargs: C{dict}
+ """
+
+ for key, (methodwrapper, onetime) in self.callbacks.items():
+ try:
+ methodwrapper(*args, **kwargs)
+ except:
+ log.err()
+
+ if onetime:
+ del self.callbacks[key]
+
+
+ def isEmpty(self):
+ """
+ Return if list of registered callbacks is empty.
+
+ @rtype: C{bool}
+ """
+
+ return len(self.callbacks) == 0
+
+
+
+class EventDispatcher:
+ """
+ Event dispatching service.
+
+ The C{EventDispatcher} allows observers to be registered for certain events
+ that are dispatched. There are two types of events: XPath events and Named
+ events.
+
+ Every dispatch is triggered by calling L{dispatch} with a data object and,
+ for named events, the name of the event.
+
+ When an XPath type event is dispatched, the associated object is assumed to
+ be an L{Element<twisted.words.xish.domish.Element>} instance, which is
+ matched against all registered XPath queries. For every match, the
+ respective observer will be called with the data object.
+
+ A named event will simply call each registered observer for that particular
+ event name, with the data object. Unlike XPath type events, the data object
+ is not restricted to L{Element<twisted.words.xish.domish.Element>}, but can
+ be anything.
+
+ When registering observers, the event that is to be observed is specified
+ using an L{xpath.XPathQuery} instance or a string. In the latter case, the
+ string can also contain the string representation of an XPath expression.
+ To distinguish these from named events, each named event should start with
+ a special prefix that is stored in C{self.prefix}. It defaults to
+ C{//event/}.
+
+ Observers registered using L{addObserver} are persistent: after the
+ observer has been triggered by a dispatch, it remains registered for a
+ possible next dispatch. If instead L{addOnetimeObserver} was used to
+ observe an event, the observer is removed from the list of observers after
+ the first observed event.
+
+ Observers can also be prioritized, by providing an optional C{priority}
+ parameter to the L{addObserver} and L{addOnetimeObserver} methods. Higher
+ priority observers are then called before lower priority observers.
+
+ Finally, observers can be unregistered by using L{removeObserver}.
+ """
+
+ def __init__(self, eventprefix="//event/"):
+ self.prefix = eventprefix
+ self._eventObservers = {}
+ self._xpathObservers = {}
+ self._dispatchDepth = 0 # Flag indicating levels of dispatching
+ # in progress
+ self._updateQueue = [] # Queued updates for observer ops
+
+
+ def _getEventAndObservers(self, event):
+ if isinstance(event, xpath.XPathQuery):
+ # Treat as xpath
+ observers = self._xpathObservers
+ else:
+ if self.prefix == event[:len(self.prefix)]:
+ # Treat as event
+ observers = self._eventObservers
+ else:
+ # Treat as xpath
+ event = xpath.internQuery(event)
+ observers = self._xpathObservers
+
+ return event, observers
+
+
+ def addOnetimeObserver(self, event, observerfn, priority=0, *args, **kwargs):
+ """
+ Register a one-time observer for an event.
+
+ Like L{addObserver}, but is only triggered at most once. See there
+ for a description of the parameters.
+ """
+ self._addObserver(True, event, observerfn, priority, *args, **kwargs)
+
+
+ def addObserver(self, event, observerfn, priority=0, *args, **kwargs):
+ """
+ Register an observer for an event.
+
+ Each observer will be registered with a certain priority. Higher
+ priority observers get called before lower priority observers.
+
+ @param event: Name or XPath query for the event to be monitored.
+ @type event: C{str} or L{xpath.XPathQuery}.
+ @param observerfn: Function to be called when the specified event
+ has been triggered. This callable takes
+ one parameter: the data object that triggered
+ the event. When specified, the C{*args} and
+ C{**kwargs} parameters to addObserver are being used
+ as additional parameters to the registered observer
+ callable.
+ @param priority: (Optional) priority of this observer in relation to
+ other observer that match the same event. Defaults to
+ C{0}.
+ @type priority: C{int}
+ """
+ self._addObserver(False, event, observerfn, priority, *args, **kwargs)
+
+
+ def _addObserver(self, onetime, event, observerfn, priority, *args, **kwargs):
+ # If this is happening in the middle of the dispatch, queue
+ # it up for processing after the dispatch completes
+ if self._dispatchDepth > 0:
+ self._updateQueue.append(lambda:self._addObserver(onetime, event, observerfn, priority, *args, **kwargs))
+ return
+
+ event, observers = self._getEventAndObservers(event)
+
+ if priority not in observers:
+ cbl = CallbackList()
+ observers[priority] = {event: cbl}
+ else:
+ priorityObservers = observers[priority]
+ if event not in priorityObservers:
+ cbl = CallbackList()
+ observers[priority][event] = cbl
+ else:
+ cbl = priorityObservers[event]
+
+ cbl.addCallback(onetime, observerfn, *args, **kwargs)
+
+
+ def removeObserver(self, event, observerfn):
+ """
+ Remove callable as observer for an event.
+
+ The observer callable is removed for all priority levels for the
+ specified event.
+
+ @param event: Event for which the observer callable was registered.
+ @type event: C{str} or L{xpath.XPathQuery}
+ @param observerfn: Observer callable to be unregistered.
+ """
+
+ # If this is happening in the middle of the dispatch, queue
+ # it up for processing after the dispatch completes
+ if self._dispatchDepth > 0:
+ self._updateQueue.append(lambda:self.removeObserver(event, observerfn))
+ return
+
+ event, observers = self._getEventAndObservers(event)
+
+ emptyLists = []
+ for priority, priorityObservers in observers.iteritems():
+ for query, callbacklist in priorityObservers.iteritems():
+ if event == query:
+ callbacklist.removeCallback(observerfn)
+ if callbacklist.isEmpty():
+ emptyLists.append((priority, query))
+
+ for priority, query in emptyLists:
+ del observers[priority][query]
+
+
+ def dispatch(self, obj, event=None):
+ """
+ Dispatch an event.
+
+ When C{event} is C{None}, an XPath type event is triggered, and
+ C{obj} is assumed to be an instance of
+ L{Element<twisted.words.xish.domish.Element>}. Otherwise, C{event}
+ holds the name of the named event being triggered. In the latter case,
+ C{obj} can be anything.
+
+ @param obj: The object to be dispatched.
+ @param event: Optional event name.
+ @type event: C{str}
+ """
+
+ foundTarget = False
+
+ self._dispatchDepth += 1
+
+ if event != None:
+ # Named event
+ observers = self._eventObservers
+ match = lambda query, obj: query == event
+ else:
+ # XPath event
+ observers = self._xpathObservers
+ match = lambda query, obj: query.matches(obj)
+
+ priorities = observers.keys()
+ priorities.sort()
+ priorities.reverse()
+
+ emptyLists = []
+ for priority in priorities:
+ for query, callbacklist in observers[priority].iteritems():
+ if match(query, obj):
+ callbacklist.callback(obj)
+ foundTarget = True
+ if callbacklist.isEmpty():
+ emptyLists.append((priority, query))
+
+ for priority, query in emptyLists:
+ del observers[priority][query]
+
+ self._dispatchDepth -= 1
+
+ # If this is a dispatch within a dispatch, don't
+ # do anything with the updateQueue -- it needs to
+ # wait until we've back all the way out of the stack
+ if self._dispatchDepth == 0:
+ # Deal with pending update operations
+ for f in self._updateQueue:
+ f()
+ self._updateQueue = []
+
+ return foundTarget
+
+
+
+class XmlPipe(object):
+ """
+ XML stream pipe.
+
+ Connects two objects that communicate stanzas through an XML stream like
+ interface. Each of the ends of the pipe (sink and source) can be used to
+ send XML stanzas to the other side, or add observers to process XML stanzas
+ that were sent from the other side.
+
+ XML pipes are usually used in place of regular XML streams that are
+ transported over TCP. This is the reason for the use of the names source
+ and sink for both ends of the pipe. The source side corresponds with the
+ entity that initiated the TCP connection, whereas the sink corresponds with
+ the entity that accepts that connection. In this object, though, the source
+ and sink are treated equally.
+
+ Unlike Jabber
+ L{XmlStream<twisted.words.protocols.jabber.xmlstream.XmlStream>}s, the sink
+ and source objects are assumed to represent an eternal connected and
+ initialized XML stream. As such, events corresponding to connection,
+ disconnection, initialization and stream errors are not dispatched or
+ processed.
+
+ @since: 8.2
+ @ivar source: Source XML stream.
+ @ivar sink: Sink XML stream.
+ """
+
+ def __init__(self):
+ self.source = EventDispatcher()
+ self.sink = EventDispatcher()
+ self.source.send = lambda obj: self.sink.dispatch(obj)
+ self.sink.send = lambda obj: self.source.dispatch(obj)
diff --git a/twisted/words/xish/xmlstream.py b/twisted/words/xish/xmlstream.py
new file mode 100644
index 0000000..3018a0e
--- /dev/null
+++ b/twisted/words/xish/xmlstream.py
@@ -0,0 +1,261 @@
+# -*- test-case-name: twisted.words.test.test_xmlstream -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+XML Stream processing.
+
+An XML Stream is defined as a connection over which two XML documents are
+exchanged during the lifetime of the connection, one for each direction. The
+unit of interaction is a direct child element of the root element (stanza).
+
+The most prominent use of XML Streams is Jabber, but this module is generically
+usable. See Twisted Words for Jabber specific protocol support.
+
+Maintainer: Ralph Meijer
+"""
+
+from twisted.python import failure
+from twisted.internet import protocol
+from twisted.words.xish import domish, utility
+
+STREAM_CONNECTED_EVENT = intern("//event/stream/connected")
+STREAM_START_EVENT = intern("//event/stream/start")
+STREAM_END_EVENT = intern("//event/stream/end")
+STREAM_ERROR_EVENT = intern("//event/stream/error")
+
+class XmlStream(protocol.Protocol, utility.EventDispatcher):
+ """ Generic Streaming XML protocol handler.
+
+ This protocol handler will parse incoming data as XML and dispatch events
+ accordingly. Incoming stanzas can be handled by registering observers using
+ XPath-like expressions that are matched against each stanza. See
+ L{utility.EventDispatcher} for details.
+ """
+ def __init__(self):
+ utility.EventDispatcher.__init__(self)
+ self.stream = None
+ self.rawDataOutFn = None
+ self.rawDataInFn = None
+
+ def _initializeStream(self):
+ """ Sets up XML Parser. """
+ self.stream = domish.elementStream()
+ self.stream.DocumentStartEvent = self.onDocumentStart
+ self.stream.ElementEvent = self.onElement
+ self.stream.DocumentEndEvent = self.onDocumentEnd
+
+ ### --------------------------------------------------------------
+ ###
+ ### Protocol events
+ ###
+ ### --------------------------------------------------------------
+
+ def connectionMade(self):
+ """ Called when a connection is made.
+
+ Sets up the XML parser and dispatches the L{STREAM_CONNECTED_EVENT}
+ event indicating the connection has been established.
+ """
+ self._initializeStream()
+ self.dispatch(self, STREAM_CONNECTED_EVENT)
+
+ def dataReceived(self, data):
+ """ Called whenever data is received.
+
+ Passes the data to the XML parser. This can result in calls to the
+ DOM handlers. If a parse error occurs, the L{STREAM_ERROR_EVENT} event
+ is called to allow for cleanup actions, followed by dropping the
+ connection.
+ """
+ try:
+ if self.rawDataInFn:
+ self.rawDataInFn(data)
+ self.stream.parse(data)
+ except domish.ParserError:
+ self.dispatch(failure.Failure(), STREAM_ERROR_EVENT)
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ """ Called when the connection is shut down.
+
+ Dispatches the L{STREAM_END_EVENT}.
+ """
+ self.dispatch(reason, STREAM_END_EVENT)
+ self.stream = None
+
+ ### --------------------------------------------------------------
+ ###
+ ### DOM events
+ ###
+ ### --------------------------------------------------------------
+
+ def onDocumentStart(self, rootElement):
+ """ Called whenever the start tag of a root element has been received.
+
+ Dispatches the L{STREAM_START_EVENT}.
+ """
+ self.dispatch(self, STREAM_START_EVENT)
+
+ def onElement(self, element):
+ """ Called whenever a direct child element of the root element has
+ been received.
+
+ Dispatches the received element.
+ """
+ self.dispatch(element)
+
+ def onDocumentEnd(self):
+ """ Called whenever the end tag of the root element has been received.
+
+ Closes the connection. This causes C{connectionLost} being called.
+ """
+ self.transport.loseConnection()
+
+ def setDispatchFn(self, fn):
+ """ Set another function to handle elements. """
+ self.stream.ElementEvent = fn
+
+ def resetDispatchFn(self):
+ """ Set the default function (C{onElement}) to handle elements. """
+ self.stream.ElementEvent = self.onElement
+
+ def send(self, obj):
+ """ Send data over the stream.
+
+ Sends the given C{obj} over the connection. C{obj} may be instances of
+ L{domish.Element}, C{unicode} and C{str}. The first two will be
+ properly serialized and/or encoded. C{str} objects must be in UTF-8
+ encoding.
+
+ Note: because it is easy to make mistakes in maintaining a properly
+ encoded C{str} object, it is advised to use C{unicode} objects
+ everywhere when dealing with XML Streams.
+
+ @param obj: Object to be sent over the stream.
+ @type obj: L{domish.Element}, L{domish} or C{str}
+
+ """
+ if domish.IElement.providedBy(obj):
+ obj = obj.toXml()
+
+ if isinstance(obj, unicode):
+ obj = obj.encode('utf-8')
+
+ if self.rawDataOutFn:
+ self.rawDataOutFn(obj)
+
+ self.transport.write(obj)
+
+
+
+class BootstrapMixin(object):
+ """
+ XmlStream factory mixin to install bootstrap event observers.
+
+ This mixin is for factories providing
+ L{IProtocolFactory<twisted.internet.interfaces.IProtocolFactory>} to make
+ sure bootstrap event observers are set up on protocols, before incoming
+ data is processed. Such protocols typically derive from
+ L{utility.EventDispatcher}, like L{XmlStream}.
+
+ You can set up bootstrap event observers using C{addBootstrap}. The
+ C{event} and C{fn} parameters correspond with the C{event} and
+ C{observerfn} arguments to L{utility.EventDispatcher.addObserver}.
+
+ @since: 8.2.
+ @ivar bootstraps: The list of registered bootstrap event observers.
+ @type bootstrap: C{list}
+ """
+
+ def __init__(self):
+ self.bootstraps = []
+
+
+ def installBootstraps(self, dispatcher):
+ """
+ Install registered bootstrap observers.
+
+ @param dispatcher: Event dispatcher to add the observers to.
+ @type dispatcher: L{utility.EventDispatcher}
+ """
+ for event, fn in self.bootstraps:
+ dispatcher.addObserver(event, fn)
+
+
+ def addBootstrap(self, event, fn):
+ """
+ Add a bootstrap event handler.
+
+ @param event: The event to register an observer for.
+ @type event: C{str} or L{xpath.XPathQuery}
+ @param fn: The observer callable to be registered.
+ """
+ self.bootstraps.append((event, fn))
+
+
+ def removeBootstrap(self, event, fn):
+ """
+ Remove a bootstrap event handler.
+
+ @param event: The event the observer is registered for.
+ @type event: C{str} or L{xpath.XPathQuery}
+ @param fn: The registered observer callable.
+ """
+ self.bootstraps.remove((event, fn))
+
+
+
+class XmlStreamFactoryMixin(BootstrapMixin):
+ """
+ XmlStream factory mixin that takes care of event handlers.
+
+ All positional and keyword arguments passed to create this factory are
+ passed on as-is to the protocol.
+
+ @ivar args: Positional arguments passed to the protocol upon instantiation.
+ @type args: C{tuple}.
+ @ivar kwargs: Keyword arguments passed to the protocol upon instantiation.
+ @type kwargs: C{dict}.
+ """
+
+ def __init__(self, *args, **kwargs):
+ BootstrapMixin.__init__(self)
+ self.args = args
+ self.kwargs = kwargs
+
+
+ def buildProtocol(self, addr):
+ """
+ Create an instance of XmlStream.
+
+ The returned instance will have bootstrap event observers registered
+ and will proceed to handle input on an incoming connection.
+ """
+ xs = self.protocol(*self.args, **self.kwargs)
+ xs.factory = self
+ self.installBootstraps(xs)
+ return xs
+
+
+
+class XmlStreamFactory(XmlStreamFactoryMixin,
+ protocol.ReconnectingClientFactory):
+ """
+ Factory for XmlStream protocol objects as a reconnection client.
+ """
+
+ protocol = XmlStream
+
+ def buildProtocol(self, addr):
+ """
+ Create a protocol instance.
+
+ Overrides L{XmlStreamFactoryMixin.buildProtocol} to work with
+ a L{ReconnectingClientFactory}. As this is called upon having an
+ connection established, we are resetting the delay for reconnection
+ attempts when the connection is lost again.
+ """
+ self.resetDelay()
+ return XmlStreamFactoryMixin.buildProtocol(self, addr)
diff --git a/twisted/words/xish/xpath.py b/twisted/words/xish/xpath.py
new file mode 100644
index 0000000..bf5b529
--- /dev/null
+++ b/twisted/words/xish/xpath.py
@@ -0,0 +1,333 @@
+# -*- test-case-name: twisted.words.test.test_xpath -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+XPath query support.
+
+This module provides L{XPathQuery} to match
+L{domish.Element<twisted.words.xish.domish.Element>} instances against
+XPath-like expressions.
+"""
+
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+class LiteralValue(str):
+ def value(self, elem):
+ return self
+
+
+class IndexValue:
+ def __init__(self, index):
+ self.index = int(index) - 1
+
+ def value(self, elem):
+ return elem.children[self.index]
+
+
+class AttribValue:
+ def __init__(self, attribname):
+ self.attribname = attribname
+ if self.attribname == "xmlns":
+ self.value = self.value_ns
+
+ def value_ns(self, elem):
+ return elem.uri
+
+ def value(self, elem):
+ if self.attribname in elem.attributes:
+ return elem.attributes[self.attribname]
+ else:
+ return None
+
+
+class CompareValue:
+ def __init__(self, lhs, op, rhs):
+ self.lhs = lhs
+ self.rhs = rhs
+ if op == "=":
+ self.value = self._compareEqual
+ else:
+ self.value = self._compareNotEqual
+
+ def _compareEqual(self, elem):
+ return self.lhs.value(elem) == self.rhs.value(elem)
+
+ def _compareNotEqual(self, elem):
+ return self.lhs.value(elem) != self.rhs.value(elem)
+
+
+class BooleanValue:
+ """
+ Provide boolean XPath expression operators.
+
+ @ivar lhs: Left hand side expression of the operator.
+ @ivar op: The operator. One of C{'and'}, C{'or'}.
+ @ivar rhs: Right hand side expression of the operator.
+ @ivar value: Reference to the method that will calculate the value of
+ this expression given an element.
+ """
+ def __init__(self, lhs, op, rhs):
+ self.lhs = lhs
+ self.rhs = rhs
+ if op == "and":
+ self.value = self._booleanAnd
+ else:
+ self.value = self._booleanOr
+
+ def _booleanAnd(self, elem):
+ """
+ Calculate boolean and of the given expressions given an element.
+
+ @param elem: The element to calculate the value of the expression from.
+ """
+ return self.lhs.value(elem) and self.rhs.value(elem)
+
+ def _booleanOr(self, elem):
+ """
+ Calculate boolean or of the given expressions given an element.
+
+ @param elem: The element to calculate the value of the expression from.
+ """
+ return self.lhs.value(elem) or self.rhs.value(elem)
+
+
+def Function(fname):
+ """
+ Internal method which selects the function object
+ """
+ klassname = "_%s_Function" % fname
+ c = globals()[klassname]()
+ return c
+
+
+class _not_Function:
+ def __init__(self):
+ self.baseValue = None
+
+ def setParams(self, baseValue):
+ self.baseValue = baseValue
+
+ def value(self, elem):
+ return not self.baseValue.value(elem)
+
+
+class _text_Function:
+ def setParams(self):
+ pass
+
+ def value(self, elem):
+ return str(elem)
+
+
+class _Location:
+ def __init__(self):
+ self.predicates = []
+ self.elementName = None
+ self.childLocation = None
+
+ def matchesPredicates(self, elem):
+ if self.elementName != None and self.elementName != elem.name:
+ return 0
+
+ for p in self.predicates:
+ if not p.value(elem):
+ return 0
+
+ return 1
+
+ def matches(self, elem):
+ if not self.matchesPredicates(elem):
+ return 0
+
+ if self.childLocation != None:
+ for c in elem.elements():
+ if self.childLocation.matches(c):
+ return 1
+ else:
+ return 1
+
+ return 0
+
+ def queryForString(self, elem, resultbuf):
+ if not self.matchesPredicates(elem):
+ return
+
+ if self.childLocation != None:
+ for c in elem.elements():
+ self.childLocation.queryForString(c, resultbuf)
+ else:
+ resultbuf.write(str(elem))
+
+ def queryForNodes(self, elem, resultlist):
+ if not self.matchesPredicates(elem):
+ return
+
+ if self.childLocation != None:
+ for c in elem.elements():
+ self.childLocation.queryForNodes(c, resultlist)
+ else:
+ resultlist.append(elem)
+
+ def queryForStringList(self, elem, resultlist):
+ if not self.matchesPredicates(elem):
+ return
+
+ if self.childLocation != None:
+ for c in elem.elements():
+ self.childLocation.queryForStringList(c, resultlist)
+ else:
+ for c in elem.children:
+ if isinstance(c, (str, unicode)):
+ resultlist.append(c)
+
+
+class _AnyLocation:
+ def __init__(self):
+ self.predicates = []
+ self.elementName = None
+ self.childLocation = None
+
+ def matchesPredicates(self, elem):
+ for p in self.predicates:
+ if not p.value(elem):
+ return 0
+ return 1
+
+ def listParents(self, elem, parentlist):
+ if elem.parent != None:
+ self.listParents(elem.parent, parentlist)
+ parentlist.append(elem.name)
+
+ def isRootMatch(self, elem):
+ if (self.elementName == None or self.elementName == elem.name) and \
+ self.matchesPredicates(elem):
+ if self.childLocation != None:
+ for c in elem.elements():
+ if self.childLocation.matches(c):
+ return True
+ else:
+ return True
+ return False
+
+ def findFirstRootMatch(self, elem):
+ if (self.elementName == None or self.elementName == elem.name) and \
+ self.matchesPredicates(elem):
+ # Thus far, the name matches and the predicates match,
+ # now check into the children and find the first one
+ # that matches the rest of the structure
+ # the rest of the structure
+ if self.childLocation != None:
+ for c in elem.elements():
+ if self.childLocation.matches(c):
+ return c
+ return None
+ else:
+ # No children locations; this is a match!
+ return elem
+ else:
+ # Ok, predicates or name didn't match, so we need to start
+ # down each child and treat it as the root and try
+ # again
+ for c in elem.elements():
+ if self.matches(c):
+ return c
+ # No children matched...
+ return None
+
+ def matches(self, elem):
+ if self.isRootMatch(elem):
+ return True
+ else:
+ # Ok, initial element isn't an exact match, walk
+ # down each child and treat it as the root and try
+ # again
+ for c in elem.elements():
+ if self.matches(c):
+ return True
+ # No children matched...
+ return False
+
+ def queryForString(self, elem, resultbuf):
+ raise NotImplementedError(
+ "queryForString is not implemented for any location")
+
+ def queryForNodes(self, elem, resultlist):
+ # First check to see if _this_ element is a root
+ if self.isRootMatch(elem):
+ resultlist.append(elem)
+
+ # Now check each child
+ for c in elem.elements():
+ self.queryForNodes(c, resultlist)
+
+
+ def queryForStringList(self, elem, resultlist):
+ if self.isRootMatch(elem):
+ for c in elem.children:
+ if isinstance(c, (str, unicode)):
+ resultlist.append(c)
+ for c in elem.elements():
+ self.queryForStringList(c, resultlist)
+
+
+class XPathQuery:
+ def __init__(self, queryStr):
+ self.queryStr = queryStr
+ from twisted.words.xish.xpathparser import parse
+ self.baseLocation = parse('XPATH', queryStr)
+
+ def __hash__(self):
+ return self.queryStr.__hash__()
+
+ def matches(self, elem):
+ return self.baseLocation.matches(elem)
+
+ def queryForString(self, elem):
+ result = StringIO.StringIO()
+ self.baseLocation.queryForString(elem, result)
+ return result.getvalue()
+
+ def queryForNodes(self, elem):
+ result = []
+ self.baseLocation.queryForNodes(elem, result)
+ if len(result) == 0:
+ return None
+ else:
+ return result
+
+ def queryForStringList(self, elem):
+ result = []
+ self.baseLocation.queryForStringList(elem, result)
+ if len(result) == 0:
+ return None
+ else:
+ return result
+
+
+__internedQueries = {}
+
+def internQuery(queryString):
+ if queryString not in __internedQueries:
+ __internedQueries[queryString] = XPathQuery(queryString)
+ return __internedQueries[queryString]
+
+
+def matches(xpathstr, elem):
+ return internQuery(xpathstr).matches(elem)
+
+
+def queryForStringList(xpathstr, elem):
+ return internQuery(xpathstr).queryForStringList(elem)
+
+
+def queryForString(xpathstr, elem):
+ return internQuery(xpathstr).queryForString(elem)
+
+
+def queryForNodes(xpathstr, elem):
+ return internQuery(xpathstr).queryForNodes(elem)
diff --git a/twisted/words/xish/xpathparser.g b/twisted/words/xish/xpathparser.g
new file mode 100644
index 0000000..02c67c9
--- /dev/null
+++ b/twisted/words/xish/xpathparser.g
@@ -0,0 +1,375 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# DO NOT EDIT xpathparser.py!
+#
+# It is generated from xpathparser.g using Yapps. Make needed changes there.
+# This also means that the generated Python may not conform to Twisted's coding
+# standards.
+
+# HOWTO Generate me:
+#
+# 1.) Grab a copy of yapps2, version 2.1.1:
+# http://theory.stanford.edu/~amitp/Yapps/
+#
+# Note: Do NOT use the package in debian/ubuntu as it has incompatible
+# modifications.
+#
+# 2.) Generate the grammar:
+#
+# yapps2 xpathparser.g xpathparser.py.proto
+#
+# 3.) Edit the output to depend on the embedded runtime, not yappsrt.
+#
+# sed -e '/^import yapps/d' -e '/^[^#]/s/yappsrt\.//g' \
+# xpathparser.py.proto > xpathparser.py
+
+"""
+XPath Parser.
+
+Besides the parser code produced by Yapps, this module also defines the
+parse-time exception classes, a scanner class, a base class for parsers
+produced by Yapps, and a context class that keeps track of the parse stack.
+These have been copied from the Yapps runtime.
+"""
+
+import sys, re
+
+class SyntaxError(Exception):
+ """When we run into an unexpected token, this is the exception to use"""
+ def __init__(self, charpos=-1, msg="Bad Token", context=None):
+ Exception.__init__(self)
+ self.charpos = charpos
+ self.msg = msg
+ self.context = context
+
+ def __str__(self):
+ if self.charpos < 0: return 'SyntaxError'
+ else: return 'SyntaxError@char%s(%s)' % (repr(self.charpos), self.msg)
+
+class NoMoreTokens(Exception):
+ """Another exception object, for when we run out of tokens"""
+ pass
+
+class Scanner:
+ """Yapps scanner.
+
+ The Yapps scanner can work in context sensitive or context
+ insensitive modes. The token(i) method is used to retrieve the
+ i-th token. It takes a restrict set that limits the set of tokens
+ it is allowed to return. In context sensitive mode, this restrict
+ set guides the scanner. In context insensitive mode, there is no
+ restriction (the set is always the full set of tokens).
+
+ """
+
+ def __init__(self, patterns, ignore, input):
+ """Initialize the scanner.
+
+ @param patterns: [(terminal, uncompiled regex), ...] or C{None}
+ @param ignore: [terminal,...]
+ @param input: string
+
+ If patterns is C{None}, we assume that the subclass has defined
+ C{self.patterns} : [(terminal, compiled regex), ...]. Note that the
+ patterns parameter expects uncompiled regexes, whereas the
+ C{self.patterns} field expects compiled regexes.
+ """
+ self.tokens = [] # [(begin char pos, end char pos, token name, matched text), ...]
+ self.restrictions = []
+ self.input = input
+ self.pos = 0
+ self.ignore = ignore
+ self.first_line_number = 1
+
+ if patterns is not None:
+ # Compile the regex strings into regex objects
+ self.patterns = []
+ for terminal, regex in patterns:
+ self.patterns.append( (terminal, re.compile(regex)) )
+
+ def get_token_pos(self):
+ """Get the current token position in the input text."""
+ return len(self.tokens)
+
+ def get_char_pos(self):
+ """Get the current char position in the input text."""
+ return self.pos
+
+ def get_prev_char_pos(self, i=None):
+ """Get the previous position (one token back) in the input text."""
+ if self.pos == 0: return 0
+ if i is None: i = -1
+ return self.tokens[i][0]
+
+ def get_line_number(self):
+ """Get the line number of the current position in the input text."""
+ # TODO: make this work at any token/char position
+ return self.first_line_number + self.get_input_scanned().count('\n')
+
+ def get_column_number(self):
+ """Get the column number of the current position in the input text."""
+ s = self.get_input_scanned()
+ i = s.rfind('\n') # may be -1, but that's okay in this case
+ return len(s) - (i+1)
+
+ def get_input_scanned(self):
+ """Get the portion of the input that has been tokenized."""
+ return self.input[:self.pos]
+
+ def get_input_unscanned(self):
+ """Get the portion of the input that has not yet been tokenized."""
+ return self.input[self.pos:]
+
+ def token(self, i, restrict=None):
+ """Get the i'th token in the input.
+
+ If C{i} is one past the end, then scan for another token.
+
+ @param i: token index
+
+ @param restrict: [token, ...] or C{None}; if restrict is
+ C{None}, then any token is allowed. You may call
+ token(i) more than once. However, the restrict set
+ may never be larger than what was passed in on the
+ first call to token(i).
+ """
+ if i == len(self.tokens):
+ self.scan(restrict)
+ if i < len(self.tokens):
+ # Make sure the restriction is more restricted. This
+ # invariant is needed to avoid ruining tokenization at
+ # position i+1 and higher.
+ if restrict and self.restrictions[i]:
+ for r in restrict:
+ if r not in self.restrictions[i]:
+ raise NotImplementedError("Unimplemented: restriction set changed")
+ return self.tokens[i]
+ raise NoMoreTokens()
+
+ def __repr__(self):
+ """Print the last 10 tokens that have been scanned in"""
+ output = ''
+ for t in self.tokens[-10:]:
+ output = '%s\n (@%s) %s = %s' % (output,t[0],t[2],repr(t[3]))
+ return output
+
+ def scan(self, restrict):
+ """Should scan another token and add it to the list, self.tokens,
+ and add the restriction to self.restrictions"""
+ # Keep looking for a token, ignoring any in self.ignore
+ while 1:
+ # Search the patterns for the longest match, with earlier
+ # tokens in the list having preference
+ best_match = -1
+ best_pat = '(error)'
+ for p, regexp in self.patterns:
+ # First check to see if we're ignoring this token
+ if restrict and p not in restrict and p not in self.ignore:
+ continue
+ m = regexp.match(self.input, self.pos)
+ if m and len(m.group(0)) > best_match:
+ # We got a match that's better than the previous one
+ best_pat = p
+ best_match = len(m.group(0))
+
+ # If we didn't find anything, raise an error
+ if best_pat == '(error)' and best_match < 0:
+ msg = 'Bad Token'
+ if restrict:
+ msg = 'Trying to find one of '+', '.join(restrict)
+ raise SyntaxError(self.pos, msg)
+
+ # If we found something that isn't to be ignored, return it
+ if best_pat not in self.ignore:
+ # Create a token with this data
+ token = (self.pos, self.pos+best_match, best_pat,
+ self.input[self.pos:self.pos+best_match])
+ self.pos = self.pos + best_match
+ # Only add this token if it's not in the list
+ # (to prevent looping)
+ if not self.tokens or token != self.tokens[-1]:
+ self.tokens.append(token)
+ self.restrictions.append(restrict)
+ return
+ else:
+ # This token should be ignored ..
+ self.pos = self.pos + best_match
+
+class Parser:
+ """Base class for Yapps-generated parsers.
+
+ """
+
+ def __init__(self, scanner):
+ self._scanner = scanner
+ self._pos = 0
+
+ def _peek(self, *types):
+ """Returns the token type for lookahead; if there are any args
+ then the list of args is the set of token types to allow"""
+ tok = self._scanner.token(self._pos, types)
+ return tok[2]
+
+ def _scan(self, type):
+ """Returns the matched text, and moves to the next token"""
+ tok = self._scanner.token(self._pos, [type])
+ if tok[2] != type:
+ raise SyntaxError(tok[0], 'Trying to find '+type+' :'+ ' ,'.join(self._scanner.restrictions[self._pos]))
+ self._pos = 1 + self._pos
+ return tok[3]
+
+class Context:
+ """Class to represent the parser's call stack.
+
+ Every rule creates a Context that links to its parent rule. The
+ contexts can be used for debugging.
+
+ """
+
+ def __init__(self, parent, scanner, tokenpos, rule, args=()):
+ """Create a new context.
+
+ @param parent: Context object or C{None}
+ @param scanner: Scanner object
+ @param tokenpos: scanner token position
+ @type tokenpos: C{int}
+ @param rule: name of the rule
+ @type rule: C{str}
+ @param args: tuple listing parameters to the rule
+
+ """
+ self.parent = parent
+ self.scanner = scanner
+ self.tokenpos = tokenpos
+ self.rule = rule
+ self.args = args
+
+ def __str__(self):
+ output = ''
+ if self.parent: output = str(self.parent) + ' > '
+ output += self.rule
+ return output
+
+def print_line_with_pointer(text, p):
+ """Print the line of 'text' that includes position 'p',
+ along with a second line with a single caret (^) at position p"""
+
+ # TODO: separate out the logic for determining the line/character
+ # location from the logic for determining how to display an
+ # 80-column line to stderr.
+
+ # Now try printing part of the line
+ text = text[max(p-80, 0):p+80]
+ p = p - max(p-80, 0)
+
+ # Strip to the left
+ i = text[:p].rfind('\n')
+ j = text[:p].rfind('\r')
+ if i < 0 or (0 <= j < i): i = j
+ if 0 <= i < p:
+ p = p - i - 1
+ text = text[i+1:]
+
+ # Strip to the right
+ i = text.find('\n', p)
+ j = text.find('\r', p)
+ if i < 0 or (0 <= j < i): i = j
+ if i >= 0:
+ text = text[:i]
+
+ # Now shorten the text
+ while len(text) > 70 and p > 60:
+ # Cut off 10 chars
+ text = "..." + text[10:]
+ p = p - 7
+
+ # Now print the string, along with an indicator
+ print >>sys.stderr, '> ',text
+ print >>sys.stderr, '> ',' '*p + '^'
+
+def print_error(input, err, scanner):
+ """Print error messages, the parser stack, and the input text -- for human-readable error messages."""
+ # NOTE: this function assumes 80 columns :-(
+ # Figure out the line number
+ line_number = scanner.get_line_number()
+ column_number = scanner.get_column_number()
+ print >>sys.stderr, '%d:%d: %s' % (line_number, column_number, err.msg)
+
+ context = err.context
+ if not context:
+ print_line_with_pointer(input, err.charpos)
+
+ while context:
+ # TODO: add line number
+ print >>sys.stderr, 'while parsing %s%s:' % (context.rule, tuple(context.args))
+ print_line_with_pointer(input, context.scanner.get_prev_char_pos(context.tokenpos))
+ context = context.parent
+
+def wrap_error_reporter(parser, rule):
+ try:
+ return getattr(parser, rule)()
+ except SyntaxError, e:
+ input = parser._scanner.input
+ print_error(input, e, parser._scanner)
+ except NoMoreTokens:
+ print >>sys.stderr, 'Could not complete parsing; stopped around here:'
+ print >>sys.stderr, parser._scanner
+
+
+from twisted.words.xish.xpath import AttribValue, BooleanValue, CompareValue
+from twisted.words.xish.xpath import Function, IndexValue, LiteralValue
+from twisted.words.xish.xpath import _AnyLocation, _Location
+
+%%
+parser XPathParser:
+ ignore: "\\s+"
+ token INDEX: "[0-9]+"
+ token WILDCARD: "\*"
+ token IDENTIFIER: "[a-zA-Z][a-zA-Z0-9_\-]*"
+ token ATTRIBUTE: "\@[a-zA-Z][a-zA-Z0-9_\-]*"
+ token FUNCNAME: "[a-zA-Z][a-zA-Z0-9_]*"
+ token CMP_EQ: "\="
+ token CMP_NE: "\!\="
+ token STR_DQ: '"([^"]|(\\"))*?"'
+ token STR_SQ: "'([^']|(\\'))*?'"
+ token OP_AND: "and"
+ token OP_OR: "or"
+ token END: "$"
+
+ rule XPATH: PATH {{ result = PATH; current = result }}
+ ( PATH {{ current.childLocation = PATH; current = current.childLocation }} ) * END
+ {{ return result }}
+
+ rule PATH: ("/" {{ result = _Location() }} | "//" {{ result = _AnyLocation() }} )
+ ( IDENTIFIER {{ result.elementName = IDENTIFIER }} | WILDCARD {{ result.elementName = None }} )
+ ( "\[" PREDICATE {{ result.predicates.append(PREDICATE) }} "\]")*
+ {{ return result }}
+
+ rule PREDICATE: EXPR {{ return EXPR }} |
+ INDEX {{ return IndexValue(INDEX) }}
+
+ rule EXPR: FACTOR {{ e = FACTOR }}
+ ( BOOLOP FACTOR {{ e = BooleanValue(e, BOOLOP, FACTOR) }} )*
+ {{ return e }}
+
+ rule BOOLOP: ( OP_AND {{ return OP_AND }} | OP_OR {{ return OP_OR }} )
+
+ rule FACTOR: TERM {{ return TERM }}
+ | "\(" EXPR "\)" {{ return EXPR }}
+
+ rule TERM: VALUE {{ t = VALUE }}
+ [ CMP VALUE {{ t = CompareValue(t, CMP, VALUE) }} ]
+ {{ return t }}
+
+ rule VALUE: "@" IDENTIFIER {{ return AttribValue(IDENTIFIER) }} |
+ FUNCNAME {{ f = Function(FUNCNAME); args = [] }}
+ "\(" [ VALUE {{ args.append(VALUE) }}
+ (
+ "," VALUE {{ args.append(VALUE) }}
+ )*
+ ] "\)" {{ f.setParams(*args); return f }} |
+ STR {{ return LiteralValue(STR[1:len(STR)-1]) }}
+
+ rule CMP: (CMP_EQ {{ return CMP_EQ }} | CMP_NE {{ return CMP_NE }})
+ rule STR: (STR_DQ {{ return STR_DQ }} | STR_SQ {{ return STR_SQ }})
diff --git a/twisted/words/xish/xpathparser.py b/twisted/words/xish/xpathparser.py
new file mode 100644
index 0000000..312f6ec
--- /dev/null
+++ b/twisted/words/xish/xpathparser.py
@@ -0,0 +1,508 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+# DO NOT EDIT xpathparser.py!
+#
+# It is generated from xpathparser.g using Yapps. Make needed changes there.
+# This also means that the generated Python may not conform to Twisted's coding
+# standards.
+
+# HOWTO Generate me:
+#
+# 1.) Grab a copy of yapps2, version 2.1.1:
+# http://theory.stanford.edu/~amitp/Yapps/
+#
+# Note: Do NOT use the package in debian/ubuntu as it has incompatible
+# modifications.
+#
+# 2.) Generate the grammar:
+#
+# yapps2 xpathparser.g xpathparser.py.proto
+#
+# 3.) Edit the output to depend on the embedded runtime, not yappsrt.
+#
+# sed -e '/^import yapps/d' -e '/^[^#]/s/yappsrt\.//g' \
+# xpathparser.py.proto > xpathparser.py
+
+"""
+XPath Parser.
+
+Besides the parser code produced by Yapps, this module also defines the
+parse-time exception classes, a scanner class, a base class for parsers
+produced by Yapps, and a context class that keeps track of the parse stack.
+These have been copied from the Yapps runtime.
+"""
+
+import sys, re
+
+class SyntaxError(Exception):
+ """When we run into an unexpected token, this is the exception to use"""
+ def __init__(self, charpos=-1, msg="Bad Token", context=None):
+ Exception.__init__(self)
+ self.charpos = charpos
+ self.msg = msg
+ self.context = context
+
+ def __str__(self):
+ if self.charpos < 0: return 'SyntaxError'
+ else: return 'SyntaxError@char%s(%s)' % (repr(self.charpos), self.msg)
+
+class NoMoreTokens(Exception):
+ """Another exception object, for when we run out of tokens"""
+ pass
+
+class Scanner:
+ """Yapps scanner.
+
+ The Yapps scanner can work in context sensitive or context
+ insensitive modes. The token(i) method is used to retrieve the
+ i-th token. It takes a restrict set that limits the set of tokens
+ it is allowed to return. In context sensitive mode, this restrict
+ set guides the scanner. In context insensitive mode, there is no
+ restriction (the set is always the full set of tokens).
+
+ """
+
+ def __init__(self, patterns, ignore, input):
+ """Initialize the scanner.
+
+ @param patterns: [(terminal, uncompiled regex), ...] or C{None}
+ @param ignore: [terminal,...]
+ @param input: string
+
+ If patterns is C{None}, we assume that the subclass has defined
+ C{self.patterns} : [(terminal, compiled regex), ...]. Note that the
+ patterns parameter expects uncompiled regexes, whereas the
+ C{self.patterns} field expects compiled regexes.
+ """
+ self.tokens = [] # [(begin char pos, end char pos, token name, matched text), ...]
+ self.restrictions = []
+ self.input = input
+ self.pos = 0
+ self.ignore = ignore
+ self.first_line_number = 1
+
+ if patterns is not None:
+ # Compile the regex strings into regex objects
+ self.patterns = []
+ for terminal, regex in patterns:
+ self.patterns.append( (terminal, re.compile(regex)) )
+
+ def get_token_pos(self):
+ """Get the current token position in the input text."""
+ return len(self.tokens)
+
+ def get_char_pos(self):
+ """Get the current char position in the input text."""
+ return self.pos
+
+ def get_prev_char_pos(self, i=None):
+ """Get the previous position (one token back) in the input text."""
+ if self.pos == 0: return 0
+ if i is None: i = -1
+ return self.tokens[i][0]
+
+ def get_line_number(self):
+ """Get the line number of the current position in the input text."""
+ # TODO: make this work at any token/char position
+ return self.first_line_number + self.get_input_scanned().count('\n')
+
+ def get_column_number(self):
+ """Get the column number of the current position in the input text."""
+ s = self.get_input_scanned()
+ i = s.rfind('\n') # may be -1, but that's okay in this case
+ return len(s) - (i+1)
+
+ def get_input_scanned(self):
+ """Get the portion of the input that has been tokenized."""
+ return self.input[:self.pos]
+
+ def get_input_unscanned(self):
+ """Get the portion of the input that has not yet been tokenized."""
+ return self.input[self.pos:]
+
+ def token(self, i, restrict=None):
+ """Get the i'th token in the input.
+
+ If C{i} is one past the end, then scan for another token.
+
+ @param i: token index
+
+ @param restrict: [token, ...] or C{None}; if restrict is
+ C{None}, then any token is allowed. You may call
+ token(i) more than once. However, the restrict set
+ may never be larger than what was passed in on the
+ first call to token(i).
+ """
+ if i == len(self.tokens):
+ self.scan(restrict)
+ if i < len(self.tokens):
+ # Make sure the restriction is more restricted. This
+ # invariant is needed to avoid ruining tokenization at
+ # position i+1 and higher.
+ if restrict and self.restrictions[i]:
+ for r in restrict:
+ if r not in self.restrictions[i]:
+ raise NotImplementedError("Unimplemented: restriction set changed")
+ return self.tokens[i]
+ raise NoMoreTokens()
+
+ def __repr__(self):
+ """Print the last 10 tokens that have been scanned in"""
+ output = ''
+ for t in self.tokens[-10:]:
+ output = '%s\n (@%s) %s = %s' % (output,t[0],t[2],repr(t[3]))
+ return output
+
+ def scan(self, restrict):
+ """Should scan another token and add it to the list, self.tokens,
+ and add the restriction to self.restrictions"""
+ # Keep looking for a token, ignoring any in self.ignore
+ while 1:
+ # Search the patterns for the longest match, with earlier
+ # tokens in the list having preference
+ best_match = -1
+ best_pat = '(error)'
+ for p, regexp in self.patterns:
+ # First check to see if we're ignoring this token
+ if restrict and p not in restrict and p not in self.ignore:
+ continue
+ m = regexp.match(self.input, self.pos)
+ if m and len(m.group(0)) > best_match:
+ # We got a match that's better than the previous one
+ best_pat = p
+ best_match = len(m.group(0))
+
+ # If we didn't find anything, raise an error
+ if best_pat == '(error)' and best_match < 0:
+ msg = 'Bad Token'
+ if restrict:
+ msg = 'Trying to find one of '+', '.join(restrict)
+ raise SyntaxError(self.pos, msg)
+
+ # If we found something that isn't to be ignored, return it
+ if best_pat not in self.ignore:
+ # Create a token with this data
+ token = (self.pos, self.pos+best_match, best_pat,
+ self.input[self.pos:self.pos+best_match])
+ self.pos = self.pos + best_match
+ # Only add this token if it's not in the list
+ # (to prevent looping)
+ if not self.tokens or token != self.tokens[-1]:
+ self.tokens.append(token)
+ self.restrictions.append(restrict)
+ return
+ else:
+ # This token should be ignored ..
+ self.pos = self.pos + best_match
+
+class Parser:
+ """Base class for Yapps-generated parsers.
+
+ """
+
+ def __init__(self, scanner):
+ self._scanner = scanner
+ self._pos = 0
+
+ def _peek(self, *types):
+ """Returns the token type for lookahead; if there are any args
+ then the list of args is the set of token types to allow"""
+ tok = self._scanner.token(self._pos, types)
+ return tok[2]
+
+ def _scan(self, type):
+ """Returns the matched text, and moves to the next token"""
+ tok = self._scanner.token(self._pos, [type])
+ if tok[2] != type:
+ raise SyntaxError(tok[0], 'Trying to find '+type+' :'+ ' ,'.join(self._scanner.restrictions[self._pos]))
+ self._pos = 1 + self._pos
+ return tok[3]
+
+class Context:
+ """Class to represent the parser's call stack.
+
+ Every rule creates a Context that links to its parent rule. The
+ contexts can be used for debugging.
+
+ """
+
+ def __init__(self, parent, scanner, tokenpos, rule, args=()):
+ """Create a new context.
+
+ @param parent: Context object or C{None}
+ @param scanner: Scanner object
+ @param tokenpos: scanner token position
+ @type tokenpos: C{int}
+ @param rule: name of the rule
+ @type rule: C{str}
+ @param args: tuple listing parameters to the rule
+
+ """
+ self.parent = parent
+ self.scanner = scanner
+ self.tokenpos = tokenpos
+ self.rule = rule
+ self.args = args
+
+ def __str__(self):
+ output = ''
+ if self.parent: output = str(self.parent) + ' > '
+ output += self.rule
+ return output
+
+def print_line_with_pointer(text, p):
+ """Print the line of 'text' that includes position 'p',
+ along with a second line with a single caret (^) at position p"""
+
+ # TODO: separate out the logic for determining the line/character
+ # location from the logic for determining how to display an
+ # 80-column line to stderr.
+
+ # Now try printing part of the line
+ text = text[max(p-80, 0):p+80]
+ p = p - max(p-80, 0)
+
+ # Strip to the left
+ i = text[:p].rfind('\n')
+ j = text[:p].rfind('\r')
+ if i < 0 or (0 <= j < i): i = j
+ if 0 <= i < p:
+ p = p - i - 1
+ text = text[i+1:]
+
+ # Strip to the right
+ i = text.find('\n', p)
+ j = text.find('\r', p)
+ if i < 0 or (0 <= j < i): i = j
+ if i >= 0:
+ text = text[:i]
+
+ # Now shorten the text
+ while len(text) > 70 and p > 60:
+ # Cut off 10 chars
+ text = "..." + text[10:]
+ p = p - 7
+
+ # Now print the string, along with an indicator
+ print >>sys.stderr, '> ',text
+ print >>sys.stderr, '> ',' '*p + '^'
+
+def print_error(input, err, scanner):
+ """Print error messages, the parser stack, and the input text -- for human-readable error messages."""
+ # NOTE: this function assumes 80 columns :-(
+ # Figure out the line number
+ line_number = scanner.get_line_number()
+ column_number = scanner.get_column_number()
+ print >>sys.stderr, '%d:%d: %s' % (line_number, column_number, err.msg)
+
+ context = err.context
+ if not context:
+ print_line_with_pointer(input, err.charpos)
+
+ while context:
+ # TODO: add line number
+ print >>sys.stderr, 'while parsing %s%s:' % (context.rule, tuple(context.args))
+ print_line_with_pointer(input, context.scanner.get_prev_char_pos(context.tokenpos))
+ context = context.parent
+
+def wrap_error_reporter(parser, rule):
+ try:
+ return getattr(parser, rule)()
+ except SyntaxError, e:
+ input = parser._scanner.input
+ print_error(input, e, parser._scanner)
+ except NoMoreTokens:
+ print >>sys.stderr, 'Could not complete parsing; stopped around here:'
+ print >>sys.stderr, parser._scanner
+
+
+from twisted.words.xish.xpath import AttribValue, BooleanValue, CompareValue
+from twisted.words.xish.xpath import Function, IndexValue, LiteralValue
+from twisted.words.xish.xpath import _AnyLocation, _Location
+
+
+# Begin -- grammar generated by Yapps
+import sys, re
+
+class XPathParserScanner(Scanner):
+ patterns = [
+ ('","', re.compile(',')),
+ ('"@"', re.compile('@')),
+ ('"\\)"', re.compile('\\)')),
+ ('"\\("', re.compile('\\(')),
+ ('"\\]"', re.compile('\\]')),
+ ('"\\["', re.compile('\\[')),
+ ('"//"', re.compile('//')),
+ ('"/"', re.compile('/')),
+ ('\\s+', re.compile('\\s+')),
+ ('INDEX', re.compile('[0-9]+')),
+ ('WILDCARD', re.compile('\\*')),
+ ('IDENTIFIER', re.compile('[a-zA-Z][a-zA-Z0-9_\\-]*')),
+ ('ATTRIBUTE', re.compile('\\@[a-zA-Z][a-zA-Z0-9_\\-]*')),
+ ('FUNCNAME', re.compile('[a-zA-Z][a-zA-Z0-9_]*')),
+ ('CMP_EQ', re.compile('\\=')),
+ ('CMP_NE', re.compile('\\!\\=')),
+ ('STR_DQ', re.compile('"([^"]|(\\"))*?"')),
+ ('STR_SQ', re.compile("'([^']|(\\'))*?'")),
+ ('OP_AND', re.compile('and')),
+ ('OP_OR', re.compile('or')),
+ ('END', re.compile('$')),
+ ]
+ def __init__(self, str):
+ Scanner.__init__(self,None,['\\s+'],str)
+
+class XPathParser(Parser):
+ Context = Context
+ def XPATH(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'XPATH', [])
+ PATH = self.PATH(_context)
+ result = PATH; current = result
+ while self._peek('END', '"/"', '"//"') != 'END':
+ PATH = self.PATH(_context)
+ current.childLocation = PATH; current = current.childLocation
+ if self._peek() not in ['END', '"/"', '"//"']:
+ raise SyntaxError(charpos=self._scanner.get_prev_char_pos(), context=_context, msg='Need one of ' + ', '.join(['END', '"/"', '"//"']))
+ END = self._scan('END')
+ return result
+
+ def PATH(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'PATH', [])
+ _token = self._peek('"/"', '"//"')
+ if _token == '"/"':
+ self._scan('"/"')
+ result = _Location()
+ else: # == '"//"'
+ self._scan('"//"')
+ result = _AnyLocation()
+ _token = self._peek('IDENTIFIER', 'WILDCARD')
+ if _token == 'IDENTIFIER':
+ IDENTIFIER = self._scan('IDENTIFIER')
+ result.elementName = IDENTIFIER
+ else: # == 'WILDCARD'
+ WILDCARD = self._scan('WILDCARD')
+ result.elementName = None
+ while self._peek('"\\["', 'END', '"/"', '"//"') == '"\\["':
+ self._scan('"\\["')
+ PREDICATE = self.PREDICATE(_context)
+ result.predicates.append(PREDICATE)
+ self._scan('"\\]"')
+ if self._peek() not in ['"\\["', 'END', '"/"', '"//"']:
+ raise SyntaxError(charpos=self._scanner.get_prev_char_pos(), context=_context, msg='Need one of ' + ', '.join(['"\\["', 'END', '"/"', '"//"']))
+ return result
+
+ def PREDICATE(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'PREDICATE', [])
+ _token = self._peek('INDEX', '"\\("', '"@"', 'FUNCNAME', 'STR_DQ', 'STR_SQ')
+ if _token != 'INDEX':
+ EXPR = self.EXPR(_context)
+ return EXPR
+ else: # == 'INDEX'
+ INDEX = self._scan('INDEX')
+ return IndexValue(INDEX)
+
+ def EXPR(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'EXPR', [])
+ FACTOR = self.FACTOR(_context)
+ e = FACTOR
+ while self._peek('OP_AND', 'OP_OR', '"\\)"', '"\\]"') in ['OP_AND', 'OP_OR']:
+ BOOLOP = self.BOOLOP(_context)
+ FACTOR = self.FACTOR(_context)
+ e = BooleanValue(e, BOOLOP, FACTOR)
+ if self._peek() not in ['OP_AND', 'OP_OR', '"\\)"', '"\\]"']:
+ raise SyntaxError(charpos=self._scanner.get_prev_char_pos(), context=_context, msg='Need one of ' + ', '.join(['OP_AND', 'OP_OR', '"\\)"', '"\\]"']))
+ return e
+
+ def BOOLOP(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'BOOLOP', [])
+ _token = self._peek('OP_AND', 'OP_OR')
+ if _token == 'OP_AND':
+ OP_AND = self._scan('OP_AND')
+ return OP_AND
+ else: # == 'OP_OR'
+ OP_OR = self._scan('OP_OR')
+ return OP_OR
+
+ def FACTOR(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'FACTOR', [])
+ _token = self._peek('"\\("', '"@"', 'FUNCNAME', 'STR_DQ', 'STR_SQ')
+ if _token != '"\\("':
+ TERM = self.TERM(_context)
+ return TERM
+ else: # == '"\\("'
+ self._scan('"\\("')
+ EXPR = self.EXPR(_context)
+ self._scan('"\\)"')
+ return EXPR
+
+ def TERM(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'TERM', [])
+ VALUE = self.VALUE(_context)
+ t = VALUE
+ if self._peek('CMP_EQ', 'CMP_NE', 'OP_AND', 'OP_OR', '"\\)"', '"\\]"') in ['CMP_EQ', 'CMP_NE']:
+ CMP = self.CMP(_context)
+ VALUE = self.VALUE(_context)
+ t = CompareValue(t, CMP, VALUE)
+ return t
+
+ def VALUE(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'VALUE', [])
+ _token = self._peek('"@"', 'FUNCNAME', 'STR_DQ', 'STR_SQ')
+ if _token == '"@"':
+ self._scan('"@"')
+ IDENTIFIER = self._scan('IDENTIFIER')
+ return AttribValue(IDENTIFIER)
+ elif _token == 'FUNCNAME':
+ FUNCNAME = self._scan('FUNCNAME')
+ f = Function(FUNCNAME); args = []
+ self._scan('"\\("')
+ if self._peek('"\\)"', '"@"', 'FUNCNAME', '","', 'STR_DQ', 'STR_SQ') not in ['"\\)"', '","']:
+ VALUE = self.VALUE(_context)
+ args.append(VALUE)
+ while self._peek('","', '"\\)"') == '","':
+ self._scan('","')
+ VALUE = self.VALUE(_context)
+ args.append(VALUE)
+ if self._peek() not in ['","', '"\\)"']:
+ raise SyntaxError(charpos=self._scanner.get_prev_char_pos(), context=_context, msg='Need one of ' + ', '.join(['","', '"\\)"']))
+ self._scan('"\\)"')
+ f.setParams(*args); return f
+ else: # in ['STR_DQ', 'STR_SQ']
+ STR = self.STR(_context)
+ return LiteralValue(STR[1:len(STR)-1])
+
+ def CMP(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'CMP', [])
+ _token = self._peek('CMP_EQ', 'CMP_NE')
+ if _token == 'CMP_EQ':
+ CMP_EQ = self._scan('CMP_EQ')
+ return CMP_EQ
+ else: # == 'CMP_NE'
+ CMP_NE = self._scan('CMP_NE')
+ return CMP_NE
+
+ def STR(self, _parent=None):
+ _context = self.Context(_parent, self._scanner, self._pos, 'STR', [])
+ _token = self._peek('STR_DQ', 'STR_SQ')
+ if _token == 'STR_DQ':
+ STR_DQ = self._scan('STR_DQ')
+ return STR_DQ
+ else: # == 'STR_SQ'
+ STR_SQ = self._scan('STR_SQ')
+ return STR_SQ
+
+
+def parse(rule, text):
+ P = XPathParser(XPathParserScanner(text))
+ return wrap_error_reporter(P, rule)
+
+if __name__ == '__main__':
+ from sys import argv, stdin
+ if len(argv) >= 2:
+ if len(argv) >= 3:
+ f = open(argv[2],'r')
+ else:
+ f = stdin
+ print parse(argv[1], f.read())
+ else: print >>sys.stderr, 'Args: <rule> [<filename>]'
+# End -- grammar generated by Yapps
diff --git a/twisted/words/xmpproutertap.py b/twisted/words/xmpproutertap.py
new file mode 100644
index 0000000..0bdae0a
--- /dev/null
+++ b/twisted/words/xmpproutertap.py
@@ -0,0 +1,30 @@
+# -*- test-case-name: twisted.words.test.test_xmpproutertap -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+from twisted.application import strports
+from twisted.python import usage
+from twisted.words.protocols.jabber import component
+
+class Options(usage.Options):
+ optParameters = [
+ ('port', None, 'tcp:5347:interface=127.0.0.1',
+ 'Port components connect to'),
+ ('secret', None, 'secret', 'Router secret'),
+ ]
+
+ optFlags = [
+ ('verbose', 'v', 'Log traffic'),
+ ]
+
+
+
+def makeService(config):
+ router = component.Router()
+ factory = component.XMPPComponentServerFactory(router, config['secret'])
+
+ if config['verbose']:
+ factory.logTraffic = True
+
+ return strports.service(config['port'], factory)